Tech Hotoke Blog

IT観音とは私のことです。

ネットワーク入門【TCP/IP】

f:id:TechHotoke:20211218000524p:plain

まえがき

少しずつ更新していきます

今回はIPについて

目的

TCP/IPの概要について復習

前提

  • ITパスポート取得レベルの知識があること
  • インターネットの基本的な用語などが分かること

環境

TCP/IP

f:id:TechHotoke:20211217232451p:plain

ネットワーク層

IP(Internet Protocol)

  • TCP/IPのインターネット層で中心的な役割を担う。エンドツーエンド(データの送信元と送信先をつなぐ経路全体)の通信を実現するプロトコル
  • コネクションレス:正しく届いたかは認知しない仕組み。送ったら送りっぱなし状態。
  • ベストエフォート:保証はしないができるだけ通信が行われるような最低限の仕組みが備わっている。
  • データ回復機能はない

  • IPアドレス

    • 世界中で一意の値
    • 32ビットで構成(例:11000000.10101000.00000001.00000011)
    • ネットワーク部とホスト部に分けられる((例:11000000.10101000.|00000001.00000011))
    • IPアドレスクラス f:id:TechHotoke:20211217231819p:plain

    • グローバルIPアドレス:インターネットに接続できるアドレス

    • プライベートIPアドレス:インターネットに接続できないアドレス f:id:TechHotoke:20211217233603p:plain
  • デフォルトゲートウェイ

    • 外部のインターネットとの出入り口
    • ルータのようなL3デバイスが担う
    • ポートごとにIPアドレスを持ち、各ネットワーク内のノードがそのIPアドレスを記憶することで外部との通信を実現することが可能

ARP(Address Resolution Protocol)

f:id:TechHotoke:20211217234924p:plain

ARPリプライで返却されたMACアドレスARPテーブルという場所に一定時間保存されます。そうすることでブロードキャストを毎回行う必要がなくなります。

ARPテーブルから情報が一定時間で削除されることをARPキャッシュと呼びます。

f:id:TechHotoke:20211217235408p:plain

また、arp -aコマンドで現在のARPテーブルの中身を確認することも可能です。 f:id:TechHotoke:20211217235824p:plain

ICMP(Internet Control Message Protocol)

エラー通知や問い合わせを転送するプロトコル

ベストエフォート型のプロトコルであるIPの補助的な役割が大きい。

  • ping:特定のノードと通信を行うことができるか確認する際に用いられるコード。エコー要求(タイプ8)を発信し、対象のノードからエコー応答(タイプ0)が返ってきたら接続可能と判断する。(エコー要求を受けたら必ず何かしらの応答を返す)

traceroute <接続先ホスト名>で目標としたホストまでのネットワークの経路情報を表示することが可能です。 f:id:TechHotoke:20211218230732p:plain

トランスポート層

  • トランスポート層:通信の信頼性に関するプロトコル群(主にTCPUDP)がまとめられた層
  • ポート番号:送信元のアプリケーションと受信先のアプリケーションを特定するためのデータ。PCを一つの大きな会社だとしたらIPは住所、ポート番号は何部の誰さん宛てかを判別するもの。
  • セッションの多重化:ポート番号が存在することで、一つのIPアドレスで複数のアプリケーションと通信することができる。 f:id:TechHotoke:20211219231846p:plain
  • ウェルノウンポート:エンジニアの教養として覚えておくことをオススメします。(自分が覚えているかは別)

ウェルノウンポートの一覧

TCP(Transmission Control Protocol)

  • 正確性重視
  • コネクション型:送信元・送信先で相互に通信の状況を確認しながら接続を行う
  • データはセグメントという単位に分割される f:id:TechHotoke:20211220232612p:plain

  • 制御ビットを用いて、通信の確立を行う f:id:TechHotoke:20211220234553p:plain

  • 3wayハンドシェイク:TCPにおける接続の確立の手法

  • ACKとSYNが使われる f:id:TechHotoke:20211220234627p:plain

  • SYNのフラグが立てられ、送信。それを受けてACKフラグを立てて返却 f:id:TechHotoke:20211220235349p:plain

  • ACKフラグと一緒にSYNフラグも立てて返却。それを受けてACKフラグを立てて返却。通信の確立。 f:id:TechHotoke:20211220235538p:plain

UDP

  • 速さ重視

【Spring】SpringBootで作成したプロジェクト名を変更する

f:id:TechHotoke:20211217175419p:plain

目的

SpringBootプロジェクトの備忘録。

備忘録のため、詳細な説明を省略している部分があります。

前提

Springの基本的な知識があること。

環境

  • SpringBoot2.6.0
  • Gradle 7.1.1

プロジェクト名の変更

f:id:TechHotoke:20211217171006p:plain 該当のプロジェクトを選択し、右クリック>リファクタリング>名前の変更 を選択する。

名前を変更するとたくさんエラーが出る。

f:id:TechHotoke:20211217171136p:plain

エラー解消

.projectファイルを確認し、nameタグを変更したプロジェクト名に変更する。

f:id:TechHotoke:20211217170931p:plain

settings.gradleを確認し、rootProject.nameの右辺を変更したプロジェクト名に変更する。

rootProject.name = 'TMS' IDEを再起動して、SpringBootの実行構成に変更後のプロジェクトが反映されていることを確認する。

f:id:TechHotoke:20211217174556p:plain

GradleProjectをリフレッシュする。

起動確認して、完了です。

.projectファイルとは

プロジェクト名などのプロジェクト固有の情報を管理するファイル

settings.gradleとは

設定スクリプトでSettingsオブジェクトに対するなにがしかを行うもの。

gradle.monochromeroad.com

お付き合いいただきありがとうございます。

AWS入門【 Cloudformation 】

f:id:TechHotoke:20211217070645p:plain

目的

AWS学習の備忘録。

備忘録のため、詳細な説明を省略している部分があります。

前提

ネットワークの基本的な知識があること。

ゴール

添付画像のテンプレートを作成すること f:id:TechHotoke:20211217071735p:plain

AWS CloudFormationとは

  • AWSリソース(EC2やRDS、S3等)をテンプレート化して自動で構築できる。
  • CloudFormation自体は利用無料(利用したインスタンス等のみ料金がかかる)

    メリット

  • 検証と本番で同じテンプレートを利用することにより、構成を同じにできる。
  • AWSの構成を可視化出来る。
  • バージョン管理も出来る。

作成

コンソールからCloudFormationを検索して選択します。 f:id:TechHotoke:20211217071042p:plain

スタック*この作成を選択し、

   *テンプレートで作成されるAWSリソースの一つのまとまり

f:id:TechHotoke:20211217071341p:plain 事前に作成した、jsonファイル(テンプレート)をアップロードします。

sample-template.json

  "Parameters" : {
    "InstanceType" : {
      "Description" : "WebServer EC2 instance type",
      "Type" : "String",
      "Default" : "t2.small",
      "AllowedValues" : [ "t1.micro", "t2.nano", "t2.micro", "t2.small", "t2.medium", "t2.large", "m1.small", "m1.medium", "m1.large", "m1.xlarge", "m2.xlarge", "m2.2xlarge", "m2.4xlarge", "m3.medium", "m3.large", "m3.xlarge", "m3.2xlarge", "m4.large", "m4.xlarge", "m4.2xlarge", "m4.4xlarge", "m4.10xlarge", "c1.medium", "c1.xlarge", "c3.large", "c3.xlarge", "c3.2xlarge", "c3.4xlarge", "c3.8xlarge", "c4.large", "c4.xlarge", "c4.2xlarge", "c4.4xlarge", "c4.8xlarge", "g2.2xlarge", "g2.8xlarge", "r3.large", "r3.xlarge", "r3.2xlarge", "r3.4xlarge", "r3.8xlarge", "i2.xlarge", "i2.2xlarge", "i2.4xlarge", "i2.8xlarge", "d2.xlarge", "d2.2xlarge", "d2.4xlarge", "d2.8xlarge", "hi1.4xlarge", "hs1.8xlarge", "cr1.8xlarge", "cc2.8xlarge", "cg1.4xlarge"]
,
      "ConstraintDescription" : "must be a valid EC2 instance type."
    }
  },

  "Mappings" : {
    "AWSInstanceType2Arch" : {
      "t1.micro"    : { "Arch" : "HVM64"  },
      "t2.nano"     : { "Arch" : "HVM64"  },
      "t2.micro"    : { "Arch" : "HVM64"  },
      "t2.small"    : { "Arch" : "HVM64"  },
      "t2.medium"   : { "Arch" : "HVM64"  },
      "t2.large"    : { "Arch" : "HVM64"  },
      "m1.small"    : { "Arch" : "HVM64"  },
      "m1.medium"   : { "Arch" : "HVM64"  },
      "m1.large"    : { "Arch" : "HVM64"  },
      "m1.xlarge"   : { "Arch" : "HVM64"  },
      "m2.xlarge"   : { "Arch" : "HVM64"  },
      "m2.2xlarge"  : { "Arch" : "HVM64"  },
      "m2.4xlarge"  : { "Arch" : "HVM64"  },
      "m3.medium"   : { "Arch" : "HVM64"  },
      "m3.large"    : { "Arch" : "HVM64"  },
      "m3.xlarge"   : { "Arch" : "HVM64"  },
      "m3.2xlarge"  : { "Arch" : "HVM64"  },
      "m4.large"    : { "Arch" : "HVM64"  },
      "m4.xlarge"   : { "Arch" : "HVM64"  },
      "m4.2xlarge"  : { "Arch" : "HVM64"  },
      "m4.4xlarge"  : { "Arch" : "HVM64"  },
      "m4.10xlarge" : { "Arch" : "HVM64"  },
      "c1.medium"   : { "Arch" : "HVM64"  },
      "c1.xlarge"   : { "Arch" : "HVM64"  },
      "c3.large"    : { "Arch" : "HVM64"  },
      "c3.xlarge"   : { "Arch" : "HVM64"  },
      "c3.2xlarge"  : { "Arch" : "HVM64"  },
      "c3.4xlarge"  : { "Arch" : "HVM64"  },
"sample_template.json" [dos] 201L, 10360B

JSONチェッカーとデザイナーで表示機能などを駆使してJSONファイルを頑張って作成します。 参考:JSONチェッカー lab.syncer.jp

f:id:TechHotoke:20211217071418p:plain 適当な名前をつけて、料金の安価なt2-microを選択します。 パラメータのスペックに応じて料金が変動するので注意が必要。 スタックオプションは特に設定せず、スタックの作成を行い、

f:id:TechHotoke:20211217071856p:plain ステータスが添付画像のようになったらOK

確認のため、EC2の物理IDリンクを押下し、EC2インスタンスが作成されていることを確認してください。 f:id:TechHotoke:20211217071949p:plain

f:id:TechHotoke:20211217072100p:plain

削除

削除しないと料金が発生しますので削除までちゃんと行なってください。。。!

CloudFormationから先ほど作成したスタックを選択し、削除ボタンを押下 f:id:TechHotoke:20211217072234p:plain こちらの画面に切り替わったことを確認し、 f:id:TechHotoke:20211217072412p:plain EC2のインスタンスのステータスが終了済となっていることを確認して削除完了です。

これで終了です。

お付き合いいただきありがとうございます。

Vue×SpringでSPA作成4【Storeのmodule分割】

f:id:TechHotoke:20211216133942p:plain

まえがき

こちらの記事の続編です。

techhotoke.hatenablog.com

目的

VueとSpringで作成したプロジェクトの構築手順の備忘録。

備忘録のため、詳細な説明を省略している部分があります。

前提

基本的なJavaの知識やSpring、Vueの知識があること。

環境

  • Java 11
  • Spring Boot2.5.6
  • Gradle 7.1.1
  • Vue2.6
  • IDESTS

Storeのmodule分割

前回はVuexのstoreフォルダ直下のindex.jsにUserのログイン情報を格納していました。

こんな感じ↓

f:id:TechHotoke:20211206231442p:plain

これだと管理するstateが増えれば増えるほどindex.jsが肥大化していくので非常によろしくないです。

こういったボトルネックは初期のうちから取り除きたいですよね。

そこでVuexのstoreはモジュールに分割して管理することができるのでそちらを実践してみようと思います。

モジュール | Vuex

まずはstoreフォルダ直下にmodulesという名称のフォルダを作成し、その中にauth.jsとindex.jsを作成します。

auth.js

import axios from 'axios'

export const auth = {
    namespaced: true,

    state: {
            emailAddress: null,
            error: false
    },
    
    getters: {
      isAuthenticated: state => {
        return state.emailAddress != null;
      },
    },
    
    mutations: {
      updateId(state, emailAddress) {
        state.emailAddress = emailAddress;
      },
      resetData(state) {
        state.emailAddress = null;
        state.error = false;
      },
      updateError(state) {
        state.error = true;
      }
    },
    
    actions: {
      login({ commit }, authData) {
        axios
          .post('/login', {
            emailAddress: authData.emailAddress,
            password: authData.password,
          })
          .then(() => {
            commit('updateId', authData.emailAddress);
          })
          .catch(() => {
            commit('updateError')
          });
      },
      logout({ commit }) {
        axios.post('/logout').then(() => {
          commit('resetData');
        });
      },
    }
}     

index.js

export { default as auth } from './auth' 

index.jsは各moduleを一括でexportするファイルとして活用します。

次にstoreフォルダ直下のindex.jsは認証情報のstateを持つ必要がなくなったので下記のようになります。

import Vue from 'vue'
import Vuex from 'vuex'
import { auth } from './modules'

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    auth
  }
});

続いて、Login.vueの修正を行なっていきます。

<script>
import {mapState} from 'vuex'
...

mapStateヘルパー関数を使用していきたいのでこちらをscriptタグ内部でimportします。

 computed: mapState({
        isLogedIn: state => state.auth.emailAddress,
        isError: state => state.auth.error
    }),

Vuexの作法的にcomputedプロパティでstoreのstateの情報を取得します。

data関数などにstateを渡してしまうと参照渡しとなるため、値が間接的に書き換えられる恐れがあるためです。

mapStateはVuexが用意しているヘルパー関数で、this.$store.state.[module名]としていたところを上記のように記述できるようになります。

また、mapStateはオブジェクトを返却するため、複数Storeを渡した場合、ローカルの算術プロパティと共存することができなくなりますが、スプレッド演算子を使用することで、複数Storeを参照した場合の記述も可能のようです。

詳しくはこの辺りの記事をご参照ください。

Vuex: mapStateの使い方を理解する - Qiita

同時にwatch式も書き換えておきます。

 watch: {
        isLogedIn: function() {
            this.$router.push('/home')
        },
        isError: function() {
            this.hasError = true
        }
    },

次にrouter/indexを修正します。

router.beforeEach((to, from, next) => {
  // 非公開コンポーネントで未ログインの場合ログイン画面にリダイレクト
  console.log(Store)
  if (
    to.matched.some(
      record => (record.meta.isPublic || Store.state)
    )
  ) {
    next();
  } else {
    next({ path: "/", query: { redirect: to.fullPath } });
  }
});

最後にログアウト処理のパスをLogin.vueと同様のイメージで修正すれば完了です。

補足〜routerのStoreのstate取得〜

ちょっと困ったというか迷ったのですが、Storeオブジェクトの中身を確認してみると添付画像のようにmodulesにアクセスできないんですよねえ。。。知見のある方が閲覧してましたらご教示ください。。。

とりあえず今はStore.stateでアクセスできていますが、modulesが複数できたらどうなるのか・・・

一旦、この問題はStoreを複数使った時に問題として浮上してくるはずなのと、これから修正していく宿題としてここではこのように書いておきます。

コメントとしてTODOを残しておこう。。。個人開発の特権ということで。。。実務ではやらないでください。 VSCode拡張機能ToDoHighlightToDoTreeとか入れておくと見やすいですよ。

f:id:TechHotoke:20211209174134p:plain

今回はここまでとなります。

お付き合いいただきありがとうございます。

Vue×SpringでSPA作成5【UIをいい感じにする】

f:id:TechHotoke:20211216134028p:plain

まえがき

こちらの記事の続編です。

techhotoke.hatenablog.com

目的

VueとSpringで作成したプロジェクトの構築手順の備忘録。

備忘録のため、詳細な説明を省略している部分があります。

前提

基本的なJavaの知識やSpring、Vueの知識があること。

環境

  • Java 11
  • Spring Boot2.5.6
  • Gradle 7.1.1
  • Vue2.6
  • IDESTS

ゴール

今はUIがこんな↓感じなので、

f:id:TechHotoke:20211209180456p:plain

これをいい感じ↓にしていきます。

f:id:TechHotoke:20211215213342p:plain

画面をいい感じにする

構成的には親コンポーネントにサイドバーとヘッダを入れて、メインコンテンツを子コンポーネントとして適宜切り替えていくイメージで実装を進めてまいります。

まずは親コンポーネントとなるコンポーネント達を作成します。 frontend/src直下にlayouts/defaultという命名ディレクトリを作成します。その直下にまず親コンポーネントとなるIndex.vueを作成します。

<template>
    <v-app>
        <default-bar />
    
        <default-drawer />
    
        <default-view />
    
        <default-footer />
    </v-app>
</template>

<script>
export default {
    name: 'DefaultLayout',

    components: {
        DefaultBar: () =>
            import (
                /* webpackChunkName: "default-app-bar" */
                './AppBar'
            ),
        DefaultDrawer: () =>
            import (
                /* webpackChunkName: "default-drawer" */
                './Drawer'
            ),
        DefaultFooter: () =>
            import (
                /* webpackChunkName: "default-footer" */
                './Footer'
            ),
        DefaultView: () =>
            import (
                /* webpackChunkName: "default-view" */
                './View'
            ),
    },
}
</script>
  • AppBar:ヘッダ
  • Drawer:サイドバー
  • Footrt:フッタ
  • View:メインコンテンツ

大体こんな構成でいこうかと思います。

コンポーネントを構成している部品が多く、冗長なので今回の記事ではヘッダ、フッタ、サイドバーについては説明を割愛しますので、GitHubをご参照ください。

github.com

メインコンテンツの実装

まず、default/layoutの直下にView.vueを作成します。

<template>
  <v-main>
    <v-container fluid>
      <router-view :key="$route.path" />
    </v-container>
  </v-main>
</template>

<script>
  export default {
    name: 'DefaultView',
  }
</script>
  • <router-view>のkeyに子コンポーネントのパスを渡すことで動的にViewを切り替えていきます。

公式サイトを見ると、Vue-routerでのコンポーネントのネストの実装方法が載っていましたので、こちらを参考にしつつ、

https://router.vuejs.org/ja/guide/essentials/nested-routes.html#%E3%83%8D%E3%82%B9%E3%83%88%E3%81%95%E3%82%8C%E3%81%9F%E3%83%AB%E3%83%BC%E3%83%88

まずはlodshをインストールしていきます。(lodashを使うとフォーマットの成形が楽になるので)

Lodash

次に、routingの制御を行う汎用的なメソッドを作成していきたいので、src直下にutilという名称のフォルダを作成し、routes.jsファイルを作成します。

// Imports
import { kebabCase } from 'lodash'

export function layout (layout = 'Default', children, path = '') {
  const dir = kebabCase(layout)

  return {
    children,
    component: () => import(
      /* webpackChunkName: "layout-[request]" */
      `@/layouts/${dir}/Index`
    ),
    path,
  }
}

export function route (name, component, path = '', publishingSettings) {
  component = Object(component) === component
    ? component
    : { default: name.replace(' ', '') }

  const components = {}

  for (const [key, value] of Object.entries(component)) {
    components[key] = () => import(
      /* webpackChunkName: "views-[request]" */
      `@/views/${value}`
    )
  }
  const meta = {isPublic: publishingSettings}

  return {
    name,
    components,
    path,
    meta
  }
}

続いて、router/index.jsを修正します。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import store from '../store/index.js'
import {
  layout,
  route,
} from '@/util/routes'

Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  scrollBehavior: (to, from, savedPosition) => {
    if (to.hash) return { selector: to.hash }
    if (savedPosition) return savedPosition

    return { x: 0, y: 0 }
  },
  routes: [
    {
          path: '/',
          name: 'Login',
          component: Login,
          meta: {isPublic: true}
    },
    layout('Default', [
      route('Home', null, '/home', false)
    ])
  ]
})

router.beforeEach((to, from, next) => {
  console.log(store)
  // 非公開コンポーネントで未ログインの場合ログイン画面にリダイレクト
  if (
    to.matched.some(
      // TODO:Store.stateのアクセス方法が不明確なので今後修正を行うこと
      record => (record.meta.isPublic || store.state)
    )
  ) {
    next();
  } else {
    next({ path: "/", query: { redirect: to.fullPath } });
  }
});

export default router

公式曰く、

<router-view> コンポーネントは与えられたパスに対してマッチしたコンポーネントを描画する関数型コンポーネント

なので、Homeコンポーネントのパスをkeyに渡すことでview-routerを動的に切り替えてみようと思います。これで、子コンポーネントのあたるメインコンテンツをroutingに応じて切り替えられ(るはず)ます。

おまけ〜Vuex-pathify〜

VuexのSotreにアクセスする記述方法が手続き的で毎回記述するのも冗長だと感じていたところ、Vuex-pathifyというライブラリを発見しましたので、試しに使ってみました。

davestewart.github.io

一応公式にもこのような形でvuex-pthifyを導入することで得られるメリットなどが記載されています。

  • less cognitive overhead
  • zero store boilerplate
  • one-liner wiring
  • cleaner code
  • lighter files

先頭のcongnitive overheadが何を指しているか分かりにくいのですが、おそらくコードをぱっと見たときの感覚的に理解できる時間が短くなるよ的なことかと。。。

ネックなのは参考になる日本語の記事が見当たらなかった点くらいですかね。。。

それでは早速、 こちらをインストールしてみます。

  • インストール
npm i vuex-pathify

こちらができたら、storeのエントリポイントとなるindex.jsにimportして、

import pathify from 'vuex-pathify'

Vuexインスタンスにマウントします。

export default new Vuex.Store({
  plugins: [ pathify.plugin ], // activate plugin
});

そして、続いてstore直下にmodulesという名称のフォルダを作成して配置。 その直下にapp.jsというファイルを作成します。

// Pathify
import { make } from 'vuex-pathify'

// Data
const state = {
  drawer: null,
  drawerImage: true,
  mini: false,
  items: [
    {
      title: 'Home',
      icon: 'mdi-home-circle-outline',
      to: '/home',
    },
  ],
}

const mutations = make.mutations(state)

const actions = {
  ...make.actions(state),
  init: async ({ dispatch }) => {
    //
  },
}

const getters = {}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters,
}

  • vue-pathifyを利用するため、mutationsにmakeメソッドを代入します。

例として、今回説明を省いたDrawer.vueを下記に引用します。

<template>
  <v-navigation-drawer
    id="default-drawer"
    v-model="drawer"
    :right="$vuetify.rtl"
    :mini-variant.sync="mini"
    mini-variant-width="80"
    app
    width="260"
    color="#424242"
  >

    <div class="px-2">
      <default-drawer-header />

      <v-divider class="mx-3 mb-2" />

      <default-list :items="items" />
    </div>

    <div class="pt-12" />
  </v-navigation-drawer>
</template>

<script>
  // Utilities
  import { get, sync } from 'vuex-pathify'

  export default {
    name: 'DefaultDrawer',

    components: {
      DefaultDrawerHeader: () => import(
        /* webpackChunkName: "default-drawer-header" */
        './widgets/DrawerHeader'
      ),
      DefaultList: () => import(
        /* webpackChunkName: "default-list" */
        './List'
      ),
    },

    computed: {
      ...get('app', [
        'items',
        'version',
      ]),
      ...sync('app', [
        'drawer',
        'drawerImage',
        'mini',
      ]),
    },
  }
</script>
  • computedプロパティ内で、...get(),...sync()などを用いてStoreにアクセスすることが可能です。

個人的にはこちらの方が宣言的で好みでした。

今回はここまでとなります。

お付き合いいただきありがとうございます。

Vue×SpringでSPA作成3【ログイン機能のカスタマイズ~フロントエンド編~】

f:id:TechHotoke:20211216134054p:plain

まえがき

こちらの記事の続編です。

techhotoke.hatenablog.com

目的

VueとSpringで作成したプロジェクトの構築手順の備忘録。

備忘録のため、詳細な説明を省略している部分があります。

前提

基本的なJavaの知識やSpring、Vueの知識があること。

環境

  • Java 11
  • Spring Boot2.5.6
  • Gradle 7.1.1
  • Vue2.6
  • IDESTS

やること

  • ログイン画面の実装
  • ルーティングの実装
  • ホーム画面の実装(APIで取得したテキストを画面に表示させる)
  • ログアウト機能の実装

実装

それでは早速、実装を進めていきます。

環境構築

まずはaxiosとvuex,vuetifyをインストールします。

npm install vuex
npm install axios
npm install vuetify
npm install sass@~1.32 sass-loader deepmerge -D

git diff コマンドなどで確認してpackage.jsonにインストールしたパッケージの依存関係が追加されていればOKです。

※ Vuetify導入時にエラーが発生したので記事にまとめましたのでご参考までに。。。(フロントエンド開発は流れが特に早いので各ライブラリ間でのバージョンの互換性が取れなくなるのは当たりまえなのかなぁ・・・)

techhotoke.hatenablog.com

Vutifyの導入にあたって公式のnpmに沿ってwebpack.config.jsなどを作成しましたが、公式のまんまなので省略。 下記参考にしてください〜

vuetifyjs.com

次に、src直下のmain.jsファイルを以下のように書き換えます。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import axios from 'axios'
import vuetify from '@/plugins/vuetify'

Vue.config.productionTip = false

axios.defaults.baseURL = '/api';

// Cookieを有効にする。
axios.defaults.withCredentials = true;

Vue.prototype.$axios = axios;

Vue.use(vuetify)

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

説明

  • インストールしたパッケージたちをVueのインスタンスにマウントしてます。

  • axiosのベースとなるURLを指定しています。これでaxiosを利用する際は/apiプレフィックスとして付きます。

  • Vueのprototypeにaxiosを追加することで各componentからthis.$axiosの形でアクセスすることができます。

Storeの実装

続いて、Storeにログイン処理時のstateを登録していきます。 srcディレクトリ直下にstoreディレクトリを作成し、index.jsを以下のように記述します。

import Vue from "vue";
import Vuex from "vuex";
import axios from "axios"

Vue.use(Vuex);

const getAuthState = () => ({
  emailAddress: null
});

export default new Vuex.Store({

state: getAuthState(),

getters: {
  isAuthenticated: state => {
    return state.emailAddress != null;
  },
},
mutations: {
  updateId(state, emailAddress) {
    state.emailAddress = emailAddress;
  },
  resetData(state) {
    state.emailAddress = null;
  },
},
actions: {
  login({ commit }, authData) {
    axios
      .post('/login', {
        emailAddress: authData.emailAddress,
        password: authData.password,
      })
      .then(() => {
        commit('updateId', authData.emailAddress);
      });
  },
  logout({ commit }) {
    axios.post('/logout').then(() => {
      commit('resetData');
    });
  },
},
});

説明

  • state

stateはアプリケーションレベルの状態が全て含まれており、"信頼できる唯一の情報源 (single source of truth)" として機能します。ここでは、getAuthStateという関数に切り出しています。stateの初期値を返却する関数を作ることでstateのリセットをしやすくなるからです。

Vue.jsではオブジェクト内の一部を変更してもリアクティブに伝搬してくれないので、resetDataメソッドでstate全体を変更することで対応しています。 ちなみに、getAuthStateを関数ではなく変数にしてしまうと、コンポーネント側で色々とstateを変えたりしているとgetAuthStateの値も変わってしまいますので注意(参照渡しになるので)。

認証の判定にはemailaddressをユーザーIDとして利用する想定なので、こちらに値がセットされているか否かで判定を行なっていきます。

  • getters

VuexではgettersからStoreのstateにアクセスします。

ここでは、emailaddressがnullでなければtureを返し、nullならfalseを返すメソッドを定義しこの値に応じてcomponentの公開・非公開を制御していきます。

  • mutations

Vuex ではミューテーションをコミットすることでストアの状態を変更できるので、mutationにはstateを更新するメソッドとstateをリセットするメソッドを定義しています。

  • actions

actionはmutationに似た役割を担っていますが、公式には以下の点が異なると記載されています。

アクションは、状態を変更するのではなく、ミューテーションをコミットします。 アクションは任意の非同期処理を含むことができます。

なのでここではaxiosを使用してAPIでから取得したデータをmutationにコミットすることで間接的にstateを書き換えています。

ルーティングの実装

お次は、ルーティングの設定を行なっていきます。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Login from '../views/Login.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Login',
    component: Login,
    meta: {isPublic: true}
  },
  {
    path: '/home',
    name: 'Home',
    component: Home,
    meta: {isPublic: false}
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
    meta: {isPublic: false}
  }
]

const router = new VueRouter({
  routes,
  mode: 'history',
  base: process.env.BASE_URL,
  scrollBehavior: (to, from, savedPosition) => {
    if (to.hash) return { selector: to.hash }
    if (savedPosition) return savedPosition

    return { x: 0, y: 0 }
  },
})

router.beforeEach((to, from, next) => {
  // 非公開コンポーネントで未ログインの場合ログイン画面にリダイレクト
  if (
    to.matched.some(
      record => (record.meta.isPublic || Store.getters.isAuthenticated)
    )
  ) {
    next();
  } else {
    next({ path: "/login", query: { redirect: to.fullPath } });
  }
});

export default router

説明

  • routes変数にcomponentにアクセスするパスを記載します。
  • metaプロパティにコンポーネントの公開設定を判定するisPublicというプロパティを設定します。
  • router変数にVueRouterインスタンスを格納します。
  • historymodeのバックエンドの実装を前回の記事で行ったので、VueRouterのhistorymodeを有効にします。
  • BASE_URLはvue.config.jsに記載されているpublicpathになります。(ここではfrontend) 環境変数についてはプロジェクトルートに.envファイルなどを作成し、そこにキーバリュー形式で記載することでテスト環境本番環境ごとに変更するなどが可能のようですが今回は作成していません。下記参照。

Modes and Environment Variables | Vue CLI

  • scrollBehaviorはルーティング時に任意の位置にスクロールを行う設定が可能です。

https://router.vuejs.org/ja/guide/advanced/scroll-behavior.html#%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%81%AA%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AA%E3%83%B3%E3%82%AF%E3%82%99

  • VueRouterでは遅延ローディングルートを使用することが出来ます。大規模なアプリケーション開発になるとバンドルされるJSファイルが大きくなるため、初期ロード時は全体のJSを読み込み、ルーティング時にはチャンク分割したファイルのみ読み込むことでローディングの遅延に対処する仕組みが備わってるみたいです。

https://router.vuejs.org/ja/guide/advanced/lazy-loading.html#%E5%90%8C%E3%81%97%E3%82%99%E3%83%81%E3%83%A3%E3%83%B3%E3%82%AF%E5%86%85%E3%81%A6%E3%82%99%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%9B%E3%82%9A%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%AF%E3%82%99%E3%83%AB%E3%83%BC%E3%83%95%E3%82%9A%E5%8C%96

  • beforeEachメソッドに非公開コンポーネントに未ログイン状態でアクセスした場合にログインフォームにリダイレクトさせるように設定します。

ログイン・ログアウトの実装

次に、 Vuetifyの設定、ログイン画面、ログイン後の遷移画面、ログイン後の遷移画面から遅延ローディングの実証画面を実装します。

srcフォルダ直下にviewsという名称のフォルダを作成し、その直下に各画面を作成していきます。

Login.vue

<template>
    <div>
        <v-main>
            <v-card :tile="$vuetify.breakpoint.sm || $vuetify.breakpoint.xs" elevation="2" class="mx-auto fill-width" flat max-width="540" height="300">
                <div class="pt-6">
                    <v-card-title>ログイン</v-card-title>
                    <div class="forms">
                        <v-text-field v-model="emailAddress" :rules="[emailRules.required, emailRules.regex]" autofocus dense height="48px" outlined placeholder="メールアドレスを入力してください"></v-text-field>
                        <v-text-field v-model="password" :append-icon="passwordShow ? 'mdi-eye' : 'mdi-eye-off'" :rules="[passwordRules.required]" :type="passwordShow ? 'text' : 'password'" dense height="48px" name="input-password" outlined placeholder="パスワードを入力してください" @click:append="passwordShow = !passwordShow"></v-text-field>
                    </div>
                    <v-btn @click="login" color="#7b68ee">ログイン</v-btn>
                </div>
            </v-card>
        </v-main>
    </div>
</template>
  
<script>
export default {
    name: 'Login',
    data() {
        return {
            emailAddress: '',
            password: '',
            emailRules: {
                required: value =>
                    !!value || 'メールアドレスは必須です',
                regex: value =>
                    /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
                        value
                    ) || 'メールアドレスの形式が違います'
            },
            passwordShow: false,
            passwordRules: {
                required: value =>
                    !!value || 'パスワードは必須です',
            }
        };
    },
    methods: {
        login() {
            this.$store.dispatch('login', {
                emailAddress: this.emailAddress,
                password: this.password
            });
        }
    },
    computed: {
        isLogedIn() {
            return this.$store.state.emailAddress
        }
    },
    watch: {
        isLogedIn() {
            this.$router.push('/home').catch(err => console.log(`エラー:${err}`));
        }
    }
};
</script>

<style scoped>
.fill-width {
    top: 50%;
    text-align: center;
}

.error--text {
    color: red !important;
}

.forms {
    width: 80%;
    margin:0 auto;
}
</style>

Home.vue

<template>
    <div>
        <h1>Home画面です</h1>
        <router-link to="/about">about</router-link>
        <p>API取得値:{{ text }}</p>
    </div>
</template>

<script>

export default {
    name: 'Home',
    data(){
        return{
            text: null
        }
    },
    created() {
        this.getApi();
    },
    methods: {
        getApi() {
            const _this = this;
            this.$axios.get('/home')
            .then(response => {
                _this.text = response.data
            })
            .catch(error => {
                _this.text = error
            })
        }
    },

}
</script>

About.vue

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <router-link to="/home">back to Home</router-link>
  </div>
</template>

<script>

export default {
    name: 'About',

    components: {
    },
}
</script>

また、App.vueファイルを以下のように変更します。

<template>
  <v-app>
    <v-main>
      <router-view/>
    </v-main>
  </v-app>
</template>

<script>

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

router-vueタグの中身がルーティング時に表示されるコンポーネントに置き換えられる形になります。

次に、ログインボタン押下時にバックエンドアプリケーションにPOSTする必要があるため、vue.config.jsにプロキシ設定を追記します。

  devServer: {
    proxy: {
        '^/api': {
            target: 'http://localhost:8080/',
            ws: true,
            changeOrigin: true
        }
    }

ここまでの設定で、

  • ログイン画面→Home画面(Home画面上にバックエンドで作成したAPIの値が取得されていること)→Aboutページへの遷移が実装されていること

  • 未ログイン状態でHome/About画面にアクセスするとログイン画面にリダイレクトされること

が達成されていれば完了です。 f:id:TechHotoke:20211205002237p:plain f:id:TechHotoke:20211205002314p:plain f:id:TechHotoke:20211205002328p:plain

最後にログアウト機能を実装します。 Home.vueにログアウトボタンを実装します。 templateタグ内に下記のように実装し、

 <v-btn @click="logout" color="#7b68ee">ログアウト</v-btn>

scriptタグ内にlogoutメソッドを定義しstoreにdispatchします。

logout() {
            this.$store.dispatch('logout')
        }

storeのlogoutメソッドはstateをnullに変更するメソッドとして定義されています。(store/index.jsのコード参照) そして、stateの値をcomputed(※算術プロパティ)で取得し、watchプロパティでstateの変更を感知したらログインフォームに遷移するように実装します。

※算術プロパティ

あるデータから派生するデータをプロパティとして公開する仕組みで、Vueコンストラクタのオプションオブジェクトの1つです。データそのものに何らかの処理を与えたものをプロパティにしたいときなどに使われます。

computed: {
        isLogout() {
            return this.$store.state.emailAddress
        }
    },
    watch: {
        isLogout() {
            this.$router.push('/');
        }
    }

これでログアウト機能の実装は完了です。


今回の内容は一旦これで終了です。

ここまで読んで頂きありがとうございます。

参考

【Vue】VueのプロジェクトでSassを使用した際の「TypeError: this.getOptions is not a function」対応

f:id:TechHotoke:20211216134305p:plain

環境

  • Vue 2.6.1

  • webpack 4.46

事象

Vuetifyを導入するためにSassをnpmインストールしたところ、下記のようなエラーが発生。

 ERROR  Failed to compile with 1 error                                                                                     

 error  in ./node_modules/vuetify/src/styles/main.sass

Syntax Error: TypeError: this.getOptions is not a function

Sassのバージョン

  "sass": "~1.40",
   "sass-loader": "^12.3.0",

sass-loaderのレポジトリを参照すると、sass-loader11以降webpackのバージョンが5系出ないとサポート対象外とのこと。

⚠ BREAKING CHANGES
minimum supported webpack version is 5

github.com

なので、sass-loaderのバージョンを以下コマンドで下げます。

npm install --save-dev node-sass sass-loader@10

package.jsonのバージョンが変更されていることを確認。

 "sass": "~1.40",
  "sass-loader": "^10.2.0",

こちらの対応で表題のエラーは解消されました。

参考

Nuxt.jsでSassを使用した際に「TypeError: this.getOptions is not a function」とエラーが出る | cly7796.net