vuex-webextensions
vuex-webextensions copied to clipboard
Vuex state with a Class inside is working in background, but not in popup
Hey! I finally have some time to use this Vuex plugin on a real project! :)
I think my problem is due to serialization/unserialization (JSON?) of my store state.
Given my store is:
import Vue from 'vue';
import Vuex from 'vuex';
import { Channel } from '...';
export const store = new Vuex.Store({
state: {
channels: [new Channel('foo', 'bar')]
},
getters: {
firstChannel(state) {
return state.channels[0];
}
}
})
When I use it in background
, store.getters.firstChannel
is an instance of Channel
class.
But when I use it in my popup page inside a Vue component, this.$store.getters.firstChannel
(or mapGetters(['firstChannel'])
) is a plain object, without prototype.
I think it's a problem of serialization because some months ago, I had the same problem with chrome.runtime.sendMessage()
. For each received data, I had to manually set __proto__
property:
<!-- in a .vue file -->
<script>
import { Channel } from '...';
export default {
data() {
return {
channels: []
}
},
methods: {
retrieveChannels() {
chrome.runtime.sendMessage({ type: 'GET_CHANNELS' }, response => {
this.channels = response.data.channels.map(channel => {
channel.__proto__ = Channel.prototype;
return channel;
});
});
},
},
}
</script>
But in this case I don't know how to do this.
Maybe a store watcher inside my popup/main.js
or a Vue watcher inside popup/App.vue
?
What do you think?
Thanks! :)
Okay so I found a workaround, it's not really pretty but it works.
I'm using a computed property that use a store's getter, and set prototypes on the fly.
In my case, I do this:
<script>
import { Game } from '../entities/Game';
import { Stream } from '../entities/Stream';
import { Channel } from '../entities/Channel';
export default {
computed: {
twitchChannels() {
return this.$store.getters.twitchChannels.map(channel => {
channel.__proto__ = Channel.prototype;
if (channel.stream) {
channel.stream.__proto__ = Stream.prototype;
if (channel.stream.game) {
channel.stream.game.__proto__ = Game.prototype;
}
}
return channel;
});
},
}
}
</script>
EDIT: Something even better, make the getter setting prototypes on the fly (so we only do this only ONE time):
import { Game } from '../entities/Game';
import { Stream } from '../entities/Stream';
import { Channel } from '../entities/Channel';
export new Vuex.Store({
state: {
twitchChannels: [new Channel(...)],
},
getters: {
twitchChannels(state) {
return state.twitchChannels.map(channel => {
channel.__proto__ = Channel.prototype;
if (channel.stream) {
channel.stream.__proto__ = Stream.prototype;
if (channel.stream.game) {
channel.stream.game.__proto__ = Game.prototype;
}
}
return channel;
});
}
}
});
Hey @Kocal, nice to see you here ^^
In first place thanks for the report and sorry for the delay, I'm passing on a very busy season and I can't answer before.
It's the first time that I see a Vuex state storing a class instance, so I can't help so much before mount this enviroment and test it.
The thing are amazing, didn't know that this are possible before, I think that the problem are on the serialization of data on the webextension message, this probably it's breaking the class object at some point.
Are your project opensource to test it? If not don't worry, I can mount a simple enviroment to test this issue.
Greetings
Hey, don't worry, it can happen to anyone :)
Yes I'm regularly using classes a DTO because I can implement some useful methods on them.
And since I need to keep my objects synchronized between the background part and the popup part, I have to store them in Vuex (alongside your plugin :stuck_out_tongue:)
I didn't checked the plugin source code, but I suppose you serialize data with JSON.stringify()
and unserialize them with JSON.parse
?
My trick is working because I can import my classes and then update __proto__
prop, but I don't think we will be about to do something in the plugin... :confused:
My project is not opensourced (it's a project for a client), it's inside a private repo but I can add you as a collaborator. :slightly_smiling_face:
Or maybe we can manually register prototypes:
import { Channel } from './entities/Channel';
import { Game } from './entities/Game';
import { Stream } from './entities/Stream';
export new Vuex.Store({
plugins: [
VuexWebExtensions({
serializationPrototypes: {
Channel,
Game,
Stream,
}
})
],
});
Before serializing:
- we recursively iterate on the state to find objects that have a prototype different from
Object
- in this object, we store the name of the prototype under a private property, something like:
$__PROTOTYPE_NAME__$
(to be sure that will never conflict with the user's classes)
After unserializing:
- we recursively iterate on the unserialized data, trying to find
$__PROTOTYPE_NAME__$
prop - when we found it, we can set the prototype like this:
obj.__proto__ = this.serializationPrototypes[obj.$__PROTOTYPE_NAME__$].prototype
I think it can works, but we should be careful about nested objects.
Hi again @Kocal, after some days researching for this, can't offer any solution for now.
I don't apply any serialization (the browser do it automatically), I try some things, only one work but it's so dangerous.
The unic way that I found to restore class automatically with the plugin, it's jsonify the class methods as plain text, restore it with eval and then restore the values, it is the only thing that work for now but I can't implement it because security reasons.
Eval are so dangerous and the review team of any browser gona reject the extensions that use it on any part.
I don't have any more things for now, the best way that I see to restore the class, it's create new instance and then merge values with it but I can't do this on the plugin, you should import the class and create new instance manually.
I think the best way it's add proto inside computed property, if you have any more things just say to me to try,
Hi 👋
I don't apply any serialization (the browser do it automatically), I try some things, only one work but it's so dangerous.
Ah yes, I forgot that you use sendMessage()
method that automatically serialize data.
The unic way that I found to restore class automatically with the plugin, it's jsonify the class methods as plain text, restore it with eval and then restore the values, it is the only thing that work for now but I can't implement it because security reasons.
And what about https://github.com/MitsuhaKitsune/vuex-webextensions/issues/17#issuecomment-473802209?
This can be working I guess, it need a optional extra initialization and a serializer class to compare data structure of specified classes and assign his prototype, but can work I guess.
I gona prepare and test this think and I come here with the feedback and the feature if it works.
Or maybe we can manually register prototypes:
import { Channel } from './entities/Channel'; import { Game } from './entities/Game'; import { Stream } from './entities/Stream'; export new Vuex.Store({ plugins: [ VuexWebExtensions({ serializationPrototypes: { Channel, Game, Stream, } }) ], });
Before serializing:
- we recursively iterate on the state to find objects that have a prototype different from
Object
- in this object, we store the name of the prototype under a private property, something like:
$__PROTOTYPE_NAME__$
(to be sure that will never conflict with the user's classes)After unserializing:
- we recursively iterate on the unserialized data, trying to find
$__PROTOTYPE_NAME__$
prop- when we found it, we can set the prototype like this:
obj.__proto__ = this.serializationPrototypes[obj.$__PROTOTYPE_NAME__$].prototype
I think it can works, but we should be careful about nested objects.
What level of nested structure depth should be cloned ?