Skip to content

Commit

Permalink
perf: implement custom routes (#1447)
Browse files Browse the repository at this point in the history
Co-authored-by: Xinyu Liu <[email protected]>

BREAKING CHANGE: vue-router's route records have been replaced by custom route records to get better performance. It should not break common usage, but could be a potential breaking change for some themes. vue-router is suitable for SPAs, but not for static sites. It has a negative impact on the performance of vuepress sites, especially large-scale ones. In the long run we'll replace vue-router with a light-weighted custom router totally.
  • Loading branch information
Mister-Hope authored Feb 3, 2024
1 parent 5281a42 commit 7d37350
Show file tree
Hide file tree
Showing 65 changed files with 703 additions and 543 deletions.
10 changes: 10 additions & 0 deletions e2e/docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@ export default defineUserConfig({
bundler: E2E_BUNDLER === 'webpack' ? webpackBundler() : viteBundler(),

theme: e2eTheme(),

extendsPage: (page) => {
if (page.path === '/page-data/route-meta.html') {
page.routeMeta = {
a: 1,
b: 2,
...page.routeMeta,
}
}
},
})
4 changes: 4 additions & 0 deletions e2e/docs/404.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
routeMeta:
foo: bar
---
5 changes: 5 additions & 0 deletions e2e/docs/page-data/route-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
routeMeta:
a: 0
c: 3
---
43 changes: 43 additions & 0 deletions e2e/docs/router/resolve-route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## resolve

### Path

#### Index

- Clean URL: {{ JSON.stringify(resolveRoute('/')) }}
- HTML: {{ JSON.stringify(resolveRoute('/index.html')) }}
- Markdown: {{ JSON.stringify(resolveRoute('/README.md')) }}

#### Non-Index

- Clean URL: {{ JSON.stringify(resolveRoute('/router/resolve-route')) }}
- HTML: {{ JSON.stringify(resolveRoute('/router/resolve-route.html')) }}
- Markdown: {{ JSON.stringify(resolveRoute('/router/resolve-route.md')) }}

#### Non-ASCII

- Clean URL: {{ JSON.stringify(resolveRoute('/routes/non-ascii-paths/中文目录名/中文文件名')) }}
- HTML: {{ JSON.stringify(resolveRoute('/routes/non-ascii-paths/中文目录名/中文文件名.html')) }}
- Markdown: {{ JSON.stringify(resolveRoute('/routes/non-ascii-paths/中文目录名/中文文件名.md')) }}

#### Non-ASCII Encoded

- Clean URL: {{ JSON.stringify(resolveRoute(encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名'))) }}
- HTML: {{ JSON.stringify(resolveRoute(encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名.html'))) }}
- Markdown: {{ JSON.stringify(resolveRoute(encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名.md'))) }}

#### Non-Existent

- Clean URL: {{ JSON.stringify(resolveRoute('/non-existent')) }}
- HTML: {{ JSON.stringify(resolveRoute('/non-existent.html')) }}
- Markdown: {{ JSON.stringify(resolveRoute('/non-existent.md')) }}

#### Route Meta

- Clean URL: {{ JSON.stringify(resolveRoute('/page-data/route-meta')) }}
- HTML: {{ JSON.stringify(resolveRoute('/page-data/route-meta.html')) }}
- Markdown: {{ JSON.stringify(resolveRoute('/page-data/route-meta.md')) }}

<script setup>
import { resolveRoute } from 'vuepress/client'
</script>
66 changes: 66 additions & 0 deletions e2e/tests/router/resolve-route.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const testCases = [
{
selector: '#index',
expected: {
path: '/',
meta: {},
notFound: false,
},
},
{
selector: '#non-index',
expected: {
path: '/router/resolve-route.html',
meta: {},
notFound: false,
},
},
{
selector: '#non-ascii',
expected: {
path: encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名.html'),
meta: {},
notFound: false,
},
},
{
selector: '#non-ascii-encoded',
expected: {
path: encodeURI('/routes/non-ascii-paths/中文目录名/中文文件名.html'),
meta: {},
notFound: false,
},
},
{
selector: '#non-existent',
expected: {
path: '/non-existent.html',
meta: { foo: 'bar' },
notFound: true,
},
},
{
selector: '#route-meta',
expected: {
path: '/page-data/route-meta.html',
meta: { a: 0, b: 2, c: 3 },
notFound: false,
},
},
]

const parseResolvedRouteFromElement = (el: Cypress.JQueryWithSelector) =>
JSON.parse(/: (\{.*\})\s*$/.exec(el.text())![1])

it('should resolve routes correctly', () => {
cy.visit('/router/resolve-route.html')

testCases.forEach(({ selector, expected }) => {
cy.get(`.e2e-theme-content ${selector} + ul > li`).each((el) => {
const resolvedRoute = parseResolvedRouteFromElement(el)
expect(resolvedRoute.path).to.equal(expected.path)
expect(resolvedRoute.meta).to.deep.equal(expected.meta)
expect(resolvedRoute.notFound).to.equal(expected.notFound)
})
})
})
4 changes: 1 addition & 3 deletions packages/bundler-vite/src/build/resolvePageChunkFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ export const resolvePageChunkFiles = ({
output
.filter(
(item): item is OutputChunk =>
item.type === 'chunk' &&
(item.facadeModuleId === page.componentFilePath ||
item.facadeModuleId === page.dataFilePath),
item.type === 'chunk' && item.facadeModuleId === page.chunkFilePath,
)
.flatMap(({ fileName, imports, dynamicImports }) => [
fileName,
Expand Down
14 changes: 5 additions & 9 deletions packages/cli/src/commands/dev/handlePageAdd.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {
createPage,
preparePageChunk,
preparePageComponent,
preparePageData,
preparePagesComponents,
preparePagesData,
preparePagesRoutes,
prepareRoutes,
} from '@vuepress/core'
import type { App, Page } from '@vuepress/core'

Expand Down Expand Up @@ -33,12 +31,10 @@ export const handlePageAdd = async (

// prepare page files
await preparePageComponent(app, page)
await preparePageData(app, page)
await preparePageChunk(app, page)

// prepare pages entry
await preparePagesComponents(app)
await preparePagesData(app)
await preparePagesRoutes(app)
// prepare routes file
await prepareRoutes(app)

return page
}
18 changes: 5 additions & 13 deletions packages/cli/src/commands/dev/handlePageChange.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {
createPage,
preparePageChunk,
preparePageComponent,
preparePageData,
preparePagesComponents,
preparePagesData,
preparePagesRoutes,
prepareRoutes,
} from '@vuepress/core'
import type { App, Page } from '@vuepress/core'

Expand Down Expand Up @@ -36,21 +34,15 @@ export const handlePageChange = async (

// prepare page files
await preparePageComponent(app, pageNew)
await preparePageData(app, pageNew)
await preparePageChunk(app, pageNew)

const isPathChanged = pageOld.path !== pageNew.path
const isRouteMetaChanged =
JSON.stringify(pageOld.routeMeta) !== JSON.stringify(pageNew.routeMeta)

// prepare pages entry if the path is changed
if (isPathChanged) {
await preparePagesComponents(app)
await preparePagesData(app)
}

// prepare pages routes if the path or routeMeta is changed
// prepare routes file if the path or route meta is changed
if (isPathChanged || isRouteMetaChanged) {
await preparePagesRoutes(app)
await prepareRoutes(app)
}

return [pageOld, pageNew]
Expand Down
12 changes: 3 additions & 9 deletions packages/cli/src/commands/dev/handlePageUnlink.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
preparePagesComponents,
preparePagesData,
preparePagesRoutes,
} from '@vuepress/core'
import { prepareRoutes } from '@vuepress/core'
import type { App, Page } from '@vuepress/core'

/**
Expand All @@ -25,10 +21,8 @@ export const handlePageUnlink = async (
// remove the old page
app.pages.splice(pageIndex, 1)

// re-prepare page files
await preparePagesComponents(app)
await preparePagesData(app)
await preparePagesRoutes(app)
// re-prepare routes file
await prepareRoutes(app)

return page
}
4 changes: 1 addition & 3 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@
"external": [
"@internal/clientConfigs",
"@internal/layoutComponents",
"@internal/pagesComponents",
"@internal/pagesData",
"@internal/pagesRoutes",
"@internal/routes",
"@internal/siteData"
],
"format": [
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { clientConfigs } from '@internal/clientConfigs'
import { createApp, createSSRApp, h } from 'vue'
import { RouterView } from 'vue-router'
import { siteData } from './composables/index.js'
import { createVueRouter } from './router.js'
import { createVueRouter } from './createVueRouter.js'
import { setupGlobalComponents } from './setupGlobalComponents.js'
import { setupGlobalComputed } from './setupGlobalComputed.js'
import { setupUpdateHead } from './setupUpdateHead.js'
Expand Down
28 changes: 10 additions & 18 deletions packages/client/src/components/Content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { pagesComponents } from '@internal/pagesComponents'
import { computed, defineComponent, h } from 'vue'
import { computed, defineAsyncComponent, defineComponent, h } from 'vue'
import { usePageData } from '../composables/index.js'
import { resolveRoute } from '../router/index.js'

/**
* Markdown rendered content
Expand All @@ -10,28 +10,20 @@ export const Content = defineComponent({
name: 'Content',

props: {
pageKey: {
path: {
type: String,
required: false,
default: '',
},
},

setup(props) {
const page = usePageData()
const pageComponent = computed(
() => pagesComponents[props.pageKey || page.value.key],
)
return () =>
pageComponent.value
? // use page component
h(pageComponent.value)
: // fallback content
h(
'div',
__VUEPRESS_DEV__
? 'Page does not exist. This is a fallback content.'
: '404 Not Found',
)
const pageData = usePageData()
const pageComponent = computed(() => {
const route = resolveRoute(props.path || pageData.value.path)
return defineAsyncComponent(() => route.loader().then(({ comp }) => comp))
})

return () => h(pageComponent.value)
},
})
53 changes: 53 additions & 0 deletions packages/client/src/components/VPLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { h } from 'vue'
import type { FunctionalComponent, VNode } from 'vue'
import { useRouter } from 'vue-router'
import { withBase } from '../helpers/index.js'
import { resolveRoutePath } from '../router/index.js'

/**
* Forked from https://github.com/vuejs/router/blob/941b2131e80550009e5221d4db9f366b1fea3fd5/packages/router/src/RouterLink.ts#L293
*/
const guardEvent = (event: MouseEvent): boolean | void => {
// don't redirect with control keys
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return
// don't redirect when preventDefault called
if (event.defaultPrevented) return
// don't redirect on right click
if (event.button !== undefined && event.button !== 0) return
// don't redirect if `target="_blank"`
if (event.currentTarget) {
const target = (event.currentTarget as HTMLElement).getAttribute('target')
if (target?.match(/\b_blank\b/i)) return
}
event.preventDefault()
return true
}

export interface VPLinkProps {
to: string
}

export const VPLink: FunctionalComponent<
VPLinkProps,
Record<never, never>,
{
default: () => string | VNode | (string | VNode)[]
}
> = ({ to = '' }, { slots }) => {
const router = useRouter()
const path = withBase(resolveRoutePath(to))

return h(
'a',
{
class: 'vp-link',
href: path,
onClick: (event: MouseEvent = {} as MouseEvent) => {
guardEvent(event) ? router.push(to).catch() : Promise.resolve()
},
},
slots.default?.(),
)
}

VPLink.displayName = 'VPLink'
1 change: 1 addition & 0 deletions packages/client/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './ClientOnly.js'
export * from './Content.js'
export * from './VPLink.js'
2 changes: 1 addition & 1 deletion packages/client/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export * from './pageHead.js'
export * from './pageHeadTitle.js'
export * from './pageLang.js'
export * from './pageLayout.js'
export * from './pagesData.js'
export * from './routeLocale.js'
export * from './routes.js'
export * from './siteData.js'
export * from './siteLocaleData.js'
export * from './updateHead.js'
14 changes: 1 addition & 13 deletions packages/client/src/composables/pageData.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { PageData } from '@vuepress/shared'
import type { InjectionKey, Ref } from 'vue'
import { inject, readonly } from 'vue'
import { inject } from 'vue'

export type { PageData }

Expand All @@ -17,18 +17,6 @@ export const pageDataSymbol: InjectionKey<PageDataRef> = Symbol(
__VUEPRESS_DEV__ ? 'pageData' : '',
)

/**
* Empty page data to be used as the fallback value
*/
export const pageDataEmpty = readonly({
key: '',
path: '',
title: '',
lang: '',
frontmatter: {},
headers: [],
} as PageData) as PageData

/**
* Returns the ref of the data of current page
*/
Expand Down
Loading

0 comments on commit 7d37350

Please sign in to comment.