router.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import { reactive, inject, markRaw, nextTick, readonly } from 'vue';
  2. import { inBrowser } from './utils';
  3. import { siteDataRef } from './data';
  4. export const RouterSymbol = Symbol();
  5. // we are just using URL to parse the pathname and hash - the base doesn't
  6. // matter and is only passed to support same-host hrefs.
  7. const fakeHost = `http://a.com`;
  8. const notFoundPageData = {
  9. relativePath: '',
  10. title: '404',
  11. description: 'Not Found',
  12. headers: [],
  13. frontmatter: {},
  14. lastUpdated: 0
  15. };
  16. const getDefaultRoute = () => ({
  17. path: '/',
  18. component: null,
  19. data: notFoundPageData
  20. });
  21. export function createRouter(loadPageModule, fallbackComponent) {
  22. const route = reactive(getDefaultRoute());
  23. function go(href = inBrowser ? location.href : '/') {
  24. // ensure correct deep link so page refresh lands on correct files.
  25. const url = new URL(href, fakeHost);
  26. if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) {
  27. url.pathname += '.html';
  28. href = url.pathname + url.search + url.hash;
  29. }
  30. if (inBrowser) {
  31. // save scroll position before changing url
  32. history.replaceState({ scrollPosition: window.scrollY }, document.title);
  33. history.pushState(null, '', href);
  34. }
  35. return loadPage(href);
  36. }
  37. let latestPendingPath = null;
  38. async function loadPage(href, scrollPosition = 0, isRetry = false) {
  39. const targetLoc = new URL(href, fakeHost);
  40. const pendingPath = (latestPendingPath = targetLoc.pathname);
  41. try {
  42. let page = loadPageModule(pendingPath);
  43. // only await if it returns a Promise - this allows sync resolution
  44. // on initial render in SSR.
  45. if ('then' in page && typeof page.then === 'function') {
  46. page = await page;
  47. }
  48. if (latestPendingPath === pendingPath) {
  49. latestPendingPath = null;
  50. const { default: comp, __pageData } = page;
  51. if (!comp) {
  52. throw new Error(`Invalid route component: ${comp}`);
  53. }
  54. route.path = pendingPath;
  55. route.component = markRaw(comp);
  56. route.data = import.meta.env.PROD
  57. ? markRaw(JSON.parse(__pageData))
  58. : readonly(JSON.parse(__pageData));
  59. if (inBrowser) {
  60. nextTick(() => {
  61. if (targetLoc.hash && !scrollPosition) {
  62. let target = null;
  63. try {
  64. target = document.querySelector(decodeURIComponent(targetLoc.hash));
  65. }
  66. catch (e) {
  67. console.warn(e);
  68. }
  69. if (target) {
  70. scrollTo(target, targetLoc.hash);
  71. return;
  72. }
  73. }
  74. window.scrollTo(0, scrollPosition);
  75. });
  76. }
  77. }
  78. }
  79. catch (err) {
  80. if (!err.message.match(/fetch/)) {
  81. console.error(err);
  82. }
  83. // retry on fetch fail: the page to hash map may have been invalidated
  84. // because a new deploy happened while the page is open. Try to fetch
  85. // the updated pageToHash map and fetch again.
  86. if (!isRetry) {
  87. try {
  88. const res = await fetch(siteDataRef.value.base + 'hashmap.json');
  89. window.__VP_HASH_MAP__ = await res.json();
  90. await loadPage(href, scrollPosition, true);
  91. return;
  92. }
  93. catch (e) { }
  94. }
  95. if (latestPendingPath === pendingPath) {
  96. latestPendingPath = null;
  97. route.path = pendingPath;
  98. route.component = fallbackComponent ? markRaw(fallbackComponent) : null;
  99. route.data = notFoundPageData;
  100. }
  101. }
  102. }
  103. if (inBrowser) {
  104. window.addEventListener('click', (e) => {
  105. const link = e.target.closest('a');
  106. if (link) {
  107. const { href, protocol, hostname, pathname, hash, target } = link;
  108. const currentUrl = window.location;
  109. const extMatch = pathname.match(/\.\w+$/);
  110. // only intercept inbound links
  111. if (!e.ctrlKey &&
  112. !e.shiftKey &&
  113. !e.altKey &&
  114. !e.metaKey &&
  115. target !== `_blank` &&
  116. protocol === currentUrl.protocol &&
  117. hostname === currentUrl.hostname &&
  118. !(extMatch && extMatch[0] !== '.html')) {
  119. e.preventDefault();
  120. if (pathname === currentUrl.pathname) {
  121. // scroll between hash anchors in the same page
  122. if (hash && hash !== currentUrl.hash) {
  123. history.pushState(null, '', hash);
  124. // still emit the event so we can listen to it in themes
  125. window.dispatchEvent(new Event('hashchange'));
  126. // use smooth scroll when clicking on header anchor links
  127. scrollTo(link, hash, link.classList.contains('header-anchor'));
  128. }
  129. }
  130. else {
  131. go(href);
  132. }
  133. }
  134. }
  135. }, { capture: true });
  136. window.addEventListener('popstate', (e) => {
  137. loadPage(location.href, (e.state && e.state.scrollPosition) || 0);
  138. });
  139. window.addEventListener('hashchange', (e) => {
  140. e.preventDefault();
  141. });
  142. }
  143. return {
  144. route,
  145. go
  146. };
  147. }
  148. export function useRouter() {
  149. const router = inject(RouterSymbol);
  150. if (!router) {
  151. throw new Error('useRouter() is called without provider.');
  152. }
  153. // @ts-ignore
  154. return router;
  155. }
  156. export function useRoute() {
  157. return useRouter().route;
  158. }
  159. function scrollTo(el, hash, smooth = false) {
  160. let target = null;
  161. try {
  162. target = el.classList.contains('header-anchor')
  163. ? el
  164. : document.querySelector(decodeURIComponent(hash));
  165. }
  166. catch (e) {
  167. console.warn(e);
  168. }
  169. if (target) {
  170. let offset = siteDataRef.value.scrollOffset;
  171. if (typeof offset === 'string') {
  172. offset =
  173. document.querySelector(offset).getBoundingClientRect().bottom + 24;
  174. }
  175. const targetPadding = parseInt(window.getComputedStyle(target).paddingTop, 10);
  176. const targetTop = window.scrollY +
  177. target.getBoundingClientRect().top -
  178. offset +
  179. targetPadding;
  180. // only smooth scroll if distance is smaller than screen height.
  181. if (!smooth || Math.abs(targetTop - window.scrollY) > window.innerHeight) {
  182. window.scrollTo(0, targetTop);
  183. }
  184. else {
  185. window.scrollTo({
  186. left: 0,
  187. top: targetTop,
  188. behavior: 'smooth'
  189. });
  190. }
  191. }
  192. }