Vue3+Vite3 SSR基本搭建
Vue3+Vite3 SSR基本搭建
- 首先说明如果是生产使用强烈推荐Nuxt,但是如果想深入服务端渲染的运行原理,可以看本篇,会根据渲染流程搭建一个demo版ssr,源码在最后会贴上
- 主要技术栈:Vite3 + Vue3 + pinia + VueRouter4 + express
- 开始搭建之前,先说一下SSR渲染流程
SSR渲染流程
- 首先浏览器向服务器请求,然后服务器根据请求的路由,会匹配相关的路由组件,然后执行组件的自定义服务端生命周期(例:Nuxt的asyncData)或者自定义获取数据的hook,并且把执行后的数据收集起来,统一在window的属性中存储
- 然后vue的组件会被renderToString渲染成静态HTML字符串,替换掉index.html的提前指定的占位代码。然后index.html改变后的静态字符串发给客户端
- 客户端拿到后,首先对数据进行初始化,然后进行激活,因为当前html只是静态数据,激活主要做两件事
- 把页面中的DOM元素与虚拟DOM之间建立联系
- 为页面中的DOM元素添加事件绑定
1. 创建项目
- 首先用vite命令创建项目
pnpm create vite vue-ssr --template vue-ts
- 安装相关依赖:
pnpm add express pinia vue-router@4
- 安装相关依赖:
- 创建三个文件
touch server.js src/entry-client.ts src/entry-server.js
- server.js:服务端启动文件
- entry-client.ts:客户端入口,应用挂载元素
- entry-server.js:服务端入口,处理服务端逻辑和静态资源
- 修改
package.json
运行脚本"scripts": {"dev": "node server", // 运行开发环境 }
- 然后需要把应用创建都改为函数的方式进行调用创建,因为在SSR环境下,和纯客户端不一样,服务器只会初始化一次,所以为了防止状态污染,每次请求必须是全新的实例
// src/main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createPinia } from 'pinia'export function createApp() {const app = createSSRApp(App)const router = createRouter()const pinia = createPinia()app.use(router)app.use(pinia)return { app, router, pinia }
}
- roter同理
// src/router/index
import { createRouter as _createRrouter, createMemoryHistory, createWebHistory, RouteRecordRaw } from 'vue-router'const routes: RouteRecordRaw[] = [...
]export function createRouter() {return _createRrouter({history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),routes,})
}
- 然后修改index.html,增加注释占位和客户端入口文件,在之后的服务端渲染时注入
<html lang="en"><head><meta charset="UTF-8" /><link rel="icon" type="image/svg+xml" href="/vite.svg" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Vite + Vue + TS</title><!-- 静态资源占位 .js .css ... --><!--preload-links--></head><body><!-- 应用代码占位 --><div id="app"><!--ssr-outlet--></div><script type="module" src="/src/main.ts"></script><!-- 引用客户端入口文件 --><script type="module" src="/src/entry-client.ts" ></script><script>// 服务端获取的数据统一挂载到window上window.__INITIAL_STATE__ = '<!--pinia-state-->'</script></body>
</html>
2. 服务端启动文件
- 创建项目后,就开始编写服务端启动文件,也就是项目根路径下的server.js文件
- 这个文件的功能是启动一个node服务,然后根据请求,读取html文件,处理资源后把注释进行替换,最后把html发送给客户端
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import express from 'express'import { createRequire } from 'module';
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url);
const resolve = (p) => path.resolve(__dirname, p);const createServer = async () => {// 创建node服务const app = express()/*** @官方解释* 以中间件模式创建vite应用,这将禁用vite自身的HTML服务逻辑* 并让上级服务器接管*/const vite = await require('vite').createServer({server: {middlewareMode: true,},appType: 'custom'});app.use(vite.middlewares);app.use('*', async (req, res, next) => {const url = req.originalUrltry {// 读取index.htmllet template = fs.readFileSync(resolve('index.html'),'utf-8')// 应用vite html转换,会注入vite HMRtemplate = await vite.transformIndexHtml(url, template)// 加载服务端入口const render = (await vite.ssrLoadModule('/src/entry-server.js')).renderconst [ appHtml, piniaState ] = await render(url)// 替换处理过后的模版const html = template.replace(`<!--ssr-outlet-->`, appHtml).replace(`<!--pinia-state-->`, piniaState)res.status(200).set({ 'Content-Type': 'text/html' }).end(html)} catch (error) {vite?.ssrFixStacktrace(error)next(e)}})// 监听5100端口app.listen(5100)
}createServer();
3. 服务端入口文件
- 服务端入口文件主要是调用SSR的renderToString和收集需要发送的资源和数据
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'export async function render(url, manifest) {const { app, router, pinia } = createApp()router.push(url)await router.isReady()const ctx = {}const html = await renderToString(app, ctx)return [html, JSON.stringify(pinia.state.value)]
}
4. 客户端入口文件
- 客户端入口文件主要用于挂载节点和初始化数据
import { createApp } from './main'const { app, router, pinia } = createApp()router.isReady().then(() => {if (window.__INITIAL_STATE__) {pinia.state.value = JSON.parse(window.__INITIAL_STATE__);}app.mount('#app')
})
5. 组件和页面
- 组件和页面获取数据主要有两种方式,一种是增加一个
asyncData
选项,然后在enter-server.js的逻辑中增加遍历当前组件的逻辑,统一触发asyncData
,但是现在都是用script setup
的方式写业务代码,所以有点麻烦,
<script>export defualt {asyncData() {// 服务端获取数据逻辑}}
</script><script setup lang='ts'>...
</script>
- 另一种就是hook的方式,通过
import.meta.env.SSR
的方式进行判断 - 对于数据具体存储方式,大概有三种,一种是存在vuex或者pinia这种全局状态库中,一种是存在context上下文中,还有一种是自定义数据
6. 生产环境
6.1 pacnakge.json
- 增加构建脚本
"scripts": {"dev": "node server",
+ "build": "npm run build:client && npm run build:server",
+ "build:client": "vite build --ssrManifest --outDir dist/client",
+ "build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
+ "serve": "cross-env NODE_ENV=production node server"
},
6.2 服务端运行文件
- 针对生产环境,需要启动静态资源服务,引用路径需要改为dist目录下
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import express from 'express'import { createRequire } from 'module';
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url);
const resolve = (p) => path.resolve(__dirname, p);const createServer = async (isProd = process.env.NODE_ENV === 'production') => {const app = express()- const vite = await require('vite').createServer({
- server: {
- middlewareMode: true,
- },
- appType: 'custom'
- });
- app.use(vite.middlewares);+ let vite;
+ if (isProd) {
+ app.use(require('compression')());
+ app.use(
+ require('serve-static')(resolve('./dist/client'), {
+ index: false
+ })
+ );
+ } else {
+ vite = await require('vite').createServer({
+ server: {
+ middlewareMode: true,
+ },
+ appType: 'custom'
+ });
+ app.use(vite.middlewares);
+ }// 通过bulid --ssrManifest命令生成的静态资源映射需要在生产环境下引用
+ const manifest = isProd ? fs.readFileSync(resolve('./dist/client/ssr-manifest.json'), 'utf-8') :{}app.use('*', async (req, res, next) => {const url = req.originalUrltry {
- let template = fs.readFileSync(
- resolve('index.html'),
- 'utf-8'
- )
- template = await vite.transformIndexHtml(url, template)
- const render = (await vite.ssrLoadModule('/src/entry-server.js')).render
- const [ appHtml, piniaState ] = await render(url)+ let template, render
+ if (isProd) {
+ template = fs.readFileSync(resolve('./dist/client/index.html'), 'utf-8')
+ render = (await import('./dist/server/entry-server.js')).render
+ } else {
+ template = fs.readFileSync(
+ resolve('index.html'),
+ 'utf-8'
+ )
+ template = await vite.transformIndexHtml(url, template)
+ render = (await vite.ssrLoadModule('/src/entry-server.js')).render
+ }
+ const [ appHtml, preloadLinks, piniaState ] = await render(url, manifest)const html = template
+ .replace(`<!--preload-links-->`, preloadLinks).replace(`<!--ssr-outlet-->`, appHtml).replace(`<!--pinia-state-->`, piniaState)res.status(200).set({ 'Content-Type': 'text/html' }).end(html)} catch (error) {vite?.ssrFixStacktrace(error)next()}})app.listen(5100)
}createServer();
6.3 服务端入口文件
- 服务端入口文件主要是增加了构建时生成的静态资源映射处理的逻辑
import { basename } from 'path'
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'export async function render(url, manifest) {const { app, router, pinia } = createApp()router.push(url)await router.isReady()const ctx = {}const html = await renderToString(app, ctx)
+ const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
+ return [html, preloadLinks, JSON.stringify(pinia.state.value)]
}+ function renderPreloadLinks(modules, manifest) {
+ let links = ''
+ const seen = new Set()
+ modules.forEach((id) => {
+ const files = manifest[id]
+ if (files) {
+ files.forEach((file) => {
+ if (!seen.has(file)) {
+ seen.add(file)
+ const filename = basename(file)
+ if (manifest[filename]) {
+ for (const depFile of manifest[filename]) {
+ links += renderPreloadLink(depFile)
+ seen.add(depFile)
+ }
+ }
+ links += renderPreloadLink(file)
+ }
+ })
+ }
+ })
+ return links
+ }
+
+ function renderPreloadLink(file) {
+ if (file.endsWith('.js')) {
+ return `<link rel="modulepreload" crossorigin href="${file}">`
+ } else if (file.endsWith('.css')) {
+ return `<link rel="stylesheet" href="${file}">`
+ } else if (file.endsWith('.woff')) {
+ return ` <link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
+ } else if (file.endsWith('.woff2')) {
+ return ` <link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
+ } else if (file.endsWith('.gif')) {
+ return ` <link rel="preload" href="${file}" as="image" type="image/gif">`
+ } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
+ return ` <link rel="preload" href="${file}" as="image" type="image/jpeg">`
+ } else if (file.endsWith('.png')) {
+ return ` <link rel="preload" href="${file}" as="image" type="image/png">`
+ } else {
+ return ''
+ }
+ }
总结
- repo 欢迎star⭐️
参考资料
- Server-Side Rendering
Vue3+Vite3 SSR基本搭建相关推荐
- vue3+vite3+vant搭建移动端简易模版
前言 vue3.x相关的生态已经在不断的完善中,相应的UI/路由/pinia等都已成熟,新的项目也在考虑使用新版本开发了,开一个帖子记录一下搭建移动端简易模版的过程,方便以后回顾. vite前端构建工 ...
- 使用vue3 +vite + typeScript + elementPlus搭建一个项目脚手架
使用vue3 + vite + elementPlus搭建一个项目脚手架 这篇文章就教大家如何使用vue3+vite+ts+element-plus搭建一个项目,步骤详细,献给不爱看文档的诸位,希望这 ...
- vue3+vite+electron项目搭建
vue3+vite+electron项目搭建 一.vite创建新项目 二.安装项目依赖 三.修改package.json文件 添加build节点 修改scripts节点 添加main节点 packag ...
- vite+vue3+ts+element-plus项目搭建--超详细
vite+vue3+ts+element-plus项目搭建–超详细 vite 作用 快速的冷启动:不需要等待打包操作: 即时的热模块更新:替换性能和模块数量的解耦让更新飞起: 真正的按需编译:不再等待 ...
- koa 设置cache_node koa2 ssr项目搭建的方法步骤
一.创键项目 1.创建目录 koa2 2.npm init 创建 package.json,然后执行 npm install 3.通过 npm install koa 安装 koa 模块 4.通过 n ...
- vant 项目_基于 vue3.x+vant3.x 搭建示例项目
今天给大家分享一些如何使用Vue3.0+Vant3搭建demo项目. 目前市面上有关vue3的项目并不多,vue3的UI组件库有ant-design-vue和vant-ui. 接下来讲解下使用vue3 ...
- vue前端进阶之SSR篇 --- 搭建简单的SSR框架
目录 理解ssr SSR的重要性 非SSR SSR 了解vapper 制作简易版脚手架 vue框架 环境区分 目录结构 api router views head信息 打包运行 结束 理解ssr 博主 ...
- vue3+ElementPlus后台管理搭建
搭建一个vite项目 https://vitejs.cn/guide/#scaffolding-your-first-vite-project 文章有点长,有写的不对的地方,请各位大佬指点,有问题一起 ...
- vue3+vite+ant-design-vue项目搭建
vite官方传送门:Vite | 下一代的前端工具链 使用如下命令安装vue3+vite项目. my-vue-app是项目名称 npm create vite@latest my-vue-app - ...
最新文章
- java生成pdf看不到至值_java生成pdf报错找不到DPF标题签名
- python编程入门经典实例-编程语言入门经典100例【Python版】
- Java - Jackson JSON Java Parser API
- 在C++中侦测内嵌型别的存在(rev#2)
- 鹅厂设计师是如何做设计的?
- 关于datediff(year,开始日期,结束日期)中year格式的说明,特别注意year两边不能加引号,否则报错...
- SQL逗号分隔的字段统计(摘自网络)
- pptv如何绑定邮箱账号
- 【Linux】Linux 标准目录结构
- JDBC结合JSP使用(2)
- android缩放动画的两种实现方法
- [置顶] “非主流”Web容器之TomJetty之让服务动起来
- Ubuntu 16 安装JDK1.8
- Influx kafka
- 操作系统真象还原_一步步编写操作系统 43 二进制程序的加载方式3
- 实现mvcc_数据库中的引擎、事务、锁、MVCC(三)
- JAVA实现商品信息管理系统
- 中国人想在外国卖东西,这些经典网站不能错过
- 京东商品图片 自动下载 抓取 c# 爬虫
- 搜狗浏览器安装自定义JS脚本的方法~
热门文章
- 汉德森基因检测丨认清自己,才能成为更好的自己
- 计算机xp bios密码设置方法,如何设置电脑的密码
- matlab 判断大小写,匹配正则表达式(区分大小写)
- 不要再「外包」AI 模型了!最新研究发现:有些破坏机器学习模型安全的「后门」无法被检测到...
- 数学系列:数学在计算机图形学中的应用
- 奢侈太后慈禧的起居生活
- java计算机毕业设计高校四六级报名管理系统源程序+mysql+系统+lw文档+远程调试
- 对BLOG皮肤的具体修改
- 2022最火土味情话文案
- 关于ISO26262功能安全管理的理解