提起nuxt,玩过vue的同事们,应该都有一种似曾相识的感觉。
nuxt框架最初是为了解决前端SSR,SSG的需求而诞生的。由于能上SSR,SSG的项目实在是少,能施展nuxt大法的地方自然也十分有限。
但是流行度和一个框架的优秀程度是不能划等号的。
虽然单纯的SPA+CSR能解决很大一部分场景,但是SPA+CSR不是web的全貌。在高度这个内卷的时代,掌握一个全栈框架逐渐成为前端开发的必备技能。
Nuxt的闪亮特性
3.0版本后的nuxt已蜕变为全栈框架。不光可以玩SSR SSG,还可以配置成单纯CSR,也可以写后端接口,封装bff接口或是连接db,redis请求数据。
目前它包含如下优秀特性:
- 支持五种渲染模式,对混合渲染具有灵活的控制。
- 开发体验提升,文件路由,自动导入等功能都可以大大提升开发效率。
- 框架成熟,nuxt深耕vue和SSR领域多年,是vue SSR的最佳方案。
- 快速实现后端接口,前后端一体化开发。
- nuxt devtools可视化管理全部路由和组件。
初识nuxt
使用如下命令搭建一个nuxt项目:
npx nuxi@latest init <project-name>
这个命令因为要去github上拉点东西。考虑到可能因为某些不可抗拒的原因执行失败,附件里放了一个zip包,方便你上手。
初始化后的目录中核心的只有如下3个文件。
nuxt.config.ts
app.vue
package.json
package.json
咱就不多说了,地球人都知道了。nuxt.config.ts
是nuxt的配置文件,可以把很多相关系统都配置在这个文件中。比如vite, postcss等。app.vue
是入口的组件,初始内容是<NuxtWelcome />
这么一个欢迎页组件。
文件路由
vue实现多页面一般是通过vue router控制路由。vue router是vue的全家桶的标准组件之一,但是随着项目的增长,router会越来越复杂。维护起来既不美丽也不简单。
nuxt为我们提供了文件的方式维护路由。用过umi的开发,对这点不会陌生。概念是相同的。约定大于配置。增加一个文件就是增加一条route。nuxt构建系统会自动为我们生成vue router。
增加页面入口
我们把刚才的app.vue改为如下内容,开启文件路由:
<NuxtPage />
然后,nuxt会去读取pages文件夹中的文件树,生成路由。比如下面的文件树:
pages/
--| about.vue
--| index.vue
--| posts/
----| [id].vue
vue为我们建立了3条路由。/about /index /posts/:id,[id]
是特殊的文件名,转化为路由后就带了一个参数id。方括号也可以用在文件夹上。另外还有三个点的形式[...path]
,转化一条全匹配的路由,即vue router的/:path*
形式。
[!NOTE]
nuxt也支持嵌套路由,具体可以参考文档。
[!TIP]
vue router的beforEach是每个页面打开时候执行一些公共方法,一般做登录态、权限判断。
nuxt里实现类似逻辑的地方是在middleware文件夹中定义路由中间件。
统一页面布局
一个网站的多个页面一般都有固定的布局,比如header,footer, sidebar这些统一的元素。我们用CSR框架框架的时候一般都自己实现layout功能处理这些重复的元素。nuxt也为我们实现了这个功能,让我们告别反复实现layout的尴尬😅。
我们首先把app.vue改为如下内容:
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
NuxtLayout组件默认读取layouts
文件夹中的default.vue
作为布局组件,你也可以通过name
属性指定布局文件。
比如下面的default.vue
布局文件。
<template>
<header>Site Header</header>
<slot></slot>
<footer><footer>
</template>
我们通过引入一个NuxtLayout
组件为每个页面增加了header和footer元素。
自动导入
nuxt的自动导入是一个能大大节省开发时间的特性。nuxt会扫描一些文件夹比如components,utils识别我们用到的组件,这样我们在页面里面使用这些组件的时候不需要再import这些组件,nuxt会自动帮我们引入。vue的API也都是自动导入的。
扫描的文件夹可以在nuxt.config.ts
文件夹中配置,我们可以把stores文件夹也加上,这样能直接导入pinia store。
export default defineNuxtConfig({
imports: {
dirs: ['./stores'] // 自动扫描目录增加stores文件夹
},
});
[!NOTE]
感谢这个功能,治好了我多年的强迫症!代码里再也没有多余的import了 👏
服务器接口
SSR服务的相比于纯静态前端工程,一个很大的优势是服务端的进程存在。这样直接解锁了一大波服务端的骚操作。你可以在服务端管控一下权限,可以基于环境变量对不同环境做出不同响应,也可以做一些复杂的db, redis逻辑。nuxt的服务端并没有使用express, koa之类的node经典框架。而是另起炉灶,再造轮子,发明了一个叫nitro的服务端框架。
这个框架有什么优点?接下来我们探索一下。
在server/api
目录中建立一个文件hello.ts
export default defineEventHandler((event) => {
return {
hello: 'world'
}
})
这样就打开一个/api/hello
的json接口。
这里可以看到nitro的两个特点。
- 第一,这和页面开发方式又是如出一辙,也是文件路由。一个文件就是一个api接口。
- 第二,这个方法的参数很有意思的。没有request,response,而是Event。Event是一个来源于serverless的概念。仔细翻看nitro官网发现,nitro的确做了很多serverless平台的整合。从AWS Lambda,vercel到deno都有,能实现一个命令全球部署。
最后,咱放几个使用prisma ORM连接db的接口。供大家观赏下,真实的接口大概如此。
// get a single record
export default defineEventHandler((event) => {
const { name } = getQuery(event);
if (!name) throw 'name is required';
return prisma.project.findFirst({
where: {
name,
},
});
})
// create record
export default defineEventHandler((event) => {
const data = await readBody(event);
return prisma.project.create({
data,
});
})
网络请求
有了后台接口,我们接下来在前端试一试我们的接口,体验下急速前后端联调。
useFetch和useLazyFetch
首先,nuxt里做网络请求需要用nuxt特定的API。为什么不用前端常用的网络请求库axios呢?
因为在SSR下,axios请求逻辑会同时执行在服务端和客户端。这样你写一个axios.post
,服务端发一次,客户端发一次,成功给后台造成了双倍的压力😨。后台能不K你吗?SSR下正确的操作方式是这样的:首先服务端发出请求,请求的结果会被内联到html中。当客户端接手渲染之后,网络请求直接从html內联的数据中恢复,而不发出真正的网络请求。这是useFetch
接口处理的核心逻辑。另外,它还支持loading状态处理,手动refresh,错误处理封装等。
useFetch使用方式如下:
const { data, pending, error, refresh } = useFetch('/api/hello');
useFetch的返回值可以解构为4个部分。refresh是一个刷新请求的方法。其他三个是响应式ref对象。
看!从我们定义的http接口到前端请求拿到结果,就这样一行。不用自己封装axios了。
还有个类似的API叫useLazyFetch
,签名跟useFetch
是一致的,区别是:使用了useLazyFetch
后,页面需要处理data完全为null的情况。页面直接渲染,不等待请求结果。
原理分析:nuxt把异步网络数据序列化后,写入window.NUXT.data对象,并通过html下放给client,实现client侧网络数据恢复。
useAsyncData和useLazyAsyncData
除了useFetch
和useLazyFetch
,还有一套接口useAsyncData
和useLazyAsyncData
,它们的作用是从异步方法获取数据,并完成数据从后端到前端的恢复。它们相当于是useFetch
的底层,对于useAsyncData
,网络请求只是特殊的异步方法,而且它们的返回值是类似的。下面例子,我们用useAsyncData封装rpc接口。
const { data, pending, error, refresh } = useAsyncData(
'posts', () => rpc('getPosts')
)
状态管理
首先大部分的状态是不需要后端感知的,这些状态只存在于浏览器中,用户通过UI操作改变状态。这部分是大家熟知的,就不赘述了。
但是有的项目确实存在需要服务端感知的状态。这部分状态需要从服务端同步到客户端。
我们用一个简单的例子看下为什么要做前后端状态同步:
const num = ref(Math.random());
这个数字渲染到页面上会造成random函数在服务端执行一遍,然后前端重新执行一遍,因此水化的时候由于结构不同而造成抖动。
useState
nuxt为解决这个问题提供了useState方法,传入callback函数获取状态数据。
const num = useState(() => Math.random());
这个callback函数会在服务端执行,然后下发给客户端。客户端执行useState函数的时候,不执行callback,直接从服务端下发的数据中恢复。
useState返回值其实是个vue的ref类型。不要以为ref只能放值类型,复杂类型是照样可以的。
原理分析
nuxt把useState的状态序列化后,写入window.NUXT.state对象,并通过html下放给client,实现client侧状态的恢复。
pinia全局状态管理
vue的官方状态管理pinia也对这种数据下发机制也做了适配,推出了@pinia/nuxt
库。
nuxt开启了pinia之后,用法和之前基本一致。但是有个如下地方需要注意:你会发现state函数是在服务端执行的。客户端完全不执行state函数。
这是因为客户端使用server下发的state恢复状态。
翻看pinia适配nuxt的代码主要逻辑就如下这么几行。server端,状态赋值给nuxtApp.payload.pinia
,nuxt把nuxtApp.payload
序列化后通过html下发。
client端初始化的时候通过下发的数据恢复状态。
if (process.server) {
// 服务端下发状态
nuxtApp.payload.pinia = pinia.state.value
} else if (nuxtApp.payload && nuxtApp.payload.pinia) {
// 客户端恢复状态
pinia.state.value = nuxtApp.payload.pinia
}
pinia下发的数据保存在window.NUXT.pinia变量中。
渲染控制
nuxt除了支持SSR,SSG,CSR外还支持Hybrid rendering和ESR(Edge side rendering)。ESR需要云平台支撑,一般是vercel,deno等国外云平台较多。
我们下面着重讨论下混合渲染。对于这点nuxt提供了几种灵活的配置。
整体关闭SSR
nuxt虽然是一个SSR框架,但是不代表你需要SSR一路走到黑。
有些应用天生没必要做SSR,比如一些控制台类的应用。
这时依然可以使用nuxt。因为你可以一键关闭所有SSR。
如下nuxt.config.ts
文件整体关闭了SSR。
export default defineNuxtConfig({
ssr: false,
});
除了非黑即白的方式配置,你也可以部分开启SSR。这包含如下四种更精细的控制。
部分关闭SSR
把组件包裹在<ClientOnly>
组件中,可以使下面的组件树只在浏览器中渲染。
组件级控制
为组件增加client或是server后缀表示组件渲染的环境。
比如Counter.server.vue
和Counter.client.vue
。服务端渲染会用Counter.server.vue
产出html,而Counter.client.vue
组件会被打包给前端,水化后渲染。
路由级控制
还有一种通过路由控制SSR的方式,需要在nuxt.config.ts
中修改路由配置。
如下路由对于app开头的url都适用CSR渲染。
export default defineNuxtConfig({
nitro: {
routeRules: {
'/app/**': {
ssr: false,
},
},
},
});
语句级别控制
最后,还有一种精细度最高的语句级别的控制,帮助我们在前后端执行不同的逻辑,抹平前后端端的差异。
if (process.client) {
// run on client
} else if (process.server) {
// run on server
}
以上这些不同的SSR控制方式灵活搭配,可以用来解决有些脚本或组件在非浏览器环境运行报错,也能用来减少不必要的SSR,提升性能。
感悟
本文的nuxt就介绍到这里了。文中介绍涵盖了大部分nuxt精华内容。是不是感觉nuxt比单纯的SPA好太多了。
在我接触nuxt之前,常年SPA+CSR的我,基本可以用SPA打天下了。直到产品提了一个首页SEO需求。
“我们的app里需要加入一堆介绍性的页面”。继续SPA+CSR就不美丽了。也成为我们使用nuxt的根本原因。
除此之外,前端从单一的静态资源切换到nuxt服务端进程后,为我们的应用提供了另外一层控制力。从此前端不再单纯😈。
Nuxt的学习资料挺多的,首先是官网的文档是比较丰富的。然后官方的github里提供了两个小的项目的demo代码方便大家学习。
Nuxt movies
Nuxt hackernews