Vue搭建SSR全栈平台,Nuxt才是Top 1!
前言
什么是SSR(Server-Side Rendering)?类似于用Java、ASP.NET、php开发前端页面,服务端准备数据并执行页面渲染,然后把完整的HTML发送给客户端。现代化的SSR通常和Vue、React等前端框架强关联,通过Node.js在服务端提前将page、component使用同构方式渲染为html代码,其目的是在实现SSR时尽量复用一套渲染机制。例如Vue框架就同时支持了CSR(Client-Side Rendering)和SSR。
SSR能解决什么问题?
-
SEO: Google、百度、Yandex、Bing或者Yahoo等搜索引擎会通过网络爬取你的页面并建立索引,如果的信息越完整,那么你的网站就更容易被检索到。使用SSR技术,让页面在服务端提前渲染好再返回给客户端,这样各类搜索引擎就能拿到比较完整的页面信息,提升检索质量。 -
首屏渲染:SPA(Single Page Application)将页面渲染放到客户端执行,并且在渲染之前要加载大量的Javascript代码,所以首屏渲染需要花费较长时间。而SSR直接在服务端渲染完再返回客户端,使用户能够快速看到页面内容。
「有哪些框架支持SSR?」
| 开源库 | 支持语言 | star数 | 描述 |
|---|---|---|---|
| Nuxt | Vue | 54.9k | 快速构架、类型安全、高性能、易扩展 |
| Next | React | 127k | 老牌框架、社区完善、Data Fetch、Styling |
| quasar | Vue | 26k | 丰富的UI、支持桌面端和移动端 |
| Remix | React | 29.9k | 极致的用户体验、开箱即用 |
什么是Nuxt

基于Vue、React、Angular实现的full-stack框架数不胜数,但就Vue框架领域内,应该数Nuxt最为Top 1。Nuxt保持了Vue生态的“开箱即用”特性,类似于Vite,几条指令就可直接run起来。如果你熟悉Vue,那么基于Nuxt写应用,体验上和写Vue客户端应用极其相似。
Nuxt提前预制了一套目录结构,并自动处理路由、导入、数据获取,开发人员仅需按约定的规则实现页面、组件、数据API即可,极大降低开发人员学习成本。Nuxt的特性可总结为:
-
基于文件的路由: 页面统一添加到 pages/目录下,例如添加index.vue、about.vue、contact.vue,就可以通过/访问首页,通过/about返回关于页面; -
代码分割:得益于Vue的 Virtual Node,Nuxt根据目录规则提前生成完整的依赖树,这样能够将所有code拆分为最小单元的chunks, 从而减少应用初始化加载时间; -
开箱即用的SSR:Nuxt内部使用Vue的SSR,除了Vue实现SSR、CSR本身的差异,可以像开发CSR的Vue应用一样来开发SSR,降低研发心智负担; -
自动导入:在实现页面或组件时,不需要像CSR手动导入( import Card from './Card.Vue')依赖,当Nuxt识别到有使用外部组件,bundle过程会自动识别; -
数据获取工具:提供一套组合式FetchAPI,实现client、server端的同构; -
配置好的构建工具:「默认使用Vite构建,同时支持Webpack、Rspack」;
Nuxt的架构设计和vue/core很相似,将整体功能拆分为独立的package:
-
核心引擎:nuxt; -
bundlers: 打包器包含 @nuxt/vite-builde、nuxt/webpack-builder、@nuxt/rspack-builder; -
命令行工具:nuxi; -
服务端引擎:nitro; -
开发套件:@nuxt/kit;
上手体验
初始化项目
使用nuxi提供的指令初始化项目:
npx nuxi@latest init nuxt-learn-examples
如果安装过程下载失败,可能需要配置hosts:
185.199.108.133 raw.githubusercontent.com
「最新nuxt版本3.14初始化的项目目录比较简单,少了pages、plugins等目录」,入口为app.vue:

nuxt默认在app.vue中添加了两个demo组件,需要手动删除,并安需添加路由组件:
<template><NuxtPage /></template>
「如果要使用三方的UI组件,例如element-ui,先创建plugins目录,并添加element-ui.ts」,nuxt框架在构建过程会自动加载plugins目录下的所有插件。
import Vue from 'vue'import Element from 'element-ui'import locale from 'element-ui/lib/locale/lang/en'Vue.use(Element, { locale })
路由
Nuxt基于Vue-Router实现路由,区别于CSR路由,使用Nuxt仅需要在pages目录下添加页面,并且支持动态路由。例如新增目录:
- pages- index.vue- about.vue- products- [id].vue
Nuxt会自动将文件转换为Vue-Router的路由配置:
{"routes": [{"path": "/index","component": "pages/index.vue"},{"path": "/about","component": "pages/about.vue"},{"path": "/products/:id","component": "pages/products/[id].vue"}]}
「Nuxt提供<NuxtLink>创建导航连接,例如<NuxtLink to="/about">关于</NuxtLink>,当<NuxtLink>标签在视图范围内可见,则自动预取链接页面的组件,从而加快导航速度。」
在写SPA页面时,可通过router.beforeEach((to, from, next) => {}添加路由验证、拦截。而Nuxt通过路由中间件形式实现路由拦截,在middleware目录下添加auth.ts文件:
export default defineNuxtRouteMiddleware((to, from) => {if (isAuthenticated() === false) {return navigateTo('/login')}})
「navigationTo函数重定向到给定的路径,并在服务端发生重定向时设置response code为302。文件名auth也会作为中间件的ID,路由对哪些页面生效,需要在页面添加中间件配置,指明需要使用哪些中间件。」
<script setup lang="ts">definePageMeta({middleware: 'auth'})</script>
「如果中间件是全局性的,则可以通过添加global后缀标示」,例如setup.global.ts。
「当有多个中间件被执行时,按什么顺序执行」?nuxt根据文件名按字母进行排序,可通过如下形式排好执行顺序:
middleware/01.setup.global.ts02.analytics.global.tsauth.ts
「除了通过在middleware目录下添加中间件外,Nuxt提供了动态添加中间件方式addRouteMiddleware」,例如在插件中添加。
export default defineNuxtPlugin(() => {addRouteMiddleware('setup', () => {}, { global: true })}
SEO和Meta
Nuxt提供了多种设置SEO和head属性的方法,不管是配置或者函数都提供完整的TypeScript支持。
「useSEOMeta函数支持通过一个扁平化的Object对象设置SEO相关属性」。
<script setup lang="ts">useSeoMeta({title: '淘贝购物',ogTitle: '陶贝购物',description: '我是一个购物网站,比淘宝还厉害。',ogDescription: '我是一个购物网站,比淘宝还厉害。',ogImage: 'https://taobei.com/image.png',twitterCard: 'taobei_summary_large_image',})</script>
除了通过useSEOMeta函数设置SEO属性,「Nuxt还提供了<Title>、<Base>、<NoScript>、<Style>、<Meta>、<Link>、<Body>、<Html>和<Head>组件可直接在template使用。」 例如在Head组件下添加Title、Meta、Style。
<script setup lang="ts">const title = ref('你好,世界')</script><template><div><Head><Title>{{ title }}</Title><Meta name="description" :content="title" /><Style type="text/css" children="body { background-color: green; }" /></Head><h1>{{ title }}</h1></div></template>
除了使用组件形式为head添加Meta信息外,还可以使用useHead函数设置。例如在app.vue添加:
<script setup lang="ts">const description = ref('我的神奇网站。')useHead({meta: [{ name: 'description', content: description }],})</script>
如果想引入外部css、字体等资源,也可以通过useHead的Link属性设置。
<script setup lang="ts">useHead({link: [{rel: 'preconnect',href: 'https://fonts.googleapis.com'},{rel: 'stylesheet',href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap',crossorigin: ''}]})</script>
样式使用
在样式化方面,Nuxt 非常灵活。你可以编写自己的样式,或者引用本地和外部样式表。你可以使用 CSS 预处理器、CSS 框架、UI 库和 Nuxt 模块来为你的应用程序添加样式。
本地编写的样式表,可将其放到assets目录下,如果想在组件中引入这些css文件,可通过javascript的import,或者使用css的@import语句。
<script>import '~/assets/css/first.css'</script><style>@import url("~/assets/css/second.css");</style>
一些css文件需要全局导入,那么可以在nuxt.config.ts文件添加css文件导入。
export default defineNuxtConfig({css: ['~/assets/css/main.css']})
如果要使用字体文件,可将字体文件放到public目录下,例如放到public/fonts/FarAwayGalaxy.woff。这样就可以在css样式中使用这些字体了。
// 字体定义-face {font-family: 'FarAwayGalaxy';src: url('/fonts/FarAwayGalaxy.woff') format('woff');font-weight: normal;font-style: normal;font-display: swap;}// 字体使用:h1 {font-family: 'FarAwayGalaxy', sans-serif;}
通过npm安装的样式,例如npm install animate.css,可直接在组件中使用, 可使用javascript的import或者style的@import方式导入。
<script>import 'animate.css'</script><style>@import url("animate.css");</style>
如果想把animate.css添加到全局,上文中有介绍在nuxt.config.ts中导入。
export default defineNuxtConfig({css: ['animate.css']})
引入三方cdn的css资源,一般会添加到head的link中。添加方式包含静态、动态两种。静态方式为在nuxt.cofig.ts的head属性下附加link属性,如下述代码所示。
export default defineNuxtConfig({app: {head: {link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' }]}}})
动态方式可使用useHead函数添加三方css资源。
useHead({link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css' }]})
Nuxt框架下,组件使用样式方式和客户端组件类似。
<script setup lang="ts">const isActive = ref(true)const hasError = ref(false)const classObject = reactive({active: true,'text-danger': false})</script><template><div class="static" :class="{ active: isActive, 'text-danger': hasError }"></div><div :class="classObject"></div></template>
像客户端支持的.scss、.sass、.less、.styl 和 .stylus,Nuxt也支持在组件的style标签上设置lang,例如<style lang="less"></style。
数据获取
Nuxt 提供了两个组合函数和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetch、useAsyncData 和 $fetch。
「请求数据为什么需要特定的组合函数?一个组件在服务端、客户端都被执行,通过组合函数能解决服务端、客户端的同构问题,让数据请求在两个端都能正常运行,可避免数据重复请求以及异步加载等问题。」
「解决网络重复请求」
useFetch 和 useAsyncData 组合函数确保一旦在服务器上进行了 API 调用,数据将以有效的方式在负载中传递到客户端。只要服务端执行过useFetch、useAsyncData函数,则其结果将被序列化传送给客户端,而客户端通过useNuxtApp().payload访问这些数据。
使用 Nuxt DevTools 在 「Payload 选项卡」 中检查此数据。

「解决数据请求和界面交互同步」
组件支持top level方式请求数据,例如直接在script下使用useFetch获取数据,并且一个页面下可能有多个组件都会请求数据,那何时界面可交互?Nuxt 在底层使用 Vue 的 <Suspense> 组件防止在数据请求完成前进行交互、导航。
<script setup lang="ts">const { data: count } = await useFetch('/api/count')</script><template>页面访问量:{{ count }}</template>
「$ofetch」
Nuxt 包括了 ofetch 库,并且作为全局别名 $fetch 自动导入到应用程序中。它是 useFetch 在幕后使用的工具。
ofetch 库是基于 Fetch API 构建的,并为其添加了便利功能:
-
在浏览器、Node 或 worker 环境中的使用方式相同 -
自动解析响应 -
错误处理 -
自动重试 -
拦截器
什么时候使用$fetch?当客户端异步提交数据时,不涉及到页面状态,因此可直接使用$fetch提交数据。
「仅在客户端获取数据」
默认情况下,useFetch组合函数在客户端、服务端都会执行,可通过给第二个参数server:false关闭服务端的请求。对于首次渲染不需要的数据(如非SEO敏感数据),可通过设置lazy: true让首次渲染不用等待该请求。
/* 此调用仅在客户端执行 */const { pending, data: posts } = useFetch('/api/comments', {lazy: true,server: false})
「缓存和重新获取数据」
useFetch使用提供的url作为缓存键,也可在最后一个options参数显式指定key作为缓存键。
useAsyncData如果第一个参数是字符串,则将其用作缓存键。如果第一个参数是执行查询的处理函数,则「会为useAsyncData的实例生成一个基于文件名和行号的唯一键」。
「useFetch将返回的数据转换为响应式,并且提供了手动请求或刷新的方法。」
<script setup lang="ts">const { data, error, execute, refresh } = await useFetch('/api/users')</script><template><div><p>{{ data }}</p><button @click="refresh">刷新数据</button></div></template>
「想要查询条件变化时自动重新请求数据?」 可在options的watch属性指定监听值。
const id = ref(1)const { data, error, refresh } = await useFetch('/api/users', {/* 更改id将触发重新获取 */watch: [id]})
如果「query的参数变化也自动请求,只要传递的响应式」,则useFetch会帮你自动完成监听。
const id = ref(null)const { data, pending } = useLazyFetch('/api/user', {query: {user_id: id}})
状态管理
Nuxt提供了强大的状态管理库和useState组合函数,用于创建响应式且适用于SSR的共享状态。useState用于在组件之间创建响应式且适用于SSR的共享状态。
「useState的值将在服务端渲染后保留,并在客户端渲染期间进行水合(hydration),其唯一键在多个组件间共享。由于useState在服务端渲染后要传递给客户端,需进行序列化,所以像类、函数等不支持共享。」
如何共享?例如在app.vue中使用useState定义了key为counter的state,其他组件可通过useState('counter')获取key为counter的state值。
<script setup lang="ts">const counter = useState('counter', () => Math.round(Math.random() * 1000))</script><template><div>计数器:{{ counter }}<button @click="counter++">+</button><button @click="counter--">-</button></div></template>
「如何定义全局状态?」
通过使用「自动导入的组合函数」,我们可以定义全局类型安全的状态并在整个应用程序中导入它们。例如添加composables/states.ts文件并附加内容:
export const useCounter = () => useState<number>('counter', () => 0)export const useColor = () => useState<string>('color', () => 'pink
')
那么服务端在渲染每一个页面时都会加载states.ts中的状态,因此可以在组件中直接读取。
<script setup lang="ts">const color = useColor() // 与useState('color')相同</script><template><p>当前颜色:{{ color }}</p></template>
「使用第三方库」
Nuxt与流行的状态库有多种集成方式:
-
pinia npm i pinia @pinia/nuxt❝如果你正在使用 npm,你可能会遇到 ERESOLVE unable to resolve dependency tree 错误。如果那样的话,将以下内容添加到
package.json中:❞
"overrides": { "vue": "latest" }在
nuxt.config.ts中配置pinia。// Nuxt 3 export default defineNuxtConfig({ modules: ['@pinia/nuxt'], })配置以后就可以正常使用pinia了,例如新增一个
stores/myStore.ts文件,添加Store内容:import { defineStore } from "pinia"; interface MyState { version: string; } export const useStore = defineStore<'myStore', MyState>('myStore', { state: () => { return { version: "1.0" } } })在组件中使用myStore:
<script setup lang="ts"> import { useStore } from '~/stores/myStore' const store = useStore() </script>
除此之外,Nuxt还支持了:
-
Harlem - 不可变的全局状态管理库 -
XState - 基于状态机的方法,具有可视化和测试状态逻辑的工具
Nuxt功能不够用?使用Module轻松扩展
250个Module
一个框架好不好用,功能丰不丰富当属核心考量方面。Nuxt通过Module机制能够轻松的扩展其功能。到目前Nuxt已提供「250个Module」,「800个贡献者」参与。

如果以上的模块还满足不了你的需求,那可以考虑上手写一个Module。「Nuxt的配置和钩子系统使得可以定制Nuxt的每个方面,并添加任何可能需要的集成(Vue插件、服务器路由、组件、日志记录等)。」
扩展自己的Module
nuxt命令行工具也提供了Module项目快速创建指令。
npx nuxi init -t module my-module
接着使用npm run dev:prepare为项目准备本地文件。
在编写Module时,常常需要一个可运行的程序来测试,而Nuxt在创建module项目时默认会为你创建一个playground目录,其下就包含了可运行的Nuxt程序, 你可以在此基础上添加Module的测试代码。

Module入口文件指定在src/module.ts,Nuxt提供了多种Module编写方式,但官方推荐使用对象编写方法,并使用meta属性来标识你的模块。
import { defineNuxtModule } from '@nuxt/kit'export default defineNuxtModule({meta: {// 通常是你的模块的npm包名称name: '@nuxtjs/example',// `nuxt.config`中保存你的模块选项的键configKey: 'sample',// 兼容性约束compatibility: {// 支持的Nuxt版本的Semver版本nuxt: '^3.0.0'}},// 模块的默认配置选项,也可以是返回这些选项的函数defaults: {},// 注册Nuxt钩子的简写形式hooks: {},// 包含模块逻辑的函数,可以是异步的setup(moduleOptions, nuxt) {// ...}})
meta为Module的元数据配置信息,核心扩展逻辑包含在setup函数中。Nuxt的Module几乎可以覆盖Nuxt的方方面面功能,例如组件、Composables、插件、路由、中间件等。而Module中添加相关文件需要放到src/runtime目录下。
下图左侧为pinia实现的@nuxt/pinia模块,runtime目录下包含了composables和Vue相关的插件。图右侧为Vue3的水合插件 plugin.vue3.ts实现代码。

定义了插件和composables,但如何将其添加到Nuxt中?参考@nuxt/pinia实现的module.ts核心代码, 通过addPlugin函数将runtime下定义的插件动态注册到Nuxt,通过addImports将dfineStore、usePinia、storeToRefs函数自动导入,因此在组件中使用时不需要再手动import。
const module: NuxtModule<ModuleOptions> = defineNuxtModule<ModuleOptions>({...setup(options, nuxt) {// configure transpilationconst { resolve } = createResolver(import.meta.url)const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))// Transpile runtimenuxt.options.build.transpile.push(resolve(runtimeDir))nuxt.hook('prepare:types', ({ references }) => {references.push({ types: '@pinia/nuxt' })})nuxt.hook('modules:done', () => {if (isNuxtMajorVersion(2, nuxt)) {addPlugin(resolve(runtimeDir, 'plugin.vue2'))} else {addPlugin(resolve(runtimeDir, 'plugin.vue3'))addPlugin(resolve(runtimeDir, 'payload-plugin'))}})addImports([{ from: composables, name: 'defineStore' },{ from: composables, name: 'acceptHMRUpdate' },{ from: composables, name: 'usePinia' },{ from: composables, name: 'storeToRefs' },])},})export default module
Module几乎可以覆盖Nuxt所有功能,你可以为Nuxt添加其支持的任何资源:
-
Vue 组件 -
Composables -
Nuxt 插件
对于 服务器引擎 Nitro 来说:
-
API 路由 -
中间件 -
Nitro 插件
或者任何其他你想要注入到用户的 Nuxt 应用程序中的资源:
-
样式表 -
3D 模型 -
图片 -
等等
总结
「Nuxt贯彻了Vue生态的一贯作风,开箱即用,学习成本低,丰富的生态社区。」 和实现客户端SPA应用类似,Nuxt完全复用Vue相关的常用框架,路由使用Vue-Router,状态管理使用Pinia,UI组件可直接使用element-ui等,因此对于习惯了Vue的开发者来说,编写Nuxt完全没有心智负担。
「Nuxt的核心难点是如何保持服务端和客户端的同构性」。使用pinia创建的store,如果服务端对其进行了设置,那客户端使用时如何能够获取到设置后的值,而不是再重新初始化一次?使用useFetch、useAsyncFetch获取接口数据后,如何避免客户端重复请求?「Nuxt使用的方案都是通过key来标识一次请求,并将请求结果序列化到payload中,客户端在读取store、调用useFetch时,会先判断payload中是否存储有对应的值,有则直接使用。」
一个优秀的框架少不了好的扩展生态,Nuxt通过Module机制扩展其生态,目前已支持了250个Module。Module支持了插件、组件、路由等各个方面的扩展,并且在工程化方面也提供了nuxi、@nuxt/module-builder、@nuxt/kit、@nuxt/test-utils等工具。
参考:
-
Nuxt Modules官方文档 -
Nuxt3正式版发布,教你用vite+nuxt+pinia+vueuse搞定前端SSR项目 -
nitrohttps://juejin.cn/post/7073756223664816142 -
优化SPA:使得网站对SEO更友好
来源:






