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:admin888@127.0.0.1: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 };
}
相关推荐
拾光拾趣录2 分钟前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css
莫空00002 分钟前
深入理解JavaScript属性描述符:从数据属性到存取器属性
前端·面试
guojl3 分钟前
深度剖析Kafka读写机制
前端
FogLetter4 分钟前
图片懒加载:让网页飞起来的魔法技巧 ✨
前端·javascript·css
Mxuan4 分钟前
vscode webview 插件开发(精装篇)
前端
Mxuan5 分钟前
vscode webview 插件开发(交付篇)
前端
Mxuan7 分钟前
vscode 插件与 electron 应用跳转网页进行登录的实践
前端
拾光拾趣录7 分钟前
JavaScript 加载对浏览器渲染的影响
前端·javascript·浏览器
Codebee7 分钟前
OneCode图表配置速查手册
大数据·前端·数据可视化
然我8 分钟前
React 开发通关指南:用 HTML 的思维写 JS🚀🚀
前端·react.js·html