State management with vue3 and webpack module federation - vue.js

I'm creating an app with MFE with Vuejs3 and webpack 5 module federation. One main app made with Vue will consume other apps (should be framework agnostic) and I need to share State from my Vue shell to other apps.
I tried making a store with the Composition api but the value get only updated in the app that call the event.
Here is the store that I expose from the vue shell:
import { reactive, toRefs } from 'vue'
const state = reactive({
data: {
quantity: 1,
},
})
export default function useStoreData() {
const updateQuantity = (quantity) => {
state.data.quantity += quantity
}
return {
...toRefs(state),
updateQuantity,
}
}
in vue shell :
<template>
<div>
<button #click="updateQuantity(1)">FOO +1</button>
<div>Quantity = {{ data.quantity }} </div>
</div>
</template>
<script setup>
import useStoreData from '../store/storeData'
const { updateQuantity, data } = useStoreData()
</script>
when I click on the button "FOO +1", value gets updated +1.
in my remote app:
<template>
<div>
<button #click="updateQuantity(5)">BAR +5</button>
<div>Quantity = {{ data.quantity }}</div>
</div>
</template>
<script setup>
import store from 'store/storeData'
const useStoreData = store
const { data, updateQuantity } = useStoreData()
</script>
When i click on button "BAR +5" the value get update +5
BUT everytime I click on one of those button, the value in the other app doesn't get updated.
What did I miss ?

Needed to add the shell app as a remote of itself then import the store in the shell app, same way as i'm doing in my remote app.
Here is the vue.config.js of the shell, where I need to expose AND remote the store.
const { ModuleFederationPlugin } = require('webpack').container
const deps = require('./package.json').dependencies
module.exports = {
publicPath: '/',
configureWebpack: {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
filename: 'remoteEntry.js',
remotes: {
test: 'test#http://localhost:8081/remoteEntry.js',
test2: 'test2#http://localhost:8082/remoteEntry.js',
store: 'shell#http://localhost:8080/remoteEntry.js', <= ADDED HERE the remote of itself
},
exposes: {
'./storeData': './src/store/storeData.js',
},
shared: [
{
...deps,
},
],
}),
],
},
devServer: {
port: 8080,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
},
},
}
In the Vue shell app:
<script setup>
// import useStoreData from '../store/storeData' <= wrong
import store from 'store/storeData' <= good
</script>

Related

vite 2 production env ref element is undefined with compostion api

I use vue3 with composition api, but when I build my project, the ref element always undefined.
I reproduced it, maybe I used it incorrectly, but I don't know why.
I defined a ref in hooks function.
const isShow = ref(false)
const rootRef = ref<HTMLDivElement>();
export default function () {
function changeShow() {
isShow.value = !isShow.value;
console.log(isShow.value, rootRef.value);
}
return { isShow, rootRef, changeShow };
}
Use rootRef in the HelloWorld.vue and linked element.
<script setup lang="ts">
import useShow from "../composables/useShow";
const { rootRef, isShow } = useShow();
</script>
<template>
<div ref="rootRef" v-show="isShow" class="test"></div>
</template>
Create a button in App.vue and bind click function.
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import useShow from "./composables/useShow";
const { changeShow } = useShow();
</script>
<template>
<button #click="changeShow">切换</button>
<HelloWorld />
</template>
When I click button, it works.
But when I build it and import from lib, it doesn't work.
My vite.config.ts is as follows:
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"#": path.resolve(__dirname, "src")
}
},
build: {
cssCodeSplit: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "my-project",
fileName: format => `my-project.${format}.js`
},
rollupOptions: {
external: ["vue"],
preserveEntrySignatures: "strict",
output: {
globals: {
vue: "Vue"
}
}
}
}
});
I think the problem is the definition of rootRef. It seems that only binding location can use it. This is no different from defining it in a component. I need to use it in multiple places.
Oddly, in this way, the Dev environment works fine, but Pro env is not available. Do I need to modify the build configuration of vite.
How do I do that?
The problem is your App.vue uses its own copy of composables/useShow instead of the one from the lib.
The solution is to export the composable from the lib so that your app can use the same one:
// src/index.ts
import { default as useShow } from './composables/useShow';
//...
export default {
//...
useShow
};
In App.vue, use the lib's composable:
import MyProject from "../dist/my-project.es";
const { changeShow } = MyProject.useShow();
GitHub PR

Monaco Editor Web Worker Issue with Vue 3

I was trying to implement Monaco Editor in Vue 3 application but I could not get the web worker running.
// Editor.vue
<template>
<div id="container">
<div id="editor-section"></div>
<button #click="runCode">Run</button>
</div>
</template>
<script>
import * as monaco from "monaco-editor";
import { onMounted } from "vue";
export default {
name: "Editor",
setup() {
let codeEditor = null;
function initEditor() {
codeEditor = monaco.editor.create(document.getElementById("editor-section"), {
value: "function hello() {\n\talert('Hello world!');\n}",
language: "javascript",
theme: "vs-dark"
});
}
function runCode() {
console.log("runCode");
console.log(codeEditor.getValue());
}
onMounted(() => {
initEditor();
})
return { codeEditor, runCode }
},
};
</script>
I am getting the Editor but there is this issue
I am using
// vue.config.js
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
module.exports = {
plugins: [
new MonacoWebpackPlugin()
]
};
Am I missing anything?
Should I care about the Issue anyway?
My goal of the project is I want to implement a web editor that takes the written file and compiles in docker container.
It looks like you put the plugin in the wrong place. It's supposed to be placed in configureWebpack which represents for webpack configuration instead:
vue.config.js
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
module.exports = {
configureWebpack: {
plugins: [
new MonacoWebpackPlugin(), // Place it here
]
},
// ...
}

Is there a way to use Hypenova with Nuxt and VueJs and make SSR?

I try to use Ara Framework to implement micro-frontend. I chose Nuxt framework as my main application which "concatanate" my micro-frontends. Micro-frontend are implemented with VueJs framework.
Here is one of my micro-frontend (in VueJs) which implement a pretty simple component :
ResumeFournisseur.vue:
<template>
<b-row>
<b-col cols="3">
<div>
LOGO
</div>
<div>
<label>Choisissez votre fournisseur :</label>
<select name="supplier" v-model="sellerSelectedValue">
<option v-for="fournisseur in fournisseurs"
:key="fournisseur.id"
v-bind:value="fournisseur.id">
{{ fournisseur.name }}
</option>
</select>
<button class="u-btn u-btn-primary">Voir les produits</button>
</div>
</b-col>
<b-col cols="9">
<h1>{{ sellerSelectedLabel }}</h1>
</b-col>
</b-row>
</template>
<script>
export default {
name: 'ResumeFournisseur',
props: {
supplierId: String
},
data() {
const fournisseurs = [
{
id: -1,
name: 'Aucun fournisseur'
},
{
id: 1,
name: 'Renault'
},
{
id: 2,
name: 'Acial'
}
];
return {
sellerSelectedValue: fournisseurs[0].id,
fournisseurs : fournisseurs,
sellerSelectedLabel: fournisseurs[0].name,
}
},
mounted() {
if (typeof this.supplierId != 'undefined'){
this.sellerSelectedValue = this.supplierId;
}
},
watch: {
sellerSelectedValue: function () {
const recup = this.fournisseurs.filter(four => four.id == this.sellerSelectedValue);
this.sellerSelectedLabel = recup[0].name;
}
}
}
</script>
And here my index.js file :
import hypernova from 'hypernova/server'
import {renderVuex, renderVue, Vue} from 'hypernova-vue/server'
import express from 'express'
import path from 'path'
import BootstrapVue from 'bootstrap-vue/dist/bootstrap-vue.esm';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
//import createStore from './store/modules/listComponent'
import ResumeFournisseur from './components/ResumeFournisseur';
Vue.use(BootstrapVue)
hypernova({
devMode: process.env.NODE_ENV !== 'production',
getComponent(name) {
switch (name) {
case 'ResumeFournisseur' :
return renderVue(name, Vue.extend(ResumeFournisseur));
}
},
port: process.env.PORT || 3001,
createApplication() {
const app = express()
app.use('/public', express.static(path.join(process.cwd(), 'dist')))
return app
}
})
Then in my Nuxt application I do :
<template>
<b-container fluid>
<div>
<nova name="ResumeFournisseur" :data="{}"/>
</div>
</b-container>
</template>
<script>
import Nova from "nova-vue-bridge";
import NovaClusterService from "../middleware/novaClusterService";
export default {
components: {
Nova
},
head () {
return {
title: 'Accueil',
script: [
{ src:
'http://localhost:3001/public/client.js'
}
]
}
}
}
</script>
It works pretty good.
But when I tried to use Nova Cluster aggregator combined with Nova Proxy, I don't know how to render my micro-fontend in my Nuxt application without using the http://localhost:3001/public/client.js.
Here my views.json file :
{
"ResumeFournisseur": {
"server": "http://localhost:3001/batch"
}
}
And here my nova-proxy.json file :
{
"locations": [
{
"path": "/",
"host": "http://localhost:3000",
"modifyResponse": true
}
]
}
(for remember, Nuxt is running on 3000 port).
I run ara run cluster --config ./views.json (as the documentation said). Then I run
set HYPERNOVA_BATCH=htpp://localhost:8000/batch
ara run:proxy --config nova-proxy.json
As I'm on Windows environnement I do a set.
When I make a post on nova cluster like :
{
"ResumeFournisseur": {
"name": "ResumeFournisseur",
"data": {
}
}
}
It makes a good response. Pretty good !! But nova proxy doesn't do anythning :(. Documentation said that if it's bound to nova cluster (thanks to HYPERNOVA_BATCH variable) it will able to render the views rendered by nova cluster.
Of course, if I embed the cluster reponse in a v-html directive (in my NuxtJs main application), the micro-frontend is embeded but is not doing anything (not interactive).
Am I missing something ?? I read a lot of documentation of this subject, and I'm doubting on my choices/understanding.
If there's anyone who could help me, it's could really great :) !!!
Finally, I found my missunderstanding. I have to go to http://localhost:8080 on my browser and it will call the nova proxy which calls himself the nova cluster.
In fact, you can't remove the client.js use, because thanks to that you got your business side.
I found an article on stackoverflow about this subject.

Call nuxt/axios module from external js/ts file

I am new to vue and trying to build my first vue app using nuxtjs. My problem right now has to do with architecture and folder structure.
In my other non-vue apps I always have a "services" directory where I keep all my code that makes http requests.
example under my services folder I will have a auth.ts file that contains code that posts login credentials to my API. This file/class returns a promise which I access from within my store.
I am trying to do this with vue using nuxtjs but I realised I am unable to access the axios module from anywhere aside my .vue file.
This is an example of how my code is now:
<template>
...
</template>
<script lang="ts">
import Vue from 'vue'
import ActionBar from '../../components/ActionBar.vue'
export default Vue.extend({
components: { ActionBar },
data() {
return {
example: ''
},
methods: {},
mounted() {
this.$axios.$get('/examples').then((res) => {
this.examples = res.data;
})
}
})
</script>
<style>
...
</style>
I would like to move the axios calls to their own files in my services folder. How do I do this?
what you can do is create a file inside the ./store folder, let's imagine, ./store/products.js, that will create a products store, inside, simple getters, mutations and actions:
export const state = () => ({
products: [],
fetchingProducts: false,
})
export const getters = {
getAllProducts(state) {
return state.products
},
hasProducts(state) {
return state.products.length > 0
},
isFetchingProducts(state) {
return state.fetchingProducts
},
}
export const mutations = {
setInitialData(state, products) {
state.products = products
},
setLoadingProducts(state, isLoading) {
state.fetchingProducts = isLoading
},
}
export const actions = {
async fetchProducts(context, payload) {
context.commit('setLoadingProducts', true)
const url = `/api/example/${payload.something}`
const res = await this.$axios.get(url)
context.commit('setInitialData', res.data)
context.commit('setLoadingProducts', false)
},
}
then in your .vue file, you can now use the store as:
<template>
<div>
<div v-if="isFetchingProducts"> loading... </div>
<div v-else-if="!hasProducts">no products found</div>
<div v-else>
<ul>
<li v-for="product in allProducts" :key="product.id">
{{ product.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data () {
return {
products: []
}
},
methods: {
...mapGetters({
isFetchingProducts: 'products/isFetchingProducts',
allProducts: 'products/getAllProducts',
hasProducts: 'products/hasProducts',
})
},
mounted() {
this.$store.dispatch('products/fetchProducts', {})
},
}
</script>
<style>
...
</style>
remember that:
to call a store action, you should use $store.dispatch()
to call a mutation, you should use $store.commit()
to call a getter, you should use $store.getter()
you can also use the Vuex helper mapGetters, mapActions and even mapMutations
You might also know that you can leverage the Plugins in Nuxt, that article has demo code as well so you can follow up really quick

Module parse failed: Unexpected token (1:0) vue.js vuex store

Can't figure out this error with vuex store and vue.js:
Is this a webpack-cli thing? or am i not doing something right? Thanks for the help!
Module parse failed: /Users/me/sites/vue/src/components/SectionView.Vue Unexpected token (1:0)
You may need an appropriate loader to handle this file type.
| <template>
| <ul class="listing">
| <li v-for="item in $store.state.items">
# ./~/babel-loader/lib!./~/vue-loader/lib/selector.js?type=script&index=0!./src/components/PanelBody.vue 3:0-56
# ./src/components/PanelBody.vue
# ./~/babel-loader/lib!./~/vue-loader/lib/selector.js?type=script&index=0!./src/components/Panel.vue
# ./src/components/Panel.vue
# ./~/babel-loader/lib!./~/vue-loader/lib/selector.js?type=script&index=0!./src/components/Accordion.vue
# ./src/components/Accordion.vue
# ./~/babel-loader/lib!./~/vue-loader/lib/selector.js?type=script&index=0!./src/components/Sidebar.vue
# ./src/components/Sidebar.vue
# ./~/babel-loader/lib!./~/vue-loader/lib/selector.js?type=script&index=0!./src/components/Body.vue
# ./src/components/Body.vue
# ./src/router/index.js
# ./src/main.js
# multi ./build/dev-client ./src/main.js
My SectionView.vue file:
<template>
<ul class="listing">
<li v-for="item in $store.state.items">
<router-link :to="{ name: 'item', params: { id: item.id }}">
<img :src="item.image" />
<br>{{ item.name }}
</router-link>
</li>
</ul>
</template>
<script>
import Item from '../components/Item',
export default {
name: 'SectionView',
components: {
'item': Item
},
created () {
this.$store.dispatch('fetch')
}
},
}
</script>
My Item.vue:
<template>
<div id="section-view">
<div class="item-view">
<router-link class="back-listing" :to="{name: 'section'}">U+0003C</router-link>
<div class="item">
<h1>{{ item.name }}</h1>
</div>
</div>
</template>
<script>
export default {
name: 'item',
computed: {
item: function () {
return this.$store.state.items.find(item => item.id === this.$route.params.id)
}
},
created () {
this.$store.dispatch('open', this.$route.params.id)
}
}
</script>
My store which is in a src/store/index.js:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const db = [
{ id: 'a', name: 'Item #1', image: 'http://lorempicsum.com/simpsons/350/200/1' },
{ id: 'b', name: 'Item #2', image: 'http://lorempicsum.com/simpsons/350/200/2' },
{ id: 'c', name: 'Item #3', image: 'http://lorempicsum.com/simpsons/350/200/3' }
]
const store = new Vuex.Store({
state: {
items: [],
opened: {}
},
actions: {
fetch: function ({commit, state}, payload) {
commit('SET_LIST', db) // Just clone the array
},
open: function ({commit, state}, payload) {
// look for item in local state
var localItem = state.items.find(item => payload === item.id)
if (!localItem) {
new Promise(function (resolve, reject) {
return db.find(item => payload === item.id)
})
.then(result => {
commit('ADD_ITEM', result)
})
}
}
},
mutations: {
SET_LIST: function (state, payload) {
Vue.set(state, 'items', payload)
},
ADD_ITEM: function (state, playload) {
state.items.push()
}
}
})
console.log('State', store)
export default store
And my main.js calling the store:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import store from './store'
import App from './App'
import router from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
router,
template: '<App/>',
components: { App }
})
Like #MateenRay said the ".Vue" instead ".vue" really could be the issue.
This is all about your operating system.
On windows "FILE.txt" and "file.txt" are the same. So when calling you vue file upper or down case it doesn't matter.
But for linux "FILE.txt" and "file.txt" are two separate files ! So calling your ".Vue" file is not the same as calling the ".vue" one.
Its important to remember this because on my project we were on windows and didn't pay attention to this. Next we changed all our file names from "file" to "File". Next we had to deliver it on a linux machine and nothing was working because some files were not found... Some of us were still importing files like the following :
import file from ... instead of import File from ...
It was working on windows but not on linux.
/Users/me/sites/vue/src/components/SectionView.Vue
I think the name SectionView.Vue should be SectionView.vue
components extensions are vue
For me, reinstalling babel and webpack fixed this issue.
npm install babel-core babel-loader babel-preset-es2015 webpack --save-devnpm install babel-core babel-loader babel-preset-es2015 webpack --save-dev
Link