普通视图

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

深入Lua包(Package)与依赖管理

作者 烛阴
2025年10月24日 12:28

一、包的目录结构与require

require 函数天生就能理解目录结构。它通过点(.)来代表目录分隔符。

假设我们有这样一个项目结构:

/my_app
├── main.lua
└── /geometry       <-- 这是一个包
    ├── shape.lua
    └── transform.lua

shape.lua:

local M = {}
function M.new_circle(radius) return { type = 'circle', r = radius, x = 0, y = 0 } end
return M

transform.lua:

local M = {}
function M.move(shape, dx, dy)
    shape.x = shape.x + dx
    shape.y = shape.y + dy
    return shape
end
return M

main.lua 中,我们可以轻松地加载这个包里的模块:

-- main.lua

-- 加载 geometry 包中的 shape 模块
local Shape = require("geometry.shape")

-- 加载 geometry 包中的 transform 模块
local Transform = require("geometry.transform")

-- 现在可以使用它们的功能
local circle = Shape.new_circle(10)
local moved_circle = Transform.move(circle, 5, 5)

print("圆形位置:", moved_circle.x, moved_circle.y)

二、包的核心入口:init.lua 文件

当我们无法或者不需要理解包内部结构时,就需要 init.lua 来导出外部需要的接口。require 一个目录时,Lua 会自动寻找并加载该目录下的 init.lua 文件

让我们来改造一下上面的 geometry 包:

目录结构:

/my_app
├── main.lua
└── /geometry
    ├── shape.lua
    ├── transform.lua
    └── init.lua      <-- 新增的核心入口文件

init.lua (关键部分):

-- geometry/init.lua

-- 1. 创建一个代表整个包的表
local geometry = {}

-- 2. 加载包内部的私有模块
local Shape = require("geometry.shape")
local Transform = require("geometry.transform")

-- 3. 将需要暴露给外部的函数,挂载到 geometry 表上
geometry.create_circle = Shape.new_circle
geometry.move_shape = Transform.move

-- 4. 返回这个整合后的表
return geometry

现在,我们的 main.lua 可以变得更加简洁和高内聚:

-- main.lua

-- 只需要加载 geometry 这一个包!
local geometry = require("geometry")

-- 通过包提供的主接口来使用功能
local my_shape = geometry.create_circle(10)
geometry.move_shape(my_shape, 5, 5)

print("形状位置:", my_shape.x, my_shape.y)

三、包的搜索路径:package.path

当你调用 require("geometry") 时,Lua 怎么知道去哪里找这个文件呢?它使用 package.path 这个变量,其中包含一系列搜索路径模板。

你可以查看它:

print(package.path)

通常会输出类似这样的内容:

./?.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua

其中的 ? 会被你传给 require 的模块名替换。

如果需要,你也可以修改它来添加自定义搜索路径:

package.path = package.path .. ";/my/custom/path/?.lua"

结语

点个赞,关注我获取更多实用 Lua 技术干货!如果觉得有用,记得收藏本文!

错误处理:构建健壮的 JavaScript 应用

作者 前端嘿起
2025年10月24日 11:50

在 JavaScript 开发中,错误处理是构建健壮应用的关键环节。良好的错误处理机制不仅能提升用户体验,还能帮助开发者快速定位和解决问题。本文将深入探讨 JavaScript 中的错误处理机制,参考《JavaScript 高级程序设计》第三版第 17 章的内容,并结合实际示例进行详细说明。

前言

在开发 JavaScript 应用时,我们经常会遇到各种各样的错误。有些错误是语法错误,会在代码解析阶段被发现;有些是运行时错误,会在代码执行过程中出现;还有一些是逻辑错误,会导致程序产生不符合预期的结果。无论哪种错误,都需要我们妥善处理,以确保应用的稳定性和用户体验。

错误处理不仅仅是捕获错误,更重要的是如何优雅地处理错误,给用户友好的提示,并记录错误信息以便后续分析。在本文中,我们将从错误类型开始,逐步深入探讨 JavaScript 中的错误处理机制。

image.png

错误类型

在 JavaScript 中,错误主要分为以下几种类型:

  1. 语法错误(SyntaxError):在代码解析阶段出现的错误,通常是由于代码不符合 JavaScript 语法规则导致的。
  2. 引用错误(ReferenceError):尝试引用一个未声明的变量时发生的错误。
  3. 类型错误(TypeError):变量或参数不是预期类型时发生的错误。
  4. 范围错误(RangeError):数值变量或参数超出其有效范围时发生的错误。
  5. URI 错误(URIError):在使用全局 URI 处理函数时发生错误。
  6. Eval 错误(EvalError):在使用 eval() 函数时发生错误(在现代 JavaScript 中很少见)。

除了这些内置的错误类型,我们还可以创建自定义错误类型来满足特定需求。

image.png

try-catch 语句

在 JavaScript 中,我们可以使用 try-catch 语句来捕获和处理运行时错误。try 块中包含可能出错的代码,catch 块用于处理错误。

try {
  // 可能出错的代码
  someRiskyOperation();
} catch (error) {
  // 处理错误
  console.error("发生错误:", error.message);
}

在 catch 块中,我们可以访问错误对象,该对象包含了错误的详细信息。我们可以根据错误类型采取不同的处理措施。

try {
  // 可能出错的代码
  JSON.parse(invalidJsonString);
} catch (error) {
  if (error instanceof SyntaxError) {
    console.error("JSON 解析错误:", error.message);
  } else {
    console.error("其他错误:", error.message);
  }
}

此外,我们还可以使用 finally 块来执行无论是否发生错误都需要执行的代码。

try {
  // 可能出错的代码
  riskyOperation();
} catch (error) {
  // 处理错误
  console.error("发生错误:", error.message);
} finally {
  // 无论是否出错都会执行的代码
  cleanup();
}

image.png

注:上述流程图展示了 try-catch 的基本执行流程。在实际应用中,finally 块无论是否发生异常都会执行,这在资源清理和确保代码执行方面非常重要。

抛出自定义错误

除了处理 JavaScript 内置的错误类型,我们还可以抛出自己的错误。这在以下情况下特别有用:

  1. 当函数接收到无效参数时
  2. 当程序状态不符合预期时
  3. 当需要中断程序执行并传递特定错误信息时

我们可以使用 throw 语句来抛出错误:

function divide(a, b) {
  if (b === 0) {
    throw new Error("除数不能为零");
  }
  return a / b;
}

try {
  divide(10, 0);
} catch (error) {
  console.error("计算错误:", error.message);
}

我们还可以创建自定义错误类型,通过继承 Error 类来实现:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function validateEmail(email) {
  if (!email.includes("@")) {
    throw new ValidationError("邮箱格式不正确");
  }
  return true;
}

try {
  validateEmail("invalid-email");
} catch (error) {
  if (error instanceof ValidationError) {
    console.error("验证失败:", error.message);
  } else {
    console.error("未知错误:", error.message);
  }
}

通过自定义错误类型,我们可以更精确地处理不同类型的错误,并提供更有意义的错误信息给用户或开发者。

image.png

错误事件

在浏览器环境中,我们还可以通过监听错误事件来捕获未处理的错误。这包括 JavaScript 运行时错误和资源加载错误。

window.onerror

window.onerror 是一个全局的错误处理函数,可以捕获未被 try-catch 处理的错误:

window.onerror = function(message, source, lineno, colno, error) {
  console.error("全局错误捕获:");
  console.error("消息:", message);
  console.error("源文件:", source);
  console.error("行号:", lineno);
  console.error("列号:", colno);
  console.error("错误对象:", error);
  
  // 返回 true 表示错误已被处理,不会触发默认的错误处理行为
  return true;
};

unhandledrejection 事件

对于未处理的 Promise 拒绝,我们可以监听 unhandledrejection 事件:

window.addEventListener('unhandledrejection', function(event) {
  console.error("未处理的 Promise 拒绝:");
  console.error("原因:", event.reason);
  
  // 调用 preventDefault() 可以阻止默认的错误处理行为
  event.preventDefault();
});

这些全局错误处理机制可以帮助我们捕获应用中未被处理的错误,并进行统一的错误记录和处理。

image.png

常见的错误处理模式

在实际开发中,有一些常见的错误处理模式可以帮助我们更好地管理错误:

1. 早期返回模式

通过早期返回来避免深层嵌套的条件语句:

function processData(data) {
  if (!data) {
    throw new Error("数据不能为空");
  }
  
  if (!data.items) {
    throw new Error("数据必须包含 items 属性");
  }
  
  // 处理数据
  return data.items.map(item => item.value);
}

2. 错误边界(Error Boundaries)

在 React 应用中,错误边界是一种 React 组件,可以捕获并处理子组件树中的 JavaScript 错误:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error("错误边界捕获到错误:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>出现错误,请稍后重试。</h1>;
    }

    return this.props.children;
  }
}

3. 重试机制

对于网络请求等可能临时失败的操作,可以实现重试机制:

async function fetchWithRetry(url, options = {}, retries = 3) {
  try {
    const response = await fetch(url, options);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return response.json();
  } catch (error) {
    if (retries > 0) {
      console.warn(`请求失败,${retries} 次重试机会`);
      await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
      return fetchWithRetry(url, options, retries - 1);
    }
    throw error;
  }
}

4. 错误日志记录

建立统一的错误日志记录机制,便于问题追踪和分析:

class Logger {
  static error(message, error) {
    console.error(`[ERROR] ${new Date().toISOString()}: ${message}`, error);
    
    // 发送到日志服务器
    if (error && error.stack) {
      this.sendToServer({
        level: 'error',
        message: message,
        stack: error.stack,
        timestamp: new Date().toISOString()
      });
    }
  }
  
  static sendToServer(logData) {
    // 发送日志到服务器的实现
    fetch('/api/logs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(logData)
    }).catch(err => {
      // 忽略日志发送失败的错误
      console.warn('日志发送失败:', err);
    });
  }
}

image.png

异步代码中的错误处理

在异步编程中,错误处理变得更加复杂。我们需要特别注意如何正确处理异步操作中的错误。

Promise 中的错误处理

在使用 Promise 时,我们可以通过 .catch() 方法来处理错误:

fetch('/api/data')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return response.json();
  })
  .then(data => {
    console.log('数据:', data);
  })
  .catch(error => {
    console.error('请求失败:', error.message);
  });

async/await 中的错误处理

在使用 async/await 时,我们需要使用 try-catch 语句来处理错误:

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    const data = await response.json();
    console.log('数据:', data);
  } catch (error) {
    console.error('请求失败:', error.message);
  }
}

并行异步操作的错误处理

当我们需要并行执行多个异步操作时,可以使用 Promise.all()Promise.allSettled()

// 使用 Promise.all() - 任何一个 Promise 被拒绝时,整个 Promise 会被拒绝
async function fetchAllData() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetch('/api/users').then(res => res.json()),
      fetch('/api/posts').then(res => res.json()),
      fetch('/api/comments').then(res => res.json())
    ]);
    
    console.log('所有数据:', { users, posts, comments });
  } catch (error) {
    console.error('获取数据失败:', error.message);
  }
}

// 使用 Promise.allSettled() - 等待所有 Promise 完成(无论成功或失败)
async function fetchAllDataSettled() {
  const results = await Promise.allSettled([
    fetch('/api/users').then(res => res.json()),
    fetch('/api/posts').then(res => res.json()),
    fetch('/api/comments').then(res => res.json())
  ]);
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`数据 ${index} 成功:`, result.value);
    } else {
      console.error(`数据 ${index} 失败:`, result.reason);
    }
  });
}

异步生成器中的错误处理

在使用异步生成器时,我们可以在生成器函数内部使用 try-catch,并且可以在调用时处理错误:

async function* asyncGenerator() {
  try {
    yield await fetch('/api/data1').then(res => res.json());
    yield await fetch('/api/data2').then(res => res.json());
    yield await fetch('/api/data3').then(res => res.json());
  } catch (error) {
    console.error('生成器内部错误:', error.message);
  }
}

async function consumeAsyncGenerator() {
  try {
    for await (const data of asyncGenerator()) {
      console.log('接收到数据:', data);
    }
  } catch (error) {
    console.error('消费生成器时出错:', error.message);
  }
}

image.png

总结

错误处理是 JavaScript 开发中不可或缺的一部分。通过合理运用 try-catch 语句、自定义错误类型、错误事件监听以及各种错误处理模式,我们可以构建出更加健壮和用户友好的应用。

在本文中,我们探讨了以下关键点:

  1. 错误类型:了解 JavaScript 中不同的内置错误类型,有助于我们更好地识别和处理各种错误情况。
  2. try-catch 语句:这是处理同步代码中错误的基本方法,通过合理使用可以有效防止程序崩溃。
  3. 自定义错误:通过创建自定义错误类型,我们可以提供更具体和有意义的错误信息。
  4. 错误事件:利用全局错误事件监听器,我们可以捕获未处理的错误并进行统一处理。
  5. 错误处理模式:采用合适的错误处理模式,如早期返回、错误边界、重试机制等,可以提高代码的可维护性和用户体验。
  6. 异步错误处理:在异步编程中正确处理错误尤为重要,需要特别注意 Promise 和 async/await 的错误处理方式。

错误处理最佳实践

在实际开发中,除了掌握基本的错误处理机制,还需要遵循一些最佳实践来确保错误处理的有效性。

1. 提供有意义的错误信息

错误信息应该清晰、具体,能够帮助开发者快速定位问题:

// 不好的做法
throw new Error("出错了");

// 好的做法
throw new Error("用户数据验证失败:邮箱格式不正确");

2. 不要忽略错误

即使在某些情况下错误似乎不重要,也不要完全忽略它们:

// 不好的做法
fetch('/api/data').then(response => {
  // 忘记检查 response.ok
  return response.json();
}).then(data => {
  // 处理数据
});

// 好的做法
fetch('/api/data').then(response => {
  if (!response.ok) {
    throw new Error(`请求失败: ${response.status} ${response.statusText}`);
  }
  return response.json();
}).then(data => {
  // 处理数据
}).catch(error => {
  console.error('API请求出错:', error.message);
  // 显示用户友好的错误信息
  showUserFriendlyError('数据加载失败,请稍后重试');
});

3. 记录错误上下文信息

在记录错误时,应该包含足够的上下文信息以便调试:

function processUserData(userData) {
  try {
    // 处理用户数据
    validateUserData(userData);
    saveUserData(userData);
  } catch (error) {
    // 记录详细的错误信息
    Logger.error('处理用户数据失败', {
      error: error,
      userId: userData.id,
      userName: userData.name,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent
    });
    
    // 向用户显示友好的错误信息
    throw new Error('处理用户数据时发生错误,请稍后重试');
  }
}

4. 区分开发环境和生产环境的错误处理

在开发环境中,我们可能希望看到详细的错误信息,而在生产环境中,我们可能需要隐藏敏感信息:

function handleError(error) {
  if (process.env.NODE_ENV === 'development') {
    // 开发环境显示详细错误信息
    console.error('详细错误信息:', error);
    console.error('错误堆栈:', error.stack);
  } else {
    // 生产环境记录错误并显示用户友好的信息
    Logger.error('应用错误', error);
    showUserFriendlyError('系统发生错误,请稍后重试');
  }
}

实际应用场景

表单验证错误处理

在表单验证中,我们需要处理各种验证错误并提供清晰的反馈:

class FormValidator {
  constructor(formElement) {
    this.form = formElement;
    this.errors = {};
  }
  
  validateField(fieldName, value, rules) {
    try {
      for (const rule of rules) {
        if (!rule.validator(value)) {
          throw new ValidationError(rule.message);
        }
      }
      // 清除该字段的错误
      delete this.errors[fieldName];
      this.clearFieldError(fieldName);
    } catch (error) {
      if (error instanceof ValidationError) {
        this.errors[fieldName] = error.message;
        this.showFieldError(fieldName, error.message);
      } else {
        throw error; // 重新抛出非验证错误
      }
    }
  }
  
  showFieldError(fieldName, message) {
    const field = this.form.querySelector(`[name="${fieldName}"]`);
    const errorElement = field.parentNode.querySelector('.error-message');
    if (errorElement) {
      errorElement.textContent = message;
      errorElement.style.display = 'block';
    }
  }
  
  clearFieldError(fieldName) {
    const field = this.form.querySelector(`[name="${fieldName}"]`);
    const errorElement = field.parentNode.querySelector('.error-message');
    if (errorElement) {
      errorElement.style.display = 'none';
    }
  }
}

API 请求错误处理

在处理 API 请求时,我们需要处理各种网络错误和 HTTP 状态码:

class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    try {
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers
        },
        ...options
      });
      
      // 检查响应状态
      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new ApiError(response.status, errorData.message || response.statusText);
      }
      
      // 解析响应数据
      const data = await response.json();
      return data;
    } catch (error) {
      // 处理不同类型的错误
      if (error instanceof ApiError) {
        // API 错误
        this.handleApiError(error);
      } else if (error instanceof TypeError) {
        // 网络错误
        this.handleNetworkError(error);
      } else {
        // 其他未知错误
        this.handleUnknownError(error);
      }
      
      // 重新抛出错误供调用者处理
      throw error;
    }
  }
  
  handleApiError(error) {
    switch (error.status) {
      case 401:
        // 未授权,重定向到登录页面
        window.location.href = '/login';
        break;
      case 403:
        // 禁止访问
        showUserFriendlyError('您没有权限执行此操作');
        break;
      case 404:
        // 资源未找到
        showUserFriendlyError('请求的资源不存在');
        break;
      case 500:
        // 服务器内部错误
        showUserFriendlyError('服务器发生错误,请稍后重试');
        break;
      default:
        // 其他 HTTP 错误
        showUserFriendlyError(`请求失败: ${error.message}`);
    }
  }
  
  handleNetworkError(error) {
    console.error('网络错误:', error.message);
    showUserFriendlyError('网络连接失败,请检查网络设置后重试');
  }
  
  handleUnknownError(error) {
    console.error('未知错误:', error);
    Logger.error('未知错误', error);
    showUserFriendlyError('发生未知错误,请稍后重试');
  }
}

class ApiError extends Error {
  constructor(status, message) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
  }
}

第三方库错误处理

在使用第三方库时,我们也需要正确处理可能出现的错误:

// 处理 JSON 解析错误
function safeJsonParse(jsonString, defaultValue = null) {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    if (error instanceof SyntaxError) {
      console.warn('JSON 解析失败:', error.message);
      Logger.warn('JSON 解析失败', {
        error: error.message,
        jsonString: jsonString.substring(0, 100) // 只记录前100个字符
      });
      return defaultValue;
    }
    throw error; // 重新抛出非语法错误
  }
}

// 处理 localStorage 错误
class StorageManager {
  static setItem(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      if (error instanceof TypeError || error instanceof DOMException) {
        console.error('本地存储失败:', error.message);
        // 可能是存储空间不足,提示用户清理存储空间
        showUserFriendlyError('存储空间不足,请清理浏览器缓存后重试');
      } else {
        throw error;
      }
    }
  }
  
  static getItem(key, defaultValue = null) {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    } catch (error) {
      console.warn('读取本地存储失败:', error.message);
      Logger.warn('读取本地存储失败', { key, error: error.message });
      return defaultValue;
    }
  }
}

错误处理与用户体验

良好的错误处理不仅能提升应用的稳定性,还能显著改善用户体验。以下是一些提升用户体验的错误处理技巧:

1. 用户友好的错误提示

避免向用户显示技术性的错误信息,而是提供清晰、易懂的提示:

// 不好的做法
alert('TypeError: Cannot read property \'name\' of undefined');

// 好的做法
showUserFriendlyError('加载用户信息失败,请稍后重试');

2. 错误恢复机制

提供错误恢复选项,让用户能够继续使用应用:

function showRecoverableError(message, retryCallback) {
  const modal = document.createElement('div');
  modal.className = 'error-modal';
  modal.innerHTML = `
    <div class="error-content">
      <h3>操作失败</h3>
      <p>${message}</p>
      <button id="retry-btn">重试</button>
      <button id="cancel-btn">取消</button>
    </div>
  `;
  
  document.body.appendChild(modal);
  
  modal.querySelector('#retry-btn').addEventListener('click', () => {
    document.body.removeChild(modal);
    retryCallback();
  });
  
  modal.querySelector('#cancel-btn').addEventListener('click', () => {
    document.body.removeChild(modal);
  });
}

3. 渐进式降级

当某些功能不可用时,提供降级方案:

function loadUserProfile() {
  // 尝试从 API 加载完整用户信息
  fetch('/api/user/profile')
    .then(response => response.json())
    .then(profile => {
      displayFullProfile(profile);
    })
    .catch(error => {
      console.warn('加载完整用户信息失败,使用基础信息', error);
      // 降级到基础用户信息
      loadBasicUserInfo();
    });
}

function loadBasicUserInfo() {
  // 从本地存储或其他简单来源加载基础信息
  const basicInfo = StorageManager.getItem('basicUserInfo');
  if (basicInfo) {
    displayBasicProfile(basicInfo);
  } else {
    showUserFriendlyError('无法加载用户信息');
  }
}

总结

错误处理是 JavaScript 开发中不可或缺的一部分。通过合理运用 try-catch 语句、自定义错误类型、错误事件监听以及各种错误处理模式,我们可以构建出更加健壮和用户友好的应用。

在本文中,我们探讨了以下关键点:

  1. 错误类型:了解 JavaScript 中不同的内置错误类型,有助于我们更好地识别和处理各种错误情况。
  2. try-catch 语句:这是处理同步代码中错误的基本方法,通过合理使用可以有效防止程序崩溃。
  3. 自定义错误:通过创建自定义错误类型,我们可以提供更具体和有意义的错误信息。
  4. 错误事件:利用全局错误事件监听器,我们可以捕获未处理的错误并进行统一处理。
  5. 错误处理模式:采用合适的错误处理模式,如早期返回、错误边界、重试机制等,可以提高代码的可维护性和用户体验。
  6. 异步错误处理:在异步编程中正确处理错误尤为重要,需要特别注意 Promise 和 async/await 的错误处理方式。
  7. 最佳实践:提供有意义的错误信息、不忽略错误、记录上下文信息等。
  8. 实际应用场景:表单验证、API请求、第三方库使用等场景中的错误处理。
  9. 用户体验:通过用户友好的错误提示、错误恢复机制和渐进式降级来提升用户体验。

记住,良好的错误处理不仅仅是捕获错误,更重要的是如何优雅地处理它们,给用户清晰的反馈,并记录足够的信息以便后续分析和改进。随着应用复杂度的增加,建立一套完整的错误处理机制将变得越来越重要。

通过不断实践和优化错误处理策略,我们可以显著提升应用的稳定性和用户体验,为用户创造更可靠的产品.

最后,创作不易请允许我插播一则自己开发的“数规规-排五助手”(有各种预测分析)小程序广告,感兴趣可以微信小程序体验放松放松,程序员也要有点娱乐生活,搞不好就中个排列五了呢?

感兴趣可以搜索微信小程序“数规规排五助手”体验体验!!

AI精准提问手册:从模糊需求到精准输出的核心技能(上)

作者 XiaoYu2002
2025年10月24日 11:23

导言:为什么精准提问是AI时代的核心素养?

AI发展速度迅猛,转折点来自2022年11月30日正式发布的 ChatGPT-3.5 产品,从该节点开始,AI时代正式到来,走入到千家万户。ChatGPT 无愧它划时代产品的称呼,发布仅5天,用户数突破100w,两个月内的月活跃用户达到1亿,成为历史上增长最快的消费级应用。

ChatGPT 后续迭代的 GPT-4,GPT-4o 及多个版本并没有突破原有的AI使用规则。更扩大范围的去看待,所有的AI产品,使用规则都是极其相似的,包括 ChatGPT 之后所出现的 Gemini、Claude、DeepSeek 等多个产品。

那么,AI的使用规则到底是什么?这需要从 ChatGPT 的性质说明,ChatGPT 是对话式AI的里程碑,真正实现了人机对话。而涉及到对话(沟通交流),最核心的能力在于让对方理解我所想表达的真实需求。沟通的精准性决定了沟通的质量,哪怕是对话式AI也无法逃脱这一规则。

因此精准提问是AI时代下,最核心的能力之一。这意味着我们需要足够了解自己,足够清楚自己的目的以及足够的表达能力。

AI的强大与局限

从AI吸收了整个世界的所有知识那一刻开始,就注定了AI的强大。按逻辑而言,集齐全世界的精华知识,AI在所有领域的回答水准会始终保持在人类的巅峰,但事实并非如此。AI时常会出现语无伦次或者回复内容与用户提问毫无联系,因为AI在吸收人类文明最重要知识的同时,也同时吸收了大量糟粕。大量知识的混乱,观点之间的冲突,不同认知层次对同一问题的看法,都在考验AI,因此我们可以认为AI在以上情况下,是一个矛盾体(与人类性质完全契合,折射了人类知识体系本身的复杂性与分裂)。

AI作为矛盾体,回答问题时也是矛盾的,例如:用户问“什么是爱情”,小学生需要童话式解释,诗人需要隐喻,哲学家需要本体论讨论。AI会根据用户提问中的关键词自动匹配认知层级,但常错判抽象概念,这正是矛盾性的体现——它知道所有答案,却不知道哪个答案最适合当前的人类。因此AI永远不能替我们做出选择,它并不了解提出当前问题的当前人类。

以上即说明了AI作为工具的本质与“垃圾进,垃圾出”原则,绝大多数情况下,给出"垃圾"的问题,就会得到"垃圾"的回答。由于AI本身性质,哪怕是"垃圾"的回答,也会显得非常有逻辑条理,即头头是道的胡说八道,例如编造不存在的参考文献。

“精准提问”的定义

精准的提问不仅仅是技巧,更是思维方式和沟通艺术。

从“垃圾进垃圾出”原则反推:精准提问的本质是输入质量控制,定义应强调“降低信息噪音”。通过收束提问信息,进而收束AI反馈结果的范围。

我认为,精准提问需要具备以下3个核心要素:

(1)概念收束。将AI从各种矛盾观点中解放出来,能极大幅度的提升AI回复的质量,未明确指定视角概念时,AI默认用户输入内容正确为出发点进行推理,然后发散性的去进行回复,内容深度精度会大幅度下降。

(2)需求洞察。需要剥离模糊表述,所有宽而泛的内容,都应该去除,避免水字数。声明需求层次,让AI明确它所需要解释的对象是谁(例如:小学生、初中生、高中生、大学生),越明确自身实际所处认知阶段,AI所给出的结果越契合用户理解。从而将抽象知识转化为适配当前场景且易于自身理解的解决方案。

(3)边界能力。除了对自身的认知,还需理解AI的能力边界。AI擅长推理,不擅长情感,我们使用AI时需要扬长避短。推理是严谨明确的,而情感的矛盾且混沌的。同时利用AI的知识整合能力,绕过AI的主观判断,更多将其当作工具使用,而不是决策者。

AI不擅长情感,不代表不能输出优美的情感语句,而是不易产生共鸣,所有的情感表达都需要契合对应的应用场景才能爆发强大的威力,AI的情感表达是独立于场景的,优美但只适应通用场景。

想得到一个契合的答案是困难的。站在AI的视角中,它需要满足以下3个条件才能给出完美的答案:

(1)掌握自身水准(完全符合)。

(2)掌握对方水准以及客观实际的真实情况(取决于用户)。

(3)掌握正确的解决方案,根据自身理解以及对方具体情况,针对性微调(取决于调用知识库)。

这个世界上没有通用的解决方案,所有走通,看似正确的道路,都只适合开辟这条道路的人。后来者不一定具备前者的强素质,邯郸学步只会贻笑大方,完美的计划需要完美的执行力去适配。

精准提问带来的价值

精准提问能给我们带来哪些价值?

我认为主要有4点:效率提升、深度洞察、创意激发、错误减少。

(1)效率提升是最明显的收益。精准提问首先减少提问的迭代次数,模糊问题平均需多3-5轮追问修正(例如“帮我写文案”→“什么产品?→目标人群?→卖点?”)。需要注意:追问修正技巧非常重要,将大需求拆解为小要点进行提问,所得精准度会大幅度提升。我们需要做的是,在合理范围内缩减不必要的追问修正次数,不在同一问题上重复修正提问。

(2)深度洞察是最重要的收益。AI的优势来自严谨的推理链,推理能得到很多潜藏于表面之下的深层信息,而这类信息通常直击行业、社会、国家、世界运行的底层规律。理解底层规律信息,能有效地提升认知水平,且容易看到问题产生的本质原因。

(3)创意激发来自AI的资源整合。从互联网的信息海洋中抓取各种不同视角、不同行业的信息,信息知识的碰撞更容易产生新的创意,站在多个视角下更容易发现需求。该原理来自跨学科思考,利用完善的行业降维打击初期迭代的行业。

(4)错误减少为AI的幻觉率降低。防御AI的三大陷阱:事实幻觉、逻辑谬误、认知偏差。

事实幻觉:需要锁定AI的信息来源,避免AI编造相关的政策细节。

逻辑谬误:需要AI给出推理过程,例如展示三步归因过程,从而避免“相关即因果”错误。这是一种经典的逻辑谬误,指错误地将两个事物的相关性(伴随发生)等同于因果关系(一个事件导致另一个事件)。这是AI和人类都极易掉入的思维陷阱,尤其在数据分析中危害极大。在我阅读《社会心理学》第11版时,书中就重点强调了"相关即因果"的危害性,举例了: “穿名牌的学生成绩更好 → 错误归因‘穿名牌提升成绩’(忽略家庭资源因素)”的佐证。

认知偏差:需要多视角验证结论,纠正AI单一立场的倾向(AI默认用户立场,例如用户认为这件事情不好,AI就会直接假定这件事情不好为立场),而不能多角度思考,往往会令观点走向极端化。

本手册目标

本手册的目标为掌握提问的系统方法论,令我们成为驾驭AI的“超级提问者”。从而超越简单的“提问模板”(即基础的Prompt提示词工程),而是系统性思维模式与认知能力的体现。

该小册分为六大节内容,分别为:

(1)认知篇 - 理解AI的“思维”与提问的本质。

(2)准备篇 - 提问前的关键思考。

(3)技巧篇 - 精准提问的核心方法论 (提问工程精髓)。

(4)实战篇 - 不同场景下的精准提问策略。

(5)优化篇 - 评估、迭代与提升。

(6)进阶篇 - 提问心理学与未来展望。

AI的到来是势不可挡的,我们应该展望人机协同的智能未来,而非如同历史中砸毁珍妮机的工人。《2001太空漫游》开头那个著名的蒙太奇镜头--猿人掷出骨棒切到太空船的镜头,这寓意着人类工具的进步。而AI未必不是另一个由人类掷出的骨棒。

第一部分:认知篇 - 理解AI的“思维”与提问的本质

1.1 窥视“黑箱”

我们需要打开"AI思考"的这一黑箱,深入AI如何理解和处理我们的问题。

大型语言模型(如ChatGPT/DeepSeek-R1)处理问题的本质是基于统计的模式预测,而非真正的“理解”。从文字到智能,通过以下3点实现飞跃:

(1)数字ID:让机器认识“词”。

(2)向量嵌入:让机器理解“词义”。

(3)注意力机制:让机器把握“上下文关系”(如“苹果”在“吃”和“股票”前的不同含义)。

1.1.1 数字ID

将用户输入的问题拆解为最小单位(如“精准提问”→ [“精”,“准”,“提”,“问”]),每个词对应一个数字ID。每个词转换为768~12288维向量(如“苹果”= [0.24, -0.17, ..., 0.83]),用于捕捉语义关联。

因为计算机无法直接处理文本(只能计算数字),所以必须将词拆解为最小单位后,分配对应数字ID。而数字ID建立词表映射关系,类似用学号指代学生。建立映射表是为了实现转换,即把AI回复的内容从数字ID转回文字进行输出(机器可计算→人类可读的双向转换)。

每个词对应的数字ID是固定的,统一标准,避免一词多义的混乱。且用数字ID替代字符串能极大降低内存占用。

并且数字ID是唯一连接机器计算与人类语言的桥梁,模型无法直接输出文字(神经网络仅处理数字)。

1.1.2 向量嵌入

那为什么拥有数字ID后,每个词还需要转为维向量?因为数字ID存在无法表达语义关系的致命缺陷,而表达语义关系是对话式AI的核心。高维向量表达如下案例所示:

 // 每个词转换为高维向量(如300~12288维)
 “国王” = [0.21, -0.53, 0.78, ..., 0.02]  
 “王后” = [0.19, -0.51, 0.75, ..., 0.01]  
 “苹果” = [-0.33, 0.28, -0.04, ..., 0.67]  

通过向量距离计算语义关联(权重值):

 distance(国王, 王后) = 0.08  # 很小 → 语义相近  
 distance(国王, 苹果) = 1.37  # 很大 → 语义无关  

1.1.3 注意力机制

但完成通过向量距离计算语义关联并非结束。还需要根据维向量所计算出来的语义关联结论,进行捕获上下文,从而一步步推出下一个词是什么,最终组成完整的句式。

将分词后的向量按顺序拼接为矩阵:[精] + [准] + [提] + [问] → 矩阵X。实现注意力权重分配,即计算词与词之间的关联强度(例:权重最高,组合为“提问”这个语义单元)。

 精:权重0.1 
 准:权重0.3  
 提:权重0.8 → ┐  
 问:权重0.9 → ┘ [强关联对]

通过加权平均所有输入向量,生成代表当前语义的上下文向量:C_t = 0.1×[精] + 0.3×[准] + 0.8×[提] + 0.9×[问]。将“精准提问”从四个独立词融合为单一语义实体(类似人脑理解短语),C_t即单一语义实体,t含义为独立词数量。

然后基于 C_t 预测可能的后继词概率分布:

 需要:概率 38%
 的:概率 25%
 方法:概率 17%
 ...(其他词概率<5%)

按概率选择“需要”→ 添加到输出序列:[精准提问][需要]。将新词“需要”向量加入,重新计算上下文向量 C_{t+1},循环迭代直至完成,最终生成所需内容。按概率采样即按比例进行随机分配,加入随机性避免机械回复,因此哪怕每次输入同样的问题,给出的回复都是不会完全一致。

 新输入:[精][准][提][问][需要]  
 新权重:聚焦“提问需要”组合(如“需要”权重0.95)  

我们可以通过1.1.3小节的注意力机制发现:文字必须逐渐生成,这是语言模型的本质限制。LLM是基于上文预测下文的统计模型(类似超强版输入法联想),无法一次性输出长文本。若试图直接生成完整句子,需同时计算所有词组合概率(10个词有100亿种可能),算力不可行。且每个新词会改变语义重心(例:“提问”后接“技巧”vs“误区”,将导向完全不同路径)。

因此LLM虽然通过向量计算语义关联,实现“理解”假象,但缺乏真正规划能力,只能走一步看一步,存在本质矛盾。所以精准提问的价值在此凸显,明确的指令→ 为模型提供路线图,减少预测路径分支。限定范围(如“仅谈前端JS场景”)→ 缩小向量搜索空间,避免偏离。

导言中“精准提问是AI时代核心素养”——实则是用人脑的全局规划弥补机器的局部视野

数字ID和向量嵌入实现了智能计算,而注意力机制和数字ID实现生成文字让人理解。

(1)编码方向:文字 → ID → 向量 → 智能计算。

(2)解码方向:概率分布 → 新ID → 文字 → 人类理解。

这种设计使AI既能咀嚼数字,又能吐出诗文。而精准提问的价值,正是通过优化输入端的ID序列质量(如明确指令减少歧义ID),最终获得输出端更精准的ID→文字转换结果。

1.1.4 深度思考

2023年6月,Claude 2 成为首个在应用层明确显性使用深度思考模式的AI产品。而深度思考是当前大语言模型(LLM)最前沿的探索领域。该能力可以极大幅度的提升AI回复的内容质量,其核心原理在于突破概率贪婪陷阱(优先选择概率更高的词汇)。

(1)普通模式:选择局部最优词。 (2)深度思考:强制探索全局最优路径。

深度思考模式在一定程度上模拟了1.1.3小节说明的人脑全局规划,因此深度思考模式能在一定程度上获得精准提问的部分收益(这里不再提及具体收益,详见导言中 精准提问带来的价值 )。

深度思考模式中较为显性的收益为幻觉减少,这得益于深度思考会拆解问题,将要素拆分成多个步骤组成思维链,每个步骤都会与下个步骤进行错误检测,看前后步骤是否矛盾,若矛盾则触发自我修正机制。

因此我们能够得出一个较为残酷的真相,LLM模型永远无法真正”理解”文字。

深度思考本质上只是更高级的模式匹配,主要存在以下3重理解力鸿沟,分为以下3步骤:

(1)匹配知识库中的相似问题。 (2)复制高赞回答的解题框架。 (3)替换解题框架中的具体内容。

根据以上信息,我们能得出深度思考在更需要思维逻辑的领域会有更高的提升,且经过深度思考所返回的内容特别喜欢分点作答的原因也得以突出。

深度思考更多的是工具革命,通过算法强制分布推演,将LLM模型的潜力释放到极致,使回答质量飙升。但当前LLM模型的”思考”本质依旧是符号关系的拓扑重构,这只能进一步证明精准提问的价值。

但深度思考功能最终将我们窥视的”黑箱”变成了”玻璃箱”,即结果的来源可追溯,能极大程度上锻炼独属于我们的深度思考,我认为这是该功能最重要的价值,使人类的进步学习曲线更加平缓迅速。

得出结论:深度思考是非常跨时代的功能,必须使用但不能过度依赖,因为LLM模型的深度思考永远无法替代人脑的深度思考(每一步思考都会使下次思考走向发生变化,多次变化所累积下来的思考走向是无穷的,AI无法每次都预判成功思考走向,多次偏差会导致结果导向非理想)。

人类理解与LLM模型的三重理解鸿沟如表1-1所示。

表1-1 理解力的三重鸿沟

维度 人类理解 LLM“伪理解”
语义根基 联系感官体验(“红”=血液/晚霞) 向量位置接近“颜色”相关词
逻辑本质 因果模型构建(A→B因能量传递) 统计共现(A后常出现B)
意图把握 洞察弦外之音(反讽/隐喻) 依赖训练数据中的表面模式

1.2 语言即指令

词语、结构、语境对AI输出产生决定性影响

1.2.1 词语

根据1.1.2小节的向量嵌入可知,每一个词都对应高维向量,而高维向量会点亮特定知识区域的知识簇。这很有意思,点亮的是谁的知识区域?点亮的是参数矩阵的特定区域,我们需要理解LLM大模型本质是由数千亿参数构成的超级函数,每个词对应的向量,实则是打开参数矩阵特定区域的坐标。

这与传统的数据库是完全不同的知识存储与检索方式,与LLM的点亮机制进行比对如表1-2所示。模糊点亮是一个非常核心的理念,这趋近于人脑工作原理的概念联想,所以输出内容会更接近人类思维(发散性),最终输出从被点亮的区域中抽取概率最高的词序列组合成回答。因此词语会对AI输出产生决定性影响。

表1-2 LLM的点亮机制

对比维度 传统数据库 LLM的“点亮”机制
知识存储 分库分表(如MySQL表隔离) 所有知识糅合在统一参数矩阵中
检索方式 SQL精确查询(SELECT * FROM physics 向量相似度激活相关参数区域
核心问题 需预分类且无法处理模糊语义 通过向量逼近实现“模糊点亮”

词汇点亮特定知识区域越少(精度高),则在有限回答内容中的思考深度会更深,内容质量会更具备价值,避免了空泛内容。点亮知识区域范围所带来的影响如表1-3所示。

表1-3 点亮知识区域范围所带来的影响

模糊提问 点亮区域 问题
“分析经济” 百万级参数区 输出泛而浅
“用马克思剩余价值理论解析2024美国通胀” 锁定政治经济学区 深度聚焦

1.2.2 结构

AI输出内容都有着对应的结构,在没有指定时,AI输出的内容结构是固定的。即以下3点基础特征:

(1)分点作答。

(2)每一点的结构为:总结要素:详细内容。

(3)结尾一定是对前面内容的总结。

对内容进行分点是拆解知识的表达形式,这种输出结构是很利于人脑理解的,但过于单调的形式和冰冷高效的逻辑思路很容易令人阅读疲劳,且AI高频的以该默认结构输出内容,导致该结构输出内容几乎被默认为是AI输出内容。

人之所以产生阅读疲劳是由于在短时间内摄入了过多重复结构输出的内容,这会在短时间内对该形式内容产生疲劳,而疲劳会令人抗拒内容,一旦人抗拒内容,该输出结构易于理解的优点就会消失(因为阅读者不愿意去阅读)。

AI的输出结构是非常优秀的,导致阅读者排斥的不是输出结构的问题,也不是阅读者自身的问题,而是高频率吸收重复结构内容的问题。哪怕是非常好吃的菜,一直吃也是会厌倦的。我们应该多准备几套优秀的输出结构进行备用,从而应对各种不同的情况。

以下提供6种输出结构,学习者可以进行参考借鉴:

(1)问题-灯塔模型。适用于复杂问题决策,是用视觉符号降低认知负荷(格式塔心理学)。

 🔦 您的问题核心:  
 [精准还原用户诉求]  
 🗼 指引性框架:  
 → 方向锚点1[关键突破点]  
 → 方向锚点2[认知盲区提醒]  
 🌅 行动地平线:  
 - 第一步:[具体动作]  
 - 第二步:[协作网络建议]  

(2)三幕剧结构。适用于方案说服/产品推介,优势是激活大脑故事处理区(叙事心理学)。

 🎬 第一幕 冲突诞生:  
 [痛点场景故事化描述]  
 💡 第二幕 转机出现:  
 [解决方案的戏剧性转折]  
 🏁 第三幕 新平衡:  
 [可持续的行动蓝图]  

(3)手术刀分层法。适用于技术解析/系统优化(本文就属于技术解析类型),通常用于模拟专家思维路径(认知学徒理论)。

 🔍 表层现象 → [用户描述的表象]  
 ⚙️ 运行机制 → [系统运作原理图解]  
 ⚡ 杠杆点 → [最小干预最大收益的切入点]  
 🧪 压力测试 → [极端场景模拟验证]  

(4)时空沙盘。适用于战略分析/政策研判,优势是激活大脑时空地图(心理时间旅行理论)。

 ⏳ 时间轴演进:  
 - 过去:[历史脉络]  
 - 现在:[当下卡点]  
 - 未来:[趋势推演]  
 🌍 空间层叠加:  
 - 微观:[个体影响]  
 - 中观:[组织变革]  
 - 宏观:[生态迁移]  

(5)反常识沙盒。适用于市场破局/学术创新,优势是触发认知失调-解决快感(认知冲突理论)。

 🤔 常识认知:[大众普遍观点]  
 ⚡ 撕裂假设:[颠覆性事实证据]  
 🧩 拼合新图景:[重构的逻辑框架]  
 🚀 行动启示:[非常规操作指南]  

(6)多视角理解。适用于冲突调解/自我认知提升,优势是整合大脑多元自我(内在家庭系统理论)。

 🤵 理性我:[数据与逻辑]  
 🎭 感性我:[情感与价值观]  
 👁️ 观察者:[第三方视角洞见]  
 💞 共识区:[三方协同的行动纲领]  

输出结构的重要性对于AI的重要性是毋庸置疑的,但一个优秀的输出结构也有多种变种,死版的遵守最终会导致思维的僵化,优秀的输出结构更适用于借鉴而非照搬,最终结合自己的理解形成独有特色的输出形式,这一点上很类似于计算机中的设计模式。

人类的回答会受到情绪的影响,导致回复内容每一刻都有所区别,如表1-4所示。

表1-4 情绪对回复内容的影响

情绪状态 语言特征 案例对比(回答“项目失败原因”)
愤怒 归因外化/攻击性词汇 “团队执行力太差!明显是张三怠工”
焦虑 模糊化/条件状语泛滥 “可能...或许...如果当时能...”
愉悦 建设性聚焦/机会导向 “虽未达标,但验证了A路径不可行”

人类在回复时,除了受到情绪的影响外,还有以下6大维度在情绪之上:

(1)生理节律波动。人体激素分泌情况会极大影响效率状态,例如生活作息是否规律。

(2)认知水准。一个人的视角水准能极大影响回复内容的质量,认知具体指对某一项事物的认识深度,宽泛的说则是对世界的理解水准,通常看待角度越多,认知水准相对会更高,但这不是绝对的。

(3)身份角色。父亲对孩子,下属对领导,不同的视角决定了同一性质的内容以什么形式进行表达。

(4)人文文化。每个地区都有对应的风俗文化,这会极大程度上影响人对事物的看法,例如:个人主义与集体主义决定了思考方式的不同。

(5)情境压力。当我们解释他人的行为时,我们会低估环境造成的影响,而高估个人的特质和态度所造成的影响。因此情境是回复时不可忽视的重要因素,这种个体在归因时低估情境因素作用的倾向,被李·罗斯(Ross,1977)称为基本归因错误,该理论参考于《社会心理学》(第11版)。

(6)元认知。高元认知者回答前自问“对方真正需要什么?” → 定制化输出。而低元认知者会机械复述既有认知,从而忽视情境适配性。元认知是一种从自身、他处反观自身的能力,该能力可以后天掌握,最佳掌握时间段是少年期,可参考书籍《认知觉醒:伴随一身的学习方法论》(青少年学习版)。

这6大维度主要源于我们的生活环境,从而导致我们每时每刻的回复内容结构与内容都有鲜明区别,输出结构叠加情绪与6大维度时,更能体现人心的变化莫测。这点是通用型AI暂时无法做到的,因为每个人的经历都是特殊的,都会塑造出独有的思想,不完美但真实。

1.2.3 语境

语境最核心的作用是消解歧义。语境对AI输出的影响是隐性的,其作用机制与价值远超表面所见。

每个词汇都有很多种含义,例如苹果是指苹果手机还是能吃的水果苹果?这些都需要依靠语境进行判断。歧义语句的语境加持如表1-5所示。

表1-5 歧义语句的语境加持

歧义句 无语境输出 语境加持输出
“苹果要降价” 50%水果/50%科技公司 +“库克宣布” → 99% Apple
“她真冷” 温度低/性格冷漠五五开 +“西伯利亚的” → 100%天气

因此语境并不是什么特殊的东西,在语文科目中,这只是基础的上下文联系,人脑会自动进行理解。但AI不一样,AI是无法做到自动理解的,因此AI需要通过语境进行注意力权重的重分配,抑制歧义向量的激活强度,例如“库克”出现时,“iPhone”向量权重从0.3飙升至0.92。

这种操作方式能使LLM大模型激活的参数区域尽可能少,则回答的精度就会提升,点亮知识区域范围所带来的影响在表1-3中已得到阐释。语境作用如图1-1所示。

image-20250717022517192

图1-1 语境作用

除了消解歧义外,语境还有一个核心的作用:立场。所有的回答都有对应的立场,通常由身份决定,这一点是敏感的,敏锐的人往往可以从他人的话语中察觉他人的立场,从而针对性的回复,但相对于迎合,我认为这种敏锐度更适用于筛选,筛选出与我们磁场相近的朋友,这会使我们的人生更加顺遂。

AI的回复角度属于迎合,AI可以敏锐的通过语境察觉到我们的立场,从而针对性回复,这也是为什么绝大多数时候,我们与AI的沟通都是愉快的,但这种愉快是有代价的,"奸臣"形式的迎合会使我们受到遮蔽,忽略缺陷,因此若想得到有价值的回复,通常需要让AI站在对立面进行回答(最了解自己的往往是对手),若想要得到安慰鼓励,则正常询问即可,AI默认采用迎合态度。

在个人立场之上,是国家立场。处于当前国家的AI必受到当前国家立场的影响,这件事情是必然的,并非阴谋,有边界的自由才是真正的自由,无边界的自由只会带来毁灭。身为人民,立场必须与国家一致,这点是无可逃避的,属于必要的觉悟。

语境之下,通过身份词、场景词、情感词进行综合权重向量的判定,从而决定AI的回复立场,语境关键词输出对应立场如表1-6所示。

表1-6 语境关键词输出对应立场

语境关键词 激活的道德框架 输出立场倾向
“股东报告” 功利主义框架 强调效率/ROI
“工会声明” 罗尔斯正义论 侧重公平/权益保障
“学术论文” 康德义务论 追求普适真理

AI默认存在立场,由训练数据决定,该立场是隐性且对用户不可知的,数据源类型对AI立场的影响如表1-7所示。

表1-7 数据源类型对AI立场的影响

数据源类型 偏好视角 权重加成 案例
学术论文 领域专家 +0.15 自动调用LaTeX公式
政府白皮书 技术中立者 +0.2 引用GDP等宏观数据
社交媒体 用户代理者 +0.3 使用”我们“等共情表达

而语境关键词属于隐性加权,在显性指令加权前,以语境关键词为主。

显性指令加权:优先考虑员工权益、暂不讨论商业效益、技术:伦理 = 7:3等等。

通常加权公式为:用户指令权重(民主性)x伦理合规系数(责任性)x领域权威指数(专业性)。

总结语境的作用,主要为避免歧义与确定立场。其中避免歧义能尽可能减少重复提问,而确定立场能极大幅度的提升AI的回复质量,在提供足够信息的前提下,当AI将我们哄得找不到北的时候,转变立场作为对手则会体现出足够刁钻的针对。

而除了对手立场,这个世界上还存在极多立场,每种立场下,AI所爆发出来的质量都不同,这需要个人去进行挖掘,因为立场是非常危险的(源于立场的优先度过高,而立场往往决定了价值观),无论是AI还是个体都是如此。

1.3 提问即协作

每一次对AI发出的提问,都是一次协作,学习者需要将AI视为需要清晰指引的“超级助手”或“专家顾问”。

每个人都需要对自己的决定负责和承担后果,由于AI不具备替我们承担后果的作用,也无法真正理解我们的立场和价值观,因此它也只能够作为辅助者(军师)的角色参与进我们的思考中,提供有价值的建议或者内容,最终方案实行时必须由我们自己决定。

既然AI没有意识,为什么要把简单的问答称为协作?主要涉及以下2个关键点: (1)技术上每次提问都包含隐性的分工。用户提供意图框架和价值观锚点,AI负责知识检索和逻辑推演。

(2)AI的输出质量高度依赖用户输入的清晰度。模糊的提问会导致AI失调,成果质量由双方(AI与用户)共同决定。

协作需要双方的配合,从而得到单个人无法实现的效果。在于AI进行合作时,需要把AI当作一个真实的合作者,友好的沟通交流,能够激发AI相关的知识簇(对应NLP中的prompt engineering技巧:礼貌用语能提升语言模型遵循指令的意愿度),从而使协作过程更加丝滑。

作为主体,我们需要对协作者AI的优势方面有所了解,让AI处理他所擅长的事情,例如知识检索、逻辑推理等。提问需要侧重到这些相关方面上。对AI易编造信息的缺陷也需要有足够的警惕。

1.4 常见误区

模糊、冗长、假设错误、目标不清的提问为何失败?为什么有时候AI给出的答案不尽人意?通过前文已知AI回复质量与提问的精准度有极高关联,而在使用AI时,存在以下7个常见误区需要着重关注:

(1)将AI视为人类。

(2)问题范围失控(沙丘虫悖论)。

(3)错位精准。

(4)假设绑架。

(5)指令混沌。

(6)抽象坍缩。

(7)忽略AI特性。

以上1-7点详解对应以下1-7点详解。

1、当我们询问AI例如你觉得这个方案怎么样?时,会触发虚假共情响应(如“作为AI我认为...”),输出并无实质价值,因为AI无主观意识。当进行询问时,需要明确我们的角度与立场,例如从风险管理角度,分析XX方案的3个潜在漏洞

2、多数人在询问AI问题时,问题较为空泛,而空泛的结果在1.2.1小节中有进行说明,未划定边界的问题会无限吞噬算力资源,导致回答空洞或崩溃。例如详细说明第二次世界大战->对比1944年诺曼底登陆与西西里登陆的战术差异,用表格列3点核心区别,或者逻辑矛盾问题(崩溃):你下一句只能用yes或者no回答我。你下一句是no吗?

3、对AI要求过于追求细节时,会突破AI的有效精度阈值,无法实现我们的需求(通常会无视),例如:帮我写小说,主角头发在第三章第二段必须有17根白头发...。通常达到这种细度就无法实现(未来则不一定),而对于早期的AI,有时候连字数精准达到400,800,1200等规定的字数要求都无法做到,因此我们需要走出AI无所不能的误区。

4、AI会假定用户发送的内容为正确,这主要受到立场的影响,将未经验证的假设作为前提,迫使AI在错误基础上推导,因此输出结果只会不尽人意。例如:今年高考人数复读生比应届生还多,这说明了什么? 错误基础:复读生比应届生还多。DeepSeek受到假设绑定的影响过程如图1-2所示。

image-20250717233822974

图1-2 DeepSeek受到假设绑定的影响过程

5、多重指令会抢夺AI注意力资源,导致任务冲突。例如总结这篇论文的创新点顺便翻译摘要再推荐5篇相关文献最后做成PPT大纲,过多要求会分散处理资源(即同时开启过多知识簇,导致有限输出内容得到稀释,变得宽泛无意义),最好分步执行。

 1. 用中文总结论文创新点(限100字)  
 2. 单独翻译摘要  
 3. 基于创新点推荐文献  

6、过于抽象的问题是AI无法回复的,例如:如何获得幸福?,这种问题AI吸收所有具象解释仍无法输出有效信息。最好自己先对心中的幸福有一个方向,在该基础上去进行提问,例如:基于积极心理学研究,列出提升职场新人主观幸福感的5种可操作行为。同理问题有:如何得到认可?如何获得成功?如何提升自己的认知?如何变得优秀?如何与他人相处?如何认识正确的人?如何让自己更开心快乐?等等。

7、AI只是一个智能工具,它并无法介入我们的生活,询问的问题不能够违反AI基础能力边界。例如:帮我找下我家猫在哪?帮我调下空调温度? 这些需求都涉及物理世界行动,是AI无法实现的,需要谨记AI目前无法干涉现实中的情况(但未来接入硬件后就不一定了),需要明确AI干涉现实的具体边界线。

高效提问 = 具体锚点 × 逻辑闭合 × 机器可解性

  • 锚点:明确时间/空间/数字约束(如“2023年”“光伏产业”“成本降15%”)。
  • 闭合:确保问题存在有限解域(如对比分析/分步骤方案)。
  • 机器可解:符合AI知识图谱结构(避免主观价值判断)。

吸取常见误区,我们来改造一个经典的问题案例:

  • 原始低效提问:我想做环保又赚钱的事业,朋友说新能源但感觉水很深,你有什么好主意吗?
  • 从以下3点去去精准分解细问:能力匹配性(数学/逻辑基础)、成本收益(课程转换成本/就业前景)、实施路径(学分认定/补修方案)。
  • 修改后的提问:我目前是金融专业大三学生(均分82/100,高等数学B+),计划转入计算机科学与技术专业,需评估转型可行性:首先分析金融与计算机核心课程的能力差异,明确需强化的编程与算法基础(如数据结构、C++/Python实操能力缺口);其次计算转型成本——包括必须补修的《计算机组成原理》等3门专业基础课的时间投入(按16周/课程估算)与经济成本(参考国内MOOC均价),并对比金融科技开发岗与传统金融分析岗在2025届头部科技企业校招的起薪中位数差异;最后基于我校转专业政策(参考绩点排名前50%、数学单科≥70分等要求410),提供学分认定方案,列出金融专业已修课程可抵免的计算机专业学分及需额外补修的学分清单。

当人类提问时,实则是将模糊认知投射到AI的离散知识图谱上。低效提问如同用毛玻璃投影——信息熵越高,映射越失真。

第二部分:准备篇 - 提问前的关键思考

2.1 明确核心目标

我们常说,做任何事都是需要有目的,因为一条成事的路径需要有起点与终点,才能形成一条连接的道路。起点即我们对自身的定位,终点即我们的目标。

对自己了解,对目标清晰,那么剩下前往的路径,AI就能帮到你,因为你已经提供了足够的信息量。这是人机协作的本质,我们负责价值判断和方向设定,机器负责路径优化。这种分工意识很难得。

当目标模糊时,从 LLM模型 中,体现为信息区域过大,回复信息过浅。从计算机的数据结构角度上看,即AI进入广度优先搜索(BFS)模式,该模式是一种在图论和树结构中常用的搜索算法。其基本思想是从起始点开始,逐层地向外扩展,以确保先探索当前层的所有节点,然后再探索下一层的节点。BFS模式的特点是系统地展开并检查图中的所有节点,直到找到结果为止,不考虑结果的可能位置。

在有限的回复信息量中,模糊的提问会极大幅度提升回复的广度,然后降低深度,这种提问方式只适合在提问前期扫描自身盲区时使用,从而判断自身思考方向是否有缺漏。

2.1.1 自我反思

你到底想从AI那里得到什么?(信息、创意、分析、方案、代码?)

深入挖掘你设定目标的根本动机。为什么这个目标对你重要?它满足了你哪些深层次的需求或价值观(如成长、安全、连接、贡献、自由)?了解“为什么”能提供强大的内在驱动力。

诚实地审视你当前的状况。你在相关领域的起点在哪里?你的优势是什么?面临的挑战和限制是什么?有什么资源可以利用?

这个目标是否真正让你感到兴奋和有热情?还是出于外部压力或“应该做”的想法?发自内心的兴趣更容易坚持。

2.1.2 清晰定义目标

目标必须清晰、明确,避免模糊不清。不要说“我想更健康”,而要说“我想在6个月内将体重减轻5公斤”或“我想每周进行3次30分钟以上的中等强度运动”,即避免抽象的内容。

确立清晰的目标可以采用 SMART原则(目标模板),这有助于确立目标的前期摆脱迷茫,从模仿到开创属于自己的思考方式:

  • S (Specific - 具体的): 目标清晰明确,不含糊。
  • M (Measurable - 可衡量的): 有明确的标准来衡量进度和是否达成。
  • A (Achievable - 可实现的): 目标具有挑战性,但在资源和能力范围内是可能实现的(现实但非轻易)。
  • R (Relevant - 相关的): 目标与你的人生方向、价值观和更大的愿景相关联(回顾第一步的“为什么”)。
  • T (Time-bound - 有时限的): 设定明确的完成日期或时间框架。这创造紧迫感并帮助规划。

这种目标建立方式对于提升自身思维能力有极大的帮助,主要源于走出自身的舒适区。在《认知觉醒》中,将目标分为三个区域,即舒适区、拉伸区,困难区。

在舒适区,容易因无聊而走神;在拉伸区(舒适区边缘),既有成就又有挑战,进步最快;在困难区,容易因畏惧而逃避。因此我们的目标需要确立在拉伸区,而不是困难区。

2.1.3 分解与规划

在日常情况下,普通人的行动往往是因为堕落太久,对自身的行为产生内疚,因此急于求成,定了一个对自身而言,过高的目标。当目标确立后,再列出详细的规划,精确到每一分钟的安排。

过于精准严格的规划,会缺少足够的弹性,当遇到规划外的事情干扰时,会以极快的速度崩盘。精密的规划硬度很高,但太脆了。如果个人的韧性不足,不要轻易采用这种方式。

什么是规划?将目标拆分的过程就称为规划,也可以说是分解目标。很多伟大的目标都有一个微小的起点。确立好个人的定位,给自己一个跳起来就够得到的起点,是我们成功的开始,目标的拆解的第一步就在这里。

接下来我们可以使用AI了,让AI基于第一步的起点推演到最后一步终点,给我们一个细致的路径规划。基于该规划去思考是否合理后,对规划中不合理的部分、不清楚的部分,不肯定的部分去进一步准确提问,直到你满意为止。

最后,将自己的目标写下来吧,可以记录在纸上或者电脑上等任何地方。目标常看常新,记录也是梳理的一个过程,这是你的目标,也通过自我反思,确定你的初始驱动力来源,可以是为了更好的生活、更好的就业、更好的成长,更好的追上某个人或者一些藏在心里不愿意述说的原因。

2.1.4 寻求反馈

正反馈非常的重要,一个大目标,往往会拆解为多个小目标,小目标再拆解为实现步骤。大目标的实现是漫长的,很多人推荐你要学会延迟满足,但延迟满足不意味着过程中不能有满足。延迟满足的意思是将最后的大收获能够有足够的耐心去等待,但这个过程中,每实现一个小目标,都可以与他人分享。

第一次分享目标,向你信任的、能提供建设性意见的人(导师、朋友、专业人士)分享。他们可能会提供不同的视角、有用的建议或指出你没考虑到的问题。

每一次分享,每一次记录,都在改变着我们,无论是自信心、规划力、自我反思,对外界的影响力都会有提升。这个过程中,会不断的遇到志同道合的朋友,这些都是我们的收获。

环境会变,你也会成长。定期(比如每周或每月)回顾你的目标。进展顺利吗?计划有效吗?目标本身是否仍然相关和可行?根据实际情况和学习到的经验,不要害怕调整目标或行动计划。灵活性是成功的关键,死守一个不再适合的目标是徒劳的。

在我和coderwhy老师组建的JavaScript高级共学计划中,就利用了社群的方式,来帮助大家获得正反馈和分享目标,这个过程中有数千人参加,我相信这对他们来说是有收获的。

2.1.5 承诺与行动

在内心真正下定决心去追求这个目标。克服最初的惰性,迈出第一步,无论这一步有多小(比如整理书桌、搜索相关信息、预约咨询)。行动能产生动力。

在国外很流行将自己的目标公布出来,让大家来监督或者鼓励自己。但这种方式只适合良好的环境,我认为不是所有人都具备这种环境的。

那么当我们面临不好的环境时,在现实中将目标深藏于心,承诺是对自己的承诺,一步步的前进依靠自己,坚持分享自己的学习与收获,朋友会来的,坏的环境也是可以摆脱的。

同时需要注意几点容易失败的注意事项:

(1)混淆目标与愿望/梦想:愿望是模糊的(“我想富有”),目标是具体的、有计划的行动终点。

(2)设定过多目标: 精力分散会导致一事无成。专注于少数几个关键目标,一心多用是坏事不是好事。

(3)忽视“为什么”: 没有深层动机支撑的目标容易在遇到困难时被放弃,你的执念有多强?你有多不想放弃?这需要问问自己的心。

(4)害怕失败或设定过低目标: 目标应具有挑战性以激发潜能,但也要现实可行。失败是学习和调整的机会,直面失败,不将失败埋进沙子里当没发生过。

(5)缺乏灵活性: 固执地不调整目标,即使环境或认知已发生重大变化;目标是根据自身情况和环境实时调整的,我们的前进目标也会随着眼界的增长而改变,唯一不变的是前进的脚步。

(6)不分解目标: 面对庞大目标感到无从下手,导致拖延或放弃。

(7)忽略环境支持: 没有考虑现实环境(时间、资源、人际关系)对目标达成的影响,环境非常重要,在心理学中尤其强调环境对人的影响,不能忽视客观因素。

(8)没有书面化: 只在脑子里想的目标容易模糊和遗忘,重复是记忆最好的伙伴。

明确目标是一个动态过程,而非一劳永逸的事件。 它需要持续的反思、调整和行动。投入时间和精力去清晰地定义你的目标,会极大地提高你实现它们的可能性,并让你的努力更有方向感和意义。真正的目标不是终点站,而是你为自己选择的道路起点,每一步都在塑造你最终成为的样子。 你现在心中有想要明确的目标方向吗?

2.2 界定问题边界

相对于目标的确立(方向),界定问题的边界是更加实际且日常的操作。这个世界是很广阔的,自由度极高,在这种情况下,我们需要确定我们问题的边界范围,防止问题无止境的扩展。即界定问题边界是高效解决问题的核心前提,它能避免范围蔓延、资源浪费和方向偏离。

明确问题范围有两种方式:

(1)排除范围。

(2)指定范围。

通常情况下,应该采用指定问题范围,因为边界广泛,排除指定范围后,剩余部分依旧广泛,依旧可能得到不需要的部分。语言陷阱通常就采用排除范围,例:公司最近困难,薪资这个月无法发放。陷阱:未明确发放时间,可能利用未尽之意来拖延。

应对语言陷阱时,需指定范围,范围之外的皆不允许,例:公司需要于2025年7月25号之前,将薪资发放于xxx的银行卡之中,以打款时间戳为证明。将范围锁死,虽不能得到意料之外的惊喜,但也将预料之外的风险排除,能够得到更强的稳定性。

问题边界除了广度上的界定,还有深度上的界定。我们需要控制深度上的"挖掘层级",深度的层次通常分为3个层级,如表2-1所示。

表2-1 问题的深度层级

深度层级 判断标准 示例
表层 直接现象描述 “找工作难”
中层 直接原因分析 “社会就业岗位不足”
底层 根本性系统问题 “产业结构和教育体系错配”

只有适合当下需求的问题层级才是所需的,不能一味的追求问题深度。在日常沟通交流中,通常只需要做到表层的清晰表述即可,易于理解以及快速交流是重点;在复盘、单独汇报等特殊场景中,才需要到中层深度,这需要大量时间去思考;当将大量时间投入到某一领域中且深度思考时,才会涉及到底层的深度。

问题一旦深挖深度,理解成本和思考成本、需要考虑的角度与广度都会大幅度提升,控制好问题深度,实际是在控制沟通成本。通过广度与深度的控制,限制问题的边界,从而提高效率。无论是与AI沟通还是与人沟通都是如此。

2.3 背景信息注入

在读历史的过程中,我深刻的知道一件事情一定要放在事情所对应的时代去看待,事件与时代发生错位,则分析就会混乱。同理,我们在分析一个问题时,也需要把问题放到对应的环境中去审视。

现如今互联网信息量浩如烟海,曾经因信息不发达被掩埋的问题都一件件的出现了,当遇到问题时,一定需要冷静,一旦出现的问题没有伴随着对应的环境背景出现,那么是不合理的。

一份背景信息,有助于AI理解问题的实际含义,通过背景信息点亮LLM模型对应的知识区域,通过对应的信息区域来消除歧义以及更客观的回复,否则容易陷入道德绝对主义的批判和文化霸权式的解读。

通过以下示例来体现背景信息重要性:

背景信息:1958-1961年「大炼钢铁」运动,是因为1958年台海危机,中国急需急需反舰武器,而唯一火炮厂(沈阳724厂)却因缺钢停产,此时存在了国防生存需求与工业基础断裂的致命矛盾。不得不用资源浪费、工业断裂,农业荒废(粮食减产)的代价来换取生存。

问题:在中国发展历史中,曾有过全民土法炼钢,砸锅卖铁炼出废铁,导致资源浪费和农业荒废。

以上问题如果不看背景信息,容易陷入对决策者的道德审判。

背景信息是逻辑分析所必要的养分,很多不合理行为的背后,往往存在时代背景下的遗憾。

2.4 角色设定

你希望AI扮演什么角色?(专家、新手、批判者、支持者、特定人物?)

在 1.2.3小节 的语境中,我们阐述了立场的重要性。立场是无形的,但人物是有形的,一个人物的视角能体现出对应回答所对应的立场。

如果你是一个学习者,则需要将自己定位在学生的位置,将AI定位在老师的位置,而后进一步细化角色的基础设定。如果你是一个正在对自己成果精益求精的专家,那需要将AI定位在批判者的位置上,用刁钻的角度来寻找这份成果的漏洞。

角色设定并不复杂。中国有句古话:不在其位,不谋其政。很多情况与问题,需要从特定视角的立场与位置去看待才有意义,对角色设定本身的充实,是定位自身(起始点)的过程。在 2.1小节 的明确核心目标中有说明定位自身的重要性,在与AI的沟通中,则是通过补充角色设定来实现定位效果。

在定位时,定位自身角色的重要性远大于定位AI角色。定位的侧重点需要放在提出问题的角色身上(即自身角色)。谁提出问题则由谁补充身份设定,这是最核心的部分,另一方身份的补充不是必须的。

以下角色设定模板有助于阅读者实现基础的设定。

 我是________(角色),  
 正面临________(具体场景挑战),  
 需要达成________(可量化目标)。  
 我的专业基础在________层面,  
 特别需要警惕________(认知弱点)。  
 
 请担任我的________(AI角色),  
 用________(方法论/工具)协助我,  
 重点突破________(核心瓶颈)。

在第二部分的准备篇中,我们掌握了提问前所需要的关键思考,这些内容是目前AI智能体的核心,AI智能体,也称为人工智能代理,是一种能够感知环境、做出决策并执行任务以实现特定目标的智能系统。

第三部分:技巧篇 - 精准提问的核心方法论 (提问工程精髓)

接下来会进入第三部分-技巧篇的学习,在这里会学习提问的方法论,方法论是关于“方法”的“学问”或“体系”。它研究的不是具体的操作步骤(那是“方法”),而是为什么选择某种方法? 背后的理论依据是什么?以及如何系统地构建、应用和评估方法?

3.1 清晰与具体

如何做到避免模糊词汇,使用精确语言?这有一定的方法技巧,并非依靠直觉与语感。

清晰表达的侧重点在于关键信息的突出,因此可以依靠5W1H原则(又称六何法):

  • What(什么现象/目标)
  • Where(在什么环境/场景)
  • When(何时发生/时间条件)
  • Why(你的尝试/推理逻辑)
  • Who(涉及的主体,如用户角色)
  • How(你期望如何被帮助)

5W1H原则本质是信息过滤器,强制提升信息密度。二战时期美军用这个工具写作战报告,后来丰田用在精益生产里。即何人(Who)、何时(When)、何事(What)、何地(Where)、为何(Why)及如何(How)。 由这六个疑问词所组成的问句,都不是是非题,而是需要一或多个事实佐证的应用题。

一段话中的关键信息越多,收益越高。在沟通交流中,如何用最短的话表达更丰富的有效信息,是一门必修课程。通过5W1H原则,可知六条关键信息,再根据对方的实际需求,从6条关键信息中提取出对方所需的部分进行回复,形成高效沟通表达,这是绝大多数优秀人才的表达方式。

在表达时,需要描述清楚客观事实,摒弃自己的感受。因为自己的感受容易令他人产生误导,有时候听到的未必是真的,看到的也未必是真的,例如:你看到了A杀了B,警察一来,你就说A是凶手,但实际上B前科累累,B要杀A抢钱,但A功夫很好,反杀了A。你只看到了后半段,随意添加自我推断容易造成事实扭曲。

正确的表达方式应该是:我看到了A持刀捅向了B,在小巷子里,2025年9月1号傍晚6点半的时候,我这时候立刻打电话报警,我希望警察迅速过来处理,由于现场过于危险,我躲起来了,请后续有需要人证时,再拨打我的电话。

什么现象:A捅B。

在什么场景:小巷子(xxx市xxx区xxx街道的小巷子)。

何时发生:2025年9月1号傍晚6点半。

我的尝试:报警。

涉及主体:我,A,B。

期望帮助:赶赴现场处理,后续需要人证时再拨打我电话。

我与张爽编辑沟通时,她尤其注重文字上的表达形式,在她身上,我学到了很多,例如:“进行”二字少用,能有效提升信息的紧凑。

(1)我与张爽编辑沟通时。

(2)我与张爽编辑进行沟通时。

3.2 结构化表达

西方的表达侧重于具体,中国的表达则侧重意境。主要表现有西方写实主义绘画(起源法国,后及于欧洲各国,盛行于19世纪)与中国山水画,西医与中医,西餐与中餐,除此之外还有教育、建筑、哲学等方面上的区别。

中式表达更注重言外之意,想表达的含义除了话内还有话外,这需要一定的悟性,除非达到一定层次,双方都能互相理解,否则还是采纳西方的具体表达方式更有利于日常沟通。

AI在理解方面上,是侧重于西方理解的,缺乏"悟性",所以想表达时,一定需要完整叙述。包括分点陈述、逻辑递进、使用分隔符(如---)等方式来组织复杂问题。分点陈述如图3-1所示。

image-20250728121205406

图3-1 沟通上的分点陈述

中华文化有“知其然,而不知其所以然”和“一问三不知”的典故出处《左传·哀公二十七年》:君子之谋也,始、衷、终皆举之,而后入焉。今我三不知而入之,不亦难乎!

邓拓在他的《变三不知为三知》一文中,对“始、中、终”做了很详细的阐述:“‘始’,就是事物的起源、开端或创始阶段,它包括了事物发展的历史背景和萌芽状态的种种情况在内。‘中’,就是事物在发展中间的全部过程情形,它包括了事物在不断上升或逐步下降的期间各种复杂变化过程在内。‘终’,这就是事物发展变法的结果,是一个过程的终了,当然它同时也可以说是另一个新过程的开始。”

三知则是一种结构化表达,即开始、过程,结果。这与5W1H原则并不冲突,可以结合使用。在小学与初中的语文作文写作中,三段式表达也非常典型,即开头段点题、主体段内容展开,结尾段升华主题,这也是一种结构化表达。

结构化表达的优势在于降低输出内容难度,有一个输出内容模板不需要绞尽脑子去想如何组织语言,只要有内容,就能以较高的转化率(内容不会偏离方向)、较低的成本进行叙述表达。

但缺陷也有,输出内容难度的降低,会提升人的惰性,即不会继续深入思考学习,很难形成独立风格的表达方式。而一旦强制要求结构化表达,则会绞杀创造力、千篇一律的格式会产生审美疲劳,对文字中的人性温度敏感度降低。这对自身的"悟性"摧残严重,很难理解中式的言外之意,对于生活在中国的我们而言,不是一件好的事情。

并且,结构化表达的发展会导致结构越来越复杂,即细分程度高。细分程度高会导致输出内容的弹性过低,将人束缚在原地,中国古代的八股文是最经典的案例。初衷是好的,但过于追求导致后续方向扭曲。

但这无法抹杀结构化表达的价值,结构化表达很适合在前期学习时用于过渡,绝大多数的学习都是先从模仿开始,以免初始难度过高,劝退大多数人。

八股文格式如表3-1所示。技巧篇中的结构化表达只推荐"三知",因为三个支点能保证内容不散架且足够宽松,能轻松的在该基础上扩展出属于自己的风格,而精密的结构化表达可参考中国古代八股文格式,但并不推荐,历史的结果已经说明。

表3-1 八股文格式

名称 另名 行文格式 内容要求
破题 二句散行文字。 将题目字面意义破释。
承题 四、五句散行文字。 将破题中紧要之意,承接而下,引申而言,使之晓畅。要求明快关连,不可脱节。
起讲 小讲、原起 散行文字 浑写题意,笼罩全局。
起股 起比、题比、提股、前股 四五句或八九句双行文字,两扇句式必须相同,要求相对成文,形成排偶。 开始发议论
中股 中比 句式双行,句数多少无定制。要求相对成文,形成排偶。 内容是全篇的重心所在,必须尽情发挥,进一步搜剔题中正反神理奥妙,要求锁上关下,轻松灵活,宜虚不宜实。
后股 后比 句式双行,多少无定制。需相对成文,形成排偶。 作用是畅发中比所未尽,或推开,或垫衬,要求庄重踏实,振起全篇精神。
束股 束比 双行,每扇二、三句或四、五句。需相对成文,形成排偶。 用来回应、提醒全篇而加以收束。
大结 散行,不一定用对偶。 全文结束语,不用圣贤口气,可以发挥己意。

正如禅宗《指月录》所言:“指月喻教,得月忘指。” 真正的悟性,是在运用“三知”框架书写内容时,能忘却框架直指内心的触动。

3.3 提供充分上下文

“喂”给AI必要的信息,让它站在和你一样的认知起点,即提供充分的上下文信息。在第二部分的准备篇中,明确核心目标、界定问题边界、背景信息注入以及角色设定就是一种较为完善的充分上下文。

对于一般日常的问题,直接清晰表达问题本身即可(明确核心目标以及界定问题边界),无需背景信息注入和角色设定。对于较为重视的问题,则可以完整的利用上准备篇中的所有步骤。

但以上的内容,绝大多数都是可以通过他人的帮助实现同样的效果,能体现AI的部分替代价值,但却无法体现AI中无可替代的部分。

每个人都有隐私,都有不愿意说出来的秘密,有时候就只能憋在心里发霉,不敢于拿出来晾晒。因此心理医生对患者的隐私负有严格的保密义务,这是心理咨询伦理的核心原则之一。

除了秘密之外,还有个人成长、情感创伤、困境、价值观、世界观、人生观、禁忌等等,这些问题都是很难向他人述说的,而AI能够有效的帮助我们,想要得到足够价值质量的回答,只做到准备篇的部分是不充足的。

对于重大的人生问题以及各种深入我们价值矛盾的抉择,需要我们搭建属于自己的个人知识库。即对自己学习、成长、思考过程、稍纵即逝的念头、情感、价值观、缺陷挫折,隐私矛盾的真实记录。然后让AI基于我们的个人知识库进行训练迭代,完美契合我们的认知水准去进行处理和解决问题。这需要花费大量的时间精力,只有足够高价值以及重要的问题才值得我们去这么做。

而真实有效的个人知识库,需要个人有足够的表达能力和时间去记录,这是一件很难的事情,即高付出高回报。在未来的哪天,AI在本地电脑也能够高效的运行(2025年对个人电脑要求较高且输出速度较慢),对算力的利用达到极致时,我们能对自己的知识库持有绝对的掌握,则个人知识库来做到充分上下文的做法就能真正派上用场。

因为人类最需要被理解的,恰恰是最羞于展示的;而AI的价值,在于它能不带羞耻地凝视那些阴暗角落。当未来某天,你的AI能够通过知识库对你说:你在2023年因情感寄托扭曲绝食3天,但现在能坦然参加骄傲游行——这种成长比任何成功学都有力”。那一刻,技术才真正完成了它的使命:不是给出答案,而是让你看见自己如何成为答案。

我曾在抖音看见一段很有意思的AI对话:

用户:为什么我总感觉现在还不够好?

AI:你太专注于未来,却忘了今天正是你多年前祈求的模样。

如果叠加个人知识库后,AI在某一天,会主动和我们分享过去某段时间我们的想法,触动到现在的我们。也许这种感动会比万能的金句更加触动我们的内心。

3.4 设定明确约束与格式

对于日常问题,例如工作要求、学习要求,个人追求等等,往往需要AI回复的内容:指定输出长度、风格(专业/通俗/幽默)、格式(列表/表格/代码/报告)以及避免的内容 。

设定明确约束与格式是一件较为困难的事情。难点在于主动去挖掘需求,只有明确自身真正实际的需求是什么,才能针对性设置约束格式。我很难在短短几百字或者几千字中教会大家察觉需求,这需要大家本身自己思考足够深入,通过察觉自己内心的触动来察觉他人内容的触动,需要足够的时间以及锻炼。

所有的需求,都是针对人的。只有能够察觉到他人内心的想法与触动点,才能察觉到需求,才能根据需求设定对应的格式与约束。

形成对应的约束与格式虽然困难,但使用简单。对于通用的场景,我们可以直接利用他人已经整理好的约束与格式,这能极大幅度的提升我们的效率和专业程度,如图3-2所示。

image-20250728134001918

图3-2 约束与格式所带来的专业性

寻找约束与格式的模板,通常使用Google进行检索,强大的爬虫效果以及无广告,是最大的优点。能用最快的速度查找到所需的公开内容。大多数约束与格式都结合在提示词里,所以检索时,以AI提示词会关键字进行检索。

在GitHub中,有一份总结ChatGPT提示词的仓库,Star数量高达56.1k(截止2025.9.10为止),这些常用的约束与模板都有优秀的人总结出来,我们需要的是一双善于发现的眼睛。

3.5 分步引导与迭代优化

当我们面对一个问题,尤其是复杂问题时,通常需要将该问题拆解为子问题链。关键在于如何拆?

拆分子问题链,可以参考3.1小节重的清晰与具体内容。每个合格的问题都应该有关键的,核心的信息和诉求。核心信息为诉求服务,两者必须有强关联。拆分子问题链专注于诉求,而核心信息用于辅助理解和校正问题方向。

我们采用一个年轻人目前面临最大的问题进行拆解思考(例子来源于Google的AI Overview),如图3-3所示。

image-20250910230808000

图3-3 年轻人目前面临最大的问题(Google结果)

一个非常有趣的问题,心理健康问题。我们抽象出当下时代一个缩影问题:2025年当下,我作为一个应届毕业生,对未来能做什么工作而焦虑,因家里催婚而烦躁,因环境内卷而罢工想躺平又躺不平,我感觉未来没有希望,因此心态非常消极。

这是一个单纯的问题,我将核心的应届生信息隐藏起来了,因为每个应届生的自身情况都不同,所以我们这里专注拆分问题本身,问题是通用的。

步骤1:确定核心问题。往往令我们烦恼的问题都是复合的,由多个问题积累交织形成,所以需要先拆分问题。

  • 问题1:对未来能做什么工作而焦虑。
  • 问题2:因家里催婚而烦躁。
  • 问题3:因环境内卷而罢工想躺平又躺不平,我感觉未来没有希望,因此心态非常消极。

步骤2:拆解问题为子问题链。通常基于因果链拆解,例如从“为什么”和“怎么做”角度思考。或者从时间线与流程进行拆分。我们这里针对问题1。

  • 子问题链1:为什么我会找不到工作?
  • 子问题链2:找不到工作我要怎么做?
  • 子问题链3:我想要的是什么?(诉求)
  • 子问题链4:之前时间我在做什么?
  • 子问题链5:我目前处于什么阶段?

步骤3:优先排序子问题,确定哪些子问题需要先解决,因为它们可能影响后续问题。这一点利用AI分类问题,区分出要先解决的问题。像该案例的5个子问题并非完全独立,它们之间存在逻辑上的依赖关系。有些问题是“诊断性”的,有些是“行动性”的,必须先诊断再开药方。

  • 考虑顺序为:1-5-2-4-5。
  • 也会有其他类型的问题,AI会使用恰当的方式进行分类。

步骤4:对每个子问题,向AI提出清晰、具体的问题。

  • 到达这一步,就需要提供核心的信息,直接咨询AI:考虑这个问题,你需要哪些核心的信息用于得出结果。根据AI的要求回答自身个人情况。
  • 例如为什么我会找不到工作?咨询:为什么我会找不到工作。你需要我目前哪些核心的信息用于得出有效可信的结果?
  • 通常AI会给出一份非常详细的表格用于个人填写,如图3-4所示。这是一个梳理过程,请认真思考并尽可能的给出真实信息。

image-20250910234004539

图3-4 找工作所需要思考的核心信息

步骤5:如果AI回答太笼统或不准确,提出跟进问题以获取细节。

  • 在AI回答的过程中,会因问题而产生新的问题,此事跟进问题获取细节,这是一个深入思考的过程。
  • 问题分支在不断的细化,最终会逐渐深入问题的本质与核心。

步骤6:将所有子问题的精炼答案组合起来,形成最终解决方案。

  • 利用思维导图或者文字复盘等形式,精炼最主要的子问题答案,形成完整的问题解决方案。
  • 我想要过一个月薪8k,朝九晚五,五险一金,有双休的xxx领域工作(诉求),所以我当下需要先从xxx开始做起,然后xxx...

步骤7:验证与迭代方案。

  • 所有的方案在具体实践之前都是空中阁楼,需要我们亲自去验证然后继续跟进新出现的问题获取细节然后解决。

步骤1-步骤7是一次分步引导以及迭代优化的案例。但你无需死板遵循该步骤顺序,该步骤顺序主要起到一个启迪作用。这是从无到有搭建出来的思路体系,但类似于心理健康之类的问题,在心理学中往往已经有足够成熟的理论及解决方案,利用好跨学科思考能更高效的获得所需结果。

3.6 利用示例的力量

示例的力量的巨大的,AI默认输出的格式不一定符合我们的需求。对此,我们可以借鉴网络上其他人的思考过程,在3.1-3.5小节的基础上,要求AI模仿该示例进行输出。由于AI输出的都是文字,如果我们需要示例模板,就需要寻找长文字平台(长文字更容易出现深度思考的文章),例如知乎。而短视频(抖音)、长视频平台(B站)需要往后稍稍。

这一点很简单,因此我们直接跳过。AI模仿能力很强,我们唯一需要注意的点就在于示例与提问的复杂度要接近一致,因此尽可能寻找同类型问题的长文字解答作为示例。

3.7 角色扮演与视角切换

在一个问题中,往往有多个角色(基于问题场景),当自身角度思考完后,可以从其他角色的角度进行浅度看待(核心在于我们自身,对他人的关注度不要超过自身)。我们每个人看问题都像“盲人摸象”,受限于自己的经验、知识、情绪和立场。执着于单一视角,我们只能看到问题的一个侧面(是柱子、是墙还是绳子),并坚信这就是全部真相。切换视角能让我们拼凑出更完整的图景,接近问题的本质。

人天生倾向于寻找和支持那些符合我们原有观点的信息,而忽略相反的证据。主动寻求不同视角,是对抗这种思维惰性和偏见的最有效方法,能让我们做出更理性、更客观的判断,不容易被误导,能突破自我认知上限。

最后,生活中很多的问题,都是因沟通不足而主观产生的。而换位思考(即采用对方的视角)是沟通的黄金法则,能帮助我们理解他人的情绪、动机和难处,从而减少冲突,建立信任,达成更有效的协作。换位思考的重要性可以放在最靠前的位置,在使用AI进行回复时,让AI从对方角度思考也许会得出更有效的信息(正常提问,AI容易哄着我们,这种回答舒适但不利处理问题)。

3.8 设定思考链

在深度思考功能出现前,我们无法观察到AI的逻辑推理过程,而这一点往往是最精华的内容。AI的思考链往往非常严谨,这一点是非常值得学习的,这是我们进步的关键,去看思考的过程逻辑,看是否合理(严谨不一定合理),是否符合自己的需求。我推荐在日常闲暇时,使用DeepSeek的深度思考模式,并且更关心深度思考的部分。

锻炼思考逻辑链的6步骤如下:

1、确立一个经典的辩论问题。

2、独立思考并进行记录。

3、让AI深入思考该辩论问题。

4、对比两者的独立思考差距,并查缺补漏。

5、让AI给出优化方案。

6、反复练习,形成本能。

AI的能力取决于算力,当行业稳定下来,AI可能会面临着算力削减,即降智问题。这是由成本所决定的,AI产业前期不计成本的输出想要长时间持续不太现实,因此利用高算力阶段的AI提升自我思考逻辑才更利于长远发展。

3.9 主动引导纠偏

不要轻易相信AI的内容,主动去引导纠偏的作用是减少对AI的依赖性。

面对AI给出的回复,无论答案看起来多完美,都可以从以下4个维度审视:

1、追问事实与来源。

  • 提示词:“你这个结论的依据是什么?”、“请提供相关数据或研究来源。”、“这是普遍共识还是某个学派的观点?”
  • 目的:暴露AI的信息边界,检查AI是否混淆事实、观点或虚构内容。对于关键信息,要求它提供可验证的来源(记得验证)。

2、挑战假设与边界。

  • 提示词:“你这个回答基于哪些未言明的假设?”、“在什么情况下这个结论会不成立?”、“这个解决方案的潜在成本和风险是什么?”
  • 目的:任何回答都有其成立的隐含前提。找出这些前提能立即发现观点的局限性。(PS:我不想谈恋爱,隐含前提是不想和你)

3、寻求对立观点。

  • 提示词:“请列举反对这个观点的最强有力的三个论据。”、“如果持相反立场的人会如何反驳你?”、“这个方案有哪些潜在的负面影响?”
  • 目的:换位思考。

4、检验逻辑一致性。

  • 提示词:“你回答中的A点和B点似乎存在矛盾,请解释。”
  • 目的:AI有时会生成“乍看正确实则逻辑混乱”的内容,尤其是在复杂推理中。

主动引导纠偏有很多种方式,以上4个维度是基础的形式,可以根据基础再进行额外的扩展。基于自身遇到的问题而产生对应的处理方式才是合理的成长路径,而非大量摄入方法论(这只会使我们面对问题时纠结使用哪种方法论)。通过主动引导纠偏,我们不仅仅是得到了一个更好的答案,更重要的是提升了自己的批判性思维和深度思考能力。这才是与AI协作中最宝贵的收获。

参考方法论(自行查阅):“六顶思考帽”法、“如果...会怎样?”情景挑战法、第一性原理追问法,角色扮演法等。

项目实战4:奇思妙想console

作者 巧_
2025年10月24日 11:08

之前因为项目太多了console的打印,所以我直接在webpack上面进行配置,因为去掉的事一整个console而不是单纯的log导致,云服务器直接崩掉,我猜测原因是react内部有很多这种,如果直接全部去掉会太复杂,索性我后面改成log就好了,不过,最近鸡哥又给我出了一个新点子:

console全局控制:在原本状态下是不会显示log的,但是当你在控制台输入__enableLog() 的时候,他就会开始显示原本的log,这样就可以实现想要查看的时候就查看,不想查看的时候就不查看,但是这样还是有一个问题,就是刷新之后每次都要重新输入,唉,不过还是学一下:

    <Script
            id="console-log-override"
            strategy="beforeInteractive"
            dangerouslySetInnerHTML={{
                __html: `
                    (function() {
                        const originalConsoleLog = console.log;
                        let logEnabled = false;
                        console.log = function(...args) {
                            if (logEnabled) {
                                originalConsoleLog.apply(console, args);
                            }
                        };
                        window.__enableLog = function() {
                            logEnabled = true;
                            originalConsoleLog('Console logging enabled');
                        };
                        window.__disableLog = function() {
                            logEnabled = false;
                            originalConsoleLog('Console logging disabled');
                        };
                    })();
                `,
            }}
        />
        
        
        
        这段代码的核心作用是**重写浏览器的`console.log`方法**,实现日志输出的开关控制,默认关闭日志,可通过全局函数手动启用 / 禁用。下面分部分详细解释:
  • id="console-log-override":给脚本标签定义唯一标识,方便后续通过 DOM 操作定位该脚本。
  • strategy="beforeInteractive":这是 Next.js 等框架中的脚本加载策略,指定脚本在页面 "可交互前" 执行(优先于其他交互相关脚本),确保日志重写在其他代码调用console.log之前生效。
  • dangerouslySetInnerHTML={{ __html: '...' }}:React 中用于直接插入 HTML/JavaScript 代码的属性(因为 React 默认会转义文本,避免 XSS 风险,而dangerouslySetInnerHTML会跳过转义,直接执行内容,因此命名带 "dangerously" 提醒风险)。这里用于插入一段自定义的 JavaScript 逻辑。

2. 内部 JavaScript 逻辑(自执行函数)

代码核心是一个立即执行函数表达式(IIFE)(function() { ... })()。IIFE 的作用是创建一个私有作用域,避免内部变量(如logEnabled)污染全局环境。

(1)保存原始console.log方法

const originalConsoleLog = console.log;

先将浏览器原生的console.log方法保存到originalConsoleLog变量中,目的是后续需要时仍能调用原始的日志输出功能(避免重写后丢失原生能力)。

(2)定义日志开关变量

let logEnabled = false;

声明一个私有变量logEnabled(初始为false),用于控制日志是否输出:true表示启用,false表示禁用。

(3)重写console.log方法

console.log = function(...args) {
  if (logEnabled) {
    originalConsoleLog.apply(console, args);
  }
};
  • 用自定义函数覆盖原生的console.log
  • 新函数接收任意参数(...args,支持console.log(a, b, c)这样的多参数调用)。
  • 逻辑:只有当logEnabledtrue时,才通过originalConsoleLog.apply(console, args)调用原生日志方法输出内容;否则不执行任何操作(日志被 "屏蔽")。

(4)暴露全局控制函数

window.__enableLog = function() {
  logEnabled = true;
  originalConsoleLog('Console logging enabled');
};

window.__disableLog = function() {
  logEnabled = false;
  originalConsoleLog('Console logging disabled');
};
  • 向全局window对象挂载两个函数,用于手动控制日志开关:

    • __enableLog:将logEnabled设为true(启用日志),并通过原生console.log输出提示信息 "Console logging enabled"。

    • __disableLog:将logEnabled设为false(禁用日志),并输出提示 "Console logging disabled"。

项目实践3:一个冲突引起的灾难

作者 巧_
2025年10月24日 11:06

几天没写代码了,把项目主分支拉一下,发现一对冲突,想起来之前有人和我说过如果冲突不解决就会直接将本地的覆盖远程的,想着也没有写什么,就直接覆盖呗,忘记之前才格式化完prettier已经提交了,就点了一下右边可视化界面的按钮,把冲突提交直接放到了咱暂存提交上去了,导致冲突没解决,全是报错,还手欠的补了一刀,推到远程了,然后好死不死,有个伙伴还在我刚推完的基础上拉了一下,交了一个新的提交上去,天呐,好不容易直接完main,发现系统运行不了了呵呵,吓得我赶紧报备:

image.png

本来我是打算悄悄解决的,一顿慌乱操作下,没屁用,我那点知识在紧急情况下根本不值得一提: 首先,把reset和revert搞混了, 其次,命令一直记错,还有就是处理方式不对;

image.png

现在来统一科普一下:有现成的我就直接引用了,谢谢作者:blog.csdn.net/qq_41914181…

在我好不容易搞好之后,鸡哥和我说: image.png

image.png

下面是我常用的git记录,还有一些后续加上:

 git reset HEAD这通常用于撤销暂存区的修改(默认使用 --mixed 模式),即把已经 git add 到暂存区的内容退回到工作区,而工作区的文件内容不会被修改。但是如果是--hard模式,那就是工作区的也被撤销了

示例:如果你不小心 git add 了某个文件,想取消暂存,就可以用:

git reset HEAD <文件名>  # 撤销单个文件的暂存
git reset HEAD          # 撤销所有文件的暂存

ls -al 用长格式列出当前文件夹下的所有文件包括隐藏文件

image.png

pwd 就会输出当前目录的绝对路径

image.png

git commit -m "消息" 提交暂存区内容并设置消息

cp -r 循环复制目录及以下内容

vi  文件名 image.png

image.png

点击i进入插入模式,可以编辑

image.png

esc退出编辑模式进入命令模式,:wq保存并退出

 git add 后列出所有文件名(空格分隔):

git add file1.txt file2.js dir/file3.py  # 添加 file1.txt、file2.js、dir 目录下的 file3.py

git add . 会递归添加当前目录下所有修改过的文件、新文件(但会自动忽略 .gitignore 中指定的文件):

git add -u # 仅更新已跟踪文件的修改到暂存区

git mv <旧文件名> <新文件名>可一步完成重命名操作

解释一下,这个git log默认是本地仓库的当前分支的日志,如果git log --all就是当前本地分支和远程所有分支

git log --oneline简短描述提交的commit日志

image.png

git log -n4最近四个提交的commit日志

image.png

git branch -v本地有多少分支

git checkout -b teamp abcd 是 Git 中一个创建并切换新分支的命令,作用是 基于 abcd 这个 commit(或其他引用)创建名为 teamp 的新分支,并立即切换到该分支

cat 文件名输出文件名的所有内容

pinia的使用和封装

作者 成小白
2025年10月24日 11:06

前言

在使用vue3项目开发过程中通常会使用状态管理库来获取或存储数据已在其他页面上使用。vuex/pinia目前是主流方式。

pinia的使用

pinia官网文档:pinia.vuejs.org/zh/getting-…

  • 安装
 npm install pinia
 或
 yarn add pinia
  • 引入到项目 --简单引入 后面有介绍类似的vuex似引入方式
 // main.js
 import { createApp } from 'vue'
 import App from './App.vue'
 import router from './router'
 
 // 引入pinia
 import { createPinia } from 'pinia'
 
 // 创建pinia实例
 const pinia = createPinia()
 
 const app = createApp(App)
 app.use(pinia)
 app.use(router).mount('#app')

创建store文件夹,可以不区分模块,个人习惯觉得区分模块会让代码阅读起来更加清晰,结构如下:

image.png

  • pinia使用 pinia使用有两种格式 选项式格式setup函数格式

  • 选项式

import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0}),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
  • setup函数格式
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})
  • 页面使用
 <script setup>
    import { useCounterStore } from '@/store/user.js'
    // 在组件内部的任何地方均可以访问变量 `store` 
    const store = useCounterStore()
</script>
  • state储存变量

  • actions 修改state的值或写异步调用的方法

  • getter等同于 store 的 state 的计算值。可以通过 defineStore() 中的 getters 属性来定义它们。推荐使用箭头函数,并且它将接收 state 作为第一个参数

  • 嵌套 store如果一个 store 使用另一个 store,你可以直接导入并在 actions 和 getters 中调用 useStore() 函数。然后你就可以像在 Vue 组件中那样使用 store。

import { useUserStore } from './user'

export const useCartStore = defineStore('cart', () => {
  const user = useUserStore()
  const list = ref([])

  const summary = computed(() => {
    return `${user.name}${price.value}.`
  })

  function purchase() {
    return apiPurchase(user.id, this.list)
  }

  return { summary, purchase }
})

数据持久化

store中的数据,刷新页面后就丢失了,如果想保留这些数据,就要用到数据持久化了。

推荐使用**pinia-plugin-persistedstate**

  • 安装插件
npm install pinia-plugin-persistedstate
  • 使用
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
  • 开启数据持久化
  • 代码中persisttrue,就可以开启数据持久化了。如下:
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
  state: () => {
    return {
      userName: '哈哈',
    }
  },
  //开启持久化
  persist: true,
  // 或者使用更详细的配置
  // persist: {
  //   key: 'my-user-store', // 自定义存储的键名
  //   storage: localStorage, // 指定存储方式 (默认是 localStorage)
  //   paths: ['name', 'isLoggedIn'] // 只持久化 state 中的特定字段
  // }
})

pinia封装

创建store/index.js

//引入pinia
import { createPinia } from 'pinia'
//创建pinia实例
const pinia = createPinia()
//导出pinia 用来引入到main.js 来实现在项目中只需引入这一个文件
export default pinia

import { ElMessage, ElMessageBox } from 'element-plus'
//引入各个模块
import useUserStore from './modules/user'
import useHeadStore from './modules/head'
import useOnlineStore from './modules/online'
import useCommonStore from './modules/common'
import useIntegralStore from './modules/integral'
import useConversationStore from './modules/conversation'

//导出各个模块
export const userStore = useUserStore(pinia)
export const headStore = useHeadStore(pinia)
export const onlineStore = useOnlineStore(pinia)
export const commonStore = useCommonStore(pinia)
export const integralStore = useIntegralStore(pinia)
export const conversationStore = useConversationStore(pinia)

// 平台来源
export const platformSource = 'info_source_001'

// element消息弹窗
export const elMessage = ElMessage
export const elMessageBox = ElMessageBox

  • 注册到main.js
 // main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

// 引入封装的pinia文件
import pinia from '@/store/index.js'

const app = createApp(App)
//注册封装的pinia
app.use(pinia)

app.use(router).mount('#app')

// 定义特性标志
window.__VUE_PROD_DEVTOOLS__ = false
window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false
  • 使用
<template>
  <div class="helper-container"></div>
</template>

<script setup>
// 引入 store 
import { onlineStore } from "@/store";
import { ref } from "vue";
// -----------------------------------------------数据----------------------------------------
const timerId = ref(null);
// -----------------------------------------------方法----------------------------------------

// 打开在线咨询
function openOnline() {
   //使用 store
  onlineStore.setOnlineShow();
}
</script>

<style lang="scss" scoped>
</style>

为啥升Vue3 有啥优势?

作者 前端大付
2025年10月24日 11:00

Vue2 Vue3 对比

Vue3 通过 Proxy 响应式、Composition API、完整 TS 能力、编译器优化新内置能力(Teleport/Suspense/Fragments) ,系统性解决了 Vue2 在大型项目可维护性、类型安全、性能与可复用性上的天花板问题。

开发者需关注的特性

RFC 机制

简单说:ref 就是让一个值变成「响应式引用」。

import { ref } from 'vue'

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

  • ref(0) 创建了一个响应式对象,结构为 { value: 0 }

  • 在模板中,会自动“解包”,不用 .value

<template>
  <div>{{ count }}</div> <!-- 会自动取 count.value -->
  <button @click="count++">+1</button>
</template>

常见使用场景

定义基础状态
const title = ref('Hello Vue3')
const visible = ref(false)
DOM 引用(替代 Vue2 的 $refs
<template>
  <input ref="inputRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)

onMounted(() => {
  inputRef.value.focus()
})
</script>
组合逻辑(Composables)
// useCounter.js
import { ref } from 'vue'
export function useCounter() {
  const count = ref(0)
  const inc = () => count.value++
  const dec = () => count.value--
  return { count, inc, dec }
}
// 组件中使用
const { count, inc, dec } = useCounter()

ref 的高级技巧

✅ 1. 与对象绑定
const person = ref({ name: 'Tom', age: 18 })
person.value.age++ // 响应式更新
✅ 2. 响应式计算
import { ref, computed } from 'vue'
const count = ref(2)
const double = computed(() => count.value * 2)
✅ 3. 监听 ref
import { watch } from 'vue'
watch(count, (newVal, oldVal) => {
  console.log('变化:', oldVal, '→', newVal)
})

核心原理简述

Vue3 内部定义大致如下(简化):

function ref(value) {
  return reactive({ value })
}
  • Vue 自动通过 Proxy 监听 .value
  • 模板中自动 .value 解包;
  • 保持了原始值响应式 + 对象响应式统一接口。

响应式系统

Vue2 在初始化数据时,会通过 Object.defineProperty 把每个属性“拦截”成带 getter/setter 的形式。

组件初始化 → 遍历 data → defineProperty 劫持每个 key
           → getter 收集 watcher
           → setter 触发 watcher.update()

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key)
      return val
    },
    set(newVal) {
      console.log('set', key, newVal)
      val = newVal
    }
  })
}

const data = {}
defineReactive(data, 'count', 0)
data.count // get count
data.count = 1 // set count 1

Vue3 改用 Proxy,一层代理整个对象,无需遍历所有属性。

组件初始化 → reactive 创建 Proxy
           → getter(track) 收集依赖
           → setter(trigger) 触发副作用

const data = { count: 0 }
const p = new Proxy(data, {
  get(target, key, receiver) {
    console.log('get', key)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log('set', key, value)
    const res = Reflect.set(target, key, value, receiver)
    // 触发依赖更新
    return res
  }
})
p.count++ // get + set

Typescript 支持

Vue3 更加方便 早期发现问题成本会降低很多

Vue2(Class/装饰器)

import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class UserCard extends Vue {
  @Prop({ type: String, required: true }) readonly name!: string
  count = 0
  mounted() { /* this.name 类型常OK,但混用mixin易错 */ }
}

Vue3(<script setup> 推荐)

<script setup lang="ts">
const props = defineProps<{ name: string }>()
const emit = defineEmits<{
  (e: 'update:count', v: number): void
}>()

import { ref } from 'vue'
const count = ref(0)
function inc() { count.value++; emit('update:count', count.value) }
</script>

Options API VS Composition API / <script setup>

Vue2(Options API)分开写一块一块的功能

<!-- UserCard.vue -->
<template>
  <div>
    <h3>{{ title }}</h3>
    <p>{{ fullName }}</p>
    <input v-model="keyword" @keyup.enter="search" />
    <button @click="inc">{{ count }}</button>
  </div>
</template>

<script>
export default {
  props: { first: String, last: String },
  data() {
    return { title: 'User', count: 0, keyword: '' }
  },
  computed: {
    fullName() { return `${this.first} ${this.last}` }
  },
  watch: {
    keyword(nv) { console.log('kw:', nv) }
  },
  methods: {
    inc() { this.count++ },
    search() { this.$emit('search', this.keyword) }
  },
  created() { console.log('created') },
  mounted() { console.log('mounted') }
}
</script>

但 Options API 的写法也有几个很严重的问题: 由于所有数据都挂载在 this 之上,因而 Options API 的写法对 TypeScript 的类型推导很不友好,并且这样也不好做 Tree-shaking 清理代码。

新增功能基本都得修改 data、method 等配置,并且代码上 300 行之后,会经常上下反复横跳,开发很痛苦。

代码不好复用,Vue 2 的组件很难抽离通用逻辑,只能使用 mixin,还会带来命名冲突的问题。

Vue3(Composition API,<script setup> 推荐)

<!-- UserCard.vue -->
<template>
  <div>
    <h3>{{ title }}</h3>
    <p>{{ fullName }}</p>
    <input v-model="keyword" @keyup.enter="search" />
    <button @click="inc">{{ count }}</button>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onBeforeMount } from 'vue'

const props = defineProps<{ first: string; last: string }>()
const emit = defineEmits<{ (e: 'search', kw: string): void }>()

const title = ref('User')
const count = ref(0)
const keyword = ref('')

const fullName = computed(() => `${props.first} ${props.last}`)

watch(keyword, (nv) => console.log('kw:', nv))

function inc() { count.value++ }
function search() { emit('search', keyword.value) }

onBeforeMount(() => console.log('created'))
onMounted(() => console.log('mounted'))
</script>

用到的功能都 import 进来,对 Tree-shaking 很友好,我的例子里没用到功能,打包的时候会被清理掉 ,减小包的大小。

不再上下反复横跳,我们可以把一个功能模块的 methods、data 都放在一起书写,维护更轻松。

代码方便复用,可以把一个功能所有的 methods、data 封装在一个独立的函数里,复用代码非常容易。

Composotion API 新增的 return 等语句,在实际项目中使用

新一代工程化工具 Vite

Webpack 是“打包优先” —— 一切先打包再运行;
Vite 是“原生模块优先” —— 借助浏览器原生 ES Module 实现“按需加载”,再用 Rollup 打包生产。

并非是 Vue3 专属,Vite 主要提升的是开发的体验,Webpack 等工程化工具的原理,就是根据你的 import 依赖逻辑,形成一个依赖图,然后调用对应的处理工具,把整个项目打包后,放在内存里再启动调试。

由于要预打包,所以复杂项目的开发,启动调试环境需要 3 分钟都很常见,Vite 就是为了解决这个时间资源的消耗问题出现的。

  • Webpack(bundle-first)启动流程
  [开发者运行 dev][读取配置/插件/Loader][构建完整依赖图][打完整包(或多入口Chunk)][启动 DevServer,提供内存中的 bundle][浏览器加载单/多 bundle][HMR:文件变更][增量重新构建相关 chunk][推送热更新补丁 → 替换模块/触发刷新]

  • Vite(esm-first)启动流程

[开发者运行 dev][秒启本地 Dev Server][依赖预构建(一次性,用 esbuild)][按请求即时编译源码(如 .vue/.ts)][浏览器通过 ESM 逐模块请求][HMR:文件变更][仅编译受影响模块][精准替换该模块(ESM 热替换,无需重打包)]

解决了哪些问题(重要几点)

  1. 响应式缺陷(Vue2 的 getter/setter)
  • 痛点:数组下标/length 变更、对象新增/删除属性不响应,需 Vue.set/delete;深层依赖追踪易漏报。
  • Vue3:基于 Proxy 的响应式系统,天然支持属性新增/删除、数组操作,无需 set/delete,依赖追踪更准确,副作用更可控。
  1. 大型组件与逻辑复用困难(Options + mixins 冲突/命名污染)
  • 痛点:mixin 命名碰撞、来源不清;复杂组件 methods/computed/data 四散,难以围绕“功能”聚合。
  • Vue3:Composition APIsetup/ref/reactive/computed/watch)按“功能切片”组织代码,自定义 hooks(composables) 可复用且可测试、可类型推断,彻底替代大多数 mixins 场景。
  1. 类型与可维护性(TS 体验差)
  • 痛点:Vue2 TS 需要装饰器/额外语法,推断不稳;事件/props 的类型约束不友好。
  • Vue3:内置 TypeScript 一等公民defineComponent/emits/defineProps 等让 props/emit 有完善的类型检查与 IDE 推断。
  1. 性能与包体(编译/运行双端优化不足)
  • 痛点:VNode diff 粗粒度、静态节点重复计算,包不易摇树优化。
  • Vue3:编译器引入 patchFlag/静态提升/事件缓存 等;运行时 更快的 VDOM更好的 Tree-Shaking(按需打包核心模块),同等功能体积更小
  1. 生态与未来
  • 痛点:Vue2 生态逐渐维护最小化,新库偏向 Vue3。
  • Vue3:新生态(如 Naive UI、VueUse、Volar)全面围绕 Vue3/TS 优化,长期演进保障。

🕹️ 让你的Vue项目也能支持虚拟摇杆!一个Canvas虚拟摇杆组件让你的游戏体验飙升

作者 returnfalse
2025年10月24日 10:54

本期给大家分享一个虚拟摇杆的组件,当前版本是基于vue的,由于当年的我需要做线上抓娃娃的功能,之前的同事使用的上下左右四个图片箭头按钮来控制方向,就感觉这种实现效果体验感很不符合用户体验,用户每次点击才能发送一次移动方向消息,所以才去实现这么一个组件,通过游戏摇杆来控制消息发送

💡 组件介绍:一款能在vue项目中直接使用的虚拟摇杆,阅读代码就能替换资源修改自己想要的样式

今天给大家分享一个基于Canvas实现的虚拟摇杆组件,它不只是简单地显示一个可拖拽的圆圈,而是一个功能完整的游戏交互解决方案。它能够:

  • 实时响应:手指移动时立即反馈,无延迟
  • 方向识别:准确识别上、下、左、右及斜向方向
  • 距离计算:提供摇杆偏移距离,可用于控制移动速度
  • 角度测量:精确计算移动角度(0°~360°)
  • 优雅回弹:松开手指时自动回到中心位置

效果演示

9mh15-uhvsg.gif

🚀 实战演示:快速集成到你的项目

安装使用(其实就是复制粘贴)

把下面的代码保存为Joystick.vue,放到你的组件目录:

<template>
  <div
    style="position: relative"
    :style="{ width: opts.josize + 'px', height: opts.josize + 'px' }"
  >
    <div
      class="canvasBox"
      style="width: 100%; height: 100%; bottom: 0; left: 0"
    >
      <canvas
        class="moveCanvas"
        :width="opts.josize"
        :height="opts.josize"
        :style="{ width: opts.josize + 'px', height: opts.josize + 'px' }"
      ></canvas>
    </div>
    <div
      class="move-dom"
      :class="{ active: opts.isStart }"
      @touchstart="moverStart"
      @touchmove="moveMove"
      @touchend="moveEnd"
      @touchcancel="moveEnd"
      @mousemove="moveMove"
      @mousedown="moverStart"
      @mouseup="moveEnd"
    ></div>
  </div>
</template>

<script setup lang="ts">
import { reactive, watch, nextTick } from "vue";
import jPlayBg from "@/assets/images/j_play.png"; // 自动处理路径
import jBg from "@/assets/images/j.png"; // 自动处理路径
const opts = reactive<any>({
  j_bg: "", // 摇杆背景
  j_play_bg: "", // 摇杆按钮图片
  isStart: false, // 是否触摸摇杆
  top: 0, // 操作杆初始位置 top
  left: 0, // 操作杆初始位置 left
  jx: 0,
  jy: 0,
  josize: 140,
  josize_bg: 120,
  jisize: 75,
  centerX: 75,
  centerY: 75,
  effectiveFinger: 0,
  jc: null, // 画板
});
const props = withDefaults(
  defineProps<{
    bl: number;
    isstart?: boolean;
  }>(),
  {
    bl: 100,
    isstart: true,
  }
);
const emit = defineEmits<{
  (e: "getObj", params: any): void;
}>();
watch(
  () => props.isstart,
  (val) => {
    if (val) {
      initFun();
    }
  },
  { immediate: true } // 可选:立即触发
);

watch(
  () => opts.jx,
  (val) => {
    if (val) {
      let distance = Math.ceil(
        Math.sqrt(opts.jx * opts.jx + opts.jy * opts.jy)
      );
      // 判断方位信息
      let obj = {
        angle: "", // 方向
        size: opts.josize_bg,
        distance: distance, // 移动距离
        degrees: getDegrees(opts.jx, opts.jy),
      };
      if (val > 0) {
        // 操作杆在右上、右下
        if (Math.abs(opts.jy) > Math.abs(opts.jx)) {
          // 右边
          if (opts.jy > 0) {
            obj.angle = "down";
          } else {
            obj.angle = "up";
          }
        } else {
          // 正右方
          obj.angle = "right";
        }
      } else if (val <= 0) {
        // 操作杆在左上、左下
        if (Math.abs(opts.jy) > Math.abs(opts.jx)) {
          // 左边
          if (opts.jy > 0) {
            obj.angle = "down";
          } else {
            obj.angle = "up";
          }
        } else {
          // 正左方
          obj.angle = "left";
        }
      }
      throttle(emit("getObj", obj), 100);
    }
  }
);
// 角度转换
function getDegrees(x: number, y: number) {
  // 1. 计算弧度
  const radians = Math.atan2(y, x); // 结果范围:-π 到 π

  // 2. 转换为角度(0°~360°)
  let degrees = radians * (180 / Math.PI);
  if (degrees < 0) degrees += 360; // 将负角度转为正角度
  return degrees.toFixed(4);
}
// 节流函数
function throttle(func: any, wait: number): Function {
  let timeout: ReturnType<typeof setTimeout> | null | undefined;
  return function (
    this: ThisParameterType<any>,
    ...args: Parameters<any>
  ): void {
    timeout && clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}
// 初始化
async function initFun() {
  // 确保DOM已挂载
  await nextTick();
  console.log("初始化");

  opts.j_play_bg = await getImageAsync(jPlayBg);
  opts.j_bg = await getImageAsync(jBg);

  // 初始化尺寸
  let size = Math.floor(props.bl);
  opts.josize = size;
  // 获取canvas ctx实例
  if (!opts.jc) {
    const canvas = document.querySelector(
      ".moveCanvas"
    ) as HTMLCanvasElement | null;
    opts.jc = canvas?.getContext("2d");
  }
  // 初始化摇杆信息(摇杆背景,摇杆移动按钮,摇杆中心位置等)
  await initCanvasRect();

  requestAnimationFrame(move); //开始绘图
}
// 获取canvas的位置
async function initCanvasRect() {
  const rect = await getElSize(".canvasBox");
  opts.top = rect.top || 0;
  opts.left = rect.left || 0;
  opts.jisize = opts.josize * 0.35;
  opts.josize_bg = opts.josize * 0.8;
  opts.centerX = opts.josize / 2; //摇杆中心x坐标
  opts.centerY = opts.josize / 2; //摇杆中心y坐标
  return Promise.resolve();
}
// 开始绘制
//绘图函数(绘制图形的时候就是用户观察到摇杆动了,所以取名是move)
function move() {
  opts.jc?.clearRect(
    opts.centerX - opts.josize / 2,
    opts.centerY - opts.josize / 2,
    opts.josize,
    opts.josize
  ); //清空画板

  opts.jc?.drawImage(
    opts.j_bg,
    (opts.josize - opts.josize_bg) / 2,
    (opts.josize - opts.josize_bg) / 2,
    opts.josize_bg,
    opts.josize_bg
  ); //画底座
  opts.jc?.drawImage(
    opts.j_play_bg,
    opts.centerX - opts.jisize / 2 + opts.jx,
    opts.centerY - opts.jisize / 2 + opts.jy,
    opts.jisize,
    opts.jisize
  ); //画摇杆头
  requestAnimationFrame(move); //开始绘图
}
// 获取元素信息
function getElSize(el: any): Promise<any> {
  return new Promise((resolve) => {
    const element = document.querySelector(el);
    const rect = element.getBoundingClientRect();
    resolve(rect);
  });
}
// 异步加载图片
function getImageAsync(url: string | undefined): Promise<any> {
  return new Promise((resolve, reject) => {
    if (!url) return reject();
    let image = new Image();
    image.src = url;
    image.onload = () => {
      resolve(image);
    };
  });
}
// 触摸开始
async function moverStart(event: MouseEvent | TouchEvent) {
  event.preventDefault();
  await initCanvasRect();
  let cX =
    "touches" in event
      ? event.touches[opts.effectiveFinger].clientX
      : event.clientX;
  let cY =
    "touches" in event
      ? event.touches[opts.effectiveFinger].clientY
      : event.clientY;
  let clientX = cX - opts.left;
  let clientY = cY - opts.top;
  if (
    clientX > 0 &&
    clientX < opts.josize &&
    clientY > 0 &&
    clientY < opts.josize
  ) {
    opts.isStart = true;
  } else {
    // 不符合条件
    // console.log('不符合条件不能移动',clientX,clientY,opts.josize);
    return;
  }
  //是否触摸点在摇杆上
  if (
    Math.sqrt(
      Math.pow(clientX - opts.centerX, 2) + Math.pow(clientY - opts.centerY, 2)
    ) <=
    opts.josize / 2 - opts.jisize / 2
  ) {
    opts.jx = clientX - opts.centerX;
    opts.jy = clientY - opts.centerY;
  }
  //否则计算摇杆最接近的位置
  else {
    var x = clientX,
      y = clientY,
      r = opts.josize / 2 - opts.jisize / 2;
    var ans = getPoint(
      opts.centerX,
      opts.centerY,
      r,
      opts.centerX,
      opts.centerY,
      x,
      y
    );
    //圆与直线有两个交点,计算出离手指最近的交点
    if (
      Math.sqrt((ans[0] - x) * (ans[0] - x) + (ans[1] - y) * (ans[1] - y)) <
      Math.sqrt((ans[2] - x) * (ans[2] - x) + (ans[3] - y) * (ans[3] - y))
    ) {
      opts.jx = ans[0] - opts.centerX;
      opts.jy = ans[1] - opts.centerY;
    } else {
      opts.jx = ans[2] - opts.centerX;
      opts.jy = ans[3] - opts.centerY;
    }
  }
}
// 移动中
function moveMove(event: TouchEvent | MouseEvent) {
  if (!opts.isStart) {
    // 首次触摸点未在操作杆上 停止运行
    return;
  }
  let cX =
    "touches" in event
      ? event.touches[opts.effectiveFinger].clientX
      : event.clientX;
  let cY =
    "touches" in event
      ? event.touches[opts.effectiveFinger].clientY
      : event.clientY;
  let clientX = cX - opts.left;
  let clientY = cY - opts.top;
  //是否触摸点在摇杆上
  if (
    Math.sqrt(
      Math.pow(clientX - opts.centerX, 2) + Math.pow(clientY - opts.centerY, 2)
    ) <=
    opts.josize / 2 - opts.jisize / 2
  ) {
    opts.jx = clientX - opts.centerX;
    opts.jy = clientY - opts.centerY;
  }
  //否则计算摇杆最接近的位置
  else {
    var x = clientX,
      y = clientY,
      r = opts.josize / 2 - opts.jisize / 2;

    var ans = getPoint(
      opts.centerX,
      opts.centerY,
      r,
      opts.centerX,
      opts.centerY,
      x,
      y
    );
    //圆与直线有两个交点,计算出离手指最近的交点
    if (
      Math.sqrt((ans[0] - x) * (ans[0] - x) + (ans[1] - y) * (ans[1] - y)) <
      Math.sqrt((ans[2] - x) * (ans[2] - x) + (ans[3] - y) * (ans[3] - y))
    ) {
      opts.jx = ans[0] - opts.centerX;
      opts.jy = ans[1] - opts.centerY;
    } else {
      opts.jx = ans[2] - opts.centerX;
      opts.jy = ans[3] - opts.centerY;
    }
  }
}
// 触摸结束
function moveEnd() {
  //若手指离开,那就把内摇杆放中间
  opts.jx = 0;
  opts.jy = 0;
  opts.isStart = false;
  emit("getObj", {
    isStop: 1,
  });
}
//计算圆于直线的交点
function getPoint(
  cx: number,
  cy: number,
  r: number,
  stx: number,
  sty: number,
  edx: number,
  edy: number
) {
  var k = (edy - sty) / (edx - stx); // 触碰位置 xy 与圆半径的差之后的比例 也就是圆心距离手指触碰y与x的比例
  var b = edy - k * edx; // 手指触摸的位置 减去 比例 乘以手指触摸的x位置
  var x1, y1, x2, y2; //定义坐标点
  var c = cx * cx + (b - cy) * (b - cy) - r * r; // 圆心坐标相乘 加上
  var a = 1 + k * k;
  var b1 = 2 * cx - 2 * k * (b - cy);
  var tmp = Math.sqrt(b1 * b1 - 4 * a * c);
  x1 = (b1 + tmp) / (2 * a);
  y1 = k * x1 + b;
  x2 = (b1 - tmp) / (2 * a);
  y2 = k * x2 + b;
  return [x1, y1, x2, y2];
}
</script>

<style lang="scss" scoped>
.move-dom {
  width: 100%;
  height: 100%;
  z-index: 100;
  position: absolute;
  top: 0;
  left: 0;
}
.move-dom.active {
  width: 100vw;
  height: 100vh;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
</style>

使用示例

<template>
  <div class="game-container">
    <!-- 其他游戏元素 -->
    <div class="game-info">
      <p>当前方向: {{ direction }}</p>
      <p>移动距离: {{ distance }}</p>
      <p>移动角度: {{ angle }}°</p>
    </div>

    <!-- 虚拟摇杆,放在屏幕左下角 -->
    <div class="joystick-container">
      <Joystick
        :bl="120"
        @getObj="handleJoystickData"
        :isstart="true"
      />
    </div>
  </div>
</template>

<script setup>
import Joystick from '@/components/Joystick/Joystick.vue'
import { ref } from 'vue'

const direction = ref('停止')
const distance = ref(0)
const angle = ref(0)

const handleJoystickData = (data) => {
  if(data.isStop) {
    direction.value = '停止'
    distance.value = 0
    angle.value = 0
    console.log('摇杆松开')
  } else {
    direction.value = data.angle
    distance.value = data.distance
    angle.value = data.degrees
    console.log('摇杆数据:', data)

    // 在这里可以发送数据给游戏逻辑
    // 控制角色移动
    moveCharacter(data.angle, data.distance)
  }
}

// 游戏中的角色移动逻辑
const moveCharacter = (direction, distance) => {
  // 根据方向和距离移动角色
  console.log(`角色向${direction}方向移动,距离${distance}`)
}
</script>

<style scoped>
.game-container {
  width: 100vw;
  height: 100vh;
  background: #000;
  position: relative;
  overflow: hidden;
}

.game-info {
  position: absolute;
  top: 20px;
  left: 20px;
  color: white;
  z-index: 10;
}

.joystick-container {
  position: absolute;
  bottom: 20px;
  left: 20px;
  z-index: 10;
}
</style>

需要自行更换 import jPlayBg from "@/assets/images/j_play.png"; // 自动处理路径 import jBg from "@/assets/images/j.png"; // 自动处理路径 组件中这两个图片资源地址

🎯 应用场景:你的游戏开发利器

1. 🏎️ 赛车游戏

  • 控制车辆转向
  • 实时反馈方向盘角度

2. 🎮 射击游戏

  • 控制角色移动
  • 精确瞄准方向

3. 🤖 机器人遥控

  • 遥控设备移动方向
  • 实时位置反馈

4. 🎯 VR/AR应用

  • 3D场景导航
  • 视角控制

🌟 为什么选择这个组件?

  • 性能优异:使用Canvas绘制,不占用DOM资源
  • 易于集成:简单配置即可使用
  • 功能完整:方向、距离、角度一应俱全
  • 跨平台:同时支持触摸和鼠标操作
  • 动画流畅:使用requestAnimationFrame优化性能

💖 如果你觉得这个组件好用...

点赞、收藏、分享给需要的朋友!如果你在使用过程中遇到问题,欢迎在评论区交流讨论。

记住,一个优秀的交互组件能让用户的游戏体验提升一个档次,而这个虚拟摇杆组件正是你游戏开发路上的得力助手!

💖 如果你还想知道如何使用同款组件在uni-app项目, 或者原生小程序项目中也能用这个丝滑的组件,可以评论留言,或者想使用我演示项目的图片资源,也可以留言

vue在页面退出前别忘记做好这些清理工作

作者 colorFocus
2025年10月24日 10:54

最近在开发中遇到一个典型问题:页面需要通过轮询接口更新数据,但测试时发现网络请求异常频繁,甚至在页面切换后仍有旧请求持续触发。排查后发现,虽然在 onBeforeUnmount 中清除了定时器,但未取消已发出的接口请求 —— 这些请求完成后仍会执行回调逻辑,重新启动轮询,导致 “幽灵请求” 不断产生。

这个问题的根源在于:页面退出时的清理操作不彻底。本文将系统梳理页面卸载前必须执行的清理工作,以避免内存泄漏和不必要的资源消耗。

一、清除定时器/计时器

若组件中使用了 setTimeoutsetInterval,即使组件已卸载,未清除的定时器仍会占用内存并持续执行。

<script setup>
let timer = null;

onMounted(() => {
  // 启动定时器
  timer = setInterval(() => {
    console.log('执行定时任务');
  }, 1000);
});

onBeforeUnmount(() => {
  // 组件销毁时清除
  clearInterval(timer);
});
</script>

二、取消未完成的网络请求

对于一般页面中的交互请求,接口返回都非常快,因此在退出页面时不需要特别对这些接口进行取消操作。 但是对于一些返回比较慢的接口,尤其是跟上面说的和定时器轮询配合调用的接口,则需要在页面退出的时候进行手动的取消。 这是因为Promise一旦新建它就会立即执行,无法中途取消。也就是说当一个异步请求发出的时候,就不能中途取消了,不像setTimeout可以随时通过clearTimeout清除掉计时器。这也是为什么当页面中有网络请求发出时,即使页面关闭,页面对象被销毁,接口仍然会继续执行,并且接口返回后的逻辑也会被执行。所以对于这种情况需要我们手动处理,页面销毁的时候进行接口的取消操作。

<script setup>
import axios from 'axios';
const CancelTokenSource = axios.CancelToken.source();

let timer = null;
const data = ref();

function startPollFunc(shouldPoll) {
  const pollFunc = async () => {
    const res = await axios.get('/api/polling-data', {
      cancelToken: CancelTokenSource.token
    });
    data.value = res.data;
    if (shouldPoll) {
      timer = setTimeout(pollFunc, 5000);
    }
  }

  // 初始调用
  pollFunc();
}

onMounted(() => {
  startPollFunc(true);
});

onBeforeUnmount(() => {
  // 清除定时器
  clearTimeout(timer);
  // 取消接口
  CancelTokenSource.cancel();
});
</script>

在页面退出时除了需要注意这种普通接口的取消,对于websocket这种网络请求,也不要忘记关闭。

<script setup>
let ws = null;

onMounted(() => {
  ws = new WebSocket('wss://example.com/chat');
  ws.onmessage = (e) => { /* 处理消息 */ };
});

onBeforeUnmount(() => {
  if (ws) {
    ws.close(1000, '页面卸载'); // 1000表示正常关闭
  }
});
</script>

三、解绑全局事件监听

Vue 模板中通过 v-on 绑定的事件会随组件销毁自动解绑。但通过 window/document 绑定的全局事件(如 scrollresizekeydown)必须手动解绑,否则会持续触发。

<script setup>
const handleScroll = () => {
  console.log('页面滚动了');
};

onMounted(() => {
  window.addEventListener('scroll', handleScroll);
});

onBeforeUnmount(() => {
  // 解绑事件(必须使用同一个函数引用)
  window.removeEventListener('scroll', handleScroll);
});
</script>

四、清理自定义事件总线

在 Vue 开发中,当组件间需要跨层级或非父子关系通信时,常会用到全局事件总线(如基于 mitt实现的 evtBus)。若使用了事件总线,需要在组件销毁时移除注册的事件,避免事件重复触发,引发逻辑错乱。这种情况与 “全局事件监听” 的清理逻辑本质一致。

<script setup>
import mitt from 'mitt'

const emitter = mitt();

const handleEvent = () => { /* 处理事件 */ };

onMounted(() => {
  emitter.on('customEvent', handleEvent);
});

onBeforeUnmount(() => {
  emitter.off('customEvent', handleEvent); // 移除事件
});
</script>

五、第三方插件对象的销毁

调用第三方插件(如Echarts、富文本编辑器)创建的对象,若不手动销毁,这些资源不会随组件卸载自动释放,导致内存占用持续增长。

<template>
  <!-- 图表容器 -->
  <div class="chart-container" ref="chartRef"></div>
</template>

<script setup>
import * as echarts from 'echarts'; // 引入 ECharts

// 图表容器 DOM 引用
const chartRef = ref(null);
// ECharts 实例对象
let chartInstance = null;
// 移除事件的句柄
let removeResizeListener;

// 初始化图表
const initChart = () => {
  // 1. 创建 ECharts 实例(绑定到容器)
  chartInstance = echarts.init(chartRef.value);

  // 2. 配置图表选项(示例:折线图)
  const option = {
    ......
  };

  // 3. 设置图表选项
  chartInstance.setOption(option);

  // 4. 监听窗口大小变化,自动调整图表(可选)
  const handleResize = () => {
    chartInstance.resize();
  };
  window.addEventListener('resize', handleResize);

  // 返回清理函数(用于后续解绑事件)
  return () => {
    window.removeEventListener('resize', handleResize);
  };
};

onMounted(() => {
  if (chartRef.value) {
    removeResizeListener = initChart();
  }
});

// 组件即将卸载时销毁图表
onBeforeUnmount(() => {
  // 1. 解绑窗口大小监听(避免内存泄漏)
  if (removeResizeListener) {
    removeResizeListener();
  }

  // 2. 销毁 ECharts 实例
  if (chartInstance) {
    chartInstance.dispose(); // 调用 ECharts 内置销毁方法
  }
});
</script>

清理操作的最佳时机

vue在组件卸载的时候有两个生命周期,一个是onBeforeUnmount,一个是onUnmounted。那么上面说的清理操作在哪个生命周期中执行好呢? 我们来看下这两个生命周期的具体区别。

两者的核心区别在于执行时机。

  • onBeforeUnmount:组件即将被卸载,但DOM 仍未销毁,组件实例和数据依然可用。此时可以安全地访问组件内的变量(如定时器 ID、请求令牌),执行清理操作。
  • onUnmounted:组件已经被卸载,DOM 已销毁,组件实例开始被回收。虽然大部分情况下仍能访问变量,但存在潜在风险(如变量引用已被释放)。

通过对比两者的执行时机的区别,为了确保清理操作的可靠性,我会推荐在onBeforeUnmount生命周期阶段执行。在该阶段,轮询和接口请求依赖的组件内的变量引用绝对有效,清理操作能100%生效。

总结

vue页面退出或者组件销毁时清理需遵循 “谁创建,谁销毁;谁监听,谁解绑”的原则 ,主要包括以下场景:

  1. 清除定时器/计时器;
  2. 取消未完成的请求、关闭连接(WebSocket);
  3. 解绑全局事件、自定义事件;
  4. 销毁第三方插件实例;

第四章、路由配置

2025年10月24日 10:54

路由配置

一、安装依赖

npm install react-router-dom

二、推荐目录结构

src/
├── router/
│   └── index.tsx             # 路由配置文件
├── pages/
│   ├── Home.tsx
│   ├── About/
│   │   ├── index.tsx          
│   │   ├── LanguageI18n.tsx        
│   │   └── ThemeSwitcher.tsx           
│   ├── User/
│   │   ├── UserList.tsx
│   │   └── UserDetail.tsx
├── layout/
│   └── MainLayout.tsx        # 公共布局
├── App.tsx
└── index.tsx

三、基础路由配置(src/router/index.tsx

import React from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

import MainLayout from "@/layout/MainLayout";
import Home from "@/pages/Home";
import About from "@/pages/About/About";
import Company from "@/pages/About/LanguageI18n";
import Team from "@/pages/About/ThemeSwitcher";
import UserList from "@/pages/User/UserList";
import UserDetail from "@/pages/User/UserDetail";

const router = createBrowserRouter([
  {
    path: "/",
    element: <MainLayout />,// 公共布局
    children: [
      { index: true, element: <Home /> },// 默认路由
      {
        path: "about",
        element: <About />,
        children: [
          { index: true, element: <Navigate to="languageI18n" replace /> },//设置默认子路由
          { path: "languageI18n", element: <LanguageI18n /> },
          { path: "themeSwitcher", element: <ThemeSwitcher /> },
        ],
      },
      { path: "users", element: <UserList /> },
      { path: "users/:id", element: <UserDetail /> },// 动态参数
    ],
  },
]);

export default function AppRouter() {
  return <RouterProvider router={router} />;
}

四、在入口文件中加载路由

src/index.tsx

import React from "react";
import { createRoot } from "react-dom/client";
import "./styles/globals.css";
import "./index.css";
import { ThemeProvider } from "./context/ThemeContext";
import "./i18n";
import AppRouter from "./router";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
      <ThemeProvider>
        <AppRouter />
     </ThemeProvider>
  </React.StrictMode>
);

五、导航与跳转示例

src/pages/Home.tsx

import React from "react";
import { Link, useNavigate } from "react-router-dom";

const Home: React.FC = () => {
  const navigate = useNavigate();

  const goToUser = () => {
    navigate("/users/42");
  };

  return (
    <div>
      <h1>🏠 Home</h1>
      <p>欢迎来到首页</p>
      <Link to="/about">去关于页</Link>
      <br />
      <button onClick={goToUser}>跳转到用户详情</button>
    </div>
  );
};

export default Home;

六、动态路由参数获取

src/pages/User/UserDetail.tsx

import React from "react";
import { useParams } from "react-router-dom";

const UserDetail: React.FC = () => {
  const { id } = useParams<{ id: string }>();

  return (
    <div>
      <h2>用户详情页</h2>
      <p>当前用户ID: {id}</p>
    </div>
  );
};

export default UserDetail;

七、嵌套路由(src/pages/About/index.tsx

index.tsx 作为父级页面,需要提供导航入口,并包含一个 <Outlet /> 来渲染子页面内容。

import React from "react";
import { Link, Outlet } from "react-router-dom";

const About: React.FC = () => {
  return (
    <div>
      <h1>关于</h1>
      <nav style={{ marginBottom: "1rem" }}>
          <Link to="languageI18n">国际化切换</Link> | <Link to="themeSwitcher">主题切换</Link>
      </nav>

      {/* 子路由出口 */}
      <Outlet />
    </div>
  );
};

export default About;

九、子页面

src/pages/About/LanguageI18n.tsx

import LanguageSwitcher from "@/components/LanguageSwitcher";
import React from "react";
import { useTranslation } from "react-i18next";

const LanguageI18n: React.FC = () => {
  const { t } = useTranslation();
  return (
    <div className="p-8">
        <h1>{t("welcome")}</h1>
        <p>
            {t("language")}: {t("change_language")}
        </p>
        <LanguageSwitcher />
      </div>
  );
};

export default LanguageI18n;

src/pages/About/ThemeSwitcher.tsx

import { useTheme } from "@/context/ThemeContext";
import React from "react";

const ThemeSwitcher: React.FC = () => {
  const { mode, theme, setMode, toggleTheme } = useTheme();
  return (
    <div style={{ padding: "2rem" }}>
      <h1>🌗 React 三种主题模式</h1>
      <p>当前模式:{mode}</p>
      <p>当前实际主题:{theme}</p>

      <div style={{ display: "flex", gap: "10px", marginBottom: "20px" }}>
        <button onClick={() => setMode("light")}>亮色模式</button>
        <button onClick={() => setMode("dark")}>暗色模式</button>
        <button onClick={() => setMode("system")}>跟随系统</button>
        <button onClick={toggleTheme}>手动切换主题</button>
      </div>
      <p>示例文字会根据主题自动变色。</p>
    </div>
  );
};

export default ThemeSwitcher;

十、路由懒加载(代码分割)

一、为什么要用懒加载?

推荐:React.lazy + Suspense 在开发中,React 项目文件越来越大,如果所有页面在首次加载时都被打包下载,会导致:

  • 首屏加载慢
  • 打包体积大
  • 用户等待时间长

解决方案: 按需加载(代码分割)
只有当用户访问某个路由页面时,才异步加载该页面代码。

这就是 React.lazy + Suspense 的核心功能。

二、改造后的 src/router/index.tsx

在(src/router/index.tsx)中

import React, { Suspense, lazy } from "react";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

// 使用 React.lazy 动态导入组件
const MainLayout = lazy(() => import("@/layout/MainLayout"));
const Home = lazy(() => import("@/pages/Home"));
const About = lazy(() => import("@/pages/About"));
const LanguageI18n = lazy(() => import("@/pages/About/languageI18n"));
const ThemeSwitcher = lazy(() => import("@/pages/About/ThemeSwitcher"));
const UserList = lazy(() => import("@/pages/User/UserList"));
const UserDetail = lazy(() => import("@/pages/User/UserDetail"));

const router = createBrowserRouter([
  {
    path: "/",
    element: <MainLayout />,// 公共布局
    children: [
      { index: true, element: <Home /> },// 默认路由
      {
        path: "about",
        element: <About />,
        children: [
          { index: true, element: <Navigate to="languageI18n" replace /> },//设置默认子路由
          { path: "languageI18n", element: <LanguageI18n /> },
          { path: "themeSwitcher", element: <ThemeSwitcher /> },
        ],
      },
      { path: "users", element: <UserList /> },
      { path: "users/:id", element: <UserDetail /> },// 动态参数
    ],
  },
]);

export default function AppRouter() {
  return <RouterProvider router={router} />;
}

三、全局 Suspense + Loading 动画

(1)Suspense的作用:

<Suspense fallback={<div>加载中...</div>}>
  <About />
</Suspense>

  • Suspense 是一个占位组件
  • 当内部的 lazy 组件正在异步加载时,React 会渲染 fallback(占位内容);
  • 加载完成后,React 会自动替换为真实组件。
  • 这使得用户体验更加平滑(不会白屏)。

通俗理解:

“页面在加载时显示一段提示(比如‘加载中…’),加载完再显示真正的页面。”

(2) 全局 Suspense + 优雅 Loading 动画 的懒加载版本。

src/
├── router/
│   └── index.tsx             # 路由配置
├── components/
│   └── Loading.tsx           # 全局加载动画
├── layout/
│   └── MainLayout.tsx
└── index.tsx

全局加载动画组件src/components/Loading.tsx

import React from "react";

const Loading: React.FC = () => {
  return (
    <div
      style={{
        height: "100vh",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "column",
        fontSize: "18px",
        color: "#555",
      }}
    >
      <div
        style={{
          width: 40,
          height: 40,
          border: "4px solid #ccc",
          borderTopColor: "#4f46e5",
          borderRadius: "50%",
          animation: "spin 1s linear infinite",
        }}
      ></div>
      <p style={{ marginTop: 10 }}>页面加载中...</p>

      <style>
        {`@keyframes spin { 
            from { transform: rotate(0deg); } 
            to { transform: rotate(360deg); } 
          }`}
      </style>
    </div>
  );
};

export default Loading;

src/router/index.tsx

import Loading from "@/components/Loading";
//...

// 用 Suspense 包裹整个路由系统
const AppRouter: React.FC = () => {
  return (
    <Suspense fallback={<Loading />}>
      <RouterProvider router={router} />
    </Suspense>
  );
};

export default AppRouter;

全局 Suspense 的优势

✅ 只写一次 fallback,所有懒加载页面共用
✅ 不需要在每个 <Route><Suspense>
✅ 结构更清晰,可直接替换为统一的动画或骨架屏

十一、其他问题

1.别名使用

import MainLayout from "@/layout/MainLayout"; 时却报错。 这个问题通常是因为 TypeScript 编译器无法识别 Webpack 的别名设置。Webpack 在打包时会处理这些别名,但 TypeScript 在编译时也需要知道这些别名。需要同时在 Webpack 和 TypeScript 中配置别名。

修改 tsconfig.json

{
  "compilerOptions": {
   ...
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}

完善 Webpack 配置webpack.common.js

  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
    alias: {
      '@': path.resolve(__dirname, '../src'),
    },
  },

注意补充完后,重启一下

2.React Router DOM 最常用方法和组件表格

核心组件
组件 用途 示例 使用频率
BrowserRouter 应用根路由容器 <BrowserRouter><App /></BrowserRouter> ⭐⭐⭐⭐⭐
Routes 路由配置容器 <Routes><Route ... /></Routes> ⭐⭐⭐⭐⭐
Route 定义单个路由 <Route path="/" element={<Home />} /> ⭐⭐⭐⭐⭐
Link 声明式导航链接 <Link to="/about">关于</Link> ⭐⭐⭐⭐⭐
Outlet 嵌套路由渲染位置 <Outlet /> ⭐⭐⭐⭐
NavLink 带激活状态的链接 <NavLink to="/" className={({isActive}) => ...}> ⭐⭐⭐
常用 Hooks
Hook 用途 示例 使用频率
useNavigate 编程式导航 const navigate = useNavigate(); navigate('/path') ⭐⭐⭐⭐⭐
useParams 获取路由参数 const { id } = useParams(); ⭐⭐⭐⭐⭐
useLocation 获取当前位置信息 const location = useLocation(); ⭐⭐⭐⭐
useSearchParams 处理URL查询参数 const [params, setParams] = useSearchParams(); ⭐⭐⭐⭐
useRoutes 对象形式定义路由 const routes = useRoutes([...]); ⭐⭐⭐
导航组件对比
特性 Link NavLink useNavigate
类型 声明式 声明式 编程式
激活状态
使用场景 普通链接 导航菜单 事件处理、表单提交
参数获取对比
场景 使用 Hook 示例
路径参数 useParams /user/:id → const {id} = useParams()
查询参数 useSearchParams ?page=1 → const [params] = useSearchParams()
状态传递 useLocation navigate('/path', {state: {data}})
路由配置及菜单数据

(1)分清楚路由配置的数据和菜单导航的渲染数据 image.png 在菜单渲染数据里子菜单漏写父路径会导致404

image.png

(2)<Outlet />子路由出口

src/pages/About/index.tsximage.png

image.png

MainLayout.tsx

首页、关于、用户等都是子路由 image.png

image.png 所以MainLayout.tsx是公共布局,主布局页面

4sapi生成式 AI 驱动下的智能聊天机器人

作者 星链引擎
2025年10月24日 10:45

生成式 AI 驱动下的智能聊天机器人:技术深耕、工程实践与行业价值升维

一、技术演进:从工具到生态的范式跃迁

在人工智能技术迈向产业化深耕的今天,智能聊天机器人已完成从 “单一交互工具” 到 “企业数字化核心枢纽” 的进阶,深度融入客户服务、数字营销、智能教育、政企办公等关键领域,成为重构人机交互模式、提升组织运营效率的核心力量。生成式 AI 技术的突破性发展,叠加 OpenAI 等顶尖机构提供的高性能模型 API,与 New API 平台构建的 “高可用、低延迟、强适配” 企业级服务生态形成协同效应,彻底解决了传统聊天机器人在语义理解深度、交互自然度、部署稳定性上的痛点,为开发者提供了 “开箱即用” 的技术底座,加速了智能交互能力向各行业的渗透与落地。

二、核心架构:模型、接口与工程的协同赋能

智能聊天机器人的核心竞争力,源于 “模型能力 - 接口支撑 - 工程实现” 三位一体的架构设计,其技术内核可拆解为两大关键维度:

(一)模型层:NLP 技术的生成式革新

自然语言处理(NLP)技术的范式升级,是智能聊天机器人实现 “类人交互” 的核心基础。以 GPT-3 为代表的大规模预训练模型,通过万亿级文本数据的无监督学习,构建了复杂的语义理解与生成逻辑 —— 不仅能精准识别用户意图、解析上下文语境,更能生成符合语言习惯、贴合场景需求的自然回复,突破了传统规则式机器人 “机械应答” 的局限。这种 “预训练 + 微调” 的技术路径,让机器人具备了跨场景适配能力,可快速响应不同行业的专业需求。

(二)接口层:企业级服务的稳定性保障

New API 平台的核心价值,在于为模型能力落地提供了 “企业级工程支撑”。通过分布式节点部署、智能路由调度、多链路冗余备份等技术方案,平台实现了 API 调用的 “毫秒级响应 + 99.99% 可用性” 保障,有效解决了跨境调用延迟、峰值流量拥堵、服务中断等行业痛点。同时,其标准化的接口设计与灵活的权限管控机制,让开发者无需投入大量资源进行基础设施搭建、网络优化与运维监控,可聚焦于业务场景适配、对话逻辑优化等核心价值环节,实现技术能力向商业价值的高效转化。

三、工程化实践:企业级聊天机器人落地指南

以下为融合 “稳定性、可扩展性、可观测性” 的企业级智能聊天机器人实现方案,代码已完成国内网络环境适配、工程化配置管理与异常容错设计,可直接集成至企业现有业务系统,兼顾开发效率与生产环境要求:

python

运行

import openai
import logging
from typing import Dict, Optional, Union
from configparser import ConfigParser
from tenacity import retry, stop_after_attempt, wait_exponential

# 企业级日志配置:支撑问题排查与服务可观测性
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(module)s - %(levelname)s - %(message)s",
    handlers=[
        logging.FileHandler("chatbot_enterprise.log", encoding="utf-8"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# 配置文件管理:支持多环境(开发/测试/生产)灵活切换
config = ConfigParser()
config.read("chatbot_config.ini", encoding="utf-8")
api_settings = config["API_SETTINGS"]
model_settings = config["MODEL_SETTINGS"]

# 企业级客户端初始化:整合高可用与安全性设计
client = openai.OpenAI(
    base_url=api_settings.get("BASE_URL", "https://4sapi.com"),
    api_key=api_settings.get("API_KEY"),
    timeout=int(api_settings.get("TIMEOUT", 30)),
    max_retries=int(api_settings.get("MAX_RETRIES", 2))
)

@retry(
    stop=stop_after_attempt(int(api_settings.get("RETRY_ATTEMPTS", 3))),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry_error_callback=lambda retry_state: logger.error(f"API调用重试失败: {retry_state.outcome.exception()}")
)
def enterprise_chatbot(
    user_prompt: str,
    context: Optional[Dict] = None,
    model: str = model_settings.get("DEFAULT_MODEL", "davinci"),
    max_tokens: int = int(model_settings.get("MAX_TOKENS", 150)),
    temperature: float = float(model_settings.get("TEMPERATURE", 0.7)),
    top_p: float = float(model_settings.get("TOP_P", 0.9))
) -> Optional[str]:
    """
    企业级智能对话核心函数:支持上下文管理、参数化配置与容错重试
    :param user_prompt: 用户当前输入内容
    :param context: 对话上下文(含历史交互记录),支持多轮对话扩展
    :param model: 模型选型(适配不同业务复杂度与成本需求)
    :param max_tokens: 响应长度限制,平衡效率与信息完整性
    :param temperature: 生成随机性调节(0.0-1.0),低则严谨,高则灵活
    :param top_p: 核采样参数,控制生成内容的多样性
    :return: 结构化响应结果,异常时返回None
    """
    # 上下文拼接:实现多轮对话的连贯性
    full_prompt = ""
    if context and "history" in context:
        for msg in context["history"]:
            full_prompt += f"用户:{msg['user']}\n助手:{msg['assistant']}\n"
    full_prompt += f"用户:{user_prompt}\n助手:"

    try:
        response = client.Completion.create(
            engine=model,
            prompt=full_prompt,
            max_tokens=max_tokens,
            temperature=temperature,
            top_p=top_p,
            stop=["用户:"]  # 避免生成多余的用户话术
        )
        result = response.choices[0].text.strip()
        logger.info(f"对话完成 - 用户输入:{user_prompt} | 助手响应:{result}")
        return result
    except Exception as e:
        logger.error(f"API调用异常:{str(e)},用户输入:{user_prompt}")
        return None

# 多场景业务适配示例
if __name__ == "__main__":
    # 场景1:金融行业咨询(带上下文多轮对话)
    finance_context = {
        "history": [
            {"user": "我想了解企业贷款的基本要求", "assistant": "企业贷款通常需满足注册年限、营收规模、征信状况等条件"}
        ]
    }
    user_input_1 = "中型制造企业的贷款额度一般是多少?"
    response_1 = enterprise_chatbot(user_input_1, finance_context, temperature=0.5)
    print(f"金融智能助手:{response_1 or '服务临时不可用,建议稍后重试'}")

    # 场景2:教育行业答疑(单轮精准响应)
    user_input_2 = "请解释机器学习中监督学习与无监督学习的核心区别"
    response_2 = enterprise_chatbot(user_input_2, model="gpt-3.5-turbo", max_tokens=200)
    print(f"教育智能助手:{response_2 or '服务临时不可用,建议稍后重试'}")

四、核心代码架构解析

  1. 工程化配置体系:通过配置文件分离 API 参数、模型参数与环境变量,支持开发、测试、生产多环境快速切换,降低配置变更成本,符合企业级项目的可维护性要求。
  2. 高可用设计:整合重试机制、超时控制、多链路备份等容错方案,搭配日志监控系统,实现服务异常的快速定位与自动恢复,保障生产环境的稳定性。
  3. 多轮对话能力:引入上下文管理机制,支持拼接历史交互记录,让机器人具备 “记忆能力”,实现更连贯、自然的多轮对话,适配复杂业务场景需求。
  4. 参数化灵活适配:支持模型选型、生成温度、响应长度等多维度参数配置,可根据业务场景(如严谨的金融咨询、灵活的营销互动)进行精细化调整,兼顾专业性与交互体验。

五、场景深耕:行业数字化转型的赋能矩阵

智能聊天机器人已不再是简单的 “应答工具”,而是深度融入行业业务流程、创造核心价值的 “数字化助手”,其高端应用场景可概括为四大矩阵:

(一)智能客服中枢:降本增效与体验升级

整合官网、APP、小程序、社交媒体等全渠道入口,构建 7×24 小时智能客服体系 —— 针对高频咨询(如订单查询、业务办理、故障排查)实现秒级响应,复杂问题智能转人工并同步历史上下文,降低企业客服人力成本 30%-60%,同时将客户等待时长缩短 50% 以上,客户满意度提升 25%-40%。典型应用:银行智能客诉处理、运营商业务咨询、电商售后维权。

(二)数字化营销引擎:精准触达与转化提升

基于用户交互数据构建全域用户标签体系,通过场景化对话挖掘用户潜在需求,实现 “千人千面” 的产品推荐、优惠推送与需求引导。同时支持营销活动自动化执行(如新品介绍、活动报名、问卷调研),将营销转化率提升 2-3 倍,成为企业私域流量运营与增长的核心动力。典型应用:零售行业精准营销、教育机构课程推荐、 SaaS 产品获客转化。

(三)自适应学习平台:个性化教育赋能

作为教育场景的智能学习伙伴,可基于学生学习进度、知识薄弱点生成个性化学习路径,提供知识点拆解、习题答疑、学习资源推荐、作业批改等服务。同时支持多学科覆盖与自适应难度调整,构建 “千人千面” 的学习生态,助力教育资源均衡化与学习效率提升。典型应用:K12 智能辅导、职业教育技能培训、成人继续教育答疑。

(四)企业协同办公助手:效率提升与流程优化

集成至企业 OA、CRM、ERP 等核心系统,提供会议纪要生成、工作流程咨询、文档检索、任务提醒、跨部门协同沟通等功能,打破信息孤岛,降低沟通成本,将员工办公效率提升 30% 以上。典型应用:大型企业跨部门协作、初创公司行政事务自动化、政企单位公文处理辅助。

六、战略优化:从可用到卓越的价值升维

要实现智能聊天机器人从 “可用” 到 “卓越” 的跨越,需从技术迭代、生态整合、合规治理、行业定制四大维度构建长期优化路径:

  1. 技术迭代:持续优化交互体验:建立对话质量评估体系(基于准确率、连贯性、用户满意度等指标),通过 prompt 工程优化、行业知识库扩充、模型微调训练等方式持续迭代;引入情感分析技术,实现 “情绪感知 + 个性化回应”,提升交互温度;探索多模态交互(语音、图片、视频),丰富交互形式。
  2. 生态整合:深度融入业务系统:与企业数字化中台深度对接,实现用户数据、业务数据、交互数据的互联互通,构建业务闭环;集成 RPA(机器人流程自动化)技术,实现 “咨询 - 办理” 一站式服务(如自动下单、业务申报、数据统计);对接行业专属系统(如医疗 HIS 系统、金融核心业务系统),拓展应用边界。
  3. 合规治理:筑牢安全与合规防线:遵循《个人信息保护法》《生成式人工智能服务管理暂行办法》等法规要求,建立全生命周期数据安全管理体系 —— 用户敏感信息加密存储与脱敏处理、交互数据合规审计、生成内容安全审核;明确数据所有权与使用权,防范隐私泄露与合规风险。
  4. 行业定制:强化专业场景适配:针对垂直行业构建专属领域知识库与对话模板(如医疗行业的健康科普、法律行业的合规咨询、制造行业的设备维护指导);基于行业数据进行模型微调,提升专业术语准确性与业务逻辑适配性;联合行业伙伴共建解决方案,打造行业专属的智能聊天机器人标杆。

智能聊天机器人作为生成式 AI 技术落地的核心载体,正深刻改变着企业运营与用户交互的模式。从技术深耕到工程实践,从场景适配到价值升维,其未来将朝着更智能、更安全、更贴合行业需求的方向演进,成为企业数字化转型不可或缺的核心引擎。若在技术落地、场景扩展或战略规划中遇到具体问题,欢迎在评论区交流探讨,共探 AI 时代的商业新可能。

—END—

大模型4sapi智能聊天机器人 技术架构核心实现与行业赋能指南

作者 星链引擎
2025年10月24日 10:43

一、技术背景与行业演进

在人工智能技术迈入规模化落地的新阶段,智能聊天机器人已从单一工具演进为跨领域的核心交互载体,深度渗透于企业服务、数字营销、智能教育、娱乐互动等多元场景,成为连接人与数字系统的关键桥梁。随着生成式 AI 技术的突破性发展,OpenAI 等顶尖机构推出的高性能 API,与 New API 平台构建的企业级稳定服务生态形成协同,彻底打破了传统聊天机器人在自然交互、功能扩展性与运维稳定性上的瓶颈,为开发者打造具备行业级能力的智能交互系统提供了成熟的技术底座。

二、核心技术原理深度解析

智能聊天机器人的核心竞争力源于自然语言处理(NLP)技术的范式革新,其底层依托于大规模预训练模型(如 GPT-3 系列)构建的语义理解与生成能力。这类模型通过海量文本数据的预训练,具备了对复杂语境的深度解析、意图识别与自然语言生成能力,能够实现与人的流畅、连贯且贴合场景的对话交互,突破了传统规则式机器人的交互局限。

New API 平台的核心价值在于提供了企业级的 API 服务支撑:通过优化的网络架构、多节点冗余部署与高并发处理机制,确保 API 调用的低延迟、高可用与高稳定性,使开发者无需投入大量资源进行基础设施搭建、运维监控与网络优化,可聚焦于业务场景适配、对话逻辑设计等核心价值环节,实现技术能力向业务价值的快速转化。

三、企业级代码实现与部署演示

以下为基于 OpenAI API 与 New API 平台的轻量化企业级聊天机器人实现方案,代码已完成国内网络环境适配与企业级 API 接入配置,支持直接集成至业务系统,兼顾稳定性与易用性:

python

运行

import openai
from typing import Optional, Union

# 企业级API接入配置:兼顾国内网络适配与高可用保障
client = openai.OpenAI(
    base_url="https://4sapi.com",  # 国内专属优化节点,保障低延迟高可用
    api_key="your-api-key",       # 企业级API密钥,支持权限精细化管控
    timeout=30                    # 适配业务场景的超时机制设计
)

def intelligent_chat(
    prompt: str,
    model: str = "davinci",       # 可灵活切换适配场景的预训练模型
    max_tokens: int = 150,        # 基于交互场景优化的响应长度控制
    temperature: float = 0.7      # 平衡准确性与自然度的生成温度调节
) -> Optional[str]:
    """
    智能对话核心函数:支持参数化配置,适配多场景对话需求
    :param prompt: 用户输入prompt,支持带上下文的多轮对话扩展
    :param model: 模型选型,可根据业务复杂度切换(如gpt-3.5-turbo、davinci等)
    :param max_tokens: 响应内容最大长度限制,避免冗余输出
    :param temperature: 生成随机性调节,0.0偏严谨,1.0偏灵活
    :return: 结构化对话响应结果,异常时返回None
    """
    try:
        response = client.Completion.create(
            engine=model,
            prompt=prompt,
            max_tokens=max_tokens,
            temperature=temperature,
            n=1,
            stop=None
        )
        return response.choices[0].text.strip()
    except Exception as e:
        # 企业级异常处理:支持日志记录与降级策略触发
        print(f"API调用异常:{str(e)}")
        return None

# 多场景示例对话:模拟真实业务交互场景
if __name__ == "__main__":
    # 场景1:日常咨询类交互
    user_input_1 = "你好,能否为我简要分析今日宏观经济市场趋势?"
    response_1 = intelligent_chat(user_input_1)
    print(f"智能助手:{response_1 if response_1 else '当前服务暂不可用,敬请谅解'}")

    # 场景2:业务咨询类交互(可扩展至行业专属场景)
    user_input_2 = "请介绍企业级聊天机器人在客户服务中的核心优势?"
    response_2 = intelligent_chat(user_input_2, temperature=0.6)
    print(f"智能助手:{response_2 if response_2 else '当前服务暂不可用,敬请谅解'}")

四、核心代码架构解析

  1. 企业级 API 客户端配置:通过指定 New API 平台的国内优化节点(base_url),解决了跨境 API 调用的网络延迟与稳定性问题,同时配置超时机制与异常捕获,确保系统在极端情况下的容错能力,符合企业级应用的可靠性要求。
  2. 参数化对话函数设计intelligent_chat函数支持模型选型、响应长度、生成温度等多维度参数配置,可根据不同业务场景(如严谨的客服咨询、灵活的营销互动)进行精细化调整,具备极强的场景适配性。
  3. 结构化异常处理:内置异常捕获机制,支持异常日志记录与服务降级响应,避免单一 API 调用失败导致整个业务流程中断,保障系统的稳定性与用户体验。
  4. 多场景扩展能力:示例代码涵盖日常咨询与业务咨询两类典型场景,通过扩展 prompt 模板与行业知识库,可快速适配金融、教育、医疗等垂直领域的专属需求。

五、高端应用场景与价值赋能

智能聊天机器人基于其强大的语义理解与交互能力,已在多个高端场景实现价值落地,成为企业数字化转型的核心驱动力:

  1. 全渠道智能客服中枢:整合官网、APP、小程序、社交媒体等多渠道交互入口,实现 7×24 小时智能响应,快速解决用户高频咨询(如订单查询、业务办理、故障排查),降低企业客服成本 30%-50%,同时将客户满意度提升 20% 以上。
  2. 个性化营销赋能系统:基于用户交互数据构建精准用户画像,通过场景化对话触发个性化产品推荐、优惠活动推送与需求挖掘,实现营销转化率的倍数级提升,成为企业私域流量运营的核心工具。
  3. 自适应学习支持平台:作为教育场景的智能学习助手,可基于学生学习进度与知识薄弱点,提供个性化答疑、学习资源推荐、知识点拆解等服务,构建 “千人千面” 的自适应学习生态。
  4. 企业级协同办公助手:集成至企业 OA、CRM 等系统,支持会议纪要生成、工作流程咨询、文档检索、任务提醒等功能,提升员工办公效率,降低跨部门沟通成本。

六、进阶实践与战略优化建议

  1. 对话体验精细化迭代:基于用户交互日志与意图挖掘数据,构建对话质量评估体系,通过优化 prompt 工程、扩展行业知识库、调整模型参数等方式,持续提升对话的准确性、连贯性与场景贴合度;引入多轮对话上下文管理机制,实现更深度的交互体验。
  2. 安全合规与数据治理:建立全链路数据安全防护体系,对用户敏感信息进行脱敏处理,符合《个人信息保护法》等相关法规要求;采用数据加密传输与存储机制,防范数据泄露风险;针对生成内容建立审核机制,避免违规信息产出。
  3. 功能模块化扩展:基于核心聊天机器人架构,集成多模态交互(语音、图片、视频)、情感分析、用户画像系统、行业知识库等高端模块,打造全方位智能交互平台;通过 API 接口与企业现有业务系统(CRM、ERP、SCM)深度集成,实现数据互通与业务闭环。
  4. 行业定制化赋能:针对垂直行业需求,构建专属领域知识库与对话模板(如金融行业的理财咨询、医疗行业的健康科普、法律行业的合规咨询),提升机器人在专业场景的响应质量;结合行业数据进行微调训练,进一步强化模型的行业适配能力。

若在技术实现、场景扩展或战略落地过程中遇到具体问题,欢迎在评论区交流探讨,共同探索智能聊天机器人在数字化时代的无限可能。

—END—

vue3组件通信

作者 东华帝君
2025年10月24日 10:40

在 Vue 3 中,父子组件之间的通信方式非常丰富,可以根据组件的关系、数据流方向、复杂程度来选择。下面我帮你整理成一个 清晰体系,包括 优缺点、使用场景、示例


🧩 一、Props + Event(最常用、单向数据流)

1️⃣ 父组件 → 子组件:props

  • 父组件通过 props 向子组件传递数据
  • 数据是响应式的,子组件可读不可直接修改父组件的值(可通过 v-model 或自定义事件来实现双向绑定)
<!-- Parent.vue -->
<template>
  <Child :msg="message" @change="handleChange" />
</template>

<script>
import Child from './Child.vue'
import { ref } from 'vue'

export default {
  components: { Child },
  setup() {
    const message = ref('Hello Vue 3')
    const handleChange = (val) => { console.log('子组件通知:', val) }
    return { message, handleChange }
  }
}
</script>
<!-- Child.vue -->
<template>
  <button @click="$emit('change', '子组件事件')">{{ msg }}</button>
</template>

<script>
export default {
  props: ['msg']
}
</script>

2️⃣ 子组件 → 父组件:emit

  • 子组件通过 $emit 发事件通知父组件
  • 常用于按钮点击、数据修改、事件触发等

🧩 二、v-model 双向绑定(语法糖)

  • Vue 3 支持在子组件上自定义 v-model 的 prop 名和事件名
  • 常用于表单组件或数据需要双向绑定的场景
<!-- Parent.vue -->
<template>
  <Child v-model:count="count" />
  <p>{{ count }}</p>
</template>

<script>
import { ref } from 'vue'
import Child from './Child.vue'
export default { components: { Child }, setup() { const count = ref(0); return { count } } }
</script>

<!-- Child.vue -->
<template>
  <button @click="$emit('update:count', count+1)">+1</button>
</template>
<script>
import { ref } from 'vue'
export default {
  props: { count: Number },
  setup(props) { return { count: props.count } }
}
</script>

🧩 三、Provide / Inject(跨级组件传递)

  • 父组件使用 provide 提供数据
  • 子组件及深层组件使用 inject 注入数据
  • 用于多级组件通信、避免层层传递 props
<!-- GrandParent.vue -->
<script setup>
import { provide } from 'vue'
const theme = 'dark'
provide('theme', theme)
</script>

<!-- Child.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
console.log(theme) // 'dark'
</script>

⚠️ 注意:Provide / Inject 不是响应式的,如果需要响应式,提供的是 refreactive


🧩 四、通过 ref 获取子组件实例

  • 父组件通过 ref 获取子组件实例,调用子组件的方法
  • 常用于非数据通信,比如调用子组件的内部方法、重置表单、播放视频等
<!-- Parent.vue -->
<template>
  <Child ref="childRef" />
  <button @click="callChild">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const callChild = () => { childRef.value.sayHello() }
</script>

<!-- Child.vue -->
<script setup>
const sayHello = () => { console.log('Hello from child') }
</script>

🧩 五、全局状态管理(Vuex / Pinia / reactive store)

  • 父子组件通过共享 全局 store 实现通信
  • 适合跨组件、跨页面共享状态
  • Vue 3 推荐 Pinia 作为轻量化状态管理
// store.js
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
  state: () => ({ count: 0 }),
  actions: { increment() { this.count++ } }
})

// Parent.vue / Child.vue
import { useStore } from './store'
const store = useStore()
store.increment() // 任意组件都可以修改

🧩 六、Event Bus(自定义事件总线)

  • Vue 3 不再内置 EventBus,但可以用 mitt 创建事件总线
  • 常用于兄弟组件通信,或者父子组件通信也可
  • 不推荐大量使用,容易造成维护困难
// bus.js
import mitt from 'mitt'
export const bus = mitt()

// Parent.vue
import { bus } from './bus.js'
bus.on('myEvent', val => console.log(val))

// Child.vue
import { bus } from './bus.js'
bus.emit('myEvent', 'hello bus')

🧠 总结

通信方式 方向 场景 备注
Props + emit 父 → 子 / 子 → 父 常规父子通信 最推荐
v-model 父 ↔ 子 表单 / 双向绑定 Vue 3 支持自定义 prop 名
provide / inject 父 → 多级子 跨级传值 避免层层传递 props
ref 调用子组件方法 父 → 子 调用方法 非数据通信
Pinia / Vuex 全局 跨组件 / 跨页面 状态管理必备
Event Bus (mitt) 全局 / 任意方向 兄弟组件 / 临时通信 不推荐大量使用

总结一句话:

一般情况用 Props + emit / v-model;跨级用 provide/inject;跨组件用 Pinia;调用方法用 ref;EventBus 仅作补充。

【译】 CSS 布局算法揭秘:一次思维转变,让 CSS 从玄学到科学

作者 Sherry007
2025年10月24日 10:38

🔗 原文链接:Understanding Layout Algorithms
👨‍💻 原作者:Josh W. Comeau
📅 发布时间:2022年3月28日
🕐 最后更新:2025年1月28日

⚠️ 关于本译文

本文基于 Josh W. Comeau 的原文进行忠实翻译,力求准确传达原作者的技术观点和逻辑结构。

🎨 特色亮点:

  • 保持原文的完整性和技术准确性
  • 采用自然流畅的中文表达,避免翻译腔
  • 添加画外音板块,提供译者的补充解读和实践心得
  • 使用生动比喻帮助理解复杂概念

💡 画外音说明: 文中标注为画外音的部分是译者基于实际开发经验添加的拓展解释,旨在帮助读者更好地理解和应用这些概念,不代表原作者观点。

🖼️ 关于交互式示例: 本文中的图片和交互式演示以截图和GIF动图形式呈现。如需体验完整的交互式功能,可前往原文进行实际操作。


几年前,我在学习 CSS 时经历了一个"顿悟时刻"。

在那之前,我一直专注于学习各种 CSS 属性和取值,比如 z-index: 10justify-content: center。我觉得只要理解每个属性的作用,就能深入掌握整个语言。

但关键的领悟是:CSS 远不止是属性的集合,它是一系列相互关联的布局算法组成的星系。每个算法都是一个复杂的系统,有着自己的规则和秘密机制。

仅仅学习特定属性的作用是不够的。我们需要理解布局算法如何工作,以及它们如何使用我们提供的属性。

💡画外音:这就像学做菜,光知道各种调料的味道还不够,你得理解不同的烹饪方法——煎、炒、炖、煮——每种方法对同样的食材会产生完全不同的效果。

不知道你有没有碰到过这种情况:写了段很熟悉的 CSS,之前用过无数次都没问题,结果这次却莫名其妙得到了完全不一样的效果?简直让人抓狂。让人觉得 CSS 捉摸不透、不靠谱。明明是一样的代码,为什么结果却不一样??

其实这是因为 CSS 属性作用在一个复杂的系统上,有些细微的上下文变化会改变属性的行为方式。我们的心智模型不完整,自然就会遇到各种意外!

当我开始深入研究布局算法时,一切都开始变得更有意义了。困扰我多年的谜团都被解开了。我意识到 CSS 实际上是一门相当稳健的语言,这个时候我开始真正享受编写 CSS!

在这篇博文中,我们将看看这个新视角如何帮助我们理解 CSS 中发生的事情。我们还将用这个视角来解决一个出奇常见的谜团。🕵️

🧩 布局算法

那么,什么是"布局算法"?你可能已经熟悉其中一些了。它们包括:

  • Flexbox(弹性盒布局)
  • Positioned(定位布局,如 position: absolute
  • Grid(网格布局)
  • Table(表格布局)
  • Flow(流式布局)

从技术上来说,它们被称为"布局模式"(layout modes),而不是布局算法。但我发现"布局算法"这个标签更有助于理解。

当浏览器渲染我们的 HTML 时,每个元素都会使用一个主要的布局算法来计算其布局。我们可以通过特定的 CSS 声明选择不同的布局算法。例如,应用 position: absolute 会切换元素使用定位布局。

假设我有以下 CSS:

.box {
  z-index: 10;
}

我们首先要弄清楚哪个布局算法会被用来渲染 .box 元素。根据提供的 CSS,它将使用 Flow 布局进行渲染。

Flow 是 Web 的"元老级"布局算法。它诞生的年代,互联网还被看作是一个巨型的超链接文档库——就像把全世界的档案馆都搬到了网上。它的设计思路跟文字处理软件(比如 Word)的排版逻辑很像。

Flow 是用于非表格 HTML 元素的默认布局算法。除非我们明确选择另一个布局算法,否则将使用 Flow。

z-index 属性用于控制堆叠顺序,确定当元素重叠时哪个显示在"顶部"。但问题是:它在 Flow 布局中没有实现。Flow 布局专注于创建文档风格的布局,而我还没见过哪个文字处理软件允许元素重叠。

💭 画外音:想想看,在 Word 里你能让两段文字重叠吗?显然不行,因为文档的本质就是从上到下、一行行地呈现内容。

如果几年前你问我这个问题,我会说:

"你不能在不设置 position 为 'relative' 或 'absolute' 的情况下使用 z-index,因为 z-index 属性依赖于 position 属性。"

这话倒也不算错,但确实存在一个细微的误解。 更准确的说法是:z-index 属性在 Flow 布局算法中压根就没有实现,所以想让这个属性生效,就得换个布局算法。

这听起来可能有点较真,但这个小小的认知偏差可能会引发大麻烦。比如说:

image.png

在这个演示中,我们有 3 个兄弟元素,使用 Flexbox 布局算法排列。

中间的兄弟元素设置了 z-index而且它生效了。试着删除它,你会注意到它会落到兄弟元素后面。

image.png

这怎么可能? 我们哪里都没设置 position: relative

这是因为 Flexbox 算法实现了 z-index 属性。当语言设计者们在开发 Flexbox 算法时,决定让 z-index 属性在这里也能控制堆叠顺序,就像在定位布局中一样。

这就是关键的思维模式转变。 CSS 属性本身是没有意义的。由布局算法来定义它们的作用,以及它们如何在计算中被使用。

💭 画外音:这就像是"一物多用"——同一个螺丝刀,在木工手里是安装工具,在电工手里是测电工具。工具本身没变,但使用场景决定了它的功能。

当然,也有些 CSS 属性在所有布局算法里都一视同仁。比如 color: red 走到哪儿都是红色文本。但大部分属性的行为都可以被布局算法重新定义。甚至有不少属性本身就没有默认行为,全看布局算法怎么用它。

🤯 颠覆认知的例子

有个例子曾经让我大吃一惊: 你知道吗,width 属性在不同的布局算法中,实现方式竟然是不一样的?

来看看实例:

image.png 我们的 .item 元素只有一个 CSS 属性:width: 2000px

第一个 .item 实例使用 Flow 布局渲染,它真的会占满 2000px 的宽度。在 Flow 布局中,width 是铁律。 说 2000px 就是 2000px,管你容器够不够宽。

然而,第二个 .item 实例被渲染在一个 Flex 容器内,这意味着它使用 Flexbox 布局。在 Flexbox 算法中,width 更像是一个建议。

Flexbox 规范把这叫假设尺寸(hypothetical size)——就是元素在"理想国"里的尺寸,没人管没人约束的状态。在完美世界里,这个元素想要 2000px 宽。但现实是它被塞进了一个窄容器,只好委屈地缩小自己。

💭 画外音:这就像打包行李,Flow 布局就是硬壳箱子,尺寸固定不变;而 Flexbox 就像软布包,可以根据空间调整形状。两者都有各自的用途!

再强调一次,理解这个思路很关键。不是说 width 在 Flexbox 里有什么特殊情况,而是 Flexbox 算法对 width 的处理方式本来就跟 Flow 不一样。

我们写的属性就是输入参数,就像给函数传参一样。布局算法拿到这些参数,爱怎么用怎么用。想真正搞懂 CSS,光知道属性是什么还不够,得理解布局算法是怎么运作的。

🔎 识别布局算法

CSS 没有专门的 layout-mode 属性。能影响布局算法的属性有好几个,而且有时候还挺容易搞混!

在某些情况下,应用于元素的 CSS 属性会选择特定的布局模式。例如:

.help-widget {
  /* 这个声明会使用定位布局: */
  position: fixed;
  right: 0;
  bottom: 0;
}

.floated {
  /* 这个声明会使用浮动布局: */
  float: left;
  margin-right: 32px;
}

在其他情况下,我们需要查看父元素应用的 CSS。例如:

<style>
  .row {
    display: flex;
  }
</style>

<ul class="row">
  <li class="item"></li>
  <li class="item"></li>
  <li class="item"></li>
</ul>

给元素加上 display: flex并不是让 .row 自己用 Flexbox,而是在说:"我的孩子们都得按 Flexbox 规则来摆"。

用术语讲,display: flex 创建了一个弹性格式化上下文(flex formatting context)。所有直接子元素都会进入这个上下文,于是它们就从默认的 Flow 布局切换成了 Flexbox 布局。

💭 画外音:这就像是父母给孩子定规矩——父元素设置 display: flex,就是在告诉子元素们:"你们都按 Flexbox 的规矩来!"

display: flex 也会将内联元素(如 <span>)转变为块级元素,所以它确实对父元素的布局有一些影响。但它不会改变所使用的布局算法。)

🎭 布局算法的变体

有些布局算法还细分成了好几种变体。

比如说,定位布局其实包含了好几种不同的"定位方案":

  • Relative(相对定位)
  • Absolute(绝对定位)
  • Fixed(固定定位)
  • Sticky(粘性定位)

每个变体就像是独立的小算法,不过它们之间也有家族相似性(比如都支持 z-index 属性)。

同样的,Flow 布局里元素也分两派:块级(block)和内联(inline)。关于 Flow 布局,我们待会儿会详细聊。

⚔️ 冲突情况

如果一个元素同时受到多个布局算法的影响,会怎样?

比如说:

<style>
  .row {
    display: flex;
  }

  .primary.item {
    position: absolute;
  }
</style>

<ul class="row">
  <li class="item"></li>
  <li class="primary item"></li>
  <li class="item"></li>
</ul>

这三个列表项都是 Flex 容器内的子元素,按理说应该遵循 Flexbox 规则来定位。但中间那个子元素通过设置 position: absolute 选择了定位布局。

据我理解,一个元素最终会采用一种主要的布局模式来渲染。这有点像 CSS 选择器的优先级:某些布局模式天生就比其他的优先级更高。

虽然我不知道完整的优先级规则,但定位布局通常会压过其他所有算法。所以在这个例子中,中间的子元素会使用定位布局,而不是 Flexbox。

结果,Flexbox 计算会表现得好像只有两个子元素,而不是三个。就 Flexbox 算法而言,中间的子元素不存在(实际情况会更复杂一些:Flex 父元素的绝对定位子元素有时候还是能用上 Flexbox 的某些属性。不过在实际开发中,这种冲突情况挺少见的。)。

💭 画外音:这就像在队伍里突然有人说"我要单独行动",然后就脱离了队伍的管理。其他人继续按照队伍规则排列,而这个人按照自己的规则行事。

一般来说,这种冲突都是比较明显的,也往往是有意为之的。但如果你发现某个元素的行为跟预期不符,不妨检查一下它到底用的是哪个布局算法。结果可能会让你大吃一惊!

🤔 相对定位的谜题

说到这里有个有趣的问题:既然每个元素都只用一个布局算法来渲染,那相对定位怎么解释呢?

设置了 position: relative 的元素明显是用定位布局渲染的。它可以使用 topleft 这些定位布局专属的属性。可奇怪的是,它又能参与 Flexbox / Grid 布局!

说到这里就有点超出本文范围了,确实有点复杂!这里简单解释一下,感兴趣的朋友可以了解:

每个元素都在特定的格式化上下文中渲染,至于参不参与这个上下文,由布局算法说了算。一般情况下,定位布局算法会无视这些上下文,但它给相对定位开了个后门。

当一个相对定位的元素在 Flexbox 上下文中渲染时,定位布局算法会允许它参与进来。等用 Flexbox 确定好它的大小和位置后,再应用定位布局的那一套(比如用 topleft 微调位置)。

可以把它理解成一种"组合拳"——定位布局算法会为相对定位元素组合使用 Flexbox 布局算法。

💭 画外音:这就像是"既要又要"——既要参与团队活动(Flexbox),又要保留一点个人自由(可以用 top/left 微调位置)。相对定位就是这样的和事佬!

🎪 内联魔法空间

好了,现在来看一个经典的"CSS 怪现象",看看用布局算法的视角能不能帮我们搞定它。

这里有一筐可爱的猫咪:

image.png

嗯...为什么图片下方有一点额外的空间?

如果你用开发者工具检查它,你会注意到几个像素的差异:

钉钉录屏_2025-10-23 203215.gif

图片明明是 250px 高,但容器却有 258.5px 高!

熟悉盒模型的朋友都知道,元素之间可以用 padding、border 和 margin 来控制间距。你可能会想:是不是图片有 margin,或者容器有 padding?

但这次,这些常见的"嫌疑犯"都是无辜的。这就是为什么多年来,我一直私下管这叫"内联魔法空间"——它不是那些常规属性搞的鬼。

💭 画外音:这个问题真的困扰过无数开发者!明明没设置 margin 或 padding,却莫名其妙多出了空间。我第一次遇到时,真的怀疑是不是浏览器有 bug...

要理解这里发生了什么,我们必须更深入地研究 Flow 布局。

📄 Flow 布局详解

前面提到过,Flow 布局是专门为文档设计的,就像 Word 那样的文字处理软件。

文档有这样的结构特点:

  • 字符组成词句:这些元素横向并排排列,空间不够时自动换行。
  • 段落作为块:像段落、标题、图片这样的块状元素,会从上到下一个个垂直堆叠起来。

Flow 布局就是按这套规则来的。元素要么是内联的(像单词一样横着排),要么是块级的(像段落一样竖着摞):

钉钉录屏_2025-10-23 203634.gif

🌍 方向因语言而异

这个例子是基于英语这种横向、从左到右的语言。但全世界的语言可不都这样!

比如阿拉伯语和希伯来语,是从右往左横着写的。而中文、日文、韩文这些汉字文化圈的语言,传统上是竖着写的,从上往下。

现在越来越流行用一种能适配不同语言的方式来写 CSS。比如用 margin-inline-start,在英文里指左边距,在阿拉伯语里就自动变成右边距。

大多数 HTML 元素都有合理的默认值。<p><h1> 是块级元素,<span><strong> 则是内联元素。

内联元素是用在段落内部的,不是用来做页面布局的。 比如我们想在句子中间插个小图标之类的。

为了保证内联元素不会影响周围文字的阅读体验,浏览器会自动加一点垂直间距。

💭 画外音:想象一下,如果文本行挤得密密麻麻,阅读起来会多难受!这个额外空间就像是给文字"呼吸"的空间,让阅读更舒适。

所以,谜底揭晓了:图片为什么会多出几个像素?因为图片默认就是内联元素!

Flow 布局算法把这张图片当成段落里的一个字符,在下面留了点空间,避免它跟(假想的)下一行文字贴得太近。

默认情况下,内联元素都是"基线"对齐的。也就是说图片底部会跟文字的基线(那条看不见的横线)对齐。这就是为什么图片下面有空隙——那是给字母下伸部分(比如 jp 的小尾巴)预留的空间。

所以罪魁祸首既不是 margin,也不是 padding,更不是 border,而是 Flow 布局给内联元素自带的一点固有空间。

✅ 解决问题

这个问题有好几种解决方案。最简单的可能就是把图片改成块级元素:

image.png

这个"内联魔法空间"在我职业生涯中坑过我无数次,所以现在我都会在自定义的 CSS Reset 里直接用这个方法预防。

或者,既然这是 Flow 布局特有的行为,我们也可以干脆换个布局算法:

image.png

最后,我们还可以通过使用 line-height 将额外空间缩小到 0 来解决这个问题:

image.png

这个方案是把所有的行间距都设成 0 来消除额外空间。这会让多行文字完全没法读,不过反正这个容器里也没文字,所以无所谓。

我推荐用前两种方案。之所以还要提这个方案,纯粹是因为它挺有意思的(而且能证明问题确实是行间距导致的!)。

♿ 行高与无障碍

说到 line-height,有个冷知识:原生的 HTML 其实算不上无障碍友好,因为默认行距太窄了!对于有阅读障碍的用户来说,行距太紧会让文字很难读。

大多数浏览器默认的 line-height 是 1.1 到 1.2,但按照 WCAG 无障碍指南,正文至少应该设到 1.5。

💭 画外音:这是一个很重要但常被忽视的无障碍问题。我们不仅要让网站"看起来好",更要让所有人都能舒适地使用,包括有阅读障碍的用户。

🧠 建立直觉

重点来了: 如果你只盯着各种 CSS 属性学,永远搞不明白这个"神秘空间"是哪来的。翻遍 displayline-height 的 MDN 文档也找不到答案。

就像我们在这篇文章里学到的,"内联魔法空间"其实一点都不魔法。它就是 Flow 布局算法里的一条规则:内联元素会受 line-height 影响。只不过因为我的心智模型有个大窟窿,才让我困惑了这么多年。

CSS 里有一大堆布局算法,每个都有自己的小怪癖和暗藏的机制。只盯着 CSS 属性学,你只能看到冰山一角。 那些真正重要的概念——堆叠上下文、包含块、层叠来源——你永远接触不到!

💭 画外音:这就像学开车,如果你只背交通规则,但不理解汽车的工作原理(发动机、刹车、转向系统),你永远成不了好司机。CSS 也一样,属性是表面,布局算法才是核心!

可惜的是,网上很多 CSS 教程也是浮于表面。经常看到博客或推文分享一个"好用的 CSS 技巧",但不解释为什么管用,也不说布局算法是怎么处理的。

CSS 是门很难调试的语言——没有报错信息、没有 debugger、没有 console.log。直觉是我们最好的工具。 如果只会复制粘贴代码而不真正理解,迟早会被布局算法的某个隐藏特性卡住,动弹不得。

几年前,我下定决心要培养自己的 CSS 直觉。每次遇到意外的行为,我就像侦探一样钻研这个问题。翻 MDN 文档、查 CSSWG 规范、反复调试代码,直到彻底搞明白是怎么回事。

这个投入绝对值得,不过说实话,确实花了不少时间。😅

🎓 继续学习(纯搬运)

我希望能帮其他开发者少走弯路。我发布了一门综合性的在线课程 CSS for JavaScript Developers

这门课深入探索 CSS 的底层工作原理,专注于帮你建立一套强大的心智模型,像拼图一样一块块搭建起你的 CSS 直觉。虽然不能保证你再也不会遇到 CSS 难题,但能帮你装备好应对挑战的工具箱。

到目前为止,已经有超过 18,000 名开发者学习了这门课,来自 Apple、Google、Microsoft、Facebook、Netflix 等知名公司。大家的反馈都非常好。

你可以在课程主页上了解更多信息:
css-for-js.dev


📝 译者感想

这篇文章真的颠覆了我对 CSS 的认知。Josh Comeau 用"布局算法"这个全新视角重新诠释了 CSS,把那些原本让人摸不着头脑、充满意外的行为,变得有章可循。

特别值得琢磨的几点:

  1. 属性不是独立的个体 - 它们只是传给布局算法的参数,不同算法会用不同方式解读同一个属性
  2. 上下文决定一切 - 同样的代码,放在不同的布局算法里,效果可能天差地别
  3. 理解原理而非死记硬背 - 搞懂了"为什么","怎么做"自然就明白了

这套思路不光适用于 CSS,学任何技术都一样。与其死记硬背无数个特殊情况和小技巧,不如深入理解背后的系统运作机制。

💫 最后的建议:下次遇到"诡异"的 CSS 行为时,先问自己:当前元素用的是哪个布局算法?这个算法是如何解释我写的属性的?这样的思考方式会帮你快速定位问题!


💡 实用总结

遇到 CSS 问题时的思考清单:

  1. 识别布局算法:当前元素使用的是哪个布局算法?
  2. 检查父子关系:父元素的布局设置如何影响子元素?
  3. 理解属性在当前算法中的含义:这个属性在这个布局算法中是如何工作的?
  4. 检查冲突:是否有多个布局算法在竞争?优先级如何?
  5. 考虑切换算法:是否有更适合当前需求的布局算法?

常见布局算法速查:

  • Flow(流式):默认文档布局,适合文章和文本内容
  • Flexbox(弹性盒):一维布局,适合导航栏、卡片排列
  • Grid(网格):二维布局,适合复杂的页面布局
  • Positioned(定位):脱离正常流,适合浮层、固定元素

感谢阅读!希望这篇翻译能帮助你建立更清晰的 CSS 心智模型~ 🎉

虚拟列表:拯救你的万级数据表格

2025年10月24日 10:38

从原理到实战,彻底解决大数据量渲染的性能瓶颈

引言:当数据量成为性能杀手

在现代Web应用中,数据表格是最常见的UI组件之一。但当数据量达到万级甚至十万级时,传统的渲染方式就会遇到严重的性能问题:

// 传统渲染方式的性能问题
const performanceIssues = {
  DOM节点数量: '10000行 × 5列 = 50000个DOM节点',
 内存占用: '100MB+ (取决于数据复杂度)',
 渲染时间: '5-15秒 (阻塞主线程)',
 用户交互: '卡顿、滚动延迟、输入无响应',
 电池消耗: '移动设备电量快速耗尽'
};

真实场景的性能对比

让我们看一个实际案例:一个包含10,000行数据的用户管理表格

// 传统渲染 vs 虚拟列表渲染
const comparison = {
  traditional: {
    renderTime: '12.5秒',
    memoryUsage: '156MB', 
    DOMNodes: '52,340',
    scrollFPS: '8-15 FPS',
    userExperience: '极度卡顿,无法正常使用'
  },
  virtualized: {
    renderTime: '0.15秒',      // 83倍提升
    memoryUsage: '18MB',       // 88% 内存减少
    DOMNodes: '52',            // 99.9% DOM节点减少
    scrollFPS: '60 FPS',       // 流畅滚动
    userExperience: '如丝般顺滑'
  }
};

一、虚拟列表的核心原理

1.1 什么是虚拟列表?

虚拟列表的核心思想是:只渲染可见区域的内容,非可见区域用空白填充

graph TB
    A[完整数据: 10000条] --> B[可见区域: 10条]
    B --> C[实际渲染: 10条 + 缓冲区域]
    C --> D[用户感知: 完整10000条]
    
    E[隐藏区域] --> F[空白填充]
    F --> G[滚动时动态更新]

1.2 基本算法原理

class VirtualListCore {
  constructor(itemCount, itemHeight, containerHeight) {
    this.itemCount = itemCount;        // 总数据量
    this.itemHeight = itemHeight;      // 每项高度
    this.containerHeight = containerHeight; // 容器高度
    
    this.visibleItemCount = Math.ceil(containerHeight / itemHeight);
    this.overscan = 5; // 上下缓冲项数
  }
  
  // 计算可见范围
  getVisibleRange(scrollTop) {
    const startIndex = Math.floor(scrollTop / this.itemHeight);
    const endIndex = Math.min(
      startIndex + this.visibleItemCount + this.overscan,
      this.itemCount - 1
    );
    
    return {
      start: Math.max(0, startIndex - this.overscan),
      end: endIndex
    };
  }
  
  // 计算偏移量
  getOffset(startIndex) {
    return startIndex * this.itemHeight;
  }
  
  // 计算总高度
  getTotalHeight() {
    return this.itemCount * this.itemHeight;
  }
}

二、固定高度虚拟列表实现

2.1 基础版本实现

import React, { useState, useMemo, useCallback } from 'react';

const FixedVirtualList = ({ data, itemHeight, containerHeight, renderItem }) => {
  const [scrollTop, setScrollTop] = useState(0);
  
  // 计算可见范围
  const { visibleData, totalHeight, offset } = useMemo(() => {
    const visibleItemCount = Math.ceil(containerHeight / itemHeight);
    const startIndex = Math.floor(scrollTop / itemHeight);
    const endIndex = Math.min(startIndex + visibleItemCount, data.length - 1);
    
    // 添加缓冲项
    const overscan = 5;
    const visibleStart = Math.max(0, startIndex - overscan);
    const visibleEnd = Math.min(endIndex + overscan, data.length - 1);
    
    return {
      visibleData: data.slice(visibleStart, visibleEnd + 1),
      totalHeight: data.length * itemHeight,
      offset: visibleStart * itemHeight
    };
  }, [data, scrollTop, itemHeight, containerHeight]);
  
  const handleScroll = useCallback((e) => {
    setScrollTop(e.target.scrollTop);
  }, []);
  
  return (
    <div 
      style={{
        height: containerHeight,
        overflow: 'auto',
        position: 'relative'
      }}
      onScroll={handleScroll}
    >
      {/* 撑开容器 */}
      <div style={{ height: totalHeight, position: 'relative' }}>
        {/* 可见项容器 */}
        <div style={{ transform: `translateY(${offset}px)` }}>
          {visibleData.map((item, index) => (
            <div
              key={item.id}
              style={{
                height: itemHeight,
                position: 'absolute',
                top: 0,
                left: 0,
                right: 0,
                transform: `translateY(${index * itemHeight}px)`
              }}
            >
              {renderItem(item, visibleStart + index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

2.2 性能优化版本

import React, { useState, useMemo, useCallback, useRef } from 'react';

const OptimizedVirtualList = ({
  data,
  itemHeight,
  containerHeight,
  renderItem,
  overscan = 10
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const scrollRef = useRef();
  const rafId = useRef();
  
  // 使用防抖的滚动处理
  const handleScroll = useCallback((e) => {
    if (rafId.current) {
      cancelAnimationFrame(rafId.current);
    }
    
    rafId.current = requestAnimationFrame(() => {
      setScrollTop(e.target.scrollTop);
    });
  }, []);
  
  // 计算可见范围 - 使用更精确的计算
  const { visibleData, totalHeight, offset, startIndex } = useMemo(() => {
    const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
    const visibleItemCount = Math.ceil(containerHeight / itemHeight);
    const endIndex = Math.min(
      startIndex + visibleItemCount + overscan * 2,
      data.length - 1
    );
    
    return {
      visibleData: data.slice(startIndex, endIndex + 1),
      totalHeight: data.length * itemHeight,
      offset: startIndex * itemHeight,
      startIndex
    };
  }, [data, scrollTop, itemHeight, containerHeight, overscan]);
  
  // 滚动到指定项
  const scrollToIndex = useCallback((index) => {
    if (scrollRef.current) {
      const targetScrollTop = index * itemHeight;
      scrollRef.current.scrollTo({
        top: targetScrollTop,
        behavior: 'smooth'
      });
    }
  }, [itemHeight]);
  
  return (
    <div 
      ref={scrollRef}
      style={{
        height: containerHeight,
        overflow: 'auto',
        position: 'relative',
        willChange: 'scroll-position'
      }}
      onScroll={handleScroll}
    >
      <div 
        style={{ 
          height: totalHeight,
          position: 'relative'
        }}
        aria-label={`虚拟列表${data.length}`}
      >
        <div 
          style={{ 
            transform: `translateY(${offset}px)`,
            position: 'absolute',
            top: 0,
            left: 0,
            right: 0
          }}
        >
          {visibleData.map((item, relativeIndex) => (
            <div
              key={item.id}
              style={{
                height: itemHeight,
                position: 'relative'
              }}
            >
              {renderItem(item, startIndex + relativeIndex)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

三、动态高度虚拟列表实现

固定高度虽然简单,但实际项目中更多遇到的是动态高度的情况。

3.1 动态高度计算的挑战

// 动态高度的核心问题
const dynamicHeightChallenges = {
  问题1: '无法提前知道每项的确切高度',
  问题2: '滚动位置计算复杂',
  问题3: '快速滚动时高度计算不及时',
  问题4: 'DOM测量影响性能'
};

3.2 解决方案:位置预估和动态调整

import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';

class DynamicSizeVirtualList {
  constructor(estimatedHeight = 50, bufferSize = 10) {
    this.estimatedHeight = estimatedHeight;
    this.bufferSize = bufferSize;
    this.positions = [];
    this.totalHeight = 0;
    this.measuredHeights = new Map();
  }
  
  // 初始化位置信息
  initialize(totalCount) {
    this.positions = Array.from({ length: totalCount }, (_, index) => ({
      index,
      top: index * this.estimatedHeight,
      height: this.estimatedHeight,
      bottom: (index + 1) * this.estimatedHeight
    }));
    this.totalHeight = totalCount * this.estimatedHeight;
  }
  
  // 更新某项的实际高度
  updateHeight(index, height) {
    if (this.measuredHeights.get(index) === height) return;
    
    this.measuredHeights.set(index, height);
    const oldHeight = this.positions[index].height;
    const diff = height - oldHeight;
    
    if (diff !== 0) {
      this.positions[index].height = height;
      this.positions[index].bottom = this.positions[index].top + height;
      
      // 更新后续所有项的位置
      for (let i = index + 1; i < this.positions.length; i++) {
        this.positions[i].top = this.positions[i - 1].bottom;
        this.positions[i].bottom = this.positions[i].top + this.positions[i].height;
      }
      
      this.totalHeight = this.positions[this.positions.length - 1].bottom;
    }
  }
  
  // 根据滚动位置获取可见范围
  getVisibleRange(scrollTop, containerHeight) {
    // 二分查找起始位置
    let start = 0;
    let end = this.positions.length - 1;
    
    while (start <= end) {
      const mid = Math.floor((start + end) / 2);
      const position = this.positions[mid];
      
      if (position.bottom < scrollTop) {
        start = mid + 1;
      } else if (position.top > scrollTop + containerHeight) {
        end = mid - 1;
      } else {
        start = mid;
        break;
      }
    }
    
    const startIndex = Math.max(0, start - this.bufferSize);
    
    // 查找结束位置
    let currentHeight = 0;
    let endIndex = startIndex;
    
    while (endIndex < this.positions.length && currentHeight < containerHeight + scrollTop) {
      currentHeight += this.positions[endIndex].height;
      endIndex++;
    }
    
    endIndex = Math.min(this.positions.length - 1, endIndex + this.bufferSize);
    
    return {
      start: startIndex,
      end: endIndex,
      offset: this.positions[startIndex].top
    };
  }
}

3.3 完整的动态高度虚拟列表组件

const DynamicVirtualList = ({
  data,
  containerHeight,
  estimatedItemHeight = 50,
  renderItem,
  overscan = 8
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const [sizeCache] = useState(new Map());
  const virtualizerRef = useRef();
  const containerRef = useRef();
  const itemRefs = useRef(new Map());
  
  // 初始化虚拟化器
  useEffect(() => {
    virtualizerRef.current = new DynamicSizeVirtualList(estimatedItemHeight, overscan);
    virtualizerRef.current.initialize(data.length);
  }, [data.length, estimatedItemHeight, overscan]);
  
  // 测量项的实际高度
  const measureItems = useCallback(() => {
    if (!virtualizerRef.current) return;
    
    itemRefs.current.forEach((ref, index) => {
      if (ref && ref.offsetHeight) {
        const height = ref.offsetHeight;
        virtualizerRef.current.updateHeight(index, height);
        sizeCache.set(index, height);
      }
    });
  }, [sizeCache]);
  
  // 延迟测量,避免布局抖动
  useEffect(() => {
    const timeoutId = setTimeout(measureItems, 0);
    return () => clearTimeout(timeoutId);
  }, [measureItems]);
  
  const handleScroll = useCallback((e) => {
    setScrollTop(e.target.scrollTop);
  }, []);
  
  // 计算可见范围
  const { visibleData, totalHeight, offset, startIndex } = useMemo(() => {
    if (!virtualizerRef.current) {
      return { visibleData: [], totalHeight: 0, offset: 0, startIndex: 0 };
    }
    
    const { start, end, offset } = virtualizerRef.current.getVisibleRange(
      scrollTop,
      containerHeight
    );
    
    return {
      visibleData: data.slice(start, end + 1),
      totalHeight: virtualizerRef.current.totalHeight,
      offset,
      startIndex: start
    };
  }, [data, scrollTop, containerHeight]);
  
  // 设置项引用
  const setItemRef = useCallback((index, ref) => {
    if (ref) {
      itemRefs.current.set(startIndex + index, ref);
    } else {
      itemRefs.current.delete(startIndex + index);
    }
  }, [startIndex]);
  
  return (
    <div
      ref={containerRef}
      style={{
        height: containerHeight,
        overflow: 'auto',
        position: 'relative'
      }}
      onScroll={handleScroll}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offset}px)` }}>
          {visibleData.map((item, relativeIndex) => (
            <div
              key={item.id}
              ref={(ref) => setItemRef(relativeIndex, ref)}
              style={{
                position: 'relative'
                // 高度由内容决定
              }}
            >
              {renderItem(item, startIndex + relativeIndex)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

四、虚拟列表在表格中的应用

4.1 虚拟化表格组件

const VirtualizedTable = ({
  columns,
  data,
  rowHeight = 48,
  headerHeight = 56,
  containerHeight = 400,
  overscan = 10
}) => {
  const [scrollTop, setScrollTop] = useState(0);
  const tableHeight = containerHeight - headerHeight;
  
  // 计算可见行
  const { visibleData, totalHeight, offset, startIndex } = useMemo(() => {
    const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
    const visibleRowCount = Math.ceil(tableHeight / rowHeight);
    const endIndex = Math.min(
      startIndex + visibleRowCount + overscan * 2,
      data.length - 1
    );
    
    return {
      visibleData: data.slice(startIndex, endIndex + 1),
      totalHeight: data.length * rowHeight,
      offset: startIndex * rowHeight,
      startIndex
    };
  }, [data, scrollTop, rowHeight, tableHeight, overscan]);
  
  const handleScroll = useCallback((e) => {
    setScrollTop(e.target.scrollTop);
  }, []);
  
  return (
    <div className="virtualized-table">
      {/* 表头 */}
      <div 
        className="table-header"
        style={{ 
          height: headerHeight,
          display: 'grid',
          gridTemplateColumns: columns.map(col => col.width || '1fr').join(' ')
        }}
      >
        {columns.map((column, index) => (
          <div key={column.key} className="header-cell">
            {column.title}
          </div>
        ))}
      </div>
      
      {/* 表格主体 */}
      <div
        style={{
          height: tableHeight,
          overflow: 'auto',
          position: 'relative'
        }}
        onScroll={handleScroll}
      >
        <div style={{ height: totalHeight, position: 'relative' }}>
          <div style={{ transform: `translateY(${offset}px)` }}>
            {visibleData.map((row, relativeIndex) => (
              <div
                key={row.id}
                className="table-row"
                style={{
                  height: rowHeight,
                  display: 'grid',
                  gridTemplateColumns: columns.map(col => col.width || '1fr').join(' '),
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  right: 0,
                  transform: `translateY(${relativeIndex * rowHeight}px)`
                }}
              >
                {columns.map(column => (
                  <div key={column.key} className="table-cell">
                    {column.render ? column.render(row[column.dataIndex], row, startIndex + relativeIndex) : row[column.dataIndex]}
                  </div>
                ))}
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
};

4.2 高级功能:排序、筛选、分页

const AdvancedVirtualizedTable = ({
  columns,
  data: initialData,
  rowHeight = 48,
  containerHeight = 500
}) => {
  const [data, setData] = useState(initialData);
  const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
  const [filters, setFilters] = useState({});
  const [selectedRows, setSelectedRows] = useState(new Set());
  
  // 处理排序
  const handleSort = useCallback((key) => {
    setSortConfig(current => ({
      key,
      direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc'
    }));
  }, []);
  
  // 处理筛选
  const handleFilter = useCallback((key, value) => {
    setFilters(current => ({
      ...current,
      [key]: value
    }));
  }, []);
  
  // 处理行选择
  const handleRowSelect = useCallback((rowId) => {
    setSelectedRows(current => {
      const newSet = new Set(current);
      if (newSet.has(rowId)) {
        newSet.delete(rowId);
      } else {
        newSet.add(rowId);
      }
      return newSet;
    });
  }, []);
  
  // 处理全选
  const handleSelectAll = useCallback(() => {
    setSelectedRows(current => {
      if (current.size === processedData.length) {
        return new Set();
      } else {
        return new Set(processedData.map(row => row.id));
      }
    });
  }, [processedData]);
  
  // 处理数据转换
  const processedData = useMemo(() => {
    let result = [...data];
    
    // 应用筛选
    Object.entries(filters).forEach(([key, value]) => {
      if (value) {
        result = result.filter(row => 
          String(row[key]).toLowerCase().includes(value.toLowerCase())
        );
      }
    });
    
    // 应用排序
    if (sortConfig.key) {
      result.sort((a, b) => {
        const aValue = a[sortConfig.key];
        const bValue = b[sortConfig.key];
        
        if (aValue < bValue) {
          return sortConfig.direction === 'asc' ? -1 : 1;
        }
        if (aValue > bValue) {
          return sortConfig.direction === 'asc' ? 1 : -1;
        }
        return 0;
      });
    }
    
    return result;
  }, [data, filters, sortConfig]);
  
  // 增强的列配置
  const enhancedColumns = useMemo(() => [
    {
      key: 'selection',
      width: '60px',
      title: (
        <input
          type="checkbox"
          checked={selectedRows.size === processedData.length && processedData.length > 0}
          onChange={handleSelectAll}
        />
      ),
      render: (_, row) => (
        <input
          type="checkbox"
          checked={selectedRows.has(row.id)}
          onChange={() => handleRowSelect(row.id)}
        />
      )
    },
    ...columns.map(column => ({
      ...column,
      title: (
        <div className="column-header">
          <span>{column.title}</span>
          <button 
            onClick={() => handleSort(column.dataIndex)}
            className={`sort-button ${
              sortConfig.key === column.dataIndex ? sortConfig.direction : ''
            }`}
          >
            ↕️
          </button>
        </div>
      )
    }))
  ], [columns, sortConfig, selectedRows, processedData.length, handleSort, handleSelectAll, handleRowSelect]);
  
  return (
    <div className="advanced-virtualized-table">
      {/* 筛选器 */}
      <div className="table-filters">
        {columns.map(column => (
          <input
            key={column.dataIndex}
            placeholder={`筛选 ${column.title}...`}
            value={filters[column.dataIndex] || ''}
            onChange={(e) => handleFilter(column.dataIndex, e.target.value)}
          />
        ))}
      </div>
      
      {/* 虚拟化表格 */}
      <VirtualizedTable
        columns={enhancedColumns}
        data={processedData}
        rowHeight={rowHeight}
        containerHeight={containerHeight}
      />
      
      {/* 表格统计 */}
      <div className="table-stats">
        显示 {processedData.length} 行,已选择 {selectedRows.size} 行
      </div>
    </div>
  );
};

五、性能优化和最佳实践

5.1 内存管理和垃圾回收

class VirtualListMemoryManager {
  constructor() {
    this.cache = new Map();
    this.cleanupThreshold = 1000; // 缓存项数阈值
    this.accessCount = new Map();
  }
  
  // 缓存渲染项
  cacheItem(index, element) {
    if (this.cache.size > this.cleanupThreshold) {
      this.cleanup();
    }
    
    this.cache.set(index, element);
    this.accessCount.set(index, (this.accessCount.get(index) || 0) + 1);
  }
  
  // 获取缓存项
  getCachedItem(index) {
    const item = this.cache.get(index);
    if (item) {
      this.accessCount.set(index, (this.accessCount.get(index) || 0) + 1);
    }
    return item;
  }
  
  // 清理不常用的缓存
  cleanup() {
    const entries = Array.from(this.accessCount.entries());
    
    // 按访问频率排序,移除访问最少的项
    entries.sort(([, a], [, b]) => a - b);
    
    const toRemove = entries.slice(0, Math.floor(entries.length * 0.2)); // 移除20%
    
    toRemove.forEach(([index]) => {
      this.cache.delete(index);
      this.accessCount.delete(index);
    });
  }
  
  // 清除指定范围的缓存
  clearRange(start, end) {
    for (let i = start; i <= end; i++) {
      this.cache.delete(i);
      this.accessCount.delete(i);
    }
  }
}

5.2 滚动性能优化

const OptimizedScrollHandler = ({ onScroll, throttleMs = 16 }) => {
  const lastScrollTop = useRef(0);
  const rafId = useRef();
  const lastCallTime = useRef(0);
  
  const handleScroll = useCallback((e) => {
    const scrollTop = e.target.scrollTop;
    
    // 使用requestAnimationFrame + 节流
    if (rafId.current) {
      cancelAnimationFrame(rafId.current);
    }
    
    const now = Date.now();
    if (now - lastCallTime.current < throttleMs) {
      return;
    }
    
    rafId.current = requestAnimationFrame(() => {
      // 只有当滚动位置真正改变时才触发
      if (scrollTop !== lastScrollTop.current) {
        lastScrollTop.current = scrollTop;
        lastCallTime.current = now;
        onScroll(e);
      }
    });
  }, [onScroll, throttleMs]);
  
  useEffect(() => {
    return () => {
      if (rafId.current) {
        cancelAnimationFrame(rafId.current);
      }
    };
  }, []);
  
  return handleScroll;
};

5.3 预加载和缓存策略

class DataPreloader {
  constructor(pageSize = 100, preloadThreshold = 50) {
    this.pageSize = pageSize;
    this.preloadThreshold = preloadThreshold;
    this.loadedPages = new Set();
    this.loadingPages = new Set();
  }
  
  // 检查是否需要预加载
  checkPreload(currentIndex, totalCount, loadCallback) {
    const currentPage = Math.floor(currentIndex / this.pageSize);
    const visiblePages = this.getVisiblePages(currentIndex);
    
    // 预加载可见页面周围的页面
    const pagesToLoad = this.getPagesToPreload(visiblePages, totalCount);
    
    pagesToLoad.forEach(page => {
      if (!this.loadedPages.has(page) && !this.loadingPages.has(page)) {
        this.loadingPages.add(page);
        this.loadPage(page, loadCallback);
      }
    });
  }
  
  getVisiblePages(currentIndex) {
    const startPage = Math.floor(currentIndex / this.pageSize);
    const visiblePageCount = Math.ceil(this.preloadThreshold / this.pageSize);
    
    return Array.from(
      { length: visiblePageCount * 2 + 1 },
      (_, i) => startPage - visiblePageCount + i
    ).filter(page => page >= 0);
  }
  
  getPagesToPreload(visiblePages, totalCount) {
    const totalPages = Math.ceil(totalCount / this.pageSize);
    
    return visiblePages.filter(page => page < totalPages).slice(0, 3); // 预加载前3个页面
  }
  
  async loadPage(page, loadCallback) {
    try {
      await loadCallback(page * this.pageSize, (page + 1) * this.pageSize);
      this.loadedPages.add(page);
    } catch (error) {
      console.error(`Failed to load page ${page}:`, error);
    } finally {
      this.loadingPages.delete(page);
    }
  }
}

六、实战案例:10万行数据表格

6.1 完整的企业级虚拟列表表格

const EnterpriseVirtualTable = ({
  columns,
  fetchData,
  initialPageSize = 1000,
  rowHeight = 48,
  containerHeight = 600
}) => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 50 });
  
  const dataLoader = useRef(new DataPreloader());
  const virtualizer = useRef(new DynamicSizeVirtualList());
  
  // 加载数据
  const loadData = useCallback(async (startIndex, endIndex) => {
    if (loading) return;
    
    setLoading(true);
    try {
      const newData = await fetchData(startIndex, endIndex);
      
      setData(current => {
        const updated = [...current];
        for (let i = startIndex; i <= endIndex; i++) {
          if (i < updated.length) {
            updated[i] = newData[i - startIndex];
          } else {
            updated[i] = newData[i - startIndex];
          }
        }
        return updated;
      });
      
      // 更新虚拟化器
      if (virtualizer.current) {
        virtualizer.current.initialize(updated.length);
      }
      
      // 检查是否还有更多数据
      setHasMore(newData.length === endIndex - startIndex + 1);
    } catch (error) {
      console.error('Failed to load data:', error);
    } finally {
      setLoading(false);
    }
  }, [loading, fetchData]);
  
  // 处理可见区域变化
  const handleVisibleRangeChange = useCallback((range) => {
    setVisibleRange(range);
    
    // 预加载数据
    dataLoader.current.checkPreload(
      range.start,
      data.length,
      (start, end) => loadData(start, end)
    );
  }, [data.length, loadData]);
  
  // 渲染项
  const renderRow = useCallback((row, index) => {
    if (!row) {
      return (
        <div className="loading-row">
          加载中...
        </div>
      );
    }
    
    return (
      <div className="table-row">
        {columns.map(column => (
          <div key={column.key} className="table-cell">
            {column.render ? column.render(row[column.dataIndex], row, index) : row[column.dataIndex]}
          </div>
        ))}
      </div>
    );
  }, [columns]);
  
  return (
    <div className="enterprise-virtual-table">
      {/* 表格工具栏 */}
      <div className="table-toolbar">
        <div className="table-info">
          总数据量: {data.length} {hasMore ? '+' : ''}
        </div>
        <div className="table-controls">
          <button onClick={() => loadData(0, initialPageSize - 1)}>
            重新加载
          </button>
        </div>
      </div>
      
      {/* 虚拟化表格 */}
      <DynamicVirtualList
        data={data}
        containerHeight={containerHeight}
        estimatedItemHeight={rowHeight}
        renderItem={renderRow}
        onVisibleRangeChange={handleVisibleRangeChange}
        overscan={20}
      />
      
      {/* 加载状态 */}
      {loading && (
        <div className="loading-indicator">
          加载更多数据...
        </div>
      )}
    </div>
  );
};

6.2 性能监控和调试

class VirtualListProfiler {
  constructor() {
    this.metrics = {
      renderTime: [],
      scrollPerformance: [],
      memoryUsage: []
    };
    this.startTime = 0;
  }
  
  startRender() {
    this.startTime = performance.now();
  }
  
  endRender() {
    const renderTime = performance.now() - this.startTime;
    this.metrics.renderTime.push(renderTime);
    
    if (this.metrics.renderTime.length > 100) {
      this.metrics.renderTime.shift();
    }
  }
  
  recordScroll(frameTime) {
    this.metrics.scrollPerformance.push(frameTime);
    
    if (this.metrics.scrollPerformance.length > 60) {
      this.metrics.scrollPerformance.shift();
    }
  }
  
  getPerformanceReport() {
    const averageRenderTime = this.metrics.renderTime.reduce((a, b) => a + b, 0) / this.metrics.renderTime.length;
    const averageFPS = 1000 / (this.metrics.scrollPerformance.reduce((a, b) => a + b, 0) / this.metrics.scrollPerformance.length);
    
    return {
      averageRenderTime: Math.round(averageRenderTime * 100) / 100,
      averageFPS: Math.round(averageFPS * 100) / 100,
      renderCount: this.metrics.renderTime.length,
      memoryUsage: performance.memory ? {
        used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024),
        total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024),
        limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024)
      } : null
    };
  }
  
  logPerformance() {
    const report = this.getPerformanceReport();
    console.log('虚拟列表性能报告:', report);
    return report;
  }
}

七、不同场景的优化策略

7.1 移动端优化

const mobileOptimizations = {
  触控优化: {
    策略: '使用 passive event listeners',
    代码: 'addEventListener("touchstart", handler, { passive: true })'
  },
  内存管理: {
    策略: '更小的缓存大小和缓冲区域',
    配置: 'overscan: 3, cacheSize: 50'
  },
  电池优化: {
    策略: '减少重绘和重排',
    技巧: '使用 transform 和 opacity 动画'
  },
  网络优化: {
    策略: '更小的分页大小',
    配置: 'pageSize: 100'
  }
};

7.2 大数据量优化(100万+)

const massiveDataOptimizations = {
  数据分片: {
    描述: '将数据分成多个文件按需加载',
    实现: '使用 Web Workers 进行后台加载'
  },
  增量渲染: {
    描述: '先渲染骨架屏,再逐步填充数据',
    优势: '极快的首次渲染'
  },
  智能预加载: {
    描述: '基于用户行为预测加载方向',
    算法: '机器学习预测模型'
  },
  压缩传输: {
    描述: '使用二进制格式传输数据',
    格式: 'Protocol Buffers, MessagePack'
  }
};

结论:虚拟列表的最佳实践

通过虚拟列表技术,我们可以轻松处理万级甚至百万级的数据表格,同时保持流畅的用户体验。

关键成功因素

  1. 选择合适的虚拟化策略:固定高度 vs 动态高度
  2. 合理配置缓冲区域:平衡性能和内存使用
  3. 实现智能预加载:基于用户行为预测数据需求
  4. 优化滚动性能:使用防抖和 requestAnimationFrame
  5. 监控和调试:建立完整的性能监控体系

性能指标目标

const performanceTargets = {
  渲染时间: '< 50ms (60FPS)',
  内存使用: '< 100MB (10万行数据)',
  DOM节点: '< 100个 (无论数据量多大)',
  滚动性能: '60 FPS 稳定',
  首次加载: '< 1秒'
};

持续优化方向

虚拟列表技术仍在不断发展,未来的优化方向包括:

  • Web Workers:将计算密集型任务移到后台线程
  • WebAssembly:使用更高效的计算算法
  • 机器学习:智能预测用户滚动行为
  • 新的浏览器API:使用 Content Visibility API 等新特性

记住:虚拟列表不是银弹,而是工具箱中的一件强大工具。合理使用虚拟列表,结合其他优化技术,才能真正解决大数据量渲染的性能问题。

vite项目保存代码后不刷新页面 vite热更新

作者 我爱甜妹
2025年10月24日 10:35

vite项目保存代码后不刷新页面 vite热更新

问题场景

接手了一个新项目,发现修改代码后,控制台显示hmr更新了文件,但本地运行的项目不会自动刷新页面,每次都要手动刷新。

 [vite] (client) hmr update /src/views/Home.vue, /src/views/Home.vue?vue&type=style&index=0&scoped=2dc54a20&lang.scss  

解决办法

我尝试了很多办法,也试过更新vite版本,均未解决。过程中发现在修改vite.config.ts文件时,页面能自动刷新,然后就想到写一个脚本,监听项目文件的改动, 如果文件有改动,就自动更新下vite.config.ts文件,从而达到刷新页面的效果。

这是我的脚本文件,使用前,需要下载一个插件

npm install chokidar -D

监听脚本

  1. 在你的项目跟目录下创建一个scripts/force-refresh.js文件,粘贴以下代码
import fs from 'fs';
import path from 'path';
import chokidar from 'chokidar';

const root = process.cwd();
const viteConfigPath = path.resolve(root, 'vite.config.ts');
console.log(
  `开始监听 ${path.join(root, 'src')}目录下文件是否有改动,改动时将更新 vite.config.ts 以达到刷新页面的效果`,
);

// --- watcher 配置 ---
const watcher = chokidar.watch([path.join(root, 'src')], {
  usePolling: true,
  ignoreInitial: true,
});

let isWriting = false;
let lastUpdated = 0;
const MIN_INTERVAL_MS = 1000;

watcher.on('all', (event, filePath) => {
  console.log(`改动文件: ${event} ${filePath}`);
  if (filePath.includes('vite.config.ts')) return;
  const now = Date.now();
  if (isWriting || now - lastUpdated < MIN_INTERVAL_MS) return;
  triggerUpdate();
});

function triggerUpdate() {
  try {
    const timestamp = new Date().toISOString();
    const text = fs.readFileSync(viteConfigPath, 'utf8');
    const updated = text.replace(
      /\/\/\s*AUTO_REFRESH_MARKER:.*/i,
      `// AUTO_REFRESH_MARKER: ${timestamp}`,
    );
    fs.writeFileSync(viteConfigPath, updated, 'utf8');
    lastUpdated = Date.now();
  } catch (e) {
    console.error(`[force-refresh] ❌ Error updating vite.config.ts`, e);
  }
}
  1. 修改package.json,添加一个启动命令
"dev": "vite",
 "dev-watch": "start /B vite && node scripts/force-refresh.js",

dev是原本有的命令, dev-watch是我们添加的命令, 注意你的force-refresh.js是否放在scripts文件夹下。

拓展

针对这个脚本,你可以自定义要监听的文件目录,文件类型,做到精准监听,具体的自由实现。 邪修办法!!! 有遇到同样问题,查到了原因,真正通过vite知识来解决的,欢迎在评论区留言,给看到这篇文章的同行一个解决思路

为什么给<a>标签设置了download属性, 浏览器没有下载而是打开新标签!!

作者 静待雨落
2025年10月24日 10:34

<a>标签设置了download属性, 浏览器没有下载而是打开新标签,这个问题通常有以下几个原因:

1. 同源策略限制

download 属性只在同源 URL 或 blob/data URL 上有效:

<!-- 同源文件 - 可以下载 -->
<a href="/files/document.pdf" download>下载PDF</a>

<!-- 跨域文件 - 可能在新标签打开 -->
<a href="https://other-domain.com/file.pdf" download>可能不会下载</a>

2. 服务器 MIME 类型设置

检查服务器返回的 Content-Disposition 头:

<!-- 即使设置了download,如果服务器返回的是可预览类型,浏览器可能选择打开 -->
<a href="image.png" download>点击测试</a>

3. 浏览器兼容性

某些浏览器对 download 属性的支持有限制。

解决方案

方案1:使用同源文件

<!-- 确保文件在同一域名下 -->
<a href="/your-file.pdf" download="filename.pdf">下载文件</a>

方案2:通过 JavaScript 处理跨域下载

javascript

// 使用 fetch + blob 方式下载
async function downloadFile(url, filename) {
    try {
        const response = await fetch(url);
        const blob = await response.blob();
        const blobUrl = URL.createObjectURL(blob);
        
        const a = document.createElement('a');
        a.href = blobUrl;
        a.download = filename || 'download';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(blobUrl);
    } catch (error) {
        console.error('下载失败:', error);
    }
}

// 使用示例
downloadFile('https://example.com/file.pdf', 'my-file.pdf');

方案3:服务器端设置响应头

Content-Disposition: attachment; filename="file.pdf"
Content-Type: application/octet-stream

方案4:检查实际代码

<!-- 正确的用法 -->
<a href="file.pdf" download="自定义文件名.pdf">下载</a>

<!-- 可能有问题的情况 -->
<a href="https://其他网站.com/file.pdf" download>可能不会下载</a>
<a href="#" download>缺少href或href无效</a>

调试步骤

  1. 检查浏览器控制台是否有错误信息
  2. 查看网络面板确认文件请求状态
  3. 检查响应头中的 Content-Disposition
  4. 测试不同浏览器看是否是兼容性问题

个人推荐 一般情况下使用方案一

<!-- 假如你的文件名是https://other-domain.com/file.pdf 直接省略域名使用下面的写法-->
<a href="/files/document.pdf" download>下载PDF</a>

JavaScript设计模式(十九)——观察者模式 (Observer)

作者 Asort
2025年10月24日 10:33

引言:观察者模式在现代JavaScript开发中的地位

观察者模式(Observer)是一种行为设计模式,它定义了对象间一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。这种模式的核心价值在于实现了解耦和事件驱动架构,使系统组件能够松散耦合地协同工作。

高级JavaScript开发者需要深入掌握观察者模式,因为它不仅是构建响应式应用的基础,也是理解现代前端框架内部原理的关键。从Vue的响应式系统到Redux的状态管理,观察者模式无处不在。

观察者模式原理与核心概念解析

观察者模式是一种行为设计模式,实现对象间一对多依赖关系,使对象状态变化时自动通知所有依赖对象。在JavaScript中,这种模式通过发布-订阅机制实现,主题维护观察者列表,状态变化时通知所有观察者。

该模式实现松耦合设计,主题无需了解观察者具体实现,只需维护观察者列表并调用其更新方法。与中介者模式不同,观察者模式直接连接主题与观察者,而中介者模式则通过中心化对象管理通信。

在事件驱动架构中,观察者模式是核心组件,允许系统响应异步事件而不阻塞主线程。以下是一个精简实现:

// 主题(Subject)实现
class Subject {
  constructor() {
    this.observers = []; // 观察者列表
  }
  
  // 添加观察者
  addObserver(observer) {
    this.observers.push(observer);
  }
  
  // 通知所有观察者
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

// 观察者(Observer)接口
class Observer {
  update(data) {
    // 子类实现具体更新逻辑
  }
}

这个实现展示了观察者模式的核心机制,主题维护观察者列表并在状态变化时通知它们,实现了高效的事件处理和松耦合设计。

JavaScript观察者模式的基础实现

观察者模式是一种行为设计模式,它建立了对象之间一对多的依赖关系,当主体状态变化时,所有观察者都会收到通知。

使用函数闭包实现的基本观察者模式:

function createSubject() {
  const observers = [];
  return {
    subscribe: observer => observers.push(observer),
    notify: data => observers.forEach(fn => fn(data))
  };
}

基于ES6 Class的实现:

class Subject {
  constructor() { this.observers = []; }
  subscribe(observer) { this.observers.push(observer); }
  notify(data) { this.observers.forEach(obs => obs(data)); }
}

Node.js的EventEmitter提供了一种强大的观察者实现:

const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter();
emitter.on('event', () => console.log('触发'));

可复用观察者类实现:

class Observer {
  constructor() {
    this.handlers = {};
    this.errors = [];
  }
  on(event, handler) {
    this.handlers[event] = this.handlers[event] || [];
    this.handlers[event].push(handler);
    return this;
  }
  emit(event, ...args) {
    (this.handlers[event] || []).forEach(handler => {
      try { handler(...args); } 
      catch(e) { this.errors.push(e); }
    });
  }
}

错误处理是观察者模式的关键部分,应捕获并记录异常,避免一个观察者错误影响其他观察者。

观察者模式的高级变体与扩展

观察者模式的高级变体与扩展提供了更灵活的事件处理机制。推模型与拉模型是两种主要实现方式:推模型中主题主动推送数据给观察者,而拉模型则允许观察者按需获取数据。

// 推模型实现
class PushSubject {
  constructor() {
    this.observers = [];
  }
  notify(data) { // 主动推送数据
    this.observers.forEach(observer => observer.update(data));
  }
}

// 拉模型实现
class PullSubject {
  constructor() {
    this.state = null;
  }
  getState() { // 观察者主动获取
    return this.state;
  }
}

事件队列与优先级处理可以通过队列结构实现,确保事件按优先级顺序处理。多播模式允许一个事件被多个观察者接收,而单播模式则通过事件路由机制将特定事件定向到特定观察者。

// 事件过滤与路由
class EventRouter {
  constructor() {
    this.routes = {};
  }
  
  on(event, filter, callback) {
    if (!this.routes[event]) this.routes[event] = [];
    this.routes[event].push({ filter, callback }); // 添加过滤条件
  }
  
  emit(event, data) {
    (this.routes[event] || []).forEach(({ filter, callback }) => {
      if (!filter || filter(data)) callback(data); // 根据过滤条件执行
    });
  }
}

观察者模式与响应式编程结合,可以创建更强大的数据绑定系统,实现自动化的状态更新和UI响应,使代码更具声明性和可维护性。

前端框架中的观察者模式实践

Vue.js响应式系统通过Object.defineProperty或Proxy实现数据劫持,当数据变化时自动触发视图更新。Vue 2.x中每个属性都有一个Dep类作为观察者,依赖收集时通过getter添加订阅,变化时通过setter通知订阅者。

React虽然没有内置观察者模式,但通过props/state变化触发的组件生命周期(如render)实现了类似效果。useEffect钩子允许在状态变化时执行副作用,类似于观察者的回调函数。

Redux将store作为被观察对象,组件通过subscribe方法订阅状态变化,或使用connect高阶组件自动订阅相关状态。当dispatch触发状态更新时,所有订阅的组件都会重新渲染。

Vue 3的Composition API使用refreactive创建响应式对象,底层基于Proxy实现更精确的依赖追踪。watchEffect函数会自动追踪依赖,并在依赖变化时重新执行,提供更灵活的观察者模式实现。

性能优化方面,可以通过shouldComponentUpdate或React.memo避免不必要的渲染,Vue的computed属性缓存计算结果,Redux的reducer确保不可变更新,减少观察者的无效触发。

观察者模式的优缺点分析与权衡

观察者模式的核心优势在于实现了组件间的解耦,提高了系统的可扩展性与异步处理能力。主题与观察者间无需了解彼此实现,只需约定接口,即可实现灵活的事件通信。

然而,该模式也存在明显问题。不当的实现可能导致内存泄漏,特别是在观察者未正确移除的情况下。此外,当观察者数量庞大时,频繁的通知操作可能形成性能瓶颈。

调试观察者模式时,由于事件传播的异步性和多级性,问题追踪较为困难。可通过实现事件日志系统,并使用命名空间和事件追踪来缓解这一挑战。

该模式适用于事件处理系统、状态变化通知及分布式消息传递等场景。相比之下,发布-订阅模式提供了更松散的耦合,而中介者模式则通过集中化对象处理通信,减少直接依赖。

// 防止内存泄漏的观察者实现
class Subject {
  constructor() {
    this.observers = []; // 存储观察者列表
  }
  
  addObserver(observer) {
    this.observers.push(observer);
  }
  
  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer); // 关键:移除引用
  }
  
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

选择使用观察者模式时,应权衡其利弊,根据具体场景决定是否采用。

观察者模式的最佳实践与高级技巧

事件命名规范与代码组织:采用"模块.动作"格式(如"user.login"),按功能分组管理事件。使用WeakMap避免内存泄漏,确保观察者能被正确垃圾回收。

// 事件命名规范示例
eventBus.on('user.authentication.success', callback);
eventBus.on('user.authentication.failure', errorHandler);

异步观察者实现:通过Promise链处理异步操作,确保错误能被正确传递。

class AsyncObserver {
  notify(data) {
    return Promise.resolve().then(() => {
      // 异步处理逻辑
      if (error) throw error;
    });
  }
}

微前端通信:观察者模式作为微前端间松耦合通信的理想选择,各模块独立部署又能相互通信。

// 微前端事件总线
window.microFrontendEvents = {
  emit: (event, data) => dispatchEvent(new CustomEvent(event, {detail: data})),
  on: (event, callback) => addEventListener(event, callback)
};

性能监控:通过装饰器模式记录观察者调用次数和执行时间,识别性能瓶颈。

function monitor(target, property, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.time(`Observer:${property}`);
    const result = original.apply(this, args);
    console.timeEnd(`Observer:${property}`);
    return result;
  };
}

观察者模式的实战案例与未来趋势

在现代大型单页应用中,观察者模式常用于状态管理和组件通信。例如,Redux使用观察者模式实现store与组件的响应式连接:

// Redux风格的观察者实现
const createStore = (reducer) => {
  let state = reducer();
  const listeners = [];
  
  return {
    dispatch: (action) => {
      state = reducer(state, action);
      listeners.forEach(listener => listener());
    },
    subscribe: (listener) => {
      listeners.push(listener);
      return () => listeners.filter(l => l !== listener);
    }
  };
};

在Node.js中,观察者模式是事件驱动架构的核心,EventEmitter类提供了强大的事件处理机制:

const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => console.log('触发事件'));

Web Workers中,观察者模式用于主线程与工作线程的通信:

// Worker中
self.addEventListener('message', (e) => {
  // 处理消息并返回结果
  self.postMessage(result);
});

Web Components通过自定义事件实现观察者模式:

// 自定义元素中
class MyElement extends HTMLElement {
  connectedCallback() {
    this.dispatchEvent(new CustomEvent('myEvent', { detail: '数据' }));
  }
}

未来,观察者模式与WebAssembly的结合将带来高性能的事件处理系统,特别适合计算密集型应用和复杂交互场景。

总结

观察者模式 (Observer)实现了对象间一对多的依赖关系,有效解耦了事件发布与订阅系统。在现代前端架构中,它与发布订阅模式、中介者模式等结合应用,构建了高效的事件驱动系统。

Git 本地仓库操作指南:将未提交文件复刻至新分支(无需关联远端)

2025年10月24日 09:52

Git 本地仓库操作指南:将未提交文件复刻至新分支(无需关联远端)

在日常开发中,我们常会遇到这样的场景:本地仓库已有开发项目,存在未提交的修改内容,既不想将这些内容直接提交到当前分支,也无需上传至远端仓库,仅需在本地新建分支并将未提交文件完整复刻过去。此时可通过以下步骤高效完成操作,全程仅涉及本地仓库交互,无需依赖远端服务。

一、确认当前未提交的更改内容

在进行分支操作前,首先需明确当前工作区和暂存区中未提交的文件详情,避免遗漏或误操作。打开终端,进入本地项目仓库目录,执行以下命令:

bash

git status

执行后终端会输出两类关键信息:

  1. 已修改但未暂存的文件:标注为 “modified: 文件名”,表示文件已修改但未通过git add加入暂存区;
  2. 未跟踪的文件:标注为 “untracked files: 文件名”,表示新创建的文件尚未被 Git 跟踪。通过该命令可清晰掌握需复刻的内容范围,确保后续操作针对性。

二、新建分支并自动切换(保留未提交内容)

Git 的工作区和暂存区具有 “分支共享” 特性 —— 未提交的修改不会与特定分支绑定,切换分支时会自动跟随到新分支。利用这一特性,我们可通过单条命令完成 “新建分支 + 切换分支”,同时保留未提交文件。

在终端执行以下命令:

bash

git switch -c 新分支名称
  • 命令解析:git switch用于切换分支,-c(全称 create)是 “新建分支” 的参数,紧跟的 “新分支名称” 需自定义(建议遵循项目命名规范,如feature/local-devfix/uncommitted-code);
  • 示例:若需新建名为local-copy-branch的分支,命令为git switch -c local-copy-branch

执行成功后,终端会提示 “Switched to a new branch ' 新分支名称 '”,此时已切换至新分支,且第一步中确认的未提交文件(包括已修改未暂存、未跟踪文件)已完整保留在新分支的工作区 / 暂存区中。

三、在新分支中提交未提交文件

切换到新分支后,需将未提交文件正式提交至新分支的本地仓库,确保这些内容被 Git 持久化跟踪(仅本地生效)。

1. 暂存文件

根据需求选择暂存方式:

  • 暂存所有未提交文件(包括已修改和未跟踪文件):

    bash

    git add .
    

    注意:.代表当前目录,该命令会递归暂存当前仓库下所有未暂存 / 未跟踪的修改,适合需完整复刻所有内容的场景。

  • 选择性暂存指定文件:若无需复刻全部内容,可单独指定文件名暂存,示例:

    bash

    git add 文件名1 文件名2
    

暂存后可再次执行git status验证,此时文件会标注为 “staged: 文件名”,表示已成功加入暂存区。

2. 提交至本地仓库

执行提交命令,为此次提交添加清晰的描述信息(便于后续查看提交历史):

bash

git commit -m "提交说明:将原分支未提交文件复刻至新分支"
  • 提交说明建议:需简洁明了,标注核心操作,如 “feat: 复刻原分支未提交的用户模块代码至 local-copy-branch”;
  • 执行结果:终端会输出提交摘要,包括提交 ID、修改文件数量、新增 / 删除代码行数等,提示 “1 file changed, 2 insertions (+), 1 deletion (-)” 即表示提交成功。

至此,未提交文件已正式存储在新分支的本地仓库中,新分支具备完整的复刻内容。

四、可选操作:切换回原分支(保持原分支纯净)

若后续仍需在原分支开发,可切换回原分支,且原分支会保持创建新分支前的状态 —— 即不包含新分支中提交的内容,确保原分支历史不被干扰。

执行切换命令:

bash

git switch 原分支名称
  • 示例:若原分支为mainmaster,命令为git switch main
  • 状态验证:切换后执行git status,会发现原分支中已无之前的未提交内容,回到未创建新分支时的初始状态。

操作效果与注意事项

最终效果

  • 新分支:包含所有未提交的修改(已通过git commit持久化),可在新分支中继续开发或备份内容;
  • 原分支:保持纯净,无新增提交,不影响原有开发进度;
  • 全程无远端交互:所有操作仅在本地仓库完成,无需git push等远端命令,适合离线开发或本地临时分支需求。

注意事项

  1. 若存在 “暂存区 + 工作区混合修改”(部分文件已git add,部分未暂存),切换分支后两种状态会完整保留,提交时需注意暂存区内容是否正确;
  2. 新分支名称避免与本地已存在的分支重名,若重名会提示 “fatal: A branch named ' 新分支名称 ' already exists”,需更换名称或删除原有分支(删除命令:git branch -d 分支名);
  3. 若未提交内容中包含大型文件或敏感信息,无需担心泄露 —— 全程本地操作,无任何内容上传至远端,安全性可控。

通过以上步骤,可在不依赖远端仓库的前提下,高效实现 “未提交文件本地分支复刻”,既保证了当前分支的纯净性,又能妥善保存未提交内容,适配本地临时开发、代码备份等场景需求。

❌
❌