From 54c35687c9d780f18bff5f57bc44366b533da587 Mon Sep 17 00:00:00 2001 From: expressgy Date: Fri, 25 Apr 2025 11:33:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=99=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/transitions.css | 43 +++++++- docs/06-Nuxt生命周期.md | 111 ++++++++++++++++++++ drizzle/schema.ts | 4 +- layouts/home.vue | 8 +- modules/my-imports.ts | 18 ++++ nuxt.config.ts | 14 ++- pages/home/blog/index.vue | 8 +- pages/home/book/index.vue | 4 + pages/home/fun/index.vue | 4 + pages/home/hello/index.vue | 4 + pages/home/index.vue | 56 ++++++---- pages/home/tools/index.vue | 17 +++ pages/index.vue | 3 +- server/api/blog/blogMenu/[blogId].delete.ts | 2 +- server/api/blog/blogMenu/[blogId].patch.ts | 2 +- server/api/blog/blogMenu/[blogId].put.ts | 2 +- server/api/blog/blogMenu/index.get.ts | 13 ++- server/api/blog/blogMenu/index.post.ts | 10 +- server/api/hello.ts | 1 - server/middleware/02.auth.ts | 4 +- server/plugins/mysql.ts | 3 +- server/services/blog/blogMenu.ts | 40 +++++++ server/utils/entity.ts | 2 +- server/utils/jwt.ts | 2 - tsconfig.json | 7 +- types/nuxt.config.d.ts | 7 ++ 26 files changed, 340 insertions(+), 49 deletions(-) create mode 100644 docs/06-Nuxt生命周期.md create mode 100644 modules/my-imports.ts create mode 100644 pages/home/tools/index.vue create mode 100644 server/services/blog/blogMenu.ts diff --git a/assets/css/transitions.css b/assets/css/transitions.css index 2a46885..f4328f7 100644 --- a/assets/css/transitions.css +++ b/assets/css/transitions.css @@ -1,10 +1,43 @@ -/* 淡入淡出动画 */ -.fade-enter-active, -.fade-leave-active { +.indexFade-enter-active, +.indexFade-leave-active { transition: opacity 0.3s ease; } -.fade-enter-from, -.fade-leave-to { +.indexFade-enter-from, +.indexFade-leave-to { opacity: 0; } +/* 淡入淡出动画 */ + +.slide-enter-active, +.slide-leave-active { + transition: all 0.5s ease; +} +.slide-enter-from, +.slide-leave-to { + opacity: 0; + transform: translate(100%, 0); +} + +.slide-left-enter-active, +.slide-left-leave-active, +.slide-right-enter-active, +.slide-right-leave-active { + transition: all 0.2s; +} +.slide-left-enter-from { + opacity: 0; + transform: translate(50px, 0); +} +.slide-left-leave-to { + opacity: 0; + transform: translate(-50px, 0); +} +.slide-right-enter-from { + opacity: 0; + transform: translate(-50px, 0); +} +.slide-right-leave-to { + opacity: 0; + transform: translate(50px, 0); +} \ No newline at end of file diff --git a/docs/06-Nuxt生命周期.md b/docs/06-Nuxt生命周期.md new file mode 100644 index 0000000..790e469 --- /dev/null +++ b/docs/06-Nuxt生命周期.md @@ -0,0 +1,111 @@ +# Nuxt HTTP 请求生命周期 + +当一个 HTTP 请求发送到 Nuxt 应用时,它会经过一系列步骤,下面是完整的请求生命周期: + +## 服务器端处理 + +### 1. Nitro 服务器初始化(仅一次) + +- Nitro 服务器启动并初始化 `/server/plugins` 目录下的插件 +- 这些插件只在服务器启动时执行一次 +- 在无服务器环境中,每个请求都会启动服务器,但插件不会被等待执行完成 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-1-setup-nitro-server-and-nitro-plugins-once) + +### 2. Nitro 服务器中间件 + +- 对于每个请求,执行 `server/middleware/` 下的中间件 +- 这些中间件可用于身份验证、日志记录或请求转换 +- 如果中间件返回值,请求将终止并将返回值作为响应 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-2-nitro-server-middleware) + +### 3. 请求路由分发 + +根据请求路径,Nitro 将请求分发到不同的处理程序: + +- 如果请求路径匹配 `/api/*`,则路由到 API 处理程序(`server/api/` 目录) +- 如果请求路径匹配 `server/routes/` 中定义的路由,则路由到相应的处理程序 +- 如果是页面请求,则继续 Nuxt 应用的初始化 + +[服务器目录结构](https://nuxt.com/docs/guide/directory-structure/server) + +### 4. API 处理(如果是 API 请求) + +- 如果请求匹配 `server/api/` 目录中的路由,相应的处理程序将被执行 +- 处理程序可以直接返回 JSON 数据、Promise 或使用 `event.node.res.end()` 发送响应 +- API 处理完成后,请求结束,不会继续到 Vue 应用 + +[服务器 API](https://nuxt.com/docs/guide/directory-structure/server) + +### 5. Nuxt 应用初始化(如果是页面请求) + +- 创建 Vue 和 Nuxt 实例 +- 执行 Nuxt 服务器插件,包括内置插件和 `plugins/` 目录中的自定义插件 +- 调用 `app:created` 钩子 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-3-initialize-nuxt-and-execute-nuxt-app-plugins) + +### 6. 路由验证 + +- 如果在 `definePageMeta` 中定义了 `validate` 方法,则调用该方法验证动态路由参数 +- 如果验证失败,可能会终止请求或重定向 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-4-route-validation) + +### 7. 执行 Nuxt 应用中间件 + +- 执行全局路由中间件、命名路由中间件和匿名路由中间件 +- 中间件可以执行重定向,这会导致浏览器收到 `Location:` 头并发起新请求 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-5-execute-nuxt-app-middleware) + +### 8. 页面和组件设置 + +- 初始化页面和组件 +- 使用 `useFetch` 和 `useAsyncData` 获取所需数据 +- 服务器端不执行 Vue 生命周期钩子如 `onBeforeMount`、`onMounted` 等 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-6-setup-page-and-components) + +### 9. 渲染和生成 HTML 输出 + +- 将组件与 `unhead` 设置结合生成完整的 HTML 文档 +- 调用 `app:rendered` 钩子 +- 调用 `render:html` 钩子,允许操作生成的 HTML +- 将 HTML 和相关数据发送回客户端 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-7-render-and-generate-html-output) + +## 客户端处理 + +### 1. 执行 Nuxt 应用中间件 + +- 中间件在服务器端和客户端都会运行 +- 可以使用 `import.meta.client` 和 `import.meta.server` 区分环境 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-3-execute-nuxt-app-middleware) + +### 2. 挂载 Vue 应用和水合 + +- 调用 `app:beforeMount` 钩子 +- 调用 `app.mount('#__nuxt')` 将 Vue 应用挂载到 DOM +- Vue 执行水合步骤,使客户端应用程序具有交互性 +- 调用 `app:mounted` 钩子 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-4-mount-vue-application-and-hydration) + +### 3. Vue 生命周期 + +- 浏览器执行完整的 Vue 生命周期 +- 包括 `onBeforeMount`、`onMounted` 等钩子 + +[Nuxt 生命周期](https://nuxt.com/docs/guide/concepts/nuxt-lifecycle#step-5-vue-lifecycle) + +## 重要说明 + +- 服务器端重定向会导致浏览器收到 `Location:` 头并发起新请求,所有应用状态将重置 +- 中间件在服务器端渲染和客户端水合期间都会执行,可能导致代码执行两次 +- 对于 API 请求,建议使用 `useAsyncData`、`useFetch` 等 SSR 友好的组合式函数,确保服务器端获取的数据在水合期间被重用 + +[通用渲染](https://nuxt.com/docs/guide/concepts/rendering#universal-rendering) \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 30e7ccd..edfb077 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,10 +1,10 @@ import { mysqlTable, index, primaryKey, unique, varchar, int, tinyint, datetime } from "drizzle-orm/mysql-core" import { sql } from "drizzle-orm" -import { bigintString } from './customType.js'; +import { bigintString } from '~/drizzle/customType'; const bigint = bigintString; export const blogMenu = mysqlTable("blog_menu", { id: bigint({ mode: "number" }).notNull(), - pid: bigint({ mode: "number" }).notNull(), + pid: bigint({ mode: "number" }).default('0').notNull(), name: varchar({ length: 255 }).notNull(), desc: varchar({ length: 255 }), icon: varchar({ length: 255 }).default("").notNull(), diff --git a/layouts/home.vue b/layouts/home.vue index 33054a0..e2f090d 100644 --- a/layouts/home.vue +++ b/layouts/home.vue @@ -5,11 +5,9 @@
-
- +
-
@@ -38,7 +36,7 @@ overflow-y: auto; } - & > div.homeMainLeft { + & > .homeMainLeft { flex: 1; } @@ -48,7 +46,7 @@ background-color: var(--bg-color-be); } - & > div.homeMainRight { + & > .homeMainRight { flex: 1; } diff --git a/modules/my-imports.ts b/modules/my-imports.ts new file mode 100644 index 0000000..88b7bba --- /dev/null +++ b/modules/my-imports.ts @@ -0,0 +1,18 @@ +import { defineNuxtModule, addImportsSources } from '@nuxt/kit' + +export default defineNuxtModule({ + meta: { + name: 'my-imports' + }, + setup() { + addImportsSources({ + from: 'consola', + imports: [ + // 列出您需要从consola导入的函数 + 'createConsola', + 'consola' + // 其他需要的函数... + ], + }) + } +}) \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index c0c6a43..60aa9e8 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -13,6 +13,9 @@ export default defineNuxtConfig({ host: '0.0.0.0', port: 3000, }, + features: { + devLogs: 'silent' // 这是默认值,确保它没有被设置为 true,日志区分server和client, false为不在client打印 + }, css: [ '~/assets/css/style.css', '~/assets/css/font.css', @@ -40,12 +43,12 @@ export default defineNuxtConfig({ ] }, layoutTransition:{ - name: 'fade', + name: 'indexFade', mode: 'out-in', type: 'transition', // 明确指定动画类型 duration: { - enter: 200, - leave: 500 + enter: 300, + leave: 300 }, appear: true }, @@ -92,6 +95,11 @@ export default defineNuxtConfig({ '/docs/json' ], }, + defaultToken: { + username: process.env.USERNAME || 'expressgy', + nickname: process.env.PASSWORD || 'Nie', + userId: '1' + } }, }) diff --git a/pages/home/blog/index.vue b/pages/home/blog/index.vue index 3780e72..c8f8f41 100644 --- a/pages/home/blog/index.vue +++ b/pages/home/blog/index.vue @@ -1,13 +1,17 @@ \ No newline at end of file diff --git a/pages/home/book/index.vue b/pages/home/book/index.vue index 4974a96..f3e8293 100644 --- a/pages/home/book/index.vue +++ b/pages/home/book/index.vue @@ -1,6 +1,10 @@ diff --git a/pages/home/fun/index.vue b/pages/home/fun/index.vue index d5fc4cb..2801b00 100644 --- a/pages/home/fun/index.vue +++ b/pages/home/fun/index.vue @@ -1,6 +1,10 @@ diff --git a/pages/home/hello/index.vue b/pages/home/hello/index.vue index bcc1ff9..0164eee 100644 --- a/pages/home/hello/index.vue +++ b/pages/home/hello/index.vue @@ -1,6 +1,10 @@ diff --git a/pages/home/index.vue b/pages/home/index.vue index a21b081..172a6e3 100644 --- a/pages/home/index.vue +++ b/pages/home/index.vue @@ -1,27 +1,42 @@ \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue index 67b173a..d725c1c 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -8,9 +8,10 @@ const leaveTime = ref(false) // 离开前路由拦截 onBeforeRouteLeave((to, from, next) => { leaveTime.value = true + consola.error('leave') setTimeout(() => { next(true) - }, 1300) + }, 1000) }) diff --git a/server/api/blog/blogMenu/[blogId].delete.ts b/server/api/blog/blogMenu/[blogId].delete.ts index 675cda1..7d36572 100644 --- a/server/api/blog/blogMenu/[blogId].delete.ts +++ b/server/api/blog/blogMenu/[blogId].delete.ts @@ -1,3 +1,3 @@ export default defineEventHandler(event => { - return 'Hello blog/blogMenu.[blogId].delete' + event.context.params.blogId; + return 'Hello blog/blogMenu.[blogId].delete' + event.context.params?.blogId; }) diff --git a/server/api/blog/blogMenu/[blogId].patch.ts b/server/api/blog/blogMenu/[blogId].patch.ts index 0fc841f..de8aeba 100644 --- a/server/api/blog/blogMenu/[blogId].patch.ts +++ b/server/api/blog/blogMenu/[blogId].patch.ts @@ -1,3 +1,3 @@ export default defineEventHandler(event => { - return 'Hello blog/blogMenu.[blogId].patch' + event.context.params.blogId; + return 'Hello blog/blogMenu.[blogId].patch' + event.context.params?.blogId; }) diff --git a/server/api/blog/blogMenu/[blogId].put.ts b/server/api/blog/blogMenu/[blogId].put.ts index 41ca639..5a7d69d 100644 --- a/server/api/blog/blogMenu/[blogId].put.ts +++ b/server/api/blog/blogMenu/[blogId].put.ts @@ -1,3 +1,3 @@ export default defineEventHandler(event => { - return 'Hello blog/blogMenu.[blogId].update' + event.context.params.blogId; + return 'Hello blog/blogMenu.[blogId].update' + event.context.params?.blogId; }) diff --git a/server/api/blog/blogMenu/index.get.ts b/server/api/blog/blogMenu/index.get.ts index 62b16e8..d50a39c 100644 --- a/server/api/blog/blogMenu/index.get.ts +++ b/server/api/blog/blogMenu/index.get.ts @@ -1,3 +1,12 @@ -export default defineEventHandler(event => { - return 'Hello blog/blogMenu.get' +import {BlogMenuDB} from "~/server/services/blog/blogMenu"; +import consola from "consola"; + +export default defineEventHandler(async event => { + // 获取登录信息 + const headerAuth = event.context.auth + // 初始化DB + const blogMenuDB = new BlogMenuDB(event) + const resd = await blogMenuDB.getBlogMenu(headerAuth) + consola.info(resd) + return resd }) diff --git a/server/api/blog/blogMenu/index.post.ts b/server/api/blog/blogMenu/index.post.ts index 4df4c78..9ce077a 100644 --- a/server/api/blog/blogMenu/index.post.ts +++ b/server/api/blog/blogMenu/index.post.ts @@ -1,3 +1,11 @@ -export default defineEventHandler(event => { +export default defineEventHandler(async event => { + const requAuth = event.context.auth + const body = await readBody(event) + if(!requAuth.isTrue) { + // 判断为正常登录 + throw createError({ + + }) + } return 'Hello blog/blogMenu.post' }) diff --git a/server/api/hello.ts b/server/api/hello.ts index 2736958..935448e 100644 --- a/server/api/hello.ts +++ b/server/api/hello.ts @@ -3,7 +3,6 @@ import consola from "consola"; export default defineEventHandler(async event => { // console.log(await event.context.redis.get('SI HI')) consola.info('API') - await new Promise(resolve => setTimeout(resolve, 1000)); consola.info('API TimeOut') const result = await event.context.redis.get('SI HI'); return { diff --git a/server/middleware/02.auth.ts b/server/middleware/02.auth.ts index 8d831b5..2b14461 100644 --- a/server/middleware/02.auth.ts +++ b/server/middleware/02.auth.ts @@ -2,10 +2,12 @@ import consola from "consola"; export default defineEventHandler(event => { // 跳过无需验证的路由 if (event.path?.startsWith('/api/auth')) return + const {defaultToken} = useRuntimeConfig() const token = getHeader(event, 'Authorization')?.split('Bearer ')[1]; const t = generateAccessToken({ - name: event.path, + ...defaultToken, + isTrue: false }) consola.info(`Token ${t}`); diff --git a/server/plugins/mysql.ts b/server/plugins/mysql.ts index 93f8532..4ea0fbe 100644 --- a/server/plugins/mysql.ts +++ b/server/plugins/mysql.ts @@ -44,7 +44,8 @@ export default defineNitroPlugin(async nitroApp => { const db = drizzle(pool,{ logger: { logQuery: (query, params) => { - consola.debug(`SQL: ${query} - Params: ${JSON.stringify(params)}`); // 打印日志,包括查询和参数 + console.log(`SQL: ${query} - Params: ${JSON.stringify(params, (_, v) => + typeof v === 'bigint' ? v.toString() : v)}`); // 打印日志,包括查询和参数 } } }); diff --git a/server/services/blog/blogMenu.ts b/server/services/blog/blogMenu.ts new file mode 100644 index 0000000..f399b7c --- /dev/null +++ b/server/services/blog/blogMenu.ts @@ -0,0 +1,40 @@ +import type {EventHandlerRequest, H3Event} from "h3"; +import type {MySql2Database, MySqlRawQueryResult} from "drizzle-orm/mysql2"; +import type {RedisClient} from "ioredis/built/connectors/SentinelConnector/types"; +import type {Pool} from "mysql2/promise"; +import type {HeaderAuth} from "~/types/nuxt.config"; +import {blogMenu} from "~/drizzle/schema"; +import {and, asc, desc, eq} from "drizzle-orm"; + +type InsertBlogMenu = typeof blogMenu.$inferInsert; + +export class BlogMenuDB { + private readonly db:MySql2Database>& { $client: Pool } + private readonly redis: RedisClient + constructor(event: H3Event) { + this.db = event.context.mysql + this.redis = event.context.redis + } + + // 插入目录 + async insertBlogMenu(insertBlogMenu: InsertBlogMenu, headerAuth: HeaderAuth): Promise { + const result = await this.db.insert(blogMenu).values({ + ...insertBlogMenu, + createdBy: headerAuth.userId, + }) + return result; + } + + // 查询目录 + async getBlogMenu(headerAuth: HeaderAuth){ + // 指定用户的 + // 判断未删除的 + // 如果是登陆的,可以查看未公开的,如果不是,只能查看公开的 + return this.db + .select() + .from(blogMenu) + .where( + and(eq(blogMenu.createdBy, headerAuth.userId), eq(blogMenu.deleted, 0), headerAuth.isTrue ? undefined : eq(blogMenu.public, 1)) + ).orderBy(asc(blogMenu.sort), desc(blogMenu.createdAt)); + } +} \ No newline at end of file diff --git a/server/utils/entity.ts b/server/utils/entity.ts index 67d56b8..d711548 100644 --- a/server/utils/entity.ts +++ b/server/utils/entity.ts @@ -1,4 +1,4 @@ -import * as schema from "../../drizzle/schema"; +import * as schema from "~/drizzle/schema"; export function entity() { return schema } diff --git a/server/utils/jwt.ts b/server/utils/jwt.ts index 5da12be..fbb6b86 100644 --- a/server/utils/jwt.ts +++ b/server/utils/jwt.ts @@ -1,8 +1,6 @@ import jwt from 'jsonwebtoken' import type {StringValue} from "ms"; const {sign, verify} = jwt -console.log(jwt) -// export function jwt() {} export const generateAccessToken = (payload: object) => { diff --git a/tsconfig.json b/tsconfig.json index a746f2a..61c3b06 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,9 @@ { // https://nuxt.com/docs/guide/concepts/typescript - "extends": "./.nuxt/tsconfig.json" + "extends": "./.nuxt/tsconfig.json", + "compilerOptions": { + "paths": { +// "~/*": ["./*"] + } + } } diff --git a/types/nuxt.config.d.ts b/types/nuxt.config.d.ts index 9569a23..2100a85 100644 --- a/types/nuxt.config.d.ts +++ b/types/nuxt.config.d.ts @@ -9,3 +9,10 @@ // }, // } // } + +export type HeaderAuth = { + username: string; + nickname: string; + userId: string; + isTrue: boolean; +} \ No newline at end of file