VueJS 反应式绑定到模块导出 [英] VueJS reactive binding to module export
问题描述
I'm new to Vue and I'm trying to bind a component value to a property of an exported object. The initial value is set correctly but it's not reactive. I'm not sure I'm using the right terminology, but the relevant sections are
// Settings.js
export const settings = { showOverlay: true }
// Overlay.vue
<template>
<div v-show="enabled"> Some stuff </div>
</template>
<script>
import { settings } from "../js/Settings.js";
export default {
data() {
return {
enabled: settings.showOverlay
};
}
};
</script>
Now, I know that the exported object (settings
) is a read-only view onto the object, because that's how modules work, so probably Vue can't put its hooks into it. The thing is, I want the setting to be "owned" by this Settings service, which is responsible for persisting the values between page loads, but I don't feel like the service should have to be aware that the component wants to watch a value and take care of manually triggering updates on the component when the value changes -- I probably just misunderstand the pattern I'm supposed to use for cases like this.
This is being built with Webpack / babel, if that makes any difference.
I'm feeling a little bit sheepish at the moment. I went down a little rabbit hole based on some syntax I saw in your question and that let to a whole bunch of unnecessary gyrations. The syntax was this:
data() {
return {
enabled: settings.showOverlay
};
}
Which, for some reason, I interpreted as "well sure, whenever enabled
changes, settings.showOverlay
is going to change because Vue is reactive".
Yeah, no.
In that code, settings.showOverlay
is just the initial value for the enabled
property. The enabled
property will be reactive, but in no way is it going to pass values to the settings object. Basically the data function returns an object with an enabled property that has an initial value of whatever settings.showOverlay
is and then that object is turned into a reactive object.
If you want the changes made in Vue to be passed along to your settings object then all you need to do is expose the settings object on Vue's data object.
data() {
return {
settings,
};
}
Now if you have code like
<div v-show="settings.showOverlay"> Some stuff </div>
<button @click="settings.showOverlay= !settings.showOverlay"></button>
settings.showOverlay
will not only be reactive in the Vue, but in the settings
object. No need for any of the hoops I jumped through below (/facepalm).
FWIW I believe some of the links I mentioned in the comments are referring to the data object itself. The data object needs to be a plain javascript object, not necessarily all the properties on it.
In other words, in
data() {
return something
}
something
must be a plain javascript object.
Original Answer
I've done this in a couple ways in my Vue apps. In my first app I wanted to do the same thing, store the settings in an external module that could manage persisting the settings and expose those settings on my Vue. I ended up writing a class that looks like this.
class Settings {
constructor(){
// read settings from persisted solution
}
get(key){
// return "key" from settings
}
set(key){
// set "key" in settings
}
save(){
// save settings to persisted solution
}
}
export default Settings
And then used that in my Vue like this.
import Settings from "./settings"
new Vue({
data:{
someSetting: Settings.get("someSetting")
}
})
And then some point later, trigger set() and save(). That point for me was whenever a route change was triggered, I'd just set all the settings back to the Settings object and then save.
It sounds like what you have is you're exporting an object that has getter/setter properties possibly something like this.
export const settings = {
overlay: stored.showOverlay,
get showOverlay(){
return this.overlay
},
set showOverlay(v){
this.overlay = v
}
}
Where you maybe trigger a save when set
is triggered. I like that idea better than the solution I described above. But getting it to work is a little more work. First I tried using a computed.
new Vue({
computed:{
showOverlay: {
get(){ return settings.showOverlay }
set(v) { settings.showOverlay = v }
}
}
})
But that doesn't quite work because it doesn't reflect changes to the Vue. That makes sense because Vue doesn't really know the value changed. Adding a $forceUpdate
to the setter doesn't work either, I expect because of the caching nature of computed values. Using a computed in combination with a data property, however, does work.
new Vue({
data(){
return {
showOverlay_internal: settings.showOverlay
}
},
computed:{
showOverlay: {
get(){ return this.showOverlay_internal }
set(v) {
settings.showOverlay = v
this.showOverlayInternal = v
}
}
}
})
That changes both the state of the Vue and triggers the change in the settings object (which in turn can trigger persisting it).
But, damn, that's a lot of work.
It's important to remember sometimes, though, that the objects we use to instantiate Vue are just plain old javascript objects and we can manipulate them. I wondered if I could write some code that creates the data property and the computed value for us. Taking a cue from Vuex, yes we can.
What I ended up with was this.
import {settings, mapSetting} from "./settings"
const definition = {
name:"app"
}
mapSetting(definition, "showOverlay"
export default definition
mapSetting
does all the work we did above for us. showOverlay
is now a computed property that reacts to changes in Vue and updates our settings object. The only drawback at the moment is that it exposes a showOverlay_internal
data property. I'm not sure how much that matters. It could be improved to map multiple properties at a time.
Here is the complete code I wrote that uses localStorage
as a persistence medium.
function saveData(s){
localStorage.setItem("settings", JSON.stringify(s))
}
let stored = JSON.parse(localStorage.getItem("settings"))
if (null == stored) {
stored = {}
}
export const settings = {
overlay: stored.showOverlay,
get showOverlay(){
return this.overlay
},
set showOverlay(v){
this.overlay = v
saveData(this)
}
}
function generateDataFn(definition, setting, internalName){
let originalDataFn = definition.data
return function(){
let data = originalDataFn ? originalDataFn() : {}
data[internalName] = settings[setting]
return data
}
}
function generateComputed(internalName, setting){
return {
get(){
return this[internalName]
},
set(v){
settings[setting] = v
this[internalName] = v
}
}
}
export function mapSetting(definition, setting){
let internalName = `${setting}_internal`
definition.data = generateDataFn(definition, setting, internalName)
if (!definition.computed)
definition.computed = {}
definition.computed[setting] = generateComputed(internalName, setting)
}
这篇关于VueJS 反应式绑定到模块导出的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!