阅读视图

第一次漂泊的一点点勇气

距离上次写文章已经将近三个月了。中间各种事情堆在一起,学校的课、每天跟着学校实习累成狗、办理各种离校手续,时间被压得一点不剩。好不容易空出几天,又重新做回了“~~网瘾少年~~”,结果博客自然也就落下了。现在回头想想,那段离校前后的日子,其实挺让人感慨的。 离校前的节奏完全是匆匆忙忙的,赶着把所有课程上完,赶着处理学校里所有该收尾的事,赶着收拾宿舍、打包四年的生活。直到真正站在校门口准备离开时,那种突然冒出来的不舍感,让我一下子意识到大学真的要结束了。不舍我的辅导员,不舍朋友和同学,不舍那些曾经天天见面的老师。大学四年就这么一晃而过,下次再见,不知道得是多久以后了。那种惬意的大学生活,真的只有离开的时候才会格外怀念。 离校之后,我坐了一整天火车来到了广州——一个我之前从来没想过自己会来的城市。对我来说,这里离家太远了,我在山西,而广州在地图上几乎是对角线的尽头。我想家,而家人则担心我一个人在外面生活得不好。刚到广州那天,把行李放到公司之后我就去找房子了,很快租下了一间小公寓。第一晚没枕头、没褥子、没被子,睡得凉飕飕的,但过几天东西都置办齐了,小屋也被我整理得干干净净。
图片
现在这个家虽然不大,却有种刚刚好的温度。楼下什么都有——餐馆、药店、快递驿站;再往前走一点就是商场和电影院;路上还能看到各种 coser。去公司也特别近,走路十几分钟,骑车七八分钟就到。老实说,这就是我梦寐以求的生活,以后换地方要是离公司不够近,我可能都会嫌弃。 刚来广州时还担心会不会水土不服,但住了几天就发现挺适合我。冬天的气候跟我家立秋差不多,白天甚至还要穿短袖。广州的饭也很对我胃口,尤其是我这种爱吃肉的人。隆江猪脚饭、煲仔饭、烧鸭、烧鹅、烧腊……每一样都能让我吃得很满足。来广州的某天下午,我还骑车去了广州塔。路是真的堵,又赶上下班高峰,我又不熟路,绕来绕去折腾了半天。不过好在最后还是打了卡,那天拍到的照片我还挺满意的。
图片
到了公司没几天,我就跟产品、UI、后端还有主管都熟络了。主管人很好,不是那种高压型领导,更像朋友一样。今天跟他聊天,他提到招人特别难,根本招不到。我当时还愣了一下,大家不是都说前端难找工作吗?那为什么企业又招不到人?聊着聊着,也算是得出一个共识:可能是很多求职者学得不够扎实,基础和技术栈都没准备到位。所以也想顺便分享一句感受:找工作的时候,真的要把基础打牢,把技术栈补全,准备越充分,选择就越多。 顺便说一句,公司配备的 AI 工具 Cursor 真是太好用啦,各种模型能随便切换还免费用,再也不用自己掏钱了😄,这大概是我入职后最快感受到“幸福感”的一件事。
图片
写到这里,其实心里多少有点感慨。从慢慢离开学校,到正式走向社会,这个过程像是人生的一个明显转折。不过每天上班路上,看着很多和我差不多年纪的人走在同一条街上,突然又觉得其实大家都一样,都是在为了自己的目标努力着。那一刻反而不觉得孤单了。 加油吧,各位。也加油吧,我自己。
  •  

前端存储技术的对比

  在前端开发中,我们经常需要在浏览器中存储数据。无论是保存用户的登录状态、记住用户的偏好设置,还是实现购物车功能,都离不开浏览器存储技术。目前主流的浏览器存储方案有三种:sessionStorage、localStorage 和 cookie。虽然它们都能在浏览器中存储数据,但各自有着不同的特性和适用场景。 ## 一、存储机制的本质差异 ### Cookie:HTTP 协议的产物   Cookie 最初是为了解决 HTTP 协议无状态特性而设计的。HTTP 协议本身是无状态的,这意味着服务器无法区分不同的请求是否来自同一个用户。Cookie 通过在客户端存储标识信息,并在每次请求时自动发送给服务器,从而解决了这个问题。   Cookie 的工作机制是这样的:当服务器需要记住用户状态时,会在响应头中设置 `Set-Cookie`,浏览器接收到这个响应后,会将 Cookie 信息存储在本地。之后,浏览器在向同一域名发送请求时,会自动在请求头中携带这些 Cookie 信息。 ### localStorage:HTML5 的本地存储解决方案   localStorage 是 HTML5 规范中引入的 Web Storage API 的一部分。它的设计目标是提供一个简单的键值对存储机制,让开发者可以在客户端持久化存储数据。与 Cookie 不同,localStorage 的数据不会自动发送给服务器,它纯粹是客户端的存储方案。   localStorage 的数据会一直保存在浏览器中,直到用户主动清除或者通过 JavaScript 代码删除。即使关闭浏览器、重启电脑,数据依然存在。 ### sessionStorage:会话级别的存储   sessionStorage 同样属于 HTML5 的 Web Storage API,但它与 localStorage 有着根本性的区别。sessionStorage 的数据只在当前浏览器标签页或窗口中有效,一旦标签页关闭,数据就会被清除。   这种设计使得 sessionStorage 非常适合存储一些临时的、会话级别的数据,比如表单的草稿、临时的用户操作状态等。 ## 二、详细特性对比 ### 存储容量限制 **Cookie**:每个 Cookie 的大小限制为 4KB,每个域名下最多可以存储 20 个 Cookie(不同浏览器可能有差异)。这个限制相对较小,主要原因是 Cookie 会在每次 HTTP 请求中传输,过大的 Cookie 会显著影响网络性能。 **localStorage**:通常可以存储 5-10MB 的数据(具体大小因浏览器而异)。这个容量对于大多数客户端存储需求来说是足够的。 **sessionStorage**:容量限制与 localStorage 相同,通常也是 5-10MB。 ### 数据生命周期 **Cookie**: - 可以设置过期时间(expires 或 max-age) - 如果不设置过期时间,则为会话 Cookie,关闭浏览器后失效 - 设置了过期时间的 Cookie 会一直存在,直到过期或被手动删除 **localStorage**: - 数据永久存储,除非用户手动清除或通过代码删除 - 不会因为关闭浏览器而丢失 - 不会因为重启电脑而丢失 **sessionStorage**: - 数据只在当前标签页有效 - 关闭标签页后数据立即清除 - 刷新页面不会清除数据 ### 数据访问方式 **Cookie**: ```javascript // 设置 Cookie document.cookie = "username=john; expires=Thu, 18 Dec 2024 12:00:00 UTC; path=/"; // 读取 Cookie function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); } // 删除 Cookie document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; ``` **localStorage**: ```javascript // 设置数据 localStorage.setItem('username', 'john'); // 读取数据 const username = localStorage.getItem('username'); // 删除数据 localStorage.removeItem('username'); // 清空所有数据 localStorage.clear(); ``` **sessionStorage**: ```javascript // 设置数据 sessionStorage.setItem('username', 'john'); // 读取数据 const username = sessionStorage.getItem('username'); // 删除数据 sessionStorage.removeItem('username'); // 清空所有数据 sessionStorage.clear(); ``` ### 作用域和安全性   Cookie 在作用域控制方面提供了最为精细的配置选项。通过 domain 属性,开发者可以控制 Cookie 在哪些域名下有效。比如,如果你设置 `domain=.example.com`,那么这个 Cookie 就可以在 `www.example.com`、`api.example.com` 等所有子域名下被访问。这种设计使得大型网站可以在不同子域名之间共享用户状态,比如让用户在购物网站和用户中心之间保持登录状态。   path 属性则进一步细化了 Cookie 的作用范围,它决定了 Cookie 在网站的哪些路径下有效。例如,设置 `path=/admin` 意味着这个 Cookie 只在 `/admin` 路径及其子路径下有效,这为不同功能模块提供了数据隔离的可能性。   在安全性方面,Cookie 提供了多层防护机制。secure 属性确保 Cookie 只在 HTTPS 连接下传输,这在当今网络安全要求越来越高的环境下显得尤为重要。httpOnly 属性则是一个重要的安全特性,它防止恶意 JavaScript 代码通过 `document.cookie` 访问敏感的 Cookie 数据,有效防止了 XSS(跨站脚本攻击)对用户认证信息的窃取。   sameSite 属性是近年来新增的重要安全特性,它有三种设置:Strict、Lax 和 None。Strict 模式最为严格,完全禁止跨站请求携带 Cookie,有效防止 CSRF(跨站请求伪造)攻击。Lax 模式则在保证安全性的同时提供了一定的灵活性,允许某些安全的跨站请求携带 Cookie。None 模式则允许所有跨站请求携带 Cookie,但必须配合 secure 属性使用。 ### localStorage 的作用域和安全性特点   localStorage 的作用域控制相对简单,它严格遵循浏览器的同源策略。这意味着只有来自相同协议、域名和端口的页面才能访问同一个 localStorage 存储空间。这种设计在保证数据安全的同时,也为不同网站之间的数据隔离提供了天然的屏障。   然而,localStorage 在安全性方面存在一些局限性。它没有像 Cookie 那样的丰富安全属性,所有的数据都以明文形式存储,任何能够访问该页面的 JavaScript 代码都可以读取和修改 localStorage 中的数据。这种特性使得 localStorage 不适合存储敏感信息,如用户密码、支付信息等。   localStorage 的另一个特点是数据共享范围。一旦数据被存储到 localStorage 中,同源的所有页面和标签页都可以访问这些数据。这种设计使得 localStorage 非常适合存储用户偏好设置、应用配置等需要在多个页面间共享的数据,但同时也意味着如果某个页面被恶意代码攻击,可能会影响到整个域名的数据安全。 ### sessionStorage 的作用域和安全性特征   sessionStorage 在作用域控制方面与 localStorage 类似,同样遵循同源策略,只有同源的页面才能访问。但它的独特之处在于数据隔离的粒度更细。sessionStorage 的数据只能在创建它的那个特定标签页或窗口中访问,即使是同一个域名的其他标签页也无法访问这些数据。   这种标签页级别的隔离设计使得 sessionStorage 在安全性方面具有独特的优势。即使某个标签页被恶意代码攻击,也不会影响到其他标签页的数据安全。这种特性使得 sessionStorage 非常适合存储临时性的敏感数据,比如表单的临时输入、临时的用户操作状态等。   不过,sessionStorage 同样缺乏额外的安全属性设置,数据以明文形式存储,任何能够访问该标签页的 JavaScript 代码都可以读取和修改数据。因此,虽然标签页级别的隔离提供了一定的安全保障,但在存储敏感信息时仍然需要谨慎考虑。   总的来说,这三种存储技术在作用域和安全性方面各有特点:Cookie 提供了最精细的作用域控制和最丰富的安全属性,但同时也带来了性能开销;localStorage 提供了简单的作用域控制和数据共享能力,但安全性相对较弱;sessionStorage 则在保证同源安全的基础上,提供了标签页级别的数据隔离,适合存储临时数据。在实际应用中,开发者需要根据数据的安全要求和共享需求来选择合适的存储方案。 ## 三、一些实际应用场景 ### Cookie 的典型应用场景 **用户认证和会话管理**:   Cookie 在用户认证和会话管理方面发挥着不可替代的作用。当用户成功登录后,服务器会生成一个唯一的认证令牌,这个令牌通过 Cookie 的方式存储在用户的浏览器中。由于 Cookie 会在每次 HTTP 请求中自动发送给服务器,服务器就能够通过这个令牌来识别用户的身份,无需用户重复登录。   这种机制的优势在于它的透明性和自动化。用户不需要手动处理认证信息,浏览器会自动管理这些数据。同时,通过设置合适的过期时间,可以实现"记住我"功能,让用户在指定时间内保持登录状态。更重要的是,通过设置 httpOnly 属性,可以防止恶意 JavaScript 代码窃取认证令牌,大大提高了安全性。 ```javascript // 登录成功后设置认证 Cookie function setAuthCookie(token) { const expires = new Date(); expires.setTime(expires.getTime() + (7 * 24 * 60 * 60 * 1000)); // 7天 document.cookie = `auth_token=${token}; expires=${expires.toUTCString()}; path=/; secure; httpOnly`; } ``` **用户偏好设置**:   Cookie 也非常适合存储用户的个性化偏好设置,比如语言选择、时区设置、显示偏好等。这些设置通常需要在用户访问网站的不同页面时保持一致,而 Cookie 的自动传输特性正好满足了这个需求。   当用户选择了某种语言后,这个偏好会被存储在 Cookie 中,用户访问网站的任何页面时,服务器都能读取到这个设置,从而为用户提供相应语言的内容。这种机制使得用户不需要在每个页面都重新设置偏好,大大提升了用户体验。 ```javascript // 记住用户的语言偏好 function setLanguagePreference(lang) { document.cookie = `language=${lang}; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/`; } ``` **购物车状态**:   在电商网站中,Cookie 经常被用来保存用户的购物车状态。当用户添加商品到购物车时,商品信息会被存储在 Cookie 中。即使用户关闭浏览器后重新打开,购物车中的商品依然存在,这避免了用户因为意外关闭浏览器而丢失购物车内容的困扰。   这种应用场景特别适合那些不需要用户登录就能购物的网站,或者用户还没有登录但已经开始浏览商品的情况。通过 Cookie 保存购物车状态,可以确保用户的购物体验不会因为技术问题而中断。 ```javascript // 保存购物车商品ID function saveCartItems(itemIds) { const cartData = JSON.stringify(itemIds); document.cookie = `cart_items=${cartData}; expires=Thu, 18 Dec 2024 12:00:00 UTC; path=/`; } ``` ### localStorage 的典型应用场景 **用户设置和配置**:   localStorage 是存储用户个性化设置的理想选择。与 Cookie 不同,localStorage 的数据不会在每次请求中传输,这意味着它不会增加网络开销,同时也没有大小限制的困扰。用户可以设置各种界面偏好,比如主题颜色、字体大小、布局方式等,这些设置会被永久保存在 localStorage 中。   这种持久化存储的特性使得用户在任何时候访问网站,都能看到自己熟悉的界面设置。即使是在不同的设备上,只要用户登录了同一个账户,这些设置也能被同步和恢复。这种一致性对于提升用户体验非常重要。 ```javascript // 保存用户的主题偏好 function saveThemePreference(theme) { localStorage.setItem('user_theme', theme); } // 保存用户的界面设置 function saveUISettings(settings) { localStorage.setItem('ui_settings', JSON.stringify(settings)); } ``` **离线数据缓存**:   localStorage 在数据缓存方面表现出色。现代 Web 应用经常需要从服务器获取大量数据,如果每次都通过网络请求获取,不仅会增加服务器负担,还会影响用户体验。通过 localStorage 缓存 API 响应数据,可以显著减少网络请求,提升应用性能。   这种缓存机制特别适合那些数据更新频率不高,但对访问速度要求较高的场景。比如新闻网站的文章内容、电商网站的商品信息、社交媒体的用户资料等。通过合理的缓存策略,可以在保证数据新鲜度的同时,大幅提升用户体验。 ```javascript // 缓存API响应数据 function cacheAPIData(key, data) { const cacheData = { data: data, timestamp: Date.now(), expires: Date.now() + (60 * 60 * 1000) // 1小时过期 }; localStorage.setItem(`cache_${key}`, JSON.stringify(cacheData)); } // 读取缓存数据 function getCachedData(key) { const cached = localStorage.getItem(`cache_${key}`); if (!cached) return null; const cacheData = JSON.parse(cached); if (Date.now() > cacheData.expires) { localStorage.removeItem(`cache_${key}`); return null; } return cacheData.data; } ``` **表单数据持久化**:   localStorage 在表单处理方面也有独特的优势。当用户在填写复杂的表单时,如果因为网络问题或其他原因导致页面刷新,用户之前输入的内容就会丢失,这会给用户带来很大的困扰。通过 localStorage 自动保存表单数据,可以避免这种情况的发生。   这种机制不仅适用于简单的表单,对于多步骤的复杂表单更加有用。用户可以随时中断填写过程,稍后继续完成,而不用担心数据丢失。这种贴心的功能设计,往往能够显著提升用户对网站的满意度和信任度。 ```javascript // 自动保存表单草稿 function saveFormDraft(formId, formData) { localStorage.setItem(`draft_${formId}`, JSON.stringify(formData)); } // 恢复表单草稿 function restoreFormDraft(formId) { const draft = localStorage.getItem(`draft_${formId}`); return draft ? JSON.parse(draft) : null; } ``` ### sessionStorage 的典型应用场景 **临时状态管理**:   sessionStorage 在管理临时状态方面表现出色。当用户在浏览网页时,经常会遇到需要保持当前状态的情况,比如滚动位置、展开的菜单状态、选中的标签页等。这些状态信息通常只在当前会话中有效,不需要跨会话保持,sessionStorage 正好满足这种需求。   比如,当用户在一个长页面中滚动到某个位置,然后点击链接跳转到其他页面,再通过浏览器的后退按钮返回时,如果能够自动恢复到之前的滚动位置,用户体验会大大提升。sessionStorage 可以轻松实现这种功能,而且数据会在标签页关闭时自动清理,不会占用不必要的存储空间。 ```javascript // 保存当前页面的滚动位置 function saveScrollPosition() { sessionStorage.setItem('scroll_position', window.scrollY.toString()); } // 恢复滚动位置 function restoreScrollPosition() { const position = sessionStorage.getItem('scroll_position'); if (position) { window.scrollTo(0, parseInt(position)); } } ``` **多步骤表单状态**:   在多步骤表单中,sessionStorage 发挥着重要作用。用户填写复杂表单时,往往需要分多个步骤完成,每个步骤都有相应的数据需要保存。sessionStorage 可以保存当前步骤信息以及每个步骤的填写数据,确保用户可以在不同步骤之间自由切换,而不会丢失已填写的内容。   这种应用场景在注册流程、调查问卷、订单提交等场景中非常常见。用户可以在填写过程中随时返回修改之前的内容,也可以暂时离开页面,稍后继续完成。sessionStorage 的标签页级别隔离特性,确保了不同标签页中的表单数据不会相互干扰。 ```javascript // 保存多步骤表单的当前步骤 function saveFormStep(step, formData) { sessionStorage.setItem('current_step', step.toString()); sessionStorage.setItem(`step_${step}_data`, JSON.stringify(formData)); } // 获取当前步骤 function getCurrentStep() { return parseInt(sessionStorage.getItem('current_step') || '1'); } ``` **临时用户操作记录**:   sessionStorage 还经常被用来记录用户的临时操作行为。这些记录通常用于分析用户的使用习惯、提供个性化的用户体验,或者用于调试和问题排查。由于这些数据只在当前会话中有效,不需要长期保存,sessionStorage 是最合适的选择。   比如,可以记录用户在当前会话中访问了哪些页面、进行了哪些操作、遇到了哪些错误等。这些信息可以帮助开发者了解用户的使用模式,优化产品设计。同时,由于数据会在会话结束时自动清理,也避免了长期存储可能带来的隐私问题。   总的来说,这三种存储技术在实际应用中各有其独特的价值。Cookie 凭借其自动传输特性,在需要与服务器交互的场景中不可替代;localStorage 的大容量和持久化特性,使其成为客户端数据存储的主力军;sessionStorage 的临时性和标签页隔离特性,使其在管理临时状态方面表现出色。在实际开发中,往往需要根据具体的业务需求,灵活运用这些不同的存储技术。 ```javascript // 记录用户的临时操作 function logUserAction(action, data) { const actions = JSON.parse(sessionStorage.getItem('user_actions') || '[]'); actions.push({ action: action, data: data, timestamp: Date.now() }); sessionStorage.setItem('user_actions', JSON.stringify(actions)); } ``` ## 四、与浏览器缓存策略的区别   读到这里,有些小伙伴可能会产生疑问:这些存储技术听起来和浏览器的缓存策略有点像,比如强缓存、协商缓存之类的。确实,它们之间有一些相似之处,但本质上有着根本性的区别。 ### 缓存策略的本质:性能优化的技术手段   浏览器缓存策略(包括强缓存和协商缓存)本质上是一种性能优化技术。它的主要目的是减少网络请求,提升页面加载速度。当浏览器请求一个资源时,会先检查本地缓存,如果缓存有效就直接使用,避免重复的网络请求。   强缓存通过设置 `Cache-Control` 和 `Expires` 等响应头来控制资源的缓存时间。在这个时间内,浏览器会直接使用本地缓存的资源,不会向服务器发送请求。协商缓存则通过 `ETag` 和 `Last-Modified` 等机制,让服务器判断资源是否有更新,如果没有更新就返回 304 状态码,告诉浏览器继续使用缓存。   这种缓存机制主要针对的是静态资源,比如 HTML 文件、CSS 样式表、JavaScript 脚本、图片、字体等。这些资源通常不会频繁变化,通过缓存可以显著减少网络传输,提升用户体验。 ### Storage 的本质:用户偏好和状态管理   而 sessionStorage、localStorage 和 cookie 这些存储技术,它们的本质是用户偏好和状态管理。它们存储的不是静态资源,而是用户的个性化数据、应用状态、用户行为记录等。   比如,用户选择了深色主题,这个偏好会被存储在 localStorage 中,下次访问时应用会自动应用这个主题。用户填写了一半的表单,这些数据会被保存在 sessionStorage 中,即使页面刷新也不会丢失。用户登录后的认证信息会被存储在 cookie 中,确保用户在整个网站中保持登录状态。 ### 数据生命周期和管理方式的不同   缓存策略的数据生命周期通常由服务器控制,通过 HTTP 响应头来设置缓存时间。当缓存过期时,浏览器会自动向服务器请求新的资源。这种管理方式是自动化的,用户通常感知不到缓存的存在。   而 Storage 的数据生命周期则由开发者控制,通过 JavaScript 代码来设置、读取和删除数据。开发者需要根据业务需求来决定何时存储数据、何时清理数据。这种管理方式更加灵活,但也需要开发者承担更多的责任。 ### 存储内容的性质差异   缓存策略存储的内容通常是"公共的",即所有用户访问同一个资源时,缓存的内容都是一样的。比如,所有用户访问同一个 CSS 文件时,缓存的内容都是相同的。   而 Storage 存储的内容通常是"个性化的",即每个用户存储的内容可能都不同。比如,不同用户可能有不同的主题偏好、不同的表单数据、不同的浏览历史等。 ### 性能影响的不同   缓存策略主要影响的是网络性能,通过减少网络请求来提升页面加载速度。它的性能优化效果是全局的,对所有用户都有益。   而 Storage 主要影响的是用户体验,通过保存用户状态和偏好来提升交互体验。它的优化效果是个性化的,主要针对单个用户的使用体验。 ## 总结   通过上文的分析,我们对 sessionStorage、localStorage 和 cookie 这三种前端存储技术有了基本的认识。它们虽然都能在浏览器中存储数据,但各自有着独特的特性和适用场景。   Cookie 最适合需要与服务器交互的场景,如用户认证、会话管理等。虽然容量限制较大,但其自动传输特性和丰富的安全属性使其在特定场景下不可替代。   localStorage 是客户端持久化存储的最佳选择,适合存储用户偏好、应用配置等需要长期保存的数据。其大容量和简单的 API 使其成为现代 Web 应用的首选。   sessionStorage 则专注于临时数据存储,适合存储表单草稿、临时状态等不需要持久化的数据。其标签页级别的隔离特性使其在特定场景下非常有用。   值得注意的是,这三种存储技术与浏览器缓存策略有着本质性的区别。缓存策略是一种性能优化技术,主要解决网络传输效率问题;而 Storage 技术是一种状态管理技术,主要解决用户体验和个性化问题。   在实际开发中,我们应该根据数据的特点、安全需求和性能要求来选择合适的存储方案。很多时候,一个应用会同时使用多种存储技术,每种技术负责不同的数据存储需求。
  •  

让代码自己说话:单元测试的艺术

  刚开始接触前端开发时,我对单元测试充满了困惑。为什么要写这些看起来"多余"的代码?为什么要花时间测试那些"显而易见"的功能?亦或是单元测试不是测试工程师应该干的,跟我们前端开发有什么关系?直到我在项目中踩了几个坑,好不容易写好了但是在运行的时候出bug了,让我不得不重新修改,重新提交,这种麻烦,才让我真正理解了单元测试的价值。   还记得有一次,我修改了一个看似简单的工具函数,结果把整个页面搞崩了,花了大半天才找到问题所在。如果当时有测试,这个问题可能几分钟就能发现。还有一次,我在重构代码时,不小心改坏了一个组件的逻辑,直到一位朋友反馈才知道出了问题。这些经历让我意识到,单元测试不是可有可无的,而是现代前端开发中不可或缺的一环。   近期刚对单元测试做了一些了解,也写了部分UI组件和工具函数的测试用例,收获颇丰。从最初的抗拒到现在的认可,这个过程让我对代码质量有了更深的理解。   今天,我想和大家分享我对单元测试的理解和感悟,希望能帮助刚入门的小伙伴们少走一些弯路。 ## 一、什么是单元测试?为什么前端需要它? ### 什么是单元测试?   简单来说,单元测试就是**测试你写的每一个小功能是否按预期工作**。比如你写了一个计算两个数相加的函数,单元测试就是验证这个函数在各种情况下都能给出正确的结果。 ```javascript // 你写的函数 function add(a, b) { return a + b; } // 对应的单元测试 test('add函数应该能正确计算两个数的和', () => { expect(add(2, 3)).toBe(5); expect(add(-1, 1)).toBe(0); expect(add(0.1, 0.2)).toBeCloseTo(0.3); }); ``` ### 为什么前端需要单元测试? **1. 前端代码越来越复杂** 现在的前端不再是简单的页面展示,而是复杂的单页应用。一个按钮点击可能触发多个状态变化,一个表单提交可能涉及多个API调用。没有测试,真的很难保证所有功能都正常工作。 **2. 用户界面变化频繁** 前端需求变化快,今天要改个样式,明天要加个功能。有了测试,你就能放心地修改代码,不用担心改坏了其他地方。 **3. 团队协作需要** 在团队开发中,你的代码可能会被其他人修改。有了测试,就能确保代码在修改后仍然按预期工作。   由此可见,单元测试的重要性是不可忽略的。它不仅能提高代码质量,还能让开发过程更加高效和可靠。无论是个人项目还是团队协作,单元测试都是现代前端开发中不可或缺的一环。 ## 二、单元测试的核心原则 ### 1. 测试单一功能   每个测试应该只验证一个功能点。不要在一个测试中验证多个不相关的功能。这样做的好处是,当测试失败时,你能立即知道是哪个具体功能出了问题,而不是在一大堆测试中慢慢排查。   想象一下,如果你把用户登录的所有功能都放在一个测试里,当测试失败时,你根本不知道是用户名验证出了问题,还是密码验证有问题,或者是登录逻辑本身有bug。分开测试后,问题定位就变得非常精准。 ```javascript // 不好的做法:一个测试验证多个功能 test('用户登录功能', () => { // 验证用户名验证 // 验证密码验证 // 验证登录成功 // 验证错误处理 }); // 好的做法:分别测试每个功能 test('应该验证用户名格式', () => { expect(validateUsername('')).toBe(false); expect(validateUsername('abc')).toBe(true); }); test('应该验证密码强度', () => { expect(validatePassword('123')).toBe(false); expect(validatePassword('abc123')).toBe(true); }); ``` ### 2. 测试边界情况   不仅要测试正常情况,还要测试边界情况和异常情况。这是很多新手容易忽略的地方。我们往往只测试"正常流程",但用户在使用过程中会遇到各种意外情况。   比如一个除法函数,不仅要测试10除以2等于5这种正常情况,还要测试0除以5等于0这种边界情况,更要测试10除以0这种异常情况。只有把这些都考虑到了,你的代码才真正健壮。 ```javascript test('计算器除法功能', () => { // 正常情况 expect(divide(10, 2)).toBe(5); // 边界情况 expect(divide(0, 5)).toBe(0); // 异常情况 expect(() => divide(10, 0)).toThrow('除数不能为零'); }); ``` ### 3. 测试应该是独立的   每个测试应该能够独立运行,不依赖其他测试的结果。这个原则非常重要,因为测试的执行顺序是不确定的,如果测试之间有依赖关系,就会出现"有时候能通过,有时候不能通过"的奇怪现象。   大家想一下,如果第一个测试创建了一个用户,第二个测试依赖这个用户来删除,那么当测试顺序改变时,第二个测试就会失败。正确的做法是每个测试都自己准备需要的数据,测试结束后自己清理。 ```javascript // 不好的做法:测试之间有依赖 let user = null; test('创建用户', () => { user = createUser('test@example.com'); expect(user).toBeTruthy(); }); test('删除用户', () => { deleteUser(user.id); // 依赖上一个测试 expect(getUser(user.id)).toBeNull(); }); // 好的做法:每个测试独立 test('创建用户', () => { const user = createUser('test@example.com'); expect(user).toBeTruthy(); // 测试结束后清理 deleteUser(user.id); }); test('删除用户', () => { const user = createUser('test@example.com'); deleteUser(user.id); expect(getUser(user.id)).toBeNull(); }); ``` ## 三、前端单元测试的一些例子 ### 测试React组件   在React项目中,我们通常使用Jest和React Testing Library来测试组件。React Testing Library的设计理念是"测试组件的行为,而不是实现细节",这意味着我们关注的是用户能看到和交互的内容,而不是组件内部的状态管理。   比如测试一个按钮组件,我们关心的是按钮是否显示了正确的文本,点击后是否触发了正确的事件,而不是按钮内部用了什么状态管理方式。 ```javascript import { render, screen, fireEvent } from '@testing-library/react'; import Button from './Button'; test('按钮应该显示正确的文本', () => { render(); expect(screen.getByText('点击我')).toBeInTheDocument(); }); test('按钮点击应该触发onClick事件', () => { const handleClick = jest.fn(); render(); fireEvent.click(screen.getByText('点击我')); expect(handleClick).toHaveBeenCalledTimes(1); }); test('按钮应该支持不同的变体', () => { render(); expect(screen.getByText('主要按钮')).toHaveClass('btn-primary'); }); ``` ### 测试工具函数   工具函数通常是最容易测试的,因为它们没有副作用,输入什么就输出什么。但即使是简单的工具函数,也要考虑各种边界情况。   比如这个日期格式化函数,不仅要测试正常的日期输入,还要测试空值、无效值等边界情况。这些边界情况往往是bug的温床。 ```javascript // 工具函数 export function formatDate(date) { if (!date) return ''; return new Date(date).toLocaleDateString('zh-CN'); } // 测试 test('formatDate应该正确格式化日期', () => { const date = '2023-12-25'; expect(formatDate(date)).toBe('2023/12/25'); }); test('formatDate应该处理空值', () => { expect(formatDate(null)).toBe(''); expect(formatDate(undefined)).toBe(''); }); ``` ### 测试API调用   API调用是前端测试中的一个难点,因为涉及到网络请求。我们需要模拟这些网络请求,确保测试的稳定性和速度。   这里的关键是使用`jest.fn()`来模拟`fetch`函数,然后控制它的返回值。这样我们就能测试各种情况:成功的情况、失败的情况、网络错误的情况等等。 ```javascript // API函数 export async function fetchUser(id) { const response = await fetch(`/api/users/${id}`); if (!response.ok) { throw new Error('用户不存在'); } return response.json(); } // 测试 test('fetchUser应该返回用户数据', async () => { // 模拟fetch global.fetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ id: 1, name: '张三' }) }); const user = await fetchUser(1); expect(user).toEqual({ id: 1, name: '张三' }); }); test('fetchUser应该处理错误情况', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: false }); await expect(fetchUser(999)).rejects.toThrow('用户不存在'); }); ``` ## 四、常见测试场景 ### 1. 测试表单组件   表单是前端开发中最常见的组件之一,也是最容易出错的地方。用户输入验证、提交处理、错误提示等等,每一个环节都需要仔细测试。   这里我们测试两个关键场景:一是表单验证,确保必填字段的验证逻辑正确;二是用户输入处理,确保表单能正确响应用户的输入操作。 ```javascript test('表单提交应该验证必填字段', async () => { render(); const submitButton = screen.getByText('登录'); fireEvent.click(submitButton); expect(await screen.findByText('用户名不能为空')).toBeInTheDocument(); expect(await screen.findByText('密码不能为空')).toBeInTheDocument(); }); test('表单应该正确处理用户输入', () => { render(); const usernameInput = screen.getByLabelText('用户名'); const passwordInput = screen.getByLabelText('密码'); fireEvent.change(usernameInput, { target: { value: 'testuser' } }); fireEvent.change(passwordInput, { target: { value: 'password123' } }); expect(usernameInput.value).toBe('testuser'); expect(passwordInput.value).toBe('password123'); }); ``` ### 2. 测试条件渲染   条件渲染是React中非常常见的模式,根据不同的状态显示不同的内容。测试这种场景时,我们需要验证组件在不同状态下是否正确渲染了对应的内容。   这里使用`rerender`来重新渲染组件,模拟状态变化,然后验证渲染结果是否符合预期。 ```javascript test('应该根据用户状态显示不同内容', () => { const { rerender } = render(); expect(screen.getByText('请先登录')).toBeInTheDocument(); rerender(); expect(screen.getByText('欢迎,张三')).toBeInTheDocument(); }); ``` ### 3. 测试异步操作   异步操作是前端开发中的另一个难点,特别是涉及到数据加载、状态更新等场景。测试异步操作时,我们需要等待异步操作完成,然后验证结果。   这里使用`waitFor`来等待异步操作完成,确保测试的稳定性。 ```javascript test('应该显示加载状态', async () => { render(); expect(screen.getByText('加载中...')).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('用户列表')).toBeInTheDocument(); }); }); ``` ## 五、测试最好的实践 ### 1. 测试命名要清晰   测试的名称应该清楚地描述测试的内容和预期结果。一个好的测试名称应该让其他人(包括未来的你)一眼就能看懂这个测试在做什么,就像你在git上提交代码一样,如果写的不清不楚,到时候根本就不知道自己在干什么。   想象一下,几个月后你看到测试失败的报告,如果测试名称是"测试1",你根本不知道这个测试在验证什么功能。但如果名称是"当用户点击删除按钮时,应该显示确认对话框",你立即就知道问题出在哪里。   这就像写代码注释一样,好的测试名称本身就是最好的文档。当你的项目越来越大,测试用例越来越多时,清晰的命名就显得尤为重要。一个团队中,如果每个人都能通过测试名称快速理解测试的目的,那么代码维护的效率会大大提高。 ```javascript // 不好的命名 test('测试1', () => {}); test('应该工作', () => {}); // 好的命名 test('当用户点击删除按钮时,应该显示确认对话框', () => {}); test('当输入无效邮箱时,应该显示错误信息', () => {}); ``` ### 2. 使用描述性的断言   断言应该尽可能具体和描述性。不要只写`expect(result).toBe(true)`,而要写`expect(result).toHaveLength(3)`或`expect(result).toContain('expected value')`。   这样当测试失败时,错误信息会更加有用,能帮助你快速定位问题。 ```javascript // 不好的断言 expect(result).toBe(true); // 好的断言 expect(result).toBe(true); expect(result).toHaveLength(3); expect(result).toContain('expected value'); ``` ### 3. 测试覆盖率要合理   不要盲目追求100%的覆盖率,重点测试核心业务逻辑和容易出错的地方。有些代码(比如简单的getter/setter)可能不值得写测试,而有些复杂的业务逻辑则需要重点测试。 ### 4. 保持测试简单   测试代码应该比被测试的代码更简单,更容易理解。如果测试代码本身就很复杂,那么当测试失败时,你不仅要调试被测试的代码,还要调试测试代码,这就本末倒置了。 ## 六、Jest单元测试的一些常见问题和解决方案 ### 1. 测试环境配置问题   测试环境的配置往往是新手遇到的第一个难题。Jest需要知道如何运行你的代码,如何处理模块导入,如何模拟浏览器环境等等。   这个配置告诉Jest使用`jsdom`环境(模拟浏览器),设置测试文件的路径映射,以及测试前的准备工作。 ```javascript // 在jest.config.js中配置 module.exports = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['/src/setupTests.ts'], moduleNameMapping: { '^@/(.*)$': '/src/$1' } }; ``` ### 2. 模拟外部依赖   前端代码经常依赖浏览器的API,比如`localStorage`、`fetch`等。在测试环境中,这些API可能不存在或者行为不一致,所以我们需要模拟它们。   这里我们创建了一个`localStorage`的模拟对象,用`jest.fn()`来模拟各种方法,然后将其赋值给`global.localStorage`,这样测试代码就能正常使用了。 ```javascript // 模拟localStorage const localStorageMock = { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn() }; global.localStorage = localStorageMock; ``` ### 3. 测试异步代码   异步代码的测试是另一个常见难点。JavaScript的异步特性让测试变得复杂,我们需要等待异步操作完成,然后验证结果。   这里展示了两种处理异步测试的方法:使用async/await直接等待异步函数完成,或者使用waitFor等待DOM更新。 ```javascript // 使用async/await test('异步函数测试', async () => { const result = await asyncFunction(); expect(result).toBe('expected'); }); // 使用waitFor test('等待元素出现', async () => { render(); await waitFor(() => { expect(screen.getByText('加载完成')).toBeInTheDocument(); }); }); ``` ## 七、测试驱动开发(TDD)   测试驱动开发是一种开发方法,先写测试,再写代码。这种方法听起来反直觉,但实际上能带来很多好处:更好的代码设计、更高的测试覆盖率、更少的bug等等。 ### TDD的基本流程 1. **Red**:写一个失败的测试 2. **Green**:写最少的代码让测试通过 3. **Refactor**:重构代码,保持测试通过 这个循环不断重复,每次循环都会让代码质量得到提升。 ### 示例:实现一个简单的计算器   让我们通过一个简单的例子来理解TDD的过程。我们要实现一个计算器,能够计算两个数的和。   首先,我们写一个测试,描述我们期望的行为。这个测试会失败,因为我们还没有实现Calculator类。 ```javascript // 第一步:写测试(Red) test('计算器应该能计算两个数的和', () => { const calculator = new Calculator(); expect(calculator.add(2, 3)).toBe(5); }); // 第二步:写代码(Green) class Calculator { add(a, b) { return a + b; } } // 第三步:重构(Refactor) // 优化代码结构,保持测试通过 ```   然后,我们写最少的代码让测试通过。这里我们只需要实现一个简单的add方法。   最后,我们可以重构代码,优化结构,但必须保持测试通过。这样我们就有了一个既简单又可靠的实现。 ## 总结:测试让代码更可靠   单元测试不是负担,而是帮助我们写出更好代码的工具。通过测试,我们能够: - **提前发现问题**:在开发阶段就发现bug,而不需要你眼中那位“点点点”一直在给你挑毛病。 - **安全重构**:放心地优化代码结构,不用担心改坏了其他地方,测试会告诉你哪里出了问题。 - **提高代码质量**:测试过程促使我们思考边界情况,那些你平时容易忽略的异常场景。 - **增强信心**:知道代码按预期工作,部署时心里有底,不用提心吊胆。   记住,好的测试不是追求100%的覆盖率,而是测试那些真正重要的功能。从简单的函数开始,逐步建立测试习惯,你会发现测试让开发变得更加有趣和可靠。
  •  

本地极速体验指南

> **性能优化,用户体验。LCP 0.6s,FCP 0.4s,内容快速加载,交互流畅。** --- 这是一个博客系统项目,主要关注前端体验和工程化实践。项目注重代码质量和用户体验,在功能实现和性能优化方面做了一些尝试。 ## 在线体验 👉 [在线体验博客](https://www.gfbzsblog.site) ## 🛠️ 技术栈 - 前端:Next.js 15 + React 18 + TypeScript + Redux Toolkit + SCSS + Framer Motion - 后端:Spring Boot 3 + MyBatis Plus + MySQL - 部署:宝塔Linux + Nginx - 其他:腾讯云COS、腾讯云SES、DeepSeek API 接口、腾讯云CDN内容分发与全球加速 ## 目录结构 - /api:接口集中封装,类型安全,便于维护,接口变动一处修改即可全站生效 - /components:多个可复用组件,支持动画、响应式、主题切换,包括AI聊天、评论、目录、弹窗、分页、表情、打字机、代码高亮、友链、头像预览等 - /pages:Next.js 路由自动映射,包含主站、后台、404、首页、文章、归档、友链、相册、灵感、统计、AI 聊天、搜索、标签、留言板等页面 - /redux:全局状态管理,覆盖权限、主题、系统设置、认证、管理员认证等,支持异步操作、持久化 - /hooks:自定义 Hook,业务逻辑解耦,包括useAIChat、useAuth、useLoading、useDebounce、useThrottle、useTheme、useError等 - /utils:工具函数库,http 封装、加密、格式化、AI 工具、文章工具、类型安全、校验等功能 - /routes:路由集中配置,导航菜单自动生成,支持嵌套路由、权限控制 - /styles:全局样式、字体、主题变量、CSS Modules 样式隔离,支持暗黑/明亮模式切换 - /config:全局配置项,AI 助手、主题、接口、系统参数统一管理 - /client、/admin、/context、/http、/types:细分业务和基础能力,代码结构清晰,便于维护和扩展 ## 工程化和体验 - HTTP 封装:自研 HTTP工具,支持全局 BASE_URL、自动带 token、错误统一处理,具备请求拦截、响应拦截、错误拦截、请求取消拦截、请求重试拦截等拦截器,API 层全部基于 http 封装 - 性能优化:采用SSG/SSR/ISR/CSR混合策略,首页 LCP 0.6s、FCP 0.4s,虚拟滚动优化长列表,图片懒加载+CDN,渐进加载,代码分割/懒加载分包策略,Tree Shaking减少包体积 - 类型安全:全量 TypeScript,接口、组件、工具、全局状态全部类型覆盖 - 动画和交互:使用Framer Motion、Typewriter等库,注重用户体验 - 功能:包含文章、标签、友链、相册、公告、AI 聊天、评论、后台管理、权限、邮箱通知、云存储、统计分析等 - 细节体验:智能文章目录、评论区 Markdown/表情/楼中楼、AI 聊天流式响应、操作提示弹窗、主题切换、后台权限管理、性能监控等 --- ## 1. 博客内容截图 & Lighthouse 评分 **内容截图** 图片 **Lighthouse 评分** 图片 > **首页 LCP 0.6s,FCP 0.4s,Lighthouse 评分 99+** > 通过SSR/SSG/ISR、Next.js图片压缩、图片懒加载、条件渲染+虚拟滚动、Tree Shaking、CDN内容分发、路由懒加载、组件懒加载等优化实现 --- ## 2. 博客界面体验 - 首页设计参考了[@grtsinry43大佬](https://github.com/grtsinry43)的实现思路 - 文章列表页 图片 - 文章详情页 文章详情页是博客系统的核心页面,支持Markdown 全格式渲染、代码高亮、表格、视频、序列图等内容的展示。左侧有智能目录,实时高亮当前阅读进度,右侧有文章侧边栏,支持字体调节、阅读时间统计、MarkDown导出和分享等功能。包含点赞动画、评论区、最近文章推荐、返回列表等交互功能。 图片 - 标签云页 标签云页面的动画和交互设计,参考了@grtsinry43大佬的实现思路。每个标签都有独立的配色和动态背景,入场时带有弹性和旋转的动画效果,鼠标悬停时有缩放与旋转反馈。底部还叠加了动态的 TagCloud 背景,增加视觉层次感。 图片 - 灵光一瞬 灵光一瞬页面用于记录生活中的点滴和小想法。每条内容以简洁的动漫风卡片形式展示,错落有致,方便浏览。整体风格轻松温和,可以随时回顾和分享这些生活片段。 图片 - AI智能助手 小熙AI页面是博客里的智能助手空间。整体风格亲切,顶部有氛围感文案和欢迎语。未登录时会提示登录,登录后可以直接和AI对话。对话区采用了自研的 AIChat 组件,基于DeepSeek大模型API,支持流式消息、Markdown 渲染、代码高亮。页面布局简洁,交互细节到位。 图片 - 留言板 留言板页面提供留言、互动、交流的功能。支持匿名或带头像留言(支持QQ邮箱自动解析头像,也可以自定义上传),每条留言都能被置顶、回复,支持性别标识和时间显示。包含表单校验、头像上传、操作提示、加载动画等基础功能。留言内容支持动态打字机动画,管理员回复会高亮展示。 图片 - 友链页面 友链页面除了展示链接外,还提供了实时预览功能:点击任意友链,右侧会弹出对方网站的实时窗口(iframe 预览),不用跳转就能预览对方站点。如果对方网站不支持嵌入,会有友好的提示。预览区自适应缩放,窗口高度和布局会自动同步。 图片 - 暗黑/明亮模式切换 主题切换支持在明亮模式和暗黑模式之间切换,整个站点的配色、背景、字体会实时变化。所有页面、组件、动画都适配了主题变化,细节统一。 图片 - 后台管理 后台管理系统用于管理博客的所有内容和功能。采用模块化、组件化的设计,每个功能区独立成模块,便于维护和扩展。 后台分为文章管理、评论管理、标签管理、用户管理、友链管理、留言板管理、相册管理、灵感管理、面试题收集管理、系统设置等多个板块。每个板块都有专属的表单、列表、筛选、批量操作、权限校验等功能,支持增删改查、批量导入导出、内容审核、置顶、回复、关联等操作。 后台采用响应式布局,适配不同屏幕,包含权限管理和路由守卫。每个管理模块都拆分成独立的 React 组件,样式隔离,逻辑清晰。 **仪表盘** 图片 **文章管理表单** 图片 **留言板管理** 图片 **全局系统设置** 图片 --- ## 3. 本地安装 ### 环境要求 - Node.js 18+ - npm 9+ - Java 17+ - Maven 3+ - MySQL 5.7+/8.0+ - 推荐 Edge 浏览器体验 ### 克隆项目 ```bash git clone cd ``` --- ### 后端配置(Spring Boot) #### 1. 数据库准备 创建数据库(如有需要): ```sql CREATE DATABASE my_blog ; ``` 数据库文件路径:src/main/resources/db/migration/mysql_db.sql #### 2. .env 配置(blogBackend/.env) ```env # 数据库 MYSQL_USERNAME=your_mysql_user MYSQL_PASSWORD=your_mysql_password # 邮箱 TENCENT_CLOUD_MAIL_USERNAME=your_mail@domain.com TENCENT_CLOUD_MAIL_PASSWORD=your_mail_smtp_password # 腾讯云COS COS_SECRET_ID=your_cos_secret_id COS_SECRET_KEY=your_cos_secret_key COS_BUCKET_NAME=your_cos_bucket COS_REGION=your_cos_region COS_URL=https://your-cos-url # Google OAuth(如用到) GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret ``` #### 3. 启动后端 ```bash cd blogBackend mvn spring-boot:run ``` 默认端口:**8000** 配置文件:`src/main/resources/application.yml`(支持环境变量注入) --- ### 前端配置(Next.js) #### 1. .env 配置(blogFrontend/.env.local) ```env NEXT_PUBLIC_BASE_URL=http://localhost:8000/api NEXT_PUBLIC_PASSWORD_SALT=your-salt-here NEXT_PUBLIC_API_ENDPOINT=https://api.deepseek.com/v1/chat/completions NEXT_PUBLIC_API_KEY=your-deepseek-api-key ``` #### 2. 启动前端 ```bash cd blogFrontend npm install npm run dev ``` 默认端口:**3000** 访问地址:[http://localhost:3000](http://localhost:3000) --- ## 4. 性能优化 - **SSR + SSG + ISR**:采用多种渲染策略,首屏内容直出,提升加载速度 - **图片懒加载 + CDN**:使用CDN加速和Next.js图片压缩,优化图片加载 - **按需加载**:使用虚拟滚动和IntersectionObserver API,按需加载内容 - **代码分割**:路由级和组件级的代码分割,减少主包体积 - **Tree Shaking**:移除未使用的代码,减少打包体积 - **缓存策略**:静态资源强缓存,接口智能缓存 - **Lighthouse 评分 99+**:性能、可访问性、最佳实践等方面表现良好 > **首页 LCP 0.6s,FCP 0.4s** --- ## 5. 致谢 特别感谢 [@grtsinry43大佬](https://github.com/grtsinry43) 的灵感与技术分享,他的博客项目给了我很多启发。 ```meta title: grtsinry43 url: https://github.com/grtsinry43 image: https://dogeoss.grtsinry43.com/img/author-removebg.png desc: Hi there, I'm grtsinry43 ``` --- ## 6. 生产部署 如需了解一键部署、线上环境变量配置等生产环境相关内容, 👉 [查看生产部署指南](https://www.gfbzsblog.site/main/Articles/49) ```meta title: 孤芳不自赏 url: https://www.gfbzsblog.site/main/Articles/49 image: https://www.gfbzsblog.site/images/avatar_20250520_215057.png desc: 手把手教你将前后端分离项目部署上线 ``` --- ## 贡献指南 欢迎 PR、Issue、建议和反馈! 如需本地开发、二次开发、定制功能,欢迎联系或直接提 Issue。 ## License / 许可证 This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. 本项目采用 MIT 开源许可证,详见 [LICENSE](./LICENSE) 文件。 ## 📬联系方式 - 邮箱:624787243@qq.com
  •  

新功能上线——胡桃妙记&豆包析义

### 新功能上线啦 作为一个将要上职场的学生,我始终相信,技术的温度在于更好地服务他人。经过几天的探索与实现,我很高兴为博客带来了全新的**双模式 AI 摘要系统**,希望能为每一位读者提供更贴心、更个性化的阅读预览。 **当前,您可以选择两种独特的摘要风格:** - **豆包析义**:清晰亲切的技术概览,精准提炼文章核心要点,助您快速抓住重点,节省阅读时间。 - **胡桃妙记**:一份轻松有趣的“技术甜点”!借助《原神》中胡桃的活泼语气与生动比喻,让技术概念变得好玩好记,为阅读增添一抹亮色。 其实在此之前,我对胡桃这个角色也并未深入了解。但她那种活泼洒脱、亲切可爱的性格,却令人过目难忘。我希望通过她的语言风格,为读者在阅读技术长文前带来一丝轻松,用俏皮与热情缓解技术的枯燥,同时也为您的学习过程增添一份快乐。 我为摘要提供了部分淡色配色方案,并且您的所有偏好(摘要风格与配色)都会通过 **LocalStorage 保存在本地**。如果您偏好严谨专业的表述,**豆包**愿为您提供清晰的技术解读;如果您喜欢轻松活泼的风格,**胡桃**也乐意随时相伴。下次访问,您依然会看到自己最熟悉的界面。 我深知,再智能的AI也无法替代创作者的思考。因此,**“博主观点的”** 摘要也在规划之中。它将由我亲自执笔,为您点明文章的核心动机与设计脉络,为您提供一份最终的阅读“地图”。(其实已经做好了,但是我写的摘要太流水账了😅) 希望这个新功能能让您的技术探索之旅更愉悦、更高效。如果您喜欢这一设计,欢迎在评论区告诉我~~~ 您的认可,将是我持续开发新功能的最大动力。 P.S. 也欢迎来抖音私信我哦!下面是我的抖音,最常用的通信工具😘 ```meta title: 熙夜 url: https://www.douyin.com/user/MS4wLjABAAAAeK227ZM7DqkdYg7Qr-KL7CSqB9B3hp8hPhtZPbjRKus image: https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-i-0813c000-ce_oofo189CAYBoajuiGPieQEAyEBAGIwE0zFAevA~c5_300x300.jpeg?from=2956013662 desc: 水拥抱水沉默,水听见风起舞 ```
  •  

Web3前端探索长篇手记 🌌

## 初识 Web3 最初我接触 Web3,只觉得它和前端没啥大不了的区别,无非是在页面上多一个“连接钱包”的按钮。甚至一度以为,直接用现成的 React 包装组件就能搞定。 可真正开始动手时,才发现这东西完全不是“复制代码”就能跑起来的。钱包、链、Gas、合约、事件监听……这些陌生的词在短短几天内一股脑地涌过来,让我在兴奋和迷惑之间反复切换。 ------ ## 每日踩坑流水账 **第一天:钱包初体验** 我安装了 MetaMask,照着文档写了第一行请求账户的代码: ```js const accounts = await ethereum.request({ method: 'eth_requestAccounts' }); ``` 页面跑起来的瞬间,钱包弹窗出来了,那一刻我以为自己马上要成功。结果问题接踵而至: - 用户可能没装 MetaMask - 有的用户用 Coinbase Wallet、Trust Wallet - 连接到的链不对,测试网和主网经常乱套 于是我被迫写下一堆“防御性”代码,去判断浏览器有没有钱包、链对不对、用户要不要切换网络。第一次感受到 Web3 开发的复杂性:不是功能写不出来,而是要考虑的情况太多。 **第三天:Gas 机制的坑** 发起第一次交易的时候,钱包里跳出一行“预计费用”,我完全不知道它怎么算的。结果我点了确认,交易直接失败。后来才知道可以用 `estimateGas()` 预估一下费用,否则很容易白白浪费。那天我光为了验证一笔最简单的转账,就在测试网上折腾了一整天,还跑去 faucet 一直领测试币。 **第五天:第一个 NFT 合约** 部署成功的一刻真的有点激动。我用测试网跑通了一个最简单的 NFT mint 合约,钱包里居然出现了一张属于我的“空白 NFT”。虽然没啥价值,但那种“链上真的认了我”的感觉挺奇妙的。 **第七天:事件监听的大坑** 我想在前端实时显示合约事件,比如监听 mint 成功后更新界面。写起来倒是不难,用 ethers.js 订阅事件就行。但问题是:当组件卸载的时候忘了清理监听,结果页面反复触发回调,状态全乱了。那天我反复调试到深夜,才意识到 Web3 前端和普通前端的一个最大区别:状态不仅在应用里,还在链上。 ## 钱包与上链的多次尝试 在把文章内容“上链”的过程中,我走了好几条弯路。 1. **直接用 React 包安装** 一开始图省事,找了个现成的 React 钱包组件包,以为能一键解决。结果版本冲突一堆,依赖不兼容,最后连本地都跑不起来。 2. **后端服务集成** 第二次尝试是把钱包交互放在后端,让前端只发请求。这样确实能跑,但缺点很明显:体验太差,每一步都要等后端转发,延迟感特别重,用户完全不知道发生了什么。 3. **回归 MetaMask API** 折腾一圈后,我还是乖乖回到最原始的做法:直接调 MetaMask API,用户在浏览器里操作,所有签名和确认都是透明的。麻烦是麻烦,但至少真实。 4. **Crossbell 的尝鲜** 在内容上链这块,我还试过 Crossbell。它更偏向社交场景,我把一篇文章通过 Crossbell 发布。流程大概是: - 前端调用钱包签名 - 交易发到链上,进入 pending - 成功上链后,文章 ID 和内容永久存档 整个过程很新鲜,但也很折磨。有一次交易卡在 pending 半小时,我干脆去泡了个茶回来才看到它成功。那一刻,我真的感受到“区块链世界的时间感和前端开发完全不同”。 ## 硬核技术拆解 ### 1. 钱包连接 从“几行代码搞定”到“写一堆判断逻辑”,让我意识到用户钱包环境不可控。尤其是新用户,连装钱包都是门槛。 ### 2. 合约交互 `ethers.js` 用起来比 `web3.js` 更舒服,读合约和写合约的逻辑清晰。但写操作一定要小心,签名 + 等矿工确认是必须的,不然就像“交易消失了”。 ### 3. 跨链 Wagmi 配置多条 EVM 链挺方便,但一旦遇到非 EVM 链(比如 Solana),逻辑完全不同。这个时候才体会到“Web3 并不是一个统一标准”,每个链都像是一个独立的世界。 ## 我的实验案例 - **DEX 前端** 一开始没考虑滑点,交易几乎全失败。后来加了价格影响判断,体验才正常。 - **NFT 盲盒** 最开始把元数据解密逻辑放在前端,结果被别人轻松看穿。后来才改到合约的 `reveal()` 方法上,才算安全。 - **文章上链** 多次尝试失败之后,最终用 MetaMask + Crossbell 成功发布文章。那一刻的感觉有点像“打了个存档”:我的文字哪怕服务器关了、博客删了,链上依旧有记录。 ## 结语 Web3 前端的开发过程,比我预想得要复杂得多。它不像普通前端那样可以快速迭代,而是到处都要考虑:用户的钱包情况、链的状态、Gas 的消耗、合约的逻辑…… 但也正因为这些麻烦,每次成功都特别有成就感。第一次交易成功、第一次 NFT 部署、第一次文章上链,每一步都让我觉得自己和区块链之间的距离缩短了一点。 接下来我想继续探索: - Lens Protocol 的社交组件 - 全链游戏的前端架构 - 更多跨链应用的可能性 **麻烦是真麻烦,但乐趣也是真的。** ```meta title: lichenxigk2002 url: https://github.com/lichenxigk2002 image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/6300e6b4-ed9a-4765-81aa-332285ba0313.jpg desc: 日益努力,而后风生水起 ``` (欢迎来我的 [GitHub](https://github.com/lichenx2002) 一起交流 🤠)
  •  

Node.js 包管理工具(npm、yarn、pnpm、cnpm)详解

## 概述 在 Node.js 生态中,**包管理工具(Package Manager)** 是开发者日常使用最频繁的工具之一。它的主要职责包括: * **安装依赖**:从远程仓库下载第三方包,并保存到本地 `node_modules`。 * **版本管理**:通过 `lock` 文件锁定依赖版本,避免团队成员环境不一致。 * **依赖解析**:根据 `package.json` 的描述,解析依赖树,安装直接依赖与间接依赖。 * **脚本执行**:运行 `package.json` 中的脚本命令,例如 `npm run dev`。 随着社区的发展,涌现了多个包管理工具: * **npm**:Node.js 自带,最早也是最广泛使用。 * **yarn**:Facebook 推出,强调性能与一致性。 * **pnpm**:新一代方案,磁盘利用率和速度表现最佳。 * **cnpm**:国内团队推出的 npm 镜像工具,加速下载。 ## 1. npm ### 背景 npm(Node Package Manager)是 **Node.js 官方默认的包管理器**,自 2010 年起逐渐成为全球最大的软件包生态。 ### 特点 * **官方维护**,与 Node.js 版本强绑定。 * **package-lock.json**:确保依赖版本一致。 * **npx 工具**:便于执行临时命令或二进制文件。 * **生态最庞大**:npm registry 已经是世界上最大的软件仓库。 ### 常见用法 ```bash npm init -y # 初始化 npm install express # 安装依赖 npm uninstall lodash # 卸载依赖 npm update # 更新依赖 npm run dev # 运行脚本 ``` ### 适用场景 * 默认选择,**小型项目或通用场景**足够。 * 当不考虑磁盘占用和速度时,npm 是最稳妥的方案。 ## 2. Yarn ### 背景 Yarn 由 Facebook 在 2016 年推出,当时 **npm v4** 还没有 lock 文件,团队协作中经常出现依赖版本不一致的问题。Yarn 引入了 **yarn.lock** 和并行下载,大幅度提升了开发体验。 ### 特点 * **一致性强**:`yarn.lock` 保证每个人安装相同版本依赖。 * **安装速度快**:并行下载 + 本地缓存机制。 * **命令简洁**:如 `yarn add`,语义更清晰。 * **Yarn 2/3**:支持 Plug’n’Play (PnP),不再依赖 `node_modules`。 ### 常见用法 ```bash yarn init -y yarn add express yarn remove lodash yarn upgrade yarn dev ``` ### 适用场景 * **团队协作项目**,确保依赖一致。 * 对速度有要求,但还未迁移到 pnpm。 ## 3. pnpm ### 背景 **pnpm (Performant npm)** 是近几年迅速崛起的包管理器,以 **高效磁盘利用率** 和 **极快的安装速度** 著称。它的核心创新是 **内容寻址存储(Content-Addressable Storage)**。 ### 特点 * **磁盘节省**:不同项目相同依赖只保存一份,通过符号链接共享。 * **极快安装**:硬链接 + 并行安装。 * **严格依赖隔离**:避免“幽灵依赖”(phantom dependency)问题。 * **支持 monorepo**:天然支持大型仓库管理(如 NX、Turborepo)。 ### 常见用法 ```bash pnpm init pnpm install express pnpm remove lodash pnpm update pnpm dev ``` ### 适用场景 * **大型项目**,依赖体积庞大,磁盘压力大。 * **monorepo 管理**,多个子项目共享依赖。 ## 4. cnpm ### 背景 国内开发者使用 npm 时,经常因为网络原因下载缓慢或失败。为此,淘宝团队推出了 **cnpm**,默认使用 **淘宝 npm 镜像**。 ### 特点 * 解决国内网络环境问题,加快安装速度。 * 命令与 npm 基本一致,学习成本低。 ### 安装 ```bash npm install -g cnpm --registry=https://registry.npmmirror.com ``` ### 使用 ```bash cnpm install express ``` ### 适用场景 * 国内开发环境。 * 仅作为 **加速工具**,本质上依然依赖 npm。 ## 5. 包管理工具对比 | 工具 | 速度 | 磁盘占用 | 一致性 | 特点 | 适用场景 | | ---- | ------ | -------- | --- | ------------------- | ---- | | npm | 中等 | 较大 | ✔️ | 官方维护,生态最广 | 默认选择 | | yarn | 较快 | 中等 | ✔️ | 锁文件一致性、团队协作友好 | 团队项目 | | pnpm | **最快** | **最小** | ✔️ | 硬链接存储、monorepo 管理优秀 | 大型项目 | | cnpm | 取决于镜像 | 与 npm 一致 | ✔️ | 国内加速工具 | 国内环境 | ## 6. 包管理工具工作原理(Mermaid 图) 下面这张图展示了 **npm/yarn/pnpm 在安装依赖时的基本工作流程**: ```mermaid flowchart TD A[读取 package.json] --> B[解析依赖树] B --> C{是否已有 lock 文件} C -- 有 --> D[锁定版本 安装依赖] C -- 无 --> E[解析最新版本 写入 lock 文件] D --> F[下载包] E --> F[下载包] F --> G[npm: 每个项目独立 node_modules] F --> H[yarn: 并行下载 + 缓存] F --> I[pnpm: 硬链接到全局存储] G --> J[生成 node_modules] H --> J I --> J ``` ## 7. 总结与最佳实践 * **npm**:默认选择,稳定可靠,适合所有场景。 * **yarn**:协作一致性好,适合已有团队规范。 * **pnpm**:性能与磁盘利用率最优,推荐新项目和 monorepo。 * **cnpm**:仅用于国内加速,建议改用 npm 镜像(如 `npmmirror.com`)。 👉 **推荐策略** * 小型项目 → `npm` * 团队协作 → `yarn` * 大型项目 / monorepo → `pnpm` * 国内环境 → 配置 npm 镜像,而非依赖 `cnpm` 如果想要了解更多Node.js,可以点击下方链接⬇️ ```meta title: Node.js url: https://github.com/nodejs/examples image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/33e3f444-6a9e-4077-95b5-e480e6f66a5d.png desc: Node.js 官方示例 ```
  •  

Node.js 模块化详解

## 概述 在现代 JavaScript 开发中,**模块化** 是核心思想之一。它的目标是将代码拆分为 **独立的、可复用的单元**,从而提高可维护性与可扩展性。 Node.js 从诞生开始,就内置了 **模块化系统**,并且伴随着 ECMAScript 标准的发展,经历了从 **CommonJS → ES Module(ESM)** 的演进。 常见的模块化方案: - **CommonJS**:Node.js 默认支持的规范(`require` / `module.exports`)。 - **ES Module(ESM)**:JavaScript 官方标准(`import` / `export`)。 - **AMD / CMD / UMD**:主要用于浏览器的早期模块化方案。 ## 1. 为什么需要模块化? 早期 JavaScript 没有模块化,所有代码都放在一个文件或全局作用域中,导致: - **命名冲突**:全局变量容易相互覆盖。 - **代码难以维护**:随着项目变大,逻辑混乱。 - **复用性差**:无法轻易在不同项目间共享代码。 模块化的优势: - **隔离作用域**:不同模块互不干扰。 - **按需加载**:提高性能。 - **代码复用**:形成生态(如 npm 包)。 - **可维护性**:分治思想,便于协作开发。 ## 2. Node.js 模块化体系 Node.js 默认支持 **CommonJS**,后来逐步支持 **ESM**,目前两者并存。 ### 2.1 CommonJS Node.js 早期采用的模块系统。 **语法**: ```js // 导出 const fs = require('fs'); module.exports = { readConfig: () => fs.readFileSync('./config.json', 'utf8') }; // 引入 const { readConfig } = require('./config.js'); console.log(readConfig()); ``` 特点: - **同步加载**:适合服务端(磁盘读取速度快)。 - **缓存机制**:同一个模块只会加载一次,多次 `require` 返回同一个对象。 - **动态加载**:`require` 可以在逻辑语句中调用。 ### 2.2 ES Module(ESM) JavaScript 官方标准,Node.js 从 v12 开始支持。 **语法**: ```js // 导出 export function sum(a, b) { return a + b; } export default function greet(name) { return `Hello, ${name}`; } // 引入 import greet, { sum } from './utils.js'; console.log(greet('Alice')); console.log(sum(1, 2)); ``` 特点: - **静态分析**:编译阶段就能确定依赖关系。 - **严格模式**:默认启用 `strict mode`。 - **顶层 await**:支持在模块顶层使用 `await`。 Node.js 中使用 ESM 需要: 1. 文件扩展名为 `.mjs`,或在 `package.json` 设置 `"type": "module"`。 2. 使用 `import` / `export`。 ### 2.3 CommonJS 与 ESM 的区别 | 特性 | CommonJS (`require`) | ESM (`import/export`) | | -------- | ---------------------------- | --------------------------- | | 加载方式 | **同步** | **异步** | | 导出 | `module.exports` / `exports` | `export` / `export default` | | 引入 | `require()` 动态调用 | `import` 静态分析 | | 缓存机制 | 有(单例) | 有(单例) | | 运行环境 | Node.js 原生支持 | Node.js / 浏览器标准 | ## 3. 其他模块化方案(了解) 在 Node.js 出现之前,前端社区也有一些模块化方案: - **AMD (Asynchronous Module Definition)**:RequireJS 提出,适合浏览器异步加载。 - **CMD (Common Module Definition)**:SeaJS 提出,按需加载,更灵活。 - **UMD (Universal Module Definition)**:兼容 AMD 和 CommonJS 的方案。 如今这些方案已逐渐被 ESM 取代。 ## 4. Node.js 模块加载机制 Node.js 内部通过 **模块解析算法** 来加载模块: 1. **核心模块**:如 `fs`、`path`,优先级最高。 2. **文件模块**:相对路径或绝对路径。 3. **第三方模块**:通过 `node_modules` 目录查找(逐层向上)。 查找规则: ```bash require('lodash') # 查找顺序: # ./node_modules/lodash # ../node_modules/lodash # ../../node_modules/lodash ``` **模块缓存**: - 首次加载模块时会执行并缓存结果。 - 再次加载时直接从缓存中取,避免重复执行。 ## 5. 总结与最佳实践 - 在 **Node.js 服务端开发**中,仍然以 **CommonJS** 为主,但 ESM 越来越普及。 - **新项目推荐使用 ESM**,与前端保持统一。 - **不要混用 CommonJS 和 ESM**,可能导致兼容性问题。 - **合理拆分模块**:保持单一职责,避免“大而全”的文件。 - **利用 npm 包生态**:模块化的最终目标是代码复用与共享。 如果想要了解更多Node.js,可以点击下方链接⬇️ ```meta title: Node.js url: https://github.com/nodejs/examples image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/33e3f444-6a9e-4077-95b5-e480e6f66a5d.png desc: Node.js 官方示例 ```
  •  

Next.js 与 Nuxt.js:现代前端框架的王者对决

在追求高性能、高 SEO 友好度的现代 Web 开发中,基于 React 的 Next.js 和基于 Vue 的 Nuxt.js 无疑是两大顶流框架。它们都解决了单页面应用(SPA)在SEO和首屏加载上的核心痛点,提供了强大的服务端渲染(SSR)和静态站点生成(SSG)能力。但选择哪一个,往往取决于您的技术栈、项目需求和团队背景。 #### 一、框架概述与技术定位 Next.js 是基于 React 的全栈框架,由 Vercel 公司维护。它提供服务器端渲染(SSR)、静态站点生成(SSG)、API 路由等能力,是 React 生态中最成熟的企业级框架。最新版本支持的 App Router 引入了 React Server Components 等现代特性,进一步提升了开发体验和应用性能。 Nuxt.js 是基于 Vue.js 的渐进式框架,其设计理念深受 Next.js 启发。它通过约定优于配置的原则,为 Vue 开发者提供开箱即用的 SSR、SSG 和全栈开发能力。Nuxt 3 版本基于 Vue 3 重构,在性能和开发者体验上有显著提升。 #### 二、核心特性对比 1. **路由系统** Next.js 同时支持 Pages Router(基于文件系统)和 App Router(基于 React Server Components)。App Router 支持更复杂的布局嵌套、加载状态和错误处理,但学习曲线相对较高。 Nuxt.js 采用基于文件系统的自动路由生成,只需在 `pages` 目录中创建 `.vue` 文件即可生成对应路由,配置简单直观。 2. **数据获取** Next.js 提供精细的数据获取方法: - `getServerSideProps`:服务器端渲染时获取数据 - `getStaticProps`:构建时静态生成 - `getStaticPaths`:指定动态路由的静态生成 Nuxt.js 提供统一的 `useAsyncData` 和 `useFetch` 组合式函数,可根据运行环境自动选择客户端或服务端数据获取。 3. **状态管理** Next.js 不内置状态管理,开发者可自由选择 Redux、Zustand 或 Context API 等方案。 Nuxt.js 内置轻量级状态管理(`useState`),并推荐使用 Pinia 作为官方状态管理库。 4. **配置方式** Next.js 采用配置优于约定,通过 `next.config.js` 提供高度可定制化。 Nuxt.js 采用约定优于配置,通过 `nuxt.config.ts` 提供合理的默认配置,减少决策成本。 | 特性 | Next.js (React) | Nuxt.js (Vue) | | ------------ | ----------------------- | --------------------- | | **技术栈** | React | Vue | | **路由** | 文件系统/App Router | 约定式文件路由 | | **数据获取** | getServerSideProps等 | useAsyncData/useFetch | | **状态管理** | 需自行选择(如Zustand) | 内置 + 推荐Pinia | | **配置方式** | 配置优于约定 | 约定优于配置 | | **部署优化** | 增量静态再生(ISR) | Nitro服务器引擎 | #### 三、性能表现 两者在性能方面都表现出色: - Next.js 的增量静态再生(ISR)是其标志性特性,允许在构建后更新静态内容 - Nuxt.js 基于 Nitro 服务器引擎,提供跨平台部署能力和卓越的服务器性能 - 两者都提供自动代码分割、图片优化等性能优化功能 #### 四、生态系统 Next.js 拥有更庞大的生态系统: - 丰富的插件市场和社区资源 - 与 Vercel 平台深度集成 - 更完善的企业级解决方案 Nuxt.js 的模块系统颇具特色: - 官方和社区提供大量功能模块 - 集成体验更加流畅 - 对内容型网站的支持更友好(如 @nuxt/content 模块) #### 五、选型建议 选择 Next.js 当: - 团队熟悉 React 生态系统 - 项目需要高度定制化和控制力 - 需要利用最新的 React 特性 - 项目复杂度高,需要企业级解决方案 选择 Nuxt.js 当: - 团队熟悉 Vue.js 技术栈 - 追求开发效率和开箱即用体验 - 项目以内容展示为主(如文档、博客) - 需要快速原型开发 #### 六、总结 Next.js 和 Nuxt.js 都是优秀的元框架,它们之间的差异主要反映了 React 和 Vue 两个生态系统不同的设计哲学。Next.js 更注重灵活性和控制力,适合复杂的大型应用;Nuxt.js 更注重开发体验和开箱即用,适合快速迭代的项目。 技术选型应该基于团队的技术背景、项目需求和长期维护考虑。无论选择哪个框架,都能获得现代化的开发体验和优秀的应用性能。建议在实际选型前,可以用两个框架分别实现一个简单的功能原型,亲身体验其开发模式和特性。
  •  

Vue 的双向绑定原理

在 Vue 中,所谓的“双向绑定”指的是 **数据变化会自动更新视图,而视图输入的变化也会反过来修改数据**。典型例子: ```vue

{{ message }}

``` 当用户输入时,`message` 自动更新;当代码里改 `message`,输入框的值也会变。这种自动同步效果背后,就是 **数据劫持 + 发布订阅 + 事件监听**。 ## 一、数据劫持:监听数据的变化 Vue 2 使用 `Object.defineProperty`,Vue 3 使用 `Proxy`。以 Vue 2 为例,核心逻辑是: ```javascript function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { console.log("读取属性:", key); return val; }, set(newVal) { console.log("修改属性:", key, "=", newVal); val = newVal; // 通知视图更新 updateView(); } }); } function updateView() { console.log("视图更新"); } let data = {}; defineReactive(data, "message", "hello"); console.log(data.message); // 读取属性: message data.message = "hi"; // 修改属性: message = hi // 视图更新 ``` 这里 `Object.defineProperty` 就是 Vue 2 的核心,让每个属性变成“可观测的”。 ## 二、依赖收集:视图订阅数据变化 Vue 渲染时,模板里的 `{{ message }}` 会触发 getter。Vue 在 getter 中做了一件事:**把当前的 Watcher(订阅者)记录到依赖里**。 ```javascript let currentWatcher = null; function defineReactive(obj, key, val) { let dep = []; // 存储依赖的订阅者 Object.defineProperty(obj, key, { get() { if (currentWatcher) dep.push(currentWatcher); return val; }, set(newVal) { val = newVal; dep.forEach(fn => fn()); // 通知所有订阅者更新 } }); } let data = {}; defineReactive(data, "message", "hello"); // 模拟 Watcher currentWatcher = () => console.log("视图重新渲染:", data.message); console.log(data.message); // 依赖收集 currentWatcher = null; data.message = "hi"; // 视图重新渲染: hi ``` 这就是 **发布订阅模式**:数据一改 → 通知订阅者 → 重新渲染。 ## 三、视图到数据:事件监听 `v-model` 并不是什么魔法,它等价于: ```vue ``` 也就是说: 1. `:value="message"` 保证输入框显示数据。 2. `@input="..."` 保证输入时数据被更新。 于是形成闭环: - **数据改 → set → 通知更新 → 视图刷新** - **视图改 → input 事件 → 数据更新 → 通知更新** ## 四、Vue 2 和 Vue 3 的区别 - **Vue 2**:基于 `Object.defineProperty`,不能检测到新增/删除属性,需要 `Vue.set`。 - **Vue 3**:基于 `Proxy`,可拦截对象的各种操作(读、写、删、增),更加彻底。 ```javascript let data = new Proxy({ message: "hello" }, { get(target, key) { console.log("读取:", key); return target[key]; }, set(target, key, value) { console.log("设置:", key, "=", value); target[key] = value; return true; } }); console.log(data.message); // 读取: message data.message = "hi"; // 设置: message = hi ``` ## 小结 Vue 的双向绑定就是三步: 1. **数据劫持**:用 `defineProperty` 或 `Proxy` 让数据可被追踪。 2. **依赖收集**:渲染时记录依赖,数据变了就通知视图更新。 3. **事件监听**:通过 `v-model` 的 `input` 事件,把用户输入同步回数据。 这样就形成了 **数据 ↔ 视图** 的双向绑定机制。
  •  

Git 提交信息规范学习笔记

在写项目的时候,提交信息往往被忽视。很多时候我只是随便写 `update`、`test`、`fix bug`,当时觉得能交差就好,但过几天回头看,就完全不知道自己到底改了什么。后来才发现 Git 提交信息其实也有一套规范,写清楚能带来很多好处: - 方便自己回顾项目历史,快速定位问题 - 团队协作时能让别人一眼看懂改动内容 - 结合自动化工具(如 changelog 生成、版本管理)时更有用 所以我整理了一份适合学生、个人项目使用的 **简易版 Git 提交规范**,以后就按照这个来写。 ## 提交信息格式 提交信息的基本格式如下: ```Summary (): ``` - **type**:提交类型,说明这次改动属于哪一类 - **scope**:作用范围,比如是某个页面、模块,写不写都行 - **subject**:一句话简洁描述改动内容,不超过 50 个字 > 例子: > > feat(Login): 新增记住密码功能 > > fix(Article): 修复文章列表分页错误 > ## 常见类型 这里整理了一个常见的 **type 对照表**,不仅有前面提到的几种,还补充了一些团队里常用的类型,方便对照记忆。 | 类型 | 说明 | 示例 | | ------------ | ------------------------------------------------ | ----------------------------------- | | **feat** | 新功能(feature) | `feat(Comments): 添加评论回复功能` | | **fix** | 修复 bug | `fix(Navbar): 修复导航栏跳转错误` | | **docs** | 文档修改 | `docs(README): 更新项目启动说明` | | **style** | 代码样式修改,不影响逻辑(空格、缩进、格式化等) | `style(App): 调整代码缩进` | | **refactor** | 重构,优化代码但不影响功能 | `refactor(utils): 简化工具函数` | | **test** | 测试相关修改 | `test(api): 添加登录接口的单元测试` | | **perf** | 性能优化(performance) | `perf(List): 优化列表渲染性能` | | **build** | 构建相关修改(webpack、vite、npm 等) | `build: 升级依赖到最新版本` | | **ci** | 持续集成相关配置 | `ci: 修改 GitHub Actions 测试脚本` | | **chore** | 杂项,不影响代码逻辑(构建流程、依赖管理) | `chore: 更新 eslint 配置` | | **revert** | 回滚之前的提交 | `revert: 回滚 Login 模块的修改` | ## 示例提交 ### 新增功能 ```Summary feat(Home): 新增骨架屏组件 feat(Articles): 添加文章点赞功能 feat(Comments): 实现评论系统 ``` ### 修复问题 ```Summary fix(Articles): 修复文章 ID 类型错误 fix(Login): 解决登录页面样式问题 fix(Navbar): 修复导航栏显示 bug ``` ### 样式调整 ```Summary style(global): 调整全局字体大小 style(Article): 优化文章页面布局 style(Button): 改进按钮样式 ``` ### 代码重构 ```Summary refactor(api): 重构 API 调用逻辑 refactor(components): 优化组件结构 refactor(utils): 简化工具函数 ``` ### 文档更新 ```Summary docs(README): 更新项目说明 docs(components): 添加组件文档 docs(api): 完善 API 文档 ``` ### 性能优化 ```Summary perf(List): 使用虚拟滚动提升渲染性能 perf(Image): 优化图片加载策略 ``` ### 构建 & 配置 ```Summary build: 升级 vite 到 v5.0 ci: 调整测试环境 Node.js 版本 chore: 更新依赖,修改 eslint 配置 ``` ### 回滚 ```Summary revert: 回滚 utils 模块的重构 ``` ## 编写规则总结 1. 提交信息用 **现在时态**(写“add”,不要写“added”) 2. **第一行简短**,控制在 50 字以内,重点说明改了什么 3. 不要写模糊词,比如 “update”、“change”,要具体 4. 有明确模块就写 scope,没有可以省略 5. 每次提交尽量保持单一目的(一个提交只做一件事) ## 我的实际记录示例 在自己的项目里,我会这样写提交信息: ```Summary feat: 新增博客文章页面 feat: 添加文章目录导航 feat: 实现点赞功能 fix: 修复移动端显示问题 style: 调整文章字体样式 refactor: 优化组件结构 perf: 使用懒加载优化首页性能 docs: 更新 README 添加部署说明 chore: 升级依赖到最新版本 ``` ## 小结 Git 提交规范看似小事,但能大大提升代码可维护性。 哪怕是自己写的小项目,坚持用规范写提交信息,过几个月回头再看也能马上回忆起当时做了什么,而不是面对一堆“update”一脸懵。 对我来说,这更像是一个好习惯的养成。以后写提交时,就按照上面的表格来,不仅让自己更清晰,也能让别人看懂。
  •  

Node.js 网络模块(http)详解

## 一、概述 在 Node.js 中,`http` 模块是最核心的内置模块之一,它允许开发者快速搭建 HTTP 服务器或发起 HTTP 请求。 无论是本地调试 API,还是在生产环境中支撑一个 Web 服务,几乎所有 Node.js 项目都会直接或间接依赖 `http` 模块。 与浏览器的 `fetch` 或其他 HTTP 客户端不同,Node.js 的 `http` 模块提供了**更底层的接口**: - 可以创建一个 **HTTP 服务器**,监听端口,接收并处理客户端请求; - 可以作为 **HTTP 客户端**,向远程服务器发起请求; - 提供对 **请求(IncomingMessage)** 和 **响应(ServerResponse)** 的底层控制权,例如分块传输、流式处理等。 由于其是底层 API,`http` 模块通常作为 Express、Koa、NestJS 等框架的基础,但理解它的工作原理仍然非常重要。 ## 二、HTTP 服务器 ### 2.1 创建服务器 使用 `http.createServer` 可以快速创建一个 HTTP 服务器: ```javascript const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello, world!'); }); ``` - **参数**: `createServer` 接收一个回调函数 `(req, res)`,当有新的请求进入时,这个回调就会被执行。 - `req` 是 `http.IncomingMessage` 实例,表示请求对象。 - `res` 是 `http.ServerResponse` 实例,表示响应对象。 - **返回值**: 返回一个 `http.Server` 实例,它是一个事件驱动的对象,后续可以通过 `listen` 方法启动监听。 这种模式非常适合快速开发 Demo 或简单服务。但在复杂应用中,通常会把请求处理逻辑拆分出来,避免 `createServer` 回调函数里代码过于臃肿。 ------ ### 2.2 端口绑定与监听 创建服务器之后,还需要调用 `server.listen` 来让它真正运行起来。这个方法告诉操作系统:“请在某个端口等待请求,并交给我处理”。 常见的用法有三种: 1. **指定端口监听(最常见)** ```javascript server.listen(3000, () => { console.log('Server is running at http://localhost:3000'); }); ``` - 这里 `3000` 是端口号。 - 回调函数会在服务成功启动时触发。 - 省略 `hostname` 参数时,服务器会监听所有可用的网络接口(包括 `localhost` 和外网 IP)。 > 实际场景:在开发环境中,我们通常监听 `localhost:3000`;在生产环境,很多时候服务器只监听一个内部端口(如 `127.0.0.1:8080`),然后由 Nginx 或其他反向代理转发外部请求。 2. **监听本地回环地址** ```javascript server.listen(8080, '127.0.0.1'); ``` - 这样服务器只会接受来自本机的请求,不会对外暴露。 - 适用于本地开发、测试,或者只需要被同一台机器上的其他服务调用的场景。 3. **通过 UNIX 域套接字监听**(Linux/Unix 环境) ```javascript server.listen('/tmp/server.sock'); ``` - 这里 `path` 是一个文件路径,表示使用 UNIX 域套接字进行通信。 - 适用于高性能 IPC(进程间通信)或与 Nginx 的本地 socket 转发结合使用。 - 注意:需要保证路径有权限写入,否则会抛出错误。 ## 三、请求对象(IncomingMessage) 当客户端发起请求时,`http.createServer` 回调中的第一个参数 `req` 就是一个 `http.IncomingMessage` 实例,它封装了请求的所有信息,包括请求方法、URL、头信息以及请求体。 ### 3.1 请求方法与 URL `req.method` 和 `req.url` 是最基础的两个属性: ```javascript const http = require('http'); http.createServer((req, res) => { console.log('Request Method:', req.method); console.log('Request URL:', req.url); res.end('Check console for request details'); }).listen(3000); ``` - **req.method**:表示 HTTP 方法,如 `GET`、`POST`、`PUT`、`DELETE` 等。 - **req.url**:表示请求的路径,包括查询参数(query string)。 > 注意:它并不是完整的 URL,通常会结合 `URL` 类或第三方库(如 `querystring`、`url`)解析参数。 **应用场景**: - 根据 `req.method` 做不同路由处理,例如 `GET /users` 返回用户列表,`POST /users` 创建新用户。 - 使用 `req.url` 解析路径和查询参数,实现路由匹配。 ------ ### 3.2 请求头(Headers) `req.headers` 是一个对象,包含客户端发送的所有 HTTP 头信息: ```javascript console.log(req.headers['user-agent']); // 浏览器信息 console.log(req.headers['content-type']); // 请求内容类型 ``` - **注意事项**:HTTP 头的键名总是小写,无论客户端怎么发送都统一小写。 - **常用头**: - `content-type`:指示请求体类型,如 `application/json`、`application/x-www-form-urlencoded`。 - `authorization`:常用于携带 JWT 或 Token。 - `cookie`:客户端 Cookie 信息。 **应用场景**: - 判断请求类型决定如何解析请求体; - 提取鉴权信息进行身份验证。 ------ ### 3.3 请求体(Body)处理 对于 `POST`、`PUT` 等方法,请求体可能包含数据。Node.js HTTP 模块将请求体作为 **流(Stream)** 处理,而不是一次性读取。 ```javascript let body = ''; req.on('data', chunk => { body += chunk; // 逐块累加数据 }); req.on('end', () => { console.log('Received body:', body); res.end('Request processed'); }); ``` - **流式处理**的优势: - 支持大文件上传,不会一次性占用大量内存; - 可以在接收数据时就进行处理(如实时写入数据库或文件)。 - **注意事项**: - `data` 事件可能触发多次,每次接收到的是 Buffer,需要根据 `content-type` 转换; - `end` 事件表示整个请求体已接收完毕。 **实践建议**: - 对于 JSON 数据,可在 `end` 中调用 `JSON.parse`; - 对于文件上传,通常结合第三方库(如 `multer`、`busboy`)处理 multipart/form-data。 ------ ### 3.4 高级属性 - `req.httpVersion`:请求使用的 HTTP 版本(如 `1.1`、`2.0`)。 - `req.socket`:底层 TCP 套接字,可以获取客户端 IP、端口等。 - `req.rawHeaders`:原始头信息数组,保持客户端发送顺序。 **应用场景**: - 日志记录客户端 IP 或请求来源; - 在代理或负载均衡场景下,分析原始请求头。 ## 四、响应对象(ServerResponse) `http.createServer` 回调函数的第二个参数 `res` 是一个 `http.ServerResponse` 实例,用于构建并发送 HTTP 响应。理解 `res` 的工作方式,对于正确返回数据、设置状态码和响应头至关重要。 ### 4.1 设置状态码 HTTP 状态码是服务器告诉客户端请求结果的标准方式。Node.js 提供了多种方式设置状态码: ```javascript res.statusCode = 200; // 直接赋值 res.statusMessage = 'OK'; // 可选,自定义状态消息 res.end('Response body'); ``` - **statusCode**:必须设置,默认为 200(成功)。 - **statusMessage**:可选,如果不设置,Node.js 会根据 `statusCode` 自动匹配标准文本。 **常用状态码示例**: - 200 OK:请求成功 - 301 Moved Permanently:永久重定向 - 400 Bad Request:客户端请求错误 - 404 Not Found:请求资源不存在 - 500 Internal Server Error:服务器内部错误 **场景**: - 根据业务逻辑返回不同状态码,例如验证失败返回 401,资源未找到返回 404。 - 与前端框架结合时,状态码可以用于条件渲染或错误提示。 ------ ### 4.2 设置响应头 响应头用于描述响应的元信息,如内容类型、缓存策略、CORS 等。可以使用 `setHeader` 方法: ```javascript res.setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'no-cache'); res.end(JSON.stringify({ message: 'Hello World' })); ``` - **注意事项**: - 必须在调用 `res.write()` 或 `res.end()` 前设置; - 同一个头字段调用多次会覆盖之前的值,可使用 `res.getHeader()` 检查。 **常用响应头**: - `Content-Type`:指定响应体类型,如 `text/plain`、`application/json`、`text/html`。 - `Content-Length`:可选,告知客户端响应体字节长度;不设置 Node.js 会自动计算。 - `Set-Cookie`:用于发送 Cookie 给客户端。 - `Access-Control-Allow-Origin`:处理跨域请求。 ------ ### 4.3 发送响应体 最常见的发送方式是 `res.end()`,它既可以设置响应体,也会结束响应: ```javascript res.end('Hello World'); // 发送文本 res.end(JSON.stringify({ ok: true })); // 发送 JSON ``` - 如果响应体较大,或需要分块发送,可以使用 `res.write()` + `res.end()`: ```javascript res.write('Part 1\n'); res.write('Part 2\n'); res.end('End'); ``` - **应用场景**: - 流式处理大文件或长轮询; - 分块传输 HTML 模板或日志数据; - 与 `fs.createReadStream()` 配合,实现文件下载。 ------ ### 4.4 响应流(Stream)与管道 由于 `res` 继承自 `Writable Stream`,可以直接将文件流或其他可读流管道输出: ```javascript import fs from 'fs'; const stream = fs.createReadStream('./large-file.txt'); stream.pipe(res); ``` - **优势**: - 不需要一次性将整个文件读入内存,节省内存开销; - 对大文件传输和实时数据推送非常高效。 **注意事项**: - 管道会自动处理 `res.end()`,无需手动调用; - 出现错误时要监听 `error` 事件,否则可能导致服务器崩溃。 ------ ### 4.5 重定向 通过设置状态码和 `Location` 响应头可以实现 HTTP 重定向: ```javascript res.statusCode = 301; res.setHeader('Location', 'https://example.com'); res.end(); ``` - **301**:永久重定向 - **302**:临时重定向 - **应用场景**: - URL 变更后保持 SEO; - 登录验证后跳转到目标页面。 ## 五、HTTP 客户端请求 Node.js 的 `http` 模块不仅可以创建服务器,还可以向远程服务器发起 HTTP 请求。这对于实现 API 调用、微服务通信或数据抓取非常重要。核心方法是 `http.request` 和 `http.get`。 ### 5.1 http.request `http.request` 是最底层、功能最强大的客户端方法,用于发起任意 HTTP 请求(GET、POST、PUT 等)。 ```javascript import http from 'http'; const options = { hostname: 'example.com', port: 80, path: '/api/data', method: 'POST', headers: { 'Content-Type': 'application/json' } }; const req = http.request(options, res => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { console.log('Response:', data); }); }); req.on('error', err => { console.error('Request error:', err); }); req.write(JSON.stringify({ name: 'Node.js' })); req.end(); ``` - **options 解释**: - `hostname`:远程服务器域名或 IP 地址 - `port`:端口,HTTP 默认 80,HTTPS 默认 443 - `path`:请求路径,可包含查询参数 - `method`:请求方法,如 `GET`、`POST` - `headers`:自定义请求头 - **req.write() / req.end()**: - 对于带请求体的请求(如 POST),需要调用 `req.write()` 写入数据,再用 `req.end()` 结束请求; - 对于 GET 请求,通常直接调用 `req.end()` 即可。 **应用场景**: - 调用第三方 API; - 服务间数据交互; - 上传数据到远程服务器。 **注意事项**: - 必须监听 `error` 事件,否则出现网络错误会导致进程崩溃; - 对于 HTTPS 请求,应使用 `https` 模块(Node.js 内置)。 ------ ### 5.2 http.get `http.get` 是 `http.request` 的简化版本,专门用于发起 GET 请求,内部已经自动调用了 `req.end()`。 ```javascript http.get('http://example.com/api/data', res => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { console.log('GET Response:', data); }); }).on('error', err => { console.error('GET request error:', err); }); ``` - **优点**: - 简化了 GET 请求流程,无需手动调用 `req.end()` - 更适合快速抓取数据或读取简单 API - **注意事项**: - 如果需要发送请求体或自定义方法(POST、PUT 等),仍然需要使用 `http.request`; - 需要处理数据流,否则响应体过大可能导致内存问题。 ------ ### 5.3 响应事件 客户端请求返回的数据是一个 **流(IncomingMessage)**,支持标准事件: - `data`:接收响应数据块 - `end`:响应接收完毕 - `error`:网络或协议错误 - `aborted`:请求被中止 **示例:使用事件组合处理响应** ```javascript req.on('response', res => { console.log('Status:', res.statusCode); res.on('data', chunk => process.stdout.write(chunk)); res.on('end', () => console.log('\nRequest finished')); }); ``` **应用场景**: - 流式读取大文件或图片数据; - 实时处理数据流,如日志收集或 SSE(Server-Sent Events); - 捕获错误、超时等异常情况,提高客户端健壮性。 ------ ### 5.4 超时与错误处理 HTTP 客户端请求可能遇到网络延迟或远程服务器异常,因此需要设置超时: ```javascript const req = http.request(options, res => { /* ... */ }); req.setTimeout(5000, () => { console.error('Request timed out'); req.abort(); }); req.on('error', err => console.error('Request error:', err)); req.end(); ``` - **setTimeout**:指定毫秒数超时 - **req.abort()**:超时或需要中断请求时调用 - **错误监听**:防止未捕获异常导致 Node.js 进程退出 **实践建议**: - 为所有客户端请求设置合理超时; - 使用 `try/catch` 或错误事件捕获,确保服务稳定; - 对高并发请求,可结合连接池或 `keep-alive` 提升效率。 ## 六、HTTPS 与安全通信 Node.js 提供了 `https` 模块,它与 `http` 模块接口类似,但支持 **TLS/SSL** 加密通信,用于保护数据在网络传输中的安全性。HTTPS 是现代 Web 的标准通信方式,尤其在涉及用户信息、支付或敏感数据时必不可少。 ### 6.1 创建 HTTPS 服务器 HTTPS 服务器需要 **证书(cert)和私钥(key)**。创建方式与 HTTP 类似,但需要引入 `https` 模块并提供安全配置: ```javascript import https from 'https'; import fs from 'fs'; const options = { key: fs.readFileSync('./private-key.pem'), cert: fs.readFileSync('./certificate.pem') }; const server = https.createServer(options, (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello, Secure World!'); }); server.listen(443, () => { console.log('HTTPS server running at https://localhost'); }); ``` - **options.key**:服务器私钥,用于解密客户端发送的加密信息 - **options.cert**:公钥证书,用于向客户端证明服务器身份 - **场景**: - 任何涉及敏感信息的服务,如用户登录、支付接口; - 强制全站 HTTPS,提高安全性和 SEO 优势 **注意事项**: - 在生产环境中通常使用由受信任 CA 签发的证书,而不是自签名证书; - 可以结合 Nginx 或负载均衡器做证书管理,Node.js 服务器仅监听内部端口。 ------ ### 6.2 HTTPS 客户端请求 HTTPS 客户端请求与 HTTP 类似,但使用 `https.request` 或 `https.get`: ```javascript import https from 'https'; https.get('https://api.example.com/data', res => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { console.log('Response:', data); }); }).on('error', err => { console.error('HTTPS request error:', err); }); ``` - **优点**: - 数据在传输过程中加密,防止被窃听或篡改 - 支持证书验证,保障服务器身份真实性 - **场景**: - 调用安全 API,如支付网关、第三方登录服务; - 爬取 HTTPS 网站的数据 ------ ## 七、HTTP/2 支持 HTTP/2 是 HTTP 协议的升级版,引入了多路复用、头压缩和服务器推送,显著提高性能。Node.js 提供 `http2` 模块用于创建 HTTP/2 服务器和客户端。 ### 7.1 HTTP/2 服务器 ```javascript import http2 from 'http2'; import fs from 'fs'; const server = http2.createSecureServer({ key: fs.readFileSync('./private-key.pem'), cert: fs.readFileSync('./certificate.pem') }); server.on('stream', (stream, headers) => { stream.respond({ ':status': 200 }); stream.end('Hello HTTP/2'); }); server.listen(8443, () => { console.log('HTTP/2 server running at https://localhost:8443'); }); ``` - **核心特点**: - `stream` 事件替代传统 `request` 回调,每个请求对应一个流对象 - 可以实现**多路复用**,一个 TCP 连接上同时传输多个请求/响应 - 支持**服务器推送**,主动向客户端发送资源,减少延迟 **场景**: - 高并发 Web 服务,提高性能和资源加载速度; - 结合 HTTPS 使用,提升安全性和性能。 ------ ### 7.2 HTTP/2 客户端请求 ```javascript import http2 from 'http2'; const client = http2.connect('https://localhost:8443'); const req = client.request({ ':path': '/' }); let data = ''; req.on('data', chunk => { data += chunk; }); req.on('end', () => { console.log('HTTP/2 Response:', data); client.close(); }); req.end(); ``` - **特点**: - 使用 `http2.connect` 建立客户端连接,复用 TCP 连接 - 请求通过 `client.request()` 发起,响应数据通过流式事件接收 - **注意事项**: - 必须使用 TLS 加密连接,否则只能使用 HTTP/2 的明文版本(h2c),不常见 - 适合高性能微服务或前后端通信 ## 八、HTTP 实用技巧与最佳实践 在实际 Node.js 开发中,仅仅掌握 HTTP API 的基本用法还不够。高性能、稳定、安全的服务器需要注意连接管理、请求体处理、响应优化和错误容忍。 ### 8.1 连接复用(Keep-Alive) HTTP 默认每次请求都会创建新的 TCP 连接,这会带来性能开销。使用 **Keep-Alive** 可以在同一连接上复用多个请求,提高吞吐量: ```javascript const http = require('http'); const options = { hostname: 'example.com', port: 80, path: '/api/data', method: 'GET', headers: { 'Connection': 'keep-alive' } }; const req = http.request(options, res => { console.log('Status:', res.statusCode); }); req.end(); ``` - **优点**: - 减少 TCP 握手次数,降低延迟 - 更适合频繁访问同一服务的微服务或前端 API 调用 - **注意事项**: - Keep-Alive 连接可能被防火墙或代理中断,需要处理异常 - 对于长时间未使用的连接,可设置服务器或客户端超时策略 ------ ### 8.2 超时与重试策略 网络环境复杂,请求可能超时或失败,需要合理设置超时与重试机制: ```javascript req.setTimeout(5000, () => { console.error('Request timed out'); req.abort(); }); req.on('error', err => console.error('Request failed:', err)); ``` - **应用场景**: - 生产环境 API 调用,确保异常不会导致进程崩溃 - 对高可用服务,结合重试策略减少请求失败率 **最佳实践**: - 对客户端请求设置合理超时(通常 3~10 秒); - 对重要业务请求,可增加指数退避(exponential backoff)重试机制; - 错误日志记录详细信息,便于定位问题。 ------ ### 8.3 大文件流处理 处理大文件时,避免一次性读取或写入内存,可使用流(Stream)方式: ```javascript import fs from 'fs'; import http from 'http'; http.createServer((req, res) => { const readStream = fs.createReadStream('./large-file.zip'); readStream.pipe(res); // 流式传输,自动处理 backpressure }).listen(3000); ``` - **优势**: - 节约内存,支持任意大小文件传输 - 实时传输,提高响应速度 - **注意事项**: - 流中可能出现错误,需要监听 `error` 事件 - 对于压缩或加密文件,可能需要在流中处理变换(Transform Stream) ------ ### 8.4 响应压缩 为了减小传输数据量,提高页面或 API 响应速度,可使用 gzip 或 Brotli 压缩: ```javascript import http from 'http'; import zlib from 'zlib'; http.createServer((req, res) => { res.writeHead(200, { 'Content-Encoding': 'gzip' }); const gzip = zlib.createGzip(); gzip.pipe(res); gzip.end('Hello compressed world!'); }).listen(3000); ``` - **应用场景**: - 返回大量文本、JSON 或 HTML 内容 - 提升 Web 性能,减少带宽占用 - **注意事项**: - 二进制文件无需压缩,可能反而增大 - 可以结合中间件(如 Express 的 `compression`)自动处理 ------ ### 8.5 CORS 与安全策略 对于跨域请求,需要设置响应头来允许访问: ```javascript res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); ``` - **应用场景**: - 前后端分离项目 - 微服务架构中不同域名间的 API 调用 - **安全建议**: - 尽量限定允许的域名,不要使用 `*` 作为生产策略 - 配合身份验证、Token 或 Cookie 保证安全 ------ ### 8.6 日志与监控 HTTP 服务通常需要记录访问日志和监控状态: ```javascript http.createServer((req, res) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); res.end('Logged'); }).listen(3000); ``` - **重要性**: - 方便调试和定位问题 - 用于统计流量、监控性能 - **推荐做法**: - 结合第三方库(如 `morgan`、`pino`)实现结构化日志 - 生产环境可接入监控系统,如 Prometheus、Grafana ## 九、HTTP 模块方法总结 | 操作 | 方法 | 参数 & 说明 | 使用场景 | | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------ | | **创建 HTTP 服务器** | `http.createServer([options], requestListener)` | `requestListener` 回调函数接收 `(req, res)`,`options` 可配置 `IncomingMessage` 和 `ServerResponse` 行为 | 快速搭建 HTTP 服务,处理请求和响应 | | **监听端口** | `server.listen(port[, hostname][, backlog][, callback])` `server.listen(path[, callback])` | `port`:监听端口 `hostname`:绑定地址 `backlog`:连接队列长度 UNIX 域套接字可使用 path | 开启服务器监听,支持端口或 Unix Socket | | **关闭服务器** | `server.close([callback])` | 停止接收新请求,回调在关闭完成后执行 | 用于优雅退出服务或单元测试 | | **HTTP 请求** | `http.request(options[, callback])` | `options`:hostname, port, path, method, headers 返回 `ClientRequest` | 发起 GET/POST/PUT 等请求,支持请求体和自定义方法 | | **简化 GET 请求** | `http.get(options[, callback])` | 内部自动调用 `req.end()`,用于 GET 请求 | 快速抓取数据或访问 API | | **请求写入** | `req.write(chunk[, encoding])` | 向请求体写入数据,常用于 POST | 发送请求体数据,如 JSON 或表单 | | **结束请求** | `req.end([data[, encoding]])` | 结束请求,发送最后的数据 | 必须调用结束请求,确保服务器接收完整 | | **请求事件** | `req.on('response', callback)` `req.on('error', callback)` | `response`:获取响应对象 `error`:捕获请求异常 | 流式处理响应和错误处理 | | **响应状态码** | `res.statusCode` / `res.statusMessage` | 设置 HTTP 状态码和状态消息 | 控制请求结果,如 200、404、500 | | **设置响应头** | `res.setHeader(name, value)` | 在 `res.write()` 或 `res.end()` 前设置 | 设置 Content-Type、Cache-Control、CORS 等 | | **发送响应体** | `res.write(chunk)` + `res.end([chunk])` | 分块发送响应,可组合流式输出 | 大文件传输或分块数据处理 | | **管道输出** | `readableStream.pipe(res)` | 将可读流直接发送给客户端 | 文件下载、实时数据流输出 | | **重定向** | `res.statusCode = 301/302` + `res.setHeader('Location', url')` | 设置重定向状态码和 Location | URL 重写、登录跳转、资源搬迁 | | **HTTPS 服务器** | `https.createServer(options, requestListener)` | `options`:key, cert, ca 等安全配置 | 安全通信,保护敏感数据 | | **HTTP/2 服务器** | `http2.createSecureServer(options)` | `options`:key, cert 等 | 支持多路复用、服务器推送,提高性能 | | **HTTP/2 客户端** | `http2.connect(authority)` + `client.request(headers)` | 建立连接并发起请求,返回可读流 | 高性能微服务间通信 | | **超时控制** | `req.setTimeout(ms, callback)` | 超时触发回调并可调用 `req.abort()` | 避免长时间阻塞,提高服务健壮性 | | **监听请求事件** | `res.on('data', callback)` `res.on('end', callback)` `res.on('error', callback)` | 流式处理响应数据 | 处理大文件、实时数据、错误捕获 | | **响应压缩** | `zlib.createGzip()` / `zlib.createBrotliCompress()` | 与流管道结合发送压缩内容 | 提升网络传输效率,减少带宽占用 | ------ ### 总结说明 - **服务端**:`http.createServer` + `server.listen` 是基础搭建方式;`res` 提供状态码、响应头、响应体、流式输出和重定向。 - **客户端**:`http.request` / `http.get` 支持自定义请求方法、请求体、事件处理和错误捕获;HTTPS 与 HTTP/2 提供加密和高性能特性。 - **高级实践**:连接复用、超时控制、大文件流、响应压缩、CORS、安全配置、日志监控是生产环境必备技巧。 ## 十、HTTP 模块常见场景实战示例合集 以下示例展示了 Node.js HTTP 模块在实际开发中的典型用法,从服务器搭建、API 调用到大文件传输与压缩响应。 ------ ### 10.1 创建基础 HTTP 服务器 ```javascript import http from 'http'; const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello, Node.js HTTP!'); }); server.listen(3000, '127.0.0.1', () => { console.log('Server listening on http://127.0.0.1:3000'); }); ``` **说明**: - 适用于快速搭建本地测试服务 - 通过 `writeHead` 设置响应头,`end` 发送响应体 ------ ### 10.2 HTTP 客户端请求调用 API ```javascript import http from 'http'; const options = { hostname: 'jsonplaceholder.typicode.com', port: 80, path: '/todos/1', method: 'GET' }; const req = http.request(options, res => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { console.log('API Response:', data); }); }); req.on('error', err => console.error('Request error:', err)); req.end(); ``` **说明**: - 发起 GET 请求获取 JSON 数据 - 流式处理响应,避免内存占用过大 ------ ### 10.3 HTTPS 服务器示例 ```javascript import https from 'https'; import fs from 'fs'; const options = { key: fs.readFileSync('./private-key.pem'), cert: fs.readFileSync('./certificate.pem') }; const server = https.createServer(options, (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello, Secure HTTPS!'); }); server.listen(443, () => { console.log('HTTPS server running at https://localhost'); }); ``` **说明**: - 用于保护敏感数据传输 - 需要正确配置证书和私钥 - 常用于生产环境与支付、登录等安全场景 ------ ### 10.4 HTTP/2 服务器与多路复用 ```javascript import http2 from 'http2'; import fs from 'fs'; const server = http2.createSecureServer({ key: fs.readFileSync('./private-key.pem'), cert: fs.readFileSync('./certificate.pem') }); server.on('stream', (stream, headers) => { stream.respond({ ':status': 200 }); stream.end('Hello HTTP/2, multiple streams!'); }); server.listen(8443, () => { console.log('HTTP/2 server running at https://localhost:8443'); }); ``` **说明**: - 每个请求对应一个流 (`stream`) - 支持多路复用,提升高并发性能 - 支持服务器推送,可主动发送资源 ------ ### 10.5 大文件流传输示例 ```javascript import http from 'http'; import fs from 'fs'; http.createServer((req, res) => { const fileStream = fs.createReadStream('./large-file.zip'); res.writeHead(200, { 'Content-Type': 'application/zip' }); fileStream.pipe(res); fileStream.on('error', err => { console.error('File read error:', err); res.statusCode = 500; res.end('Internal Server Error'); }); }).listen(3000, () => { console.log('File streaming server running on port 3000'); }); ``` **说明**: - 避免一次性加载整个文件到内存 - 支持大文件下载或流式传输 ------ ### 10.6 响应压缩示例 ```javascript import http from 'http'; import zlib from 'zlib'; http.createServer((req, res) => { res.writeHead(200, { 'Content-Encoding': 'gzip' }); const gzip = zlib.createGzip(); gzip.pipe(res); gzip.end('This is compressed response using gzip.'); }).listen(3000, () => { console.log('Compressed server running on port 3000'); }); ``` **说明**: - 对文本或 JSON 响应进行压缩,提高传输效率 - 可结合流处理,实现大文件压缩传输 ------ ### 10.7 API 代理 / 跨域示例(CORS) ```javascript import http from 'http'; http.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST'); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: 'CORS enabled API' })); }).listen(3000, () => { console.log('CORS API server running on port 3000'); }); ``` **说明**: - 支持前后端分离项目跨域访问 - 生产环境应限制允许域名,确保安全 ------ ### 10.8 日志与监控示例 ```javascript import http from 'http'; http.createServer((req, res) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Logged request!'); }).listen(3000, () => { console.log('Server with logging running on port 3000'); }); ``` **说明**: - 记录访问时间、方法和路径 - 结合日志库可实现结构化日志与监控 - 是生产环境常用的运维手段 ## 结语 Node.js 的 `http` 模块是构建服务端应用的核心基础,它提供了完整的服务器与客户端 API,使开发者能够灵活地创建 HTTP/HTTPS 以及 HTTP/2 服务。通过掌握 `createServer`、`request`、流式处理、事件监听等机制,可以处理大规模并发请求、传输大文件、实现压缩响应和跨域访问。结合生产环境实践,如 Keep-Alive 连接复用、超时与重试策略、日志监控和安全配置,开发者能够构建高性能、可靠且安全的网络服务。掌握该模块不仅有助于理解 Node.js 异步 I/O 与事件驱动模型,也为进一步使用 Express、Koa 等框架打下坚实基础,使整个服务开发流程从请求接收、数据处理到响应发送都能高效、可控。 如果想要了解更多Node.js,可以点击下方链接⬇️ ```meta title: Node.js url: https://github.com/nodejs/examples image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/33e3f444-6a9e-4077-95b5-e480e6f66a5d.png desc: Node.js 官方示例 ```
  •  

Node.js 文件系统模块(fs)详解

## 概述 在 Node.js 应用开发中,`fs` 模块是使用频率极高的核心模块之一,它提供了访问操作系统文件系统的 API。无论是 Web 服务端加载配置文件、日志写入,还是命令行工具扫描目录,几乎都会用到 `fs`。 与大多数异步 API 一样,Node.js 文件操作有三种调用形式: - **异步回调(Callback)**:早期 Node.js 的默认风格,适合简单任务,但复杂逻辑会导致“回调地狱”。 - **同步(Sync)**:以 `Sync` 结尾的方法会阻塞事件循环,一般只在应用初始化阶段使用。 - **Promise**:`fs/promises` 提供了现代异步接口,推荐在生产环境中使用 `async/await`,提高可读性。 在使用 `fs` 时,路径拼接也非常重要,跨平台时推荐使用 `path` 模块,而不是手写斜杠(`/` 或 `\`)。 ## 1. 文件操作 文件操作是 `fs` 模块最常见的使用场景,包括读取、写入、追加、复制和删除等。 ### 1.1 读取文件(Read) 读取文件是开发中最基本的需求之一: - 读取配置文件(如 `config.json`); - 加载模板文件; - 读取日志内容; - 处理图片、视频等二进制文件。 Node.js 提供三种方式: - `fs.readFile(path[, options], callback)` - `fs.readFileSync(path[, options])` - `fs.promises.readFile(path[, options])` 常见参数: - `encoding`: 文本文件常用 `'utf8'`,二进制文件保持 `null`,返回 Buffer。 - `flag`: 文件打开方式,默认 `'r'`(只读)。 **注意事项**: - 对于大文件,`readFile` 会一次性读入内存,可能造成内存压力。推荐使用 **流(stream)** 来处理大文件。 - 异步方式优先于同步方式,除非是在初始化时必须阻塞。 **示例** ```js import { readFile } from 'fs/promises'; async function readFileExample() { try { const data = await readFile('./file.txt', { encoding: 'utf8' }); console.log(data); } catch (err) { console.error('Error reading file:', err); } } readFileExample(); ``` ### 1.2 写入文件(Write) 写入文件常用于生成日志、缓存、导出报表等。 - `fs.writeFile(file, data[, options], callback)` - `fs.writeFileSync(file, data[, options])` - `fs.promises.writeFile(file, data[, options])` 常见选项: - `encoding`: 默认 `'utf8'`。 - `mode`: 文件权限,默认 `0o666`(可读写)。 - `flag`: 默认 `'w'`,覆盖写入。 **注意事项**: - 如果文件不存在,会自动创建。 - 覆盖写入可能导致数据丢失,日志场景建议使用 `appendFile`。 - 可使用 `flag: 'wx'` 保证只有文件不存在时才写入。 ### 1.3 追加内容(Append) 日志写入、记录用户行为时,通常选择追加写: - `fs.appendFile(path, data[, options], callback)` - `fs.appendFileSync(path, data[, options])` - `fs.promises.appendFile(path[, options])` **注意事项**: - 内部默认 `flag: 'a'`,即在文件末尾追加。 - 高并发日志写入时,建议使用日志库(如 `winston`)封装。 ### 1.4 打开文件(Open)与文件描述符操作 `fs.open` 提供对文件的低层级控制,可以获取文件描述符 `fd`,再通过 `fs.read`、`fs.write`、`fs.close` 等操作。 适合的场景包括: - 只读取或修改文件的某一部分; - 实现大文件分块处理; - 构建数据库式的二进制存储格式。 **注意事项**: - 打开文件后,必须记得调用 `fs.close` 关闭,否则可能导致文件句柄泄漏。 - 现代 Node.js 开发中,除非需要精细控制,否则一般直接用 `readFile` 和 `writeFile` 即可。 ### 1.5 复制文件(Copy) 文件复制是常见的文件管理操作。 - `fs.copyFile(src, dest[, mode], callback)` - `fs.promises.copyFile(src, dest[, mode])` **注意事项**: - 默认会覆盖目标文件。 - 如果希望在目标文件存在时报错,可以传 `fs.constants.COPYFILE_EXCL`。 ### 1.6 删除文件(Unlink) 删除文件使用 `unlink`: - `fs.unlink(path, callback)` - `fs.promises.unlink(path)` 注意不要误用在目录上(删除目录请使用 `rmdir` 或 `rm`)。 ## 2. 目录操作 Node.js 也提供了目录相关的操作 API,包括创建、读取和删除。 ### 2.1 创建目录(Mkdir) - `fs.mkdir(path[, options], callback)` - `fs.promises.mkdir(path[, options])` 常见场景: - 创建临时缓存目录; - 搭建项目时批量生成目录结构。 常见选项: - `recursive`: 递归创建目录。 - `mode`: 目录权限,默认 `0o777`。 ### 2.2 读取目录(Readdir) - `fs.readdir(path[, options], callback)` - `fs.promises.readdir(path[, options])` 选项: - `withFileTypes: true` 返回 `Dirent[]`,可以判断每一项是文件还是目录。 常见用途: - 遍历目录下的所有文件; - 实现批量文件处理工具。 ### 2.3 删除目录(Rmdir / Rm) - `fs.rmdir` 仅支持删除空目录; - `fs.rm`(Node.js v14.14+)支持递归删除目录及内容。 选项: - `recursive`: 删除非空目录。 - `force`: 忽略不存在路径的报错。 ## 3. 文件与目录信息 ### 3.1 文件状态(Stat) - `fs.stat(path[, options], callback)` - `fs.promises.stat(path[, options])` 返回 `Stats` 对象,可以获取: - `stats.isFile()` - `stats.isDirectory()` - `stats.size` 应用场景: - 区分目录与文件; - 检查文件大小(如日志轮转)。 ### 3.2 检查权限(Access) - `fs.access(path[, mode], callback)` - `fs.promises.access(path[, mode])` 常见检查: - `F_OK`: 是否存在 - `R_OK`: 是否可读 - `W_OK`: 是否可写 **最佳实践**: 代替已废弃的 `fs.exists`。 ## 4. 高级操作 ### 4.1 监听文件/目录(Watch) - `fs.watch(filename[, options][, listener])` 应用场景: - 热加载配置文件; - 实现文件同步工具。 注意事项: - 行为在不同操作系统上可能不一致; - 建议生产环境使用 `chokidar`。 ### 4.2 重命名/移动 - `fs.rename(oldPath, newPath, callback)` - `fs.promises.rename(oldPath, newPath)` 可以用来重命名文件,或者移动到新目录。 ### 4.3 链接 - `fs.link`: 硬链接 - `fs.symlink`: 符号链接 ## 常用 API 总览表 | 操作 | 异步回调 | 同步 | Promise | 说明 | | -------- | -------------- | ---------------- | ------------ | ------------- | | 读文件 | `readFile` | `readFileSync` | `readFile` | 读取整个文件 | | 写文件 | `writeFile` | `writeFileSync` | `writeFile` | 覆盖写 | | 追加写 | `appendFile` | `appendFileSync` | `appendFile` | 末尾追加 | | 删文件 | `unlink` | `unlinkSync` | `unlink` | 删除文件 | | 复制文件 | `copyFile` | `copyFileSync` | `copyFile` | 复制 | | 创建目录 | `mkdir` | `mkdirSync` | `mkdir` | 可递归 | | 读目录 | `readdir` | `readdirSync` | `readdir` | 列出内容 | | 删目录 | `rmdir` / `rm` | 同左 | 同左 | 递归删除 | | 获取状态 | `stat` | `statSync` | `stat` | 文件/目录信息 | | 检查权限 | `access` | `accessSync` | `access` | 权限检查 | | 重命名 | `rename` | `renameSync` | `rename` | 重命名/移动 | | 监听 | `watch` | N/A | N/A | 文件变化监控 | ## 最佳实践与总结 - **优先使用 Promise API**,配合 `async/await`。 - **避免使用 Sync API**,除非在初始化阶段。 - **路径操作必须用 `path` 模块**,保证跨平台安全性。 - **大文件处理推荐用流(Stream)**,避免内存溢出。 - **监听文件变化**时,用 `chokidar` 等库替代 `fs.watch`。 - **注意资源释放**:打开的文件记得关闭,防止文件句柄泄漏。 如果想要了解更多Node.js,可以点击下方链接⬇️ ```meta title: Node.js url: https://github.com/nodejs/examples image: https://images-1359353257.cos.ap-beijing.myqcloud.com/images/33e3f444-6a9e-4077-95b5-e480e6f66a5d.png desc: Node.js 官方示例 ```
  •  

Ajax、Axios 与 Fetch 的比较

在前端开发中,客户端与服务器之间的数据交互是必不可少的。无论是传统的页面刷新,还是现代的单页应用(SPA),都依赖异步请求来完成数据的获取与提交。常见的实现方式主要包括三种:基于 `XMLHttpRequest` 的 Ajax、第三方库 Axios 以及原生的 Fetch API。它们都能完成类似的任务,但在使用体验、功能特性和适用场景上却有明显差异。 ## 一、Ajax Ajax 的全称是 *Asynchronous JavaScript and XML*,它并不是某种具体的技术,而是通过 `XMLHttpRequest` 对象来实现异步数据请求的一种方式。Ajax 的最大贡献在于推动了网页的异步交互,使得无需整页刷新即可获取数据,从而改善了用户体验。 - **优点** - 开创了异步请求模式,历史意义重大 - 兼容性极好,几乎所有浏览器都支持 - 能够高度自定义请求流程 - **缺点** - API 设计相对复杂,需要监听 `readyState` 和 `status` - 代码冗长,容易出现“回调地狱” - 没有内置的 Promise 支持,需要手动封装 **示例:** ```js const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/data"); xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { console.log(xhr.responseText); } }; xhr.send(); ``` ## 二、Axios Axios 是一个基于 Promise 的第三方库,本质上是对 `XMLHttpRequest` 的封装。它的目标是让网络请求更易用、更工程化。除了基本的 GET、POST 请求外,它还提供了拦截器、取消请求、自动转换响应数据等特性,因而在 Vue、React 等框架项目中被广泛使用。 - **优点** - 基于 Promise,语法简洁,避免回调地狱 - 请求和响应拦截器,便于统一处理 token、错误提示 - 自动 JSON 数据转换,减少手工操作 - 支持取消请求、设置超时,适合复杂业务场景 - 对老旧浏览器兼容性好 - **缺点** - 需要额外引入依赖,增加包体积 - 封装较多,底层不透明 **示例:** ```js axios.get("/api/data") .then(response => console.log(response.data)) .catch(error => console.error(error)); ``` ## 三、Fetch Fetch 是浏览器原生提供的现代化网络请求 API,它是对 `XMLHttpRequest` 的替代方案。其最大特点是基于 Promise 的语法,更加简洁易读。同时,Fetch 支持流式处理,能更好地应对大文件下载和实时数据场景。 - **优点** - 浏览器原生支持,无需引入额外库 - 基于 Promise,代码清晰,配合 async/await 更加直观 - 支持流式处理,适合大数据传输 - API 设计更现代化,可读性强 - **缺点** - 不会自动 reject HTTP 错误,需要手动判断 `response.ok` - 默认不支持请求取消和超时控制(需结合 `AbortController`) - 对于旧版浏览器需要 polyfill **示例:** ```js fetch("/api/data") .then(response => { if (!response.ok) throw new Error("Network error"); return response.json(); }) .then(data => console.log(data)) .catch(error => console.error(error)); ``` ## 四、对比总结 ### 1. 功能特性对比表 | 特性 | Ajax (`XMLHttpRequest`) | Axios | Fetch (原生 API) | | ------------ | ----------------------- | ------------------------- | -------------------------------- | | API 设计 | 回调风格,代码冗长 | 基于 Promise,语法简洁 | 基于 Promise,语法现代化 | | 错误处理 | 需手动判断 `status` | 自动抛出错误,支持拦截器 | 需手动判断 `response.ok` | | 数据转换 | 手动 `JSON.parse` | 自动处理 JSON | 手动调用 `response.json()` | | 取消请求 | 较为复杂 | 内置支持 | 通过 `AbortController` | | 超时设置 | 需手动实现 | 内置支持 | 需结合 `Promise.race` 或中断机制 | | 浏览器兼容性 | 最佳,老旧浏览器也支持 | 良好,依赖 XMLHttpRequest | 较新浏览器支持,需要 polyfill | | 工程化能力 | 较弱 | 很强,适合中大型项目 | 中等,适合现代项目 | ## 五、总结 Ajax 是异步请求的起点,主要作为理解底层原理的工具。Axios 在工程实践中脱颖而出,凭借简洁的 API 和丰富的扩展能力,成为目前最常用的解决方案。而 Fetch 则是浏览器原生的现代化替代方案,代表了未来的发展方向。选择哪一种方式,取决于项目环境和需求:若强调兼容性和底层控制,可以用 Ajax;若项目复杂且需要统一的请求管理体系,Axios 更合适;若追求现代化、减少依赖,并且目标环境是新浏览器,那么 Fetch 是最佳选择。
  •  

十年之约 | 致未来的自己

致未来的自己

嗨,十年后的我:

  当你读到这段文字时,已经是2035年了。不知道那时的你,是否还记得2025年这个夏天的忐忑与期待?此刻的我,正坐在电脑前敲下这些字,刚刚结束大三的课程,准备踏入大四,一边投递前端实习简历,一边为秋招焦头烂额。

  现在的我,可能还在为一道 LeetCode 题抓耳挠腮,为某个框架的 API 文档熬夜啃读,或者因为一场技术面试紧张到失眠。但我想对你说:别笑当年的自己笨拙,因为每一步跌倒,都是你走到今天的垫脚石。

  十年后的你,是否已经成为了理想中的“前端架构师”?或者换了赛道,却依然热爱技术?无论答案是什么,我希望你记住:

  1. 永远保持学习
  前端的世界变得太快,但好奇心不该被时间磨平。如果2035年“前端”这个词还存在,记得回头看看2024年你学过的 React、Vue,它们或许像今天的 jQuery 一样成了“古董”,但正是这些碎片拼成了你的星辰大海。

  2. 珍惜那些“第一次”
  你会记得人生第一份实习的 mentor 吗?第一次提交的垃圾代码、第一次线上事故、第一次涨薪的狂喜……这些琐碎的“新手村”记忆,会成为你最珍贵的源代码。

  3. 别忘记为什么出发
  如果有一天你厌倦了,想想23岁的自己:那个为了一个动画效果调试到凌晨3点,却因为浏览器终于跑通而欢呼的年轻人。技术会过时,但解决问题的快乐永不褪色。

  最后,替我摸摸2035年的猫(你肯定养猫了吧?),看看窗外是不是已经有了飞行汽车(笑)。如果这十年里,你曾崩溃过、想放弃过,但最终挺过来了——那么,谢谢你的坚持。

  约定好了:十年前的我负责勇敢,十年后的你负责骄傲。

2025年7月16日

于某个通宵研究RTK的深夜

(P.S. 如果现在你还没财富自由……记得提醒我早点买比特币!)

  •  

React 组件调用的两种方式区别

#### 1. **标准调用方式 (``)** ```jsx ``` - **组件实例管理**:React 内部通过组件类型和 `key` 识别组件实例。 - **生命周期行为**:父组件重渲染时,若子组件的 `props` 或 `key` 未变化,**复用现有实例**,触发更新而非重新挂载。 - **状态保留**:子组件内部状态(`useState`, `useRef` 等)持续保留。 - **性能优化**:符合 React 协调算法,避免不必要的 DOM 操作。 - **Props 传递**:直接通过属性传递数据。 #### 2. **箭头函数调用方式 (`{() => }`)** ```jsx {() => } ``` - **组件实例管理**:每次渲染创建**新的函数实例**,React 视其为全新组件。 - **生命周期行为**:父组件每次重渲染都会触发子组件的 **完整卸载(unmount)和重新挂载(mount)**。 - **状态丢失**:子组件内部状态完全重置,所有 `useEffect` 依赖重新执行。 - **性能问题**: - 重复创建组件实例增加内存开销 - 频繁触发挂载/卸载生命周期 - 破坏 React 的复用优化机制 - **Props 传递**:需通过闭包捕获变量,可能引发过时闭包问题。 --- ### 核心差异总结 | **特性** | 标准方式 | 箭头函数方式 | | -------------- | ------------------- | --------------------------------- | | **实例标识** | 稳定 (类型/key) | 每次创建新函数 (标识不稳定) | | **更新行为** | 可复用实例 (Update) | 销毁旧实例 + 创建新实例 (Remount) | | **状态保留** | ✅ | ❌ (完全重置) | | **性能影响** | 优化友好 | 额外实例化开销 | | **React 推荐** | ✅ 标准做法 | ❌ 反模式 (特殊情况除外) | --- ### 技术本质 ```mermaid graph TD A[父组件渲染] --> B{调用方式} B -->|标准方式| C[React 检查组件标识] C -->|标识匹配| D[复用实例/更新] C -->|标识不匹配| E[卸载旧实例 + 挂载新实例] B -->|箭头函数| F[每次创建新函数] F --> G[React 视为新组件] G --> E ```
  •  

React 类组件 vs 函数组件:一份够用又够深的对照技术文档

React 中的类组件(Class Components)和函数组件(Function Components)是构建 UI 的两种主要方式。它们之间的区别随着 React 的发展(特别是 Hooks 的引入)而发生了重大变化。 ## 一、一句话结论 **新代码优先用函数组件(Hooks)**:表达力强、逻辑复用更自然、与并发特性和数据获取模式更贴合。 **类组件仍有价值**:**错误边界**目前只能由类实现;存量代码大量存在,读懂维护/迁移很重要。 ## 二、快速对比表 | 维度 | 类组件 Class Component | 函数组件 Function Component | | ---------- | ----------------------------------------- | ------------------------------------------- | | 定义方式 | `class extends React.Component` | `function`/箭头函数 | | 状态 | `this.setState`(**浅合并**) | `useState`(**替换**/自行合并) | | 生命周期 | `componentDidMount/Update/WillUnmount` 等 | `useEffect/useLayoutEffect` 统一描述副作用 | | 性能控制 | `shouldComponentUpdate`、`PureComponent` | `React.memo`、`useMemo`、`useCallback` | | 事件绑定 | 需处理 `this` 绑定 | 无 `this`,闭包与依赖管理更关键 | | Refs | `createRef`、实例字段 | `useRef`、`forwardRef` | | Context | `contextType` / `Context.Consumer` | `useContext` | | 错误边界 | 支持(`componentDidCatch`) | 不能直接实现错误边界 | | 并发特性 | 可用 `startTransition`(非 Hook) | `useTransition`、`useDeferredValue` 等 Hook | | 逻辑复用 | HOC、Render Props | **自定义 Hook**(组合更轻量) | | 类型(TS) | 类泛型参数(`Props, State`) | `FC` 或普通函数+类型 | | 默认值 | `defaultProps` | 更推荐函数参数默认值/解构默认值 | ## 三、语法对照:最小计数器 **类组件:** ```jsx class Counter extends React.Component { state = { count: 0 }; componentDidMount() { document.title = `count: ${this.state.count}`; } componentDidUpdate() { document.title = `count: ${this.state.count}`; } render() { return ( ); } } ``` **函数组件:** ```jsx function Counter() { const [count, setCount] = React.useState(0); React.useEffect(() => { document.title = `count: ${count}`; }, [count]); return ; } ``` 细看上面的对照,结果显而易见: - **类组件**需要定义 `state`、在 `componentDidMount` 和 `componentDidUpdate` 中分别处理副作用,并且通过 `this.setState` 来更新状态,写法相对冗长且容易出错; - **函数组件**则通过 `useState` 钩子直接声明状态,用 `useEffect` 钩子集中处理副作用逻辑,代码简洁明了,结构更加直观。 这种写法上的精简不仅仅是“代码量减少”,更重要的是函数组件在逻辑上更符合开发者的思维模型:状态与副作用都以函数形式组合,更容易拆分与复用。 ## 四、生命周期 ⇄ Hooks 对照 | 类生命周期 | 对应 Hook 模式 | 说明 | | ---------------------------------------------- | ----------------------------------------- | ------------------------ | | `constructor` | 组件外部初始化 / `useState` 初始值 | 尽量少做逻辑,避免副作用 | | `componentDidMount` | `useEffect(fn, [])` | 首次渲染后执行 | | `componentDidUpdate` | `useEffect(fn, [deps])` | 依赖变化后执行 | | `componentWillUnmount` | `useEffect(() => { return cleanup }, [])` | 返回清理函数 | | `getDerivedStateFromProps` | **手动派生**:`useMemo`/在渲染中计算 | 尽量避免“派生状态” | | `shouldComponentUpdate` | `React.memo` + `useMemo/useCallback` | 控制重渲染 | | `componentDidCatch`/`getDerivedStateFromError` | **无** | 错误边界仅类支持 | 从这个对照表可以看出: 1. **大部分生命周期都能在 Hooks 中找到等价或更灵活的实现方式**,例如 `componentDidMount`、`componentDidUpdate` 对应 `useEffect`,`shouldComponentUpdate` 对应 `React.memo` 搭配 `useMemo/useCallback`。 2. **函数组件的核心理念是“按逻辑组织代码”,而不是“按生命周期阶段”**。Hooks 不再要求开发者记住各种生命周期名词,而是通过依赖项数组精准声明“什么时候运行”。 3. **并非所有生命周期都有直接对应**。比如 `componentDidCatch` 和 `getDerivedStateFromError` 目前依然只有类组件支持,用于错误边界;这意味着在需要错误边界的场景里仍需要类组件。 4. **最佳实践趋势**:减少“派生状态”的使用(即 `getDerivedStateFromProps` 的场景),优先通过 `useMemo` 或直接在渲染中计算衍生数据,保持状态单一、可控。 总结来说,Hooks 的设计让副作用和状态逻辑更加聚合、灵活和可复用,这是它逐渐取代类组件的根本原因;但在少数场景(如错误边界)类组件仍然不可替代。 ## 五、Hooks副作用与常见陷阱(重点) **副作用Hooks可分为两个**: - `useEffect`:主要用于数据/订阅/事件。 - `useLayoutEffect`:主要用于视觉/布局,但是需要注意的是,`useLayoutEffect`是在所有的DOM变更之后,浏览器绘制之前同步执行的,这意味着它会阻塞浏览器的绘制。在 React 的[Fiber](https://www.gfbzsblog.site/main/Articles/38)架构下,尽管渲染过程已被优化为**可中断的异步模式**,但 `useLayoutEffect` 的使用仍需谨慎,原因在于它与 React 的**提交阶段(Commit Phase)**强关联,该阶段具有**同步且不可中断**的特性。 ```meta title: 孤芳不自赏 url: https://www.gfbzsblog.site/main/Articles/38 image: https://www.gfbzsblog.site/images/avatar_20250520_215057.png desc: React Fiber架构全方位分析 ``` **依赖数组**: - 使用规则:使用到的外部变量必须出现在依赖中(也可以通过 `useRef` 规避重建)。 - 常见Bug:漏填依赖导致读到**过期闭包**中的值;乱填依赖导致不必要重跑,这些都是闭包陷阱,需要额外关注一下。 - 工具:配合 `eslint-plugin-react-hooks` 强制校验。 **清理**: - 订阅、定时器、事件监听务必在 `return () => {...}` 中清理,依赖变化或卸载时运行。 ## 六、性能:渲染控制与记忆化 **类组件**: - `shouldComponentUpdate(nextProps, nextState)`:返回 `false` 可跳过渲染。 - `PureComponent`:浅比较 `props/state`。 **函数组件**: - `React.memo(Component, areEqual?)`:浅比较 `props`。 - `useMemo(fn, deps)`:缓存**计算结果**;避免昂贵计算在不变时重复执行。 - `useCallback(fn, deps)`:缓存**函数引用**;配合 `memo`/依赖稳定化。 - 过度记忆化会增加复杂度;以**真实瓶颈**为准(profiling)。 ## 七、事件与 `this` vs 闭包 类组件需要处理 `this`:构造器绑定或类字段箭头函数。 函数组件没有 `this`,但要管理**闭包**与依赖: - 事件处理里用到的状态/参数如果会变,要么放依赖里,要么用函数式更新:`setX(x => x + 1)`。 ## 八、Refs 与实例变量 **类**:`this.ref = React.createRef()`;实例字段天然存在。 **函数**:`const ref = useRef(initial)`;`ref.current` 在渲染间持久但**变更不触发渲染**。 - 常用作:DOM 引用、外部 API 句柄、**可变的实例变量**(节流标记、上一次值等)。 ```tsx function Player() { const audioRef = React.useRef(null); const playingRef = React.useRef(false); const toggle = () => { const el = audioRef.current!; if (playingRef.current) el.pause(); else el.play(); playingRef.current = !playingRef.current; }; return >; } ``` ## 九、 Context **类**:`static contextType = ThemeContext` 或 ``。 **函数**:`const theme = useContext(ThemeContext)` 更直接;避免“消费者地狱”。 ## 十、错误边界(类独占能力) **UI 崩溃隔离**:捕获子树渲染错误并展示降级 UI。 目前只能用类组件实现: ```tsx class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error: unknown, info: React.ErrorInfo) { /* 上报日志 */ } render() { return this.state.hasError ? this.props.fallback : this.props.children; } } ``` > 函数组件**不能**直接成为错误边界;通常把上层包成类的错误边界组件。 ## 十一、并发与交互响应 两者都能使用**非 Hook** 的 `startTransition(() => setState(...))`。 函数组件独享 Hook: - `useTransition()`:把某些更新标记为“可延后”,保持输入流畅。 - `useDeferredValue(value)`:延迟昂贵衍生计算。 场景:搜索输入→列表过滤;打字不卡,结果稍后刷新。 ## 十二、逻辑复用:HOC/Render Props vs 自定义 Hook 类时代常用 HOC、Render Props,容易出现**包装地狱**与命名冲突。 Hooks 时代用**自定义 Hook** 聚合状态与副作用,组合更平坦: ```tsx function useOnlineStatus() { const [online, setOnline] = React.useState(navigator.onLine); React.useEffect(() => { const on = () => setOnline(true); const off = () => setOnline(false); window.addEventListener('online', on); window.addEventListener('offline', off); return () => { window.removeEventListener('online', on); window.removeEventListener('offline', off); }; }, []); return online; } ``` ## 十三、TypeScript 要点 **类**:`class C

extends React.Component

`;方法签名更显式。 函数:`function Comp(props: Props) {}` 或 `const Comp: React.FC = (props) => {}` - `React.FC` 可省,**建议普通函数签名**(避免隐式 `children`、与泛型更灵活)。 **默认值**:函数参数默认值/解构默认值优先,少用 `defaultProps`(在函数组件上支持不佳且不推荐)。 ## 十四、SSR / SSG / RSC(简述) SSR/SSG 对两者都可用;不过 Hooks 的数据获取与 Suspense 配合更自然。 服务器组件(RSC)只适用于**函数式组件**范式(不含 stateful client hooks),这是现代数据获取的趋势点。 ## 十五、迁移指南:类 → 函数的映射 状态:`this.state` → `useState`/`useReducer`。 副作用:`componentDidMount/Update/WillUnmount` → **一个** `useEffect`,在其中做订阅与清理。 `shouldComponentUpdate`/`PureComponent` → `React.memo` + 局部 `useMemo/useCallback`。 实例字段 → `useRef`。 错误边界:**保留类组件**或单独抽出成类错误边界。 注意**依赖数组**与**闭包**问题,是迁移中最常见的 bug 来源。 ## 十六、常见坑位清单 **漏依赖**导致读到旧值:配合 ESLint 规则修正。 在 `useEffect` 里直接 `setInterval` 却不清理:卸载后仍在跑。 把大对象放入 `useState` 且频繁 `setState({...obj, x})`:考虑 `useReducer`。 过度使用 `useMemo/useCallback`:微优化吞掉了可读性。 在渲染期间做副作用(发请求/操作 DOM):应放入 `useEffect`/`useLayoutEffect`。 React.memo` 搭配**不稳定 props**(匿名函数/字面量对象)→ 失效:用 `useCallback/useMemo` 稳定引用。 ## 十七、什么时候仍考虑类组件? 需要**错误边界**且不想引入额外包装。 大量存量类代码,团队短期内以维护为主。 某些基于类的第三方库封装(逐渐变少)。 ## 十八、实战建议 新功能:**默认函数组件 + Hooks**。 清晰的副作用分层:数据副作用(请求/订阅)与布局副作用(测量/同步)分开写。 必配:`eslint-plugin-react-hooks`、`@types/react` 最新版本。 性能:先量化(Profiler/Performance 面板),再上 `memo`/`useMemo`。 复杂状态:首选 `useReducer` + 自定义 Hook 拆模块。 错误边界:在路由/页面级放一层类组件 `ErrorBoundary`。 组件 API:优先**组合**而非继承/HOC;导出“受控 + 非受控”两种用法。 TS:函数组件用普通函数签名,避免 `React.FC` 带来的隐式行为。 ## 十九、结语 函数组件是 React 的**主流与未来**,尤其在并发特性、数据获取与组件组合上优势明显。 类组件仍是生态的一部分,理解它的生命周期与错误边界机制,对维护老项目和设计稳健的边界层都很重要。 最理想的姿势:**以函数为主,错误边界用类兜底**;按实际性能与复杂度,理性使用记忆化与抽象。

  •