阅读时长:15分钟
本文内容: 国内关于Nuxt3的资料太少了,而Nuxt3又发布了没有多久,导致资料太少。本文浓缩讲解了,对于一个前端开发,上手使用 Nuxt3,并一个人承担前后端开发的所有须知内容
环境要求:
export default defineEventHandler(async (event) => {
}
关于目录的解释
看一遍: nuxt3 目录pages
,所有写法与传统Vue3一模一样.server
, 重点讲解服务端一旦设置 Layout,那么文档结构被分为三部分: header,content,footer
例如::layout/page.vue
<template>
<div class="overflow-x-hidden">
<LayoutPageNavbar class="h-[64px] max-h-[64px]" />
<LayoutPageContent>
<slot />
LayoutPageContent>
<LayoutPageFooter class="h-[52px] md:h-[42px]" />
div>
template>
composables 通常放置全局方法,比如统一 api 接口。因为这个目录的方法会被自动导入
例如:composables/index.ts
import request from '@/utils/request'
/**
* get请求示例
* @param params
* @returns
*/
export function httpGetResponse(params: any) {
return request.get('/user-center/getUser', params)
}
/**
* post请求示例
* @param data
* @returns
*/
export function httpPostResponse(data: any) {
return request.post('/user-center/updateUser', data)
}
// 合成百度语音
export function getBaiduVoice(data: any): any {
return request.post('/speech/baidu', data, { responseType: 'blob' })
}
这个目录放置插件
例如: plugins/antDesignVue.ts
// 1. 引入组件
import Antd from 'ant-design-vue';
// 2. 引入组件样式
import 'ant-design-vue/dist/antd.css';
export default defineNuxtPlugin((nuxt) => {
nuxt.vueApp.use(Antd);
})
例如: plugins/axios.ts
import axios from 'axios'
export default defineNuxtPlugin((nuxtApp) => {
return {
provide: {
axios,
},
}
})
这个目录显然是存储数据的,使用的 pinia
例如: stores/use-example.ts
export interface ICounterState {
count: number
}
export const useExample = definePiniaStore('example', {
state: (): ICounterState => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
reset() {
this.count = 0
},
increment2x() {
this.count *= 2
},
},
})
目录 server
比较复杂,单独讲解
服务端 Server Api 设置(H3)
Nuxt3 的服务端使用 Nitro 构建,与 H3 z这个库紧密结合
# 安装
pnpm add h3
import { H3Event } from "h3"
export default defineEventHandler(async (event: H3Event) => {
setResponseStatus(event, 204)
return {
data: '',
code: 0,
msg: 'success'
}
}
const AUTH_COOKIE_NAME = '__session'
const AUTH_COOKIE_MAX_AGE = 60 * 60 * 24 * 5 * 1_000
export default defineEventHandler(async (event: H3Event) => {
const cookie = 'ffkeifpoeapoifm321654'
setCookie(event, AUTH_COOKIE_NAME, cookie, {
maxAge: AUTH_COOKIE_MAX_AGE,
secure: true,
httpOnly: true,
path: '/',
sameSite: 'lax',
})
return {
data: '',
code: 0,
msg: 'success'
}
}
interface Query {
region?: 'zh-CN' | 'en-US' | 'ja-JP' | 'en-AU' | 'en-UK' | 'de-DE' | 'en-NZ' | 'en-CA'
}
export default defineEventHandler(async (event) => {
const { type = 'img', region = 'zh-CN' } = getQuery<Query>(event)
if (type === 'img') {
const cache = await useStorage('cache').getItem('bing-wallpaper')
if (cache)
return await sendRedirect(event, cache, 302)
}
// https://github.com/zkeq/Bing-Wallpaper-Action
const data = await (await fetch(`https://raw.onmicrosoft.cn/Bing-Wallpaper-Action/main/data/${region}_all.json`)).json()
if (type === 'img') {
const url = `https://bing.com${data.data[0].url}`
useStorage('cache').setItem('cache:bing-wallpaper', url, { ttl: getTodayRemainMillisecond() })
return await sendRedirect(event, url, 302)
// event.node.res.setHeader('Content-Type', 'image/png;charset=utf-8')
// return Buffer.from(await (await fetch(url)).arrayBuffer())
}
else {
event.context.cache = { ttl: TimeUnitMap.hour }
return data
}
})
import { z, useValidatedBody } from 'h3-zod'
export default defineEventHandler(async event => {
const body = await useValidatedBody(
event,
z.object({
userId: z.string(),
})
)
})
export default defineEventHandler(async (event) => {
try {
const cookie = getCookie(event, 'refresh_token') as Token;
await deleteRefreshToken(cookie);
return {
cookie,
};
} catch (error) {
sendError(
event,
createError({
statusCode: 400,
statusMessage: 'username or password is invalid',
})
);
}
sendRefreshToken(event, '');
return {
message: 'Logout successful',
};
});
audio
类型数据服务端设置 header
// server/api/speech.ts
export default defineEventHandler(async (event: H3Event) => {
try {
// 获取音频数据
const audioBlob = await fetch_to_audio()
let size = 0;
if (audioBlob instanceof Blob) {
size = audioBlob.size
}
setResponseHeader(event, 'Accept-Ranges', 'bytes')
setResponseHeader(event, 'Content-Type', 'audio/wav')
setResponseHeader(event, 'Content-Length', size)
return audioBlob;
} catch (error) {
sendError(
event,
createError({
statusCode: 400,
statusMessage: 'username or password is invalid',
})
);
}
})
前端(客户端)设置数据返回类型为blob
<script lang="ts" setup>
const createAudio = async () => {
const response: any = await axios({
url: '/api/speech',
method: "POST",
data: {
content: '123456',
},
responseType: "blob"
})
const { data: arrayBuffer } = response
console.log('------收到服务端数据------', response)
const box = document.querySelector("#audio-box") as HTMLElement
const audioElement = document.createElement("audio"); //创建标签
audioElement.autoplay = true
audioElement.controls = true
audioElement.src = URL.createObjectURL(arrayBuffer); // 指定链接
box.appendChild(audioElement)
}
script>
<template>
<div>
<figure id="audio-box">
<figcaption>音频:figcaption>
figure>
div>
template>
image
图片类型数据服务端设置 header
// server/api/image.ts
export default defineEventHandler(async (event: H3Event) => {
try {
// 获取图片
const image = await fetch_to_image()
let size = 0;
if (audioBlob instanceof Blob) {
size = audioBlob.size
}
setHeader(e, 'Content-Type', 'image/png')
return image
} catch (error) {
sendError(
event,
createError({
statusCode: 400,
statusMessage: 'username or password is invalid',
})
);
}
})
File
图片类型数据以文件上传为例
前端页面: pages/login.vue
<script lang="ts" setup>
const $config = useRuntimeConfig()
const axios = useNuxtApp().$axios
const BaseUrl:string = "http://127.0.0.1:9000/api/v1/login"
const testServerApi = async () => {
// 图片: File格式
const imageFile = imageFiles.value as File
// 音频:Blob格式
const audioFileBlob = audioBlob.value as Blob
// 音频Blob转file
const newAudioFileBlob = new Blob([audioFileBlob], { type: 'audio/wav' })
const audioFile = new File([newAudioFileBlob], new Date().getTime() + '.wav')
if (!imageFile) {
return
}
// 创建formData
const formData = new FormData()
formData.append('image', imageFile)
formData.append('audio', audioFile)
// 方式一:使用 useFetch() 推荐 (文末有完整封装)
const res: any = await useFetch($config.BaseUrl, formData)
// 方式二:使用 axios
// const response = await axios({
// method: 'POST',
// url: 'http://127.0.0.1:9000/api/v1/login',
// data: formData,
// headers,
// })
console.log(res)
}
script>
<template>
<div>
<div @click="testServerApi">点击div>
div>
template>
服务端接口: server/api/user/login.ts
import { H3Event, MultiPartData } from 'h3'
import { FormData } from 'node-fetch-native'
import { AjaxResult } from '../../../types'
export default defineEventHandler(
async (event: H3Event): Promise<AjaxResult> => {
const response = await send_image_audio_to_remote(event)
return {
data: response,
code: 0,
msg: 'success',
}
}
)
const send_image_audio_to_remote = async (event: H3Event) => {
// 重点!!!前端传输的文件使用此方法获取
const form = await readMultipartFormData(event)
try {
if (form) {
const imageMultiPartData: MultiPartData = form[0]
const audioMultiPartData: MultiPartData = form[1]
// 服务端,也可以继续发起请求,比如百度云的语音服务
const formData = new FormData()
formData.append('image', new Blob([imageMultiPartData.data]))
formData.append('audio', new Blob([audioMultiPartData.data]))
const responseData: any = await create_access_token()
console.log('=========responseData==============')
console.log(responseData)
// 返回
return true
}
return ''
} catch (error) {
sendError(
event,
createError({
statusCode: 400,
statusMessage: 'server is invalid',
})
)
}
}
/**
* 百度云语音服务
*/
const create_access_token = async () => {
const { awesome } = useAppConfig()
const client_id = awesome.baiduTTS.client_id
const client_secret = awesome.baiduTTS.client_secret
const url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}`
const option = {
grant_type: 'client_credentials',
client_id,
client_secret,
}
const headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
}
const res: BaiduTTSAccessToken = await $fetch(url, {
method: 'POST',
headers,
body: option,
})
if (res.error_code) {
return res.error_code + ''
}
return res.access_token
}
export interface AjaxResult {
data?: any
code: number
msg: string
}
import { createPool, sql } from '@vercel/postgres'
async function seed() {
const createTable = await sql`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
image VARCHAR(255),
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
`
console.log(`Created "users" table`)
const users = await Promise.all([
sql`
INSERT INTO users (name, email, image)
VALUES ('Guillermo Rauch', '[email protected]', 'https://pbs.twimg.com/profile_images/1576257734810312704/ucxb4lHy_400x400.jpg')
ON CONFLICT (email) DO NOTHING;
`,
sql`
INSERT INTO users (name, email, image)
VALUES ('Lee Robinson', '[email protected]', 'https://pbs.twimg.com/profile_images/1587647097670467584/adWRdqQ6_400x400.jpg')
ON CONFLICT (email) DO NOTHING;
`,
sql`
INSERT INTO users (name, email, image)
VALUES ('Steven Tey', '[email protected]', 'https://pbs.twimg.com/profile_images/1506792347840888834/dS-r50Je_400x400.jpg')
ON CONFLICT (email) DO NOTHING;
`,
])
console.log(`Seeded ${users.length} users`)
return {
createTable,
users,
}
}
export default defineEventHandler(async () => {
const startTime = Date.now()
const db = createPool()
try {
const { rows: users } = await db.query('SELECT * FROM users')
const duration = Date.now() - startTime
return {
users: users,
duration: duration,
}
} catch (error) {
// @ts-ignore
if (error?.message === `relation "users" does not exist`) {
console.log(
'Table does not exist, creating and seeding it with dummy data now...'
)
// Table is not created yet
await seed()
const { rows: users } = await db.query('SELECT * FROM users')
const duration = Date.now() - startTime
return {
users: users,
duration: duration,
}
} else {
throw error
}
}
})
# Using pnpm
pnpm add -D @ant-design-vue/nuxt
nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@ant-design-vue/nuxt'
],
antd:{
// Options
}
})
使用
<script lang="ts" setup>
const handleMessage = () => {
message.info("This is a normal message");
}
script>
<template>
<a-button @click="handleMessage">
button
a-button>
template>
# Using pnpm
pnpm add -D ant-design-vue
在plugins文件中新建
awesome.ts
// 1. 引入组件
import Antd from 'ant-design-vue';
// 2. 引入组件样式
import 'ant-design-vue/dist/antd.css';
export default defineNuxtPlugin((nuxt) => {
nuxt.vueApp.use(Antd);
})
使用
<script lang="ts" setup>
const handleMessage = () => {
message.info("This is a normal message");
}
script>
<template>
<a-button @click="handleMessage">
button
a-button>
template>
Nuxt的服务端通常只作为转发/代理层,因此数据存储通常也使用轻量的 Redis 存储
server/plugins/storage.ts
import redisDriver from 'unstorage/drivers/redis'
export default defineNitroPlugin(() => {
const storage = useStorage()
// Dynamically pass in credentials from runtime configuration, or other sources
const driver = redisDriver({
base: 'redis',
host: useRuntimeConfig().redis.host,
port: useRuntimeConfig().redis.port,
/* other redis connector options */
})
// Mount driver
storage.mount('redis', driver)
})
nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
redis: { // Default values
host: '',
port: 0,
/* other redis connector options */
}
}
})
nitro.storage
nuxt.config.ts
export default defineNuxtConfig({
nitro: {
storage: {
'redis': {
driver: 'redis',
/* redis connector options */
port: 6379, // Redis port
host: "127.0.0.1", // Redis host
username: "", // needs Redis >= 6
password: "",
db: 0, // Defaults to 0
tls: {} // tls/ssl
}
}
}
})
import { NuxtAuthHandler } from "#auth";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
const nuxtAuthHandler = NuxtAuthHandler({
secret: useRuntimeConfig().authSecret,
providers: [
// @ts-ignore
GithubProvider.default({
clientId: useRuntimeConfig().githubClientId,
clientSecret: useRuntimeConfig().githubClientSecret,
httpOptions: {
timeout: 10000,
},
}),
// @ts-ignore
GoogleProvider.default({
clientId: useRuntimeConfig().googleClientId,
clientSecret: useRuntimeConfig().googleClientSecret,
}),
],
});
export default defineEventHandler(async (event) => {
const result = await nuxtAuthHandler(event);
const headerCookies = event.node.res.getHeader("set-cookie");
if (headerCookies && typeof headerCookies === "string") {
const sessionToken = headerCookies
.split(/,(?!\s)/)
.find((v) => v.includes("session-token"));
if (sessionToken) {
event.node.res.removeHeader("set-cookie");
event.node.res.setHeader("set-cookie", sessionToken);
}
}
return result;
});
import { UseFetchOptions } from 'nuxt/app'
import { AjaxResult, Methods } from '../types'
/*
* @Date: 2023-08-18 00:16:29
* @LastEditors: ifredom [email protected]
* @LastEditTime: 2023-08-29 13:46:46
* @FilePath: \first-nuxt\utils\request.ts
*/
class HttpService {
request<T = any>(
url: string,
method: Methods,
data: any,
options?: UseFetchOptions<T>
) {
const $config = useRuntimeConfig()
const requestUrl =
url.includes('https') || url.includes('http')
? url
: $config.app.BASE_URL + url
const newOptions: UseFetchOptions<T> = {
baseURL: requestUrl,
method,
...options,
}
if (method === 'GET' || method === 'DELETE') {
newOptions.params = data
}
if (method === 'POST' || method === 'PUT') {
newOptions.body = data
}
return new Promise((resolve, reject) => {
useFetch(requestUrl, newOptions)
.then((response) => {
const { data, error } = response
if (error && error.value) {
console.log('toast 网络错误')
reject(error.value)
return
}
if (newOptions.responseType === 'blob') {
resolve(response.data.value)
return
}
const res = data.value as AjaxResult
if (res && res.code === 0) {
resolve(res.data)
} else if (res && res.msg) {
if (res.code === 40000) {
// 会话过期跳登录
setTimeout(() => {
navigateTo('/login')
}, 1000)
return
}
reject(res.data)
}
resolve(res)
})
.catch((error) => {
reject(error)
})
})
}
get<T = any>(url: string, params?: any, options?: UseFetchOptions<T>) {
return this.request(url, 'GET', params, options)
}
post<T = any>(url: string, data?: any, options?: UseFetchOptions<T>) {
return this.request(url, 'POST', data, options)
}
put<T = any>(url: string, data: any, options?: UseFetchOptions<T>) {
return this.request(url, 'PUT', data, options)
}
delete<T = any>(url: string, params: any, options?: UseFetchOptions<T>) {
return this.request(url, 'DELETE', params, options)
}
}
const httpRequest = new HttpService()
export default httpRequest
types.ts类型文件
export type Methods = "GET" | "POST" | "DELETE" | "PUT";
export interface AjaxResult {
data?: any
code: number
msg: string
}
使用示例:
/**
* get请求示例
* @param params
* @returns
*/
export function httpGetResponse(params: any) {
return request.get('/user-center/getUser', params)
}
/**
* post请求示例
* @param data
* @returns
*/
export function httpPostResponse(data: any) {
return request.post('/user-center/updateUser', data)
}
/**
* post请求: 携带请求头
* @param data
* @returns
*/
export function httpPostWithHeader(data: any): any {
return request.post('/user-center/update_Avator', data, { responseType: 'blob' })
}
参考 API
参考例子
------ 如果文章对你有用,感谢右上角 >>>点赞 | 收藏 <<<