クマのブログ

つまづいたところ、学びを書いていきます

ライフサイクルフックの違いによるレンダリングのタイミング~img src編①~

はじめに

Vueのライフサイクルフックの理解が曖昧だった。

特にコンポーネント間でpropsを使った、ページが描画されてすぐのcreated, mounted...あたりの理解が曖昧

以下のようなコードを見た時に

「どのタイミングで」

「どのようにurlプロパティが変化しているのか」

が本当に理解しておらず結構ハマった(表示されるsrc属性の値はいつ ”” / url / transparentImageUrl / img/hoge.png のどれかになるんだ?)

↓親コンポーネント

<template>
    <parent-component
    :url="images.url"
  />
</template>

↓子コンポ―ネント

<template>
    <img
      v-if="url"
      src="url"
    >
    <img
      v-else
      :src="transparentImageUrl"
    >
</template>

<script>
export default {
  props: {
    url: {
      type: String,
      default: "",
    },
  },

  data() {
    return {
      transparentImageUrl: image.TRANSPARENT_IMAGE_URL,
    };
  },
    created() {
        this.url = 'img/hoge.png';
  },
    ...
}
</script>

いまだに画面描画時に処理をしたい時にbeforeCreate, created, beforeMount, mountedの内どれを使えばいいのかの判断も曖昧なので、そんな自分の一助になればと思った

加えてmethodsでsrcの値をreturnするパターンはどうなのか、ということも確認

話すこと

以下コードを例にどのライフサイクルフックをつかったら、どんな挙動を示すのかを順にみていく

Vue公式ドキュメントのライフサイクルダイアグラムの図と一緒に見ていければと思う

やりたいこと

コンポーネントで定義するimgタグのsrc属性の内容に応じて表示する画像を変更する

OKな時

f:id:kuma_kuma0121:20220223214525p:plain

NGな時

f:id:kuma_kuma0121:20220223214455p:plain

ソースコード

親コンポ―ネント

<template>
  <div>
    <ImageChildComponent
      :url="url"
      @replace="replaceUrl"
    ></ImageChildComponent>
  </div>
</template>

<script>
import ImageChildComponent from './ImageChildComponent.vue'

export default {
  components: {
    ImageChildComponent
  },
  data() {
    return {
      url: '',
    }
  },
  methods: {
    replaceUrl(newUrl) {
      console.log(newUrl);
      this.url = newUrl;
    },
  }
}
</script>

<template>でやっていることは

<script>でやっていることは

コンポーネント

<template>
  <div>
    <!-- OK時の画像 -->
    <div
      v-if="url"
    >
      <img
        :src="url"
        width=200
      >
      <h2>OK!!!</h2>
    </div>
    <!-- NG時の画像 -->
    <div
      v-else
    >
      <img
        src="../assets/no-image.png"
        width=200
      >
      <h2>NG...</h2>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    url: {
      type: String,
      default: "",
    },
  },
  beforeCreate() {
    // console.log('beforeCreate: ', this.url);
    // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    // this.$emit("replace", newUrl);
  },
  created() {
    // console.log('created: ', this.url);
    // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    // this.$emit("replace", newUrl);
  },
  beforeMount() {
    // console.log('beforeMount: ', this.url);
    // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    // this.$emit("replace", newUrl);
  },
  mounted() {
    // console.log('mounted: ', this.url);
    // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    // this.$emit("replace", newUrl);
  },
  methods: {
    getUrl() {
      // console.log('methods(getUrl): ', this.url);
      // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
      // this.$emit("replace", newUrl);
      return this.url;
    }
  }
}
</script>

<template>でやっていることは

  • コンポーネントから受け取ったurlの有無によって、表示内容を変更
    • 成功時:色付きの画像+「OK!!!」の文字列
    • 失敗時:no-image画像+「NG...」の文字列

<script>でやっていることは

  • コンポーネントから渡されたurlpropsプロパティで受け取る
  • 挙動を比較確認するためにbeforeCreate, created, beforeMoute, mounted, methods(getUrl)を各々定義
    • 手順としてはコメントアウトしてある箇所を随時コメントインして実行していく

デフォルト

ブラウザ

f:id:kuma_kuma0121:20220223214455p:plain

コンソール

f:id:kuma_kuma0121:20220223214818p:plain

エラーが2つ

[Vue warn]: Error in beforeCreate hook: "TypeError: Cannot read properties of undefined (reading 'url')”

TypeError: Cannot read properties of undefined (reading 'url')

  • インスタンス化する前にthis.urlを参照しに行っているので、「urlなんてプロパティは存在しない!」って怒られている

beforeCreated

...
beforeCreate() {
    console.log('beforeCreate: ', this.url);
    // 以下2行をコメントイン
    const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    this.$emit("replace", newUrl);
},
...

ブラウザ

デフォルトと同じなので割愛

コンソール

コチラもデフォルトと同じなので割愛

beforeCreateではインスタンスは生成しているがデータの初期化ができていないので、data()やpropsが参照できない

なのでデータ初期化していない(url = “” すらやっていない)状況でプロパティ参照しようが、urlにnewUrlを設定しようが意味ないよってこと

※beforeCreatedに console.log('beforeCreate: ', this.url); を書いていると常にエラーがでてくるので、これ以降はこの処理をコメントアウトする

created

created() {
    console.log('created: ', this.url);
    const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    this.$emit("replace", newUrl);
},

ブラウザ

f:id:kuma_kuma0121:20220223214525p:plain

コンソール

f:id:kuma_kuma0121:20220223214932p:plain

個人的には3行目の@replaceイベント発火させた後のthis.urlに@replaceしたnewUrlが入っていないのが意外だった

なので、そのままcreated以降のライフサイクルフック内のthis.urlもcreatedで@replaceした値が反映されてない

ただ、再インスタンス化する(今回は以下のようにconsole.logをcreatedに追加)と、、、

created() {
    console.log('created: ', this.url);
    const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    this.$emit("replace", newUrl);
        // インスタンス化するために
    console.log('created: ', this.url);
},

コンソール(再インスタンス化)

f:id:kuma_kuma0121:20220223215015p:plain

設定済みのurlが全てのライフサイクルフックで参照できるようになる

ギモン

ここはどういう流れなんだろう。。。

特に、初回読み込み時はブラウザ上ではnewUrlが表示されているのにもかかわらず、createdでも他のライフサイクルフックでもthis.urlがnewUrlを参照しておらず、nullだったのはまだ理解ができない。。。

createdの動きについてつかみ切れていない…

beforeMount

beforeMount() {
    console.log('beforeMount: ', this.url);
    const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    this.$emit("replace", newUrl);
},

ブラウザ

createdと同じ

コンソール

f:id:kuma_kuma0121:20220223215100p:plain

ここもcreatedと同じく、beforeMounteを通った以降はthis.urlがnewUrlを参照していないんだが、ブラウザの表示はnewUrlになっている、という状況

ギモン

createdと同じ

mounted

mounted() {
    console.log('mounted: ', this.url);
    const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
    this.$emit("replace", newUrl);
},

ブラウザ

created, beforeMouteと同じ

コンソール

created, beforeMouteと同じ

ギモン

created, beforeMouteと同じ

methods

ここで想定するのは以下のようなソースコード

<template>
        <div
      v-if="url"
    >
      <h2>OK!!!</h2>
      <img
        :src="getUrl()" // 変更
        width=200
      >
    </div>
        ...
</template>

<script>
export default {
    ...,
    methods: {
        getUrl() {
            console.log('methods(getUrl): ', this.url);
      const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
      this.$emit("replace", newUrl);
      return this.url;
  },
    ...
}
</script>

ブラウザ

f:id:kuma_kuma0121:20220223214455p:plain

まさかのno-image表示

コンソール

f:id:kuma_kuma0121:20220223215147p:plain

なので、各ライフサイクルフックでもthis.urlの値はnull

getUrl内にあるconsole.log('methods(getUrl): ', this.url);も実行されていない

これに関しては流れとしては以下の通り

1, 親→子にurlを渡され、v-if=”url”を評価。親から渡されるurlはnullなので、v-else側に処理が入る

2, 結果、no-imageが表示され、各ライフサイクルフックでもthis.urlはnullとなる

3, さらにgetUrlメソッドが呼び出されないため、console.log('methods(getUrl): ', this.url);も実行されない

※getUrlを呼び出すにはv-if="url”をv-if="getUrl”にすれば、OK画像の表示ができる

    <div
      v-if="getUrl()"  <!-- 変更 -->
    >
      <h2>OK!!!</h2>
      <img
        :src="getUrl()"
        width=200
      >
    </div>

コンソール(OK画像表示の際)

f:id:kuma_kuma0121:20220223215244p:plain

v-if時と:srcの値設定の際にgetUrlが3回呼び出されるので、見ずらい

気になったのは3行目でmountedの前にgetUrlメソッドが呼ばれ、処理を実行してから、mountedに入っている点がライフサイクルフックの通りだなと実感できた

3回呼ばれている内訳は以下推測

  • 1回目:初回インスタンス時、v-if内のgetUrlメソッドのみ実行
  • 2,3回目:templateが描画される際にv-ifと:srcのgetUrlが呼び出される

現状ではすぐわからないため、仮説を立て、情報を当てつつ正解を見つけていく

参考記事

https://jp.vuejs.org/v2/guide/instance.html#ライフサイクルダイアグラム

https://qiita.com/chan_kaku/items/7f3233053b0e209ef355

https://qiita.com/ksh-fthr/items/2a9f173c706ef6939f93

ギモン

  • re-renderすることでbeforeMountとmountedが呼ばれるが、re-renderのトリガーになる事象は何なのか気になった
    • 推測としてはdataやpropsの内容に変化が起こった時にre-renderされると推測
  • src属性が子コンポーネント特有dataプロパティだったら…というパターンで別途同じ検証する

ブラウザキャッシュについて

はじめに

業務で、画像(img)がキャッシュから取得することで想定外の挙動をする事態になっており対応した時に

「そもそも『キャッシュ』についてよく理解していないな…」

と痛感したので、対応にすごく時間がかかったので自分用メモ

※「キャッシュ」と一言で言ってもいろんなキャッシュがあるので、前提として「ブラウザキャッシュ」のみを取り上げる

※以下「キャッシュ」=「ブラウザキャッシュ」とする

キャッシュについて

キャッシュとは?

https://www.siteengine.co.jp/blog/web-cash/

  • 一度サーバーから取得したデータを一時保管する機能
  • クライアントとサーバーの間に存在する
  • クライアントから全く同じデータを取得するリクエストを受けたら自動でキャッシュからデータを取得する

メリット

https://www.siteengine.co.jp/blog/web-cash/

サーバーへの負荷軽減

サーバーへのリクエストを必要な分だけ送信出来る

サイトの読み込み時間短縮

サーバーからのレスポンス待たなくていい

どこに保存される?

ブラウザ

  • 保存先はブラウザによってまちまち
  • Chromeの場合は以下
%LOCALAPPDATA%\Google\Chrome\User Data\Default
または
%LOCALAPPDATA%\Google\Chrome\User Data\Profile

※他のキャッシュだとRedisなどのデータベースをキャッシュとして使うことも多々ある

キャッシュの内容確認は難しい

  • 内容確認は上記パスを見に行くことだが、ファイル名が「data...」「f...」などわけのわからないファイル名になっているので、実質不可
    • imgのsrcやcssのパスで保存されているわけではない
  • 「どうしても確認したい」という場合は以下記事参照

https://aprico-media.com/posts/1338#:~:text=Chromeのキャッシュは、Windows,に保存されています。

キャッシュの削除方法

  • 上記パスの削除したいファイルの削除
  • 現在のブラウザのキャッシュだけ削除ならスーパーリロード

保存されるキャッシュの単位

  • 保存されるデータはCSSファイル(hoge.css)、JSファイル(hoge.js)、画像(hoge.png, jpg)のパスを有しているファイル

その他

bfcache

  • ブラウザの戻る、進むボタンを押したときに使用する通常のブラウザキャッシュとは別でデータを保存、取得されるキャッシュ

キャッシュの無効化

https://zenn.dev/mindwood/articles/ba4594c53a93b0

コチラの4種類のやり方

1, METAタグ指定

// 追加
<meta http-equiv="Cache-Control" content="no-cache">

2, Cache Busting

<link href="https://example.com/css/style.css?ver=210615" rel="stylesheet">
  • ?ver=***とバージョンを付けたりするとリクエストの度に参照するパスが変わるので、必ずサーバーへアクセスするようになる
  • 更新頻度が高い(imgのsrc属性など)であればtimestampsのパラメータに現在時刻(UNIX時間)を持たせるのも選択肢の一つ

3, HTTPレスポンスヘッダーで制御

<Files ~ ".(jpe?g|png|gif)$">
  Header set Cache-Control "no-cache"
</Files>
<?php
header('Cache-Control: no-cache');

4, JavaScriptでキャッシュを無視

$('.container img').each(function(index, element) {
  const src = $(element).attr('src');
  $(element).attr('src', src + '?' + new Date().getTime());
});
  • JSで手動でimgのsrcにパラメータとその値を持たせることで、Cache Bustingを実現

Laravelでのキャッシュの無効化↓

1, Controllerからview単位でキャッシュ無効化

https://qiita.com/MR_ROBOT/items/46aeacd6590e3e500c46

return response(view('example'))
            ->withHeaders([
                'Cache-Control' => 'no-store',
            ]);

2, ミドルウェアでキャッシュ無効化

https://traveler0401.com/laravel-security/

<?php

namespace App\Http\Middleware;

use Closure;

class ResponseHeader {
    public function handle($request, Closure $next) {
        $response = $next($request);

        // デフォルトは「no-cache, private」だが、明示的に記載
        $response->header('Cache-Control', 'no-cache, private');

        return $response;
    }
}

ただ、2, のやり方ではLaravelの仕様上、キャッシュしきれないデータがあるので注意

https://zenn.dev/yuzuyuzu0830/articles/011681894a0f6b

やるなら、no-storeにする必要がある

        // デフォルトは「no-cache, private」だが、明示的に記載
        $response->header('Cache-Control', 'no-store, private');

キャッシュからデータを取得したか否かの判定方法

  • JavaScriptpageshowイベントにあるpersistedプロパティで判定可能

pageshow : https://developer.mozilla.org/ja/docs/Web/API/Window/pageshow_event

persisted : https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted

  • ドキュメントそのままですがこれで、ページ遷移時にキャッシュから取得したデータであればconsoleが表示
window.addEventListener('pageshow', function(event) {
  if (event.persisted) {
    console.log('Page was loaded from cache.');
  }
});
  • ただ、これだと「HTMLファイルだけの判定しかできないのでは…」と使いどころがイマイチ思い当たらない。

おわりに

  • メモ書きなので、間違ったところがあるかもです。
  • 間違った理解していれば、ご指摘いただけますと幸いです。
  • 最後になり申し訳ないですが、参考にさせて頂いた記事については感謝申し上げます。

インターフェースとは?

はじめに

Laravelで認証周りをいじっていた際、先輩から「ここらへん参考になるかも」と教えてもらったドキュメントを読んでいた

https://readouble.com/laravel/8.x/ja/authentication.html#the-user-provider-contract

↓Illuminate\Contracts\Auth\UserProvider

<?php

namespace Illuminate\Contracts\Auth;

interface UserProvider
{
    public function retrieveById($identifier);
    public function retrieveByToken($identifier, $token);
    public function updateRememberToken(Authenticatable $user, $token);
    public function retrieveByCredentials(array $credentials);
    public function validateCredentials(Authenticatable $user, array $credentials);
}

ここのupdateRememberを呼び出したいというのが今回のやりたいことだったが、ふと「インターフェースってどうやって使うんだっけ?」となり、実装に迷った

(ここで、Controllerにある見慣れないメソッドの処理内容を知りたい時、VSCode拡張機能PHP Intelephenseを使ってジャンプした先がInterfaceのメソッドで、そっと見て見ぬふりをしていたツケが回ってきた…)

このままではいかん、と思い、実務でインターフェースを使いこなせるようになるよう基本の勉強

インターフェースとは

https://www.php.net/manual/ja/language.oop5.interfaces.php

PHPのドキュメントの定義によると

「メソッドの実装を定義せずに、 クラスが実装する必要があるメソッドを指定するコードを作成」

できるモノ、とある。

特徴としては

  • クラスと同様に定義できるが、 キーワード class のかわりに interface を用いる
  • メソッドの実装は全く定義しない

というものがある

メリット

使う側で呼出し元を意識しなくていい

こちらの参考記事をに沿って行くと

1, interface(メソッド名) ------ 2, 処理定義側class(メソッド名) ------- 3, 使用側class(メソッド名)

Notification(`メール送信`)  ─   MailNotifier(`メール送信`)    ─     Hoge(`メール送信`)
                            └  SlackNotifier(`メール送信`)  ┘

こんな感じになっていて、3, class使用側では呼び出し元が2, のMailNotifierだろうがSlackNotifierだろうが関係なく使える、というのがいいポイントかな、と感じました。

追加機能も最低限の差分で追加できる

先ほどのメールを送信するを実行した際に、「MailとSlackのほかにもう1か所メッセージを送信したい!」となった場合は先ほどの2, にclassを追加し、以下のようにすればいい。

1, interface(メソッド名)       2, 処理定義側class(メソッド名)      3, 使用側class(メソッド名)

Notification(`メール送信`) ─  MailNotifier(`メール送信`)    ─    Hoge(`メール送信`)

                          └  SlackNotifier(`メール送信`)    ┘
                          └  OtherNotifier(`メール送信`)    ┘   // 追加

1, から全部処理内容書いて…みたいなことをしなくていいので、すごくコードがスッキリするイメージが持てる

使用側での可読性が向上

こちら)のソースコードの比較が分かりやすいですがインターフェースを使って書くと使用側でのコード量が圧倒的に減る

結局はバグ修正の際に開発者が最初に見るのは使用側、往々にしてビジネスロジック側なので所見でのコードがすっきりしていると、業務効率も上がってくる、ということにつながる

疑問

ここまで知識を入れた時点で浮かんだ疑問と、その回答を以下に記載

※完全に自分用メモなので、間違っている点、観点がズレている点もあるかもですが、都度修正していきます。

Q.1 interfaceって必要か?

そもそも論ですが、先ほどのこの図を見て思ったのが「1, interfaceって必要か?」という点

1, interface(メソッド名)       2, 処理定義側class(メソッド名)      3, 使用側class(メソッド名)

Notification(`メール送信`) ─  MailNotifier(`メール送信`)    ─   Hoge(`メール送信`)
                          └  SlackNotifier(`メール送信`)   ┘
                          └  OtherNotifier(`メール送信`)   ┘

これを以下のようにしてしまっても、先ほどのメリットを享受できるのでは?と思った

1, interface(メソッド名)   -    2, 処理定義側class(メソッド名) -   3, 使用側class(メソッド名)

(delete)                   ─  MailNotifier(`メール送信`)       ─     Hoge(`メール送信`)
                           └  SlackNotifier(`メール送信`)      ┘
                           └  OtherNotifier(`メール送信`)      ┘

その中でinterfaceを使うメリットといえばメソッド名と変数の共通化ができる点だな、と感じた

要は、「メソッド、引数、その引数の型」の箱を作るイメージ

Q2. オーバーライドと何が違う?

interfaceをimplementsしたクラスは「1, から2,」及び「2, から3, 」で同じメソッドを使っているがふと、「オーバーライドと似ている気が、、、」と思ったけど具体的に何が違うのか?

これはシンプルで呼出し先が呼出し元に影響を与えているか、否かの違い

各々呼出し元のメソッドを呼び出した場合、

  • オーバーライド:呼び出し元の処理内容を上書きして使う。一旦白紙に戻す(参照状態)
  • インターフェースの使用:呼出し元に全く影響を与えず、呼出し元で定義された処理をそのまま実行する

その点で、インターフェースの利用により「1つのプロジェクトで同じメソッド名でも違う処理をしている…」みたいなカオス状態を作ることを防ぐことができる

トレイトと何が違う?

「他のクラスで定義した処理内容を使用できる」という点でトレイトとの違いは何だろうか、と思いました。

そもそも、トレイトは「単一継承の制約を減らすために作られたもの」であり、useすれば際限なく使える

https://www.php.net/manual/ja/language.oop5.traits.php

トレイトは機能ごとに作られることが多く、トレイトの中でたくさんのメソッドを定義し、処理内容もトレイト内で記述される。イメージは以下のような感じかと

 1, トレイト         2, 使用側class
 Auth          ─     Hoge
 Jsonable      ┘
 Formatter     ┘   

インターフェースに比べて1階層分減った分全体のディレクトリ構造はすっきりしたが、トレイトはuseできる個数に制限がないので、

「どこでどのトレイトを用いた結果、この処理が実行できているのか」

というカオス状態が生まれる危険性があるな、と推測

最後に

  • インターフェースの基本、使用方法についてはイメージがついた。
  • が、最後のギモンについてはまだ「完全に理解した」状態になっていないので、インターフェース、トレイト、オーバーライドの使用の際に整理が必要

参考記事

https://qiita.com/tkek321/items/a6112bc195b73438a9b0

https://qiita.com/sasakure-kei/items/212637b2f32197441a3a

「DBへの保存時にエスケープしちゃいけない」からわかったこと

背景

  • バグ改修をせっせとやっていた時の話
  • その一つに「XSS発生の対応」があった
  • その時に見事にやらかした話
  • 言語はLaravel

バグ状況

状況はこんな感じ

  1. 管理画面でユーザーの注文一覧を見れる
  2. この各注文に管理者が「メモ」を書けるようにしていた
  3. この「メモ」がtextareaで書かれていた
  4. この「メモ」に以下のようなコードを書くとJSが実行されてしまうバグが発生していた
<script>alert('危険!')</script>

典型的なXSSですね

当初の実装方針

  • 注文の一覧表示までの処理の流れはこう
  • ユーザーが注文=注文テーブルへの保存処理
  • 管理者が注文一覧を閲覧=注文テーブルからデータを取得

  • そのうえで、私の実装方針は

「じゃあ、1. の保存時にエスケープ処理して対応して終わりじゃん」

と思い、「メモ」のsave時にe()(※)を実装しました。イメージは以下。

public function update(Request $request)
{
    ...
    DB::beginTransaction();
  try {
      $user->order->fill([
        // e()でエスケープ処理で対応
        'memo' => e($validated['memo']),
      ]);
      $authenticatedUser->cart->save();
      DB::commit();
    ...
    }

e()はLaravelのエスケープ処理メソッド

結論

もうすでに「エスケープされたデータがDBに保存される」という問題が発生しており、「注文一覧の出力時に、エスケープ処理された文字(特殊文字)がそのまま出力してしまう」という脅威を招いている。

ということで即修正。

本当の問題点

ただ、テストを全くしてないわけではなかった。

実際に「管理画面の注文一覧ではXSSは発生せず出力内容に問題はなかった」ことは確認済みだった。

  • 実際にタスクが完了したのちに、再度確認してみると本当の問題点が発覚。

起こっていた問題点

実装箇所を間違えてテストをしていました。。。

原因①:なぜエスケープ処理したのか(なぜ)

  • ソースコードを読み違えによる見落とし」が原因
    • 本来改修すべきは注文新規登録(=storeメソッド)
    • ただ、自分が今回実装した箇所は注文編集(=updateメソッド)
    • テストしたのは改修をしたと思い込んでいる新規登録箇所
      • ココでは何も追加実装をしていないので、当たり前のように正常に表示される

原因②:なぜ実装箇所とテスト範囲が違うのか

  • これはデバッグ不足
    • ちゃんと本来改修すべきstoreメソッドでXSSが起きていることをPHP Debugで確認せずに実装した
  • 自分の中では無意識に

「新規登録処理の中でupdateメソッドを通っている」

と思い込んで実装していた

原因③:なぜデバッグを怠ったのか

  • 主に2点
    • バグ改修の期限が迫っており、精神的に焦っていたため
    • 最近デバッグせずとも実装できることが多かったので、驕った

再発防止

レビュー前のチェック項目の追加

  • 想定している処理をしているか、デバッグを使って確認したか
    • レビュー前には自作のチェックシートでテスト漏れがないか確認してます。
    • これの追加し、必ず想定したソースコードを通って処理されていることをテストする

所見

  • なかなかしょぼいことしたな、と実感したので自戒

  • 当初は「XSSの理解不足」でまとめようとしたら、問題はそこじゃなかったので記事の方向性を軌道修正しました。

computed, watch, methodsの違い

https://qiita.com/yukibe/items/f49fcb935363200916fe

computed


  • 算出プロパティ
    • 以下のような可読性の悪いテンプレート内のコードをすっきりさせる

↓before

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

↓after

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>

...

<script>
export default {
    computed: {
        // 算出 getter 関数
        reversedMessage: function () {
          // `this` は vm インスタンスを指します
          return this.message.split('').reverse().join('')
        }
     }
}
</script>
  • リアクティブな依存関係にもとづきキャッシュされる
    • data()内のデータが変わってもキャッシュされた値を取得してくるので、再描画されない
    • 参考記事の例を見るとわかりやすい
      • dataに定義されているfirstNamelastNameが変更されるたびに、コンポーネント内のテンプレートは再描画されるが、nowは初回読み込み時の値をキャッシュしたモノを取得してくるので、変更されない

methods


  • computedの逆で、リアクティブな依存関係にもとづきキャッシュされないのがmethods
  • これまた参考記事の例を見るとわかりやすい
    • computedとの比較で今回はfirstNamelastNameが変更されるたびに、nowは再描画され、現在時刻を返す(=表示する)
  • 一般的には「メソッド呼び出しは、再描画が起きると常に関数を実行する」と言われる

watch


  • 再描画時に特定のdataプロパティに対して、computedでは書けない複雑な処理を書こうとした時に使用
  • 通常、「dataの監視」という目的であれば冗長性を避けるためにcomputedを使われる

https://jp.vuejs.org/v2/guide/computed.html#算出プロパティ-vs-監視プロパティ

まとめ


どれを使えばいいか迷った際には1→2の流れで進める。


1, dataの内容が変更された際に常に再描画をしてほしいか

  • Yes(常に更新)→methods
  • No(初回アクセス時の値をキープ)→computed、watch
    • 但し、冗長化を避けるためにcomputed推奨
  • computedで実現できない(複雑な処理を書く)場合はwatch

動的コンポーネントって?

はじめに

業務でVueを使っており、「動的コンポーネント」という概念が原因でうまくタスクが進まなかったことがあった

具体的には、共通JSであるapp.jsに書いたjQueryが、Vueから呼び出せない、という事象

尚、Vueではv-forを使っていた

当時の実装イメージ

イメージとしては、Vue側でjQueryを発火させるためのセレクタを設定し

      <ul>
      <li
        v-for="item in items" // v-for使用
        :key="item.id"
      >
                <a
          href=""
          @click.prevent.stop="hoge"
          id="selector"
        >
        URL
        </a>
      </li>
    </ul>

以下のようにjQueryでaタグがクリックされたらイベントを検知して処理をする

app.js

$('#selector').on('click', function(){
    console.log('clicked!!');
    // クリック時の処理
});

そして、コンポーネントを使っているblade(HTML)でapp.jsを読み込んでいる、という状況

<link rel="stylesheet" href="./app.js">

これでaタグをクリック時にconsoleに’clicked!!’が表示されるはずが、されない、という困りごと

※本筋ではないので適当に書いてます。あくまでイメージです。

相談の結果…

煮詰まった結果、先輩に相談させてもらって対応。そこで、今回の原因は

「app.jsの読み込み時に動的コンポーネント内のDOMが生成されていないからだね」

と言われた。

全くピンと来ておらず、特に「動的コンポーネント」ってのが不明でそれ以降が頭に入らなかった…

ということでタイトルの疑問に至った、という背景

結論

調べた結果、要は

動的にDOMの内容が切り替わるコンポーネント

のこと

こちら)に具体例があったのですが

  • v-if / v-show
  • is
  • v-for

などを使って状況に応じてDOMの生成を切り替えるコンポーネントのことをいう、という理解でした。

おわりに

  • 本当は「動的コンポーネントとは?」より「なぜ動的コンポーネントはapp.jsを読み込めないのか?」という点を深堀した方が圧倒的にタメになるのですが、「動的コンポーネント」の言葉があいまいな理解だったので、ワンクッション置きました。
  • 別途「なぜ動的コンポーネントはapp.jsを読み込めないのか?」については深堀します。

スタイルガイド(優先度:B)

https://jp.vuejs.org/v2/style-guide/#優先度B-のルール-強く推奨-読みやすさの向上

コンポーネントのファイル

悪い例

Vue.component('TodoList', {
  // ...
})

Vue.component('TodoItem', {
  // ...
})

良い例

components/
|- TodoList.js
|- TodoItem.js

components/
|- TodoList.vue
|- TodoItem.vue

単一ファイルコンポーネントのファイル名の形式

  • すべてパスカルケース (PascalCase) にするか、すべてケバブケース (kebab-case) にする

基底コンポーネントの名前

悪い例

components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue

良い例

components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue

単一インスタンスコンポーネント

悪い例

components/
|- Heading.vue
|- MySidebar.vue

良い例

components/
|- TheHeading.vue
|- TheSidebar.vue

コンポ―ネント名における単語の順番

最高レベルの(最も一般的な)単語から始めて、説明的な修飾語で終わるべき

悪い例

components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue

良い例

components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputQuery.vue
|- SearchInputExcludeGlob.vue
|- SettingsCheckboxTerms.vue
|- SettingsCheckboxLaunchOnStartup.vue

テンプレート内でのコンポーネント名の形式

JS/JSX 内でのコンポーネント名の形式

完全な単語によるコンポーネント

プロパティ名の型式

  • プロパティ名は、定義の時は常にキャメルケース(camelCase)にするべき
  • テンプレートや JSX ではケバブケース(kebab-case)

悪い例

props: {
  'greeting-text': String
}

良い例

props: {
  greetingText: String
}

テンプレート内での単純な式

複雑な式は算出プロパティかメソッドにリファクタリングして、コンポーネントのテンプレートには単純な式だけを含むようにするべきです。

悪い例

{{
  fullName.split(' ').map(function (word) {
    return word[0].toUpperCase() + word.slice(1)
  }).join(' ')
}}

良い例

<!-- テンプレート内 -->
{{ normalizedFullName }}

// 複雑な式を算出プロパティに移動
computed: {
  normalizedFullName: function () {
    return this.fullName.split(' ').map(function (word) {
      return word[0].toUpperCase() + word.slice(1)
    }).join(' ')
  }
}

単純な算出プロパティ

できる限りたくさんの単純なプロパティに分割するべき

悪い例

computed: {
  price: function () {
    var basePrice = this.manufactureCost / (1 - this.profitMargin)
    return (
      basePrice -
      basePrice * (this.discountPercent || 0)
    )
  }
}

良い例

computed: {
  basePrice: function () {
    return this.manufactureCost / (1 - this.profitMargin)
  },
  discount: function () {
    return this.basePrice * (this.discountPercent || 0)
  },
  finalPrice: function () {
    return this.basePrice - this.discount
  }
}

ディレクティブの短縮記法

ディレクティブの短縮記法は、常に使うか、まったく使わないかのどちらか