vaadin / hilla

Build better business applications, faster. No more juggling REST endpoints or deciphering GraphQL queries. Hilla seamlessly connects Spring Boot and React to accelerate application development.
https://hilla.dev
Apache License 2.0
894 stars 56 forks source link

embedding Flow components into TypeScript views does not work #320

Closed vlukashov closed 3 weeks ago

vlukashov commented 4 years ago

If I follow the Creating an Embedded Vaadin Application Tutorial in order to embed a server-side Vaadin component into a TypeScript Vaadin view, my application does not work as expected:

ServerCounter.java:

public class ServerCounter extends Div {

    public ServerCounter() {
        add(new Label("Server-side counter"));
    }

    public static class Exporter extends WebComponentExporter<ServerCounter> {
        public Exporter() {
            super("server-counter");
        }

        @Override
        protected void configureInstance(
                WebComponent<ServerCounter> webComponent,
                ServerCounter counter) {
        }
    }
}

index.html:

<script type="module" src="web-component/server-counter.js"></script>

main-layout.ts:

render() {
  return html`
      <server-counter></server-counter>
      <slot></slot>
  `;
}
vlukashov commented 4 years ago

Workaround (by @⁠tomivirkki):

We had the same issue = Polymer (and the Vaadin components) got imported from two separate bundles: the one from docs-app and the one from Vaadin (Embedded views)

This is resolved by excluding all Web Components / Polymer related imports from the Vaadin bundle: https://github.com/vaadin/docs/blob/master/webpack.config.js#L35-L36

const fileNameOfTheFlowGeneratedMainEntryPoint = require('path').resolve(
  __dirname,
  'target/frontend/generated-flow-imports.js'
);
const filteredFileNameOfTheFlowGeneratedMainEntryPoint =
  fileNameOfTheFlowGeneratedMainEntryPoint + '-filtered.js';

// @ts-ignore
module.exports = merge(flowDefaults, {
  entry: {
    bundle: filteredFileNameOfTheFlowGeneratedMainEntryPoint
  },
  plugins: [
    function(compiler) {
      compiler.hooks.afterPlugins.tap(
        'Filter out external deps',
        compilation => {
          const original = fs.readFileSync(
            fileNameOfTheFlowGeneratedMainEntryPoint,
            'utf8'
          );

          // Exclude component imports which are included in the "bundle" module
          const filtered = original
            .split('\n')
            .filter(row => {
              if (row.startsWith("import '@vaadin")) return false;
              if (row.startsWith("import '@polymer")) return false;
              if (!row.startsWith('import')) return false;
              return true;
            })
            .join('\n');

          fs.writeFileSync(
            filteredFileNameOfTheFlowGeneratedMainEntryPoint,
            filtered
          );
        }
      );
    }
  ]
});
vlukashov commented 4 years ago

More context in a slack discussion

Flow embedding currently works in a way that it creates its own vaadin-export bundle out of the generated-flow-imports file. In webpack terms that's an entry, i.e. a root of a separate dependency graph.

By design, webpack entries are not expected to be loaded several into the same page - there is no dependency dedup between them. This leads to problems when embedding Vaadin into any app that also uses Polymer, including V15 TS apps.

If I create a V15 TS app that does not use Polymer (i.e. does not use Vaadin components), embedding server-side Vaadin components into it works just fine as described in the embedding docs. However, as soon as I add a <vaadin-button> into the client-side bundle, embedding breaks because now Polymer dependencies end up both in vaadin-bundle and vaadin-export bundles.

But that's not all. Apparently, in addition to the npm dependencies conflict that comes with embedding there is also a conflict between the server-side WebComponents bootstrapping code and the AppShell bootstrapping code. Or that's what I would guess from the Flow.initApplication is not a function error in the browser logs.

haijian-vaadin commented 3 years ago

Note, the steps to import a server-side web component do not need to be the same as embedding into a general application (e.g. JSF). To embed a server-side component, a user could import the web component by import 'xxx/server-counter'; or sth similar.

platosha commented 3 years ago

I have reproduced the issues above and several others. Let me first try to list and categorize them.

1. Remote import issues

Flow’s web component export scripts are served using a request handler. Importing that as a remote script (e. g., from a URL) is hard for Fusion TypeScript views:

As of now, the only way to load the remote web component export bundle in Fusion is to add a <script> tag to the document, either in the index.html template, or dynamically using DOM APIs.

2. Duplicate web component dependencies

As already mentioned above, any duplicate web component dependencies, such as using <vaadin-button> or Polymer in both embedded and consumer applications, result in a conflict, because there is no deduplication between bundles, and because web components are registered in the global customElements registry.

Global registry for custom elements is a limitation in the web platform, which is not likely to be relaxed anytime soon. There is a proposal for Scoped Custom Element Registries, with no implementation or shim available yet.

This issue is less specific to Fusion, and likely manifests in Flow applications embedding other Flow applications too.

3. Conflict in global Vaadin namespace

In addition to errors from web component registration conflicts, there are also errors likely caused by conflicts of declaring and using global Vaadin namespace in both consumer and embedded application:

4. Documentation

Following the Creating an Embedded Vaadin Application Tutorial with Fusion application is hard. The tutorial is generic and does not tell about the above issues, and there is no separate Fusion specific article.


Now on to solution ideas. While issues (3) and (4) are more straightforward to address, we have to consider which way to go about (1) and (2). Here are some options:

Add generated sources for importing exported web components

Example import:

import 'Frontend/generated/web-component/server-counter';

This solves both (1) and (2). Here the import becomes local instead of remote, and web component dependencies are managed in a regular way by the consumer application.

Pros:

Cons:

Use Module Federation in webpack

Webpack has introduced the Module Federation concept, which is targeted at the same use case (Micro-Frontends). This could solve (1) and (2) with the following ideas:

Pros:

Cons:

Developing own custom solution for (1) and (2)

We could also tackle (1) and (2) as separate issues, with a custom solution for each:

As with any custom solutions, there are cons of more development and maintenance effort, as well as compatibility risks.

vlukashov commented 3 years ago

Thanks for a great summary, @platosha! 🥇

Embedding within one app

One use case for embedding server-side web components into TypeScript views is hybrid apps. The currently supported hybrid approach is to combine Flow (Java) and Fusion (TypeScript) UIs keeping them in separate routes. Sometimes a more granular combination may be handy: one may want to include an existing server-side Java component into a TypeScript view. This would be the case when an existing Flow-based app is updated to Vaadin 15+ and developers want to keep using existing (Java) components in TypeScript views as well.

In this case the option A (add generated sources for importing exported web components) looks like a good fit. The breakdown of work for the option A is as follows

Here is how DX would look like: ServerCounter.java:

public class ServerCounter extends Div {

    public ServerCounter() {
        add(new Label("Server-side counter"));
    }

    public static class Exporter extends WebComponentExporter<ServerCounter> {
        public Exporter() {
            super("server-counter");
        }

        @Override
        protected void configureInstance(
                WebComponent<ServerCounter> webComponent,
                ServerCounter counter) {
        }
    }
}

main-layout.ts:

import 'Frontend/generated/web-component/server-counter';

render() {
  return html`
      <server-counter></server-counter>
      <slot></slot>
  `;
}

It will have the limitation that the embedded server-side components cannot be in a different project - embedding works only within one Vaadin project.

Embedding across apps

Another use case for embedding server-side web components into TypeScript views is portal-like apps. The current support for portals includes portlet and OSGi support. This has a limitation that all portlets in a portal need to use the same version of Vaadin, and TypeScript views are not supported at all. No support for TypeScript views could be a road block for customers looking to adopt TypeScript views in portlet / OSGi environments.

In this case a variation of the option C (using an own in-house solution for splitting bundles) looks like a good fit. In fact, the portlet / OSGi support is made possible by a custom bundling logic where Polymer and other frontend dependencies are deployed as a single shared bundle and re-used by all portlets in a portal. When designing support for TypeScript views this existing implementation should be taken into account.

vlukashov commented 3 years ago

Another use case for cross-app embedding: gradual migration from an older Vaadin app to Fusion: want to embed parts of the old app inside the new app, without putting the old and the new app into one project.

haijian-vaadin commented 3 years ago

One extra task to consider is that currently the web component is generated into target/frontend folder, should we keep the file there? or change the logic to use the file in the generated/frontend folder.

The component would have some communication API to the consumer view, e.g. is it ready. since the web component is normally loaded asynchronously.

A first rough estimation of the effort is 2 persons 2-4 sprints.

Artur- commented 3 years ago

A working example can be found here https://artur.app.fi/embed-fusion-flow/. Code here https://github.com/Artur-/embed-fusion-flow

Legioth commented 3 weeks ago

Proper embedding support has been implemented and documented for Vaadin 24.4