如何使用vite,做vue3.0的服务端渲染(ssr)
新春伊始,想必在座的各位都正在嗷嗷待哺的等待需求中(ps:划水摸鱼),不好意思,理直气壮的说我也是。几天鱼摸下来,心里也不是滋味,看着身边的同学一个个每天都在学这学那,搞得我也不是很好意思。于是趁着现在各种完全体的方案和框架还没出来之前,那我们把vue3.0的服务端搭一搭吧,自己写写还是很有意思的。好了,废话就先到此,开局调研了基于webpack和vue-cli去搭,途中碰到了一些问题就放弃了,由
新春伊始,想必在座的各位都正在嗷嗷待哺的等待需求中(ps:划水摸鱼),不好意思,理直气壮的说我也是。几天鱼摸下来,心里也不是滋味,看着身边的同学一个个每天都在学这学那,搞得我也不是很好意思。于是趁着现在各种完全体的方案和框架还没出来之前,那我们把vue3.0的服务端搭一搭吧,自己写写还是很有意思的。
好了,废话就先到此,开局调研了基于webpack和vue-cli去搭,途中碰到了一些问题就放弃了,(后记:不好意思,我胡汉三又回来了,怎么说放弃就放弃,反手就是甩一套教程),由于尤大最近很痴迷于vite,vue-cli相关的生态也有些滞后。于是反手就一手vite搞起,看看到底有什么魔力让我们尤大大年三十晚上还在撸代码。
最开始的学习毫无疑问就是看文档,刚好碰上vite2.0发布,简直是可喜可贺。轻车熟路的就找到了ssr的demo。
一看到这句话,就说明接下来的旅途非常的刺激,可以动手发挥想象力的地方非常的多。
话不多说,先download下来,再根据自己想要的去改造就可以了。
打开demo项目,大体的逻辑已经帮我们写的差不多了,剩余服务端预取数据,store状态接管等这些没有去弄,可以说稍加改造就可以用于生产了,非常的nice。
下面讲解下代码吧。
首先看到server.js文件,这个文件其实就是帮我们启动一个ssr的服务器。
// @ts-check
const fs = require('fs')
const path = require('path')
const express = require('express')
const serialize = require('serialize-javascript');
const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
async function createServer(
root = process.cwd(),
isProd = process.env.NODE_ENV === 'production'
) {
const resolve = (p) => path.resolve(__dirname, p)
const indexProd = isProd
? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
: ''
const manifest = isProd
? // @ts-ignore
require('./dist/client/ssr-manifest.json')
: {}
const app = express()
/**
* @type {import('vite').ViteDevServer}
*/
let vite
if (!isProd) {
vite = await require('vite').createServer({
root,
logLevel: isTest ? 'error' : 'info',
server: {
middlewareMode: true
}
})
app.use(vite.middlewares)
} else {
app.use(require('compression')())
// 把打包好的css,js等文件,放到静态文件服务器
app.use(
require('serve-static')(resolve('dist/client'), {
index: false
})
)
}
app.use('*', async (req, res) => {
try {
const url = req.originalUrl
let template, render
// 读取index.html模板文件
if (!isProd) {
console.log('当前请求路径', url);
template = fs.readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
render = (await vite.ssrLoadModule('/src/entry-server.js')).render
} else {
template = indexProd
// @ts-ignore
render = require('./dist/server/entry-server.js').render
}
// 调用服务端渲染方法,将vue组件渲染成dom结构,顺带分析出需要预加载的js,css等文件。
const [appHtml, preloadLinks, store] = await render(url, manifest)
// 新加 + 将服务端预取数据的store,插入html模板文件
const state = ("<script>window.__INIT_STATE__" + "=" + serialize(store, {isJSON: true}) + "</script>");
// 把html中的展位符替换成相对应的资源文件
const html = template
.replace(`<!--preload-links-->`, preloadLinks)
.replace(`<!--app-html-->`, appHtml)
.replace(`<!--app-store-->`, state)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})
return { app, vite }
}
// 创建node服务器用作ssr
if (!isTest) {
createServer().then(({ app }) =>
app.listen(3000, () => {
console.log('http://localhost:3000')
})
)
}
// for test use
exports.createServer = createServer
index.html
就是server.js里面的模板文件,可以看到里面有对应要替换的展位符
src/main.ts
因为每个请求到达服务端,都需要一份全新的不受上个请求污染的代码,所以这个文件其实就是当前运行环境的工厂函数,每次都返回全新的vue实例,router实例,store实例等。
src/entry-server.js
服务端渲染入口函数
import { createApp } from "./main";
import { renderToString } from "@vue/server-renderer";
import { getAsyncData } from '@src/utils/publics';
export async function render(url, manifest) {
const { app, router, store } = createApp();
// 同步url
router.push(url);
store.$setSsrPath(url);
await router.isReady();
// 新加 + 当路由准备完毕,调用自定义钩子,在服务端获取数据
await getAsyncData(router, store, true);
// 生成html字符串
const ctx = {};
const html = await renderToString(app, ctx);
// 根据打包时生成的服务端预取清单manifest,生成资源预取数组
const preloadLinks = ctx.modules
? renderPreloadLinks(ctx.modules, manifest)
: [];
return [html, preloadLinks, store];
}
省略......
src/entry-client.js
客户端渲染入口函数
import { createApp } from './main'
const { app, router, store } = createApp()
// 这里需要先进行客户端状态同步 - 服务端携带过来的store
// 假设同学们用到的是vuex,我这边用的是自己写的状态管理包,就不写了
// 获取服务端渲染时,注入的__INITIAL_STATE__信息,并同步到客户端的vuex store中
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.isReady().then(() => {
// 挂在当前vue实例于id为app的dom上
app.use(VueRescroll)
.use(VueImageLazyLoad)
.mount('#app');
})
// 开启路由后置钩子,进行页面数据请求
router.afterEach(() => {
getAsyncData(router, store, false);
})
getAsyncData
像vue2.0的做法那样,新加asyncData钩子作为数据预取的钩子
// 执行注册store钩子
export const registerModules = (
components: Component[],
router: Router,
store: BaseStore
) => {
return components
.filter((i: any) => typeof i.registerModule === "function")
.forEach((component: any) => {
component.registerModule({ router: router.currentRoute, store });
});
};
// 调用当前匹配到的组件里asyncData钩子,预取数据
export const prefetchData = (
components: Component[],
router: Router,
store: BaseStore
) => {
const asyncDatas: any[] = components.filter(
(i: any) => typeof i.asyncData === "function"
);
return Promise.all(
asyncDatas.map((i) => {
return i.asyncData({ router: router.currentRoute.value, store });
})
);
};
// ssr自定义钩子
export const getAsyncData = (
router: Router,
store: BaseStore,
isServer: boolean
): Promise<void> => {
return new Promise(async (resolve) => {
const { matched, fullPath } = router.currentRoute.value;
// 当前路由匹配到的组件
const components: Component[] = matched.map((i) => {
return i.components.default;
});
// 动态注册store
registerModules(components, router, store);
if (isServer || store.ssrPath !== fullPath) {
// 预取数据
await prefetchData(components, router, store);
!isServer && store.$setSsrPath("");
}
resolve();
});
};
.vue里面预取数据
跟data,computed同级
async asyncData({ store, router }: any) {
if (!store.blog) return;
const { blogDetail } = store.blog;
blogDetail.$assignParams({
id: router.query.id
})
await blogDetail.loadData();
},
对于ssr的改造做了上述这些,还有些项目优化,比如模块化,ts,store的按需注册,以及一些自定义插件等,就不一一道来了,喜欢的同学可以download源码或者fork过去玩玩。
有需要交流的同学,也欢迎评论区交流交流。
项目仓库:https://github.com/Vitaminaq/cfsw-vue-cli3.0/tree/vue3.0-ssr
项目中用到的插件仓库:https://github.com/Vitaminaq/plugins-vue(喜欢的同学可以自取,欢迎同学们加入开发)
有同学想了解vue2.0最开始服务端渲染做法的,可以参考我之前的文章
有想了解vue3.0 + vue-cli服务端渲染的,可参考我最新的文章
2023-05-06补充
感谢上面的这位同学,让我又活过来了,最新的hooks写法还有点简陋,有时间再补全封装下,参考下这个仓库
更多推荐
所有评论(0)