refactor: expose pageData on route instead of via usePageData()

- also fix inconsistent state & duplicate updates of the sidebar when
  switching pages
pull/131/head
Evan You 4 years ago
parent e682675f96
commit a6bfdd41fa

@ -9,6 +9,6 @@ export const Content = {
// in prod mode, enable intersectionObserver based pre-fetch.
usePrefetch()
}
return () => (route.contentComponent ? h(route.contentComponent) : null)
return () => (route.component ? h(route.component) : null)
}
}

@ -1,11 +1,8 @@
import { watchEffect, Ref } from 'vue'
import { PageDataRef } from './pageData'
import { HeadConfig, SiteData } from '../../../../types/shared'
import { Route } from '../router'
export function useUpdateHead(
pageDataRef: PageDataRef,
siteDataByRouteRef: Ref<SiteData>
) {
export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta'))
let isFirstUpdate = true
@ -28,7 +25,7 @@ export function useUpdateHead(
}
watchEffect(() => {
const pageData = pageDataRef.value
const pageData = route.data
const siteData = siteDataByRouteRef.value
const pageTitle = pageData && pageData.title
document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.title

@ -1,14 +0,0 @@
import { inject, InjectionKey, Ref } from 'vue'
import { PageData } from '../../../../types/shared'
export type PageDataRef = Ref<PageData>
export const pageDataSymbol: InjectionKey<PageDataRef> = Symbol()
export function usePageData(): PageDataRef {
const data = inject(pageDataSymbol)
if (!data) {
throw new Error('usePageData() is called without provider.')
}
return data
}

@ -1,12 +1,11 @@
// exports in this file are exposed to themes and md files via 'vitepress'
// so the user can do `import { usePageData } from 'vitepress'`
// so the user can do `import { useRoute, useSiteData } from 'vitepress'`
// theme types
export * from './theme'
// composables
export { useSiteData } from './composables/siteData'
export { usePageData } from './composables/pageData'
export { useSiteDataByRoute } from './composables/siteDataByRoute'
export { useRouter, useRoute, Router, Route } from './router'

@ -1,7 +1,6 @@
import { createApp as createClientApp, createSSRApp, ref, readonly } from 'vue'
import { createApp as createClientApp, createSSRApp } from 'vue'
import { createRouter, RouterSymbol } from './router'
import { useUpdateHead } from './composables/head'
import { pageDataSymbol } from './composables/pageData'
import { Content } from './components/Content'
import Debug from './components/Debug.vue'
import Theme from '/@theme/index'
@ -12,22 +11,6 @@ import { siteDataRef } from './composables/siteData'
const NotFound = Theme.NotFound || (() => '404 Not Found')
export function createApp() {
// unlike site data which is static across all requests, page data is
// distinct per-request.
const pageDataRef = ref()
if (import.meta.hot) {
// hot reload pageData
import.meta.hot!.on('vitepress:pageData', (data) => {
if (
data.path.replace(/(\bindex)?\.md$/, '') ===
location.pathname.replace(/(\bindex)?\.html$/, '')
) {
pageDataRef.value = data.pageData
}
})
}
let isInitialPageLoad = inBrowser
let initialPath: string
@ -48,27 +31,32 @@ export function createApp() {
if (inBrowser) {
isInitialPageLoad = false
// in browser: native dynamic import
return import(/*@vite-ignore*/ pagePath).then((page) => {
if (page.__pageData) {
pageDataRef.value = readonly(JSON.parse(page.__pageData))
}
return page.default
})
return import(/*@vite-ignore*/ pagePath)
} else {
// SSR, sync require
const page = require(pagePath)
pageDataRef.value = JSON.parse(page.__pageData)
return page.default
return require(pagePath)
}
}, NotFound)
// update route.data on HMR updates of active page
if (import.meta.hot) {
// hot reload pageData
import.meta.hot!.on('vitepress:pageData', (payload) => {
if (
payload.path.replace(/(\bindex)?\.md$/, '') ===
location.pathname.replace(/(\bindex)?\.html$/, '')
) {
router.route.data = payload.pageData
}
})
}
const app =
process.env.NODE_ENV === 'production'
? createSSRApp(Theme.Layout)
: createClientApp(Theme.Layout)
app.provide(RouterSymbol, router)
app.provide(pageDataSymbol, pageDataRef)
app.component('Content', Content)
app.component(
@ -80,7 +68,7 @@ export function createApp() {
if (inBrowser) {
// dynamically update head tags
useUpdateHead(pageDataRef, siteDataByRouteRef)
useUpdateHead(router.route, siteDataByRouteRef)
}
Object.defineProperties(app.config.globalProperties, {
@ -96,7 +84,7 @@ export function createApp() {
},
$page: {
get() {
return pageDataRef.value
return router.route.data
}
},
$theme: {

@ -1,9 +1,11 @@
import { reactive, inject, markRaw, nextTick } from 'vue'
import { reactive, inject, markRaw, nextTick, readonly } from 'vue'
import type { Component, InjectionKey } from 'vue'
import { PageData } from '../../../types/shared'
export interface Route {
path: string
contentComponent: Component | null
data: PageData
component: Component | null
}
export interface Router {
@ -19,19 +21,26 @@ const fakeHost = `http://a.com`
const getDefaultRoute = (): Route => ({
path: '/',
contentComponent: null
component: null,
// this will be set upon initial page load, which is before
// the app is mounted, so it's guaranteed to be avaiable in
// components
data: null as any
})
interface PageModule {
__pageData: string
default: Component
}
export function createRouter(
loadComponent: (route: Route) => Component | Promise<Component>,
loadPageModule: (route: Route) => PageModule | Promise<PageModule>,
fallbackComponent?: Component
): Router {
// TODO: the cast shouldn't be necessary
const route = reactive(getDefaultRoute()) as Route
const route = reactive(getDefaultRoute())
const inBrowser = typeof window !== 'undefined'
function go(href?: string) {
href = href || (inBrowser ? location.href : '/')
function go(href: string = inBrowser ? location.href : '/') {
// ensure correct deep link so page refresh lands on correct files.
const url = new URL(href, fakeHost)
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
@ -46,21 +55,30 @@ export function createRouter(
return loadPage(href)
}
let latestPendingPath: string | null = null
async function loadPage(href: string, scrollPosition = 0) {
const targetLoc = new URL(href, fakeHost)
const pendingPath = (route.path = targetLoc.pathname)
const pendingPath = (latestPendingPath = targetLoc.pathname)
try {
let comp = loadComponent(route)
let page = loadPageModule(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 ('then' in page && typeof page.then === 'function') {
page = await page
}
if (route.path === pendingPath) {
if (latestPendingPath === pendingPath) {
latestPendingPath = null
const { default: comp, __pageData } = page as PageModule
if (!comp) {
throw new Error(`Invalid route component: ${comp}`)
}
route.contentComponent = markRaw(comp)
route.path = pendingPath
route.component = markRaw(comp)
route.data = readonly(JSON.parse(__pageData)) as PageData
if (inBrowser) {
nextTick(() => {
if (targetLoc.hash && !scrollPosition) {
@ -80,10 +98,10 @@ export function createRouter(
if (!err.message.match(/fetch/)) {
console.error(err)
}
if (route.path === pendingPath) {
route.contentComponent = fallbackComponent
? markRaw(fallbackComponent)
: null
if (latestPendingPath === pendingPath) {
latestPendingPath = null
route.path = pendingPath
route.component = fallbackComponent ? markRaw(fallbackComponent) : null
}
}
}

@ -54,7 +54,7 @@ import Home from './components/Home.vue'
import ToggleSideBarButton from './components/ToggleSideBarButton.vue'
import SideBar from './components/SideBar.vue'
import Page from './components/Page.vue'
import { useRoute, usePageData, useSiteData, useSiteDataByRoute } from 'vitepress'
import { useRoute, useSiteData, useSiteDataByRoute } from 'vitepress'
export default {
components: {
@ -67,16 +67,15 @@ export default {
setup() {
const route = useRoute()
const pageData = usePageData()
const siteData = useSiteData()
const siteRouteData = useSiteDataByRoute()
const openSideBar = ref(false)
const enableHome = computed(() => !!pageData.value.frontmatter.home)
const enableHome = computed(() => !!route.data.frontmatter.home)
const showNavbar = computed(() => {
const { themeConfig } = siteRouteData.value
const { frontmatter } = pageData.value
const { frontmatter } = route.data
if (
frontmatter.navbar === false
|| themeConfig.navbar === false) {
@ -91,7 +90,7 @@ export default {
})
const showSidebar = computed(() => {
const { frontmatter } = pageData.value
const { frontmatter } = route.data
const { themeConfig } = siteRouteData.value
return (
!frontmatter.home

@ -57,7 +57,7 @@
import { defineComponent, computed } from 'vue'
import NavBarLink from './NavBarLink.vue'
import { withBase } from '../utils'
import { usePageData, useSiteData } from 'vitepress'
import { useRoute, useSiteData } from 'vitepress'
export default defineComponent({
components: {
@ -65,10 +65,10 @@ export default defineComponent({
},
setup() {
const pageData = usePageData()
const route = useRoute()
const siteData = useSiteData()
const data = computed(() => pageData.value.frontmatter)
const data = computed(() => route.data.frontmatter)
const actionLink = computed(() => ({
link: data.value.actionLink,
text: data.value.actionText

@ -1,13 +1,14 @@
import { computed } from 'vue'
import { usePageData, useSiteData } from 'vitepress'
import { useRoute, useSiteData } from 'vitepress'
import { DefaultTheme } from '../config'
export default {
setup() {
const pageData = usePageData()
const route = useRoute()
// TODO: could this be useSiteData<DefaultTheme.Config> or is the siteData
// resolved and has a different structure?
const siteData = useSiteData()
const resolveLink = (targetLink: string) => {
let target: DefaultTheme.SideBarLink | undefined
Object.keys(siteData.value.themeConfig.sidebar).some((k) => {
@ -24,27 +25,33 @@ export default {
})
return target
}
const next = computed(() => {
if (pageData.value.frontmatter.next === false) {
const pageData = route.data
if (pageData.frontmatter.next === false) {
return undefined
}
if (typeof pageData.value.frontmatter.next === 'string') {
return resolveLink(pageData.value.frontmatter.next)
if (typeof pageData.frontmatter.next === 'string') {
return resolveLink(pageData.frontmatter.next)
}
return pageData.value.next
return pageData.next
})
const prev = computed(() => {
if (pageData.value.frontmatter.prev === false) {
const pageData = route.data
if (pageData.frontmatter.prev === false) {
return undefined
}
if (typeof pageData.value.frontmatter.prev === 'string') {
return resolveLink(pageData.value.frontmatter.prev)
if (typeof pageData.frontmatter.prev === 'string') {
return resolveLink(pageData.frontmatter.prev)
}
return pageData.value.prev
return pageData.prev
})
const hasLinks = computed(() => {
return !!next || !!prev
})
return {
next,
prev,

@ -1,7 +1,7 @@
import { computed } from 'vue'
import OutboundLink from './icons/OutboundLink.vue'
import { endingSlashRE, isExternal } from '../utils'
import { usePageData, useSiteData } from 'vitepress'
import { useRoute, useSiteData } from 'vitepress'
import { DefaultTheme } from '../config'
function createEditLink(
@ -42,14 +42,15 @@ export default {
},
setup() {
const pageData = usePageData()
const route = useRoute()
const siteData = useSiteData<DefaultTheme.Config>()
const editLink = computed(() => {
const pageData = route.data
const showEditLink: boolean | undefined =
pageData.value.frontmatter.editLink == null
pageData.frontmatter.editLink == null
? siteData.value.themeConfig.editLinks
: pageData.value.frontmatter.editLink
: pageData.frontmatter.editLink
const {
repo,
docsDir = '',
@ -57,7 +58,7 @@ export default {
docsRepo = repo
} = siteData.value.themeConfig
const { relativePath } = pageData.value
const { relativePath } = pageData
if (showEditLink && relativePath && repo) {
return createEditLink(
repo,
@ -69,6 +70,7 @@ export default {
}
return null
})
const editLinkText = computed(
() => siteData.value.themeConfig.editLinkText || 'Edit this page'
)

@ -1,9 +1,4 @@
import {
usePageData,
useRoute,
useSiteDataByRoute,
useSiteData
} from 'vitepress'
import { useRoute, useSiteDataByRoute, useSiteData } from 'vitepress'
import { computed, h, FunctionalComponent, VNode } from 'vue'
import { Header } from '../../../../types/shared'
import { isActive, joinUrl, getPathDirName } from '../utils'
@ -19,12 +14,11 @@ const SideBarItem: FunctionalComponent<{
} = props
const route = useRoute()
const pageData = usePageData()
const siteData = useSiteData()
const link = resolveLink(siteData.value.base, relLink || '')
const active = isActive(route, link)
const headers = pageData.value.headers
const headers = route.data.headers
return h('li', { class: 'sidebar-item' }, [
createLink(active, text, link),
@ -39,9 +33,8 @@ export default {
},
setup() {
const pageData = usePageData()
const siteData = useSiteDataByRoute()
const route = useRoute()
const siteData = useSiteDataByRoute()
useActiveSidebarLinks()
@ -50,7 +43,7 @@ export default {
const {
headers,
frontmatter: { sidebar, sidebarDepth = 2 }
} = pageData.value
} = route.data
if (sidebar === 'auto') {
// auto, render headers of current page

@ -50,6 +50,8 @@ export function useActiveSidebarLinks() {
(scrollTop >= getAnchorTop(anchor) &&
(!nextAnchor || scrollTop < getAnchorTop(nextAnchor)))
// TODO: fix case when at page bottom
if (isActive) {
const targetHash = decode(anchor.hash)
history.replaceState(null, document.title, targetHash)

Loading…
Cancel
Save