普通视图

发现新文章,点击刷新页面。
今天 — 2025年6月8日首页

实现 ECharts 多国地区可视化方案

2025年6月8日 12:39

基于 Vue3 + Vite 实现 ECharts 多国地图可视化方案

技术背景

项目采用 Vue3 + Vite 构建,前端需要实现一个地图可视化模块,目标是支持全球多个国家的地图展示,并能精确到洲省(地区)层级。

ECharts 本身提供了强大的地图可视化能力,但默认只内置了一些常见地理区域。为了满足多国家、多地区的地图展示需求,我们选择通过引入第三方资源和自定义配置的方式扩展地图数据。


地图数据获取与注册流程

1. 第三方地图资源引入

最初尝试使用 echarts-countries-js 库来快速集成各国地图数据。该库基于 pissang/starbucks 提供了多个 GeoJSON 文件,覆盖全球多个国家和地区。

但在实际使用中发现以下问题:

  • 该库依赖全局 echarts 模块进行地图注册。
  • 在按需引入 ECharts 的模块化项目中(如使用 Vite + Vue3),无法直接访问全局 echarts 对象,导致注册失败。

因此决定放弃该库,转为手动加载 GeoJSON 数据。

2. 手动注册地图数据

从 echarts-countries-js 中提取所需的 GeoJSON 文件后,我们在项目中自行注册地图(注意:china名字是固定的,因为echarts会有南海诸岛的小地图):

深色版本
import * as echarts from 'echarts/core';
import chinaGeoJson from '@/assets/maps/china.geojson';

echarts.registerMap('china', chinaGeoJson);

这里只是测试地图展示直接导入,实际是根据需要展示的地图动态导入json文件进行注册。


地区名称映射处理

问题描述

  • GeoJSON 文件中的地区名称为英文(或其他语言)。
  • 后端存储的数据中使用的地区名称为中文。
  • 因此在地图渲染时,无法直接匹配数据项与地理区域。

解决方案:nameMap 映射机制

ECharts 提供了 nameMap 配置项,允许将 GeoJSON 中的原始名称映射为指定的中文名称(我这么配置没有效果,需要放到series-map.nameMap里才有效,可能是其他原因,没有深入研究):

深色版本
option = {
  geo: {
    map: 'countryName',
    type: 'map',
    nameMap: {
      'Beijing': '北京市',
      'Shanghai': '上海市'
    }
  }
};

为了批量生成 nameMap,我用AI编写了一个 Python 脚本,可以从 GeoJSON 文件中提取所有地区名称,并构建初始的 nameMap JSON 结构。

3. 自动翻译与人工校验结合

由于手动翻译时间不允许(维基百科应该可以搜到每个国家对应的中文名),我使用 AI 辅助工具对提取出的地区名进行批量翻译:

  • 使用 googletrans 进行初步自动翻译。
  • 发现部分翻译结果不够准确,于是采用 VS Code 插件(如同义灵码)分批次提交给 AI 翻译。
  • 最终生成完整的 nameMap JSON 文件。

虽然未能逐一校对,但通过 AI 加快了整体映射构建速度,同时保留了后续人工修正的空间。


当前方案存在的限制与改进方向

尽管实现了基本的地图可视化功能,但仍存在以下几个主要问题:

1. 地图数据准确性不足

  • GeoJSON 数据来源较为陈旧(来自 9 年前的开源项目)。

2. 地区名称映射不准确

  • AI 翻译存在误差。

3. 数据输入自由度过高

  • 用户可通过输入框自由输入地区名称,缺乏统一规范。

总结

本文介绍了一个基于 Vue3 + Vite 的 ECharts 多国地图可视化实现方案,重点包括:

  • 如何绕过第三方库限制,手动注册地图数据;
  • 利用 ECharts 的 nameMap 功能实现地区名称映射;
  • 借助 AI 工具提升地区名称翻译与映射效率;
  • 当前实现的局限性及未来可优化方向。

当前内容是自己写了一遍,让ai整理了一遍,然后改了改,所以会有股子AI味,我个人表达能力不是很好,所以采用这个方案。

从原生 JS 到 React:手把手带你开启 React 业务开发之旅

作者 然我
2025年6月8日 12:30

一、前端开发演进:从切图崽到全干工程师的成长路径

在开始React业务开发之前,让我们先了解前端工程师的成长路线:

  1. 前端切图崽:掌握HTML+CSS+基础JS,能实现静态页面
  2. 前端开发工程师:掌握Vue/React等框架,能开发复杂Web应用
  3. 全栈开发工程师:掌握Node.js+数据库,实现前后端一体化
  4. 跨平台应用开发:掌握React Native等,开发Android/iOS应用
  5. AI应用开发:整合AI能力,开发智能化应用
  6. 全干工程师:掌握全技术栈,独立完成项目全流程

React作为现代前端开发的三大框架之一,让开发者得以脱离底层 DOM 操作,聚焦于业务逻辑,是我们进阶路上的重要里程碑。

二、React 项目初始化:从 vite 模板开始

1. 核心工具:npm 与 vite 的作用

  • npm(Node Package Manager) :作为 Node.js 的包管理器,负责安装 React 等开发依赖,如通过npm install react react-dom引入核心库。
  • vite:新一代前端构建工具,相比传统 webpack,具有极速冷启动、按需编译的特点,尤其适合 React 项目的工程化搭建。

2. 快速创建项目的 4 步流程

# 1. 初始化vite项目(选择react模板语言选择js)
npm init vite@latest my-react-app -- --template react

# 2. 进入项目目录
cd my-react-app

# 3. 安装依赖
npm install

# 4. 启动开发服务器(默认端口5173)
npm run dev

执行完成后,浏览器访问http://localhost:5173即可看到 React 欢迎页面

image.png

项目结构如下:

image.png

三、React 初体验:组件化开发的核心逻辑

1. 组件:HTML+CSS+JS 的封装单元

React 组件是完成开发任务的最小单元,它将 HTML、CSS、JS 逻辑封装在一个函数中,通过 组件组合 构建完整页面。以下是一个基础的 App 组件示例,演示如何通过函数组件渲染静态数据:

import { useState } from 'react';
import './App.css';

// 函数组件:接收参数并返回 JSX 结构
function App() {
  const staticTodos = ['吃饭', '睡觉', '打豆豆']; 
  return (
    <>
      <table>
        <thead>
          <tr>
            <th>序号</th>
            <th>任务</th>
          </tr>
        </thead>
        <tbody>
          {/* 在 JSX 中通过 {} 嵌入 JS 表达式,使用 map 遍历渲染列表 */}
          {staticTodos.map((item, index) => (
            <tr key={index}> {/* 列表渲染必须指定唯一 key,此处仅为演示,实际应使用数据唯一标识 */}
              <td>{index + 1}</td>
              <td>{item}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}

export default App;

image.png

关键知识点:

  • JSX 语法:允许在 JS 中直接书写类 HTML 结构,{} 用于插入 JS 表达式(如变量、函数调用等)。
  • 列表渲染:通过 Array.map() 遍历数据并生成多个 JSX 元素,需为每个列表项指定唯一 key(React 用于追踪组件更新的标识)。
  • 组件结构:函数组件必须有一个根返回值(如 <> 空标签或 <div> 等容器元素)。

2. 响应式数据:useState Hook 实现数据驱动视图

当数据需要动态变更并触发视图更新时,需使用 React 的 状态(State)  机制。通过 useState Hook 声明响应式数据,数据变更时 React 会自动重新渲染组件。

示例:动态更新标题与任务列表

import { useState } from 'react';
import './App.css';

function App() {
  // 声明响应式状态:todos(任务列表)和 title(页面标题)
  const [todos, setTodos] = useState(['吃饭', '睡觉', '打豆豆']);
  const [title, setTitle] = useState('今天你吃饭了吗'); 

  // 模拟异步操作:5 秒后更新
  setTimeout(() => {
    setTitle('今天你完成任务了吗');
    setTodos([...todos, '养鱼']); 
  }, 5000);

  return (
    <div>
      <h1 className="title">{title}</h1> {/* 绑定响应式数据 title */}
      <table>
        <thead>
          <tr>
            <th>序号</th>
            <th>任务</th>
          </tr>
        </thead>
        <tbody>
          {todos.map((item, index) => (
            <tr key={index}>
              <td>{index + 1}</td>
              <td>{item}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default App;

CSS 样式(App.css):

* {
  margin: 0;
  padding: 0;
}

.title {
  background-color: aqua;
  color: aliceblue;
  padding: 10px;
  margin-bottom: 20px;
}

image.png

五秒后更新为: image.png

核心概念解析:

  • useState 用法

    • 调用 useState(initialValue) 返回一个数组,第一项是状态值(如 todos),第二项是更新状态的函数(如 setTodos)。
    • 状态更新函数(如 setTitle)的调用会触发组件重新渲染,确保视图与数据同步。
  • 异步更新:即使在定时器、Promise 等异步场景中调用 setState,React 也能正确捕获变更并更新视图。

  • 数据不可变性:更新数组或对象状态时,需通过 [...todos] 或 {...obj} 创建新对象,避免直接修改原数据导致 React 无法检测变更。

3. 组件组合:拆解复杂页面为可复用单元

React 推荐将页面拆分为多个小组件,通过嵌套组合实现复杂功能。以下是对示例代码的扩展思路 :

// 拆分标题组件
function Title({ title }) {
  return <h1 className="title">{title}</h1>;
}

// 拆分任务列表组件
function TodoTable({ todos }) {
  return (
    <table>
      <thead>
        <tr>
          <th>序号</th>
          <th>任务</th>
        </tr>
      </thead>
      <tbody>
        {todos.map((item, index) => (
          <tr key={index}>
            <td>{index + 1}</td>
            <td>{item}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function App() {
  const [todos, setTodos] = useState(['吃饭', '睡觉', '打豆豆']);
  const [title, setTitle] = useState('今天你吃饭了吗');

  setTimeout(() => setTitle('今天你完成任务了吗'), 5000);

  return (
    <div>
      <Title title={title} /> {/* 父组件向子组件传递数据 via props */}
      <TodoTable todos={todos} /> {/* 复用列表组件,传入不同数据 */}
    </div>
  );
}

组件通信规则:

  • 父传子:通过 props 向子组件传递数据(如 Title 组件的 title 属性)。
  • 子传父:子组件通过回调函数(如 onClick={handleClick})向父组件传递事件。
  • 跨组件通信:状态提升(将共享状态提升至共同父组件)或使用 Context 实现全局状态管理。

4. 对比原生 JS:React 如何简化开发

场景 原生 JS 实现方式 React 实现方式
渲染列表 使用 createElement 或 innerHTML 拼接 DOM 字符串 通过 JSX + map 直接声明视图结构
数据更新 手动查找 DOM 节点并更新 textContent/innerHTML 调用 setState 自动触发视图更新
组件复用 复制粘贴代码或封装函数返回 DOM 节点 定义函数组件并通过 props 传递差异化数据

核心优势:React 通过声明式编程(描述 “是什么” 而非 “怎么做”)和组件化架构,让开发者聚焦业务逻辑而非底层 DOM 操作,极大提升开发效率与代码可维护性。

四、实战案例:构建响应式待办事项应用

1. 功能拆解

我们将通过实际代码实现一个包含以下功能的待办应用:

  • 输入框添加新任务(表单组件TodoForm
  • 任务列表展示(列表组件Todos
  • 数据持久化(暂存于组件状态,后续可扩展 localStorage)

2. 核心组件实现

首先我们先在src目录下创建一个components文件夹,存放各个组件

(1)表单组件TodoForm:双向绑定与提交处理

import { useState } from 'react';

function TodoForm(props) {
  // 从 props 中获取父组件传递的回调函数(用于接收新增任务)
  const onAdd = props.onAdd;
  // 声明本地状态:text 存储输入框内容,初始值为"打豆豆"
  const [text, setText] = useState('打豆豆');

  // 表单提交处理函数
  const handleSubmit = (e) => {
    // 阻止表单默认提交行为(避免页面跳转)
    e.preventDefault();
    // 调用父组件回调,传递当前输入的文本
    onAdd(text);
    console.log(e.target.value);
  };

  // 输入框内容变化处理函数
  const handleChange = (e) => {
    // 更新本地状态,实现双向绑定(输入框值与 state 同步)
    setText(e.target.value);
  };

  return (
    <form action="http://www.baidu.com" onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="请输入待办事项"
        value={text}          // 绑定 state 到输入框 value
        onChange={handleChange} // 监听输入变化并更新 state
      />
      <button type="submit">添加</button>
    </form>
  );
}

export default TodoForm;

在这个组件中,通过useState定义text状态,实现输入框的双向绑定。handleChange函数监听输入变化更新状态,handleSubmit函数阻止表单默认提交,并调用父组件传递的onAdd回调函数,将输入内容传递出去。

(2)列表组件Todos:数据渲染与 key 的重要性

function Todos(props) {
  console.log(props, '/////');
  // 解构 props 中的 todos 数据
  const todos = props.todos;

  return (
    <ul>
      {
        // 遍历 todos 数组,渲染为列表项
        todos.map((todo) => (
          // key 是 React 识别列表项的唯一标识,必须使用稳定的唯一值(如数据 ID)
          <li key={todo.id}>{todo.text}</li>
        ))
      }
    </ul>
  );
}

export default Todos;

该组件接收父组件传递的props.todos数据,使用map方法遍历数据并渲染为列表项。为每个列表项设置唯一的key属性(这里使用todo.id),这能帮助 React 高效地更新列表,避免在数据变化时出现渲染错误。

(3)核心逻辑组件TodoList:状态管理与组件组合

import { useState } from 'react';
import '../Todo.css'; // 引入组件级样式(需确保路径正确)
import TodoForm from './TodoForm'; // 引入表单组件(子组件)
import Todos from './Todos'; // 引入列表组件(子组件)

function TodoList() {
  const [hi, setHi] = useState('haha');
  const [title, setTitle] = useState('Todo List');
  const [todos, setTodos] = useState([
    {
      id: 1,          // 唯一标识(必填,用于列表渲染和更新)
      text: '吃饭',  
      completed: false 
    }
  ]);

  // 添加任务的回调函数(供子组件调用)
  const handleAdd = (text) => {
    // 更新 todos 状态:使用展开运算符保留原有数据,新增任务项
    setTodos([
      ...todos,
      {
        id: todos.length + 1, 
        text,                // 解构参数作为任务文本
        completed: false      
      }
    ]);
  };

  return (
    <div className="container">
      <h1 className="title">
        {title}{hi} {/* 组合显示标题和演示文本 */}
      </h1>
      <TodoForm onAdd={handleAdd} /> {/* 向子组件传递回调函数,实现子传父通信 */}
      <Todos todos={todos} />       {/* 向子组件传递数据,实现父传子通信 */}
    </div>
  );
}

export default TodoList;

TodoList组件作为整个待办应用的核心,通过useState定义了多个状态,如titletodoshandleAdd函数用于处理添加任务的逻辑,当调用setTodos更新任务列表状态时,React 会自动重新渲染包含Todos组件的部分,实现数据驱动视图更新。同时,通过propshandleAdd函数传递给TodoForm组件,完成子传父的通信。

(4)根组件App:整合应用

import { useState } from 'react'
import './App.css'
import TodoList from './components/TodoList'

function App() {
  return (
    <>
      <div>
      </div> 
 {/* 渲染待办事项核心组件 */}
      <TodoList />
    </>
  )
}

export default App

App组件作为应用的入口,只负责引入并渲染TodoList组件,将整个待办应用集成到项目中。

image.png

通过这个完整的待办事项应用案例,我们能更清晰地看到 React 中组件化开发、数据驱动视图、组件间通信等核心概念的实际应用。后续还可以在此基础上,添加任务删除、任务状态切换等功能,进一步深化对 React 开发的理解 。

学习 React 之路道阻且长,唯有持续深耕

从原生 JS 到 React,前端开发变得更加高效和便捷。React 的组件化开发和响应式数据管理,让开发者能够聚焦于业务逻辑,而无需过多关注底层的 DOM 操作。随着技术的不断发展,相信前端开发还会迎来更多的变革和创新。

chrome架构-多进程和进程中的线程

2025年6月8日 11:22

1. 进程和线程

在这里插入图片描述 如图所示,每个应用都是一个进程。拥有一定的资源(cpu,内存、磁盘等等)

进程详细解释:当我们启动一个程序时,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,这样的一个运行环境就叫进程。

线程:存在于进程内部,并执行其进程程序的部分功能。多线程可以提升进程的预算效率。

1.1 进程和线程的关系

  1. 进程与进程之间的内容相互隔离;

    每个进程只能访问自己占有的数据,避免线程之间相互读写数据。如果想要互相通信,走IPC机制。因为进程间的隔离,一个进程挂了,不会影响到其他进程。

  2. 进程关闭后,操作系统会回收该进程所占用的内存;

    当一个进程退出后,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。

  3. 进程中的各线程共享该进程的所有数据;

  4. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。

2. 浏览器单进程

2007年之前,浏览器都是单进程的。浏览器的所有功能模块都运行在同一个进程中。这些模块包括network、plugin、JS 运行环境、render和UI等。

缺点是什么呢:

  1. 稳定性:早期浏览器依赖非常容易出问题的插件,一个插件的意外崩溃都会导致浏览器的崩溃。在渲染进程中复杂的js代码同理。
  2. 性能:由于所有的网页都在一个进程中运行,若存在无限循环的JavaScript代码,将会导致整个浏览器卡顿或者失去响应。并且由于渲染引擎可能存在的内存泄露问题,在关闭时不能完全的回收内存,这样导致的问题是使用时间越长,内存占用越高,浏览器会变得越慢。
  3. 安全:由于在同一个进程中运行,通过插件可以获取到操作系统的任意资源,当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑。

3. 浏览器多进程

Chrome 浏览器右上角点击“选项”>“更多工具”>“任务管理器”,可以看到打开一个页面运行的多个进程。

在这里插入图片描述

3.1 早期架构

2008年发布的浏览器架构图: 在这里插入图片描述 从图中可以看出,Chrome 的页面是运行在render process中,页面里的插件运行在plugin process之中,进程之间是通过 IPC 机制进行通信(如图中虚线部分),可以解决什么问题呢?

  1. 稳定性:各进程之间相互隔离,每个页面的渲染进程相互独立,一个页面或者插件崩溃,不会影响浏览器或者其他页面。
  2. 性能:关闭一个页面时,整个渲染进程也会被关闭,之后该进程所占用的内存都会被系统回收。解决了内存泄漏的问题。
  3. 安全:chrome把插件进程和渲染进程放到了沙箱中,不能直接访问系统的硬盘、内存和系统权限。

3.2 目前架构

在这里插入图片描述在这里插入图片描述

  1. 浏览器进程:控制应用程序的“浏览器”部分,主要负责界面的显示、用户交互和子进程管理,同时也提供存储等功能。负责应用程序不同部分的其他进程协调。
  2. 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  3. GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU进程。
  4. 网络进程:主要负责页面的网络资源发起和接收。
  5. 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
  6. Storage Service进程:为local/session storage, service worker, indexed_db提供存储服务。

3.3 多进程的问题:

  • 更高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
  • 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。

4. 未来面向服务的架构

Chromium在2016年提出了以SOA(Services Oriented Architecture)为基础的高内聚,低耦合,易扩展的架构。该架构期望将功能组件以服务的形式拆分或聚合。 在这里插入图片描述 同时 Chrome 还提供灵活的弹性架构,在强大性能设备上会以多进程的方式运行基础服务,但是如果在资源受限的设备上(如下图),Chrome 会将很多服务整合到一个进程中,从而节省内存占用。

在这里插入图片描述

5. 小问题

因为每开一个页签,通常是开一个新的进程。但是当新开的页面和父页面是同一站点(根域名+协议相同)时,新页面会复用父页面的渲染进程。所以,这种情况下,一个页面崩溃了,会导致同一站点的页面同时崩溃,因为他们使用了同一个渲染进程。

为什么要让他们跑在一个进程里面呢? 因为在一个渲染进程里面,他们就会共享JS的执行环境,也就是说A页面可以直接在B页面中执行脚本。因为是同一家的站点,所以是有这个需求的。

学习笔记参考:time.geekbang.org/column/intr… blog.csdn.net/youzhangjin…

HarmonyOS Next 枚举递归定义与表达式树建模:从理论到实践

作者 SameX
2025年6月8日 11:04

在 HarmonyOS Next 开发中,枚举的递归定义是构建复杂数据结构的核心技术。通过允许枚举构造器引用自身类型,开发者能够轻松建模表达式树、树形结构等递归数据模型。本文结合仓颉语言特性,解析递归枚举的定义规则、模式匹配技巧及在表达式求值中的实战应用。

一、递归枚举的基础定义与规则

递归枚举是指枚举的构造器参数中包含枚举自身类型。这种特性使得枚举能够表示具有层次结构的数据,例如数学表达式、文件系统目录等。

1. 基础语法结构

enum 枚举名 {
    | 基础构造器(非枚举类型参数)
    | 递归构造器(枚举名, 枚举名)  // 参数为枚举自身类型
}

2. 表达式树示例

以数学表达式为例,定义包含数值、加法和减法的递归枚举:

enum Expr {
    | Num(Int64)           // 基础构造器:数值
    | Add(Expr, Expr)       // 递归构造器:加法表达式(左操作数,右操作数)
    | Sub(Expr, Expr)       // 递归构造器:减法表达式(左操作数,右操作数)
}
  • Num(Int64):表示单个数值(如 5)。
  • Add(Expr, Expr):表示加法表达式(如 Add(Num(3), Num(4)) 表示 3 + 4)。
  • Sub(Expr, Expr):表示减法表达式(如 Sub(Num(10), Add(Num(2), Num(3)) 表示 10 - (2 + 3))。

3. 编译器限制

递归枚举需确保构造器中至少包含一个非递归构造器(基础构造器),避免无限递归。仓颉编译器会自动检查递归枚举的有效性,若所有构造器均为递归类型,将触发编译错误。

二、递归枚举的模式匹配与遍历

1. 递归模式匹配逻辑

通过 match 表达式处理递归枚举时,需为基础构造器和递归构造器分别定义处理逻辑。基础构造器作为递归终止条件,递归构造器则负责处理层次结构。

示例:计算表达式值

func evaluate(expr: Expr): Int64 {
    match (expr) {
        case Num(n) => n                          // 基础构造器:直接返回数值
        case Add(lhs, rhs) => evaluate(lhs) + evaluate(rhs)  // 递归处理左、右操作数
        case Sub(lhs, rhs) => evaluate(lhs) - evaluate(rhs)  // 递归处理左、右操作数
    }
}

2. 表达式树构建示例

// 构建表达式:10 - (3 + 2)
let expr = Sub(
    Num(10),
    Add(Num(3), Num(2))
)

let result = evaluate(expr: expr)  // 计算结果为5
println("表达式结果:\(result)")  // 输出:5

3. 遍历递归枚举的技巧

对于更复杂的递归结构(如包含多层嵌套的表达式),可通过模式匹配结合循环尾递归优化实现遍历:

// 尾递归优化版求值函数
func evaluateTailRecursive(expr: Expr, acc: Int64 = 0): Int64 {
    while true {
        match (expr) {
            case Num(n) => return acc + n
            case Add(lhs, rhs) => expr = lhs; acc += evaluateTailRecursive(rhs, 0)
            case Sub(lhs, rhs) => expr = lhs; acc -= evaluateTailRecursive(rhs, 0)
        }
    }
}

三、实战场景:文件系统目录建模

递归枚举不仅适用于数学表达式,还可用于建模文件系统中的目录结构,其中目录可包含子目录和文件。

1. 递归枚举定义

enum FileSystemItem {
    | File(name: String, size: Int64)         // 文件:名称、大小
    | Directory(name: String, items: [FileSystemItem])  // 目录:名称、子项列表
}

2. 构建目录结构示例

// 创建文件系统结构:
// root/
// ├─ file1.txt (1024B)
// └─ subdir/
//    └─ file2.txt (512B)
let file1 = File(name: "file1.txt", size: 1024)
let file2 = File(name: "file2.txt", size: 512)
let subdir = Directory(name: "subdir", items: [file2])
let root = Directory(name: "root", items: [file1, subdir])

3. 递归计算目录总大小

func calculateSize(item: FileSystemItem): Int64 {
    match (item) {
        case File(_, size) => size                          // 文件直接返回大小
        case Directory(_, items) => items.map(calculateSize).reduce(0, +)  // 递归计算子项大小之和
    }
}

let totalSize = calculateSize(item: root)  // 计算结果为1536(1024 + 512)
println("目录总大小:\(totalSize)B")

四、递归枚举的性能考量与优化

1. 栈溢出风险

递归枚举的深度若超过系统栈限制,会导致运行时栈溢出错误。例如,深度为 1000 的表达式树可能触发栈溢出。

2. 优化策略

(1)迭代替代递归

通过手动维护栈结构,将递归遍历转换为迭代遍历:

func evaluateIterative(expr: Expr): Int64 {
    var stack = [expr]
    var result = 0

    while !stack.isEmpty {
        let current = stack.pop()!
        match (current) {
            case Num(n) => result += n
            case Add(lhs, rhs) => stack.append(rhs); stack.append(lhs)
            case Sub(lhs, rhs) => stack.append(rhs); stack.append(Expr.Sub(lhs, Num(0)))  // 调整符号
        }
    }

    return result
}

(2)尾递归优化

若编译器支持尾递归优化,可将递归函数改写为尾递归形式,避免栈深度增加:

@tailrec
func evaluateTailRecursive(expr: Expr, acc: Int64 = 0): Int64 {
    match (expr) {
        case Num(n) => acc + n
        case Add(lhs, rhs) => evaluateTailRecursive(lhs, acc + evaluateTailRecursive(rhs))
        case Sub(lhs, rhs) => evaluateTailRecursive(lhs, acc - evaluateTailRecursive(rhs))
    }
}

3. 编译器优化支持

仓颉编译器会对尾递归函数进行优化,将其转换为迭代形式,从而避免栈溢出。开发者可通过 @tailrec 注解显式标记尾递归函数(需编译器支持)。

五、总结

递归枚举是 HarmonyOS Next 中处理层次化数据的强大工具,其核心优势在于:

  1. 简洁的数据建模:通过枚举构造器的递归引用,轻松定义表达式树、目录结构等复杂模型;
  2. 类型安全的遍历:结合模式匹配,确保在编译期处理所有可能的枚举构造器;
  3. 灵活的优化策略:通过迭代或尾递归优化,平衡代码简洁性与性能需求。

ES6~ES13 新特性

作者 梨子同志
2025年6月8日 11:01

一、ES6 (ES2015) 重要新特性

1. 模板字符串 (Template Literals)

  • 使用反引号(``)定义字符串
  • 支持多行字符串和插值表达式
// 基本用法
const name = 'Alice';
const greeting = `Hello, ${name}!`; // "Hello, Alice!"

// 多行字符串
const multiLine = `
  This is 
  a multi-line
  string
`;

// 标签模板
function tag(strings, ...values) {
  console.log(strings); // ["Hello ", "!"]
  console.log(values);  // ["Alice"]
  return strings[0] + values[0].toUpperCase() + strings[1];
}
const result = tag`Hello ${name}!`; // "Hello ALICE!"

2. 箭头函数 (Arrow Functions)

  • 更简洁的函数语法
  • 自动绑定词法作用域的 this
// 基本语法
const add = (a, b) => a + b;

// 单参数可省略括号
const square = x => x * x;

// 无参数需要括号
const sayHi = () => console.log('Hi');

// 返回对象需要用括号包裹
const makePerson = (name, age) => ({ name, age });

// this 绑定示例
const counter = {
  count: 0,
  increment: function() {
    setInterval(() => {
      this.count++; // 正确绑定this
      console.log(this.count);
    }, 1000);
  }
};

3. 函数参数增强

默认参数 (Default Parameters)

function greet(name = 'Guest', greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}
console.log(greet()); // "Hello, Guest!"

剩余参数 (Rest Parameters)

function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6

4. 解构赋值 (Destructuring Assignment)

对象解构

const person = { name: 'John', age: 30, city: 'New York' };
const { name, age } = person;
console.log(name, age); // "John" 30

// 别名
const { name: personName } = person;
console.log(personName); // "John"

// 默认值
const { country = 'USA' } = person;
console.log(country); // "USA"

数组解构

const numbers = [1, 2, 3];
const [first, second] = numbers;
console.log(first, second); // 1 2

// 跳过元素
const [,, third] = numbers;
console.log(third); // 3

// 剩余元素
const [head, ...tail] = numbers;
console.log(tail); // [2, 3]

5. 对象字面量增强

属性简写

const name = 'Alice';
const age = 25;
const person = { name, age }; // { name: 'Alice', age: 25 }

方法简写

const obj = {
  // 传统写法
  sayHello: function() {},
  
  // 简写写法
  sayHi() {},
  
  // 箭头函数
  sayBye: () => {}
};

计算属性名

const propKey = 'name';
const obj = {
  [propKey]: 'John',
  [`get${propKey}`]() {
    return this[propKey];
  }
};
console.log(obj.getName()); // "John"

二、ES2016 (ES7) 新特性

1. 数组 includes() 方法

const arr = [1, 2, 3];
console.log(arr.includes(2)); // true
console.log(arr.includes(4)); // false

// 与indexOf的区别
console.log([NaN].includes(NaN)); // true
console.log([NaN].indexOf(NaN) !== -1); // false

2. 指数运算符 (**)

console.log(2 ** 3); // 8
console.log(Math.pow(2, 3)); // 8

// 与Math.pow()的区别
console.log((-2) ** 3); // -8
console.log(Math.pow(-2, 3)); // -8

三、ES2017 (ES8) 新特性

1. async/await

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}

2. Object.values() / Object.entries()

const obj = { a: 1, b: 2, c: 3 };

console.log(Object.values(obj)); // [1, 2, 3]
console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]]

// 与for...in的区别
for (const [key, value] of Object.entries(obj)) {
  console.log(`${key}: ${value}`);
}

3. 字符串填充方法

console.log('5'.padStart(2, '0')); // "05"
console.log('12'.padStart(2, '0')); // "12"
console.log('abc'.padEnd(5, '*')); // "abc**"

4. 函数参数列表尾逗号

function foo(
  param1,
  param2, // 允许尾逗号
) {
  // ...
}

四、ES2018 (ES9) 新特性

1. 对象扩展运算符

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }

// 覆盖属性
const obj3 = { ...obj1, a: 3 }; // { a: 3, b: 2 }

// 浅拷贝
const obj4 = { ...obj1 };
console.log(obj4 === obj1); // false

2. Promise.finally()

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error))
  .finally(() => console.log('Request completed'));

3. 正则表达式增强

命名捕获组

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = re.exec('2023-05-15');
console.log(match.groups.year); // "2023"

dotAll 模式 (s 标志)

const re = /foo.bar/s;
console.log(re.test('foo\nbar')); // true

后行断言

console.log(/(?<=\$)\d+/.exec('$100')[0]); // "100" (匹配$后的数字)
console.log(/(?<!\$)\d+/.exec('€100')[0]); // "100" (匹配前面不是$的数字)

五、ES2019 (ES10) 新特性

1. Array.flat() / Array.flatMap()

const arr = [1, [2, [3]]];
console.log(arr.flat()); // [1, 2, [3]]
console.log(arr.flat(2)); // [1, 2, 3]

const sentences = ["Hello world", "Goodbye universe"];
const words = sentences.flatMap(sentence => sentence.split(' '));
console.log(words); // ["Hello", "world", "Goodbye", "universe"]

2. Object.fromEntries()

const entries = [['name', 'John'], ['age', 30]];
const obj = Object.fromEntries(entries);
console.log(obj); // { name: "John", age: 30 }

// 与Object.entries()配合使用
const newObj = Object.fromEntries(
  Object.entries(obj).map(([key, value]) => [key, String(value)])
);

3. String.trimStart() / String.trimEnd()

const str = '   hello   ';
console.log(str.trimStart()); // "hello   "
console.log(str.trimEnd());   // "   hello"

4. 可选的 catch 绑定

try {
  // ...
} catch { // 不需要指定error参数
  console.log('An error occurred');
}

六、ES2020 (ES11) 新特性

1. 可选链操作符 (?.)

const user = { profile: { name: 'John' } };

// 传统写法
const name = user && user.profile && user.profile.name;

// 可选链写法
const name = user?.profile?.name;

// 函数调用
user.sayHi?.(); // 如果sayHi存在则调用

// 数组访问
const firstItem = arr?.[0];

2. 空值合并运算符 (??)

const value = null ?? 'default'; // "default"
const value2 = 0 ?? 'default';   // 0 (与||不同)

// 与||的区别
console.log(0 || 'default'); // "default"
console.log('' || 'default'); // "default"
console.log(false || 'default'); // "default"

3. 动态导入 (Dynamic Import)

// 按需加载模块
button.addEventListener('click', async () => {
  const module = await import('./module.js');
  module.doSomething();
});

4. BigInt

const bigNum = 9007199254740991n; // 字面量加n
const anotherBigNum = BigInt("9007199254740991");

console.log(bigNum + anotherBigNum); // 18014398509481982n

// 不能与Number混合运算
console.log(bigNum + 1); // TypeError

七、ES2021 (ES12) 新特性

1. 逻辑赋值运算符

// 逻辑或赋值
let a = false;
a ||= true; // a = a || true

// 逻辑与赋值
let b = true;
b &&= false; // b = b && false

// 空值合并赋值
let c = null;
c ??= 'default'; // c = c ?? 'default'

2. String.replaceAll()

const str = 'hello world';
console.log(str.replaceAll('l', 'x')); // "hexxo worxd"

// 之前需要正则表达式
console.log(str.replace(/l/g, 'x')); // "hexxo worxd"

3. Promise.any()

const promises = [
  Promise.reject('Error 1'),
  Promise.resolve('Success 1'),
  Promise.reject('Error 2')
];

Promise.any(promises)
  .then(result => console.log(result)) // "Success 1"
  .catch(errors => console.log(errors));

4. 数字分隔符

const billion = 1_000_000_000; // 更易读
console.log(billion === 1000000000); // true

八、ES2022 (ES13) 新特性

1. 类字段声明

class Counter {
  count = 0; // 实例字段
  
  increment = () => { // 箭头函数方法
    this.count++;
  };
  
  static version = '1.0'; // 静态字段
}

2. 私有字段和方法

class Person {
  #name; // 私有字段
  
  constructor(name) {
    this.#name = name;
  }
  
  #privateMethod() { // 私有方法
    return `Hello, ${this.#name}`;
  }
  
  greet() {
    console.log(this.#privateMethod());
  }
}

const person = new Person('John');
person.greet(); // "Hello, John"
// person.#name; // SyntaxError

3. 静态初始化块

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein'
  };
  
  static englishWords = [];
  static germanWords = [];
  
  static { // 静态初始化块
    for (const [english, german] of Object.entries(this.translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
  }
}

4. 顶层 await

// 在模块顶层使用await
const data = await fetchData();
console.log(data);

// 之前需要包装在async函数中
(async () => {
  const data = await fetchData();
  console.log(data);
})();

九、ES2023 (ES14) 新特性

1. Array.findLast() / Array.findLastIndex()

const numbers = [1, 2, 3, 4, 3, 2, 1];

console.log(numbers.findLast(n => n > 2)); // 3 (最后一个满足条件的元素)
console.log(numbers.findLastIndex(n => n > 2)); // 4 (索引)

2. Hashbang 语法标准化

#!/usr/bin/env node
// 现在ES标准正式支持Shebang语法
console.log('Hello from CLI tool');

3. WeakMap 支持 Symbol 键

const weakMap = new WeakMap();
const key = Symbol('privateData');
const obj = {};

weakMap.set(key, 'secret');
weakMap.set(obj, 'other secret');

console.log(weakMap.get(key)); // "secret"
console.log(weakMap.get(obj)); // "other secret"

十、总结与应用建议

1. 版本特性快速参考

版本 重要特性
ES6 类、模块、箭头函数、模板字符串、解构、Promise、let/const
ES2016 includes()、指数运算符
ES2017 async/await、Object.values/entries、字符串填充
ES2018 对象扩展运算符、Promise.finally、正则增强
ES2019 Array.flat/flatMap、Object.fromEntries、trimStart/End
ES2020 可选链、空值合并、动态导入、BigInt
ES2021 逻辑赋值、replaceAll、Promise.any、数字分隔符
ES2022 类字段、私有方法、静态块、顶层await
ES2023 Array.findLast、Hashbang、WeakMap Symbol键

2. 现代JavaScript开发建议

  1. 优先使用新语法:如箭头函数、模板字符串、解构赋值等
  2. 渐进式采用:根据项目环境逐步引入新特性
  3. 关注兼容性:使用Babel等工具确保代码兼容性
  4. 代码可读性:合理使用新特性提升代码可读性而非炫技
  5. 团队一致性:制定团队编码规范统一新特性的使用方式

3. 学习资源推荐

  1. 官方文档ECMAScript规范
  2. 兼容性查询Can I use
  3. 实践教程现代JavaScript教程
  4. 新特性演示ES6+示例

HarmonyOS Next自定义枚举与标准库的协同:Option与Result

作者 SameX
2025年6月8日 10:52

在HarmonyOS Next开发中,自定义枚举与标准库类型(如OptionResult)的协同使用,是构建类型安全、可维护代码的关键。仓颉语言通过枚举的代数数据类型特性,与标准库的泛型枚举形成互补,能够高效处理值缺失、操作失败等场景。本文结合文档知识点,解析如何通过自定义枚举扩展标准库能力,实现更健壮的业务逻辑。

一、Option类型的本质与自定义扩展

1. Option类型的核心语义

Option<T>是标准库提供的泛型枚举,用于表示“可能存在的值”:

enum Option<T> {
    | Some(T)
    | None
}
  • Some(T):表示值存在,携带类型为T的实例。
  • None:表示值缺失,等价于其他语言的null,但更安全。

2. 自定义枚举适配Option语义

当需要更具体的值缺失场景时,可通过自定义枚举继承Option语义:

// 业务场景:用户权限可能未初始化
enum UserPermission {
    | Granted(String)        // 已授权(携带权限范围)
    | Denied                 // 拒绝授权
    | Uninitialized        // 未初始化(等价于Option的None)
}

// 转换为Option类型
func toOption(perm: UserPermission) -> Option<String> {
    match (perm) {
        case .Granted(scope) => Some(scope)
        case .Denied | .Uninitialized => None
    }
}

3. 与if-let/while-let结合使用

利用标准库的解构语法处理自定义枚举:

let permission = UserPermission.Granted("read")
if (let Some(scope) <- toOption(perm: permission)) {
    println("有权限:\(scope)")  // 输出:有权限:read
}

二、Result类型:处理操作失败的枚举范式

1. Result类型的标准定义

Result<T, E>用于表示可能失败的操作结果,是标准库提供的另一个泛型枚举:

enum Result<T, E> {
    | Ok(T)        // 操作成功,携带结果值
    | Err(E)       // 操作失败,携带错误信息
}
  • 应用场景:文件读写、网络请求、数据解析等可能失败的操作。

2. 自定义错误枚举与Result结合

定义业务专属错误类型,与Result协同处理失败场景:

// 自定义错误枚举
enum FileError {
    | NotFound(String)
    | PermissionDenied
    | CorruptedData
}

// 返回Result的函数示例
func readConfigFile(path: String) -> Result<String, FileError> {
    if !fileExists(path) {
        return Err(.NotFound(path))
    } else if !hasReadPermission(path) {
        return Err(.PermissionDenied)
    } else {
        let content = readFile(path)
        return content.isCorrupted ? Err(.CorruptedData) : Ok(content)
    }
}

3. 模式匹配处理Result结果

func processConfig() {
    let result = readConfigFile(path: "/config.json")
    match (result) {
        case Ok(content) => println("配置内容:\(content)")
        case Err(error) => handleFileError(error)
    }
}

func handleFileError(error: FileError) {
    match (error) {
        case .NotFound(path) => println("文件未找到:\(path)")
        case .PermissionDenied => println("权限不足")
        case .CorruptedData => println("数据损坏")
    }
}

三、自定义枚举与标准库的混合使用

1. 多层级错误处理:从自定义到标准库

将自定义枚举的错误转换为标准库Error类型,适配通用接口:

extension FileError : Error { }  // 使FileError符合标准库Error协议

func loadData() throws {
    let result = readConfigFile(path: "/data.txt")
    // 将Result转换为Throws风格接口
    if let Err(e) = result {
        throw e
    }
}

2. Option与Result的组合模式

处理“可能缺失值+可能失败操作”的双重不确定性:

func fetchOptionalData() -> Result<Option<String>, NetworkError> {
    if isNetworkAvailable() {
        let data = networkRequest()  // 可能返回None
        return Ok(data)
    } else {
        return Err(.NoConnection)
    }
}

// 解构组合类型
match (fetchOptionalData()) {
    case Ok(Some(data)) => println("成功获取数据:\(data)")
    case Ok(None) => println("数据不存在")
    case Err(error) => println("网络错误:\(error)")
}

3. 自定义枚举的泛型抽象

通过泛型定义可复用的枚举结构,与标准库保持一致:

enum Either<L, R> {
    | Left(L)
    | Right(R)
}

// 示例:转换Result为Either
func resultToEither<T, E>(result: Result<T, E>) -> Either<E, T> {
    match (result) {
        case Ok(t) => .Right(t)
        case Err(e) => .Left(e)
    }
}

四、最佳实践:避免过度设计与类型滥用

1. 优先使用标准库类型

  • 反例:重复实现类似Option的枚举
    // 避免自定义类似Option的枚举
    enum Maybe<T> {
        | Just(T)
        | Nothing
    }
    
  • 正例:直接使用Option<T>,必要时通过扩展增加业务逻辑。

2. 错误枚举的粒度控制

  • 细化错误类型:区分临时错误(如.Timeout)与永久错误(如.InvalidData),便于上层逻辑处理。
  • 避免枚举爆炸:通过泛型参数复用错误类型,如Result<Int, MyError>而非为每种类型定义独立枚举。

3. 与模式匹配的协同原则

  • 使用match而非if-else处理枚举,确保穷尽性检查;
  • 复杂枚举解构可拆分为独立函数,提升可读性:
    func decode(data: Data) -> Result<Config, DecodeError> {
        // 复杂解析逻辑
    }
    
    func handleDecodeResult(result: Result<Config, DecodeError>) {
        match (result) {
            case .Ok(config) => applyConfig(config)
            case .Err(error) => logDecodeError(error)
        }
    }
    

总结

自定义枚举与HarmonyOS Next标准库的协同,本质是通过代数数据类型构建统一的错误处理与值管理体系:

  1. **Option**处理值的存在性,替代不安全的null
  2. **Result**处理操作失败,提供类型安全的错误信息;
  3. 自定义枚举扩展标准库语义,适配特定业务场景。

Vue 自定义进度条实现方法与应用场景解析

作者 spionbo
2025年6月8日 10:06

在前端开发中,进度条是一种常见的用户界面元素,用于展示任务的完成情况或者指示加载过程。然而,有时候标准的进度条并不能完全满足我们的需求,因此,我们可能需要创建自定义的进度条来实现特定的效果或功能。

在本文中,我将介绍如何使用Vue.js创建一个灵活多样的自定义进度条组件,该组件可以根据传入的数据动态渲染进度段,并且支持动画效果和内容展示。

Vue自定义进度条组件实践

一、组件设计目标

我们的进度条组件应具备以下功能:

  1. 接受一个包含进度段数据的数组作为输入。
  2. 根据传入的数据动态渲染进度段。
  3. 支持动画效果,当启用动画时,进度条会以动画形式展示。
  4. 可选择是否展示进度段的内容。
  5. 当前进度超出总长时超出部分红色填充。

二、实现步骤

  1. 创建Vue组件并定义props 首先,创建一个Vue组件,在组件中定义需要的props。这里我们需要定义一个数组props来接收进度段数据,还需要定义一些布尔类型props来控制是否展示内容、是否启用动画等。
export default {
  name: 'CustomProgress',
  props: {
    segmentsData: {
      type: Array,
      default: () => []
    },
    showContent: {
      type: Boolean,
      default: false
    },
    isAnimated: {
      type: Boolean,
      default: false
    },
    totalValue: {
      type: Number,
      default: 100
    }
  }
}
  1. 动态计算各进度段的宽度和样式 在组件的计算属性中,根据传入的进度段数据和总长度,计算每个进度段的宽度和对应的样式。
computed: {
  segments() {
    let currentValue = 0;
    return this.segmentsData.map((segment, index) => {
      const percentage = (segment.value / this.totalValue) * 100;
      const width = `${percentage}%`;
      const isLast = index === this.segmentsData.length - 1;
      const isExceed = currentValue + segment.value > this.totalValue;
      currentValue += segment.value;
      return {
      ...segment,
        width,
        isLast,
        isExceed
      };
    });
  }
}
  1. 在模板中使用v-for指令渲染进度段 在模板中,使用v-for指令遍历计算后的segments数组,为每个进度段渲染对应的DOM元素,并根据其属性应用相应的样式。
<template>
  <div class="progress-bar">
    <div
      v-for="(segment, index) in segments"
      :key="index"
      :style="{ width: segment.width, backgroundColor: segment.color }"
      :class="{ 'is-last': segment.isLast, 'is-exceed': segment.isExceed }"
    >
      <div
        v-if="segment.content && segment.value!== 0"
        :class="{ 'is-last': segment.isLast }"
      >
        {{ segment.content }}
      </div>
    </div>
    <div v-if="hasExceed" class="exceed-part"></div>
  </div>
</template>
  1. 处理动画效果 为了实现动画效果,我们可以利用CSS的动画特性。当isAnimatedtrue时,为进度条容器添加特定的类名,触发动画。
/* 定义动画 */
@keyframes ant - progress - active {
  0% {
    opacity: 0;
    transform: translateX(-100%);
  }
  50% {
    opacity: 0.5;
  }
  100% {
    opacity: 0;
    transform: translateX(100%);
  }
}
.is - animated {
  position: relative;
}
.is - animated::before {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z - index: 3;
  background: #fff;
  opacity: 0;
  animation: ant - progress - active 3s cubic - bezier(0.23, 1, 0.32, 1) infinite;
  content: '';
  cursor: default;
}

在模板中,根据isAnimated的值为进度条容器添加类名:

<template>
  <div class="progress-bar" :class="{ 'is - animated': isAnimated }">
    <!-- 进度段内容 -->
  </div>
</template>
  1. 处理超出部分 当当前进度超出总长时,需要将超出部分以红色填充。在计算属性中判断是否有超出部分:
computed: {
  hasExceed() {
    return this.segments.some(segment => segment.isExceed);
  }
}

在模板中,当有超出部分时,渲染一个额外的DOM元素来显示超出部分,并通过CSS设置其样式为红色:

.exceed - part {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 100%;
  background: #cc0000;
  z - index: 1;
}

三、组件使用示例

在父组件中使用我们创建的CustomProgress组件,传入相应的数据和配置。

<template>
  <div>
    <CustomProgress
      :totalValue="totalValue"
      :showContent="true"
      :isAnimated="true"
      :segmentsData="segmentsData"
    />
  </div>
</template>
<script>
import CustomProgress from './CustomProgress.vue';
export default {
  components: {
    CustomProgress
  },
  data() {
    return {
      totalValue: 80,
      segmentsData: [
        { value: 20, color: '#0099ff', content: '训练' },
        { value: 40, color: '#00b23b', content: '测试' }
      ]
    };
  }
}
</script>

Vue, 自定义进度条,前端开发,进度条实现方法,应用场景,JavaScript,HTML,CSS, 组件化开发,响应式设计,用户体验,Web 开发,动画效果,交互式组件,前端框架



资源地址: pan.quark.cn/s/35324205c…


四、总结

通过以上步骤,我们成功创建了一个功能丰富的Vue自定义进度条组件。它可以根据不同的数据和配置,灵活地展示进度情况,并且具有动画效果和对超出部分的处理。在实际项目中,你可以根据具体需求对这个组件进行进一步的优化和扩展,比如添加更多的自定义样式选项、支持不同的主题等,以更好地满足项目的UI设计和用户体验要求。

通过这篇文章,你应该对Vue自定义进度条有了初步认识。你在实际应用中,是否有特定的样式或交互需求想融入进度条呢?可以和我分享,咱们进一步探讨如何实现 。

跟着官方示例学习 @tanStack-table --- Column Ordering

2025年6月8日 08:06

🌲系列一:跟着官方示例学习 @tanStack-table --- Basic

🌲系列二:跟着官方示例学习 @tanStack-table --- Header Groups

🌲系列三:跟着官方示例学习 @tanStack-table --- Column Filters


🧱 列可见性切换

在实际项目中,我们经常需要根据用户需求隐藏不必要的列,或者让用户自由切换列的显示与否。例如:用户在查看员工信息时可能只关注姓名和职位,而不需要每次都看到访问次数或进度等辅助信息。

🧪 核心代码解析

⚙️ 1. 设置初始列定义:

const [columns] = React.useState(() => [...defaultColumns]);

通过 useState 列定义,确保列只初始化一次。

⚙️ 2. 控制列可见性的状态:

const [columnVisibility, setColumnVisibility] = React.useState({});

用于记录每个列的可见状态(key 是列 id,value 是 true 或 false)。

⚙️ 3. 在 useReactTable 中配置状态与回调:

const table = useReactTable({
  data,
  columns,
  state: { columnVisibility },
  onColumnVisibilityChange: setColumnVisibility,
  getCoreRowModel: getCoreRowModel(),
});

一旦用户勾选复选框,表格状态会同步更新并触发 UI 渲染。

⚙️ 4. UI 部分实现:

全选开关:

<input
  type="checkbox"
  checked={table.getIsAllColumnsVisible()}
  onChange={table.getToggleAllColumnsVisibilityHandler()}
/>

单列开关:

table.getAllLeafColumns().map((column) => (
  <input
    type="checkbox"
    checked={column.getIsVisible()}
    onChange={column.getToggleVisibilityHandler()}
  />
))

getAllLeafColumns API 🔗:获取所有叶子列(leaf columns),即最终渲染到表格中的“最底层的列”。

Jun-07-2025 14-54-35.gif

🔔 对官方示例代码可能存在一些删减的情况

代码地址🔗:Gitee

官方代码地址🔗: @tanStack/react-table

✨ 列排序

在某些使用场景中,我们希望用户可以 动态调整表格列的顺序,以便根据业务需求进行自定义展示。

🔧 如何实现列顺序切换?

我们先来看最核心的两行代码:

const [columnOrder, setColumnOrder] = React.useState<ColumnOrderState>([]);

table.setColumnOrder(
  faker.helpers.shuffle(table.getAllLeafColumns().map(d => d.id))
);

⚙️ 1. 步骤拆解:

初始化状态:

const [columnOrder, setColumnOrder] = React.useState<ColumnOrderState>([])

columnOrder 是一个数组,表示当前列的显示顺序(每个元素是一个列 ID)。

⚙️ 2. 配置到表格中:

const table = useReactTable({
  //...
  state: { columnOrder },
  onColumnOrderChange: setColumnOrder,
});

当列顺序发生改变时,React Table会使用你提供的onColumnOrderChange回调更新状态。

⚙️ 3. 动态修改顺序(示例是随机打乱顺序):

table.setColumnOrder(
  faker.helpers.shuffle(table.getAllLeafColumns().map(d => d.id))
);
// 返回所有叶子列(最底层列), 得到它们的 ID 列表
getAllLeafColumns().map(d => d.id)

// 用于打乱顺序
faker.helpers.shuffle()

最终将新的顺序通过 setColumnOrder 应用到表格中

Jun-07-2025 16-30-44.gif

🔔 对官方示例代码可能存在一些删减的情况

代码地址🔗:Gitee

官方代码地址🔗: @tanStack/react-table

🧀 列拖拽排序

🧲 用 DnD Kit 加持列排序交互

我们使用 @dnd-kit/core 来为表格添加拖拽能力

⚙️ 1. 每个列头变成可拖拽单元

我们创建了一个组件 <DraggableTableHeader />,它包裹住了每一个 <th>,并绑定了拖拽行为:

const DraggableTableHeader = ({ header }) => {
  const { setNodeRef, listeners, attributes, transform, isDragging } = useSortable({ id: header.column.id });

  const style = {
    transform: CSS.Translate.toString(transform),
    opacity: isDragging ? 0.8 : 1,
    //...
  }

  return (
    <th ref={setNodeRef} style={style}>
      {header.isPlaceholder
        ? null
        : flexRender(header.column.columnDef.header, header.getContext())}
      <button {...attributes} {...listeners}>🟰</button>
    </th>
  )
}

这个组件使用了 useSortable,我们传入当前列的 IDDnd Kit 会帮我们处理拖拽逻辑,transform 会让列在拖动时产生动画过渡。

⚙️ 2. 将所有列头包进 <SortableContext />

SortableContextDnD Kit 的容器,它告诉系统:这一组元素是可以排序的,排序方式是“水平排列”。

<SortableContext items={columnOrder} strategy={horizontalListSortingStrategy}>
  {headerGroup.headers.map(header => (
    <DraggableTableHeader key={header.id} header={header} />
  ))}
</SortableContext>

其中 items 是当前的列顺序(用 ID 表示),strategy 选择横向排序。

⚙️ 3. 配置 DndContext 管理拖拽行为

最外层我们包了一个 <DndContext>,这是 DnD Kit 的顶级组件,用于统一管理拖拽逻辑:

<DndContext
  sensors={sensors}
  collisionDetection={closestCenter}
  modifiers={[restrictToHorizontalAxis]}
  onDragEnd={handleDragEnd}
>
  {/* 表格渲染 */}
</DndContext>

我们使用 closestCenter 判断碰撞区域,使用 restrictToHorizontalAxis 限制只能左右拖动。

⚙️ 4. 拖拽完成后更新 columnOrder

当用户释放拖拽时,handleDragEnd 会被触发:

function handleDragEnd(event: DragEndEvent) {
  const { active, over } = event
  if (active && over && active.id !== over.id) {
    setColumnOrder((old) => {
      const oldIndex = old.indexOf(active.id)
      const newIndex = old.indexOf(over.id)
      return arrayMove(old, oldIndex, newIndex)
    })
  }
}

通过 arrayMove 工具函数,我们调整列的顺序数组 columnOrder,表格会自动根据新的顺序重新渲染。

⚙️ 5. 同步拖动列单元格

为了更真实的拖拽体验,不只是 <th> 头部,单元格 <td> 也要一起动。通过 <DragAlongCell /> 组件,实现列单元格的同步拖拽动画:

const DragAlongCell = ({ cell }) => {
  const { setNodeRef, transform } = useSortable({ id: cell.column.id })
  const style = {
    transform: CSS.Translate.toString(transform),
    ...
  }
  return <td ref={setNodeRef} style={style}>{...}</td>
}

并为每一行的每一格 <td> 也包进 SortableContext 中,确保拖动时位置变化一致:

<tr>
  {row.getVisibleCells().map(cell => (
    <SortableContext key={cell.id} items={columnOrder} strategy={horizontalListSortingStrategy}>
      <DragAlongCell key={cell.id} cell={cell} />
    </SortableContext>
  ))}
</tr>

Jun-07-2025 16-17-07.gif

🔔 对官方示例代码可能存在一些删减的情况

代码地址🔗:Gitee

官方代码地址🔗: @tanStack/react-table

一起来学习React哲学,理解数据驱动的现代框架

作者 XXUZZWZ
2025年6月8日 02:23

React 想给我们带来什么不同的思考?

官方文档说法

React 可以改变你对可见设计和应用构建的思考。当你使用 React 构建用户界面时,你首先会把它分解成一个个 组件,然后,你需要把这些组件连接在一起,使数据 流经 它们。 React官方链接 : React 哲学 – React 中文文档

怎么理解呢?

组件的思想

  1. 面对一个要实现的界面,React希望把界面划分为若干组件,然后再组合在一起。
  2. 每个组件都是独立的,有独立的样式交互逻辑

让数据在组件之间流动的思想

  1. 界面需要的数据可以在组件中传递。
  2. 传递有两个方向,正向数据流,父向子流动;子向父流动,被称为反向数据流。
  3. 在React中要求区别两种数据,简单来说就是静态的和动态的数据。
  4. 对于静态的数据只要在需要的组件内简单声明就可以,对于动态数据我们则需要React的特殊声明。

让我们结合一个简单例子来理解

假如我们接到一个开发这样一个页面的任务。

原型图: image.png

要渲染的静态数据:

[  


{ category: "Fruits", price: "$1", stocked: true, name: "Apple" },  


{ category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit" },  


{ category: "Fruits", price: "$2", stocked: false, name: "Passionfruit" },  


{ category: "Vegetables", price: "$2", stocked: true, name: "Spinach" },  


{ category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin" },  


{ category: "Vegetables", price: "$1", stocked: true, name: "Peas" }  


]

第一步我们该做什么呢?

将界面划分为若干组件,想好我们要写几个组件,以及组件之间的嵌套关系

像这样:

image.png

  1. FilterableProductTable(灰色)包含完整的应用。
  2. SearchBar(蓝色)获取用户输入。
  3. ProductTable(淡紫色)根据用户输入,展示和过滤清单。
  4. ProductCategoryRow(绿色)展示每个类别的表头。
  5. ProductRow(黄色)展示每个产品的行。

我们可以看到整个FilterableProductTable是最大的组件,容纳了 SearchBarProductTable两个子组件,ProductTable也包含了 ProductCategoryRowProductRow组件。

第二步我们该干什么呢?

我们应该先写出设计图的静态页面,那我们先写那个组件呢?从大到小还是从小到大呢? 一般来说面对小项目一般从大到小,中大项目一般从小到大。 我们这里采取从子

// 产品目录行 组件
function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}
// 产品行 组件
function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}
// 产品表 组件
function ProductTable({ products }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category} />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name} />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}
// 搜索栏组件
function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Search..." />
      <label>
        <input type="checkbox" />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}
// 要实现的筛选表界面组件 最终结果
function FilterableProductTable({ products }) {
  return (
    <div>
      <SearchBar />
      <ProductTable products={products} />
    </div>
  );
}
// 静态 数据
const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];
// 导出可以在外部引用
export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

第三步我们该干什么呢?

为了让界面实现交互,我们找出必须要用state表示的数据。

什么数据要用state表示呢? 这是React官网给出的建议:“考虑将 state 作为应用程序需要记住改变数据的最小集合。” 我们一起来看它的分析:

“ 现在考虑示例应用程序中的每一条数据:

  1. 产品原始列表
  2. 搜索用户键入的文本
  3. 复选框的值
  4. 过滤后的产品列表

其中哪些是 state 呢?标记出那些不是的:

  • 随着时间推移 保持不变?如此,便不是 state。
  • 通过 props 从父组件传递?如此,便不是 state。
  • 是否可以基于已存在于组件中的 state 或者 props 进行计算?如此,它肯定不是state!

剩下的可能是 state。

让我们再次一条条验证它们:

  1. 原始列表中的产品 被作为 props 传递,所以不是 state
  2. 搜索文本似乎应该是 state,因为它会随着时间的推移而变化,并且无法从任何东西中计算出来。
  3. 复选框的值似乎是 state,因为它会随着时间的推移而变化,并且无法从任何东西中计算出来。
  4. 过滤后列表中的产品 不是 state,因为可以通过被原始列表中的产品,根据搜索框文本和复选框的值进行计算

这就意味着只有搜索文本和复选框的值是 state!非常好!” 简要来说就是数据在和用户交互过程中产生的变化的数据,要用state 来管理。

第四步

验证 state 应该被放置在哪里

我们现在知道了有两个数据,一个是输入框的值,一个是一个复选框的值, ProductTable这个组件需要根据这两个值来筛选出要渲染的动态页面。SearchBar这个组件要根据一个是输入框的值来渲染输入的数据。这两个state要放在ProductTableSearchBar的父组件,所以我们要把这两个state放到FilterableProductTable,使得ProductTableSearchBar都能得到通过state方式产生的数据

function FilterableProductTable({ products }) {  


const [filterText, setFilterText] = useState('');  


const [inStockOnly, setInStockOnly] = useState(false);
//  ······ 其他省略
}

然后,filterTextinStockOnly 作为 props 传递至 ProductTableSearchBar

<div>

  <SearchBar

    filterText={filterText}

    inStockOnly={inStockOnly} />

  <ProductTable

    products={products}

    filterText={filterText}

    inStockOnly={inStockOnly} />

</div>

我们一起看看组件内是怎么使用传入的参数:

function SearchBar({ filterText, inStockOnly }) {  


return (  


<form>  

<input  
type="text"  
value={filterText}  
placeholder="Search..."/>

</form> )
}

第五步

步骤五:添加反向数据流

当state的数据和组件里的值绑定后,比如 SearchBar里的

 <input  
type="text"  
value={filterText}  
placeholder="Search..."/>

当input的value和{filterText} 绑定,如果我们不主动添加响应value变化的函数的话,React会忽视你的输入。效果如下

录屏_20250608_021217.gif 敲多少回车都没用。

SearchBar 中,添加一个 onChange 事件处理器,使用其设置父组件的 state:

function SearchBar({

  filterText,

  inStockOnly,

  onFilterTextChange,

  onInStockOnlyChange

}) {

  return (

    <form>

      <input

        type="text"

        value={filterText}

        placeholder="搜索"

        onChange={(e) => onFilterTextChange(e.target.value)}

      />

      <label>

        <input

          type="checkbox"

          checked={inStockOnly}

          onChange={(e) => onInStockOnlyChange(e.target.checked)}

现在应用程序可以完整工作了!

录屏_20250608_021511.gif 最后附上完整源码App.jsx文件里的所有内容

import { useState } from 'react';

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar
        filterText={filterText}
        inStockOnly={inStockOnly}
        onFilterTextChange={setFilterText}
        onInStockOnlyChange={setInStockOnly} />
      <ProductTable
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -1
    ) {
      return;
    }
    if (inStockOnly && !product.stocked) {
      return;
    }
    if (product.category !== lastCategory) {
      rows.push(
        <ProductCategoryRow
          category={product.category}
          key={product.category} />
      );
    }
    rows.push(
      <ProductRow
        product={product}
        key={product.name} />
    );
    lastCategory = product.category;
  });

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>{rows}</tbody>
    </table>
  );
}

function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange
}) {
  return (
    <form>
      <input
        type="text"
        value={filterText} placeholder="Search..."
        onChange={(e) => onFilterTextChange(e.target.value)} />
      <label>
        <input
          type="checkbox"
          checked={inStockOnly}
          onChange={(e) => onInStockOnlyChange(e.target.checked)} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

如果有有不理解的地方,可以去React 哲学 – React 中文文档看看,这篇博客基本就是基于这个文档写的。

React组件化开发实战:从"待办事项"看前端乐高搭建术

2025年6月8日 00:43

《React组件化开发:把前端变成乐高积木大师之旅》

警告:当你学会组件化思维后,看任何网站都会自动拆解成乐高积木,此症状不可逆!

一、开篇:从“一锅炖”到“分餐制”的进化史

还记得你第一次写网页的样子吗?一个HTML文件塞满1000行代码,CSS和JS在文件里打架——这就像把披萨、冰淇淋、螺蛳粉全倒进一个碗里搅拌(别试,会后悔的)。

而现代React开发如同米其林大厨备餐:

  • Vite 是智能厨房(自动处理火候/刀工)
  • 组件 是预制菜包(每个独立封装)
  • 数据流 是传菜机器人

就像我的TodoList项目,被拆解成三个精致“料理包”:

// 料理包1:TodoForm.jsx(食材输入机)
const handleSubmit = (e) => {
  e.preventDefault(); // 拦截百度外卖订单!
  onAdd(text)         // 呼叫主厨加菜
}

// 料理包2:TodoList.jsx(中央厨房)
const [todos, setTodos] = useState([ { id: 1, text: '吃饭' } ])
const handleAdd = (text) => {
  setTodos([...todos, { id: todos.length+1, text }]) 
}

// 料理包3:Todos.jsx(菜品展示台)
todos.map(todo => <li key={todo.id}>{todo.text}</li>)

二、组件化:前端的乐高革命

1. 为什么说DOM操作像用镊子搭积木?

传统开发如同用镊子组装微观乐高:

// 远古时代的痛苦记忆
document.querySelector('ul').innerHTML = 
  todos.map(todo => `<li>${todo.text}</li>`).join('')

每当数据变化就要:

  1. 找积木盒(querySelector)
  2. 拆旧积木(innerHTML = '')
  3. 拼新积木(拼接字符串)
  4. 手抖拼错全塌(页面崩溃)

2. React组件是智能积木块

想象有会自我更新的魔法积木:

// 积木块1:TodoForm
<Form魔法盒 onAdd={加菜方法} />

// 积木块2:TodoList
<中央厨房 
  菜单={todos} 
  加菜方法={handleAdd} 
/>

// 积木块3:Todos
<展示柜 菜单={todos} />

魔法原理:当你往中央厨房(TodoList)的菜单数组里塞新菜时:

setTodos([...todos, 新菜])

所有关联积木自动重组!就像乐高城市突然长出新建筑。

三、深度解密组件通信:组件间的摩斯密码

1. Props:父子组件的“悄悄话管道”

TodoList.jsx中:

<TodoForm onAdd={handleAdd} /> {/* 塞给儿子一部对讲机 */}
<Todos todos={todos} />        {/* 递给女儿一张菜单 */}

子组件接收时如同拆快递:

// TodoForm.jsx
function TodoForm(props) {
  props.onAdd(text) // 用对讲机呼叫爸爸
}

// Todos.jsx
function Todos(props) {
  props.todos.map(...) // 查看爸爸给的菜单
}

2. 数据流:单向快递系统

React的数据流像严格的快递网络:

中央仓库(setTodos) → 卡车(props) → 子组件签收

禁止反向运输!  子组件不能直接修改props(就像不能篡改快递单)

四、useState:给组件装上“记忆芯片”

1. 变量失忆症治疗指南

普通变量刷新就失忆:

let todos = [] // 刷新页面?失忆了

useState给变量装上记忆芯片:

const [todos, setTodos] = useState(() => {
  // 首次加载从缓存读取记忆
  return JSON.parse(localStorage.getItem('todos')) || []
})

2. 数据驱动UI:魔法镜子原理

想象todos数组是现实世界,UI是它的魔法镜像:

// 现实世界改变...
setTodos([...todos, { text: "写React博客", completed: false }])

// 魔法镜像自动同步更新!
<ul>
  <li>吃饭</li>
  <li>睡觉</li>
  <li>写React博客</li> {/* 自动出现! */}
</ul>

这就是为什么叫“数据驱动” ——数据是提线木偶师,UI是听话的木偶。

五、实战黑科技:动态TodoList诞生记

1. 表单拦截术:从百度嘴里抢回数据

TodoForm.jsx中上演谍战大戏:

<form action="http://www.baidu.com" onSubmit={handleSubmit}>
  {/* 表面伪装成百度搜索... */}
  <input type="text" placeholder="伪装成搜索框" />
  
  {/* 实际暗度陈仓 */}
  {handleSubmit(e) => {
    e.preventDefault() // 截获百度快递车!
    onAdd(text)        // 把数据偷运给自家仓库
  }}
</form>

2. 数组更新绝技:三明治堆叠法

添加新任务像做三明治:

setTodos([
  ...todos, // 1. 铺下面包(原数组)
  {         // 2. 放新食材(新对象)
    id: todos.length + 1, 
    text: "学习React魔法",
    completed: false 
  }          // 3. 自动封装!(新数组)
])

3. Key的重要性:给积木贴防撞条

渲染列表时:

todos.map(todo => (
  <li key={todo.id}>{todo.text}</li>
))

没有key的后果:React会哭喊着:“这些积木长得都一样!我分不清谁是谁!” 然后胡乱重新排列,导致性能崩溃。

六、组件化哲学:从码农到乐高大师

1. 组件设计黄金法则

  • 单一职责原则:像瑞士军刀,但每个工具独立

    我的TodoForm只管输入,Todos只管展示,绝不越界

  • 可配置性:预留插槽如乐高凸点

    // 高度可配置的标题
    <TodoList title="今日待办" theme="dark" />
    
  • 无状态优先:尽量做“傻白甜”组件

    // 最佳实践:只负责展示的傻组件
    const Display = ({ value }) => <div>{value}</div>
    

2. 数据流架构图

deepseek_mermaid_20250607_57184a.png

七、血泪教训:新手村避坑指南

  1. Props命名惨案
// 父组件传参
<Todos todoList={todos} />

// 子组件拆包
function Todos(props) {
  props.todos.map(...) // 报错!实际叫todoList
}

急救方案:解构时直接重命名
const { todoList: todos } = props

  1. 直接修改状态灾难
// 错误示范(引发静默失效)
todos.push(newTodo) 
setTodos(todos) // React:这俩不是同一个数组?不理你!

// 正确姿势(创建新数组)
setTodos([...todos, newTodo])
  1. Key的重复危机
// 用索引当key?删除第二项时...
[<li key=0>A</li>, <li key=1>B</li>, <li key=2>C</li>]
// 删除B后:
[<li key=0>A</li>, <li key=1>C</li>] // React以为B→C变了!

黄金准则:用唯一ID(如数据库id/crypto.randomUUID())

八、终极思考:为什么说React是界面编程的范式革命?

回顾传统方式添加一个待办事项:

sequenceDiagram
   程序员->>DOM: 找到ul元素
   程序员->>DOM: 创建li元素
   程序员->>DOM: 设置li内容
   程序员->>DOM: 插入ul末尾

React范式下:

sequenceDiagram
   程序员->>数据: setTodos(更新数组)
   数据->>React: 通知状态变更
   React->>虚拟DOM: 计算差异
   虚拟DOM->>真实DOM: 精准更新

本质区别:从“指挥DOM做每个动作”变成“声明数据状态”,如同从驾驶马车变成设置GPS导航。

结语:你的乐高帝国正在崛起

当你掌握组件化思维后:

  • 看到按钮 → “这是个Button组件”
  • 看到导航栏 → “NavBar+MenuItem组合”
  • 看到整个页面 → “Header, Content, Footer三大模块”

现在尝试给你的TodoList添加新功能:

  1. TodoItem组件中添加删除按钮
  2. 通过父组件传递onDelete回调
  3. 使用filter更新状态:
// TodoList.jsx
const handleDelete = (id) => {
  setTodos(todos.filter(todo => todo.id !== id))
}

挑战:如何让待办事项支持完成状态切换?提示:map+条件样式

记住React哲学的核心口诀: “UI是数据的函数” 。当你下次对着页面发呆时,不妨想想——眼前的一切,不过是数据的精致舞衣罢了。

纯css实现一个沙漏动画

作者 JYeontu
2025年6月8日 00:37

说在前面

沙漏大家都见过吧,使用js的话相信大家都能很轻易地实现一个沙漏动画吧,今天我们纯css来实现一个试试。

在线体验

码上掘金

codePen

codepen.io/yongtaozhen…

代码实现

html

<div class="container">
  <div class="hourglass">
    <div class="frame"></div>
    <div class="glass"></div>
    <div class="sand-top-stroke"></div>
    <div class="sand-top"></div>
    <div class="sand-bottom-stroke"></div>
    <div class="sand-bottom"></div>
    <div class="sand-flow"></div>
    <div class="sand-drop"></div>
    <div class="glass-reflection"></div>
  </div>
</div>
  • container:包裹整个沙漏,调整整个沙漏的定位
  • hourglass:沙漏的主容器
  • frame:沙漏的外框,一个木架子

  • sand-top-strokesand-bottom-stroke:存放沙子的玻璃容器

  • sand-topsand-bottom:上下部分的沙子

  • sand-flow:连接上下两部分沙漏的管道

  • sand-drop:滴落的沙子

  • glass-reflection:添加一个玻璃反光效果

css

通用变量

:root {
  --rotateTime: 10s;
}

定义沙漏动画时间,在动画中需要用到。

沙漏翻转

.hourglass {
  position: relative;
  width: 120px;
  height: 200px;
  margin: 0 auto;
  animation: rotate var(--rotateTime) linear infinite;
  transform-origin: center 100px;
}
@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }
  45% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(180deg);
  }
  99% {
    transform: rotate(180deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

计时结束(沙漏沙子漏完)后需要将整个沙漏框架翻转,在rotateTime时间内沙漏需要翻转2次,也就是说沙漏漏完一次的时间是rotateTime

沙堆减少和增加

.sand-top {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 90px;
  background-color: #f5d76e;
  clip-path: polygon(0 0, 100% 0, 51% 100%, 49% 100%);
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
  animation: sand-top var(--rotateTime) linear infinite;
}
.sand-bottom {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 0;
  background-color: #f5d76e;
  clip-path: polygon(49% 0, 51% 0, 100% 100%, 0 100%);
  animation: sand-bottom var(--rotateTime) linear infinite;
  border-bottom-left-radius: 10px;
  border-bottom-right-radius: 10px;
}
/* 上半部分沙子减少动画 */
@keyframes sand-top {
  0% {
    height: 90px;
    width: 100%;
  }
  49% {
    height: 0;
    width: 0;
    left: 50%;
    top: 90px;
  }
  50% {
    height: 0px;
    width: 100%;
    top: 0;
    left: 0;
  }
  99% {
    width: 100%;
    height: 90px;
  }
  100% {
    width: 100%;
    height: 90px;
  }
}

/* 下半部分沙子增加动画 */
@keyframes sand-bottom {
  0% {
    height: 0;
    width: 100%;
    bottom: 0;
    left: 0;
  }
  50% {
    height: 90px;
    width: 100%;
    bottom: 0;
    left: 0;
  }
  51% {
    height: 90px;
    width: 100%;
    bottom: 0;
    left: 0;
  }
  99% {
    height: 0px;
    width: 0;
    bottom: 90px;
    left: 50%;
  }
  100% {
    height: 0px;
    width: 0;
    bottom: 90px;
    left: 50%;
  }
}

上半部分沙堆需要先减少后增加,下半部分沙堆需要先增加后减少;通过改变高度、宽度和位置,模拟沙子流动的过程。

管道沙子滴落

@keyframes sand-drop {
  0% {
    opacity: 1;
    transform: translate(-50%, 0);
  }
  4.9%,
  9.9%,
  14.9%,
  ……,
  44.9%,
  49.9% {
    opacity: 0;
    transform: translate(-50%, 15px);
  }
  5%,
  10%,
  15%,
  ……,
  40%,
  45% {
    opacity: 0;
    transform: translate(-50%, 0);
  }
  5.1%,
  10.1%,
  ……,
  90.1%,
  95.1% {
    opacity: 1;
  }
  50%,
  55%,
  ……,
  100% {
    opacity: 0;
    transform: translate(-50%, -15px);
  }
  54.9%,
  59.9%,
  64.9%,
  ……,
  94.9%,
  99.9% {
    opacity: 0;
    transform: translate(-50%, -30px);
  }
}

通过控制透明度和位移,模拟沙子滴落的连贯动作,每5%的动画时间完成一个滴落动作,以10s为例的话也就是每0.5s完成一个滴落动作;因为沙子漏完之后整个沙漏需要翻转180°,所以沙子滴落的动画应该分为前后两段,前半段是从上往下滴( transform: translate(-50%, 15px) ),后半段是从下往上滴( transform: translate(-50%, -15px) )。

源码

gitee

gitee.com/zheng_yongt…

github

github.com/yongtaozhen…


  • 🌟 觉得有帮助的可以点个 star~
  • 🖊 有什么问题或错误可以指出,欢迎 pr~
  • 📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

Promise 解决过程(Promise Resolution Procedure)详解

作者 古夕
2025年6月8日 00:22

Promise 解决过程是 Promise A+ 规范的核心机制,用于处理 then 回调返回值与 Promise 状态的关系。以下从规范角度结合代码示例解析这一过程。

一、解决过程的核心定义

Promise 解决过程是一个抽象操作 [[Resolve]](promise, x),表示将 promise 的状态与值根据 x 的类型和状态进行解析。其核心逻辑是:x 是 Promise 或 Thenable(具有 then 方法),则使 promise 接受 x 的状态;否则用 x 的值完成 promise

二、解决过程的具体规则

1. xpromise 为同一对象

  • 规则:若 promisex 指向同一实例,以 TypeError 拒绝 promise,避免循环引用。
  • 示例
    const promise = new Promise(resolve => {
      resolve(promise); // promise 和 x 为同一对象
    });
    
    promise.then(
      value => console.log('成功'),
      reason => console.log('错误:', reason) // 输出:错误: TypeError
    );
    

2. x 是 Promise 实例

  • 规则
    • x 处于 pendingpromise 保持 pending,直至 x 状态确定。
    • x 处于 fulfilledpromise 以相同值 fulfilled
    • x 处于 rejectedpromise 以相同原因 rejected
  • 示例
    function createPromise(delay, value) {
      return new Promise(resolve => {
        setTimeout(() => resolve(value), delay);
      });
    }
    
    const p1 = createPromise(100, 'p1 resolved');
    const p2 = new Promise(resolve => {
      resolve(p1); // x 是 Promise
    });
    
    p2.then(
      value => console.log('p2 结果:', value), // 输出:p2 结果: p1 resolved
      reason => console.log('p2 错误:', reason)
    );
    

3. x 是 Object 或 Function(Thenable)

  • 规则
    1. 尝试获取 x.then,若获取时抛出异常 e,则以 e 拒绝 promise
    2. then 是函数,调用 x.then(resolvePromise, rejectPromise),其中:
      • resolvePromise(y):递归执行 [[Resolve]](promise, y)
      • rejectPromise(r):以 r 拒绝 promise
      • resolvePromiserejectPromise 被多次调用,仅首次有效。
    3. 若调用 then 时抛出异常 e,且 resolvePromise/rejectPromise 未被调用,则以 e 拒绝 promise
    4. then 不是函数,以 x 完成 promise
  • 示例
    // 定义 Thenable 对象
    const thenable = {
      then(resolve, reject) {
        console.log('调用 then 方法');
        setTimeout(() => {
          resolve('thenable value'); // 调用 resolvePromise
        }, 500);
      }
    };
    
    const promise = new Promise(resolve => {
      resolve(thenable); // x 是 Thenable
    });
    
    promise.then(
      value => console.log('结果:', value), // 输出:结果: thenable value
      reason => console.log('错误:', reason)
    );
    

4. x 是普通值(非对象、非函数)

  • 规则:以 x 直接完成 promise
  • 示例
    const promise = new Promise(resolve => {
      resolve(42); // x 是普通值
    });
    
    promise.then(
      value => console.log('普通值结果:', value), // 输出:普通值结果: 42
      reason => console.log('错误:', reason)
    );
    

三、解决过程在链式调用中的体现

function promise1() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('promise1 完成');
      resolve('p1 value');
    }, 1000);
  });
}

function promise2() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('promise2 完成');
      resolve('p2 value');
    }, 2000);
  });
}

// 链式调用中返回 Promise
promise1()
  .then(value => {
    console.log('then1 接收值:', value); // then1 接收值: p1 value
    return promise2(); // x 是 Promise,触发解决过程
  })
  .then(value => {
    console.log('then2 接收值:', value); // then2 接收值: p2 value(等待 promise2 完成)
  })
  .catch(error => {
    console.log('错误:', error);
  });

解决过程解析

  1. promise1 完成后,then1 返回 promise2x 是 Promise)。
  2. [[Resolve]](promise2, x) 执行:
    • promise2 处于 pending,当前 then2 的 Promise 保持 pending
    • promise2 完成时,当前 Promise 以相同值完成,触发 then2 回调。

四、解决过程中的异常处理

const promise = new Promise(resolve => {
  // 模拟 Thenable 中抛出异常
  resolve({
    then: function() {
      throw new Error('Thenable 内部错误');
    }
  });
});

promise.then(
  value => console.log('成功:', value),
  reason => console.log('错误:', reason) // 输出:错误: Thenable 内部错误
);

异常流程

  1. resolve 传入 Thenable 对象。
  2. 获取 x.then 时未出错,但调用 x.then 时抛出异常。
  3. 解决过程捕获异常,以该异常拒绝 promise

五、解决过程的核心意义

  1. 保证状态一致性:无论 x 是普通值、Promise 还是 Thenable,解决过程确保 promise 状态与 x 正确关联。
  2. 支持链式调用:通过递归处理 x,实现 Promise 链的无缝衔接。
  3. 兼容不同实现:使原生 Promise 与第三方 Promise 库(如 Bluebird)可相互操作。

理解 Promise 解决过程是掌握 Promise 异步编程的关键,尤其在处理复杂链式调用和异常场景时,能帮助开发者预测和调试代码行为。

Promise A+ 规范解读

作者 古夕
2025年6月8日 00:20

前言

任何符合 promise 规范的对象或函数都可以成为 promise,promise A plus 规范地址:promisesaplus.com/

术语

  • Promise:promise 是一个拥有 then 方法的对象或函数,其行为符合本规范。
  • 具有 then 方法(thenable):是一个定义了 then 方法的对象或函数;
  • 值(value):指任何 JavaScript 的合法值(包括 undefined,thenable 和 promise);
  • 异常(exception):是使用 throw 语句抛出的一个值。
  • 原因(reason):表示一个 promise 的拒绝原因。

promise 的状态

一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、已完成(Fulfilled)和已拒绝(Rejected)。

  • 处于等待态时,promise 需满足以下条件:可以变为「已完成」或「已拒绝」
  • 处于已完成时,promise 需满足以下条件:
    • 不能迁移至其他任何状态
    • 必须拥有一个不可变的值
  • 处于已拒绝时,promise 需满足以下条件:
    • 不能迁移至其他任何状态
    • 必须拥有一个不可变的原因

必须有一个 then 方法

一个 promise 必须提供一个 then 方法以访问其当前值和原因。 promise 的 then 方法接受两个参数:promise.then(onFulfilled, onRejected) 他们都是可选参数, 同时他们都是函数,如果 onFulfilled 或 onRejected 不是函数,则需要忽略他们。

  • 如果 onFulfilled 是一个函数
    • 当 promise 执行结束后其必须被调用,其第一个参数为 promise 的值
    • 在 promise 执行结束前其不可被调用
    • 其调用次数不可超过一次
  • 如果 onRejected 是一个函数
    • 当 promise 被拒绝执行后其必须被调用,其第一个参数为 promise 的原因
    • 在 promise 被拒绝执行前其不可被调用
    • 其调用次数不可超过一次

其他规则

  • 在执行上下文堆栈仅包含平台代码之前,不得调用 onFulfilled 或 onRejected
  • onFulfilled 和 onRejected 必须被作为普通函数调用(即非实例化调用,这样函数内部 this 非严格模式下指向 window)
  • then 方法可以被同一个 promise 调用多次
    • 当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
    • 当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
  • then 方法必须返回一个 promise 对象 promise2 = promise1.then(onFulfilled, onRejected);
    • 只要 onFulfilled 或者 onRejected 返回一个值 x,promise 2 都会进入 onFulfilled 状态
    • 如果 onFulfilled 或者 onRejected 抛出一个异常 e,则 promise2 必须拒绝执行,并返回拒因 e
    • 如果 onFulfilled 不是函数且 promise1 状态变为已完成,promise2 必须成功执行并返回相同的值
    • 如果 onRejected 不是函数且 promise1 状态变为已拒绝,promise2 必须执行拒绝回调并返回相同的据因
var promise1 = new Promise((resolve, reject) => {
    reject();
});
const promise2 = promise1
   .then(null, function() {
        return 123
    });

promise2
   .then(
        () => {
            console.log('promise2 已完成');
        },
        () => {
            console.log('promise2 已拒绝');
        }
    );

Promise 基础概念与实践详解

作者 古夕
2025年6月8日 00:14

在支持 ES6 的高级浏览器环境中,我们通过 new Promise() 即可构造一个 Promise 实例。这个构造函数接受一个函数作为参数,该函数分别接收两个参数 resolvereject,用于将当前实例的状态改变为 已完成已拒绝

一、Promise 实例与构造函数核心特性

  • 实例方法:Promise 实例可通过 .then 方法注册回调函数,根据实例状态(已完成/已拒绝)执行对应回调。
  • 状态控制:通过 new Promise() 构造函数的回调函数可动态更改实例状态。
  • 本质:Promise 并非新语法,而是一个新函数。
  • 执行机制:Promise 同步注册,异步执行。

二、Promise 链式调用示例

function promise1() {
    return new Promise(function(resolve, reject) {
        // 定义异步操作
        setTimeout(function() {
            console.log('1s 后输出');
            // 执行 resolve 标记为已完成,继续执行 then 链
            resolve();
        }, 1000);
    });
}

function promise2() {
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log('2s 后输出');
            resolve();
        }, 2000);
    });
}

// 链式调用写法
promise1().then(function() { return promise2(); });
// 简写形式
promise1().then(promise2);

执行结果:1 秒后输出 "1s 后输出",再过 2 秒输出 "2s 后输出"。

核心逻辑:当 Promise 状态变为 已完成(调用 resolve),则执行 .then 中的下一个 Promise 函数;若状态变为 已拒绝(调用 reject),则进入异常处理函数。

三、Promise 状态处理与参数传递

function promise3() {
    return new Promise(function(resolve, reject) {
        const random = Math.random() * 10; // 生成 0-10 的随机数
        setTimeout(function() {
            if (random >= 5) {
                resolve(random); // 状态变为已完成,传递参数
            } else {
                reject(random);  // 状态变为已拒绝,传递参数
            }
        }, 1000);
    });
}

const onResolve = function(val) {
    console.log('已完成: 输出的数字是', val);
};

const onReject = function(val) {
    console.log('已拒绝: 输出的数字是', val);
};

// 方式1:通过 then 接收两个回调
promise3().then(onResolve, onReject);

// 方式2:通过 catch 捕获异常
promise3().catch(onReject).then(onResolve);

// 方式3:通过 try catch 拦截
try {
    promise3().then(onResolve);
} catch (e) {
    onReject(e);
}

关键说明

  • 三种方式可拦截 已拒绝 状态的 Promise:then 的第二个参数、.catch 方法、try catch
  • resolvereject 可传递参数,供后续 .then 回调接收。

四、Promise 核心概念总结

  1. 状态机制

    • Promise 有三种状态:进行中(pending)已完成(fulfilled)已拒绝(rejected)
    • 状态仅能从 pending 转为 fulfilledrejected,且一旦变更不可逆转。
  2. 构造函数

    • ES6 中通过 new Promise(executor) 构造实例,executor 接收 resolvereject 函数。
    • 执行 resolve 使状态变为 fulfilled,执行 reject 使状态变为 rejected
  3. 链式调用

    • 通过 .then 方法在 Promise 状态为 fulfilled 时继续执行后续逻辑。
    • resolve/reject 传递的参数可被后续 .then 回调获取。
  4. 异常处理

    • 已拒绝的 Promise 可通过 .catchthen 的第二个参数或 try catch 捕获处理。

深度解析:解决大型 Git 仓库克隆失败的完整指南

2025年6月8日 00:11

深度解析:解决大型 Git 仓库克隆失败的完整指南

问题背景与现象

在尝试克隆一个包含大量历史提交、大文件或众多分支的大型 Git 仓库时,通常会遇到以下问题:

$ git clone https://github.com/large-repo.git
Cloning into 'large-repo'...
remote: Enumerating objects: 2500000, done.
remote: Counting objects: 100% (2500000/2500000), done.
error: RPC failed; curl 18 transfer closed with outstanding read data remaining
fatal: the remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed

根本原因分析

1. 网络传输限制

  • HTTP 缓冲区大小限制:Git 默认的 http.postBuffer 较小(1MB)
  • 网络不稳定:长时间传输过程中的网络波动
  • 服务器限制:Git 服务器端的传输超时设置

2. 资源限制

  • 内存不足:克隆过程中的解包操作需要大量内存
  • 磁盘空间不足:大型仓库需要足够的临时空间

3. 仓库特性

  • 深度历史:包含数万次提交
  • 大文件(LFS):未正确配置 Git LFS
  • 分支过多:特别是包含大量旧分支

系统化解决方案

方案一:优化 Git 配置(推荐首选)

# 增加 HTTP 缓冲区大小 (500MB)
git config --global http.postBuffer 524288000

# 提高内存限制 (4GB)
git config --global pack.deltaCacheSize 2048m
git config --global pack.packSizeLimit 2048m
git config --global pack.windowMemory 2048m

# 启用压缩
git config --global core.compression 9

# 使用更快的 HTTP 版本
git config --global http.version HTTP/1.1

# 设置低速限制(避免超时)
git config --global http.lowSpeedLimit 0
git config --global http.lowSpeedTime 999999

方案二:分阶段克隆

# 1. 创建空仓库
mkdir large-repo && cd large-repo
git init

# 2. 启用部分克隆功能
git config core.repositoryFormatVersion 1
git config extensions.partialClone origin

# 3. 获取最小必要数据
git remote add origin https://github.com/large-repo.git
git fetch --filter=blob:none --depth=1 origin

# 4. 检出默认分支
git checkout -b main origin/main

# 5. 按需获取完整历史(可选)
git fetch --unshallow

方案三:浅层克隆 + 渐进式获取

# 1. 浅层克隆(仅获取最新提交)
git clone --depth 1 https://github.com/large-repo.git

# 2. 进入仓库
cd large-repo

# 3. 逐步获取更多历史
git fetch --depth=100

# 4. 获取完整历史(当需要时)
git fetch --unshallow

方案四:使用 Git Bundle(离线迁移)

# 在可访问仓库的机器上:
git bundle create repo.bundle --all

# 传输 bundle 文件(使用 rsync/scp)
scp repo.bundle user@target-machine:/path/

# 在目标机器上:
git clone repo.bundle -b main large-repo

方案五:处理 Git LFS 大文件

# 1. 安装 Git LFS
git lfs install

# 2. 指定 LFS 跟踪模式
GIT_LFS_SKIP_SMUDGE=1 git clone https://github.com/large-repo.git

# 3. 进入仓库
cd large-repo

# 4. 按需下载大文件
git lfs pull --include="path/to/large/files"

故障排查工具箱

1. 诊断命令

# 检查仓库大小
git count-objects -vH

# 测试服务器连接
GIT_TRACE_PACKET=1 GIT_TRACE=1 GIT_CURL_VERBOSE=1 \
git clone -v https://github.com/large-repo.git

2. 网络优化

# 使用 SSH 替代 HTTPS
git clone git@github.com:large-repo.git

# 启用多路复用
git config --global ssh.variant ssh
git config --global ssh.multiplexing yes

3. 资源监控

# 实时监控 git 进程
watch -n 1 "ps aux | grep 'git' | grep -v grep"

# 监控网络流量
nethogs -t

总结与最佳实践

  1. 评估需求:是否真的需要完整历史?最新代码是否足够?
  2. 渐进式克隆:优先使用 --depth 1--filter=blob:none
  3. 资源预配:确保至少 2 倍于仓库大小的可用内存和磁盘空间
  4. 网络优化:使用有线连接,企业环境配置 Git 代理
  5. LFS 处理:对于二进制文件,务必正确配置 Git LFS
  6. 监控诊断:使用诊断命令识别具体瓶颈

关键提示:对于超大型仓库(>10GB),考虑使用分片克隆策略:

# 克隆主干
git clone --single-branch --branch main https://github.com/large-repo.git

# 按需添加其他分支
git remote set-branches --add origin dev-branch
git fetch origin dev-branch

通过以上系统化的解决方案,即使面对数十 GB 的巨型仓库,也能高效可靠地完成克隆操作。

前端文件下载实现深度解析:Blob与ObjectURL的完美协作

2025年6月7日 23:47

前端文件下载实现深度解析:Blob与ObjectURL的完美协作

函数源码分析

export const downFile = (content: Blob, fileName: string) => {
  // 创建Blob对象(确保内容类型正确)
  const blob = new Blob([content]);
  
  // 创建虚拟下载链接
  const downloadLink = document.createElement('a');
  
  // 配置下载属性
  downloadLink.download = fileName;
  downloadLink.style.display = 'none';
  
  // 生成Blob URL
  downloadLink.href = URL.createObjectURL(blob);
  
  // 挂载到DOM并触发点击
  document.body.appendChild(downloadLink);
  downloadLink.click();
  
  // 清理资源
  URL.revokeObjectURL(downloadLink.href);
  document.body.removeChild(downloadLink);
};

核心机制解析

1. Blob对象处理

const blob = new Blob([content]);
  • 作用:将任意内容封装为浏览器可处理的二进制对象
  • 注意事项
    • 支持ArrayBuffer、String、TypedArray等数据类型
    • 可指定MIME类型:new Blob([content], {type: 'application/pdf'})
    • 大文件切片处理:new Blob(chunks, {type: 'video/mp4'})

2. Object URL生命周期管理

// 创建临时URL
const objectURL = URL.createObjectURL(blob);

// 释放内存
URL.revokeObjectURL(objectURL);
  • 内存机制

    • 每个ObjectURL占用独立内存空间
    • 需手动释放防止内存泄漏
    • 最佳实践:在click()后立即释放
  • URL特性

    • 格式:blob:https://example.com/550e8400-e29b-41d4-a716-446655440000
    • 作用域:当前文档内有效
    • 生命周期:与创建文档绑定

3. 虚拟链接的创建与触发

const downloadLink = document.createElement('a');
downloadLink.download = 'report.pdf';
downloadLink.click();
  • 关键属性

    • download:指定下载文件名(跨域资源无效)
    • href:绑定ObjectURL
    • target:可设置为_blank强制新窗口打开
  • 浏览器兼容性

    • 现代浏览器全支持
    • IE10+部分支持(需polyfill)

性能优化实践

1. 大文件下载优化

// 分片流式下载
const chunkSize = 5 * 1024 * 1024; // 5MB分片
for (let start = 0; start < file.size; start += chunkSize) {
  const chunk = file.slice(start, start + chunkSize);
  downFile(chunk, `part-${start/chunkSize}.bin`);
}

2. 内存泄漏防护

// 封装安全下载器
const safeDownload = (content, filename) => {
  try {
    downFile(content, filename);
  } catch (error) {
    console.error('下载失败:', error);
    // 强制释放资源
    if(downloadLink?.href) URL.revokeObjectURL(downloadLink.href);
  } finally {
    // 确保移除DOM节点
    if(downloadLink?.parentNode) document.body.removeChild(downloadLink);
  }
}

应用场景示例

1. 导出CSV报表

const exportCSV = (data) => {
  const csvContent = data.map(row => row.join(',')).join('\n');
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
  downFile(blob, '报表.csv');
}

2. 前端生成PDF下载

const generatePDF = async () => {
  const pdfDoc = await PDFDocument.create();
  const page = pdfDoc.addPage();
  page.drawText('前端生成PDF内容');
  
  const pdfBytes = await pdfDoc.save();
  downFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'document.pdf');
}

3. 大文件断点续传

const resumeDownload = async (fileId) => {
  const { chunks, fileName } = await getDownloadState(fileId);
  
  const newChunks = [];
  for (let i = chunks.length; i < totalChunks; i++) {
    const chunk = await fetchChunk(fileId, i);
    newChunks.push(chunk);
    saveChunkState(fileId, i, chunk);
  }
  
  downFile(new Blob([...chunks, ...newChunks]), fileName);
}

关键知识点总结

  1. Blob对象:浏览器中表示原始二进制数据的核心API
  2. Object URL:将内存数据转化为可访问URL的黑科技
  3. 内存管理:及时调用revokeObjectURL()防止内存泄漏
  4. 文件名安全:处理特殊字符和路径分隔符
  5. 大文件策略:分片下载与断点续传实现
  6. 跨域处理:通过fetch代理解决跨域限制

最佳实践建议:生产环境中建议添加下载超时控制(30秒自动取消)和错误重试机制,对于超过100MB的文件推荐使用Service Worker进行后台下载管理。

通过深入理解Blob和ObjectURL的协作机制,开发者可以创建出高效可靠的前端文件下载方案,满足各种复杂业务场景的需求。

SSE的使用

作者 Struggler281
2025年6月7日 23:44

SSE的使用

客户端请求头

1. 基本请求头

const eventSource = new EventSource('/api/stream');

浏览器会自动发送以下请求头:

GET /api/stream HTTP/1.1
Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

2. 自定义请求头配置

注意:EventSource API 本身不支持自定义请求头,如需自定义头部,可以使用以下方案:

方案一:通过 URL 参数传递认证信息
const token = localStorage.getItem('authToken');
const eventSource = new EventSource(`/api/stream?token=${token}`);
方案二:使用 fetch API 实现类似 SSE 的功能
async function createSSEConnection(url, headers = {}) {
    const response = await fetch(url, {
        method: 'GET',
        headers: {
            'Accept': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Authorization': `Bearer ${token}`,
            ...headers
        }
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
        const { value, done } = await reader.read();
        if (done) break;
      
        const chunk = decoder.decode(value);
        const lines = chunk.split('\n');
      
        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const data = line.slice(6);
                // 处理数据
                handleSSEMessage(data);
            }
        }
    }
}

服务端响应头配置

1. 必需的响应头

// Node.js/Express 示例
app.get('/api/stream', (req, res) => {
    // 设置 SSE 必需的响应头
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*', // CORS
        'Access-Control-Allow-Headers': 'Cache-Control',
        'X-Accel-Buffering': 'no' // Nginx 禁用缓冲
    });
});

2. CORS 相关配置

app.get('/api/stream', (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      
        // CORS 配置
        'Access-Control-Allow-Origin': 'http://localhost:3000',
        'Access-Control-Allow-Credentials': 'true',
        'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
        'Access-Control-Allow-Methods': 'GET, OPTIONS'
    });
});

// 处理 OPTIONS 预检请求
app.options('/api/stream', (req, res) => {
    res.writeHead(200, {
        'Access-Control-Allow-Origin': 'http://localhost:3000',
        'Access-Control-Allow-Credentials': 'true',
        'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
        'Access-Control-Allow-Methods': 'GET, OPTIONS'
    });
    res.end();
});

3. 认证和安全相关头部

app.get('/api/stream', authenticateToken, (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      
        // 安全相关
        'X-Content-Type-Options': 'nosniff',
        'X-Frame-Options': 'DENY',
        'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
      
        // CORS
        'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN,
        'Access-Control-Allow-Credentials': 'true'
    });
});

// 中间件:验证 token
function authenticateToken(req, res, next) {
    const token = req.query.token || req.headers.authorization?.split(' ')[1];
  
    if (!token) {
        return res.status(401).json({ error: 'Token required' });
    }
  
    // 验证 token 逻辑
    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) return res.status(403).json({ error: 'Invalid token' });
        req.user = user;
        next();
    });
}

反向代理配置

Nginx 配置

location /api/stream {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  
    # SSE 特有配置
    proxy_cache_bypass $http_upgrade;
    proxy_buffering off;
    proxy_read_timeout 24h;
    proxy_send_timeout 24h;
  
    # 禁用缓冲
    proxy_set_header X-Accel-Buffering no;
}

Apache 配置

<Location "/api/stream">
    ProxyPass http://backend/api/stream
    ProxyPassReverse http://backend/api/stream
  
    # 禁用缓冲
    ProxyPreserveHost On
    SetEnv proxy-nokeepalive 1
    SetEnv proxy-sendchunked 1
</Location>

完整的前端实现示例

class SecureSSE {
    constructor(url, options = {}) {
        this.url = url;
        this.options = options;
        this.eventSource = null;
        this.token = options.token || this.getToken();
      
        this.connect();
    }
  
    getToken() {
        return localStorage.getItem('authToken') || 
               sessionStorage.getItem('authToken');
    }
  
    connect() {
        // 由于 EventSource 不支持自定义头部,通过 URL 传递 token
        const urlWithAuth = `${this.url}?token=${encodeURIComponent(this.token)}`;
      
        this.eventSource = new EventSource(urlWithAuth);
      
        this.eventSource.onopen = (event) => {
            console.log('SSE 连接建立成功');
            if (this.options.onOpen) {
                this.options.onOpen(event);
            }
        };
      
        this.eventSource.onmessage = (event) => {
            if (this.options.onMessage) {
                this.options.onMessage(event);
            }
        };
      
        this.eventSource.onerror = (event) => {
            console.error('SSE 连接错误:', event);
          
            // 如果是认证错误,可能需要重新获取 token
            if (event.target.readyState === EventSource.CLOSED) {
                this.handleAuthError();
            }
          
            if (this.options.onError) {
                this.options.onError(event);
            }
        };
    }
  
    handleAuthError() {
        // 尝试刷新 token
        this.refreshToken().then(newToken => {
            this.token = newToken;
            setTimeout(() => this.connect(), 1000);
        }).catch(err => {
            console.error('Token 刷新失败:', err);
            // 重定向到登录页面
            window.location.href = '/login';
        });
    }
  
    async refreshToken() {
        const response = await fetch('/api/refresh-token', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.token}`
            }
        });
      
        if (!response.ok) {
            throw new Error('Token refresh failed');
        }
      
        const data = await response.json();
        localStorage.setItem('authToken', data.token);
        return data.token;
    }
  
    close() {
        if (this.eventSource) {
            this.eventSource.close();
        }
    }
}

// 使用示例
const sse = new SecureSSE('/api/stream', {
    token: localStorage.getItem('authToken'),
    onMessage: (event) => {
        const data = JSON.parse(event.data);
        console.log('收到消息:', data);
    },
    onError: (event) => {
        console.error('连接错误:', event);
    }
});

关键要点总结

  1. 客户端限制:EventSource API 不支持自定义请求头
  2. 认证方案:通过 URL 参数或使用 fetch API 替代
  3. CORS 配置:服务端必须正确配置跨域头部
  4. 缓冲控制:服务端和代理都需要禁用缓冲
  5. 安全考虑:实现适当的认证和授权机制

与传统http长连接的区别

使用 HTTP 长连接的判断标准:

  1. 频繁的请求-响应交互
  2. 短时间内多个API调用
  3. 需要优化连接开销
  4. 传统的客户端主动请求模式
// 决策示例:电商购物车
class ShoppingCart {
    // ✅ 适合 HTTP 长连接
    // 用户在短时间内可能进行多次操作:
    // - 添加商品
    // - 删除商品  
    // - 更新数量
    // - 应用优惠券
    // - 计算总价
    
    async addItem(productId, quantity) {
        return fetch('/api/cart/add', {
            method: 'POST',
            headers: {
                'Connection': 'keep-alive', // 保持连接用于后续操作
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ productId, quantity })
        });
    }
    
    async updateQuantity(itemId, quantity) {
        return fetch('/api/cart/update', {
            method: 'PUT',
            headers: {
                'Connection': 'keep-alive',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ itemId, quantity })
        });
    }
}

使用 SSE 的判断标准:

  1. 需要服务器主动推送数据
  2. 实时性要求高
  3. 单向数据流(服务器→客户端)
  4. 长时间保持连接监听
// 决策示例:股票交易系统
class StockTradingDashboard {
    // ✅ 适合 SSE
    // 股价需要实时更新,服务器主动推送
    
    constructor() {
        this.priceStream = new EventSource('/api/stock-prices/stream');
        
        this.priceStream.addEventListener('price-update', (event) => {
            const priceData = JSON.parse(event.data);
            this.updateStockPrice(priceData);
        });
        
        this.priceStream.addEventListener('market-alert', (event) => {
            const alertData = JSON.parse(event.data);
            this.showMarketAlert(alertData);
        });
    }
    
    updateStockPrice(data) {
        // 实时更新股价显示
        const element = document.getElementById(`stock-${data.symbol}`);
        element.textContent = `$${data.price}`;
    }

showMarketAlert(data) {
        // 实时更新股价显示
        const element = document.getElementById(`market-${data.symbol}`);
        element.textContent = `$${data.price}`;
    }
}

从零开始:前端开发者的SEO优化入门与实战

作者 coding随想
2025年6月7日 23:36

从零开始:前端开发者的SEO优化入门与实战

一、SEO是什么?——给网站写一份“高颜值简历”

想象一下,你精心装修了一家米其林餐厅,但食客们却找不到门牌号,甚至地图上连个定位都没有——这大概就是网站不做SEO的下场。

SEO(搜索引擎优化),简单来说就是让搜索引擎(比如百度、谷歌)能轻松找到你的网站,并认为它“足够优秀”,从而在搜索结果中给你一个靠前的位置。而浏览器SEO,则是前端开发者需要关注的部分——通过优化网页的代码、结构和性能,让搜索引擎的“爬虫机器人”(没错,它们就是一群程序小精灵)能高效地读懂你的网站,就像给网站写一份“高颜值简历”。


二、为什么前端开发者要关心SEO?——你的代码就是“第一生产力”

  1. “简历封面”决定生死

    • 如果你的网页加载速度比乌龟爬树还慢,或者代码一团乱麻,爬虫可能还没看完前几行就“划走”了。
    • 数据说话:Google统计显示,网页加载超过3秒,53%的用户会直接离开!(没错,人类的耐心比你想象的更脆弱)
  2. “内容排版”决定分数

    • 搜索引擎不会看你的网页有多“美”,但会检查你的标题、描述、关键词是否清晰。
    • 案例:一个卖烤鸭的网站,标题写着“首页_XX美食”,和标题为“北京烤鸭外卖_1小时送达_老字号秘方”相比,哪个更吸引人?显然后者直接告诉用户“我有什么、能解决什么问题”。
  3. “技术细节”是隐藏加分项

    • 从图片的alt标签到响应式设计,从JavaScript的渲染效率到移动端适配,前端代码的每一处优化都在告诉搜索引擎:“我专业,我靠谱!”

三、前端优化实战:七大招式让你的网站“爬虫友好”

1. TDK优化:给网页起个“好名字”

  • Title(标题)

    • 规则:50-65字符(中文约18-25字),关键词前置,用短横线分隔。
    • 错误示范首页 | 公司官网
    • 正确示范智能门锁_全屋智能家居解决方案_XX科技
    • 幽默比喻:标题就像相亲的“自我介绍”,要短、准、狠,别让爬虫猜你卖的是烤鸭还是烤红薯!
  • Description(描述)

    • 写成吸引用户的“广告语”,包含核心关键词,但别堆砌。
    • 例子XX科技提供专业智能家居解决方案,已服务1000+家庭,免费获取定制方案→
  • Keywords(关键词)

    • Google已经不怎么看这个了,但部分搜索引擎可能参考。
    • 建议:选3-5个核心词,用英文逗号分隔,别加营销词(如“便宜”“优惠”)。

2. 图片优化:让爬虫“看懂”你的图片

  • Alt标签:给每张图片写一句“描述性”文字,比如<img src="duck.jpg" alt="焦糖色脆皮北京烤鸭,配薄饼和葱丝">
  • 文件名:别用IMG_1234.jpg,改用beijing-duck-special.jpg
  • 压缩:用TinyPNG压缩图片,加载速度直接拉满!

3. JavaScript与动态内容:别让爬虫“晕菜”

  • 问题:前端框架(React/Vue)常通过JS动态加载内容,但很多爬虫可能无法执行JS,导致内容“不可见”。
  • 解决方案
    • 服务器端渲染(SSR):比如用Next.js或Nuxt.js,让服务器直接生成HTML,爬虫秒懂!
    • 静态站点生成(SSG):提前生成静态HTML文件,适合内容更新不频繁的页面。

4. 响应式设计:手机端也要“高颜值”

  • 为什么重要?超过60%的流量来自手机,Google会优先推荐移动友好的网站。
  • 怎么做
    • 使用<meta name="viewport" content="width=device-width, initial-scale=1">适配屏幕。
    • 避免弹窗广告遮挡内容,别让用户点“×”的手停不下来!

5. 速度优化:加载快才是真的快

  • 关键指标
    • LCP(最大内容绘画时间):页面主要元素渲染时间要<2.5秒。
    • FID(首次输入延迟):用户点击按钮时,别让页面卡成PPT!
  • 优化技巧
    • 压缩CSS/JS文件,合并代码。
    • 使用CDN加速,把图片存到云存储(比如阿里云、七牛)。
    • 延迟加载图片:用户滚动到再加载,省流量又省时间!

6. URL结构:给网页起个“好地址”

  • 好的URL/product/smart-lock-model-x(清晰、含关键词)。
  • 坏的URL/index.php?id=123&category=5(全是参数,爬虫一脸懵)。

7. 错误页面处理:404别让用户“迷路”

  • 404页面:别只写“404 Not Found”,加个搜索框或“返回首页”按钮,再配上猫咪表情包,用户可能还会逗留两分钟!
  • 301重定向:如果页面搬家了,用301跳转告诉爬虫“新地址在这儿”。

四、进阶技巧:让爬虫“爱上”你的网站

  1. 结构化数据(Schema)
    • 用JSON-LD标注内容类型(如餐厅、产品),让搜索结果展示“富媒体摘要”(比如带评分、价格的卡片)。
  2. XML站点地图
    • 生成sitemap.xml,告诉爬虫“我的网站结构是这样的,请查收”。
  3. 定期检查
    • 用Google Search Console查抓取错误、关键词排名;用Lighthouse测性能分数。

五、总结:SEO不是“玄学”,而是“科学种草”

做好前端SEO,就像给网站穿上一件“隐形战袍”——既让爬虫高效抓取,又让用户爽到飞起。记住:

  • 标题要精准,描述要诱人
  • 代码要清爽,速度要飞快
  • 移动端要友好,错误页要暖心

现在,是时候打开你的代码编辑器,给网站来个“SEO大改造”了!毕竟,流量不等人,你的竞争对手可能已经偷偷优化到第10页去了~


彩蛋:如果你觉得这篇文章有用,不妨分享给你的前端小伙伴,一起“优化”出更好的互联网世界! 😄

超越 console.log():前端调试的 10 个神级技巧

2025年6月7日 23:34

超越 console.log():前端调试的 10 个神级技巧

你以为 console.log() 只能打印字符串?我曾因不会高效调试每天加班 2 小时,直到掌握这些隐藏技能...

一、为什么你的 console.log 是低效的?

新手常见痛点

// 痛点1:打印对象时无法实时查看修改
const user = { name: '张三', points: 80 };
console.log(user); 
user.points = 100;  // 控制台显示 points:100?不!显示80!

// 痛点2:异步代码中日志混乱
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 输出? 3,3,3!
}

// 痛点3:生产环境不敢留调试代码

💡 解决方案: 往下看高阶技巧


二、90% 开发者不知道的 Console 高阶 API

1. 结构化打印:console.table() 数据可视化

const users = [
  { id: 1, name: '李四', role: 'admin' },
  { id: 2, name: '王五', role: 'user' }
];

console.table(users);

适合场景:接口返回数据预览、JSON 结构分析

2. 精准性能分析:console.time()console.timeEnd()

console.time('filterData');
// 模拟大数据处理
const bigData = Array(100000).fill().map((_, i) => i);
const result = bigData.filter(x => x % 2 === 0);
console.timeEnd('filterData'); // 控制台输出:filterData: 15.6ms

🚀 性能优化技巧: 对比两种算法的耗时差异

3. 条件断点替代者:console.assert()

function transferFunds(amount) {
  console.assert(amount > 0, '转账金额必须大于0', amount);
  // 业务逻辑...
}
transferFunds(-100); // 触发断言:Assertion failed: 转账金额必须大于0 -100

优势:无需暂停代码执行即可捕获异常

4. 堆栈追踪:console.trace() 定位调用来源

function service() {
  console.trace('服务调用追踪');
}
function controller() { service(); }
controller();

应用场景:追踪第三方库的调用链路


三、拯救复杂调试场景

场景 1:监控特定 DOM 变化

const targetNode = document.getElementById('user-info');
console.log('DOM初始状态:', targetNode.cloneNode(true));

// 监听 DOM 变化
const observer = new MutationObserver(mutations => {
  mutations.forEach(mut => console.table(mut.addedNodes));
});
observer.observe(targetNode, { childList: true });

场景 2:日志分组归类

console.group('用户模块');
console.log('获取用户信息');
console.debug('用户ID: 12345');
console.groupCollapsed('网络请求详情'); 
console.log('Request URL: /api/user');
console.log('Response Time: 120ms');
console.groupEnd();
console.groupEnd();

场景 3:带样式的调试信息

console.log(
  '%c支付成功!',
  'color: green; font-weight: bold; font-size: 16px;',
  `\n订单号: ${orderId}\n金额: ¥${amount}`
);

四、生产环境调试安全方案

技巧:封装智能 Logger 类

class Debugger {
  constructor(env = 'development') {
    this.env = env;
  }

  log(...args) {
    if (this.env === 'production') return;
    
    // 添加追踪信息
    const stack = new Error().stack.split('\n')[2].trim();
    console.log(`[${new Date().toISOString()}]`, ...args, `\n${stack}`);
  }

  // 关键操作持久化日志
  critical(message) {
    console.error('[CRITICAL]', message);
    // 这里可接入 Sentry 等监控系统
  }
}

// 使用示例
const debug = new Debugger(process.env.NODE_ENV);
debug.log('组件初始化完成');

五、终极调试组合拳

性能分析 + 内存监控

// 记录初始内存
const startMem = window.performance.memory.usedJSHeapSize;

console.time('renderComponent');
renderBigList(); // 执行复杂操作
console.timeEnd('renderComponent');

// 计算内存变化
const endMem = window.performance.memory.usedJSHeapSize;
console.log(`内存占用增加: ${(endMem - startMem) / 1024} KB`);

控制台实时监控变量

// 在控制台输入 monitor(myComponent.state) 即可开启监控
window.monitor = (obj, key) => {
  setInterval(() => console.log(obj[key]), 1000);
}

六、避坑指南:Console 的隐秘陷阱

  1. 日志异步问题(对象打印的延迟展示)

    const obj = { a: 1 };
    console.log(obj); // 展开后可能显示修改后的值
    obj.a = 2;
    

    ✅ 解决方案console.log(JSON.parse(JSON.stringify(obj)))

  2. 生产环境日志泄漏风险
    使用 webpack 插件在构建时移除:

    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
    

结语:成为调试高手的关键思维

graph TD
  A[打印基本信息] --> B[结构化数据]
  B --> C[添加调试上下文]
  C --> D[性能分析]
  D --> E[生产环境安全方案]

最后忠告
“不会用 console 调试的开发者,就像没有听诊器的医生。
这些技巧帮我将日均调试时间从 3 小时降到 40 分钟,
现在轮到你升级武器库了!

附录:控制台全家福

方法 适用场景 示例
console.debug() 开发环境详细日志 debug('API Response:', res)
console.dir() DOM 对象结构化展示 dir(document.body)
console.count() 函数调用次数统计 count('render')
console.clear() 清空控制台 复杂操作前清理环境

📌 行动建议:

  1. 在项目中创建 debugUtils.js 封装这些方法
  2. 下次调试时尝试用 console.table 替代普通 log
  3. 在评论区分享你最爱的 console 技巧!

关注我获取更多前端深度技巧!

❌
❌