[{"data":1,"prerenderedAt":3241},["ShallowReactive",2],{"page-/post/nuxt/nuxt3-fetch-usefetch-useasyncdata":3,"surrounding-page":3232},{"id":4,"title":5,"author":6,"body":7,"date":3219,"description":3220,"extension":3221,"group":6,"lastmod":3222,"meta":3223,"navigation":375,"path":3224,"rawbody":3225,"seo":3226,"showTitle":6,"stem":3227,"tags":3228,"versions":3229,"__hash__":3231},"content/post/nuxt/Nuxt3-fetch-useFetch-useAsyncData.md","Nuxt3全栈开发 · $fetch、useFetch、useAsyncData 你用对了吗？",null,{"type":8,"value":9,"toc":3207},"minimark",[10,14,19,36,39,55,58,61,67,70,78,85,104,110,113,117,128,146,153,167,173,182,264,276,290,295,298,311,508,511,534,537,596,603,650,656,666,670,679,685,690,754,765,771,774,785,801,811,817,820,825,832,899,902,911,918,921,971,974,982,1060,1074,1077,1080,1103,1106,1116,1129,1132,1341,1354,1357,1363,1368,1375,1378,1383,1401,1408,1416,1422,1425,1434,1440,1510,1519,1528,1542,1556,1564,1573,1579,1584,1603,1612,1615,1618,1627,1633,1638,2808,2811,2817,2827,2841,2846,2849,2866,3001,3006,3020,3027,3049,3067,3070,3155,3169,3180,3183,3193,3196,3203],[11,12,13],"p",{},"Nuxt3 中有三种获取数据的方式，看起来有点绕，那实际使用中有什么区别，应该怎样使用呢？",[15,16,18],"h2",{"id":17},"fetch","$fetch",[11,20,21,24,25,28,29,31,32,35],{},[22,23,18],"code",{}," 基于 ",[22,26,27],{},"ofetch"," ，",[22,30,27],{}," 是一个类似 ",[22,33,34],{},"axios"," 的请求库，可以运行在 node、浏览器、workers 上。",[11,37,38],{},"所以它的用法类似原生 fetch 、axios，在 Nuxt3 中全局可用。",[40,41,42,46,49,52],"ul",{},[43,44,45],"li",{},"在 app 中直接向 server 内的 api 发起请求",[43,47,48],{},"在 app 中向其他服务发出请求",[43,50,51],{},"在 server 的一个接口中向另一个接口请求",[43,53,54],{},"在 server 的一个接口中向其他服务发出请求",[11,56,57],{},"总之，是一个底层的请求库。",[11,59,60],{},"用它是最简单的方法。",[11,62,63,64,66],{},"但 Nuxt 是一个可以在服务器和客户端两个环境下运行同构代码的框架，如果在 setup 中使用 ",[22,65,18],{}," 来获取数据，可能会导致执行两次，一次在服务器上由 nitro 渲染 html 时，另一次是在客户端水合时。",[11,68,69],{},"所以水合是一个必须要理解的过程：",[11,71,72,73,77],{},"当在浏览器打开一个页面时，首先会在服务端渲染（SSR)，渲染后的",[74,75,76],"strong",{},"完整 html 代码","会被发送到客户端。此时已经在服务端拿到请求的数据了，所以客户端收到 HTML后，用户已经能够看到内容。",[11,79,80,81,84],{},"对比单页应用（SPA），浏览器只是加载了一个 带有根元素的的 html 页面，所以此时页面是空白的，还需要在加载完相关 JS文件后，由 JS 去插入内容。所以单页应用只要在根元素 ",[22,82,83],{},"div id = app"," 里写点内容（如loading、骨架），就会早于实际内容出现，达到不白屏的效果。",[11,86,87,88,91,92,95,96,99,100,103],{},"回到 ",[22,89,90],{},"Nuxt"," ，用户看到内容后，此时页面还无法交互，因为仅仅是渲染了 ",[22,93,94],{},"HTML","，",[22,97,98],{},"Vue"," 相关的逻辑还在 ",[22,101,102],{},"JS"," 里，需要下载和执行。",[11,105,106,109],{},[74,107,108],{},"下载和执行时被 Vue 接管 HTML 的过程就叫水合","，水合后界面就可以响应用户的交互了。",[11,111,112],{},"有了渲染方式的差异，才会有其他的请求方式来契合这种渲染方式。",[15,114,116],{"id":115},"useasyncdata","useAsyncData",[11,118,119,120,123,124,127],{},"从 ",[22,121,122],{},"vue2"," 到 ",[22,125,126],{},"vue3","，我们都知道多了个选项式和组合式的区别。",[11,129,130,131,134,135,138,139,142,143,145],{},"选项式中通常把功能集中在一个组件或函数中，每个组件都有 ",[22,132,133],{},"data","、",[22,136,137],{},"methods","，组件内定义属性都会暴漏在函数内部的 ",[22,140,141],{},"this"," 上，指向当前实例。它的特点是，不关心响应式细节，强制按照选项来组织代码，你的后端同事看了 ",[22,144,98],{}," 之后都表示很简单。",[11,147,148,149,152],{},"而组合式（",[22,150,151],{},"composable","）的核心思想是直接在函数作用于内定义响应式变量，要比选项式自由和高效的多。",[11,154,155,156,159,160,162,163,166],{},"所以 Vue3 的代码中，经常可以看到 ",[22,157,158],{},"UseXXXX"," 这类的函数，其内部就包含响应式变量，也就类似选项式代码中的 ",[22,161,133],{}," 和 ",[22,164,165],{},"method","。",[11,168,169,170],{},"所以其内部的响应式变量发生变化时，通常会有一个对应的逻辑随之发生变化，可能是另一个响应式变量，也可能是与之对应的",[22,171,172],{},"Template",[11,174,175,176,178,179,181],{},"所以 ",[22,177,116],{}," 这个 ",[22,180,151],{}," ，也有类似的功能和效果。",[183,184,189],"pre",{"className":185,"code":186,"language":187,"meta":188,"style":188},"language-typescript shiki shiki-themes github-light","const { data, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'))\n","typescript","",[22,190,191],{"__ignoreMap":188},[192,193,196,200,204,207,210,213,215,218,220,223,225,228,231,234,237,241,244,248,251,254,257,259,261],"span",{"class":194,"line":195},"line",1,[192,197,199],{"class":198},"sD7c4","const",[192,201,203],{"class":202},"sgsFI"," { ",[192,205,133],{"class":206},"sYu0t",[192,208,209],{"class":202},", ",[192,211,212],{"class":206},"error",[192,214,209],{"class":202},[192,216,217],{"class":206},"clear",[192,219,209],{"class":202},[192,221,222],{"class":206},"status",[192,224,209],{"class":202},[192,226,227],{"class":206},"refresh",[192,229,230],{"class":202}," } ",[192,232,233],{"class":198},"=",[192,235,236],{"class":198}," await",[192,238,240],{"class":239},"s7eDp"," useAsyncData",[192,242,243],{"class":202},"(",[192,245,247],{"class":246},"sYBdl","'users'",[192,249,250],{"class":202},", () ",[192,252,253],{"class":198},"=>",[192,255,256],{"class":239}," myGetFunction",[192,258,243],{"class":202},[192,260,247],{"class":246},[192,262,263],{"class":202},"))\n",[11,265,266,268,269,272,273,166],{},[22,267,116],{}," 第一个参数是唯一键，用于",[74,270,271],{},"缓存","第二个参数的",[74,274,275],{},"响应",[11,277,278,279,281,282,285,286,289],{},"所以用 ",[22,280,116],{}," 时，在 ",[22,283,284],{},"SSR"," 时和在 ",[22,287,288],{},"水合"," 时，不会发生两次重复的请求。可以保证渲染的一致性。",[11,291,292,293,166],{},"其次，因为第二个参数只是一个获取数据的匿名函数，所以你可以用它来请求任意的服务，比如你用了其他 CMS服务来管理数据，这时候就应该使用 ",[22,294,116],{},[11,296,297],{},"这东西竟然有五个返回值，看看都有啥用，怎么就响应式了。",[11,299,300,134,302,134,304,306,307,310],{},[22,301,133],{},[22,303,212],{},[22,305,222],{}," 这三个值，都是 Vue 的引用（ Vue refs accessible），也就是说类似于你用 ",[22,308,309],{},"ref"," 提前定义好了一样：",[183,312,314],{"className":185,"code":313,"language":187,"meta":188,"style":188},"const data = ref()\nconst error = ref()\nconst status = ref('idle') // 还没请求\n\nconst fetchData = async () => {\n    status.value = 'pendding'\n    const data = await fetch('xxxx').catch( err => {\n        error.value = err\n        status.value = 'error'\n    })\n    ...\n    data.value = res.data\n    status.value = 'success'\n}\n",[22,315,316,332,346,370,377,398,409,447,458,469,475,481,492,502],{"__ignoreMap":188},[192,317,318,320,323,326,329],{"class":194,"line":195},[192,319,199],{"class":198},[192,321,322],{"class":206}," data",[192,324,325],{"class":198}," =",[192,327,328],{"class":239}," ref",[192,330,331],{"class":202},"()\n",[192,333,335,337,340,342,344],{"class":194,"line":334},2,[192,336,199],{"class":198},[192,338,339],{"class":206}," error",[192,341,325],{"class":198},[192,343,328],{"class":239},[192,345,331],{"class":202},[192,347,349,351,354,356,358,360,363,366],{"class":194,"line":348},3,[192,350,199],{"class":198},[192,352,353],{"class":206}," status",[192,355,325],{"class":198},[192,357,328],{"class":239},[192,359,243],{"class":202},[192,361,362],{"class":246},"'idle'",[192,364,365],{"class":202},") ",[192,367,369],{"class":368},"sAwPA","// 还没请求\n",[192,371,373],{"class":194,"line":372},4,[192,374,376],{"emptyLinePlaceholder":375},true,"\n",[192,378,380,382,385,387,390,393,395],{"class":194,"line":379},5,[192,381,199],{"class":198},[192,383,384],{"class":239}," fetchData",[192,386,325],{"class":198},[192,388,389],{"class":198}," async",[192,391,392],{"class":202}," () ",[192,394,253],{"class":198},[192,396,397],{"class":202}," {\n",[192,399,401,404,406],{"class":194,"line":400},6,[192,402,403],{"class":202},"    status.value ",[192,405,233],{"class":198},[192,407,408],{"class":246}," 'pendding'\n",[192,410,412,415,417,419,421,424,426,429,432,435,438,442,445],{"class":194,"line":411},7,[192,413,414],{"class":198},"    const",[192,416,322],{"class":206},[192,418,325],{"class":198},[192,420,236],{"class":198},[192,422,423],{"class":239}," fetch",[192,425,243],{"class":202},[192,427,428],{"class":246},"'xxxx'",[192,430,431],{"class":202},").",[192,433,434],{"class":239},"catch",[192,436,437],{"class":202},"( ",[192,439,441],{"class":440},"sqxcx","err",[192,443,444],{"class":198}," =>",[192,446,397],{"class":202},[192,448,450,453,455],{"class":194,"line":449},8,[192,451,452],{"class":202},"        error.value ",[192,454,233],{"class":198},[192,456,457],{"class":202}," err\n",[192,459,461,464,466],{"class":194,"line":460},9,[192,462,463],{"class":202},"        status.value ",[192,465,233],{"class":198},[192,467,468],{"class":246}," 'error'\n",[192,470,472],{"class":194,"line":471},10,[192,473,474],{"class":202},"    })\n",[192,476,478],{"class":194,"line":477},11,[192,479,480],{"class":198},"    ...\n",[192,482,484,487,489],{"class":194,"line":483},12,[192,485,486],{"class":202},"    data.value ",[192,488,233],{"class":198},[192,490,491],{"class":202}," res.data\n",[192,493,495,497,499],{"class":194,"line":494},13,[192,496,403],{"class":202},[192,498,233],{"class":198},[192,500,501],{"class":246}," 'success'\n",[192,503,505],{"class":194,"line":504},14,[192,506,507],{"class":202},"}\n",[11,509,510],{},"这样看能明白了吧。",[11,512,513,515,516,519,520,522,523,525,526,529,530,533],{},[22,514,133],{}," 就是我们接收返回后的数据的  ",[22,517,518],{},"Ref"," 引用，",[22,521,212],{}," 同理，",[22,524,222],{}," 则是类似于在 Vue2 中使用一个 ",[22,527,528],{},"isFetch"," 变量去管理 ",[22,531,532],{},"loading"," 状态。",[11,535,536],{},"也可以直接给 data 重命名一下：",[183,538,540],{"className":185,"code":539,"language":187,"meta":188,"style":188},"const { data: userList, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'))\n",[22,541,542],{"__ignoreMap":188},[192,543,544,546,548,550,553,556,558,560,562,564,566,568,570,572,574,576,578,580,582,584,586,588,590,592,594],{"class":194,"line":195},[192,545,199],{"class":198},[192,547,203],{"class":202},[192,549,133],{"class":440},[192,551,552],{"class":202},": ",[192,554,555],{"class":206},"userList",[192,557,209],{"class":202},[192,559,212],{"class":206},[192,561,209],{"class":202},[192,563,217],{"class":206},[192,565,209],{"class":202},[192,567,222],{"class":206},[192,569,209],{"class":202},[192,571,227],{"class":206},[192,573,230],{"class":202},[192,575,233],{"class":198},[192,577,236],{"class":198},[192,579,240],{"class":239},[192,581,243],{"class":202},[192,583,247],{"class":246},[192,585,250],{"class":202},[192,587,253],{"class":198},[192,589,256],{"class":239},[192,591,243],{"class":202},[192,593,247],{"class":246},[192,595,263],{"class":202},[11,597,598,599,602],{},"不过要注意，这个 ",[22,600,601],{},"data.value"," 是什么，和第二个参数返回的数据是什么有关，比如你的接口固定返回：",[183,604,606],{"className":185,"code":605,"language":187,"meta":188,"style":188},"{\n    code: 200,\n    data: [],\n    msg: 'ok',\n}\n",[22,607,608,613,626,634,646],{"__ignoreMap":188},[192,609,610],{"class":194,"line":195},[192,611,612],{"class":202},"{\n",[192,614,615,618,620,623],{"class":194,"line":334},[192,616,617],{"class":239},"    code",[192,619,552],{"class":202},[192,621,622],{"class":206},"200",[192,624,625],{"class":202},",\n",[192,627,628,631],{"class":194,"line":348},[192,629,630],{"class":239},"    data",[192,632,633],{"class":202},": [],\n",[192,635,636,639,641,644],{"class":194,"line":372},[192,637,638],{"class":239},"    msg",[192,640,552],{"class":202},[192,642,643],{"class":246},"'ok'",[192,645,625],{"class":202},[192,647,648],{"class":194,"line":379},[192,649,507],{"class":202},[11,651,652,653],{},"那要想取得列表渲染要用的数据，应该是 ",[22,654,655],{},"userList.value?.data",[11,657,658,659,661,662,665],{},"因为渲染方式的特点，",[22,660,116],{}," 还可以传入第三个参数 ",[22,663,664],{},"options"," 来控制其行为。",[667,668,669],"h3",{"id":669},"lazy",[11,671,672,673,675,676,678],{},"默认情况下，页面使用了 ",[22,674,116],{}," 来获取数据，这个 ",[22,677,151],{}," 会等待异步函数的解析，然后再导航到新的页面。",[11,680,681,682,166],{},"解析会耗时，所以你的导航动作就会有延迟，给人一种：",[74,683,684],{},"点了，但是没立马动起来的迟滞感",[11,686,687,689],{},[22,688,669],{}," 选项可以使其忽略异步函数的解析。",[183,691,693],{"className":185,"code":692,"language":187,"meta":188,"style":188},"const { data: userList, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'), { lazy: true })\n",[22,694,695],{"__ignoreMap":188},[192,696,697,699,701,703,705,707,709,711,713,715,717,719,721,723,725,727,729,731,733,735,737,739,741,743,745,748,751],{"class":194,"line":195},[192,698,199],{"class":198},[192,700,203],{"class":202},[192,702,133],{"class":440},[192,704,552],{"class":202},[192,706,555],{"class":206},[192,708,209],{"class":202},[192,710,212],{"class":206},[192,712,209],{"class":202},[192,714,217],{"class":206},[192,716,209],{"class":202},[192,718,222],{"class":206},[192,720,209],{"class":202},[192,722,227],{"class":206},[192,724,230],{"class":202},[192,726,233],{"class":198},[192,728,236],{"class":198},[192,730,240],{"class":239},[192,732,243],{"class":202},[192,734,247],{"class":246},[192,736,250],{"class":202},[192,738,253],{"class":198},[192,740,256],{"class":239},[192,742,243],{"class":202},[192,744,247],{"class":246},[192,746,747],{"class":202},"), { lazy: ",[192,749,750],{"class":206},"true",[192,752,753],{"class":202}," })\n",[11,755,756,757,759,760,134,762,166],{},"此时，当你点击进入一个新的页面时，导航不会被阻塞，但进入后内容可能还没拿到，所以需要使用 ",[22,758,222],{}," 来加载 ",[22,761,532],{},[22,763,764],{},"骨架组件",[11,766,767,768,770],{},"我觉得应该没人想在点击后阻塞导航吧，所以这个 ",[22,769,669],{}," 建议一直开启。",[667,772,773],{"id":773},"server",[11,775,776,777,780,781,784],{},"上一篇关于在 Nuxt 中使用 Prisma 的文章里，我提到了本地有个 ",[22,778,779],{},"dev.db"," ，线上也有一个 ",[22,782,783],{},"prod.db"," 。",[11,786,787,788,790,791,794,795,797,798,166],{},"两个数据库里存的东西肯定是不一样嘛，因为本地需要测试。所以我本地的 ",[22,789,555],{}," 里是 ",[74,792,793],{},"张三","，线上的 ",[22,796,555],{}," 是",[74,799,800],{},"李四",[11,802,803,804,806,807,810],{},"但在 ",[22,805,90],{}," 打包时，会 ",[22,808,809],{},"prerender","，预渲染！（我需要本地打包的）",[11,812,813,814,816],{},"也就是先把接口请求一遍，把真实的 ",[22,815,94],{}," 先给组装好，并且还有缓存。因为是为了 SEO，方便搜索引擎快速抓取到页面的内容。",[11,818,819],{},"于是，在线上打开用户列表页时，我的张三被显示出来了，因为他就是 HTML 里的内容。",[11,821,822,823],{},"这个时候就需要另一个选项： ",[22,824,773],{},[11,826,827,828,831],{},"当设置 ",[22,829,830],{},"server: false"," 时，第一次渲染就不会去请求数据，也就是会渲染出一个空的用户列表页。",[183,833,835],{"className":185,"code":834,"language":187,"meta":188,"style":188},"const { data: userList, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'), { lazy: true, server: false })\n",[22,836,837],{"__ignoreMap":188},[192,838,839,841,843,845,847,849,851,853,855,857,859,861,863,865,867,869,871,873,875,877,879,881,883,885,887,889,891,894,897],{"class":194,"line":195},[192,840,199],{"class":198},[192,842,203],{"class":202},[192,844,133],{"class":440},[192,846,552],{"class":202},[192,848,555],{"class":206},[192,850,209],{"class":202},[192,852,212],{"class":206},[192,854,209],{"class":202},[192,856,217],{"class":206},[192,858,209],{"class":202},[192,860,222],{"class":206},[192,862,209],{"class":202},[192,864,227],{"class":206},[192,866,230],{"class":202},[192,868,233],{"class":198},[192,870,236],{"class":198},[192,872,240],{"class":239},[192,874,243],{"class":202},[192,876,247],{"class":246},[192,878,250],{"class":202},[192,880,253],{"class":198},[192,882,256],{"class":239},[192,884,243],{"class":202},[192,886,247],{"class":246},[192,888,747],{"class":202},[192,890,750],{"class":206},[192,892,893],{"class":202},", server: ",[192,895,896],{"class":206},"false",[192,898,753],{"class":202},[667,900,901],{"id":901},"watch",[11,903,904,905,907,908,910],{},"看到这，我也没看出来 useAsyncData 响应个啥了啊，不就是帮我省下了创建接收数据的 ",[22,906,309],{}," 和 管理状态的 ",[22,909,309],{}," 的功夫？",[11,912,913,914,917],{},"那我们再把",[74,915,916],{},"获取列表这个场景","丰富一下。",[11,919,920],{},"用户多了，有分页了，应该怎么处理？",[183,922,924],{"className":185,"code":923,"language":187,"meta":188,"style":188},"const page = ref(1)\nconst changePage = () => {\n    myFetchData()\n}\n",[22,925,926,945,960,967],{"__ignoreMap":188},[192,927,928,930,933,935,937,939,942],{"class":194,"line":195},[192,929,199],{"class":198},[192,931,932],{"class":206}," page",[192,934,325],{"class":198},[192,936,328],{"class":239},[192,938,243],{"class":202},[192,940,941],{"class":206},"1",[192,943,944],{"class":202},")\n",[192,946,947,949,952,954,956,958],{"class":194,"line":334},[192,948,199],{"class":198},[192,950,951],{"class":239}," changePage",[192,953,325],{"class":198},[192,955,392],{"class":202},[192,957,253],{"class":198},[192,959,397],{"class":202},[192,961,962,965],{"class":194,"line":348},[192,963,964],{"class":239},"    myFetchData",[192,966,331],{"class":202},[192,968,969],{"class":194,"line":372},[192,970,507],{"class":202},[11,972,973],{},"如果再加个类型的筛选，日期的筛选等等一切和重新获取数据相关的响应式变量，都要写同样的代码。",[11,975,175,976,978,979,981],{},[22,977,116],{}," 支持直接 ",[22,980,901],{}," 响应式变量：",[183,983,985],{"className":185,"code":984,"language":187,"meta":188,"style":188},"const page = ref(1)\nconst { data: userList, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'), { lazy: true, watch: [page, tags] })\n",[22,986,987,1003],{"__ignoreMap":188},[192,988,989,991,993,995,997,999,1001],{"class":194,"line":195},[192,990,199],{"class":198},[192,992,932],{"class":206},[192,994,325],{"class":198},[192,996,328],{"class":239},[192,998,243],{"class":202},[192,1000,941],{"class":206},[192,1002,944],{"class":202},[192,1004,1005,1007,1009,1011,1013,1015,1017,1019,1021,1023,1025,1027,1029,1031,1033,1035,1037,1039,1041,1043,1045,1047,1049,1051,1053,1055,1057],{"class":194,"line":334},[192,1006,199],{"class":198},[192,1008,203],{"class":202},[192,1010,133],{"class":440},[192,1012,552],{"class":202},[192,1014,555],{"class":206},[192,1016,209],{"class":202},[192,1018,212],{"class":206},[192,1020,209],{"class":202},[192,1022,217],{"class":206},[192,1024,209],{"class":202},[192,1026,222],{"class":206},[192,1028,209],{"class":202},[192,1030,227],{"class":206},[192,1032,230],{"class":202},[192,1034,233],{"class":198},[192,1036,236],{"class":198},[192,1038,240],{"class":239},[192,1040,243],{"class":202},[192,1042,247],{"class":246},[192,1044,250],{"class":202},[192,1046,253],{"class":198},[192,1048,256],{"class":239},[192,1050,243],{"class":202},[192,1052,247],{"class":246},[192,1054,747],{"class":202},[192,1056,750],{"class":206},[192,1058,1059],{"class":202},", watch: [page, tags] })\n",[11,1061,1062,1063,1066,1067,1069,1070,1073],{},"当 ",[22,1064,1065],{},"page"," 发生变化时，",[22,1068,116],{}," 就会重新执行它的 ",[22,1071,1072],{},"handler"," ，做到刷新数据。",[11,1075,1076],{},"代码量又减少了不少",[667,1078,1079],{"id":1079},"其他选项",[11,1081,1082,1083,1086,1087,1089,1090,1092,1093,1096,1097,1099,1100,1102],{},"和 ",[22,1084,1085],{},"vue"," 的 ",[22,1088,901],{}," 相比，",[22,1091,116],{}," 也有一个 ",[22,1094,1095],{},"immediate"," 选项，只不过它默认是 ",[22,1098,750],{}," ，你可以设置为 ",[22,1101,896],{}," 来阻止立即触发请求。",[11,1104,1105],{},"还有一种场景，使用一个接口获取一组比较大的数据比如文章时，有时候不需要文章的内容，每次都传输内容的话，数据量太大了，影响传输速度。",[11,1107,1108,1109,1112,1113],{},"可以使用 ",[22,1110,1111],{},"pick"," 选项，选取指定的键：",[22,1114,1115],{},"pick: [\"title\", \"description\"]",[11,1117,1118,1119,1122,1123,1125,1126,1128],{},"还有 ",[22,1120,1121],{},"deep"," 选项，默认为 ",[22,1124,750],{}," ，如果不需要深度深度响应时，可以设置为 ",[22,1127,896],{}," 以提高性能。",[11,1130,1131],{},"其他选项就不一一介绍了，我直接放在这里，后续发现比较有用的场景，再来分享。",[183,1133,1135],{"className":185,"code":1134,"language":187,"meta":188,"style":188},"type AsyncDataOptions\u003CDataT> = {\n  server?: boolean\n  lazy?: boolean\n  immediate?: boolean\n  deep?: boolean\n  dedupe?: 'cancel' | 'defer'\n  default?: () => DataT | Ref\u003CDataT> | null\n  transform?: (input: DataT) => DataT | Promise\u003CDataT>\n  pick?: string[]\n  watch?: WatchSource[]\n  getCachedData?: (key: string, nuxtApp: NuxtApp) => DataT\n}\n",[22,1136,1137,1158,1169,1178,1187,1196,1212,1243,1279,1292,1304,1337],{"__ignoreMap":188},[192,1138,1139,1142,1145,1148,1151,1154,1156],{"class":194,"line":195},[192,1140,1141],{"class":198},"type",[192,1143,1144],{"class":239}," AsyncDataOptions",[192,1146,1147],{"class":202},"\u003C",[192,1149,1150],{"class":239},"DataT",[192,1152,1153],{"class":202},"> ",[192,1155,233],{"class":198},[192,1157,397],{"class":202},[192,1159,1160,1163,1166],{"class":194,"line":334},[192,1161,1162],{"class":440},"  server",[192,1164,1165],{"class":198},"?:",[192,1167,1168],{"class":206}," boolean\n",[192,1170,1171,1174,1176],{"class":194,"line":348},[192,1172,1173],{"class":440},"  lazy",[192,1175,1165],{"class":198},[192,1177,1168],{"class":206},[192,1179,1180,1183,1185],{"class":194,"line":372},[192,1181,1182],{"class":440},"  immediate",[192,1184,1165],{"class":198},[192,1186,1168],{"class":206},[192,1188,1189,1192,1194],{"class":194,"line":379},[192,1190,1191],{"class":440},"  deep",[192,1193,1165],{"class":198},[192,1195,1168],{"class":206},[192,1197,1198,1201,1203,1206,1209],{"class":194,"line":400},[192,1199,1200],{"class":440},"  dedupe",[192,1202,1165],{"class":198},[192,1204,1205],{"class":246}," 'cancel'",[192,1207,1208],{"class":198}," |",[192,1210,1211],{"class":246}," 'defer'\n",[192,1213,1214,1217,1219,1221,1223,1226,1228,1231,1233,1235,1237,1240],{"class":194,"line":411},[192,1215,1216],{"class":239},"  default",[192,1218,1165],{"class":198},[192,1220,392],{"class":202},[192,1222,253],{"class":198},[192,1224,1225],{"class":239}," DataT",[192,1227,1208],{"class":198},[192,1229,1230],{"class":239}," Ref",[192,1232,1147],{"class":202},[192,1234,1150],{"class":239},[192,1236,1153],{"class":202},[192,1238,1239],{"class":198},"|",[192,1241,1242],{"class":206}," null\n",[192,1244,1245,1248,1250,1253,1256,1259,1261,1263,1265,1267,1269,1272,1274,1276],{"class":194,"line":449},[192,1246,1247],{"class":239},"  transform",[192,1249,1165],{"class":198},[192,1251,1252],{"class":202}," (",[192,1254,1255],{"class":440},"input",[192,1257,1258],{"class":198},":",[192,1260,1225],{"class":239},[192,1262,365],{"class":202},[192,1264,253],{"class":198},[192,1266,1225],{"class":239},[192,1268,1208],{"class":198},[192,1270,1271],{"class":239}," Promise",[192,1273,1147],{"class":202},[192,1275,1150],{"class":239},[192,1277,1278],{"class":202},">\n",[192,1280,1281,1284,1286,1289],{"class":194,"line":460},[192,1282,1283],{"class":440},"  pick",[192,1285,1165],{"class":198},[192,1287,1288],{"class":206}," string",[192,1290,1291],{"class":202},"[]\n",[192,1293,1294,1297,1299,1302],{"class":194,"line":471},[192,1295,1296],{"class":440},"  watch",[192,1298,1165],{"class":198},[192,1300,1301],{"class":239}," WatchSource",[192,1303,1291],{"class":202},[192,1305,1306,1309,1311,1313,1316,1318,1320,1322,1325,1327,1330,1332,1334],{"class":194,"line":477},[192,1307,1308],{"class":239},"  getCachedData",[192,1310,1165],{"class":198},[192,1312,1252],{"class":202},[192,1314,1315],{"class":440},"key",[192,1317,1258],{"class":198},[192,1319,1288],{"class":206},[192,1321,209],{"class":202},[192,1323,1324],{"class":440},"nuxtApp",[192,1326,1258],{"class":198},[192,1328,1329],{"class":239}," NuxtApp",[192,1331,365],{"class":202},[192,1333,253],{"class":198},[192,1335,1336],{"class":239}," DataT\n",[192,1338,1339],{"class":194,"line":483},[192,1340,507],{"class":202},[11,1342,1343,1344,1346,1347,134,1349,134,1351,1353],{},"现在可以确定，",[22,1345,116],{}," 可以帮助缓存数据，防止多次请求，保证渲染的一致性。提供响应式的 ",[22,1348,133],{},[22,1350,212],{},[22,1352,222],{}," 来完成页面的渲染，同时提供一些选项来控制其行为。",[11,1355,1356],{},"最后再来捋一捋什么场景下使用什么选项的问题：",[11,1358,1359],{},[1360,1361],"img",{"alt":188,"src":1362},"https://img.zzao.club/article/202411271747999.png",[11,1364,1365],{},[74,1366,1367],{},"PS：这图我在 Youtube 刷到的，但是没截屏，所以凭借印象又画了一遍",[11,1369,1370,1371,1374],{},"那 ",[22,1372,1373],{},"useFetch"," 还有什么活能整吗？",[15,1376,1373],{"id":1377},"usefetch",[11,1379,1380,1382],{},[22,1381,1373],{}," 的整活就像是：",[11,1384,1385,1386,1389,1390,1393,1394,1397,1398,1400],{},"虽然在 ",[22,1387,1388],{},"script"," 需要使用 ",[22,1391,1392],{},".value"," 获取响应式数据，但 ",[22,1395,1396],{},"template"," 中不需要，因为 ",[22,1399,98],{}," 帮你处理了。",[11,1402,1403,1404,1407],{},"以前 ",[22,1405,1406],{},"props"," 解构会失去响应式，现在（3.5+）解构也能直接用了。",[11,1409,1410,1411,162,1413,1415],{},"是的，它就是个语法糖一样，是 ",[22,1412,116],{},[22,1414,18],{}," 的包装器。",[11,1417,1418,1419,1421],{},"它不用传第一个 ",[22,1420,1315],{}," 值，会根据 url 和选项自动生成，并且可以推断 API 的响应类型。",[11,1423,1424],{},"同时有着和 useAsyncData 一样的第三个参数（选项），但做了一些增强。",[11,1426,1427,1428,134,1430,1433],{},"还是拿刚才获取用户列表的场景举例，我们监听 ",[22,1429,1065],{},[22,1431,1432],{},"tags","，变化时会再次请求。",[11,1435,1436,1437,1439],{},"在 ",[22,1438,1373],{}," 中可以再进一步：",[183,1441,1443],{"className":185,"code":1442,"language":187,"meta":188,"style":188},"const page = ref(1)\nconst { data: userList, error, clear, status, refresh } = await useFetch('/api/user/list', { page }, { lazy: true })\n",[22,1444,1445,1461],{"__ignoreMap":188},[192,1446,1447,1449,1451,1453,1455,1457,1459],{"class":194,"line":195},[192,1448,199],{"class":198},[192,1450,932],{"class":206},[192,1452,325],{"class":198},[192,1454,328],{"class":239},[192,1456,243],{"class":202},[192,1458,941],{"class":206},[192,1460,944],{"class":202},[192,1462,1463,1465,1467,1469,1471,1473,1475,1477,1479,1481,1483,1485,1487,1489,1491,1493,1495,1498,1500,1503,1506,1508],{"class":194,"line":334},[192,1464,199],{"class":198},[192,1466,203],{"class":202},[192,1468,133],{"class":440},[192,1470,552],{"class":202},[192,1472,555],{"class":206},[192,1474,209],{"class":202},[192,1476,212],{"class":206},[192,1478,209],{"class":202},[192,1480,217],{"class":206},[192,1482,209],{"class":202},[192,1484,222],{"class":206},[192,1486,209],{"class":202},[192,1488,227],{"class":206},[192,1490,230],{"class":202},[192,1492,233],{"class":198},[192,1494,236],{"class":198},[192,1496,1497],{"class":239}," useFetch",[192,1499,243],{"class":202},[192,1501,1502],{"class":246},"'/api/user/list'",[192,1504,1505],{"class":202},", { page }, { lazy: ",[192,1507,750],{"class":206},[192,1509,753],{"class":202},[11,1511,1512,1513,1515,1516,1518],{},"我们只需要传入 ",[22,1514,1065],{}," 这个响应式变量，",[22,1517,1373],{}," 就会追踪到这个响应式变量。",[11,1520,1521,1522,162,1524,1527],{},"类似 ",[22,1523,901],{},[22,1525,1526],{},"watchEffect","。一个需要显式的传入，一个会自动的追踪。",[11,1529,1530,1531,1534,1535,1538,1539],{},"但是这里要注意：",[22,1532,1533],{},"{ page }"," 等价于 ",[22,1536,1537],{},"{ page: page }","， 而不是 ",[22,1540,1541],{},"{ page: page.value }",[11,1543,1544,1550,1551,178,1553],{},[74,1545,1546,1549],{},[22,1547,1548],{},"page.value"," 只是一个值！"," 所以要传入的是 ",[22,1552,1065],{},[22,1554,1555],{},"ref对象",[11,1557,1558],{},[74,1559,1560,1561,1563],{},"使用 ",[22,1562,1373],{}," 后代码被进一步简化。",[11,1565,1566,1567,1569,1570,1572],{},"所以在思考使用 ",[22,1568,1373],{}," 还是 ",[22,1571,18],{},"  来进行接口请求时就十分明了了：",[11,1574,1575,1576,1578],{},"一般基于用户的交互才去做出反应的，就用 ",[22,1577,18],{}," 就可以。",[11,1580,1581,1582,784],{},"如果基于页面状态需要重复获取数据的，就用 ",[22,1583,1373],{},[11,1585,1586,1587,1589,1590,1592,1593,1596,1597,1599,1600,1602],{},"但刚才也强调了，传入的是一个 ",[22,1588,1555],{}," 才会时 ",[22,1591,1373],{}," 正常的响应式的自动请求，所以如果是",[74,1594,1595],{},"想统一请求方式","，也可以选择直接传值给 ",[22,1598,1373],{}," ，这样他就像一个普通的增强版 ",[22,1601,18],{}," 一样了。",[11,1604,1605,1606,1608,1609,1611],{},"但 ",[22,1607,1373],{}," 返回的值还是个响应式的，使用 ",[22,1610,18],{}," 的场景很多都不需要返回值再触发其他响应，所以显得有些\"多余\"。",[11,1613,1614],{},"怎么取舍就看自己了。",[15,1616,1617],{"id":1617},"最佳实践",[11,1619,1620,1621,1623,1624,1626],{},"我在项目中以使用 ",[22,1622,1373],{},"为主 , 那怎么封装 ",[22,1625,1373],{}," 用起来才最顺手呢？",[11,1628,1629,1630,1632],{},"经过我自己的使用，以及搜索多个关于 ",[22,1631,1373],{}," 的使用。最后在最少三个博客站内发现了同样的封装代码，究竟谁是作者我也不清楚，代码里也没有说明，这里直接贴给大家，再分析如何使用。",[11,1634,1635],{},[22,1636,1637],{},"composables/useHttp.ts",[183,1639,1641],{"className":185,"code":1640,"language":187,"meta":188,"style":188},"import type {FetchError, FetchResponse, SearchParameters} from 'ofetch';\nimport {hash} from 'ohash';\nimport type {AsyncData, UseFetchOptions} from '#app';\nimport type {KeysOf} from '#app/composables/asyncData';\ntype UrlType = string | Request | Ref\u003Cstring | Request> | (() => string | Request);\n\ntype HttpOption\u003CT> = UseFetchOptions\u003CResOptions\u003CT>, T, KeysOf\u003CT>, any>;\ninterface ResOptions\u003CT> {\n    data: T;\n    code: number;\n    message: boolean;\n    err?: string[];\n}\n\nfunction handleError\u003CT>(\n    _method: string | undefined,\n    _response: FetchResponse\u003CResOptions\u003CT>> & FetchResponse\u003Cany>,\n) {\n    // Implement error handling logic here\n    if (_response?._data?.statusCode === 401) { \n      // setUser('')\n    }\n    console.error(`[useHttp] [error] ${_method}:`, _response);\n}\n\nfunction checkRef(obj: Record\u003Cstring, any>) {\n    return Object.keys(obj).some(key => isRef(obj[key]));\n}\n\nfunction fetch\u003CT>(url: UrlType, opts: HttpOption\u003CT>): AsyncData\u003CResOptions\u003CT>, FetchError\u003CResOptions\u003CT>>> {\n    // Check the `key` option\n    const { key, params, watch } = opts;\n    if (!key && ((params && checkRef(params)) || (watch && checkRef(watch))))\n        console.error('\\x1B[31m%s\\x1B[0m %s', '[useHttp] [error]', 'The `key` option is required when `params` or `watch` has ref properties, please set a unique key for the current request.');\n\n    const options = opts as UseFetchOptions\u003CResOptions\u003CT>>;\n    options.lazy = options.lazy ?? true;\n\n    // const { baseUrl } = useRuntimeConfig().public;\n\n    return useFetch\u003CResOptions\u003CT>>(url, {\n        // Request interception\n        onRequest({ options }) {\n            // options.baseURL = baseUrl;\n            // Set the base URL\n        },\n        // Response interception\n        onResponse(_context) {\n            // Handle the response\n        },\n        // Error interception\n        onResponseError({ response, options: { method } }) {\n            handleError\u003CT>(method, response);\n        },\n        // Set the cache key\n        key: key ?? hash(['api-fetch', url, JSON.stringify({ method: options.method, params: options.params })]),\n        // Merge the options\n        ...options,\n    }) as AsyncData\u003CResOptions\u003CT>, FetchError\u003CResOptions\u003CT>>>;\n}\n\nexport const $http = {\n    get: \u003CT>(url: UrlType, params?: SearchParameters, option?: HttpOption\u003CT>) => {\n        return fetch\u003CT>(url, { method: 'get', params, ...option });\n    },\n\n    post: \u003CT>(url: UrlType, body?: RequestInit['body'] | Record\u003Cstring, any>, option?: HttpOption\u003CT>) => {\n        return fetch\u003CT>(url, { method: 'post', body, ...option });\n    }\n};\n\nexport default function UseHttp() {\n    return {\n        $http,\n    };\n}\n",[22,1642,1643,1663,1677,1693,1709,1756,1760,1810,1825,1836,1847,1859,1871,1875,1879,1895,1912,1946,1952,1958,1976,1982,1988,2010,2015,2020,2049,2079,2084,2089,2155,2161,2186,2225,2261,2266,2295,2314,2319,2325,2330,2348,2354,2368,2374,2380,2386,2392,2405,2411,2416,2422,2445,2458,2463,2469,2501,2507,2516,2549,2554,2559,2575,2623,2650,2656,2661,2728,2751,2756,2762,2767,2784,2791,2797,2803],{"__ignoreMap":188},[192,1644,1645,1648,1651,1654,1657,1660],{"class":194,"line":195},[192,1646,1647],{"class":198},"import",[192,1649,1650],{"class":198}," type",[192,1652,1653],{"class":202}," {FetchError, FetchResponse, SearchParameters} ",[192,1655,1656],{"class":198},"from",[192,1658,1659],{"class":246}," 'ofetch'",[192,1661,1662],{"class":202},";\n",[192,1664,1665,1667,1670,1672,1675],{"class":194,"line":334},[192,1666,1647],{"class":198},[192,1668,1669],{"class":202}," {hash} ",[192,1671,1656],{"class":198},[192,1673,1674],{"class":246}," 'ohash'",[192,1676,1662],{"class":202},[192,1678,1679,1681,1683,1686,1688,1691],{"class":194,"line":348},[192,1680,1647],{"class":198},[192,1682,1650],{"class":198},[192,1684,1685],{"class":202}," {AsyncData, UseFetchOptions} ",[192,1687,1656],{"class":198},[192,1689,1690],{"class":246}," '#app'",[192,1692,1662],{"class":202},[192,1694,1695,1697,1699,1702,1704,1707],{"class":194,"line":372},[192,1696,1647],{"class":198},[192,1698,1650],{"class":198},[192,1700,1701],{"class":202}," {KeysOf} ",[192,1703,1656],{"class":198},[192,1705,1706],{"class":246}," '#app/composables/asyncData'",[192,1708,1662],{"class":202},[192,1710,1711,1713,1716,1718,1720,1722,1725,1727,1729,1731,1734,1736,1738,1740,1742,1745,1747,1749,1751,1753],{"class":194,"line":379},[192,1712,1141],{"class":198},[192,1714,1715],{"class":239}," UrlType",[192,1717,325],{"class":198},[192,1719,1288],{"class":206},[192,1721,1208],{"class":198},[192,1723,1724],{"class":239}," Request",[192,1726,1208],{"class":198},[192,1728,1230],{"class":239},[192,1730,1147],{"class":202},[192,1732,1733],{"class":206},"string",[192,1735,1208],{"class":198},[192,1737,1724],{"class":239},[192,1739,1153],{"class":202},[192,1741,1239],{"class":198},[192,1743,1744],{"class":202}," (() ",[192,1746,253],{"class":198},[192,1748,1288],{"class":206},[192,1750,1208],{"class":198},[192,1752,1724],{"class":239},[192,1754,1755],{"class":202},");\n",[192,1757,1758],{"class":194,"line":400},[192,1759,376],{"emptyLinePlaceholder":375},[192,1761,1762,1764,1767,1769,1772,1774,1776,1779,1781,1784,1786,1788,1791,1793,1795,1798,1800,1802,1804,1807],{"class":194,"line":411},[192,1763,1141],{"class":198},[192,1765,1766],{"class":239}," HttpOption",[192,1768,1147],{"class":202},[192,1770,1771],{"class":239},"T",[192,1773,1153],{"class":202},[192,1775,233],{"class":198},[192,1777,1778],{"class":239}," UseFetchOptions",[192,1780,1147],{"class":202},[192,1782,1783],{"class":239},"ResOptions",[192,1785,1147],{"class":202},[192,1787,1771],{"class":239},[192,1789,1790],{"class":202},">, ",[192,1792,1771],{"class":239},[192,1794,209],{"class":202},[192,1796,1797],{"class":239},"KeysOf",[192,1799,1147],{"class":202},[192,1801,1771],{"class":239},[192,1803,1790],{"class":202},[192,1805,1806],{"class":206},"any",[192,1808,1809],{"class":202},">;\n",[192,1811,1812,1815,1818,1820,1822],{"class":194,"line":449},[192,1813,1814],{"class":198},"interface",[192,1816,1817],{"class":239}," ResOptions",[192,1819,1147],{"class":202},[192,1821,1771],{"class":239},[192,1823,1824],{"class":202},"> {\n",[192,1826,1827,1829,1831,1834],{"class":194,"line":460},[192,1828,630],{"class":440},[192,1830,1258],{"class":198},[192,1832,1833],{"class":239}," T",[192,1835,1662],{"class":202},[192,1837,1838,1840,1842,1845],{"class":194,"line":471},[192,1839,617],{"class":440},[192,1841,1258],{"class":198},[192,1843,1844],{"class":206}," number",[192,1846,1662],{"class":202},[192,1848,1849,1852,1854,1857],{"class":194,"line":477},[192,1850,1851],{"class":440},"    message",[192,1853,1258],{"class":198},[192,1855,1856],{"class":206}," boolean",[192,1858,1662],{"class":202},[192,1860,1861,1864,1866,1868],{"class":194,"line":483},[192,1862,1863],{"class":440},"    err",[192,1865,1165],{"class":198},[192,1867,1288],{"class":206},[192,1869,1870],{"class":202},"[];\n",[192,1872,1873],{"class":194,"line":494},[192,1874,507],{"class":202},[192,1876,1877],{"class":194,"line":504},[192,1878,376],{"emptyLinePlaceholder":375},[192,1880,1882,1885,1888,1890,1892],{"class":194,"line":1881},15,[192,1883,1884],{"class":198},"function",[192,1886,1887],{"class":239}," handleError",[192,1889,1147],{"class":202},[192,1891,1771],{"class":239},[192,1893,1894],{"class":202},">(\n",[192,1896,1898,1901,1903,1905,1907,1910],{"class":194,"line":1897},16,[192,1899,1900],{"class":440},"    _method",[192,1902,1258],{"class":198},[192,1904,1288],{"class":206},[192,1906,1208],{"class":198},[192,1908,1909],{"class":206}," undefined",[192,1911,625],{"class":202},[192,1913,1915,1918,1920,1923,1925,1927,1929,1931,1934,1937,1939,1941,1943],{"class":194,"line":1914},17,[192,1916,1917],{"class":440},"    _response",[192,1919,1258],{"class":198},[192,1921,1922],{"class":239}," FetchResponse",[192,1924,1147],{"class":202},[192,1926,1783],{"class":239},[192,1928,1147],{"class":202},[192,1930,1771],{"class":239},[192,1932,1933],{"class":202},">> ",[192,1935,1936],{"class":198},"&",[192,1938,1922],{"class":239},[192,1940,1147],{"class":202},[192,1942,1806],{"class":206},[192,1944,1945],{"class":202},">,\n",[192,1947,1949],{"class":194,"line":1948},18,[192,1950,1951],{"class":202},") {\n",[192,1953,1955],{"class":194,"line":1954},19,[192,1956,1957],{"class":368},"    // Implement error handling logic here\n",[192,1959,1961,1964,1967,1970,1973],{"class":194,"line":1960},20,[192,1962,1963],{"class":198},"    if",[192,1965,1966],{"class":202}," (_response?._data?.statusCode ",[192,1968,1969],{"class":198},"===",[192,1971,1972],{"class":206}," 401",[192,1974,1975],{"class":202},") { \n",[192,1977,1979],{"class":194,"line":1978},21,[192,1980,1981],{"class":368},"      // setUser('')\n",[192,1983,1985],{"class":194,"line":1984},22,[192,1986,1987],{"class":202},"    }\n",[192,1989,1991,1994,1996,1998,2001,2004,2007],{"class":194,"line":1990},23,[192,1992,1993],{"class":202},"    console.",[192,1995,212],{"class":239},[192,1997,243],{"class":202},[192,1999,2000],{"class":246},"`[useHttp] [error] ${",[192,2002,2003],{"class":202},"_method",[192,2005,2006],{"class":246},"}:`",[192,2008,2009],{"class":202},", _response);\n",[192,2011,2013],{"class":194,"line":2012},24,[192,2014,507],{"class":202},[192,2016,2018],{"class":194,"line":2017},25,[192,2019,376],{"emptyLinePlaceholder":375},[192,2021,2023,2025,2028,2030,2033,2035,2038,2040,2042,2044,2046],{"class":194,"line":2022},26,[192,2024,1884],{"class":198},[192,2026,2027],{"class":239}," checkRef",[192,2029,243],{"class":202},[192,2031,2032],{"class":440},"obj",[192,2034,1258],{"class":198},[192,2036,2037],{"class":239}," Record",[192,2039,1147],{"class":202},[192,2041,1733],{"class":206},[192,2043,209],{"class":202},[192,2045,1806],{"class":206},[192,2047,2048],{"class":202},">) {\n",[192,2050,2052,2055,2058,2061,2064,2067,2069,2071,2073,2076],{"class":194,"line":2051},27,[192,2053,2054],{"class":198},"    return",[192,2056,2057],{"class":202}," Object.",[192,2059,2060],{"class":239},"keys",[192,2062,2063],{"class":202},"(obj).",[192,2065,2066],{"class":239},"some",[192,2068,243],{"class":202},[192,2070,1315],{"class":440},[192,2072,444],{"class":198},[192,2074,2075],{"class":239}," isRef",[192,2077,2078],{"class":202},"(obj[key]));\n",[192,2080,2082],{"class":194,"line":2081},28,[192,2083,507],{"class":202},[192,2085,2087],{"class":194,"line":2086},29,[192,2088,376],{"emptyLinePlaceholder":375},[192,2090,2092,2094,2096,2098,2100,2103,2106,2108,2110,2112,2115,2117,2119,2121,2123,2126,2128,2131,2133,2135,2137,2139,2141,2144,2146,2148,2150,2152],{"class":194,"line":2091},30,[192,2093,1884],{"class":198},[192,2095,423],{"class":239},[192,2097,1147],{"class":202},[192,2099,1771],{"class":239},[192,2101,2102],{"class":202},">(",[192,2104,2105],{"class":440},"url",[192,2107,1258],{"class":198},[192,2109,1715],{"class":239},[192,2111,209],{"class":202},[192,2113,2114],{"class":440},"opts",[192,2116,1258],{"class":198},[192,2118,1766],{"class":239},[192,2120,1147],{"class":202},[192,2122,1771],{"class":239},[192,2124,2125],{"class":202},">)",[192,2127,1258],{"class":198},[192,2129,2130],{"class":239}," AsyncData",[192,2132,1147],{"class":202},[192,2134,1783],{"class":239},[192,2136,1147],{"class":202},[192,2138,1771],{"class":239},[192,2140,1790],{"class":202},[192,2142,2143],{"class":239},"FetchError",[192,2145,1147],{"class":202},[192,2147,1783],{"class":239},[192,2149,1147],{"class":202},[192,2151,1771],{"class":239},[192,2153,2154],{"class":202},">>> {\n",[192,2156,2158],{"class":194,"line":2157},31,[192,2159,2160],{"class":368},"    // Check the `key` option\n",[192,2162,2164,2166,2168,2170,2172,2175,2177,2179,2181,2183],{"class":194,"line":2163},32,[192,2165,414],{"class":198},[192,2167,203],{"class":202},[192,2169,1315],{"class":206},[192,2171,209],{"class":202},[192,2173,2174],{"class":206},"params",[192,2176,209],{"class":202},[192,2178,901],{"class":206},[192,2180,230],{"class":202},[192,2182,233],{"class":198},[192,2184,2185],{"class":202}," opts;\n",[192,2187,2189,2191,2193,2196,2199,2202,2205,2207,2209,2212,2215,2218,2220,2222],{"class":194,"line":2188},33,[192,2190,1963],{"class":198},[192,2192,1252],{"class":202},[192,2194,2195],{"class":198},"!",[192,2197,2198],{"class":202},"key ",[192,2200,2201],{"class":198},"&&",[192,2203,2204],{"class":202}," ((params ",[192,2206,2201],{"class":198},[192,2208,2027],{"class":239},[192,2210,2211],{"class":202},"(params)) ",[192,2213,2214],{"class":198},"||",[192,2216,2217],{"class":202}," (watch ",[192,2219,2201],{"class":198},[192,2221,2027],{"class":239},[192,2223,2224],{"class":202},"(watch))))\n",[192,2226,2228,2231,2233,2235,2238,2241,2244,2246,2249,2251,2254,2256,2259],{"class":194,"line":2227},34,[192,2229,2230],{"class":202},"        console.",[192,2232,212],{"class":239},[192,2234,243],{"class":202},[192,2236,2237],{"class":246},"'",[192,2239,2240],{"class":206},"\\x1B",[192,2242,2243],{"class":246},"[31m%s",[192,2245,2240],{"class":206},[192,2247,2248],{"class":246},"[0m %s'",[192,2250,209],{"class":202},[192,2252,2253],{"class":246},"'[useHttp] [error]'",[192,2255,209],{"class":202},[192,2257,2258],{"class":246},"'The `key` option is required when `params` or `watch` has ref properties, please set a unique key for the current request.'",[192,2260,1755],{"class":202},[192,2262,2264],{"class":194,"line":2263},35,[192,2265,376],{"emptyLinePlaceholder":375},[192,2267,2269,2271,2274,2276,2279,2282,2284,2286,2288,2290,2292],{"class":194,"line":2268},36,[192,2270,414],{"class":198},[192,2272,2273],{"class":206}," options",[192,2275,325],{"class":198},[192,2277,2278],{"class":202}," opts ",[192,2280,2281],{"class":198},"as",[192,2283,1778],{"class":239},[192,2285,1147],{"class":202},[192,2287,1783],{"class":239},[192,2289,1147],{"class":202},[192,2291,1771],{"class":239},[192,2293,2294],{"class":202},">>;\n",[192,2296,2298,2301,2303,2306,2309,2312],{"class":194,"line":2297},37,[192,2299,2300],{"class":202},"    options.lazy ",[192,2302,233],{"class":198},[192,2304,2305],{"class":202}," options.lazy ",[192,2307,2308],{"class":198},"??",[192,2310,2311],{"class":206}," true",[192,2313,1662],{"class":202},[192,2315,2317],{"class":194,"line":2316},38,[192,2318,376],{"emptyLinePlaceholder":375},[192,2320,2322],{"class":194,"line":2321},39,[192,2323,2324],{"class":368},"    // const { baseUrl } = useRuntimeConfig().public;\n",[192,2326,2328],{"class":194,"line":2327},40,[192,2329,376],{"emptyLinePlaceholder":375},[192,2331,2333,2335,2337,2339,2341,2343,2345],{"class":194,"line":2332},41,[192,2334,2054],{"class":198},[192,2336,1497],{"class":239},[192,2338,1147],{"class":202},[192,2340,1783],{"class":239},[192,2342,1147],{"class":202},[192,2344,1771],{"class":239},[192,2346,2347],{"class":202},">>(url, {\n",[192,2349,2351],{"class":194,"line":2350},42,[192,2352,2353],{"class":368},"        // Request interception\n",[192,2355,2357,2360,2363,2365],{"class":194,"line":2356},43,[192,2358,2359],{"class":239},"        onRequest",[192,2361,2362],{"class":202},"({ ",[192,2364,664],{"class":440},[192,2366,2367],{"class":202}," }) {\n",[192,2369,2371],{"class":194,"line":2370},44,[192,2372,2373],{"class":368},"            // options.baseURL = baseUrl;\n",[192,2375,2377],{"class":194,"line":2376},45,[192,2378,2379],{"class":368},"            // Set the base URL\n",[192,2381,2383],{"class":194,"line":2382},46,[192,2384,2385],{"class":202},"        },\n",[192,2387,2389],{"class":194,"line":2388},47,[192,2390,2391],{"class":368},"        // Response interception\n",[192,2393,2395,2398,2400,2403],{"class":194,"line":2394},48,[192,2396,2397],{"class":239},"        onResponse",[192,2399,243],{"class":202},[192,2401,2402],{"class":440},"_context",[192,2404,1951],{"class":202},[192,2406,2408],{"class":194,"line":2407},49,[192,2409,2410],{"class":368},"            // Handle the response\n",[192,2412,2414],{"class":194,"line":2413},50,[192,2415,2385],{"class":202},[192,2417,2419],{"class":194,"line":2418},51,[192,2420,2421],{"class":368},"        // Error interception\n",[192,2423,2425,2428,2430,2433,2435,2437,2440,2442],{"class":194,"line":2424},52,[192,2426,2427],{"class":239},"        onResponseError",[192,2429,2362],{"class":202},[192,2431,2432],{"class":440},"response",[192,2434,209],{"class":202},[192,2436,664],{"class":440},[192,2438,2439],{"class":202},": { ",[192,2441,165],{"class":440},[192,2443,2444],{"class":202}," } }) {\n",[192,2446,2448,2451,2453,2455],{"class":194,"line":2447},53,[192,2449,2450],{"class":239},"            handleError",[192,2452,1147],{"class":202},[192,2454,1771],{"class":239},[192,2456,2457],{"class":202},">(method, response);\n",[192,2459,2461],{"class":194,"line":2460},54,[192,2462,2385],{"class":202},[192,2464,2466],{"class":194,"line":2465},55,[192,2467,2468],{"class":368},"        // Set the cache key\n",[192,2470,2472,2475,2477,2480,2483,2486,2489,2492,2495,2498],{"class":194,"line":2471},56,[192,2473,2474],{"class":202},"        key: key ",[192,2476,2308],{"class":198},[192,2478,2479],{"class":239}," hash",[192,2481,2482],{"class":202},"([",[192,2484,2485],{"class":246},"'api-fetch'",[192,2487,2488],{"class":202},", url, ",[192,2490,2491],{"class":206},"JSON",[192,2493,2494],{"class":202},".",[192,2496,2497],{"class":239},"stringify",[192,2499,2500],{"class":202},"({ method: options.method, params: options.params })]),\n",[192,2502,2504],{"class":194,"line":2503},57,[192,2505,2506],{"class":368},"        // Merge the options\n",[192,2508,2510,2513],{"class":194,"line":2509},58,[192,2511,2512],{"class":198},"        ...",[192,2514,2515],{"class":202},"options,\n",[192,2517,2519,2522,2524,2526,2528,2530,2532,2534,2536,2538,2540,2542,2544,2546],{"class":194,"line":2518},59,[192,2520,2521],{"class":202},"    }) ",[192,2523,2281],{"class":198},[192,2525,2130],{"class":239},[192,2527,1147],{"class":202},[192,2529,1783],{"class":239},[192,2531,1147],{"class":202},[192,2533,1771],{"class":239},[192,2535,1790],{"class":202},[192,2537,2143],{"class":239},[192,2539,1147],{"class":202},[192,2541,1783],{"class":239},[192,2543,1147],{"class":202},[192,2545,1771],{"class":239},[192,2547,2548],{"class":202},">>>;\n",[192,2550,2552],{"class":194,"line":2551},60,[192,2553,507],{"class":202},[192,2555,2557],{"class":194,"line":2556},61,[192,2558,376],{"emptyLinePlaceholder":375},[192,2560,2562,2565,2568,2571,2573],{"class":194,"line":2561},62,[192,2563,2564],{"class":198},"export",[192,2566,2567],{"class":198}," const",[192,2569,2570],{"class":206}," $http",[192,2572,325],{"class":198},[192,2574,397],{"class":202},[192,2576,2578,2581,2584,2586,2588,2590,2592,2594,2596,2598,2600,2603,2605,2608,2610,2612,2614,2616,2619,2621],{"class":194,"line":2577},63,[192,2579,2580],{"class":239},"    get",[192,2582,2583],{"class":202},": \u003C",[192,2585,1771],{"class":239},[192,2587,2102],{"class":202},[192,2589,2105],{"class":440},[192,2591,1258],{"class":198},[192,2593,1715],{"class":239},[192,2595,209],{"class":202},[192,2597,2174],{"class":440},[192,2599,1165],{"class":198},[192,2601,2602],{"class":239}," SearchParameters",[192,2604,209],{"class":202},[192,2606,2607],{"class":440},"option",[192,2609,1165],{"class":198},[192,2611,1766],{"class":239},[192,2613,1147],{"class":202},[192,2615,1771],{"class":239},[192,2617,2618],{"class":202},">) ",[192,2620,253],{"class":198},[192,2622,397],{"class":202},[192,2624,2626,2629,2631,2633,2635,2638,2641,2644,2647],{"class":194,"line":2625},64,[192,2627,2628],{"class":198},"        return",[192,2630,423],{"class":239},[192,2632,1147],{"class":202},[192,2634,1771],{"class":239},[192,2636,2637],{"class":202},">(url, { method: ",[192,2639,2640],{"class":246},"'get'",[192,2642,2643],{"class":202},", params, ",[192,2645,2646],{"class":198},"...",[192,2648,2649],{"class":202},"option });\n",[192,2651,2653],{"class":194,"line":2652},65,[192,2654,2655],{"class":202},"    },\n",[192,2657,2659],{"class":194,"line":2658},66,[192,2660,376],{"emptyLinePlaceholder":375},[192,2662,2664,2667,2669,2671,2673,2675,2677,2679,2681,2684,2686,2689,2692,2695,2698,2700,2702,2704,2706,2708,2710,2712,2714,2716,2718,2720,2722,2724,2726],{"class":194,"line":2663},67,[192,2665,2666],{"class":239},"    post",[192,2668,2583],{"class":202},[192,2670,1771],{"class":239},[192,2672,2102],{"class":202},[192,2674,2105],{"class":440},[192,2676,1258],{"class":198},[192,2678,1715],{"class":239},[192,2680,209],{"class":202},[192,2682,2683],{"class":440},"body",[192,2685,1165],{"class":198},[192,2687,2688],{"class":239}," RequestInit",[192,2690,2691],{"class":202},"[",[192,2693,2694],{"class":246},"'body'",[192,2696,2697],{"class":202},"] ",[192,2699,1239],{"class":198},[192,2701,2037],{"class":239},[192,2703,1147],{"class":202},[192,2705,1733],{"class":206},[192,2707,209],{"class":202},[192,2709,1806],{"class":206},[192,2711,1790],{"class":202},[192,2713,2607],{"class":440},[192,2715,1165],{"class":198},[192,2717,1766],{"class":239},[192,2719,1147],{"class":202},[192,2721,1771],{"class":239},[192,2723,2618],{"class":202},[192,2725,253],{"class":198},[192,2727,397],{"class":202},[192,2729,2731,2733,2735,2737,2739,2741,2744,2747,2749],{"class":194,"line":2730},68,[192,2732,2628],{"class":198},[192,2734,423],{"class":239},[192,2736,1147],{"class":202},[192,2738,1771],{"class":239},[192,2740,2637],{"class":202},[192,2742,2743],{"class":246},"'post'",[192,2745,2746],{"class":202},", body, ",[192,2748,2646],{"class":198},[192,2750,2649],{"class":202},[192,2752,2754],{"class":194,"line":2753},69,[192,2755,1987],{"class":202},[192,2757,2759],{"class":194,"line":2758},70,[192,2760,2761],{"class":202},"};\n",[192,2763,2765],{"class":194,"line":2764},71,[192,2766,376],{"emptyLinePlaceholder":375},[192,2768,2770,2772,2775,2778,2781],{"class":194,"line":2769},72,[192,2771,2564],{"class":198},[192,2773,2774],{"class":198}," default",[192,2776,2777],{"class":198}," function",[192,2779,2780],{"class":239}," UseHttp",[192,2782,2783],{"class":202},"() {\n",[192,2785,2787,2789],{"class":194,"line":2786},73,[192,2788,2054],{"class":198},[192,2790,397],{"class":202},[192,2792,2794],{"class":194,"line":2793},74,[192,2795,2796],{"class":202},"        $http,\n",[192,2798,2800],{"class":194,"line":2799},75,[192,2801,2802],{"class":202},"    };\n",[192,2804,2806],{"class":194,"line":2805},76,[192,2807,507],{"class":202},[11,2809,2810],{},"总体封装的很简单，基本没啥看不懂的点，细节还是要自己去扩充。",[11,2812,2813,2814,2816],{},"其中 ",[22,2815,1783],{}," 需要根据自己的返回值类型修改",[11,2818,2819,2822,2823,2826],{},[22,2820,2821],{},"handleError"," 负责在 ",[22,2824,2825],{},"onResponseError"," 时处理错误。",[11,2828,2829,2832,2833,2836,2837,2840],{},[22,2830,2831],{},"onRequest"," 可以自行添加比如 ",[22,2834,2835],{},"baseUrl"," 、自定义 ",[22,2838,2839],{},"header"," 等。",[11,2842,2843,2845],{},[22,2844,669],{}," 默认开启是最好用的，loading（status）状态自己维护",[11,2847,2848],{},"使用时的几种情况大概是这样：",[2850,2851,2852,2855,2858,2863],"ol",{},[43,2853,2854],{},"基于请求传入的 ref 对象，自动重新请求",[43,2856,2857],{},"watch 其他 ref 对象，以重新请求",[43,2859,2860,2861],{},"不需要马上在页面中展示的，",[22,2862,830],{},[43,2864,2865],{},"除此之外基本用默认配置就可以了",[183,2867,2869],{"className":185,"code":2868,"language":187,"meta":188,"style":188},"// hash('memo-list-search')\nconst { data: memoList, error, status } = await $http.get('/api/v1/memo/list', { page: 1, size: 100 }, { key: hash('memo-list-search'), server: false, watch: [user, refreshKey] })\n\nconst { data: memoList, error, status } = await $http.get('/api/v1/memo/list', { page, size: 100 }, { key: hash('memo-list-search'), server: false })\n",[22,2870,2871,2876,2944,2948],{"__ignoreMap":188},[192,2872,2873],{"class":194,"line":195},[192,2874,2875],{"class":368},"// hash('memo-list-search')\n",[192,2877,2878,2880,2882,2884,2886,2889,2891,2893,2895,2897,2899,2901,2903,2906,2909,2911,2914,2917,2919,2922,2925,2928,2931,2933,2936,2939,2941],{"class":194,"line":334},[192,2879,199],{"class":198},[192,2881,203],{"class":202},[192,2883,133],{"class":440},[192,2885,552],{"class":202},[192,2887,2888],{"class":206},"memoList",[192,2890,209],{"class":202},[192,2892,212],{"class":206},[192,2894,209],{"class":202},[192,2896,222],{"class":206},[192,2898,230],{"class":202},[192,2900,233],{"class":198},[192,2902,236],{"class":198},[192,2904,2905],{"class":202}," $http.",[192,2907,2908],{"class":239},"get",[192,2910,243],{"class":202},[192,2912,2913],{"class":246},"'/api/v1/memo/list'",[192,2915,2916],{"class":202},", { page: ",[192,2918,941],{"class":206},[192,2920,2921],{"class":202},", size: ",[192,2923,2924],{"class":206},"100",[192,2926,2927],{"class":202}," }, { key: ",[192,2929,2930],{"class":239},"hash",[192,2932,243],{"class":202},[192,2934,2935],{"class":246},"'memo-list-search'",[192,2937,2938],{"class":202},"), server: ",[192,2940,896],{"class":206},[192,2942,2943],{"class":202},", watch: [user, refreshKey] })\n",[192,2945,2946],{"class":194,"line":348},[192,2947,376],{"emptyLinePlaceholder":375},[192,2949,2950,2952,2954,2956,2958,2960,2962,2964,2966,2968,2970,2972,2974,2976,2978,2980,2982,2985,2987,2989,2991,2993,2995,2997,2999],{"class":194,"line":372},[192,2951,199],{"class":198},[192,2953,203],{"class":202},[192,2955,133],{"class":440},[192,2957,552],{"class":202},[192,2959,2888],{"class":206},[192,2961,209],{"class":202},[192,2963,212],{"class":206},[192,2965,209],{"class":202},[192,2967,222],{"class":206},[192,2969,230],{"class":202},[192,2971,233],{"class":198},[192,2973,236],{"class":198},[192,2975,2905],{"class":202},[192,2977,2908],{"class":239},[192,2979,243],{"class":202},[192,2981,2913],{"class":246},[192,2983,2984],{"class":202},", { page, size: ",[192,2986,2924],{"class":206},[192,2988,2927],{"class":202},[192,2990,2930],{"class":239},[192,2992,243],{"class":202},[192,2994,2935],{"class":246},[192,2996,2938],{"class":202},[192,2998,896],{"class":206},[192,3000,753],{"class":202},[11,3002,3003,3004,784],{},"重点是 ",[22,3005,1315],{},[11,3007,3008,3009,3012,3013,3016,3017,3019],{},"如果需要响应式请求，则",[74,3010,3011],{},"必须传入 key 值","，传入的对象会被 ",[22,3014,3015],{},"checkRef(params)"," 检测是不是 ",[22,3018,518],{}," ，其他选项（options）还是可以正常传入。",[11,3021,3022,3023,3026],{},"虽然代码里有个默认的 key 值，只是会打印一个错误，但这个 key 还是",[74,3024,3025],{},"建议手动传入","，以免造成key值重复时，发生错误的缓存问题。",[11,3028,3029,3030,3032,3033,3036,3037,178,3039,3041,3042,162,3045,3048],{},"虽然这里封装了 ",[22,3031,2821],{}," ，但实际上我没发现没什么用，原因是我用的 ",[22,3034,3035],{},"primevue/toast"," 目前不能套在 ",[22,3038,1373],{},[22,3040,151],{}," 中使用。我基本尝试了 ",[22,3043,3044],{},"github",[22,3046,3047],{},"stackoverflow"," 能搜到的几种方式都没有奏效。",[11,3050,3051,3052,3055,3056,3058,3059,3062,3063,3066],{},"而发生错误时提示出来基本是必做的一个动作，所以我只能把 ",[22,3053,3054],{},"show error toast"," 这个动作，放在了另一个 ",[22,3057,151],{}," （useErrorDispose）中，在使用 $http 的 vue 文件中，使用这个 ",[22,3060,3061],{},"useErrorDispose"," 处理 ",[22,3064,3065],{},"error.value","，以及本地的缓存信息。也算是完成了我的需求。",[11,3068,3069],{},"另外，请求时，（公司中）一般会设置 header 中携带 token，但实际上没有比 cookie 更好用",[183,3071,3073],{"className":185,"code":3072,"language":187,"meta":188,"style":188},"{\n    httpOnly: true,\n    sameSite: isProd ? 'strict' : 'lax',\n    maxAge: 2592000, // maxAge 优先级高， expires 受客户端时间的影响\n    secure: true,\n    domain: 'abc.com',\n  }\n",[22,3074,3075,3079,3090,3112,3127,3138,3150],{"__ignoreMap":188},[192,3076,3077],{"class":194,"line":195},[192,3078,612],{"class":202},[192,3080,3081,3084,3086,3088],{"class":194,"line":334},[192,3082,3083],{"class":239},"    httpOnly",[192,3085,552],{"class":202},[192,3087,750],{"class":206},[192,3089,625],{"class":202},[192,3091,3092,3095,3098,3101,3104,3107,3110],{"class":194,"line":348},[192,3093,3094],{"class":239},"    sameSite",[192,3096,3097],{"class":202},": isProd ",[192,3099,3100],{"class":198},"?",[192,3102,3103],{"class":246}," 'strict'",[192,3105,3106],{"class":198}," :",[192,3108,3109],{"class":246}," 'lax'",[192,3111,625],{"class":202},[192,3113,3114,3117,3119,3122,3124],{"class":194,"line":372},[192,3115,3116],{"class":239},"    maxAge",[192,3118,552],{"class":202},[192,3120,3121],{"class":206},"2592000",[192,3123,209],{"class":202},[192,3125,3126],{"class":368},"// maxAge 优先级高， expires 受客户端时间的影响\n",[192,3128,3129,3132,3134,3136],{"class":194,"line":379},[192,3130,3131],{"class":239},"    secure",[192,3133,552],{"class":202},[192,3135,750],{"class":206},[192,3137,625],{"class":202},[192,3139,3140,3143,3145,3148],{"class":194,"line":400},[192,3141,3142],{"class":239},"    domain",[192,3144,552],{"class":202},[192,3146,3147],{"class":246},"'abc.com'",[192,3149,625],{"class":202},[192,3151,3152],{"class":194,"line":411},[192,3153,3154],{"class":202},"  }\n",[11,3156,3157,3158,3161,3162,3165,3166,3168],{},"这样设置后，也能节省不少（操作 localstorage 的）代码，还比使用 ",[22,3159,3160],{},"localstorage"," 存储 ",[22,3163,3164],{},"token"," 再放在 ",[22,3167,2839],{}," 上更安全一些。",[11,3170,3171,3172,3175,3176,3179],{},"而 ",[22,3173,3174],{},"nuxt","（nitro）中操作 ",[22,3177,3178],{},"Cookie"," 也是十分简单的，可以自行了解一下。",[15,3181,3182],{"id":3182},"总结",[11,3184,3185,3186,134,3188,134,3190,3192],{},"以上就是本人在使用 ",[22,3187,18],{},[22,3189,1373],{},[22,3191,116],{},"时的一些经验分享，希望能够帮助到你。",[11,3194,3195],{},"同时，如果你有更多的经验，也希望在评论区指正文中的错误，让大家学到更多",[11,3197,3198,3199,3202],{},"更多 Nuxt 最新的全栈开发内容，欢迎关注「",[74,3200,3201],{},"早早集市","」",[3204,3205,3206],"style",{},"html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"title":188,"searchDepth":334,"depth":334,"links":3208},[3209,3210,3216,3217,3218],{"id":17,"depth":334,"text":18},{"id":115,"depth":334,"text":116,"children":3211},[3212,3213,3214,3215],{"id":669,"depth":348,"text":669},{"id":773,"depth":348,"text":773},{"id":901,"depth":348,"text":901},{"id":1079,"depth":348,"text":1079},{"id":1377,"depth":334,"text":1373},{"id":1617,"depth":334,"text":1617},{"id":3182,"depth":334,"text":3182},"2024-11-27T00:00:00.000Z","Nuxt3中 $fetch、useFetch、useAsyncData 的使用区别，各自的用法，以及最佳实践","md","2025-08-19T00:00:00.000Z",{},"/post/nuxt/nuxt3-fetch-usefetch-useasyncdata","---\ntitle: Nuxt3全栈开发 · $fetch、useFetch、useAsyncData 你用对了吗？\ndate: 2024-11-27\nlastmod: 2025-08-19\ntags: [\"Nuxt\"]\nversions: [\"nuxt@3.14.0\"]\ndescription: Nuxt3中 $fetch、useFetch、useAsyncData 的使用区别，各自的用法，以及最佳实践\n---\nNuxt3 中有三种获取数据的方式，看起来有点绕，那实际使用中有什么区别，应该怎样使用呢？\n\n## $fetch\n\n`$fetch` 基于 `ofetch` ，`ofetch` 是一个类似 `axios` 的请求库，可以运行在 node、浏览器、workers 上。\n\n所以它的用法类似原生 fetch 、axios，在 Nuxt3 中全局可用。\n\n- 在 app 中直接向 server 内的 api 发起请求\n- 在 app 中向其他服务发出请求\n- 在 server 的一个接口中向另一个接口请求\n- 在 server 的一个接口中向其他服务发出请求\n\n总之，是一个底层的请求库。\n\n用它是最简单的方法。\n\n但 Nuxt 是一个可以在服务器和客户端两个环境下运行同构代码的框架，如果在 setup 中使用 `$fetch` 来获取数据，可能会导致执行两次，一次在服务器上由 nitro 渲染 html 时，另一次是在客户端水合时。\n\n所以水合是一个必须要理解的过程：\n\n当在浏览器打开一个页面时，首先会在服务端渲染（SSR)，渲染后的**完整 html 代码**会被发送到客户端。此时已经在服务端拿到请求的数据了，所以客户端收到 HTML后，用户已经能够看到内容。\n\n对比单页应用（SPA），浏览器只是加载了一个 带有根元素的的 html 页面，所以此时页面是空白的，还需要在加载完相关 JS文件后，由 JS 去插入内容。所以单页应用只要在根元素 `div id = app` 里写点内容（如loading、骨架），就会早于实际内容出现，达到不白屏的效果。\n\n回到 `Nuxt` ，用户看到内容后，此时页面还无法交互，因为仅仅是渲染了 `HTML`，`Vue` 相关的逻辑还在 `JS` 里，需要下载和执行。\n\n**下载和执行时被 Vue 接管 HTML 的过程就叫水合**，水合后界面就可以响应用户的交互了。\n\n有了渲染方式的差异，才会有其他的请求方式来契合这种渲染方式。\n\n## useAsyncData\n\n从 `vue2` 到 `vue3`，我们都知道多了个选项式和组合式的区别。\n\n选项式中通常把功能集中在一个组件或函数中，每个组件都有 `data`、`methods`，组件内定义属性都会暴漏在函数内部的 `this` 上，指向当前实例。它的特点是，不关心响应式细节，强制按照选项来组织代码，你的后端同事看了 `Vue` 之后都表示很简单。\n\n而组合式（`composable`）的核心思想是直接在函数作用于内定义响应式变量，要比选项式自由和高效的多。\n\n所以 Vue3 的代码中，经常可以看到 `UseXXXX` 这类的函数，其内部就包含响应式变量，也就类似选项式代码中的 `data` 和 `method`。\n\n所以其内部的响应式变量发生变化时，通常会有一个对应的逻辑随之发生变化，可能是另一个响应式变量，也可能是与之对应的`Template`\n\n所以 `useAsyncData` 这个 `composable` ，也有类似的功能和效果。\n\n```typescript\nconst { data, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'))\n```\n\n`useAsyncData` 第一个参数是唯一键，用于**缓存**第二个参数的**响应**。\n\n所以用 `useAsyncData` 时，在 `SSR` 时和在 `水合` 时，不会发生两次重复的请求。可以保证渲染的一致性。\n\n其次，因为第二个参数只是一个获取数据的匿名函数，所以你可以用它来请求任意的服务，比如你用了其他 CMS服务来管理数据，这时候就应该使用 `useAsyncData`。\n\n这东西竟然有五个返回值，看看都有啥用，怎么就响应式了。\n\n`data`、`error`、`status` 这三个值，都是 Vue 的引用（ Vue refs accessible），也就是说类似于你用 `ref` 提前定义好了一样：\n\n```typescript\nconst data = ref()\nconst error = ref()\nconst status = ref('idle') // 还没请求\n\nconst fetchData = async () => {\n\tstatus.value = 'pendding'\n\tconst data = await fetch('xxxx').catch( err => {\n\t\terror.value = err\n\t\tstatus.value = 'error'\n\t})\n\t...\n\tdata.value = res.data\n\tstatus.value = 'success'\n}\n```\n\n这样看能明白了吧。\n\n`data` 就是我们接收返回后的数据的  `Ref` 引用，`error` 同理，`status` 则是类似于在 Vue2 中使用一个 `isFetch` 变量去管理 `loading` 状态。\n\n也可以直接给 data 重命名一下：\n\n```typescript\nconst { data: userList, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'))\n```\n\n不过要注意，这个 `data.value` 是什么，和第二个参数返回的数据是什么有关，比如你的接口固定返回：\n\n```typescript\n{\n\tcode: 200,\n\tdata: [],\n\tmsg: 'ok',\n}\n```\n\n那要想取得列表渲染要用的数据，应该是 `userList.value?.data` \n\n因为渲染方式的特点，`useAsyncData` 还可以传入第三个参数 `options` 来控制其行为。\n\n### lazy\n\n默认情况下，页面使用了 `useAsyncData` 来获取数据，这个 `composable` 会等待异步函数的解析，然后再导航到新的页面。\n\n解析会耗时，所以你的导航动作就会有延迟，给人一种：**点了，但是没立马动起来的迟滞感**。\n\n`lazy` 选项可以使其忽略异步函数的解析。\n\n```typescript\nconst { data: userList, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'), { lazy: true })\n```\n\n此时，当你点击进入一个新的页面时，导航不会被阻塞，但进入后内容可能还没拿到，所以需要使用 `status` 来加载 `loading`、`骨架组件`。\n\n我觉得应该没人想在点击后阻塞导航吧，所以这个 `lazy` 建议一直开启。\n\n### server\n\n上一篇关于在 Nuxt 中使用 Prisma 的文章里，我提到了本地有个 `dev.db` ，线上也有一个 `prod.db` 。\n\n两个数据库里存的东西肯定是不一样嘛，因为本地需要测试。所以我本地的 `userList` 里是 **张三**，线上的 `userList` 是**李四**。\n\n但在 `Nuxt` 打包时，会 `prerender`，预渲染！（我需要本地打包的）\n\n也就是先把接口请求一遍，把真实的 `HTML` 先给组装好，并且还有缓存。因为是为了 SEO，方便搜索引擎快速抓取到页面的内容。\n\n于是，在线上打开用户列表页时，我的张三被显示出来了，因为他就是 HTML 里的内容。\n\n这个时候就需要另一个选项： `server`\n\n当设置 `server: false` 时，第一次渲染就不会去请求数据，也就是会渲染出一个空的用户列表页。\n\n```typescript\nconst { data: userList, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'), { lazy: true, server: false })\n```\n\n### watch\n\n看到这，我也没看出来 useAsyncData 响应个啥了啊，不就是帮我省下了创建接收数据的 `ref` 和 管理状态的 `ref` 的功夫？\n\n那我们再把**获取列表这个场景**丰富一下。\n\n用户多了，有分页了，应该怎么处理？\n\n```typescript\nconst page = ref(1)\nconst changePage = () => {\n\tmyFetchData()\n}\n```\n\n如果再加个类型的筛选，日期的筛选等等一切和重新获取数据相关的响应式变量，都要写同样的代码。\n\n所以 `useAsyncData` 支持直接 `watch` 响应式变量：\n\n```typescript\nconst page = ref(1)\nconst { data: userList, error, clear, status, refresh } = await useAsyncData('users', () => myGetFunction('users'), { lazy: true, watch: [page, tags] })\n```\n\n当 `page` 发生变化时，`useAsyncData` 就会重新执行它的 `handler` ，做到刷新数据。\n\n代码量又减少了不少\n\n### 其他选项\n\n和 `vue` 的 `watch` 相比，`useAsyncData` 也有一个 `immediate` 选项，只不过它默认是 `true` ，你可以设置为 `false` 来阻止立即触发请求。\n\n还有一种场景，使用一个接口获取一组比较大的数据比如文章时，有时候不需要文章的内容，每次都传输内容的话，数据量太大了，影响传输速度。 \n\n可以使用 `pick` 选项，选取指定的键：`pick: [\"title\", \"description\"]`\n\n还有 `deep` 选项，默认为 `true` ，如果不需要深度深度响应时，可以设置为 `false` 以提高性能。\n\n其他选项就不一一介绍了，我直接放在这里，后续发现比较有用的场景，再来分享。\n\n```typescript\ntype AsyncDataOptions\u003CDataT> = {\n  server?: boolean\n  lazy?: boolean\n  immediate?: boolean\n  deep?: boolean\n  dedupe?: 'cancel' | 'defer'\n  default?: () => DataT | Ref\u003CDataT> | null\n  transform?: (input: DataT) => DataT | Promise\u003CDataT>\n  pick?: string[]\n  watch?: WatchSource[]\n  getCachedData?: (key: string, nuxtApp: NuxtApp) => DataT\n}\n```\n\n现在可以确定，`useAsyncData` 可以帮助缓存数据，防止多次请求，保证渲染的一致性。提供响应式的 `data`、`error`、`status` 来完成页面的渲染，同时提供一些选项来控制其行为。\n\n最后再来捋一捋什么场景下使用什么选项的问题：\n\n![](https://img.zzao.club/article/202411271747999.png)\n\n**PS：这图我在 Youtube 刷到的，但是没截屏，所以凭借印象又画了一遍**\n\n那 `useFetch` 还有什么活能整吗？\n\n## useFetch\n\n`useFetch` 的整活就像是：\n\n虽然在 `script` 需要使用 `.value` 获取响应式数据，但 `template` 中不需要，因为 `Vue` 帮你处理了。\n\n以前 `props` 解构会失去响应式，现在（3.5+）解构也能直接用了。\n\n是的，它就是个语法糖一样，是 `useAsyncData` 和 `$fetch` 的包装器。\n\n它不用传第一个 `key` 值，会根据 url 和选项自动生成，并且可以推断 API 的响应类型。\n\n同时有着和 useAsyncData 一样的第三个参数（选项），但做了一些增强。\n\n还是拿刚才获取用户列表的场景举例，我们监听 `page`、`tags`，变化时会再次请求。\n\n在 `useFetch` 中可以再进一步：\n\n```typescript\nconst page = ref(1)\nconst { data: userList, error, clear, status, refresh } = await useFetch('/api/user/list', { page }, { lazy: true })\n```\n\n我们只需要传入 `page` 这个响应式变量，`useFetch` 就会追踪到这个响应式变量。\n\n类似 `watch` 和 `watchEffect`。一个需要显式的传入，一个会自动的追踪。\n\n但是这里要注意：`{ page }` 等价于 `{ page: page }`， 而不是 `{ page: page.value }`\n\n**`page.value` 只是一个值！** 所以要传入的是 `page` 这个 `ref对象`\n\n**使用 `useFetch` 后代码被进一步简化。**\n\n所以在思考使用 `useFetch` 还是 `$fetch`  来进行接口请求时就十分明了了：\n\n一般基于用户的交互才去做出反应的，就用 `$fetch` 就可以。 \n\n如果基于页面状态需要重复获取数据的，就用 `useFetch` 。\n\n但刚才也强调了，传入的是一个 `ref对象` 才会时 `useFetch` 正常的响应式的自动请求，所以如果是**想统一请求方式**，也可以选择直接传值给 `useFetch` ，这样他就像一个普通的增强版 `$fetch` 一样了。\n\n但 `useFetch` 返回的值还是个响应式的，使用 `$fetch` 的场景很多都不需要返回值再触发其他响应，所以显得有些\"多余\"。\n\n怎么取舍就看自己了。\n\n## 最佳实践\n\n我在项目中以使用 `useFetch`为主 , 那怎么封装 `useFetch` 用起来才最顺手呢？\n\n经过我自己的使用，以及搜索多个关于 `useFetch` 的使用。最后在最少三个博客站内发现了同样的封装代码，究竟谁是作者我也不清楚，代码里也没有说明，这里直接贴给大家，再分析如何使用。\n\n`composables/useHttp.ts`\n\n```typescript\nimport type {FetchError, FetchResponse, SearchParameters} from 'ofetch';\nimport {hash} from 'ohash';\nimport type {AsyncData, UseFetchOptions} from '#app';\nimport type {KeysOf} from '#app/composables/asyncData';\ntype UrlType = string | Request | Ref\u003Cstring | Request> | (() => string | Request);\n\ntype HttpOption\u003CT> = UseFetchOptions\u003CResOptions\u003CT>, T, KeysOf\u003CT>, any>;\ninterface ResOptions\u003CT> {\n    data: T;\n    code: number;\n    message: boolean;\n    err?: string[];\n}\n\nfunction handleError\u003CT>(\n    _method: string | undefined,\n    _response: FetchResponse\u003CResOptions\u003CT>> & FetchResponse\u003Cany>,\n) {\n    // Implement error handling logic here\n    if (_response?._data?.statusCode === 401) { \n      // setUser('')\n    }\n    console.error(`[useHttp] [error] ${_method}:`, _response);\n}\n\nfunction checkRef(obj: Record\u003Cstring, any>) {\n    return Object.keys(obj).some(key => isRef(obj[key]));\n}\n\nfunction fetch\u003CT>(url: UrlType, opts: HttpOption\u003CT>): AsyncData\u003CResOptions\u003CT>, FetchError\u003CResOptions\u003CT>>> {\n    // Check the `key` option\n    const { key, params, watch } = opts;\n    if (!key && ((params && checkRef(params)) || (watch && checkRef(watch))))\n        console.error('\\x1B[31m%s\\x1B[0m %s', '[useHttp] [error]', 'The `key` option is required when `params` or `watch` has ref properties, please set a unique key for the current request.');\n\n    const options = opts as UseFetchOptions\u003CResOptions\u003CT>>;\n    options.lazy = options.lazy ?? true;\n\n    // const { baseUrl } = useRuntimeConfig().public;\n\n    return useFetch\u003CResOptions\u003CT>>(url, {\n        // Request interception\n        onRequest({ options }) {\n            // options.baseURL = baseUrl;\n            // Set the base URL\n        },\n        // Response interception\n        onResponse(_context) {\n            // Handle the response\n        },\n        // Error interception\n        onResponseError({ response, options: { method } }) {\n            handleError\u003CT>(method, response);\n        },\n        // Set the cache key\n        key: key ?? hash(['api-fetch', url, JSON.stringify({ method: options.method, params: options.params })]),\n        // Merge the options\n        ...options,\n    }) as AsyncData\u003CResOptions\u003CT>, FetchError\u003CResOptions\u003CT>>>;\n}\n\nexport const $http = {\n    get: \u003CT>(url: UrlType, params?: SearchParameters, option?: HttpOption\u003CT>) => {\n        return fetch\u003CT>(url, { method: 'get', params, ...option });\n    },\n\n    post: \u003CT>(url: UrlType, body?: RequestInit['body'] | Record\u003Cstring, any>, option?: HttpOption\u003CT>) => {\n        return fetch\u003CT>(url, { method: 'post', body, ...option });\n    }\n};\n\nexport default function UseHttp() {\n    return {\n        $http,\n    };\n}\n```\n\n总体封装的很简单，基本没啥看不懂的点，细节还是要自己去扩充。\n\n其中 `ResOptions` 需要根据自己的返回值类型修改\n\n`handleError` 负责在 `onResponseError` 时处理错误。\n\n`onRequest` 可以自行添加比如 `baseUrl` 、自定义 `header` 等。\n\n`lazy` 默认开启是最好用的，loading（status）状态自己维护\n\n使用时的几种情况大概是这样：\n\n1. 基于请求传入的 ref 对象，自动重新请求\n2. watch 其他 ref 对象，以重新请求\n3. 不需要马上在页面中展示的，`server: false`\n4. 除此之外基本用默认配置就可以了\n\n```typescript\n// hash('memo-list-search')\nconst { data: memoList, error, status } = await $http.get('/api/v1/memo/list', { page: 1, size: 100 }, { key: hash('memo-list-search'), server: false, watch: [user, refreshKey] })\n\nconst { data: memoList, error, status } = await $http.get('/api/v1/memo/list', { page, size: 100 }, { key: hash('memo-list-search'), server: false })\n```\n\n重点是 `key` 。\n\n如果需要响应式请求，则**必须传入 key 值**，传入的对象会被 `checkRef(params)` 检测是不是 `Ref` ，其他选项（options）还是可以正常传入。\n\n虽然代码里有个默认的 key 值，只是会打印一个错误，但这个 key 还是**建议手动传入**，以免造成key值重复时，发生错误的缓存问题。\n\n虽然这里封装了 `handleError` ，但实际上我没发现没什么用，原因是我用的 `primevue/toast` 目前不能套在 `useFetch` 这个 `composable` 中使用。我基本尝试了 `github` 和 `stackoverflow` 能搜到的几种方式都没有奏效。 \n\n而发生错误时提示出来基本是必做的一个动作，所以我只能把 `show error toast` 这个动作，放在了另一个 `composable` （useErrorDispose）中，在使用 $http 的 vue 文件中，使用这个 `useErrorDispose` 处理 `error.value`，以及本地的缓存信息。也算是完成了我的需求。\n\n另外，请求时，（公司中）一般会设置 header 中携带 token，但实际上没有比 cookie 更好用\n\n```typescript\n{\n    httpOnly: true,\n    sameSite: isProd ? 'strict' : 'lax',\n    maxAge: 2592000, // maxAge 优先级高， expires 受客户端时间的影响\n    secure: true,\n    domain: 'abc.com',\n  }\n```\n\n这样设置后，也能节省不少（操作 localstorage 的）代码，还比使用 `localstorage` 存储 `token` 再放在 `header` 上更安全一些。\n\n而 `nuxt`（nitro）中操作 `Cookie` 也是十分简单的，可以自行了解一下。\n\n## 总结\n\n以上就是本人在使用 `$fetch`、`useFetch`、`useAsyncData`时的一些经验分享，希望能够帮助到你。\n\n同时，如果你有更多的经验，也希望在评论区指正文中的错误，让大家学到更多\n\n更多 Nuxt 最新的全栈开发内容，欢迎关注「**早早集市**」\n\n\n\n\n\n\n \n\n\n\n\n\n\n\n",{"title":5,"description":3220},"post/nuxt/Nuxt3-fetch-useFetch-useAsyncData",[90],[3230],"nuxt@3.14.0","aXycpwaGCIavvtJjcilSHDVUcVZVehR2jUm56gWlYyI",[3233,3237],{"title":3234,"path":3235,"stem":3236},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":3238,"path":3239,"stem":3240},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005086516]