20200729のvue.jsに関する記事は15件です。

GASでWebアプリ「映画鑑賞記録」を作る⑤

今回やること

 今まで、本アプリの機能をBootstrapとjQueryで実現していましたが、Vue.jsに変更します。

 クライアント側(画面表示)の変更になるので、サーバ側(GAS)の修正はありません。
 変更は次の項目に沿って行いますが、今回は「サーバから…」までに対応します。

  • Vue.jsの導入
  • サーバから取得したデータの処理を変更
  • ダイアログボックスの変更

 今回追加するファイルは、Vuejs.html のみです。

Vue.jsの導入

Index.html

 Vue.jsを使える様にするのは、次の参照を追加するだけです。
 ◆参考サイト 「Vue.js 公式サイト」/インストール #CDN

  <!-- Vue.js -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> 

 不要になったBootstrapとjQueryの記述は削除します。

  <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/themes/redmond/jquery-ui.css" />
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.min.js"></script>

  <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>

 次のスタイルシートだけ残しておきます。

  <!-- Bootstrap -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

サーバから取得したデータの処理を変更

javascript.html

 searchResults()(サーバからデータを取得する処理)の実行が成功した後に実行されるdrawTable()を、下記の様に修正します。
 以前の処理では、HTMLを意識していたので複雑でしたが、Vue.jsを使用する事により凄くシンプルになりました。
 勿論、HTMLファイル側では意識する必要がありますが、Javascriptではデータや処理を意識するだけで良いので分かりやすくなったと思います。
 app.recordsは、後述の Vuejs.html に定義されています。

  function drawTable(data) {
    let records = JSON.parse(data)
    app.records = records;
  }
以前のコード
  function drawTable(data) {
    var records = JSON.parse(data)
    var tag = '';
    for(var i = 0; i < records.length; i++) {
      tag += '<tr data-toggle="modal" data-target="#exampleModal" data-processing-type="記録更新" data-data-id="' + records[i][0] + '" ';
      tag += 'data-viewing-date="' + records[i][1] + '" data-movie-name="' + records[i][2] + '" data-first-look="' + records[i][3] + '" ';
      tag += 'data-viewing-type="' + records[i][4] + '" data-theater-name="' + records[i][5] + '">';
      tag += '<td class="col01">'+ records[i][0] + '</td>';      
      tag += '<td class="col02">'+ convDate(records[i][1]) + '</td>';
      tag += '<td class="col03">'+ records[i][2] + '</td>';
      tag += '<td class="col04">'+ records[i][3] + '</td>';
      tag += '<td class="col05">'+ records[i][4] + '</td>';
      tag += '<td class="col06">'+ records[i][5] + '</td>';
      tag += '</tr>';
    }
    $('#resultList tbody').html(tag);
  }

Vuejs.html

 Vue.jsの肝となるコードです。
 Vue.jsの詳しい説明については、他のサイトや書籍を参照してください。 :sweat_smile:

  1. 「鑑賞日」の表示変換に使用していたconvDate(date)を、javascript.html からfilters:に移動します。
  2. ページ表示時のsearchResults()の実行を、javascript.html からmounted:に移動します。

 ◆参考書籍のサイト 「基礎から学ぶ Vue.js」mio著 C&R研究所

<script>  
var app = new Vue({
  el: '#app',
  data: {
    message: 'テスト',
    records: []
  },
  mounted: function() {
    searchResults();
  },
  filters: {
    convDate: function(date) {
      let day = moment(date);
      let res = (day.month() + 1) + '/' + day.date();
      return res;
    }
  }
})
</script>  
以前のコード
  $(document).ready(function() {
    searchResults();
  });

Index.html

 次の修正を行います。

  1. <body>内全体を<div id="app">で括ります。
  2. <tbody>を修正します。
    <tr v-for="(record, index) in records"…recordsは、Vuejs.htmlrecordsを参照しています。
    records(2次元配列のデータ)を繰り返し処理する事によりテーブルを作成します。
    recordに1行分のデータがセットされます。
    indexに配列の添字がセットされます。使用しない場合は記述する必要はありません。
    v-bind:key="…"には、一意となる値をセットします。
    <td class="col02">{{ record[1] | convDate }}</td>convDateは、Vue.jsのフィルターで Vuejs.htmlconvDateが実体です。
  3. 今回作成した Vuejs.html の取込みを一番下に追加します。
  <body>
    <div id="app">
    <p></p>
    <div class="container-fluid">
    <table id="resultList" class="table-striped table-bordered table-headerfixed"><tbody class="scrollBody">
        <tr v-for="(record, index) in records" v-bind:key="record[0]">
          <td class="col01">{{ record[0] }}</td>
          <td class="col02">{{ record[1] | convDate }}</td>
          <td class="col03">{{ record[2] }}</td>
          <td class="col04">{{ record[3] }}</td>
          <td class="col05">{{ record[4] }}</td>
          <td class="col06">{{ record[5] }}</td>
        </tr>
      </tbody>
    </table>
    </div>
      :
      :
  <?!= HtmlService.createHtmlOutputFromFile('Vuejs').getContent(); ?>

  </body>
以前の記述
  <body>
    <p></p>
    <div class="container-fluid">
    <table id="resultList" class="table-striped table-bordered table-headerfixed">
        :
      <tbody class="scrollBody"></tbody>
    </table>
    </div>

結果

 一覧表示が完成しました。

映画鑑賞記録.png

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

axiosライブラリを用いてクエリパラメータを指定する方法(GET,POST)

はじめに

時々忘れることがあるので、今回も自分用メモとして残します。
今回は、簡潔に非同期処理を記述する為にasyncとawaitを利用していますが説明等はしておりません。ご了承くださいませ?‍♂️

GETリクエスト

主にAPIを叩いて、データを取得する時に利用します。

const response = await axios.get(url, {
  params: {
    id: 1,
    name: 'hoge'
  }
})

上記のように記述すると、id=1とname='hoge'をクエリパラメータとして渡すことができます。
urlの箇所は、APIにアクセスしたいエンドポイントを記述してください:writing_hand:

また、直接URLに記述する方法もあります。

const response = await axios.get('url?id=1&name="hoge"')

複数のパラメータを指定する場合は、&記号(アンパサンド)でつないで記述します。

POSTリクエスト

主にAPIを叩いて、データを保存する時に利用します。

const response = await axios.post(url, {
  id: 1,
  name: 'hoge'
})

第二引数に保存したいデータをオブジェクトで指定します。

はい、以上になります。

おわりに

毎回、自分用のメモとして記述してしまってすみません...もし今回の記事で誤字や脱字等ありましたらご指摘頂けると助かります。閲覧して頂き、ありがとうございました。

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

Vue.js+Flaskで画像のアップロード機能

概要

今回はフロントエンドにVue.js、バックエンドにFlaskを用いた画像認識アプリを作ります。
ひとまず今回は画像アップロード機能までの実装です。

環境

  • Docker
  • Vue-cli
  • flask (pipenv)

上記の環境で環境構築しました。
手順や詳細は以下のリンクを参照してください。

Vue + Flask on Docker

要所の説明

Vue

詳細を説明するのは、以下のコード。

Home.vue
// 画像をサーバーへアップロード
    onUploadImage () {
      var params = new FormData()
      params.append('image', this.uploadedImage)
      // Axiosを用いてFormData化したデータをFlaskへPostしています。
      axios.post(`${API_URL}/classification`, params).then(function (response) {
        console.log(response)
    })
  1. 取得した画像はBase64化がなされている。「data:image/jpeg:base64,〜」
  2. FormDataにより、データをHTTPリクエストで「キー:値」の形式へ。
  3. Axiosを適用し、'127.0.0.1:5000/classification'+ POSTメソッドでデータを送信。

Flask

詳細を説明するのは、以下のコード。

app.py
@app.route('/classification', methods=['POST'])
def uploadImage():
    if request.method == 'POST':
        base64_png =  request.form['image']
        code = base64.b64decode(base64_png.split(',')[1]) 
        image_decoded = Image.open(BytesIO(code))
        image_decoded.save(Path(app.config['UPLOAD_FOLDER']) / 'image.png')
        return make_response(jsonify({'result': 'success'}))
    else: 
        return make_response(jsonify({'result': 'invalid method'}), 400)
  1. FormDataの内部に「data:image/jpeg:base64,〜」が存在。ファイル名を取得。
  2. Pillow(PIL)で画像を取得。
  3. 画像の保存。

全体像

Vue

Home.vue
<template>
  <div>
    <div class="imgContent">
      <div class="imagePreview">
        <img :src="uploadedImage" style="width:100%;" />
      </div>
      <input type="file" class="file_input" name="photo" @change="onFileChange"  accept="image/*" />
      <button @click='onUploadImage'>画像判定してくだちい・・・</button>
    </div>
  </div>
</template>

<script>
import axios from 'axios'

const API_URL = 'http://127.0.0.1:5000'
export default {
  data () {
    return {
      uploadedImage: ''
    }
  },
  methods: {
    // 選択した画像を反映
    onFileChange (e) {
      let files = e.target.files || e.dataTransfer.files
      this.createImage(files[0])
    },
    // アップロードした画像を表示
    createImage (file) {
      let reader = new FileReader()
      reader.onload = (e) => {
        this.uploadedImage = e.target.result
      }
      reader.readAsDataURL(file)
    },
    // 画像をサーバーへアップロード
    onUploadImage () {
      var params = new FormData()
      params.append('image', this.uploadedImage)
      // Axiosを用いてFormData化したデータをFlaskへPostしています。
      axios.post(`${API_URL}/classification`, params).then(function (response) {
        console.log(response)
      })
    }
  }
}
</script>

Flask

留意点

  • CORSにより、異なるオリジン(プロトコルやドメイン、ポート)でもリソースを共有できる。
  • CORSは異なるWebアプリケーションを持つ場合、必須。
  • app.config['JSON_AS_ASCII'] = False により日本語対応可能。
app.py
# render_template:参照するテンプレートを指定
# jsonify:json出力
from flask import Flask, render_template, jsonify, request, make_response

# CORS:Ajax通信するためのライブラリ
from flask_restful import Api, Resource
from flask_cors import CORS 
from random import *
from PIL import Image
from pathlib import Path
from io import BytesIO
import base64

# static_folder:vueでビルドした静的ファイルのパスを指定
# template_folder:vueでビルドしたindex.htmlのパスを指定
app = Flask(__name__, static_folder = "./../frontend/dist/static", template_folder="./../frontend/dist")

#日本語
app.config['JSON_AS_ASCII'] = False
#CORS=Ajaxで安全に通信するための規約
api = Api(app)
CORS(app)

UPLOAD_FOLDER = './uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

# 任意のリクエストを受け取った時、index.htmlを参照
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def index(path):
    return render_template("index.html")

@app.route('/classification', methods=['POST'])
def uploadImage():
    if request.method == 'POST':
        base64_png =  request.form['image']
        code = base64.b64decode(base64_png.split(',')[1]) 
        image_decoded = Image.open(BytesIO(code))
        image_decoded.save(Path(app.config['UPLOAD_FOLDER']) / 'image.png')
        return make_response(jsonify({'result': 'success'}))
    else: 
        return make_response(jsonify({'result': 'invalid method'}), 400)

# app.run(host, port):hostとportを指定してflaskサーバを起動
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

様子

スクリーンショット 2020-07-29 18.47.42.png

こんな感じ

非常に参考になりました

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
画像をPOST、顔検出、canvasで顔にお絵かき

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

Nuxt + Composition-APIの導入手順とはじめかた

Nuxt + Composition-APIの導入手順とはじめかた

はじめに

Vueは、JavaScript製のUIフレームワークです。そのVueについてもう少しWebサイト構築に便利なようVue-routerやVuexなどをまとめたのがNuxtになります。つまるところNuxtというのはフレームワークのフレームワークになります。

Nuxtは非常に便利なのですが、残念ながら今はVueのメジャーバージョンのアップデート時期ということもありいささか混乱している状況です。おそらく他のQiitaの記事を読んでも「書きかたが全然違う……」ということもありえますが、今後新しい書き方が一般的になるということで、composition-apiを使用した書き方を紹介したいと思います。

対象読者

  • ある程度JavaScript/TypeScript、Nodejsがわかる方
  • Vueちょっとわかる方
  • Composition APIが初めてという方

環境

以下はインストールしておく必要があるものです。

  • Nodejs
  • Yarn (※必ず必要というものではありませんが、Yarnインストールされている前提で説明します)

インストール

Nuxtが開発しているCLIのcreate-nuxt-appから簡単にNuxtアプリが作れます。
nuxtをそのままインストールすることもできますが、基本的にはcreate-nuxt-app経由で作ることがほとんどです。

yarn create nuxt-app project-name

# または
npx create-nuxt-app project-name

project-nameには今回作るアプリの名前を入れます。

✨  Generating Nuxt.js project in project-name
? Project name: project-name
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: ESLint, Prettier
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)

CLIから対話的に選択項目を色々と聞かれますが、今回の開発に必須なのはTypeScriptであることとだけです。
上記ではついでにESLint+Prettier+Axiosモジュールを入れていますが、必須ではありません。
インストールが終わったらcd project-nameでプロジェクトディレクトリに入り、以下のコマンドを打ってください。

yarn dev

# またはyarnをインストールしていない場合
npm run dev

Nuxtの開発モードで開始します。デフォルトではhttp://localhost:3000でリッスンしていますので、アクセスして正常にNuxt初期画面が出ればOKです。

Composition APIをインストール

次にComposition APIをインストールします。

yarn add @vue/composition-api

plugins/composition-api.tsを用意します。

plugins/composition-api.ts
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)

nuxt.config.jsにプラグインとして登録します。

plugins: [
  '@/plugins/composition-api'
],

これで使用したいVueファイルなどで以下のように宣言すればcomposition-apiが使えます。

import { ref } from '@vue/composition-api'

リアクティブシステムを使ってみる

早速リアクティブシステムを使ってみます。

<template>
  <div>{{ count }}</div>
</template>
<script lang="ts">
import { ref } from '@vue/composition-api'
export default {
  setup() {
    const count = ref(1)
    return { count }
  },
}
</script>
<style></style>

Vueの基本的なファイルは、テンプレートブロックとスクリプトブロック、スタイルブロックの3つに分けられます。
テンプレートには主にhtml要素を書いていきます。スクリプトブロックではJavaScript/TypeScriptを書いていきます。
今回はTypeScriptを使用するので、scriptタグのプロパティで言語を以下のように指定します。

<script lang="ts"></script>

テンプレートブロック内でデータバインディングしたい際は、ムスターシュ構文({{}}記号で変数を囲むような構文)で可能です。

リアクティブ用関数について

Composition APIで変数をリアクティブにするにはref()とreactive()関数の2つを使用することができます。
どちらもリアクティブにする関数で、どっちがいいかと言われると、どっちも両方使えたほうが良いので丸暗記してほしいです。最初はrefから覚えるといいと思います。

const count = ref(1)

これで初期値1のリアクティブ変数を作れます。
今回はref1という値を渡していますので、勝手にnumber型と推論されていますが、もし渡したい変数の型を明確にしたい場合は、以下のように型引数を渡します。

const count = ref<number>(0)

基本的には型引数がない場合は型推論されるので、単純な数字や文字列の場合はあまり気にせず使用します。
型注釈も含めて書きたい場合は以下のようにします(普段はこのようには書いてませんが)。

import { Ref } from '@vue/composition-api'
const count:Ref<number> = ref<number>(0)

動的に変更を加える

次に、この数を変更するためのボタン要素とインクリメント関数を追加してみましょう。
インクリメントは以下のように関数にして、return でその関数を返します。

export default {
  setup() {
    const count = ref(1)
    const increment = () => {
      count.value++
    }
    return { count, increment }
  },
}

または

export default {
  setup() {
    const count = ref(1)
    function increment() {
      count.value++
    }
    return { count, increment }
  },
}

上の方が関数式。下は関数宣言によるものです。
どちらでも使えますが、関数式で作るほうが色々と便利なので、上の書き方をおすすめします。
returnは返り値を意味しますが、返り値は1つのオブジェクトです。
基本的にJavaScript/TypeScriptは1つの値しか返せません。もしも複数の要素を返したい場合によく使われるのは、複数の変数を1つのオブジェクトにまとめることです。

return { count: count, increment: increment }

この書き方はES6以降の書き方で、以下のように省略しできます。

return { count, increment }

プロパティ名とその値の変数名が同じ場合は、1つにまとめることができます。
テンプレートブロック内で使いたい変数をオブジェクトのプロパティにすればいいので、例えば以下のような書き方でも普通に使用できます。

<template>
  <div>
      {{numberOne}}<br />
      {{numberTwo}}<br />
      {{numberThree()}}<br />
  </div>
</template>
<script lang="ts">
export default {
    setup(){
        return { 
            numberOne: 'いち', // 文字リテラル
            numberTwo: 4/2, // 計算式
            numberThree: () => 'さん' //関数式。()=>{ return 'さん'}と同じ
        }
    }
}
</script>

あとはテンプレートブロックに、ボタン要素とクリックイベント発火用のディレクティブを追加すればOKです。

<template>
  <div>
      <button @click="increment">
          {{count}}
      </button>
  </div>
</template>

v-on:clickディレクティブの省略形である@clickで、先程returnしたincrementを指定しています。これでクリックされたとき、incrementが実行されます。
まとめると以下のようになります。

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>
<script lang="ts">
import { ref } from '@vue/composition-api'
export default {
  setup() {
    const count = ref(1)
    const increment = () => {
      count.value++
    }
    return { count, increment }
  },
}
</script>
<style></style>

実際にボタンをクリックすると、数がカウントアップされれば成功です。

次回

次回からはCompositionAPIの追加機能など紹介したいと思います。

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

Vue.jsを1から学んでみた〜Vue コンポーネント〜

Vue.js を1から学んでみた。にて書いてきましたが、
長くなり編集しにくくなってきたので、分割しました(こんなやりかたしないのかな?知りたい。)

7.Vueコンポーネント

基本

前提:コンポーネントを使わない場合

Vueインスタンスを一つ作成した場合、DOMとして展開されるのは一つだけ。

sample7-1.html
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app7-1"></div>
<!-- これは二つ目なので無視される --> 
<div id="app7-1"></div>
<p>これは表示される</p>
sample7-1.js
var vm = new Vue({
  el: '#app7-1',
  data: {
    message: '1つしか表示されないはず'
  },
  // template→renderに
  render: function(createElement) {
    return createElement('h1', this.message)
  }
})

結果
image.png

上記のように、vmインスタンスをhtmlで使い回すことができない。。

命名規則

ケバブケースかパスカルケースならOK。

  • ケバブケース: first-name
  • パスカルケース: FirstName

どっちかに統一させることが大事。

パスカルケースでおすすめ。理由は以下の通り。

  • JavaScriptでケバブケースは使わないため。
  • HTMLタグと見分けつきやすい。パスカルケースは、Vueコンポーネントとわかりやすい。

コンポーネント使用例

同じインスタンスを複数使う場合、Vueコンポーネントを使用する

sample7-2.html
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<div id="app7-2">
  <Component-name></component-name>
  <component-name></component-name>
  <component-name></component-name>

  <!-- これはvmインスタンスができてから宣言されるため、使えない -->
  <component-name2></component-name2>
</div>

<p>最下層</p>
<!-- 以下はマウントされていないので使えない -->
<component-name></component-name>
sample7-2.js
// 第1引数:コンポーネント名
Vue.component('component-name', {
  // 第2引数:Vueコンポーネント用の書き方色々
  // 今回は簡単にtemplateだけ
  template: '<p>2つ以上表示される</p>'
});

var vm = new Vue({
  el: '#app7-2',
  data: {
    message: '1つしか表示されないはず'
  }
});

// 上から呼ばれるので、#app7-2では使えない
Vue.component('component-name2', {
  // 第2引数:Vueコンポーネント用の書き方色々
  // 今回は簡単にtemplateだけ
  template: '<p>これは表示されないはず</p>'
});

結果
image.png

dataは関数にする

Vue.jsの仕様として、コンポーネントのdata関数にする必要がある
なぜ関数にする必要があるかというと、仮にdataがオブジェクトだったとすると全てのインスタンスでdataが共有されてしまうため。
コンポーネント化することで複数のインスタンスを作成することができるようになったが、
そのインスタンスのフィールドの値が全て同じだったら意味がない。
よって、関数にする必要があります。

sample7-3.html
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app7-3">
  <component-name></component-name>
</div>
sample7-3.js
Vue.component('component-name', {
  // コンポーネントではdataを関数にする
  data: function() {
    return {
      message: 'コンポーネントdata'
    }
  },
  template: '<p>{{message}}</p>'
});

var vm = new Vue({
  el: '#app7-3',
});

結果
image.png

グローバル登録・ローカル登録

sample7-3.jsのように、Vueコンポーネントを定義したより後ろのVueインスタンス(インスタンス名:vm)で使えるようにする登録方法をグローバル登録という。
それに対して、指定したVueインスタンスに直接Vueコンポーネントを定義できる。この登録方法をローカル登録という。

sample7-4.html
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app7-4-1">
  <!-- ローカル登録のコンポーネント名が勝つ -->
  <component-name></component-name>
</div>
<hr>
<div id="app7-4-2">
  <!-- グローバル登録のコンポーネントが表示される -->
  <component-name></component-name>
</div>
sample7-4.js
// コンポーネントのグローバル登録
Vue.component('component-name', {
  // コンポーネントではdataを関数にする
  data: function() {
    return {
      message: 'グローバル登録'
    }
  },
  template: '<p>{{message}}</p>'
});

// ローカル登録では、コンポーネントを変数で定義する
var component = {
  // コンポーネントではdataを関数にする
  data: function() {
    return {
      message: 'ローカル登録'
    }
  },
  template: '<p>{{message}}</p>'
}

var vm1 = new Vue({
  el: '#app7-4-1',
  // コンポーネントのローカル登録
  components: {
    'component-name' : component
  }
});

var vm2 = new Vue({
  el: '#app7-4-2',
});

結果
image.png

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

【npm】パッケージの脆弱性対処(備忘録)

nuxtでアプリをビルドしたときにパッケージの脆弱性に関する警告文が出た
npmの依存関係を修復したい
警告文を1㍈も理解せずnpm install で胡麻化してきた」人にとって、一歩踏み出す補助輪になればとも思いメモを残します
根本的な理解については別途お調べください

目次

package.json と package-lock.json

node_modules(パッケージの大群)はgitへpushするものではない
なので、再現するためにパッケージを記しておくファイル(package.json)が必要
package.json で指定している内容を元に npm install では各種パッケージがインストールされる
が、実際にインストールされるものは package.json への書き方やタイミングでインストールされるものが変わる
そのため、実際にインストールされたものが package-lock.json へ記録される(package-lock.jsonが存在しない場合は生成される)
これにより、他の開発環境で npm install する際は package-lock.json が参照され
固定されたバージョンで各パッケージがインストールがされるため、node_modulesが再現できる

脆弱性の確認

脆弱性のチェック
npm audit

npmaudit1.png
audit は監査という意味
パッケージの依存関係に脆弱性がある箇所を教えてくれるコマンド
※ただし、npm v6以上

脆弱性の自動修正

脆弱な箇所の自動修正
npm audit fix
npm audit fix --force

npmaudit2.png
ある程度は自動修正してくれる
high,criticalを潰しておけばとりあえずはOK
自動修正してくれない部分はログを見て手動で直した

脆弱性の手動修正

パッケージの在り処を参照
npm ls "パッケージ名"

パッケージ名を指定して、どこにそのパッケージがあるか探すことができる
不必要なパッケージを削除して、インストールしなおす

packageのインストール
npm install "パッケージ名"
npm install "パッケージ名@バージョン" ※バージョン指定する場合

参考記事

Qiita記事

脆弱性の警告を受けたnpmパッケージの依存関係を力技で直す
あなたがnpm installをしてはいけない時
npmパッケージのvulnerability対応フロー
【Node.js】package.jsonとpackage-lock.jsonについて簡単にまとめる
package-lock.jsonについて知りたくても聞けなかったこと

外部記事

"npm install"と "npm ci"の違いは何ですか?
package.jsonとpackage-lock.jsonの運用方法について

以上

間違った理解や記述があればご指摘いただけると幸いです

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

Vue.jsを1から学んでみた〜Vue CLIとVue Component〜

Vue.js を1から学んでみた。にて書いてきましたが、
長くなり編集しにくくなってきたので、分割しました(こんなやりかたしないのかな?知りたい。)

8-1. Vue CLIとは

Vueのフレームワーク。
簡単に利用でき、サーバーを立ち上げることができる。

$ vue create vue-cli-sample


Vue CLI v4.4.6
? Please pick a preset: default (babel, eslint)


Vue CLI v4.4.6
✨  Creating project in /Users/shinri/development/study/vuejs/vue-cli-sample.
?  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...

・・・省略

$ cd vue-cli-sample/
$ npm run serve

> vue-cli-sample@0.1.0 serve /Users/shinri/development/study/vuejs/vue-cli-sample
> vue-cli-service serve

 INFO  Starting development server...
98% after emitting CopyPlugin

 DONE  Compiled successfully in 2287ms                                                                                                                                                                                       13:24:23


  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://192.168.100.16:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

http://localhost:8080/ にアクセスするとVueの画面が表示される。
image.png

8-2. Vue CLI構成

image.png

初期ディレクトリ概要

  • node_modules
    • nodeでインストールされた様々なモジュール群
    • 何か便利なライブラリを見つけたらnpmでインストールするが、ここに入る
  • public
    • 静的なファイルを置いておく。webpackの対象にならない。
    • index.html しか初期は入っていない(favicon除く)。
    • あんまり追加しない。
  • src
    • vueファイルを書いてアプリを作る(雑)
    • webpackの対象となる

初期ファイル概要

  • .gitIgnore
    • gitにpushしないファイル/ディレクトリの指定
  • babel.config.js
    • ES5にする時の指定。
    • 初期設定のままで基本はOK。複雑なものを作る場合は別。
  • package.json
    • 使用するライブラリのバージョンを指定する
    • scriptsでは、コマンドのオプションを指定する。$npm run serveは、"serve": "vue-cli-service serve"のように定義されている
  • package-lock.json

srcディレクトリ詳細

assetsディレクトリ

ロゴファイルなど、リソースを入れる。

main.js

render関数で、App.vueオブジェクトを指定しているのみ。
#appにマウントしているが、これはpublic/index.htmlに指定されている。
つまり、App.vueオブジェクトをindex.htmlに表示させていることになる。

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <!-- ↓ここに#appが指定されている -->
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
src/main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

App.vue

単一ファイルコンポーネントと呼ばれる。
画面に表示する内容を、このコンポーネントで実装する。

なお、.vueファイルはVue CLIで使われることで、webpackにて単一コンポーネントとして扱われる。
import App from './App.vue'のように定義すると、.vueファイル内で単一コンポーネントとして使用できる。

以下の初期作成の例では、HellowWorld.vueコンポーネントを作成し、
それをApp.Vueでタグで使うことで画面を表示させている。

src/App.Vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
src/components/HelloWorld.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>
    <h3>Installed CLI Plugins</h3>
    <ul>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
    </ul>
    <h3>Essential Links</h3>
    <ul>
      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
    </ul>
    <h3>Ecosystem</h3>
    <ul>
      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

本番用ビルド

$ npm run build を実行することで、src,publicの内容が最適化ビルドされ、distディレクトリが作成される。
これをそのまんまリリースできる。
image.png

8-3. Vueコンポーネントの基本

Vueコンポーネント(.vueファイル)は、以下で構成される。

  • template
    • DOMとして展開される内容を記述する箇所
    • scriptにrender関数があれば、不要。
    • 注意点:ルート要素は必ず1つであること。エラーになる
  • script
    • JavaScriptで記載する箇所
    • 特に必要なければ、不要。
  • style
    • CSSを記載する箇所
    • CSSが不要であれば、不要。

templateとscriptだけを書いて簡単なvueにて実行してみる

App.vue
<template>
  <p>こんにちは{{name}}さん</p>
</template>

<script>
export default {
  data: function() {
    return {
      name: "太郎"
    }
  }
}
</script>

<!-- styleは消しました -->

http://localhost:8080/ にアクセスするとVueの画面が表示される。
image.png

補足:ルート要素は1つ

以下のように、template配下にルート要素を2つ以上持つと、コンパイルエラーとなる。

hoge.vue
<template>
  <p>こんにちは{{name}}さん</p>
  <p>こんにちは{{name}}さん</p>
</template>
 error  in ./src/FirstName.vue?vue&type=template&id=50ec6832&

Module Error (from ./node_modules/vue-loader/lib/loaders/templateLoader.js):
(Emitted value instead of an instance of Error) 

  Errors compiling template:

  Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

  1  |  
  2  |  <p>こんにちは{{name}}さん</p>
     |                         
  3  |  <p>こんにちは{{name}}さん</p>
     |  ^^^^^^^^^^^^^^^^^^^^^^
  4  |  

Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

一つにしなさいと怒られます。ただ、シンプルに機能を分けるサポートをしてくれるという意味では助かることかと。

8-4. Vueコンポーネントのグローバル登録

どこの.vueでも単一コンポーネントを使えるように、グローバル登録を行う。
グローバル登録はmain.jsに対して行う。

以下のように、FirstName.vueを新規作成し、グローバル登録した後、そのコンポーネントを使用する。
image.png

FirstName.vue
<template>
  <p>こんにちは{{name}}さん</p>
</template>

<script>
export default {
  data: function() {
    return {
      name: "次郎"
    }
  }
}
</script>
App.vue
<template>
  <!-- グローバル登録したコンポーネントを使用 -->
  <FirstName></FirstName>
</template>
main.js
import Vue from 'vue'
import App from './App.vue'
// コンポーネント追加
import FirstName from './FirstName.vue'

Vue.config.productionTip = false

// グローバル登録
Vue.component("FirstName", FirstName);

new Vue({
  render: h => h(App),
}).$mount('#app')

結果
image.png

8-5. Vueコンポーネントのローカル登録

グローバル登録の次にローカル登録について。
単一コンポーネントのローカル登録と同じく、Js内(今回で言うと、<script>内)でimportし、vueインスタンスのcompnentに登録する。

以下のように、LastName.vueを新規作成し、ローカル登録した後、そのコンポーネントを使用する。
main.jsFirstName.vueは変わらないので割愛。

image.png

LastName.vue
<template>
  <div>
    <p>私は{{name}}です</p>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      name: "山田"
    }
  }
}
</script>
App.vue
<template>
  <!-- ルートは一つ -->
  <div>
    <!-- グローバル登録したコンポーネントを使用 -->
    <FirstName></FirstName>
    <!-- ローカル登録したコンポーネントを使用 -->
    <LastName></LastName>
  </div>
</template>

<script>
import LastName from "./LastName.vue";

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

結果
image.png

8-6. VueコンポーネントのCSS

<style>タグにCSSを書けば良いが、必ずscopedをつける。
そうすることで、同じvue内のtemplateにのみ適用させることができる。

FirstName.vue
<template>
  <div>
    <p>こんにちは{{name}}さん</p>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      name: "次郎"
    }
  }
}
</script>

<!-- `scoped`をつける -->
<style scoped>
div {
  border: 1px blue solid;
}
</style>

image.png

内部的にみると、該当タグに識別子をつけて判断している感じ。
image.png

scopedを付けないと、全divに反映されてしまう

<!-- `scoped`をつけない -->
<style>
div {
  border: 1px blue solid;
}
</style>

image.png

8-7. コンポーネント間のデータの受け渡し(親→子:props)

基本:プリミティブ/配列/オブジェクト

親→子の場合、propsを使います。
例として、App.vueから数値と文字列を受け渡すやり方を載せます。
注意:propsは変更値不可なので、子画面で利用する場合は変数に詰め替えてから使う。

FirstName.vue
<template>
  <div>
    <p>こんにちは{{name}}さん</p>
    <button @click="changeMethod">押した分だけ年齢増えます</button>
    <p>年齢:{{age}}</p>
    <p>性別:{{gender}}</p>
  </div>
</template>

<script>
export default {
  // 親→子の受け渡しに`props`を使う。
  // 子では受け口として定義するイメージ。
  // 書き方は配列かオブジェクトで可能。
  // ただ、詳しく書きたいのであれば、オブジェクト型とする。
  props: {
    // 変数名:キャメルケース
    yourAge: {
      type: Number,  // 型
      required: true // 必須かどうか
    },
    gender: {
      type: String,
      required: true
    }
  },
  data: function() {
    return {
      name: "次郎",
      // propsのデータは変更不可が推奨されているため、
      // このようにdataに設定する
      age: this.yourAge
    }
  },
  methods:{
    changeMethod: function() {
      // propsの値は変更させない
      this.age += 1
    }
  },
}
</script>

<style scoped>
div {
  border: 1px blue solid;
}
</style>
App.vue
<template>
  <div>
    <!-- ケバブケースでOK -->
    <!-- propsの値を設定する。v-bindも勿論OK! -->
    <FirstName :your-age="defaultAge" gender="男性"></FirstName>
    <LastName></LastName>
  </div>
</template>

<script>
import LastName from "./components/LastName.vue";

export default {
  data: function() {
    return {
      defaultAge: 15
    }
  },
  components: {
    LastName: LastName
  }
}
</script>

結果
aaa10.gif

応用:slot

htmlをそのまんま渡す場合、
v-htmlタグを使って頑張ればいけそうだけど、めんどくさい笑
その代わりにVueコンポーネント内に(DOM要素のように)htmlを書けるようにすることができる機能がある。

そのためにslotタグとv-slotディレクティブを使う (ジャグラー打ちてー)

1つslotの例

LastName.vue
<template>
  <div>
    <!-- slotタグを指定 -->
    <slot></slot>
  </div>
</template>
App.vue
<template>
  <div>
    <FirstName :your-age="defaultAge" gender="男性" @my-click="emitData = $event"></FirstName>
    <!-- 要素にHTMLを指定 -->
    <LastName>
      <p>私は山田です</p>
      <p>職業はSEです</p>
    </LastName>
    <p>思っていた年齢{{defaultAge}}歳でしたが、{{emitData}}歳違っていたようでした。すみません。</p>
  </div>
</template>

<script>
import LastName from "./components/LastName.vue";

export default {
  data: function() {
    return {
      defaultAge: 15,
      emitData: 0
    }
  },
  components: {
    LastName: LastName
  }
}
</script>

結果
image.png

複数のslotの例

結論から言うと、templateタグと、v-slotディレクティブを使う。

LastName.vue
<template>
  <div>
    <!-- slotタグのnameにテンプレート名を指定 -->
    <slot name="name"></slot>
    <hr>
    <!-- slotタグのnameにテンプレート名を指定 -->
    <slot name="job"></slot>
  </div>
</template>
App.vue
<template>
  <div>
    <FirstName :your-age="defaultAge" gender="男性" @my-click="emitData = $event"></FirstName>
    <LastName>
      <!--  v-slotでテンプレート名を指定 -->
      <template v-slot:name>
        <p>私は山田です</p>
      </template>
      <!--  v-slotでテンプレート名を指定 -->
      <template v-slot:job>
        <p>職業はSEです</p>
      </template>
    </LastName>
    <p>思っていた年齢{{defaultAge}}歳でしたが、{{emitData}}歳違っていたようでした。すみません。</p>
  </div>
</template>

<script>
import LastName from "./components/LastName.vue";

export default {
  data: function() {
    return {
      defaultAge: 15,
      emitData: 0
    }
  },
  components: {
    LastName: LastName
  }
}
</script>

結果
image.png

デフォルトslotの例

templateがついていないものは、Vue.jsが<template v-slot:defalut>を勝手に作成しまとめる。
あくまでも、slotを使う場合はtemplateが必要ということ。

App.vue
<template>
  <div>
    <FirstName :your-age="defaultAge" gender="男性" @my-click="emitData = $event"></FirstName>
    <LastName>
      <!-- templateがついていないのは、デフォルトスロットとしてまとめられる1 -->
      <p>デフォルトスロット用1</p>

      <!--  v-slotでテンプレート名を指定 -->
      <template v-slot:name>
        <p>私は山田です</p>
      </template>
      <!--  v-slotでテンプレート名を指定 -->
      <template v-slot:job>
        <p>職業はSEです</p>
      </template>

      <!-- templateがついていないのは、デフォルトスロットとしてまとめられる2 -->
      <p>デフォルトスロット用2</p>
    </LastName>
  </div>
</template>
LastName.vue
<template>
  <div>
    <!-- デフォルトスロット表示用 -->
    <slot></slot>
    <hr>
    <!-- slotタグのnameにテンプレート名を指定 -->
    <slot name="name"></slot>
    <hr>
    <!-- slotタグのnameにテンプレート名を指定 -->
    <slot name="job"></slot>
  </div>
</template>

結果
image.png

スロットプロパティ

スロットプロパティを使えば、子のdataを使うことができる。

  • 親の指定
    • <template v-slot:name="slotProps">"slotProps"がスロットプロパティ(の名称)。
    • 好きな命名でOK。
  • 子の指定:<slot name="title" :name="name"> ←v-bindを使って、スロットプロパティを指定する。
  • 子はVueインスタンスのフィールドを使用して、親のスロットプロパティが受け取るイメージ。
LastName.vue
<template>
  <div>
    <!-- デフォルトスロット表示用 -->
    <slot></slot>
    <hr>
    <!-- v-bindでスロットプロパティを定義 -->
    <slot name="name" :sei="sei" :mei="mei"></slot>
    <hr>
    <!-- slotタグのnameにテンプレート名を指定 -->
    <slot name="job" :syokugyou="syokugyou"></slot>
  </div>
</template>

<script>
export default {
  props: {
    job: {
      type: String,
      required: false
    }
  },
  data: function() {
    return {
      sei: "山田",
      mei: "太郎",
      syokugyou: this.job
    }
  }
}
</script>
App.vue
<template>
  <div>
    <FirstName :your-age="defaultAge" gender="男性" @my-click="emitData = $event"></FirstName>
    <LastName :job="job">
      <!--  スロットプロパティ「slotProps」を定義 -->
      <template v-slot:name="slotProps">
        <!-- このタグでのみ「slotProps」使用できる -->
        <p>私は{{slotProps.sei}}{{slotProps.mei}}です</p>
      </template>

      <!--  スロットプロパティ「hoge」を定義 -->
      <template v-slot:job="hoge">
        <!-- このタグでのみ「hoge」を 使用できる -->
        <p>職業は{{hoge.syokugyou}}です</p>
      </template>
    </LastName>
    <p>思っていた年齢{{defaultAge}}歳でしたが、{{emitData}}歳違っていたようでした。すみません。</p>
  </div>
</template>

<script>
import LastName from "./components/LastName.vue";

export default {
  data: function() {
    return {
      defaultAge: 15,
      emitData: 0,
      job: "家庭教師"
    }
  },
  components: {
    LastName: LastName
  }
}
</script>

結果
image.png

スロットプロパティ(デフォルトスロットのみ)

デフォルトスロットのみの場合、templateタグが不要(Vue.jsが勝手にtemplateタグを付与してくれる)。
その時のスロットプロパティは、単一コンポーネントに定義する。

LastName.vue
<template>
  <div>
    <!-- デフォルトスロット表示用 -->
    <slot :sei="sei" :mei="mei" :syokugyou="syokugyou"></slot>
  </div>
</template>

<script>
export default {
  props: {
    job: {
      type: String,
      required: false
    }
  },
  data: function() {
    return {
      sei: "山田",
      mei: "太郎",
      syokugyou: this.job
    }
  }
}
</script>
App.vue
<template>
  <div>
    <FirstName :your-age="defaultAge" gender="男性" @my-click="emitData = $event"></FirstName>

    <!-- デフォルトスロットのときのスロットプロパティの命名 -->
    <LastName :job="job" v-slot:default="slotProps">
    <!-- <LastName :job="job" v-slot="slotProps"> のようにdefalutは省略可-->

      <!-- templateがなくデフォルトスロットのみ -->
      <p>私は{{slotProps.sei}}{{slotProps.mei}}です</p>
      <p>職業は{{slotProps.syokugyou}}です</p>
    </LastName>
    <p>思っていた年齢{{defaultAge}}歳でしたが、{{emitData}}歳違っていたようでした。すみません。</p>
  </div>
</template>

<script>
import LastName from "./components/LastName.vue";

export default {
  data: function() {
    return {
      defaultAge: 15,
      emitData: 0,
      job: "広告業界"
    }
  },
  components: {
    LastName: LastName
  }
}
</script>

結果
image.png

v-slotの省略記法

#に置き換えることができる

<template v-slot:job="hoge">

<template #job="hoge">
このように省略して書ける。

ただし、defaultスロットについては省略はNG。

8-8. コンポーネント間のデータの受け渡し(子→親:$emit)

子→親の場合、$emitを使います。
元々、$emitは、カスタムイベントを作る時に使用される。
任意のタイミングで$emitを実行することで、親のVueコンポーネント(今回はApp.vue)でイベントを発火させる。
その発火する時に、データを付け加えて戻すような仕組み。
重要:子は親のデータを変更することはできない。あくまでも子からデータを返している。

なお、カスタムイベントはJavaScript内では使われない。
よって(htmlは大文字小文字を区別しないので)、ケバブケースで命名するのが良い。

FirstName.vue
<template>
  <div>
    <p>こんにちは{{name}}さん</p>
    <button @click="changeMethod">押した分だけ年齢増えます</button>
    <p>年齢:{{age}}</p>
    <p>性別:{{gender}}</p>
  </div>
</template>

<script>
export default {
  props: {
    yourAge: {
      type: Number,
      required: true
    },
    gender: {
      type: String,
      required: true
    }
  },
  data: function() {
    return {
      name: "次郎",
      age: this.yourAge
    }
  },
  methods:{
    changeMethod: function() {
      this.age += 1

      // 子→親への値受け渡し。
      // $emitで値を返す。正確いうと、親のイベントを発火させて、引数で渡す。
      // 第1引数:イベント名(ケバブケースで指定する)、第2引数:イベントで渡す値(任意)
      // 元々 $emit はカスタムイベントを作る時に使用。
      this.$emit("my-click", this.age - this.yourAge)
    }
  },
}
</script>

<style scoped>
div {
  border: 1px blue solid;
}
</style>
App.vue
<template>
  <div>
    <!-- v-onディレクティブでイベントを受け取る -->
    <FirstName :your-age="defaultAge" gender="男性" @my-click="emitData = $event"></FirstName>
    <LastName></LastName>
    <p>思っていた年齢{{defaultAge}}歳でしたが、{{emitData}}歳違っていたようでした。すみません。</p>
  </div>
</template>

<script>
import LastName from "./components/LastName.vue";

export default {
  data: function() {
    return {
      defaultAge: 15,
      emitData: 0
    }
  },
  components: {
    LastName: LastName
  }
}
</script>

結果
aaa11.gif

8-8. コンポーネントの動的切り替え

同じ場所に表示するコンポーネントを切り替える時には、componentタグを使う。
以下、新たに ParentコンポーネントとChildコンポーネントを定義し、切り替える。

Parent.vue
<template>
  <div>
    <p>親の山田一郎です</p>
  </div>
</template>
Child.vue
<template>
  <div>
    <p>子供の山田一太郎です</p>
  </div>
</template>
App.vue
<template>
  <div>
    <!-- クリックでコンポーネントを切り替える操作 -->
    <button @click="changeComponent('Parent')">押したら親の名前が表示されます</button>
    <br>
    <button @click="changeComponent('Child')">押したら子供の名前が表示されます</button>
    <hr>

    <!-- compenentタグで動的にVueコンポーネントを切り替える -->
    <component :is='currentComponents'></component>
  </div>
</template>

<script>
import Parent from "./components/Parent.vue";
import Child from "./components/Child.vue";

export default {
  data: function() {
    return {
      currentComponents: "Parent"
    }
  },
  components: {
    Parent: Parent,
    Child: Child
  },
  methods:{
    changeComponent: function(compName) {
        this.currentComponents = compName
    }
  },
}
</script>

結果
aaa12.gif

切り替え時のキャッシュ

なお、切り替えの度にVueインスタンスがdestroycreateを繰り返している。
インスタンスが消えてしまうため、dataが毎回削除(=初期化)されるということなので、注意が必要。

もしdestroyさせないためには(=キャッシュさせるためには)、keep-aliveタグを使う。

App.vue
<!-- keep-aliveタグを使ってキャッシュ -->
<keep-alive>
  <component :is='currentComponents'></component>
</keep-alive>

ただし、こうすると従来のライフサイクルメソッドのdestroyed()が使えなくなる。
その代わりに、deactivated()activated()が使えるようになる。

deactivated():該当コンポーネントが表示されなくなった時にコールされる
activated():該当コンポーネントが表示された時にコールされる

Parent.vue
<template>
  <div>
    <p>親の山田一郎です</p>
  </div>
</template>

<script>
export default {
  // keep-alive用 
  deactivated() {
    console.log("deactivated:非表示になりました")
  },
  // keep-alive用
  activated() {
    console.log("activated:表示になりました")
  }
}
</script>

8-9. コンポーネントでのv-model(propsとemit)

親コンポーネントにてv-modelを指定して、子コンポーネントでその値を反映するやり方は、
props$emitをを両方使う。

typeによって、子コンポーネントのinputタグの書き方が異なる。

テキストボックス

App.vue
<template>
  <div>
    <!-- v-modelの指定 -->
    <LastName v-model="defaultMsg"></LastName>
  </div>
</template>

<script>
import LastName from "./components/LastName.vue";

export default {
  data: function() {
    return {
      defaultAge: 15,
      defaultMsg: 'デフォルトメッセージ'
    }
  },
  components: {
    LastName: LastName
  }
}
</script>
LastName.vue
<template>
  <div>
    <p>私は{{name}}です</p>

    <!-- valueにpropsの値を反映させ、 -->
    <!-- $emitにinputメソッドを指定して発火させて親コンポーネントに反映させる -->
    <input
      id="title"
      type="text"
      :value="value"
      @input="$emit('input', $event.target.value)"
    >
    <pre>{{ value }}</pre>
  </div>
</template>

<script>
export default {
  props: {
    // v-modelの受け口として、valueという変数名で、propsを指定する
    value: {
      type: String,
      required: true
    }
  },
  data: function() {
    return {
      name: "山田",
    }
  }
}
</script>

結果
aaa20.gif

チェックボックス

App.vue
<template>
  <div>
    <!-- v-modelの指定 -->
    <LastName v-model="defaultCheck"></LastName>
  </div>
</template>

<script>
import LastName from "./components/LastName.vue";

export default {
  data: function() {
    return {
      defaultAge: 15,
      defaultCheck: true
    }
  },
  components: {
    LastName: LastName
  }
}
</script>
LastName.vue
<template>
  <div>
    <p>私は{{name}}です</p>

    <!-- チェックボックスのchangeイベントを利用する -->
    <!-- $emitにchangeメソッドを指定して発火させて親コンポーネントに反映させる -->
    <input
      type="checkbox"
      :checked="childChecked"
      @change="childChecked=$event.target.checked"
    >
    <pre>{{ childChecked }}</pre>
  </div>
</template>

<script>
export default {
  props: {
    // valueという変数名で、propsを指定する
    // チェックは、boolean
    value: {
      type: Boolean,
      required: true
    }
  },
  data: function() {
    return {
      name: "山田",
      childChecked: this.value
    }
  }
}
</script>

結果
aaa21.gif

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

tiptap editor ペースト時にタグを削除

tiptapエディタを使うときにペーストする。
その際ゴミタグまでペーストされてしまう。
そんなとき、ゴミタグは削除してペーストする方法。

editor: new Editor({

editorProps: {

    //ペースト時に br タグと p タグは許可。それ以外の画像とかは削除する
    transformPastedHTML(str){

    var arrowTag = ['br', 'p'];

    // // 配列形式の場合は'|'で結合
    if ((Array.isArray ?
    Array.isArray(arrowTag)
    : Object.prototype.toString.call(arrowTag) === '[object Array]')
    ) {
    arrowTag = arrowTag.join('|');
    }

    // arrowTag が空の場合は全てのHTMLタグを除去する
    arrowTag = arrowTag ? arrowTag : '';

    // パターンを動的に生成
    var pattern = new RegExp('(?!<\\/?(' + arrowTag + ')(>|\\s[^>]*>))<("[^"]*"|\\\'[^\\\']*\\\'|[^\\\'">])*>', 'gim');
    return str.replace(pattern, '');
},

ちなみに改行brタグを利用するときは

import HardBreak from '../assets/HardBreak'

も必要なので読み込んでおく。

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

Firebase+Vue.js(composition-api with typescript)でオセロを作った感想

はじめに

次のエントリー用のメモです。
読み飛ばして、質問があればできる限りでエントリーするつもりです。

https://github.com/iwamoto-takaaki/reversi

題の通り作りました。
スマホでやってもリアルタイムに反映されるので面白い。

ひっくり返すのを自分でしなきゃだったり、ルール無用で置けたりだけど・・・

見どころ

  • 組み合わせとしてはqiitaにはたくさんあるエントリー
  • composion-api + typescript は少し珍しい
  • pugやsassが見ずらいという人、ごめん
  • firebase+vueで組む場合の戦略など
  • 細かい部分はあとで別記事書きますのでリクエストあれば優先します

なぜFirebaseとvueは相性が良いのか

サーバーが要らず、サーバとの通信も気にせずアプリケーションに集中できる。実のところReactとか競合する他のフレームワーク使って無いのでVueに限った話でも無いかも。

データバインディング!!

正直、おすすめはこの一点にあり、あとはおまけだと言いたいくらい。

FirebaseにはSnapshotという機能があり、リアルタイムリスナーをクライアントと結びクエリの結果に変更があった際に通知する機能がある。Vueにはデータ変更があった場合、変更を即座に画面に反映させる。これを組み合わせると、一度クエリを投げたらリアルタイムに更新。

  1. Firebaseはリアルタイムリスナーによりクライアントに変更内容を通知する
  2. クライアントはそれをVueのstateに適用する(コーディングする部分)
  3. Vueはstateの変更を検知して、画面の再描写(差分のみ)をおこなう。

※リアルタイムリスナーってなにか私はよく判っていません。(websocketよりブラウザの縛りが少なく、ちょっとだけ遅いらしい。)

Firebase(リアルタイムリスナー) -> クライアント(コールバック) -> reactive(再描写)

つまり、開発時にクライアントとサーバのやり取りはほとんど意識する必要がなく、データの変換を定義するだけ。

RailsやLaravelよりVueが良い理由

もちろん、Firebaseとの相性の話ですよ。
Firebaseのみで完結し、サーバがいらないから。コードに集中できる。

たとえばRailsやLaravelと組み合わせる場合、RailsやLaravelのサーバを用意しなきゃならない。ところがVue.jsでは、SPAなので1つの固定ページでwebアプリケーションが簡潔する。これをFirebaseのHostingというサービスで提供すると、Firebaseのみで済む。

僕はバックエンドエンジニアなんだけどインフラは得意じゃない。フロントエンドの人や初学者はもっとそうではないかと思う。セキュリティー面で考慮すべきことも格段に減るのも優れた点だと思う。

安い

従量課金制なんだけど、サーバレスなのでCPU時間とかデータ量とか通信量なので初期費用が0なのがありがたい。
https://firebase.google.com/pricing

サーバーが必要だとどうしても最低額が必要になる。復数のサービスをkubernetesで走らせてるって言う人は関係ないだろうけど。サーバがなく利用量と金額が初期から線形に一致するというのはかなりのメリットだと思うし、今回の用に限定公開を行う場合においては課金が起こることはまずないとおもっていい。

小規模な社内システムなんかもFirebaseで組むとインフラの計画や費用の計画などする必要が無いのでよいと思う。サービスが成長し、めでたく割高になったら、利用しているサービスを一個ずつ他に移動すれば良い。

ものすごく軽い

Single Page Application(SPA)の利点ですね。

利用した技術など

Firebaseで使ったもの

Firestore

NoSQLデータベース。
firebaseには、もう一つRealtime Databaseがあるけど今ひとつ違いが判らないので、とりあえずFirestoreを使っている。

ちょっとハマったのが、階層型のデータベース構造を持っていて、復数のドキュメントを集約したものがコレクションで、ドキュメントはコレクションを持てるという構造。これによって、データベースの構造は、root/collection1/document1/collection2/みたいなパスでアクセスする。

あと、インデックスにも癖があり、ハマった。ここらへん詳しく書きたいけど調べが足りない。

結構独特な使い方だけど、ブラウザからDBを直接操作できるようになっていて、(かつセキュリティも満たせる)面白いので別エントリで書きたい。

バックアップも試していない。少なくとも今はJSONでExport&Importで済む量だから。

Firebase Hosting

https://firebase.google.com/docs/hosting
静的ファイルのホスティング。

ここにVueのビルド結果のファイルを置く。動的コンテンツだったらCloud Functionsで構築する必要がある。Functions上でどんなフレームワークが有効かは興味があるが今は知らない。

Firebase Authorication

https://firebase.google.com/docs/auth
Email承認やGoogle Account以外にTwittergithubログインを可能にできる。

ログインの状態はデータベースと同様に ログインしているユーザーのスナップショットを取得できる。

Functionsは使っていない

使わないでアプリが作れるのが良いところなんだけど、実際サニタイズなどを真面目にやろうとすると必要。
呼び出し方は、HTTPリクエストもあるんだけど、バッチやデータ登録を起点にできるらしい。
つまり、フロントエンドからお気楽にデータ登録をしているように見えて、バックエンドではFunctionsがバッチリとサニタイズして、リジェクトしたりエラー返したり、履歴をバックエンドのDBに送ったりとかできる。

是非ともマスターしていきたい。

Vue.jsで使ったもの

Vue Router

ゲーム画面を共有するには一意なURLを定義する必要があるので使っている。

Vuexは使っていない

Vuexはページをまたいだ状態の管理ができるのが便利。またサーバとのデータアクセスが、構造化できる。型安全でオブジェクト指向にしたいので、vuex-module-decoratorsを入れたが思ったような挙動をしてくれなかった。

使い方が悪かったのは自分の能力不足であるけど、Vueを使うのしReactiveの動作についての理解がどうしても必要でその当たりが、見え辛いツールだと思う。また、Vuexで状態を持つのではなくFirestoreのsnapshotがあれば大きく不自由は感じないというのもある。

その点、composition-apiはわかりやすかった。(とはいえ使いこなすには時間がかかった。)この点もそのうちエントリーしたい。

composition-api

今回使ったVueは2.6何だけど、Vue3で導入されるcomposition-apiはアドオンでインストールできる。setupメソッドの中でReactiveとComputedを定義して、setupメソッドの実行スコープで定義されたReactiveとComputedのみが再描写の対象となる。
(see: 先取りVue 3.x !! Composition API を試してみる)

これまでのVueは、Vueの中で定義されたメソッドが対象だったために外部のモジュールで定義したモジュールは値を変更しても再描写がおこらない。これに対してWatchなどで対応する方法もあるんだけどまどろっこしいし、動作も重いらしい。

composition-apiだとsetup中に呼び出したReactiveやComputedも対象になるので、外部モジュールはコンストラクタで定義を行うと再描写の対象になる。ある種独特の組み方になるので非常にわかり辛かったが、やり方が分かるとTypescriptの型安全を利用して組むことができるので非常にありがたい。

モジュール化はできたものの、モジュール間でオブジェクト指向らしい相互作用を実現できていない。今回の場合は、Firebaseのスナップショット関心事の通知が行われるので特に問題はないが・・・

Nuxt.jsは使わない

Firebaseでは、Serve Side Rendering(SSR)だとHostingが使えなくなるのでしない。Single Page Applicationをする機能もあるとも訊いているけどよく知らない。正直、メリットがよく判っていない。

その他の技術

pug

コンパイル時にHTMLに展開されるテンプレートエンジン。または、AlterHTMLかもしれない。
閉じタグどころか、タグのカッコ<>を書かなくていいので楽。行数が減ってコードが読みやすい。基本的は省略記法なので、Vueはタグとしてモジュールを指定するのでpugとの相性は良い。(余談だけど、Wordpressのテーマをpugで書こうとしたけどPHPのコードの中はHTMLのままになってしまうので相性が悪かった。)

また、記法がセレクタと同じなのでsass書くときにpugをコピペして要らんとこ削って書き始められるのすごく良い。

sass

中カッコ{}省略して良い程度の使い方しかしていないけど、使わいない理由が無い。

pugもそうだけどインデントが意味を持つ言語ってそれほど好きじゃない。でも、ネストが深いと閉じカッコがどんどん邪魔になる。行数が多くなるか、まとめると今度は個数の調整が面倒になる。こういうものにはカッコが無いほうがやりやすいと感じてる。

VueのStyleの設定をscopedで書くカラーパレットなどの変数を毎回インポートしなきゃいけないのが面倒。というか、毎回展開されるとロードに負荷が掛かりそうで嫌だ。

まとめ

Vueはマジック(どのような原理で動いているかわからない)が良い点でもあり悪い点でもある。ちょいとカスタマイズしようとすると途端に引っかかる。基本的な動作原理を理解していれば問題ないけど、その点で苦労した。composition-apiはその点がわかりやすくなってくれてありがたい。

ソースコード的にはまだまた簡単にできる点があると思うしそもそも、粒立ちが揃っていないところもあるのでぼちぼち修正してベストプラクティスを作り上げたい。

Firebaseは、本当に気軽に開始できてサービスの成長に合わせて機能追加できると考えている。また、サーバレスってマイクロサービスアーキテクチャの最終形とも言えるので大規模になった際に移行もそれほど難しくないと思える。

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

Vue3でv-modelがどう変わったか

はじめに

Vue3になって、v-modelにも少し変更があったようなので、
どう変わったのか動作を確かめてみました。
変更点については、以下を参考にしています。
https://github.com/vuejs/rfcs/blob/master/active-rfcs/0005-replace-v-bind-sync-with-v-model-argument.md
https://github.com/vuejs/rfcs/blob/master/active-rfcs/0011-v-model-api-change.md

変更点

通常の使い方は変わらない

これまで通り、<input>要素に対してのv-modelの使い方は変わらないようです。

<input v-model="name" type="text">

複数のデータをv-modelで同期できるようになる

Vue2.0だとv-modelは子コンポーネントのvaluepropsにバインドされていたため、
v-modelによって子コンポーネントの複数のデータと同期することができなかったのですが、
Vue3では、props名を指定することで複数データの同期ができるようになってます。

ParentForm.vue(親)
<name-input
  v-model:familyName="familyName"
  v-model:givenName="givenName"
></name-input>
NameInput.vue(子)
<div>
  <input :input="givenName" @input="$emit('update:givenName', $event.target.value)" type="text">
  <input :input="familyName" @input="$emit('update:familyName', $event.target.value)" type="text">
</div>

実のところVue2.0でも複数のデータを同期したい場合は、
.syncを使用することで同様のことができました。(※2.3.0以降)

.sync 修飾子 — Vue.js

例えば、名前を入力するコンポーネントがあった場合、
次のような形で、子のプロパティと同期することができました。

ParentForm.vue(親)
<name-input
  :familyName.sync="familyName"
  :givenName.sync="givenName"
></name-input>
NameInput.vue(子)
<div>
  <input :input="givenName" @input="$emit('update:givenName', $event.target.value)" type="text">
  <input :input="familyName" @input="$emit('update:familyName', $event.target.value)" type="text">
</div>

見ての通り子コンポーネント側は変わってないです。
このことから、
今回の変更は、「これまでの.syncをv-modelという名前で使えるようになった」という理解の方が近いと思いました。
.syncはv-modelと同じ動きになるということで、廃止されています。

最後に

内部的な仕様は変わりつつも、
使う側がその変更を極力意識しなくて済むように
アップデートされていていいなと思いました。
v-modelは便利なので、これからも上手く使っていきたいです。

また、間違いや指摘etcがあれば、是非コメントにお願いします!

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

vue.js truncate

以下を参考にしたが、引数を取得できなくて
うまく動かない。

https://qiita.com/7kaji/items/a280c6b5353efa0a7c35

ということで最新版。

app.js
Vue.filter('truncate', (value, length) => {
    if(value){
        var length = length ? parseInt(length, 10) : 20;
        if (value.length <= length) {
            return value;
        }
        return value.substring(0, length) + '...';
    }
});

hoge.vue
<p>{{v.make_body_text | truncate(180) }}</p>


これで 180文字に省略されて出力される。

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

Nuxt.jsとvue-chartjsで実践的な管理画面やレポート画面の生成

完成イメージ

1.UPLOADボタンからCSVデータをアップロードする。
2.CSVデータがテーブルに表示される。
3.レコードにチェックを入れてレンダリングボタンを押す。
4.CSVデータがレポートエリア(グラフなど)に反映される。
5.PRINTボタンでレポートエリアのみ印刷できる。
図1.png

動作環境

バージョン情報

node.js
npx nuxt -v
@nuxt./cli v2.14.0

インポートデータ

node.js
npm install vue-json-to-csv
npm install chart.js --save
npm install vue-chartjs --save

pluginフォルダ内にvue-chartjs.jsを作成する

vue-chartjs.js
import Vue from 'vue';
import { HorizontalBar, mixins } from 'vue-chartjs';
const { reactiveProp } = mixins;

Vue.component('horizontalbar-chart', {
  extends: HorizontalBar,
  mixins: [reactiveProp],
  props: {
    options: {
      type: Object,
      default: () => { },
    },
  },
  mounted() {
    this.renderChart(this.chartData, this.options);
  },
});

Vue.component('radar-chart', {
  extends: Radar,
  mixins: [reactiveProp],
  props: {
    options: {
      type: Object,
      default: () => { },
    },
  },
  mounted() {
    this.renderChart(this.radarChartData, this.radarOptions);
  },
});

Vue.component('scatter-chart', {
  extends: Scatter,
  mixins: [reactiveProp],
  props: {
    options: {
      type: Object,
      default: () => { },
    },
  },
  mounted() {
    this.renderChart(this.scatterChartData, this.options);
  },
});

コンセプト

定量的な人事データをグラフ化することで、評価面談や目標設定などの人事活動に活用できるデータを閲覧、レポート化することを前提にしています。
今回のソースコードはそのうちの一部です。
PDFを一括生成にしたら良いんだけどそれはまたの機会に。

やりたいこと

・csvデータを読み込んでテーブル表示する管理画面的な動作
・任意のレコードをチェック→レコードデータがグラフに反映
・レポート用紙として印刷する

内容

上記動作環境のインポートやプラグインファイルの作成を事前に行って、下記ソースコードをコピーして、pagesフォルダ内に置けば、そのままローカル環境ですぐに動作確認できるはずです。

nuxt.js
<template>
  <v-container fluid class="pa-0">
    <v-flex class="pt-6">
      <v-row id="disp">
        <v-col cols="12">
          <v-card class="pa-3" outlined color="rgbs(245,245,245,1)">
            <div>
              <input
                @change="fileChange"
                type="file"
                ref="input"
                accept="text/csv"
                style="display: none"
              />
              <v-btn color="primary" class="mx-2" @click="csvUpload">Upload</v-btn>
              <v-btn color="primary" class="mx-2" @click="renderingReport">Rendering</v-btn>
              <v-btn color="primary" class="mx-2" @click="display()">Print</v-btn>
            </div>
            <div class="mt-6 mb-2">
              <v-data-table
                v-model="selected"
                :headers="headers"
                :items="csvData"
                :single-select="singleSelect"
                :items-per-page="3"
                show-select
                item-key="num"
                class="elevation-0"
              >
                <template slot="items" slot-scope="props">
                  <td class="text-xs-right">{{ props.item.num }}</td>
                  <td class="text-xs-right">{{ props.item.P1 }}</td>
                  <td class="text-xs-right">{{ props.item.P2 }}</td>
                  <td class="text-xs-right">{{ props.item.P3 }}</td>
                  <td class="text-xs-right">{{ props.item.P4 }}</td>
                  <td class="text-xs-right">{{ props.item.P5 }}</td>
                  <td class="text-xs-right">{{ props.item.P6 }}</td>
                </template>
              </v-data-table>
            </div>
          </v-card>
        </v-col>
      </v-row>

      <v-container height="1050px" class="ma-0 px-1 py-0">
        <v-card class="ma-1" elevation="0">
          <v-card class="px-0 py-1" dark color="#4682b4">
            <v-card-title class="subtitle-1 pa-2 pl-4">個人分析レポート {{ name }}</v-card-title>
          </v-card>
          <v-card-text>
            <v-row>
              <v-col cols="12" class="pl-1 pr-0">
                <!-- Row1-title -->
                <v-card class="py-3" elevation="0">
                  <v-row>
                    <v-col cols="3" class="py-1">
                      <span class="subtitle-2 px-4">要素</span>
                    </v-col>
                    <v-col cols="3" class="py-1">
                      <span class="subtitle-2">行動目標</span>
                    </v-col>
                    <v-col cols="6" class="py-1">
                      <span class="subtitle-2">能力</span>
                    </v-col>
                  </v-row>

                  <v-divider></v-divider>

                  <!-- Row1-content1-->
                  <v-card elevation="0">
                    <v-card
                      v-for="(item, i) in row11Items"
                      :key="i"
                      class="my-1"
                      :color="row11Items[i].cardColor"
                      outlined
                    >
                      <v-row class="align-center">
                        <v-col cols="3" class="pl-3 pr-1">
                          <v-card-text class="caption pl-3 pr-0 py-0">{{ row11Items[i].element }}</v-card-text>
                        </v-col>

                        <v-col cols="3" class="px-0 py-2">
                          <v-row class="pb-2" align="center">
                            <v-col cols="8" class="py-0">
                              <v-card-text
                                class="body-2 pl-3 pr-0 py-0"
                              >{{ row11Items[i].actionTarget[0] }}</v-card-text>
                            </v-col>
                            <v-col cols="3" class="py-0">
                              <v-card
                                class="subtitle-1"
                                color="rgba(71,164,233,0)"
                                width="31px"
                                elevation="0"
                              >{{ row11Items[i].sten[0] }}</v-card>
                            </v-col>
                          </v-row>
                          <v-row class="pt-2" align="center">
                            <v-col cols="8" class="py-0">
                              <v-card-text
                                class="body-2 pl-3 pr-0 py-0"
                              >{{ row11Items[i].actionTarget[1] }}</v-card-text>
                            </v-col>
                            <v-col cols="3" class="py-0">
                              <v-card
                                class="subtitle-1"
                                color="rgba(71,164,233,0)"
                                width="31px"
                                elevation="0"
                              >{{ row11Items[i].sten[1] }}</v-card>
                            </v-col>
                          </v-row>
                        </v-col>

                        <v-col cols="4" class="pl-0 py-1">
                          <v-card-text
                            class="caption pl-3 pr-1 py-0 mb-1"
                            v-if="csvData.length != 0"
                          >
                            <horizontalbar-chart
                              id="bars"
                              :chart-data="chartData1"
                              :options="chartOptions"
                            />
                          </v-card-text>
                          <v-card-text
                            class="caption pl-3 pr-1 py-0 mt-1"
                            v-if="csvData.length != 0"
                          >
                            <horizontalbar-chart
                              id="bars"
                              :chart-data="chartData2"
                              :options="chartOptions"
                            />
                          </v-card-text>
                        </v-col>
                      </v-row>
                    </v-card>

                    <!-- Row1-content2-->
                    <v-card
                      v-for="(item, i) in row12Items"
                      :key="i"
                      class="my-1"
                      :color="row12Items[i].cardColor"
                      outlined
                    >
                      <v-row class="align-center">
                        <v-col cols="3" class="pl-3 pr-1">
                          <v-card-text class="caption pl-3 pr-0 py-0">{{ row12Items[i].element }}</v-card-text>
                        </v-col>

                        <v-col cols="3" class="px-0 py-2">
                          <v-row class="pb-2" align="center">
                            <v-col cols="8" class="py-0">
                              <v-card-text
                                class="body-2 pl-3 pr-0 py-0"
                              >{{ row12Items[i].actionTarget[0] }}</v-card-text>
                            </v-col>
                            <v-col cols="3" class="py-0">
                              <v-card
                                class="subtitle-1"
                                color="rgba(71,164,233,0)"
                                width="31px"
                                elevation="0"
                              >{{ row12Items[i].sten[0] }}</v-card>
                            </v-col>
                          </v-row>
                          <v-row class="pt-2" align="center">
                            <v-col cols="8" class="py-0">
                              <v-card-text
                                class="body-2 pl-3 pr-0 py-0"
                              >{{ row12Items[i].actionTarget[1] }}</v-card-text>
                            </v-col>
                            <v-col cols="3" class="py-0">
                              <v-card
                                class="subtitle-1"
                                color="rgba(71,164,233,0)"
                                width="31px"
                                elevation="0"
                              >{{ row12Items[i].sten[1] }}</v-card>
                            </v-col>
                          </v-row>
                        </v-col>

                        <v-col cols="4" class="pl-0 py-1">
                          <v-card-text
                            class="caption pl-3 pr-1 py-0 mb-1"
                            v-if="csvData.length != 0"
                          >
                            <horizontalbar-chart
                              id="bars"
                              :chart-data="chartData3"
                              :options="chartOptions"
                            />
                          </v-card-text>
                          <v-card-text
                            class="caption pl-3 pr-1 py-0 mt-1"
                            v-if="csvData.length != 0"
                          >
                            <horizontalbar-chart
                              id="bars"
                              :chart-data="chartData4"
                              :options="chartOptions"
                            />
                          </v-card-text>
                        </v-col>
                      </v-row>
                    </v-card>

                    <!-- Row1-content3-->
                    <v-card
                      v-for="(item, i) in row13Items"
                      :key="i"
                      class="my-1"
                      :color="row13Items[i].cardColor"
                      outlined
                    >
                      <v-row class="align-center">
                        <v-col cols="3" class="pl-3 pr-1">
                          <v-card-text class="caption pl-3 pr-0 py-0">{{ row13Items[i].element }}</v-card-text>
                        </v-col>

                        <v-col cols="3" class="px-0 py-2">
                          <v-row class="pb-2" align="center">
                            <v-col cols="8" class="py-0">
                              <v-card-text
                                class="body-2 pl-3 pr-0 py-0"
                              >{{ row13Items[i].actionTarget[0] }}</v-card-text>
                            </v-col>
                            <v-col cols="3" class="py-0">
                              <v-card
                                class="subtitle-1"
                                color="rgba(71,164,233,0)"
                                width="31px"
                                elevation="0"
                              >{{ row13Items[i].sten[0] }}</v-card>
                            </v-col>
                          </v-row>
                          <v-row class="pt-2" align="center">
                            <v-col cols="8" class="py-0">
                              <v-card-text
                                class="body-2 pl-3 pr-0 py-0"
                              >{{ row13Items[i].actionTarget[1] }}</v-card-text>
                            </v-col>
                            <v-col cols="3" class="py-0">
                              <v-card
                                class="subtitle-1"
                                color="rgba(71,164,233,0)"
                                width="31px"
                                elevation="0"
                              >{{ row13Items[i].sten[1] }}</v-card>
                            </v-col>
                          </v-row>
                        </v-col>

                        <v-col cols="4" class="pl-0 py-1">
                          <v-card-text
                            class="caption pl-3 pr-1 py-0 mb-1"
                            v-if="csvData.length != 0"
                          >
                            <horizontalbar-chart
                              id="bars"
                              :chart-data="chartData5"
                              :options="chartOptions"
                            />
                          </v-card-text>
                          <v-card-text
                            class="caption pl-3 pr-1 py-0 mt-1"
                            v-if="csvData.length != 0"
                          >
                            <horizontalbar-chart
                              id="bars"
                              :chart-data="chartData6"
                              :options="chartOptions"
                            />
                          </v-card-text>
                        </v-col>
                      </v-row>
                    </v-card>
                  </v-card>

                  <v-divider class="mb-4"></v-divider>
                </v-card>
              </v-col>
            </v-row>
          </v-card-text>

          <v-footer absolute app color="white" class="pb-0">
            <span class="caption">{{ name }} 様のレポート &copy;{{ new Date().getFullYear() }} NOMOTOM</span>
          </v-footer>
        </v-card>
      </v-container>
    </v-flex>
  </v-container>
</template>

<script>
import VueJsonToCsv from "vue-json-to-csv";

export default {
  data: function () {
    return {
      name: "サンプル太郎",
      singleSelect: true,
      row11Items: [
        {
          element: "マーケットの価値を高め果敢に挑戦する",
          actionTarget: ["マーケット能力", "問題解決力"],
          sten: [10, 5],
          cardColor: "rgba(71,164,233,0.1)",
        },
      ],
      row12Items: [
        {
          element: "つながりを活かし、価値を創造する",
          actionTarget: ["コミュニケーション力", "適応力"],
          sten: [1, 3],
          cardColor: "rgba(214,77,84,0.1)",
        },
      ],
      row13Items: [
        {
          element: "自ら考え、動き、変化に対応しながら自己を高めているか。",
          actionTarget: ["実行力", "自己開発力"],
          sten: [3, 4],
          cardColor: "rgba(0,173,121,0.1)",
        },
      ],

      selected: [],
      labels: {
        no: { title: "no" },
        name: { title: "Name" },
        P1: { title: "P1" },
        P2: { title: "P2" },
        P3: { title: "P3" },
        P4: { title: "P4" },
        P5: { title: "P5" },
        P6: { title: "P6" },
      },
      jsonData: [],
      headers: [
        { text: "No", align: "left", value: "num", width: "81px" },
        { text: "Name", align: "left", value: "name" },
        { text: "P1", align: "left", value: "P1" },
        { text: "P2", align: "left", value: "P2" },
        { text: "P3", align: "left", value: "P3" },
        { text: "P4", align: "left", value: "P4" },
        { text: "P5", align: "left", value: "P5" },
        { text: "P6", align: "left", value: "P6" },
      ],
      csvData: [],
      dataRow: [],
      chartDataValues1: "",
      chartDataValues2: "",
      chartDataValues3: "",
      chartDataValues4: "",
      chartDataValues5: "",
      chartDataValues6: "",
      chartColors1t2: "rgba(71,164,233,0.5)",
      chartColors3t4: "rgba(214,77,84,0.5)",
      chartColors5t6: "rgba(0,173,121,0.5)",
      chartLabels1: ["B"],
      chartLabels2: ["Bar2"],
      chartLabels3: ["Bar3"],
      chartLabels4: ["Bar4"],
      chartLabels5: ["Bar5"],
      chartLabels6: ["Bar6"],
      chartOptions: {
        responsive: true,
        maintainAspectRatio: false,
        title: {
          display: false,
        },
        legend: {
          display: false,
        },
        scales: {
          yAxes: [
            {
              display: false,
              position: "left",
              drawBorder: false,
            },
          ],
          xAxes: [
            {
              display: false,
              position: "top",
              ticks: {
                min: 0,
                max: 10,
                stepSize: 2,
              },
              gridLines: {
                display: false,
              },
            },
          ],
        },
      },
    };
  },
  components: {
    VueJsonToCsv,
  },
  computed: {
    chartData1() {
      return {
        datasets: [
          {
            data: this.chartDataValues1,
            backgroundColor: this.chartColors1t2,
          },
        ],
        labels: this.chartLabels1,
      };
    },
    chartData2() {
      return {
        datasets: [
          {
            data: this.chartDataValues2,
            backgroundColor: this.chartColors1t2,
          },
        ],
        labels: this.chartLabels2,
      };
    },
    chartData3() {
      return {
        datasets: [
          {
            data: this.chartDataValues3,
            backgroundColor: this.chartColors3t4,
          },
        ],
        labels: this.chartLabels3,
      };
    },
    chartData4() {
      return {
        datasets: [
          {
            data: this.chartDataValues4,
            backgroundColor: this.chartColors3t4,
          },
        ],
        labels: this.chartLabels4,
      };
    },
    chartData5() {
      return {
        datasets: [
          {
            data: this.chartDataValues5,
            backgroundColor: this.chartColors5t6,
          },
        ],
        labels: this.chartLabels5,
      };
    },
    chartData6() {
      return {
        datasets: [
          {
            data: this.chartDataValues6,
            backgroundColor: this.chartColors5t6,
          },
        ],
        labels: this.chartLabels6,
      };
    },
  },
  methods: {
    renderingReport() {
      if (this.selected.length != 0) {
        console.log(this.selected[0].P1);
        this.name = this.selected[0].name;
        this.chartDataValues1 = this.selected[0].P1;
        this.chartDataValues2 = this.selected[0].P2;
        this.chartDataValues3 = this.selected[0].P3;
        this.chartDataValues4 = this.selected[0].P4;
        this.chartDataValues5 = this.selected[0].P5;
        this.chartDataValues6 = this.selected[0].P6;
        this.row11Items[0].sten[0] = this.selected[0].P1;
        this.row11Items[0].sten[1] = this.selected[0].P2;
        this.row12Items[0].sten[0] = this.selected[0].P3;
        this.row12Items[0].sten[1] = this.selected[0].P4;
        this.row13Items[0].sten[0] = this.selected[0].P5;
        this.row13Items[0].sten[1] = this.selected[0].P6;
      }
    },
    display() {
      navigator.clipboard.writeText(
        this.selected[0].name + ", Personal Analysis Report"
      );
      document.getElementById("disp").style.display = "none";
      window.print();
      document.getElementById("disp").style.display = "block";
    },
    csvUpload() {
      this.$refs.input.click();
    },
    fileChange: function (e) {
      const file = e.target.files[0];
      const reader = new FileReader();
      const csvData = [];

      const loadFunc = () => {
        const lines = reader.result.split("\n");
        console.log(lines);

        let i = 0;
        lines.forEach((element) => {
          if (i == 0) {
            console.log("0 row was skipped");
          } else {
            const csvDataSplit = element.split(",");

            const csvDataRow = {
              num: csvDataSplit[0],
              name: csvDataSplit[1],
              P1: csvDataSplit[2],
              P2: csvDataSplit[3],
              P3: csvDataSplit[4],
              P4: csvDataSplit[5],
              P5: csvDataSplit[6],
              P6: csvDataSplit[7],
            };

            csvData.push(csvDataRow);
          }
          i++;
        });
        this.csvData = csvData;
      };
      reader.onload = loadFunc;
      reader.readAsBinaryString(file);
    },
  },
};
</script>

<style lang="scss" scoped>
#bars {
  width: 330px;
  max-height: 41px;
  padding-right: 18px;
  padding-left: 2px;
}
</style>

個別のメソッドについて

csvUpload()
読み込んだcsvデータをテーブルデータ用の配列変数に格納する。

renderingReport()
csvデータをグラフなどの配列変数に渡す。

display()
レポートエリア以外を非表示にして印刷を実行する。

おわりに

複数機能あるけど動作確認しやすいと思うので好きにいじってくれればいいです。
管理画面作りは楽しい。

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

docker-composeで超簡単にNuxt.jsで静的サイトの開発環境を構築する方法

はじめに

今回用意するコンテナは4つです
必要なタイミングに応じてdocker-compose upコマンドで1つずつコンテナを起動します

  1. nodeコンテナ
    create-nuxt-appするために使用

  2. devコンテナ
    yarn devを実行し、サーバーを起動する
    アプリ開発中に使用します

  3. generateコンテナ
    静的ファイルを出力するために使用します

  4. dev_staticコンテナ
    出力した静的ファイルをApatchサーバーを立ててローカルで確認するために使用します
    出力結果はdevコンテナで起動中のものと同じ結果なはずなので、必要に応じて使用してください

環境

  • Windows10 Pro バージョン2004
  • WSL2
  • Docker For Windows

1. docker-composeファイル準備

指定するimageのタグはこちらから最新版を取得
おすすめはalpineの最新です
https://hub.docker.com/_/node/

docker-compose.yml
version: '3'

services:
  node: &app_base
    image: node:14.5.0-alpine3.10
    tty: true
    working_dir: /var/www
    volumes:
      - ./src:/var/www # ./srcをコンテナ内の/var/wwwにマウントする
    environment:
      PORT: 3000
      HOST: 0.0.0.0
    restart: always # エラーなどでコンテナが落ちた際に自動で再起動してくれる
  dev:
    <<: *app_base
    ports:
      - "3000:3000"
    command: yarn dev # upする際に実行されるコマンド
  generate:
    <<: *app_base
    command: yarn generate # upする際に実行されるコマンド
  dev_static:
    image: httpd
    volumes:
      - ./src/dist:/usr/local/apache2/htdocs/
    ports:
      - "8080:80"

2. Nuxt.jsのプロジェクト作成

  1. こちらでコンテナ内に入ります
    docker-compose run --rm node sh

  2. 必要なパッケージをインストール
    yarn global add create-nuxt-app

    ※Nuxt.js modulesでVuetify.jsを選択する場合
    apk add python make g++

  3. プロジェクトの作成

create-nuxt-app ./コマンドを実行し、対話形式でアプリのベースを作成

```
$ create-nuxt-app ./
? Project name: test
> test
? Programming language: JavaScript 
> JavaScript
? Package manager: Yarn
> Yarn
? UI framework: None
> None
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
> 未選択でEnter
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
> 未選択でEnter
? Testing framework: None
> None
? Rendering mode: Universal (SSR / SSG)
> Universal (SSR / SSG)
? Deployment target: Static (Static/JAMStack hosting)
> Static (Static/JAMStack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
> 未選択でEnter
```
  1. コンテナから出る
exit

3. 実行

  1. 下記コマンドで起動します

    docker-compose up dev
    
  2. アクセス

http://localhost:3000

4. 静的ファイル出力

  1. 下記コマンドを実行すると/distに出力される
docker-compose up generate

5. 出力した静的ファイルをApacheサーバーで確認

  1. 下記コマンドで起動します

    docker-compose up dev_static
    
  2. アクセス

http://localhost:8080/

6.参考

以上です、お疲れさまでした。

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

Vuetifyでアイコンを個別に読み込むようにしたときにチェックボックスが消える問題

前提

あまり頭を使わずVue + Vuetify + TypeScriptの初期描画を高速化するためにやったことを参考に使うアイコンを個別に読み込むように変更した

すると、チェックボックスとラジオボタン(v-checkboxとv-radio)はそれぞれ内部的にv-iconを使ってmdiのアイコンを使っているようで四角い箱と丸がそれぞれ表示されなくなってしまった。

解消方法

先ほどのコンポーネントたちは、アイコンをonIconoffIconというpropsで管理しているらしい。

参考

なので、それらのpropsに対応するアイコンをバインドしてやれば復活する

使われているアイコン

v-checbox
未選択: mdi-checkbox-blank-outline
選択済: mdi-checkbox-marked

v-radio
未選択: mdi-radiobox-blank
選択済: mdi-radiobox-marked

参考

- mdiチートシート

最終的なコード

<v-checkbox
  :onIcon="icons.mdiCheckboxMarked"
  :offIcon="icons.mdiCheckboxBlankOutline"
>
</v-checkbox>

<script>
import { mdiCheckboxMarked, mdiCheckboxBlankOutline } from "@mdi/js"

export default {
  data() {
    return {
      icons: {
        mdiCheckboxMarked,
        mdiCheckboxBlankOutline
      }
    }
  }
}
</script>

まとめ

mdiチートシート、アイコン探しにくい

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

Nuxt × Go × AWS で比較コミュニケーション的な掲示板サービス作ってみた

比較コミュニケーションサイト Versus
https://versus-web.net

FireShot Capture 075 - 比較コミュニケーションサイト『Versus(バーサス)』 - versus-web.net.png

FireShot Capture 076 - 比較コミュニケーションサイト『Versus(バーサス)』 - versus-web.net.png

Twitter で技術マウント合戦している人たちが面白かったので
AWS と Go の勉強がてら、テーマを持ち寄ってコミュニケーションできる簡単な掲示板サイトを作ってみたので作業工程など記録してみる。

構成

インフラ

  • ECS
  • Lambda
  • DynamoDB
  • API Gateway
  • Route 53

バックエンド

  • Go

フロントエンド

  • Nuxt (SSR)

Go と Fargate 辺りを使ってみたかったのでこんな構成になりました。

作業工程

タスク管理

タスク管理ツールとして Trello を使用した。
スクリーンショット 2020-02-23 17.21.58.png
こんな感じに5カラムで管理してみた。
Doc としてドキュメント管理にも使用してみたが、シンプルでサクサク動くし、個人開発規模ならすごく使いやすいと思った。

デザイン

Figma を使用。
FireShot Capture 066 - Untitled – Figma - www.figma.com.png
こんな感じに遷移図作ったり
FireShot Capture 065 - Untitled – Figma - www.figma.com.png
色管理もしつつ、わりとしっかりめに使ってみたつもり(デザイナーの方からするとまだまだだろう)
このくらいやっておくと開発時だいぶ楽。

開発

Go

Lambda で採用。ディレクトリ構成は以下の通り。

.
├── posts-function
│   └── posts
│       ├── bin
│       ├── cmd
│       └── internal
│           ├── actions
│           ├── handler
│           ├── models
│           └── repositories
└── threads-function
    └── threads
        ├── bin
        ├── cmd
        └── internal
            ├── actions
            ├── handler
            ├── models
            └── repositories
                └── mock

AWS SAM を使用することで、コマンドで Go の Lambda 関数を作成することができる。
ディレクトリ構成は golang-standards を参考にした。
どの粒度で Lambda 関数を作成するか迷ったが、リソース単位で分割することにし、以下のようにハンドリングしてみた。

handler.go
package handler

import (
    "context"

    "posts/internal/actions"

    "github.com/aws/aws-lambda-go/events"
)

var routes = map[string]actions.ActionFactory{
    "GET":  actions.NewPostsGetter,
    "POST": actions.NewPostsPoster,
}

// CORS compatible
var headers = map[string]string{
    "Content-Type":                "application/json",
    "Access-Control-Allow-Origin": "*",
}

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    factory := routes[request.HTTPMethod]
    action := factory()
    jsonData, err := action.Run(request)
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    return events.APIGatewayProxyResponse{
        Body:       string(jsonData),
        StatusCode: 200,
        Headers:    headers,
    }, nil
}

HTTP メソッドで振り分けるだけなのでルーティングは楽。関数を分ければ当然デプロイも分けられるので、この辺の粒度は規模を見つつといった感じになりそう。(ちなみに他プロジェクトではマイクロサービス粒度で分けていた)

Nuxt

フロントエンドで採用。
基本的に初期表示速度的な意味で SSR をすべきと思っているので今回も SSR を採用。
難しいことをしていないのであまり特筆する点がないのだが
バリデーションに vuelidate を使用してみてとても使いやすかったのでおすすめしたい。
人気の VeeValidate と比較すると、vuelidate はテンプレートとアプリケーションロジックが分離しているので、バリデーション時の処理を細く設定したい要件があるときに使いやすい。

所感

AWS と Go がほぼ初見だったためだいぶ苦労した。
Node なら Lambda 上のコードを AWS コンソールから見れるため、とっつきやすさもあるが、Go の場合それができず、デプロイ周りを整えるまで一苦労だった。
新しく何かを作るときは未知の技術は1種類にすべき、みたいな話をどっかで聞いたけどその通りだと思った。
しかし今回の技術スタックはだいぶ耳にすることも多くなってきたので、試してみるのは一興だと思う。

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