20200910のvue.jsに関する記事は14件です。

【Laravel×Vue.js】リダイレクト時にセッションデータを持ってメッセージを表示

ログインユーザーのみアクセス可能なURL/sample/に非ログイン状態でアクセスした際、
ログインページにリダイレクトし、「この機能を使うためにはログインしてください。」とメッセージを表示させた時の実装です。
/sample/からリダイレクトされた場合のみメッセージを表示する。)

SampleController.php

リダイレクトとともにセッションデータを渡す処理を記載

SampleController.php
namespace App\Http\Controllers;

// 現在のHTTPリクエストインスタンスを取得するため、Illuminate\Http\Requestクラスを指定
use Illuminate\Http\Request;

class SampleController extends Controller
{
    public function __invoke(Request $request)

        // 未ログイン時はloginページにリダイレクト
        // withで「showMessage」というセッションデータを渡す
        if (!\Auth::check()) {
            return redirect('login')->with('showMessage', true);
        }

        // ログインしていれば/sample/を表示
        return view('sample.index');
    }
}

redirect時に、withを使ってshowMessageというBoolean型のデータを持たせている。
(trueのところに'メッセージです。'とすると、テキストメッセージを持たせることも可能。)

これで未ログイン時に/sample/ページにアクセスすると、
/login/にリダイレクトされ、 ページ上でshowMessageの値を使用できる。

login.blade.php

ログイン用のviewであるlogin.blade.phpで、セッションを表示してみる。

login.blade.php
{{ session('showMessage') }}
// 1

/login/にshowMessageの真偽値true が渡っているので、「1」と表示される。
これでセッションデータで値が渡っているのを確認できた。

この値をvue.jsのコンポーネント に渡す処理をかく。

login.blade.php
@section('content')
    <sample-component>
                 :show-message="{{ session('showMessage') ? 'true' : 'false'}} ">
    </sample-component>
@endsection

SampleComponent.vue

コンポーネントが埋め込まれているlogin.blade.php から渡されたセッションデータshow-message を、
親から子がデータを受け取る際に使うprops で受け取っておく。
(vue.jsではケバブケースではなくキャメルケースで受け取る。)

SampleComponent.vue
export default {
  props: {
    showMessage: {
      type: Boolean,
      default: false,
    },
  },
};

ここまできたら、受け取ったshowMessageを使って、
v-if などでtrueだったらメッセージを表示するなどが可能。

SampleComponent.vue
<template>
  <div class="mb-4">
    <div v-if="showMessage">
      <p>この機能を使うためにはログインしてください。</p>
    </div>
    ...省略...
  </div>
</template>

<script>
export default {
...省略...
  props: {
    showMessage: {
      type: Boolean,
      default: false,
    },
  },
};
</script>

これで未ログイン時に/sample/ページから/login/にリダイレクトしてきた時のみ、
showMessageというセッションデータを持って真偽の判定を持たせることができました。

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

2020 Vue/Vuetify WEB開発 TestCafeを使ったE2Eテスト 基礎編

記事の目的

フロントエンドエンジニアとして
エンドツーエンド (E2E) テストをTestCafeで書く機会があった

E2Eテストの自動化は初めての経験だったので
これをまとめ、紹介および備忘録とする

記事の対象者

  • NuxtやVueに実際に触れたことがある人
  • フロントエンドエンジニアとして実装経験のある人
  • TestCafe に興味がある人
  • e2eは全然わからないけどやってみたいという人

開発環境

  • MacBook Pro (Retina, 13-inch, Early 2015)
  • OS:macOS Mojava 10.14.6
  • サーバサイドJavaScript:node v12.14.1
  • パッケージマネージャー: yarn v1.22.4
  • フレームワーク: nuxt v2.0.0
  • JavaScriptライブラリ: Vue v2.6.11
  • UIライブラリー: @nuxtjs/vuetify v1.11.2

目次

  1. Nuxtの導入とサンプル作成
  2. E2Eテスト作成(TestCafe

Nuxtの導入とサンプル作成

Githubからこの記事で作成したサンプルをダウンロード可能
https://github.com/shiho-hoshino/e2e-sample

Nuxtの導入

実際のプロジェクトと同じようにyarnでNuxtをインストール
インストールの初期設定は以下の通り

$ yarn create nuxt-app e2e-sample

Project name: e2e-sample
Programming language: JavaScript
Package manager: Yarn
UI framework: Vuetify.js
Nuxt.js modules: Axios
Linting tools: ESLint, Prettier, StyleLint
Testing framework: Jest
Rendering mode: Single Page App 
Deployment target: Static (Static/JAMStack hosting)
Development tools: #無選択

VueとVuetifyを使ったサンプル用ページの作成

e2eのテストとしてわかりやすくログインページのサンプルを作成
サンプルの状態はこのようにしました

  • 入力欄が二つと、ログインボタンのみ
  • ログインの認証処理などは含んでいない
  • ボタンクリックでHOME画面に遷移する

実際の案件と同じくVueとVuetifyを使用している
サンプルの環境はeslintの設定も追加・変更済み

package.jsonをinstallし起動

$ yarn install
$ yarn run dev

http://localhost:3000/login
起動後にログインページを確認
login.png

E2Eテスト作成とローカル実行(TestCafe)

ログイン処理のテストコードを作成する

testcafeをインストール

$ yarn add testcafe

サンプルページにテスト用のIDを追加

サンプルで作成した page/login.vue テンプレートに
以下の対応を追加。

テストしたい要素にカスタムデータ属性「data-testid」を付ける
後ほどTestCafeでIDを指定して操作する

<template>
  <div>
    <v-card>
      <v-card-text>
        <h1 class="title mb-4">
          Login
        </h1>
        <v-text-field
          data-testid="login.email"
          v-model="data.email"
          single-line
          outlined
          dense
        />
        <v-text-field
          data-testid="login.password"
          v-model="data.password"
          single-line
          outlined
          dense
        />
        <div class="mt-2 d-flex justify-center">
          <v-btn
            data-testid="login.btn"
            small
            rounded
            @click="login()"
          >
            Login
          </v-btn>
        </div>
      </v-card-text>
    </v-card>
  </div>
</template>

テストコードの作成(TestCafe)

test/e2e/Login.js に以下のコードを記載
Loginテストの流れは以下の通り

  1. testcafeから必要な関数をimport
  2. 最初に開くページを「Getting Started」で設定
    ローカルでのテスト前提のため、パスは「http://localhost:3000/login/」
  3. 作成したログインページの入力欄にEmailを入力
  4. 作成したログインページの入力欄にパスワードを入力
  5. スクリーンショットを撮影
  6. ログインボタンをクリック
  7. TOPに遷移したか確認
import {Selector, ClientFunction} from 'testcafe';

fixture`Getting Started`.page`http://localhost:3000/login/`;
const getWindowLocation = ClientFunction(() => window.location);

test('Login', async (targetPage) => {
  let location;
  // 現在のURLを取得
  location = await getWindowLocation();
  // パスが想定通りか確認
  location.pathname.match('/login/');
  await targetPage
      // Emailを入力
      .typeText(Selector('[data-testid="login.email"]'), 'user@email.com')
      // パスワードを入力
      .typeText(Selector('[data-testid="login.password"]'), 'userpass')
      // Screenshotを保存
      .takeScreenshot('e2e/login.png'),
      // ログインボタンをクリック
      .click(Selector('[data-testid="login.btn"]'));
  // 現在のURLを取得
  location = await getWindowLocation();
  // パスが想定通りか確認
  location.pathname.match('/');
});

実行

2つのターミナルを使用する
1のターミナルでサーバーを起動した状態で
2のターミナルでテストコマンドを実行する
サーバーが起動した状態でなければ
指定のサーバーが存在しない状態になるのでテストは走らない

[1] ローカルでnuxtを起動する

$ yarn run dev

[2] テストを実行する

$ yarn testcafe chrome test/e2e/Login.js

※ Testcafeの対象ブラウザ
https://devexpress.github.io/testcafe/documentation/guides/concepts/browsers.html#locally-installed-browsers

実行すると以下のように指定のブラウザが起動して
テストコード通りに自動で操作処理が走る

testcafe.gif

処理が終わると以下のようにパスしたテスト数と
かかった時間が表示されます

$ 1 passed (9s)

まとめ

未経験でも環境構築が容易
シンプルでわかりやすく簡単にテストコードが書けた

本記事では紹介していないが
BrowserstackとTestCafeを連携すれば
自分の所持していないブラウザや環境でも
テストを行うことが可能になる

https://www.browserstack.com/

ローカルホストのテストは行えないため
テスト用のサーバーなどを用意して
テストを走らせるのが良い

Browserstackについて調査後、記事にまとめる予定
以上 TestCafeを使ったE2Eテスト 基礎編

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

【Nuxt.js】Vuexを利用したデータのやり取りを追っていく〜ログインデータ編〜【Vue.js】

Vuexのデータのやり取りの流れって何回も移動するので、ややこしくなりますよね。。。。
そんなあなたのために、データのやり取りを仕事に例えて追っていこうと思います。

まずは、おなじみのデータのサイクルから復習しておきましょう

Vuex

参考記事
https://qiita.com/nasum/items/d17c0a628e6c32616b85

メソッドの早見表

仕事する場所 仕事を依頼する関数 仕事を受け取る関数
Vueコンポーネント this.$store.dispatch(引継ぎ先関数名, データ) this.$store.getters[stateデータ名]
actionsプロパティ context.commit(引き継ぎ先関数名, データ) 関数名(context, payload)
mutationsプロパティ 関数名(state, payload)

payloadとは受け取ったデータのこと

1.Vueコンポーネント

仕事を依頼する関数(actionsプロパティへ)

this.$store.dispatch(引継ぎ先関数名, データ)で、actionsプロパティに引き継ぎさせます。

<script>
export default {
  data () {
    return {
      loginForm: {
        email: '',
        password: '',
      },
    }
  },
  methods: {
    loginSubmit (session) {
      this.$store.dispatch('login', {
        email: this.loginForm.email,
        password: this.loginForm.password,
      })
    }
  }
}
</script>

2.actionsプロパティ

仕事を受け取る関数(Vueコンポーネントから)

関数名(context, payload)で、データを受け取る

仕事を依頼する関数(mutationsプロパティへ)

context.commit(引き継ぎ先関数名, データ)で、mutationプロパティにデータを引き継がせる。

store/index.js
export const state = () => ({
  user: {
    uid: '',
    name: '',
    email: '',
    login: false
  }
})

export const actions = {
  async login ({ commit }, payload) {

    const loginUser = await this.$axios.$post('/api/v1/sessions', {
      session: {
        email: payload.email,
        password: payload.password,
      }
    })

    await commit('getData', {
      uid: loginUser.data.id,
      name: loginUser.data.name,
      email: loginUser.data.email,
    })

    await commit('switchLogin')

    alert(loginUser.data.name)
    this.$router.push({ path: `/users/${loginUser.data.id}` })
  }
}

引き継がれたactionsプロパティのloginメソッドの主なお仕事は三つ。
1. 引き継がれたデータをAPIに投げて、帰ってきた値をキャッチする
2. キャッチした値の中のデータを、mutationプロパティに引き継がせる
3. もう一つだけ違う仕事を、同じくmutationプロパティに依頼する

3.mutationsプロパティ

仕事を受け取る関数(actionsプロパティから)

関数名(state, payload)で、データを受け取る

store/index.js
export const mutations = {

  getData (state, payload) {
    state.user.uid = payload.uid
    state.user.name = payload.name
    state.user.email = payload.email
  },

  switchLogin (state) {
    state.user.login = true
  }
}

mutationsプロパティ内のメソッドのそれぞれの仕事は以下の通り。
1. getDataの仕事は、受け取ったデータをstateデータに格納する
2. switchLoginの仕事は、stateのloginの真偽値をtrueにする

4.gettersプロパティ

仕事を納品する(Vueコンポーネントへ)

store/index.js
export const getters = {
  user: state => {
    return state.user
  }
}

5.Vueコンポーネント

納品されたものを受け取る(gettesプロパティから)

<template>
  <v-card>
    {{ user }}
  </v-card>
</template>

<script>
・・・
  computed: {
    user () {
      return this.$store.getters['user']
    }
  }
・・・
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンポーネント間のデータのやりとりを簡単なTODOアプリでまとめる

Vuexに頼りすぎてコンポーネント間のデータの受け渡し方が曖昧だったので、簡単なサンプルアプリでまとめる

また、Vueで開発をしていてコンポーネントを細かく分けない事で以下の問題がよく起こったのでその反省

コンポーネントを分けない問題
・汎用性の悪さ
例えばフォームとボタンを一緒のコンポーネントに作ると、ボタンだけ使い回したい時にフォームまでついてきて汎用性が悪い。

・同じような記述をしたコンポーネントファイルが増えDRYに反する

・1ファイルのコード量が増えて可読性が悪い

作成したコンポーネント

・ボタン
・入力フォーム
・ボタンと入力フォームをまとめたコンポーネント

ただ単にクリックイベントとボタン名を使いまわせるボタン

button.vue
<template>
  <button @click="onClick">{{name}}</button>
</template>
<script>
export default {
  props:{
    name:{
      type:String,
      default:"button"
    },
    onClick:{
      type:Function,
      required:true
    }
  },
}
</script>

入力フォーム。
propsの値はv-modelで直接変更するとエラーになる
computedでv-modelの変更を検知し
親コンポーネントへ入力された値(input)を送り、親側でpropsの値を更新する

Input.vue
<template>
<input type="text" v-model="input">
</template>
<script>
export default {
  props:{
    inputValue:String
  },
  computed:{
    // v-modelのinputの変更を検知
   input:{
     get(){
       return this.$props.inputValue
     },
    //  親コンポーネントにinputを送り出して親側でpropsの値を書き換える
     set(value){
        this.$emit("setValue",value)
     }
   }
  }
}
</script>

ボタンと入力フォームが存在するコンポーネント
入力フォームの値をボタンクリックで配列に格納し親コンポーネントへ渡す

Form.vue
<template>
  <div>
    <!-- $emitで渡ってきたイベントを実行 -->
    <!-- 子コンポーネントのpropsに$emitの引数で受け取った値をセット -->
    <Input
    @setValue="setValue"
    :inputValue = value />
    <!-- Buttonコンポーネントのイベント発火とボタン名を設定 -->
    <Button
    :onClick="postTodo"
    name="add"
     />
  </div>
</template>
<script>
import Button from "@/components/Button.vue"
import Input from "@/components/Input.vue"
export default {
  data(){
    return{
      value:null,
      todos:[]
    }
  },
  components:{
    Button,
    Input
  },
  methods:{
    postTodo(){
      this.todos.push(this.value)
      this.value=""
   //todoが追加された配列を親コンポーネントへ渡す
      this.$emit("setTodo",this.todos)
    },
    setValue(value){
      this.value=value
    }
  }
}
</script>

Form.vueから受け取った配列todosをv-forでレンダリング

App.vue
<template>
  <div id="app">
    <h1>Todo</h1>
    <Form 
    @setTodo="setTodo"
    />
    <hr>
    <template v-for="(todo,index) in todos">
      <li :key="index">{{todo}}</li>
    </template>
  </div>
</template>
<script>
import Form from "@/components/Form.vue"
export default {
  name: 'App',
  components: {
    Form
  },
  data(){
    return{
      todos:null
    }
  },
  methods:{
    setTodo(todos){
      this.todos=todos.reverse()
    },
  }
}
</script>

まとめ

・v-modelの値を渡したい時はcomputedで変更を検知して$emit経由で値を渡して親側からpropsを更新する
・$emitでどんどん親へ親へ渡していく。
・兄弟のデータを使う時は親のdataに保管したものを使う

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

Nuxt.jsでFacebook Customer Chatを導入

思ったより情報が落ちてなかったので、残しておきます。

Customer Chat Pluginとは名の通りFacebookページに顧客からチャットが飛ばせるよっていう仕組みで、こんな感じのMessengerアイコンがサイトに表示されるようになります。

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

FBページからCustomer Chatの設定

最近UIが変更されたので、ちょっとわかりやすくなったかもしれません。

ページの左側にあるメニューから、設定を選択
スクリーンショット 2020-09-10 17.01.19.png

メッセージを選択
スクリーンショット 2020-09-10 17.01.28.png

半分より下くらいに、 ウェブサイトにMessengerを追加 とのメニューが表示されるので、利用を開始ボタンを押します。
スクリーンショット 2020-09-10 17.01.44.png

そうすると、stepダイアログみたいな表示になるので最後まで進めます。
最後にスニペットがでてくるので、コピペしておきましょう。
スクリーンショット 2020-09-10 17.02.00.png

Nuxt側の設定

ぺたっとスニペットを貼り付けるだけだと楽なんですけど、実際にフレームワーク使ってるとそうはいかなかったりしますよね。

ここではPluginとして読み込んでいきたいと思います。

pluginファイルの作成

さっきのスニペットを以下のように書きます。

ここはVueインスタンスで利用できるようにしてますが、Injectしても良さそうですね。

  import Vue from = 'vue'

  const vueFb = {}
  vueFb.install = function install(Vue, options) {
    (function (d, s, id) {
      var js
      var fjs = d.getElementsByTagName(s)[0]
      if (d.getElementById(id)) { return }
      js = d.createElement(s)
      js.id = id
      js.src = 'https://connect.facebook.net/en_US/sdk/xfbml.customerchat.js'
      fjs.parentNode.insertBefore(js, fjs)
    }(document, 'script', 'facebook-jssdk'))

    window.fbAsyncInit = () => {
      window.FB.init(options)
      Vue.FB = window.FB

      // ここでEventのSubscribeしておくと、ハンドリングがやりやすかったりします
      // FB.Event.subscribe ~
    }
    Vue.FB = undefined
  }

  Vue.use(vueFb, {
    appId: YOUR FACEBOOK ID,
    autoLogAppEvents: true,
    xfbml: true, // ここをfalseにすると任意のタイミングでParseすることができます (表示) 
    version: Graph API Version (v3.3 etc)
  })  

nuxt.config.jsの追加

clientだけでいいと思うので、modeはclientで指定しています。

plugins: [
    { src: "~/plugins/fb.js", mode: 'client' },

任意の場所にhtmlを追加

Ex. default.vue

<template>
  <div id='app'>
    <nuxt/>
    <div class="fb-customerchat"
      :page_id="YOUR PAGE ID"
      theme_color="#0084ff">
    </div>
  </div>
</template>

これで終わりです。

まとめ

導入はめちゃくちゃ簡単ですね!

まだ安定していませんが、無料でChatサポートができるので個人で開発している方とかはかなりいいんじゃないかなと思います。
もし何かできない、動かないってなった場合はお気軽にDMで質問してください。

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

セクション17

axiosを使ってデータのやり取りをする

npm install axios
import axios from "axios"

cloud firestore rest api検索
結果
https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/cities/LA

YOUR_PROJECT_IDとcities/LAを変える

methods:{
    createComment(){
      axios.post(”https://firestore.googleapis.com/v1/projects/scrapoo/databases/(default)/documents/comment2”,
      {
        fields:{
          name:{
            stringValue:this.name
          },
          comment:{
            stringValue:this.comment
          }
        }
      }
      )
      .then(response=>{
        console.log(response)
      })
    }
  }

getもpostもおなじurl

baseurlなどの共通の設定はmain.jsで読み込む
なぜならmain.jsが最初に読み込まれるから

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

axios

axiosを使ってデータのやり取りをする

npm install axios
import axios from "axios"

cloud firestore rest api検索
結果
https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/cities/LA

YOUR_PROJECT_IDとcities/LAを変える

methods:{
    createComment(){
      axios.post(”https://firestore.googleapis.com/v1/projects/scrapoo/databases/(default)/documents/comment2”,
      {
        fields:{
          name:{
            stringValue:this.name
          },
          comment:{
            stringValue:this.comment
          }
        }
      }
      )
      .then(response=>{
        console.log(response)
      })
    }
  }

getもpostもおなじurl

baseurlなどの共通の設定はmain.jsで読み込む
なぜならmain.jsが最初に読み込まれるから

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

Vue3 でプロジェクトを作成する 〜 変更点 確認編 〜

Prologue

Vue3 のプロジェクトの立ち上げ方と Vue2 -> Vue3 の移行が現状どのようになっているのか確認するために検証を行いました。

Vue3や使用している CLI 等は今後変更があると思いますので、自身で設定する場合には都度公式を確認することをお勧めします。

環境

  • macOS: v10.15.6
  • node.js: v12.18.2
  • terminal: iTerm
  • エディタ: VS Code
  • パッケージマネージャ: yarn

install

vueCLI v4.5.0 で Vue3 のプロジェクトを選択できるようになりました。

参考:

今回 バージョンが Vue CLI v4.4.6 だったため、 yarn global add @vue/cli を実行し、バージョンを上げます。
参考: https://github.com/vuejs/vue-cli/releases

以下のコマンドを実行してバージョンの確認の確認をし、問題なければOKです。

vue -V 
@vue/cli 4.5.4

プロジェクトの作成 : Default Vue 3 Preview を選択

vue create vue3-next コマンドを実行します。今回プロジェクトは vue3-next としています。

Vue CLI v4.5.4
? Please pick a preset: 
  Default ([Vue 2] babel, eslint) 
❯ Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
  Manually select features 

デフォルトで Vue3 が選択できるようになっています。感動。

package.json を確認します。

  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0-0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0-0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0-0"
  },

yarn.lock も確認してみます。

vue@^3.0.0-0:
  version "3.0.0-rc.10"
  resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.0-rc.10.tgz#31298a757b4fad6ee8973d0fa27c4fde8574bd01"
  integrity sha512-nRsyIQtOWLDMBb5dsPwg/WdIqznCMVWN6O6wJSzhseKC768wHlZKcJ7SPHhWPid9wi3Ykhtl9vtgvxTK/qICkw==
  dependencies:
    "@vue/compiler-dom" "3.0.0-rc.10"
    "@vue/runtime-dom" "3.0.0-rc.10"
    "@vue/shared" "3.0.0-rc.10"

vue3.0.0-rc.10 がインストールされていて、 yarn serve を実行し問題なく表示されて入ればOKです。

プロジェクトの作成 : Manually select features を選択

manually select を選択します。

Vue CLI v4.5.4
? Please pick a preset: 
  Default ([Vue 2] babel, eslint) 
  Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
❯ Manually select features 

次に Vue のバージョンの選択肢が表示されるため、3.x を選択します。

? Choose a version of Vue.js that you want to start the project with 
  2.x 
❯ 3.x (Preview) 

以下はこれまで通り、自分が使うモジュール等を選択していきます。

vue create vue3-next-manually


Vue CLI v4.5.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Rou
ter, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Previe
w)
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfi
lls, transpiling JSX)? Yes
? 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
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated confi
g files
? Save this as a preset for future projects? No

manually select だと従来通り TS も選択できるため、TS を使用したい方はこちらを選択すると設定周りも楽になります。(Default (Vue 3 Preview) だと手動で入れる必要があります。)

package.json を確認します。

  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0-0",
    "vue-class-component": "^8.0.0-0",
    "vue-router": "^4.0.0-0",
    "vuex": "^4.0.0-0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^2.33.0",
    "@typescript-eslint/parser": "^2.33.0",
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-typescript": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0-0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/eslint-config-typescript": "^5.0.2",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.1.3",
    "eslint-plugin-vue": "^7.0.0-0",
    "prettier": "^1.19.1",
    "typescript": "~3.9.3"
  }

次に yarn.lock も確認してみます。

vue@^3.0.0-0:
  version "3.0.0-rc.10"
  resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.0-rc.10.tgz#31298a757b4fad6ee8973d0fa27c4fde8574bd01"
  integrity sha512-nRsyIQtOWLDMBb5dsPwg/WdIqznCMVWN6O6wJSzhseKC768wHlZKcJ7SPHhWPid9wi3Ykhtl9vtgvxTK/qICkw==
  dependencies:
    "@vue/compiler-dom" "3.0.0-rc.10"
    "@vue/runtime-dom" "3.0.0-rc.10"
    "@vue/shared" "3.0.0-rc.10"

以上、2種類の操作を試しましたが、Vue3 のバージョンに違いがないことを確認しました。こちらも yarn serve を実行して画面が表示されればOKです。

プロジェクトの作成 : Manually select features を選択, class-style component を使わない

ここでは CompositionAPI を使いたい場合、 class-style component を選択しないと違いがあるのかを検証しています。

プロジェクトの作成

vue create vue3-next-manually-no-class 


Vue CLI v4.5.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Rou
ter, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Previe
w)
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfi
lls, transpiling JSX)? Yes
? 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
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated confi
g files
? Save this as a preset for future projects? No

package.json を確認します。

  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0-0",
    "vue-router": "^4.0.0-0",
    "vuex": "^4.0.0-0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^2.33.0",
    "@typescript-eslint/parser": "^2.33.0",
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-typescript": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0-0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/eslint-config-typescript": "^5.0.2",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.1.3",
    "eslint-plugin-vue": "^7.0.0-0",
    "prettier": "^1.19.1",
    "typescript": "~3.9.3"
  }

yarn.lock も確認してみます。

vue@^3.0.0-0:
  version "3.0.0-rc.10"
  resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.0-rc.10.tgz#31298a757b4fad6ee8973d0fa27c4fde8574bd01"
  integrity sha512-nRsyIQtOWLDMBb5dsPwg/WdIqznCMVWN6O6wJSzhseKC768wHlZKcJ7SPHhWPid9wi3Ykhtl9vtgvxTK/qICkw==
  dependencies:
    "@vue/compiler-dom" "3.0.0-rc.10"
    "@vue/runtime-dom" "3.0.0-rc.10"
    "@vue/shared" "3.0.0-rc.10"

// 略

vuex@^4.0.0-0:
  version "4.0.0-beta.4"
  resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0-beta.4.tgz#7b319ead2ef30432b7eaa4e37258ea3c5e44f698"
  integrity sha512-/+4E1dokq5cwbl4mohOqOj8h0vOLOWmLSqlqTf++bfmN9/JKWtwYfsBrzlK0sYrNfuYcpQeX0BVxQHoHXDfYZQ==

// 略

vue-router@^4.0.0-0:
  version "4.0.0-beta.9"
  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.0-beta.9.tgz#8552c38c9b015527e74c9c6cb4c54868219d601e"
  integrity sha512-k8AGMm3LCTqnsEuF37AD4kcZVMwtnFEzdjACgmIII/xbLnTj3+o5XyH/zREBZutgv5q2hzlLltMVglqDQYMd/A==

"vue-class-component": "^8.0.0-0" がないだけとなります。この辺りも従来通りという感じです。

CompositionAPI が使えるか確認

Vue2 までは plugin の CompositionAPI を import して使う必要がありましたが、Vue3 ではその必要が無くなったということで、実際書いてみます。

src/components/HelloWorld.vue を以下のように変更

  • before
<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "HelloWorld",
  props: {
    msg: String
  }
});
</script>
  • after
<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "HelloWorld",
  props: {
    msg: {
      type: String
    }
  },
  setup(props) {
    const message = `${props.msg}. + Vue3`;
    return { message };
  }
});
</script>

before の時点で vue から defineComponent が import されているため使えることは当たり前なのですが、念のため setup 等も追記してみました。
追加したテキスト等が表示されていればOKです。


既存のプロジェクトをVue3 に変更

ここからが一番使うのではないでしょうか。
まずはVue2 でプロジェクトを作成します。今回プロジェクト名は vue3-next-cli-plugin としました。
TS, Vuex, vue-router も一緒にインストールしていきます。

vue create vue3-next-cli-plugin           


Vue CLI v4.5.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Rou
ter, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 2.x
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfi
lls, transpiling JSX)? Yes
? 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
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated confi
g files
? Save this as a preset for future projects? No

package.json の中身を確認します。

"dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vue-router": "^3.2.0",
    "vuex": "^3.4.0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^2.33.0",
    "@typescript-eslint/parser": "^2.33.0",
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-typescript": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/eslint-config-typescript": "^5.0.2",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.1.3",
    "eslint-plugin-vue": "^6.2.2",
    "prettier": "^1.19.1",
    "typescript": "~3.9.3",
    "vue-template-compiler": "^2.6.11"
  }

yarn.lock の中身も確認します。

vue@^2.6.11:
  version "2.6.12"
  resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123"
  integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==

//

vuex@^3.4.0:
  version "3.5.1"
  resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.5.1.tgz#f1b8dcea649bc25254cf4f4358081dbf5da18b3d"
  integrity sha512-w7oJzmHQs0FM9LXodfskhw9wgKBiaB+totOdb8sNzbTB2KDCEEwEs29NzBZFh/lmEK1t5tDmM1vtsO7ubG1DFw==

//

vue-router@^3.2.0:
  version "3.4.3"
  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.3.tgz#fa93768616ee338aa174f160ac965167fa572ffa"
  integrity sha512-BADg1mjGWX18Dpmy6bOGzGNnk7B/ZA0RxuA6qedY/YJwirMfKXIDzcccmHbQI0A6k5PzMdMloc0ElHfyOoX35A==

HelloWorld.vue は以下の通り。

<script lang="ts">
import Vue from "vue";

export default Vue.extend({
  name: "HelloWorld",
  props: {
    msg: String
  }
});
</script>

以上は、以下から行う比較のために参考として置いておきます。

vue add vue-next で Vue3 を入れる

参考: https://github.com/vuejs/vue-cli-plugin-vue-next

コマンドを実行します。

vue add vue-next

?  Installing vue-cli-plugin-vue-next...

yarn add v1.22.4
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...

success Saved lockfile.
success Saved 2 new dependencies.
info Direct dependencies
└─ vue-cli-plugin-vue-next@0.1.3
info All dependencies
├─ vue-cli-plugin-vue-next@0.1.3
└─ vue-loader@16.0.0-beta.5
✨  Done in 4.19s.
✔  Successfully installed plugin: vue-cli-plugin-vue-next


?  Invoking generator for vue-cli-plugin-vue-next...
?  Installing additional dependencies...

yarn install v1.22.4
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...
success Saved lockfile.
✨  Done in 7.65s.
⚓  Running completion hooks...

✔  Successfully invoked generator for plugin: vue-cli-plugin-vue-next
 vue-next  Installed vuex 4.0.
 vue-next  Documentation available at https://github.com/vuejs/vuex/tree/4.0
 vue-next  Installed vue-router 4.0.
 vue-next  Documentation available at https://github.com/vuejs/vue-router-next

インストールが完了しました。次に package.json の中身を確認してみます。

  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0-beta.1",
    "vue-router": "^4.0.0-alpha.6",
    "vuex": "^4.0.0-alpha.1"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^2.33.0",
    "@typescript-eslint/parser": "^2.33.0",
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-typescript": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0-beta.1",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/eslint-config-typescript": "^5.0.2",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.1.3",
    "eslint-plugin-vue": "^7.0.0-alpha.0",
    "prettier": "^1.19.1",
    "typescript": "~3.9.3",
    "vue-cli-plugin-vue-next": "~0.1.3"
  }

yarn.lock の中身も確認します。

vue@^3.0.0-beta.1:
  version "3.0.0-rc.10"
  resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.0-rc.10.tgz#31298a757b4fad6ee8973d0fa27c4fde8574bd01"
  integrity sha512-nRsyIQtOWLDMBb5dsPwg/WdIqznCMVWN6O6wJSzhseKC768wHlZKcJ7SPHhWPid9wi3Ykhtl9vtgvxTK/qICkw==
  dependencies:
    "@vue/compiler-dom" "3.0.0-rc.10"
    "@vue/runtime-dom" "3.0.0-rc.10"
    "@vue/shared" "3.0.0-rc.10"

// 略

vuex@^4.0.0-alpha.1:
  version "4.0.0-beta.4"
  resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0-beta.4.tgz#7b319ead2ef30432b7eaa4e37258ea3c5e44f698"
  integrity sha512-/+4E1dokq5cwbl4mohOqOj8h0vOLOWmLSqlqTf++bfmN9/JKWtwYfsBrzlK0sYrNfuYcpQeX0BVxQHoHXDfYZQ==

// 略

vue-router@^4.0.0-alpha.6:
  version "4.0.0-beta.9"
  resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.0-beta.9.tgz#8552c38c9b015527e74c9c6cb4c54868219d601e"
  integrity sha512-k8AGMm3LCTqnsEuF37AD4kcZVMwtnFEzdjACgmIII/xbLnTj3+o5XyH/zREBZutgv5q2hzlLltMVglqDQYMd/A==

package.json では vue: ^3.0.0-beta.1 となっていましたが、yarn.lock で最新版が落とされていることを確認しました。vue-router, vuex も同様です。

Error の解消

このままで yarn serve を実行すると、エラーが出ます。

 ERROR  Failed to compile with 6 errors                                 22:57:58

Module Error (from ./node_modules/vue-loader/lib/index.js):
[vue-loader] vue-template-compiler must be installed as a peer dependency, or a compatible compiler implementation must be passed via options.

// 略

Module build failed (from ./node_modules/vue-loader/lib/index.js):
TypeError: Cannot read property 'parseComponent' of undefined
    at parse (/Users/mi**/work/vue3-next-cli-plugin/node_modules/@vue/component-compiler-utils/dist/parse.js:15:23)
    at Object.module.exports (/Users/mi**/work/vue3-next-cli-plugin/node_modules/vue-loader/lib/index.js:67:22)

// 略

You may use special comments to disable some warnings.
Use // eslint-disable-next-line to ignore the next line.
Use /* eslint-disable */ to ignore all warnings in a file.
ERROR in src/main.ts:6:11
TS2345: Argument of type 'typeof import("/Users/mi**/work/vue3-next-cli-plugin/node_modules/vue/dist/vue")' is not assignable to parameter of type 'Component'.
  Type 'typeof import("/Users/mi**/work/vue3-next-cli-plugin/node_modules/vue/dist/vue")' is not assignable to type 'ComponentOptionsWithObjectProps<any, any, any, any, any, ComponentOptionsMixin, ComponentOptionsMixin, EmitsOptions, string, Readonly<{ [x: string]: any; }> | Readonly<...>>'.
    Type 'typeof import("/Users/mi**/work/vue3-next-cli-plugin/node_modules/vue/dist/vue")' is not assignable to type 'ComponentOptionsBase<Readonly<{ [x: string]: any; }> | Readonly<{ [x: string]: any; } & { [x: number]: any; }>, any, any, any, any, ComponentOptionsMixin, ComponentOptionsMixin, EmitsOptions, string>'.
      Types of property 'watch' are incompatible.
        Type '{ <T extends readonly (object | Ref<unknown> | ComputedRef<unknown> | (() => unknown))[], Immediate extends boolean = false>(sources: T, cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>, options?: WatchOptions<...> | undefined): WatchStopHandle; <T, Immediate extends boolean = false>(source: WatchSource...' is not assignable to type 'Record<string, ComponentWatchOptionItem>'.
          Index signature is missing in type '{ <T extends readonly (object | Ref<unknown> | ComputedRef<unknown> | (() => unknown))[], Immediate extends boolean = false>(sources: T, cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>, options?: WatchOptions<...> | undefined): WatchStopHandle; <T, Immediate extends boolean = false>(source: WatchSource...'.
    4 | import store from "./store";
    5 | 
  > 6 | createApp(App)
      |           ^^^
    7 |   .use(router)
    8 |   .use(store)
    9 |   .mount("#app");

ERROR in src/router/index.ts:1:10
TS2305: Module '"../../node_modules/vue-router/dist/vue-router"' has no exported member 'RouteConfig'.
  > 1 | import { RouteConfig, createRouter, createWebHistory } from "vue-router";
      |          ^^^^^^^^^^^
    2 | import Home from "../views/Home.vue";
    3 | 
    4 | const routes: Array<RouteConfig> = [

ERROR in src/store/index.ts:3:21
TS2339: Property 'createStore' does not exist on type '{ Store: typeof Store; mapState: Mapper<Computed> & MapperWithNamespace<Computed> & MapperForState & MapperForStateWithNamespace; ... 4 more ...; createLogger: <S>(option?: LoggerOption<...> | undefined) => Plugin<...>; }'.
    1 | import Vuex from "vuex";
    2 | 
  > 3 | export default Vuex.createStore({
      |                     ^^^^^^^^^^^
    4 |   state: {},
    5 |   mutations: {},
    6 |   actions: {},

ERROR in src/views/Home.vue:12:20
TS2339: Property 'extend' does not exist on type 'typeof import("/Users/mi**/work/vue3-next-cli-plugin/node_modules/vue/dist/vue")'.
    10 | import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
    11 | 
  > 12 | export default Vue.extend({
       |                    ^^^^^^
    13 |   name: "Home",
    14 |   components: {
    15 |     HelloWorld

一つずつ修正していきます。

src/**.vue 内を CompositionAPI に則した形で書く

component 内が Vue.extend のままなので、修正します。こちらは問題なく終了。

createApp について

こちらは Vue3 での変更点となります。

参考: https://github.com/vuejs/rfcs/blob/49de6decac4dc99840410d082e3bd7c98b580193/active-rfcs/0009-global-api-change.md

変更点: GlobalAPI の再設計によって、createApp が作成され、それによりアプリインスタンスのみにスコープされるようになりました。

自動で書き換えられていますが、型に関する変更を手動で行う必要があります。

shims-vue.d.ts の変更
参考: https://github.com/vuejs/vue-next-webpack-preview/issues/5

調べると以下の方がより型に沿っているそうなのですが、今回はとりあえず上記 Issue で議論された内容で修正します。

参考: https://dev.to/lmillucci/building-a-vue-3-component-with-typescript-4pge

shims-vue.d.ts を修正します。

  • before
  declare module "*.vue" {
    import Vue from "vue";
    export default Vue;
  }
  • after
  declare module "*.vue" {
    import { ComponentOptions } from "vue";
    const component: ComponentOptions
    export default component;
  }

compile エラーは無くなりました。

vue-loader

こちらはエラー内容から依存関係がうまくいっていないような気がしたため、そこに推測を立て、yarn.lock を確認しながら以下の方法で検証しました。

検証:

  1. "vue-cli-plugin-vue-next": "~0.1.3" を削除してインストールすると動く
  • before: yarn.lock
"vue-loader-v16@npm:vue-loader@^16.0.0-beta.3", vue-loader@^16.0.0-alpha.3:
  name vue-loader-v16
  version "16.0.0-beta.5"
  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.0.0-beta.5.tgz#04edc889492b03a445e7ac66e9226a70175ca8a0"
  integrity sha512-ciWfzNefqWlmzKznCWY9hl+fPP4KlQ0A9MtHbJ/8DpyY+dAM8gDrjufIdxwTgC4szE4EZC3A6ip/BbrqM84GqA==
  dependencies:
    "@types/mini-css-extract-plugin" "^0.9.1"
    chalk "^3.0.0"
    hash-sum "^2.0.0"
    loader-utils "^1.2.3"
    merge-source-map "^1.1.0"
    source-map "^0.6.1"
  • after: yarn.lock
"vue-loader-v16@npm:vue-loader@^16.0.0-beta.3", vue-loader@^16.0.0-alpha.3:
  version "16.0.0-beta.5"
  resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-16.0.0-beta.5.tgz#04edc889492b03a445e7ac66e9226a70175ca8a0"
  integrity sha512-ciWfzNefqWlmzKznCWY9hl+fPP4KlQ0A9MtHbJ/8DpyY+dAM8gDrjufIdxwTgC4szE4EZC3A6ip/BbrqM84GqA==
  dependencies:
    "@types/mini-css-extract-plugin" "^0.9.1"
    chalk "^3.0.0"
    hash-sum "^2.0.0"
    loader-utils "^1.2.3"
    merge-source-map "^1.1.0"
    source-map "^0.6.1"

2. 別にもう一つプロジェクトを立ち上げて yarn.lock を削除、インストールして検証した結果、問題なく動いたためやはりうまく依存関係が解決できなかっただけかなと思います。
こちらに関して何かありましたらご意見ください。

vue-router

参考: https://github.com/vuejs/vue-router-next

RouteConfigRouteRecordRaw に rename されるとあるので修正します。

  • src/router/index.ts
import { RouteRecordRaw, createRouter, createWebHistory } from "vue-router";
// 略

const routes: Array<RouteRecordRaw> = [
// 略

breaking changes の確認
  • mode: historyhistory に変更されます。こちらは以下の通りインストール時点で変換されていました。

src/router/index.ts

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
});
  • 全ての route を示す /* はカスタム正規表現 /:catchAll(.*) を用いて宣言するようになるとのことなので、以下のように修正して確認します。
const routes: Array<RouteRecordRaw> = [
  {
    path: "/:catchAll(.*)",
    name: "Home",
    component: Home
  },
];

BASE_URL/asdf 等 asdf 部分を良きように変えても Home component が表示されればOKです。

params の取り扱いについて

params が global な root を通す必要が無くなったため、どのように取得できるのか確認します。

  • /src/router/index.ts に以下のように追記
const routes: Array<RouteRecordRaw> = [
  {
    path: "/:catchAll(.*)",
    name: "Home",
    component: Home
  },
  {
    path: '/account/:id', component: Account // 追記
  },
  {
    path: "/about",
    name: "About",
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue")
  }
];

  • src/components/Account.vue を作成

template 内では従来通りに取得できます。

<template>
    {{ $route.params.id }}
</template>

setup 内では以下のようにして取得することができました。

<script lang="ts">
import { defineComponent } from 'vue';
import { useRouter } from 'vue-router';
import router from '@/router'


export default defineComponent ({
    name: 'Account',
    setup(props, context){

       const { currentRoute }  = router
       console.log(currentRoute.value.params.id)
    }
})
</script>

ただ、こちらは検証不十分のため、ベストプラクティスかわからず...
近い内にもう少し掘り下げたいと思います。

Vuex

参考: https://github.com/vuejs/vuex/tree/4.0#vuex-4

breaking changes として、 Vuex のインストールプロセスが変更になり、 createStore 関数を使用することになった、とのことなので修正します。

  • src/store/index.ts
import { createStore } from "vuex";

export default createStore({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
});

ここまでで compile エラーで出ていた箇所の修正は完了ですが、TS で使用する場合には独自にモジュール拡張を宣言する必要があるとのことなので、ドキュメント通りに修正しました。

  • vuex-shim.d.ts
import { ComponentCustomProperties } from "vue";
import { Store } from "vuex";

declare module "@vue/runtime-core" {
  interface State {
  }

  interface ComponentCustomProperties {
    $store: Store<State>;
  }
}

今回の breaking changes で、 Vue component 内でのグローバルな型付の削除が行われたため、完全に型付された構造を作ることができるようになりました。

debug をとって確かめるために、以下のように追加していきます。

  • vuex-shim.d.ts
import { ComponentCustomProperties } from "vue";
import { Store } from "vuex";

declare module "@vue/runtime-core" {
  interface State {
    count: number;
  }

  interface ComponentCustomProperties {
    $store: Store<State>;
  }
}
  • src/store/index.ts
import { createStore } from "vuex";

export default createStore({
  state: {
    count: 1
  },
  mutations: {},
  actions: {},
  modules: {}
});
  • src/components/index.ts
<script lang="ts">
import { defineComponent, SetupContext } from "vue";
import { useStore } from 'vuex';

export default defineComponent({
  name: "HelloWorld",
  props: {
    msg: String
  },
  setup() {
    const store = useStore()
    console.log(store.state.count) // 1 が表示される
  }
});
</script>

参考: https://blog.logrocket.com/whats-new-in-vuex-4/

以上となります。

Epilogue

今回は Vue3 を実装する上でどこから手をつければいいのか把握する、を目標に実際に手を動かして検証してみました。次回は vuejs/rfcs を触りつつもう少し広げて作ってみようと思います。

また実際触ってみて global にする必要がなくなった、という点を忘れてしまいどこに格納されている or どこから取得すればいいのか、と迷いましたが、スコープが小さくなったという点に気をつけ、Document を確認すれば特に困ることはあまりないのかな、と感じました。
ただ、Vue2 から Vue3 へ段階的に上げることができるのか、Vuex や vue-router の切替等、大きなプロジェクトの運用、進め方は やはり考慮しないといけないのか、と思います。

勉強途中のため、何かありましたらご連絡ください。

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

vue/composition-apiでscrollの状態を扱うComposition Functionを作った

概要

上向きにスクロールしたときに表示して、下向きにスクロールしたら隠すフッターを表示する要件が現れたので、useScrollを作成して、スクロールの向きを絶えずリアクティブに返すComposition Functionとして扱ってみました。

ソースコード

import { useWindowScroll } from '@vueuse/core'
import {
  reactive,
  toRefs,
  watch,
} from '@vue/composition-api'

export const useScroll = () => {
  const { x, y } = process.browser ? useWindowScroll() : { ...toRefs(reactive({x: 0, y: 0})) }

  const state = reactive<{
    isUp: boolean,
    isDown: boolean
  }>({
    isUp: false,
    isDown: false,
  })

  watch(() => y.value, (newY, oldY) => {
    state.isUp = newY < oldY
    state.isDown = newY > oldY
  })

  return {
    x,
    y,
    ...toRefs(state),
  }
}

解説

こちらのFunctionには、vueuseというライブラリを使わせてもらっています。
composition-apiを使った、特にブラウザのネイティブAPIで扱える値に関してリアクティブに活用できるFunctionがたくさんあります。

今回はuseWindowScrollを利用させていただきました。
これは、windowへのEvent Listenerとして、ステート管理しているスクロールの高さを変更するハンドラを登録していることによって、リアクティブに座標の値を管理できるようにしています。

composition-apiのwatchで、スクロールのy値を監視しており、変化したときの古い値との比較によって上昇中か、下降中かを判定しています。

使い方

こんな感じで書くと、スクロールの向きに応じて出たり消えたりするフッターが作れます。高さが70px決め打ちになっているのが少し悔やまれますが、Vue3でStyle周りの改善が入るらしいのでちょっとそれを心待ちにしていようと思っています。

<template>
  <footer :style="footerStyle" class="sync-scroll" :class="{ appear: isUp }">
    <slot />
  </footer>
</template>

<script lang='ts'>
import {
  computed,
  defineComponent,
} from '@nuxtjs/composition-api'
import { useScroll } from '~/composables/utils/window/useScroll'

export default defineComponent({
  setup() {
    const scrollState = useScroll()

    const footerStyle = computed(() => {
      if (scrollState.isUp) {
        return {
          height: '70px',
        }
      }

      return {
        bottom: '-70px',
        height: '70px',
      }
    })

    return {
      ...scrollState,
      footerStyle,
    }
  },
})
</script>

<style lang='scss' scoped>
@import '@/assets/css/variable.scss';

.sync-scroll {
  width: 100%;
  position: fixed;
  bottom: -70px;
  animation-name: hide;
  animation-duration: .4s;
  animation-timing-function: linear;

  &.appear {
    bottom: 0;
    animation-name: appear;
    animation-duration: .4s;
    animation-timing-function: linear;
  }
}

@keyframes appear {
  0% {
    bottom: -70px;
  }
  100% {
    bottom: 0;
  }
}
@keyframes hide {
  0% {
    bottom: 0;
  }
  100% {
    bottom: -70px;
  }
}
</style>

注意点

もとのuseWindowScrollが、スクロールのイベントハンドラにthrottleのような処理を噛ませていないっぽくて、全てのスクロールイベントに対してハンドラを呼び出しているようです。パフォーマンスを重視するならthrottleを噛ませたほうが良いと思います。

最後に

composition-apiにハマって日々いろいろ試して発信しているTwitterアカウントはこちらです。
https://twitter.com/Meijin_garden

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

【完全版】Nuxt.jsにおけるライフサイクルまとめ(SSR/SPA/layouts/pages/components別)

はじめに

Nuxtのライフサイクル記事はたくさんありますが、コンポーネント別にまとめてくれている記事がなかったため、自分用に作成しました。よく使うんですが、よく忘れるんですよね、、、。

前提

バージョン

Nuxt.js 2.14.4

補足

  • nuxtServerInit
    store/index.jsに記述しています。

  • beforeEach/afterEach
    pluginsにファイルを作成して、nuxt.config.jsで読み込んでいます。

  • fetch
    fetchは、公式推奨のcontextなし(引数なし)と非推奨のcontextあり(引数あり)の2種類があります。その同時実行はできませんが、流れをわかりやすくするために「fetch(有)」と「fetch(無)」という表記で両方記載しています。

  • computed
    各コンポーネント内のtemplateで呼び出した場合のタイミングを記載しています。

全体の流れ

以下の5つに分けてライフサイクルをまとめています。

  • SSR(初回アクセス時)
  • CSR(初回アクセス時)
  • SPA(初回アクセス時)
  • データ更新時
  • ページ遷移時

SSR(初回アクセス時)

modeをuniversalに設定かつ初回アクセス時にサーバーサイドで1回実行されます。

  1. plugins【nuxt.config.js】
  2. beforeEach【plugins:nuxt.config.js】
  3. afterEach【plugins:nuxt.config.js】
  4. nuxtServerInit【store】
  5. middleware【nuxt.config.js】
  6. middleware【layouts】
  7. middleware【pages】
  8. validate【pages】
  9. asyncData【pages】
  10. fetch(有)【pages】
  11. beforeCreate【layouts】
  12. created【layouts】
  13. fetch(無)【layouts】
  14. computed【layouts】
  15. beforeCreate【pages】
  16. created【pages】
  17. fetch(無)【pages】
  18. computed【pages】
  19. beforeCreate【component】
  20. created【component】
  21. fetch(無)【component】
  22. computed【component】

CSR(初回アクセス時)

SSR後に、クライアントサイドで1回だけ実行されます。

  1. plugin【nuxt.config.js】
  2. beforeEach【plugin:nuxt.config.js】
  3. afterEach【plugin:nuxt.config.js】
  4. beforeCreate【layouts】
  5. created【layouts】
  6. beforeMount【layouts】
  7. computed【layouts】
  8. beforeCreate【pages】
  9. created【pages】
  10. beforeMount【pages】
  11. computed【pages】
  12. beforeCreate【components】
  13. created【components】
  14. beforeMount【components】
  15. computed【components】
  16. mounted【components】
  17. mounted【pages】
  18. mounted【layouts】

SPA(初回アクセス時)

modeをspaに設定かつ初回アクセス時にクライアントサイドで1回実行されます。

  1. plugin【nuxt.config.js】
  2. beforeEach【plugin:nuxt.config.js】
  3. afterEach【plugin:nuxt.config.js】
  4. middleware【nuxt.config.js】
  5. middleware【layouts】
  6. middleware【pages】
  7. validate【pages】
  8. asyncData【pages】
  9. fetch(有)【pages】
  10. beforeCreate【layouts】
  11. created【layouts】
  12. beforeMount【layouts】
  13. fetch(無)【layouts】
  14. computed【layouts】
  15. beforeCreate【pages】
  16. created【pages】
  17. beforeMount【pages】
  18. fetch(無)【pages】
  19. computed【pages】
  20. beforeCreate【components】
  21. created【components】
  22. beforeMount【components】
  23. fetch(無)【components】
  24. computed【components】
  25. mounted【components】
  26. mounted【pages】
  27. mounted【layouts】

データ更新時

ページ内のデータが変更されるときに実行されます。

  1. beforeUpdate【layouts】
  2. beforeUpdate【pages】
  3. beforeUpdate【components】
  4. updated【components】
  5. updated【pages】
  6. updated【layouts】

ページ遷移時

SSRでもSPAでも静的サイトでもページ遷移動作は同じです。
※layoutは変更したときのみ

  1. beforeEach【plugin:nuxt.config.js】
  2. middleware【nuxt.config.js】
  3. middleware【layouts】
  4. middleware【pages】
  5. validate【pages】
  6. asyncData【pages】
  7. fetch(有)【pages】
  8. afterEach【plugin:nuxt.config.js】
  9. beforeDestroy【遷移元layouts】
  10. beforeDestroy【遷移元pages】
  11. beforeDestroy【遷移元components】
  12. destroyed【遷移元components】
  13. destroyed【遷移元pages】
  14. destroyed【遷移元layouts】
  15. beforeCreate【layouts】
  16. created【layouts】
  17. beforeMount【layouts】
  18. fetch(無)【layouts】
  19. computed【layouts】
  20. beforeCreate【pages】
  21. created【pages】
  22. beforeMount【pages】
  23. fetch(無)【pages】
  24. computed【pages】
  25. beforeCreate【components】
  26. created【components】
  27. beforeMount【components】
  28. fetch(無)【components】
  29. computed【components】
  30. mounted【components】
  31. mounted【pages】
  32. mounted【layouts】

ファイル別の利用可能ライフサイクル

あれ、これってここで記述できるんだっけ?ってことが度々あるので、こちらもまとめました。

layouts

  • middleware
  • beforeCreate
  • created
  • fetch(無)
  • beforeMount
  • computed
  • mounted
  • beforeUpdate
  • update
  • beforeDestroy
  • destroy

pages

  • middleware
  • validate
  • asyncData
  • fetch(有)
  • beforeCreate
  • created
  • fetch(無)
  • beforeMount
  • computed
  • mounted
  • beforeUpdate
  • update
  • beforeDestroy
  • destroy

component

middrewareが呼び出せないこと以外は、基本的にlayoutsと同じです。

  • beforeCreate
  • created
  • fetch(無)
  • beforeMount
  • computed
  • mounted
  • beforeUpdate
  • update
  • beforeDestroy
  • destroy

その他

今回整理する中で、明確になったことまとめ。

fetchの仕様変更

私の知っているfetchいつの間にか非推奨になっていた、、、
まぁ確かに、asyncDataで事足りてはいましたからね。

2020/9/10時点では、context有fetchだと、asyncDataの次に呼ばれ、なしfetchだと、SSRではcreatedの次、CSRではbeforeMountedの次に呼ばれます。

ですので、context無の場合だと、thisを使ってdataやcomputedにアクセスができるんですよね。どんな場合に利用するのかは、下記の記事がわかりやすくまとめてくれています。

Nuxt2.12.0で新しくなったfetchについて

computedの実行されるタイミング

  • computedに記載しただけでは実行されない。(template内や関数などで呼ぶ必要がある)
  • template内での呼び出しの場合は、beforeMountとmountedの間で実行される。
  • ライフサイクルから呼び出しができるのは、beforeCreate以降。

同一レイアウトのページ遷移時はmiddlewareのみ実行

同一レイアウトの場合、ページ遷移のたびに実行されるのはmiddlewareのみで、それ以外はレイアウトが切り替わるまで実行されません。

dataにアクセスできるのはcreated以降

beforeCreateではアクセスできませんでした。地味な発見。

静的ページを生成した場合のライフサイクル

基本的にSSRでのgenerateする形になると思いますので、generate時にSSRのライフサイクルが実行されます。そこで生成されたソースにアクセスする形になりますが、その後(CSRとページ遷移)は全く一緒のライフサイクルになります。

beforeEachとafterEachの存在

今回、この2つの存在を初めて知りました。
私は、ページ遷移の共通処理はいつもmiddlewareでやってるんですが、より細かい事をしようと思ったときに活躍してくれそうな予感、、、

Nuxt.jsでページ遷移ごとに共通の処理を実行する方法

pluginsは初回に1回しか呼ばれない

いや、これ当たり前といえば当たり前なことなんですが、改めて顕在化されました。

1回だけ処理したい系は基本ここなんですよね。

例えば、SPAモードでnuxtServerInit的なことをしたい場合も、以下の記事のようにpluginsを使って実装します。

[Nuxt.js]nuxtServerInitがSPAモードでは使えないので代替のアクションを用意する

おわりに

もし違いがあれば、ご指摘いただけると幸いです。

参考

Nuxt.js(Vue.js)SSRのライフサイクルを完全に理解したい(wip)
Vue.js と Nuxt.js のライフサイクル早引きメモ
Nuxt.jsにおけるサーバーサイドレンダリングの挙動とライフサイクル

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

【Nuxt.js+Firebase】ログイン不要!気軽に投稿できるオススメのカレー共有サイトを作りました!

カレーの時代到来

近年、カレーブームがますます加速しているように思われます。

ちょうど今、西武池袋で行われてるカレーイベントでは連日行列が続き、
売り切れ商品も多数あるとのこと!

かくいう私も、カレーフリーク。
カレー専用のInstagramを開設し、食べ歩いたカレーを投稿しています。

今回、静的サイトを作る練習として、Nuxt.jsとFirebaseを使って、カレー屋さんの共有サイトを作ってみました!

食べログやInstagramと違い、アカウントは不要、匿名で気軽に投稿できるのが特徴です♪
下記にリンクを貼っているので、ぜひ投稿してみてください!

カレー共有サイト「Curry Freak」

サイトURL ▶▶▶ https://curryfreak.ml/

「新しいCurryを追加」のページから、投稿画面が確認できます。
Image from Gyazo

入力が完了すると、投稿ボタンが出現!
(アップロードが完了する前に投稿するとエラーになるので)
Image from Gyazo

投稿ボタンを押すと、「Curry一覧」ページに遷移して、
投稿したカレー屋さん情報をご覧いただけます♪
Image from Gyazo

作り方

まずNuxt.jsでプロジェクトを作成(参考記事
UIフレームワークを選べるので、今回Buefyを選択しました。

yarn add firebaseでFirebaseのライブラリを追加

yarn dev で動作確認しながら、ページやコンポーネントを作成

UIをちょこっと修正

yarn generateを実行し、distフォルダをNetlifyでデプロイ

独自ドメインを設定して、完成♪(参考記事

ページなどのソースコードはgithubにあげたのでご覧ください!

ソースコード ▶▶▶ https://github.com/twtjudy1128/CurryFreak

つまずいたところ

Firebaseの呼び出し

序盤で「Firebase App named '[DEFAULT]' already exists」というエラーが出て進まず。
Image from Gyazo

こちらの記事を拝見し、
何度もFirebaseを初期化して呼び出してしまっていることが原因だとわかったので、
以下のように初期化のコードを修正したら、すぐエラーが消えました♪

            // Initialize Firebase
            if (firebase.apps.length === 0) {
            firebase.initializeApp(config);
            }

V-modelの使い方

v-onとv-bindをまとめて書くことができるv-model。
フォームで色んな人が使ってるのを見て、私も投稿画面で使ってみました。

     <div class="postform">
        <div>
          <input v-model="title" placeholder="店名"><br>
          <input v-model="name" placeholder="名前"><br>
          <input v-model="memo" placeholder="ひとこと"><br>
          <input v-show="!image_url" type="file" id="image_file" @change="onFileChange"  accept="image/*" required/>
          <b-button type="is-warning" v-on:click='post' v-show="show"><b>投稿</b></b-button>
        </div>
      </div>

ところが、以下のように真っ赤になっちゃいました><

Image from Gyazo

調べると、対応するデータが定義されていなかったので、script部分で、以下のように定義。

data(){
    return {
      image_url: null,
      title:'',
      name:'',
      memo:'',
      downloadURL:'',
      show: false,
    };

すると今度は、以下のエラー

The “data” option should be a function that returns a per-instance value in component definitions.

子コンポーネントでは、dataをオブジェクトではなく、関数として定義する必要があるとのこと・・・(難しい)
というわけで、以下のようにちょこっと変更しただけでエラーが綺麗に消えました!

data:function(){
    return {
      image_url: null,
      title:'',
      name:'',
      memo:'',
      downloadURL:'',
      show: false,
    };

【参考記事】
Vue.jsのv-modelを正しく使う
【Vue.js】The “data” option should be a function that returns a per-instance value in component definitions.というエラーについて

画像とテキストを一緒に投稿する

1番苦戦したところです。笑

Image from Gyazo

上記のような構造を目指して、あーだこーだやってみたのですがエラー続きで心折れかけました。笑

その時に、GyazoやimgurのAPIを使う方法を見つけたのでトライしてみることに。
ところが、GyazoAPIはアクセス権限で引っかかり使えず…。

imgurで試してみたのですが、下記のようなエラーが出ました…(今だ解決しておらず)
Image from Gyazo

かなり苦戦していたのですが、 @tkyko13 さんにご協力いただき、
本来やりたかったFirebase StorageとCloud Firestoreを使った方法でうまく投稿できるようになりました。(大感謝)

コード長くなるので、ソースコードのpost.vueをご覧いただけると幸いです!
ソースコード ▶▶▶ https://github.com/twtjudy1128/CurryFreak

心残りなPOINT

・投稿日時も入れればよかった
・投稿した順に表示できるようにしたい
・画像をアップロードしている間、「アップロード中」のクルクルみたいのを出したい
・UIをもっと綺麗にしたい(フレームワークは便利だけどカスタムのコツがまだ掴めてない)
・ロゴを作りたい
・もう1つページを増やして、おふざけ要素作りたかった

色々やりたいこと挙げるとキリがないですね・・・
でも、手こずりながらも、また1つアウトプットできたことが嬉しいです。

あなたのオススメのカレー屋さんを教えてください♪

最後までご覧いただき、ありがとうございます!
匿名で簡単に投稿できるので、是非あなたのオススメカレーを投稿してみてくださいね!

カレー共有サイト「Curry Freak」 ▶▶▶ https://curryfreak.ml/
(9/11 0:30現在 ストレージの制限超過で画像表示されなくなっています)

みんなでカレー食べて、免疫つけて、今日も1日がんばりまっしょー!!!!!

(*^^)v「よろしければLGTMも宜しくお願いします!」

 

<9/10 18:28追記>
もうこんなに投稿集まってきました~ありがとうございます!
こういう機能も欲しいなどフィードバックもいただけて嬉しいです!
バシバシ投稿よろしくお願いします!
Image from Gyazo

※私の独断で不適切だと思った画像は随時削除しております。ご了承ください。

<9/11 0:30追記>
 ストレージの制限超過で一時的に画像が表示されなくなってしまいました…。
でも、それだけ多くのアクセスがあったということなので、前向きに捉えていこうと思います!
 こういう事態に直面しないと、自分では予測できなかったと思うので、これを機にリサイズの実装などもできないか検討してみようと思います。

<9/11 16:22追記>
画像復活しました!どうやら1日1GBという制限があるようです。やはりリサイズが必要ですねん。

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

【Nuxt.js + Firebase】写真を投稿するとAIがハッピー指数を測定してランキング付けするシステムを作ったよ

作ったもの

タイトルの通り、写真を投稿すると、どれくらいハッピーなのかAIがハッピー指数を算出します。
スクリーンショット 2020-09-09 22.48.59.png
写真はパクタソさんより

※「ハッピー指数」としましたが、これって「指数」って言ってもいいものだろうか不安になりましたが、頭の悪そうな語感が気に入ったのでこのまま「ハッピー指数」とします。

そして、同時にハッピー指数のランキングを表示します。

スクリーンショット 2020-09-09 22.57.11.png
写真はパクタソさんより

おお、眩しすぎる。

という訳で、
あなたが世界でどれくらい幸せなのか、調べてみましょう。

公開URL

ウェブアプリとして公開していますので、是非お試してください。

URL:
http://happy-ranking.tk/

ちなみにこのドメインはこちらで取得しました。
freenom

環境

フレームワーク :Nuxt.js
CSSフレームワーク:BULMA
開発プラットフォーム:Firebase

技術面

表情解析

アップロードした写真を、face-api.js というTensorFlow.jsで学習済みの顔認識の機械学習モデルのライブラリに投げて、表情からハッピー度を取得します。
ハッピー度は0から1の間で、大きいほどハッピー度が高いです。

faceapi.resizeResults(detections, displaySize)[0].expressions.happy =
0.999991774559021

ハッピー度はこんな感じで取得されます。
小数点が15桁くらいあるので、1000倍して見やすくしました。

(ご参考)自分の過去記事
【忙しい現代人のために】表情で扇風機を操作するシステムを作ったよ

写真およびデータ保存

  • FirebaseのCloud FireStoreというストレージに写真を保存 storage.png

ここでハマったことを別記事に書きました。
【Firebase + Nuxt.js】FirebaseStorageへの画像アップロードでハマったところ

  • あとで呼び出すために、写真を保存した場所のURLを、FirebaseのFirestoreに保存 (下記画像の picURLフィールド) firestore.png

ハマったところ

Nuxt.js、Vue.js

リストの中でのイメージの表示方法

こちらを見て解決しました。ありがとうございました。
【Vue.js】imgタグのsrc要素は指定の仕方によって読み込み方が違う

たった、これだけなんですけど、凄くハマりました。

<img :src="data.picurl"/>

非同期処理のコードの書き方

以下のようなコードのことです。
毎回、頭がおかしくなりそうです。
=> を多用すると、訳わからなくなります。
じっくりと勉強しなければ。

Pic.vue
methods: {
  post(pic){
                〜中略〜        
                // 画像をStorageにアップロード
                storageRef.put(file).then(() => {   
                    let debug_document =  document.getElementById("happyScore");
                    debug_document.innerHTML = "しばらくお待ちください";

                    // アップロードした画像のURLを取得 
                    const picurl = firebase.storage().ref(fileName).getDownloadURL().then((url) => {    
                        const happyScoreGet = getFaceData( document.getElementById('attachedFile')).then((happyScore) => {
                            let debug_document =  document.getElementById("happyScore");
                            let realhappyScore = happyScore;
                            happyScore = Math.floor(happyScore * 1000); //1000倍
                            debug_document.innerHTML = "ハッピー指数: "+String(happyScore)+"点";
                            //firestoreにデータを保存
                            const setScore = docRef.set({
                                name: this.name,
                                happyScore: happyScore,
                                realhappyScore: realhappyScore ,
                                fileName: fileName ,
                                picurl: url
                            });
                    //ランキング作成へ
                    this.get();
                    });

                }).catch((error) => {
                    console.log(error)
                })
            })
        }
}



face-api.js のファイルのままデプロイすると怒られた

[BABEL] Note: The code generator has deoptimised the styling of /pages/face-api.js as it exceeds the max of 500KB.

500KB以上はダメらしいので、
https://github.com/justadudewhohacks/face-api.js/
に書いてある通り、npmでインストールしました。

npm i face-api.js



face-api.jsの機械学習モデルの場所はstaticフォルダへ

同じディレクトリに保存してはいけません。
デプロイされません。





ハマったところは、その他、たくさんあったけど、書ききれないです。




やりたかったけど出来なかったこと

・送信ボタンを付けたかった
(添付ファイルを選んだ瞬間に送信される)
・画像サイズ制限
・画像をリサイズして保存する
・ハッピー指数がでた瞬間に順位が出るように

コード

index.vue
<template>
  <div class="container">
    <div>
      <br><br>
      <h1 class="title">
        ハッピー・ランキング
      </h1>
      写真を投稿するとハッピー指数を判定して、ランキングします。<br>
      ハッピー指数は1000点が最高得点です。<br><br>
      ※投稿写真は作者が管理しているクラウドサーバーに保存されますので、ご注意ください。
      <br><hr><br><br>
      <client-only placeholder="Loading...">
        <Pic />
      </client-only>
    </div>
  </div>
</template>

<script defer src="face-api.js"></script>
<script defer src="scripts.js"></script>

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

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

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

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

#happyScore{
  font-size: 30px;
  font-weight: bold; 
}

.list{
padding-top: 50px;
}

</style>

index.vue
<template>
    <div>
        投稿者名:<input v-model="name" placeholder="投稿者名">           
        <br><br>
        <input @change="post" type="file"  data-label="画像の添付">
        <br>
        <img id="attachedFile" width=350 v-show="uploadedImage" :src="uploadedImage"  />

        <div id="happyScore"></div>
        <br><hr>

      <div class="list">
            <h1 class="title">
                ハッピー指数 トップ100
            </h1>
            <br>
            <ul v-for="(data, index) in allData" :key="data.id" class="menu-list" >
                <li>
                順位: {{index + 1}}<br>
                ハッピー指数 :  {{data.happyScore}}<br>
                投稿者名:{{data.name}} <br>
                <img width=350 :src="data.picurl"/>
                </li>
                <br><br>
            </ul>
      </div>
        <br>
    </div>
</template>

<script>
    import firebase from "firebase/app";
    import "firebase/firestore";
    import 'firebase/storage';
    import * as faceapi from 'face-api.js';
    import uuid from 'uuid';

    export default {
        components: {},

        data(){
            return{
                db: {},
                allData: [],
                name: '',
                fileName: '',
                picurl: '',
                uploadedImage: '',
                happyScore: '',
                realhappyScore: '',
                testId: ''
            }
        },

        methods: {
            //初期化、設定 
            //各人の数値を入れること
            init: () => {
                const config = {
                    apiKey: "",
                    authDomain: "",
                    databaseURL: "",
                    projectId: "",
                    storageBucket: "gs://xxxxxx-99999.appspot.com",
                    messagingSenderId: "",
                    appId: "",
                    measurementId: ""
                };

                // Initialize Firebase
                firebase.initializeApp(config);       
            },

            post(pic){
                const file = pic.target.files[0];
                if(!file.type.match('image.*')) {
                    alert("画像ファイルでお願いします");
                return;
                }

                //イメージファイル描画
                let reader = new FileReader();  
                reader.onload = (pic) => {
                    this.uploadedImage = pic.target.result;
                };
                let imagefiles = pic.target.files || pic.dataTransfer.files;
                reader.readAsDataURL(imagefiles[0]);
                let attachedFile = document.getElementById('attachedFile');           
                const testId = firebase.firestore().collection('pics').doc().id; //ユニークなIDを生成
                const docRef = firebase.firestore().collection('pics').doc(testId);
                const fileName = uuid(); //ファイル名は他と被らないように uuid ライブラリを使って動的に生成
                const storageRef = firebase.storage().ref(fileName);          

                // 画像をStorageにアップロード
                storageRef.put(file).then(() => {   
                    let debug_document =  document.getElementById("happyScore");
                    debug_document.innerHTML = "しばらくお待ちください";

                    // アップロードした画像のURLを取得 
                    const picurl = firebase.storage().ref(fileName).getDownloadURL().then((url) => {    
                        const happyScoreGet = getFaceData( document.getElementById('attachedFile')).then((happyScore) => {
                            let debug_document =  document.getElementById("happyScore");
                            let realhappyScore = happyScore;
                            happyScore = Math.floor(happyScore * 1000); //1000倍
                            debug_document.innerHTML = "ハッピー指数: "+String(happyScore)+"";
                            //firestoreにデータを保存
                            const setScore = docRef.set({
                                name: this.name,
                                happyScore: happyScore,
                                realhappyScore: realhappyScore ,
                                fileName: fileName ,
                                picurl: url
                            });
                    //ランキング作成へ
                    this.get();
                    });

                }).catch((error) => {
                    console.log(error)
                })
            })
        },

        //データ取得
        get: function(){
            this.allData = [];
            //スコアの降順に100個取得    
            firebase.firestore().collection('pics').orderBy('realhappyScore', 'desc').limit(100).get().then(snapshot => {
                snapshot.forEach(doc => {              
                    this.allData.push(doc.data());
                })
            });    
        }
        },  
        mounted(){
        //ページ読み込み時に実行される
        this.init();
        },
    }

    //表情取得
    async function getFaceData(img) { 
        await faceapi.nets.tinyFaceDetector.load("/models") ;//モデル読み込み
        await faceapi.nets.faceLandmark68Net.load("/models") ;//モデル読み込み
        await faceapi.nets.faceRecognitionNet.load("/models") ;//モデル読み込み
        await faceapi.nets.faceExpressionNet.load("/models") ;//モデル読み込み

        const detectionsWithLandmarks = await faceapi.detectAllFaces(img,new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks();
        if (detectionsWithLandmarks.length == 0){
            alert('人間じゃないよ');
        return(0)
        }else{
            const displaySize = { width: attachedFile.width, height: attachedFile.height }
            //1つの顔だけなのでfaceapi.detectAllFacesではなくて detectSingleFaceでよいはずが、本件はdetectAllFacesを使った。
            const detections = await faceapi.detectAllFaces(attachedFile , new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions()
            const resizedDetections = faceapi.resizeResults(detections, displaySize)
            return(resizedDetections[0].expressions.happy); //ハッピー指数を返す
        }
    }

</script> 












番外編 (うちわ受け)

現在、一緒にProtoOut Studioで学んでる受講生が今までQiitaで取り上げた人物で試してみました。

Juri Tawaraさん
代表作:ジェイソン・ステイサムで妄想するのが日課になっていたので、いっそBOTにしてみた。
ステイサム.png

[UhRhythm](https://qiita.com/UhRhythm)さん
【Vue.js】さ迷うハロオタがお誕生日カレンダーを作った
スクリーンショット 2020-09-09 22.44.53.png

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

【Nuxt.js + Firebase】写真を投稿するとAIがハッピー指数を測定してランキング付けするリア充向けのシステムを作ったよ

作ったもの

タイトルの通り、写真を投稿すると、どれくらいハッピーなのかAIがハッピー指数を算出します。
スクリーンショット 2020-09-09 22.48.59.png
写真はパクタソさんより

※「ハッピー指数」としましたが、これって「指数」って言ってもいいものだろうか不安になりましたが、頭の悪そうな語感が気に入ったのでこのまま「ハッピー指数」とします。



そして、同時にハッピー指数のランキングを表示します。

スクリーンショット 2020-09-09 22.57.11.png
写真はパクタソさんより

おお、眩しすぎる。
キラキラする。
まさにリア充向けシステムだ。

という訳で、
あなたが世界でどれくらい幸せなのか、調べてみましょう。

公開URL

ウェブアプリとして公開していますので、是非お試してください。

URL:
http://happy-ranking.tk/

ちなみにこのドメインはこちらで取得しました。
freenom

環境

フレームワーク :Nuxt.js
CSSフレームワーク:BULMA
開発プラットフォーム:Firebase

技術面

表情解析

アップロードした写真を、face-api.js というTensorFlow.jsで学習済みの顔認識の機械学習モデルのライブラリに投げて、表情からハッピー度を取得します。
ハッピー度は0から1の間で、大きいほどハッピー度が高いです。

faceapi.resizeResults(detections, displaySize)[0].expressions.happy =
0.999991774559021

ハッピー度はこんな感じで取得されます。
小数点が15桁くらいあるので、1000倍して見やすくし、ハッピー指数ということにしました。

(ご参考)自分の過去記事
【忙しい現代人のために】表情で扇風機を操作するシステムを作ったよ

写真およびデータ保存

  • FirebaseのCloud FireStorageというストレージに写真を保存 storage.png

ここでハマったことを別記事に書きました。
【Firebase + Nuxt.js】FirebaseStorageへの画像アップロードでハマったところ

  • あとで呼び出すために、写真を保存した場所のURLを、FirebaseのFirestoreに保存 (下記画像の picURLフィールド) firestore.png

ハマったところ

リストの中でのイメージの表示方法

こちらを見て解決しました。ありがとうございました。
【Vue.js】imgタグのsrc要素は指定の仕方によって読み込み方が違う

たった、これだけなんですけど、凄くハマりました。

<img :src="data.picurl"/>

非同期処理のコードの書き方

非同期処理が苦手です。
then(() =>{ とか、Promiseとか async とか await とか・・・
毎回、頭が混乱してしまいます。



じっくりと勉強しなければ。


face-api.js のファイルのままデプロイすると怒られた

[BABEL] Note: The code generator has deoptimised the styling of /pages/face-api.js as it exceeds the max of 500KB.

500KB以上はダメらしいので、
https://github.com/justadudewhohacks/face-api.js/
に書いてある通り、npmでインストールしました。

npm i face-api.js


face-api.jsの機械学習モデルの場所はstaticフォルダへ

同じディレクトリに保存してはいけません。
デプロイされません。





ハマったところは、その他、たくさんあったけど、書ききれないです。




やりたかったけど出来なかったこと

・送信ボタンを付けたかった
(今のは、添付ファイルを選んだ瞬間に送信される)
・画像をリサイズして保存したかった
・ハッピー指数がでた瞬間に順位が出るようにしたかった

コード

index.vue
<template>
  <div class="container">
    <div>
      <br><br>
      <h1 class="title">
        ハッピー・ランキング
      </h1>
      写真を投稿するとハッピー指数を判定して、ランキングします。<br>
      ハッピー指数は1000点が最高得点です。<br><br>
      ※投稿写真は作者が管理しているクラウドサーバーに保存されますので、ご注意ください。
      <br><hr><br><br>
      <client-only placeholder="Loading...">
        <Pic />
      </client-only>
    </div>
  </div>
</template>

<script defer src="face-api.js"></script>
<script defer src="scripts.js"></script>

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

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

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

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

#happyScore{
  font-size: 30px;
  font-weight: bold; 
}

.list{
padding-top: 50px;
}

</style>

index.vue
<template>
    <div>
        投稿者名:<input v-model="name" placeholder="投稿者名">           
        <br><br>
        <input @change="post" type="file"  data-label="画像の添付">
        <br>
        <img id="attachedFile" width=350 v-show="uploadedImage" :src="uploadedImage"  />

        <div id="happyScore"></div>
        <br><hr>

      <div class="list">
            <h1 class="title">
                ハッピー指数 トップ100
            </h1>
            <br>
            <ul v-for="(data, index) in allData" :key="data.id" class="menu-list" >
                <li>
                順位: {{index + 1}}<br>
                ハッピー指数 :  {{data.happyScore}}<br>
                投稿者名:{{data.name}} <br>
                <img width=350 :src="data.picurl"/>
                </li>
                <br><br>
            </ul>
      </div>
        <br>
    </div>
</template>

<script>
    import firebase from "firebase/app";
    import "firebase/firestore";
    import 'firebase/storage';
    import * as faceapi from 'face-api.js';
    import uuid from 'uuid';

    export default {
        components: {},

        data(){
            return{
                db: {},
                allData: [],
                name: '',
                fileName: '',
                picurl: '',
                uploadedImage: '',
                happyScore: '',
                realhappyScore: '',
                testId: ''
            }
        },

        methods: {
            //初期化、設定 
            //各人の数値を入れること
            init: () => {
                const config = {
                    apiKey: "",
                    authDomain: "",
                    databaseURL: "",
                    projectId: "",
                    storageBucket: "gs://xxxxxx-99999.appspot.com",
                    messagingSenderId: "",
                    appId: "",
                    measurementId: ""
                };

                // Initialize Firebase
                firebase.initializeApp(config);       
            },

            post(pic){
                const file = pic.target.files[0];
                if(!file.type.match('image.*')) {
                    alert("画像ファイルでお願いします");
                return;
                }

                //イメージファイル描画
                let reader = new FileReader();  
                reader.onload = (pic) => {
                    this.uploadedImage = pic.target.result;
                };
                let imagefiles = pic.target.files || pic.dataTransfer.files;
                reader.readAsDataURL(imagefiles[0]);
                let attachedFile = document.getElementById('attachedFile');           
                const testId = firebase.firestore().collection('pics').doc().id; //ユニークなIDを生成
                const docRef = firebase.firestore().collection('pics').doc(testId);
                const fileName = uuid(); //ファイル名は他と被らないように uuid ライブラリを使って動的に生成
                const storageRef = firebase.storage().ref(fileName);          

                // 画像をStorageにアップロード
                storageRef.put(file).then(() => {   
                    let debug_document =  document.getElementById("happyScore");
                    debug_document.innerHTML = "しばらくお待ちください";

                    // アップロードした画像のURLを取得 
                    const picurl = firebase.storage().ref(fileName).getDownloadURL().then((url) => {    
                        const happyScoreGet = getFaceData( document.getElementById('attachedFile')).then((happyScore) => {
                            let debug_document =  document.getElementById("happyScore");
                            let realhappyScore = happyScore;
                            happyScore = Math.floor(happyScore * 1000); //1000倍
                            debug_document.innerHTML = "ハッピー指数: "+String(happyScore)+"";
                            //firestoreにデータを保存
                            const setScore = docRef.set({
                                name: this.name,
                                happyScore: happyScore,
                                realhappyScore: realhappyScore ,
                                fileName: fileName ,
                                picurl: url
                            });
                    //ランキング作成へ
                    this.get();
                    });

                }).catch((error) => {
                    console.log(error)
                })
            })
        },

        //データ取得
        get: function(){
            this.allData = [];
            //スコアの降順に100個取得    
            firebase.firestore().collection('pics').orderBy('realhappyScore', 'desc').limit(100).get().then(snapshot => {
                snapshot.forEach(doc => {              
                    this.allData.push(doc.data());
                })
            });    
        }
        },  
        mounted(){
        //ページ読み込み時に実行される
        this.init();
        },
    }

    //表情取得
    async function getFaceData(img) { 
        await faceapi.nets.tinyFaceDetector.load("/models") ;//モデル読み込み
        await faceapi.nets.faceLandmark68Net.load("/models") ;//モデル読み込み
        await faceapi.nets.faceRecognitionNet.load("/models") ;//モデル読み込み
        await faceapi.nets.faceExpressionNet.load("/models") ;//モデル読み込み

        const detectionsWithLandmarks = await faceapi.detectAllFaces(img,new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks();
        if (detectionsWithLandmarks.length == 0){
            alert('人間じゃないよ');
        return(0)
        }else{
            const displaySize = { width: attachedFile.width, height: attachedFile.height }
            //1つの顔だけなのでfaceapi.detectAllFacesではなくて detectSingleFaceでよいはずが、本件はdetectAllFacesを使った。
            const detections = await faceapi.detectAllFaces(attachedFile , new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions()
            const resizedDetections = faceapi.resizeResults(detections, displaySize)
            return(resizedDetections[0].expressions.happy); //ハッピー指数を返す
        }
    }

</script> 


















番外編 (内輪ネタ)

現在、一緒にProtoOut Studioで学んでる受講生が今までQiitaで取り上げた人物で試してみました。

Juri Tawaraさん
代表作:ジェイソン・ステイサムで妄想するのが日課になっていたので、いっそBOTにしてみた。
ステイサム.png

UhRhythmさん
【Vue.js】さ迷うハロオタがお誕生日カレンダーを作った
スクリーンショット 2020-09-09 22.44.53.png

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

みんなで物語をつくりながら連想力を鍛えてアイデア発想力を磨こう

アイデア発想力とは?

アイデアとは、情報と情報の掛け合わせであると言われています。
その掛け合わせるために必要な力が、連想力です。

例えば、情報Aと情報Bの関連性を発見したり、情報Aと情報Bをベースにホップ・ステップ・ジャンプで飛躍してみたりするような連想によって、新しいアイデアを生み出していきます。

なので、アイデアを生み出す力を磨くには、連想力を磨くことが大事です。

じゃあ、どうやると磨いていけるのか?

言葉遊びゲーム「空文字アワー」とは?

その1つの方法が、言葉遊びゲーム「空文字アワー」です。

このゲームは、ある一文から連想した情報を文に追加することで、
新たな物語をつくっていきます。

まあやってみるとわかると思うので、
詳細はこちらをご覧いただき、
ぜひ最新の文に情報を追加してみてください。

使った技術

・nuxt.js
・Vue.js
・FirebaseのCloud Firestore

実装の仕方

今回は、初めてnuxt.jsってやつを使いました。
正直使いこなすには程遠いですが、それでも、nuxt.js、Vue.js、Cloud Firestoreを使い、独自ドメインでNetlifyで公開まで持っていけたのは進歩です。

nuxt.jsの流れ

yarn create nuxt-app [任意のプロジェクト名]

今回のUI frameworkは、

UI framework: Bulma

を使用。

ひとまず形ができたら、

$ yarn dev

をして、ローカルサーバーからサイトが閲覧できるようにしながら、
pagesやComponentsの中のファイルを作りました。

で、それができたら、

$ yarn generate

する。

これによって、distファイルができるので、そのファイルごとNetlifyに取り込むと、すぐにWEBアプリが公開できました。

独自ドメインでの公開の流れ

まずは無料で独自ドメインを取得します。色々無料で取得する方法があるようですが、今回はfreenomで取得しました。

image10.png

freenomで取得する際の流れはこちらを参照しました。

ドメインを取得できたら、My domainのURLや情報をNetlifyの方に入力すると、少し時間はかかかるものの、わあしはすぐににWEBが独自ドメインで公開できます。

nuxt.jsのpages

index.vue
<template>
  <div class="container">
    <div>
      <Logo />
      <h1 class="title">
        【言葉ゲーム】空文字アワー
        <p>~ないものつなぎ~</p>
      </h1>
      <div class = "content-explain">
        <p>ある簡単な文に( )があります。ここに「言葉」を入れ、さらに( )を加えます。
          次の人も同じことをする。これを全員で繰り返していくゲームです。
          「空文字アワー」は、どんどんつないで変化を起こすのが醍醐味です。
          最終的にどんなストーリーができあがるかは、みなさんのセンス次第。</p>
      </div>
      <div class = "content-explain">
        <h2>進め方</h2>
        <ol>
          <li>最初の一文と( )を提示します。</li>
          <li>次に答える人が( )に言葉を加え、自分の言葉を加えた( )を外します。</li>
          <li>さらに( )を好きな位置に加えます。これを期間内繰り返します。</li> 
        </ol> 
      </div>

      <div class = "content-explain2">
        <h2>ルール</h2>
        <ol>
          <li>連続して同じ人が答えることはできません。</li>
          <li>連続しない限り、期間中何度でも回答できます。</li>
          <li>手前の全ての回答をコピペして残してください。</li> 
          <li>元の文を修正することはできません。</li>
          <li>文意が伝わるように展開していきましょう。</li> 
        </ol> 
      </div>

      <div class = "content-explain2">
        <h2>例はこちら</h2>
        <p>【Hiro】東京では( )雨が降っていた。</p>
        <p>【お名前】( )東京では季節はずれの雨が降っていた。</p>
        <p>【お名前】高層ビルの立ち並ぶ東京では季節はずれの雨が( )降っていた。</p>
        <p>【お名前】高層ビルの立ち並ぶ東京では季節はずれの雨がしとしとと降っていた( )。</p>
        <p>【お名前】高層ビルの立ち並ぶ東京で( )は季節はずれの雨がしとしとと降っていたのを見て昨年のことを思い出した。</p> 
      </div>

      <div class = "content-explain2">
        <h2>今回のテーマ:桃太郎</h2>
        <img src = "../image01.jpg">
        <client-only placeholder="Loading...">
              <Memo />
        </client-only> 
      </div>
      <div class = "footer">

      </div>

    </div>
  </div>
</template>

<script>
export default {}
</script>

<style>
h2{
  font-size:30px;
}

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

.content-explain{
  margin: 40 auto;
  height: 150px;
}

.content-explain2{
  margin: 40 auto;
  height: 200px;
}

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

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

.links {
  padding-top: 15px;
}

.footer{
  height: 200px;
}
</style>

memo.vue
<template>
    <div>
        ★★★つくられた物語★★★
        <!-- {{allData}} -->
        <ul v-for="data in allData" :key="data.id" class="menu-list" >
            <li>
                {{data.name}} / {{data.answer}}
            </li>
        </ul>

         <p>
            <input v-model="name" placeholder="名前">
            <input v-model="answer" placeholder="回答">
            <button v-on:click='post'>送信</button>
        </p>
    </div>
</template>

<script>
    import firebase from "firebase/app";
    import "firebase/firestore";

    export default {
        components: {},

        data(){
            return{
                db: {},
                allData: [],
                name: '',
                answer: 'ここに回答',
            }
        },

        methods: {
            init: () => {
                const config = {
                    apiKey: "AIzaSyBvouEQqs3Cqz_F-re7SCW-FLvPuISQsnc",
                    authDomain: "protoout-2359e.firebaseapp.com",
                    databaseURL: "https://protoout-2359e.firebaseio.com",
                    projectId: "protoout-2359e",
                    storageBucket: "protoout-2359e.appspot.com",
                    messagingSenderId: "1085072592944",
                    appId: "1:1085072592944:web:30da6171b08c3734979df5",
                    measurementId: "G-T3FMEWMY87"
            };

            // Initialize Firebase
            firebase.initializeApp(config);
            },

            post: function(){
                const testId = firebase.firestore().collection('memos').doc().id; //ユニークなIDを生成
                const docRef = firebase.firestore().collection('memos').doc(testId);
                const setAda = docRef.set({
                name: this.name,
                answer: this.answer
            });
            this.get();
            },

            get: function(){
                this.allData = [];
                firebase.firestore().collection('memos').get().then(snapshot => {
                    snapshot.forEach(doc => {
                    // console.log(doc);
                    this.allData.push(doc.data());
                })
            });

            }

        },

        mounted(){
            this.init();
            this.get();
        },
    }
</script>

<style>

</style>

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