news 2026/9/15 14:10:23

使用 Nitro Vite 插件在 Vue 3 项目中实现 Vue Router 服务端渲染(SSR)与按路由代码分割

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Nitro Vite 插件在 Vue 3 项目中实现 Vue Router 服务端渲染(SSR)与按路由代码分割

使用 Nitro Vite 插件在 Vue 3 项目中实现 Vue Router 服务端渲染(SSR)与按路由代码分割

【免费下载链接】nitroNext Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.项目地址: https://gitcode.com/GitHub_Trending/ni/nitro

本文基于 Nitro 仓库中的 vite-ssr-vue-router 示例 与配套文档 docs/4.examples/vite-ssr-vue-router.md 展开。你将学会在 Vite 项目中引入nitro/vite插件,通过client/ssr/nitro三套环境构建一个支持 Vue Router 的服务端渲染应用:按路由懒加载组件与资源、使用 unhead 管理<head>标签、实现服务端渲染与客户端水合(hydration),并理解?assets导入与多环境构建的底层原理。

Nitro 的 Vite 插件(nitro())允许你在纯 Vite 工程中直接获得完整的服务端能力:它自动初始化 Nitro 实例、注入client/ssr/nitro等构建环境,并默认启用?assets资源导入语法(由@hiogawa/vite-plugin-fullstack/assets实现)。本文的示例工程就是一个"自定义框架"式的 SSR 应用——不依赖任何重型元框架,只由 Vue 3 + Vue Router + unhead + Nitro 组装而成,非常适合理解 SSR 体系的最小必要组件。

示例工程总览

示例位于 examples/vite-ssr-vue-router,目录结构如下:

examples/vite-ssr-vue-router/ ├── app/ │ ├── pages/ │ │ ├── about.vue │ │ ├── index.vue │ │ └── not-found.vue │ ├── app.vue │ ├── entry-client.ts │ ├── entry-server.ts │ ├── routes.ts │ ├── shims.d.ts │ └── styles.css ├── README.md ├── package.json ├── tsconfig.json └── vite.config.mjs

package.json声明了完整的依赖与脚本:

{ "type": "module", "scripts": { "build": "vite build", "dev": "vite dev", "preview": "vite preview" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.5", "nitro": "latest", "unhead": "^2.1.12", "vite": "latest", "vite-plugin-devtools-json": "^1.0.0", "vue": "^3.5.31", "vue-router": "^5.0.4" } }

关键依赖说明:

  • nitro:以latest引入,nitro/vite子路径提供nitro()插件;
  • unhead:负责服务端与客户端的<head>管理,transformHtmlTemplate用于把 head 标签注入 HTML 模板;
  • vue / vue-router:应用与路由核心;
  • vite-plugin-devtools-json:开发期调试辅助插件。

tsconfig.json直接继承 Nitro 提供的 TypeScript 配置,保证#nitro/...等虚拟模块与 Vite 环境类型可用:

{ "extends": "nitro/tsconfig" }

整体搭建流程共五步:

  1. 在 Vite 配置中加入 Nitro 插件;
  2. 定义带懒加载组件的路由表;
  3. 编写服务端入口,用 router 渲染应用;
  4. 编写客户端入口,负责水合与接管路由;
  5. 编写页面组件。

一、配置 Vite:接入 Nitro 插件与多环境构建

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" } } }, nitro: { build: { rollupOptions: { treeshake: { moduleSideEffects: () => false } } } }, }, })); // 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; }

nitro() 插件内部做了什么

nitro()由 src/vite.ts 导出(实际实现在 src/build/vite/plugin.ts)。从源码可以看到,它返回一组分工明确的插件:

export function nitro(pluginConfig: NitroPluginConfig = {}): VitePlugin[] { ... return [ nitroInit(ctx), // 初始化 Nitro 实例、解析用户配置 nitroEnv(ctx), // 注入 client / nitro 环境,自动注册服务环境 nitroMain(ctx), // 配置 appType、别名、端口,接管构建与 HMR nitroPrepare(ctx), // 构建前清理输出目录 nitroDevServiceProxy(), nitroPreviewPlugin(ctx), pluginConfig.experimental?.vite?.assetsImport !== false && assetsPlugin({ ... }), // 启用 ?assets 导入 ].filter(Boolean) as VitePlugin[]; }

其中与本文主题直接相关的机制有三个:

  1. 多环境注入nitroEnv):插件会自动补充clientnitro环境,并把 SSR 入口所在的ssr环境自动注册为可 fetch 的服务(源码见setupNitroContextctx.services.ssr的解析逻辑,默认尝试./entry-server)。示例里显式声明三个环境的入口,Nitro 就会据此构建。
  2. ?assets导入assetsPlugin):由@hiogawa/vite-plugin-fullstack/assets提供,可通过experimental.vite.assetsImport关闭(默认true,参见 src/build/vite/types.ts)。它让import xxx from "./file.vue?assets"返回一个携带css/js资源清单与entry信息的资源对象。
  3. SSR 渲染器:当检测到ssr服务入口且未配置自定义 renderer 时,Nitro 会自动挂载内置的ssr-renderer(见nitroEnv.configResolved),ssr环境构建出的入口将以fetch形式对外提供服务。

patchVueExclude 为什么必要

@vitejs/plugin-vue会接管所有.vue文件的转换,但带有?assets查询参数的导入应交给 assets 插件处理,而非被 Vue 插件当作普通组件转换。patchVueExclude包装了 Vue 插件的transform.handler:当请求 id 命中/\?assets/时直接跳过,从而避免 vite-plugin-vue issue #677 中描述的转换冲突。

三个环境的职责

环境入口产物去向作用
client./app/entry-client.ts浏览器静态资源水合逻辑与前端路由接管
ssr./app/entry-server.tsNitro 可 fetch 的服务入口服务端渲染 HTML
nitroNitro 自动生成最终可部署服务串联 SSR 服务与静态资源、处理请求

nitro环境的treeshake.moduleSideEffects设为() => false,是为避免打包服务端时误执行模块副作用(例如页面组件中的顶层副作用代码)。

二、定义路由:懒加载、资源元数据与嵌套路由

app/routes.ts使用RouteRecordRaw类型定义整张路由表:

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"), }, }, ], }, ];

要点解读:

  • 懒加载 + 代码分割:所有组件均使用() => import(...)动态导入,Rollup/Vite 会据此把每个页面切成独立 chunk,实现"访问哪个页面才加载哪个页面的 JS"。
  • meta.assets资源函数:每个路由的meta里挂一个assets函数,它返回import("./xxx.vue?assets")。借助?assets导入,服务端渲染时能精确拿到"该页面组件打包产出的 CSS 与 JS 清单",从而实现按路由精确注入<link>/<script>,避免全量注入。
  • 嵌套路由:根路由app对应app.vue(充当布局),children里注册了首页//about/:catchAll(.*)兜底页,形成父子路由结构。

三、服务端入口:内存路由、按需资源与 head 注入

app/entry-server.ts是整套 SSR 的核心:

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<Response> { 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 */ `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vue Router Custom Framework</title> </head> <body> <div id="root">${body}</div> </body> </html>`; } export default { fetch: handler, };

逐步拆解:

  1. 内存路由:服务端没有浏览器地址栏,因此使用createMemoryHistory()创建路由实例;解析出请求路径后执行await router.push(href)await router.isReady(),确保目标路由的懒加载组件加载完成、导航解析完毕后再渲染。
  2. 按匹配路由收集资源router.currentRoute.value.matched返回当前匹配的整条路由记录链(包含父布局与子页面),逐个取出meta.assets并并行执行,得到各页面的资源清单,最后通过clientAssets.merge(...)合并为一份资源集合。这样每个请求只会注入当前页面真正需要的 CSS 与 JS。
  3. head 管理:unhead 的createHead()创建 head 实例,head.push注入样式表、modulepreload预加载标签与入口module脚本(clientAssets.entry即客户端入口产出的文件名),最后由transformHtmlTemplate把渲染后的 body 与 head 合并进 HTML 模板。
  4. 标准fetch入口:模块默认导出{ fetch: handler }——这正是 Nitro 服务期望的接口形态,ssr环境构建出的入口会被 Nitro 的ssr-renderer直接调用(handler接收标准Request、返回Response),因此该入口天然可被 docs/1.docs/6.server-entry.md 中描述的机制消费。

四、客户端入口:浏览器历史路由与水合

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(),让路由基于浏览器 History API 工作,支持前进/后退与 URL 同步;
  • await router.isReady()等待初始导航完成(含异步组件的解析),随后app.mount("#root")挂载到服务端渲染出的#root节点上——Vue 会复用已渲染的 DOM 完成水合,而不是重新渲染一遍;
  • 挂载后客户端路由接管页面切换,后续导航全部在浏览器内完成,无需再次请求服务器。

五、根组件与页面组件

根布局组件 app.vue

<script setup lang="ts"> import { RouterLink, RouterView } from "vue-router"; import "./styles.css"; </script> <template> <nav> <ul> <li> <RouterLink to="/" exact-active-class="active">Home</RouterLink> </li> <li> <RouterLink to="/about" active-class="active">About</RouterLink> </li> </ul> </nav> <RouterView /> </template> <style scoped> nav { background: white; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); padding: 1rem; } nav ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 2rem; max-width: 800px; margin: 0 auto; } nav a { color: #666; text-decoration: none; } nav a:hover { color: #333; } nav a.active { color: #646cff; } </style>

根组件同时充当导航栏与路由出口:RouterLink渲染导航链接(exact-active-class用于首页精确高亮,active-class用于/about前缀匹配高亮),RouterView渲染当前路由的子组件。由于app.vue对应路由表中的根路由,所有子页面都会渲染在它的<RouterView />之内。

首页 index.vue(含交互状态)

<script setup lang="ts"> import { ref } from "vue"; const count = ref(0); function increment() { count.value++; } </script> <template> <main> <div class="hero"> <h1>Vue Router Custom Framework</h1> <p class="subtitle">A simple demo app with Vite</p> </div> <div class="card counter-card"> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </div> </main> </template> <style scoped> .hero { text-align: center; margin-bottom: 2rem; } .hero h1 { color: rgb(100, 108, 255); } .counter-card { text-align: center; } .counter-card h2 { color: #646cff; margin-bottom: 1rem; } .counter-card p { font-size: 1.5rem; font-weight: bold; margin: 1rem 0; } </style>

首页包含一个计数器演示:服务端渲染时count初始为 0 输出到 HTML;水合后@click="increment"恢复交互,验证了 SSR 输出的静态 HTML 能与客户端响应式状态正确衔接。

About 页与 404 页

<template> <main> <h1>About</h1> <div class="card"> <p>This is a simple Vue Router demo app built with Vite Plugin Fullstack.</p> <p>It demonstrates basic routing and server-side rendering.</p> </div> </main> </template>
<template> <main> <h1>Not Found 404</h1> </main> </template>

not-found.vue/:catchAll(.*)兜底路由承接,任何未匹配路径都会得到 404 页面——并且这一页同样经过服务端渲染与资源注入,保证 SEO 与首屏完整性。

类型声明与全局样式

app/shims.d.ts.vue单文件组件补充 TypeScript 声明:

declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; }

app/styles.css提供全局基础样式(盒模型、字体、背景、卡片与按钮样式),在app.vue中被全局引入,同时服务于服务端渲染产出的 HTML 与客户端水合后的页面。

六、运行与构建

在 examples/vite-ssr-vue-router 目录下安装依赖后(pnpm install),即可使用package.json中的三个脚本:

命令行为
vite dev启动开发服务器,Nitro 与 Vite 协同提供热更新(HMR)与即时 SSR
vite build构建三个环境:客户端静态资源、SSR 服务入口与 Nitro 服务
vite preview本地预览生产构建产物

开发模式下,nitroMain插件的hotUpdate钩子(src/build/vite/plugin.ts)会区分"仅服务端模块"与"共享模块":仅服务端模块变更时向 dev worker 发送full-reload,共享模块变更则走常规 HMR,保证改页面组件时浏览器即时生效。

七、原理小结

这套方案的本质是一张清晰的 SSR 数据流:

  1. 请求进入Nitro 服务(nitro环境产物),由内置ssr-renderer转发到ssr环境构建的服务入口;
  2. 服务端渲染:入口用createMemoryHistory路由匹配请求路径,并行收集匹配路由的?assets资源,renderToString产出 HTML 字符串;
  3. head 组装:unhead 把样式表、modulepreload与入口脚本注入 HTML 模板,返回完整响应;
  4. 客户端水合:浏览器加载入口 JS,createWebHistory路由接管导航,Vue 复用服务端 DOM 完成水合,此后为纯 SPA 交互。

如果你想在此基础上继续深入,可以进一步阅读:

  • Renderer 渲染器文档:了解 Nitro 渲染管线与自定义渲染器的接入方式;
  • Server Entry 服务入口文档:了解{ fetch: handler }入口约定与 Nitro 如何调用它;
  • 仓库中其他基于同套nitro/vite插件体系的示例(如 vite-ssr-react、vite-ssr-solid、vite-ssr-preact),对照阅读可快速迁移到其他前端框架;
  • Nitro Vite 插件源码 src/build/vite/plugin.ts 与类型定义 src/build/vite/types.ts,深入理解环境注入、?assets开关(experimental.vite.assetsImport)与 SSR 服务自动注册机制。

【免费下载链接】nitroNext Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.项目地址: https://gitcode.com/GitHub_Trending/ni/nitro

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 14:09:34

K8s集群Helm仓库私有部署对接实操

K8s集群Helm仓库私有部署对接实操技术栈&#xff1a;Kubernetes v1.32.13 Rocky Linux 8.6 Containerd 1.7.x操作环境 / 对接原理 / 详细步骤 / 完整命令 / 配置文件 / 验证流程 / 排错方案K8s集群Helm仓库私有部署对接实操操作环境K8s 集群 3 节点&#xff1a;k8s-master(19…

作者头像 李华
网站建设 2026/9/15 14:09:34

React Native与鸿蒙跨平台健康数据管理实践

1. 跨平台健康数据管理的核心挑战在移动应用开发领域&#xff0c;健康数据管理一直是个特殊的存在。这类数据通常具有三个典型特征&#xff1a;高频更新&#xff08;如心率监测&#xff09;、多源异构&#xff08;来自不同传感器和设备&#xff09;以及强一致性要求&#xff08…

作者头像 李华
网站建设 2026/9/15 14:07:40

网页版贪吃蛇游戏:从Canvas绘制到游戏循环的完整实现

简介&#xff1a;一款基于HTML、CSS与JavaScript实现的网页版贪吃蛇游戏完整源码包&#xff0c;面向前端初学者、游戏开发爱好者以及需要完成课程设计的学生群体&#xff0c;能够解决从零搭建一个可运行贪吃蛇游戏的需求&#xff0c;体验完整的界面渲染、事件监听与游戏循环开发…

作者头像 李华
网站建设 2026/9/15 14:07:14

基于OpenCV的车牌号码识别Python代码:HSV定位与模板匹配实战

简介&#xff1a;面向计算机视觉课程设计与期末大作业场景&#xff0c;基于OpenCV的车牌号码识别项目提供了可直接运行的Python源码&#xff0c;适合需要快速搭建完整识别流程的本科学生&#xff0c;也适合作为项目实战练习的入门范例。代码覆盖车牌定位、字符分割、SVM分类识别…

作者头像 李华
网站建设 2026/9/15 14:07:07

Vue3工业MQTT实时监控系统设计与实践

简介&#xff1a;本资源是一个面向工业物联网场景的前端管理系统实战项目&#xff0c;专为中高级前端开发者及IoT系统工程师设计&#xff0c;解决工业设备实时监控、多源数据可视化与MQTT协议集成等核心问题。压缩包共262个文件&#xff0c;主体为96个Vue3单文件组件与140个Typ…

作者头像 李华
网站建设 2026/9/15 14:06:46

kubeasz 集群 DNS 部署实战:CoreDNS 与 NodeLocal DNSCache 架构解析

kubeasz 集群 DNS 部署实战&#xff1a;CoreDNS 与 NodeLocal DNSCache 架构解析 【免费下载链接】kubeasz 使用Ansible脚本安装K8S集群&#xff0c;介绍组件交互原理&#xff0c;方便直接&#xff0c;不受国内网络环境影响 项目地址: https://gitcode.com/GitHub_Trending/k…

作者头像 李华