返回

解决 Filament Alpine.js includes null 报错 (4种方案)

php

解决 Filament 中的 Alpine.js 报错:Cannot read properties of null (reading 'includes')

写代码的时候,碰见 Cannot read properties of nullCannot 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 是否在这个数组里。

所以,问题根源就比较清晰了:

  1. $store.sidebar 本身可能是 nullundefined 在 Alpine.js 组件初始化,或者页面某些状态下,$store.sidebar 可能还没被 Filament 完全准备好。这时候直接访问 $store.sidebar.groupIsCollapsed 就会出错(虽然错误可能发生在更深层的 includes 调用上,但源头是 $store 还没就位)。
  2. groupIsCollapsed 函数内部依赖的数据是 null 即使 $store.sidebar 存在,groupIsCollapsed 函数内部用来存储折叠组状态的那个(推测的)数组,在某个时刻可能是 nullundefined。当它尝试在这个 null 数组上调用 .includes(label) 时,错误就发生了。

这通常和组件加载、初始化的时序有关。Alpine.js 需要时间来初始化它的 Store 和组件,尤其是在动态加载内容或者复杂交互的场景下。

怎么解决?

知道了原因,解决起来就有方向了。下面提供几个可行的方案,你可以根据自己的情况选择。

方案一:添加空值检查(防御性编程)

这是最常见也最直接的法子。在访问可能为 nullundefined 的对象的属性或方法之前,先检查它是否存在。

原理:

通过增加判断条件,确保只有在 $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 不是 nullundefined
  • typeof $store.sidebar.groupIsCollapsed === 'function' (可选但更严谨) 确保 groupIsCollapsed 是个函数再调用。但在这种场景下,如果 $store.sidebar 结构固定,只检查 $store.sidebar 存在通常就够了。

方法 B:使用 Optional Chaining (?.)

现代 JavaScript (Alpine.js 支持) 提供了 Optional Chaining 操作符 (?.),它允许你读取嵌套对象深处的属性值,而无需显式验证链中的每个引用是否有效。如果某个引用是 nullundefined,表达式会短路并返回 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.sidebarnullundefined,整个表达式直接返回 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-initAlpine.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 的正常流程。

操作步骤:

  1. 检查 Filament 更新日志和 Issues: 去 Filament 的 GitHub 仓库看看最新的 Release Notes 和 Issues,搜索是否有其他人报告了类似的关于侧边栏 $storegroupIsCollapsed 的问题。可能有已知的 Bug 并且在新版本中修复了。
  2. 尝试更新 Filament: 如果你没有使用最新版本,备份你的项目,然后尝试更新 Filament 到最新的稳定版。 composer update filament/filament
  3. 检查自定义 JavaScript: 你是否在 Filament 项目中添加了自定义的 JavaScript 文件,特别是那些也使用了 Alpine.js 或者直接操作了 DOM 的脚本?检查这些脚本是否可能影响了 $store.sidebar 的初始化或状态。尝试暂时禁用这些自定义脚本,看看问题是否消失。
  4. 简化复现: 尝试在一个新的、干净的 Filament 项目中复现这个侧边栏结构,看是否还会出现同样的问题。如果在新项目中没问题,那大概率是你现有项目的某些配置或代码导致的。

安全建议:

  • 在更新任何依赖(如 Filament)之前,务必备份你的项目代码和数据库。
  • 仔细阅读更新日志,了解版本之间的不兼容变更。

通常,优先尝试 方案一 (Optional Chaining) ,因为它最简单直接。如果问题依旧,或者你怀疑是初始化时序问题,再考虑 方案二 (x-if)方案三 (nextTick) 。如果这些都无效,或者你觉得问题可能更深层,那就该去排查 方案四 (版本和自定义代码) 了。

希望这些方法能帮你搞定那个烦人的 Cannot read properties of null (reading 'includes') 错误,让你的 Filament 应用顺畅跑起来!