ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

使用 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.mjspackage.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 }整体搭建流程共五步在 Vite 配置中加入 Nitro 插件定义带懒加载组件的路由表编写服务端入口用 router 渲染应用编写客户端入口负责水合与接管路由编写页面组件。一、配置 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[]; }其中与本文主题直接相关的机制有三个多环境注入nitroEnv插件会自动补充client与nitro环境并把 SSR 入口所在的ssr环境自动注册为可 fetch 的服务源码见setupNitroContext中ctx.services.ssr的解析逻辑默认尝试./entry-server。示例里显式声明三个环境的入口Nitro 就会据此构建。?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信息的资源对象。SSR 渲染器当检测到ssr服务入口且未配置自定义 renderer 时Nitro 会自动挂载内置的ssr-renderer见nitroEnv.configResolvedssr环境构建出的入口将以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 的服务入口服务端渲染 HTMLnitroNitro 自动生成最终可部署服务串联 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?assetsclient; async function handler(request: Request): PromiseResponse { 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;charsetutf-8 }, }); } function htmlTemplate(body: string): string { return /* html */ !DOCTYPE html html langen head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleVue Router Custom Framework/title /head body div idroot${body}/div /body /html; } export default { fetch: handler, };逐步拆解内存路由服务端没有浏览器地址栏因此使用createMemoryHistory()创建路由实例解析出请求路径后执行await router.push(href)与await router.isReady()确保目标路由的懒加载组件加载完成、导航解析完毕后再渲染。按匹配路由收集资源router.currentRoute.value.matched返回当前匹配的整条路由记录链包含父布局与子页面逐个取出meta.assets并并行执行得到各页面的资源清单最后通过clientAssets.merge(...)合并为一份资源集合。这样每个请求只会注入当前页面真正需要的 CSS 与 JS。head 管理unhead 的createHead()创建 head 实例head.push注入样式表、modulepreload预加载标签与入口module脚本clientAssets.entry即客户端入口产出的文件名最后由transformHtmlTemplate把渲染后的 body 与 head 合并进 HTML 模板。标准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.vuescript setup langts import { RouterLink, RouterView } from vue-router; import ./styles.css; /script template nav ul li RouterLink to/ exact-active-classactiveHome/RouterLink /li li RouterLink to/about active-classactiveAbout/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 langts import { ref } from vue; const count ref(0); function increment() { count.value; } /script template main div classhero h1Vue Router Custom Framework/h1 p classsubtitleA simple demo app with Vite/p /div div classcard counter-card pCount: {{ count }}/p button clickincrementIncrement/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水合后clickincrement恢复交互验证了 SSR 输出的静态 HTML 能与客户端响应式状态正确衔接。About 页与 404 页template main h1About/h1 div classcard pThis is a simple Vue Router demo app built with Vite Plugin Fullstack./p pIt demonstrates basic routing and server-side rendering./p /div /main /templatetemplate main h1Not Found 404/h1 /main /templatenot-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与即时 SSRvite build构建三个环境客户端静态资源、SSR 服务入口与 Nitro 服务vite preview本地预览生产构建产物开发模式下nitroMain插件的hotUpdate钩子src/build/vite/plugin.ts会区分仅服务端模块与共享模块仅服务端模块变更时向 dev worker 发送full-reload共享模块变更则走常规 HMR保证改页面组件时浏览器即时生效。七、原理小结这套方案的本质是一张清晰的 SSR 数据流请求进入Nitro 服务nitro环境产物由内置ssr-renderer转发到ssr环境构建的服务入口服务端渲染入口用createMemoryHistory路由匹配请求路径并行收集匹配路由的?assets资源renderToString产出 HTML 字符串head 组装unhead 把样式表、modulepreload与入口脚本注入 HTML 模板返回完整响应客户端水合浏览器加载入口 JScreateWebHistory路由接管导航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),仅供参考
返回列表