Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

Vue3.0における状態管理(Vuex,Provide / inject)

こんにちは、フロントエンドエンジニアの小玉です。

今回は、Vue.jsにおける状態管理についてお話したいと思います。

Vue2.x時代、多くの方はVuexを使用していたのではないでしょうか?Vue2.x、Vue3.xともに公式のドキュメントにおいても、状態管理のセクションでVuexが紹介されています。
ただVue3.0以降は、Vuexに打って変わる状態管理方法がいくつか提唱されていますので、それぞれの内容を簡単に見つつ比較したいと思います。

Vuex

VuexVue.jsのアプリケーションのための状態管理ライブラリであり、単純なグローバルオブジェクトとは異なり、Vuexのストアはリアクティブです。
また、Vuexは以下のように単方向のデータフローになるので、変更される状態の追跡が明示的です。
vue-devtoolsを使用すると、getterで得られる値、mutationcommitした履歴などが確認できます。

vuex.png

vuex.vuejs.org

actions

  • stateを直接更新することはなく、mutationsを経由することでstateを更新する。
  • 非同期の処理を入れることができる。

mutations

  • actionsから受け取った値をstateにセット

mutationsでは単にstateの値を変更するだけの処理を行うことが多いかと思います。
それに対しactionsでは比較的複雑なロジックを記述しmutationsに対して値を返すイメージです。

実際のコード

今回は簡単なカウンターアプリを実装します。
SFC内部で全て完結させる場合は上記のような形になります。

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="decrement">-</button>
    <button @click="increment">+</button>
  </div>
</template>

<script lang="ts">
import { reactive, computed, defineComponent } from "vue";
export default defineComponent({
  name: "App",
  setup() {
    const state = reactive({
      count: 0,
    });
    const count = computed(() => state.count);
    return {
      count,
      increment() {
        state.count += 1;
      },
      decrement() {
        state.count -= 1;
      },
    };
  },
});
</script>

では、実際にVuexを使用し、コンポーネントを分割した場合はどのような記述になるでしょうか。
ディレクトリ構成は以下のような形で進めてみます。

/src
 ├ components
 │  ├ DecrementBtn.vue
 │  └ IncrementBtn.vue
 ├ store
 │  └ index.js
 └ App.vue

Vuexのストアの定義を以下のようにします。

▼store/index.js

import { createStore } from "vuex";

const state = {
  count: 0
};
const mutations = {
  increment(state) {
    state.count++;
  },
  decrement(state) {
    state.count--;
  }
};
const actions = {
  increment: ({ commit }) => commit("increment"),
  decrement: ({ commit }) => commit("decrement")
};

export default createStore({
  state,
  actions,
  mutations
});

▼親コンポーネント(App.vue)

<template>
  <div>
    <p>{{ count }}</p>
    <increment-btn />
    <decrement-btn />
  </div>
</template>

<script lang="ts">
import { computed, defineComponent } from "vue";
import { useStore } from "vuex";
import IncrementBtn from "./components/IncrementBtn.vue";
import DecrementBtn from "./components/DecrementBtn.vue";

export default defineComponent({
  components: {
    IncrementBtn,
    DecrementBtn,
  },
  setup() {
    const counter = useStore();
    const count = computed(() => counter.state.count);

    return {
      count,
    };
  },
});
</script>

▼子コンポーネント(DecrementBtn.vue)

<template>
  <button @click="decrement">+</button>
</template>
<script>
import { defineComponent } from "vue";
import { useStore } from "vuex";

export default defineComponent({
  setup() {
    const counter = useStore();
    return {
      decrement: () => counter.dispatch("decrement"),
    };
  },
});
</script>

実際の挙動の確認はこちら↓

codesandbox.io

簡単なアプリケーションということもありますが、コンポーネント間のストアへのアクセス方法や、データフローなど非常に明示的であることがわかるかと思います。

Provide / inject

Provide / injectは、これまでコンポーネントの階層の深さに関係なく、親コンポーネントから子階層へ依存関係を提供するプロバイダとして機能します。
これまでは親コンポーネントから子コンポーネントにデータを渡す際、propsを使用していたかと思います。
しかし、階層が深くなるにつれバケツリレーのように伝搬していくのは非常に面倒でした。

components_provide.png

v3.ja.vuejs.org

こちらはVue2.2から実装されているものでしたが、Vue3.0からはComposition APIのリリースに伴い注目されています。

Composition API

Composition APIは、ロジックをカプセル化することでコンポーネント間でのロジックの再利用を可能にするAPIです。
特に大規模なアプリケーションになると、Vue2.xまではViewとロジックが密結合になっており、SFCが冗長になってしまうことが問題でした。
しかし、Composition APIを活用することでSFCにはViewに関することのみを記述し、ロジックを外部に切り出すようなことも可能になります。

実際のコード

基本的なディレクトリ構成は変わらず、以下のような形。

/src
 ├ components
 │  ├ CountView.vue
 │  ├ DecrementBtn.vue
 │  ├ ParentProvider.vue
 │  └ IncrementBtn.vue
 ├ store
 │  └ index.ts
 │  └ key.ts
 └ App.vue

コンポーネント内でしていた状態管理の部分を切り抜き、ストアを作成しキーを定義します。

▼index.ts

import { reactive } from "vue";
export default function counterStore() {
  const state = reactive({
    count: 0
  });
  return {
    get count() {
      return state.count;
    },
    increment() {
      state.count += 1;
    },
    decrement() {
      state.count -= 1;
    }
  };
}
export type CounterStore = ReturnType<typeof counterStore>;

▼key.ts

import { InjectionKey } from "vue";
import { CounterStore } from "./index";

const CounterKey: InjectionKey<CounterStore> = Symbol("counterStore");
export default CounterKey;

provideは( key , value )を受け取り、provideされた値はコンポーネントからキーを用いてinjectを取り出せます。

▼親コンポーネント(ParentProvider.vue)

<template>
  <div>
    <slot />
  </div>
</template>

<script lang="ts">
import { provide } from "vue";
import counterStore from "../store/index";
import CounterKey from "../store/key";

export default {
  setup() {
    provide(CounterKey, counterStore());
    return {};
  },
};
</script>

これでParentProvider.vue以下のコンポーネントはどの階層においてもストアオブジェクトを受け取ることが可能です。

▼子コンポーネント(CountView.vue)

<template>
  <p>{{ count }}</p>
</template>
<script>
import { defineComponent, inject, computed } from "vue";
import CounterKey from "../store/key";

export default defineComponent({
  setup() {
    const counter = inject(CounterKey);
    const count = computed(() => counter.count);
    return {
      count,
    };
  },
});
</script>

ストアオブジェクトを受け取る側はinjectにキーを指定します。同様の記述でボタン関係も実装します。
最終的には以下のようにまとめることでVuexやSFC1つで記述していたものと同様の動きをします。

▼App.vue

<template>
  <div>
    <parent-provider>
      <count-view />
      <increment-btn />
      <decrement-btn />
    </parent-provider>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import ParentProvider from "./components/ParentProvider.vue";
import CountView from "./components/CountView.vue";
import IncrementBtn from "./components/IncrementBtn.vue";
import DecrementBtn from "./components/DecrementBtn.vue";

export default defineComponent({
  components: {
    ParentProvider,
    CountView,
    IncrementBtn,
    DecrementBtn,
  },
  setup() {
    return {};
  },
});
</script>

実際の挙動の確認はこちら↓

codesandbox.io

まとめ

provideは、今回のようなルートコンポーネント以外のコンポーネントからも使用することができます。
つまり、限定的な範囲でストアを定義することが可能となります。
Vuexではグローバルに定義する必要があったため、プロジェクトの規模によってはスコープの小さい状態管理ができるProvide / injectを使用するのもいいかもしれません。
一方で、Vuexは、Fluxでデータフローなどが決まっているため、チーム内でのガイドライン等を作る手間が少なく、さらにDevToolでのデバックができることも大きな利点かと思います。
チームとしてComposition APIを使用し、かつ規模の小さいプロジェクトの場合Provide / injectを使用し、大規模なプロジェクトの場合Vuexを使う。そんな選択が今後できるかと思います。

参考にさせていただいた記事等


最後になりますが、Wizではエンジニアを募集中です!

興味のある方は是非覗いてみてください↓

careers.012grp.co.jp

Laravelで既存画像のWebP対応

今回はPHPのLaravelで既存画像のWebP化及び切替表示について実装例を紹介してみようと思います。

目的

記事の画像をWebP化することによって記事の表示スピードを早くする。

WebP(うぇっぴー)とは

軽量の新画像形式で開発したグーグルによると、画像をWebP化することによって可逆形式ではpngの26%、不可逆形式では25〜34%もファイルサイズを減らすことができると紹介されています。 詳細については[こちら]をご覧ください。

Chromeブラウザのみの対応だったWebP 画像が Edge, Firefox 等の最新版で使用でき、8割近いブラウザで使えるようになってきたそうです。 WebP対応ブラウザ確認は[こちら]をご覧ください。

たくさん画像を使う記事サイトで高速化のためにWebPを使うことが有効ですが、サポートしないブラウザもあるのでWebP対応ブラウザはWebPを、非対応ブラウザは既存画像(jpegpng等)を利用するなど切替表示の必要があります。

現状

1) 画像ファイルURL

画像ファイルURLは、認証制御の理由で実際のファイルURLを利用しないで下記のようにAPIで取得します。

src="https://xxxxx.jp/api/image/1"
2) 画像ファイルの情報

画像ファイルの情報は下記のようにDBのテーブルに格納しています。

id name context_type created_at
1 image01.jpg image/jpeg 2018-02-08 16:13:38
2 image01.png image/png 2018-02-08 16:13:59

App\Models\ImageFileには上記テーブルの項目が入っています。

実装手順

1) 画像の新規登録&変更登録時対応

管理画面で画像の新規登録&変更登録する際に、アップロードされた画像からWebP画像を作成し別途保存する処理を追加

2) 画像表示の切替処理

サイトで画像を表示する際に、WebP対応ブラウザかどうか判定してWebP対応ならWebP画像、非対応なら既存の画像タイプを表示するように切替処理を.htaccessで設定

3) 既存画像の対応

バッチで一括で既存画像をWebp化する仕組みを作る

前提条件

1) libwebpのインストール

下記のコマンドで

$ php -r 'print_r(gd_info());'

下記が表示される場合、サポートされていないのでlibwebpライブラリをインストールする必要があります。

[WebP Support] => 0 
或は
[WebP Support] => 

下記のコマンドでlibwebpをインストール

$ sudo yum --enablerepo=remi-php73 install php73-gd

下記のコマンドでapache再起動

$ service httpd reload

下記のコマンドで確認

$ php -r 'print_r(gd_info());'

下記が表示されればWebPがサポートされていると考えられます。

[WebP Support] => 1

2) LaravelにIntervention Imageを追加

Composer Installation

$ php composer.phar require intervention/image

config/app.phpにパッケージを登録

'providers' => [
    Intervention\Image\ImageServiceProvider::class,
]

'aliases' => [
    'Image' => Intervention\Image\Facades\Image::class,
]

下記のコマンドで、configフォルダ内にimage.phpという設定ファイルが作成されます。

Laravelでconfigファイルを公開(作成)する場合

$ php artisan vendor:publish --provider="Intervention\Image\ImageServiceProviderLaravelRecent"

Laravelのバージョンが4以下の場合

$ php artisan config:publish intervention/image

これでLaravelでのIntervention Imageインストールは完了です。

              

1. 画像の新規&変更登録時対応

ルート

// 画像アップロード
Route::post('/image', 'ImageController@uploadImage');
// 画像取得
Route::get('/image/{id}', 'ImageController@getImage);

blade

Laravel:
{{ Form::file('image') }}

html:
<input name="image" type="file">

Controller

ImageController.php

<?php 
use App\Models\ImageFile;
use Intervention\Image\ImageManagerStatic as Image;

//画像アップロード
public function uploadImage(Request $request) {
        //  画像情報を取得
        $file = $request->file('image');
        
         // 画像情報をDBに保存
        $ImageFile = new ImageFile();
        $ImageFile->name            = $file->getClientOriginalName();
        $ImageFile->context_type  = $file->getMimeType();
        $ImageFile->created_at     = Carbon::now();
        $ImageFile->save();

        // storageフォルダ以下に画像を保存(物理的なファイルのURLがないパターン)
        $file->move(storage_path(config('system.path.imgae')), $ImageFile->id);

        // 画像を読み込む: make()
        $path = storage_path(config('system.path.imgae')).$ImageFile->id;
        $image = \Image::make($path);

        // WebPを作成
        $webp_path = storage_path(config('system.path.imgae').$ImageFile->id .'.webp');
        $image->save($webp_path);
}

2. 画像表示の切替処理

.htaccess処理

.htaccessで画像取得のURLが /image/{id}の場合、/image/{id}?webp=1にリダイレクトさせることによって、 ブラウザによって自動的に切り替えできるようにします。

<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews
    </IfModule>

    RewriteEngine On

    RewriteCond %{HTTP_ACCEPT} image/webp
    RewriteCond %{REQUEST_URI} ^/image/[0-9]*$
    RewriteCond %{QUERY_STRING} !webp=[^&]*
    RewriteRule ^(.*)$ $1?webp=1 [L]
</IfModule>

.htaccessはできることが多いので良かったら[こちら]も参考に見てください。

画像取得処理

.httaccessのリダイレクトでURLに追加した?webp=1を取得し、対応ブラウザかWebPだったらWebPに切り替えるようにします。

ImageController.php

<?php
use App\Models\ImageFile;
use Intervention\Image\ImageManagerStatic as Image;

public function getImage(Request $request, $id) {
    
    // ファイル情報を取得
    $imageFile = ImageFile::find($id);
    if (!$imageFile) {
        return;
    }

    $contextType = $imageFile->context_type;
    $fileName = $imageFile->id;
    
    // 対応ブラウザかWebPだったらWebPに切り替える
    $webp = $request->input('webp');
    if($webp == true){
        $contextType = 'image/webp';
        $fileName = $imageFile->id . '.webp';
    }

    // ファイルをダウンロードさせる
    $headers = array('Content-Type' => $contextType, 'Content-Disposition' => 'inline; filename="'.$imageFile->name.'"');
    $pathToFile = storage_path(config('system.path.imgae')).$fileName;
    $response   = response()->file($pathToFile, $headers);

    return $response;
}

3. 既存画像のWebp化対応

バッチで一括で記事既存画像のWebp化の仕組みを作ります。

app/Console/Kernel.php

<?php
use App\Console\Commands\CreateWebpFile;
class Kernel extends ConsoleKernel
{
    protected $commands = [
        CreateWebpFile::class,
    ];
}

下記のコマンドでクラスを作成

$ php artisan make:command CreateWebpFile

app/Console/Commands/CreateWebpFile.php

<?php
namespace App\Console\Commands;
use Illuminate\Support\Facades\DB;
use Intervention\Image\ImageManagerStatic as Image;

class Kernel extends ConsoleKernel
{
    protected $signature = 'CreateWebpFile';
    
     public function handle()
    {
        $imgaeFile= DB::table('imgae_file')->get();
        if (!$imgaeFile) {
            return;
        }

        foreach ($imgaeFile as $image) {
            // id取得
            $imageId = $image->id;

            $Path = storage_path(config('system.path.imgae')) . $imageId;
            $PathWebp = $Path . '.webp';

            if(file_exists($Path) && !file_exists($PathWebp)){
                // webp保存
                $image = Image::make($Path)->orientate();
                $image->save($PathWebp);
            }
        }
    }
}

プロジェクトディレクトリに入って、下記のコマンドでWebPファイルが作成されました。

$ php artisan CreateWebpFile

さいごに

バックエンドエンジニアとしてWizに入社後、アプリケーション実装だけでなくSEO対策、SSL更新、メールサーバー構築、WEBサーバー構築など幅広く挑戦・成長できるチャンスをたくさんもらいました。

Wizではエンジニアを募集しております。 興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

PHPのInterface -メリットと使い所-

今回はPHPのInterfaceの使い所について、例を交えつつ紹介してみようと思います。

本題に入る前にこの記事内のサンプルコードについてです。
サンプルコードは全て、フレームワークにLaravelを使用しているという前提で記述しています。
LaravelでInterfaceを使う場合、サービスプロバイダーのregisterメソッド内でbindメソッドを使用し、実装クラスとInterfaceの紐付けを行ってください。

$this->app->bind(XxxInterface::class, ConcreteClass::class);

これがないと"XxxInterface is not instantiable..."みたいなエラーが出てしまいますので、ご注意ください。

Interfaceの概要

PHPの公式ドキュメントを確認すると、以下のように説明されています。

オブジェクトインターフェイスにより、あるクラスが実装する必要があるメソッドの 種類を、これらのメソッドの実装を定義することなく、指定するコードを作成できる ようになります。

インターフェイスは通常のクラスと同様に定義することができますが、 キーワード class のかわりに interface を用います。またメソッドの実装は全く定義されません。

インターフェイス内で宣言される全てのメソッドはpublicである必要があります。 これは、インターフェイスの特性によります。 https://www.php.net/manual/ja/language.oop5.interfaces.php

Interfaceを使うメリット

"php interface メリット"などのキーワードでググるといろいろ出てきますが、大きなメリットの一つとして

ということがあげられます。
Interfaceを使うと、複数のクラスがお互いに直接依存し合わず、Interfaceに依存している状態を作ることができます。

どういう状態なのか、以下の例1をご覧ください。

例1

  • メルマガを配信するような機能があったとする
  • 外部のメールサービスを使っている
  • メールサービスのAPI呼び出し用にライブラリが提供されており、それを使って機能を実装している

という状態があるとします。
サンプルコードで書いてみると、こういう感じです。

ある時、使用しているメールサービスを変更するような仕様変更があったとします。
その場合、上記のようにInterfaceを使った実装をしていれば、変更の影響範囲をEmailLibServiceのみに抑えることができます。

コントローラーのemailMagazineメソッドでは、EmailLibInterfaceのsendというメソッドを呼び出しています。
EmailLibServiceのsendメソッドを直接使用していません。
なので使用するメールサービスが変わった場合にEmailLibServiceの中身を書き換えようが、新しくクラスを作って差し替えようが、EmailLibInterfaceさえimplementsされていれば呼び出し元に影響が及ぶことはありません。

例えばコントローラーからライブラリのクラスを直接newして使用していたり、あるいはEmailLibServiceのメソッドを直接呼び出していたとすると、コントローラーにも影響が及んでいたかもしれません。

Abstract classでもいいのでは?

Abstract classについてはこちらをご覧ください。

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

InterfaceとAbstract classは

  • 抽象メソッドを定義できる
  • 定義されたメソッドを継承先のクラス(Interfaceは実装クラス)で必ず実装しなければならない
  • そのままでは使えない(直接newしてインスタンスを作成したりできない)

などの点で似ています。しかし

  • Interfaceのメソッドは全てpublicでなければならない
  • Abstract classはInterfaceと違って、メソッドに具体的な実装内容を持たせることができる

などの違いがあります。
なのでAbstract classを使えば共通処理を具体的な実装部分まで定義してしまって、動きが変わる部分のみ抽象メソッドで定義するようなことが可能です。
Abstract classの方ができることが多く自由度が高いので、Interfaceを使う必要がなさそうに思えます。
しかし、具体的な処理を持った共通メソッドに変更を加える必要がある場合、継承先クラス全てに影響が及んでしまうというデメリットがあります。
そう考えるとInterfaceの場合は元々実装内容を持たないので、変更に強いと考えられます。
振る舞いが異なるという前提で同じメソッドを不特定多数のクラスに持たせたい場合は、Interfaceを使用するメリットがあるのではないでしょうか。
ということを踏まえた上で、2つ目の例です。

例2

例えばユーザーに何かしらの通知を送るようなことがあったとして

  • パラメーターによってどういう通知を送信するかを変えたい
  • 通知のフォーマットや通知に必要な要素はバラバラ

みたいな条件を満たして実装したい場合、以下のような使い方ができます。

(参考: https://youtu.be/cUV1nXPfjFY)

Interfaceは複数implementsできる

もう一つAbstract classとInterfaceの特徴的な違いは、Interfaceは複数implementsできるという点です。

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

PHPは多重継承ができない仕様です。Abstract classを使用する場合、あくまでもクラスの継承なので1つのAbstract classしか継承できません。対してInterfaceに個数の縛りはありません。カンマ区切りで複数implementsが可能です。

例えば例2で "ある通知クラスの場合は管理者にも同時に通知を送りたい" みたいな場合、以下のような形で複数implementsすると便利なのではないでしょうか。

↑のように空のInterfaceを作成して、自作の型のように手軽に扱えるのもInterfaceのいいところだと思います。

さいごに

Wizではエンジニアを募集しております。 興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

Next.js10新機能 next/imageを使ってみた

こんにちは、フロントエンドエンジニアの松尾です。

2020年10月27日にNext.js10がリリースされ、パフォーマンスを改善するための20個を超える新しい機能が追加されました。

nextjs.org

私たちフロントエンドチームでも、プロジェクトにNext.js10を導入しており様々な恩恵を得ています。

本記事では、その中でも最も便利だと感じたnext/imageについてご紹介したいと思います。

next/imageとは

next/imageとは、画像の表示を自動で最適化してくれるコンポーネントです。

主に以下の最適化を行います。

  1. WebPフォーマット*1変換(対応ブラウザのみ)
  2. 画像遅延読み込み(Lazy Load)
  3. レスポンシブ対応

導入方法

Next.jsにデフォルトで備わっているため、next/imageをimportするだけで使用することができます。

基本的に<img>要素の書き方と変わりませんが、width・heightの記述は必須ですので注意が必要です。

import Image from "next/image"
~~~
<Image src="/free.jpg" width={600} height={400} />

画像比較

では実際に<img>next/imageの比較をし、どのように画像の最適化が行われているか確認してみましょう。

比較対象として、以下のフリー画像を使用します。(free.jpg 843kb)

林道

▼ コード

import Image from "next/image"

const ImageArea = () => {
  return (
    <>
      // 通常のimg
      <img src="/free.jpg" width={600} height={400} alt="通常のimg 林道"/>
      // next/imageのコンポーネント
      <Image src="/free.jpg" width={600} height={400} alt="next/imageのコンポーネント 林道"/>
    </>
  )
}

export default ImageArea

表示を見てみると、同じ見た目の画像が並んでいることが確認できます。

林道画像比較

こちらは最終的に吐き出されたnext/imageのHTMLです。

next/imageのHTML

src/srcset属性を見てみると、/_next/imageを参照していることが確認できます。

/_next/imageビルド時に生成される画像サーバーでして、こちらにパラメータを送ることで表示させる画像の切り替えを行っています。

以下は開発者ツールの画像読み込み箇所です。

開発者ツールの画像読み込み箇所

next/imageではWebPフォーマットでの表示となっており、ファイルサイズが70%程削減されています。

様々な画像で試してみたのですが、元々のファイルサイズが大きい画像ほど圧縮率が高い傾向があるようです。

次にレスポンシブ対応を確認してみましょう。

以下のように、next/imageの画像のみレスポンシブが適用されていることが確認できます。


width・heightの直接的な指定は、レイアウトまわりで色々と問題を引き起こす原因となるため避けがちです。

しかし、next/imageではアスペクト比に基づいて自動的にレスポンシブ対応となるため問題なく指定することができます。

最後に遅延読み込みによる最適化です。

左の開発者ツールを見てみると、画像から200px程近づいた時に読み込みを行っていることが確認できます。


このように、next/imageでは「サイズ最適化」「遅延読み込み」「レスポンシブ対応」の最適化を全て自動で行ってくれます。

外部ファイルの読み込み

外部サーバーから画像を読み込む場合、そのままURLを指定するとエラーが出てしまいます。

next.config.jsに以下の設定を追記することで、Next.js側がURLを認識し画像の読み込みをしてくれます。

module.exports = {
  images: {
    domains: ["example.com"], //ドメインを指定
  },
}

オプション引数

next/imageでは豊富なオプション引数が存在しています。

以下はオプション引数の設定例です。

<Image
  src={"/free.jpg"}           // 画像へのパスまたはURL ※必須
  width={600}                 // 画像の横幅 ※必須
  height={400}                // 画像の高さ ※必須
  layout={"fill"}             // レイアウト, "fill" | "fixed" | "intrinsic" | "responsive", default:"intrinsic"
  sizes={"80vw"}              // メディアクエリのマッピングサイズ, default:"100vh"
  quality={50}                // 画質, default:100
  priority={true}             // 表示優先度, default:false
  loading={"lazy"}            // 遅延読み込み, "lazy" | "eager", default:"lazy"
  unoptimized={false}         // 画像最適化, default:true
  objectFit={"cover"}         // object-fit ※layout='fill'の場合
  objectPosition={"50% 50%;"} // object-position ※layout='fill'の場合
/>

対象の画像に合わせて柔軟に設定できることもnext/imageの魅力の一つです。

それぞれの引数の詳細は下記の公式ドキュメントに記載されてますので、興味のある方は見てみてください。

nextjs.org

まとめ

以上next/imageの導入方法・最適化についての説明でした。

画像はWebページのトータルバイト数のうち50%を占めていると言われてますので、next/imageを積極的に活用しパフォーマンスを改善していきましょう!

最後になりますが、Wizではエンジニアを募集中です!

興味のある方は是非覗いてみてください↓

careers.012grp.co.jp

*1:Google開発の画像フォーマット。ファイルサイズを大幅に軽量化できる。

Lambda@EdgeでCloudFrontの配信をコントロールしたい

Lambda@Edgeとは

CloudFrontのエッジサーバでコードを実行するLambda関数です。

配信をカスタマイズ出来ます。

感覚としてはCloudFrontのフックで動くLambda、という感じですが考慮する点・制約がいくつかあります。

  • 番号付きバージョンのみをトリガーに設定出来る(エイリアスに設定出来ない)
  • バージニア北部(us-east-1)リージョンのみ

詳しくはドキュメント

設定出来るフック

f:id:wiz-yoshitomi:20210126161055j:plain

Lambda@Edgeがフックに設定出来る箇所は四つあります。

  • ビューワーリクエス
  • ビューワーレスポンス
  • オリジンリクエス
  • オリジンレスポンス

キャッシュがある時はオリジンリクエスト・オリジンレスポンスは発生しません。

例:webp画像を配信して表示速度高速化したい

  1. リクエストした画像(jpeg,png)のwebpファイルが存在したらwebpファイルを取得する
  2. 存在しない場合はwebpファイルを作成してから取得する

このようなLambda@Edgeを作成し、オリジンリクエストに設定してみます。

前提:S3+CloudFrontの構成は既に作成されているものとする。

Lambdaを用いたS3画像の取得・加工はドキュメントを参考にしています。

①IAMロールを作成

アクセス許可

AWSLambdaExecuteをアタッチ

信頼関係

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

② Lambda関数を作成

最初に述べた制約に則り関数を作成します。 今回の設定は以下です。

リージョン:us-east-1 ランタイム言語:Node.js 12.x 実行ロール:前項目で作成したロールを設定

③関数コードを作成

index.jsファイルを作成

const AWS = require('aws-sdk');
const sharp = require('sharp');
const https = require('https');
const url = require('url');
const s3 = new AWS.S3();

const BASE_URL = 'CloudFrontURL';
const BUCKET_NAME = 'S3バケット名';

exports.handler = async (event, context) => {
    const request = event.Records[0].cf.request;
    
    // 画像のみ
    if (request.uri.match(/\.(jpe?g|png)$/) && request.uri.split[1] !== 'tmp') {

        const webpUri = request.uri + '.webp';
        let cloudfrontReq = url.parse(BASE_URL + webpUri);
        let webpExits = false;

        // webpファイルへ正常にアクセス出来るか
        await new Promise((resolve, reject) => {
            cloudfrontReq.method = 'HEAD';
            cloudfrontReq.timeout = 3000;
            https.request(cloudfrontReq, cloudfrontres => {
                if (cloudfrontres.statusCode >= 200 && cloudfrontres.statusCode < 300)
                    webpExits = true;

                resolve();
            }).on('error', reject).end();
        });

        // webpファイルが無い場合は作成する
        if (!webpExits) {

            const key = request.uri.substr(1)
            const webpKey = webpUri.substr(1)

            // オリジンの画像を取得
            try {
                const params = {
                    Bucket: BUCKET_NAME,
                    Key: key
                };
                let origimage = await s3.getObject(params).promise();

            } catch (error) {
                console.log(error);
                return;
            }

            // webpへ変換
            try {
                let buffer = await sharp(origimage.Body).webp().toBuffer();

            } catch (error) {
                console.log(error);
                return;
            }
            
            // S3へput
            try {
                const putParams = {
                    Bucket: BUCKET_NAME,
                    Key: webpKey,
                    Body: buffer,
                    ContentType: "image"
                };

                const putResult = await s3.putObject(putParams).promise();
                webpExits = true;
            } catch (error) {
                console.log(error);
                return;
            }
        }
        
        // webpが存在する or 変換が成功した場合webpのuriでrequestを上書きする
        if (webpExits)
            request.uri = webpUri;
    }

    return request;
};

④デプロイパッケージを作成

convert-webpフォルダ(名前は任意)を作成し、そこにindex.jsを入れ、そこにnpmでsharpライブラリをインストールします。

$ npm install --arch=x64 --platform=linux --target=12.13.0 sharp

今回の場合のファイル構造は、以下のようになります。

convert-webp
├ index.js
└ node_modules
    ├ sharp
    ├ ...

⑤.zipファイルをアップロード

作成したファイルをzipへ圧縮し、「関数コード」→「アクション」→「.zipファイルをアップロード」の手順でアップロードを行います。

⑥テストを行ってみる

こちらのドキュメントより、オリジンリクエストのイベント構造を確認出来ます。

⑦ Lambda@EdgeをデプロイしCloudFrontへ設定する

「デザイナー」→「トリガーの追加」をクリックします。

「トリガーを選択」→「CloudFront」で出現する「Lambda@Edgeへのデプロイ」項目を選択します。

f:id:wiz-yoshitomi:20210126181909p:plain

以上を入力し「デプロイ」をクリックで設定完了です。

⑧確認

DevToolを開き「Networkタブ」→対象の画像を選択

Responce headerのcontent-typeがimage/webpになっていればOKです!

注意点なのですが今回はオリジンリクエストにフックしているため、キャッシュが残っている状態だとオリジンリクエストが発生せず関数が発火しません。

確認の際はご注意ください。

まとめ

今回は省略していますが、CloudFrontのheaderのホワイトリストAcceptを追加し、webpを許可しているブラウザかの確認の処理があった方が良いかと思います。

Lambda@Edgeを使用せずにS3へのオブジェクト追加をフックにしたLambdaでwebpファイルを生成するという方法も考えられますが、同一バケットの「オブジェクト生成」フックで新規オブジェクト追加する構成は再起的に実行されてしまう危険性があるため推奨されていません。

やはりキャッシュがない時(オリジンリクエストが発生する時)の起動は体感でも遅く感じるので、ユースケースに合わせてキャッシュの有効期限を長くしたりなど対処した方が良いかなと思いました。

最近話題のLambdaのコンテナイメージフォーマットもどうやら使えるようなので、こちらも試してローカルまで一貫したサクサク開発にしてみたいです。

〜最後に〜

Wizではエンジニアを募集中です!

興味のある方は是非ご覧ください!

careers.012grp.co.jp

Snowpack 試してみました

皆さんこんにちは、フロントエンドエンジニアの高野です。

皆さんはJSのバンドラーは何を使われていますか?

私たちフロントエンドチームでは主にwebpack、一部browserifyを使用しています。

最近はReactやVueをブイブイ使いビジネスロジックをもつことも多くなっており、そのためJSのバンドルサイズは嵩む一方です。

そうなってくると気になるのはビルド時間です。

私の担当しているプロジェクトでもwebpackのビルドが34秒かかっており、今後さらにプロジェクトが大きくなれば1分越えも夢ではないです...

そこで、Snowpack v3をにチャレンジしてみました。

Snowpackとは

Snowpackは初回(v1.0.1)リリースが1年前と、比較的新しいフロントエンドのビルドツールです。

SnowpackはPikaが中心で開発しており、この団体は今よりもwebを90%早くする!!という大きな目標を掲げていて、その一環としてのプロジェクトがSnowpackのようです。

また、PikaはSkypackも開発しています。

www.pika.dev  

Pikaのaboutページに書かれている内容を一部抜粋します。

ES module syntax (ESM) is JavaScript's latest native module system. Officially ratified in 2015, its import / export syntax is more compact, more easily analyzed and more reliably optimized. This all results in smaller, faster JavaScript on the web.

Unfortunately, most sites still generate a single "oldest browser" bundle, which means that every user (even the majority using modern browsers) get over-compiled JavaScript bundles that don't support ESM.

Pika is working to build better tools and services that make modern, ESM-focused JavaScript more accessible to package authors and application developers.

 

deepL訳+細かい部分微調整したもの

ESモジュール構文(ESM)は、JavaScriptの最新のネイティブモジュールシステムです。2015年に正式に批准されたこのESMのimport/export構文は、よりコンパクトで、より解析が容易で、より確実に最適化されています。これにより、Web上でのJavaScriptの軽量化、高速化が実現します。

残念ながら、ほとんどのサイトではいまだに単一の「一番古いブラウザ用」バンドルが生成されており、これはすべてのユーザー(モダンブラウザを使用している大多数のユーザーでさえも)がESMをサポートしていない無駄にコンパイルされたJavaScriptバンドルを使用しなければならないことを意味します。

Pikaは、ESMに焦点を当てた最新のJavaScriptを、パッケージ作成者やアプリケーション開発者がより利用しやすいものにする、より良いツールやサービスの構築に取り組んでいます。

ということで、SnowpackもESMをフル活用する仕組みになっています。

ESMについて

ご存知の方はこちらの項目は読み飛ばしていただけたらと思います。

元々JSにはモジュールという仕様がなかったのですが、JSで中〜大規模な開発が行われ始めると、その必要性からモジュールの仕様がいくつか誕生しました。(CommonJS, AMDなど)

そしてさらに時は流れ、ES2015でESModule(略称ESM)という仕様が策定されました。

仕様が乱立していると色々と不便ですので、公式が仕様を定めてくれるのはありがたいことです。

当然ESModuleで一本化しようという流れになっています。

...IE(とその他レガシーブラウザ)以外は。

f:id:iricocco:20210119135256p:plain

 

つまり、ESMをフル活用する場合のSnowpackでは、IE11は切るしかありません。
(Snowpackでバンドルしてしまう方法もあります。)

JSモジュールの仕様についての歴史は、下記の記事がとても良くまとまっていたので紹介させていただきます。

uuuundefined.tokyo

最近のJSビルドツール事情

f:id:iricocco:20210119152900p:plain

www.npmtrends.com

npm trendsで確認すると、webpackの一人勝ちといった様子です。

Snowpackは新参者ということもあり、競争上はまだまだと言えます。

また、 2020年のstateofjsにおけるUsage(使用ランキング)でもwebpackはぶっちぎりです。

f:id:iricocco:20210119154403p:plain

2020.stateofjs.com

しかし特筆すべきなのは、満足度/関心で今熱いのはesbuildとSnowpackということです。

実は、何がそんなにユーザーを惹きつけているのか気になり、この記事を書くに至りました。

【Satisfaction: 満足度】

f:id:iricocco:20210119154527p:plain

【Interest: 関心】

f:id:iricocco:20210119154510p:plain

Snowpackの仕組み

通常設定では、Snowpackはバンドルを行いません。

先ほど説明したESMありきの構成なので、実際のバンドルはブラウザ標準のESMの方で行います。

これにより、従来のバンドルされた単一JSファイルでは出来ないことができるようになっています。

・全てを再ビルドする必要がない

単一JSバンドルの場合、たとえ1ファイルの一行を更新しただけだとしても、全てを再ビルドする必要があります。

しかし、ESMを利用したSnowpackであれば更新のあったファイルのみを更新し、その他はキャッシュを利用することができます。  

・自然にコードスプリッティングされる

依存パッケージが多数ある場合は、単一のJSだと初期ロードがとても重くなってしまいます。

それを解決するためにコードスプリッティングを行いますが、Snowpackだと依存ファイルはブラウザでバンドルされますので、自然にファイルが分割されます。

また、それぞれのビルドファイルは個別に作成され、無期限にキャッシュされます。  

そうはいってもnpm公開パッケージの中にはCommonJSというESM仕様でないものも多いです。

では、一つでもESMに準拠していないパッケージがあった場合は、Snowpack式を諦めてバンドルしなければいけないのでしょうか?

 

実はそうではなくて、Snowpackはそのようなパッケージを個別に処理してくれます。

一つ一つの依存関係のファイルを個別にバンドルし(例えばreact なら react.js、 react-domなら react-dom.js)、それをimportするというのです!

依存ファイルは滅多に更新されないので、こういったバンドル作業もほとんど発生しません。

 より詳しく知りたい方は公式の「How Snowpack Works」を参照ください。

www.snowpack.dev

また、小規模であればブラウザのESMによるバンドルでも問題ありませんが、大規模化してくるとやはりバンドルでの最適化が必要になってきます。

Snowpackではバンドルを行うように切り替えることも可能です。

www.snowpack.dev

Snowpackのビルドをめっちゃ早くできる話

実は2021年1月13日にSnowpack v3が公開されました🎉

このアップデートにより内部のバンドルツールにesbuildを採用できるようになりました。

esbuild.github.io

esbuildはGoで記述されたビルドツールで、本当に魔法のように早いのが売りです。

実際ベンチマークテストでは、他のツールより10~100倍早い結果となっています。

f:id:iricocco:20210119171739p:plain

私自身もesbuild単体でどれくらいスピードが上がるかテストしました。 下記がtimeコマンドで計測した結果です。

【Webpack】
real 0m4.018s 
user 0m3.989s
sys 0m0.585s

【esbuild】
real 0m0.451s
user 0m0.494s
sys 0m0.090s

real・・・プログラムの呼びだし~終了までの時間
user・・・プログラム自体の処理時間
sys・・・プログラムを処理するために、OSが処理をした時間

軽いプロジェクトで試したためなのか100倍は行きませんでした...が、

あきらかにスピードアップしています。

とは言っても、esbuildはまだバージョンがv0.8.33なので、これから成長していくプロジェクトです。

Snowpackがesbuildを採用できたのは、buildありきでバンドルはその後の最適化であるという設計に基づいていたからだと語られていました。

www.snowpack.dev

これからの成長に目が離せません。

Snowpack曰く、

esbuild is still a young project, but it’s future looks promising. In the meantime, we will also continue to invest in the existing Webpack & Rollup bundler plugins for a long time to come.

deepL訳+微調整

esbuildはまだ若いプロジェクトですが、将来性が見込めます。その間、既存のWebpack&Rollupバンドルプラグインへの投資も今後も長く続けていきます。

とのことです💰

Snowpackを実際に使ってみる

Snowpackに対するモチベーションが上がったところで、実際にReactのプロジェクトを下記のチュートリアルを参考にビルドしてみます。

www.snowpack.dev

 

まずは、craeate-snowpack-appコマンドを用いてアプリの土台を作成します。

テンプレートには今回使用するminimalのみでなく、lit-elementreactsveltevueなどが準備されています。

snowpack/create-snowpack-app/cli at main · snowpackjs/snowpack · GitHub

npx create-snowpack-app react-snowpack --template @snowpack/app-template-minimal

react-snowpackは作成するフォルダ名です。

--template @snowpack/app-template-minimal オプションをつけるとminimalというテンプレートが適用され、今回のチュートリアル用のシンプルな状態で構築してくれます。

 

下記コマンドでローカルサーバーを起動しましょう。

cd react-snowpack
npm run start

localhost:8080にて下記の画面が表示されればOKです。

f:id:iricocco:20210119161107p:plain

下記コマンドで依存パッケージをインストールします。

npm install react react-dom --save

Snowpackは.jsxextensionを見つけるとJSXをサポートしてくれます。

なので、index.jsindex.jsxにリネームします。

index.htmlのscriptタグのindex.jsはリネームする必要はありません。

なぜならコンパイル後は.js形式に変換してくれるからです。

 mv index.js index.jsx

index.htmlにidを振ったdivを配置し、reactを動かします。

-  <h1>Welcome to Snowpack!</h1>
+  <div id="root"></div>
- console.log('Hello World! You did it! Welcome to Snowpack :D');
+ import React from 'react'
+ import ReactDOM from 'react-dom'
+ ReactDOM.render(<div>"HELLO REACT"</div>, document.getElementById('root'))

 f:id:iricocco:20210119170449p:plain 無事マウントできました。

ビルドするためにほとんど設定ファイルを書かずに済んだのがわかるかと思います。

本記事ではここまでとしますが、公式チュートアルではHMRやFast Refresh、ファイル構成の変更などのやり方を学ぶことができるので、興味のある方はご確認ください。

Snowpack 3.0 で追加されたStreaming Imports

先日リリースされたver 3ですが、esbuildが組み込まれた以外にもStreaming Importsという面白い機能が追加されていました。

npm installしなくても、依存パッケージ使えるようにしちゃおうよ!

というとんでもない機能です。

import * as React from 'react'; //これを

import * as React from 'https://cdn.skypack.dev/react@17.0.1'; // こう解釈しちゃう

importを実行するとinstall済みのnode_modulesではなく、Skypack配信のCDNを読み込みに行く動作をします。

👨‍🦰「待て待て、そんなことしたらオフラインで動作できないじゃないか!」

と思われるかもしれませんが、一度ロードしたデータはキャッシュしますので、2回目以降はオフラインでも動作します。

かなり先進的な機能ですが、オプトイン式なので、使いたい人だけ使用することができます。

installしなくていい未来など想像していなかったので、個人的にとても衝撃的でした。

まとめ

シェアなどを見ると今すぐ採用するのはまだためらわれるところですが、非常に未来を感じるプロジェクトだと思いました!

今後も伸びていく予感がしています!

動向をチェックしつつ、機会があれば担当のプロジェクトに導入していきたいと思っています。

最後に...

Wizではエンジニアを募集中です。

興味のある方は是非覗いてみてください!↓

careers.012grp.co.jp

Laravel8 使ってみた。

Laravel7との違い

①ルートファイルの書き方

Laravel7では以下の様な記述でしたが、

<?php

use Illuminate\Support\Facades\Route;

Route::get('/home', 'SampleController@index');

Laravel8ではuseでクラス名を指定し、[クラス名, 'メソッド名']で括る様な記述になりました。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\SampleController;

Route::get('/home', [SampleController::class, 'index']);
②Modelsディレクトリの位置
php artisan make:model Post
  • Laravel7では

  • Laravel8からは

Jetstreamとは

Laravel8から新パッケージ「Jetstream」が導入されました。

何が出来るのかというと、Laravelにアプリケーションのスカフォールドを提供し、

ログイン、ユーザー登録、メール検証、2要素認証、セッション管理、Laravel Sanctumを介したAPIサポート、

およびオプションとしてチーム管理機能を含みます。

JetstreamはTailwindCSSを使用して設計されており、

フロントエンドスタックとして、LivewireとInertia.jsから選択して利用します。

Jetstreamを導入してみよう

まずLaravel8のプロジェクトを作成します。

f:id:shuto_komuro:20210118143323p:plain
Laravel8

ComposerでJetstreamをインストールします。

composer require laravel/jetstream

インストール後、jetstream:install Artisanコマンドを実行しフロントエンドスタックを導入します。(livewireかinertia)

簡単に説明すると、

  • livewire

    • PHP + Bladeで作成されたscaffolding(プロファイル画面など)
  • inertia

    • vue.jsで作成されたscaffolding(プロファイル画面など)

PHPでフロント部分、バックエンド部分共に実装したい方は、livewire。

この部分に関してはドキュメントを読むことをお勧めします。

php artisan jetstream:install livewire --teams
#チーム管理機能が不必要な場合、引数なし

インストールできたら、NPM依存をインストール・構築し、データベースをマイグレートします。

npm install && npm run dev

php artisan migrate

再度ブラウザをリロードします。

f:id:shuto_komuro:20210118151154p:plain
Jetstream

registerでユーザー登録しログインすると、「登録したユーザー名’s team」にオーナとして自動的に割り振られます。

f:id:shuto_komuro:20210118151422p:plain
home

「登録したユーザー名’s team」-> Team Settings からチームにメンバーを追加できます。

その際に役割、権限なども同時に割り振ることもできます。

f:id:shuto_komuro:20210118151941p:plain
team-setting

また役割、権限の追加、変更に関しては、 app/Providers/JetstreamServiceProvider.php より変更可能です。

<?php

protected function configurePermissions()
    {
        Jetstream::defaultApiTokenPermissions(['read']);

        Jetstream::role('admin', __('Administrator'), [
            'create',
            'read',
            'update',
            'delete',
        ])->description(__('Administrator users can perform any action.'));

        Jetstream::role('editor', __('Editor'), [
            'read',
            'create',
            'update',
        ])->description(__('Editor users have the ability to read, create, and update.'));

         -------  追記 -------
        Jetstream::role('checker', __('Checker'), [
            'read',
            'update',
            'delete',
        ])->description(__('Checker users have the ability to read, update, and delete.'));

    }

再度、「登録したユーザー名’s team」-> Team Settings を開くと、「Checker」が追加されました。

f:id:shuto_komuro:20210118153053p:plain
add role

〜最後に〜

Wizではエンジニアを募集しています!!

興味のある方は是非覗いてみてください!↓

careers.012grp.co.jp