nuxtjs

nuxtjs

Nuxt.js

Nuxt.js 是一个基于 Vue.js 的渐进式框架,用于构建现代 Web 应用程序。它简化了 Vue.js 应用的开发流程,特别适合构建:

  • 服务端渲染(SSR)应用 静态站点生成(SSG)应用 单页应用(SPA) 混合应用

详细介绍可以参考官网: nuxtjs 中文nuxtjs 英文(推荐) 确保已安装 Node.js (建议 v14+) 和 npm/yarn

nextjs的安装

npx create-nuxt-app my-project

What is your project named? my-app
Would you like to use Programming language(JavaScript/TypeScript)? 
Would you like to use Package manager (npm/yarn/pnpm) 
Would you like to use  CSS?  ui框架
...

{
  "scripts": {
    "dev": "nuxt", // 服务开发
    "build": "nuxt build", // 构建打包项目
    "start": "nuxt start", // 启动项目
    "generate": "nuxt generate" // 静态站点生成
  }
}

nextjs的APP路由目录结构

my-project
├── .nuxt
├── node_modules 
├── components 组件 
├── assets 
├── composables  定义全局函数方法 
├── layouts  定义 NuxtLayout  文件夹下的 .vue文件对应路由的layout
├── pages  定义页面路由 
├── middleware 定义全局中间件 
├── modules 定义全局的注册模块
├── pages 定义页面路由
│   ├── /page1 其他跳转的页面文件夹作为路由
│   │   ├── page2.vue 
│   │   └── page3.vue 
│   ├── /page2 
│   │   ├── page4.vue
│   │   └── [id].vue 路径为 /page2/[id] 的页面
│   ├── /(page2)   用 () 包裹表示这是某一个模块的路径,但是不读取括号内容作为路径一部分
│   │   ├── /@(id) 路径为 /page2/@(id) 的页面
│   │   └── /(..) photos\[id] 路径为 /page2/.. 的页面
│   ├── /dashboard 
│   │   ├── /[...slug]   表示 /dashboard/[...slug] 任意路由页面的动态路由,避免了dashboard路由下的404页面
│   │   └── /[id] 路径为 /page2/[id] 的页面
│   ├── layout.js
│   ├── app.config.ts
│   └── app.vue
├── server   和 nextjs 的api文件是一样的作为项目api接口
│   └── auth.ts
├── public
│   └── favicon.ico
├── plugins
│   └── nuxt.config.ts  
├── README.md
├── next.config.js
├── package.json
└── tsconfig.json

layouts 文件夹下定义的文件会作为路由的布局文件,例如 layout.js 会作为所有路由的布局文件,layout/default.js 会作为默认路由的布局文件,layout/blog.js 会作为 /blog 路由的布局文件。

<script setup lang="ts">
function enableCustomLayout () {
  setPageLayout('custom')
}
definePageMeta({
  layout: false,
});
</script>

<template>
  <div>
    <button @click="enableCustomLayout">Update layout</button>
  </div>
</template>

生命周期

Server

  • Nitro 启动:会初始化并执行该目录下的插件/server/plugins 执行一次
  • 初始化 Nitro:server/middleware/每个请求都会执行下面的中间件
  • 创建 Vue 和 Nuxt 实例
  • Route Validation:初始化插件之后、执行中间件之前
  • 执行Nuxt App中间件
  • 设置页面和组件: Nuxt 在此步骤中初始化页面及其组件,并使用useFetch和获取所有所需数据useAsyncData。Vue 生命周期钩子(例如onBeforeMount、onMounted和后续钩子)不会在 SSR 期间执行。
  • 渲染并生成 HTML 输出

lifecycle

Client

  • 初始化 Nuxt 并执行 Nuxt App 插件
  • Route Validation
  • 执行Nuxt App中间件
  • Mount Vue Application and Hydration
  • Vue 生命周期
// plugins/global-hooks.js
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('app:created', () => {
    console.log('Nuxt app has been created');
  });
  nuxtApp.hook('page:finish', () => {
    console.log('Page has finished rendering');
  });
  nuxtApp.hook('route:before', (to, from) => {
    console.log('Navigating to:', to.fullPath);
  });
});

hooks 详情

页面

<script lang="tsx" setup>
// Component could be a simple function with JSX syntax
const Welcome = () => <span>Welcome </span>

// Or using defineComponent setup that returns render function with JSX syntax
const Nuxt3 = defineComponent(() => {
  return () => <span class="text-(--ui-primary) font-bold">Nuxt 3</span>
})

// We can combine components with JSX syntax too
const InlineComponent = () => (
  <div>
    <Welcome />
    <span>to </span>
    <Nuxt3 />
  </div>
)
</script>

<template>
  <NuxtExample
    dir="advanced/jsx"
    icon="i-simple-icons-react"
  >
    <InlineComponent />
    <!-- Defined in components/jsx-component.ts -->
    <MyComponent message="This is an external JSX component" />
  </NuxtExample>
</template>

布局组件

<!-- layouts/default.vue -->
<template>
  <div>
    <header>网站头部</header>
    <Nuxt /> <!-- 页面内容会渲染在这里 -->
    <footer>网站底部</footer>
  </div>
</template>

渲染模式

主要分为两种渲染模式:服务端渲染(SSR)和客户端渲染(CSR)

// nuxt.config.js 全局禁用 SSR
export default {
  ssr: false, // 整个应用变为 CSR
}

<script>
export default {
  ssr: false, // 仅此页面不进行 SSR
}
</script>

// 动态SSR
<script  setup>
asyncData({ isServer }) {
  if (isServer) {
    // 服务端逻辑
  } else {
    // 客户端逻辑
  }
},
mounted() {
  if (process.client) {
    // 仅客户端执行的代码
  }
}
</script>

控制组件渲染

<template>
  <div>
    <ClientOnly>
      <ThirdPartyComponent /> <!-- 这个组件只在客户端渲染 -->
    </ClientOnly>
  </div>
</template>

<script setup>
import ThirdPartyComponent from '~/components/ThirdPartyComponent.vue'
</script>

// 或者通过状态判断
if (process.client) {
  // 客户端专用代码
}

路由方式

在项目中,app下面的每个文件夹就是路由的一个路径(用括号包裹作为模块的除外)

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Homepage pre-rendered at build time
    '/': { prerender: true },
    // Products page generated on demand, revalidates in background, cached until API response changes
    '/products': { swr: true },
    // Product pages generated on demand, revalidates in background, cached for 1 hour (3600 seconds)
    '/products/**': { swr: 3600 },
    // Blog posts page generated on demand, revalidates in background, cached on CDN for 1 hour (3600 seconds)
    '/blog': { isr: 3600 },
    // Blog post page generated on demand once until next deployment, cached on CDN
    '/blog/**': { isr: true },
    // Admin dashboard renders only on client-side
    '/admin/**': { ssr: false },
    // Add cors headers on API routes
    '/api/**': { cors: true },
    // Redirects legacy urls
    '/old-page': { redirect: '/new-page' }
  }
})

动态路由

在路由中,可以使用方括号app/dashboard/[...slug]来表示动态路由参数。例如,/dashboard/[slug]表示一个动态路由,其中slug是一个动态参数。当访问/dashboard/page/a时,slug的值为page/a。 也可以通过app/page/[id]方式定义动态路由,例如,/page/[id]表示一个动态路由,其中id是一个动态参数。当访问/page/123时,id的值为123

嵌套路由

在嵌套路由中,在父路由下的文件夹。这种情况下,父路由和子路由都可以独立地渲染和导航。例如商品列表和商品详情product/listproduct/detail中,路由直接互不影响(当然管理后台类型的除外)

数据请求

默认项目的数据请求适用fetch方法,但是也可以使用@nuxt/axios(和axios一样需要自行封装)等库进行数据请求。

// server/api/submit.js
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  return { body }
})

//页面使用
<script setup lang="ts">
async function submit() {
  const { body } = await $fetch('/api/submit', {
    method: 'post',
    body: { test: 123 }
  })
}
</script>

@nuxt/axios使用

// 安装 @nuxtjs/axios
npm install @nuxtjs/axios

// nuxt.config.js
export default {
  modules: [
    '@nuxtjs/axios'
  ],
  axios: {
    baseURL: 'https://api.example.com'
  }
}

<script>
export default {
  async asyncData({ params }) {
    const { data } = await axios.get(`/posts/${params.id}`)
    return { post: data }
  }
}
</script>

数据获取策略

数据缓存


Middleware

中间件是一种在请求和响应之间执行的函数,可以用于处理请求、响应、路由等。在 Next.js 中,可以使用中间件来处理路由、请求、响应等。 middleware.js 文件必须是在根目录下middleware文件夹,否则可能不响应

export default defineNuxtRouteMiddleware((to, from) => {
  if (to.params.id === '1') {
    return abortNavigation()
  }
  // In a real app you would probably not redirect every route to `/`
  // however it is important to check `to.path` before redirecting or you
  // might get an infinite redirect loop
  if (to.path !== '/') {
    return navigateTo('/')
  }
})

// Adding Middleware Dynamically
export default defineNuxtPlugin(() => {
  addRouteMiddleware('global-test', () => {
    console.log('this global middleware was added in a plugin and will be run on every route change')
  }, { global: true })

  addRouteMiddleware('named-test', () => {
    console.log('this named middleware was added in a plugin and would override any existing middleware of the same name')
  })
})

UI组件

nextjs内置了UI组件库,可以直接使用

  • NuxtImg
  • NuxtPage
  • Teleport
  • NuxtPicture
  • ClientOnly
  • NuxtLink
  • NuxtLayout

也可以使用官方推荐第三方UI组件库,element-plustemplatesui.nuxt

插件

// plugins/my-plugin.js
export default (context, inject) => {
  inject('myPlugin', {
    sayHello() {
      console.log('Hello from plugin!')
    }
  })
}

// nuxt.config.js
export default {
  plugins: [
    '~/plugins/my-plugin.js'
  ]
}

// 在页面中使用
this.$myPlugin.sayHello()

nuxt.config.ts

export default defineNuxtConfig({
  extends: [
    '@nuxt/examples-ui',
    './ui',
    './base',
  ],
  runtimeConfig: {
    public: {
      theme: {
        primaryColor: 'user_primary',
      },
    },
  },

  compatibilityDate: '2024-04-03',
})

详情查看

常用 API

useAppConfig

获取App的配置信息

const appConfig = useAppConfig()

console.log(appConfig)

useAsyncData

<script setup lang="ts">
const { data, status, error, refresh, clear } = await useAsyncData(
  'mountains',
  () => $fetch('https://api.nuxtjs.dev/mountains')
)
</script>

// 监听参数
<script setup lang="ts">
const page = ref(1)
const { data: posts } = await useAsyncData(
  'posts',
  () => $fetch('https://fakeApi.com/posts', {
    params: {
      page: page.value
    }
  }), {
    watch: [page]
  }
)

// 监听路由
<script setup lang="ts">
const route = useRoute()
const userId = computed(() => `user-${route.params.id}`)

// When the route changes and userId updates, the data will be automatically refetched
const { data: user } = useAsyncData(
  userId,
  () => fetchUserById(route.params.id)
)
</script>

</script>

useCookie

<script setup lang="ts">
const counter = useCookie('counter')

counter.value = counter.value || Math.round(Math.random() * 1000)
</script>

useFetch

<script setup lang="ts">
const { data, status, error, refresh, clear } = await useFetch('/api/modules', {
  pick: ['title']
})
</script>

const { data, status, error, refresh, clear } = await useFetch('/api/auth/login', {
  onRequest({ request, options }) {
    // Set the request headers
    // note that this relies on ofetch >= 1.4.0 - you may need to refresh your lockfile
    options.headers.set('Authorization', '...')
  },
  onRequestError({ request, options, error }) {
    // Handle the request errors
  },
  onResponse({ request, response, options }) {
    // Process the response data
    localStorage.setItem('token', response._data.token)
  },
  onResponseError({ request, response, options }) {
    // Handle the response errors
  }
})

<script setup lang="ts">
const route = useRoute()
const id = computed(() => route.params.id)

// When the route changes and id updates, the data will be automatically refetched
const { data: post } = await useFetch(() => `/api/posts/${id.value}`)
</script>

useHead

useHead(meta: MaybeComputedRef<MetaObject>): void

interface MetaObject {
  title?: string
  titleTemplate?: string | ((title?: string) => string)
  base?: Base
  link?: Link[]
  meta?: Meta[]
  style?: Style[]
  script?: Script[]
  noscript?: Noscript[]
  htmlAttrs?: HtmlAttributes
  bodyAttrs?: BodyAttributes
}

useHeadSafe

useHeadSafe({
  script: [
    { id: 'xss-script', innerHTML: 'alert("xss")' }
  ],
  meta: [
    { 'http-equiv': 'refresh', content: '0;javascript:alert(1)' }
  ]
})
// Will safely generate
// <script id="xss-script"></script>
// <meta content="0;javascript:alert(1)">

useLazyAsyncData

<script setup lang="ts">
/* Navigation will occur before fetching is complete.
  Handle 'pending' and 'error' states directly within your component's template
*/
const { status, data: count } = await useLazyAsyncData('count', () => $fetch('/api/count'))

watch(count, (newCount) => {
  // Because count might start out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

useLazyFetch

<script setup lang="ts">
/* Navigation will occur before fetching is complete.
 * Handle 'pending' and 'error' states directly within your component's template
 */
const { status, data: posts } = await useLazyFetch('/api/posts')
watch(posts, (newPosts) => {
  // Because posts might start out null, you won't have access
  // to its contents immediately, but you can watch it.
})
</script>

<template>
  <div v-if="status === 'pending'">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- do something -->
    </div>
  </div>
</template>

useNuxtApp

const nuxtApp = useNuxtApp()
nuxtApp.provide('hello', (name) => `Hello ${name}!`)

// Prints "Hello name!"
console.log(nuxtApp.$hello('name'))

useNuxtData

<script setup lang="ts">
// Access to the cached value of useFetch in posts.vue (parent route)
const { data: posts } = useNuxtData('posts')

const route = useRoute()

const { data } = useLazyFetch(`/api/posts/${route.params.id}`, {
  key: `post-${route.params.id}`,
  default() {
    // Find the individual post from the cache and set it as the default value.
    return posts.value.find(post => post.id === route.params.id)
  }
})
</script>

useRequestFetch

<script setup lang="ts">
// This will forward the user's headers to the `/api/cookies` event handler
// Result: { cookies: { foo: 'bar' } }
const requestFetch = useRequestFetch()
const { data: forwarded } = await useAsyncData(() => requestFetch('/api/cookies'))

// This will NOT forward anything
// Result: { cookies: {} }
const { data: notForwarded } = await useAsyncData(() => $fetch('/api/cookies')) 
</script>

useRouter

<script setup lang="ts">
const router = useRouter()
router.back()
</script>

useRoute

<script setup lang="ts">
const route = useRoute()
const { data: mountain } = await useFetch(`/api/mountains/${route.params.slug}`)
</script>

useSeoMeta

// app.vue
<script setup lang="ts">
useSeoMeta({
  title: 'My Amazing Site',
  ogTitle: 'My Amazing Site',
  description: 'This is my amazing site, let me tell you all about it.',
  ogDescription: 'This is my amazing site, let me tell you all about it.',
  ogImage: 'https://example.com/image.png',
  twitterCard: 'summary_large_image',
})
</script>

<script setup lang="ts">
const title = ref('My title')

useSeoMeta({
  title,
  description: () => `This is a description for the ${title.value} page`
})
</script>

<script setup lang="ts">
if (import.meta.server) {
  // These meta tags will only be added during server-side rendering
  useSeoMeta({
    robots: 'index, follow',
    description: 'Static description that does not need reactivity',
    ogImage: 'https://example.com/image.png',
    // other static meta tags...
  })
}

const dynamicTitle = ref('My title')
// Only use reactive meta tags outside the condition when necessary
useSeoMeta({
  title: () => dynamicTitle.value,
  ogTitle: () => dynamicTitle.value,
})
</script>

useServerSeoMeta

// app.vue
<script setup lang="ts">
useServerSeoMeta({
  robots: 'index, follow'
})
</script>

useState

// Create a reactive state and set default value
const count = useState('counter', () => Math.round(Math.random() * 100))

const state = useState('my-shallow-state', () => shallowRef({ deep: 'not reactive' }))
// isShallow(state) === true