首页
/ Svelte 编译器 Script 警告完全指南:11 个 script 类 compile-warnings 的原理、触发条件与修复方案

Svelte 编译器 Script 警告完全指南:11 个 script 类 compile-warnings 的原理、触发条件与修复方案

2026-09-06 15:46:50作者:咎竹峻Karen

本篇指南基于 Svelte 官方警告文档 script.md,逐一讲解 Svelte 编译器在解析组件 <script> 阶段会产生的全部 11 个警告(warning):它们的准确文案、触发条件、源码中的判定逻辑以及修复方式。读完后,你将能够准确读懂 non_reactive_updatestate_referenced_locallystore_rune_conflict 等警告背后的检测机制,并知道在什么场景下出现该警告、应如何改写代码消除它。

警告消息的生成机制:从 Markdown 到编译器

在逐条讲解之前,先理解这些警告是如何"长"出来的,有助于你在遇到陌生警告时快速定位。

Svelte 仓库将每类警告的文案定义在 packages/svelte/messages/ 目录下的 Markdown 文件中,按类别拆分:a11y.mdmisc.mdoptions.mdscript.mdstyle.mdtemplate.mdpackages/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_identifiernon_reactive_updatestate_referenced_locallystore_rune_conflict 等。这个数组同时是 @svelte-ignore 注释的校验依据——拼错忽略代码会触发 unknown_code 警告,代码已废弃则会触发 legacy_code 警告。
  • 统一的上报函数 w()(见 warnings.js#L25-L39):每个警告都有对应的导出函数(如 non_reactive_update(node, name)),内部都调用 w(node, code, message)w() 做了三件事:
    1. 结合 ignore_map 检查当前 AST 节点是否被 @svelte-ignore 覆盖,命中则直接吞掉;
    2. 构造 InternalCompileWarning,携带 code、message 和节点的 [start, end] 源码位置;
    3. 通过 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 the customElement.props option.

触发条件与源码印证

当以 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 mount or hydrate (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 模式成立)

  1. 变量未经 $state$state.raw 声明;
  2. 该变量被重新赋值;
  3. 该变量在响应式上下文(通常是模板)中被读取。

此时改变它的值不会正确触发更新。文档给出的示例:

<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.scopeinstance.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文档](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/03derived.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?

触发条件(文档原文三条)

  1. 声明了一个响应式变量($state/$derived/prop 等);
  2. 该变量之后会被重新赋值;
  3. 它在同一作用域中被"值捕获式"引用(例如作为函数参数、被 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文档"Passingstateintofunctions"一节](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/02state 文档 "Passing state into functions" 一节](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/02-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_statederivedproprest_prop 则无此豁免;
  • 仅针对读操作(排除赋值表达式的左值和自增自减表达式);
  • 消息里的 %type%closurederived)由沿父节点回溯得到:若捕获点位于某次 $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) 重命名局部变量

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐