13

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.

0

3 Answers 3

20

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).

Sign up to request clarification or add additional context in comments.

2 Comments

This is really helpful @tao - I am a total Vue newbie but need to do this sort of thing in Vue 3. Does this same pattern work?
@Richard, yes, the same pattern would certainly work with Vue 3, but the implementation details would be different. In my estimation, the result would actually be cleaner. On a different note, while having a go at it can't hurt, if you really are just starting with Vue, trying the above in Vue 3 means you won't have many examples to follow, so you have to rely on your ability to dig into Vue's source code, whereas with Vue 2 you'll find plugins, examples and helpers.
3

This is how I did it using Vue3 and Vite:

Example demo

1. Minimize the main.ts (main.js)

Commit

Don't create the App there, and not mount it. Just import it and export it.

// /src/main.ts
import './assets/main.css'
import App from './App.vue'

export { App }

2. Adapt your dev index.html

Commit

Now the main.ts is very passive so we have to trigger the app creation and mounting it manually in our dev index.html. This is also what have to be done when the lib is integrated in another app.

<!-- /index.html -->
<body>
  <div id="app"></div>

  <script type="module">
    import { createApp } from 'vue';
    import { App as VueLibExample } from '/src/main.ts';

    createApp(VueLibExample).mount('#app');
  </script>
</body>

We are using an alias to avoid conflict with another imported apps.

3. The library mode build

Commit

The main concept is "build your app using library mode". Here is the documentation to do this with Vite.

There is described how to set up your vite.config.js and package.json.

In summary this is how my vite.config.js looks like at the end:

// vite.config.js
import { fileURLToPath, URL } from 'node:url'

import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  build: {
    lib: {
      // Could also be a dictionary or array of multiple entry points
      entry: resolve(__dirname, 'src/main.ts'),
      name: 'VueLibExample',
      // the proper extensions will be added
      fileName: 'vue-lib-example',
    },
    rollupOptions: {
      // make sure to externalize deps that shouldn't be bundled
      // into your library
      external: ['vue'],
      output: {
        // Provide global variables to use in the UMD build
        // for externalized deps
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
})

I am not sure about the external: ['vue'], it may be a good idea to include the whole vue in your library to prevent possible dependencies version problems.

And this is my package.json:

{
  "name": "vue-lib-example",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "files": [
    "dist"
  ],
  "main": "./dist/vue-lib-example.umd.cjs",
  "module": "./dist/vue-lib-example.js",
  "exports": {
    ".": {
      "import": "./dist/vue-lib-example.js",
      "require": "./dist/vue-lib-example.umd.cjs"
    }
  },
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check \"build-only {@}\" --",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build --force"
  },
  "dependencies": {
    "vue": "^3.4.15"
  },
  "devDependencies": {
    "@tsconfig/node20": "^20.1.2",
    "@types/node": "^20.11.10",
    "@vitejs/plugin-vue": "^5.0.3",
    "@vue/tsconfig": "^0.5.1",
    "npm-run-all2": "^6.1.1",
    "typescript": "~5.3.0",
    "vite": "^5.0.11",
    "vue-tsc": "^1.8.27"
  }
}

We can build our lib:

> yarn build
yarn run v1.22.21
$ run-p type-check "build-only {@}" --
$ vite build
$ vue-tsc --build --force
vite v5.1.3 building for production...
✓ 10 modules transformed.
dist/style.css  2.65 kB │ gzip: 0.94 kB
dist/vue-lib-example.js  1.94 kB │ gzip: 0.92 kB
dist/style.css       2.65 kB │ gzip: 0.94 kB
dist/vue-lib-example.umd.cjs  1.83 kB │ gzip: 0.95 kB
✓ built in 198ms
✨  Done in 3.11s.

4. Integrating the lib in a HTML page

Commit

Once we have the exported files we can use them in any HTML web page like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My website</title>

    <link rel="stylesheet" href="/dist/style.css">
  </head>
  <body>
    <div id="the-div"></div>

    <script type="importmap">
      {
        "imports": {
          "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"
        }
      }
    </script>

    <script type="module">
      import { createApp } from 'vue';
      import { App as VueLibExample } from '/dist/vue-lib-example.js';

      createApp(VueLibExample).mount('#the-div');
    </script>
  </body>
</html>

Special attention to the <link> element importing the css style and <script type="importmap"> importing the vue library undert the vue name. It is like this how is expected in our library js.

Comments

1

You could simply solve using JS. I have done similar task to append my Vue widget into any other website. Here's an example code you can do:

   const appendWidget = () => new Promise((resolve, reject) => {
       let e = document.createElement("div")
       e.setAttribute("id", "widgetToAppend");
       document.body.appendChild(e);
       let app = new Vue({
         el: "#widgetToAppend",
         template: `<div>YOUR COMPONENTS</div>`
       })
       resolve(app);
    })

1 Comment

how would the end user add the above by a script tag? you will still need vue instance inside the script file.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.