From 6ecc66ac115d9a42249c8b693846bbea7be9781b Mon Sep 17 00:00:00 2001 From: wanghao1993 Date: Wed, 4 Sep 2024 18:03:22 +0800 Subject: [PATCH] =?UTF-8?q?doc:=20=E5=A2=9E=E5=8A=A0=E5=87=A0=E7=AF=87?= =?UTF-8?q?=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/blog/create/route.ts | 3 +- components/Article/ArticleItem.tsx | 4 +- components/Icon/icon.tsx | 4 +- components/Image/MdxImage.tsx | 5 + components/LoginOutBtn.tsx | 9 +- components/TOCInline.tsx | 14 +- components/blog/Categories.tsx | 5 +- components/blog/Posts.tsx | 13 + data/utils/index.ts | 10 +- lib/fetch_utils.ts | 3 + lib/heading.ts | 1 + "posts/JS\344\272\213\344\273\266.mdx" | 382 ++-- ...6\347\232\204\350\256\260\345\275\225.mdx" | 1864 +++++++++++++++++ posts/prisma.mdx | 29 +- ...7\345\217\212\345\256\236\350\267\265.mdx" | 8 +- ...3\350\242\253\345\213\222\347\264\242.mdx" | 12 +- ...6\234\254http\345\214\272\345\210\253.mdx" | 144 ++ ...347\232\204JS\346\211\213\345\206\231.mdx" | 362 ++++ ...\350\203\275\344\274\230\345\214\2261.mdx" | 248 +++ ...\350\203\275\344\274\230\345\214\2262.mdx" | 291 +++ ...6\345\272\223\345\272\224\347\224\250.mdx" | 4 + 21 files changed, 3256 insertions(+), 159 deletions(-) create mode 100644 components/Image/MdxImage.tsx create mode 100644 "posts/nextjs\345\274\200\345\217\221\346\255\244\351\241\271\347\233\256\347\232\204\350\256\260\345\275\225.mdx" create mode 100644 "posts/\345\220\204\347\211\210\346\234\254http\345\214\272\345\210\253.mdx" create mode 100644 "posts/\345\270\270\350\247\201\347\232\204JS\346\211\213\345\206\231.mdx" create mode 100644 "posts/\346\200\247\350\203\275\344\274\230\345\214\2261.mdx" create mode 100644 "posts/\346\200\247\350\203\275\344\274\230\345\214\2262.mdx" diff --git a/app/api/blog/create/route.ts b/app/api/blog/create/route.ts index e57c8ed..abb2ccd 100644 --- a/app/api/blog/create/route.ts +++ b/app/api/blog/create/route.ts @@ -18,6 +18,7 @@ export async function POST(request: Request) { views_count: 1, }, }); + return responseHandler(res); } else { const updateRes = await prisma.post.update({ @@ -28,10 +29,10 @@ export async function POST(request: Request) { views_count: res.views_count + 1, }, }); + return responseHandler(updateRes); } } catch (e) { - console.log(e); return responseHandler( null, BusinessCode.normal, diff --git a/components/Article/ArticleItem.tsx b/components/Article/ArticleItem.tsx index d8108cb..c1e6f16 100644 --- a/components/Article/ArticleItem.tsx +++ b/components/Article/ArticleItem.tsx @@ -33,7 +33,7 @@ export default function ArticleItem(data: { articleInfo: Post }) { {item} @@ -47,7 +47,7 @@ export default function ArticleItem(data: { articleInfo: Post }) { {item} diff --git a/components/Icon/icon.tsx b/components/Icon/icon.tsx index b3f71cf..3cb9428 100644 --- a/components/Icon/icon.tsx +++ b/components/Icon/icon.tsx @@ -40,8 +40,8 @@ export function SignIn(props: { color: string }) { return ( diff --git a/components/Image/MdxImage.tsx b/components/Image/MdxImage.tsx new file mode 100644 index 0000000..3f4d3bc --- /dev/null +++ b/components/Image/MdxImage.tsx @@ -0,0 +1,5 @@ +import NextImage, { ImageProps } from "next/image"; + +const Image = ({ ...rest }: ImageProps) => ; + +export default Image; diff --git a/components/LoginOutBtn.tsx b/components/LoginOutBtn.tsx index 2798870..f74117a 100644 --- a/components/LoginOutBtn.tsx +++ b/components/LoginOutBtn.tsx @@ -48,8 +48,11 @@ export default function LoginInOut() { setVisible(false)} /> {status === "unauthenticated" ? ( -
setVisible(true)} className="login-icon"> - +
setVisible(true)} + className="login-icon cursor-pointer" + > +
) : status === "authenticated" ? ( ) : ( diff --git a/components/TOCInline.tsx b/components/TOCInline.tsx index 4fa4bf8..98831de 100644 --- a/components/TOCInline.tsx +++ b/components/TOCInline.tsx @@ -56,10 +56,7 @@ const TOCInline = ({ key={heading.value} className={`${heading.depth >= indentDepth && "ml-6"}`} > - + {heading.value} @@ -71,13 +68,14 @@ const TOCInline = ({ <> {asDisclosure ? (
- - Table of Contents - + 目录
{tocList}
) : ( - tocList +
+

目录

+ {tocList} +
)} ); diff --git a/components/blog/Categories.tsx b/components/blog/Categories.tsx index 6596e95..2363e7d 100644 --- a/components/blog/Categories.tsx +++ b/components/blog/Categories.tsx @@ -4,17 +4,18 @@ import Link from "next/link"; const getCategory = () => { return getAllCategory(); }; + export default async function Category() { const categories = getCategory(); return (

分类

-
+
{Object.keys(categories).map((item) => ( {item} diff --git a/components/blog/Posts.tsx b/components/blog/Posts.tsx index 664cfa0..ec0f9e8 100644 --- a/components/blog/Posts.tsx +++ b/components/blog/Posts.tsx @@ -1,3 +1,11 @@ +/* + * @Author: wanghao1993 whao53333@gmail.com + * @Date: 2024-09-04 13:57:31 + * @LastEditors: wanghao1993 whao53333@gmail.com + * @LastEditTime: 2024-09-04 17:24:33 + * @FilePath: \blog-offical\components\blog\Posts.tsx + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ "use client"; import { ArticleType } from "@/types/article"; import ArticleItem from "@/components/Article/ArticleItem"; @@ -7,6 +15,11 @@ import { Post } from "contentlayer/generated"; export interface PostCardProps { posts: Post[]; } +/** + * @description: + * @param {PostCardProps} param1 + * @return {*} + */ export default function PostsCard({ posts }: PostCardProps) { const [mounted, setMounted] = useState(false); diff --git a/data/utils/index.ts b/data/utils/index.ts index d536478..e167619 100644 --- a/data/utils/index.ts +++ b/data/utils/index.ts @@ -1,9 +1,17 @@ +/* + * @Author: wanghao1993 whao53333@gmail.com + * @Date: 2024-09-04 13:57:31 + * @LastEditors: wanghao1993 whao53333@gmail.com + * @LastEditTime: 2024-09-04 17:08:15 + * @FilePath: \blog-offical\data\utils\index.ts + * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE + */ import { allPosts } from "contentlayer/generated"; export const getAllPostsMeta = () => { // 文章日期排序,最新的在最前面 allPosts.sort((a, b) => { - return +new Date(a.date) < +new Date(a.date) ? 1 : -1; + return +new Date(a.date) < +new Date(b.date) ? 1 : -1; }); return allPosts; }; diff --git a/lib/fetch_utils.ts b/lib/fetch_utils.ts index 9a92fb7..2784954 100644 --- a/lib/fetch_utils.ts +++ b/lib/fetch_utils.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import prisma from "./pg"; export const StatusMsg = { 200: "Success", @@ -22,5 +23,7 @@ export function responseHandler( code = BusinessCode.normal, message = "success" ) { + prisma.$disconnect(); + return NextResponse.json({ message, data, code }, { status }); } diff --git a/lib/heading.ts b/lib/heading.ts index 7ff10c0..d929087 100644 --- a/lib/heading.ts +++ b/lib/heading.ts @@ -26,5 +26,6 @@ export function remarkTocHeadings() { export async function extractTocHeadings(markdown: string) { const vfile = await remark().use(remarkTocHeadings).process(markdown); + console.log(vfile.data.toc); return vfile.data.toc; } diff --git "a/posts/JS\344\272\213\344\273\266.mdx" "b/posts/JS\344\272\213\344\273\266.mdx" index 1e64b2f..4a27406 100644 --- "a/posts/JS\344\272\213\344\273\266.mdx" +++ "b/posts/JS\344\272\213\344\273\266.mdx" @@ -3,241 +3,383 @@ title: 关于事件你需要知道一些事情 date: 2021-03-08 key: something-about-event tags: JS基础 +keywords: js事件,js event,事件冒泡,事件代理,自定义事件,事件中心 categories: JS description: 关于事件的一些事情,事件流,事件冒泡,事件捕获,如何实现自定义的事件等等 --- -3.8妇女节,祝愿天下的妇女节日快乐。 -# 事件 -Js与HTML之间的交互通过事件完成,事件,就是文档或浏览器窗口中发生的一些 特定的交互瞬间。事件类型有很多分类,例如:DOM事件类型,焦点事件,滚轮事件等等,说到这里就不得不说一下js的事件流 -# 事件流 +import TOCInline from "@/components/TOCInline"; + + + +3.8 妇女节,祝愿天下的妇女节日快乐。 + +## 事件 + +Js 与 HTML 之间的交互通过事件完成,事件,就是文档或浏览器窗口中发生的一些 特定的交互瞬间。事件类型有很多分类,例如:DOM 事件类型,焦点事件,滚轮事件等等,说到这里就不得不说一下 js 的事件流 + ## 事件冒泡 -事件冒泡,事件开始从具体的节点出发,逐步向上冒泡到外层节点。例如:点击id为a的div时,事件会一步步向上传递 a=> b => c,当我们点击click的时候,会依次输出123 + +事件冒泡,事件开始从具体的节点出发,逐步向上冒泡到外层节点。例如:点击 id 为 a 的 div 时,事件会一步步向上传递 a=> b => c,当我们点击 click 的时候,会依次输出 123 ```html -
-
-
- click -
-
+
+
+
click
+
``` ## 事件捕获 + 虽然事件捕获是 Netscape 唯一支持的事件流模型,但 IE9、Safari、Chrome、Opera 和 Firefox 目前也都支持这种事件流模型。尽管“DOM2 级事件”规范要求事件应该从 document 对象开始传播,但这些浏览器都是从 window 对象开始捕获事件的。 由于老版本的浏览器不支持,因此很少有人使用事件捕获。 -## DOM事件流 -DOM事件流分三个阶段,捕获阶段,目标阶段和冒泡阶段,DOM事件流又分为DOM0级和DOM2级。 DOM事件流 +## DOM 事件流 -## DOM0级事件处理程序 -比如我们常见的click,load等等,DOM0级别事件被认定是元素的方法,所以其中this为元素本身,其作用域也是当前目标元素。 +DOM 事件流分三个阶段,捕获阶段,目标阶段和冒泡阶段,DOM 事件流又分为 DOM0 级和 DOM2 级。 DOM 事件流 -```html -
- click -
+## DOM0 级事件处理程序 +比如我们常见的 click,load 等等,DOM0 级别事件被认定是元素的方法,所以其中 this 为元素本身,其作用域也是当前目标元素。 + +```html +
click
``` -打印出来的this 就是元素本身。清除DOM0级事件可以直接赋值为null就会清除掉。 +打印出来的 this 就是元素本身。清除 DOM0 级事件可以直接赋值为 null 就会清除掉。 ```js -target.onclick = null +target.onclick = null; ``` -## DOM2级事件处理程序 -DOM2级事件提供了两个方法,即大名鼎鼎的 addEventListener 和 removeEventListener. 他们都提供了三个参数 1.事件名称 2.事件方法 3.事件是在捕获阶段执行,还是在冒泡阶段执行,类型为布尔值,true在捕获阶段,false在冒泡阶段,默认为false +## DOM2 级事件处理程序 + +DOM2 级事件提供了两个方法,即大名鼎鼎的 addEventListener 和 removeEventListener. 他们都提供了三个参数 1.事件名称 2.事件方法 3.事件是在捕获阶段执行,还是在冒泡阶段执行,类型为布尔值,true 在捕获阶段,false 在冒泡阶段,默认为 false ### addEventListener ```js -var target = document.getElementById('a') -target.addEventListener('click', function () { - console.log(this.id) -}, false) +var target = document.getElementById("a"); +target.addEventListener( + "click", + function () { + console.log(this.id); + }, + false +); ``` -DOM2级事件,不会和DOM0级的冲突,所以在click的时候。会先打印this,再打印id,同时可以添加多个click事件,此时会先打印id,在打印 hello world,执行顺序和添加顺序保持一致 + +DOM2 级事件,不会和 DOM0 级的冲突,所以在 click 的时候。会先打印 this,再打印 id,同时可以添加多个 click 事件,此时会先打印 id,在打印 hello world,执行顺序和添加顺序保持一致 ```js -var target = document.getElementById('a') -target.addEventListener('click', function () { - console.log(this.id) -}, false) - -target.addEventListener('click', function () { - console.log('hello world') -}, false) +var target = document.getElementById("a"); +target.addEventListener( + "click", + function () { + console.log(this.id); + }, + false +); + +target.addEventListener( + "click", + function () { + console.log("hello world"); + }, + false +); ``` -事件优先级:DOM0级事件 > DOM2 一道简单的面试题,点击c依次输出abc和依次输出cba +事件优先级:DOM0 级事件 > DOM2 一道简单的面试题,点击 c 依次输出 abc 和依次输出 cba ```html
- a -
- b -
- c -
-
+ a +
+ b +
c
+
-```` +``` + ### removeEventListener + 用于移除事件,移除事件的时候,必须的有两个参数,事件名称和处理函数。函数必须为是同一个函数,否则无法移除。 ```js -var target = document.getElementById('a') -function sayHello () { - console.log('hello world') +var target = document.getElementById("a"); +function sayHello() { + console.log("hello world"); } -target.addEventListener('click', sayHello, false) +target.addEventListener("click", sayHello, false); -target.removeEventListener('click', function () { - console.log('hello world') -}, false) +target.removeEventListener( + "click", + function () { + console.log("hello world"); + }, + false +); // 无效不能移除 -target.removeEventListener('click', sayHello, false) +target.removeEventListener("click", sayHello, false); // 有效 ``` + IE 实现了与 DOM 中类似的两个方法:attachEvent()和 detachEvent()。这两个方法接受相同 的两个参数:事件处理程序名称与事件处理程序函数。由于 IE8 及更早版本只支持事件冒泡,所以通过 attachEvent()添加的事件处理程序都会被添加到冒泡阶段。 鉴于此封装一个小型的事件处理方法,用来兼用跨平台: ```js var EventUtil = { - addHandler: function(element, type, handler, isBubble = false) { - if (element.addEventListener){ + addHandler: function (element, type, handler, isBubble = false) { + if (element.addEventListener) { element.addEventListener(type, handler, isBubble); - } else if (element.attachEvent){ - element.attachEvent("on" + type, handler); + } else if (element.attachEvent) { + element.attachEvent("on" + type, handler); } else { - element["on" + type] = handler; + element["on" + type] = handler; } }, - removeHandler: function(element, type, handler, isBubble = false){ - if (element.removeEventListener){ + removeHandler: function (element, type, handler, isBubble = false) { + if (element.removeEventListener) { element.removeEventListener(type, handler, isBubble); - } else if (element.detachEvent){ + } else if (element.detachEvent) { element.detachEvent("on" + type, handler); } else { element["on" + type] = null; - } - } + } + }, }; - ``` + ## 事件对象 -无论是DOM0 还是 DOM2 在执行事件的时候,都会返回一个Event对象给我们。其中包含很多的属性,重点说下下面几个。 -1.preventDefault,取消默认行为,例如a标签的跳转;相关的属性为cacelable,可以通过preventDefault取消默认行为,cacelable为true,否则为false +无论是 DOM0 还是 DOM2 在执行事件的时候,都会返回一个 Event 对象给我们。其中包含很多的属性,重点说下下面几个。 + +1.preventDefault,取消默认行为,例如 a 标签的跳转;相关的属性为 cacelable,可以通过 preventDefault 取消默认行为,cacelable 为 true,否则为 false -2.type 为出发该行为的事件是什么类型,例如click等 +2.type 为出发该行为的事件是什么类型,例如 click 等 3.eventPhase 当前触发事件的阶段,是冒泡,捕获,还是目标阶段 4.stopPropagation 可以停止冒泡和捕获的继续传递 -5.srcElement 和 target一样,事件的目标对象 +5.srcElement 和 target 一样,事件的目标对象 ## 自定义事件 -除了平时使用的原生事件,有时候需要自定义事件。创建自定义事件的方法有两种,Event() 构造函数 和 CustomEvent() 构造函数,还有个createEvent()已经废弃,但是仍然有浏览器支持,所以我们暂且不说,有兴趣的可以自己看看。 + +除了平时使用的原生事件,有时候需要自定义事件。创建自定义事件的方法有两种,Event() 构造函数 和 CustomEvent() 构造函数,还有个 createEvent()已经废弃,但是仍然有浏览器支持,所以我们暂且不说,有兴趣的可以自己看看。 ## Event() + 支持两个参数: 1.typeArg 事件名称 2.eventInitOption 事件初始化配置,配置内容包含三个字段 -参数 类型 说明 -bubbles 布尔 表示该事件是否冒泡 -cancelable 布尔 表示该事件能否被取消 -composed 布尔 指示事件是否会在影子DOM根节点之外触发侦听器。 +参数 类型 说明 +bubbles 布尔 表示该事件是否冒泡 +cancelable 布尔 表示该事件能否被取消 +composed 布尔 指示事件是否会在影子 DOM 根节点之外触发侦听器。 ex: ```js // 创建事件 -const ev = new Event('testEvent') +const ev = new Event("testEvent"); // 监听事件 -document.addEventListener('testEvent', function () { - console.log('testEvent执行了') -}) +document.addEventListener("testEvent", function () { + console.log("testEvent执行了"); +}); // 触发事件 -document.dispatchEvent(ev) - +document.dispatchEvent(ev); ``` + ## CustomEvent() -支持两个参数: 1.typeArg 事件名称 2.eventInitOption 事件初始化配置,配置内容包含事件部分字段,详情可以查看CustomEvent,额外还有一个detail字段可用于传递额外的参数 + +支持两个参数: 1.typeArg 事件名称 2.eventInitOption 事件初始化配置,配置内容包含事件部分字段,详情可以查看 CustomEvent,额外还有一个 detail 字段可用于传递额外的参数 ```js // 创建事件 -const ev = new CustomEvent('testEvent', { +const ev = new CustomEvent("testEvent", { detail: { - extraParams: 1 - } -}) + extraParams: 1, + }, +}); // 监听事件 -document.addEventListener('testEvent', function (e) { - console.log('testEvent执行了', e.detail) -}) +document.addEventListener("testEvent", function (e) { + console.log("testEvent执行了", e.detail); +}); // 触发事件 -document.dispatchEvent(ev) +document.dispatchEvent(ev); ``` + ## 事件代理 事件代理:就是将子元素的事件通过冒泡的形式交由父元素来执行。 常规应用: ```html -
-
-
-
1
-
1
-
1
-
1
-
1
-
1
-
1
-
1
-
1
-
1
-
1
-
1
-
-
+
+
+
+
1
+
1
+
1
+
1
+
1
+
1
+
1
+
1
+
1
+
1
+
1
+
1
+
- ``` - ```js -function findParent (child, parent) { +
+``` + +```js +function findParent(child, parent) { if (child.parentNode === parent) { - return true + return true; } else { - if (child.parentNode.nodeName.toLowerCase !== 'html') { - findParent(child.parentNode, parent) + if (child.parentNode.nodeName.toLowerCase !== "html") { + findParent(child.parentNode, parent); } else { - return false + return false; } } } window.onload = function () { - const parent = document.getElementById('idd') + const parent = document.getElementById("idd"); parent.onclick = function (e) { - let flag = findParent(e.target, parent) - if (e.target.nodeName.toLowerCase() === 'div' && flag) { - console.log('div') + let flag = findParent(e.target, parent); + if (e.target.nodeName.toLowerCase() === "div" && flag) { + console.log("div"); } - } + }; +}; +``` + +## 常见的事件中心 + +### 思考 + +一个事件中心应该包含哪些功能 + +· 发布 + +· 订阅 + +· 取消订阅 + +· 一次执行 + +基本上起码要包含以上几种方法 + +下面我们就来实现一下 + +```js +// 第一步:新建一个类别 +function E () { + } +// 原型 +E.prototype = { + // on 方法 + // e是一个对象,维护了事件的名称和cb + // e: { + 'eventName': [{ + ctx: _this1, + fn: () => {console.log(1)} + }, { + ctx: _this2, + fn: () => {console.log(3)} + }] + } + on: function (name, callback, ctx) { + var e = this.e || (this.e = {}); + + (e[name] || (e[name] = [])).push({ + fn: callback, + ctx: ctx + }); + + return this; + }, + + // once 方法 + // 创建一个listener,通过调用off,在执行一次就移除掉 + once: function (name, callback, ctx) { + var self = this; + function listener () { + self.off(name, listener); + callback.apply(ctx, arguments); + }; + + listener._ = callback + return this.on(name, listener, ctx); + }, + + // emit方法 + + emit: function (name) { + // emit 参数 + var data = [].slice.call(arguments, 1); + + // 当前事件下面的所有cb的拷贝 + var evtArr = ((this.e || (this.e = {}))[name] || []).slice(); + var i = 0; + var len = evtArr.length; + + // 执行 + for (i; i < len; i++) { + evtArr[i].fn.apply(evtArr[i].ctx, data); + } + + return this; + }, + + // off 方法 + + off: function (name, callback) { + var e = this.e || (this.e = {}); + // 某个事件 cb list + var evts = e[name]; + var liveEvents = []; + // 如果存在 evts and cb + // 过滤出cb不一致的 + // 这里可以用filter改造 + + // const liveEvents = evts.filter(item => item.fn != callback && item.fn._ != callback /* 用于once的取消订阅 */) + if (evts && callback) { + for (var i = 0, len = evts.length; i < len; i++) { + if (evts[i].fn !== callback && evts[i].fn._ !== callback) + liveEvents.push(evts[i]); + } + } + + // 将过滤掉的重新赋值 + // 或则直接删除掉eventName + (liveEvents.length) + ? e[name] = liveEvents + : delete e[name]; + + return this; + } +}; + +module.exports = E; +module.exports.TinyEmitter = E; ``` -以上的栗子,能够让我们减少对DOM的操作,提升性能,发现这段代码的事前消耗更低,因为只取得了一个 DOM 元素,只添加了一个事件处理程序。虽然对用户来说最终的结果相同,但这种技术需要占用的内存更少。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术。 + +以上的栗子,能够让我们减少对 DOM 的操作,提升性能,发现这段代码的事前消耗更低,因为只取得了一个 DOM 元素,只添加了一个事件处理程序。虽然对用户来说最终的结果相同,但这种技术需要占用的内存更少。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术。 最后,事件种类太多,使用的难易程度也不尽相同;在使用事件时,需要考虑如下一些内存与性能方面的问题。 1.有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户感觉页面反应不够灵敏。 2. 建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量。 - 3. 建议在浏览器卸载页面之前移除页面中的所有事件处理程序。 - 有错误的地方或者不足的地方欢迎指正。 diff --git "a/posts/nextjs\345\274\200\345\217\221\346\255\244\351\241\271\347\233\256\347\232\204\350\256\260\345\275\225.mdx" "b/posts/nextjs\345\274\200\345\217\221\346\255\244\351\241\271\347\233\256\347\232\204\350\256\260\345\275\225.mdx" new file mode 100644 index 0000000..a35b2d4 --- /dev/null +++ "b/posts/nextjs\345\274\200\345\217\221\346\255\244\351\241\271\347\233\256\347\232\204\350\256\260\345\275\225.mdx" @@ -0,0 +1,1864 @@ +--- +title: NextJs学习-以及项目实战使用NextJs开发个人博客 +date: 2024-08-19 +key: "use-nextjs-develop-blog" +categories: 项目 +tags: NextJs,Postgresql,Nginx,Docker,PM2,seo +keywords: nextjs 博客 docker nginx pm2 全栈项目 Postgresql seo +description: 本文详细介绍了 NextJs 框架的众多特性和相关技术要点,包括其基本框架结构、路由、数据请求、渲染方式、客户端组件、样式优化、主题实现、滚动优化、数据库安装与配置、Mongoose 数据模型以及登录方式, seo等内容。 +--- + +import TOCInline from "@/components/TOCInline"; + + + +## Nextjs 是什么 + +[NextJs](https://nextjs.org/docs/getting-started/installation) 是一个基于 React 的开源框架,用于构建生产级别的 React 应用程序。它为开发人员提供了一套丰富的工具和约定,使得创建高性能、可扩展的 Web 应用程序变得更加容易。 + +## Next.js 的主要特性: + + **服务器端渲染 (SSR):** + + - 提高首屏加载速度,改善用户体验。 + - 有利于 SEO,搜索引擎可以更好地抓取页面内容。 + + **静态站点生成 (SSG):** + + - 将整个应用程序或部分页面预渲染成静态 HTML 文件。 + - 适用于数据变化不频繁的网站,提供极快的加载速度。 + + **API 路由:** + + - 内置 API 路由功能,可以轻松创建 RESTful API 或 GraphQL API。 + + **文件系统路由:** + + - 根据文件系统结构自动生成路由,简化路由配置。 + + **图像优化:** + + - 内置图像优化功能,自动优化图片大小和格式,提升页面加载速度。 + + **TypeScript 支持:** + + - 原生支持 TypeScript,提供静态类型检查,提高代码质量。 + + **自定义服务器:** + + - 可以自定义服务器,满足各种复杂场景的需求。 + + **插件系统:** + + - 丰富的插件生态系统,可以扩展 Next.js 的功能。 + +## 安装 + +使用官网推荐的方式安装,执行 `npx create-next-app@latest`,输入项目名称,选择相关options选项就可以完成项目的初始化。如下 +![img](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/mx_screencap_20240730_093007.png) +可以看到,我们可以选择是否使用 `ts`, `eslint`, `tailwindcss`等等。 + +## 锁定引擎 + +在 `package.json`中添加 `"engines": { "node": ">=18.0.0", "pnpm": ">=9.0.0" }` 这样可以限制启动的 `node`版本和 `pnpm`版本,防止出现兼容性问题。 + +## 启动 + +`pnpm dev`,看到这个界面就是启动成功了![SUCCESS](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/next.png) + +> [pnpm/yarn/npm的区别](https://juejin.cn/post/7286362110211489855?from=search-suggest) + +## 路由 + +### 创建路由 + +nextjs中的路由采用的是 `约定式路由`,根据文件的配置自动生成,我们可以看一下默认生成的文件结构。![目录结构](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/mx_screencap_20240730_104511.png) + +`.next`目录是运行的文件,`app`是路由的目录,`public`可以用于存放一些静态资源。 + +所有的路由文件都放在 `app`中,`page`就是内容页,`layout`就是布局页面的模板,`error`是错误页面,`loading`是加载页面,`not-found`是404页面。比如我们需要创建一个 `dashborad`页面,只需要在 `app`下新增一个 `dashboard/page.tsx`即可。 + +带params的路由格式是 `[id].tsx`,如下。 + +![路由结构](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/layout.png) + +**app/layout**: 等于react中的main.ts,app.ts,以及Vue中的App.vue,全局的布局,可以用来加载全局的样式,字体,metadata等等。 + +```js +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
头部
+
{children}
+ + + ); +} +``` + +**[folder]/layout**: 单个菜单下的布局,类似vue/react中的二级路由 + +**page**:页面内容 + +### 路由跳转 + +这里有四种方式来实现路由的跳转。 + +- 使用 `next/link`内置组件 + + ```js + import Link from 'next/link' + + + ``` + +- 使用 `useRouter`钩子函数,适用于 `client` + + ```js + import { useRouter } from 'next/navigation' + export default function Page() { + const router = useRouter() + return ( + + ) + } + ``` + +- 对于 `server component` + + ```js + + import { redirect } from 'next/navigation' + + async function fetchTeam(id: string) { + const res = await fetch('https://...') + if (!res.ok) return undefined + return res.json() + } + + export default async function Profile({ params }: { params: { id: string } }) { + const team = await fetchTeam(params.id) + if (!team) { + redirect('/login') + } + // ... + } + ``` +- 使用原生 `history` + + ```js + 'use client' + + import { useSearchParams } from 'next/navigation' + + export default function SortProducts() { + const searchParams = useSearchParams() + + function updateSorting(sortOrder: string) { + const params = new URLSearchParams(searchParams.toString()) + params.set('sort', sortOrder) + window.history.pushState(null, '', `?${params.toString()}`) + } + + return ( + <> + + + + ) + } + ``` + +### 路由参数传递和获取 + +对于服务端组件来说,主函数的参数中自动注入,`params`和 `searchParams`,例如我们访问这个链接的时候 +`http://localhost:3001/dashboard/1?a=1` + +此时在主函数的 `datas`, 他的内容就是 `{ params: { id: '1' }, searchParams: { a: '1' } }` + +```js +// { params: { id: '1' }, searchParams: { a: '1' } } +export default async function DashboardPage(datas: any) { + const data = await getData(); + return ( +
+
+ 这是仪表盘页面 {datas.params.id} {JSON.stringify(data)}{" "} +
+
+ ); +} +``` + +对于客户端组件来说,也可以用上面的方式来获取,初次除此以外还可以通过 `useParams`和 `useSearchParams`来获取。 + +```js +import { useParams, useSearchParams } from "next/navigation"; + +// { params: { id: '1' }, searchParams: { a: '1' } } +export default function DashboardPage() { + const params = useParams(); + const searchParams = useSearchParams(); + return ( +
+
这是仪表盘页面 {params.id}
+
+ ); +} +``` + +### API路由 + +由于nextjs是运行在服务端的,所以他也能实现后端的服务,实现的方式是在 `app`下创建一个 `api`目录。在 `api`目录下插入一个 `dashboard/metrics`目录,然后创建一个 `route.ts`,这样子就实现了一个api路由了。请求路径是 `/dashboard/metrics`,方法为 `get`的请求。 + +```js +import { responseHandler } from "@/lib/fetch"; + +export async function GET(request: Request) { + return responseHandler({ + rate: '100%', + rate2: '100%', + rate3: '100%' + }); +} + +``` + +### 文件约定 + +**error.js**:当页面UI加载错误的时候会显示此页面; + +**not-found.js**: 当页面不存在的时候显示此页面; +例如当我们访问这个链接 `http://localhost:3001/xx`,此时就会渲染此页面。 + +**loading.js**: 当组件加载的时候就会触发这个动画,使用[Suspense](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming)实现 + +**middleware.js**: 中间件,常用于来做鉴权,拦截, 重写和重定向,自定义头部,缓存等等。 + +```js + + // middleware.js + export async function middleware(req) { + const token = req.cookies.token; + if (!token) { + return NextResponse.redirect(new URL('/login', req.url)); + } // 验证 token + return NextResponse.next(); + } + + // 配置匹配的路由,符合才走中间件 + export const config = { matcher: ['/protected/:path*'] }; +``` + +除 `middleware`外,其他都支持全局和局部。 + +## 数据请求 + +- 在服务端组件中,使用 `fetch`请求,而且自带缓存,`Post`请求不缓存,通过在 `fetch`中的配置,默认是 + `{ cache: 'force-cache' }`。 + + 如果需要重新验证数据的一致性,可以设置 `{ next: { revalidate: 3600 } }`,这样每小时会重新验证一次,或者在 `page/layout.ts`设置 `export const revalidate = 3600` + +```js + export const revalidate = 3600 + + async function getData() { + const res = await fetch('http://localhost:3000/api/dashboard/metric') + + if (!res.ok) { + throw new Error('Failed to fetch data') + } + + return res.json() + } + + export default async function Page() { + const data = await getData() + + return
+ 指标是: {{ JSON.stringify(data.data) }} +
+ } +``` + +也可以不缓存,通过设置 `{ cache: 'no-store' }`,也可以按需重新验证,参考此文档[on-demand-revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#on-demand-revalidation),也可以在文件头部 +`export const dynamic = 'force-dynamic';`这样每次都会新的。 + +- Server Actions and Mutations + +## 渲染 + +### 服务端组件 + +在app下的 + +服务端组件渲染有三种方式。 + +1. Static Rendering(静态渲染) + > 这是默认的渲染方式,在build的时候就会完成数据的请求和页面组装,比如一个项目的首页 + > +2. Dynamic Rendering(动态渲染) + > 在用户请求的时候渲染完成,比如一个订单的详情页,根据不同的ID来渲染不同的数据 + > +3. Streaming(流式渲染) + > 简单来说就是将一整个 HTML 脚本文件通过切成一小段一小段的方式返回给客户端,客户端收到每一段内容时进行分批渲染。这样的方式相较于传统的服务端一次性渲染完成整个 HTML 内容进行返回,在视觉上大大减少了 TTFB 以及 FP 的时间,在用户体验上更好。主要原理是基于 `Suspense/Lazy` 进行异步渲染组件,这样我们可以把一些不变的静态数据和动态数据拆分成多个组件,父组件中用 `Suspense`包裹,然静态数据先渲染,从而提高 `TTFB(time to first byte)`和FP `(first paint)`指标。 + > + +```js +// Metric1.tsx +function getDate(): Promise { + return new Promise((resolve) => + setTimeout(() => { + resolve("This is Great."); + }, 3000) + ); +} + +export default async function Metric1() { + const data: string = await getDate(); + + return ( + <> +
{data}
+ + ); +} + +``` + +```js +// Metric2.tsx +function getDate(): Promise { + return new Promise((resolve) => + setTimeout(() => { + resolve("This is Great."); + }, 3000) + ); +} + +export default async function Metric1() { + const data: string = await getDate(); + + return ( + <> +
{data}
+ + ); +} + +``` + +```js +import Metric1 from "@/components/metric1"; +import Metric2 from "@/components/metric2"; +import { Suspense } from "react"; + +async function getData() { + const res = await fetch("http://localhost:3001/api/dashboard/metric/"); + // The return value is *not* serialized + // You can return Date, Map, Set, etc. + + if (!res.ok) { + // This will activate the closest `error.js` Error Boundary + throw new Error("Failed to fetch data"); + } + + return res.json(); +} + +export default async function DashboardPage(datas: any) { + const data = await getData(); + return ( +
+
+ 这是仪表盘页面 {datas.params.id} {JSON.stringify(data)}{" "} +
+ + Loading metric1...
}> + + + + Loading metric2...
}> + + +
+ ); +} + +``` + +这时候我们就可以看到主体内容先渲染了,metric1,和metric2在loading,等待数据获得后再渲染。 +![streaming](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/streaming.png) + +#### 优势 + +在服务器上执行渲染工作有几个好处,包括: + +**数据提取**:服务器组件允许您将数据提取移动到更接近数据源的服务器。这可以通过减少获取渲染所需数据所需的时间以及客户端需要发出的请求数量来提高性能。 + +**安全**:服务器组件允许您将敏感数据和逻辑保留在服务器上,例如令牌和API密钥,而不会面临将它们暴露给客户端的风险。 + +**缓存**:通过在服务器上呈现,结果可以被缓存并在后续请求和跨用户中重复使用。这可以通过减少每次请求上完成的渲染和数据获取量来提高性能并降低成本。 + +**性能**:服务器组件为您提供了额外的工具来从基线优化性能。例如,如果您从完全由客户端组件组成的应用程序开始,则将UI的非交互式部分移动到服务器组件可以减少所需的客户端JavaScript量。这对于互联网速度较慢或设备功能较弱的用户来说是有利的,因为浏览器需要下载、解析和执行的客户端JavaScript较少。 + +**初始页面加载和第一个内容绘制(FCP)**:在服务器上,我们可以生成HTML以允许用户立即查看页面,而无需等待客户端下载、解析和执行渲染页面所需的JavaScript。 + +**搜索引擎优化和社交网络共享性**:搜索引擎机器人可以使用渲染的HTML来索引页面,社交网络机器人可以使用渲染的HTML为您的页面生成社交卡预览。 + +**流媒体**:服务器组件允许您将渲染工作拆分为块,并在准备好后将其流媒体传输给客户端。这允许用户更早地查看页面的部分内容,而不必等待整个页面在服务器上呈现。 + +### 客户端组件(Client Components) + +使用客户端组件的方式也很简单,首行写 `use client`即开启 + +```js +'use client' + +import { useState } from 'react' + +export default function Counter() { + const [count, setCount] = useState(0) + + return ( +
+

You clicked {count} times

+ +
+ ) +} +``` + +客户端组件并非仅仅在客户端渲染,也可以在服务端渲染,这取决于是否是全页面加载(Full page load),还是页面中导航过去的([Subsequent Navigations](https://nextjs.org/docs/app/building-your-application/rendering/client-components#subsequent-navigations))。 + +如果是全页加载的,即首次加载或刷新,这时候也是通过服务端渲染的而导航过去的,这时候是客户端渲染的。 + +#### 优势 + +- 可以使用相关的API和交互 `useEffect`,`state`,`event listener`等 +- 可以使用浏览器API + +## 样式 + +样式在初始化项目的时候安装了[tailwindcss](https://www.tailwindcss.cn/),可以看文档。 + +## 优化 + +### 字体 + +nextjs中使用next/font来加载谷歌字体,而不是在css到声明字体,因为它帮我们优化了字体的加载,很方便使用各种各样的字体。官方推荐使用[可变字体](https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_fonts/Variable_fonts_guide),这里是[字体库](https://fonts.google.com/variablefonts) + +如下,我们可以看到引入了两种字体,字体中可以设置 `子集`,样式等options. + +```js +import Link from "next/link"; +import { Inter, Roboto_Mono } from "next/font/google"; + +// If loading a variable font, you don't need to specify the font weight +const inter = Inter({ + subsets: ["latin"], + display: "swap", +}); + +export const roboto_mono = Roboto_Mono({ + subsets: ["latin"], + weight: ["700"], + display: "swap", + variable: "--Roboto_Mono", +}); +export default function Home() { + return ( +
+
这是头部
+
这是一种字体
+
这是另一种字体
+
+ dashboard +
+ +
这是footer
+
+ ); +} + +``` + +同时也可以加载本地字体。 + +```js +import localFont from 'next/font/local' +const myFont = localFont({ src: './my-font.woff2', display: 'swap',}) +``` + +`display`就是“font-display”专用于 @font-face 指令的描述符,它可以取如下几个值: + +- auto 。这个是 font-display 的默认值,字体的加载过程由浏览器自行决定,不过基本上和取值为 block 时的处理方式一致。 +- block 。在字体加载前,会使用备用字体渲染,但是显示为空白,使得它一直处于阻塞期,当字体加载完成之后,进入交换期,用下载下来的字体进行文本渲染。不过有些浏览器并不会无限的处于阻塞期,会有超时限制,一般在 3 秒后,如果阻塞期仍然没有加载完字体,那么直接就进入交换期,显示后备字体(而非空白),等字体下载完成之后直接替换。 +- swap 。基本上没有阻塞期,直接进入交换期,使用后备字体渲染文本,等用到的字体加载完成之后替换掉后备字体。 +- fallback 。阻塞期很短(大约100毫秒),也就是说会有大约 100 毫秒的显示空白的后备字体,然后交换期也有时限(大约 3 秒),在这段时间内如果字体加载成功了就会替换成该字体,如果没有加载成功那么后续会一直使用后备字体渲染文本。 +- optional 。与 fallback 的阻塞期一致,但是没有交换期,如果在阻塞期的 100 毫秒内字体加载完成,那么会使用该字体,否则直接使用后备字体。这个就是说指定的网络字体是可有可无的,如果加载很快那么可以显示,加载稍微慢一点就不会显示了,适合网络情况不好的时候,例如移动网络。 + +### 图片 + +#### 优点 + +Image组件,Image是在img得基础上的封装,主要增强了以下功能。 + +**大小优化**:使用 WebP 和 AVIF 等现代图像格式,自动为每台设备提供正确大小的图像。 + +**视觉稳定**:防止偏移,有效优化[Cumulative Layout Shift (CLS)](https://nextjs.org/learn-pages-router/seo/web-performance/cls)累计布局偏移量。 + +**更快的加载速度**:进入视口后在进行加载,更快的页面加载速度。 + +**自适应**:可以设置优先级,压缩质量,包括宽高,即使图片是在服务器上。 + +```js + +cover_img +``` + +#### 安全 + +可以设置指定域名的远程图片,在 `next.config.mjs` + +```js + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + }, + { + protocol: "https", + hostname: "**.myqcloud.com", + }, + { + protocol: "https", + hostname: "**.xitu.io", + }, + { + protocol: "https", + hostname: "**.byteimg.com", + }, + ], + }, +``` + +## 主题的实现 + +通用的主题的实现有以下几种方式。 + +1. 使用CSS变量来实现 +2. 使用CSS-in-JS实现主题切换 +3. 引入不同的CSS文件来实现主题的切换 +4. 使用CSS预处理器来实现主题 + +具体实现可以参考这篇文章[如何实现前端页面主题切换:多种方法详解](https://super-super.cn/blog/66aa059a9822e0e619b587c7) + +我们这里使用css变量的方式结合[next-themes](https://github.com/pacocoursey/next-themes#readme)来实现,`暗黑`,`明亮`,`跟随系统`三种主题。 + +### 安装 + +```bash +pnpm add next-themes +``` + +### 增加主题变量 + +然后修改 `global.css` + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + /* 背景色 */ + --background-color: #111827; + /* 文字颜色 */ + --text-color: white; +} + +body { + background-color: var(--background-color); + color: var(--text-color); + line-height: 1.5; + font-size: 14px; +} + +// 明亮主题 +.light { + --background-color: #fff; + --text-color: #111827; +} +// 暗黑主题 +.dark { + --background-color: #111827; + --text-color: #fff; +} + +// 系统自适应暗黑 +@media (prefers-color-scheme: dark) { + :root:not(.dark):not(.light) { + --background-color: #111827; + --text-color: #fff; + } + +} + +// 系统自适应明亮 +@media (prefers-color-scheme: light) { + :root:not(.dark):not(.light) { + --background-color: #fff; + --text-color: #111827; + } + +} +``` + +### 实现ThemeProvider + +然后在 `provider`目录创建 `ThemeProvider`. + +```js +"use client"; + +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type { ThemeProviderProps } from "next-themes/dist/types"; + +export default function ThemeProvider({ + children, + ...props +}: ThemeProviderProps) { + return {children}; +} + +``` + +最后在 `app/layout.txs`中使用,设置 `attribute`用class,默认主题用暗黑模式,支持跟随系统,mac支持,windows不支持。 + +```js + +
头部
+
{children}
+
+ +``` + +### 主题切换组件 + +然后就可以创建一个切换主题的组件,通过 `setTheme`来实现主题的设置。 + +```js +"use client"; +import { useTheme } from "next-themes"; + +const ThemeChanger = () => { + const { theme, setTheme } = useTheme(); + + return ( +
+
+ 现在主题是: {theme} +
+ + {" "} + + {" "} + + +
+ ); +}; + +export default ThemeChanger; + +``` + +到此,我们即完成了切换主题。 + +## 滚动优化 + +此处我们我使用了 `lenis`来实现滚动的优化,代替默认的滚动。 + +### 安装 + +```bash +pnpm add lenis +``` + +### 新建Provider + +创建 `provider/leniProvider.tsx` + +```ts +"use client"; +import { ReactLenis } from "lenis/react"; +import { ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +export default function LenisProvider({ children }: Props) { + return {children}; +} + +``` + +创建 `provider/scrollProvider.tsx` + +```js +"use client"; + +import { useLenis } from "lenis/react"; +import { createContext, ReactNode, useState } from "react"; + +interface ScrollValue { + scrollY: number; +} + +export const ScrollContext = createContext({ + scrollY: 0, +}); + +interface ScrollProviderProps { + children: ReactNode; +} + +export const ScrollProvider = ({ children }: ScrollProviderProps) => { + const [scrollY, setScrollY] = useState(0); + + const [mounted, setMounted] = useState(false); + + useLenis(({ scroll }: any) => { + setMounted(true); + setScrollY(scroll); + }); + if (!mounted) return null; + + return ( + + {children} + + ); +}; + +``` + +### 使用 + +在 `app/layout.tsx`中使用。 + +```js +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import ThemeProvider from "@/provider/themeprovider"; +import dynamic from "next/dynamic"; +import LenisProvider from "@/provider/LenisProvider"; +import { ScrollProvider } from "@/provider/scrollProvider"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +const ThemeChanger = dynamic(() => import("@/components/theme"), { + ssr: false, +}); + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + +
头部
+ + +
{children}
+
+ +
+
+ + + ); +} + + +``` + +此时我们可以通过 `Scrollprivider`来访问 `scrollY`,以及progress等等 + +在 `dashboard/page.tsx`中我们使用context来inject进来scrollY。 + +```js +"use client"; + +import { ScrollContext } from "@/provider/scrollProvider"; +import Link from "next/link"; +import { useContext } from "react"; + +export default function DashboardPage({ + children, +}: { + children: React.ReactNode; +}) { + const { scrollY } = useContext(ScrollContext); + return ( +
+
+ 我们去仪表盘1{scrollY} + +
scrollY: {scrollY}
+
+
{children}
+
+ ); +} + +``` + +## 数据库/Docker安装 + +我所使用的是腾讯云的服务器,系统为 `OpenCloudOS Docker版本`,所以不需要安装,只需要安装 `mongodb`即可。 + +### Docker命令 + +这里说明一下docker的常用命令。 + +- `systemctl start docker` 启动 +- `systemctl stop docker` 启动 +- `systemctl enable docker` 开机启动 +- `docker search mongodb` 查询镜像 +- `docker pull mongodb` 拉取镜像 +- `docker run -d --name=xxx xxx mongodb:latest` 基于某镜像创建一个容器 +- `docker ps` 查看运行中的容器 +- `docker stop container_id/name` 停止运行中的容器 +- `docker start container_id/name` 启动运行中的容器 + 这些是常用的命令。 + +### MongoDb + +所以此处我们需要安装mongodb的话,就需要执行命令。 + +`docker pull mongo` + +在启动之前,我们需要把数据挂载到本地服务器,这样容器重启数据也不会丢失,还有数据的备份,日志,配置等等。 + +```bash +// 数据保存到/data/mdb文件夹中 +mkdir -p /data/mdb + +// 数据备份 +mkdir -p /data/backup/mongodb + +// 日志 +mkdir -p /data/mdblog + +// 配置 +mkdir -p /data/mongo_conf +``` + +其它的东西不用动,我这边加了一个配置,需要设置一个bindIP和auth来设置需要密码和指定IP可访问,否则会被黑客勒索。 + +```bash +# Where and how to store data. +storage: + dbPath: /data/mdb + journal: + enabled: true +systemLog: + destination: file + logAppend: true + path: /data/mdblog/mongod.log + +# network interfaces +net: + port: 3009 + bindIp: 127.0.0.1,150.109.25.213 + +#auth + +auth:true + +``` + +现在我们就可以启动moogodb了。 + +```bash +docker run --name mongo --restart=always -p 3009:27017 -v /data/mdb:/data/db -v /data/backup/mongodb:/data/backup -v /data/mdblog:/data/log -v /data/mongo_conf:/data/conf -d mongo +``` + +// -v映射目录 + +// --name名称 + +// 3009:27017端口映射服务器的3009端口映射到容器的的27017端口 + +![SUCCESS](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/WX20240731-232403%402x.png) + +这时候我们看到了已经启动成功了。 + +### 创建管理员账户 + +进入容器。 + +```BASH +docker exec -it mongo mongosh +``` + +![LOGIN](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/login_success.png) + +显示这样基本上就表示登录成功。 + +查看数据库。 + +```bash +show dbs; +``` + +这是可以看到现有的db + +![show_db](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/show.png) + +然后进入admin `use admin`. + +创建管理员账户。 + +```bash +admin> db.createUser({ user: 'admin', pwd: 'admin', roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] }); +{ ok: 1 } +admin> db.auth('admin', 'admin') +{ ok: 1 } +``` + +这样就表示创建成功且auth通过,这时候就可以测试连接了。`mongodb://admin:admin@host:port/admin?authMechanism=DEFAULT` + +### 创建DB + +这里我们创建一个 `blog`的db. + +```bash + use blog // 没有会自动创建 + + db.createCollection('articles') // 创建一个集合 + + db.articles.insertOne({title: 'xxx'}) // 插入数据 +``` + +![查看数据](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/mongo_DB.png) + +可以看到数据已经插入了。 + +### mongoose + +[Mongoose](https://mongoosejs.com/) 提供了一种直接的、基于架构的解决方案来为您的应用程序数据建模。它包括内置的类型转换、验证、查询构建、业务逻辑挂钩等,开箱即用。 + +#### 安装 + +```bash +pnpm add mongoose --save +``` + +创建一个工具函数 `lib/mongoose.ts` + +```js +import mongoose from "mongoose"; + +const connectMongo = async () => + await mongoose.connect(process.env.MONGO_URI as string, { + autoCreate: true, + }); +// MONGO_URI 写在环境变量中 +export default connectMongo; + +``` + +#### 数据模型 + +我们先增加一个 `USER`,相关规则可以查看此处[schematypes](https://mongoosejs.com/docs/schematypes.html) + +首先定义一个user的 `DTO`和 `VO`在 `types/user.ts` + +```ts +export interface USER_DTO { + name: string; + image: string; + email: string; + password: string; + created_at?: Date; + updated_at?: Date; +} + +export interface USER_VO { + name: string; + image: string; + email: string; + created_at: Date; + updated_at: Date; +} + +``` + +```ts +import { Schema, model, models } from "mongoose"; +import { USER_DTO } from '@/types/user + +const userSchema = new Schema({ + name: { + type: String, + required: true, + trim: true, + max_length: 50, + }, + image: { + type: String, + required: true, + default: "https://blog-1302483222.cos.ap-shanghai.myqcloud.com/images.jpg", + }, + email: { + type: String, + required: true, + unique: true, + lowercase: true, + match: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*\.(\w{2,})+$/, + }, + password: { + type: String, + required: true, + minlength: 8, + }, + created_at: { + type: Date, + default: Date.now, + }, + updated_at: { + type: Date, + default: Date.now, + }, +}); + +const User = models?.User || model("User", userSchema); +export default User; + +``` + +#### 常用API + +可以熟悉以下常用的语法。 + +```bash + +**增**:Modal.save() + +**删**:Modal.deleteOne({name: 'xxx'}) + +**改**:Modal.findByIdAndUpdate(id, { name: 'jason bourne' }, options) + +**查**::Modal.deleteOne({ name: 'xxx }) +``` + +每种操作都有很多种方法,可以按需使用,具体文档查找, [model操作](https://mongoosejs.com/docs/api/model.html) + +到这里我们就基本完成了数据库的部分,可以进入到下一阶段登录。 + +## 登录 + +登录使用[next-auth](https://next-auth.js.org/),这里可以很方便的让我实现第三方的登录。 + +### NEXT-AUTH安装 + +```bash +pnpm add next-auth +``` + +### 初始化配置 + +新建配置,在 `lib`目录中创建 `auth_option.ts`,这边我打算使用自定义登录以及github和google登录,微信登录看了一下需要注册企业,个人的话也要交钱,所以就暂时没有接入,短信登录需要接入短信服务,也要收费。 + +#### 申请Github登录 + +进入 GitHub 之后,打开 [Settings](https://github.com/settings) 中的 [Developer Settings](https://github.com/settings/apps),点击左侧的 [OAuth Apps](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/WX20240731-224830%402x.png) 后,再点击右边的按钮 **New OAuth App**,创建一个新的配置。 + +![New OAuth App](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/WX20240731-224830%402x.png),然后一步步往下走新建成功后,即可以拿到 `CLIENT_ID`和 `Secret`,可以把这些内容维护到 `env`文件中 + +然后完成以下配置。 + +```js +import GitHubProvider from "next-auth/providers/github"; +import GoogleProvider from "next-auth/providers/google"; +import CredentialsProvider from "next-auth/providers/credentials"; +import User from "models/user"; +import connectMongo from "@/lib/mongoose"; +import { decrypt, encrypt } from "@/lib/crypto"; +import { AuthOptions } from "next-auth"; + +export const authOptions: AuthOptions = { + secret: process.env.SECRET_KEY, // 密钥用来加密token + // adapter: PrismaAdapter(MongoPrisma as PrismaClient) as Adapter, + debug: true, // 可以查看登录时候的日志 + providers: [ + GitHubProvider({ + clientId: process.env.GIT_CLIENT_ID as string, + clientSecret: process.env.GIT_CLIENT_SECRET as string, + httpOptions: { + timeout: 100000, + }, + // 获取到用户profile后可以存储到数据库 + async profile(profile) { + try { + // 连接数据库 + await connectMongo(); + + // 查询是否存在 + const existingUser = await User.findOne({ email: profile.email }); + + + // 如果存在就更新以下名字和图像 + if (existingUser) { + // Update existing user + const res = await User.findByIdAndUpdate(existingUser._id, { + name: profile.name || profile.login, + image: profile.avatar_url, + }); + return existingUser; + } + + /// 如果不存在就新增一个 + const newUser = new User({ + name: profile.name || profile.login, + email: profile.email, + image: profile.avatar_url, + password: encrypt(profile.id.toString()), // Use GitHub ID as password + }); + + await newUser.save(); + + return newUser; + } catch (e: any ) { + console.log(e.message); + } + }, + }), + GoogleProvider({ + clientId: process.env.GOOGLE_ID as string, + clientSecret: process.env.GOOGLE_SECRET_KEY as string, + httpOptions: { + timeout: 100000, + }, + }), + CredentialsProvider({ + name: "Credentials", + credentials: { + email: { + label: "用户名", + type: "text", + placeholder: "请输入用户名", + }, + password: { + label: "密码", + type: "password", + placeholder: "请输入密码", + }, + }, + + // 自定义登录的鉴权 + async authorize(credentials, req) { + // 出入进来账号和密码 + if (!credentials?.email || !credentials?.password) { + return null + } + await connectMongo(); + // 查询用户 + const user = await User.findOne({ email: credentials.email }); + + if (!user) { + throw Error('用户不存在,请检查邮箱是否正确') + } else { + if (credentials.password === decrypt(user?.password as string)) { + return user + } else { + throw Error('密码不正确,请重新输入') + } + } + }, + }), + ], + // session 有效期 2天 + session: { + strategy: "jwt", + maxAge: 2 * 24 * 60 * 60, + }, + + // session的callback可以修改session传递的数据 + callbacks: { + session: async (data: { session: any; }) => { + return data.session; + }, + }, + + pages: { + signIn: "/", + }, + } +``` + +### 创建路由 + +在 `api`下创建,`[...nextauth]/route.ts` + +```js +import { authOptions } from "@/lib/auth_options"; +import NextAuth from "next-auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; + +``` + +### 编写登录组件 + +通过调用 `signIn`的方法传入不同的参数即可实现对应的登录。 + +```js +"use client"; +import { ChangeEvent, FocusEvent, FormEvent, useEffect, useState } from "react"; +import loginStyle from "./login.module.scss"; +import classnames from "classnames"; +import { motion } from "framer-motion"; +import LoginBox from "./component/LoginBox"; +import { Modal, Divider, Button, Input } from "antd"; +import { post } from "lib/fetch"; +import { signIn } from "next-auth/react"; +import { SwapOutlined } from "@ant-design/icons"; + +interface FormState { + password: string; + email: string; +} + +export default function LoginModal(data: { + open: boolean; + className?: string; + onClose?: () => void; +}) { + const [formState, setFormState] = useState({ + email: "", + password: "", + }); + // + + // 登录 + const login = () => { + signIn("credentials", { ...formState }); + }; + + const confirm = () => { + if (isLogin) { + login(); + } + }; + const [isLogin, setIsLogin] = useState(true); + return ( + <> + +
+
+
+

+ {isLogin ? "登录" : "注册"} + +

+
+
+
+ + ) => { + setFormState({ + email: event.target.value, + password: formState.password, + }); + }} + /> +
+
+ + ) => { + setFormState({ + password: event.target.value, + email: formState.email, + }); + }} + /> +
+ {!isLogin && ( +
+ +
+ + +
+
+ )} +
+ +
+ + +
+
+ + ); +} + +// loginBox + +import { signIn } from "next-auth/react"; +import { Button } from "antd"; +import { GithubOutlined, GoogleOutlined } from "@ant-design/icons"; + +export default function LoginBox() { + const sign = async (type: "github" | "google") => { + await signIn(type, { + callbackUrl: location.origin, + }); + }; + return ( + <> +
+ + + +
+ + ); +} + +``` + +在页面上点击登录,弹出登录窗口。 +![LOGIN](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/github_login.png) + +登录成功后可以看到,返回了信息以及在 `cookie`中写入了token. + +![session](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/session.png) + +![token](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/token.png) + +这样我们就完成了登录,注册还没做可以用一个 `nodemailer`来实现发送验证码,数据存储在 `redis`,缓存有效期1分钟,注册的时候验证以下验证码即可,密码加密存入数据库即可。 + +到此我们的准备工作基本上都做完了,现在就是业务开发了,这边直接不说了,没什么说的。 + +--- + +## 打包 + +直接使用 `pnpm build`即可打包,记住,打包的时候dev模式需要停止。 + +## CI + +这里使用 `GITHUB_ACTION`完成,在 `.github`下新增一个 `nodejs.yml`,添加如下内容。 + +具体操作就是使用 `ssh`登录到服务端。 + +然后执行拉取代码和打包的操作 + +```bash +cd /www/blog-offical && git pull && pnpm install && pnpm build +``` + +```yaml +name: deploy +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: executing remote ssh commands + + uses: appleboy/ssh-action@master + with: + host: ${{secrets.DEPLOY_HOST}} + username: ${{secrets.DEPLOY_USER}} + password: ${{secrets.DEPLOY_PASS_WORD}} + script: cd /www/blog-offical && git pull && pnpm install && pnpm build && pm2 stop all && pm2 delete blog && pm2 start --name blog npm -- run start + +``` + +## CD + +构建完成了,我们需要部署,部署的话我们采用 `nginx`来作为web服务器,使用 `pm2`来管理进程,确保稳定性。 + +### Nginx + +一般linux系统安装,是通过 `yum install nginx`来完成,安装目录在 `/etc/nginx` + +这是Nginx的配置,开启了转发,http2,以及跨域问题,以及 `https` https的配置需要申请证书,一般都有免费的,只是时间比较短,需要经常更换。 + +```nginx + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; +} + +http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Load modular configuration files from the /etc/nginx/conf.d directory. + # See http://nginx.org/en/docs/ngx_core_module.html#include + # for more information. + include /etc/nginx/conf.d/*.conf; + + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + root /usr/share/nginx/html; + # Load configuration files for the default server block. + include /etc/nginx/default.d/*.conf; + + location / { + # 添加以下配置以启用 CORS + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT'; + add_header 'Access-Control-Allow-Headers' 'Origin, Authorization, Accept, Content-Type, X-Requested-With'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT'; + add_header 'Access-Control-Allow-Headers' 'Origin, Authorization, Accept, Content-Type, X-Requested-With'; + + proxy_pass http://localhost:3000; # 将请求转发到本地主机的 3000 端口 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + error_page 404 /404.html; + location = /40x.html { + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + } + } + +# Settings for a TLS enabled server. + + server { + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2 default_server; + server_name _; + root /usr/share/nginx/html; + + ssl_certificate "/www/cer/www.super-super.cn_bundle.pem"; + ssl_certificate_key "/www/cer/www.super-super.cn.key"; + ssl_session_cache shared:SSL:1m; + ssl_session_timeout 10m; + ssl_ciphers PROFILE=SYSTEM; + ssl_prefer_server_ciphers on; + + # Load configuration files for the default server block. + include /etc/nginx/default.d/*.conf; + + location / { + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT'; + add_header 'Access-Control-Allow-Headers' 'Origin, Authorization, Accept, Content-Type, X-Requested-With'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE, PUT'; + add_header 'Access-Control-Allow-Headers' 'Origin, Authorization, Accept, Content-Type, X-Requested-With'; + + proxy_pass http://localhost:3000; # 将请求转发到本地主机的 3000 端口 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + error_page 404 /404.html; + location = /40x.html { + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + } + } + +} + +``` + +### 常用命令 + +启动服务 + +``` +nginx +``` + +停止服务 + +``` +nginx -s stop +``` + +重新加载,因为一般重新配置之后,不希望重启服务,这时可以使用重新加载。 + +``` +nginx -s realod +``` + +## 域名解析 + +主要说一下域名解析的过程以及记录值的区别。 + + +## SEO相关 + +### Metadata + +在NextJs中,提供了设置metadata的方式,设置在`page.tsx`或者`layout.tsx`. + +分为静态和动态两种。 + +#### 静态metadata + +静态的metadata直接export一个metada对象即可。 +```js +import type { Metadata, Viewport } from "next"; + +const APP_NAME = "Blog"; +const APP_DEFAULT_TITLE = "汪浩的博客"; +const APP_TITLE_TEMPLATE = "博客"; +const APP_DESCRIPTION = "汪浩(Isaac Wang)的博客,一些关于技术和生活的的记录"; + +export const metadata: Metadata = { + keywords: + "博客,汪浩,Isaac Wang, Javascript, Vue, Css, Nextjs, React, TypeScript, NextJs, NestJs, Nodejs, Docker, web3,区块链", + applicationName: APP_NAME, + title: { + default: APP_DEFAULT_TITLE, + template: APP_TITLE_TEMPLATE, + }, + description: APP_DESCRIPTION, + manifest: "./manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "default", + title: APP_DEFAULT_TITLE, + // startUpImage: [], + }, + formatDetection: { + telephone: false, + }, + openGraph: { + type: "website", + siteName: APP_NAME, + title: { + default: APP_DEFAULT_TITLE, + template: APP_TITLE_TEMPLATE, + }, + description: APP_DESCRIPTION, + }, + twitter: { + card: "summary", + title: { + default: APP_DEFAULT_TITLE, + template: APP_TITLE_TEMPLATE, + }, + description: APP_DESCRIPTION, + }, +}; +``` + +然后就会在页面上得到解析,会被加载到head中。 + +![metadata](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/metadata_static.png) + +#### 动态Metadata + +比如我们一些详情页,希望我们的标题是详情的title,content是详情的内容。这时候我们就可以用,例如博客的详情页面,这时候我们可以先获取到数据,然后使用`generateMetadata`函数来生成动态的metadata,如下: + +```js + + +export async function generateMetadata({ + params, +}: { + params: { id: string }; +}): Promise { + const detail = await getBlogDetail(params.id); + + if (!detail) { + return {}; + } + + return { + title: detail.title, + description: detail.content, + keywords: detail.tags.join(","), + category: detail.categories.join(", "), + abstract: detail.abstract, + creator: "汪浩(isaac wang)", + authors: [ + { url: "https://github.com/wanghao1993", name: "汪浩(isaac wang)" }, + ], + publisher: "汪浩(isaac wang)", + }; +} + +``` + +此时我们就可以在这里看到,刚刚动态配置的metadata。 + +![动态的metadata](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/metadata_static.png) + + +### Sitemap + +sitemap叫站点地图,可帮助搜索引擎更有效地发现您的网页并为其建立索引,也分为动态和静态两种。 + +静态的,可以建立一个文件叫`app/sitemap.xml`,如下。 + +```xml + + + + + https://www.super-super.cn/ + 2024-07-22T07:08:29+00:00 + 1.00 + + + https://www.super-super.cn/blog + 2024-07-22T07:08:29+00:00 + 0.80 + + + https://www.super-super.cn/about + 2024-07-22T07:08:29+00:00 + 0.80 + + +``` + +动态的可以建立一个文件叫`app/sitemap.ts`,如下 + +```js +import { MetadataRoute } from 'next' + +export default function sitemap(): MetadataRoute.Sitemap { + return [ + { + url: 'https://acme.com', + lastModified: new Date(), + changeFrequency: 'yearly', + priority: 1, + }, + { + url: 'https://acme.com/about', + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 0.8, + }, + { + url: 'https://acme.com/blog', + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 0.5, + }, + ] +} +``` + +### 实施开放图谱和 Twitter 卡: + +[OpenGraph](https://segmentfault.com/a/1190000040863000),又叫OG协议,可以简单的看一下介绍。 + +**Twitter 卡片**标签看上去与开放图谱标签相似,基于与开放图谱协议相同的约定。当使用开放图谱协议描述页面上的数据时,很容易生成 Twitter 卡片,而无需复制标签和数据。当 Twitter 卡片处理器在页面上寻找标签时,它会首先检查 Twitter 特定的属性;如果不存在,则会返回受支持的开放图谱属性。它允许在页面上独立定义这两种属性,并最大程度减少描述内容和体验所需的标记复制量。 + +如何定义呢,也是在metadata中定义,openGraph和twitter。 + +```js + openGraph: { + type: "website", + siteName: APP_NAME, + title: { + default: APP_DEFAULT_TITLE, + template: APP_TITLE_TEMPLATE, + }, + description: APP_DESCRIPTION, + }, + twitter: { + card: "summary", + title: { + default: APP_DEFAULT_TITLE, + template: APP_TITLE_TEMPLATE, + }, + description: APP_DESCRIPTION, + }, +}; + +``` + +此时我们可以看到在metadata中多了几个。 + +![og-tw](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/og-tw.png) + +### 语义化标签 + +语义化的标签在SEO中也起到了很关键的作用,平时开发的时候也是需要注意的,这些都会被搜索引擎发现,比如`p`,`article`,`img.alt`等等标签都是会被作为seo的考虑的因素。 + +### robots.txt + +当我们的网站发布时,搜索引擎将会尝试去抓取我们的内容,这个时候`robots.txt`可以规定能够被抓取的范围。 + +```txt +User-Agent: * // 意思是任何搜索引擎都可以 +Allow: / // 允许抓取任何内容 +Disallow: /admin // /admin下的内容不允许 + +Sitemap: https://super-super.cn/sitemap.xml // sitemap的地址 +``` + + +这是google的seo文档,介绍的很详细[Google_SEO](https://developers.google.com/search/docs/fundamentals/seo-starter-guide?hl=zh-cn) + +## 未完成 + +- PWA + - 通过next-pwa,manifast完成PWA +- 埋点 + - 接入百度统计和谷歌统计 +- 响应式 + +- 全局搜索/标签搜索 +- 评论 +- 社交分享 +... diff --git a/posts/prisma.mdx b/posts/prisma.mdx index 7439797..a8b04b4 100644 --- a/posts/prisma.mdx +++ b/posts/prisma.mdx @@ -8,7 +8,11 @@ keywords: prisma,数据库,nextjs description: 初次使用prisma构建 --- -## Prisma初体验 +import TOCInline from "@/components/TOCInline"; + + + +## Prisma 初体验 [prisma](https://www.prisma.io/docs/getting-started) 是一个现代化的 Node.js 和 TypeScript 的 ORM(对象关系映射)库。 @@ -49,7 +53,7 @@ pnpm add prisma - 创建一个名为 prisma 的新目录,其中包含一个名为 schema.prisma 的文件,该文件包含了`prisma schema`以及数据模型 - 创建一个`.env`文件在更目录下 -出现下面的log就初始化成功了,这时候我们可以看到产生了一个prisma目录以及`schema.prisma`文件和`.env`. +出现下面的 log 就初始化成功了,这时候我们可以看到产生了一个 prisma 目录以及`schema.prisma`文件和`.env`. ![prisma-init](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/prisma-init.png?q-sign-algorithm=sha1&q-ak=AKIDrRaYms09JN-8iUaayQJW7tpvkn7KXciZEdcszE6tEgaTHbzJEDonHvDIaZrZhp9r&q-sign-time=1724999654;1725003254&q-key-time=1724999654;1725003254&q-header-list=&q-url-param-list=&q-signature=2805620d429687527db74ffddf112a2a899deae1&x-cos-security-token=k2uU2nfnAZdPkl0nsWsrAuxLJB6vZq1ae18dd34ed7be6d50e8c113d91dc1f5f3009mJHc-APpMxNUNJ1eib6zg1d779NIXA5t_f52SXt5OXut4ozjq5dkU9KA5P13WVFyQiIQW8AwDZz-Rw4XnuKVOzeojSaKXngjErCZkyWbaF-exLXBTwSTYVvTFXbgSNZLZzZw3k_O2UByXp_4V2pmHdUks81Nde34xI3XRZN_5nY3YKG2lvv4CyX_HAZBu&imageMogr2/interlace/0|watermark/2/text/5rGq5rWp/font/c2ltZmFuZ-S7v-Wuiy50dGY/fontsize/24/fill/Izk5MDA2Ng/dissolve/100/shadow/0/gravity/southeast/dx/20/dy/20) @@ -71,11 +75,11 @@ datasource db { 其中一个是`generator client`一个是`datasource db`,`client`是生成客户端代码,`database`是数据库连接信息。 -### 创建Model +### 创建 Model 我用数据库的功能,主要是想做博客的点赞,查看数据等等。 -所以我们需要创建`User`, `Post`的model,还有转发和commont的以后再做,由于我的博客,用的是mdx,所以不需要存储内容等信息。只用给文章生成一个id保存到数据库即可,用于对其它数据做关联。 +所以我们需要创建`User`, `Post`的 model,还有转发和 commont 的以后再做,由于我的博客,用的是 mdx,所以不需要存储内容等信息。只用给文章生成一个 id 保存到数据库即可,用于对其它数据做关联。 ```ts model Post { @@ -83,7 +87,7 @@ model Post { blog_key String blog_title String likes_count Int[] - views_count Int[] + views_count Int } model User { @@ -102,11 +106,11 @@ model User { ### 运行数据库迁移 -这里先需要安装`pnpm add @prisma/client`,用来生成prisma客户端。 +这里先需要安装`pnpm add @prisma/client`,用来生成 prisma 客户端。 ![prisma-client-install-and-generat](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/prisma-client-install-and-generate-ece3e0733edc615e416d6d654c05e980.png) -这是prisma官方文档的流程截图,我们只需要执行`npx prisma migrate dev --name init`命令就生成sql文件,在我们的数据prisma目录下,每次变更的话建议执行此命令,`name`参数需要变更,变更内容为本次修改数据库的备注。 +这是 prisma 官方文档的流程截图,我们只需要执行`npx prisma migrate dev --name init`命令就生成 sql 文件,在我们的数据 prisma 目录下,每次变更的话建议执行此命令,`name`参数需要变更,变更内容为本次修改数据库的备注。 ## 使用 @@ -120,9 +124,9 @@ const prisma = new PrismaClient(); export default prisma; ``` -接下来我们可以愉快的使用prisma了。 +接下来我们可以愉快的使用 prisma 了。 -我现在登录模块中使用一下,这样我们就完成了,用户在使用github登录的时候,没有用户就去新增用户,有的话,如果头像和名字发生了变更就去更新。 +我现在登录模块中使用一下,这样我们就完成了,用户在使用 github 登录的时候,没有用户就去新增用户,有的话,如果头像和名字发生了变更就去更新。 ```ts import GitHubProvider from "next-auth/providers/github"; @@ -144,15 +148,15 @@ export const authOptions: AuthOptions = { }, async profile(profile) { try { - // 连接 + // 连接 await prisma.$connect(); // 查询是否有 const existingUser = await prisma.user.findUnique({ where: { email: profile.email }, }); - // 更新 - if ( + // 更新 + if ( existingUser && (existingUser.image !== profile.avatar_url || existingUser.name !== (profile.name || profile.login)) @@ -202,5 +206,4 @@ export const authOptions: AuthOptions = { signIn: "/", }, }; - ``` diff --git "a/posts/\345\205\250\351\235\242\350\247\243\346\236\220\357\274\232\344\275\277\347\224\250TypeScript\345\222\214Rollup\346\236\204\345\273\272JavaScript\345\272\223\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237\345\217\212\345\256\236\350\267\265.mdx" "b/posts/\345\205\250\351\235\242\350\247\243\346\236\220\357\274\232\344\275\277\347\224\250TypeScript\345\222\214Rollup\346\236\204\345\273\272JavaScript\345\272\223\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237\345\217\212\345\256\236\350\267\265.mdx" index 447e40d..5505e79 100644 --- "a/posts/\345\205\250\351\235\242\350\247\243\346\236\220\357\274\232\344\275\277\347\224\250TypeScript\345\222\214Rollup\346\236\204\345\273\272JavaScript\345\272\223\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237\345\217\212\345\256\236\350\267\265.mdx" +++ "b/posts/\345\205\250\351\235\242\350\247\243\346\236\220\357\274\232\344\275\277\347\224\250TypeScript\345\222\214Rollup\346\236\204\345\273\272JavaScript\345\272\223\347\232\204\347\224\237\345\221\275\345\221\250\346\234\237\345\217\212\345\256\236\350\267\265.mdx" @@ -4,10 +4,14 @@ description: description categories: 前端 tags: NPM keywords: npm,rollup,规范,最佳实践,ts,swc -key: 'use-ts-and-rollup-build-a-npm-package' +key: "use-ts-and-rollup-build-a-npm-package" date: 2023-12-19 --- +import TOCInline from "@/components/TOCInline"; + + + ![coverimage](https://blog-1302483222.cos.ap-shanghai.myqcloud.com/mx_screencap_20240722_101317.png) 在本文中,我们将详细探讨如何从零开始搭建一个使用 TypeScript 的 JavaScript 包,涵盖从环境配置到发布的整个过程。通过这一系列的步骤,你将能够理解并实践如何构建一个现代化的前端库或应用。 @@ -264,8 +268,6 @@ pnpm install commitizen cz-conventional-changelog -D 然后我们就可以通过`npm run commit`的命令来提交了。 - - #### 2.2.4 开发启动 新建一个`src`目录新建一个入口文件 main.ts,和 index.html, 执行`npm run dev`,这时候就启动成功了。 diff --git "a/posts/\345\205\263\344\272\216MongoDB\346\225\260\346\215\256\345\272\223\350\242\253\345\213\222\347\264\242.mdx" "b/posts/\345\205\263\344\272\216MongoDB\346\225\260\346\215\256\345\272\223\350\242\253\345\213\222\347\264\242.mdx" index a89f39e..20f7ffd 100644 --- "a/posts/\345\205\263\344\272\216MongoDB\346\225\260\346\215\256\345\272\223\350\242\253\345\213\222\347\264\242.mdx" +++ "b/posts/\345\205\263\344\272\216MongoDB\346\225\260\346\215\256\345\272\223\350\242\253\345\213\222\347\264\242.mdx" @@ -1,13 +1,17 @@ --- -title: 关于MongoDB数据库被勒索 +title: 关于 MongoDB 数据库被勒索 date: 2023-12-19 -key: 'about-mongodb-attack' +key: "about-mongodb-attack" categories: 问题记录 tags: MongoDB -keywords: mongod勒索,默认端口 -description: 记录遇到mongodb被勒索的问题以及解决方法 +keywords: mongod,勒索,默认端口 +description: 记录遇到 mongodb 被勒索的问题以及解决方法 --- +import TOCInline from "@/components/TOCInline"; + + + # 背景 事情是这样的,在服务器上部署了`docker`,使用`docker`启动的`moogodb`,然后经常发现数据不见了。然后也没找到原因,在网上查了。最后发现在数据库下面多了一个`collection`,叫`READ__ME_TO_RECOVER_DATA`。![pic](https://blog-offical-1302483222.cos.ap-guangzhou.myqcloud.com/mx_screencap_20240716_141146.png) diff --git "a/posts/\345\220\204\347\211\210\346\234\254http\345\214\272\345\210\253.mdx" "b/posts/\345\220\204\347\211\210\346\234\254http\345\214\272\345\210\253.mdx" new file mode 100644 index 0000000..0b74aa4 --- /dev/null +++ "b/posts/\345\220\204\347\211\210\346\234\254http\345\214\272\345\210\253.mdx" @@ -0,0 +1,144 @@ +--- +title: http各版本的区别 +date: 2022-1-21 +key: "difference-of-different-http" +categories: 网络 +tags: Http +keywords: http0.9 http1 http1.1 http2 http3 +description: http0.9 http1 http1.1 http2 http3 区别以及改进 +--- + +import TOCInline from "@/components/TOCInline"; + + + +## 1.http0.9, 1.0, 1.1 2.0 3.0的区别 + +> http0.9 现在已经淘汰,最开始的版本,只支持get请求 + +> http1.0 在0.9的基础上增加一些特性 +>> 1.增加了请求头和相应头 +>> 2.响应对象不再是文本,可以是图片文件等 +>> 3.支持post,head方法 +>> 4.支持长链接,默认还是短连接 +>> 5.增加了缓存机制,expires和since-last-modify + +> http1.1 +> > 1.默认长连接 +> > 2.请求流水线,一个tcp可执行多个请求,不需要重复建立连接 +> > 3.提供范围请求content-range +> > 4.提供HOST +> > 5.增加了一些缓存字段 +> > 6.新增了多个状态码 + +> http2.0 +> > 1.服务端推送 +> > 2.多路复用 +> > 3.头部压缩 +> > 4.二进制分帧 +> > 5.请求优先级 + +=====未完 +> http3.0 +> > 1.采用QUIC +> > 2.解决了队头阻塞,拥塞控制 +> > 3.两个密钥 +> > 4.前向安全问题 + +## 2.tcp和udp的区别 + + 1.TCP需要先建立连接,udp不需要 + 2.tcp点对点,udp随意 + 3.tcp面向字节流,可拆分,udp面向报文不可拆分 + 4.udp主机不需要维护很多的状态 +## 3.http和https的区别 + + 1.http端口是80 https是443 + 2.http是不安全的,明文传输,https传输过程是加密的,比较安全 + 3.https和http链接方式不同,http是无状态的链接,https是由SSL+http协议构建的可进行加密传输,身份认证的网络协议 + +## 4.https原理 + 本质是http+ssl加密。 + 1.首先http向服务端发送请求, + 2.服务端发送证书过来 + 3.客户端验证证书是否合法,有效,生成公钥,公钥加密后传输给服务端 + 4.服务端拿到加密后的消息,然后用私钥解密,拿到密钥 + 5.建立ssl链接,https通道建立 +## 5.三次握手和四次挥手 + 三次握手 + 目的:确定建立起了可靠的连接 + 1.客户端向服务端发送SYN + 2.服务端接收到SYN,发送自己的ACK和SYN + 3.客户端接收到发送自己的ACK + + 四次挥手 + 目的:确保大家完全断开 + 1.客户端发送FIN给服务端,进入FIN_wait状态 + 2.服务端接收到后,发送ACK=1到客户端,进入CLOSE_wait状态 + 3.服务端将FIN置为1,然后进入lact_ack状态 + 4.客户端收到FIN,发送ACK到服务端,服务端关闭,客户端变成Time_wait状态 + 5.等待2个MSL后客户端关闭 +## 6.如何保证不丢包和有序进行 + +HTTP(超文本传输协议)本身并不保证不丢包和有序进行,它是建立在传输层协议(通常是 TCP)之上的应用层协议,主要依靠 TCP 协议来实现可靠的数据传输和有序性。 + +**一、TCP 如何保证不丢包** + +1. **确认和重传机制**: + - TCP 在发送数据时,会为每个发送的数据包分配一个序号。接收方在接收到数据包后,会向发送方发送一个确认(ACK)消息,告知发送方已成功接收到特定序号的数据包。 + - 如果发送方在一定时间内没有收到某个数据包的确认消息,它会认为这个数据包丢失了,并重新发送该数据包,直到收到确认为止。 + +2. **校验和**: + - TCP 会为每个数据包计算一个校验和。接收方在接收到数据包后,也会计算校验和,并与发送方计算的校验和进行比较。如果校验和不匹配,说明数据包在传输过程中可能被损坏,接收方会丢弃该数据包,并通知发送方重新发送。 + +**二、TCP 如何保证有序进行** + +1. **序号机制**: + - 如前所述,TCP 为每个发送的数据包分配一个序号。接收方根据这些序号来对数据包进行排序,确保数据按照发送的顺序被接收和处理。 + - 即使数据包到达接收方的顺序是乱的,接收方也可以根据序号将它们重新排列成正确的顺序。 + +2. **滑动窗口机制**: + - TCP 使用滑动窗口机制来控制数据的发送和接收速度。发送方在发送一定数量的数据包后,会等待接收方的确认消息,然后再根据窗口大小继续发送数据包。 + - 这种机制可以确保发送方不会发送过多的数据,导致接收方无法处理,同时也可以保证数据的有序传输。 + +需要注意的是,虽然 TCP 在很大程度上保证了数据的可靠性和有序性,但在网络环境恶劣或出现严重故障的情况下,仍然可能会出现丢包或乱序的情况。此外,HTTP 本身也可能会因为各种原因(如服务器故障、网络中断等)导致请求失败或响应不完整。在实际应用中,可以通过一些技术手段(如重试机制、错误处理等)来提高 HTTP 通信的可靠性。 + + + +## 7.一个http连接的过程经历哪些步骤 + +一个 HTTP 连接的过程通常经历以下步骤: +1. **DNS 解析**: + - 当在浏览器中输入一个网址时,浏览器首先需要查找该域名对应的 IP 地址。它会按照一定的顺序查找缓存,首先查看浏览器缓存,看是否之前访问过该域名并保存了其 DNS 信息;如果没有,接着查看操作系统缓存、路由器缓存、本地(ISP)域名服务器缓存,若仍然找不到,会向根域名服务器发起搜索,直到找到域名对应的 IP 地址或者确定该域名不存在。 +2. **TCP 连接建立(三次握手)**: + - 浏览器得到 IP 地址后,通过 TCP 协议与 Web 服务器建立连接。 + - 第一次握手:客户端向服务器发送一个带有 SYN(同步)标志位的数据包,请求建立连接,并生成一个随机序列号 `seq` 发送给服务器,表明客户端想要建立连接的请求同步校验。 + - 第二次握手:服务器收到客户端的请求后,返回一个带有 SYN 和 ACK(确认)标志位的数据包。SYN 标志位表示服务器确认建立连接的请求,ACK 标志位表示对客户端序列号的确认。服务器生成一个随机序列号 `seq`,并将客户端的序列号加 1 作为确认号 `ack` 返回给客户端。 + - 第三次握手:客户端收到服务器的响应后,再发送一个带有 ACK 标志位的数据包,确认号为服务器的序列号加 1,表明客户端确认收到了服务器的响应,连接建立完成。 +3. **HTTP 请求发送**: + - 一旦 TCP 连接建立成功,浏览器会向服务器发送 HTTP 请求。请求由请求行、请求头和请求体组成(对于 GET 请求通常没有请求体)。 + - 请求行包含请求方法(如 GET、POST、PUT、DELETE 等)、请求的 URL 和 HTTP 版本。例如:`GET /index.html HTTP/1.1`。 + - 请求头提供关于请求的其他信息,如 `Host`(指定服务器的域名或 IP 地址)、`User-Agent`(浏览器信息)、`Accept`(客户端可接受的响应内容类型)等。 +4. **服务器处理请求**: + - 服务器接收到客户端的 HTTP 请求后,会根据请求的类型和 URL 路由到正确的处理程序。这可能涉及到数据库查询、文件读取或其他业务逻辑的处理。 +5. **HTTP 响应发送**: + - 处理完成后,服务器会返回一个 HTTP 响应给客户端。响应由状态行、响应头和响应体组成。 + - 状态行包含 HTTP 版本、状态码(如 200 表示成功、404 表示资源不存在、500 表示服务器内部错误等)和状态消息。 + - 响应头提供关于响应的其他信息,如 `Content-Type`(响应内容的类型)、`Content-Length`(响应体的长度)等。 + - 响应体包含请求的资源,如 HTML 页面、图片、JSON 数据等。 +6. **TCP 连接关闭(四次挥手)**: + - 如果是 HTTP/1.0 版本,默认情况下每次请求完成后会立即关闭 TCP 连接。但在 HTTP/1.1 及之后的版本中,默认使用长连接(Keep-Alive),可以在一个 TCP 连接上发送多个 HTTP 请求和响应。如果需要关闭连接,会进行四次挥手的过程。 + - 第一次挥手:客户端向服务器发送一个带有 FIN(结束)标志位的数据包,表明客户端想要关闭连接。 + - 第二次挥手:服务器收到客户端的关闭请求后,返回一个带有 ACK 标志位的数据包,确认收到了客户端的关闭请求。此时服务器可能还在发送剩余的数据。 + - 第三次挥手:服务器发送完所有数据后,也会向客户端发送一个带有 FIN 标志位的数据包,表明服务器也想要关闭连接。 + - 第四次挥手:客户端收到服务器的关闭请求后,返回一个带有 ACK 标志位的数据包,确认收到了服务器的关闭请求,连接正式关闭。 + +## 8. 为什么是三次握手,四次行不行? + +从理论上来说,四次握手也是可行的,但不是最有效的方式。 +如果进行四次握手,比如: +- 客户端发送连接请求 SYN。 +- 服务器回复 ACK,表示收到了客户端的连接请求。 +- 服务器再发送自己的连接请求 SYN。 +- 客户端回复 ACK,表示收到了服务器的连接请求。 +- 这样做虽然也能建立连接,但比三次握手多了一次交互,会增加连接建立的时间和网络开销。在正常情况下,三次握手已经能够满足确认双方收发能力和防止无效连接请求的目的,所以没有必要采用四次握手。 \ No newline at end of file diff --git "a/posts/\345\270\270\350\247\201\347\232\204JS\346\211\213\345\206\231.mdx" "b/posts/\345\270\270\350\247\201\347\232\204JS\346\211\213\345\206\231.mdx" new file mode 100644 index 0000000..5357b0b --- /dev/null +++ "b/posts/\345\270\270\350\247\201\347\232\204JS\346\211\213\345\206\231.mdx" @@ -0,0 +1,362 @@ +--- +title: 常见的js手写 +date: 2022-03-13 +key: "js-handerwriting" +categories: JS +tags: JS基础 +keywords: 防抖 节流 new call, apply, bind 发布订阅 instanceOf Promise async/await 深拷贝 数组flatten扁平化 +description: 常见的一些JS手写实现 +--- + +import TOCInline from "@/components/TOCInline"; + + + +## 1.防抖 +作用:停止动作后多久才开始执行函数 +```js +function debounce (fn, delay = 300, imm) { + let timer + + return function () { + if (imm) { + fn.apply(this, arguments) + } + if (timer) { + clearTimeout(timer) + } + timer = setTimeout(() => { + fn.apply(this, arguments) + }, delay) + } +} + +``` +## 2.节流 +多久执行一次 +```js +function throttle (fn, delay = 300) { + let lastTime + return function () { + const now = +new Date() + if (now - lastTime > wait) { + fn.apply(this, arguments) + } + } +} +``` +## 3.new +new操作符做了这些事: + +1.它创建了一个全新的对象。 + +2.它会被执行[[Prototype]](也就是__proto__)链接。 + +3.它使this指向新创建的对象。。 + +4.通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。 +如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用。 +```js + +function MyNew (func, ...args) { + const obj = {} + + if (func.prototype !== null) { + obj.__proto__ = func.prototype; + } + + let res = func.call(obj, ...args) + + if (res && (typeof obj === 'object' || typeof obj === 'function')) { + return res + } + + return obj +} + +``` + +## 4.call, apply, bind + +```js +Function.prototype.call2 = function (ctx = globalThis) { + ctx.fn = this + + const res = ctx.fn(...args) + + delete ctx.fn + + return res +} +``` +## 5.发布订阅 +```js + +function E () {} + +E.prototype = { + event: {}, + on: function (ename, fn) { + if (this.event[ename]) { + this.event[ename].push(fn) + } else { + this.event[ename] = [fn] + } + }, + + remove: function (ename) { + delete this.events[ename] + }, + + emit: function (ename) { + this.events[ename].forEach(fn => { + fn.apply(this, args) + }) + } +} + + +``` +## 6.instanceOf +```js +function myInstanceOf(obj, constructor) { + let proto = Object.getPrototypeOf(obj); + while (proto) { + if (proto === constructor.prototype) { + return true; + } + proto = Object.getPrototypeOf(proto); + } + return false; +} +``` +## 7.Promise + +```js +class MyPromise { + constructor(executor) { + this.state = 'pending'; + this.value = undefined; + this.reason = undefined; + this.onFulfilledCallbacks = []; + this.onRejectedCallbacks = []; + + const resolve = (value) => { + if (this.state === 'pending') { + this.state = 'fulfilled'; + this.value = value; + this.onFulfilledCallbacks.forEach((callback) => callback(this.value)); + } + }; + + const reject = (reason) => { + if (this.state === 'pending') { + this.state = 'rejected'; + this.reason = reason; + this.onRejectedCallbacks.forEach((callback) => callback(this.reason)); + } + }; + + try { + executor(resolve, reject); + } catch (error) { + reject(error); + } + } + + then(onFulfilled, onRejected) { + onFulfilled = typeof onFulfilled === 'function'? onFulfilled : (value) => value; + onRejected = typeof onRejected === 'function'? onRejected : (reason) => { + throw reason; + }; + + let promise2; + if (this.state === 'fulfilled') { + return (promise2 = new MyPromise((resolve, reject) => { + setTimeout(() => { + try { + let x = onFulfilled(this.value); + resolvePromise(promise2, x, resolve, reject); + } catch (error) { + reject(error); + } + }, 0); + })); + } else if (this.state === 'rejected') { + return (promise2 = new MyPromise((resolve, reject) => { + setTimeout(() => { + try { + let x = onRejected(this.reason); + resolvePromise(promise2, x, resolve, reject); + } catch (error) { + reject(error); + } + }, 0); + })); + } else { + return (promise2 = new MyPromise((resolve, reject) => { + this.onFulfilledCallbacks.push((value) => { + setTimeout(() => { + try { + let x = onFulfilled(value); + resolvePromise(promise2, x, resolve, reject); + } catch (error) { + reject(error); + } + }, 0); + }); + + this.onRejectedCallbacks.push((reason) => { + setTimeout(() => { + try { + let x = onRejected(reason); + resolvePromise(promise2, x, resolve, reject); + } catch (error) { + reject(error); + } + }, 0); + }); + })); + } + } + + catch(onRejected) { + return this.then(null, onRejected); + } +} + +function resolvePromise(promise2, x, resolve, reject) { + if (promise2 === x) { + return reject(new TypeError('Chaining cycle detected for promise')); + } + let called; + if (x instanceof MyPromise) { + if (x.state === 'pending') { + x.then((y) => { + resolvePromise(promise2, y, resolve, reject); + }, reject); + } else { + x.then(resolve, reject); + } + } else if (x!== null && (typeof x === 'object' || typeof x === 'function')) { + try { + let then = x.then; + if (typeof then === 'function') { + then.call( + x, + (y) => { + if (called) return; + called = true; + resolvePromise(promise2, y, resolve, reject); + }, + (r) => { + if (called) return; + called = true; + reject(r); + } + ); + } else { + resolve(x); + } + } catch (e) { + if (called) return; + called = true; + reject(e); + } + } else { + resolve(x); + } +} +``` + +## 8.async/await + +```js +function asyncToGenerator(generatorFunc) { + return function() { + const gen = generatorFunc.apply(this, arguments) + return new Promise((resolve, reject) => { + function step(key, arg) { + let generatorResult + try { + generatorResult = gen[key](arg) + } catch (error) { + return reject(error) + } + const { value, done } = generatorResult + if (done) { + return resolve(value) + } else { + return Promise.resolve(value).then(val => step('next', val), err => step('throw', err)) + } + } + step("next") + }) + } +} + +``` +## 9.深拷贝 + +简单实现一下 +```js + +const isObject = o => Object.prototype.toString.call(o) === '[object Object]' + +const isArray = o => Object.prototype.toString.call(o) === '[object Array]' + +const isDate = o => Object.prototype.toString.call(o) === '[object Date]' + +const isNull = o => o === null +function deepClone (Obj) { + if (typeof Obj !== 'object') return Obj + let cloneObj = isArray(Obj[key]) ? [] : {} + for (const key in Obj) { + if (isObject(Obj[key]) || isArray(Obj[key])) { + cloneObj[key] = deepClone(Obj[key]) + } else if (isDate(Obj[key])) { + cloneObj[key] = new Date(Obj[key]) + } else { + cloneObj[key] = Obj[key] + } + } + + return cloneObj +} + +``` + +## 10.数组flatten扁平化 + +```js +function flatten (arr, n = 1) { + if (Array.isArray(arr)) { + throw Error('First argument must be an array') + } + + if (!arr.length) return [] + let count = 0 + + let res = [] + const loop = a => { + res = [] + a.forEach(item => { + if (Array.isArray(item)) { + res = res.concat([...item]) + } else { + res.push(item) + } + }) + + count++ + + if (count < n) { + loop(res) + } + } + + loop(arr) + + return res +} +``` \ No newline at end of file diff --git "a/posts/\346\200\247\350\203\275\344\274\230\345\214\2261.mdx" "b/posts/\346\200\247\350\203\275\344\274\230\345\214\2261.mdx" new file mode 100644 index 0000000..f258228 --- /dev/null +++ "b/posts/\346\200\247\350\203\275\344\274\230\345\214\2261.mdx" @@ -0,0 +1,248 @@ +--- +title: 性能优化-输入URL到页面展示发生了什么 +date: 2021-12-02 +key: "web-performance-optimization-1" +categories: 性能优化 +tags: 性能指标,优化手段,perfermanceApi +keywords: 性能优化 浏览器 web加载 渲染流程 +description: 这篇文章主要介绍了网页性能优化的原因及从输入 URL 到页面加载出来的过程和浏览器渲染流程,并提出了一些思考点和后续规划。具体内容如下: +--- +import TOCInline from "@/components/TOCInline"; + + + +# 性能优化1 + +## 一.为什么要优化性能 + +1.网页加载时间超过3秒就会有百分之57的用户会选择关闭当前网页; +2.在ToC的业务中糟糕的性能直接影响到交易量,下单率等等; + +曾经我们的在学习前端的时候相信都接触过[雅虎军规35条](https://www.jianshu.com/p/4cbcd202a591)等等的性能优化建议 + +## 二.我们如何看到网页的 + +### 2.1 进程和线程 + +进程包含线程,线程依赖进程。曾经的浏览器是单进程的。 +![png](https://static001.geekbang.org/resource/image/6d/ca/6ddad2419b049b0eb2a8036f3dfff1ca.png) +如此多的功能模块运行在一个进程里,是导致单进程浏览器*不稳定*、*不流畅*和*不安全*的一个主要因素。下面我就来一一分析下出现这些问题的原因。 + +2007年以后多进程浏览器的开始盛行;Chrome的改进了浏览器的设计。 +如下: + +![png](https://static001.geekbang.org/resource/image/cd/60/cdc9215e6c6377fc965b7fac8c3ec960.png) + +如今演变成现在的这样子: + +![png](https://static001.geekbang.org/resource/image/b6/fc/b61cab529fa31301bde290813b4587fc.png) + +从图中可以看出,最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。 + +*浏览器进程*:负责交互,显示,提供存储等功能; + +*渲染进程*:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下; + +*GPU进程*:处理3D css; + +*网络进程*:负责网络的资源的加载工作; + +*插件进程*:····· + +## 三.老生常谈-从url输入到页面加载出来经历了什么 + +1.浏览器进程,输入URL会开始域名解析,先去本地host去匹配,如果发现host中存在映射关系,就直接去对应的ip,如果没有就要经过dns解析,查找ip; + + dns解析过程如下: + 浏览器: + 域名 -> 浏览器缓存 -> Y -> ip + 浏览器没有就会到本机: + -> OS DNS缓存/hosts -> Y -> 返回ip + 没有就到路由器: + -> dns缓存中是否有 -> Y ->返回 + 没有 DNS服务器: + -> dns服务器 // 如果没有命中还会去到顶级域名服务器查找 +2.获取端口号,http默认为80 https默认为443 +3.网络进程,根据ip和端口号建立tcp链接,经历3次握手如果是https还要建立tls链接; + + 相关建立链接的过程自行查找,可以查看一下http0.9,1.0,1.1,2.0,3.0的区别 +4.发起请求前会读取本地的缓存; + + 首先会读取内存中的,再是硬盘,如果都没有再去服务端; + 在network中会看到from memory cache,from disk cache, 304的状态码; +5.http发起请求先检查浏览器的强缓存,如果命中,直接返回对应资源文件的副本; + + 强缓存通过响应头字段控制:Expires,cache-control,expires是过期时间,是一个绝对值,过期就重新请求; + cache-control有几个属性值,比如max-age,no-cache,no-store等 + + cache-control优先级高于expires + +6.如果没有命中,就需要走协商缓存会去对比资源,是否需要更新,如果不需要读取本地的缓存,如果需要更新就加载新的资源; +7.服务器返回HTML响应给浏览器 +8.渲染进程:浏览器解析HTML +9.对HTML页面引用的所有资源包括js,css,图片等等,浏览器都发送GET请求,又重复上面的过程 + + 以上是表面上的经历,在浏览器环境中又做了些什么呢? + 1.首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。 + 2.然后,在网络进程中发起真正的 URL 请求。接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。浏览器进程接收到网络进程的响应头数据之后,发送“提交导航 (CommitNavigation)”消息到渲染进程;渲染进程接收到“提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道; + 3.最后渲染进程会向浏览器进程“确认提交”,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。 + 4.浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态 + + 简单来说就是:url输入 -> 网络进程 -> 浏览器进程 -> 遇到html -> 提交导航 -> beforeunload -> 导航完成 -> 渲染进程开始准备 + +## 4.渲染流程是怎么样的 + +渲染流程非常的复杂,但是可以分步来看,总体流程大概如下: + +HTML/CSS/JS -> 子流程 -> 子流程 -> 子流程 -> 完整的页面。 + +子流程包含以下内容:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。 + +如何去分析这个过程;关注三要素:输入-> 加工 -> 输出。 + +### 4.1 构建DOM树 + +问1: 为何要构建DOM树 ? + +问2: 如何构建DOM树 ? + + 输入HTML -> 解析HTML -> DOM树 + +问3: 如何解析? + + 1.首先浏览器会将原始字节的按照编码格式转成字符串 + 2.第二部 令牌化;将字符串转成成HTML,标签; + 3.给这些标签转化成一个个对象,赋予样式和属性; + 4.生成DOM树 + +![png](https://wh-blog.obs.cn-south-1.myhuaweicloud.com/blog/dom%E6%A0%91%E6%B5%81%E7%A8%8B.png) + +### 4.2 样式计算 + +目的:计算DOM节点的样式; + +如何计算? + +第一步:渲染进程在接收到css文件的时候,需要该文件进行转化,转化成浏览器能够认识的样式表(stylesheets) + +第二步:标准化属性,例如em,rem => px等; + +第三步:计算每个DOM节点的样式; + + 1.先继承样式 + 2.再根据样式的优先级来; + +总体来说和DOM差不多; + +### 4.3 布局(LayOut) + +DOM树和节点的样式已经有了,下一步该做什么呢? + +1.创建布局树(LayOut tree) + + 创建过程分两步: + 1.遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中; + 2.而不可见的节点会被布局树忽略掉 + +2.计算位置 + +总结:在 HTML 页面内容被提交给渲染引擎之后,渲染引擎首先将 HTML 解析为浏览器可以理解的 DOM;然后根据 CSS 样式表,计算出 DOM 树所有节点的样式;接着又计算每个元素的几何坐标位置,并将这些信息保存在布局树中。 + +### 4.4 分层 + +我们知道页面是二维的,但是有些属性会导致页面出现层级的情况,比如z-index; +这时候浏览器会对我们的DOm节点进行分图层,设计同学应该知道。一个页面是有多个图层堆叠起来的; +此时此刻会生成一颗 图层树(Layer Tree) + +![png](https://wh-blog.obs.cn-south-1.myhuaweicloud.com/blog/%E6%B8%B2%E6%9F%93%E6%A0%91%E5%92%8C%E5%88%86%E5%B1%82%E6%A0%91%E7%9A%84%E5%85%B3%E7%B3%BB.pngg) + +1.优先级如下: +正z-index > z-index = 0 > inline > float > block > 负z-index > border > background + +2.初次以为需要裁剪的地方也会单独占有一个层; + + 这里所说的裁剪并非是canavs的裁剪,而是绘制页面中的裁剪 + +### 4.5 绘制(paint) + +绘制图层,绘制过程会生成绘制列表,然后commit到渲染进程,由渲染进程中的合成线程完成绘制; + +### 4.6 栅格化 + +合成线程会把图层分为256x256或者是 512x512的图块;合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。 + +### 4.7 合成和显示 + +一旦所有图块都被栅格化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。这时候浏览器进程接收到命令后就会将页面内容提交到内存中;最后显示在页面上。 + +总结大概是这样: + + 1.渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。 + 2.渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算并生成 CSSOM 树。 + 3.创建布局树,并计算元素的布局信息。 + 4.对布局树进行分层,并生成图层树。 + 5.为每个图层生成绘制列表,并将其提交到合成线程。 + 6.合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。 + 7.合成线程发送绘制图块命令 DrawQuad 给浏览器进程。 + 8.浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。 + +## 大总结 + + 1.浏览器进程,输入URL会开始域名解析,先去本地host去匹配,如果发现host中存在映射关系,就直接去对应的ip,如果没有就要经过dns解析,查找ip; + + dns解析过程如下: + 域名 -> hosts命中 -> 返回ip + 域名 -> dns缓存中是否有 -> Y ->返回 + 域名 -> dns缓存中是否有 -> dns服务器 // 如果没有命中还会去到顶级域名服务器查找 +2.获取端口号,http默认为80 https默认为443 +3.网络进程,根据ip和端口号建立tcp链接,经历3次握手如果是https还要建立tls链接; + + 相关建立链接的过程自行查找,可以查看一下http0.9,1.0,1.1,2.0,3.0的区别 +4.发起请求前会读取本地的缓存; + + 首先会读取内存中的,再是硬盘,如果都没有再去服务端; + 在network中会看到from memory cache,from disk cache, 304的状态码; +5.http发起请求先检查浏览器的强缓存,如果命中,直接返回对应资源文件的副本; + + 强缓存通过响应头字段控制:Expires,cache-control,expires是过期时间,是一个绝对值,过期就重新请求; + cache-control有几个属性值,比如max-age,no-cache,no-store等 + + cache-control优先级高于expires + +6.如果没有命中,就需要走协商缓存会去对比资源,是否需要更新,如果不需要读取本地的缓存,如果需要更新就加载新的资源; +7.服务器返回HTML响应给浏览器 +8.渲染进程:浏览器解析HTML +9.对HTML页面引用的所有资源包括js,css,图片等等,浏览器都发送GET请求,又重复上面的过程 + + 以上是表面上的经历,在浏览器环境中又做了些什么呢? + 1.首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。 + 2.然后,在网络进程中发起真正的 URL 请求。接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。浏览器进程接收到网络进程的响应头数据之后,发送“提交导航 (CommitNavigation)”消息到渲染进程;渲染进程接收到“提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道; + 3.最后渲染进程会向浏览器进程“确认提交”,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。 + 4.浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态 + + 简单来说就是:url输入 -> 网络进程 -> 浏览器进程 -> 遇到html -> 提交导航 -> beforeunload -> 导航完成 -> 渲染进程开始准备 +10.构建DOM树 +11.构建CSSOM +12.生成渲布局树 +13.分层 +14.绘制 +15.栅格化 +16.合成 +17.存入内存 +18.展示页面 + +## 思考 + +1.什么是重绘和重排? +2.每个阶段如何优化? +3.为什么需要虚拟DOM? +4.微观视角下的浏览器-每一帧的过程? +... + +## 后续规划 + +1.代码层面 +2.工程化,代码分割,按需加载 +3.API缓存方案 +4.规范化 +... diff --git "a/posts/\346\200\247\350\203\275\344\274\230\345\214\2262.mdx" "b/posts/\346\200\247\350\203\275\344\274\230\345\214\2262.mdx" new file mode 100644 index 0000000..e7df850 --- /dev/null +++ "b/posts/\346\200\247\350\203\275\344\274\230\345\214\2262.mdx" @@ -0,0 +1,291 @@ +--- +title: 性能优化-Performance +date: 2021-12-05 +key: "web-performance-optimization-2" +categories: 性能优化 +tags: 性能指标,优化手段,perfermanceApi +keywords: 性能优化 浏览器 web加载 渲染流程 +description: 这篇文章主要介绍了性能指标,如何测试指标,指标上报等等: +--- + +import TOCInline from "@/components/TOCInline"; + + + +# 性能优化2 + +第一章讲到从url输入到页面显示经历了哪些过程,这一节我们来讲一讲如何检测一个网页的性能。 + +## web性能标准 + +本章内容主要讲解以下内容: + +![png](https://user-gold-cdn.xitu.io/2020/5/12/172067ba148dacfc?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +## 性能度量 + +我们关注的页面性能主要包括页面呈现时间以及交互时候的流畅度。当页面呈现时间越短,交互越流畅的时,我们主观上会认为页面性能越好。 + +客观上我们需要从相关指标去检测性能以及根据检测结果做出相应的优化。 + +web性能API,Performance就可以为这些指标的分析提供数据支持。 + +那么Performance从何而来呢? + +[web Performance Working Group](https://www.w3.org/webperf/)制定了这些标准。文档中给出了定义和说明。 +```txt + Status说明: + + Recommendation(REC)正式发布 + + Proposed Recommendation(PR)提出推荐 + + Candidate Recommendation(CR)候选推荐 + + Working Draft(WD)工作草案 + + Editor's Draft(ED)编辑草案,最早期的版本,可能会各种改来改去,变数比较大,属于非官方行为 + + Retired(ret)已退休,不推荐 + + Group Note(Note)工作组笔记 +``` + +## High Resolution Time(高精度时间) + +这个规范包含了三个部分内容: + +**1.定义了测量数据的初始时间(Time Origin)** +**2.定义了高精度时间戳 DOMHighResTimeStamp** +**3.定义了 Performance 对象,以及 Performance 对象的几个属性和方法** + + +## Performance Timeline + +这个规范包含了三个部分内容: + +**1.给Performance添加三个方法** + + - getEntries() + - getEntriesByType() + - getEntriesByName() + +type和name可以在[这里查到](https://www.w3.org/TR/timing-entrytypes-registry/) + +**2.定义了 PerformanceEntry 对象** + + PerformanceEntry对象包含四个属性:duration,entryType, name, startTime + +**3.定义了 PerformanceObserver 对象** + +用于观察性能时间线,以便在记录新的性能指标时发出通知, 这在采集性能数据时经常用到。例如下面的例子,观察 resource 类型的性能数据并打印。 + +## Resource Timing + +该规范定义了用于访问文档中资源的完整计时信息的 API, 例如请求资源的 DNS、TCP、Request 等的开始时间和结束时间,可以帮助我们收集文档中静态资源的加载时间线、资源大小和资源类型。 +![png](https://user-gold-cdn.xitu.io/2020/5/10/171fbe834ad606c5?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +资源处理流出 + +该规范定义了三个内容: + +**1.定义了 [PerformanceResourceTiming](https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming) 对象** + +**2.给 Performance 对象添加了如下方法** +- clearResourceTimings() +- setResourceTimingBufferSize() + +**3.定义了 Timing-Allow-Origin 响应头** + +对于跨域请求的资源,获取到的 PerformanceResourceTiming 对象中的属性值(时间),由于跨域限制,浏览器不会将资源的性能数据提供给用户,这些时间值都会被设置为 0 。 +如果服务端在请求资源的响应头中添加 Timing-Allow-Origin,则浏览器就会将此资源的性能时间值暴露给用户。 +我们可以通过以下语句获取文档中所有资源的性能数据: + +```js +performance.getEntriesByType('resource'); +``` + +或者指定资源: +```js +performance.getEntriesByName('https://xxxxxx/img/logo_white.636ab47.png'); +``` + +## Navigation Timing + +此标准定义了文档导航过程中完整的性能度量,即一个文档从发起请求到加载完毕各阶段的性能耗时。 + + +Navigation Timing 有两个版本: +1.[Navigation Timing Level 1](https://www.w3.org/TR/navigation-timing/) +2.[Navigation Timing Level 2](https://www.w3.org/TR/navigation-timing-2/) + + +### Navigation Timing Level 1 + +给performance新增了两个属性,timing和navigation + +**1.定义了 PerformanceTiming 对象** + +用来衡量页面性能,我们可以通过通过 window.performance.timing 获取页面的性能数据,返回的对象中每个字段的含义可以在 [PerformanceTiming | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming) 上查阅。 + +按照事件发生的先后顺序,这些性能数据的 TimeLine 如下: + +![png](https://user-gold-cdn.xitu.io/2020/5/10/171fbe8383a1bf40?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + + +**2、定义了 PerformanceNavigation 对象** + +用来描述加载相关的操作,通过 window.performance.navigation 获得,返回的 PerformanceNavigation 对象存储了两个属性,它们表示触发页面加载的原因。这些原因可能是页面重定向、前进后退按钮或者普通的 URL 加载。 + +相关参数表示请查阅这里:[navigation](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceNavigation) + +**3、定义了 window.performance 属性** + +为 Window 对象添加了 performance 属性:timing 和 navigation + +### Navigation Timing Level 2 + +将会替代 Navigation Timing Level 1。在 Navigation Timing Level 1 中定义的两个属性 performance.timing 和 performance.navigation 被废弃了。 + +**1、定义了 PerformanceNavigationTiming 对象** + +此对象用于度量文档的性能,我们可以通过以下方式获取文档的性能数据,所有时间值都是以 Origin Time 为起点测量的。 + +``` +window.performance.getEntriesByType("navigation"); +``` + +并且更新了 Processing Model + +![png](https://user-gold-cdn.xitu.io/2020/5/10/171fbe839645dd88?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + + +**2、定义了 NavigationType 的枚举值** + +NavigationType 是一个枚举类型,包含四种值:navigate、reload 、 back_forward 和 prerender + + +## Paint Timing + +此规范定义了一个 API 用来记录在页面加载期间的一些关键时间点的性能度量,比如 First Paint 、First Contentful Paint。 + +此规范包含以下内容: + +**1、定义了 PerformancePaintTiming 对象** + +用于描述在页面加载期间的一些关键时间点的性能度量,我们可以在控制台通过以下语句查看: + +```js +performance.getEntriesByType('paint'); +``` + +结果返回一个每一项都为 PerformancePaintTiming 类型的数组,一项为 first-paint ,另一项为 first-contentful-paint。 + +**2、提出了一些关键时间点的定义,例如 First Paint 、First Contentful Paint +** + +**First Paint**,是从导航到浏览器将第一个像素呈现到屏幕的时间,这不包括默认的背景绘制,但包括非默认的背景绘制。这是开发人员关心页面加载的第一个关键时刻——当浏览器开始呈现页面时。 +**First Contentful Paint (FCP)**,是当浏览器呈现来自 DOM 的第一位内容时,向用户提供页面实际加载的第一个反馈。这是用户第一次开始使用页面内容。 +好了,了解了此规范定义的内容,大家也清楚了为什么此规范名为 Paint Timing 了吧?因为该规范定义了获取Paint 关键时间点的性能数据的 API。 + +性能指标会在下一章讲解。 + + +## User Timing + +此规范定义了一个可以让 Web 开发者测量性能的 API 。 + +**1、给 Performance 对象添加了几个方法** + + - mark + - clearmark + - measure + - clearMeasures + +**2、定义了 PerformanceMark 对象** + +**3、定义了 PerformanceMeasure 对象** + +## Server Timing + +**1、定义了与服务端的通信协议:Server-Timing 响应头** + +响应头信息如下, 可以在 [https://www.w3.org/TR/server-timing/#examples](https://www.w3.org/TR/server-timing/#examples) 查看响应头的具体含义 + +**2、定义了描述服务端性能度量的接口 PerformanceServerTiming 对象** + +**3、给 PerformanceResourceTiming 对象添加了 serverTiming 属性** + + +## Long Tasks API + +当用户与页面交互时,应用程序和浏览器都会将浏览器随后执行的各种事件排队,例如,用户代理根据用户的活动安排输入事件,应用程序为 requestAnimationFrame 和其他回调等安排回调。一旦进入队列,这些事件就会被浏览器安排逐个出列并执行。 +但是,有些任务可能需要很长时间,如果发生这种情况,UI 线程将被锁定,所有其他任务也将被阻止。对于用户来说,这是一个卡死的页面,浏览器无法响应用户的输入,这是目前 Web 上不良用户体验的主要来源。 +此规范定义了一个 API,可以使用它来检测这些“长任务(Long Task)”的存在,“长任务”在很长一段时间内独占 UI 线程,并阻止执行其他关键任务,例如响应用户输入。 + +[Long Tasks API](https://www.w3.org/TR/longtasks-1/) + +此规范包含以下内容: + +**1、定义了 PerformanceLongTaskTiming 对象** +用于描述 Long Task 信息,对象中各字段的含义可在 [Long Tasks API | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Long_Tasks_API) 查阅。 + +**2、定义了什么是 Long Task** + +Long Task 是指超过 50ms 的事件循环任务。 + +# 优化策略 + +## Resource Hints - 加载性能 + +此规范定义了 HTML 的 link 元素的 rel 属性值,包括 dns-prefetch、preconnect、prefetch 和 prerender。我们可以使用这些资源提示让用用户代理帮助我们预解析 DNS、预链接、预加载资源以及预处理资源以提高页面性能。 + +此规范目前只有一个版本,还在工作草案阶段。 + +[Resource Hints](https://www.w3.org/TR/resource-hints/) + +下面介绍此规范定义的 4 个资源提示: + +**1、资源提示: dns-prefetch(Resource Hints: dns-prefetch)** + +``` css + +``` + +**2、资源提示:预连接(Resource Hints: preconnect)** + +``` css + + + +``` + +**3、资源提示:预取(Resource Hints: prefetch)** + +```css + + +``` + +**4、资源提示:预渲染(Resource Hints: prerender)** + + +## Page Visibility - 节省资源 + + +此规范提供了观察页面可见性状态的 API ,例如当用户最小化窗口或切换到另一个选项卡时,API 会发送[visibilitychange](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event) 事件,让监听者知道页面状态已更改,我们可以检测事件并执行某些操作。 + + +## requestIdleCallback API - 充分利用资源 + +## Beacon - 数据上报 + +解决数据上报占用线程的,或者遗漏卸载的时候的异步请求中的数据 + +```js +window.addEventListener('unload', logData, false); + +function logData() { + navigator.sendBeacon("/log", analyticsData (url, data)); +} +``` \ No newline at end of file diff --git "a/posts/\346\267\261\345\205\245\346\216\242\347\264\242\345\211\215\347\253\257\351\241\265\351\235\242\344\270\273\351\242\230\345\210\207\346\215\242\347\232\204\345\244\232\347\247\215\345\256\236\347\216\260\346\226\271\346\263\225\357\274\232\344\273\216CSS\345\217\230\351\207\217\345\210\260\347\273\204\344\273\266\345\272\223\345\272\224\347\224\250.mdx" "b/posts/\346\267\261\345\205\245\346\216\242\347\264\242\345\211\215\347\253\257\351\241\265\351\235\242\344\270\273\351\242\230\345\210\207\346\215\242\347\232\204\345\244\232\347\247\215\345\256\236\347\216\260\346\226\271\346\263\225\357\274\232\344\273\216CSS\345\217\230\351\207\217\345\210\260\347\273\204\344\273\266\345\272\223\345\272\224\347\224\250.mdx" index 58b5f95..907ad55 100644 --- "a/posts/\346\267\261\345\205\245\346\216\242\347\264\242\345\211\215\347\253\257\351\241\265\351\235\242\344\270\273\351\242\230\345\210\207\346\215\242\347\232\204\345\244\232\347\247\215\345\256\236\347\216\260\346\226\271\346\263\225\357\274\232\344\273\216CSS\345\217\230\351\207\217\345\210\260\347\273\204\344\273\266\345\272\223\345\272\224\347\224\250.mdx" +++ "b/posts/\346\267\261\345\205\245\346\216\242\347\264\242\345\211\215\347\253\257\351\241\265\351\235\242\344\270\273\351\242\230\345\210\207\346\215\242\347\232\204\345\244\232\347\247\215\345\256\236\347\216\260\346\226\271\346\263\225\357\274\232\344\273\216CSS\345\217\230\351\207\217\345\210\260\347\273\204\344\273\266\345\272\223\345\272\224\347\224\250.mdx" @@ -8,6 +8,10 @@ key: how-to-makeup-multiple-theme-webapp date: 2024-07-19 --- +import TOCInline from "@/components/TOCInline"; + + + # 如何实现前端页面主题切换:多种方法详解 实现前端页面的主题切换可以提升用户体验和界面美观度。本文将详细介绍四种以上的实现方式,包括 CSS 变量、CSS-in-JS、引入不同的 CSS 文件等,分析其优缺点,并讨论组件库(如 Ant Design、Element Plus)的主题实现原理,最后探讨 Vue 和 React 中的主题切换实现及 Next.js 的 next-themes 库的实现原理。本文旨在提供全面的技术细节和实现示例,帮助开发者在不同的场景中选择合适的方案。