提起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

除了useFetchuseLazyFetch,还有一套接口useAsyncDatauseLazyAsyncData,它们的作用是从异步方法获取数据,并完成数据从后端到前端的恢复。它们相当于是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.vueCounter.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