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_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 文档),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 文档 "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_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(...))。
【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考