本文是深入浅出 ahooks 源码系列文章的第二篇,该系列已整理成文档-地址。觉得还不错,给个 star 支持一下哈,Thanks。
本文来讲下 ahooks 的核心 hook —— useRequest。
根据官方文档的介绍,useRequest 是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用 useRequest 就够了。
useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:
这里可以看到 useRequest 的功能是非常强大的,如果让你来实现,你会如何实现?也可以从介绍中看到官方的答案——插件化机制。

如上图所示,我把整个 useRequest 分成了几个模块。
先从入口文件开始,packages/hooks/src/useRequest/src/useRequest.ts。
function useRequest<TData, TParams extends any[]>(
service: Service<TData, TParams>,
options?: Options<TData, TParams>,
plugins?: Plugin<TData, TParams>[],
) {
return useRequestImplement<TData, TParams>(service, options, [
// 插件列表,用来拓展功能,一般用户不使用。文档中没有看到暴露 API
...(plugins || []),
useDebouncePlugin,
useLoadingDelayPlugin,
usePollingPlugin,
useRefreshOnWindowFocusPlugin,
useThrottlePlugin,
useAutoRunPlugin,
useCachePlugin,
useRetryPlugin,
] as Plugin<TData, TParams>[]);
}
export default useRequest;
这里第一(service 请求实例)第二个参数(配置选项),我们比较熟悉,第三个参数文档中没有提及,其实就是插件列表,用户可以自定义插件拓展功能。
可以看到返回了 useRequestImplement 方法。主要是对 Fetch 类进行实例化。
const update = useUpdate();
// 保证请求实例都不会发生改变
const fetchInstance = useCreation(() => {
// 目前只有 useAutoRunPlugin 这个 plugin 有这个方法
// 初始化状态,返回 { loading: xxx },代表是否 loading
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
// 返回请求实例
return new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
// 可以 useRequestImplement 组件
update,
Object.assign({}, ...initState),
);
}, []);
fetchInstance.options = fetchOptions;
// run all plugins hooks
// 执行所有的 plugin,拓展能力,每个 plugin 中都返回的方法,可以在特定时机执行
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
实例化的时候,传参依次为请求实例,options 选项,父组件的更新函数,初始状态值。
这里需要非常留意的一点是最后一行,它执行了所有的 plugins 插件,传入的是 fetchInstance 实例以及 options 选项,返回的结果赋值给 fetchInstance 实例的 pluginImpls。
另外这个文件做的就是将结果返回给开发者了,这点不细说。
接下来最核心的源码部分 —— Fetch 类。其代码不多,算是非常精简,先简化一下:
export default class Fetch<TData, TParams extends any[]> {
// 插件执行后返回的方法列表
pluginImpls: PluginReturn<TData, TParams>[];
count: number = 0;
// 几个重要的返回值
state: FetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};
constructor(
// React.MutableRefObject —— useRef创建的类型,可以修改
public serviceRef: MutableRefObject<Service<TData, TParams>>,
public options: Options<TData, TParams>,
// 订阅-更新函数
public subscribe: Subscribe,
// 初始值
public initState: Partial<FetchState<TData, TParams>> = {},
) {
this.state = {
...this.state,
loading: !options.manual, // 非手动,就loading
...initState,
};
}
// 更新状态
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}
// 执行插件中的某个事件(event),rest 为参数传入
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
// 省略代码...
}
// 如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 run 或者 runAsync 来触发执行。
// runAsync 是一个返回 Promise 的异步函数,如果使用 runAsync 来调用,则意味着你需要自己捕获异常。
async runAsync(...params: TParams): Promise<TData> {
// 省略代码...
}
// run 是一个普通的同步函数,其内部也是调用了 runAsync 方法
run(...params: TParams) {
// 省略代码...
}
// 取消当前正在进行的请求
cancel() {
// 省略代码...
}
// 使用上一次的 params,重新调用 run
refresh() {
// 省略代码...
}
// 使用上一次的 params,重新调用 runAsync
refreshAsync() {
// 省略代码...
}
// 修改 data。参数可以为函数,也可以是一个值
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
// 省略代码...
}
在 constructor 中,主要是进行了数据的初始化。其中维护的数据主要包含一下几个重要的数据以及通过 setState 方法设置数据,设置完成通过 subscribe 调用通知 useRequestImplement 组件重新渲染,从而获取最新值。
// 几个重要的返回值
state: FetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};
// 更新状态
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}
上文有提到所有的插件运行的结果都赋值给 pluginImpls。它的类型定义如下:
export interface PluginReturn<TData, TParams extends any[]> {
onBefore?: (params: TParams) =>
| ({
stopNow?: boolean;
returnNow?: boolean;
} & Partial<FetchState<TData, TParams>>)
| void;
onRequest?: (
service: Service<TData, TParams>,
params: TParams,
) => {
servicePromise?: Promise<TData>;
};
onSuccess?: (data: TData, params: TParams) => void;
onError?: (e: Error, params: TParams) => void;
onFinally?: (params: TParams, data?: TData, e?: Error) => void;
onCancel?: () => void;
onMutate?: (data: TData) => void;
}
除了最后一个 onMutate 之外,可以看到返回的方法都是在一个请求的生命周期中的。一个请求从开始到结束,如下图所示:

如果你比较仔细,你会发现基本所有的插件功能都是在一个请求的一个或者多个阶段中实现的,也就是说我们只需要在请求的相应阶段,执行我们的插件的逻辑,就能完成我们插件的功能。
执行特定阶段插件方法的函数为 runPluginHandler,其 event 入参就是上面 PluginReturn key 值。
// 执行插件中的某个事件(event),rest 为参数传入
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
// @ts-ignore
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
return Object.assign({}, ...r);
}
通过这样的方式,Fetch 类的代码会变得非常的精简,只需要完成整体流程的功能,所有额外的功能(比如重试、轮询等等)都交给插件去实现。这么做的优点:

可以看到 runAsync 是运行请求的最核心方法,其他的方法比如 run/refresh/refreshAsync 最终都是调用该方法。
并且该方法中就可以看到整体请求的生命周期的处理。这跟上面插件返回的方法设计是保持一致的。

处理请求前的状态,并执行 Plugins 返回的 onBefore 方法,并根据返回值执行相应的逻辑。比如,useCachePlugin 如果还存于新鲜时间内,则不用请求,返回 returnNow,这样就会直接返回缓存的数据。
this.count += 1;
// 主要为了 cancel 请求
const currentCount = this.count;
const {
stopNow = false,
returnNow = false,
...state
// 先执行每个插件的前置函数
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
return new Promise(() => {});
}
this.setState({
// 开始 loading
loading: true,
// 请求参数
params,
...state,
});
// return now
// 立即返回,跟缓存策略有关
if (returnNow) {
return Promise.resolve(state.data);
}
// onBefore - 请求之前触发
// 假如有缓存数据,则直接返回
this.options.onBefore?.(params);
这个阶段只有 useCachePlugin 执行了 onRequest 方法,执行后返回 service Promise(有可能是缓存的结果),从而达到缓存 Promise 的效果。
// replace service
// 如果有 cache 的实例,则使用缓存的实例
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
const res = await servicePromise;
useCachePlugin 返回的 onRequest 方法:
// 请求阶段
onRequest: (service, args) => {
// 看 promise 有没有缓存
let servicePromise = cachePromise.getCachePromise(cacheKey);
// If has servicePromise, and is not trigger by self, then use it
// 如果有servicePromise,并且不是自己触发的,那么就使用它
if (servicePromise && servicePromise !== currentPromiseRef.current) {
return { servicePromise };
}
servicePromise = service(...args);
currentPromiseRef.current = servicePromise;
// 设置 promise 缓存
cachePromise.setCachePromise(cacheKey, servicePromise);
return { servicePromise };
},
刚刚在请求开始前定义了 currentCount 变量,其实为了 cancel 请求。
this.count += 1;
// 主要为了 cancel 请求
const currentCount = this.count;
在请求过程中,开发者可以调用 Fetch 的 cancel 方法:
// 取消当前正在进行的请求
cancel() {
// 设置 + 1,在执行 runAsync 的时候,就会发现 currentCount !== this.count,从而达到取消请求的目的
this.count += 1;
this.setState({
loading: false,
});
// 执行 plugin 中所有的 onCancel 方法
this.runPluginHandler('onCancel');
}
这个时候,currentCount !== this.count,就会返回空数据。
// 假如不是同一个请求,则返回空的 promise
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
这部分也就比较简单了,通过 try...catch...最后成功,就直接在 try 末尾加上 onSuccess 的逻辑,失败在 catch 末尾加上 onError 的逻辑,两者都加上 onFinally 的逻辑。
try {
const res = await servicePromise;
// 省略代码...
this.options.onSuccess?.(res, params);
// plugin 中 onSuccess 事件
this.runPluginHandler('onSuccess', res, params);
// service 执行完成时触发
this.options.onFinally?.(params, res, undefined);
if (currentCount === this.count) {
// plugin 中 onFinally 事件
this.runPluginHandler('onFinally', params, res, undefined);
}
return res;
// 捕获报错
} catch (error) {
// 省略代码...
// service reject 时触发
this.options.onError?.(error, params);
// 执行 plugin 中的 onError 事件
this.runPluginHandler('onError', error, params);
// service 执行完成时触发
this.options.onFinally?.(params, undefined, error);
if (currentCount === this.count) {
// plugin 中 onFinally 事件
this.runPluginHandler('onFinally', params, undefined, error);
}
// 抛出错误。
// 让外部捕获感知错误
throw error;
}
useRequest 是 ahooks 最核心的功能之一,它的功能非常丰富,但核心代码(Fetch 类)相对简单,这得益于它的插件化机制,把特定功能交给特定的插件去实现,自己只负责主流程的设计,并暴露相应的执行时机即可。
这对于我们平时的组件/hook 封装很有帮助,我们对一个复杂功能的抽象,可以尽可能保证对外接口简单。内部实现需要遵循单一职责的原则,通过类似插件化的机制,细化拆分组件,从而提升组件可维护性、可测试性。
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t