20200527のvue.jsに関する記事は13件です。

久しぶりにaxiosを使ったらエラーになったので変更点をメモ

はじめに

久しぶりにVue.js+axiosを使用して、WEBアプリケーションを作成することになりました。
ライブラリの採用理由としては、あまり開発に時間も取れず、既存のものを利用するためです。

画面の実装は特に問題なく進めていたのですが、APIを呼び出す際に以前使っていたプログラムをコピペ参考にしていたところ、エラーが発生したので、メモとして残します。

コピペ参考ソースコード

中身はそれっぽく変えてます

  public async callAxios() {
    const requestParam = {
      method: "get",
      url: "endpoint",
      data: {
        from_time: "2019-01-01T00:00:00+09:00",
        to_time: "2020-01-01T00:00:00+09:00",
      },
      headers: {
        "x-api-key": "apikey",
      },
    };
    const apiResponse = await axios(requestParam);
  }

エラー内容

No overload matches this call.
  Overload 1 of 2, '(config: AxiosRequestConfig): AxiosPromise<any>', gave the following error.
    型 '{ method: string; url: string; data: any; headers: { "x-api-key": any; }; }' の引数を型 'AxiosRequestConfig' のパラメーターに割り当てることはできません。
      プロパティ 'method' の型に互換性がありません。
        型 'string' を型 '"get" | "GET" | "delete" | "DELETE" | "head" | "HEAD" | "options" | "OPTIONS" | "post" | "POST" | "put" | "PUT" | "patch" | "PATCH" | "link" | "LINK" | "unlink" | "UNLINK" | undefined' に割り当てることはできません。
  Overload 2 of 2, '(url: string, config?: AxiosRequestConfig | undefined): AxiosPromise<any>', gave the following error.
    型 '{ method: string; url: string; data: any; headers: { "x-api-k

エラーが発生している箇所は

await axios(requestParam) 

の型定義によるものです
引数はAxiosRequestConfigになりますが、こちらの内容がどうやら変わっているようでした。

以前(いつだったか覚えていない)axiosを使用した際には、methodはstring型でしたが、
今回使用しているバージョン0.19.2ではMethod型である必要がありました。

修正としては、requestParamの型定義を行い解決です。

修正版ソースコード

import { AxiosRequestConfig } from "axios";

  public async callAxios() {
   // 型定義を追加
    const requestParam: AxiosRequestConfig = {
      method: "get",
      url: "endpoint",
      data: {
        from_time: "2019-01-01T00:00:00+09:00",
        to_time: "2020-01-01T00:00:00+09:00",
      },
      headers: {
        "x-api-key": "apikey",
      },
    };
    const apiResponse = await axios(requestParam);
  }

まとめ

今回作成するWEBアプリケーションは時間をかけて作ることができなかったので、
既存の構成を利用したのですが、バージョンによる影響がありました。

すぐに解決できるレベルの問題でしたが、やはりコピペ駆動開発をする際には、理解して行わないと余計に時間がかかる可能性があることを考えさせられました。

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

Vue(Vue CLI)で他ファイルをインポート

<script>
import "./components/Component.vue"

export default {
  el: '#app',
  data() {
    return{
      now: "",
      wakachiko:"uhouho"
    }
  },
  methods:{
    onClick(){
      // alert("onClick!");
      this.now =new Date().toLocaleString();
    }
  }
}
</script>

こんな感じでimport+"ファイル場所"って書けばOKです。

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

tns run iosよ、あなたはなぜiPad Airを起動するのか?NativeScript-VueのiOSエミュレータの初期デバイスをiPhoneに変更する

TL;DR

package.json

"scripts": {
   "ios-iphone": "tns run ios --device 'iPhone 8'" 
  },

を追加する

教えてくれた人

https://www.youtube.com/watch?v=o6CALjTUNSQ

tns run iosよ、あなたはなぜiPad Airを起動するのか

NativeScript−VueのアプリをMac OS Catalina10.15.4で開発中にiOSエミュレータをtns run ios(デフォルト)やnpm run serve:ios(vue-cli-plugin-nativescript-vueでコードシェアリングしている場合)で起動すると、iPad AirのSimulatorが起動する。
image.png
しかし私はiPhoneで確認したい。
image.png
デフォルトのデバイスの設定を変更するにはどうすればよいのか?
利用可能なエミュレータデバイスの一覧を取得し、好みのデバイスの正式名称をみつけ、それをつかってpackage.jsonにnpmのコマンドを追加すればよい。

デバイスの一覧を確認する

$ tns device ios --available-devices

Available emulators
┌───────────────────────┬──────────┬─────────┬────────────────────┬────────────────────┬────────────┐
│ Device Name           │ Platform │ Version │ Device Identifier  │ Image Identifier   │ Error Help │
│ iPhone 8              │ iOS      │ 13.5    │ 77A88E1D-AE2F-4D55 │ 77A88E1D-AE2F-4D55 │            │
│                       │          │         │ -91C6-443A655EC2AF-91C6-443A655EC2AF │            │
│ iPhone 8 Plus         │ iOS      │ 13.5    │ 4B3BE0B3-03D6-4D65 │ 4B3BE0B3-03D6-4D65 │            │
│                       │          │         │ -997F-6EC1EAFB67BE-997F-6EC1EAFB67BE │            │
│ iPhone 11             │ iOS      │ 13.5    │ E42203D6-B46A-4B32 │ E42203D6-B46A-4B32 │            │
│                       │          │         │ -BC4C-E313010EC310-BC4C-E313010EC310 │            │
│ iPhone 11 Pro         │ iOS      │ 13.5    │ D3C18B17-25B1-438E │ D3C18B17-25B1-438E │            │
│                       │          │         │ -ACFD-9BE0AD63A688-ACFD-9BE0AD63A688 │            │
│ iPhone 11 Pro Max     │ iOS      │ 13.5    │ FB6AB7FD-F383-4279 │ FB6AB7FD-F383-4279 │            │
│                       │          │         │ -9D84-11D0C3600A07-9D84-11D0C3600A07 │            │
│ iPhone SE (2nd        │ iOS      │ 13.5    │ D3C08BAB-44FF-4716 │ D3C08BAB-44FF-4716 │            │
│ generation)           │          │         │ -9155-351ACA2C0C6A-9155-351ACA2C0C6A │            │
│ iPad Pro (9.7-inch)   │ iOS      │ 13.5    │ A2CD1BE4-6E7C-453E │ A2CD1BE4-6E7C-453E │            │
│                       │          │         │ -834C-471AC302AB4B-834C-471AC302AB4B │            │
│ iPad (7th generation) │ iOS      │ 13.5    │ AD6AE69D-351F-491B │ AD6AE69D-351F-491B │            │
│                       │          │         │ -8500-275BAFEB5F24-8500-275BAFEB5F24 │            │
│ iPad Pro (11-inch)    │ iOS      │ 13.5    │ 3E65C5CC-A8DE-4519 │ 3E65C5CC-A8DE-4519 │            │
│ (2nd generation)      │          │         │ -AEC7-A7D06437A6A1-AEC7-A7D06437A6A1 │            │
│ iPad Pro (12.9-inch)  │ iOS      │ 13.5    │ DC3C58C4-9A3D-48F9 │ DC3C58C4-9A3D-48F9 │            │
│ (4th generation)      │          │         │ -A0CA-6F978AA4D3BD-A0CA-6F978AA4D3BD │            │
│ iPad Air (3rd         │ iOS      │ 13.5    │ E49C87CF-02ED-4BB1 │ E49C87CF-02ED-4BB1 │            │
│ generation)           │          │         │ -9685-88A8F612033F-9685-88A8F612033F │            │
└───────────────────────┴──────────┴─────────┴────────────────────┴────────────────────┴────────────┘

自分の好みのデバイスをひとつDevice Nameから選ぶ。ここではiPhone 8を使うことにしよう。

package.jsonを編集する

つぎに、自分のNativeScript-Vueプロジェクト配下のpackage.jsonをテキストエディタで開く。scriptsのセクションがなければ追加する。あればそこに次の行を追加する。デバイス名は上述の通り'iPhone 8'になる。"serve:iphone"の部分は実際にコマンドラインで打ち込む際のオプションであり、好きなように変えてよい。

"scripts": {
   "serve:iphone": "tns run ios --device 'iPhone 8'" 
  },

全文は例えばこうなる

package.json
{
  "nativescript": {
    "id": "org.nativescript.radsidenavigator",
    "tns-android": {
      "version": "6.5.0"
    },
    "tns-ios": {
      "version": "6.5.0"
    }
  },
  "scripts": {
   "serve:iphone": "tns run ios --device 'iPhone 8'"  ← added!
  },
  "description": "NativeScript Application",
  "license": "SEE LICENSE IN <your-license-filename>",
  "repository": "<fill-your-repository-here>",
  "dependencies": {
    "@nativescript/theme": "~2.3.3",
    "eslint": "^7.1.0",
    "eslint-loader": "^4.0.2",
    "eslint-plugin-prettier": "^3.1.3",
    "eslint-plugin-vue": "^6.2.2",
    "nativescript-ui-sidedrawer": "~8.0.0",
    "nativescript-vue": "~2.5.0",
    "nativescript-vue-navigator": "^1.2.0",
    "prettier": "^2.0.5",
    "rxjs": "^6.4.0",
    "tns-core-modules": "~6.5.0"
  },
  "devDependencies": {
    "@babel/core": "~7.1.0",
    "@babel/preset-env": "~7.1.0",
    "babel-loader": "~8.0.0",
    "nativescript-dev-webpack": "~1.5.1",
    "nativescript-vue-template-compiler": "~2.5.0",
    "node-sass": "^4.7.1",
    "vue-loader": "~15.9.0"
  },
  "gitHead": "2250137db8c1e0bd0eb543e8e4563cb71480c00d",
  "readme": "NativeScript Application"
}

Vue-CLIのコードシェアリング機能を使ってプロジェクトをはじめた場合は

"serve:iphone": "npm run setup-webpack-config && tns run ios --device 'iPhone 8' && npx vue-devtools",

を追加すればよい。その場合、scriptsのセクションは次のようになる。

package.json
"scripts": {
    "lint": "vue-cli-service lint",
    "build:android": "npm run setup-webpack-config && tns build android --env.production && npm run remove-webpack-config",
    "build:ios": "npm run setup-webpack-config && tns build ios --env.production && npm run remove-webpack-config",
    "build:web": "vue-cli-service build --mode production.web",
    "clean:android": "rimraf platforms/android",
    "clean:ios": "rimraf platforms/ios",
    "clean:platforms": "rimraf platforms",
    "debug:android": "npm run setup-webpack-config && tns debug android --env.development",
    "debug:ios": "npm run setup-webpack-config && tns debug ios --env.development",
    "preview:android": "npm run setup-webpack-config && tns preview --env.development --env.android",
    "preview:ios": "npm run setup-webpack-config && tns preview --env.development --env.ios",
    "remove-webpack-config": "node ./node_modules/vue-cli-plugin-nativescript-vue/lib/scripts/webpack-maintenance post",
    "serve:android": "npm run setup-webpack-config && tns run android --env.development",
    "serve:ios": "npm run setup-webpack-config && tns run ios --env.development",
    "serve:iphone": "npm run setup-webpack-config && tns run ios --device 'iPhone 8' && npx vue-devtools",
    "serve:web": "vue-cli-service serve --mode development.web",
    "setup-webpack-config": "node ./node_modules/vue-cli-plugin-nativescript-vue/lib/scripts/webpack-maintenance pre"
  },

実際にiPhoneエミュレータで起動してみる

以降は、tns run iosではなく先述の"serve:iphone"の部分を使って

$ npm run serve:iphone

などとすればiPhone 8のエミュレータが起動する(iPad Airは起動しない)。
コードシェアリング機能を使って上記のように設定した場合も同様に

npm run serve:iphone

とすればよい。コマンド名は自分に馴染むものであればなんでもよいと思うが、チームでやっている場合は共通でひとつ決めてしまえばよいと思う。

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

Vue.jsでTODOアプリを実装してみる

はじめに

vue-test-utils に触れながら Vue.js について学んでいこうと思います。

https://vue-test-utils.vuejs.org/ja/

vue-test-utils とは

vue-test-utilsは Vue.js 向けの公式単体テストライブラリです。

TODO アプリ

TODO アプリを作りながら、 vue-test-utils について学んでいきます。
Adobe XDでプロトタイプを作成してみました。

スクリーンショット 2020-05-26 23.30.54.png

create-nuxt-app

https://ja.nuxtjs.org/guide/installation/

まず、以下のコマンドを叩きます。

$ npx create-nuxt-app todo

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in todo
? Project name todo
? Project description My impressive Nuxt.js project
? Author name shinoshu
? Choose programming language TypeScript
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose the runtime for TypeScript @nuxt/typescript-runtime
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools ESLint, Prettier, Lint staged files, StyleLint
? Choose test framework Jest
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code), Semantic Pull Requests
yarn run v1.22.4
$ eslint --ext .js,.vue --ignore-path .gitignore . --fix

/Users/shinozaki/workspace/todo/nuxt.config.js
  75:12  error  'config' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars
  75:20  error  'ctx' is defined but never used. Allowed unused args must match /^_/u     @typescript-eslint/no-unused-vars

✖ 2 problems (2 errors, 0 warnings)

error Command failed with exit code 1.

eslint に引っかかったので、 .eslintrc.js を修正。


.eslintrc.js
.eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  extends: [
    '@nuxtjs/eslint-config-typescript',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended'
  ],
  plugins: ['prettier'],
  // add your custom rules here
  rules: {
    '@typescript-eslint/no-unused-vars': 'warn'
  }
}


やーんでぶして、アプリを起動します。
ついでに initial commit も済ませておきます。

$ cd todo
$ git add .
$ git commit -m "initial commit"
$ yarn dev

http://localhost:3000/

このURLにアクセスすると、以下のような画面が表示されると思います。

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

不要な要素を削除、ダークモードを無効にする

ヘッダーの不要な要素と、フッターを削除します。


default.vue
layouts/default.vue
<template>
  <!-- <v-app dark> -->
  <v-app>
    <v-navigation-drawer
      v-model="drawer"
      :mini-variant="miniVariant"
      :clipped="clipped"
      fixed
      app
    >
      <v-list>
        <v-list-item
          v-for="(item, i) in items"
          :key="i"
          :to="item.to"
          router
          exact
        >
          <v-list-item-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title v-text="item.title" />
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
    <v-app-bar :clipped-left="clipped" fixed app>
      <!-- <v-app-bar-nav-icon @click.stop="drawer = !drawer" />
      <v-btn icon @click.stop="miniVariant = !miniVariant">
        <v-icon>mdi-{{ `chevron-${miniVariant ? 'right' : 'left'}` }}</v-icon>
      </v-btn>
      <v-btn icon @click.stop="clipped = !clipped">
        <v-icon>mdi-application</v-icon>
      </v-btn>
      <v-btn icon @click.stop="fixed = !fixed">
        <v-icon>mdi-minus</v-icon>
      </v-btn> -->
      <v-toolbar-title v-text="title" />
      <!-- <v-spacer />
      <v-btn icon @click.stop="rightDrawer = !rightDrawer">
        <v-icon>mdi-menu</v-icon>
      </v-btn> -->
    </v-app-bar>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
    <v-navigation-drawer v-model="rightDrawer" :right="right" temporary fixed>
      <v-list>
        <v-list-item @click.native="right = !right">
          <v-list-item-action>
            <v-icon light>
              mdi-repeat
            </v-icon>
          </v-list-item-action>
          <v-list-item-title>Switch drawer (click me)</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
    <!-- <v-footer :fixed="fixed" app>
      <span>&copy; {{ new Date().getFullYear() }}</span>
    </v-footer> -->
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      items: [
        {
          icon: 'mdi-apps',
          title: 'Welcome',
          to: '/'
        },
        {
          icon: 'mdi-chart-bubble',
          title: 'Inspire',
          to: '/inspire'
        }
      ],
      miniVariant: false,
      right: true,
      rightDrawer: false,
      // title: 'Vuetify.js'
      title: 'TODO'
    }
  }
}
</script>



nuxt.config.js
nuxt.config.js
import colors from 'vuetify/es5/util/colors'

export default {
  mode: 'spa',
  /*
   ** Headers of the page
   */
  head: {
    titleTemplate: '%s - ' + process.env.npm_package_name,
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    '@nuxt/typescript-build',
    // Doc: https://github.com/nuxt-community/stylelint-module
    '@nuxtjs/stylelint-module',
    '@nuxtjs/vuetify'
  ],
  /*
   ** Nuxt.js modules
   */
  modules: [],
  /*
   ** vuetify module configuration
   ** https://github.com/nuxt-community/vuetify-module
   */
  vuetify: {
    customVariables: ['~/assets/variables.scss'],
    theme: {
      // dark: true,
      dark: false,
      themes: {
        dark: {
          primary: colors.blue.darken2,
          accent: colors.grey.darken3,
          secondary: colors.amber.darken3,
          info: colors.teal.lighten1,
          warning: colors.amber.base,
          error: colors.deepOrange.accent4,
          success: colors.green.accent3
        }
      }
    }
  },
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}


こんな感じになると思います。

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

TODOアプリの実装

アイコンのインストール

https://vuetifyjs.com/ja/customization/icons/

ドキュメントに記載されている通り、以下のコマンドを叩きます。

$ yarn add @mdi/font -D # もしかしたら不要かも
$ yarn add material-design-icons-iconfont -D

また、以下のファイルを作成します。


vuetify.js
src/plugins/vuetify.js
// src/plugins/vuetify.js

import 'material-design-icons-iconfont/dist/material-design-icons.css' // Ensure you are using css-loader
import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  icons: {
    iconfont: 'md',
  },
})


また、以下のファイルの plugins に vuetify.js を追加します。

nuxt.config.js
  /*
   ** Plugins to load before mounting the App
   */
  plugins: ['~/plugins/vuetify.js'],

TODOカードの実装

まず、default.vue をちょいと修正します。

layouts/default.vue
    <v-app-bar color="primary" dark :clipped-left="clipped" fixed app>
      <v-toolbar-title color="white" v-text="title" />
    </v-app-bar>

index.vue も修正。


index.vue
pages/index.vue
<template>
  <v-layout column justify-center align-center>
    <v-tabs>
      <v-tab>すべて</v-tab>
      <v-tab>未完了</v-tab>
      <v-tab>完了</v-tab>
    </v-tabs>
    <div class="mt-8">
      <v-card class="todo d-flex px-4 mb-4">
        <v-icon color="green">check_box_outline_blank</v-icon>
        <v-card-title>食器を洗う</v-card-title>
        <span class="mr-auto"></span>
        <v-icon color="red">favorite_border</v-icon>
      </v-card>
      <v-card class="todo d-flex px-4 mb-4">
        <v-icon color="green">check_box</v-icon>
        <v-card-title>洗濯物をする</v-card-title>
        <span class="mr-auto"></span>
        <v-icon color="red">favorite</v-icon>
      </v-card>
    </div>
  </v-layout>
</template>

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

<style scoped>
.todo {
  width: 960px;
}
</style>


こんな感じになります。

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

タスクを追加する入力欄の実装

index.vue を修正する。


default.vue

```html:pages/index.vue



すべて
未完了
完了



check_box_outline_blank
食器を洗う

favorite_border


check_box
洗濯物をする

favorite



</div>
</details>

プロトタイプでは、「タスクを追加する」は画面の一番下にあったと思うのですが、ちょっとめんどくさいので一番上に持ってきました。

<img width="1552" alt="スクリーンショット 2020-05-27 18.35.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244226/6c7d9a3f-52cf-90ed-b0f6-237c9c9fff5c.png">

### default.vue 修正

ブラウザの横幅を大きくしたときに、ナビゲーションが表示されてしまったので、表示されないようにします。

<details>
<summary>default.vue</summary>
<div>

```html:pages/index.vue
<template>
  <v-app>
    <v-app-bar color="primary" dark :clipped-left="clipped" fixed app>
      <v-toolbar-title color="white" v-text="title" />
    </v-app-bar>
    <v-content>
      <v-container>
        <nuxt />
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      items: [
        {
          icon: 'mdi-apps',
          title: 'Welcome',
          to: '/'
        },
        {
          icon: 'mdi-chart-bubble',
          title: 'Inspire',
          to: '/inspire'
        }
      ],
      miniVariant: false,
      right: true,
      rightDrawer: false,
      // title: 'Vuetify.js'
      title: 'TODO'
    }
  }
}
</script>


「タスクを追加する」機能を実装


index.vue
pages/index.vue
<template>
  <v-layout column justify-center align-center>
    <v-tabs>
      <v-tab>すべて</v-tab>
      <v-tab>未完了</v-tab>
      <v-tab>完了</v-tab>
    </v-tabs>
    <div class="mt-8">
      <v-card class="todo d-flex px-4 mb-4">
        <v-icon @click="addTask">add</v-icon>
        <v-text-field
          v-model="task"
          class="px-4"
          label="タスクを追加する"
          autocomplete="off"
          @keydown.enter="onEnter"
        ></v-text-field>
        <span class="mr-auto"></span>
      </v-card>

      <template v-for="(todo, index) in todoList">
        <v-card :key="index" class="todo d-flex px-4 mb-4">
          <v-icon color="green">check_box_outline_blank</v-icon>
          <v-card-title>{{ todo }}</v-card-title>
          <span class="mr-auto"></span>
          <v-icon color="red">favorite_border</v-icon>
        </v-card>
      </template>

      <!-- <v-card class="todo d-flex px-4 mb-4">
        <v-icon color="green">check_box</v-icon>
        <v-card-title>洗濯物をする</v-card-title>
        <span class="mr-auto"></span>
        <v-icon color="red">favorite</v-icon>
      </v-card> -->
    </div>
  </v-layout>
</template>

<script>
export default {
  components: {},
  data() {
    return {
      task: '',
      todoList: []
    }
  },
  methods: {
    onEnter(event) {
      if (event.keyCode === 13) {
        this.addTask()
      }
    },
    addTask() {
      this.todoList = this.todoList.concat(this.task)
      this.task = ''
    }
  }
}
</script>

<style scoped>
.todo {
  width: 960px;
}
</style>


実装しました。

画面収録 2020-05-28 11.50.44.gif

「タスクを完了にする」「タスクを重要なタスクにする」機能を実装する

https://github.com/tokyo-metropolitan-gov/covid19

todo.d.ts
interface Todo {
  name: string
  done: boolean
  important: boolean
  createdAt: Date
  updatedAt: Date
}


index.vue
pages/index.vue
<template>
  <v-layout column justify-center align-center>
    <v-tabs>
      <v-tab>すべて</v-tab>
      <v-tab>重要</v-tab>
      <v-tab>未完了</v-tab>
      <v-tab>完了</v-tab>
    </v-tabs>
    <div class="mt-8">
      <v-card class="todo d-flex px-4 mb-4">
        <v-icon @click="addTask">add</v-icon>
        <v-text-field
          v-model="task"
          class="px-4"
          label="タスクを追加する"
          autocomplete="off"
          @keydown.enter="onEnter"
        ></v-text-field>
        <span class="mr-auto"></span>
      </v-card>

      <template v-for="(todo, index) in todoList">
        <v-card :key="index" class="todo d-flex px-4 mb-4">
          <v-icon
            v-if="!todo.done"
            color="green"
            @click="todo.done = !todo.done"
            >radio_button_unchecked</v-icon
          >
          <v-icon v-if="todo.done" color="green" @click="todo.done = !todo.done"
            >check_circle</v-icon
          >

          <v-card-title>{{ todo.name }}</v-card-title>
          <span class="mr-auto"></span>

          <v-icon
            v-if="!todo.important"
            color="red"
            @click="todo.important = !todo.important"
            >favorite_border</v-icon
          >
          <v-icon
            v-if="todo.important"
            color="red"
            @click="todo.important = !todo.important"
            >favorite</v-icon
          >
        </v-card>
      </template>
    </div>
  </v-layout>
</template>

<script>
export default {
  components: {},
  data() {
    return {
      task: '',
      todoList: []
    }
  },
  methods: {
    onEnter(event) {
      if (event.keyCode === 13) {
        this.addTask()
      }
    },
    addTask() {
      const todo = {
        name: this.task,
        done: false,
        important: false,
        createdAt: new Date(),
        updatedAt: new Date()
      }
      this.todoList = this.todoList.concat(todo)
      this.task = ''
    }
  }
}
</script>

<style scoped>
.todo {
  width: 960px;
}
</style>


キャプチャ忘れてしまったので、かわりに猫貼っておきます。

eltuneko9V9A9721_TP_V.jpg

「タスクのフィルタ」機能を実装する


index.vue
pages/index.vue
<template>
  <v-layout column justify-center align-center>
    <v-tabs @change="setSelectedTab">
      <v-tab>すべて</v-tab>
      <v-tab>重要</v-tab>
      <v-tab>未完了</v-tab>
      <v-tab>完了</v-tab>
    </v-tabs>
    <div class="mt-8">
      <v-card class="todo d-flex px-4 mb-4">
        <v-icon @click="addTask">add</v-icon>
        <v-text-field
          v-model="task"
          class="px-4"
          label="タスクを追加する"
          autocomplete="off"
          @keydown.enter="onEnter"
        ></v-text-field>
        <span class="mr-auto"></span>
      </v-card>

      <template v-for="(todo, index) in filterTodoList">
        <v-card :key="index" class="todo d-flex px-4 mb-4">
          <v-icon v-if="!todo.done" color="green" @click="toggleDone(todo)"
            >radio_button_unchecked</v-icon
          >
          <v-icon v-if="todo.done" color="green" @click="toggleDone(todo)"
            >check_circle</v-icon
          >

          <v-card-title>{{ todo.name }}</v-card-title>
          <span class="mr-auto"></span>

          <v-icon
            v-if="!todo.important"
            color="red"
            @click="toggleImportant(todo)"
            >favorite_border</v-icon
          >
          <v-icon
            v-if="todo.important"
            color="red"
            @click="toggleImportant(todo)"
            >favorite</v-icon
          >
        </v-card>
      </template>
    </div>
  </v-layout>
</template>

<script>
export default {
  components: {},
  data() {
    return {
      task: '',
      todoList: [],
      selectedTab: 0
    }
  },
  computed: {
    filterTodoList() {
      if (this.selectedTab === 1) {
        return this.todoList.filter((todo) => todo.important)
      }
      if (this.selectedTab === 2) {
        return this.todoList.filter((todo) => !todo.done)
      }
      if (this.selectedTab === 3) {
        return this.todoList.filter((todo) => todo.done)
      }
      return this.todoList
    }
  },
  methods: {
    onEnter(event) {
      if (event.keyCode === 13) {
        this.addTask()
      }
    },
    addTask() {
      const todo = {
        name: this.task,
        done: false,
        important: false,
        createdAt: new Date(),
        updatedAt: new Date()
      }
      this.todoList = this.todoList.concat(todo)
      this.task = ''
    },
    toggleDone(todo) {
      todo.done = !todo.done
    },
    toggleImportant(todo) {
      todo.important = !todo.important
    },
    setSelectedTab(event) {
      this.selectedTab = event
    }
  }
}
</script>

<style scoped>
.todo {
  width: 960px;
}
</style>


こんな感じになりました。

画面収録 2020-05-28 14.18.39.gif

Vuexの導入

https://vuex.vuejs.org/ja/

Nuxt.js には Store を実装する方法として

  • クラスベース
  • Vanilla

の2つがあるみたいです。

https://typescript.nuxtjs.org/ja/cookbook/store.html

Vanilla の方で実装していたんですが、 Nuxt TypeScript と Vanilla はどうやら相性が悪いということが分かりました。
相性が悪いというのは、 Nuxt.js の モジュールモード を使って Store を実装すると、 TypeScript の恩恵が受けられないみたいです。

ただ、 Store の ファイルを分けた ときのみに限ります。
ファイルを分けないとテストが書きづらいんですよね…

クラスベースで実装することも考えたんですが、アノテーションを使うことになるみたいで、コンポーネントは クラススタイル で実装していないので、個人的に違和感があるので Vanilla で頑張ることにしました。

なので、 Store のファイルは分けないで進めていきます。

「タスクを追加する」機能を実装(Store)

タスクを追加すると、 Store に登録されるようにしました。


todo.ts
todo.ts
import { GetterTree, ActionTree, MutationTree } from 'vuex'
import { Todo } from '@/types/todo'

export const state = () => ({
  todoList: [] as Todo[]
})

export type TodoState = ReturnType<typeof state>

export const getters: GetterTree<TodoState, TodoState> = {
  getTodoList: (state) => state.todoList
}

export const actions: ActionTree<TodoState, TodoState> = {
  addTodo({ commit }, todo: Todo) {
    commit('ADD_TODO', todo)
  }
}

export const mutations: MutationTree<TodoState> = {
  ADD_TODO: (state, todo: Todo) => {
    state.todoList.push(todo)
  }
}



index.vue
pages/index.vue
<template>
  <v-layout column justify-center align-center>
    <v-tabs @change="setSelectedTab">
      <v-tab>すべて</v-tab>
      <v-tab>重要</v-tab>
      <v-tab>未完了</v-tab>
      <v-tab>完了</v-tab>
    </v-tabs>
    <div class="mt-8">
      <v-card class="todo d-flex px-4 mb-4">
        <v-icon @click="addTask">add</v-icon>
        <v-text-field
          v-model="task"
          class="px-4"
          label="タスクを追加する"
          autocomplete="off"
          @keydown.enter="onEnter"
        ></v-text-field>
        <span class="mr-auto"></span>
      </v-card>

      <template v-for="(todo, index) in getTodoList">
        <v-card :key="index" class="todo d-flex px-4 mb-4">
          <v-icon v-if="!todo.done" color="green" @click="toggleDone(todo)"
            >radio_button_unchecked</v-icon
          >
          <v-icon v-if="todo.done" color="green" @click="toggleDone(todo)"
            >check_circle</v-icon
          >

          <v-card-title>{{ todo.name }}</v-card-title>
          <span class="mr-auto"></span>

          <v-icon
            v-if="!todo.important"
            color="red"
            @click="toggleImportant(todo)"
            >favorite_border</v-icon
          >
          <v-icon
            v-if="todo.important"
            color="red"
            @click="toggleImportant(todo)"
            >favorite</v-icon
          >
        </v-card>
      </template>
    </div>
  </v-layout>
</template>

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

export default {
  components: {},
  data() {
    return {
      task: '',
      todoList: [],
      selectedTab: 0
    }
  },
  computed: {
    ...mapGetters('todo', ['getTodoList']),
    filterTodoList() {
      if (this.selectedTab === 1) {
        return this.todoList.filter((todo) => todo.important)
      }
      if (this.selectedTab === 2) {
        return this.todoList.filter((todo) => !todo.done)
      }
      if (this.selectedTab === 3) {
        return this.todoList.filter((todo) => todo.done)
      }
      return this.todoList
    }
  },
  methods: {
    ...mapActions('todo', ['addTodo']),
    onEnter(event) {
      if (event.keyCode === 13) {
        this.addTask()
      }
    },
    addTask() {
      const todo = {
        name: this.task,
        done: false,
        important: false,
        createdAt: new Date(),
        updatedAt: new Date()
      }

      this.addTodo(todo)

      this.task = ''
    },
    toggleDone(todo) {
      todo.done = !todo.done
    },
    toggleImportant(todo) {
      todo.important = !todo.important
    },
    setSelectedTab(event) {
      this.selectedTab = event
    }
  }
}
</script>

<style scoped>
.todo {
  width: 960px;
}
</style>


Vue.js devtools で確認すると、 Store に登録されていることが分かります。

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

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

「タスクのフィルタ」機能を実装する

実装中&執筆中

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

【Vue初心者向け】BootstrapVueでシンプルなスライダーをコピペのみで作成してみた

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

開発環境

vue@2.6.11

BootstrapVueでシンプルなスライダーを導入

これ超簡単なので文字付きのスライダーをVue開発にて導入したい場合は是非覚えといてください

シンプルかつ洗練されたデザインなのでおすすめです

では早速templateタグ内に下記コードをコピペしてください

home.vue
<template>
  <div>
    <b-carousel
      id="carousel-1"
      v-model="slide"
      :interval="4000"
      controls
      indicators
      background="#ababab"
      img-width="1024"
      img-height="480"
      style="text-shadow: 1px 1px 2px #333;"
      @sliding-start="onSlideStart"
      @sliding-end="onSlideEnd"
    >
      <!-- Text slides with image -->
      <b-carousel-slide
        caption="First slide"
        text="Nulla vitae elit libero, a pharetra augue mollis interdum."
        img-src="https://picsum.photos/1024/480/?image=52"
      ></b-carousel-slide>

      <!-- Slides with custom text -->
      <b-carousel-slide img-src="https://picsum.photos/1024/480/?image=54">
        <h1>Hello world!</h1>
      </b-carousel-slide>

      <!-- Slides with image only -->
      <b-carousel-slide img-src="https://picsum.photos/1024/480/?image=58"></b-carousel-slide>

      <!-- Slides with img slot -->
      <!-- Note the classes .d-block and .img-fluid to prevent browser default image alignment -->
      <b-carousel-slide>
        <template v-slot:img>
          <img
            class="d-block img-fluid w-100"
            width="1024"
            height="480"
            src="https://picsum.photos/1024/480/?image=55"
            alt="image slot"
          >
        </template>
      </b-carousel-slide>

      <!-- Slide with blank fluid image to maintain slide aspect ratio -->
      <b-carousel-slide caption="Blank Image" img-blank img-alt="Blank image">
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse eros felis, tincidunt
          a tincidunt eget, convallis vel est. Ut pellentesque ut lacus vel interdum.
        </p>
      </b-carousel-slide>
    </b-carousel>
  </div>
</template>

はい次はscriptタグ内に下記コードをコピペです

home.vue
<script>
export default {
 data () {
  return {
   slide: 0,
   sliding: null
  }
 },
 methods: {
  onSlideStart (slide) {
   this.sliding = true
  },
  onSlideEnd (slide) {
   this.sliding = false
  }
 }
}
</script>

ブラウザで確認してみてください

どうですか?シンプルでそれなりに洗練されたスライダーができましたね

あとは自分なりにデザインを変更してみてください

以上です

参考記事
【Vue初心者必見】BootstrapVueでシンプルな文字付きスライダーをコピペのみで導入する方法

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

NativeScript-Vueでサイドドロワーをつくる:RadSideDrawerのメニューアイテムのコンポーネント化とNavigator化

NativeScript-Vueとは

Vue.jsで作ったWebページのロジックを流用しながらネイティブアプリをNativeScriptで書く方法。Vue Nativeよりも開発が盛んでWeexよりもオープンなプラットフォームらしい。NativeScriptのXML記法を新たに学んでテンプレートを置き換えていかなければならないがとてもかんたんで良い。NativeScript Coreという親の下に、Angular, React, Vue, Svelteへの応用がある。

image.png

RadSideDrawerとは

NativeScript Coreが提供している公式テンプレートの一つで、左からニュニュッと出るドロワーが作れる。NativeScript-Vueのドキュメントにも少しだけ紹介されている。ややこしいことに本家にもNS+Vueのドキュメントが少しだけあり、そこでもRadSideDrawerが紹介されている.そもそも追加されたのが2ヶ月前(2020-03)とかなので仕方ない。

インストール法:なぜかめちゃめちゃ探しにくいのだがここにVue用のRadSideDrawerのテンプレートが落ちている。

まっさらなものをつくるなら、my-drawer-vueというプロジェクト名であれば

tns create my-drawer-vue --template tns-template-drawer-navigation-vue

とすればよい。

既存のプロジェクトに追加するなら、npmを参考に、

tns plugin add nativescript-ui-sidedrawer

する。テンプレートではapp.js

import Vue from 'nativescript-vue'
import App from './components/App'
import Home from './components/Home'
import DrawerContent from './components/DrawerContent'
import RadSideDrawer from 'nativescript-ui-sidedrawer/vue'

Vue.use(RadSideDrawer)

Vue.config.silent = TNS_ENV === 'production'

new Vue({
  render(h) {
    return h(App, [
      h(DrawerContent, { slot: 'drawerContent' }),
      h(Home, { slot: 'mainContent' }),
    ])
  },
}).$start()

となっているので、これを自らのアプリのmain.js(コードシェアリングを使っていればmain.native.js)などに適用すればよい。

App.vueでは、上記で定義したrendering functionに対応付けてこのように生成される:

<template lang="html">
  <RadSideDrawer ref="drawer" drawerLocation="Left" gesturesEnabled="true" :drawerTransition="transition">
    <StackLayout ~drawerContent backgroundColor="#ffffff">
      <slot name="drawerContent"></slot>
    </StackLayout>
    <Frame ~mainContent ref="drawerMainContent">
      <slot name="mainContent"></slot>
    </Frame>
  </RadSideDrawer>
</template>

<script>
    import { SlideInOnTopTransition } from 'nativescript-ui-sidedrawer';

    export default {
        data () {
          return {
            transition: new SlideInOnTopTransition()
          }
        }
  }
</script>

<style scoped lang="scss">
    // Start custom common variables
    @import '~@nativescript/theme/scss/variables/blue';
    // End custom common variables

    // Custom styles
</style>

このニョロ~mainContent~drawerContentがミソで、v-viewというNativeScript-Vue特有の方法で「この部分はこっちにレンダリングしてね」というのを指定しているようだ。

しかしデフォルトのDrawerContent.vueは極めて煩雑な作りになっている:

<template lang="html">
  <GridLayout rows="auto, *" class="nt-drawer__content">
    <StackLayout row="0" class="nt-drawer__header">
      <Image
        class="nt-drawer__header-image fas t-36"
        src.decode="font://&#xf2bd;"
      ></Image>
      <Label class="nt-drawer__header-brand" text="User Name"></Label>
      <Label
        class="nt-drawer__header-footnote"
        text="username@mail.com"
      ></Label>
    </StackLayout>

    <ScrollView row="1" class="nt-drawer__body">
      <StackLayout>
        <GridLayout
          columns="auto, *"
          :class="
            'nt-drawer__list-item' +
            (selectedPage === 'Home' ? ' -selected' : '')
          "
          @tap="onNavigationItemTap(Home)"
        >
          <Label col="0" text.decode="&#xf015;" class="nt-icon fas"></Label>
          <Label col="1" text="Home" class="p-r-10"></Label>
        </GridLayout>

        <GridLayout
          columns="auto, *"
          :class="
            'nt-drawer__list-item' +
            (selectedPage === 'Browse' ? ' -selected' : '')
          "
          @tap="onNavigationItemTap(Browse)"
        >
          <Label col="0" text.decode="&#xf1ea;" class="nt-icon far"></Label>
          <Label col="1" text="Browse" class="p-r-10"></Label>
        </GridLayout>

        <GridLayout
          columns="auto, *"
          :class="
            'nt-drawer__list-item' +
            (selectedPage === 'Search' ? ' -selected' : '')
          "
          @tap="onNavigationItemTap(Search)"
        >
          <Label col="0" text.decode="&#xf002;" class="nt-icon fas"></Label>
          <Label col="1" text="Search" class="p-r-10"></Label>
        </GridLayout>

        <GridLayout
          columns="auto, *"
          :class="
            'nt-drawer__list-item' +
            (selectedPage === 'Featured' ? ' -selected' : '')
          "
          @tap="onNavigationItemTap(Featured)"
        >
          <Label col="0" text.decode="&#xf005;" class="nt-icon fas"></Label>
          <Label col="1" text="Featured" class="p-r-10"></Label>
        </GridLayout>

        <StackLayout class="hr"></StackLayout>

        <GridLayout
          columns="auto, *"
          :class="
            'nt-drawer__list-item' +
            (selectedPage === 'Settings' ? ' -selected' : '')
          "
          @tap="onNavigationItemTap(Settings)"
        >
          <Label col="0" text.decode="&#xf013;" class="nt-icon fas"></Label>
          <Label col="1" text="Settings" class="p-r-10"></Label>
        </GridLayout>
      </StackLayout>
    </ScrollView>
  </GridLayout>
</template>

<script>
import Home from './Home'
import Browse from './Browse'
import Featured from './Featured'
import Search from './Search'
import Settings from './Settings'
import * as utils from '~/shared/utils'
import SelectedPageService from '~/shared/selected-page-service'

export default {
  mounted() {
    SelectedPageService.getInstance().selectedPage$.subscribe(
      (selectedPage) => (this.selectedPage = selectedPage)
    )
  },
  data() {
    return {
      Home: Home,
      Browse: Browse,
      Featured: Featured,
      Search: Search,
      Settings: Settings,
      selectedPage: '',
    }
  },
  components: {
    Home,
    Browse,
    Featured,
    Search,
    Settings,
  },
  methods: {
    onNavigationItemTap(component) {
      this.$navigateTo(component, {
        clearHistory: true,
      })
      utils.closeDrawer()
    },
  },
}
</script>

<style scoped lang="scss">
// Start custom common variables
@import '~@nativescript/theme/scss/variables/blue';
// End custom common variables

// Custom styles
</style>

使いにくいなと思った点:

  • ひとつひとつのメニューアイテムがコンポーネント化されていないため繰り返しの多いテンプレートになっている
  • リンク先のコンポーネントをいちいち追加する必要があり煩雑

これらの問題を、Navigatorを使い、コンポーネント化することで改善を図る。

デフォルトの状態

May-27-2020 12-36-35.gif

Navigatorの準備をする

さっそくメニューアイテムのコンポーネント化をしたいのだが、Navigatorで必要となる引数(いわばリンク先を知らせたい)をコンポーネントにpropsとして渡したいので、Navigatorをまずは導入する。Github Repoに加え、数日前につくられたNativeScriptingのオッチャンとNS-Vueの作者によるチュートリアル動画が大変参考になる。英語だけど…

まずはインストール

$ npm install --save nativescript-vue-navigator

main.jsでインポートして使う。navigatorのページだと以下のようにrendering functionが書かれているが、

main.js
new Vue({
-   render: h => h('frame', App),
+   render: h => h(App),
}).$start()

我々はRadSideDrawerを使うのでRender functionは一文字も変えない。とにかく

main.js
+ import Navigator from 'nativescript-vue-navigator'
+ import {routes} from './routes'
+ Vue.use(Navigator, { routes })

の部分だけやればよい。

router.jsに似たroutes.jsを追加する。たとえばこんな感じというのがNavigatorのページに書かれているが、今回はRadSideDrawerのテンプレとの整合性で次のようにする。

routes.js
import Home from './components/Home'
import Browse from './components/Browse'

export const routes = {
  '/home': {
    component: Home,
  },
  '/browse': {
    component: Browse,
  },
}

そしてドキュメントを読むとApp.vue<Navigator>を追加してね、

App.vue
<template>
+  <Navigator :defaultRoute="isLoggedIn ? '/home' : '/login'"/>
</template>

というのと、manual routingのthis.$navigateTo(Component)ではなくthis.$navigator.navigate('/component')を使ってね、ということを頭に留めておく。前者はかなりあとになって対応が必要になる。
これで準備は終わり。コンポーネント化をしよう。

メニューアイテムのコンポーネント化

  • 名前を決める
  • propsを決める
  • propsを渡す
  • propsを受け取る
  • navigatorに置き換える という流れで行こう。

子コンポーネントの名前を決める

案外重要だと思っている。Style Guideに従い、DrawerContentの子であることからDrawerContentMenuItemとした。

propsを決める

何を渡すのかを決める。アイコンはちょっと手こずったので今回はナシ…。ルート名とメニューに表示するStringは渡したい。なぜなら表示したい文言とComponent名が完全に一致することはスタイルガイドに従っていればまず起きないから。それとリンク先のページに変数を渡したい場合もある。

propsを渡す

コンポーネントのルート名とメニュー表示名。どちらもStringでそのまま指定する。
テンプレではリンク先のコンポーネント名(HomeBrowse)を渡してリンクをつくっていたが、navigatorではルート名(routes.jsで定義した'/home''/browse')を渡す必要がある。コンポーネント名とかぶってわからなくなるとやっかいなのですぐにわかるようにふざけた文言にした。

DrawerContent.vue
...
<DrawerContentMenuItem
  route="/home"
  name="Home dayo"
/>
...

propsを受け取る

次のように受け取る。どちらのpropsも必須でいいと思う。

DrawerContentMenuItem.vue
...
<script>
export default {
  props: {
    props: { type: Object, required: false }, // navigator props
    route: {
      type: String,
      required: true,
    },
    name: {
      type: String,
      required: true,
    },
  },
}
<script>

props内のprops`についてはあとで紹介するがリンク先に何かを伝えたいときに使う。動画だとこのへんで紹介されている。必須ではないが、複数のメニューアイテムから同一のコンポーネントにリンクする場合などに必要になるため、今のうちに用意しておく。

$navigateToを$navigatorに置き換える

navigatorを使えばリンク先のコンポーネントを全部importする必要がなくなる。Navigatorのドキュメントに従い、次のようにリンクを貼ることができる。

DrawerContentMenuItem.vue
...
<script>
...
methods: {
    onNavigationItemTap(route) {
      this.$navigator.navigate(route)
      utils.closeDrawer()
    },
  },
...
</script>

でも色々プロパティを渡したいよね、ということで拡張したのが以下↓

DrawerContentMenuItem.vue
...
<script>
...
methods: {
    onNavigationItemTap(route) {
      this.$navigator.navigate(route, {
        props: this.props,
        clearHistory: true,
      })
      utils.closeDrawer()
    },
  },
...
</script>

SelectedPageServiceまわりの実装を移植

訪れているページのメニューアイテムを青く塗るための工夫。

コンポーネント名をルート名から割り出…せない!

routes.jsにコンポーネント名:ルート名のコンビが保管されているので照会するのも楽勝だろうと思っていたらそんなことはできないとSOで怒られた。SOでの助言に従い、routes.jscomponent: Homeに加えてmeta: {name: 'Home'}を登録しておく。metaにしたのはNavigatorのページにGood Practiceと書いてあったため。

routes.js
import Home from './components/Home'
import Browse from './components/Browse'

export const routes = {
  '/home': {
    component: Home,
    meta: { name: 'Home' },
  },
  '/browse': {
    component: Browse,
    meta: { name: 'Browse' },
  },
}

この変更により、コンポーネント名がroute名から簡単に割り出せるようになる。

DrawerContentMenuItem.vue
...
<script>
import { routes } from '~/routes'
...
export default {
  computed: {
    component() {
      return routes[this.route].meta.name
    },
  }
}
</script>

リンク先コンポーネント名とselectedPageをもとにクラスをスイッチする

selectedPageはなんのためにあるかというと適用するCSSのクラスをスイッチするだけなのである。簡単に言うと「このメニューアイテムのリンク先のコンポーネント名と現在のselectedPageが一緒だったら青く、そうでなければ白く塗ってね」としている。Vuexでやればよいのでは…?と思わなくもないがよくわからないのでそのままデフォルトのものを使う。テンプレだと少しわかりづらかったので次のようにした。関連する行だけ抜き出してみると、

DrawerContentMenuItem.vue
<template>
 <GridLayout
    columns="48, *"
    :class="
      isSelected ? 'nt-drawer__list-item -selected' : 'nt-drawer__list-item'
    "
    @tap="onNavigationItemTap(route)"
  > 
...

</template>

<script>
import SelectedPageService from '~/shared/selected-page-service'
...
export default {
  data() {
    return {
      selectedPage: '',
    }
  },
  computed: {
    isSelected() {
      return this.selectedPage === this.component
    },
  },
  mounted() {
    SelectedPageService.getInstance().selectedPage$.subscribe(
      (selectedPage) => (this.selectedPage = selectedPage)
    )
  },
 // ...
}
</script>

<Frame><Navigator>に置き換える

これではまだクリックしたときにページが移動しない。肝心の<Navigator>コンポーネントで「どこに表示するか」を明示していないためである。App.vue<template>を次のように書き換える:

App.vue
...
    <Navigator :defaultRoute="'/home'" ~mainContent ref="drawerMainContent">
      <slot name="mainContent"></slot>
    </Navigator>
...

:defaultRoute="'route-name'"は必須。これがオプショナルだと思ってだいぶ時間を溶かした。

最終的にこうなる

これらの変更をすべて加えると以下のようになる。

App.vue
<template lang="html">
  <RadSideDrawer
    ref="drawer"
    drawerLocation="Left"
    gesturesEnabled="true"
    :drawerTransition="transition"
  >
    <StackLayout ~drawerContent backgroundColor="#ffffff">
      <slot name="drawerContent"></slot>
    </StackLayout>
    <Navigator :defaultRoute="'/home'" ~mainContent ref="drawerMainContent">
      <slot name="mainContent"></slot>
    </Navigator>
  </RadSideDrawer>
</template>

<script>
import { SlideInOnTopTransition } from 'nativescript-ui-sidedrawer'

export default {
  data() {
    return {
      transition: new SlideInOnTopTransition(),
    }
  },
}
</script>

<style scoped lang="scss">
@import '~@nativescript/theme/scss/variables/blue';

</style>
DrawerContentMenuItem.vue
<template native>
  <GridLayout
    columns="48, *"
    :class="
      isSelected ? 'nt-drawer__list-item -selected' : 'nt-drawer__list-item'
    "
    @tap="onNavigationItemTap(route)"
  >
    <Label col="1" :text="name" class="p-r-10"></Label>
    <Label col="1" :text="component" class="p-r-10"></Label>
  </GridLayout>
</template>

<script>
import * as utils from '~/shared/utils'
import SelectedPageService from '~/shared/selected-page-service'
import { routes } from '~/routes'

export default {
  data() {
    return {
      selectedPage: '',
    }
  },
  props: {
    props: { type: Object, required: false }, // navigator props
    route: {
      type: String,
      required: true,
    },
    name: {
      type: String,
      required: true,
    },
  },
  computed: {
    component() {
      return routes[this.route].meta.name
    },
    isSelected() {
      return this.selectedPage === this.component
    },
  },
  methods: {
    onNavigationItemTap(route) {
      this.$navigator.navigate(route, {
        props: this.props,
        clearHistory: true,
      })
      utils.closeDrawer()
    },
  },
  mounted() {
    SelectedPageService.getInstance().selectedPage$.subscribe(
      (selectedPage) => (this.selectedPage = selectedPage)
    )
  },
}
</script>

Navigatorのpropsを用意したのは、たとえば次のようにリンク先に変数を渡したいことがあるため。
リンク元のDrawerContent.vueでは次のように{query: 'books'}などとpropsを渡す。

DrawerContent.vue
<template lang="html">
  <GridLayout rows="auto, *" class="nt-drawer__content">
    <StackLayout row="0" class="nt-drawer__header">
      <Image
        class="nt-drawer__header-image fas t-36"
        src.decode="font://&#xf2bd;"
      ></Image>
      <Label class="nt-drawer__header-brand" text="User Name"></Label>
      <Label
        class="nt-drawer__header-footnote"
        text="username@mail.com"
      ></Label>
    </StackLayout>

    <ScrollView row="1" class="nt-drawer__body">
      <StackLayout>
        <DrawerContentMenuItem route="/home" name="Home dayo" />
        <DrawerContentMenuItem
          route="/browse"
          :props="{ query: 'books' }"
          name="Browse Books"
        />
        <DrawerContentMenuItem
          route="/browse"
          :props="{ query: 'movies' }"
          name="Browse Movies"
        />
      </StackLayout>
    </ScrollView>
  </GridLayout>
</template>

<script>
import DrawerContentMenuItem from '~/components/DrawerContentMenuItem'
export default {
  components: {
    DrawerContentMenuItem,
  },
}
</script>

<style scoped lang="scss">
@import '~@nativescript/theme/scss/variables/blue';
</style>

リンク先のBrowse.vueでは、このqueryを受け取ってテキストとしてレンダリングしてみる:

Browse.vue
<template>
  <Page class="page">
    <ActionBar class="action-bar">
      <NavigationButton
        ios:visibility="collapsed"
        icon="res://menu"
        @tap="onDrawerButtonTap"
      ></NavigationButton>
      <ActionItem
        icon="res://menu"
        android:visibility="collapsed"
        @tap="onDrawerButtonTap"
        ios.position="left"
      >
      </ActionItem>
      <Label class="action-bar-title" text="Browse"></Label>
    </ActionBar>
    <GridLayout class="page__content">
      <Label class="page__content-icon far" text.decode="&#xf1ea;"></Label>
      <Label
        class="page__content-placeholder"
        :text="`Query is ${query}`"
      ></Label>
    </GridLayout>
  </Page>
</template>

<script>
import * as utils from '~/shared/utils'
import SelectedPageService from '../shared/selected-page-service'

export default {
  props: {
    query: String,
  },
  mounted() {
    SelectedPageService.getInstance().updateSelectedPage('Browse')
  },
  computed: {
    message() {
      return '<!-- Page content goes here -->'
    },
  },
  methods: {
    onDrawerButtonTap() {
      utils.showDrawer()
    },
  },
}
</script>

<style scoped lang="scss">
@import '~@nativescript/theme/scss/variables/blue';
</style>

動いている様子

May-27-2020 17-05-26.gif

ふたつとも青くなっちゃったりしてるけどまぁやりたいことはできた

Github repo: https://github.com/xerroxcopy/rad-side-navigator

間違っていたり改善できるポイントがあれば教えて下さい

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

【Vue/コピペでOK】 ページ遷移アニメーション(フェードアウト・イン)を実装する方法

スクリーンショット 2020-05-21 23.55.08.png

開発環境

vue@2.6.11

実際のコード

app.vueのstyleタグ内に下記コードをコピペしましょう

app.vue
.v-enter { transform: translate(-100px, 0); opacity: 0; } 
.v-enter-to { opacity: 1; } 
.v-enter-active { transition: all 1s 0s ease; } 
.v-leave { transform: translate(0, 0); opacity: 1; } 
.v-leave-to { transform: translate(100px, 0); opacity: 0; }
.v-leave-active { transition: all .5s 0s ease; }

これで気持ちよく右にフェードアウトして左からフェードインするアニメーションが導入できたかと思います

他にも左にフェードアウトして右からフェードインするようなコードもあるのですが

複雑になるしあまり気持ちの良いアニメーションではないので今回は割愛させてもらいます。

以上です。

参考記事(templateのコードなどより詳しく記述してあります)
【Vue開発】ページ遷移CSSアニメーション(フェードアウト・イン)をコピペで実装する方法

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

Vue.js プロップスの話

プロップスとは

親コンポーネントから子コンポーネントにデータを渡す方法:arrow_right:

大変な蛇足を交えながらメモしていきたいと思います。

親コンポーネントを準備

今回は親(Parent.vue)と子(Child.vue)の2つのコンポーネントを準備。
いつも、scriptタグの中身はいろいろ見ながら書いてしまう。
いい加減覚えたい。:frowning2::frowning2::frowning2:

Parent.vue
<!--親コンポーネント-->
<template>
<Child></Child>
</template>
<script>
    import Child from "../components/Child";
    export default {
        components:{Child}
    }
</script>

↑初期状態。

子にわたすデータを追加する

親は子に「はやく寝なさい」と伝えたいようなので、msg:"はやく寝なさい"を追加しました。

Parent.vue
<template>
<Child></Child>
</template>
<script>
    import Child from "../components/Child";
    export default {
        components:{Child},
// ここから追加
        data(){ 
            return{
                msg:"はやく寝なさい"
            }
        }
    }
</script>

子コンポーネントを準備

scriptタグでprops:以下省略を設定します。

Child.vue
<!--子コンポーネント-->
<template>
    <div>
        {{nanikaitteru}}
    </div>
</template>
<script>
export default {
    components:{},
//ここから追加
    props:{             
        nanikaitteru:{
            type:Text
        }
    }
}
</script>

子のほうは親に言われたことがまだわからないようで、「なにか言ってる」としか思っていないようです。

nanikaitteruと思っているので、一応type:Textに指定して受け取る準備をしました。
数字(int)が聞こえてきたら耳を塞ぐようです。

子にデータを渡す

親のtemplateタグで、子にデータを渡します。

Parent.vue
<template>
<Child :nanikaitteru = msg></Child>
</template>

親は子側で受け取る値をnanikaitteruに設定し、そこにmsgを送ります。
※子側で受け取る値は絶対もっとわかりやすいものでよいです。親側のデータ名と同じmsgでも絶対いいです。

おまけ

親のtemplateタグの中身を
日常会話で表現するならば
「〇〇!『なにか言ってる』って思ってるんでしょうけど、”はやく寝なさい”と言っているの!」
でしょうか。
大体、調べると子が受け取るプロパティ名と親が渡すデータ名は同じなので、よくわからない”呪文”のように思っていたけど、仕組みがわかるとなるほど〜って感じがする。
そもそもカタカナが嫌い。:sob::sob::sob:

次はプロップスの中身を配列にしていく話をします。

参考にしたもの

Vue.js公式 -プロパティ
Vueのpropsの使い方

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

WSlで,Vue.jsによるアンドロイドアプリ開発環境を構築しようとしたときにハマった

備忘録

【Vue.js】WSL 上で Vue.js を用いた Android アプリを開発する
こちらを用いて,よっしゃ開発環境作るどーーと意気込んでいたら…

sudo $ANDROID_HOME/tools/bin/sdkmanager "tools" "emulator" "platform-tools" "platforms;android-28" "build-tools;28.0.3" "extras;android;m2repository" "extras;google;m2repository"

Error: Could not find or load main class com.android.sdklib.tool.sdkmanager.SdkManagerCli

とのエラーが出た.調べていると,JDKが8よりも新しいものを使うとうまくいかないとのこと.この段階で最新版のjava14を入れていた.
入れなおしてもう一度!

としたが変わらなかった.いろいろ探しまくった結果,以下でsdkmanagerのバージョンを古くするといけた.

エラー:メインクラスcom.android.sdklib.tool.sdkmanager.SdkManagerCli #5304が見つからないか、読み込めませんでした

https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip
結局これでダウンロードしたtoolsに置き換えるとうまくいった.

めっちゃ悩んだのにあっけない…
というか,最新のを使う方法はないんでしょうか?

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

Vue.jsでテキストを切り替えたいときに悩んだこと

テキストを変数の内容によって切り替えたい

前回の、Vue.jsでメニュー 切り替えに機能を追加していきます。

メニューを開く/閉じるボタンを作りたい!
ボタンのテキスト表示もメニューを開く/閉じると切り替えたかった。:thinking:
試行錯誤した記録。途中途中に「ほーん」となることがあった。

データとメソッドを準備した

絶対もっといいメソッドの書き方があるだろうと思いながら書いた。

<script>
    export default {
        components: {},
        data: function () {
            return {
                openMenu:false
            }
        },
        methods: {
            open:function(){
                if(this.openMenu ===false){
                    this.openMenu = true
                }else{
                    this.openMenu = false
                }
            }
        }
    }
</script>

問題のbuttonタグ

自力で考えたもの

<template>
<button v-if:openMenu = false>メニューを開く</button>
<button v-if:openMenu = true>メニューを閉じる</button>
</template>

やりたいことはこれだけど、buttonタグ2個もあるのなんか変!他にもあるはず!

「ほーん」ポイント1

<template>
<button v-on:click="openMenu =!openMenu"> //これはスマート。すぐに出てこない
</template>

!を使って切り替えるのはスマート。もはやメソッドは要らなかった!!!
確かに!と納得したはいいけど、テキストの切り替えがわからん:fearful:

「ほーん」ポイント2

<template>
      <button v-on:click="openMenu =!openMenu">
          <template v-if="openMenu === false">メニューを開く</template>
          <template v-else>メニューを閉じる</template>
      </button>
</template>

!!!まさかのテキストのところにまた<template></template>が書けるなんて驚き。
まだ自分で活用できるかは微妙だけど、覚えておきたい。:point_up_tone2:

「ほーん」ポイント3

<template>
    <button v-on:click="openMenu =!openMenu">
        {{ openMenu ? 'メニューを閉じる': 'メニューを開く' }}
    </button>
</template>

シンプルイズベストって感じがしますね。
?三項演算子というらしい。
?の前がtruefalseかによって:で分けられた値を返すんだそうな。
tureなら左側、falseなら右側。。。
すごい!:v_tone2::v_tone2::v_tone2::v_tone2:

感想

いろんな書き方があるんだなぁ、、:rolling_eyes:

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

SPA(Single Page Application) を採用するメリット再考(2020年)

1. 2010~2020年にかけて、WebシステムはSPAで作るのが主流

 近年、WebシステムはJavascript(Typescript)を用いたSPA(Single Page Application)を作ることが主流になっている。
 とはいえ、本当に、一つのHTMLファイルだけで、全画面をJavascriptで描画するSPAがどれだけあるかは分からないが、少なくとも、SPAで"作ろう"とはしている。
 (2010年はSPAの黎明期で、Backbone.jsやknockout.jsが生まれた年)

(1) SPAの構成要素

 "Single Page"に着眼するならば、SPAは、以下を特徴とする。
 ページの基本となるHTMLファイルを1回のリクエストで取得し、その後の画面描画に関しては、大半を初回に取得したブラウザ上のリソースで行う。そして、適宜、必要なデータのみAPIへの問合せで取得する。

 そして、SPAを特徴づけるのは、描画のパーツを作成するComponent、そして、画面遷移を実現するRouterの存在であると思う。そして、状態の管理をするState Manager(VuexやRedux)によりアクションと描画が連結される。

a. Component:視覚要素の部品

 ReactやVueを使って作られる、視覚要素の部品、すなわち、Componentは、SPAにあっては画面の描画を行う基本となる。特に、データバインディングによりHTMLのタグとJavascriptのデータを双方向で対応付け、動的に互いの状態変化を伝達する仕組みをもつのが特徴的だと思う。
 そして、これらの要素を細かく管理し、適宜、必要な部品を描画することでページ全体をサーバから取得する必要がなくなる。
 SPAの心臓部分といっても過言ではないと思う。
 

b. Router:画面遷移の管理

 Routerは、画面というグループでComponentの配置換えを指示するコントローラー。そして、React RouterやVue Routerのように、URLの指定によって、画面を描画することで、既存のWebページと同じ画面遷移方法やUXを提供している。

c. State Manager(状態管理)

最小構成を考えた時、これがないとSPAが本当に成立しないかは別にして、実際にアプリケーションを作る上では、画面上のアクションやイベントをうけて、値や状態の変化をComponentの描画につなげていくときに、VuexやReduxのような状態管理モジュールが利用される。
 Componentは、タグとJavascriptの値の双方向バインディングを特徴とするが、データの流れを規制することで、意図しないデータ変更を防ぎ、バグの防止、バグの早期発見に役立つ。
 大量のComponentや状態を扱ううえでは、必要な構成要素であると思う。

2. jQuery不要論の背景

 SPAの登場と共に、jQuery不要論も散見されるようになった。
 ただ、それはSPAを作ろうとする上での話で、SPAをしないのであれば、jQueryは依然として便利なライブラリである。
 jQueryの特徴としては以下が挙げられる。

(1) jQueryの特徴、メリット

  • Javascriptのシンタックスシュガー、すなわち、既存の構文などを別の構文や記法で記述できるようにしたもの。
  • ツールプリセットであり、事前に様々な処理を関数として定義し手軽に利用できるようにしたもの。
  • 既存のHTML構造を外部から、HTMLを壊すことなく操作したり、イベントを設定できる。

(2) jQueryはSPA専用のライブラリではない

 jQueryはSPAを作るために作られたものではないので、SPAを作ろうとすると不要と言われるのは当然である。
 もちろん、jQueryは先に上げたように、Javascriptを便利に扱うためのAltJS的な存在でもあるので、それをつかってSPAを作れなくもない。

 ただ、それよりはすでにSPAの構成要素がすでに作られている、ReactやVueをつかって、Componentをつくったした方がSPAを作るには便利なのは確かである。

 jQueryは、先に上げた、SPAの構成要素(Component、Router、State Manager)を標準でもっているわけでもなく、自作する必要がある。そういった点で、SPAをするなら、jQueryを使うよりは、Vue、Reactを使うほうが遥かに工数削減ができてメリットがある。

(3) SPAをしないのであればjQueryを利用するメリットはあるのでは?

 つまり、jQuery不要論は、SPAを作るにあたっての話で、そうでないならば、jQueryは依然として有用なライブラリであると思われる。
 問題は、WebシステムをSPAにしているか否か、また、データバインディングしたコンポーネントを使うか否かではないだろうか。例えば、単純なDOM操作やイベントを少し設定するぐらいならjQueryでも十分だと思う。

 もちろん、SPAの中に、jQueryを使うのも問題はないが、ライブラリの数を抑えるためには不要と思われるものは除外しておくほうがよいのかもしれない。ただ、それは開発者の「好み」に依存するようにも思える。

3. SPAを利用するメリット

 SPAは、前述の構成要素を持つが、そのメリットとしては、以下が挙げられる。

  • ページ表示に関する通信回数やデータサイズを削減する。
  • 上記を通じて、サーバとの通信という描画上のオーバーヘッドを小さくし描画速度を上げる。

4. メリットを台無しにするSPAライブラリの利用方法(CSR:Client Side Renderingの速度低下)

 SPAを採用しているが、初回アクセスした際に、真っ白になったり、ローディング画面の表示が長いページがある。
 こういう場合、SPAを利用するメリットを享受していない。
 
 その背景としては、ページ内の構成要素をすべてComponentで、クライアントサイドで描画(CSR:Client Side Rendering)させすぎて、実行時間がかかっているケースもある。

5. Pre-Rendering(デプロイ前描画)、SSR(Server Side Rendring)というSPA以前の技術の採用

 CSRの速度改善策として、Nuxt.jsやNext.jsなどを利用したSSR(Server Side Rendering)などがある。要は、ページの要素であるComponentの描画をサーバサイドでしておこうというもの。
 
 また、サーバへデプロイする前にHTML部分を作成するPre-Renderingもあり、これはデータベースなどアプリケーション稼働時に動的なデータ結合を行わずにできる部分において行われる。

 たとえば、ヘッダやフッタなど構成上共通化した部分を各ページごとに結合することなどが代表的だろう。eleventy、Gatsbyなど静的サイトジェネレーターがこうした部分に利用されることがある。

 ここでSPA登場以前に、RailsやLaravelなどのフルスタックWebフレームワークを利用している人からすると、ビューのテンプレートエンジン使っているのと同じではという指摘があると思う。

 強いて、最近のPre-RenderingとSSRの特徴を挙げると、Javascriptベースで作られたComponentを描画するところにある。フロントの要素であるJavascriptで作成されたものを利用しているのが異なっている。

 サーバサイドはJSONを返すAPIに限定し、ビューやフロント側のリソースをサーバサイドと分けて管理するという観点では、JavascriptベースのPre-RenderingやSSRは良いのかもしれない。 

 尤も、RailsやLaravelがもつビューのテンプレートエンジンでSSR部分をしてしまえば、システム構成が少しシンプルになるのではという指摘もあるので、SSRの実現方法については、ソフトウェア実行上の問題よりは、開発方法という人間側の問題になるのではないだろうか。

 ビューの作成やレンダリングの過程について、以下に改めて整理しておく。

(1) ビュー作成、レンダリングの過程

a. Pre-Rendring:デプロイ前、コーディング時にHTML作成

 これはヘッダやフッタなど共通部分を分けて作成して、静的サイトジェネレーターなどでデプロイする前にHTMLコードやComponentを作成するもの。HTMLに関して言えばgo製のHugo、Javascript製のものだとeleventy(11ty)、Gatsbyなどのツールがある。これは実行時のデータベースの値などに依存しない箇所には適用できる。

b. SSR(Server Side Rendring):DBの値とビューをサーバサイドで結合する。

 アプリケーションが稼働後、ユーザーの操作に関係しない、DBなどサーバサイドで取得できる値とビューを結合する際には、SSRによってビューを作成する。
 ここは、Javascriptのコンポーネントから作成するだけでなく、RailsやLaravelなどWebフレームワークがもつテンプレートエンジンによっても、ビューを作成することができる。
 

c. CSR(Client Side Renderig):ユーザーの操作に応じて描画

 ブラウザ上で描画する。多くの場合、ユーザーの操作に応じて描画をする。ここはJavascriptによる描画が必要となってくる。SPAとして尤も特徴を表す箇所。

 

6. そもそもSPAにする意味、その適用範囲

 Pre-RenderingやSSRを見ていると、Javascriptによる動的なCSRは局所的なものとなり、SPAの構成要素の扱いも変わってっくる。
 フロントのソースコードは同じだとしても、SSRの仕組みを入れたりすると開発や実行環境での構成要素が変わってくる。
 端的に言えば、それだけ手間が発生する。実際には、コーディング上でも完全なCSRとは書き方が変わる。

 SPAが採用されだした2010年辺りは、2007年にiPhoneの初代が発売され、携帯電話でのWebブラウジングが本格的に普及してきた。当時は、端末自体の性能も低く、通信回線も3G回線(数Mbps~14Mbps)で低速であった。このような状況のなか、SPAにより通信回数を削減し、フロントサイドの描画速度の向上は意味があったかもしれない。

 2020年現在、通信回線は4G回線(75Mbps~100Mbps)になり、SPAが登場した当時より回線速度が5倍強になっている。もちろん、端末の性能も上がっている。
 
 こうしたなか、逆に、Javascriptを多用しない極力静的なサイトに近いほうが表示が速いケースもある。(画像のサイズなどが大きくないなどの条件付き)。

 もちろん、SPAだけが速度低下を招くわけではなく、画像のサイズ、広告など別サイトのAPIへの通信なども関係する。

 いずれにしろ、SPAが採用されだした当時とは、環境も変わっているし、行き過ぎたSPAライブラリの利用は本来のSPAのメリットを台無しにしかねないのではないだろうか。

 SPAの構成要素をみて、CSRにおいて、動的にJavascriptで実現しないといけない部分はどこなのかを改めて検討する必要があるのではないだろうか。

7. 局所的なComponentの利用(Router、State Managerの役割低減)

SPA登場以前は、ページ描画は頻繁にサーバとの通信が発生しており、それよりは動的に画面を描画したり、必要な箇所だけサーバと通信するSPAの方がユーザー操作の観点ではよいと思う。

 けれど、行き過ぎたSPA化、フルコンポーネント化はどうなのだろうか?

 必要な箇所だけ、Componentを利用し、Javascriptでの管理範囲を下げることで、State Managerの役割も限定可されてくる。

 複数の画面をまたいSPA化にすればするほど、状態管理も複雑になり、コーディングのストレスが発生するのはVuexやReduxを使った事がある人は感じるのではないだろうか。

 SSRなどをみていると、そもそもサイトやページ全体に亘って、JavascriptによるComponentで作成する必要があるのだろうかという疑問があり、実際、そうではないものが多い。

 であれば、局所的に、Componentを利用し、できるだけRouterは排除し、状態管理の役割も居所化シていくのがよいのではないだろうか。

 SPAにおけるComponentという構成要素は、データバインディングを念頭においた時、動的な画面描画にはなくてはならないものであるかもしれない。
 
 その良さは利用しつつも、行き過ぎたSPA化を抑制し、シンプルな構成にして、結果的、通信も描画もシンプルになり、開発者もユーザーのストレスも減るのではないだろうか。

 

技術選定は、時代ごとの環境、構成要素、工程など細分化して評価する

 Webシステムを作るなら、Javascript(Typescript)使って、SPAをするのが流行っているそれらの技術を使おうという安易な発想よりは、それが流行った理由、どの工程において威力を発揮するかを細かく評価する必要があると思う。

 勿論、分からなければ、とりあえず流行っているものにするというのはありだと思う。けれど、ある程度その道で製造を行えばいろいろなメリットとデメリットが分かる。
 その際には、より細かく物事を整理して評価することが必要ではないだろうか。特に、なんらかの問題がおきているのならば。

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

[AzureのComputer Vision API&Face API ] WEBカメラでパシャリと感情分析やOCRしてみる

やったこと / 書いてあること

これまでやってきたも含めた下記を1つのWEBページにまとめました。

  • 1.FirebaseのAuthentication を使ったログイン機能
  • 2.FirebaseのDatabaseへのログインのロギング機能
  • 3.ローカルストレージを使った履歴やお気に入り画像の保存機能
  • 4.Webカメラを使った写真撮影機能
  • 5.Webカメラで撮影した写真の感情分析やOCRを即時で行う機能

長くなるので1〜3の説明は別記事を参照ください。
ここでは4,5に関してソースコードや簡単な説明をしていきます。

参考

■1,2のFirebaseのログインやそのログの保存に関して
Firebaseでログイン機能を作ってみる

■ 3.ローカルストレージを使った履歴やお気に入り画像の保存機能
vue.jsとlocalStrageで閲覧履歴とお気に入り履歴を作ってみた

■ ホスティング
ここに関しては下記記事を参考にして、vercel(旧 now)を使っています。
ドメインに関しては、昔取って全く使ってないものがあったので、freenomでなくお名前ドットコムです。
爆速!Vercelとfreenomで独自ドメインのサイトを無料で作成する

サンプル

下記ページから確認することができます。
https://simasima.work/index.html

ログインはあらかじめ登録したアカウントでしかできないので、その際は下記をお試しください。(予告もなくログインできなくなることもあるかと思いますが、ご了承ください)

テストユーザアカウント
- メール : test@test.com
- PW : test0000
(まあ、ログインしたらナビゲーションヘッダーが出るだけなんですけどね・・・)

ナビゲーションヘッダーから感情判定やOCRができますので、ぜひお試しください!

image.png

PC中のファイル読み込みと、webカメラでの撮影の2種類が楽しめます

【OCR】

試しに本の表紙を読み取らせてみました。

■1冊目
image.png

■2冊目
image.png

すごい! ちゃんと読み取れてました!!

【感情分析】

こちらもそれっぽくできてました!(口を開けたらだいたいsurpriseになる)
感情.png

では、ここから少し説明していきます。

4.Webカメラを使った写真撮影機能

こちらに関しては下記記事がとても参考になります。
Vue.jsでWebカメラで撮影した画像を取得する

ページ開いてすぐにカメラが立ち上がるのは嫌だ

参考例ではページが立ち上がったらすぐにWEBカメラが起動してしまって少し嫌だったので、私はボタンで起動するように変更しました。

WEBカメラを停止したい

カメラ起動後は、ずっとWEBカメラが起動しっぱなしもちょっと嫌だったので調べたところ、stream.getTracks().forEach(track => track.stop())とすることで停止できるようでした。
参考にしたページ

ただ、私のページの場合、ストリーミングは止まったのけど、カメラ自体は止まらなかったです。。。理解が足りないようです。

5.Webカメラで撮影した写真の感情分析やOCRを即時で行う機能

AzureのAPI

AzureのAPIを使う部分に関しては下記の記事を参考にしてください。
Azure画像認識系の機能をいくつか試してみた(Face API / Custom Vision API / Computer Vision API)

Webカメラで撮った画像をAPIに渡す

URL形式ではなく、撮ったものをバイナリで取得して Array Buffer で変換する処理に私はつまずき下記の記事に助けていただきました!
Vue.js で axios await/async を使って Azure Face API にWebカメラの画像データを application/octet-stream で送るメモ

ソースコード

キーやAzureのエンドポイントなどを入れ替えてください。

感情分析

emotion.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Email/Password Authentication Example</title>

  <!-- Load required Bootstrap and BootstrapVue CSS -->
  <link rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
  <link rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />

  <!-- Load polyfills to support older browsers -->
  <script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver"
    crossorigin="anonymous"></script>

  <!-- Load Vue followed by BootstrapVue -->
  <script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
  <script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>

  <!-- Load the following for BootstrapVueIcons support -->
  <script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>

</head>

<body>

    <div>
        <nav class="navbar navbar-expand-lg navbar navbar-dark bg-dark" > 
            <a class="navbar-brand">Menu</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNavDropdown">
              <ul class="navbar-nav">
                <li class="nav-item active">
                  <a class="nav-link" href="../index.html">Home <span class="sr-only">(current)</span></a>
                </li>
                <li class="nav-item">
                  <a class="nav-link" href="./face-emotion.html">感情分析</a>
                </li>
                <li class="nav-item">
                  <a class="nav-link" href="./ocr-read.html">OCR機能</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="./dog.html">ワンちゃん検索</a>
                  </li>
              </ul>
            </div>
          </nav>
    </div>


  <div class="container">

    <div id="appFaceAPI-File">

      <div class="row">
        <div class="col">
            <h2>①判定したい画像を選んでください</h2>
        </div>
      </div>

      <div class="row">
        <div class="col">
          <div class="form-group">
            <!-- https://bootstrap-vue.org/docs/components/form-file -->
            <div>
              <!-- Styled -->
              <b-form-file
                v-model="file"
                :state="Boolean(file)"
                placeholder="Choose a file or drop it here..."
                drop-placeholder="Drop file here..."
                @change="handlerFileChange"
              ></b-form-file>              
              <div class="mt-3">ファイルネーム: {{ file ? file.name : '' }}</div>
            </div>

            <div>
                <br>
                <h2><p>②カメラで撮影します。</p></h2>
                <p>※ カメラで撮影する前にカメラをONにしてください。</p> 
                <p>※ カメラOFFで消えない時はリロードするとカメラを停止できます。</p> 
            </div>

            <div>
                <table>
                    <tr>
                        <td><button id="on" v-on:click="btnON()">カメラON</button></td>
                        <td><button id="off" v-on:click="btnOFF()">カメラOFF</button></td>
                        <td><b-button v-on:click="hanlderCapture">写真を撮る!</button></td>
                    </tr>
                </table>

            </div>

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

      <div>
        <table>
            <tr>
                <td>
                    <!-- キャプチャした画像 -->
                          <h3>カメラの映像</h3>
                          <video ref="video" id="video" width="320" height="240" autoplay></video>
                </td>

                <td>
                          <h3>写真のキャプチャ</h3>
                          <canvas ref="canvas" id="canvas" width="320" height="240"></canvas>
                </td>

                <td>
                    <!-- 選択したサムネイル -->

                        <h3>選択した画像のサムネ</h3>
                        <img id="output"  width="320" height="240" >
                </td>


            </tr>
        </table>
    </div>

      <div class="row">
        <div class="col">
          <h2 v-if="feel.length">この人の感情は・・・ {{feel}} が {{score}}% です</h2>
          <div>
            <p v-if="response">Response JSON データ:</p>
            <pre><code>{{ response }}</code></pre>
          </div>
        </div>
      </div>

    </div>

  </div>

  <!-- axios -->
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

  <script>
    const app = new Vue({
      el: '#appFaceAPI-File',
      data: {
        response: '',
        file: null,
        feel:'',
        score:'',

        /// ここから下はカメラ用
        video: {},
        canvas: {},
        captures: []

      },
      methods: {


        handlerFileChange: async function (e) {
          //console.log('handlerFileChange');

          // 1. ファイルアップロードの入力から、まずファイル情報を取り出す
          const files = e.target.files || e.dataTransfer.files;
          const file = files[0];

          /// サムネイルを取得するための記述
          // https://web.dev/read-files/ 参照
          const output = document.getElementById('output');
          const reader = new FileReader();
               reader.addEventListener('load', event => {
                 output.src = event.target.result;
               });
               reader.readAsDataURL(file);
               console.log(output);

          // 2. ファイル参照の中から ArrayBuffer としてデータを取り出す
          let contentBuffer = await this.readFileAsync(file);
          //console.log('bbbbbb' + file);

          this.sendCognitiveAsFile(contentBuffer);
        },

        // ファイル情報の中から ArrayBuffer としてデータを取り出す
        // 本来 onnload で取り出すが await / async で呼び出せるようにしている
        readFileAsync: function (file) {
          return new Promise((resolve, reject) => {
            let reader = new FileReader();
            //console.log('aaaaaa' + reader.result);
            reader.onload = () => {
              resolve(reader.result);
            };
            reader.onerror = reject;
            reader.readAsArrayBuffer(file);
          })
        },

///////////////////////// 写真
       // キャプチャボタンの挙動
       hanlderCapture: async function () {
          this.canvas = this.$refs.canvas
          this.canvas.getContext('2d').drawImage(this.video, 0, 0, 320, 240);

          // まず、CanvasからBlogデータを取得
          const blob = await this.getBlogData(this.canvas);
          // BlogデータをArrayBufferに変換
          const contentBuffer = await this.readBlobToArrayBuffTo(blob);
          // console.log(contentBuffer);

          this.sendCognitiveAsFile(contentBuffer);

        },

        getBlogData: function (canvas) {
          return new Promise((resolve, reject) => {
            try {
              // 標準はPNG 読み込みなので、ファイルサイズが重くなりがち
              /*
              canvas.toBlob(function(blob){
                resolve(blob);
              });
              */
              // JPEG画質も指定できる
              canvas.toBlob(function(blob){
                resolve(blob);
              },"image/jpeg", 0.8);
            } catch( e ){
              reject(e);
            }
          })
        },

        // Blob の中から ArrayBuffer としてデータを取り出す
        // Blob : Binary Large Object バイナリデータを格納する場合のデータ型
        // 
        // 本来 onnload で取り出すが await / async で呼び出せるようにしている
        readBlobToArrayBuffTo: function (file) {
          return new Promise((resolve, reject) => {
            let reader = new FileReader();
            reader.onload = () => {
              resolve(reader.result);
            };
            reader.onerror = reject;
            reader.readAsArrayBuffer(file);
          })
        },

/////////////////////////////////



        // このあたりは以下の記事を参考に。
        // https://www.1ft-seabass.jp/memo/2020/05/07/azure-face-api-application-octet-stream-using-axios/

        sendCognitiveAsFile: async function(contentBuffer) {
          // エンドポイント
          const FACE_API_ENDPOINT_URL = ' xxxxxxxxxxxxxxxx + /face/v1.0/detect?returnFaceId=true&returnFaceLandmarks=false&returnFaceAttributes=emotion&recognitionModel=recognition_01&returnRecognitionModel=false&detectionModel=detection_01';

          // サブスクリプションをOcp-Apim-Subscription-Keyヘッダーに
          // JSONで送るのでContent-typeヘッダーにapplication/octet-stream指定        
          const config = {
            url: FACE_API_ENDPOINT_URL,
            method: 'post',
            headers: {
              'Content-type': 'application/octet-stream',
              'Ocp-Apim-Subscription-Key':'xxxxxxxxxxxxxxxx'
            },
            data: contentBuffer
          };

          // axios
          try {
              // POSTリクエストで送る
              const responseAzure = await axios.request(config);
              console.log('post OK');
              // データ送信が成功するとレスポンスが来る
              console.log(responseAzure.data);

              var emo = responseAzure.data[0].faceAttributes.emotion;
              var type;
              var val = 0;
              this.response = emo;
              //this.response = emo;

              for(key in emo){
              if(emo[key] > val){
                val = emo[key];
                type = key ;
               }
              this.feel = type;
              this.score = Math.round(val *100);

    }


          } catch (error) {
              console.log('post Error');
              // ダメなときはエラー
              console.error(error);
          }

        },


        // ストリームの停止
        btnOFF(){
            this.video = this.$refs.video;
            if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
                navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
                    this.video.srcObject = stream ;
                    stream.getTracks().forEach(track => track.stop()); // ストリームの停止
                })
                }
            },

        // ストリームの開始
        btnON(){
            this.video = this.$refs.video;
            if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
                navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
                    this.video.srcObject = stream ;
                    this.video.play() ;
                })
                }
            }

      }
      ,
      mounted() {
        console.log('mounted');
      }
    })
  </script>
</body>

</html>

OCR

こちらに関しては上と共通の部分も多いので、const app = new Vue({ 以下の部分を貼ります。

ocr.html
  <script>

    const app = new Vue({
      el: '#appFaceAPI-File',
      data: {
        response: '',
        file: null,
        feel:'',
        score:'',
        letter:'',
        oarray:'',

        /// ここから下はカメラ用
        video: {},
        canvas: {},
        captures: []


      },
      methods: {


        handlerFileChange: async function (e) {
          //console.log('handlerFileChange');

          // 1. ファイルアップロードの入力から、まずファイル情報を取り出す
          const files = e.target.files || e.dataTransfer.files;
          const file = files[0];

          /// サムネイルを取得するための記述
          // https://web.dev/read-files/ 参照
          const output = document.getElementById('output');
          const reader = new FileReader();
               reader.addEventListener('load', event => {
                 output.src = event.target.result;
               });
               reader.readAsDataURL(file);

          ////

          // 2. ファイル参照の中から ArrayBuffer としてデータを取り出す
          let contentBuffer = await this.readFileAsync(file);
          console.log('bbbbbb' + file);

          this.sendCognitiveAsFile(contentBuffer);
        },

        // ファイル情報の中から ArrayBuffer としてデータを取り出す
        // 本来 onnload で取り出すが await / async で呼び出せるようにしている
        readFileAsync: function (file) {
          return new Promise((resolve, reject) => {
            let reader = new FileReader();
            //console.log('aaaaaa' + reader.result);
            reader.onload = () => {
              resolve(reader.result);
            };
            reader.onerror = reject;
            reader.readAsArrayBuffer(file);


          })
        },

///////////////////////// 写真
       // キャプチャボタンの挙動
       hanlderCapture: async function () {
          this.canvas = this.$refs.canvas
          this.canvas.getContext('2d').drawImage(this.video, 0, 0, 320, 240);

          // まず、CanvasからBlogデータを取得
          const blob = await this.getBlogData(this.canvas);
          // BlogデータをArrayBufferに変換
          const contentBuffer = await this.readBlobToArrayBuffTo(blob);
          // console.log(contentBuffer);

          this.sendCognitiveAsFile(contentBuffer);

        },

        getBlogData: function (canvas) {
          return new Promise((resolve, reject) => {
            try {
              // 標準はPNG 読み込みなので、ファイルサイズが重くなりがち
              /*
              canvas.toBlob(function(blob){
                resolve(blob);
              });
              */
              // JPEG画質も指定できる
              canvas.toBlob(function(blob){
                resolve(blob);
              },"image/jpeg", 0.8);
            } catch( e ){
              reject(e);
            }
          })
        },

        // Blob の中から ArrayBuffer としてデータを取り出す
        // Blob : Binary Large Object バイナリデータを格納する場合のデータ型
        // 
        // 本来 onnload で取り出すが await / async で呼び出せるようにしている
        readBlobToArrayBuffTo: function (file) {
          return new Promise((resolve, reject) => {
            let reader = new FileReader();
            reader.onload = () => {
              resolve(reader.result);
            };
            reader.onerror = reject;
            reader.readAsArrayBuffer(file);
          })
        },

/////////////////////////////////

        // このあたりは以下の記事を参考に。
        // https://www.1ft-seabass.jp/memo/2020/05/07/azure-face-api-application-octet-stream-using-axios/

        sendCognitiveAsFile: async function(contentBuffer) {

          const FACE_API_ENDPOINT_URL = 'xxxxxxxxxxxxxxxx + /vision/v2.0/ocr?language=ja&detect0';

          // サブスクリプションをOcp-Apim-Subscription-Keyヘッダーに
          // JSONで送るのでContent-typeヘッダーにapplication/octet-stream指定

          const config = {
            url: FACE_API_ENDPOINT_URL,
            method: 'post',
            headers: {
              'Content-type': 'application/octet-stream',
              'Ocp-Apim-Subscription-Key':'xxxxxxxxxxxxxxxx'
            },
            data: contentBuffer
          };

          var alltext = '';
          var ocrarray = [];

          // axios
          try {
              // POSTリクエストで送る
              const responseAzure = await axios.request(config);
              console.log('post OK');
              // データ送信が成功するとレスポンスが来る
              console.log(responseAzure.data);

              for(let i = 0; i <= responseAzure.data.regions[0].lines.length -1; i++){
                  ocrarray.push(responseAzure.data.regions[0].lines[i].words);
                  for(let j = 0; j <= responseAzure.data.regions[0].lines[i].words.length - 1 ; j++){
                      alltext = alltext + responseAzure.data.regions[0].lines[i].words[j].text ;

                      //console.log(responseAzure.data.regions[0].lines[i].words.length);

                  }
              }

              this.letter = alltext ;
              this.oarray = ocrarray ;

          } catch (error) {
              console.log('post Error');
              // ダメなときはエラー
              console.error(error);
          }

        },

        // ストリームの停止
        btnOFF(){
            this.video = this.$refs.video;
            if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
                navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
                    this.video.srcObject = stream ;
                    stream.getTracks().forEach(track => track.stop()); // ストリームの停止
                })
                }
            },

        // ストリームの開始
        btnON(){
            this.video = this.$refs.video;
            if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
                navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
                    this.video.srcObject = stream ;
                    this.video.play() ;
                })
                }
            }



      }
      ,
      mounted() {
        console.log('mounted');
      }
    })
  </script>


おわりに

最後まで見ていただきありがとうございます。
画面のデザインやタグもグチャついていますがそのあたりはご容赦願います・・・。

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

Firebaseでログイン機能を作ってみる

やったこと

FireBaseを使ってログイン機能を作りました。
(ただし、あらかじめ登録してあるものだけログイン可)

最後にコードも貼り付けます。

使っているもの

できること(サンプル)

  • FirebaseのAuthenticationであらかじめ登録したユーザでログインができます
  • ログインするとFirebaseのDatabaseにタイムスタンプが送られます
  • ログイン時のみナビゲーションヘッダーを出すようにしています

サンプル:
https://simasima.work/index.html

テストユーザアカウント
- メール : test@test.com
- PW : test0000

こちらでテスト的にログインできるはずです。
※ 何のお知らせなくログインできなくなることもあるかも知れませんがご了承ください。

ログイン前

image.png

ログイン後

image.png

まずFireBaseのプロジェクトを作成する

下記記事を参考にして、「1個のアプリ」と表示されたことを確認 ⇒ 左メニューから各機能の設定を行うまでやります。

下記画像にあるスクリプトはすぐ使うので控えておきます。
image.png

参考:Firebaseプロジェクト作成方法

Authenticationでログインアカウントを作る

ログインを許可するアカウントを作ります。

まず、Authentication を選択して、Sign-in methodを選択する。
そして、メール/パスワード を有効にする。
image.png

次に、Usersを選択する(Sign-in methodの隣にある)。
そして、「ユーザを追加」からログインさせたいメールアドレスとパスワードを登録する。
image.png

Databaseの設定をする

左ナビのDatabaseを選択して「データベースの作成」をする。
本番かテストモードかを選択する。
image.png

ロケーションも設定したら完了をクリックする。

完了したら、「コレクションを開始」をクリックする。
ソースコード上はmemosというコレクションIDでやっているので、コピペで使いたい場合はコレクションIDをメモにする。

ここには下図のようにフィールドを設定しておきます。
image.png

準備しておくべきことは以上で完了です!

ソースコード

画像のリンクや、Firebaseの情報を入れ変えることで動くかと思います。
ナビゲーションヘッダーのリンク先も適宜変更してください。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Email/Password Authentication Example</title>

  <!-- Load required Bootstrap and BootstrapVue CSS -->
  <link rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
  <link rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />

  <!-- Load polyfills to support older browsers -->
  <script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>

  <!-- Load Vue followed by BootstrapVue -->
  <script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
  <script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>

  <!-- Load the following for BootstrapVueIcons support -->
  <script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>

</head>

<body>

  <div class="container">

    <div id="appFirebaseLogin">

    <div v-if="loginStatus">
        <nav class="navbar navbar-expand-lg navbar navbar-dark bg-dark" > 
            <a class="navbar-brand">Menu</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNavDropdown">
              <ul class="navbar-nav">
                <li class="nav-item active">
                  <a class="nav-link" href="./index.html">Home <span class="sr-only">(current)</span></a>
                </li>
                <li class="nav-item">
                  <a class="nav-link" href="./contents/face-emotion.html">感情分析</a>
                </li>
                <li class="nav-item">
                  <a class="nav-link" href="./contents/ocr-read.html">OCR機能</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="./contents/dog.html">ワンちゃん検索</a>
                  </li>
              </ul>
            </div>
          </nav>
        </div>

      <div class="row" v-if="loginStatus">
        <div class="col">
          <img class="rounded mx-auto d-block" src="xxxxxxxxx" width="400" height="auto" vspace="30">
        </div>
      </div>

      <div class="row" v-else>
        <div class="col">
          <img class="rounded mx-auto d-block" src="xxxxxxxxx" width="400" height="auto" vspace="30">
        </div>
      </div>

      <div class="row">
        <div class="col">

          <div v-if="loginStatus">
            <div class="row">
              <div class="col-6">
                <div class="form-group">
                  ようこそ、{{ userName }} さん! 
                </div>
              </div>
              <div class="col-6">
                <div class="form-group">
                  <button class="btn btn-primary" v-on:click="handlerLogout">Logout</button>
                </div>
              </div>
            </div>
          </div>

          <div v-else>
              <div class="form-group">
                <label>Email</label>
                <input v-model="email" class="form-control"/>
              </div>
              <div class="form-group">
                <label>Password</label>
                <input v-model="password" type="password" class="form-control">
              </div>
              <div class="form-group">
                <button class="btn btn-primary" v-on:click="handlerLogin">Login</button>
              </div>
          </div>

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

  </div>

  <!-- FireBaseの使う機能を指定する -->
    <!-- https://firebase.google.com/docs/web/setup?hl=ja#available-libraries -->
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-firestore.js"></script>
    <script>
        // ここは自分のアカウントう情報
        // Your web app's Firebase configuration
        var firebaseConfig = {
            apiKey: "xxxxxxxxxxxxxxx",
            authDomain: "xxxxxxxxxxxxxxx",
            databaseURL: "xxxxxxxxxxxxxxx",
            projectId: "xxxxxxxxxxxxxxx",
            storageBucket: "xxxxxxxxxxxxxxx",
            messagingSenderId: "xxxxxxxxxxxxxxx",
            appId: "xxxxxxxxxxxxxxx",
            measurementId: "xxxxxxxxxxxxxxx"
        };
        // Initialize Firebase
        firebase.initializeApp(firebaseConfig);
        // firestoreのインスタンス
        const db = firebase.firestore();
    </script>


<script>
  const app = new Vue({
    el: '#appFirebaseLogin',
    data: {
      email: '',
      password: '',
      loginStatus: false,
      userName:'',
      userID:'',
      message:'',
      items:[]
    },
    methods: {

            /////////////////// Firebase Authentication ///////////////////

            // ログアウトボタンの挙動
            handlerLogout: async function () {
            console.log('handlerLogout');
            await firebase.auth().signOut();
            await this.checkAuthStateChanged();
            // データ更新を監視するリスナーを削除する Cloud Firestore 用の処理
            this.endSnapShot();
            },

            // ログインボタンの挙動
            handlerLogin: async function () {
            console.log('handlerLogin');

            // DBにログイン日時を書き込む処理
            this.handlerAddMemo();

            try {
                const resSignInWithEmailAndPassword = await firebase.auth().signInWithEmailAndPassword(this.email, this.password);
                console.log(resSignInWithEmailAndPassword);
            } catch(error) {
                console.log(error);
                const errorCode = error.code;
                const errorMessage = error.message;
                if (errorCode === 'auth/wrong-password') {
                alert('Wrong password.');
                } else {
                alert(errorMessage);
                }
            }


            // ログイン処理後 Firebase Authentication 認証状態のチェック
            this.checkAuthStateChanged();


            },

            checkAuthStateChanged: async function(){

            // 認証状態の変更を取得
            let user = await this.promiseAuthStateChanged();



            if (user) {
                // ログイン済み
                console.log('ログイン済み');
                // Vue に値を反映
                this.loginStatus = true;    // ログイン状況
                this.userName = user.email; // ユーザー名(メールアドレス)
                this.userID = user.uid;     // Firebase で管理されているユニークなユーザーID
                // データ更新を監視するリスナーに登録する Cloud Firestore 用の処理
                this.startSnapShot();

            } else {
                // 未ログイン
                console.log('未ログイン');
                this.loginStatus = false;  // ログイン状況
            }

            },

            promiseAuthStateChanged: async _ => {
            return new Promise( (resolve, reject) => {
                const f_auth = firebase.auth();
                f_auth.onAuthStateChanged( user => {
                resolve(user);
                });
            });

            },

      /////////////////// Cloud Firestore ///////////////////

        // Cloud Firestore用。これを呼ぶとDBにログイン日時が書き込まれる
        handlerAddMemo: async function() {
            console.log('handlerAddMemo');

            // ここの中にuser情報をとってくる。
            const data = {
                message: this.message,
                timestamp: firebase.firestore.FieldValue.serverTimestamp()
            };

            try {
            await db.collection("memos").doc().set(data);
            console.log("Document successfully written!");
            } catch(error) {
            console.log(error);
            console.error("Error writing document: ", error);
            }
        },

        // データ更新を監視するためリスナーに登録する
        startSnapShot: function(){
            console.log('常に監視するためリスナーに登録する');
            this.unsubscribe = db.collection('memos').orderBy('timestamp', 'desc').onSnapshot(this.listenerSnapShot);
        },

        // データ更新を監視するリスナーを削除する
        endSnapShot: function(){
            console.log('常に監視するリスナーを削除する');
            this.unsubscribe();
        },

        //データ更新が行われた場合に動作する
        listenerSnapShot: function(querySnapshot){
            //console.log('listenerSnapShot');
            this.items = [];
            const _this = this;

            querySnapshot.docs.map(function(doc){
            const _data = doc.data({serverTimestamps: 'estimate'});
            let timestampDate = new Date();
            timestampDate.setTime(_data.timestamp.seconds * 1000);

            console.log(_this.userName)
            _this.items.push({
                message:_data.message,
                dateString:timestampDate.toString(),
                timestamp:_data.timestamp

            });
            });
        }

            }
            ,
            mounted() {
            console.log('mounted');

            /////////////////// Firebase Authentication ///////////////////
            // はじめて表示されたときの Firebase Authentication 認証状態のチェック
            this.checkAuthStateChanged();
}
  })
</script>


</body>
</html>

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