nuxt3 搭建基础模板

搭建基础模板文档

nuxt3搭建基础模板 使用 eslint pinia navive-ui unocss以及 后端全栈基础模板的搭建

代码规范

安装需要的依赖

sql 复制代码
pnpm add -D eslint @antfu/eslint-config typescript

配置 eslint.config.mjs

根目录新建文件eslint.config.mjs,配置如下:

js 复制代码
import antfu from '@antfu/eslint-config'

export default antfu(
  {
    type: 'app', // 项目类型为 'app',适用于应用程序项目 可选lib

    stylistic: {
      indent: 2, // 缩进风格
      quotes: 'single', // 单引号
    },

    typescript: true,
    vue: {
      overrides: {
        // enforce order of component top-level elements 自定义 Vue 文件中标签的顺序,模板 -> 脚本 -> 样式
        'vue/block-order': ['error', {
          order: ['template', 'script', 'style'],
        }],
      },
    },
    jsonc: false,
    yaml: false,
    ignores: [
      '**/fixtures', // 忽略特定路径下的文件(如 fixtures 目录)
    ],
  },

  // 第二部分,应用于所有 TypeScript 文件的规则配置
  {
    files: ['**/*.ts'], // 仅匹配 TypeScript 文件
    rules: {},
  },

  // 第三部分,额外的规则配置
  {
    rules: {},
  },
)

添加脚本

json 复制代码
{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

自动格式化

我们 使用 eslintrc 进行代码格式化

配置 VS Code 可以实现自动格式化代码:

json 复制代码
{
  // Disable the default formatter, use eslint instead
  "prettier.enable": false,
  "editor.formatOnSave": true,

  // Auto fix
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "never"
  },

  // Silent the stylistic rules in you IDE, but still auto fix them
  "eslint.rules.customizations": [
    { "rule": "style/*", "severity": "off", "fixable": true },
    { "rule": "format/*", "severity": "off", "fixable": true },
    { "rule": "*-indent", "severity": "off", "fixable": true },
    { "rule": "*-spacing", "severity": "off", "fixable": true },
    { "rule": "*-spaces", "severity": "off", "fixable": true },
    { "rule": "*-order", "severity": "off", "fixable": true },
    { "rule": "*-dangle", "severity": "off", "fixable": true },
    { "rule": "*-newline", "severity": "off", "fixable": true },
    { "rule": "*quotes", "severity": "off", "fixable": true },
    { "rule": "*semi", "severity": "off", "fixable": true }
  ],

  // Enable eslint for all supported languages
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "",
    "html",
    "markdown",
    "json",
    "jsonc",
    "yaml",
    "toml",
    "xml",
    "gql",
    "graphql",
    "astro",
    "css",
    "less",
    "scss",
    "pcss",
    "postcss"
  ]
}

整和unocss

安装 sass

复制代码
pnpm install sass -D

安装 unocss

sql 复制代码
pnpm add -D unocss @unocss/nuxt

// 配置 unocss 和 scss 进行搭配使用的依赖插件
pnpm add @unocss/transformer-directives 
tsx 复制代码
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@unocss/nuxt',
  ],
  unocss: {
    // unocss 配置
    uno: true, // 启用默认预设
    icons: true, // 启用图标
    attributify: true, // 启用属性化模式
    shortcuts: {
      // 自定义快捷方式
      'btn': 'px-4 py-2 rounded inline-block bg-teal-600 text-white cursor-pointer hover:bg-teal-700',
    },
    rules: [
      // 自定义规则
    ],
  }
})

创建 uno.config.ts

ts 复制代码
// uno.config.ts
import transformerDirective from '@unocss/transformer-directives'
import { defineConfig, presetAttributify, presetIcons, presetWind3 } from 'unocss'

export default defineConfig({
  presets: [
    // presetWind3(),({
    //  preflight: false, // 禁用预设样式 例如又使用了 tailwindcss 冲突的时候 可以这样去禁用掉默认的样式
    // }),
    presetWind3(),
    presetAttributify(),
    presetIcons(),
  ],
  transformers: [
    transformerDirective(), // 支持 @apply 指令
  ],
  shortcuts: {
    // 自定义快捷方式
  },
  theme: {
    // 主题配置
    colors: {
      // 自定义颜色
    }
  }
})

样式重置

csharp 复制代码
pnpm add normalize.css
ts 复制代码
// nuxt.config.ts

export default defineNuxtConfig({
  css: ['normalize.css'], // 全局引入normalize.css
})

配置 图标 组件

完成以上配置以后 我们可以创建一个 图标组件封装进行管理和使用

sql 复制代码
pnpm add -D @nuxt/icon
tsx 复制代码
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nuxt/icon', 
  ],
})

到此 我们的 图标依赖就安装好了 但是 这样每次我们取的图标 都是网络上请求的 会闪一下 所以还是建议下载到本地

sql 复制代码
pnpm add -D @iconify-json/ant-design

components/SvgIcon/index.vue

vue 复制代码
<template>
  <Icon v-if="!isExt" :class="svgClass" :name="iconName" />
  <template v-else>
    <!-- v-bind="$attrs" 这样当 进入到 这里的时候 不会丢失事件 跨组件传递 -->
    <div :style="styleExternalIcon" :class="svgClass" bg-current v-bind="$attrs" />
  </template>
</template>

<script setup lang="ts">
import { isExternal } from '@/utils/validate'
// import { Icon as IconifyIcon } from '@iconify/vue'

// vue3.5 是可以支持解构 保持响应式的 但是 加了 withDefaults 就会失去响应式 所以这样不能直接解构
const props = withDefaults(
  defineProps<{
    iconName: string
    customClass: string
  }>(),
  {
    customClass: '',
  },
)

// 判断是否 是外链
const isExt = computed(() => isExternal(props.iconName))

// 合成 类名将定义好的初始化 icon 进行整合传递进行来的类名
const svgClass = computed(() => (props.customClass ? `icon ${props.customClass}` : 'icon'))

// 使用 mask 渲染 svg 图标 兼容性不是很好
const styleExternalIcon = computed(() => ({
  'mask': `url(${props.iconName}) no-repeat 50% 50%`,
  '-webkit-mask': `url(${props.iconName}) no-repeat 50% 50%`,
  'mask-size': 'cover',
}))
</script>

<style scoped></style>

工具函数 判断是否是外链

untils/validate.ts

typescript 复制代码
// 判断 是否传入的是外链
export const isExternal = (path: string): boolean => {
  return /https?/.test(path)
}

配置组件库:Naive UI

安装相关依赖

bash 复制代码
pnpm install @bg-dev/nuxt-naiveui
ts 复制代码
// .npmrc
# Option 1 (recommended)
public-hoist-pattern[]=@css-render/vue3-ssr
public-hoist-pattern[]=vueuc
public-hoist-pattern[]=naive-ui
tsx 复制代码
// nuxt.config.ts
export default defineNuxtConfig({
  //
  modules: ["@bg-dev/nuxt-naiveui"],
  //
});

全局配置: <naive-config> 组件是 Naive UI 提供的一种方式,让开发者能够设置一些全局配置。比如,可以在该组件内配置全局的 Naive UI 主题,或者修改 Naive UI 组件的样式等。

NaiveConfig``app.vue``error.vue 中设置为根组件 这个就是可以设置全局的 主题颜色的 可选配置

vue 复制代码
<template>
  <naive-config>
    <!-- start here -->
  </naive-config>
</template>

配置加载动画 可选配置

tsx 复制代码
// nuxt.config.ts
export default defineNuxtConfig({
  // 其他配置项...
  naiveui: {
    spaLoadingTemplate: {
      name: "bar-scale", // 使用条形缩放加载动画
    },
  },
});

集成 pinia 全局

  1. 安装pinia
js 复制代码
pnpm install pinia @pinia/nuxt
  1. 配置nuxt.config.ts
js 复制代码
modules: [
  '@pinia/nuxt',
],
  1. 持久化配置
  • 安装
js 复制代码
pnpm i -D @pinia-plugin-persistedstate/nuxt
js 复制代码
modules: [
  '@pinia-plugin-persistedstate/nuxt',
],

// 默认存在cookies
// persist: {
//  storage: persistedState.localStorage,
// },
tsx 复制代码
export const useCountStore = defineStore('counter', () => {
  const count = ref(0)

  const addCount = () => {
    count.value++
  }

  return {
    count,
    addCount,
  }
}, {
  persist: true,
})

server 基础搭建

三层架构

/api 页面层

/service 业务逻辑层

/dao 数据处理层

utils/validator 参数校验

安装 Prisma 和数据库

  1. 安装依赖
sql 复制代码
pnpm install @prisma/client
pnpm install --save-dev prisma
  1. 初始化 Prisma
csharp 复制代码
// 我们必须进入到 server 文件目录下进行初始化

npx prisma init

会创建.env文件与prisma文件夹

  • .env 用于定义数据库连接
  • prisma用于定义模型结构与数据迁移与数据填充文件

修改.env文件设置mysql连接,以下连接请根据你的情况修改

ini 复制代码
DATABASE_URL="mysql://root:[email protected]:3306/nest"
  1. 配置 schema.prisma
json 复制代码
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model user {
    //BigInt类型  主键 自增值  非负BitInt
  id       BigInt    @id @default(autoincrement()) @db.UnsignedBigInt()
  //字符串,默认为varchar(191)
  email    String
  password String
  //添加时自动设置时间,即设置Mysql默认值为CURRENT_TIMESTAMP
  createdAt DateTime @default(now())
  // 让Prisma在添加与更新时自动维护该字段
  updatedAt DateTime @updatedAt
}

model category {
  id       Int       @id @default(autoincrement()) @db.UnsignedInt()
  title    String
  articles article[]
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt
}

model article {
  id         Int      @id @default(autoincrement()) @db.UnsignedInt()
  title      String
  content    String   @db.Text()
  //类型category  关联定义(本表字段catgoryId,关联category表id,主表记录删除时同时删除关联表数据)
  category   category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
  categoryId Int      @db.UnsignedInt()
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt
}

执行以下命令,将自动根据已经存在的数据库生成文件 prisman/schema.prisma ,而不需要向上面一样手动定义。

ebnf 复制代码
npx prisma db pull 

生成迁移

当创建好结构定义后,执行以下命令会在prisma/migrations 目录生成迁移文件,同时在数据库中创建表。

  • 这时数据表也已经创建了
  • 数据库中会有表 _prisma_migrations 记录了迁移文件
ebnf 复制代码
npx prisma migrate dev

以下命令执行动作为:

  • 根据定义生成迁移文件
  • 执行新的迁移文件修改数据表
  • 生成 Prisma Client

重置数据库

我们也可以执行以下命令重置数据库

ebnf 复制代码
npx prisma migrate reset

// pnpm prisma migrate reset --schema=.//server/prisma/schema.prisma 我们在根目录下 进行数据库重置

以下命令执行动作为:

  • 删除数据库
  • 创建数据库
  • 执行所有迁移文件
  • 运行 seed 数据填充

常用命令

创建迁移文件

coffeescript 复制代码
pnpm prisma migrate dev

重置迁移并执行数据,同时会执行数据填充

coffeescript 复制代码
pnpm prisma migrate reset

客户端prisma

/server/dao/prisma/client.ts

tsx 复制代码
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()
export default prisma

三层接口例子

注册接口

页面路由层 server/api/auth/register.post.ts

ts 复制代码
import { registerService } from '~/server/service/auth/registerService'

export interface RegisterBody {
  email: string
  password: string
}

export default defineEventHandler(async (event) => {
  try {
    const body = await readBody<RegisterBody>(event)
    const result = await registerService(body)
    return result
  }
  catch (error) {
    console.error('注册接口发生错误:', error)
    return { code: 1, message: '服务器内部错误,请稍后重试', data: {} }
  }
})

业务逻辑层 server/service/auth/registerService.ts

tsx 复制代码
/**
 * 1、获取传递的数据
 * 2、数据校验
 * 3、密码加密
 * 4、判断账号是否注册
 * 5、创建账号
 */

import type { RegisterBody } from '~/server/api/auth/register.post'
import { createUser, getUserByEmail } from '~/server/dao/registerDao'
import { formatResponse, hashPassword } from '~/server/utils/helper'
import { validateUserInput } from '~/server/utils/validator/registerValidator'

export async function registerService(body: RegisterBody) {
  try {
    // 2. 参数校验
    const validationError = await validateUserInput(body)
    if (validationError) {
      return formatResponse(1, validationError, {})
    }

    // 3. 检查用户是否已注册
    const existingUser = await getUserByEmail(body.email)
    if (existingUser) {
      return formatResponse(1, '该邮箱已被注册', {})
    }

    // 4. 加密密码
    body.password = await hashPassword(body.password)

    // 5. 创建用户
    const newUser = await createUser(body)

    return formatResponse(0, '注册成功', newUser)
  }
  catch (error) {
    console.error('注册服务发生错误:', error)
    return formatResponse(1, '服务器内部错误,请稍后重试', {})
  }
}

数据操作层 /server/dao/registerDao.ts

tsx 复制代码
import type { user } from '@prisma/client'
import type { RegisterBody } from '../api/auth/register.post'
import prisma from './prisma/client'

// 查询是否存在该 邮箱
export async function getUserByEmail(email: string): Promise<user | null> {
  const result = await prisma.user.findFirst({
    where: {
      email,
    },
  })

  return result
}

// 注册用户
export async function createUser(data: RegisterBody) {
  const user = await prisma.user.create({ data })
  return user
}
csharp 复制代码
// 安装校验数据库

pnpm add joi

数据校验 /utils/validator/registerValidator.ts

tsx 复制代码
import type { RegisterBody } from '~/server/api/auth/register.post'
import Joi from 'joi'

// 校验用户输入
export async function validateUserInput(body: RegisterBody) {
  const schema = Joi.object({
    nickname: Joi.string().required().messages({ 'any.required': '昵称是必填项' }),
    password: Joi.string().min(6).max(22).required().messages({
      'string.min': '密码至少需要 6 位字符',
      'string.max': '密码最多 22 位字符',
      'any.required': '密码是必填项',
    }),
    email: Joi.string().email().required().messages({
      'string.email': '邮箱格式不正确',
      'any.required': '邮箱是必填项',
    }),
  })

  try {
    await schema.validateAsync(body, { abortEarly: false })
    return null
  }
  catch (error) {
    if (error instanceof Joi.ValidationError) {
      return error.details.map(err => err.message).join(', ') || '参数错误'
    }
    return '服务器错误'
  }
}

密码加密 与 格式化数据

bash 复制代码
 pnpm install bcryptjs
 
 pnpm install @types/bcryptjs -D
tsx 复制代码
import bcrypt from 'bcryptjs'

// 格式化数据
export function formatResponse(code: number, msg: string, data: any) {
  return {
    code,
    msg,
    data: JSON.parse(JSON.stringify(data, (_, value) =>
      typeof value === 'bigint' ? value.toString() : value)),
  }
}

// 密码加密
export async function hashPassword(password: string): Promise<string> {
  const salt = await bcrypt.genSalt(10)
  return bcrypt.hash(password, salt)
}

解决BigInt报错

在格式化代码上进行转换即可

tsx 复制代码
export function formatResponse(code: number, msg: string, data: any) {
  return {
    code,
    msg,
    data: JSON.parse(JSON.stringify(data, (_, value) =>
      typeof value === 'bigint' ? value.toString() : value)),
  }
}

jwt校验

1、生成token

sql 复制代码
pnpm add @types/jsonwebtoken -D

pnpm add jsonwebtoken

~/utils/helper/index.ts

tsx 复制代码
// 生成 token
export function generateToken(payload: any, loginInfo = 1) {
  const secret = process.env.JSON_SECRET || 'default_secret' // 提供默认值
  // Bearer 是约定熟成 的一个前缀
  // payload 是传入函数内容 一般为用户的一些信息  密钥  加密方式 以及 token时效性
  return (
    `Bearer ${
      jwt.sign(payload, secret, {
        algorithm: 'HS512',
        expiresIn: 60 * 60 * 24 * loginInfo,
      })}`
  )
}
tsx 复制代码
/**
 * 1、获取传递的数据
 * 2、数据校验
 * 3、密码加密
 * 4、判断账号是否注册
 * 5、创建账号
 */

import type { RegisterBody } from '~/server/api/auth/register.post'
import { createUser, getUserByEmail } from '~/server/dao/registerDao'
import { formatResponse, generateToken, hashPassword } from '~/server/utils/helper'
import { validateUserInput } from '~/server/utils/validator/registerValidator'

export async function registerService(body: RegisterBody) {
  try {
    // 2. 参数校验
    const validationError = await validateUserInput(body)
    if (validationError) {
      return formatResponse(1, validationError, {})
    }

    // 3. 检查用户是否已注册
    const existingUser = await getUserByEmail(body.email)
    if (existingUser) {
      return formatResponse(1, '该邮箱已被注册', {})
    }

    // 4. 加密密码
    body.password = await hashPassword(body.password)

    // 5. 创建用户
    const newUser = await createUser(body)
    // 6、生成token
    const token = generateToken({ email: newUser.email, password: newUser.nickname })
    const data = { ...newUser, token }

    return formatResponse(0, '注册成功', data)
  }
  catch (error) {
    console.error('注册服务发生错误:', error)
    return formatResponse(1, '服务器内部错误,请稍后重试', {})
  }
}

2、中间件校验token

middleware/auth.ts

tsx 复制代码
import type { EventHandlerRequest, H3Event } from 'h3'
import { createError, getHeader } from 'h3'
import jwt from 'jsonwebtoken'

// 配置需要拦截的路径前缀
const protectedRoutes = ['/api/user']

const secret = process.env.JSON_SECRET || 'default_secret' // 确保有默认值

export default defineEventHandler(async (event: H3Event<EventHandlerRequest>) => {
  const url = event.node.req.url || ''

  // **仅拦截匹配 `protectedRoutes` 数组中任意前缀的请求**
  if (!protectedRoutes.some(prefix => url.startsWith(prefix))) {
    return
  }

  try {
    // 获取 Authorization 头部信息
    const authHeader = getHeader(event, 'authorization')
    if (!authHeader) {
      throw createError({ statusCode: 401, message: '未提供 Token' })
    }

    // 解析 Token
    const token = authHeader.split(' ')[1]
    if (!token) {
      throw createError({ statusCode: 401, message: 'Token 格式错误' })
    }

    // 验证 Token
    const decoded = jwt.verify(token, secret, { algorithms: ['HS512'] })

    // 将解析出的用户信息存储到 event.context
    event.context.user = decoded
  }
  catch (error) {
    console.error('JWT 验证错误: ', error)
    throw createError({ statusCode: 401, message: 'Token 无效或已过期' })
  }
})

使用

前后端联调

网络请求的二次封装

封装 message 消息弹窗

第一步封装组件

~/components/UseMessage/index.vue

vue 复制代码
<template>
</template>

<script>
import { useMessage } from 'naive-ui'
import { defineComponent } from 'vue'

// content
export default defineComponent({
  setup() {
    onMounted(() => {
      if (import.meta.client) {
        window.$message = useMessage()
      }
    })
  },
})
</script>

第二步定义类型

types/message.d.ts

ts 复制代码
import type { MessageApiInjection } from 'naive-ui/lib/message/src/MessageProvider'

declare global {
  interface Window {
    $message: MessageApiInjection
  }
}

第三步 app.vue进行引入 使用 只在客户端渲染标签包裹 以防止控制台的警告

网络请求封装

定义基础类型

~/types/ResponseResult.d.ts

ts 复制代码
interface ResponseResult<T> {
  code: number
  msg: string
  data: T
}

~/utils/request.ts

tsx 复制代码
import type { FetchOptions, FetchRequest } from 'ofetch'
import { createError } from 'nuxt/app'
import { $fetch } from 'ofetch'
import { useRouter } from 'vue-router'

// 定义全局的接口域名
export const baseURL = 'http://localhost:3000/'

// 创建 $fetch 实例
const _useApi = $fetch.create({
  baseURL,
  retry: 1, // 失败后自动重试 1 次
  timeout: 10000, // 超时 10s

  // 请求拦截器
  async onRequest({ options }) {
    const token = store.get('token')

    const header = {
      ...options.headers,
      'Authorization': token ? `Bearer ${token}` : '',
      'Content-Type': 'application/json',
    }

    options.headers = header
  },

  // 响应拦截器
  async onResponse({ response }) {
    if (response.status >= 200 && response.status < 300) {
      const resData = response._data
      if (resData.code !== 0) {
        window.$message.error(resData.msg)
        throw createError({ statusCode: response.status, statusMessage: resData.msg })
      }
      return resData
    }
  },

  // 处理响应错误
  async onResponseError({ response }) {
    const router = useRouter()

    if (!response) {
      throw createError({ statusCode: 500, statusMessage: '网络错误,请检查您的网络连接' })
    }

    const status = response.status
    switch (status) {
      case 400:
        throw createError({ statusCode: 400, statusMessage: '请求参数错误' })
      case 401:
        router.push('/login')
        throw createError({ statusCode: 401, statusMessage: '未授权,请重新登录' })
      case 403:
        throw createError({ statusCode: 403, statusMessage: '权限不足' })
      case 500:
        throw createError({ statusCode: 500, statusMessage: '服务器错误,请稍后再试' })
      default:
        throw createError({ statusCode: status, statusMessage: `未知错误: ${response.status}` })
    }
  },
})

// 通用 API 调用
export async function useApi<T, D = ResponseResult<T>>(request: FetchRequest, options?: FetchOptions<'json'>): Promise<D> {
  return _useApi(request, options)
}

// GET 请求封装
export async function getApi<T, D = ResponseResult<T>>(url: string, params?: object): Promise<D> {
  return useApi<T, D>(url, {
    method: 'GET',
    params,
  })
}

// POST 请求封装
export async function postApi<T, D = ResponseResult<T>>(url: string, body?: object): Promise<D> {
  return useApi<T, D>(url, {
    method: 'POST',
    body,
  })
}

使用

~/api/apiLogin.ts

tsx 复制代码
import { getApi, postApi } from '~/composabes/request'

interface LoginBody {
  email: string
  password: string
}

export interface LoginFrom {
  email: string
  password: string
  token: string
  nickname: string
  id: string
}

export async function apiLogin(data: LoginBody) {
  return postApi<LoginFrom>('/api/auth/login', data)
}

hooks 说明

composabes 文件目录下的 就是表示hooks文件 就是vue 文件里面的函数如果过多 可以将其抽离出来封装成一个个的hooks 也可以实现复用

写法 和 pinia 写法是一致的

ts 复制代码
// composables/useCounter.ts
import { ref, computed } from 'vue';

export function useCounter() {
  const count = ref(0);

  const doubleCount = computed(() => count.value * 2);

  const increment = () => {
    count.value++;
  };

  return { count, doubleCount, increment };
}
相关推荐
Codelinghu3 分钟前
做后端的我在公司造了一个前端轮子,领导:嘿!你他娘的真是个天才。
前端
小old弟8 分钟前
vue3模板中ref的实现原理
前端·vue.js
招风的黑耳12 分钟前
ElementUI元件库——提升Axure原型设计效率与质量
前端·elementui·axure
Captaincc15 分钟前
用MCP 让Claude控制ChatGPT 4o,自动生成吉卜力风格的分镜
前端·claude·mcp
阿白的白日梦21 分钟前
JSX 元素隐式具有类型 "any",因为不存在接口 "JSX.IntrinsicElements"。ts 无法使用 JSX,除非提供了 "--js
前端·javascript·typescript
amagi60021 分钟前
关于黑马程序员微信小程序案例3-3的静态配置问题
前端
curdcv_po25 分钟前
React 进阶:useReducer 详解与实战指南
前端
用户40993225021230 分钟前
深入掌握FastAPI与OpenAPI规范的高级适配技巧
前端·javascript·后端
东东__net30 分钟前
1_vue基本_插件
java·前端·javascript
curdcv_po38 分钟前
聊一聊js的全局作用域和局部作用域
前端