[{"data":1,"prerenderedAt":1341},["ShallowReactive",2],{"page-/post/hono/hono-bun-fast":3,"surrounding-page":1332},{"id":4,"title":5,"author":6,"body":7,"date":1321,"description":202,"extension":1322,"group":6,"lastmod":1323,"meta":1324,"navigation":334,"path":1325,"rawbody":1326,"seo":1327,"showTitle":5,"stem":1328,"tags":1329,"versions":6,"__hash__":1331},"content/post/Hono/hono-bun-fast.md","【Hono】Bun竟然能这么快？搭配HonoJS的入门指南",null,{"type":8,"value":9,"toc":1313},"minimark",[10,13,21,24,41,48,53,59,70,76,99,102,112,123,128,143,146,153,159,162,166,172,178,181,184,187,190,193,196,229,232,251,258,268,663,674,695,699,705,711,932,942,954,960,963,1037,1040,1047,1050,1053,1056,1059,1258,1268,1271,1297,1300,1303,1306,1309],[11,12],"br",{},[14,15,16],"p",{},[17,18],"img",{"alt":19,"src":20},"1.00","https://img.zzao.club/article/202411191444086.png",[14,22,23],{},"最近我用bun+hono搭建了一个web服务，并尝试用docker打包部署。",[14,25,26,27,31,32,36,37,40],{},"在没有缓存的情况下，",[28,29,30],"code",{},"docker build"," ",[33,34,35],"strong",{},"打包仅用了30s","，如果是项目修改后再重新打包，更是",[33,38,39],{},"连5s都用不了","。",[14,42,43,44,47],{},"而这个小服务虽然只是刚起步，但已经具备了",[33,45,46],{},"日志、响应和错误标准化返回、数据库连接（sqlite）、路由分组、环境变量配置、jwt鉴权","这几项，可以说当成个人的小玩具已经够格了。",[49,50,52],"h2",{"id":51},"bun和node到底是什么关系","Bun和Node到底是什么关系",[54,55,56],"blockquote",{},[14,57,58],{},"Bun is an all-in-one JavaScript runtime & toolkit designed for speed, complete with a\nbundler, test runner, and Node.js-compatible package manager.",[14,60,61,62,65,66,69],{},"Bun 是一款专为提高速度而设计的一体化 JavaScript ",[33,63,64],{},"运行时","和",[33,67,68],{},"工具包","，配有捆绑器、测试运行器和与 Node.js 兼容的包管理器。",[14,71,72,73,75],{},"Node.js 是一个免费、开源、跨平台的 JavaScript ",[33,74,64],{},"环境。",[14,77,78,79,82,83,86,87,90,91,94,95,98],{},"通过他们官方的一句话介绍，可以清晰的看到，他们都是为了",[33,80,81],{},"让JavaScript脱离浏览器","环境而创造的一个运行时环境。只不过Node使用",[28,84,85],{},"C艹","编写，而Bun使用",[28,88,89],{},"Zig","。Node基于",[28,92,93],{},"V8引擎","(Goggle Chrome)，而Bun使用",[28,96,97],{},"JavaScriptCore","(Apple Safari)",[14,100,101],{},"以前我们用JavaScript都是和html配合写前端代码，写完后需要打开浏览器才能看到效果，而有了其他的运行时环境后，就可以像其他Python/Go语言一样直接在命令行执行JavaScript脚本，所以其功能也从操作Dom变为了操作系统级Api，如：文件IO、数据库等等。",[14,103,104,105,65,108,111],{},"在只有Node一家独大的时候，我们甚至可以在（某些）面试官问：「你会什么后端语言」的时候，说：「我会NodeJS」,而现在有了",[28,106,107],{},"Deno",[28,109,110],{},"Bun","，我岂不是会了三门后端语言？（🤫bushi",[14,113,114,115,118,119,122],{},"截止到当前，Node已经发布到了",[28,116,117],{},"22.8.0","，庞大的开源module支撑起了整个社群的，而他的官方包管理器",[28,120,121],{},"npm","有点让人一言难尽",[14,124,125],{},[17,126],{"alt":19,"src":127},"https://img.zzao.club/article/202411191444087.png",[14,129,130,131,134,135,138,139,142],{},"于是又出现了",[28,132,133],{},"yarn","、",[28,136,137],{},"pnpm","，",[33,140,141],{},"老外写这些东西可能真的是在解决需求，到了咱这边真的也就是给面试官多提供了一些出题思路。"," 而Bun本身就自带包管理器。",[14,144,145],{},"另外，现在要想写一个“时髦的”前端项目，还必须有一个打包器，因为在不同的运行环境中，不同的浏览器中，对JavaScript的支持标准大不相同，所以需要把新版本的JavaScript降级，或者把TypeScript转换为JavaScript。或者是把JavaScript文件大小进行压缩。",[14,147,148,149,152],{},"而Node生态下光打包器就有：Webpack、Rollup、Vite等等，更不要说Rust开始被大厂卷起来之后，又用Rust对以前打包速度、运行速度有上限的打包器进行重构。",[33,150,151],{},"面试官的出题角度还在增加","。\n而Bun本身也是一个打包器。",[14,154,155,156],{},"另外还有测试运行器..  ",[28,157,158],{}," Vitest/Jest",[14,160,161],{},"所以现在可以明白，在2022年才发布的Bun究竟是想要做什么了",[49,163,165],{"id":164},"hono简介","Hono简介",[14,167,168,169,171],{},"Hono🔥是一个基于 Web 标准构建的小型、简单且超快的 Web 框架。可以运行在所有JavaScript运行时，当然也包括了",[28,170,110],{},"，所以作为尝鲜，就图个鲜上加鲜。",[14,173,174,177],{},[33,175,176],{},"在使用任何框架前，我都习惯先通读一遍官方文档","，这大概会花费我2-4小时的时候。",[14,179,180],{},"在读完一遍官方文档后，如果使用过其他框架完成过类似的项目，就会大概知道这个框架哪些是自带的，哪些需要借助第三方。",[14,182,183],{},"如果框架本身过于精简（比如Koa），你就不得不去研究一些官方插件或是第三方插件，或者说去拜读一些开源项目，或者去找一些快速启动模板，以便自己快速上手。",[14,185,186],{},"Hono则是在文档里提供了很多官方的插件（Helper），无需翻看其他文档就能实现功能",[49,188,189],{"id":189},"搭建项目",[14,191,192],{},"按照官方文档开始搭建，因为我这里使用的是Bun，所以需要先下载好Bun",[14,194,195],{},"Macos/Linux",[197,198,203],"pre",{"className":199,"code":200,"language":201,"meta":202,"style":202},"language-shell shiki shiki-themes github-light","curl -fsSL https://bun.sh/install | bash\n","shell","",[28,204,205],{"__ignoreMap":202},[206,207,210,214,218,222,226],"span",{"class":208,"line":209},"line",1,[206,211,213],{"class":212},"s7eDp","curl",[206,215,217],{"class":216},"sYu0t"," -fsSL",[206,219,221],{"class":220},"sYBdl"," https://bun.sh/install",[206,223,225],{"class":224},"sD7c4"," |",[206,227,228],{"class":212}," bash\n",[14,230,231],{},"然后创建项目",[197,233,235],{"className":199,"code":234,"language":201,"meta":202,"style":202},"bun create hono@latest my-app\n",[28,236,237],{"__ignoreMap":202},[206,238,239,242,245,248],{"class":208,"line":209},[206,240,241],{"class":212},"bun",[206,243,244],{"class":220}," create",[206,246,247],{"class":220}," hono@latest",[206,249,250],{"class":220}," my-app\n",[14,252,253,254,257],{},"创建项目后会有一个入口",[28,255,256],{},"index.ts","，在Bun中TS是一等公民，无需进行编译就能直接运行，所以速度非常快。",[14,259,260,261,134,264,267],{},"然后我们需要添加一些常用的中间件，如",[28,262,263],{},"cors",[28,265,266],{},"csrf","，然后给自己配置一下喜欢的端口号",[197,269,273],{"className":270,"code":271,"language":272,"meta":202,"style":202},"language-typescript shiki shiki-themes github-light","import { Hono } from 'hono'\nimport { showRoutes } from 'hono/dev'\nimport { cors } from 'hono/cors'\nimport { csrf } from 'hono/csrf'\n\nconst app = new Hono()\n// 统一的前缀\nconst api = app.basePath('/api')\n\n// 预防csrf攻击\napi.use(csrf())\n// 所有接口设置cors， 也可以分别设置cors， 如user相关接口只允许指定ip访问\napi.use('*', cors())\n\napp.get('/', (c) => { return c.text('Hello Hono!') })\napp.post('/', (c) => c.text('POST /')) \napp.put('/', (c) => c.text('PUT /')) \napp.delete('/', (c) => c.text('DELETE /'))\n\n// 每个实例的err要自己监听\n// api.onError(errorHandler)\n\n// verbose 会显示详情信息， 如： 是否使用了中间件\n// showRoutes(api, { verbose: false })\n\nexport default {\n  port: Bun.env.PORT,\n  fetch: app.fetch,\n}\n","typescript",[28,274,275,290,303,316,329,336,357,364,389,394,400,416,422,441,446,493,525,556,588,593,599,605,610,616,622,627,639,651,657],{"__ignoreMap":202},[206,276,277,280,284,287],{"class":208,"line":209},[206,278,279],{"class":224},"import",[206,281,283],{"class":282},"sgsFI"," { Hono } ",[206,285,286],{"class":224},"from",[206,288,289],{"class":220}," 'hono'\n",[206,291,293,295,298,300],{"class":208,"line":292},2,[206,294,279],{"class":224},[206,296,297],{"class":282}," { showRoutes } ",[206,299,286],{"class":224},[206,301,302],{"class":220}," 'hono/dev'\n",[206,304,306,308,311,313],{"class":208,"line":305},3,[206,307,279],{"class":224},[206,309,310],{"class":282}," { cors } ",[206,312,286],{"class":224},[206,314,315],{"class":220}," 'hono/cors'\n",[206,317,319,321,324,326],{"class":208,"line":318},4,[206,320,279],{"class":224},[206,322,323],{"class":282}," { csrf } ",[206,325,286],{"class":224},[206,327,328],{"class":220}," 'hono/csrf'\n",[206,330,332],{"class":208,"line":331},5,[206,333,335],{"emptyLinePlaceholder":334},true,"\n",[206,337,339,342,345,348,351,354],{"class":208,"line":338},6,[206,340,341],{"class":224},"const",[206,343,344],{"class":216}," app",[206,346,347],{"class":224}," =",[206,349,350],{"class":224}," new",[206,352,353],{"class":212}," Hono",[206,355,356],{"class":282},"()\n",[206,358,360],{"class":208,"line":359},7,[206,361,363],{"class":362},"sAwPA","// 统一的前缀\n",[206,365,367,369,372,374,377,380,383,386],{"class":208,"line":366},8,[206,368,341],{"class":224},[206,370,371],{"class":216}," api",[206,373,347],{"class":224},[206,375,376],{"class":282}," app.",[206,378,379],{"class":212},"basePath",[206,381,382],{"class":282},"(",[206,384,385],{"class":220},"'/api'",[206,387,388],{"class":282},")\n",[206,390,392],{"class":208,"line":391},9,[206,393,335],{"emptyLinePlaceholder":334},[206,395,397],{"class":208,"line":396},10,[206,398,399],{"class":362},"// 预防csrf攻击\n",[206,401,403,406,409,411,413],{"class":208,"line":402},11,[206,404,405],{"class":282},"api.",[206,407,408],{"class":212},"use",[206,410,382],{"class":282},[206,412,266],{"class":212},[206,414,415],{"class":282},"())\n",[206,417,419],{"class":208,"line":418},12,[206,420,421],{"class":362},"// 所有接口设置cors， 也可以分别设置cors， 如user相关接口只允许指定ip访问\n",[206,423,425,427,429,431,434,437,439],{"class":208,"line":424},13,[206,426,405],{"class":282},[206,428,408],{"class":212},[206,430,382],{"class":282},[206,432,433],{"class":220},"'*'",[206,435,436],{"class":282},", ",[206,438,263],{"class":212},[206,440,415],{"class":282},[206,442,444],{"class":208,"line":443},14,[206,445,335],{"emptyLinePlaceholder":334},[206,447,449,452,455,457,460,463,467,470,473,476,479,482,485,487,490],{"class":208,"line":448},15,[206,450,451],{"class":282},"app.",[206,453,454],{"class":212},"get",[206,456,382],{"class":282},[206,458,459],{"class":220},"'/'",[206,461,462],{"class":282},", (",[206,464,466],{"class":465},"sqxcx","c",[206,468,469],{"class":282},") ",[206,471,472],{"class":224},"=>",[206,474,475],{"class":282}," { ",[206,477,478],{"class":224},"return",[206,480,481],{"class":282}," c.",[206,483,484],{"class":212},"text",[206,486,382],{"class":282},[206,488,489],{"class":220},"'Hello Hono!'",[206,491,492],{"class":282},") })\n",[206,494,496,498,501,503,505,507,509,511,513,515,517,519,522],{"class":208,"line":495},16,[206,497,451],{"class":282},[206,499,500],{"class":212},"post",[206,502,382],{"class":282},[206,504,459],{"class":220},[206,506,462],{"class":282},[206,508,466],{"class":465},[206,510,469],{"class":282},[206,512,472],{"class":224},[206,514,481],{"class":282},[206,516,484],{"class":212},[206,518,382],{"class":282},[206,520,521],{"class":220},"'POST /'",[206,523,524],{"class":282},")) \n",[206,526,528,530,533,535,537,539,541,543,545,547,549,551,554],{"class":208,"line":527},17,[206,529,451],{"class":282},[206,531,532],{"class":212},"put",[206,534,382],{"class":282},[206,536,459],{"class":220},[206,538,462],{"class":282},[206,540,466],{"class":465},[206,542,469],{"class":282},[206,544,472],{"class":224},[206,546,481],{"class":282},[206,548,484],{"class":212},[206,550,382],{"class":282},[206,552,553],{"class":220},"'PUT /'",[206,555,524],{"class":282},[206,557,559,561,564,566,568,570,572,574,576,578,580,582,585],{"class":208,"line":558},18,[206,560,451],{"class":282},[206,562,563],{"class":212},"delete",[206,565,382],{"class":282},[206,567,459],{"class":220},[206,569,462],{"class":282},[206,571,466],{"class":465},[206,573,469],{"class":282},[206,575,472],{"class":224},[206,577,481],{"class":282},[206,579,484],{"class":212},[206,581,382],{"class":282},[206,583,584],{"class":220},"'DELETE /'",[206,586,587],{"class":282},"))\n",[206,589,591],{"class":208,"line":590},19,[206,592,335],{"emptyLinePlaceholder":334},[206,594,596],{"class":208,"line":595},20,[206,597,598],{"class":362},"// 每个实例的err要自己监听\n",[206,600,602],{"class":208,"line":601},21,[206,603,604],{"class":362},"// api.onError(errorHandler)\n",[206,606,608],{"class":208,"line":607},22,[206,609,335],{"emptyLinePlaceholder":334},[206,611,613],{"class":208,"line":612},23,[206,614,615],{"class":362},"// verbose 会显示详情信息， 如： 是否使用了中间件\n",[206,617,619],{"class":208,"line":618},24,[206,620,621],{"class":362},"// showRoutes(api, { verbose: false })\n",[206,623,625],{"class":208,"line":624},25,[206,626,335],{"emptyLinePlaceholder":334},[206,628,630,633,636],{"class":208,"line":629},26,[206,631,632],{"class":224},"export",[206,634,635],{"class":224}," default",[206,637,638],{"class":282}," {\n",[206,640,642,645,648],{"class":208,"line":641},27,[206,643,644],{"class":282},"  port: Bun.env.",[206,646,647],{"class":216},"PORT",[206,649,650],{"class":282},",\n",[206,652,654],{"class":208,"line":653},28,[206,655,656],{"class":282},"  fetch: app.fetch,\n",[206,658,660],{"class":208,"line":659},29,[206,661,662],{"class":282},"}\n",[14,664,665,666,669,670,673],{},"这样就完成了一个简单的服务，可以打开",[28,667,668],{},"localhost:port","，看一下是否返回了",[28,671,672],{},"Hello Hono","!",[14,675,676,677,679,680,683,684,134,687,690,691,694],{},"设置统一的前缀可以用",[28,678,379],{},"，设置环境变量可以在",[28,681,682],{},".env"," 、",[28,685,686],{},".env.development",[28,688,689],{},".env.production"," 中配置（在bun的官方文档中），并使用",[28,692,693],{},"Bun.env.XXXX","读取。",[49,696,698],{"id":697},"user模块路由","User模块路由",[14,700,701,702,704],{},"只有一个接口，我们可以写在",[28,703,256],{},"里，那如果有一堆接口呢，肯定要进行分组的",[14,706,707,708],{},"Hono中路由分组也比较简单，只要再用一次",[28,709,710],{},"new Hono()",[197,712,714],{"className":270,"code":713,"language":272,"meta":202,"style":202},"\n// user模块\nconst user = new Hono()\n\nuser.get('/list', (c) => c.text('List users')) // GET /user\nuser.get('/:id', (c) => {\n  // GET /user/:id\n  const id = c.req.param('id')\n  return c.text('Get user: ' + id)\n})\nuser.post('/', (c) => c.text('Create user')) // POST /user\n\n// indext.ts\nconst app = new Hono()\n\n// 使用user路由组\napp.route('/user', user)\n",[28,715,716,720,725,740,744,779,800,805,828,848,853,885,889,894,908,912,917],{"__ignoreMap":202},[206,717,718],{"class":208,"line":209},[206,719,335],{"emptyLinePlaceholder":334},[206,721,722],{"class":208,"line":292},[206,723,724],{"class":362},"// user模块\n",[206,726,727,729,732,734,736,738],{"class":208,"line":305},[206,728,341],{"class":224},[206,730,731],{"class":216}," user",[206,733,347],{"class":224},[206,735,350],{"class":224},[206,737,353],{"class":212},[206,739,356],{"class":282},[206,741,742],{"class":208,"line":318},[206,743,335],{"emptyLinePlaceholder":334},[206,745,746,749,751,753,756,758,760,762,764,766,768,770,773,776],{"class":208,"line":331},[206,747,748],{"class":282},"user.",[206,750,454],{"class":212},[206,752,382],{"class":282},[206,754,755],{"class":220},"'/list'",[206,757,462],{"class":282},[206,759,466],{"class":465},[206,761,469],{"class":282},[206,763,472],{"class":224},[206,765,481],{"class":282},[206,767,484],{"class":212},[206,769,382],{"class":282},[206,771,772],{"class":220},"'List users'",[206,774,775],{"class":282},")) ",[206,777,778],{"class":362},"// GET /user\n",[206,780,781,783,785,787,790,792,794,796,798],{"class":208,"line":338},[206,782,748],{"class":282},[206,784,454],{"class":212},[206,786,382],{"class":282},[206,788,789],{"class":220},"'/:id'",[206,791,462],{"class":282},[206,793,466],{"class":465},[206,795,469],{"class":282},[206,797,472],{"class":224},[206,799,638],{"class":282},[206,801,802],{"class":208,"line":359},[206,803,804],{"class":362},"  // GET /user/:id\n",[206,806,807,810,813,815,818,821,823,826],{"class":208,"line":366},[206,808,809],{"class":224},"  const",[206,811,812],{"class":216}," id",[206,814,347],{"class":224},[206,816,817],{"class":282}," c.req.",[206,819,820],{"class":212},"param",[206,822,382],{"class":282},[206,824,825],{"class":220},"'id'",[206,827,388],{"class":282},[206,829,830,833,835,837,839,842,845],{"class":208,"line":391},[206,831,832],{"class":224},"  return",[206,834,481],{"class":282},[206,836,484],{"class":212},[206,838,382],{"class":282},[206,840,841],{"class":220},"'Get user: '",[206,843,844],{"class":224}," +",[206,846,847],{"class":282}," id)\n",[206,849,850],{"class":208,"line":396},[206,851,852],{"class":282},"})\n",[206,854,855,857,859,861,863,865,867,869,871,873,875,877,880,882],{"class":208,"line":402},[206,856,748],{"class":282},[206,858,500],{"class":212},[206,860,382],{"class":282},[206,862,459],{"class":220},[206,864,462],{"class":282},[206,866,466],{"class":465},[206,868,469],{"class":282},[206,870,472],{"class":224},[206,872,481],{"class":282},[206,874,484],{"class":212},[206,876,382],{"class":282},[206,878,879],{"class":220},"'Create user'",[206,881,775],{"class":282},[206,883,884],{"class":362},"// POST /user\n",[206,886,887],{"class":208,"line":418},[206,888,335],{"emptyLinePlaceholder":334},[206,890,891],{"class":208,"line":424},[206,892,893],{"class":362},"// indext.ts\n",[206,895,896,898,900,902,904,906],{"class":208,"line":443},[206,897,341],{"class":224},[206,899,344],{"class":216},[206,901,347],{"class":224},[206,903,350],{"class":224},[206,905,353],{"class":212},[206,907,356],{"class":282},[206,909,910],{"class":208,"line":448},[206,911,335],{"emptyLinePlaceholder":334},[206,913,914],{"class":208,"line":495},[206,915,916],{"class":362},"// 使用user路由组\n",[206,918,919,921,924,926,929],{"class":208,"line":527},[206,920,451],{"class":282},[206,922,923],{"class":212},"route",[206,925,382],{"class":282},[206,927,928],{"class":220},"'/user'",[206,930,931],{"class":282},", user)\n",[14,933,934,935,937,938,941],{},"所以，要想给路由分组，只需要在src下再新建一个user文件夹，里面实现user的路由，在从",[28,936,256],{},"里使用",[28,939,940],{},"app.route('/user', user)","就可以了。",[14,943,944,945,947,948,683,951],{},"由于我们使用了",[28,946,379],{},"，所以此时的user接口为",[28,949,950],{},"/api/user/list",[28,952,953],{},"/api/user/:id",[14,955,956,957],{},"我们在使用公司后端接口或者其他网站的开放接口接口时，经常会看到这样的结构",[28,958,959],{},"/api/v1/user/a/b/c",[14,961,962],{},"在Hono中可以这样实现",[197,964,966],{"className":270,"code":965,"language":272,"meta":202,"style":202},"user.get('/list', (c) => c.text('我是 user/list'))\nv1.route('/user', user)\napp.route('/v1', v1)\n\nexport default app\n",[28,967,968,997,1010,1024,1028],{"__ignoreMap":202},[206,969,970,972,974,976,978,980,982,984,986,988,990,992,995],{"class":208,"line":209},[206,971,748],{"class":282},[206,973,454],{"class":212},[206,975,382],{"class":282},[206,977,755],{"class":220},[206,979,462],{"class":282},[206,981,466],{"class":465},[206,983,469],{"class":282},[206,985,472],{"class":224},[206,987,481],{"class":282},[206,989,484],{"class":212},[206,991,382],{"class":282},[206,993,994],{"class":220},"'我是 user/list'",[206,996,587],{"class":282},[206,998,999,1002,1004,1006,1008],{"class":208,"line":292},[206,1000,1001],{"class":282},"v1.",[206,1003,923],{"class":212},[206,1005,382],{"class":282},[206,1007,928],{"class":220},[206,1009,931],{"class":282},[206,1011,1012,1014,1016,1018,1021],{"class":208,"line":305},[206,1013,451],{"class":282},[206,1015,923],{"class":212},[206,1017,382],{"class":282},[206,1019,1020],{"class":220},"'/v1'",[206,1022,1023],{"class":282},", v1)\n",[206,1025,1026],{"class":208,"line":318},[206,1027,335],{"emptyLinePlaceholder":334},[206,1029,1030,1032,1034],{"class":208,"line":331},[206,1031,632],{"class":224},[206,1033,635],{"class":224},[206,1035,1036],{"class":282}," app\n",[14,1038,1039],{},"它会这样响应",[197,1041,1045],{"className":1042,"code":1044,"language":484},[1043],"language-text","GET /api/v1/user/list ---> `我是 user/list`\n",[28,1046,1044],{"__ignoreMap":202},[14,1048,1049],{},"注意，如果上述代码中，route注册的顺序出错，则不会正常响应",[49,1051,1052],{"id":1052},"错误捕捉",[14,1054,1055],{},"当发生一些致命错误时，为了不让服务挂掉，我们需要catch住，并且返回给前端一些友好的提示。不然我们那些年骂过的xx后端就变成了自己。",[14,1057,1058],{},"在Hono中使用也很简单，不需要自己单独写个中间件",[197,1060,1062],{"className":270,"code":1061,"language":272,"meta":202,"style":202},"import { HTTPException } from 'hono/http-exception'\n\n// ...\n\napp.onError((err, c) => {\n  // 任何请求， http status 返回200， 错误码在返回体自定义\n  const status = 200;\n  // 记录原始的错误， 返回给前端的是友好的信息\n  // TODO Logger\n  const errorCode = 40001\n  const errorMsg = '不是我的错，想想前端的问题！'\n  if (err instanceof HTTPException) {\n    errorCode = ErrorCode.UNAUTHORIZED\n  }\n\n  const response = {\n    code: errorCode,\n    data: null,\n    message: errorMsg,\n  };\n  return c.json(response, status)\n})\n",[28,1063,1064,1076,1080,1085,1089,1112,1117,1132,1137,1142,1154,1166,1183,1197,1202,1206,1217,1222,1232,1237,1242,1254],{"__ignoreMap":202},[206,1065,1066,1068,1071,1073],{"class":208,"line":209},[206,1067,279],{"class":224},[206,1069,1070],{"class":282}," { HTTPException } ",[206,1072,286],{"class":224},[206,1074,1075],{"class":220}," 'hono/http-exception'\n",[206,1077,1078],{"class":208,"line":292},[206,1079,335],{"emptyLinePlaceholder":334},[206,1081,1082],{"class":208,"line":305},[206,1083,1084],{"class":362},"// ...\n",[206,1086,1087],{"class":208,"line":318},[206,1088,335],{"emptyLinePlaceholder":334},[206,1090,1091,1093,1096,1099,1102,1104,1106,1108,1110],{"class":208,"line":331},[206,1092,451],{"class":282},[206,1094,1095],{"class":212},"onError",[206,1097,1098],{"class":282},"((",[206,1100,1101],{"class":465},"err",[206,1103,436],{"class":282},[206,1105,466],{"class":465},[206,1107,469],{"class":282},[206,1109,472],{"class":224},[206,1111,638],{"class":282},[206,1113,1114],{"class":208,"line":338},[206,1115,1116],{"class":362},"  // 任何请求， http status 返回200， 错误码在返回体自定义\n",[206,1118,1119,1121,1124,1126,1129],{"class":208,"line":359},[206,1120,809],{"class":224},[206,1122,1123],{"class":216}," status",[206,1125,347],{"class":224},[206,1127,1128],{"class":216}," 200",[206,1130,1131],{"class":282},";\n",[206,1133,1134],{"class":208,"line":366},[206,1135,1136],{"class":362},"  // 记录原始的错误， 返回给前端的是友好的信息\n",[206,1138,1139],{"class":208,"line":391},[206,1140,1141],{"class":362},"  // TODO Logger\n",[206,1143,1144,1146,1149,1151],{"class":208,"line":396},[206,1145,809],{"class":224},[206,1147,1148],{"class":216}," errorCode",[206,1150,347],{"class":224},[206,1152,1153],{"class":216}," 40001\n",[206,1155,1156,1158,1161,1163],{"class":208,"line":402},[206,1157,809],{"class":224},[206,1159,1160],{"class":216}," errorMsg",[206,1162,347],{"class":224},[206,1164,1165],{"class":220}," '不是我的错，想想前端的问题！'\n",[206,1167,1168,1171,1174,1177,1180],{"class":208,"line":418},[206,1169,1170],{"class":224},"  if",[206,1172,1173],{"class":282}," (err ",[206,1175,1176],{"class":224},"instanceof",[206,1178,1179],{"class":212}," HTTPException",[206,1181,1182],{"class":282},") {\n",[206,1184,1185,1188,1191,1194],{"class":208,"line":424},[206,1186,1187],{"class":282},"    errorCode ",[206,1189,1190],{"class":224},"=",[206,1192,1193],{"class":282}," ErrorCode.",[206,1195,1196],{"class":216},"UNAUTHORIZED\n",[206,1198,1199],{"class":208,"line":443},[206,1200,1201],{"class":282},"  }\n",[206,1203,1204],{"class":208,"line":448},[206,1205,335],{"emptyLinePlaceholder":334},[206,1207,1208,1210,1213,1215],{"class":208,"line":495},[206,1209,809],{"class":224},[206,1211,1212],{"class":216}," response",[206,1214,347],{"class":224},[206,1216,638],{"class":282},[206,1218,1219],{"class":208,"line":527},[206,1220,1221],{"class":282},"    code: errorCode,\n",[206,1223,1224,1227,1230],{"class":208,"line":558},[206,1225,1226],{"class":282},"    data: ",[206,1228,1229],{"class":216},"null",[206,1231,650],{"class":282},[206,1233,1234],{"class":208,"line":590},[206,1235,1236],{"class":282},"    message: errorMsg,\n",[206,1238,1239],{"class":208,"line":595},[206,1240,1241],{"class":282},"  };\n",[206,1243,1244,1246,1248,1251],{"class":208,"line":601},[206,1245,832],{"class":224},[206,1247,481],{"class":282},[206,1249,1250],{"class":212},"json",[206,1252,1253],{"class":282},"(response, status)\n",[206,1255,1256],{"class":208,"line":607},[206,1257,852],{"class":282},[14,1259,1260,1261,1264,1265],{},"这样，在后端有任何错误的时候，我们都会以http status 200的状态码返回，并且可以在返回体中定义好固定的结果，并且返回出一个自定义的",[28,1262,1263],{},"errorCode","，外加一个友好的前端能看得懂的",[28,1266,1267],{},"errorMsg",[14,1269,1270],{},"随着项目的复杂度增加，可以把这个handler单独拆分出去，已达到精简入口文件的目的。",[14,1272,1273,1274,1277,1278,1281,1282,1284,1285,1288,1289,1292,1293,1296],{},"比如新建一个",[28,1275,1276],{},"common"," 文件夹，里面写一个",[28,1279,1280],{},"errorHandler.ts","，在",[28,1283,256],{},"中或者在user模块",[28,1286,1287],{},"src/user/index.ts","中，都可以分别使用",[28,1290,1291],{},"app.onError()"," 和  ",[28,1294,1295],{},"user.onError()","具体处理通用的或者是自定义的错误处理逻辑！",[49,1298,1299],{"id":1299},"总结",[14,1301,1302],{},"这篇文章是一个入门篇，主要目的是讲述一下Node和Bun的区别，以及使用Bun+Hono的一个入门项目。",[14,1304,1305],{},"路由分组、错误捕捉这些功能很简单的就可以实现了，因为篇幅原因，我就把其他功能拆分成多篇教程了。后续教程会涉及：数据库、响应标准化、日志、jwt鉴权、docker/docker-compose打包部署等等，是一个完整闭环的小项目，代码也会开源分享出来，感兴趣的可以关注起来~",[14,1307,1308],{},"欢迎点赞催更👍",[1310,1311,1312],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}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 .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}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":202,"searchDepth":292,"depth":292,"links":1314},[1315,1316,1317,1318,1319,1320],{"id":51,"depth":292,"text":52},{"id":164,"depth":292,"text":165},{"id":189,"depth":292,"text":189},{"id":697,"depth":292,"text":698},{"id":1052,"depth":292,"text":1052},{"id":1299,"depth":292,"text":1299},"2025-02-10T00:00:00.000Z","md","2025-04-02T00:00:00.000Z",{},"/post/hono/hono-bun-fast","---\ntitle: 【Hono】Bun竟然能这么快？搭配HonoJS的入门指南\ndate: 2025-02-10\nlastmod: 2025-04-02\nshowTitle: 【Hono】Bun竟然能这么快？搭配HonoJS的入门指南\ntags: [\"Hono\"]\n\n---\n\u003Cbr />\n\n![1.00](https://img.zzao.club/article/202411191444086.png)\n\n最近我用bun+hono搭建了一个web服务，并尝试用docker打包部署。\n\n在没有缓存的情况下，`docker build` **打包仅用了30s**，如果是项目修改后再重新打包，更是**连5s都用不了**。\n\n而这个小服务虽然只是刚起步，但已经具备了**日志、响应和错误标准化返回、数据库连接（sqlite）、路由分组、环境变量配置、jwt鉴权**这几项，可以说当成个人的小玩具已经够格了。\n\n## Bun和Node到底是什么关系\n\n> Bun is an all-in-one JavaScript runtime & toolkit designed for speed, complete with a\n> bundler, test runner, and Node.js-compatible package manager.\n\nBun 是一款专为提高速度而设计的一体化 JavaScript **运行时**和**工具包**，配有捆绑器、测试运行器和与 Node.js 兼容的包管理器。\n\nNode.js 是一个免费、开源、跨平台的 JavaScript **运行时**环境。\n\n通过他们官方的一句话介绍，可以清晰的看到，他们都是为了**让JavaScript脱离浏览器**环境而创造的一个运行时环境。只不过Node使用`C艹`编写，而Bun使用`Zig`。Node基于`V8引擎`(Goggle Chrome)，而Bun使用`JavaScriptCore`(Apple Safari)\n\n以前我们用JavaScript都是和html配合写前端代码，写完后需要打开浏览器才能看到效果，而有了其他的运行时环境后，就可以像其他Python/Go语言一样直接在命令行执行JavaScript脚本，所以其功能也从操作Dom变为了操作系统级Api，如：文件IO、数据库等等。\n\n在只有Node一家独大的时候，我们甚至可以在（某些）面试官问：「你会什么后端语言」的时候，说：「我会NodeJS」,而现在有了`Deno`和`Bun`，我岂不是会了三门后端语言？（🤫bushi\n\n截止到当前，Node已经发布到了`22.8.0`，庞大的开源module支撑起了整个社群的，而他的官方包管理器`npm`有点让人一言难尽\n\n![1.00](https://img.zzao.club/article/202411191444087.png)\n\n于是又出现了`yarn`、`pnpm`，**老外写这些东西可能真的是在解决需求，到了咱这边真的也就是给面试官多提供了一些出题思路。** 而Bun本身就自带包管理器。\n\n另外，现在要想写一个“时髦的”前端项目，还必须有一个打包器，因为在不同的运行环境中，不同的浏览器中，对JavaScript的支持标准大不相同，所以需要把新版本的JavaScript降级，或者把TypeScript转换为JavaScript。或者是把JavaScript文件大小进行压缩。\n\n而Node生态下光打包器就有：Webpack、Rollup、Vite等等，更不要说Rust开始被大厂卷起来之后，又用Rust对以前打包速度、运行速度有上限的打包器进行重构。**面试官的出题角度还在增加**。\n而Bun本身也是一个打包器。\n\n另外还有测试运行器..  ` Vitest/Jest`\n\n所以现在可以明白，在2022年才发布的Bun究竟是想要做什么了\n\n## Hono简介\n\nHono🔥是一个基于 Web 标准构建的小型、简单且超快的 Web 框架。可以运行在所有JavaScript运行时，当然也包括了`Bun`，所以作为尝鲜，就图个鲜上加鲜。\n\n**在使用任何框架前，我都习惯先通读一遍官方文档**，这大概会花费我2-4小时的时候。\n\n在读完一遍官方文档后，如果使用过其他框架完成过类似的项目，就会大概知道这个框架哪些是自带的，哪些需要借助第三方。\n\n如果框架本身过于精简（比如Koa），你就不得不去研究一些官方插件或是第三方插件，或者说去拜读一些开源项目，或者去找一些快速启动模板，以便自己快速上手。\n\nHono则是在文档里提供了很多官方的插件（Helper），无需翻看其他文档就能实现功能\n\n## 搭建项目\n\n按照官方文档开始搭建，因为我这里使用的是Bun，所以需要先下载好Bun\n\nMacos/Linux\n\n```shell\ncurl -fsSL https://bun.sh/install | bash\n```\n\n然后创建项目\n\n```shell\nbun create hono@latest my-app\n```\n\n创建项目后会有一个入口`index.ts`，在Bun中TS是一等公民，无需进行编译就能直接运行，所以速度非常快。\n\n然后我们需要添加一些常用的中间件，如`cors`、`csrf`，然后给自己配置一下喜欢的端口号\n\n```typescript\nimport { Hono } from 'hono'\nimport { showRoutes } from 'hono/dev'\nimport { cors } from 'hono/cors'\nimport { csrf } from 'hono/csrf'\n\nconst app = new Hono()\n// 统一的前缀\nconst api = app.basePath('/api')\n\n// 预防csrf攻击\napi.use(csrf())\n// 所有接口设置cors， 也可以分别设置cors， 如user相关接口只允许指定ip访问\napi.use('*', cors())\n\napp.get('/', (c) => { return c.text('Hello Hono!') })\napp.post('/', (c) => c.text('POST /')) \napp.put('/', (c) => c.text('PUT /')) \napp.delete('/', (c) => c.text('DELETE /'))\n\n// 每个实例的err要自己监听\n// api.onError(errorHandler)\n\n// verbose 会显示详情信息， 如： 是否使用了中间件\n// showRoutes(api, { verbose: false })\n\nexport default {\n  port: Bun.env.PORT,\n  fetch: app.fetch,\n}\n```\n\n这样就完成了一个简单的服务，可以打开`localhost:port`，看一下是否返回了`Hello Hono`!\n\n设置统一的前缀可以用`basePath`，设置环境变量可以在`.env` 、`.env.development`、`.env.production` 中配置（在bun的官方文档中），并使用`Bun.env.XXXX`读取。\n\n## User模块路由\n\n只有一个接口，我们可以写在`index.ts`里，那如果有一堆接口呢，肯定要进行分组的\n\nHono中路由分组也比较简单，只要再用一次`new Hono()`\n\n```typescript\n\n// user模块\nconst user = new Hono()\n\nuser.get('/list', (c) => c.text('List users')) // GET /user\nuser.get('/:id', (c) => {\n  // GET /user/:id\n  const id = c.req.param('id')\n  return c.text('Get user: ' + id)\n})\nuser.post('/', (c) => c.text('Create user')) // POST /user\n\n// indext.ts\nconst app = new Hono()\n\n// 使用user路由组\napp.route('/user', user)\n```\n\n所以，要想给路由分组，只需要在src下再新建一个user文件夹，里面实现user的路由，在从`index.ts`里使用`app.route('/user', user)`就可以了。\n\n由于我们使用了`basePath`，所以此时的user接口为`/api/user/list` 、`/api/user/:id`\n\n我们在使用公司后端接口或者其他网站的开放接口接口时，经常会看到这样的结构`/api/v1/user/a/b/c`\n\n在Hono中可以这样实现\n\n```typescript\nuser.get('/list', (c) => c.text('我是 user/list'))\nv1.route('/user', user)\napp.route('/v1', v1)\n\nexport default app\n```\n\n它会这样响应\n\n```\nGET /api/v1/user/list ---> `我是 user/list`\n```\n\n注意，如果上述代码中，route注册的顺序出错，则不会正常响应\n\n## 错误捕捉\n\n当发生一些致命错误时，为了不让服务挂掉，我们需要catch住，并且返回给前端一些友好的提示。不然我们那些年骂过的xx后端就变成了自己。\n\n在Hono中使用也很简单，不需要自己单独写个中间件\n\n```typescript\nimport { HTTPException } from 'hono/http-exception'\n\n// ...\n\napp.onError((err, c) => {\n  // 任何请求， http status 返回200， 错误码在返回体自定义\n  const status = 200;\n  // 记录原始的错误， 返回给前端的是友好的信息\n  // TODO Logger\n  const errorCode = 40001\n  const errorMsg = '不是我的错，想想前端的问题！'\n  if (err instanceof HTTPException) {\n    errorCode = ErrorCode.UNAUTHORIZED\n  }\n\n  const response = {\n    code: errorCode,\n    data: null,\n    message: errorMsg,\n  };\n  return c.json(response, status)\n})\n```\n\n这样，在后端有任何错误的时候，我们都会以http status 200的状态码返回，并且可以在返回体中定义好固定的结果，并且返回出一个自定义的`errorCode`，外加一个友好的前端能看得懂的`errorMsg`\n\n随着项目的复杂度增加，可以把这个handler单独拆分出去，已达到精简入口文件的目的。\n\n比如新建一个`common` 文件夹，里面写一个`errorHandler.ts`，在`index.ts`中或者在user模块`src/user/index.ts`中，都可以分别使用`app.onError()` 和  `user.onError()`具体处理通用的或者是自定义的错误处理逻辑！\n\n## 总结\n\n这篇文章是一个入门篇，主要目的是讲述一下Node和Bun的区别，以及使用Bun+Hono的一个入门项目。\n\n路由分组、错误捕捉这些功能很简单的就可以实现了，因为篇幅原因，我就把其他功能拆分成多篇教程了。后续教程会涉及：数据库、响应标准化、日志、jwt鉴权、docker/docker-compose打包部署等等，是一个完整闭环的小项目，代码也会开源分享出来，感兴趣的可以关注起来\\~\n\n欢迎点赞催更👍\n",{"title":5,"description":202},"post/Hono/hono-bun-fast",[1330],"Hono","qelN51TVyurneRMeadXiKNKdJnYQcSMMu4WdEB5dzgc",[1333,1337],{"title":1334,"path":1335,"stem":1336},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":1338,"path":1339,"stem":1340},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005086180]