How to dynamically access a remote component in vue js with module federation - vue.js

I am trying to build a vue js 2 microfrontend with module federation. I dont want to use static remote imports via the webpack.config.js like this
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1#http://localhost:3001/remoteEntry.js',
},
}),
],
};
I am looking for a way to dynamically import vue components into my host application. I tried this approach so far, but i only found examples that worked with angular or react.
The goal is to have multiple remote frontends that can automatically register somewhere, maybe in some kind of store. The host application then can access this store and get all of the registered remote applications (name, url, components). The host application then loads the components and should be able to use them. I remote import the component HelloDerp, the loading process is working fine but i dont know how to render it on my host application. I read the vue js doc about dynamic and async imports but i think that only works for local components.
What i've got so far in the host application:
<template>
<div id="app">
<HelloWorld />
<HelloDerp />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
const HelloDerp = null;
export default {
name: "App",
components: {
HelloWorld,
HelloDerp,
},
mounted() {
var remoteUrlWithVersion = "http://localhost:9000/remoteEntry.js";
const element = document.createElement("script");
element.type = "text/javascript";
element.async = true;
element.src = remoteUrlWithVersion;
element.onload = () => {
console.log(`Dynamic Script Loaded: ${element.src}`);
HelloDerp = loadComponent("core", "./HelloDerp");
};
document.head.appendChild(element);
return null;
},
};
async function loadComponent(scope, module) {
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__("default");
const container = window[scope]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
}
</script>

Sorry i almost forgot about this. Here's my solution.
Load Modules:
export default async function loadModules(
host: string,
ownModuleName: string,
wantedNames: string[]
): Promise<RemoteComponent[]> {
...
uiApplications.forEach((uiApplication) => {
const remoteURL = `${uiApplication.protocol}://${uiApplication.host}:${uiApplication.port}/${uiApplication.moduleName}/${uiApplication.fileName}`;
const { componentNames } = uiApplication;
const { moduleName } = uiApplication;
const element = document.createElement('script');
element.type = 'text/javascript';
element.async = true;
element.src = remoteURL;
element.onload = () => {
componentNames?.forEach((componentName) => {
const component = loadModule(moduleName, `./${componentName}`);
component.then((result) => {
if (componentName.toLowerCase().endsWith('view')) {
// share views
components.push(new RemoteComponent(result.default, componentName));
} else {
// share business logic
components.push(new RemoteComponent(result, componentName));
}
});
});
};
document.head.appendChild(element);
});
});
...
}
export default async function loadModule(scope: string, module: string): Promise<any> {
await __webpack_init_sharing__('default');
const container = window[scope]; // or get the container somewhere else
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
}
Add Modules to routes
router.addRoute({
name: remoteComponent.componentName,
path: `/${remoteComponent.componentName}`,
component: remoteComponent.component,
});

Related

How to perform actions before providing downloadable files in Vue

I want to be able to keep track of file downloads in a Vue project. The goal is to provide a url like mysite.com/some/path/file-name.txt/tracking-source, perform an action like send the path to tracking api, then serve the file at mysite.com/some/path/file-name.txt
I tried using a redirect but it doesn't seem to provide a file download, it just updates the path in the browser.
use a route that captures the "tracking-source" parameter and performs the necessary tracking action, and then serves the file using the sendFile method from the express library.
Here is an example of how to set up a route in a Vue project using the vue-router library:
import Vue from 'vue'
import Router from 'vue-router'
import path from 'path'
import express from 'express'
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/some/path/:fileName/:trackingSource',
name: 'download-file',
component: {
beforeRouteEnter (to, from, next) {
const { params } = to
// Perform tracking action using the trackingSource parameter
// ...
// Serve the file
const filePath = path.join(__dirname, 'path/to/files', `${params.fileName}.txt`)
express.sendFile(filePath, (err) => {
if (err) next(err)
})
}
}
}
]
})
here the route captures the "fileName" nd "trackingSource" parameters from the URL, and uses the beforeRouteEnter navigation guard to perform the tracking action and serve the file.
without express you can do something like this
<template>
<div>
<a ref="downloadLink" :href="fileUrl" download>Download</a>
<button #click="downloadFile">Download</button>
</div>
</template>
<script>
export default {
data() {
return {
fileUrl: ''
}
},
methods: {
async downloadFile() {
const { params } = this.$route
const fileName = `${params.fileName}.txt`
const filePath = `/path/to/files/${fileName}`
const response = await fetch(filePath)
if (!response.ok) {
throw new Error(`Failed to fetch file: ${response.status}`)
}
const blob = await response.blob()
this.fileUrl = window.URL.createObjectURL(blob)
this.$refs.downloadLink.click()
}
}
}
</script>
Since I also store my files in the public/files directory of the vue project, I opted to not fetch it.
{
path: '/files/:fileName/:source',
redirect: to => {
const fileName = to.params.fileName
logEvent(analytics, fileName, {source: to.params.source});
const a = document.createElement('a');
document.body.appendChild(a);
a.href = `/files/${fileName}`;
a.download = fileName;
a.click();
setTimeout(() => {
window.URL.revokeObjectURL(a.href);
document.body.removeChild(a);
}, 0)
return {path: '/' }
}
}

CASL - Vue 3 - Element not showing for role

I am having a bit of a challenge implementing CASL in my app.
I have created the following composable useAppAbility ("hook") that defines all the rules:
import { AbilityBuilder, createMongoAbility, subject } from "#casl/ability";
import { useAbility } from "#casl/vue";
const service = {};
const user = {};
const subscription = {};
const invoice = {};
const account = {};
const ability = createMongoAbility();
const ROLES = ["admin", "account_owner", "beneficiary", "super_admin"];
const defineAbilityFor = (role: Object) => {
const { can, rules } = new AbilityBuilder(createMongoAbility);
const is = (r: string) => {
return ROLES.indexOf(r) >= ROLES.indexOf(role);
};
if (is("admin")) {
can("add", subject("User", user));
can("remove", subject("User", user));
}
return ability.update(rules);
};
export { defineAbilityFor, ability, subject };
export const useAppAbility = () => useAbility();
Added the plugin to the main.ts:
import { ability } from "#/composables/useAppAbility";
import { abilitiesPlugin } from "#casl/vue";
createApp(App)
.use(abilitiesPlugin, ability, {
useGlobalProperties: true,
})
//stuff
.mount("#app");
And then, I found that using the beforeEach hook in the router and passing in the user before each route was the simplest way to deal with page load and SPA routing.
I have therefore added the following to my router/index.ts:
import { ability, defineAbilityFor } from "#/composables/useAppAbility";
import useAuth from "#/composables/useAuth";
const {
getUserByClaims,
} = useAuth();
// routes
router.beforeEach(async (to, _from, next) => {
defineAbilityFor(getUserByClaims.value.roles)
})
At this stage I can verify that the user is being passed properly to the defineAbilityFor function and when using the ability.on("update") hook to log the rules object, I have the following output:
Which seems to confirm that the rules for this user are built and updated correctly?
However, when trying to display a button for the said admin in a component, the button does not show.
MyComponent.vue:
<script setup lang="ts">
import { useAppAbility, subject } from "#/composables/useAppAbility";
const { can } = useAppAbility();
</script>
<template>
<div v-if="can('add', subject('User', {}))">TEST FOR CASL</div> <!-- DOES NOT SHOW-->
</template>
Not sure where to go from there, any help would be appreciated.
Thanks

switch between root component based on different url parameter

i am developing a vujs based museum installation with several clients and one server. I would like to develop the two apps, client and server, in one application.
when calling the url I want to read out the parameters.
https://example.com/mode=client&id=1 or mode=server
then I want to load different root components with creatapp
if server .. const app = Vue.createApp(serverComponent)
if client ... const app = Vue.createApp(serverComponent)
is that a good way?
if so, how can I pass the clientID directly into the root component
EDITED
its simple to pass props to the root component with Vue.createApp(clientComponent, {id:id,...})
but currently I fail to choose between 2 different root components.
following setup
import App from './App.vue'
import AppServer from './AppServer.vue'
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const mode = urlParams.get('mode')
if (mode == "server"){
let app = createApp(AppServer);
} else {
let id = urlParams.get('id')
app = createApp(App, { id: parseInt(id) } );
}
but let app = createApp(AppServer); throws an error. app never initialised
I've implemented and tested the functionality that you need.
import Vue from 'vue'
import App from './App'
import AppServer from './AppServer'
Vue.config.productionTip = false
const NotFound = {
template: '<p>Page not found</p>'
}
/* eslint-disable no-new */
new Vue({
el: '#app',
data: {
currentRoute: window.location.pathname
},
methods: {
RequestDispatcher (url) {
let urlParams = new URLSearchParams(url)
if (url.includes('mode=server')) {
return AppServer // createApp(AppServer)
} else if (url.includes('mode=client')) {
let id = urlParams.get('id')
console.log(id)
return App // createApp(App, { id: parseInt(id) })
}
}
},
computed: {
ViewComponent () {
return this.RequestDispatcher(this.currentRoute) || NotFound
}
},
render (h) {
return h(this.ViewComponent)
}
})

Socket.io with Vue3

I have a Vue 3 app and an express server. The server does not serve any pages just acts as an API so no socket.io/socket.io.js file is sent to client.
I am trying to set up socket.io in one of my vue components but whatever I try does not work. Using vue-3-socket.io keeps giving 't.prototype is undefined' errors.
I have tried vue-socket.io-extended as well with no luck.
Any advice would be appreciated as to the reason and solution for the error above, I have tried various SO solutions without success, and the best way forward.
You can use socket.io-client. I have used socket.io-client of 4.4.1 version.
step: 1
Write class inside src/services/SocketioService.js which returns an instance of socketio.
import {io} from 'socket.io-client';
class SocketioService {
socket;
constructor() { }
setupSocketConnection() {
this.socket = io(URL, {
transports: ["websocket"]
})
return this.socket;
}
}
export default new SocketioService();
Step 2:
Import SocketioService in App.vue. You can instantiate in any lifecycle hook of vue. I have instantiated on mounted as below. After instantiation, I am listening to welcome and notifications events and used quasar notify.
<script>
import { ref } from "vue";
import SocketioService from "./services/socketio.service.js";
export default {
name: "LayoutDefault",
data() {
return {
socket: null,
};
},
components: {},
mounted() {
const socket = SocketioService.setupSocketConnection();
socket.on("welcome", (data) => {
const res = JSON.parse(data);
if (res?.data == "Connected") {
this.$q.notify({
type: "positive",
message: `Welcome`,
classes: "glossy",
});
}
});
socket.on("notifications", (data) => {
const res = JSON.parse(data);
let type = res?.variant == "error" ? "negative" : "positive";
this.$q.notify({
type: type,
message: res?.message,
position: "bottom-right",
});
});
},
};
</script>

How do I seperate ipcMain.on() functions in different file from main.js

I'm creating an electron app with vuejs as frontend. How can I create all the ipcMain.on() functions in a separate file from the main.js. I want this for a more clean code structure.
The app has to work offline so I need to store the data in a local database. So when I create an object in the frontend, I send it with ipcMain to the electron side. Electron can then write it to the local database.
I want something like this:
main.js:
import { app, protocol, BrowserWindow } from "electron";
import {
createProtocol,
installVueDevtools
} from "vue-cli-plugin-electron-builder/lib";
require("./ipcListeners.js");
ipcListeners.js:
import { ipcMain } from "electron";
ipcMain.on("asynchronous-message", (event, arg) => {
console.log(arg);
event.reply("asynchronous-reply", "pong");
});
ipcMain.on("random-message", (event, arg) => {
console.log(arg);
event.reply("random-reply", "random");
});
The problem here is that only the first ipcMain.on() functions works but the second,... doesn't
What I did in my project is, I arranged all the IPCs in different folders according to their categories and in every file, I exported all the IPCs like the example below
products.js
module.exports = {
products: global.share.ipcMain.on('get-products', (event, key) => {
getProducts()
.then(products => {
event.reply('get-products-response', products)
})
.catch(err => {
console.log(err)
})
})
}
then I created a new file which imported all the exported IPCs
index.js
const {products} = require('./api/guestIpc/products')
module.exports = {
products
}
then finally I imported this file into the main.js file.
main.js
const { app, BrowserWindow, ipcMain } = require('electron')
global.share = {ipcMain} #this is only for limiting the number of ipcMain calls in all the IPC files
require('./ipc/index') #require the exported index.js file
That's it, now all the external IPCs are working as if they were inside the main.js file
i don't know this going to help any way i am going to post what i did. now your implementation worked for me but still i had problem if i am requiring 100 files now in all those requires i have to import ipcMain repeatedly so that's going to be a performances issue so what i did is created global object by inserting electon and IpcMain then it's worked perfectly
my Main.js file
const { app, BrowserWindow } = require('electron')
const electron = require('electron');
const { ipcMain } = require('electron')
global.share= {electron,ipcMain};
function createWindow () {
// Create the browser window.
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
win.loadFile('./views/index.html')
// Open the DevTools.
win.webContents.openDevTools()
}
app.whenReady().then(createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
require('./test.js');
test.js
global.share.ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.returnValue = 'pong'
})
this is my html call
const { ipcRenderer } = require('electron')
document.querySelector('.btn').addEventListener('click', () => {
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
})
Now this one worked me perfectly so no more messy and long main.js resources