20200528のvue.jsに関する記事は18件です。

爆速でwh.imのゲームを実装する(NGワードゲーム編)

はじめに

こんにちは。普段は情報系の学科で大学生をしている者です。

この記事に紹介されている、wh.im(ウィム)というサービスの立ち上げに関わっているのですが、その一環でwh.im上で楽しめるゲームを開発しました。

このサービスの特徴として、誰でもゲームを投稿 できます!そのやり方を知っていただきたく、前回に引き続き記事を書きますので、興味を持った方はぜひゲーム開発を試してみてください!

前回のじゃんけんに引き続き、より複雑なゲームを実装していきます。

今回は実際にwh.im上で遊ぶことのできる「NGワードゲーム」を例に挙げて、実装方法を説明いたします。このゲームで出てくる、phaseやグローバルで使える関数などは、前回出てこなかったテクニックとなります。

まずは開発環境で実際に動かしてみる

実際にNGワードゲームを開発環境で動かしてみたいと思います。

まず

$ cd ダウンロードしたいディレクトリ
$ git clone https://github.com/whimRTC/whim-ng_word.git

とします。そして

$ cd whim-ng_word
$ yarn # or npm install
$ yarn serve # or npm run serve

とします。yarnまたはnpmがインストールされていない場合はインストールしてください。
するとlocalhost:3001にゲームが起動します。

そして、wh.imから「遊び場」へ入室し、そのアドレスの末尾に&develop=trueをつけます。するとwh.imが開発者用のモードとなります。右上のメニューの「アプリを選ぶ」から「開発用(port:3001)」を選ぶことにより、自分の手元でゲームを試すことができます。

起動画面.png

このように表示されれば成功です!

実際のコードを見てみる

続いて、実際のコードを使いながら通信方法を説明していきます。

App

まず表示される画面が書かれているsrc/App.vueをご覧ください。

src/App.vue
<template>
  <div id="app">
    <Main class="main" />

    <Player
      v-for="user in $whim.users"
      :key="user.id"
      class="box"
      :class="`pos${user.positionNumber}`"
      :displayUser="user"
    />
  </div>
</template>

<script>
export default {
  name: "App",
  components: {
    Main: () => import("@/components/main/Index"),
    Player: () => import("@/components/player/Index")
  }
};
</script>
<!-- 以下略 -->

このゲームではMain画面とPlayer画面に分かれて実装されています。そのため、App.vue内でMainPlayerの2つのコンポーネントを呼び出しています。
Mainは画面中央部の画面を、Playerはそれぞれのユーザーのいる場所に表示される画面を表します。
MainPlayerの実装はそれぞれ、 src/components/main/Index.vuesrc/components/player/Index.vueに実装があります。

wh.imを経由した通信の方法

前回の復習です。
App.vue$whim.usersという呼び出しがあります(this.$whim.usersの省略形です)が、これはwhim-client-vueというパッケージに入っています。このようにすることで、this.$whimから始まる関数だけで、利用者間の非同期通信まわりは全てできるようになっています。

ここに扱える関数一覧を示します。scriptタグ内では適宜thisを先頭に付けてください。

状態取得(呼び出すたびに通信する)

コード 説明
$whim.users [User] ルームに入っているユーザー一覧
$whim.room Room Room Object
$whim.accessUser User 現在アクセスしているUser
$whim.state State ゲームの状態(自由に設計可能)

状態変更

コード 引数 説明
$whim.assignState(Object) Object ゲーム情報を追記更新、
存在しないキーの場合:追記
存在するキーの場合:更新
$whim.replaceState(Object) Object ゲーム情報を渡されたObjectにすべて変える
$whim.deleteState ゲーム情報を空にする

これでは分かりにくいと思うので、後ほどのコードで使っている部分を見ながら、理解していただけると助かります。
より詳細な説明は、開発者ドキュメントをご覧ください。

このゲームのデータ構造

stateはゲームに合わせて自由に設計することができます。
今回のゲームでは、次のような設計です。

state
├── phase   // ゲームのフェーズ: "shuffling" | "playing" | "answer" 
└── ngWords // NGワード: {ユーザーID: そのユーザーのNGワード}

phaseを用いることで、ゲームの状態を整理しながらコードを書くことができます。

これらの変数は開発環境ではどんな時でも見ることができます。右上のメニューから「SHOW APP STATE」を選択すると

image.png

image.png

白い部分には私のuserIdが表示されています。

このように、現在のphaseや自分のNGワードを確認することができます(ngWordsは畳み込まれて表示されるので一度クリックして展開してください)。このようにして、カンニングすることができます(友達とやるときはやめましょう)。

Vueのグローバル変数

wh.imで通信したいときに、別のコンポーネント内で処理を共通化したいことがあると思います。そういったときには、次のように、main.jsに追記します。

src/main.js
const NG_WORD_PATTERNS = require("@/assets/ng_word_patterns.json");

Vue.prototype.$gameStart = () => {
  const shuffledPattern = shuffle(
    NG_WORD_PATTERNS[Math.floor(Math.random() * NG_WORD_PATTERNS.length)]
  );
  let ngWords = {};
  Vue.prototype.$whim.users.forEach((user, i) => {
    ngWords[user.id] = shuffledPattern[i];
  });
  Vue.prototype.$whim.assignState({
    phase: "shuffling",
    ngWords: ngWords
  });
};

Vue.jsではvueファイル内でthis.$hogeで表されるグローバル関数は、Vue.prototypeに定義されています(whim-client-vueの実装もそのようになっています)。だから、上のようにVue.prototype.$gameStartに関数を代入しておけば、vueファイル内でthis.$gameStartのように呼び出すことができます(Vueには様々な$から始まるメソッドがあるのでゲームで使う関数だとわかるように$gameから始まる関数名にしました)。

また、Vue.prototype.$whimでwhim-client-vueの関数を呼び出せるようになっています(この関数定義より前にVue.use(whimClientVue, { store });が必要になります)。

この$gameStartという関数には、ゲームの開始時の処理を定義しています。具体的にはランダムにお題をstate.ngWordsに格納して、state.phase'shuffling'に切り替えています。

Player

次にPlayer画面のコードについて説明していきます。Player画面は各プレイヤーの上に表示されます。ここにはゲームの状態に応じて、NGワードを表示するかどうかを変えています。

src/components/player/Index.vue
<template>
  <div class="container">
    <div v-if="status === 'hidden'" class="card hidden">
      <span class="text--subtitle"> NGワード</span>
    </div>
    <div v-else-if="status === 'shuffling'" class="card">
      <img :src="require('@/assets/shuffling.gif')" class="shuffling" />
    </div>
    <div v-else-if="status === 'visible'" class="card">
      <span class="text--subtitle">{{ appState.ngWords[displayUser.id] }}</span>
    </div>
  </div>
</template>
<script>
export default {
  name: "Player",
  props: {
    displayUser: Object // 表示されているUserの情報
  },
  computed: {
    phase() {
      return this.$whim.state.phase;
    },
    isMe() {
      return this.displayUser.id === this.$whim.accessUser.id;
    },
    appState() {
      return this.$whim.state;
    },
    status() {
      if (this.phase === "shuffling") {
        return "shuffling";
      }
      if ((this.phase === "playing" && !this.isMe) || this.phase === "answer") {
        return "visible";
      }
      return "hidden";
    }
  }
};
</script>
<!-- 以下略 -->

computedstatusで何を画面に表示すべきかを決めています。status'hidden'の場合にはNGワードが隠れている状態が表示され、status'shuffling'の場合にはシャッフルの演出がされ、status'visible'の場合には答えが表示されます。

Main

Main画面は画面中央部に表示されます。state.phaseによって表示するコンポーネントを切り替えています。

src/components/main/Index.vue
<template>
  <div>
    <Shuffling v-if="phase === 'shuffling'" />
    <Playing v-else-if="phase === 'playing'" />
    <Answer v-else-if="phase === 'answer'" />
    <GenreSelection v-else />
  </div>
</template>
<script>
export default {
  name: "Main",
  components: {
    GenreSelection: () => import("@/components/main/GenreSelection"),
    Shuffling: () => import("@/components/main/Shuffling"),
    Playing: () => import("@/components/main/Playing"),
    Answer: () => import("@/components/main/Answer")
  },
  computed: {
    phase() {
      return this.$whim.state.phase;
    }
  }
};
</script>
<style lang="scss" scoped></style>

GenreSelection

ここではゲームのスタート画面を定義しています。ジャンル選択は未実装です。クリックすることでstart関数が呼ばれます。start関数の内部で先程定義した$gameStart関数が呼ばれます。

src/components/main/GenreSelection.vue
<template>
  <div>
    <a class="fuwatto_btn_yellow" @click="start">スタート</a>
  </div>
</template>

<script>
export default {
  name: "GenreSelection",
  props: {
    msg: String
  },
  data() {
    return {
      genre: "random"
    };
  },
  methods: {
    start() {
      this.$gameStart();
    }
  }
};
</script>
<!-- 以下略 -->

Shuffling

ここではお題をシャッフルしている画面を定義しています。mounted関数は、このコンポーネントが表示されたとき呼ばれ、2000ミリ秒後にstate.phase'playing'に切り替えています。

src/components/main/Shuffling.vue
<template>
  <div>
    <a class="fuwatto_btn_yellow">シャッフル中...</a>
  </div>
</template>

<script>
export default {
  name: "Shuffling",
  mounted() {
    setTimeout(() => {
      this.$whim.assignState({
        phase: "playing"
      });
    }, 2000);
  }
};
</script>
<!-- 以下略 -->

Playing

ここでは残り時間を中央に表示しています。NGワードやALLシャッフルをクリックするとgoAnswer関数が呼ばれ、state.phaseanswerに切り替えます。

src/components/main/Playing.vue
<template>
  <div>
    <div class="text--subtitle title">タイムリミットまで</div>
    <countdown
      :time="10 * 60 * 1000"
      @end="goAnswer"
      class="countdown"
      :transform="transform"
      ref="countdown"
    >
      <template slot-scope="props"
        >{{ props.minutes }}:{{ props.seconds }}</template
      >
    </countdown>
    <a class="fuwatto_btn yellow" @click="goAnswer">NGワード!</a>
    <a class="fuwatto_btn grey" @click="goAnswer">ALLシャッフル</a>
  </div>
</template>

<script>
export default {
  name: "Playing",

  methods: {
    goAnswer() {
      this.$refs.countdown.abort();
      this.$whim.assignState({
        phase: "answer"
      });
    },

    transform(props) {
      props.seconds = props.seconds.toString().padStart(2, "0");
      return props;
    }
  }
};
</script>
<!-- 以下略 -->

Answer

答えを表示するフェーズですが、中央にはシャッフルで始めに戻るようにしています。start関数で$gameStartが呼ばれます。

src/components/main/Answer.vue
<template>
  <div>
    <a class="fuwatto_btn_yellow" @click="start">シャッフル開始</a>
  </div>
</template>

<script>
export default {
  name: "Answer",
  methods: {
    start() {
      this.$gameStart();
    }
  }
};
</script>
<!-- 以下略 -->

最後に

いかがでしたでしょうか。自分でゲームを作れるような気がしてきましたか?
引き続き、ゲーム作りのTipsのようなものは投稿し続けたいと思いますので、よろしくお願いします!

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

【BootstrapVueコピペのみ】導入から画像一覧画面の実装まで

スクリーンショット 2020-05-28 20.39.23.png

Vueバージョン確認

npm list vue

まずは上記コマンドでバージョンの確認

twinzlabo@0.1.0 /Users/twinzlabo

── vue@2.6.11

BootstrapVueの導入

BootstrapVueの導入がまだの方のために念のため導入方法書いときますね

とりあえずコピペして環境を整えてください

main.js
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue)
npm install vue bootstrap-vue bootstrap

以上でBootstrapVueの導入は完了です

画像一覧画面の実装

説明は抜きにしてコードを下に貼ってあるのでどんどんコピペして

自分のプロジェクトに合った修正を加えてみてください

一応先に完成イメージです(モバイルに合わせてコーディングしてます)

<template>
  <b-container>
    <b-row>
      <b-col>
        <div v-for="(item, i) in items" class="images" :key="i">
          <b-img thumbnail fluid :src="item.imageUrl"></b-img>
        </div>
      </b-col>
    </b-row>
  </b-container>
</template>
<script>
export default {
  data () {
    return {
      items: [
        { imageUrl: require('@/assets/images/1.png') },
        { imageUrl: require('@/assets/images/2.png') },
        { imageUrl: require('@/assets/images/3.png') },
        { imageUrl: require('@/assets/images/4.png') },
        { imageUrl: require('@/assets/images/5.png') },
        { imageUrl: require('@/assets/images/6.png') },
        { imageUrl: require('@/assets/images/7.png') },
        { imageUrl: require('@/assets/images/8.png') },
        { imageUrl: require('@/assets/images/9.png') },
        { imageUrl: require('@/assets/images/10.png') }
      ]
    }
  }
}
</script>

いかがでしたでしょうか?
見た目はこれからですが画像一覧画面にはなったかと思います

以上です

こちらの記事にてstyleのコードまで詳しく参照できます
【Vue/BootstrapVueコピペのみ】Bootstarap導入からシンプルな画像一覧画面の実装方法までを徹底解説

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

Vue.js で作ったサイトを 無償の Azure Static Web Apps で CI/CD も含めて数分で構築する方法

Microsoft Build 2020 で Public Preview になったスタティックサイト向けの新しいホスティングサービスである Azure Static Web Apps を使うと Vue.js のアプリケーションを数分でデプロイしてクラウド上に公開できるようになります。

cover.png

Vue.js はプログレッシブフレームワークというコンセプトで作られているため、最初は小さく始めて徐々に本格的な構成のアプリケーションに育てていくことができるようになっています。この記事では、 Vue.js と Azure Static Web Apps を使ってスケーラブルなアプリケーションを作るための最初の一歩を紹介したいと思います。

Vue CLI を使ったアプリケーションの新規作成

Azure Static Web Apps では、アプリケーションのコードが GitHub に存在していることが前提です。なので、まず基本的なコードを書いてしまいましょう。
Vue.js アプリケーションを新規に作るには Vue CLI を使うのが近道です。Vue CLI を使えば、例えば Router の導入や TypeScript の開発などを適切な方法で開始することができます。

では、早速 Vue CLI を実行してみましょう。ターミナルを使ってアプリケーションを作成するディレクトリに移動して以下のコマンドを実行します。

npm i -g @vue/cli
vue create .

Vue CLI のプロンプトには以下のように良く使う設定でプロジェクトを新規作成してみます。TypeScript は Vue の開発でも今後主流になると思うので選択しておきます。また Router や ESLint + Prettier 、そして Jest も多くのプロジェクトで使われているので Vue CLI で最初から導入しておきます。
なお、 TypeScript の Use class-style component syntax? は、 Vue 3.x ではメインストリームから外れたので、ここでは No を選択したほうが良いでしょう。

Vue CLI v4.3.1
? Please pick a preset: Manually select features
? Check the features needed for your project: TS, Router, Linter, Unit
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save   
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) N

しばらくすると Vue.js のプロジェクトが生成されます。まずローカルでアプリケーションが起動することを確認します。

npm run serve

無事に Vue.js の緑のアイコンとともに初期画面が表示されれば、Vue.js のアプリケーション基本構成はできあがりです。TypeScript のコンパイルはもちろん、ユニットテストやフォーマッティングも既に実行できるようになっています。あとはプロダクト固有の機能を追加するコードを書くだけです。

この時点で GitHub にコミットして Azure にデプロイする準備をしておきましょう。

Azure Static Web Apps のプロビジョニング

Azure Static Web Apps は Azure Portal の Create a resourcestatic web apps と入力し、以下のように必要事項を入力します。この時、さきほど作成した GitHub Repo の情報をリンクさせます。

swa01.png

次の Build 設定画面では App artifact locationdist と入力します。dist は、Vue CLI 標準のビルドコマンドである npm run build を実行した際に HTML, CSS, Javascript などホスティングの対象となるファイルが出力されるディレクトリです。

swa02.png

あとは、 Review + create をクリックして数分待てば、デプロイが完了です。デプロイのために CI サービスを設定したり、YAML でビルドスクリプトを書いたりする必要はありません!

なお、ビルドとデプロイはバックグラウンドで GitHub Actions のビルドワークフローが自動で実行されます。ビルドの状況を見るには、リポジトリの Actions タブを開いて確認できます。アイコンがグリーンになっていればビルドとデプロイが成功しています。

swa03.png

Azure Portal に戻って サイトの URL をクリックしてみましょう。ローカルで実行したアプリケーションと同じページが公開されているのを見ることができるはずです。

なお、この時点で GitHub リポジトリの .github/workflows に YAML ファイルが追加されているので、ローカルに Pull するのを忘れないようにしましょう。

フォールバックを設定する

Vue CLI で Router を導入した際に、 History Mode を選択しました。SPA ではサーバー側に何も設定をしなければ、特定のページ URL を直接指定すると 404 が発生してしまいます。

swa04.png

History Mode をホスティング環境でも有効にするには、その環境に適した設定を行う必要があります。Azure Static Web Apps では以下のような routes.json ファイルを /public 以下に作成します。

{
  "routes": [
    {
      "route": "/*",
      "serve": "/index.html",
      "statusCode": 200
    }
  ]
}

では、この変更をブランチにプッシュしてプルリクエストを作成しましょう。

swa05.png

プルリクエストをトリガーに Static Web Apps へのデプロイが実行されています。ビルドプロセスが完了すると、ポータルの Environments から変更がデプロイされたアプリケーションを実際に確認することができるようになります。

swa06.png

ステージングされた変更から Browse をクリックして実際にページを確認してみると、 about ページをリロードしても404ページが表示されないように改善されていることを確認できました。

swa07.png

あとは、プルリクエストをマージして production 環境にデプロイされるのを待つだけです。なお、プルリクエストがクローズされると、ステージング環境は自動的に削除されます。

このように、スケーラブルな構成の Vue.js アプリケーションがとても簡単に Azure でホスティングができるようになることが確認できたのではないでしょうか?

ちなみに Azure Static Web Apps は現在 Public Preview で無償で利用できます。

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

Vue.js で作るサイトを 無償の Azure Static Web Apps に CI/CD も含めて数分でホスティングする方法

Microsoft Build 2020 で Public Preview になったスタティックサイト向けの新しいホスティングサービスである Azure Static Web Apps を使うと Vue.js のアプリケーションを数分でデプロイしてクラウド上に公開できるようになります。

cover.png

Vue.js はプログレッシブフレームワークというコンセプトで作られているため、最初は小さく始めて徐々に本格的な構成のアプリケーションに育てていくことができるようになっています。この記事では、 Vue.js と Azure Static Web Apps を使ってスケーラブルなアプリケーションを作るための最初の一歩を紹介したいと思います。

Vue CLI を使ったアプリケーションの新規作成

Azure Static Web Apps では、アプリケーションのコードが GitHub に存在していることが前提です。なので、まず基本的なコードを書いてしまいましょう。
Vue.js アプリケーションを新規に作るには Vue CLI を使うのが近道です。Vue CLI を使えば、例えば Router の導入や TypeScript の開発などを適切な方法で開始することができます。

では、早速 Vue CLI を実行してみましょう。ターミナルを使ってアプリケーションを作成するディレクトリに移動して以下のコマンドを実行します。

npm i -g @vue/cli
vue create .

Vue CLI のプロンプトには以下のように良く使う設定でプロジェクトを新規作成してみます。TypeScript は Vue の開発でも今後主流になると思うので選択しておきます。また Router や ESLint + Prettier 、そして Jest も多くのプロジェクトで使われているので Vue CLI で最初から導入しておきます。
なお、 TypeScript の Use class-style component syntax? は、 Vue 3.x ではメインストリームから外れたので、ここでは No を選択したほうが良いでしょう。

Vue CLI v4.3.1
? Please pick a preset: Manually select features
? Check the features needed for your project: TS, Router, Linter, Unit
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save   
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) N

しばらくすると Vue.js のプロジェクトが生成されます。まずローカルでアプリケーションが起動することを確認します。

npm run serve

無事に Vue.js の緑のアイコンとともに初期画面が表示されれば、Vue.js のアプリケーション基本構成はできあがりです。TypeScript のコンパイルはもちろん、ユニットテストやフォーマッティングも既に実行できるようになっています。あとはプロダクト固有の機能を追加するコードを書くだけです。

この時点で GitHub にコミットして Azure にデプロイする準備をしておきましょう。

Azure Static Web Apps のプロビジョニング

Azure Static Web Apps は Azure Portal の Create a resourcestatic web apps と入力し、以下のように必要事項を入力します。この時、さきほど作成した GitHub Repo の情報をリンクさせます。

swa01.png

次の Build 設定画面では App artifact locationdist と入力します。dist は、Vue CLI 標準のビルドコマンドである npm run build を実行した際に HTML, CSS, Javascript などホスティングの対象となるファイルが出力されるディレクトリです。

swa02.png

あとは、 Review + create をクリックして数分待てば、デプロイが完了です。デプロイのために CI サービスを設定したり、YAML でビルドスクリプトを書いたりする必要はありません!

なお、ビルドとデプロイはバックグラウンドで GitHub Actions のビルドワークフローが自動で実行されます。ビルドの状況を見るには、リポジトリの Actions タブを開いて確認できます。アイコンがグリーンになっていればビルドとデプロイが成功しています。

swa03.png

Azure Portal に戻って サイトの URL をクリックしてみましょう。ローカルで実行したアプリケーションと同じページが公開されているのを見ることができるはずです。

なお、この時点で GitHub リポジトリの .github/workflows に YAML ファイルが追加されているので、ローカルに Pull するのを忘れないようにしましょう。

フォールバックを設定する

Vue CLI で Router を導入した際に、 History Mode を選択しました。SPA ではサーバー側に何も設定をしなければ、特定のページ URL を直接指定すると 404 が発生してしまいます。

swa04.png

History Mode をホスティング環境でも有効にするには、その環境に適した設定を行う必要があります。Azure Static Web Apps では以下のような routes.json ファイルを /public 以下に作成します。

{
  "routes": [
    {
      "route": "/*",
      "serve": "/index.html",
      "statusCode": 200
    }
  ]
}

では、この変更をブランチにプッシュしてプルリクエストを作成しましょう。

swa05.png

プルリクエストをトリガーに Static Web Apps へのデプロイが実行されています。ビルドプロセスが完了すると、ポータルの Environments から変更がデプロイされたアプリケーションを実際に確認することができるようになります。

swa06.png

ステージングされた変更から Browse をクリックして実際にページを確認してみると、 about ページをリロードしても404ページが表示されないように改善されていることを確認できました。

swa07.png

あとは、プルリクエストをマージして production 環境にデプロイされるのを待つだけです。なお、プルリクエストがクローズされると、ステージング環境は自動的に削除されます。

このように、スケーラブルな構成の Vue.js アプリケーションがとても簡単に Azure でホスティングができるようになることが確認できたのではないでしょうか?

ちなみに Azure Static Web Apps は現在 Public Preview で無償で利用できます。

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

Vue.jsでinputフォームの文字数制限をする

今回のゴール

  • Vue.jsで実装するinputフォームで入力できる文字数を制限し、特定の文字数以上は入力できないようにする

実際のコード

サンプル

CodeSandboxに実際に動くもの作りました。
https://codesandbox.io/s/input-form-chara-limit-o9qp1?fontsize=14&hidenavigation=1&theme=dark

<template>
  <div id="app">
    <p>5文字以上入力ができないinputフォーム</p>
    <input type="text" v-model="inputText">
    <p>{{ inputText }}</p>
  </div>
</template>

<script>

export default {
  data() {
    return {
      inputText: ""
    };
  },
  watch: {
    inputText(inputText) {
      this.inputText = this.charaLimit(inputText);
    }
  },
  methods: {
    charaLimit(inputText) {
      return inputText.length > 5 ? inputText.slice(0, -1) : inputText;
    }
  }
};
</script>

watchによってv-modelで文字列inputTextが新しく入力された時に、methodのcharaLimitが動くようにします。
cahraLimmitではinputTextが5文字より多い場合は文字列の末尾を削除し、そうでなければ入力された文字列をそのまま
returnしてinputTextに代入するようにします。

inputTextが入力される度に文字列の末尾を削除するので実質的に、文字列が5文字より多い場合はinputTextに値が入らなくなります。

所感

入力された文字のバリデーションは正規表現に任して、入力文字数はsliceで対応すると簡単でした。

参考

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

【Vue/Font Awesome導入時のエラー解決】error There should be no space after this paren space-in-parens

スクリーンショット 2020-05-28 17.03.05.png

Vueバージョン確認

npm list vue

まずは上記コマンドでバージョンの確認

twinzlabo@0.1.0 /Users/twinzlabo

── vue@2.6.11

エラーメッセージ

Failed to compile.

./src/main.js Module Error (from ./node_modules/eslint-loader/index.js): /Users/twinzlabo/src/main.js

17:15 error Multiple spaces found before ”font-awesome-icon” no-multi-spaces
17:15 error There should be no space after this paren space-in-parens
17:53 error Multiple spaces found before ‘)’ no-multi-spaces
17:53 error There should be no space before this paren space-in-parens

解決策

今回のエラー文で注目すべき箇所は

17:15 error Multiple spaces found before ”font-awesome-icon” no-multi-spaces
17:15 error There should be no space after this paren space-in-parens
17:53 error Multiple spaces found before ‘)’ no-multi-spaces
17:53 error There should be no space before this paren space-in-parens

つまりmain.js17行目でスペースがおかしい使い方されているよというエラーです

ではまず実際のエラー箇所を確認してみましょう

main.js
// about fontawesome---
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCoffee, faSpinner, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// -------------------
import App from './App.vue'
import router from './router'
import store from './store'

library.add(faCoffee, faSpinner, faAngleDoubleUp)
Vue.component(  'font-awesome-icon', FontAwesomeIcon  )

どうやらFont Awesome導入時にエラーが発生してしまったようですね

Vue.component( ‘font-awesome-icon’, FontAwesomeIcon )

こちらぱっと見は全然問題なさそうですが、

‘font-awesome-icon’, FontAwesomeIcon

この前後にスペースが1つずつ存在してしまっています

細かいですがESlintではこの程度でもエラーを表示してしまうので気をつけましょう

修正後のコードはこのようになります

// about fontawesome---
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCoffee, faSpinner, faAngleDoubleUp } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// -------------------
import App from './App.vue'
import router from './router'
import store from './store'

library.add(faCoffee, faSpinner, faAngleDoubleUp)
Vue.component('font-awesome-icon', FontAwesomeIcon)

これで無事解決しましたね

以上です

参考記事
【Vue/Font Awesome導入】Font Awesomeを導入してアイコンを使用するまでの流れを徹底解説

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

VuexFireでバインドしたデータをcomputedやmethodsで使う

Nuxt.js+Firebaseの勉強を始めたばかりです。

前回: Nuxt.js + Firebase + VuexFireでデータ表示をソートする

やりたいこと

Vuexfireでバインドしたデータをcomputedやmethodsで使いたいです。

Webページにリストボックスを用意しています。リスト項目の内容をFirestoreで管理します。itemsへの設定はcomputedで行います。

環境

  • Firebase 8.3.0
  • Vue CLI 4.0.5
  • Nuxt.js 2.11.1
  • VuexFire 3.2.0

失敗したこと

例えば、Firestoreの構成は次のようにしておいて、

コレクション ドキュメント フィールド
pages (document) title

pages/index.vueの cumputed...mapGetters({ pages: 'getPages' }) としておいたとします。 template の中で下記コードを書けばtitleフィールドのデータが表示されますよね。

<v-flex v-for="(page, i) in pages" :key="i"
        xs12 sm4 mb-2 pa-2>
  <v-card>
    <v-card-text>
      {{page.title}}
    </v-card-text>
  </v-card>
</v-flex>

ネットで調べると同様の記事が多く出てきますので、フロント技術に疎い私でもコピペすれば期待通り動かすことができました。

だから私は最初、computedやmethodsで利用するときも下記のようなコードで利用できるのではないかと考えました。しかしこれは失敗でした。

これではうまくいきません
for (let page in this.pages) {
  console.log(page.title);
}

この例では、pageには 0,1,2.. が入っているので、 this.pages[page].title でデータを取り出します。

これはうまくいきます
for (let page in this.pages) {
  console.log(this.pages[page].title);
}

私が実装したこと

コレクション ドキュメント フィールド
categories (document) A001: カテA
A002: カテB
A003: カテC
pages/index.vue
<template>
  <v-layout justify-center align-start row wrap>
    <v-flex xs12 sm9 mb-2 >
      <v-card>
        <v-card-text>
          <v-select
            v-model="selectedCategories"
            item-text="label"
            item-value="value"
            :items="categoriesList"
            label="カテゴリーを選んでください"
            return-object
          />
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
import { mapGetters } from 'vuex';
import { db } from '../plugins/firebase';

export default {
  data () {
    return {
      selectedCategories: { label: 'カテA', value: 'A001' },
    }
  },
  computed: {
    ...mapGetters({ categories: 'getCategories' }),
    categoriesList: function () {
      let arr = [];

      for (let i in this.categories) {
        let doc = this.categories[i];
        let keys = Object.keys(doc);
        for (let j in keys) {
          let key = keys[j];
          arr.push({ label: doc[key] , value: key });
        }
      }

      return arr;
    },
  },
  mounted () {
    this.$store.dispatch('setCategoriesRef', 
                         db.collection('categories'));
  },
}
</script>
store/index.js
import { vuexfireMutations, firestoreAction } from 'vuexfire';

export const state = () => ({
      categories: [],
    });

export const mutations = {
      ...vuexfireMutations
    };

export const getters = {
      getCategories: (state) => {
        return state.categories;
      },
    };

export const actions = {
      setCategoriesRef: firestoreAction(({ bindFirestoreRef }, ref) => {
        bindFirestoreRef('categories', ref);
      }),
    };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails: ActionView::Template::Error http error 500 対処法について

現在、ポートフォリオを作成している途中のゆーた(@onoblog)です。
スクリーンショット 2020-05-22 13.48.07.png
そこそこ、ローカル環境で、ポートフォリオのデザインも整ってきたし、1回、デプロイしとこうかなと思い、デプロイしたときのエラーをまとめておきます。

環境

  • rails 5.2.3
  • carrierwave
  • fog-aws
  • jquery-rails (4.3.5)
  • vue/cli 4.1.2
  • yarn 1.21.1
  • webpacker (5.0.1)
  • Docker 19.03.5
  • docker-compose 1.25.2
  • nginx 1.15.8

対処法

assetへのパイプラインを通してあげる設定にしてあげます。

config/enviroments/production.rb
config.assets.compile = true

状況

スクリーンショット 2020-05-22 13.48.07.png

このように、デプロイしたものを表示しようとするとhttp error 500が起きてしまいました。

最初に、nginxのlogかawsのec2の設定を疑い、無駄な時間を使ってしまいました。

docker-compose -f staging.yml exec app tail -f log/production.log

その後、上記コマンドで本番環境のログを見るとActionView::Template::Errorが起きている状況でした。

原因

ローカル環境では、trueになっていたことにより、動的にコンパイルができていましたが、本番環境では、falseになってしまっていました。

precompileしていないファイルを動的にコンパイルするので、本番環境では、負荷がかかってしまうので、良くないと思いますが、一時的に、適宜変更します。

development production
true false

参照

https://qiita.com/metheglin/items/c5c756246b7afbd34ae2

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

VuejsでGoogleMapAPIを使用して現在地を中心にしたマップを表示する

VuejsでGoogleMapAPIを使用して現在地を中心にしたマップを表示する

概要

  • VuejsのGoogleMapのプラグインを使わないで現在地を中心としたマップを表示する

環境

  • Mac Catalina(10.15.2)
  • Node (v12.7.0)
  • VSCode

前提

  • VueCLIを使用する
  • GoogleMapAPIのKeyは取得済み

手順

プロジェクトを立ち上げる

  • 以下のコマンドでプロジェクトフォルダ新規作成する
vue create google-map-vue-practice
  • プロジェクトフォルダをVSCodeで開く

ファイルを変更する

  • public/index.htmlに以下のコードを追加する
    • API_KEYは自身のGoogleMapAPIKeyに置き換える
index.html
        <div id="app"></div>
    <!-- built files will be auto injected -->
    <script async defer
    src="https://maps.googleapis.com/maps/api/js?key=API_KEY&callback=initMap">
  </script>
  </body>
  • App.vueを以下のようにする
    • navigator.geolocation.getCurrentPosition
      • 現在地を取得する関数
    • window.google.maps.LatLng
      • 緯度経度のオブジェクトを作る関数
    • window.google.maps.Map
      • GoogleMapを作成する関数
    • window.google.maps.Marker
      • マーカーを作成する関数
App.vue
<template>
  <div id="app">
    <div id="map" ref="map" />
  </div>
</template>

<script>

export default {
  name: "App",
  data: () => ({
    map:null
  }),
  mounted() {
    if (navigator.geolocation) {
      // callback関数内でthis使えないため
      let vm = this
      navigator.geolocation.getCurrentPosition(
        function(position){
          let latlng = new window.google.maps.LatLng(
            position.coords.latitude,
            position.coords.longitude
          );
          vm.map = new window.google.maps.Map(vm.$refs["map"], {
            center: latlng,
            zoom: 4
          })
          new window.google.maps.Marker({
            position: latlng,
            map: vm.map
          })
        }
      )
    }
  }
};
</script>

<style>
#map {
  height: 600px;
  background: gray;
}
</style>

実行する

  • 以下のコマンドでローカル実行する
npm run serve
  • http://localhost:8080/でブラウザを開くと以下の画面が出る

スクリーンショット 2020-05-28 17.20.51.png

終わりに

  • 今回のプロジェクトを私のリポジトリで公開
    • 上記のコード以外も少し混ざってますのでご了承ください
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヒープ領域制限によりビルドが通らない場合

環境
インスタンス:t3a.nano
OS: AmazonLinux2
node.js:v14.3.0

上記環境でnpm run serveを実施してVueアプリの動作確認をした後に
npm run buildでビルドを実行したところ

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

のエラーが表示されてビルドが通らなかった。

ビルドするためのメモリが足らないと判断できるため
スワップ領域を確保したうえで
https://qiita.com/nakamto/items/5e78e9caceeff6b9e2b4

$ export NODE_OPTIONS="--max-old-space-size=1024"
$ npm run build

※1024は確保できるメモリ量に応じて変更してください。
max-old-space-sizeの値を指定することでビルドが通りました。

メモリの少ないインスタンスを利用していることが原因
と推測しますので、メモリが大きいインスタンスへ変更しても良いかもしれません。

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

Laravel+Vue propsでの親から子へのデータの渡し方

親から子へのデータ流れ

  1. 子コンポーネントにpropsを定義する
<!-- Like.vue(子コンポーネント) -->
<script>
export default {
  props: ["postId", "userId", "defaultLiked", "defaultCount"],
 }
</script>

2 Laravelならこのデータ渡し方をコントローラー(親)で定義する。

// PostController
class class PostController extends Controller{
public function show(Post $post)
 {
  $userAuth = \Auth::user();

  $post->load('likes');

  $defaultCount = count($post->likes);
  $defaultLiked = $post->likes->where('user_id', $userAuth->id)->first();
  if (count($defaultLiked) === 0) {
   $defaultLiked == false;
  } else {
   $defaultLiked == true;
  }

  return view('posts.show', [
   'post' => $post,
   'userAuth' => $userAuth,
   'defaultLiked' => $defaultLiked,
   'defaultCount' => $defaultCount
  ]);
 }
}

3 親コンポーネントにv-bindを使い、2(コントローラー)で受けとったデータを参照し子コンポーネント(Like.vue)に受け渡す。

<!-- show.blade.php(親コンポーネント) -->
<like :post-id="{{ json_encode($post->id) }}" 
      :user-id="{{ json_encode($userAuth->id) }}"
      :default-Liked="{{ json_encode($defaultLiked) }}" 
      :default-Count="{{ json_encode($defaultCount) }}"
></like>

アドバイスあればよろしくお願いします!

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

vue nuxt 個人メモ

vue nuxt 個人メモ

データセット

dataメソッドで行う

index.vue
<script>
export default Vue.extend({
  data() {
    return {
      modal: false,
      email: '',
      password: ''
    }
  }
})
</script>

ハンドラー、イベント

methodsで行う

index.vue
<script>
export default Vue.extend({
  data() {
    return {
      modal: false,
      email: '',
      password: ''
    }
  },
  methods: {
    openModal() {
      this.modal = true
    },
    closeModal() {
      this.modal = false
    }
  }
})
</script>

api取得 ライフサイクル

画面の描画前にデータを取得する場合 asyncData

画面の描画後にデータを取得する場合 mounted

index.vue
<script>
export default Vue.extend({
  data() {
    return {
      modal: false,
      email: '',
      password: ''
    }
  },
  async asyncData({ query, params, $axios }) {
    try {
      const response = await $axios
        .$get(
          `/api/~~${query.id}`,
          {...params},
          { withCredentials: true }
        )
        .catch((error) => error.response)
      if (response === undefined) {
        throw new Error('undefined')
      }
      if (response.status !== 'success') {
        throw new Error(response.data.message)
      }
      return { error: false }
    } catch (error) {
      console.error(error.message)
      return { error: true }
    }
  },
  async mounted() {
    await this.getAPI()
  }
})
</script>

大きな違いはSSRで画面を表示するかどうか??
https://medium.com/veltra-engineering/in-ssr-vue-js-is-created-twice-7f9122de9b77

Nuxtでは特にユニバーサルなコードを書くことを意識するのが重要なので、
違いを理解する必要がある(まだ理解してない)

props

v-forなどでデータを渡す際はv-bindでそのまま渡す

index.vue
  <ul>
    <ListData
      v-for="item in data"
      :key="item.id"
      v-bind="item"
    />
  </ul>

渡されたデータがスネークケースの場合で、
キャメルケースに変換したい場合はハイフンでつなぐ

index.vue
  <ul>
    <ListData
      v-for="item in data"
      :key="item.id"
      :index="index + 1"
      :start-date="item.start_date"
      :end-date="item.end_date"
    />
  </ul>

watch method

dataで定義した値に変更が加わった時にフックして処理を実行したい場合などに使う

<script>
export default {
  props: {
    id: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      value: ''
    }
  },
  watch: {
    value(newValue, oldValue) {
      console.log(
    },
    id(newValue, oldValue) {
      console.log(
    }
  }
}
</script>

nuxt link

to="" でリンク先を指定するけど
変数を入れたい場合など

:to="`/myPage/${user.id}`"

親から子へ渡す props

親コンポーネント

書き方は下記

<template>
  <Child hoge1="ABC" hoge2="DEF"> 
</template>

値が変動する場合はデータバインドで渡す

<template>
  <Child :hoge1="dat1" :hoge2="dat2">
</template>

子コンポーネント

配列形式で受けとる場合

<script>
export default {
  props: ['hoge1', 'hoge2']
}
</script>

オブジェクトで受け取る場合

<script>
export default {
  props: {
    hoge1: {
      type: String,
      default: 'abc',
      required: true
    },
    hoge2: {
      type: String,
      default: '',
      required: false
    }
  }
}
</script>

子から親に伝達するemit

v-on:または@でイベントを紐づける

親コンポーネント
<Child  @click="onClick">

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  methods: {
    onClick(e: Event) {
      e.preventDefault()
      console.log(e)
    }
  }
})
</script>
子コンポーネント
<template>
    <button
      type="button"
      @click="$emit('click')"
    />
</template>
methodsなどで使用する場合
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  methods: {
    onClick(e: Event) {
      e.preventDefault()
      this.$emit('click')
      this.$emit('click', e) // 引数を渡す場合は第二引数
    }
  }
})
</script>

@clickに引数を渡す

忘れガチなのでメモ

<template>
  <div id="app">
    <button @click="click">button</button>
    <button @click="superClick('引数です')">param button</button>
  </div>
</template>

<script>
export default {
  methods: {
    superClick(value) {
      alert(value);
    }
  }
};

propsType

Typescript interface でpropsの受け取る型を指定

index.vue
<script lang="ts">
interface Schedule {
  id: Number,
  startDate: String,
  endDate: String,
  deliveryType: Number,
  afterTalkFlag: Number
}


export default Vue.extend({
  props: {
    userName: String,
    schedule: {
      type: Object as PropType<Schedule>,
      default: null
    }
  }
})
</script>

nuxt での form入力

input checkbox

まずデータを用意

<script>
export default {
  data() {
    return {
      dataList: [  // api から取得してきたデータ
        {name: "hoge"},
        {name: "fuga"},
        {name: "fugafuga"}
      ],
      formData: [] // form送信用
    }
  }
}
</script>

用意したデータをバインドする

<template>
  <div
    v-for="(item, index) in dataList"
    :key="'dataItem' + index"
  >
    <input
      :id="'dataItem_' + item.name"
      type="checkbox"
      :value="item.name"
      v-model="formData"
      name="'dataItem_' + item.name"
    />
    <label :for="'dataItem_' + item.name">
      {{ item.name }}
    </label>
  </div>
</template>

デフォルトでチェックを入れる場合

デフォルトでチェックを入れる場合はv-modelで紐付ける
form送信用のdataオブジェクトに初期値をセットする

<script>
export default {
  data() {
    return {
      dataList: [  // api から取得してきたデータ
        {
          name: "hoge",
          value: true
        },
        {
          name: "fuga",
          value: true
        },
        {
          name: "hogege",
          value: false
        }
      ],
      formData: [] // form送信用
    }
  },
  mounted() {
    this.formData = this.dataList.map((item: any) => item.value)
  }
}
</script>

子のプロパティまたはメソッドを参照する(refs)

親のresetボタンを押した際に、子供のフォームの内容をresetする場合など

ChildForm.vue
<script>
export default {
  name: 'ChildForm',
  data() {
    value: ''
  },
  methods: {
    onClear() {
      this.value = ''
    }
  }
}
</script>
parent.vue
<template>
  <div>
    <ChildForm ref="childForm"/>
    <button @click="clear">
      Clear
    </base-button>
  </div>
</template>

<script>
import ChildForm from '@/components/ChildForm.vue'

export default {
  components: ChildForm,
  methods: {
    clear() {
      this.$refs.childForm.onClear()
    }
  }
}
</script>

v-for での特定回数の繰り返し

記事データを表示する際、1 ~ 10 件のデータを表示する場合がある
他に、ページャーなどの機能で配列の10 ~ 20件の間のデータを抽出したいなど
v-forに整数値を与えることで繰り返しの回数を制御できるがそれだと実現したい機能とは違うのでまとめておく

<template>
  <div v-for="item in pages">
    <div>{{ item.name }}</div>
    <div>{{ item.age }}</div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      currentPage: 1, //現在のページ
      pageLimit: 50,  // 表示できるページ数
      limitItem: 10, // 表示できる要素数
      customerList: [
        { id:1, name: 'hoge', age: 20 },
        { id:2, name: 'fuga', age: 22 },
        { id:3, name: 'aaaa', age: 30 },
        { id:4, name: 'fugafuga', age: 10 }
        // 以降 200件のデータが続く
      ]
    }
  },
  computed: {
    pages() {
      const maxItem = Number(this.itemLimit)
      const start = this.currentPage === 1 ? 0 : (this.currentPage - 1) * maxItem
      const end = this.currentPage === 1 ? maxItem : this.currentPage * maxItem
      return this.customerList.filter((_element, index) => {
        return index >= start && index < end
      })
    }
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vueでチェックボックス全選択の作り方

概要

タイトルの通り、以下のようなチェックボックスを全て選択するチェックボックスを実装する方法
AGDRec.gif

コード

テンプレート側
<el-row :gutter="24">
  <el-col :span="21" class="disability_name_area_allcheck"
    ><el-checkbox
      v-model="checkAll"
      :indeterminate="isIndeterminate"
      :disabled="!selectRireki.isEditable"
      @change="checkAllObstacles"
      >すべて選択</el-checkbox
    >
  </el-col>
</el-row>
<el-row :gutter="24">
  <el-col :span="24" class="disability_name_area">
    <original-check-list
      v-model="selectedDisabilityCertificateData.body_disability_name"
      item-physical-name="body_disability_name"
      item-display-name=" "
      :option-list="obstaclesList"
      :input-required="false"
      :disabled-condition="!selectRireki.isEditable"
    ></original-check-list>
  </el-col>
</el-row>

スクリプト側
private isIndeterminate: boolean = false;
private checkAll: boolean = false;

checkAllObstacles(val: boolean) {
  if (!this.selectedDisabilityCertificateData) {
    return;
  }
  this.selectedDisabilityCertificateData.body_disability_name = val
    ? this.getObstaclesListKeys()
    : [];
  this.isIndeterminate = false;
}

@Watch("selectedDisabilityCertificateData.body_disability_name")
checkedObstaclesChange(value: Array<boolean>) {
  let checkedCount = value.length;
  this.checkAll = checkedCount === this.obstaclesList.length;
  this.isIndeterminate = checkedCount > 0 && checkedCount < this.obstaclesList.length;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでVue.jsのscaffoldを生成した時の違和感

Laravelでphp artisan ui vueをするとVue.jsのscaffold(「スカフォールド」とか「スキャフォールド」とか言って、ひな形を意味するらしい)を生成できるのだが...

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

これは違和感を感じないか??

ExampleComponent.vue側でmodule.exportsして、

Vue.component(
    'example-component',
    require('./components/ExampleComponent.vue')
);

とするか、

import ExampleComponent from './components/ExampleComponent';
Vue.component('example-component', ExampleComponent);

としたくない?
Laravel公式にも堂々と.defaultって書いてるし、敢えてなのか?

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

【Vuetify】NuxtのSSRでVuetifyのDatePickerを使用したら「The client-side rendered virtual DOM tree is not matching server-rendered content. 」というエラー発生

現象

スクリーンショット 2020-05-28 3.51.15.png
SSRでVuetifyのDatePickerを使用したら
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.
というエラーが発生

解決策

調べてみると tableにtbodyを入れると解決するとのこと。
https://qiita.com/k5690033/items/d1e436fd3b92fcc1fe61

しかし今回はコンポーネントを丸ごと使用しているためこれができない。

no-ssrを使う

DatePickerに関してはssrする必要がそもそもないので no-ssr で囲って解決した。
https://github.com/nuxt/nuxt.js/issues/1700

    <no-ssr>
      <v-date-picker
        v-model="picker"
        :landscape="landscape"
        :reactive="reactive"
        :full-width="fullWidth"
        :show-current="showCurrent"
        :type="month ? 'month' : 'date'"
        :multiple="multiple"
        :readonly="readonly"
        :disabled="disabled"
        :events="enableEvents ? functionEvents : null"
      ></v-date-picker>
    </no-ssr>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでAPIのBasic認証をした

はじめに

Vue.jsによる開発においてAPIのBasic認証の実装をしました。
エラーについての検索をした際に類似のケースで苦しんでいる方が国内外問わず多くいらっしゃったので、本記事では私が嵌ったエラーと解決策の一例について書いていきます。
当記事の使用言語はVue.jsとNode.jsです。

課題の切り分け

実装に当たって私が悩んだ点は大きく分けて下記二つとなります。
・CORSの不一致
・非同期処理の中でBasic認証が出来ない(404エラーが返ってくる)

◎CORSの不一致

APIのBase URLに対してGETでデータの取得を初めて求めた際に、

Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. 
Origin 'http://localhost:8080' is therefore not allowed access. If an opaque response serves your needs, 
set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

以上のエラーが返ってきた。

エラーはどうやら「same-origin」と呼ばれるセキュリティポリシーにより、ブラウザが同一のドメインを持つアプリケーションのみと対話するように保護されているためであり、

Google先生に質問をしたところ、VueではProxyの設定でCORSのエラーを回避出来ることが分かり、configを設定するファイルに下記のような記述をした。

vue.config.js
module.exports = {
        devServer   : {
      // proxy all requests starting with /api to jsonplaceholder
        "/testapi": {
          target:
           "https://api.test.hogehoge.jp/",
          changeOrigin: true,
          pathRewrite: {
            "^/testapi": ""
          }
    }
}

公式「フロントエンドアプリとバックエンドAPIサーバーが同じホスト上で実行されていない場合は、開発中にAPIリクエストをAPIサーバーにプロキシする必要がある」・・・。
改めてドキュメントを読み込む大切さを思い知った・・・?

ちなみにCORSについてはこちらの記事が大変分かりやすかったです、ありがとうございます!

◎非同期処理の中でBasic認証が出来ない(404エラーが返ってくる)

CORSの問題を解決して一件落着!と思いきや、GETして拾ってきたのはJsonではなく404エラーだった。

エラーはBasic認証の部分で発生しており、axiosを利用した非同期処理のロジックを組みながら最終的に下記のようなコードで実装をした。

getApiData.js
 getApiData({ commit }) {
    const clientId = "hoge";
    const clientSecret = "test";
   //Basic認証のために、base64でエンコードする
    const encodedData = Buffer.from(clientId + ":" + clientSecret).toString(
      "base64"
    );

    axios
      .get("/testapi", {
        withCredentials: true,
        headers: { Authorization: "Basic " + encodedData }
      })
      .then(function(response) {
        console.log("成功");
      })
      .catch(function(error) {
        console.log("失敗");
      });
  }

これは定型文的に決まってルールとして、
Basic認証がかかったURLにアクセスする場合にはAuthorizationヘッダーへ向けて、

Authorization: Basic <Base64エンコードしたユーザ名:パスワード>

以上をリクエストをする必要があるようだった。

おわりに

今回APIを扱うに当たって、はじめてポストマン を使ってみたところ、素早くテストをすることが出来ました。学習コストはほとんどなく、直感的に扱えたので次回以降も継続して利用をしていこうと思います。

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

.sync修飾子を使って複数のフォームの変更監視をする

.sync修飾子とは

https://jp.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A3%BE%E5%AD%90

上記をご覧ください。
.sync修飾子を使用すれば複数のフォームで入力した内容を編集したりできる便利。

どうやって使うか

下記のようにモーダルを呼び出すスクリプトを用意しました。
やっていることはボタンを押した際にopenModal(todo)が発火されtodoオブジェクトをスクリプトに渡しつつモーダルを開くような形にしています。

親コンポーネント
<!-- モーダルを開く際todoObjectを渡す -->
<b-button variant="outline-info" @click="openModal(todo)">
  編集         
</b-button>

<transition name="modal">
<!-- .syncで子コンポーネントの変更監視 -->
<!-- モーダルを動的にするために子コンポーネントにtodoの情報を渡している -->
  <show-modal
    :id="todoId"
    :timelimit.sync="todoTimelimit"
    :content.sync="todoContent"
    v-if="showContent"
    @close="modalClose"
    v-model="editContent"
    @edit="editTodo($event)"
  />
</transition>

<script>
export default {
  name: 'todo-list',
  components: {
    ShowModal,
  },
// 送られてきたtodoObjectを変数に代入
  openModal (todo) {
    this.showContent = true
    this.todoId = todo.id
    this.todoTimelimit = todo.timelimit
    this.todoContent = todo.content
  },
  editTodo (e) {                     
    this.$store.commit('editTodo', {
      newContent: this.todoContent,
      newTimelimit: this.todoTimelimit,
      id: e
  })

</script>

続いて子コンポーネントです。
$emitに注目していただけるとわかる通りpropsで受けとっている物の後ろにupdate:をつけて$emitで渡しています。第二引数は$eventじゃないと親コンポーネントに渡すことができませんでした。
.sync修飾子を利用することでフォームの中に書かれている内容を検知しそれぞれが親のコンポーネントに渡す為、v-modelによって変更感知する必要が無くなります。それにv-modelと違って複数の変更が感知できるようになった為、さらに子コンポーネント内でのフォーム作成の自由度が上がると思います。

子コンポーネント
<template>
  <div>
    <div class="modal-mask">
      <div class="modal-wrapper" @click.self="$emit('close')">
        <div class="modal-container">
          <div class="modal-header">
            編集
          </div>
          <div class="modal-form">
            <b-input-group class="mb-2">
<!-- $emitで入力内容を親コンポーネントに渡している -->
              <b-form-input
                type="text"
                placeholder="やりたいことを入力してください"
                @input="$emit('update:content', $event)"
                :value="content"
              ></b-form-input>
<!-- $emitで入力内容を親コンポーネントに渡している -->
              <b-form-datepicker
                class="mb-2"
                placeholder="何日までに行いますか?"
                :value="timelimit"
                @input="$emit('update:timelimit', $event)"
              ></b-form-datepicker>
            </b-input-group>
          </div>

          <div class="modal-footer">
            <button @click="edit(id)">OK</button>
            <button @click="$emit('close')">Close</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'show-modal',
  props: ['id', 'timelimit', 'content'] ,
  deta () {
    return {
      closeModal: false,
      text: '',
    }
  },
  methods: {
    edit(id) {
      this.$emit('edit', id)
      this.$emit('close')
    }
  }
}
</script>

最後に.sync修飾子によって送られてきたフォームの内容を下記のスクリプトで送信しています。

<script>
  editTodo (e) {                     
    this.$store.commit('editTodo', {
      newContent: this.todoContent,
      newTimelimit: this.todoTimelimit,
      id: e
  })
</script>

複数のフォームに対してv-model以外で検知する方法を探していたらsync修飾子というものを見つけました。どん詰まって何時間も浪費するかと思いきや以外とあっさり解決できたのでよかったです。
お役に立てれば幸いです。

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

未来の観光業をLINEBotで支えたい。

スクリーンショット 2020-05-27 12.47.36.png

はじめに

緊急事態宣言が解除されましたが、全く同じ生活に戻ることはまずないでしょう。そんな中僕たちが救いたいと思ったのは日本全国にある観光地です。各地の特産品やお土産品は、色々なインターネットサービスを使って格安で在庫処分が行われています。このままではなかなか利益が見込めず、一刻も早く観光客が安心して旅行をできる環境を作らなければいけません。今回作った作品は後ほど紹介するハッカソンで奨励賞をいただいた作品の記録用記事です。開発構成/使用技術などについても少し書いています。
また、未来を見据えた物なので自粛警察はやめてください。

メンバー

今回は広島県等が主催するレッドハッカソン2020にしんぶんぶん(@shinbunbun_)とゆうせい(@ItyuJ)と僕(@inoue2002)の高校3年生3人でチームを組んで参加しました。
アイデア出しは一応始まる前から初めてはいたのですが、なかなかいいアイデアが出ず、実質、他の参加者の方のグループより出発は遅くなってしまいました。それぞれ普段からよく開発している分野がLINEBot/フロントエンド/バックエンドと綺麗に別れていたので、自分の担当する部分をメインにゴリゴリ開発しました。(LINEbotはぶんぶん君に力を貸してもらった部分もあります)

作ったもの(登壇動画)(5分)

IMAGE ALT TEXT HERE

実際の同級生の声をヒントに

スクリーンショット 2020-05-27 13.29.58.png

自粛に疲れた高校生は今、何を従っているのか。聞いてみました。そこで、カラオケなど以外に、「県外に遊びに行きたい」「旅行に行きたい」との声も多くあったので、観光業に何かアプローチしようと思ったのがきっかけです。

観光業の今の現状

観光していて外せないものといえば、その土地の名産品やお土産品ですよね。
しかし誰も観光客が来なくなった今、それらの行き場を失った物達はネット通販等で格安に在庫処分されるようになりました。
いくら在庫処分が全てできたとしても利益が出たり、観光客が増えたりする訳でも無く、解決の方向には進んでいません。
そんなこれらの状況から生まれたのがこの「トラべる!」です。

「トラべる!」は何がどうなってどうしたいの?

完結に言うと 「LINEグループにBotを招待して、みんなで旅のしおりを作るサービスです」
これを使うことで、みんなが行きたい場所を取り入れながらそれらをすぐに可視化できます。
こうして、旅行計画を立てるきっかけを提供することが観光客の増加に繋がるのではないかと考えました。

このサービスの特徴

・観光情報は広島県/広島市のオープンデータを活用した。
広島県
広島市

・DBを操作するAPIを作ったりして、水平展開しやすい構成を作った

・実際に観光に行った人から感染症対策などが取られていて、安心できたかなどを聞くことができる。

開発体制

連絡

基本的にはハッカソンでオールだったので常にZoomを繋いでいました。
だから、分からない事や聞きたいことはいつでも聞ける体制を取っていました。

アイデア出し

Miroを使って、"社会の変化"が何で"必要とされるもの"は何なのかみたいなブレストから入りました。
スクリーンショット 2020-05-27 14.32.13.png
スクリーンショット 2020-05-27 14.33.00.png
スクリーンショット 2020-05-27 14.34.34.png

仕様策定

基本的に、HackMDに書くようにしました。APIだと以下のような感じです。
以下は一部の切り抜きですが、全機能のPOST/GETについて策定してくれました。
スクリーンショット 2020-05-27 14.40.21.png
スクリーンショット 2020-05-27 14.40.38.png

やりとり

基本的にはSlackに専用のチャンネルを作成し、そちらで行いました。
スクリーンショット 2020-05-27 14.55.33.png

開発構成/仕様技術

スクリーンショット 2020-05-27 15.01.01.png

AWSにBotのコードやらマップのページやらぽんぽん置いて完成させました。

作ったものについて

スクリーンショット 2020-05-27 15.11.13.png
※基本的に現在の開発段階ではグループでのイベントにしか対応していません。
※グループに招待→やりたいことを選択して記録する→みんなのやりたいことを確認する。というのが現在ユーザーに使っていただける機能になります。

実際に試して見る(現在止めてます)

スクリーンショット 2020-05-27 17.10.40.png

スクリーンショット 2020-05-27 16.03.34.png
スクリーンショット 2020-05-27 16.03.43.png

ぜひ一度試してみてください。APIの利用回数が異常だったので現在は利用を停止しています。

開発を終えて

今回作ってみたものはプロトタイプに過ぎません。実装したい機能がまだまだ沢山あるので、もし時間があるならばもう少し開発に時間を割いて、よりユーザー体験の良いサービスにしたいなと思いました。
開発の裏話ですが、徹夜ハッカソンだったものの、色々と個人がこだわっていたりして時間が足りなかったです。発表の1時間前までハッピーパスが通っていないと言うなかなかギリギリの戦いでした。なんとか作った即席のスライドで最終の審査に挑みました。
結果発表では奨励賞と言う形をいただき、2日間それぞれの力を合わせて開発したものがこのように目に見える形で評価を頂き嬉しい限りです。これからもすごくなりたいがくせいぐるーぷのメンバーでハッカソンなどに参加し、色々な技術に挑戦したいです。一緒に開発してくれた@shinbunbun_@ItyuJに感謝です。

後日得た教訓


後日、APIの利用利用料金がどうなってるんだろうとコンソールを見ると上のツイートのようにPlaceAPIに7000リクエストも入って請求が¥16,000を超えていました。幸い初心者クレジットで$300あったので負担してもらえたのですが、おそらく実際に開発で使ったのは2000ほど。どこかでシークレットキーがバレたのかな。。。
クラウド破産には気をつけてこれからも開発を続けて行きます。。

今後の展望

面白そう!私も観光業助けたいのでこのプロジェクトやりたいです!みたいな声が多くなると実際に動くかもしれないです。
APIやフロントエンドの技術的記事はメンバーの2人がこの後書いてくれるかもしれないです!

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