Следует ли библиотекам Svelte включать external: ['svelte'] в rollup.config.js? (Функция вызывается вне инициализации компонента getContext в {#each})

Может ли кто-нибудь сказать мне, почему это приложение запускает функцию, называемую ошибкой инициализации внешнего компонента? (Обновление: обнаружена причина этой конкретной ошибки, но все еще есть вопросы ниже о лучших практики использования свертки со стройными библиотеками.)

Кажется, это происходит только тогда, когда я вызываю getContext (или onMount и т. Д.) Из компонента (что должно быть разрешено) внутри цикла {#each}. Но это произойдет только в том случае, если я включу external: ['svelte'] в библиотеку, так что это может быть скорее вопрос для накопления, чем вопрос Svelte.

Вот мой код (который вы можете клонировать с здесь и попробовать сами):

  "dependencies": {                                                             
    "my-new-component": "file:packages/my-new-component", 
    …
  }

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>

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} />

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

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

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

{context}

Довольно простые вещи, правда?

Что я делаю неправильно?

Я понимаю, что setContext можно вызывать только синхронно во время инициализации компонента (на верхнем уровне раздела <script>) и что вызов _14 _ / _ 15_ или любых методов жизненного цикла (onMount) асинхронно после инициализации компонента (например, из обработчик событий) может привести (и, вероятно, является наиболее частой причиной) этой ошибки.

Но я я вызываю его только синхронно из сценария верхнего уровня компонента UsesContext.svelte ... так что это не может быть проблемой, верно?

Единственное, что я делаю асинхронно, - это обновляю переменную let. Но это единственное, что разрешено делать (и обычно это делается) асинхронно со Svelte, не так ли?

(Конечно, это надуманный пример, чтобы сделать его как можно более минимальным воспроизводимым примером. В разделе настоящая библиотека, над которой я работаю, я подписываюсь на form.registerField из окончательной формы и асинхронное обновление let переменных компонента из этого обратного вызова ... подход, который отлично работает в текущей версии - но вызывает эту ошибку, когда я пытаюсь использовать его в способ, описанный здесь.)

Я не чувствую, что делаю что-то, что запрещено в Svelte. Я?

Вещи, из-за которых ошибка исчезает

Если я изменю один из следующих факторов (что не должно иметь никакого значения), все будет работать нормально:

  1. Уберите петлю {#each}. (совершить)

    <FieldArray let:names>
      <div>{names}</div>
      <UsesContext />
    </FieldArray>
    
  2. Обновлять переменную синхронно, а не асинхронно. (совершить)

  3. Скопируйте компонент UsesContext из библиотеки в приложение и вместо этого импортируйте локальную копию компонента. (совершить)

    Несмотря на то, что это идентичная копия компонента, она работает при импорте из приложения, но с ошибками при импорте из библиотеки.

  4. Используйте локальную копию (фиксация версии () или встроенная версия //github.com/TylerRick/repro_getContext_from_comp_inside_each/commit/2885063cdb327afea731bcc3569018048af2aa6e "rel =" nofollow noreferrer "> commit) компонента FieldArray.

    Почему это не работает, если любой из них импортирован из пакета? Может быть связано со следующим фактором ...

  5. Удаление external: ['svelte'] из packages/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) или встроена как часть пакета dist, который строится накопительным пакетом (в случае параметра external накопления). (См. эту статью.)

Вероятно, гораздо важнее включить дополнительную копию гигантской библиотеки времени выполнения, такой как React или Angular, чем включить дополнительную копию минимального кода времени выполнения, используемого Svelte. Но меня беспокоит не столько размер пакета, сколько возможные побочные эффекты / ошибки, которые могут возникнуть из-за наличия более чем одной копии Svelte. (Я определенно сталкивался с подобными проблемами раньше с React, когда у меня было несколько экземпляров ReactDOM, плавающих вокруг.)

Так почему же нет официального component-template в том числе external: ['svelte']? (И почему этот комментарий предлагает добавить external: ['svelte/internal'], а не external: ['svelte'] ? Кто импортирует напрямую from 'svelte/internal'? Неважно, я думаю, что нашел ответ на этот вопрос. Подробнее ниже.)

Но почему (например) svelte-urql использовать external для всех из его _41 _ / _ 42_ (включая svelte)? Должны ли они не делать этого? Конечно, в их случае они в настоящее время не включают каких-либо изящных компонентов (только вспомогательные функции и setContext), так что может быть поэтому это еще не вызвало у них никаких проблем.

По иронии судьбы, я считаю, что на самом деле эта функция, вызванная ошибкой инициализации внешнего компонента, сначала побудила меня добавить эту external: ['svelte'] строку.

В пакете моего приложения (созданном с использованием webpack) я заметил, что он включает несколько копий svelte - и под этим я имею в виду несколько копий общих функций, таких как setContext. Это обеспокоило меня, поэтому я решил попытаться выяснить, как сделать так, чтобы в мой комплект была включена только одна копия svelte.

Я был особенно обеспокоен, когда увидел несколько случаев появления _47 _ / _ 48_ в моем наборе приложений.

Если вам интересно, из каких библиотек / модулей берутся эти копии, похоже, вот эти (комментарии любезно добавлены веб-пакетом):

  • !*** /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);
    }
    
  • !*** /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 = [];
    …
    

    (function get_current_component() даже не фигурировал в этом разделе, по-видимому, потому что сценарий компонента ссылается на getContext из другой внешней копии Svelte, поэтому при раскачивании дерева было замечено, что его локальная версия 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];
    }
    
  • !*** ./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);
    }
    
  • !*** ./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);
    }
    
  • !*** ./node_modules/svelte/internal/index.mjs ***![email protected])

    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);
    }
    

Как видите, каждая копия представляет собой немного отличающуюся версию svelte (в зависимости от номера версии svelte, используемой для сборки каждого модуля, и от того, какие неиспользуемые функции были удалены из-за встряхивания дерева).

Моя первоначальная гипотеза заключалась в том, что ошибка if (!current_component) throw new Error("Function called outside component initialization"); была обнаружена , потому что каждый компонент / библиотека поддерживал свою собственную копию current_component, поэтому, возможно, когда она пересекала границу от одного компонента приложения / библиотеки (копия Svelte) до компонент другой библиотеки (копия Svelte), current_component не был определен в этой новой области, хотя он был правильно установлен в старой области?

Я до сих пор этого не исключил. И это предчувствие заставило меня попытаться уничтожить эти лишние копии, добавив в первую очередь external: ['svelte'] - чтобы попытаться устранить ошибку.

Как external: ['svelte'] влияет на содержимое my-new-component пакета

Вот как изменяется вывод 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 = [];

Сначала это выглядело действительно хорошо, потому что это означало, что эта библиотека может повторно использовать функции setContext, getContext (и, предположительно, любые другие функции Svelte API) из своей одноранговой зависимости - пакета svelte, установленного в приложении < / em> node_modules/ dir - вместо того, чтобы без необходимости включать дублирующую копию этих функций в пакет библиотеки.

Но чем больше я вглядываюсь в это, я задаюсь вопросом, не совсем ли это правильно. Больше всего беспокоит то, что хотя некоторые функции Svelte исчезли из пакета JS моей библиотеки, некоторые из них - особенно set_current_component и init - остались в пакете, потому что моя библиотека не import их специально - это внутренние методы, вставленные компилятором Svelte ...

Так что, возможно, именно эта проблема вызвала ошибку: функции _84 _ / _ 85_, которые остаются в пакете моей библиотеки, ссылаются на свои собственные current_component с локальной областью видимости, но _87 _ / _ 88_, который я специально импортировал, в конечном итоге вызывает 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;
-}

Единственными строками, которые остались теперь, являются сгенерированные функции (create_fragment, create_fragment$1,…), которые относятся к конкретным компонентам. Пакет теперь очень маленький - 148 строк, по сравнению с 432. Это именно то, что я хотел! Лучше всего то, что он заставляет код работать (устраняет ошибку) (совершить)

Итак, я предполагаю, что проблема, с которой я столкнулся, заключается в том, что я только частично внешне реализовал svelte, поэтому пакет моей библиотеки содержал смесь ссылок на внешнюю копию Svelte и внутреннюю копию Svelte ... что не могло ' t видеть друг друга или делиться своими экземплярами let current_component друг с другом.

Эта ошибка особенно неприятна, поскольку может быть вызвана разными способами, и ошибка не раскрывает точную причину проблемы. Конечно, это исправление применимо только к этой конкретной причине ошибки.

Я до сих пор не уверен, что заставило меня получить эту ошибку первый раз (это побудило меня добавить external: ['svelte']). Раньше это должно было быть вызвано чем-то другим. Я предполагаю, что делал что-то вроде попытки вызвать getContext из обратного вызова, который был запущен final-form асинхронно. Если это произойдет снова, по крайней мере, я буду лучше подготовлен и буду знать, как решить эту проблему на этот раз (возможно, переместите getContext() в верхнюю часть тега сценария и используйте хранилища для обработки асинхронных обратных вызовов).

Вопросов

Чтобы собрать все это воедино, вот несколько общих вопросов, которые мне действительно хотелось бы понять:

  • Является ли Svelte исключением из общего принципа, согласно которому библиотеки, которые, как ожидается, будут использоваться приложением, и одна или несколько его зависимостей должны быть указаны в peerDependencies и external этих зависимостей, чтобы только одна копия этих библиотек попала в итоговый пакет приложений? Или этот принцип звучит, но я просто делаю что-то не так?

  • Ожидается ли / нормально ли, что в пакете .js моего приложения будет несколько копий _106 _ / _ 107_? Или я должен быть обеспокоен этим?

  • Если ожидается наличие нескольких копий current_component (в приложении, содержащем компоненты из нескольких библиотек), как различные копии Svelte координируются между собой? Или они не нужны, потому что каждый класс компонентов самодостаточен?

    Например, меня может беспокоить то, что когда компонент переходит к следующему экземпляру Svelte (я полагаю, его дочерние компоненты), _109 _ / _ 110_ здесь будет неопределенным (но, может быть, это не имеет значения?):

    function init(component, options, instance, create_fragment, not_equal, props) {
      var dirty = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : [-1];
      var parent_component = current_component;
      set_current_component(component);
      …
      set_current_component(parent_component);
    }
    
  • Что, если разные копии Svelte на самом деле являются разными версиями пакета svelte? Не может ли это вызвать ошибки, если они взаимодействуют друг с другом, но имеют разные API? (Или, может быть, внешние API класса компонента стабильны, поэтому не имеет значения, отличается ли внутренний API?)

    • The nice thing about peerDependencies is that you only have one copy of each of them in your app. It just seems wrong to have multiple copies have a library in your app. But then I keep wondering if Svelte is an exception to that rule because it compiles components into self-contained classes (that can be used stand-alone or together) rather than needing a single run-time to tie them together into a single unified component tree like React? Does Svelte not need something like that, too, in order to handle context and stores that may cross library/component boundaries? How Svelte works is still too much of a mystery to me.
  • Есть ли лучший способ использования библиотеки Svelte external, чтобы избежать подобных проблем? И если да, можем ли мы канонизировать его, включив в шаблон компонента? (Я открою там вопрос.)

  • Очень странно перечислять и 'svelte/internal', и 'svelte'. Кажется, что svelte/internal должна быть деталью реализации (того, как svelte организовал свое дерево исходных текстов внутри), о которой потребители svelte не должны беспокоиться. Почему это необходимо и есть ли способ изменить svelte так, чтобы не указывать и то, и другое?

    • Я никогда не видел примеров других пакетов, которые требуют нечетного суффикса, такого как /internal, при добавлении к externals. Все примеры, которые вы видите (например, в docs), являются просто основным именем библиотеки:

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

      external: ['d3'],

      Почему стройность - исключение из этого обычного соглашения?


person Tyler Rick    schedule 01.10.2020    source источник


Ответы (1)


Не уверен, связано ли это с Sapper, но я здесь, потому что столкнулся с этой проблемой, когда переместил svelte из devDependencies в dependencies в моем приложении Sapper. Проблема проявляется в том, что компонент Sapper App бросает

Функция вызывается вне инициализации компонента

tl; dr - сохранить svelte в devDependencies.

Я считаю, что Sapper создает svelte/internal, и наличие как внутренней копии Sapper, так и обычной копии (теперь также присутствующей при вызове NODE_ENV=production yarn install) вызывает проблемы.

Спасибо за подробное описание - я бы никогда не подумал заглянуть в package.json по поводу этой проблемы!

person dubaniewicz    schedule 16.10.2020
comment
Да, есть разные вещи, которые могут вызвать ошибку Function called outside component initialization. Я думаю, у вас была другая причина, но я рад, что вы поняли это и что моя рецензия немного помогла. Я удивлен, что перенос его на devDependencies может иметь здесь большое значение. Я буду помнить об этом, если когда-нибудь буду использовать Sapper в проекте. - person Tyler Rick; 21.10.2020
comment
Кстати, об этой ошибке в этом саперном выпуске шла целая дискуссия. github.com/sveltejs/sapper/issues/592#issuecomment-469924549 пришли к такому же выводу, что и вы: похоже, проблема связана с наличием svelte в качестве зависимости вместо devDependencies в package.json внутри проекта sapper. - person Tyler Rick; 22.10.2020
comment
Я надеюсь / хочу, чтобы было что-то, что можно было бы сделать, чтобы людям было сложнее случайно получить две копии svelte и столкнуться с этой запутанной ошибкой ... - person Tyler Rick; 22.10.2020