vite + vue3 + ts + SSR 使用 Vite 内置 SSR 功能 - vue3 项目实战
vite + vue3 + ts + SSR 使用 Vite 内置 SSR 功能
搭建 vue3 ssr,有多种方式:
- 使用
Vite 内置 SSR 功能
,它 Vite 的底层 SSR 功能。 - 使用
vite-ssr
插件。基于Vite 内置 SSR 功能
,简化了服的务器端渲染。只适用于 vue3。 - 使用
vite-plugin-ssr
插件。基于Vite 内置 SSR 功能
,提供比 Vite 的底层 SSR 更流畅的体验。适用于 vue3、vue2。 - 使用
ssr
插件。基于Vite 内置 SSR 功能
。适用于 vue3、vue2。
安装 vue3
cd /var/web/www npm init vue@latest
项目名称:exampleSSR
cd /var/web/www exampleSSR npm install
安装 axios
cd /var/web/www exampleSSR npm install axios
至此,已经安装 vite + vue3 + TypeScript + vue-router + pinia + axios。还有 vitest 单元测试框架、cypress 端到端测试、ESLint 语法规则检测工具、Prettier 代码格式化工具。
局部安装 express
Express基于 NodeJs 平台,快速、简洁的 Web 应用框架,为 Web 和移动应用程序提供一组强大的功能。中文:https://www.expressjs.com.cn
cd /var/web/www/exampleSSR # 在项目中,安装 Express npm install express -S# 在项目中,安装 TypeScript 依赖 npm install @types/express -D
node 无法直接运行 ts 文件,需要使用 ts-node。Express 是基于 node 环境的,所以还需要安装 node TypeScript 依赖。
cd /var/web/www/exampleSSR # 在项目中,安装 ts-node npm install ts-node -D# 在项目中,安装 TypeScript 依赖 npm install @types/node -D
查看已安装
npm list npm list express npm list ts-node
配置格式化
项目根目录下,tsconfig.json文件中,新增compilerOptions选项:
"compilerOptions": { "types": ["node", "jsdom", "vite/client", "element-plus/global"], "target": "esnext", "module": "esnext", "lib": ["dom", "esnext"], "strict": true },
在项目根目录下,修改.prettierrc.json文件
{ "semi": true, "singleQuote": true, "printWidth": 200, "endOfLine": "lf" }
在项目根目录下,修改.eslintrc.cjs文件,新增rules选项:
/* eslint-env node */ require('@rushstack/eslint-patch/modern-module-resolution'); module.exports = { root: true, extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier'], overrides: [ { files: ['cypress/e2e/**.{cy,spec}.{js,ts,jsx,tsx}'], extends: ['plugin:cypress/recommended'], }, ], parserOptions: { ecmaVersion: 'latest', }, rules: { 'prettier/prettier': [ 'warn', { semi: true, singleQuote: true, printWidth: 200, endOfLine: 'lf', }, ], }, };
配置完毕后,需要重启 vscode 编辑器。然后运行以下命令,来格式化文件。
npm run lint npm run dev
使用 Vite 内置 SSR 功能
SSR(Server-Side Rendering):服务器端渲染。Vite 为服务端渲染(SSR)提供了内置支持,一个底层 API,是为制作库和框架准备的。
SSR 文件结构
一个典型的 SSR 应用应该有如下的源文件结构:
- index.html - server.ts # 服务端启动文件 - src/ - main.ts # 导出环境无关的(通用的)应用代码 - entry-client.ts # 客户端入口,应用挂载元素。将应用挂载到一个 DOM 元素上 - entry-server.ts # 服务端入口,处理服务端逻辑和静态资源。使用某框架的 SSR API 渲染该应用
创建三个文件:
touch server.ts src/entry-client.ts src/entry-server.ts
Node 运行 ESM 模式
在服务端运行 node,需要开启 esm 模式,另外,需要 ts-node,用node --loader ts-node/esm
来执行 TS 文件。官方文档:package.json文件中,必须设置"type":"module"
,实现运行esm模式。tsconfig.json文件中,必须设置"module":"ESNext"
。注意:这里是node 16+版本。
# package.json "name": "exampleSSR", "version": "0.0.0", "type": "module", "scripts": { "dev": "node --loader ts-node/esm server.ts", // 运行 server.ts 文件 }
# tsconfig.json "compilerOptions": { "strict": true, "target": "esnext", "module": "esnext", "lib": ["esnext", "dom", "dom.iterable", "scripthost"], "types": ["node", "jsdom", "vite/client", "element-plus/global"] }, "ts-node": { "esm": true, "compilerOptions": { "moduleResolution": "node", "allowSyntheticDefaultImports": true, "esModuleInterop": true, } },
index.html
index.html将需要引用entry-client.ts,以实现客户端(在浏览器中)动态交互。另外,占位标记,服务端渲染时替换为输出的 HTML 内容。静态资源占位符(js 、css 文件):
。占位标记可以根据个人喜好,随意起名称。
Vite SSR
server.ts
这个server.ts文件的功能是启动一个 Nodejs Web 服务,来响应客户端请求,然后根据请求,读取index.html文件,处理资源后把其中的占位符(注释)进行替换,最后把 HTML 页面内容,发送给请求者阅览。
在构建 SSR 应用程序时,你可能希望由 Web 服务器控制,并将 Vite 与生产环境脱钩。因此,建议以中间件模式使用 Vite。
import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import express from 'express'; import { createServer as createViteServer } from 'vite'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function createServer() { // 创建node服务,设置端口 const app = express(); const PORT = 5173; app.set('port', PORT); // 以中间件模式创建 Vite 应用,这将禁用 Vite 自身的 HTML 服务逻辑 // 并让上级服务器接管控制 const vite = await createViteServer({ server: { middlewareMode: true }, appType: 'custom', }); // 使用 vite 的 Connect 实例作为中间件 // 如果你使用了自己的 express 路由(express.Router()),你应该使用 router.use app.use(vite.middlewares); // 服务 index.html // exporss 请求拦截器, * 全部路由, req 参数是 HTTP 请求 request,res 参数是请求响应 respones,next 参数,路由 app.use('*', async (req, res, next) => { const url = req.originalUrl; try { // 1. 读取 index.html let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8'); // 2. 应用 Vite HTML 转换。这将会注入 Vite HMR 客户端, // 同时也会从 Vite 插件应用 HTML 转换。 // 例如:@vitejs/plugin-react 中的 global preambles template = await vite.transformIndexHtml(url, template); // 3. 加载服务器入口。vite.ssrLoadModule 将自动转换 // 你的 ESM 源码使之可以在 Node.js 中运行!无需打包 // 并提供类似 HMR 的根据情况随时失效。 const { render } = await vite.ssrLoadModule('/src/entry-server.ts'); // 4. 渲染应用的 HTML。这假设 entry-server.ts 导出的 `render` // 函数调用了适当的 SSR 框架 API。 // 例如 ReactDOMServer.renderToString() const [appHtml, renderState] = await render(url); // 传递 Pinia 状态管理。自定义 window 属性 __pinia let appState = ''; if (renderState) { appState = ""; } // 5. 注入渲染后的应用程序 HTML 到模板中。 const html = template.replace(``, appHtml).replace(``, appState); // 6. 返回渲染后的 HTML。 res.status(200).set({ 'Content-Type': 'text/html' }).end(html); } catch (e: any) { // 如果捕获到了一个错误,让 Vite 来修复该堆栈,这样它就可以映射回你的实际源码中。 vite?.ssrFixStacktrace(e); next(e); } }); process.on('warning', (warning) => { console.warn(warning.name); // 打印告警名称 console.warn(warning.message); // 打印告警信息 console.warn(warning.stack); // 打印堆栈信息 }); app.listen(PORT); } createServer();
router/index.ts
import { createRouter as _createRrouter, createMemoryHistory, createWebHistory } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router'; import HomeView from '../views/HomeView.vue'; const routes: RouteRecordRaw[] = [ { path: '/', name: 'home', component: HomeView, }, { path: '/about', name: 'about', component: () => import('../views/AboutView.vue'), }, ]; export function createRouter() { return _createRrouter({ history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(import.meta.env.BASE_URL), routes, }); }
stores/index.ts
import { defineStore, createPinia } from 'pinia'; import type { MetaObj } from '@/types/book'; export const useUserStore = defineStore('user', { state: () => { return { name: '张三', age: 20 }; }, actions: { updateName(name: string) { this.name = name; }, updateAge(age: number) { this.age = age; } } }); export const useBrowserStore = defineStore('browser', { state: () => ({ browserStatus: 0, }), getters: { initBrowserStatus(state) { return (state.browserStatus = 0); }, }, }); export const createStore = () => { const pinia = createPinia(); useBrowserStore(pinia); useUserStore(pinia); return pinia; }; export default createStore;
main.ts
在 SSR 环境下,服务器只会初始化一次。因为每个请求到达服务端,每次请求必须是全新的实例,为了防止状态污染,所以main.ts每次都返回全新的 vue 实例,router 实例,store 实例等。
import { createSSRApp } from 'vue'; import createStore from '@/stores'; import router from './router'; import App from './App.vue'; import './assets/main.css'; // SSR 要求每个请求都有一个新的应用程序实例,因此我们导出一个函数。 // 创建新的应用程序实例。如果使用 Pinia、Vuex,我们也会在这里创建一个新 Store。 export function createApp() { const app = createSSRApp(App); const pinia = createPinia(); app.use(router); app.use(pinia); return { app, router, pinia }; }
entry-server.ts
服务端入口文件entry-server.ts:主要是调用 SSR 的renderToString
和收集需要发送的资源数据。
import type { RouteLocationRaw } from 'vue-router'; import { renderToString } from 'vue/server-renderer'; import { createApp } from './main'; export async function render(url: RouteLocationRaw) { const { app, router, pinia } = createApp(); router.push(url); await router.isReady(); const ctx = {}; const html = await renderToString(app, ctx); return [html, pinia.state.value]; }
entry-client.ts
客户端入口文件entry-client.ts:主要用于挂载节点和初始化数据。
import { createApp } from './main' const { app, router, pinia } = createApp() // wait until router is ready before mounting to ensure hydration match router.isReady().then(() => { if (window.__pinia) { pinia.state.value = JSON.parse(window.__pinia); } app.mount('#app') })
TypeScript 文件env.d.ts中添加:
interface Window { __pinia: any; }
鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com
图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!