いがにんのぼやき

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

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

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で保存されたファイルだということが特定できる

Goでtext/templateを使ってSQLを組み立てる

今、VちゃんではGoでコードを書いており、その中でDBにアクセスするコードではクエリビルダーは使わずSQLを直書きしている

書き込み系のSQLは基本的にシンプルになるので特に問題ない
しかし、読み取り系の特定条件での検索をするようなSQLを書くときにはSQLが複雑になり、そのままSQLを文字列として扱うだけでなく、特定のJOINを追加したりWHEREを追加したり、ということが起こる
こういった検索クエリは検索エンジンに投げることが一般的に多いと思うが、今の自分のアプリケーションは検索エンジンを入れているわけではないのでここをSQLで頑張る必要がある

まずはtext/templateの使い方

import (
    "bytes"
    "fmt"
    "testing"
    "text/template"
)

func TestExample(t *testing.T) {
    var buf bytes.Buffer
    tmpl := `
{{ if .flag }}
true
{{ else }}
false
{{ end }}
`
    data := map[string]interface{}{
        "flag": true,
    }
    tm := template.Must(template.New("tmpl").Parse(tmpl))
    if err := tm.Execute(&buf, data); err != nil {
        t.Fatal(err)
    }
    fmt.Println(buf.String())
}

出力例


true

{{ if .flag }} などを書いているところの改行はそのまま出力されるので注意 html/template というのもあるのでたまにimport間違えたりするので注意

複数条件は以下のように書く

func TestExample(t *testing.T) {
    var buf bytes.Buffer
    tmpl := `
{{ if and .flag1 .flag2 }}
true
{{ end }}
`
    data := map[string]interface{}{
        "flag1": true,
        "flag2": true,
    }
    tm := template.Must(template.New("tmpl").Parse(tmpl))
    if err := tm.Execute(&buf, data); err != nil {
        t.Fatal(err)
    }
    fmt.Println(buf.String())
}

and の後に条件を並べる
and に否定条件などを入れる場合は and (not .flag1) (not flag2) みたいにかっこで囲っておく

ドキュメントを見ると比較演算子を使えたりいくつかの演算子がtemplate内での独自構文で提供されている

golang.org

SQLを書く

実際にSQLを書いていく

SELECT
    vtuber.id
FROM
    vtuber
WHERE
    vtuber.deleted_at IS NULL

これは実際のGoのアプリケーションでsqlxで実行されるSQLをシンプルにしたもの
VTuberの一覧を取得しているSQLである
一覧を取得するときにはここに検索条件が追加される
例えばある個性が設定されているVTuberのみ取得するSQLにしてみよう

SELECT
    vtuber.id
FROM
    vtuber
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
WHERE
    vtuber.deleted_at IS NULL
    AND vtuber_personality.personality_id IN (:personalities)

個性の検索をつけることで関連テーブルへのJOIN、WHEREでの絞り込みが追加された

更に配信スタイルでの絞り込みも行ってみよう

SELECT
    vtuber.id
FROM
    vtuber
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id
WHERE
    vtuber.deleted_at IS NULL
    AND vtuber_personality.personality_id IN (:personalities)
    AND vtuber_video_style.vtuber_style_id IN (:video_styles)
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count

条件に全て合致していることを確認するためにGROUP BYが追加された

こんな感じで検索のクエリは条件が追加されるたびに少しづつ複雑さを増していく
じゃあこのSQLをアプリケーション内で実行しようと思ったときにいくつかの選択肢が浮かぶ

  • SQLを文字列連結で動的に変える
  • クエリビルダーを使う
  • SQL自体は変わらないようにSQL内で条件分岐を活用する(WHEREの条件などで頑張る)

それぞれの方法は良し悪しあるかと思うが今回はこのSQLをtext/templateパッケージを使用して組み立ててみようと思う

text/templateでSQLを組み立てる

上記の条件を元にSQLが組み立てられるようにしたtemplateはこのようになる

SELECT
    vtuber.id
FROM
    vtuber
{{ if .personalities }}
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
{{ end }}
{{ if .vtuberGroups }}
INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id
{{ end }}
WHERE
    vtuber.deleted_at IS NULL
{{ if .personalities }}
    AND vtuber_personality.personality_id IN (:personalities)
{{ end }}
{{ if .vtuberGroups }}
    AND vtuber_video_style.vtuber_style_id IN (:video_styles)
{{ end }}
{{ if or .personalities .vtuberGroups }}
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count
{{ end }}

Goで実行するとするとこんな感じ

func TestExample(t *testing.T) {
    tmpl := `
SELECT
  vtuber.id
FROM
  vtuber
{{ if .personalities }}
INNER JOIN vtuber_personality
  ON vtuber_personality.vtuber_id = vtuber.id
{{ end }}
{{ if .vtuberGroups }}
INNER JOIN vtuber_video_style
  ON vtuber_video_style.vtuber_id = vtuber.id
{{ end }}
WHERE
  vtuber.deleted_at IS NULL
{{ if .personalities }}
  AND vtuber_personality.personality_id IN (:personalities)
{{ end }}
{{ if .vtuberGroups }}
  AND vtuber_video_style.vtuber_style_id IN (:video_styles)
{{ end }}
{{ if or .personalities .vtuberGroups }}
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count
{{ end }}
`

    var buf bytes.Buffer
    data := map[string]interface{}{
        "personalities": []string{"p1"},
        "vtuberGroups": []string{"g1"},
    }
    tm := template.Must(template.New("tmpl").Parse(tmpl))
    if err := tm.Execute(&buf, data); err != nil {
        t.Fatal(err)
    }

    fmt.Println(buf.String())
}

出力されるSQLはこちら
実際にはこの buf.String() をsqlxなどの実行クエリとして流し込むことになる

SELECT
    vtuber.id
FROM
    vtuber

INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id


INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id

WHERE
    vtuber.deleted_at IS NULL

    AND vtuber_personality.personality_id IN (:personalities)


    AND vtuber_video_style.vtuber_style_id IN (:video_styles)


GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count

改行が変に入るのが気になるところ
何も指定しなかった場合、さらにこんな感じになる

SELECT
    vtuber.id
FROM
    vtuber


WHERE
    vtuber.deleted_at IS NULL




そんなときは {{ if .personalities -}}~~~{{- end }} のように波かっこに改行やスペースを削除したいところにハイフンを記述してあげればマシになる

golang.org

SELECT
    vtuber.id
FROM
    vtuber
{{ if .personalities -}}
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
{{- end }}
{{ if .vtuberGroups -}}
INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id
{{- end }}
WHERE
    vtuber.deleted_at IS NULL
{{ if .personalities -}}
    AND vtuber_personality.personality_id IN (:personalities)
{{- end }}
{{ if .vtuberGroups -}}
    AND vtuber_video_style.vtuber_style_id IN (:video_styles)
{{- end }}
{{ if or .personalities .vtuberGroups -}}
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count
{{- end }}

これを実行すると

SELECT
    vtuber.id
FROM
    vtuber
INNER JOIN vtuber_personality
    ON vtuber_personality.vtuber_id = vtuber.id
INNER JOIN vtuber_video_style
    ON vtuber_video_style.vtuber_id = vtuber.id
WHERE
    vtuber.deleted_at IS NULL
AND vtuber_personality.personality_id IN (:personalities)
AND vtuber_video_style.vtuber_style_id IN (:video_styles)
GROUP BY vtuber.id
HAVING COUNT(*) = :condition_count

うんうん、条件指定した場合は綺麗になった

条件未指定の場合は?

SELECT
    vtuber.id
FROM
    vtuber


WHERE
    vtuber.deleted_at IS NULL




まあまあ改行入ってしまう
ここは微妙なところ

所感

  • template内に全ての条件が入っているので条件の全容を把握しやすい
  • ただしちょっと見にくい
  • パッとコピーしてSQL流して試すとかはやりにくい
  • シンタックスハイライトが効かないかもね、SQLかつtext/templateの構文が混ざるので

Realforceのキーボードを買った

勝ったのはかなーりまえだけど下書きに残りっぱなしだったので放流


f:id:igatea:20190504211727j:image
f:id:igatea:20190504211815j:image
f:id:igatea:20190504211825j:image

 

以下付属品各種


f:id:igatea:20190504211810j:image
f:id:igatea:20190504211820j:image
f:id:igatea:20190504211839j:image
f:id:igatea:20190504211834j:image

 

 

他にも選択肢としてHHKBとかあると思うだけど、自分はメインがWindowsなのでWindowsボタンがどうしても欲しいのです

テンキーも使ったとしてもゲームくらい?不要なのでテンキーレス

使ってみて

もう数年使っているが前の標準キーボードには戻れない

音も小さいし打鍵の深さとかも調整出来てめっちゃ楽

文字を打つ時は結構浅めが好きなんだけど、浅いとゲームしているときに結構誤爆する。最初は困ったけど、それも慣れて問題なくなった

色は白がおすすめ、黒の同じものを会社で使っているがキーに書いてある文字が見えにくくてちょっと微妙。かっこいいんだけどね