虽然前端总是被戏谑为切图仔,但是不管在什么场景,我相信下面几个问题你一定并不陌生:
- 如何
中断重复请求
? 频次:⭐️⭐️⭐️⭐️⭐️- 图片懒加载时
如何取消已经触发但不在可视区的请求
? 频次:⭐️⭐️⭐️⭐️⭐️- 如何
取消页面中长时间运行或者低优先级的请求
? 频次:⭐️⭐️⭐️⭐️- 页面切换时如何取消之前的请求? 频次:⭐️⭐️⭐️⭐️(虽然实际应用中会同时衡量性能和必要性并不常见,但问题却很典型)
- 搜索或者过滤时,新关键词触发时如何取消之前的请求? 频次:⭐️⭐️⭐️⭐️
- 取消请求底层到
底是如何实现的呢
? 频次:⭐️⭐️⭐️⭐️⭐️取消请求后,服务端还会继续收到请求吗
?频次:⭐️⭐️⭐️⭐️⭐️- AbortController
除了可以取消请求还有其它功能
?(题外加餐😄)
带着上面的问题,开启我们的探索之旅吧?本文主要会介绍前端常见几种取消请求的场景及实现方式
、每种方式都是如何实现的?此外 AbortController可不止能取消请求,除了取消请求还有哪些你不知道的用途呢?
以及AbortController内部实现原理
等。
一、前端常见取消请求的几种方式
1. 使用AbortController(适用于axios、fetch等场景)
AbortController是一个现代 JavaScript API,用于取消一个或多个网络请求,它通过创建一个AbortController
实例,获取其signal
属性,并将signal
传递给请求的配置选项,当需要取消请求时,调用AbortController
实例的abort()
方法。这种方式可以有效控制请求的生命周期,避免不必要的资源消耗。
2. 使用XMLHttpRequest
对象的abort()
方法
XMLHttpRequest
是一种比较传统的发起 HTTP 请求的方式。在创建XMLHttpRequest
对象并发起请求后,可以通过调用该对象的abort方法来取消请求。这种方法在一些老项目或者对兼容性有特定要求的场景中可能会用到。例如:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.send();
// 取消请求
xhr.abort();
3. 使用请求库自带的取消机制
一些流行的请求库(如axios
)提供了专门的取消请求机制。以axios
为例
-
CancelToken方式
通过创建CancelToken
实例,并将其传递给请求配置,可以在需要时取消请求。CancelToken
的原理是创建一个CancelToken
源,这个源包含一个promise
和一个cancel
函数。当调用cancel
函数时,会改变promise
的状态,并且在请求拦截器中监听这个promise
的状态,如果已经被取消,则阻止请求继续发送。
-
AxiosCancel方式(axios 版本 >= 0.22.0)
AxiosCancel
是axios在较新版本中提供的另一种取消请求的方式。它基于AbortController
实现,通过将AbortController
的signal
与请求关联起来,当调用AbortController
的abort()
方法时,正在进行的axios
请求会被取消。这种方式利用了现代浏览器原生支持的AbortController
机制,使得取消请求的实现更加简洁和高效。
下面将结合常见的请求场景,一起来看看到底如何实现?
二、前端常见取消请求的场景及实现
用户切换页面或视图
以 React 应用为例,当用户切换路由时,可能需要取消之前未完成的请求。可以利用React Router
的路由钩子来实现。这里使用AbortController
结合fetch
来取消请求。
- 示例代码:
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
const MyComponent = () => {
const location = useLocation();
const navigate = useNavigate();
const controller = React.useRef(new AbortController());
React.useEffect(() => {
return () => {
controller.current.abort(); // 在组件卸载(路由切换)时取消请求
};
}, []);
const fetchData = () => {
const signal = controller.current.signal;
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求在路由切换时被取消');
} else {
console.error('其他错误', error);
}
});
};
return (
<div>
<button onClick={fetchData}>获取数据</button>
</div>
);
};
取消重复请求
这个可能是最常见的应用场景了,当用户快速多次触发某个请求时,为了减轻服务端压力,可以在 React 中使用一个状态变量来控制请求的取消与发起。这里以axios
的CancelToken
为例。
import React, { useState } from 'react';
import axios from 'axios';
const MyForm = () => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [cancelTokenSource, setCancelTokenSource] = useState(null);
const handleSubmit = (e) => {
e.preventDefault();
if (isSubmitting) {
if (cancelTokenSource) {
cancelTokenSource.cancel('重复请求取消');
}
}
setIsSubmitting(true);
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
setCancelTokenSource(source);
axios.post('https://jsonplaceholder.typicode.com/todos/1', {
cancelToken: source.token
}).then(response => {
setIsSubmitting(false);
console.log('提交成功', response);
}).catch(error => {
setIsSubmitting(false);
if (axios.isCancel(error)) {
console.log('重复请求被取消');
} else {
console.error('提交失败', error);
}
});
};
return (
<form onSubmit={handleSubmit}>
{/* 表单内容 */}
<button type="submit">提交</button>
</form>
);
};
当搜索关键词变更时取消之前的请求
在异步搜索功能中,当用户输入新的搜索关键词时,应取消之前的搜索请求。使用AbortController
与fetch
来实现此场景下的请求取消。
import React, { useState, useEffect } from 'react';
const SearchComponent = () => {
const [searchTerm, setSearchTerm] = useState('');
const controller = React.useRef(new AbortController());
useEffect(() => {
return () => {
controller.current.abort(); // 取消上一次搜索请求
};
}, [searchTerm]);
const handleSearch = () => {
const signal = controller.current.signal;
fetch(`https://jsonplaceholder.typicode.com/todos/1?q=${searchTerm}`, { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('上一次搜索请求被取消');
} else {
console.error('搜索错误', error);
}
});
};
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button onClick={handleSearch}>搜索</button>
</div>
);
};
当过滤条件变更时取消之前的请求
与搜索关键词变更类似,当用户更改过滤条件时,需要取消之前基于旧条件的请求。以下是使用axios
和CancelToken
实现的示例。
import React, { useState } from 'react';
import axios from 'axios';
const FilterComponent = () => {
const [filterOption, setFilterOption] = useState('');
const [cancelTokenSource, setCancelTokenSource] = useState(null);
const handleFilter = () => {
if (cancelTokenSource) {
cancelTokenSource.cancel('过滤条件变更,取消旧请求');
}
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
setCancelTokenSource(source);
axios.get(`https://jsonplaceholder.typicode.com/todos/1?option=${filterOption}`, {
cancelToken: source.token
}).then(response => console.log(response))
.catch(error => {
if (axios.isCancel(error)) {
console.log('旧请求被取消');
} else {
console.error('过滤请求错误', error);
}
});
};
return (
<div>
<select value={filterOption} onChange={(e) => setFilterOption(e.target.value)}>
{/* 过滤选项 */}
</select>
<button onClick={handleFilter}>应用过滤</button>
</div>
);
};
取消长时间运行的请求
对于可能长时间运行的请求(如大数据下载),可以设置一个定时器来决定是否取消请求。这里使用AbortController
和fetch
实现。
import React, { useState, useEffect } from 'react';
const LongRequestComponent = () => {
const [isLoading, setIsLoading] = useState(false);
const controller = React.useRef(new AbortController());
const timeoutId = React.useRef(null);
useEffect(() => {
return () => {
if (timeoutId.current) {
clearTimeout(timeoutId.current);
}
controller.current.abort(); // 取消请求
};
}, []);
const handleLongRequest = () => {
setIsLoading(true);
const signal = controller.current.signal;
const requestPromise = fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('长时间请求被取消');
} else {
console.error('请求错误', error);
}
});
timeoutId.current = setTimeout(() => {
controller.current.abort();
console.log('长时间无响应,取消请求');
}, 5000); // 5 秒后无响应则取消请求
return requestPromise;
};
return (
<div>
{isLoading? (
<div>正在加载...</div>
) : (
<button onClick={handleLongRequest}>获取大数据</button>
)}
</div>
);
};
取消低优先级请求(以图片懒加载为例)
在图片懒加载场景中,大部分实现方案是滑到可视区时加载出现在可视区的图片,但是如果用户滑动非常快,会导致大量已经滑过可视区的图片也需要加载,很显然这不是最佳方案。最佳方案是仅加载可视区内的图片,如果之前的请求还没回来时可以取消
。这里使用IntersectionObserver
和AbortController
。
import React, { useRef, useEffect } from 'react';
const LazyLoadImageComponent = () => {
const imgRef = useRef(null);
const controller = useRef(new AbortController());
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
const signal = controller.current.signal;
fetch(imgRef.current.dataset.src, { signal })
.then(response => response.blob())
.then(blob => {
imgRef.current.src = URL.createObjectURL(blob);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('图片懒加载请求被取消');
} else {
console.error('图片加载错误', error);
}
});
}
});
observer.observe(imgRef.current);
return () => {
observer.disconnect();
controller.current.abort(); // 取消未进入可视区域图片的请求
};
}, []);
return (
<img ref={imgRef} data-src="http://example.com/lazy-image.jpg" alt="懒加载图片" />
);
};
网络切换
在React 应用中,可以通过监听navigator.onLine
事件来处理网络切换时的请求取消。使用AbortController
结合fetch
实现。
import React, { useEffect, useState } from 'react';
const NetworkSensitiveComponent = () => {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const controller = React.useRef(new AbortController());
useEffect(() => {
const updateOnlineStatus = () => setIsOnline(navigator.onLine);
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
return () => {
window.removeEventListener('online', updateOnlineStatus);
window.removeEventListener('offline', updateOnlineStatus);
if (!isOnline) {
controller.current.abort(); // 网络离线时取消请求
}
};
}, [isOnline]);
const fetchData = () => {
if (isOnline) {
const signal = controller.current.signal;
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('网络切换时请求被取消');
} else {
console.error('网络请求错误', error);
}
});
}
};
return (
<div>
<button onClick={fetchData}>获取网络数据</button>
</div>
);
};
网络不稳定
对于网络不稳定情况,可以在请求库(如axios
)中设置超时时间和拦截器来处理请求取消和可能的重试逻辑。
import React, { useState } from 'react';
import axios from 'axios';
axios.defaults.timeout = 3000; // 设置超时时间为 3 秒
axios.interceptors.response.use(null, function (error) {
if (error.code === 'ECONNABORTED') {
console.log('网络超时,请求被取消');
}
return Promise.reject(error);
});
const UnstableNetworkComponent = () => {
const [data, setData] = useState(null);
const fetchData = () => {
axios.get('https://jsonplaceholder.typicode.com/todos/1')
.then(response => setData(response.data))
.catch(error => console.error('请求错误', error));
};
return (
<div>
<button onClick={fetchData}>获取数据</button>
{data && <div>{data}</div>}
</div>
);
};
三、如何实现取消请求呢?
我们先以axios的两种实现方式为例说一下:
CancelToken
的底层原理
- 创建CancelToken 源
CancelToken
通过一个工厂函数创建,这个函数内部返回一个包含promise
和cancel
函数的对象。promise
用于跟踪取消状态,cancel
函数用于触发取消操作。当创建CancelToken
源时,promise
处于未完成状态。
- 请求拦截
在axios
的请求拦截器中,会检查请求配置中是否存在CancelToken
。如果存在,它会将CancelToken
的promise
与请求关联起来。在请求发送之前和整个请求生命周期内,axios
会持续监听这个promise
的状态(通过在Promise的then方法中判断Promise状态的方式监听promise状态)。
- 取消操作
当调用CancelToken
的cancel
函数时,cancel
函数会改变promise
的状态(通常将其标记为已取消)。同时,在请求拦截器中检测到promise
状态改变后,axios
会阻止请求继续发送(我们知道axios内部其实是通过Promist封装了xhr请求,然后取消请求就可以通过XMLHttpRequest的abort方案
取消请求的发布)。
AxiosCancel
(基于AbortController
)的底层原理
-
AbortController
关联AxiosCancel
利用AbortController
的signal
属性。当使用AxiosCancel
时,axios
会将AbortController
的signal
与请求关联起来。AbortController
是浏览器原生提供的用于取消异步操作的对象。 -
请求处理与取消
在
axios
内部,当发起请求时,会将signal
传递给底层的请求实现(无论是基于XMLHttpRequest
还是fetch
等)。如果在请求执行过程中调用了AbortController
的abort()
方法,signal
会触发相应的取消机制。对于支持AbortController
的底层请求实现,请求会立即被取消,包括终止正在进行的网络传输、停止接收服务器响应等操作。这是因为signal
的abort
操作会在整个请求处理链路中传播取消信号,使相关的操作能够及时响应并停止执行,从而实现请求的取消。这种方式利用了现代浏览器的原生功能,提供了一种简洁且高效的取消请求机制。
fetch库如何实现取消请求呢?
仍然需要通过结合AbortController
来实现的:
import React, { useState } from 'react';
import { Input } from 'antd;
const UserInput = () => {
const [controller, setController] = useState(null);
const [keyword, setKeyword] = ('');
const fetchData = () => {
if(controller) {
controller.abort();
setController(new AbortController())
}
// 请求url为伪代码
const list = await fetch('https://jsonplaceholder.typicode.com/todos/1?keyword=${keyword}', { signal: controller.signal }).then((newData) => {setKeyword(res.data)})
};
return (
<div>
<Input value={keyword} onChange={fetchData} />
</div>
);
};
四、总结
综上,我们可以总结一下:
- fetch通过
AbortController
实现 - Axios的AxiosCancel通过
AbortController
实现 - Axios的CancelToken是通过巧妙
使用Promise的状态与请求结合
实现的,而我们知道 Axios内部封装了基于Promise的xhr请求,因此真正的取消是通过XMLHttpRequest的abort方法取消请求的
- XMLHttpRequest是通过自身的
abort方法
实现的
除了Axios的CancelToken,其它都是通过AbortController或者XMLHttpRequest的abort实现,哪种最好呢?取决于对浏览器的版本要求:
- 如果项目主要面向现代浏览器环境,且使用了
fetch
API 或其他支持AbortController
的请求库,注重代码的简洁性、通用性和资源管理效率,那么AbortController
是更好的选择,它能够提供更优雅、更符合现代开发标准的请求取消解决方案(AbortController虽然在19年开始已被各大主流浏览器使用,但是截止发稿,早期浏览器还是不被支持的,AbortController兼容性) - 如果项目需要兼容较老的浏览器版本,或者已经大量使用了
XMLHttpRequest
且对现有代码的改动成本有较高要求,那么XMLHttpRequest
的abort
方法则更为合适,它能够在保证兼容性的前提下,快速实现请求取消功能,满足基本的业务需求。
其实开发中可能使用第一种偏多,其实不管采用哪种方案,在底层浏览器实现原理是会关闭相关的网络套接字,释放与该请求相关的资源,从而阻止请求继续进行
。请注意: 虽然连接被中断,但是服务端依然会收到该请求但是客户端不会继续处理
,这是因为服务器端收到的是一个连接关闭的信号,但不会接收到完整的请求数据,因此无法对该请求进行正常的处理
。
五、AbortController 额外拓展
“我以为AbortController只能取消请求?” No No No,AbortController
的能力可不止于此,AbortController
是 JavaScript 中的一个全局类,它可以用来终止任何异步操作
,
再来回顾下它的用法:
const controller = new AbortController();
controller.signal;
controller.abort();
我们创建一个 AbortController
实例后,会得到两个东西:
signal
属性,这是一个AbortSignal
实例,我们可以将它传递给要中断的 API,来响应中断事件并进行相应处理,例如,传递给fetch()
方法就可以终止这个请求了;.abort()
方法,调用这个方法会触发signal
上的中止事件,并将信号标记为已中止。
我们要了解AbortController的其它功能,就不得不研究一下AbortController内部的实现原理
了,为啥它可以取消一切异步操作?
AbortController内部做了啥?
AbortController
实现取消异步操作的底层原理主要涉及以下几个关键部分:
-
基于事件监听与信号传递机制
当创建AbortController
实例时,它内部会创建一个与之关联的AbortSignal
对象,并维护一个状态来表示是否已被取消。AbortSignal
作为一种信号对象
,其核心是通过事件监听机制来实现与异步操作的通信。具体而言,AbortSignal
具有一个aborted
属性和一个addEventListener
方法,异步操作可以通过监听aborted
事件来获取取消信号。
-
关联控制器与事件
AbortController
的主要作用是作为控制中心
,管理AbortSignal
的状态变化。它持有对AbortSignal
的引用,并提供了一个公开的abort
方法。当调用abort
方法时,AbortController
会将关联的AbortSignal
的aborted
属性设置为true
,同时触发aborted
事件,从而向所有监听该事件的异步操作发送取消信号。 -
监听异步操作中的事件信号并处理
在异步操作中,如fetch
请求、axios
请求或其他自定义的异步任务等,会接收并监听AbortSignal
。以fetch
为例,在发起请求时,可以将AbortSignal
作为选项传递给fetch
函数。fetch
内部会在请求执行过程中不断检查AbortSignal
的aborted
属性状态。一旦该属性变为true
,fetch就会停止请求的发送和接收,释放相关资源,并抛出一个名为AbortError
的错误,以此来表示请求已被取消。
-
精确管理内部状态
AbortController
内部需要精确地管理自身的状态以及与AbortSignal
状态的一致性。在多线程或复杂的异步环境下,要确保abort
方法的调用是线程安全的,并且AbortSignal
的状态变化能够及时、准确地被所有相关的异步操作感知到。这涉及到一些底层的状态机设计和同步机制,以保证在各种情况下,取消操作的执行都是可靠和可预期的。
AbortController
通过巧妙地利用事件监听、信号传递、状态管理实现了一种高效、可靠的异步操作取消机制。
AbortController 还可以做哪些事呢?
取消监听事件(可批量取消)
很多时候如果代码中添加了一些监听事件,我们希望页面不可视或者卸载时可以集中批量处理,又不想使用removeEventListener
方法,就可以使用AbortController优雅的取消
。
useEffect(() => {
const controller = new AbortController();
window.addEventListener('resize', handleResize, {
signal: controller.signal,
});
window.addEventListener('hashchange', handleHashChange, {
signal: controller.signal,
});
window.addEventListener('storage', handleStorageChange, {
signal: controller.signal,
});
return () => {
// 删除所有关联的事件监听器
controller.abort();
};
}, []);
利用AbortSignal.timeout静态方法设置请求超时时取消
我们使用AbortSignal.timeout
静态方法创建一个在经过一定超时时间后会触发中止事件的信号,如果只想在请求超时后取消请求,就不需要创建一个 AbortController
了。
fetch('https://jsonplaceholder.typicode.com/posts/1', {
// 如果请求超过 1700 毫秒则自动中止
signal: AbortSignal.timeout(1700),
})
利用AbortSignal.any静态方法将多个终止信号组合到一个中
类似于 Promise.race()
处理多个 Promise 的方式,我们可以使用 AbortSignal.any()
静态方法将多个中止信号组合到一个里面:
import React, { useState, useRef } from 'react';
import { Button } from 'antd';
const App = () => {
const [taskResult, setTaskResult] = useState('');
// 来两个控制器
const controller1 = useRef(new AbortController());
const controller2 = useRef(new AbortController());
const asyncTask = () => {
//使用AbortSignal.any进行组合
const combinedSignal = AbortSignal.any([controller1.current.signal, controller2.current.signal]);
const taskPromise = new Promise((resolve, reject) => {
const timerId = setTimeout(() => {
resolve('任务正常完成');
}, 3000);
combinedSignal.addEventListener('abort', () => {
clearTimeout(timerId);
reject(new Error('任务被中止'));
});
});
taskPromise.then((result) => setTaskResult(result))
.catch((error) => setTaskResult(error.message));
};
const handleAbort1 = () => controller1.current.abort();
const handleAbort2 = () => controller2.current.abort();
useEffect(() => {
asyncTask();
return () => {
controller1.current.abort();
controller2.current.abort();
};
}, []);
return (
<div>
<Button type="danger" onClick={handleAbort1}>通过按钮1中止任务</Button>
<Button type="danger" onClick={handleAbort2}>通过按钮2中止任务</Button>
<p>{taskResult}</p>
</div>
);
};
export default App;
可能有同学会比较好奇,为啥需要将多个信号组合到一起,这里其实也不难理解:
- 比如在一个在线预订系统中,用户在预订过程中可能会同时与多个预订选项进行交互,而每个选项的操作都可能影响到最终的预订确认异步操作。当用户在某个预订选项上做出取消操作或者整体预订流程出现错误时,都需要终止预订确认的异步任务
- 再比如一个电商商品详情页,需要同时获取商品基本信息、库存信息、用户评价等多个数据来源的异步请求。当用户快速切换到其他商品详情页或者执行其他操作时,需要能够一次性取消所有未完成的相关请求
核心是通过 AbortSignal.any
将全局和局部的 AbortController
信号合并,能够灵活地根据用户的不同操作来控制异步数据获取操作的终止。
取消流
可以使用AbortController取消流的操作,更多关于WritableStream详见 WritableStream
import React, { useRef } from 'react';
import { Button } from 'antd';
const CancelStream = () => {
// 创建一个AbortController的引用,用于控制流的取消操作,通过useRef可以在组件的不同阶段方便地访问和操作它
const controllerRef = useRef(new AbortController());
// 定义处理写入操作的函数
const handleWrite = () => {
// 创建一个WritableStream实例,用于定义可写流的相关行为
const stream = new WritableStream({
write(chunk, controller) {
console.log('正在写入:', chunk);
// 监听AbortSignal的abort事件,当流被取消时会触发此事件,进而执行相应的取消逻辑
controller.signal.addEventListener('abort', () => {
console.log('写操作被取消');
});
},
close() {
console.log('写入完成');
},
abort(reason) {
console.warn('写入中止:', reason);
}
});
// 获取可写流的writer对象,用于执行实际的写入操作
const writer = stream.getWriter();
// 向流中写入一个简单的数据块,模拟实际的写入数据行为
writer.write('数据块');
// 释放写锁,以便后续可以再次进行写入操作等相关操作
writer.releaseLock();
};
// 定义取消写入操作的函数
const handleCancel = () => {
// 检查AbortController是否存在,如果存在则调用abort方法来触发流的取消逻辑
if (controllerRef.current) {
controllerRef.current.abort();
}
};
return (
<div>
{/* 渲染一个Ant Design的Button按钮,点击它将触发handleWrite函数,开始写入操作 */}
<Button type="primary" onClick={handleWrite}>开始写入</Button>
{/* 渲染一个Ant Design的Button按钮,点击它将触发handleCancel函数,取消写入操作 */}
<Button type="danger" onClick={handleCancel}>取消写入</Button>
</div>
);
};
export default CancelStream;
WritableStream
控制器暴露了 signal
属性,即同样的 AbortSignal
。这样,我可以调用 writer.abort()
,这会在流的 write()
方法中的 controller.signal
上冒泡触发中止事件。
此外AbortController还有很多功能,就不在此一一列举了感兴的还可以继续深挖。
创作不易😊,如果本文对你相对有所帮助,那咱点赞收藏支持一个呗?☕️
《重学JavaScript系列专栏》其它文章推荐:
- async 和 defer啥区别?
- 发布订阅者模式原来是这样 “搞事情” 的!
- 你家3岁小孩也能看懂的防抖和节流
- 手动模拟实现call、apply和bind方法---call和apply方法实现
- 手写call、apply、bind方法---bind方法实现
《手撕源码系列专栏》文章推荐
《Webpack配置从基础到高阶系列专栏》文章推荐