解决 Filament Alpine.js includes null 报错 (4种方案)
2025-04-28 04:35:23
解决 Filament 中的 Alpine.js 报错:Cannot read properties of null (reading 'includes')
写代码的时候,碰见 Cannot read properties of null
或 Cannot read properties of undefined
这类 JavaScript 报错挺常见的。最近在使用 PHP Filament 构建后台界面时,不少人遇到了一个特定的 Alpine.js 错误:
Alpine Expression Error: Cannot read properties of null (reading 'includes')
这个错误通常出现在类似下面这样的 Alpine.js 表达式里:
!($store.sidebar.groupIsCollapsed(label) && $store.sidebar.isOpen)
这个表达式通常用在 Filament 的侧边栏导航,用来控制导航组(group)的展开和折叠状态的显示逻辑。那这个错到底是怎么回事,又该怎么修呢?咱们来捋一捋。
问题出在哪?
这个错误信息 Cannot read properties of null (reading 'includes')
说白了就是:你尝试在一个值为 null
的东西上去调用 .includes()
方法。JavaScript 里,只有数组(Array)和字符串(String)有 .includes()
方法。当你对一个 null
或者 undefined
的值用这个方法时,JavaScript 老哥就直接撂挑子不干了,抛出这个错误。
再看看那个出问题的表达式: !($store.sidebar.groupIsCollapsed(label) && $store.sidebar.isOpen)
这里面,$store.sidebar
是 Alpine.js 的全局状态存储,Filament 用它来管理侧边栏的状态信息,比如哪些导航组是折叠的 (groupIsCollapsed
),侧边栏整体是否打开 (isOpen
) 等等。
错误指向 includes
,虽然表达式里没有直接写 .includes
,但极有可能是 $store.sidebar.groupIsCollapsed(label)
这个函数 内部 尝试调用了 .includes
方法。一种常见的实现方式是,groupIsCollapsed
内部维护了一个包含所有当前折叠状态的导航组 label
的数组,然后通过 collapsedGroupsArray.includes(label)
来判断传入的 label
是否在这个数组里。
所以,问题根源就比较清晰了:
$store.sidebar
本身可能是null
或undefined
: 在 Alpine.js 组件初始化,或者页面某些状态下,$store.sidebar
可能还没被 Filament 完全准备好。这时候直接访问$store.sidebar.groupIsCollapsed
就会出错(虽然错误可能发生在更深层的includes
调用上,但源头是$store
还没就位)。groupIsCollapsed
函数内部依赖的数据是null
: 即使$store.sidebar
存在,groupIsCollapsed
函数内部用来存储折叠组状态的那个(推测的)数组,在某个时刻可能是null
或undefined
。当它尝试在这个null
数组上调用.includes(label)
时,错误就发生了。
这通常和组件加载、初始化的时序有关。Alpine.js 需要时间来初始化它的 Store 和组件,尤其是在动态加载内容或者复杂交互的场景下。
怎么解决?
知道了原因,解决起来就有方向了。下面提供几个可行的方案,你可以根据自己的情况选择。
方案一:添加空值检查(防御性编程)
这是最常见也最直接的法子。在访问可能为 null
或 undefined
的对象的属性或方法之前,先检查它是否存在。
原理:
通过增加判断条件,确保只有在 $store.sidebar
和其内部依赖的数据都准备好的情况下,才执行后续的逻辑。
操作步骤:
修改你的 Alpine.js 表达式,加入检查:
方法 A:使用逻辑与 (&&
) 短路特性
<!-- 修改前的表达式 -->
<!-- x-show="!($store.sidebar.groupIsCollapsed(label) && $store.sidebar.isOpen)" -->
<!-- 修改后的表达式 -->
<div x-show="$store.sidebar && typeof $store.sidebar.groupIsCollapsed === 'function' && !($store.sidebar.groupIsCollapsed(label) && $store.sidebar.isOpen)">
<!-- 你的导航组内容 -->
</div>
<!-- 或者更简单点,假设 groupIsCollapsed 存在就意味着 store 存在 -->
<div x-show="!($store.sidebar?.groupIsCollapsed(label) && $store.sidebar?.isOpen)">
<!-- 你的导航组内容 -->
</div>
解释:
$store.sidebar && ...
确保$store.sidebar
不是null
或undefined
。typeof $store.sidebar.groupIsCollapsed === 'function'
(可选但更严谨) 确保groupIsCollapsed
是个函数再调用。但在这种场景下,如果$store.sidebar
结构固定,只检查$store.sidebar
存在通常就够了。
方法 B:使用 Optional Chaining (?.
)
现代 JavaScript (Alpine.js 支持) 提供了 Optional Chaining 操作符 (?.
),它允许你读取嵌套对象深处的属性值,而无需显式验证链中的每个引用是否有效。如果某个引用是 null
或 undefined
,表达式会短路并返回 undefined
,而不是抛出错误。
<!-- 使用 Optional Chaining -->
<div x-show="!($store.sidebar?.groupIsCollapsed(label) && $store.sidebar?.isOpen)">
<!-- 你的导航组内容 -->
<!-- 注意:这里假设 groupIsCollapsed 和 isOpen 都是 $store.sidebar 的直接属性/方法 -->
<!-- 如果 groupIsCollapsed 本身可能返回 null/undefined,并且其内部调用 includes, -->
<!-- 这个 ?. 只能保证对 groupIsCollapsed 的调用不报错,无法防止内部的 includes 错误 -->
<!-- 但实践中,错误通常发生在访问 groupIsCollapsed 之前或其本身是 undefined 时 -->
</div>
解释:
$store.sidebar?.groupIsCollapsed(label)
: 如果$store.sidebar
是null
或undefined
,整个表达式直接返回undefined
,不会尝试调用groupIsCollapsed
。$store.sidebar?.isOpen
: 同理,如果$store.sidebar
不存在,返回undefined
。! (undefined && undefined)
或者! (result && undefined)
等最终会根据逻辑运算得到一个布尔值(undefined
在逻辑运算中通常表现为false
),避免了includes
错误。
安全建议:
- Optional Chaining (
?.
) 是目前更推荐的方式,代码更简洁易读。但要确保你的项目构建环境支持它(大部分现代环境都支持)。 - 这种方法解决了眼前的报错,但没有解决
$store.sidebar
为什么会是null
的根本原因(如果它是)。
方案二:使用 x-if
确保 Store 初始化后再渲染
有时候,问题出在包含该表达式的 HTML 元素在 $store.sidebar
完全准备好之前就被渲染和计算了。我们可以用 x-if
来延迟渲染。
原理:
x-if
指令会等到其表达式结果为 true
时,才将模板内容渲染到 DOM 中。我们可以用它来判断 $store.sidebar
是否已经有效。
操作步骤:
在外层元素上使用 x-if
包裹使用 $store
的部分:
<template x-if="$store.sidebar">
<div x-show="!($store.sidebar.groupIsCollapsed(label) && $store.sidebar.isOpen)">
<!-- 你的导航组内容 -->
</div>
</template>
<!-- 或者直接用在同一个元素上,结合 x-show -->
<!-- 但通常 x-if 用 template 包裹更清晰 -->
<div x-if="$store.sidebar" x-show="!($store.sidebar.groupIsCollapsed(label) && $store.sidebar.isOpen)">
<!-- 你的导航组内容 -->
</div>
解释:
<template x-if="$store.sidebar">
: 这部分内容(包括里面的div
)只会在$store.sidebar
的值“truthy”(即不是null
,undefined
,false
,0
,""
)时才会被添加到页面。这样,当内部的x-show
表达式计算时,$store.sidebar
肯定已经存在了。
进阶使用:
- 如果你的逻辑比较复杂,或者
$store.sidebar
内部的某些属性也是异步加载的,你可能需要更精细的x-if
判断,比如x-if="$store.sidebar && $store.sidebar.someRequiredProperty"
。
安全建议:
- 无特定安全问题,但要注意
x-if
会真正地添加/移除 DOM 元素,如果元素内部状态复杂或有副作用,需要考虑这些影响。
方案三:利用 x-init
和 Alpine.nextTick
延迟计算
如果问题确实是时序竞争,即表达式计算发生得太早,可以尝试将依赖 $store
的计算推迟到下一个 "tick"。
原理:
Alpine.nextTick()
会将一个回调函数推迟到 Alpine.js 完成当前的响应式更新之后执行。这给了 Store 初始化等操作更多时间。通常配合组件内部的 x-data
使用。
操作步骤:
这个方法对于直接用在 x-show
或类似指令里的表达式不太直接。它更适用于需要在组件初始化时基于 Store 计算一个状态,然后用这个状态来控制显示。
<div x-data="{ isGroupExpanded: true }"
x-init="Alpine.nextTick(() => {
if ($store.sidebar && typeof $store.sidebar.groupIsCollapsed === 'function') {
// 确保 store 和方法都可用后再计算
isGroupExpanded = !($store.sidebar.groupIsCollapsed(label) && $store.sidebar.isOpen);
} else {
// 可以设置一个默认值或记录日志
console.warn('Sidebar store not ready on init for label:', label);
// isGroupExpanded = true; // 或者 false,取决于你的默认逻辑
}
})">
<div x-show="isGroupExpanded">
<!-- 你的导航组内容 -->
</div>
</div>
解释:
x-data="{ isGroupExpanded: true }"
: 定义了一个本地组件状态isGroupExpanded
,初始值为true
(或者你期望的默认值)。x-init="Alpine.nextTick(() => { ... })"
: 组件初始化时,会执行nextTick
里的回调。- 回调函数内部,再次检查
$store.sidebar
是否就绪。如果就绪,就根据 Store 的状态计算isGroupExpanded
的实际值。 x-show="isGroupExpanded"
: 元素的显示状态由本地的isGroupExpanded
控制,而这个状态的值是在 Store 准备好之后才计算的。
安全建议:
- 这种方式会稍微增加一点点复杂度,因为引入了组件本地状态。
- 如果
nextTick
里的逻辑执行稍晚,可能会有短暂的“闪烁”(从默认状态变为计算后的状态)。
进阶使用:
- 你可以将这个逻辑封装到一个可复用的 Alpine.js 组件中,使得代码更干净。
- 如果 Store 状态会频繁变化,你可能还需要使用
$watch
来监听 Store 的变化并更新本地状态isGroupExpanded
。
// 在 x-init 中添加 watcher
x-init="
// ... initial calculation inside nextTick ...
$watch('$store.sidebar.isOpen', value => { /* re-calculate isGroupExpanded */ });
// 可能还需要监听 groupIsCollapsed 的变化,这取决于其内部实现是否可监听
"
方案四:检查 Filament 版本和自定义代码
最后,别忘了检查是不是环境本身的问题。
原理:
有时候这类问题可能是特定 Filament 或 Alpine.js 版本中的 Bug,或者是你自己添加的某个 JavaScript 代码干扰了 Filament 的正常流程。
操作步骤:
- 检查 Filament 更新日志和 Issues: 去 Filament 的 GitHub 仓库看看最新的 Release Notes 和 Issues,搜索是否有其他人报告了类似的关于侧边栏
$store
或groupIsCollapsed
的问题。可能有已知的 Bug 并且在新版本中修复了。 - 尝试更新 Filament: 如果你没有使用最新版本,备份你的项目,然后尝试更新 Filament 到最新的稳定版。
composer update filament/filament
- 检查自定义 JavaScript: 你是否在 Filament 项目中添加了自定义的 JavaScript 文件,特别是那些也使用了 Alpine.js 或者直接操作了 DOM 的脚本?检查这些脚本是否可能影响了
$store.sidebar
的初始化或状态。尝试暂时禁用这些自定义脚本,看看问题是否消失。 - 简化复现: 尝试在一个新的、干净的 Filament 项目中复现这个侧边栏结构,看是否还会出现同样的问题。如果在新项目中没问题,那大概率是你现有项目的某些配置或代码导致的。
安全建议:
- 在更新任何依赖(如 Filament)之前,务必备份你的项目代码和数据库。
- 仔细阅读更新日志,了解版本之间的不兼容变更。
通常,优先尝试 方案一 (Optional Chaining) ,因为它最简单直接。如果问题依旧,或者你怀疑是初始化时序问题,再考虑 方案二 (x-if
) 或 方案三 (nextTick
) 。如果这些都无效,或者你觉得问题可能更深层,那就该去排查 方案四 (版本和自定义代码) 了。
希望这些方法能帮你搞定那个烦人的 Cannot read properties of null (reading 'includes')
错误,让你的 Filament 应用顺畅跑起来!