Svelte 编译器 Script 警告完全指南:11 个 script 类 compile-warnings 的原理、触发条件与修复方案
本篇指南基于 Svelte 官方警告文档 script.md,逐一讲解 Svelte 编译器在解析组件 <script> 阶段会产生的全部 11 个警告(warning):它们的准确文案、触发条件、源码中的判定逻辑以及修复方式。读完后,你将能够准确读懂 non_reactive_update、state_referenced_locally、store_rune_conflict 等警告背后的检测机制,并知道在什么场景下出现该警告、应如何改写代码消除它。
警告消息的生成机制:从 Markdown 到编译器
在逐条讲解之前,先理解这些警告是如何"长"出来的,有助于你在遇到陌生警告时快速定位。
Svelte 仓库将每类警告的文案定义在 packages/svelte/messages/ 目录下的 Markdown 文件中,按类别拆分:a11y.md、misc.md、options.md、script.md、style.md、template.md。packages/svelte/scripts/process-messages/index.js 会在构建时读取这些 Markdown,生成 packages/svelte/src/compiler/warnings.js(文件头部标注"This file is generated ... Do not edit!")。
生成的文件中有两个关键结构:
- 警告代码注册表:
codes数组(见 warnings.js#L41-L123)列出全部警告代码,包括本文覆盖的custom_element_props_identifier、non_reactive_update、state_referenced_locally、store_rune_conflict等。这个数组同时是@svelte-ignore注释的校验依据——拼错忽略代码会触发unknown_code警告,代码已废弃则会触发legacy_code警告。 - 统一的上报函数
w()(见 warnings.js#L25-L39):每个警告都有对应的导出函数(如non_reactive_update(node, name)),内部都调用w(node, code, message)。w()做了三件事:- 结合
ignore_map检查当前 AST 节点是否被@svelte-ignore覆盖,命中则直接吞掉; - 构造
InternalCompileWarning,携带 code、message 和节点的[start, end]源码位置; - 通过
warning_filter过滤后推入全局warnings数组,最终随编译结果返回给构建工具。
- 结合
每条警告消息尾部都附带 https://svelte.dev/e/<code> 形式的文档链接,方便从终端报错直接跳转到文档。
以下按 script.md 的原始顺序逐条展开。
custom_element_props_identifier:自定义元素的 props 无法推断
警告文案
Using a rest element or a non-destructured declaration with
$props()means that Svelte can't infer what properties to expose when creating a custom element. Consider destructuring all the props or explicitly specifying thecustomElement.propsoption.
触发条件与源码印证
当以 customElement: true 编译组件时,Svelte 需要知道哪些 prop 要暴露为自定义元素的 HTML 属性。检查逻辑位于 VariableDeclarator.js#L72-L83:当 analysis.custom_element 为真且 customElementOptions.props == null 时,若 $props() 的声明形式是"裸标识符"(如 let props = $props())或对象解构中包含 RestElement(如 let { a, ...rest } = $props()),就会对该声明节点发出此警告。
修复方式
<!-- 触发警告 -->
<script>
let { name, ...rest } = $props();
</script>
<!-- 修复方式一:完整解构所有 props -->
<script>
let { name, title } = $props();
</script>
// 修复方式二:显式声明 customElement.props
import { compile } from 'svelte/compiler';
compile(source, {
customElement: true,
customElementOptions: {
props: { name: String, title: String }
}
});
export_let_unused:未被使用的 export let 属性
警告文案
Component has unused export property '%name%'. If it is for external reference only, please consider using
export const %name%
含义与修复
这是 legacy(Svelte 4 风格)export let 组件接口的警告:某个通过 export let 声明的属性在组件内部从未被读取。若该属性只是供外部通过实例 API 访问,应改用 export const;在 runes 模式下则对应"通过 $props() 解构但从未使用的 prop"这类未被消费的状态。
<!-- 触发警告:name 声明了却在模板/逻辑中从未使用 -->
<script>
export let name;
</script>
<p>Hello!</p>
<!-- 若只是给外部引用,改为 export const -->
<script>
export const name = 'world';
</script>
注意在 Svelte 5 中 export let 本身已属于 legacy 语法,长期方案是迁移到 $props()(参见 legacy-props 文档)。
legacy_component_creation:Svelte 5 组件不再是 class
警告文案
Svelte 5 components are no longer classes. Instantiate them using
mountorhydrate(imported from 'svelte') instead.
含义与修复
这条警告直接指向 Svelte 5 最核心的运行时 API 变更:组件不再是 new Component({ target, props }) 这样的 class,必须改用函数式 API:
// 旧写法(触发 legacy_component_creation 警告)
import App from './App.svelte';
const app = new App({ target: document.body, props: { name: 'world' } });
// 新写法
import { mount } from 'svelte';
const app = mount(App, { target: document.body, props: { name: 'world' } });
// 服务端渲染产物的水合同样改用 hydrate
import { hydrate } from 'svelte';
hydrate(App, { target: document.body });
更多细节(包括 bind:this 返回值变化、$set/$on/$destroy 的替代方案)见 v5 迁移指南 "Components are no longer classes" 一节。
non_reactive_update:非 $state 变量被重新赋值
警告文案
%name%is updated, but is not declared with$state(...). Changing its value will not correctly trigger updates
触发条件(文档原文列出的三条,均需在 runes 模式成立)
- 变量未经
$state或$state.raw声明; - 该变量被重新赋值;
- 该变量在响应式上下文(通常是模板)中被读取。
此时改变它的值不会正确触发更新。文档给出的示例:
<script>
let reactive = $state('reactive');
let stale = 'stale';
</script>
<p>This value updates: {reactive}</p>
<p>This value does not update: {stale}</p>
<button onclick={() => {
stale = 'updated';
reactive = 'updated';
}}>update</button>
修复方式:用 $state 包裹声明,即 let stale = $state('stale');。
源码判定逻辑:检测代码在 2-analyze/index.js#L737-L778。编译器遍历 module.scope 与 instance.scope 中所有 kind === 'normal' && reassigned 的 binding,再检查其引用路径:只有当引用直接位于 Fragment(模板表达式)下才告警。从源码结构看,这里有两个精细的豁免规则:
- 若引用路径上出现
FunctionDeclaration/FunctionExpression/ArrowFunctionExpression,说明该变量只是被某个闭包引用,而非直接被模板响应式读取,则跳过; - 若是
bind:this绑定的变量,且不在IfBlock/EachBlock/AwaitBlock/KeyBlock内,则视为"不会变化的 DOM 引用"而豁免(bind:this在会重建节点的块内仍需 state,否则块重建后引用失效)。
<!-- 以下写法不会触发 non_reactive_update:
变量仅被函数闭包引用,不在模板中直接读取 -->
<script>
let log = [];
function record() {
log.push(Date.now()); // 闭包内引用,不触发警告
}
</script>
perf_avoid_inline_class:避免 new class
警告文案
Avoid 'new class' — instead, declare the class at the top level scope
触发条件与源码印证
位于 NewExpression.js#L9-L12:当 new 的目标是 ClassExpression 且当前 function_depth > 0(即不在模块顶层)时触发。性能动机在于:类声明在顶层作用域会被提升到模块/组件作用域,实例化时可直接引用;而 new class {...} 会在每次执行到该表达式时创建一个新的类构造器,无法被缓存复用。
// 触发警告
function createCounter() {
return new class {
count = 0;
increment() { return ++this.count; }
};
}
// 推荐写法
class Counter {
count = 0;
increment() { return ++this.count; }
}
function createCounter() {
return new Counter();
}
perf_avoid_nested_class:避免在顶层以下声明 class
警告文案
Avoid declaring classes below the top level scope
触发条件与源码印证
位于 ClassDeclaration.js#L15-L22。源码中的注释说明了规则:在模块脚本中只允许 function_depth === 0(顶层),在组件实例脚本中允许 function_depth <= 1(组件实例脚本整体被视为组件函数体,深度 1 即"组件作用域的顶层");而 new class 表达式连组件作用域层面也不允许。原因与上一条相同——嵌套 class 声明会在组件每次实例化时重新创建类,产生不必要的重复分配。
<script>
// OK:组件作用域顶层,等价于 function_depth === 1
class Item {}
function make() {
class Nested {} // 触发 perf_avoid_nested_class
return new Nested();
}
</script>
reactive_declaration_invalid_placement:$: 声明位置错误
警告文案
Reactive declarations only exist at the top level of the instance script
含义
这是 legacy 响应式声明($: 语句,Svelte 4 语法)的位置约束:$: doubled = count * 2 只能出现在实例脚本(不带 module 属性的 <script>)的顶层,不能出现在 <script context="module">、函数体内或模板中。runes 模式下等价的能力是 $derived(见 [derived.md?utm_source=gitcode_repo_files)),legacy $: 语法的完整说明见 legacy reactive statements 文档。
<script>
let count = $state(1);
$: doubled = count * 2; // OK:实例脚本顶层
function f() {
$: invalid = count; // 触发 reactive_declaration_invalid_placement
}
</script>
reactive_declaration_module_script_dependency:模块级变量参与响应式语句
警告文案
Reassignments of module-level declarations will not cause reactive statements to update
含义与源码印证
响应式语句(legacy $: 声明)依赖其引用的变量重新赋值来触发更新,但模块脚本(<script module>)中的声明被重新赋值不会触发组件内的响应式语句重新执行——模块作用域没有绑定到组件实例的响应式系统。检测代码在 Identifier.js#L154-L160:当引用发生在响应式语句内(context.state.reactive_statement 为真)、且该 binding 属于 analysis.module.scope、且该变量确实被重新赋值(binding.reassigned)时触发。
<script module>
let theme = 'light';
export { theme };
</script>
<script>
import { theme } from ...; // 模块级引用
$: label = theme === 'light' ? 'dark mode' : 'light mode'; // 触发警告:
// 模块级 theme 重新赋值不会让该响应式语句重新执行
</script>
state_referenced_locally:状态引用被"局部捕获"
警告文案
This reference only captures the initial value of
%name%. Did you mean to reference it inside a %type% instead?
触发条件(文档原文三条)
- 声明了一个响应式变量(
$state/$derived/prop 等); - 该变量之后会被重新赋值;
- 它在同一作用域中被"值捕获式"引用(例如作为函数参数、被
setContext传递)。
这会"断开"与原始 state 声明的链接。文档中的经典场景是把 state 通过 context 传给子组件:
<!--- file: Parent.svelte --->
<script>
import { setContext } from 'svelte';
let count = $state(0);
// warning: state_referenced_locally
setContext('count', count);
</script>
<button onclick={() => count++}>
increment
</button>
<!--- file: Child.svelte --->
<script>
import { getContext } from 'svelte';
const count = getContext('count');
</script>
<!-- This will never update -->
<p>The count is {count}</p>
修复方式:让引用变为惰性求值——把 state 包进函数传递,需要值时再调用读取:
<!--- file: Parent.svelte --->
<script>
import { setContext } from 'svelte';
let count = $state(0);
setContext('count', () => count);
</script>
<button onclick={() => count++}>
increment
</button>
<!--- file: Child.svelte --->
<script>
import { getContext } from 'svelte';
const count = getContext('count');
</script>
<!-- This will update -->
<p>The count is {count()}</p>
更多背景见 [state.md?utm_source=gitcode_repo_files):JavaScript 是 pass-by-value 语言,直接传值就固定了初始值;传递 getter 函数才能拿到"当前值"。
源码判定逻辑:完整条件在 Identifier.js#L104-L152。从源码可以读出几个精确细节:
- 仅 runes 模式生效,且引用位置必须与声明处于同一
function_depth(跨函数边界的不算"本地捕获"); - 对
kind === 'state'的 binding,额外要求"被重新赋值,或初始化表达式是单参数、且参数不可被 proxy 包装的$state调用"——因为可被 proxy/freeze 的非基本类型(如对象、数组)天然支持深层访问,此时值捕获并不致命,警告帮助有限(源码注释原文:"isn't that helpful");raw_state、derived、prop、rest_prop则无此豁免; - 仅针对读操作(排除赋值表达式的左值和自增自减表达式);
- 消息里的
%type%(closure或derived)由沿父节点回溯得到:若捕获点位于某次$derived(...)调用的参数中,提示"是否想引用在 derived 内部";否则提示引用在闭包内部。
store_rune_conflict:$ 前缀局部绑定与 rune 冲突
警告文案
It looks like you're using the
$%name%rune, but there is a local binding called%name%. Referencing a local variable with a$prefix will create a store subscription. Please rename%name%to avoid the ambiguity
含义与源码印证
在 runes 模式下,$name 既可能是对名为 name 的变量的 store 订阅(legacy $store 语法),又可能是用户想使用的 rune。检测逻辑位于 2-analyze/index.js#L403-L413:当一个 $xxx 表达式被识别为 store 订阅,且存在名为 xxx 的局部声明、而该引用实际位于某个 CallExpression(即 $xxx(...) 的调用形式,看起来像 $state(...) 之类的 rune)中时,说明开发者大概率是想写 rune,却撞上了局部变量名——此时应重命名该局部变量以消除歧义。
<script>
// 局部存在名为 `derived` 的变量
let derived = { value: 1 };
// 想调用 $derived rune,但 $derived 被解析为 store 订阅
const result = $derived(derived.value * 2); // 触发 store_rune_conflict
</script>
<script>
// 修复:重命名局部变量
let data = { value: 1 };
const result = $derived(data.value * 2);
</script>
警告速查表
| 警告代码 | 一句话含义 | 触发阶段 | 修复方向 |
|---|---|---|---|
custom_element_props_identifier |
自定义元素无法推断要暴露哪些 prop | 分析 $props() 声明 |
完整解构,或显式配置 customElement.props |
export_let_unused |
export let 属性从未使用 |
分析实例导出 | 删除,或改为 export const |
legacy_component_creation |
用 new Component() 实例化组件 |
用户代码(Svelte 5) | 改用 mount / hydrate |
non_reactive_update |
非 $state 变量被重赋值且在模板读取 |
响应式分析(runes) | 用 $state 声明 |
perf_avoid_inline_class |
new class 出现在非顶层 |
分析 NewExpression |
把类提到顶层作用域 |
perf_avoid_nested_class |
class 声明位于函数深度过深的位置 | 分析 ClassDeclaration |
把类提到组件/模块顶层 |
reactive_declaration_invalid_placement |
$: 声明不在实例脚本顶层 |
分析 legacy 响应式语句 | 移到实例脚本顶层(或迁移到 $derived) |
reactive_declaration_module_script_dependency |
响应式语句依赖模块级可变声明 | 分析标识符引用 | 将模块变量作为只读输入,或改为实例级声明 |
state_referenced_locally |
state 在同一作用域被值捕获 | 分析标识符引用(runes) | 以函数/getter 形式惰性引用 |
store_rune_conflict |
局部变量名与 rune 的 $ 前缀写法冲突 |
分析 store 订阅(runes) | 重命名局部变量 |
延伸阅读
- 警告类别总览:compile-warnings 参考,其余类别(模板、样式、选项、可访问性)分别对应 template.md、style.md、options.md、a11y.md;
- 消息文案的原始定义与生成流程:packages/svelte/messages/compile-warnings/ 与 process-messages 脚本;
- 各警告的触发点实现集中在 packages/svelte/src/compiler/phases/2-analyze/ 分析阶段(各 AST visitor 中调用
w.xxx(...))。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00