搭建基础模板文档
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 可以实现自动格式化代码:
-
创建配置文件:
.vscode/settings.json
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 全局
- 安装
pinia
js
pnpm install pinia @pinia/nuxt
- 配置
nuxt.config.ts
js
modules: [
'@pinia/nuxt',
],
- 持久化配置
- 安装
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 和数据库
- 安装依赖
sql
pnpm install @prisma/client
pnpm install --save-dev prisma
- 初始化 Prisma
csharp
// 我们必须进入到 server 文件目录下进行初始化
npx prisma init
会创建.env
文件与prisma
文件夹
- .env 用于定义数据库连接
- prisma用于定义模型结构与数据迁移与数据填充文件
修改.env
文件设置mysql连接,以下连接请根据你的情况修改
ini
DATABASE_URL="mysql://root:[email protected]:3306/nest"
- 配置
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 };
}