[{"data":1,"prerenderedAt":2828},["ShallowReactive",2],{"page-/post/hono/hono-params-check-response-standardized":3,"surrounding-page":2819},{"id":4,"title":5,"author":6,"body":7,"date":2807,"description":58,"extension":2808,"group":6,"lastmod":2809,"meta":2810,"navigation":146,"path":2811,"rawbody":2812,"seo":2813,"showTitle":5,"stem":2814,"tags":2815,"versions":2816,"__hash__":2818},"content/post/Hono/hono-params-check-response-standardized.md","【Hono】完善：参数校验+响应标准化",null,{"type":8,"value":9,"toc":2801},"minimark",[10,13,22,25,29,32,46,49,52,165,168,253,256,262,268,420,423,513,516,899,906,913,1060,1067,1219,1229,1252,1255,1390,1393,1631,1645,1652,1849,1852,1855,1861,1886,1901,1904,2070,2073,2077,2080,2083,2086,2089,2094,2131,2134,2311,2318,2334,2340,2343,2346,2376,2381,2384,2716,2730,2733,2736,2750,2757,2770,2776,2786,2789,2794,2797],[11,12],"br",{},[14,15,16,17,21],"p",{},"上一章我们完成了基于",[18,19,20],"code",{},"Hono","的web项目的搭建工作，并实现了路由分组，错误处理等逻辑。",[14,23,24],{},"这一章来继续完善项目，让它变的健壮起来💪。",[26,27,28],"h2",{"id":28},"参数校验",[14,30,31],{},"平时我们写前端的时候，最希望后端把校验做的越全越好，提示信息越详细越友好越好，那现在就轮到我们自己实现后端了。",[14,33,34,37,38,41,42,45],{},[18,35,36],{},"hono"," 官方比较推荐的是",[18,39,40],{},"zod","作为校验库，并提供了",[18,43,44],{},"@hono/zod-validator","，封装了一下中间件，让我们可以直接放在请求路径后面用。",[14,47,48],{},"并且官方并不推荐路由第二个参数再写个 handler 封装，然后再传进来",[14,50,51],{},"这种写法在其他node框架中十分常见，如Koa、Nest",[53,54,59],"pre",{"className":55,"code":56,"language":57,"meta":58,"style":58},"language-typescript shiki shiki-themes github-light","// 🙁\n// A RoR-like Controller\nconst booksList = (c: Context) => {\n  return c.json('list books')\n}\n\napp.get('/books', booksList)\n","typescript","",[18,60,61,70,76,113,135,141,148],{"__ignoreMap":58},[62,63,66],"span",{"class":64,"line":65},"line",1,[62,67,69],{"class":68},"sAwPA","// 🙁\n",[62,71,73],{"class":64,"line":72},2,[62,74,75],{"class":68},"// A RoR-like Controller\n",[62,77,79,83,87,90,94,98,101,104,107,110],{"class":64,"line":78},3,[62,80,82],{"class":81},"sD7c4","const",[62,84,86],{"class":85},"s7eDp"," booksList",[62,88,89],{"class":81}," =",[62,91,93],{"class":92},"sgsFI"," (",[62,95,97],{"class":96},"sqxcx","c",[62,99,100],{"class":81},":",[62,102,103],{"class":85}," Context",[62,105,106],{"class":92},") ",[62,108,109],{"class":81},"=>",[62,111,112],{"class":92}," {\n",[62,114,116,119,122,125,128,132],{"class":64,"line":115},4,[62,117,118],{"class":81},"  return",[62,120,121],{"class":92}," c.",[62,123,124],{"class":85},"json",[62,126,127],{"class":92},"(",[62,129,131],{"class":130},"sYBdl","'list books'",[62,133,134],{"class":92},")\n",[62,136,138],{"class":64,"line":137},5,[62,139,140],{"class":92},"}\n",[62,142,144],{"class":64,"line":143},6,[62,145,147],{"emptyLinePlaceholder":146},true,"\n",[62,149,151,154,157,159,162],{"class":64,"line":150},7,[62,152,153],{"class":92},"app.",[62,155,156],{"class":85},"get",[62,158,127],{"class":92},[62,160,161],{"class":130},"'/books'",[62,163,164],{"class":92},", booksList)\n",[14,166,167],{},"下面是官方推荐的写法",[53,169,171],{"className":55,"code":170,"language":57,"meta":58,"style":58},"// 😃\napp.get('/books/:id', (c) => {\n  const id = c.req.param('id') // Can infer the path param\n  return c.json(`get ${id}`)\n})\n",[18,172,173,178,200,227,248],{"__ignoreMap":58},[62,174,175],{"class":64,"line":65},[62,176,177],{"class":68},"// 😃\n",[62,179,180,182,184,186,189,192,194,196,198],{"class":64,"line":72},[62,181,153],{"class":92},[62,183,156],{"class":85},[62,185,127],{"class":92},[62,187,188],{"class":130},"'/books/:id'",[62,190,191],{"class":92},", (",[62,193,97],{"class":96},[62,195,106],{"class":92},[62,197,109],{"class":81},[62,199,112],{"class":92},[62,201,202,205,209,211,214,217,219,222,224],{"class":64,"line":78},[62,203,204],{"class":81},"  const",[62,206,208],{"class":207},"sYu0t"," id",[62,210,89],{"class":81},[62,212,213],{"class":92}," c.req.",[62,215,216],{"class":85},"param",[62,218,127],{"class":92},[62,220,221],{"class":130},"'id'",[62,223,106],{"class":92},[62,225,226],{"class":68},"// Can infer the path param\n",[62,228,229,231,233,235,237,240,243,246],{"class":64,"line":115},[62,230,118],{"class":81},[62,232,121],{"class":92},[62,234,124],{"class":85},[62,236,127],{"class":92},[62,238,239],{"class":130},"`get ${",[62,241,242],{"class":92},"id",[62,244,245],{"class":130},"}`",[62,247,134],{"class":92},[62,249,250],{"class":64,"line":137},[62,251,252],{"class":92},"})\n",[14,254,255],{},"上述写法的原因和类型有关，如果不写复杂的泛型，就无法在Controller中推断出路径参数。",[14,257,258],{},[259,260,261],"strong",{},"这样也好，在业务真正复杂前来之前，保持程序的简洁。",[14,263,264,265,267],{},"回到参数校验部分，",[18,266,44],{},"使用比较简单，就是在路由第二个参数插入这个中间件",[53,269,271],{"className":55,"code":270,"language":57,"meta":58,"style":58},"import { zValidator } from '@hono/zod-validator'\n\nconst route = app.post(\n  '/posts',\n  zValidator(\n    'form',\n    z.object({\n      body: z.string(),\n    })\n  ),\n  (c) => {\n    const validated = c.req.valid('form')\n    // ... use your validated data\n  }\n)\n",[18,272,273,287,291,309,317,324,331,342,354,360,366,380,403,409,415],{"__ignoreMap":58},[62,274,275,278,281,284],{"class":64,"line":65},[62,276,277],{"class":81},"import",[62,279,280],{"class":92}," { zValidator } ",[62,282,283],{"class":81},"from",[62,285,286],{"class":130}," '@hono/zod-validator'\n",[62,288,289],{"class":64,"line":72},[62,290,147],{"emptyLinePlaceholder":146},[62,292,293,295,298,300,303,306],{"class":64,"line":78},[62,294,82],{"class":81},[62,296,297],{"class":207}," route",[62,299,89],{"class":81},[62,301,302],{"class":92}," app.",[62,304,305],{"class":85},"post",[62,307,308],{"class":92},"(\n",[62,310,311,314],{"class":64,"line":115},[62,312,313],{"class":130},"  '/posts'",[62,315,316],{"class":92},",\n",[62,318,319,322],{"class":64,"line":137},[62,320,321],{"class":85},"  zValidator",[62,323,308],{"class":92},[62,325,326,329],{"class":64,"line":143},[62,327,328],{"class":130},"    'form'",[62,330,316],{"class":92},[62,332,333,336,339],{"class":64,"line":150},[62,334,335],{"class":92},"    z.",[62,337,338],{"class":85},"object",[62,340,341],{"class":92},"({\n",[62,343,345,348,351],{"class":64,"line":344},8,[62,346,347],{"class":92},"      body: z.",[62,349,350],{"class":85},"string",[62,352,353],{"class":92},"(),\n",[62,355,357],{"class":64,"line":356},9,[62,358,359],{"class":92},"    })\n",[62,361,363],{"class":64,"line":362},10,[62,364,365],{"class":92},"  ),\n",[62,367,369,372,374,376,378],{"class":64,"line":368},11,[62,370,371],{"class":92},"  (",[62,373,97],{"class":96},[62,375,106],{"class":92},[62,377,109],{"class":81},[62,379,112],{"class":92},[62,381,383,386,389,391,393,396,398,401],{"class":64,"line":382},12,[62,384,385],{"class":81},"    const",[62,387,388],{"class":207}," validated",[62,390,89],{"class":81},[62,392,213],{"class":92},[62,394,395],{"class":85},"valid",[62,397,127],{"class":92},[62,399,400],{"class":130},"'form'",[62,402,134],{"class":92},[62,404,406],{"class":64,"line":405},13,[62,407,408],{"class":68},"    // ... use your validated data\n",[62,410,412],{"class":64,"line":411},14,[62,413,414],{"class":92},"  }\n",[62,416,418],{"class":64,"line":417},15,[62,419,134],{"class":92},[14,421,422],{},"如果需要多个验证器",[53,424,426],{"className":55,"code":425,"language":57,"meta":58,"style":58},"app.post(\n  '/posts/:id',\n  validator('param', ...),\n  validator('query', ...),\n  validator('json', ...),\n  (c) => {\n    //...\n  }\n",[18,427,428,436,443,462,477,492,504,509],{"__ignoreMap":58},[62,429,430,432,434],{"class":64,"line":65},[62,431,153],{"class":92},[62,433,305],{"class":85},[62,435,308],{"class":92},[62,437,438,441],{"class":64,"line":72},[62,439,440],{"class":130},"  '/posts/:id'",[62,442,316],{"class":92},[62,444,445,448,450,453,456,459],{"class":64,"line":78},[62,446,447],{"class":85},"  validator",[62,449,127],{"class":92},[62,451,452],{"class":130},"'param'",[62,454,455],{"class":92},", ",[62,457,458],{"class":81},"...",[62,460,461],{"class":92},"),\n",[62,463,464,466,468,471,473,475],{"class":64,"line":115},[62,465,447],{"class":85},[62,467,127],{"class":92},[62,469,470],{"class":130},"'query'",[62,472,455],{"class":92},[62,474,458],{"class":81},[62,476,461],{"class":92},[62,478,479,481,483,486,488,490],{"class":64,"line":137},[62,480,447],{"class":85},[62,482,127],{"class":92},[62,484,485],{"class":130},"'json'",[62,487,455],{"class":92},[62,489,458],{"class":81},[62,491,461],{"class":92},[62,493,494,496,498,500,502],{"class":64,"line":143},[62,495,371],{"class":92},[62,497,97],{"class":96},[62,499,106],{"class":92},[62,501,109],{"class":81},[62,503,112],{"class":92},[62,505,506],{"class":64,"line":150},[62,507,508],{"class":68},"    //...\n",[62,510,511],{"class":64,"line":344},[62,512,414],{"class":92},[14,514,515],{},"加入校验后，来使用 apifox 测试一下，可以看到 zod 返回如下的校验结果",[53,517,520],{"className":518,"code":519,"language":124,"meta":58,"style":58},"language-json shiki shiki-themes github-light","{\n    \"success\": false,\n    \"error\": {\n        \"issues\": [\n            {\n                \"code\": \"invalid_type\",\n                \"expected\": \"number\",\n                \"received\": \"undefined\",\n                \"path\": [\n                    \"page\"\n                ],\n                \"message\": \"Required\"\n            },\n            {\n                \"code\": \"invalid_type\",\n                \"expected\": \"number\",\n                \"received\": \"undefined\",\n                \"path\": [\n                    \"size\"\n                ],\n                \"message\": \"Required\"\n            }\n        ],\n        \"name\": \"ZodError\"\n    },\n    \"_error\": {\n        \"issues\": [\n            {\n                \"code\": \"invalid_type\",\n                \"expected\": \"number\",\n                \"received\": \"undefined\",\n                \"path\": [\n                    \"page\"\n                ],\n                \"message\": \"Required\"\n            },\n            {\n                \"code\": \"invalid_type\",\n                \"expected\": \"number\",\n                \"received\": \"undefined\",\n                \"path\": [\n                    \"size\"\n                ],\n                \"message\": \"Required\"\n            }\n        ],\n        \"name\": \"ZodError\"\n    }\n}\n",[18,521,522,527,540,548,556,561,573,585,597,604,609,614,624,629,633,643,654,665,672,678,683,692,698,704,715,721,729,736,741,752,763,774,781,786,791,800,805,810,821,832,843,850,855,860,869,874,879,888,894],{"__ignoreMap":58},[62,523,524],{"class":64,"line":65},[62,525,526],{"class":92},"{\n",[62,528,529,532,535,538],{"class":64,"line":72},[62,530,531],{"class":207},"    \"success\"",[62,533,534],{"class":92},": ",[62,536,537],{"class":207},"false",[62,539,316],{"class":92},[62,541,542,545],{"class":64,"line":78},[62,543,544],{"class":207},"    \"error\"",[62,546,547],{"class":92},": {\n",[62,549,550,553],{"class":64,"line":115},[62,551,552],{"class":207},"        \"issues\"",[62,554,555],{"class":92},": [\n",[62,557,558],{"class":64,"line":137},[62,559,560],{"class":92},"            {\n",[62,562,563,566,568,571],{"class":64,"line":143},[62,564,565],{"class":207},"                \"code\"",[62,567,534],{"class":92},[62,569,570],{"class":130},"\"invalid_type\"",[62,572,316],{"class":92},[62,574,575,578,580,583],{"class":64,"line":150},[62,576,577],{"class":207},"                \"expected\"",[62,579,534],{"class":92},[62,581,582],{"class":130},"\"number\"",[62,584,316],{"class":92},[62,586,587,590,592,595],{"class":64,"line":344},[62,588,589],{"class":207},"                \"received\"",[62,591,534],{"class":92},[62,593,594],{"class":130},"\"undefined\"",[62,596,316],{"class":92},[62,598,599,602],{"class":64,"line":356},[62,600,601],{"class":207},"                \"path\"",[62,603,555],{"class":92},[62,605,606],{"class":64,"line":362},[62,607,608],{"class":130},"                    \"page\"\n",[62,610,611],{"class":64,"line":368},[62,612,613],{"class":92},"                ],\n",[62,615,616,619,621],{"class":64,"line":382},[62,617,618],{"class":207},"                \"message\"",[62,620,534],{"class":92},[62,622,623],{"class":130},"\"Required\"\n",[62,625,626],{"class":64,"line":405},[62,627,628],{"class":92},"            },\n",[62,630,631],{"class":64,"line":411},[62,632,560],{"class":92},[62,634,635,637,639,641],{"class":64,"line":417},[62,636,565],{"class":207},[62,638,534],{"class":92},[62,640,570],{"class":130},[62,642,316],{"class":92},[62,644,646,648,650,652],{"class":64,"line":645},16,[62,647,577],{"class":207},[62,649,534],{"class":92},[62,651,582],{"class":130},[62,653,316],{"class":92},[62,655,657,659,661,663],{"class":64,"line":656},17,[62,658,589],{"class":207},[62,660,534],{"class":92},[62,662,594],{"class":130},[62,664,316],{"class":92},[62,666,668,670],{"class":64,"line":667},18,[62,669,601],{"class":207},[62,671,555],{"class":92},[62,673,675],{"class":64,"line":674},19,[62,676,677],{"class":130},"                    \"size\"\n",[62,679,681],{"class":64,"line":680},20,[62,682,613],{"class":92},[62,684,686,688,690],{"class":64,"line":685},21,[62,687,618],{"class":207},[62,689,534],{"class":92},[62,691,623],{"class":130},[62,693,695],{"class":64,"line":694},22,[62,696,697],{"class":92},"            }\n",[62,699,701],{"class":64,"line":700},23,[62,702,703],{"class":92},"        ],\n",[62,705,707,710,712],{"class":64,"line":706},24,[62,708,709],{"class":207},"        \"name\"",[62,711,534],{"class":92},[62,713,714],{"class":130},"\"ZodError\"\n",[62,716,718],{"class":64,"line":717},25,[62,719,720],{"class":92},"    },\n",[62,722,724,727],{"class":64,"line":723},26,[62,725,726],{"class":207},"    \"_error\"",[62,728,547],{"class":92},[62,730,732,734],{"class":64,"line":731},27,[62,733,552],{"class":207},[62,735,555],{"class":92},[62,737,739],{"class":64,"line":738},28,[62,740,560],{"class":92},[62,742,744,746,748,750],{"class":64,"line":743},29,[62,745,565],{"class":207},[62,747,534],{"class":92},[62,749,570],{"class":130},[62,751,316],{"class":92},[62,753,755,757,759,761],{"class":64,"line":754},30,[62,756,577],{"class":207},[62,758,534],{"class":92},[62,760,582],{"class":130},[62,762,316],{"class":92},[62,764,766,768,770,772],{"class":64,"line":765},31,[62,767,589],{"class":207},[62,769,534],{"class":92},[62,771,594],{"class":130},[62,773,316],{"class":92},[62,775,777,779],{"class":64,"line":776},32,[62,778,601],{"class":207},[62,780,555],{"class":92},[62,782,784],{"class":64,"line":783},33,[62,785,608],{"class":130},[62,787,789],{"class":64,"line":788},34,[62,790,613],{"class":92},[62,792,794,796,798],{"class":64,"line":793},35,[62,795,618],{"class":207},[62,797,534],{"class":92},[62,799,623],{"class":130},[62,801,803],{"class":64,"line":802},36,[62,804,628],{"class":92},[62,806,808],{"class":64,"line":807},37,[62,809,560],{"class":92},[62,811,813,815,817,819],{"class":64,"line":812},38,[62,814,565],{"class":207},[62,816,534],{"class":92},[62,818,570],{"class":130},[62,820,316],{"class":92},[62,822,824,826,828,830],{"class":64,"line":823},39,[62,825,577],{"class":207},[62,827,534],{"class":92},[62,829,582],{"class":130},[62,831,316],{"class":92},[62,833,835,837,839,841],{"class":64,"line":834},40,[62,836,589],{"class":207},[62,838,534],{"class":92},[62,840,594],{"class":130},[62,842,316],{"class":92},[62,844,846,848],{"class":64,"line":845},41,[62,847,601],{"class":207},[62,849,555],{"class":92},[62,851,853],{"class":64,"line":852},42,[62,854,677],{"class":130},[62,856,858],{"class":64,"line":857},43,[62,859,613],{"class":92},[62,861,863,865,867],{"class":64,"line":862},44,[62,864,618],{"class":207},[62,866,534],{"class":92},[62,868,623],{"class":130},[62,870,872],{"class":64,"line":871},45,[62,873,697],{"class":92},[62,875,877],{"class":64,"line":876},46,[62,878,703],{"class":92},[62,880,882,884,886],{"class":64,"line":881},47,[62,883,709],{"class":207},[62,885,534],{"class":92},[62,887,714],{"class":130},[62,889,891],{"class":64,"line":890},48,[62,892,893],{"class":92},"    }\n",[62,895,897],{"class":64,"line":896},49,[62,898,140],{"class":92},[14,900,901,902,905],{},"可以看到返回了详细的校验信息，但美中不足的就是这个中间件",[259,903,904],{},"会以自己的结构直接返回","给前端，这显然不合理，我们要的是标准化的返回。",[14,907,908,909,912],{},"所以这个中间件还可以",[259,910,911],{},"传入第三个参数作为回调","，然后自己手动抛出错误",[53,914,916],{"className":55,"code":915,"language":57,"meta":58,"style":58},"zValidator(source, schema, (result, c: Context) => {\n    if (!result.success) {\n      const errMsg = result.error.errors.map((e: any) => `field:${e.path[0]} - ${e.message}`).join(', ')\n      throw new HTTPException(400, { message: errMsg })\n    }\n  })\n",[18,917,918,943,956,1032,1051,1055],{"__ignoreMap":58},[62,919,920,923,926,929,931,933,935,937,939,941],{"class":64,"line":65},[62,921,922],{"class":85},"zValidator",[62,924,925],{"class":92},"(source, schema, (",[62,927,928],{"class":96},"result",[62,930,455],{"class":92},[62,932,97],{"class":96},[62,934,100],{"class":81},[62,936,103],{"class":85},[62,938,106],{"class":92},[62,940,109],{"class":81},[62,942,112],{"class":92},[62,944,945,948,950,953],{"class":64,"line":72},[62,946,947],{"class":81},"    if",[62,949,93],{"class":92},[62,951,952],{"class":81},"!",[62,954,955],{"class":92},"result.success) {\n",[62,957,958,961,964,966,969,972,975,978,980,983,985,987,990,992,995,998,1001,1004,1007,1010,1012,1014,1017,1019,1022,1025,1027,1030],{"class":64,"line":78},[62,959,960],{"class":81},"      const",[62,962,963],{"class":207}," errMsg",[62,965,89],{"class":81},[62,967,968],{"class":92}," result.error.errors.",[62,970,971],{"class":85},"map",[62,973,974],{"class":92},"((",[62,976,977],{"class":96},"e",[62,979,100],{"class":81},[62,981,982],{"class":207}," any",[62,984,106],{"class":92},[62,986,109],{"class":81},[62,988,989],{"class":130}," `field:${",[62,991,977],{"class":92},[62,993,994],{"class":130},".",[62,996,997],{"class":92},"path",[62,999,1000],{"class":130},"[",[62,1002,1003],{"class":207},"0",[62,1005,1006],{"class":130},"]",[62,1008,1009],{"class":130},"} - ${",[62,1011,977],{"class":92},[62,1013,994],{"class":130},[62,1015,1016],{"class":92},"message",[62,1018,245],{"class":130},[62,1020,1021],{"class":92},").",[62,1023,1024],{"class":85},"join",[62,1026,127],{"class":92},[62,1028,1029],{"class":130},"', '",[62,1031,134],{"class":92},[62,1033,1034,1037,1040,1043,1045,1048],{"class":64,"line":115},[62,1035,1036],{"class":81},"      throw",[62,1038,1039],{"class":81}," new",[62,1041,1042],{"class":85}," HTTPException",[62,1044,127],{"class":92},[62,1046,1047],{"class":207},"400",[62,1049,1050],{"class":92},", { message: errMsg })\n",[62,1052,1053],{"class":64,"line":137},[62,1054,893],{"class":92},[62,1056,1057],{"class":64,"line":143},[62,1058,1059],{"class":92},"  })\n",[14,1061,1062,1063,1066],{},"抛出错误后我们在 ",[18,1064,1065],{},"errorHandler"," 中就可以接收到错误信息了，再经过处理一下，返回固定的格式（此处代码只是演示）",[53,1068,1070],{"className":55,"code":1069,"language":57,"meta":58,"style":58},"export const errorHandler = async (err: Error, c: Context) => {\n  // 错误处理\n  const errorMsg = \"出错了\"\n  if (err instanceof HTTPException) {\n     return err.getResponse()\n  }\n  const response = {\n    code: 50001,\n    data: null,\n    message: errorMsg,\n  };\n  return c.json(response, status)\n}\n",[18,1071,1072,1112,1117,1129,1145,1159,1163,1174,1184,1194,1199,1204,1215],{"__ignoreMap":58},[62,1073,1074,1077,1080,1083,1085,1088,1090,1093,1095,1098,1100,1102,1104,1106,1108,1110],{"class":64,"line":65},[62,1075,1076],{"class":81},"export",[62,1078,1079],{"class":81}," const",[62,1081,1082],{"class":85}," errorHandler",[62,1084,89],{"class":81},[62,1086,1087],{"class":81}," async",[62,1089,93],{"class":92},[62,1091,1092],{"class":96},"err",[62,1094,100],{"class":81},[62,1096,1097],{"class":85}," Error",[62,1099,455],{"class":92},[62,1101,97],{"class":96},[62,1103,100],{"class":81},[62,1105,103],{"class":85},[62,1107,106],{"class":92},[62,1109,109],{"class":81},[62,1111,112],{"class":92},[62,1113,1114],{"class":64,"line":72},[62,1115,1116],{"class":68},"  // 错误处理\n",[62,1118,1119,1121,1124,1126],{"class":64,"line":78},[62,1120,204],{"class":81},[62,1122,1123],{"class":207}," errorMsg",[62,1125,89],{"class":81},[62,1127,1128],{"class":130}," \"出错了\"\n",[62,1130,1131,1134,1137,1140,1142],{"class":64,"line":115},[62,1132,1133],{"class":81},"  if",[62,1135,1136],{"class":92}," (err ",[62,1138,1139],{"class":81},"instanceof",[62,1141,1042],{"class":85},[62,1143,1144],{"class":92},") {\n",[62,1146,1147,1150,1153,1156],{"class":64,"line":137},[62,1148,1149],{"class":81},"     return",[62,1151,1152],{"class":92}," err.",[62,1154,1155],{"class":85},"getResponse",[62,1157,1158],{"class":92},"()\n",[62,1160,1161],{"class":64,"line":143},[62,1162,414],{"class":92},[62,1164,1165,1167,1170,1172],{"class":64,"line":150},[62,1166,204],{"class":81},[62,1168,1169],{"class":207}," response",[62,1171,89],{"class":81},[62,1173,112],{"class":92},[62,1175,1176,1179,1182],{"class":64,"line":344},[62,1177,1178],{"class":92},"    code: ",[62,1180,1181],{"class":207},"50001",[62,1183,316],{"class":92},[62,1185,1186,1189,1192],{"class":64,"line":356},[62,1187,1188],{"class":92},"    data: ",[62,1190,1191],{"class":207},"null",[62,1193,316],{"class":92},[62,1195,1196],{"class":64,"line":362},[62,1197,1198],{"class":92},"    message: errorMsg,\n",[62,1200,1201],{"class":64,"line":368},[62,1202,1203],{"class":92},"  };\n",[62,1205,1206,1208,1210,1212],{"class":64,"line":382},[62,1207,118],{"class":81},[62,1209,121],{"class":92},[62,1211,124],{"class":85},[62,1213,1214],{"class":92},"(response, status)\n",[62,1216,1217],{"class":64,"line":405},[62,1218,140],{"class":92},[14,1220,1221,1222,1224,1225,1228],{},"现在",[18,1223,1065],{}," 已经处理了好几种错误：",[259,1226,1227],{},"jwt、zod、系统错误","等等",[14,1230,1231,1232,1235,1236,1239,1240,1243,1244,1247,1248,1251],{},"我们总不能每次想起一种错误来，就来这个写个 ",[18,1233,1234],{},"if else"," 处理一下，所以我们可以定义一组通用的 ",[18,1237,1238],{},"errorCode","和",[18,1241,1242],{},"errorMsg"," map结构，并且让每个抛出错误的",[259,1245,1246],{},"中间件把相关信息写入到上下文中","，",[259,1249,1250],{},"由于上下文仅在当前的请求链路有效","，所以也不用担心污染。",[14,1253,1254],{},"在上下文中传递信息",[53,1256,1258],{"className":55,"code":1257,"language":57,"meta":58,"style":58},"app.use(async (c, next) => {\n  c.set('message', 'Hono is cool!!')\n  await next()\n})\n\napp.get('/', (c) => {\n  const message = c.get('message')\n  return c.text(`The message is \"${message}\"`)\n})\n",[18,1259,1260,1287,1307,1317,1321,1325,1346,1365,1386],{"__ignoreMap":58},[62,1261,1262,1264,1267,1269,1272,1274,1276,1278,1281,1283,1285],{"class":64,"line":65},[62,1263,153],{"class":92},[62,1265,1266],{"class":85},"use",[62,1268,127],{"class":92},[62,1270,1271],{"class":81},"async",[62,1273,93],{"class":92},[62,1275,97],{"class":96},[62,1277,455],{"class":92},[62,1279,1280],{"class":96},"next",[62,1282,106],{"class":92},[62,1284,109],{"class":81},[62,1286,112],{"class":92},[62,1288,1289,1292,1295,1297,1300,1302,1305],{"class":64,"line":72},[62,1290,1291],{"class":92},"  c.",[62,1293,1294],{"class":85},"set",[62,1296,127],{"class":92},[62,1298,1299],{"class":130},"'message'",[62,1301,455],{"class":92},[62,1303,1304],{"class":130},"'Hono is cool!!'",[62,1306,134],{"class":92},[62,1308,1309,1312,1315],{"class":64,"line":78},[62,1310,1311],{"class":81},"  await",[62,1313,1314],{"class":85}," next",[62,1316,1158],{"class":92},[62,1318,1319],{"class":64,"line":115},[62,1320,252],{"class":92},[62,1322,1323],{"class":64,"line":137},[62,1324,147],{"emptyLinePlaceholder":146},[62,1326,1327,1329,1331,1333,1336,1338,1340,1342,1344],{"class":64,"line":143},[62,1328,153],{"class":92},[62,1330,156],{"class":85},[62,1332,127],{"class":92},[62,1334,1335],{"class":130},"'/'",[62,1337,191],{"class":92},[62,1339,97],{"class":96},[62,1341,106],{"class":92},[62,1343,109],{"class":81},[62,1345,112],{"class":92},[62,1347,1348,1350,1353,1355,1357,1359,1361,1363],{"class":64,"line":150},[62,1349,204],{"class":81},[62,1351,1352],{"class":207}," message",[62,1354,89],{"class":81},[62,1356,121],{"class":92},[62,1358,156],{"class":85},[62,1360,127],{"class":92},[62,1362,1299],{"class":130},[62,1364,134],{"class":92},[62,1366,1367,1369,1371,1374,1376,1379,1381,1384],{"class":64,"line":344},[62,1368,118],{"class":81},[62,1370,121],{"class":92},[62,1372,1373],{"class":85},"text",[62,1375,127],{"class":92},[62,1377,1378],{"class":130},"`The message is \"${",[62,1380,1016],{"class":92},[62,1382,1383],{"class":130},"}\"`",[62,1385,134],{"class":92},[62,1387,1388],{"class":64,"line":356},[62,1389,252],{"class":92},[14,1391,1392],{},"在errorHandler中就可以这样接受错误信息",[53,1394,1396],{"className":55,"code":1395,"language":57,"meta":58,"style":58},"export const errorHandler = async (err: Error, c: Context) => {\n  // 错误处理\n  // 任何请求， http status 返回200， 错误码在返回体自定义\n  const status = 200;\n  // TODO 记录原始的错误， 返回给前端的是友好的信息\n  // 从上下文拿错误码, 优先取自定义的msg => 错误码对应信息  => 未知错误\n  let errorCode = c.get('errCode')\n  if (!errorCode) {\n    // 抛出了HTTPException， 视为权限不错\n    if (err instanceof HTTPException) {\n      errorCode = ErrorCode.UNAUTHORIZED\n    }\n  }\n  let errorMsg = c.get('errMsg') || ErrorCodeMsg[errorCode] || ErrorCodeMsg[ErrorCode.UNKOWN_ERROR]\n\n  const response = {\n    code: errorCode || ErrorCode.UNKOWN_ERROR,\n    data: null,\n    message: errorMsg,\n  };\n  return c.json(response, status)\n}\n",[18,1397,1398,1432,1436,1441,1456,1461,1466,1488,1499,1504,1516,1529,1533,1537,1574,1578,1588,1601,1609,1613,1617,1627],{"__ignoreMap":58},[62,1399,1400,1402,1404,1406,1408,1410,1412,1414,1416,1418,1420,1422,1424,1426,1428,1430],{"class":64,"line":65},[62,1401,1076],{"class":81},[62,1403,1079],{"class":81},[62,1405,1082],{"class":85},[62,1407,89],{"class":81},[62,1409,1087],{"class":81},[62,1411,93],{"class":92},[62,1413,1092],{"class":96},[62,1415,100],{"class":81},[62,1417,1097],{"class":85},[62,1419,455],{"class":92},[62,1421,97],{"class":96},[62,1423,100],{"class":81},[62,1425,103],{"class":85},[62,1427,106],{"class":92},[62,1429,109],{"class":81},[62,1431,112],{"class":92},[62,1433,1434],{"class":64,"line":72},[62,1435,1116],{"class":68},[62,1437,1438],{"class":64,"line":78},[62,1439,1440],{"class":68},"  // 任何请求， http status 返回200， 错误码在返回体自定义\n",[62,1442,1443,1445,1448,1450,1453],{"class":64,"line":115},[62,1444,204],{"class":81},[62,1446,1447],{"class":207}," status",[62,1449,89],{"class":81},[62,1451,1452],{"class":207}," 200",[62,1454,1455],{"class":92},";\n",[62,1457,1458],{"class":64,"line":137},[62,1459,1460],{"class":68},"  // TODO 记录原始的错误， 返回给前端的是友好的信息\n",[62,1462,1463],{"class":64,"line":143},[62,1464,1465],{"class":68},"  // 从上下文拿错误码, 优先取自定义的msg => 错误码对应信息  => 未知错误\n",[62,1467,1468,1471,1474,1477,1479,1481,1483,1486],{"class":64,"line":150},[62,1469,1470],{"class":81},"  let",[62,1472,1473],{"class":92}," errorCode ",[62,1475,1476],{"class":81},"=",[62,1478,121],{"class":92},[62,1480,156],{"class":85},[62,1482,127],{"class":92},[62,1484,1485],{"class":130},"'errCode'",[62,1487,134],{"class":92},[62,1489,1490,1492,1494,1496],{"class":64,"line":344},[62,1491,1133],{"class":81},[62,1493,93],{"class":92},[62,1495,952],{"class":81},[62,1497,1498],{"class":92},"errorCode) {\n",[62,1500,1501],{"class":64,"line":356},[62,1502,1503],{"class":68},"    // 抛出了HTTPException， 视为权限不错\n",[62,1505,1506,1508,1510,1512,1514],{"class":64,"line":362},[62,1507,947],{"class":81},[62,1509,1136],{"class":92},[62,1511,1139],{"class":81},[62,1513,1042],{"class":85},[62,1515,1144],{"class":92},[62,1517,1518,1521,1523,1526],{"class":64,"line":368},[62,1519,1520],{"class":92},"      errorCode ",[62,1522,1476],{"class":81},[62,1524,1525],{"class":92}," ErrorCode.",[62,1527,1528],{"class":207},"UNAUTHORIZED\n",[62,1530,1531],{"class":64,"line":382},[62,1532,893],{"class":92},[62,1534,1535],{"class":64,"line":405},[62,1536,414],{"class":92},[62,1538,1539,1541,1544,1546,1548,1550,1552,1555,1557,1560,1563,1565,1568,1571],{"class":64,"line":411},[62,1540,1470],{"class":81},[62,1542,1543],{"class":92}," errorMsg ",[62,1545,1476],{"class":81},[62,1547,121],{"class":92},[62,1549,156],{"class":85},[62,1551,127],{"class":92},[62,1553,1554],{"class":130},"'errMsg'",[62,1556,106],{"class":92},[62,1558,1559],{"class":81},"||",[62,1561,1562],{"class":92}," ErrorCodeMsg[errorCode] ",[62,1564,1559],{"class":81},[62,1566,1567],{"class":92}," ErrorCodeMsg[ErrorCode.",[62,1569,1570],{"class":207},"UNKOWN_ERROR",[62,1572,1573],{"class":92},"]\n",[62,1575,1576],{"class":64,"line":417},[62,1577,147],{"emptyLinePlaceholder":146},[62,1579,1580,1582,1584,1586],{"class":64,"line":645},[62,1581,204],{"class":81},[62,1583,1169],{"class":207},[62,1585,89],{"class":81},[62,1587,112],{"class":92},[62,1589,1590,1593,1595,1597,1599],{"class":64,"line":656},[62,1591,1592],{"class":92},"    code: errorCode ",[62,1594,1559],{"class":81},[62,1596,1525],{"class":92},[62,1598,1570],{"class":207},[62,1600,316],{"class":92},[62,1602,1603,1605,1607],{"class":64,"line":667},[62,1604,1188],{"class":92},[62,1606,1191],{"class":207},[62,1608,316],{"class":92},[62,1610,1611],{"class":64,"line":674},[62,1612,1198],{"class":92},[62,1614,1615],{"class":64,"line":680},[62,1616,1203],{"class":92},[62,1618,1619,1621,1623,1625],{"class":64,"line":685},[62,1620,118],{"class":81},[62,1622,121],{"class":92},[62,1624,124],{"class":85},[62,1626,1214],{"class":92},[62,1628,1629],{"class":64,"line":694},[62,1630,140],{"class":92},[14,1632,1633,1634,1637,1638,1640,1641,1644],{},"这样每个抛出错误的中间价，可以写入详细的错误信息，而一组自定义的 ",[18,1635,1636],{},"errorcode"," 也可以应付更多的业务场景，如果增加了一个场景，我们**只需要去map结构中再加一组key-value，**如果没有自定义错误信息，则使用 ",[18,1639,18],{}," 对应的默认 ",[18,1642,1643],{},"msg"," 进行返回。",[14,1646,1647,1648,1651],{},"抛出错误时，自定义错误信息。 这一块可以进行一个封装，因为每个接口都要写这么一大串，明显不合理，所以",[259,1649,1650],{},"提取到公共的文件夹下面去","。",[53,1653,1655],{"className":55,"code":1654,"language":57,"meta":58,"style":58},"// 封装自定义的zvalidator\nexport const zvalidator = (source: any, schema: any) => {\n  return zValidator(source, schema, (result, c: Context) => {\n    if (!result.success) {\n      const errMsg = result.error.errors.map((e: any) => `field:${e.path[0]} - ${e.message}`).join(', ')\n      c.set('errMsg', errMsg)\n      c.set('errCode', ErrorCode.VALIDATION_ERROR)\n      throw new HTTPException(400, { message: errMsg })\n    }\n  })\n};\n",[18,1656,1657,1662,1697,1722,1732,1790,1804,1822,1836,1840,1844],{"__ignoreMap":58},[62,1658,1659],{"class":64,"line":65},[62,1660,1661],{"class":68},"// 封装自定义的zvalidator\n",[62,1663,1664,1666,1668,1671,1673,1675,1678,1680,1682,1684,1687,1689,1691,1693,1695],{"class":64,"line":72},[62,1665,1076],{"class":81},[62,1667,1079],{"class":81},[62,1669,1670],{"class":85}," zvalidator",[62,1672,89],{"class":81},[62,1674,93],{"class":92},[62,1676,1677],{"class":96},"source",[62,1679,100],{"class":81},[62,1681,982],{"class":207},[62,1683,455],{"class":92},[62,1685,1686],{"class":96},"schema",[62,1688,100],{"class":81},[62,1690,982],{"class":207},[62,1692,106],{"class":92},[62,1694,109],{"class":81},[62,1696,112],{"class":92},[62,1698,1699,1701,1704,1706,1708,1710,1712,1714,1716,1718,1720],{"class":64,"line":78},[62,1700,118],{"class":81},[62,1702,1703],{"class":85}," zValidator",[62,1705,925],{"class":92},[62,1707,928],{"class":96},[62,1709,455],{"class":92},[62,1711,97],{"class":96},[62,1713,100],{"class":81},[62,1715,103],{"class":85},[62,1717,106],{"class":92},[62,1719,109],{"class":81},[62,1721,112],{"class":92},[62,1723,1724,1726,1728,1730],{"class":64,"line":115},[62,1725,947],{"class":81},[62,1727,93],{"class":92},[62,1729,952],{"class":81},[62,1731,955],{"class":92},[62,1733,1734,1736,1738,1740,1742,1744,1746,1748,1750,1752,1754,1756,1758,1760,1762,1764,1766,1768,1770,1772,1774,1776,1778,1780,1782,1784,1786,1788],{"class":64,"line":137},[62,1735,960],{"class":81},[62,1737,963],{"class":207},[62,1739,89],{"class":81},[62,1741,968],{"class":92},[62,1743,971],{"class":85},[62,1745,974],{"class":92},[62,1747,977],{"class":96},[62,1749,100],{"class":81},[62,1751,982],{"class":207},[62,1753,106],{"class":92},[62,1755,109],{"class":81},[62,1757,989],{"class":130},[62,1759,977],{"class":92},[62,1761,994],{"class":130},[62,1763,997],{"class":92},[62,1765,1000],{"class":130},[62,1767,1003],{"class":207},[62,1769,1006],{"class":130},[62,1771,1009],{"class":130},[62,1773,977],{"class":92},[62,1775,994],{"class":130},[62,1777,1016],{"class":92},[62,1779,245],{"class":130},[62,1781,1021],{"class":92},[62,1783,1024],{"class":85},[62,1785,127],{"class":92},[62,1787,1029],{"class":130},[62,1789,134],{"class":92},[62,1791,1792,1795,1797,1799,1801],{"class":64,"line":143},[62,1793,1794],{"class":92},"      c.",[62,1796,1294],{"class":85},[62,1798,127],{"class":92},[62,1800,1554],{"class":130},[62,1802,1803],{"class":92},", errMsg)\n",[62,1805,1806,1808,1810,1812,1814,1817,1820],{"class":64,"line":150},[62,1807,1794],{"class":92},[62,1809,1294],{"class":85},[62,1811,127],{"class":92},[62,1813,1485],{"class":130},[62,1815,1816],{"class":92},", ErrorCode.",[62,1818,1819],{"class":207},"VALIDATION_ERROR",[62,1821,134],{"class":92},[62,1823,1824,1826,1828,1830,1832,1834],{"class":64,"line":344},[62,1825,1036],{"class":81},[62,1827,1039],{"class":81},[62,1829,1042],{"class":85},[62,1831,127],{"class":92},[62,1833,1047],{"class":207},[62,1835,1050],{"class":92},[62,1837,1838],{"class":64,"line":356},[62,1839,893],{"class":92},[62,1841,1842],{"class":64,"line":362},[62,1843,1059],{"class":92},[62,1845,1846],{"class":64,"line":368},[62,1847,1848],{"class":92},"};\n",[26,1850,1851],{"id":1851},"响应标准化",[14,1853,1854],{},"完成了参数的校验，并顺着问题一步步封装了关于错误信息的处理。接下来就开始让接口能正常的返回数据了。",[14,1856,1857,1858],{},"然而虽然错误信息我们已经标准化，但正常的返回",[259,1859,1860],{},"不方便用中间件直接去拦截。",[14,1862,1863,1864,1867,1868,1871,1872,1874,1875,1878,1879,1882,1883],{},"原因是 ",[18,1865,1866],{},"c.json"," 直接就是一个 ",[18,1869,1870],{},"Response","，虽然会走到我们的全局中间件里去，但没法再二次加工 ",[18,1873,1870],{}," 了，官方给出了一个例子，可以把 ",[18,1876,1877],{},"res"," 设置为",[18,1880,1881],{},"undefined","，然后重新 ",[18,1884,1885],{},"new Response",[14,1887,1888,1889,1892,1893,1896,1897,1900],{},"我觉得破坏性太大了，也不优雅，所以",[259,1890,1891],{},"暂时没找到","类似 ",[18,1894,1895],{},"nest"," 那样，在 ",[18,1898,1899],{},"response"," 之后拦截的钩子。",[14,1902,1903],{},"但这都是小问题，以后有看到更好的处理方式再进行优化就行，这里我们就简单的封装一下对象，塞到 c.json 中返回就好了",[53,1905,1907],{"className":55,"code":1906,"language":57,"meta":58,"style":58},"export const standardRes = (data: any) => {\n  return {\n    code: 200,\n    data,\n    message: 'success'\n  }\n}\n// case\nuser.post(\"/list\", zvalidator('json', pageSchema), (c) => {\n  const params = c.req.valid('json')\n  // do something\n  const list = userModal.getList(params)\n  return c.json(standardRes(list))\n})\n",[18,1908,1909,1935,1941,1950,1955,1963,1967,1971,1976,2008,2027,2032,2050,2066],{"__ignoreMap":58},[62,1910,1911,1913,1915,1918,1920,1922,1925,1927,1929,1931,1933],{"class":64,"line":65},[62,1912,1076],{"class":81},[62,1914,1079],{"class":81},[62,1916,1917],{"class":85}," standardRes",[62,1919,89],{"class":81},[62,1921,93],{"class":92},[62,1923,1924],{"class":96},"data",[62,1926,100],{"class":81},[62,1928,982],{"class":207},[62,1930,106],{"class":92},[62,1932,109],{"class":81},[62,1934,112],{"class":92},[62,1936,1937,1939],{"class":64,"line":72},[62,1938,118],{"class":81},[62,1940,112],{"class":92},[62,1942,1943,1945,1948],{"class":64,"line":78},[62,1944,1178],{"class":92},[62,1946,1947],{"class":207},"200",[62,1949,316],{"class":92},[62,1951,1952],{"class":64,"line":115},[62,1953,1954],{"class":92},"    data,\n",[62,1956,1957,1960],{"class":64,"line":137},[62,1958,1959],{"class":92},"    message: ",[62,1961,1962],{"class":130},"'success'\n",[62,1964,1965],{"class":64,"line":143},[62,1966,414],{"class":92},[62,1968,1969],{"class":64,"line":150},[62,1970,140],{"class":92},[62,1972,1973],{"class":64,"line":344},[62,1974,1975],{"class":68},"// case\n",[62,1977,1978,1981,1983,1985,1988,1990,1993,1995,1997,2000,2002,2004,2006],{"class":64,"line":356},[62,1979,1980],{"class":92},"user.",[62,1982,305],{"class":85},[62,1984,127],{"class":92},[62,1986,1987],{"class":130},"\"/list\"",[62,1989,455],{"class":92},[62,1991,1992],{"class":85},"zvalidator",[62,1994,127],{"class":92},[62,1996,485],{"class":130},[62,1998,1999],{"class":92},", pageSchema), (",[62,2001,97],{"class":96},[62,2003,106],{"class":92},[62,2005,109],{"class":81},[62,2007,112],{"class":92},[62,2009,2010,2012,2015,2017,2019,2021,2023,2025],{"class":64,"line":362},[62,2011,204],{"class":81},[62,2013,2014],{"class":207}," params",[62,2016,89],{"class":81},[62,2018,213],{"class":92},[62,2020,395],{"class":85},[62,2022,127],{"class":92},[62,2024,485],{"class":130},[62,2026,134],{"class":92},[62,2028,2029],{"class":64,"line":368},[62,2030,2031],{"class":68},"  // do something\n",[62,2033,2034,2036,2039,2041,2044,2047],{"class":64,"line":382},[62,2035,204],{"class":81},[62,2037,2038],{"class":207}," list",[62,2040,89],{"class":81},[62,2042,2043],{"class":92}," userModal.",[62,2045,2046],{"class":85},"getList",[62,2048,2049],{"class":92},"(params)\n",[62,2051,2052,2054,2056,2058,2060,2063],{"class":64,"line":405},[62,2053,118],{"class":81},[62,2055,121],{"class":92},[62,2057,124],{"class":85},[62,2059,127],{"class":92},[62,2061,2062],{"class":85},"standardRes",[62,2064,2065],{"class":92},"(list))\n",[62,2067,2068],{"class":64,"line":411},[62,2069,252],{"class":92},[14,2071,2072],{},"有细节没优化不是什么大问题，重要的是把流程先打通",[26,2074,2076],{"id":2075},"jwt-token","JWT TOKEN",[14,2078,2079],{},"现在路由也分组了，错误也捕捉了，正常响应也处理了，参数也进行了校验。那就到了接口权限这一步上。",[14,2081,2082],{},"虽然你的网站可能只需要给用户展示信息，但有时也需要一个平台去写入数据，修改数据或删除数据才行。",[14,2084,2085],{},"这种敏感操作不可能让普通用户去做，一般都是管理员，甚至只有自己去操作，所以才需要一个登录操作，以便确认用户身份。",[14,2087,2088],{},"而登录操作，是为了拿到一个令牌，好让这个用户在后续的操作中畅通无阻。这里我们是用 jwt 来给用户发放令牌，jwt 的使用 hono 官方也有说明。",[14,2090,2091],{},[259,2092,2093],{},"具体流程就是：用户登录 - 拿到令牌 - 后续操作携带令牌 - 校验令牌是否有效 - 有效就允许用户继续操作 - 无效则返回相关错误信息 - 前端提示用户或引导进入登录页",[14,2095,2096,2097,2100,2101,2104,2105,2108,2109,2111,2112,2115,2116,2119,2120,2123,2124,2126,2127,2130],{},"而我们的用户有很多个，所以一般 jwt 的 ",[259,2098,2099],{},"payload 会和用户信息挂钩","，每个登录的用户通过 ",[18,2102,2103],{},"sign"," 拿到一个 ",[18,2106,2107],{},"token","，并在后续操作中把 ",[18,2110,2107],{}," 放在 ",[18,2113,2114],{},"header"," 中，接口则是在中间件中通过 ",[18,2117,2118],{},"c.get('jwtPayload')","拿到令牌中包含的用户信息，去进行相关的校验。比如数据库中有无此用户，此用户的权限等级够不够等情况，如果校验不通过就 ",[18,2121,2122],{},"throw new HTTPException(code)","， 并把 ",[18,2125,1242],{}," 写入上下文 让 ",[18,2128,2129],{},"errorHanlder"," 去处理。校验通过则继续后续的业务逻辑。",[14,2132,2133],{},"这里我演示一个登录接口，来生成token",[53,2135,2137],{"className":55,"code":2136,"language":57,"meta":58,"style":58},"user.post(\"/login\", async (c) => {\n  const user = { id: 1, name: 'zzao.club' };  // 假设这是经过验证的用户信息\n  const payload = {\n    id: user.id,\n    exp: Math.floor(Date.now() / 1000) + 60 * 60, // 检查令牌不会过期 in 60 minutes\n  }\n  const token = await sign(payload, JWT_SECRET);\n  console.log(`token`, token)\n  return c.json(standardRes(token));\n})\n",[18,2138,2139,2164,2191,2202,2207,2248,2252,2276,2292,2307],{"__ignoreMap":58},[62,2140,2141,2143,2145,2147,2150,2152,2154,2156,2158,2160,2162],{"class":64,"line":65},[62,2142,1980],{"class":92},[62,2144,305],{"class":85},[62,2146,127],{"class":92},[62,2148,2149],{"class":130},"\"/login\"",[62,2151,455],{"class":92},[62,2153,1271],{"class":81},[62,2155,93],{"class":92},[62,2157,97],{"class":96},[62,2159,106],{"class":92},[62,2161,109],{"class":81},[62,2163,112],{"class":92},[62,2165,2166,2168,2171,2173,2176,2179,2182,2185,2188],{"class":64,"line":72},[62,2167,204],{"class":81},[62,2169,2170],{"class":207}," user",[62,2172,89],{"class":81},[62,2174,2175],{"class":92}," { id: ",[62,2177,2178],{"class":207},"1",[62,2180,2181],{"class":92},", name: ",[62,2183,2184],{"class":130},"'zzao.club'",[62,2186,2187],{"class":92}," };  ",[62,2189,2190],{"class":68},"// 假设这是经过验证的用户信息\n",[62,2192,2193,2195,2198,2200],{"class":64,"line":78},[62,2194,204],{"class":81},[62,2196,2197],{"class":207}," payload",[62,2199,89],{"class":81},[62,2201,112],{"class":92},[62,2203,2204],{"class":64,"line":115},[62,2205,2206],{"class":92},"    id: user.id,\n",[62,2208,2209,2212,2215,2218,2221,2224,2227,2230,2232,2235,2238,2241,2243,2245],{"class":64,"line":137},[62,2210,2211],{"class":92},"    exp: Math.",[62,2213,2214],{"class":85},"floor",[62,2216,2217],{"class":92},"(Date.",[62,2219,2220],{"class":85},"now",[62,2222,2223],{"class":92},"() ",[62,2225,2226],{"class":81},"/",[62,2228,2229],{"class":207}," 1000",[62,2231,106],{"class":92},[62,2233,2234],{"class":81},"+",[62,2236,2237],{"class":207}," 60",[62,2239,2240],{"class":81}," *",[62,2242,2237],{"class":207},[62,2244,455],{"class":92},[62,2246,2247],{"class":68},"// 检查令牌不会过期 in 60 minutes\n",[62,2249,2250],{"class":64,"line":143},[62,2251,414],{"class":92},[62,2253,2254,2256,2259,2261,2264,2267,2270,2273],{"class":64,"line":150},[62,2255,204],{"class":81},[62,2257,2258],{"class":207}," token",[62,2260,89],{"class":81},[62,2262,2263],{"class":81}," await",[62,2265,2266],{"class":85}," sign",[62,2268,2269],{"class":92},"(payload, ",[62,2271,2272],{"class":207},"JWT_SECRET",[62,2274,2275],{"class":92},");\n",[62,2277,2278,2281,2284,2286,2289],{"class":64,"line":344},[62,2279,2280],{"class":92},"  console.",[62,2282,2283],{"class":85},"log",[62,2285,127],{"class":92},[62,2287,2288],{"class":130},"`token`",[62,2290,2291],{"class":92},", token)\n",[62,2293,2294,2296,2298,2300,2302,2304],{"class":64,"line":356},[62,2295,118],{"class":81},[62,2297,121],{"class":92},[62,2299,124],{"class":85},[62,2301,127],{"class":92},[62,2303,2062],{"class":85},[62,2305,2306],{"class":92},"(token));\n",[62,2308,2309],{"class":64,"line":362},[62,2310,252],{"class":92},[14,2312,2313,2314,2317],{},"其中如果 ",[18,2315,2316],{},"exp","在payload中，则jwt会检查token是否过期了，payload还可以传入其他参数：",[2319,2320,2321,2328],"ul",{},[2322,2323,2324,2327],"li",{},[18,2325,2326],{},"nbf"," : 检查token在指定时间之前没有被使用",[2322,2329,2330,2333],{},[18,2331,2332],{},"iat"," : 检查token没有使用未来的时间进行签发。意思是，设置一个未来时间使自己的token一直有效（I guess） （The token is checked to ensure it is not issued in the future.）",[14,2335,2336,2337,2339],{},"这里我只使用了",[18,2338,2316],{},"设置token 60min后过期就可以了。",[14,2341,2342],{},"当然，还有一种需求。",[14,2344,2345],{},"作为一个用户，我每天都在你的网站上使用，不想隔几天登录就失效，还要重新登录。",[14,2347,2348,2349,2352,2353,2355,2356,2359,2360,2363,2364,2366,2367,2369,2370,2373,2375],{},"所以我们还可以再设置 ",[18,2350,2351],{},"refresh  token","，这个 ",[18,2354,2107],{}," 专门用来更新 ",[18,2357,2358],{},"access token"," （也就是上边例子里的token）的有效期。比如 ",[18,2361,2362],{},"refresh token"," 3 天过期，",[18,2365,2358],{}," 7 天过期，在 ",[18,2368,2362],{}," 过期时，前端就调用一个",[259,2371,2372],{},"刷新 token 的接口去生成一个新的",[18,2374,2358],{}," ，前端拿到新token后再在之后的请求中带上新的token即可。",[14,2377,2378],{},[259,2379,2380],{},"这样用户登录过一次后，只要平时在一直使用，就可以一直保持登录状态。",[14,2382,2383],{},"下面是一个为user模块使用jwt中间件的中间件case",[53,2385,2387],{"className":55,"code":2386,"language":57,"meta":58,"style":58},"import { jwt } from 'hono/jwt'\n\nconst jwtMiddware = jwt({\n  secret: 'your secret!!!!',\n})\n\nuser.use('/*', async (c, next) => {\n  // 检查当前请求路径是否在排除列表中\n  if (NoAuthPaths.includes(c.req.path)) {\n    await next();\n    return;\n  }\n  // 如果不在排除列表中，则进行JWT验证\n  await jwtMiddware(c, async () => {\n    const user = c.get('jwtPayload')\n    // 获取payload中的user信息\n    // 这些信息由登录接口提供\n    if (!user) {\n      c.set('errMsg', '用户未登录')\n      c.set('errCode', ErrorCode.UNAUTHORIZED)\n      throw new HTTPException(401)\n    }\n\n    if (user.id !== 1) {\n      c.set('errMsg', '用户无权限')\n      c.set('errCode', ErrorCode.PERMISSION_DENIED)\n      throw new HTTPException(401)\n    }\n\n    await next()\n  })\n\n})\n",[18,2388,2389,2401,2405,2419,2429,2433,2437,2466,2471,2484,2494,2501,2505,2510,2528,2547,2552,2557,2568,2585,2602,2617,2621,2625,2640,2657,2674,2688,2692,2696,2704,2708,2712],{"__ignoreMap":58},[62,2390,2391,2393,2396,2398],{"class":64,"line":65},[62,2392,277],{"class":81},[62,2394,2395],{"class":92}," { jwt } ",[62,2397,283],{"class":81},[62,2399,2400],{"class":130}," 'hono/jwt'\n",[62,2402,2403],{"class":64,"line":72},[62,2404,147],{"emptyLinePlaceholder":146},[62,2406,2407,2409,2412,2414,2417],{"class":64,"line":78},[62,2408,82],{"class":81},[62,2410,2411],{"class":207}," jwtMiddware",[62,2413,89],{"class":81},[62,2415,2416],{"class":85}," jwt",[62,2418,341],{"class":92},[62,2420,2421,2424,2427],{"class":64,"line":115},[62,2422,2423],{"class":92},"  secret: ",[62,2425,2426],{"class":130},"'your secret!!!!'",[62,2428,316],{"class":92},[62,2430,2431],{"class":64,"line":137},[62,2432,252],{"class":92},[62,2434,2435],{"class":64,"line":143},[62,2436,147],{"emptyLinePlaceholder":146},[62,2438,2439,2441,2443,2445,2448,2450,2452,2454,2456,2458,2460,2462,2464],{"class":64,"line":150},[62,2440,1980],{"class":92},[62,2442,1266],{"class":85},[62,2444,127],{"class":92},[62,2446,2447],{"class":130},"'/*'",[62,2449,455],{"class":92},[62,2451,1271],{"class":81},[62,2453,93],{"class":92},[62,2455,97],{"class":96},[62,2457,455],{"class":92},[62,2459,1280],{"class":96},[62,2461,106],{"class":92},[62,2463,109],{"class":81},[62,2465,112],{"class":92},[62,2467,2468],{"class":64,"line":344},[62,2469,2470],{"class":68},"  // 检查当前请求路径是否在排除列表中\n",[62,2472,2473,2475,2478,2481],{"class":64,"line":356},[62,2474,1133],{"class":81},[62,2476,2477],{"class":92}," (NoAuthPaths.",[62,2479,2480],{"class":85},"includes",[62,2482,2483],{"class":92},"(c.req.path)) {\n",[62,2485,2486,2489,2491],{"class":64,"line":362},[62,2487,2488],{"class":81},"    await",[62,2490,1314],{"class":85},[62,2492,2493],{"class":92},"();\n",[62,2495,2496,2499],{"class":64,"line":368},[62,2497,2498],{"class":81},"    return",[62,2500,1455],{"class":92},[62,2502,2503],{"class":64,"line":382},[62,2504,414],{"class":92},[62,2506,2507],{"class":64,"line":405},[62,2508,2509],{"class":68},"  // 如果不在排除列表中，则进行JWT验证\n",[62,2511,2512,2514,2516,2519,2521,2524,2526],{"class":64,"line":411},[62,2513,1311],{"class":81},[62,2515,2411],{"class":85},[62,2517,2518],{"class":92},"(c, ",[62,2520,1271],{"class":81},[62,2522,2523],{"class":92}," () ",[62,2525,109],{"class":81},[62,2527,112],{"class":92},[62,2529,2530,2532,2534,2536,2538,2540,2542,2545],{"class":64,"line":417},[62,2531,385],{"class":81},[62,2533,2170],{"class":207},[62,2535,89],{"class":81},[62,2537,121],{"class":92},[62,2539,156],{"class":85},[62,2541,127],{"class":92},[62,2543,2544],{"class":130},"'jwtPayload'",[62,2546,134],{"class":92},[62,2548,2549],{"class":64,"line":645},[62,2550,2551],{"class":68},"    // 获取payload中的user信息\n",[62,2553,2554],{"class":64,"line":656},[62,2555,2556],{"class":68},"    // 这些信息由登录接口提供\n",[62,2558,2559,2561,2563,2565],{"class":64,"line":667},[62,2560,947],{"class":81},[62,2562,93],{"class":92},[62,2564,952],{"class":81},[62,2566,2567],{"class":92},"user) {\n",[62,2569,2570,2572,2574,2576,2578,2580,2583],{"class":64,"line":674},[62,2571,1794],{"class":92},[62,2573,1294],{"class":85},[62,2575,127],{"class":92},[62,2577,1554],{"class":130},[62,2579,455],{"class":92},[62,2581,2582],{"class":130},"'用户未登录'",[62,2584,134],{"class":92},[62,2586,2587,2589,2591,2593,2595,2597,2600],{"class":64,"line":680},[62,2588,1794],{"class":92},[62,2590,1294],{"class":85},[62,2592,127],{"class":92},[62,2594,1485],{"class":130},[62,2596,1816],{"class":92},[62,2598,2599],{"class":207},"UNAUTHORIZED",[62,2601,134],{"class":92},[62,2603,2604,2606,2608,2610,2612,2615],{"class":64,"line":685},[62,2605,1036],{"class":81},[62,2607,1039],{"class":81},[62,2609,1042],{"class":85},[62,2611,127],{"class":92},[62,2613,2614],{"class":207},"401",[62,2616,134],{"class":92},[62,2618,2619],{"class":64,"line":694},[62,2620,893],{"class":92},[62,2622,2623],{"class":64,"line":700},[62,2624,147],{"emptyLinePlaceholder":146},[62,2626,2627,2629,2632,2635,2638],{"class":64,"line":706},[62,2628,947],{"class":81},[62,2630,2631],{"class":92}," (user.id ",[62,2633,2634],{"class":81},"!==",[62,2636,2637],{"class":207}," 1",[62,2639,1144],{"class":92},[62,2641,2642,2644,2646,2648,2650,2652,2655],{"class":64,"line":717},[62,2643,1794],{"class":92},[62,2645,1294],{"class":85},[62,2647,127],{"class":92},[62,2649,1554],{"class":130},[62,2651,455],{"class":92},[62,2653,2654],{"class":130},"'用户无权限'",[62,2656,134],{"class":92},[62,2658,2659,2661,2663,2665,2667,2669,2672],{"class":64,"line":723},[62,2660,1794],{"class":92},[62,2662,1294],{"class":85},[62,2664,127],{"class":92},[62,2666,1485],{"class":130},[62,2668,1816],{"class":92},[62,2670,2671],{"class":207},"PERMISSION_DENIED",[62,2673,134],{"class":92},[62,2675,2676,2678,2680,2682,2684,2686],{"class":64,"line":731},[62,2677,1036],{"class":81},[62,2679,1039],{"class":81},[62,2681,1042],{"class":85},[62,2683,127],{"class":92},[62,2685,2614],{"class":207},[62,2687,134],{"class":92},[62,2689,2690],{"class":64,"line":738},[62,2691,893],{"class":92},[62,2693,2694],{"class":64,"line":743},[62,2695,147],{"emptyLinePlaceholder":146},[62,2697,2698,2700,2702],{"class":64,"line":754},[62,2699,2488],{"class":81},[62,2701,1314],{"class":85},[62,2703,1158],{"class":92},[62,2705,2706],{"class":64,"line":765},[62,2707,1059],{"class":92},[62,2709,2710],{"class":64,"line":776},[62,2711,147],{"emptyLinePlaceholder":146},[62,2713,2714],{"class":64,"line":783},[62,2715,252],{"class":92},[14,2717,2718,2719,2722,2723,2726,2727,1651],{},"其中 ",[18,2720,2721],{},"const user = c.get('jwtPayload')"," 这个写法也是官方文档中的写法，也是把",[18,2724,2725],{},"jwtPayload","写入到了上下文中，然后在后续的中间件中就可以拿到这个",[18,2728,2729],{},"payload",[14,2731,2732],{},"每个模块的中间件处理逻辑可能相同也可能不同，后续我们再看情况， 把它抽离到根路由下或者写成一个单独的中间件，需要的模块自己去引入。",[26,2734,2735],{"id":2735},"总结",[14,2737,2738,2739,2741,2742,2745,2746,2749],{},"目前对项目模块进行了分组，如：用户，商品等等。 每个模块可以写自己的",[18,2740,1065],{},"，也可以去设置自定义中间件。而像 ",[18,2743,2744],{},"csrf"," ",[18,2747,2748],{},"cors","等共同的中间件则放在根路由下",[14,2751,2752,2753,2756],{},"针对接口能否被请求，使用了",[18,2754,2755],{},"hono/jwt","，并为了让某些接口跳过校验以及不通过时返回自定义的错误信息，又封装装了一层。",[14,2758,2759,2760,2762,2763,2765,2766,2769],{},"接口可以请求之后，来到参数校验的中间件",[18,2761,44],{},"，由于每个接口schema可能比较多，以及为了让",[18,2764,1065],{},"来处理zod校验不通过的情况，又自定义了一个中间件在内部抛出错误。schema则是被提取到公共的文件夹（如",[18,2767,2768],{},"common","）下",[14,2771,2772,2773,2775],{},"请求成功时，使用一个",[18,2774,2062],{},"函数简单包装一下，统一返回值。",[14,2777,2778,2779,2782,2783,2785],{},"请求失败时，在上下文中使用",[18,2780,2781],{},"c.set/get","注入错误信息，在",[18,2784,1065],{},"中间件中取出错误信息，并返回和成功时一致的json结构。",[14,2787,2788],{},"这样看起来就又完善了一些~~",[14,2790,2791],{},[259,2792,2793],{},"下一章为日志、数据库操作、配置文件相关逻辑",[14,2795,2796],{},"**欢迎点赞催更(¯▽¯)**👍",[2798,2799,2800],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}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 .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}",{"title":58,"searchDepth":72,"depth":72,"links":2802},[2803,2804,2805,2806],{"id":28,"depth":72,"text":28},{"id":1851,"depth":72,"text":1851},{"id":2075,"depth":72,"text":2076},{"id":2735,"depth":72,"text":2735},"2025-02-10T00:00:00.000Z","md","2025-02-12T00:00:00.000Z",{},"/post/hono/hono-params-check-response-standardized","---\ntitle: 【Hono】完善：参数校验+响应标准化\ndate: 2025-02-10\nlastmod: 2025-02-12\ntags: [\"Hono\"]\nversions: [\"hono@4.5.11\"]\nshowTitle: 【Hono】完善：参数校验+响应标准化\n---\n\u003Cbr />\n\n上一章我们完成了基于`Hono`的web项目的搭建工作，并实现了路由分组，错误处理等逻辑。\n\n这一章来继续完善项目，让它变的健壮起来💪。\n\n## 参数校验\n\n平时我们写前端的时候，最希望后端把校验做的越全越好，提示信息越详细越友好越好，那现在就轮到我们自己实现后端了。\n\n`hono` 官方比较推荐的是`zod`作为校验库，并提供了`@hono/zod-validator`，封装了一下中间件，让我们可以直接放在请求路径后面用。\n\n并且官方并不推荐路由第二个参数再写个 handler 封装，然后再传进来\n\n这种写法在其他node框架中十分常见，如Koa、Nest\n\n```typescript\n// 🙁\n// A RoR-like Controller\nconst booksList = (c: Context) => {\n  return c.json('list books')\n}\n\napp.get('/books', booksList)\n```\n\n下面是官方推荐的写法\n\n```typescript\n// 😃\napp.get('/books/:id', (c) => {\n  const id = c.req.param('id') // Can infer the path param\n  return c.json(`get ${id}`)\n})\n```\n\n上述写法的原因和类型有关，如果不写复杂的泛型，就无法在Controller中推断出路径参数。\n\n**这样也好，在业务真正复杂前来之前，保持程序的简洁。**\n\n回到参数校验部分，`@hono/zod-validator`使用比较简单，就是在路由第二个参数插入这个中间件\n\n```typescript\nimport { zValidator } from '@hono/zod-validator'\n\nconst route = app.post(\n  '/posts',\n  zValidator(\n    'form',\n    z.object({\n      body: z.string(),\n    })\n  ),\n  (c) => {\n    const validated = c.req.valid('form')\n    // ... use your validated data\n  }\n)\n```\n\n如果需要多个验证器\n\n```typescript\napp.post(\n  '/posts/:id',\n  validator('param', ...),\n  validator('query', ...),\n  validator('json', ...),\n  (c) => {\n    //...\n  }\n```\n\n加入校验后，来使用 apifox 测试一下，可以看到 zod 返回如下的校验结果\n\n```json\n{\n    \"success\": false,\n    \"error\": {\n        \"issues\": [\n            {\n                \"code\": \"invalid_type\",\n                \"expected\": \"number\",\n                \"received\": \"undefined\",\n                \"path\": [\n                    \"page\"\n                ],\n                \"message\": \"Required\"\n            },\n            {\n                \"code\": \"invalid_type\",\n                \"expected\": \"number\",\n                \"received\": \"undefined\",\n                \"path\": [\n                    \"size\"\n                ],\n                \"message\": \"Required\"\n            }\n        ],\n        \"name\": \"ZodError\"\n    },\n    \"_error\": {\n        \"issues\": [\n            {\n                \"code\": \"invalid_type\",\n                \"expected\": \"number\",\n                \"received\": \"undefined\",\n                \"path\": [\n                    \"page\"\n                ],\n                \"message\": \"Required\"\n            },\n            {\n                \"code\": \"invalid_type\",\n                \"expected\": \"number\",\n                \"received\": \"undefined\",\n                \"path\": [\n                    \"size\"\n                ],\n                \"message\": \"Required\"\n            }\n        ],\n        \"name\": \"ZodError\"\n    }\n}\n```\n\n可以看到返回了详细的校验信息，但美中不足的就是这个中间件**会以自己的结构直接返回**给前端，这显然不合理，我们要的是标准化的返回。\n\n所以这个中间件还可以**传入第三个参数作为回调**，然后自己手动抛出错误\n\n```typescript\nzValidator(source, schema, (result, c: Context) => {\n    if (!result.success) {\n      const errMsg = result.error.errors.map((e: any) => `field:${e.path[0]} - ${e.message}`).join(', ')\n      throw new HTTPException(400, { message: errMsg })\n    }\n  })\n```\n\n抛出错误后我们在 `errorHandler` 中就可以接收到错误信息了，再经过处理一下，返回固定的格式（此处代码只是演示）\n\n```typescript\nexport const errorHandler = async (err: Error, c: Context) => {\n  // 错误处理\n  const errorMsg = \"出错了\"\n  if (err instanceof HTTPException) {\n     return err.getResponse()\n  }\n  const response = {\n    code: 50001,\n    data: null,\n    message: errorMsg,\n  };\n  return c.json(response, status)\n}\n```\n\n现在`errorHandler` 已经处理了好几种错误：**jwt、zod、系统错误**等等\n\n我们总不能每次想起一种错误来，就来这个写个 `if else` 处理一下，所以我们可以定义一组通用的 `errorCode`和`errorMsg` map结构，并且让每个抛出错误的**中间件把相关信息写入到上下文中**，**由于上下文仅在当前的请求链路有效**，所以也不用担心污染。\n\n在上下文中传递信息\n\n```typescript\napp.use(async (c, next) => {\n  c.set('message', 'Hono is cool!!')\n  await next()\n})\n\napp.get('/', (c) => {\n  const message = c.get('message')\n  return c.text(`The message is \"${message}\"`)\n})\n```\n\n在errorHandler中就可以这样接受错误信息\n\n```typescript\nexport const errorHandler = async (err: Error, c: Context) => {\n  // 错误处理\n  // 任何请求， http status 返回200， 错误码在返回体自定义\n  const status = 200;\n  // TODO 记录原始的错误， 返回给前端的是友好的信息\n  // 从上下文拿错误码, 优先取自定义的msg => 错误码对应信息  => 未知错误\n  let errorCode = c.get('errCode')\n  if (!errorCode) {\n    // 抛出了HTTPException， 视为权限不错\n    if (err instanceof HTTPException) {\n      errorCode = ErrorCode.UNAUTHORIZED\n    }\n  }\n  let errorMsg = c.get('errMsg') || ErrorCodeMsg[errorCode] || ErrorCodeMsg[ErrorCode.UNKOWN_ERROR]\n\n  const response = {\n    code: errorCode || ErrorCode.UNKOWN_ERROR,\n    data: null,\n    message: errorMsg,\n  };\n  return c.json(response, status)\n}\n```\n\n这样每个抛出错误的中间价，可以写入详细的错误信息，而一组自定义的 `errorcode` 也可以应付更多的业务场景，如果增加了一个场景，我们\\*\\*只需要去map结构中再加一组key-value，\\*\\*如果没有自定义错误信息，则使用 `code` 对应的默认 `msg` 进行返回。\n\n抛出错误时，自定义错误信息。 这一块可以进行一个封装，因为每个接口都要写这么一大串，明显不合理，所以**提取到公共的文件夹下面去**。\n\n```typescript\n// 封装自定义的zvalidator\nexport const zvalidator = (source: any, schema: any) => {\n  return zValidator(source, schema, (result, c: Context) => {\n    if (!result.success) {\n      const errMsg = result.error.errors.map((e: any) => `field:${e.path[0]} - ${e.message}`).join(', ')\n      c.set('errMsg', errMsg)\n      c.set('errCode', ErrorCode.VALIDATION_ERROR)\n      throw new HTTPException(400, { message: errMsg })\n    }\n  })\n};\n```\n\n## 响应标准化\n\n完成了参数的校验，并顺着问题一步步封装了关于错误信息的处理。接下来就开始让接口能正常的返回数据了。\n\n然而虽然错误信息我们已经标准化，但正常的返回**不方便用中间件直接去拦截。**\n\n原因是 `c.json` 直接就是一个 `Response`，虽然会走到我们的全局中间件里去，但没法再二次加工 `Response` 了，官方给出了一个例子，可以把 `res` 设置为`undefined`，然后重新 `new Response`\n\n我觉得破坏性太大了，也不优雅，所以**暂时没找到**类似 `nest` 那样，在 `response` 之后拦截的钩子。\n\n但这都是小问题，以后有看到更好的处理方式再进行优化就行，这里我们就简单的封装一下对象，塞到 c.json 中返回就好了\n\n```typescript\nexport const standardRes = (data: any) => {\n  return {\n    code: 200,\n    data,\n    message: 'success'\n  }\n}\n// case\nuser.post(\"/list\", zvalidator('json', pageSchema), (c) => {\n  const params = c.req.valid('json')\n  // do something\n  const list = userModal.getList(params)\n  return c.json(standardRes(list))\n})\n```\n\n有细节没优化不是什么大问题，重要的是把流程先打通\n\n## JWT TOKEN\n\n现在路由也分组了，错误也捕捉了，正常响应也处理了，参数也进行了校验。那就到了接口权限这一步上。\n\n虽然你的网站可能只需要给用户展示信息，但有时也需要一个平台去写入数据，修改数据或删除数据才行。\n\n这种敏感操作不可能让普通用户去做，一般都是管理员，甚至只有自己去操作，所以才需要一个登录操作，以便确认用户身份。\n\n而登录操作，是为了拿到一个令牌，好让这个用户在后续的操作中畅通无阻。这里我们是用 jwt 来给用户发放令牌，jwt 的使用 hono 官方也有说明。\n\n**具体流程就是：用户登录 - 拿到令牌 - 后续操作携带令牌 - 校验令牌是否有效 - 有效就允许用户继续操作 - 无效则返回相关错误信息 - 前端提示用户或引导进入登录页**\n\n而我们的用户有很多个，所以一般 jwt 的 **payload 会和用户信息挂钩**，每个登录的用户通过 `sign` 拿到一个 `token`，并在后续操作中把 `token` 放在 `header` 中，接口则是在中间件中通过 `c.get('jwtPayload')`拿到令牌中包含的用户信息，去进行相关的校验。比如数据库中有无此用户，此用户的权限等级够不够等情况，如果校验不通过就 `throw new HTTPException(code)`， 并把 `errorMsg` 写入上下文 让 `errorHanlder` 去处理。校验通过则继续后续的业务逻辑。\n\n这里我演示一个登录接口，来生成token\n\n```typescript\nuser.post(\"/login\", async (c) => {\n  const user = { id: 1, name: 'zzao.club' };  // 假设这是经过验证的用户信息\n  const payload = {\n    id: user.id,\n    exp: Math.floor(Date.now() / 1000) + 60 * 60, // 检查令牌不会过期 in 60 minutes\n  }\n  const token = await sign(payload, JWT_SECRET);\n  console.log(`token`, token)\n  return c.json(standardRes(token));\n})\n```\n\n其中如果 `exp`在payload中，则jwt会检查token是否过期了，payload还可以传入其他参数：\n\n* `nbf` : 检查token在指定时间之前没有被使用\n\n* `iat` : 检查token没有使用未来的时间进行签发。意思是，设置一个未来时间使自己的token一直有效（I guess） （The token is checked to ensure it is not issued in the future.）\n\n这里我只使用了`exp`设置token 60min后过期就可以了。\n\n当然，还有一种需求。\n\n作为一个用户，我每天都在你的网站上使用，不想隔几天登录就失效，还要重新登录。\n\n所以我们还可以再设置 `refresh  token`，这个 `token` 专门用来更新 `access token` （也就是上边例子里的token）的有效期。比如 `refresh token` 3 天过期，`access token` 7 天过期，在 `refresh token` 过期时，前端就调用一个**刷新 token 的接口去生成一个新的**`access token` ，前端拿到新token后再在之后的请求中带上新的token即可。\n\n**这样用户登录过一次后，只要平时在一直使用，就可以一直保持登录状态。**\n\n下面是一个为user模块使用jwt中间件的中间件case\n\n```typescript\nimport { jwt } from 'hono/jwt'\n\nconst jwtMiddware = jwt({\n  secret: 'your secret!!!!',\n})\n\nuser.use('/*', async (c, next) => {\n  // 检查当前请求路径是否在排除列表中\n  if (NoAuthPaths.includes(c.req.path)) {\n    await next();\n    return;\n  }\n  // 如果不在排除列表中，则进行JWT验证\n  await jwtMiddware(c, async () => {\n    const user = c.get('jwtPayload')\n    // 获取payload中的user信息\n    // 这些信息由登录接口提供\n    if (!user) {\n      c.set('errMsg', '用户未登录')\n      c.set('errCode', ErrorCode.UNAUTHORIZED)\n      throw new HTTPException(401)\n    }\n\n    if (user.id !== 1) {\n      c.set('errMsg', '用户无权限')\n      c.set('errCode', ErrorCode.PERMISSION_DENIED)\n      throw new HTTPException(401)\n    }\n\n    await next()\n  })\n\n})\n```\n\n其中 `const user = c.get('jwtPayload')` 这个写法也是官方文档中的写法，也是把`jwtPayload`写入到了上下文中，然后在后续的中间件中就可以拿到这个`payload`。\n\n每个模块的中间件处理逻辑可能相同也可能不同，后续我们再看情况， 把它抽离到根路由下或者写成一个单独的中间件，需要的模块自己去引入。\n\n## 总结\n\n目前对项目模块进行了分组，如：用户，商品等等。 每个模块可以写自己的`errorHandler`，也可以去设置自定义中间件。而像 `csrf` `cors`等共同的中间件则放在根路由下\n\n针对接口能否被请求，使用了`hono/jwt`，并为了让某些接口跳过校验以及不通过时返回自定义的错误信息，又封装装了一层。\n\n接口可以请求之后，来到参数校验的中间件`@hono/zod-validator`，由于每个接口schema可能比较多，以及为了让`errorHandler`来处理zod校验不通过的情况，又自定义了一个中间件在内部抛出错误。schema则是被提取到公共的文件夹（如`common`）下\n\n请求成功时，使用一个`standardRes`函数简单包装一下，统一返回值。\n\n请求失败时，在上下文中使用`c.set/get`注入错误信息，在`errorHandler`中间件中取出错误信息，并返回和成功时一致的json结构。\n\n这样看起来就又完善了一些\\~\\~\n\n**下一章为日志、数据库操作、配置文件相关逻辑**\n\n\\*\\*欢迎点赞催更(¯▽¯)\\*\\*👍\n",{"title":5,"description":58},"post/Hono/hono-params-check-response-standardized",[20],[2817],"hono@4.5.11","5D6Hg1b-NBkMTSPorHnvrQMkhLJJm79S2d4KI2wxxoQ",[2820,2824],{"title":2821,"path":2822,"stem":2823},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":2825,"path":2826,"stem":2827},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005086219]