refactor router

pull/1/head
Evan You 5 years ago
parent 4d09d8c3da
commit 61fdaad519

@ -10,27 +10,35 @@ const NotFound = Theme.NotFound || (() => '404 Not Found')
* contentComponent: import('vue').Component | null * contentComponent: import('vue').Component | null
* pageData: { path: string } | null * pageData: { path: string } | null
* }} Route * }} Route
*
* @typedef {{
* route: Route
* go: (href: string) => Promise<void>
* }} Router
*/ */
/** /**
* @type {import('vue').InjectionKey<Route>} * @type {import('vue').InjectionKey<Router>}
*/ */
const RouteSymbol = Symbol() const RouterSymbol = Symbol()
/** /**
* @returns {Route} * @returns {Route}
*/ */
const getDefaultRoute = () => ({ const getDefaultRoute = () => ({
path: location.pathname, path: '/',
contentComponent: null, contentComponent: null,
pageData: null pageData: null
}) })
export function useRouter() { /**
const loc = location * @returns {Router}
*/
export function initRouter() {
const route = shallowReactive(getDefaultRoute()) const route = shallowReactive(getDefaultRoute())
const inBrowser = typeof window !== 'undefined'
if (__DEV__) { if (__DEV__ && inBrowser) {
// hot reload pageData // hot reload pageData
hot.on('vitepress:pageData', (data) => { hot.on('vitepress:pageData', (data) => {
if ( if (
@ -42,119 +50,151 @@ export function useRouter() {
}) })
} }
window.addEventListener( /**
'click', * @param {string} href
/** * @returns {Promise<void>}
* @param {*} e */
*/ function go(href) {
(e) => { if (inBrowser) {
if (e.target.tagName === 'A') { // save scroll position before changing url
const { href, target } = e.target history.replaceState({ scrollPosition: window.scrollY }, document.title)
const url = new URL(href) history.pushState(null, '', href)
if ( }
target !== `_blank` && return loadPage(href)
url.protocol === loc.protocol && }
url.hostname === loc.hostname
) { /**
if (url.pathname === loc.pathname) { * @param {string} href
// smooth scroll bewteen hash anchors in the same page * @param {number} scrollPosition
if (url.hash !== loc.hash) { * @returns {Promise<void>}
*/
function loadPage(href, scrollPosition = 0) {
const targetLoc = new URL(href)
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
await nextTick()
if (targetLoc.hash && !scrollPosition) {
/**
* @type {HTMLElement | null}
*/
const target = document.querySelector(targetLoc.hash)
if (target) {
scrollPosition = target.offsetTop
}
}
window.scrollTo({
left: 0,
top: scrollPosition,
behavior: 'auto'
})
}
})
.catch((err) => {
if (!err.message.match(/fetch/)) {
throw err
} else if (route.path === pendingPath) {
route.contentComponent = NotFound
}
})
}
if (inBrowser) {
window.addEventListener(
'click',
/**
* @param {*} e
*/
(e) => {
if (e.target.tagName === 'A') {
const { href, target } = e.target
const targetUrl = new URL(href)
const currentUrl = window.location
if (
target !== `_blank` &&
targetUrl.protocol === currentUrl.protocol &&
targetUrl.hostname === currentUrl.hostname
) {
if (targetUrl.pathname === currentUrl.pathname) {
// smooth scroll bewteen hash anchors in the same page
if (targetUrl.hash !== currentUrl.hash) {
e.preventDefault()
window.scrollTo({
left: 0,
top: e.target.offsetTop,
behavior: 'smooth'
})
}
} else {
e.preventDefault() e.preventDefault()
window.scrollTo({ go(href)
left: 0,
top: e.target.offsetTop,
behavior: 'smooth'
})
} }
} else {
e.preventDefault()
// save scroll position before changing url
saveScrollPosition()
history.pushState(null, '', href)
loadPage(route)
} }
} }
},
{ capture: true }
)
window.addEventListener(
'popstate',
/**
* @param {*} e
*/
(e) => {
loadPage((e.state && e.state.scrollPosition) || 0)
} }
}, )
{ capture: true } }
)
window.addEventListener(
'popstate',
/**
* @param {*} e
*/
(e) => {
loadPage(route, (e.state && e.state.scrollPosition) || 0)
}
)
provide(RouteSymbol, route) /**
* @type {Router}
*/
const router = {
route,
go
}
loadPage(route) provide(RouterSymbol, router)
}
export function useRoute() { loadPage(location.href)
return inject(RouteSymbol) || getDefaultRoute()
return router
} }
/** /**
* @param {Route} route * @return {Router}
* @param {number} scrollPosition
*/ */
function loadPage(route, scrollPosition = 0) { export function useRouter() {
const pendingPath = (route.path = location.pathname) const router = inject(RouterSymbol)
let pagePath = pendingPath.replace(/\.html$/, '') if (__DEV__ && !router) {
if (pagePath.endsWith('/')) { throw new Error(
pagePath += 'index' 'useRouter() is called without initRouter() in an ancestor component.'
} )
if (__DEV__) {
// awlays force re-fetch content in dev
pagePath += `.md?t=${Date.now()}`
} else {
pagePath += `.md.js`
} }
// @ts-ignore
import(pagePath) return router
.then(async (m) => {
if (route.path === pendingPath) {
route.contentComponent = m.default
route.pageData = m.__pageData
await nextTick()
if (location.hash && !scrollPosition) {
/**
* @type {HTMLElement | null}
*/
const target = document.querySelector(location.hash)
if (target) {
scrollPosition = target.offsetTop
}
}
window.scrollTo({
left: 0,
top: scrollPosition,
behavior: 'auto'
})
}
})
.catch((err) => {
if (!err.message.match(/fetch/)) {
throw err
} else if (route.path === pendingPath) {
route.contentComponent = NotFound
}
})
} }
function saveScrollPosition() { /**
history.replaceState( * @returns {Route}
{ */
scrollPosition: window.scrollY export function useRoute() {
}, return useRouter().route
document.title,
''
)
} }

@ -1,17 +1,13 @@
import { createApp, h } from 'vue' import { createApp, h } from 'vue'
import { Content } from './components/Content' import { Content } from './components/Content'
import { useRouter } from './composables/router' import { initRouter } from './composables/router'
import { useSiteData } from './composables/siteData' import { useSiteData } from './composables/siteData'
import { usePageData } from './composables/pageData' import { usePageData } from './composables/pageData'
import Theme from '/@theme/index' import Theme from '/@theme/index'
const App = { const App = {
setup() { setup() {
if (typeof window !== 'undefined') { initRouter()
useRouter()
} else {
// TODO inject static route for SSR
}
return () => h(Theme.Layout) return () => h(Theme.Layout)
} }
} }

@ -53,14 +53,15 @@ export async function buildClient(options: BuildOptions) {
// for each .md entry chunk, adjust its name to its correct path. // for each .md entry chunk, adjust its name to its correct path.
for (const name in bundle) { for (const name in bundle) {
const chunk = bundle[name] const chunk = bundle[name]
if ( if (chunk.type === 'chunk') {
chunk.type === 'chunk' && if (
chunk.isEntry && chunk.isEntry &&
chunk.facadeModuleId && chunk.facadeModuleId &&
chunk.facadeModuleId.endsWith('.md') chunk.facadeModuleId.endsWith('.md')
) { ) {
const relativePath = path.relative(root, chunk.facadeModuleId) const relativePath = path.relative(root, chunk.facadeModuleId)
chunk.fileName = relativePath + '.js' chunk.fileName = relativePath + '.js'
}
} }
} }
} }

Loading…
Cancel
Save