Svelte 编译器 Script 警告完全指南:11 个 script 类 compile-warnings 的原理、触发条件与修复方案
2026/9/7 10:25:54 网站建设 项目流程

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

【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

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

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

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

Svelte 仓库将每类警告的文案定义在 packages/svelte/messages/ 目录下的 Markdown 文件中,按类别拆分:a11y.mdmisc.mdoptions.md、script.md、style.mdtemplate.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_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 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 usingexport 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 usingmountorhydrate(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 文档),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 文档 "Passing state into functions" 一节: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_unusedexport let属性从未使用分析实例导出删除,或改为export const
legacy_component_creationnew Component()实例化组件用户代码(Svelte 5)改用mount/hydrate
non_reactive_update$state变量被重赋值且在模板读取响应式分析(runes)$state声明
perf_avoid_inline_classnew class出现在非顶层分析NewExpression把类提到顶层作用域
perf_avoid_nested_classclass 声明位于函数深度过深的位置分析ClassDeclaration把类提到组件/模块顶层
reactive_declaration_invalid_placement$:声明不在实例脚本顶层分析 legacy 响应式语句移到实例脚本顶层(或迁移到$derived
reactive_declaration_module_script_dependency响应式语句依赖模块级可变声明分析标识符引用将模块变量作为只读输入,或改为实例级声明
state_referenced_locallystate 在同一作用域被值捕获分析标识符引用(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(...))。

【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询