之前用的基于 Vite 构建的 Vue3 项目已经是两年前搭建的了,迭代得越来越大,也遇到一些痛点。最近要起一个新的项目,Nuxt3 也已经发布了新版,就尝试用它作为新项目的构建系统,整体用下来体验还是比较不错的,这里总结一下它的基本的概念,方便查阅。后续使用过程中,随着不断的熟悉也会继续完善这篇文章。

基本命令

通过 npx nuxi init <project-name> 创建了一个项目后,比较常用的会有以下三个命令:

  nuxi dev
  nuxi build
  nuxi generate

dev 命令启动一个支持 HMR 的开发服务器,默认运行在 http://localhost:3000 端口。也可以加上 --https 参数来使用 https 协议,如果使用自签证书的话,需要设置环境变量 NODE_TLS_REJECT_UNAUTHORIZED=0

build 命令在本地创建一个 .output 目录保存构建到生产环境的产物,会将 process.env.NODE_ENV 设为 production

generate 命令会 预渲染 每个路由页面,查看产物会发现每个路由都有一个对应的 HTML 文件。实际上它等同于 nuxi build --prerender true

自动导入

自动导入是 Nuxt 一个很有用的特性。在以往的开发过程中,每个页面都要 import 我们用到的 Vue 的 Api ,还有我们写的组件等等,这样显得繁琐。而现在这些都不需要了,Nuxt 会在当前上下文里自动导入 Vue 的 API 如 ref, computed ,还有 Nuxt 提供的组件、插件以及自带的工具函数 如 useRuntimeConfig ,以及在特定的文件目录下定义的内容。此外,Nuxt 会在 .nuxt/imports.d.ts 下为这些自动导入的函数生成对应的类型声明,以免 IDE 会抛出错误。

所有这些自动导入的内容,Nuxt 也把它们放在一个 #imports 别名里,在需要的时候,我们可以像下面这样来使用:

  <script setup>
    import { ref, computed } from '#imports'
  </script>

composables 定义在该目录下的 Vue 组合函数会被自动导入到我们的应用上下文中。Nuxt 只会扫描该目录下的顶级文件而忽略嵌套目录的文件,如果连嵌套目录下的文件也一起导入,可以在 composables/index.ts 下把对应文件的模块内容重新导出。也可以在配置中修改扫描规则:

  export default defineNuxtConfig({
    imports: {
      dirs: [
        // 扫描顶级模块
        'composables',
        // ... 扫描指定文件
        'composables/*/index.{ts,js,mjs,mts}',
        // ... 扫描指定目录的所有模块
        'composables/**'
      ]
    }
  })

components 可以把所有自定义的组件放在该目录下,然后就能在任何页面或者其他组件中使用。嵌套目录下的组件,自动导入后默认会在组件名中带上它的父级目录的名字,如 components/foo/bar/Button.vue ,在使用的时候应该是 <FooBarButton /> 。最好在命名的时候直接命名为 FooBarButton.vue ,这样看上去更直观,而前面的 FooBar 部分也不会被重复添加。如果不想带上目录名,可以在配置中关掉。如果想把一个组件变成动态导入的,只要简单的在组件名前加上 Lazy 即可,<LazyFooBarButton />

utils 目录跟 composables 基于同样的扫描规则,定义在这个目录下的工具函数都会被自动导入。

layouts 目录下的排版组件会被自动导入,并且是异步加载的。可以直接在 app.vue 中通过 <NuxtLayout /> 来引用它们。我们可以有多种方法来使用这些组件:

如果我们整个应用只有一种布局,那么只需要定义一个 ~/layouts/default.vue 即可,然后在通过插槽的方式来使用它:

  <template>
    <NuxtLayout>
      Your content...
    </NuxtLayout>
  </template>

最常见的方式是,我们用它来包裹一个 <RouterView /> ,这样所有的页面都会默认使用这个布局,实际上 Nuxt 提供了一个自带组件 <NuxtPage /> ,它内置了 <RouterView /> ,让我们很方便将其和<NuxtLayout />结合起来。

  <template>
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </template>

如果要使用其它排版组件,也通过改变 <NuxtLayout />name 属性。

还有一种方式就是在组件中定义页面的元信息:

  <script>
    definePageMeta({
      layout: "custom-layout",
    });
  </script>

路由系统

基于文件系统的页面路由是 Nuxt 的另外一个关键特性。定义在 pages 目录下的文件会自动生成一条表示该页面的路由记录(Route) ,并且页面是动态加载的,这样可以自动进行代码分割,降低初次加载的资源体积。 文件系统路由基于命名约定的方式来创建动态和嵌套的路由:

      pages/
      --| about.vue
      --| posts/
      ----| [id].vue

像上面这样最终会生成两条路由记录:/about, /post/:id ,并且自动导入相应的组件。在进行路由导航的时间,我们可以使用内置的 <NuxtLink /> 来做跳转,它是优化了的 <RouterLink />

通过 definePageMeta 方法,我们可以在组件内定义路由的属性,如中间件,元信息等。此外,Nuxt 还提供了 validate 属性来允许我们对页面路由进行校验,它是一个函数,如果该函数返回 false ,就会抛出 404 错误。

插件系统

在常规的 Vue 项目中,我们一般会有一个 main.ts 文件,在这里创建 Vue 实例,注册第三方插件以及初始化一些全局数据等等。

在 Nuxt 中没有这个入口文件,我们可以通过更强大的插件系统来完成这些事情。使用 defineNuxtPlugin 可以让我们自定义一个 Nuxt 插件,plugins 目录下的文件以及子目录下的 index 文件会被自动扫描并注册成插件,并在 Vue 应用创建的时候进行加载,通过在文件名后加 .client 或者 .server 来让插件只在在客户端还是服务端使用,如果要决定插件的注册顺序,可以在文件名前面添加数字 1.plugin.ts 2.plugin.ts 这样来进行控制,后注册的插件可以使用前面注册的插件注入的数据。

插件工厂函数接受的参数是当前 Nuxt 实例,我们可以通过 nuxtApp.vueApp 拿到 Vue 实例从而进行第三方插件或者库的注册,下面是一个引入 ant-design-vue 的简单插件:

  import Antd from 'ant-design-vue'
  import 'ant-design-vue/dist/antd.css'

  export default defineNuxtPlugin((nuxt) => {
    nuxt.vueApp.use(Antd)
  })

在插件中返回一个包含 provide 属性的对象,我们可以在 Nuxt 实例中注入一些全局内容,从而在任意组件中通过 useNuxtApp 来使用:

  // page1.vue
  export default defineNuxtPlugin(() => {
    return {
      provide: {
        scrollToTop: () => {
          window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
        },
      },
    }
  })

  // page2.vue
  <script setup>
    const { scrollToTop } = useNuxtApp()
  </script>

此外,我们还可以在插件中使用自定义或者内置的组合函数。总之,Nuxt 的插件系统是一个非常强大且灵活的功能。

自定义 html 模板

有时候我们需要自定义页面 html 结构。 使用内置的 useHead 组合函数,可以让我们以编程式和响应式的方法去管理 head 标签,如 script, link, meta 等。

以上就是一个基本的 Nuxt3 项目的核心概念,了解这些之后我们就可以直接进行开发了。在 layouts 下定义好页面整体结构, components 下写组件,在 composables 下抽象出一些公共的业务逻辑,在 utils 下写一个常用的工具方法,就可以在 pages 下声明的页面里直接使用他们。