いがにんのぼやき

WEBエンジニアのブログ。IT、WEB、バンド、アニメ。

簡単!Viteのプラグインを書いて、実行するソースコードを置き換える

この記事は 一休.comのカレンダー | Advent Calendar 2023 - Qiita20日目の記事です。
この記事ではViteのプラグインを書いて、実行するソースコードを置き換える方法を書いていきます。

自分はメインでNuxt3を触っていて、その開発ビルド、本番ビルドにはViteを用いています。
Viteではプラグインを書くことができるのですが、実はとても簡単に書くことができます。

Viteのプラグインのドキュメントはこちら
https://ja.vitejs.dev/guide/api-plugin.html

Viteのプラグインを書くのは実質的にはRollupのプラグインを書くことになるのでRollupのドキュメントも読むと理解が進みます。
https://rollupjs.org/plugin-development/

では実際に書いていきましょう。

まずはViteのアプリケーションを作成します。
templateはなんでもよいですが、自分はVueを書くことが多いのでVueにしました。

pnpm create vite transform-vite-example --template vue-ts
pnpm install

後述しますが、ソースマップの更新のために必要なライブラリもインストールしておきます。 https://github.com/rich-harris/magic-string

pnpm add -D magic-string

今回は簡単に以下の要件を実装してみます。

console.logが記載されている場合、console.logが出力される値をすべて [masked] に変更する

実際に以下のコードを書き換えるViteプラグインを実装していきます。

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)

function add() {
  count.value++
  console.log(`console.log:${count.value}`)
}
</script>

<template>
  <button type="button" @click="add">count is {{ count }}</button>
</template>

上記のコードの、

console.log(`console.log:${count.value}`)

という箇所を

console.log('[masked]')

というコードに置き換えます。

コードの変換にはRollupのtranformフックを設定することで実現できます。
https://rollupjs.org/plugin-development/#transform

実際のtransformを使ったコードがこちら。

import MagicString from 'magic-string'
import type { Plugin } from 'vite'

export const examplePlugin = <Plugin>{
  name: 'transform-vite-example',
  transform(code, id) {
    if (id.includes('node_modules') || id.endsWith('.spec.ts')) {
      return
    }

    const newCode = new MagicString(code)

    newCode.replaceAll(
      /console.log\(.+?\)/g,
      `console.log('[masked]')`
    )

    return {
      code: newCode.toString(), 
      map: newCode.generateMap({ hires: true }),
    }
  }
}

このファイルを vite.config.ts 内で読み込めば上記要件が実装できます。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import { examplePlugin } from './plugins/example'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    examplePlugin, // 追加
  ],
})

そのまま出力されていたものが

[masked] に置き換わっています。

コードをそれぞれ解説していきます。

まずは前述したRollupのtransformフックです。コードを変換するにはこのフックを用います。

  transform(code, id) {

コードの変換対象に node_modules やテストファイルを含めないようにしています。

    if (id.includes('node_modules') || id.endsWith('.spec.ts')) {
      return
    }

ここからが重要で、magic-stringを用いてコードの変換を行います。
なぜmagic-stringというライブラリを使う必要があるのかというと、ソースマップを正しく更新するためです。
Viteでは開発したコードをビルド時に変換して実行用のコードを生成しています。
そのため、エラーが出たときなどにそのままstacktraceを出力すると実行用のコード位置、関数名で出力されてしまい理解することが難しくなります。
開発コードと実行用のコードを紐づけるソースマップをビルド時に出力することで、stacktraceが元のコードの位置、関数名で出力されるようになります。

Viteのプラグインではそれを考慮して書き換えをする必要があります。
そのまま直接文字列置換を行ってしまうとソースマップが正しく更新されず意図したstacktraceが出力されないようになってしまいます。
それを防ぐのがmagic-stringです。

ということで実際に使ったコードがこちら。console.log(~~~) という部分を console.log('[masked]') に変換します。
(改行対応とかあんまり細かいことはしていません)

    const newCode = new MagicString(code)

    newCode.replaceAll(
      /console.log\(.+?\)/g,
      `console.log('[masked]')`
    )

これをmagic-stringのメソッドを使って返してあげることで正しい書き換えを行うことができます。

    return {
      code: newCode.toString(), 
      map: newCode.generateMap({ hires: true }),
    }

こんな形でViteの書き換え処理はさっと実装できるので、ぜひ活用していきたいですね

Nuxt3+Vue Apollo v4でSSR時に同時にGraphQLにアクセスできない問題が解消する

Nuxt2+NuxtのApollo moduleを使用したときに、各コンポーネントでGraphQLにアクセスするとそれぞれのリクエスト時にそこで処理待ちが発生し、とてもパフォーマンスの悪いものになっていた。
これは BatchHttpLink を導入しても変わりわない。(CSR時は1つのリクエストにまとめてくれる)
本当は処理待ちせず同時に実行をしてほしい。

だが色々実験していたところ、どうやらNuxt3とVue Apollo v4ではその問題が解消するようだ。

以下のリポジトリApollo Serverを立ててNuxt2とNuxt3の動きを色々試していた。 github.com

各resolverで3秒待ってからレスポンスを返すようなGraphQLのサーバーを用意。
ページでは2つのコンポーネントを呼び出し、各コンポーネントでGraphQLへのアクセスを実行する。(今回のケースは /batch で試すことができる)
そのときにレスポンスが返ってくる時間が3秒~6秒なら同時に実行が出来ていて、6秒以上かかるならそれぞれのGraphQLのアクセスの処理待ちをしてしまっているだろうと判断。
そうするとNuxt3で試していた方が同時実行が確認でき、Nuxt2のほうが処理待ちをしてしまっていることが確認できる。

NuxtのApollo moduleは内部的にはVue Apolloのv3を使っている。
v3とv4ではComposition APIへの対応という大きなトピックがある。

この同時実行が可能となった変更はVue3、Nuxt3の改善なのかVue Apolloの改善によるものなのかはよく分かっていない。
このSSRでGraphQLへのリクエストの同時実行ができるというのは凄く大きなことだ。今までパフォーマンス問題でなるべくルートのコンポーネントにGraphQLとの通信処理を寄せる必要があったが、その考慮は必要なくなり、コンポーネントの設計にも大きくかかわる。

Vue3のtemplate型検査でできること、できないこと

6/8追記-------------------------
最新のvue-tsc 0.37.3で試したところ以下の変化があった

Props Component injection VSCode上ではエラーにならないが効く。vue-tscではエラーになる

vue-tscでもエラーにならないようになった

Slot Property VSCode上では効かない、vue-tscでは効く

VSCode上でも効くようになった

追記終わり-------------------------

Vue3において、どのくらいVueのtemplateで型をチェックできるようになったのか確認する
https://igatea.hatenablog.com/entry/2022/05/01/194916 と同じように npm init vite で生成したVue3テンプレートで検証をしていく
VSCode上でVolarを使ってエディタ上でも型検査が効きエラーメッセージが出るか、vue-tsc --noEmit を叩いて型検査が行えるかを確認する

検証項目

  • Props
    • String union
    • Function
    • Component injection
  • Emit
  • Slot
  • Slot property

基本的にVueのSFCのtemplateとscript setupで検証をする
JSXやoption api、option apiの中でのsetupでは検証していない

検証コード https://gist.github.com/igayamaguchi/650726b9e84ce9fb14817657a278c857

結論

先に結論

検査項目
Props String union 効く
Props Function 効く
Props Component injection VSCode上ではエラーにならないが効く。vue-tscではエラーになる
Emit 効く。ただし部分集合のようになっているようで引数が足りていないものはエラーにならない。未定義のイベントを設定してもエラーにならない
Slot 効かない
Slot Property VSCode上では効かない、vue-tscでは効く

なぜかVSCodeとvue-tscで若干差がある。原因は不明

Props

String union

コンポーネント

<template>
  <div>
    <h3>StringUnion</h3>
    <p>{{ theme }}</p>
  </div>
</template>

<script lang="ts">
export const themes = ['primary', 'secondary'] as const
export type Theme = typeof themes[number]
</script>
<script setup lang="ts">
defineProps<{
  theme: Theme
}>()
</script>

コンポーネント

<script setup lang="ts">
import StringUnion from './components/StringUnion.vue';
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Props</h2>
      <!-- OK -->
      <StringUnion theme="primary" />
      <!-- NG -->
      <StringUnion theme="no-theme" />
      <!-- OK:定義していないpropsを設定していてもエラーにはならない -->
      <StringUnion theme="primary" invalid-value="primary" />
    </div>
  </div>
</template>

vue-tsc

src/App.vue:33:20 - error TS2322: Type '"no-theme"' is not assignable to type '"primary" | "secondary"'.

33       <StringUnion theme="no-theme" />
                      ~~~~~

VSCode上も、vue-tscでもしっかりString unionも型が効いた

Function

コンポーネント

<template>
  <div>
    <h3>Function</h3>
  </div>
</template>

<script lang="ts">
export type ChangeFn = (id: number) => void
</script>
<script setup lang="ts">
defineProps<{
  change: ChangeFn
}>()
</script>

コンポーネント

<script setup lang="ts">
import Function from './components/Function.vue';

const changePropsCorrect = (id: number) => {}
const changePropsIncorrect = (id: string) => {}
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Props</h2>
      <!-- OK -->
      <Function :change="changePropsCorrect" />
      <!-- NG -->
      <Function :change="changePropsIncorrect" />
  </div>
</template>

vue-tsc

src/App.vue:40:18 - error TS2322: Type '(id: string) => void' is not assignable to type 'ChangeFn'.
  Types of parameters 'id' and 'id' are incompatible.
    Type 'number' is not assignable to type 'string'.

40       <Function :change="changePropsIncorrect" />
                    ~~~~~~

これも正しく型の検査がされている

Component injection

コンポーネント
ComponentInjection.vue

<template>
  <div>
    <h3>ComponentInjection</h3>
    <Component :is="item" />
  </div>
</template>

<script setup lang="ts">
import { DefineComponent } from 'vue';
import ComponentInjectionItem from './ComponentInjectionItem.vue';
defineProps<{
  item: typeof ComponentInjectionItem,
}>()
</script>

Propsに指定するコンポーネント
ComponentInjectionItem.vue

<template>
  <div>
    <h4>ComponentInjectionItem</h4>
    <p>{{ id }}</p>
  </div>
</template>

<script setup lang="ts">
defineProps<{
  id: number
}>()
</script>

コンポーネント

<script setup lang="ts">
import ComponentInjection from './components/ComponentInjection.vue';
import ComponentInjectionItem from './components/ComponentInjectionItem.vue';
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Props</h2>
      <!-- NG:VSCode上はエラーにならないけどvue-tscだとエラーになる -->
      <ComponentInjection :item="ComponentInjectionItem" />
    </div>
  </div>
</template>

vue-tsc

src/App.vue:43:28 - error TS2322: Type 'DefineComponent<__VLS_DefinePropsToOptions<{ id: number; }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 4 more ..., {}>' is not assignable to type 'ComponentPublicInstanceConstructor<{ $: ComponentInternalInstance; $data: {}; $props: Partial<{}> & Omit<Readonly<ExtractPropTypes<__VLS_DefinePropsToOptions<{ id: number; }>>> & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>; ... 10 more ...; $watch(source: string | Function, cb: Function, option...'.
  Type 'ComponentPublicInstanceConstructor<{ $: ComponentInternalInstance; $data: {}; $props: Partial<{}> & Omit<Readonly<ExtractPropTypes<__VLS_DefinePropsToOptions<{ id: number; }>>> & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>; ... 10 more ...; $watch(source: string | Function, cb: Function, option...' is missing the following properties from type '{ __VLS_raw: DefineComponent<__VLS_DefinePropsToOptions<{ id: number; }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 4 more ..., {}>; __VLS_options: { ...; }; __VLS_slots: {}; }': __VLS_raw, __VLS_options, __VLS_slots

43       <ComponentInjection :item="ComponentInjectionItem" />
                              ~~~~

Emit

コンポーネント

<template>
  <div @click="handleClick">
    <h3>Emit</h3>
  </div>
</template>

<script setup lang="ts">
const emits = defineEmits<{
    (e: 'click-event', id: number): void
}>()
function handleClick() {
    emits('click-event', 1)
}
</script>

コンポーネント

<script setup lang="ts">
import Emit from './components/Emit.vue';

const handleChangeCorrect = (id: number) => {
  console.log(id)
}
const handleChangeInorrect = (id: string) => {
  console.log(id)
}
const handleChangeNotEnough = () => {
  console.log('not enough')
}
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Emit</h2>
      <!-- OK -->
      <Emit @click-event="handleChangeCorrect" />
      <!-- NG -->
      <Emit @click-event="handleChangeIncorrect" />
      <!-- OK:引数が足りていない場合でもエラーにならない -->
      <Emit @click-event="handleChangeNotEnough" />
      <!-- OK:定義していないeventを設定していてもエラーにはならない -->
      <Emit @invalid-event="handleChangeCorrect" />
    </div>
  </div>
</template>

vue-tsc

src/App.vue:51:27 - error TS2552: Cannot find name 'handleChangeIncorrect'. Did you mean 'handleChangeInorrect'?

51       <Emit @click-event="handleChangeIncorrect" />
                             ~~~~~~~~~~~~~~~~~~~~~

Slot

コンポーネント

<template>
  <div>
    <h3>Slot</h3>
    <slot name="header" />
    <slot />
  </div>
</template>

<script setup lang="ts">
</script>

コンポーネント

<template>
  <div>
    <h3>Slot</h3>
    <slot name="header" />
    <slot />
  </div>
</template>

<script setup lang="ts">
</script>

vue-tsc

<script setup lang="ts">
import SlotComponent from './components/SlotComponent.vue';
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Slot</h2>

      <SlotComponent>
        <template #header>
          <div>slot header</div>
        </template>
        <div>slot</div>
        <!-- エラーにはならない -->
        <template #invalid>
          <div>slot invalid</div>
        </template>
      </SlotComponent>
    </div>
  </div>
</template>

Slot property

コンポーネント

<template>
  <div>
    <h3>SlotProperty</h3>
    <slot name="header" value="header" />
    <slot :id="1" />
  </div>
</template>

<script setup lang="ts">
</script>

コンポーネント

<script setup lang="ts">
import SlotProperty from './components/SlotProperty.vue';
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Slot</h2>

      <SlotProperty v-slot="slotProps">
        <!-- OK -->
        <div>slot {{ slotProps.id }}</div>
        <!-- NG -->
        <div>slot {{ slotProps.x }}</div>
      </SlotProperty>
      <!-- slot properyに未定義のxはエラーに -->
      <SlotProperty v-slot="{ id, x }">
        <div>slot {{ id }}</div>
        <div>slot {{ x }}</div>
      </SlotProperty>

      <SlotProperty>
        <template #header="headerProps">
          <!-- OK -->
          <div>header {{ headerProps.value }}</div>
          <!-- NG -->
          <div>header {{ headerProps.x }}</div>
        </template>
        <template v-slot="slotProps">
          <!-- OK -->
          <div>slot {{ slotProps.id }}</div>
          <!-- NG -->
          <div>slot {{ slotProps.x }}</div>
        </template>
      </SlotProperty>
      <SlotProperty>
        <!-- slot properyに未定義のxはエラーに -->
        <template #header="{ value, x }">
          <div>header {{ value }}</div>
          <div>header {{ x }}</div>
        </template>
        <!-- slot properyに未定義のxはエラーに -->
        <template v-slot="{ id, x }">
          <div>slot {{ id }}</div>
          <div>slot {{ x }}</div>
        </template>
      </SlotProperty>
    </div>
  </div>
</template>

vue-tsc

src/App.vue:76:32 - error TS2339: Property 'x' does not exist on type '{ id: number; }'.

76         <div>slot {{ slotProps.x }}</div>
                                  ~

src/App.vue:79:35 - error TS2339: Property 'x' does not exist on type '{ id: number; }'.

79       <SlotProperty v-slot="{ id, x }">
                                     ~

src/App.vue:89:38 - error TS2339: Property 'x' does not exist on type '{ value: string; }'.

89           <div>header {{ headerProps.x }}</div>
                                        ~

src/App.vue:95:34 - error TS2339: Property 'x' does not exist on type '{ id: number; }'.

95           <div>slot {{ slotProps.x }}</div>
                                    ~

src/App.vue:100:37 - error TS2339: Property 'x' does not exist on type '{ value: string; }'.

100         <template #header="{ value, x }">
                                        ~

src/App.vue:105:33 - error TS2339: Property 'x' does not exist on type '{ id: number; }'.

105         <template v-slot="{ id, x }">
                                    ~

Vue3のコンポーネントファイルサイズを検証する

Vue2のときはコンポーネントを切りすぎるとファイルサイズが増大すると言われていた
ではVue3になった今、実際どのくらいファイルサイズが変わるのか、検証してみる

まずはインストール

% npm init vite vue3-component-file-size -- --template vue
npx: 6個のパッケージを3.688秒でインストールしました。
✔ Select a framework: › vue
✔ Select a variant: › vue-ts

package.json

{
  "name": "vue3-component-file-size",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.25"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^2.3.1",
    "typescript": "^4.5.4",
    "vite": "^2.9.5",
    "vite-plugin-compression": "^0.5.1",
    "vue-tsc": "^0.34.7"
  }
}

vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()]
})

このようなファイルが生成される

<template>
  <div>
    <h1>{{ h1 }}</h1>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const h1 = ref('App')
</script>

このVueのコードをエンドポイントのApp.vueとして定義、各コンポーネントをimportすることでファイルサイズの変化を確認する
確認時は npm run build で確認。devビルドではなくminifyされた状態で確認

% npm run build   

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 9 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.530e5712.js   51.28 KiB / gzip: 20.64 KiB

この時点でのファイルサイズは51.28 KiB、gzipで20.64 KiB

npm run preview でサーバーを立ち上げてSafariでファイルサイズを確認するとヘッダーが304 B、本文が21.16 KBとなっている
ビルド時の表示がKiB表記になっているが大体近しい数字になっている。少しだけずれているがまあ誤差の範囲として無視する

このファイルサイズがどのくらい増えるかを確認していく

scriptやstyleのありなしでもファイルサイズが変わるので以下の4つのコンポーネントを作ってみた

templateとscriptのコンポーネント

コンポーネント
TemplateAndScript.vue

<template>
  <div>
    <h2 @click="handleClick">Component{{ id }}</h2>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ id: number }>()

function handleClick() {
  alert(`click ${props.id}`)
}
</script>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <TemplateAndScript :id="1" />
  </div>
</template>

<script setup lang="ts">
import TemplateAndScript from './components/TemplateAndScript.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build  

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 10 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.fcfb82ce.js   51.45 KiB / gzip: 20.72 KiB
  • 51.28 KiB → 51.45 KiB = 0.17 KiB = 174.08 byte
  • gzipで20.64 KiB → 20.72KiB = 0.08 KiB = 81.92 byte

templateとscriptとstyleのコンポーネント

コンポーネント
TemplateAndScriptAndStyle.vue

<template>
  <div>
    <h2 :class="$style.title" @click="handleClick">Component{{ id }}</h2>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ id: number }>()

function handleClick() {
  alert(`click ${props.id}`)
}
</script>

<style module>
.title {
  color: red;
}
</style>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <TemplateAndScriptAndStyle :id="1" />
  </div>
</template>

<script setup lang="ts">
import TemplateAndScriptAndStyle from './components/TemplateAndScriptAndStyle.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 12 modules transformed.
dist/index.html                  0.42 KiB
dist/assets/index.cdfc0d7e.css   0.03 KiB / gzip: 0.05 KiB
dist/assets/index.d9a18fc7.js    51.64 KiB / gzip: 20.82 KiB
  • 51.28 KiB → 51.64 KiB = 0.36 KiB = 368.64 byte
  • gzipで20.64 KiB → 20.82 KiB = 0.18 KiB = 184.32 byte

ここでindex.htmlにも変化があり、かつCSSファイルが追加された
これはHTMLに <link rel="stylesheet" href="/assets/index.cdfc0d7e.css"> が追加されて、App.vueの描画に必要となるCSSファイルが出力されているだけ
今回の例だとCSSの中身は ._title_y99vv_2{color:red} となっている

templateのみのコンポーネント

コンポーネント
Template.vue

<template>
  <div>
    <h2>Title</h2>
  </div>
</template>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <Template :id="1" />
  </div>
</template>

<script setup lang="ts">
import Template from './components/Template.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 11 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.05c6e5e0.js   51.49 KiB / gzip: 20.73 KiB
  • 51.28 KiB → 51.49 KiB = 0.21 KiB = 215.04 byte
  • gzipで20.64 KiB → 20.73KiB = 0.09 KiB = 92.16 byte

templateとstyleのコンポーネント

コンポーネント
TemplateAndStyle.vue

<template>
  <div>
    <h2 :class="$style.title">Title</h2>
  </div>
</template>

<style module>
.title {
  color: red;
}
</style>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <TemplateAndStyle :id="1" />
  </div>
</template>

<script setup lang="ts">
import TemplateAndStyle from './components/TemplateAndStyle.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 12 modules transformed.
dist/index.html                  0.42 KiB
dist/assets/index.cdfc0d7e.css   0.03 KiB / gzip: 0.05 KiB
dist/assets/index.3ee1a737.js    51.58 KiB / gzip: 20.78 KiB
  • 51.28 KiB → 51.58 KiB = 0.3 KiB = 307.2 byte
  • gzipで20.64 KiB → 20.78 KiB = 0.14 KiB = 143.36 byte

結果

コンポーネントを追加した場合、以下のようにファイルサイズが追加される形となった

templateとscript templateとscriptとstyle templateのみ templateとstyle
174.08 byte, gzip: 81.92 byte 368.64 byte, gzip: 184.32 byte 215.04 byte, gzip: 92.16 byte 307.2 byte, gzip: 143.36 byte

こうみると大体1コンポーネントを追加するたびに多い場合、最低360byte(gzipなら180byte)から追加される形になる模様
このファイルサイズなら結構細かくコンポーネントを切っても問題ないように思う
なんかVue2とかはもっとファイルサイズが大きかった気がする

おまけ

Dynamic import

読み込み側をDynamic importに変えてみる

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <div>

    </div>
    <TemplateAndScriptAndStyle v-if="visible" :id="1" />
  </div>
</template>

<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue';

const h1 = ref('App')

const TemplateAndScriptAndStyle = defineAsyncComponent(() => import('./components/TemplateAndScriptAndStyle.vue'))
const visible = ref(false)

function importComponent() {
  visible.value = true
}
</script>
% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 13 modules transformed.
dist/index.html                                      0.36 KiB
dist/assets/TemplateAndScriptAndStyle.5352c308.js    0.41 KiB / gzip: 0.32 KiB
dist/assets/TemplateAndScriptAndStyle.ceb24f11.css   0.03 KiB / gzip: 0.05 KiB
dist/assets/index.78270881.js                        53.41 KiB / gzip: 21.56 KiB
  • 51.28 KiB → 53.41 KiB = 2.13 KiB = 2181.12 byte
  • gzipで20.64 KiB → 21.56 KiB = 0.92 KiB = 942.08 byte

Dynamic importで0.41 KiBのJSファイルと0.03 KiBのCSSが追加される形になった

TSX

TSXで記述した場合にサイズは変わるのか

import { FunctionalComponent } from 'vue'

export const FC: FunctionalComponent<{ id: number }> = (props, ctx) => {
    function handleClick() {
        alert(`click ${props.id}`)
    }
    return <>
            <div onClick={handleClick}>
                <h2>Title{props.id}</h2>
            </div>
        </>
}
<template>
  <div>
    <h1>{{ h1 }}</h1>
    <FC :id="1" />
  </div>
</template>

<script setup lang="ts">
import { FC } from './components/TSX'
import { ref } from 'vue';

const h1 = ref('App')
</script>
% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 10 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.639095d2.js   51.47 KiB / gzip: 20.73 KiB
  • 51.28 KiB → 51.47 KiB = 0.19 KiB = 194.56 byte
  • gzipで20.64 KiB → 20.73 KiB = 0.09 KiB = 92.16 byte

templateとscriptで実装した時とほとんど変わらない。10~20byteだけ変わるが誤差

2つのコンポーネントを読み込む場合

最初の検証はコンポーネントを1つだけ追加する形だった
そこに加えてもう一つコンポーネントを追加した場合のファイルサイズも確認してみる

通常

1つ目のコンポーネント。templateとscriptの時と同じ

<template>
  <div>
    <h2 @click="handleClick">Title{{ id }}</h2>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ id: number }>()

function handleClick() {
  alert(`click ${props.id}`)
}
</script>

2つ目のコンポーネント。1つ目のコンポーネントの文字列を少し変えただけ

<template>
  <div>
    <h2 @click="handleClick">Title2{{ id }}</h2>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{ id: number }>()

function handleClick() {
  alert(`click2 ${props.id}`)
}
</script>

読み込み側

<template>
  <div>
    <h1>{{ h1 }}</h1>
    <TemplateAndScript :id="1" />
    <TemplateAndScript2 :id="1" />
  </div>
</template>

<script setup lang="ts">
import TemplateAndScript from './components/TemplateAndScript.vue';
import TemplateAndScript2 from './components/TemplateAndScript2.vue';
import { ref } from 'vue';

const h1 = ref('App')
</script>

ビルド結果

% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 11 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.07aa95e9.js   51.62 KiB / gzip: 20.73 KiB

1つしか読み込まなかった時と比べると51.45 KiB → 51.62 KiB(gzipで20.72 KiB → 20.73 KiB)とコンポーネントを追加しているのに全然ファイルサイズが増えていない!
つまり最初にコンポーネントを追加した場合数百byte追加されるが、2つ目以降は100~200byteくらいに収まり、しかもgzipだと10byteくらいになる

TSX

import { FunctionalComponent } from 'vue'

export const FC: FunctionalComponent<{ id: number }> = (props, ctx) => {
    function handleClick() {
        alert(`click ${props.id}`)
    }
    return <>
            <div onClick={handleClick}>
                <h2>Title{props.id}</h2>
            </div>
        </>
}

export const FC2: FunctionalComponent<{ id: number }> = (props, ctx) => {
    function handleClick() {
        alert(`click2 ${props.id}`)
    }
    return <>
            <div onClick={handleClick}>
                <h2>Title2{props.id}</h2>
            </div>
        </>
}
<template>
  <div>
    <h1>{{ h1 }}</h1>
    <FC :id="1" />
    <FC2 :id="1" />
  </div>
</template>

<script setup lang="ts">
import { FC, FC2 } from './components/TSX'
import { ref } from 'vue';

const h1 = ref('App')
</script>
% npm run build

> vue3-component-file-size@0.0.0 build /Users/igayamaguchi/IdeaProjects/vue3-component-file-size
> vue-tsc --noEmit && vite build

vite v2.9.6 building for production...
✓ 10 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.0746aaec.js   51.67 KiB / gzip: 20.75 KiB

TSXもほぼ変わらない

結論

Vueコンポーネントを切りすぎて問題になる、ということはほぼないだろう
これからは設計的に綺麗になるなら小さなコンポーネントも積極的に分割を行っていきたい

2021年の振り返り

2021年最後の日、ということで今年を振り返っていきたい
2021年は仕事では変化の年、プライベートにはあまり変化のない年だったなと思う

仕事

仕事はと言うと2021年明けた頃はまだGoToとか色々あってまだがむしゃらに働いていた気がする(今の会社は宿泊予約サイトの運営をしています)
そこから半年は施策を回す、UI改善をするといったところを頑張っていた
いろんな画面が使いやすくなっていき、より良いものになっていく体験はすごくよい
また、開発体験の向上があったのも印象に残っている
今の会社は新しいアーキテクチャにリニューアルをしていく動きだが古いアーキテクチャ、新しいアーキテクチャどちらを触ることになるのかはタスクによって全然違う
今までは古いアーキテクチャを触ることが多かったが今年はずっと新しいアーキテクチャを触ることが大半になり、自分の作業の進み具合が大幅に改善して、新しいアーキテクチャ移行のモチベーションは大きく高まった
年の後半は今のチームに籍を置きつつアーキテクチャリニューアルチームに一時異動みたいなことをして、ガッツリアーキテクチャの改善、実装に入っていくことになりそれに従事していた
宿泊予約領域におけるNuxt + Go + GraphQLのアプリケーション設計の議論が大いにでき、とても学びがあり楽しい期間となった

そんな順風満帆ではなく、途中にフロントエンドのテックリードの退職や使用しているライブラリのいろんなバグにハマりめちゃくちゃ辛いこともあった
施策をするときは大体すでに解決策があったり相談すると解決してくれる人がいたりで頭を悩ませることは少なかったが、テックリード退職などで自分でちゃんと解決策を考えなければいけない
ここらへんは今まで甘えていたなと思った

開発以外に今までもやっていたことだが採用などで話す機会が増えた
面接はもちろんだが面談したり、スカウトを送ったり
個人的にはスカウトは結構消耗するので比重を少なくしたいと思っている
自分はスパムみたいに送るのはどうしても出来ず一人一人をちゃんと見てうちに合うか、活躍できるか、転職者の望むものを提供できるかとかよく考えた上でスカウトを送るのでカロリーが高い

プライベート

プライベートは今年も引き続きコロナで家に引き篭もりがちだった
人と会いたい気もするが昨今の情勢もあり特別声をかけるようなモチベーションも出ず家にいることが多かった
時たま一人でも友人とも旅行やキャンプには行っていたのでまあそれで十分満足できた感じがする
ゲームも結構やっていたが友達とやるのは面白いが一人でやるのは最近モチベが湧かない
今年大きいのはAmong usにハマったことだろうか
友人グループで定期的にYouTubeに配信しながらAmong usをやっている
ゲームは化かし化かされで面白いし、あそこ面白かったなって配信を見返したりできるのでいい

来年の抱負

仕事は引き続きがんばる
だけどがんばりすぎない
土日も働いたり夜中に働くことがあったので、オンオフはちゃんと区切る

仕事以外は何かハマるものを見つけたいなと思っている

データ指向アプリケーションデザインがいい本

www.amazon.co.jp

少し前からデータ指向アプリケーションデザインを読み始めた
誰かがどこかに貼った転職ドラフトの友達紹介コードを使って転職ドラフトを登録してくれたらしく招待成立メールが来て、招待特典としてオライリー本が貰えるので前々から気になっていたこの本をお願いした
といってもこれが去年のことでずっと積んでいたんだけど最近読み始めたら面白くてもっと早く読んでおけばよかったと後悔している

この本、この画像の通り分厚い
600ページほどあり今のところ150ページくらいまで読んだ
そこまで読んだ感想としては、WEBエンジニアでサーバーサイドの開発を行っている方ならみんな読むべき本だと思った

今のところ読んだ章は以下

  • 1章 信頼性、スケーラビリティ、メンテナンス性に優れたアプリケーション
  • 2章 データモデルとクエリ言語
  • 3章 ストレージと抽出
  • 4章 エンコーディングと進化

1章ではデータシステムとはどういったものかの考察を行っている
信頼性やスケーラビリティ、メンテナンス性の考えの掘り下げを行いつつ、監視でパーセンタイルを使う考え方やTwitterを例にアプリケーションに応じてどのようにスケーラビリティを考えるかについても書かれている

2章ではリレーショナルモデルとドキュメントモデルについて扱い、それに付随するクエリ言語の説明になっている
ここではRDBやNoSQL周りの歴史や移り変わり、メリットデメリット、各クエリ言語の記法の特徴を抑えることが出来た

3章ではRDBなどのインデックスの元となる知識の説明がある
Bツリーについてはもちろん、列指向ストレージで用いられるLSMツリーなどについても掘り下げており、どうやってインデックスを作成していくのか、参照していくのか、メリットデメリットなどを説明している
この章だけでもこの本を読んでよかったと思える内容だった

4章ではエンコーディングについて扱っている
JSONやThift、Protocol Buffersなどのエンコーディングや圧縮方法などに加えて、どうやって前方互換後方互換を保つのかに踏み込んでいる
この本をDBだけを扱う本だと思っていたのでこういうところまで踏み込むのかと驚いた

この後の章にはレプリケーショントランザクション、それを分散システムで扱う場合、のような面白そうな話題が続いているので読み進めるのが楽しみな内容になっている

この本、作者があらゆる情報を網羅して分かりやすくまとまっている点がよい
凄く説明がすっと入ってくるし全く知らなかった古の知識も入ってくる
残りも読み進めていきたい

バイナリから文字エンコードを確認する

あるサーバーで問題があり、配信されているHTMLファイルが文字化けしていて謎の文字エンコードになっていたことがあり、各エディタでも正しく文字エンコードが認識できなかったのでバイナリから文字エンコードを特定できるのか調べて試してみた
(結局そのファイルは本当におかしくなっていたようで文字エンコードが特定出来はしなかったが)
試した環境はWSL上のUbuntu

以下のようなHTMLファイルがUTF-8で保存されているとき

<div>テストtest</div>

hexdump -C でバイナリを確認することが出来る

$ hexdump -C test-utf8.html
00000000  3c 64 69 76 3e e3 83 86  e3 82 b9 e3 83 88 74 65  |<div>.........te|
00000010  73 74 3c 2f 64 69 76 3e  0a                       |st</div>.|
00000019

大体文字エンコードUTF-8、Shift-JIS、EUC-JP、UTF-16あたりのどれかで間違って保存されていることが多いと思うのでそれぞれのマルチバイトの文字列がそれぞれバイナリだとどのような値になるか確認する

例えば上のHTMLには「テスト」というマルチバイト文字列が含まれているので「テスト」という文字列のそれぞれの文字エンコードでのバイナリを確認してみる
echo で文字列出力→ iconv -t [エンコード形式] で指定の文字エンコードに→ hexdump -C でバイナリ出力、という流れ
iconvを挟んだ時に改行が挟まるので最後に 0a がついていることに注意

$ echo "テスト" | iconv -t UTF-8 | hexdump -C
00000000  e3 83 86 e3 82 b9 e3 83  88 0a                    |..........|
0000000a

$ echo "テスト" | iconv -t Shift-JIS | hexdump -C
00000000  83 65 83 58 83 67 0a                              |.e.X.g.|
00000007

$ echo "テスト" | iconv -t EUC-JP | hexdump -C
00000000  a5 c6 a5 b9 a5 c8 0a                              |.......|
00000007

$ echo "テスト" | iconv -t UTF-16 | hexdump -C
00000000  ff fe c6 30 b9 30 c8 30  0a 00                    |...0.0.0..|
0000000a

上のHTMLを見てみるとUTF-8e3 83 86 e3 82 b9 e3 83 88 が含まれていることが分かるのでこれはUTF-8で保存されたファイルだということが特定できる