Axure变量与表达式使用详解:跨页传值、中继器分页筛选避坑
简介:面向具备一定Axure基础的产品经理、UI/UX设计师与前端原型开发人员,这份专题PDF聚焦变量与函数系统在复杂原型交互中的落地方法,帮助读者从基础操作过渡到跨页面数据共享、动态列表生成、响应式布局调整与条件分支等进阶实现。资源包仅含1个PDF文件,体积约556KB,轻量便于随查随用;内容按全局变量、中继器数据集、元件、页面、窗口、鼠标指针、数值、字符串、日期处理及布尔逻辑运算符展开,并配具体表达式示例说明作用范围。已有218人学习,适合作为案头速查与实操练习材料,读者可从中掌握中继器分页、用户状态管理、动态内容控制等常见做法,同时关注命名规范、作用域管理与数据类型匹配,减少命名冲突和逻辑错误,提升原型可维护性。
1. 变量不是选修课:原型一交互就露馅,问题多半出在变量上
上个月陪某公司产品团队过一版内部系统的交互稿,列表页刚演示到筛选,业务方顺口问了句"筛完翻到第二页,条件还在吗"。点了一下,条件归零,页码回到第一页,会议室安静了两秒。静态摆位画得再齐整,只要没把 Axure 常用变量用起来,原型就只是一张会翻页的图片。这份《Axure常用变量及使用方法详解.pdf》把全局变量、中继器数据集、元件/页面/窗口/鼠标指针这四类环境变量,以及数值、字符串、日期三类处理函数和布尔逻辑运算符串成了一条线,每一类都配了能直接抄进项目的表达式写法。它适合已经能画页面、但一碰到跨页传值、动态列表、滚动触发就卡住的交互设计和原型设计从业者,也适合想回头确认自己那套写法有没有摸到边界的老手。
2. 全局变量与中继器数据集:跨页传值和动态列表的两根主梁
这两块是 Axure 变量函数体系里最容易被低估的部分。全局变量看起来只是"存个值",实际决定了整个原型的用户状态能不能连贯;中继器看起来只是"循环一个列表",实际决定了数据量上来之后原型还能不能演示。把这两个吃透,剩下的元件、页面、窗口、鼠标变量都只是取值来源不同而已。下面按"先讲清结构、再落到可抄的表达式"的顺序推。
2.1 全局变量的作用域与命名约定
全局变量在"项目 > 全局变量"里定义,命名后在整个原型的任意页面、任意元件、任意中继器行内都能通过[[变量名]]读到。它没有强类型,存文本就是文本,存数字就是数字,类型完全靠你自己守。这一点是后面大量踩坑的源头:同一个变量先存"1"再存1,显示出来一模一样,比较起来结果完全不同。
命名上我一般按前缀分三类,落到表格里更直观:
| 前缀 | 承载内容 | 示例 | 备注 |
|---|---|---|---|
g_ | 全局登录态、主题、语言 | g_isLoggedIn | 全原型唯一,改动前先搜引用 |
u_ | 用户输入、表单值 | u_userName | 只在写入页和读取页出现 |
f_ | 筛选条件、排序方式、分页大小 | f_filterCriteria | 中继器筛选基本都靠它 |
前缀不是形式主义。全局变量在较新版本的 Axure RP 里没有分组视图,变量一多就是一条长列表,命名不统一的时候找theme能翻出theme、Theme、themeColor三个,改一个漏两个,这种玄学问题排查起来最费时间。
作用域上要守住一条:全局变量只放"跨页面还需要活着"的东西。页面内的临时计算,比如"当前行是否选中",用元件自身的[[This.isSelected]]或者中继器的[[Item.isMarked]],别往全局塞。全局变量一旦被当成草稿纸用,数据污染就是必然的,尤其在多人协作的原型文件里,谁都能改,谁都不敢删。
提示:全局变量改值之后,已经渲染在页面上的静态文本不会自动刷新。文本是在"页面载入时"求值一次的,后续变更必须靠"设置文本"动作再赋一次值。
最后一条容易被忽略的:自定义全局变量名不要和 Axure 内置的对象名撞车,Page、Window、This、Target、Item、Now、Cursor这些词拿来做变量名,表达式会解析成内置对象,取值直接是空或者一串看不懂的东西,而且不报错。这种黑匣子式的失败,新手基本无从下手。
2.2 中继器数据集的三层结构:Item、Repeater、TargetItem
中继器的变量分三层,理清层级比背函数表有用得多。
第一层是数据集本身,也就是你在中继器面板里那张表格的行和列,列名就是ColumnName。第二层是行上下文Item,它只在"每项加载时""项被点击时"这类逐行触发的场景里有效,代表当前这一行。第三层是组件对象Repeater,代表中继器整体,能拿到总条数、总页数、当前页码。
Item这一层的常用函数:
| 函数 | 含义 | 典型用法 |
|---|---|---|
[[Item.ColumnName]] | 当前行指定列的值 | [[Item.Name]] |
[[Item.index]] | 当前项索引,从 1 开始 | 序号列、隔行变色 |
[[Item.isFirst]] | 是否第一项,返回 true/false | 首项加圆角、隐藏上分割线 |
[[Item.isLast]] | 是否最后一项 | 末项隐藏下分割线 |
[[Item.isEven]]/[[Item.isOdd]] | 索引奇偶 | 斑马纹 |
[[Item.isMarked]] | 当前行是否被标记 | 多选、批量操作 |
[[Item.isVisible]] | 当前行是否可见 | 筛选后统计 |
Repeater这一层的常用函数:
| 函数 | 含义 | 注意点 |
|---|---|---|
[[Repeater.dataCount]] | 数据集总行数 | 筛选后仍是原始总数,不受筛选影响 |
[[Repeater.itemCount]] | 当前加载出来的项数 | 分页时是当前页条数 |
[[Repeater.pageCount]] | 总页数 | 由每页条数和筛选结果共同决定 |
[[Repeater.pageIndex]] | 当前页码 | 从 1 开始 |
[[Repeater.visibleItemCount]] | 当前页可见项数 | 最后一页不满时用它算真实条数 |
[[Repeater.TargetItem]] | 目标数据行对象 | 配合"更新行""标记行"定位 |
这里有个特别容易混的点:dataCount和itemCount在没分页、没筛选的时候数值一样,看不出差别;一旦开了分页,dataCount还是 20,itemCount变成 5。做"共 X 条 / 当前页 Y 条"这种统计文案,两个都得用上,写错一个数字就露馅。
还有TargetItem。它不是"当前行",是"你要操作的那一行"。典型场景是列表里每行有个删除按钮,点击后要标记这一行,这时候用"标记行"动作,目标行选[[Repeater.TargetItem]],而不是[[Item]]。这两者在"项被点击时"里经常是同一个值,但在"外部按钮批量操作"的场景里完全不同,用混了就是点 A 行删 B 行。
2.3 动手:登录态跨页传递与可翻页列表
把上面两块拼成一个能跑的最小案例:登录页写入全局变量,主页读取并渲染一个带分页、带筛选、带斑马纹的中继器列表。步骤按顺序来,每一步的表达式都能直接抄。
第一步,在"项目 > 全局变量"里建两个变量:g_userName存文本,g_isLoggedIn存true/false。登录页放一个输入框元件,命名input_userName,一个登录按钮。
// 登录按钮 - 单击时 // 动作1:设置变量值 // 变量:g_userName,值:元件文字 > input_userName // 动作2:设置变量值 // 变量:g_isLoggedIn,值:true // 动作3:打开链接 > 主页这段没有任何函数,但顺序不能反。先写变量再跳转,主页载入时才能读到值;反过来写,主页拿到的就是空字符串,而且不会报错,只是页面上"欢迎,"后面光秃秃的。参数说明一下:g_isLoggedIn存的是字符串"true"而不是布尔 true,后面比较时必须用"true"带引号,这是 Axure 全局变量的固有行为,别指望它做类型转换。
第二步,主页的"页面载入时"把值读出来。
// 页面载入时 // 设置文本 > 欢迎语 的值为: 欢迎, [[g_userName]]! // 设置文本 > 登录状态 的值为: [[If(g_isLoggedIn == "true", "已登录", "未登录")]]If(条件, 真值, 假值)是 Axure 里最常用的条件函数,返回的是字符串。逻辑说明:第一行是纯插值,变量为空就显示"欢迎, !";第二行做了兜底,未登录时显示"未登录",不会出现空白。失败时看什么:如果两个都显示异常,先回到全局变量面板确认变量名拼写,再确认登录页是否真的执行了"设置变量值"动作——用"预览"里的调试不方便,我一般会在主页角落放一个临时文本框,值设成[[g_userName]],一眼就能看出传没传过来。
第三步,中继器列表。数据集建三列:Name、Description、Category。中继器里放三个文本元件,分别命名txt_name、txt_desc、txt_index。
// 中继器 - 每项加载时 // 设置文本 txt_name 为:[[Item.Name]] // 设置文本 txt_desc 为:[[Item.Description]] // 设置文本 txt_index 为:[[Item.index]] // 设置填充色(作用于整行矩形 row_bg)为: [[If(Item.isEven, "#F7F8FA", "#FFFFFF")]]逻辑说明:Item.index是数据集内的绝对序号,从 1 开始,跨页连续,第 2 页第一行的 index 是 6 而不是 1。参数上,isEven判定的是 index 的奇偶,不是"视觉上的第几行",所以开了分页之后斑马纹可能出现"上一页末行灰色、下一页首行也是灰色",想强制每页从白色起始,得用页内序号:
// 页内序号 = 绝对序号 - (当前页 - 1) * 每页条数 [[Item.index - (Repeater.pageIndex - 1) * 5]]这里的 5 要和分页设置里的"每页项目数"保持一致,改了一处忘了另一处,序号就会从 3 开始跳,是高频翻车点。
第四步,分页控制条。
// 上一页按钮 - 单击时 // 动作:设置当前显示页面 > 中继器 > 上一页 // 页码文本 - 页面载入时 + 每次翻页后 第 [[Repeater.pageIndex]] / [[Repeater.pageCount]] 页,共 [[Repeater.dataCount]] 条 // 上一页按钮的"启用/禁用"状态 [[If(Repeater.pageIndex > 1, "启用", "禁用")]]逻辑说明:pageIndex到边界时不会变成 0 或超出,点击"下一页"在最后一页是无效的,所以按钮状态要单独判断,否则用户会以为卡住了。dataCount是筛选前的总数,如果要做"筛选后共 X 条",得换成Repeater.visibleItemCount配合筛选结果累加,这一点原料里没展开,但实际项目里业务方一定会问。
第五步,筛选。
// 筛选按钮 - 单击时 // 动作1:设置变量值 f_filterCriteria = 元件文字 > drp_category(下拉框选中项) // 动作2:给中继器"添加筛选" // 条件:[[Item.Category]] == [[f_filterCriteria]] // 动作3:设置当前显示页面 > 中继器 > 第 1 页第三步的"回到第 1 页"是必须的。筛选之后结果只剩 2 条,但当前页码还停在第 3 页,中继器会显示空白,用户以为筛选没生效。这个坑我在三个不同项目里都见过,属于典型的"代码没错、体验翻车"。
3. 元件、页面、窗口、鼠标:四类环境变量的取值边界与触发时机
这四类变量的共同点是"不用你定义,Axure 自己维护",共同的问题也在这——它们各自有明确的生效范围,超出范围取值就是空或者 0,而且不会报错。分清楚谁是"触发者"、谁是"目标"、谁是"视口"、谁只在拖拽里活着,比记住函数表更重要。
3.1 This 与 Target:触发者和目标的区别
[[This]]指触发当前事件的元件本身,[[Target]]指动作里被指定的目标元件。听起来像同义词,实际用起来完全不同。
做鼠标悬停放大效果,用 This 最省事,不用绑定目标:
// 卡片元件 - 鼠标悬停时 // 设置尺寸 > This > 宽 [[This.width * 1.05]],高 [[This.height * 1.05]] // 鼠标移出时 // 设置尺寸 > This > 宽 [[This.width / 1.05]],高 [[This.height / 1.05]]逻辑说明:这里读取的是元件的当前尺寸再乘系数,所以能反复触发不失真。参数上 1.05 是经验值,超过 1.1 在列表里会互相压盖,小于 1.02 又看不出来。失败时看什么:如果悬停后元件越变越大收不住,多半是把"鼠标移出"的除数写成了乘数,/1.05和*1.05写反了在预览里要反复悬停十几次才看得出漂移。
Target的典型场景是"一个按钮控制一堆元件"。比如侧边栏折叠,按钮点一下,五个图标的透明度同时变:
// 折叠按钮 - 单击时 // 设置不透明度 > Target > [[If(This.isSelected, 1, 0.3)]] // 配合"切换选中状态"动作,先把按钮自身设为选中这样写的好处是一个动作能带多个目标,不用给每个图标单独挂交互。代价是可读性差,交接的时候别人看到Target得先点开动作看目标是谁,所以我一般会在元件命名上带前缀,icon_menu、icon_search,看名字就知道这一组是一起动的。
元件变量里还有一组坐标和滚动相关的:This.x、This.y、This.scrollX、This.scrollY。x/y是相对父容器的坐标,放在动态面板里就是相对面板的,不是相对页面的。做"拖拽后归位"时用它记初始位置,比写死坐标稳。
3.2 Page 与 Window:画布尺寸和视口尺寸别混用
这两个最容易写错,因为函数名太像了。
| 变量 | 取的是什么 | 什么时候用 |
|---|---|---|
[[Page.width]]/[[Page.height]] | 页面画布尺寸,你在设计区设定的固定值 | 计算元素居中、按设计稿栅格布局 |
[[Window.width]]/[[Window.height]] | 浏览器视口尺寸,随窗口缩放变化 | 响应式断点、全屏适配 |
[[Page.scrollX]]/[[Page.scrollY]] | 页面滚动位置 | 页面内锚点定位 |
[[Window.scrollX]]/[[Window.scrollY]] | 视口滚动位置 | 吸顶导航、回顶按钮 |
[[Page.url]]/[[Page.title]] | 当前页面地址与标题 | 埋点演示、面包屑 |
[[PageName]] | 当前页面名称 | 高亮当前导航项 |
响应式判断必须用 Window,用 Page 的话无论窗口怎么缩,值都不变,看起来"判断写了但没反应":
// 页面载入时 + 窗口尺寸改变时 // 设置文本 > layout_flag 为: [[If(Window.width >= 1200, "large", If(Window.width >= 768, "medium", "small"))]]逻辑说明:If可以嵌套,从宽到窄依次判断,第一个命中的分支返回。参数上 1200 和 768 是常见的断点值,实际项目按设计稿栅格调整。要注意 Axure 不会自动监听窗口尺寸变化,得靠"窗口尺寸改变时"这个事件重新求值一次,否则用户拉大窗口之后文案还停在 small。
吸顶导航用Window.scrollY做阈值判断:
// 窗口滚动时 // 设置文本 > nav_bar 的背景色为: [[If(Window.scrollY > 100, "#FFFFFF", "transparent")]] // 设置阴影可见性为: [[If(Window.scrollY > 100, "显示", "隐藏")]]阈值 100 不是随便定的,一般取导航栏自身高度,滚过导航栏才变色,早了会显得页面一直在"半悬浮"状态。这个数值在预览里滚两下就能校准。
3.3 Cursor:拖拽判定与长按计时
鼠标指针变量是一组"有严格生效范围"的变量,Cursor.x、Cursor.y、Cursor.dragX、Cursor.dragY、Cursor.totalDragX、Cursor.totalDragY、Cursor.downTime这些,只在拖拽类交互(拖动开始、拖动时、拖动结束)里取值有效。挂在"单击时"上,读出来全是 0。
dragX/dragY是相对上一次采样点的增量,totalDragX/totalDragY是从按下到当前的总位移。做"左滑删除",判定要用总量:
// 列表行 - 拖动结束时 // 设置文本 > action_hint 为: [[If(Cursor.totalDragX < -50, "触发删除", "回弹归位")]]逻辑说明:负数代表向左滑,50 是触发阈值,单位像素。阈值低于 30 时轻微手抖就触发,用户会误删;高于 80 又要滑很久,手感发涩。参数调优只能靠预览里反复拖,我在触屏模拟下一般定在 50~60 之间。
长按用downTime,单位是毫秒:
// 元件 - 拖动结束时 [[If(Cursor.downTime > 800, "长按进入编辑", "普通点击")]]800ms 是常见的长按判定线,比 500ms 稳,比 1000ms 跟手。要注意downTime计的是按下到当前的总时长,拖动过程中它一直在涨,所以判定要放在"拖动结束"或"鼠标松开"里,不能放在"拖动时"里,否则拖到一半就误触发。
Cursor.x/Cursor.y适合做跟随效果,比如自定义光标提示:
// 跟随提示元件 - 拖动时 // 移动 > This > 到 ([[Cursor.x + 12]], [[Cursor.y + 12]])偏移 12 像素是为了让提示不压住系统光标,这个数在预览里调到顺手为止。
4. 数值、字符串、日期:三类处理函数的组合写法
函数单独用都不难,难在组合。一个"手机号脱敏 + 金额千分位 + 日期补零"的详情页,三行表达式能写对的人不多,原因基本集中在两处:类型没转、索引没对齐。这一章按数值、字符串、日期的顺序,每类给一个能落地的组合写法。
4.1 Number:精度、取整和类型转换
数值函数分两组。一组是格式化:toFixed(n)保留 n 位小数并返回字符串,toExponential(n)转科学计数法,toPrecision(n)保留总位数。另一组是数学运算:Math.abs、Math.ceil、Math.floor、Math.round、Math.max、Math.min、Math.random、Math.sqrt。
先讲最常见的金额计算。单价乘以数量,直接乘会出现19.900000000000002这种尾巴:
// 局部变量 LVAR1 = 单价(数字型),LVAR2 = 数量(数字型) // 设置文本 > txt_total 为: ¥[[ Math.round(LVAR1 * LVAR2 * 100) / 100 ]]逻辑说明:先乘 100 把两位小数抬成整数,Math.round四舍五入到整数,再除 100 还原。参数 100 对应"保留两位",要保留一位就改成 10。为什么不用toFixed(2)?因为toFixed返回的是字符串,后面还要参与运算的话类型会断掉,显示用toFixed、运算用Math.round是我的一般做法。
再讲取整的选择。分页算总页数:
// 每页 5 条,共 23 条 [[ Math.ceil(Repeater.dataCount / 5) ]]这里必须是Math.ceil,向上取整,23 条要算 5 页。用Math.floor会得到 4 页,最后一页的 3 条永远显示不出来。反过来,做"整屏展示几行"这种场景,就要Math.floor,因为不足一行的部分放不下。
单页展示条数:
// 视口高度 800,每行 60 [[ Math.floor(800 / 60) ]]Math.random()返回 0 到 1 之间的随机数,做"随机推荐"要取整:
// 从 10 条数据里随机取一条序号(1~10) [[ Math.floor(Math.random() * 10) + 1 ]]参数说明:Math.random()是左闭右开,取不到 1,所以乘 10 得 0~9.99,floor之后是 0~9,加 1 才是 1~10。少加这个 1,永远取不到最后一条,这种边界问题在演示时几乎不会被发现,上线前才被较真的人挑出来。
类型转换是这一节最重要的部分。Axure 里从输入框读出来的值永远是字符串,"12" + "3"得到的是"123"不是 15。要么用* 1强转,要么在局部变量里把它绑成数字:
// 输入框 input_qty 里用户填了 "3",单价是 19.9 // 错误写法:[[LVAR1 * LVAR2]] 直接乘,有时能算有时算出 NaN // 正确写法: [[ Math.round((LVAR2 * 1) * LVAR1 * 100) / 100 ]]* 1是最轻的转换手法,比parseInt稳,因为它对"3.5"也有效。用户在输入框里多敲一个空格,* 1得到 NaN,页面上就是"¥NaN",这反而是好事,一眼能看出输入有问题,比默默算出 0 强。
4.2 String:索引对齐和切片函数的选择
字符串函数表很长,真正需要记的是三件事:索引从 0 开始、slice/substring/substr的差异、replace只换第一个。
| 函数 | 作用 | 索引规则 |
|---|---|---|
length | 字符长度 | 无 |
concat(s) | 拼接 | 无 |
indexOf(s) | 首次出现位置 | 找不到返回 -1 |
lastIndexOf(s) | 最后出现位置 | 找不到返回 -1 |
slice(start, end) | 截取 [start, end) | 支持负数,从头算 |
substring(start, end) | 截取 [start, end) | 负数按 0 处理 |
substr(start, len) | 从 start 取 len 个 | 第二个参数是长度不是终点 |
split(sep) | 分割成数组 | 无 |
replace(a, b) | 替换 | 只替换首个匹配 |
toLowerCase()/toUpperCase() | 大小写 | 无 |
trim() | 去首尾空白 | 无 |
手机号脱敏,用substring:
// 局部变量 LVAR1 = 输入框 input_phone 的文字 [[ LVAR1.substring(0, 3) ]]****[[ LVAR1.substring(7, 11) ]]逻辑说明:11 位手机号,前 3 位保留,中间 4 位打码,后 4 位保留。substring(7, 11)的终点写 11 不是 10,因为区间是左闭右开,写 10 会少一位。这个 off-by-one 是字符串处理里最高频的错误,预览时数字看着差不多,仔细数才发现少了一位。
用户名截断显示,用slice更安全:
// 超过 8 个字符就截断加省略号 [[ If(LVAR1.length > 8, LVAR1.slice(0, 8) + "...", LVAR1) ]]slice和substring在正索引下行为一致,区别在负索引:slice(-4)取末尾 4 位,substring(-4)直接当 0 处理,返回整个字符串。做"取文件扩展名"这种要从后往前数的,必须用slice:
// 从 "report.pdf" 里取 "pdf" [[ LVAR1.slice(LVAR1.lastIndexOf(".") + 1) ]]lastIndexOf找不到匹配时返回 -1,加 1 之后是 0,slice(0)返回整个字符串,不会报错但结果是错的。加一层保护:
[[ If(LVAR1.lastIndexOf(".") > 0, LVAR1.slice(LVAR1.lastIndexOf(".") + 1), "无扩展名") ]]split返回数组,单独打印出来是逗号拼接的一串,实际用法是配合中继器做标签云:
// 输入框里填 "设计,原型,交互" // 中继器的数据集用"更新"动作,按 split 结果逐项添加 [[ LVAR1.split(",") ]]trim()专门治输入框首尾空格。登录校验里这个几乎是必备:
[[ If(LVAR1.trim().length == 0, "请输入用户名", "校验通过") ]]不加trim,用户敲一个空格就能"通过"必填校验,这种问题在真实用户手里百分百会遇到。
4.3 Date:月份偏移、补零和时区
[[Now]]是当前时间对象,日期函数都是从它上面取。第一个必须记住的:getMonth()返回 0 到 11,一月是 0,所以显示的时候永远要 +1。第二个:getDay()返回 0 到 6,0 是周日。
// 日期时间格式化 [[ Now.getFullYear() ]]-[[ Now.getMonth() + 1 ]]-[[ Now.getDate() ]] [[ Now.getHours() ]]:[[ Now.getMinutes() ]]这样写出来是2024-6-1 9:5,不是2024-06-01 09:05。补零得手动做:
// 分钟补零 [[ If(Now.getMinutes() < 10, "0", "") ]][[ Now.getMinutes() ]] // 完整版 [[ Now.getFullYear() ]]-[[ If(Now.getMonth() + 1 < 10, "0", "") ]][[ Now.getMonth() + 1 ]]-[[ If(Now.getDate() < 10, "0", "") ]][[ Now.getDate() ]]逻辑说明:If返回空串或者"0",拼在前面。参数上判断条件是< 10而不是<= 10,10 本身是两位数不需要补。这串表达式很长,实际项目里我会把它做成一个不可见的"日期文本"元件,页面载入时算一次,其他地方引用这个元件的文字,避免到处复制。
倒计时用时间戳相减:
// 距离目标日期还有几天 [[ Math.floor((Date.parse("2025-01-01") - Now.getTime()) / 86400000) ]]逻辑说明:Date.parse把日期字符串解析成毫秒时间戳,Now.getTime()也是毫秒,相减得到毫秒差,除以 86400000(一天的毫秒数)得到天数。Math.floor保证不足一天按 0 天算,不会出现3.7 天这种显示。
时区用getTimezoneOffset(),返回的是"本地时间与 UTC 的分钟差",而且符号和直觉相反:东八区返回 -480。做跨时区演示时这个值要摆正:
// 换算成小时,东八区显示 +8 [[ -Now.getTimezoneOffset() / 60 ]]前面加负号是关键,不加的话东八区显示 -8,业务方一眼就看出不对。
5. 避坑:五条变量表达式翻车记录与一套排查顺序
变量这块的失败有个共同特点:不报错。表达式写错了,页面照常渲染,只是内容是空的、数字是 0、按钮点不动。没有报错信息可看,只能靠一套固定的排查路径把范围缩小。下面五条是我和团队里其他人踩过的,每条按"现象 → 原因 → 解决"记。
5.1 五条高频翻车记录
第一条:全局变量改了,页面上文字没变。
现象是登录之后跳回主页,"欢迎,XXX"里的名字还是空的,但重新刷新预览就正常了。原因是静态文本只在"页面载入时"求值一次,后续变量变更不会触发重新渲染。解决办法是在改变量的同一个交互里追加一个"设置文本"动作,重新赋一次[[g_userName]];或者把显示逻辑统一收到"页面载入时"的"设置文本"里,不用静态插值。我现在的习惯是:凡是绑定全局变量的文本,一律走"设置文本"动作,不用默认文本里的[[ ]]。
第二条:中继器筛选后当前页变成空白。
现象是列表本来在第 3 页,点了筛选,结果只剩 2 条,但页码还停在第 3 页,区域一片空白。原因是筛选只更新了数据集,没有重置页码。解决办法是在"添加筛选"动作之后追加一条"设置当前显示页面 > 第 1 页"。这条要养成肌肉记忆,就像写 SQL 分页必须带order by一样。
第三条:Item.index和视觉行号对不上。
现象是开了分页之后,第二页第一行的序号显示成 6 而不是 1。原因是Item.index是数据集内的绝对索引,跨页连续,它不关心你分了几页。解决办法是用[[Item.index - (Repeater.pageIndex - 1) * 每页条数]]算页内序号,并且把"每页条数"抽成一个全局变量g_pageSize,分页设置和表达式都引用它,改一处就够。写死数字的版本,改分页大小的时候必然漏改。
第四条:数字比较结果反直觉。
现象是"10" > "9"返回 false。原因是从输入框或中继器读出来的值是字符串,字符串比较按字符逐位比,"1"小于"9",所以"10"排在"9"前面。解决办法是参与大小比较前统一转数字:[[LVAR1 * 1 > LVAR2 * 1]],或者在局部变量绑定时就选"数字"类型。这条坑在价格区间筛选里特别常见,"100 元以上"筛出来的结果里混着 20 元的商品。
第五条:Cursor相关函数在单击事件里全是 0。
现象是给列表行加了"单击时读取Cursor.x",结果永远是 0。原因是鼠标指针变量只在拖拽类交互里有上下文,单击事件里没有拖拽会话。解决办法是把逻辑挪到"拖动时"或"拖动结束"里;确实需要在单击里拿到位置,就用元件的This.x/This.y配合"鼠标移入时"缓存到局部变量。这条没有捷径,只能记住哪些函数属于哪个事件域。
5.2 一套可复用的四步排查顺序
翻车之后不要从头改表达式,按这个顺序走,八成问题在前两步就能定位。
第一步,打印。在页面角落放一个临时文本框,把可疑变量直接[[变量名]]打出来。空的话说明没写进去或者作用域不对;有值但显示不对,说明是格式化问题,跳到第四步。这一步的价值在于把"变量没值"和"变量有值但用错了"两类问题分开,省掉大量瞎猜。
第二步,查作用域。全局变量看是否在目标页面的"页面载入时"之前写入;中继器的Item系列看是否写在了"每项加载时"里,写到外面就是空;Target看动作的目标是不是被复制出来的旧元件——复制粘贴元件之后,目标引用有时还指着原来那个,预览里点新元件没反应,点旧位置反而有反应,这种最费时间。
第三步,查求值时机。同一个表达式,放在"页面载入时"和放在"单击时"结果可能完全不同,因为依赖的变量在中间被改过。判断方法是给两个时机各打一次日志,看值在哪一步变的。带条件分支的交互,尤其要注意"切换选中状态"这类动作会改变This.isSelected,写在它前面的表达式读到的是旧值。
第四步,查类型。到这一步基本只剩数字和字符串混用的问题。判据很土但有效:如果结果是拼接而不是相加,就是字符串;如果比较结果和数值直觉不符,也是字符串。统一用* 1转一遍,再不行就在局部变量里明确绑成数字类型。
这四步走完还不对的,我会把整段交互删了重画。Axure 里有些状态是删不干净的,尤其是反复复制粘贴过的元件,重画比修快,这是血泪经验。
6. 布尔逻辑与表达式收敛:把散落的判断收成一行
布尔这块的坑不在语法,在可维护性。一个列表项要不要显示"删除"按钮,条件可能是"已登录 且 是本人 且 未锁定 且 不在回收站",四个条件分别写在四个交互里,改一个漏三个,最后没人敢动。收敛成一行表达式,是这类问题的唯一解。
先看优先级,Axure 的运算符顺序和 JS 一致:
| 优先级 | 运算符 | 说明 |
|---|---|---|
| 1 | ! | 逻辑非,最高 |
| 2 | ><>=<= | 比较 |
| 3 | ==!= | 相等判断 |
| 4 | && | 逻辑与 |
| 5 | || | 逻辑或,最低 |
&&比||优先级高,所以a || b && c实际是a || (b && c)。记不住就全加括号,多打两个字符不会有人怪你,写错了要查半天。短路特性也要用起来:&&左边为假,右边不求值;||左边为真,右边不求值。这个特性可以用来防错,比如先判空再取长度:
// 先判空,再判长度,避免对空值调用字符串函数 [[ If(LVAR1 != "" && LVAR1.length > 3, "有效", "无效") ]]如果顺序反过来写成LVAR1.length > 3 && LVAR1 != "",空值的时候前半段已经在求值了,虽然 Axure 大多时候能兜住,但换成别的表达式就容易出空指针。这个顺序我一般固定成"先判存在,再判内容,最后判业务"。
把四个条件收成一行:
// 局部变量:LVAR1 = 当前用户 ID,LVAR2 = 内容作者 ID // 全局变量:g_isLoggedIn、g_isLocked、g_isDeleted [[ If(g_isLoggedIn == "true" && LVAR1 == LVAR2 && g_isLocked != "true" && g_isDeleted != "true", "显示", "隐藏") ]]逻辑说明:四个条件用&&串起来,任一不满足就隐藏。参数上注意g_isLocked != "true"而不是== "false",因为全局变量可能从没被赋过值,这时候是空字符串,== "false"判断不出来。用"不等于 true"来兜底,空值也能正确走到"显示"分支。这个写法看着别扭,但它能挡住一类"变量忘了初始化"的线上问题。
If和三元表达式的取舍:Axure 里没有真正的三元运算符,日常说的三元其实就是If。嵌套超过三层就该拆了:
// 三层嵌套,能读但费劲 [[ If(Window.width >= 1200, "large", If(Window.width >= 768, "medium", If(Window.width >= 480, "small", "tiny"))) ]] // 拆成全局变量 g_layout,在"窗口尺寸改变时"分四次判断分别赋值拆开之后每个分支单独一行,改断点值不用去数括号。验证方法很简单:预览时把窗口从宽拖到窄,看文案是不是按 1200 / 768 / 480 三个点依次变化,卡在某个值不变就说明那一层的条件写反了。
最后说个我自己的习惯。每次交付前,我会把所有带[[ ]]的文本元件过一遍,凡是里面出现三个以上&&或者两层以上If的,一律抽成全局变量,在页面载入时算一次,元件里只留一个变量名。从那以后我每次改判断条件都强制走一遍这个收敛流程,改一处就够,不用满页面搜表达式。希望帮到你。这份《Axure常用变量及使用方法详解.pdf》里的函数表和示例可以当手册翻,配合上面这套收敛习惯用,原型的交互逻辑会稳很多,需要的可以从文末入口自取。
本文还有配套的精品资源,点击获取