布莱克的充电站

世界上只有一种真正的英雄主义,就是在认清生活的真相之后依然热爱生活——罗曼·罗兰

展开
前端部署后的版本检测与自动更新
问题现象: SPA 应用部署后,已打开页面的用户不会自动加载新版本 部署前(用户当前运行版本 v1) ├── index.html # 引用 _nuxt/abc123.js └── _nuxt/ └── abc123.js 部署后(服务器版本 v2) ├── index.html # 引用 _nuxt/def789.js └── _nuxt/ ├── abc123.js # 已删除 └── def789.js 技术方案对比 方案 实时性 实现成本 服务器开销 适用场景 WebSocket 推送 毫秒级 高 需维护长连接 实时协作、IM SSE 毫秒级 中 需服务端支持 通知、监控面板 轮询 0-5min 低 极低 内容型网站、后台管理 我的方案选择是轮询,成本较低,且五分钟的延迟对用户体验影响较小 ┌─────────────────────────────────────────────────────────────────┐ │ 构建阶段 │ │ ┌─────────────┐ ┌─────────────────────────────────────┐ │ │ │ 生成版本号 │ → │ 写入 build-version.json + 注入到 │ │ │ │ (Git SHA) │ │ 运行时配置 (appBuildVersion) │ │ │ └─────────────┘ └─────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 部署阶段 │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Nginx 配置: 静态资源长缓存 + JSON 不缓存 + 备份清理 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ 运行阶段 │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 立即检测一次 │ → │ 每 5 分钟轮询 │ → │ 版本不一致 │ │ │ │ │ │ │ │ 弹窗通知刷新 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ ↑ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ visibilitychange 控制轮询启停 + 切回时立即检测 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ 模块职责 模块 职责 版本号生成 每次构建生成唯一标识 构建注入 写入 build-version.json ,注入运行时配置 Nginx 路由 静态资源服务 + 缓存策略控制 前端检测插件 轮询、对比版本、通知、生命周期控制 版本号生成策略 //优先git命令 获取当前代码仓库最新提交的短哈希值 //时间戳兜底 function getAppBuildVersion() { try { return execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim() } catch { return String(Date.now()) } } 构建时写入version.json 以Nuxt项目为例,不同项目写法不同 build-version.json 必须放在 public 目录下 hooks: { compiled(nitro) { const publicDir = nitro.options.output.publicDir mkdirSync(publicDir, { recursive: true }) writeFileSync( join(publicDir, 'build-version.json'), JSON.stringify({ version: appBuildVersion }, null, 2) ) } } Nginx配置: # JSON 文件:禁止缓存(核心配置) location ~* \.json$ { root /var/nuxt-blog/.output/public; try_files $uri =404; access_log off; add_header Cache-Control "no-cache, no-store, must-revalidate"; } 核心检测逻辑: const check = async () => { if (!localVersion) return // ← 本地没有版本号,无法对比,直接退出 try { // 1. 请求服务器版本文件 const res = await fetch(`${VERSION_URL}?t=${Date.now()}`, { cache: 'no-store' // ← 强制不走浏览器缓存 }) if (!res.ok) return // ← 请求失败(404/500等),静默退出 const data = await res.json() // ← 解析 JSON // 2. 对比版本号 if (data?.version && data.version !== localVersion) { notify() // ← 不一致 → 弹窗! } } catch { // 捕获异常,静默处理,不打断用户阅读 } } 正常用户访问流程: 时间点 ① 用户首次打开页面(版本 v1) 服务器上有 build-version.json → { version: "v1" } 构建时注入到页面的 localVersion = "v1" 时间点 ② 用户发起 fetch(/build-version.json) 服务器返回 { version: "v1" } 对比:localVersion ("v1") === json.version ("v1") ✅ 一致 → 不弹窗 时间点 ③ 你部署了新版本 v2 服务器上的 build-version.json 变成了 { version: "v2" } ⚠️ 但用户浏览器里还跑着旧页面(没刷新) 时间点 ④ 用户页面里的 localVersion 仍然是构建时注入的旧值 "v1" (因为页面本身没刷新,JS 上下文没重建) 时间点 ⑤ 用户发起 fetch(/build-version.json) 服务器返回 { version: "v2" } 对比:localVersion ("v1") !== json.version ("v2") 🔔 不一致 → 弹窗! 核心流程编排: const start = () => { // 1. 立即执行一次检测 throttledCheck() // 2. 启动定时轮询 startPolling() // 3. 监听页面可见性变化 document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { startPolling() // ← 用户回来了,恢复轮询 throttledCheck() // ← 立即检测一次 return } stopPolling() // ← 用户切到后台,停止轮询 }) // 4. 用户聚焦页面时触发检测 window.addEventListener('focus', throttledCheck) } 行为 触发 作用 切到后台 stopPolling() 节省 CPU/电量 切回前台 startPolling() + throttledCheck() 恢复检测,立即查一次 点击页面 throttledCheck() 用户活跃时多一次检测
布莱克
2026-08-18 22:33部署已编辑
流水线自动部署
懒惰是第一生产力 ,本篇文章关于项目自动化部署,告别手动打包,上传至终端等等操作 作为一名前端开发者,你是否也经历过这样的场景: 本地开发完成,npm run build 打包 打开终端,scp -r ./.output root@xxx:/www 登录服务器,pm2 restart my-project 刷新页面,确认更新 每次上线都要重复这"四大步",虽然熟练了,但总归是机械劳动。如果一天上线好几次,时间就被这些重复操作吃掉了。 因为代码存储在Github,所以使用Github Actions实现自动部署 工具 用途 GitHub Actions CI/CD 自动化平台 Nuxt 3 前端框架 阿里云 ECS 服务器运行环境 PM2 Node.js 进程管理 SSH/SCP 远程连接与文件传输 什么是 GitHub Actions? 简单说,GitHub Actions 是 GitHub 内置的 CI/CD 工具。当你的代码仓库发生特定事件(如 push、PR 合并)时,它会在云端的虚拟机上自动执行你预设的任务。 你可以把它理解为一个"机器人助手": 你告诉它:"每当 main 分支有新代码,就帮我做这几件事" 它就会:拉代码 → 安装依赖 → 构建 → 上传服务器 → 重启服务 整个过程完全自动化,无需人工干预。 关于服务器的某些配置在此就不再赘述,安装pm2,创建项目目录等等 配置 GitHub Secrets 为了避免将服务器密码等敏感信息写在代码中,将它们存储在 GitHub Secrets 中。 操作步骤 进入 GitHub 仓库 → Settings → Secrets and variables → Actions 点击 New repository secret,依次添加: Secret Name 示例值 说明 HOST 1.2.3.4 服务器公网 IP USERNAME root SSH 登录用户名 PASSWORD ******** SSH 登录密码 编写 GitHub Actions 工作流 在项目根目录下创建 .github/workflows/deploy.yml : 该配置以 nuxt技术栈 项目为例,不同技术栈会有些许不同 # ============================================ # GitHub Actions 工作流配置文件 # 作用:当代码推送到 main 分支时,自动构建并部署 Nuxt 项目到阿里云 ECS # ============================================ # 工作流名称(显示在 GitHub Actions 页面) name: Deploy Nuxt to Aliyun ECS # ============================================ # 触发条件配置 # ============================================ on: push: branches: [ main ] # 只有推送到 main 分支时才触发 # ============================================ # 工作任务定义 # ============================================ jobs: # 任务名称:构建并部署 build-and-deploy: # 运行环境:使用 GitHub 提供的 Ubuntu 虚拟机 runs-on: ubuntu-latest # 指定环境(用于读取该环境下的 Secrets) environment: prod # ============================================ # 执行步骤(按顺序从上到下执行) # ============================================ steps: # ---------- 步骤 1:拉取代码 ---------- - name: Checkout code uses: actions/checkout@v4 # 作用:将 GitHub 仓库的代码下载到虚拟机上 # @v4 是版本号,表示使用第 4 个大版本 # ---------- 步骤 2:安装 Node.js ---------- - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' # 指定 Node.js 版本(改成你项目实际使用的版本) cache: 'npm' # 缓存 node_modules,加速后续构建 # 作用:在虚拟机上安装 Node.js 环境 # 为什么需要:虚拟机是干净的,没有 Node.js # ---------- 步骤 3:安装项目依赖 ---------- - name: Install dependencies run: npm ci # 作用:安装 package.json 中声明的所有依赖 # npm ci 特点: # - 比 npm install 更快 # - 严格按照 package-lock.json 安装,保证版本一致性 # - 专门为 CI/CD 环境设计 # 注意:这只是在 GitHub 虚拟机上安装依赖用于构建! # 服务器上不需要再安装,因为 .output 已自包含 # ---------- 步骤 4:构建 Nuxt 项目 ---------- - name: Build application run: npm run build # 作用:执行 package.json 中的 build 命令 # 对于 Nuxt 3:生成 .output 目录(自包含的构建产物) # 对于 Nuxt 2:生成 .nuxt 目录 # # ⚠️ 重要:Nuxt 3 的 .output 目录特点: # - 包含了所有编译后的代码 # - 包含了运行时需要的所有依赖 # - 是"自包含"的,不需要在服务器上再执行 npm install # - 直接可以用 node .output/server/index.mjs 启动 # ---------- 步骤 5:打包构建产物 ---------- - name: Package build artifacts run: tar -czf release.tgz .output # 作用:将 .output 文件夹压缩成 .tgz 文件 # 为什么压缩: # - 单个文件传输比成千上万个小文件快得多 # - 减少网络传输时间 # 参数说明: # - c:创建压缩包 (create) # - z:使用 gzip 压缩 # - f:指定文件名 (file) # # ⚠️ 注意:只打包 .output,不打包 node_modules # 因为 .output 已经自包含,不需要服务器端安装依赖 # ---------- 步骤 6:部署到阿里云 ECS ---------- - name: Deploy to Aliyun ECS # 从 GitHub Secrets 中读取服务器连接信息 env: HOST: ${{ secrets.HOST }} # 服务器公网 IP USERNAME: ${{ secrets.USERNAME }} # SSH 登录用户名 PASSWORD: ${{ secrets.PASSWORD }} # SSH 登录密码 run: | # -------------------------------------------- # 6.1 安装 sshpass 工具 # -------------------------------------------- sudo apt-get update sudo apt-get install -y sshpass # sshpass 作用:在命令行中自动输入 SSH 密码 # 为什么需要:scp 和 ssh 命令本身不支持 -p 密码参数 # 没有 sshpass 的话,每次都需要手动输入密码 # 在 CI/CD 中无法手动输入,所以需要自动认证 # -------------------------------------------- # 6.2 上传压缩包到服务器 # -------------------------------------------- echo "=== 1. 上传压缩包到服务器 ===" sshpass -p "$PASSWORD" scp \ -o StrictHostKeyChecking=no \ release.tgz $USERNAME@$HOST:/nuxt/project/ # 命令拆解: # sshpass -p "$PASSWORD" :用密码自动认证 # scp :Secure Copy,基于 SSH 的文件传输 # -o StrictHostKeyChecking=no:跳过"是否信任此主机"的询问 # release.tgz :要上传的本地文件 # root@1.2.3.4:/nuxt/project/:目标位置 # # ⚠️ 路径说明: # /nuxt/project/ 是你的项目部署目录 # 如果路径不同,修改成你服务器上的实际路径 # -------------------------------------------- # 6.3 在服务器上执行部署命令 # -------------------------------------------- echo "=== 2. 执行部署命令 ===" sshpass -p "$PASSWORD" ssh \ -o StrictHostKeyChecking=no \ $USERNAME@$HOST << 'EOF' # 上面的命令通过 SSH 连接到服务器 # << 'EOF' 表示之后的内容都在服务器上执行,直到遇到 EOF # ---------- 错误处理 ---------- set -e # 作用:任何命令失败(返回非 0 值),脚本立即退出 # 防止"部分失败"导致部署不完整 # ---------- 进入部署目录 ---------- cd /nuxt/project # 切换到项目部署目录 # ⚠️ 如果路径不同,请修改 # ---------- 显示当前状态 ---------- echo "📂 当前目录: $(pwd)" echo "📦 检查压缩包:" ls -lh release.tgz # 确认压缩包已成功上传 # ---------- 备份旧版本 ---------- echo "📦 备份旧版本..." if [ -d ".output" ]; then mv .output .output.backup.$(date +%Y%m%d_%H%M%S) fi # 作用:如果存在旧版本,先备份 # 为什么备份:如果新版本有问题,可以快速回滚 # 备份命名:.output.backup.20260817_120000(带时间戳) # ---------- 解压新版本 ---------- echo "📦 解压新版本..." tar -xzf release.tgz # 解压压缩包,生成 .output 目录 # # ⚠️ Nuxt 3 特别说明: # .output 目录是"自包含"的 # 不需要执行 npm install # 不需要执行 npm run build # 直接就可以运行 # ---------- 验证解压结果 ---------- echo "✅ 验证解压结果:" ls -la .output/server/index.mjs # 检查关键入口文件是否存在 # 如果文件不存在,ls 会报错,set -e 会停止执行 # ---------- 清理压缩包 ---------- echo "🧹 清理压缩包..." rm -f release.tgz # 删除压缩包,释放磁盘空间 # ---------- 重启 PM2 服务 ---------- echo "🔄 重启 PM2..." if pm2 list | grep -q "nuxt-blog"; then # 如果 PM2 中已有该应用,则重启 pm2 restart nuxt-blog else # 如果是首次部署,PM2 中还没有该应用,则启动 pm2 start .output/server/index.mjs --name nuxt-blog fi # pm2 list :列出所有 PM2 应用 # grep -q "nuxt-blog":静默搜索(不输出结果,只返回成功/失败) # # 为什么需要判断: # - 首次部署时,PM2 中没有该应用 # - restart 会报错 "Process not found" # - 所以先用 start,后续用 restart # ---------- 保存 PM2 配置 ---------- pm2 save # 作用:保存当前 PM2 进程列表 # 效果:服务器重启后,PM2 会自动恢复这些进程 # 相当于"开机自启"配置 # ---------- 显示最终状态 ---------- pm2 status # 显示所有 PM2 应用的运行状态 # 确认 nuxt-blog 是 online 状态 EOF # EOF 结束标记:以上都是在服务器上执行的命令 # -------------------------------------------- # 6.4 部署完成提示 # -------------------------------------------- echo "✅ 部署完成!" 触发部署 当你将代码推送到main分支时,一切自动发生 上述为大致总结,个人项目通过此操作可实现自动化部署~
布莱克
2026-08-17 22:29部署已编辑
无界-Wujie
微前端痛点与“无界”的诞生 痛点回首:传统微前端框架(如 qiankun)依赖 JS 劫持与运行时样式沙箱,带来了 Vite 适配困难(UMD 限制)、CSS 隔离不彻底、多页签无法无损保活等顽疾 wujie-vue 的魔力 :将复杂的微前端架构,封装成了一个简单的 Vue 组件 <WujieVue/>。引入它,就像引入一个普通的 Element-UI 组件一样自然。 无界之所以能做到“零改造成本”与“极速加载”,秘密在于它对浏览器原生特性的精妙组合: JS 沙箱(物理隔离):子应用运行在无 DOM 渲染的空 <iframe> 中。全局变量互不干扰,天然支持原生 ESM(Vite 项目无需任何降级打包)。 CSS 与 DOM 沙箱(WebComponent):子应用的 HTML/CSS 被渲染在 Shadow DOM 内。样式绝对隔离,主、子应用样式互不影响,且能完美同屏共存。 连接桥梁:无界底层重定向了 iframe 内部 JS 对 DOM 的操作,让子应用误以为自己运行在一个正常的浏览器页面中。 接入及使用 安装依赖 npm install wujie-vue3 页面使用 <template> <div class="container"> <WujieVue name="my-sub-app" url="http://localhost:8080/" //根据env环境动态调整url :sync="true" :alive="true" :props="{ theme: 'dark' }" /> </div> </template> ## js部分 import WujieVue from "wujie-vue3"; 运行模式: 1.保活模式 alive: true 官方定义:主应用将子应用的 alive 属性设为 true。 状态表现:子应用只进行一次渲染,切走时 DOM 节点和 JS 状态全部保留在内存中(仅对 WebComponent 进行热插拔),状态和路由不丢失。 无需改造:子应用不需要进行任何生命周期改造。 <template> <div class="app-container"> <WujieVue name="crm-sub-app" url="http://localhost:8001/" :alive="true" :sync="true" /> </div> </template> ⚠️ 性能提醒 :由于子应用的 DOM 树和 JS 内存常驻,保活的子应用数量过多(例如超过 5-8 个重型应用)会导致主应用的浏览器内存占用过大。建议对 高频、重度录入 的子应用开启此模式,其他低频应用不推荐。 2. 单例模式 alive: false+生命周期改造 官方定义:主应用 alive 为 false(默认),且子应用导出了生命周期(即改造了 window.__WUJIE_MOUNT 和 window.__WUJIE_UNMOUNT)。 状态表现:页面切走时,调用子应用的 __WUJIE_UNMOUNT 销毁当前实例(释放 DOM);页面切回时,调用 __WUJIE_MOUNT 渲染新的实例。 核心优势:因为承载子应用 JS 的 iframe 并没有被销毁,只是实例销毁了,所以二次打开时,JS 不需要重新下载和解析,挂载速度极快 <template> <div class="app-container"> <WujieVue name="erp-sub-app" url="http://localhost:8002/" :alive="false" :sync="true" /> </div> </template> 子应用需要导出 __WUJIE_MOUNT 和 __WUJIE_UNMOUNT 两个生命周期钩子,负责实例的挂载与销毁。 子应用配置: import { createApp, App as VueApp } from "vue"; import App from "./App.vue"; import router from "./router"; let app: VueApp | null = null; // 1. 封装挂载方法 function mount() { app = createApp(App); app.use(router); app.mount("#app"); } // 2. 封装销毁方法 function unmount() { app?.unmount(); app = null; } // 3. 接入无界生命周期 if (window.__POWERED_BY_WUJIE__) { window.__WUJIE_MOUNT = mount; window.__WUJIE_UNMOUNT = unmount; // 必须手动触发首次 mount window.__WUJIE?.mount(); } else { // 独立运行环境 mount(); } 3. 重建模式 alive: false+无生命周期改造 官方定义:主应用 alive 为 false,且子应用完全没有做任何生命周期改造。 状态表现:因为子应用没有提供销毁和挂载的生命周期钩子,无界无法精细化控制它。所以每次切走页面时,无界只能采用“暴力手段”——直接销毁子应用的 WebComponent + 销毁承载 JS 的整个 iframe;切回时,重新全量创建。 特点:真正的零改造,但每次切换都是“冷启动”,性能最差 ⚠️ 重建模式代价: 由于每次切走都会强制销毁并重新创建 <iframe>(包含里面的 JS 执行上下文、运行时变量),重新切回时需要重新加载 HTML/JS、重新进行 JS 词法解析、重新执行生命周期, 性能最差,白屏时间最长 。 待补充.....
布莱克
2026-07-16 09:53架构已编辑
rem适配原理
什么是 rem? rem = root em = 相对于根元素(html)字体大小的单位 /* 核心定义 */ html { font-size: 16px; /* 设置根字体大小 */ } /* 此时 */ 1rem = 16px 2rem = 32px 0.5rem = 8px 关键点 :rem 本身只是一个 CSS 单位,它 不会自动适配屏幕 计算机制: // rem 的最终渲染公式 实际px值 = rem值 × html元素的font-size值 // 例子 html { font-size: 20px; } .box { width: 10rem; } // 浏览器计算: 10 × 20 = 200px // 最终渲染: 200px 与 em 的区别 <style> /* em - 相对于父元素 */ .parent { font-size: 20px; /* 父级 */ } .child-em { font-size: 1.5em; /* 30px (相对于父级20px) */ padding: 1em; /* 30px (相对于自身font-size) */ } /* rem - 相对于根元素 */ html { font-size: 16px; /* 根元素 */ } .child-rem { font-size: 1.5rem; /* 24px (相对于html) */ padding: 1rem; /* 16px (始终相对于html) */ } </style> rem 适配屏幕的原理 改变 html 的 font-size ,所有使用 rem 的元素 等比例缩放 // 1. 初始状态 html { font-size: 16px; } .box { width: 10rem; } // 160px // 2. 改变根字体 html { font-size: 32px; } .box { width: 10rem; } // 320px (自动变大) // 3. 等比例缩放的核心 // 所有rem元素都会随着根字体变化而等比变化 实际适配逻辑: /* 一行代码解决适配 */ html { /* 公式: 屏幕宽度 / 设计稿宽度 * 基准值 */ /* 375px设计稿, 1rem = 10px(便于计算) */ font-size: calc(100vw / 37.5); } 使用插件或者手写px转rem逻辑: // 方案2:SCSS 函数手动转换 @function rem($px) { @return $px / 10 * 1rem; } .button { width: rem(300); height: rem(44); font-size: rem(16); } 关键点: rem 本身不自动适配,需要配合动态设置 html根元素 font-size 限制范围,避免极端屏幕下字体过大或过小 html { /* clamp(最小值, 理想值, 最大值) */ font-size: clamp(12px, calc(100vw / 37.5), 20px); } 边框建议保持 1px,不要等比例缩放
布莱克
2026-06-06 22:46css
HTTP发展历程
HTTP/1.0(1996) 特性 解决的问题 请求方法 (GET、POST、HEAD) 支持表单提交、获取元信息 状态码 (200、404、500 等) 客户端能判断请求结果 请求/响应头 Content-Type、Content-Length、User-Agent Cache 机制 (Expires、Pragma: no-cache) 减少重复请求 ❌ 核心问题:短连接 每个请求建立一次 TCP 连接,请求完成后立即断开 一个 HTML 里引用了 10 张图片 → 建立 11 次 TCP 连接 每次 TCP 连接都要三次握手 +(HTTPS 时)TLS 握手 连接数过多时,操作系统文件描述符耗尽 HTTP/1.1(1999) 1. 持久连接(Keep-Alive) Connection: keep-alive 一个 TCP 连接可以发送多个请求/响应 减少 TCP 握手和 TLS 握手次数 成为默认行为(HTTP/1.1 中默认开启) 2. 管道化(Pipelining) 客户端可以连续发送多个请求,无需等待每个响应返回 但服务器必须按顺序返回响应 实际使用中问题太多(队头阻塞、幂等性要求),浏览器默认关闭 3. Host 头 Host: www.example.com 一台服务器可以托管多个域名(虚拟主机) 这是今天云服务、CDN 的基础 4. 分块传输编码(Chunked Transfer Encoding) text Transfer-Encoding: chunked 服务器不知道 Content-Length 时,可以分块发送 流式传输、Server-Sent Events(SSE)的基础 5. 更强的缓存控制 Cache-Control(max-age、no-cache、no-store) ETag(实体标签,用于条件请求) If-None-Match、If-Modified-Since 6. 新增请求方法 PUT、DELETE、OPTIONS、TRACE、CONNECT HTTP1.1请求 页面加载 example.com(包含 20 个资源) 浏览器行为: ┌─────────────────────────────────────────────────────────────┐ │ 建立 6 个 TCP 连接到 example.com(连接池) │ └─────────────────────────────────────────────────────────────┘ 连接1(TCP端口 12345): 请求A → 响应A → 请求G → 响应G → 请求M → 响应M └─串行─┘ └─串行─┘ └─串行─┘ 连接2(TCP端口 12346): 请求B → 响应B → 请求H → 响应H → 请求N → 响应N └─串行─┘ └─串行─┘ └─串行─┘ 连接3(TCP端口 12347): 请求C → 响应C → 请求I → 响应I → 请求O → 响应O └─串行─┘ └─串行─┘ └─串行─┘ 连接4(TCP端口 12348): 请求D → 响应D → 请求J → 响应J → 请求P → 响应P └─串行─┘ └─串行─┘ └─串行─┘ 连接5(TCP端口 12349): 请求E → 响应E → 请求K → 响应K → 请求Q → 响应Q └─串行─┘ └─串行─┘ └─串行─┘ 连接6(TCP端口 12350): 请求F → 响应F → 请求L → 响应L → 请求R → 响应R └─串行─┘ └─串行─┘ └─串行─┘ 关键点: ✅ 6个连接之间是并行的(同时工作) ❌ 每个连接内部是串行的(必须等前一个响应回来,才能发下一个请求) HTTP/2(2015) 1. 二进制分帧层(Binary Framing Layer) ⭐ 将 HTTP 消息拆分为二进制帧(HEADERS 帧、DATA 帧、SETTINGS 帧等) 帧可以被乱序发送、乱序接收,最后按流 ID 重组 为多路复用奠定基础 2. 多路复用(Multiplexing) ⭐ 一个 TCP 连接上同时交错发送多个请求和响应 彻底解决了 HTTP/1.1 的应用层队头阻塞 无需域名分片,一个域名一个连接足够 3. 头部压缩(HPACK) ⭐ 静态字典:预定义 61 个常见头(:method: GET、:path: / 等) 动态字典:通信过程中双方共建的键值对表 Huffman 编码:压缩文本 头部体积减少 80%~90%,无安全漏洞(区别于 SPDY 的压缩方案) 4. 服务器推送(Server Push) 请求 HTML 时,服务器可主动推送 CSS、JS 减少往返延迟 ⚠️ 但容易过度推送,实际使用中谨慎 HTTP2请求 同一个 TCP 连接(只需要1个): 时间轴 → 请求A(Stream1) ────────────────────── 响应A(Stream1) ─────────> 请求B(Stream2) ──────────── 响应B(Stream2) ──────> 请求C(Stream3) ── 响应C(Stream3) ──> 请求D(Stream4) ───────────────────────────────── 响应D(Stream4) → 所有请求可以同时发送,响应可以乱序返回 因为每个帧都有 Stream ID 来标识属于哪个请求
布莱克
2026-06-03 16:27浏览器已编辑
垃圾回收机制(GC)
JS引擎通过 可达性 判断内存是否需要保留: 从根对象(Roots)出发,能访问到的对象标记为"存活",无法访问的即为"可回收" 这里的“根”可以理解为浏览器或 Node.js 中的全局对象,比如 window 、 globalThis ,或者正在执行的函数上下文 标记-清除(现代引擎核心) 原理 : 标记阶段:从根开始遍历,标记所有可达对象 清除阶段:回收未标记的内存 优点 :完美解决循环引用(互相引用但都不可达时,都不会被标记) V8引擎的进阶策略:分代回收 V8将内存堆分为两个区域,针对性优化: 新生代(Young Generation) 存放的是:临时变量、局部变量、短时间内用完就丢弃的对象 特点:存活时间短(局部变量、临时对象) 空间:1-20MB,小且频繁回收 算法:Scavenge(复制算法)内存分为From和To两个半区只复制存活对象到To区,清空From区速度极快(毫秒级),但浪费一半空间 晋升:存活超过2次GC的对象 → 移入老生代 老生代(Old Generation) 存放的是:全局变量、闭包变量、长期存活的对象 特点:存活时间长(全局变量、闭包、大型数组) 空间:大且回收成本高 算法:Mark-Compact(标记-整理)并发标记:后台线程标记,不阻塞主线程整理:移动存活对象,消除内存碎片清除:回收边界外内存 老生代 GC 的触发时机 触发条件1:老生代空间快满了 V8 为老生代设置了 内存阈值 ,当老生代中的对象数量超过这个阈值,就会触发一次 Major GC(全量垃圾回收) 。 触发条件2:新生代晋升失败 当新生代的对象要晋升到老生代,但老生代 空间不足 时,也会强制触发老生代 GC。 触发条件3:内存压力信号(操作系统/浏览器) 浏览器会监听系统内存状态: 操作系统内存不足时,浏览器会主动通知 V8 进行激进回收 页面切换到后台时,降低 GC 阈值,更积极地回收 Chrome 的 Tab 休眠机制:长时间不用的 Tab 会被冻结,触发 GC 内存泄漏: 内 存泄漏 :不再需要的内存,由于某种原因未被GC回收,导致内存占用持续增长 场景1:意外创建的全局变量 function leak() { // 没有 var/let/const,挂载到 window accidentalGlobal = new Array(1000000); // 函数结束,数组仍被全局引用 → 泄漏 } // 修复:使用严格模式 'use strict' 禁止隐式全局变量 场景2:被遗忘的定时器和回调 场景3:脱离DOM的引用 let cache = {}; function addElement() { const div = document.getElementById('container'); cache.div = div; // JS 持有 DOM 引用 div.remove(); // 从 DOM 树移除 // 但 cache.div 还在引用,DOM 节点无法回收 ❌ } // 修复:移除时同步清除引用 function fixedAddElement() { const div = document.getElementById('container'); cache.div = div; div.remove(); delete cache.div; // 或 cache.div = null } 场景4:闭包滥用 场景5:事件监听器未移除 场景6:Map/Set 无限增长 let userCache = new Map(); function cacheUser(id, data) { userCache.set(id, data); // 只增不减 } // 使用 WeakMap 自动回收 let userWeakCache = new WeakMap(); // 键为对象,弱引用
布莱克
2026-06-01 19:31JavaScript
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 10
assistant