20200810のvue.jsに関する記事は17件です。

Gridsome入門 SPAを作ってみよう 【3日目 ドーナッツグラフ編】

スケジュール

前提条件

  • node.js v8.3以上
  • yarn or npmが入っている(Document見るとyarnの方が推奨とのこと)
  • Gridsomeのプロジェクトを作成している

vue-chart.jsでグラフを作成

まず前回入れたvue-chart.jsでドーナッツグラフを表示する処理を作っていきます。

src/components/Chart.vue
<script>
import { Doughnut } from 'vue-chartjs'
export default {
  extends: Doughnut,
  name: 'chart',
  data () {
    return {
      labels: ['QQQ', 'VOO', 'NFLX', 'MSFT', 'BND','VWO','MRNA', 'SQ', 'JNJ', 'VZ'],
      chartData: [271.47, 307.36, 494.73, 212.48, 89.38, 43.44, 74.10, 147.22, 148.60, 58.53],
      options: {
        cutoutPercentage: 30,
      }
    }
  },
  mounted () {
    this.renderChart(
        {
          labels: this.labels,
          datasets: [
            {
              backgroundColor: [
                'rgba(255, 99, 132, 0.2)',
                'rgba(54, 162, 235, 0.2)',
                'rgba(255, 206, 86, 0.2)',
                'rgba(75, 192, 192, 0.2)',
                'rgba(153, 102, 255, 0.2)',
                'rgba(255, 159, 64, 0.2)'
              ],
              data: this.chartData,
            }
          ]
        },
        this.options)
  }
}
</script>

まず前回入れたvue-chart.jsをimportします。今回使うのはドーナッツグラフなので、Doughnutを指定します。
import { Doughnut } from 'vue-chartjs'
他にもLineやPieなど色々な種類があります。
https://www.chartjs.org/docs/latest/charts/
vue-chart.jsについてはこちらの記事が大変参考になりました。
https://qiita.com/kiyc/items/a94a202bf06fff644f62

src/pages/portfolio/index.vue
<template>
  <Layout>
    <h1>Portfolio</h1>
    <chart></chart>
  </Layout>
</template>

<script>
import Chart from "../../components/Chart"
export default {
  components: {
    Chart
  },
  metaInfo : {
    title: 'Hello, world!'
  }
}
</script>

<style>
.home-links a {
  margin-right: 1rem;
}
</style>

作ったChart.vueのcomponentsを表示させたいpageに追加します。

tooltipsにcallback処理を追加する

グラフにカーソルを合わせた時に表示させるtooltipsに処理を追加していきます。

src/components/Chart.vue
<script>
import { Doughnut } from 'vue-chartjs'
export default {
  extends: Doughnut,
  name: 'chart',
  data () {
    return {
      labels: ['QQQ', 'VOO', 'NFLX', 'MSFT', 'BND','VWO','MRNA', 'SQ', 'JNJ', 'VZ'],
      chartData: [271.47, 307.36, 494.73, 212.48, 89.38, 43.44, 74.10, 147.22, 148.60, 58.53],
      options: {
        // ここに追加
        tooltips: {
          callbacks: {
            label: function(tooltipItem, data) {
              let label = data.labels[tooltipItem.index] || '';
              return label;
            },
            afterLabel: function (tooltipItem, data) {
              const currentData = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
              const total = data.datasets[tooltipItem.datasetIndex].data.reduce((a,x) => a+=x,0);
              const ratio = ((currentData / total) * 100);
              let afterLabel = '$' + currentData;
              afterLabel += '(' + (Math.floor(ratio * 10) / 10) + '%)';
              return afterLabel;
            }
          }
        },
        cutoutPercentage: 30,
      }
    }
  },
  mounted () {
    this.renderChart(
        {
          labels: this.labels,
          datasets: [
            {
              backgroundColor: [
                'rgba(255, 99, 132, 0.2)',
                'rgba(54, 162, 235, 0.2)',
                'rgba(255, 206, 86, 0.2)',
                'rgba(75, 192, 192, 0.2)',
                'rgba(153, 102, 255, 0.2)',
                'rgba(255, 159, 64, 0.2)'
              ],
              data: this.chartData,
            }
          ]
        },
        this.options)
  }
}
</script>

labelの表示とlabelの後にthis.chartDataの値と全体に対するパーセントを表示をする処理を入れました。

グラフの色を自動で割り当てる

今は固定でbackground-colorを配列で指定しているので、自動で割り当てるようにします。
こちらの記事が大変参考になりました。
https://qiita.com/muramasawani/items/e61d889ebb53e0974e5f
まず追加でライブラリを追加します。

yarn add google-palette

こちらのライブラリを追加した最終的なコードがこちら

src/components/Chart.vue
<script>
import { Doughnut } from 'vue-chartjs'
// importを追加
import * as Palette from 'google-palette'
export default {
  extends: Doughnut,
  name: 'chart',
  data () {
    return {
      labels: ['QQQ', 'VOO', 'NFLX', 'MSFT', 'BND','VWO','MRNA', 'SQ', 'JNJ', 'VZ'],
      chartData: [271.47, 307.36, 494.73, 212.48, 89.38, 43.44, 74.10, 147.22, 148.60, 58.53],
      options: {
        tooltips: {
          callbacks: {
            label: function(tooltipItem, data) {
              let label = data.labels[tooltipItem.index] || '';
              return label;
            },
            afterLabel: function (tooltipItem, data) {
              const currentData = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
              const total = data.datasets[tooltipItem.datasetIndex].data.reduce((a,x) => a+=x,0);
              const ratio = ((currentData / total) * 100);
              let afterLabel = '$' + currentData;
              afterLabel += '(' + (Math.floor(ratio * 10) / 10) + '%)';
              return afterLabel;
            }
          }
        },
        cutoutPercentage: 30,
      }
    }
  },
  mounted () {
    this.renderChart(
        {
          labels: this.labels,
          datasets: [
            {
              // ここに追加
              backgroundColor: Palette('mpn65', this.chartData.length).map(
                  function(hex) {
                    return '#' + hex
                  }
              ),
              data: this.chartData,
            }
          ]
        },
        this.options)
  }
}
</script>

Palette('mpn65', this.chartData.length)の第一引数にどんなカラータイプを使うかを指定できます。
指定するカラータイプによっては8つまでしか用意していなかったりするので、今回はmpn65というのにしました。
http://google.github.io/palette.js/

最終的な出来上がったドーナッツグラフ

demo.gif

あとがき

今回はドーナッツグラフを作ってみました。明日は折れ線グラフを作る予定です。

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

Vue.js向けの高機能・高性能なテーブルコンポーネントvxe-tableを紹介したい

何に関する記事か?

:white_check_mark: vxe-table というVue.js向けのテーブルUIコンポーネントを紹介する記事です。
かなり高機能・高性能なライブラリなのですが、中国発ということもあり日本語の情報が見当たらなかったので記事にしてみました。

:warning: この記事ではコンポーネントの提供する機能のほんの一部しか紹介していません。より詳細な情報は以下のリンクから参照してください。

:link: Link

  • GitHub - vxe-table
    • コンポーネントの概要やインストール手順、サンプルコードなど
  • vxe-table 公式ガイド(英語・中国語)
    • コンポーネントの提供する機能・サンプルコードやAPIリファレンスなど
    • かなりたくさんの機能が提供されているので、是非一度チェックしてみてください!

誰にとってオススメか?

Element, Vuetify などのコンポーネントライブラリを使っていて、テーブル(グリッド)コンポーネントで実現に手間のかかる機能がある場合や性能的な問題を抱えている場合にオススメです。
とくにテーブルコンポーネントに多様な機能・性能を求められる業務系のアプリなどで良さを発揮しやすいと思っています。

以下では、vxe-table の基本的な書き方を押さえた後、
Element / Vuetify との簡単な性能比較を行うサンプルを作成しています。

vxe-table の基本的な書き方

install

以下のリンクに沿って、CDNかnpmでインストールしましょう。
https://github.com/x-extends/vxe-table/blob/HEAD/README.en.md#installing

日本語化にも対応されています。
https://x-extends.github.io/vxe-table/#/table/start/i18n

シンプルなテーブルの例

まずは GitHub - Example のシンプルな例から見てみます。
※コメントを追記しています。

<template>
  <div>
    <!-- vxe-tableコンポーネントにtableDataをバインドする -->
    <vxe-table :data="tableData">
      <!-- 各カラムをvxe-table-columnで定義する -->
      <!-- type="seq"で行番号を表示する -->
      <vxe-table-column type="seq" title="Seq" width="60"></vxe-table-column>
      <!-- tableDataのkey名をfieldとして指定する -->
      <vxe-table-column field="name" title="Name"></vxe-table-column>
      <vxe-table-column field="sex" title="Sex"></vxe-table-column>
      <vxe-table-column field="address" title="Address"></vxe-table-column>
    </vxe-table>
  </div>
</template>

<script>
export default {
  data () {
    return {
      // vxe-tableにバインドされるデータ
      tableData: [
        { id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', address: 'Shenzhen' },
        { id: 10002, name: 'Test2', role: 'Test', sex: 'Man', address: 'Guangzhou' },
        { id: 10003, name: 'Test3', role: 'PM', sex: 'Man', address: 'Shanghai' }
      ]
    }
  }
}
</script>

これまでに他のテーブルコンポーネントを利用したことのある方はすぐに理解できると思います。
実装ではこれを基本形として、<vxe-table><vxe-table-column>タグにPropsを追加していく形になります。

JSからのテーブル操作

JSからテーブルを操作したい場合は vxe-table タグに ref を付けて、this.$refs から取得したテーブルコンポーネントからAPIを利用します。
例えば、2行目のチェックボックスをtoggleさせたい場合は以下のようなソースになります。

<template>
  <div>
    <button @click="toggleSecondRow">2行目のチェックボックスを操作</button>

    <!-- テーブルにrefで名前を付ける -->
    <vxe-table ref="myTable" :data="tableData">
      <vxe-table-column type="checkbox" width="60"></vxe-table-column>
      <vxe-table-column field="name" title="Name"></vxe-table-column>
      <vxe-table-column field="sex" title="Sex"></vxe-table-column>
      <vxe-table-column field="address" title="Address"></vxe-table-column>
    </vxe-table>
  </div>
</template>

<script>
export default {
  data () {
    return {
      tableData: [
        { id: 10001, name: 'Test1', role: 'Develop', sex: 'Man', address: 'Shenzhen' },
        { id: 10002, name: 'Test2', role: 'Test', sex: 'Man', address: 'Guangzhou' },
        { id: 10003, name: 'Test3', role: 'PM', sex: 'Man', address: 'Shanghai' }
      ]
    }
  },

  methods: {
    toggleSecondRow() {
      // vxe-tableのAPIを利用してチェックボックスの2行目をtoggleさせる
      this.$refs.myTable.toggleCheckboxRow(this.tableData[1]);
    }
  }
}
</script>

:tada: これで vxe-table のガイドを読むのに最低限必要な準備は完了です。
後は vxe-table 公式ガイド を読めば実現したい機能を実装していけるはずです!

vxe-table と他のコンポーネントライブラリとの性能比較

vxe-table では virtual scroller が標準で組み込まれているため、大量データでも高い性能を発揮します。
個人的にこのライブラリで一番感動したポイントなので、大量データのテーブル実装を行ってみて、簡易的な性能比較をしてみたいと思います。

具体的には、Element / Vuetify / vxe-table でそれぞれ10列×1000行のテーブルデータと行選択できるチェックボックスを実装します。

1. Element

https://jsfiddle.net/Nag729/f3j2txnm/20/

スクロールからすでに遅く、チェックボックスの選択はスムーズとは言い難いですね:cry:
ソートも同様に時間がかかっています。

2. Vuetify

https://jsfiddle.net/Nag729/u5jgqvr4/2/

Element と比較すると優秀で、スクロールにストレスはありません
ただ、全データの選択やカラムソートになると結構待たされます:expressionless:

3. vxe-table

https://jsfiddle.net/Nag729/o13xmpn0/6/

スクロール・チェックボックス選択・ソートの全てがスムーズに動いています:blush:
自前でライブラリや設定を追加しなくてもこれだけの大量データに対応してくれるのは嬉しいですね!

終わりに

以上、vxe-table の紹介でした。
現状でもかなりの機能が用意されている上に、これから先もバージョンアップが予定されているようなので、是非一度使ってみてください。

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

現場に残る Vuex の map ヘルパーのコードベースを消し去り、 Vue 3 時代の型に追従するための CLI ツールを TypeScript Compiler API で実現する

はじめに

Vue.js 製アプリケーションのレガシーコードベースにおいて、頻繁に課題となるのが mapGetters のような map ヘルパーのメソッドです。
これらはショートハンド的に使えて過去には便利なケースもありましたが、現在ではほとんど利用されることもなくなりました。

それもそのはず。現在のフロントエンド開発の主流となる言語は JavaScript ではなく TypeScript となっています。しかし、 map 系ヘルパーはその構造から任意の文字列を受け取った上でオブジェクトに影響を及ぼす形となっており、根本的に型システムとの相性が悪い存在です。
これを利用している限り、 Vue Component において map ヘルパーから this に生えたものは、型もつかなければそもそも this に生えていることすら TypeScript 側で検知できず、コンパイルエラーとなってしまいます。

そのため、今では Vue.js + TypeScript でのプロジェクトでは利用されることがほとんどなくなった map ヘルパーですが、記述時点で TypeScript が導入されていなかったコードベースでは、利便性からこれらのヘルパー関数が利用されているコードが残っていることもしばしばあります。

すぐに置き換えられると理想ですが、ネームスペースのあるなしなどの都合で一括置換で終わりといかないのがなかなかつらいところ。

今回はそんな課題を解決するため、7月のオリンピック連休を生かして vuex-map-purge という CLI ツールを作ってみました。

この記事では、簡単にそのモチベーションと利用方法、内部の構造をご紹介します。

vuex-map-purge について

https://github.com/potato4d/vuex-map-purge

vuex-map-purge は、その名の通り map ヘルパー、 mapGettersmapMutations, そして mapActions を分解し、 this にそれ相当の methods または computed を定義してくれる CLI ツールとなります。

例によって例のごとく MIT ライセンスの OSS です。よかったら star とかつけてもらえると。

具体的には、例えばこのような Vuex が利用されている JavaScript あるいは TypeScript のコードベースがあったとき

example.vue
<template>
  <div></div>
</template>

<script>
import Vue from 'vue'
import { mapActions } from 'vuex'

export default Vue.extend({
  methods: {
    ...mapActions(['loginUser']),
    ...mapActions('ui', ['switchToEditorView'])
  }
})
</script>

以下のような、 map ヘルパーを削除したコードベースへと変換してくれます。TypeScript の場合は unknown で型が定義され、 JavaScript のコードベースの場合は型定義をスキップします。

example.vue
<template>
  <div></div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  methods: {
    loginUser(payload?: unknown) {
      return this.$store.dispatch('loginUser', payload)
    },
    switchToEditorView(payload?: unknown) {
      return this.$store.dispatch('ui/switchToEditorView', payload)
    },
  },
})
</script>

導入と利用

基本的にプロジェクトローカルではなく、手元の Node.js 環境にグローバルに導入して実行します。

$ npm i -g vuex-map-purge

執筆時点 (v0.1.2) では特に CLI オプションはなく、対象となるディレクトリを glob 形式の文字列として渡すことで実行できます。
アプリケーション内で glob での走査を行うため、 Prettier などと同様の感覚で引数を渡してください。

$ vuex-map-purge './src/**/*.vue'

vuex-map-purge は purge だけ行いますが、標準出力に影響のあったファイルを出力するため、実際の利用時は xargs などとの併用をオススメします。

$ vuex-map-purge './src/**/*.vue' | xargs prettier --write

このように実行することで、完全な purge が可能です。

なぜこれを利用するのか

TypeScript との親和性はもちろんですが、 来たる Vue 3 への準備 が大きなモチベーションです。
これは README にも記述されています。

Vuex 4.0 fixes a problem that Generics had with the Store in the previous Vuex, making it possible to build a more type-safe system.

However, Vuex's mapXXX utility, which exists in Vuex, does not solve the type problem and hinders future type-safe coding.

As a result, we needed a tool to eliminate mapXXX from existing Vue.js projects as soon as possible.

Vue 3 時代に利用可能となる Vuex 4.0 では、 Vuex が 3.x 時代まで抱えていた Vuex.Store<T>Tany でハードコーディングされている問題が改善されています。

これによって、 this.$store からアクセスするストア構造にユーザー側で型を付与することが可能 となります。
これは大きな Vuex + TypeScript の改善であり、自分たちで Vuex をラップしたような層を用意する必要がなくなります。

ですが前述の通り、 map ヘルパーは文字列とオブジェクトの複雑なマッピングにより実現しており、これ自体の型定義は改善されないように見えます。

そのため、現時点では Vue 3 時代にストアの型を完全に守るためには、 map ヘルパーを取り除く必要がある という状態です。

これまではどのみち Vuex.Store<any> のために移行の大きなモチベーションが沸かない人もいたかと思いますが、これからはやらない意味がなくなるため、需要も出てくるかなと思って開発しました。

しくみについて

今回、この purge のために TypeScript Compiler API を利用してみました。

これは TypeScript のパッケージに含まれるコンパイラの挙動に介入するための API であり、ざっくりいうと今回は以下のようなことをしています。

  • AST ベースで mapXXX を検知し、中の構造をチェック
    • その中で、 AST の種別によって名前空間付きの定義か、ルートの名前空間であるかなどをチェック
  • 上記でチェックした内容をもとにコードを生成し、 this 内にフィールドとして定義を追加
    • AST 上で正しいコードであることが担保された形で this へとメソッドなどを気軽に生やすことができる
  • 結果をコードテキストとして出力する

今回 Compiler API を利用したのは、以前 ESLint の独自ルールを制定しているときに AST を JavaScript で触るのが辛かったため、 型に強い AST 関連のツールキットがほしい というモチベーションでした。

TS なしで AST 触るのって鬼のように console.log してテストコードにしていく以外無理ゲーな気がしてるんですが、何か良いやり方あるんですかね……

実際に行っているステップは以下です。

  1. glob パッケージで glob を判定し、対象となるファイルを洗い出す
  2. cheerio で <script> ブロックを抜き出す
  3. Compiler API に対して自作した transformer (自作 TS プラグインみたいなもの) を渡して変換を実行
  4. Compiler API が吐き出したコードを <script> ブロックの中身に設定
  5. File I/O で書き出す
  6. 書き出したファイルのパスを標準出力に書き出す

本来は Vue の SFC パーサーを正しいものを利用するべきですが、パース自体はできてもパースしたものを再度書き直す処理ができるパーサーが見つからなかったので今回はこのスタイルです。

ちょっとしたリファクタリング程度なら正規表現で行うことも多いと思いますが、TypeScript Compiler API で AST を操作する場合は、基本的には想定するコード以外はスキップした後に、該当するコードだけに処理を行うことができるため、考慮漏れが起きづらいことや、テストコードとの親和性が非常に高いのが良い点かなと思いました。

AST に少しなれるとリファクタリングの効率化が進みそうなので、よかったらコードなど参考にしてもらえればと思います。

https://github.com/potato4d/vuex-map-purge/blob/master/transformers/purgeMapActions.ts

未実装の feature について

そんなわけで publish したばかりの vuex-map-purge ですが、現時点では対応できていない仕組みがいくつか存在するため、注意が必要です。

  • <script> を含むコードベースをうまく変換できない
    • SFC パーサーを導入していないことが原因であるため、近日中に対応します。
  • mapXXX の Object 記法の対応
    • 私が見た中ではこれの利用ケースがほぼ無いため実装から省いています
    • 今後実装予定自体はありますが、私自身が目にすることがないケースのため、モチベーションのある方は PR いただけると幸いです
  • store 内の型定義の反映
    • これは Nuxt.js に限定するなどの場合はストアの構造が割れているため簡単ですが、プロジェクトによってディレクトリ構成が不明なため省いています
    • 今後オプションで型定義を渡すなどで解決される可能性はあります
  • mapState の対応
    • mapState を利用することをやめましょう

上記以外にもなにか要望などあれば、Issue にお願いいたします。

https://github.com/potato4d/vuex-map-purge/issues

おわりに

今回はコードベースを楽に改善したいモチベーションが半分、型定義が十分な AST を触るツールとしての TypeScript Compiler API を利用してみたかったというのが半分でのツール作成となりました。

Vue 3 と合わせて利用可能となる Vuex 4 では、 Vuex.Store<T>any ハードコーディングが修正され、 store の型定義を正しく引き回すことができるようになります。

Vue 3 のコードベースにおいてどの程度 Vuex が利用されるかは未知数ですが、 依然として Vue 2.x からのマイグレーションでは、切っても切り離せない重要な役割になるのではないでしょうか。

そんな中、 TypeScript と親和性の低い map ヘルパーは常に課題として残り続けます。

早期に課題を解決するためにも、 vuex-map-purge が役に立てば幸いです。

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

Resolve error: No valid exports main found for /node_modules/colorette の解決法

Vue.js書いていてコミットしようとしたら、 vue-cli-service lint で怒られた。

✖ vue-cli-service lint found some errors. Please fix them and try committing again.
Error resolving webpackConfig Error: No valid exports main found for '/path/to/project/node_modules/colorette'

なんのこっちゃ・・・と思って調べたら、Node.jsのバージョンが古いのが問題らしい。
v13.5.0 でした。

別PCで作ったプロジェクトをクローンしたときに、差異が発生したのだろうか。

nvmが入っていたので、この記事を参考にバージョン上げ。

nvm ls-remote

で最新バージョンを確認。

nvm install v14.7.0

で最新バージョンをインストール。

プロジェクト内の node_modules を削除し、改めてパッケージをインストール。

npm install

無事、コミットできるようになりました:v:

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

【Nuxt】SSGとSPAの『nuxt generate』やアプリケーションの挙動の違いについて調べてみる

こちらの記事に移行しました。

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

【ただの自己満】VueCLIで使いやすいCSS画像アニメーション31選

スクリーンショット 2020-08-10 16.07.33.png

こちらの記事に記載のデザイン・コードは全てオリジナルなので自由に使っていただいて大丈夫です(筆者が作成したため)

Web制作で使えるかは保証できません。自己判断で修正するなりして参考程度に使ってください。


Webデザイナーに興味のある方はこちらも合わせてどうぞ↓
スクリーンショット 2020-08-08 16.41.27.png


【回転×サイズ拡大/縮小】CSS画像アニメーション3選『解説付』

動きは下の画像のような感じになります

1. hoverで回転×拡大する画像アニメーション

image-animation-size-rotate1 (1).png

2. hover前後でサイズが拡大・縮小×回転する画像アニメーション

image-animation-size-rotate2 (1).png

3. 回転×拡大でギャラリーのように一覧表示される画像アニメーション

image-animation-size-rotate3 (1).png

:point_down:コードを確認する

スクリーンショット 2020-08-10 15.50.19.png

【rotate×scale】画像が回転・拡大するCSSアニメーション3選(解説あり)

動きは下の画像のような感じになります

1. rotate×box-shadowでリアルすぎる画像回転アニメーション

images-rotate-animation-3picks1 (1).png

2. rotate×scale(拡大)で失敗しない画像回転アニメーション

images-rotate-animation-3picks2 (1).png

3. rotate×scale(拡大)×filterで想像を上回る画像回転アニメーション

images-rotate-animation-3picks3 (1).png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.13.11.png

【transform rotate(360deg)×flexbox】画像が回転+伸縮するCSSアニメーション3選

動きは下の画像のような感じになります

1. 【縦回転】transform rotateX(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation1-228x300 (1).png

2. 【横回転】transform rotateY(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation2-265x300 (1).png

3. 【平面回転】transform rotateZ(360deg)×flexbox画像アニメーション

transform-rotate-flexbox-animation3-275x300 (1).png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.16.43.png

【徹底解説】flexを応用して画像が伸縮するCSSアニメーション3選

動きは下の画像のような感じになります

1. flexで2つの画像が伸縮する画像アニメーション

flex-bigger-images-animation1-265x300 (1).png

2. flexでhoverした画像が100%拡大する画像アニメーション

flex-bigger-images-animation2-266x300 (1).png

3. filterでエフェクト+flexで3つの画像が伸縮する画像アニメーション

flex-bigger-images-animation3-300x249 (1).png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.20.11.png

【flexboxのjustify−content:center4選】CSS画像アニメーションを画面中央に配置

動きは下の画像のような感じになります

1. 半分の背景が左右から現れるCSS画像アニメーションをflexboxのjustify−content:centerで中央配置

flexbox-justify−content-animation-image1-300x74 (1).png

2. 背景が両斜めから現れるCSS画像アニメーションをflexboxのjustify−content:centerで中央配置

flexbox-justify−content-animation-image2-300x76 (1).png

3. 背景が上下から現れる自動ドアのようなCSS画像アニメーションをflexboxのjustify−content:centerで中央配置

flexbox-justify−content-animation-image3-300x77 (1).png

4. 半分の背景が上下から現れるCSS画像アニメーションをflexboxのjustify−content:centerで中央配置

flexbox-justify−content-animation-image4-300x77 (1).png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.22.35.png

【Web制作に最適】背景を重ねるCSS画像アニメーション4選

動きは下の画像のような感じになります

1. 半分の背景が左右から現れるCSS画像アニメーション

4picks-images-background-animation1-300x298.png

2. 背景が両斜めから現れるCSS画像アニメーション

4picks-images-background-animation2-287x300.png

3. 背景が上下から現れる自動ドアのようなCSS画像アニメーション

4picks-images-background-animation3-282x300.png

4. 半分の背景が上下から現れるCSS画像アニメーション

4picks-images-background-animation4-300x293.png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.26.30.png

【絶対使える!】hoverで背景がなめらかに動くCSS画像アニメーション4選

動きは下の画像のような感じになります

1. 右端から2つの背景が現れて文字が浮き出るCSS画像アニメーション

four-good-image-animation1-271x300.png

2. 右端から背景がスーッと表示されるCSS画像アニメーション

four-good-image-animation2-264x300.png

3. 下から2つの背景がボンっボンと画像全体を覆うCSS画像アニメーション

four-good-image-animation3-273x300.png

4. 下から背景がスーッと現れるCSS画像アニメーション

four-good-image-animation4-300x298.png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.29.02.png

マウスオーバーで画像背景が切り替わるCSS【trasition】アニメーション 4選

動きは下の画像のような感じになります

1. マウスオーバーで二つの背景が左端から現れる画像transitionアニメーション

hover-image-background-change1-279x300.png

2. マウスオーバーで背景が左端から現れるシンプルな画像transitionアニメーション

hover-image-background-change2-268x300.png

3. マウスオーバーで2つの背景が上部から落ちてくる画像transitionアニメーション

hover-image-background-change3-262x300.png

4. マウスオーバーで背景が上部から落ちてくるシンプルな画像transitionアニメーション

hover-image-background-change4-295x300.png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.32.42.png

transitionでCSS画像背景アニメーション3選(画像を横並びに配置)

動きは下の画像のような感じになります

1. transitionの速度調整で画像背景が階段のように現れるアニメーション

transition-animation-images-background1-280x300.png

2. transitionの速度調整で画像背景が上から落ちて現れるアニメーション

transition-animation-images-background2-260x300.png

3. 画像背景がクルクルと手裏剣のように表示されるアニメーション

transition-animation-images-background3-282x300.png

:point_down:コードを確認する

スクリーンショット 2020-07-31 0.37.22.png

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

配列内にあるオブジェクトを取り出して、スタイル要素に適用する。

やること

配列の中にあるオブジェクトの色要素を取り出して、表示させる文字の色にオブジェクトの色要素を割り当てるプログラムです。

ソースコード

全体のソースコードは以下の通りになります。(headタグやvueの読み込み箇所は省きます。)

HTMLファイルのソースコード

        <div id="app">
            <ul>
                <li v-for="(addColor,index) in colors">
                    <span :style="colors[index]">
                        {{addColor.color}}
                    </span>
                </li>
            </ul>
        </div>

jsファイルのソースコード

var app = new Vue({
    el: "#app",
    data: {
        colors: [
            {
                color: "red",
            },
            {
                color: "blue",
            },
            {
                color: "green",
            },
        ],
    },
});

ソースコードの解説

コードを書く順番で見ていきます。

色データの作成

data: {
        colors: [
            {
                color: "red",
            },
            {
                color: "blue",
            },
            {
                color: "green",
            },
        ],
    },

配列の中に3つのオブジェクトがある状態です。
具体的に説明するとdataの中にcolorsという名前の配列があり、その配列の中のオブジェクトにcolorというキーの名前と文字列が入っています。

配列の中身を一つずつ取り出して、表示させている。(反復処理、リストレンダリング)

<!-- (オブジェクト,インデックス番号) -->
<li v-for="(addColor,index) in colors"></li>

colors配列の中から、オブジェクトとインデックス番号を引数として取り出しています。

オブジェクト内にある文字列を取り出して、スタイルで色を割り当てる。

<span :style="colors[index]">
    {{addColor.color}} <!-- オブジェクト内になる文字列を表示 -->
</span>

vuestyle属性の操作を行うため、v-bindをつけます。
そのstyle属性の中で、配列の中身をインデックス番号で指定することによって、指定した先にあるオブジェクトを取り出しています。その結果、表示させている文字色に合わせて色のスタイルが割り当てらるようになります。
Qiita 2回目.JPG

最後に

基礎のv-ifやv-forだけでも、簡単なことから複雑なことまでたくさんのことができるので、これからどんどん挑戦していきたいと思います。

最後まで読んでいただきありがとうございました。
この記事が少しでもあなたのお役に立てば幸いです。

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

AWS Amplify & Nuxt 使って爆速でWebアプリ作る

AWS でWebアプリ作る機会があったので、色々調べるとAmplifyが便利そうだったので使ってみました。

今回作成するアプリの主な機能は以下:

  • NuxtベースのTODOアプリ
  • Lambda使ったGraphQLのAPI
  • DynamoDBへのデータ保存/読込
  • DynamoDBのデータとリアルタイム同期
  • Cognitoによる認証機能

基本的には公式チュートリアルをなぞってますが、Nuxtで使うに当たりいくつか変更点があるので、Nuxt使いに役立てば幸いです。

Amplifyの初期設定

まずAmplifyのCLIをインストールしましょう。

$ npm install -g @aws-amplify/cli

インストールしたらamplifyコマンドが使えるようになってるはずです。早速configureで設定をしてみます。

$ amplify configure
Scanning for plugins...
Plugin scan successful
Follow these steps to set up access to your AWS account:

Sign in to your AWS administrator account:
https://console.aws.amazon.com/
Press Enter to continue

上記のようなログが流れて、AWSへのサインインが求められるので、サインインしてください。その次にまたコンソール戻りましょう。

戻ったら、regionの設定です。東京ならap-northeast-Xを選べば良いはず。user nameはデフォルトでもいいですが、わかりやすくamplify-adminとしてみました。

Specify the AWS Region
? region:  ap-northeast-1
Specify the username of the new IAM user:
? user name:  amplify-admin
Complete the user creation using the AWS console
https://console.aws.amazon.com/iam/home?region=undefined#/users$new?step=final&accessKey&userNames=amplify-admin&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess
Press Enter to continue

ここでAWS Consoleがブラウザで立ち上がり、ユーザー追加が求められます。基本的に全部デフォルトでOKです。

image.png

このままで次の画面進みます。

image.png

これもこのままで次に進みます。

image.png

最終的に以下の画面が表示されます。このページはまだ閉じないでください。

image.png

コンソール画面に戻ります。

戻ったら、成功画面に表示されてる値を元にキーを入力してってください。キーは流出しないように注意して管理すること。Profile NameはなんでもOKです。今回はdefaultにしました。

Enter the access key of the newly created user:
? accessKeyId:  # 成功画面に表示されてるアクセスキーID
? secretAccessKey:  # 成功画面に表示されてるシークレットアクセスキー
This would update/create the AWS Profile in your local machine
? Profile Name:  default

Successfully set up the new user.

Nuxtアプリを用意する

Nuxtをcreate-nuxt-app使ってベースとなるアプリを用意します。アプリを作りたいディレクトリで以下のコマンド実行します。各オプションはお好みで。

$ npx create-nuxt-app nuxt-amplify

create-nuxt-app v3.2.0
✨  Generating Nuxt.js project in nuxt-amplify
? Project name: nuxt-amplify
? Programming language: JavaScript 
? Package manager: Npm
? UI framework: Vuetify.js
? Nuxt.js modules: Axios
? Linting tools: ESLint, Prettier    
? Testing framework: Jest
? Rendering mode: Single Page App    
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)

完了したら起動して動作するか試してみましょう。

$ npm run dev

(省略)

i Waiting for file changes                                                                                                                              
i Memory usage: 415 MB (RSS: 516 MB)                                                                                                                            
i Listening on: http://localhost:3000/      

http://localhost:3000/ にアクセスして下の画面が見えたらOKです。

image.png

ついでに、次以降の作業のためにgenerateコマンドを使ってファイルを生成しておきます。

$ npm run generate

distディレクトリが作られて中身が入ってればOKです。

Amplifyのバックエンドを作成する

Nuxtプロジェクトのルートディレクトリで以下のコマンドを打ち、設定を進めます。

$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project nuxtamplify
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  .
? Distribution Directory Path: dist
? Build Command:  npm.cmd run-script generate
? Start Command: npm.cmd run-script start

そうすると、以下の内容が更新されます。

  • /amplifyディレクトリが作成される。このディレクトリにはバックエンドの定義をコードベースで実装していき、Infrastructure as Codeを実現できる。
  • aws-exports.jsが作成されてバックエンドの情報が保存される。
  • .gitignoreが更新されていくつかのファイルが指定される。
  • AWS Console上にプロジェクトが作られる。 ???

Amplifyのフロント用ライブラリをインストールする

続いて、Nuxt側から使うフロント用のAmplifyライブラリをnpmからインストールします。

$ npm install aws-amplify @aws-amplify/ui-vue

そしてNuxt側で/pluginsディレクトリにamplify.jsファイルを作成し、プラグインとしてインストールしたライブラリを読み込みます。

amplify.js
import Vue from 'vue'
import Amplify from 'aws-amplify'
import '@aws-amplify/ui-vue'
import awsExports from '../aws-exports'

Amplify.configure(awsExports)
Vue.use(Amplify)

作成したプラグインを読み込むようnuxt.config.jsplugins配列で指定します。

nuxt.config.js
// ...
  plugins: [{ src: '~/plugins/amplify.js', ssr: false }],
// ...

この状態でnpm run devしてビルドに失敗しなければ問題ないはずです。

GraphQL APIを作りデプロイする

続いて、API作っていきます。AmplifyではREST APIとGraphQL APIのどちらかが作れます。今回はGraphQLを使ってみましょう。

$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: todoapi
? Choose the default authorization type for the API API key
? Enter a description for the API key: todo
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? No

以上で、Graph QLのスキーマも自動で生成されます。ファイル内容を見てみましょう。

/amplify/backend/api/todoapi/schema.graphql
type Todo @model {
  id: ID!
  name: String!
  description: String
}

idnamedescriptionというデータが格納できるスキーマのモデルが作られてます。@modelと指定されていますがこれはGraph QL transformライブラリが用意するディレクティブです。これがあることにより、テーブルとCRUD機能が用意されます。つまり、APIの実装自体はもうほぼ完成だったりします。

なので、GraphQL APIをデプロイしましょう。

$ amplify push

Current Environment: dev

| Category | Resource name | Operation | Provider plugin   |
| -------- | ------------- | --------- | ----------------- |
| Api      | todoapi       | Create    | awscloudformation |
? Are you sure you want to continue? Yes

? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src\graphql\**\*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2

これでAPIが作成されました。作成されたかを確認してみましょう。

$ amplify status

Current Environment: dev

| Category | Resource name | Operation | Provider plugin   |
| -------- | ------------- | --------- | ----------------- |
| Api      | todoapi       | No Change | awscloudformation |

GraphQL endpoint: https://######################.appsync-api.ap-northeast-1.amazonaws.com/graphql
GraphQL API KEY: ######################

そしてAWS Console上からも確認してみましょう。

$ amplify console

これで以下画面が開きます。何も表示されなかったらリージョンがオハイオとかになってる可能性あるので、右上のメニューから東京にしてください。

image.png

これクリックしてbackendを選択して以下の画面でView in AppSyncを選択します。

image.png

フロントエンドとAPIを接続する

フロントのNuxtアプリからAPIを叩くようにページを用意しましょう。

/pages/index.vue
<template>
  <div>
    <h1>TodoApp</h1>
    <v-text-field v-model="name" label="Name"></v-text-field>
    <v-text-field v-model="description" label="Description"></v-text-field>
    <v-btn @click="createTodo">Create</v-btn>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.name }} : {{ todo.description }}
      </li>
    </ul>
  </div>
</template>

<script>
import { API } from 'aws-amplify'
import { createTodo } from '~/src/graphql/mutations'
import { listTodos } from '~/src/graphql/queries'

export default {
  data() {
    return {
      name: '',
      description: '',
      todos: [],
    }
  },
  async created() {
    await this.getTodos()
  },
  methods: {
    async createTodo() {
      const { name, description } = this
      if (!name || !description) return false
      const todo = { name, description }
      await API.graphql({
        query: createTodo,
        variables: { input: todo },
      })
      this.name = ''
      this.description = ''
      this.getTodos()
    },
    async getTodos() {
      const todos = await API.graphql({
        query: listTodos,
      })
      this.todos = todos.data.listTodos.items
    },
  },
}
</script>

このコードだと、初期のTODO取得と新規追加ができます。ですが、AmplifyのGraphQLはsubscriptionという機能を使ってリアルタイムでのデータ更新を実現できます。これはAPI作成時に自動で生成されているので、特に設定は不要で読み込む設定させすればできちゃいます。リアルタイムで同期するので、createTodoの末尾にあるthis.getTodos()は要らなくなります。

/pages/index.vue
<template>
  <div>
    <h1>TodoApp</h1>
    <v-text-field v-model="name" label="Name"></v-text-field>
    <v-text-field v-model="description" label="Description"></v-text-field>
    <v-btn @click="createTodo">Create</v-btn>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.name }} : {{ todo.description }}
      </li>
    </ul>
  </div>
</template>

<script>
import { API } from 'aws-amplify'
import { createTodo } from '~/src/graphql/mutations'
import { listTodos } from '~/src/graphql/queries'
import { onCreateTodo } from '~/src/graphql/subscriptions'

export default {
  data() {
    return {
      name: '',
      description: '',
      todos: [],
    }
  },
  created() {
    this.getTodos()
    this.subscribe()
  },
  methods: {
    async createTodo() {
      const { name, description } = this
      if (!name || !description) return false
      const todo = { name, description }
      await API.graphql({
        query: createTodo,
        variables: { input: todo },
      })
      this.name = ''
      this.description = ''
    },
    async getTodos() {
      const todos = await API.graphql({
        query: listTodos,
      })
      this.todos = todos.data.listTodos.items
    },
    subscribe() {
      API.graphql({ query: onCreateTodo }).subscribe({
        next: (eventData) => {
          const todo = eventData.value.data.onCreateTodo
          if (this.todos.some((item) => item.name === todo.name)) return // remove duplications
          this.todos = [...this.todos, todo]
        },
      })
    },
  },
}
</script>

認証機能を追加する

認証もAmplifyで追加できます。認証はAWSのCognitoを利用します。

$ amplify add auth
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. # 以下の設定は設定後の変更不可
 How do you want users to be able to sign in? Email
 Do you want to configure advanced settings? No, I am done.

これだけで準備完了です。後はサービスをデプロイするためにはpushしてあげる必要があります。

$ amplify push
? Are you sure you want to continue? Y

これでCognitoのセットアップが完了します。次はAmplifyが用意してくれてるVueコンポーネントを差し込んでみましょう。

<template>
  <amplify-authenticator>
    <h1>TodoApp</h1>
    <v-text-field v-model="name" label="Name"></v-text-field>
    <v-text-field v-model="description" label="Description"></v-text-field>
    <v-btn @click="createTodo">Create</v-btn>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.name }} : {{ todo.description }}
      </li>
    </ul>
    <amplify-sign-out></amplify-sign-out>
  </amplify-authenticator>
</template>

<amplify-authenticator>コンポーネントの子ノードは認証が通った後に表示されるようになります。<amplify-sign-out>コンポーネントは、いわゆるサインアウトボタンを表示させるだけです。スタイルはドキュメント見た限りカスタマイズできそう。以下はデフォルトの見た目です。英語です。

image.png

サインインすると、子ノードの内容が表示されます。

image.png

認証がこれだけで出来ちゃいます。もちろんCognito側でのアカウント管理もできます。サインインについては提供されてるコンポーネントの他にライブラリ側に用意されたAPIを利用して独自の処理を追加できます。例えば、アカウント情報登録時に追加の情報を登録するなども実現できそうです。

アプリをホスティング環境にデプロイする

AmplifyのCLI使ってデプロイするとS3上でホスティングしてくれるようになります。早速設定していきましょう。

$ amplify add hosting
? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
? Choose a type Manual deployment

これでもう設定OKです。ではホスティング環境にデプロイしてみましょう。

$ amplify publish

Current Environment: dev

| Category | Resource name       | Operation | Provider plugin   |
| -------- | ------------------- | --------- | ----------------- |
| Hosting  | amplifyhosting      | Create    | awscloudformation |
| Api      | todoapi             | No Change | awscloudformation |
| Auth     | nuxtamplify2271a807 | No Change | awscloudformation |
? Are you sure you want to continue? Yes

...

✔ Deployment complete!
https://dev.XXXXXXXXXXX.amplifyapp.com

このURLクリックしてもらうともうアプリに触れると思います。

次のステップ

わずか数時間でシンプルなアプリがもう実現できたかと思います。
冒頭の繰り返しになりますが、Amplifyでは以下がAmplifyの機能を使って簡単に実現できるので、ドキュメントは英語中心になってますが色々調べながら次のステップを踏みアプリを効率よく開発していきましょう!

  • Authentication
  • DataStore
  • User File Storage
  • Serverless APIs
  • Analytics
  • AI/ML
  • Push Notification
  • PubSub
  • AR/VR

https://docs.amplify.aws/start/getting-started/nextsteps/q/integration/vue

トラブルシューティング

ビルド時にLinterで怒られる

Prettierの設定で怒られている可能性があります。.prettierrcファイルの差し替えてみてください。

.prettierrc
{
  "tabWidth": 2,
  "arrowParens": "always",
  "trailingComma": "es5",
  "printWidth": 80,
  "semi": false,
  "singleQuote": true,
  "bracketSpacing": true,
  "useTabs": false,
  "endOfLine": "auto
}

あるいは、VSCodeの拡張機能であるVeturを入れてる場合は、VeturのHTMLデフォルトフォーマッターがprettyhtmlになっている可能性があります。ワークスペースの設定からフォーマッターをprettierに変更してください。

image.png

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

初学者がVue→Vuexの橋を渡ってみた

0.Vuex

logo.png
Vueを学習しているとVuexというものに出会う。「なんとなく難しそう...」ってので避けてたけど、学習してみることに。

初学者の自分にとって多少目新しい概念だったので、一旦記事にして整理しようと思います。

概念ベースでまとめたので文字が多めです。

1.まずVueの復習

Vuexに入る前にVueについて軽く復習したい。

Vueのメインの特徴といえば、"コンポーネント"の概念。

機能毎に部品に区切り、それを1つの.vueファイルとして扱う。

フッターのコンポーネント、サイドバーのコンポーネント、もっと細かくいくとボタンのコンポーネントなんてのも定義できる。

vueimages.001.jpeg

さらにこれらのコンポーネントはプロジェクト内で繰り返し使えるので非常に便利な機能となる。

このように便利な機能をもつVueだが、アプリケーションが大規模になってくるとちょっとした問題が出てくる。

2.Vueの弱点:コンポーネントが増えた時にどうなる?

Vueアプリケーションが大規模になってくると「異なるコンポーネントで別の状態を管理したい」と言った状況になることがしばしば発生する。

具体例以下のような状況が考えられる。

ECサイトの構築を行なっていて、ECサイト内のカートの実装を考えているものとする。ここで、カート内の商品の個数が変化する時はどういう時が考えられるだろうか??

・「カートから削除」を押す
・「購入決定」を押す

上記の様な状況が考えられる。この2つの状況を同じコンポーネント内で管理するのは難しいかと思われる。なのでコンポーネントを分けることになるだろう。そうするとコンポーネント間でのデータのやり取りが必要になる。

ただコンポーネント間のデータのやり取りを増やす事はあまり得策ではない。

・単純にコンポーネントへの記述書が増える
・子⇄親の双方向の受け渡しを迫られがち(結果、コードの可読性が下がる)

じゃあどうする??

3.ようやくVuexの出番

上記の問題を解決するのが、まさに「Vuex」。
つまり、コンポーネント毎にデータのやりとりを行うのではなく、プロジェクト内に各コンポーネントで共通に使うデータの置き場所を1箇所定め、各コンポーネントはそのデータの置き場を参照する。
vueimages.002.jpeg

こうすることでいちいち親子で値の受け渡しをすることもなく、データの源泉がより鮮明になる。

この共通データの置き場の概念をVuexではstoreと呼ぶ。

4.Vuexがどのように解決してくれるか

具体的にどのようにしてVuexがデータの管理や変更を行うのかVuexの代表的な概念である「state」「getter」「mutation」を説明しながらVuexの挙動を見ていきたいと思う。

4-1.state

Vuexにおけるデータの置き場。Vueにおける"data"に近い概念。
ただVueの"data"と違ってVuexの"state"はどのコンポーネントからもアクセスできる。

4-2.getter

stateを参照して、stateの値をちょろっと書き換えたものを出力したい時などに使用する。Vueにおける"computed"に近い概念。
ただし、getterはstateの値を書き換えることはできない。(重要)

4-3.mutation

stateを変更、更新する際に用いる。Vueにて、メソッドを定義してdataの値を変更する操作に近いイメージ。
Vuexにおいて、stateの値を変更するのは原則としてこのmutationしか行わない。

--

こう見るとVuexとVueって結構似てますね。図にすると下見ないなイメージでしょうか。

vueimages.003.jpeg


storeの値を書き換えるには、mutationにコミットするしかないの、不便じゃね??

って最初直感的に思った。でもどうやらそうでもないっぽい。

それは、制約を外してどこからでもstateを変更できるようにすると、後々の開発で「どこからstateが変更されたか」を追うのが大変になるから。

プログラムの世界では「ある機能に一定の制約をあえて設けることで、その機能の役割を明確化させる」みたいな仕組みにたまーに遭遇するけど、今回もその1例かなと。

この辺は実際に大規模アプリの開発とかに携わったりして経験を積まないとなかなか見えないところなのかもしれない。

まとめ:Vuexをうまく組み込んでベストな設計を築こう。

概念はなんとなく掴めたけど、結局使いこなせなければ意味がない。

次はVuexを使った具体的な設計パターンを学んでいって、より効率的な開発をVue.jsで行えるようにしていきたいと思う。

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

Vue.jsで広告を埋め込んだコンポーネントを作る

はじめに

エンジニア歴4年目に突入したハヤシと申します。(インフラ:2年,WEB:1年)
現在、前勤めていた会社の先輩と一緒に「Pokeloop」というアプリを作っており、そろそろ広告埋め込みたいなぁーと思ってVueで広告コンポーネントをつくってみました。google adsense等、大手の広告をvueに埋め込むやり方は結構出てくるけど、あまり有名じゃないところだと埋め込む方法が出てこなかったので備忘録的に残したいと思います!

ちなみに「pokeloop」はパーティー相性表をはじめとするポケモン対戦における便利なツールを提供しているサイトです!UI等かなりこだわってますので、ぜひ一度訪れてください!
https://pokeloop.com/

開発環境

vue: 2.6.10

扱う広告の種類

今回はjsを埋め込むと、自動的にDOMが生成されるタイプの広告を扱います。それ以外ではこのやり方ではうまく行かない場合があるかもしれません。

■動作確認広告
1.忍者AdMax
2.アスタ

作り方

自動的にDOMが生成されるタイプの広告をvueで扱うためには、iframeという他のサイトを埋め込めるhtmlのタグを使用して、広告を埋め込みます。単純にjsを埋め込むだけだとダメでした。

OKな例

iframeタグを作成して、その中にjsタグを埋め込む形にしています。

ads.vue
<template>
 <div ref="ads" class="ad"></div>
</template>

<script>
export default {
  async mounted() {
    const iframe = document.createElement('iframe');
    const head = document.getElementsByTagName('head')[0];
    this.$refs.ads.appendChild(iframe);
    const html = '<body><script src="https://cdn.com/somescript.js"><\/script><\/body>';
    const iframeDocument = iframe.contentWindow.document;
    iframeDocument.open();
    iframeDocument.write(html);
    iframeDocument.close();
  }
}
</script>

ダメな例

これではうまくいきません。多分document.writeでDOMを作ってる部分が動かないためです。

ads.vue
<template>
 <div ref="ads" class="ad"></div>
</template>

<script>
export default {
  async mounted() {
    // スクリプトタグを生成
    let scriptEl = document.createElement('script');
   // スクリプトタグにjsをセットする
    scriptEl.setAttribute('src', 'https://cdn.com/somescript.js');
    // this.$refsを使い、DOMに埋め込む
    this.$refs.ads.appendChild(scriptEl);
  }
}
</script>

<style lang="scss" scoped>
</style>

終わりに

いかがでしたでしょうか。これで広告コンポーネントを作成することができると思います。

なにか記事の内容に不備があればご指摘お願いします。

見ていただきありがとうございました!

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

Vue.jsでwasm-bindgen(Rust)の入出力を扱う

昨日、一昨日もwasm-bindgenのJsValueについて書いてたが、Vue.jsと組み合わせて簡単に入出力をWebAssemblyとやりとりするところまで試せた。

まず、下記の記事を参考にwasm-pack build --target webでpkgディレクトリにwasmファイルができたところまで確認する。

https://qiita.com/jp_ibis/items/3205b4799cb567f8ebf5

ここで生成されたpkgディレクトリにJSONファイルを置く。

monster.json
{
    "monsters":[
        {"name":"ピカチュウ","value":10},
        {"name":"イーブイ","value":8}
    ]
}

参考記事のlib.rsを、こんな感じに書き換えて、into_serde()でMonsters型に取り込み、最もvalueが大きいモンスターの値をMonster型で出力する。

lib.rs
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
pub struct Monster {
    name: String,
    value: usize
}

impl Monster{    
    fn new() -> Self {
        Monster {name: String::new(), value: 0}
    }
    pub fn set_monster(&mut self, name: String, value: usize){
        self.name = name;
        self.value = value;
    }
}

#[derive(Serialize, Deserialize)]
pub struct Monsters {
    monsters: Vec<Monster>
}

#[wasm_bindgen]
pub fn return_max_monster(val: &JsValue) -> JsValue {
    let monsters: Monsters = val.into_serde().unwrap();
    let mut max_monster = Monster::new();
    for monster in monsters.monsters {
        let val: usize = monster.value;
        if val > max_monster.value {
            max_monster.set_monster(monster.name,monster.value);
        }
    }
    JsValue::from_serde(&max_monster).unwrap()
}

続けてVueでwasmとの入出力を書く。htmlではボタンのみ用意する。

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>hello-wasm example</title>
  </head>
  <body>
    <div id="app">
      <input v-on:click="returnMaxMonster" type="button" value="Max">
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="monster.js" type="module"></script>
</body>
</html>

monster.jsで(プロジェクト名).jsをimportする。(自分の場合はcargo new --lib wasmで作ったプロジェクトをちびちび書き足してるのでwasm.js)

async/awaitでwasmのモジュールを初期化してからnew Vue()して、lib.rsで書いたreturn_max_monster()を呼び出す。

monster.js
import * as mod from "./wasm.js";
(async () => {
    await mod.default();
    var app = new Vue({
      el: '#app',
      data: {
        monsters: null,
      },
      mounted: function(){
        axios.get("./monster.json").then(
            response => ( this.monsters = response.data )
        );
      },
      methods:{
        returnMaxMonster: function(){
          let s = mod.return_max_monster(this.monsters);
          alert(s.name);
        }
      }
    })
})();

ここまで書けたら、サーバを立ち上げて画面上のボタンをクリックしたらJSONがwasmで処理され、最も値が大きいモンスターの名前が表示される。

スクリーンショット 2020-08-10 12.06.56.png

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

CypressでComponentテスト

Cypressでunit-testをやるメリット

vue-test-utilsはbrowser APIをスタブ化して実行するが、Cypress Vue Unit Testは本物のブラウザーで実行される

インスト-ル

npm i -D cypress cypress-vue-unit-test cypress-nuxt

https://www.cypress.io/
https://github.com/bahmutov/cypress-vue-unit-test
https://github.com/NickBolles/cypress-nuxt

設定

cypress.json
{
  "$schema": "https://raw.githubusercontent.com/cypress-io/cypress/develop/cli/schema/cypress.schema.json",
  "pluginsFile": "./test/component/plugins/index.js",
  "experimentalComponentTesting": true,
  "componentFolder": "test/component/integration",
  "testFiles": "**/*.js"
}
text/component/plugins/index.js
const cypressNuxt = require("cypress-nuxt");
const path = require('path')

module.exports = async (on, config) => {
  return cypressNuxt
    .plugin({
      loadOptions: {
        rootDir: path.join(__dirname, '../../..')
      }
    })
    .then(function(webpackPreProcessor) {
      on('file:preprocessor', webpackPreProcessor)
      return config
    })
};

cypress-nuxt入れずに自前でやる人はこの辺りを参考に
https://github.com/bahmutov/cypress-vue-unit-test/issues/200

テストファイルの作成

index.spec.js
/// <reference types="Cypress" />

import { mountCallback } from 'cypress-vue-unit-test'
import Index from '../../../pages/index.vue'

import Vue from 'vue'
import Vuex from 'vuex'
import VueI18n from 'vue-i18n'

import en from '../../../locales/en.json'
import ja from '../../../locales/ja.json'

import { state, getters, mutations, actions } from '../../../store/todo'

import bootstrap from '../../../node_modules/bootstrap/dist/css/bootstrap.min.css'

Vue.use(Vuex)
Vue.use(VueI18n)

const i18n = new VueI18n({
  locale: 'en',
  messages: {
    en,
    ja
  }
})

const store = new Vuex.Store({
  modules: {
    todo: {
      namespaced: true,
      state,
      getters,
      actions,
      mutations
    }
  }
})
Vue.prototype.$store = store
const extensions = {
  filters: {},
  plugins: [],
  mixins: []
}

describe('Index', () => {
  const template = `
    <div v-if="flag">
      <Index />
    </div>
    `
  function data() {
    return { flag: false }
  }
  const components = { Index }
  beforeEach(
    mountCallback(
      { template, data, components },
      { extensions, i18n, cssFiles: [bootstrap] }
    )
  )

  describe('test', () => {
    beforeEach(() => {
      Cypress.vue.flag = true
    })
    afterEach(() => {
      Cypress.vue.flag = false
    })
    it('', () => {
      cy.log(Cypress.vue)
      cy.get('h1')
    })
  })
})

直接mount

コンポーネントを直接mountもできるが、テスト後にdestroyされないので、次のテスト時も残る。
Cypress.vue.$destroy() でdestroy処理は動かせるがコンポーネントはしなない

import { mount } from 'cypress-vue-unit-test'

describe('test', () => {
  beforeEach(mount(Index, { extensions, i18n, cssFiles: [bootstrap] }))

なのでmountは一回だけにしてCypress.vueでcomponentのdataになっているため直接代入してクリアもできるがdataが多いとめんどう
⇒ コンポーネントを自動的に削除してcreateからやり直してくれる何かやり方があるのでしょうか?

クリア例

    afterEach(() => {
      Cypress.vue.text = ''
      Cypress.vue.users = []
    })
    it('mount', () => {
      mount(Index, { extensions, i18n, cssFiles: [bootstrap] })
    })
    it('2', () => {
      cy.get('h1')
    })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.jsにお手軽にcssエフェクトをかけたいanimate.cssを導入したいときに読む記事

vue.jsにお手軽にcssエフェクトをかけたいときに読む記事

いろいろなアニメーションをcssで表現できるよいうになってきていますが、自分で1から作るのはめんどくさいです。
そこでanimate.cssを導入してお手軽にサイトに動きを加えて行く方法をまとめました。
エディターは好きなものを使ってください。
私はVScodeを使ってます。

vue-cliの導入

terminalでvue-cliをインストールします。

$ npm install -g @vue/cli

参照

https://cli.vuejs.org/

vue-cliでプロジェクトを作る

ターミナルでプロジェクトを作ります。名前はお好きなものを。

$ vue create プロジェクト名

プロジェクトができたらできたプロジェクトに移動して、とりあえずローカルサーバーをたたげてみる。

$ cd my-project
$ npm run serve

http://localhost:8080/にアクセスする

以下のようなページが開かれたらOKです。
image.png

参照

https://cli.vuejs.org/

animate.cssの導入

続いてanimate.cssをインストールしていきます。

$ npm install animate.css --save

続いて、main.jsでanimate.cssを呼び出せるようにしておきます。
以下のようにコードを追加してください。

main.js
import VAnimateCss from 'animate.css';
// Install Animate.css
Vue.use(VAnimateCss);

参照

https://animate.style/

animate.cssを適用する

サイトを確認すると英語で使い方が書いてあります。google翻訳で日本語にしてもいいです。
とりあえず、簡単な使い方を紹介します。
classにanimate__animatedをつけて、プラスどんな効果かを示すclass名をつけます。
例えば、バウンドする効果をつかたければ、以下のようになります。

class="animate__animated animate__bounce"

試しにVueのアイコンを動かしてみます。
App.vueのアイコン部分をにclassを追加して動くようにしていきます。

before

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

after

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

localhostで確認すると、Vueアイコンが動いているのがわかります。

abcfef7615a2f72741769298d5216c1e.gif

ただし、これだとロード時だけしか動かないので、ずっと動かしたり、タイミングをつけたりするにはもうちょっと工夫が必要です。

classにinfiniteをつけて、ずっと動くようにする

先程のclassにinfiniteを加えると、ずっと動くようになります。

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

でもできればタイミングは自分で設定したいですよね。

animate.cssの効果を微調整する

オリジナルのクラスを作る

animate.cssが提供しているクラス名ではなくて自分で設定することができます。
例えばmy_bounceというクラス名をつけたいときは以下のように自分でcssを設定します。

App.vue
<template>
  <div id="app">
    <img
      class="animate__animated my_bounce"
      alt="Vue logo"
      src="./assets/logo.png"
    />
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>
.my_bounce {
  animation: bounce;
  animation-delay: 2s;
}

animationは自分の好きなものを選ぶことができます。
animation-delayでタイミングを設定する事ができます。
他にも動く時間を決めたり、動作を何回繰り返すかなどを設定できます

:classを使ってanimate.cssを適用する方法

最後にclassごとに別々のアニメーションを設定し、それを切り替えられるようにしてみます。
Vue.jsでは:classを使えばcssを簡単に切り替えられるのでこれを応用します。
例えば、ボタンを押したときだけmy_bounceが作動するようにしてみましょう。
そのためにdataでボタンのon/offを操作するisBounceを作り、falseにしておきます。
これがtrueになることでmy_bounceが作動するようにします。

App.vue
  data() {
    return {
      isBounce: false,
    }
  }

そして、:classを使ってisBounceがtrueならmy_bounceが動作するようにコードを書きます。

App.vue
<template>
  <div id="app">
    <img
      class="animate__animated" 
      :class="{my_bounce: isBounce}"
      alt="Vue logo"
      src="./assets/logo.png"
    />
    <div>
      <button @click="isBounce = !isBounce">クリック</button>
    </div>
    <HelloWorld msg="Welcome to Your Vue.js App" />
  </div>
</template>

@click="isBounce = !isBounce"はクリックするごとにture/falseを切り替えるというコードです。
これでボタンをクリックするごとにisBounceの値が切り替わりtrueのときはVueアイコンが動きます。
ee5b943244c7ae663ae9eb9cceb6dd31.gif

まとめ

Vue.jsにanimate.cssを導入する方法を紹介しました。
Vue.jsはもともとアニメーションを設定しやすいのですが、animate.cssを使えば更に早く実装することができ、コードがわかりやすいので使って見ました。
皆さんのお役に立てば幸いです。

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

Docker × Laravel Vueをインストールしてログイン機能まで実装する

DockerでLaravel環境を構築する記事を書きましたが、
Vue以外にもReactを選択したり、フロント側はそもそもリポジトリを分けて開発する場合もあり前回の記事では紹介しきれていませんでした。

今回はLaravel環境を構築したあと、Vueのインストールまで試してみます。

Laravel UI とは

Laravelバージョン 5.8 までは、php artisan make:auth というコマンドが用意されていましたが、 Laravel 6.0 以降は Laravel UI パッケージに切り離されました。

前提

最強のLaravel開発環境をDockerを使って構築する【新編集版】

当記事は上記の記事の補足になる記事です。

Laravel環境構築

$ git clone git@github.com:ucan-lab/docker-laravel.git
$ cd docker-laravel/infrastructure
$ make create-project

http://127.0.0.1

とりあえず、Laravelの環境を構築します。

環境

  • PHP: 7.4.6
  • Laravel: 7.24.0
  • Laravel UI: 2.1.0
  • Node: 14.2.0
  • npm: 6.14.4
  • yarn: 1.22.4
  • Vue: 2.6.11

補足: npm, yarn どちらを使うのか?

正直どちらでも構わないと思います。
ただ、どちらを使うかプロジェクトで統一されている必要はあるかと思います。

今回はnpm, yarnコマンドを併記する形で進めたいと思います。

Vue プリセットのインストール

公式の手順に沿って、実行します。
私のDocker環境の場合は下記の流れになります。

$ cd infrastructure
$ docker-compose exec app bash
$ composer require laravel/ui
$ php artisan ui vue --auth
$ exit

app コンテナを抜けて web コンテナに入ります。

$ docker-compose exec web ash
$ npm install # yarn
$ npm run dev # yarn dev

http://127.0.0.1

補足: スクリーンショット

ホーム画面

ScreenShot 2020-08-10 2.48.30.png

右上にLOGIN, REGISTERのメニューが追加されています。

登録画面

ScreenShot 2020-08-10 13.52.16.png

ログイン画面

ScreenShot 2020-08-10 13.52.50.png

ログイン後のホーム画面(ダッシュボード)

ScreenShot 2020-08-10 13.52.27.png

リセットパスワード画面

ScreenShot 2020-08-10 13.53.41.png

ScreenShot 2020-08-10 13.53.54.png

ScreenShot 2020-08-10 13.53.59.png

ScreenShot 2020-08-10 13.54.23.png

ScreenShot 2020-08-10 13.55.02.png

パスワードリセットのメールはMailHogで確認しています。

補足: webコンテナのNode.js

webコンテナ内にNode(npm, yarn)が入ってますが、コンテナ内でビルドするのは非常に時間がかかってしまうので実際の開発ではMacローカルにNodeを入れて実行させるのが良いです。

補足: Nodeバージョン固定化

特定のバージョンのNode.jsでしか動かして欲しくない場合、package.jsonenginesフィールドにNode.jsのバージョンを明記しておくとyarn installnpm installした時に警告を表示してくれます。

package.json
{
    "private": true,
    "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "npm run development -- --watch",
        "watch-poll": "npm run watch -- --watch-poll",
        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js",
        "prod": "npm run production",
        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    },
    "devDependencies": {
        "axios": "^0.19",
        "bootstrap": "^4.0.0",
        "cross-env": "^7.0",
        "jquery": "^3.2",
        "laravel-mix": "^5.0.1",
        "lodash": "^4.17.13",
        "popper.js": "^1.12",
        "resolve-url-loader": "^2.3.1",
        "sass": "^1.20.1",
        "sass-loader": "^8.0.0",
        "vue": "^2.5.17",
        "vue-template-compiler": "^2.6.10"
    },
    "engines": {
        "node": "14.2.0"
    }
}

補足: Nodeバージョン自動切り替え設定ファイル

.node-versionpackage.json と同じディレクトリに配置しておくと、nodenvが自動的にNodeのバージョンを切り替えてくれて便利です。

.node-version
14.2.0

参考

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

Vuexでネストしたデータに悩まされている方に知って欲しいnormalizr

はじめに

Vuexを使用している時に、ネストしたデータを非常に扱いにくいと感じたことはないでしょうか?
今回はそういった悩みを持って開発をしている人向けにnomalizrを紹介していきたいと思います。

normalizrとは

https://github.com/paularmstrong/normalizr
↑が公式のライブラリで
下記のように記載してあります。

Motivation
Many APIs, public or not, return JSON data that has deeply nested objects. Using data in this kind of structure is often very difficult for JavaScript applications, especially those using Flux or Redux.

Solution
Normalizr is a small, but powerful utility for taking JSON with a schema definition and returning nested entities with their IDs, gathered in dictionaries.

簡単に要約すると

多くのAPIはオブジェクトが深くネストされているJSONデータを返してくる
このようなデータはFluxやReduxで非常に扱いにくい
Normalizrはスキーマ定義でJSONを取得し、ネストされたエンティティとID一覧を返す強力なものです。

具体的にはどう使う?

よくあるユーザーの投稿と投稿に対するコメントいうテーブルを考えてみましょう。

簡易テーブル構造

User
    id - integer
    name - string

posts
    id - integer
    title - string
    user_id - integer

comments
    id - integer
    comment - string
    user_id - integer
    post_id - integer

投稿一覧をAPIから取得した場合は下記のようなデータが帰ってきます。

[
  {
    id: 1,
    title: 'Post1',
    user: {
      id: 1,
      name: 'Tanaka'
    },
    comments:[{
      id: 1,
      comment: 'comment1',
      user: {
        id:2,
        mame: 'yamada'
      }
    }]
  },
  {
    id: 2,
    title: 'Post2',
    user: {
      id: 1,
      name: 'Tanaka'
    },
    comments:[{
      id: 2,
      comment: 'comment2',
      user: {
        id:2,
        mame: 'yamada'
      }
    }]
  }
]

このままだと例えばですが、コメントしたユーザーの名前をフロント側で編集ができる場合、データが正規化されてないので
全ての同じユーザーの名前を変更する必要があり、非常に辛い作業となってしまいます。

使い方

公式ドキュメントを見ればある程度使い方は書いてありますが、実際に使ってみます。

まずはインストール

npm install normalizr

storeでnomalizrを使用する

postsのstore
import { normalize, schema } from 'normalizr'

// user用のエンティティを定義
// entities.usersにuserデータが格納される。
const user = new schema.Entity('users')
// comment用のエンティティを定義
// entities.commentsにcommentデータが格納される。
const comment = new schema.Entity('comments', {
  // commentはuserデータを持っているので下記のように定義
  user,
})
// post用のエンティティを定義
// entities.postsにcommentデータが格納される。
const post = new schema.Entity('posts', {
  // postはuserデータを持っているので下記のように定義
  user,
  // postはcommentを配列で持っているので下記のように定義
  comments: [comment],
})
// APIがpostエンティティの配列を返すようなデータ構造なのでschema.Arrayにpostを入れる
// [post]と書いても良い
const postsSchema = new schema.Array(post)

exports const mutations = {
  // ActionでpostDataをAPIから取得し、setPostsに渡しているイメージ
  setPosts(state, postData) {
    // normalizeを使用して、正規化データを取得
    const posts = normalize(postData, postsSchema)
    state.entities = posts.entities
    state.result = posts.result
  },
}
// 下記のようなgetterを用意して使うようなイメージ
export const getters = {
  postGetter: (state) => state.entities.posts ?? {},
  commentGetter: (state) => state.entities.comments ?? {},
  userGetter: (state) => state.entities.users ?? {},
}

export const state = () => ({
  entities: {
  },
  result: []
})

上記のようにデータをいれてあげることで、
state.entities.postsには

{
  '1': {
    id: 1,
    title: 'Post1',
    user: 1,
    comments: [
      1
    ]
  },
  '2': {
    id: 2,
    title: 'Post2',
    user: 1,
    comments: [
      2
    ]
  }
}

state.entities.usersには

{
  '1': {
    id: 1,
    name: 'Tanaka'
  },
  '2': {
    id: 2,
    name: 'yamada'
  }
}

state.entities.commentsには

{
  '1': {
    id: 1,
    comment: 'comment1',
    post_id: 1,
    user: 2
  },
  '2': {
    id: 2,
    comment: 'comment2',
    post_id: 2,
    user: 2
  }
}

というような正規化されたデータが入り、扱いやすくなります。

最後に

今までVuexでネストされたデータの扱いに困っていた方は是非normalizrを使用してみてください。

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

通勤時間でまだそんなに消耗してるの?〜時給計測シミュレーター作った〜

前置き

タイトルは釣りです。リモートワーク関連に期待された方はすみませんorz
内容は一年前に作った動的っぽい挙動をする静的サイトをwebアプリにしたという内容です。ご了承ください。

↓前回
【客先常駐SES向け時給シミュレーターを作る(やだ・・・あたしの年収低すぎ・・・?)+転職についての雑記】
https://qiita.com/lunalice/items/c788b82ba52ad940ac9c

最近また転職について考える事があって、前回と違って今回はそれなりにエンジニア職っぽい事を経験してやれる事は増えてるはずなんですが、プロジェクト内のコミットは持ってこれないし、何かしら作れるという事をアッピルするために何かちょいちょいコミットしておきたいなぁと思って今回2回目の作成物です、1回目はまた別記事にする予定。

本題

先に作成物から。

¥Time-Is-Money$
https://re-time-is-money.herokuapp.com/

ソースコードはこちらにございます。
https://github.com/lunalice/time-is-money

いつも業務で使ってるのはRuby on Railsなんですが、前回転職の際pythonかrubyか迷って求人数からrubyを選択した経緯があって、でも自分の好きなように作るならpythonにしました。で、フルスタックなフレームワークよりミニマムでいいかなと思ってFlaskを選択。フロントはちょっと調べてvue-cliが楽そうなのでそうしました。業務でもvue使ってますが設定関係が凄く面倒臭い・・・

デザインセンスがないので本当どうにかしたいお気持ち表明。
deploy先は全てにおいて無料を貫くつもりだったのでherokuにしました、楽なので。
作ろうと思って本当に合間合間でしたが(休日はほぼゲームしてた)二週間くらいで形にはなってくれました。

簡単に環境をまとめると以下となります。同環境で作成する方の参考になればと思います。
- @vue/cli 4.4.6
- python-3.7.8
- heroku
- pipenv
- その他ライブラリいろいろ
- 鯖代無料で作る!

前回について補足

前回作成した環境は記事を見て頂いてもろた方がいいんですが、こんな環境で作成してました。

  • 客先常駐
  • 外部インターネットが使えない。
  • Windows(旧版)
  • 作業自体は楽だったり待ちの時間が多い環境
  • 開発環境が無い

そんな中出来上がったソースコードはこれ

長すぎるので見なくてもよきソースコード

html
| <!DOCTYPE html> |
|:--|
| <html lang="ja"> |
| <meta charset="Shift_JIS"> |
| |
| <head> |
| <title>時給シミュレーター</title> |
| <link rel="stylesheet" type="text/css" href="./css/main.css"> |
| </head> |
| |
| <body> |
| <!-- タイトル --> |
| <h1><center>時給シミュレーター</center></h1> |
| <center> |
| <table> |
| <form id="inputbox"> |
| <tr> |
| <th>年齢</th> |
| <td><input type="text" required placeholder="何歳?" id="input_nen"/></td> |
| <td class="soe">歳</td> |
| </tr> |
| <tr> |
| <th>年収</th> |
| <td><input type="text" required placeholder="5000兆円?" id="input_money"/></td> |
| <td class="soe">万円</td> |
| </tr> |
| <tr> |
| <th>休日(年)</th> |
| <td><input type="text" required placeholder="何日?" id="input_rest"/></td> |
| <td class="soe">日</td> |
| </tr> |
| <tr> |
| <th>勤務時間(日)</th> |
| <td><input type="text" required placeholder="何時間?" id="input_dotime"/></td> |
| <td class="soe">時間</td> |
| </tr> |
| <tr> |
| <th>残業時間(年)</th> |
| <td><input type="text" required placeholder="何時間?" id="input_overtime"/></td> |
| <td class="soe">時間</td> |
| </tr> |
| <tr> |
| <th>通勤時間(片)</th> |
| <td><input type="text" required placeholder="何分?" id="input_rostime"/></td> |
| <td class="soe">分</td> |
| </tr> |
| <tr> |
| <th>家賃(月)</th> |
| <td><input type="text" required placeholder="何円?" id="input_rosmoney"/></td> |
| <td class="soe">円</td> |
| </tr> |
| </form> |
| </table> |
| <input type="submit" value="計算" id="zikko" onclick="simulate();return false;"/> |
| </center> |
| <!-- 結果を挿入するようにする --> |
| <center><div id="result"><div></center> |
| <center><table id="data_list"></table></center> |
| |
| <script type="text/javascript"> |
| // 位置取得用定数 |
| var str_path = location.pathname; |
| var str_array = str_path.split("/"); |
| var input_list_path = str_path.replace(str_array.pop(),"").substring(1).replace("/","\/") + "data\/inputdata.txt"; // スクリプト実行フォルダの取得 |
| |
| // オープン時、画面を描写する:完 |
| window.onload=function(){ |
| readRecord(); |
| } |
| |
| // 時給計算したり損失金額を考えたり:完 |
| function simulate(){ |
| var input_nen = document.getElementById("input_nen").value; // 年齢 |
| var input_money = document.getElementById("input_money").value; // 年収 |
| var input_rest = document.getElementById("input_rest").value; // 休日 |
| var input_dotime = document.getElementById("input_dotime").value; // 勤務時間 |
| var input_overtime = document.getElementById("input_overtime").value; // 残業時間 |
| var input_rostime = document.getElementById("input_rostime").value; // 通勤時間 |
| var input_rosmoney = document.getElementById("input_rosmoney").value; // 家賃 |
| simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney); |
| insertRecord(); //挿入処理 |
| } |
| |
| // 出力:完 |
| function simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney){ |
| var str_edit = ""; |
| var result = exRound(keisan(input_money,input_rest,input_dotime,input_overtime),1); // 時給 |
| str_edit = " " + input_nen + "歳さんの時給は <font color=\"red\">" + result + "</font> 円です。</br>"; |
| str_edit = str_edit + "日給は <font color=\"red\">" + exRound((input_money * 10000) / activeDays(input_rest),1) + "</font> 円(稼働 <font color=\"red\">" + exRound((Number(input_dotime) + Number(input_overtime / activeDays(input_rest))),2) + "</font> 時間)です。</br>"; |
| var result_ros = exRound(ros_keisan(result,input_rostime,input_rest),1); // 年間損失金額 |
| str_edit = str_edit + "通勤時間は年間 <font color=\"red\">" + exRound(rosTime(input_rostime,input_rest),1); |
| str_edit = str_edit + "</font> 時間で <font color=\"red\">" + result_ros + "</font> 円消費しています。</br>"; |
| var yachin = input_rosmoney * 12; //家賃 |
| str_edit = str_edit + "家賃(<font color=\"red\">" + yachin + "</font>円)合わせ年間 <font color=\"red\">" + exRound(Number(result_ros) + yachin,1) + "</font> 円消費しています。</br>"; |
| str_edit = str_edit + "年間収入は (<font color=\"red\">" + input_money*10000 + "</font>円 - <font color=\"red\">" + exRound(Number(result_ros) + yachin,1) + "</font>円) = <font color=\"red\">" + (input_money*10000 - exRound(Number(result_ros) + yachin,1)) + "</font>(残業分<font color=\"red\">" + (input_overtime * result) + "</font>)円です!</br></br>"; |
| str_edit = str_edit + "節約ヒント:家賃を上げて通勤時間を下げてみよう!</br>"; |
| str_edit = str_edit + "※時間のみ想定している為、光熱費・食費等は考慮しておりません。</br>"; |
| document.getElementById("result").innerHTML = str_edit; |
| } |
| |
| // 指定した桁数まで四捨五入する:完 |
| function exRound(input_number,input_keta){ |
| return Math.round(input_number*Math.pow(10,input_keta))/Math.pow(10,input_keta); |
| } |
| |
| // 時給計算:完 |
| function keisan(input_money,input_rest,input_dotime,input_overtime){ |
| var activeday = activeDays(input_rest); // 勤務日数 |
| var day_money = (input_money * 10000) / activeday; // 日給 |
| var day_time = Number(input_dotime) + (input_overtime / activeday); // 1日の稼働 |
| return ( day_money / day_time ); // 時給 |
| } |
| |
| // 勤務日数:完 |
| function activeDays(rest){ |
| return (365-rest); |
| } |
| |
| // 年間損失金額計算:完 |
| function ros_keisan(result,input_rostime,input_rest){ |
| return (result * rosTime(input_rostime,input_rest)); |
| } |
| |
| // 年間通勤時間:完 |
| function rosTime(input_rostime,input_rest){ |
| return (input_rostime * 2 / 60) * (365 - input_rest); |
| } |
| |
| // linkの挿入機能:完 |
| function insertRecord(){ |
| // テキストに追記する。 |
| var fs = new ActiveXObject("Scripting.FileSystemObject"); |
| // 1:読み込み |
| var file_path = fs.openTextFile(input_list_path,1,false,0); |
| var string_array = file_path.AtEndOfStream ? "" : file_path.ReadAll(); // 全て読み込み |
| string_array = string_array.split(","); // 配列格納 |
| file_path.close(); |
| var createID = string_array.length != 0 ? 1 + (string_array.length - 1) / 8 : 1; // ユニークIDを振る |
| var file_path = fs.openTextFile(input_list_path,8,false,0); // 8は追記。 |
| var str_record = createID + "," + document.getElementById("input_nen").value + "," + document.getElementById("input_money").value + "," + document.getElementById("input_rest").value + ","; |
| str_record = str_record + document.getElementById("input_dotime").value + "," + document.getElementById("input_overtime").value + "," + document.getElementById("input_rostime").value + ","; |
| str_record = str_record + document.getElementById("input_rosmoney").value + ","; // 家賃 |
| file_path.write(str_record); |
| file_path.close(); |
| readRecord() // 画面更新 |
| } |
| |
| // linkの削除機能:完 |
| function deleteRecord(){ |
| // テキストを最初に取得しておく。 |
| var fs = new ActiveXObject("Scripting.FileSystemObject"); |
| // 1:読み込み |
| var file_path = fs.openTextFile(input_list_path,1,false,0); |
| var string_array = file_path.ReadAll(); // 全て読み込み |
| file_path.close(); |
| var array_buff = string_array.split(","); |
| // data_listから要素を取得する。 |
| var data_list = document.getElementById("data_list"); |
| for (var i = 1;i<data_list.rows.length;i++){ |
| // チェックボックス付けたものを削除する。 |
| if (data_list.rows(i).getElementsByTagName("input")[0].checked == true){ |
| data_list.rows(i).getElementsByTagName("input")[0].checked = false; |
| // 横ループ |
| var str_buff = ""; |
| for(var j = 0;j<data_list.rows[i].cells.length;j++){ |
| str_buff = str_buff + data_list.rows[i].cells[j].innerText + ","; |
| } |
| alert(str_buff + "を削除します。"); |
| string_array = string_array.replace(str_buff, ""); // 削除処理 |
| i = i - 1; // 位置調整 |
| } |
| } |
| // テキスト書き込み |
| file_path = fs.openTextFile(input_list_path,2,false,0); |
| file_path.write(string_array); |
| file_path.close(); |
| // 画面描写更新 |
| readRecord(); |
| } |
| |
| // linkの読み込み機能:完 |
| function readRecord(){ |
| // 描写リセット |
| document.getElementById("data_list").innerHTML=""; |
| var fs = new ActiveXObject("Scripting.FileSystemObject"); |
| // 1:読み込み |
| var file_path = fs.openTextFile(input_list_path,1,false,0); |
| var string_array = file_path.AtEndOfStream ? "" : file_path.ReadAll(); // 全て読み込み |
| var string_array = string_array.split(","); // 配列格納 |
| // 項目作成 |
| var tr = document.createElement("tr"); |
| tr.innerHTML = "<tr><th></th><th>ID</th><th>年齢</th><th>年収</th><th>休日</th><th>勤務時間</th><th>残業時間</th><th>通勤時間</th><th>家賃</th></tr>"; // ヘッダー |
| document.getElementById("data_list").appendChild(tr); |
| // html描写処理 |
| for (var i=0;i<string_array.length-1;i=i+8){ |
| var tr = document.createElement("tr"); |
| tr.innerHTML = "<input name=\"selectTarget\" type=\"radio\" onChange=\"checkAdd();\"/>"; |
| // テーブル結合 |
| for (var j=0;j<8;j++){ |
| tr.innerHTML = tr.innerHTML + "<td>" + string_array[i+j] + "</td>"; |
| } |
| tr.innerHTML = tr.innerHTML + "<input type=\"submit\" value=\"削除\" onclick=\"deleteRecord();\"/>"; |
| document.getElementById("data_list").appendChild(tr); |
| } |
| file_path.close(); |
| } |
| |
| // checkbox処理:完 |
| function checkAdd(){ |
| // data_listから要素を取得する。 |
| var data_list = document.getElementById("data_list"); |
| for (var i = 1;i<data_list.rows.length;i++){ |
| // チェックボックス付けたものをキャッチする。 |
| if (data_list.rows(i).getElementsByTagName("input")[0].checked == true){ |
| // 横ループ |
| for(var j = 0;j<data_list.rows[i].cells.length;j++){ |
| switch(j){ |
| case 1: |
| var input_nen = data_list.rows[i].cells[j].innerHTML; // 年齢 |
| break; |
| case 2: |
| var input_money = data_list.rows[i].cells[j].innerHTML; // 年収 |
| break; |
| case 3: |
| var input_rest = data_list.rows[i].cells[j].innerHTML; // 休日 |
| break; |
| case 4: |
| var input_dotime = data_list.rows[i].cells[j].innerHTML; // 勤務時間 |
| break; |
| case 5: |
| var input_overtime = data_list.rows[i].cells[j].innerHTML; // 残業時間 |
| break; |
| case 6: |
| var input_rostime = data_list.rows[i].cells[j].innerHTML; // 通勤時間 |
| break; |
| case 7: |
| var input_rosmoney = data_list.rows[i].cells[j].innerHTML; // 家賃 |
| break; |
| } |
| } |
| } |
| } |
| simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney); //表示 |
| } |
| |
| </script> |
| <footer> |
| <center><p>Copyright (C)&nbsp; 2018 時給シミュレーター 辛い人</p></center> |
| </footer> |
| </body> |
| </html> |

当時の自分に言いたい、きったないし色々間違ってる!
まぁ調べ物も出来ないし確かメモ帳かサクラエディタで書いてたのでフォーマッターとかない状況でよく書いたと思います。ライブラリも何もない状態でhtmlだけでDBのCRAD再現してるの頭おかしいと思います。

これをwebアプリにreplaceする感じで作成していきます。
で、一から説明も長くなるし先人が大量に情報を残してある状態なので、参考リンクを張りつつ、詰まった部分やポイントと思ったとこ、大変だった所だけ記載していこうかと思います。


【参考】
FlaskとVue.jsでSPA Webアプリ開発
https://qiita.com/y-tsutsu/items/67f71fc8430a199a3efd

Vue.js + FlaskでWebアプリケーション制作 - herokuにデプロイするまで -
https://qiita.com/Nonta0605/items/5d8fa9a8eda9b3b7bc33

Vue.js(vue-cli)とFlaskを使って簡易アプリを作成する【前半 - フロントエンド編】
https://qiita.com/mitch0807/items/2a93d93adbf6b5fc445c


詰まった所

まず先に結論から申し上げると以下を使えばおこらなかった可能性が高いので、もし同環境で作りたい方は使用をおすすめ致します。
https://github.com/gtalarico/flask-vuejs-template

旧仕様から現仕様の置き換えが大変だった

前回作ったものをベースにしたのですが、javascriptに見えてWScriptというものだと思います、確か。
普通にvueにぺたっと貼り付けても使えないのとそもそもファイルの読み書きでCRADを表現してましたが全然いらない処理ですし、今回は文明の力、フォーマッター入れててめっちゃエラーはいてくるし実はアイデアだけ持ってきて1から記載しました。今回から得られた教訓は現行踏襲って大変だよね。

Vue-cliは最新のものを使う

凄く初歩的な話ですが、参考記事そのままの環境ではダメです。
このアプリを作り直す際、気軽な気分で始めたのもあるんですが、frontendは進化の早い分野ですので、バージョンの違いが挙動を著しくバグらせます。

自分がやらかしたのはvue-cli 3.0.0(現在4?)で作り始めて、vue-cli触った事なかったのもあって知らずに、やたらとwebpackがエラーふくなぁこんなもんかなぁとpackage.jsonのupdateを始めてしまいました(業務でやったので・・・苦行やん・・・)いやいや、開発楽にする為のvue-cliなのにおかしいやろと現実に帰ってバージョンあげたら一発で通りました、気をつけます。

エラーの理由としてはクリティカルのものとしてvue-cli3の依存関係にあるライブラリがセキュリティ的にアウトで使えなくてバージョンあげないと行けなくて芋づる式にいろいろバージョンをあげないと行けなくなっていました、苦行。

vue.config.jsonをしっかり見る

vue-cliの大元と言ってもいいんですが、ここの記載によりアウトプットがガラッと変わります。
自分が困ったのはFlaskと連携するindex.htmlがFaviconを認識してくれない所が始まりでした。

この関連で困ったのはservice-worker・favicon・manifestだったと思います。

vue.config.json
module.exports = {
  outputDir: "dist",
  assetsDir: "static",
  pages: {
    index: {
      entry: "src/main.js",
      title: "TimeIsMoney",
    },
  },
  pwa: {
    name: "time-is-money",
    manifestPath: "static/manifest.json",
    manifestOptions: {
      icons: [
        {
          src: "img/icons/android-chrome-192x192.png",
          sizes: "192x192",
          type: "image/png",
        },
        {
          src: "img/icons/android-chrome-512x512.png",
          sizes: "512x512",
          type: "image/png",
        },
      ],
    },
    outputDir: "static",
    iconPaths: {
      favicon: "static/favicon.ico",
      favicon32: "static/favicon.ico",
      favicon16: "static/favicon.ico",
      appleTouchIcon: "static/img/icons/apple-touch-icon-152x152.png",
      maskIcon: "static/img/icons/safari-pinned-tab.svg",
      msTileImage: "static/img/icons/msapplication-icon-144x144.png",
    },
    workboxPluginMode: "GenerateSW",
    workboxOptions: {
      swDest: "static/service-worker.js",
      // ...other Workbox options...
      importsDirectory: "static",
    },
  },
};

当初はほぼ何も書かずに生成しようとしていました。浅はかなり・・・
まず勘違いしやすいポイントですがiconPathsとmanifestOptions.iconsは別ものだという事が一つ。

一番やられたのが自分の設定だとFlaskがdist/staticの中身しか参照しないのですが、初期設定だとdist内にservice-worker/manifest/faviconもろもろが生成されるので、Flask立ち上げからのlocalhostで確認してもずっとfavicon変わらないし変なエラーおきてるしvue-cliもFlaskも経験値なさすぎで全然わからんわ・・・ってつまりました。冷静になって生成物を確認するとurlとかなんかいろいろおかしいなってなり気付きました。無記載だとデフォルト設定されるものが多いと思うのでその辺りの確認が必要ですね

SQLAlchemyが変なエラーを吐く

これは自分がpythonを使い慣れてないせいもあるんですが、readしただけなのにやたらとエラー吐くなぁと思いpython使い的には常識なのかしら・・・以下記事を参考にとりあえずエラーは減少しました。

[Python]SQLAlchemyのエラー回避備忘録
https://qiita.com/yukiB/items/67336716b242df3be350

herokuにdeployしたらエラー起きる問題

まずherokuの使用としてpackage.jsonがローカルにある想定で動きます。
ので今回のディレクトリ構成だと以下の感じで動くわけもなく・・・

  • application
    • frontend
      • package.json
      • .gitignore
    • backend
      • flask_application
    • .gitignore
    • .env

いろいろ記事を参考にしてpackage.jsonをローカルに移し.gitignoreからdistを消してdeployとか個人的にちょっとうーん・・・と思う内容が多くて、流石にherokuさん毎回生成物コミットしてpushしないといけないとかherokuとvue-cli素人の自分でも頭悪いとおm(自主規制)

解決策としては二つやる事があり、まずはシンボリックリンク

ln -s 「シンボリックリンク元のパス」 「シンボリックリンクを作成する場所のパス」

注意としてはこれはデプロイするので相対パスで作成する事。
deploy先で/user/hogehoge/application/frontend/package.jsonとか存在しませんからね!これで少しやらかしました。恥ずかしい。

次にheroku特有の設定。

"heroku-postbuild": "cd frontend; npm install; npm run build"

heroku-postbuildをpackage.jsonのscriptに記入すると優先して読んでくれます。でとりあえずの解決策として無理やりfrontendに移動してbuildしてます。スマートじゃないと思うので詳しい方は助言ください。

スマートじゃないと思っているのはなぜかというと、多分したみたいなフォルダ構成になっちゃってる。いや確認してないけど多分きっとそう。

  • application
    • node_module
    • frontend
      • dist
      • node_module
      • package.json
      • .gitignore
    • backend
      • flask_application
    • .gitignore
    • package.json
    • .env

herokuの環境変数どうやって設定する??

散々フォルダ構成を提示してきましたが、センシティブなデータは.envに記載してdeployしないように。frontもbackendもライブラリを使って読み込むはずです。しかしこのフォルダ構成。vueの環境変数はfrontendの中に存在するし、一定の行動しかしないherokuにどうやって読み込ませたらいい??問題発生、flaskは普通にos.getenvで読めるのにどうして・・・って思いましたが、結局herokuの設定で解決。

heroku buildpacks:add heroku/nodejs
heroku config:set VUE_APP_SENSITIVE=yarn

まずherokuにbuildpacksを追加します。その後にVUE_APP_をつけて環境変数を設定。
vue-cli3くらいからの仕様のようで環境変数にVUE_APPが必要になってます。あとはbuildpackがよしなに環境変数を読み取ってくれますえらい。

あとがき

いろいろ細かい事を書けばあるんですが、大きく困ったのは記載した通り(忘れてるのもあります)。まだちっこうバグいっぱいあるんですが、形になったのでよし。結局触った事ないpython、vue-cli、herokuに振り回されただけだった気もしますが、楽しめました。

今度はVPSかAWSと迷って実務よりなAWSかつRailsでちゃんとしたプロダクト作ろうかなぁと思ってる最中です。簡単なアプリで合間合間で二週間とかかるとガチものプロダクトだと数ヶ月かかりそう(しかもアイデアはあるけどソースにはない)状態なのでやっぱり時間が全てだなぁと思ったり。自分は休日はずっとゲームしたい派なので回らない・・・

最初に書きましたが、じわじわ転職を考えてるので拾ってくれる企業様はtwitterとかでDM頂けたらなと
https://twitter.com/yamashitaP21

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

通勤時間でまだそんなに消耗してるの?〜無料でWEBアプリ(時給計測シミュレーター)作った〜

前置き

タイトルは釣りです。リモートワーク関連に期待された方はすみませんorz
内容は一年前に作った動的っぽい挙動をする静的サイトをwebアプリにしたという内容です。ご了承ください。

↓前回
【客先常駐SES向け時給シミュレーターを作る(やだ・・・あたしの年収低すぎ・・・?)+転職についての雑記】
https://qiita.com/lunalice/items/c788b82ba52ad940ac9c

最近また転職について考える事があって、前回と違って今回はそれなりにエンジニア職っぽい事を経験してやれる事は増えてるはずなんですが、プロジェクト内のコミットは持ってこれないし、何かしら作れるという事をアッピルするために何かちょいちょいコミットしておきたいなぁと思って今回2回目の作成物です、1回目はまた別記事にする予定。

本題

先に作成物から。

¥Time-Is-Money$
https://re-time-is-money.herokuapp.com/

ソースコードはこちらにございます。
https://github.com/lunalice/time-is-money

いつも業務で使ってるのはRuby on Railsなんですが、前回転職の際pythonかrubyか迷って求人数からrubyを選択した経緯があって、でも自分の好きなように作るならpythonにしました。で、フルスタックなフレームワークよりミニマムでいいかなと思ってFlaskを選択。フロントはちょっと調べてvue-cliが楽そうなのでそうしました。業務でもvue使ってますが設定関係が凄く面倒臭い・・・

デザインセンスがないので本当どうにかしたいお気持ち表明。
deploy先は全てにおいて無料を貫くつもりだったのでherokuにしました、楽なので。
作ろうと思って本当に合間合間でしたが(休日はほぼゲームしてた)二週間くらいで形にはなってくれました。

簡単に環境をまとめると以下となります。同環境で作成する方の参考になればと思います。
- @vue/cli 4.4.6
- python-3.7.8
- heroku
- pipenv
- その他ライブラリいろいろ
- 鯖代無料で作る!

前回について補足

前回作成した環境は記事を見て頂いてもろた方がいいんですが、こんな環境で作成してました。

  • 客先常駐
  • 外部インターネットが使えない。
  • Windows(旧版)
  • 作業自体は楽だったり待ちの時間が多い環境
  • 開発環境が無い

そんな中出来上がったソースコードはこれ

長すぎるので見なくてもよきソースコード
| <!DOCTYPE html> |
|:--|
| <html lang="ja"> |
| <meta charset="Shift_JIS"> |
|  |
| <head> |
| <title>時給シミュレーター</title> |
| <link rel="stylesheet" type="text/css" href="./css/main.css"> |
| </head> |
|  |
| <body> |
| <!-- タイトル --> |
| <h1><center>時給シミュレーター</center></h1> |
| <center> |
| <table> |
|  <form id="inputbox"> |
|   <tr> |
|    <th>年齢</th> |
|    <td><input type="text" required placeholder="何歳?" id="input_nen"/></td> |
|    <td class="soe"></td> |
|   </tr> |
|   <tr> |
|    <th>年収</th> |
|    <td><input type="text" required placeholder="5000兆円?" id="input_money"/></td> |
|    <td class="soe">万円</td> |
|   </tr> |
|   <tr> |
|    <th>休日(年)</th> |
|    <td><input type="text" required placeholder="何日?" id="input_rest"/></td> |
|    <td class="soe"></td> |
|   </tr> |
|   <tr> |
|    <th>勤務時間(日)</th> |
|    <td><input type="text" required placeholder="何時間?" id="input_dotime"/></td> |
|    <td class="soe">時間</td> |
|   </tr> |
|   <tr> |
|    <th>残業時間(年)</th> |
|    <td><input type="text" required placeholder="何時間?" id="input_overtime"/></td> |
|    <td class="soe">時間</td> |
|   </tr> |
|   <tr> |
|    <th>通勤時間(片)</th> |
|    <td><input type="text" required placeholder="何分?" id="input_rostime"/></td> |
|    <td class="soe"></td> |
|   </tr> |
|   <tr> |
|    <th>家賃(月)</th> |
|    <td><input type="text" required placeholder="何円?" id="input_rosmoney"/></td> |
|    <td class="soe"></td> |
|   </tr> |
|  </form> |
| </table> |
| <input type="submit" value="計算" id="zikko" onclick="simulate();return false;"/> |
| </center> |
| <!-- 結果を挿入するようにする --> |
| <center><div id="result"><div></center> |
| <center><table id="data_list"></table></center> |
|  |
| <script type="text/javascript"> |
| // 位置取得用定数 |
| var str_path = location.pathname; |
| var str_array = str_path.split("/"); |
| var input_list_path = str_path.replace(str_array.pop(),"").substring(1).replace("/","\/") + "data\/inputdata.txt"; // スクリプト実行フォルダの取得 |
|  |
| // オープン時、画面を描写する:完 |
| window.onload=function(){ |
|  readRecord(); |
| } |
|  |
| // 時給計算したり損失金額を考えたり:完 |
| function simulate(){ |
|  var input_nen = document.getElementById("input_nen").value; // 年齢 |
|  var input_money = document.getElementById("input_money").value; // 年収 |
|  var input_rest = document.getElementById("input_rest").value; // 休日 |
|  var input_dotime = document.getElementById("input_dotime").value; // 勤務時間 |
|  var input_overtime = document.getElementById("input_overtime").value; // 残業時間 |
|  var input_rostime = document.getElementById("input_rostime").value; // 通勤時間 |
|  var input_rosmoney = document.getElementById("input_rosmoney").value; // 家賃 |
|  simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney); |
|  insertRecord(); //挿入処理 |
| } |
|  |
| // 出力:完 |
| function simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney){ |
|  var str_edit = ""; |
|  var result = exRound(keisan(input_money,input_rest,input_dotime,input_overtime),1); // 時給 |
|  str_edit = " " + input_nen + "歳さんの時給は <font color=\"red\">" + result + "</font> 円です。</br>"; |
|  str_edit = str_edit + "日給は <font color=\"red\">" + exRound((input_money * 10000) / activeDays(input_rest),1) + "</font> 円(稼働 <font color=\"red\">" + exRound((Number(input_dotime) + Number(input_overtime / activeDays(input_rest))),2) + "</font> 時間)です。</br>"; |
|  var result_ros = exRound(ros_keisan(result,input_rostime,input_rest),1); // 年間損失金額 |
|  str_edit = str_edit + "通勤時間は年間 <font color=\"red\">" + exRound(rosTime(input_rostime,input_rest),1); |
|  str_edit = str_edit + "</font> 時間で <font color=\"red\">" + result_ros + "</font> 円消費しています。</br>"; |
|  var yachin = input_rosmoney * 12; //家賃 |
|  str_edit = str_edit + "家賃(<font color=\"red\">" + yachin + "</font>円)合わせ年間 <font color=\"red\">" + exRound(Number(result_ros) + yachin,1)  + "</font> 円消費しています。</br>"; |
|  str_edit = str_edit + "年間収入は (<font color=\"red\">" + input_money*10000 + "</font>円 - <font color=\"red\">" + exRound(Number(result_ros) + yachin,1) + "</font>円) = <font color=\"red\">" + (input_money*10000 - exRound(Number(result_ros) + yachin,1)) + "</font>(残業分<font color=\"red\">" + (input_overtime * result) + "</font>)円です!</br></br>"; |
|  str_edit = str_edit + "節約ヒント:家賃を上げて通勤時間を下げてみよう!</br>"; |
|  str_edit = str_edit + "※時間のみ想定している為、光熱費・食費等は考慮しておりません。</br>"; |
|  document.getElementById("result").innerHTML = str_edit; |
| } |
|  |
| // 指定した桁数まで四捨五入する:完 |
| function exRound(input_number,input_keta){ |
|  return Math.round(input_number*Math.pow(10,input_keta))/Math.pow(10,input_keta); |
| } |
|  |
| // 時給計算:完 |
| function keisan(input_money,input_rest,input_dotime,input_overtime){ |
|  var activeday = activeDays(input_rest); // 勤務日数 |
|  var day_money = (input_money * 10000) / activeday; // 日給 |
|  var day_time = Number(input_dotime) + (input_overtime / activeday); // 1日の稼働 |
|  return ( day_money / day_time ); // 時給 |
| } |
|  |
| // 勤務日数:完 |
| function activeDays(rest){ |
|  return (365-rest); |
| } |
|  |
| // 年間損失金額計算:完 |
| function ros_keisan(result,input_rostime,input_rest){ |
|  return (result * rosTime(input_rostime,input_rest)); |
| } |
|  |
| // 年間通勤時間:完 |
| function rosTime(input_rostime,input_rest){ |
|  return (input_rostime * 2 / 60) * (365 - input_rest); |
| } |
|  |
| // linkの挿入機能:完 |
| function insertRecord(){ |
|  // テキストに追記する。 |
|  var fs = new ActiveXObject("Scripting.FileSystemObject"); |
|  // 1:読み込み |
|  var file_path = fs.openTextFile(input_list_path,1,false,0); |
|  var string_array = file_path.AtEndOfStream ? "" : file_path.ReadAll(); // 全て読み込み |
|  string_array = string_array.split(","); // 配列格納 |
|  file_path.close(); |
|  var createID = string_array.length != 0 ? 1 + (string_array.length - 1) / 8 : 1; // ユニークIDを振る |
|  var file_path = fs.openTextFile(input_list_path,8,false,0); // 8は追記。 |
|  var str_record = createID + "," + document.getElementById("input_nen").value + "," + document.getElementById("input_money").value + "," + document.getElementById("input_rest").value + ","; |
|  str_record = str_record + document.getElementById("input_dotime").value + "," + document.getElementById("input_overtime").value + "," + document.getElementById("input_rostime").value + ","; |
|  str_record = str_record + document.getElementById("input_rosmoney").value + ","; // 家賃 |
|  file_path.write(str_record); |
|  file_path.close(); |
|  readRecord() // 画面更新 |
| } |
|  |
| // linkの削除機能:完 |
| function deleteRecord(){ |
|  // テキストを最初に取得しておく。 |
|  var fs = new ActiveXObject("Scripting.FileSystemObject");  |
|  // 1:読み込み |
|  var file_path = fs.openTextFile(input_list_path,1,false,0); |
|  var string_array = file_path.ReadAll(); // 全て読み込み |
|  file_path.close(); |
|  var array_buff = string_array.split(","); |
|  // data_listから要素を取得する。 |
|  var data_list = document.getElementById("data_list"); |
|  for (var i = 1;i<data_list.rows.length;i++){ |
|   // チェックボックス付けたものを削除する。 |
|   if (data_list.rows(i).getElementsByTagName("input")[0].checked == true){ |
|    data_list.rows(i).getElementsByTagName("input")[0].checked = false; |
|    // 横ループ |
|    var str_buff = ""; |
|    for(var j = 0;j<data_list.rows[i].cells.length;j++){ |
|     str_buff = str_buff + data_list.rows[i].cells[j].innerText + ","; |
|    } |
|    alert(str_buff + "を削除します。"); |
|    string_array = string_array.replace(str_buff, ""); // 削除処理 |
|    i = i - 1; // 位置調整 |
|   } |
|  } |
|  // テキスト書き込み |
|  file_path = fs.openTextFile(input_list_path,2,false,0); |
|  file_path.write(string_array); |
|  file_path.close(); |
|  // 画面描写更新 |
|  readRecord(); |
| } |
|  |
| // linkの読み込み機能:完 |
| function readRecord(){ |
|  // 描写リセット |
|  document.getElementById("data_list").innerHTML=""; |
|  var fs = new ActiveXObject("Scripting.FileSystemObject");  |
|  // 1:読み込み |
|  var file_path = fs.openTextFile(input_list_path,1,false,0); |
|  var string_array = file_path.AtEndOfStream ? "" : file_path.ReadAll(); // 全て読み込み |
|  var string_array = string_array.split(","); // 配列格納 |
|  // 項目作成 |
|  var tr = document.createElement("tr"); |
|  tr.innerHTML = "<tr><th></th><th>ID</th><th>年齢</th><th>年収</th><th>休日</th><th>勤務時間</th><th>残業時間</th><th>通勤時間</th><th>家賃</th></tr>"; // ヘッダー |
|  document.getElementById("data_list").appendChild(tr); |
|  // html描写処理 |
|  for (var i=0;i<string_array.length-1;i=i+8){ |
|   var tr = document.createElement("tr"); |
|   tr.innerHTML = "<input name=\"selectTarget\" type=\"radio\" onChange=\"checkAdd();\"/>"; |
|   // テーブル結合 |
|   for (var j=0;j<8;j++){ |
|    tr.innerHTML = tr.innerHTML + "<td>" + string_array[i+j] + "</td>"; |
|   } |
|   tr.innerHTML = tr.innerHTML + "<input type=\"submit\" value=\"削除\" onclick=\"deleteRecord();\"/>"; |
|   document.getElementById("data_list").appendChild(tr); |
|  } |
|  file_path.close(); |
| } |
|  |
| // checkbox処理:完 |
| function checkAdd(){ |
|  // data_listから要素を取得する。 |
|  var data_list = document.getElementById("data_list"); |
|  for (var i = 1;i<data_list.rows.length;i++){ |
|   // チェックボックス付けたものをキャッチする。 |
|   if (data_list.rows(i).getElementsByTagName("input")[0].checked == true){ |
|    // 横ループ |
|    for(var j = 0;j<data_list.rows[i].cells.length;j++){ |
|     switch(j){ |
|      case 1: |
|       var input_nen = data_list.rows[i].cells[j].innerHTML; // 年齢 |
|       break; |
|      case 2: |
|       var input_money = data_list.rows[i].cells[j].innerHTML; // 年収 |
|       break; |
|      case 3: |
|       var input_rest = data_list.rows[i].cells[j].innerHTML; // 休日 |
|       break; |
|      case 4: |
|       var input_dotime = data_list.rows[i].cells[j].innerHTML; // 勤務時間 |
|       break; |
|      case 5: |
|       var input_overtime = data_list.rows[i].cells[j].innerHTML; // 残業時間 |
|       break; |
|      case 6: |
|       var input_rostime = data_list.rows[i].cells[j].innerHTML; // 通勤時間 |
|       break; |
|      case 7: |
|       var input_rosmoney = data_list.rows[i].cells[j].innerHTML; // 家賃 |
|       break; |
|     } |
|    } |
|   } |
|  } |
|  simulate_output(input_nen,input_money,input_rest,input_rest,input_dotime,input_overtime,input_rostime,input_rosmoney); //表示 |
| } |
|  |
| </script> |
| <footer> |
|  <center><p>Copyright (C)&nbsp; 2018 時給シミュレーター 辛い人</p></center> |
| </footer> |
| </body> |
| </html> |

当時の自分に言いたい、きったないし色々間違ってる!
まぁ調べ物も出来ないし確かメモ帳かサクラエディタで書いてたのでフォーマッターとかない状況でよく書いたと思います。ライブラリも何もない状態でhtmlだけでDBのCRAD再現してるの頭おかしいと思います。

これをwebアプリにreplaceする感じで作成していきます。
で、一から説明も長くなるし先人が大量に情報を残してある状態なので、参考リンクを張りつつ、詰まった部分やポイントと思ったとこ、大変だった所だけ記載していこうかと思います。


【参考】
FlaskとVue.jsでSPA Webアプリ開発
https://qiita.com/y-tsutsu/items/67f71fc8430a199a3efd

Vue.js + FlaskでWebアプリケーション制作 - herokuにデプロイするまで -
https://qiita.com/Nonta0605/items/5d8fa9a8eda9b3b7bc33

Vue.js(vue-cli)とFlaskを使って簡易アプリを作成する【前半 - フロントエンド編】
https://qiita.com/mitch0807/items/2a93d93adbf6b5fc445c


詰まった所

まず先に結論から申し上げると以下を使えばおこらなかった可能性が高いので、もし同環境で作りたい方は使用をおすすめ致します。
https://github.com/gtalarico/flask-vuejs-template

旧仕様から現仕様の置き換えが大変だった

前回作ったものをベースにしたのですが、javascriptに見えてWScriptというものだと思います、確か。
普通にvueにぺたっと貼り付けても使えないのとそもそもファイルの読み書きでCRADを表現してましたが全然いらない処理ですし、今回は文明の力、フォーマッター入れててめっちゃエラーはいてくるし実はアイデアだけ持ってきて1から記載しました。今回から得られた教訓は現行踏襲って大変だよね。

Vue-cliは最新のものを使う

凄く初歩的な話ですが、参考記事そのままの環境ではダメです。
このアプリを作り直す際、気軽な気分で始めたのもあるんですが、frontendは進化の早い分野ですので、バージョンの違いが挙動を著しくバグらせます。

自分がやらかしたのはvue-cli 3.0.0(現在4?)で作り始めて、vue-cli触った事なかったのもあって知らずに、やたらとwebpackがエラーふくなぁこんなもんかなぁとpackage.jsonのupdateを始めてしまいました(業務でやったので・・・苦行やん・・・)いやいや、開発楽にする為のvue-cliなのにおかしいやろと現実に帰ってバージョンあげたら一発で通りました、気をつけます。

エラーの理由としてはクリティカルのものとしてvue-cli3の依存関係にあるライブラリがセキュリティ的にアウトで使えなくてバージョンあげないと行けなくて芋づる式にいろいろバージョンをあげないと行けなくなっていました、苦行。

vue.config.jsonをしっかり見る

vue-cliの大元と言ってもいいんですが、ここの記載によりアウトプットがガラッと変わります。
自分が困ったのはFlaskと連携するindex.htmlがFaviconを認識してくれない所が始まりでした。

この関連で困ったのはservice-worker・favicon・manifestだったと思います。

vue.config.json
module.exports = {
  outputDir: "dist",
  assetsDir: "static",
  pages: {
    index: {
      entry: "src/main.js",
      title: "TimeIsMoney",
    },
  },
  pwa: {
    name: "time-is-money",
    manifestPath: "static/manifest.json",
    manifestOptions: {
      icons: [
        {
          src: "img/icons/android-chrome-192x192.png",
          sizes: "192x192",
          type: "image/png",
        },
        {
          src: "img/icons/android-chrome-512x512.png",
          sizes: "512x512",
          type: "image/png",
        },
      ],
    },
    outputDir: "static",
    iconPaths: {
      favicon: "static/favicon.ico",
      favicon32: "static/favicon.ico",
      favicon16: "static/favicon.ico",
      appleTouchIcon: "static/img/icons/apple-touch-icon-152x152.png",
      maskIcon: "static/img/icons/safari-pinned-tab.svg",
      msTileImage: "static/img/icons/msapplication-icon-144x144.png",
    },
    workboxPluginMode: "GenerateSW",
    workboxOptions: {
      swDest: "static/service-worker.js",
      // ...other Workbox options...
      importsDirectory: "static",
    },
  },
};

当初はほぼ何も書かずに生成しようとしていました。浅はかなり・・・
まず勘違いしやすいポイントですがiconPathsとmanifestOptions.iconsは別ものだという事が一つ。

一番やられたのが自分の設定だとFlaskがdist/staticの中身しか参照しないのですが、初期設定だとdist内にservice-worker/manifest/faviconもろもろが生成されるので、Flask立ち上げからのlocalhostで確認してもずっとfavicon変わらないし変なエラーおきてるしvue-cliもFlaskも経験値なさすぎで全然わからんわ・・・ってつまりました。冷静になって生成物を確認するとurlとかなんかいろいろおかしいなってなり気付きました。無記載だとデフォルト設定されるものが多いと思うのでその辺りの確認が必要ですね

SQLAlchemyが変なエラーを吐く

これは自分がpythonを使い慣れてないせいもあるんですが、readしただけなのにやたらとエラー吐くなぁと思いpython使い的には常識なのかしら・・・以下記事を参考にとりあえずエラーは減少しました。

[Python]SQLAlchemyのエラー回避備忘録
https://qiita.com/yukiB/items/67336716b242df3be350

herokuにdeployしたらエラー起きる問題

まずherokuの使用としてpackage.jsonがローカルにある想定で動きます。
ので今回のディレクトリ構成だと以下の感じで動くわけもなく・・・

  • application
    • frontend
      • package.json
      • .gitignore
    • backend
      • flask_application
    • .gitignore
    • .env

いろいろ記事を参考にしてpackage.jsonをローカルに移し.gitignoreからdistを消してdeployとか個人的にちょっとうーん・・・と思う内容が多くて、流石にherokuさん毎回生成物コミットしてpushしないといけないとかherokuとvue-cli素人の自分でも頭悪いとおm(自主規制)

解決策としては二つやる事があり、まずはシンボリックリンク

ln -s 「シンボリックリンク元のパス」 「シンボリックリンクを作成する場所のパス」

注意としてはこれはデプロイするので相対パスで作成する事。
deploy先で/user/hogehoge/application/frontend/package.jsonとか存在しませんからね!これで少しやらかしました。恥ずかしい。

次にheroku特有の設定。

"heroku-postbuild": "cd frontend; npm install; npm run build"

heroku-postbuildをpackage.jsonのscriptに記入すると優先して読んでくれます。でとりあえずの解決策として無理やりfrontendに移動してbuildしてます。スマートじゃないと思うので詳しい方は助言ください。

スマートじゃないと思っているのはなぜかというと、多分したみたいなフォルダ構成になっちゃってる。いや確認してないけど多分きっとそう。

  • application
    • node_module
    • frontend
      • dist
      • node_module
      • package.json
      • .gitignore
    • backend
      • flask_application
    • .gitignore
    • package.json
    • .env

herokuの環境変数どうやって設定する??

散々フォルダ構成を提示してきましたが、センシティブなデータは.envに記載してdeployしないように。frontもbackendもライブラリを使って読み込むはずです。しかしこのフォルダ構成。vueの環境変数はfrontendの中に存在するし、一定の行動しかしないherokuにどうやって読み込ませたらいい??問題発生、flaskは普通にos.getenvで読めるのにどうして・・・って思いましたが、結局herokuの設定で解決。

heroku buildpacks:add heroku/nodejs
heroku config:set VUE_APP_SENSITIVE=yarn

まずherokuにbuildpacksを追加します。その後にVUE_APP_をつけて環境変数を設定。
vue-cli3くらいからの仕様のようで環境変数にVUE_APPが必要になってます。あとはbuildpackがよしなに環境変数を読み取ってくれますえらい。

herokuをスリープしないようにする

heroku無料枠だと30分アクセスがない状態だとスリープモードに入ってユーザー的に遅くて残念な感じになってしまいます。それを回避する為に定期的にアクセスするようにしないといけません。

  • sendgridを使う
  • uptimerobotを使う
  • 自身にアクセスするタスクを組む
  • etc

調べると色々ある感じだったので自分は無料でいけて手軽なuptimerobotさんを使いました。

その他抱えてる問題

herokuのclearDBが無料枠なので5MBしか使えません。MAX10000レコードだったかな?5mbに本当に入るんかいな・・・って感じなんですが、もしアプリが動かなかったら容量バーストしてると思うので許してください、、

あとがき

いろいろ細かい事を書けばあるんですが、大きく困ったのは記載した通り(忘れてるのもあります)。まだちっこうバグいっぱいあるんですが、形になったのでよし。結局触った事ないpython、vue-cli、herokuに振り回されただけだった気もしますが、楽しめました。

今度はVPSかAWSと迷って実務よりなAWSかつRailsでちゃんとしたプロダクト作ろうかなぁと思ってる最中です。簡単なアプリで合間合間で二週間とかかるとガチものプロダクトだと数ヶ月かかりそう(しかもアイデアはあるけどソースにはない)状態なのでやっぱり時間が全てだなぁと思ったり。自分は休日はずっとゲームしたい派なので回らない・・・

最初に書きましたが、じわじわ転職を考えてるので拾ってくれる企業様はtwitterとかでDM頂けたらなと
https://twitter.com/yamashitaP21

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