阅读视图

发现新文章,点击刷新页面。

使用Fetch API 探索前后端数据交互

image

前言

  在当今的 Web 开发中,前端与后端的数据交互是构建动态应用的核心。API 是连接不同软件应用的重要桥梁,允许开发者通过 HTTP 请求与服务器交互,高效调用API数据对于构建现代 Web 应用至关重要。传统的页面刷新方式已经无法满足用户对流畅体验的需求,而 Fetch API 的出现为 JavaScript 带来了全新的生命力。

一、Fetch API 概述

1.1 Fetch API 是什么❓

  Fetch API 是现代浏览器提供的一个用于发起网络请求的接口,用于发起 HTTP 请求。它提供简洁的异步API,使开发者能够以更现代的方式与服务器交互。它是传统的 XMLHttpRequest 的替代品,提供了更简洁、更强大的功能。基于 Promise 实现,使异步操作更加直观。

image

  相比于传统的 XMLHttpRequest 更加强大、灵活且易于使用。Fetch 基于 Promise 设计,使得异步请求的处理更加优雅。

特性 Fetch API XMLHttpRequest
语法 基于 Promise,更简洁 回调函数,较复杂
请求/响应对象 标准化 非标准化
默认携带 Cookie 不携带 携带
超时控制 需要额外实现 原生支持
取消请求 使用 AbortController 原生支持
进度事件 有限支持 完整支持

1.2 Fetch 的基本语法

  Fetch API 的基本用法是通过调用 fetch() 函数并传入一个 URL 作为参数来发起网络请求。该函数返回一个Promise对象,可以在其then()方法中处理请求成功的情况,在catch()方法中处理请求失败的情况。Fetch API 最基本的形式如下所示。

fetch(url, options)
.then(response => response.json()) // 解析 JSON 数据
  .then(data => console.log(data))   // 处理数据
.catch(error => console.error('出现错误:', error)); // 错误处理

  上述代码示例展示了使用 Fetch API 发起一个请求,返回的 Promise 解析为响应对象,进而能访问响应体数据。处理响应体通常包含 JSON 数据,通过 .json() 方法解析。如果请求失败,fetch 返回的 promise 会拒绝,并将错误信息传给 catch 方法。

1.3 fetch 配置选项

  fetch 接受第二个可选参数,一个可以控制不同配置的对象,常见属性如下表所示。

配置项 简要描述 常用值
method 请求的 HTTP 方法,默认方法为GET GET、POST、PUT、PATCH、DELETE
headers 请求中 HTTP 标头
body 请求体。
请注意,使用 GET 和 HEAD 方法的请求不能有正文
mode 指定请求的模式。 cors:默认值,允许跨域请求
same-origin:只允许同源请求。
no-cors:不能添加跨域的复杂标头,相当于提交表单所能发出的请求
credentials 指定是否发送 Cookie same-origin:默认值,同源请求时发送 Cookie,跨域请求时不发送
include:不管同源请求,还是跨域请求,一律发送 Cookie
omit:一律不发送
cache 指定如何处理缓存 default:默认值,先在缓存里面寻找匹配的请求
no-store:直接请求远程服务器,并且不更新缓存
reload:直接请求远程服务器,并且更新缓存
no-cache:
force-cache:缓存优先,只有不存在缓存的情况下,才请求远程服务器
only-if-cached:只检查缓存,如果缓存里面不存在,将返回504错误
redirect 如何处理 HTTP 重定向响应,默认设置为follow follow、error、manual
referrer 包含请求的反向链接的字符串,默认为空字符串
referrerPolicy 指定用于请求的反向链接政策
signal AbortSignal 对象实例,支持接口中止请求
priority 指定当前请求相对于其他同类请求的优先级,
默认设置为auto
high、low、auto

二、Fetch API 的基本使用

  Fetch API 支持多种 HTTP 请求方法,如GET、POST、PUT、DELETE等。默认情况下,fetch() 函数会发送 GET 请求。如果需要发送其他类型的请求,可以在fetch() 函数的第二个参数中指定请求的配置对象。

2.1 发起 GET 请求

  GET 请求是最常见的请求类型,用于从服务器获取数据。在Fetch API中,构造一个 GET 请求的URL是一件非常简单的事情。首先需要了解的是,GET请求的参数通常是通过URL的查询字符串(query string)部分传递给服务器的。

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('出现错误:', error));

  在上述例子中,fetch 执行 GET 请求,在构建URL时,需要确保查询参数是经过URL编码的,以避免查询字符串解析错误。一旦发起 GET 请求,就需要处理服务器返回的响应数据,Fetch API 返回的 response 是一个 Response 对象,可以使用以下任一方法获取响应内容:

方法 简要说明
response.text() 返回一个使用以文本为响应正文解析的 Promise
response.json() 返回一个使用从 JSON 响应中解析的对象解析的 Promise
response.blob() 返回一个使用以 Blob 对象为响应正文解析的 Promise
response.ArrayBuffer() 返回一个使用以 ArrayBuffer 实例为响应正文解析的 Promise
response.formData() 返回一个使用以 FormData 对象为响应正文解析的 Promise

2.2 发起 POST 请求

  POST 请求用于向服务器发送数据,如提交表单或调用 API 提交数据到服务器。通过 Fetch API 调用 POST 请求需要构造一个包含请求体的对象,并将这个对象作为第二个参数传递给 fetch 函数。

const userData = {
  username: 'example',
  email: 'example@example.com'
};

fetch('https://api.example.com/users', {
  method: 'POST',
  body: JSON.stringify(userData)
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('出现错误:', error));

  使用 fetch() 发送 POST 请求的关键是指定要发送至服务器的数据,它可以采用多种格式,包括 JSON、FormData 和文本格式。在上述示例中,我们通过设置 method 为 POST 来发送 POST 请求,并在请求体 body 中发送 JSON 格式的数据。fetch 函数会将这些信息发送到服务器,并等待响应。

2.3 使用 async/await

  Fetch API 支持 async/await 语法,可以更简洁地处理异步操作,如下所示。fetch 接收到的 response 是一个 Stream 对象,response.json() 是一个异步操作,取出所有内容,并将其转为 JSON 对象。

const response = await fetch(url, options);
const data = await response.json();

三、Fetch API 的响应处理

3.1 处理 HTPP 响应

  fetch 请求成功以后,得到的是一个 Response 对象,它对应服务器的 HTTP 响应。

const res=await fetch(url)

  Response 包含的数据通过 Stream 接口异步读取,但它还有一些同步属性,对应 HTTP 回应的标头信息(Headers),如下表所示。

标头属性 类型 简要说明
Response.ok boolean 表示请求是否成功,true 对应的 HTTP 请求状态码200-299,false对应其他的状态码
Response.status number 返回一个数字,表示HTTP响应的状态码
Response.statusText string 表示HTTP响应的状态信息,例如请求成功以后,服务器返回 OK
Response.url string 返回请求的URL。如果URL存在跳转,该属性返回的是最终的URL
Response.type string 返回的是请求的类型。可能为以下值:
 basic:普通请求,即同源请求
 cors:跨域请求
 error:网络错误,主要用于Service Worker
 opaque:如果fetch请求的type属性为no-cors,就会返回这个值,表示发出的是简单的跨域请求
 opaqueredirect:如果fetch请求的redirect属性设为manual,就会返回这个值
Response.redirected boolean 表示请求是否有过重定向

image

3.2 处理请求响应状态

  在处理请求响应时,我们首先检查响应状态是否成功(response.ok),如果不成功则抛出错误。fetch 发出请求后,只有网络错误或无法连接时才会报错,即使服务器返回的状态码 是4xx或5xx。只有通过 Response.status 属性得到 HTTP 响应的真实状态码时,才能判断请求是否成功。

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error('网络请求错误,' + response.statusText);
    }
    return response.json();
  })
  .then(data => console.log('Success:', data))
  .catch(error => console.error('出现错误:', error));

3.3 处理不同的响应类型

  当接收到服务器的响应后,通常需要解析响应体。Response 对象根据服务器返回的不同类型的数据,提供了不同的读取方法。这几个方法都是异步的,返回的都是 Promise 对象。必须等到异步操作结束,才能得到服务器返回的完整数据。

方法 简要说明
response.text() 获取文本字符串,主要用于获取文本数据,比如 HTML 文件
response.json() 获取 JSON 对象,主要用于获取服务器返回的 JSON 数据
response.blob() 获取二进制 Blob 对象
response.formData() 获取 FormData 表单对象,主要用于拦截用户提交的表单,修改某些数据后再提交给服务器
response.arrayBuffer() 得到二进制 ArrayBuffer 对象,主要用于获取流媒体文件

  Fetch API 可以处理多种响应格式:

// 处理JSON响应
fetch('/api/data.json')
  .then(response => response.json())
  .then(data => console.log(data));

// 处理文本响应
fetch('/api/data.txt')
  .then(response => response.text())
  .then(text => console.log(text));

// 处理Blob响应(如图片)
fetch('/image.png')
  .then(response => response.blob())
  .then(blob => {
    const objectURL = URL.createObjectURL(blob);
    document.getElementById('image').src = objectURL;
  });

注意,Response 是一个 Stream 对象,而 Stream 对象只能读取一次,读取完就没了。这意味着,上面的几个读取方法,只能使用一个,否则会报错。

四、高级 Fetch 用法

4.1 设置请求头

  在使用Fetch API进行请求时,可以通过 Headers 对象来设置请求头。每个请求或响应都有一个与之关联的 Headers 对象,这个对象包含了请求头和响应头,例如 Content-Type、Authorization 等。

fetch('https://example.com/api', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-token'
    },
    body: JSON.stringify({ name: 'John', age: 30 })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('出现错误:', error));

  Response 对象还有一个 Response.headers 属性,指向一个Headers 对象,对应HTTP响应的所有标头。Headers 对象提供了以下方法来操作标头:

方法 简要说明
Headers.get() 根据指定的键名,返回键值
Headers.has() 返回一个布尔值,表示是否包含某个标头
Headers.set() 将指定的键名设置为新的键值,如果该键名不存在则会添加
Headers.append() 添加标头
Headers.delete() 删除标头
Headers.keys() 返回一个遍历器,可以依次遍历所有键名
Headers.values() 返回一个遍历器,可以依次遍历所有键值
Headers.entries() 返回一个遍历器,可以依次遍历所有键值对([key, value])
Headers.forEach() 依次遍历标头,每个标头都会执行一次参数函数

  上面的有些方法可以修改标头,那是因为继承自 Headers 接口。有些标头不能通过headers属性设置,比如Content-Length、Cookie 、Host等等。它们是由浏览器自动生成,无法修改。这些方法中,最常用的是 response.headers.get(),用于读取某个标头的值。

let response =  await  fetch(url);  
response.headers.get('Content-Type')

4.2 设置请求体参数

  当需要发送POST请求时,经常需要向服务器发送一些数据。使用Fetch API可以很便捷地通过 body 属性发送请求体。

// 发送JSON数据
const data = JSON.stringify({ name: 'John', age: 30 });
 
const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  },
  body: data
};
 
fetch('https://example.com/api/users', options)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
 
// 发送表单数据
const formdata = new FormData();
formdata.append('username', 'john');
formdata.append('email', 'john@example.com');
 
fetch('https://example.com/api/register', {
  method: 'POST',
  body: formdata
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

4.3 设置请求超时

  Fetch API 本身不支持超时设置,但可以通过 AbortController 实现:

const controller = new AbortController();
const signal = controller.signal;

// 设置5秒超时
const timeoutId = setTimeout(() => controller.abort(), 5000);

fetch('https://api.example.com/data', { signal })
  .then(response => response.json())
  .then(data => {
    clearTimeout(timeoutId);
    console.log(data);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request timed out');
    } else {
      console.error('Other error:', error);
    }
  });

4.4 跨域请求

  如果需要进行跨域请求,可以在服务器端设置 CORS(Cross-Origin Resource Sharing)。在前端,也可以通过 credentials 选项来指定是否发送 cookies 等凭据。

fetch('https://example.com/api', {
    method: 'GET',
    credentials: 'include' // 允许跨域请求时携带 cookie
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('出现错误:', error));

4.5 上传文件

  如果表单里面有文件选择器,使用 Fetch 上传文件时,可以构造出一个表单,进行上传。

const fileInput = document.querySelector('input[type="file"]');

const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('username', 'exampleUser');

fetch('https://api.example.com/upload', {
  method: 'POST',
  body: formData
  // 注意:不要手动设置Content-Type头,浏览器会自动设置正确的boundary
})
.then(response => response.json())
.then(data => console.log('Upload success:', data))
.catch(error => console.error('Upload error:', error));

4.6 请求取消

  使用 AbortController 取消正在进行的请求:

const controller = new AbortController();

// 开始请求
fetch('https://api.example.com/data', {
  signal: controller.signal
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
  if (error.name === 'AbortError') {
    console.log('Request was aborted');
  } else {
    console.error('Error:', error);
  }
});

// 在某个事件中取消请求
document.getElementById('cancel-button').addEventListener('click', () => {
  controller.abort();
});

4.7 并发请求

  使用 Promise.all 处理多个并发请求:

async function fetchMultipleResources() {
  try {
    const [usersResponse, postsResponse] = await Promise.all([
      fetch('https://api.example.com/users'),
      fetch('https://api.example.com/posts')
    ]);

    if (!usersResponse.ok || !postsResponse.ok) {
      throw new Error('One or more requests failed');
    }

    const users = await usersResponse.json();
    const posts = await postsResponse.json();

    console.log('Users:', users);
    console.log('Posts:', posts);

    // 合并数据并更新UI
    displayCombinedData(users, posts);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

function displayCombinedData(users, posts) {
  // 实现数据合并和显示逻辑
}

五、总结

  Fetch API 是一个强大而简洁的网络请求 API,它基于 Promise 实现,提供了更好的可读性和可维护性。通过学习和掌握 Fetch API 的基本用法和请求方法,我们可以更轻松地发起网络请求并处理响应结果。同时,我们也需要注意 Fetch API 的一些限制和常见问题,并采取相应的措施来解决它们。

image

充电只要 9 分钟!方程豹钛 3 闪充版 15.38 万元起,拒绝「比大更大」

今晚,纯电 SUV 市场又来了一辆新车——

方程豹正式公布了 2026 款钛 3 闪充版的最终价格。

这辆新车有两个版本,主打长续航的 620KM 后驱闪充版售价定在了 15.38 万元;对动力和舒适配置有更高要求的用户可以看看 16.98 万元的 565KM 四驱闪充版。

回看当下 15 万级纯电 SUV 市场的竞争状况,大多数产品都在极力迎合基础的家庭代步需求,厂商们非常习惯于在车身尺寸和价格上做文章,和当年的 iPhone 6 一样,主打一个「比大更大」。

方程豹在钛 3 闪充版给出了一套不太一样的产品思路,不强调「大」,而是强调「个性」。

比亚迪方程豹事业部总经理熊甜波在发布会上表示,汽车不应该只注重功能,而是应该「充满乐趣和生命力」,应该能够与用户的生活发生一点点化学反应。

不要常态,要一反常态。

发布会上的这句话听起来多少有些营销味儿,但放在钛 3 这辆车上,倒是挺贴切的。

充电速度,依旧是最大亮点

我们先看这台车最核心的技术升级部分。

看产品命名就知道,钛 3 闪充版把升级的重点放在了第二代刀片电池上。

根据官方提供的测试数据,方程豹钛 3 在常温环境下电量从 10% 充到 70% 只需要 4 分 59 秒;如果一口气充到 90%,耗时也只要 8 分 45 秒,显著提升车主补能体验。

极寒环境下的充电速度同样给出了明确的数据。在哈尔滨零下 20°C 的气温中,车辆从 20% 充至 97% 的耗时仅为 10 分 47 秒。

方程豹很清楚,超高功率的快充往往会引发用户对电池寿命的担忧,因此,他们也给出了具体的质保承诺:

在六年或十五万公里的使用周期内,只要电池容量保持率低于 77.5%,车主就可以免费更换电池。

与车辆硬件同步推进的还有补能基础设施,比亚迪计划在今年年底前建成两万座闪充站,并顺势推出了一项「点单式建桩服务」。只要有四位车主共同提出需求,官方在评估条件允许后,就会在一周内于指定的公共充电站加装闪充设备。

底盘的机械素质也是发布会花费大量篇幅讲解的内容。

钛 3 闪充版基于智能 EVO+ 平台打造,悬架部分采用了前双球节麦弗逊配合后五连杆的独立结构。结合云辇-C 智能阻尼车身控制系统,这台车能够主动预瞄前方路况,并实时调整悬架阻尼来平衡操控性与舒适度。

针对纯电车型常见的晕车现象,研发团队还提供了一套智能防晕车模式,系统会综合底盘控制、动力平顺性以及新风循环进行多维度调节,以此降低乘客的眩晕感。

主动安全方面的配置也给得很密集,方程豹着重介绍了两套系统。

一个是专门用来应对雨雪结冰等低附着力路面的 iTAC 智能扭矩控制系统,它能在毫秒级识别轮胎抓地力的变化,并进行实时的扭矩分配,防止车辆打滑;另一个是 TSC 高速爆胎辅助稳定控制系统,当车辆在时速 140km/h 的状态下发生爆胎时,系统会瞬间介入控制,帮助驾驶员稳住车身姿态。

考虑到它是一台带有越野风格的 SUV,钛 3 闪充版还搭载了 iATS 智能全地形识别系统。在内置的蠕行模式和豹式掉头功能配合下,车辆完全具备了应对轻度非铺装路面和沙漠地形的通行能力。

钛 3 的内饰在视觉上基本没什么变化,但好在,智能化的升级还是有的。不仅天神之眼 B 驾驶辅助系统开放了自主选装,车机系统也接入了 DeepSeek 大模型,语音交互表现得到提升。

更关键的是,车机 UI 终于能好看些了。

如果你愿意选择 16.98 万元的四驱版本,车内会直接配齐帝瓦雷音响、双温区空调和副驾腿托,并且带有一个支持 -6°C 到 50°C 的冷暖箱,加上车头那个 151L 的电动前备箱,整体的配置丰富度确实拉得比较满。

至于外观细节与周边生态,方程豹这次也花了些心思。

全新的星辰紫车漆加入了高反射率的铝粉,车身在不同的光影下能呈现出更细腻的金属质感。车尾那个 28 升的储物箱也有了更个性的玩法,方程豹顺着流行趋势推出了 16 种 MBTI 个性车贴方案,让原本纯粹的储物空间多了一层社交展示功能。

方程豹还顺势做起了联名,做了一个叫 INNOBAO 的盲盒与和主题车衣。

好不好看另说,单看这些试探年轻圈层的动作,方程豹明显和比亚迪其他子品牌划出了一道分水岭。他们正在投入真实的精力去琢磨年轻人的社交偏好,把这台车,当成一个年轻向的潮流单品来运营。

属于比亚迪的「精品车」

在发布会开始之前,我跟一位钛 3 的意向车主聊了聊。

作为年轻的女性消费者,李壹壹把这台车列入首选清单的理由很简单,她喜欢钛 3 那种恰到好处的「风味越野」。硬派的机甲线条满足了她的审美偏好,而紧凑的车身尺寸又正好切中了日常城市通勤和停车时的便利需求。

品牌光环也是一个无法回避的因素。方程豹早期的产品矩阵已经在市场上树立了相对高端的形象,李壹壹坦言,买这台车多少能满足一点个人的小虚荣心。

「豹 5、豹 8、钛 7 都还不错,方程豹的知名度算是起来了,钛 3 算是给我这种手头没那么宽裕的人准备的吧。」李壹壹说。

聊到最敏感的价格环节,准车主们心里其实都有一本清晰的账。

李壹壹提到,如果新款闪充版只比老款贵上一万块左右,她则完全可以接受,比亚迪提供的一年免费充电已经足够把价格上的落差拉回来。

结合今晚公布的实际售价来看,方程豹的定价策略确实踩准了用户的心理预期。

以 15.38 万元的后驱闪充版作为参考,这个价格仅仅比老款后驱顶配版本高出 4000 元。在这 4000 元的差价里,用户换来的是多出 119 公里的续航里程,以及那套极速闪充技术。

为了进一步刺激购车决策,方程豹还在首发权益上加码:只要在 4 月 13 日前成功下定,原本承诺的一年免费闪充就会被延长至 18 个月。

但要注意的是,这项权益是否能够发挥实际的价值,还是要看比亚迪闪充的铺设情况。以李壹壹目前所在的广东东莞为例,目前的比亚迪闪充站点还并不算密集。

对了,还有一个好消息,不具备闪充功能的现款钛 3 还会继续卖的,价格依旧是 13.38 万到 17.78 万元。那些家里装了充电桩的朋友们可以放心了,这闪充版也不是非买不可。

即便没有二代刀片电池和闪充,钛 3 在这个价位段,也能凭借自身的「态度」来俘获一部分用户。

我们把硬派和潮流结合起来,让每个人都能去拥有独属于自己的风格。

熊甜波在发布会现场说的这段话,其实描绘了一个很清晰的用户画像。这种通过硬核工具去承载生活方式的路径,在消费电子领域其实有一个非常有名的先行者,那就是卡西欧的 G-Shock。

很多年前,卡西欧的工程师伊部菊雄只有一个念头,那就是造一块摔不坏的手表。第一代 G-Shock 诞生的时候,外观甚至有些笨拙,它完全是按照一个坚固工具的标准去打造的。但后来,这块表通过各种多彩的涂装和频繁的跨界联名,慢慢脱离了纯粹的计时属性,变成了街头潮流中一个极具辨识度的符号。

G-Shock 的故事证明,硬核的工具属性和潮流的审美表达,也可以产生很好的共鸣。

现在的方程豹钛 3 也在做类似的尝试。

如果拆开这台车的外壳,你会发现里面都是比亚迪对于技术的执念。底盘、悬架与电控的底层架构让这台紧凑型车具备了一定程度的越野底子,闪充技术与第二代刀片电池的加入,也顺势拉长了它去野外撒野时的半径。

而在这些硬件底座之上,营销团队又在尝试用另一种语境去跟用户对话。

这种策略其实很贴合当下年轻群体的消费特征。他们需要一台足够好开和实用的代步工具,同时也非常在意这台车能不能跟自己的审美态度挂上钩。

当然了,这种反传统的思路最终能否转化成销量,还得看消费者对这种溢价的认可程度。毕竟在一部分人眼里它是充满乐趣的单品,而在另一部分人眼里,它可能是一台不够大的代步车。

这可能就是钛 3 闪充版最鲜明的标签,它给这个卷到窒息的市场提供了一个新的选择,用实打实的技术去支撑一份独特的个性,在激烈的市场里拿走属于自己的那一票。

带轮子的都关注,欢迎交流。 邮箱:tanjiewen@ifanr.com

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


用 Cursor 半年了,效率还是没提升?是因为你没用对这 7 个功能

这篇文章,写给那些已经会用 Cursor、已经在用 AI 辅助编程的前端工程师。 我结合 Cursor 官方文档和实战经验,聊聊哪 7 个功能真正能提升效率。

我踩过的坑

刚开始用 Cursor 的时候,我只会:

"帮我写个登录表单。" "这个报错怎么解决?" "帮我写个 README。"

然后就没有然后了。

我说句扎心的话:这跟你用 ChatGPT 搜代码有啥区别,本质上都是"依赖 AI",只是换了个更智能的工具。

爽了一段时间后,我开始反思:我这几个月效率好像确实提升了,但好像也没有质的飞跃

直到我真正用好这 7 个功能。


1. Rules:一次配置,终身受益

Cursor 官方文档-Rules

效果:不用每次重复解释项目规范

我踩过的坑

刚开始用 Rules,我把所有规范塞进一个文件:

# 项目规范
- 使用 React 18 + TypeScript
- 使用 Tailwind CSS
- 组件放在 components/ 目录
- hooks 放在 hooks/ 目录
- 使用 Zustand 做状态管理
- 使用 TanStack Query 做数据请求
- ...(此处省略100条)

结果:AI 根本记不住,每次还是要重复解释。

正确做法:按场景拆分

.cursor/rules/
├── 01-项目概述.md      # 技术栈、定位
├── 02-编码规范.md      # 命名、格式
├── 03-项目结构.md      # 目录规范
├── 04-组件规范.md      # 组件写法
├── 05-API规范.md       # 接口请求
└── README.md           # 索引

一个真实的 Rules 文件示例

---
description: 编码规范
globs: ["**/*.{ts,tsx,js,jsx}"]
---

# 编码规范

## 命名规范
- 变量/函数:camelCase
- 常量:UPPER_SNAKE_CASE
- 接口/类型:PascalCase
- 组件:PascalCase

## 函数规范
- 优先使用箭头函数
- 使用函数式组件,不使用 class 组件
- 每个函数不超过 50 

## 必写项
- 必写 TypeScript 类型
- 必写错误处理
- 减少 any 的使用

## 禁止项
- 禁止 console.log(用 logger 代替)
- 禁止 var
- 禁止 ==(用 ===)

Rules 的 4 种应用方式

方式 说明 适用场景
Always Apply 每次对话都应用 通用规范
Apply Intelligently AI 自动判断是否相关 中性规范
Apply to Specific Files 特定文件才应用 特定格式
Apply Manual 手动 @ 调用 特定场景

我的效果:配置好之后,再也不用每次解释"我们用 React + TypeScript + Tailwind"。新人加入项目,AI 自动知道规范。


2. Plan Mode:减少返工

Cursor 官方博客-Plan Mode

效果:一次想清楚再做,减少推翻重来

Cursor 的 3 种模式

模式 快捷键 说明
Chat Mode 默认 普通对话,AI 收到指令直接执行
Plan Mode Shift+Tab 计划模式,AI 先做计划等你确认
Agent Mode Ctrl+K Agent 模式,AI 自动执行多步骤任务

我踩过的坑

以前我习惯一次性把需求全丢过去:

"帮我写一个完整的用户管理页面,包含列表,新增、编辑、删除,用 React + TypeScript + Tailwind,需要有权限控制,有表单校验..."

结果:AI 咔咔一顿写,写了几百行代码。然后:

  • 代码有 bug
  • 样式不对
  • 逻辑不对

正确做法:用 Plan Mode

Shift+Tab 开启计划模式,AI 会:

  1. 先研究你的代码库
  2. 问澄清问题
  3. 创建详细的实现计划
  4. 等你批准后再写

什么时候用什么模式?

场景 推荐模式 原因
复杂功能、多文件改动 Plan Mode 先计划,避免返工
简单修改、单文件 Chat Mode 直接执行更快
自动化任务、多步骤 Agent Mode 自动执行完整流程

一个真实的 Plan Mode 对话

我想要做一个用户管理页面,包含:
1. 用户列表(分页、搜索)
2. 新增用户
3. 编辑用户
4. 删除用户(需要二次确认)
5. 权限控制(admin 才能删除)

先帮我分析一下实现思路和步骤。

AI(Plan Mode)

# 用户管理页面实现计划

## 技术方案
- 使用 TanStack Table 做表格
- 使用 React Hook Form + Zod 做表单
- 使用 Radix UI Dialog 做弹窗

## 实现步骤

### 第一步:用户列表页面
- 创建 `src/app/users/page.tsx`
- 实现表格组件,支持分页、搜索

### 第二步:新增用户
- 创建 `src/components/users/UserForm.tsx`
- 实现表单校验

### 第三步:编辑用户
- 复用 UserForm 组件

### 第四步:删除用户
- 添加二次确认弹窗

### 第五步:权限控制
- 根据用户角色控制按钮显示

## 需要确认的问题
1. 分页是前端还是后端?
2. 搜索是按用户名还是邮箱?

:确认方案,AI 开始写代码。

官方提示:保存计划

点击 "Save to workspace" 可以把计划保存到 .cursor/plans/,方便:

  • 团队协作
  • 中断后继续
  • 后续参考

我的效果:以前写 10 个功能,有 6 个要返工;现在 10 个功能,只有 1-2 个需要调整。


3. MCP:闭环工作流

来源:Cursor 官方文档-MCP

效果:AI 能看到真实世界,不只是代码

什么是 MCP?

MCP = Model Context Protocol(模型上下文协议)

你可以理解为:AI 版的"小程序"

  • 手机 App → 需要安装微信、抖音、淘宝...
  • AI 工具 → 需要安装 MCP 来连接各种服务

没有 MCP:AI 只能看到代码,看不到设计稿、报错、日志...

有 MCP:AI 可以读取 Figma、Sentry、GitHub...

官方支持的 MCP

MCP 效果
Figma 设计稿直接转代码
Sentry 报错自动调查原因
GitHub 自动创建 PR
Slack 读取/发送消息
Notion 读取项目文档
Datadog 日志分析

我最常用的 MCP 组合

日常开发

  • Figma → 读设计稿
  • GitHub → 管代码
  • Sentry → 查报错

我的效果:以前要手动复制设计稿、查报错、跑 CI;现在 AI 都能自己完成。


4. 设计稿直出:10 倍速

来源:Cursor 官方博客-include-image

效果:AI 看图写代码,比文字描述快 10 倍

基础用法

直接把设计稿截图发给 AI:

帮我实现这个登录页面

AI 能识别:

  • 布局结构
  • 颜色配色
  • 间距尺寸
  • 字体大小

直接写出对应的 React + Tailwind 代码。

进阶用法:Figma MCP

这是我的最爱,用了之后就回不去了。

第一步:安装 Figma MCP

  1. 打开 Cursor 设置 → MCP
  2. 点击 "Add new MCP server"
  3. 输入 Figma MCP 服务器地址:
https://mcp.figma.com/mcp
  1. 登录 Figma 账号授权

第二步:使用 Figma MCP

读取设计稿

Figma 读取这个文件:https://www.figma.com/file/xxx/xxx

实现页面

根据这个设计稿实现登录页面

问 AI 问题

这个按钮的颜色是什么?
这个组件的间距是多少?

第三步:Figma MCP 能做什么

功能 说明
读取层级结构 获取设计稿的组件树
获取组件属性 颜色、字体、间距、尺寸
导出截图 自动截图给 AI 参考
生成代码 直接输出 React/Vue/HTML 代码

一个真实的使用案例

Figma 读取这个设计稿:https://www.figma.com/file/abc123/Login-Page
根据设计稿实现登录页面

AI

好的,我来分析设计稿...

## 设计稿分析

### 页面结构
- 外层容器:居中,灰色背景 (#F5F5F5)
- 登录卡片:白色背景,圆角 8px,阴影
- 输入框:高度 44px,圆角 4px,灰色边框
- 按钮:主色 #0066FF,白色文字,高度 44px

### 颜色
- 主色:#0066FF
- 背景:#F5F5F5
- 卡片:#FFFFFF
- 文字:#333333
- 副文字:#666666

### 布局
- 页面居中
- 卡片最大宽度 400px
- 内边距 32px
- 输入框间距 16px

我现在开始写代码...

然后 AI 直接写出了完整的登录页面代码。

我的效果:一个页面从 2 小时 → 15 分钟。


5. 自定义命令:告别重复

来源:Cursor 官方博客-git-workflows

效果:一键执行复杂工作流

我踩过的坑

以前每次创建 PR,都要手动敲:

git status
git diff
git add .
git commit -m "feat: xxx"
git push
# 打开浏览器,创建 PR...

每次都要重复 5-10 分钟。

正确做法:自定义命令

创建 .cursor/commands/

.cursor/commands/
├── pr.md        # 自动创建 PR
├── fix-issue.md # 自动修复 Issue
└── review.md   # 自动代码审查

一个真实的 PR 命令

# .cursor/commands/pr.md

创建一个 PR:

## 步骤

1. 用 git status 查看当前状态
2. 用 git diff 查看变更内容
3. 根据变更内容生成合适的 commit message
4. git add . 
5. git commit -m "xxx"
6. git push
7. 用 gh pr create 创建 PR
8. 返回 PR URL

## 要求
- commit message 要清晰描述改动
- PR 标题用中文,描述用英文

使用方式

/pr

AI 自动完成整个流程,最后给你 PR 链接。

常用命令推荐

命令 功能
/pr 自动创建 PR
/fix-issue [number] 自动修复 Issue
/review 运行 linter + 简单审查
/update-deps 更新依赖并测试

我的效果:以前每次都要手动敲 5 条命令,现在 1 条 /pr 搞定。


6. TDD:代码质量

来源:Cursor 官方博客-test-driven-development

效果:AI 有明确目标,代码质量更高

我踩过的坑

以前让 AI 写代码:

  • AI 写完就不管了
  • 没有测试,不知道对不对
  • 后来改需求,全部重写

正确做法:TDD

第一步:先写测试

帮我写一个用户验证的测试用例:
- 输入有效邮箱,返回 true
- 输入无效邮箱,返回 false
- 输入空字符串,抛出异常

使用 Jest + React Testing Library
不要写实现代码,只写测试

第二步:确认测试失败

AI 写出测试后,运行测试,确认失败。

运行测试,确认测试失败

第三步:让 AI 写实现

现在根据测试写实现代码
不要修改测试,只写实现
直到所有测试通过

第四步:提交

测试通过后,提交测试和实现

TDD 的核心优势

AI 需要明确目标才能迭代成功。

  • 写代码没有目标 → AI 随意发挥 → 质量不稳
  • 有测试作为目标 → AI 迭代改进 → 质量保证

我的效果:Bug 减少了 70%,代码可维护性大幅提升。


7. @Branch:不丢状态

来源:Cursor 官方博客-managing-context

效果:AI 始终知道你在做什么

我踩过的坑

  • 对话太长了,AI 开始"失忆"
  • 切换功能后,AI 还在聊之前的话题
  • 每次都要重新解释上下文

正确做法:用好上下文管理

@Branch:了解当前工作

@Branch 这个分支做了什么改动?
@Branch 帮我 review 这个分支的代码

@Past Chats:引用过去对话

@Past Chats 之前那个登录功能是怎么实现的?

适时开启新对话

需要开启新对话的场景

  • 切换到不同的任务/功能
  • AI 看起来困惑或重复犯错
  • 完成了某个逻辑单元的工作

继续当前对话的场景

  • 迭代同一个功能
  • AI 需要之前的上下文
  • 调试刚写好的代码

小技巧:监控对话长度

如果发现 AI 效率下降(开始重复问题、忘记之前的上下文),就是该开新对话的时候了。

我的效果:以前经常"AI 不记得我刚才说什么";现在几乎没有这种情况。


认知升级

很多人还在想:怎么让 AI 帮我写更多代码?

但真正拉开差距的思维是:怎么让 AI 帮我构建 核心竞争力

AI 是 杠杆 ,放大你的能力,不是替代你的能力。

  • AI 写代码,你来审核 → 质量提升
  • AI 写测试,你来设计用例 → 覆盖率提升
  • AI 跑 CI,你来分析结果 → 效率提升

核心永远是:你来做决策,AI 来执行。


写在最后

回到最初的问题:前端工程师还要学 AI 吗?

我的答案是:不是要学,是要精通。

真正拉开差距的,不是"会用 AI",而是"用好 AI"。

这 7 个功能,才是效率翻倍的关键。


参考资料:

如果觉得有帮助,欢迎点赞、收藏。

自动导入 AutoImport:告别手动引入依赖,优化Vue3开发体验

image

前言

  模块化已经是现代 Web 开发必不可少的开发方式,频繁引入依赖包是一个常见的操作。但是,手动引入依赖包往往繁琐,尤其是当依赖包数量较多时,会显著降低开发效率。如果正在用 Vue3 开发项目时,每写一个页面,都要重复引入 ref、reactive等等API。代码开头总是一堆 import 语句,不仅繁琐,还容易因为漏写导致运行时错误。更头疼的是,团队协作时,不同成员可能引入方式不一致,代码风格难以统一。这种重复劳动其实完全可以避免,unplugin-auto-import 插件就是专门解决这个痛点的利器。它能帮助我们在项目中,自动导入常用的使用的第三方库的 API,就可以方便我们开发,提升开发效率。

一、自动导入的价值:不止是少写几行代码

1.1 理解核心

  在深入具体配置之前,我们需要先扭转一个观念:unplugin-auto-import 不仅仅是一个 “帮助开发者写 import 语句” 的工具,更是一个模块解析与依赖管理的智能层。它的核心价值在于,通过声明式的配置,将开发者从繁琐的、重复的模块导入工作中解放出来,同时确保类型安全和代码整洁,但“智能”的前提是精准的规则定义。这感觉就像你每次想用家里的电视遥控器,都得先跑到储物柜里把它拿出来,用完了再放回去。明明遥控器就该放在茶几上,随手就能拿到。unplugin-auto-import 这个插件,干的就是这个“把遥控器放到茶几上”的活儿。它能自动帮你完成这些常用 API 的导入,让你在代码里直接使用 ref、onMounted、useRouter 等,就像它们是天生的全局变量一样。

  并非所以依赖都适合自动导入,项目内的代码可能就不一定适合自动引入。因为自动引入后,就能像全局变量那样直接使用,但从开发的角度就会丢失依赖链路,虽然另外生成了 Typescript 声明文件,IDE 能够正常识别, 但对于新加入项目的小伙伴来说,他们不一定知道是自动引入,因此可能会降低了一些可读性。那么,什么样的内容适合自动引入?简单来说,那些被广泛认知和使用、不用关注实现、不变的内容,不会影响可读性,不会影响开发,不会对开发者心智造成影响,就适合自动引入。

1.2 为什么需要自动导入 🤔

  在传统的前端开发中,我们经常需要手动导入各种函数和组件。在开发过程中我们需要自己去导入ref、reactive、computed等响应式说明,这些重复的导入语句不仅让代码变得冗长,还增加了维护成本。所以为了减少每个文件中的声明,就引入 Auto-Import 去解决这个问题。先看一个对比,这是配置前的典型代码:

<script setup lang="ts">
import { computed, ref } from 'vue';

const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>

  使用 unplugin-auto-import 插件后,不需要再去引入,同样的功能只需要:

<script setup lang="ts">
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>

  变化看似不大,但实际开发中的体验提升是显著的:

  • 减少认知负担:不用再记忆每个 API 来自哪个包,这些细节交给工具处理。
  • 降低出错概率:不会因为忘记导入某个 API 而出现运行时错误,只需要在配置文件中更新预设,所有文件都会自动适应新的导入方式。
  • 提升编码流畅度:新成员加入项目,不用先花时间熟悉项目的 import 规范,直接开始写业务逻辑即可。想到什么直接写,不用在文件顶部和代码主体间来回跳转。
  • 类型安全的保障:对于 TypeScript 项目,unplugin-auto-import 会自动生成类型声明文件(通常是 auto-imports.d.ts),确保即使没有显式导入,也能获得完整的类型提示和检查。

二、从零开始配置:避开那些常踩的坑

  理论说再多,不如动手试一下。我们先来把插件跑起来,感受一下“开箱即用”的爽快感。

2.1 安装依赖

  在开始动手修改配置文件之前,我们有必要先理清几个核心概念和它们之间的关系。对于 Vite 项目,自动导入生态主要依赖于两个社区明星插件:unplugin-vue-components 和 unplugin-auto-import,以及一个由官方提供的“粘合剂”:unplugin-icons。

  • unplugin-vue-components 的职责是 “自动按需引入Vue组件”,这个插件会在背后悄悄帮你完成导入和注册组件这两件事。
  • unplugin-auto-import 的职责则更进一步,它专注于 “自动导入 Composition API、工具函数等”

  在开始复杂配置前,请确保项目已正确安装并集成了 unplugin-auto-import。对于 Vite 项目,基础安装和引入如下:

npm install --save-dev unplugin-vue-components
npm install --save-dev unplugin-auto-import
npm install --save-dev unplugin-icons

  这里安装的是开发依赖,因为自动导入是构建时和开发时工具,不会打包进生产代码。

2.2 基本配置

  安装好依赖只是第一步,正确的配置才是让一切运转起来的关键。我们将在 vite.config.ts 或 vite.config.js 文件中进行配置,这是配置的入口,在其中引入 AutoImport ,同时配置相关信息。

// vite.config.ts
import { defineConfig } from 'vite'
// 1. 引入 auto-import 插件
import AutoImport from 'unplugin-auto-import/vite'
 
export default defineConfig({
  plugins: [
    AutoImport({ /* options */ }),
  ]
})

  还需要在 tsconfig.json 文件里加入。

{
  "include": ["src/**/*.ts", "src/types/**/*.d.ts"]
}

2.3 核心配置

预设支持

  unplugin-auto-import内置了丰富的预设,支持多种流行库和框架,如Vue、Vue-router、pinia等,这些预设可以通过 imports 选项轻松配置:

AutoImport({
  imports: [ // 选择需要配置的插件
    'vue', // 自动导入 Vue 3 的 Composition API,如 ref, reactive, computed 等
    'vue-router', // 自动导入 Vue Router 4 的 API,如 useRouter, useRoute
    'pinia'// 自动导入 Pinia 的 API,如 defineStore, storeToRefs
  ]
})

  看,配置就是这么简单。核心就是 imports 数组,只要把想自动导入的包名写进去就行。

类型定义生成

  解决了运行时的导入问题,接下来是类型系统的挑战。TypeScript 需要知道这些“凭空出现”的标识符的类型是什么,否则就会报红,失去代码提示,unplugin-auto-import通过生成全局类型声明文件来解决这个问题。

  启用dts选项可以自动生成类型定义文件,提升开发体验:

AutoImport({
  // 为 TypeScript 生成全局类型声明文件
  dts: 'src/type/auto-imports.d.ts',  // 或者设置为 true,会在根目录生成
})

  看,配置就是这么简单。dts 选项是关键,它告诉插件为 TypeScript 生成类型声明文件的位置。有了这个文件,IDE 才能正确识别这些自动导入的变量。如果为 true,则会在导入冲突时,生成一个 auto-imports.d.ts 和一个 components.d.ts(如果用了组件自动导入)。也可以设置为一个自定义的文件名,我个人的习惯是把 dts 文件放在 “src/type” 目录下,并把它加入到 .gitignore 中,因为它是生成文件,不应该被提交。

  为了让 TypeScript 识别这个全局声明文件,还需要确保它被包含在 tsconfig.json 的 include 或 files 配置中。通常,生成的路径会自动被 Vite 的 TypeScript 插件处理,但手动检查一下是好的习惯。

{
  "include": [
    "src/type/auto-imports.d.ts" // 确保这一行存在
  ]
}

  这种机制的美妙之处在于,它实现了开发时无感导入完整的类型安全的完美结合。你既享受了代码的简洁,又没有牺牲TypeScript带来的智能提示和错误检查能力。

ESLint集成

  在真实的工程化项目中,unplugin-auto-import 从来不是孤军奋战。它必须与 TypeScript 编译器、ESLint 代码检查工具完美配合,否则就会陷入“代码能跑,但编辑器一片红”的尴尬境地。ESLint 默认规则会检查未声明的变量,自动导入的变量在源代码中没有显式导入,ESLint 会认为它们是未定义的,从而抛出错误。为了避免 ESLint 报错,可以配置自动生成 ESLint 配置文件:

AutoImport({
  eslintrc: {
    enabled: true, // 开启生成ESLint配置的功能
filepath: './.eslintrc-auto-import.json', // 指定生成的配置文件路径
globalsPropValue: true, // 声明为全局只读变量
  }
})

  插件会在项目根目录生成类型文件 .eslintrc-auto-import.json ,确保该文件在 eslint 配置中被 extends。

// .eslintrc.js 或 .eslintrc.cjs
module.exports = {
  // ... 其他配置
  extends: [
    // ... 其他扩展
    // 添加这行
    './.eslintrc-auto-import.json' // 这是插件生成的另一个文件
  ]
}

  unplugin-auto-import 在生成 dts 文件的同时,通常也会在项目根目录生成一个 .eslintrc-auto-import.json 文件,里面定义了所有自动导入变量的规则。把它包含进你的 ESLint 配置,ESLint 就知道这些变量是全局可用的,不会报错误。

三、深度定制:让自动导入更贴合项目

  基础配置只能算“能用”,但要想“好用”,还得根据项目情况深度定制。unplugin-auto-import 提供了非常灵活的配置项,让我们一起来看看。

3.1 导入更多生态库

  现代 Vue 3 项目很少只用到核心库,工具库如 VueUse,UI 组件库如 Element Plus 的某些工具函数,都可以纳入自动导入。

import AutoImport from 'unplugin-auto-import/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import IconsResolver from 'unplugin-icons/resolver';

AutoImport({
  imports: [
    'vue',
    'vue-router',
    'pinia',
    // 添加 VueUse,它是一个函数工具集合
    '@vueuse/core',
  ],
   resolvers: [
      // 自动导入 Element Plus 相关函数
      ElementPlusResolver(),
      IconsResolver({
        prefix: 'Icon'
      })
    ],
  dts: 'src/auto-imports.d.ts',
})

  对于 Element Plus 的这类 API,配置会稍微复杂一点,通常需要结合 unplugin-vue-components(用于自动导入组件)和 resolvers 选项,但核心思想不变:把重复的 import 从代码中抹去。

3.2 自动导入项目本地工具函数

  这才是提升团队开发效率的大杀器。想象一下,项目里封装了很多好用的 useFetch、useTable、useModal 这样的组合式函数,散落在 src/hooks 目录下。以前用的时候,总要去找路径然后导入。现在,可以全部自动导入。

AutoImport({
  imports: [ ... ], // 第三方库
  dts: 'src/auto-imports.d.ts',
  // 关键配置:自动扫描指定目录下的文件
  dirs: [
    './src/stores', // 自动导入 Pinia store 的 useStore 函数
    './src/utils', // 自动导入工具函数
    './src/hooks/**', // 使用 glob 模式匹配子目录
  ],
})

  配置好 dirs 后,插件会在构建时扫描这些目录下的 ts、js 文件,将默认导出的函数或变量自动添加到全局可用列表中。比如在 src/hooks/useDarkMode.ts 里导出了一个 useDarkMode 函数,那么在任意组件中,你就可以直接 const { isDark, toggle } = useDarkMode(),无需导入。

3.3 精细化控制

  随着导入的东西越来越多,生成的 auto-imports.d.ts 文件可能会非常庞大,有时会影响 IDE 性能(虽然通常影响不大),这就可以通过一些配置进行优化。

按需导入

  如果你觉得 VueUse 全部导入太多,可以只导入你确定会用到的。

imports: [
  'vue',
  'vue-router',
  'pinia',
  {
    '@vueuse/core': [
      'useMouse',
      'useLocalStorage',
      'useDark',
      'useClipboard',
      'useDebounceFn',
    ],
  }
]

解决命名冲突

  如果两个库导出了同名的函数(虽然少见),或者你不想用默认的变量名,可以使用 alias 配置别名。

AutoImport({
  imports: [
    { 'vue-router': ['useRouter', 'useRoute'] },
    { 'my-router': ['useRouter as useMyRouter'] }, // 假设有另一个库
  ]
})

四、总结

  unplugin-auto-import作为一款强大的自动导入工具,通过智能化的模块分析和导入管理,彻底改变了传统的手动import方式。它不仅支持多种构建工具和框架,还提供了丰富的自定义选项,满足不同项目的需求。无论是小型应用还是大型项目,unplugin-auto-import都能显著提升开发效率,让开发者更专注于业务逻辑的实现。

image

kill Cheatsheet

Basic Syntax

Core kill command forms.

Command Description
kill PID Send SIGTERM (graceful stop) to a process
kill -9 PID Force kill a process (SIGKILL)
kill -1 PID Reload process config (SIGHUP)
kill -l List all available signal names and numbers
kill -0 PID Check if a PID exists without sending a signal

Signal Reference

Signals most commonly used with kill, killall, and pkill.

Signal Number Description
SIGHUP 1 Reload config — most daemons reload settings without restarting
SIGINT 2 Interrupt process — equivalent to pressing Ctrl+C
SIGQUIT 3 Quit and write a core dump for debugging
SIGKILL 9 Force kill — cannot be caught or ignored; always terminates immediately
SIGTERM 15 Graceful stop — default signal; process can clean up before exiting
SIGUSR1 10 User-defined signal 1 — meaning depends on the application
SIGUSR2 12 User-defined signal 2 — meaning depends on the application
SIGCONT 18 Resume a process that was suspended with SIGSTOP
SIGSTOP 19 Suspend process — cannot be caught or ignored
SIGTSTP 20 Terminal stop — equivalent to pressing Ctrl+Z

Kill by PID

Send signals to one or more specific processes.

Command Description
kill 1234 Gracefully stop PID 1234
kill -9 1234 5678 Force kill multiple PIDs at once
kill -HUP 1234 Reload config for PID 1234
kill -STOP 1234 Suspend PID 1234
kill -CONT 1234 Resume suspended PID 1234
kill -9 $(pidof firefox) Force kill all PIDs for a process by name

killall: Kill by Name

Send signals to all processes matching an exact name.

Command Description
killall process_name Send SIGTERM to all matching processes
killall -9 process_name Force kill all matching processes
killall -HUP nginx Reload all nginx processes
killall -u username process_name Kill matching processes owned by a user
killall -v process_name Kill and report which processes were signaled
killall -r "pattern" Match process names with a regex pattern

Background Jobs

Kill jobs running in the current shell session.

Command Description
jobs List background jobs and their job numbers
kill %1 Send SIGTERM to background job number 1
kill -9 %1 Force kill background job number 1
kill %+ Kill the most recently started background job
kill %% Kill the current (most recent) background job

Troubleshooting

Quick fixes for common kill issues.

Issue Check
Process does not stop after SIGTERM Wait a moment, then escalate to kill -9 PID
No such process error PID has already exited; verify with ps -p PID
Operation not permitted Process belongs to another user — use sudo kill PID
SIGKILL has no effect Process is in uninterruptible sleep (D state in ps); only a reboot can free it
Not sure which PID to kill Find it first with pidof name, pgrep name, or ps aux | grep name

Related Guides

Guide Description
kill Command in Linux Full guide to kill options, signals, and examples
How to Kill a Process in Linux Practical walkthrough for finding and stopping processes
pkill Command in Linux Kill processes by name and pattern
pgrep Command in Linux Find process PIDs before signaling
ps Command in Linux Inspect the running process list

移山所需的最少秒数

方法一:二分答案

思路与算法

根据题目描述,如果 $t$ 秒可以使山的高度降低到 $0$,那么任何大于 $t$ 的秒数也可以。因此答案具有单调性,我们可以使用二分查找来解决本题。

对于二分查找的每一步,假设当前猜测的秒数为 $\textit{mid}$,我们需要判断所有工人在 $\textit{mid}$ 秒内能否将山的高度降低 $H = \textit{mountainHeight}$。对于第 $i$ 个工人,他将山的高度降低 $k$ 所需的时间为:

$$
\textit{workerTimes}[i] \cdot (1 + 2 + \cdots + k) = \textit{workerTimes}[i] \cdot \frac{k(k+1)}{2}
$$

因此在 $\textit{mid}$ 秒内,第 $i$ 个工人 $i$ 最多能将山降低的高度,是满足

$$\textit{workerTimes}[i] \cdot \frac{k(k+1)}{2} \leq \textit{mid}$$

的最大正整数 $k$。

令 $\textit{work} = \lfloor \dfrac{\textit{mid}}{\textit{workerTimes}[i]} \rfloor$,其中 $\lfloor \cdot \rfloor$ 表示下取整,则需要满足 $\dfrac{k(k+1)}{2} \leq \textit{work}$,利用求一元二次方程求根公式可得:

$$
k = \left\lfloor \frac{-1 + \sqrt{1 + 8 \cdot \textit{work}}}{2} \right\rfloor
$$

我们将所有工人计算得到的 $k$ 值相加,如果总和大于等于 $H$,则说明 $\textit{mid}$ 秒可以完成任务,应当尝试更少的时间,否则尝试更多的时间。

二分查找的下界为 $1$,上界为 $\max(\textit{workerTimes}) \cdot \dfrac{H(H + 1)}{2}$,即最慢的工人独自完成所有工作所需的时间。

代码

###C++

class Solution {
public:
    long long minNumberOfSeconds(int mountainHeight, vector<int>& workerTimes) {
        int maxWorkerTimes = *max_element(workerTimes.begin(), workerTimes.end());
        long long l = 1, r = static_cast<long long>(maxWorkerTimes) * mountainHeight * (mountainHeight + 1) / 2;
        long long ans = 0;

        while (l <= r) {
            long long mid = (l + r) / 2;
            long long cnt = 0;
            for (int t: workerTimes) {
                long long work = mid / t;
                // 求最大的 k 满足 1+2+...+k <= work
                long long k = (-1.0 + sqrt(1 + work * 8)) / 2 + eps;
                cnt += k;
            }
            if (cnt >= mountainHeight) {
                ans = mid;
                r = mid - 1;
            }
            else {
                l = mid + 1;
            }
        }

        return ans;
    }

private:
    static constexpr double eps = 1e-7;
};

###Python

class Solution:
    def minNumberOfSeconds(self, mountainHeight: int, workerTimes: List[int]) -> int:
        maxWorkerTimes = max(workerTimes)
        l, r, ans = 1, maxWorkerTimes * mountainHeight * (mountainHeight + 1) // 2, 0
        eps = 1e-7
        
        while l <= r:
            mid = (l + r) // 2
            cnt = 0
            for t in workerTimes:
                work = mid // t
                # 求最大的 k 满足 1+2+...+k <= work
                k = int((-1 + ((1 + work * 8) ** 0.5)) / 2 + eps)
                cnt += k
            if cnt >= mountainHeight:
                ans = mid
                r = mid - 1
            else:
                l = mid + 1

        return ans

###Java

class Solution {
    private static final double EPS = 1e-7;
    
    public long minNumberOfSeconds(int mountainHeight, int[] workerTimes) {
        int maxWorkerTimes = 0;
        for (int t : workerTimes) {
            maxWorkerTimes = Math.max(maxWorkerTimes, t);
        }
        
        long l = 1;
        long r = (long) maxWorkerTimes * mountainHeight * (mountainHeight + 1) / 2;
        long ans = 0;
        
        while (l <= r) {
            long mid = (l + r) / 2;
            long cnt = 0;
            for (int t : workerTimes) {
                long work = mid / t;
                // 求最大的 k 满足 1+2+...+k <= work
                long k = (long)((-1.0 + Math.sqrt(1 + work * 8)) / 2 + EPS);
                cnt += k;
            }
            
            if (cnt >= mountainHeight) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        
        return ans;
    }
}

###C#

class Solution {
    private const double EPS = 1e-7;
    
    public long MinNumberOfSeconds(int mountainHeight, int[] workerTimes) {
        int maxWorkerTimes = 0;
        foreach (int t in workerTimes) {
            maxWorkerTimes = Math.Max(maxWorkerTimes, t);
        }
        
        long l = 1;
        long r = (long)maxWorkerTimes * mountainHeight * (mountainHeight + 1) / 2;
        long ans = 0;
        
        while (l <= r) {
            long mid = (l + r) / 2;
            long cnt = 0;
            
            foreach (int t in workerTimes) {
                long work = mid / t;
                // 求最大的 k 满足 1+2+...+k <= work
                long k = (long)((-1.0 + Math.Sqrt(1 + work * 8)) / 2 + EPS);
                cnt += k;
            }
            
            if (cnt >= mountainHeight) {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        
        return ans;
    }
}

###Go

const eps = 1e-7

func minNumberOfSeconds(mountainHeight int, workerTimes []int) int64 {
    maxWorkerTimes := 0
    for _, t := range workerTimes {
        if t > maxWorkerTimes {
            maxWorkerTimes = t
        }
    }
    
    l := int64(1)
    r := int64(maxWorkerTimes) * int64(mountainHeight) * int64(mountainHeight + 1) / 2
    var ans int64 = 0
    
    for l <= r {
        mid := (l + r) / 2
        var cnt int64 = 0
        
        for _, t := range workerTimes {
            work := mid / int64(t)
            // 求最大的 k 满足 1+2+...+k <= work
            k := int64((-1.0 + math.Sqrt(1 + float64(work) * 8)) / 2 + eps)
            cnt += k
        }
        if cnt >= int64(mountainHeight) {
            ans = mid
            r = mid - 1
        } else {
            l = mid + 1
        }
    }
    
    return ans
}

###C

#define EPS 1e-7

long long minNumberOfSeconds(int mountainHeight, int* workerTimes, int workerTimesSize) {
    int maxWorkerTimes = 0;
    for (int i = 0; i < workerTimesSize; i++) {
        if (workerTimes[i] > maxWorkerTimes) {
            maxWorkerTimes = workerTimes[i];
        }
    }
    
    long long l = 1;
    long long r = (long long)maxWorkerTimes * mountainHeight * (mountainHeight + 1) / 2;
    long long ans = 0;
    
    while (l <= r) {
        long long mid = (l + r) / 2;
        long long cnt = 0;
        for (int i = 0; i < workerTimesSize; i++) {
            long long work = mid / workerTimes[i];
            // 求最大的 k 满足 1+2+...+k <= work
            long long k = (long long)((-1.0 + sqrt(1 + work * 8)) / 2 + EPS);
            cnt += k;
        }
        
        if (cnt >= mountainHeight) {
            ans = mid;
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    
    return ans;
}

###JavaScript

const EPS = 1e-7;

var minNumberOfSeconds = function(mountainHeight, workerTimes) {
    const maxWorkerTimes = Math.max(...workerTimes);
    let l = 1;
    let r = maxWorkerTimes * mountainHeight * (mountainHeight + 1) / 2;
    let ans = 0;
    
    while (l <= r) {
        const mid = Math.floor((l + r) / 2);
        let cnt = 0;
        for (const t of workerTimes) {
            const work = Math.floor(mid / t);
            // 求最大的 k 满足 1+2+...+k <= work
            const k = Math.floor((-1.0 + Math.sqrt(1 + work * 8)) / 2 + EPS);
            cnt += k;
        }
        
        if (cnt >= mountainHeight) {
            ans = mid;
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    
    return ans;
}

###TypeScript

const EPS: number = 1e-7;

function minNumberOfSeconds(mountainHeight: number, workerTimes: number[]): number {
    const maxWorkerTimes: number = Math.max(...workerTimes);
    let l: number = 1;
    let r: number = maxWorkerTimes * mountainHeight * (mountainHeight + 1) / 2;
    let ans: number = 0;
    
    while (l <= r) {
        const mid: number = Math.floor((l + r) / 2);
        let cnt: number = 0;
        for (const t of workerTimes) {
            const work: number = Math.floor(mid / t);
            // 求最大的 k 满足 1+2+...+k <= work
            const k: number = Math.floor((-1.0 + Math.sqrt(1 + work * 8)) / 2 + EPS);
            cnt += k;
        }
        
        if (cnt >= mountainHeight) {
            ans = mid;
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    
    return ans;
}

###Rust

const EPS: f64 = 1e-7;

impl Solution {
    pub fn min_number_of_seconds(mountain_height: i32, worker_times: Vec<i32>) -> i64 {
        let mountain_height = mountain_height as i64;
        let max_worker_times = *worker_times.iter().max().unwrap_or(&0) as i64;
        
        let mut l: i64 = 1;
        let mut r: i64 = max_worker_times * mountain_height * (mountain_height + 1) / 2;
        let mut ans: i64 = 0;
        
        while l <= r {
            let mid = (l + r) / 2;
            let mut cnt: i64 = 0;
            
            for &t in &worker_times {
                let work = mid / t as i64;
                // 求最大的 k 满足 1+2+...+k <= work
                let k = ((-1.0 + (1.0 + (work as f64) * 8.0).sqrt()) / 2.0 + EPS) as i64;
                cnt += k;
            }
            
            if cnt >= mountain_height {
                ans = mid;
                r = mid - 1;
            } else {
                l = mid + 1;
            }
        }
        
        ans
    }
}

复杂度分析

  • 时间复杂度:$O(n \log(MH^2))$,其中 $n$ 是数组 $\textit{workerTimes}$ 的长度,$M$ 是数组 $\textit{workerTimes}$ 中的最大值,$H$ 是 $\textit{mountainHeight}$。二分查找需要 $O(\log(MH^2))$ 次迭代,每次迭代遍历所有工人,需要 $O(n)$ 的时间。

  • 空间复杂度:$O(1)$。

《实时渲染》第3章-图形处理单元-3.4可编程着色和及其API的演变

实时渲染

3. 图形处理单元

3.4 可编程着色和及其API的演变

可编程着色框架的想法可以追溯到1984年Cook的《shade trees》[287]。图3.4显示了一个简单的着色器及其相应的着色树。RenderMan着色语言[63, 1804]是在1980年代后期从这个想法发展而来的。它今天仍然用于电影制作渲染,以及其他不断发展的规范,例如开放着色语言(OSL)项目[608]。

图3.4. 一个简单的铜材质着色器的着色树,及其相应的着色器语言程序。(在Cook [287] 之后。)

消费级图形硬件于1996年10月1日由3dfx In-teractive首次成功推出。有关今年的时间表,请参见图3.5。他们的 Voodoo显卡能够以高品质和高性能渲染游戏Quake,因此很快被采用。该硬件自始至终都实现了一个固定功能的流水线。在GPU原生支持可编程着色器之前,曾多次尝试通过多个渲染通道实时实现可编程着色操作。Quake III:Arena脚本语言是1999年该领域第一个广泛的商业成功。正如本章开头提到的,NVIDIA的GeForce256是第一个被称为GPU的硬件,但它不可编程。但是,它是可配置的。

图3.5. 一些API和图形硬件发布的时间表。

2001年初,NVIDIA的GeForce 3是第一个支持可编程顶点着色器的GPU[1049],通过DirectX8.0和对OpenGL的扩展来开放接口。这些着色器是用类似汇编的语言编程的,驱动程序可以将其即时转换为微代码。像素着色器也包含在DirectX 8.0中,但像素着色器缺乏实际的可编程性——支持的有限“程序”被驱动程序转换为纹理混合状态,驱动程序又将硬件“寄存组合器”连接在一起。这些“程序”不仅长度有限(12条指令或更少),而且缺乏重要的功能。Peercy等人从他们对RenderMan的研究[1363]中发现,依赖纹理读取和浮点数据对真正的可编程性至关重要。

此时的着色器不允许流控制(分支),因此必须通过计算两个条件分支并在结果之间选择或插值来模拟条件。DirectX定义了着色器模型 (SM) 的概念,以区分具有不同着色器功能的硬件。2002年,DirectX9.0发布,包括Shader Model 2.0,它具有真正可编程的顶点和像素着色器。使用各种扩展在OpenGL下也公开了类似的功能。添加了对任意依赖纹理读取和16位浮点值存储的支持,最终完成了Peercy等人确定的一组要求。对着色器资源(例如指令、纹理和寄存器的限制有所增加,因此着色器能够实现更复杂的效果。还增加了对流控制的支持。着色器的长度和复杂性不断增加,使得汇编编程模型变得越来越繁琐。幸运的是,DirectX 9.0还包含 HLSL。这种着色语言是由Microsoft与NVIDIA合作开发的。大约在同一时间,OpenGL ARB(架构审查委员会)发布了GLSL,这是一种与OpenGL非常相似的语言 [885]。这些语言深受 C编程语言的语法和设计理念的影响,并包含来自RenderMan着色语言的元素。

Shader Model 3.0于2004年推出,增加了动态流控制,使着色器更加强大。它还将可选功能变成了要求,进一步增加了资源限制,并增加了对顶点着色器中纹理读取的有限支持。2005年末(微软的Xbox 360)和2006年末(索尼电脑娱乐的PLAYSTATION 3系统)推出新一代游戏机时,都配备了Shader Model 3.0级别的GPU。任天堂的Wii游戏机是最后一批著名的固定功能GPU之一,最初于 2006 年底发货。纯粹的固定功能管道在在之后已不复存在。着色器语言已经发展到可以使用各种工具来创建和管理它们的地步。图3.6显示了使用Cook着色树概念的此类工具的屏幕截图。

图3.6. 用于着色器设计的视觉着色器图形系统。各种操作都封装在功能框中,在左侧可选择。选中后,每个功能框都有可调参数,如右图所示。每个功能框的输入和输出相互链接以形成最终结果,显示在中心框的右下方。(截图来自“mental mill”,mental images inc。)

可编程性的下一个重要步骤也在2006年底附近发生。包含在DirectX 10.0[175]中的着色器模型4.0引入了几个主要功能,例如几何着色器和流输出。Shader Model 4.0包括一个适用于所有着色器(顶点、像素和几何)的统一编程模型,即前面描述的统一着色器设计。进一步增加了资源限制,并添加了对整数数据类型(包括按位运算)的支持。OpenGL 3.3中GLSL 3.30的引入提供了类似的着色器模型。

2009年DirectX 11和Shader Model 5.0发布,添加了曲面细分阶段着色器和计算着色器,也称为DirectCompute。该版本还专注于更有效地支持CPU多处理,这是第18.5节中讨论的主题。OpenGL在4.0版中添加了曲面细分,在4.3版中添加了计算着色器。DirectX和OpenGL的发展方式不同。两者都设置了特定版本发布所需的一定级别的硬件支持。Microsoft控制DirectX API,因此直接与AMD、NVIDIA和Intel等独立硬件供应商(IHV)以及游戏开发商和计算机辅助设计软件公司合作,以确定要公开的功能。OpenGL由硬件和软件供应商联盟开发,由非营利组织Khronos Group管理。由于涉及的公司数量众多,API功能通常在DirectX中引入后的某个时间出现在OpenGL的发行版中。但是,OpenGL允许特定于供应商或更通用的扩展,允许在发布正式支持之前使用最新的GPU功能。

API的下一个重大变化是AMD于2013年推出的Mantle API。与视频游戏开发商DICE合作开发,Mantle的想法是剥离大部分图形驱动程序的开销,并将此控制直接交给开发人员。除了这种重构之外,还进一步支持了有效的CPU多处理。这类新的API侧重于大大减少CPU在驱动程序中花费的时间,以及更高效的CPU多处理器支持(第18章)。 Mantle中首创的想法被微软采纳并于2015年作为DirectX 12发布。请注意,DirectX 12并不专注于展示新的GPU功能——DirectX 11.3展示了相同的硬件功能。这两个API都可用于将图形发送到虚拟现实系统,例如Oculus Rift和HTC Vive。然而,DirectX 12是对API的彻底重新设计,更好地映射到现代GPU架构。低开销驱动程序对于CPU驱动程序成本导致瓶颈的应用程序很有用,或者使用更多的CPU处理器来处理图形可以提高性能[946]。从早期的API移植可能很困难,而且一个不成熟的实现会导致较低的性能[249, 699, 1438]。

Apple于2014年发布了自己的低开销API,称为Metal。Metal最初可用于iPhone 5S和iPad Air等移动设备,一年后更新的Macintosh可通过 OS X El Capitan访问。除了效率之外,降低CPU使用率还可以节省电量,这是移动设备的一个重要因素。这个API有自己的着色语言,适用于图形和GPU计算程序。

AMD将其Mantle工作捐赠给了Khronos Group,后者于2016年初发布了自己的新API,称为Vulkan。与OpenGL一样,Vulkan可在多个操作系统上运行。Vulkan使用一种称为SPIRV的新高级中间语言,它用于着色器表示和通用GPU计算。预编译着色器是可移植的,因此可以在支持所需功能的任何GPU上使用[885]。Vulkan也可用于非图形GPU计算,因为它不需要显示窗口[946]。Vulkan与其他低开销驱动程序的一个显着区别在于,它旨在与从工作站到移动设备的各种系统一起使用。

在移动设备上,标准是使用OpenGL ES。“ES”代表嵌入式系统,因为此API是为移动设备开发的。当时的标准OpenGL在其某些调用结构中相当庞大和缓慢,并且需要支持很少使用的功能。OpenGL ES 1.0于2003年发布,是OpenGL 1.3的精简版本,描述了一个固定功能的管道。虽然DirectX的发布与支持它们的图形硬件的发布同步,但为移动设备开发图形支持并没有以同样的方式进行。例如,2010年发布的第一款iPad实现了 OpenGL ES 1.1。2007年,OpenGL ES 2.0规范发布,提供可编程着色。它基于OpenGL 2.0,但没有固定功能组件,因此不向后兼容 OpenGL ES 1.1。OpenGL ES 3.0 于2012 年发布,提供了多个渲染目标、纹理压缩、变换反馈、实例化以及更广泛的纹理格式和模式以及着色器语言改进等功能。OpenGL ES 3.1添加了计算着色器,3.2添加了几何和曲面细分着色器等功能。第23章讨论更详细的移动设备架构。

OpenGL ES的一个分支是基于浏览器的 API WebGL,通过JavaScript调用。该API的第一个版本于2011年发布,可用于大多数移动设备,因为它在功能上等同于OpenGL ES 2.0。与OpenGL一样,扩展可以访问更高级的GPU功能。WebGL 2假定支持OpenGL ES 3.0。

WebGL特别适合在课堂上试验特性或使用:

  • 它是跨平台的,适用于所有个人计算机和几乎所有移动设备。
  • 驱动程序批准由浏览器处理。即使一个浏览器不支持特定的GPU或扩展,通常另一个浏览器会支持。
  • 代码是解释性的,而不是编译性的,开发时只需要一个文本编辑器。 • 大多数浏览器都内置了调试器,可以检查在任何网站上运行的代码。 • 例如,可以通过将程序上传到网站或Github来部署程序。

更高级别的场景图和效果库(例如three.js [218])可以轻松访问各种更复杂的效果的代码,例如阴影算法、后处理效果、基于物理的着色和延迟渲染。

中国东航:控股股东中国东航集团拟增持5亿元至10亿元A股股份

36氪获悉,中国东航公告,公司控股股东中国东方航空集团有限公司(简称“中国东航集团”)于3月13日通过集中竞价方式增持公司A股股份3397.13万股,占公司总股份的0.15%。自本次增持之日起12个月内,中国东航集团拟继续通过上海证券交易所交易系统以集中竞价方式增持。在符合一定市场条件下,中国东航集团拟累计增持A股金额(即本次增持和本次增持计划合计金额)不少于5亿元(含本数),不超过10亿元(不含本数)。

郭明錤:英伟达下一代Rubin平台启动新材测试,PCB升级周期将至

分析师郭明錤最新供应链调查,英伟达已与PCB厂商就下一代覆铜板(CCL)材料M10启动测试,目标应用涵盖Rubin Ultra及Feynman平台的正交背板与交换刀片主板。若M10测试如期推进,量产时间节点锁定在2027年下半年,届时将开启新一轮AI服务器PCB材料的规模化采购周期,相关供应链厂商有望迎来业绩催化窗口。(财联社)

创源股份:董事长陈刚被留置,董事华天获授权代行职责

36氪获悉,创源股份公告,公司董事长陈刚因被有关监察机关留置暂时无法正常履职。为保证公司运作,公司董事会审议通过议案,推举董事华天代为履行董事长及陈刚在董事会相关委员会的职责,代行期限自董事会审议通过之日起至陈刚恢复履职或选举产生新任人员之日止。

赛诺医疗:拟以1500万元—3000万元回购公司股份

36氪获悉,赛诺医疗公告,公司拟以上海浦东发展银行股份有限公司天津分行(简称“浦发银行”)提供的股票回购专项贷款及公司自有资金通过集中竞价交易方式回购股份,用于员工持股计划或股权激励。本次回购股份的资金总额不低于1500万元(含)且不超过3000万元(含),其中浦发银行回购专项贷款金额不超过2700万元(含)。回购价格不超过35.1元/股(含)。
❌