Skip to content

core-vapor のディレクトリ構成

リポジトリの用語について

これから行う説明について,vuejs/core と vuejs/core-vapor のどちらもに適応される話は v3 のリポジトリ として表記します.(例,v3 のリポジトリでは,~~~)
何が core-vapor 固有の話で,何がもと (vuejs/core) からある話なのかを区別することで差分を予想しながら core-vapor の理解に繋げます.

主要なパッケージ

v3 のリポジトリは pnpm workspace によってモノレポで管理されています.
各パッケージは /packages ディレクトリに配置されています.
https://github.com/vuejs/core-vapor/packages

そして,それらのパッケージは大きく分けてコンパイラとランタイムの 2 つに分けられます.
compiler- で始まるパッケージはコンパイラに関連するパッケージで,runtime- で始まるパッケージはランタイムに関連するパッケージです.


core-vapor では新たに compiler-vaporruntime-vapor が追加されています.


また,次に重要なパッケージが reactivity です.
refcomputed, watch などの実装はランタイムパッケージからは独立して @vue/reactivity として提供されています.
こちらは /packages/reactivity に配置されています.


そして,Vue.js のエントリとなるパッケージは /packages/vue に配置されています.
core-vapor においては,これに加え,/packages/vue-vapor と言う Vapor Mode のエントリとなるパッケージが追加されています.


全体像:

structure-overview

compiler-core

compiler-core はその名の通りコンパイラのコア部分を提供します.
コンパイラのパッケージはこれらの他に,compiler-domcompiler-sfc などがありますが,
core は,sfc や dom といった特定の用途や特定の環境に依存しないコアな実装です.

Vue.js にはさまざまなコンパイラが存在しています.

例えば,template オプションを利用するとランタイム上でテンプレートがコンパイルされます.

ts
createApp({
  template: `<div>{{ msg }}</div>`,
  setup() {
    const msg = ref("Hello, Vue!");
    return { msg };
  },
}).mount("#app");

しかし,このテンプレートは見てわかる通り,SFC でも同様のテンプレート構文を利用しています.

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

const msg = ref("Hello, Vue!");
</script>

<template>
  <div>{{ msg }}</div>
</template>

また,これ以外にも HTML の innerHtml として記載したものをコンパイルするケースなど,Vue.js のテンプレートとしてのコンパイルは様々です.
このような様々な用途の共通部分を提供するのが compiler-core だという理解で概ね問題ありません.

具体的には,templaterender 関数にコンパイルするコアな実装が含まれます.

compiler-dom

Vue.js では,DOM の関する操作やコード生成を行うものは 環境依存である という考えのもと,これらはコアから分離されています.
これは後ほど runtime の方でも登場します.

コンパイラに関して言えば,DOM イベントに関するコードを生成したり,特定の DOM 要素に関するコードを生成したりする実装が含まれます.
Vue.js のイベント修飾子あたりを想像してもらうとわかりやすいかもしれません.

例えば,@submit.prevent といった修飾子は,

ts
(e: Event) => e.preventDefault()

のようなコードが必要となり,これは DOM API に依存するコード生成です. このようなものを提供するのが compiler-dom です.

例:

registerRuntimeHelpers({
  [V_MODEL_RADIO]: `vModelRadio`,
  [V_MODEL_CHECKBOX]: `vModelCheckbox`,
  [V_MODEL_TEXT]: `vModelText`,
  [V_MODEL_SELECT]: `vModelSelect`,
  [V_MODEL_DYNAMIC]: `vModelDynamic`,
  [V_ON_WITH_MODIFIERS]: `withModifiers`,

const modifierGuards: Record<
  ModifierGuards,
  | ((e: Event) => void | boolean)
  | ((e: Event, modifiers: string[]) => void | boolean)
> = {
  stop: (e: Event) => e.stopPropagation(),
  prevent: (e: Event) => e.preventDefault(),

compiler-sfc

これは名前の通り SFC (Single File Component) に関するコンパイラです.
具体的には,<script setup><style scoped> などの機能を提供します.

多くの場合,このコンパイラは別パッケージになているバンドラ等のツールのプラグインに呼ばれることで機能します.
有名な例としては,Vite で利用される vite-plugin-vue や,webpack で利用される vue-loader などがあります.

function tryResolveCompiler(root?: string) {
  const vueMeta = tryRequire('vue/package.json', root)
  // make sure to check the version is 3+ since 2.7 now also has vue/compiler-sfc
  if (vueMeta && vueMeta.version.split('.')[0] >= 3) {
    return tryRequire('vue/compiler-sfc', root)
  }
}

import * as _compiler from 'vue/compiler-sfc'

export let compiler: typeof _compiler

try {
  // Vue 3.2.13+ ships the SFC compiler directly under the `vue` package
  // making it no longer necessary to have @vue/compiler-sfc separately installed.
  compiler = require('vue/compiler-sfc')
} catch (e) {
  try {
    compiler = require('@vue/compiler-sfc')
  } catch (e) {
    throw new Error(
      `@vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc ` +
        `to be present in the dependency tree.`
    )
  }
}

runtime-core

ランタイムのコア部分を提供します.
こちらも DOM には依存しない,コンポーネントのランタイムの実装や,仮想 DOM とそのパッチ,スケジューラの実装などが含まれます.
パッチ処理 (renderer) に関しては,実際に DOM 操作が行われそうな雰囲気がありますが,runtime-core では非 DOM API 依存に定義された interface の呼び出しのみを行っており,
実際の関数は runtime-dom に実装され,注入されています.(依存性逆転の法則を利用しています.)

interface:

export interface RendererOptions<
  HostNode = RendererNode,
  HostElement = RendererElement,
> {
  patchProp(
    el: HostElement,
    key: string,
    prevValue: any,
    nextValue: any,
    namespace?: ElementNamespace,
    parentComponent?: ComponentInternalInstance | null,
  ): void
  insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
  remove(el: HostNode): void
  createElement(
    type: string,
    namespace?: ElementNamespace,
    isCustomizedBuiltIn?: string,
    vnodeProps?: (VNodeProps & { [key: string]: any }) | null,
  ): HostElement
  createText(text: string): HostNode
  createComment(text: string): HostNode
  setText(node: HostNode, text: string): void
  setElementText(node: HostElement, text: string): void
  parentNode(node: HostNode): HostElement | null
  nextSibling(node: HostNode): HostNode | null
  querySelector?(selector: string): HostElement | null
  setScopeId?(el: HostElement, id: string): void
  cloneNode?(node: HostNode): HostNode
  insertStaticContent?(
    content: string,
    parent: HostElement,
    anchor: HostNode | null,
    namespace: ElementNamespace,
    start?: HostNode | null,
    end?: HostNode | null,
  ): [HostNode, HostNode]
}

createRenderer という関数が option として実際のオペレーションを受け取る(runtime-core では直接呼び出さない):

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement> {

runtime-dom

上記で説明したうちの,実際の DOM オペレーションの実装や,それらを core に注入する実装が含まれます.

export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },

  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },

  createElement: (tag, namespace, is, props): Element => {
    const el =
      namespace === 'svg'
        ? doc.createElementNS(svgNS, tag)
        : namespace === 'mathml'
          ? doc.createElementNS(mathmlNS, tag)
          : is
            ? doc.createElement(tag, { is })
            : doc.createElement(tag)

    if (tag === 'select' && props && props.multiple != null) {
      ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
    }

    return el
  },

  createText: text => doc.createTextNode(text),

  createComment: text => doc.createComment(text),

  setText: (node, text) => {
    node.nodeValue = text
  },

  setElementText: (el, text) => {
    el.textContent = text
  },

  parentNode: node => node.parentNode as Element | null,

  nextSibling: node => node.nextSibling,

  querySelector: selector => doc.querySelector(selector),

他にも,compiler の説明でも触れた,実際に DOM イベントを処理するための実装なども含まれています.
(compiler-dom はこれらの呼び出しを行うコードを出力するための実装です.)

type ModifierGuards =
  | 'shift'
  | 'ctrl'
  | 'alt'
  | 'meta'
  | 'left'
  | 'right'
  | 'stop'
  | 'prevent'
  | 'self'
  | 'middle'
  | 'exact'
const modifierGuards: Record<
  ModifierGuards,
  | ((e: Event) => void | boolean)
  | ((e: Event, modifiers: string[]) => void | boolean)
> = {
  stop: (e: Event) => e.stopPropagation(),
  prevent: (e: Event) => e.preventDefault(),
  self: (e: Event) => e.target !== e.currentTarget,
  ctrl: (e: Event) => !(e as KeyedEvent).ctrlKey,
  shift: (e: Event) => !(e as KeyedEvent).shiftKey,
  alt: (e: Event) => !(e as KeyedEvent).altKey,
  meta: (e: Event) => !(e as KeyedEvent).metaKey,
  left: (e: Event) => 'button' in e && (e as MouseEvent).button !== 0,
  middle: (e: Event) => 'button' in e && (e as MouseEvent).button !== 1,
  right: (e: Event) => 'button' in e && (e as MouseEvent).button !== 2,
  exact: (e, modifiers) =>
    systemModifiers.some(m => (e as any)[`${m}Key`] && !modifiers.includes(m)),
}

reactivity

名前の通り,Vue.js のリアクティビティシステムを提供します.
どこかで,「Vue.js のリアクティビティシステムは out of box で利用可能だ」,という話を聞いたことがあるかもしれませんが,これはこのパッケージが他のパッケージに依存せず独立して実装されているためです.
そして,この「独立している」という点も Vapor Mode の実装においては重要なポイントとなります.

それもそのはず,少しネタバレをしておくと,Vapor Mode は仮想 DOM を使わずにリアクティビティシステムを活用することで画面を更新していくわけですが,実際のところこのリアクティビティのパッケージにはほとんど変更が入っていません.
詰まるところ,Vapor の機能の一部としてスッと使えてしまうほど Vue.js のランタイムには依存していないのです.

compiler-vapor, runtime-vapor

さて,ようやく今回のメインです. 名前の通り,Vapor Mode のコンパイラとランタイムの実装です.

Vapor Mode は現在 R&D のフェーズであるため,なるべく upstream にある既存の実装には手を加えずに済むように独立したパッケージとして実装されています.
そのため,既存の runtime, compiler と大きく被る部分もありますが,実はこの部分関しても重複して実装しています.

このパッケージでどのような実装がされているかなどはこれから見ていく (というかそれがこの本のメインの話) なので,ここでは省略します.


ざっくり,パッケージの全体構成がわかったところで早速 Vapor Mode の実装を理解するために必要なソースコードを読んでいきましょう!