VueJS 反应式绑定到模块导出 [英] VueJS reactive binding to module export

查看:32
本文介绍了VueJS 反应式绑定到模块导出的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆