Build technical notes archive
This commit is contained in:
+264
-10
@@ -1,8 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #101411;
|
||||
--foreground: #f4efe4;
|
||||
--background: #05050b;
|
||||
--foreground: #ffffff;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -18,38 +18,292 @@
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--background);
|
||||
overflow-x: clip;
|
||||
overscroll-behavior-y: none;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100dvh;
|
||||
overflow-x: clip;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-body), sans-serif;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #d3ff30;
|
||||
color: #101411;
|
||||
background: #22d3ee;
|
||||
color: #05050b;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.noise-layer {
|
||||
.tech-doc-content {
|
||||
animation: doc-rise 720ms ease both;
|
||||
contain: paint;
|
||||
max-width: 100%;
|
||||
color: rgba(255, 255, 255, 0.74);
|
||||
font-size: 16px;
|
||||
line-height: 2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tech-doc-toc a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tech-doc-toc div::-webkit-scrollbar,
|
||||
.tech-doc-content pre::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.tech-doc-toc div::-webkit-scrollbar-thumb,
|
||||
.tech-doc-content pre::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(103, 232, 249, 0.38);
|
||||
}
|
||||
|
||||
.tech-doc-content::before {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 3px;
|
||||
margin-bottom: 28px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #22d3ee, #a855f7, #f0abfc, #84cc16);
|
||||
box-shadow: 0 0 26px rgba(34, 211, 238, 0.42);
|
||||
}
|
||||
|
||||
.tech-doc-content :where(h3, h4) {
|
||||
margin: 42px 0 16px;
|
||||
scroll-margin-top: 32px;
|
||||
color: #ffffff;
|
||||
font-weight: 900;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.tech-doc-content h3 {
|
||||
font-size: clamp(1.45rem, 2vw, 2rem);
|
||||
background: linear-gradient(90deg, #67e8f9, #f0abfc 58%, #bef264);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 30px rgba(34, 211, 238, 0.2);
|
||||
}
|
||||
|
||||
.tech-doc-content h4 {
|
||||
font-size: 1.05rem;
|
||||
color: #a5f3fc;
|
||||
}
|
||||
|
||||
.tech-doc-content :where(p, ul, ol, blockquote, pre) {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.tech-doc-content :where(ul, ol) {
|
||||
padding-left: 1.35rem;
|
||||
}
|
||||
|
||||
.tech-doc-content li {
|
||||
margin: 8px 0;
|
||||
padding-left: 0.2rem;
|
||||
}
|
||||
|
||||
.tech-doc-content li::marker {
|
||||
color: #67e8f9;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.tech-doc-content a {
|
||||
color: #67e8f9;
|
||||
border-bottom: 1px solid rgba(103, 232, 249, 0.35);
|
||||
transition:
|
||||
color 160ms ease,
|
||||
border-color 160ms ease,
|
||||
text-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.tech-doc-content a:hover {
|
||||
color: #f0abfc;
|
||||
border-color: rgba(240, 171, 252, 0.75);
|
||||
text-shadow: 0 0 18px rgba(240, 171, 252, 0.35);
|
||||
}
|
||||
|
||||
.tech-doc-content strong {
|
||||
color: #ffffff;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.tech-doc-content code {
|
||||
border: 1px solid rgba(103, 232, 249, 0.18);
|
||||
border-radius: 8px;
|
||||
background: rgba(34, 211, 238, 0.1);
|
||||
color: #a5f3fc;
|
||||
font-family: var(--font-code), monospace;
|
||||
font-size: 0.9em;
|
||||
padding: 0.1rem 0.38rem;
|
||||
}
|
||||
|
||||
.tech-doc-content pre {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(45, 212, 191, 0.28);
|
||||
border-radius: 20px;
|
||||
padding-top: 2.75rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 23, 42, 0.96) 0 2.75rem, transparent 2.75rem),
|
||||
radial-gradient(circle at 16% 0%, rgba(45, 212, 191, 0.2), transparent 28%),
|
||||
radial-gradient(circle at 88% 8%, rgba(14, 165, 233, 0.22), transparent 26%),
|
||||
#020617;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.03),
|
||||
0 16px 46px rgba(2, 6, 23, 0.42),
|
||||
0 0 24px rgba(45, 212, 191, 0.08);
|
||||
}
|
||||
|
||||
.tech-doc-content pre::before {
|
||||
content: attr(data-language);
|
||||
position: absolute;
|
||||
left: 1.1rem;
|
||||
top: 0.9rem;
|
||||
color: #5eead4;
|
||||
font-family: var(--font-code), monospace;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
.tech-doc-code-copy {
|
||||
position: absolute;
|
||||
right: 0.85rem;
|
||||
top: 0.56rem;
|
||||
z-index: 1;
|
||||
width: 4.1rem;
|
||||
border: 1px solid rgba(103, 232, 249, 0.38);
|
||||
border-radius: 999px;
|
||||
background: rgba(8, 47, 73, 0.72);
|
||||
color: #cffafe;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-body), sans-serif;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
padding: 0.52rem 0.72rem;
|
||||
transition:
|
||||
background 160ms ease,
|
||||
border-color 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
color 160ms ease;
|
||||
}
|
||||
|
||||
.tech-doc-code-copy:hover,
|
||||
.tech-doc-code-copy[data-copied="true"] {
|
||||
border-color: rgba(94, 234, 212, 0.9);
|
||||
background: rgba(20, 184, 166, 0.22);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 0 24px rgba(45, 212, 191, 0.26);
|
||||
}
|
||||
|
||||
.tech-doc-content pre code {
|
||||
display: block;
|
||||
width: max-content;
|
||||
min-width: max-content;
|
||||
max-width: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.8;
|
||||
padding: 0 1.25rem 1.25rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.tech-doc-content img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
margin: 24px 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
box-shadow: 0 18px 52px rgba(34, 211, 238, 0.12);
|
||||
}
|
||||
|
||||
.tech-doc-content blockquote {
|
||||
border-left: 3px solid #22d3ee;
|
||||
border-radius: 14px;
|
||||
background: rgba(34, 211, 238, 0.08);
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
|
||||
.tech-doc-content hr {
|
||||
height: 1px;
|
||||
margin: 32px 0;
|
||||
border: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(103, 232, 249, 0.45), transparent);
|
||||
}
|
||||
|
||||
.neon-aurora {
|
||||
position: absolute;
|
||||
inset: -18% -10% auto -10%;
|
||||
z-index: -3;
|
||||
height: 72%;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(circle at 12% 24%, rgba(34, 211, 238, 0.44), transparent 28%),
|
||||
radial-gradient(circle at 48% 8%, rgba(99, 102, 241, 0.36), transparent 30%),
|
||||
radial-gradient(circle at 86% 32%, rgba(217, 70, 239, 0.42), transparent 28%),
|
||||
radial-gradient(circle at 74% 72%, rgba(132, 204, 22, 0.18), transparent 24%);
|
||||
filter: blur(12px) saturate(1.18);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.cyber-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(34, 211, 238, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(217, 70, 239, 0.08) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 56%, transparent 100%);
|
||||
}
|
||||
|
||||
.scanline {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
opacity: 0.2;
|
||||
background-image:
|
||||
radial-gradient(circle at 18% 30%, rgba(244, 239, 228, 0.18) 0 1px, transparent 1px),
|
||||
radial-gradient(circle at 72% 64%, rgba(211, 255, 48, 0.14) 0 1px, transparent 1px);
|
||||
background-size: 34px 34px, 46px 46px;
|
||||
opacity: 0.18;
|
||||
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
||||
background-size: 100% 7px;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
@keyframes doc-rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(18px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PlaceholderPage } from "@/components/placeholder-page";
|
||||
|
||||
export default function LifePage() {
|
||||
return (
|
||||
<PlaceholderPage
|
||||
eyebrow="Life journal"
|
||||
title="生活分享"
|
||||
description="技术之外的个人切面:日常、阅读、音乐、照片、旅行,以及阶段性的想法。"
|
||||
items={["日常记录", "阅读和音乐", "照片和旅行", "一些想法"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { FrontendNotesPage } from "@/components/frontend-notes-page";
|
||||
import { frontendArticles } from "@/content/notes/frontend/articles";
|
||||
|
||||
const modules = ["frontend", "backend", "ai-agent"] as const;
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{
|
||||
module: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return modules.map((module) => ({ module }));
|
||||
}
|
||||
|
||||
export default async function NotesModulePage({ params }: PageProps) {
|
||||
const { module } = await params;
|
||||
|
||||
if (!modules.includes(module as (typeof modules)[number])) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (module === "frontend") {
|
||||
const articles = frontendArticles.map((article) => ({
|
||||
slug: article.slug,
|
||||
title: article.title,
|
||||
description: article.description,
|
||||
href: `/notes/frontend/${article.slug}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<FrontendNotesPage
|
||||
eyebrow="Frontend knowledge base"
|
||||
title="前端笔记"
|
||||
description="fly 的前端知识索引,覆盖基础语言、框架机制、浏览器、网络、性能与代码执行题。"
|
||||
listDescription="每个入口都是一组可独立阅读的知识点,适合快速定位概念、机制与常见问题。"
|
||||
articles={articles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (module === "backend") {
|
||||
return (
|
||||
<FrontendNotesPage
|
||||
eyebrow="Backend knowledge base"
|
||||
title="后端笔记"
|
||||
description="后端方向会整理服务端基础、接口设计、数据库、部署、鉴权、缓存和稳定性相关内容。"
|
||||
featuredTitle="敬请期待"
|
||||
listTitle="规划内容"
|
||||
listDescription="这些模块会在后续逐步补齐,先保留入口和内容结构。"
|
||||
articles={[
|
||||
{
|
||||
slug: "backend-api",
|
||||
title: "API 设计",
|
||||
description: "接口规范、请求响应结构、错误码、鉴权边界和版本管理等内容整理。",
|
||||
status: "soon",
|
||||
},
|
||||
{
|
||||
slug: "backend-database",
|
||||
title: "数据库",
|
||||
description: "数据建模、索引、事务、查询优化和常见业务存储方案。",
|
||||
status: "soon",
|
||||
},
|
||||
{
|
||||
slug: "backend-deployment",
|
||||
title: "部署与服务稳定性",
|
||||
description: "服务部署、日志、监控、缓存、限流和故障排查相关实践。",
|
||||
status: "soon",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (module === "ai-agent") {
|
||||
return (
|
||||
<FrontendNotesPage
|
||||
eyebrow="AI Agent knowledge base"
|
||||
title="AI Agent 笔记"
|
||||
description="AI Agent 方向会记录工作流、工具调用、上下文工程、提示词和自动化场景实践。"
|
||||
featuredTitle="敬请期待"
|
||||
listTitle="规划内容"
|
||||
listDescription="这些方向会随着实践逐步沉淀成可回看的技术文章。"
|
||||
articles={[
|
||||
{
|
||||
slug: "agent-workflow",
|
||||
title: "Agent 工作流",
|
||||
description: "任务拆解、执行循环、状态管理和人机协作流程。",
|
||||
status: "soon",
|
||||
},
|
||||
{
|
||||
slug: "agent-tools",
|
||||
title: "工具调用",
|
||||
description: "函数调用、MCP、浏览器自动化、文件操作和外部系统集成。",
|
||||
status: "soon",
|
||||
},
|
||||
{
|
||||
slug: "agent-prompting",
|
||||
title: "提示词与上下文工程",
|
||||
description: "角色约束、上下文压缩、记忆策略和输出质量控制。",
|
||||
status: "soon",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
notFound();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { TechDocPage } from "@/components/tech-doc-page";
|
||||
import { frontendArticles, getFrontendArticle } from "@/content/notes/frontend/articles";
|
||||
import { getTechArticle } from "@/lib/tech-article";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return frontendArticles.map((article) => ({ slug: article.slug }));
|
||||
}
|
||||
|
||||
export default async function FrontendArticlePage({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
const articleMeta = getFrontendArticle(slug);
|
||||
|
||||
if (!articleMeta) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const article = getTechArticle(`frontend/${articleMeta.file}`);
|
||||
|
||||
return (
|
||||
<TechDocPage
|
||||
eyebrow="Frontend notes"
|
||||
title={articleMeta.title}
|
||||
description={articleMeta.description}
|
||||
contentHtml={article.contentHtml}
|
||||
toc={articleMeta.slug === "code-output" ? [] : article.toc}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
|
||||
export function BackToTopButton() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
|
||||
useEffect(() => {
|
||||
let frame = 0;
|
||||
|
||||
const updateVisibility = () => {
|
||||
frame = 0;
|
||||
setVisible(window.scrollY > 520);
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
frame = window.requestAnimationFrame(updateVisibility);
|
||||
};
|
||||
|
||||
updateVisibility();
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
if (frame) {
|
||||
window.cancelAnimationFrame(frame);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: prefersReducedMotion ? "auto" : "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible ? (
|
||||
<motion.button
|
||||
type="button"
|
||||
aria-label="回到顶部"
|
||||
title="回到顶部"
|
||||
className="fixed bottom-6 right-5 z-50 inline-flex h-12 w-12 items-center justify-center rounded-full border border-cyan-300/35 bg-[#07111f]/95 text-cyan-100 shadow-[0_0_28px_rgba(34,211,238,0.24)] outline-none transition hover:-translate-y-0.5 hover:border-fuchsia-300/70 hover:text-white hover:shadow-[0_0_34px_rgba(217,70,239,0.25)] focus-visible:ring-2 focus-visible:ring-cyan-300 sm:bottom-8 sm:right-8"
|
||||
initial={{ opacity: 0, scale: 0.82, y: 12 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.82, y: 12 }}
|
||||
transition={{ duration: 0.18, ease: "easeOut" }}
|
||||
onClick={scrollToTop}
|
||||
>
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
</motion.button>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowUpRight,
|
||||
BookOpen,
|
||||
Braces,
|
||||
Code2,
|
||||
Gauge,
|
||||
Globe2,
|
||||
Layers3,
|
||||
MonitorCog,
|
||||
RadioTower,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
|
||||
type FrontendArticle = {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
href?: string;
|
||||
status?: "ready" | "soon";
|
||||
};
|
||||
|
||||
type FrontendNotesPageProps = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
archiveLabel?: string;
|
||||
featuredTitle?: string;
|
||||
listTitle?: string;
|
||||
listDescription: string;
|
||||
articles: readonly FrontendArticle[];
|
||||
};
|
||||
|
||||
const articleVisuals: Record<
|
||||
string,
|
||||
{
|
||||
accent: string;
|
||||
glyph: string;
|
||||
track: string;
|
||||
Icon: typeof Code2;
|
||||
}
|
||||
> = {
|
||||
html: {
|
||||
accent: "from-orange-300 via-rose-300 to-fuchsia-300",
|
||||
glyph: "HTML",
|
||||
track: "Markup",
|
||||
Icon: Layers3,
|
||||
},
|
||||
css: {
|
||||
accent: "from-sky-300 via-cyan-300 to-lime-300",
|
||||
glyph: "CSS",
|
||||
track: "Visual system",
|
||||
Icon: Sparkles,
|
||||
},
|
||||
"javascript-part-1": {
|
||||
accent: "from-yellow-200 via-cyan-300 to-blue-400",
|
||||
glyph: "JS-I",
|
||||
track: "Core language",
|
||||
Icon: Braces,
|
||||
},
|
||||
"javascript-part-2": {
|
||||
accent: "from-cyan-300 via-violet-300 to-fuchsia-300",
|
||||
glyph: "JS-II",
|
||||
track: "Runtime",
|
||||
Icon: Code2,
|
||||
},
|
||||
"vue-part-1": {
|
||||
accent: "from-emerald-300 via-teal-300 to-cyan-300",
|
||||
glyph: "Vue I",
|
||||
track: "Framework",
|
||||
Icon: MonitorCog,
|
||||
},
|
||||
"vue-part-2": {
|
||||
accent: "from-green-300 via-cyan-300 to-blue-300",
|
||||
glyph: "Vue II",
|
||||
track: "Ecosystem",
|
||||
Icon: MonitorCog,
|
||||
},
|
||||
"react-part-1": {
|
||||
accent: "from-cyan-200 via-blue-300 to-indigo-300",
|
||||
glyph: "React I",
|
||||
track: "UI model",
|
||||
Icon: RadioTower,
|
||||
},
|
||||
"react-part-2": {
|
||||
accent: "from-blue-300 via-fuchsia-300 to-rose-300",
|
||||
glyph: "React II",
|
||||
track: "Architecture",
|
||||
Icon: RadioTower,
|
||||
},
|
||||
network: {
|
||||
accent: "from-lime-200 via-cyan-300 to-blue-400",
|
||||
glyph: "NET",
|
||||
track: "Protocol",
|
||||
Icon: Globe2,
|
||||
},
|
||||
browser: {
|
||||
accent: "from-purple-300 via-cyan-300 to-emerald-300",
|
||||
glyph: "BOM",
|
||||
track: "Browser",
|
||||
Icon: MonitorCog,
|
||||
},
|
||||
performance: {
|
||||
accent: "from-lime-200 via-yellow-200 to-cyan-300",
|
||||
glyph: "Perf",
|
||||
track: "Optimization",
|
||||
Icon: Gauge,
|
||||
},
|
||||
"code-output": {
|
||||
accent: "from-fuchsia-300 via-rose-300 to-orange-200",
|
||||
glyph: "Quiz",
|
||||
track: "Execution",
|
||||
Icon: Braces,
|
||||
},
|
||||
};
|
||||
|
||||
const featuredSlugs = ["javascript-part-1", "css", "browser"] as const;
|
||||
|
||||
function getArticleVisual(slug: string) {
|
||||
return (
|
||||
articleVisuals[slug] ?? {
|
||||
accent: "from-cyan-300 via-fuchsia-300 to-lime-200",
|
||||
glyph: "Soon",
|
||||
track: "Coming soon",
|
||||
Icon: Code2,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function FrontendNotesPage({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
archiveLabel = "ARCHIVE",
|
||||
featuredTitle = "精选入口",
|
||||
listTitle = "全部文章",
|
||||
listDescription,
|
||||
articles,
|
||||
}: FrontendNotesPageProps) {
|
||||
const featuredArticles =
|
||||
featuredSlugs
|
||||
.map((slug) => articles.find((article) => article.slug === slug))
|
||||
.filter((article): article is FrontendArticle => Boolean(article)).length > 0
|
||||
? featuredSlugs
|
||||
.map((slug) => articles.find((article) => article.slug === slug))
|
||||
.filter((article): article is FrontendArticle => Boolean(article))
|
||||
: articles.slice(0, 3);
|
||||
|
||||
const renderArticleContent = (article: FrontendArticle, index: number, compact = false) => {
|
||||
const visual = getArticleVisual(article.slug);
|
||||
const Icon = visual.Icon;
|
||||
const isSoon = article.status === "soon";
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`flex aspect-square items-center justify-center bg-gradient-to-br ${visual.accent} font-mono text-sm font-black text-[#05050b] shadow-lg shadow-cyan-500/15`}
|
||||
>
|
||||
{isSoon ? "..." : String(index + 1).padStart(2, "0")}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-cyan-100/65">
|
||||
<Icon className="h-4 w-4" />
|
||||
{visual.track}
|
||||
</div>
|
||||
<h3 className="mt-2 text-xl font-black text-white">{article.title}</h3>
|
||||
<p className="mt-2 line-clamp-2 text-sm leading-6 text-white/54">
|
||||
{article.description}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowUpRight className="h-5 w-5 text-white/42 transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-cyan-100" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`absolute inset-x-0 top-0 h-1 bg-gradient-to-r ${visual.accent}`} />
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div
|
||||
className={`inline-flex h-14 w-14 items-center justify-center bg-gradient-to-br ${visual.accent} font-mono text-xs font-black text-[#05050b]`}
|
||||
>
|
||||
{visual.glyph}
|
||||
</div>
|
||||
<span className="font-mono text-sm text-fuchsia-200/70">
|
||||
{isSoon ? "SOON" : String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center gap-2 text-xs font-bold uppercase tracking-[0.16em] text-cyan-100/60">
|
||||
<Icon className="h-4 w-4" />
|
||||
{visual.track}
|
||||
</div>
|
||||
<h3 className="mt-4 text-2xl font-black leading-tight text-white">{article.title}</h3>
|
||||
<p className="mt-4 line-clamp-3 text-sm leading-7 text-white/55">{article.description}</p>
|
||||
|
||||
<div className="absolute inset-x-5 bottom-5 flex items-center justify-between border-t border-white/10 pt-4">
|
||||
<span className="font-mono text-xs uppercase tracking-[0.16em] text-white/42">
|
||||
{isSoon ? "敬请期待" : "Read article"}
|
||||
</span>
|
||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-cyan-300/24 bg-cyan-300/[0.08] text-cyan-100 transition group-hover:border-fuchsia-300/70 group-hover:text-fuchsia-100">
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="relative min-h-dvh overflow-hidden bg-[#05050b] px-5 py-6 text-white sm:px-8 lg:px-10">
|
||||
<div className="neon-aurora" />
|
||||
<div className="cyber-grid" />
|
||||
<div className="scanline" />
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-7xl">
|
||||
<header className="flex items-center justify-between rounded-full border border-white/10 bg-white/[0.04] px-4 py-3 shadow-2xl shadow-cyan-500/10">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex h-11 items-center gap-2 rounded-full border border-white/12 bg-white/[0.05] px-4 text-sm font-semibold text-white/80 outline-none transition hover:border-cyan-300/70 hover:text-cyan-100 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
首页
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-full bg-white px-4 py-2 text-sm font-black text-[#05050b] outline-none transition hover:bg-cyan-200 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
fly
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<section className="grid min-h-[calc(100dvh-92px)] gap-10 py-12 lg:grid-cols-[0.9fr_1.1fr] lg:items-center lg:py-14">
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-300/30 bg-cyan-300/10 px-4 py-2 font-mono text-xs uppercase text-cyan-100 shadow-lg shadow-cyan-500/10">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
{eyebrow}
|
||||
</div>
|
||||
<h1 className="mt-7 font-display text-5xl font-black leading-tight sm:text-7xl lg:text-8xl">
|
||||
{title}
|
||||
<span className="block bg-gradient-to-r from-cyan-200 via-fuchsia-200 to-lime-200 bg-clip-text text-transparent">
|
||||
{archiveLabel}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mt-7 max-w-2xl text-base leading-8 text-white/62 sm:text-lg">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div className="mt-9 h-px max-w-xl bg-gradient-to-r from-cyan-300 via-fuchsia-300 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-6 bg-[radial-gradient(circle_at_20%_12%,rgba(34,211,238,0.25),transparent_30%),radial-gradient(circle_at_84%_80%,rgba(217,70,239,0.2),transparent_32%)] blur-2xl" />
|
||||
<div className="relative overflow-hidden border border-white/10 bg-[#090915]/90 p-5 shadow-2xl shadow-black/40 sm:p-6">
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-4">
|
||||
<div>
|
||||
<p className="font-mono text-xs uppercase tracking-[0.2em] text-fuchsia-200/70">
|
||||
launch sequence
|
||||
</p>
|
||||
<h2 className="mt-2 text-2xl font-black">{featuredTitle}</h2>
|
||||
</div>
|
||||
<div className="h-3 w-3 rounded-full bg-lime-300 shadow-[0_0_24px_rgba(190,242,100,0.85)]" />
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-3">
|
||||
{featuredArticles.map((article, index) => {
|
||||
const className =
|
||||
"group grid gap-4 border border-white/10 bg-white/[0.045] p-4 outline-none transition hover:-translate-y-0.5 hover:border-cyan-300/50 hover:bg-cyan-300/[0.07] focus-visible:ring-2 focus-visible:ring-cyan-300 sm:grid-cols-[4.8rem_1fr_auto] sm:items-center";
|
||||
|
||||
return article.href ? (
|
||||
<Link key={article.slug} href={article.href} className={className}>
|
||||
{renderArticleContent(article, index, true)}
|
||||
</Link>
|
||||
) : (
|
||||
<div key={article.slug} className={className}>
|
||||
{renderArticleContent(article, index, true)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pb-16">
|
||||
<div className="flex flex-col justify-between gap-4 border-y border-white/10 py-7 sm:flex-row sm:items-end">
|
||||
<div>
|
||||
<p className="font-mono text-xs uppercase tracking-[0.2em] text-cyan-200/70">
|
||||
all dossiers
|
||||
</p>
|
||||
<h2 className="mt-3 text-3xl font-black text-white sm:text-4xl">{listTitle}</h2>
|
||||
</div>
|
||||
<p className="max-w-xl text-sm leading-7 text-white/52">
|
||||
{listDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{articles.map((article, index) => {
|
||||
const className =
|
||||
"group relative min-h-72 overflow-hidden border border-white/10 bg-white/[0.04] p-5 outline-none transition hover:-translate-y-1 hover:border-white/25 hover:bg-white/[0.065] focus-visible:ring-2 focus-visible:ring-cyan-300";
|
||||
|
||||
return article.href ? (
|
||||
<Link
|
||||
key={article.slug}
|
||||
href={article.href}
|
||||
className={className}
|
||||
>
|
||||
{renderArticleContent(article, index)}
|
||||
</Link>
|
||||
) : (
|
||||
<div key={article.slug} className={className}>
|
||||
{renderArticleContent(article, index)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+303
-174
@@ -1,217 +1,346 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Bot,
|
||||
Braces,
|
||||
Database,
|
||||
GitBranch,
|
||||
LifeBuoy,
|
||||
Mail,
|
||||
Sparkles,
|
||||
TerminalSquare,
|
||||
} from "lucide-react";
|
||||
|
||||
const highlights = [
|
||||
{ label: "Frontend", value: "React / Vue / Next.js" },
|
||||
{ label: "Notes", value: "HTML, CSS, JS, Git" },
|
||||
{ label: "Current", value: "Rebuilding a personal web identity" },
|
||||
const navItems = [
|
||||
{ label: "技术文章", href: "#articles" },
|
||||
{ label: "生活分享", href: "#life" },
|
||||
{ label: "联系我", href: "mailto:3505891631@qq.com" },
|
||||
{ label: "Gitea", href: "http://124.221.147.173:3000/fly/fly-personal-site" },
|
||||
];
|
||||
|
||||
const chapters = [
|
||||
const portals = [
|
||||
{
|
||||
title: "Engineering Notes",
|
||||
eyebrow: "01",
|
||||
body: "把前端知识拆成可复用的笔记,不追求堆砌,追求每次回看都能拿来解决问题。",
|
||||
href: "http://124.221.147.173:3000/fly/flys_blog",
|
||||
title: "技术文章",
|
||||
href: "#articles",
|
||||
eyebrow: "Knowledge archive",
|
||||
description: "围绕前端工程、服务端基础和 AI Agent 的技术内容索引。",
|
||||
icon: TerminalSquare,
|
||||
gradient: "from-cyan-300 via-blue-500 to-violet-500",
|
||||
},
|
||||
{
|
||||
title: "Personal Experiments",
|
||||
eyebrow: "02",
|
||||
body: "记录一些页面、工具、交互和生活灵感,把学习过程变成能被看见的作品。",
|
||||
href: "http://124.221.147.173:3000/fly/fly-personal-site",
|
||||
},
|
||||
{
|
||||
title: "Quiet Systems",
|
||||
eyebrow: "03",
|
||||
body: "偏爱清晰、克制、有质感的界面。让复杂内容保持秩序,让细节自己发光。",
|
||||
href: "#signal",
|
||||
title: "生活分享",
|
||||
href: "#life",
|
||||
eyebrow: "Life stream",
|
||||
description: "技术之外的日常片段,呈现更完整的个人侧面。",
|
||||
icon: LifeBuoy,
|
||||
gradient: "from-lime-300 via-emerald-400 to-cyan-400",
|
||||
},
|
||||
];
|
||||
|
||||
const stack = [
|
||||
"Next.js",
|
||||
"Tailwind CSS",
|
||||
"Motion",
|
||||
"TypeScript",
|
||||
"Vue",
|
||||
"React",
|
||||
"Git",
|
||||
"VitePress",
|
||||
const noteModules = [
|
||||
{
|
||||
name: "前端",
|
||||
href: "/notes/frontend",
|
||||
icon: Braces,
|
||||
description: "React、Vue、Next.js、CSS、动画、工程化。",
|
||||
},
|
||||
{
|
||||
name: "后端",
|
||||
href: "/notes/backend",
|
||||
icon: Database,
|
||||
description: "API、数据库、部署、鉴权、缓存、服务基础。",
|
||||
},
|
||||
{
|
||||
name: "AI Agent",
|
||||
href: "/notes/ai-agent",
|
||||
icon: Bot,
|
||||
description: "Agent 工作流、工具调用、提示词和自动化实践。",
|
||||
},
|
||||
];
|
||||
|
||||
const lifeTopics = ["Daily", "Reading", "Music", "Photo", "Travel", "Thoughts"];
|
||||
|
||||
export function HomePage() {
|
||||
const reduceMotion = useReducedMotion();
|
||||
const distance = reduceMotion ? 0 : 28;
|
||||
const duration = reduceMotion ? 0 : 0.58;
|
||||
const easeOut = [0.22, 1, 0.36, 1] as const;
|
||||
|
||||
const container = {
|
||||
hidden: {},
|
||||
show: {
|
||||
transition: {
|
||||
staggerChildren: reduceMotion ? 0 : 0.075,
|
||||
delayChildren: reduceMotion ? 0 : 0.08,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const reveal = {
|
||||
hidden: { opacity: 0, y: reduceMotion ? 0 : 24 },
|
||||
show: { opacity: 1, y: 0 },
|
||||
hidden: { opacity: 0, y: distance, filter: reduceMotion ? "none" : "blur(10px)" },
|
||||
show: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration, ease: easeOut } },
|
||||
};
|
||||
|
||||
const scaleReveal = {
|
||||
hidden: { opacity: 0, y: distance, scale: reduceMotion ? 1 : 0.96 },
|
||||
show: { opacity: 1, y: 0, scale: 1, transition: { duration, ease: easeOut } },
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-dvh overflow-hidden bg-[#101411] text-[#f4efe4]">
|
||||
<section className="relative isolate flex min-h-dvh items-center px-5 py-6 sm:px-8 lg:px-12">
|
||||
<div className="absolute inset-0 -z-10 bg-[linear-gradient(90deg,rgba(244,239,228,0.08)_1px,transparent_1px),linear-gradient(180deg,rgba(244,239,228,0.08)_1px,transparent_1px)] bg-[size:72px_72px]" />
|
||||
<div className="absolute inset-x-0 top-0 -z-10 h-80 bg-[radial-gradient(circle_at_18%_18%,rgba(211,255,48,0.18),transparent_34%),radial-gradient(circle_at_82%_4%,rgba(242,124,74,0.22),transparent_28%)]" />
|
||||
<div className="noise-layer" />
|
||||
<main className="min-h-dvh overflow-hidden bg-[#05050b] text-white">
|
||||
<section className="relative isolate min-h-dvh px-5 py-5 sm:px-8 lg:px-10">
|
||||
<motion.div
|
||||
className="neon-aurora"
|
||||
initial={{ opacity: 0, scale: reduceMotion ? 1 : 0.92 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: reduceMotion ? 0 : 1.2, ease: "easeOut" }}
|
||||
/>
|
||||
<motion.div
|
||||
className="cyber-grid"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: reduceMotion ? 0 : 0.9, delay: reduceMotion ? 0 : 0.18 }}
|
||||
/>
|
||||
<motion.div
|
||||
className="scanline"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.18 }}
|
||||
transition={{ duration: reduceMotion ? 0 : 0.8, delay: reduceMotion ? 0 : 0.28 }}
|
||||
/>
|
||||
|
||||
<div className="mx-auto grid w-full max-w-7xl gap-10 lg:grid-cols-[minmax(0,1.05fr)_minmax(360px,0.95fr)] lg:items-center">
|
||||
<motion.header
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
variants={container}
|
||||
className="relative z-10 mx-auto flex max-w-7xl items-center justify-between rounded-full border border-white/10 bg-white/[0.04] px-4 py-3 shadow-2xl shadow-cyan-500/10 backdrop-blur-2xl"
|
||||
>
|
||||
<motion.div variants={scaleReveal}>
|
||||
<Link
|
||||
href="/"
|
||||
className="group inline-flex h-11 items-center gap-3 rounded-full bg-white px-4 text-sm font-black text-[#05050b] outline-none transition hover:bg-cyan-200 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
<Sparkles className="h-4 w-4 transition group-hover:rotate-12" />
|
||||
fly
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
<nav aria-label="主导航" className="hidden items-center gap-1 md:flex">
|
||||
{navItems.map((item) => (
|
||||
<motion.a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
variants={reveal}
|
||||
className="rounded-full px-4 py-2 text-sm text-white/68 outline-none transition hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:ring-fuchsia-300"
|
||||
>
|
||||
{item.label}
|
||||
</motion.a>
|
||||
))}
|
||||
</nav>
|
||||
</motion.header>
|
||||
|
||||
<div className="relative z-10 mx-auto grid min-h-[calc(100dvh-88px)] max-w-7xl gap-8 py-10 lg:grid-cols-[0.9fr_1.1fr] lg:items-center lg:py-6">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
transition={{ staggerChildren: reduceMotion ? 0 : 0.08 }}
|
||||
className="flex min-h-[calc(100dvh-48px)] flex-col justify-between gap-10 py-2 lg:min-h-[calc(100dvh-64px)]"
|
||||
variants={container}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
<motion.header variants={reveal} className="flex items-center justify-between gap-4">
|
||||
<a
|
||||
href="#top"
|
||||
aria-label="fly homepage"
|
||||
className="group inline-flex h-11 items-center gap-3 rounded-full border border-[#f4efe4]/18 bg-[#f4efe4]/8 px-4 text-sm font-medium text-[#f4efe4] outline-none transition hover:border-[#d3ff30]/70 hover:bg-[#d3ff30] hover:text-[#101411] focus-visible:ring-2 focus-visible:ring-[#d3ff30]"
|
||||
>
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-[#d3ff30] transition group-hover:bg-[#101411]" />
|
||||
fly
|
||||
</a>
|
||||
<nav aria-label="Primary navigation" className="hidden items-center gap-2 sm:flex">
|
||||
{["notes", "work", "signal"].map((item) => (
|
||||
<a
|
||||
key={item}
|
||||
href={`#${item}`}
|
||||
className="rounded-full px-4 py-2 text-sm text-[#f4efe4]/68 outline-none transition hover:bg-[#f4efe4]/10 hover:text-[#f4efe4] focus-visible:ring-2 focus-visible:ring-[#d3ff30]"
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</motion.header>
|
||||
|
||||
<div id="top" className="max-w-4xl">
|
||||
<motion.p variants={reveal} className="mb-6 text-sm font-medium text-[#d3ff30]">
|
||||
Personal website / frontend notes / visual experiments
|
||||
</motion.p>
|
||||
<motion.h1
|
||||
variants={reveal}
|
||||
className="max-w-4xl font-display text-7xl font-semibold leading-none text-[#f8f4ea] sm:text-8xl lg:text-9xl"
|
||||
>
|
||||
fly
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
variants={reveal}
|
||||
className="mt-8 max-w-2xl text-lg leading-8 text-[#d8d0bf] sm:text-xl sm:leading-9"
|
||||
>
|
||||
前端开发者,正在把技术笔记、页面实验和个人审美重新整理成一个更自由、更有质感的个人网站。
|
||||
</motion.p>
|
||||
<motion.div variants={reveal} className="mt-9 flex flex-col gap-3 sm:flex-row">
|
||||
<a
|
||||
href="#work"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[#d3ff30] px-6 text-sm font-semibold text-[#101411] outline-none transition hover:bg-[#f4efe4] focus-visible:ring-2 focus-visible:ring-[#f4efe4]"
|
||||
>
|
||||
查看主页结构
|
||||
</a>
|
||||
<a
|
||||
href="http://124.221.147.173:3000/fly/flys_blog"
|
||||
className="inline-flex h-12 items-center justify-center rounded-full border border-[#f4efe4]/22 px-6 text-sm font-semibold text-[#f4efe4] outline-none transition hover:border-[#f27c4a] hover:bg-[#f27c4a] hover:text-[#101411] focus-visible:ring-2 focus-visible:ring-[#f27c4a]"
|
||||
>
|
||||
打开旧博客仓库
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
<motion.p
|
||||
variants={reveal}
|
||||
className="grid gap-3 border-y border-[#f4efe4]/14 py-5 sm:grid-cols-3"
|
||||
className="mb-6 inline-flex rounded-full border border-cyan-300/30 bg-cyan-300/10 px-4 py-2 font-mono text-xs uppercase text-cyan-100 shadow-lg shadow-cyan-500/10"
|
||||
>
|
||||
{highlights.map((item) => (
|
||||
<div key={item.label} className="min-h-24">
|
||||
<p className="text-xs font-semibold uppercase text-[#f27c4a]">{item.label}</p>
|
||||
<p className="mt-3 max-w-[18rem] text-sm leading-6 text-[#f4efe4]/72">{item.value}</p>
|
||||
</div>
|
||||
))}
|
||||
frontend engineer / web experience / creative code
|
||||
</motion.p>
|
||||
<motion.h1
|
||||
variants={reveal}
|
||||
className="font-display text-7xl font-semibold leading-none tracking-normal text-white sm:text-8xl lg:text-[10rem]"
|
||||
>
|
||||
fly
|
||||
</motion.h1>
|
||||
<motion.p variants={reveal} className="mt-7 max-w-2xl text-xl leading-9 text-white/72">
|
||||
我是 fly,前端开发工程师。专注现代 Web 界面、工程化体验和 AI Agent 方向的探索。
|
||||
</motion.p>
|
||||
<motion.div variants={container} className="mt-9 flex flex-col gap-3 sm:flex-row">
|
||||
<motion.a
|
||||
variants={scaleReveal}
|
||||
href="#articles"
|
||||
className="group inline-flex h-12 items-center justify-center gap-2 rounded-full bg-gradient-to-r from-cyan-300 via-blue-400 to-fuchsia-400 px-6 text-sm font-black text-[#05050b] shadow-xl shadow-cyan-500/25 outline-none transition hover:scale-[1.02] focus-visible:ring-2 focus-visible:ring-cyan-200"
|
||||
>
|
||||
进入内容矩阵
|
||||
<ArrowUpRight className="h-4 w-4 transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
|
||||
</motion.a>
|
||||
<motion.a
|
||||
variants={scaleReveal}
|
||||
href="http://124.221.147.173:3000/fly/fly-personal-site"
|
||||
className="inline-flex h-12 items-center justify-center gap-2 rounded-full border border-white/14 bg-white/[0.04] px-6 text-sm font-semibold text-white shadow-xl shadow-fuchsia-500/10 outline-none backdrop-blur transition hover:border-fuchsia-300/70 hover:bg-fuchsia-300/10 focus-visible:ring-2 focus-visible:ring-fuchsia-300"
|
||||
>
|
||||
<GitBranch className="h-4 w-4" />
|
||||
Gitea 仓库
|
||||
</motion.a>
|
||||
<motion.a
|
||||
variants={scaleReveal}
|
||||
href="mailto:3505891631@qq.com"
|
||||
className="inline-flex h-12 items-center justify-center gap-2 rounded-full border border-cyan-300/24 bg-cyan-300/[0.08] px-6 text-sm font-semibold text-cyan-100 shadow-xl shadow-cyan-500/10 outline-none backdrop-blur transition hover:border-cyan-200/80 hover:bg-cyan-300/14 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
3505891631@qq.com
|
||||
</motion.a>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<motion.aside
|
||||
id="signal"
|
||||
initial={{ opacity: 0, y: reduceMotion ? 0 : 28, rotate: reduceMotion ? 0 : 1.5 }}
|
||||
animate={{ opacity: 1, y: 0, rotate: 0 }}
|
||||
transition={{ duration: reduceMotion ? 0 : 0.7, ease: "easeOut" }}
|
||||
className="relative mx-auto w-full max-w-[520px] lg:ml-auto"
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
variants={container}
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="absolute -inset-4 rounded-[2rem] border border-[#f4efe4]/10 bg-[#f4efe4]/6" />
|
||||
<div className="relative overflow-hidden rounded-[1.5rem] border border-[#f4efe4]/16 bg-[#f4efe4]/8 p-3 shadow-2xl shadow-black/40">
|
||||
<Image
|
||||
src="/fly-signal.png"
|
||||
alt="Abstract fly personal field notes artwork"
|
||||
width={1200}
|
||||
height={1400}
|
||||
priority
|
||||
className="aspect-[6/7] w-full rounded-[1rem] object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-5 left-5 right-5 rounded-2xl border border-[#101411]/12 bg-[#f8f4ea]/88 p-4 text-[#101411] shadow-xl shadow-black/20 backdrop-blur">
|
||||
<p className="text-xs font-semibold uppercase text-[#6e6a5e]">signal</p>
|
||||
<p className="mt-2 text-sm leading-6">
|
||||
A new home for notes, projects, experiments, and the quiet parts of making things.
|
||||
</p>
|
||||
</div>
|
||||
</motion.aside>
|
||||
</div>
|
||||
</section>
|
||||
{portals.map((portal, index) => {
|
||||
const Icon = portal.icon;
|
||||
|
||||
<section id="work" className="bg-[#f4efe4] px-5 py-20 text-[#101411] sm:px-8 lg:px-12">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid gap-8 border-t border-[#101411]/16 pt-8 lg:grid-cols-[0.72fr_1.28fr]">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[#a24325]">What this site becomes</p>
|
||||
<h2 className="mt-5 max-w-md font-display text-5xl font-semibold leading-tight sm:text-6xl">
|
||||
一个更像 fly 的个人主页。
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{chapters.map((chapter) => (
|
||||
<a
|
||||
key={chapter.title}
|
||||
href={chapter.href}
|
||||
className="group flex min-h-72 flex-col justify-between rounded-[1.25rem] border border-[#101411]/14 bg-white/55 p-5 outline-none transition hover:-translate-y-1 hover:border-[#101411]/35 hover:bg-white focus-visible:ring-2 focus-visible:ring-[#101411]"
|
||||
return (
|
||||
<motion.a
|
||||
key={portal.title}
|
||||
href={portal.href}
|
||||
variants={scaleReveal}
|
||||
className="group relative overflow-hidden rounded-[1.5rem] border border-white/12 bg-white/[0.055] p-5 shadow-2xl shadow-black/40 outline-none backdrop-blur-2xl transition hover:-translate-y-1 hover:border-white/28 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[#a24325]">{chapter.eyebrow}</p>
|
||||
<h3 className="mt-8 text-2xl font-semibold">{chapter.title}</h3>
|
||||
<p className="mt-5 text-sm leading-7 text-[#4d493f]">{chapter.body}</p>
|
||||
<div className={`absolute inset-y-0 left-0 w-1/2 bg-gradient-to-r ${portal.gradient} opacity-20 blur-3xl transition group-hover:opacity-40`} />
|
||||
<div className="relative flex items-start justify-between gap-6">
|
||||
<div>
|
||||
<p className="font-mono text-xs uppercase text-white/48">0{index + 1} / {portal.eyebrow}</p>
|
||||
<h2 className="mt-5 text-3xl font-black text-white sm:text-4xl">{portal.title}</h2>
|
||||
<p className="mt-3 max-w-md text-sm leading-7 text-white/62">{portal.description}</p>
|
||||
</div>
|
||||
<span className={`inline-flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br ${portal.gradient} text-[#05050b] shadow-lg shadow-cyan-500/20`}>
|
||||
<Icon className="h-6 w-6" />
|
||||
</span>
|
||||
</div>
|
||||
<span className="mt-8 inline-flex h-10 w-10 items-center justify-center rounded-full border border-[#101411]/18 text-lg transition group-hover:bg-[#d3ff30]">
|
||||
/
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.a>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="notes" className="bg-[#101411] px-5 py-20 text-[#f4efe4] sm:px-8 lg:px-12">
|
||||
<div className="mx-auto grid max-w-7xl gap-10 lg:grid-cols-[1fr_1fr] lg:items-end">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[#d3ff30]">Stack and rhythm</p>
|
||||
<h2 className="mt-5 font-display text-5xl font-semibold leading-tight sm:text-6xl">
|
||||
写代码,也整理判断。
|
||||
</h2>
|
||||
<p className="mt-6 max-w-xl text-base leading-8 text-[#d8d0bf]">
|
||||
首页先完成品牌感和结构感。后续可以接入 MDX 博客、项目详情页、关于页和部署流程,把旧 VitePress 的内容慢慢迁过来。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{stack.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="rounded-full border border-[#f4efe4]/16 bg-[#f4efe4]/8 px-4 py-3 text-sm text-[#f4efe4]/78"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
<motion.section
|
||||
id="articles"
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, amount: 0.18 }}
|
||||
variants={container}
|
||||
className="relative px-5 py-24 sm:px-8 lg:px-10"
|
||||
>
|
||||
<motion.div variants={reveal} className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-cyan-300/70 to-transparent" />
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-10 flex flex-col justify-between gap-6 md:flex-row md:items-end">
|
||||
<motion.div variants={container}>
|
||||
<motion.p variants={reveal} className="font-mono text-xs uppercase text-cyan-200">
|
||||
technical archive
|
||||
</motion.p>
|
||||
<motion.h2 variants={reveal} className="mt-4 font-display text-5xl font-semibold sm:text-7xl">
|
||||
技术文章
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
<motion.p variants={reveal} className="max-w-xl text-base leading-8 text-white/58">
|
||||
以「前端」为主线,同时延展到后端基础和 AI Agent。这里展示我的技术内容入口、实践路径和能力边界。
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<motion.div variants={container} className="grid gap-4 lg:grid-cols-3">
|
||||
{noteModules.map((module) => {
|
||||
const Icon = module.icon;
|
||||
|
||||
return (
|
||||
<motion.div key={module.name} variants={scaleReveal}>
|
||||
<Link
|
||||
href={module.href}
|
||||
className="group flex min-h-80 rounded-[1.75rem] border border-white/10 bg-gradient-to-b from-white/[0.08] to-white/[0.025] p-6 shadow-2xl shadow-black/30 outline-none backdrop-blur-xl transition hover:-translate-y-1 hover:border-cyan-300/45 hover:shadow-cyan-500/15 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div>
|
||||
<span className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-cyan-300/12 text-cyan-100 ring-1 ring-cyan-300/25">
|
||||
<Icon className="h-6 w-6" />
|
||||
</span>
|
||||
<h3 className="mt-8 text-4xl font-black">{module.name}</h3>
|
||||
<p className="mt-5 text-sm leading-7 text-white/58">{module.description}</p>
|
||||
</div>
|
||||
<span className="mt-10 inline-flex items-center gap-2 text-sm font-semibold text-cyan-100">
|
||||
打开模块
|
||||
<ArrowUpRight className="h-4 w-4 transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
</motion.section>
|
||||
|
||||
<motion.section
|
||||
id="life"
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, amount: 0.18 }}
|
||||
variants={container}
|
||||
className="px-5 py-24 sm:px-8 lg:px-10"
|
||||
>
|
||||
<motion.div
|
||||
variants={scaleReveal}
|
||||
className="mx-auto overflow-hidden rounded-[2rem] border border-white/10 bg-[radial-gradient(circle_at_18%_12%,rgba(217,70,239,0.28),transparent_32%),radial-gradient(circle_at_78%_80%,rgba(34,211,238,0.24),transparent_30%),rgba(255,255,255,0.05)] p-6 shadow-2xl shadow-fuchsia-500/10 backdrop-blur-xl sm:p-10 lg:max-w-7xl"
|
||||
>
|
||||
<div className="grid gap-10 lg:grid-cols-[0.8fr_1.2fr] lg:items-end">
|
||||
<motion.div variants={container}>
|
||||
<motion.p variants={reveal} className="font-mono text-xs uppercase text-lime-200">
|
||||
life channel
|
||||
</motion.p>
|
||||
<motion.h2 variants={reveal} className="mt-4 font-display text-5xl font-semibold sm:text-7xl">
|
||||
生活分享
|
||||
</motion.h2>
|
||||
<motion.p variants={reveal} className="mt-6 max-w-xl text-base leading-8 text-white/66">
|
||||
技术之外的日常切面。阅读、音乐、照片、旅行和一些阶段性的想法,让这个网站不只是一份技术档案。
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
<motion.div variants={container} className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{lifeTopics.map((topic) => (
|
||||
<motion.div key={topic} variants={scaleReveal}>
|
||||
<Link
|
||||
href="/life"
|
||||
className="block rounded-2xl border border-white/12 bg-black/20 px-5 py-5 text-sm font-bold text-white/82 shadow-lg shadow-black/10 outline-none transition hover:-translate-y-0.5 hover:border-cyan-200/55 hover:bg-cyan-300/[0.08] hover:text-cyan-50 hover:shadow-[0_0_28px_rgba(34,211,238,0.18)] focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
{topic}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.section>
|
||||
|
||||
<motion.footer
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, amount: 0.4 }}
|
||||
variants={container}
|
||||
className="px-5 py-10 text-white/46 sm:px-8 lg:px-10"
|
||||
>
|
||||
<motion.div
|
||||
variants={reveal}
|
||||
className="mx-auto flex max-w-7xl flex-col justify-between gap-4 border-t border-white/10 pt-6 text-sm md:flex-row"
|
||||
>
|
||||
<p>© 2026 fly. Built with Next.js, Tailwind CSS and Motion.</p>
|
||||
<a
|
||||
href="http://124.221.147.173:3000/fly/fly-personal-site"
|
||||
className="inline-flex items-center gap-2 text-cyan-100 outline-none transition hover:text-fuchsia-200 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
Gitea repository
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</a>
|
||||
</motion.div>
|
||||
</motion.footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import Link from "next/link";
|
||||
|
||||
type PlaceholderPageProps = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
items: readonly (
|
||||
| string
|
||||
| {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
)[];
|
||||
};
|
||||
|
||||
export function PlaceholderPage({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
items,
|
||||
}: PlaceholderPageProps) {
|
||||
return (
|
||||
<main className="min-h-dvh bg-[#080807] px-5 py-6 text-[#f7f1e3] sm:px-8 lg:px-12">
|
||||
<div className="mx-auto flex min-h-[calc(100dvh-48px)] max-w-6xl flex-col">
|
||||
<header className="flex items-center justify-between border-b border-[#d6b56d]/20 pb-5">
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-full border border-[#d6b56d]/28 px-4 py-2 font-display text-xl outline-none transition hover:bg-[#d6b56d] hover:text-[#080807] focus-visible:ring-2 focus-visible:ring-[#d6b56d]"
|
||||
>
|
||||
fly
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-sm text-[#d6b56d] outline-none transition hover:text-[#f7f1e3] focus-visible:ring-2 focus-visible:ring-[#d6b56d]"
|
||||
>
|
||||
返回首页
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<section className="grid flex-1 gap-10 py-16 lg:grid-cols-[0.9fr_1.1fr] lg:items-center">
|
||||
<div>
|
||||
<p className="font-mono text-xs uppercase text-[#d6b56d]">{eyebrow}</p>
|
||||
<h1 className="mt-5 font-display text-5xl font-medium leading-tight sm:text-7xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-7 max-w-2xl text-lg leading-9 text-[#b9ad91]">{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
{items.map((item, index) => {
|
||||
const label = typeof item === "string" ? item : item.label;
|
||||
const content = (
|
||||
<div
|
||||
key={label}
|
||||
className="rounded-[1rem] border border-[#d6b56d]/16 bg-[#f7f1e3]/[0.035] p-5 transition hover:border-cyan-300/45 hover:bg-cyan-300/[0.06]"
|
||||
>
|
||||
<p className="font-mono text-xs text-[#d6b56d]">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</p>
|
||||
<p className="mt-3 text-base text-[#f7f1e3]">{label}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (typeof item === "string") {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={label}
|
||||
href={item.href}
|
||||
className="block outline-none focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
type TechDocArticleProps = {
|
||||
contentHtml: string;
|
||||
};
|
||||
|
||||
export function TechDocArticle({ contentHtml }: TechDocArticleProps) {
|
||||
const articleRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const article = articleRef.current;
|
||||
if (!article) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeouts: ReturnType<typeof setTimeout>[] = [];
|
||||
const buttons: HTMLButtonElement[] = [];
|
||||
const copyText = async (text: string) => {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
textarea.remove();
|
||||
};
|
||||
|
||||
article.querySelectorAll("pre").forEach((pre) => {
|
||||
const code = pre.querySelector("code");
|
||||
pre.setAttribute("data-language", code?.getAttribute("lang")?.toUpperCase() || "CODE");
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "tech-doc-code-copy";
|
||||
button.textContent = "复制";
|
||||
|
||||
button.addEventListener("click", async () => {
|
||||
const text = code?.textContent?.trimEnd();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyText(text);
|
||||
button.textContent = "已复制";
|
||||
button.dataset.copied = "true";
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
button.textContent = "复制";
|
||||
delete button.dataset.copied;
|
||||
}, 1400);
|
||||
timeouts.push(timeout);
|
||||
} catch {
|
||||
button.textContent = "复制失败";
|
||||
}
|
||||
});
|
||||
|
||||
pre.appendChild(button);
|
||||
buttons.push(button);
|
||||
});
|
||||
|
||||
return () => {
|
||||
timeouts.forEach(clearTimeout);
|
||||
buttons.forEach((button) => button.remove());
|
||||
};
|
||||
}, [contentHtml]);
|
||||
|
||||
return (
|
||||
<article
|
||||
ref={articleRef}
|
||||
className="tech-doc-content min-w-0 max-w-full rounded-[1.75rem] border border-white/10 bg-[#090915]/95 p-5 shadow-2xl shadow-cyan-500/10 sm:p-8 lg:p-10"
|
||||
dangerouslySetInnerHTML={{ __html: contentHtml }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, ExternalLink } from "lucide-react";
|
||||
import { BackToTopButton } from "@/components/back-to-top-button";
|
||||
import { TechDocArticle } from "@/components/tech-doc-article";
|
||||
import { TechDocToc } from "@/components/tech-doc-toc";
|
||||
|
||||
type KnowledgePoint = {
|
||||
title: string;
|
||||
note?: string;
|
||||
children?: readonly string[];
|
||||
};
|
||||
|
||||
type TechDocPageProps = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
points?: readonly KnowledgePoint[];
|
||||
contentHtml?: string;
|
||||
toc?: readonly {
|
||||
title: string;
|
||||
href: string;
|
||||
}[];
|
||||
source?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export function TechDocPage({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
points = [],
|
||||
contentHtml,
|
||||
toc = [],
|
||||
source,
|
||||
}: TechDocPageProps) {
|
||||
return (
|
||||
<main className="relative min-h-dvh overflow-x-clip bg-[#05050b] px-5 py-6 text-white sm:px-8 lg:px-10">
|
||||
<div className="neon-aurora" />
|
||||
<div className="cyber-grid" />
|
||||
<div className="scanline" />
|
||||
|
||||
<div className="relative z-10 mx-auto w-full max-w-[96rem]">
|
||||
<header className="flex items-center justify-between rounded-full border border-white/10 bg-white/[0.04] px-4 py-3 shadow-2xl shadow-cyan-500/10 backdrop-blur-2xl">
|
||||
<Link
|
||||
href="/notes/frontend"
|
||||
className="inline-flex h-11 items-center gap-2 rounded-full border border-white/12 bg-white/[0.05] px-4 text-sm font-semibold text-white/80 outline-none transition hover:border-cyan-300/70 hover:text-cyan-100 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
前端笔记
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-full bg-white px-4 py-2 text-sm font-black text-[#05050b] outline-none transition hover:bg-cyan-200 focus-visible:ring-2 focus-visible:ring-cyan-300"
|
||||
>
|
||||
fly
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<section className="grid gap-10 py-14 lg:grid-cols-[20rem_minmax(0,64rem)] lg:justify-center lg:py-20">
|
||||
<aside className="min-w-0 lg:w-80">
|
||||
<p className="font-mono text-xs uppercase text-cyan-200">{eyebrow}</p>
|
||||
<h1 className="mt-5 font-display text-5xl font-semibold leading-tight sm:text-7xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-6 max-w-xl text-base leading-8 text-white/62">{description}</p>
|
||||
{source ? (
|
||||
source.href ? (
|
||||
<a
|
||||
href={source.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-8 inline-flex h-11 items-center gap-2 rounded-full border border-fuchsia-300/24 bg-fuchsia-300/[0.08] px-4 text-sm font-semibold text-fuchsia-100 outline-none transition hover:border-fuchsia-200/80 hover:bg-fuchsia-300/14 focus-visible:ring-2 focus-visible:ring-fuchsia-300"
|
||||
>
|
||||
{source.label}
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="mt-8 inline-flex h-11 items-center rounded-full border border-fuchsia-300/24 bg-fuchsia-300/[0.08] px-4 text-sm font-semibold text-fuchsia-100">
|
||||
{source.label}
|
||||
</span>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{toc.length ? <TechDocToc toc={toc} /> : null}
|
||||
</aside>
|
||||
|
||||
{contentHtml ? (
|
||||
<TechDocArticle contentHtml={contentHtml} />
|
||||
) : (
|
||||
<div className="min-w-0 space-y-4">
|
||||
{points.map((point, index) => (
|
||||
<article
|
||||
key={point.title}
|
||||
className="rounded-[1.5rem] border border-white/10 bg-white/[0.045] p-5 shadow-xl shadow-black/20 backdrop-blur transition hover:border-cyan-300/40 hover:bg-cyan-300/[0.06] sm:p-6"
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<span className="mt-1 font-mono text-sm text-cyan-200">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-white sm:text-2xl">{point.title}</h2>
|
||||
{point.note ? (
|
||||
<p className="mt-3 text-sm leading-7 text-white/58">{point.note}</p>
|
||||
) : null}
|
||||
{point.children?.length ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{point.children.map((child) => (
|
||||
<span
|
||||
key={child}
|
||||
className="rounded-full border border-white/10 bg-black/20 px-3 py-2 text-xs font-semibold text-white/70"
|
||||
>
|
||||
{child}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<BackToTopButton />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ListTree } from "lucide-react";
|
||||
|
||||
type TocItem = {
|
||||
title: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
type TechDocTocProps = {
|
||||
toc: readonly TocItem[];
|
||||
};
|
||||
|
||||
export function TechDocToc({ toc }: TechDocTocProps) {
|
||||
return (
|
||||
<div className="mt-8 w-full lg:sticky lg:top-8 lg:w-80">
|
||||
<nav
|
||||
className="tech-doc-toc w-full rounded-[1.25rem] border border-white/10 bg-[#090915]/90 p-4 shadow-2xl shadow-black/20"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-2 font-mono text-xs uppercase tracking-[0.18em] text-cyan-200">
|
||||
<ListTree className="h-4 w-4" />
|
||||
目录
|
||||
</div>
|
||||
<div className="max-h-[calc(100dvh-8rem)] space-y-1 overflow-y-auto pr-1">
|
||||
{toc.map((item, index) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="group flex items-start gap-3 rounded-xl px-3 py-2 text-sm text-white/58 transition hover:bg-cyan-300/[0.08] hover:text-cyan-100"
|
||||
>
|
||||
<span className="block w-7 shrink-0 font-mono text-xs leading-5 text-fuchsia-200/70 transition group-hover:text-fuchsia-100">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<span className="block flex-1 leading-5">{item.title}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
export const frontendArticles = [
|
||||
{
|
||||
slug: "html",
|
||||
title: "HTML 高频知识点",
|
||||
description: "HTML 基础、语义化、HTML5 能力、浏览器渲染模式、表单与多媒体等知识点整理。",
|
||||
file: "html.html",
|
||||
},
|
||||
{
|
||||
slug: "css",
|
||||
title: "CSS 高频知识点",
|
||||
description: "CSS 选择器、布局、盒模型、定位、响应式、动画和工程实践等知识点整理。",
|
||||
file: "css.html",
|
||||
},
|
||||
{
|
||||
slug: "javascript-part-1",
|
||||
title: "JavaScript 高频知识点(上)",
|
||||
description: "JavaScript 基础、作用域、类型、原型、闭包、执行机制等知识点整理。",
|
||||
file: "javascript-part-1.html",
|
||||
},
|
||||
{
|
||||
slug: "javascript-part-2",
|
||||
title: "JavaScript 高频知识点(下)",
|
||||
description: "JavaScript 进阶、异步、模块、浏览器 API 和工程化相关知识点整理。",
|
||||
file: "javascript-part-2.html",
|
||||
},
|
||||
{
|
||||
slug: "vue-part-1",
|
||||
title: "Vue 高频知识点(上)",
|
||||
description: "Vue 基础、组件、响应式、生命周期、模板语法和常见机制整理。",
|
||||
file: "vue-part-1.html",
|
||||
},
|
||||
{
|
||||
slug: "vue-part-2",
|
||||
title: "Vue 高频知识点(下)",
|
||||
description: "Vue 进阶、路由、状态管理、源码机制和实践问题整理。",
|
||||
file: "vue-part-2.html",
|
||||
},
|
||||
{
|
||||
slug: "react-part-1",
|
||||
title: "React 高频知识点(上)",
|
||||
description: "React 基础、组件、状态、事件、Hooks 和渲染机制整理。",
|
||||
file: "react-part-1.html",
|
||||
},
|
||||
{
|
||||
slug: "react-part-2",
|
||||
title: "React 高频知识点(下)",
|
||||
description: "React 进阶、性能优化、状态管理、Fiber 和生态实践整理。",
|
||||
file: "react-part-2.html",
|
||||
},
|
||||
{
|
||||
slug: "network",
|
||||
title: "计算机网络",
|
||||
description: "HTTP、HTTPS、TCP、DNS、缓存、跨域和网络协议相关知识点整理。",
|
||||
file: "network.html",
|
||||
},
|
||||
{
|
||||
slug: "browser",
|
||||
title: "浏览器原理",
|
||||
description: "浏览器渲染、事件循环、缓存、安全、存储和页面加载机制整理。",
|
||||
file: "browser.html",
|
||||
},
|
||||
{
|
||||
slug: "performance",
|
||||
title: "前端性能优化",
|
||||
description: "加载性能、渲染性能、资源优化、监控指标和体验优化方案整理。",
|
||||
file: "performance.html",
|
||||
},
|
||||
{
|
||||
slug: "code-output",
|
||||
title: "代码输出结果",
|
||||
description: "异步编程、事件循环、作用域、this、类型转换等代码输出题整理。",
|
||||
file: "code-output.html",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function getFrontendArticle(slug: string) {
|
||||
return frontendArticles.find((article) => article.slug === slug);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,372 @@
|
||||
<h3 data-id="heading-1" id="html-point-1">1. src和href的区别</h3>
|
||||
<p>src和href都是<strong>用来引用外部的资源</strong>,它们的区别如下:</p>
|
||||
<ul>
|
||||
<li><strong>src:</strong> 表示对资源的引用,它指向的内容会嵌入到当前标签所在的位置。src会将其指向的资源下载并应⽤到⽂档内,如请求js脚本。当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执⾏完毕,所以⼀般js脚本会放在页面底部。</li>
|
||||
<li><strong>href:</strong> 表示超文本引用,它指向一些网络资源,建立和当前元素或本文档的链接关系。当浏览器识别到它他指向的⽂件时,就会并⾏下载资源,不会停⽌对当前⽂档的处理。 常用在a、link等标签上。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-2" id="html-point-2">2. 对HTML语义化的理解</h3>
|
||||
<p><strong>语义化是指根据内容的结构化(内容语义化),选择合适的标签(代码语义化)</strong>。通俗来讲就是用正确的标签做正确的事情。</p>
|
||||
<p>语义化的优点如下:</p>
|
||||
<ul>
|
||||
<li>对机器友好,带有语义的文字表现力丰富,更适合搜索引擎的爬虫爬取有效信息,有利于SEO。除此之外,语义类还支持读屏软件,根据文章可以自动生成目录;</li>
|
||||
<li>对开发者友好,使用语义类标签增强了可读性,结构更加清晰,开发者能清晰的看出网页的结构,便于团队的开发与维护。</li>
|
||||
</ul>
|
||||
<p>常见的语义化标签:</p>
|
||||
<pre><code class="language-html" lang="html"><header></header> 头部
|
||||
|
||||
<nav></nav> 导航栏
|
||||
|
||||
<section></section> 区块(有语义化的div)
|
||||
|
||||
<main></main> 主要区域
|
||||
|
||||
<article></article> 主要内容
|
||||
|
||||
<aside></aside> 侧边栏
|
||||
|
||||
<footer></footer> 底部</code></pre>
|
||||
<h3 data-id="heading-3" id="html-point-3">3. DOCTYPE(⽂档类型) 的作⽤</h3>
|
||||
<p>DOCTYPE是HTML5中一种标准通用标记语言的文档类型声明,它的目的是<strong>告诉浏览器(解析器)应该以什么样(html或xhtml)的文档类型定义来解析文档</strong>,不同的渲染模式会影响浏览器对 CSS 代码甚⾄ JavaScript 脚本的解析。它必须声明在HTML⽂档的第⼀⾏。</p>
|
||||
<p>浏览器渲染页面的两种模式(可通过document.compatMode获取,比如,语雀官网的文档类型是<strong>CSS1Compat</strong>):</p>
|
||||
<ul>
|
||||
<li><strong>CSS1Compat:标准模式(Strick mode)</strong>,默认模式,浏览器使用W3C的标准解析渲染页面。在标准模式中,浏览器以其支持的最高标准呈现页面。</li>
|
||||
<li><strong>BackCompat:怪异模式(混杂模式)(Quick mode)</strong>,浏览器使用自己的怪异模式解析渲染页面。在怪异模式中,页面以一种比较宽松的向后兼容的方式显示。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-4" id="html-point-4">4. script标签中defer和async的区别</h3>
|
||||
<p>如果没有defer或async属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。</p>
|
||||
<p>下图可以直观的看出三者之间的区别:
|
||||
<img src="/articles/html/script-defer-async-timeline.awebp" alt="script defer async 加载时序图" loading="lazy" class="medium-zoom-image">
|
||||
其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。</p>
|
||||
<p><strong>defer 和 async属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析</strong>,其区别如下:</p>
|
||||
<ul>
|
||||
<li><strong>执行顺序:</strong> 多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行;</li>
|
||||
<li><strong>脚本是否并行执行:<strong>async属性,表示</strong>后续文档的加载和执行与js脚本的加载和执行是并行进行的</strong>,即异步执行;defer属性,加载后续文档的过程和js脚本的加载(此时仅加载不执行)是并行进行的(异步),js脚本需要等到文档所有元素解析完成之后才执行,DOMContentLoaded事件触发执行之前。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-5" id="html-point-5">5. 常⽤的meta标签有哪些</h3>
|
||||
<p><code>meta</code> 标签由 <code>name</code> 和 <code>content</code> 属性定义,<strong>用来描述网页文档的属性</strong>,比如网页的作者,网页描述,关键词等,除了HTTP标准固定了一些<code>name</code>作为大家使用的共识,开发者还可以自定义name。</p>
|
||||
<p>常用的meta标签:
|
||||
(1)<code>charset</code>,用来描述HTML文档的编码类型:</p>
|
||||
<pre><code class="language-html" lang="html"><meta charset="UTF-8" ></code></pre>
|
||||
<p>(2) <code>keywords</code>,页面关键词:</p>
|
||||
<pre><code class="language-html" lang="html"><meta name="keywords" content="关键词" /></code></pre>
|
||||
<p>(3)<code>description</code>,页面描述:</p>
|
||||
<pre><code class="language-html" lang="html"><meta name="description" content="页面描述内容" /></code></pre>
|
||||
<p>(4)<code>refresh</code>,页面重定向和刷新:</p>
|
||||
<pre><code class="language-html" lang="html"><meta http-equiv="refresh" content="0;url=" /></code></pre>
|
||||
<p>(5)<code>viewport</code>,适配移动端,可以控制视口的大小和比例:</p>
|
||||
<pre><code class="language-html" lang="html"><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"></code></pre>
|
||||
<p>其中,<code>content</code> 参数有以下几种:</p>
|
||||
<ul>
|
||||
<li><code>width viewport</code> :宽度(数值/device-width)</li>
|
||||
<li><code>height viewport</code> :高度(数值/device-height)</li>
|
||||
<li><code>initial-scale</code> :初始缩放比例</li>
|
||||
<li><code>maximum-scale</code> :最大缩放比例</li>
|
||||
<li><code>minimum-scale</code> :最小缩放比例</li>
|
||||
<li><code>user-scalable</code> :是否允许用户缩放(yes/no)</li>
|
||||
</ul>
|
||||
<p>(6)搜索引擎索引方式:</p>
|
||||
<pre><code class="language-html" lang="html"><meta name="robots" content="index,follow" /></code></pre>
|
||||
<p>其中,<code>content</code> 参数有以下几种:</p>
|
||||
<ul>
|
||||
<li><code>all</code>:文件将被检索,且页面上的链接可以被查询;</li>
|
||||
<li><code>none</code>:文件将不被检索,且页面上的链接不可以被查询;</li>
|
||||
<li><code>index</code>:文件将被检索;</li>
|
||||
<li><code>follow</code>:页面上的链接可以被查询;</li>
|
||||
<li><code>noindex</code>:文件将不被检索;</li>
|
||||
<li><code>nofollow</code>:页面上的链接不可以被查询。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-6" id="html-point-6">6. HTML5有哪些更新</h3>
|
||||
<h4 data-id="heading-7">1. 语义化标签</h4>
|
||||
<ul>
|
||||
<li>header:定义文档的页眉(头部);</li>
|
||||
<li>nav:定义导航链接的部分;</li>
|
||||
<li>footer:定义文档或节的页脚(底部);</li>
|
||||
<li>article:定义文章内容;</li>
|
||||
<li>section:定义文档中的节(section、区段);</li>
|
||||
<li>aside:定义其所处内容之外的内容(侧边);</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-8">2. 媒体标签</h4>
|
||||
<p>(1) audio:音频</p>
|
||||
<pre><code class="language-html" lang="html"><audio src='' controls autoplay loop='true'></audio></code></pre>
|
||||
<p>属性:</p>
|
||||
<ul>
|
||||
<li>controls 控制面板</li>
|
||||
<li>autoplay 自动播放</li>
|
||||
<li>loop=‘true’ 循环播放</li>
|
||||
</ul>
|
||||
<p>(2)video视频</p>
|
||||
<pre><code class="language-html" lang="html"><video src='' poster='imgs/aa.jpg' controls></video></code></pre>
|
||||
<p>属性:</p>
|
||||
<ul>
|
||||
<li>poster:指定视频还没有完全下载完毕,或者用户还没有点击播放前显示的封面。默认显示当前视频文件的第一针画面,当然通过poster也可以自己指定。</li>
|
||||
<li>controls 控制面板</li>
|
||||
<li>width</li>
|
||||
<li>height</li>
|
||||
</ul>
|
||||
<p>(3)source标签
|
||||
因为浏览器对视频格式支持程度不一样,为了能够兼容不同的浏览器,可以通过source来指定视频源。</p>
|
||||
<pre><code class="language-html" lang="html"><video>
|
||||
<source src='aa.flv' type='video/flv'></source>
|
||||
<source src='aa.mp4' type='video/mp4'></source>
|
||||
</video></code></pre>
|
||||
<h4 data-id="heading-9">3. 表单</h4>
|
||||
<p><strong>表单类型:</strong></p>
|
||||
<ul>
|
||||
<li>email :能够验证当前输入的邮箱地址是否合法</li>
|
||||
<li>url : 验证URL</li>
|
||||
<li>number : 只能输入数字,其他输入不了,而且自带上下增大减小箭头,max属性可以设置为最大值,min可以设置为最小值,value为默认值。</li>
|
||||
<li>search : 输入框后面会给提供一个小叉,可以删除输入的内容,更加人性化。</li>
|
||||
<li>range : 可以提供给一个范围,其中可以设置max和min以及value,其中value属性可以设置为默认值</li>
|
||||
<li>color : 提供了一个颜色拾取器</li>
|
||||
<li>time : 时分秒</li>
|
||||
<li>data : 日期选择年月日</li>
|
||||
<li>datatime : 时间和日期(目前只有Safari支持)</li>
|
||||
<li>datatime-local :日期时间控件</li>
|
||||
<li>week :周控件</li>
|
||||
<li>month:月控件</li>
|
||||
</ul>
|
||||
<p><strong>表单属性:</strong></p>
|
||||
<ul>
|
||||
<li>placeholder :提示信息</li>
|
||||
<li>autofocus :自动获取焦点</li>
|
||||
<li>autocomplete=“on” 或者 autocomplete=“off” 使用这个属性需要有两个前提:
|
||||
<ul>
|
||||
<li>表单必须提交过</li>
|
||||
<li>必须有name属性。</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>required:要求输入框不能为空,必须有值才能够提交。</li>
|
||||
<li>pattern=" " 里面写入想要的正则模式,例如手机号patte="^(+86)?\d{10}$"</li>
|
||||
<li>multiple:可以选择多个文件或者多个邮箱</li>
|
||||
<li>form=" form表单的ID"</li>
|
||||
</ul>
|
||||
<p><strong>表单事件:</strong></p>
|
||||
<ul>
|
||||
<li>oninput 每当input里的输入框内容发生变化都会触发此事件。</li>
|
||||
<li>oninvalid 当验证不通过时触发此事件。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-10">4. 进度条、度量器</h4>
|
||||
<ul>
|
||||
<li>progress标签:用来表示任务的进度(IE、Safari不支持),max用来表示任务的进度,value表示已完成多少</li>
|
||||
<li>meter属性:用来显示剩余容量或剩余库存(IE、Safari不支持)
|
||||
<ul>
|
||||
<li>high/low:规定被视作高/低的范围</li>
|
||||
<li>max/min:规定最大/小值</li>
|
||||
<li>value:规定当前度量值</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>设置规则:min < low < high < max</p>
|
||||
<h4 data-id="heading-11">5.DOM查询操作</h4>
|
||||
<ul>
|
||||
<li>document.querySelector()</li>
|
||||
<li>document.querySelectorAll()</li>
|
||||
</ul>
|
||||
<p>它们选择的对象可以是标签,可以是类(需要加点),可以是ID(需要加#)</p>
|
||||
<h4 data-id="heading-12">6. Web存储</h4>
|
||||
<p>HTML5 提供了两种在客户端存储数据的新方法:</p>
|
||||
<ul>
|
||||
<li>localStorage - 没有时间限制的数据存储</li>
|
||||
<li>sessionStorage - 针对一个 session 的数据存储</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-13">7. 其他</h4>
|
||||
<ul>
|
||||
<li>拖放:拖放是一种常见的特性,即抓取对象以后拖到另一个位置。设置元素可拖放:</li>
|
||||
</ul>
|
||||
<pre><code class="language-html" lang="html"><img draggable="true" /></code></pre>
|
||||
<ul>
|
||||
<li>画布(canvas ): canvas 元素使用 JavaScript 在网页上绘制图像。画布是一个矩形区域,可以控制其每一像素。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。</li>
|
||||
</ul>
|
||||
<pre><code class="language-html" lang="html"><canvas id="myCanvas" width="200" height="100"></canvas></code></pre>
|
||||
<ul>
|
||||
<li>SVG:SVG 指可伸缩矢量图形,用于定义用于网络的基于矢量的图形,使用 XML 格式定义图形,图像在放大或改变尺寸的情况下其图形质量不会有损失,它是万维网联盟的标准</li>
|
||||
<li>地理定位:Geolocation(地理定位)用于定位用户的位置。‘</li>
|
||||
</ul>
|
||||
<p><strong>总结:</strong>
|
||||
(1)新增语义化标签:nav、header、footer、aside、section、article
|
||||
(2)音频、视频标签:audio、video
|
||||
(3)数据存储:localStorage、sessionStorage
|
||||
(4)canvas(画布)、Geolocation(地理定位)、websocket(通信协议)
|
||||
(5)input标签新增属性:placeholder、autocomplete、autofocus、required
|
||||
(6)history API:go、forward、back、pushstate</p>
|
||||
<p><strong>移除的元素有:</strong></p>
|
||||
<ul>
|
||||
<li>纯表现的元素:basefont,big,center,font, s,strike,tt,u;</li>
|
||||
<li>对可用性产生负面影响的元素:frame,frameset,noframes;</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-14" id="html-point-7">7. img的srcset属性的作⽤?</h3>
|
||||
<p>响应式页面中经常用到根据屏幕密度设置不同的图片。这时就用到了 img 标签的srcset属性。srcset属性用于设置不同屏幕密度下,img 会自动加载不同的图片。用法如下:</p>
|
||||
<pre><code class="language-html" lang="html"><img src="image-128.png" srcset="image-256.png 2x" /></code></pre>
|
||||
<p>使用上面的代码,就能实现在屏幕密度为1x的情况下加载image-128.png, 屏幕密度为2x时加载image-256.png。</p>
|
||||
<p>按照上面的实现,不同的屏幕密度都要设置图片地址,目前的屏幕密度有1x,2x,3x,4x四种,如果每一个图片都设置4张图片,加载就会很慢。所以就有了新的srcset标准。代码如下:</p>
|
||||
<pre><code class="language-html" lang="html"><img src="image-128.png"
|
||||
srcset="image-128.png 128w, image-256.png 256w, image-512.png 512w"
|
||||
sizes="(max-width: 360px) 340px, 128px" /></code></pre>
|
||||
<p>其中srcset指定图片的地址和对应的图片质量。sizes用来设置图片的尺寸零界点。对于 srcset 中的 w 单位,可以理解成图片质量。如果可视区域小于这个质量的值,就可以使用。浏览器会自动选择一个最小的可用图片。</p>
|
||||
<p>sizes语法如下:</p>
|
||||
<pre><code class="language-html" lang="html">sizes="[media query] [length], [media query] [length] ... "</code></pre>
|
||||
<p>sizes就是指默认显示128px, 如果视区宽度大于360px, 则显示340px。</p>
|
||||
<h3 data-id="heading-15" id="html-point-8">8. 行内元素有哪些?块级元素有哪些? 空(void)元素有那些?</h3>
|
||||
<ul>
|
||||
<li>行内元素有:<code>a b span img input select strong</code>;</li>
|
||||
<li>块级元素有:<code>div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p</code>;</li>
|
||||
</ul>
|
||||
<p>空元素,即没有内容的HTML元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签:</p>
|
||||
<ul>
|
||||
<li>常见的有:<code><br></code>、<code><hr></code>、<code><img></code>、<code><input></code>、<code><link></code>、<code><meta></code>;</li>
|
||||
<li>鲜见的有:<code><area></code>、<code><base></code>、<code><col></code>、<code><colgroup></code>、<code><command></code>、<code><embed></code>、<code><keygen></code>、<code><param></code>、<code><source></code>、<code><track></code>、<code><wbr></code>。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-16" id="html-point-9">9. 说一下 web worker</h3>
|
||||
<p>在 HTML 页面中,如果在执行脚本时,页面的状态是不可相应的,直到脚本执行完成后,页面才变成可相应。web worker 是运行在后台的 js,独立于其他脚本,不会影响页面的性能。 并且通过 postMessage 将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了。</p>
|
||||
<p>如何创建 web worker:</p>
|
||||
<ol>
|
||||
<li>检测浏览器对于 web worker 的支持性</li>
|
||||
<li>创建 web worker 文件(js,回传函数等)</li>
|
||||
<li>创建 web worker 对象</li>
|
||||
</ol>
|
||||
<h3 data-id="heading-17" id="html-point-10">10. HTML5的离线储存怎么使用,它的工作原理是什么</h3>
|
||||
<p>离线存储指的是:在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件。</p>
|
||||
<p>**原理:**HTML5的离线存储是基于一个新建的 <code>.appcache</code> 文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像cookie一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示</p>
|
||||
<p><strong>使用方法:</strong>
|
||||
(1)创建一个和 html 同名的 manifest 文件,然后在页面头部加入 manifest 属性:</p>
|
||||
<pre><code class="language-html" lang="html"><html lang="en" manifest="index.manifest"></code></pre>
|
||||
<p>(2)在 <code>cache.manifest</code> 文件中编写需要离线存储的资源:</p>
|
||||
<pre><code class="language-html" lang="html">CACHE MANIFEST
|
||||
#v0.11
|
||||
CACHE:
|
||||
js/app.js
|
||||
css/style.css
|
||||
NETWORK:
|
||||
resourse/logo.png
|
||||
FALLBACK:
|
||||
/ /offline.html</code></pre>
|
||||
<ul>
|
||||
<li><strong>CACHE</strong>: 表示需要离线存储的资源列表,由于包含 manifest 文件的页面将被自动离线存储,所以不需要把页面自身也列出来。</li>
|
||||
<li><strong>NETWORK</strong>: 表示在它下面列出来的资源只有在在线的情况下才能访问,他们不会被离线存储,所以在离线情况下无法使用这些资源。不过,如果在 CACHE 和 NETWORK 中有一个相同的资源,那么这个资源还是会被离线存储,也就是说 CACHE 的优先级更高。</li>
|
||||
<li><strong>FALLBACK</strong>: 表示如果访问第一个资源失败,那么就使用第二个资源来替换他,比如上面这个文件表示的就是如果访问根目录下任何一个资源失败了,那么就去访问 offline.html 。</li>
|
||||
</ul>
|
||||
<p>(3)在离线状态时,操作 <code>window.applicationCache</code> 进行离线缓存的操作。</p>
|
||||
<p><strong>如何更新缓存:</strong></p>
|
||||
<p>(1)更新 manifest 文件</p>
|
||||
<p>(2)通过 javascript 操作</p>
|
||||
<p>(3)清除浏览器缓存</p>
|
||||
<p><strong>注意事项:</strong></p>
|
||||
<p>(1)浏览器对缓存数据的容量限制可能不太一样(某些浏览器设置的限制是每个站点 5MB)。</p>
|
||||
<p>(2)如果 manifest 文件,或者内部列举的某一个文件不能正常下载,整个更新过程都将失败,浏览器继续全部使用老的缓存。</p>
|
||||
<p>(3)引用 manifest 的 html 必须与 manifest 文件同源,在同一个域下。</p>
|
||||
<p>(4)FALLBACK 中的资源必须和 manifest 文件同源。</p>
|
||||
<p>(5)当一个资源被缓存后,该浏览器直接请求这个绝对路径也会访问缓存中的资源。</p>
|
||||
<p>(6)站点中的其他页面即使没有设置 manifest 属性,请求的资源如果在缓存中也从缓存中访问。</p>
|
||||
<p>(7)当 manifest 文件发生改变时,资源请求本身也会触发更新。</p>
|
||||
<h3 data-id="heading-18" id="html-point-11">11. 浏览器是如何对 HTML5 的离线储存资源进行管理和加载?</h3>
|
||||
<ul>
|
||||
<li><strong>在线的情况下</strong>,浏览器发现 html 头部有 manifest 属性,它会请求 manifest 文件,如果是第一次访问页面 ,那么浏览器就会根据 manifest 文件的内容下载相应的资源并且进行离线存储。如果已经访问过页面并且资源已经进行离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的 manifest 文件与旧的 manifest 文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,就会重新下载文件中的资源并进行离线存储。</li>
|
||||
<li><strong>离线的情况下</strong>,浏览器会直接使用离线存储的资源。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-19" id="html-point-12">12. title与h1的区别、b与strong的区别、i与em的区别?</h3>
|
||||
<ul>
|
||||
<li>strong标签有语义,是起到加重语气的效果,而b标签是没有的,b标签只是一个简单加粗标签。b标签之间的字符都设为粗体,strong标签加强字符的语气都是通过粗体来实现的,而搜索引擎更侧重strong标签。</li>
|
||||
<li>title属性没有明确意义只表示是个标题,H1则表示层次明确的标题,对页面信息的抓取有很大的影响</li>
|
||||
<li><strong>i内容展示为斜体,em表示强调的文本</strong></li>
|
||||
</ul>
|
||||
<h3 data-id="heading-20" id="html-point-13">13. <strong>iframe 有那些优点和缺点?</strong></h3>
|
||||
<p>iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。</p>
|
||||
<p><strong>优点:</strong></p>
|
||||
<ul>
|
||||
<li>用来加载速度较慢的内容(如广告)</li>
|
||||
<li>可以使脚本可以并行下载</li>
|
||||
<li>可以实现跨子域通信</li>
|
||||
</ul>
|
||||
<p><strong>缺点:</strong></p>
|
||||
<ul>
|
||||
<li>iframe 会阻塞主页面的 onload 事件</li>
|
||||
<li>无法被一些搜索引擎索识别</li>
|
||||
<li>会产生很多页面,不容易管理</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-21" id="html-point-14">14. label 的作用是什么?如何使用?</h3>
|
||||
<p>label标签来定义表单控件的关系:当用户选择label标签时,浏览器会自动将焦点转到和label标签相关的表单控件上。</p>
|
||||
<ul>
|
||||
<li>使用方法1:</li>
|
||||
</ul>
|
||||
<pre><code class="language-html" lang="html"><label for="mobile">Number:</label>
|
||||
<input type="text" id="mobile"/></code></pre>
|
||||
<ul>
|
||||
<li>使用方法2:</li>
|
||||
</ul>
|
||||
<pre><code class="language-html" lang="html"><label>Date:<input type="text"/></label></code></pre>
|
||||
<h3 data-id="heading-22" id="html-point-15">15. Canvas和SVG的区别</h3>
|
||||
<p><strong>(1)SVG:</strong>
|
||||
SVG可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标记语言XML描述的2D图形的语言,SVG基于XML就意味着SVG DOM中的每个元素都是可用的,可以为某个元素附加Javascript事件处理器。在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。</p>
|
||||
<p>其特点如下:</p>
|
||||
<ul>
|
||||
<li>不依赖分辨率</li>
|
||||
<li>支持事件处理器</li>
|
||||
<li>最适合带有大型渲染区域的应用程序(比如谷歌地图)</li>
|
||||
<li>复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)</li>
|
||||
<li>不适合游戏应用</li>
|
||||
</ul>
|
||||
<p><strong>(2)Canvas:</strong>
|
||||
Canvas是画布,通过Javascript来绘制2D图形,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。</p>
|
||||
<p>其特点如下:</p>
|
||||
<ul>
|
||||
<li>依赖分辨率</li>
|
||||
<li>不支持事件处理器</li>
|
||||
<li>弱的文本渲染能力</li>
|
||||
<li>能够以 .png 或 .jpg 格式保存结果图像</li>
|
||||
<li>最适合图像密集型的游戏,其中的许多对象会被频繁重绘</li>
|
||||
</ul>
|
||||
<p>注:矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。</p>
|
||||
<h3 data-id="heading-23" id="html-point-16">16. head 标签有什么作用,其中什么标签必不可少?</h3>
|
||||
标签用于定义文档的头部,它是所有头部元素的容器。 中的元素可以引用脚本、指示浏览器在哪里找到样式表、提供元信息等。
|
||||
<p>文档的头部描述了文档的各种属性和信息,包括文档的标题、在 Web 中的位置以及和其他文档的关系等。绝大多数文档头部包含的数据都不会真正作为内容显示给读者。</p>
|
||||
<p>下面这些标签可用在 head 部分:<code><base>, <link>, <meta>, <script>, <style>, <title></code>。</p>
|
||||
<p>其中 <code><title></code> 定义文档的标题,它是 head 部分中唯一必需的元素。</p>
|
||||
<h3 data-id="heading-24" id="html-point-17">17. 文档声明(Doctype)和<code><!Doctype html></code>有何作用? 严格模式与混杂模式如何区分?它们有何意义?</h3>
|
||||
<p><strong>文档声明的作用:</strong> 文档声明是为了告诉浏览器,当前<code>HTML</code>文档使用什么版本的<code>HTML</code>来写的,这样浏览器才能按照声明的版本来正确的解析。</p>
|
||||
<p><strong>的作用:</strong><code><!doctype html></code> 的作用就是让浏览器进入标准模式,使用最新的 <code>HTML5</code> 标准来解析渲染页面;如果不写,浏览器就会进入混杂模式,我们需要避免此类情况发生。</p>
|
||||
<p><strong>严格模式与混杂模式的区分:</strong></p>
|
||||
<ul>
|
||||
<li><strong>严格模式</strong>: 又称为标准模式,指浏览器按照<code>W3C</code>标准解析代码;</li>
|
||||
<li><strong>混杂模式</strong>: 又称怪异模式、兼容模式,是指浏览器用自己的方式解析代码。混杂模式通常模拟老式浏览器的行为,以防止老站点无法工作;</li>
|
||||
</ul>
|
||||
<p><strong>区分</strong>:网页中的<code>DTD</code>,直接影响到使用的是严格模式还是浏览模式,可以说<code>DTD</code>的使用与这两种方式的区别息息相关。</p>
|
||||
<ul>
|
||||
<li>如果文档包含严格的<code>DOCTYPE</code> ,那么它一般以严格模式呈现(<strong>严格 DTD ——严格模式</strong>);</li>
|
||||
<li>包含过渡 <code>DTD</code> 和 <code>URI</code> 的 <code>DOCTYPE</code> ,也以严格模式呈现,但有过渡 <code>DTD</code> 而没有 <code>URI</code> (统一资源标识符,就是声明最后的地址)会导致页面以混杂模式呈现(<strong>有 URI 的过渡 DTD ——严格模式;没有 URI 的过渡 DTD ——混杂模式</strong>);</li>
|
||||
<li><code>DOCTYPE</code> 不存在或形式不正确会导致文档以混杂模式呈现(<strong>DTD不存在或者格式不正确——混杂模式</strong>);</li>
|
||||
<li><code>HTML5</code> 没有 <code>DTD</code> ,因此也就没有严格模式与混杂模式的区别,<code>HTML5</code> 有相对宽松的 法,实现时,已经尽可能大的实现了向后兼容(<strong>HTML5 没有严格和混杂之分</strong>)。</li>
|
||||
</ul>
|
||||
<p>总之,<strong>严格模式让各个浏览器统一执行一套规范兼容模式保证了旧网站的正常运行。</strong></p>
|
||||
<h3 data-id="heading-25" id="html-point-18">18. 浏览器乱码的原因是什么?如何解决?</h3>
|
||||
<p><strong>产生乱码的原因:</strong></p>
|
||||
<ul>
|
||||
<li>网页源代码是<code>gbk</code>的编码,而内容中的中文字是<code>utf-8</code>编码的,这样浏览器打开即会出现<code>html</code>乱码,反之也会出现乱码;</li>
|
||||
<li><code>html</code>网页编码是<code>gbk</code>,而程序从数据库中调出呈现是<code>utf-8</code>编码的内容也会造成编码乱码;</li>
|
||||
<li>浏览器不能自动检测网页编码,造成网页乱码。</li>
|
||||
</ul>
|
||||
<p><strong>解决办法:</strong></p>
|
||||
<ul>
|
||||
<li>使用软件编辑HTML网页内容;</li>
|
||||
<li>如果网页设置编码是<code>gbk</code>,而数据库储存数据编码格式是<code>UTF-8</code>,此时需要程序查询数据库数据显示数据前进程序转码;</li>
|
||||
<li>如果浏览器浏览时候出现网页乱码,在浏览器中找到转换编码的菜单进行转换。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-26" id="html-point-19">19. 渐进增强和优雅降级之间的区别</h3>
|
||||
<p><strong>(1)渐进增强(progressive enhancement)</strong>:主要是针对低版本的浏览器进行页面重构,保证基本的功能情况下,再针对高级浏览器进行效果、交互等方面的改进和追加功能,以达到更好的用户体验。
|
||||
<strong>(2)优雅降级 graceful degradation</strong>: 一开始就构建完整的功能,然后再针对低版本的浏览器进行兼容。</p>
|
||||
<p><strong>两者区别:</strong></p>
|
||||
<ul>
|
||||
<li>优雅降级是从复杂的现状开始的,并试图减少用户体验的供给;而渐进增强是从一个非常基础的,能够起作用的版本开始的,并在此基础上不断扩充,以适应未来环境的需要;</li>
|
||||
<li>降级(功能衰竭)意味着往回看,而渐进增强则意味着往前看,同时保证其根基处于安全地带。</li>
|
||||
</ul>
|
||||
<p>“优雅降级”观点认为应该针对那些最高级、最完善的浏览器来设计网站。而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。 在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable)” 的浏览体验。可以做一些小的调整来适应某个特定的浏览器。但由于它们并非我们所关注的焦点,因此除了修复较大的错误之外,其它的差异将被直接忽略。</p>
|
||||
<p>“渐进增强”观点则认为应关注于内容本身。内容是建立网站的诱因,有的网站展示它,有的则收集它,有的寻求,有的操作,还有的网站甚至会包含以上的种种,但相同点是它们全都涉及到内容。这使得“渐进增强”成为一种更为合理的设计范例。这也是它立即被 Yahoo 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support)”策略的原因所在。</p>
|
||||
<h3 data-id="heading-27" id="html-point-20">20. 说一下 HTML5 drag API</h3>
|
||||
<ul>
|
||||
<li>dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发。</li>
|
||||
<li>darg:事件主体是被拖放元素,在正在拖放被拖放元素时触发。</li>
|
||||
<li>dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。</li>
|
||||
<li>dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。</li>
|
||||
<li>dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。</li>
|
||||
<li>drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。</li>
|
||||
<li>dragend:事件主体是被拖放元素,在整个拖放操作结束时触发。</li>
|
||||
</ul>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,598 @@
|
||||
<h3 data-id="heading-3" id="javascript-part-2-point-1">1. 对this对象的理解</h3>
|
||||
<p>this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。</p>
|
||||
<ul>
|
||||
<li>第一种是<strong>函数调用模式</strong>,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。</li>
|
||||
<li>第二种是<strong>方法调用模式</strong>,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。</li>
|
||||
<li>第三种是<strong>构造器调用模式</strong>,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。</li>
|
||||
<li>第四种是 <strong>apply 、 call 和 bind 调用模式</strong>,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。</li>
|
||||
</ul>
|
||||
<p>这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。</p>
|
||||
<h3 data-id="heading-4" id="javascript-part-2-point-2">2. call() 和 apply() 的区别?</h3>
|
||||
<p>它们的作用一模一样,区别仅在于传入参数的形式的不同。</p>
|
||||
<ul>
|
||||
<li>apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。</li>
|
||||
<li>call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-5" id="javascript-part-2-point-3">3. 实现call、apply 及 bind 函数</h3>
|
||||
<p><strong>(1)call 函数的实现步骤:</strong></p>
|
||||
<ul>
|
||||
<li>判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。</li>
|
||||
<li>判断传入上下文对象是否存在,如果不存在,则设置为 window 。</li>
|
||||
<li>处理传入的参数,截取第一个参数后的所有参数。</li>
|
||||
<li>将函数作为上下文对象的一个属性。</li>
|
||||
<li>使用上下文对象来调用这个方法,并保存返回结果。</li>
|
||||
<li>删除刚才新增的属性。</li>
|
||||
<li>返回结果。</li>
|
||||
</ul>
|
||||
<pre><code class="language-javascript" lang="javascript">Function.prototype.myCall = function(context) {
|
||||
// 判断调用对象
|
||||
if (typeof this !== "function") {
|
||||
console.error("type error");
|
||||
}
|
||||
// 获取参数
|
||||
let args = [...arguments].slice(1),
|
||||
result = null;
|
||||
// 判断 context 是否传入,如果未传入则设置为 window
|
||||
context = context || window;
|
||||
// 将调用函数设为对象的方法
|
||||
context.fn = this;
|
||||
// 调用函数
|
||||
result = context.fn(...args);
|
||||
// 将属性删除
|
||||
delete context.fn;
|
||||
return result;
|
||||
};</code></pre>
|
||||
<p><strong>(2)apply 函数的实现步骤:</strong></p>
|
||||
<ul>
|
||||
<li>判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。</li>
|
||||
<li>判断传入上下文对象是否存在,如果不存在,则设置为 window 。</li>
|
||||
<li>将函数作为上下文对象的一个属性。</li>
|
||||
<li>判断参数值是否传入</li>
|
||||
<li>使用上下文对象来调用这个方法,并保存返回结果。</li>
|
||||
<li>删除刚才新增的属性</li>
|
||||
<li>返回结果</li>
|
||||
</ul>
|
||||
<pre><code class="language-javascript" lang="javascript">Function.prototype.myApply = function(context) {
|
||||
// 判断调用对象是否为函数
|
||||
if (typeof this !== "function") {
|
||||
throw new TypeError("Error");
|
||||
}
|
||||
let result = null;
|
||||
// 判断 context 是否存在,如果未传入则为 window
|
||||
context = context || window;
|
||||
// 将函数设为对象的方法
|
||||
context.fn = this;
|
||||
// 调用方法
|
||||
if (arguments[1]) {
|
||||
result = context.fn(...arguments[1]);
|
||||
} else {
|
||||
result = context.fn();
|
||||
}
|
||||
// 将属性删除
|
||||
delete context.fn;
|
||||
return result;
|
||||
};</code></pre>
|
||||
<p><strong>(3)bind 函数的实现步骤:</strong></p>
|
||||
<ul>
|
||||
<li>判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。</li>
|
||||
<li>保存当前函数的引用,获取其余传入参数值。</li>
|
||||
<li>创建一个函数返回</li>
|
||||
<li>函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。</li>
|
||||
</ul>
|
||||
<pre><code class="language-javascript" lang="javascript">Function.prototype.myBind = function(context) {
|
||||
// 判断调用对象是否为函数
|
||||
if (typeof this !== "function") {
|
||||
throw new TypeError("Error");
|
||||
}
|
||||
// 获取参数
|
||||
var args = [...arguments].slice(1),
|
||||
fn = this;
|
||||
return function Fn() {
|
||||
// 根据调用方式,传入不同绑定值
|
||||
return fn.apply(
|
||||
this instanceof Fn ? this : context,
|
||||
args.concat(...arguments)
|
||||
);
|
||||
};
|
||||
};</code></pre>
|
||||
<h2 data-id="heading-6">七、异步编程</h2>
|
||||
<h3 data-id="heading-7" id="javascript-part-2-point-4">1. 异步编程的实现方式?</h3>
|
||||
<p>JavaScript中的异步机制可以分为以下几种:</p>
|
||||
<ul>
|
||||
<li><strong>回调函数</strong> 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。</li>
|
||||
<li><strong>Promise</strong> 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。</li>
|
||||
<li><strong>generator</strong> 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。</li>
|
||||
<li><strong>async 函数</strong> 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-8" id="javascript-part-2-point-5">2. setTimeout、Promise、Async/Await 的区别</h3>
|
||||
<h4 data-id="heading-9">(1)setTimeout</h4>
|
||||
<pre><code class="language-javascript" lang="javascript">console.log('script start') //1. 打印 script start
|
||||
setTimeout(function(){
|
||||
console.log('settimeout') // 4. 打印 settimeout
|
||||
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
|
||||
console.log('script end') //3. 打印 script start
|
||||
// 输出顺序:script start->script end->settimeout</code></pre>
|
||||
<h4 data-id="heading-10">(2)Promise</h4>
|
||||
<p>Promise本身是<strong>同步的立即执行函数</strong>, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">console.log('script start')
|
||||
let promise1 = new Promise(function (resolve) {
|
||||
console.log('promise1')
|
||||
resolve()
|
||||
console.log('promise1 end')
|
||||
}).then(function () {
|
||||
console.log('promise2')
|
||||
})
|
||||
setTimeout(function(){
|
||||
console.log('settimeout')
|
||||
})
|
||||
console.log('script end')
|
||||
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout</code></pre>
|
||||
<p>当JS主线程执行到Promise对象时:</p>
|
||||
<ul>
|
||||
<li>promise1.then() 的回调就是一个 task</li>
|
||||
<li>promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue</li>
|
||||
<li>promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中</li>
|
||||
<li>setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-11">(3)async/await</h4>
|
||||
<pre><code class="language-javascript" lang="javascript">async function async1(){
|
||||
console.log('async1 start');
|
||||
await async2();
|
||||
console.log('async1 end')
|
||||
}
|
||||
async function async2(){
|
||||
console.log('async2')
|
||||
}
|
||||
console.log('script start');
|
||||
async1();
|
||||
console.log('script end')
|
||||
// 输出顺序:script start->async1 start->async2->script end->async1 end</code></pre>
|
||||
<p>async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。</p>
|
||||
<p>例如:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">async function func1() {
|
||||
return 1
|
||||
}
|
||||
console.log(func1())</code></pre>
|
||||
<p><img src="/articles/javascript-part-2/javascript-part-2-figure-1.awebp" alt="" loading="lazy">
|
||||
func1的运行结果其实就是一个Promise对象。因此也可以使用then来处理后续逻辑。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">func1().then(res => {
|
||||
console.log(res); // 30
|
||||
})</code></pre>
|
||||
<p>await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。</p>
|
||||
<h3 data-id="heading-12" id="javascript-part-2-point-6">3. 对Promise的理解</h3>
|
||||
<p>Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。</p>
|
||||
<p>所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。</p>
|
||||
<p>(1)Promise的实例有<strong>三个状态</strong>:</p>
|
||||
<ul>
|
||||
<li>Pending(进行中)</li>
|
||||
<li>Resolved(已完成)</li>
|
||||
<li>Rejected(已拒绝)</li>
|
||||
</ul>
|
||||
<p>当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。</p>
|
||||
<p>(2)Promise的实例有<strong>两个过程</strong>:</p>
|
||||
<ul>
|
||||
<li>pending -> fulfilled : Resolved(已完成)</li>
|
||||
<li>pending -> rejected:Rejected(已拒绝)</li>
|
||||
</ul>
|
||||
<p>注意:一旦从进行状态变成为其他状态就永远不能更改状态了。</p>
|
||||
<p><strong>Promise的特点:</strong></p>
|
||||
<ul>
|
||||
<li>对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,<code>pending</code>(进行中)、<code>fulfilled</code>(已成功)、<code>rejected</code>(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“<strong>承诺</strong>”;</li>
|
||||
<li>一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从<code>pending</code>变为<code>fulfilled</code>,从<code>pending</code>变为<code>rejected</code>。这时就称为<code>resolved</code>(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。</li>
|
||||
</ul>
|
||||
<p><strong>Promise的缺点:</strong></p>
|
||||
<ul>
|
||||
<li>无法取消Promise,一旦新建它就会立即执行,无法中途取消。</li>
|
||||
<li>如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。</li>
|
||||
<li>当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。</li>
|
||||
</ul>
|
||||
<p><strong>总结:</strong>
|
||||
Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。</p>
|
||||
<p>状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。</p>
|
||||
<p><strong>注意:</strong> 在构造 <code>Promise</code> 的时候,构造函数内部的代码是立即执行的</p>
|
||||
<h3 data-id="heading-13" id="javascript-part-2-point-7">4. Promise的基本用法</h3>
|
||||
<h4 data-id="heading-14">(1)创建Promise对象</h4>
|
||||
<p>Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。</p>
|
||||
<p>Promise构造函数接受一个函数作为参数,该函数的两个参数分别是<code>resolve</code>和<code>reject</code>。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">const promise = new Promise(function(resolve, reject) {
|
||||
// ... some code
|
||||
if (/* 异步操作成功 */){
|
||||
resolve(value);
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});</code></pre>
|
||||
<p><strong>一般情况下都会使用</strong><code>new Promise()</code><strong>来创建promise对象,但是也可以使用</strong><code>promise.resolve</code><strong>和</strong><code>promise.reject</code><strong>这两个方法:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Promise.resolve</strong></li>
|
||||
</ul>
|
||||
<p><code>Promise.resolve(value)</code>的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">Promise.resolve(11).then(function(value){
|
||||
console.log(value); // 打印出11
|
||||
});</code></pre>
|
||||
<p><code>resolve(11)</code>代码中,会让promise对象进入确定(<code>resolve</code>状态),并将参数<code>11</code>传递给后面的<code>then</code>所指定的<code>onFulfilled</code> 函数;</p>
|
||||
<p>创建promise对象可以使用<code>new Promise</code>的形式创建对象,也可以使用<code>Promise.resolve(value)</code>的形式创建promise对象;</p>
|
||||
<ul>
|
||||
<li><strong>Promise.reject</strong></li>
|
||||
</ul>
|
||||
<p><code>Promise.reject</code> 也是<code>new Promise</code>的快捷形式,也创建一个promise对象。代码如下:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">Promise.reject(new Error(“我错了,请原谅俺!!”));</code></pre>
|
||||
<p>就是下面的代码new Promise的简单形式:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">new Promise(function(resolve,reject){
|
||||
reject(new Error("我错了!"));
|
||||
});</code></pre>
|
||||
<p>下面是使用resolve方法和reject方法:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">function testPromise(ready) {
|
||||
return new Promise(function(resolve,reject){
|
||||
if(ready) {
|
||||
resolve("hello world");
|
||||
}else {
|
||||
reject("No thanks");
|
||||
}
|
||||
});
|
||||
};
|
||||
// 方法调用
|
||||
testPromise(true).then(function(msg){
|
||||
console.log(msg);
|
||||
},function(error){
|
||||
console.log(error);
|
||||
});</code></pre>
|
||||
<p>上面的代码的含义是给<code>testPromise</code>方法传递一个参数,返回一个promise对象,如果为<code>true</code>的话,那么调用promise对象中的<code>resolve()</code>方法,并且把其中的参数传递给后面的<code>then</code>第一个函数内,因此打印出 “<code>hello world</code>”, 如果为<code>false</code>的话,会调用promise对象中的<code>reject()</code>方法,则会进入<code>then</code>的第二个函数内,会打印<code>No thanks</code>;</p>
|
||||
<h4 data-id="heading-15">(2)Promise方法</h4>
|
||||
<p>Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。</p>
|
||||
<ol>
|
||||
<li><strong>then()</strong></li>
|
||||
</ol>
|
||||
<p>当Promise执行的内容符合成功条件时,调用<code>resolve</code>函数,失败就调用<code>reject</code>函数。Promise创建完了,那该如何调用呢?</p>
|
||||
<pre><code class="language-javascript" lang="javascript">promise.then(function(value) {
|
||||
// success
|
||||
}, function(error) {
|
||||
// failure
|
||||
});</code></pre>
|
||||
<p><code>then</code>方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为<code>resolved</code>时调用,第二个回调函数是Promise对象的状态变为<code>rejected</code>时调用。其中第二个参数可以省略。
|
||||
<code>then</code>方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即<code>then</code>方法后面再调用另一个then方法。</p>
|
||||
<p>当要写有顺序的异步事件时,需要串行时,可以这样写:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">let promise = new Promise((resolve,reject)=>{
|
||||
ajax('first').success(function(res){
|
||||
resolve(res);
|
||||
})
|
||||
})
|
||||
promise.then(res=>{
|
||||
return new Promise((resovle,reject)=>{
|
||||
ajax('second').success(function(res){
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
}).then(res=>{
|
||||
return new Promise((resovle,reject)=>{
|
||||
ajax('second').success(function(res){
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
}).then(res=>{
|
||||
|
||||
})</code></pre>
|
||||
<p>那当要写的事件没有顺序或者关系时,还如何写呢?可以使用<code>all</code> 方法来解决。</p>
|
||||
<p><strong>2. catch()</strong></p>
|
||||
<p>Promise对象除了有then方法,还有一个catch方法,该方法相当于<code>then</code>方法的第二个参数,指向<code>reject</code>的回调函数。不过<code>catch</code>方法还有一个作用,就是在执行<code>resolve</code>回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入<code>catch</code>方法中。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">p.then((data) => {
|
||||
console.log('resolved',data);
|
||||
},(err) => {
|
||||
console.log('rejected',err);
|
||||
}
|
||||
);
|
||||
p.then((data) => {
|
||||
console.log('resolved',data);
|
||||
}).catch((err) => {
|
||||
console.log('rejected',err);
|
||||
});</code></pre>
|
||||
<p><strong>3. all()</strong></p>
|
||||
<p><code>all</code>方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个<code>promise</code>对象。当数组中所有的<code>promise</code>的状态都达到<code>resolved</code>的时候,<code>all</code>方法的状态就会变成<code>resolved</code>,如果有一个状态变成了<code>rejected</code>,那么<code>all</code>方法的状态就会变成<code>rejected</code>。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">javascript
|
||||
let promise1 = new Promise((resolve,reject)=>{
|
||||
setTimeout(()=>{
|
||||
resolve(1);
|
||||
},2000)
|
||||
});
|
||||
let promise2 = new Promise((resolve,reject)=>{
|
||||
setTimeout(()=>{
|
||||
resolve(2);
|
||||
},1000)
|
||||
});
|
||||
let promise3 = new Promise((resolve,reject)=>{
|
||||
setTimeout(()=>{
|
||||
resolve(3);
|
||||
},3000)
|
||||
});
|
||||
Promise.all([promise1,promise2,promise3]).then(res=>{
|
||||
console.log(res);
|
||||
//结果为:[1,2,3]
|
||||
})</code></pre>
|
||||
<p>调用<code>all</code>方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象<code>resolve</code>执行时的值。</p>
|
||||
<p><strong>(4)race()</strong></p>
|
||||
<p><code>race</code>方法和<code>all</code>一样,接受的参数是一个每项都是<code>promise</code>的数组,但是与<code>all</code>不同的是,当最先执行完的事件执行完之后,就直接返回该<code>promise</code>对象的值。如果第一个<code>promise</code>对象状态变成<code>resolved</code>,那自身的状态变成了<code>resolved</code>;反之第一个<code>promise</code>变成<code>rejected</code>,那自身状态就会变成<code>rejected</code>。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">let promise1 = new Promise((resolve,reject)=>{
|
||||
setTimeout(()=>{
|
||||
reject(1);
|
||||
},2000)
|
||||
});
|
||||
let promise2 = new Promise((resolve,reject)=>{
|
||||
setTimeout(()=>{
|
||||
resolve(2);
|
||||
},1000)
|
||||
});
|
||||
let promise3 = new Promise((resolve,reject)=>{
|
||||
setTimeout(()=>{
|
||||
resolve(3);
|
||||
},3000)
|
||||
});
|
||||
Promise.race([promise1,promise2,promise3]).then(res=>{
|
||||
console.log(res);
|
||||
//结果:2
|
||||
},rej=>{
|
||||
console.log(rej)};
|
||||
)</code></pre>
|
||||
<p>那么<code>race</code>方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})</code></pre>
|
||||
<p><strong>5. finally()</strong></p>
|
||||
<p><code>finally</code>方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">promise
|
||||
.then(result => {···})
|
||||
.catch(error => {···})
|
||||
.finally(() => {···});</code></pre>
|
||||
<p>上面代码中,不管<code>promise</code>最后的状态,在执行完<code>then</code>或<code>catch</code>指定的回调函数以后,都会执行<code>finally</code>方法指定的回调函数。</p>
|
||||
<p>下面是一个例子,服务器使用 Promise 处理请求,然后使用<code>finally</code>方法关掉服务器。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">server.listen(port)
|
||||
.then(function () {
|
||||
// ...
|
||||
})
|
||||
.finally(server.stop);</code></pre>
|
||||
<p><code>finally</code>方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是<code>fulfilled</code>还是<code>rejected</code>。这表明,<code>finally</code>方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。<code>finally</code>本质上是<code>then</code>方法的特例:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">promise
|
||||
.finally(() => {
|
||||
// 语句
|
||||
});
|
||||
// 等同于
|
||||
promise
|
||||
.then(
|
||||
result => {
|
||||
// 语句
|
||||
return result;
|
||||
},
|
||||
error => {
|
||||
// 语句
|
||||
throw error;
|
||||
}
|
||||
);</code></pre>
|
||||
<p>上面代码中,如果不使用<code>finally</code>方法,同样的语句需要为成功和失败两种情况各写一次。有了<code>finally</code>方法,则只需要写一次。</p>
|
||||
<h3 data-id="heading-16" id="javascript-part-2-point-8">5. Promise解决了什么问题</h3>
|
||||
<p>在工作中经常会碰到这样一个需求,比如我使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求;那么需要如下编写代码:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">let fs = require('fs')
|
||||
fs.readFile('./a.txt','utf8',function(err,data){
|
||||
fs.readFile(data,'utf8',function(err,data){
|
||||
fs.readFile(data,'utf8',function(err,data){
|
||||
console.log(data)
|
||||
})
|
||||
})
|
||||
})</code></pre>
|
||||
<p>上面的代码有如下缺点:</p>
|
||||
<ul>
|
||||
<li>后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致多个ajax请求嵌套的情况,代码不够直观。</li>
|
||||
<li>如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上编写代码,导致代码不够直观。</li>
|
||||
</ul>
|
||||
<p><code>Promise</code>出现之后,代码变成这样:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">let fs = require('fs')
|
||||
function read(url){
|
||||
return new Promise((resolve,reject)=>{
|
||||
fs.readFile(url,'utf8',function(error,data){
|
||||
error && reject(error)
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
read('./a.txt').then(data=>{
|
||||
return read(data)
|
||||
}).then(data=>{
|
||||
return read(data)
|
||||
}).then(data=>{
|
||||
console.log(data)
|
||||
})</code></pre>
|
||||
<p>这样代码看起了就简洁了很多,解决了地狱回调的问题。</p>
|
||||
<h3 data-id="heading-17" id="javascript-part-2-point-9">6. Promise.all和Promise.race的区别的使用场景</h3>
|
||||
<p><strong>(1)Promise.all</strong>
|
||||
<code>Promise.all</code>可以将多个<code>Promise</code>实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是<strong>一个结果数组</strong>,而失败的时候则返回<strong>最先被reject失败状态的值</strong>。</p>
|
||||
<p>Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。</p>
|
||||
<p>需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。</p>
|
||||
<p><strong>(2)Promise.race</strong></p>
|
||||
<p>顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})</code></pre>
|
||||
<h3 data-id="heading-18" id="javascript-part-2-point-10">7. 对async/await 的理解</h3>
|
||||
<p>async/await其实是<code>Generator</code> 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中,先来看看async函数返回了什么:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">async function testAsy(){
|
||||
return 'hello world';
|
||||
}
|
||||
let result = testAsy();
|
||||
console.log(result)</code></pre>
|
||||
<p><img src="/articles/javascript-part-2/javascript-part-2-figure-2.awebp" alt="" loading="lazy"></p>
|
||||
<p>所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 <code>return</code> 一个直接量,async 会把这个直接量通过 <code>Promise.resolve()</code> 封装成 Promise 对象。</p>
|
||||
<p>async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:<code>then()</code> 链来处理这个 Promise 对象,就像这样:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">async function testAsy(){
|
||||
return 'hello world'
|
||||
}
|
||||
let result = testAsy()
|
||||
console.log(result)
|
||||
result.then(v=>{
|
||||
console.log(v) // hello world
|
||||
})</code></pre>
|
||||
<p>那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 <code>Promise.resolve(undefined)</code>。</p>
|
||||
<p>联想一下 Promise 的特点——无等待,所以在没有 <code>await</code> 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。</p>
|
||||
<p><strong>注意:</strong><code>Promise.resolve(x)</code> 可以看作是 <code>new Promise(resolve => resolve(x))</code> 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。</p>
|
||||
<h3 data-id="heading-19" id="javascript-part-2-point-11">8. await 到底在等啥?</h3>
|
||||
<p><strong>await 在等待什么呢?</strong> 一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。</p>
|
||||
<p>因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">function getSomething() {
|
||||
return "something";
|
||||
}
|
||||
async function testAsync() {
|
||||
return Promise.resolve("hello async");
|
||||
}
|
||||
async function test() {
|
||||
const v1 = await getSomething();
|
||||
const v2 = await testAsync();
|
||||
console.log(v1, v2);
|
||||
}
|
||||
test();</code></pre>
|
||||
<p>await 表达式的运算结果取决于它等的是什么。</p>
|
||||
<ul>
|
||||
<li>如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。</li>
|
||||
<li>如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。</li>
|
||||
</ul>
|
||||
<p>来看一个例子:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">function testAsy(x){
|
||||
return new Promise(resolve=>{setTimeout(() => {
|
||||
resolve(x);
|
||||
}, 3000)
|
||||
}
|
||||
)
|
||||
}
|
||||
async function testAwt(){
|
||||
let result = await testAsy('hello world');
|
||||
console.log(result); // 3秒钟之后出现hello world
|
||||
console.log('cuger') // 3秒钟之后出现cug
|
||||
}
|
||||
testAwt();
|
||||
console.log('cug') //立即输出cug</code></pre>
|
||||
<p>这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以'cug''最先输出,hello world'和‘cuger’是3秒钟后同时出现的。</p>
|
||||
<h3 data-id="heading-20" id="javascript-part-2-point-12">9. async/await的优势</h3>
|
||||
<p>单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。</p>
|
||||
<p>假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用 <code>setTimeout</code> 来模拟异步操作:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">/**
|
||||
* 传入参数 n,表示这个函数执行的时间(毫秒)
|
||||
* 执行的结果是 n + 200,这个值将用于下一步骤
|
||||
*/
|
||||
function takeLongTime(n) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(n + 200), n);
|
||||
});
|
||||
}
|
||||
function step1(n) {
|
||||
console.log(`step1 with ${n}`);
|
||||
return takeLongTime(n);
|
||||
}
|
||||
function step2(n) {
|
||||
console.log(`step2 with ${n}`);
|
||||
return takeLongTime(n);
|
||||
}
|
||||
function step3(n) {
|
||||
console.log(`step3 with ${n}`);
|
||||
return takeLongTime(n);
|
||||
}</code></pre>
|
||||
<p>现在用 Promise 方式来实现这三个步骤的处理:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">function doIt() {
|
||||
console.time("doIt");
|
||||
const time1 = 300;
|
||||
step1(time1)
|
||||
.then(time2 => step2(time2))
|
||||
.then(time3 => step3(time3))
|
||||
.then(result => {
|
||||
console.log(`result is ${result}`);
|
||||
console.timeEnd("doIt");
|
||||
});
|
||||
}
|
||||
doIt();
|
||||
// c:\var\test>node --harmony_async_await .
|
||||
// step1 with 300
|
||||
// step2 with 500
|
||||
// step3 with 700
|
||||
// result is 900
|
||||
// doIt: 1507.251ms</code></pre>
|
||||
<p>输出结果 <code>result</code> 是 <code>step3()</code> 的参数 <code>700 + 200</code> = <code>900</code>。<code>doIt()</code> 顺序执行了三个步骤,一共用了 <code>300 + 500 + 700 = 1500</code> 毫秒,和 <code>console.time()/console.timeEnd()</code> 计算的结果一致。</p>
|
||||
<p>如果用 async/await 来实现呢,会是这样:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">async function doIt() {
|
||||
console.time("doIt");
|
||||
const time1 = 300;
|
||||
const time2 = await step1(time1);
|
||||
const time3 = await step2(time2);
|
||||
const result = await step3(time3);
|
||||
console.log(`result is ${result}`);
|
||||
console.timeEnd("doIt");
|
||||
}
|
||||
doIt();</code></pre>
|
||||
<p>结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样</p>
|
||||
<h3 data-id="heading-21" id="javascript-part-2-point-13">10. async/await对比Promise的优势</h3>
|
||||
<ul>
|
||||
<li>代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担</li>
|
||||
<li>Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅</li>
|
||||
<li>错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余</li>
|
||||
<li>调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-22" id="javascript-part-2-point-14">11. async/await 如何捕获异常</h3>
|
||||
<pre><code class="language-javascript" lang="javascript">async function fn(){
|
||||
try{
|
||||
let a = await Promise.reject('error')
|
||||
}catch(error){
|
||||
console.log(error)
|
||||
}
|
||||
}</code></pre>
|
||||
<h2 data-id="heading-23">八、面向对象</h2>
|
||||
<h3 data-id="heading-24" id="javascript-part-2-point-15">1. 对象创建的方式有哪些?</h3>
|
||||
<p>一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:</p>
|
||||
<p>(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。</p>
|
||||
<p>(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。</p>
|
||||
<p>(3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。</p>
|
||||
<p>(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。</p>
|
||||
<p>(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。</p>
|
||||
<p>(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。</p>
|
||||
<h3 data-id="heading-25" id="javascript-part-2-point-16">2. 对象继承的方式有哪些?</h3>
|
||||
<p>(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。</p>
|
||||
<p>(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。</p>
|
||||
<p>(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。</p>
|
||||
<p>(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。</p>
|
||||
<p>(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是自定义类型时。缺点是没有办法实现函数的复用。</p>
|
||||
<p>(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。</p>
|
||||
<h2 data-id="heading-26">九、垃圾回收与内存泄漏</h2>
|
||||
<h3 data-id="heading-27" id="javascript-part-2-point-17">1. 浏览器的垃圾回收机制</h3>
|
||||
<h4 data-id="heading-28">(1)垃圾回收的概念</h4>
|
||||
<p><strong>垃圾回收</strong>:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。</p>
|
||||
<p><strong>回收机制</strong>:</p>
|
||||
<ul>
|
||||
<li>Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。</li>
|
||||
<li>JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。</li>
|
||||
<li>不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-29">(2)垃圾回收的方式</h4>
|
||||
<p>浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。
|
||||
<strong>1)标记清除</strong></p>
|
||||
<ul>
|
||||
<li>标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。</li>
|
||||
<li>垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。</li>
|
||||
</ul>
|
||||
<p><strong>2)引用计数</strong></p>
|
||||
<ul>
|
||||
<li>另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。</li>
|
||||
<li>这种方法会引起<strong>循环引用</strong>的问题:例如:<code> obj1</code>和<code>obj2</code>通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,<code>obj1</code>和<code>obj2</code>还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。</li>
|
||||
</ul>
|
||||
<pre><code class="language-javascript" lang="javascript">function fun() {
|
||||
let obj1 = {};
|
||||
let obj2 = {};
|
||||
obj1.a = obj2; // obj1 引用 obj2
|
||||
obj2.a = obj1; // obj2 引用 obj1
|
||||
}</code></pre>
|
||||
<p>这种情况下,就要手动释放变量占用的内存:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">obj1.a = null
|
||||
obj2.a = null</code></pre>
|
||||
<h4 data-id="heading-30">(3)减少垃圾回收</h4>
|
||||
<p>虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。</p>
|
||||
<ul>
|
||||
<li><strong>对数组进行优化:</strong> 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。</li>
|
||||
<li><strong>对</strong><code>object</code><strong>进行优化:</strong> 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。</li>
|
||||
<li><strong>对函数进行优化:</strong> 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-31" id="javascript-part-2-point-18">2. 哪些情况会导致内存泄漏</h3>
|
||||
<p>以下四种情况会造成内存的泄漏:</p>
|
||||
<ul>
|
||||
<li><strong>意外的全局变量:</strong> 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。</li>
|
||||
<li><strong>被遗忘的计时器或回调函数:</strong> 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。</li>
|
||||
<li><strong>脱离 DOM 的引用:</strong> 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。</li>
|
||||
<li><strong>闭包:</strong> 不合理的使用闭包,从而导致某些变量一直被留在内存当中。</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,956 @@
|
||||
<h3 data-id="heading-2" id="network-point-1">1. GET和POST的请求的区别</h3>
|
||||
<p>Post 和 Get 是 HTTP 请求的两种方法,其区别如下:</p>
|
||||
<ul>
|
||||
<li><strong>应用场景:</strong> GET 请求是一个幂等的请求,一般 Get 请求用于对服务器资源不会产生影响的场景,比如说请求一个网页的资源。而 Post 不是一个幂等的请求,一般用于对服务器资源会产生影响的情景,比如注册用户这一类的操作。</li>
|
||||
<li><strong>是否缓存:</strong> 因为两者应用场景不同,浏览器一般会对 Get 请求缓存,但很少对 Post 请求缓存。</li>
|
||||
<li><strong>发送的报文格式:</strong> Get 请求的报文中实体部分为空,Post 请求的报文中实体部分一般为向服务器发送的数据。</li>
|
||||
<li><strong>安全性:</strong> Get 请求可以将请求的参数放入 url 中向服务器发送,这样的做法相对于 Post 请求来说是不太安全的,因为请求的 url 会被保留在历史记录中。</li>
|
||||
<li><strong>请求长度:</strong> 浏览器由于对 url 长度的限制,所以会影响 get 请求发送数据时的长度。这个限制是浏览器规定的,并不是 RFC 规定的。</li>
|
||||
<li><strong>参数类型:</strong> post 的参数传递支持更多的数据类型。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-3" id="network-point-2">2. POST和PUT请求的区别</h3>
|
||||
<ul>
|
||||
<li>PUT请求是向服务器端发送数据,从而修改数据的内容,但是不会增加数据的种类等,也就是说无论进行多少次PUT操作,其结果并没有不同。(可以理解为时<strong>更新数据</strong>)</li>
|
||||
<li>POST请求是向服务器端发送数据,该请求会改变数据的种类等资源,它会创建新的内容。(可以理解为是<strong>创建数据</strong>)</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-4" id="network-point-3">3. 常见的HTTP请求头和响应头</h3>
|
||||
<p><strong>HTTP Request Header 常见的请求头:</strong></p>
|
||||
<ul>
|
||||
<li>Accept:浏览器能够处理的内容类型</li>
|
||||
<li>Accept-Charset:浏览器能够显示的字符集</li>
|
||||
<li>Accept-Encoding:浏览器能够处理的压缩编码</li>
|
||||
<li>Accept-Language:浏览器当前设置的语言</li>
|
||||
<li>Connection:浏览器与服务器之间连接的类型</li>
|
||||
<li>Cookie:当前页面设置的任何Cookie</li>
|
||||
<li>Host:发出请求的页面所在的域</li>
|
||||
<li>Referer:发出请求的页面的URL</li>
|
||||
<li>User-Agent:浏览器的用户代理字符串</li>
|
||||
</ul>
|
||||
<p><strong>HTTP Responses Header 常见的响应头:</strong></p>
|
||||
<ul>
|
||||
<li>Date:表示消息发送的时间,时间的描述格式由rfc822定义</li>
|
||||
<li>server:服务器名称</li>
|
||||
<li>Connection:浏览器与服务器之间连接的类型</li>
|
||||
<li>Cache-Control:控制HTTP缓存</li>
|
||||
<li>content-type:表示后面的文档属于什么MIME类型</li>
|
||||
</ul>
|
||||
<p>常见的 Content-Type 属性值有以下四种:</p>
|
||||
<p>(1)application/x-www-form-urlencoded:浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL转码。</p>
|
||||
<p>(2)multipart/form-data:该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。</p>
|
||||
<p>(3)application/json:服务器消息主体是序列化后的 JSON 字符串。</p>
|
||||
<p>(4)text/xml:该种方式主要用来提交 XML 格式的数据。</p>
|
||||
<h3 data-id="heading-5" id="network-point-4">4. HTTP状态码304是多好还是少好</h3>
|
||||
<p>服务器为了提高网站访问速度,对之前访问的部分页面指定缓存机制,当客户端在此对这些页面进行请求,服务器会根据缓存内容判断页面与之前是否相同,若相同便直接返回304,此时客户端调用缓存内容,不必进行二次下载。</p>
|
||||
<p>状态码304不应该认为是一种错误,而是对客户端<strong>有缓存情况下</strong>服务端的一种响应。</p>
|
||||
<p>搜索引擎蜘蛛会更加青睐内容源更新频繁的网站。通过特定时间内对网站抓取返回的状态码来调节对该网站的抓取频次。若网站在一定时间内一直处于304的状态,那么蜘蛛可能会降低对网站的抓取次数。相反,若网站变化的频率非常之快,每次抓取都能获取新内容,那么日积月累,的回访率也会提高。</p>
|
||||
<p><strong>产生较多304状态码的原因:</strong></p>
|
||||
<ul>
|
||||
<li>页面更新周期长或不更新</li>
|
||||
<li>纯静态页面或强制生成静态html</li>
|
||||
</ul>
|
||||
<p><strong>304状态码出现过多会造成以下问题:</strong></p>
|
||||
<ul>
|
||||
<li>网站快照停止;</li>
|
||||
<li>收录减少;</li>
|
||||
<li>权重下降。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-6" id="network-point-5">5. 常见的HTTP请求方法</h3>
|
||||
<ul>
|
||||
<li>GET: 向服务器获取数据;</li>
|
||||
<li>POST:将实体提交到指定的资源,通常会造成服务器资源的修改;</li>
|
||||
<li>PUT:上传文件,更新数据;</li>
|
||||
<li>DELETE:删除服务器上的对象;</li>
|
||||
<li>HEAD:获取报文首部,与GET相比,不返回报文主体部分;</li>
|
||||
<li>OPTIONS:询问支持的请求方法,用来跨域请求;</li>
|
||||
<li>CONNECT:要求在与代理服务器通信时建立隧道,使用隧道进行TCP通信;</li>
|
||||
<li>TRACE: 回显服务器收到的请求,主要⽤于测试或诊断。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-7" id="network-point-6">6. OPTIONS请求方法及使用场景</h3>
|
||||
<p>OPTIONS是除了GET和POST之外的其中一种 HTTP请求方法。</p>
|
||||
<p>OPTIONS方法是用于请求获得由<code>Request-URI</code>标识的资源在请求/响应的通信过程中可以使用的功能选项。通过这个方法,客户端可以<strong>在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能</strong>。该请求方法的响应不能缓存。</p>
|
||||
<p>OPTIONS请求方法的<strong>主要用途</strong>有两个:</p>
|
||||
<ul>
|
||||
<li>获取服务器支持的所有HTTP请求方法;</li>
|
||||
<li>用来检查访问权限。例如:在进行 CORS 跨域资源共享时,对于复杂请求,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-8" id="network-point-7">7. HTTP 1.0 和 HTTP 1.1 之间有哪些区别?</h3>
|
||||
<p><strong>HTTP 1.0和 HTTP 1.1 有以下区别</strong>:</p>
|
||||
<ul>
|
||||
<li><strong>连接方面</strong>,http1.0 默认使用非持久连接,而 http1.1 默认使用持久连接。http1.1 通过使用持久连接来使多个 http 请求复用同一个 TCP 连接,以此来避免使用非持久连接时每次需要建立连接的时延。</li>
|
||||
<li><strong>资源请求方面</strong>,在 http1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,http1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。</li>
|
||||
<li><strong>缓存方面</strong>,在 http1.0 中主要使用 header 里的 If-Modified-Since、Expires 来做为缓存判断的标准,http1.1 则引入了更多的缓存控制策略,例如 Etag、If-Unmodified-Since、If-Match、If-None-Match 等更多可供选择的缓存头来控制缓存策略。</li>
|
||||
<li>http1.1 中<strong>新增了 host 字段</strong>,用来指定服务器的域名。http1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。因此有了 host 字段,这样就可以将请求发往到同一台服务器上的不同网站。</li>
|
||||
<li>http1.1 相对于 http1.0 还新增了很多<strong>请求方法</strong>,如 PUT、HEAD、OPTIONS 等。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-9" id="network-point-8">8. HTTP 1.1 和 HTTP 2.0 的区别</h3>
|
||||
<ul>
|
||||
<li><strong>二进制协议</strong>:HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。</li>
|
||||
<li><strong>多路复用:</strong> HTTP/2 实现了多路复用,HTTP/2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"【1】的问题。</li>
|
||||
<li><strong>数据流:</strong> HTTP/2 使用了数据流的概念,因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流 ID ,用来区分它属于哪个数据流。</li>
|
||||
<li><strong>头信息压缩:</strong> HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。</li>
|
||||
<li><strong>服务器推送:</strong> HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用 SSE 等方式向客户端发送即时数据的推送是不同的。</li>
|
||||
</ul>
|
||||
<p><strong>【1】队头堵塞:</strong></p>
|
||||
<blockquote>
|
||||
<p>队头阻塞是由 HTTP 基本的“请求 - 应答”模型所导致的。HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求是没有优先级的,只有入队的先后顺序,排在最前面的请求会被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本,造成了队头堵塞的现象。</p>
|
||||
</blockquote>
|
||||
<h3 data-id="heading-10" id="network-point-9">9. HTTP和HTTPS协议的区别</h3>
|
||||
<p>HTTP和HTTPS协议的主要区别如下:</p>
|
||||
<ul>
|
||||
<li>HTTPS协议需要CA证书,费用较高;而HTTP协议不需要;</li>
|
||||
<li>HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议;</li>
|
||||
<li>使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443;</li>
|
||||
<li>HTTP协议连接很简单,是无状态的;HTTPS协议是有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-11" id="network-point-10">10. GET方法URL长度限制的原因</h3>
|
||||
<p>实际上HTTP协议规范并没有对get方法请求的url长度进行限制,这个限制是特定的浏览器及服务器对它的限制。
|
||||
IE对URL长度的限制是2083字节(2K+35)。由于IE浏览器对URL长度的允许值是最小的,所以开发过程中,只要URL不超过2083字节,那么在所有浏览器中工作都不会有问题。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">GET的长度值 = URL(2083)- (你的Domain+Path)-2(2是get请求中?=两个字符的长度)</code></pre>
|
||||
<p>下面看一下主流浏览器对get方法中url的长度限制范围:</p>
|
||||
<ul>
|
||||
<li>Microsoft Internet Explorer (Browser):IE浏览器对URL的最大限制为2083个字符,如果超过这个数字,提交按钮没有任何反应。</li>
|
||||
<li>Firefox (Browser):对于Firefox浏览器URL的长度限制为 65,536 个字符。</li>
|
||||
<li>Safari (Browser):URL最大长度限制为 80,000 个字符。</li>
|
||||
<li>Opera (Browser):URL最大长度限制为 190,000 个字符。</li>
|
||||
<li>Google (chrome):URL最大长度限制为 8182 个字符。</li>
|
||||
</ul>
|
||||
<p>主流的服务器对get方法中url的长度限制范围:</p>
|
||||
<ul>
|
||||
<li>Apache (Server):能接受最大url长度为8192个字符。</li>
|
||||
<li>Microsoft Internet Information Server(IIS):能接受最大url的长度为16384个字符。</li>
|
||||
</ul>
|
||||
<p>根据上面的数据,可以知道,get方法中的URL长度最长不超过2083个字符,这样所有的浏览器和服务器都可能正常工作。</p>
|
||||
<h3 data-id="heading-12" id="network-point-11">11. 当在浏览器中输入 Google.com 并且按下回车之后发生了什么?</h3>
|
||||
<p>(1)<strong>解析URL:</strong> 首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。</p>
|
||||
<p>(2)<strong>缓存判断:</strong> 浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。</p>
|
||||
<p>(3)<strong>DNS解析:</strong> 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。</p>
|
||||
<p>(4)<strong>获取MAC地址:</strong> 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。</p>
|
||||
<p>(5)<strong>TCP三次握手:</strong> 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。</p>
|
||||
<p>(6)<strong>HTTPS握手:</strong> 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。</p>
|
||||
<p>(7)<strong>返回数据:</strong> 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。</p>
|
||||
<p>(8)<strong>页面渲染:</strong> 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。</p>
|
||||
<p>(9)<strong>TCP四次挥手:</strong> 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。</p>
|
||||
<h3 data-id="heading-13" id="network-point-12">12. 对keep-alive的理解</h3>
|
||||
<p>HTTP1.0 中默认是在每次请求/应答,客户端和服务器都要新建一个连接,完成之后立即断开连接,这就是<strong>短连接</strong>。当使用Keep-Alive模式时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接,这就是<strong>长连接</strong>。其使用方法如下:</p>
|
||||
<ul>
|
||||
<li>HTTP1.0版本是默认没有Keep-alive的(也就是默认会发送keep-alive),所以要想连接得到保持,必须手动配置发送<code>Connection: keep-alive</code>字段。若想断开keep-alive连接,需发送<code>Connection:close</code>字段;</li>
|
||||
<li>HTTP1.1规定了默认保持长连接,数据传输完成了保持TCP连接不断开,等待在同域名下继续用这个通道传输数据。如果需要关闭,需要客户端发送<code>Connection:close</code>首部字段。</li>
|
||||
</ul>
|
||||
<p>Keep-Alive的<strong>建立过程</strong>:</p>
|
||||
<ul>
|
||||
<li>客户端向服务器在发送请求报文同时在首部添加发送Connection字段</li>
|
||||
<li>服务器收到请求并处理 Connection字段</li>
|
||||
<li>服务器回送Connection:Keep-Alive字段给客户端</li>
|
||||
<li>客户端接收到Connection字段</li>
|
||||
<li>Keep-Alive连接建立成功</li>
|
||||
</ul>
|
||||
<p><strong>服务端自动断开过程(也就是没有keep-alive)</strong>:</p>
|
||||
<ul>
|
||||
<li>客户端向服务器只是发送内容报文(不包含Connection字段)</li>
|
||||
<li>服务器收到请求并处理</li>
|
||||
<li>服务器返回客户端请求的资源并关闭连接</li>
|
||||
<li>客户端接收资源,发现没有Connection字段,断开连接</li>
|
||||
</ul>
|
||||
<p><strong>客户端请求断开连接过程</strong>:</p>
|
||||
<ul>
|
||||
<li>客户端向服务器发送Connection:close字段</li>
|
||||
<li>服务器收到请求并处理connection字段</li>
|
||||
<li>服务器回送响应资源并断开连接</li>
|
||||
<li>客户端接收资源并断开连接</li>
|
||||
</ul>
|
||||
<p>开启Keep-Alive的<strong>优点:</strong></p>
|
||||
<ul>
|
||||
<li>较少的CPU和内存的使⽤(由于同时打开的连接的减少了);</li>
|
||||
<li>允许请求和应答的HTTP管线化;</li>
|
||||
<li>降低拥塞控制 (TCP连接减少了);</li>
|
||||
<li>减少了后续请求的延迟(⽆需再进⾏握⼿);</li>
|
||||
<li>报告错误⽆需关闭TCP连;</li>
|
||||
</ul>
|
||||
<p>开启Keep-Alive的<strong>缺点</strong>:</p>
|
||||
<ul>
|
||||
<li>长时间的Tcp连接容易导致系统资源无效占用,浪费系统资源。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-14" id="network-point-13">13. 页面有多张图片,HTTP是怎样的加载表现?</h3>
|
||||
<ul>
|
||||
<li>在<code>HTTP 1</code>下,浏览器对一个域名下最大TCP连接数为6,所以会请求多次。可以用<strong>多域名部署</strong>解决。这样可以提高同时请求的数目,加快页面图片的获取速度。</li>
|
||||
<li>在<code>HTTP 2</code>下,可以一瞬间加载出来很多资源,因为,HTTP2支持多路复用,可以在一个TCP连接中发送多个HTTP请求。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-15" id="network-point-14">14. HTTP2的头部压缩算法是怎样的?</h3>
|
||||
<p>HTTP2的头部压缩是HPACK算法。在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。</p>
|
||||
<p>具体来说:</p>
|
||||
<ul>
|
||||
<li>在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送;</li>
|
||||
<li>首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;</li>
|
||||
<li>每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前的值。</li>
|
||||
</ul>
|
||||
<p>例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。
|
||||
<img src="/articles/network/network-figure-1.awebp" alt="" loading="lazy"></p>
|
||||
<h3 data-id="heading-16" id="network-point-15">15. HTTP请求报文的是什么样的?</h3>
|
||||
<p>请求报⽂有4部分组成:</p>
|
||||
<ul>
|
||||
<li>请求⾏</li>
|
||||
<li>请求头部</li>
|
||||
<li>空⾏</li>
|
||||
<li>请求体</li>
|
||||
</ul>
|
||||
<p><img src="/articles/network/network-figure-2.awebp" alt="image.png" loading="lazy">
|
||||
<strong>其中:</strong>
|
||||
(1)请求⾏包括:请求⽅法字段、URL字段、HTTP协议版本字段。它们⽤空格分隔。例如,GET /index.html HTTP/1.1。
|
||||
(2)请求头部:请求头部由关键字/值对组成,每⾏⼀对,关键字和值⽤英⽂冒号“:”分隔</p>
|
||||
<ul>
|
||||
<li>User-Agent:产⽣请求的浏览器类型。</li>
|
||||
<li>Accept:客户端可识别的内容类型列表。</li>
|
||||
<li>Host:请求的主机名,允许多个域名同处⼀个IP地址,即虚拟主机。</li>
|
||||
</ul>
|
||||
<p>(3)请求体: post put等请求携带的数据
|
||||
<img src="/articles/network/network-figure-3.awebp" alt="image.png" loading="lazy"></p>
|
||||
<h3 data-id="heading-17" id="network-point-16">16. HTTP响应报文的是什么样的?</h3>
|
||||
<p>请求报⽂有4部分组成:</p>
|
||||
<ul>
|
||||
<li>响应⾏</li>
|
||||
<li>响应头</li>
|
||||
<li>空⾏</li>
|
||||
<li>响应体</li>
|
||||
</ul>
|
||||
<p><img src="/articles/network/network-figure-4.awebp" alt="image.png" loading="lazy"></p>
|
||||
<ul>
|
||||
<li>响应⾏:由网络协议版本,状态码和状态码的原因短语组成,例如 HTTP/1.1 200 OK 。</li>
|
||||
<li>响应头:响应部⾸组成</li>
|
||||
<li>响应体:服务器响应的数据</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-18" id="network-point-17">17. HTTP协议的优点和缺点</h3>
|
||||
<p>HTTP 是超文本传输协议,它定义了客户端和服务器之间交换报文的格式和方式,默认使用 80 端口。它使用 TCP 作为传输层协议,保证了数据传输的可靠性。</p>
|
||||
<p>HTTP协议具有以下<strong>优点</strong>:</p>
|
||||
<ul>
|
||||
<li>支持客户端/服务器模式</li>
|
||||
<li><strong>简单快速</strong>:客户向服务器请求服务时,只需传送请求方法和路径。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。</li>
|
||||
<li><strong>无连接</strong>:无连接就是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。</li>
|
||||
<li><strong>无状态</strong>:HTTP 协议是无状态协议,这里的状态是指通信过程的上下文信息。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能会导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就比较快。</li>
|
||||
<li><strong>灵活</strong>:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。</li>
|
||||
</ul>
|
||||
<p>HTTP协议具有以下<strong>缺点</strong>:</p>
|
||||
<ul>
|
||||
<li><strong>无状态:</strong> HTTP 是一个无状态的协议,HTTP 服务器不会保存关于客户的任何信息。</li>
|
||||
<li><strong>明文传输:</strong> 协议中的报文使用的是文本形式,这就直接暴露给外界,不安全。</li>
|
||||
<li><strong>不安全</strong></li>
|
||||
</ul>
|
||||
<p>(1)通信使用明文(不加密),内容可能会被窃听;
|
||||
(2)不验证通信方的身份,因此有可能遭遇伪装;
|
||||
(3)无法证明报文的完整性,所以有可能已遭篡改;</p>
|
||||
<h3 data-id="heading-19" id="network-point-18">18. 说一下HTTP 3.0</h3>
|
||||
<p>HTTP/3基于UDP协议实现了类似于TCP的多路复用数据流、传输可靠性等功能,这套功能被称为QUIC协议。
|
||||
<img src="/articles/network/network-figure-5.awebp" alt="" loading="lazy"></p>
|
||||
<ol>
|
||||
<li>流量控制、传输可靠性功能:QUIC在UDP的基础上增加了一层来保证数据传输可靠性,它提供了数据包重传、拥塞控制、以及其他一些TCP中的特性。</li>
|
||||
<li>集成TLS加密功能:目前QUIC使用TLS1.3,减少了握手所花费的RTT数。</li>
|
||||
<li>多路复用:同一物理连接上可以有多个独立的逻辑数据流,实现了数据流的单独传输,解决了TCP的队头阻塞问题。</li>
|
||||
</ol>
|
||||
<p><img src="/articles/network/network-figure-6.awebp" alt="" loading="lazy"></p>
|
||||
<ol start="4">
|
||||
<li>快速握手:由于基于UDP,可以实现使用0 ~ 1个RTT来建立连接。</li>
|
||||
</ol>
|
||||
<h3 data-id="heading-20" id="network-point-19">19. HTTP协议的性能怎么样</h3>
|
||||
<p>HTTP 协议是基于 TCP/IP,并且使用了<strong>请求-应答</strong>的通信模式,所以性能的关键就在这两点里。</p>
|
||||
<ul>
|
||||
<li><strong>长连接</strong></li>
|
||||
</ul>
|
||||
<p>HTTP协议有两种连接模式,一种是持续连接,一种非持续连接。
|
||||
(1)非持续连接指的是服务器必须为每一个请求的对象建立和维护一个全新的连接。
|
||||
(2)持续连接下,TCP 连接默认不关闭,可以被多个请求复用。采用持续连接的好处是可以避免每次建立 TCP 连接三次握手时所花费的时间。</p>
|
||||
<p>对于不同版本的采用不同的连接方式:</p>
|
||||
<ul>
|
||||
<li>在HTTP/1.0 每发起一个请求,都要新建一次 TCP 连接(三次握手),而且是串行请求,做了无畏的 TCP 连接建立和断开,增加了通信开销。该版本使用的非持续的连接,但是可以在请求时,加上 Connection: keep-a live 来要求服务器不要关闭 TCP 连接。</li>
|
||||
<li>在HTTP/1.1 提出了<strong>长连接</strong>的通信方式,也叫持久连接。这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。该版本及以后版本默认采用的是持续的连接。目前对于同一个域,大多数浏览器支持同时建立 6 个持久连接。</li>
|
||||
</ul>
|
||||
<p><img src="/articles/network/network-figure-7.awebp" alt="" loading="lazy"></p>
|
||||
<ul>
|
||||
<li><strong>管道网络传输</strong></li>
|
||||
</ul>
|
||||
<p>HTTP/1.1 采用了长连接的方式,这使得管道(pipeline)网络传输成为了可能。</p>
|
||||
<p>管道(pipeline)网络传输是指:可以在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。但是服务器还是按照顺序回应请求。如果前面的回应特别慢,后面就会有许多请求排队等着。这称为队头堵塞。</p>
|
||||
<ul>
|
||||
<li><strong>队头堵塞</strong></li>
|
||||
</ul>
|
||||
<p>HTTP 传输的报文必须是一发一收,但是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是HTTP队头阻塞问题。</p>
|
||||
<p><strong>队头阻塞的解决方案:</strong>
|
||||
(1)并发连接:对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。
|
||||
(2)域名分片:将域名分出很多二级域名,它们都指向同样的一台服务器,能够并发的长连接数变多,解决了队头阻塞的问题。</p>
|
||||
<h3 data-id="heading-21" id="network-point-20">20. URL有哪些组成部分</h3>
|
||||
<p>以下面的URL为例:<strong><a href="https://link.juejin.cn/?target=http%3A%2F%2Fwww.aspxfans.com%3A8080%2Fnews%2Findex.asp%3FboardID%3D5%26ID%3D24618%26page%3D1%23name" target="_blank" rel="noreferrer" title="http://www.aspxfans.com:8080/news/index.asp?boardID=5&ID=24618&page=1#name" ref="nofollow noopener noreferrer">www.aspxfans.com:8080/news/index.…</a></strong></p>
|
||||
<p>从上面的URL可以看出,一个完整的URL包括以下几部分:</p>
|
||||
<ul>
|
||||
<li><strong>协议部分</strong>:该URL的协议部分为“http:”,这代表网页使用的是HTTP协议。在Internet中可以使用多种协议,如HTTP,FTP等等本例中使用的是HTTP协议。在"HTTP"后面的“//”为分隔符;</li>
|
||||
<li><strong>域名部分</strong>:该URL的域名部分为“<a href="https://link.juejin.cn/?target=http%3A%2F%2Fwww.aspxfans.com%25E2%2580%259D%25E3%2580%2582%25E4%25B8%2580%25E4%25B8%25AAURL%25E4%25B8%25AD%25EF%25BC%258C%25E4%25B9%259F%25E5%258F%25AF%25E4%25BB%25A5%25E4%25BD%25BF%25E7%2594%25A8IP%25E5%259C%25B0%25E5%259D%2580%25E4%25BD%259C%25E4%25B8%25BA%25E5%259F%259F%25E5%2590%258D%25E4%25BD%25BF%25E7%2594%25A8" target="_blank" rel="noreferrer" title="http://www.aspxfans.com%E2%80%9D%E3%80%82%E4%B8%80%E4%B8%AAURL%E4%B8%AD%EF%BC%8C%E4%B9%9F%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8IP%E5%9C%B0%E5%9D%80%E4%BD%9C%E4%B8%BA%E5%9F%9F%E5%90%8D%E4%BD%BF%E7%94%A8" ref="nofollow noopener noreferrer">www.aspxfans.com”。一个URL中,也可以使用IP地址作为域名使用</a></li>
|
||||
<li><strong>端口部分</strong>:跟在域名后面的是端口,域名和端口之间使用“:”作为分隔符。端口不是一个URL必须的部分,如果省略端口部分,将采用默认端口(HTTP协议默认端口是80,HTTPS协议默认端口是443);</li>
|
||||
<li><strong>虚拟目录部分</strong>:从域名后的第一个“/”开始到最后一个“/”为止,是虚拟目录部分。虚拟目录也不是一个URL必须的部分。本例中的虚拟目录是“/news/”;</li>
|
||||
<li><strong>文件名部分</strong>:从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。本例中的文件名是“index.asp”。文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件名;</li>
|
||||
<li><strong>锚部分</strong>:从“#”开始到最后,都是锚部分。本例中的锚部分是“name”。锚部分也不是一个URL必须的部分;</li>
|
||||
<li><strong>参数部分</strong>:从“?”开始到“#”为止之间的部分为参数部分,又称搜索部分、查询部分。本例中的参数部分为“boardID=5&ID=24618&page=1”。参数可以允许有多个参数,参数与参数之间用“&”作为分隔符。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-22" id="network-point-21">21. 与缓存相关的HTTP请求头有哪些</h3>
|
||||
<p>强缓存:</p>
|
||||
<ul>
|
||||
<li>Expires</li>
|
||||
<li>Cache-Control</li>
|
||||
</ul>
|
||||
<p>协商缓存:</p>
|
||||
<ul>
|
||||
<li>Etag、If-None-Match</li>
|
||||
<li>Last-Modified、If-Modified-Since</li>
|
||||
</ul>
|
||||
<h2 data-id="heading-23">二、HTTPS协议</h2>
|
||||
<h3 data-id="heading-24" id="network-point-22">1. 什么是HTTPS协议?</h3>
|
||||
<p>超文本传输安全协议(Hypertext Transfer Protocol Secure,简称:HTTPS)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用SSL/TLS来加密数据包。HTTPS的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
|
||||
<img src="/articles/network/network-figure-8.awebp" alt="" loading="lazy">
|
||||
HTTP协议采用<strong>明文传输</strong>信息,存在<strong>信息窃听</strong>、<strong>信息篡改</strong>和<strong>信息劫持</strong>的风险,而协议TLS/SSL具有<strong>身份验证</strong>、<strong>信息加密</strong>和<strong>完整性校验</strong>的功能,可以避免此类问题发生。</p>
|
||||
<p>安全层的主要职责就是<strong>对发起的HTTP请求的数据进行加密操作</strong> 和 <strong>对接收到的HTTP的内容进行解密操作</strong>。</p>
|
||||
<h3 data-id="heading-25" id="network-point-23">2. TLS/SSL的工作原理</h3>
|
||||
<p><strong>TLS/SSL</strong>全称<strong>安全传输层协议</strong>(Transport Layer Security), 是介于TCP和HTTP之间的一层安全协议,不影响原有的TCP协议和HTTP协议,所以使用HTTPS基本上不需要对HTTP页面进行太多的改造。</p>
|
||||
<p>TLS/SSL的功能实现主要依赖三类基本算法:<strong>散列函数hash</strong>、<strong>对称加密</strong>、<strong>非对称加密</strong>。这三类算法的作用如下:</p>
|
||||
<ul>
|
||||
<li>基于散列函数验证信息的完整性</li>
|
||||
<li>对称加密算法采用协商的秘钥对数据加密</li>
|
||||
<li>非对称加密实现身份认证和秘钥协商</li>
|
||||
</ul>
|
||||
<p><img src="/articles/network/network-figure-9.awebp" alt="" loading="lazy"></p>
|
||||
<h4 data-id="heading-26">(1)散列函数hash</h4>
|
||||
<p>常见的散列函数有MD5、SHA1、SHA256。该函数的特点是单向不可逆,对输入数据非常敏感,输出的长度固定,任何数据的修改都会改变散列函数的结果,可以用于防止信息篡改并验证数据的完整性。</p>
|
||||
<p><strong>特点:</strong> 在信息传输过程中,散列函数不能三都实现信息防篡改,由于传输是明文传输,中间人可以修改信息后重新计算信息的摘要,所以需要对传输的信息和信息摘要进行加密。</p>
|
||||
<h4 data-id="heading-27">(2)对称加密</h4>
|
||||
<p>对称加密的方法是,双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。 这就要用到非对称加密的方法。</p>
|
||||
<p>常见的对称加密算法有AES-CBC、DES、3DES、AES-GCM等。相同的秘钥可以用于信息的加密和解密。掌握秘钥才能获取信息,防止信息窃听,其通讯方式是一对一。</p>
|
||||
<p><strong>特点:</strong> 对称加密的优势就是信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。</p>
|
||||
<h4 data-id="heading-28">(3)非对称加密</h4>
|
||||
<p>非对称加密的方法是,我们拥有两个秘钥,一个是公钥,一个是私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户, 都可以使用我们提供的公钥对数据进行加密,这样我们就可以使用私钥进行解密,这样就能保证数据的安全了。但是非对称加密有一个缺点就是加密的过程很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。</p>
|
||||
<p>常见的非对称加密算法有RSA、ECC、DH等。秘钥成对出现,一般称为公钥(公开)和私钥(保密)。公钥加密的信息只有私钥可以解开,私钥加密的信息只能公钥解开,因此掌握公钥的不同客户端之间不能相互解密信息,只能和服务器进行加密通信,服务器可以实现一对多的的通信,客户端也可以用来验证掌握私钥的服务器的身份。</p>
|
||||
<p><strong>特点:</strong> 非对称加密的特点就是信息一对多,服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。</p>
|
||||
<p>综合上述算法特点,TLS/SSL的工作方式就是客户端使用非对称加密与服务器进行通信,实现身份的验证并协商对称加密使用的秘钥。对称加密算法采用协商秘钥对信息以及信息摘要进行加密通信,不同节点之间采用的对称秘钥不同,从而保证信息只能通信双方获取。这样就解决了两个方法各自存在的问题。</p>
|
||||
<h3 data-id="heading-29" id="network-point-24">3. 数字证书是什么?</h3>
|
||||
<p>现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。</p>
|
||||
<p>首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。</p>
|
||||
<p>这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样才能保证数据的安全。
|
||||
<img src="/articles/network/network-figure-10.awebp" alt="" loading="lazy"></p>
|
||||
<h3 data-id="heading-30" id="network-point-25">4. HTTPS通信(握手)过程</h3>
|
||||
<p>HTTPS的通信过程如下:</p>
|
||||
<ol>
|
||||
<li>客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。</li>
|
||||
<li>服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。</li>
|
||||
<li>客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服 务器。并且还会提供一个前面所有内容的 hash 的值,用来供服务器检验。</li>
|
||||
<li>服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 hash 值来供客户端检验。</li>
|
||||
<li>客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。</li>
|
||||
</ol>
|
||||
<h3 data-id="heading-31" id="network-point-26">5. HTTPS的特点</h3>
|
||||
<p>HTTPS的<strong>优点</strong>如下:</p>
|
||||
<ul>
|
||||
<li>使用HTTPS协议可以认证用户和服务器,确保数据发送到正确的客户端和服务器;</li>
|
||||
<li>使用HTTPS协议可以进行加密传输、身份认证,通信更加安全,防止数据在传输过程中被窃取、修改,确保数据安全性;</li>
|
||||
<li>HTTPS是现行架构下最安全的解决方案,虽然不是绝对的安全,但是大幅增加了中间人攻击的成本;</li>
|
||||
</ul>
|
||||
<p>HTTPS的<strong>缺点</strong>如下:</p>
|
||||
<ul>
|
||||
<li>HTTPS需要做服务器和客户端双方的加密个解密处理,耗费更多服务器资源,过程复杂;</li>
|
||||
<li>HTTPS协议握手阶段比较费时,增加页面的加载时间;</li>
|
||||
<li>SSL证书是收费的,功能越强大的证书费用越高;</li>
|
||||
<li>HTTPS连接服务器端资源占用高很多,支持访客稍多的网站需要投入更大的成本;</li>
|
||||
<li>SSL证书需要绑定IP,不能再同一个IP上绑定多个域名。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-32" id="network-point-27">6. <strong>HTTPS</strong>是如何保证安全的?</h3>
|
||||
<p>先理解两个概念:</p>
|
||||
<ul>
|
||||
<li>对称加密:即通信的双⽅都使⽤同⼀个秘钥进⾏加解密,对称加密虽然很简单性能也好,但是⽆法解决⾸次把秘钥发给对⽅的问题,很容易被⿊客拦截秘钥。</li>
|
||||
<li>⾮对称加密:</li>
|
||||
</ul>
|
||||
<ol>
|
||||
<li>私钥 + 公钥= 密钥对</li>
|
||||
<li>即⽤私钥加密的数据,只有对应的公钥才能解密,⽤公钥加密的数据,只有对应的私钥才能解密</li>
|
||||
<li>因为通信双⽅的⼿⾥都有⼀套⾃⼰的密钥对,通信之前双⽅会先把⾃⼰的公钥都先发给对⽅</li>
|
||||
<li>然后对⽅再拿着这个公钥来加密数据响应给对⽅,等到到了对⽅那⾥,对⽅再⽤⾃⼰的私钥进⾏解密</li>
|
||||
</ol>
|
||||
<p>⾮对称加密虽然安全性更⾼,但是带来的问题就是速度很慢,影响性能。</p>
|
||||
<p><strong>解决⽅案:</strong></p>
|
||||
<p>结合两种加密⽅式,将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,然后发送出去,接收⽅使⽤私钥进⾏解密得到对称加密的密钥,然后双⽅可以使⽤对称加密来进⾏沟通。</p>
|
||||
<p>此时⼜带来⼀个问题,中间⼈问题:
|
||||
如果此时在客户端和服务器之间存在⼀个中间⼈,这个中间⼈只需要把原本双⽅通信互发的公钥,换成⾃⼰的公钥,这样中间⼈就可以轻松解密通信双⽅所发送的所有数据。</p>
|
||||
<p>所以这个时候需要⼀个安全的第三⽅颁发证书(CA),证明身份的身份,防⽌被中间⼈攻击。 证书中包括:签发者、证书⽤途、使⽤者公钥、使⽤者私钥、使⽤者的HASH算法、证书到期时间等。</p>
|
||||
<p>但是问题来了,如果中间⼈篡改了证书,那么身份证明是不是就⽆效了?这个证明就⽩买了,这个时候需要⼀个新的技术,数字签名。</p>
|
||||
<p>数字签名就是⽤CA⾃带的HASH算法对证书的内容进⾏HASH得到⼀个摘要,再⽤CA的私钥加密,最终组成数字签名。当别⼈把他的证书发过来的时候,我再⽤同样的Hash算法,再次⽣成消息摘要,然后⽤CA的公钥对数字签名解密,得到CA创建的消息摘要,两者⼀⽐,就知道中间有没有被⼈篡改了。这个时候就能最⼤程度保证通信的安全了。</p>
|
||||
<h2 data-id="heading-33">三、HTTP状态码</h2>
|
||||
<p>状态码的类别:</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table><thead><tr><th><strong>类别</strong></th><th><strong>原因</strong></th><th><strong>描述</strong></th></tr></thead><tbody><tr><td>1xx</td><td>Informational(信息性状态码)</td><td>接受的请求正在处理</td></tr><tr><td>2xx</td><td>Success(成功状态码)</td><td>请求正常处理完毕</td></tr><tr><td>3xx</td><td>Redirection(重定向状态码)</td><td>需要进行附加操作一完成请求</td></tr><tr><td>4xx</td><td>Client Error (客户端错误状态码)</td><td>服务器无法处理请求</td></tr><tr><td>5xx</td><td>Server Error(服务器错误状态码)</td><td>服务器处理请求出错</td></tr></tbody></table>
|
||||
<h3 data-id="heading-34" id="network-point-28">1. 2XX (Success 成功状态码)</h3>
|
||||
<p>状态码2XX表示请求被正常处理了。</p>
|
||||
<h4 data-id="heading-35">(1)200 OK</h4>
|
||||
<p>200 OK表示客户端发来的请求被服务器端正常处理了。</p>
|
||||
<h4 data-id="heading-36">(2)204 No Content</h4>
|
||||
<p>该状态码表示客户端发送的请求已经在服务器端正常处理了,但是没有返回的内容,响应报文中不包含实体的主体部分。一般在只需要从客户端往服务器端发送信息,而服务器端不需要往客户端发送内容时使用。</p>
|
||||
<h4 data-id="heading-37">(3)206 Partial Content</h4>
|
||||
<p>该状态码表示客户端进行了范围请求,而服务器端执行了这部分的 GET 请求。响应报文中包含由 Content-Range 指定范围的实体内容。</p>
|
||||
<h3 data-id="heading-38" id="network-point-29">2. 3XX (Redirection 重定向状态码)</h3>
|
||||
<p>3XX 响应结果表明浏览器需要执行某些特殊的处理以正确处理请求。</p>
|
||||
<h4 data-id="heading-39">(1)301 Moved Permanently</h4>
|
||||
<p><strong>永久重定向。</strong>
|
||||
该状态码表示请求的资源已经被分配了新的 URI,以后应使用资源指定的 URI。新的 URI 会在 HTTP 响应头中的 Location 首部字段指定。若用户已经把原来的URI保存为书签,此时会按照 Location 中新的URI重新保存该书签。同时,搜索引擎在抓取新内容的同时也将旧的网址替换为重定向之后的网址。</p>
|
||||
<p><strong>使用场景:</strong></p>
|
||||
<ul>
|
||||
<li>当我们想换个域名,旧的域名不再使用时,用户访问旧域名时用301就重定向到新的域名。其实也是告诉搜索引擎收录的域名需要对新的域名进行收录。</li>
|
||||
<li>在搜索引擎的搜索结果中出现了不带www的域名,而带www的域名却没有收录,这个时候可以用301重定向来告诉搜索引擎我们目标的域名是哪一个。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-40">(2)302 Found</h4>
|
||||
<p><strong>临时重定向。</strong>
|
||||
该状态码表示请求的资源被分配到了新的 URI,希望用户(本次)能使用新的 URI 访问资源。和 301 Moved Permanently 状态码相似,但是 302 代表的资源不是被永久重定向,只是临时性质的。也就是说已移动的资源对应的 URI 将来还有可能发生改变。若用户把 URI 保存成书签,但不会像 301 状态码出现时那样去更新书签,而是仍旧保留返回 302 状态码的页面对应的 URI。同时,搜索引擎会抓取新的内容而保留旧的网址。因为服务器返回302代码,搜索引擎认为新的网址只是暂时的。</p>
|
||||
<p><strong>使用场景:</strong></p>
|
||||
<ul>
|
||||
<li>当我们在做活动时,登录到首页自动重定向,进入活动页面。</li>
|
||||
<li>未登陆的用户访问用户中心重定向到登录页面。</li>
|
||||
<li>访问404页面重新定向到首页。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-41">(3)303 See Other</h4>
|
||||
<p>该状态码表示由于请求对应的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。
|
||||
303 状态码和 302 Found 状态码有着相似的功能,但是 303 状态码明确表示客户端应当采用 GET 方法获取资源。</p>
|
||||
<p>303 状态码通常作为 PUT 或 POST 操作的返回结果,它表示重定向链接指向的不是新上传的资源,而是另外一个页面,比如消息确认页面或上传进度页面。而请求重定向页面的方法要总是使用 GET。</p>
|
||||
<p>注意:</p>
|
||||
<ul>
|
||||
<li>当 301、302、303 响应状态码返回时,几乎所有的浏览器都会把 POST 改成GET,并删除请求报文内的主体,之后请求会再次自动发送。</li>
|
||||
<li>301、302 标准是禁止将 POST 方法变成 GET方法的,但实际大家都会这么做。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-42">(4)304 Not Modified</h4>
|
||||
<p><strong>浏览器缓存相关。</strong>
|
||||
该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但未满足条件的情况。304 状态码返回时,不包含任何响应的主体部分。304 虽然被划分在 3XX 类别中,但是和重定向没有关系。</p>
|
||||
<p>带条件的请求(Http 条件请求):使用 Get方法 请求,请求报文中包含(<code>if-match</code>、<code>if-none-match</code>、<code>if-modified-since</code>、<code>if-unmodified-since</code>、<code>if-range</code>)中任意首部。</p>
|
||||
<p>状态码304并不是一种错误,而是告诉客户端有缓存,直接使用缓存中的数据。返回页面的只有头部信息,是没有内容部分的,这样在一定程度上提高了网页的性能。</p>
|
||||
<h4 data-id="heading-43">(5)307 Temporary Redirect</h4>
|
||||
<p><strong>307表示临时重定向。</strong> 该状态码与 302 Found 有着相同含义,尽管 302 标准禁止 POST 变成 GET,但是实际使用时还是这样做了。</p>
|
||||
<p>307 会遵守浏览器标准,<strong>不会从 POST 变成 GET</strong>。但是对于处理请求的行为时,不同浏览器还是会出现不同的情况。规范要求浏览器继续向 Location 的地址 POST 内容。规范要求浏览器继续向 Location 的地址 POST 内容。</p>
|
||||
<h3 data-id="heading-44" id="network-point-30">3. 4XX (Client Error 客户端错误状态码)</h3>
|
||||
<p>4XX 的响应结果表明客户端是发生错误的原因所在。</p>
|
||||
<h4 data-id="heading-45">(1)400 Bad Request</h4>
|
||||
<p>该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码。</p>
|
||||
<h4 data-id="heading-46">(2)401 Unauthorized</h4>
|
||||
<p>该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。若之前已进行过一次请求,则表示用户认证失败</p>
|
||||
<p>返回含有 401 的响应必须包含一个适用于被请求资源的 WWW-Authenticate 首部用以质询(challenge)用户信息。当浏览器初次接收到 401 响应,会弹出认证用的对话窗口。</p>
|
||||
<p>以下情况会出现401:</p>
|
||||
<ul>
|
||||
<li>401.1 - 登录失败。</li>
|
||||
<li>401.2 - 服务器配置导致登录失败。</li>
|
||||
<li>401.3 - 由于 ACL 对资源的限制而未获得授权。</li>
|
||||
<li>401.4 - 筛选器授权失败。</li>
|
||||
<li>401.5 - ISAPI/CGI 应用程序授权失败。</li>
|
||||
<li>401.7 - 访问被 Web 服务器上的 URL 授权策略拒绝。这个错误代码为 IIS 6.0 所专用。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-47">(3)403 Forbidden</h4>
|
||||
<p>该状态码表明请求资源的访问被服务器拒绝了,服务器端没有必要给出详细理由,但是可以在响应报文实体的主体中进行说明。进入该状态后,不能再继续进行验证。该访问是永久禁止的,并且与应用逻辑密切相关。</p>
|
||||
<p>IIS 定义了许多不同的 403 错误,它们指明更为具体的错误原因:</p>
|
||||
<ul>
|
||||
<li>403.1 - 执行访问被禁止。</li>
|
||||
<li>403.2 - 读访问被禁止。</li>
|
||||
<li>403.3 - 写访问被禁止。</li>
|
||||
<li>403.4 - 要求 SSL。</li>
|
||||
<li>403.5 - 要求 SSL 128。</li>
|
||||
<li>403.6 - IP 地址被拒绝。</li>
|
||||
<li>403.7 - 要求客户端证书。</li>
|
||||
<li>403.8 - 站点访问被拒绝。</li>
|
||||
<li>403.9 - 用户数过多。</li>
|
||||
<li>403.10 - 配置无效。</li>
|
||||
<li>403.11 - 密码更改。</li>
|
||||
<li>403.12 - 拒绝访问映射表。</li>
|
||||
<li>403.13 - 客户端证书被吊销。</li>
|
||||
<li>403.14 - 拒绝目录列表。</li>
|
||||
<li>403.15 - 超出客户端访问许可。</li>
|
||||
<li>403.16 - 客户端证书不受信任或无效。</li>
|
||||
<li>403.17 - 客户端证书已过期或尚未生效</li>
|
||||
<li>403.18 - 在当前的应用程序池中不能执行所请求的 URL。这个错误代码为 IIS 6.0 所专用。</li>
|
||||
<li>403.19 - 不能为这个应用程序池中的客户端执行 CGI。这个错误代码为 IIS 6.0 所专用。</li>
|
||||
<li>403.20 - Passport 登录失败。这个错误代码为 IIS 6.0 所专用。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-48">(4)404 Not Found</h4>
|
||||
<p>该状态码表明服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。
|
||||
以下情况会出现404:</p>
|
||||
<ul>
|
||||
<li>404.0 -(无) – 没有找到文件或目录。</li>
|
||||
<li>404.1 - 无法在所请求的端口上访问 Web 站点。</li>
|
||||
<li>404.2 - Web 服务扩展锁定策略阻止本请求。</li>
|
||||
<li>404.3 - MIME 映射策略阻止本请求。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-49">(5)405 Method Not Allowed</h4>
|
||||
<p>该状态码表示客户端请求的方法虽然能被服务器识别,但是服务器禁止使用该方法。GET 和 HEAD 方法,服务器应该总是允许客户端进行访问。客户端可以通过 OPTIONS 方法(预检)来查看服务器允许的访问方法, 如下</p>
|
||||
<pre><code class="language-javascript" lang="javascript">Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE</code></pre>
|
||||
<h3 data-id="heading-50" id="network-point-31">4. 5XX (Server Error 服务器错误状态码)</h3>
|
||||
<p>5XX 的响应结果表明服务器本身发生错误.</p>
|
||||
<h4 data-id="heading-51">(1)500 Internal Server Error</h4>
|
||||
<p>该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web 应用存在的 bug 或某些临时的故障。</p>
|
||||
<h4 data-id="heading-52">(2)502 Bad Gateway</h4>
|
||||
<p>该状态码表明扮演网关或代理角色的服务器,从上游服务器中接收到的响应是无效的。注意,502 错误通常不是客户端能够修复的,而是需要由途经的 Web 服务器或者代理服务器对其进行修复。以下情况会出现502:</p>
|
||||
<ul>
|
||||
<li>502.1 - CGI (通用网关接口)应用程序超时。</li>
|
||||
<li>502.2 - CGI (通用网关接口)应用程序出错。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-53">(3)503 Service Unavailable</h4>
|
||||
<p>该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入 RetryAfter 首部字段再返回给客户端。</p>
|
||||
<p><strong>使用场景:</strong></p>
|
||||
<ul>
|
||||
<li>服务器停机维护时,主动用503响应请求;</li>
|
||||
<li>nginx 设置限速,超过限速,会返回503。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-54">(4)504 Gateway Timeout</h4>
|
||||
<p>该状态码表示网关或者代理的服务器无法在规定的时间内获得想要的响应。他是HTTP 1.1中新加入的。</p>
|
||||
<p>使用场景:代码执行时间超时,或者发生了死循环。</p>
|
||||
<h3 data-id="heading-55" id="network-point-32">5. 总结</h3>
|
||||
<p><strong>(1)2XX 成功</strong></p>
|
||||
<ul>
|
||||
<li>200 OK,表示从客户端发来的请求在服务器端被正确处理</li>
|
||||
<li>204 No content,表示请求成功,但响应报文不含实体的主体部分</li>
|
||||
<li>205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容</li>
|
||||
<li>206 Partial Content,进行范围请求</li>
|
||||
</ul>
|
||||
<p><strong>(2)3XX 重定向</strong></p>
|
||||
<ul>
|
||||
<li>301 moved permanently,永久性重定向,表示资源已被分配了新的 URL</li>
|
||||
<li>302 found,临时性重定向,表示资源临时被分配了新的 URL</li>
|
||||
<li>303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源</li>
|
||||
<li>304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况</li>
|
||||
<li>307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求</li>
|
||||
</ul>
|
||||
<p><strong>(3)4XX 客户端错误</strong></p>
|
||||
<ul>
|
||||
<li>400 bad request,请求报文存在语法错误</li>
|
||||
<li>401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息</li>
|
||||
<li>403 forbidden,表示对请求资源的访问被服务器拒绝</li>
|
||||
<li>404 not found,表示在服务器上没有找到请求的资源</li>
|
||||
</ul>
|
||||
<p><strong>(4)5XX 服务器错误</strong></p>
|
||||
<ul>
|
||||
<li>500 internal sever error,表示服务器端在执行请求时发生了错误</li>
|
||||
<li>501 Not Implemented,表示服务器不支持当前请求所需要的某个功能</li>
|
||||
<li>503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-56" id="network-point-33">6. 同样是重定向,<strong>307</strong>,<strong>303</strong>,<strong>302</strong>的区别?</h3>
|
||||
<p>302是http1.0的协议状态码,在http1.1版本的时候为了细化302状态码⼜出来了两个303和307。 303明确表示客户端应当采⽤get⽅法获取资源,他会把POST请求变为GET请求进⾏重定向。 307会遵照浏览器标准,不会从post变为get。</p>
|
||||
<h2 data-id="heading-57">四、DNS协议介绍</h2>
|
||||
<h3 data-id="heading-58" id="network-point-34">1. DNS 协议是什么</h3>
|
||||
<p><strong>概念</strong>: DNS 是域名系统 (Domain Name System) 的缩写,提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。它是一个由分层的 DNS 服务器组成的分布式数据库,是定义了主机如何查询这个分布式数据库的方式的应用层协议。能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。</p>
|
||||
<p><strong>作用</strong>: 将域名解析为IP地址,客户端向DNS服务器(DNS服务器有自己的IP地址)发送域名查询请求,DNS服务器告知客户机Web服务器的 IP 地址。</p>
|
||||
<h3 data-id="heading-59" id="network-point-35">2. DNS同时使用TCP和UDP协议?</h3>
|
||||
<p><strong>DNS占用53号端口,同时使用TCP和UDP协议。</strong>
|
||||
(1)在区域传输的时候使用TCP协议</p>
|
||||
<ul>
|
||||
<li>辅域名服务器会定时(一般3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用TCP而不是UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。</li>
|
||||
<li>TCP是一种可靠连接,保证了数据的准确性。</li>
|
||||
</ul>
|
||||
<p>(2)在域名解析的时候使用UDP协议</p>
|
||||
<ul>
|
||||
<li>客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过三次握手,这样DNS服务器负载更低,响应更快。理论上说,客户端也可以指定向DNS服务器查询时用TCP,但事实上,很多DNS服务器进行配置的时候,仅支持UDP查询包。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-60" id="network-point-36">3. DNS完整的查询过程</h3>
|
||||
<p>DNS服务器解析域名的过程:</p>
|
||||
<ul>
|
||||
<li>首先会在<strong>浏览器的缓存</strong>中查找对应的IP地址,如果查找到直接返回,若找不到继续下一步</li>
|
||||
<li>将请求发送给<strong>本地DNS服务器</strong>,在本地域名服务器缓存中查询,如果查找到,就直接将查找结果返回,若找不到继续下一步</li>
|
||||
<li>本地DNS服务器向<strong>根域名服务器</strong>发送请求,根域名服务器会返回一个所查询域的顶级域名服务器地址</li>
|
||||
<li>本地DNS服务器向<strong>顶级域名服务器</strong>发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的地址</li>
|
||||
<li>本地DNS服务器向<strong>权威域名服务器</strong>发送请求,域名服务器返回对应的结果</li>
|
||||
<li>本地DNS服务器将返回结果保存在缓存中,便于下次使用</li>
|
||||
<li>本地DNS服务器将返回结果返回给浏览器</li>
|
||||
</ul>
|
||||
<p>比如要查询 <a href="https://link.juejin.cn/?target=http%3A%2F%2Fwww.baidu.com%2F" target="_blank" rel="noreferrer" title="http://www.baidu.com/" ref="nofollow noopener noreferrer">www.baidu.com</a> 的 IP 地址,首先会在浏览器的缓存中查找是否有该域名的缓存,如果不存在就将请求发送到本地的 DNS 服务器中,本地DNS服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com 的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。</p>
|
||||
<h3 data-id="heading-61" id="network-point-37">4. 迭代查询与递归查询</h3>
|
||||
<p>实际上,DNS解析是一个包含迭代查询和递归查询的过程。</p>
|
||||
<ul>
|
||||
<li><strong>递归查询</strong>指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归 查询,用户只需要发出一次查询请求。</li>
|
||||
<li><strong>迭代查询</strong>指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出 多次的查询请求。</li>
|
||||
</ul>
|
||||
<p>一般我们向本地 DNS 服务器发送请求的方式就是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我 们最终的请求结果。而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次 查询的结果,下一级的查询由本地 DNS 服务器自己进行。</p>
|
||||
<h3 data-id="heading-62" id="network-point-38">5. DNS 记录和报文</h3>
|
||||
<p>DNS 服务器中以资源记录的形式存储信息,每一个 DNS 响应报文一般包含多条资源记录。一条资源记录的具体的格式为</p>
|
||||
<pre><code class="language-http" lang="http">(Name,Value,Type,TTL)</code></pre>
|
||||
<p>其中 TTL 是资源记录的生存时间,它定义了资源记录能够被其他的 DNS 服务器缓存多长时间。</p>
|
||||
<p>常用的一共有四种 Type 的值,分别是 A、NS、CNAME 和 MX ,不同 Type 的值,对应资源记录代表的意义不同:</p>
|
||||
<ul>
|
||||
<li>如果 Type = A,则 Name 是主机名,Value 是主机名对应的 IP 地址。因此一条记录为 A 的资源记录,提供了标 准的主机名到 IP 地址的映射。</li>
|
||||
<li>如果 Type = NS,则 Name 是个域名,Value 是负责该域名的 DNS 服务器的主机名。这个记录主要用于 DNS 链式 查询时,返回下一级需要查询的 DNS 服务器的信息。</li>
|
||||
<li>如果 Type = CNAME,则 Name 为别名,Value 为该主机的规范主机名。该条记录用于向查询的主机返回一个主机名 对应的规范主机名,从而告诉查询主机去查询这个主机名的 IP 地址。主机别名主要是为了通过给一些复杂的主机名提供 一个便于记忆的简单的别名。</li>
|
||||
<li>如果 Type = MX,则 Name 为一个邮件服务器的别名,Value 为邮件服务器的规范主机名。它的作用和 CNAME 是一 样的,都是为了解决规范主机名不利于记忆的缺点。</li>
|
||||
</ul>
|
||||
<h2 data-id="heading-63">五、网络模型</h2>
|
||||
<h3 data-id="heading-64" id="network-point-39">1. OSI七层模型</h3>
|
||||
<p><code>ISO</code>为了更好的使网络应用更为普及,推出了<code>OSI</code>参考模型。
|
||||
<img src="/articles/network/network-figure-11.awebp" alt="" loading="lazy"></p>
|
||||
<h4 data-id="heading-65">(1)应用层</h4>
|
||||
<p><code>OSI</code>参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:<code>HTTP</code>,<code>HTTPS</code>,<code>FTP</code>,<code>POP3</code>、<code>SMTP</code>等。</p>
|
||||
<ul>
|
||||
<li>在客户端与服务器中经常会有数据的请求,这个时候就是会用到<code>http(hyper text transfer protocol)(超文本传输协议)</code>或者<code>https</code>.在后端设计数据接口时,我们常常使用到这个协议。</li>
|
||||
<li><code>FTP</code>是文件传输协议,在开发过程中,个人并没有涉及到,但是我想,在一些资源网站,比如<code>百度网盘``迅雷</code>应该是基于此协议的。</li>
|
||||
<li><code>SMTP</code>是<code>simple mail transfer protocol(简单邮件传输协议)</code>。在一个项目中,在用户邮箱验证码登录的功能时,使用到了这个协议。</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-66">(2)表示层</h4>
|
||||
<p>表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。</p>
|
||||
<p>在项目开发中,为了方便数据传输,可以使用<code>base64</code>对数据进行编解码。如果按功能来划分,<code>base64</code>应该是工作在表示层。</p>
|
||||
<h4 data-id="heading-67">(3)会话层</h4>
|
||||
<p>会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。</p>
|
||||
<h4 data-id="heading-68">(4)传输层</h4>
|
||||
<p>传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,<code>TCP</code> <code>UDP</code>就是在这一层。端口号既是这里的“端”。</p>
|
||||
<h4 data-id="heading-69">(5)网络层</h4>
|
||||
<p>本层通过<code>IP</code>寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的<code>IP</code>层。这一层就是我们经常说的<code>IP</code>协议层。<code>IP</code>协议是<code>Internet</code>的基础。我们可以这样理解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。</p>
|
||||
<h4 data-id="heading-70">(6)数据链路层</h4>
|
||||
<p>将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。
|
||||
网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理解,网络层是规划了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还增加了差错控制的功能。</p>
|
||||
<h4 data-id="heading-71">(7)物理层</h4>
|
||||
<p>实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。</p>
|
||||
<p><strong>OSI七层模型通信特点:对等通信</strong>
|
||||
对等通信,为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。</p>
|
||||
<h3 data-id="heading-72" id="network-point-40">2. TCP/IP五层协议</h3>
|
||||
<p><code>TCP/IP</code>五层协议和<code>OSI</code>的七层协议对应关系如下:
|
||||
<img src="/articles/network/network-figure-12.awebp" alt="" loading="lazy"></p>
|
||||
<ul>
|
||||
<li><strong>应用层 (application layer)</strong>:直接为应用进程提供服务。应用层协议定义的是应用进程间通讯和交互的规则,不同的应用有着不同的应用层协议,如 HTTP协议(万维网服务)、FTP协议(文件传输)、SMTP协议(电子邮件)、DNS(域名查询)等。</li>
|
||||
<li><strong>传输层 (transport layer)</strong>:有时也译为运输层,它负责为两台主机中的进程提供通信服务。该层主要有以下两种协议:
|
||||
<ul>
|
||||
<li>传输控制协议 (Transmission Control Protocol,TCP):提供面向连接的、可靠的数据传输服务,数据传输的基本单位是报文段(segment);</li>
|
||||
<li>用户数据报协议 (User Datagram Protocol,UDP):提供无连接的、尽最大努力的数据传输服务,但不保证数据传输的可靠性,数据传输的基本单位是用户数据报。</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>网络层 (internet layer)</strong>:有时也译为网际层,它负责为两台主机提供通信服务,并通过选择合适的路由将数据传递到目标主机。</li>
|
||||
<li><strong>数据链路层 (data link layer)</strong>:负责将网络层交下来的 IP 数据报封装成帧,并在链路的两个相邻节点间传送帧,每一帧都包含数据和必要的控制信息(如同步信息、地址信息、差错控制等)。</li>
|
||||
<li><strong>物理层 (physical Layer)</strong>:确保数据可以在各种物理媒介上进行传输,为数据的传输提供可靠的环境。</li>
|
||||
</ul>
|
||||
<p>从上图中可以看出,<code>TCP/IP</code>模型比<code>OSI</code>模型更加简洁,它把<code>应用层/表示层/会话层</code>全部整合为了<code>应用层</code>。</p>
|
||||
<p>在每一层都工作着不同的设备,比如我们常用的交换机就工作在数据链路层的,一般的路由器是工作在网络层的。
|
||||
<img src="/articles/network/network-figure-13.awebp" alt="" loading="lazy">
|
||||
在每一层实现的协议也各不同,即每一层的服务也不同,下图列出了每层主要的传输协议:
|
||||
<img src="/articles/network/network-figure-14.awebp" alt="" loading="lazy"></p>
|
||||
<p>同样,<code>TCP/IP</code>五层协议的通信方式也是对等通信:
|
||||
<img src="/articles/network/network-figure-15.awebp" alt="image.png" loading="lazy"></p>
|
||||
<h2 data-id="heading-73">六、TCP与UDP</h2>
|
||||
<h3 data-id="heading-74" id="network-point-41">1. TCP 和 UDP的概念及特点</h3>
|
||||
<p>TCP 和 UDP都是传输层协议,他们都属于TCP/IP协议族:</p>
|
||||
<p><strong>(1)UDP</strong></p>
|
||||
<p>UDP的全称是<strong>用户数据报协议</strong>,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。</p>
|
||||
<p>它的特点如下:</p>
|
||||
<p><strong>1)面向无连接</strong></p>
|
||||
<p>首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。</p>
|
||||
<p>具体来说就是:</p>
|
||||
<ul>
|
||||
<li>在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了</li>
|
||||
<li>在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作</li>
|
||||
</ul>
|
||||
<p><strong>2)有单播,多播,广播的功能</strong></p>
|
||||
<p>UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。</p>
|
||||
<p><strong>3)面向报文</strong></p>
|
||||
<p>发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文</p>
|
||||
<p><strong>4)不可靠性</strong></p>
|
||||
<p>首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。</p>
|
||||
<p>并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。</p>
|
||||
<p>再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。</p>
|
||||
<p><strong>5)头部开销小,传输数据报文时是很高效的。</strong>
|
||||
<img src="/articles/network/network-figure-16.awebp" alt="" loading="lazy"></p>
|
||||
<p>UDP 头部包含了以下几个数据:</p>
|
||||
<ul>
|
||||
<li>两个十六位的端口号,分别为源端口(可选字段)和目标端口</li>
|
||||
<li>整个数据报文的长度</li>
|
||||
<li>整个数据报文的检验和(IPv4 可选字段),该字段用于发现头部信息和数据中的错误</li>
|
||||
</ul>
|
||||
<p>因此 UDP 的头部开销小,只有8字节,相比 TCP 的至少20字节要少得多,在传输数据报文时是很高效的。</p>
|
||||
<p><strong>(2)TCP</strong>
|
||||
TCP的全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是面向连接的、可靠的流协议(流就是指不间断的数据结构)。</p>
|
||||
<p>它有以下几个特点:</p>
|
||||
<p><strong>1)面向连接</strong></p>
|
||||
<p>面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。</p>
|
||||
<p><strong>2)仅支持单播传输</strong></p>
|
||||
<p>每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。</p>
|
||||
<p><strong>3)面向字节流</strong></p>
|
||||
<p>TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。</p>
|
||||
<p><strong>4)可靠传输</strong></p>
|
||||
<p>对于可靠传输,判断丢包、误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。</p>
|
||||
<p><strong>5)提供拥塞控制</strong></p>
|
||||
<p>当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞。</p>
|
||||
<p><strong>6)提供全双工通信</strong></p>
|
||||
<p>TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)</p>
|
||||
<h3 data-id="heading-75" id="network-point-42">2. TCP和UDP的区别</h3>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<table><thead><tr><th></th><th>UDP</th><th>TCP</th></tr></thead><tbody><tr><td>是否连接</td><td>无连接</td><td>面向连接</td></tr><tr><td>是否可靠</td><td>不可靠传输,不使用流量控制和拥塞控制</td><td>可靠传输(数据顺序和正确性),使用流量控制和拥塞控制</td></tr><tr><td>连接对象个数</td><td>支持一对一,一对多,多对一和多对多交互通信</td><td>只能是一对一通信</td></tr><tr><td>传输方式</td><td>面向报文</td><td>面向字节流</td></tr><tr><td>首部开销</td><td>首部开销小,仅8字节</td><td>首部最小20字节,最大60字节</td></tr><tr><td>适用场景</td><td>适用于实时应用,例如视频会议、直播</td><td>适用于要求可靠传输的应用,例如文件传输</td></tr></tbody></table>
|
||||
<h3 data-id="heading-76" id="network-point-43">3. TCP和UDP的使用场景</h3>
|
||||
<ul>
|
||||
<li><strong>TCP应用场景:</strong> 效率要求相对低,但对准确性要求相对高的场景。因为传输中需要对数据确认、重发、排序等操作,相比之下效率没有UDP高。例如:文件传输(准确高要求高、但是速度可以相对慢)、接受邮件、远程登录。</li>
|
||||
<li><strong>UDP应用场景:</strong> 效率要求相对高,对准确性要求相对低的场景。例如:QQ聊天、在线视频、网络语音电话(即时通讯,速度要求高,但是出现偶尔断续不是太大问题,并且此处完全不可以使用重发机制)、广播通信(广播、多播)。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-77" id="network-point-44">4. UDP协议为什么不可靠?</h3>
|
||||
<p>UDP在传输数据之前不需要先建立连接,远地主机的运输层在接收到UDP报文后,不需要确认,提供不可靠交付。总结就以下四点:</p>
|
||||
<ul>
|
||||
<li>不保证消息交付:不确认,不重传,无超时</li>
|
||||
<li>不保证交付顺序:不设置包序号,不重排,不会发生队首阻塞</li>
|
||||
<li>不跟踪连接状态:不必建立连接或重启状态机</li>
|
||||
<li>不进行拥塞控制:不内置客户端或网络反馈机制</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-78" id="network-point-45">5. TCP的重传机制</h3>
|
||||
<p>由于TCP的下层网络(网络层)可能出现<strong>丢失、重复或失序</strong>的情况,TCP协议提供可靠数据传输服务。为保证数据传输的正确性,TCP会重传其认为已丢失(包括报文中的比特错误)的包。TCP使用两套独立的机制来完成重传,一是<strong>基于时间</strong>,二是<strong>基于确认信息</strong>。</p>
|
||||
<p>TCP在发送一个数据之后,就开启一个定时器,若是在这个时间内没有收到发送数据的ACK确认报文,则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号。</p>
|
||||
<h3 data-id="heading-79" id="network-point-46">6. TCP的拥塞控制机制</h3>
|
||||
<p>TCP的拥塞控制机制主要是以下四种机制:</p>
|
||||
<ul>
|
||||
<li>慢启动(慢开始)</li>
|
||||
<li>拥塞避免</li>
|
||||
<li>快速重传</li>
|
||||
<li>快速恢复</li>
|
||||
</ul>
|
||||
<p><strong>(1)慢启动(慢开始)</strong></p>
|
||||
<ul>
|
||||
<li>在开始发送的时候设置cwnd = 1(cwnd指的是拥塞窗口)</li>
|
||||
<li>思路:开始的时候不要发送大量数据,而是先测试一下网络的拥塞程度,由小到大增加拥塞窗口的大小。</li>
|
||||
<li>为了防止cwnd增长过大引起网络拥塞,设置一个慢开始门限(ssthresh 状态变量)
|
||||
<ul>
|
||||
<li>当cnwd < ssthresh,使用慢开始算法</li>
|
||||
<li>当cnwd = ssthresh,既可使用慢开始算法,也可以使用拥塞避免算法</li>
|
||||
<li>当cnwd > ssthresh,使用拥塞避免算法</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p><strong>(2)拥塞避免</strong></p>
|
||||
<ul>
|
||||
<li>拥塞避免未必能够完全避免拥塞,是说在拥塞避免阶段将拥塞窗口控制为按线性增长,使网络不容易出现阻塞。</li>
|
||||
<li>思路: 让拥塞窗口cwnd缓慢的增大,即每经过一个返回时间RTT就把发送方的拥塞控制窗口加一</li>
|
||||
<li>无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法。如图所示:
|
||||
<img src="/articles/network/network-figure-17.awebp" alt="" loading="lazy">
|
||||
其中,判断网络出现拥塞的根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理。</li>
|
||||
</ul>
|
||||
<p><strong>(3)快速重传</strong></p>
|
||||
<ul>
|
||||
<li>快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)。发送方只要连续收到三个重复确认就立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。</li>
|
||||
<li>由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量</li>
|
||||
</ul>
|
||||
<p><strong>(4)快速恢复</strong></p>
|
||||
<ul>
|
||||
<li>当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半。但是接下去并不执行慢开始算法。</li>
|
||||
<li>考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。
|
||||
<img src="/articles/network/network-figure-18.awebp" alt="" loading="lazy"></li>
|
||||
</ul>
|
||||
<h3 data-id="heading-80" id="network-point-47">7. TCP的流量控制机制</h3>
|
||||
<p>一般来说,流量控制就是为了让发送方发送数据的速度不要太快,要让接收方来得及接收。TCP采用大小可变的<strong>滑动窗口</strong>进行流量控制,窗口大小的单位是字节。这里说的窗口大小其实就是每次传输的数据大小。</p>
|
||||
<ul>
|
||||
<li>当一个连接建立时,连接的每一端分配一个缓冲区来保存输入的数据,并将缓冲区的大小发送给另一端。</li>
|
||||
<li>当数据到达时,接收方发送确认,其中包含了自己剩余的缓冲区大小。(剩余的缓冲区空间的大小被称为窗口,指出窗口大小的通知称为窗口通告 。接收方在发送的每一确认中都含有一个窗口通告。)</li>
|
||||
<li>如果接收方应用程序读数据的速度能够与数据到达的速度一样快,接收方将在每一确认中发送一个正的窗口通告。</li>
|
||||
<li>如果发送方操作的速度快于接收方,接收到的数据最终将充满接收方的缓冲区,导致接收方通告一个零窗口 。发送方收到一个零窗口通告时,必须停止发送,直到接收方重新通告一个正的窗口。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-81" id="network-point-48">8. TCP的可靠传输机制</h3>
|
||||
<p>TCP 的可靠传输机制是基于连续 ARQ 协议和滑动窗口协议的。</p>
|
||||
<p>TCP 协议在发送方维持了一个发送窗口,发送窗口以前的报文段是已经发送并确认了的报文段,发送窗口中包含了已经发送但 未确认的报文段和允许发送但还未发送的报文段,发送窗口以后的报文段是缓存中还不允许发送的报文段。当发送方向接收方发 送报文时,会依次发送窗口内的所有报文段,并且设置一个定时器,这个定时器可以理解为是最早发送但未收到确认的报文段。 如果在定时器的时间内收到某一个报文段的确认回答,则滑动窗口,将窗口的首部向后滑动到确认报文段的后一个位置,此时如 果还有已发送但没有确认的报文段,则重新设置定时器,如果没有了则关闭定时器。如果定时器超时,则重新发送所有已经发送 但还未收到确认的报文段,并将超时的间隔设置为以前的两倍。当发送方收到接收方的三个冗余的确认应答后,这是一种指示, 说明该报文段以后的报文段很有可能发生丢失了,那么发送方会启用快速重传的机制,就是当前定时器结束前,发送所有的已发 送但确认的报文段。</p>
|
||||
<p>接收方使用的是累计确认的机制,对于所有按序到达的报文段,接收方返回一个报文段的肯定回答。如果收到了一个乱序的报文 段,那么接方会直接丢弃,并返回一个最近的按序到达的报文段的肯定回答。使用累计确认保证了返回的确认号之前的报文段都 已经按序到达了,所以发送窗口可以移动到已确认报文段的后面。</p>
|
||||
<p>发送窗口的大小是变化的,它是由接收窗口剩余大小和网络中拥塞程度来决定的,TCP 就是通过控制发送窗口的长度来控制报文 段的发送速率。</p>
|
||||
<p>但是 TCP 协议并不完全和滑动窗口协议相同,因为许多的 TCP 实现会将失序的报文段给缓存起来,并且发生重传时,只会重 传一个报文段,因此 TCP 协议的可靠传输机制更像是窗口滑动协议和选择重传协议的一个混合体。</p>
|
||||
<h3 data-id="heading-82" id="network-point-49">9. TCP的三次握手和四次挥手</h3>
|
||||
<h4 data-id="heading-83">(1)三次握手</h4>
|
||||
<p><img src="/articles/network/network-figure-19.awebp" alt="" loading="lazy">
|
||||
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。</p>
|
||||
<p>刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。</p>
|
||||
<ul>
|
||||
<li>第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN,此时客户端处于 SYN_SEND 状态。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。</p>
|
||||
</blockquote>
|
||||
<p><strong>那为什么要三次握手呢?两次不行吗?</strong></p>
|
||||
<ul>
|
||||
<li>为了确认双方的接收能力和发送能力都正常</li>
|
||||
<li>如果是用两次握手,则会出现下面这种情况:</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。</p>
|
||||
</blockquote>
|
||||
<p><strong>简单来说就是以下三步:</strong></p>
|
||||
<ul>
|
||||
<li><strong>第一次握手:</strong> 客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。</li>
|
||||
<li><strong>第二次握手:</strong> 服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。</li>
|
||||
<li><strong>第三次握手:</strong> 当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。</li>
|
||||
</ul>
|
||||
<p>TCP 三次握手的建立连接的过程就是相互确认初始序号的过程,告诉对方,什么样序号的报文段能够被正确接收。 第三次握手的作用是客户端对服务器端的初始序号的确认。如果只使用两次握手,那么服务器就没有办法知道自己的序号是否 已被确认。同时这样也是为了防止失效的请求报文段被服务器接收,而出现错误的情况。</p>
|
||||
<h4 data-id="heading-84">(2)四次挥手</h4>
|
||||
<p><img src="/articles/network/network-figure-20.awebp" alt="" loading="lazy">
|
||||
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:</p>
|
||||
<ul>
|
||||
<li>第一次挥手: 客户端会发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。</p>
|
||||
</blockquote>
|
||||
<p><strong>那为什么需要四次挥手呢?</strong></p>
|
||||
<blockquote>
|
||||
<p>因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四次挥手。</p>
|
||||
</blockquote>
|
||||
<p><strong>简单来说就是以下四步:</strong></p>
|
||||
<ul>
|
||||
<li><strong>第一次挥手:</strong> 若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。</li>
|
||||
<li><strong>第二次挥手</strong>:服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。</li>
|
||||
<li><strong>第三次挥手</strong>:服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。</li>
|
||||
<li><strong>第四次挥手:</strong> 客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。</li>
|
||||
</ul>
|
||||
<p>TCP 使用四次挥手的原因是因为 TCP 的连接是全双工的,所以需要双方分别释放到对方的连接,单独一方的连接释放,只代 表不能再向对方发送数据,连接处于的是半释放的状态。</p>
|
||||
<p>最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止发送给服务器的确认报文段丢失或者出错,从而导致服务器 端不能正常关闭。</p>
|
||||
<h3 data-id="heading-85" id="network-point-50">10. TCP粘包是怎么回事,如何处理?</h3>
|
||||
<p>默认情况下, TCP 连接会启⽤延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到⼀起作⼀次发送 (缓冲⼤⼩⻅ socket.bufferSize ), 这样可以减少 IO 消耗提⾼性能.</p>
|
||||
<p>如果是传输⽂件的话, 那么根本不⽤处理粘包的问题, 来⼀个包拼⼀个包就好了。但是如果是多条消息, 或者是别的⽤途的数据那么就需要处理粘包.</p>
|
||||
<p>下面看⼀个例⼦, 连续调⽤两次 send 分别发送两段数据 data1 和 data2, 在接收端有以下⼏种常⻅的情况:
|
||||
A. 先接收到 data1, 然后接收到 data2 .
|
||||
B. 先接收到 data1 的部分数据, 然后接收到 data1 余下的部分以及 data2 的全部.
|
||||
C. 先接收到了 data1 的全部数据和 data2 的部分数据, 然后接收到了 data2 的余下的数据.
|
||||
D. ⼀次性接收到了 data1 和 data2 的全部数据.</p>
|
||||
<p>其中的 BCD 就是我们常⻅的粘包的情况. ⽽对于处理粘包的问题, 常⻅的解决⽅案有:</p>
|
||||
<ul>
|
||||
<li><strong>多次发送之前间隔⼀个等待时间</strong>:只需要等上⼀段时间再进⾏下⼀次 send 就好, 适⽤于交互频率特别低的场景. 缺点也很明显, 对于⽐较频繁的场景⽽⾔传输效率实在太低,不过⼏乎不⽤做什么处理.</li>
|
||||
<li><strong>关闭 Nagle 算法</strong>:关闭 Nagle 算法, 在 Node.js 中你可以通过 socket.setNoDelay() ⽅法来关闭 Nagle 算法, 让每⼀次 send 都不缓冲直接发送。该⽅法⽐较适⽤于每次发送的数据都⽐较⼤ (但不是⽂件那么⼤), 并且频率不是特别⾼的场景。如果是每次发送的数据量⽐较⼩, 并且频率特别⾼的, 关闭 Nagle 纯属⾃废武功。另外, 该⽅法不适⽤于⽹络较差的情况, 因为 Nagle 算法是在服务端进⾏的包合并情况, 但是如果短时间内客户端的⽹络情况不好, 或者应⽤层由于某些原因不能及时将 TCP 的数据 recv, 就会造成多个包在客户端缓冲从⽽粘包的情况。 (如果是在稳定的机房内部通信那么这个概率是⽐较⼩可以选择忽略的)</li>
|
||||
<li><strong>进⾏封包/拆包:</strong> 封包/拆包是⽬前业内常⻅的解决⽅案了。即给每个数据包在发送之前, 于其前/后放⼀些有特征的数据, 然后收到数据的时 候根据特征数据分割出来各个数据包。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-86" id="network-point-51">11. 为什么<strong>udp</strong>不会粘包?</h3>
|
||||
<ul>
|
||||
<li>TCP协议是⾯向流的协议,UDP是⾯向消息的协议。UDP段都是⼀条消息,应⽤程序必须以消息为单位提取数据,不能⼀次提取任意字节的数据</li>
|
||||
<li>UDP具有保护消息边界,在每个UDP包中就有了消息头(消息来源地址,端⼝等信息),这样对于接收端来说就容易进⾏区分处理了。传输协议把数据当作⼀条独⽴的消息在⽹上传输,接收端只能接收独⽴的消息。接收端⼀次只能接收发送端发出的⼀个数据包,如果⼀次接受数据的⼤⼩⼩于发送端⼀次发送的数据⼤⼩,就会丢失⼀部分数据,即使丢失,接受端也不会分两次去接收。</li>
|
||||
</ul>
|
||||
<h2 data-id="heading-87">七、WebSocket</h2>
|
||||
<h3 data-id="heading-88" id="network-point-52">1. 对 WebSocket 的理解</h3>
|
||||
<p>WebSocket是HTML5提供的一种浏览器与服务器进行<strong>全双工通讯</strong>的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。</p>
|
||||
<p>WebSocket 的出现就解决了半双工通信的弊端。它最大的特点是:<strong>服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。</strong></p>
|
||||
<p><strong>WebSocket原理</strong>:客户端向 WebSocket 服务器通知(notify)一个带有所有接收者ID(recipients IDs)的事件(event),服务器接收后立即通知所有活跃的(active)客户端,只有ID在接收者ID序列中的客户端才会处理这个事件。
|
||||
</p>
|
||||
<p><strong>WebSocket 特点的如下:</strong></p>
|
||||
<ul>
|
||||
<li>支持双向通信,实时性更强</li>
|
||||
<li>可以发送文本,也可以发送二进制数据‘’</li>
|
||||
<li>建立在TCP协议之上,服务端的实现比较容易</li>
|
||||
<li>数据格式比较轻量,性能开销小,通信高效</li>
|
||||
<li>没有同源限制,客户端可以与任意服务器通信</li>
|
||||
<li>协议标识符是ws(如果加密,则为wss),服务器网址就是 URL</li>
|
||||
<li>与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。</li>
|
||||
</ul>
|
||||
<p><strong>Websocket的使用方法如下:</strong>
|
||||
</p>
|
||||
<p>在客户端中:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">// 在index.html中直接写WebSocket,设置服务端的端口号为 9999
|
||||
let ws = new WebSocket('ws://localhost:9999');
|
||||
// 在客户端与服务端建立连接后触发
|
||||
ws.onopen = function() {
|
||||
console.log("Connection open.");
|
||||
ws.send('hello');
|
||||
};
|
||||
// 在服务端给客户端发来消息的时候触发
|
||||
ws.onmessage = function(res) {
|
||||
console.log(res); // 打印的是MessageEvent对象
|
||||
console.log(res.data); // 打印的是收到的消息
|
||||
};
|
||||
// 在客户端与服务端建立关闭后触发
|
||||
ws.onclose = function(evt) {
|
||||
console.log("Connection closed.");
|
||||
};</code></pre>
|
||||
<h3 data-id="heading-89" id="network-point-53">2. 即时通讯的实现:短轮询、长轮询、SSE 和 WebSocket 间的区别?</h3>
|
||||
<p>短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。</p>
|
||||
<p><strong>短轮询的基本思路:</strong> 浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点是这种方式由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。</p>
|
||||
<p><strong>长轮询的基本思路:</strong> 首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。长轮询和短轮询比起来,它的优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。</p>
|
||||
<p><strong>SSE 的基本思想:</strong> 服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支持。它相对于前面两种方式来说,不需要建立过多的 http 请求,相比之下节约了资源。</p>
|
||||
<p><strong>WebSocket</strong> 是 HTML5 定义的一个新协议议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息,而 SSE 的方式是单向通信的,只能由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。</p>
|
||||
<p><strong>上面的四个通信协议,前三个都是基于HTTP协议的。</strong></p>
|
||||
<p>对于这四种即使通信协议,从性能的角度来看:
|
||||
<strong>WebSocket > 长连接(SEE) > 长轮询 > 短轮询</strong>
|
||||
但是,我们如果考虑浏览器的兼容性问题,顺序就恰恰相反了:
|
||||
<strong>短轮询 > 长轮询 > 长连接(SEE) > WebSocket</strong>
|
||||
所以,还是要根据具体的使用场景来判断使用哪种方式。</p>
|
||||
@@ -0,0 +1,415 @@
|
||||
<h3 data-id="heading-2" id="performance-point-1">1. CDN的概念</h3>
|
||||
<p>CDN(Content Delivery Network,<strong>内容分发网络</strong>)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。</p>
|
||||
<p>典型的CDN系统由下面三个部分组成:</p>
|
||||
<ul>
|
||||
<li><strong>分发服务系统:</strong> 最基本的工作单元就是Cache设备,cache(边缘cache)负责直接响应最终用户的访问请求,把缓存在本地的内容快速地提供给用户。同时cache还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保存在本地。Cache设备的数量、规模、总服务能力是衡量一个CDN系统服务能力的最基本的指标。</li>
|
||||
<li><strong>负载均衡系统:</strong> 主要功能是负责对所有发起服务请求的用户进行访问调度,确定提供给用户的最终实际访问地址。两级调度体系分为全局负载均衡(GSLB)和本地负载均衡(SLB)。<strong>全局负载均衡</strong>主要根据用户就近性原则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的cache的物理位置。<strong>本地负载均衡</strong>主要负责节点内部的设备负载均衡</li>
|
||||
<li><strong>运营管理系统:</strong> 运营管理系统分为运营管理和网络管理子系统,负责处理业务层面的与外界系统交互所必须的收集、整理、交付工作,包含客户管理、产品管理、计费管理、统计分析等功能。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-3" id="performance-point-2">2. CDN的作用</h3>
|
||||
<p>CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。</p>
|
||||
<p>(1)在性能方面,引入CDN的作用在于:</p>
|
||||
<ul>
|
||||
<li>用户收到的内容来自最近的数据中心,延迟更低,内容加载更快</li>
|
||||
<li>部分资源请求分配给了CDN,减少了服务器的负载</li>
|
||||
</ul>
|
||||
<p>(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击:</p>
|
||||
<ul>
|
||||
<li>针对DDoS:通过监控分析异常流量,限制其请求频率</li>
|
||||
<li>针对MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信</li>
|
||||
</ul>
|
||||
<p>除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。</p>
|
||||
<h3 data-id="heading-4" id="performance-point-3">3. CDN的原理</h3>
|
||||
<p>CDN和DNS有着密不可分的联系,先来看一下DNS的解析域名过程,在浏览器输入 <a href="https://link.juejin.cn/?target=http%3A%2F%2Fwww.test.com" target="_blank" rel="noreferrer" title="http://www.test.com" ref="nofollow noopener noreferrer">www.test.com</a> 的解析过程如下:
|
||||
(1) 检查浏览器缓存
|
||||
(2)检查操作系统缓存,常见的如hosts文件
|
||||
(3)检查路由器缓存
|
||||
(4)如果前几步都没没找到,会向ISP(网络服务提供商)的LDNS服务器查询
|
||||
(5)如果LDNS服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:</p>
|
||||
<ul>
|
||||
<li>根服务器返回顶级域名(TLD)服务器如<code>.com</code>,<code>.cn</code>,<code>.org</code>等的地址,该例子中会返回<code>.com</code>的地址</li>
|
||||
<li>接着向顶级域名服务器发送请求,然后会返回次级域名(SLD)服务器的地址,本例子会返回<code>.test</code>的地址</li>
|
||||
<li>接着向次级域名服务器发送请求,然后会返回通过域名查询到的目标IP,本例子会返回<code>www.test.com</code>的地址</li>
|
||||
<li>Local DNS Server会缓存结果,并返回给用户,缓存在系统中</li>
|
||||
</ul>
|
||||
<p><strong>CDN的工作原理:</strong>
|
||||
(1)用户未使用CDN缓存资源的过程:</p>
|
||||
<ol>
|
||||
<li>浏览器通过DNS对域名进行解析(就是上面的DNS解析过程),依次得到此域名对应的IP地址</li>
|
||||
<li>浏览器根据得到的IP地址,向域名的服务主机发送数据请求</li>
|
||||
<li>服务器向浏览器返回响应数据</li>
|
||||
</ol>
|
||||
<p>(2)用户使用CDN缓存资源的过程:</p>
|
||||
<ol>
|
||||
<li>对于点击的数据的URL,经过本地DNS系统的解析,发现该URL对应的是一个CDN专用的DNS服务器,DNS系统就会将域名解析权交给CNAME指向的CDN专用的DNS服务器。</li>
|
||||
<li>CND专用DNS服务器将CND的全局负载均衡设备IP地址返回给用户</li>
|
||||
<li>用户向CDN的全局负载均衡设备发起数据请求</li>
|
||||
<li>CDN的全局负载均衡设备根据用户的IP地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求</li>
|
||||
<li>区域负载均衡设备选择一台合适的缓存服务器来提供服务,将该缓存服务器的IP地址返回给全局负载均衡设备</li>
|
||||
<li>全局负载均衡设备把服务器的IP地址返回给用户</li>
|
||||
<li>用户向该缓存服务器发起请求,缓存服务器响应用户的请求,将用户所需内容发送至用户终端。</li>
|
||||
</ol>
|
||||
<p>如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。</p>
|
||||
<p><img src="/articles/performance/performance-figure-1.awebp" alt="" loading="lazy">
|
||||
CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的IP地址,或者该域名的一个CNAME,然后再根据这个CNAME来查找对应的IP地址。</p>
|
||||
<h3 data-id="heading-5" id="performance-point-4">4. CDN的使用场景</h3>
|
||||
<ul>
|
||||
<li>**使用第三方的CDN服务:**如果想要开源一些项目,可以使用第三方的CDN服务</li>
|
||||
<li>**使用CDN进行静态资源的缓存:**将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。</li>
|
||||
<li>**直播传送:**直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度。CDN在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。</li>
|
||||
</ul>
|
||||
<h2 data-id="heading-6">二、懒加载</h2>
|
||||
<h3 data-id="heading-7" id="performance-point-5">1. 懒加载的概念</h3>
|
||||
<p>懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。</p>
|
||||
<p>如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。</p>
|
||||
<h3 data-id="heading-8" id="performance-point-6">2. 懒加载的特点</h3>
|
||||
<ul>
|
||||
<li><strong>减少无用资源的加载</strong>:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。</li>
|
||||
<li><strong>提升用户体验</strong>: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。</li>
|
||||
<li><strong>防止加载过多图片而影响其他资源文件的加载</strong> :会影响网站应用的正常使用。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-9" id="performance-point-7">3. 懒加载的实现原理</h3>
|
||||
<p>图片的加载是由<code>src</code>引起的,当对<code>src</code>赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的<code>data-xxx</code>属性来储存图片的路径,在需要加载图片的时候,将<code>data-xxx</code>中图片的路径赋值给<code>src</code>,这样就实现了图片的按需加载,即懒加载。</p>
|
||||
<p>注意:<code>data-xxx</code> 中的<code>xxx</code>可以自定义,这里我们使用<code>data-src</code>来定义。</p>
|
||||
<p>懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。</p>
|
||||
<p>使用原生JavaScript实现懒加载:</p>
|
||||
<p><strong>知识点:</strong></p>
|
||||
<p>(1)<code>window.innerHeight</code> 是浏览器可视区的高度</p>
|
||||
<p>(2)<code>document.body.scrollTop || document.documentElement.scrollTop</code> 是浏览器滚动的过的距离</p>
|
||||
<p>(3)<code>imgs.offsetTop</code> 是元素顶部距离文档顶部的高度(包括滚动条的距离)</p>
|
||||
<p>(4)图片加载条件:<code>img.offsetTop < window.innerHeight + document.body.scrollTop;</code></p>
|
||||
<p><strong>图示:</strong>
|
||||
<img src="/articles/performance/performance-figure-2.awebp" alt="" loading="lazy">
|
||||
<strong>代码实现:</strong></p>
|
||||
<pre><code class="language-javascript" lang="javascript"><div class="container">
|
||||
<img src="loading.gif" data-src="pic.png">
|
||||
<img src="loading.gif" data-src="pic.png">
|
||||
<img src="loading.gif" data-src="pic.png">
|
||||
<img src="loading.gif" data-src="pic.png">
|
||||
<img src="loading.gif" data-src="pic.png">
|
||||
<img src="loading.gif" data-src="pic.png">
|
||||
</div>
|
||||
<script>
|
||||
var imgs = document.querySelectorAll('img');
|
||||
function lozyLoad(){
|
||||
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
|
||||
var winHeight= window.innerHeight;
|
||||
for(var i=0;i < imgs.length;i++){
|
||||
if(imgs[i].offsetTop < scrollTop + winHeight ){
|
||||
imgs[i].src = imgs[i].getAttribute('data-src');
|
||||
}
|
||||
}
|
||||
}
|
||||
window.onscroll = lozyLoad();
|
||||
</script></code></pre>
|
||||
<h3 data-id="heading-10" id="performance-point-8">4. 懒加载与预加载的区别</h3>
|
||||
<p>这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。</p>
|
||||
<ul>
|
||||
<li><strong>懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载</strong>,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。</li>
|
||||
<li><strong>预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。</strong> 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。</li>
|
||||
</ul>
|
||||
<h2 data-id="heading-11">三、回流与重绘</h2>
|
||||
<h3 data-id="heading-12" id="performance-point-9">1. 回流与重绘的概念及触发条件</h3>
|
||||
<h4 data-id="heading-13">(1)回流</h4>
|
||||
<p>当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为<strong>回流</strong>。</p>
|
||||
<p>下面这些操作会导致回流:</p>
|
||||
<ul>
|
||||
<li>页面的首次渲染</li>
|
||||
<li>浏览器的窗口大小发生变化</li>
|
||||
<li>元素的内容发生变化</li>
|
||||
<li>元素的尺寸或者位置发生变化</li>
|
||||
<li>元素的字体大小发生变化</li>
|
||||
<li>激活CSS伪类</li>
|
||||
<li>查询某些属性或者调用某些方法</li>
|
||||
<li>添加或者删除可见的DOM元素</li>
|
||||
</ul>
|
||||
<p>在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:</p>
|
||||
<ul>
|
||||
<li>全局范围:从根节点开始,对整个渲染树进行重新布局</li>
|
||||
<li>局部范围:对渲染树的某部分或者一个渲染对象进行重新布局</li>
|
||||
</ul>
|
||||
<h4 data-id="heading-14">(2)重绘</h4>
|
||||
<p>当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是<strong>重绘</strong>。</p>
|
||||
<p>下面这些操作会导致回流:</p>
|
||||
<ul>
|
||||
<li>color、background 相关属性:background-color、background-image 等</li>
|
||||
<li>outline 相关属性:outline-color、outline-width 、text-decoration</li>
|
||||
<li>border-radius、visibility、box-shadow</li>
|
||||
</ul>
|
||||
<p>注意: <strong>当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。</strong></p>
|
||||
<h3 data-id="heading-15" id="performance-point-10">2. 如何避免回流与重绘?</h3>
|
||||
<p><strong>减少回流与重绘的措施:</strong></p>
|
||||
<ul>
|
||||
<li>操作DOM时,尽量在低层级的DOM节点进行操作</li>
|
||||
<li>不要使用<code>table</code>布局, 一个小的改动可能会使整个<code>table</code>进行重新布局</li>
|
||||
<li>使用CSS的表达式</li>
|
||||
<li>不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。</li>
|
||||
<li>使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素</li>
|
||||
<li>避免频繁操作DOM,可以创建一个文档片段<code>documentFragment</code>,在它上面应用所有DOM操作,最后再把它添加到文档中</li>
|
||||
<li>将元素先设置<code>display: none</code>,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。</li>
|
||||
<li>将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于<strong>浏览器的渲染队列机制</strong>。</li>
|
||||
</ul>
|
||||
<p>浏览器针对页面的回流与重绘,进行了自身的优化——<strong>渲染队列</strong></p>
|
||||
<p><strong>浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。</strong></p>
|
||||
<p>上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。</p>
|
||||
<h3 data-id="heading-16" id="performance-point-11">3. 如何优化动画?</h3>
|
||||
<p>对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的<code>position</code>属性设置为<code>absolute</code>或者<code>fixed</code>,将动画脱离文档流,这样他的回流就不会影响到页面了。</p>
|
||||
<h3 data-id="heading-17" id="performance-point-12">4. documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?</h3>
|
||||
<p>MDN中对<code>documentFragment</code>的解释:</p>
|
||||
<blockquote>
|
||||
<p>DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。</p>
|
||||
</blockquote>
|
||||
<p>当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。</p>
|
||||
<h2 data-id="heading-18">四、节流与防抖</h2>
|
||||
<h3 data-id="heading-19" id="performance-point-13">1. 对节流与防抖的理解</h3>
|
||||
<ul>
|
||||
<li>函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。</li>
|
||||
<li>函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。</li>
|
||||
</ul>
|
||||
<p><strong>防抖函数的应用场景:</strong></p>
|
||||
<ul>
|
||||
<li>按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次</li>
|
||||
<li>服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce</li>
|
||||
</ul>
|
||||
<p><strong>节流函数的适⽤场景:</strong></p>
|
||||
<ul>
|
||||
<li>拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动</li>
|
||||
<li>缩放场景:监控浏览器resize</li>
|
||||
<li>动画场景:避免短时间内多次触发动画引起性能问题</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-20" id="performance-point-14">2. 实现节流函数和防抖函数</h3>
|
||||
<p><strong>函数防抖的实现:</strong></p>
|
||||
<pre><code class="language-javascript" lang="javascript">function debounce(fn, wait) {
|
||||
var timer = null;
|
||||
|
||||
return function() {
|
||||
var context = this,
|
||||
args = [...arguments];
|
||||
|
||||
// 如果此时存在定时器的话,则取消之前的定时器重新记时
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
// 设置定时器,使事件间隔指定事件后执行
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(context, args);
|
||||
}, wait);
|
||||
};
|
||||
}</code></pre>
|
||||
<p><strong>函数节流的实现:</strong></p>
|
||||
<pre><code class="language-javascript" lang="javascript">// 时间戳版
|
||||
function throttle(fn, delay) {
|
||||
var preTime = Date.now();
|
||||
|
||||
return function() {
|
||||
var context = this,
|
||||
args = [...arguments],
|
||||
nowTime = Date.now();
|
||||
|
||||
// 如果两次时间间隔超过了指定时间,则执行函数。
|
||||
if (nowTime - preTime >= delay) {
|
||||
preTime = Date.now();
|
||||
return fn.apply(context, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 定时器版
|
||||
function throttle (fun, wait){
|
||||
let timeout = null
|
||||
return function(){
|
||||
let context = this
|
||||
let args = [...arguments]
|
||||
if(!timeout){
|
||||
timeout = setTimeout(() => {
|
||||
fun.apply(context, args)
|
||||
timeout = null
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
<h2 data-id="heading-21">五、图片优化</h2>
|
||||
<h3 data-id="heading-22" id="performance-point-15">1. 如何对项目中的图片进行优化?</h3>
|
||||
<ol>
|
||||
<li>不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。</li>
|
||||
<li>对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。</li>
|
||||
<li>小图使用 base64 格式</li>
|
||||
<li>将多个图标文件整合到一张图片中(雪碧图)</li>
|
||||
<li>选择正确的图片格式:
|
||||
<ul>
|
||||
<li>对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好</li>
|
||||
<li>小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替</li>
|
||||
<li>照片使用 JPEG</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h3 data-id="heading-23" id="performance-point-16">2. 常见的图片格式及使用场景</h3>
|
||||
<p>(1)<strong>BMP</strong>,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。</p>
|
||||
<p>(2)<strong>GIF</strong>是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。</p>
|
||||
<p>(3)<strong>JPEG</strong>是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。</p>
|
||||
<p>(4)<strong>PNG-8</strong>是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。</p>
|
||||
<p>(5)<strong>PNG-24</strong>是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多。</p>
|
||||
<p>(6)<strong>SVG</strong>是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制Logo、Icon等。</p>
|
||||
<p>(7)<strong>WebP</strong>是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,兼容性不太好。</p>
|
||||
<ul>
|
||||
<li>在无损压缩的情况下,相同质量的WebP图片,文件大小要比PNG小26%;</li>
|
||||
<li>在有损压缩的情况下,具有相同图片精度的WebP图片,文件大小要比JPEG小25%~34%;</li>
|
||||
<li>WebP图片格式支持图片透明度,一个无损压缩的WebP图片,如果要支持透明度只需要22%的格外文件大小。</li>
|
||||
</ul>
|
||||
<h2 data-id="heading-24">六、Webpack优化</h2>
|
||||
<h3 data-id="heading-25" id="performance-point-17">1. 如何提⾼<strong>webpack</strong>的打包速度?</h3>
|
||||
<h4 data-id="heading-26">(1)优化 Loader</h4>
|
||||
<p>对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,<strong>转换代码越多,效率就越低</strong>。当然了,这是可以优化的。</p>
|
||||
<p>首先我们<strong>优化 Loader 的文件搜索范围</strong></p>
|
||||
<pre><code class="language-javascript" lang="javascript">module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// js 文件才使用 babel
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
// 只在 src 文件夹下查找
|
||||
include: [resolve('src')],
|
||||
// 不会去查找的路径
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
}
|
||||
}</code></pre>
|
||||
<p>对于 Babel 来说,希望只作用在 JS 代码上的,然后 <code>node_modules</code> 中使用的代码都是编译过的,所以完全没有必要再去处理一遍。</p>
|
||||
<p>当然这样做还不够,还可以将 Babel 编译过的文件<strong>缓存</strong>起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间</p>
|
||||
<pre><code class="language-javascript" lang="javascript">loader: 'babel-loader?cacheDirectory=true'</code></pre>
|
||||
<h4 data-id="heading-27">(2)HappyPack</h4>
|
||||
<p>受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。</p>
|
||||
<p><strong>HappyPack 可以将 Loader 的同步执行转换为并行的</strong>,这样就能充分利用系统资源来加快打包效率了</p>
|
||||
<pre><code class="language-javascript" lang="javascript">module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: [resolve('src')],
|
||||
exclude: /node_modules/,
|
||||
// id 后面的内容对应下面
|
||||
loader: 'happypack/loader?id=happybabel'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new HappyPack({
|
||||
id: 'happybabel',
|
||||
loaders: ['babel-loader?cacheDirectory'],
|
||||
// 开启 4 个线程
|
||||
threads: 4
|
||||
})
|
||||
]</code></pre>
|
||||
<h4 data-id="heading-28">(3)DllPlugin</h4>
|
||||
<p><strong>DllPlugin 可以将特定的类库提前打包然后引入</strong>。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">// 单独配置在一个文件中
|
||||
// webpack.dll.conf.js
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
module.exports = {
|
||||
entry: {
|
||||
// 想统一打包的类库
|
||||
vendor: ['react']
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].dll.js',
|
||||
library: '[name]-[hash]'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DllPlugin({
|
||||
// name 必须和 output.library 一致
|
||||
name: '[name]-[hash]',
|
||||
// 该属性需要与 DllReferencePlugin 中一致
|
||||
context: __dirname,
|
||||
path: path.join(__dirname, 'dist', '[name]-manifest.json')
|
||||
})
|
||||
]
|
||||
}</code></pre>
|
||||
<p>然后需要执行这个配置文件生成依赖文件,接下来需要使用 <code>DllReferencePlugin</code> 将依赖文件引入项目中</p>
|
||||
<pre><code class="language-javascript" lang="javascript">// webpack.conf.js
|
||||
module.exports = {
|
||||
// ...省略其他配置
|
||||
plugins: [
|
||||
new webpack.DllReferencePlugin({
|
||||
context: __dirname,
|
||||
// manifest 就是之前打包出来的 json 文件
|
||||
manifest: require('./dist/vendor-manifest.json'),
|
||||
})
|
||||
]
|
||||
}</code></pre>
|
||||
<h4 data-id="heading-29">(4)代码压缩</h4>
|
||||
<p>在 Webpack3 中,一般使用 <code>UglifyJS</code> 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 <code>webpack-parallel-uglify-plugin</code> 来并行运行 <code>UglifyJS</code>,从而提高效率。</p>
|
||||
<p>在 Webpack4 中,不需要以上这些操作了,只需要将 <code>mode</code> 设置为 <code>production</code> 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 <code>console.log</code> 这类代码的功能。</p>
|
||||
<h4 data-id="heading-30">(5)其他</h4>
|
||||
<p>可以通过一些小的优化点来加快打包速度</p>
|
||||
<ul>
|
||||
<li><code>resolve.extensions</code>:用来表明文件后缀列表,默认查找顺序是 <code>['.js', '.json']</code>,如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面</li>
|
||||
<li><code>resolve.alias</code>:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径</li>
|
||||
<li><code>module.noParse</code>:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-31" id="performance-point-18">2. 如何减少 Webpack 打包体积</h3>
|
||||
<h4 data-id="heading-32">(1)按需加载</h4>
|
||||
<p>在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,<strong>这时候就可以使用按需加载,将每个路由页面单独打包为一个文件</strong>。当然不仅仅路由可以按需加载,对于 <code>loadash</code> 这种大型类库同样可以使用这个功能。</p>
|
||||
<p>按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 <code>Promise</code>,当 <code>Promise</code> 成功以后去执行回调。</p>
|
||||
<h4 data-id="heading-33">(2)Scope Hoisting</h4>
|
||||
<p><strong>Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。</strong></p>
|
||||
<p>比如希望打包两个文件:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">// test.js
|
||||
export const a = 1
|
||||
// index.js
|
||||
import { a } from './test.js'</code></pre>
|
||||
<p>对于这种情况,打包出来的代码会类似这样:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">[
|
||||
/* 0 */
|
||||
function (module, exports, require) {
|
||||
//...
|
||||
},
|
||||
/* 1 */
|
||||
function (module, exports, require) {
|
||||
//...
|
||||
}
|
||||
]</code></pre>
|
||||
<p>但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">[
|
||||
/* 0 */
|
||||
function (module, exports, require) {
|
||||
//...
|
||||
}
|
||||
]</code></pre>
|
||||
<p>这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 <code>optimization.concatenateModules</code> 就可以了:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">module.exports = {
|
||||
optimization: {
|
||||
concatenateModules: true
|
||||
}
|
||||
}</code></pre>
|
||||
<h4 data-id="heading-34">(3)Tree Shaking</h4>
|
||||
<p><strong>Tree Shaking 可以实现删除项目中未被引用的代码</strong>,比如:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">// test.js
|
||||
export const a = 1
|
||||
export const b = 2
|
||||
// index.js
|
||||
import { a } from './test.js'</code></pre>
|
||||
<p>对于以上情况,<code>test</code> 文件中的变量 <code>b</code> 如果没有在项目中使用到的话,就不会被打包到文件中。</p>
|
||||
<p>如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。</p>
|
||||
<h3 data-id="heading-35" id="performance-point-19">3. 如何⽤<strong>webpack</strong>来优化前端性能?</h3>
|
||||
<p>⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。</p>
|
||||
<ul>
|
||||
<li><strong>压缩代码</strong>:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css</li>
|
||||
<li><strong>利⽤CDN加速</strong>: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径</li>
|
||||
<li><strong>Tree Shaking</strong>: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现</li>
|
||||
<li><strong>Code Splitting:</strong> 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存</li>
|
||||
<li><strong>提取公共第三⽅库</strong>: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-36" id="performance-point-20">4. 如何提⾼<strong>webpack</strong>的构建速度?</h3>
|
||||
<ol>
|
||||
<li>多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码</li>
|
||||
<li>通过 externals 配置来提取常⽤库</li>
|
||||
<li>利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。</li>
|
||||
<li>使⽤ Happypack 实现多线程加速编译</li>
|
||||
<li>使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度</li>
|
||||
<li>使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码</li>
|
||||
</ol>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,625 @@
|
||||
<h3 data-id="heading-2" id="vue-part-2-point-1">1. Vue-Router 的懒加载如何实现</h3>
|
||||
<p>非懒加载:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">import List from '@/components/list.vue'
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{ path: '/list', component: List }
|
||||
]
|
||||
})</code></pre>
|
||||
<p>(1)方案一(常用):使用箭头函数+import动态加载</p>
|
||||
<pre><code class="language-javascript" lang="javascript">const List = () => import('@/components/list.vue')
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{ path: '/list', component: List }
|
||||
]
|
||||
})</code></pre>
|
||||
<p>(2)方案二:使用箭头函数+require动态加载</p>
|
||||
<pre><code class="language-javascript" lang="javascript">const router = new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/list',
|
||||
component: resolve => require(['@/components/list'], resolve)
|
||||
}
|
||||
]
|
||||
})</code></pre>
|
||||
<p>(3)方案三:使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。</p>
|
||||
<pre><code class="language-javascript" lang="javascript">// r就是resolve
|
||||
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
|
||||
// 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载
|
||||
const router = new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/list',
|
||||
component: List,
|
||||
name: 'list'
|
||||
}
|
||||
]
|
||||
}))</code></pre>
|
||||
<h3 data-id="heading-3" id="vue-part-2-point-2">2. 路由的hash和history模式的区别</h3>
|
||||
<p>Vue-Router有两种模式:<strong>hash模式</strong>和<strong>history模式</strong>。默认的路由模式是hash模式。</p>
|
||||
<h4 data-id="heading-4">1. hash模式</h4>
|
||||
<p><strong>简介:</strong> hash模式是开发中默认的模式,它的URL带着一个#,例如:<a href="https://link.juejin.cn/?target=http%3A%2F%2Fwww.abc.com%2F%23%2Fvue" target="_blank" rel="noreferrer" title="http://www.abc.com/#/vue" ref="nofollow noopener noreferrer">www.abc.com/#/vue</a>,它的hash值就是<code>#/vue</code>。</p>
|
||||
<p><strong>特点</strong>:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。</p>
|
||||
<p><strong>原理:</strong> hash模式的主要原理就是<strong>onhashchange()事件</strong>:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">window.onhashchange = function(event){
|
||||
console.log(event.oldURL, event.newURL);
|
||||
let hash = location.hash.slice(1);
|
||||
}</code></pre>
|
||||
<p>使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。</p>
|
||||
<h4 data-id="heading-5">2. history模式</h4>
|
||||
<p><strong>简介:</strong> history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
|
||||
<strong>特点:</strong> 当使用history模式时,URL就像这样:<a href="https://link.juejin.cn/?target=http%3A%2F%2Fabc.com%2Fuser%2Fid" target="_blank" rel="noreferrer" title="http://abc.com/user/id" ref="nofollow noopener noreferrer">abc.com/user/id</a>。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
|
||||
<strong>API:</strong> history api可以分为两大部分,切换历史状态和修改历史状态:</p>
|
||||
<ul>
|
||||
<li><strong>修改历史状态</strong>:包括了 HTML5 History Interface 中新增的 <code>pushState()</code> 和 <code>replaceState()</code> 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。</li>
|
||||
<li><strong>切换历史状态:</strong> 包括<code>forward()</code>、<code>back()</code>、<code>go()</code>三个方法,对应浏览器的前进,后退,跳转操作。</li>
|
||||
</ul>
|
||||
<p>虽然history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。</p>
|
||||
<p>如果想要切换到history模式,就要进行以下配置(后端也要进行配置):</p>
|
||||
<pre><code class="language-javascript" lang="javascript">const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: [...]
|
||||
})</code></pre>
|
||||
<h4 data-id="heading-6">3. 两种模式对比</h4>
|
||||
<p>调用 history.pushState() 相比于直接修改 hash,存在以下优势:</p>
|
||||
<ul>
|
||||
<li>pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;</li>
|
||||
<li>pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;</li>
|
||||
<li>pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;</li>
|
||||
<li>pushState() 可额外设置 title 属性供后续使用。</li>
|
||||
<li>hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。</li>
|
||||
</ul>
|
||||
<p>hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。</p>
|
||||
<h3 data-id="heading-7" id="vue-part-2-point-3">3. 如何获取页面的hash变化</h3>
|
||||
<p><strong>(1)监听$route的变化</strong></p>
|
||||
<pre><code class="language-javascript" lang="javascript">// 监听,当路由发生变化的时候执行
|
||||
watch: {
|
||||
$route: {
|
||||
handler: function(val, oldVal){
|
||||
console.log(val);
|
||||
},
|
||||
// 深度观察监听
|
||||
deep: true
|
||||
}
|
||||
},</code></pre>
|
||||
<p><strong>(2)window.location.hash读取#值</strong>
|
||||
window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。</p>
|
||||
<h3 data-id="heading-8" id="vue-part-2-point-4">4. <code>$route 和$router</code> 的区别</h3>
|
||||
<ul>
|
||||
<li>$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数</li>
|
||||
<li>$router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-9" id="vue-part-2-point-5">5. 如何定义动态路由?如何获取传过来的动态参数?</h3>
|
||||
<p><strong>(1)param方式</strong></p>
|
||||
<ul>
|
||||
<li>配置路由格式:<code>/router/:id</code></li>
|
||||
<li>传递的方式:在path后面跟上对应的值</li>
|
||||
<li>传递后形成的路径:<code>/router/123</code></li>
|
||||
</ul>
|
||||
<p>1)路由定义</p>
|
||||
<pre><code class="language-javascript" lang="javascript">//在APP.vue中
|
||||
<router-link :to="'/user/'+userId" replace>用户</router-link>
|
||||
|
||||
//在index.js
|
||||
{
|
||||
path: '/user/:userid',
|
||||
component: User,
|
||||
},</code></pre>
|
||||
<p>2)路由跳转</p>
|
||||
<pre><code class="language-javascript" lang="javascript">// 方法1:
|
||||
<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
|
||||
|
||||
// 方法2:
|
||||
this.$router.push({name:'users',params:{uname:wade}})
|
||||
|
||||
// 方法3:
|
||||
this.$router.push('/user/' + wade)</code></pre>
|
||||
<p>3)参数获取
|
||||
通过 <code>$route.params.userid</code> 获取传递的值</p>
|
||||
<p><strong>(2)query方式</strong></p>
|
||||
<ul>
|
||||
<li>配置路由格式:<code>/router</code>,也就是普通配置</li>
|
||||
<li>传递的方式:对象中使用query的key作为传递方式</li>
|
||||
<li>传递后形成的路径:<code>/route?id=123</code></li>
|
||||
</ul>
|
||||
<p>1)路由定义</p>
|
||||
<pre><code class="language-javascript" lang="javascript">//方式1:直接在router-link 标签上以对象的形式
|
||||
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
|
||||
|
||||
// 方式2:写成按钮以点击事件形式
|
||||
<button @click='profileClick'>我的</button>
|
||||
|
||||
profileClick(){
|
||||
this.$router.push({
|
||||
path: "/profile",
|
||||
query: {
|
||||
name: "kobi",
|
||||
age: "28",
|
||||
height: 198
|
||||
}
|
||||
});
|
||||
}</code></pre>
|
||||
<p>2)跳转方法</p>
|
||||
<pre><code class="language-javascript" lang="javascript">// 方法1:
|
||||
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
|
||||
|
||||
// 方法2:
|
||||
this.$router.push({ name: 'users', query:{ uname:james }})
|
||||
|
||||
// 方法3:
|
||||
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
|
||||
|
||||
// 方法4:
|
||||
this.$router.push({ path: '/user', query:{ uname:james }})
|
||||
|
||||
// 方法5:
|
||||
this.$router.push('/user?uname=' + jsmes)</code></pre>
|
||||
<p>3)获取参数</p>
|
||||
<pre><code class="language-javascript" lang="javascript">通过$route.query 获取传递的值</code></pre>
|
||||
<h3 data-id="heading-10" id="vue-part-2-point-6">6. Vue-router 路由钩子在生命周期的体现</h3>
|
||||
<p>一、Vue-Router导航守卫</p>
|
||||
<p>有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。
|
||||
为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的</p>
|
||||
<ol>
|
||||
<li>全局路由钩子</li>
|
||||
</ol>
|
||||
<p>vue-router全局有三个路由钩子;</p>
|
||||
<ul>
|
||||
<li>router.beforeEach 全局前置守卫 进入路由之前</li>
|
||||
<li>router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用</li>
|
||||
<li>router.afterEach 全局后置钩子 进入路由之后</li>
|
||||
</ul>
|
||||
<p>具体使用∶</p>
|
||||
<ul>
|
||||
<li>beforeEach(判断是否登录了,没登录就跳转到登录页)</li>
|
||||
</ul>
|
||||
<pre><code class="language-javascript" lang="javascript">router.beforeEach((to, from, next) => {
|
||||
let ifInfo = Vue.prototype.$common.getSession('userData'); // 判断是否登录的存储信息
|
||||
if (!ifInfo) {
|
||||
// sessionStorage里没有储存user信息
|
||||
if (to.path == '/') {
|
||||
//如果是登录页面路径,就直接next()
|
||||
next();
|
||||
} else {
|
||||
//不然就跳转到登录
|
||||
Message.warning("请重新登录!");
|
||||
window.location.href = Vue.prototype.$loginUrl;
|
||||
}
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
})</code></pre>
|
||||
<ul>
|
||||
<li>afterEach (跳转之后滚动条回到顶部)</li>
|
||||
</ul>
|
||||
<pre><code class="language-javascript" lang="javascript">router.afterEach((to, from) => {
|
||||
// 跳转之后滚动条回到顶部
|
||||
window.scrollTo(0,0);
|
||||
});</code></pre>
|
||||
<ol start="2">
|
||||
<li>单个路由独享钩子</li>
|
||||
</ol>
|
||||
<p><strong>beforeEnter</strong>
|
||||
如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next</p>
|
||||
<pre><code class="language-javascript" lang="javascript">export default [
|
||||
{
|
||||
path: '/',
|
||||
name: 'login',
|
||||
component: login,
|
||||
beforeEnter: (to, from, next) => {
|
||||
console.log('即将进入登录页面')
|
||||
next()
|
||||
}
|
||||
}
|
||||
]</code></pre>
|
||||
<ol start="3">
|
||||
<li>组件内钩子</li>
|
||||
</ol>
|
||||
<p>beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave</p>
|
||||
<p>这三个钩子都有三个参数∶to、from、next</p>
|
||||
<ul>
|
||||
<li>beforeRouteEnter∶ 进入组件前触发</li>
|
||||
<li>beforeRouteUpdate∶ 当前地址改变并且改组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foa组件,这个钩子在这种情况下就会被调用</li>
|
||||
<li>beforeRouteLeave∶ 离开组件被调用</li>
|
||||
</ul>
|
||||
<p>注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">beforeRouteEnter(to, from, next) {
|
||||
next(target => {
|
||||
if (from.path == '/classProcess') {
|
||||
target.isFromProcess = true
|
||||
}
|
||||
})
|
||||
}</code></pre>
|
||||
<p>二、Vue路由钩子在生命周期函数的体现</p>
|
||||
<ol>
|
||||
<li>完整的路由导航解析流程(不包括其他生命周期)</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>触发进入其他路由。</li>
|
||||
<li>调用要离开路由的组件守卫beforeRouteLeave</li>
|
||||
<li>调用局前置守卫∶ beforeEach</li>
|
||||
<li>在重用的组件里调用 beforeRouteUpdate</li>
|
||||
<li>调用路由独享守卫 beforeEnter。</li>
|
||||
<li>解析异步路由组件。</li>
|
||||
<li>在将要进入的路由组件中调用 beforeRouteEnter</li>
|
||||
<li>调用全局解析守卫 beforeResolve</li>
|
||||
<li>导航被确认。</li>
|
||||
<li>调用全局后置钩子的 afterEach 钩子。</li>
|
||||
<li>触发DOM更新(mounted)。</li>
|
||||
<li>执行beforeRouteEnter 守卫中传给 next 的回调函数</li>
|
||||
</ul>
|
||||
<ol start="2">
|
||||
<li>触发钩子的完整顺序</li>
|
||||
</ol>
|
||||
<p>路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶</p>
|
||||
<ul>
|
||||
<li>beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。</li>
|
||||
<li>beforeEach:路由全局前置守卫,可用于登录验证、全局路由loading等。</li>
|
||||
<li>beforeEnter:路由独享守卫</li>
|
||||
<li>beforeRouteEnter:路由组件的组件进入路由前钩子。</li>
|
||||
<li>beforeResolve:路由全局解析守卫</li>
|
||||
<li>afterEach:路由全局后置钩子</li>
|
||||
<li>beforeCreate:组件生命周期,不能访问tAis。</li>
|
||||
<li>created;组件生命周期,可以访问tAis,不能访问dom。</li>
|
||||
<li>beforeMount:组件生命周期</li>
|
||||
<li>deactivated:离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。</li>
|
||||
<li>mounted:访问/操作dom。</li>
|
||||
<li>activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。</li>
|
||||
<li>执行beforeRouteEnter回调函数next。</li>
|
||||
</ul>
|
||||
<ol start="3">
|
||||
<li>导航行为被触发到导航完成的整个过程</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>导航行为被触发,此时导航未被确认。</li>
|
||||
<li>在失活的组件里调用离开守卫 beforeRouteLeave。</li>
|
||||
<li>调用全局的 beforeEach守卫。</li>
|
||||
<li>在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。</li>
|
||||
<li>在路由配置里调用 beforeEnteY。</li>
|
||||
<li>解析异步路由组件(如果有)。</li>
|
||||
<li>在被激活的组件里调用 beforeRouteEnter。</li>
|
||||
<li>调用全局的 beforeResolve 守卫(2.5+),标示解析阶段完成。</li>
|
||||
<li>导航被确认。</li>
|
||||
<li>调用全局的 afterEach 钩子。</li>
|
||||
<li>非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted</li>
|
||||
<li>触发 DOM 更新。</li>
|
||||
<li>用创建好的实例调用 beforeRouteEnter守卫中传给 next 的回调函数。</li>
|
||||
<li>导航完成</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-11" id="vue-part-2-point-7">7. Vue-router跳转和location.href有什么区别</h3>
|
||||
<ul>
|
||||
<li>使用 <code>location.href= /url </code>来跳转,简单方便,但是刷新了页面;</li>
|
||||
<li>使用 <code>history.pushState( /url )</code> ,无刷新页面,静态跳转;</li>
|
||||
<li>引进 router ,然后使用 <code>router.push( /url )</code> 来跳转,使用了 <code>diff</code> 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 <code>history.pushState()</code> 没什么差别的,因为vue-router就是用了 <code>history.pushState()</code> ,尤其是在history模式下。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-12" id="vue-part-2-point-8">8. params和query的区别</h3>
|
||||
<p><strong>用法</strong>:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 <code>this.$route.query.name</code> 和 <code>this.$route.params.name</code> 。</p>
|
||||
<p><strong>url地址显示</strong>:query更加类似于ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示</p>
|
||||
<p><strong>注意</strong>:query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据。</p>
|
||||
<h3 data-id="heading-13" id="vue-part-2-point-9">9. Vue-router 导航守卫有哪些</h3>
|
||||
<ul>
|
||||
<li>全局前置/钩子:beforeEach、beforeResolve、afterEach</li>
|
||||
<li>路由独享的守卫:beforeEnter</li>
|
||||
<li>组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-14" id="vue-part-2-point-10">10. 对前端路由的理解</h3>
|
||||
<p>在前端技术早期,一个 url 对应一个页面,如果要从 A 页面切换到 B 页面,那么必然伴随着页面的刷新。这个体验并不好,不过在最初也是无奈之举——用户只有在刷新页面的情况下,才可以重新去请求数据。</p>
|
||||
<p>后来,改变发生了——Ajax 出现了,它允许人们在不刷新页面的情况下发起请求;与之共生的,还有“不刷新页面即可更新页面内容”这种需求。在这样的背景下,出现了 <strong>SPA(单页面应用</strong>)。</p>
|
||||
<p>SPA极大地提升了用户体验,它允许页面在不刷新的情况下更新页面内容,使内容的切换更加流畅。但是在 SPA 诞生之初,人们并没有考虑到“定位”这个问题——在内容切换前后,页面的 URL 都是一样的,这就带来了两个问题:</p>
|
||||
<ul>
|
||||
<li>SPA 其实并不知道当前的页面“进展到了哪一步”。可能在一个站点下经过了反复的“前进”才终于唤出了某一块内容,但是此时只要刷新一下页面,一切就会被清零,必须重复之前的操作、才可以重新对内容进行定位——SPA 并不会“记住”你的操作。</li>
|
||||
<li>由于有且仅有一个 URL 给页面做映射,这对 SEO 也不够友好,搜索引擎无法收集全面的信息</li>
|
||||
</ul>
|
||||
<p>为了解决这个问题,前端路由出现了。</p>
|
||||
<p>前端路由可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步——为 SPA 中的各个视图匹配一个唯一标识。这意味着用户前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便他刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。</p>
|
||||
<p>那么如何实现这个目的呢?首先要解决两个问题:</p>
|
||||
<ul>
|
||||
<li>当用户刷新页面时,浏览器会默认根据当前 URL 对资源进行重新定位(发送请求)。这个动作对 SPA 是不必要的,因为我们的 SPA 作为单页面,无论如何也只会有一个资源与之对应。此时若走正常的请求-刷新流程,反而会使用户的前进后退操作无法被记录。</li>
|
||||
<li>单页面应用对服务端来说,就是一个URL、一套资源,那么如何做到用“不同的URL”来映射不同的视图内容呢?</li>
|
||||
</ul>
|
||||
<p>从这两个问题来看,服务端已经完全救不了这个场景了。所以要靠咱们前端自力更生,不然怎么叫“前端路由”呢?作为前端,可以提供这样的解决思路:</p>
|
||||
<ul>
|
||||
<li>拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化掉。</li>
|
||||
<li>感知 URL 的变化。这里不是说要改造 URL、凭空制造出 N 个 URL 来。而是说 URL 还是那个 URL,只不过我们可以给它做一些微小的处理——这些处理并不会影响 URL 本身的性质,不会影响服务器对它的识别,只有我们前端感知的到。一旦我们感知到了,我们就根据这些变化、用 JS 去给它生成不同的内容。</li>
|
||||
</ul>
|
||||
<h2 data-id="heading-15">五、Vuex</h2>
|
||||
<h3 data-id="heading-16" id="vue-part-2-point-11">1. Vuex 的原理</h3>
|
||||
<p>Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。</p>
|
||||
<ul>
|
||||
<li>Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。</li>
|
||||
<li>改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。</li>
|
||||
</ul>
|
||||
<p><img src="/articles/vue-part-2/vue-part-2-figure-1.awebp" alt="b025e120ca3d0bd2ded3d038d58cacf4.jpg" loading="lazy">
|
||||
Vuex为Vue Components建立起了一个完整的生态圈,包括开发中的API调用一环。
|
||||
<strong>(1)核心流程中的主要功能:</strong></p>
|
||||
<ul>
|
||||
<li>Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;</li>
|
||||
<li>在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;</li>
|
||||
<li>然后 Mutations 就去改变(Mutate)State 中的数据;</li>
|
||||
<li>当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。</li>
|
||||
</ul>
|
||||
<p><strong>(2)各模块在核心流程中的主要功能:</strong></p>
|
||||
<ul>
|
||||
<li><code>Vue Components</code>∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。</li>
|
||||
<li><code>dispatch</code>∶操作行为触发方法,是唯一能执行action的方法。</li>
|
||||
<li><code>actions</code>∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。</li>
|
||||
<li><code>commit</code>∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。</li>
|
||||
<li><code>mutations</code>∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。</li>
|
||||
<li><code>state</code>∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。</li>
|
||||
<li><code>getters</code>∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-17" id="vue-part-2-point-12">2. Vuex中action和mutation的区别</h3>
|
||||
<p>mutation中的操作是一系列的同步函数,用于修改state中的变量的的状态。当使用vuex时需要通过commit来提交需要操作的内容。mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是实际进行状态更改的地方,并且它会接受 state 作为第一个参数:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">const store = new Vuex.Store({
|
||||
state: {
|
||||
count: 1
|
||||
},
|
||||
mutations: {
|
||||
increment (state) {
|
||||
state.count++ // 变更状态
|
||||
}
|
||||
}
|
||||
})</code></pre>
|
||||
<p>当触发一个类型为 increment 的 mutation 时,需要调用此函数:</p>
|
||||
<pre><code class="language-javascript" lang="javascript">store.commit('increment')</code></pre>
|
||||
<p>而Action类似于mutation,不同点在于:</p>
|
||||
<ul>
|
||||
<li>Action 可以包含任意异步操作。</li>
|
||||
<li>Action 提交的是 mutation,而不是直接变更状态。</li>
|
||||
</ul>
|
||||
<pre><code class="language-javascript" lang="javascript">const store = new Vuex.Store({
|
||||
state: {
|
||||
count: 0
|
||||
},
|
||||
mutations: {
|
||||
increment (state) {
|
||||
state.count++
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
increment (context) {
|
||||
context.commit('increment')
|
||||
}
|
||||
}
|
||||
})</code></pre>
|
||||
<p>Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。
|
||||
所以,两者的不同点如下:</p>
|
||||
<ul>
|
||||
<li>Mutation专注于修改State,理论上是修改State的唯一途径;Action业务代码、异步请求。</li>
|
||||
<li>Mutation:必须同步执行;Action:可以异步,但不能直接操作State。</li>
|
||||
<li>在视图更新时,先触发actions,actions再触发mutation</li>
|
||||
<li>mutation的参数是state,它包含store中的数据;store的参数是context,它是 state 的父级,包含 state、getters</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-18" id="vue-part-2-point-13">3. Vuex 和 localStorage 的区别</h3>
|
||||
<p><strong>(1)最重要的区别</strong></p>
|
||||
<ul>
|
||||
<li>vuex存储在内存中</li>
|
||||
<li>localstorage 则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要 JSON的stringify和parse方法进行处理。 读取内存比读取硬盘速度要快</li>
|
||||
</ul>
|
||||
<p><strong>(2)应用场景</strong></p>
|
||||
<ul>
|
||||
<li>Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex用于组件之间的传值。</li>
|
||||
<li>localstorage是本地存储,是将数据存储到浏览器的方法,一般是在跨页面传递数据时使用 。</li>
|
||||
<li>Vuex能做到数据的响应式,localstorage不能</li>
|
||||
</ul>
|
||||
<p><strong>(3)永久性</strong></p>
|
||||
<p>刷新页面时vuex存储的值会丢失,localstorage不会。</p>
|
||||
<p><strong>注意:</strong> 对于不变的数据确实可以用localstorage可以代替vuex,但是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件改变了该数据源,希望另一个组件响应该变化时,localstorage无法做到,原因就是区别1。</p>
|
||||
<h3 data-id="heading-19" id="vue-part-2-point-14">4. Redux 和 Vuex 有什么区别,它们的共同思想</h3>
|
||||
<p><strong>(1)Redux 和 Vuex区别</strong></p>
|
||||
<ul>
|
||||
<li>Vuex改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,无需switch,只需在对应的mutation函数里改变state值即可</li>
|
||||
<li>Vuex由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可</li>
|
||||
<li>Vuex数据流的顺序是∶View调用store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)</li>
|
||||
</ul>
|
||||
<p>通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;</p>
|
||||
<p><strong>(2)共同思想</strong></p>
|
||||
<ul>
|
||||
<li>单—的数据源</li>
|
||||
<li>变化可以预测</li>
|
||||
</ul>
|
||||
<p>本质上:redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案;
|
||||
形式上:vuex借鉴了redux,将store作为全局的数据中心,进行mode管理;</p>
|
||||
<h3 data-id="heading-20" id="vue-part-2-point-15">5. 为什么要用 Vuex 或者 Redux</h3>
|
||||
<p>由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致代码无法维护。</p>
|
||||
<p>所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为。</p>
|
||||
<p>另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,代码将会变得更结构化且易维护。</p>
|
||||
<h3 data-id="heading-21" id="vue-part-2-point-16">6. Vuex有哪几种属性?</h3>
|
||||
<p>有五种,分别是 State、 Getter、Mutation 、Action、 Module</p>
|
||||
<ul>
|
||||
<li>state => 基本数据(数据源存放地)</li>
|
||||
<li>getters => 从基本数据派生出来的数据</li>
|
||||
<li>mutations => 提交更改数据的方法,同步</li>
|
||||
<li>actions => 像一个装饰器,包裹mutations,使之可以异步。</li>
|
||||
<li>modules => 模块化Vuex</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-22" id="vue-part-2-point-17">7. Vuex和单纯的全局对象有什么区别?</h3>
|
||||
<ul>
|
||||
<li>Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。</li>
|
||||
<li>不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-23" id="vue-part-2-point-18">8. 为什么 Vuex 的 mutation 中不能做异步操作?</h3>
|
||||
<ul>
|
||||
<li>Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。</li>
|
||||
<li>每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-24" id="vue-part-2-point-19">9. Vuex的严格模式是什么,有什么作用,如何开启?</h3>
|
||||
<p>在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。</p>
|
||||
<p>在Vuex.Store 构造器选项中开启,如下</p>
|
||||
<pre><code class="language-php" lang="php">const store = new Vuex.Store({
|
||||
strict:true,
|
||||
})</code></pre>
|
||||
<h3 data-id="heading-25" id="vue-part-2-point-20">10. 如何在组件中批量使用Vuex的getter属性</h3>
|
||||
<p>使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中</p>
|
||||
<pre><code class="language-javascript" lang="javascript">import {mapGetters} from 'vuex'
|
||||
export default{
|
||||
computed:{
|
||||
...mapGetters(['total','discountTotal'])
|
||||
}
|
||||
}</code></pre>
|
||||
<h3 data-id="heading-26" id="vue-part-2-point-21">11. 如何在组件中重复使用Vuex的mutation</h3>
|
||||
<p>使用mapMutations辅助函数,在组件中这么使用</p>
|
||||
<pre><code class="language-javascript" lang="javascript">import { mapMutations } from 'vuex'
|
||||
methods:{
|
||||
...mapMutations({
|
||||
setNumber:'SET_NUMBER',
|
||||
})
|
||||
}</code></pre>
|
||||
<p>然后调用<code>this.setNumber(10)</code>相当调用<code>this.$store.commit('SET_NUMBER',10)</code></p>
|
||||
<h2 data-id="heading-27">六、Vue 3.0</h2>
|
||||
<h3 data-id="heading-28" id="vue-part-2-point-22">1. Vue3.0有什么更新</h3>
|
||||
<p><strong>(1)监测机制的改变</strong></p>
|
||||
<ul>
|
||||
<li>3.0 将带来基于代理 Proxy的 observer 实现,提供全语言覆盖的反应性跟踪。</li>
|
||||
<li>消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:</li>
|
||||
</ul>
|
||||
<p><strong>(2)只能监测属性,不能监测对象</strong></p>
|
||||
<ul>
|
||||
<li>检测属性的添加和删除;</li>
|
||||
<li>检测数组索引和长度的变更;</li>
|
||||
<li>支持 Map、Set、WeakMap 和 WeakSet。</li>
|
||||
</ul>
|
||||
<p><strong>(3)模板</strong></p>
|
||||
<ul>
|
||||
<li>作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。</li>
|
||||
<li>同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。</li>
|
||||
</ul>
|
||||
<p><strong>(4)对象式的组件声明方式</strong></p>
|
||||
<ul>
|
||||
<li>vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。</li>
|
||||
<li>3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易</li>
|
||||
</ul>
|
||||
<p><strong>(5)其它方面的更改</strong></p>
|
||||
<ul>
|
||||
<li>支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。</li>
|
||||
<li>支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。</li>
|
||||
<li>基于 tree shaking 优化,提供了更多的内置功能。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-29" id="vue-part-2-point-23">2. defineProperty和proxy的区别</h3>
|
||||
<p>Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。</p>
|
||||
<p>Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。</p>
|
||||
<p>但是这样做有以下问题:</p>
|
||||
<ol>
|
||||
<li>添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过<code>$set</code> 来调用<code>Object.defineProperty()</code>处理。</li>
|
||||
<li>无法监控到数组下标和长度的变化。</li>
|
||||
</ol>
|
||||
<p>Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于<code>Object.defineProperty()</code>,其有以下特点:</p>
|
||||
<ol>
|
||||
<li>Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。</li>
|
||||
<li>Proxy 可以监听数组的变化。</li>
|
||||
</ol>
|
||||
<h3 data-id="heading-30" id="vue-part-2-point-24">3. Vue3.0 为什么要用 proxy?</h3>
|
||||
<p>在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶</p>
|
||||
<ul>
|
||||
<li>不需用使用 <code>Vue.$set</code> 或 <code>Vue.$delete</code> 触发响应式。</li>
|
||||
<li>全方位的数组变化检测,消除了Vue2 无效的边界情况。</li>
|
||||
<li>支持 Map,Set,WeakMap 和 WeakSet。</li>
|
||||
</ul>
|
||||
<p>Proxy 实现的响应式原理与 Vue2的实现原理相同,实现方式大同小异∶</p>
|
||||
<ul>
|
||||
<li>get 收集依赖</li>
|
||||
<li>Set、delete 等触发依赖</li>
|
||||
<li>对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触发逻辑。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-31" id="vue-part-2-point-25">4. Vue 3.0 中的 Vue Composition API?</h3>
|
||||
<p>在 Vue2 中,代码是 Options API 风格的,也就是通过填充 (option) data、methods、computed 等属性来完成一个 Vue 组件。这种风格使得 Vue 相对于 React极为容易上手,同时也造成了几个问题:</p>
|
||||
<ol>
|
||||
<li>由于 Options API 不够灵活的开发方式,使得Vue开发缺乏优雅的方法来在组件间共用代码。</li>
|
||||
<li>Vue 组件过于依赖<code>this</code>上下文,Vue 背后的一些小技巧使得 Vue 组件的开发看起来与 JavaScript 的开发原则相悖,比如在<code>methods</code> 中的<code>this</code>竟然指向组件实例来不指向<code>methods</code>所在的对象。这也使得 TypeScript 在Vue2 中很不好用。</li>
|
||||
</ol>
|
||||
<p>于是在 Vue3 中,舍弃了 Options API,转而投向 Composition API。Composition API本质上是将 Options API 背后的机制暴露给用户直接使用,这样用户就拥有了更多的灵活性,也使得 Vue3 更适合于 TypeScript 结合。</p>
|
||||
<p>如下,是一个使用了 Vue Composition API 的 Vue3 组件:</p>
|
||||
<pre><code class="language-javascript" lang="javascript"><template>
|
||||
<button @click="increment">
|
||||
Count: {{ count }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Composition API 将组件属性暴露为函数,因此第一步是导入所需的函数
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
// 使用 ref 函数声明了称为 count 的响应属性,对应于Vue2中的data函数
|
||||
const count = ref(0)
|
||||
|
||||
// Vue2中需要在methods option中声明的函数,现在直接声明
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
// 对应于Vue2中的mounted声明周期
|
||||
onMounted(() => console.log('component mounted!'))
|
||||
|
||||
return {
|
||||
count,
|
||||
increment
|
||||
}
|
||||
}
|
||||
}
|
||||
</script></code></pre>
|
||||
<p>显而易见,Vue Composition API 使得 Vue3 的开发风格更接近于原生 JavaScript,带给开发者更多地灵活性</p>
|
||||
<h3 data-id="heading-32" id="vue-part-2-point-26">5. Composition API与React Hook很像,区别是什么</h3>
|
||||
<p>从React Hook的实现角度看,React Hook是根据useState调用的顺序来确定下一次重渲染时的state是来源于哪个useState,所以出现了以下限制</p>
|
||||
<ul>
|
||||
<li>不能在循环、条件、嵌套函数中调用Hook</li>
|
||||
<li>必须确保总是在你的React函数的顶层调用Hook</li>
|
||||
<li>useEffect、useMemo等函数必须手动确定依赖关系</li>
|
||||
</ul>
|
||||
<p>而Composition API是基于Vue的响应式系统实现的,与React Hook的相比</p>
|
||||
<ul>
|
||||
<li>声明在setup函数内,一次组件实例化只调用一次setup,而React Hook每次重渲染都需要调用Hook,使得React的GC比Vue更有压力,性能也相对于Vue来说也较慢</li>
|
||||
<li>Compositon API的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用</li>
|
||||
<li>响应式系统自动实现了依赖收集,进而组件的部分的性能优化由Vue内部自己完成,而React Hook需要手动传入依赖,而且必须必须保证依赖的顺序,让useEffect、useMemo等函数正确的捕获依赖变量,否则会由于依赖不正确使得组件性能下降。</li>
|
||||
</ul>
|
||||
<p>虽然Compositon API看起来比React Hook好用,但是其设计思想也是借鉴React Hook的。</p>
|
||||
<h2 data-id="heading-33">七、虚拟DOM</h2>
|
||||
<h3 data-id="heading-34" id="vue-part-2-point-27">1. 对虚拟DOM的理解?</h3>
|
||||
<p>从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。</p>
|
||||
<p>虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。 在代码渲染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实DOM结构,最终渲染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。</p>
|
||||
<p>另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。</p>
|
||||
<h3 data-id="heading-35" id="vue-part-2-point-28">2. 虚拟DOM的解析过程</h3>
|
||||
<p>虚拟DOM的解析过程:</p>
|
||||
<ul>
|
||||
<li>首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。</li>
|
||||
<li>当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。</li>
|
||||
<li>最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-36" id="vue-part-2-point-29">3. 为什么要用虚拟DOM</h3>
|
||||
<p><strong>(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能</strong>
|
||||
看一下页面渲染的流程:<strong>解析HTML -> 生成DOM -> 生成 CSSOM -> Layout -> Paint -> Compiler</strong>
|
||||
下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗∶</p>
|
||||
<ul>
|
||||
<li>真实DOM∶ 生成HTML字符串+重建所有的DOM元素</li>
|
||||
<li>虚拟DOM∶ 生成vNode+ DOMDiff+必要的dom更新</li>
|
||||
</ul>
|
||||
<p>Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。
|
||||
<strong>(2)跨平台</strong>
|
||||
Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。</p>
|
||||
<h3 data-id="heading-37" id="vue-part-2-point-30">4. 虚拟DOM真的比真实DOM性能好吗</h3>
|
||||
<ul>
|
||||
<li>首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。</li>
|
||||
<li>正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-38" id="vue-part-2-point-31">5. DIFF算法的原理</h3>
|
||||
<p>在新老虚拟DOM对比时:</p>
|
||||
<ul>
|
||||
<li>首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换</li>
|
||||
<li>如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)</li>
|
||||
<li>比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。</li>
|
||||
<li>匹配时,找到相同的子节点,递归比较子节点</li>
|
||||
</ul>
|
||||
<p>在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。</p>
|
||||
<h3 data-id="heading-39" id="vue-part-2-point-32">6. Vue中key的作用</h3>
|
||||
<p>vue 中 key 值的作用可以分为两种情况来考虑:</p>
|
||||
<ul>
|
||||
<li>第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。</li>
|
||||
<li>第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。</li>
|
||||
</ul>
|
||||
<p>key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速</p>
|
||||
<ul>
|
||||
<li>更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。</li>
|
||||
<li>更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快</li>
|
||||
</ul>
|
||||
<h3 data-id="heading-40" id="vue-part-2-point-33">7. 为什么不建议用index作为key?</h3>
|
||||
<p>使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。</p>
|
||||
@@ -0,0 +1,79 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
type TocItem = {
|
||||
title: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
type TechArticle = {
|
||||
contentHtml: string;
|
||||
toc: TocItem[];
|
||||
};
|
||||
|
||||
const contentRoot = join(process.cwd(), "src", "content", "notes");
|
||||
|
||||
function decodeHtml(value: string) {
|
||||
return value
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function stripTags(value: string) {
|
||||
return decodeHtml(value.replace(/<[^>]+>/g, "")).trim();
|
||||
}
|
||||
|
||||
const headingPrefixPattern =
|
||||
/^(?:\s| )*(?:(?:第?[一二三四五六七八九十百千万零〇]+|\d+)\s*[、..)]|[((]\s*(?:[一二三四五六七八九十百千万零〇]+|\d+)\s*[))]|[①②③④⑤⑥⑦⑧⑨⑩])\s*/;
|
||||
|
||||
function normalizeHeadingTitle(value: string) {
|
||||
return value.replace(headingPrefixPattern, "").trim();
|
||||
}
|
||||
|
||||
function normalizeHeadingInnerHtml(value: string) {
|
||||
let stripped = false;
|
||||
|
||||
return value.replace(/(^|>)([^<]+)/g, (match, leadingToken: string, text: string) => {
|
||||
if (stripped) {
|
||||
return match;
|
||||
}
|
||||
|
||||
if (!text.replace(/(?:\s| )+/g, "")) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const nextText = text.replace(headingPrefixPattern, "");
|
||||
stripped = nextText !== text;
|
||||
|
||||
return `${leadingToken}${nextText}`;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeArticleHeadings(contentHtml: string) {
|
||||
return contentHtml.replace(
|
||||
/(<(h[2-5])\b[^>]*>)([\s\S]*?)(<\/\2>)/g,
|
||||
(_match, openTag: string, _tagName: string, innerHtml: string, closeTag: string) =>
|
||||
`${openTag}${normalizeHeadingInnerHtml(innerHtml)}${closeTag}`,
|
||||
);
|
||||
}
|
||||
|
||||
function getToc(contentHtml: string) {
|
||||
return [...contentHtml.matchAll(/<h3\b[^>]*id="([^"]+)"[^>]*>([\s\S]*?)<\/h3>/g)].map(
|
||||
([, id, title]) => ({
|
||||
title: normalizeHeadingTitle(stripTags(title)),
|
||||
href: `#${id}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function getTechArticle(relativePath: string): TechArticle {
|
||||
const contentHtml = normalizeArticleHeadings(readFileSync(join(contentRoot, relativePath), "utf8"));
|
||||
|
||||
return {
|
||||
contentHtml,
|
||||
toc: getToc(contentHtml),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user