# 使用 Vue Router 的 SSR > 使用 Vite 的 Nitro 中使用 Vue Router 进行服务端渲染。 ```json [package.json] { "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "preview": "vite preview" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.4", "nitro": "latest", "unhead": "^2.1.3", "vite": "beta", "vite-plugin-devtools-json": "^1.0.0", "vue": "^3.5.27", "vue-router": "^4.6.4" } } ``` ```json [tsconfig.json] { "extends": "nitro/tsconfig" } ``` ```js [vite.config.mjs] import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; import devtoolsJson from "vite-plugin-devtools-json"; import { nitro } from "nitro/vite"; export default defineConfig((_env) => ({ plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], environments: { client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, }, })); // Workaround https://github.com/vitejs/vite-plugin-vue/issues/677 function patchVueExclude(plugin, exclude) { const original = plugin.transform.handler; plugin.transform.handler = function (...args) { if (exclude.test(args[1])) return; return original.call(this, ...args); }; return plugin; } ``` ```vue [app/app.vue] ``` ```ts [app/entry-client.ts] import { createSSRApp } from "vue"; import { RouterView, createRouter, createWebHistory } from "vue-router"; import { routes } from "./routes.ts"; async function main() { const app = createSSRApp(RouterView); const router = createRouter({ history: createWebHistory(), routes }); app.use(router); await router.isReady(); app.mount("#root"); } // eslint-disable-next-line unicorn/prefer-top-level-await main(); ``` ```ts [app/entry-server.ts] import { createSSRApp } from "vue"; import { renderToString } from "vue/server-renderer"; import { RouterView, createMemoryHistory, createRouter } from "vue-router"; import { createHead, transformHtmlTemplate } from "unhead/server"; import { routes } from "./routes.ts"; import clientAssets from "./entry-client.ts?assets=client"; async function handler(request: Request): Promise { const app = createSSRApp(RouterView); const router = createRouter({ history: createMemoryHistory(), routes }); app.use(router); const url = new URL(request.url); const href = url.href.slice(url.origin.length); await router.push(href); await router.isReady(); const assets = clientAssets.merge( ...(await Promise.all( router.currentRoute.value.matched .map((to) => to.meta.assets) .filter(Boolean) .map((fn) => (fn as any)().then((m: any) => m.default)) )) ); const head = createHead(); head.push({ link: [ ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), ], script: [{ type: "module", src: clientAssets.entry }], }); const renderedApp = await renderToString(app); const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); return new Response(html, { headers: { "Content-Type": "text/html;charset=utf-8" }, }); } function htmlTemplate(body: string): string { return /* html */ ` Vue Router Custom Framework
${body}
`; } export default { fetch: handler, }; ``` ```ts [app/routes.ts] import type { RouteRecordRaw } from "vue-router"; export const routes: RouteRecordRaw[] = [ { path: "/", name: "app", component: () => import("./app.vue"), meta: { assets: () => import("./app.vue?assets"), }, children: [ { path: "/", name: "home", component: () => import("./pages/index.vue"), meta: { assets: () => import("./pages/index.vue?assets"), }, }, { path: "/about", name: "about", component: () => import("./pages/about.vue"), meta: { assets: () => import("./pages/about.vue?assets"), }, }, { path: "/:catchAll(.*)", name: "not-found", component: () => import("./pages/not-found.vue"), meta: { assets: () => import("./pages/not-found.vue?assets"), }, }, ], }, ]; ``` ```ts [app/shims.d.ts] declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } ``` ```css [app/styles.css] * { box-sizing: border-box; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f5f5; color: #333; } main { max-width: 800px; margin: 0 auto; padding: 2rem; } h1 { font-size: 2.5rem; margin-bottom: 0.5rem; } .card { background: white; border-radius: 8px; padding: 2rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin: 2rem 0; } button { background: rgb(83, 91, 242); color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; font-size: 1rem; cursor: pointer; } button:hover { background: #535bf2; } .subtitle { color: #666; font-size: 1.1rem; margin-bottom: 2rem; } ``` ```vue [app/pages/about.vue] ``` ```vue [app/pages/index.vue] ``` ```vue [app/pages/not-found.vue] ```
设置使用 Vue、Vue Router、Vite 和 Nitro 进行服务器端渲染(SSR)。此配置支持按路由拆分代码、使用 unhead 进行 head 管理以及客户端水合(hydration)。 ## 概览 #### 在 Vite 配置中添加 Nitro Vite 插件 #### 定义带有懒加载组件的路由 #### 创建支持路由的服务器入口渲染应用 #### 创建客户端入口进行水合并接管路由 #### 创建页面组件 ## 1. 配置 Vite 在 Vite 配置中添加 Nitro 和 Vue 插件。定义 `client` 和 `ssr` 两个环境: ```js [vite.config.mjs] import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; import devtoolsJson from "vite-plugin-devtools-json"; import { nitro } from "nitro/vite"; export default defineConfig((_env) => ({ plugins: [patchVueExclude(vue(), /\?assets/), devtoolsJson(), nitro()], environments: { client: { build: { rollupOptions: { input: "./app/entry-client.ts" } } }, ssr: { build: { rollupOptions: { input: "./app/entry-server.ts" } } }, }, })); // 解决 https://github.com/vitejs/vite-plugin-vue/issues/677 function patchVueExclude(plugin, exclude) { const original = plugin.transform.handler; plugin.transform.handler = function (...args) { if (exclude.test(args[1])) return; return original.call(this, ...args); }; return plugin; } ``` `patchVueExclude` 辅助函数防止 Vue 插件处理带有 `?assets` 查询参数的资源导入(asset imports)。 ## 2. 定义路由 创建路由定义,使用懒加载组件和资源元数据: ```ts [app/routes.ts] import type { RouteRecordRaw } from "vue-router"; export const routes: RouteRecordRaw[] = [ { path: "/", name: "app", component: () => import("./app.vue"), meta: { assets: () => import("./app.vue?assets"), }, children: [ { path: "/", name: "home", component: () => import("./pages/index.vue"), meta: { assets: () => import("./pages/index.vue?assets"), }, }, { path: "/about", name: "about", component: () => import("./pages/about.vue"), meta: { assets: () => import("./pages/about.vue?assets"), }, }, { path: "/:catchAll(.*)", name: "not-found", component: () => import("./pages/not-found.vue"), meta: { assets: () => import("./pages/not-found.vue?assets"), }, }, ], }, ]; ``` 使用动态导入实现组件懒加载,支持代码拆分。`meta.assets` 函数用于加载与路由相关的 CSS 和 JS 代码块。在根布局组件下定义子路由,实现嵌套路由。 ## 3. 创建服务器入口 服务器入口渲染 Vue 应用,支持路由和 head 管理: ```ts [app/entry-server.ts] import { createSSRApp } from "vue"; import { renderToString } from "vue/server-renderer"; import { RouterView, createMemoryHistory, createRouter } from "vue-router"; import { createHead, transformHtmlTemplate } from "unhead/server"; import { routes } from "./routes.ts"; import clientAssets from "./entry-client.ts?assets=client"; async function handler(request: Request): Promise { const app = createSSRApp(RouterView); const router = createRouter({ history: createMemoryHistory(), routes }); app.use(router); const url = new URL(request.url); const href = url.href.slice(url.origin.length); await router.push(href); await router.isReady(); const assets = clientAssets.merge( ...(await Promise.all( router.currentRoute.value.matched .map((to) => to.meta.assets) .filter(Boolean) .map((fn) => (fn as any)().then((m: any) => m.default)) )) ); const head = createHead(); head.push({ link: [ ...assets.css.map((attrs: any) => ({ rel: "stylesheet", ...attrs })), ...assets.js.map((attrs: any) => ({ rel: "modulepreload", ...attrs })), ], script: [{ type: "module", src: clientAssets.entry }], }); const renderedApp = await renderToString(app); const html = await transformHtmlTemplate(head, htmlTemplate(renderedApp)); return new Response(html, { headers: { "Content-Type": "text/html;charset=utf-8" }, }); } function htmlTemplate(body: string): string { return /* html */ ` Vue Router Custom Framework
${body}
`; } export default { fetch: handler, }; ``` 服务器端使用 `createMemoryHistory()`,因为没有浏览器地址栏——路由器会先导航至请求的 URL 再进行渲染。根据匹配的路由动态加载对应资源,确保只包含当前页面所需的 CSS 和 JS。`unhead` 库管理 `` 元素,通过 `transformHtmlTemplate` 注入样式与脚本。 ## 4. 创建客户端入口 客户端入口负责水合服务器渲染后的 HTML 并接管路由: ```ts [app/entry-client.ts] import { createSSRApp } from "vue"; import { RouterView, createRouter, createWebHistory } from "vue-router"; import { routes } from "./routes.ts"; async function main() { const app = createSSRApp(RouterView); const router = createRouter({ history: createWebHistory(), routes }); app.use(router); await router.isReady(); app.mount("#root"); } // eslint-disable-next-line unicorn/prefer-top-level-await main(); ``` 客户端入口使用 `createWebHistory()` 创建 Vue 应用以支持浏览器路由。路由器准备完毕后,挂载到 `#root` 元素,并完成 HTML 水合。 ## 5. 创建根组件 根组件提供导航功能并渲染子路由: ```vue [app/app.vue] ``` ## 了解更多 - [Vue Router 文档](https://router.vuejs.org/) - [Unhead 文档](https://unhead.unjs.io/) - [Renderer](/docs/renderer) - [Server Entry](/docs/server-entry)