布莱克的充电站

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

展开
无界-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"; 待补充.....
布莱克
2026-07-14 16:57架构
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
uni-app的生命周期
当运行一个 uni-app 项目时,实际上有两个"大脑"在同时工作: Vue 的生命周期:负责组件本身的创建、挂载、更新、销毁(管理数据、DOM/虚拟DOM)。 uni-app 的生命周期:负责页面的加载、显示、隐藏、卸载(管理应用/页面的状态,对应微信小程序的生命周期) uni-app 应用级 vs 页面级 生命周期 生命周期类型 所属文件 触发时机 执行次数 onLaunch App.vue 应用启动时 全局只执行1次 onShow App.vue 应用从后台进入前台 多次 onHide App.vue 应用从前台进入后台 多次 onLoad 页面 .vue 页面加载时 每个页面独立执行 onShow 页面 .vue 页面显示时 每个页面多次 onReady 页面 .vue 页面首次渲染完成 每个页面1次 onHide 页面 .vue 页面隐藏时 每个页面多次 onUnload 页面 .vue 页面卸载时 每个页面1次 应用级生命周期 就是监听“应用程序”这个整体,从生到死,以及从前台到后台的整个过程 onLaunch :应用的“出生”与“初始化” 这是应用启动时触发的第一个,也是 唯一一个只触发一次 的生命周期。 深入理解: 它不仅仅是代码执行的第一步,更是 整个应用环境准备的黄金窗口 。在 onLaunch 没有执行完毕前,你的页面其实还没有被真正渲染。这也是为什么常说,不要在 onLaunch 里做耗时太长的同步操作,否则用户会一直看到启动白屏。 适合做什么(重中之重): 全局配置获取:从服务器拉取应用的基础配置(如 API 地址、功能开关、主题色)。 全局登录态检查:检查本地存储中是否有 token,判断用户是“已登录”还是“未登录”。 全局数据初始化:初始化 Vuex/Pinia 中的全局状态,或者设置 globalData。 路由拦截:根据登录状态,决定是跳转到首页还是登录页(注意异步延迟跳转问题)。 埋点与统计:发送应用启动事件,记录启动来源(通过 options 参数获取)。 应用级的 onLaunch 中的异步接口请求(如 uni.request、uni.login 等) 不会阻塞 页面级的生命周期执行。页面级的 onLoad、onShow 会 立即执行 ,完全不管应用级的异步请求是否完成 onShow :应用的“前台”与“激活” 只要应用的状态从“后台”变为“前台”, onShow 就会被触发。 如何理解小程序的前台和后台 前台 (Foreground) 定义:用户正在使用你的小程序,能看见它的界面,并能与之交互。 对应生命周期:onShow (页面级和应用级都会触发)。此时,页面正在被用户看到和操作。 后台 (Background) 定义:用户没有在使用你的小程序,看不见它的界面,但它可能还活着,进程驻留在内存中。 对应生命周期:用户按下 Home 键或切换到其他 App 时,触发 App.onHide。小程序进入后台挂起状态。 适合做什么: 刷新页面数据:这是最核心的用途。比如用户从详情页返回列表页,你希望在列表页 onShow 时重新拉取列表数据,以显示最新的状态。 重新校验登录态:用户可能在后台切换了账号,或者 token 过期了,回到应用时可以重新校验一次。 恢复应用状态:比如恢复音视频的播放,或者重连 WebSocket。 场景值/参数处理:onShow 的参数里,可以获取到应用是通过什么方式被唤醒的(比如小程序码、分享卡片)。 onHide :应用的“后台”与“休眠” 只要有新的事件或界面覆盖了你的应用, onHide 就会触发。它是 onShow 的镜像。 深入理解: onHide 触发时,你的应用界面还在,但用户看不到了。此时,应用会进入一个“冻结”或“低优先级”状态,以节省系统资源。操作系统可能在内存紧张时,销毁处于后台的应用。 适合做什么: 暂停正在进行的操作:比如暂停背景音乐的播放、暂停视频、停止动画循环。 保存临时数据或草稿:用户可能在输入框里写了一半的内容,担心应用被系统回收导致数据丢失,可以在 onHide 时自动保存草稿到本地。 断开长连接:比如断开 WebSocket,以减少不必要的资源消耗。 记录应用使用时长:埋点统计时,可以记录 onHide 的时间点,从而计算出用户在本轮会话中的停留时长。 页面级生命周期: onLoad :页面创建时执行(只执行一次) 执行时机 :页面被创建时,在 beforeCreate 和 created 之间。 核心特点 : ✅ 能拿到页面参数(路由传递的参数) ✅ 只执行一次 ✅ 页面还未渲染,DOM 不存在 onShow :页面显示/切换前台时执行(可多次执行) 执行时机 : 页面首次加载时(onLoad 之后) 从其他页面返回时(navigateBack) 从后台切回前台时 从 tabBar 切换回来时 onReady :页面首次渲染完成时执行(只执行一次) 执行时机 :页面第一次渲染完成后,相当于 Vue 的 mounted 之后。 核心特点 : ✅ 可以安全地操作 DOM 和组件 ✅ 可以获取元素的位置、尺寸 ✅ 只执行一次 onHide :页面隐藏时执行(可多次执行) 执行时机 : 跳转到其他页面时 小程序切到后台时 关闭当前页面时(但 onUnload 也会触发) onUnload :页面销毁时执行(只执行一次) 执行时机 : 调用 uni.navigateBack() 返回上一页 调用 uni.redirectTo() 重定向 调用 uni.reLaunch() 重启 Tab 页切换(Tab 页不会被销毁,所以不会触发) uniapp相关跳转 跳转方法 当前页(A) 新页面(B) 能否返回 A A 的生命周期 B 的生命周期 navigateTo 保留,推入栈 新建 ✅ 能 onHide onLoad → onShow redirectTo 替换,移出栈 新建 ❌ 不能 onUnload onLoad → onShow navigateBack 不适用 (用于返回) 不适用 - 被返回的页面: onShow 当前页面: onUnload switchTab 保留,缓存 切换显示 ✅ 能(切换回) onHide 首次: onLoad → onShow ;再次:仅 onShow reLaunch 销毁所有 新建 ❌ 不能 A 及所有页面: onUnload onLoad → onShow
布莱克
2026-05-31 21:45Vue已编辑
Nuxt的水合
什么是水合? SSR 的静态 HTML 变成交互应用的过程 水合的作用? 恢复交互能力: // 服务端返回的只是死 HTML <div> <button>点击我</button> // ❌ 点了没反应 </div> // 水合后变成活应用 <div> <button @click="handleClick">点击我</button> // ✅ 可以交互 </div> 接管响应式数据: // 水合前:显示初始计数 0 <button>0</button> // 水合后:Vue/React 接管数据,点击可以增加 const count = ref(0) // 恢复到 0 <button @click="count++">{{ count }}</button> 同步组件状态: // 服务端创建了某个状态 <div class="user-login">欢迎, 张三</div> // 水合时恢复对应的 JS 状态 const user = ref({ name: '张三' }) // 恢复状态 // 后续操作基于这个状态继续 Nuxt的渲染流程 Nuxt 完整保留了 Vue 的所有生命周期钩子(如 beforeCreate 、 mounted 等),但关键在于 它们运行的环境不同 : 服务端:beforeCreate 和 created 会在 Node.js 环境中执行。这意味着在这里不能使用 window、document 等浏览器特有的 API。 客户端:beforeMount 和 mounted 只在浏览器中执行,因此需要操作 DOM 或访问浏览器 API 的代码,都应该放在这里。从 mounted 开始,之后的钩子就只运行在客户端了。 Nuxt 特有的生命周期钩子(在服务端运行): 钩子 运行环境 主要作用 nuxtServerInit 服务端 在服务端渲染前,用于初始化 Vuex 状态,例如将服务端获取的用户信息存入 store。 middleware 服务端/客户端 在进入页面或布局之前执行,常用于路由验证、重定向等。 validate() 服务端 校验动态路由参数是否有效,若无效可返回 404 页面。 asyncData 服务端/客户端 最重要 的钩子之一,用于在页面组件加载前异步获取数据,并将返回值合并到组件的数据中。它在服务端执行后,会在客户端水合前再次执行。 fetch 服务端/客户端 用于在页面渲染前填充 Vuex store 的状态。 渲染流程及水合位置: 第一步:浏览器请求 → Node.js 服务端 当用户首次访问你的 Nuxt 应用时,请求会先到达 Node.js 服务器。服务器会执行一系列操作,包括运行 nuxtServerInit 、 middleware 、以及页面组件中的 asyncData 和 fetch 函数来获取所有需要的数据。 第二步:服务端 → 完全静态的 HTML 服务器利用获取到的数据,将 Vue 组件“渲染”成一个完整的 HTML 字符串,然后作为响应发送回浏览器。这时,用户能 立即看到 页面的完整内容,这就是 SSR 带来的首屏加载优势。 第三步:浏览器接收 → 水合 (Hydration) 加载 JS:浏览器接收到 HTML 后,会继续下载页面所需的 JavaScript 文件。 执行水合 (Hydration):这是最关键的一步。当所有 JS 加载完毕,Vue 会接管页面的静态 HTML。它会在后台重新执行一遍组件逻辑(比如重新初始化 data),并将事件监听器绑定到已有的 HTML 元素上。 应用激活:水合完成后,原本静态的 HTML 就变成了一个动态的、可交互的 Vue 应用。页面上的按钮可以点击了,数据也可以响应式更新了。 水合是连接“服务端渲染的静态页面”和“客户端动态应用”的桥梁,它发生在浏览器下载并执行完所有必要的 JS 代码之后 水合过程中,最大的坑就是 服务端生成的 HTML 和客户端 Vue 期望生成的 HTML 结构不一致 。这会导致 Vue 被迫放弃服务端渲染的 DOM 树,重新在客户端渲染,引发性能问题、交互失效,甚至页面闪烁 为什么需要延迟水合? 全量水合的问题: // 传统方式:所有组件一起水合 页面加载 (0ms) ↓ 下载所有 JS (300ms) ↓ 同时水合 7 个组件 (500ms) ← 主线程完全阻塞 ↓ 页面可交互 (800ms) ❌ 用户需要等待 800ms 才能点击按钮 主线程阻塞:水合是 CPU 密集型操作,全部同时做会卡死页面 浪费资源:用户可能根本看不到页脚,却要为它执行水合 TTI 时间长:Time to Interactive 很慢,用户以为页面卡死了 移动端更糟:低端设备水合比如 7 个组件可能需要 2-3 秒 水合过程: // 服务端返回的静态 HTML <div id="app"> <button class="counter">0</button> </div> // 水合过程(简化版) function hydrate() { // 1. 遍历整个 DOM 树(CPU 密集) const buttons = document.querySelectorAll('button') // 2. 为每个元素绑定事件(CPU 密集) buttons.forEach(button => { // 3. 解析指令、建立响应式连接(CPU 密集) button.addEventListener('click', () => { // 更新数据、重新渲染... }) // 4. 恢复组件状态(CPU 密集) const initialValue = button.textContent // 建立 Vue 的响应式系统... }) } 什么时候不需要水合 小型应用:组件少于 5 个,水合成本低 首屏简单:没有复杂交互组件
布莱克
2026-05-25 17:49Nuxt已编辑
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 10
assistant