前提
项目前期的时候虽然在nuxt中开启了SSR,但是因为api请求还是用的客户端请求。所以并没有完全达到服务端渲染的目的,最主要的表现就是在源代码中没有体现动态数据的展示。
问题点
其实整个项目最主要解决的页面只有三个。首页、商品列表、商品详情页。但是这里面有个很坑的问题?
首页还好,所有的数据都是从接口获取的,直接将接口修改请求方式即可。但是列表和详情页的数据有些需要依赖本地数据。就是需要从接口获取当前的数据,还需要从之前预先请求的本地数据中获取特定的字段来合并最终的展示。
实践操作
在首页中直接采用useAsyncData的方式来进行数据请求。
const { data: goodsTabData } = await useAsyncData("goodsTabData", () =>
IndexApi.fetchHomeGoodsTab(),
);
对于一些客户端数据请求,还在onMounted中进行处理。
onMounted(async () => {
await reportActive();
});
而动态数据请求则通过watchEffect来处理api返回的数据。
watchEffect(() => {
if (goodsTabData.value?.code === 0) {
goodsTabs.value = goodsTabData.value.data.gameInfos;
const gid = goodsTabData.value.data.gameInfos[0].gid;
activeGoodsTab.value = gid;
if (process.client) {
getGoodsListData(gid);
}
}
});
需要注意的是,有些方法需要区分客户端还是服务端请求。通过process.client来进行判断。
在商品列表页面的时候,因为需要有客户端的操作,所以使用使用 useLazyAsyncData
来实现商品列表在服务端数据预取。同时区分客户端的操作,保持服务端和客户端水合一致。
const { data: initialGoodsData } = await useLazyAsyncData(
goods-list-${route.params.gid},
async () => {
const gid = route.params.gid as string;
if (!gid) return null;
try {
const res = await GoodsApi.xxxx({
pageNum: 1,
cat: "H",
});
if (res?.code === 0 && res?.data) {
return {
total: res.data.total,
rows: res.data.rows || [],
};
}
} catch (error) {
console.error("服务端获取商品列表失败:", error);
}
return null;
},
);
详情页也是,数据获取的层级太深,最终渲染的结果不仅依赖具体的详情接口返回的数据。返回数据的具体label名称。展示形式,展示交互全部依赖于前置请求的本地数据。
打个比方接口返回一个字段K:2,你需要根据当前商品的类型去特定的数据中找到K对应的名称是什么。然后展现最终的效果。
所以在详情页也是深度的依赖水合效果。html服务端渲染返回,但是也是数据转换操作在客户端处理。
const {
data: goodsDetailsResponse,
error: goodsDetailsError,
pending: detailsLoading,
refresh,
} = await useAsyncData(
goods-detail-${goodsProductId.value},
async () => {
return response;
},
{
server: true,
default: () => ({ code: -1, data: null, msg: "" }),
},
);
另外进入页面的初始交互也从onMounted改成通过watchEffect来处理。
- 还有一个注意点,会发现在请求的时候有
goods-detail-${goodsProductId.value}
,有这么一个key。那么他是干嘛的呢?
他是useAsyncData
的唯一key。可以用来标识唯一的数据请求,当多个组件请求相同数据时可以通过这个key来区分是否重复请求。还可以自动处理依赖关系,比如goodsProductId发生变化的时候,会自动触发重新请求。
最终的效果
最终的效果就是在查看页面源代码的时候,相应的接口返回数据会在源代码中呈现。这样搜索引擎的爬虫才会抓取到页面的内容有更好的seo效果。
总结
为什么要使用nuxt?因为vue是一个单页面应用,并且所有的内容都是客户端动态渲染的。这就导致无法在浏览器中查看源码的时候看到返回的数据内容。页面上只有固定的骨架内容。
在nuxt3中其实有多个数据获取方式,像上面所使用的api基本都是fetch封装的请求方法。所以在使用useAsyncData的时候需要手动开启ssr特性。
不同api的简单对比
useFetch
vs $fetch
// useFetch - 响应式数据获取,支持 SSR
const { data, pending, error } = await useFetch('/api/goods')
// $fetch - 简单的 HTTP 请求,类似 fetch API
const data = await $fetch('/api/goods')
useAsyncData
vs useLazyAsyncData
// useAsyncData - 阻塞式数据获取-等待数据加载完成后才渲染页面
const { data } = await useAsyncData('goods', () => $fetch('/api/goods'))
// useLazyAsyncData - 非阻塞式数据获取-立即渲染页面,数据异步加载
const { data, pending } = await useLazyAsyncData('goods', () => $fetch('/api/goods'))
他们的核心区别:
场景 | 推荐方法 | 原因 |
---|---|---|
关键首屏数据 | useAsyncData |
确保数据完整性 |
次要数据 | useLazyAsyncData |
提升页面响应速度 |
用户交互数据 | $fetch |
简单直接 |
响应式数据 | useFetch |
自动状态管理 |
最后
另外在上文中的列表页面和详情页多次提高水合这个概念,那么什么是水合呢?他又解决了什么?
什么是水合?
水合就是将服务端渲染的静态HTML转换为可交互操作的客户端应用的过程。在这个过程中vue"接管"服务端生成的静态 HTML,为其添加事件监听器、数据绑定和其他客户端功能。
水合的过程
- 服务端:生成静态 HTML 并发送给客户端
- 客户端:下载并执行 JavaScript
- 激活:Vue 应用接管 DOM,绑定事件监听器
- 同步:确保客户端状态与服务端渲染的内容一致
举一个简单的例子
{{ title }}
服务端会生成<h1>水合示例</h1><button>点击次数: 0</button>
这个HTML并返回给客户端,而在客户端中vue会找到这个按钮并添加点击事件监听器。
另外在水合过程中如何找到对应的HTML呢。其实vue 项目在渲染的时候dom上都会data-v开头的标记。一个是通过这个来匹配。还有就是我们的虚拟dom树的比对。类似diff算法的更新的机制。
为了避免和解决水合不匹配的问题,我们可以采用一开始的方法。总结如下:
1、使用useAsyncData
和useFetch
确保数据一致性
2、使用process.client
条件判断隔离客户端特定代码
3、使用ClientOnly
组件包装仅客户端渲染的内容
4、确保服务端和客户端的初始状态一致