[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"knowledge-\u002Fapi\u002Fknowledge\u002Fteam-documents\u002F02":3,"knowledge-related-\u002Fknowledge\u002Fteam-documents\u002F02":1950,"knowledge-series-\u002Fknowledge\u002Fteam-documents\u002F02":1976},{"source":4,"connector":5,"page":6},"sqlite","better-sqlite3",{"path":7,"title":8,"description":9,"category":10,"tags":11,"date":17,"pinned":18,"draft":18,"body":19,"seo":1946,"stem":1947,"id":1948,"extension":1949},"\u002Fknowledge\u002Fteam-documents\u002F02","防篡改水印","在各类管理后台、SaaS 平台或内部系统中，页面水印已经是非常常见的能力：一方面在截图时携带账号、姓名、时间等信息，降低截图外传的风险；另一方面在上传图片或文档预览时，通过水印标记上传者与时间，满足审计和合规需求。","团队文档",[12,13,14,15,16],"team","vue","watermark","performance","geo","2024-01-09",false,{"type":20,"value":21,"toc":1935},"minimark",[22,25,28,36,41,44,61,68,71,83,87,92,97,114,117,143,147,484,494,498,508,519,562,565,779,785,796,799,806,810,819,1737,1740,1753,1767,1770,1849,1852,1932],[23,24,8],"h1",{"id":8},[26,27,9],"p",{},[26,29,30,31,35],{},"从实现难度看，水印本身并不复杂；但从安全视角看，如果前端水印可以被轻易删除或隐藏，其安全价值会大幅打折。本文结合浏览器原生 ",[32,33,34],"code",{},"MutationObserver"," 与 Vue 3 的响应式机制，介绍一种「被删除 \u002F 被改动后自动恢复」的防篡改水印实现思路。",[37,38,40],"h2",{"id":39},"水印的安全","水印的安全？",[26,42,43],{},"水印一般是为了保证数据的安全性。例如，有这样一个典型场景：",[45,46,47,51,58],"ul",{},[48,49,50],"li",{},"企业账户登录某平台系统，每个核心页面都需要叠加水印；",[48,52,53,54,57],{},"水印内容包含登录人姓名与时间，如：",[32,55,56],{},"张三 2023-05-27","；",[48,59,60],{},"当用户截图用于反馈问题或沟通时，水印可以帮助追溯截图来源，从而降低泄露风险。",[26,62,63,64,67],{},"但如果水印只是一个普通 DOM 元素，部分开发者可以在浏览器 DevTools 中定位到水印节点，直接删除或改成 ",[32,65,66],{},"display: none","，使得页面在本次会话中一直处于「无水印」状态——这会显著降低水印的安全性。",[26,69,70],{},"因此，「防篡改水印」的目标是：",[45,72,73,80],{},[48,74,75,76,57],{},"即便水印节点被删除 \u002F 被修改，也能",[77,78,79],"strong",{},"在极短时间内自动恢复",[48,81,82],{},"同时尽量避免为页面引入明显的性能与稳定性问题。",[37,84,86],{"id":85},"提高安全依赖-mutationobserver-的-dom-监听","提高安全：依赖 MutationObserver 的 DOM 监听",[88,89,91],"h3",{"id":90},"_1-mutationobserver-是什么","1. MutationObserver 是什么？",[26,93,94,96],{},[32,95,34],{}," 是浏览器原生提供的、用于监听 DOM 树变化的接口，是早期同步 Mutation Events 的替代方案。",[98,99,100],"blockquote",{},[26,101,102,103,105,106,113],{},"MDN：",[32,104,34],{}," 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品，该功能是 DOM3 Events 规范的一部分。参见 ",[107,108,112],"a",{"href":109,"rel":110},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FMutationObserver",[111],"nofollow","MutationObserver - Web APIs | MDN","。",[26,115,116],{},"在防篡改水印场景下，我们最关心：",[45,118,119,127,135],{},[48,120,121,126],{},[77,122,123],{},[32,124,125],{},"childList","：水印节点是否被删除 \u002F 替换；",[48,128,129,134],{},[77,130,131],{},[32,132,133],{},"attributes","：水印节点的样式、class 等是否被恶意修改；",[48,136,137,142],{},[77,138,139],{},[32,140,141],{},"subtree","：是否对子树做了批量变更（如整个水印容器被清空）。",[88,144,146],{"id":145},"_2-典型用法示例","2. 典型用法示例",[148,149,154],"pre",{"className":150,"code":151,"language":152,"meta":153,"style":153},"language-javascript shiki shiki-themes material-theme-lighter github-light github-dark","\u002F\u002F 创建观察器：records 是本轮收集到的所有 DOM 变更\nconst observer = new MutationObserver((records) => {\n  for (const record of records) {\n    if (record.type === \"childList\") {\n      console.log(\"子节点发生了增删\");\n    }\n    if (record.type === \"attributes\") {\n      console.log(\"属性被修改：\", record.attributeName);\n    }\n  }\n});\n\n\u002F\u002F 配置并开始观察\nobserver.observe(targetNode, {\n  attributes: true,\n  childList: true,\n  subtree: true,\n});\n\n\u002F\u002F 之后如有需要，可停止观察\nobserver.disconnect();\n","javascript","",[32,155,156,165,207,235,269,294,300,325,357,362,368,378,385,391,409,425,437,449,458,463,469],{"__ignoreMap":153},[157,158,161],"span",{"class":159,"line":160},"line",1,[157,162,164],{"class":163},"sutJx","\u002F\u002F 创建观察器：records 是本轮收集到的所有 DOM 变更\n",[157,166,168,172,176,180,183,187,191,194,198,201,204],{"class":159,"line":167},2,[157,169,171],{"class":170},"sbsja","const",[157,173,175],{"class":174},"s_hVV"," observer",[157,177,179],{"class":178},"smGrS"," =",[157,181,182],{"class":178}," new",[157,184,186],{"class":185},"sGLFI"," MutationObserver",[157,188,190],{"class":189},"su5hD","(",[157,192,190],{"class":193},"sP7_E",[157,195,197],{"class":196},"s99_P","records",[157,199,200],{"class":193},")",[157,202,203],{"class":170}," =>",[157,205,206],{"class":193}," {\n",[157,208,210,214,218,220,223,226,229,232],{"class":159,"line":209},3,[157,211,213],{"class":212},"sVHd0","  for",[157,215,217],{"class":216},"skxfh"," (",[157,219,171],{"class":170},[157,221,222],{"class":174}," record",[157,224,225],{"class":178}," of",[157,227,228],{"class":189}," records",[157,230,231],{"class":216},") ",[157,233,234],{"class":193},"{\n",[157,236,238,241,243,246,249,252,255,259,262,265,267],{"class":159,"line":237},4,[157,239,240],{"class":212},"    if",[157,242,217],{"class":216},[157,244,245],{"class":189},"record",[157,247,248],{"class":193},".",[157,250,251],{"class":189},"type",[157,253,254],{"class":178}," ===",[157,256,258],{"class":257},"sjJ54"," \"",[157,260,125],{"class":261},"s_sjI",[157,263,264],{"class":257},"\"",[157,266,231],{"class":216},[157,268,234],{"class":193},[157,270,272,275,277,280,282,284,287,289,291],{"class":159,"line":271},5,[157,273,274],{"class":189},"      console",[157,276,248],{"class":193},[157,278,279],{"class":185},"log",[157,281,190],{"class":216},[157,283,264],{"class":257},[157,285,286],{"class":261},"子节点发生了增删",[157,288,264],{"class":257},[157,290,200],{"class":216},[157,292,293],{"class":193},";\n",[157,295,297],{"class":159,"line":296},6,[157,298,299],{"class":193},"    }\n",[157,301,303,305,307,309,311,313,315,317,319,321,323],{"class":159,"line":302},7,[157,304,240],{"class":212},[157,306,217],{"class":216},[157,308,245],{"class":189},[157,310,248],{"class":193},[157,312,251],{"class":189},[157,314,254],{"class":178},[157,316,258],{"class":257},[157,318,133],{"class":261},[157,320,264],{"class":257},[157,322,231],{"class":216},[157,324,234],{"class":193},[157,326,328,330,332,334,336,338,341,343,346,348,350,353,355],{"class":159,"line":327},8,[157,329,274],{"class":189},[157,331,248],{"class":193},[157,333,279],{"class":185},[157,335,190],{"class":216},[157,337,264],{"class":257},[157,339,340],{"class":261},"属性被修改：",[157,342,264],{"class":257},[157,344,345],{"class":193},",",[157,347,222],{"class":189},[157,349,248],{"class":193},[157,351,352],{"class":189},"attributeName",[157,354,200],{"class":216},[157,356,293],{"class":193},[157,358,360],{"class":159,"line":359},9,[157,361,299],{"class":193},[157,363,365],{"class":159,"line":364},10,[157,366,367],{"class":193},"  }\n",[157,369,371,374,376],{"class":159,"line":370},11,[157,372,373],{"class":193},"}",[157,375,200],{"class":189},[157,377,293],{"class":193},[157,379,381],{"class":159,"line":380},12,[157,382,384],{"emptyLinePlaceholder":383},true,"\n",[157,386,388],{"class":159,"line":387},13,[157,389,390],{"class":163},"\u002F\u002F 配置并开始观察\n",[157,392,394,397,399,402,405,407],{"class":159,"line":393},14,[157,395,396],{"class":189},"observer",[157,398,248],{"class":193},[157,400,401],{"class":185},"observe",[157,403,404],{"class":189},"(targetNode",[157,406,345],{"class":193},[157,408,206],{"class":193},[157,410,412,415,418,422],{"class":159,"line":411},15,[157,413,414],{"class":216},"  attributes",[157,416,417],{"class":193},":",[157,419,421],{"class":420},"syTEX"," true",[157,423,424],{"class":193},",\n",[157,426,428,431,433,435],{"class":159,"line":427},16,[157,429,430],{"class":216},"  childList",[157,432,417],{"class":193},[157,434,421],{"class":420},[157,436,424],{"class":193},[157,438,440,443,445,447],{"class":159,"line":439},17,[157,441,442],{"class":216},"  subtree",[157,444,417],{"class":193},[157,446,421],{"class":420},[157,448,424],{"class":193},[157,450,452,454,456],{"class":159,"line":451},18,[157,453,373],{"class":193},[157,455,200],{"class":189},[157,457,293],{"class":193},[157,459,461],{"class":159,"line":460},19,[157,462,384],{"emptyLinePlaceholder":383},[157,464,466],{"class":159,"line":465},20,[157,467,468],{"class":163},"\u002F\u002F 之后如有需要，可停止观察\n",[157,470,472,474,476,479,482],{"class":159,"line":471},21,[157,473,396],{"class":189},[157,475,248],{"class":193},[157,477,478],{"class":185},"disconnect",[157,480,481],{"class":189},"()",[157,483,293],{"class":193},[26,485,486,487,489,490,493],{},"相较于早期同步事件，",[32,488,34],{}," 是",[77,491,492],{},"异步批量触发","的：浏览器会先收集一批 DOM 变更，再统一调用回调，从而减少性能压力。",[37,495,497],{"id":496},"vue-3-中的依赖监听watch-与-watcheffect","Vue 3 中的依赖监听：watch 与 watchEffect",[26,499,500,501,504,505,507],{},"为了让「被篡改 → 触发重建」这条链路在代码层更清晰，我们通常会引入一个简单的「触发标记」：例如计数器 ",[32,502,503],{},"uId","，每当检测到水印被动手脚，就让它自增一次。然后使用 Vue 3 的侦听能力，在 ",[32,506,503],{}," 变化时重新生成水印。",[26,509,510,511,514,515,518],{},"在 Vue 3 中，",[32,512,513],{},"watch"," 和 ",[32,516,517],{},"watchEffect"," 是最常用的两个侦听 API：",[45,520,521,544],{},[48,522,523,528,529],{},[77,524,525],{},[32,526,527],{},"watch(source, callback, options)","：\n",[45,530,531,534,541],{},[48,532,533],{},"需要显式指定监听对象；",[48,535,536,537,540],{},"默认首次不执行，可通过 ",[32,538,539],{},"immediate: true"," 控制；",[48,542,543],{},"能获取新旧值，适合需要做差异比较的场景。",[48,545,546,528,551],{},[77,547,548],{},[32,549,550],{},"watchEffect(effect)",[45,552,553,556,559],{},[48,554,555],{},"不需要显式 source，会自动收集 effect 中读取到的响应式依赖；",[48,557,558],{},"首次一定会执行一次，之后在依赖变化时再次执行；",[48,560,561],{},"适合「依赖变了就整体重算」的场景。",[26,563,564],{},"简单示例：",[148,566,568],{"className":150,"code":567,"language":152,"meta":153,"style":153},"const count = ref(0);\n\n\u002F\u002F 模拟自增\nconst addCount = () => count.value++;\n\nwatch(\n  () => count.value,\n  (val, oldVal) => {\n    \u002F\u002F val: 最新值；oldVal: 上一次的值\n  },\n  {\n    immediate: true,\n    deep: true,\n  }\n);\n\nwatchEffect(() => {\n  \u002F\u002F 访问 count 时会被自动收集为依赖\n  console.log(\"当前 count:\", count.value);\n});\n",[32,569,570,592,596,601,628,632,639,654,673,678,683,688,699,710,714,720,724,736,741,771],{"__ignoreMap":153},[157,571,572,574,577,579,582,584,588,590],{"class":159,"line":160},[157,573,171],{"class":170},[157,575,576],{"class":174}," count",[157,578,179],{"class":178},[157,580,581],{"class":185}," ref",[157,583,190],{"class":189},[157,585,587],{"class":586},"srdBf","0",[157,589,200],{"class":189},[157,591,293],{"class":193},[157,593,594],{"class":159,"line":167},[157,595,384],{"emptyLinePlaceholder":383},[157,597,598],{"class":159,"line":209},[157,599,600],{"class":163},"\u002F\u002F 模拟自增\n",[157,602,603,605,609,611,614,616,618,620,623,626],{"class":159,"line":237},[157,604,171],{"class":170},[157,606,608],{"class":607},"sfCm-"," addCount",[157,610,179],{"class":178},[157,612,613],{"class":193}," ()",[157,615,203],{"class":170},[157,617,576],{"class":189},[157,619,248],{"class":193},[157,621,622],{"class":189},"value",[157,624,625],{"class":178},"++",[157,627,293],{"class":193},[157,629,630],{"class":159,"line":271},[157,631,384],{"emptyLinePlaceholder":383},[157,633,634,636],{"class":159,"line":296},[157,635,513],{"class":185},[157,637,638],{"class":189},"(\n",[157,640,641,644,646,648,650,652],{"class":159,"line":302},[157,642,643],{"class":193},"  ()",[157,645,203],{"class":170},[157,647,576],{"class":189},[157,649,248],{"class":193},[157,651,622],{"class":189},[157,653,424],{"class":193},[157,655,656,659,662,664,667,669,671],{"class":159,"line":327},[157,657,658],{"class":193},"  (",[157,660,661],{"class":196},"val",[157,663,345],{"class":193},[157,665,666],{"class":196}," oldVal",[157,668,200],{"class":193},[157,670,203],{"class":170},[157,672,206],{"class":193},[157,674,675],{"class":159,"line":359},[157,676,677],{"class":163},"    \u002F\u002F val: 最新值；oldVal: 上一次的值\n",[157,679,680],{"class":159,"line":364},[157,681,682],{"class":193},"  },\n",[157,684,685],{"class":159,"line":370},[157,686,687],{"class":193},"  {\n",[157,689,690,693,695,697],{"class":159,"line":380},[157,691,692],{"class":216},"    immediate",[157,694,417],{"class":193},[157,696,421],{"class":420},[157,698,424],{"class":193},[157,700,701,704,706,708],{"class":159,"line":387},[157,702,703],{"class":216},"    deep",[157,705,417],{"class":193},[157,707,421],{"class":420},[157,709,424],{"class":193},[157,711,712],{"class":159,"line":393},[157,713,367],{"class":193},[157,715,716,718],{"class":159,"line":411},[157,717,200],{"class":189},[157,719,293],{"class":193},[157,721,722],{"class":159,"line":427},[157,723,384],{"emptyLinePlaceholder":383},[157,725,726,728,730,732,734],{"class":159,"line":439},[157,727,517],{"class":185},[157,729,190],{"class":189},[157,731,481],{"class":193},[157,733,203],{"class":170},[157,735,206],{"class":193},[157,737,738],{"class":159,"line":451},[157,739,740],{"class":163},"  \u002F\u002F 访问 count 时会被自动收集为依赖\n",[157,742,743,746,748,750,752,754,757,759,761,763,765,767,769],{"class":159,"line":460},[157,744,745],{"class":189},"  console",[157,747,248],{"class":193},[157,749,279],{"class":185},[157,751,190],{"class":216},[157,753,264],{"class":257},[157,755,756],{"class":261},"当前 count:",[157,758,264],{"class":257},[157,760,345],{"class":193},[157,762,576],{"class":189},[157,764,248],{"class":193},[157,766,622],{"class":189},[157,768,200],{"class":216},[157,770,293],{"class":193},[157,772,773,775,777],{"class":159,"line":465},[157,774,373],{"class":193},[157,776,200],{"class":189},[157,778,293],{"class":193},[26,780,781,782,784],{},"在水印组件中，我们可以借助 ",[32,783,517],{}," 来：",[45,786,787,790],{},[48,788,789],{},"把「水印配置」（文字、颜色、透明度等）和「重建水印 DOM」的逻辑绑在一起；",[48,791,792,793,795],{},"只要依赖变化（包括 ",[32,794,503],{}," 自增），就重新创建水印节点。",[26,797,798],{},"下图是整体逻辑示意：",[26,800,801],{},[802,803],"img",{"alt":804,"src":805},"水印逻辑","https:\u002F\u002Fcdn.wangxiaoze.cn\u002Fstatic\u002Fimages\u002Fe118fa47-7ef2-4c0c-b8fb-7c13abd89a5f.png",[37,807,809],{"id":808},"综合实现防篡改水印核心代码","综合实现：防篡改水印核心代码",[26,811,812,813,815,816,818],{},"下面是一段基于 Vue 3 的核心实现逻辑，演示如何结合 ",[32,814,517],{}," 与 ",[32,817,34],{}," 来实现防篡改水印：",[148,820,822],{"className":150,"code":821,"language":152,"meta":153,"style":153},"\u002F\u002F parentRef：水印父容器（一般为页面主容器或某个 section）\nconst parentRef = ref();\n\n\u002F\u002F uId：触发重建水印的计数器\nconst uId = ref(0);\n\n\u002F\u002F useWater：内部基于 canvas 生成水印图案，返回 { base, styleSize }\nconst bg = useWater(props);\nconst { base, styleSize } = bg.value;\n\nlet div; \u002F\u002F 当前水印节点\n\n\u002F\u002F 依赖驱动的水印重建\nwatchEffect(() => {\n  \u002F\u002F 访问 uId，建立依赖关系\n  uId.value;\n  if (!parentRef.value) return;\n\n  \u002F\u002F 移除旧水印\n  if (div) {\n    div.remove();\n  }\n\n  \u002F\u002F 创建新水印容器\n  div = document.createElement(\"div\");\n  div.style.backgroundImage = `url(${base})`;\n  div.style.backgroundSize = `${styleSize}px ${styleSize}px`;\n  div.style.backgroundRepeat = \"repeat\";\n  div.style.width = \"100%\";\n  div.style.height = \"100%\";\n  div.style.position = \"absolute\";\n  div.style.inset = \"0\";\n\n  parentRef.value.appendChild(div);\n});\n\n\u002F\u002F 初始化观察器\nlet ob;\n\nonMounted(() => {\n  ob = new MutationObserver((records) => {\n    for (const record of records) {\n      \u002F\u002F 1. 节点被删除\n      for (const dom of record.removedNodes) {\n        if (dom === div) {\n          uId.value++;\n          return;\n        }\n      }\n\n      \u002F\u002F 2. 节点属性被修改（例如被设置 display:none）\n      if (record.target === div) {\n        uId.value++;\n        return;\n      }\n    }\n  });\n\n  ob.observe(parentRef.value, {\n    childList: true, \u002F\u002F 监听子节点新增\u002F删除\n    attributes: true, \u002F\u002F 监听属性变更\n    subtree: true, \u002F\u002F 监听子树\n  });\n});\n\n\u002F\u002F 组件卸载时清理，防止内存泄漏\nonUnmounted(() => {\n  if (ob) {\n    ob.disconnect();\n  }\n  div = null;\n});\n",[32,823,824,829,844,848,853,872,876,881,898,926,930,944,948,953,965,970,981,1005,1009,1014,1027,1041,1046,1051,1057,1085,1123,1163,1188,1213,1237,1262,1286,1291,1314,1323,1328,1334,1344,1349,1363,1387,1407,1413,1439,1458,1472,1480,1486,1492,1497,1503,1526,1540,1548,1553,1558,1568,1573,1594,1609,1624,1639,1648,1657,1662,1668,1682,1696,1710,1715,1728],{"__ignoreMap":153},[157,825,826],{"class":159,"line":160},[157,827,828],{"class":163},"\u002F\u002F parentRef：水印父容器（一般为页面主容器或某个 section）\n",[157,830,831,833,836,838,840,842],{"class":159,"line":167},[157,832,171],{"class":170},[157,834,835],{"class":174}," parentRef",[157,837,179],{"class":178},[157,839,581],{"class":185},[157,841,481],{"class":189},[157,843,293],{"class":193},[157,845,846],{"class":159,"line":209},[157,847,384],{"emptyLinePlaceholder":383},[157,849,850],{"class":159,"line":237},[157,851,852],{"class":163},"\u002F\u002F uId：触发重建水印的计数器\n",[157,854,855,857,860,862,864,866,868,870],{"class":159,"line":271},[157,856,171],{"class":170},[157,858,859],{"class":174}," uId",[157,861,179],{"class":178},[157,863,581],{"class":185},[157,865,190],{"class":189},[157,867,587],{"class":586},[157,869,200],{"class":189},[157,871,293],{"class":193},[157,873,874],{"class":159,"line":296},[157,875,384],{"emptyLinePlaceholder":383},[157,877,878],{"class":159,"line":302},[157,879,880],{"class":163},"\u002F\u002F useWater：内部基于 canvas 生成水印图案，返回 { base, styleSize }\n",[157,882,883,885,888,890,893,896],{"class":159,"line":327},[157,884,171],{"class":170},[157,886,887],{"class":174}," bg",[157,889,179],{"class":178},[157,891,892],{"class":185}," useWater",[157,894,895],{"class":189},"(props)",[157,897,293],{"class":193},[157,899,900,902,905,908,910,913,916,918,920,922,924],{"class":159,"line":359},[157,901,171],{"class":170},[157,903,904],{"class":193}," {",[157,906,907],{"class":174}," base",[157,909,345],{"class":193},[157,911,912],{"class":174}," styleSize",[157,914,915],{"class":193}," }",[157,917,179],{"class":178},[157,919,887],{"class":189},[157,921,248],{"class":193},[157,923,622],{"class":189},[157,925,293],{"class":193},[157,927,928],{"class":159,"line":364},[157,929,384],{"emptyLinePlaceholder":383},[157,931,932,935,938,941],{"class":159,"line":370},[157,933,934],{"class":170},"let",[157,936,937],{"class":189}," div",[157,939,940],{"class":193},";",[157,942,943],{"class":163}," \u002F\u002F 当前水印节点\n",[157,945,946],{"class":159,"line":380},[157,947,384],{"emptyLinePlaceholder":383},[157,949,950],{"class":159,"line":387},[157,951,952],{"class":163},"\u002F\u002F 依赖驱动的水印重建\n",[157,954,955,957,959,961,963],{"class":159,"line":393},[157,956,517],{"class":185},[157,958,190],{"class":189},[157,960,481],{"class":193},[157,962,203],{"class":170},[157,964,206],{"class":193},[157,966,967],{"class":159,"line":411},[157,968,969],{"class":163},"  \u002F\u002F 访问 uId，建立依赖关系\n",[157,971,972,975,977,979],{"class":159,"line":427},[157,973,974],{"class":189},"  uId",[157,976,248],{"class":193},[157,978,622],{"class":189},[157,980,293],{"class":193},[157,982,983,986,988,991,994,996,998,1000,1003],{"class":159,"line":439},[157,984,985],{"class":212},"  if",[157,987,217],{"class":216},[157,989,990],{"class":178},"!",[157,992,993],{"class":189},"parentRef",[157,995,248],{"class":193},[157,997,622],{"class":189},[157,999,231],{"class":216},[157,1001,1002],{"class":212},"return",[157,1004,293],{"class":193},[157,1006,1007],{"class":159,"line":451},[157,1008,384],{"emptyLinePlaceholder":383},[157,1010,1011],{"class":159,"line":460},[157,1012,1013],{"class":163},"  \u002F\u002F 移除旧水印\n",[157,1015,1016,1018,1020,1023,1025],{"class":159,"line":465},[157,1017,985],{"class":212},[157,1019,217],{"class":216},[157,1021,1022],{"class":189},"div",[157,1024,231],{"class":216},[157,1026,234],{"class":193},[157,1028,1029,1032,1034,1037,1039],{"class":159,"line":471},[157,1030,1031],{"class":189},"    div",[157,1033,248],{"class":193},[157,1035,1036],{"class":185},"remove",[157,1038,481],{"class":216},[157,1040,293],{"class":193},[157,1042,1044],{"class":159,"line":1043},22,[157,1045,367],{"class":193},[157,1047,1049],{"class":159,"line":1048},23,[157,1050,384],{"emptyLinePlaceholder":383},[157,1052,1054],{"class":159,"line":1053},24,[157,1055,1056],{"class":163},"  \u002F\u002F 创建新水印容器\n",[157,1058,1060,1063,1065,1068,1070,1073,1075,1077,1079,1081,1083],{"class":159,"line":1059},25,[157,1061,1062],{"class":189},"  div",[157,1064,179],{"class":178},[157,1066,1067],{"class":189}," document",[157,1069,248],{"class":193},[157,1071,1072],{"class":185},"createElement",[157,1074,190],{"class":216},[157,1076,264],{"class":257},[157,1078,1022],{"class":261},[157,1080,264],{"class":257},[157,1082,200],{"class":216},[157,1084,293],{"class":193},[157,1086,1088,1090,1092,1095,1097,1100,1102,1105,1108,1111,1114,1116,1118,1121],{"class":159,"line":1087},26,[157,1089,1062],{"class":189},[157,1091,248],{"class":193},[157,1093,1094],{"class":189},"style",[157,1096,248],{"class":193},[157,1098,1099],{"class":189},"backgroundImage",[157,1101,179],{"class":178},[157,1103,1104],{"class":257}," `",[157,1106,1107],{"class":261},"url(",[157,1109,1110],{"class":257},"${",[157,1112,1113],{"class":189},"base",[157,1115,373],{"class":257},[157,1117,200],{"class":261},[157,1119,1120],{"class":257},"`",[157,1122,293],{"class":193},[157,1124,1126,1128,1130,1132,1134,1137,1139,1142,1145,1147,1150,1152,1154,1156,1159,1161],{"class":159,"line":1125},27,[157,1127,1062],{"class":189},[157,1129,248],{"class":193},[157,1131,1094],{"class":189},[157,1133,248],{"class":193},[157,1135,1136],{"class":189},"backgroundSize",[157,1138,179],{"class":178},[157,1140,1141],{"class":257}," `${",[157,1143,1144],{"class":189},"styleSize",[157,1146,373],{"class":257},[157,1148,1149],{"class":261},"px ",[157,1151,1110],{"class":257},[157,1153,1144],{"class":189},[157,1155,373],{"class":257},[157,1157,1158],{"class":261},"px",[157,1160,1120],{"class":257},[157,1162,293],{"class":193},[157,1164,1166,1168,1170,1172,1174,1177,1179,1181,1184,1186],{"class":159,"line":1165},28,[157,1167,1062],{"class":189},[157,1169,248],{"class":193},[157,1171,1094],{"class":189},[157,1173,248],{"class":193},[157,1175,1176],{"class":189},"backgroundRepeat",[157,1178,179],{"class":178},[157,1180,258],{"class":257},[157,1182,1183],{"class":261},"repeat",[157,1185,264],{"class":257},[157,1187,293],{"class":193},[157,1189,1191,1193,1195,1197,1199,1202,1204,1206,1209,1211],{"class":159,"line":1190},29,[157,1192,1062],{"class":189},[157,1194,248],{"class":193},[157,1196,1094],{"class":189},[157,1198,248],{"class":193},[157,1200,1201],{"class":189},"width",[157,1203,179],{"class":178},[157,1205,258],{"class":257},[157,1207,1208],{"class":261},"100%",[157,1210,264],{"class":257},[157,1212,293],{"class":193},[157,1214,1216,1218,1220,1222,1224,1227,1229,1231,1233,1235],{"class":159,"line":1215},30,[157,1217,1062],{"class":189},[157,1219,248],{"class":193},[157,1221,1094],{"class":189},[157,1223,248],{"class":193},[157,1225,1226],{"class":189},"height",[157,1228,179],{"class":178},[157,1230,258],{"class":257},[157,1232,1208],{"class":261},[157,1234,264],{"class":257},[157,1236,293],{"class":193},[157,1238,1240,1242,1244,1246,1248,1251,1253,1255,1258,1260],{"class":159,"line":1239},31,[157,1241,1062],{"class":189},[157,1243,248],{"class":193},[157,1245,1094],{"class":189},[157,1247,248],{"class":193},[157,1249,1250],{"class":189},"position",[157,1252,179],{"class":178},[157,1254,258],{"class":257},[157,1256,1257],{"class":261},"absolute",[157,1259,264],{"class":257},[157,1261,293],{"class":193},[157,1263,1265,1267,1269,1271,1273,1276,1278,1280,1282,1284],{"class":159,"line":1264},32,[157,1266,1062],{"class":189},[157,1268,248],{"class":193},[157,1270,1094],{"class":189},[157,1272,248],{"class":193},[157,1274,1275],{"class":189},"inset",[157,1277,179],{"class":178},[157,1279,258],{"class":257},[157,1281,587],{"class":261},[157,1283,264],{"class":257},[157,1285,293],{"class":193},[157,1287,1289],{"class":159,"line":1288},33,[157,1290,384],{"emptyLinePlaceholder":383},[157,1292,1294,1297,1299,1301,1303,1306,1308,1310,1312],{"class":159,"line":1293},34,[157,1295,1296],{"class":189},"  parentRef",[157,1298,248],{"class":193},[157,1300,622],{"class":189},[157,1302,248],{"class":193},[157,1304,1305],{"class":185},"appendChild",[157,1307,190],{"class":216},[157,1309,1022],{"class":189},[157,1311,200],{"class":216},[157,1313,293],{"class":193},[157,1315,1317,1319,1321],{"class":159,"line":1316},35,[157,1318,373],{"class":193},[157,1320,200],{"class":189},[157,1322,293],{"class":193},[157,1324,1326],{"class":159,"line":1325},36,[157,1327,384],{"emptyLinePlaceholder":383},[157,1329,1331],{"class":159,"line":1330},37,[157,1332,1333],{"class":163},"\u002F\u002F 初始化观察器\n",[157,1335,1337,1339,1342],{"class":159,"line":1336},38,[157,1338,934],{"class":170},[157,1340,1341],{"class":189}," ob",[157,1343,293],{"class":193},[157,1345,1347],{"class":159,"line":1346},39,[157,1348,384],{"emptyLinePlaceholder":383},[157,1350,1352,1355,1357,1359,1361],{"class":159,"line":1351},40,[157,1353,1354],{"class":185},"onMounted",[157,1356,190],{"class":189},[157,1358,481],{"class":193},[157,1360,203],{"class":170},[157,1362,206],{"class":193},[157,1364,1366,1369,1371,1373,1375,1377,1379,1381,1383,1385],{"class":159,"line":1365},41,[157,1367,1368],{"class":189},"  ob",[157,1370,179],{"class":178},[157,1372,182],{"class":178},[157,1374,186],{"class":185},[157,1376,190],{"class":216},[157,1378,190],{"class":193},[157,1380,197],{"class":196},[157,1382,200],{"class":193},[157,1384,203],{"class":170},[157,1386,206],{"class":193},[157,1388,1390,1393,1395,1397,1399,1401,1403,1405],{"class":159,"line":1389},42,[157,1391,1392],{"class":212},"    for",[157,1394,217],{"class":216},[157,1396,171],{"class":170},[157,1398,222],{"class":174},[157,1400,225],{"class":178},[157,1402,228],{"class":189},[157,1404,231],{"class":216},[157,1406,234],{"class":193},[157,1408,1410],{"class":159,"line":1409},43,[157,1411,1412],{"class":163},"      \u002F\u002F 1. 节点被删除\n",[157,1414,1416,1419,1421,1423,1426,1428,1430,1432,1435,1437],{"class":159,"line":1415},44,[157,1417,1418],{"class":212},"      for",[157,1420,217],{"class":216},[157,1422,171],{"class":170},[157,1424,1425],{"class":174}," dom",[157,1427,225],{"class":178},[157,1429,222],{"class":189},[157,1431,248],{"class":193},[157,1433,1434],{"class":189},"removedNodes",[157,1436,231],{"class":216},[157,1438,234],{"class":193},[157,1440,1442,1445,1447,1450,1452,1454,1456],{"class":159,"line":1441},45,[157,1443,1444],{"class":212},"        if",[157,1446,217],{"class":216},[157,1448,1449],{"class":189},"dom",[157,1451,254],{"class":178},[157,1453,937],{"class":189},[157,1455,231],{"class":216},[157,1457,234],{"class":193},[157,1459,1461,1464,1466,1468,1470],{"class":159,"line":1460},46,[157,1462,1463],{"class":189},"          uId",[157,1465,248],{"class":193},[157,1467,622],{"class":189},[157,1469,625],{"class":178},[157,1471,293],{"class":193},[157,1473,1475,1478],{"class":159,"line":1474},47,[157,1476,1477],{"class":212},"          return",[157,1479,293],{"class":193},[157,1481,1483],{"class":159,"line":1482},48,[157,1484,1485],{"class":193},"        }\n",[157,1487,1489],{"class":159,"line":1488},49,[157,1490,1491],{"class":193},"      }\n",[157,1493,1495],{"class":159,"line":1494},50,[157,1496,384],{"emptyLinePlaceholder":383},[157,1498,1500],{"class":159,"line":1499},51,[157,1501,1502],{"class":163},"      \u002F\u002F 2. 节点属性被修改（例如被设置 display:none）\n",[157,1504,1506,1509,1511,1513,1515,1518,1520,1522,1524],{"class":159,"line":1505},52,[157,1507,1508],{"class":212},"      if",[157,1510,217],{"class":216},[157,1512,245],{"class":189},[157,1514,248],{"class":193},[157,1516,1517],{"class":189},"target",[157,1519,254],{"class":178},[157,1521,937],{"class":189},[157,1523,231],{"class":216},[157,1525,234],{"class":193},[157,1527,1529,1532,1534,1536,1538],{"class":159,"line":1528},53,[157,1530,1531],{"class":189},"        uId",[157,1533,248],{"class":193},[157,1535,622],{"class":189},[157,1537,625],{"class":178},[157,1539,293],{"class":193},[157,1541,1543,1546],{"class":159,"line":1542},54,[157,1544,1545],{"class":212},"        return",[157,1547,293],{"class":193},[157,1549,1551],{"class":159,"line":1550},55,[157,1552,1491],{"class":193},[157,1554,1556],{"class":159,"line":1555},56,[157,1557,299],{"class":193},[157,1559,1561,1564,1566],{"class":159,"line":1560},57,[157,1562,1563],{"class":193},"  }",[157,1565,200],{"class":216},[157,1567,293],{"class":193},[157,1569,1571],{"class":159,"line":1570},58,[157,1572,384],{"emptyLinePlaceholder":383},[157,1574,1576,1578,1580,1582,1584,1586,1588,1590,1592],{"class":159,"line":1575},59,[157,1577,1368],{"class":189},[157,1579,248],{"class":193},[157,1581,401],{"class":185},[157,1583,190],{"class":216},[157,1585,993],{"class":189},[157,1587,248],{"class":193},[157,1589,622],{"class":189},[157,1591,345],{"class":193},[157,1593,206],{"class":193},[157,1595,1597,1600,1602,1604,1606],{"class":159,"line":1596},60,[157,1598,1599],{"class":216},"    childList",[157,1601,417],{"class":193},[157,1603,421],{"class":420},[157,1605,345],{"class":193},[157,1607,1608],{"class":163}," \u002F\u002F 监听子节点新增\u002F删除\n",[157,1610,1612,1615,1617,1619,1621],{"class":159,"line":1611},61,[157,1613,1614],{"class":216},"    attributes",[157,1616,417],{"class":193},[157,1618,421],{"class":420},[157,1620,345],{"class":193},[157,1622,1623],{"class":163}," \u002F\u002F 监听属性变更\n",[157,1625,1627,1630,1632,1634,1636],{"class":159,"line":1626},62,[157,1628,1629],{"class":216},"    subtree",[157,1631,417],{"class":193},[157,1633,421],{"class":420},[157,1635,345],{"class":193},[157,1637,1638],{"class":163}," \u002F\u002F 监听子树\n",[157,1640,1642,1644,1646],{"class":159,"line":1641},63,[157,1643,1563],{"class":193},[157,1645,200],{"class":216},[157,1647,293],{"class":193},[157,1649,1651,1653,1655],{"class":159,"line":1650},64,[157,1652,373],{"class":193},[157,1654,200],{"class":189},[157,1656,293],{"class":193},[157,1658,1660],{"class":159,"line":1659},65,[157,1661,384],{"emptyLinePlaceholder":383},[157,1663,1665],{"class":159,"line":1664},66,[157,1666,1667],{"class":163},"\u002F\u002F 组件卸载时清理，防止内存泄漏\n",[157,1669,1671,1674,1676,1678,1680],{"class":159,"line":1670},67,[157,1672,1673],{"class":185},"onUnmounted",[157,1675,190],{"class":189},[157,1677,481],{"class":193},[157,1679,203],{"class":170},[157,1681,206],{"class":193},[157,1683,1685,1687,1689,1692,1694],{"class":159,"line":1684},68,[157,1686,985],{"class":212},[157,1688,217],{"class":216},[157,1690,1691],{"class":189},"ob",[157,1693,231],{"class":216},[157,1695,234],{"class":193},[157,1697,1699,1702,1704,1706,1708],{"class":159,"line":1698},69,[157,1700,1701],{"class":189},"    ob",[157,1703,248],{"class":193},[157,1705,478],{"class":185},[157,1707,481],{"class":216},[157,1709,293],{"class":193},[157,1711,1713],{"class":159,"line":1712},70,[157,1714,367],{"class":193},[157,1716,1718,1720,1722,1726],{"class":159,"line":1717},71,[157,1719,1062],{"class":189},[157,1721,179],{"class":178},[157,1723,1725],{"class":1724},"s39Yj"," null",[157,1727,293],{"class":193},[157,1729,1731,1733,1735],{"class":159,"line":1730},72,[157,1732,373],{"class":193},[157,1734,200],{"class":189},[157,1736,293],{"class":193},[26,1738,1739],{},"可以总结为：",[98,1741,1742],{},[26,1743,1744],{},[77,1745,1746,1747,1749,1750,1752],{},"MutationObserver 负责发现「水印被篡改」，",[32,1748,503],{}," 负责发出「需要重建」的信号，",[32,1751,517],{}," 则负责真正重建水印。",[26,1754,1755,1758,1759,1762,1763,1766],{},[32,1756,1757],{},"useWater"," 的内部实现通常基于 ",[32,1760,1761],{},"\u003Ccanvas>","：绘制水印文字、角度、透明度等，再通过 ",[32,1764,1765],{},"canvas.toDataURL"," 生成 base64 图片，本文不展开，可按项目实际需求设计。",[37,1768,1769],{"id":1769},"设计细节与注意事项",[45,1771,1772,1800,1819,1836],{},[48,1773,1774,528,1777],{},[77,1775,1776],{},"监听范围要适度",[45,1778,1779,1786],{},[48,1780,1781,1782,1785],{},"对父容器使用 ",[32,1783,1784],{},"subtree: true"," 虽然方便，但也会带来更多变更记录；",[48,1787,1788,1789,1792,1793,1795,1796,1799],{},"可以结合 ",[32,1790,1791],{},"attributeFilter"," 仅监听如 ",[32,1794,1094],{},"、",[32,1797,1798],{},"class"," 等必要属性，减少无关噪音。",[48,1801,1802,528,1805],{},[77,1803,1804],{},"避免死循环",[45,1806,1807,1810],{},[48,1808,1809],{},"重建水印本身也是一次 DOM 变更；",[48,1811,1812,1813,1815,1816,1818],{},"回调中只在「确认是水印节点相关变更」时才修改 ",[32,1814,503],{},"，并尽量在检测到后立即 ",[32,1817,1002],{},"，避免同一批 records 内多次自增导致频繁重建。",[48,1820,1821,528,1824],{},[77,1822,1823],{},"性能权衡",[45,1825,1826,1833],{},[48,1827,1828,1829,1832],{},"对于大规模复杂 DOM 树，应避免把观察范围设置在 ",[32,1830,1831],{},"document.body"," 级别；",[48,1834,1835],{},"更推荐为特定业务容器创建局部水印和局部监控。",[48,1837,1838,528,1841],{},[77,1839,1840],{},"安全边界认知",[45,1842,1843,1846],{},[48,1844,1845],{},"这种前端防篡改方案可以有效阻挡「随手删 DOM」这类轻度攻击；",[48,1847,1848],{},"但对于刻意对抗（例如关闭 JS、注入脚本、修改浏览器环境等），仍然需要配合后端审计、权限控制、操作日志等手段综合防护。",[37,1850,1851],{"id":1851},"参考文献",[1853,1854,1855,1868],"table",{},[1856,1857,1858],"thead",{},[1859,1860,1861,1865],"tr",{},[1862,1863,1864],"th",{},"资料",[1862,1866,1867],{},"链接",[1869,1870,1871,1882,1893,1910,1921],"tbody",{},[1859,1872,1873,1877],{},[1874,1875,1876],"td",{},"MDN：MutationObserver（英文）",[1874,1878,1879],{},[107,1880,109],{"href":109,"rel":1881},[111],[1859,1883,1884,1887],{},[1874,1885,1886],{},"MDN：MutationObserver（中文）",[1874,1888,1889],{},[107,1890,1891],{"href":1891,"rel":1892},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FWeb\u002FAPI\u002FMutationObserver",[111],[1859,1894,1895,1904],{},[1874,1896,1897,1898,1900,1901,1903],{},"Vue 3 官方文档：侦听器（",[32,1899,513],{}," \u002F ",[32,1902,517],{},"）",[1874,1905,1906],{},[107,1907,1908],{"href":1908,"rel":1909},"https:\u002F\u002Fcn.vuejs.org\u002Fapi\u002Freactivity-core.html#watch",[111],[1859,1911,1912,1915],{},[1874,1913,1914],{},"Vue 3 官方文档：响应性基础",[1874,1916,1917],{},[107,1918,1919],{"href":1919,"rel":1920},"https:\u002F\u002Fcn.vuejs.org\u002Fguide\u002Fessentials\u002Freactivity-fundamentals.html",[111],[1859,1922,1923,1926],{},[1874,1924,1925],{},"防篡改水印组件实现示例（思路参考）",[1874,1927,1928],{},[107,1929,1930],{"href":1930,"rel":1931},"https:\u002F\u002Fwww.wsisp.com\u002Fhelps\u002F61733.html",[111],[1094,1933,1934],{},"html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#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 .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .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 .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sfCm-, html code.shiki .sfCm-{--shiki-light:#90A4AE;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":153,"searchDepth":167,"depth":167,"links":1936},[1937,1938,1942,1943,1944,1945],{"id":39,"depth":167,"text":40},{"id":85,"depth":167,"text":86,"children":1939},[1940,1941],{"id":90,"depth":209,"text":91},{"id":145,"depth":209,"text":146},{"id":496,"depth":167,"text":497},{"id":808,"depth":167,"text":809},{"id":1769,"depth":167,"text":1769},{"id":1851,"depth":167,"text":1851},{"title":8,"description":9},"knowledge\u002Fteam-documents\u002F02","knowledge\u002Fknowledge\u002Fteam-documents\u002F02.md","md",{"items":1951},[1952,1956,1960,1964,1968,1972],{"path":1953,"title":1954,"description":1955,"category":10,"score":451},"\u002Fknowledge\u002Fteam-documents\u002F10","图片上传前的自定义水印实践","项目需要在图片上传前叠加动态水印。水印内容不是固定文案，而是由多个动态元素组成（如时间、地点、业务字段等）。",{"path":1957,"title":1958,"description":1959,"category":10,"score":411},"\u002Fknowledge\u002Fteam-documents\u002F19","GridView 宫格加载渲染优化","系统首页 GridView 宫格模块接口耗时不高，但首次进入总耗时接近 22s。复盘耗时分层定位过程、代码层面的瓶颈（约 2000 行、100 个 tab 重复节点）、优化手段与最终指标对比。",{"path":1961,"title":1962,"description":1963,"category":10,"score":411},"\u002Fknowledge\u002Fteam-documents\u002F04","老系统升级与兼容改造实践","公司原有业务系统采用 Node + jQuery 架构，长期运行后暴露出典型遗留系统问题：",{"path":1965,"title":1966,"description":1967,"category":10,"score":380},"\u002Fknowledge\u002Fteam-documents\u002F13","新商家系统性能优化实践","商家系统新版本上线后，团队持续针对构建效率与用户体验做了一轮系统性优化。本文记录核心方案与落地结果，供后续版本复用。",{"path":1969,"title":1970,"description":1971,"category":10,"score":380},"\u002Fknowledge\u002Fteam-documents\u002F03","键盘弹起导致底部被顶起问题（H5 适配）","在移动端 H5 页面中，当用户聚焦 input、textarea 等可编辑元素、软键盘弹出时，常见表现包括：",{"path":1973,"title":1974,"description":1975,"category":10,"score":380},"\u002Fknowledge\u002Fteam-documents\u002F22","企业微信 uni-app H5：OAuth 回退白屏与列表缓存","第三方 BI 报表项目，企业微信内嵌 H5，uni-app 编译，history 模式。主页面 BiLink 同时承担静默授权和列表展示，上线后碰到两个问题：授权完清掉 URL 参数，iOS 侧滑返回还是白屏；列表加了缓存以后，刷新行…",{"series":1977,"title":1978,"items":1979,"index":160,"prev":2053,"next":2054},"team-documents","team documents",[1980,1984,1985,1987,1989,1993,1997,2001,2005,2009,2011,2015,2019,2021,2025,2029,2033,2037,2041,2043,2047,2051],{"path":1981,"title":1982,"date":1983,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F01","执行价格弹窗：表头 \u002F 表体合并与可配置表格","2024-07-27",{"path":7,"title":8,"date":17,"pinned":18},{"path":1969,"title":1970,"date":1986,"pinned":18},"2024-09-17",{"path":1961,"title":1962,"date":1988,"pinned":18},"2024-12-12",{"path":1990,"title":1991,"date":1992,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F05","内部插件 \u002F 工具库开发实践","2021-08-19",{"path":1994,"title":1995,"date":1996,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F06","企业微信与小程序工单问题","2025-10-01",{"path":1998,"title":1999,"date":2000,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F07","企业微信群工具打开缓慢原因分析","2020-04-12",{"path":2002,"title":2003,"date":2004,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F08","属性排序需求","2024-06-16",{"path":2006,"title":2007,"date":2008,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F09","同业主重复单提醒规则","2020-06-07",{"path":1953,"title":1954,"date":2010,"pinned":18},"2021-06-10",{"path":2012,"title":2013,"date":2014,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F11","系统下单（重复单提示框）异常复盘","2023-11-21",{"path":2016,"title":2017,"date":2018,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F12","系统自动更新说明","2025-05-26",{"path":1965,"title":1966,"date":2020,"pinned":18},"2025-04-08",{"path":2022,"title":2023,"date":2024,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F14","选项卡切换与 Loading 状态封装","2019-11-03",{"path":2026,"title":2027,"date":2028,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F15","移动端返回事件触发两次问题复盘","2021-03-09",{"path":2030,"title":2031,"date":2032,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F16","在线协作表格接入实践","2023-10-27",{"path":2034,"title":2035,"date":2036,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F17","支付中转页面升级实践","2022-10-15",{"path":2038,"title":2039,"date":2040,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F18","中英文输入法下搜索触发冲突复盘","2024-09-01",{"path":1957,"title":1958,"date":2042,"pinned":18},"2026-03-17",{"path":2044,"title":2045,"date":2046,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F20","H5 GPS 定位问题说明","2023-08-31",{"path":2048,"title":2049,"date":2050,"pinned":18},"\u002Fknowledge\u002Fteam-documents\u002F21","vxe-table 行 Hover 联动高亮","2024-08-31",{"path":1973,"title":1974,"date":2052,"pinned":18},"2023-08-17",{"path":1981,"title":1982,"date":1983,"pinned":18},{"path":1969,"title":1970,"date":1986,"pinned":18}]