Laravel8+vue3+Typescriptの環境でdynamic importを行う

# はじめに

LaravelのBladeテンプレー上でVueを動かす場合、webpackでバンドルされたJSファイルが肥大化しやすくなります。Nuxt.js, Next.jsではCode Splittingが自動で行われますが、laravel-mixのデフォルトの設定では似たようなことが行われないので、page.jsを用いてdynamic importする環境を構築していきます。

# Installation

まずは、必要なパッケージをインストールしていきます。

先日、以下の記事で2022年2月7日にVueのデフォルトバージョンがVue3になると発表がありましたが、記事執筆時点では2.6.14がlatestなので、vue@nextをインストールします。

[Vue 3 as the New Default](https://blog.vuejs.org/posts/vue-3-as-the-new-default.html)

```bash
yarn add -D ts-loader typescript @types/page
yarn add vue@next page
```

# tsconfig作成

tsconfig.jsonを作成し、includeにresources配下を追加します。

**tsconfig.json**

```jsx
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node",
  },
  "include": [
    "resources/**/*"
  ],
}
```

# laravel-mix設定

**webpack.mix.js**

```jsx
const mix = require('laravel-mix');
const ASSET_PATH = process.env.ASSET_PATH || '/';

mix.js('resources/ts/app.ts', 'public/js')
    .vue()
    .postCss('resources/css/app.css', 'public/css')
    .webpackConfig({
        module: {
            rules: [
                {
                    test: /\.tsx?$/,
                    loader: "ts-loader",
                    options: { appendTsSuffixTo: [/\.vue$/] },
                    exclude: /node_modules/
                }
            ]
        },
        output: {
            chunkFilename: mix.inProduction() ? 'js/[name].[chunkhash].js' : 'js/frontend/chunk/[name].js',
        },
        resolve: {
            extensions: ["*", ".js", ".jsx", ".vue", ".ts", ".tsx"],
        },
    })
    .setResourceRoot(ASSET_PATH)
    .version();
```

# dynamic import

entryポイントのapp.tsを作成していきます。

今回は`/login`と`/welcome`の2つのルーティングを設定し、ページに訪れた際にそれぞれ必要なtsファイルのみを読み込むようにします。

**resources/ts/app.ts**

```jsx
import page from "page";

page("/login", () => {
    import(/* webpackChunkName: "login" */ "./Login").then(({ Login }) => {
        new Login();
    });
});

page("/welcome", () => {
    import(/* webpackChunkName: "welcom" */ "./Welcome").then(({ Welcome }) => {
        new Welcome();
    });
});

page();
```

例えば、`/login`に訪れた際はLogin.tsを読み込み、Loginのクラスをインスタンス化しています。dynamic importしているファイルは以下のようになります。

**resources/ts/Login.ts**

```jsx
import { createApp } from "vue";
import LoginComponent from "./LoginComponent.vue";
export class Login {
    constructor() {
        createApp({
            components: {
                LoginComponent,
            },
            mounted() {
                console.log("Login class created");
            },
        }).mount("#app");
    }
}
```

**resources/ts/Welcome.ts**

```jsx
import { createApp } from "vue";
import WelcomeComponent from "./WelcomeComponent.vue";
export class Welcome {
    constructor() {
        createApp({
            components: {
                WelcomeComponent,
            },
            mounted() {
                console.log("Welcome class created");
            },
        }).mount("#app");
    }
}
```

こうすることで、Vueをインスタンス化した際に読み込むコンポーネントを必要最小限に抑えることができます。

次に読み込んでいるVueコンポーネントを作成します。

**resources/ts/LoginComponent.vue**

```jsx
<template>
  <h1>Login Component</h1>
  <p>{{ message }}</p>
</template>

<script lang="ts">
import { onMounted, ref, defineComponent } from "vue";
export default defineComponent({
  setup() {
    const message = ref("Hello Laravel Vue 3");
    onMounted(() => {
      console.log("Component mounted.");
    });
    return {
      message,
    };
  },
});
</script>
```

**resources/ts/WelcomeComponent.vue**

```jsx
<template>
  <h1>Welcome Component</h1>
  <p>{{ message }}</p>
</template>

<script lang="ts">
import { onMounted, ref, defineComponent } from "vue";
export default defineComponent({
  setup() {
    const message = ref("Hello Laravel Vue 3");
    onMounted(() => {
      console.log("Component mounted.");
    });
    return {
      message,
    };
  },
});
</script>
```

# routeとblade作成

Vueコンポーネントを読み込むbladeを作成していきます。

**routes/web.php**

```jsx
Route::get('/login', function () {
    return view('login');
});

Route::get('/welcome', function () {
    return view('welcome');
});
```

**resources/views/login.blade.php**

```html
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Login</title>
</head>

<body>
    <div id="app">
        <login-component />
    </div>
</body>
<script src="{{ mix('/js/app.js') }}"></script>

</html>
```

**resources/views/welcome.blade.php**

```html
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Login</title>
</head>

<body>
    <div id="app">
        <welcome-component />
    </div>
</body>
<script src="{{ mix('/js/app.js') }}"></script>

</html>
```

これで完成です。

`/login` を開くと、Welcome.tsとWelcomeComponent.vueのconsoleログが出力されていないのが分かります。

![スクリーンショット_2022-01-24_15.26.16.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349238/9e8cd56f-36f8-11ad-9250-6916d82d8c07.jpeg)
![スクリーンショット_2022-01-24_15.26.27.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349238/8d52e329-393e-8892-6f71-a080fc046b79.jpeg)

# まとめ

LaravelのBladeテンプレートのように、ページ遷移毎に静的アセットを取得するような場合、ファイルサイズが大きいとパフォーマンスに影響がでてきます。今回のようなdynamic importをすることで、取得するのはentoryポイントのapp.jsとchunkFileのみに抑えることができます。