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 5 years ago
parent e682675f96
commit a6bfdd41fa

@ -9,6 +9,6 @@ export const Content = {
// in prod mode, enable intersectionObserver based pre-fetch. // in prod mode, enable intersectionObserver based pre-fetch.
usePrefetch() 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 { watchEffect, Ref } from 'vue'
import { PageDataRef } from './pageData'
import { HeadConfig, SiteData } from '../../../../types/shared' import { HeadConfig, SiteData } from '../../../../types/shared'
import { Route } from '../router'
export function useUpdateHead( export function useUpdateHead(route: Route, siteDataByRouteRef: Ref<SiteData>) {
pageDataRef: PageDataRef,
siteDataByRouteRef: Ref<SiteData>
) {
const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta')) const metaTags: HTMLElement[] = Array.from(document.querySelectorAll('meta'))
let isFirstUpdate = true let isFirstUpdate = true
@ -28,7 +25,7 @@ export function useUpdateHead(
} }
watchEffect(() => { watchEffect(() => {
const pageData = pageDataRef.value const pageData = route.data
const siteData = siteDataByRouteRef.value const siteData = siteDataByRouteRef.value
const pageTitle = pageData && pageData.title const pageTitle = pageData && pageData.title
document.title = (pageTitle ? pageTitle + ` | ` : ``) + siteData.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' // 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 // theme types
export * from './theme' export * from './theme'
// composables // composables
export { useSiteData } from './composables/siteData' export { useSiteData } from './composables/siteData'
export { usePageData } from './composables/pageData'
export { useSiteDataByRoute } from './composables/siteDataByRoute' export { useSiteDataByRoute } from './composables/siteDataByRoute'
export { useRouter, useRoute, Router, Route } from './router' 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 { createRouter, RouterSymbol } from './router'
import { useUpdateHead } from './composables/head' import { useUpdateHead } from './composables/head'
import { pageDataSymbol } from './composables/pageData'
import { Content } from './components/Content' import { Content } from './components/Content'
import Debug from './components/Debug.vue' import Debug from './components/Debug.vue'
import Theme from '/@theme/index' import Theme from '/@theme/index'
@ -12,22 +11,6 @@ import { siteDataRef } from './composables/siteData'
const NotFound = Theme.NotFound || (() => '404 Not Found') const NotFound = Theme.NotFound || (() => '404 Not Found')
export function createApp() { 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 isInitialPageLoad = inBrowser
let initialPath: string let initialPath: string
@ -48,27 +31,32 @@ export function createApp() {
if (inBrowser) { if (inBrowser) {
isInitialPageLoad = false isInitialPageLoad = false
// in browser: native dynamic import // in browser: native dynamic import
return import(/*@vite-ignore*/ pagePath).then((page) => { return import(/*@vite-ignore*/ pagePath)
if (page.__pageData) {
pageDataRef.value = readonly(JSON.parse(page.__pageData))
}
return page.default
})
} else { } else {
// SSR, sync require // SSR, sync require
const page = require(pagePath) return require(pagePath)
pageDataRef.value = JSON.parse(page.__pageData)
return page.default
} }
}, NotFound) }, 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 = const app =
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
? createSSRApp(Theme.Layout) ? createSSRApp(Theme.Layout)
: createClientApp(Theme.Layout) : createClientApp(Theme.Layout)
app.provide(RouterSymbol, router) app.provide(RouterSymbol, router)
app.provide(pageDataSymbol, pageDataRef)
app.component('Content', Content) app.component('Content', Content)
app.component( app.component(
@ -80,7 +68,7 @@ export function createApp() {
if (inBrowser) { if (inBrowser) {
// dynamically update head tags // dynamically update head tags
useUpdateHead(pageDataRef, siteDataByRouteRef) useUpdateHead(router.route, siteDataByRouteRef)
} }
Object.defineProperties(app.config.globalProperties, { Object.defineProperties(app.config.globalProperties, {
@ -96,7 +84,7 @@ export function createApp() {
}, },
$page: { $page: {
get() { get() {
return pageDataRef.value return router.route.data
} }
}, },
$theme: { $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 type { Component, InjectionKey } from 'vue'
import { PageData } from '../../../types/shared'
export interface Route { export interface Route {
path: string path: string
contentComponent: Component | null data: PageData
component: Component | null
} }
export interface Router { export interface Router {
@ -19,19 +21,26 @@ const fakeHost = `http://a.com`
const getDefaultRoute = (): Route => ({ const getDefaultRoute = (): Route => ({
path: '/', 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( export function createRouter(
loadComponent: (route: Route) => Component | Promise<Component>, loadPageModule: (route: Route) => PageModule | Promise<PageModule>,
fallbackComponent?: Component fallbackComponent?: Component
): Router { ): Router {
// TODO: the cast shouldn't be necessary const route = reactive(getDefaultRoute())
const route = reactive(getDefaultRoute()) as Route
const inBrowser = typeof window !== 'undefined' const inBrowser = typeof window !== 'undefined'
function go(href?: string) { function go(href: string = inBrowser ? location.href : '/') {
href = href || (inBrowser ? location.href : '/')
// ensure correct deep link so page refresh lands on correct files. // ensure correct deep link so page refresh lands on correct files.
const url = new URL(href, fakeHost) const url = new URL(href, fakeHost)
if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) { if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
@ -46,21 +55,30 @@ export function createRouter(
return loadPage(href) return loadPage(href)
} }
let latestPendingPath: string | null = null
async function loadPage(href: string, scrollPosition = 0) { async function loadPage(href: string, scrollPosition = 0) {
const targetLoc = new URL(href, fakeHost) const targetLoc = new URL(href, fakeHost)
const pendingPath = (route.path = targetLoc.pathname) const pendingPath = (latestPendingPath = targetLoc.pathname)
try { try {
let comp = loadComponent(route) let page = loadPageModule(route)
// only await if it returns a Promise - this allows sync resolution // only await if it returns a Promise - this allows sync resolution
// on initial render in SSR. // on initial render in SSR.
if ('then' in comp && typeof comp.then === 'function') { if ('then' in page && typeof page.then === 'function') {
comp = await comp page = await page
} }
if (route.path === pendingPath) { if (latestPendingPath === pendingPath) {
latestPendingPath = null
const { default: comp, __pageData } = page as PageModule
if (!comp) { if (!comp) {
throw new Error(`Invalid route component: ${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) { if (inBrowser) {
nextTick(() => { nextTick(() => {
if (targetLoc.hash && !scrollPosition) { if (targetLoc.hash && !scrollPosition) {
@ -80,10 +98,10 @@ export function createRouter(
if (!err.message.match(/fetch/)) { if (!err.message.match(/fetch/)) {
console.error(err) console.error(err)
} }
if (route.path === pendingPath) { if (latestPendingPath === pendingPath) {
route.contentComponent = fallbackComponent latestPendingPath = null
? markRaw(fallbackComponent) route.path = pendingPath
: null route.component = fallbackComponent ? markRaw(fallbackComponent) : null
} }
} }
} }

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

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

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

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

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

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

Loading…
Cancel
Save