哎呀,这有点难。请稍等。
你实际上缺少的是 "链接",也就是将编译后的代码中的 import
语句解析为浏览器可使用的内容。这通常由捆绑器(例如 Rollup、Webpack...)完成。
这些导入可以来自用户(小部件开发人员)代码。例如:
import { onMount } from 'svelte'
import { readable } from 'svelte/store'
import { fade } from 'svelte/transition'
import Foo from './Foo.svelte'
或者它们可以由编译器注入,这取决于组件中使用的功能。例如:
import {
SvelteComponent,
detach,
element,
init,
insert,
noop,
safe_not_equal,
} from 'svelte/internal'
Svelte将.svelte
编译成.js
,以及可选的.css
,但不处理您代码中的导入。相反,它增加了一些(但仍未解决它们,这超出了其范围)。
您需要解析编译后的代码,以查找那些从编译器中提取出来的导入,这些导入可能指向您文件系统和node_modules
目录中的路径,并将它们重写为对浏览器有意义的内容即URL...
看起来并不是很有趣,对吧?(或太多,这取决于您如何看待事物...)幸运的是,您在这个需求上并不孤单,我们拥有非常强大的工具专门用于此任务:打包程序!
解决链接问题
解决这个问题的一个相对简单的方法(更多方法即将到来,请不要过早激动)是使用Rollup和Svelte插件编译小部件,而不是使用Svelte的编译器API。
Svelte插件基本上做了你用编译器API所做的事情,但Rollup也会完成所有繁重的工作,重构导入和依赖关系,以产生一个整洁的小包(捆绑包),可以被浏览器消费(即不依赖于您的文件系统)。
您可以使用类似于这样的Rollup配置编译一个小部件(这里是Foo.svelte
):
rollup.config.Foo.js
import svelte from 'rollup-plugin-svelte'
import commonjs from '@rollup/plugin-commonjs'
import resolve from '@rollup/plugin-node-resolve'
import css from 'rollup-plugin-css-only'
import { terser } from 'rollup-plugin-terser'
const production = !process.env.ROLLUP_WATCH
const emitCss = false
const cmp = 'Foo'
export default {
input: `widgets/${cmp}.svelte`,
output: {
format: 'es',
file: `public/build/widgets/${cmp}.js`,
sourcemap: true,
},
plugins: [
svelte({
emitCss,
compilerOptions: {
dev: !production,
},
}),
emitCss && css({ output: `${cmp}.css` }),
resolve({
browser: true,
dedupe: ['svelte'],
}),
commonjs(),
production && terser(),
],
}
这里没有什么特别的……这基本上是官方Svelte模板的Rollup配置,减去了与开发服务器相关的部分。
使用上述配置的命令如下:
rollup --config rollup.config.Foo.js
你将在 public/build/Foo.js
中获取到可用于浏览器的编译好的 Foo 小部件!
Rollup 还具有 JS API,因此您可以根据需要从Web服务器或其他地方以编程方式运行它。
然后,您将能够通过以下方式动态导入并使用此模块:
const widget = 'Foo'
const url = `/build/widgets/${widget}.js`
const { default: WidgetComponent } = await import(url)
const cmp = new WidgetComponent({ target, props })
在您的情况下,动态导入可能是必要的,因为您在构建主应用程序时不会知道小部件的存在--因此您需要像上面一样在运行时动态构造导入URL。请注意,导入URL是动态字符串的事实将防止Rollup在捆绑时尝试解析它。这意味着导入将以上述方式在浏览器中结束,并且它必须是浏览器能够解析的 URL (而不是您计算机上的文件路径)。
这是因为我们使用浏览器本地的动态导入来消耗编译后的小部件,我们需要在Rollup配置中设置output.format
为es
。Svelte组件将以export default ...
语法公开,现代浏览器可以自然理解它们。
动态导入在当前浏览器中得到很好的支持。值得注意的例外是“旧”的Edge(在它基本上成为Chrome之前)。如果您需要支持旧版本的浏览器,则可以使用polyfill(实际上有许多--例如dimport)。
此配置可以进一步自动化,以便能够编译任何小部件,而不仅仅是Foo
。例如,像这样:
rollup.config.widget.js
...
export default ({ configWidget: cmp }) => ({
input: `widgets/${cmp}.svelte`,
output: {
...
file: `public/build/widgets/${cmp}.js`,
},
...
})
那么你可以像这样使用它:
rollup --config rollup.config.widget.js --configTarget Bar
我们正在取得进展,但仍有一些注意事项和障碍需要注意(并且可能需要进一步优化——由您决定)。
注意事项:共享依赖项
以上方法应该为您的部件提供已编译代码,在浏览器中运行,没有未解析的导入。很好。然而,它是通过在构建给定部件时解决其所有依赖关系,并将所有这些依赖关系捆绑在同一文件中来实现的。
换句话说,所有在多个部件之间共享的依赖关系都将被复制到每个部件中,特别是 Svelte 依赖项(即从svelte
或svelte/*
导入的内容)。这不完全是坏事,因为它为您提供了非常独立的小部件……不幸的是,这也增加了您的小部件代码的一些负担。我们在讨论每个小部件中可能会添加大约20-30 kb的 JavaScript。
此外,正如我们很快将看到的那样,在应用程序中具有 Svelte 内部的独立副本也有一些缺点,我们需要考虑到这些问题……
一种简单的方法是提取共同依赖项,以便它们可以共享而不是重复。这对于所有用户的所有小部件来说可能并不可行,但也许在个别用户级别上可以实现?
无论如何,这里有一个一般想法。您将更改上面的 Rollup 配置,变成像这样:
rollup.config.widget-all.js
...
export default {
input: ['widgets/Foo.svelte', 'widgets/Bar.svelte', ...],
output: {
format: 'es',
dir: 'public/build/widgets',
},
...
}
我们传递了一个文件数组,而不是只传递一个文件作为输入(你可能会通过列出给定目录中的文件来自动完成此步骤),并且我们将 output.file
更改为 output.dir
,因为现在我们将一次生成多个文件。这些文件将包括 Rollup 提取的小部件的共同依赖项,并且所有小部件都将共享它们以进行重用。
进一步的展望
甚至可以进一步推动,手动提取一些共享依赖项(例如 Svelte...)并将其作为 URL 提供给浏览器(即使用 Web 服务器提供它们)。这样,您可以将编译后的代码中的这些导入重写为已知的 URL,而不是依靠 Rollup 解决它们。
这将完全减少代码重复,节省空间,也将允许所有使用它们的小部件共享这些依赖项的单个版本。这样做还将解除构建共享依赖项的所有小部件的需求,这很有吸引力...但是,这将非常(!)复杂设置,并且您会很快遇到收益递减的情况。
实际上,当您将一堆或甚至只有一个小部件捆绑在一起并让 Rollup 提取依赖项时,捆绑器可以知道消费代码实际上需要哪些依赖项的部分,并跳过其余部分(记住:Rollup 的主要优先事项之一是树摇优化,而 Svelte 是由同一位开发者构建的,这意味着您可以期望 Svelte 很好地支持树摇!)。另一方面,如果您手动提取某些依赖项:它会减轻一次捆绑所有消费代码的需求,但您将不得不公开整个已使用依赖项,因为您无法预先知道其中将需要的部分。
这是您需要在效率和实际情况之间找到的平衡点,考虑到每种解决方案对您设置的增加的复杂性。鉴于您的用例,我自己的感觉是甜蜜点可能是完全独立地捆绑每个小部件,或者将来自同一用户的一堆小部件捆绑在一起以节省一些空间,如上所述。再进一步可能会是一个有趣的技术挑战,但它只会带来一点额外的收益,却是爆炸性的复杂...
好的,现在我们知道如何为浏览器捆绑我们的小部件了。我们甚至可以对如何打包我们的小部件完全独立或者承担一些额外的基础设施复杂度来共享它们之间的依赖项有某种程度的控制。现在,我们需要考虑一个特殊的依赖关系,那就是 Svelte 本身...
注意陷阱:不能复制 Svelte
因此,我们明白了当我们使用 Rollup 捆绑单个小部件时,所有依赖项都将包含在“捆绑包”中(在这种情况下只有一个小部件文件)。如果以这种方式捆绑两个小部件并且它们共享一些
<script>
import Foo from '/build/widgets/Foo.js'
</script>
<Foo />
<svelte:component this={Foo} />
这也是为什么官方 Svelte 模板的 Rollup 配置中有 dedupe: ['svelte']
选项的原因…… 这旨在防止捆绑不同版本的 Svelte,例如如果您曾经使用过链接包。
无论如何,在您的情况下,很难避免在浏览器中出现多个 Svelte 的副本,因为您可能不想在用户添加或更改其小部件时重新构建整个主应用程序……除非您花费很大力气来提取、集中和重写 Svelte 导入;但是,正如我所说,我不相信这将是一个合理和可持续的方法。
所以我们陷入了困境。
或者说我们吗?
重复的 Svelte 副本只会在冲突组件属于同一组件树时出现问题。也就是说,当你让 Svelte 创建和管理组件实例时,像上面那样。当您自己创建和管理组件实例时,问题不存在。
...
const foo = new Foo({ target: document.querySelector('#foo') })
const bar = new Bar({ target: document.querySelector('#bar') })
在Svelte中,foo
和bar
将被视为完全独立的组件树。无论何时以及如何编译和捆绑这些组件(以及使用哪个Svelte版本等),这样的代码始终起作用。
据我所知,这对您的使用情况并不是一个主要障碍。您将无法像<svelte:component />
那样将用户小部件嵌入到您的主应用程序中...但是,您可以在适当的位置自己创建和管理小部件实例。您可以创建一个包装器组件(在您的主应用程序中),以通用化此方法。类似于:
Widget.svelte
<script>
import { onDestroy } from 'svelte'
let component
export { component as this }
let target
let cmp
const create = () => {
cmp = new component({
target,
props: $$restProps,
})
}
const cleanup = () => {
if (!cmp) return
cmp.$destroy()
cmp = null
}
$: if (component && target) {
cleanup()
create()
}
$: if (cmp) {
cmp.$set($$restProps)
}
onDestroy(cleanup)
</script>
<div bind:this={target} />
我们从主应用程序创建一个目标DOM元素,在其中渲染“外部”组件,将所有属性传递下去(我们代理响应),并在代理组件销毁时不要忘记清除。
这种方法的主要限制是,应用程序的Svelte上下文(setContext / getContext)对于代理组件不可见。
再次强调,这在小部件使用情况下似乎并不是问题 - 甚至更好:我们是否真的希望小部件可以访问周围应用程序的每个细节? 如果确实需要,您可以通过props向小部件组件传递上下文位。
上述Widget代理组件在您的主应用程序中将像此示例中使用:
<script>
import Widget from './Widget.svelte'
const widgetName = 'Foo'
let widget
import(`/build/widgets/${widgetName}.js`)
.then(module => {
widget = module.default
})
.catch(err => {
console.error(`Failed to load ${widgetName}`, err)
})
</script>
{#if widget}
<Widget this={widget} prop="Foo" otherProp="Bar" />
{/if}
那么...我们来总结一下吧!
总结
使用Rollup编译你的小部件,而不是直接使用Svelte编译器,以生成浏览器可用的包。
在简洁性、重复性和额外负荷之间找到正确的平衡点。
使用动态导入来消耗你的小部件,在浏览器中独立构建。
不要试图混合使用不使用相同Svelte副本的组件(基本上意味着捆绑在一起,除非你进行了某些非凡的黑客)。虽然一开始看起来可能有效,但实际上它不会有效。