如何使用vite,做vue3.0的服务端渲染(ssr)

新春伊始,想必在座的各位都正在嗷嗷待哺的等待需求中(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_BUILDasync 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-ignorerequire('./dist/client/ssr-manifest.json'): {}const app = express()/*** @type {import('vite').ViteDevServer}*/let viteif (!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.originalUrllet 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-ignorerender = require('./dist/server/entry-server.js').render}// 调用服务端渲染方法,将vue组件渲染成dom结构,顺带分析出需要预加载的js,css等文件。const [appHtml, preloadLinks, store] = await render(url, manifest)// 新加 + 将服务端预取数据的store,插入html模板文件const state =  ("");// 把html中的展位符替换成相对应的资源文件const html = template.replace(``, preloadLinks).replace(``, appHtml).replace(``, 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();// 同步urlrouter.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;});// 动态注册storeregisterModules(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写法还有点简陋,有时间再补全封装下,参考下这个仓库


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部