[{"data":1,"prerenderedAt":14075},["ShallowReactive",2],{"post-nuxt-content-lessons":3,"series-source-nuxt-content-lessons":1258},{"id":4,"title":5,"author":6,"body":7,"description":1241,"difficulty":1242,"draft":1243,"extension":1244,"image":1245,"meta":1246,"navigation":752,"path":1247,"pinned":1243,"published":1248,"seo":1249,"series":1250,"seriesOrder":72,"stem":1251,"tags":1252,"updated":1256,"__hash__":1257},"posts\u002Fposts\u002Fnuxt-content-lessons\u002Findex.md","Nuxt Content v3 踩坑记录：目录、图片与那些反直觉的设计",null,{"type":8,"value":9,"toc":1231},"minimark",[10,14,18,21,25,29,42,45,52,140,147,150,157,449,459,463,466,469,495,509,512,537,543,546,549,552,557,564,798,803,847,851,854,861,864,920,926,1156,1166,1169,1221,1224,1227],[11,12,13],"h1",{"id":13},"背景",[15,16,17],"p",{},"最近在用 Nuxt 3 + Nuxt Content v3 搭建一个个人博客，参考了一个 SvelteKit 项目（svaf）的架构。原以为换个框架只是语法差异，没想到在几个关键功能上踩了不少坑。",[15,19,20],{},"这篇文章记录了三个核心问题的排查过程和最终方案。",[11,22,24],{"id":23},"问题一目录导航toc只有-h2","问题一：目录导航（TOC）只有 h2",[26,27,28],"h2",{"id":28},"现象",[15,30,31,32,36,37,41],{},"Nuxt Content 内置了 TOC 数据，挂在 ",[33,34,35],"code",{},"post.body.toc"," 上。直接用它渲染目录，发现",[38,39,40],"strong",{},"只有 h2 标题，h3 全部丢失","。",[26,43,44],{"id":44},"排查",[15,46,47,48,51],{},"查了官方文档，发现 ",[33,49,50],{},"build.markdown.toc.depth"," 可以控制深度：",[53,54,59],"pre",{"className":55,"code":56,"language":57,"meta":58,"style":58},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F nuxt.config.ts\ncontent: {\n  build: {\n    markdown: {\n      toc: { depth: 3 }  \u002F\u002F 包含 h3\n    }\n  }\n}\n","typescript","",[33,60,61,70,81,89,97,122,128,134],{"__ignoreMap":58},[62,63,66],"span",{"class":64,"line":65},"line",1,[62,67,69],{"class":68},"sHbNN","\u002F\u002F nuxt.config.ts\n",[62,71,73,77],{"class":64,"line":72},2,[62,74,76],{"class":75},"sw2iP","content",[62,78,80],{"class":79},"sIX_F",": {\n",[62,82,84,87],{"class":64,"line":83},3,[62,85,86],{"class":75},"  build",[62,88,80],{"class":79},[62,90,92,95],{"class":64,"line":91},4,[62,93,94],{"class":75},"    markdown",[62,96,80],{"class":79},[62,98,100,103,106,109,112,116,119],{"class":64,"line":99},5,[62,101,102],{"class":75},"      toc",[62,104,105],{"class":79},": { ",[62,107,108],{"class":75},"depth",[62,110,111],{"class":79},": ",[62,113,115],{"class":114},"suQ91","3",[62,117,118],{"class":79}," }  ",[62,120,121],{"class":68},"\u002F\u002F 包含 h3\n",[62,123,125],{"class":64,"line":124},6,[62,126,127],{"class":79},"    }\n",[62,129,131],{"class":64,"line":130},7,[62,132,133],{"class":79},"  }\n",[62,135,137],{"class":64,"line":136},8,[62,138,139],{"class":79},"}\n",[15,141,142,143,146],{},"配置改了，清了缓存重启，",[38,144,145],{},"依然只有 h2","。这个配置似乎不起作用。",[26,148,149],{"id":149},"最终方案",[15,151,152,153,156],{},"放弃依赖 Nuxt Content 的 toc，直接从 ",[33,154,155],{},"post.body"," 的 AST 里递归提取所有标题：",[53,158,160],{"className":55,"code":159,"language":57,"meta":58,"style":58},"function extractHeadings(node: any, list: Heading[] = []): Heading[] {\n  if (!node) return list\n  if (Array.isArray(node)) {\n    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n    if (typeof node[0] === 'string' && \u002F^h[1-6]$\u002F.test(node[0])) {\n      const level = Number(node[0].charAt(1))\n      const text = extractText(node.slice(2)).trim()\n      list.push({ id: node[1]?.id || slugify(text), text, level })\n    }\n    for (const child of node) extractHeadings(child, list)\n  }\n  return list\n}\n",[33,161,162,211,231,244,249,311,343,375,400,405,431,436,444],{"__ignoreMap":58},[62,163,164,168,171,174,178,181,184,187,190,192,195,198,201,204,206,208],{"class":64,"line":65},[62,165,167],{"class":166},"siTax","function",[62,169,170],{"class":75}," extractHeadings",[62,172,173],{"class":79},"(",[62,175,177],{"class":176},"sPK6S","node",[62,179,180],{"class":166},":",[62,182,183],{"class":114}," any",[62,185,186],{"class":79},", ",[62,188,189],{"class":176},"list",[62,191,180],{"class":166},[62,193,194],{"class":75}," Heading",[62,196,197],{"class":79},"[] ",[62,199,200],{"class":166},"=",[62,202,203],{"class":79}," [])",[62,205,180],{"class":166},[62,207,194],{"class":75},[62,209,210],{"class":79},"[] {\n",[62,212,213,216,219,222,225,228],{"class":64,"line":72},[62,214,215],{"class":166},"  if",[62,217,218],{"class":79}," (",[62,220,221],{"class":166},"!",[62,223,224],{"class":79},"node) ",[62,226,227],{"class":166},"return",[62,229,230],{"class":79}," list\n",[62,232,233,235,238,241],{"class":64,"line":83},[62,234,215],{"class":166},[62,236,237],{"class":79}," (Array.",[62,239,240],{"class":75},"isArray",[62,242,243],{"class":79},"(node)) {\n",[62,245,246],{"class":64,"line":91},[62,247,248],{"class":68},"    \u002F\u002F minimark 格式：[\"h2\", {id: \"xxx\"}, \"文本\"]\n",[62,250,251,254,256,259,262,265,268,271,275,278,281,284,288,291,294,297,300,303,306,308],{"class":64,"line":99},[62,252,253],{"class":166},"    if",[62,255,218],{"class":79},[62,257,258],{"class":166},"typeof",[62,260,261],{"class":79}," node[",[62,263,264],{"class":114},"0",[62,266,267],{"class":79},"] ",[62,269,270],{"class":166},"===",[62,272,274],{"class":273},"scXbn"," 'string'",[62,276,277],{"class":166}," &&",[62,279,280],{"class":273}," \u002F",[62,282,283],{"class":166},"^",[62,285,287],{"class":286},"sPKmS","h",[62,289,290],{"class":114},"[1-6]",[62,292,293],{"class":166},"$",[62,295,296],{"class":273},"\u002F",[62,298,299],{"class":79},".",[62,301,302],{"class":75},"test",[62,304,305],{"class":79},"(node[",[62,307,264],{"class":114},[62,309,310],{"class":79},"])) {\n",[62,312,313,316,319,322,325,327,329,332,335,337,340],{"class":64,"line":124},[62,314,315],{"class":166},"      const",[62,317,318],{"class":114}," level",[62,320,321],{"class":166}," =",[62,323,324],{"class":75}," Number",[62,326,305],{"class":79},[62,328,264],{"class":114},[62,330,331],{"class":79},"].",[62,333,334],{"class":75},"charAt",[62,336,173],{"class":79},[62,338,339],{"class":114},"1",[62,341,342],{"class":79},"))\n",[62,344,345,347,350,352,355,358,361,363,366,369,372],{"class":64,"line":130},[62,346,315],{"class":166},[62,348,349],{"class":114}," text",[62,351,321],{"class":166},[62,353,354],{"class":75}," extractText",[62,356,357],{"class":79},"(node.",[62,359,360],{"class":75},"slice",[62,362,173],{"class":79},[62,364,365],{"class":114},"2",[62,367,368],{"class":79},")).",[62,370,371],{"class":75},"trim",[62,373,374],{"class":79},"()\n",[62,376,377,380,383,386,388,391,394,397],{"class":64,"line":136},[62,378,379],{"class":79},"      list.",[62,381,382],{"class":75},"push",[62,384,385],{"class":79},"({ id: node[",[62,387,339],{"class":114},[62,389,390],{"class":79},"]?.id ",[62,392,393],{"class":166},"||",[62,395,396],{"class":75}," slugify",[62,398,399],{"class":79},"(text), text, level })\n",[62,401,403],{"class":64,"line":402},9,[62,404,127],{"class":79},[62,406,408,411,413,416,419,422,425,428],{"class":64,"line":407},10,[62,409,410],{"class":166},"    for",[62,412,218],{"class":79},[62,414,415],{"class":166},"const",[62,417,418],{"class":114}," child",[62,420,421],{"class":166}," of",[62,423,424],{"class":79}," node) ",[62,426,427],{"class":75},"extractHeadings",[62,429,430],{"class":79},"(child, list)\n",[62,432,434],{"class":64,"line":433},11,[62,435,133],{"class":79},[62,437,439,442],{"class":64,"line":438},12,[62,440,441],{"class":166},"  return",[62,443,230],{"class":79},[62,445,447],{"class":64,"line":446},13,[62,448,139],{"class":79},[15,450,451,452,454,455,458],{},"关键点：Nuxt Content v3 的 body 使用 ",[38,453,8],{}," 格式，标题节点是数组 ",[33,456,457],{},"[\"h2\", {id: \"xxx\"}, \"标题文本\"]","，不是对象。",[11,460,462],{"id":461},"问题二图片路径解析错误","问题二：图片路径解析错误",[26,464,28],{"id":465},"现象-1",[15,467,468],{},"markdown 里用相对路径引用图片：",[53,470,474],{"className":471,"code":472,"language":473,"meta":58,"style":58},"language-markdown shiki shiki-themes github-light github-dark","![SSHFS挂载效果](img\u002Fsshfs.avif)\n","markdown",[33,475,476],{"__ignoreMap":58},[62,477,478,481,485,488,492],{"class":64,"line":65},[62,479,480],{"class":79},"![",[62,482,484],{"class":483},"smiIW","SSHFS挂载效果",[62,486,487],{"class":79},"](",[62,489,491],{"class":490},"szbl0","img\u002Fsshfs.avif",[62,493,494],{"class":79},")\n",[15,496,497,498,501,502,505,506,41],{},"HTML 输出的 ",[33,499,500],{},"\u003Cimg src=\"img\u002Fsshfs.avif\">"," 看起来没问题，但浏览器请求的却是 ",[33,503,504],{},"\u002Fposts\u002Fimg\u002Fsshfs.avif"," 而不是 ",[33,507,508],{},"\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif",[26,510,44],{"id":511},"排查-1",[513,514,515,523,526,532],"ol",{},[516,517,518,519,522],"li",{},"检查了 ",[33,520,521],{},"\u003Cbase>"," 标签 —— 没有",[516,524,525],{},"检查了重定向 —— 没有",[516,527,528,529,531],{},"直接访问 ",[33,530,508],{}," —— 返回 200 OK",[516,533,528,534,536],{},[33,535,504],{}," —— 404",[15,538,539,540,41],{},"服务器路由没问题，问题是",[38,541,542],{},"浏览器解析相对路径时基于了错误的基准 URL",[15,544,545],{},"这可能和 Nuxt 的客户端路由有关：SPA 导航时，浏览器的\"当前页面\"和 Vue Router 的\"当前路由\"不同步，导致相对路径解析出错。",[26,547,149],{"id":548},"最终方案-1",[15,550,551],{},"两步解决：",[15,553,554],{},[38,555,556],{},"第一步：自定义 ProseImg 组件",[15,558,559,560,563],{},"创建 ",[33,561,562],{},"components\u002Fcontent\u002FProseImg.vue","，覆盖 Nuxt Content 默认的图片渲染：",[53,565,569],{"className":566,"code":567,"language":568,"meta":58,"style":58},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nconst route = useRoute()\nconst resolvedSrc = computed(() => {\n  if (!props.src) return ''\n  if (props.src.startsWith('\u002F') || props.src.startsWith('http')) return props.src\n  const base = route.path.replace(\u002F\\\u002F$\u002F, '')\n  return `${base}\u002F${props.src}`\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cimg :src=\"resolvedSrc\" :alt=\"alt\" \u002F>\n\u003C\u002Ftemplate>\n","vue",[33,570,571,586,600,621,637,675,710,734,739,748,754,763,790],{"__ignoreMap":58},[62,572,573,576,580,583],{"class":64,"line":65},[62,574,575],{"class":79},"\u003C",[62,577,579],{"class":578},"sbB4o","script",[62,581,582],{"class":75}," setup",[62,584,585],{"class":79},">\n",[62,587,588,590,593,595,598],{"class":64,"line":72},[62,589,415],{"class":166},[62,591,592],{"class":114}," route",[62,594,321],{"class":166},[62,596,597],{"class":75}," useRoute",[62,599,374],{"class":79},[62,601,602,604,607,609,612,615,618],{"class":64,"line":83},[62,603,415],{"class":166},[62,605,606],{"class":114}," resolvedSrc",[62,608,321],{"class":166},[62,610,611],{"class":75}," computed",[62,613,614],{"class":79},"(() ",[62,616,617],{"class":166},"=>",[62,619,620],{"class":79}," {\n",[62,622,623,625,627,629,632,634],{"class":64,"line":91},[62,624,215],{"class":166},[62,626,218],{"class":79},[62,628,221],{"class":166},[62,630,631],{"class":79},"props.src) ",[62,633,227],{"class":166},[62,635,636],{"class":273}," ''\n",[62,638,639,641,644,647,649,652,655,657,660,662,664,667,670,672],{"class":64,"line":99},[62,640,215],{"class":166},[62,642,643],{"class":79}," (props.src.",[62,645,646],{"class":75},"startsWith",[62,648,173],{"class":79},[62,650,651],{"class":273},"'\u002F'",[62,653,654],{"class":79},") ",[62,656,393],{"class":166},[62,658,659],{"class":79}," props.src.",[62,661,646],{"class":75},[62,663,173],{"class":79},[62,665,666],{"class":273},"'http'",[62,668,669],{"class":79},")) ",[62,671,227],{"class":166},[62,673,674],{"class":79}," props.src\n",[62,676,677,680,683,685,688,691,693,695,699,701,703,705,708],{"class":64,"line":124},[62,678,679],{"class":166},"  const",[62,681,682],{"class":114}," base",[62,684,321],{"class":166},[62,686,687],{"class":79}," route.path.",[62,689,690],{"class":75},"replace",[62,692,173],{"class":79},[62,694,296],{"class":273},[62,696,698],{"class":697},"sCnZR","\\\u002F",[62,700,293],{"class":166},[62,702,296],{"class":273},[62,704,186],{"class":79},[62,706,707],{"class":273},"''",[62,709,494],{"class":79},[62,711,712,714,717,720,723,726,728,731],{"class":64,"line":130},[62,713,441],{"class":166},[62,715,716],{"class":273}," `${",[62,718,719],{"class":79},"base",[62,721,722],{"class":273},"}\u002F${",[62,724,725],{"class":79},"props",[62,727,299],{"class":273},[62,729,730],{"class":79},"src",[62,732,733],{"class":273},"}`\n",[62,735,736],{"class":64,"line":136},[62,737,738],{"class":79},"})\n",[62,740,741,744,746],{"class":64,"line":402},[62,742,743],{"class":79},"\u003C\u002F",[62,745,579],{"class":578},[62,747,585],{"class":79},[62,749,750],{"class":64,"line":407},[62,751,753],{"emptyLinePlaceholder":752},true,"\n",[62,755,756,758,761],{"class":64,"line":433},[62,757,575],{"class":79},[62,759,760],{"class":578},"template",[62,762,585],{"class":79},[62,764,765,768,771,774,776,779,782,784,787],{"class":64,"line":438},[62,766,767],{"class":79},"  \u003C",[62,769,770],{"class":578},"img",[62,772,773],{"class":75}," :src",[62,775,200],{"class":79},[62,777,778],{"class":273},"\"resolvedSrc\"",[62,780,781],{"class":75}," :alt",[62,783,200],{"class":79},[62,785,786],{"class":273},"\"alt\"",[62,788,789],{"class":79}," \u002F>\n",[62,791,792,794,796],{"class":64,"line":446},[62,793,743],{"class":79},[62,795,760],{"class":578},[62,797,585],{"class":79},[15,799,800],{},[38,801,802],{},"第二步：启用 MDC prose 组件",[53,804,806],{"className":55,"code":805,"language":57,"meta":58,"style":58},"\u002F\u002F nuxt.config.ts\nmdc: {\n  components: {\n    prose: true  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n  }\n}\n",[33,807,808,812,819,826,839,843],{"__ignoreMap":58},[62,809,810],{"class":64,"line":65},[62,811,69],{"class":68},[62,813,814,817],{"class":64,"line":72},[62,815,816],{"class":75},"mdc",[62,818,80],{"class":79},[62,820,821,824],{"class":64,"line":83},[62,822,823],{"class":75},"  components",[62,825,80],{"class":79},[62,827,828,831,833,836],{"class":64,"line":91},[62,829,830],{"class":75},"    prose",[62,832,111],{"class":79},[62,834,835],{"class":114},"true",[62,837,838],{"class":68},"  \u002F\u002F 让 Nuxt Content 使用自定义 ProseImg\n",[62,840,841],{"class":64,"line":99},[62,842,133],{"class":79},[62,844,845],{"class":64,"line":124},[62,846,139],{"class":79},[11,848,850],{"id":849},"问题三content-目录的图片如何服务","问题三：content 目录的图片如何服务",[26,852,28],{"id":853},"现象-2",[15,855,856,857,860],{},"原站（SvelteKit）的图片放在 ",[33,858,859],{},"content\u002Fposts\u002F{slug}\u002Fimg\u002F"," 下，markdown 用相对路径引用，一切正常。Nuxt 项目里，content 目录的文件不会被当成静态资源服务。",[26,862,863],{"id":863},"方案对比",[865,866,867,883],"table",{},[868,869,870],"thead",{},[871,872,873,877,880],"tr",{},[874,875,876],"th",{},"方案",[874,878,879],{},"优点",[874,881,882],{},"缺点",[884,885,886,898,909],"tbody",{},[871,887,888,892,895],{},[889,890,891],"td",{},"复制到 public\u002F",[889,893,894],{},"简单",[889,896,897],{},"文件重复，需同步",[871,899,900,903,906],{},[889,901,902],{},"Vite 插件拦截",[889,904,905],{},"原站方案",[889,907,908],{},"Nuxt 不直接支持",[871,910,911,914,917],{},[889,912,913],{},"Server Route",[889,915,916],{},"不复制文件",[889,918,919],{},"需要手动写路由",[15,921,922,923,925],{},"最终选择 ",[38,924,913],{},"，和原站的 Vite 插件思路一致：",[53,927,929],{"className":55,"code":928,"language":57,"meta":58,"style":58},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\nexport default defineEventHandler(async (event) => {\n  const url = getRequestURL(event)\n  const match = url.pathname.match(\u002F^\\\u002Fposts\\\u002F([^\u002F]+)\\\u002Fimg\\\u002F(.+)$\u002F)\n  if (!match) return\n\n  const [, slug, filename] = match\n  const filePath = resolve('content\u002Fposts', slug, 'img', filename)\n  const data = await readFile(filePath)\n  setResponseHeader(event, 'content-type', MIME[ext])\n  return data\n})\n",[33,930,931,936,963,978,1042,1056,1060,1082,1108,1126,1145,1152],{"__ignoreMap":58},[62,932,933],{"class":64,"line":65},[62,934,935],{"class":68},"\u002F\u002F server\u002Froutes\u002Fposts\u002F[slug]\u002Fimg\u002F[...file].ts\n",[62,937,938,941,944,947,949,952,954,957,959,961],{"class":64,"line":72},[62,939,940],{"class":166},"export",[62,942,943],{"class":166}," default",[62,945,946],{"class":75}," defineEventHandler",[62,948,173],{"class":79},[62,950,951],{"class":166},"async",[62,953,218],{"class":79},[62,955,956],{"class":176},"event",[62,958,654],{"class":79},[62,960,617],{"class":166},[62,962,620],{"class":79},[62,964,965,967,970,972,975],{"class":64,"line":83},[62,966,679],{"class":166},[62,968,969],{"class":114}," url",[62,971,321],{"class":166},[62,973,974],{"class":75}," getRequestURL",[62,976,977],{"class":79},"(event)\n",[62,979,980,982,985,987,990,993,995,997,999,1001,1004,1006,1008,1011,1013,1016,1019,1022,1024,1026,1028,1030,1032,1034,1036,1038,1040],{"class":64,"line":91},[62,981,679],{"class":166},[62,983,984],{"class":114}," match",[62,986,321],{"class":166},[62,988,989],{"class":79}," url.pathname.",[62,991,992],{"class":75},"match",[62,994,173],{"class":79},[62,996,296],{"class":273},[62,998,283],{"class":166},[62,1000,698],{"class":697},[62,1002,1003],{"class":286},"posts",[62,1005,698],{"class":697},[62,1007,173],{"class":286},[62,1009,1010],{"class":114},"[",[62,1012,283],{"class":166},[62,1014,1015],{"class":114},"\u002F]",[62,1017,1018],{"class":166},"+",[62,1020,1021],{"class":286},")",[62,1023,698],{"class":697},[62,1025,770],{"class":286},[62,1027,698],{"class":697},[62,1029,173],{"class":286},[62,1031,299],{"class":114},[62,1033,1018],{"class":166},[62,1035,1021],{"class":286},[62,1037,293],{"class":166},[62,1039,296],{"class":273},[62,1041,494],{"class":79},[62,1043,1044,1046,1048,1050,1053],{"class":64,"line":99},[62,1045,215],{"class":166},[62,1047,218],{"class":79},[62,1049,221],{"class":166},[62,1051,1052],{"class":79},"match) ",[62,1054,1055],{"class":166},"return\n",[62,1057,1058],{"class":64,"line":124},[62,1059,753],{"emptyLinePlaceholder":752},[62,1061,1062,1064,1067,1070,1072,1075,1077,1079],{"class":64,"line":130},[62,1063,679],{"class":166},[62,1065,1066],{"class":79}," [, ",[62,1068,1069],{"class":114},"slug",[62,1071,186],{"class":79},[62,1073,1074],{"class":114},"filename",[62,1076,267],{"class":79},[62,1078,200],{"class":166},[62,1080,1081],{"class":79}," match\n",[62,1083,1084,1086,1089,1091,1094,1096,1099,1102,1105],{"class":64,"line":136},[62,1085,679],{"class":166},[62,1087,1088],{"class":114}," filePath",[62,1090,321],{"class":166},[62,1092,1093],{"class":75}," resolve",[62,1095,173],{"class":79},[62,1097,1098],{"class":273},"'content\u002Fposts'",[62,1100,1101],{"class":79},", slug, ",[62,1103,1104],{"class":273},"'img'",[62,1106,1107],{"class":79},", filename)\n",[62,1109,1110,1112,1115,1117,1120,1123],{"class":64,"line":402},[62,1111,679],{"class":166},[62,1113,1114],{"class":114}," data",[62,1116,321],{"class":166},[62,1118,1119],{"class":166}," await",[62,1121,1122],{"class":75}," readFile",[62,1124,1125],{"class":79},"(filePath)\n",[62,1127,1128,1131,1134,1137,1139,1142],{"class":64,"line":407},[62,1129,1130],{"class":75},"  setResponseHeader",[62,1132,1133],{"class":79},"(event, ",[62,1135,1136],{"class":273},"'content-type'",[62,1138,186],{"class":79},[62,1140,1141],{"class":114},"MIME",[62,1143,1144],{"class":79},"[ext])\n",[62,1146,1147,1149],{"class":64,"line":433},[62,1148,441],{"class":166},[62,1150,1151],{"class":79}," data\n",[62,1153,1154],{"class":64,"line":438},[62,1155,738],{"class":79},[15,1157,1158,1159,1161,1162,1165],{},"请求 ",[33,1160,508],{}," → 读取 ",[33,1163,1164],{},"content\u002Fposts\u002Fsshfs\u002Fimg\u002Fsshfs.avif"," → 返回图片。",[11,1167,1168],{"id":1168},"总结",[865,1170,1171,1183],{},[868,1172,1173],{},[871,1174,1175,1178,1181],{},[874,1176,1177],{},"问题",[874,1179,1180],{},"根因",[874,1182,876],{},[884,1184,1185,1199,1210],{},[871,1186,1187,1190,1196],{},[889,1188,1189],{},"TOC 只有 h2",[889,1191,1192,1195],{},[33,1193,1194],{},"toc.depth"," 配置不生效",[889,1197,1198],{},"从 body AST 手动提取",[871,1200,1201,1204,1207],{},[889,1202,1203],{},"图片路径错误",[889,1205,1206],{},"SPA 路由导致相对路径解析基准错误",[889,1208,1209],{},"ProseImg 组件 + mdc.prose 配置",[871,1211,1212,1215,1218],{},[889,1213,1214],{},"content 图片 404",[889,1216,1217],{},"content 目录不暴露给浏览器",[889,1219,1220],{},"Server Route 直接读取返回",[15,1222,1223],{},"Nuxt Content v3 的设计理念是\"图片放 public\u002F，用绝对路径\"。如果想像 SvelteKit 项目那样把图片和文章放一起，需要额外做不少工作。",[15,1225,1226],{},"不过一旦这些坑都踩过，整体开发体验还是很流畅的。Nuxt 的生态系统、自动导入、模块系统都比 SvelteKit 成熟很多。",[1228,1229,1230],"style",{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sPKmS, html code.shiki .sPKmS{--shiki-light:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .sCnZR, html code.shiki .sCnZR{--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .smiIW, html code.shiki .smiIW{--shiki-light:#032F62;--shiki-light-text-decoration:underline;--shiki-dark:#DBEDFF;--shiki-dark-text-decoration:underline}html pre.shiki code .szbl0, html code.shiki .szbl0{--shiki-light:#24292E;--shiki-light-text-decoration:underline;--shiki-dark:#E1E4E8;--shiki-dark-text-decoration:underline}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":58,"searchDepth":72,"depth":83,"links":1232},[1233,1234,1235,1236,1237,1238,1239,1240],{"id":28,"depth":72,"text":28},{"id":44,"depth":72,"text":44},{"id":149,"depth":72,"text":149},{"id":465,"depth":72,"text":28},{"id":511,"depth":72,"text":44},{"id":548,"depth":72,"text":149},{"id":853,"depth":72,"text":28},{"id":863,"depth":72,"text":863},"从 SvelteKit 迁移到 Nuxt 3 的过程中，遇到了不少 Nuxt Content v3 的坑。这篇文章记录了目录导航、图片路径、ProseImg 组件等问题的排查和解决过程。","进阶",false,"md","\u002Fposts\u002Fnuxt-content-lessons\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fnuxt-content-lessons","2026-05-31T14:00:00",{"title":5,"description":1241},"Nuxt 博客开发","posts\u002Fnuxt-content-lessons\u002Findex",[1253,1254,1255],"Nuxt","踩坑","Nuxt Content","2026-05-31","vbSt6ND1z7xt7L8FxgZ8Qe3EdxV9InsRerN3E23GC7s",[1259,2067,2485,2704,3116,4022,4301,6370,7152,8131,9540,10100,11279,12815],{"id":1260,"title":1261,"author":6,"body":1262,"description":2054,"difficulty":1242,"draft":1243,"extension":1244,"image":2055,"meta":2056,"navigation":752,"path":2057,"pinned":1243,"published":2058,"seo":2059,"series":2060,"seriesOrder":65,"stem":2061,"tags":2062,"updated":2058,"__hash__":2066},"posts\u002Fposts\u002Fdocker-musl-build\u002Findex.md","Docker 编译 musl 可执行文件",{"type":8,"value":1263,"toc":2043},[1264,1267,1270,1273,1502,1506,1547,1549,1567,1570,1721,1724,1859,1862,1865,1923,1926,2040],[26,1265,1266],{"id":1266},"解决问题",[15,1268,1269],{},"用 Go 进行跨平台编译时往往步骤复杂，用 Docker 可以轻松解决。但 Docker 的当前最小镜像 Alpine 其动态链接库采用 musl，与主流的 glibc 不同，往往不能方便执行。",[26,1271,1272],{"id":1272},"查阅资料",[53,1274,1278],{"className":1275,"code":1276,"language":1277,"meta":58,"style":58},"language-bash shiki shiki-themes github-light github-dark","# 启动容器并挂载当前目录到 `\u002Fapp`\ndocker run -it --rm -v \"$PWD\":\u002Fapp -w \u002Fapp ubuntu:20.04 bash\n\n# 在容器内执行以下操作：\napt-get update && apt-get install -y musl-tools golang-go  # 安装 musl 和 Go\nexport CC=musl-gcc  # 指定 musl 编译器\nexport CGO_ENABLED=1  # 启用 CGO\n# 设置代理\ngo env -w GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\ngo build -ldflags '-linkmode external -extldflags \"-static\"' -o myapp  # 静态编译\nexit  # 退出容器\n\n# 验证结果（在宿主机执行）\nfile myapp  # 应显示 \"statically linked\"\nldd myapp   # 应显示 \"not a dynamic executable\"\n\nwget https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n\nENV PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\"\n","bash",[33,1279,1280,1285,1323,1327,1332,1360,1375,1389,1394,1407,1429,1437,1441,1446,1457,1468,1473,1482,1487],{"__ignoreMap":58},[62,1281,1282],{"class":64,"line":65},[62,1283,1284],{"class":68},"# 启动容器并挂载当前目录到 `\u002Fapp`\n",[62,1286,1287,1290,1293,1296,1299,1302,1305,1308,1311,1314,1317,1320],{"class":64,"line":72},[62,1288,1289],{"class":75},"docker",[62,1291,1292],{"class":273}," run",[62,1294,1295],{"class":114}," -it",[62,1297,1298],{"class":114}," --rm",[62,1300,1301],{"class":114}," -v",[62,1303,1304],{"class":273}," \"",[62,1306,1307],{"class":79},"$PWD",[62,1309,1310],{"class":273},"\":\u002Fapp",[62,1312,1313],{"class":114}," -w",[62,1315,1316],{"class":273}," \u002Fapp",[62,1318,1319],{"class":273}," ubuntu:20.04",[62,1321,1322],{"class":273}," bash\n",[62,1324,1325],{"class":64,"line":83},[62,1326,753],{"emptyLinePlaceholder":752},[62,1328,1329],{"class":64,"line":91},[62,1330,1331],{"class":68},"# 在容器内执行以下操作：\n",[62,1333,1334,1337,1340,1343,1345,1348,1351,1354,1357],{"class":64,"line":99},[62,1335,1336],{"class":75},"apt-get",[62,1338,1339],{"class":273}," update",[62,1341,1342],{"class":79}," && ",[62,1344,1336],{"class":75},[62,1346,1347],{"class":273}," install",[62,1349,1350],{"class":114}," -y",[62,1352,1353],{"class":273}," musl-tools",[62,1355,1356],{"class":273}," golang-go",[62,1358,1359],{"class":68},"  # 安装 musl 和 Go\n",[62,1361,1362,1364,1367,1369,1372],{"class":64,"line":124},[62,1363,940],{"class":166},[62,1365,1366],{"class":79}," CC",[62,1368,200],{"class":166},[62,1370,1371],{"class":79},"musl-gcc  ",[62,1373,1374],{"class":68},"# 指定 musl 编译器\n",[62,1376,1377,1379,1382,1384,1386],{"class":64,"line":130},[62,1378,940],{"class":166},[62,1380,1381],{"class":79}," CGO_ENABLED",[62,1383,200],{"class":166},[62,1385,339],{"class":114},[62,1387,1388],{"class":68},"  # 启用 CGO\n",[62,1390,1391],{"class":64,"line":136},[62,1392,1393],{"class":68},"# 设置代理\n",[62,1395,1396,1399,1402,1404],{"class":64,"line":402},[62,1397,1398],{"class":75},"go",[62,1400,1401],{"class":273}," env",[62,1403,1313],{"class":114},[62,1405,1406],{"class":273}," GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\n",[62,1408,1409,1411,1414,1417,1420,1423,1426],{"class":64,"line":407},[62,1410,1398],{"class":75},[62,1412,1413],{"class":273}," build",[62,1415,1416],{"class":114}," -ldflags",[62,1418,1419],{"class":273}," '-linkmode external -extldflags \"-static\"'",[62,1421,1422],{"class":114}," -o",[62,1424,1425],{"class":273}," myapp",[62,1427,1428],{"class":68},"  # 静态编译\n",[62,1430,1431,1434],{"class":64,"line":433},[62,1432,1433],{"class":114},"exit",[62,1435,1436],{"class":68},"  # 退出容器\n",[62,1438,1439],{"class":64,"line":438},[62,1440,753],{"emptyLinePlaceholder":752},[62,1442,1443],{"class":64,"line":446},[62,1444,1445],{"class":68},"# 验证结果（在宿主机执行）\n",[62,1447,1449,1452,1454],{"class":64,"line":1448},14,[62,1450,1451],{"class":75},"file",[62,1453,1425],{"class":273},[62,1455,1456],{"class":68},"  # 应显示 \"statically linked\"\n",[62,1458,1460,1463,1465],{"class":64,"line":1459},15,[62,1461,1462],{"class":75},"ldd",[62,1464,1425],{"class":273},[62,1466,1467],{"class":68},"   # 应显示 \"not a dynamic executable\"\n",[62,1469,1471],{"class":64,"line":1470},16,[62,1472,753],{"emptyLinePlaceholder":752},[62,1474,1476,1479],{"class":64,"line":1475},17,[62,1477,1478],{"class":75},"wget",[62,1480,1481],{"class":273}," https:\u002F\u002Fgo.dev\u002Fdl\u002Fgo1.21.linux-amd64.tar.gz\n",[62,1483,1485],{"class":64,"line":1484},18,[62,1486,753],{"emptyLinePlaceholder":752},[62,1488,1490,1493,1496,1499],{"class":64,"line":1489},19,[62,1491,1492],{"class":75},"ENV",[62,1494,1495],{"class":273}," PATH=\"\u002Fdata\u002Fgo\u002Fbin:",[62,1497,1498],{"class":79},"$PATH",[62,1500,1501],{"class":273},"\"\n",[1503,1504,1505],"h3",{"id":1505},"遇到问题",[513,1507,1508,1533],{},[516,1509,1510,1513,1514,1517,1520,1521,1524,1526,1527,1529],{},[33,1511,1512],{},"go mod tidy"," 时报错 ",[33,1515,1516],{},"tls: failed to verify certificate: x509: certificate",[1518,1519],"br",{},"解决：",[33,1522,1523],{},"RUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*",[1518,1525],{},"基础镜像的证书未更新，导致 https 的请求出现类似错误。",[1518,1528],{},[770,1530],{"alt":1531,"src":1532},"image-20250327100813512","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fimage-20250327100813512.png",[516,1534,1535,1536,1539,1540,1542,1543,1546],{},"直接安装 ",[33,1537,1538],{},"golang-go"," 其版本过低",[1518,1541],{},"解决: ",[33,1544,1545],{},"PATH=\"\u002Fdata\u002Fgo\u002Fbin:$PATH\""," 手动指定环境",[26,1548,149],{"id":149},[513,1550,1551,1558,1561],{},[516,1552,1553,1554,1557],{},"下载 ",[33,1555,1556],{},"go1.24.1.linux-amd64.tar.gz"," 解压",[516,1559,1560],{},"编写 Dockerfile",[516,1562,1563,1564],{},"执行 ",[33,1565,1566],{},"docker build -t go-build:v1 .",[15,1568,1569],{},"Dockerfile 内容：",[53,1571,1575],{"className":1572,"code":1573,"language":1574,"meta":58,"style":58},"language-dockerfile shiki shiki-themes github-light github-dark","FROM ubuntu:20.04\n\nLABEL org.opencontainers.image.authors=\"sutong\"\n\nCOPY go\u002F \u002Fgo\u002F\n\nENV PATH=\"\u002Fgo\u002Fbin:$PATH\"\n\nRUN apt-get update && apt-get install -y musl-tools\n\n## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\nRUN apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n# 指定 musl 编译器\nENV CC=musl-gcc\n# 启用 CGO\nENV CGO_ENABLED=1\n\nVOLUME \u002Fdata\n\nWORKDIR \u002Fdata\n\nCMD [\"\u002Fbin\u002Fbash\"]\n","dockerfile",[33,1576,1577,1585,1589,1600,1604,1612,1616,1626,1630,1638,1642,1647,1654,1658,1665,1670,1677,1681,1689,1693,1701,1706],{"__ignoreMap":58},[62,1578,1579,1582],{"class":64,"line":65},[62,1580,1581],{"class":166},"FROM",[62,1583,1584],{"class":79}," ubuntu:20.04\n",[62,1586,1587],{"class":64,"line":72},[62,1588,753],{"emptyLinePlaceholder":752},[62,1590,1591,1594,1597],{"class":64,"line":83},[62,1592,1593],{"class":166},"LABEL",[62,1595,1596],{"class":79}," org.opencontainers.image.authors=",[62,1598,1599],{"class":273},"\"sutong\"\n",[62,1601,1602],{"class":64,"line":91},[62,1603,753],{"emptyLinePlaceholder":752},[62,1605,1606,1609],{"class":64,"line":99},[62,1607,1608],{"class":166},"COPY",[62,1610,1611],{"class":79}," go\u002F \u002Fgo\u002F\n",[62,1613,1614],{"class":64,"line":124},[62,1615,753],{"emptyLinePlaceholder":752},[62,1617,1618,1620,1623],{"class":64,"line":130},[62,1619,1492],{"class":166},[62,1621,1622],{"class":79}," PATH=",[62,1624,1625],{"class":273},"\"\u002Fgo\u002Fbin:$PATH\"\n",[62,1627,1628],{"class":64,"line":136},[62,1629,753],{"emptyLinePlaceholder":752},[62,1631,1632,1635],{"class":64,"line":402},[62,1633,1634],{"class":166},"RUN",[62,1636,1637],{"class":79}," apt-get update && apt-get install -y musl-tools\n",[62,1639,1640],{"class":64,"line":407},[62,1641,753],{"emptyLinePlaceholder":752},[62,1643,1644],{"class":64,"line":433},[62,1645,1646],{"class":68},"## go mod tidy 时报错 tls: failed to verify certificate: x509: certificate\n",[62,1648,1649,1651],{"class":64,"line":438},[62,1650,1634],{"class":166},[62,1652,1653],{"class":79}," apt-get update && apt-get install -y ca-certificates && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[62,1655,1656],{"class":64,"line":446},[62,1657,1374],{"class":68},[62,1659,1660,1662],{"class":64,"line":1448},[62,1661,1492],{"class":166},[62,1663,1664],{"class":79}," CC=musl-gcc\n",[62,1666,1667],{"class":64,"line":1459},[62,1668,1669],{"class":68},"# 启用 CGO\n",[62,1671,1672,1674],{"class":64,"line":1470},[62,1673,1492],{"class":166},[62,1675,1676],{"class":79}," CGO_ENABLED=1\n",[62,1678,1679],{"class":64,"line":1475},[62,1680,753],{"emptyLinePlaceholder":752},[62,1682,1683,1686],{"class":64,"line":1484},[62,1684,1685],{"class":166},"VOLUME",[62,1687,1688],{"class":79}," \u002Fdata\n",[62,1690,1691],{"class":64,"line":1489},[62,1692,753],{"emptyLinePlaceholder":752},[62,1694,1696,1699],{"class":64,"line":1695},20,[62,1697,1698],{"class":166},"WORKDIR",[62,1700,1688],{"class":79},[62,1702,1704],{"class":64,"line":1703},21,[62,1705,753],{"emptyLinePlaceholder":752},[62,1707,1709,1712,1715,1718],{"class":64,"line":1708},22,[62,1710,1711],{"class":166},"CMD",[62,1713,1714],{"class":79}," [",[62,1716,1717],{"class":273},"\"\u002Fbin\u002Fbash\"",[62,1719,1720],{"class":79},"]\n",[1503,1722,1723],{"id":1723},"构建",[53,1725,1727],{"className":1275,"code":1726,"language":1277,"meta":58,"style":58},"# docker build -t go-build:v1 .\n\n# 直接执行编译 编译后删除\ndocker run --rm -v .:\u002Fdata go-build:v1 go build -o app_musl\n\n# 构建编译环境\ndocker run -it -d -v .:\u002Fdata --name \u003C容器名称> go-build:v1 \u002Fbin\u002Fbash\n# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n\ndocker exec -it \u003C容器名称> go build -o \u003Cmyapp>\n# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[33,1728,1729,1734,1738,1743,1769,1773,1778,1813,1818,1822,1854],{"__ignoreMap":58},[62,1730,1731],{"class":64,"line":65},[62,1732,1733],{"class":68},"# docker build -t go-build:v1 .\n",[62,1735,1736],{"class":64,"line":72},[62,1737,753],{"emptyLinePlaceholder":752},[62,1739,1740],{"class":64,"line":83},[62,1741,1742],{"class":68},"# 直接执行编译 编译后删除\n",[62,1744,1745,1747,1749,1751,1753,1756,1759,1762,1764,1766],{"class":64,"line":91},[62,1746,1289],{"class":75},[62,1748,1292],{"class":273},[62,1750,1298],{"class":114},[62,1752,1301],{"class":114},[62,1754,1755],{"class":273}," .:\u002Fdata",[62,1757,1758],{"class":273}," go-build:v1",[62,1760,1761],{"class":273}," go",[62,1763,1413],{"class":273},[62,1765,1422],{"class":114},[62,1767,1768],{"class":273}," app_musl\n",[62,1770,1771],{"class":64,"line":99},[62,1772,753],{"emptyLinePlaceholder":752},[62,1774,1775],{"class":64,"line":124},[62,1776,1777],{"class":68},"# 构建编译环境\n",[62,1779,1780,1782,1784,1786,1789,1791,1793,1796,1799,1802,1805,1808,1810],{"class":64,"line":130},[62,1781,1289],{"class":75},[62,1783,1292],{"class":273},[62,1785,1295],{"class":114},[62,1787,1788],{"class":114}," -d",[62,1790,1301],{"class":114},[62,1792,1755],{"class":273},[62,1794,1795],{"class":114}," --name",[62,1797,1798],{"class":166}," \u003C",[62,1800,1801],{"class":273},"容器名",[62,1803,1804],{"class":79},"称",[62,1806,1807],{"class":166},">",[62,1809,1758],{"class":273},[62,1811,1812],{"class":273}," \u002Fbin\u002Fbash\n",[62,1814,1815],{"class":64,"line":136},[62,1816,1817],{"class":68},"# eg：docker run -it -d -v .:\u002Fdata --name logbus-build go-build:v1 \u002Fbin\u002Fbash\n",[62,1819,1820],{"class":64,"line":402},[62,1821,753],{"emptyLinePlaceholder":752},[62,1823,1824,1826,1829,1831,1833,1835,1837,1839,1841,1843,1845,1847,1850,1852],{"class":64,"line":407},[62,1825,1289],{"class":75},[62,1827,1828],{"class":273}," exec",[62,1830,1295],{"class":114},[62,1832,1798],{"class":166},[62,1834,1801],{"class":273},[62,1836,1804],{"class":79},[62,1838,1807],{"class":166},[62,1840,1761],{"class":273},[62,1842,1413],{"class":273},[62,1844,1422],{"class":114},[62,1846,1798],{"class":166},[62,1848,1849],{"class":273},"myap",[62,1851,15],{"class":79},[62,1853,585],{"class":166},[62,1855,1856],{"class":64,"line":433},[62,1857,1858],{"class":68},"# eg：docker exec -it logbus-build go build -o logbus_musl_1\n",[1503,1860,1861],{"id":1861},"额外注意",[15,1863,1864],{},"若没科学上网需要设置代理：",[53,1866,1868],{"className":1275,"code":1867,"language":1277,"meta":58,"style":58},"docker exec -it \u003C容器名称> \u002Fbin\u002Fbash\ngo env -w GOPROXY=https:\u002F\u002Fgoproxy.cn,direct\ngo build -ldflags '-linkmode external -extldflags \"-static\"' -o myapp  # 静态编译\ngo build # musl 库编译\n",[33,1869,1870,1888,1898,1914],{"__ignoreMap":58},[62,1871,1872,1874,1876,1878,1880,1882,1884,1886],{"class":64,"line":65},[62,1873,1289],{"class":75},[62,1875,1828],{"class":273},[62,1877,1295],{"class":114},[62,1879,1798],{"class":166},[62,1881,1801],{"class":273},[62,1883,1804],{"class":79},[62,1885,1807],{"class":166},[62,1887,1812],{"class":273},[62,1889,1890,1892,1894,1896],{"class":64,"line":72},[62,1891,1398],{"class":75},[62,1893,1401],{"class":273},[62,1895,1313],{"class":114},[62,1897,1406],{"class":273},[62,1899,1900,1902,1904,1906,1908,1910,1912],{"class":64,"line":83},[62,1901,1398],{"class":75},[62,1903,1413],{"class":273},[62,1905,1416],{"class":114},[62,1907,1419],{"class":273},[62,1909,1422],{"class":114},[62,1911,1425],{"class":273},[62,1913,1428],{"class":68},[62,1915,1916,1918,1920],{"class":64,"line":91},[62,1917,1398],{"class":75},[62,1919,1413],{"class":273},[62,1921,1922],{"class":68}," # musl 库编译\n",[1503,1924,1925],{"id":1925},"运行环境示例",[53,1927,1929],{"className":1572,"code":1928,"language":1574,"meta":58,"style":58},"FROM alpine:latest\n\nLABEL org.opencontainers.image.authors=\"sutong\"\n\nCOPY glog_musl \u002Fapp\u002F\n\nCOPY conf.ini \u002Fdata\u002F\nCOPY ipname.ini \u002Fdata\u002F\n\nWORKDIR \u002Fapp\n\nRUN chmod 777 \u002Fapp\u002Fglog_musl\n\nCMD [\".\u002Fglog_musl\", \"logsweb\", \"\u002Fdata\u002Fconf.ini\"]\n\n# docker build -t glog:v1 .\n# docker run -p 6801:6801 glog:v1\n",[33,1930,1931,1938,1942,1950,1954,1961,1965,1972,1979,1983,1990,1994,2001,2005,2026,2030,2035],{"__ignoreMap":58},[62,1932,1933,1935],{"class":64,"line":65},[62,1934,1581],{"class":166},[62,1936,1937],{"class":79}," alpine:latest\n",[62,1939,1940],{"class":64,"line":72},[62,1941,753],{"emptyLinePlaceholder":752},[62,1943,1944,1946,1948],{"class":64,"line":83},[62,1945,1593],{"class":166},[62,1947,1596],{"class":79},[62,1949,1599],{"class":273},[62,1951,1952],{"class":64,"line":91},[62,1953,753],{"emptyLinePlaceholder":752},[62,1955,1956,1958],{"class":64,"line":99},[62,1957,1608],{"class":166},[62,1959,1960],{"class":79}," glog_musl \u002Fapp\u002F\n",[62,1962,1963],{"class":64,"line":124},[62,1964,753],{"emptyLinePlaceholder":752},[62,1966,1967,1969],{"class":64,"line":130},[62,1968,1608],{"class":166},[62,1970,1971],{"class":79}," conf.ini \u002Fdata\u002F\n",[62,1973,1974,1976],{"class":64,"line":136},[62,1975,1608],{"class":166},[62,1977,1978],{"class":79}," ipname.ini \u002Fdata\u002F\n",[62,1980,1981],{"class":64,"line":402},[62,1982,753],{"emptyLinePlaceholder":752},[62,1984,1985,1987],{"class":64,"line":407},[62,1986,1698],{"class":166},[62,1988,1989],{"class":79}," \u002Fapp\n",[62,1991,1992],{"class":64,"line":433},[62,1993,753],{"emptyLinePlaceholder":752},[62,1995,1996,1998],{"class":64,"line":438},[62,1997,1634],{"class":166},[62,1999,2000],{"class":79}," chmod 777 \u002Fapp\u002Fglog_musl\n",[62,2002,2003],{"class":64,"line":446},[62,2004,753],{"emptyLinePlaceholder":752},[62,2006,2007,2009,2011,2014,2016,2019,2021,2024],{"class":64,"line":1448},[62,2008,1711],{"class":166},[62,2010,1714],{"class":79},[62,2012,2013],{"class":273},"\".\u002Fglog_musl\"",[62,2015,186],{"class":79},[62,2017,2018],{"class":273},"\"logsweb\"",[62,2020,186],{"class":79},[62,2022,2023],{"class":273},"\"\u002Fdata\u002Fconf.ini\"",[62,2025,1720],{"class":79},[62,2027,2028],{"class":64,"line":1459},[62,2029,753],{"emptyLinePlaceholder":752},[62,2031,2032],{"class":64,"line":1470},[62,2033,2034],{"class":68},"# docker build -t glog:v1 .\n",[62,2036,2037],{"class":64,"line":1475},[62,2038,2039],{"class":68},"# docker run -p 6801:6801 glog:v1\n",[1228,2041,2042],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":58,"searchDepth":72,"depth":83,"links":2044},[2045,2046,2049],{"id":1266,"depth":72,"text":1266},{"id":1272,"depth":72,"text":1272,"children":2047},[2048],{"id":1505,"depth":83,"text":1505},{"id":149,"depth":72,"text":149,"children":2050},[2051,2052,2053],{"id":1723,"depth":83,"text":1723},{"id":1861,"depth":83,"text":1861},{"id":1925,"depth":83,"text":1925},"解决 Go 语言跨平台编译问题，使用 Docker + musl 编译静态链接的可执行文件。","\u002Fposts\u002Fdocker-musl-build\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-musl-build","2025-03-27",{"title":1261,"description":2054},"Docker 实践笔记","posts\u002Fdocker-musl-build\u002Findex",[2063,2064,2065],"Docker","Go","运维","YLtdOOc-gMVDSNjE-ntKXz-7hff-VtvMB2dB5RXNi1o",{"id":2068,"title":2069,"author":6,"body":2070,"description":2474,"difficulty":2475,"draft":1243,"extension":1244,"image":2476,"meta":2477,"navigation":752,"path":2478,"pinned":1243,"published":2479,"seo":2480,"series":2060,"seriesOrder":72,"stem":2481,"tags":2482,"updated":2479,"__hash__":2484},"posts\u002Fposts\u002Fdocker-user-start\u002Findex.md","Docker 构建时指定用户启动",{"type":8,"value":2071,"toc":2464},[2072,2075,2078,2081,2085,2088,2105,2109,2142,2146,2153,2201,2204,2423,2425,2428,2458,2461],[26,2073,2074],{"id":2074},"前言",[15,2076,2077],{},"我们在 Linux 使用应用时，往往不会直接使用 root 用户作为应用启动项，但 Dockerfile 默认就是 root 用户，导致挂载生成的文件路径往往是 root 权限，非 root 用户访问就很不方便。",[26,2079,2080],{"id":2080},"解决方案",[1503,2082,2084],{"id":2083},"_1-安装用户管理工具","1. 安装用户管理工具",[15,2086,2087],{},"若镜像非自带用户管理：",[53,2089,2091],{"className":1572,"code":2090,"language":1574,"meta":58,"style":58},"# 权限管理\nRUN apk add --no-cache shadow\n",[33,2092,2093,2098],{"__ignoreMap":58},[62,2094,2095],{"class":64,"line":65},[62,2096,2097],{"class":68},"# 权限管理\n",[62,2099,2100,2102],{"class":64,"line":72},[62,2101,1634],{"class":166},[62,2103,2104],{"class":79}," apk add --no-cache shadow\n",[1503,2106,2108],{"id":2107},"_2-创建用户并修改权限","2. 创建用户并修改权限",[53,2110,2112],{"className":1572,"code":2111,"language":1574,"meta":58,"style":58},"# 创建用户组和用户\nRUN addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n\n# 修改目录权限\nRUN chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[33,2113,2114,2119,2126,2130,2135],{"__ignoreMap":58},[62,2115,2116],{"class":64,"line":65},[62,2117,2118],{"class":68},"# 创建用户组和用户\n",[62,2120,2121,2123],{"class":64,"line":72},[62,2122,1634],{"class":166},[62,2124,2125],{"class":79}," addgroup -S \u003Capp_user_group> && adduser -S -G \u003Capp_user_group> \u003Capp_user>\n",[62,2127,2128],{"class":64,"line":83},[62,2129,753],{"emptyLinePlaceholder":752},[62,2131,2132],{"class":64,"line":91},[62,2133,2134],{"class":68},"# 修改目录权限\n",[62,2136,2137,2139],{"class":64,"line":99},[62,2138,1634],{"class":166},[62,2140,2141],{"class":79}," chown -R \u003Capp_user_group>:\u003Capp_user> \u002Fdata\n",[1503,2143,2145],{"id":2144},"_3-指定用户启动","3. 指定用户启动",[15,2147,2148,2149,2152],{},"执行 docker 时加 ",[33,2150,2151],{},"--user"," 参数：",[53,2154,2156],{"className":1275,"code":2155,"language":1277,"meta":58,"style":58},"docker run -it --user \u003Capp_user> \u003Capp_image>:\u003Cimage_version>\n",[33,2157,2158],{"__ignoreMap":58},[62,2159,2160,2162,2164,2166,2169,2171,2174,2177,2179,2181,2184,2187,2189,2191,2193,2196,2199],{"class":64,"line":65},[62,2161,1289],{"class":75},[62,2163,1292],{"class":273},[62,2165,1295],{"class":114},[62,2167,2168],{"class":114}," --user",[62,2170,1798],{"class":166},[62,2172,2173],{"class":273},"app_use",[62,2175,2176],{"class":79},"r",[62,2178,1807],{"class":166},[62,2180,1798],{"class":166},[62,2182,2183],{"class":273},"app_imag",[62,2185,2186],{"class":79},"e",[62,2188,1807],{"class":166},[62,2190,180],{"class":273},[62,2192,575],{"class":166},[62,2194,2195],{"class":273},"image_versio",[62,2197,2198],{"class":79},"n",[62,2200,585],{"class":166},[26,2202,2203],{"id":2203},"完整示例",[53,2205,2207],{"className":1572,"code":2206,"language":1574,"meta":58,"style":58},"FROM alpine:latest\n\n# 国内源\nRUN sed -i 's\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg' \u002Fetc\u002Fapk\u002Frepositories\n\nRUN apk update --no-cache\n# 权限管理\nRUN apk add --no-cache shadow\n# 调试\nRUN apk add --no-cache bash\n# 设置时区\nRUN apk add --no-cache tzdata\n\nENV TZ=Asia\u002FShanghai\n\n# 创建用户组和用户\nRUN addgroup -S glog && adduser -S -G glog glog\n\nCOPY .\u002Fglog_static_musl_1 \u002Fapp\u002F\n\nRUN mkdir \u002Fdata\n\nVOLUME [ \"\u002Fdata\" ]\nWORKDIR \u002Fdata\n\n# 修改权限\nRUN chown -R glog:glog \u002Fdata\nRUN chown -R glog:glog \u002Fapp\n\n# 指定用户启动\nUSER glog\nCMD [ \"\u002Fapp\u002Fglog_static_musl_1\", \"logsweb\", \".\u002Fconf.ini\" ]\n",[33,2208,2209,2215,2219,2224,2237,2241,2248,2252,2258,2263,2270,2275,2282,2286,2293,2297,2301,2308,2312,2319,2323,2330,2334,2348,2355,2360,2366,2374,2382,2387,2393,2402],{"__ignoreMap":58},[62,2210,2211,2213],{"class":64,"line":65},[62,2212,1581],{"class":166},[62,2214,1937],{"class":79},[62,2216,2217],{"class":64,"line":72},[62,2218,753],{"emptyLinePlaceholder":752},[62,2220,2221],{"class":64,"line":83},[62,2222,2223],{"class":68},"# 国内源\n",[62,2225,2226,2228,2231,2234],{"class":64,"line":91},[62,2227,1634],{"class":166},[62,2229,2230],{"class":79}," sed -i ",[62,2232,2233],{"class":273},"'s\u002Fdl-cdn.alpinelinux.org\u002Fmirrors.ustc.edu.cn\u002Fg'",[62,2235,2236],{"class":79}," \u002Fetc\u002Fapk\u002Frepositories\n",[62,2238,2239],{"class":64,"line":99},[62,2240,753],{"emptyLinePlaceholder":752},[62,2242,2243,2245],{"class":64,"line":124},[62,2244,1634],{"class":166},[62,2246,2247],{"class":79}," apk update --no-cache\n",[62,2249,2250],{"class":64,"line":130},[62,2251,2097],{"class":68},[62,2253,2254,2256],{"class":64,"line":136},[62,2255,1634],{"class":166},[62,2257,2104],{"class":79},[62,2259,2260],{"class":64,"line":402},[62,2261,2262],{"class":68},"# 调试\n",[62,2264,2265,2267],{"class":64,"line":407},[62,2266,1634],{"class":166},[62,2268,2269],{"class":79}," apk add --no-cache bash\n",[62,2271,2272],{"class":64,"line":433},[62,2273,2274],{"class":68},"# 设置时区\n",[62,2276,2277,2279],{"class":64,"line":438},[62,2278,1634],{"class":166},[62,2280,2281],{"class":79}," apk add --no-cache tzdata\n",[62,2283,2284],{"class":64,"line":446},[62,2285,753],{"emptyLinePlaceholder":752},[62,2287,2288,2290],{"class":64,"line":1448},[62,2289,1492],{"class":166},[62,2291,2292],{"class":79}," TZ=Asia\u002FShanghai\n",[62,2294,2295],{"class":64,"line":1459},[62,2296,753],{"emptyLinePlaceholder":752},[62,2298,2299],{"class":64,"line":1470},[62,2300,2118],{"class":68},[62,2302,2303,2305],{"class":64,"line":1475},[62,2304,1634],{"class":166},[62,2306,2307],{"class":79}," addgroup -S glog && adduser -S -G glog glog\n",[62,2309,2310],{"class":64,"line":1484},[62,2311,753],{"emptyLinePlaceholder":752},[62,2313,2314,2316],{"class":64,"line":1489},[62,2315,1608],{"class":166},[62,2317,2318],{"class":79}," .\u002Fglog_static_musl_1 \u002Fapp\u002F\n",[62,2320,2321],{"class":64,"line":1695},[62,2322,753],{"emptyLinePlaceholder":752},[62,2324,2325,2327],{"class":64,"line":1703},[62,2326,1634],{"class":166},[62,2328,2329],{"class":79}," mkdir \u002Fdata\n",[62,2331,2332],{"class":64,"line":1708},[62,2333,753],{"emptyLinePlaceholder":752},[62,2335,2337,2339,2342,2345],{"class":64,"line":2336},23,[62,2338,1685],{"class":166},[62,2340,2341],{"class":79}," [ ",[62,2343,2344],{"class":273},"\"\u002Fdata\"",[62,2346,2347],{"class":79}," ]\n",[62,2349,2351,2353],{"class":64,"line":2350},24,[62,2352,1698],{"class":166},[62,2354,1688],{"class":79},[62,2356,2358],{"class":64,"line":2357},25,[62,2359,753],{"emptyLinePlaceholder":752},[62,2361,2363],{"class":64,"line":2362},26,[62,2364,2365],{"class":68},"# 修改权限\n",[62,2367,2369,2371],{"class":64,"line":2368},27,[62,2370,1634],{"class":166},[62,2372,2373],{"class":79}," chown -R glog:glog \u002Fdata\n",[62,2375,2377,2379],{"class":64,"line":2376},28,[62,2378,1634],{"class":166},[62,2380,2381],{"class":79}," chown -R glog:glog \u002Fapp\n",[62,2383,2385],{"class":64,"line":2384},29,[62,2386,753],{"emptyLinePlaceholder":752},[62,2388,2390],{"class":64,"line":2389},30,[62,2391,2392],{"class":68},"# 指定用户启动\n",[62,2394,2396,2399],{"class":64,"line":2395},31,[62,2397,2398],{"class":166},"USER",[62,2400,2401],{"class":79}," glog\n",[62,2403,2405,2407,2409,2412,2414,2416,2418,2421],{"class":64,"line":2404},32,[62,2406,1711],{"class":166},[62,2408,2341],{"class":79},[62,2410,2411],{"class":273},"\"\u002Fapp\u002Fglog_static_musl_1\"",[62,2413,186],{"class":79},[62,2415,2018],{"class":273},[62,2417,186],{"class":79},[62,2419,2420],{"class":273},"\".\u002Fconf.ini\"",[62,2422,2347],{"class":79},[26,2424,1168],{"id":1168},[15,2426,2427],{},"关键步骤：",[513,2429,2430,2436,2446,2452],{},[516,2431,2432,2435],{},[33,2433,2434],{},"apk add --no-cache shadow"," - 安装用户管理工具",[516,2437,2438,2441,2442,2445],{},[33,2439,2440],{},"addgroup"," + ",[33,2443,2444],{},"adduser"," - 创建用户",[516,2447,2448,2451],{},[33,2449,2450],{},"chown -R"," - 修改目录权限",[516,2453,2454,2457],{},[33,2455,2456],{},"USER \u003Cusername>"," - 指定运行用户",[15,2459,2460],{},"这样容器内的应用就不再以 root 身份运行，更加安全。",[1228,2462,2463],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}",{"title":58,"searchDepth":72,"depth":83,"links":2465},[2466,2467,2472,2473],{"id":2074,"depth":72,"text":2074},{"id":2080,"depth":72,"text":2080,"children":2468},[2469,2470,2471],{"id":2083,"depth":83,"text":2084},{"id":2107,"depth":83,"text":2108},{"id":2144,"depth":83,"text":2145},{"id":2203,"depth":72,"text":2203},{"id":1168,"depth":72,"text":1168},"解决 Docker 容器默认以 root 用户运行导致的权限问题，指定非 root 用户启动应用。","入门","\u002Fposts\u002Fdocker-user-start\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdocker-user-start","2025-04-01",{"title":2069,"description":2474},"posts\u002Fdocker-user-start\u002Findex",[2063,2065,2483],"Linux","b4Uyw5ms87xr7NhpIkavHnNbT20oaEPtXz8k1w6UPj0",{"id":2486,"title":2487,"author":6,"body":2488,"description":2694,"difficulty":6,"draft":1243,"extension":1244,"image":2695,"meta":2696,"navigation":752,"path":2697,"pinned":1243,"published":2698,"seo":2699,"series":6,"seriesOrder":6,"stem":2700,"tags":2701,"updated":6,"__hash__":2703},"posts\u002Fposts\u002Fsveltekit-intro\u002Findex.md","SvelteKit 入门：为什么它比你想象的更简单",{"type":8,"value":2489,"toc":2684},[2490,2494,2497,2503,2506,2510,2517,2565,2569,2575,2583,2587,2590,2593,2662,2664,2675,2681],[26,2491,2493],{"id":2492},"什么是-sveltekit","什么是 SvelteKit？",[15,2495,2496],{},"SvelteKit 是 Svelte 的官方全栈框架，类似于 React 之于 Next.js 的关系。它提供了路由、SSR、SSG 等能力，让开发者可以快速构建现代 Web 应用。",[15,2498,2499],{},[770,2500],{"alt":2501,"src":2502},"SvelteKit Logo","https:\u002F\u002Fraw.githubusercontent.com\u002Fsveltejs\u002Fbranding\u002Fmaster\u002Fsvelte-logo.svg",[26,2504,2505],{"id":2505},"核心特点",[1503,2507,2509],{"id":2508},"_1-编译时框架","1. 编译时框架",[15,2511,2512,2513,2516],{},"Svelte 和 React\u002FVue 最大的区别在于：",[38,2514,2515],{},"它在编译阶段就把组件转成原生 JS","，运行时没有虚拟 DOM。",[53,2518,2520],{"className":55,"code":2519,"language":57,"meta":58,"style":58},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\nlet count = 0\nfunction increment() {\n  count += 1\n}\n",[33,2521,2522,2527,2540,2550,2561],{"__ignoreMap":58},[62,2523,2524],{"class":64,"line":65},[62,2525,2526],{"class":68},"\u002F\u002F Svelte 组件（编译后变成直接的 DOM 操作）\n",[62,2528,2529,2532,2535,2537],{"class":64,"line":72},[62,2530,2531],{"class":166},"let",[62,2533,2534],{"class":79}," count ",[62,2536,200],{"class":166},[62,2538,2539],{"class":114}," 0\n",[62,2541,2542,2544,2547],{"class":64,"line":83},[62,2543,167],{"class":166},[62,2545,2546],{"class":75}," increment",[62,2548,2549],{"class":79},"() {\n",[62,2551,2552,2555,2558],{"class":64,"line":91},[62,2553,2554],{"class":79},"  count ",[62,2556,2557],{"class":166},"+=",[62,2559,2560],{"class":114}," 1\n",[62,2562,2563],{"class":64,"line":99},[62,2564,139],{"class":79},[1503,2566,2568],{"id":2567},"_2-文件路由","2. 文件路由",[15,2570,2571,2574],{},[33,2572,2573],{},"src\u002Froutes\u002F"," 目录结构即 URL，不需要手动配置路由表：",[53,2576,2581],{"className":2577,"code":2579,"language":2580},[2578],"language-text","src\u002Froutes\u002F\n├── +page.svelte        → \u002F\n├── about\u002F\n│   └── +page.svelte    → \u002Fabout\n└── posts\u002F\n    ├── +page.svelte    → \u002Fposts\n    └── [slug]\u002F\n        └── +page.svelte → \u002Fposts\u002F:slug\n","text",[33,2582,2579],{"__ignoreMap":58},[1503,2584,2586],{"id":2585},"_3-全栈能力","3. 全栈能力",[15,2588,2589],{},"SvelteKit 内置了服务端渲染（SSR）、静态站点生成（SSG）、API 路由等功能，开箱即用。",[26,2591,2592],{"id":2592},"和其他框架对比",[865,2594,2595,2610],{},[868,2596,2597],{},[871,2598,2599,2602,2605,2608],{},[874,2600,2601],{},"特性",[874,2603,2604],{},"SvelteKit",[874,2606,2607],{},"Next.js",[874,2609,1253],{},[884,2611,2612,2625,2638,2651],{},[871,2613,2614,2617,2620,2623],{},[889,2615,2616],{},"学习曲线",[889,2618,2619],{},"低",[889,2621,2622],{},"中",[889,2624,2622],{},[871,2626,2627,2630,2633,2636],{},[889,2628,2629],{},"包体积",[889,2631,2632],{},"小",[889,2634,2635],{},"大",[889,2637,2622],{},[871,2639,2640,2643,2646,2649],{},[889,2641,2642],{},"运行时开销",[889,2644,2645],{},"无",[889,2647,2648],{},"有",[889,2650,2648],{},[871,2652,2653,2656,2658,2660],{},[889,2654,2655],{},"生态",[889,2657,2632],{},[889,2659,2635],{},[889,2661,2635],{},[26,2663,1168],{"id":1168},[15,2665,2666,2667,2670,2671,2674],{},"如果你追求",[38,2668,2669],{},"简洁的代码","和",[38,2672,2673],{},"极致的性能","，SvelteKit 是一个非常值得尝试的选择。它的学习曲线平缓，写起来就像在写原生 HTML\u002FCSS\u002FJS。",[15,2676,2677],{},[770,2678],{"alt":2679,"src":2680},"Svelte 编译过程","https:\u002F\u002Fsvelte.dev\u002Fsvelte-logo-horizontal.svg",[1228,2682,2683],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":58,"searchDepth":72,"depth":83,"links":2685},[2686,2687,2692,2693],{"id":2492,"depth":72,"text":2493},{"id":2505,"depth":72,"text":2505,"children":2688},[2689,2690,2691],{"id":2508,"depth":83,"text":2509},{"id":2567,"depth":83,"text":2568},{"id":2585,"depth":83,"text":2586},{"id":2592,"depth":72,"text":2592},{"id":1168,"depth":72,"text":1168},"SvelteKit 是一个全栈 Svelte 框架，这篇文章带你了解它的核心概念和优势。","\u002Fposts\u002Fsveltekit-intro\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsveltekit-intro","2026-05-30",{"title":2487,"description":2694},"posts\u002Fsveltekit-intro\u002Findex",[2604,2702],"前端框架","vsEGyunpCWR7DiJAdyV1SPDfa57xXLITBI-lKM8UeGQ",{"id":2705,"title":2706,"author":6,"body":2707,"description":3106,"difficulty":2475,"draft":1243,"extension":1244,"image":3107,"meta":3108,"navigation":752,"path":3109,"pinned":752,"published":3110,"seo":3111,"series":1250,"seriesOrder":65,"stem":3112,"tags":3113,"updated":1256,"__hash__":3115},"posts\u002Fposts\u002Fhello-world\u002Findex.md","Hello World - 欢迎使用 SuBlog",{"type":8,"value":2708,"toc":3097},[2709,2713,2720,2723,2726,2729,2803,2806,2809,2872,2875,2878,2942,2945,3009,3012,3015,3031,3038,3041,3072,3075,3086,3089,3094],[26,2710,2712],{"id":2711},"博客上线啦","博客上线啦！",[15,2714,2715,2716,2719],{},"Hello World！欢迎使用 ",[38,2717,2718],{},"SuBlog","，这是站点的第一篇示例文章。",[15,2721,2722],{},"经过一段时间的折腾，这个基于 Nuxt 3 的个人博客终于上线了。在这里记录一下这个项目的技术选型和功能特性。",[26,2724,2725],{"id":2725},"技术栈",[15,2727,2728],{},"本站使用了以下技术：",[865,2730,2731,2741],{},[868,2732,2733],{},[871,2734,2735,2738],{},[874,2736,2737],{},"技术",[874,2739,2740],{},"用途",[884,2742,2743,2753,2763,2773,2783,2793],{},[871,2744,2745,2750],{},[889,2746,2747],{},[38,2748,2749],{},"Nuxt 3",[889,2751,2752],{},"Vue 3 全栈框架，提供 SSG、路由、API 等能力",[871,2754,2755,2760],{},[889,2756,2757],{},[38,2758,2759],{},"TypeScript",[889,2761,2762],{},"类型安全，提升开发体验",[871,2764,2765,2770],{},[889,2766,2767],{},[38,2768,2769],{},"Tailwind CSS v4",[889,2771,2772],{},"原子化 CSS，快速构建 UI",[871,2774,2775,2780],{},[889,2776,2777],{},[38,2778,2779],{},"@nuxt\u002Fcontent",[889,2781,2782],{},"Markdown 驱动的内容管理",[871,2784,2785,2790],{},[889,2786,2787],{},[38,2788,2789],{},"Shiki",[889,2791,2792],{},"代码语法高亮",[871,2794,2795,2800],{},[889,2796,2797],{},[38,2798,2799],{},"Giscus",[889,2801,2802],{},"基于 GitHub Discussions 的评论系统",[26,2804,2805],{"id":2805},"功能特性",[15,2807,2808],{},"目前已实现的功能：",[2810,2811,2812,2818,2824,2830,2836,2842,2848,2854,2860,2866],"ul",{},[516,2813,2814,2817],{},[38,2815,2816],{},"博客系统"," - Markdown 文章，支持代码高亮、目录导航、搜索",[516,2819,2820,2823],{},[38,2821,2822],{},"暗色模式"," - 跟随系统或手动切换",[516,2825,2826,2829],{},[38,2827,2828],{},"文章搜索"," - 支持标题、描述、正文、标签多维度搜索",[516,2831,2832,2835],{},[38,2833,2834],{},"标签系统"," - 文章标签分类，高频标签快速筛选",[516,2837,2838,2841],{},[38,2839,2840],{},"友链页面"," - 展示友情链接，支持申请",[516,2843,2844,2847],{},[38,2845,2846],{},"赞助页面"," - 微信收款码展示",[516,2849,2850,2853],{},[38,2851,2852],{},"封面制作"," - 在线生成封面图片工具",[516,2855,2856,2859],{},[38,2857,2858],{},"网易云热评"," - 首页随机展示音乐评论",[516,2861,2862,2865],{},[38,2863,2864],{},"访客统计"," - 不蒜子计数器，带数字滚动动画",[516,2867,2868,2871],{},[38,2869,2870],{},"SEO 优化"," - 自动生成站点地图、Open Graph 标签",[26,2873,2874],{"id":2874},"代码示例",[15,2876,2877],{},"本站的配置非常集中，一个文件搞定全站定制：",[53,2879,2881],{"className":55,"code":2880,"language":57,"meta":58,"style":58},"\u002F\u002F config\u002Fsite.ts\nexport const siteConfig = {\n  siteName: '我的博客',\n  url: 'https:\u002F\u002Fexample.com\u002F',\n  avatar: 'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100',\n  \u002F\u002F ... 改这一个文件，全站生效\n}\n",[33,2882,2883,2888,2902,2913,2923,2933,2938],{"__ignoreMap":58},[62,2884,2885],{"class":64,"line":65},[62,2886,2887],{"class":68},"\u002F\u002F config\u002Fsite.ts\n",[62,2889,2890,2892,2895,2898,2900],{"class":64,"line":72},[62,2891,940],{"class":166},[62,2893,2894],{"class":166}," const",[62,2896,2897],{"class":114}," siteConfig",[62,2899,321],{"class":166},[62,2901,620],{"class":79},[62,2903,2904,2907,2910],{"class":64,"line":83},[62,2905,2906],{"class":79},"  siteName: ",[62,2908,2909],{"class":273},"'我的博客'",[62,2911,2912],{"class":79},",\n",[62,2914,2915,2918,2921],{"class":64,"line":91},[62,2916,2917],{"class":79},"  url: ",[62,2919,2920],{"class":273},"'https:\u002F\u002Fexample.com\u002F'",[62,2922,2912],{"class":79},[62,2924,2925,2928,2931],{"class":64,"line":99},[62,2926,2927],{"class":79},"  avatar: ",[62,2929,2930],{"class":273},"'https:\u002F\u002Fq2.qlogo.cn\u002Fheadimg_dl?dst_uin=xxx&spec=100'",[62,2932,2912],{"class":79},[62,2934,2935],{"class":64,"line":124},[62,2936,2937],{"class":68},"  \u002F\u002F ... 改这一个文件，全站生效\n",[62,2939,2940],{"class":64,"line":130},[62,2941,139],{"class":79},[15,2943,2944],{},"文章的 frontmatter 也很简洁：",[53,2946,2950],{"className":2947,"code":2948,"language":2949,"meta":58,"style":58},"language-yaml shiki shiki-themes github-light github-dark","---\ntitle: 文章标题\npublished: 2026-05-31T08:00:00\ndescription: 文章简介\ntags: ['Nuxt', 'TypeScript']\n---\n","yaml",[33,2951,2952,2957,2967,2977,2987,3005],{"__ignoreMap":58},[62,2953,2954],{"class":64,"line":65},[62,2955,2956],{"class":75},"---\n",[62,2958,2959,2962,2964],{"class":64,"line":72},[62,2960,2961],{"class":578},"title",[62,2963,111],{"class":79},[62,2965,2966],{"class":273},"文章标题\n",[62,2968,2969,2972,2974],{"class":64,"line":83},[62,2970,2971],{"class":578},"published",[62,2973,111],{"class":79},[62,2975,2976],{"class":114},"2026-05-31T08:00:00\n",[62,2978,2979,2982,2984],{"class":64,"line":91},[62,2980,2981],{"class":578},"description",[62,2983,111],{"class":79},[62,2985,2986],{"class":273},"文章简介\n",[62,2988,2989,2992,2995,2998,3000,3003],{"class":64,"line":99},[62,2990,2991],{"class":578},"tags",[62,2993,2994],{"class":79},": [",[62,2996,2997],{"class":273},"'Nuxt'",[62,2999,186],{"class":79},[62,3001,3002],{"class":273},"'TypeScript'",[62,3004,1720],{"class":79},[62,3006,3007],{"class":64,"line":124},[62,3008,2956],{"class":75},[26,3010,3011],{"id":3011},"部署",[15,3013,3014],{},"本站使用 SSG 静态生成，构建后直接部署到静态托管即可：",[53,3016,3018],{"className":1275,"code":3017,"language":1277,"meta":58,"style":58},"npx nuxi generate\n",[33,3019,3020],{"__ignoreMap":58},[62,3021,3022,3025,3028],{"class":64,"line":65},[62,3023,3024],{"class":75},"npx",[62,3026,3027],{"class":273}," nuxi",[62,3029,3030],{"class":273}," generate\n",[15,3032,3033,3034,3037],{},"构建产物在 ",[33,3035,3036],{},".output\u002Fpublic"," 目录下，可以直接丢到 Nginx、Vercel、Netlify 等平台。",[26,3039,3040],{"id":3040},"后续计划",[2810,3042,3045,3054,3060,3066],{"className":3043},[3044],"contains-task-list",[516,3046,3049,3053],{"className":3047},[3048],"task-list-item",[3050,3051],"input",{"disabled":752,"type":3052},"checkbox"," RSS 订阅",[516,3055,3057,3059],{"className":3056},[3048],[3050,3058],{"disabled":752,"type":3052}," 文章目录优化",[516,3061,3063,3065],{"className":3062},[3048],[3050,3064],{"disabled":752,"type":3052}," 更多小工具",[516,3067,3069,3071],{"className":3068},[3048],[3050,3070],{"disabled":752,"type":3052}," 持续输出内容",[26,3073,3074],{"id":3074},"致谢",[15,3076,3077,3078,3085],{},"感谢 ",[3079,3080,3084],"a",{"href":3081,"rel":3082},"https:\u002F\u002F2x.nz",[3083],"nofollow","二叉树树"," 的开源项目，本站的 UI 设计参考了他的项目。",[3087,3088],"hr",{},[15,3090,3091],{},[38,3092,3093],{},"感谢阅读！如果觉得不错，欢迎点个 Star 或者留个评论 (☞ﾟヮﾟ)☞",[1228,3095,3096],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":58,"searchDepth":72,"depth":83,"links":3098},[3099,3100,3101,3102,3103,3104,3105],{"id":2711,"depth":72,"text":2712},{"id":2725,"depth":72,"text":2725},{"id":2805,"depth":72,"text":2805},{"id":2874,"depth":72,"text":2874},{"id":3011,"depth":72,"text":3011},{"id":3040,"depth":72,"text":3040},{"id":3074,"depth":72,"text":3074},"博客上线啦！这篇文章介绍本站的技术架构、功能特性和搭建过程中的一些思考。","\u002Fposts\u002Fhello-world\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fhello-world","2026-05-31T08:00:00",{"title":2706,"description":3106},"posts\u002Fhello-world\u002Findex",[3114,1253],"博客","f9MCboCBfQIUxT8aKv6aKGDFxysiG8ZG0YXr5X5LTFo",{"id":4,"title":5,"author":6,"body":3117,"description":1241,"difficulty":1242,"draft":1243,"extension":1244,"image":1245,"meta":4019,"navigation":752,"path":1247,"pinned":1243,"published":1248,"seo":4020,"series":1250,"seriesOrder":72,"stem":1251,"tags":4021,"updated":1256,"__hash__":1257},{"type":8,"value":3118,"toc":4009},[3119,3121,3123,3125,3127,3129,3135,3137,3141,3195,3199,3201,3205,3415,3421,3423,3425,3427,3443,3451,3453,3469,3473,3475,3477,3479,3483,3487,3671,3675,3713,3715,3717,3721,3723,3761,3765,3955,3961,3963,4003,4005,4007],[11,3120,13],{"id":13},[15,3122,17],{},[15,3124,20],{},[11,3126,24],{"id":23},[26,3128,28],{"id":28},[15,3130,31,3131,36,3133,41],{},[33,3132,35],{},[38,3134,40],{},[26,3136,44],{"id":44},[15,3138,47,3139,51],{},[33,3140,50],{},[53,3142,3143],{"className":55,"code":56,"language":57,"meta":58,"style":58},[33,3144,3145,3149,3155,3161,3167,3183,3187,3191],{"__ignoreMap":58},[62,3146,3147],{"class":64,"line":65},[62,3148,69],{"class":68},[62,3150,3151,3153],{"class":64,"line":72},[62,3152,76],{"class":75},[62,3154,80],{"class":79},[62,3156,3157,3159],{"class":64,"line":83},[62,3158,86],{"class":75},[62,3160,80],{"class":79},[62,3162,3163,3165],{"class":64,"line":91},[62,3164,94],{"class":75},[62,3166,80],{"class":79},[62,3168,3169,3171,3173,3175,3177,3179,3181],{"class":64,"line":99},[62,3170,102],{"class":75},[62,3172,105],{"class":79},[62,3174,108],{"class":75},[62,3176,111],{"class":79},[62,3178,115],{"class":114},[62,3180,118],{"class":79},[62,3182,121],{"class":68},[62,3184,3185],{"class":64,"line":124},[62,3186,127],{"class":79},[62,3188,3189],{"class":64,"line":130},[62,3190,133],{"class":79},[62,3192,3193],{"class":64,"line":136},[62,3194,139],{"class":79},[15,3196,142,3197,146],{},[38,3198,145],{},[26,3200,149],{"id":149},[15,3202,152,3203,156],{},[33,3204,155],{},[53,3206,3207],{"className":55,"code":159,"language":57,"meta":58,"style":58},[33,3208,3209,3243,3257,3267,3271,3313,3337,3361,3379,3383,3401,3405,3411],{"__ignoreMap":58},[62,3210,3211,3213,3215,3217,3219,3221,3223,3225,3227,3229,3231,3233,3235,3237,3239,3241],{"class":64,"line":65},[62,3212,167],{"class":166},[62,3214,170],{"class":75},[62,3216,173],{"class":79},[62,3218,177],{"class":176},[62,3220,180],{"class":166},[62,3222,183],{"class":114},[62,3224,186],{"class":79},[62,3226,189],{"class":176},[62,3228,180],{"class":166},[62,3230,194],{"class":75},[62,3232,197],{"class":79},[62,3234,200],{"class":166},[62,3236,203],{"class":79},[62,3238,180],{"class":166},[62,3240,194],{"class":75},[62,3242,210],{"class":79},[62,3244,3245,3247,3249,3251,3253,3255],{"class":64,"line":72},[62,3246,215],{"class":166},[62,3248,218],{"class":79},[62,3250,221],{"class":166},[62,3252,224],{"class":79},[62,3254,227],{"class":166},[62,3256,230],{"class":79},[62,3258,3259,3261,3263,3265],{"class":64,"line":83},[62,3260,215],{"class":166},[62,3262,237],{"class":79},[62,3264,240],{"class":75},[62,3266,243],{"class":79},[62,3268,3269],{"class":64,"line":91},[62,3270,248],{"class":68},[62,3272,3273,3275,3277,3279,3281,3283,3285,3287,3289,3291,3293,3295,3297,3299,3301,3303,3305,3307,3309,3311],{"class":64,"line":99},[62,3274,253],{"class":166},[62,3276,218],{"class":79},[62,3278,258],{"class":166},[62,3280,261],{"class":79},[62,3282,264],{"class":114},[62,3284,267],{"class":79},[62,3286,270],{"class":166},[62,3288,274],{"class":273},[62,3290,277],{"class":166},[62,3292,280],{"class":273},[62,3294,283],{"class":166},[62,3296,287],{"class":286},[62,3298,290],{"class":114},[62,3300,293],{"class":166},[62,3302,296],{"class":273},[62,3304,299],{"class":79},[62,3306,302],{"class":75},[62,3308,305],{"class":79},[62,3310,264],{"class":114},[62,3312,310],{"class":79},[62,3314,3315,3317,3319,3321,3323,3325,3327,3329,3331,3333,3335],{"class":64,"line":124},[62,3316,315],{"class":166},[62,3318,318],{"class":114},[62,3320,321],{"class":166},[62,3322,324],{"class":75},[62,3324,305],{"class":79},[62,3326,264],{"class":114},[62,3328,331],{"class":79},[62,3330,334],{"class":75},[62,3332,173],{"class":79},[62,3334,339],{"class":114},[62,3336,342],{"class":79},[62,3338,3339,3341,3343,3345,3347,3349,3351,3353,3355,3357,3359],{"class":64,"line":130},[62,3340,315],{"class":166},[62,3342,349],{"class":114},[62,3344,321],{"class":166},[62,3346,354],{"class":75},[62,3348,357],{"class":79},[62,3350,360],{"class":75},[62,3352,173],{"class":79},[62,3354,365],{"class":114},[62,3356,368],{"class":79},[62,3358,371],{"class":75},[62,3360,374],{"class":79},[62,3362,3363,3365,3367,3369,3371,3373,3375,3377],{"class":64,"line":136},[62,3364,379],{"class":79},[62,3366,382],{"class":75},[62,3368,385],{"class":79},[62,3370,339],{"class":114},[62,3372,390],{"class":79},[62,3374,393],{"class":166},[62,3376,396],{"class":75},[62,3378,399],{"class":79},[62,3380,3381],{"class":64,"line":402},[62,3382,127],{"class":79},[62,3384,3385,3387,3389,3391,3393,3395,3397,3399],{"class":64,"line":407},[62,3386,410],{"class":166},[62,3388,218],{"class":79},[62,3390,415],{"class":166},[62,3392,418],{"class":114},[62,3394,421],{"class":166},[62,3396,424],{"class":79},[62,3398,427],{"class":75},[62,3400,430],{"class":79},[62,3402,3403],{"class":64,"line":433},[62,3404,133],{"class":79},[62,3406,3407,3409],{"class":64,"line":438},[62,3408,441],{"class":166},[62,3410,230],{"class":79},[62,3412,3413],{"class":64,"line":446},[62,3414,139],{"class":79},[15,3416,451,3417,454,3419,458],{},[38,3418,8],{},[33,3420,457],{},[11,3422,462],{"id":461},[26,3424,28],{"id":465},[15,3426,468],{},[53,3428,3429],{"className":471,"code":472,"language":473,"meta":58,"style":58},[33,3430,3431],{"__ignoreMap":58},[62,3432,3433,3435,3437,3439,3441],{"class":64,"line":65},[62,3434,480],{"class":79},[62,3436,484],{"class":483},[62,3438,487],{"class":79},[62,3440,491],{"class":490},[62,3442,494],{"class":79},[15,3444,497,3445,501,3447,505,3449,41],{},[33,3446,500],{},[33,3448,504],{},[33,3450,508],{},[26,3452,44],{"id":511},[513,3454,3455,3459,3461,3465],{},[516,3456,518,3457,522],{},[33,3458,521],{},[516,3460,525],{},[516,3462,528,3463,531],{},[33,3464,508],{},[516,3466,528,3467,536],{},[33,3468,504],{},[15,3470,539,3471,41],{},[38,3472,542],{},[15,3474,545],{},[26,3476,149],{"id":548},[15,3478,551],{},[15,3480,3481],{},[38,3482,556],{},[15,3484,559,3485,563],{},[33,3486,562],{},[53,3488,3489],{"className":566,"code":567,"language":568,"meta":58,"style":58},[33,3490,3491,3501,3513,3529,3543,3573,3601,3619,3623,3631,3635,3643,3663],{"__ignoreMap":58},[62,3492,3493,3495,3497,3499],{"class":64,"line":65},[62,3494,575],{"class":79},[62,3496,579],{"class":578},[62,3498,582],{"class":75},[62,3500,585],{"class":79},[62,3502,3503,3505,3507,3509,3511],{"class":64,"line":72},[62,3504,415],{"class":166},[62,3506,592],{"class":114},[62,3508,321],{"class":166},[62,3510,597],{"class":75},[62,3512,374],{"class":79},[62,3514,3515,3517,3519,3521,3523,3525,3527],{"class":64,"line":83},[62,3516,415],{"class":166},[62,3518,606],{"class":114},[62,3520,321],{"class":166},[62,3522,611],{"class":75},[62,3524,614],{"class":79},[62,3526,617],{"class":166},[62,3528,620],{"class":79},[62,3530,3531,3533,3535,3537,3539,3541],{"class":64,"line":91},[62,3532,215],{"class":166},[62,3534,218],{"class":79},[62,3536,221],{"class":166},[62,3538,631],{"class":79},[62,3540,227],{"class":166},[62,3542,636],{"class":273},[62,3544,3545,3547,3549,3551,3553,3555,3557,3559,3561,3563,3565,3567,3569,3571],{"class":64,"line":99},[62,3546,215],{"class":166},[62,3548,643],{"class":79},[62,3550,646],{"class":75},[62,3552,173],{"class":79},[62,3554,651],{"class":273},[62,3556,654],{"class":79},[62,3558,393],{"class":166},[62,3560,659],{"class":79},[62,3562,646],{"class":75},[62,3564,173],{"class":79},[62,3566,666],{"class":273},[62,3568,669],{"class":79},[62,3570,227],{"class":166},[62,3572,674],{"class":79},[62,3574,3575,3577,3579,3581,3583,3585,3587,3589,3591,3593,3595,3597,3599],{"class":64,"line":124},[62,3576,679],{"class":166},[62,3578,682],{"class":114},[62,3580,321],{"class":166},[62,3582,687],{"class":79},[62,3584,690],{"class":75},[62,3586,173],{"class":79},[62,3588,296],{"class":273},[62,3590,698],{"class":697},[62,3592,293],{"class":166},[62,3594,296],{"class":273},[62,3596,186],{"class":79},[62,3598,707],{"class":273},[62,3600,494],{"class":79},[62,3602,3603,3605,3607,3609,3611,3613,3615,3617],{"class":64,"line":130},[62,3604,441],{"class":166},[62,3606,716],{"class":273},[62,3608,719],{"class":79},[62,3610,722],{"class":273},[62,3612,725],{"class":79},[62,3614,299],{"class":273},[62,3616,730],{"class":79},[62,3618,733],{"class":273},[62,3620,3621],{"class":64,"line":136},[62,3622,738],{"class":79},[62,3624,3625,3627,3629],{"class":64,"line":402},[62,3626,743],{"class":79},[62,3628,579],{"class":578},[62,3630,585],{"class":79},[62,3632,3633],{"class":64,"line":407},[62,3634,753],{"emptyLinePlaceholder":752},[62,3636,3637,3639,3641],{"class":64,"line":433},[62,3638,575],{"class":79},[62,3640,760],{"class":578},[62,3642,585],{"class":79},[62,3644,3645,3647,3649,3651,3653,3655,3657,3659,3661],{"class":64,"line":438},[62,3646,767],{"class":79},[62,3648,770],{"class":578},[62,3650,773],{"class":75},[62,3652,200],{"class":79},[62,3654,778],{"class":273},[62,3656,781],{"class":75},[62,3658,200],{"class":79},[62,3660,786],{"class":273},[62,3662,789],{"class":79},[62,3664,3665,3667,3669],{"class":64,"line":446},[62,3666,743],{"class":79},[62,3668,760],{"class":578},[62,3670,585],{"class":79},[15,3672,3673],{},[38,3674,802],{},[53,3676,3677],{"className":55,"code":805,"language":57,"meta":58,"style":58},[33,3678,3679,3683,3689,3695,3705,3709],{"__ignoreMap":58},[62,3680,3681],{"class":64,"line":65},[62,3682,69],{"class":68},[62,3684,3685,3687],{"class":64,"line":72},[62,3686,816],{"class":75},[62,3688,80],{"class":79},[62,3690,3691,3693],{"class":64,"line":83},[62,3692,823],{"class":75},[62,3694,80],{"class":79},[62,3696,3697,3699,3701,3703],{"class":64,"line":91},[62,3698,830],{"class":75},[62,3700,111],{"class":79},[62,3702,835],{"class":114},[62,3704,838],{"class":68},[62,3706,3707],{"class":64,"line":99},[62,3708,133],{"class":79},[62,3710,3711],{"class":64,"line":124},[62,3712,139],{"class":79},[11,3714,850],{"id":849},[26,3716,28],{"id":853},[15,3718,856,3719,860],{},[33,3720,859],{},[26,3722,863],{"id":863},[865,3724,3725,3735],{},[868,3726,3727],{},[871,3728,3729,3731,3733],{},[874,3730,876],{},[874,3732,879],{},[874,3734,882],{},[884,3736,3737,3745,3753],{},[871,3738,3739,3741,3743],{},[889,3740,891],{},[889,3742,894],{},[889,3744,897],{},[871,3746,3747,3749,3751],{},[889,3748,902],{},[889,3750,905],{},[889,3752,908],{},[871,3754,3755,3757,3759],{},[889,3756,913],{},[889,3758,916],{},[889,3760,919],{},[15,3762,922,3763,925],{},[38,3764,913],{},[53,3766,3767],{"className":55,"code":928,"language":57,"meta":58,"style":58},[33,3768,3769,3773,3795,3807,3863,3875,3879,3897,3917,3931,3945,3951],{"__ignoreMap":58},[62,3770,3771],{"class":64,"line":65},[62,3772,935],{"class":68},[62,3774,3775,3777,3779,3781,3783,3785,3787,3789,3791,3793],{"class":64,"line":72},[62,3776,940],{"class":166},[62,3778,943],{"class":166},[62,3780,946],{"class":75},[62,3782,173],{"class":79},[62,3784,951],{"class":166},[62,3786,218],{"class":79},[62,3788,956],{"class":176},[62,3790,654],{"class":79},[62,3792,617],{"class":166},[62,3794,620],{"class":79},[62,3796,3797,3799,3801,3803,3805],{"class":64,"line":83},[62,3798,679],{"class":166},[62,3800,969],{"class":114},[62,3802,321],{"class":166},[62,3804,974],{"class":75},[62,3806,977],{"class":79},[62,3808,3809,3811,3813,3815,3817,3819,3821,3823,3825,3827,3829,3831,3833,3835,3837,3839,3841,3843,3845,3847,3849,3851,3853,3855,3857,3859,3861],{"class":64,"line":91},[62,3810,679],{"class":166},[62,3812,984],{"class":114},[62,3814,321],{"class":166},[62,3816,989],{"class":79},[62,3818,992],{"class":75},[62,3820,173],{"class":79},[62,3822,296],{"class":273},[62,3824,283],{"class":166},[62,3826,698],{"class":697},[62,3828,1003],{"class":286},[62,3830,698],{"class":697},[62,3832,173],{"class":286},[62,3834,1010],{"class":114},[62,3836,283],{"class":166},[62,3838,1015],{"class":114},[62,3840,1018],{"class":166},[62,3842,1021],{"class":286},[62,3844,698],{"class":697},[62,3846,770],{"class":286},[62,3848,698],{"class":697},[62,3850,173],{"class":286},[62,3852,299],{"class":114},[62,3854,1018],{"class":166},[62,3856,1021],{"class":286},[62,3858,293],{"class":166},[62,3860,296],{"class":273},[62,3862,494],{"class":79},[62,3864,3865,3867,3869,3871,3873],{"class":64,"line":99},[62,3866,215],{"class":166},[62,3868,218],{"class":79},[62,3870,221],{"class":166},[62,3872,1052],{"class":79},[62,3874,1055],{"class":166},[62,3876,3877],{"class":64,"line":124},[62,3878,753],{"emptyLinePlaceholder":752},[62,3880,3881,3883,3885,3887,3889,3891,3893,3895],{"class":64,"line":130},[62,3882,679],{"class":166},[62,3884,1066],{"class":79},[62,3886,1069],{"class":114},[62,3888,186],{"class":79},[62,3890,1074],{"class":114},[62,3892,267],{"class":79},[62,3894,200],{"class":166},[62,3896,1081],{"class":79},[62,3898,3899,3901,3903,3905,3907,3909,3911,3913,3915],{"class":64,"line":136},[62,3900,679],{"class":166},[62,3902,1088],{"class":114},[62,3904,321],{"class":166},[62,3906,1093],{"class":75},[62,3908,173],{"class":79},[62,3910,1098],{"class":273},[62,3912,1101],{"class":79},[62,3914,1104],{"class":273},[62,3916,1107],{"class":79},[62,3918,3919,3921,3923,3925,3927,3929],{"class":64,"line":402},[62,3920,679],{"class":166},[62,3922,1114],{"class":114},[62,3924,321],{"class":166},[62,3926,1119],{"class":166},[62,3928,1122],{"class":75},[62,3930,1125],{"class":79},[62,3932,3933,3935,3937,3939,3941,3943],{"class":64,"line":407},[62,3934,1130],{"class":75},[62,3936,1133],{"class":79},[62,3938,1136],{"class":273},[62,3940,186],{"class":79},[62,3942,1141],{"class":114},[62,3944,1144],{"class":79},[62,3946,3947,3949],{"class":64,"line":433},[62,3948,441],{"class":166},[62,3950,1151],{"class":79},[62,3952,3953],{"class":64,"line":438},[62,3954,738],{"class":79},[15,3956,1158,3957,1161,3959,1165],{},[33,3958,508],{},[33,3960,1164],{},[11,3962,1168],{"id":1168},[865,3964,3965,3975],{},[868,3966,3967],{},[871,3968,3969,3971,3973],{},[874,3970,1177],{},[874,3972,1180],{},[874,3974,876],{},[884,3976,3977,3987,3995],{},[871,3978,3979,3981,3985],{},[889,3980,1189],{},[889,3982,3983,1195],{},[33,3984,1194],{},[889,3986,1198],{},[871,3988,3989,3991,3993],{},[889,3990,1203],{},[889,3992,1206],{},[889,3994,1209],{},[871,3996,3997,3999,4001],{},[889,3998,1214],{},[889,4000,1217],{},[889,4002,1220],{},[15,4004,1223],{},[15,4006,1226],{},[1228,4008,1230],{},{"title":58,"searchDepth":72,"depth":83,"links":4010},[4011,4012,4013,4014,4015,4016,4017,4018],{"id":28,"depth":72,"text":28},{"id":44,"depth":72,"text":44},{"id":149,"depth":72,"text":149},{"id":465,"depth":72,"text":28},{"id":511,"depth":72,"text":44},{"id":548,"depth":72,"text":149},{"id":853,"depth":72,"text":28},{"id":863,"depth":72,"text":863},{},{"title":5,"description":1241},[1253,1254,1255],{"id":4023,"title":4024,"author":2718,"body":4025,"description":4290,"difficulty":1242,"draft":1243,"extension":1244,"image":4291,"meta":4292,"navigation":752,"path":4293,"pinned":1243,"published":4294,"seo":4295,"series":1250,"seriesOrder":83,"stem":4296,"tags":4297,"updated":1256,"__hash__":4300},"posts\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Findex.md","Nuxt 3 + Tailwind CSS 客户端导航链接下划线闪烁问题",{"type":8,"value":4026,"toc":4283},[4027,4030,4033,4048,4051,4054,4057,4098,4109,4116,4138,4145,4149,4152,4155,4157,4160,4183,4194,4251,4256,4269,4271,4277,4280],[26,4028,4029],{"id":4029},"问题描述",[15,4031,4032],{},"在 Nuxt 3 项目中使用 Tailwind CSS 时，发现一个奇怪的现象：",[2810,4034,4035,4045],{},[516,4036,4037,4038,4041,4042],{},"首次通过客户端导航（点击 ",[33,4039,4040],{},"\u003CNuxtLink>","）跳转到某个页面时，",[38,4043,4044],{},"所有链接文字都会出现下划线",[516,4046,4047],{},"手动刷新页面后，下划线消失，样式恢复正常",[15,4049,4050],{},"这个问题在开发模式和生产构建后都会出现。",[26,4052,4053],{"id":4053},"原因分析",[15,4055,4056],{},"Tailwind CSS 的 Preflight（基于 modern-normalize）会重置链接样式：",[53,4058,4062],{"className":4059,"code":4060,"language":4061,"meta":58,"style":58},"language-css shiki shiki-themes github-light github-dark","a {\n  color: inherit;\n  text-decoration: inherit;\n}\n","css",[33,4063,4064,4070,4083,4094],{"__ignoreMap":58},[62,4065,4066,4068],{"class":64,"line":65},[62,4067,3079],{"class":578},[62,4069,620],{"class":79},[62,4071,4072,4075,4077,4080],{"class":64,"line":72},[62,4073,4074],{"class":114},"  color",[62,4076,111],{"class":79},[62,4078,4079],{"class":114},"inherit",[62,4081,4082],{"class":79},";\n",[62,4084,4085,4088,4090,4092],{"class":64,"line":83},[62,4086,4087],{"class":114},"  text-decoration",[62,4089,111],{"class":79},[62,4091,4079],{"class":114},[62,4093,4082],{"class":79},[62,4095,4096],{"class":64,"line":91},[62,4097,139],{"class":79},[15,4099,4100,4101,4104,4105,4108],{},"正常情况下，这会让链接继承父元素的 ",[33,4102,4103],{},"text-decoration","（通常是 ",[33,4106,4107],{},"none","），从而去掉浏览器默认的下划线。",[15,4110,4111,4112,4115],{},"问题出在 ",[38,4113,4114],{},"Nuxt 的客户端导航机制","：",[513,4117,4118,4124,4127,4132,4135],{},[516,4119,4120,4121,4123],{},"用户点击 ",[33,4122,4040],{},"，Vue Router 拦截导航",[516,4125,4126],{},"新页面组件异步加载并渲染",[516,4128,4129],{},[38,4130,4131],{},"在 CSS 完全加载\u002F应用之前，页面已经渲染了",[516,4133,4134],{},"此时浏览器使用默认样式（链接带下划线）",[516,4136,4137],{},"CSS 加载完成后，下划线消失",[15,4139,4140,4141,4144],{},"这就是典型的 ",[38,4142,4143],{},"FOUC（Flash of Unstyled Content）"," 问题。",[26,4146,4148],{"id":4147},"为什么刷新就正常了","为什么刷新就正常了？",[15,4150,4151],{},"刷新页面时，浏览器会等待所有 CSS 加载完成后再渲染页面（SSR 模式下 HTML 和 CSS 是一起返回的）。所以刷新后样式是正确的。",[15,4153,4154],{},"而客户端导航是 JavaScript 驱动的，CSS 的加载和页面的渲染是异步的，就出现了时序差。",[26,4156,2080],{"id":2080},[15,4158,4159],{},"尝试了多种方案：",[2810,4161,4162,4168,4174,4180],{},[516,4163,4164,4167],{},[33,4165,4166],{},"@layer base"," 覆盖 — 无效，CSS layers 的优先级问题",[516,4169,4170,4173],{},[33,4171,4172],{},"corePlugins.preflight: false"," — 无效，且破坏其他基础样式",[516,4175,4176,4179],{},[33,4177,4178],{},"features.inlineStyles: true"," — 无效",[516,4181,4182],{},"页面过渡动画 — 只是掩盖，不能根治",[15,4184,4185,4186,4189,4190,4193],{},"最终有效的方案是在 ",[33,4187,4188],{},"app.vue"," 中用 ",[33,4191,4192],{},"!important"," 强制覆盖：",[53,4195,4197],{"className":566,"code":4196,"language":568,"meta":58,"style":58},"\u003Cstyle>\na:not(.prose a) {\n  text-decoration-line: none !important;\n}\n\u003C\u002Fstyle>\n",[33,4198,4199,4207,4225,4239,4243],{"__ignoreMap":58},[62,4200,4201,4203,4205],{"class":64,"line":65},[62,4202,575],{"class":79},[62,4204,1228],{"class":578},[62,4206,585],{"class":79},[62,4208,4209,4211,4214,4216,4219,4222],{"class":64,"line":72},[62,4210,3079],{"class":578},[62,4212,4213],{"class":75},":not",[62,4215,173],{"class":79},[62,4217,4218],{"class":75},".prose",[62,4220,4221],{"class":578}," a",[62,4223,4224],{"class":79},") {\n",[62,4226,4227,4230,4232,4234,4237],{"class":64,"line":83},[62,4228,4229],{"class":114},"  text-decoration-line",[62,4231,111],{"class":79},[62,4233,4107],{"class":114},[62,4235,4236],{"class":166}," !important",[62,4238,4082],{"class":79},[62,4240,4241],{"class":64,"line":91},[62,4242,139],{"class":79},[62,4244,4245,4247,4249],{"class":64,"line":99},[62,4246,743],{"class":79},[62,4248,1228],{"class":578},[62,4250,585],{"class":79},[15,4252,4253],{},[38,4254,4255],{},"关键点：",[2810,4257,4258,4263],{},[516,4259,4260,4262],{},[33,4261,4192],{}," 是为了对抗 Tailwind CSS layers 的优先级",[516,4264,4265,4268],{},[33,4266,4267],{},":not(.prose a)"," 排除文章正文区域，保留文章内链接的下划线（可读性需要）",[26,4270,1168],{"id":1168},[15,4272,4273,4274,4276],{},"这是 Nuxt 3 + Tailwind CSS 的一个已知问题，本质上是 CSS 加载时序导致的 FOUC。",[33,4275,4192],{}," 虽然不够优雅，但在这个场景下是务实的解决方案。",[15,4278,4279],{},"如果你有更好的方案，欢迎在项目仓库的 Issues 中提出！",[1228,4281,4282],{},"html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}",{"title":58,"searchDepth":72,"depth":83,"links":4284},[4285,4286,4287,4288,4289],{"id":4029,"depth":72,"text":4029},{"id":4053,"depth":72,"text":4053},{"id":4147,"depth":72,"text":4148},{"id":2080,"depth":72,"text":2080},{"id":1168,"depth":72,"text":1168},"记录 Nuxt 3 配合 Tailwind CSS 时，客户端导航导致链接文字出现下划线闪烁的问题及解决方案。","\u002Fposts\u002Fnuxt-tailwind-underline-fix\u002Fimg\u002Fcover.png",{},"\u002Fposts\u002Fnuxt-tailwind-underline-fix","2026-05-31T18:47:00",{"title":4024,"description":4290},"posts\u002Fnuxt-tailwind-underline-fix\u002Findex",[1253,4298,4299,1254],"Tailwind CSS","CSS","WMLpTEsIoJqufwEfXyIiBgUYxcPY2XkvOwqAD4hbe6g",{"id":4302,"title":4303,"author":6,"body":4304,"description":6355,"difficulty":1242,"draft":1243,"extension":1244,"image":6356,"meta":6357,"navigation":752,"path":6358,"pinned":1243,"published":6359,"seo":6360,"series":6361,"seriesOrder":65,"stem":6362,"tags":6363,"updated":6368,"__hash__":6369},"posts\u002Fposts\u002Fgo-mcp-gin\u002Findex.md","Go-MCP 接入 Gin：从零搭建 MCP 服务器",{"type":8,"value":4305,"toc":6336},[4306,4310,4313,4316,4320,4327,4333,4336,4339,4359,4362,4366,4390,4394,4494,4507,4511,4675,4678,4895,4899,4910,5009,5016,5020,5294,5297,5378,5382,5385,5387,5398,5402,5408,5775,5965,5968,6024,6027,6180,6183,6186,6257,6259,6324,6333],[26,4307,4309],{"id":4308},"什么是-mcp","什么是 MCP",[15,4311,4312],{},"MCP（Model Context Protocol）是 Anthropic 提出的开放协议，用于让 AI 助手（如 Claude）与外部服务进行标准化交互。简单来说：你提供一个 MCP 服务器，AI 就能发现并调用你定义的工具（Tools），就像给 AI 装了一双手。",[15,4314,4315],{},"传统做法是写 REST API 然后让 AI 通过函数调用（Function Calling）间接使用，但 MCP 统一了协议层，AI 客户端只需要配置一个 URL 就能接入所有工具。",[26,4317,4319],{"id":4318},"为什么选-gin-mcp-go","为什么选 Gin + mcp-go",[15,4321,4322,4323,4326],{},"Go 生态里做 MCP 服务器最成熟的库是 ",[33,4324,4325],{},"mark3labs\u002Fmcp-go","，它支持 StreamableHTTP 传输模式，可以直接挂载到任意 HTTP 框架上。Gin 作为 Go 最流行的 HTTP 框架，天然适配。",[15,4328,4329,4330],{},"核心思路：",[38,4331,4332],{},"MCP 服务器本质上就是一个 HTTP Handler，挂到 Gin 路由上即可。",[26,4334,4335],{"id":4335},"需求分析",[15,4337,4338],{},"我们要搭建一个具备以下能力的 MCP 服务器：",[513,4340,4341,4347,4353],{},[516,4342,4343,4346],{},[38,4344,4345],{},"工具注册"," — 向 AI 暴露可调用的工具（列出资源、查看详情、创建\u002F删除）",[516,4348,4349,4352],{},[38,4350,4351],{},"HTTP 集成"," — MCP 端点作为 Gin 路由的一部分，与业务 API 共存",[516,4354,4355,4358],{},[38,4356,4357],{},"鉴权"," — MCP 请求必须经过身份验证，工具处理器能获取当前用户信息",[26,4360,4361],{"id":4361},"实现步骤",[1503,4363,4365],{"id":4364},"_1-安装依赖","1. 安装依赖",[53,4367,4369],{"className":1275,"code":4368,"language":1277,"meta":58,"style":58},"go get github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\ngo get github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[33,4370,4371,4381],{"__ignoreMap":58},[62,4372,4373,4375,4378],{"class":64,"line":65},[62,4374,1398],{"class":75},[62,4376,4377],{"class":273}," get",[62,4379,4380],{"class":273}," github.com\u002Fmark3labs\u002Fmcp-go@v0.54.0\n",[62,4382,4383,4385,4387],{"class":64,"line":72},[62,4384,1398],{"class":75},[62,4386,4377],{"class":273},[62,4388,4389],{"class":273}," github.com\u002Fgin-gonic\u002Fgin@v1.12.0\n",[1503,4391,4393],{"id":4392},"_2-创建-mcp-服务器","2. 创建 MCP 服务器",[53,4395,4398],{"className":4396,"code":4397,"language":1398,"meta":58,"style":58},"language-go shiki shiki-themes github-light github-dark","import \"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver\"\n\nmcpServer := server.NewMCPServer(\n    \"my-app\",    \u002F\u002F 服务器名称\n    \"1.0.0\",     \u002F\u002F 版本\n    server.WithLogging(),\n    server.WithRecovery(),\n    server.WithInstructions(\"这是一个 MCP 服务器示例\"),\n)\n",[33,4399,4400,4412,4416,4433,4444,4455,4466,4475,4490],{"__ignoreMap":58},[62,4401,4402,4405,4407,4410],{"class":64,"line":65},[62,4403,4404],{"class":166},"import",[62,4406,1304],{"class":273},[62,4408,4409],{"class":75},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver",[62,4411,1501],{"class":273},[62,4413,4414],{"class":64,"line":72},[62,4415,753],{"emptyLinePlaceholder":752},[62,4417,4418,4421,4424,4427,4430],{"class":64,"line":83},[62,4419,4420],{"class":79},"mcpServer ",[62,4422,4423],{"class":166},":=",[62,4425,4426],{"class":79}," server.",[62,4428,4429],{"class":75},"NewMCPServer",[62,4431,4432],{"class":79},"(\n",[62,4434,4435,4438,4441],{"class":64,"line":91},[62,4436,4437],{"class":273},"    \"my-app\"",[62,4439,4440],{"class":79},",    ",[62,4442,4443],{"class":68},"\u002F\u002F 服务器名称\n",[62,4445,4446,4449,4452],{"class":64,"line":99},[62,4447,4448],{"class":273},"    \"1.0.0\"",[62,4450,4451],{"class":79},",     ",[62,4453,4454],{"class":68},"\u002F\u002F 版本\n",[62,4456,4457,4460,4463],{"class":64,"line":124},[62,4458,4459],{"class":79},"    server.",[62,4461,4462],{"class":75},"WithLogging",[62,4464,4465],{"class":79},"(),\n",[62,4467,4468,4470,4473],{"class":64,"line":130},[62,4469,4459],{"class":79},[62,4471,4472],{"class":75},"WithRecovery",[62,4474,4465],{"class":79},[62,4476,4477,4479,4482,4484,4487],{"class":64,"line":136},[62,4478,4459],{"class":79},[62,4480,4481],{"class":75},"WithInstructions",[62,4483,173],{"class":79},[62,4485,4486],{"class":273},"\"这是一个 MCP 服务器示例\"",[62,4488,4489],{"class":79},"),\n",[62,4491,4492],{"class":64,"line":402},[62,4493,494],{"class":79},[15,4495,4496,4498,4499,4502,4503,4506],{},[33,4497,4429],{}," 创建一个 MCP 协议实例，它负责处理工具发现（",[33,4500,4501],{},"tools\u002Flist","）和工具调用（",[33,4504,4505],{},"tools\u002Fcall","）的协议逻辑。",[1503,4508,4510],{"id":4509},"_3-注册工具","3. 注册工具",[53,4512,4514],{"className":4396,"code":4513,"language":1398,"meta":58,"style":58},"import \"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp\"\n\n\u002F\u002F 定义工具\nlistTool := mcp.NewTool(\"list_items\",\n    mcp.WithDescription(\"列出所有项目\"),\n)\n\n\u002F\u002F 注册处理器\nmcpServer.AddTool(listTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n    \u002F\u002F 业务逻辑\n    return mcp.NewToolResultText(\"返回结果\"), nil\n})\n",[33,4515,4516,4527,4531,4536,4556,4571,4575,4579,4584,4645,4650,4671],{"__ignoreMap":58},[62,4517,4518,4520,4522,4525],{"class":64,"line":65},[62,4519,4404],{"class":166},[62,4521,1304],{"class":273},[62,4523,4524],{"class":75},"github.com\u002Fmark3labs\u002Fmcp-go\u002Fmcp",[62,4526,1501],{"class":273},[62,4528,4529],{"class":64,"line":72},[62,4530,753],{"emptyLinePlaceholder":752},[62,4532,4533],{"class":64,"line":83},[62,4534,4535],{"class":68},"\u002F\u002F 定义工具\n",[62,4537,4538,4541,4543,4546,4549,4551,4554],{"class":64,"line":91},[62,4539,4540],{"class":79},"listTool ",[62,4542,4423],{"class":166},[62,4544,4545],{"class":79}," mcp.",[62,4547,4548],{"class":75},"NewTool",[62,4550,173],{"class":79},[62,4552,4553],{"class":273},"\"list_items\"",[62,4555,2912],{"class":79},[62,4557,4558,4561,4564,4566,4569],{"class":64,"line":99},[62,4559,4560],{"class":79},"    mcp.",[62,4562,4563],{"class":75},"WithDescription",[62,4565,173],{"class":79},[62,4567,4568],{"class":273},"\"列出所有项目\"",[62,4570,4489],{"class":79},[62,4572,4573],{"class":64,"line":124},[62,4574,494],{"class":79},[62,4576,4577],{"class":64,"line":130},[62,4578,753],{"emptyLinePlaceholder":752},[62,4580,4581],{"class":64,"line":136},[62,4582,4583],{"class":68},"\u002F\u002F 注册处理器\n",[62,4585,4586,4589,4592,4595,4598,4600,4603,4606,4608,4611,4613,4616,4619,4621,4624,4627,4630,4633,4635,4638,4640,4643],{"class":64,"line":402},[62,4587,4588],{"class":79},"mcpServer.",[62,4590,4591],{"class":75},"AddTool",[62,4593,4594],{"class":79},"(listTool, ",[62,4596,4597],{"class":166},"func",[62,4599,173],{"class":79},[62,4601,4602],{"class":176},"ctx",[62,4604,4605],{"class":75}," context",[62,4607,299],{"class":79},[62,4609,4610],{"class":75},"Context",[62,4612,186],{"class":79},[62,4614,4615],{"class":176},"req",[62,4617,4618],{"class":75}," mcp",[62,4620,299],{"class":79},[62,4622,4623],{"class":75},"CallToolRequest",[62,4625,4626],{"class":79},") (",[62,4628,4629],{"class":166},"*",[62,4631,4632],{"class":75},"mcp",[62,4634,299],{"class":79},[62,4636,4637],{"class":75},"CallToolResult",[62,4639,186],{"class":79},[62,4641,4642],{"class":166},"error",[62,4644,4224],{"class":79},[62,4646,4647],{"class":64,"line":407},[62,4648,4649],{"class":68},"    \u002F\u002F 业务逻辑\n",[62,4651,4652,4655,4657,4660,4662,4665,4668],{"class":64,"line":433},[62,4653,4654],{"class":166},"    return",[62,4656,4545],{"class":79},[62,4658,4659],{"class":75},"NewToolResultText",[62,4661,173],{"class":79},[62,4663,4664],{"class":273},"\"返回结果\"",[62,4666,4667],{"class":79},"), ",[62,4669,4670],{"class":114},"nil\n",[62,4672,4673],{"class":64,"line":438},[62,4674,738],{"class":79},[15,4676,4677],{},"带参数的工具：",[53,4679,4681],{"className":4396,"code":4680,"language":1398,"meta":58,"style":58},"getTool := mcp.NewTool(\"get_item\",\n    mcp.WithDescription(\"获取指定项目的详情\"),\n    mcp.WithString(\"item_id\",\n        mcp.Required(),\n        mcp.Description(\"项目 ID\"),\n    ),\n)\n\nmcpServer.AddTool(getTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n    itemID, err := req.RequireString(\"item_id\")\n    if err != nil {\n        return mcp.NewToolResultError(\"缺少 item_id 参数\"), nil\n    }\n    \u002F\u002F 查询并返回\n    return mcp.NewToolResultText(\"项目详情...\"), nil\n})\n",[33,4682,4683,4701,4714,4728,4738,4752,4757,4761,4765,4812,4831,4846,4865,4869,4874,4891],{"__ignoreMap":58},[62,4684,4685,4688,4690,4692,4694,4696,4699],{"class":64,"line":65},[62,4686,4687],{"class":79},"getTool ",[62,4689,4423],{"class":166},[62,4691,4545],{"class":79},[62,4693,4548],{"class":75},[62,4695,173],{"class":79},[62,4697,4698],{"class":273},"\"get_item\"",[62,4700,2912],{"class":79},[62,4702,4703,4705,4707,4709,4712],{"class":64,"line":72},[62,4704,4560],{"class":79},[62,4706,4563],{"class":75},[62,4708,173],{"class":79},[62,4710,4711],{"class":273},"\"获取指定项目的详情\"",[62,4713,4489],{"class":79},[62,4715,4716,4718,4721,4723,4726],{"class":64,"line":83},[62,4717,4560],{"class":79},[62,4719,4720],{"class":75},"WithString",[62,4722,173],{"class":79},[62,4724,4725],{"class":273},"\"item_id\"",[62,4727,2912],{"class":79},[62,4729,4730,4733,4736],{"class":64,"line":91},[62,4731,4732],{"class":79},"        mcp.",[62,4734,4735],{"class":75},"Required",[62,4737,4465],{"class":79},[62,4739,4740,4742,4745,4747,4750],{"class":64,"line":99},[62,4741,4732],{"class":79},[62,4743,4744],{"class":75},"Description",[62,4746,173],{"class":79},[62,4748,4749],{"class":273},"\"项目 ID\"",[62,4751,4489],{"class":79},[62,4753,4754],{"class":64,"line":124},[62,4755,4756],{"class":79},"    ),\n",[62,4758,4759],{"class":64,"line":130},[62,4760,494],{"class":79},[62,4762,4763],{"class":64,"line":136},[62,4764,753],{"emptyLinePlaceholder":752},[62,4766,4767,4769,4771,4774,4776,4778,4780,4782,4784,4786,4788,4790,4792,4794,4796,4798,4800,4802,4804,4806,4808,4810],{"class":64,"line":402},[62,4768,4588],{"class":79},[62,4770,4591],{"class":75},[62,4772,4773],{"class":79},"(getTool, ",[62,4775,4597],{"class":166},[62,4777,173],{"class":79},[62,4779,4602],{"class":176},[62,4781,4605],{"class":75},[62,4783,299],{"class":79},[62,4785,4610],{"class":75},[62,4787,186],{"class":79},[62,4789,4615],{"class":176},[62,4791,4618],{"class":75},[62,4793,299],{"class":79},[62,4795,4623],{"class":75},[62,4797,4626],{"class":79},[62,4799,4629],{"class":166},[62,4801,4632],{"class":75},[62,4803,299],{"class":79},[62,4805,4637],{"class":75},[62,4807,186],{"class":79},[62,4809,4642],{"class":166},[62,4811,4224],{"class":79},[62,4813,4814,4817,4819,4822,4825,4827,4829],{"class":64,"line":407},[62,4815,4816],{"class":79},"    itemID, err ",[62,4818,4423],{"class":166},[62,4820,4821],{"class":79}," req.",[62,4823,4824],{"class":75},"RequireString",[62,4826,173],{"class":79},[62,4828,4725],{"class":273},[62,4830,494],{"class":79},[62,4832,4833,4835,4838,4841,4844],{"class":64,"line":433},[62,4834,253],{"class":166},[62,4836,4837],{"class":79}," err ",[62,4839,4840],{"class":166},"!=",[62,4842,4843],{"class":114}," nil",[62,4845,620],{"class":79},[62,4847,4848,4851,4853,4856,4858,4861,4863],{"class":64,"line":438},[62,4849,4850],{"class":166},"        return",[62,4852,4545],{"class":79},[62,4854,4855],{"class":75},"NewToolResultError",[62,4857,173],{"class":79},[62,4859,4860],{"class":273},"\"缺少 item_id 参数\"",[62,4862,4667],{"class":79},[62,4864,4670],{"class":114},[62,4866,4867],{"class":64,"line":446},[62,4868,127],{"class":79},[62,4870,4871],{"class":64,"line":1448},[62,4872,4873],{"class":68},"    \u002F\u002F 查询并返回\n",[62,4875,4876,4878,4880,4882,4884,4887,4889],{"class":64,"line":1459},[62,4877,4654],{"class":166},[62,4879,4545],{"class":79},[62,4881,4659],{"class":75},[62,4883,173],{"class":79},[62,4885,4886],{"class":273},"\"项目详情...\"",[62,4888,4667],{"class":79},[62,4890,4670],{"class":114},[62,4892,4893],{"class":64,"line":1470},[62,4894,738],{"class":79},[1503,4896,4898],{"id":4897},"_4-挂载到-gin-路由","4. 挂载到 Gin 路由",[15,4900,4901,4902,4905,4906,4909],{},"关键一步：用 ",[33,4903,4904],{},"server.NewStreamableHTTPServer"," 把 MCP 实例转成 HTTP Handler，再用 ",[33,4907,4908],{},"gin.WrapH"," 适配 Gin。",[53,4911,4913],{"className":4396,"code":4912,"language":1398,"meta":58,"style":58},"import \"github.com\u002Fmark3labs\u002Fmcp-go\u002Fserver\"\n\nmcpHTTPServer := server.NewStreamableHTTPServer(mcpServer,\n    server.WithEndpointPath(\"\u002Fmcp\"),\n    server.WithHeartbeatInterval(30*time.Second),\n)\n\n\u002F\u002F 挂载到 Gin\nr.Any(\"\u002Fmcp\", gin.WrapH(mcpHTTPServer))\n",[33,4914,4915,4925,4929,4944,4958,4975,4979,4983,4988],{"__ignoreMap":58},[62,4916,4917,4919,4921,4923],{"class":64,"line":65},[62,4918,4404],{"class":166},[62,4920,1304],{"class":273},[62,4922,4409],{"class":75},[62,4924,1501],{"class":273},[62,4926,4927],{"class":64,"line":72},[62,4928,753],{"emptyLinePlaceholder":752},[62,4930,4931,4934,4936,4938,4941],{"class":64,"line":83},[62,4932,4933],{"class":79},"mcpHTTPServer ",[62,4935,4423],{"class":166},[62,4937,4426],{"class":79},[62,4939,4940],{"class":75},"NewStreamableHTTPServer",[62,4942,4943],{"class":79},"(mcpServer,\n",[62,4945,4946,4948,4951,4953,4956],{"class":64,"line":91},[62,4947,4459],{"class":79},[62,4949,4950],{"class":75},"WithEndpointPath",[62,4952,173],{"class":79},[62,4954,4955],{"class":273},"\"\u002Fmcp\"",[62,4957,4489],{"class":79},[62,4959,4960,4962,4965,4967,4970,4972],{"class":64,"line":99},[62,4961,4459],{"class":79},[62,4963,4964],{"class":75},"WithHeartbeatInterval",[62,4966,173],{"class":79},[62,4968,4969],{"class":114},"30",[62,4971,4629],{"class":166},[62,4973,4974],{"class":79},"time.Second),\n",[62,4976,4977],{"class":64,"line":124},[62,4978,494],{"class":79},[62,4980,4981],{"class":64,"line":130},[62,4982,753],{"emptyLinePlaceholder":752},[62,4984,4985],{"class":64,"line":136},[62,4986,4987],{"class":68},"\u002F\u002F 挂载到 Gin\n",[62,4989,4990,4993,4996,4998,5000,5003,5006],{"class":64,"line":402},[62,4991,4992],{"class":79},"r.",[62,4994,4995],{"class":75},"Any",[62,4997,173],{"class":79},[62,4999,4955],{"class":273},[62,5001,5002],{"class":79},", gin.",[62,5004,5005],{"class":75},"WrapH",[62,5007,5008],{"class":79},"(mcpHTTPServer))\n",[15,5010,5011,5012,5015],{},"就这样，",[33,5013,5014],{},"POST \u002Fmcp"," 端点就生效了。AI 客户端通过这个 URL 发送 JSON-RPC 请求，MCP 服务器自动处理协议握手、工具列表返回、工具调用路由。",[1503,5017,5019],{"id":5018},"_5-完整的服务启动代码","5. 完整的服务启动代码",[53,5021,5023],{"className":4396,"code":5022,"language":1398,"meta":58,"style":58},"func main() {\n    r := gin.Default()\n\n    \u002F\u002F 创建 MCP 服务器\n    mcpServer := server.NewMCPServer(\"my-app\", \"1.0.0\",\n        server.WithLogging(),\n        server.WithRecovery(),\n    )\n\n    \u002F\u002F 注册工具\n    registerTools(mcpServer)\n\n    \u002F\u002F 挂载到 Gin\n    mcpHTTP := server.NewStreamableHTTPServer(mcpServer,\n        server.WithEndpointPath(\"\u002Fmcp\"),\n        server.WithHeartbeatInterval(30*time.Second),\n    )\n    r.Any(\"\u002Fmcp\", gin.WrapH(mcpHTTP))\n\n    \u002F\u002F 其他业务路由\n    r.GET(\"\u002Fapi\u002Fhealth\", func(c *gin.Context) {\n        c.JSON(200, gin.H{\"status\": \"ok\"})\n    })\n\n    r.Run(\":8080\")\n}\n",[33,5024,5025,5034,5049,5053,5058,5081,5090,5098,5103,5107,5112,5120,5124,5129,5142,5154,5168,5172,5190,5194,5199,5232,5267,5272,5276,5290],{"__ignoreMap":58},[62,5026,5027,5029,5032],{"class":64,"line":65},[62,5028,4597],{"class":166},[62,5030,5031],{"class":75}," main",[62,5033,2549],{"class":79},[62,5035,5036,5039,5041,5044,5047],{"class":64,"line":72},[62,5037,5038],{"class":79},"    r ",[62,5040,4423],{"class":166},[62,5042,5043],{"class":79}," gin.",[62,5045,5046],{"class":75},"Default",[62,5048,374],{"class":79},[62,5050,5051],{"class":64,"line":83},[62,5052,753],{"emptyLinePlaceholder":752},[62,5054,5055],{"class":64,"line":91},[62,5056,5057],{"class":68},"    \u002F\u002F 创建 MCP 服务器\n",[62,5059,5060,5063,5065,5067,5069,5071,5074,5076,5079],{"class":64,"line":99},[62,5061,5062],{"class":79},"    mcpServer ",[62,5064,4423],{"class":166},[62,5066,4426],{"class":79},[62,5068,4429],{"class":75},[62,5070,173],{"class":79},[62,5072,5073],{"class":273},"\"my-app\"",[62,5075,186],{"class":79},[62,5077,5078],{"class":273},"\"1.0.0\"",[62,5080,2912],{"class":79},[62,5082,5083,5086,5088],{"class":64,"line":124},[62,5084,5085],{"class":79},"        server.",[62,5087,4462],{"class":75},[62,5089,4465],{"class":79},[62,5091,5092,5094,5096],{"class":64,"line":130},[62,5093,5085],{"class":79},[62,5095,4472],{"class":75},[62,5097,4465],{"class":79},[62,5099,5100],{"class":64,"line":136},[62,5101,5102],{"class":79},"    )\n",[62,5104,5105],{"class":64,"line":402},[62,5106,753],{"emptyLinePlaceholder":752},[62,5108,5109],{"class":64,"line":407},[62,5110,5111],{"class":68},"    \u002F\u002F 注册工具\n",[62,5113,5114,5117],{"class":64,"line":433},[62,5115,5116],{"class":75},"    registerTools",[62,5118,5119],{"class":79},"(mcpServer)\n",[62,5121,5122],{"class":64,"line":438},[62,5123,753],{"emptyLinePlaceholder":752},[62,5125,5126],{"class":64,"line":446},[62,5127,5128],{"class":68},"    \u002F\u002F 挂载到 Gin\n",[62,5130,5131,5134,5136,5138,5140],{"class":64,"line":1448},[62,5132,5133],{"class":79},"    mcpHTTP ",[62,5135,4423],{"class":166},[62,5137,4426],{"class":79},[62,5139,4940],{"class":75},[62,5141,4943],{"class":79},[62,5143,5144,5146,5148,5150,5152],{"class":64,"line":1459},[62,5145,5085],{"class":79},[62,5147,4950],{"class":75},[62,5149,173],{"class":79},[62,5151,4955],{"class":273},[62,5153,4489],{"class":79},[62,5155,5156,5158,5160,5162,5164,5166],{"class":64,"line":1470},[62,5157,5085],{"class":79},[62,5159,4964],{"class":75},[62,5161,173],{"class":79},[62,5163,4969],{"class":114},[62,5165,4629],{"class":166},[62,5167,4974],{"class":79},[62,5169,5170],{"class":64,"line":1475},[62,5171,5102],{"class":79},[62,5173,5174,5177,5179,5181,5183,5185,5187],{"class":64,"line":1484},[62,5175,5176],{"class":79},"    r.",[62,5178,4995],{"class":75},[62,5180,173],{"class":79},[62,5182,4955],{"class":273},[62,5184,5002],{"class":79},[62,5186,5005],{"class":75},[62,5188,5189],{"class":79},"(mcpHTTP))\n",[62,5191,5192],{"class":64,"line":1489},[62,5193,753],{"emptyLinePlaceholder":752},[62,5195,5196],{"class":64,"line":1695},[62,5197,5198],{"class":68},"    \u002F\u002F 其他业务路由\n",[62,5200,5201,5203,5206,5208,5211,5213,5215,5217,5220,5223,5226,5228,5230],{"class":64,"line":1703},[62,5202,5176],{"class":79},[62,5204,5205],{"class":75},"GET",[62,5207,173],{"class":79},[62,5209,5210],{"class":273},"\"\u002Fapi\u002Fhealth\"",[62,5212,186],{"class":79},[62,5214,4597],{"class":166},[62,5216,173],{"class":79},[62,5218,5219],{"class":176},"c",[62,5221,5222],{"class":166}," *",[62,5224,5225],{"class":75},"gin",[62,5227,299],{"class":79},[62,5229,4610],{"class":75},[62,5231,4224],{"class":79},[62,5233,5234,5237,5240,5242,5245,5247,5249,5251,5254,5257,5260,5262,5265],{"class":64,"line":1708},[62,5235,5236],{"class":79},"        c.",[62,5238,5239],{"class":75},"JSON",[62,5241,173],{"class":79},[62,5243,5244],{"class":114},"200",[62,5246,186],{"class":79},[62,5248,5225],{"class":75},[62,5250,299],{"class":79},[62,5252,5253],{"class":75},"H",[62,5255,5256],{"class":79},"{",[62,5258,5259],{"class":273},"\"status\"",[62,5261,111],{"class":79},[62,5263,5264],{"class":273},"\"ok\"",[62,5266,738],{"class":79},[62,5268,5269],{"class":64,"line":2336},[62,5270,5271],{"class":79},"    })\n",[62,5273,5274],{"class":64,"line":2350},[62,5275,753],{"emptyLinePlaceholder":752},[62,5277,5278,5280,5283,5285,5288],{"class":64,"line":2357},[62,5279,5176],{"class":79},[62,5281,5282],{"class":75},"Run",[62,5284,173],{"class":79},[62,5286,5287],{"class":273},"\":8080\"",[62,5289,494],{"class":79},[62,5291,5292],{"class":64,"line":2362},[62,5293,139],{"class":79},[15,5295,5296],{},"此时启动服务，用 curl 测试：",[53,5298,5300],{"className":1275,"code":5299,"language":1277,"meta":58,"style":58},"# 发现工具列表\ncurl -X POST http:\u002F\u002Flocalhost:8080\u002Fmcp \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n\n# 调用工具\ncurl -X POST http:\u002F\u002Flocalhost:8080\u002Fmcp \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[33,5301,5302,5307,5324,5334,5342,5346,5351,5363,5371],{"__ignoreMap":58},[62,5303,5304],{"class":64,"line":65},[62,5305,5306],{"class":68},"# 发现工具列表\n",[62,5308,5309,5312,5315,5318,5321],{"class":64,"line":72},[62,5310,5311],{"class":75},"curl",[62,5313,5314],{"class":114}," -X",[62,5316,5317],{"class":273}," POST",[62,5319,5320],{"class":273}," http:\u002F\u002Flocalhost:8080\u002Fmcp",[62,5322,5323],{"class":114}," \\\n",[62,5325,5326,5329,5332],{"class":64,"line":83},[62,5327,5328],{"class":114},"  -H",[62,5330,5331],{"class":273}," \"Content-Type: application\u002Fjson\"",[62,5333,5323],{"class":114},[62,5335,5336,5339],{"class":64,"line":91},[62,5337,5338],{"class":114},"  -d",[62,5340,5341],{"class":273}," '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools\u002Flist\"}'\n",[62,5343,5344],{"class":64,"line":99},[62,5345,753],{"emptyLinePlaceholder":752},[62,5347,5348],{"class":64,"line":124},[62,5349,5350],{"class":68},"# 调用工具\n",[62,5352,5353,5355,5357,5359,5361],{"class":64,"line":130},[62,5354,5311],{"class":75},[62,5356,5314],{"class":114},[62,5358,5317],{"class":273},[62,5360,5320],{"class":273},[62,5362,5323],{"class":114},[62,5364,5365,5367,5369],{"class":64,"line":136},[62,5366,5328],{"class":114},[62,5368,5331],{"class":273},[62,5370,5323],{"class":114},[62,5372,5373,5375],{"class":64,"line":402},[62,5374,5338],{"class":114},[62,5376,5377],{"class":273}," '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_items\",\"arguments\":{}}}'\n",[26,5379,5381],{"id":5380},"拓展接入鉴权","拓展：接入鉴权",[15,5383,5384],{},"裸奔的 MCP 端点任何人都能调用，生产环境必须加鉴权。思路和普通 API 鉴权一致：Bearer Token。",[1503,5386,1177],{"id":1177},[15,5388,5389,5390,5393,5394,5397],{},"MCP 的工具处理器签名为 ",[33,5391,5392],{},"func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)","，它不接收 Gin 的 ",[33,5395,5396],{},"*gin.Context","，所以无法直接在工具处理器里读取请求头。",[1503,5399,5401],{"id":5400},"解决方案中间件传递用户信息","解决方案：中间件传递用户信息",[15,5403,5404,5405,41],{},"用两层中间件：第一层做 Gin 的 Token 验证，第二层把用户信息从 Gin Context 复制到 ",[33,5406,5407],{},"request.Context",[53,5409,5411],{"className":4396,"code":5410,"language":1398,"meta":58,"style":58},"\u002F\u002F Gin 鉴权中间件\nfunc AuthRequired(authService *AuthService) gin.HandlerFunc {\n    return func(c *gin.Context) {\n        authHeader := c.GetHeader(\"Authorization\")\n        if authHeader == \"\" {\n            c.AbortWithStatusJSON(401, gin.H{\"message\": \"缺少认证令牌\"})\n            return\n        }\n\n        parts := strings.SplitN(authHeader, \" \", 2)\n        if len(parts) != 2 || !strings.EqualFold(parts[0], \"Bearer\") {\n            c.AbortWithStatusJSON(401, gin.H{\"message\": \"格式错误，请使用 Bearer \u003Ctoken>\"})\n            return\n        }\n\n        user, err := authService.GetUserByToken(parts[1])\n        if err != nil {\n            c.AbortWithStatusJSON(401, gin.H{\"message\": \"无效的认证令牌\"})\n            return\n        }\n\n        c.Set(\"userID\", user.ID)\n        c.Set(\"username\", user.Username)\n        c.Next()\n    }\n}\n",[33,5412,5413,5418,5446,5467,5487,5503,5536,5541,5546,5550,5575,5615,5644,5648,5652,5656,5676,5688,5717,5721,5725,5729,5744,5758,5767,5771],{"__ignoreMap":58},[62,5414,5415],{"class":64,"line":65},[62,5416,5417],{"class":68},"\u002F\u002F Gin 鉴权中间件\n",[62,5419,5420,5422,5425,5427,5430,5432,5435,5437,5439,5441,5444],{"class":64,"line":72},[62,5421,4597],{"class":166},[62,5423,5424],{"class":75}," AuthRequired",[62,5426,173],{"class":79},[62,5428,5429],{"class":176},"authService",[62,5431,5222],{"class":166},[62,5433,5434],{"class":75},"AuthService",[62,5436,654],{"class":79},[62,5438,5225],{"class":75},[62,5440,299],{"class":79},[62,5442,5443],{"class":75},"HandlerFunc",[62,5445,620],{"class":79},[62,5447,5448,5450,5453,5455,5457,5459,5461,5463,5465],{"class":64,"line":83},[62,5449,4654],{"class":166},[62,5451,5452],{"class":166}," func",[62,5454,173],{"class":79},[62,5456,5219],{"class":176},[62,5458,5222],{"class":166},[62,5460,5225],{"class":75},[62,5462,299],{"class":79},[62,5464,4610],{"class":75},[62,5466,4224],{"class":79},[62,5468,5469,5472,5474,5477,5480,5482,5485],{"class":64,"line":91},[62,5470,5471],{"class":79},"        authHeader ",[62,5473,4423],{"class":166},[62,5475,5476],{"class":79}," c.",[62,5478,5479],{"class":75},"GetHeader",[62,5481,173],{"class":79},[62,5483,5484],{"class":273},"\"Authorization\"",[62,5486,494],{"class":79},[62,5488,5489,5492,5495,5498,5501],{"class":64,"line":99},[62,5490,5491],{"class":166},"        if",[62,5493,5494],{"class":79}," authHeader ",[62,5496,5497],{"class":166},"==",[62,5499,5500],{"class":273}," \"\"",[62,5502,620],{"class":79},[62,5504,5505,5508,5511,5513,5516,5518,5520,5522,5524,5526,5529,5531,5534],{"class":64,"line":124},[62,5506,5507],{"class":79},"            c.",[62,5509,5510],{"class":75},"AbortWithStatusJSON",[62,5512,173],{"class":79},[62,5514,5515],{"class":114},"401",[62,5517,186],{"class":79},[62,5519,5225],{"class":75},[62,5521,299],{"class":79},[62,5523,5253],{"class":75},[62,5525,5256],{"class":79},[62,5527,5528],{"class":273},"\"message\"",[62,5530,111],{"class":79},[62,5532,5533],{"class":273},"\"缺少认证令牌\"",[62,5535,738],{"class":79},[62,5537,5538],{"class":64,"line":130},[62,5539,5540],{"class":166},"            return\n",[62,5542,5543],{"class":64,"line":136},[62,5544,5545],{"class":79},"        }\n",[62,5547,5548],{"class":64,"line":402},[62,5549,753],{"emptyLinePlaceholder":752},[62,5551,5552,5555,5557,5560,5563,5566,5569,5571,5573],{"class":64,"line":407},[62,5553,5554],{"class":79},"        parts ",[62,5556,4423],{"class":166},[62,5558,5559],{"class":79}," strings.",[62,5561,5562],{"class":75},"SplitN",[62,5564,5565],{"class":79},"(authHeader, ",[62,5567,5568],{"class":273},"\" \"",[62,5570,186],{"class":79},[62,5572,365],{"class":114},[62,5574,494],{"class":79},[62,5576,5577,5579,5582,5585,5587,5590,5593,5596,5599,5602,5605,5607,5610,5613],{"class":64,"line":433},[62,5578,5491],{"class":166},[62,5580,5581],{"class":75}," len",[62,5583,5584],{"class":79},"(parts) ",[62,5586,4840],{"class":166},[62,5588,5589],{"class":114}," 2",[62,5591,5592],{"class":166}," ||",[62,5594,5595],{"class":166}," !",[62,5597,5598],{"class":79},"strings.",[62,5600,5601],{"class":75},"EqualFold",[62,5603,5604],{"class":79},"(parts[",[62,5606,264],{"class":114},[62,5608,5609],{"class":79},"], ",[62,5611,5612],{"class":273},"\"Bearer\"",[62,5614,4224],{"class":79},[62,5616,5617,5619,5621,5623,5625,5627,5629,5631,5633,5635,5637,5639,5642],{"class":64,"line":438},[62,5618,5507],{"class":79},[62,5620,5510],{"class":75},[62,5622,173],{"class":79},[62,5624,5515],{"class":114},[62,5626,186],{"class":79},[62,5628,5225],{"class":75},[62,5630,299],{"class":79},[62,5632,5253],{"class":75},[62,5634,5256],{"class":79},[62,5636,5528],{"class":273},[62,5638,111],{"class":79},[62,5640,5641],{"class":273},"\"格式错误，请使用 Bearer \u003Ctoken>\"",[62,5643,738],{"class":79},[62,5645,5646],{"class":64,"line":446},[62,5647,5540],{"class":166},[62,5649,5650],{"class":64,"line":1448},[62,5651,5545],{"class":79},[62,5653,5654],{"class":64,"line":1459},[62,5655,753],{"emptyLinePlaceholder":752},[62,5657,5658,5661,5663,5666,5669,5671,5673],{"class":64,"line":1470},[62,5659,5660],{"class":79},"        user, err ",[62,5662,4423],{"class":166},[62,5664,5665],{"class":79}," authService.",[62,5667,5668],{"class":75},"GetUserByToken",[62,5670,5604],{"class":79},[62,5672,339],{"class":114},[62,5674,5675],{"class":79},"])\n",[62,5677,5678,5680,5682,5684,5686],{"class":64,"line":1475},[62,5679,5491],{"class":166},[62,5681,4837],{"class":79},[62,5683,4840],{"class":166},[62,5685,4843],{"class":114},[62,5687,620],{"class":79},[62,5689,5690,5692,5694,5696,5698,5700,5702,5704,5706,5708,5710,5712,5715],{"class":64,"line":1484},[62,5691,5507],{"class":79},[62,5693,5510],{"class":75},[62,5695,173],{"class":79},[62,5697,5515],{"class":114},[62,5699,186],{"class":79},[62,5701,5225],{"class":75},[62,5703,299],{"class":79},[62,5705,5253],{"class":75},[62,5707,5256],{"class":79},[62,5709,5528],{"class":273},[62,5711,111],{"class":79},[62,5713,5714],{"class":273},"\"无效的认证令牌\"",[62,5716,738],{"class":79},[62,5718,5719],{"class":64,"line":1489},[62,5720,5540],{"class":166},[62,5722,5723],{"class":64,"line":1695},[62,5724,5545],{"class":79},[62,5726,5727],{"class":64,"line":1703},[62,5728,753],{"emptyLinePlaceholder":752},[62,5730,5731,5733,5736,5738,5741],{"class":64,"line":1708},[62,5732,5236],{"class":79},[62,5734,5735],{"class":75},"Set",[62,5737,173],{"class":79},[62,5739,5740],{"class":273},"\"userID\"",[62,5742,5743],{"class":79},", user.ID)\n",[62,5745,5746,5748,5750,5752,5755],{"class":64,"line":2336},[62,5747,5236],{"class":79},[62,5749,5735],{"class":75},[62,5751,173],{"class":79},[62,5753,5754],{"class":273},"\"username\"",[62,5756,5757],{"class":79},", user.Username)\n",[62,5759,5760,5762,5765],{"class":64,"line":2350},[62,5761,5236],{"class":79},[62,5763,5764],{"class":75},"Next",[62,5766,374],{"class":79},[62,5768,5769],{"class":64,"line":2357},[62,5770,127],{"class":79},[62,5772,5773],{"class":64,"line":2362},[62,5774,139],{"class":79},[53,5776,5778],{"className":4396,"code":5777,"language":1398,"meta":58,"style":58},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\ntype ctxKey string\nconst (\n    ctxUserID   ctxKey = \"userID\"\n    ctxUsername ctxKey = \"username\"\n)\n\nfunc WithUserContext() gin.HandlerFunc {\n    return func(c *gin.Context) {\n        ctx := c.Request.Context()\n        ctx = context.WithValue(ctx, ctxUserID, c.GetUint(\"userID\"))\n        ctx = context.WithValue(ctx, ctxUsername, c.GetString(\"username\"))\n        c.Request = c.Request.WithContext(ctx)\n        c.Next()\n    }\n}\n",[33,5779,5780,5785,5796,5803,5816,5828,5832,5836,5854,5874,5888,5912,5934,5949,5957,5961],{"__ignoreMap":58},[62,5781,5782],{"class":64,"line":65},[62,5783,5784],{"class":68},"\u002F\u002F 用户信息桥接中间件：Gin Context → request.Context\n",[62,5786,5787,5790,5793],{"class":64,"line":72},[62,5788,5789],{"class":166},"type",[62,5791,5792],{"class":75}," ctxKey",[62,5794,5795],{"class":166}," string\n",[62,5797,5798,5800],{"class":64,"line":83},[62,5799,415],{"class":166},[62,5801,5802],{"class":79}," (\n",[62,5804,5805,5808,5811,5813],{"class":64,"line":91},[62,5806,5807],{"class":114},"    ctxUserID",[62,5809,5810],{"class":75},"   ctxKey",[62,5812,321],{"class":166},[62,5814,5815],{"class":273}," \"userID\"\n",[62,5817,5818,5821,5823,5825],{"class":64,"line":99},[62,5819,5820],{"class":114},"    ctxUsername",[62,5822,5792],{"class":75},[62,5824,321],{"class":166},[62,5826,5827],{"class":273}," \"username\"\n",[62,5829,5830],{"class":64,"line":124},[62,5831,494],{"class":79},[62,5833,5834],{"class":64,"line":130},[62,5835,753],{"emptyLinePlaceholder":752},[62,5837,5838,5840,5843,5846,5848,5850,5852],{"class":64,"line":136},[62,5839,4597],{"class":166},[62,5841,5842],{"class":75}," WithUserContext",[62,5844,5845],{"class":79},"() ",[62,5847,5225],{"class":75},[62,5849,299],{"class":79},[62,5851,5443],{"class":75},[62,5853,620],{"class":79},[62,5855,5856,5858,5860,5862,5864,5866,5868,5870,5872],{"class":64,"line":402},[62,5857,4654],{"class":166},[62,5859,5452],{"class":166},[62,5861,173],{"class":79},[62,5863,5219],{"class":176},[62,5865,5222],{"class":166},[62,5867,5225],{"class":75},[62,5869,299],{"class":79},[62,5871,4610],{"class":75},[62,5873,4224],{"class":79},[62,5875,5876,5879,5881,5884,5886],{"class":64,"line":407},[62,5877,5878],{"class":79},"        ctx ",[62,5880,4423],{"class":166},[62,5882,5883],{"class":79}," c.Request.",[62,5885,4610],{"class":75},[62,5887,374],{"class":79},[62,5889,5890,5892,5894,5897,5900,5903,5906,5908,5910],{"class":64,"line":433},[62,5891,5878],{"class":79},[62,5893,200],{"class":166},[62,5895,5896],{"class":79}," context.",[62,5898,5899],{"class":75},"WithValue",[62,5901,5902],{"class":79},"(ctx, ctxUserID, c.",[62,5904,5905],{"class":75},"GetUint",[62,5907,173],{"class":79},[62,5909,5740],{"class":273},[62,5911,342],{"class":79},[62,5913,5914,5916,5918,5920,5922,5925,5928,5930,5932],{"class":64,"line":438},[62,5915,5878],{"class":79},[62,5917,200],{"class":166},[62,5919,5896],{"class":79},[62,5921,5899],{"class":75},[62,5923,5924],{"class":79},"(ctx, ctxUsername, c.",[62,5926,5927],{"class":75},"GetString",[62,5929,173],{"class":79},[62,5931,5754],{"class":273},[62,5933,342],{"class":79},[62,5935,5936,5939,5941,5943,5946],{"class":64,"line":446},[62,5937,5938],{"class":79},"        c.Request ",[62,5940,200],{"class":166},[62,5942,5883],{"class":79},[62,5944,5945],{"class":75},"WithContext",[62,5947,5948],{"class":79},"(ctx)\n",[62,5950,5951,5953,5955],{"class":64,"line":1448},[62,5952,5236],{"class":79},[62,5954,5764],{"class":75},[62,5956,374],{"class":79},[62,5958,5959],{"class":64,"line":1459},[62,5960,127],{"class":79},[62,5962,5963],{"class":64,"line":1470},[62,5964,139],{"class":79},[1503,5966,5967],{"id":5967},"组合使用",[53,5969,5971],{"className":4396,"code":5970,"language":1398,"meta":58,"style":58},"r.Any(\"\u002Fmcp\",\n    AuthRequired(authService),  \u002F\u002F 1. 验证 Token\n    WithUserContext(),           \u002F\u002F 2. 桥接用户信息到 request.Context\n    gin.WrapH(mcpHTTP),         \u002F\u002F 3. MCP 处理\n)\n",[33,5972,5973,5985,5996,6007,6020],{"__ignoreMap":58},[62,5974,5975,5977,5979,5981,5983],{"class":64,"line":65},[62,5976,4992],{"class":79},[62,5978,4995],{"class":75},[62,5980,173],{"class":79},[62,5982,4955],{"class":273},[62,5984,2912],{"class":79},[62,5986,5987,5990,5993],{"class":64,"line":72},[62,5988,5989],{"class":75},"    AuthRequired",[62,5991,5992],{"class":79},"(authService),  ",[62,5994,5995],{"class":68},"\u002F\u002F 1. 验证 Token\n",[62,5997,5998,6001,6004],{"class":64,"line":83},[62,5999,6000],{"class":75},"    WithUserContext",[62,6002,6003],{"class":79},"(),           ",[62,6005,6006],{"class":68},"\u002F\u002F 2. 桥接用户信息到 request.Context\n",[62,6008,6009,6012,6014,6017],{"class":64,"line":91},[62,6010,6011],{"class":79},"    gin.",[62,6013,5005],{"class":75},[62,6015,6016],{"class":79},"(mcpHTTP),         ",[62,6018,6019],{"class":68},"\u002F\u002F 3. MCP 处理\n",[62,6021,6022],{"class":64,"line":99},[62,6023,494],{"class":79},[15,6025,6026],{},"在工具处理器里就能拿到用户信息了：",[53,6028,6030],{"className":4396,"code":6029,"language":1398,"meta":58,"style":58},"func (h *Handler) handleListItems(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n    userID, ok := ctx.Value(ctxUserID).(uint)\n    if !ok {\n        return mcp.NewToolResultError(\"未认证\"), nil\n    }\n\n    items, _ := h.service.ListByUser(userID)\n    \u002F\u002F ...\n    return mcp.NewToolResultText(result), nil\n}\n",[33,6031,6032,6087,6108,6117,6134,6138,6142,6158,6163,6176],{"__ignoreMap":58},[62,6033,6034,6036,6038,6041,6043,6046,6048,6051,6053,6055,6057,6059,6061,6063,6065,6067,6069,6071,6073,6075,6077,6079,6081,6083,6085],{"class":64,"line":65},[62,6035,4597],{"class":166},[62,6037,218],{"class":79},[62,6039,6040],{"class":176},"h ",[62,6042,4629],{"class":166},[62,6044,6045],{"class":75},"Handler",[62,6047,654],{"class":79},[62,6049,6050],{"class":75},"handleListItems",[62,6052,173],{"class":79},[62,6054,4602],{"class":176},[62,6056,4605],{"class":75},[62,6058,299],{"class":79},[62,6060,4610],{"class":75},[62,6062,186],{"class":79},[62,6064,4615],{"class":176},[62,6066,4618],{"class":75},[62,6068,299],{"class":79},[62,6070,4623],{"class":75},[62,6072,4626],{"class":79},[62,6074,4629],{"class":166},[62,6076,4632],{"class":75},[62,6078,299],{"class":79},[62,6080,4637],{"class":75},[62,6082,186],{"class":79},[62,6084,4642],{"class":166},[62,6086,4224],{"class":79},[62,6088,6089,6092,6094,6097,6100,6103,6106],{"class":64,"line":72},[62,6090,6091],{"class":79},"    userID, ok ",[62,6093,4423],{"class":166},[62,6095,6096],{"class":79}," ctx.",[62,6098,6099],{"class":75},"Value",[62,6101,6102],{"class":79},"(ctxUserID).(",[62,6104,6105],{"class":166},"uint",[62,6107,494],{"class":79},[62,6109,6110,6112,6114],{"class":64,"line":83},[62,6111,253],{"class":166},[62,6113,5595],{"class":166},[62,6115,6116],{"class":79},"ok {\n",[62,6118,6119,6121,6123,6125,6127,6130,6132],{"class":64,"line":91},[62,6120,4850],{"class":166},[62,6122,4545],{"class":79},[62,6124,4855],{"class":75},[62,6126,173],{"class":79},[62,6128,6129],{"class":273},"\"未认证\"",[62,6131,4667],{"class":79},[62,6133,4670],{"class":114},[62,6135,6136],{"class":64,"line":99},[62,6137,127],{"class":79},[62,6139,6140],{"class":64,"line":124},[62,6141,753],{"emptyLinePlaceholder":752},[62,6143,6144,6147,6149,6152,6155],{"class":64,"line":130},[62,6145,6146],{"class":79},"    items, _ ",[62,6148,4423],{"class":166},[62,6150,6151],{"class":79}," h.service.",[62,6153,6154],{"class":75},"ListByUser",[62,6156,6157],{"class":79},"(userID)\n",[62,6159,6160],{"class":64,"line":136},[62,6161,6162],{"class":68},"    \u002F\u002F ...\n",[62,6164,6165,6167,6169,6171,6174],{"class":64,"line":402},[62,6166,4654],{"class":166},[62,6168,4545],{"class":79},[62,6170,4659],{"class":75},[62,6172,6173],{"class":79},"(result), ",[62,6175,4670],{"class":114},[62,6177,6178],{"class":64,"line":407},[62,6179,139],{"class":79},[1503,6181,6182],{"id":6182},"客户端配置",[15,6184,6185],{},"AI 客户端（如 Claude Desktop）配置 MCP 服务器时带上 Authorization 头：",[53,6187,6191],{"className":6188,"code":6189,"language":6190,"meta":58,"style":58},"language-json shiki shiki-themes github-light github-dark","{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"url\": \"http:\u002F\u002Flocalhost:8080\u002Fmcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer your-token-here\"\n      }\n    }\n  }\n}\n","json",[33,6192,6193,6198,6205,6211,6223,6230,6240,6245,6249,6253],{"__ignoreMap":58},[62,6194,6195],{"class":64,"line":65},[62,6196,6197],{"class":79},"{\n",[62,6199,6200,6203],{"class":64,"line":72},[62,6201,6202],{"class":114},"  \"mcpServers\"",[62,6204,80],{"class":79},[62,6206,6207,6209],{"class":64,"line":83},[62,6208,4437],{"class":114},[62,6210,80],{"class":79},[62,6212,6213,6216,6218,6221],{"class":64,"line":91},[62,6214,6215],{"class":114},"      \"url\"",[62,6217,111],{"class":79},[62,6219,6220],{"class":273},"\"http:\u002F\u002Flocalhost:8080\u002Fmcp\"",[62,6222,2912],{"class":79},[62,6224,6225,6228],{"class":64,"line":99},[62,6226,6227],{"class":114},"      \"headers\"",[62,6229,80],{"class":79},[62,6231,6232,6235,6237],{"class":64,"line":124},[62,6233,6234],{"class":114},"        \"Authorization\"",[62,6236,111],{"class":79},[62,6238,6239],{"class":273},"\"Bearer your-token-here\"\n",[62,6241,6242],{"class":64,"line":130},[62,6243,6244],{"class":79},"      }\n",[62,6246,6247],{"class":64,"line":136},[62,6248,127],{"class":79},[62,6250,6251],{"class":64,"line":402},[62,6252,133],{"class":79},[62,6254,6255],{"class":64,"line":407},[62,6256,139],{"class":79},[26,6258,1168],{"id":1168},[865,6260,6261,6271],{},[868,6262,6263],{},[871,6264,6265,6268],{},[874,6266,6267],{},"步骤",[874,6269,6270],{},"说明",[884,6272,6273,6282,6293,6302,6311],{},[871,6274,6275,6279],{},[889,6276,6277],{},[33,6278,4429],{},[889,6280,6281],{},"创建 MCP 协议实例",[871,6283,6284,6290],{},[889,6285,6286,2441,6288],{},[33,6287,4548],{},[33,6289,4591],{},[889,6291,6292],{},"定义并注册工具",[871,6294,6295,6299],{},[889,6296,6297],{},[33,6298,4940],{},[889,6300,6301],{},"转为 HTTP Handler",[871,6303,6304,6308],{},[889,6305,6306],{},[33,6307,4908],{},[889,6309,6310],{},"挂载到 Gin 路由",[871,6312,6313,6321],{},[889,6314,6315,2441,6318],{},[33,6316,6317],{},"AuthRequired",[33,6319,6320],{},"WithUserContext",[889,6322,6323],{},"鉴权 + 用户信息桥接",[15,6325,6326,6327,6330,6331,41],{},"核心就一句话：",[38,6328,6329],{},"MCP 是 HTTP Handler，挂上去就行。"," 鉴权复用你已有的中间件体系，只需要多写一个桥接函数把用户信息传进 ",[33,6332,5407],{},[1228,6334,6335],{},"html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}",{"title":58,"searchDepth":72,"depth":83,"links":6337},[6338,6339,6340,6341,6348,6354],{"id":4308,"depth":72,"text":4309},{"id":4318,"depth":72,"text":4319},{"id":4335,"depth":72,"text":4335},{"id":4361,"depth":72,"text":4361,"children":6342},[6343,6344,6345,6346,6347],{"id":4364,"depth":83,"text":4365},{"id":4392,"depth":83,"text":4393},{"id":4509,"depth":83,"text":4510},{"id":4897,"depth":83,"text":4898},{"id":5018,"depth":83,"text":5019},{"id":5380,"depth":72,"text":5381,"children":6349},[6350,6351,6352,6353],{"id":1177,"depth":83,"text":1177},{"id":5400,"depth":83,"text":5401},{"id":5967,"depth":83,"text":5967},{"id":6182,"depth":83,"text":6182},{"id":1168,"depth":72,"text":1168},"介绍如何使用 mark3labs\u002Fmcp-go 与 Gin 框架集成，搭建一个带鉴权的 MCP（Model Context Protocol）服务器，让 AI Agent 能直接调用你的后端服务。","\u002Fposts\u002Fgo-mcp-gin\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fgo-mcp-gin","2026-06-01T00:50:00",{"title":4303,"description":6355},"Go 工程实践","posts\u002Fgo-mcp-gin\u002Findex",[2064,6364,6365,6366,6367],"MCP","Gin","AI Agent","后端","2026-06-01","5fZl1Vigs2lHb5fHPz-0YZFyFD38ws1heOouZUG1g6M",{"id":6371,"title":6372,"author":6,"body":6373,"description":7140,"difficulty":6,"draft":1243,"extension":1244,"image":7141,"meta":7142,"navigation":752,"path":7143,"pinned":1243,"published":7144,"seo":7145,"series":6,"seriesOrder":6,"stem":7146,"tags":7147,"updated":6,"__hash__":7151},"posts\u002Fposts\u002Fdws-dingtalk-cli\u002Findex.md","DWS — 钉钉官方出品的 AI 友好命令行工具",{"type":8,"value":6374,"toc":7126},[6375,6379,6382,6389,6515,6518,6521,6577,6580,6595,6598,6601,6608,6840,6847,6851,6854,6874,6877,6892,6895,6915,6918,6921,6928,6994,6997,7000,7004,7007,7059,7063,7084,7088,7091,7105,7108,7112,7115,7117,7120,7123],[26,6376,6378],{"id":6377},"dws-是什么","dws 是什么",[15,6380,6381],{},"dws（DingTalk Workspace CLI）是阿里巴巴 2026 年开源的钉钉命令行工具，Apache-2.0 协议，Go 语言编写。",[15,6383,6384,6385,6388],{},"它的定位很明确：",[38,6386,6387],{},"把钉钉的所有产品能力暴露给 AI Agent 和命令行用户","。不用打开钉钉 App，不用写 API 调用代码，一条命令搞定。",[53,6390,6392],{"className":1275,"code":6391,"language":1277,"meta":58,"style":58},"# 查看今天有什么会\ndws calendar event list --start 2026-06-01 --end 2026-06-01\n\n# 给群里发条消息\ndws chat send --group-id xxx --content \"今天发布推迟到下午3点\"\n\n# 创建一个待办\ndws todo task create --subject \"Review PR #123\" --due-date 2026-06-02\n\n# 读取钉钉文档\ndws doc read --url \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[33,6393,6394,6399,6425,6429,6434,6456,6460,6465,6490,6494,6499],{"__ignoreMap":58},[62,6395,6396],{"class":64,"line":65},[62,6397,6398],{"class":68},"# 查看今天有什么会\n",[62,6400,6401,6404,6407,6410,6413,6416,6419,6422],{"class":64,"line":72},[62,6402,6403],{"class":75},"dws",[62,6405,6406],{"class":273}," calendar",[62,6408,6409],{"class":273}," event",[62,6411,6412],{"class":273}," list",[62,6414,6415],{"class":114}," --start",[62,6417,6418],{"class":273}," 2026-06-01",[62,6420,6421],{"class":114}," --end",[62,6423,6424],{"class":273}," 2026-06-01\n",[62,6426,6427],{"class":64,"line":83},[62,6428,753],{"emptyLinePlaceholder":752},[62,6430,6431],{"class":64,"line":91},[62,6432,6433],{"class":68},"# 给群里发条消息\n",[62,6435,6436,6438,6441,6444,6447,6450,6453],{"class":64,"line":99},[62,6437,6403],{"class":75},[62,6439,6440],{"class":273}," chat",[62,6442,6443],{"class":273}," send",[62,6445,6446],{"class":114}," --group-id",[62,6448,6449],{"class":273}," xxx",[62,6451,6452],{"class":114}," --content",[62,6454,6455],{"class":273}," \"今天发布推迟到下午3点\"\n",[62,6457,6458],{"class":64,"line":124},[62,6459,753],{"emptyLinePlaceholder":752},[62,6461,6462],{"class":64,"line":130},[62,6463,6464],{"class":68},"# 创建一个待办\n",[62,6466,6467,6469,6472,6475,6478,6481,6484,6487],{"class":64,"line":136},[62,6468,6403],{"class":75},[62,6470,6471],{"class":273}," todo",[62,6473,6474],{"class":273}," task",[62,6476,6477],{"class":273}," create",[62,6479,6480],{"class":114}," --subject",[62,6482,6483],{"class":273}," \"Review PR #123\"",[62,6485,6486],{"class":114}," --due-date",[62,6488,6489],{"class":273}," 2026-06-02\n",[62,6491,6492],{"class":64,"line":402},[62,6493,753],{"emptyLinePlaceholder":752},[62,6495,6496],{"class":64,"line":407},[62,6497,6498],{"class":68},"# 读取钉钉文档\n",[62,6500,6501,6503,6506,6509,6512],{"class":64,"line":433},[62,6502,6403],{"class":75},[62,6504,6505],{"class":273}," doc",[62,6507,6508],{"class":273}," read",[62,6510,6511],{"class":114}," --url",[62,6513,6514],{"class":273}," \"https:\u002F\u002Falidocs.dingtalk.com\u002Fi\u002Fnodes\u002Fxxx\"\n",[26,6516,6517],{"id":6517},"安装",[15,6519,6520],{},"三种方式，选你喜欢的：",[53,6522,6524],{"className":1275,"code":6523,"language":1277,"meta":58,"style":58},"# npm（推荐，跨平台自动下载二进制）\nnpm install -g dingtalk-workspace-cli\n\n# Homebrew（macOS\u002FLinux）\nbrew install dws\n\n# 直接下载二进制\n# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[33,6525,6526,6531,6544,6548,6553,6563,6567,6572],{"__ignoreMap":58},[62,6527,6528],{"class":64,"line":65},[62,6529,6530],{"class":68},"# npm（推荐，跨平台自动下载二进制）\n",[62,6532,6533,6536,6538,6541],{"class":64,"line":72},[62,6534,6535],{"class":75},"npm",[62,6537,1347],{"class":273},[62,6539,6540],{"class":114}," -g",[62,6542,6543],{"class":273}," dingtalk-workspace-cli\n",[62,6545,6546],{"class":64,"line":83},[62,6547,753],{"emptyLinePlaceholder":752},[62,6549,6550],{"class":64,"line":91},[62,6551,6552],{"class":68},"# Homebrew（macOS\u002FLinux）\n",[62,6554,6555,6558,6560],{"class":64,"line":99},[62,6556,6557],{"class":75},"brew",[62,6559,1347],{"class":273},[62,6561,6562],{"class":273}," dws\n",[62,6564,6565],{"class":64,"line":124},[62,6566,753],{"emptyLinePlaceholder":752},[62,6568,6569],{"class":64,"line":130},[62,6570,6571],{"class":68},"# 直接下载二进制\n",[62,6573,6574],{"class":64,"line":136},[62,6575,6576],{"class":68},"# https:\u002F\u002Fgithub.com\u002Fopen-dingtalk\u002Fdingtalk-workspace-cli\u002Freleases\n",[15,6578,6579],{},"安装后认证：",[53,6581,6583],{"className":1275,"code":6582,"language":1277,"meta":58,"style":58},"dws auth login\n",[33,6584,6585],{"__ignoreMap":58},[62,6586,6587,6589,6592],{"class":64,"line":65},[62,6588,6403],{"class":75},[62,6590,6591],{"class":273}," auth",[62,6593,6594],{"class":273}," login\n",[15,6596,6597],{},"会打开浏览器走 OAuth 设备流认证，token 加密存储在系统钥匙串里（PBKDF2 + AES-256-GCM）。",[26,6599,6600],{"id":6600},"覆盖的钉钉产品",[15,6602,6603,6604,6607],{},"这是最夸张的部分 — ",[38,6605,6606],{},"19 个产品，213 个命令","，基本把钉钉全家桶都搬到了命令行：",[865,6609,6610,6622],{},[868,6611,6612],{},[871,6613,6614,6617,6620],{},[874,6615,6616],{},"产品",[874,6618,6619],{},"命令数",[874,6621,6270],{},[884,6623,6624,6638,6652,6666,6680,6694,6708,6722,6735,6749,6762,6776,6790,6803,6816,6829],{},[871,6625,6626,6632,6635],{},[889,6627,6628,6629],{},"聊天 ",[33,6630,6631],{},"chat",[889,6633,6634],{},"57",[889,6636,6637],{},"消息收发、群管理、机器人、已读状态、Webhook",[871,6639,6640,6646,6649],{},[889,6641,6642,6643],{},"AI 表格 ",[33,6644,6645],{},"aitable",[889,6647,6648],{},"42",[889,6650,6651],{},"Base\u002F数据表\u002F字段\u002F记录\u002F视图\u002F图表\u002F仪表盘，完整 CRUD",[871,6653,6654,6660,6663],{},[889,6655,6656,6657],{},"电子表格 ",[33,6658,6659],{},"sheet",[889,6661,6662],{},"34",[889,6664,6665],{},"工作表读写、行列操作、合并、筛选视图、导出",[871,6667,6668,6674,6677],{},[889,6669,6670,6671],{},"文档 ",[33,6672,6673],{},"doc",[889,6675,6676],{},"21",[889,6678,6679],{},"搜索、读写、块级编辑、评论、复制移动",[871,6681,6682,6688,6691],{},[889,6683,6684,6685],{},"会议纪要 ",[33,6686,6687],{},"minutes",[889,6689,6690],{},"19",[889,6692,6693],{},"列表、摘要、转写、思维导图、发言人、热词",[871,6695,6696,6702,6705],{},[889,6697,6698,6699],{},"日历 ",[33,6700,6701],{},"calendar",[889,6703,6704],{},"14",[889,6706,6707],{},"事件 CRUD、会议室、忙闲查询、时间建议",[871,6709,6710,6716,6719],{},[889,6711,6712,6713],{},"审批 ",[33,6714,6715],{},"oa",[889,6717,6718],{},"9",[889,6720,6721],{},"待办、发起、同意\u002F拒绝\u002F撤销",[871,6723,6724,6730,6732],{},[889,6725,6726,6727],{},"云盘 ",[33,6728,6729],{},"drive",[889,6731,6718],{},[889,6733,6734],{},"列表、下载、上传、创建目录",[871,6736,6737,6743,6746],{},[889,6738,6739,6740],{},"知识库 ",[33,6741,6742],{},"wiki",[889,6744,6745],{},"7",[889,6747,6748],{},"空间管理、成员管理",[871,6750,6751,6757,6759],{},[889,6752,6753,6754],{},"汇报 ",[33,6755,6756],{},"report",[889,6758,6745],{},[889,6760,6761],{},"日志\u002F日报\u002F周报，按模板创建、统计",[871,6763,6764,6770,6773],{},[889,6765,6766,6767],{},"待办 ",[33,6768,6769],{},"todo",[889,6771,6772],{},"6",[889,6774,6775],{},"创建、完成、删除，支持优先级和循环",[871,6777,6778,6784,6787],{},[889,6779,6780,6781],{},"邮件 ",[33,6782,6783],{},"mail",[889,6785,6786],{},"4",[889,6788,6789],{},"邮箱查询、邮件搜索、发送",[871,6791,6792,6798,6800],{},[889,6793,6794,6795],{},"考勤 ",[33,6796,6797],{},"attendance",[889,6799,6786],{},[889,6801,6802],{},"打卡记录、排班、汇总",[871,6804,6805,6811,6813],{},[889,6806,6807,6808],{},"通讯录 ",[33,6809,6810],{},"contact",[889,6812,115],{},[889,6814,6815],{},"用户搜索、部门查询",[871,6817,6818,6824,6826],{},[889,6819,6820,6821],{},"DING 消息 ",[33,6822,6823],{},"ding",[889,6825,365],{},[889,6827,6828],{},"发送\u002F撤回（应用内、短信、电话）",[871,6830,6831,6834,6837],{},[889,6832,6833],{},"其他",[889,6835,6836],{},"若干",[889,6838,6839],{},"AI 搜索、AI 应用、直播、开放平台文档",[15,6841,6842,6843,6846],{},"还有一个万能命令 ",[33,6844,6845],{},"dws api","，可以直接调用任意钉钉 OpenAPI，不受这 213 个命令的限制。",[26,6848,6850],{"id":6849},"agent-skill-系统","Agent Skill 系统",[15,6852,6853],{},"dws 内置了一套完整的 Agent Skill，可以直接被 AI 编程工具发现和使用：",[2810,6855,6856,6862,6868],{},[516,6857,6858,6861],{},[38,6859,6860],{},"Claude Code"," — 通过 SKILL.md 自动加载",[516,6863,6864,6867],{},[38,6865,6866],{},"Cursor"," — 支持 MCP 协议接入",[516,6869,6870,6873],{},[38,6871,6872],{},"Codex \u002F OpenCode \u002F Qoder"," — 通过 skill 安装命令接入",[15,6875,6876],{},"安装 skill：",[53,6878,6880],{"className":1275,"code":6879,"language":1277,"meta":58,"style":58},"dws skill setup\n",[33,6881,6882],{"__ignoreMap":58},[62,6883,6884,6886,6889],{"class":64,"line":65},[62,6885,6403],{"class":75},[62,6887,6888],{"class":273}," skill",[62,6890,6891],{"class":273}," setup\n",[15,6893,6894],{},"SKILL.md 里定义了：",[2810,6896,6897,6906,6909,6912],{},[516,6898,6899,6900,6902,6903,6905],{},"16 个产品的意图判断决策树（用户说\"表格\"→ ",[33,6901,6645],{},"，说\"打卡\"→ ",[33,6904,6797],{},"）",[516,6907,6908],{},"危险操作确认流程（删除、撤回等必须先确认）",[516,6910,6911],{},"错误处理规范（verbose 重试、recovery 事件 ID）",[516,6913,6914],{},"URL 分流规则（alidocs.dingtalk.com 的多种路径格式自动识别）",[15,6916,6917],{},"Agent 读到这份 SKILL.md 就知道什么时候该调什么命令，不需要额外的 prompt 工程。",[26,6919,6920],{"id":6920},"自动升级",[15,6922,6923,6924,6927],{},"内置 ",[33,6925,6926],{},"dws upgrade","，不需要重新下载安装包：",[53,6929,6931],{"className":1275,"code":6930,"language":1277,"meta":58,"style":58},"dws upgrade              # 交互式升级到最新版\ndws upgrade --check      # 仅检查是否有新版本\ndws upgrade --list       # 列出最近版本\ndws upgrade --version v1.0.5  # 升级到指定版本\ndws upgrade --rollback   # 回滚到上一版本\n",[33,6932,6933,6943,6955,6967,6982],{"__ignoreMap":58},[62,6934,6935,6937,6940],{"class":64,"line":65},[62,6936,6403],{"class":75},[62,6938,6939],{"class":273}," upgrade",[62,6941,6942],{"class":68},"              # 交互式升级到最新版\n",[62,6944,6945,6947,6949,6952],{"class":64,"line":72},[62,6946,6403],{"class":75},[62,6948,6939],{"class":273},[62,6950,6951],{"class":114}," --check",[62,6953,6954],{"class":68},"      # 仅检查是否有新版本\n",[62,6956,6957,6959,6961,6964],{"class":64,"line":83},[62,6958,6403],{"class":75},[62,6960,6939],{"class":273},[62,6962,6963],{"class":114}," --list",[62,6965,6966],{"class":68},"       # 列出最近版本\n",[62,6968,6969,6971,6973,6976,6979],{"class":64,"line":91},[62,6970,6403],{"class":75},[62,6972,6939],{"class":273},[62,6974,6975],{"class":114}," --version",[62,6977,6978],{"class":273}," v1.0.5",[62,6980,6981],{"class":68},"  # 升级到指定版本\n",[62,6983,6984,6986,6988,6991],{"class":64,"line":99},[62,6985,6403],{"class":75},[62,6987,6939],{"class":273},[62,6989,6990],{"class":114}," --rollback",[62,6992,6993],{"class":68},"   # 回滚到上一版本\n",[15,6995,6996],{},"升级流程：下载 → SHA256 校验 → 备份当前版本 → 原子替换 → 更新 Skill。失败可一键回滚。",[26,6998,6999],{"id":6999},"亮点",[1503,7001,7003],{"id":7002},"_1-为-ai-agent-专门优化","1. 为 AI Agent 专门优化",[15,7005,7006],{},"这可能是目前对 AI 最友好的 CLI 工具：",[2810,7008,7009,7030,7040,7049],{},[516,7010,7011,7014,7015,7018,7019,7022,7023,7018,7026,7029],{},[38,7012,7013],{},"智能纠错"," — AI 模型经常生成错误的参数格式（",[33,7016,7017],{},"--userId"," 应该是 ",[33,7020,7021],{},"--user-id","，",[33,7024,7025],{},"--limit100",[33,7027,7028],{},"--limit 100","），dws 的 5 阶段 Pipeline 会自动修正",[516,7031,7032,7035,7036,7039],{},[38,7033,7034],{},"Schema 发现"," — ",[33,7037,7038],{},"dws schema"," 让 Agent 动态查询可用工具和参数，不需要硬编码",[516,7041,7042,7035,7045,7048],{},[38,7043,7044],{},"jq 过滤",[33,7046,7047],{},"--jq '.data[] | {name, id}'"," 直接过滤输出，减少 Agent 消耗的 token",[516,7050,7051,7054,7055,7058],{},[38,7052,7053],{},"JSON 输出"," — 所有命令支持 ",[33,7056,7057],{},"--format json","，方便程序解析",[1503,7060,7062],{"id":7061},"_2-安全设计","2. 安全设计",[2810,7064,7065,7068,7071,7074,7081],{},[516,7066,7067],{},"token 用 PBKDF2 + AES-256-GCM 加密，密钥绑定设备 MAC 地址",[516,7069,7070],{},"域名白名单，只能访问钉钉官方 API",[516,7072,7073],{},"强制 HTTPS",[516,7075,7076,7077,7080],{},"写操作必须显式 ",[33,7078,7079],{},"--yes"," 确认",[516,7082,7083],{},"完整审计链",[1503,7085,7087],{"id":7086},"_3-发现驱动架构","3. 发现驱动架构",[15,7089,7090],{},"不是把所有命令硬编码在代码里，而是：",[513,7092,7093,7096,7099,7102],{},[516,7094,7095],{},"从钉钉 MCP 服务器注册中心拉取元数据",[516,7097,7098],{},"运行时发现服务端能力",[516,7100,7101],{},"转换成统一的中间表示（IR）",[516,7103,7104],{},"动态挂载到命令树",[15,7106,7107],{},"这意味着钉钉新增产品能力时，dws 不需要发版就能支持。",[1503,7109,7111],{"id":7110},"_4-多平台分发","4. 多平台分发",[15,7113,7114],{},"GoReleaser 交叉编译 + Homebrew formula + npm 包 + shell\u002FPowerShell 安装脚本，覆盖 Windows、macOS、Linux。",[26,7116,1168],{"id":1168},[15,7118,7119],{},"dws 把钉钉从一个\"必须打开 App 才能用\"的工具变成了一个\"命令行 + AI Agent 随时调用\"的平台。213 个命令覆盖日常办公的方方面面，Agent Skill 系统让 AI 编程工具可以直接理解和使用这些能力。",[15,7121,7122],{},"如果你是钉钉重度用户，或者在做企业内部的 AI Agent 集成，dws 值得一试。",[1228,7124,7125],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":58,"searchDepth":72,"depth":83,"links":7127},[7128,7129,7130,7131,7132,7133,7139],{"id":6377,"depth":72,"text":6378},{"id":6517,"depth":72,"text":6517},{"id":6600,"depth":72,"text":6600},{"id":6849,"depth":72,"text":6850},{"id":6920,"depth":72,"text":6920},{"id":6999,"depth":72,"text":6999,"children":7134},[7135,7136,7137,7138],{"id":7002,"depth":83,"text":7003},{"id":7061,"depth":83,"text":7062},{"id":7086,"depth":83,"text":7087},{"id":7110,"depth":83,"text":7111},{"id":1168,"depth":72,"text":1168},"介绍阿里巴巴官方的 DingTalk Workspace CLI（dws），一个覆盖钉钉 19 个产品、213 个命令的 AI 友好命令行工具，支持自动升级和 Agent Skill 系统。","\u002Fposts\u002Fdws-dingtalk-cli\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fdws-dingtalk-cli","2026-06-01T00:51:00",{"title":6372,"description":7140},"posts\u002Fdws-dingtalk-cli\u002Findex",[7148,7149,6366,2064,7150],"钉钉","CLI","效率工具","4ca65VUZ2Sz9yG8dNtftrBDDHE6QowA3ed9g4egQkKI",{"id":7153,"title":7154,"author":6,"body":7155,"description":8118,"difficulty":1242,"draft":1243,"extension":1244,"image":8119,"meta":8120,"navigation":752,"path":8121,"pinned":1243,"published":8122,"seo":8123,"series":6361,"seriesOrder":72,"stem":8124,"tags":8125,"updated":8129,"__hash__":8130},"posts\u002Fposts\u002Fgo-embed-frontend\u002Findex.md","Go 单文件打包实战：把前端塞进二进制",{"type":8,"value":7156,"toc":8101},[7157,7159,7166,7169,7172,7183,7186,7190,7523,7527,7635,7639,7773,7776,7780,7789,7806,7817,7836,7850,7856,7864,7963,7969,7973,7980,7989,7992,8028,8031,8036,8075,8077,8083,8095,8098],[26,7158,13],{"id":13},[15,7160,7161,7162,7165],{},"一个 Gin + Vue 3 的全栈项目，之前的发布方式是：一个 Go 二进制 + 一个 ",[33,7163,7164],{},"web\u002F"," 前端静态目录，必须放一起才能运行。",[15,7167,7168],{},"为了让部署更丝滑——一个 exe 拖哪都能跑——决定把前端构建产物嵌入到 Go 二进制里。",[26,7170,7171],{"id":7171},"核心思路",[15,7173,7174,7175,7178,7179,7182],{},"利用 Go 1.16 引入的 ",[33,7176,7177],{},"\u002F\u002Fgo:embed"," 指令，在编译时将前端 ",[33,7180,7181],{},"dist\u002F"," 目录嵌入二进制，运行时通过嵌入的文件系统提供 SPA 服务。",[26,7184,7185],{"id":7185},"逐步实现",[1503,7187,7189],{"id":7188},"_1-创建嵌入模块","1. 创建嵌入模块",[53,7191,7193],{"className":4396,"code":7192,"language":1398,"meta":58,"style":58},"\u002F\u002F service\u002Fweb\u002Fembed.go\npackage web\n\nimport (\n    \"embed\"\n    \"io\u002Ffs\"\n    \"mime\"\n    \"net\u002Fhttp\"\n    \"path\u002Ffilepath\"\n    \"strings\"\n\n    \"github.com\u002Fgin-gonic\u002Fgin\"\n)\n\n\u002F\u002Fgo:embed dist\nvar embedFS embed.FS\n\nfunc SPAHandler() gin.HandlerFunc {\n    sub, _ := fs.Sub(embedFS, \"dist\")\n\n    return func(c *gin.Context) {\n        p := strings.TrimPrefix(c.Request.URL.Path, \"\u002F\")\n        if p == \"\" {\n            p = \"index.html\"\n        }\n\n        data, err := fs.ReadFile(sub, p)\n        if err != nil {\n            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n            data, _ = fs.ReadFile(sub, \"index.html\")\n        }\n\n        c.Data(http.StatusOK, mime.TypeByExtension(filepath.Ext(p)), data)\n    }\n}\n",[33,7194,7195,7200,7208,7212,7218,7228,7237,7246,7255,7264,7273,7277,7286,7290,7294,7299,7314,7318,7335,7356,7360,7380,7400,7413,7423,7427,7431,7446,7458,7463,7482,7486,7490,7513,7518],{"__ignoreMap":58},[62,7196,7197],{"class":64,"line":65},[62,7198,7199],{"class":68},"\u002F\u002F service\u002Fweb\u002Fembed.go\n",[62,7201,7202,7205],{"class":64,"line":72},[62,7203,7204],{"class":166},"package",[62,7206,7207],{"class":75}," web\n",[62,7209,7210],{"class":64,"line":83},[62,7211,753],{"emptyLinePlaceholder":752},[62,7213,7214,7216],{"class":64,"line":91},[62,7215,4404],{"class":166},[62,7217,5802],{"class":79},[62,7219,7220,7223,7226],{"class":64,"line":99},[62,7221,7222],{"class":273},"    \"",[62,7224,7225],{"class":75},"embed",[62,7227,1501],{"class":273},[62,7229,7230,7232,7235],{"class":64,"line":124},[62,7231,7222],{"class":273},[62,7233,7234],{"class":75},"io\u002Ffs",[62,7236,1501],{"class":273},[62,7238,7239,7241,7244],{"class":64,"line":130},[62,7240,7222],{"class":273},[62,7242,7243],{"class":75},"mime",[62,7245,1501],{"class":273},[62,7247,7248,7250,7253],{"class":64,"line":136},[62,7249,7222],{"class":273},[62,7251,7252],{"class":75},"net\u002Fhttp",[62,7254,1501],{"class":273},[62,7256,7257,7259,7262],{"class":64,"line":402},[62,7258,7222],{"class":273},[62,7260,7261],{"class":75},"path\u002Ffilepath",[62,7263,1501],{"class":273},[62,7265,7266,7268,7271],{"class":64,"line":407},[62,7267,7222],{"class":273},[62,7269,7270],{"class":75},"strings",[62,7272,1501],{"class":273},[62,7274,7275],{"class":64,"line":433},[62,7276,753],{"emptyLinePlaceholder":752},[62,7278,7279,7281,7284],{"class":64,"line":438},[62,7280,7222],{"class":273},[62,7282,7283],{"class":75},"github.com\u002Fgin-gonic\u002Fgin",[62,7285,1501],{"class":273},[62,7287,7288],{"class":64,"line":446},[62,7289,494],{"class":79},[62,7291,7292],{"class":64,"line":1448},[62,7293,753],{"emptyLinePlaceholder":752},[62,7295,7296],{"class":64,"line":1459},[62,7297,7298],{"class":68},"\u002F\u002Fgo:embed dist\n",[62,7300,7301,7304,7307,7309,7311],{"class":64,"line":1470},[62,7302,7303],{"class":166},"var",[62,7305,7306],{"class":79}," embedFS ",[62,7308,7225],{"class":75},[62,7310,299],{"class":79},[62,7312,7313],{"class":75},"FS\n",[62,7315,7316],{"class":64,"line":1475},[62,7317,753],{"emptyLinePlaceholder":752},[62,7319,7320,7322,7325,7327,7329,7331,7333],{"class":64,"line":1484},[62,7321,4597],{"class":166},[62,7323,7324],{"class":75}," SPAHandler",[62,7326,5845],{"class":79},[62,7328,5225],{"class":75},[62,7330,299],{"class":79},[62,7332,5443],{"class":75},[62,7334,620],{"class":79},[62,7336,7337,7340,7342,7345,7348,7351,7354],{"class":64,"line":1489},[62,7338,7339],{"class":79},"    sub, _ ",[62,7341,4423],{"class":166},[62,7343,7344],{"class":79}," fs.",[62,7346,7347],{"class":75},"Sub",[62,7349,7350],{"class":79},"(embedFS, ",[62,7352,7353],{"class":273},"\"dist\"",[62,7355,494],{"class":79},[62,7357,7358],{"class":64,"line":1695},[62,7359,753],{"emptyLinePlaceholder":752},[62,7361,7362,7364,7366,7368,7370,7372,7374,7376,7378],{"class":64,"line":1703},[62,7363,4654],{"class":166},[62,7365,5452],{"class":166},[62,7367,173],{"class":79},[62,7369,5219],{"class":176},[62,7371,5222],{"class":166},[62,7373,5225],{"class":75},[62,7375,299],{"class":79},[62,7377,4610],{"class":75},[62,7379,4224],{"class":79},[62,7381,7382,7385,7387,7389,7392,7395,7398],{"class":64,"line":1708},[62,7383,7384],{"class":79},"        p ",[62,7386,4423],{"class":166},[62,7388,5559],{"class":79},[62,7390,7391],{"class":75},"TrimPrefix",[62,7393,7394],{"class":79},"(c.Request.URL.Path, ",[62,7396,7397],{"class":273},"\"\u002F\"",[62,7399,494],{"class":79},[62,7401,7402,7404,7407,7409,7411],{"class":64,"line":2336},[62,7403,5491],{"class":166},[62,7405,7406],{"class":79}," p ",[62,7408,5497],{"class":166},[62,7410,5500],{"class":273},[62,7412,620],{"class":79},[62,7414,7415,7418,7420],{"class":64,"line":2350},[62,7416,7417],{"class":79},"            p ",[62,7419,200],{"class":166},[62,7421,7422],{"class":273}," \"index.html\"\n",[62,7424,7425],{"class":64,"line":2357},[62,7426,5545],{"class":79},[62,7428,7429],{"class":64,"line":2362},[62,7430,753],{"emptyLinePlaceholder":752},[62,7432,7433,7436,7438,7440,7443],{"class":64,"line":2368},[62,7434,7435],{"class":79},"        data, err ",[62,7437,4423],{"class":166},[62,7439,7344],{"class":79},[62,7441,7442],{"class":75},"ReadFile",[62,7444,7445],{"class":79},"(sub, p)\n",[62,7447,7448,7450,7452,7454,7456],{"class":64,"line":2376},[62,7449,5491],{"class":166},[62,7451,4837],{"class":79},[62,7453,4840],{"class":166},[62,7455,4843],{"class":114},[62,7457,620],{"class":79},[62,7459,7460],{"class":64,"line":2384},[62,7461,7462],{"class":68},"            \u002F\u002F SPA fallback：文件不存在则返回 index.html\n",[62,7464,7465,7468,7470,7472,7474,7477,7480],{"class":64,"line":2389},[62,7466,7467],{"class":79},"            data, _ ",[62,7469,200],{"class":166},[62,7471,7344],{"class":79},[62,7473,7442],{"class":75},[62,7475,7476],{"class":79},"(sub, ",[62,7478,7479],{"class":273},"\"index.html\"",[62,7481,494],{"class":79},[62,7483,7484],{"class":64,"line":2395},[62,7485,5545],{"class":79},[62,7487,7488],{"class":64,"line":2404},[62,7489,753],{"emptyLinePlaceholder":752},[62,7491,7493,7495,7498,7501,7504,7507,7510],{"class":64,"line":7492},33,[62,7494,5236],{"class":79},[62,7496,7497],{"class":75},"Data",[62,7499,7500],{"class":79},"(http.StatusOK, mime.",[62,7502,7503],{"class":75},"TypeByExtension",[62,7505,7506],{"class":79},"(filepath.",[62,7508,7509],{"class":75},"Ext",[62,7511,7512],{"class":79},"(p)), data)\n",[62,7514,7516],{"class":64,"line":7515},34,[62,7517,127],{"class":79},[62,7519,7521],{"class":64,"line":7520},35,[62,7522,139],{"class":79},[1503,7524,7526],{"id":7525},"_2-改造路由","2. 改造路由",[53,7528,7530],{"className":4396,"code":7529,"language":1398,"meta":58,"style":58},"\u002F\u002F router.go\nimport \"myproject\u002Fweb\"\n\n\u002F\u002F 替换原来的磁盘静态文件服务\nwebHandler := web.SPAHandler()\nrouter.GET(\"\u002F\", webHandler)\nrouter.GET(\"\u002Fassets\u002F*filepath\", webHandler)\nrouter.GET(\"\u002Ffavicon.ico\", webHandler)\nrouter.GET(\"\u002Ffavicon.svg\", webHandler)\nrouter.NoRoute(webHandler)\n",[33,7531,7532,7537,7548,7552,7557,7572,7586,7599,7612,7625],{"__ignoreMap":58},[62,7533,7534],{"class":64,"line":65},[62,7535,7536],{"class":68},"\u002F\u002F router.go\n",[62,7538,7539,7541,7543,7546],{"class":64,"line":72},[62,7540,4404],{"class":166},[62,7542,1304],{"class":273},[62,7544,7545],{"class":75},"myproject\u002Fweb",[62,7547,1501],{"class":273},[62,7549,7550],{"class":64,"line":83},[62,7551,753],{"emptyLinePlaceholder":752},[62,7553,7554],{"class":64,"line":91},[62,7555,7556],{"class":68},"\u002F\u002F 替换原来的磁盘静态文件服务\n",[62,7558,7559,7562,7564,7567,7570],{"class":64,"line":99},[62,7560,7561],{"class":79},"webHandler ",[62,7563,4423],{"class":166},[62,7565,7566],{"class":79}," web.",[62,7568,7569],{"class":75},"SPAHandler",[62,7571,374],{"class":79},[62,7573,7574,7577,7579,7581,7583],{"class":64,"line":124},[62,7575,7576],{"class":79},"router.",[62,7578,5205],{"class":75},[62,7580,173],{"class":79},[62,7582,7397],{"class":273},[62,7584,7585],{"class":79},", webHandler)\n",[62,7587,7588,7590,7592,7594,7597],{"class":64,"line":130},[62,7589,7576],{"class":79},[62,7591,5205],{"class":75},[62,7593,173],{"class":79},[62,7595,7596],{"class":273},"\"\u002Fassets\u002F*filepath\"",[62,7598,7585],{"class":79},[62,7600,7601,7603,7605,7607,7610],{"class":64,"line":136},[62,7602,7576],{"class":79},[62,7604,5205],{"class":75},[62,7606,173],{"class":79},[62,7608,7609],{"class":273},"\"\u002Ffavicon.ico\"",[62,7611,7585],{"class":79},[62,7613,7614,7616,7618,7620,7623],{"class":64,"line":402},[62,7615,7576],{"class":79},[62,7617,5205],{"class":75},[62,7619,173],{"class":79},[62,7621,7622],{"class":273},"\"\u002Ffavicon.svg\"",[62,7624,7585],{"class":79},[62,7626,7627,7629,7632],{"class":64,"line":407},[62,7628,7576],{"class":79},[62,7630,7631],{"class":75},"NoRoute",[62,7633,7634],{"class":79},"(webHandler)\n",[1503,7636,7638],{"id":7637},"_3-一键构建脚本","3. 一键构建脚本",[53,7640,7642],{"className":1275,"code":7641,"language":1277,"meta":58,"style":58},"#!\u002Fbin\u002Fbash\n# build-standalone.sh\n\nset -e\n\n# 1. 构建前端\npnpm install --no-frozen-lockfile\nnpx vite build\n\n# 2. 复制到嵌入目录\nrm -rf service\u002Fweb\u002Fdist\ncp -r dist service\u002Fweb\u002Fdist\n\n# 3. 编译 Go 二进制\ncd service\ngo build -o myapp.exe \\\n    -ldflags=\"-X myproject\u002Fglobal.RUNCODE=release\" \\\n    main.go\n",[33,7643,7644,7649,7654,7658,7666,7670,7675,7685,7695,7699,7704,7715,7728,7732,7737,7745,7758,7768],{"__ignoreMap":58},[62,7645,7646],{"class":64,"line":65},[62,7647,7648],{"class":68},"#!\u002Fbin\u002Fbash\n",[62,7650,7651],{"class":64,"line":72},[62,7652,7653],{"class":68},"# build-standalone.sh\n",[62,7655,7656],{"class":64,"line":83},[62,7657,753],{"emptyLinePlaceholder":752},[62,7659,7660,7663],{"class":64,"line":91},[62,7661,7662],{"class":114},"set",[62,7664,7665],{"class":114}," -e\n",[62,7667,7668],{"class":64,"line":99},[62,7669,753],{"emptyLinePlaceholder":752},[62,7671,7672],{"class":64,"line":124},[62,7673,7674],{"class":68},"# 1. 构建前端\n",[62,7676,7677,7680,7682],{"class":64,"line":130},[62,7678,7679],{"class":75},"pnpm",[62,7681,1347],{"class":273},[62,7683,7684],{"class":114}," --no-frozen-lockfile\n",[62,7686,7687,7689,7692],{"class":64,"line":136},[62,7688,3024],{"class":75},[62,7690,7691],{"class":273}," vite",[62,7693,7694],{"class":273}," build\n",[62,7696,7697],{"class":64,"line":402},[62,7698,753],{"emptyLinePlaceholder":752},[62,7700,7701],{"class":64,"line":407},[62,7702,7703],{"class":68},"# 2. 复制到嵌入目录\n",[62,7705,7706,7709,7712],{"class":64,"line":433},[62,7707,7708],{"class":75},"rm",[62,7710,7711],{"class":114}," -rf",[62,7713,7714],{"class":273}," service\u002Fweb\u002Fdist\n",[62,7716,7717,7720,7723,7726],{"class":64,"line":438},[62,7718,7719],{"class":75},"cp",[62,7721,7722],{"class":114}," -r",[62,7724,7725],{"class":273}," dist",[62,7727,7714],{"class":273},[62,7729,7730],{"class":64,"line":446},[62,7731,753],{"emptyLinePlaceholder":752},[62,7733,7734],{"class":64,"line":1448},[62,7735,7736],{"class":68},"# 3. 编译 Go 二进制\n",[62,7738,7739,7742],{"class":64,"line":1459},[62,7740,7741],{"class":114},"cd",[62,7743,7744],{"class":273}," service\n",[62,7746,7747,7749,7751,7753,7756],{"class":64,"line":1470},[62,7748,1398],{"class":75},[62,7750,1413],{"class":273},[62,7752,1422],{"class":114},[62,7754,7755],{"class":273}," myapp.exe",[62,7757,5323],{"class":114},[62,7759,7760,7763,7766],{"class":64,"line":1475},[62,7761,7762],{"class":114},"    -ldflags=",[62,7764,7765],{"class":273},"\"-X myproject\u002Fglobal.RUNCODE=release\"",[62,7767,5323],{"class":114},[62,7769,7770],{"class":64,"line":1484},[62,7771,7772],{"class":273},"    main.go\n",[26,7774,7775],{"id":7775},"踩坑记录",[1503,7777,7779],{"id":7778},"坑一路由不匹配静态资源-404","坑一：路由不匹配，静态资源 404",[15,7781,7782,7783,7785,7786,7788],{},"首次实现时用了 Gin 的 ",[33,7784,7631],{}," 兜底，但部分静态资源返回 404。排查发现 ",[33,7787,7631],{}," 在某些路由未显式注册时不会按预期触发。",[15,7790,7791,7794,7795,7798,7799,7802,7803,7805],{},[38,7792,7793],{},"解决","：显式注册 ",[33,7796,7797],{},"\u002Fassets\u002F*filepath","、",[33,7800,7801],{},"\u002Ffavicon.ico"," 等路由，",[33,7804,7631],{}," 仅作为兜底。",[1503,7807,7809,7810,7812,7813,7816],{"id":7808},"坑二goembed-排除-_-开头文件","坑二：",[33,7811,7177],{}," 排除 ",[33,7814,7815],{},"_"," 开头文件",[15,7818,7819,7820,7823,7824,7826,7827,41],{},"这是最坑的。Vite 构建产出了一个 ",[33,7821,7822],{},"_plugin-vue_export-helper-xxx.js","，Go 的 ",[33,7825,7177],{}," 当模式为目录时，",[38,7828,7829,7830,7832,7833,7835],{},"硬编码排除了 ",[33,7831,7815],{}," 和 ",[33,7834,299],{}," 开头的文件",[53,7837,7839],{"className":4396,"code":7838,"language":1398,"meta":58,"style":58},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n\u002F\u002Fgo:embed dist\n",[33,7840,7841,7846],{"__ignoreMap":58},[62,7842,7843],{"class":64,"line":65},[62,7844,7845],{"class":68},"\u002F\u002F 这行会跳过 dist\u002Fassets\u002F_plugin-xxx.js\n",[62,7847,7848],{"class":64,"line":72},[62,7849,7298],{"class":68},[15,7851,7852,7853,41],{},"错误表现：JS 文件返回的是 HTML 内容（SPA fallback），浏览器报 ",[33,7854,7855],{},"Unexpected token '\u003C'",[15,7857,7858,7860,7861,7863],{},[38,7859,7793],{},"：改 Vite 配置，去除了 ",[33,7862,7815],{}," 前缀：",[53,7865,7869],{"className":7866,"code":7867,"language":7868,"meta":58,"style":58},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F vite.config.ts\nbuild: {\n  rollupOptions: {\n    output: {\n      chunkFileNames(chunkInfo) {\n        return `assets\u002F${chunkInfo.name.replace(\u002F^_\u002F, '')}-[hash].js`\n      },\n    },\n  },\n},\n","ts",[33,7870,7871,7876,7883,7890,7897,7905,7943,7948,7953,7958],{"__ignoreMap":58},[62,7872,7873],{"class":64,"line":65},[62,7874,7875],{"class":68},"\u002F\u002F vite.config.ts\n",[62,7877,7878,7881],{"class":64,"line":72},[62,7879,7880],{"class":75},"build",[62,7882,80],{"class":79},[62,7884,7885,7888],{"class":64,"line":83},[62,7886,7887],{"class":75},"  rollupOptions",[62,7889,80],{"class":79},[62,7891,7892,7895],{"class":64,"line":91},[62,7893,7894],{"class":75},"    output",[62,7896,80],{"class":79},[62,7898,7899,7902],{"class":64,"line":99},[62,7900,7901],{"class":75},"      chunkFileNames",[62,7903,7904],{"class":79},"(chunkInfo) {\n",[62,7906,7907,7909,7912,7915,7917,7920,7922,7924,7926,7928,7930,7932,7934,7936,7938,7940],{"class":64,"line":124},[62,7908,4850],{"class":166},[62,7910,7911],{"class":273}," `assets\u002F${",[62,7913,7914],{"class":79},"chunkInfo",[62,7916,299],{"class":273},[62,7918,7919],{"class":79},"name",[62,7921,299],{"class":273},[62,7923,690],{"class":75},[62,7925,173],{"class":273},[62,7927,296],{"class":273},[62,7929,283],{"class":166},[62,7931,7815],{"class":286},[62,7933,296],{"class":273},[62,7935,186],{"class":273},[62,7937,707],{"class":273},[62,7939,1021],{"class":273},[62,7941,7942],{"class":273},"}-[hash].js`\n",[62,7944,7945],{"class":64,"line":130},[62,7946,7947],{"class":79},"      },\n",[62,7949,7950],{"class":64,"line":136},[62,7951,7952],{"class":79},"    },\n",[62,7954,7955],{"class":64,"line":402},[62,7956,7957],{"class":79},"  },\n",[62,7959,7960],{"class":64,"line":407},[62,7961,7962],{"class":79},"},\n",[15,7964,7965,7966,7968],{},"这样就避免产生 ",[33,7967,7815],{}," 前缀文件名的同时，保持构建产物的一致性和引用关系的正确性。",[1503,7970,7972],{"id":7971},"坑三构建缓存导致-embed-内容未更新","坑三：构建缓存导致 embed 内容未更新",[15,7974,7975,7976,7979],{},"多次 ",[33,7977,7978],{},"go build"," 后，Go 的构建缓存可能使用了旧版本的 embed 内容，导致实际运行的文件不是最新的。",[15,7981,7982,7984,7985,7988],{},[38,7983,7793],{},"：使用 ",[33,7986,7987],{},"go build -a"," 强制重新编译所有包。",[26,7990,7991],{"id":7991},"最终效果",[53,7993,7995],{"className":1275,"code":7994,"language":1277,"meta":58,"style":58},"$ bash build-standalone.sh\n=== 构建成功 ===\n输出: service\u002Fmyapp.exe  41M\n",[33,7996,7997,8007,8017],{"__ignoreMap":58},[62,7998,7999,8001,8004],{"class":64,"line":65},[62,8000,293],{"class":75},[62,8002,8003],{"class":273}," bash",[62,8005,8006],{"class":273}," build-standalone.sh\n",[62,8008,8009,8011,8014],{"class":64,"line":72},[62,8010,270],{"class":273},[62,8012,8013],{"class":273}," 构建成功",[62,8015,8016],{"class":273}," ===\n",[62,8018,8019,8022,8025],{"class":64,"line":83},[62,8020,8021],{"class":75},"输出:",[62,8023,8024],{"class":273}," service\u002Fmyapp.exe",[62,8026,8027],{"class":273},"  41M\n",[15,8029,8030],{},"一个 41MB 的 exe，内置前端页面、API 后端、SQLite 数据库（运行时初始化），拖到哪都能跑。",[15,8032,8033],{},[38,8034,8035],{},"部署对比：",[865,8037,8038,8051],{},[868,8039,8040],{},[871,8041,8042,8045,8048],{},[874,8043,8044],{},"方式",[874,8046,8047],{},"文件数",[874,8049,8050],{},"操作",[884,8052,8053,8064],{},[871,8054,8055,8058,8061],{},[889,8056,8057],{},"旧方案",[889,8059,8060],{},"1 exe + 1 web 目录",[889,8062,8063],{},"解压→保持目录结构",[871,8065,8066,8069,8072],{},[889,8067,8068],{},"新方案",[889,8070,8071],{},"1 个 exe",[889,8073,8074],{},"直接扔到服务器运行",[26,8076,1168],{"id":1168},[15,8078,8079,8080,8082],{},"Go 的 ",[33,8081,7177],{}," 是一个强大的工具，但有两个隐藏行为需要注意：",[513,8084,8085,8092],{},[516,8086,8087,8088,7832,8090,7835],{},"嵌入目录时自动排除 ",[33,8089,7815],{},[33,8091,299],{},[516,8093,8094],{},"构建缓存可能导致 embed 不是最新的",[15,8096,8097],{},"对于前端+后端合体的场景，这是性价比最高的方案——不需要额外的打包工具，纯标准库搞定。",[1228,8099,8100],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sPKmS, html code.shiki .sPKmS{--shiki-light:#032F62;--shiki-dark:#DBEDFF}",{"title":58,"searchDepth":72,"depth":83,"links":8102},[8103,8104,8105,8110,8116,8117],{"id":13,"depth":72,"text":13},{"id":7171,"depth":72,"text":7171},{"id":7185,"depth":72,"text":7185,"children":8106},[8107,8108,8109],{"id":7188,"depth":83,"text":7189},{"id":7525,"depth":83,"text":7526},{"id":7637,"depth":83,"text":7638},{"id":7775,"depth":72,"text":7775,"children":8111},[8112,8113,8115],{"id":7778,"depth":83,"text":7779},{"id":7808,"depth":83,"text":8114},"坑二：\u002F\u002Fgo:embed 排除 _ 开头文件",{"id":7971,"depth":83,"text":7972},{"id":7991,"depth":72,"text":7991},{"id":1168,"depth":72,"text":1168},"记录将 Vue 3 前端嵌入 Go 二进制实现单 exe 发布的完整流程，以及遇到的坑和解决方案。","\u002Fposts\u002Fgo-embed-frontend\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fgo-embed-frontend","2026-06-06T16:00:00",{"title":7154,"description":8118},"posts\u002Fgo-embed-frontend\u002Findex",[2064,8126,8127,8128],"Vue","编译","埋坑","2026-06-06","SIQNL24wVAFb18wIGWSB3lNVZUQ9B76jWgQ55TASTYk",{"id":8132,"title":8133,"author":6,"body":8134,"description":9530,"difficulty":2475,"draft":1243,"extension":1244,"image":9531,"meta":9532,"navigation":752,"path":9533,"pinned":1243,"published":9534,"seo":9535,"series":6,"seriesOrder":6,"stem":9536,"tags":9537,"updated":9534,"__hash__":9539},"posts\u002Fposts\u002Fjira-cli-intro\u002Findex.md","jira_cli — 把企业 Jira 装进命令行，AI Agent 直接调用",{"type":8,"value":8135,"toc":9509},[8136,8140,8155,8222,8225,8228,8305,8307,8340,8347,8350,8354,8466,8470,8601,8605,8673,8676,8739,8742,8744,8748,8751,8771,8774,8789,8792,8796,8802,8940,8951,8982,8986,8989,9086,9090,9093,9211,9215,9218,9282,9285,9289,9292,9360,9363,9369,9372,9406,9413,9416,9476,9478,9481,9498,9506],[26,8137,8139],{"id":8138},"jira_cli-是什么","jira_cli 是什么",[15,8141,8142,8147,8148,7832,8151,8154],{},[3079,8143,8146],{"href":8144,"rel":8145},"https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli",[3083],"jira_cli"," 是一个面向企业 Jira（Server \u002F Data Center）的本地命令行工具，Go 语言编写，MIT 协议开源。它同时提供 ",[38,8149,8150],{},"Cobra CLI",[38,8152,8153],{},"MCP stdio server"," 两种入口，既能当日常命令行用，又能直接接入 AI 编程工具。",[53,8156,8158],{"className":1275,"code":8157,"language":1277,"meta":58,"style":58},"# CLI 日常使用\njira_cli issue search \"project = PROJ ORDER BY updated DESC\"\njira_cli issue get PROJ-123\njira_cli issue comment PROJ-123 \"处理完成，请测试验证\"\n\n# MCP 模式 — 给 AI 装上操作 Jira 的手\njira_cli mcp --user=work\n",[33,8159,8160,8165,8178,8189,8204,8208,8213],{"__ignoreMap":58},[62,8161,8162],{"class":64,"line":65},[62,8163,8164],{"class":68},"# CLI 日常使用\n",[62,8166,8167,8169,8172,8175],{"class":64,"line":72},[62,8168,8146],{"class":75},[62,8170,8171],{"class":273}," issue",[62,8173,8174],{"class":273}," search",[62,8176,8177],{"class":273}," \"project = PROJ ORDER BY updated DESC\"\n",[62,8179,8180,8182,8184,8186],{"class":64,"line":83},[62,8181,8146],{"class":75},[62,8183,8171],{"class":273},[62,8185,4377],{"class":273},[62,8187,8188],{"class":273}," PROJ-123\n",[62,8190,8191,8193,8195,8198,8201],{"class":64,"line":91},[62,8192,8146],{"class":75},[62,8194,8171],{"class":273},[62,8196,8197],{"class":273}," comment",[62,8199,8200],{"class":273}," PROJ-123",[62,8202,8203],{"class":273}," \"处理完成，请测试验证\"\n",[62,8205,8206],{"class":64,"line":99},[62,8207,753],{"emptyLinePlaceholder":752},[62,8209,8210],{"class":64,"line":124},[62,8211,8212],{"class":68},"# MCP 模式 — 给 AI 装上操作 Jira 的手\n",[62,8214,8215,8217,8219],{"class":64,"line":130},[62,8216,8146],{"class":75},[62,8218,4618],{"class":273},[62,8220,8221],{"class":114}," --user=work\n",[26,8223,8224],{"id":8224},"为什么需要它",[15,8226,8227],{},"大多数 Jira 命令行工具只解决\"开发者手动操作 Jira\"这一个场景。但 2026 年 AI 编程工具已经普及，你的 AI Agent（Claude Code、Codex、Cursor 等）也需要读写 Jira。痛点就来了：",[865,8229,8230,8240],{},[868,8231,8232],{},[871,8233,8234,8237],{},[874,8235,8236],{},"痛点",[874,8238,8239],{},"jira_cli 怎么解",[884,8241,8242,8250,8258,8270,8282,8293],{},[871,8243,8244,8247],{},[889,8245,8246],{},"企业 Jira 在内网，没有公网 API",[889,8248,8249],{},"本地 stdio MCP server，不经过外网",[871,8251,8252,8255],{},[889,8253,8254],{},"AI 可能误删、误写 Jira 数据",[889,8256,8257],{},"账号级只读策略，AI 只能查不能改",[871,8259,8260,8263],{},[889,8261,8262],{},"团队多人多 Jira 实例",[889,8264,8265,8266,8269],{},"多 Profile 管理，",[33,8267,8268],{},"--user=work"," 切换",[871,8271,8272,8275],{},[889,8273,8274],{},"Windows 终端体验差",[889,8276,8277,8278,8281],{},"原生 ",[33,8279,8280],{},".exe"," 构建 + 一键安装脚本",[871,8283,8284,8287],{},[889,8285,8286],{},"AI 生成 JQL 困难",[889,8288,8289,8290],{},"字段元数据发现：",[33,8291,8292],{},"field list --query xxx",[871,8294,8295,8298],{},[889,8296,8297],{},"MCP 直接拿原始 JSON 太冗长",[889,8299,8300,8301,8304],{},"紧凑视图 + ",[33,8302,8303],{},"--view summary"," 减少 token 消耗",[26,8306,6517],{"id":6517},[53,8308,8310],{"className":1275,"code":8309,"language":1277,"meta":58,"style":58},"# 从源码安装（要求 Go 1.26.3+）\ngo install github.com\u002Fyourname\u002Fjira_cli@latest\n\n# 或下载预编译二进制\n# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[33,8311,8312,8317,8326,8330,8335],{"__ignoreMap":58},[62,8313,8314],{"class":64,"line":65},[62,8315,8316],{"class":68},"# 从源码安装（要求 Go 1.26.3+）\n",[62,8318,8319,8321,8323],{"class":64,"line":72},[62,8320,1398],{"class":75},[62,8322,1347],{"class":273},[62,8324,8325],{"class":273}," github.com\u002Fyourname\u002Fjira_cli@latest\n",[62,8327,8328],{"class":64,"line":83},[62,8329,753],{"emptyLinePlaceholder":752},[62,8331,8332],{"class":64,"line":91},[62,8333,8334],{"class":68},"# 或下载预编译二进制\n",[62,8336,8337],{"class":64,"line":99},[62,8338,8339],{"class":68},"# https:\u002F\u002Fgithub.com\u002Fyourname\u002Fjira_cli\u002Freleases\n",[15,8341,8342,8343,8346],{},"Windows 用户下载 zip 包解压后双击 ",[33,8344,8345],{},"install.bat"," 即可全局安装，自动加入 PATH。",[26,8348,8349],{"id":8349},"快速上手",[1503,8351,8353],{"id":8352},"_1-配置认证","1. 配置认证",[53,8355,8357],{"className":1275,"code":8356,"language":1277,"meta":58,"style":58},"# 设置 Jira 地址\njira_cli set base-url https:\u002F\u002Fjira.your-company.com --global\n\n# Token 认证（推荐）\njira_cli set token \u003Cyour-token> --global\n\n# 或用户名密码认证\njira_cli set auth \u003Cusername> \u003Cpassword> --global\n\n# 验证一下\njira_cli user me\n",[33,8358,8359,8364,8380,8384,8389,8409,8413,8418,8447,8451,8456],{"__ignoreMap":58},[62,8360,8361],{"class":64,"line":65},[62,8362,8363],{"class":68},"# 设置 Jira 地址\n",[62,8365,8366,8368,8371,8374,8377],{"class":64,"line":72},[62,8367,8146],{"class":75},[62,8369,8370],{"class":273}," set",[62,8372,8373],{"class":273}," base-url",[62,8375,8376],{"class":273}," https:\u002F\u002Fjira.your-company.com",[62,8378,8379],{"class":114}," --global\n",[62,8381,8382],{"class":64,"line":83},[62,8383,753],{"emptyLinePlaceholder":752},[62,8385,8386],{"class":64,"line":91},[62,8387,8388],{"class":68},"# Token 认证（推荐）\n",[62,8390,8391,8393,8395,8398,8400,8403,8405,8407],{"class":64,"line":99},[62,8392,8146],{"class":75},[62,8394,8370],{"class":273},[62,8396,8397],{"class":273}," token",[62,8399,1798],{"class":166},[62,8401,8402],{"class":273},"your-toke",[62,8404,2198],{"class":79},[62,8406,1807],{"class":166},[62,8408,8379],{"class":114},[62,8410,8411],{"class":64,"line":124},[62,8412,753],{"emptyLinePlaceholder":752},[62,8414,8415],{"class":64,"line":130},[62,8416,8417],{"class":68},"# 或用户名密码认证\n",[62,8419,8420,8422,8424,8426,8428,8431,8433,8435,8437,8440,8443,8445],{"class":64,"line":136},[62,8421,8146],{"class":75},[62,8423,8370],{"class":273},[62,8425,6591],{"class":273},[62,8427,1798],{"class":166},[62,8429,8430],{"class":273},"usernam",[62,8432,2186],{"class":79},[62,8434,1807],{"class":166},[62,8436,1798],{"class":166},[62,8438,8439],{"class":273},"passwor",[62,8441,8442],{"class":79},"d",[62,8444,1807],{"class":166},[62,8446,8379],{"class":114},[62,8448,8449],{"class":64,"line":402},[62,8450,753],{"emptyLinePlaceholder":752},[62,8452,8453],{"class":64,"line":407},[62,8454,8455],{"class":68},"# 验证一下\n",[62,8457,8458,8460,8463],{"class":64,"line":433},[62,8459,8146],{"class":75},[62,8461,8462],{"class":273}," user",[62,8464,8465],{"class":273}," me\n",[1503,8467,8469],{"id":8468},"_2-日常使用","2. 日常使用",[53,8471,8473],{"className":1275,"code":8472,"language":1277,"meta":58,"style":58},"# 查自己今天的任务\njira_cli issue search \"assignee = currentUser() ORDER BY updated DESC\"\n\n# 看某个任务详情\njira_cli issue get PROJ-123\n\n# 流转状态\njira_cli issue transitions PROJ-123                    # 可以转哪些\njira_cli issue do-transition PROJ-123 21               # 执行流转\n\n# 添加备注\njira_cli issue comment PROJ-123 \"已定位到问题，正在修复\"\n\n# 创建任务\njira_cli issue create PROJ Task --fields @fields.json\n",[33,8474,8475,8480,8491,8495,8500,8510,8514,8519,8533,8550,8554,8559,8572,8576,8581],{"__ignoreMap":58},[62,8476,8477],{"class":64,"line":65},[62,8478,8479],{"class":68},"# 查自己今天的任务\n",[62,8481,8482,8484,8486,8488],{"class":64,"line":72},[62,8483,8146],{"class":75},[62,8485,8171],{"class":273},[62,8487,8174],{"class":273},[62,8489,8490],{"class":273}," \"assignee = currentUser() ORDER BY updated DESC\"\n",[62,8492,8493],{"class":64,"line":83},[62,8494,753],{"emptyLinePlaceholder":752},[62,8496,8497],{"class":64,"line":91},[62,8498,8499],{"class":68},"# 看某个任务详情\n",[62,8501,8502,8504,8506,8508],{"class":64,"line":99},[62,8503,8146],{"class":75},[62,8505,8171],{"class":273},[62,8507,4377],{"class":273},[62,8509,8188],{"class":273},[62,8511,8512],{"class":64,"line":124},[62,8513,753],{"emptyLinePlaceholder":752},[62,8515,8516],{"class":64,"line":130},[62,8517,8518],{"class":68},"# 流转状态\n",[62,8520,8521,8523,8525,8528,8530],{"class":64,"line":136},[62,8522,8146],{"class":75},[62,8524,8171],{"class":273},[62,8526,8527],{"class":273}," transitions",[62,8529,8200],{"class":273},[62,8531,8532],{"class":68},"                    # 可以转哪些\n",[62,8534,8535,8537,8539,8542,8544,8547],{"class":64,"line":402},[62,8536,8146],{"class":75},[62,8538,8171],{"class":273},[62,8540,8541],{"class":273}," do-transition",[62,8543,8200],{"class":273},[62,8545,8546],{"class":114}," 21",[62,8548,8549],{"class":68},"               # 执行流转\n",[62,8551,8552],{"class":64,"line":407},[62,8553,753],{"emptyLinePlaceholder":752},[62,8555,8556],{"class":64,"line":433},[62,8557,8558],{"class":68},"# 添加备注\n",[62,8560,8561,8563,8565,8567,8569],{"class":64,"line":438},[62,8562,8146],{"class":75},[62,8564,8171],{"class":273},[62,8566,8197],{"class":273},[62,8568,8200],{"class":273},[62,8570,8571],{"class":273}," \"已定位到问题，正在修复\"\n",[62,8573,8574],{"class":64,"line":446},[62,8575,753],{"emptyLinePlaceholder":752},[62,8577,8578],{"class":64,"line":1448},[62,8579,8580],{"class":68},"# 创建任务\n",[62,8582,8583,8585,8587,8589,8592,8595,8598],{"class":64,"line":1459},[62,8584,8146],{"class":75},[62,8586,8171],{"class":273},[62,8588,6477],{"class":273},[62,8590,8591],{"class":273}," PROJ",[62,8593,8594],{"class":273}," Task",[62,8596,8597],{"class":114}," --fields",[62,8599,8600],{"class":273}," @fields.json\n",[1503,8602,8604],{"id":8603},"_3-配给-ai-使用","3. 配给 AI 使用",[53,8606,8608],{"className":1275,"code":8607,"language":1277,"meta":58,"style":58},"# 先配置一个只读账号给 AI 用\njira_cli set base-url https:\u002F\u002Fjira.your-company.com --name ai --global\njira_cli set token \u003Ctoken> --name ai --global\njira_cli set readonly on --name ai --global\n",[33,8609,8610,8615,8632,8655],{"__ignoreMap":58},[62,8611,8612],{"class":64,"line":65},[62,8613,8614],{"class":68},"# 先配置一个只读账号给 AI 用\n",[62,8616,8617,8619,8621,8623,8625,8627,8630],{"class":64,"line":72},[62,8618,8146],{"class":75},[62,8620,8370],{"class":273},[62,8622,8373],{"class":273},[62,8624,8376],{"class":273},[62,8626,1795],{"class":114},[62,8628,8629],{"class":273}," ai",[62,8631,8379],{"class":114},[62,8633,8634,8636,8638,8640,8642,8645,8647,8649,8651,8653],{"class":64,"line":83},[62,8635,8146],{"class":75},[62,8637,8370],{"class":273},[62,8639,8397],{"class":273},[62,8641,1798],{"class":166},[62,8643,8644],{"class":273},"toke",[62,8646,2198],{"class":79},[62,8648,1807],{"class":166},[62,8650,1795],{"class":114},[62,8652,8629],{"class":273},[62,8654,8379],{"class":114},[62,8656,8657,8659,8661,8664,8667,8669,8671],{"class":64,"line":91},[62,8658,8146],{"class":75},[62,8660,8370],{"class":273},[62,8662,8663],{"class":273}," readonly",[62,8665,8666],{"class":273}," on",[62,8668,1795],{"class":114},[62,8670,8629],{"class":273},[62,8672,8379],{"class":114},[15,8674,8675],{},"然后在 AI 工具的 MCP 配置里加上：",[53,8677,8679],{"className":6188,"code":8678,"language":6190,"meta":58,"style":58},"{\n  \"mcpServers\": {\n    \"jira\": {\n      \"command\": \"jira_cli\",\n      \"args\": [\"mcp\", \"--user=ai\"]\n    }\n  }\n}\n",[33,8680,8681,8685,8691,8698,8710,8727,8731,8735],{"__ignoreMap":58},[62,8682,8683],{"class":64,"line":65},[62,8684,6197],{"class":79},[62,8686,8687,8689],{"class":64,"line":72},[62,8688,6202],{"class":114},[62,8690,80],{"class":79},[62,8692,8693,8696],{"class":64,"line":83},[62,8694,8695],{"class":114},"    \"jira\"",[62,8697,80],{"class":79},[62,8699,8700,8703,8705,8708],{"class":64,"line":91},[62,8701,8702],{"class":114},"      \"command\"",[62,8704,111],{"class":79},[62,8706,8707],{"class":273},"\"jira_cli\"",[62,8709,2912],{"class":79},[62,8711,8712,8715,8717,8720,8722,8725],{"class":64,"line":99},[62,8713,8714],{"class":114},"      \"args\"",[62,8716,2994],{"class":79},[62,8718,8719],{"class":273},"\"mcp\"",[62,8721,186],{"class":79},[62,8723,8724],{"class":273},"\"--user=ai\"",[62,8726,1720],{"class":79},[62,8728,8729],{"class":64,"line":124},[62,8730,127],{"class":79},[62,8732,8733],{"class":64,"line":130},[62,8734,133],{"class":79},[62,8736,8737],{"class":64,"line":136},[62,8738,139],{"class":79},[15,8740,8741],{},"这样 AI 就能查 Jira 但不能修改，既有了上下文，又不用担心误操作。",[26,8743,6999],{"id":6999},[1503,8745,8747],{"id":8746},"_1-账号级只读策略","1. 账号级只读策略",[15,8749,8750],{},"这是 jira_cli 最实用的设计。给 AI Agent 配置只读账号：",[53,8752,8754],{"className":1275,"code":8753,"language":1277,"meta":58,"style":58},"jira_cli set readonly on --name ai\n",[33,8755,8756],{"__ignoreMap":58},[62,8757,8758,8760,8762,8764,8766,8768],{"class":64,"line":65},[62,8759,8146],{"class":75},[62,8761,8370],{"class":273},[62,8763,8663],{"class":273},[62,8765,8666],{"class":273},[62,8767,1795],{"class":114},[62,8769,8770],{"class":273}," ai\n",[15,8772,8773],{},"只读模式下：",[2810,8775,8776,8779,8782],{},[516,8777,8778],{},"CLI 写命令被隐藏和拒绝",[516,8780,8781],{},"MCP 的写入工具不会注册",[516,8783,8784,8785,8788],{},"环境变量 ",[33,8786,8787],{},"JIRA_READONLY=true"," 也可以临时覆盖",[15,8790,8791],{},"AI 只能查 JQL、看任务、查字段、读评论，绝对不会误改数据。需要创建\u002F更新时，另配一个有写入权限的账号由开发者自己操作。",[1503,8793,8795],{"id":8794},"_2-多-profile-切换","2. 多 Profile 切换",[15,8797,8798,8799,8801],{},"一个配置文件里管理多个 Jira 账号，",[33,8800,2151],{}," 切换：",[53,8803,8805],{"className":1275,"code":8804,"language":1277,"meta":58,"style":58},"# 工作账号\njira_cli set base-url https:\u002F\u002Fjira.company.com --name work --global\njira_cli set token \u003Cwork-token> --name work --global\n\n# 个人账号\njira_cli set base-url https:\u002F\u002Fjira.personal.com --name personal --global\njira_cli set token \u003Cpersonal-token> --name personal --global\n\n# 使用时切换\njira_cli --user=work issue search \"project = PROJ\"\njira_cli --user=personal issue search \"project = SIDE\"\n",[33,8806,8807,8812,8830,8853,8857,8862,8880,8903,8907,8912,8926],{"__ignoreMap":58},[62,8808,8809],{"class":64,"line":65},[62,8810,8811],{"class":68},"# 工作账号\n",[62,8813,8814,8816,8818,8820,8823,8825,8828],{"class":64,"line":72},[62,8815,8146],{"class":75},[62,8817,8370],{"class":273},[62,8819,8373],{"class":273},[62,8821,8822],{"class":273}," https:\u002F\u002Fjira.company.com",[62,8824,1795],{"class":114},[62,8826,8827],{"class":273}," work",[62,8829,8379],{"class":114},[62,8831,8832,8834,8836,8838,8840,8843,8845,8847,8849,8851],{"class":64,"line":83},[62,8833,8146],{"class":75},[62,8835,8370],{"class":273},[62,8837,8397],{"class":273},[62,8839,1798],{"class":166},[62,8841,8842],{"class":273},"work-toke",[62,8844,2198],{"class":79},[62,8846,1807],{"class":166},[62,8848,1795],{"class":114},[62,8850,8827],{"class":273},[62,8852,8379],{"class":114},[62,8854,8855],{"class":64,"line":91},[62,8856,753],{"emptyLinePlaceholder":752},[62,8858,8859],{"class":64,"line":99},[62,8860,8861],{"class":68},"# 个人账号\n",[62,8863,8864,8866,8868,8870,8873,8875,8878],{"class":64,"line":124},[62,8865,8146],{"class":75},[62,8867,8370],{"class":273},[62,8869,8373],{"class":273},[62,8871,8872],{"class":273}," https:\u002F\u002Fjira.personal.com",[62,8874,1795],{"class":114},[62,8876,8877],{"class":273}," personal",[62,8879,8379],{"class":114},[62,8881,8882,8884,8886,8888,8890,8893,8895,8897,8899,8901],{"class":64,"line":130},[62,8883,8146],{"class":75},[62,8885,8370],{"class":273},[62,8887,8397],{"class":273},[62,8889,1798],{"class":166},[62,8891,8892],{"class":273},"personal-toke",[62,8894,2198],{"class":79},[62,8896,1807],{"class":166},[62,8898,1795],{"class":114},[62,8900,8877],{"class":273},[62,8902,8379],{"class":114},[62,8904,8905],{"class":64,"line":136},[62,8906,753],{"emptyLinePlaceholder":752},[62,8908,8909],{"class":64,"line":402},[62,8910,8911],{"class":68},"# 使用时切换\n",[62,8913,8914,8916,8919,8921,8923],{"class":64,"line":407},[62,8915,8146],{"class":75},[62,8917,8918],{"class":114}," --user=work",[62,8920,8171],{"class":273},[62,8922,8174],{"class":273},[62,8924,8925],{"class":273}," \"project = PROJ\"\n",[62,8927,8928,8930,8933,8935,8937],{"class":64,"line":433},[62,8929,8146],{"class":75},[62,8931,8932],{"class":114}," --user=personal",[62,8934,8171],{"class":273},[62,8936,8174],{"class":273},[62,8938,8939],{"class":273}," \"project = SIDE\"\n",[15,8941,8942,8943,8946,8947,8950],{},"同时支持",[38,8944,8945],{},"项目级配置","（",[33,8948,8949],{},"--local","）和工作目录绑定：",[53,8952,8954],{"className":1275,"code":8953,"language":1277,"meta":58,"style":58},"# 在项目目录下，配置只对当前项目生效\ncd ~\u002Fprojects\u002Fbackend\njira_cli set base-url https:\u002F\u002Fjira-backend.company.com --local\n",[33,8955,8956,8961,8968],{"__ignoreMap":58},[62,8957,8958],{"class":64,"line":65},[62,8959,8960],{"class":68},"# 在项目目录下，配置只对当前项目生效\n",[62,8962,8963,8965],{"class":64,"line":72},[62,8964,7741],{"class":114},[62,8966,8967],{"class":273}," ~\u002Fprojects\u002Fbackend\n",[62,8969,8970,8972,8974,8976,8979],{"class":64,"line":83},[62,8971,8146],{"class":75},[62,8973,8370],{"class":273},[62,8975,8373],{"class":273},[62,8977,8978],{"class":273}," https:\u002F\u002Fjira-backend.company.com",[62,8980,8981],{"class":114}," --local\n",[1503,8983,8985],{"id":8984},"_3-对-ai-友好的输出","3. 对 AI 友好的输出",[15,8987,8988],{},"jira_cli 在输出设计上充分考虑了 AI Agent 的 token 消耗：",[53,8990,8992],{"className":1275,"code":8991,"language":1277,"meta":58,"style":58},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\njira_cli issue get PROJ-123 --view summary\n\n# 表格输出 — 一眼看到关键信息\njira_cli issue search \"project = PROJ\" --output table\n\n# JSON 输出 — 程序解析，不转义 Unicode\njira_cli --output json issue get PROJ-123\n\n# 自定义搜索列 — 只要需要的字段\njira_cli set search-columns key,summary,fixVersions\n",[33,8993,8994,8999,9015,9019,9024,9041,9045,9050,9065,9069,9074],{"__ignoreMap":58},[62,8995,8996],{"class":64,"line":65},[62,8997,8998],{"class":68},"# 紧凑视图 — 去掉 self\u002Favatar\u002F缩略图等无用字段\n",[62,9000,9001,9003,9005,9007,9009,9012],{"class":64,"line":72},[62,9002,8146],{"class":75},[62,9004,8171],{"class":273},[62,9006,4377],{"class":273},[62,9008,8200],{"class":273},[62,9010,9011],{"class":114}," --view",[62,9013,9014],{"class":273}," summary\n",[62,9016,9017],{"class":64,"line":83},[62,9018,753],{"emptyLinePlaceholder":752},[62,9020,9021],{"class":64,"line":91},[62,9022,9023],{"class":68},"# 表格输出 — 一眼看到关键信息\n",[62,9025,9026,9028,9030,9032,9035,9038],{"class":64,"line":99},[62,9027,8146],{"class":75},[62,9029,8171],{"class":273},[62,9031,8174],{"class":273},[62,9033,9034],{"class":273}," \"project = PROJ\"",[62,9036,9037],{"class":114}," --output",[62,9039,9040],{"class":273}," table\n",[62,9042,9043],{"class":64,"line":124},[62,9044,753],{"emptyLinePlaceholder":752},[62,9046,9047],{"class":64,"line":130},[62,9048,9049],{"class":68},"# JSON 输出 — 程序解析，不转义 Unicode\n",[62,9051,9052,9054,9056,9059,9061,9063],{"class":64,"line":136},[62,9053,8146],{"class":75},[62,9055,9037],{"class":114},[62,9057,9058],{"class":273}," json",[62,9060,8171],{"class":273},[62,9062,4377],{"class":273},[62,9064,8188],{"class":273},[62,9066,9067],{"class":64,"line":402},[62,9068,753],{"emptyLinePlaceholder":752},[62,9070,9071],{"class":64,"line":407},[62,9072,9073],{"class":68},"# 自定义搜索列 — 只要需要的字段\n",[62,9075,9076,9078,9080,9083],{"class":64,"line":433},[62,9077,8146],{"class":75},[62,9079,8370],{"class":273},[62,9081,9082],{"class":273}," search-columns",[62,9084,9085],{"class":273}," key,summary,fixVersions\n",[1503,9087,9089],{"id":9088},"_4-字段元数据发现","4. 字段元数据发现",[15,9091,9092],{},"Jira 的 customfield 让人头疼，jira_cli 提供了完整的字段发现能力：",[53,9094,9096],{"className":1275,"code":9095,"language":1277,"meta":58,"style":58},"# 列出所有字段及其类型\njira_cli field list\n\n# 搜索包含特定关键词的字段\njira_cli field list --query \"版本\"\n\n# 只看自定义字段\njira_cli field list --custom-only\n\n# 创建任务前，先看看需要哪些字段\njira_cli issue create-meta PROJ --issue-type Task\n\n# 分页查看可选值\njira_cli issue create-meta-values PROJ Task priority --query High\n",[33,9097,9098,9103,9112,9116,9121,9135,9139,9144,9155,9159,9164,9181,9185,9190],{"__ignoreMap":58},[62,9099,9100],{"class":64,"line":65},[62,9101,9102],{"class":68},"# 列出所有字段及其类型\n",[62,9104,9105,9107,9110],{"class":64,"line":72},[62,9106,8146],{"class":75},[62,9108,9109],{"class":273}," field",[62,9111,230],{"class":273},[62,9113,9114],{"class":64,"line":83},[62,9115,753],{"emptyLinePlaceholder":752},[62,9117,9118],{"class":64,"line":91},[62,9119,9120],{"class":68},"# 搜索包含特定关键词的字段\n",[62,9122,9123,9125,9127,9129,9132],{"class":64,"line":99},[62,9124,8146],{"class":75},[62,9126,9109],{"class":273},[62,9128,6412],{"class":273},[62,9130,9131],{"class":114}," --query",[62,9133,9134],{"class":273}," \"版本\"\n",[62,9136,9137],{"class":64,"line":124},[62,9138,753],{"emptyLinePlaceholder":752},[62,9140,9141],{"class":64,"line":130},[62,9142,9143],{"class":68},"# 只看自定义字段\n",[62,9145,9146,9148,9150,9152],{"class":64,"line":136},[62,9147,8146],{"class":75},[62,9149,9109],{"class":273},[62,9151,6412],{"class":273},[62,9153,9154],{"class":114}," --custom-only\n",[62,9156,9157],{"class":64,"line":402},[62,9158,753],{"emptyLinePlaceholder":752},[62,9160,9161],{"class":64,"line":407},[62,9162,9163],{"class":68},"# 创建任务前，先看看需要哪些字段\n",[62,9165,9166,9168,9170,9173,9175,9178],{"class":64,"line":433},[62,9167,8146],{"class":75},[62,9169,8171],{"class":273},[62,9171,9172],{"class":273}," create-meta",[62,9174,8591],{"class":273},[62,9176,9177],{"class":114}," --issue-type",[62,9179,9180],{"class":273}," Task\n",[62,9182,9183],{"class":64,"line":438},[62,9184,753],{"emptyLinePlaceholder":752},[62,9186,9187],{"class":64,"line":446},[62,9188,9189],{"class":68},"# 分页查看可选值\n",[62,9191,9192,9194,9196,9199,9201,9203,9206,9208],{"class":64,"line":1448},[62,9193,8146],{"class":75},[62,9195,8171],{"class":273},[62,9197,9198],{"class":273}," create-meta-values",[62,9200,8591],{"class":273},[62,9202,8594],{"class":273},[62,9204,9205],{"class":273}," priority",[62,9207,9131],{"class":114},[62,9209,9210],{"class":273}," High\n",[1503,9212,9214],{"id":9213},"_5-mcp-写入控制","5. MCP 写入控制",[15,9216,9217],{},"除了全局只读，MCP 还有更细粒度的控制：",[865,9219,9220,9230],{},[868,9221,9222],{},[871,9223,9224,9227],{},[874,9225,9226],{},"环境变量",[874,9228,9229],{},"作用",[884,9231,9232,9242,9252,9262,9272],{},[871,9233,9234,9239],{},[889,9235,9236],{},[33,9237,9238],{},"JIRA_CREATE_OPEN=false",[889,9240,9241],{},"禁用创建\u002F更新工具",[871,9243,9244,9249],{},[889,9245,9246],{},[33,9247,9248],{},"JIRA_DISABLE_DISCLAIMER=true",[889,9250,9251],{},"去掉 AI 自动添加的备注声明",[871,9253,9254,9259],{},[889,9255,9256],{},[33,9257,9258],{},"JIRA_CREATE_SUMMARY_MAX",[889,9260,9261],{},"限制摘要长度",[871,9263,9264,9269],{},[889,9265,9266],{},[33,9267,9268],{},"JIRA_CREATE_DESCRIPTION_MAX",[889,9270,9271],{},"限制描述长度",[871,9273,9274,9279],{},[889,9275,9276],{},[33,9277,9278],{},"JIRA_CREATE_FIELDS_MAX_BYTES",[889,9280,9281],{},"限制创建字段的 JSON 大小",[15,9283,9284],{},"这些限制确保 AI 即使有写入权限也不会\"失控\"——例如防止 AI 往描述里塞几万字的废话。",[1503,9286,9288],{"id":9287},"_6-中英双语帮助","6. 中英双语帮助",[15,9290,9291],{},"帮助信息默认中文，也支持一键切英文：",[53,9293,9295],{"className":1275,"code":9294,"language":1277,"meta":58,"style":58},"# 保存英文偏好\njira_cli set help-language en\n\n# 切回中文\njira_cli set help-language zh\n\n# 临时覆盖\njira_cli --lang en issue search -h\n",[33,9296,9297,9302,9314,9318,9323,9334,9338,9343],{"__ignoreMap":58},[62,9298,9299],{"class":64,"line":65},[62,9300,9301],{"class":68},"# 保存英文偏好\n",[62,9303,9304,9306,9308,9311],{"class":64,"line":72},[62,9305,8146],{"class":75},[62,9307,8370],{"class":273},[62,9309,9310],{"class":273}," help-language",[62,9312,9313],{"class":273}," en\n",[62,9315,9316],{"class":64,"line":83},[62,9317,753],{"emptyLinePlaceholder":752},[62,9319,9320],{"class":64,"line":91},[62,9321,9322],{"class":68},"# 切回中文\n",[62,9324,9325,9327,9329,9331],{"class":64,"line":99},[62,9326,8146],{"class":75},[62,9328,8370],{"class":273},[62,9330,9310],{"class":273},[62,9332,9333],{"class":273}," zh\n",[62,9335,9336],{"class":64,"line":124},[62,9337,753],{"emptyLinePlaceholder":752},[62,9339,9340],{"class":64,"line":130},[62,9341,9342],{"class":68},"# 临时覆盖\n",[62,9344,9345,9347,9350,9353,9355,9357],{"class":64,"line":136},[62,9346,8146],{"class":75},[62,9348,9349],{"class":114}," --lang",[62,9351,9352],{"class":273}," en",[62,9354,8171],{"class":273},[62,9356,8174],{"class":273},[62,9358,9359],{"class":114}," -h\n",[26,9361,9362],{"id":9362},"架构一览",[53,9364,9367],{"className":9365,"code":9366,"language":2580},[2578],"main.go\n  cmd\u002F                         CLI 命令、MCP 启动、访问策略\n  internal\u002Fconfig\u002F             运行时配置、Profile 持久化\n  internal\u002Ftools\u002Fjira_tools\u002F   MCP 输入\u002F输出适配层\n  pkg\u002Fjira\u002F                    可复用的 Jira REST 客户端\n",[33,9368,9366],{"__ignoreMap":58},[15,9370,9371],{},"设计原则清晰：",[2810,9373,9374,9382,9390,9398],{},[516,9375,9376,9381],{},[38,9377,9378],{},[33,9379,9380],{},"pkg\u002Fjira"," 是纯 Jira 客户端，HTTP、认证、上下文处理都在这里",[516,9383,9384,9389],{},[38,9385,9386],{},[33,9387,9388],{},"cmd"," 只管 Cobra 解析和呈现",[516,9391,9392,9397],{},[38,9393,9394],{},[33,9395,9396],{},"internal\u002Ftools\u002Fjira_tools"," 只管 MCP schema 验证和结果适配",[516,9399,9400,9405],{},[38,9401,9402],{},[33,9403,9404],{},"internal\u002Fconfig"," 拥有 Profile 的全部逻辑，不分散到 cmd",[15,9407,9408,9409,9412],{},"每一层职责单一，新增 Jira 能力时按 ",[33,9410,9411],{},"endpoints.go → Client 方法 → CLI 命令 → MCP tool → 策略注册"," 的路径走就行。",[26,9414,9415],{"id":9415},"和已有方案的对比",[865,9417,9418,9430],{},[868,9419,9420],{},[871,9421,9422,9424,9427],{},[874,9423,876],{},[874,9425,9426],{},"适合场景",[874,9428,9429],{},"jira_cli 的区别",[884,9431,9432,9443,9454,9465],{},[871,9433,9434,9437,9440],{},[889,9435,9436],{},"Atlassian Rovo MCP Server",[889,9438,9439],{},"Jira Cloud、官方 OAuth",[889,9441,9442],{},"jira_cli 是本地 stdio，适合内网 Server\u002FDC",[871,9444,9445,9448,9451],{},[889,9446,9447],{},"go-jira \u002F jira-cli 社区版",[889,9449,9450],{},"纯命令行管理",[889,9452,9453],{},"jira_cli 同时提供 MCP server，面向 AI 工作流",[871,9455,9456,9459,9462],{},[889,9457,9458],{},"直接调 REST API",[889,9460,9461],{},"自定义集成",[889,9463,9464],{},"jira_cli 封装了认证、只读策略、字段元数据",[871,9466,9467,9470,9473],{},[889,9468,9469],{},"Jira 网页",[889,9471,9472],{},"日常使用",[889,9474,9475],{},"jira_cli 把 Jira 变成终端里的一行命令",[26,9477,1168],{"id":1168},[15,9479,9480],{},"如果你的团队满足以下任一条件，jira_cli 值得一试：",[2810,9482,9483,9486,9489,9492,9495],{},[516,9484,9485],{},"企业 Jira 部署在内网，想接入 AI 编程工具",[516,9487,9488],{},"想给 AI Agent 只读权限来查 Jira 上下文",[516,9490,9491],{},"需要命令行快速查看\u002F操作 Jira，不想切浏览器",[516,9493,9494],{},"需要管理多个 Jira 实例和多套账号",[516,9496,9497],{},"Windows 用户想要无摩擦的 CLI 体验",[15,9499,9500,9501,9505],{},"GitHub：",[3079,9502,9504],{"href":8144,"rel":9503},[3083],"github.com\u002Fyourname\u002Fjira_cli"," | MIT 协议 | Go 1.26.3+",[1228,9507,9508],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}",{"title":58,"searchDepth":72,"depth":83,"links":9510},[9511,9512,9513,9514,9519,9527,9528,9529],{"id":8138,"depth":72,"text":8139},{"id":8224,"depth":72,"text":8224},{"id":6517,"depth":72,"text":6517},{"id":8349,"depth":72,"text":8349,"children":9515},[9516,9517,9518],{"id":8352,"depth":83,"text":8353},{"id":8468,"depth":83,"text":8469},{"id":8603,"depth":83,"text":8604},{"id":6999,"depth":72,"text":6999,"children":9520},[9521,9522,9523,9524,9525,9526],{"id":8746,"depth":83,"text":8747},{"id":8794,"depth":83,"text":8795},{"id":8984,"depth":83,"text":8985},{"id":9088,"depth":83,"text":9089},{"id":9213,"depth":83,"text":9214},{"id":9287,"depth":83,"text":9288},{"id":9362,"depth":72,"text":9362},{"id":9415,"depth":72,"text":9415},{"id":1168,"depth":72,"text":1168},"介绍 jira_cli，一个开源的企业 Jira CLI 命令行工具和MCP，支持 Jira Server \u002F Data Center，自带 MCP stdio server，让 Claude Code、Codex 等 AI 编程工具直接读写 Jira 任务。","\u002Fposts\u002Fjira-cli-intro\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fjira-cli-intro","2026-07-17",{"title":8133,"description":9530},"posts\u002Fjira-cli-intro\u002Findex",[9538,7149,6364,2064,6366,7150],"Jira","vNH6sF2mMFmSjbHoEI2OqmYCgpFmaqyKt-MUfcQqXrU",{"id":9541,"title":9542,"author":6,"body":9543,"description":10087,"difficulty":2475,"draft":1243,"extension":1244,"image":10088,"meta":10089,"navigation":752,"path":10090,"pinned":1243,"published":10091,"seo":10092,"series":10093,"seriesOrder":65,"stem":10094,"tags":10095,"updated":10098,"__hash__":10099},"posts\u002Fposts\u002Fsublog-evolution\u002Findex.md","尼克的小窝 架构变迁：从纯静态到前后端分离",{"type":8,"value":9544,"toc":10067},[9545,9549,9552,9563,9567,9571,9577,9581,9612,9615,9629,9633,9636,9704,9708,9711,9717,9721,9731,9761,9764,9768,9776,9779,9823,9827,9948,9952,9959,9963,9969,9975,9979,9982,10033,10037,10044,10051,10057,10060,10062],[26,9546,9548],{"id":9547},"写在前面","📝 写在前面",[15,9550,9551],{},"SuBlog 最初只是一个简单的技术博客——Nuxt 3 静态生成，Markdown 写文章，部署到静态托管完事。但随着功能需求的增长，纯静态的架构开始显露出局限性。",[15,9553,9554,9555,9558,9559,9562],{},"这篇文章记录从 ",[38,9556,9557],{},"纯静态"," 到 ",[38,9560,9561],{},"前后端分离"," 的演进过程，带你了解每一步背后的思考。",[26,9564,9566],{"id":9565},"第一代纯静态nuxt-3-nuxtcontent","📦 第一代：纯静态（Nuxt 3 + @nuxt\u002Fcontent）",[1503,9568,9570],{"id":9569},"️-架构图","🏗️ 架构图",[53,9572,9575],{"className":9573,"code":9574,"language":2580,"meta":58},[2578],"┌─────────────────────────────────────┐\n│          静态托管 (Nginx\u002FVercel)      │\n│                                     │\n│  ┌───────────────────────────────┐  │\n│  │     Nuxt 3 SSG 产物            │  │\n│  │                               │  │\n│  │  pages\u002F + content\u002F + public\u002F  │  │\n│  └───────────────────────────────┘  │\n│                                     │\n│  Giscus (GitHub Discussions 评论)    │\n└─────────────────────────────────────┘\n",[33,9576,9574],{"__ignoreMap":58},[1503,9578,9580],{"id":9579},"做了什么","🔧 做了什么",[2810,9582,9583,9592,9597,9602],{},[516,9584,9585,2441,9587,2441,9589,9591],{},[38,9586,2749],{},[38,9588,2759],{},[38,9590,2769],{}," 搭建前端",[516,9593,9594,9596],{},[38,9595,2779],{}," 管理 Markdown 文章，编译时生成静态页面",[516,9598,9599,9601],{},[38,9600,2799],{}," 嵌入评论，依赖 GitHub Discussions",[516,9603,9604,9605,9608,9609,9611],{},"部署通过 ",[33,9606,9607],{},"nuxt generate"," 输出 ",[33,9610,3036],{},"，丢到 Nginx 或 Vercel",[1503,9613,9614],{"id":879},"✅ 优点",[2810,9616,9617,9623,9626],{},[516,9618,9619,9620,9622],{},"部署极简，一个 ",[33,9621,9607],{}," 搞定",[516,9624,9625],{},"加载飞快，全是 HTML，没有服务端开销",[516,9627,9628],{},"无需后端，零运维成本",[1503,9630,9632],{"id":9631},"局限","❌ 局限",[15,9634,9635],{},"随着时间的推移，纯静态的问题越来越明显：",[865,9637,9638,9647],{},[868,9639,9640],{},[871,9641,9642,9644],{},[874,9643,1177],{},[874,9645,9646],{},"具体表现",[884,9648,9649,9660,9671,9682,9693],{},[871,9650,9651,9657],{},[889,9652,9653,9654],{},"🐢 ",[38,9655,9656],{},"内容更新慢",[889,9658,9659],{},"改一篇文章要重新构建、重新部署，不能实时发布",[871,9661,9662,9668],{},[889,9663,9664,9665],{},"👤 ",[38,9666,9667],{},"没有用户体系",[889,9669,9670],{},"无法区分读者和管理员，所有内容对所有人一致可见",[871,9672,9673,9679],{},[889,9674,9675,9676],{},"🔒 ",[38,9677,9678],{},"评论受限",[889,9680,9681],{},"Giscus 需要 GitHub 账号才能评论，门槛高",[871,9683,9684,9690],{},[889,9685,9686,9687],{},"🚫 ",[38,9688,9689],{},"无法做动态功能",[889,9691,9692],{},"点赞、收藏、阅读统计、个性化推荐……统统做不了",[871,9694,9695,9701],{},[889,9696,9697,9698],{},"🔗 ",[38,9699,9700],{},"前端锁死",[889,9702,9703],{},"被 Nuxt\u002FVue 生态绑定，想换技术栈得重写",[26,9705,9707],{"id":9706},"第二代前后端分离react-go","⚡ 第二代：前后端分离（React + Go）",[1503,9709,9570],{"id":9710},"️-架构图-1",[53,9712,9715],{"className":9713,"code":9714,"language":2580,"meta":58},[2578],"┌───────────────────────┐       ┌────────────────────┐\n│    静态托管 (Vercel)    │       │   Go 服务端         │\n│                       │       │                    │\n│  ┌─────────────────┐  │       │  ┌──────────────┐  │\n│  │   sublog-react   │  │  API  │  │  HTTP API    │  │\n│  │  (Next.js SSG)  │──┼───────┼─>│              │  │\n│  │                  │  │       │  │ 文章 \u002F 用户  │  │\n│  │  首页 \u002F 文章 \u002F   │  │       │  └──────────────┘  │\n│  │  用户中心        │  │       │                    │\n│  └─────────────────┘  │       │  ┌──────────────┐  │\n│                       │       │  │   数据库      │  │\n└───────────────────────┘       │  └──────────────┘  │\n                                └────────────────────┘\n",[33,9716,9714],{"__ignoreMap":58},[1503,9718,9720],{"id":9719},"️-前端sublog-react","🖥️ 前端：sublog-react",[15,9722,9723,9724,9726,9727,9730],{},"选用 ",[38,9725,2607],{},"（App Router） + ",[38,9728,9729],{},"React 19","，理由是：",[2810,9732,9733,9740,9747,9754],{},[516,9734,9735,9736,9739],{},"🎨 ",[38,9737,9738],{},"React 生态","——组件库成熟，开发效率高",[516,9741,9742,9743,9746],{},"📐 ",[38,9744,9745],{},"App Router","——支持布局嵌套，页面组织清晰",[516,9748,9749,9750,9753],{},"📦 ",[38,9751,9752],{},"静态导出","——仍可部署到静态托管",[516,9755,9756,9757,9760],{},"⚙️ ",[38,9758,9759],{},"运行时配置","——API 地址可动态注入，切换环境无需重新构建",[15,9762,9763],{},"前端不再直接读取 Markdown 文件，而是通过 HTTP API 获取文章数据。静态导出 + 客户端数据获取的组合，既保留了首屏加载速度，又实现了内容的动态更新。",[1503,9765,9767],{"id":9766},"️-后端sublog-s","⚙️ 后端：sublog-s",[15,9769,9770,9771,2441,9773,9775],{},"后端使用 ",[38,9772,2064],{},[38,9774,6365],{},"，按业务逻辑分层组织代码。",[15,9777,9778],{},"核心技术选型：",[865,9780,9781,9789],{},[868,9782,9783],{},[871,9784,9785,9787],{},[874,9786,2737],{},[874,9788,2740],{},[884,9790,9791,9801,9812],{},[871,9792,9793,9798],{},[889,9794,9795,9796],{},"🚀 ",[38,9797,6365],{},[889,9799,9800],{},"HTTP 框架",[871,9802,9803,9809],{},[889,9804,9805,9806],{},"🗄️ ",[38,9807,9808],{},"GORM",[889,9810,9811],{},"ORM，数据库操作",[871,9813,9814,9820],{},[889,9815,9816,9817],{},"🔐 ",[38,9818,9819],{},"JWT",[889,9821,9822],{},"用户认证",[26,9824,9826],{"id":9825},"前后对比","📊 前后对比",[865,9828,9829,9842],{},[868,9830,9831],{},[871,9832,9833,9836,9839],{},[874,9834,9835],{},"维度",[874,9837,9838],{},"第一代（纯静态）",[874,9840,9841],{},"第二代（前后端分离）",[884,9843,9844,9856,9868,9882,9895,9908,9922,9934],{},[871,9845,9846,9850,9853],{},[889,9847,9735,9848],{},[38,9849,2702],{},[889,9851,9852],{},"Nuxt 3 (Vue)",[889,9854,9855],{},"Next.js (React 19)",[871,9857,9858,9863,9865],{},[889,9859,9860,9861],{},"🖥️ ",[38,9862,6367],{},[889,9864,2645],{},[889,9866,9867],{},"Go + Gin",[871,9869,9870,9876,9879],{},[889,9871,9872,9873],{},"📄 ",[38,9874,9875],{},"内容来源",[889,9877,9878],{},"Markdown 文件",[889,9880,9881],{},"API 数据库",[871,9883,9884,9889,9892],{},[889,9885,9795,9886],{},[38,9887,9888],{},"发布方式",[889,9890,9891],{},"全量重新构建",[889,9893,9894],{},"API 实时发布",[871,9896,9897,9902,9905],{},[889,9898,9664,9899],{},[38,9900,9901],{},"用户系统",[889,9903,9904],{},"❌ 无",[889,9906,9907],{},"✅ 注册\u002F登录",[871,9909,9910,9916,9919],{},[889,9911,9912,9913],{},"💬 ",[38,9914,9915],{},"评论",[889,9917,9918],{},"Giscus（需 GitHub）",[889,9920,9921],{},"自有评论系统（规划中）",[871,9923,9924,9928,9931],{},[889,9925,9749,9926],{},[38,9927,3011],{},[889,9929,9930],{},"1 个静态目录",[889,9932,9933],{},"前端静态 + 后端二进制",[871,9935,9936,9942,9945],{},[889,9937,9938,9939],{},"⚡ ",[38,9940,9941],{},"更新体验",[889,9943,9944],{},"改文章 → 构建 → 部署",[889,9946,9947],{},"改文章 → 立即生效",[26,9949,9951],{"id":9950},"新功能注册账号","🎉 新功能：注册账号",[15,9953,9954,9955,9958],{},"伴随着架构升级，",[38,9956,9957],{},"用户账号系统","也上线了。",[1503,9960,9962],{"id":9961},"目前支持","📋 目前支持",[15,9964,9965,9966,4115],{},"现在你可以在尼克的小窝 ",[38,9967,9968],{},"注册一个账号",[53,9970,9973],{"className":9971,"code":9972,"language":2580},[2578],"1. ✏️ 填写用户名、邮箱、密码\n2. 📧 查收激活邮件\n3. 🔗 点击链接激活账号\n4. 🚪 登录后查看和管理个人信息\n",[33,9974,9972],{"__ignoreMap":58},[26,9976,9978],{"id":9977},"️-后续计划","🗓️ 后续计划",[15,9980,9981],{},"正在开发或规划中的功能：",[2810,9983,9985,9991,9997,10003,10009,10015,10021,10027],{"className":9984},[3044],[516,9986,9988,9990],{"className":9987},[3048],[3050,9989],{"checked":752,"disabled":752,"type":3052}," 用户注册\u002F登录（已上线 🎉）",[516,9992,9994,9996],{"className":9993},[3048],[3050,9995],{"checked":752,"disabled":752,"type":3052}," 个人资料编辑（已上线 🎉）",[516,9998,10000,10002],{"className":9999},[3048],[3050,10001],{"disabled":752,"type":3052}," 💬 站内评论系统",[516,10004,10006,10008],{"className":10005},[3048],[3050,10007],{"disabled":752,"type":3052}," 🔖 文章收藏",[516,10010,10012,10014],{"className":10011},[3048],[3050,10013],{"disabled":752,"type":3052}," 📚 阅读历史",[516,10016,10018,10020],{"className":10017},[3048],[3050,10019],{"disabled":752,"type":3052}," 👍 文章点赞",[516,10022,10024,10026],{"className":10023},[3048],[3050,10025],{"disabled":752,"type":3052}," 🔔 关注通知",[516,10028,10030,10032],{"className":10029},[3048],[3050,10031],{"disabled":752,"type":3052}," 🎨 个人首页定制",[26,10034,10036],{"id":10035},"结语","💡 结语",[15,10038,10039,10040,10043],{},"从 Nuxt 3 纯静态到 React + Go 前后端分离，SuBlog 的架构变迁本质上是一个",[38,10041,10042],{},"功能驱动","的过程。静态博客有它的位置——写写文章、展示内容完全够用。但当你想做的不仅仅是\"展示\"时，后端的能力就不可或缺了。",[15,10045,10046,10047,10050],{},"这次重构最大的感触是：",[38,10048,10049],{},"架构选型不是越复杂越好，而是刚好够用最好","。纯静态阶段用 Nuxt 3 一点问题没有，直到功能需求超出了静态的边界，才自然推动到前后端分离。",[15,10052,10053,10054],{},"如果你也在纠结\"要不要上后端\"，不妨先问问自己：",[38,10055,10056],{},"你要做的是\"展示内容\"还是\"提供服务\"？",[15,10058,10059],{},"前者，静态就够了；后者，来吧，后端欢迎你。😄",[3087,10061],{},[15,10063,10064],{},[38,10065,10066],{},"🎉 目前注册功能已经可以用了，后续更多好玩的功能正在路上。部分功能可能需要登录才能体验，快注册一个账号吧！",{"title":58,"searchDepth":72,"depth":83,"links":10068},[10069,10070,10076,10081,10082,10085,10086],{"id":9547,"depth":72,"text":9548},{"id":9565,"depth":72,"text":9566,"children":10071},[10072,10073,10074,10075],{"id":9569,"depth":83,"text":9570},{"id":9579,"depth":83,"text":9580},{"id":879,"depth":83,"text":9614},{"id":9631,"depth":83,"text":9632},{"id":9706,"depth":72,"text":9707,"children":10077},[10078,10079,10080],{"id":9710,"depth":83,"text":9570},{"id":9719,"depth":83,"text":9720},{"id":9766,"depth":83,"text":9767},{"id":9825,"depth":72,"text":9826},{"id":9950,"depth":72,"text":9951,"children":10083},[10084],{"id":9961,"depth":83,"text":9962},{"id":9977,"depth":72,"text":9978},{"id":10035,"depth":72,"text":10036},"记录尼克的小窝从 Nuxt 3 纯静态博客演进到 React + Go 前后端分离架构的全过程，包括架构设计的思考、技术选型的变化，以及新增的账号注册功能。","\u002Fposts\u002Fsublog-evolution\u002Fimg\u002Fcover.jpg",{},"\u002Fposts\u002Fsublog-evolution","2026-07-23T12:00:00",{"title":9542,"description":10087},"尼克的小窝 开发记录","posts\u002Fsublog-evolution\u002Findex",[10096,10097,2607,2064],"架构","React","2026-07-23","VMQ6A2K8GQ5t3DSlezWPd3KgXnmDVMn9QVeaI28Hcp8",{"id":10101,"title":10102,"author":6,"body":10103,"description":11266,"difficulty":2475,"draft":1243,"extension":1244,"image":11267,"meta":11268,"navigation":752,"path":11269,"pinned":1243,"published":11270,"seo":11271,"series":6,"seriesOrder":6,"stem":11272,"tags":11273,"updated":11277,"__hash__":11278},"posts\u002Fposts\u002Fip-geolocation-guide\u002Findex.md","IP 属地是怎么查出来的？本地查询与在线查询方案对比",{"type":8,"value":10104,"toc":11242},[10105,10107,10113,10116,10127,10137,10141,10147,10160,10166,10177,10184,10187,10194,10198,10209,10213,10224,10227,10231,10237,10243,10246,10250,10257,10275,10278,10281,10285,10316,10355,10361,10365,10504,10540,10549,10553,10593,10598,10602,10613,10616,10650,10653,10657,10660,10664,10855,10859,10942,10946,10953,11060,11064,11073,11159,11162,11165,11201,11204,11207,11215,11236,11239],[26,10106,9547],{"id":9547},[15,10108,10109,10110],{},"很多网站会在「登录记录」「安全中心」里显示你的登录 IP 归属地，比如「福建·福州 · 中国移动」。你可能好奇过：",[38,10111,10112],{},"服务器是怎么根据一个 IP 就判断出我在哪个城市、哪个运营商的？",[15,10114,10115],{},"这篇科普文章讲清楚三件事：",[513,10117,10118,10121,10124],{},[516,10119,10120],{},"IP 属地到底查的是什么；",[516,10122,10123],{},"纯本地查询为什么能做到毫秒级且不联网；",[516,10125,10126],{},"几种主流查询方案的优缺点对比，方便你按场景选型。",[10128,10129,10130],"blockquote",{},[15,10131,10132,10133,10136],{},"说明：文中用到的 IP 均为 RFC 5737 保留的文档示例段（TEST-NET-2，",[33,10134,10135],{},"198.51.100.0\u002F24","），不会对应任何真实地址。",[26,10138,10140],{"id":10139},"先想清楚ip-属地查的是什么","先想清楚：IP 属地查的是什么",[15,10142,10143,10144,41],{},"一个常见误区是「每个 IP 对应一个具体地址」。实际上 IP 属地的本质是 ",[38,10145,10146],{},"IP 段与归属地的映射",[15,10148,10149,10150,4115],{},"运营商和机构在申请 IP 时，是按",[38,10151,10152,10153,10159],{},"网段（CIDR）",[38,10154,10155,10156,10158],{},"成块分配的。比如 ",[33,10157,10135],{}," 这一个段，一整块都归属同一地区、同一运营商。所以归属地数据天然可以","按段存储",[53,10161,10164],{"className":10162,"code":10163,"language":2580,"meta":58},[2578],"IP 起始          IP 结束         归属地\n────────────────────────────────────────\n198.51.100.0     198.51.100.255  中国|福建|福州|移动\n198.51.101.0     198.51.101.255  中国|浙江|杭州|电信\n",[33,10165,10163],{"__ignoreMap":58},[15,10167,10168,10169,10172,10173,10176],{},"全球公网 IP 约有 42 亿个，但",[38,10170,10171],{},"网段只有几十万条","。正因为存的是「段」而不是「每个 IP」，完整的归属地数据文件才只有几 MB（ip2region 的 ",[33,10174,10175],{},".xdb"," 约 11MB）。这是「本地离线查询」可行的第一前提。",[15,10178,10179,10180,10183],{},"所以，",[38,10181,10182],{},"IP 属地从来不是精确到街道的定位","，它返回的是段级的「省｜市｜运营商」粗粒度信息。",[26,10185,10186],{"id":10186},"纯本地查询的原理",[15,10188,10189,10190,10193],{},"以开源的 ",[38,10191,10192],{},"ip2region"," 为例，它的查询完全在本地完成，不联网、不请求第三方，几十微秒到毫秒级返回。",[1503,10195,10197],{"id":10196},"第一步把-ip-变成整数","第一步：把 IP 变成整数",[15,10199,10200,10201,10204,10205,10208],{},"以 ",[33,10202,10203],{},"198.51.100.42"," 为例，它会转成一个 32 位整数 ",[33,10206,10207],{},"0xC633642A","。IP 的四个数字本质就是 4 个字节，拼起来就是一个整数，这一步没有任何玄学。",[1503,10210,10212],{"id":10211},"第二步二分查找","第二步：二分查找",[15,10214,10215,10216,10219,10220,10223],{},"在按起始 IP 排序的段列表里做",[38,10217,10218],{},"二分查找","，找到满足 ",[33,10221,10222],{},"段的起始IP ≤ 目标整数 ≤ 段的结束IP"," 的那一段。",[15,10225,10226],{},"二分查找的复杂度是 O(log N)，N 是几十万条，所以很快——这就是「不靠网络也能毫秒返回」的核心。",[1503,10228,10230],{"id":10229},"第三步xdb-文件的两级结构","第三步：.xdb 文件的两级结构",[15,10232,10233,10234,10236],{},"ip2region 的 ",[33,10235,10175],{}," 不是简单顺序文件，内部分两个区，进一步加速：",[53,10238,10241],{"className":10239,"code":10240,"language":2580,"meta":58},[2578],"┌─────────────────────────────┐\n│  索引区 (Index)             │  \u002F\u002F 每段一条定长记录：startIP \u002F endIP \u002F 数据区指针 + 数据长度\n│  …… 二分快速定位段 ……       │\n├─────────────────────────────┤\n│  数据区 (Data)              │  \u002F\u002F 真正存放各段的地点字符串\n│  ……                         │\n└─────────────────────────────┘\n",[33,10242,10240],{"__ignoreMap":58},[15,10244,10245],{},"先二分索引区定位到「是哪一段」，再按指针去数据区读出地点字符串，一次磁盘读就能拿到结果。",[1503,10247,10249],{"id":10248},"为什么离线数据不会很快过期","为什么「离线数据」不会很快过期",[15,10251,10252,10253,10256],{},"IP 归属取决于",[38,10254,10255],{},"注册机构和运营商的分配","，一个网段通常长期稳定属于某地区、某运营商。所以：",[2810,10258,10259,10262,10272],{},[516,10260,10261],{},"下载一次数据文件，可以用很久；",[516,10263,10264,10265,10271],{},"归属偶尔变化时，",[38,10266,10267,10268,10270],{},"每月更新一次 ",[33,10269,10175],{}," 文件","即可（替换文件、不用重启服务，见下文实战）；",[516,10273,10274],{},"数据源来自 IANA\u002FAPNIC 等机构的分配表与公开归属信息，不是实时探测。",[26,10276,10277],{"id":10277},"主流查询方案对比",[15,10279,10280],{},"实际开发中，查询 IP 属地主要有三条路线。",[1503,10282,10284],{"id":10283},"方案一ip2region纯本地离线推荐","方案一：ip2region（纯本地离线，推荐）",[53,10286,10288],{"className":4396,"code":10287,"language":1398,"meta":58,"style":58},"\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\nregion, err := searcher.SearchByStr(ip)\n\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[33,10289,10290,10295,10311],{"__ignoreMap":58},[62,10291,10292],{"class":64,"line":65},[62,10293,10294],{"class":68},"\u002F\u002F Go 中的用法（github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang）\n",[62,10296,10297,10300,10302,10305,10308],{"class":64,"line":72},[62,10298,10299],{"class":79},"region, err ",[62,10301,4423],{"class":166},[62,10303,10304],{"class":79}," searcher.",[62,10306,10307],{"class":75},"SearchByStr",[62,10309,10310],{"class":79},"(ip)\n",[62,10312,10313],{"class":64,"line":83},[62,10314,10315],{"class":68},"\u002F\u002F 返回类似 \"中国|福建省|福州市|移动\"\n",[865,10317,10318,10326],{},[868,10319,10320],{},[871,10321,10322,10324],{},[874,10323,879],{},[874,10325,882],{},[884,10327,10328,10336,10344],{},[871,10329,10330,10333],{},[889,10331,10332],{},"免费、无网络依赖、无 QPS 限制",[889,10334,10335],{},"段级粒度，个别段可能显示到相邻地区",[871,10337,10338,10341],{},[889,10339,10340],{},"毫秒级查询，永不因外部服务挂掉",[889,10342,10343],{},"数据按月更新，最新分配的段可能短暂缺失",[871,10345,10346,10349],{},[889,10347,10348],{},"数据文件可随版本更新，不重启",[889,10350,10351,10352],{},"只返回省市区+运营商，无经纬度；",[38,10353,10354],{},"仅支持 IPv4",[15,10356,10357,10360],{},[38,10358,10359],{},"适用","：博客\u002F后台的「登录记录显示归属地」这类展示用途。",[1503,10362,10364],{"id":10363},"方案二在线-apiip-api-高德-腾讯位置服务","方案二：在线 API（ip-api \u002F 高德 \u002F 腾讯位置服务）",[53,10366,10368],{"className":4396,"code":10367,"language":1398,"meta":58,"style":58},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\nctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\ndefer cancel()\nreq, _ := http.NewRequestWithContext(ctx, \"GET\", \"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\", nil)\nresp, err := http.DefaultClient.Do(req)\nif err != nil {\n    return \"未知\" \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n}\ndefer resp.Body.Close()\n\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[33,10369,10370,10375,10403,10413,10444,10460,10473,10483,10487,10499],{"__ignoreMap":58},[62,10371,10372],{"class":64,"line":65},[62,10373,10374],{"class":68},"\u002F\u002F 以 ip-api.com 为例（免费版约 45 次\u002F分钟，非商用）\n",[62,10376,10377,10380,10382,10384,10387,10390,10393,10396,10398,10400],{"class":64,"line":72},[62,10378,10379],{"class":79},"ctx, cancel ",[62,10381,4423],{"class":166},[62,10383,5896],{"class":79},[62,10385,10386],{"class":75},"WithTimeout",[62,10388,10389],{"class":79},"(context.",[62,10391,10392],{"class":75},"Background",[62,10394,10395],{"class":79},"(), ",[62,10397,115],{"class":114},[62,10399,4629],{"class":166},[62,10401,10402],{"class":79},"time.Second)\n",[62,10404,10405,10408,10411],{"class":64,"line":83},[62,10406,10407],{"class":166},"defer",[62,10409,10410],{"class":75}," cancel",[62,10412,374],{"class":79},[62,10414,10415,10418,10420,10423,10426,10429,10432,10434,10437,10439,10442],{"class":64,"line":91},[62,10416,10417],{"class":79},"req, _ ",[62,10419,4423],{"class":166},[62,10421,10422],{"class":79}," http.",[62,10424,10425],{"class":75},"NewRequestWithContext",[62,10427,10428],{"class":79},"(ctx, ",[62,10430,10431],{"class":273},"\"GET\"",[62,10433,186],{"class":79},[62,10435,10436],{"class":273},"\"https:\u002F\u002Fip-api.com\u002Fjson\u002F198.51.100.42\"",[62,10438,186],{"class":79},[62,10440,10441],{"class":114},"nil",[62,10443,494],{"class":79},[62,10445,10446,10449,10451,10454,10457],{"class":64,"line":99},[62,10447,10448],{"class":79},"resp, err ",[62,10450,4423],{"class":166},[62,10452,10453],{"class":79}," http.DefaultClient.",[62,10455,10456],{"class":75},"Do",[62,10458,10459],{"class":79},"(req)\n",[62,10461,10462,10465,10467,10469,10471],{"class":64,"line":124},[62,10463,10464],{"class":166},"if",[62,10466,4837],{"class":79},[62,10468,4840],{"class":166},[62,10470,4843],{"class":114},[62,10472,620],{"class":79},[62,10474,10475,10477,10480],{"class":64,"line":130},[62,10476,4654],{"class":166},[62,10478,10479],{"class":273}," \"未知\"",[62,10481,10482],{"class":68}," \u002F\u002F 断网\u002F超时降级：展示「未知」而不是报错\n",[62,10484,10485],{"class":64,"line":136},[62,10486,139],{"class":79},[62,10488,10489,10491,10494,10497],{"class":64,"line":402},[62,10490,10407],{"class":166},[62,10492,10493],{"class":79}," resp.Body.",[62,10495,10496],{"class":75},"Close",[62,10498,374],{"class":79},[62,10500,10501],{"class":64,"line":407},[62,10502,10503],{"class":68},"\u002F\u002F 解析 JSON，status != \"success\" 时同样降级\n",[865,10505,10506,10514],{},[868,10507,10508],{},[871,10509,10510,10512],{},[874,10511,879],{},[874,10513,882],{},[884,10515,10516,10524,10532],{},[871,10517,10518,10521],{},[889,10519,10520],{},"数据最新、可能更精确",[889,10522,10523],{},"有频率限制（免费版限 QPS）",[871,10525,10526,10529],{},[889,10527,10528],{},"部分服务还返回经纬度\u002F时区",[889,10530,10531],{},"依赖第三方，断网\u002F被墙\u002F限流时拿不到",[871,10533,10534,10537],{},[889,10535,10536],{},"无需维护数据文件",[889,10538,10539],{},"每次查询都要发一次 HTTP 请求",[15,10541,10542,10544,10545,10548],{},[38,10543,10359],{},"：对精度要求高、且能接受外部依赖与限流的场景。",[38,10546,10547],{},"务必加超时和降级","，否则第三方一抖，你自己的页面就卡住。",[1503,10550,10552],{"id":10551},"方案三maxmind-geolite2离线-mmdb","方案三：MaxMind GeoLite2（离线 .mmdb）",[865,10554,10555,10563],{},[868,10556,10557],{},[871,10558,10559,10561],{},[874,10560,879],{},[874,10562,882],{},[884,10564,10565,10573,10585],{},[871,10566,10567,10570],{},[889,10568,10569],{},"全球数据，覆盖广、精度较好",[889,10571,10572],{},"免费但需注册账号并遵守 EULA；商用需 GeoIP2 商业授权",[871,10574,10575,10582],{},[889,10576,10577,10578,10581],{},"标准 ",[33,10579,10580],{},".mmdb"," 格式，各语言都有库，支持 IPv6",[889,10583,10584],{},"国内 IP 的省市粒度有时不如 ip2region 细",[871,10586,10587,10590],{},[889,10588,10589],{},"离线查询",[889,10591,10592],{},"数据同样要定期更新",[15,10594,10595,10597],{},[38,10596,10359],{},"：面向全球用户的场景。",[26,10599,10601],{"id":10600},"ipv6本地离线方案的一个盲点","IPv6：本地离线方案的一个盲点",[15,10603,10604,10605,10608,10609,10612],{},"前面聊的都是 IPv4（32 位）。但家宽和移动网络越来越多地",[38,10606,10607],{},"只下发 IPv6","（128 位）地址，而 ",[38,10610,10611],{},"ip2region 目前只支持 IPv4","——拿到一个纯 IPv6 地址是查不出归属地的，本地库会返回空结果。",[15,10614,10615],{},"实际影响与常见应对：",[513,10617,10618,10628,10634,10644],{},[516,10619,10620,10623,10624,10627],{},[38,10621,10622],{},"判断版本","：Go 里 ",[33,10625,10626],{},"net.ParseIP(ip).To4() != nil"," 就是 IPv4，先判断再查询；",[516,10629,10630,10633],{},[38,10631,10632],{},"降级展示","：IPv6 直接显示「IPv6 地址」，不显示属地，诚实第一；",[516,10635,10636,10639,10640,10643],{},[38,10637,10638],{},"优先取 IPv4","：有反向代理时，从 ",[33,10641,10642],{},"X-Forwarded-For"," 里挑一个 IPv4 来查；",[516,10645,10646,10649],{},[38,10647,10648],{},"需要 v6 归属","：换支持 IPv6 的数据源（如 MaxMind GeoLite2）。",[15,10651,10652],{},"这一点在接入时很容易漏掉，上线前记得拿一个纯 IPv6 地址回归一遍。",[26,10654,10656],{"id":10655},"实战给登录记录加-ip-属地","实战：给登录记录加 IP 属地",[15,10658,10659],{},"subLog 博客的登录记录就用了 ip2region。完整接入一共四步。",[1503,10661,10663],{"id":10662},"_1-进程启动时加载数据文件","1. 进程启动时加载数据文件",[53,10665,10667],{"className":4396,"code":10666,"language":1398,"meta":58,"style":58},"import (\n    \"sync\u002Fatomic\"\n    \"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb\"\n)\n\n\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\nvar searcher atomic.Pointer[xdb.Searcher]\n\nfunc initSearcher() error {\n    content, err := os.ReadFile(\".\u002Fdata\u002Fip2region.xdb\") \u002F\u002F 约 11MB，一次性读入内存\n    if err != nil {\n        return err\n    }\n    s, err := xdb.NewWithBuffer(content)\n    if err != nil {\n        return err\n    }\n    searcher.Store(s)\n    return nil\n}\n",[33,10668,10669,10675,10684,10693,10697,10701,10706,10733,10737,10750,10772,10784,10791,10795,10811,10823,10829,10833,10844,10851],{"__ignoreMap":58},[62,10670,10671,10673],{"class":64,"line":65},[62,10672,4404],{"class":166},[62,10674,5802],{"class":79},[62,10676,10677,10679,10682],{"class":64,"line":72},[62,10678,7222],{"class":273},[62,10680,10681],{"class":75},"sync\u002Fatomic",[62,10683,1501],{"class":273},[62,10685,10686,10688,10691],{"class":64,"line":83},[62,10687,7222],{"class":273},[62,10689,10690],{"class":75},"github.com\u002Flionsoul2014\u002Fip2region\u002Fbinding\u002Fgolang\u002Fxdb",[62,10692,1501],{"class":273},[62,10694,10695],{"class":64,"line":91},[62,10696,494],{"class":79},[62,10698,10699],{"class":64,"line":99},[62,10700,753],{"emptyLinePlaceholder":752},[62,10702,10703],{"class":64,"line":124},[62,10704,10705],{"class":68},"\u002F\u002F 用 atomic.Pointer 持有查询器，方便后面热更新\n",[62,10707,10708,10710,10713,10716,10718,10721,10723,10726,10728,10731],{"class":64,"line":130},[62,10709,7303],{"class":166},[62,10711,10712],{"class":79}," searcher ",[62,10714,10715],{"class":75},"atomic",[62,10717,299],{"class":79},[62,10719,10720],{"class":75},"Pointer",[62,10722,1010],{"class":79},[62,10724,10725],{"class":75},"xdb",[62,10727,299],{"class":79},[62,10729,10730],{"class":75},"Searcher",[62,10732,1720],{"class":79},[62,10734,10735],{"class":64,"line":136},[62,10736,753],{"emptyLinePlaceholder":752},[62,10738,10739,10741,10744,10746,10748],{"class":64,"line":402},[62,10740,4597],{"class":166},[62,10742,10743],{"class":75}," initSearcher",[62,10745,5845],{"class":79},[62,10747,4642],{"class":166},[62,10749,620],{"class":79},[62,10751,10752,10755,10757,10760,10762,10764,10767,10769],{"class":64,"line":407},[62,10753,10754],{"class":79},"    content, err ",[62,10756,4423],{"class":166},[62,10758,10759],{"class":79}," os.",[62,10761,7442],{"class":75},[62,10763,173],{"class":79},[62,10765,10766],{"class":273},"\".\u002Fdata\u002Fip2region.xdb\"",[62,10768,654],{"class":79},[62,10770,10771],{"class":68},"\u002F\u002F 约 11MB，一次性读入内存\n",[62,10773,10774,10776,10778,10780,10782],{"class":64,"line":433},[62,10775,253],{"class":166},[62,10777,4837],{"class":79},[62,10779,4840],{"class":166},[62,10781,4843],{"class":114},[62,10783,620],{"class":79},[62,10785,10786,10788],{"class":64,"line":438},[62,10787,4850],{"class":166},[62,10789,10790],{"class":79}," err\n",[62,10792,10793],{"class":64,"line":446},[62,10794,127],{"class":79},[62,10796,10797,10800,10802,10805,10808],{"class":64,"line":1448},[62,10798,10799],{"class":79},"    s, err ",[62,10801,4423],{"class":166},[62,10803,10804],{"class":79}," xdb.",[62,10806,10807],{"class":75},"NewWithBuffer",[62,10809,10810],{"class":79},"(content)\n",[62,10812,10813,10815,10817,10819,10821],{"class":64,"line":1459},[62,10814,253],{"class":166},[62,10816,4837],{"class":79},[62,10818,4840],{"class":166},[62,10820,4843],{"class":114},[62,10822,620],{"class":79},[62,10824,10825,10827],{"class":64,"line":1470},[62,10826,4850],{"class":166},[62,10828,10790],{"class":79},[62,10830,10831],{"class":64,"line":1475},[62,10832,127],{"class":79},[62,10834,10835,10838,10841],{"class":64,"line":1484},[62,10836,10837],{"class":79},"    searcher.",[62,10839,10840],{"class":75},"Store",[62,10842,10843],{"class":79},"(s)\n",[62,10845,10846,10848],{"class":64,"line":1489},[62,10847,4654],{"class":166},[62,10849,10850],{"class":114}," nil\n",[62,10852,10853],{"class":64,"line":1695},[62,10854,139],{"class":79},[1503,10856,10858],{"id":10857},"_2-查询一次","2. 查询一次",[53,10860,10862],{"className":4396,"code":10861,"language":1398,"meta":58,"style":58},"func ipRegion(ip string) string {\n    region, err := searcher.Load().SearchByStr(ip)\n    if err != nil {\n        return \"未知\"\n    }\n    return region \u002F\u002F \"中国|福建省|福州市|移动\"\n}\n",[33,10863,10864,10886,10905,10917,10924,10928,10938],{"__ignoreMap":58},[62,10865,10866,10868,10871,10873,10876,10879,10881,10884],{"class":64,"line":65},[62,10867,4597],{"class":166},[62,10869,10870],{"class":75}," ipRegion",[62,10872,173],{"class":79},[62,10874,10875],{"class":176},"ip",[62,10877,10878],{"class":166}," string",[62,10880,654],{"class":79},[62,10882,10883],{"class":166},"string",[62,10885,620],{"class":79},[62,10887,10888,10891,10893,10895,10898,10901,10903],{"class":64,"line":72},[62,10889,10890],{"class":79},"    region, err ",[62,10892,4423],{"class":166},[62,10894,10304],{"class":79},[62,10896,10897],{"class":75},"Load",[62,10899,10900],{"class":79},"().",[62,10902,10307],{"class":75},[62,10904,10310],{"class":79},[62,10906,10907,10909,10911,10913,10915],{"class":64,"line":83},[62,10908,253],{"class":166},[62,10910,4837],{"class":79},[62,10912,4840],{"class":166},[62,10914,4843],{"class":114},[62,10916,620],{"class":79},[62,10918,10919,10921],{"class":64,"line":91},[62,10920,4850],{"class":166},[62,10922,10923],{"class":273}," \"未知\"\n",[62,10925,10926],{"class":64,"line":99},[62,10927,127],{"class":79},[62,10929,10930,10932,10935],{"class":64,"line":124},[62,10931,4654],{"class":166},[62,10933,10934],{"class":79}," region ",[62,10936,10937],{"class":68},"\u002F\u002F \"中国|福建省|福州市|移动\"\n",[62,10939,10940],{"class":64,"line":130},[62,10941,139],{"class":79},[1503,10943,10945],{"id":10944},"_3-加一层内存缓存","3. 加一层内存缓存",[15,10947,10948,10949,10952],{},"登录记录里同一个 IP 可能反复出现，用 ",[33,10950,10951],{},"sync.Map"," 按 IP 缓存结果，一次查询终身复用：",[53,10954,10956],{"className":4396,"code":10955,"language":1398,"meta":58,"style":58},"var regionCache sync.Map\n\nfunc ipRegionCached(ip string) string {\n    if v, ok := regionCache.Load(ip); ok {\n        return v.(string)\n    }\n    region := ipRegion(ip)\n    regionCache.Store(ip, region)\n    return region\n}\n",[33,10957,10958,10973,10977,10996,11013,11024,11028,11039,11049,11056],{"__ignoreMap":58},[62,10959,10960,10962,10965,10968,10970],{"class":64,"line":65},[62,10961,7303],{"class":166},[62,10963,10964],{"class":79}," regionCache ",[62,10966,10967],{"class":75},"sync",[62,10969,299],{"class":79},[62,10971,10972],{"class":75},"Map\n",[62,10974,10975],{"class":64,"line":72},[62,10976,753],{"emptyLinePlaceholder":752},[62,10978,10979,10981,10984,10986,10988,10990,10992,10994],{"class":64,"line":83},[62,10980,4597],{"class":166},[62,10982,10983],{"class":75}," ipRegionCached",[62,10985,173],{"class":79},[62,10987,10875],{"class":176},[62,10989,10878],{"class":166},[62,10991,654],{"class":79},[62,10993,10883],{"class":166},[62,10995,620],{"class":79},[62,10997,10998,11000,11003,11005,11008,11010],{"class":64,"line":91},[62,10999,253],{"class":166},[62,11001,11002],{"class":79}," v, ok ",[62,11004,4423],{"class":166},[62,11006,11007],{"class":79}," regionCache.",[62,11009,10897],{"class":75},[62,11011,11012],{"class":79},"(ip); ok {\n",[62,11014,11015,11017,11020,11022],{"class":64,"line":99},[62,11016,4850],{"class":166},[62,11018,11019],{"class":79}," v.(",[62,11021,10883],{"class":166},[62,11023,494],{"class":79},[62,11025,11026],{"class":64,"line":124},[62,11027,127],{"class":79},[62,11029,11030,11033,11035,11037],{"class":64,"line":130},[62,11031,11032],{"class":79},"    region ",[62,11034,4423],{"class":166},[62,11036,10870],{"class":75},[62,11038,10310],{"class":79},[62,11040,11041,11044,11046],{"class":64,"line":136},[62,11042,11043],{"class":79},"    regionCache.",[62,11045,10840],{"class":75},[62,11047,11048],{"class":79},"(ip, region)\n",[62,11050,11051,11053],{"class":64,"line":402},[62,11052,4654],{"class":166},[62,11054,11055],{"class":79}," region\n",[62,11057,11058],{"class":64,"line":407},[62,11059,139],{"class":79},[1503,11061,11063],{"id":11062},"_4-数据热更新不用重启","4. 数据热更新（不用重启）",[15,11065,11066,11067,11069,11070,11072],{},"ip2region 每月发布新数据。下载新文件后重新 ",[33,11068,10807],{}," 并 ",[33,11071,10840],{}," 覆盖即可，进程完全不用重启：",[53,11074,11076],{"className":4396,"code":11075,"language":1398,"meta":58,"style":58},"func reloadSearcher(newContent []byte) error {\n    s, err := xdb.NewWithBuffer(newContent)\n    if err != nil {\n        return err\n    }\n    searcher.Store(s) \u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n    return nil\n}\n",[33,11077,11078,11102,11115,11127,11133,11137,11149,11155],{"__ignoreMap":58},[62,11079,11080,11082,11085,11087,11090,11093,11096,11098,11100],{"class":64,"line":65},[62,11081,4597],{"class":166},[62,11083,11084],{"class":75}," reloadSearcher",[62,11086,173],{"class":79},[62,11088,11089],{"class":176},"newContent",[62,11091,11092],{"class":79}," []",[62,11094,11095],{"class":166},"byte",[62,11097,654],{"class":79},[62,11099,4642],{"class":166},[62,11101,620],{"class":79},[62,11103,11104,11106,11108,11110,11112],{"class":64,"line":72},[62,11105,10799],{"class":79},[62,11107,4423],{"class":166},[62,11109,10804],{"class":79},[62,11111,10807],{"class":75},[62,11113,11114],{"class":79},"(newContent)\n",[62,11116,11117,11119,11121,11123,11125],{"class":64,"line":83},[62,11118,253],{"class":166},[62,11120,4837],{"class":79},[62,11122,4840],{"class":166},[62,11124,4843],{"class":114},[62,11126,620],{"class":79},[62,11128,11129,11131],{"class":64,"line":91},[62,11130,4850],{"class":166},[62,11132,10790],{"class":79},[62,11134,11135],{"class":64,"line":99},[62,11136,127],{"class":79},[62,11138,11139,11141,11143,11146],{"class":64,"line":124},[62,11140,10837],{"class":79},[62,11142,10840],{"class":75},[62,11144,11145],{"class":79},"(s) ",[62,11147,11148],{"class":68},"\u002F\u002F 原子切换，老查询继续走旧数据，新查询走新数据\n",[62,11150,11151,11153],{"class":64,"line":130},[62,11152,4654],{"class":166},[62,11154,10850],{"class":114},[62,11156,11157],{"class":64,"line":136},[62,11158,139],{"class":79},[26,11160,11161],{"id":11161},"局限与诚实提醒",[15,11163,11164],{},"无论用哪种方案，都要清楚 IP 属地的边界：",[2810,11166,11167,11173,11179,11185,11191],{},[516,11168,11169,11172],{},[38,11170,11171],{},"段级精度","：网段归属是「绝大多数」准确，个别 IP（大型企业自建段、跨省调度段）可能显示到相邻地区或只到省。",[516,11174,11175,11178],{},[38,11176,11177],{},"运营商级，不是定位","：拿到的是「省｜市｜运营商」，不是精确街道或基站位置——精确位置需要 GPS 或运营商三角定位，纯 IP 归属做不到。",[516,11180,11181,11184],{},[38,11182,11183],{},"动态 IP","：家宽通常是动态 IP，重拨会变，所以「上次登录在福州，这次在广州」不一定是异常，可能是 IP 变了。",[516,11186,11187,11190],{},[38,11188,11189],{},"别用于安全判定","：用 IP 段做封禁或风控判定风险自负，段级数据不适合当精确依据。",[516,11192,11193,11196,11197,11200],{},[38,11194,11195],{},"隐私与合规","：IP 地址属于个人信息（《个人信息保护法》）。归属地展示建议只出现在",[38,11198,11199],{},"用户本人可见","的页面（登录记录、个人中心）；若在评论区等公开场合展示，建议只显示到省市并明确告知用户。",[26,11202,11203],{"id":11203},"小结",[15,11205,11206],{},"一句话总结：",[10128,11208,11209],{},[15,11210,11211,11214],{},[38,11212,11213],{},"IP 属地 = 几十万条「IP 段 → 归属地」映射 + 一次二分查找。"," 它查的是网段归属，不是精确位置。",[2810,11216,11217,11223,11230],{},[516,11218,11219,11220,11222],{},"想省心、免费、稳定，用 ",[38,11221,10192],{}," 本地离线查询（注意 IPv6 盲点）；",[516,11224,11225,11226,11229],{},"想要最新最细、能接受外部依赖，用",[38,11227,11228],{},"在线 API","（记得超时降级）；",[516,11231,11232,11233,41],{},"面向全球用户、要 IPv6 覆盖，用 ",[38,11234,11235],{},"MaxMind GeoLite2",[15,11237,11238],{},"对个人博客「登录记录显示归属地」这种场景，本地离线方案通常是性价比最高的选择。",[1228,11240,11241],{},"html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":58,"searchDepth":72,"depth":83,"links":11243},[11244,11245,11246,11252,11257,11258,11264,11265],{"id":9547,"depth":72,"text":9547},{"id":10139,"depth":72,"text":10140},{"id":10186,"depth":72,"text":10186,"children":11247},[11248,11249,11250,11251],{"id":10196,"depth":83,"text":10197},{"id":10211,"depth":83,"text":10212},{"id":10229,"depth":83,"text":10230},{"id":10248,"depth":83,"text":10249},{"id":10277,"depth":72,"text":10277,"children":11253},[11254,11255,11256],{"id":10283,"depth":83,"text":10284},{"id":10363,"depth":83,"text":10364},{"id":10551,"depth":83,"text":10552},{"id":10600,"depth":72,"text":10601},{"id":10655,"depth":72,"text":10656,"children":11259},[11260,11261,11262,11263],{"id":10662,"depth":83,"text":10663},{"id":10857,"depth":83,"text":10858},{"id":10944,"depth":83,"text":10945},{"id":11062,"depth":83,"text":11063},{"id":11161,"depth":72,"text":11161},{"id":11203,"depth":72,"text":11203},"科普 IP 属地查询的原理：IP 段与归属地的映射、纯本地离线查询为何能做到毫秒级，对比 ip2region、在线 API 与 MaxMind 三种方案的优劣，并附登录记录场景的 Go 实战与 IPv6 盲点提醒。","\u002Fposts\u002Fip-geolocation-guide\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fip-geolocation-guide","2026-08-07T12:00:00",{"title":10102,"description":11266},"posts\u002Fip-geolocation-guide\u002Findex",[11274,11275,11276],"IP","网络","科普","2026-08-16","RY1UPgJaiKsjpC51uqMyG7w_GpCRS13_CX5j1hDD_dM",{"id":11280,"title":11281,"author":6,"body":11282,"description":12804,"difficulty":1242,"draft":1243,"extension":1244,"image":12805,"meta":12806,"navigation":752,"path":12807,"pinned":1243,"published":12808,"seo":12809,"series":6361,"seriesOrder":83,"stem":12810,"tags":12811,"updated":11277,"__hash__":12814},"posts\u002Fposts\u002Fgo-embed-react-single-binary\u002Findex.md","Go 单文件打包进阶：React 前端 + 交叉编译，一次踩完所有坑",{"type":8,"value":11283,"toc":12783},[11284,11286,11297,11308,11315,11318,11354,11356,11365,11367,11371,11381,11425,11467,11485,11489,11510,11541,11545,11837,11840,11878,11882,12225,12228,12232,12429,12439,12441,12452,12466,12479,12490,12494,12501,12517,12530,12535,12539,12555,12560,12605,12619,12630,12634,12649,12658,12680,12689,12693,12703,12705,12732,12741,12743,12746,12773,12780],[26,11285,13],{"id":13},[15,11287,11288,11289,11292,11293,11296],{},"这是「Go 单文件打包」系列第三篇。上一篇（",[3079,11290,11291],{"href":8121},"把前端塞进二进制","）记录了一个 Gin + Vue 3 项目的 embed 方案。这次换了个项目继续踩坑：",[38,11294,11295],{},"Magic Img","——一个自托管、本地优先的 AI 生图提示词管理工具，技术栈是：",[2810,11298,11299,11302],{},[516,11300,11301],{},"前端：React 19 + TypeScript + Vite + Tailwind v4（UI 库 Magic UI + shadcn\u002Fui）",[516,11303,11304,11305,6905],{},"后端：Go + Gin + GORM + SQLite（",[38,11306,11307],{},"modernc 纯 Go 驱动，无 cgo",[15,11309,11310,11311,11314],{},"目标一样：",[38,11312,11313],{},"一个二进制拖到任何机器上就能跑","——前端、API、数据库全部内嵌。",[15,11316,11317],{},"和上篇不同的地方有三个，正好是这篇要讲的重点：",[513,11319,11320,11334,11347],{},[516,11321,11322,11323,11329,11330,11333],{},"前端用了 ",[38,11324,11325,11326],{},"HashRouter + Vite base ",[33,11327,11328],{},"\u002Fdist","，服务端",[38,11331,11332],{},"不再需要 SPA fallback","（上篇坑一的新解法）；",[516,11335,11336,11337,11343,11344,11346],{},"embed 用 ",[38,11338,11339,11342],{},[33,11340,11341],{},"all:"," 前缀","一劳永逸解决 ",[33,11345,7815],{}," 开头文件被排除的问题（上篇坑二的升级解法）；",[516,11348,11349,11350,11353],{},"需要",[38,11351,11352],{},"在 Windows 上交叉编译 Linux 产物","，踩了「生成的文件无法在 Linux 执行」的坑。",[26,11355,7171],{"id":7171},[15,11357,11358,11359,11361,11362,11364],{},"和上篇一致：",[33,11360,7177],{}," 在编译期把前端 ",[33,11363,7181],{}," 塞进二进制，运行时由 Go 直接提供静态服务。差异在于路由策略和缓存策略，后面细说。",[26,11366,7185],{"id":7185},[1503,11368,11370],{"id":11369},"_1-前端hash-路由-base-dist","1. 前端：hash 路由 + base \u002Fdist",[15,11372,11373,11374,11377,11378,4115],{},"vite.config.ts 里固定 ",[33,11375,11376],{},"base: \"\u002Fdist\u002F\"","，路由用 ",[33,11379,11380],{},"HashRouter",[53,11382,11384],{"className":7866,"code":11383,"language":7868,"meta":58,"style":58},"\u002F\u002F vite.config.ts\nexport default defineConfig({\n  base: \"\u002Fdist\u002F\",   \u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n  ...\n})\n",[33,11385,11386,11390,11402,11416,11421],{"__ignoreMap":58},[62,11387,11388],{"class":64,"line":65},[62,11389,7875],{"class":68},[62,11391,11392,11394,11396,11399],{"class":64,"line":72},[62,11393,940],{"class":166},[62,11395,943],{"class":166},[62,11397,11398],{"class":75}," defineConfig",[62,11400,11401],{"class":79},"({\n",[62,11403,11404,11407,11410,11413],{"class":64,"line":83},[62,11405,11406],{"class":79},"  base: ",[62,11408,11409],{"class":273},"\"\u002Fdist\u002F\"",[62,11411,11412],{"class":79},",   ",[62,11414,11415],{"class":68},"\u002F\u002F 所有资源路径以 \u002Fdist\u002F 开头\n",[62,11417,11418],{"class":64,"line":91},[62,11419,11420],{"class":166},"  ...\n",[62,11422,11423],{"class":64,"line":99},[62,11424,738],{"class":79},[53,11426,11430],{"className":11427,"code":11428,"language":11429,"meta":58,"style":58},"language-tsx shiki shiki-themes github-light github-dark","\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n\u003CHashRouter>\n  \u003CRoutes>...\u003C\u002FRoutes>\n\u003C\u002FHashRouter>\n","tsx",[33,11431,11432,11437,11445,11459],{"__ignoreMap":58},[62,11433,11434],{"class":64,"line":65},[62,11435,11436],{"class":68},"\u002F\u002F App.tsx —— HashRouter：路径信息全在 location.hash 里\n",[62,11438,11439,11441,11443],{"class":64,"line":72},[62,11440,575],{"class":79},[62,11442,11380],{"class":114},[62,11444,585],{"class":79},[62,11446,11447,11449,11452,11455,11457],{"class":64,"line":83},[62,11448,767],{"class":79},[62,11450,11451],{"class":114},"Routes",[62,11453,11454],{"class":79},">...\u003C\u002F",[62,11456,11451],{"class":114},[62,11458,585],{"class":79},[62,11460,11461,11463,11465],{"class":64,"line":91},[62,11462,743],{"class":79},[62,11464,11380],{"class":114},[62,11466,585],{"class":79},[15,11468,11469,11472,11473,11476,11477,11480,11481,11484],{},[38,11470,11471],{},"为什么这样选","：history 路由需要服务端把未知路径全部 fallback 回 ",[33,11474,11475],{},"index.html","（上篇坑一）。hash 路由下浏览器请求的永远是 ",[33,11478,11479],{},"\u002Fdist\u002F"," 这一个入口，",[38,11482,11483],{},"静态服务直接返回 index.html 就完事，一个 fallback 都不用写","。内嵌场景里这是最省心的组合。",[1503,11486,11488],{"id":11487},"_2-构建产物复制进后端目录","2. 构建产物复制进后端目录",[15,11490,11491,11494,11495,11497,11498,11501,11502,11505,11506,11509],{},[33,11492,11493],{},"npm run build"," 除了编译，还会把 ",[33,11496,7181],{}," 复制到 ",[33,11499,11500],{},"backend\u002Fweb\u002Fdist","（一个小脚本 ",[33,11503,11504],{},"scripts\u002Fcopy-dist.mjs"," 干这个活），供 ",[33,11507,11508],{},"go:embed"," 使用：",[53,11511,11513],{"className":6188,"code":11512,"language":6190,"meta":58,"style":58},"\u002F\u002F frontend\u002Fpackage.json\n\"scripts\": {\n  \"build\": \"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n}\n",[33,11514,11515,11520,11527,11537],{"__ignoreMap":58},[62,11516,11517],{"class":64,"line":65},[62,11518,11519],{"class":68},"\u002F\u002F frontend\u002Fpackage.json\n",[62,11521,11522,11525],{"class":64,"line":72},[62,11523,11524],{"class":273},"\"scripts\"",[62,11526,80],{"class":79},[62,11528,11529,11532,11534],{"class":64,"line":83},[62,11530,11531],{"class":114},"  \"build\"",[62,11533,111],{"class":79},[62,11535,11536],{"class":273},"\"tsc -b && vite build && node scripts\u002Fcopy-dist.mjs\"\n",[62,11538,11539],{"class":64,"line":91},[62,11540,139],{"class":79},[1503,11542,11544],{"id":11543},"_3-后端embed-httpfilesystem-包装","3. 后端：embed + http.FileSystem 包装",[53,11546,11548],{"className":4396,"code":11547,"language":1398,"meta":58,"style":58},"\u002F\u002F backend\u002Fweb\u002Fembed.go\npackage web\n\nimport (\n    \"embed\"\n    \"net\u002Fhttp\"\n    \"strings\"\n)\n\n\u002F\u002Fgo:embed all:dist\nvar distFS embed.FS\n\n\u002F\u002F Dist 内嵌前端静态文件系统\nvar Dist http.FileSystem = &fileSystem{prefix: \"dist\", fs: &distFS}\n\ntype fileSystem struct {\n    prefix string\n    fs     *embed.FS\n}\n\nfunc (f *fileSystem) Open(name string) (http.File, error) {\n    if name == \"\u002F\" {\n        name = f.prefix\n    } else {\n        name = f.prefix + \"\u002F\" + strings.TrimPrefix(name, \"\u002F\")\n    }\n    return http.FS(f.fs).Open(name)\n}\n",[33,11549,11550,11555,11561,11565,11571,11579,11587,11595,11599,11603,11608,11621,11625,11630,11667,11671,11683,11691,11704,11708,11712,11751,11765,11775,11785,11812,11816,11833],{"__ignoreMap":58},[62,11551,11552],{"class":64,"line":65},[62,11553,11554],{"class":68},"\u002F\u002F backend\u002Fweb\u002Fembed.go\n",[62,11556,11557,11559],{"class":64,"line":72},[62,11558,7204],{"class":166},[62,11560,7207],{"class":75},[62,11562,11563],{"class":64,"line":83},[62,11564,753],{"emptyLinePlaceholder":752},[62,11566,11567,11569],{"class":64,"line":91},[62,11568,4404],{"class":166},[62,11570,5802],{"class":79},[62,11572,11573,11575,11577],{"class":64,"line":99},[62,11574,7222],{"class":273},[62,11576,7225],{"class":75},[62,11578,1501],{"class":273},[62,11580,11581,11583,11585],{"class":64,"line":124},[62,11582,7222],{"class":273},[62,11584,7252],{"class":75},[62,11586,1501],{"class":273},[62,11588,11589,11591,11593],{"class":64,"line":130},[62,11590,7222],{"class":273},[62,11592,7270],{"class":75},[62,11594,1501],{"class":273},[62,11596,11597],{"class":64,"line":136},[62,11598,494],{"class":79},[62,11600,11601],{"class":64,"line":402},[62,11602,753],{"emptyLinePlaceholder":752},[62,11604,11605],{"class":64,"line":407},[62,11606,11607],{"class":68},"\u002F\u002Fgo:embed all:dist\n",[62,11609,11610,11612,11615,11617,11619],{"class":64,"line":433},[62,11611,7303],{"class":166},[62,11613,11614],{"class":79}," distFS ",[62,11616,7225],{"class":75},[62,11618,299],{"class":79},[62,11620,7313],{"class":75},[62,11622,11623],{"class":64,"line":438},[62,11624,753],{"emptyLinePlaceholder":752},[62,11626,11627],{"class":64,"line":446},[62,11628,11629],{"class":68},"\u002F\u002F Dist 内嵌前端静态文件系统\n",[62,11631,11632,11634,11637,11640,11642,11645,11647,11650,11653,11656,11658,11661,11664],{"class":64,"line":1448},[62,11633,7303],{"class":166},[62,11635,11636],{"class":79}," Dist ",[62,11638,11639],{"class":75},"http",[62,11641,299],{"class":79},[62,11643,11644],{"class":75},"FileSystem",[62,11646,321],{"class":166},[62,11648,11649],{"class":166}," &",[62,11651,11652],{"class":75},"fileSystem",[62,11654,11655],{"class":79},"{prefix: ",[62,11657,7353],{"class":273},[62,11659,11660],{"class":79},", fs: ",[62,11662,11663],{"class":166},"&",[62,11665,11666],{"class":79},"distFS}\n",[62,11668,11669],{"class":64,"line":1459},[62,11670,753],{"emptyLinePlaceholder":752},[62,11672,11673,11675,11678,11681],{"class":64,"line":1470},[62,11674,5789],{"class":166},[62,11676,11677],{"class":75}," fileSystem",[62,11679,11680],{"class":166}," struct",[62,11682,620],{"class":79},[62,11684,11685,11688],{"class":64,"line":1475},[62,11686,11687],{"class":79},"    prefix ",[62,11689,11690],{"class":166},"string\n",[62,11692,11693,11696,11698,11700,11702],{"class":64,"line":1484},[62,11694,11695],{"class":79},"    fs     ",[62,11697,4629],{"class":166},[62,11699,7225],{"class":75},[62,11701,299],{"class":79},[62,11703,7313],{"class":75},[62,11705,11706],{"class":64,"line":1489},[62,11707,139],{"class":79},[62,11709,11710],{"class":64,"line":1695},[62,11711,753],{"emptyLinePlaceholder":752},[62,11713,11714,11716,11718,11721,11723,11725,11727,11730,11732,11734,11736,11738,11740,11742,11745,11747,11749],{"class":64,"line":1703},[62,11715,4597],{"class":166},[62,11717,218],{"class":79},[62,11719,11720],{"class":176},"f ",[62,11722,4629],{"class":166},[62,11724,11652],{"class":75},[62,11726,654],{"class":79},[62,11728,11729],{"class":75},"Open",[62,11731,173],{"class":79},[62,11733,7919],{"class":176},[62,11735,10878],{"class":166},[62,11737,4626],{"class":79},[62,11739,11639],{"class":75},[62,11741,299],{"class":79},[62,11743,11744],{"class":75},"File",[62,11746,186],{"class":79},[62,11748,4642],{"class":166},[62,11750,4224],{"class":79},[62,11752,11753,11755,11758,11760,11763],{"class":64,"line":1708},[62,11754,253],{"class":166},[62,11756,11757],{"class":79}," name ",[62,11759,5497],{"class":166},[62,11761,11762],{"class":273}," \"\u002F\"",[62,11764,620],{"class":79},[62,11766,11767,11770,11772],{"class":64,"line":2336},[62,11768,11769],{"class":79},"        name ",[62,11771,200],{"class":166},[62,11773,11774],{"class":79}," f.prefix\n",[62,11776,11777,11780,11783],{"class":64,"line":2350},[62,11778,11779],{"class":79},"    } ",[62,11781,11782],{"class":166},"else",[62,11784,620],{"class":79},[62,11786,11787,11789,11791,11794,11796,11798,11801,11803,11805,11808,11810],{"class":64,"line":2357},[62,11788,11769],{"class":79},[62,11790,200],{"class":166},[62,11792,11793],{"class":79}," f.prefix ",[62,11795,1018],{"class":166},[62,11797,11762],{"class":273},[62,11799,11800],{"class":166}," +",[62,11802,5559],{"class":79},[62,11804,7391],{"class":75},[62,11806,11807],{"class":79},"(name, ",[62,11809,7397],{"class":273},[62,11811,494],{"class":79},[62,11813,11814],{"class":64,"line":2362},[62,11815,127],{"class":79},[62,11817,11818,11820,11822,11825,11828,11830],{"class":64,"line":2368},[62,11819,4654],{"class":166},[62,11821,10422],{"class":79},[62,11823,11824],{"class":75},"FS",[62,11826,11827],{"class":79},"(f.fs).",[62,11829,11729],{"class":75},[62,11831,11832],{"class":79},"(name)\n",[62,11834,11835],{"class":64,"line":2376},[62,11836,139],{"class":79},[15,11838,11839],{},"两个细节：",[2810,11841,11842,11865],{},[516,11843,11844,11852,11853,7832,11855,11857,11858,11860,11861,11864],{},[38,11845,11846,11849,11850,11342],{},[33,11847,11848],{},"all:dist"," 里的 ",[33,11851,11341],{},"：embed 目录时默认会排除 ",[33,11854,7815],{},[33,11856,299],{}," 开头的文件（上篇坑二就是被这个坑了，当时靠改 Vite 输出名绕开）。",[33,11859,11341],{}," 前缀表示「全部包含」，前端构建产物里那些 ",[33,11862,11863],{},"_xxx.js"," 助手文件直接安全落地，不用再改前端配置；",[516,11866,11867,11870,11871,11873,11874,11877],{},[33,11868,11869],{},"http.FileSystem"," 包装：加了一层 ",[33,11872,7181],{}," 前缀映射，",[33,11875,11876],{},"http.FileServer"," 和 Gin 都能直接用。",[1503,11879,11881],{"id":11880},"_4-路由静态服务-缓存策略","4. 路由：静态服务 + 缓存策略",[53,11883,11885],{"className":4396,"code":11884,"language":1398,"meta":58,"style":58},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\nfunc NewRouter(db *gorm.DB, uploadDir string) *gin.Engine {\n    r := gin.Default()\n    ...\n    dist := http.FileServer(web.Dist)\n    r.GET(\"\u002Fdist\", func(c *gin.Context) { c.Redirect(302, \"\u002Fdist\u002F\") })\n    r.GET(\"\u002Fdist\u002F*filepath\", func(c *gin.Context) {\n        p := c.Param(\"filepath\")\n        if p == \"\u002F\" || p == \"\u002Findex.html\" {\n            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n            c.Header(\"Cache-Control\", \"no-cache, no-store, must-revalidate\")\n        } else if strings.HasPrefix(p, \"\u002Fassets\u002F\") {\n            \u002F\u002F 带内容 hash 的资源可放心长缓存\n            c.Header(\"Cache-Control\", \"public, max-age=31536000, immutable\")\n        }\n        http.StripPrefix(\"\u002Fdist\", dist).ServeHTTP(c.Writer, c.Request)\n    })\n    r.GET(\"\u002F\", func(c *gin.Context) { c.Redirect(302, \"\u002Fdist\u002F\") })\n    ...\n}\n",[33,11886,11887,11892,11934,11946,11951,11966,12011,12040,12058,12079,12084,12103,12126,12131,12148,12152,12173,12177,12217,12221],{"__ignoreMap":58},[62,11888,11889],{"class":64,"line":65},[62,11890,11891],{"class":68},"\u002F\u002F backend\u002Finternal\u002Fhandler\u002Frouter.go\n",[62,11893,11894,11896,11899,11901,11904,11906,11909,11911,11914,11916,11919,11921,11923,11925,11927,11929,11932],{"class":64,"line":72},[62,11895,4597],{"class":166},[62,11897,11898],{"class":75}," NewRouter",[62,11900,173],{"class":79},[62,11902,11903],{"class":176},"db",[62,11905,5222],{"class":166},[62,11907,11908],{"class":75},"gorm",[62,11910,299],{"class":79},[62,11912,11913],{"class":75},"DB",[62,11915,186],{"class":79},[62,11917,11918],{"class":176},"uploadDir",[62,11920,10878],{"class":166},[62,11922,654],{"class":79},[62,11924,4629],{"class":166},[62,11926,5225],{"class":75},[62,11928,299],{"class":79},[62,11930,11931],{"class":75},"Engine",[62,11933,620],{"class":79},[62,11935,11936,11938,11940,11942,11944],{"class":64,"line":83},[62,11937,5038],{"class":79},[62,11939,4423],{"class":166},[62,11941,5043],{"class":79},[62,11943,5046],{"class":75},[62,11945,374],{"class":79},[62,11947,11948],{"class":64,"line":91},[62,11949,11950],{"class":166},"    ...\n",[62,11952,11953,11956,11958,11960,11963],{"class":64,"line":99},[62,11954,11955],{"class":79},"    dist ",[62,11957,4423],{"class":166},[62,11959,10422],{"class":79},[62,11961,11962],{"class":75},"FileServer",[62,11964,11965],{"class":79},"(web.Dist)\n",[62,11967,11968,11970,11972,11974,11977,11979,11981,11983,11985,11987,11989,11991,11993,11996,11999,12001,12004,12006,12008],{"class":64,"line":124},[62,11969,5176],{"class":79},[62,11971,5205],{"class":75},[62,11973,173],{"class":79},[62,11975,11976],{"class":273},"\"\u002Fdist\"",[62,11978,186],{"class":79},[62,11980,4597],{"class":166},[62,11982,173],{"class":79},[62,11984,5219],{"class":176},[62,11986,5222],{"class":166},[62,11988,5225],{"class":75},[62,11990,299],{"class":79},[62,11992,4610],{"class":75},[62,11994,11995],{"class":79},") { c.",[62,11997,11998],{"class":75},"Redirect",[62,12000,173],{"class":79},[62,12002,12003],{"class":114},"302",[62,12005,186],{"class":79},[62,12007,11409],{"class":273},[62,12009,12010],{"class":79},") })\n",[62,12012,12013,12015,12017,12019,12022,12024,12026,12028,12030,12032,12034,12036,12038],{"class":64,"line":130},[62,12014,5176],{"class":79},[62,12016,5205],{"class":75},[62,12018,173],{"class":79},[62,12020,12021],{"class":273},"\"\u002Fdist\u002F*filepath\"",[62,12023,186],{"class":79},[62,12025,4597],{"class":166},[62,12027,173],{"class":79},[62,12029,5219],{"class":176},[62,12031,5222],{"class":166},[62,12033,5225],{"class":75},[62,12035,299],{"class":79},[62,12037,4610],{"class":75},[62,12039,4224],{"class":79},[62,12041,12042,12044,12046,12048,12051,12053,12056],{"class":64,"line":136},[62,12043,7384],{"class":79},[62,12045,4423],{"class":166},[62,12047,5476],{"class":79},[62,12049,12050],{"class":75},"Param",[62,12052,173],{"class":79},[62,12054,12055],{"class":273},"\"filepath\"",[62,12057,494],{"class":79},[62,12059,12060,12062,12064,12066,12068,12070,12072,12074,12077],{"class":64,"line":402},[62,12061,5491],{"class":166},[62,12063,7406],{"class":79},[62,12065,5497],{"class":166},[62,12067,11762],{"class":273},[62,12069,5592],{"class":166},[62,12071,7406],{"class":79},[62,12073,5497],{"class":166},[62,12075,12076],{"class":273}," \"\u002Findex.html\"",[62,12078,620],{"class":79},[62,12080,12081],{"class":64,"line":407},[62,12082,12083],{"class":68},"            \u002F\u002F index.html 禁缓存：升级后浏览器立刻拿到新入口\n",[62,12085,12086,12088,12091,12093,12096,12098,12101],{"class":64,"line":433},[62,12087,5507],{"class":79},[62,12089,12090],{"class":75},"Header",[62,12092,173],{"class":79},[62,12094,12095],{"class":273},"\"Cache-Control\"",[62,12097,186],{"class":79},[62,12099,12100],{"class":273},"\"no-cache, no-store, must-revalidate\"",[62,12102,494],{"class":79},[62,12104,12105,12108,12110,12113,12115,12118,12121,12124],{"class":64,"line":438},[62,12106,12107],{"class":79},"        } ",[62,12109,11782],{"class":166},[62,12111,12112],{"class":166}," if",[62,12114,5559],{"class":79},[62,12116,12117],{"class":75},"HasPrefix",[62,12119,12120],{"class":79},"(p, ",[62,12122,12123],{"class":273},"\"\u002Fassets\u002F\"",[62,12125,4224],{"class":79},[62,12127,12128],{"class":64,"line":446},[62,12129,12130],{"class":68},"            \u002F\u002F 带内容 hash 的资源可放心长缓存\n",[62,12132,12133,12135,12137,12139,12141,12143,12146],{"class":64,"line":1448},[62,12134,5507],{"class":79},[62,12136,12090],{"class":75},[62,12138,173],{"class":79},[62,12140,12095],{"class":273},[62,12142,186],{"class":79},[62,12144,12145],{"class":273},"\"public, max-age=31536000, immutable\"",[62,12147,494],{"class":79},[62,12149,12150],{"class":64,"line":1459},[62,12151,5545],{"class":79},[62,12153,12154,12157,12160,12162,12164,12167,12170],{"class":64,"line":1470},[62,12155,12156],{"class":79},"        http.",[62,12158,12159],{"class":75},"StripPrefix",[62,12161,173],{"class":79},[62,12163,11976],{"class":273},[62,12165,12166],{"class":79},", dist).",[62,12168,12169],{"class":75},"ServeHTTP",[62,12171,12172],{"class":79},"(c.Writer, c.Request)\n",[62,12174,12175],{"class":64,"line":1475},[62,12176,5271],{"class":79},[62,12178,12179,12181,12183,12185,12187,12189,12191,12193,12195,12197,12199,12201,12203,12205,12207,12209,12211,12213,12215],{"class":64,"line":1484},[62,12180,5176],{"class":79},[62,12182,5205],{"class":75},[62,12184,173],{"class":79},[62,12186,7397],{"class":273},[62,12188,186],{"class":79},[62,12190,4597],{"class":166},[62,12192,173],{"class":79},[62,12194,5219],{"class":176},[62,12196,5222],{"class":166},[62,12198,5225],{"class":75},[62,12200,299],{"class":79},[62,12202,4610],{"class":75},[62,12204,11995],{"class":79},[62,12206,11998],{"class":75},[62,12208,173],{"class":79},[62,12210,12003],{"class":114},[62,12212,186],{"class":79},[62,12214,11409],{"class":273},[62,12216,12010],{"class":79},[62,12218,12219],{"class":64,"line":1489},[62,12220,11950],{"class":166},[62,12222,12223],{"class":64,"line":1695},[62,12224,139],{"class":79},[15,12226,12227],{},"缓存策略是内嵌发布的必修课（坑二细说）：入口禁缓存、hash 资源长缓存。",[1503,12229,12231],{"id":12230},"_5-打包脚本windows-版-linux-交叉编译版","5. 打包脚本：Windows 版 + Linux 交叉编译版",[53,12233,12235],{"className":1275,"code":12234,"language":1277,"meta":58,"style":58},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\nset -euo pipefail\ncd \"$(dirname \"$0\")\"\n\nBINARY=\"${BINARY:-magic-img}\"\n\necho \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n(cd frontend && npm run build)\n\necho \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n(cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver)\nchmod +x \"backend\u002F$BINARY\"\n\necho \"[3\u002F3] 打包完成: backend\u002F$BINARY\"\n",[33,12236,12237,12242,12247,12257,12275,12279,12300,12304,12312,12331,12335,12342,12399,12414,12418],{"__ignoreMap":58},[62,12238,12239],{"class":64,"line":65},[62,12240,12241],{"class":68},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[62,12243,12244],{"class":64,"line":72},[62,12245,12246],{"class":68},"# build-linux.sh —— 在 Windows\u002FmacOS\u002FLinux 任意平台交叉编译 linux\u002Famd64 产物\n",[62,12248,12249,12251,12254],{"class":64,"line":83},[62,12250,7662],{"class":114},[62,12252,12253],{"class":114}," -euo",[62,12255,12256],{"class":273}," pipefail\n",[62,12258,12259,12261,12264,12267,12269,12272],{"class":64,"line":91},[62,12260,7741],{"class":114},[62,12262,12263],{"class":273}," \"$(",[62,12265,12266],{"class":75},"dirname",[62,12268,1304],{"class":273},[62,12270,12271],{"class":114},"$0",[62,12273,12274],{"class":273},"\")\"\n",[62,12276,12277],{"class":64,"line":99},[62,12278,753],{"emptyLinePlaceholder":752},[62,12280,12281,12284,12286,12289,12291,12294,12297],{"class":64,"line":124},[62,12282,12283],{"class":79},"BINARY",[62,12285,200],{"class":166},[62,12287,12288],{"class":273},"\"${",[62,12290,12283],{"class":79},[62,12292,12293],{"class":166},":-",[62,12295,12296],{"class":79},"magic-img",[62,12298,12299],{"class":273},"}\"\n",[62,12301,12302],{"class":64,"line":130},[62,12303,753],{"emptyLinePlaceholder":752},[62,12305,12306,12309],{"class":64,"line":136},[62,12307,12308],{"class":114},"echo",[62,12310,12311],{"class":273}," \"[1\u002F3] 构建前端并复制产物到 backend\u002Fweb\u002Fdist ...\"\n",[62,12313,12314,12316,12318,12321,12323,12325,12327,12329],{"class":64,"line":402},[62,12315,173],{"class":79},[62,12317,7741],{"class":114},[62,12319,12320],{"class":273}," frontend",[62,12322,1342],{"class":79},[62,12324,6535],{"class":75},[62,12326,1292],{"class":273},[62,12328,1413],{"class":273},[62,12330,494],{"class":79},[62,12332,12333],{"class":64,"line":407},[62,12334,753],{"emptyLinePlaceholder":752},[62,12336,12337,12339],{"class":64,"line":433},[62,12338,12308],{"class":114},[62,12340,12341],{"class":273}," \"[2\u002F3] 交叉编译后端并内嵌前端 (CGO_ENABLED=0 GOOS=linux GOARCH=amd64) ...\"\n",[62,12343,12344,12346,12348,12351,12354,12356,12358,12361,12363,12366,12369,12371,12374,12376,12378,12380,12382,12385,12388,12391,12394,12397],{"class":64,"line":438},[62,12345,173],{"class":79},[62,12347,7741],{"class":114},[62,12349,12350],{"class":273}," backend",[62,12352,12353],{"class":79}," && CGO_ENABLED",[62,12355,200],{"class":166},[62,12357,264],{"class":273},[62,12359,12360],{"class":79}," GOOS",[62,12362,200],{"class":166},[62,12364,12365],{"class":273},"linux",[62,12367,12368],{"class":79}," GOARCH",[62,12370,200],{"class":166},[62,12372,12373],{"class":273},"amd64",[62,12375,1761],{"class":75},[62,12377,1413],{"class":273},[62,12379,1422],{"class":114},[62,12381,1304],{"class":273},[62,12383,12384],{"class":79},"$BINARY",[62,12386,12387],{"class":273},"\"",[62,12389,12390],{"class":114}," -ldflags=",[62,12392,12393],{"class":273},"\"-s -w\"",[62,12395,12396],{"class":273}," .\u002Fcmd\u002Fserver",[62,12398,494],{"class":79},[62,12400,12401,12404,12407,12410,12412],{"class":64,"line":446},[62,12402,12403],{"class":75},"chmod",[62,12405,12406],{"class":273}," +x",[62,12408,12409],{"class":273}," \"backend\u002F",[62,12411,12384],{"class":79},[62,12413,1501],{"class":273},[62,12415,12416],{"class":64,"line":1448},[62,12417,753],{"emptyLinePlaceholder":752},[62,12419,12420,12422,12425,12427],{"class":64,"line":1459},[62,12421,12308],{"class":114},[62,12423,12424],{"class":273}," \"[3\u002F3] 打包完成: backend\u002F",[62,12426,12384],{"class":79},[62,12428,1501],{"class":273},[15,12430,12431,12432,12435,12436,41],{},"Windows 版（",[33,12433,12434],{},"build-win.bat","）同理，只是产物叫 ",[33,12437,12438],{},"magic-img.exe",[26,12440,7775],{"id":7775},[1503,12442,12444,12445,12448,12449,12451],{"id":12443},"坑一goembed-dist-静默漏掉-_-开头文件升级解法","坑一：",[33,12446,12447],{},"\u002F\u002Fgo:embed dist"," 静默漏掉 ",[33,12450,7815],{}," 开头文件（升级解法）",[15,12453,12454,12455,12462,12463,12465],{},"上篇说过：embed 目录时",[38,12456,12457,12458,7832,12460,7816],{},"硬编码排除 ",[33,12459,7815],{},[33,12461,299],{},"。上篇的解法是改 Vite 输出文件名去掉 ",[33,12464,7815],{}," 前缀；这次直接用：",[53,12467,12469],{"className":4396,"code":12468,"language":1398,"meta":58,"style":58},"\u002F\u002Fgo:embed all:dist   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[33,12470,12471],{"__ignoreMap":58},[62,12472,12473,12476],{"class":64,"line":65},[62,12474,12475],{"class":68},"\u002F\u002Fgo:embed all:dist",[62,12477,12478],{"class":68},"   \u002F\u002F all: 前缀 = 包含全部文件，不再静默跳过\n",[15,12480,12481],{},[38,12482,12483,12484,12486,12487,12489],{},"结论：能用 ",[33,12485,11341],{}," 就用 ",[33,12488,11341],{},"，从根上消灭这类\"文件莫名 404\"的问题。",[1503,12491,12493],{"id":12492},"坑二开发版是新的打包版却是旧的","坑二：开发版是新的、打包版却是旧的",[15,12495,12496,12497,12500],{},"现象：Vite 开发服务一切正常，",[33,12498,12499],{},":8080\u002Fdist\u002F"," 打包版却是旧界面，强刷也不一定好。",[15,12502,12503,12504,12506,12507,12510,12511,12516],{},"排查过程值得记录：先是怀疑 ",[33,12505,7978],{}," 缓存了旧 embed（上篇坑三的思路），重编重启无效；然后直接抓取 8080 返回的 HTML 和 JS，发现",[38,12508,12509],{},"服务端内容已经是新的","——问题出在",[38,12512,12513,12514],{},"浏览器缓存了 ",[33,12515,11475],{},"：它引用旧 hash 的资源，一直不更新。",[15,12518,12519,12521,12522,12525,12526,12529],{},[38,12520,7793],{},"：入口文件响应 ",[33,12523,12524],{},"Cache-Control: no-store","（见上面路由代码），带 hash 的资源给 ",[33,12527,12528],{},"immutable"," 长缓存。升级后浏览器每次拉新入口，资源文件名一变就自动换新，两全其美。",[10128,12531,12532],{},[15,12533,12534],{},"教训：排查\"新旧不一致\"先分清是哪一层——服务端产物、浏览器入口缓存、还是资源缓存。抓包看响应内容再动手。",[1503,12536,12538],{"id":12537},"坑三windows-上跑的-build-linuxsh产出的文件-linux-却执行不了","坑三：Windows 上跑的 build-linux.sh，产出的文件 Linux 却执行不了",[15,12540,12541,12542,7022,12545,12551,12552,12554],{},"最初脚本只写了 ",[33,12543,12544],{},"CGO_ENABLED=0 go build",[38,12546,12547,12548],{},"没指定 ",[33,12549,12550],{},"GOOS\u002FGOARCH","。在 Windows 上跑，产出的其实是个 Windows PE 文件（只是没叫 ",[33,12553,8280],{},"），传到 Linux 自然「无法执行」。",[15,12556,12557,12559],{},[38,12558,7793],{},"：脚本里显式交叉编译：",[53,12561,12563],{"className":1275,"code":12562,"language":1277,"meta":58,"style":58},"CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o \"$BINARY\" -ldflags=\"-s -w\" .\u002Fcmd\u002Fserver\n",[33,12564,12565],{"__ignoreMap":58},[62,12566,12567,12570,12572,12574,12576,12578,12580,12582,12584,12586,12588,12590,12592,12594,12596,12598,12600,12602],{"class":64,"line":65},[62,12568,12569],{"class":79},"CGO_ENABLED",[62,12571,200],{"class":166},[62,12573,264],{"class":273},[62,12575,12360],{"class":79},[62,12577,200],{"class":166},[62,12579,12365],{"class":273},[62,12581,12368],{"class":79},[62,12583,200],{"class":166},[62,12585,12373],{"class":273},[62,12587,1761],{"class":75},[62,12589,1413],{"class":273},[62,12591,1422],{"class":114},[62,12593,1304],{"class":273},[62,12595,12384],{"class":79},[62,12597,12387],{"class":273},[62,12599,12390],{"class":114},[62,12601,12393],{"class":273},[62,12603,12604],{"class":273}," .\u002Fcmd\u002Fserver\n",[15,12606,12607,12608,12611,12612,12615,12616],{},"能这么干的前提是",[38,12609,12610],{},"全依赖树无 cgo","——SQLite 用的是 modernc 纯 Go 驱动，bcrypt 用 ",[33,12613,12614],{},"x\u002Fcrypto","。这也是选型时就要留意的点：",[38,12617,12618],{},"想跨平台交叉编译，先把 cgo 依赖清干净。",[15,12620,12621,12622,12625,12626,12629],{},"另外两个小提醒：Windows 上编出的文件没有 Unix 执行位，上传服务器后记得 ",[33,12623,12624],{},"chmod +x","；",[33,12627,12628],{},"-ldflags=\"-s -w\""," 剥离符号，体积从 30MB+ 降到 26MB。",[1503,12631,12633],{"id":12632},"坑四dist-被-gitignore-后embed-直接编译失败","坑四：dist 被 gitignore 后，embed 直接编译失败",[15,12635,12636,12638,12639,12641,12642,12645,12646,12648],{},[33,12637,11500],{}," 是构建产物、入了 .gitignore，但 ",[33,12640,7177],{}," 在",[38,12643,12644],{},"编译期","就要读它——刚 clone 下来直接 ",[33,12647,7978],{}," 会报 embed 找不到文件。",[15,12650,12651,12653,12654,12657],{},[38,12652,7793],{},"：gitignore 里保留 ",[33,12655,12656],{},"dist\u002F.gitkeep","（一个空文件），让目录存在于版本库中：",[53,12659,12663],{"className":12660,"code":12661,"language":12662,"meta":58,"style":58},"language-gitignore shiki shiki-themes github-light github-dark","backend\u002Fweb\u002Fdist\u002F*\n!backend\u002Fweb\u002Fdist\u002F\n!backend\u002Fweb\u002Fdist\u002F.gitkeep\n","gitignore",[33,12664,12665,12670,12675],{"__ignoreMap":58},[62,12666,12667],{"class":64,"line":65},[62,12668,12669],{},"backend\u002Fweb\u002Fdist\u002F*\n",[62,12671,12672],{"class":64,"line":72},[62,12673,12674],{},"!backend\u002Fweb\u002Fdist\u002F\n",[62,12676,12677],{"class":64,"line":83},[62,12678,12679],{},"!backend\u002Fweb\u002Fdist\u002F.gitkeep\n",[15,12681,12682,12683,12685,12686,12688],{},"这样 clone 后可以先 ",[33,12684,7978],{}," 通过，再 ",[33,12687,11493],{}," 灌入真实前端。",[1503,12690,12692],{"id":12691},"坑五500kb-chunk-警告别慌","坑五：>500KB chunk 警告，别慌",[15,12694,12695,12696,12699,12700,41],{},"Vite 构建会警告单 chunk 超过 500KB。这个项目里 Base UI 组件库全量引入，单 chunk 约 640KB——",[38,12697,12698],{},"属正常，不要为了消警告去拆配置","。真要优化就上路由级 ",[33,12701,12702],{},"React.lazy",[26,12704,7991],{"id":7991},[53,12706,12708],{"className":1275,"code":12707,"language":1277,"meta":58,"style":58},"$ build-win.bat        # backendmagic-img.exe\n$ bash build-linux.sh  # backendmagic-img（linux\u002Famd64 ELF）\n",[33,12709,12710,12720],{"__ignoreMap":58},[62,12711,12712,12714,12717],{"class":64,"line":65},[62,12713,293],{"class":75},[62,12715,12716],{"class":273}," build-win.bat",[62,12718,12719],{"class":68},"        # backendmagic-img.exe\n",[62,12721,12722,12724,12726,12729],{"class":64,"line":72},[62,12723,293],{"class":75},[62,12725,8003],{"class":273},[62,12727,12728],{"class":273}," build-linux.sh",[62,12730,12731],{"class":68},"  # backendmagic-img（linux\u002Famd64 ELF）\n",[15,12733,12734,12735,12738,12739,41],{},"一个约 26MB 的二进制，内嵌 React 前端、Gin API、SQLite 数据初始化，拷到 Windows\u002FLinux 机器直接跑，浏览器访问 ",[33,12736,12737],{},"http:\u002F\u002Flocalhost:8080"," 自动重定向到 ",[33,12740,11479],{},[26,12742,1168],{"id":1168},[15,12744,12745],{},"这轮相比上篇的三点升级：",[513,12747,12748,12754,12764],{},[516,12749,12750,12753],{},[38,12751,12752],{},"hash 路由 + base \u002Fdist","：省掉 SPA fallback，静态服务即开即用；",[516,12755,12756,4115,12761,12763],{},[38,12757,12758,12760],{},[33,12759,11341],{}," embed 前缀",[33,12762,7815],{}," 开头文件问题一劳永逸；",[516,12765,12766,12772],{},[38,12767,12768,12769,12771],{},"显式 ",[33,12770,12550],{}," 交叉编译","：让「一个脚本、任意平台、产出 Linux 二进制」成为现实。",[15,12774,12775,12776,12779],{},"加上",[38,12777,12778],{},"入口 no-store \u002F 资源 immutable"," 的缓存策略，单文件分发的体验才算闭环——不然每次升级都要和浏览器缓存打架。",[1228,12781,12782],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":58,"searchDepth":72,"depth":83,"links":12784},[12785,12786,12787,12794,12802,12803],{"id":13,"depth":72,"text":13},{"id":7171,"depth":72,"text":7171},{"id":7185,"depth":72,"text":7185,"children":12788},[12789,12790,12791,12792,12793],{"id":11369,"depth":83,"text":11370},{"id":11487,"depth":83,"text":11488},{"id":11543,"depth":83,"text":11544},{"id":11880,"depth":83,"text":11881},{"id":12230,"depth":83,"text":12231},{"id":7775,"depth":72,"text":7775,"children":12795},[12796,12798,12799,12800,12801],{"id":12443,"depth":83,"text":12797},"坑一：\u002F\u002Fgo:embed dist 静默漏掉 _ 开头文件（升级解法）",{"id":12492,"depth":83,"text":12493},{"id":12537,"depth":83,"text":12538},{"id":12632,"depth":83,"text":12633},{"id":12691,"depth":83,"text":12692},{"id":7991,"depth":72,"text":7991},{"id":1168,"depth":72,"text":1168},"以 Magic Img（React 19 + Vite + Gin + SQLite）为实例，记录把前端塞进 Go 二进制、并在 Windows 上交叉编译 Linux 产物的完整过程：embed 的 all 前缀、hash 路由、缓存策略、GOOS 交叉编译等实操坑。","\u002Fposts\u002Fgo-embed-react-single-binary\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Fgo-embed-react-single-binary","2026-08-16T00:00:00",{"title":11281,"description":12804},"posts\u002Fgo-embed-react-single-binary\u002Findex",[2064,10097,7225,12812,12813],"交叉编译","打包","WV-T-MOO5Cu-aycw-Ony8s9hjA3_k0S4XU-T0QsCow0",{"id":12816,"title":12817,"author":6,"body":12818,"description":14063,"difficulty":2475,"draft":1243,"extension":1244,"image":14064,"meta":14065,"navigation":752,"path":14066,"pinned":1243,"published":12808,"seo":14067,"series":14068,"seriesOrder":65,"stem":14069,"tags":14070,"updated":11277,"__hash__":14074},"posts\u002Fposts\u002Freact-photo-album-masonry\u002Findex.md","用 react-photo-album 给生图图库做瀑布流排版",{"type":8,"value":12819,"toc":14046},[12820,12823,12826,12844,12851,12915,12926,12929,12952,12955,12959,12973,12977,12993,13171,13175,13572,13575,13609,13612,13616,13626,13632,13657,13662,13673,13679,13690,13693,13733,13746,13869,13881,13885,13892,13910,13915,14005,14011,14015,14026,14029,14032,14038,14043],[26,12821,12822],{"id":12822},"技术背景",[15,12824,12825],{},"Magic Img 是我做的一个 AI 生图提示词管理工具（自托管、本地优先），其中「生图图库」页面要把所有生成图片以图片墙的形式展示出来。图片墙有两个天然矛盾：",[513,12827,12828,12834],{},[516,12829,12830,12833],{},[38,12831,12832],{},"等宽网格","（CSS Grid）：整齐，但每行高度由最高的那张决定——不同比例的图片混排时，矮图下面会留下大洞，看起来\"很乱\"；",[516,12835,12836,12839,12840,12843],{},[38,12837,12838],{},"原生比例展示","：图库必须不裁剪（这是产品约束），所以不能用 ",[33,12841,12842],{},"object-cover"," 强行统一比例。",[15,12845,12846,12847,12850],{},"业内的标准解法是 ",[38,12848,12849],{},"Masonry 瀑布流","：按列紧密堆放，每张图保持原始比例，列间高度错落但整体紧凑。实现瀑布流的常见方案有三类：",[865,12852,12853,12867],{},[868,12854,12855],{},[871,12856,12857,12859,12862,12865],{},[874,12858,876],{},[874,12860,12861],{},"依赖",[874,12863,12864],{},"顺序",[874,12866,6270],{},[884,12868,12869,12886,12899],{},[871,12870,12871,12878,12880,12883],{},[889,12872,12873,12874,12877],{},"CSS ",[33,12875,12876],{},"columns"," 多列",[889,12879,2645],{},[889,12881,12882],{},"纵向阅读序（先下后右）",[889,12884,12885],{},"最新图片不再出现在左上角，时间语义丢失",[871,12887,12888,12891,12893,12896],{},[889,12889,12890],{},"grid row-span 自研",[889,12892,2645],{},[889,12894,12895],{},"保持",[889,12897,12898],{},"需固定列数 + 固定行高，逻辑要自己写",[871,12900,12901,12906,12909,12912],{},[889,12902,12903],{},[38,12904,12905],{},"react-photo-album",[889,12907,12908],{},"~4KB，无依赖",[889,12910,12911],{},"基本保持（逐列最短列填充）",[889,12913,12914],{},"专为相册墙设计，TS 友好，维护活跃",[15,12916,12917,12918,12921,12922,12925],{},"最终选了 ",[38,12919,12920],{},"react-photo-album v3","：体积小、类型齐全，而且它的 Masonry 模式恰好需要「每张图的 ",[33,12923,12924],{},"width\u002Fheight","」——我们的数据库里本来就存了，零额外成本。",[26,12927,12928],{"id":12928},"需求梳理",[513,12930,12931,12937,12943,12946,12949],{},[516,12932,12933,12934,12625],{},"展示全部生成图（21 张起，会持续增长），",[38,12935,12936],{},"原比例、不裁剪",[516,12938,12939,12940,12625],{},"不同尺寸（2304×1728、4608×3456 等）混排时",[38,12941,12942],{},"紧凑、不留洞",[516,12944,12945],{},"顺序基本保持「新图靠前」；",[516,12947,12948],{},"点击任意一张打开大图预览（YARL 灯箱），悬停显示所属提示词；",[516,12950,12951],{},"平板性能：墙上只用 300px 缩略图，原图只进灯箱按需加载。",[26,12953,12954],{"id":12954},"安装与使用",[1503,12956,12958],{"id":12957},"_1-安装","1. 安装",[53,12960,12962],{"className":1275,"code":12961,"language":1277,"meta":58,"style":58},"npm install react-photo-album\n",[33,12963,12964],{"__ignoreMap":58},[62,12965,12966,12968,12970],{"class":64,"line":65},[62,12967,6535],{"class":75},[62,12969,1347],{"class":273},[62,12971,12972],{"class":273}," react-photo-album\n",[1503,12974,12976],{"id":12975},"_2-准备数据关键输入是宽高","2. 准备数据（关键输入是宽高）",[15,12978,12979,12980,12983,12984,7798,12986,7798,12989,12992],{},"库要求的 ",[33,12981,12982],{},"Photo"," 对象只有三个必需字段：",[33,12985,730],{},[33,12987,12988],{},"width",[33,12990,12991],{},"height","。我们的接口本来就返回每张图的原始宽高：",[53,12994,12996],{"className":11427,"code":12995,"language":11429,"meta":58,"style":58},"import PhotoAlbum, { type Photo } from \"react-photo-album\"\n\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\nimport \"react-photo-album\u002Fstyles.css\"\n\ninterface GalleryPhoto extends Photo {\n  prompt_text?: string\n}\n\nconst albumPhotos: GalleryPhoto[] = images.map((img) => ({\n  key: String(img.id),\n  src: assetUrl(img.thumb_path) || assetUrl(img.file_path),\n  width: img.width,\n  height: img.height,\n  alt: img.prompt_text || `生成图 ${img.id}`,\n  prompt_text: img.prompt_text,\n}))\n",[33,12997,12998,13016,13021,13028,13032,13048,13058,13062,13066,13099,13110,13129,13134,13139,13161,13166],{"__ignoreMap":58},[62,12999,13000,13002,13005,13007,13010,13013],{"class":64,"line":65},[62,13001,4404],{"class":166},[62,13003,13004],{"class":79}," PhotoAlbum, { ",[62,13006,5789],{"class":166},[62,13008,13009],{"class":79}," Photo } ",[62,13011,13012],{"class":166},"from",[62,13014,13015],{"class":273}," \"react-photo-album\"\n",[62,13017,13018],{"class":64,"line":72},[62,13019,13020],{"class":68},"\u002F\u002F 必须引入样式表！容器\u002F列的布局规则都在这里（坑 1 详述）\n",[62,13022,13023,13025],{"class":64,"line":83},[62,13024,4404],{"class":166},[62,13026,13027],{"class":273}," \"react-photo-album\u002Fstyles.css\"\n",[62,13029,13030],{"class":64,"line":91},[62,13031,753],{"emptyLinePlaceholder":752},[62,13033,13034,13037,13040,13043,13046],{"class":64,"line":99},[62,13035,13036],{"class":166},"interface",[62,13038,13039],{"class":75}," GalleryPhoto",[62,13041,13042],{"class":166}," extends",[62,13044,13045],{"class":75}," Photo",[62,13047,620],{"class":79},[62,13049,13050,13053,13056],{"class":64,"line":124},[62,13051,13052],{"class":176},"  prompt_text",[62,13054,13055],{"class":166},"?:",[62,13057,5795],{"class":114},[62,13059,13060],{"class":64,"line":130},[62,13061,139],{"class":79},[62,13063,13064],{"class":64,"line":136},[62,13065,753],{"emptyLinePlaceholder":752},[62,13067,13068,13070,13073,13075,13077,13079,13081,13084,13087,13090,13092,13094,13096],{"class":64,"line":402},[62,13069,415],{"class":166},[62,13071,13072],{"class":114}," albumPhotos",[62,13074,180],{"class":166},[62,13076,13039],{"class":75},[62,13078,197],{"class":79},[62,13080,200],{"class":166},[62,13082,13083],{"class":79}," images.",[62,13085,13086],{"class":75},"map",[62,13088,13089],{"class":79},"((",[62,13091,770],{"class":176},[62,13093,654],{"class":79},[62,13095,617],{"class":166},[62,13097,13098],{"class":79}," ({\n",[62,13100,13101,13104,13107],{"class":64,"line":407},[62,13102,13103],{"class":79},"  key: ",[62,13105,13106],{"class":75},"String",[62,13108,13109],{"class":79},"(img.id),\n",[62,13111,13112,13115,13118,13121,13123,13126],{"class":64,"line":433},[62,13113,13114],{"class":79},"  src: ",[62,13116,13117],{"class":75},"assetUrl",[62,13119,13120],{"class":79},"(img.thumb_path) ",[62,13122,393],{"class":166},[62,13124,13125],{"class":75}," assetUrl",[62,13127,13128],{"class":79},"(img.file_path),\n",[62,13130,13131],{"class":64,"line":438},[62,13132,13133],{"class":79},"  width: img.width,\n",[62,13135,13136],{"class":64,"line":446},[62,13137,13138],{"class":79},"  height: img.height,\n",[62,13140,13141,13144,13146,13149,13151,13153,13156,13159],{"class":64,"line":1448},[62,13142,13143],{"class":79},"  alt: img.prompt_text ",[62,13145,393],{"class":166},[62,13147,13148],{"class":273}," `生成图 ${",[62,13150,770],{"class":79},[62,13152,299],{"class":273},[62,13154,13155],{"class":79},"id",[62,13157,13158],{"class":273},"}`",[62,13160,2912],{"class":79},[62,13162,13163],{"class":64,"line":1459},[62,13164,13165],{"class":79},"  prompt_text: img.prompt_text,\n",[62,13167,13168],{"class":64,"line":1470},[62,13169,13170],{"class":79},"}))\n",[1503,13172,13174],{"id":13173},"_3-使用-masonry-布局","3. 使用 Masonry 布局",[53,13176,13178],{"className":11427,"code":13177,"language":11429,"meta":58,"style":58},"\u003CPhotoAlbum\n  layout=\"masonry\"\n  columns={(cw) => (cw \u003C 640 ? 2 : cw \u003C 960 ? 3 : cw \u003C 1280 ? 4 : 5)}\n  spacing={16}\n  padding={0}\n  photos={albumPhotos}\n  onClick={({ index }) => openPreview(index)}\n  render={{\n    photo: (props, context) => {\n      const { onClick } = props\n      const { photo, index, width, height } = context\n      return (\n        \u003Cbutton type=\"button\" onClick={onClick} className=\"group relative block w-full ...\">\n          \u003Cimg\n            src={photo.src}\n            alt={photo.alt || \"\"}\n            decoding=\"async\"\n            style={{ width, height }}\n          \u002F>\n          {\u002F* hover 提示词浮层 *\u002F}\n          \u003Cdiv className=\"... opacity-0 group-hover:opacity-100\">\n            {photo.prompt_text}\n          \u003C\u002Fdiv>\n        \u003C\u002Fbutton>\n      )\n    },\n  }}\n\u002F>\n",[33,13179,13180,13187,13197,13265,13279,13292,13302,13326,13336,13357,13375,13403,13410,13444,13452,13462,13478,13488,13498,13503,13513,13530,13535,13544,13553,13558,13562,13567],{"__ignoreMap":58},[62,13181,13182,13184],{"class":64,"line":65},[62,13183,575],{"class":79},[62,13185,13186],{"class":114},"PhotoAlbum\n",[62,13188,13189,13192,13194],{"class":64,"line":72},[62,13190,13191],{"class":75},"  layout",[62,13193,200],{"class":166},[62,13195,13196],{"class":273},"\"masonry\"\n",[62,13198,13199,13202,13204,13207,13210,13212,13214,13217,13219,13222,13225,13227,13230,13233,13235,13238,13240,13243,13245,13247,13249,13252,13254,13257,13259,13262],{"class":64,"line":83},[62,13200,13201],{"class":75},"  columns",[62,13203,200],{"class":166},[62,13205,13206],{"class":79},"{(",[62,13208,13209],{"class":176},"cw",[62,13211,654],{"class":79},[62,13213,617],{"class":166},[62,13215,13216],{"class":79}," (cw ",[62,13218,575],{"class":166},[62,13220,13221],{"class":114}," 640",[62,13223,13224],{"class":166}," ?",[62,13226,5589],{"class":114},[62,13228,13229],{"class":166}," :",[62,13231,13232],{"class":79}," cw ",[62,13234,575],{"class":166},[62,13236,13237],{"class":114}," 960",[62,13239,13224],{"class":166},[62,13241,13242],{"class":114}," 3",[62,13244,13229],{"class":166},[62,13246,13232],{"class":79},[62,13248,575],{"class":166},[62,13250,13251],{"class":114}," 1280",[62,13253,13224],{"class":166},[62,13255,13256],{"class":114}," 4",[62,13258,13229],{"class":166},[62,13260,13261],{"class":114}," 5",[62,13263,13264],{"class":79},")}\n",[62,13266,13267,13270,13272,13274,13277],{"class":64,"line":91},[62,13268,13269],{"class":75},"  spacing",[62,13271,200],{"class":166},[62,13273,5256],{"class":79},[62,13275,13276],{"class":114},"16",[62,13278,139],{"class":79},[62,13280,13281,13284,13286,13288,13290],{"class":64,"line":99},[62,13282,13283],{"class":75},"  padding",[62,13285,200],{"class":166},[62,13287,5256],{"class":79},[62,13289,264],{"class":114},[62,13291,139],{"class":79},[62,13293,13294,13297,13299],{"class":64,"line":124},[62,13295,13296],{"class":75},"  photos",[62,13298,200],{"class":166},[62,13300,13301],{"class":79},"{albumPhotos}\n",[62,13303,13304,13307,13309,13312,13315,13318,13320,13323],{"class":64,"line":130},[62,13305,13306],{"class":75},"  onClick",[62,13308,200],{"class":166},[62,13310,13311],{"class":79},"{({ ",[62,13313,13314],{"class":176},"index",[62,13316,13317],{"class":79}," }) ",[62,13319,617],{"class":166},[62,13321,13322],{"class":75}," openPreview",[62,13324,13325],{"class":79},"(index)}\n",[62,13327,13328,13331,13333],{"class":64,"line":136},[62,13329,13330],{"class":75},"  render",[62,13332,200],{"class":166},[62,13334,13335],{"class":79},"{{\n",[62,13337,13338,13341,13344,13346,13348,13351,13353,13355],{"class":64,"line":402},[62,13339,13340],{"class":75},"    photo",[62,13342,13343],{"class":79},": (",[62,13345,725],{"class":176},[62,13347,186],{"class":79},[62,13349,13350],{"class":176},"context",[62,13352,654],{"class":79},[62,13354,617],{"class":166},[62,13356,620],{"class":79},[62,13358,13359,13361,13364,13367,13370,13372],{"class":64,"line":407},[62,13360,315],{"class":166},[62,13362,13363],{"class":79}," { ",[62,13365,13366],{"class":114},"onClick",[62,13368,13369],{"class":79}," } ",[62,13371,200],{"class":166},[62,13373,13374],{"class":79}," props\n",[62,13376,13377,13379,13381,13384,13386,13388,13390,13392,13394,13396,13398,13400],{"class":64,"line":433},[62,13378,315],{"class":166},[62,13380,13363],{"class":79},[62,13382,13383],{"class":114},"photo",[62,13385,186],{"class":79},[62,13387,13314],{"class":114},[62,13389,186],{"class":79},[62,13391,12988],{"class":114},[62,13393,186],{"class":79},[62,13395,12991],{"class":114},[62,13397,13369],{"class":79},[62,13399,200],{"class":166},[62,13401,13402],{"class":79}," context\n",[62,13404,13405,13408],{"class":64,"line":438},[62,13406,13407],{"class":166},"      return",[62,13409,5802],{"class":79},[62,13411,13412,13415,13418,13421,13423,13426,13429,13431,13434,13437,13439,13442],{"class":64,"line":446},[62,13413,13414],{"class":79},"        \u003C",[62,13416,13417],{"class":578},"button",[62,13419,13420],{"class":75}," type",[62,13422,200],{"class":166},[62,13424,13425],{"class":273},"\"button\"",[62,13427,13428],{"class":75}," onClick",[62,13430,200],{"class":166},[62,13432,13433],{"class":79},"{onClick} ",[62,13435,13436],{"class":75},"className",[62,13438,200],{"class":166},[62,13440,13441],{"class":273},"\"group relative block w-full ...\"",[62,13443,585],{"class":79},[62,13445,13446,13449],{"class":64,"line":1448},[62,13447,13448],{"class":79},"          \u003C",[62,13450,13451],{"class":578},"img\n",[62,13453,13454,13457,13459],{"class":64,"line":1459},[62,13455,13456],{"class":75},"            src",[62,13458,200],{"class":166},[62,13460,13461],{"class":79},"{photo.src}\n",[62,13463,13464,13467,13469,13472,13474,13476],{"class":64,"line":1470},[62,13465,13466],{"class":75},"            alt",[62,13468,200],{"class":166},[62,13470,13471],{"class":79},"{photo.alt ",[62,13473,393],{"class":166},[62,13475,5500],{"class":273},[62,13477,139],{"class":79},[62,13479,13480,13483,13485],{"class":64,"line":1475},[62,13481,13482],{"class":75},"            decoding",[62,13484,200],{"class":166},[62,13486,13487],{"class":273},"\"async\"\n",[62,13489,13490,13493,13495],{"class":64,"line":1484},[62,13491,13492],{"class":75},"            style",[62,13494,200],{"class":166},[62,13496,13497],{"class":79},"{{ width, height }}\n",[62,13499,13500],{"class":64,"line":1489},[62,13501,13502],{"class":79},"          \u002F>\n",[62,13504,13505,13508,13511],{"class":64,"line":1695},[62,13506,13507],{"class":79},"          {",[62,13509,13510],{"class":68},"\u002F* hover 提示词浮层 *\u002F",[62,13512,139],{"class":79},[62,13514,13515,13517,13520,13523,13525,13528],{"class":64,"line":1703},[62,13516,13448],{"class":79},[62,13518,13519],{"class":578},"div",[62,13521,13522],{"class":75}," className",[62,13524,200],{"class":166},[62,13526,13527],{"class":273},"\"... opacity-0 group-hover:opacity-100\"",[62,13529,585],{"class":79},[62,13531,13532],{"class":64,"line":1708},[62,13533,13534],{"class":79},"            {photo.prompt_text}\n",[62,13536,13537,13540,13542],{"class":64,"line":2336},[62,13538,13539],{"class":79},"          \u003C\u002F",[62,13541,13519],{"class":578},[62,13543,585],{"class":79},[62,13545,13546,13549,13551],{"class":64,"line":2350},[62,13547,13548],{"class":79},"        \u003C\u002F",[62,13550,13417],{"class":578},[62,13552,585],{"class":79},[62,13554,13555],{"class":64,"line":2357},[62,13556,13557],{"class":79},"      )\n",[62,13559,13560],{"class":64,"line":2362},[62,13561,7952],{"class":79},[62,13563,13564],{"class":64,"line":2368},[62,13565,13566],{"class":79},"  }}\n",[62,13568,13569],{"class":64,"line":2376},[62,13570,13571],{"class":79},"\u002F>\n",[15,13573,13574],{},"几个要点：",[2810,13576,13577,13586,13599],{},[516,13578,13579,13581,13582,13585],{},[33,13580,12876],{}," 可以是数字或 ",[33,13583,13584],{},"(containerWidth) => number"," 函数，做响应式列数很方便；",[516,13587,13588,13589,13591,13592,13595,13596,13598],{},"点击回调 ",[33,13590,13366],{}," 里拿到的是",[38,13593,13594],{},"原始数组下标"," ",[33,13597,13314],{},"，直接传给灯箱即可；",[516,13600,13601,13602,13605,13606,13608],{},"自定义 ",[33,13603,13604],{},"render.photo"," 后，图片尺寸要自己用 context 里的 ",[33,13607,12924],{},"（渲染尺寸）设置——这两个值库已经按列宽算好了。",[26,13610,13611],{"id":13611},"遇到的坑",[1503,13613,13615],{"id":13614},"坑-1忘记引入样式表-一张一列","坑 1：忘记引入样式表 → 「一张一列」",[15,13617,13618,13619,13622,13623,41],{},"这是最坑的一个。装完包、写好 ",[33,13620,13621],{},"\u003CPhotoAlbum layout=\"masonry\">","，页面渲染出来却是：",[38,13624,13625],{},"每张图占一整行，右侧大片置灰空白",[15,13627,13628,13629,13631],{},"排查过程：先怀疑是 ",[33,13630,12876],{}," 函数没生效，改成固定数字无效；再检查 DOM——容器和 5 个列（track）都在，列的宽度也算对了，但图片全都堆在布局里。",[15,13633,13634,13635,13638,13639,7798,13642,13645,13646,13652,13653,13656],{},"最后翻包的 ",[33,13636,13637],{},"package.json"," 发现它导出了独立的 CSS 文件（",[33,13640,13641],{},".\u002Fstyles.css",[33,13643,13644],{},".\u002Fmasonry.css"," 等），而",[38,13647,13648,13649],{},"官方 README 示例都带了一行 ",[33,13650,13651],{},"import \"react-photo-album\u002Fstyles.css\"","。容器是 flex、列是 flex-column、列宽按 ",[33,13654,13655],{},"--react-photo-album--columns"," 计算——这些规则全在这个样式表里，不引入就退化成普通块级堆叠。",[15,13658,13659,13661],{},[38,13660,7793],{},"：顶部加一行：",[53,13663,13665],{"className":11427,"code":13664,"language":11429,"meta":58,"style":58},"import \"react-photo-album\u002Fstyles.css\"\n",[33,13666,13667],{"__ignoreMap":58},[62,13668,13669,13671],{"class":64,"line":65},[62,13670,4404],{"class":166},[62,13672,13027],{"class":273},[15,13674,13675,13676],{},"经验：",[38,13677,13678],{},"用这类\"无运行时依赖\"的布局库，先看它的 exports 里有没有独立 CSS，有就一定要引。",[1503,13680,13682,13683,13685,13686,13689],{"id":13681},"坑-2renderphoto-是-props-context-双参签名","坑 2：",[33,13684,13604],{}," 是 ",[33,13687,13688],{},"(props, context)"," 双参签名",[15,13691,13692],{},"自定义渲染时我凭直觉写了：",[53,13694,13696],{"className":11427,"code":13695,"language":11429,"meta":58,"style":58},"photo: ({ photo, index, width, height, onClick }) => ... \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[33,13697,13698],{"__ignoreMap":58},[62,13699,13700,13702,13705,13707,13709,13711,13713,13715,13717,13719,13721,13723,13725,13727,13730],{"class":64,"line":65},[62,13701,13383],{"class":75},[62,13703,13704],{"class":79},": ({ ",[62,13706,13383],{"class":176},[62,13708,186],{"class":79},[62,13710,13314],{"class":176},[62,13712,186],{"class":79},[62,13714,12988],{"class":176},[62,13716,186],{"class":79},[62,13718,12991],{"class":176},[62,13720,186],{"class":79},[62,13722,13366],{"class":176},[62,13724,13317],{"class":79},[62,13726,617],{"class":166},[62,13728,13729],{"class":166}," ...",[62,13731,13732],{"class":68}," \u002F\u002F ❌ 报错：Property 'photo' does not exist\n",[15,13734,13735,13736,13738,13739,13742,13743,4115],{},"TS 报错提示 ",[33,13737,13383],{}," 等属性不存在于 ",[33,13740,13741],{},"RenderPhotoProps","。看类型定义才发现 v3 的渲染函数是",[38,13744,13745],{},"两个参数",[53,13747,13749],{"className":11427,"code":13748,"language":11429,"meta":58,"style":58},"type RenderFunction\u003CProps, Context> = (props: Props, context: Context) => ReactNode\n\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\nphoto: (props, context) => {\n  const { onClick } = props\n  const { photo, index, width, height } = context\n  ...\n}\n",[33,13750,13751,13797,13802,13822,13836,13861,13865],{"__ignoreMap":58},[62,13752,13753,13755,13758,13760,13763,13765,13767,13770,13772,13774,13776,13778,13781,13783,13785,13787,13790,13792,13794],{"class":64,"line":65},[62,13754,5789],{"class":166},[62,13756,13757],{"class":75}," RenderFunction",[62,13759,575],{"class":79},[62,13761,13762],{"class":75},"Props",[62,13764,186],{"class":79},[62,13766,4610],{"class":75},[62,13768,13769],{"class":79},"> ",[62,13771,200],{"class":166},[62,13773,218],{"class":79},[62,13775,725],{"class":176},[62,13777,180],{"class":166},[62,13779,13780],{"class":75}," Props",[62,13782,186],{"class":79},[62,13784,13350],{"class":176},[62,13786,180],{"class":166},[62,13788,13789],{"class":75}," Context",[62,13791,654],{"class":79},[62,13793,617],{"class":166},[62,13795,13796],{"class":75}," ReactNode\n",[62,13798,13799],{"class":64,"line":72},[62,13800,13801],{"class":68},"\u002F\u002F photo 的渲染上下文（photo\u002Findex\u002Fwidth\u002Fheight）在第二个参数里\n",[62,13803,13804,13806,13808,13810,13812,13814,13816,13818,13820],{"class":64,"line":83},[62,13805,13383],{"class":75},[62,13807,180],{"class":166},[62,13809,218],{"class":79},[62,13811,725],{"class":176},[62,13813,186],{"class":79},[62,13815,13350],{"class":176},[62,13817,654],{"class":79},[62,13819,617],{"class":166},[62,13821,620],{"class":79},[62,13823,13824,13826,13828,13830,13833],{"class":64,"line":91},[62,13825,679],{"class":75},[62,13827,13363],{"class":79},[62,13829,13366],{"class":176},[62,13831,13832],{"class":79}," } = ",[62,13834,13835],{"class":176},"props\n",[62,13837,13838,13840,13842,13844,13846,13848,13850,13852,13854,13856,13858],{"class":64,"line":99},[62,13839,679],{"class":75},[62,13841,13363],{"class":79},[62,13843,13383],{"class":176},[62,13845,186],{"class":79},[62,13847,13314],{"class":176},[62,13849,186],{"class":79},[62,13851,12988],{"class":176},[62,13853,186],{"class":79},[62,13855,12991],{"class":176},[62,13857,13832],{"class":79},[62,13859,13860],{"class":176},"context\n",[62,13862,13863],{"class":64,"line":124},[62,13864,11420],{"class":166},[62,13866,13867],{"class":64,"line":130},[62,13868,139],{"class":79},[15,13870,13871,13873,13874,13876,13877,13880],{},[38,13872,7793],{},"：按 ",[33,13875,13688],{}," 拆解即可。装新库先扫一眼 ",[33,13878,13879],{},".d.ts"," 里的 Render 类型签名，能少走很多弯路。",[1503,13882,13884],{"id":13883},"坑-3测试断言把dom-顺序当成了视觉顺序","坑 3：测试断言把「DOM 顺序」当成了「视觉顺序」",[15,13886,13887,13888,13891],{},"布局修好后，我在浏览器自测脚本里加了一条断言：取前两个 ",[33,13889,13890],{},"\u003Cimg>","，比较它们的左坐标，认为应该并排（不同列）。",[15,13893,13894,13895,13898,13899,7832,13902,13905,13906,13909],{},"结果断言失败——但页面明明是 5 列并排。排查发现：",[38,13896,13897],{},"瀑布流的 DOM 是按\"列\"组织的","（第 1 列的全部图片在前，第 2 列的在后），所以 ",[33,13900,13901],{},"img[0]",[33,13903,13904],{},"img[1]"," 属于",[38,13907,13908],{},"同一列","，左坐标当然相同。",[15,13911,13912,13914],{},[38,13913,7793],{},"：断言改为比较「第 1 列的首张图 vs 第 2 列的首张图」：",[53,13916,13920],{"className":13917,"code":13918,"language":13919,"meta":58,"style":58},"language-js shiki shiki-themes github-light github-dark","const a = tracks[0].querySelector(\"img\")\nconst b = tracks[1].querySelector(\"img\")\nreturn Math.abs(a.getBoundingClientRect().left - b.getBoundingClientRect().left) > 10\n","js",[33,13921,13922,13947,13970],{"__ignoreMap":58},[62,13923,13924,13926,13928,13930,13933,13935,13937,13940,13942,13945],{"class":64,"line":65},[62,13925,415],{"class":166},[62,13927,4221],{"class":114},[62,13929,321],{"class":166},[62,13931,13932],{"class":79}," tracks[",[62,13934,264],{"class":114},[62,13936,331],{"class":79},[62,13938,13939],{"class":75},"querySelector",[62,13941,173],{"class":79},[62,13943,13944],{"class":273},"\"img\"",[62,13946,494],{"class":79},[62,13948,13949,13951,13954,13956,13958,13960,13962,13964,13966,13968],{"class":64,"line":72},[62,13950,415],{"class":166},[62,13952,13953],{"class":114}," b",[62,13955,321],{"class":166},[62,13957,13932],{"class":79},[62,13959,339],{"class":114},[62,13961,331],{"class":79},[62,13963,13939],{"class":75},[62,13965,173],{"class":79},[62,13967,13944],{"class":273},[62,13969,494],{"class":79},[62,13971,13972,13974,13977,13980,13983,13986,13989,13992,13995,13997,14000,14002],{"class":64,"line":83},[62,13973,227],{"class":166},[62,13975,13976],{"class":79}," Math.",[62,13978,13979],{"class":75},"abs",[62,13981,13982],{"class":79},"(a.",[62,13984,13985],{"class":75},"getBoundingClientRect",[62,13987,13988],{"class":79},"().left ",[62,13990,13991],{"class":166},"-",[62,13993,13994],{"class":79}," b.",[62,13996,13985],{"class":75},[62,13998,13999],{"class":79},"().left) ",[62,14001,1807],{"class":166},[62,14003,14004],{"class":114}," 10\n",[15,14006,13675,14007,14010],{},[38,14008,14009],{},"验证布局类库时，先搞清楚它生成的 DOM 结构，再写断言","；另外把「真实浏览器探针」当排查手段（直接打印各 track 的 x 坐标和子元素数量），比盯着代码猜快得多。",[1503,14012,14014],{"id":14013},"坑-4等宽网格的洞引入前就存在的产品问题","坑 4：等宽网格的\"洞\"（引入前就存在的产品问题）",[15,14016,14017,14018,14021,14022,14025],{},"顺带记录一下为什么换掉原来的布局：CSS Grid ",[33,14019,14020],{},"repeat(auto-fill, minmax(220px, 1fr))"," 等宽铺满，图片 ",[33,14023,14024],{},"w-full h-auto","。当一行里混有不同比例时，行高取最高的那张，矮图底部留空，几行错落下来就显得乱；另外窗口宽度\"差一点放不下下一张\"时，右侧会空出近一整列。",[15,14027,14028],{},"换成 Masonry 后这两个问题都消失了——列内紧密堆叠、列数自适应。",[26,14030,14031],{"id":14031},"效果与小结",[15,14033,14034,14035,14037],{},"最终效果：21 张不同比例的生成图在 5 列瀑布流里紧密排列，原比例不裁剪，悬停出提示词浮层，点击进 YARL 大图预览；缩略图总大小仅 360KB，进页面即全量加载 + ",[33,14036,12528],{}," 长缓存，平板上快速滚动也跟手。",[15,14039,11206,14040],{},[38,14041,14042],{},"react-photo-album 是个小但专业的相册布局库，只要记住三件事——引样式表、喂宽高、render 函数是双参——就能在十分钟内把图片墙升级成真正的瀑布流。",[1228,14044,14045],{},"html pre.shiki code .sw2iP, html code.shiki .sw2iP{--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .scXbn, html code.shiki .scXbn{--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .siTax, html code.shiki .siTax{--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sIX_F, html code.shiki .sIX_F{--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sHbNN, html code.shiki .sHbNN{--shiki-light:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sPK6S, html code.shiki .sPK6S{--shiki-light:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .suQ91, html code.shiki .suQ91{--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sbB4o, html code.shiki .sbB4o{--shiki-light:#22863A;--shiki-dark:#85E89D}",{"title":58,"searchDepth":72,"depth":83,"links":14047},[14048,14049,14050,14055,14062],{"id":12822,"depth":72,"text":12822},{"id":12928,"depth":72,"text":12928},{"id":12954,"depth":72,"text":12954,"children":14051},[14052,14053,14054],{"id":12957,"depth":83,"text":12958},{"id":12975,"depth":83,"text":12976},{"id":13173,"depth":83,"text":13174},{"id":13611,"depth":72,"text":13611,"children":14056},[14057,14058,14060,14061],{"id":13614,"depth":83,"text":13615},{"id":13681,"depth":83,"text":14059},"坑 2：render.photo 是 (props, context) 双参签名",{"id":13883,"depth":83,"text":13884},{"id":14013,"depth":83,"text":14014},{"id":14031,"depth":72,"text":14031},"记录在 Magic Img 生图图库中引入 react-photo-album 实现 Masonry 瀑布流的过程：技术选型背景、需求梳理、安装使用，以及「忘记引入样式表导致一张一列」等踩坑与排查方法。","\u002Fposts\u002Freact-photo-album-masonry\u002Fimg\u002Fcover.svg",{},"\u002Fposts\u002Freact-photo-album-masonry",{"title":12817,"description":14063},"前端工程实践","posts\u002Freact-photo-album-masonry\u002Findex",[10097,12905,14071,14072,14073],"瀑布流","前端","Tailwind","-O-kAQLEyRzwVhOpuo-yBxZim9LtvhChPkpP92ipyjM",1786887349178]