Vue.jsのライフサイクルとVue RouterによるSPA(Single Page Application)の関係の確認

nazology.net

アダマンド並木精密宝石株式会社はBlu-Rayディスク10億枚のデータの保管を可能にする、直径約5.5cmの超高純度のダイヤモンドウェハの量産に成功したと発表しました。

Blu-Ray「10億枚分」のデータを記録可能なダイヤモンドウェハを開発 - ナゾロジー

⇧「硬度10 ダイヤモンドパワー!(キン肉マン)」じゃないですが、大容量記録が可能ということですが、やっぱりお高いんでしょ?で本当にお値段が高そうで怖い...

TypeScriptのクラスが厄介な件

Java脳で考えてると、ちょっとTypeScriptのクラスに違和感を覚えるのですが、

qiita.com

ryym.tokyo

⇧ 上記サイト様によりますと、どうも、カプセル化に難があるようです。

とは言え、今回は時間がなかったので、妥協しまくります。

Vue.jsのライフサイクルとVue RouterによるSPA(Single Page Application)の関係の確認

「SPA(Single Page Application)」については、ブラウザのリロードボタンや戻るボタンを押下されない限り、ページの情報が初期化されることはないらしいですと。

yoshitake-1201.hatenablog.com

⇧ 上記サイト様がまとめてくださってます。

あとは、セッションが切れたりなんかでも挙動がおかしくなるらしいですが、今回は、Vue.jsのVue Routerで画面遷移した際に、

  • Vue.jsのライフサイクルのメソッドが呼ばれるタイミング
  • 別クラスの値は保持されてるのか
  • Watchに指定したメソッドが呼ばれるタイミング

という局所的な部分の確認のため、諸々の考慮は割愛いたします。

Vue.jsで、ブラウザの戻るボタンなどの制御については、

qiita.com

www.fuwamaki.com

⇧ 上記サイト様が参考になりそうですが、進むボタンとの区別を判断する術がないところが課題といったところでしょうか。

Vue.jsで、ブラウザのリロードボタンなどの制御については、

stackoverflow.com

stackoverflow.com

⇧ window.onbeforeunloadメソッドで検知する方法があるようです。

Vue.jsのライフサイクルなんかについては、手前味噌になり恐縮ですが

ts0818.hatenablog.com

⇧ 上記記事で少し触れてます。

で、ブラウザのリロードボタンや戻るボタンなど押下しない前提(セッション、クッキーなども考えない)とした場合、今回、分かったこととしまして、

  • Vue.jsのライフサイクルのメソッドが呼ばれるタイミング
    コンポーネントのページに遷移される度に呼ばれる。
  • 別クラスの値は保持されてるのか
    →明示的に初期化しない限り保持され続ける。
  • Watchに指定したメソッドが呼ばれるタイミング
    Watchしてる値が設定される時。画面経由じゃくなくても値を設定する際に呼ばれる。

という感じになります。

厄介なのは、Watchに指定したメソッドで監視してる値については、値が設定される度に呼ばれるということでしょうか。

実際にコードで確認してみました。

bobbyhadz.com

⇧ オブジェクトのプロパティの値が空かどうかのチェックは、上記サイト様を参考にさせていただきました。

Vue.jsのプロジェクトとしては、

ts0818.hatenablog.com

⇧ 上記の時のものに追加していく感じです。

インストールされてるnpmのモジュールは以下のような感じ。

{
  "name": "my-project-vue",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "vue": "^2.6.14",
    "vue-class-component": "^7.2.3",
    "vue-property-decorator": "^9.1.2",
    "vue-router": "^3.5.1"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.4.0",
    "@typescript-eslint/parser": "^5.4.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-plugin-router": "~5.0.0",
    "@vue/cli-plugin-typescript": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "@vue/eslint-config-typescript": "^9.1.0",
    "axios": "^0.26.1",
    "element-ui": "^2.15.8",
    "eslint": "^7.32.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-vue": "^8.0.3",
    "prettier": "^2.4.1",
    "sass": "^1.32.7",
    "sass-loader": "^12.0.0",
    "typescript": "~4.5.5",
    "vue-template-compiler": "^2.6.14"
  }
}

で、今回追加してるファイルは、

⇧ 選択されてるところが新しく追加したものになります。

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\modules\user\user.model.ts

interface UserModelType {
  id: string;
  lastName: string;
  firstName: string;
  age: string;
  birthday: any;
}

class UserModel implements UserModelType {
  id = "";
  lastName = "";
  firstName = "";
  age = "";
  birthday = "";

  constructor(init?: Partial<UserModel>) {
    Object.assign(this, init);
  }
}
export default new UserModel();

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\service\user\userServiceImpl.ts

class UserServiceImpl {
  public setUserModel(UserModel: any, formDataModel: any) {
    Object.assign(UserModel, {
      id: formDataModel.id,
      lastName: formDataModel.lastName,
      firstName: formDataModel.firstName,
      age: formDataModel.age,
      birthday: formDataModel.birthday,
    });
  }

  public clearUserModel(UserModel: any) {
    Object.assign(UserModel, {
      id: "",
      lastName: "",
      firstName: "",
      age: "",
      birthday: "",
    });
  }
}
export default new UserServiceImpl();
    

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\views\user\list.vue

<template>
  <div>
    <el-table :data="userList" stripe style="width: 100%">
      <el-table-column prop="id" label="ID" width="180"> </el-table-column>
      <el-table-column prop="lastName" label="氏" width="180">
      </el-table-column>
      <el-table-column prop="firstName" label="名"> </el-table-column>
      <el-table-column prop="age" label="年齢"> </el-table-column>
      <el-table-column prop="birthday" label="生年月日"> </el-table-column>
      <el-table-column>
        <template slot-scope="scope">
          <el-button size="mini" @click="handleEdit(scope.$index, scope.row)"
            >Edit</el-button
          >
          <el-button
            size="mini"
            type="danger"
            @click="handleDelete(scope.$index, scope.row)"
            >Delete</el-button
          >
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script lang="ts">
/* eslint-disable no-console */
import { Component, Vue, Watch } from "vue-property-decorator";
import UserModel from "@/modules/user/user.model";
import UserServiceImpl from "@/service/user/userServiceImpl";

@Component
export default class UserList extends Vue {
  private userList = [
    {
      id: 1,
      lastName: "鈴木",
      firstName: "一郎",
      age: "38",
      birthday: "1983-07-20 15:00:00",
    },
    {
      id: 2,
      lastName: "佐藤",
      firstName: "二郎",
      age: "39",
      birthday: "1982-07-20 15:00:00",
    },
    {
      id: 3,
      lastName: "高橋",
      firstName: "三郎",
      age: "40",
      birthday: "1981-07-20 15:00:00",
    },
  ];

  created() {
    console.log("■list");
  }

  private handleEdit(index: number, row: any) {
    console.log(row);
    if (row.id) {
      UserServiceImpl.setUserModel(UserModel, row);
      this.$router.push({ name: "/user/edit", params: { id: String(row.id) } });
    }
  }

  private handleDelete(index: number, row: any) {
    console.log(row);
  }
}
</script>

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\views\user\edit\index.vue

<template>
  <div class="app-container">
    <el-form :model="formDataModel" ref="formDataModel">
      <el-form-item label="氏">
        <el-input v-model="formDataModel.lastName"></el-input>
      </el-form-item>
      <el-form-item label="名">
        <el-input v-model="formDataModel.firstName"></el-input>
      </el-form-item>
      <el-form-item label="年齢">
        <el-input v-model="formDataModel.age"></el-input>
      </el-form-item>
      <div id="datetime">
        <div class="block">
          <el-date-picker
            v-model="formDataModel.birthday"
            type="datetime"
            placeholder="日付の選択"
          >
          </el-date-picker>
        </div>
      </div>
      <el-form-item size="large">
        <el-button type="primary" @click="goConfirm">確認</el-button>
        <el-button @click="goBackList">戻る</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script lang="ts">
/* eslint-disable */
/* eslint-disable no-console */
import { Component, Vue, Watch } from "vue-property-decorator";
import UserModel from "@/modules/user/user.model";
import UserServiceImpl from '@/service/user/userServiceImpl';

@Component
export default class UserFormEdit extends Vue {

  private formDataModel: any = {};

  // ライフサイクルのメソッド
  created() {
    console.log("■created()")
    if (this.$route.params.id) {
      let id :any = this.$route.params.id;
      console.log("■this.$route.params.id");
      console.log(id);
      //this.$router.push
    }
    console.log("■UserModel")
    console.dir(UserModel)
    // 確認画面からVue Router経由で戻ってきた場合
    if (!this.isEmpty(UserModel)) {
      console.log("■再設定");
      this.formDataModel = Object.assign({}, this.formDataModel, {
        id: UserModel.id,
        lastName: UserModel.lastName,
        firstName: UserModel.firstName,
        age: UserModel.age,
        birthday: UserModel.birthday
      });
    }
    console.dir(this.formDataModel);
  }

  // 確認画面に遷移
  private goConfirm() {
    console.log("■goConfirm()");
    this.formDataModel;
    console.dir(this.formDataModel);
    UserServiceImpl.setUserModel(UserModel, this.formDataModel);
    this.$router.push({ name: "/user/confirm", params: { formDataModel: this.formDataModel } });
  }

  // 一覧画面に遷移
  private goBackList() {
    // ユーザー情報初期化
    if(!this.isEmpty(UserModel)) {
      UserServiceImpl.clearUserModel(UserModel);
    }
    this.$router.go(-1);
  }

  /**
   * Objectの各プロパティの値がすべて空か確認
   * @param model プロパティの値がすべて空か調べたいオブジェクト
   * @returns true: プロパティの値がすべて空、false:値が空でないプロパティの値が存在する
   */
  private isEmpty(model: any) {

    const isNullUndefEmptyStr = Object.values(model).every(value => {
      // check for multiple conditions
      if (value === null || value === undefined || value === '') {
        return true;
      }
      return false;
    });
    return isNullUndefEmptyStr;
  }

  @Watch("formDataModel.lastName")
  private watchLastName() {
    console.log("■@Watch formDataModel.lastName")
    console.dir(this.formDataModel.lastName)
  }

  @Watch("formDataModel.firstName")
  private watchFirstName() {
    console.log("■@Watch formDataModel.firstName")
    console.dir(this.formDataModel.firstName)
  }

  @Watch("formDataModel.age")
  private watchFirstAge() {
    console.log("■@Watch formDataModel.age")
    console.dir(this.formDataModel.age)
  }

  @Watch("formDataModel.birthday")
  private watchFirstBirthday() {
    console.log("■@Watch formDataModel.birthday")
    console.dir(this.formDataModel.birthday)
  }
}
</script>

<style lang="scss" scoped>
.app-container {
  max-width: 640px;
  margin: auto;
}
</style>

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\views\user\confirm\index.vue

<template>
  <div>
    <el-form ref="form" :model="formDataModel" label-width="120px" size="mini">
      <div>{{ formDataModel.lastName }}</div>
      <div>{{ formDataModel.firstName }}</div>
      <div>{{ formDataModel.age }}</div>
      <div>{{ formDataModel.birthday }}</div>
      <el-form-item size="large">
        <el-button type="primary" @click="goSave">登録</el-button>
        <el-button @click="goBackEdit">戻る</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script lang="ts">
/* eslint-disable */
/* eslint-disable no-console */
import { Component, Vue, Watch } from "vue-property-decorator";
import UserModel from "@/modules/user/user.model";

@Component
export default class UserFormEdit extends Vue {
  private formDataModel: any = {};

  // ライフサイクルのメソッド
  created() {
      console.log("■created() confirm")
      if (this.$route.params.formDataModel) {
        console.dir(this.$route.params.formDataModel)
        this.formDataModel = this.$route.params.formDataModel;
      }
  }

  private goSave() {
    console.log("■TODO");
  }

  private goBackEdit() {
    this.$router.go(-1);
  }
}
</script>

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\router\index.ts

import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import HomeView from "../views/HomeView.vue";

Vue.use(VueRouter);

const routes: Array<RouteConfig> = [
  {
    path: "/",
    name: "home",
    component: HomeView,
  },
  {
    path: "/about",
    name: "about",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
  },
  // {
  //   path: "/test",
  //   name: "test",
  //   component: () => import("../views/NavigationGuard.vue"),
  // },
  // {
  //   path: "/testdom",
  //   name: "testdom",
  //   component: () => import("../views/TestDom.vue"),
  // },
  {
    path: "/testui",
    name: "testui",
    component: () => import("../views/TestElementUi.vue"),
  },
  {
    path: "/user/list",
    component: () => import("../views/user/list.vue"),
  },
  {
    path: "/user/edit/:id",
    name: "/user/edit",
    component: () => import("../views/user/edit/index.vue"),
    //props: (route) => ({ id: Number(route.params.id) }),
  },
  {
    path: "/user/confirm",
    name: "/user/confirm",
    component: () => import("../views/user/confirm/index.vue"),
    //props: (route) => ({ id: Number(route.params.id) }),
  },
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes,
});

export default router;

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\main.ts

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import Element from "element-ui";
// ElementUIでの言語設定、datePickerとかで適用される
import locale from "element-ui/lib/locale/lang/ja";
import "element-ui/lib/theme-chalk/index.css";

Vue.config.productionTip = false;
Vue.use(Element, { locale });

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

■C:\Users\Toshinobu\Desktop\soft_work\vue_work\vue-router-work\my-project-vue\src\App.vue

import UserFormEdit from './views/user/edit/index.vue';
<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <!--
      <router-link to="/testdom">dom</router-link> |
      -->
      <router-link to="/testui">UI</router-link> |
      <router-link to="/user/list">User</router-link>
    </nav>
    <router-view />
  </div>
</template>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &.router-link-exact-active {
      color: #42b983;
    }
  }
}
</style>

⇧ 実際はサーバーサイドと連携してデータを取得することが一般的とは思うのですが、今回は、適当なデータをべた書きで。

開発用サーバー起動で。

VS CodeVisual Studio Code)のターミナルに表示されてるURLにブラウザでアクセスします。「デベロッパーツール」を表示しておき、Userのリンクを押下してページ遷移し、「Edit」ボタンを押下。

デベロッパーツール」の「Console」を確認すると、user.model.tsのインスタンスが生成できてます。

「確認」ボタンを押下。

デベロッパーツール」の「Console」を確認すると、user.model.tsのインスタンスの値が維持されてます。「戻る」ボタンを押下してみます。

user.model.tsのインスタンスの値が維持されてます。

値を再設定すると、Watchのメソッドが呼ばれるのが確認できます。

というわけで、Vue.jsのライフサイクルのメソッドは、ページ遷移される度に呼ばれるということが確認できました。

また、Vuexを使わない場合でも、ブラウザのリロードボタンなどを押下したりなど、外部からの影響がなければ、値は維持され続けるようです。

実際は、リロードされても大丈夫なように制御などが必要ということでしょうかね。

あと、日付とかのフォーマット調整に関しては、今回は導入していないのですがMoment.jsというものが有名らしいです。

ですが!

zenn.dev

cpoint-lab.co.jp

⇧ 新規プロジェクトで導入するのは非推奨になっているようです...

う~ん、代替となるライブラリで情報が充実してくれてるなら良いのですが...

毎度モヤモヤ感が半端ない...

今回はこのへんで。