资讯动态

XPath元素定位实战:从树模型到移动端稳定表达式

发布时间:2026/10/27 16:39:15 来源:尧图企业网站定制
做UI自动化和数据采集这几年被问得最多的一类问题不是框架怎么搭、报告怎么出而是“这个元素为什么定位不到”。问的人里有刚写完第一个脚本的新手也有跑了一年多、脚本在版本更新后集体崩掉的老手。最后排查下来八成以上都落在同一个地方XPath 表达式写得太“贴”了贴着当前版本的界面结构长界面一改表达式就废。这篇就把 XPath 从最基础的树模型讲到我目前实际在用的写法中间穿插一段在移动端做商品卡片匹配的完整实战包括为什么最后我从“纯结构定位”退回到“属性相对关系”的混合写法。读完你应该能做到两件事看到一棵界面层级树能快速写出稳定的表达式表达式失效时有一套可以照着走的排查链路而不是靠反复重试碰运气。1. 元素定位这道坎卡住的其实是“树的读法”1.1 XPath 到底在做什么把界面当成一棵可寻址的树很多人学 XPath 的第一天就被/、//、[]、这一堆符号砸晕了其实它的核心思想朴素得不能再朴素任何一份结构化文档不管是网页的 HTML 还是移动端的界面层级都可以看成由节点组成的树XPath 就是一套“寻址语法”用来描述“从树的某个位置出发怎么走到我要的那个节点”。举个生活化的类比。你要在一个大型小区里找一户人家有几种描述方式第一种是“从小区大门进去直走第三个路口右转第二栋三单元五楼左边那户”——这就是绝对路径一步都不能错中间任何一栋楼改名或者加了个门禁你就走不通了。第二种是“找一个门口挂着‘三单元’牌子、并且门牌号是 502 的住户”——这就是带属性谓词的相对路径只要这户人家还挂这两块牌子哪怕小区重新规划了道路你照样能找到。XPath 里对应的就是这两种/html/body/div[3]/div[2]/ul/li[1]是第一种//li[data-iditem-1]是第二种。新手最常犯的错就是用开发者工具或者元素查看器右键“复制 XPath”拿到的基本都是第一种。它在当下这一秒是百分之百正确的但它的信息量几乎全部来自“位置”而位置恰好是界面迭代中最容易被改动的东西。一个合格的表达式应该满足改版后仍然能命中。判断标准很直接你把这个表达式写下来然后问自己一句——如果产品经理明天在这个元素前面插了一个运营位这条表达式还找得到吗如果答案是否定的那它就只是个临时快照不是定位器。1.2 根节点、元素节点、文本节点、属性节点先分清你在找什么写表达式之前必须先明确你要找的到底是“节点”还是“值”这两者混在一起是大量诡异问题的源头。节点类型典型写法得到的结果元素节点//div[idmain]一个元素对象可以继续取属性、可以点击属性节点//div/id属性值本身通常是字符串文本节点//div/text()文本内容可能是多个片段根节点/整个文档的根不是html实际项目里踩得最多的一个坑是这样的想取某个价格文本写成了//span[classprice]/text()结果拿到的是空字符串或者只拿到一半。原因是这个span内部还有一层i或者em包着货币符号文本被拆成了多个文本节点。XPath 的text()只返回直接子文本节点不会穿透后代元素。这时候正确的做法是用string(.)在支持 XPath 1.0 全量函数的引擎里或者退一步直接取元素再调getText()让引擎把后代文本拼起来。另一类常见误解是命名空间。移动端层级树里经常能看到android.widget.TextView这种带前缀的类名那不是命名空间只是类名里的一个点。真正的命名空间在 Web 端的 SVG 或者自定义标签里更常见处理方式是用local-name()忽略前缀比如//*[local-name()svg]。别小看这一句凡是涉及图标的定位几乎都要靠它。1.3 工具给你的 XPath 能信几分元素查看器里“Copy XPath”给出的结果我一般的处理方式是看一眼然后删掉重写。原因很简单工具生成的是“当前文档结构下的唯一路径”它的目标是唯一性而不是稳定性。它会老老实实地写/div[4]/div[2]/div[1]/span[2]因为这样对当前这一份快照来说确实唯一。但它没有考虑三件事一是顺序会变列表里的运营位、广告位插入是常态二是同级节点数量会变div[2]明天可能变成div[3]三是很多框架生成的类名带哈希比如classsc-bdVaJa iJQmzE这种哈希每次构建都会变你拿它当锚点等于埋雷。我会从开发者工具里拿的只有两样东西一是id或者resource-id二是结构上下文也就是“这个元素在谁的下面、跟谁平级”用来设计相对关系。至于整条路径本身一律自己拼。提示判断一个属性能不能当锚点最省事的办法是搜一下这个属性值在整份文档里出现几次。出现一次可作唯一锚点出现多次但业务上代表同一类元素可作类型锚点再配合其他条件收窄。1.4 相对路径为什么几乎总是更优//的语义是“从任意位置开始搜索后代节点”它跳过了中间的所有层级。这看起来性能上有代价因为引擎要遍历整棵子树但换来的是对结构变化的免疫能力。实测下来Web 端一个中等复杂度页面//开头的表达式与固定层级路径相比耗时差异通常在毫秒级对自动化脚本的整体时长影响可以忽略而移动端情况不同这个后面单独讲。真正决定要不要用//的不是性能而是这个元素有没有可靠的自身特征。有 id、有稳定的业务属性、有稳定的文案那就绝对不要依赖层级只有在元素本身没有任何可靠特征只能靠“它是某个已知容器的第 N 个孩子”时才退回到结构路径而且这种情况下我还会加一条路径尽量短只描述必要的那一两层关系。2. 基础语法把七成场景覆盖掉的那几个写法2.1 节点关系与四种基本选取方式XPath 里描述节点关系的词很有限但必须记牢因为高级用法全是在它们之上组合出来的父parent、子child、兄弟sibling、祖先ancestor、后代descendant。基础选取就四种/从根开始绝对路径实际很少用//从当前节点开始的任意后代最常用.当前节点在find_element的链式调用里非常关键..父节点配合//反过来找容器.这个点值钱的地方在于上下文收缩。举个例子页面上有十个商品卡我先定位到第三个卡片容器然后要在这个容器里找标题。如果直接写//div[classtitle]引擎还是从整棵树搜索可能命中第一个卡片的标题写成.//div[classtitle]就限定在容器内部搜索。这个细节在列表批量操作时是致命的因为它不会报错只会安安静静地点错东西。..的典型场景是“从子找父”页面上有一堆“加入购物车”按钮我想点某个特定商品对应的那一个。按钮本身没有业务标识但它的兄弟节点里有商品名。这时候的思路是先定位到商品名节点再..回到卡片容器再往下找按钮。这种写法在移动端尤其常见因为移动端的按钮往往是图标没有可读文案。2.2 属性谓词等于、包含、前缀各有各的适用面谓词[]里放条件是最主要的收窄手段。三种写法在实际项目里的分工非常明确写法语义适用场景风险[idx]完全相等id、resource-id 这类确定性属性属性值本身带哈希时会失效[contains(class,btn)]包含子串多类名元素、带状态后缀的类名子串过短会误匹配[starts-with(id,item-)]前缀匹配动态生成但前缀固定的 id前缀相同的兄弟元素需要额外收窄contains()是最容易被滥用的一个。我见过有人写contains(class,a)结果把classactive、classdata-row、classloader全命中了。使用contains时子串最好带上语义边界比如contains(class, btn-)或者contains(class,price)宁可长一点也别短到失去区分度。还有一个细节值得单独说多类名元素的判断顺序。一个元素classbtn btn-primary btn-lg用contains(class,btn-primary)是对的用[classbtn-primary]就是错的因为属性值整体不等于这个子串。很多人写完发现“明明有这个类名为什么匹配不到”就是栽在这。2.3 索引从 1 开始以及位置函数带来的隐式陷阱XPath 的索引从 1 开始不是 0。这个不用多解释但真正麻烦的不是起点而是索引的生效范围。//ul/li[2]和(//ul/li)[2]是两条完全不同的表达式。前者是“每个 ul 下的第二个 li”返回的是一组节点每个 ul 各出一个后者是“所有 ul/li 组成的集合里的第二个”只返回一个节点。这个差别在只有一个 ul 的页面上看不出来一旦页面结构里嵌套了列表结果就天差地别。位置函数还有last()、position()。用last()取最后一个元素的场景挺多比如“点最后一页”“取最新的那条记录”。但要注意last()是相对于当前节点集的如果前面用了//没有收窄last()取的是整份文档里最后一个匹配元素未必是你想的那一个。提示任何带索引的表达式都是脆的。索引的正确用途是“在已经确定唯一的情况下取其中一个”而不是“用它来制造唯一性”。如果你需要靠索引才能唯一命中说明前面的条件不够应该回去补条件。2.4 文本匹配text()、contains()和normalize-space()的配合按文本定位在 UI 自动化里用得极多因为文案通常比 id 更贴合业务语义写出来可读性也好。但文本匹配的坑也是最多的主要来自三个方向。一是空白字符。页面渲染时为了排版文本节点前后常有换行和缩进[text()确定]匹配不到[text() 确定 ]。标准的解法是[normalize-space(text())确定]把首尾空白压缩掉、内部连续空白合并成一个空格。二是文本被拆分。前面提过的嵌套标签问题text()只看直接子文本。这时候可以换成contains(., 确定)用.代表节点自身会参与后代文本的比较。三是文案会变。带数字的文案最典型比如“共 12 条评论”“已有 3 人参与”。这种用完全相等一定挂用contains()才是正解但要挑文案里稳定的那一段比如contains(., 条评论)。再补一个容易被忽略的函数translate()它可以做简单的字符替换常用来处理大小写不敏感的比较[translate(class,ABCDEFGHIJKLMNOPQRSTUVWXYZ,abcdefghijklmnopqrstuvwxyz)btn-primary]。XPath 1.0 没有lower-case()只能这么绕。写得丑但管用。3. 高级用法让表达式在下一次版本迭代后还活着3.1 轴axes从“往下找”扩展到“往四周找”大部分人写 XPath 只会往下找//加谓词就完事了。但真实界面里目标元素经常在“旁边”或者“上面”这时候轴就派上用场了。常用的几个ancestor::往上找所有祖先常配合[1]取最近的那个following-sibling::找后面的兄弟适合“标签-值”成对出现的结构preceding-sibling::找前面的兄弟适合“值在标签左边”的布局descendant::和//基本等价但可以嵌在轴表达式里parent::等价于..语义更清晰ancestor::是我在移动端用得最多的一个。典型结构是列表项容器 → 多个子元素标题、评分、距离、标签、按钮。我想点“某某店”这一项的按钮稳妥的写法是先从店名定位到容器再在容器里找按钮//*[text某某小馆]/ancestor::*[resource-idcom.example.takeout:id/item_root][1]//*[text进店]这里的[1]非常关键它表示“取最近的、符合条件的那一个祖先”。不加的话返回的是整个祖先集合某些引擎会取第一个也就是最外层的根容器那就找到整个列表上去了然后你在列表里点第一个“进店”点错了还浑然不觉。following-sibling::的经典用途是表单。标签和输入框是平级兄弟输入框本身没有可识别属性//label[forphone]/following-sibling::input[1]这种写法的好处是它绑定的是“标签和输入框的相对位置”这个业务语义而不是绝对层级。表单里前后插入一个新字段只要这对兄弟还是相邻的表达式依然有效。3.2 谓词嵌套与逻辑组合收窄的层次感谓词里可以再放表达式形成嵌套。比如“找 class 包含 item 的 div 里文本包含‘已售罄’的那一个”//div[contains(class,item)][contains(., 已售罄)]多个谓词并列等价于and但写法上有个差别值得注意独立谓词是逐个过滤的引擎可以先按第一个条件筛出候选集再在候选集里按第二个条件筛。而用and连成一个表达式某些引擎的优化路径不同。实际差异通常不大但在大文档上、候选集差距悬殊时有可测的区别——先用区分度高的条件、后用区分度低的条件整体更快。逻辑运算符and、or、not()里not()的实用价值被严重低估。一个典型场景是排除隐藏元素//div[contains(class,list-item) and not(contains(class,hidden))]还有排除特定文案的//li[not(contains(., 广告))]。这类“反向筛选”在采集列表数据时几乎是必需的因为列表里混着推广位、占位符、骨架屏直接按结构取会把它们一起捞进来。不过要提醒一句or用得越多表达式的可读性和可维护性下降得越快。我给自己定的规矩是一条表达式里or不超过两组超了就说明这几个元素的业务含义其实不同应该拆成多个定位器分别管理而不是硬塞进一条。3.3 字符串函数的组合技处理动态文案与脏数据XPath 1.0 的字符串函数不多但组合起来能解决大部分动态文本问题。normalize-space()除了用于匹配还能用来做“非空判断”//div[normalize-space(text())!]筛掉只有空白的占位节点。骨架屏加载时页面上常常渲染一堆空 div这条表达式能直接把它们过滤掉。string-length()用来按长度筛比如价格文本一般不超过 10 个字符可以用[string-length(normalize-space(.)) 10]把一整个容器的长文本排除掉。substring-before()和substring-after()用于截取比如从“3.5km”里取数字substring-before(., km)。但要注意XPath 在自动化框架里通常只用来定位取到值之后再在代码里做字符串处理更方便调试。把复杂的字符串运算写进表达式可读性会崩出问题时也不好排查。concat()主要用于拼接比较实际用得少。translate()前面提过主要解决大小写。starts-with()和contains()是主力几乎每条表达式都会用到其中之一。3.4 命名空间、特殊字符与转义写 XPath 时最容易被忽略的是字符串里的引号。如果文案本身带单引号比如[text()Dont]表达式直接语法错误。两种解法外层用双引号[text()Dont]或者如果文案里单双引号都有就得用concat()拆开拼接//div[text()concat(He said , its, fine)]Web 端的 SVG 是另一个高频问题区。图标基本都是svg加一堆path用普通写法匹配不到必须用local-name()//*[local-name()svg and classicon-cart]/ancestor::button[1]移动端则要留意resource-id里带冒号的情况比如com.example.app:id/tv_title。这里的冒号在属性值字符串里不是语法的一部分用单引号包起来就没问题不需要转义。但如果你用某些框架的简写定位器语法比如idxxx冒号可能被解析成别的含义这时候老老实实用 XPath 全写反而最稳。4. 移动端实战某本地生活类 App 的商品卡匹配4.1 移动端 XPath 与 Web 端的三个本质差异前面讲的语法在移动端完全通用但运行环境差别很大不理解这三点写出来的脚本一定又慢又不稳。第一移动端的层级树是每次查询时现场抓取的。Web 端页面 DOM 常驻内存查询只是遍历移动端find_element_by_xpath往往要触发一次完整的层级 dump把当前界面序列化成 XML 再解析。这意味着一条//*开头的宽泛表达式成本可能是几百毫秒到一两秒。写十条这样的表达式脚本就卡成幻灯片了。第二层级树的属性集合不同。Web 端有id、class、>//android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout[3]这条表达式在某些时刻确实能命中。问题在于它是纯粹按位置描述的。列表页顶部有个运营位轮播、一个分类入口宫格、一个筛选栏这三块任何一块的节点数量变化[3]指向的东西就变了。更麻烦的是它不报错只是安静地指向了另一个卡片然后脚本点进了别人的店。第二版改成按文本找店名//*[text某某小馆]这条命中的是店名那个TextView本身。TextView一般是不可点击的clickablefalse直接点它在部分框架里会退化成点坐标能生效但不稳更稳的做法是往上找到卡片容器再在容器里找可点击的按钮。这就是第三版//*[text某某小馆]/ancestor::*[resource-idcom.example.takeout:id/item_root][1]先锚定卡片。这一步已经可以用来看卡片里到底有哪些元素了。实测时我发现一个细节卡片容器上有resource-id但这个 id 在整页里出现了十几次每个卡片一次所以它不能作为全局唯一锚点只能作为容器类型的标识——这正好符合我们的用法因为我们是先用店名唯一锚定再往上找容器。找到容器之后往下找按钮完整表达式//*[text某某小馆] /ancestor::*[resource-idcom.example.takeout:id/item_root][1] //*[resource-idcom.example.takeout:id/btn_enter]这个写法有一个隐含前提btn_enter这个 id 在单个卡片里只出现一次。如果卡片里还有别的地方用了同一个 id比如底部再来一个“进店”那就会命中多个脚本取第一个。这时候要么加[1]明确取第一个要么再加一个条件区分比如[clickabletrue]。4.3 卡片内元素不可点击、文本被拆分时的两种兜底实战里一定会遇到卡片内某个按钮既没有resource-id、也没有text、只有content-desc的情况。图标按钮最典型。这时候表达式要换成//*[text某某小馆] /ancestor::*[resource-idcom.example.takeout:id/item_root][1] //*[content-desc进店]content-desc的好处是它对标无障碍语义业务方改图标样式的概率远大于改无障碍描述的意愿稳定性通常不错。但它也有个坑某些框架会把content-desc和text合并处理导致两者同时存在或者互相覆盖。稳妥的做法是写一条or兜底//*[text某某小馆]/ancestor::*[resource-idcom.example.takeout:id/item_root][1] //*[resource-idcom.example.takeout:id/btn_enter or content-desc进店]另一类高频问题是店名文本不是完全相等。比如店里带了状态后缀“某某小馆营业中”或者店名后面跟了一个“新店”角标被合并进了同一个文本节点。这时候[text某某小馆]匹配不到要换成//*[starts-with(text,某某小馆)]或者更宽松一点//*[contains(text,某某小馆)]用contains要小心误伤如果列表里同时有“某某小馆”和“某某小馆二店”contains会同时命中两个取第一个可能就是错的。我的处理习惯是店名类定位优先starts-with因为它对“后缀变化”免疫但对“前缀相同”的干扰更敏感只有当前缀本身也可能变时才退到contains并且此时一定配合容器层级再收窄一次。4.4 滚动加载与视图复用为什么索引定位一定要戒掉列表页往下滚新卡片渲染出来旧卡片被回收。这个机制带来两个后果一是你之前拿到的元素引用可能失效点下去报StaleElementReferenceException二是屏幕上“第 3 个卡片”在不同滚动位置对应完全不同的商家。所以移动端的定位设计有一条铁律永远用业务属性锚定不用位置锚定。滚动加载的正确姿势是循环执行三步判断目标文本是否在当前层级树里存在 → 不存在就滚动一屏 → 再判断。伪代码大致是这样def scroll_to_shop(driver, shop_name, max_swipes10): for _ in range(max_swipes): elems driver.find_elements( xpath, f//*[starts-with(text,{shop_name})] ) if elems: return elems[0] driver.swipe(0.5, 0.8, 0.5, 0.3, 600) raise LookupError(f滚动 {max_swipes} 屏仍未找到: {shop_name})这里有两个经验点。第一判断存在用find_elements返回列表不要用find_element加try/except前者在找不到时直接返回空列表不抛异常也不触发额外的等待逻辑效率差好几倍。第二滚动距离不要设成整屏留 20% 左右的重叠区否则快速滑动时容易跳过某一屏的内容——这个坑我在长列表上遇到过定位不到的元素其实就在两屏之间被甩过去了。还有一点滚动之后重新查询时务必重新执行整条表达式不要缓存上一次的元素对象。视图被回收后旧引用指向的对象虽然还在内存里但已经脱离界面树操作它会得到各种莫名其妙的结果。4.5 一套可以抄走的表达式模板把上面的经验收拢一下我目前在用的卡片类定位模板是这样几条换业务时改属性和文案就行!-- 模板1文本锚定 容器回溯 目标收窄最常用 -- //*[text{唯一文案}] /ancestor::*[resource-id{包名}:id/{卡片容器id}][1] //*[resource-id{包名}:id/{目标控件id}] !-- 模板2前缀匹配应对文案带后缀 -- //*[starts-with(text,{文案前缀})] /ancestor::*[contains(resource-id,:id/{容器id片段})][1] !-- 模板3无障碍描述锚定应对纯图标控件 -- //*[content-desc{描述}] /ancestor::*[resource-id{包名}:id/{卡片容器id}][1] !-- 模板4排除干扰项用于列表批量采集 -- //*[resource-id{包名}:id/{卡片容器id} and not(contains(text,广告)) and clickabletrue]模板 2 里用了contains(resource-id, :id/xxx)而不是完整 id这是个取巧但实用的做法。完整包名在某些环境下会变不同渠道包、不同构建变体但:id/后面的资源名基本稳定。这个写法在单个 App 内部是安全的因为资源名通常全局唯一跨 App 就别这么写了。注意模板 4 里把clickabletrue放进条件里是有意为之。很多卡片容器上有clickable属性广告位和占位符往往是false这一条能顺手滤掉一批噪声。但要注意不同框架对clickable的继承规则不一样写之前先在层级树里确认一下该节点自身的属性值别想当然。5. 定位不到元素时我实际走的排查链路5.1 先把异常类型分清楚别一上来就重试脚本报错的第一反应如果是“加点等待再跑一次”那问题只会被推迟不会消失。异常类型本身就是线索分类看清楚能省掉一大半时间。异常/现象大概率原因排查第一步找不到元素NoSuchElement表达式错、页面未加载完、元素在滚动区外dump 层级树看当前快照元素已失效StaleElement视图被回收或页面重绘重新查询别缓存引用选择器语法错误InvalidSelector引号未闭合、函数名拼错、轴名写错拿表达式单独跑一次点到了错误的元素表达式匹配多个取了第一个用find_elements看匹配数量元素存在但点不动被弹窗/键盘遮挡或enabledfalse看bounds是否被覆盖“匹配到多个”这一类是最阴的因为它不报错。我给自己的规矩是凡是新写的表达式第一次用一律先用find_elements跑打印匹配数量。数量等于 1 才改成find_element。这个习惯帮我拦下过很多次事故尤其是那种“平时只有一个、偶发场景下出现两个”的情况——比如弹了一个推荐弹窗页面上同时存在两个同名按钮。5.2 从层级树快照到最小复现定位失败时我不建议直接在原脚本上改因为脚本里掺杂了等待、上下文切换、前后步骤的状态变量太多。正确的做法是把问题剥离出来做最小复现。第一步把当前界面的层级树完整 dump 下来存成文件。移动端就是抓页面源码Web 端就是保存当前 DOM。这份快照是你的“现场证据”很多问题看一眼属性值就水落石出了——比如你以为resource-id是btn_enter实际是btn_enter_v2或者你以为文本在TextView上实际它在父节点的content-desc里。第二步在快照上从简到繁试表达式。先试最短的//*[resource-idxxx]看命中几个不命中就说明属性名或值错了命中多个就逐层加条件收窄。这个过程中每一次修改都记下来因为最后你会知道“到底是哪一条条件起了作用”而不是碰巧蒙对。第三步确认表达式没问题之后再把问题归因到环境。这一步能排除掉大量“表达式明明对但就是找不到”的困惑常见原因有元素在可滚动区域外层级树里有但视觉上不可见某些框架会把它标记为不可交互、被WebView包着需要切换上下文才能看到内部节点、在原生与混合页面的交界处一半节点在原生树里一半在 WebView 里。5.3 把定位器当成代码资产来管走到这一步剩下的就是工程习惯问题了。我见过太多项目把 XPath 散落在脚本各处一个页面五十条字符串硬编码改一次版要全文件搜索替换改完还得赌自己没漏。现在我的做法是把定位器全部外置按页面或者业务模块分文件用 YAML 或者常量类管理并且给每条定位器加上三样元信息用途说明、上次验证时间、失败时的备选表达式。备选这一项看着累赘实际上非常值——主表达式失效时能立刻切到备选脚本不至于直接挂掉给修复留出缓冲期。shop_card: enter_button: primary: //*[text{shop_name}]/ancestor::*[resource-id{pkg}:id/item_root][1]//*[resource-id{pkg}:id/btn_enter] fallback: //*[starts-with(text,{shop_name})]/ancestor::*[contains(resource-id,:id/item_root)][1]//*[content-desc进店] note: 卡片容器id在页面内不唯一仅作类型标识必须靠店名先锚定还有一条经验值得单独讲给定位器写回归用例。不是业务用例就是纯粹的“这些定位器在当前版本还能不能命中”的检查脚本每次版本更新后跑一遍哪个失效一目了然。这个投入很小但能把“上线前一晚脚本集体崩”变成“发版当天上午花二十分钟修三条表达式”。至于表达式的复杂度我的取舍标准是这样的一条 XPath 超过三行、或者包含两个以上的轴、或者用到了嵌套谓词就应该考虑是不是可以用框架提供的其他定位方式替代。原生 id 定位、无障碍 id 定位在移动端都比 XPath 快得多XPath 的价值在于处理那些“没有唯一属性、只能靠关系描述”的疑难元素。把它当万能钥匙最后受累的是自己。我在实际项目里体会最深的一点是XPath 写得好不好跟记了多少函数关系不大跟有没有认真看过层级树关系很大。花十分钟把一棵树的属性和层级关系看清楚比照着文档试二十条表达式有用得多。真要说有什么技巧那就是每次写表达式之前先问自己一句这条表达式描述的是这个元素的“身份”还是它的“位置”前者能活很久后者活不过一次改版。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑