petite-vue 最速 使い方

2021年7月3日（日本時間）、Vueの創始者Evan Youが新たなパッケージをリリースしました。

[https://github.com/vuejs/petite-vue](https://github.com/vuejs/petite-vue)

## petite-vue

petite-vue（プティットゥ・ヴュー「プチVue」の意）は先進的な機能を備えた、超軽量なVueの下位互換です。スタンダードなVueと同等のテンプレート構文・リアクティビティモデルを有していますが、かつてサーバーフレームワークで所々に書かれていたような、ちょっとしたインタラクションを置き換えられるよう特化しています。

- 5.7KB以下
- DOMベース（仮想DOMを使用しない）
- @vue/reactivityで動作

## 使い方

ビルドは不要で、CDNから読み出すだけで使えます。

```html
<script src="<https://unpkg.com/petite-vue>" defer init></script>

<!-- anywhere on the page -->
<div v-scope="{ count: 0 }">
  {{ count }}
  <button @click="count++">inc</button>
</div>

```

- petite-vueで制御したい範囲を `v-scope` でくくる。
- HTMLがパースされた後に実行したいスクリプトには `defer` 属性をつける。
- `v-scope` を持っている要素すべてを初期化するには `init` 属性を使う。

より実際的には `createApp` を使います。

```html
<script type="module">
  import { createApp } from '<https://unpkg.com/petite-vue?module>'

  createApp({
    // exposed to all expressions
    count: 0,
    // getters
    get plusOne() {
      return this.count + 1
    },
    // methods
    increment() {
      this.count++
    }
  }).mount()
</script>

<!-- v-scope value can be omitted -->
<div v-scope>
  <p>{{ count }}</p>
  <p>{{ plusOne }}</p>
  <button @click="increment">increment</button>
</div>

```

petite-vue の適用範囲を絞るためにマウントのターゲットを指定することもできます。

```javascript
createApp().mount('#only-this-div')

```

複数指定もできます。

```javascript
createApp({
  // root scope for app one
}).mount('#app1')

createApp({
  // root scope for app two
}).mount('#app2')

```

要素ごとに `mounted` や `unmounted` のライフサイクルフックが使えます。

```html
<div
  v-if="show"
  @mounted="console.log('mounted on: ', $el)"
  @unmounted="console.log('unmounted: ', $el)"
></div>

```

要素をリアクティブ化するには `v-effect` を使用します。

```html
<div v-scope="{ count: 0 }">
  <div v-effect="$el.textContent = count"></div>
  <button @click="count++">++</button>
</div>

```

## コンポーネント

petite-vueでは、再利用可能なコンポーネントは関数として記述します。

```html
<script type="module">
  import { createApp } from '<https://unpkg.com/petite-vue?module>'

  function Counter(props) {
    return {
      count: props.initialCount,
      inc() {
        this.count++
      },
      mounted() {
        console.log(`I'm mounted!`)
      }
    }
  }

  createApp({
    Counter
  }).mount()
</script>

<div v-scope="Counter({ initialCount: 1 })" @mounted="mounted">
  <p>{{ count }}</p>
  <button @click="inc">increment</button>
</div>

<div v-scope="Counter({ initialCount: 2 })">
  <p>{{ count }}</p>
  <button @click="inc">increment</button>
</div>

```

あるテンプレートの値を再利用したいときは、`$template` キーを使用します。

```html
<script type="module">
  import { createApp } from '<https://unpkg.com/petite-vue?module>'

  function Counter(props) {
    return {
      $template: '#counter-template',
      count: props.initialCount,
      inc() {
        this.count++
      }
    }
  }

  createApp({
    Counter
  }).mount()
</script>

<template id="counter-template">
  My count is {{ count }}
  <button @click="inc">++</button>
</template>

<!-- reuse it -->
<div v-scope="Counter({ initialCount: 1 })"></div>
<div v-scope="Counter({ initialCount: 2 })"></div>

```

`reactive` メソッドを使ってグローバルな状態を有するストアも作れます。

```html
<script type="module">
  import { createApp, reactive } from '<https://unpkg.com/petite-vue?module>'

  const store = reactive({
    count: 0,
    inc() {
      this.count++
    }
  })

  // manipulate it here
  store.inc()

  createApp({
    // share it with app scopes
    store
  }).mount()
</script>

<div v-scope="{ localCount: 0 }">
  <p>Global {{ store.count }}</p>
  <button @click="store.inc">increment</button>

  <p>Local {{ localCount }}</p>
  <button @click="localCount++">increment</button>
</div>

```

## 使える機能、使えない機能

### petite-vueのみ

- `v-scope`
- `v-effect`
- `@mounted` および `@unmounted`

### Vueとふるまいが異なる

- `$el`
- `createApp()`
- コンポーネント
- カスタムディレクティブ

### Vueと同じ

- `{{ }}` テキストバインディング
- `v-bind` （`:` を含む）
- `v-on` （`@` を含む）
- `v-model`
- `v-if` / `v-else` / `v-else-if`
- `v-for`
- `v-show`
- `v-html`
- `v-text`
- `v-pre`
- `v-once`
- `v-cloak`
- `reactive()`
- `nextTick()`

### petite-vueでは使えない

- `ref()`, `computed()` など
- テンプレートrefs（セレクタを使ってね）
- render関数（仮想DOMは非サポート）
- コレクション型 (Map, Setなど）へのリアクティビティ
- Transition, KeepAlive, Teleport, Suspense
- ディープな `v-for`
- `v-on="object"`
- `v-is` & `<component :is="xxx">`
- `v-bind:style` の自動修正

## デモ

[https://codesandbox.io/s/sad-flower-c17ec?file=/index.html](https://codesandbox.io/s/sad-flower-c17ec?file=/index.html)

CodeSandboxに入れてみましたが、初期描画時にうまく動きませんでした。
デモブラウザの更新ボタンを押すと正しく表示されます。

## 考察

以下、petite-vueが作られた背景として考えられそうなものを挙げてみます。

### Reactとの差別化

Vueと並べてべて名前が挙がることの多いReactは、近年そのSSR/SSGフレームワークであるNextJSと合わせてWebフロントエンド開発の覇権を握りつつあります。Reactは仮想DOMという仕組みを採用し、それによって高いパフォーマンスを出していますが、反面その動作速度はある程度のところで頭打ちになってしまうという欠点を持っています。Vueは実は本質的には仮想DOMに基づいておらず、特定の機能にしか使われていないので、そうした機能を削ぎ落とすことで軽量化と高速化を図り、Reactとの差別化路線を打ち出したようにも見えます。

### Svelteとの競争

冒頭に書いたように、petite-vueはサーバーフレームワーク（Ruby on RailsとかDjango、Laravelあたりでしょうか）で書かれたHTMLにちょっとしたインタラクションをJSで加えるみたいな用途に最適と謳っており、Evan You自身もそうしたフレームワークであるAlpineに言及したうえで、それよりも軽量であると主張しています。
しかし軽量さで近年注目を集めたといえばSvelteです。その創始者のRich Harris（Rollup.jsの作者でもあります）は軽量さと記述量の少なさを追い求めており、特に軽量さに関してはTwitter上でEvan Youに直接メンションしてアピールするなど対抗心を燃やしていました。Evan Youもそれに触発されたのか、Rollup.jsよりも高速なバンドラであるViteを開発し、実際これはSvelteにもVueにも採用されています。こうした経緯から、「Vueを軽量化する」というモチベーションがEvan Youの中にあったものと思われます。

### Hotwireの存在

そしてこのような用途、つまり「HTMLにJSを部分的に用いる」というアプローチを前面に打ち出しているのがHotwireです。HotwireはBasecampが開発しているライブラリで、既存のWebアプリケーションにこれを振りかけるとSPAライクになりユーザー体験が向上するというアイデアに基づいています（ここの理解はわりとあいまい）。一からWebアプリケーションをリプレイスするのはやはり大変であり、既存のサービスをいかに新しいものに置き換えていくかというアプローチには小さく始められることが求められます。petite-vueはそうした需要を満たすために、Vueの下位互換として改めて作られたようにも思いました。