如何手动将 svelte 组件编译为 sapper/svelte 生成的最终 javascript 和 css? [英] How can I manually compile a svelte component down to the final javascript and css that sapper/svelte produces?

查看:68
本文介绍了如何手动将 svelte 组件编译为 sapper/svelte 生成的最终 javascript 和 css?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们公司生产了一个用 svelte/sapper 编写的自动化框架.一项功能是开发人员可以创建自定义 ui 小部件,目前使用纯 js/html/css 和我们的客户端 api.这些小部件存储在数据库中,而不是文件系统中.

Our company produces an automation framework that is written in svelte/sapper. One feature is that developers can create custom ui widgets, currently using plain js/html/css and our client side api. These widgets are stored in the database and not on the file system.

我认为允许他们将小部件创建为 svelte 组件是一个很大的优势,因为它在一个位置包含所有标记、js 和 css,并且会给他们带来 svelte 反应性的所有好处.

I think it would be a big plus to allow them to create widgets as svelte components since it contains all of the markup, js and css in one location and would give them all of the benefits of svelte's reactivity.

我已经创建了一个使用 svelte 的服务器 API 编译组件的端点,但这似乎只是生成了一个模块,该模块已准备好用于 rollup-plugin-svelte/sapper/babel 以完成浏览器可以生成的工作使用.

I have gotten as far as creating an endpoint that compiles components using svelte's server API but that just seems to generate a module that is ready for rollup-plugin-svelte/sapper/babel to finish the job of producing something the browser can use.

如何手动将 svelte 组件编译为 sapper/svelte 生成的最终 javascript 和 css.

How can I manually compile a svelte component down to the final javascript and css that sapper/svelte produces.

推荐答案

哎呀,难啊.坚持住.

您实际上缺少的是链接",即将编译代码中的 import 语句解析为浏览器可以使用的内容.这是通常由打包器完成的工作(例如 Rollup、Webpack...).

What you're missing actually is the "linking", that is resolving import statements in the compiled code to something the browser can use. This is the work that is typically done by the bundler (e.g. Rollup, Webpack...).

这些导入可以来自用户(小部件开发者)代码.例如:

These imports can come from user (widget developer) code. For example:

import { onMount } from 'svelte'
import { readable } from 'svelte/store'
import { fade } from 'svelte/transition'
import Foo from './Foo.svelte'

或者它们可以由编译器注入,具体取决于组件中使用的功能.例如:

Or they can be injected by the compiler, depending on the features that are used in your component. For example:

// those ones are inescapable (bellow is just an example, you'll 
// get different imports depending on what the compiled component 
// actually does / uses)
import {
  SvelteComponent,
  detach,
  element,
  init,
  insert,
  noop,
  safe_not_equal,
} from 'svelte/internal'

Svelte 将 .svelte 编译为 .js 和可选的 .css,但它不会对代码中的导入执行任何操作.相反,它添加了一些(但仍然没有解决它们,超出了它的范围).

Svelte compiles .svelte to .js and, optionally, .css, but it doesn't do anything with imports in your code. On the contrary, it adds some (but still, doesn't resolve them, it's out of its scope).

您需要解析编译后的代码以找到那些来自编译器的原始导入,这些导入可能指向您的文件系统和您的 node_modules 目录上的路径,并将它们重写为使浏览器的意义——即 URLs...

You'd need to parse the compiled code to find those imports that, raw from the compiler, probably points to paths on your file system and your node_modules directory, and rewrite them to something that makes sense for the browser -- that is, URLs...

看起来不是很有趣,是吗?(或者太多,取决于您如何看待事物...)幸运的是,您并不孤单,我们有非常强大的工具专门用于此任务:进入打包程序!

Doesn't seem much fun, does it? (Or too much of it, depending on how you see things...) Fortunately, you're not alone with this need and we've got pretty powerful tooling dedicated precisely to this task: enter the bundler!

解决这个问题的一个相对简单的方法(还有更多,不要太兴奋)是编译您的小部件,不是使用 Svelte 的编译器 API,而是使用 Rollup 和 Svelte 插件.

One relatively straightforward approach to this problem (more to come, don't get too excited too early) is to compile your widgets, not with Svelte's compiler API, but with Rollup and the Svelte plugin.

Svelte 插件基本上完成了您使用编译器 API 所做的工作,但 Rollup 还将完成重新连接导入和依赖项的所有繁重工作,以生成可供浏览器使用的整洁的小包(包)(即不依赖于您的文件系统).

The Svelte plugin essentially does what you were doing with the compiler API, but Rollup will also do all the hard work of rewiring imports and dependencies in order to produce a neat little package (bundle) that is consumable by the browser (i.e. that doesn't rely on your file system).

你可以使用一些像这样的 Rollup 配置来编译一个小部件(这里是 Foo.svelte):

You can compile one widget (here Foo.svelte) using some Rollup config like this:

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

// include CSS in component's JS for ease of use
//
// set to true to get separate CSS for the component (but then,
// you'll need to inject it yourself at runtime somehow)
//
const emitCss = false

const cmp = 'Foo'

export default {
  // our widget as input
  input: `widgets/${cmp}.svelte`,

  output: {
    format: 'es',
    file: `public/build/widgets/${cmp}.js`,
    sourcemap: true,
  },

  // usual plugins for Svelte... customize as needed
  plugins: [
    svelte({
      emitCss,
      compilerOptions: {
        dev: !production,
      },
    }),

    emitCss && css({ output: `${cmp}.css` }),

    resolve({
      browser: true,
      dedupe: ['svelte'],
    }),
    commonjs(),
    production && terser(),
  ],
}

这里没什么特别的......这基本上是来自 Rollup 的官方 Svelte 模板的配置,减去与开发服务器有关的部分.

Nothing very extraordinary here... This is basically the config from the official Svelte template for Rollup, minus the parts pertaining to the dev server.

使用上面的配置和这样的命令:

Use the above config with a command like this:

rollup --config rollup.config.Foo.js

并且你将在 public/build/Foo.js 中获得你的浏览器编译好的 Foo 小部件!

And you'll get your browser-ready compiled Foo widget in public/build/Foo.js!

Rollup 还有一个 JS API,因此您可以根据需要从 Web 服务器或其他任何地方以编程方式运行它.

Rollup also has a JS API so you could run this programmatically as needed from a web server or whatever.

然后您将能够动态导入,然后在您的应用程序中将此模块与类似内容一起使用:

Then you'll be able to dynamically import and then use this module with something like this in your app:

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(不是您机器上的文件路径).

Dynamic imports will probably be necessary in your case, because you won't know about the widgets at the time you build your main app -- hence you will need to construct the import URLs dynamically like above at runtime. Note that the fact that the import URL is a dynamic string will prevent Rollup from trying to resolve it at bundle time. This means the import will end up as written above in the browser, and that it must be an URL (not a file path on your machine) that the browser will be able to resolve.

那是因为我们使用浏览器本地动态导入来使用编译的小部件,我们需要在 Rollup 配置中将 output.format 设置为 es.Svelte 组件将使用 export default ... 语法公开,现代浏览器本机可以理解.

That's because we're consuming the compiled widget with a browser native dynamic import that we need to set output.format to es in the Rollup config. The Svelte component will be exposed with export default ... syntax, that modern browsers natively understand.

当前浏览器很好地支持动态导入.值得注意的例外是旧"Edge(在它基本上变成 Chrome 之前).如果您需要支持旧浏览器,可以使用 polyfills(其中许多实际上 - 例如 dimport).

Dynamic imports are very well supported by current browsers. The notable exception is the "old" Edge (before it essentially became Chrome). If you need to support older browsers, polyfills are available (many of them actually -- e.g. dimport).

这个配置可以进一步自动化,以便能够编译任何小部件,而不仅仅是Foo.例如,像这样:

This config can be further automatized to be able to compile any widget, not just Foo. For example, like this:

rollup.config.widget.js

... // same as above essentially

// using Rollup's --configXxx feature to dynamically generate config
export default ({ configWidget: cmp }) => ({
  input: `widgets/${cmp}.svelte`,
  output: {
    ...
    file: `public/build/widgets/${cmp}.js`,
  },
  ...
})

然后你可以像这样使用它:

You can then use it like this:

rollup --config rollup.config.widget.js --configTarget Bar

我们正在取得进展,但仍有一些注意事项和障碍需要注意(并且可能进一步优化 - 您的电话).

We're making progress, yet there remains a few caveats and hurdles to be aware of (and maybe optimize further -- your call).

上述方法应该为您的小部件提供编译后的代码,您可以在浏览器中运行这些代码,没有未解析的导入.好的.但是,它通过在构建时解析给定小部件的所有依赖项并将所有这些依赖项捆绑在同一文件中来实现.

The above approach should give you the compiled code for your widgets, that you can run in the browser, with no unresolved imports. Good. However, it does so by resolving all dependencies of a given widget when it is built, and bundling all these dependencies in the same file.

否则,在多个小部件之间共享的所有依赖项将为每个小部件复制,特别是 Svelte 依赖项(即从 sveltesvelte/* 导入).这并不全是坏事,因为它为您提供了非常独立的小部件......不幸的是,这也给您的小部件代码增加了一些重量.我们正在讨论的可能是 20-30 kb 的 JS 添加到每个可以在所有小部件之间共享的小部件.

Said otherwise, all dependencies that are shared between multiple widgets will be duplicated for every widget, very notably the Svelte dependencies (i.e. imports from svelte or svelte/*). This is not all bad, because it gives you very standalone widgets... Unfortunately, this also add some weight to your widgets code. We're talking something like maybe 20-30 kb of JS added to each widgets that could be shared between all of them.

此外,我们很快就会看到,在您的应用中拥有独立的 Svelte 内部副本存在一些我们需要考虑的缺点......

Also, as we will see soon, having independent copies of Svelte internals in your app has some drawbacks we need to take into consideration...

提取公共依赖项以便共享而不是重复的一种简单方法是一次性捆绑所有小部件.这可能不适用于所有用户的所有小部件,但也许在个人用户级别可行?

One easy way to extract common dependencies so they can be shared instead of duplicated is to bundle all your widgets in one pass. This might not be practicable for all the widgets of all your users, but maybe it can be doable at individual user level?

无论如何,这是大体的想法.您可以将上面的 Rollup 配置更改为如下所示:

Anyway, here's the general idea. You would change the above Rollup configs to something like this:

rollup.config.widget-all.js

...

export default {
  input: ['widgets/Foo.svelte', 'widgets/Bar.svelte', ...],
  output: {
    format: 'es',
    dir: 'public/build/widgets',
  },
  ...
}

我们正在传递一个文件数组,而不是一个,作为 input(您可能会通过列出给定目录中的文件来自动执行此步骤),并且我们正在更改 output.fileoutput.dir,因为现在我们将同时生成多个文件.这些文件将包含 Rollup 将提取的小部件的通用依赖项,并且您的所有小部件将在它们之间共享以供重复使用.

We're passing an array of files, instead of just one, as the input (you would probably automatize this step by listing files in a given directory), and we're changing output.file to output.dir, since now we're gonna have several files generated at once. Those files will include common dependencies of your widgets that Rollup will have extracted, and that all your widgets will share between them for reuse.

通过自己提取一些共享依赖项(例如 Svelte...)并将它们作为 URL 提供给浏览器(即通过您的 Web 服务器为它们提供服务),可以进一步推动.这样,您可以将编译代码中的导入重写为那些已知 URL,而不是依赖 Rollup 来解析它们.

It would be possible to push even further, by extracting some shared dependencies (say, Svelte...) yourself and make them available as URLs to the browser (i.e. serve them with your web server). This way, you could rewrite those imports in your compiled code to those known URLs instead of relying on Rollup to resolve them.

这将完全减少代码重复,减轻重量,而且这将允许在使用它们的所有小部件之间共享这些依赖项的单个版本.这样做还可以减轻同时构建所有共享依赖项的小部件的需要,这很诱人......但是,这将非常(!)设置复杂,并且您实际上会很快遇到收益递减.

This would reduce code duplication entirely, saving weight, and also this would allow to have a single version of those dependencies shared among all the widget that use them. Doing so would also relieve the need to build all widgets that share dependencies in one go at the same time, which is alluring... However, this would be pretty (!) complicated to setup, and you would actually hit diminishing returns fast.

实际上,当您将一堆小部件捆绑在一起(甚至只是一个小部件)并让 Rollup 提取依赖项时,捆绑器有可能知道消费代码实际需要依赖项的哪些部分,并且跳过其余部分(请记住:Rollup 构建时将摇树作为其主要优先事项之一——如果不是其中之一——,而 Svelte 是由同一个人构建的——意思是:你可以期待 Svelte 成为 非常 摇树友好!).另一方面,如果您自己手动提取一些依赖项:它减轻了一次性捆绑所有消耗代码的需要,但是您必须公开整个消耗的依赖项,因为您将无法提前知道需要的部分.

In effect, when you're bundling a bunch of widgets together (or even just one) and let Rollup extract the dependencies, it is possible for the bundler to know what parts of the dependencies are actually needed by the consuming code and skip the rest (keep in mind: Rollup was built with tree shaking as one -- if not the one -- of its main priority, and Svelte was built by the same guy -- meaning: you can expect Svelte to be very tree shaking friendly!). On the other hand, if you extract some dependencies manually yourself: it relieves the need to bundle all consuming code at once, but you will have to expose the whole of the consumed dependencies, because you won't be able to know in advance the parts from them that will be needed.

您需要在高效和实用之间找到平衡,考虑到每个解决方案对您的设置增加的复杂性.考虑到您的用例,我自己的感觉是,最佳点是将每个小部件完全独立地捆绑在一起,或者将来自同一用户的一堆小部件捆绑在一起以减轻重量,如上所述.加倍努力可能是一个有趣的技术挑战,但它只会获得很少的额外好处,但复杂性会有些爆炸......

It's a balance you need to find between what is efficient and what is practical, accounting for the added complexity of each solution to your setup. Given your use case, my own feeling is that the sweet spot is either bundling each widget entirely independently, or bundling a bunch of widgets from, say, the same user together to save some weight, as described above. Pushing harder would probably be an interesting technical challenge, but it would reap just little extra benefits, but somewhat exploding complexity...

好的,现在我们知道如何为浏览器捆绑我们的小部件.我们甚至可以在一定程度上控制如何完全独立地打包我们的小部件,或者承担一些额外的基础设施复杂性,以共享它们之间的依赖关系并减轻一些重量.现在,当我们决定如何制作漂亮的小包(错误,包)时,我们需要考虑一个特殊的依赖关系:这就是 Svelte 本身...

OK so we now know how to bundle our widgets for the browser. We even have some degree of control on how to pack our widgets entirely standalone, or take on some extra infrastructure complexity to rather share dependencies between them and save some weight. Now, we've got a special dependency to consider, when we decide how we make our pretty little packets (err, bundles): that's Svelte itself...

所以我们明白,当我们用 Rollup 捆绑单个小部件时,它的所有依赖项都将包含在捆绑"中.(在这种情况下只是一个小部件文件).如果您以这种方式捆绑 2 个小部件并且它们共享一些依赖项,那么这些依赖项将在每个捆绑包中重复.特别是,您将获得 2 个 Svelte 副本,每个小部件中一个.同样,您的主要"的依赖项与某些小部件共享的应用程序仍将在浏览器中复制.您将拥有相同代码的多个副本,这些副本将被这些不同的包使用——您的应用、不同的小部件...

So we understand that when we're bundling a single widget with Rollup, all of its dependencies will be included in the "bundle" (just the one widget file in this case). If you bundle 2 widgets in this way and they share some dependencies, those dependencies will be duplicated in each of those bundles. In particular, you'd get 2 copies of Svelte, one in each widget. Likewise, the dependencies of your "main" app that are shared with some widgets will nonetheless be duplicated in the browser. You'll have multiple copies of the same code that will be used by those different bundles -- your app, different widgets...

但是,您需要了解 Svelte 的一些特别之处:它不支持复制.svelte/internal 模块是有状态的,它包含一些全局变量,如果您有此代码的多个副本(见上文),这些变量将被复制.这意味着,在实践中,不使用相同 Svelte 内部组件的 Svelte 组件不能一起使用.

However, there is something special about Svelte that you need to know: it doesn't support being duplicated. The svelte/internal module is stateful, it contains some global variables that would be duplicated if you have multiple copies of this code (see above). What this means, in practice, is that Svelte components that don't use the same copie of Svelte internals can't be used together.

例如,如果您有一个 App.svelte 组件(您的主应用程序)和一个 Foo.svelte 组件(例如用户小部件),它们已独立捆绑在一起,那么你就不能在 App 中使用 Foo,否则你会得到奇怪的错误.

For example, if you have a App.svelte component (your main app) and a Foo.svelte component (e.g. a user widget) that have been bundled independently, then you can't use Foo in App, or you'd get weird bugs.

这行不通:

App.svelte

<script>
  // as we've seen, in real life, this would surely be a 
  // dynamic import but whatever, you get the idea
  import Foo from '/build/widgets/Foo.js'
</script>

<!-- NO -->
<Foo />

<!-- NO -->
<svelte:component this={Foo} />

这也是你在官方 Svelte 模板的 Rollup 配置中有这个 dedupe: ['svelte'] 选项的原因......这是为了防止捆绑 Svelte 的不同副本,这会发生例如,如果您曾经使用过链接包.

That's also the reason why you have this dedupe: ['svelte'] option in the official Svelte template's Rollup config... This is intended to prevent bundling different copies of Svelte, which would happen if you ever used linked packages, for example.

无论如何,在您的情况下,最终在浏览器中出现多个 Svelte 副本是不可避免的,因为您可能不想在用户添加或更改其小部件之一的任何时候重建整个主应用程序.. 除了不遗余力地自己提取、集中和重写 Svelte 导入;但是,正如我所说,我认为这不是一种合理且可持续的方法.

Anyway, in your case it is kind of unescapable to end up with multiple copies of Svelte in the browser, since you're probably not wanting to rebuild your whole main app anytime a user adds or changes one of their widget... Except going to great lengths to extract, centralize, and rewrite the Svelte imports yourself; but, as I said, I don't believe this would be a reasonable and sustainable approach.

所以我们被困住了.

还是我们?

重复 Svelte 副本的问题仅在冲突组件是同一组件树的一部分时发生.也就是说,当您让 Svelte 像上面一样创建和管理组件实例时.当您自己创建和管理组件实例时,问题不存在.

The problem of duplicated Svelte copies only occurs when the conflicting components are part of the same components tree. That is, when you let Svelte create and manage the component instances, like above. The problem doesn't exist when you create and manage the component instances yourself.

...

const foo = new Foo({ target: document.querySelector('#foo') })

const bar = new Bar({ target: document.querySelector('#bar') })

这里 foobar 将是完全独立的组件树,就 Svelte 而言.像这样的代码将始终有效,与编译和捆绑 FooBar 的方式和时间(以及使用哪个 Svelte 版本等)无关.

Here foo and bar will be entirely independent component trees, as far as Svelte is concerned. Code like this will always work, irrelevantly of how and when (and with which Svelte version, etc.) Foo and Bar were compiled and bundled.

据我了解您的用例,这不是主要障碍.您将无法使用诸如 <svelte:component/> 之类的东西将用户的小部件嵌入到您的主应用程序中……但是,没有什么可以阻止您在其中创建和管理小部件实例自己找对地方.您可以创建一个包装器组件(在您的主应用程序中)来推广这种方法.像这样:

As I understand your use case, this is not a major hurdle. You won't be able to embed your users' widgets into your main app with something like <svelte:component />... However, nothing prevents you from creating and managing the widget instances in the right place yourself. You can create a wrapper component (in your main app) to generalize this approach. Something like this:

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 元素,渲染一个外部"元素.组件在里面,把所有的 props 传下来(我们代理的是反应性),不要忘记在我们的代理组件被销毁时清理.

We create a target DOM element from our main app, render an "external" component in it, pass down all the props (we're proxying reactivity), and don't forget to cleanup when our proxy component is destroyed.

这种方法的主要限制是应用的 Svelte 上下文 (setContext/getContext) 对代理组件不可见.

The main limitation of such an approach is that Svelte context (setContext / getContext) of the app won't be visible to the proxied components.

再一次,这在小部件用例中似乎不是问题——也许更好:我们真的希望小部件能够访问周围应用程序的每一部分吗?如果确实需要,您始终可以通过 props 将一些上下文传递给小部件组件.

Once again, this doesn't really seem like a problem in the widget use case -- maybe even better: do we really want the widgets to have access to every bits of the surrounding app? If really needed, you can always pass bits of context down to the widget components via props.

上面的 Widget 代理组件将在您的主应用程序中像这样使用:

The above Widget proxy component would then be used like this in your main app:

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

而且……我们到了?总结一下吧!

And... Here we are? Let's sum it up!

  • 使用 Rollup 编译您的小部件,而不是直接使用 Svelte 编译器,以生成浏览器就绪包.

  • Compile your widgets with Rollup, not Svelte compiler directly, to produce browser ready bundles.

在简单、重复和额外重量之间找到适当的平衡.

Find the right balance between simplicity, duplication and extra weight.

使用动态导入在浏览器中使用您的小部件,这些小部件将独立于您的主应用而构建.

Use dynamic imports to consume your widgets, that will be built independently of your main app, in the browser.

不要尝试将不使用相同 Svelte 副本的组件混合在一起(本质上意味着捆绑在一起,除非您已经开始了一些非凡的黑客攻击).乍一看似乎有效,但实际上并没有.

Do not try to mix together components that don't use the same copy of Svelte (essentially means bundled together, except if you've launched into some extraordinary hack). It might looks like it works at first, but it won't.

这篇关于如何手动将 svelte 组件编译为 sapper/svelte 生成的最终 javascript 和 css?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆