20190625のvue.jsに関する記事は19件です。

Vue.js の vm.$scopedSlots により slot 内部要素から値を受け取る

Vue.js の .$scopedSlots により slot 内部要素から値を受け取る

vm.$scopedSlots を使用することで SSR の結果より値を受け取る際に使えるかも・・・?しれません。

実装例

使用する slot 持ちコンポーネント

slot を持ち親コンポーネントに vm.$scopedSlot の実行結果を .$emit() するコンポーネントです。
vm.$scopedSlot の結果は VNode インターフェース の型で返ります。

slot-data.vue
<template>
  <div class="slot-data">
    <slot/>
  </div>
</template>

<script>
export default {
  mounted() {
    this.$scopedSlots['default']()
      .forEach(vn => {
        if (vn.data) {
          this.$emit('slot-data', vn.data)
        }      
      });
  },
};
</script>

親コンポーネントにおいて下記のように @slot-data として受け取ります。

App.vue
<template>
  <div id="app">
    <img width="10%" src="./assets/logo.png">
    <SlotData @slot-data="slotData">
      <input name="abc" value="1">
      <input name="def" value="taro">
    </SlotData>
  </div>
</template>

<script>
import SlotData from "./components/SlotData";

export default {
  name: "App",
  components: {
    SlotData
  },
  methods: {
    slotData(data) {
      console.log(data);
    }
  }
};
</script>

親コンポーネントは slotData により 受け取った data より
取得したいデータをそれぞれ処理するような形です。

もっといい方法はありそうですが・・・

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

Nuxtのsentryモジュールでエラーを検知するときの注意点

こんにちは、プログラミングスクールのレビューサイト「スクールレポート」を運営しているアカネヤ(@ToshioAkaneya)です。

Nuxtのsentryモジュールでエラーを検知するときの注意点

以下のようにtryの中で例外が発生する場合は注意が必要です。このとき、sentryで通知するにはthis.$sentry.captureException(e)が必要なことに注意してください。

デフォルトでは、Uncaught Exceptionにしかsentryは機能しないからです。

    try {
        const a = 1
        a()
      } catch (e) {
        this.$sentry.captureException(e) // これが必要!
      }

参考 公式 https://github.com/nuxt-community/sentry-module

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

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

[初心者向け]VueとExpressを使ってパラメーターの受け渡しをしてみよう

はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います.

はじめに

Vue.jsでaxiosを使ってパラメーターをpostしexpressで作った、apiで受取るというところまでやりたいと思います。

必要な環境

Vue.jsの実装

  • まずこちらの記事を参考にしてサーバーを立ち上げてください。
  • http://localhost:8080にアクセスして、このような画面が表示されれば成功です。 スクリーンショット 2019-06-25 20.11.51.png
  • 次にaxiosをインストール
    • npm install axios -s

画面を作る

  • 次にsrc/components/HelloWorld.vueをいかのように書き換えてください
HelloWorld.vue
<template>
  <div class="hello">
    <form action>
      <input type="text" placeholder="text" v-model="text">
      <input type="submit" value="decide" @click="submitClick">
    </form>
  </div>
</template>

画面はこのようになっています
スクリーンショット 2019-06-25 20.36.00.png

HelloWorld.vueの</template>以下に<script>を追加

HelloWorld.vue
<script>
import Axios from "axios";
export default {
  name: "HelloWorld",
  data() {
    return {
      text: null
    };
  },
  methods: {
    submitClick() {
      alert(this.text)
    }
  }
};
</script>

このようにinputに文字を入力してその文字がalertに表示されれば成功です
スクリーンショット 2019-06-25 20.42.23.png

<script></script>を書き換え

次に<script></script>を以下のように書き換えてください

HelloWorld.vue
<script>
import Axios from "axios";
export default {
  name: "HelloWorld",
  data() {
    return {
      text: null
    };
  },
  methods: {
    submitClick() {
      const body = {
        text: this.text
      };
      Axios.get(`http://localhost:3000/hoge/${this.text}`)
        .then(res => {
          console.log(res.data.name)
        })
        .catch(err => {
          console.log(err);
        });
    }
  }
};
</script>

Expressの実装

簡単なapiを作ってみる

  • アプリ名/app.jsに以下のことを追記しましょう
app.js
app.get('/hoge', (req, res) => {
  res.json({
    name: "hoge"
  })
});

スクリーンショット 2019-06-25 21.03.04.png

apiを書き換える

app.js
app.get('/hoge/:name', (req, res) => {
  let data = req.params
  console.log(data.name);
  res.json({
    name: data.name
  })
});

これでコードを書く部分はすべて終了です。

動かしてみる

  • http://localhost:8080にアクセスし任意の文字を入力し送信してください。

  • Expressを動かしているconsole上にfrontから送信した文字が表示されていれば成功です
    スクリーンショット 2019-06-25 21.25.26.png
    スクリーンショット 2019-06-25 21.25.38.png

最後に

初心者向けにVue.jsからExpressのapiへのパラメーターの受け渡しを行いました。
これから始める方の手助けになれば幸いです。
なにか間違いがあればご連絡ください。

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

[Vue]テキストをクリックすると入力欄に変わる、といった実装には$nextTickが便利な件

とりあえず成功したサンプル

See the Pen [Vue]テキストを押したら入力欄が現れて、かつフォーカスする(1) by riotam (@riotam4) on CodePen.

使い方

「ここをクリックしてね」自体をクリックしたら、入力欄が現れて、そのまま編集できる。
という想定で作ってみました。

失敗したコード

See the Pen [Vue]テキストを押したら入力欄が現れて、かつフォーカスする(2) by riotam (@riotam4) on CodePen.

問題点

入力欄は出せたけど、フォーカスができていません。

失敗コードの解説

HTML側
<div id="app">
  <p v-if="inputField"><input ref="focusThis" value="ここをクリックしてね"></p>
  <p @click="show" v-else>ここをクリックしてね</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
JS側
  new Vue({
    el: "#app",
    data: {inputField: false},
    methods: {
      show() {
        this.inputField = true;
        this.$refs.focusThis.focus();
      },
    },
  })

まず、「ここをクリックしてね」テキストをクリックすると、v-on:clickディレクティブによって、showメソッドが発火されます。
showメソッドでdataのsがtrueに変更され、すぐにthis.$refs.focusThis.focus();が発火し、ref="focusThis"のついた要素を探し、そこにフォーカスします。

一方、その頃HTML側では、inputFieldがtrueになったことで、HTML側のv-ifもtrueとなり、DOM操作によりinputタグが表示されます。
それと同時に、「ここをクリックしてね」テキストはv-elseなので消え。テキストと入力欄が入れ替わります。

原因

参考サイト等を調べた結果、上の動きに間違いは無いのですが、DOM操作のタイミングがやや遅いことが原因と考えられます。
つまり、inputタグが表示されるより先に、this.$refs.focusThis.focus();が実行されてしまうことで、フォーカスできないという問題が発生します。

成功したコードの解説

JS側
  new Vue({
    el: "#app",
    data: {inputField: false},
    methods: {
      show() {
        this.inputField = true
        this.$nextTick(() => this.$refs.focusThis.focus())
      },
    },
  })

基本的には失敗したコードと同じですが、ここでは$nextTickというものを使っています。
以下に説明していきます。

$nextTickとは

$nextTickは、Vue公式インスタンスメソッド(ライフサイクル)です。公式には小難しく書かれていますが、とても簡単です。
要は、this.$nextTick()の引数の中に、関数の形で実行したいコードを入れているだけです。
(ちなみに、() => this.$refs.r.focus()これはアロー関数の書き方です。その記事についてはこちら
これによって、this.$refs.focusThis.focus()はDOM操作が終わってから実行されるようになります。

さいごに

今回は非同期処理的な実装によく使うであろう、$nextTickを中心に説明させて頂きつつ、サンプルコードの共有を致しました。
最後までありがとうございました。

参考にしたもの

参考サイト①
参考サイト②

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

Array.mapが分からないからArray.mapもどきを作ってみた

業務中ワイ

ワイ「なあ、ハスケル子ちゃん」

ハスケル子「はい」

ワイ「今、ハスケル子ちゃんの書いたJSのコードを見てたんやけど」
ワイ「これどういう意味?」

const nodes = document.querySelectorAll(".list > li");
const nodesArray = Array.from(nodes);
const nodesHeightArray = nodesArray.map(getHeight);

ハスケル子「えっと、うーん・・・」
ハスケル子「(見ての通りなんだけど、逆にどこが分からないんだろう・・・)」
ハスケル子「どこが分かりにくかったですか?」

ワイ「このnodesArray.mapいうメソッドは何なん?」

ハスケル子「
haskellko.jpeg

ハスケル子「なんでそんなことが分からないんだろう・・・

ワイ「お、おい・・・心の声が出てもうてるで・・・

ハスケル子「(す、すいません!)」

ワイ「なんで謝罪は心の声になっとんねん
ワイ「どんな忖度や
ワイ「判断基準どうなってんねん

ハスケル子「ごめんなさい」
ハスケル子「お詫びにmapメソッドについて説明しますね」

mapメソッドとは

ハスケル子「mapメソッドは、配列が持ってるメソッド1です」
ハスケル子「ある配列を元に、新しく別の配列を作ることができるメソッドですね」
ハスケル子「さっきのコードでいうと───」

const nodes = document.querySelectorAll(".list > li");

ハスケル子「↑まずquerySelectorAllで、いくつかのli要素を取得して」
ハスケル子「それをnodesとします」

const nodesArray = Array.from(nodes);

ハスケル子「↑次にnodesArray.fromメソッドで配列に変換します」
ハスケル子「nodes配列みたいだけど配列じゃない2ですからね」
ハスケル子「そしてnodesから作った配列nodesArrayの───」

const nodesHeightArray = nodesArray.map(getHeight);

ハスケル子「───mapメソッドを実行します」
ハスケル子「nodesArrayの各要素に対して何らかの処理を施して」
ハスケル子「新しくnodesHeightArrayという配列を作る、って感じです」
ハスケル子「その何らかの処理というのを、関数として渡します

ワイ「なるほどな」
ワイ「getHeightが関数なんやね」

ハスケル子「はい」

const getHeight = element => element.clientHeight;

ハスケル子「getHeight関数の中身は↑これです」

ワイ「elementという引数を1つ受け取ってelement.clientHeightを返すということは」
ワイ「要素の高さを返す関数やな」

ハスケル子「はい」
ハスケル子「さっきのコードを実行すると、最終的にnodesHeightArrayには」

[24, 48, 72, 120, 24, 72, 48]

ハスケル子「↑という配列が入ることになります」

ワイ「ほえ〜、DOM要素のリストを元に、各要素の高さを集めた配列を作れるんか」
ワイ「たまに便利そうやな」

ワイの気になったこと

ワイ「でもハスケル子ちゃん」

const getHeight = element => element.clientHeight;

ワイ「↑このgetHeightの引数であるelementどこで渡してんの?

ハスケル子「どこで渡すっていうか、勝手に入ってくるんです」

ワイ「え・・・勝手に入ってくる・・・?」
ワイ「空き巣みたいに・・・?

ハスケル子「やめ太郎さんも、自分でmapメソッドを実装してみたら分かりますよ」
ハスケル子「やってみましょう」

Object.defineProperty(Array.prototype, "myMap", {
  value: function(callback) {
    /* ここに処理内容を書く */
  }
});

ハスケル子「↑こうやって」
ハスケル子「Object.definePropertyメソッドを使うことで」
ハスケル子「配列にmyMapというメソッドを追加できるんです3

ワイ「おお、そんな機能があるんか」
ワイ「ほなちょっと挑戦してみるわ」
ワイ「でも・・・このcallbackいう引数はどこから入ってくんの」

ハスケル子「だから・・・」
ハスケル子「その辺を理解するために今からコードを書いてもらうんです」
ハスケル子「再帰的な質問をしないでください
ハスケル子「もうとりあえず空き巣が勝手に入ってくるとでも思っておいてください」

ワイ「お、おう・・・」

ハスケル子「配列のmapメソッドには、関数を1つ渡して実行しましたよね?」

ワイ「ああ、さっきしてたな」

ハスケル子「そのときmapメソッドに渡した関数が」
ハスケル子「今から書く関数のcallbackという引数として入ってくる・・・」
ハスケル子「そんなイメージでmyMapメソッドも実装してみてください」

ワイ「なるほど、やってみるで」

Object.defineProperty(Array.prototype, "myMap", {
  value: function(callback) {
    const resultArray = []; // まず空の配列を作成
    /* ここで配列に色々する */
    return resultArray; // その配列を戻り値として返す
  }
});

ワイ「まずは↑こんな感じやな」
ワイ「空の配列を作って、なんか色々して、最終的にその配列を返す、と」

ハスケル子「いいですね」

ワイ「ほんで、元々の配列の各要素に対して何らかの処理をしていけばいいんやな」
ワイ「そうか」
ワイ「その何らかの処理が、callbackとして受け取った関数に入ってるというテイで考えればいいんやな」

ハスケル子「そうです」
ハスケル子「ちなみに今書いてる関数の中ではthisって書けばその配列自身にアクセスできますよ」

ワイ「なるほど、thisが配列自身やな」
ワイ「ほな、配列の分だけfor文で回して───」

for (let i = 0; i < this.length; i++) {
  const newElement = callback(this[i]); // i番目の要素にcallbackを適用。
  resultArray.push(newElement); // それを「戻り値として返す配列」に追加。
}
return resultArray;

ワイ「↑こうやな!」

ワイ「this[i]、つまり配列のi番目の要素に対してcallback関数を実行して」
ワイ「それをnewElementとして配列にpushする」
ワイ「そして出来上がった配列resultArrayを戻り値として返す」

ハスケル子「いいですね!」

ワイ「よっしゃ、じゃあワイのmyMapメソッド試してみるで!」

const nodesHeightArray = nodesArray.myMap(getHeight);
console.log(nodesHeightArray);

// コンソール結果: [24, 48, 72, 120, 24, 72, 48]

ワイ「おお、さっきのmapメソッドと同じ結果や!」

ハスケル子「いい感じです」
ハスケル子「実際のmapメソッドはもっと複雑なアルゴリズムですけど」
ハスケル子「イメージ的はこんな感じです」

ワイ「なるほどな〜」
ワイ「mapメソッドに対して渡したgetHeightいう関数を」
ワイ「mapメソッドの中で、配列の各要素に対して実行してくれてたんやな」
ワイ「そのときにthis[i]的な感じで」
ワイ「callback関数の引数として、元の配列の各要素を渡してくれてたんやな」
ワイ「せやから引数が勝手に入って来てたんか〜」
ワイ「謎が解けましたわ〜」
ワイ「ありがとう、ハスケル子ちゃん」

ハスケル子「実際のmapメソッドは、callback関数を実行するときにthis[i]だけじゃなく」
ハスケル子「ithisも渡してくれるので、さらに色々できますよ」

ワイ「なるほどな」
ワイ「第二引数に今何番目の要素かを表すインデックス」
ワイ「第三引数には配列自身が入ってくると」
ワイ「工夫次第で色々できそうやな」
ワイ「array[i - 1]で一つ前の要素を見たりとかな」

ハスケル子「そうですそうです」

コールバック関数について

ワイ「今みたいな感じで、関数を渡して使うタイプの関数を」
ワイ「自分で作ってみてもオモロそうやな〜」
ワイ「例えば共通化できそうな処理があったとして」
ワイ「ほとんどの処理を関数として共通化できるけど」
ワイ「最後にちょっとだけ挙動を変えたい」
ワイ「しかも何パターンかの挙動があるから、引数で何かパラメータを受け取ってやる方法やと」
ワイ「if文が増えすぎて見通しが悪くなる・・・そんな時には」
ワイ「関数の最後にこの処理をやってくれい!
ワイ「っていう処理の内容を、コールバック関数として受け取って」
ワイ「最後に実行してやればええんやな〜」
ワイ「そうすれば」
ワイ「同じ関数を使うけど、一部だけ自由に挙動を変えることが出来んねやな」

ハスケル子「だんだん中級者っぽくなってきましたね」

ワイ「(いや、君よりだいぶJS歴長いねんけどな・・・)」

Object.definePropertyについて

ワイ「あと、Object.definePropertyすごいな」

ハスケル子「Vue.jsなんかも、このObject.definePropertyで」
ハスケル子「Arraypushメソッド等を上書きすることで」
ハスケル子「配列に要素を追加しただけDOMが更新されるようにしてるんですよ」

ワイ「せやろな〜(まじか!)」
ワイ「よっしゃ、ワイも明日から配列にメソッド追加しまくるで〜!」
ワイ「あ、メソッドの上書きもできるんやったな!」

ハスケル子「レッツ、プロトタイプ汚染っ!

社長「(ハスケル子ちゃん、そこはちゃんと止めてや・・・!)」

そうこうしてるうちに18時

ワイ「やば、ワイがコード読めへんせいでもう定時やないかい」
ワイ「コーディング全然進んでへん・・・」

Riot兄さん「俺に任しとけ

ワイ「あ、兄貴・・・!」

Riot兄さん「俺が界王拳2倍でコーディングして、なんとかしたるわ」
Riot兄さん「よっしゃ、今日から毎日7時間残業するで〜!」

社長「おっ、兄貴の物理界王拳やな!」

ハスケル子「物理界王拳・・・?(兄貴、すごい・・・///)」

社長「せや」
社長「ただし物理界王拳は3倍までしか使えへん」
社長「24時間超えてしまうからな」

ワイ「物理界王拳て」
ワイ「ただのブラック会社4やないかい!

〜おしまい〜

追記

ワイ「っていうかホンマの界王拳も物理やろ!


  1. 実際にはArray.prototypeのメソッドとして定義されてるけど、きっとやめ太郎さん分からないから・・・ 

  2. ノードリストですね。 

  3. グローバルなオブジェクトに手をつけることになるので、むやみにやらない方がいいと思います。 

  4. 実際には残業はほぼありません。(フロントエンドチーム以外は知りません) 

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

[Vue]<li>をループで回してつくった箇条書きに、十字キーでフォーカスを合わせてる風の挙動をさせる(+enterキーでその要素を取得する)

前回の記事

前回の記事からの続きになります。
まだ読まれていない方は、こちらからどうぞ。

今日のサンプルコード

See the Pen [Vue]ループ<li>のフォーカス移動+選択 by riotam (@riotam4) on CodePen.

サンプルコードのみですが、こちらです。
入力欄にフォーカスした状態で、下キーを押すと、その下の箇条書きのところを、フォーカスしているような挙動をします。
enterキーを押すと、ダイアログ画面でその要素を受け取り、出力します。

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

Netlify Functions を使って CORS エラーを回避する

はじめに

現在 Web アプリを Nuxt で開発していて、nuxt generateで静的コンテンツを作成し、Netlify にホスティングしている
そのアプリでは、別ドメインの API にリクエストを投げてデータを取得しているので、CORS ポリシーでブロックされないようにproxy-moduleを使って API と同一ドメインになるようにプロキシしている

nuxt.config.js
modules: ['@nuxtjs/axios', '@nuxtjs/proxy'],
proxy: {
  '/atnd': {
    target: 'http://api.atnd.org/events',
    pathRewrite: {
      '^/atnd': '/'
    }
  },
  '/connpass': {
    target: 'https://connpass.com/api/v1/event',
    pathRewrite: {
      '^/connpass': '/'
    }
  }
}

ただし、上記の proxy モジュールによる書き換えはサーバーが必要で、generate 機能を使って静的コンテンツをホスティングしている場合は機能しない
ちなみにローカルでは webpack-dev-server などを使って、Node サーバー上で動かしていれば、ちゃんとプロキシされる

この辺のことは以下に詳しく書いてある
https://github.com/nuxt-community/proxy-module/issues/1

Nuxt の generate 機能を使っていても、ちゃんと別ドメインの API が叩けるようにするというのが今回の開発の目的です

作業リポジトリはこちら
https://github.com/kurosame/event-search

Netlify Functions を使う

ブラウザには別ドメインへの通信を拒否する仕組みが標準で備わっている
Nuxt のプロキシ機能はサーバーが無いと使えないし、API 側にオリジンを許可させることもできない

これらのことを踏まえて、今回は Netlify Functions を使って Lambda 関数内で API リクエストを実行することで CORS エラーを回避することにした

Netlify Functions の実行環境は AWS Lambda だが、AWS 側の設定は一切やらなくて良い
てか AWS アカウントも不要

Netlify のアカウントがあれば使えるが、フリープランだと以下の制限がある

  • 125000 リクエスト/月
  • 100 時間/月

言語は JS と Go をサポートしている
(本記事では JS で実装している)

ローカルで動作確認

以下のモジュールをインストール

yarn add -D netlify-lambda

設定ファイルを用意する

netlify.toml
[build]
  functions = "functions/dist"

Lambda にやらせたい処理を書いて、handler 関数を export

functions/src/sample.ts
export async function handler(event, context) {
  return {
    statusCode: 200,
    body: 'Hello, World'
  }
}
package.json
"scripts": {
  "lambda": "netlify-lambda serve functions/src"
},
yarn lambda

以下のようにリクエストする
http://localhost:9000/sample

Functions から connpassAPI を叩いてみる

以下のコードが Functions 関数の実装
API の仕様上、100 件以上レスポンスが存在していても、MAX100 件しかデータが取れないので再帰関数にしているが、やっていることは API から取ってきたデータを body に JSON.stringify にして渡しているだけです
IConnpassEventResponse と IConnpassResponse の型インターフェースは後述してます

yarn add -D @types/aws-lambda
functions/src/connpass.ts
import { IConnpassEventResponse, IConnpassResponse } from '@/store/connpass'
import axios, { AxiosResponse } from 'axios'
import { APIGatewayProxyEvent } from 'aws-lambda'

export async function handler(event: APIGatewayProxyEvent) {
  const ymd = (event.queryStringParameters || { period: '' }).period
  const count = 100
  const getEvents = (
    events: IConnpassEventResponse[] = [],
    start: number = 1
  ): Promise<IConnpassEventResponse[]> =>
    axios
      .get('https://connpass.com/api/v1/event', {
        params: { ymd, start, count }
      })
      .then((res: AxiosResponse<IConnpassResponse>) =>
        res.data.results_returned === count
          ? getEvents([...events, ...res.data.events], start + count)
          : [...events, ...res.data.events]
      )

  const events: IConnpassEventResponse[] = await getEvents()

  return {
    statusCode: 200,
    body: JSON.stringify(events)
  }
}

TS で書いた場合は、以下を functions ディレクトリに追加

yarn add -D @babel/preset-typescript
functions/.babelrc
{
  "presets": ["@babel/preset-typescript", "@babel/preset-env"],
  "plugins": [["@babel/plugin-transform-runtime", { "regenerator": true }]]
}

基本的に presets だけでコンパイルエラーは回避できると思うが、plugins はエラーになったらそのエラー名で検索するとたぶん必要なプラグインが分かると思うので、必要に応じて設定する
作った後に気づいたけど、ここに書いてた

yarn lambda

正常に起動したら、以下にアクセス
http://localhost:9000/connpass

たぶん Lambda 側でタイムアウトする(デフォルト 10 秒)ので、日付で絞るかタイムアウト時間を伸ばすかしてみてください
http://localhost:9000/connpass?period=20190624
or
yarn lambda -t 20

Nuxt から Functions にリクエストする

Vuex のアクションで axios を使って Functions にリクエストしている

?period=${period}の書き方について
axios.getの param オプションを使うと、Functions 側の event 引数に入らなかったので、直接 URL にパラメータを書いている

store/connpass.ts
import { IEventState } from '@/store/events'

export interface IConnpassEventResponse {
  title: string
  catch: string
  description: string
  event_url: string
  started_at: string
  ended_at: string
  limit: number
  address: string
  place: string
}

export interface IConnpassResponse {
  results_returned: number
  events: IConnpassEventResponse[]
}

export const actions = {
  async getConnpassEvents({ commit }, period: string) {
    const events: IConnpassEventResponse[] = await (this as any).$axios.$get(
      `/connpass?period=${period}`
    )

    // Functionsのレスポンスを色々加工してStoreに入れている
    // (ここから以下は本記事の目的とは関係ない部分)
    commit(
      'events/setEvents',
      events
        .filter((e: IConnpassEventResponse) => e.limit >= 30)
        .map(
          (e: IConnpassEventResponse) =>
            ({
              title: e.title,
              catch: e.catch,
              description: e.description,
              eventUrl: e.event_url,
              startedAt: (this as any)
                .$moment(e.started_at)
                .format('YYYY-MM-DD HH:mm:ss (ddd)'),
              endedAt: (this as any)
                .$moment(e.ended_at)
                .format('YYYY-MM-DD HH:mm:ss (ddd)'),
              address: `${e.address} ${e.place}`
            } as IEventState)
        ),
      { root: true }
    )
  }
}

ローカルで netlify-lambda を使って起動するとポート 9000 で実行される
Nuxt はポート 3000 で起動しているので、以下のようにプロキシしないと CORS に引っかかる

Netlify にホスティングして動かす場合は、Nuxt と Functions は同じドメイン上で動くので、プロキシは不要
(そうじゃないと今回 Functions を使った意味がないので、、)

nuxt.config.js
proxy: {
  '/connpass': {
    target: 'http://localhost:9000'
  }
}

最後にアクションを Dispatch する

components/Summary.vue
this.$store.dispatch('connpass/getConnpassEvents', '20190624,20190625')

動作確認
ローカルで Lambda を起動して、Nuxt でアクションを Dispatch する

yarn lambda -t 100

ホスティングして動作確認

netlify.toml がルートディレクトリに存在する場合、Netlify の管理画面上の設定は無視される
その場合、netlify.toml にデプロイ設定を書く必要がある

netlify.toml
[build]
  command = "yarn generate"
  functions = "functions/dist"
  publish = "dist"

Netlify にホスティングすると、Functions はhttps://[Site name].netlify.com/.netlify/functionsというパスになるので、axios の baseURL を Functions の URL にする

nuxt.config.js
axios: { baseURL: '/.netlify/functions' },
proxy: {
  '/.netlify/functions/connpass': {
    target: 'http://localhost:9000'
  }
}

ちなみに netlify-lambda を使ってローカルでビルドした場合はhttp://localhost:9000http://localhost:9000/.netlify/functionsのどちらでも良いので、上記の proxy 設定でアクセスできる

ただし、axios の baseURL を Functions に固定するのは後々不便になるかもしれない
その場合は、環境変数を使って対応した方がいいかも

後は Netlify 上で GitHub リポジトリを紐づけて、その GitHub リポジトリのブランチに push すればホスティングされて動作するはずです

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

Nuxtでビルド時にAPIを静的化して、完全にサーバーへのリクエストをなくすト

ビルド時にAPIを静的化するジェネレータを自作しましたので、ご紹介です。(コード有りです。)

最近はやりのヘッドレスCMSをやっていこうと思ったのですが、APIのリクエスト制限きびしいんですよね...。

現状だとNuxtは静的generateモードでも、ページ遷移時にリクエストが発生してしまうので、完全に静的化してしまう必要がありました。

以下のような流れになります。

  • Nuxtビルド時にJSONを生成
  • <link rel="prefetch"> にJSONを追加
  • ルート生成
  • コンポーネントからは /_nuxt/articles/xxx.json にリクエスト

<link rel="prefetch"> の指定で、生成したJSONは前もって準備するようにしています。)

ジェネレータの中身は以下。

modules/generator.js
import axios from 'axios'

module.exports = function generateModule(moduleOptions) {
  this.nuxt.hook('build:before', async ({ app }) => {

    // 全データを取得
    const { data } = await axios.get('/hogehoge')
    const posts = data

    // JSONを生成
    this.options.build.plugins.push({
      apply(compiler) {
        compiler.plugin('emit', (compilation, cb) => {
          posts.forEach(post => {
            compilation.assets[`articles/${post.slug}.json`] = {
              source: () => JSON.stringify(post),
              size: () => {}
            }
          })
          cb()
        })
      }
    })

    // link rel="prefetch"にJSONを追加
    const url = this.options.dev ? '' : 'https://xxx'
    this.options.head.link = [
      ...this.options.head.link,
      ...posts.map(post => ({
        rel: 'prefetch',
        href: `${url}/_nuxt/articles/${post.slug}.json`
      }))
    ]

    if (this.options.dev) return

    // ルート生成
    this.options.generate.routes = posts.map(post => `/${post.slug}`)
  })
}

こんな感じにモジュールを書いて、nuxt.config.js で設定します。

nuxt.config.js
module.exports = {
  // ジェネレータ内で開発orプロダクションの書き分けに使用しています
  dev: process.env.NODE_ENV !== 'production',

  // モジュールを指定
  modules: ['~/modules/generator'],

  ...
}

コンポーネントからは以下のようにデータを取得します。

pages/_id.vue
<script>
export default {
  async asyncData({ app, params }) {
    const url = process.env.NODE_ENV === 'development' ? '' : 'https://xxx'
    const { data } = await app.$axios.get(
      `${url}/_nuxt/articles/${params.id}.json`
    )
    return { article: data }
  }
}
</script>

これで表示・遷移時ともにサーバーへデータ取得のリクエストを送らずにすむようになりました?

こちらの情報を参考にさせていただきました。
Firebase、Flamelink、Nuxt、Netlify、PWAを使ってJAMstackなブログを作る

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

Vue.jsとExpressを使って画像アップロードをしてみる

はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

はじめに

Vue.jsからリクエストを投げてExpressのpublicディレクトリに画像をアップロードしてみたいとおもいます。
今回画像アップロードにはmulterというライブラリを使って行きます。

実装

使用するライブラリのインストール

  • axios
    • npm install axios -s Vueプロジェクトの中でinstallしてください
  • multer
    • npm install multer -s Expressプロジェクトの中でinstallしてください

Vueの実装

まず一つvueファイルを作ってください

Uplord.vue
<template>
  <div>
    <form>
      <input type="file" id="file" v-on:change="onFileChange">
      <input type="submit" value="decide" @click="submitClick">
    </form>
  </div>
</template>
<script>
import Axios from "axios";
export default {
    data() {
        return {
            imageFile: null
        };
    },
    methods: {
        //画像が選択されたとき呼ばれる
        onFileChange(e) {
            this.imageFile = e.target.files || e.dataTransfer.files;
        },
        //submitされたときに呼ばれる
        async submitClick() {
            try {
                const formData = new FormData();
                formData.append("file", this.imageFile[0]);
                const config = {
                    headers: {
                        "content-type": "multipart/form-data",
                    }
                };
                let res = await Axios.post("/image", formData, config);
                console.log(res);
                if (res.data.status === "error") {
                    alert(res.data.error);
                } else {
                    alert("登録完了")
                }
            } catch (error) {
                alert("画像の送信に失敗しました");
            }
        }
    }
}
</script>
  • ここではformDataを使って画像をpostしています。
  • formData.append("key",value)を使うことでkey:valueを対応させる形で任意のデータを乗せることが出来ます。

注意点

  • valueにはオブジェクトなどは乗せることが出来ません。基本的にkeyとvalueを一つずつ載せてください。
  • formDataに載せたvalueはStingになってしまいます。

Expressの実装

src/index.jsに以下を書いていきましょう

index.js
const express = require('express')
const multer = require('multer');
const app = express()
const storage = multer.diskStorage({
    // ファイルの保存先を指定(今回はsrc/public/image以下に画像を保存)
    destination: function (req, file, cb) {
        cb(null, 'src/public/image')
    },
    // ファイル名を指定(オリジナルのファイル名を指定)
    filename: function (req, file, cb) {
        // Math.random().toString(36).slice(-9)で乱数を生成
        const imageName = `${Math.random().toString(36).slice(-9)}_${Date.now()}.png`
        cb(null, imageName)
    }
})

const upload = multer({
    storage: storage
}).single('file')

app.post('/image', (req, res) => {
    upload(req, res, (err) => {
        if (err) {
            //アップロード失敗した場合
            res.json({
                status: "error",
                error: "fail to uplord image"
            })
        } else {
            //アップロード成功した場合
            res.json({
                status: "sucess",
                // ファイル名を返す
                path: res.req.file.filename
            })
        }
    })
});
  • const storageで画像の保存先をファイル名を指定
  • ここではファイル名を乱数と時間を組み合わせていますがこれではユニークとは言えません。ユニークにしたい方はuserIdと時間を組み合わせるなどしてください。
  • /imageのエンドポイントでuplordを呼び出しています。ただアップロードするだけでしたらもっと簡単に出来ます。(詳しくはこちら)しかしここではアップロード出来たかどうかのエラーハンドリングを行いたいのでこのような形になっています。
  • アップロードされた画像のファイル名はres.req.file.filenameで取得する事ができます。
  • 無事フロントでres.data.statussucsessになっていれば登録完了です。

最後に

multeraxiosでお手軽にVueからExpressに画像をアップロードすることが出来ました。
機会があれば是非試してみてください
なにか間違いなどありましたら教えていただけると幸いです。

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

Vue.jsでオブジェクトのネストしたプロパティーにアクセスできないとき

以下のようなオブジェクトがありまして、この中のmembersの値を取得したかったのですが、、、

  var data = {
    [50]: {
      members: {
        0: {
          id: "4",
          name: "谷裏"
        },

        1: {
          is: "5",
          name: "眉村"
        },
        length: 2
      },
      facilities: "プール"
    },
  }

通常であれば以下のようなコードで取得できるはずなのですが

console.log(data[50].members.length);

一向に取れず、、、

自分の書き方がおかしいのか?っと調べてみてもあっている様子、、、
いちいちビルドし直すと時間がかかるので、ブラウザ上で簡単にコードを試したりすることができる

https://playcode.io/

でコードがあっているか試してみたところ、間違っていない様子、、、

っと思って改めて調べたところ

以下の記事と同じ現象であることが確認できました
https://codeday.me/jp/qa/20190403/539522.html

[ネストしたプロパテイーにアクセスする方法]

この記事に書いてあることは、データのロードが完了するのを待ちなさいとのことでした。

データを取得している場所にフラグのようなものを立てて、ロード完了後表示してデータを取得しなさいということでした!

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

Vueで複数条件(and,or,フリーワード)の絞り込みを実装する

まずはサンプル

https://kackie.github.io/sort/vue.html

こちらの記事で作ったお店のリストを改良しましたw
今回は管理するデータがより複雑になったので、データはjson形式でまとめてaxiosで読み込んでいます。

絞り込みの条件

グループ化されたカテゴリがあり、同グループ内を複数選択した場合はor(和食||洋食)、グループを跨いで選択した場合はand(和食||洋食&&肉)での絞り込みになります。
またフリーワード検索を作り、これもフリーワード&&選択したカテゴリというふうにandで絞り込まれます。

jQuery版との比較

データバインディングの面でVueが圧倒的に優れています。さすが。
そのおかげでjsのソースもシンプルに書くことができました。
必然的にオブジェクト指向に則って書いていくので、ソースが整理されて見やすくわかりやすいです。
またVueのtransitionクラスを利用して切り替えのインタラクションを簡単に作ることができました。

今後実装したいもの

  • URLにパラメータ付与
    jQuery版にあったやつ。管理する情報が増えたので書き方を新しく考えないと..。
  • 時間による絞り込み
    開店時間で絞り込んだり、早い順でソートしたりできればよいかなと。
    インターフェースはプルダウン?
  • json更新のシステム
    手動でもいいけど、PHP等の勉強用に作ってみたい。
  • 複数キーワードでの検索
    今は単一キーワードでの絞り込みしかできないので、スペース区切りで複数入力された場合の処理が必要。
    • その場合はand検索のほうが良いか?
  • 非表示時のインタラクション
    これはどうしようもないかもしれないけど、非表示のものが左上に集約されていくのがちょっと気持ち悪いw
    おそらくflex配下の要素にposition:absolute;を指定すると親要素基準で左上に移動してしまうせい(初めて知った)。
    flexをやめると高さ揃えをjsでやることになるしうーん。

感想

jsonを手動で作成し管理するなんてことを初めてやりましたが、このインターフェースを組み合わせれば色々できそうですね。
何かの実績を整理してみたり、読んだ本や見た映画をまとめてみたり、料理のレシピを材料やジャンル別でカテゴライズしてみたり...。
思いついたら自分用にでも使ってみようと思います。

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

JestでVueコンポーネントとVuexストアの単体テストを書いてみよう!

概要

この記事は、Ginza.js#2にて登壇した、
「Jestを使って VueコンポーネントとVuexストアのテストコードを書いてみよう!」
のLTのスライド内容を元に記事を作成しました。

スクリーンショット 2019-06-24 13.12.00.png

ソースコードは以下で公開しています。
https://github.com/karamage/vue_jest_test

動作環境
・Mac OS 10.14.4
・Nuxt v2.8.1
・Vue v2.6.10
・Node v10.15.3

この記事で説明すること

  • Jestとは?
  • Jestのインストール
  • Jestの基本的なテストの書き方
  • Vueコンポーネントの単体テスト
  • Vuexストアの単体テスト

Jestとは?

  • Facebook製のJavaScriptテストプラットフォーム
  • JavaScriptテスト界のシェア率ナンバー1

Jest公式ページ
vue-test-utils公式ページ

Jestの特徴

  • ブラウザの起動がないぶん軽快に動く(DOMのエミュ)
  • スナップショットテスト(仮想DOMをJSONでダンプして差分比較)ができる
  • テストに必要な機能全部入りで楽(テストランナー、アサーション、モック、カバレッジ)
  • 設定一つでカバレッジを簡単に取得できる
  • ウォッチでファイル変更時に依存関係のあるテストだけ走る。賢い。

便利すぎて使わない理由がない!

vue-test-utils

  • vueのテスト時に便利なUtil
  • vueコンポーネントのmount処理してくれる

Jest と vue-test-utils インストール

$ yarn add --dev jest vue-jest babel-jest @vue/test-
utils babel-preset-vue-app

まずはyarnかnpmでパッケージをインストールしましょう

Jest 設定ファイル記述

package.json
 "scripts": {
 ...,
 "test": "jest"
 },
.babelrc
{
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "node": "current"
            }
          }
        ]
      ]
    }
  }
}
jest.config.js
module.exports = { moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1', '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js'
  },
  moduleFileExtensions: ['js', 'vue', 'json'],
  transform: {
    '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest',
  },
  "collectCoverage": true, "collectCoverageFrom": [
    "<rootDir>/components/**/*.vue",
    "<rootDir>/pages/**/*.vue"
  ]
}

Jest 実行!

$ yarn test

しかし、Bebelのエラーがでて動かない。。。
Cannot find module 'babel-core' が出てしまう。

スクリーンショット 2019-06-10 21.09.53.png

泣きそうになる

babel-coreのバージョンを調整したらイケた!

$ yarn add --dev babel-jest babel-core@^7.0.0-0 @babel/core

以下のisuueを参考にした
https://github.com/vuejs/vue-jest/issues/160
https://github.com/facebook/jest/issues/5525
スクリーンショット 2019-06-10 21.15.26.png

Jestでテストコードを書いてみよう

足し算の関数があるとして

logic/sum.js
export function sum(x, y) {
  return x + y
}

テストコードは以下のように書く

test/sum.test.js
import { sum } from "@/logic/sum"

test("1 + 2 = 3", () => {
  expect(sum(1, 2)).toBe(3)
})

Jestの基本

  • test(name, fn)で単一のテストを表す。
  • it(name, fn)でも同じ意味。rspec的
  • expect(value) でテスト対象の値を入れる
  • toBe(value) で結果の値の検証を行う

Vueコンポーネントのテストを書いてみよう(Vuex使わない場合)

テスト対象のVueコンポーネント

components/Counter.vue
<template>
  <div>
    <span class="count">{{ count }}</span>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

スクリーンショット 2019-06-21 10.04.19.png
カウンターコンポーネント(ボタンを押すとカウントアップする)

Vueコンポーネントのテストコード

test/Counter.spec.js
describe('Counter', () => {
  // コンポーネントがマウントされ、ラッパが作成されます。
  const wrapper = mount(Counter)

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // 要素の存在を確認することも簡単です
  it('has a button', () => {
    expect(wrapper.contains('.count')).toBe(true)
  })

  // ボタンを押してカウントアップするテスト
  it('button click should increment the count', () => {
    expect(wrapper.vm.count).toBe(0)
    const button = wrapper.find('button')
    button.trigger('click')
    expect(wrapper.vm.count).toBe(1)
  })
})
  • mount()でテスト対象のVueコンポーネントをマウントしてラッパーを作成する
  • wrapper.html()でhtmlの中身を文字列としてチェックできる
  • wrapper.contains()でCSSセレクタを使って要素の存在を確認できる。しかし、CSSに依存したテストを書くのはよくない。data-test属性を使ったほうが良い
  • Vueコンポーネントのdataは、wrapper.vm.countで確認できる。ボタンをクリックした際にcountが1アップしているのを確認する

VueコンポーネントとVuexストアの単体テストを書こう

テスト対象

Vuexストア(テスト対象)

store/count.js
export const state = () => ({
  count: 0
})

export const mutations = {
  setCount:  (state, { count }) => state.count = count
}

export const getters = {
  count: state => state.count,
}

export const actions = {
  async increment({ commit, state }, {}) {
    commit("setCount", { count: state.count + 1 })
  },
}

Vueコンポーネント(テスト対象)

components/CounterVuex.vue
<template>
  <div>
    <span class="count">{{ count }}</span>
    <button @click="increment">Increment</button>
  </div>
</template>

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

export default {
  computed: {
    ...mapGetters("count", ["count"])
  },
  methods: {
    ...mapActions("count", ["increment"])
  }
}
</script>

テストコード

Vuexストアの単体テスト

test/store/count.spec.js
import Vuex from 'vuex'
import * as count from '@/store/count'
import { createLocalVue } from '@vue/test-utils'

const localVue = createLocalVue()
localVue.use(Vuex)

let action
const testedAction = (context = {}, payload = {}) => {
  return count.actions[action](context, payload)
}

describe('store/count.js', () => {
  let store
  beforeEach(() => {
    store = new Vuex.Store(count)
  })
  describe('getters', () => {
    test('countの値を取得', () => {
      store.replaceState({
        count: 3
      })
      expect(store.getters['count']).toBe(3)
    })
  })
  describe('actions', () => {
    let commit
    let state
    beforeEach(() => {
      commit = store.commit
      state = store.state
    })
    test('increment', async done => {
      action = "increment"
      await testedAction({ commit, state })
      expect(store.getters['count']).toBe(1)
      await testedAction({ commit, state })
      expect(store.getters['count']).toBe(2)
      done()
    })
  })
})
  • 他のテストに影響を与えないようにcreateLocalVue()を使う
  • replaceStateでstateを置き換えたときにgetterで取得できているか確認する
  • vuexのactionは非同期になるので、非同期のテストは async done { で書き始めて、最後にdone()を呼ぶのを忘れないようにする

Vueコンポーネントの単体テスト

test/CounterVuex.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import CounterVuex from '@/components/CounterVuex'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('CounterVuex.vue', () => {
  let store
  let countStoreMock
  let wrapper

  beforeEach(() => {
    //Vuexストアのモックを作成する
    countStoreMock = {
      namespaced: true,
      actions : {
        increment: jest.fn(),
      },
      getters : {
        count: () => 0,
      },
    }
    store = new Vuex.Store({
      modules: {
        count:countStoreMock
      }
    })
    // shallowMountだと子コンポーネントをスタブによって描画しなくなる(高速化)
    wrapper = shallowMount(CounterVuex, { store, localVue })
  })

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // 要素の存在を確認することも簡単です
  it('has a count label', () => {
    expect(wrapper.contains('.count')).toBe(true)
  })

  // ボタンを押してinclementが呼び出されているかテスト
  it('button click should increment call', () => {
    expect(countStoreMock.actions.increment).not.toBeCalled()
    const button = wrapper.find('button')
    button.trigger('click')
    expect(countStoreMock.actions.increment).toBeCalled()
  })

})
  • コンポーネントの単体テストなので、VuexのストアはMock化する
  • ボタンをタップしたときactionが呼び出されているかを確認する(実際にカウントアップしているかどうかはストア側の責務のためここでは確認しない)

まとめ

  • Jest はいいぞ
  • ストアとコンポーネントは責務を切り分けて 単体テストしよう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

キョリカンというサービスを作った話。

こんにちは、かけるです。本業はデザイナーなのですが、いろいろサービスのアイデアを考えたりするのが好きで自分で作れるようになりたいと思いプログラミングを始めました。もっとスタートアップ界隈のかたとつながりたいのでtwitterフォローお願いします!仕事ください。。。。
と、余談は後にして。本題です。今回は、プログラミングを始めて作ったサービスの話をしてもいいですか。

Vue.jsとfirebaseで作りました。cloud functionの使い方がいまいちつかめていないのとvuexなど全然使いこなせていないですが、何とか作ることができました。

作るなら、誰かのためになるものを

やはり何か作るなら使ってもらいたいので、誰かのためになるものを創ろうと思いました。でも、これがまた難しいんですよね、どこに課題があるのか見つけるのが難しい。それこそ、課題なんかないじゃないかと思うくらい僕には何も見えていなかったんです。その課題探しに没頭すること数日、、、
ついに見つけることができたんです。それは、僕が体験していたけど自ら「しょうがないこと」と封印していたことでした。その問題というのは、企業と採用応募者との距離感です。就活をしていて、どうも変な感じがしていたんです。もっとお互いのこと知るべきなのに形式ばったり、企業という顔が前面に出すぎていて本心が言いずらいと思っていました。緊張もしましたし。もっと面接までのやり取りをラフな感じにすれば、このような緊張感などは起きないのかなと思いたわけです。

人材業界の市場規模と問題

2019-06-23 (2).png

人材業界の市場規模は、約9兆円で電子部品・デバイスや医薬品市場よりも多いです。ということは、人材業界より低い市場は人手不足という事ではないでしょうか。専門性が高まるにつれて、人材不足数が大きくなっていきます。そうすると専門職の給料は高騰し儲かるというわけですが、そんな大金到底払えないので、インターンやアルバイト、派遣社員雇って安く済ませているという事だと思います。そして、それが嫌な人はフリーランスとして働き始めて、企業ではどんどん人手が少なくなっていくのかなと考えています。間違っているかもしれないですが、、、
そして、人手不足を抑える方法の一つに退職率を減らすことがあげられます。それは、新卒や転職者と企業とのミスマッチを減らすという事です。こんかい、私が問題だと思った事とつながってきます。本心や求めているものを引き出し、ミスマッチを減らすにはやはり距離感が必要になってくると思うのです。

企業と採用応募者との距離感

1.jpg

これは、企業と採用応募者の距離が遠いほど採用者のリラックス度や密なコミュニケーションをすることができる可能性が減っていく図です。リクナビやその他の就活サービスは「企業」という顔が前面に出ていてどうしても、応募者側に緊張感を与えてしまいます。一方Wantedlyというサービスは、企業という面を持ちながらも、その内部の人とチャットができるという面でよりリラックスさせることができると考えられます。
2.jpg
採用応募者のリラックス度が高ければ高いほどより密なコミュニケーションが取れるので本心や真の実力を知ることができると考えられます。
これは、私の予測ですが「Wantedly」を使うベンチャー企業が多いのはこの密なコミュニケーションによってより良い人材をとることができると感じているからではないでしょうか。後日アンケートを取ってみたいと思います。

より密なコミュニケーションを実現するために

より高い数値を出すにはどのようなサービスが良いのか。そこで考えたのは、プラットフォームに内包したサービスです。私が、「これだ!」と思ったのは「というサービスでした。ツイッターやFacebookでイベントや人材の募集ができるサービスで、僕は最初このサービスがバズったのは、はやりのOGPを利用したサービスだったからだと思っていました。でもそれだけじゃないと気付いたんです。「bosyu」でイベントや人材を募集する人たちは個人なんです。そう、個人が募集を共有するところはツイッター、Facebookという開けたソーシャルネットワークなため、よりラフに応募することができそのやり取りもそこで進んでいきます。企業という顔が前面に出ず個人が前面に出る事が密なコミュニケーションをとることには重要だと思っていたので、「これなら密なコミュニケーションが実現できる。」と思った瞬間でした。また、最近「ツイッター採用」という言葉を耳にする事や、そこで仕事を受注する人を良く見るようになったのでイケるかもと思い今回「キョリカン」というサービス作成に至りました。

キョリカンというサービス

https://kyorikan.tk
screencapture-localhost-8080-2019-06-23-16_10_27.png
これがトップページです。キャッチフレーズに関してですが、サービス名である「キョリカン」と「距離感」をかけて、創り出してほしいものは信頼だという事を伝えたくこのようなキャッチフレーズにしました。何かいいキャッチフレーズある人募集します。。。ツイッターに内包されたサービスですので、サインインはツイッターアカウントのみです。

使い方

note-kyorikan-2.jpg

これが一連の流れです。募集要項を作ってそして確認し、ツイッターに共有するだけです。そしてツイッターできになった人が共有されたリンクに飛び、詳細ページで募集要項確認後、興味があればツイッターで興味を知らせるボタンを押します。すると作成者に共有された通知がツイッターで届くのでそこからやり取りが始まります。このメカニズムは、募集要項作成時にツイッターIDを記入する欄があり、そこに記入することによって通知が来るようになっています。ツイッターAPIでIDを自動取得しようと思ったのですが、よくわからなかったので、今の形で作りました。今後改善していくつもりです。OGPで作成される画像と詳細で見る画像が違うのは、少しでも会社のことを詳細画面で表せたらなと思ったためです。
使い方はこれくらいです。

最後に

誰かの役に立つものを創ろうとしたと書きましたが、本当は自分のためで、僕が就職活動をしていていやだなと思ったことを解決したくて作りました。もし、この問題を分かち合える採用側、採用者側がいれば使っていただきたいです。大きな問題より小さな問題を解決するのが案外難しかったりするんです。また、見つけにくいものなんです。

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

[Docker] Vueでaxiosを使うときにbaseurlを設定する方法

はじめまして、普段はproduct managerをやっているtatsukenと申します。
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

はじめに

フロントエンドとapiをdockerで別々のサーバーとして立ち上げているときに、フロントからいちいちエンドポイントを指定するのが面倒だったので、いろいろ調べたmemo

何をするのか

vueからaxiosを用いてapiリクエストを行う際、いちいちURLを指定するのは面倒。

sample.js
import Axios from 'axios'
async getAmount() {
        try {
            const res = await Axios.get(`http://localhost:8081/hoge`);
            console.log(res.data);
        } catch (error) {
            console.log(error);
        }
    }

このように毎回http://localhost:8081/hogeというようにurlを指定するのは面倒
できれば/hogeというように簡潔にエンドポイントを指定したい

解決策

まずdocker-compose.ymlでvueプロジェクトのdockerイメージ以下のenvironmentVUE_APP_API_ENDPOINT: http://http://localhost:任意のport番号を追加する

docker-compose.yml
services:
  web:
   ...
    environment:
       VUE_APP_API_ENDPOINT: http://localhost:8081 //任意のport番号を指定
   ...

vueプロジェクト内のmain.jsに以下を追記

main.js
import Axios from 'axios'
Axios.defaults.baseURL = process.env.VUE_APP_API_ENDPOINT;

これで以下のように簡潔に書けるようになります

sample.js
import Axios from 'axios'
async getAmount() {
        try {
            const res = await Axios.get(`/hoge`);
            console.log(res.data);
        } catch (error) {
            console.log(error);
        }
    }

最後に

エンドポイントへのリクエストが増えれば増えるほどURLの指定は面倒になってくるので、よければ試してみてください。
なにか間違いなどありましたら指摘していただけると幸いです。

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

v8nを使ってvue.jsでバリデーションをかけてみた

はじめまして、普段はproduct managerをやっているtatsukenと申します。
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

何をするのか

v8nというnode moduleを使用してvue.jsの入力フォームにバリデーションをかけていきたいと思います。
vue.jsの一般的なバリデーションライブラリとしてはvee-validateなどがありますが、
v8nは簡単にバリデーションをかけたいときやバリデーションのカスタマイズを行いたいときに便利です。

使い方

install(vue project)

npm install v8n -s 

Sample code

sample.vue
<template>
  <form action>
    <input type="text" v-model="email">
    <input type="password" v-model="password">
    <input type="text" v-model="email">
    <input type="submit" value="decide" @click="onClickSubmitButton">
  </form>
</template>

<script>
import v8n from "v8n";
export default {
  data() {
    return {
      email: null,
      password: null
    };
  },
  methods: {
    onClickSubmitButton() {
      const message = {
        null: "中身が空です",
        minLength: "文字数は4文字以上です",
        pattern: "emailを入力してください",
        maxLength: "文字数が多すぎます"
      };
      try {
        v8n()
          .not.null() // 値がnullじゃないか
          .string() // 文字列
          .minLength(4) // a@b.c を想定して最低5文字
          .pattern(/[^\s@]+@[^\s@]+\.[^\s@]+/)
          .check(this.email);
      } catch (ex) {
        let rule = ex.rule.name;
        alert(`Emailに関して${message[rule]}`);
        return;
      }
      try {
        v8n()
          .not.null()
          .string()
          .minLength(4)
          .check(this.password);
      } catch (error) {
        let rule = error.rule.name;
        alert(`passwordに関して${message[rule]}`);
        return;
      }
    }
  }
};
</script>

説明

  • まずv8nをimportimport v8n from "v8n";
  • v8n()に様々な設定を加えていくことで様々なバリデーションを作ることが出来ます。
code rule
null() nullかどうかを判定する
not.null() null出ないかどうかを判定する
string() stringかどうかを判定する
minLength(4) 4文字以上かどうかを判定する
pattern(/[^\s@]+@[^\s@]+.[^\s@]+/) patternに一致しているかどうかを判定する(今回はemailのpattern maching)
  • 他にも様々なruleをつけることが出来ます(詳しくはこちらにまとまっていました)
  • 加えた条件に対して.check(変数名)を行うことで任意の変数が自分の設定したruleに一致しているか判定することができます
  • あとはpromiseなりtry catchなりでerrorを受け取り、エラーハンドリングを行ってください
  • errorの場合どのruleによってerrorになったかはerror.rule.name;などで受け取ることが出来ます。

参考にしたもの

 最後に

ざっくりとv8nの使い方について説明しました。
初めての投稿なので間違いなどあれば指摘していただけると幸いです。

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

v8nを使ってVueでバリデーションをかけてみた

はじめまして、普段はproduct managerをやっているtatsukenと申します。
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

何をするのか

v8nというnode moduleを使用してvue.jsの入力フォームにバリデーションをかけていきたいと思います。
vue.jsの一般的なバリデーションライブラリとしてはvee-validateなどがありますが、
v8nは簡単にバリデーションをかけたいときやバリデーションのカスタマイズを行いたいときに便利です。

使い方

install(vue project)

npm install v8n -s 

Sample code

sample.vue
<template>
  <form action>
    <input type="text" v-model="email">
    <input type="password" v-model="password">
    <input type="text" v-model="email">
    <input type="submit" value="decide" @click="onClickSubmitButton">
  </form>
</template>

<script>
import v8n from "v8n";
export default {
  data() {
    return {
      email: null,
      password: null
    };
  },
  methods: {
    onClickSubmitButton() {
      const message = {
        null: "中身が空です",
        minLength: "文字数は4文字以上です",
        pattern: "emailを入力してください",
        maxLength: "文字数が多すぎます"
      };
      try {
        v8n()
          .not.null() // 値がnullじゃないか
          .string() // 文字列
          .minLength(4) // a@b.c を想定して最低5文字
          .pattern(/[^\s@]+@[^\s@]+\.[^\s@]+/)
          .check(this.email);
      } catch (ex) {
        let rule = ex.rule.name;
        alert(`Emailに関して${message[rule]}`);
        return;
      }
      try {
        v8n()
          .not.null()
          .string()
          .minLength(4)
          .check(this.password);
      } catch (error) {
        let rule = error.rule.name;
        alert(`passwordに関して${message[rule]}`);
        return;
      }
    }
  }
};
</script>

説明

  • まずv8nをimportimport v8n from "v8n";
  • v8n()に様々な設定を加えていくことで様々なバリデーションを作ることが出来ます。
code rule
null() nullかどうかを判定する
not.null() null出ないかどうかを判定する
string() stringかどうかを判定する
minLength(4) 4文字以上かどうかを判定する
pattern(/[^\s@]+@[^\s@]+.[^\s@]+/) patternに一致しているかどうかを判定する(今回はemailのpattern maching)
  • 他にも様々なruleをつけることが出来ます(詳しくはこちらにまとまっていました)
  • 加えた条件に対して.check(変数名)を行うことで任意の変数が自分の設定したruleに一致しているか判定することができます
  • あとはpromiseなりtry catchなりでerrorを受け取り、エラーハンドリングを行ってください
  • errorの場合どのruleによってerrorになったかはerror.rule.name;などで受け取ることが出来ます。

参考にしたもの

 最後に

ざっくりとv8nの使い方について説明しました。
初めての投稿なので間違いなどあれば指摘していただけると幸いです。

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

v8nを使ってVue.jsでバリデーションをかけてみた

はじめまして、普段はproduct managerをやっているtatsukenと申します。
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

何をするのか

v8nというnode moduleを使用してvue.jsの入力フォームにバリデーションをかけていきたいと思います。
vue.jsの一般的なバリデーションライブラリとしてはvee-validateなどがありますが、
v8nは簡単にバリデーションをかけたいときやバリデーションのカスタマイズを行いたいときに便利です。

使い方

install(vue project)

npm install v8n -s 

Sample code

sample.vue
<template>
  <form action>
    <input type="text" v-model="email">
    <input type="password" v-model="password">
    <input type="text" v-model="email">
    <input type="submit" value="decide" @click="onClickSubmitButton">
  </form>
</template>

<script>
import v8n from "v8n";
export default {
  data() {
    return {
      email: null,
      password: null
    };
  },
  methods: {
    onClickSubmitButton() {
      const message = {
        null: "中身が空です",
        minLength: "文字数は4文字以上です",
        pattern: "emailを入力してください",
        maxLength: "文字数が多すぎます"
      };
      try {
        v8n()
          .not.null() // 値がnullじゃないか
          .string() // 文字列
          .minLength(4) // a@b.c を想定して最低5文字
          .pattern(/[^\s@]+@[^\s@]+\.[^\s@]+/)
          .check(this.email);
      } catch (ex) {
        let rule = ex.rule.name;
        alert(`Emailに関して${message[rule]}`);
        return;
      }
      try {
        v8n()
          .not.null()
          .string()
          .minLength(4)
          .check(this.password);
      } catch (error) {
        let rule = error.rule.name;
        alert(`passwordに関して${message[rule]}`);
        return;
      }
    }
  }
};
</script>

説明

  • まずv8nをimportimport v8n from "v8n";
  • v8n()に様々な設定を加えていくことで様々なバリデーションを作ることが出来ます。
code rule
null() nullかどうかを判定する
not.null() null出ないかどうかを判定する
string() stringかどうかを判定する
minLength(4) 4文字以上かどうかを判定する
pattern(/[^\s@]+@[^\s@]+.[^\s@]+/) patternに一致しているかどうかを判定する(今回はemailのpattern maching)
  • 他にも様々なruleをつけることが出来ます(詳しくはこちらにまとまっていました)
  • 加えた条件に対して.check(変数名)を行うことで任意の変数が自分の設定したruleに一致しているか判定することができます
  • あとはpromiseなりtry catchなりでerrorを受け取り、エラーハンドリングを行ってください
  • errorの場合どのruleによってerrorになったかはerror.rule.name;などで受け取ることが出来ます。

参考にしたもの

 最後に

ざっくりとv8nの使い方について説明しました。
初めての投稿なので間違いなどあれば指摘していただけると幸いです。

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

Vue.jsでinput type="number"としてもvalueは常に文字列を返すので注意!number装飾子を追加しよう

はじめに

Vue.jsで投稿機能を実装していたときにハマってしまったことをメモしておきます。

ハマってしまったこと

価格入力をするためにinput type="number"として、情報を送信したのですが、うまくデータベースに保存できませんでした。

<div class="form-group">
  <label>価格</label>
  <input type="number" v-model="price" name="price">
</div>
model
price: DataTypes.INTEGER

priceはinteger型なので、input type="number"でいけるかなと思ったのですが、そうではなかったようです。

原因

type=number と書いたときでさえ、 HTMLのinput要素のvalue は常に文字列を返すようです。
なのでvue上で文字列を数値に変換します。

解決策

v-modelに管理された入力にnumber修飾子を追加するだけです。
これで数値として値を保存することができました!!

<div class="form-group">
  <label>価格</label>
  <input type="number" v-model.number="price" name="price">
</div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptだけで麻雀アプリを作れないかやってみた(できた)

Python+Flask+Herokuで麻雀アプリを作りたい(作った)の記事に
上げたように、Pythonを使って麻雀(もどき)アプリを作成することは出来ました。

ですが、やはり牌を捨てるたびにサーバーにアクセスしに行くのは無駄ですね。
電車の中とかですと、全然通信出来ないタイミングもありますし…。

なので、JavaScriptだけで出来ないかやってみました!
JavaScriptだけって言いながらVue.jsも使いましたが(笑)
まぁJavaScriptフレームワークなのでセーフってことで。
【完成品】
https://lonlymahjong.herokuapp.com/vue

スクリーンショット 2019-06-24 23.57.50.png
Pythonで作ったのと背景色が違います!
見た目の違いはそれだけです…。

はじめに

Pythonを使って麻雀アプリを作ることは出来ているので、
そのロジックをほぼそのままJavaScriptで書くだけですね!
簡単そうです!

・・・どうやればいいんだろう?

3日ほど悩みましたが、妖精さんが夜中にコードを書いてくれることはなかったので
取り敢えず作業を以下に分けて実施して行こうと思います。

 ①麻雀牌のクラスを作る
 ②山牌を作る
 ③初期配牌する
 ④牌を捨てて積もる
 ⑤あがり判定

こうやってタスクを切り分けると
なんだかできそうな気がしきますね!

①麻雀牌のクラスを作る

まずはじめに牌のクラスを作っていきたいと思います。
・・・でもJavaScriptでクラスってどう作ればいいんだ?
プログラミングの先生もJavaとJavaScriptは違うって言ってたし…。
と3日ほど悩みましたが、オブジェクト指向が役に立った話を読んでみると
どうやらほとんどJavaと同じような感じに書けるらしい。まじか。

きっと自分の知らない新技術をこっそり使ってるんだろうなーと思ってたけど
どうやら最近のJavaScriptはES6って言うバージョンで何もしなくても
ブラウザが対応してれば使えるらしい。まじでか。

 .js
//麻雀牌クラス
class Hai{
    //コンストラクタ
    constructor(kind, value){
        this.kind = kind;       //麻雀牌の種類(萬子・筒子・索子・四風牌・三元牌)
        this.value = value;     //麻雀牌の値(1~9 東南西北白発中)
        this.pic = this.kind+'_'+String(this.value)+'.png'; //画像ファイル名
    }
}

すごく簡単に出来た!
ES6すげぇ…。技術の進化バンザイ!

②山牌を作る

麻雀牌クラスが作れたので、後は136牌インスタンスを作って
それをシャッフルすれば山牌が出来ますね。
Pythonではリスト内包表記で簡単に
インスタンスのListができたんですけど
ES6にはないみたいですね…。
せっかくなので、ジェネレーターを使って書いてみることにします。

 .js
const KINDS = {
    SUUPAI : ['manzu', 'pinzu', 'souzu'],
    JIHAI : ['sufonpai', 'sangenpai']
}
const SUUPAI_VALUE = [1,2,3,4,5,6,7,8,9];
const SUFONPAI_VALUE = [1,2,3,4];
const SANGENPAI_VALUE = [1,2,3];

//山牌作成
function createYamahai(){
    // 配列ランダムソート(シャッフル)関数
    let shuffleArray = (arr) => {
        let n = arr.length;
        let temp = 0, i = 0;
        while (n) {
            i = Math.floor(Math.random() * n--);
            temp = arr[n];
            arr[n] = arr[i];
            arr[i] = temp;
        }
        return arr;
    }
    //山牌作成ジェネレーター
    let yamahaiGenerator = function* (kinds, values){
        for (let kind of kinds) {
            for (let value of values){
                for (let i = 0; i<4; i++ ) {
                   yield new Hai(kind,value);
                }
            }
        }
    };
    //山牌返却
    return shuffleArray([...yamahaiGenerator(KINDS.SUUPAI,SUUPAI_VALUE)
                        ,...yamahaiGenerator([KINDS.JIHAI[0]],SUFONPAI_VALUE)
                        ,...yamahaiGenerator([KINDS.JIHAI[1]],SANGENPAI_VALUE)]);
}

JavaScript - 配列をランダムソート(シャッフル)するを参考に、シャッフルする関数も自作。
createYamahai関数でしか山牌作成ジェネレーターもシャッフル関数も利用しないので
createYamahai関数内にオブジェクトとして作成しました。
このほうがスコープが絞れたりしてメリットになったりするのかな?

③初期配牌する

山牌が出来たので、後はそこから14牌取ってきて画面に表示できれば
なんかそれっぽいものが一旦出来ます。
ここからはJavaScriptフレームワークとしてVue.jsを利用しました。
AngularとReact、Vue.jsどれを使うか迷ったのですが、
比較的スモールスタートできるとのことだったので。

とはいえ、vue-cliとかNode.jsとかNuxt.jsとか利用しないと使えないんだろなー
と思ってたんですが、HTMLファイルの中で普通にJsファイルを読み込むだけで使えるみたい。

 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

まじでか。Vue.jsパネェ。
ということで早速実装♪

 .js
var app = new Vue({
    el:'#app',
    data:{
        yamahai : [],
        tehai : []
    },
    created: function() {
        //山牌作成
        this.yamahai = createYamahai();
        //配牌作成(山牌から14牌取得する)
        for (let i = 0; i<14; i++ ) {
          this.tehai.push(this.yamahai.shift());
        }
    }
})
 .html
<div id="app">
    <div>
        <img v-for="hai in tehai" v-bind:src="`/static/pic/${hai.pic}`">
    </div>
</div>

双方向データバインディングを使うと簡単にブラウザ上の表示を変えられて便利ですね!
スクリーンショット 2019-06-24 23.00.58.png

④牌を捨てて積もる

これもVue.jsを使えば簡単に出来ます。

 .js
var app = new Vue({
    el:'#app',
    data:{
        yamahai : [],
        tehai : []
    },
    created: function() {
        //山牌作成
        this.yamahai = createYamahai();
        //配牌作成(山牌から14牌取得する)
        for (let i = 0; i<14; i++ ) {
          this.tehai.push(this.yamahai.shift());
        }
    },
    methods: {
        //牌の交換
        change: function(index) {
            //捨牌
            this.tehai.splice(index, 1);

            //自摸
            let tsumo = this.yamahai.shift();
            this.tehai.push(tsumo);
        }
    }
})
 .html
<div id="app">
    <div>
        <img v-for="(hai, index) in tehai" v-bind:src="`/static/pic/${hai.pic}`" v-on:click="change(index)">
    </div>
</div>

v-forディレクティブの要素にIndexを取れるので、
それを利用してクリックした牌を捨てて新しく自摸ってきます。

⑤あがり判定

あがり判定ロジックはPythonで作り込んだので、
それをJavaScriptとに書き直すだけなのでこれも簡単。
・・・と思ってたんですが、雀頭の候補取得が思いのほか難しかったです。
もっと形式化すると、「配列内の重複項目の取得」です。

Pythonでは以下のように書いていたのですが、

 .py
[x for x in set(tehai) if tehai.count(x) >= 2]

JavaScriptだとオブジェクトの配列に対してSetが使えないんですよね…。
おそらく、「===演算子が「同一のインスタンスかどうか」を判断するため」だと思うんですが…。
JabaScriptのClassオブジェクトもJavaのesualsメソッドや、Pythonのeqメソッドが
あればできるんだと思うんですが…。

なので、JavaScriptでは以下メソッドを自作して対応しました。

 .js

//麻雀牌クラス
class Hai{
    //コンストラクタ
    constructor(kind, value){
        this.kind = kind;       //麻雀牌の種類(萬子・筒子・索子・四風牌・三元牌)
        this.value = value;     //麻雀牌の値(1~9 東南西北白発中)
        this.pic = this.kind+'_'+String(this.value)+'.png'; //画像ファイル名
    }

    //イコールメソッド
    equals(hai){
        return hai.kind == this.kind && hai.value == this.value
    }
}

//重複要素取得
//duplicateCount:重複数
function findDuplicate(array,duplicateCount){
    let setArray = array.filter((val, index, self) => 
        self.findIndex(n => val.equals(n)) === index);
    let result = setArray.filter(val => 
        (array.filter(n => val.equals(n)).length >= duplicateCount));
    return result;
}

setArray(重複のない配列)=
findIndexにて配列内で要素が最初に一致する位置が取得できるので、
それをループのindexとぶつけて一致するものだけを取得。
この処理で利用しているequalsメソッドは自作したものになります。

result=
setArray(重複のない配列)の一要素が、
もとの配列に何件あるか取得し、その件数がduplicateCountを上回る値だけを取得。

…プログラム処理を日本語にするのは難しいですね。

完成!

上記の処理に加え、理牌や捨て牌の表示、あたりの際の画像表示を行えばとりあえず完成です。
思ったより長かった…。
でもJavaScriptだけですべての処理を行うことができるようになりました!
牌の画像さえキャッシュに残っていれば低レイテンシどころか0レイテンシです!

Pythonで書いたコードをJavaScriptで書き直すって言うのは結構発見があってよかったです。
一度書いたコードを別の言語で書き直すって言うのは学習の一貫としてありかもしれないですね。
それにしてもES6すごいな…。もう大抵のことはJavaScriptでできちゃいますね。
新人の研修とかもJavaScriptでいいんじゃないかな〜。

今後の改修案としては
 ①あがり役の表示と点数計算機能を実装する。
 ②サーバーサイドでもなにかやる。
  (あがりまでの打牌数や点数をDBに記録してなんか出す。
   グラフ化して上達具合を可視化するとか?)
 ③Vue.jsをもっと使いこなす。
  (なんかソシャゲっぽくする。ソシャゲっぽいってなんだ…?)
 ④Firebaseを使って見る。
  (Python(Flask)はルーティングしか使ってないですし、
   Firebaseを使っても行けるんでないかなーと。
   使ったことないけど。)
なんかを考えてます。
また時間を作ってやっていこうっと。

おまけ

今回書いたコード。GitHubにも上げてます!

mahjong.js
const KINDS = {
    SUUPAI : ['manzu', 'pinzu', 'souzu'],
    JIHAI : ['sufonpai', 'sangenpai']
}
const SUUPAI_VALUE = [1,2,3,4,5,6,7,8,9];
const SUFONPAI_VALUE = [1,2,3,4];
const SANGENPAI_VALUE = [1,2,3];
// const SUFONPAI_VALUE = {
//     1:'東',
//     2:'南',
//     3:'西',
//     4:'北'
// };
// const SANGENPAI_VALUE = {
//     1:'白',
//     2:'發',
//     3:'中'
// };
const MENTSU_KINDS = ['順子','刻子']

//麻雀牌クラス
class Hai{
    //コンストラクタ
    constructor(kind, value){
        this.kind = kind;       //麻雀牌の種類(萬子・筒子・索子・四風牌・三元牌)
        this.value = value;     //麻雀牌の値(1~9 東南西北白発中)
        this.pic = this.kind+'_'+String(this.value)+'.png'; //画像ファイル名
    }

    //ソートキーを返却
    getSortKey(){
        switch( this.kind ) {
            case KINDS.SUUPAI[0]:
                return Number('1'+String(this.value));
            case KINDS.SUUPAI[1]:
                return Number('2'+String(this.value));
            case KINDS.SUUPAI[2]:
                return Number('3'+String(this.value));
            case KINDS.JIHAI[0]:
                return Number('4'+String(this.value));
            case KINDS.JIHAI[1]:
                return Number('5'+String(this.value));
            default:
                throw new TypeError('Hai.kind is not undefined');
        }
    }

    //イコールメソッド
    equals(hai){
        return hai.kind == this.kind && hai.value == this.value
    }
}

//あがり牌クラス
class Agari{
    //コンストラクタ
    constructor(yaku,arrayHai,janto,mentsu1,mentsu2,mentsu3,mentsu4){
        this.yaku = yaku;
        this.arrayHai = arrayHai;
        this.janto = janto;
        this.mentsu1 = mentsu1;
        this.mentsu2 = mentsu2;
        this.mentsu3 = mentsu3;
        this.mentsu4 = mentsu4;
    }
}

//雀頭
class Janto{
    //コンストラクタ
    constructor(arrayHai){
        this.arrayHai = arrayHai;
    }
}

//面子
class Mentsu{
    //コンストラクタ
    constructor(kind, arrayHai){
        this.kind = kind;
        this.arrayHai = arrayHai;
    }
}


//麻雀牌オブジェとのソート用関数
function sortHai(a,b){
    return a.getSortKey()-b.getSortKey();
}

//山牌作成
function createYamahai(){
    // 配列ランダムソート(シャッフル)関数
    let shuffleArray = (arr) => {
        let n = arr.length;
        let temp = 0, i = 0;
        while (n) {
            i = Math.floor(Math.random() * n--);
            temp = arr[n];
            arr[n] = arr[i];
            arr[i] = temp;
        }
        return arr;
    }
    //山牌作成ジェネレーター
    let yamahaiGenerator = function* (kinds, values){
        for (let kind of kinds) {
            for (let value of values){
                for (let i = 0; i<4; i++ ) {
                   yield new Hai(kind,value);
                }
            }
        }
    };
    //山牌返却
    return shuffleArray([...yamahaiGenerator(KINDS.SUUPAI,SUUPAI_VALUE)
                        ,...yamahaiGenerator([KINDS.JIHAI[0]],SUFONPAI_VALUE)
                        ,...yamahaiGenerator([KINDS.JIHAI[1]],SANGENPAI_VALUE)]);
}

//あがり判定
function judge(tehai){
    agari=[];

    //雀頭
    jantoArray = findDuplicate(tehai,2);

    if(jantoArray.length === 0){
        return false;
    }

    //国士無双
    if(checkKokushimusou(tehai, jantoArray)){
        //return new Agari('国士無双',tehai,null,null,null,null,null);
        return true;
    }

    //七対子
    if(jantoArray.length === 7){
       agari= agari.concat(new Agari('七対子',tehai,null,null,null,null,null));
    }

    //通常役
    for(let janto of jantoArray){
        let copy = Object.assign([], tehai);
        removeElement(copy,janto);
        removeElement(copy,janto);

        copy.sort(sortHai);

        //刻子の種類
        koutsuArray = findDuplicate(copy,3);

        //刻子が0個のパターン
        agari= agari.concat(agariKoutsu0(copy, janto))

        //刻子が1個のパターン
        agari= agari.concat(agariKoutsu1(copy, janto, koutsuArray))

        //刻子が2個のパターン
        agari= agari.concat(agariKoutsu2(copy, janto, koutsuArray))

        //刻子が3個のパターン
        agari= agari.concat(agariKoutsu3(copy, janto, koutsuArray))

        //刻子が4個のパターン
        agari= agari.concat(agariKoutsu4(janto, koutsuArray))
    }

    return agari.length > 0
}

//重複要素取得
//duplicateCount:重複数
function findDuplicate(array,duplicateCount){
    let setArray = array.filter((val, index, self) => 
        self.findIndex(n => val.equals(n)) === index);
    let result = setArray.filter(val => 
        (array.filter(n => val.equals(n)).length >= duplicateCount));
    return result.sort(sortHai);
}

//配列に特定の要素があるか確認
//存在する場合:true
function checkAvailability(array, val) {
    return array.some(arrVal => val.equals(arrVal));
}

//配列から特定の要素を削除
function removeElement(array, val){
    let index = array.findIndex(arrVal => val.equals(arrVal));
    array.splice(index, 1);
}

//順子をひとつ見つける
function findOneSyuntsu(arrayHai){
    arrayHai.sort(sortHai);

    for(let hai of arrayHai){
        let syuntsu = createSyuntsu(hai);
        if(syuntsu == null){
            continue;
        }
        if(checkAvailability(arrayHai,syuntsu.arrayHai[0])
        && checkAvailability(arrayHai,syuntsu.arrayHai[1])
        && checkAvailability(arrayHai,syuntsu.arrayHai[2])){
            return syuntsu;
        }
    }
    throw new RangeError('No Mentsu');
}

//自身を一番最初とした順子を返却
function createSyuntsu(hai){
    if(KINDS.SUUPAI.includes(hai.kind) && hai.value <= 7){
        return new Mentsu(MENTSU_KINDS[0],
            [new Hai(hai.kind,hai.value)
            ,new Hai(hai.kind,hai.value+1)
            ,new Hai(hai.kind,hai.value+2)]);
    }
    return null;
}


//刻子が0個のあがりパターン
function agariKoutsu0(arrayHai,janto){
    try {
        let copy = Object.assign([], arrayHai);

        let first = findOneSyuntsu(copy);
        removeElement(copy,first.arrayHai[0]);
        removeElement(copy,first.arrayHai[1]);
        removeElement(copy,first.arrayHai[2]);

        let second = findOneSyuntsu(copy);
        removeElement(copy,second.arrayHai[0]);
        removeElement(copy,second.arrayHai[1]);
        removeElement(copy,second.arrayHai[2]);

        let third = findOneSyuntsu(copy);
        removeElement(copy,third.arrayHai[0]);
        removeElement(copy,third.arrayHai[1]);
        removeElement(copy,third.arrayHai[2]);

        let fourth = findOneSyuntsu(copy);

        return [new Agari(new Janto([janto,janto]),
            first, second, third, fourth)];
    }catch (e) {
        return [];
    }
}

//刻子が1個のあがりパターン
function agariKoutsu1(arrayHai,janto,koutsuArray){
    if(koutsuArray.length < 1){
        return [];
    }

    let result = [];
    for(let koutsu of koutsuArray){
        try{
            let copy = Object.assign([], arrayHai);

            let first = new Mentsu(MENTSU_KINDS[1],[koutsu,koutsu,koutsu]);
            removeElement(copy,first.arrayHai[0]);
            removeElement(copy,first.arrayHai[1]);
            removeElement(copy,first.arrayHai[2]);

            let second = findOneSyuntsu(copy);
            removeElement(copy,second.arrayHai[0]);
            removeElement(copy,second.arrayHai[1]);
            removeElement(copy,second.arrayHai[2]);

            let third = findOneSyuntsu(copy);
            removeElement(copy,third.arrayHai[0]);
            removeElement(copy,third.arrayHai[1]);
            removeElement(copy,third.arrayHai[2]);

            let fourth = findOneSyuntsu(copy);

            result.push([new Agari(new Janto([janto,janto]),
                first, second, third, fourth)]);
        }catch (e) {
            continue;
        }
    }
    return result;
}

//刻子が2個のあがりパターン
function agariKoutsu2(arrayHai,janto,koutsuArray){
    if(koutsuArray.length < 2){
        return [];
    }

    let result = [];
    for(let i = 0; i < koutsuArray.length - 1; i++){
        for(let j = i + 1; j < koutsuArray.length; j++){
            try{
                let copy = Object.assign([], arrayHai);

                let first = new Mentsu(MENTSU_KINDS[1],[koutsuArray[i],koutsuArray[i],koutsuArray[i]]);
                removeElement(copy,first.arrayHai[0]);
                removeElement(copy,first.arrayHai[1]);
                removeElement(copy,first.arrayHai[2]);

                let second = new Mentsu(MENTSU_KINDS[1],[koutsuArray[j],koutsuArray[j],koutsuArray[j]]);
                removeElement(copy,second.arrayHai[0]);
                removeElement(copy,second.arrayHai[1]);
                removeElement(copy,second.arrayHai[2]);

                let third = findOneSyuntsu(copy);
                removeElement(copy,third.arrayHai[0]);
                removeElement(copy,third.arrayHai[1]);
                removeElement(copy,third.arrayHai[2]);

                let fourth = findOneSyuntsu(copy);

                result.push([new Agari(new Janto([janto,janto]),
                    first, second, third, fourth)]);
            }catch (e) {
                continue;
            }
        }
    }
    return result;
}

//刻子が3個のあがりパターン
function agariKoutsu3(arrayHai,janto,koutsuArray){
    if(koutsuArray.length != 3){
        return [];
    }

    try {
        let copy = Object.assign([], arrayHai);

        let first = new Mentsu(MENTSU_KINDS[1],[koutsuArray[0],koutsuArray[0],koutsuArray[0]]);
        removeElement(copy,first.arrayHai[0]);
        removeElement(copy,first.arrayHai[1]);
        removeElement(copy,first.arrayHai[2]);

        let second = new Mentsu(MENTSU_KINDS[1],[koutsuArray[1],koutsuArray[1],koutsuArray[1]]);
        removeElement(copy,second.arrayHai[0]);
        removeElement(copy,second.arrayHai[1]);
        removeElement(copy,second.arrayHai[2]);

        let third = new Mentsu(MENTSU_KINDS[1],[koutsuArray[2],koutsuArray[2],koutsuArray[2]]);
        removeElement(copy,third.arrayHai[0]);
        removeElement(copy,third.arrayHai[1]);
        removeElement(copy,third.arrayHai[2]);

        let fourth = findOneSyuntsu(copy);

        return [new Agari(new Janto([janto,janto]),
            first, second, third, fourth)];
    }catch (e) {
        return [];
    }
}

//刻子が4個のあがりパターン
function agariKoutsu4(janto,koutsuArray){
    if(koutsuArray.length != 4){
        return [];
    }

    return [new Agari(new Janto([janto,janto]),
        new Mentsu(MENTSU_KINDS[1],[koutsuArray[0],koutsuArray[0],koutsuArray[0]]),
        new Mentsu(MENTSU_KINDS[1],[koutsuArray[1],koutsuArray[1],koutsuArray[1]]),
        new Mentsu(MENTSU_KINDS[1],[koutsuArray[2],koutsuArray[2],koutsuArray[2]]),
        new Mentsu(MENTSU_KINDS[1],[koutsuArray[3],koutsuArray[3],koutsuArray[3]]))];
}

//国士無双のチェック(前提として雀頭があること)
function checkKokushimusou(tehai){
    if(
        checkAvailability(tehai,new Hai(KINDS.SUUPAI[0],SUUPAI_VALUE[0])) &&
        checkAvailability(tehai,new Hai(KINDS.SUUPAI[0],SUUPAI_VALUE[8])) &&
        checkAvailability(tehai,new Hai(KINDS.SUUPAI[1],SUUPAI_VALUE[0])) &&
        checkAvailability(tehai,new Hai(KINDS.SUUPAI[1],SUUPAI_VALUE[8])) &&
        checkAvailability(tehai,new Hai(KINDS.SUUPAI[2],SUUPAI_VALUE[0])) &&
        checkAvailability(tehai,new Hai(KINDS.SUUPAI[2],SUUPAI_VALUE[8])) &&
        checkAvailability(tehai,new Hai(KINDS.JIHAI[0],SUFONPAI_VALUE[0])) &&
        checkAvailability(tehai,new Hai(KINDS.JIHAI[0],SUFONPAI_VALUE[1])) &&
        checkAvailability(tehai,new Hai(KINDS.JIHAI[0],SUFONPAI_VALUE[2])) &&
        checkAvailability(tehai,new Hai(KINDS.JIHAI[0],SUFONPAI_VALUE[3])) &&
        checkAvailability(tehai,new Hai(KINDS.JIHAI[1],SANGENPAI_VALUE[0])) &&
        checkAvailability(tehai,new Hai(KINDS.JIHAI[1],SANGENPAI_VALUE[1])) &&
        checkAvailability(tehai,new Hai(KINDS.JIHAI[1],SANGENPAI_VALUE[2]))
    ){
        return true;
    }else{
        return false;
    }
}


const TEST_TEHAI = ['23333444556688', '22333456667788', '22344445556677', '11123334445577','22223344455677', '11555677788899'];

function createTestTehai(index){
    return Array.from(TEST_TEHAI[index]).map(n => new Hai(KINDS.SUUPAI[0],Number(n)));
}
function checkTestTehai(){
    for(let i =0;i<TEST_TEHAI.length;i++){
        let tehai = createTestTehai(i);
        // console.log(TEST_TEHAI[i]);
        // console.log(judge(tehai));
        if(!judge(tehai)){
            console.log('失敗')
        }
    }
}
main.js
var app = new Vue({
    el:'#app',
    data:{
        yamahai : [],
        kawa : [],
        tehai : [],
        agari : false
    },
    created: function() {
        //山牌作成
        this.yamahai = createYamahai();

        //配牌作成(山牌から14牌取得する)
        for (let i = 0; i<14; i++ ) {
          this.tehai.push(this.yamahai.shift());
        }

        //理牌
        this.tehai.sort(sortHai)

        //あがり判定
        this.agari = judge(this.tehai);
    },
    methods: {
        //牌の交換
        change: function(index) {
            //捨牌
            sutehai = this.tehai[index];
            this.tehai.splice(index, 1);

            //河
            this.kawa.push(sutehai);

            //理牌
            this.tehai.sort(sortHai);

            //自摸
            let tsumo = this.yamahai.shift();
            this.tehai.push(tsumo);

            //あがり判定
            this.agari = judge(this.tehai);
        }
    }
})
vueJong.html
<!doctype html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet"href="/static/css/main.css">
        <title>ひとりまーじゃん</title>
    </head>

    <body class="vue">
        <div id="app">
            <div class="win" v-if="agari" >
                <a href="/vue"><img src="/static/pic/win.png"></a>
            </div>
            <div class="sutehai" v-if="!agari" >
                <template v-for="(hai, index) in kawa"><br v-if="(index % 9) == 0" ><img  v-bind:src="`/static/pic/${hai.pic}`"></template>
            </div>
            <div class="tehai">
                <img v-for="(hai, index) in tehai" v-bind:src="`/static/pic/${hai.pic}`" v-on:click="change(index)">
            </div>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="/static/js/mahjong.js"></script>
        <script src="/static/js/main.js"></script>
    </body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む