Updateable Vue3 application architecture with plugins for usage in multiple projects

Hello All!

We are new to Vue3. We are developing a product web catalog application based on vue3. This application has only the frontend, because all the product data is pulled via REST API. This application can be used by multiple customers. The functionality should be the same, but every customer may want to have his own design - so some components, views and css and some template images may be different.

In “PHP world” I would have the core, modules and templates for that, and I would use Composer to automatically update the system, while core and modules are stored in separate repositories. We want to archive something similar for this vue3 application. We want to have the possibility to easily update the application for all of these customers.

Each project (for each customer) should have its own repository on github.

Question 1:
How can we properly organize everything (to save our time for support and automate everything, what can be automated), if we plan to improve the functionality of this web catalog application in the future? It would be a horror if we will need to copy-paste everything among different projects.

Question 2:
We want that some customer should have additional functionality, which should not be available in the main application of the web catalog, lets say the ability to order some chosen products (web catalog should provide no ability to order products).
a) Should it be a plugin?
b) Can the source code of this plugin be managed in a separate repository? And if so how can I dynamically “append” it while building?
c) How can I build the application with or without this “addition”?

Thank you all for the answers and discussion!

I have been researching a way to dynamically include modules and plugins as well but for Angular which uses webpack. I finally came across some terminology that has lead me down the right path or so I think. That terminology is “module federation”. The most recent version of webpack supports module federation. I also came across a very well written article on the topic but mostly related to Angular. Although the article does highlight concepts more than specific implementation in many places.

Anyway the basic idea is splitting the front-end application into smaller micro front-ends. Wrapping those smaller front-ends in a shell application. The micro front-ends are stand alone applications running on their own ports or even their very own separate domain. This concept can also be used for any module like one that loads plugins instead of having visual components.

This is the recent Angular article about module federation.

This is my projects issue on github to record thoughts, architecture and specific tasks related to creating basically a front-end micro front-end orchestration platform using module federation. This would allow users to create web experiences by including other JavaScript projects compatible with module federation but separate from the main shell repo.

Also the project which I have been working on here has a plugin system built into it. The plugin system uses Typescript and Reactive programming providing a way to dynamically add functionality at runtime or when lazy loading modules. The idea going forward would be to extend it with a new discovery mechanism for loading external modules through module federation which could dynamically add new plugins. Effectively allowing any user to create a stand alone project and integrate with the shell without needing to change or clone the shell code.

That plugin system is highly inspired by Drupal.

Example implementation / usage / api.

Plugin Manager (manages plugins for ONE definition)

import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { ModuleLoaderService } from 'utils';
import { BasePluginManager, PluginManager, PluginDef, PluginConfigurationManager } from 'plugin';
import { CrudAdaptorPlugin } from '../models/crud.models';

@Injectable({
  providedIn: 'root'
})
export class CrudAdaptorPluginManager extends BasePluginManager<CrudAdaptorPlugin<string>, string> implements PluginManager<CrudAdaptorPlugin<string>, string> {
  constructor(pcm: PluginConfigurationManager, moduleLoader: ModuleLoaderService) {
    super(pcm, moduleLoader);
  }
  pluginDef() {
    return of(new PluginDef({ name: 'crud_adaptor' }));
  }
}

Definition plugin class

// ...

export class CrudAdaptorPlugin<T = string> extends Plugin<T>  {
  create: ({ object, identity, parentObject }: CrudOperationInput) => Observable<CrudOperationResponse>;
  read: ({ object, identity, parentObject }: CrudOperationInput) => Observable<CrudOperationResponse>;
  update: ({ object, identity, parentObject }: CrudOperationInput) => Observable<CrudOperationResponse>;
  delete: ({ object, identity, parentObject }: CrudOperationInput) => Observable<CrudOperationResponse>;
  // Query is an extensions to CRUD to support BASIC queries / compatibility with NgRx data service interface.
  // Complex queries / searches will be managed separate from CRUD.
  query?: ({ rule, objects, identity, parentObjects }: CrudCollectionOperationInput) => Observable<CrudCollectionOperationResponse>;
  constructor(data?: CrudAdaptorPlugin<T>) {
    super(data)
    if (data) {
      this.create = data.create;
      this.read = data.read;
      this.update = data.update;
      this.delete = data.delete;
      if (data.query) {
        this.query = data.query;
      }
    }
  }
}

Register plugin implementation via factory

// ...
export class Aws3Module { 
  constructor(
    @Optional() @Inject(HOST_NAME) hostName: string,
    @Optional() @Inject(PROTOCOL) protocol: string,
    @Inject(COGNITO_SETTINGS) cognitoSettings: CognitoSettings,
    @Inject(PLATFORM_ID) platformId: Object,
    cpm: CrudAdaptorPluginManager,
    authFacade: AuthFacade,
    paramsEvaluatorService: ParamEvaluatorService,
    http: HttpClient,
    asyncApiCallHelperSvc: AsyncApiCallHelperService
  ) {
    // Create and register the aws s3 crud adaptor as a crud plugin
    cpm.register(s3EntityCrudAdaptorPluginFactory(platformId, authFacade, cognitoSettings, paramsEvaluatorService, http, asyncApiCallHelperSvc, hostName, protocol));
  }
}

The simplest way to understand the the power of this is being able to create reusable strategies for something like persistence and search (crud). Different entities have different requirements in those regards but so long as a plugin is created that matches the api / interface those strategies can be swapped out for different strategies.

import { isPlatformServer } from '@angular/common';
import { CrudEntityMetadataMap, CrudEntityQueryMapping } from 'crud';

export const entityMetadataFactory = (platformId: Object): CrudEntityMetadataMap => {
  return {
    PanelPageListItem: {
      entityName: 'PanelPageListItem',
      crud: {
        /*rest: {
          // ops: ['query'],
          params: {
            entityName: 'PanelPageListItem'
          }
        },*/
        aws_opensearch_template: {
          ops: ['query'],
          params: {
            id: 'panelpagelistitems',
            index: 'classified_panelpages',
            hits: true,
            source: true,
            domain: 'search-classifieds-ui-dev-eldczuhq3vesgpjnr3vie6cagq',
            region: 'us-east-1'
          }
        },
        ...(isPlatformServer(platformId) ?
          {} :
          {
            idb_keyval: {
              params: {
                prefix: 'panelpage__'
              },
              queryMappings: new Map<string, CrudEntityQueryMapping>([
                // ['path', { defaultOperator: 'startsWith' }]
                ['site', { defaultOperator: 'term||wildcard' }],
                ['path', { defaultOperator: 'term||wildcard' }]
              ])
            }
          }
        ),
        /*aws_opensearch_entity: {
          ops: ['create', 'update', 'read', 'delete']
        },*/
      }
    },
    PanelPage: {
      entityName: 'PanelPage',
      crud: {
        aws_s3_entity: {
          ops: ['query'],
          params: {
            bucket: 'classifieds-ui-dev',
            prefix: 'panelpages/'
          }
        },
        /*rest: {
          // ops: ['query'],
          params: {
            entityName: 'PanelPage'
          }
        },*/
        ...(isPlatformServer(platformId) ? 
          {} :
          {
            idb_keyval: { // demo only
              params: {
                prefix: 'panelpage__'
              }
            }
          }
        )
      }
    },
    PanelPageState: {
      entityName: 'PanelPageState'
    }
  }
};

1 Like

Thank you @windbeneathmywings, we will investigate this direction.

Also while on the topic I think it is also worth discussing theming. While I have not used this library yet I really like what I see. I also see much potential for creating reusable, customizable themes. I only discovered this recently through another post on this forum and I’m happy I did. I plan to integrate this into my project as well probably though not a top priority at the moment.

Using a mono repo design pattern will also aid in organizing shared libraries and components across multiple applications.

There is also bazel which Angular uses that provides tooling for managing a mono repo / dependencies across completely different languages.

The other is nx

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.