Axure变量与中继器求值机制:从表达式到动态原型

简介:面向具备一定 Axure 基础的产品经理、UI/UX 设计师与前端原型开发人员的变量与函数进阶手册,聚焦原型交互中跨页面传值、动态列表、响应式布局等难点,帮助读者把静态线框推进到可演示的高保真交互。压缩包仅含 1 个 PDF 文档,体积约 556KB,轻量便于随身查阅与打印批注;内容按全局变量、中继器数据集、元件、页面、窗口、鼠标指针及数值、字符串、日期处理等模块组织,并穿插布尔逻辑运算符与表达式示例。已有 218 人学习,说明其在原型进阶人群中具备一定参考价值。读者可从中获得各类变量与函数的用途对照、常见表达式写法、分页与条件分支的实现思路,以及命名规范、作用域管理、数据类型匹配等避坑提示,适合边操作软件边查阅,用来提升原型逻辑的严谨性与可维护性。

1. 表达式原样显示成方括号,问题往往不在语法

很多人第一次在 Axure 里做动态交互,都会撞上同一个画面:明明在文本里写了[[userName]],预览时页面上却老老实实印着两个方括号和一串英文。反直觉的地方在于,这跟“变量没定义”和“语法写错”基本无关——Axure 的[[ ]]从来不是编辑器里的字符串替换,而是一条只在特定执行时机被触发的求值通道。搞懂这条通道什么时候开、谁在往里塞值,比背下几十行函数表有用得多。变量与函数系统正是 Axure 原型设计从“画页面”跨到“跑逻辑”的分水岭,交互设计里那些跨页面传参、动态列表、响应式布局、前端开发式的条件判断,底层全压在这套求值模型上。

2. 求值模型:[[ ]]里的内容在什么时候被真正算出来

2.1 编辑时是文本,运行时才是表达式

Axure 把“你写的东西”和“跑起来的东西”当成两码事。你在元件文本属性里敲下的[[userName]],在编辑器里只是一段普通字符,运行时才会被解析成一次求值。求值点大致分三类:页面加载(OnPageLoad)、动作执行(设置文本、设置值、显示/隐藏、移动等)、中继器每行加载(OnItemLoad)。写在求值点之外的地方,比如元件名称、交互备注,运行时不会求值,于是你就看到了方括号。

// 写在矩形的「文本」属性里:页面加载时求值一次,属于模板式 当前登录用户:[[userName]] // 写在「设置文本」动作的值里:只有动作被触发时才求值,属于表达式式 [[ "当前登录用户:" + userName ]]

这两段看着像,机制完全不同。第一段里[[ ]]外面是纯文本,括号内是变量名,运行时做的是“把括号段替换成值”;第二段整个值就是一个表达式,括号内是完整的 JS 风格运算,字符串常量必须带引号,拼接靠+,变量名不带引号。参数上要记住一条:括号内写的是变量名本身,不是[[userName]]套[[userName]],嵌套写会让解析器把内层当成不存在的标识符,结果是空字符串——空值不会报错,只会安静地消失,这也是它最坑的地方。

2.2 全局变量的生命周期:它活在同一个运行时里

全局变量定义在项目级,有一个默认值。原型加载的那一刻,运行时把默认值灌进去,之后每次“设置变量值”动作都会覆盖它。它的存活范围是同一个浏览器标签页里的运行时内存,页面跳转不会销毁,刷新才会回到默认值。跨页面传参之所以成立,靠的就是这一点:页面只是渲染单元,变量挂在运行时上,不在页面上。

// 登录按钮的「设置变量值」动作,两条 userName = [[ LVAR1.text ]] // LVAR1 绑定输入框 txtUser isLoggedIn = "true" // 布尔也用字符串存,别指望有类型 // 首页 OnPageLoad,用条件表达式决定文案 [[ isLoggedIn == "true" ? "欢迎, " + userName : "请先登录" ]]

这里有两个细节值得说透。第一,全局变量没有类型系统,你存进去的"true"是字符串,写isLoggedIn == true可能拿到 false,稳妥做法是两头都用字符串比较。第二,也是血泪经验:新手容易把变量名和元件名混着用,[[txtUser]]拿到的是元件对象,在文本里会被转成它的文本内容,但一旦进了数值运算就变成 NaN,所以绑定元件时显式写.text更保险。

命名规范不是形式主义。全局变量的作用域是整个原型,两个页面各定义一个name,后加载的会覆盖先加载的,数据污染来得毫无征兆。我一般要求变量名带业务前缀,比如login_userName、filter_keyword,一眼能看出归属,也能避开page、window、this这类内置标识符。

2.3 作用域优先级:局部变量为什么能压过全局变量

局部变量(LVAR1、LVAR2…)是在动作编辑器里临时定义的,只在这个动作或这条条件判断内有效。它的价值不在于“省事”,而在于给动作一个稳定的输入快照——全局变量随时可能被别的交互改掉,局部变量在动作开始时绑定好,中途不会变。

// 动作级局部变量:LVAR1 绑定元件 txtKeyword,LVAR2 绑定全局变量 filter_keyword // 全局变量里也有一个叫 keyword 的变量,故意同名测试遮蔽 [[ "输入:" + LVAR1.text + " / 全局:" + keyword ]]

常见的实现思路是这样一个名字解析顺序:局部变量 → 当前中继器 Item 上下文 → 全局变量 → 内置对象(This、Target、Page、Window、Now)。也就是说,局部变量名和全局变量名撞车时,局部赢。这不是 bug,而是刻意的设计取舍——动作内部应该优先使用自己声明的输入,避免被外部状态偷偷劫持。理解了这一层,你就能解释另一种常见现象:同一个表达式,放在中继器模板里能出数据,挪到页面上的文本框里就变成空,因为Item这个上下文只在模板行内存在。

3. 中继器:原型里的「内存表 + 渲染循环」

3.1 Item 与 Repeater:行对象和容器对象不能混用

中继器的数据集本质是一张二维表,列名就是表头,行是记录。Item指向“当前这一行”,Repeater指向“整个中继器容器”,两者是不同层级的对象,混用是最高频的错误来源。模板行内部只能用Item,因为渲染循环执行到哪一行,Item就临时指向哪一行;容器级的信息(总条数、当前页码)才用Repeater。

// 中继器模板行内的文本元件:每加载一行就求值一次 [[Item.name]] —— [[Item.price]] 元 // 行序号与斑马纹判断,index 从 1 开始 第 [[Item.index]] 行 / 偶数行:[[Item.isEven]]

Item下挂的行函数里,index从 1 开始而不是 0,isFirst和isLast用来做首尾特殊样式,isEven、isOdd做隔行底色,isMarked配合“标记行”动作实现多选,isVisible反映当前行是否被隐藏。这些属性不是静态字段,而是每次渲染时按行的实际状态算出来的,所以你在模板里写[[Item.isLast ? "最后一页" : ""]],每一行拿到的结果都不同——这正是渲染循环的意义。

// 放在中继器外部的统计文本里,用容器对象,不要用 Item 共 [[Repeater.dataCount]] 条,当前第 [[Repeater.pageIndex]] / [[Repeater.pageCount]] 页

参数说明:Item.ColumnName里的列名必须和数据集表头完全一致,大小写敏感,多一个空格就取不到值,且不会报错,只返回空。这一点和后端模板引擎的习惯不同,后者通常有位子占位符做约束,而 Axure 是静默失败。

3.2 dataCount、itemCount、pageCount 三个数字为什么会对不上

这三个计数看着都像“条数”,实际量的是三件不同的事。dataCount是数据集里的总行数,是数据的真身;itemCount是当前已加载、参与渲染的项数,筛过、分过页之后它会缩水;visibleItemCount是当前页里真正可见的项数,再叠一层隐藏状态。pageCount和pageIndex则是分页窗口的坐标。

// 筛选后对比:dataCount 不变,itemCount 变小 数据集总量:[[Repeater.dataCount]] / 当前加载:[[Repeater.itemCount]] // 自己算页数时,除法必须向上取整,否则 11 条 5 条一页会算成 2 页 [[ Math.ceil(Repeater.dataCount / 5) ]]

为什么要把“数据量”和“渲染量”分开?因为中继器不是一次性把所有行都画到画布上,它只维持一个渲染窗口。分页、筛选、动态增删都会改变窗口内容,但不改变底层的表。这个区分带来的直接好处是:做“共 N 条,当前显示 M 条”这种交互时,不用额外存状态,两个属性一减就知道有没有被筛掉。踩坑点在于,很多人用itemCount去做分页总页数计算,结果加了筛选之后页数乱跳——正确做法永远用dataCount算总页,用itemCount显示当前。

3.3 OnItemLoad 与分页:渲染窗口决定了你看到哪几行

OnItemLoad是每一行渲染时触发一次的循环体。它执行的次数等于当前渲染的项数,不是数据集行数。这一点决定了中继器的能力边界:你可以在每行加载时读取该行数据、做条件样式、拼装文本,但不能在行内修改整个数据集——那是“设置数据集”动作的活。

下面这段是很多动态列表的骨架:行内按Item的值做条件分支,再用Item.isFirst做首行加粗。

// OnItemLoad 中给行内标签设置文本 [[ Item.price > 100 ? "高价:" + Item.price : "常规:" + Item.price ]] // 首行加个标记,用于视觉引导 [[ Item.isFirst ? "★ 推荐位" : "" ]]

逻辑说明:表达式在行加载时求值,Item临时绑定当前行,所以每一行算出的字符串都不同;Item.price > 100走的是数值比较,前提是数据集里该列存的是数字而不是带单位的字符串,否则会掉进字典序比较的陷阱(第 5 章会展开)。分页控制的本质是移动渲染窗口:pageIndex加一,窗口后移一页,OnItemLoad重新对新的那批行执行一遍,所以你写在行内的初始化逻辑不会“记住”上一页的状态——需要跨页保持的东西,得存在全局变量里。

4. 取值与运算:四类上下文对象和一类函数库怎么配合

4.1 This 与 Target:同一段表达式,绑定对象不同结果就不同

This指触发当前事件的元件,Target指这个动作作用的目标元件。多数时候两者相同,因为元件自己触发、自己变化;但在“多个按钮共用一个动作”的复用场景里,它们必然分叉:触发的是某个按钮,目标是页面上那个统一的结果标签。

// 五个分类按钮共用一段「设置文本」,写进同一个 lblResult // This 是被点击的那个按钮,Target 是 lblResult [[ "你选择了:" + This.text ]] // 反过来读目标当前的文本,常用于切换式交互 [[ Target.text == "展开" ? "收起" : "展开" ]]

参数说明:This.text取的是触发元件当前的文本,This.x、This.y、This.width、This.height分别对应坐标和尺寸,This.rotation是旋转角度,This.opacity是透明度,This.isVisible判断可见性,This.isSelected判断选中态,This.scrollX、This.scrollY是元件内部滚动位置。读属性不难,难在写:表达式里做赋值(比如[[This.width]] = 200)是很多教程里的写法,实际执行靠的是“设置尺寸”这类动作,表达式本身只负责算值。把“算”和“改”分开,是理解这套系统的关键一步。

4.2 Page 与 Window:两个“宽度”差在哪,什么时候该用哪个

Page.width是原型页面的设计画布宽度,是一个固定值,你在页面设置里定多少就是多少。Window.width是预览时浏览器视口的宽度,随窗口缩放实时变化。做响应式判断必须用Window,用Page判断等于在问“我的画布多宽”,永远得不到你想要的断点效果。

// 根据视口宽度打布局标记 [[ Window.width >= 1200 ? "lg" : (Window.width >= 768 ? "md" : "sm") ]] // 滚动超过 100px 时显示回顶按钮 [[ Window.scrollY > 100 ? "show" : "hide" ]]

逻辑说明:这段嵌套三元表达式从左往右短路求值,先判大屏再判中屏,都不满足才落到sm。Window.scrollY是相对文档顶部的滚动距离,配合“窗口滚动时”事件可以做出滚动吸顶、进度条这类效果。Page.url和Page.title在预览环境下拿到的是预览地址,不要指望它和最终发布环境一致,做参数传递时这点要留神。Page.scrollX、Page.scrollY与Window的同名属性容易混,前者偏向页面容器自身的滚动,后者是视口滚动,实际使用中以预览效果为准,别死记。

4.3 Cursor 与拖动累计量:为什么 totalDragX 才可靠

Cursor.x、Cursor.y是鼠标相对页面的坐标,Cursor.dragX、Cursor.dragY是本次拖动过程中的增量,Cursor.totalDragX、Cursor.totalDragY是从按下到现在的累计距离,Cursor.downTime是按住时长。区别在于“增量”会被清掉,累计不会——判断“用户到底拖没拖够”必须用累计量。

// 拖动超过 50px 才算有效拖拽,横向纵向任一方向满足即可 [[ Math.abs(Cursor.totalDragX) > 50 || Math.abs(Cursor.totalDragY) > 50 ]] // 长按判定:按住超过 800ms [[ Cursor.downTime > 800 ]]

这里用Math.abs是因为向左拖totalDragX是负数,直接比大小会把左滑判成无效。参数上,Cursor系列只在鼠标相关事件(点击、拖动、移动)的上下文里有意义,写在页面加载里拿到的就是初始值,别指望它实时刷新。拖动交互最常见的翻车是把dragX当成累计量用,结果一松手就归零,判定永远不成立。

4.4 Number、String、Date、Boolean:函数库其实是 JS 的影子

Axure 的运算函数基本是 JavaScript 同名能力的子集,语义也一致。理解这一点,很多行为就不用背了,按 JS 的规则推就行。

数值侧,toFixed(n)返回的是字符串,Math.round()返回的是数字,两者不能互相替代。做金额展示用toFixed,做后续计算用Math.round。参与运算前先确认是数值,字符串会走隐式转换,转不动就是 NaN,而 NaN 参与比较永远为 false。

// 求和并保留两位小数:用 *1 强制走数值运算,避免字符串拼接 [[ (LVAR1 * 1 + LVAR2 * 1).toFixed(2) ]] // 随机数取整到 1-100 [[ Math.floor(Math.random() * 100) + 1 ]]

字符串侧,substr(start, length)是“从哪开始、取几个”,substring(start, end)是“从哪到哪”,slice支持负数下标。三个都能截串,混用的后果是截出来多一个字或少一个字,而且不报错。split返回数组,indexOf返回首次出现位置、找不到返回 -1,判断存在性时别写成> 0,位置 0 也是存在的。

// 从 "2024-06-01" 这种格式里拆年月 [[ LVAR1.substr(0, 4) ]] 年 [[ LVAR1.substr(5, 2) ]] 月 // 去空格后再比较,避免用户多打一个空格导致匹配失败 [[ LVAR1.trim() == "admin" ]]

日期侧,Now是内置的当前时间对象,getMonth()返回 0 到 11,所以展示时要 +1;getDay()返回 0 到 6,0 是周日;Date.parse("2024-06-01")返回毫秒时间戳,比较两个日期先后用它最稳。getTimezoneOffset()返回的是分钟数,不是小时。

// 拼一个 yyyy-MM-dd HH:mm,注意月和分不会自动补零 [[ Now.getFullYear() + "-" + (Now.getMonth() + 1) + "-" + Now.getDate() + " " + Now.getHours() + ":" + Now.getMinutes() ]]

布尔侧,==是宽松相等,"1" == 1为真,===在 Axure 的表达式里支持度不稳定,稳妥做法是两端统一类型再比。&&和||有短路特性,If(cond, a, b)只算命中的那一支,把重计算放在b里,条件不成立时就不会执行。

5. 避坑与排查:原型跑不通时先看这五处

5.1 现象:页面上原样显示[[userName]]

原因通常是写在了不会被求值的位置。元件名称、部分说明性属性、以及某些没有绑定动作的静态文本,运行时都不会解析方括号。另一种情况是表达式本身写错,解析器放弃求值,原样回显。

解决办法分三步:先确认这段文字是不是写在元件的“文本”属性或动作的“值”里;再确认所在动作确实被触发过(加一个临时的点击提示验证);最后检查括号是否成对、变量名是否带多余空格。排查时养成一个习惯——在页面角落放一个隐藏的调试文本框,专门显示中间值,比盯着成品页猜快得多。

5.2 现象:中继器十行数据渲染出来全是第一行

原因基本是把Item写成了固定值,或者在OnItemLoad里用“设置文本”把模板文本覆盖成了常量。还有一种隐蔽情况:模板行里的表达式引用了一个全局变量,而这个全局变量在每行加载时恰好没被更新,于是十行拿到同一个值。

解决办法是先分清层级——行内用Item.Column,行外用Repeater.xxx。然后检查OnItemLoad里有没有写死的赋值动作,它会覆盖掉模板里的表达式。如果是全局变量的问题,把值在行加载时绑定到局部变量再使用,快照能保证每行拿的是自己的那份。

5.3 现象:数值比较结果和肉眼看到的相反,"9" > "10"返回 true

原因是没有做类型统一,字符串比较走的是字典序,逐位比字符编码,9比1大,所以"9" > "10"成立。这在中继器排序、价格筛选里非常常见,因为数据集里的数字列很容易被当成文本存。

解决办法是在比较前强制转成数值,用* 1或Math系列函数把字符串拖进数值域。如果数据来源不可控,先用trim()清空格,再转换,最后用isNaN之类的判定拦住脏数据。我一般会在筛选动作里先算一次转换后的值,存进局部变量,后续比较全用那个变量,避免每处都写一遍转换逻辑。

5.4 现象:日期少一个月、差一天,或者跨时区对不上

原因是getMonth()从 0 开始,这是最经典的一个偏移;其次是时区,getTimezoneOffset()返回分钟,不同环境下的显示可能不一致;再就是手写拼接时没补零,2024-6-1这种格式在字符串比较时会出问题。

解决办法:展示月份统一+1,拼接时对月和日做补零处理;比较日期先后不用字符串比,统一转Date.parse()的时间戳比大小;跨时区场景下,只做展示的用本地时间,做业务判断的统一走时间戳。

5.5 现象:跳转页面后全局变量变回默认值

原因多半是新开了标签页或窗口。全局变量活在单个运行时实例的内存里,新标签页就是新实例,变量自然重置;预览刷新也一样。同一个标签页内的页面跳转不受影响,这是很多人误判的地方。

解决办法是先确认跳转方式,同标签内跳转就能保住值;确实需要跨标签页的,用 URL 参数把关键值带过去,接收页在加载时读出来写回全局变量,相当于手动做一次序列化。这个思路和前端开发里的会话状态管理是一个道理,只是原型阶段不需要做得那么重。

6. 设计取舍:为什么这套表达式语言故意做得很薄

Axure 完全可以把表达式引擎做得更强——上完整 JS 引擎、加类型检查、给控制台报错。它没有。这不是能力问题,是取舍:原型的第一诉求是快速试错和沟通,不是交付级代码。强类型意味着每写一个表达式都要先声明,报错意味着每次改动都要停下来处理异常,这两件事都会把“画一版给评审看”的节奏拖垮。所以它选择了宽松、静默、能跑就行的路线。

代价也很明确:错误不报,只表现为空值、0 或原样显示。这正是这套系统最难的地方——它不给你黑匣子,只给你结果。于是调试只能靠“打点”,我一般用三栏法把一条链路拆开验证。

调试位置写什么用来确认
源值[[userName]]变量到底有没有被赋值
中间值[[userName.length]]值是不是空串、有没有被转成字符串
结果值[[ "欢迎, " + userName ]]拼接逻辑是否成立

三栏并排放,哪一栏开始空,问题就出在它前面那一步。这套方法听着笨,但比反复改表达式猜原因快得多,尤其在中继器里,行内表达式的执行上下文和外面不一样,光看成品页根本看不出是哪一行出错。

再往深一层看,这套设计其实在教一件事:分清“数据在哪”和“数据怎么算”。全局变量是跨页面的共享状态,局部变量是动作内的输入快照,中继器数据集是结构化数据的容器,三类东西的生命周期完全不同,混用就是各种玄学问题的根源。把生命周期想清楚,Item为什么在页面文本框里失效、全局变量为什么刷新后重置、dataCount为什么和itemCount对不上,全都能自己推出来,不用背。

从那以后我每次搭带交互的原型,都强制先画一张极简的数据流草稿:哪些值要跨页面活、哪些只在一个动作里活、哪些是表里的行。写完再动手,返工少一半。希望帮到你。

本文还有配套的精品资源,点击获取