Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

VitePress のルータの実装について

はじめに

  • バージョン ... 0.2.0
  • SHA-1 ハッシュ ... d58b0231f63362aadde2d75feebb7b8c567bad0f
  1. VitePress のディレクトリ構成
  2. src/client/app と src/client/theme-default のつなぎ込み
  3. Markdown ファイルと src/client/app, Vue インスタンス本体のつなぎ込み <-- いまココです。
  4. Vite と VitePress のつなぎ込み
    1. dev 編
    2. build 編(未着手)

概要

VitePress は vue-router を使っていないそうです。

  • Lighter page weight.
    • Does not use vue-router because the need of VitePress is very simple and specific - a simple custom router (under 200 LOC) is used instead. (訳注: LOC は Line of Code の略、under 200 LOC ... 200 行以下で)

vuejs/vitepress - GitHub

リアクティブにしたルート routeRouterSymbol というシンボルに紐付けて管理しているようです。

Step 1.

route.contentComponent に Vue コンポーネントに変換された Markdown ファイルがはいります。テーマを自作する際に <Content /> を参照します。以下が <Content /> にあたります。

/src/client/app/components/Content.ts
import { h } from 'vue'
import { useRoute } from '../router'
import { usePrefetch } from '../composables/preFetch'


export const Content = {
  setup() {
    const route = useRoute()
    if (!__DEV__) {
      // in prod mode, enable intersectionObserver based pre-fetch.
      usePrefetch()
    }
    return () => (route.contentComponent ? h(route.contentComponent) : null)
  }
}

useRoute() を使って route を取り出しています。Step 5 で、useRoute() を見てみます。

Step 2.

route は「現在のパス path」と「それに対応したコンポーネント(Vue コンポーネントに変換された Markdown ファイル) contentComponent」を保持しています。

/src/client/app/router.ts#L9-L7
export interface Route {
  path: string
  contentComponent: Component | null
}

Step 3.

route は、リアクティブです。

/src/client/app/router.ts#L16-L25
const getDefaultRoute = (): Route => ({
  path: '/',
  contentComponent: null
})

// ...
// 中略
// ...

  const route = reactive(getDefaultRoute())

Step 4.

router から route を取り出しています。

/src/client/app/router.ts#L130-L141
export function useRouter(): Router {
  const router = inject(RouterSymbol)
  if (!router) {
    throw new Error('useRouter() is called without provider.')
  }
  // @ts-ignore
  return router
}


export function useRoute(): Route {
  return useRouter().route
}

inject は Composition API で策定された関数のようです。provide でキーと値を指定して詰め込んだものを、inject で取り出すことができます。

以下、provide されているところを確認します。

Step 5.

app.provide(RouterSymbol, router)provide が、確認できます。また、そのすぐ下で app.component('Content', Content) で、さきほど確認した Content, Vue コンポーネントに変換された Markdown ファイルも、確認できました。

/src/client/app/index.ts#L74-L78
  app.provide(RouterSymbol, router)
  app.provide(pageDataSymbol, pageDataRef)


  app.component('Content', Content)
  app.component('Debug', __DEV__ ? Debug : () => null)

Step 6.

createRouterrouter を作成しています。

/src/client/app/index.ts#L39-L68
  const router = createRouter(
    // ...
    // 中略
    // ...
  )

Step 7.

router のプロパティには routego が定義されています。route は "現在" の route です。currentRoute とかの方がニュアンスはもしかしたら近かったかもしれません。go の動作は、次節を参照してください。

/src/client/app/router.ts#L4-L7
export interface Router {
  route: Route
  go: (href?: string) => Promise<void>
}

Step 8.

routergo を実行すると「それに対応したコンポーネント contentComponent」を fetch し、あとは mount すればページが表示されます。

/src/client/app/index.ts#L106-L109
  // wait unitl page component is fetched before mounting
  router.go().then(() => {
    app.mount('#app')
  })

どこかで、パスの変更を検知しているのかもしれませんが、それに類するものを見つけられずにいます。SPA かどうか、ちょっと気になっています。

Step 9. createRouter

createRouter の中で go が定義されています。

/src/client/app/router.ts#L21-L128
export function createRouter(
  loadComponent: (route: Route) => Component | Promise<Component>,
  fallbackComponent?: Component
): Router {
  const route = reactive(getDefaultRoute())
  const inBrowser = typeof window !== 'undefined'


  function go(href?: string) {
    href = href || (inBrowser ? location.href : '/')
    if (inBrowser) {
      // save scroll position before changing url
      history.replaceState({ scrollPosition: window.scrollY }, document.title)
      history.pushState(null, '', href)
    }
    return loadPage(href)
  }


  async function loadPage(href: string, scrollPosition = 0) {
    // we are just using URL to parse the pathname and hash - the base doesn't
    // matter and is only passed to support same-host hrefs.
    const targetLoc = new URL(href, `http://vuejs.org`)
    const pendingPath = (route.path = targetLoc.pathname)


    try {
      //
      // - loadComponent 
      //   - Step 6 の第一引数をリンク先からご確認ください。
      //   - route.path で指定されたパスにある markdown ファイルを返します。
      //
      // - route
      //   - Route 型です。
      //     export interface Route {
      //       path: string
      //       contentComponent: Component | null
      //     }
      //   - この行 `let comp = loadComponent(route)` では、まだ
      //     contentComponent に Component が、代入されていません。
      //
      //
      let comp = loadComponent(route)
      // only await if it returns a Promise - this allows sync resolution
      // on initial render in SSR.
      if ('then' in comp && typeof comp.then === 'function') {
        comp = await comp
      }
      if (route.path === pendingPath) {
        if (!comp) {
          throw new Error(`Invalid route component: ${comp}`)
        }


        // ---- ポイント ----
        //     ここでルートが変化します。
        //     /src/client/app/components/Content.ts も変化します。  
        route.contentComponent = markRaw(comp)
        if (inBrowser) {
          await nextTick()


          if (targetLoc.hash && !scrollPosition) {
            const target = document.querySelector(targetLoc.hash) as HTMLElement
            if (target) {
              scrollPosition = target.offsetTop
            }
          }


          window.scrollTo({
            left: 0,
            top: scrollPosition,
            behavior: 'auto'
          })
        }
      }
    } catch (err) {
      if (!err.message.match(/fetch/)) {
        console.error(err)
      }
      if (route.path === pendingPath) {
        route.contentComponent = fallbackComponent
          ? markRaw(fallbackComponent)
          : null
      }
    }
  }

  // a タグにイベントを付与している。
  // これで SPA のような動作を実現しているはず...
  //
  // if (inBrowser) { となっていていr
  // dev 時のみで build 時も探したけど見当たらない...
  // build 時も動作するのかな。確認しないと。
  // -> build して Netlify にあげて、動作していることを確認。
  if (inBrowser) {
    window.addEventListener(
      'click',
      (e) => {
        const link = (e.target as Element).closest('a')
        if (link) {
          const { href, protocol, hostname, pathname, hash, target } = link
          const currentUrl = window.location
          // only intercept inbound links
          if (
            target !== `_blank` &&
            protocol === currentUrl.protocol &&
            hostname === currentUrl.hostname
          ) {
            if (pathname === currentUrl.pathname) {
              // smooth scroll bewteen hash anchors in the same page
              if (hash !== currentUrl.hash) {
                e.preventDefault()
                window.scrollTo({
                  left: 0,
                  top: link.offsetTop,
                  behavior: 'smooth'
                })
              }
            } else {
              e.preventDefault()
              go(href)
            }
          }
        }
      },
      { capture: true }
    )


    window.addEventListener('popstate', (e) => {
      loadPage(location.href, (e.state && e.state.scrollPosition) || 0)
    })
  }


  return {
    route,
    go
  }
}

おわりに

以上になります。ありがとうございました。次は、以下の記事になります。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away