refactor router [continued]

pull/1/head
Evan You 4 years ago
parent 61fdaad519
commit 2d4068bfb7

@ -1,6 +1,34 @@
import { toRef } from 'vue'
import { useRoute } from './router'
import { ref, provide, inject } from 'vue'
/**
* @typedef {{
* a: 1
* }} PageData
*
* @typedef {import('vue').Ref<PageData>} PageDataRef
*/
/**
* @type {import('vue').InjectionKey<PageDataRef>}
*/
const pageDataSymbol = Symbol()
export function initPageData() {
const data = ref()
provide(pageDataSymbol, data)
return data
}
/**
* @returns {PageDataRef}
*/
export function usePageData() {
return toRef(useRoute(), 'pageData')
const data = inject(pageDataSymbol)
if (__DEV__ && !data) {
throw new Error(
'usePageData() is called without initPageData() in an ancestor component.'
)
}
// @ts-ignore
return data
}

@ -1,14 +1,11 @@
import { shallowReactive, provide, inject, nextTick } from 'vue'
import Theme from '/@theme/index'
import { hot } from '@hmr'
const NotFound = Theme.NotFound || (() => '404 Not Found')
import { reactive, provide, inject, nextTick, markRaw } from 'vue'
/**
* @typedef {import('vue').Component} Component
*
* @typedef {{
* path: string
* contentComponent: import('vue').Component | null
* pageData: { path: string } | null
* contentComponent: Component | null
* }} Route
*
* @typedef {{
@ -27,29 +24,18 @@ const RouterSymbol = Symbol()
*/
const getDefaultRoute = () => ({
path: '/',
contentComponent: null,
pageData: null
contentComponent: null
})
/**
* @param {(route: Route) => Component | Promise<Component>} loadComponent
* @param {Component} [fallbackComponent]
* @returns {Router}
*/
export function initRouter() {
const route = shallowReactive(getDefaultRoute())
export function initRouter(loadComponent, fallbackComponent) {
const route = reactive(getDefaultRoute())
const inBrowser = typeof window !== 'undefined'
if (__DEV__ && inBrowser) {
// hot reload pageData
hot.on('vitepress:pageData', (data) => {
if (
data.path.replace(/\.md$/, '') ===
location.pathname.replace(/\.html$/, '')
) {
route.pageData = data.pageData
}
})
}
/**
* @param {string} href
* @returns {Promise<void>}
@ -68,26 +54,22 @@ export function initRouter() {
* @param {number} scrollPosition
* @returns {Promise<void>}
*/
function loadPage(href, scrollPosition = 0) {
const targetLoc = new URL(href)
async function loadPage(href, 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)
let pagePath = pendingPath.replace(/\.html$/, '')
if (pagePath.endsWith('/')) {
pagePath += 'index'
}
if (__DEV__) {
// awlays force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} else {
pagePath += `.md.js`
}
return import(pagePath)
.then(async (m) => {
if (route.path === pendingPath) {
route.contentComponent = m.default
route.pageData = m.__pageData
try {
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) {
route.contentComponent = markRaw(comp)
if (inBrowser) {
await nextTick()
if (targetLoc.hash && !scrollPosition) {
@ -106,14 +88,16 @@ export function initRouter() {
behavior: 'auto'
})
}
})
.catch((err) => {
if (!err.message.match(/fetch/)) {
throw err
} else if (route.path === pendingPath) {
route.contentComponent = NotFound
}
})
}
} catch (err) {
if (!err.message.match(/fetch/)) {
throw err
} else if (route.path === pendingPath) {
route.contentComponent = fallbackComponent
? markRaw(fallbackComponent)
: null
}
}
}
if (inBrowser) {
@ -158,7 +142,7 @@ export function initRouter() {
* @param {*} e
*/
(e) => {
loadPage((e.state && e.state.scrollPosition) || 0)
loadPage(location.href, (e.state && e.state.scrollPosition) || 0)
}
)
}

@ -1,15 +1,14 @@
import serialized from '@siteData'
import { hot } from '@hmr'
import { shallowRef, readonly } from 'vue'
import { ref, readonly } from 'vue'
/**
* @param {string} data
*/
const parse = (data) =>
__DEV__ ? readonly(JSON.parse(data)) : JSON.parse(data)
const parse = (data) => readonly(JSON.parse(data))
// site data
const siteDataRef = shallowRef(parse(serialized))
const siteDataRef = ref(parse(serialized))
export function useSiteData() {
return siteDataRef

@ -2,3 +2,4 @@
// so the user can do `import { usePageData } from 'vitepress'`
export { useSiteData } from './composables/siteData'
export { usePageData } from './composables/pageData'
export { useRouter, useRoute } from './composables/router'

@ -1,13 +1,58 @@
import { createApp, h } from 'vue'
import { createApp, h, readonly } from 'vue'
import { Content } from './components/Content'
import { initRouter } from './composables/router'
import { useSiteData } from './composables/siteData'
import { usePageData } from './composables/pageData'
import { initPageData, usePageData } from './composables/pageData'
import Theme from '/@theme/index'
import { hot } from '@hmr'
const inBrowser = typeof window !== 'undefined'
const NotFound = Theme.NotFound || (() => '404 Not Found')
const App = {
setup() {
initRouter()
const pageDataRef = initPageData()
initRouter((route) => {
let pagePath = route.path.replace(/\.html$/, '')
if (pagePath.endsWith('/')) {
pagePath += 'index'
}
if (__DEV__) {
// awlays force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} else {
// in production, each .md file is built into a .md.js file following
// the path conversion scheme.
pagePath += `.md.js`
}
if (inBrowser) {
// in browser: native dynamic import
return import(pagePath).then((m) => {
pageDataRef.value = readonly(m.__pageData)
return m.default
})
} else {
// SSR, sync require
return require(pagePath).default
}
}, NotFound)
if (__DEV__ && inBrowser) {
// hot reload pageData
hot.on('vitepress:pageData', (data) => {
if (
data.path.replace(/\.md$/, '') ===
location.pathname.replace(/\.html$/, '')
) {
pageDataRef.value = data.pageData
}
})
}
return () => h(Theme.Layout)
}
}

Loading…
Cancel
Save