
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 输出
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);
});
});
页面
<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/list和product/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-plus、templates、ui.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