Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to include vue app inside another vue app?

I'm currently requested to generate widget that will be included in different customers web sites.

Let's say somthing like:

exemple

There is 2 constraintes:

  1. The use of iframe is forbidden
  2. The customers website could be done in whatever thecnology (PHP, React, ANgular, Vue.js, JQuery,...)

As the requested widget should be interactive, I wanted to develop it using javascript framwork (I thought about Vue.js).

However, I thought that this could lead to some conflicts (or even break customer's website) for instance if the customer's website is already use another version of Vue.js.

I spent several hours looking and thinking about a solution, but expect using an iframe I didn't found anything...

=> Is there a way to include Vue.js apps inside another Vue.js app (or React, Angular, ...) ?

Thanks in advance for your help.

like image 421
David ROSEY Avatar asked Dec 13 '22 08:12

David ROSEY


1 Answers

Important edit:
since I wrote this answer, I discovered a simpler method to wrap an entire project as a single .js file, documented here.

If you need more control over when the app is instantiated and whether or not it should contain Vue, here's the original answer (not claiming it's better - i simply wrote it at a time I didn't have any alternative, and it worked):


Orignal answer:

My current company typically delivers embedded apps in our clients' webpages. Note: although the example below is written in .ts, it's almost identical in .js.

After testing various methods (Vue's quite flexible), here's what we ended up doing:

The method is inspired by mapbox-gl's model, adapted to Vue.
The base principle is simple: export your entire app as a clean JS class (imported via a single script) and run

new SomeClass(selector, config);

For this purpose, I typically create a someClass.ts file (which does the job main.ts normally does), but exports a class:

import Vue from 'vue';
import App from './App.vue';
import store from './store';
import i18n from '@/plugins/i18n';
import VA from 'vue-axios';
import axios from 'axios';
import store from './store';
...

Vue.use(VA, axios);
Vue.config.productionTip = false;

const Instance = new Vue({
  store,
  i18n,
  components: { App },
  data: () => ({ config: {} }),
  render: function(createElement) {
    return createElement('app', {
      props: { config: this.config }
    });
  }
});

export default class SomeClass {
  constructor(selector: string, config: SomeClassConfig) {
    Vue.set(Instance, 'config', {
      ...config,
      // id is optional. but allows setting the app's `id` dynamically to same `id`
      // as the context page placeholder (which app replaces), for consistency
      // for this, you have to assign `config.id` to App's `$el` in its `mounted()`
      id: selector.replace('#', '')
      // or document.querySelector(selector).id || 'someClassApp'
    });
    Instance.$mount(selector);
    return Instance;
  }
}

// optional exports, used for type inheritance throughout the app
export const { $http, $t, $store } = Instance;

Note: Making this crystal clear: none of the above imports are necessary (i.e: axios, i18n, etc... - it's just an example). Do whatever your main.ts (or main.js) is normally doing. Then create the Instance, inject config, export the class and any named exports you might need from Instance.

The above already does the job (if you build it using the build command specified below), but you'll have to also adapt main.ts to import this class and render the app when serving:

import SomeClass from '@/someClass';

export default new SomeClass('#devhook', {
 // config object
});

const app = document.querySelector('#devhook');
if (app instanceof HTMLElement) {
  app.addEventListener('some-event', function(e) {
    if (e instanceof CustomEvent) {
      console.log(e.detail);
      // you can test events emitted for context app here...
    }
  });
}

And public/index.html looks like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="./favicon.ico">
    <!-- VueJS -->
    <script src="https://cdn.jsdelivr.net/npm/vue@latest/dist/vue.min.js"></script>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="devhook"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

The build command is:

vue-cli-service build --target lib --name SomeClass --inline-css --inline-vue src/someClass.ts

Notice we're exporting src/someClass.ts. You'll end up with a file called SomeClass.umd.js.

Also, adding this setting to config.vue.js:

configureWebpack: {
  output: {
    libraryExport: 'default'
  }
}

will enable you to init the class without .default() when you import it in the context page. without the above webpack option, you'd need to init it like this:

new Someclass.default(selector, config);

Everything else is pretty standard.

And now the context page only needs to import the .umd.js, instantiate app with

new SomeClass(selector, config)

... and add one or more listeners on the rendered element.

The .umd.js export can be served from an external server and, obviously, can be imported from an npm package.

You don't have to --inline-css and --inline-vue if it makes more sense to load them separately (i.e: Vue might already be present in context page). Also, often times I rely on libraries already present in the context page (you want to reuse as many libs as possible), using vue.config.js:

configureWebpack: {
  externals: {
    lodash: 'window._'
  }
}

When you do this, remember to add a script pointing to lodash/jquery/whatever cdn in public/index.html, so it gets loaded when serving/developing, as it would in context page.

Finally, the demo.html (used to showcase the prod build), looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Some Class App Demo</title>
    <link rel="shortcut icon" href="./favicon.ico"/>

    <!-- also works when served from a different domain -->
    <script src="./SomeClass.umd.min.js"></script>
    <link rel="stylesheet" href="./SomeClass.css">

</head>
<body>
<div id="SomeClassApp"></div>

<script>
  const config = {
    // whatever
  };
  const app = new SomeClass('#SomeClassApp', config);

  if (app.$el instanceof HTMLElement) {
    app.$el.addEventListener('some-event', function(e) {
      if (e instanceof CustomEvent) {
        console.log(e.detail);
      }
    });
  }
</script>
</body>

Of course, you'll have to add all externals to it, before your app's script (./SomeClass.umd.min.js), if you used any. Don't defer them. The app expects them loaded in window by the time it inits.

As for conflicts, you shouldn't have any. Vue is highly compatible across minors so you could simply use the context page's Vue (instead of inlining it). Also, Vue 2 syntax is supposed to be fully compatible with Vue 3 (as in, you'd be able to run Vue 2 apps with Vue 3 without making any changes to them - we'll see).

like image 118
tao Avatar answered Dec 18 '22 20:12

tao