Skip to content

Commit 6a35209

Browse files
committed
fix: optimize ssr render
1 parent c3af902 commit 6a35209

File tree

10 files changed

+394
-204
lines changed

10 files changed

+394
-204
lines changed

packages/vuepress-theme-reco/src/client/components/DropdownLink/index.vue

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,21 @@ const dropdownAriaLabel = computed(
8787
)
8888
8989
const open = ref(false)
90+
91+
// 安全地使用路由,避免在服务器端渲染时出错
9092
const route = useRoute()
91-
watch(
92-
() => route.path,
93-
() => {
94-
open.value = false
95-
}
96-
)
93+
94+
// 只在客户端看视路由变化
95+
// 确保route存在且具有path属性
96+
97+
if (route && typeof route.path !== 'undefined') {
98+
watch(
99+
() => route.path,
100+
() => {
101+
open.value = false
102+
}
103+
)
104+
}
97105
98106
const inButton = ref(false)
99107
const handleButtonMouseEnter = () => {

packages/vuepress-theme-reco/src/client/components/GenericContainer/useSeries.ts

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,88 @@ import { useRouter } from 'vuepress/client'
22
import { onMounted, onUnmounted } from 'vue'
33
import { useScrollDirection } from '@composables/index.js'
44

5+
// 定义简化的Router类型
6+
// 这里仅定义我们需要的最小属性集合
7+
8+
interface RouteLocation {
9+
path: string;
10+
[key: string]: any;
11+
}
12+
13+
interface RouterType {
14+
afterEach: (callback: (to: RouteLocation, from: RouteLocation) => void) => (() => void);
15+
[key: string]: any;
16+
}
17+
518
export function useSeries() {
6-
let unregisterRouterHook
19+
let unregisterRouterHook: (() => void) | null = null;
20+
21+
// 提供一个默认的降级实现
22+
let router: RouterType = {
23+
afterEach: (callback) => {
24+
// 记录注册的回调,但不实际执行任何路由变化
25+
// 返回一个空函数作为unregister
26+
return () => {
27+
// 空函数作为unregister
28+
};
29+
}
30+
};
731

8-
const initSeriesStatus = (cb) => {
32+
// 确保SSR期间不调用useRouter
33+
const isClient = typeof window !== 'undefined';
34+
if (isClient) {
35+
try {
36+
// 只在客户端环境下调用useRouter
37+
const clientRouter = useRouter();
38+
if (clientRouter && typeof clientRouter.afterEach === 'function') {
39+
router = clientRouter;
40+
}
41+
} catch (e) {
42+
console.warn('Failed to use router, using fallback implementation', e);
43+
}
44+
} else {
45+
// 服务器端渲染环境
46+
}
47+
48+
const initSeriesStatus = (cb: () => void) => {
49+
// 在SSR期间此函数不应执行任何与路由相关的操作
50+
if (typeof window === 'undefined') {
51+
return;
52+
}
53+
954
onMounted(() => {
10-
const router = useRouter()
11-
const { direction } = useScrollDirection()
12-
unregisterRouterHook = router.afterEach((to, from) => {
13-
// close series after navigation
14-
if (to.path !== from.path) {
15-
cb()
16-
17-
direction.value = ''
18-
}
19-
})
20-
})
55+
try {
56+
const { direction } = useScrollDirection();
57+
58+
// 只在客户端环境注册路由钩子
59+
60+
// 使用router (在客户端环境已初始化)
61+
unregisterRouterHook = router.afterEach((to, from) => {
62+
// 增加健壮性检查
63+
if (to && from && typeof to.path === 'string' && typeof from.path === 'string' && to.path !== from.path) {
64+
cb();
65+
66+
if (direction && typeof direction.value !== 'undefined') {
67+
direction.value = '';
68+
}
69+
}
70+
});
71+
} catch (e) {
72+
console.warn('Error in initSeriesStatus', e);
73+
}
74+
});
2175

2276
onUnmounted(() => {
23-
unregisterRouterHook()
24-
})
25-
}
77+
if (typeof unregisterRouterHook === 'function') {
78+
try {
79+
unregisterRouterHook();
80+
unregisterRouterHook = null;
81+
} catch (e) {
82+
console.warn('Error when unregistering router hook', e);
83+
}
84+
}
85+
});
86+
};
2687

27-
return { initSeriesStatus }
88+
return { initSeriesStatus };
2889
}

packages/vuepress-theme-reco/src/client/components/Link.vue

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<RouterLink
2+
<SafeRouterLink
33
v-if="isRouterLink"
44
class="link"
55
:class="{ 'router-link-active': isActiveInSubpath }"
@@ -11,7 +11,7 @@
1111
<slot name="before" />
1212
<Xicons :icon="item.icon" :text="item.text" />
1313
<slot name="after" />
14-
</RouterLink>
14+
</SafeRouterLink>
1515

1616
<a
1717
v-else
@@ -34,6 +34,7 @@
3434
import { computed, toRefs } from 'vue'
3535
import { isLinkHttp, isLinkWithProtocol } from 'vuepress/shared'
3636
import { withBase, useSiteData, useRouteLocale, useRoute } from 'vuepress/client'
37+
import SafeRouterLink from '@components/SafeRouterLink.vue'
3738
3839
import { useThemeLocaleData } from '@composables/index.js'
3940
@@ -103,17 +104,34 @@ const shouldBeActiveInSubpath = computed(() => {
103104
104105
// if this link is active in subpath
105106
const isActiveInSubpath = computed(() => {
106-
if (!isRouterLink.value || !shouldBeActiveInSubpath.value) {
107+
// 首先检查route是否存在以及route.path是否定义,这样在服务器渲染过程中不会出错
108+
if (!route || typeof route.path === 'undefined' || !isRouterLink.value || !shouldBeActiveInSubpath.value) {
107109
return false
108110
}
109111
112+
// 确保themeLocal.value存在
113+
if (!themeLocal.value) {
114+
return false
115+
}
116+
117+
// 确保item.value.link存在且为有效字符串
118+
if (!item.value || typeof item.value.link !== 'string' || !item.value.link) {
119+
return false
120+
}
121+
122+
// 获取主页路径
123+
const homePath = themeLocal.value.home || '/'
124+
const baseHomePath = withBase(homePath)
125+
const itemLink = item.value.link
126+
110127
if (
111-
route.path === withBase(themeLocal.value.home || '/') &&
112-
item.value.link === withBase(themeLocal.value.home || '/')
128+
route.path === baseHomePath &&
129+
itemLink === baseHomePath
113130
) {
114131
return true
115132
}
116133
117-
return route.path.startsWith(item.value.link as string) && !item.value.link?.endsWith('/')
134+
// 安全地检查startsWith和endsWith
135+
return route.path.startsWith(itemLink) && !itemLink.endsWith('/')
118136
})
119137
</script>

packages/vuepress-theme-reco/src/client/components/Navbar/SiteBrand.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts" setup>
22
import { toRefs } from 'vue'
3+
import SafeRouterLink from '@components/SafeRouterLink.vue'
34
45
const props = defineProps({
56
icon: {
@@ -28,14 +29,14 @@ const { title, icon, link } = toRefs(props)
2829
:alt="title"
2930
/>
3031

31-
<RouterLink
32+
<SafeRouterLink
3233
v-if="title"
3334
:to="link"
3435
class="site-name"
3536
:class="{ 'can-hide': icon }"
3637
>
3738
{{ title }}
38-
</RouterLink>
39+
</SafeRouterLink>
3940
</div>
4041
</template>
4142

packages/vuepress-theme-reco/src/client/components/PostList/PostItem.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
<template>
22
<MagicCard class="post-item-container">
33
<div class="title">
4-
<RouterLink :to="data.path">
4+
<SafeRouterLink :to="data.path">
55
<Xicons v-if="data.frontmatter?.sticky" :icon="IconStar" />
66
<span>{{ data.title }}</span>
7-
</RouterLink>
7+
</SafeRouterLink>
88
</div>
99
<PageInfo :page-data="data" :hide-views="solution==='valine'"> </PageInfo>
1010
</MagicCard>
1111
</template>
1212

1313
<script lang="ts" setup>
1414
import { toRefs } from 'vue'
15+
import SafeRouterLink from '@components/SafeRouterLink.vue'
1516
import { useComment } from '@vuepress-reco/vuepress-plugin-comments/composables'
1617
1718
import PageInfo from '@components/PageInfo.vue'
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<template>
2+
<component
3+
:is="linkComponent"
4+
v-bind="componentProps"
5+
>
6+
<slot></slot>
7+
</component>
8+
</template>
9+
10+
<script setup>
11+
// 注意:我们有意使用普通JavaScript而非Typescript
12+
// 这可以避免TS在编译期间检查导入的组件
13+
14+
import { h, shallowRef, computed } from 'vue'
15+
16+
// 安全地检测我们是否在服务器上
17+
const isServer = typeof window === 'undefined'
18+
19+
// 使用 shallowRef 避免 Vue 深入解析导入的组件
20+
const linkComponent = shallowRef('a')
21+
22+
// 这些属性将被传递给最终渲染的组件
23+
const props = defineProps({
24+
to: {
25+
type: [String, Object],
26+
required: true
27+
},
28+
custom: {
29+
type: Boolean,
30+
default: false
31+
},
32+
replace: {
33+
type: Boolean,
34+
default: false
35+
},
36+
ariaCurrent: {
37+
type: String,
38+
default: 'page'
39+
}
40+
})
41+
42+
// 计算出给组件的属性
43+
const componentProps = computed(() => {
44+
if (isServer) {
45+
// 在服务器端,我们使用普通的a标签
46+
return {
47+
class: 'router-link-fallback',
48+
href: typeof props.to === 'string' ? props.to : '',
49+
...props.$attrs
50+
}
51+
} else {
52+
// 在客户端,我们使用RouterLink的属性
53+
return {
54+
to: props.to,
55+
custom: props.custom,
56+
replace: props.replace,
57+
'aria-current': props.ariaCurrent,
58+
...props.$attrs
59+
}
60+
}
61+
})
62+
63+
// 只在客户端环境下动态导入和使用RouterLink
64+
if (!isServer) {
65+
// 时间函数延迟运行,确保到了客户端才运行
66+
setTimeout(() => {
67+
try {
68+
// 动态导入RouterLink
69+
import('vue-router').then(module => {
70+
linkComponent.value = module.RouterLink
71+
}).catch(e => {
72+
console.warn('SafeRouterLink: Failed to load RouterLink, using fallback link', e)
73+
})
74+
} catch (e) {
75+
console.warn('SafeRouterLink: Error importing RouterLink', e)
76+
}
77+
}, 0)
78+
}
79+
</script>

packages/vuepress-theme-reco/src/client/composables/useSeriesItems.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ const resolveMultiSeriesItems = (
9999
seriesConfig: SeriesConfigObject,
100100
route: RouteLocationNormalizedLoaded
101101
): ResolvedSeriesItem[] => {
102+
// 确保route.path存在,避免在某些组件(如Timeline)中使用时出错
103+
if (!route || typeof route.path === 'undefined') {
104+
return []
105+
}
106+
102107
const seriesPath = resolveLocalePath(
103108
seriesConfig,
104109
decodeURIComponent(route.path)

packages/vuepress-theme-reco/src/client/layouts/Categories.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
},
1313
]"
1414
>
15-
<RouterLink
15+
<SafeRouterLink
1616
class="category-link"
1717
:to="`/${categoryPosts.currentCategoryKey}/${categoryValue}/1.html`"
1818
>
1919
<span class="text">{{ label }}</span>
20-
</RouterLink>
20+
</SafeRouterLink>
2121
</li>
2222
</ul>
2323

@@ -36,6 +36,7 @@
3636
<script lang="ts" setup>
3737
import { computed, onMounted, watch } from 'vue'
3838
import { useRoute, useRouter } from 'vuepress/client'
39+
import SafeRouterLink from '@components/SafeRouterLink.vue'
3940
import { useExtendPageData } from '@vuepress-reco/vuepress-plugin-page/composables'
4041
4142
import Pagation from '@components/Pagation.vue'

packages/vuepress-theme-reco/src/client/layouts/Timeline.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
<li v-for="(subItem, subIndex) in item.data" :key="subIndex" class="item">
1111
<span class="date">{{subItem.date}}</span>
1212

13-
<RouterLink class="title" :to="subItem.path">{{ subItem.title }}</RouterLink>
13+
<!-- 使用SafeRouterLink组件,它内部已包含ClientOnly处理 -->
14+
<SafeRouterLink v-if="subItem && subItem.path && typeof subItem.path === 'string'"
15+
class="title"
16+
:to="subItem.path">{{ subItem.title }}</SafeRouterLink>
17+
<span v-else class="title">{{ subItem.title }}</span>
1418
</li>
1519
</ul>
1620
</li>
@@ -20,6 +24,7 @@
2024

2125
<script setup lang="ts">
2226
import GenericContainer from '@components/GenericContainer/index.vue'
27+
import SafeRouterLink from '@components/SafeRouterLink.vue'
2328
import { useExtendPageData } from '@vuepress-reco/vuepress-plugin-page/composables'
2429
2530
import { formatISODate } from '@utils/other.js'

0 commit comments

Comments
 (0)