Svelte 库是否应该在 rollup.config.js 中包含 external: ['svelte']?({#each} 中的“调用外部组件初始化的函数”getContext)

Tyl*_*ick 5 rollupjs svelte

谁能告诉我为什么这个应用程序会遇到“称为外部组件初始化的函数”错误?更新:找到了这个特定错误的原因,但仍然有以下关于将汇总与 svelte 库一起使用的最佳实践的问题。)

它似乎只在我从循环内的组件(应该允许)调用getContext(或onMount等)时发生。但只有当我包含在库中时才会发生,所以这可能是一个汇总问题而不是一个 Svelte 问题。{#each}external: ['svelte']

这是我的代码(您可以从这里克隆并自己尝试):

  "dependencies": {                                                             
    "my-new-component": "file:packages/my-new-component", 
    …
  }
Run Code Online (Sandbox Code Playgroud)

src/App.svelte

<script>
  import { FieldArray } from "my-new-component";
  import { UsesContext } from "my-new-component";
</script>

<FieldArray let:names>
  {#each names as name, i}
    <div>{name}: <UsesContext /></div>
  {/each}
</FieldArray>
Run Code Online (Sandbox Code Playgroud)

packages/my-new-component/src/FieldArray.svelte

<script>
  let names = ['a']

  const handleClick = () => {
    names = ['a', 'b']
  }
</script>

<button on:click={handleClick}>Blow up</button>

<slot names={names} />
Run Code Online (Sandbox Code Playgroud)

packages/my-new-component/src/UsesContext.svelte

<script>
  import {setContext, getContext} from 'svelte'

  const key = {}
  setContext(key, 'context')
  let context = getContext(key)
</script>

{context}
Run Code Online (Sandbox Code Playgroud)

很基本的东西,对吧?

我究竟做错了什么?

我知道setContext只能在组件初始化期间同步调用(在本<script>节的顶层),并且在组件初始化后(例如从事件处理程序)以异步方式调用 getContext/setContext或任何生命周期方法 ( onMount) 可能导致到(并且可能是最常见的原因)此错误。

但我感到只有调用它同步从顶部级脚本UsesContext.svelte组件...所以不能成为问题,对不对?

唯一令我感到异步做的是更新let的变量。但这允许与 Svelte 异步执行(并且通常执行)的一件事,不是吗?

(当然,这是一个人为的示例,以使其成为尽可能少的可重现示例。在我正在处理的真实库中,我form.registerField从 final-form订阅,并let从该回调异步更新组件的变量...一种在当前版本中运行良好 的方法- 但是当我尝试以此处描述的方式使用它时会导致此错误。)

我不觉得我在做任何 Svelte 不允许的事情。我是吗?

导致错误消失的事情

如果我更改以下任何一个因素(应该没有任何区别),那么一切正常:

  1. 拿走{#each}环。(提交)

    <FieldArray let:names>
      <div>{names}</div>
      <UsesContext />
    </FieldArray>
    
    Run Code Online (Sandbox Code Playgroud)
  2. 同步而不是异步更新变量。(提交)

  3. UsesContext组件从库复制到应用程序中,然后导入组件的本地副本。(提交)

    即使它是组件的相同副本,它在从应用程序中导入时也能工作,但从库中导入时会出错。

  4. 使用组件的本地副本 ( commit ) 或“内联”版本 ( commit ) FieldArray

    为什么从包中导入其中任何一个都不起作用?可能与下一个因素有关......

  5. 删除external: ['svelte']frompackages/my-new-component/rollup.config.js会导致错误消失。(提交)

    请参阅下面的“Svelte 库是否应该使用external: ['svelte']”。

为什么其中任何一个都可以解决问题?它们之间的关系如何?

是谁的错误?

这是一个 Svelte 错误吗?这可能是一个与{#each}循环内初始化/分离组件相关的错误(因为它只发生在我身上)...

但我怀疑这个问题与我使用的库打包代码的方式更直接相关(使用汇总)。特别是,它们是否包含 Svelte 内部代码的额外副本。

Svelte 库应该使用external: ['svelte']?

我的理解是,在构建库时,它们依赖的其他库(如 React 或 Svelte)应同时列在两者下:

  • peerDependencies
  • external: [...]

因此,React/Svelte/etc 的副本不会安装在 node_modules 下(在 的情况下peerDependencies)或内联作为 rollup 构建的 dist 包的一部分(在 rollup 的external选项的情况下)。(见这篇文章。)

与包含 Svelte 使用的最小运行时代码的额外副本相比,包含 React 或 Angular 等大型运行时库的额外副本可能更重要。但是,我担心的并不是捆绑包的大小,而是因为运行多个“Svelte”副本而可能导致的副作用/错误。(当我有多个ReactDOM浮动实例时,我肯定在 React 之前遇到过这样的问题。)

那为什么官方不component-template包括external: ['svelte']?(为什么这条评论建议添加external: ['svelte/internal']而不是external: ['svelte']?谁直接导入from 'svelte/internal'?没关系,我想我找到了这部分的答案。更多内容如下。)

但是,为什么(例如)苗条,urql 使用external其所有的peerDependencies/ devDependencies(含svelte)?他们应该这样做吗?诚然,在他们的情况下,他们目前还没有包含任何纤巧的组件(只是辅助函数和setContext),所以这可能就是为什么它还没有给他们带来任何问题的原因。

具有讽刺意味的是,我认为实际上是这个“函数调用外部组件初始化”错误首先促使我添加了这一external: ['svelte']行。

我注意到在我的应用程序包(使用 webpack 构建)中,它包含了“svelte”的多个副本——我指的是通用函数的多个副本,比如setContext. 这让我很担心,所以我开始尝试弄清楚如何让它在我的包中只包含一个“svelte”的副本。

当我在我的应用程序包中看到多次出现let current_component;/时,我特别担心var current_component

如果您想知道这些“副本”来自哪些库/模块,看起来就是这些(webpack 善意添加的评论):

  • !*** /home/…/svelte-final-form/dist/index.mjs ***!没有 external: ['svelte']

    let current_component;
    function set_current_component(component) {
        current_component = component;
    }
    function get_current_component() {
        if (!current_component)
            throw new Error(`Function called outside component initialization`);
        return current_component;
    }
    function onMount(fn) {
        get_current_component().$$.on_mount.push(fn);
    }
    function onDestroy(fn) {
        get_current_component().$$.on_destroy.push(fn);
    }
    function setContext(key, context) {
        get_current_component().$$.context.set(key, context);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • !*** /home/…/my-new-component/dist/index.mjs ***!( external: ['svelte'])

    let current_component;
    function set_current_component(component) {
        current_component = component;
    }
    
    const dirty_components = [];
    const binding_callbacks = [];
    …
    
    Run Code Online (Sandbox Code Playgroud)

    function get_current_component()甚至没有出现在本节中,显然是因为组件的脚本引用getContext了Svelte的不同外部副本,所以 rollup 的 tree-shaking 注意到它的本地版本get_current_component()未使用,并且不需要包含其定义:)

    function instance$1($$self) {
        console.log("my-new-component UsesContext");
        const key = {};
        Object(svelte__WEBPACK_IMPORTED_MODULE_0__["setContext"])(key, "context");
        let context = Object(svelte__WEBPACK_IMPORTED_MODULE_0__["getContext"])(key);
        return [context];
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • !*** ./node_modules/svelte-forms-lib/build/index.mjs ***!没有 external: ['svelte']

    var current_component;
    
    function set_current_component(component) {
      current_component = component;
    }
    
    function get_current_component() {
      if (!current_component) throw new Error("Function called outside component initialization");
      return current_component;
    }
    
    function setContext(key, context) {
      get_current_component().$$.context.set(key, context);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • !*** ./node_modules/svelte-select/index.mjs ***!没有 external: ['svelte']

    var current_component;
    
    function set_current_component(component) {
      current_component = component;
    }
    
    function get_current_component() {
      if (!current_component) throw new Error("Function called outside component initialization");
      return current_component;
    }
    
    function beforeUpdate(fn) {
      get_current_component().$$.before_update.push(fn);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • !*** ./node_modules/svelte/internal/index.mjs ***!(来自svelte@3.29.0)

    var current_component;
    
    function set_current_component(component) {
      current_component = component;
    }
    
    function get_current_component() {
      if (!current_component) throw new Error("Function called outside component initialization");
      return current_component;
    }
    
    function beforeUpdate(fn) {
      get_current_component().$$.before_update.push(fn);
    }
    
    …
    
    function setContext(key, context) {
      get_current_component().$$.context.set(key, context);
    }
    
    Run Code Online (Sandbox Code Playgroud)

如您所见,每个副本都是“svelte”的一个略有不同的版本(取决于用于构建每个模块的 svelte 版本号,以及由于树抖动而删除了未使用的函数)。

我原来的假设是if (!current_component) throw new Error("Function called outside component initialization");错误腹背受敌,因为每个组件/库为维护自己的副本current_component,所以也许当它从一个应用程序的/库的分量越过边界(“复制”斯维尔特)的其他库的组件(“复制”的 Svelte),current_component即使它在旧范围中正确设置,在新范围中是否未定义?

我还没有排除这个。正是这种预感让我开始尝试通过添加来消除那些额外的“副本” external: ['svelte']——尝试解决错误。

如何external: ['svelte']影响my-new-componentbundle的内容

以下是my-new-component我添加时输出的变化external: ['svelte']

? git diff
diff --git a/dist/index.mjs b/dist/index.mjs
index a0dbbc7..01938f3 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1,3 +1,5 @@
+import { setContext, getContext } from 'svelte';
+
 function noop() { }
 function assign(tar, src) {
     // @ts-ignore
@@ -76,17 +78,6 @@ let current_component;
 function set_current_component(component) {
     current_component = component;
 }
-function get_current_component() {
-    if (!current_component)
-        throw new Error(`Function called outside component initialization`);
-    return current_component;
-}
-function setContext(key, context) {
-    get_current_component().$$.context.set(key, context);
-}
-function getContext(key) {
-    return get_current_component().$$.context.get(key);
-}
 
 const dirty_components = [];
 const binding_callbacks = [];
Run Code Online (Sandbox Code Playgroud)

起初,这看起来是一件非常好的事情,因为这意味着该库可以重用其对等依赖项(安装在应用程序目录中的包)中的setContext,getContext函数(以及可能的任何其他 Svelte API 函数),而不是不必要地包括库包中这些函数的副本。svelte node_modules/

但我越研究这个,我想知道这是否不太正确。在最令人关注的是,尽管一些斯维尔特功能从我的媒体库的JS包消失了,有些人-特别是set_current_componentinit-仍然在捆绑,因为我的库没有明确import他们-这些都是由苗条插入的“内部”的方法编译器...

所以也许这正是导致错误的问题:保留在我的库包中的init/set_current_component函数指的是它们自己的本地作用域current_component,但我专门导入的getContext/setContext最终get_current_component从 Svelte 的不同外部副本调用,它指的是到一个不同的current_component在不同的范围内。

哦,这就是为什么此评论建议添加external: ['svelte/internal']而不是external: ['svelte']!

更新:找到错误的解决方案(至少对于这种特殊情况)!

当我尝试添加'svelte/internal'external列表中时,一堆通用 svelte 函数从我的库包中消失了,取而代之的是更多的 Svelte imports:

+import { SvelteComponent, init, safe_not_equal, text, insert, noop, detach, create_slot, update_slot, transition_in, transition_out } from 'svelte/internal';
 import { setContext, getContext } from 'svelte';
 
-function noop() { }
-function assign(tar, src) {
 …
-let current_component;
-function set_current_component(component) {
-    current_component = component;
-}
Run Code Online (Sandbox Code Playgroud)

现在剩下的唯一几行是特定于特定组件的生成函数 ( create_fragment, create_fragment$1, ...)。这个包现在非常小——从 432 行减少到 148 行。这正是我想要的!最重要的是,它使代码工作(使错误消失)(commit

所以我猜我遇到的问题是因为我只是部分“外部化”了 svelte,所以我的库的包包含对 Svelte 的外部副本和 Svelte 的内部副本的引用的混合......let current_component互相分享他们的副本。

这个错误特别麻烦,因为它可以通过多种方式引起,并且错误并没有揭示问题的确切原因。所以当然这个修复只适用于这个特定的错误原因。

我仍然不确定是什么导致我一次收到此错误(这促使我添加external: ['svelte'])。之前应该是别的什么原因造成的。我猜我正在做一些事情,比如尝试getContextfinal-form异步触发的回调中调用。如果再次发生这种情况,至少我会做好更好的准备并且知道这次如何解决它(可能将 移动getContext()到脚本标记的顶部并使用商店来处理异步回调)。

问题

为了将所有这些放在一起,这里有一些我非常想了解的高级问题:

  • Svelte 是否是“应用程序及其一个或多个依赖项都应使用的库应在这些依赖项中列出”这一一般原则的一个例外peerDependenciesexternal以便最终结果中只有这些库的一个副本? app bundle?或者这个原则听起来不错,但我只是做错了什么?

  • 在我的应用程序的 .js 包中有多个current_component/副本是否可以预期/可以get_current_component()?或者我应该担心看到这个?

  • 如果预计的多个副本current_component(包含来自多个库组件的应用程序),怎么办他们自己之间“斯维尔特”的各个副本协调?或者他们不需要因为每个组件类都是独立的?

    例如,我可能会担心,当一个组件传递到“下一个 Svelte 实例”(我认为是它的子组件)时,这里的current_component/parent_component将是未定义的(但也许这无关紧要?):

    ? git diff
    diff --git a/dist/index.mjs b/dist/index.mjs
    index a0dbbc7..01938f3 100644
    --- a/dist/index.mjs
    +++ b/dist/index.mjs
    @@ -1,3 +1,5 @@
    +import { setContext, getContext } from 'svelte';
    +
     function noop() { }
     function assign(tar, src) {
         // @ts-ignore
    @@ -76,17 +78,6 @@ let current_component;
     function set_current_component(component) {
         current_component = component;
     }
    -function get_current_component() {
    -    if (!current_component)
    -        throw new Error(`Function called outside component initialization`);
    -    return current_component;
    -}
    -function setContext(key, context) {
    -    get_current_component().$$.context.set(key, context);
    -}
    -function getContext(key) {
    -    return get_current_component().$$.context.get(key);
    -}
     
     const dirty_components = [];
     const binding_callbacks = [];
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果 Svelte 的不同“副本”实际上是 svelte 包的不同版本怎么办?如果它们彼此交互但具有不同的 API,那会不会导致错误?(或者也许组件类的外部 API 是稳定的,所以内部 API 是否不同都没有关系?)

    • 好的一点peerDependencies是,您的应用程序中只有每个副本的一个副本。在您的应用程序中拥有多个副本似乎是错误的。但后来我一直想知道 Svelte 是否是该规则的一个例外,因为它将组件编译成独立的类(可以单独使用或一起使用),而不需要一个单一的运行时将它们组合成一个统一的组件像 React 一样的树?Svelte 是否也不需要类似的东西来处理可能跨越库/组件边界的上下文和存储?Svelte 的工作原理对我来说仍然是个谜。
  • 如果有关于如何使用 Svelte 库external来避免此类潜在问题的最佳实践?如果是这样,我们可以通过将它包含在组件模板中来规范化它吗?(我会在那里打开一个问题。)

  • 必须同时列出'svelte/internal'和似乎很奇怪'svelte'。似乎svelte/internal应该是 svelte 的消费者不必担心的实现细节(关于 svelte 如何在内部组织其源代码树)。为什么这是必要的,有什么办法可以改变苗条,这样就没有必要同时列出两者?

    • 我从未见过任何需要奇数后缀的其他软件包的示例,例如/internal添加到externals. 您看到的所有示例(如在文档中)只是主库名称本身:

      external: ['some-externally-required-library'],

      external: ['d3'],

      为什么 svelte 是通常惯例的一个例外?

dub*_*icz 2

不确定它是否与 Sapper 有关,但是,我来这里是因为当我svelte从我的 Sapper 应用程序中devDependencies移入时遇到了这个问题dependencies。该问题表现为 SapperApp组件抛出异常

组件初始化外部调用的函数

tl;dr - 保留svelte在 devDependencies 中。

我相信 Sapper 创建svelte/internal并拥有 Sapper 的内部副本和常规副本(现在在调用时也存在NODE_ENV=production yarn install)会导致问题。

感谢您的详细撰写 - 我从来没有想过要查找package.json这个问题!