预先说明,本思路与常规方案不同,也不太推荐。有能力的建议直接一步到位用ssr改造项目,后面我也会出一个vite ssr博客出来。
"数据预取"功能类似于SSR中的注水,SSR的注水,是在服务端获取数据并渲染好页面结构,客户端只需要渲染好页面就行了,省去了创建DOM的过程。在服务器端获取的数据需要通过’注水‘ 插入到页面上,到客户端读取服务器请求的数据,这个过程叫’脱水‘,通过‘脱水’,避免了再次向服务器请求的操作;
而我们今天做的事 只是通过打包的时候注水,到客户端读取数据直接去渲染DOM就行了,不需要再请求服务器数据(保险点的方案是延迟请求一遍)。通过vite在打包时提供的hooks钩子函数,我们在打包完成之后向服务器去请求某些需要的接口数据,再把数据插入到html中;在项目运行时我们去html中拿打包好的数据,然后直接去渲染到页面上即可;为避免数据过期,我们应当处理的是:
- 及时重新打包部署;
- 或者在页面加载完成之后再请求数据,对比是否有更新;
阅读vite文档中的 插件API这一章vite独有的钩子
我们知道vite在开启服务时不同时间段会触发对应的钩子函数,具体可以看文档,今天主要用到的是这个hooks
transformIndexHtml
转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包。
这个钩子可以是异步的,并且可以返回以下其中之一:
经过转换的 HTML 字符串
注入到现有 HTML 中的标签描述符对象数组({ tag, attrs, children })。每个标签也可以指定它应该被注入到哪里(默认是在 之前)
一个包含 { html, tags } 的对象
默认情况下 order 是 undefined,这个钩子会在 HTML 被转换后应用。为了注入一个应该通过 Vite 插件管道的脚本, order: ‘pre’ 指将在处理 HTML 之前应用。 order: ‘post’ 是在所有未定义的 order 的钩子函数被应用后才应用。
我们可以在这个钩子函数里把请求完的数据插入到html中,这样就能实现上述说的流程方法。
占位符
在html中插入一个占位符,用于等待数据请求回来之后替换,在head里新增一个script标签:
<title>Vite + Vue + TS</title>
<!--preload-links-->
<script>
window.__INITIAL_STATE__ = <!--pinia-state-->;
</script>
</head>
window.__INITIAL_STATE__
对象就是请求完之后的数据放置的地方,取数据直接在里面找即可。
构建vite插件
完整的钩子签名
type IndexHtmlTransformHook = (
html: string,
ctx: {
path: string
filename: string
server?: ViteDevServer
bundle?: import('rollup').OutputBundle
chunk?: import('rollup').OutputChunk
},
) =>
| IndexHtmlTransformResult
| void
| Promise<IndexHtmlTransformResult | void>
type IndexHtmlTransformResult =
| string
| HtmlTagDescriptor[]
| {
html: string
tags: HtmlTagDescriptor[]
}
interface HtmlTagDescriptor {
tag: string
attrs?: Record<string, string>
children?: string | HtmlTagDescriptor[]
/**
* 默认: 'head-prepend'
*/
injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
}
上面是官方文档里插件的类型,下面开始写插件:
/**
* 预取数据插件 非ssr
* 功能依赖Node内置fetch 否则无法请求
*/
interface PrefetchDataOptions {
url: string;
data: unknown;
key: string;
}
interface VitePluginPrefetchOption {
/**
* 是否禁用
*/
disabled?: boolean;
/**
* 预取的接口数据
*/
data: PrefetchDataOptions[];
/**
* 请求的base url
*/
BASE_URL?: string;
}
export const prefetchHttpData = async (params: VitePluginPrefetchOption) => {
const { data, BASE_URL, disabled } = params;
let temp = "{}";
if (!disabled) {
try {
temp = await prefetch(
data.map((item) => {
return {
...item,
url: `${BASE_URL}${item.url}`,
};
})
) || '{}';
} catch {
console.log(`请求出错了!`)
}
}
return {
name: "vite-plugin-prefetch",
transformIndexHtml(html: string) {
return html.replace("<!--prefetch-data-->", temp);
},
};
};
prefetchHttpData
返回一个vite hooks对象,而transformIndexHtml则返回字符串形式的html,而我们只需要替换html里的占位符即可。
prefetch
则是一个请求后的数据字符串形式。
async function prefetch(data: PrefetchDataOptions[]) {
try {
const temp = (await Promise.all(data.map(request))).reduce(
(pre, next) => Object.assign(pre, next),
{}
);
return JSON.stringify(temp);
} catch {
return "";
}
}
async function request({ url, data: body, key }: PrefetchDataOptions) {
const { Errcode, data } = (await fetch(url, {
method: "POST",
body: JSON.stringify(body),
headers: new Headers({
"Content-Type": "application/json",
}),
}).then((res) => res.json())) as {
Errcode: number;
data: {
Data: unknown;
};
};
if (Errcode) return Promise.reject({});
else return Promise.resolve({ [key]: data.Data });
}
使用插件
在vite.config.ts里配置插件plugins,插件需要传入请求地址的参数 格式为定义好的数组形式,以及请求的basrurl (可以自行改造,把baseurl放到请求地址里)