はじめに
- バージョン ... 0.2.0
- SHA-1 ハッシュ ... d58b0231f63362aadde2d75feebb7b8c567bad0f
- VitePress のディレクトリ構成
- src/client/app と src/client/theme-default のつなぎ込み
- Markdown ファイルと src/client/app, Vue インスタンス本体のつなぎ込み <-- いまココです。
- Vite と VitePress のつなぎ込み
- dev 編
- 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 行以下で)
リアクティブにしたルート route
を RouterSymbol
というシンボルに紐付けて管理しているようです。
Step 1.
route.contentComponent
に Vue コンポーネントに変換された Markdown ファイルがはいります。テーマを自作する際に <Content />
を参照します。以下が <Content />
にあたります。
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
」を保持しています。
export interface Route {
path: string
contentComponent: Component | null
}
Step 3.
route
は、リアクティブです。
const getDefaultRoute = (): Route => ({
path: '/',
contentComponent: null
})
// ...
// 中略
// ...
const route = reactive(getDefaultRoute())
Step 4.
router
から route
を取り出しています。
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 ファイルも、確認できました。
app.provide(RouterSymbol, router)
app.provide(pageDataSymbol, pageDataRef)
app.component('Content', Content)
app.component('Debug', __DEV__ ? Debug : () => null)
Step 6.
createRouter
で router
を作成しています。
const router = createRouter(
// ...
// 中略
// ...
)
Step 7.
router
のプロパティには route
と go
が定義されています。route
は "現在" の route
です。currentRoute とかの方がニュアンスはもしかしたら近かったかもしれません。go
の動作は、次節を参照してください。
export interface Router {
route: Route
go: (href?: string) => Promise<void>
}
Step 8.
router
の go
を実行すると「それに対応したコンポーネント contentComponent
」を fetch し、あとは mount すればページが表示されます。
// wait unitl page component is fetched before mounting
router.go().then(() => {
app.mount('#app')
})
どこかで、パスの変更を検知しているのかもしれませんが、それに類するものを見つけられずにいます。SPA かどうか、ちょっと気になっています。
Step 9. createRouter
createRouter
の中で go
が定義されています。
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
}
}
おわりに
以上になります。ありがとうございました。次は、以下の記事になります。