Platform-Micro Component State Management in vivo Wukong Activity

This article was first published on vivo Internet Technology WeChat Public Number
Links:   https://mp.weixin.qq.com/s/1DzTYIExVbK0uE_Oc7IHYw
Author: Wukong Radio R&D Team

Wukong Event Platform series of exciting articles in the past:

1. Background

In the last article [ Status Management of Platform-Micro Components in Wukong Activity (Top) In this article, we review the status management and the design behind the micro-components on the activity page.From the earliest EventBus upgrade iteration to the Pre-script scenario, it finally returns to the Vuex unified state management mode, which seamlessly integrates Vuex into the development of active pages through technological innovation according to the characteristics of the platform.In this article, we will continue to explore micro-component state management in both platform and cross-sandbox environments.

2. Results

Starting from the actual business scenario, we keep thinking about the requirements behind the business, rationally design the architecture and finally solve the state management in different scenarios.Specifically as follows:

  1. Within the platform, we solved the connection and state management between the micro-components and the platform.For example, business microcomponents need to be aware of key platform actions, such as activity saving, component deletion within the editor, and so on.
  2. In the Security Sandbox in the Platform Editor, we solved the connection and state management between the micro-components and the configuration panel across the sandbox.

3. State management between micro-components and platforms

(Fig.1)

1. Background

As shown in Figure 1, this is the Edit Page where our platform creates active pages. On the left is the Visualization Editor area, and on the right is the Attribute Panel area, which can be personalized for the currently selected components.According to our business requirements, components need to be aware of some core actions of the platform, such as saving activities, deleting components, and so on.Once the micro-component is aware of these operations, it will execute corresponding custom business logic, such as parameter checking, business checking, error prompting, and so on.

The structure of a standard component according to the platform's development specifications is as follows:

hello-goku/          # Directory where the current plug-in is located
├── code.vue         # Code files for the current plug-in - will be displayed on the left side of the above image [Editor area]
├── prop.vue         # Module File for code Component Configuration - Show on the right [Property Panel] Area Configuration Component Property Awareness Platform Operations
└── setting.json     # configuration file
├── package.json     # npm module information

Through the above background introduction, I believe that I have a perceptual understanding of business scenarios, abstracted as a technical solution is how to solve the connection between micro components and platforms, and how do platforms manage these states?This is a complex issue, and ultimately we address business needs by designing a hook mechanism between components and platforms.

2. Difficulties

What difficulties will we face?

  1. Following the development specifications described above, when the platform triggers a save action,Prop.vuePlugin hooks need to be aware, so the platform needs to be able to collect them in advanceProp.vueAll hooks inside.howeverProp.vueIs loaded asynchronously, only if correspondingCode.vueWhen components are selected for configuration in the Editor, they are dynamically loaded on the property surface on demand.
  2. When deleting components in the Editor, the deleted components should be aware.
  3. The internal widget supports dragging to change the rendering order, so hooks collected by the platform must strictly bind the rendering order, otherwise incorrect hook calls will occur.

3,hook?

What is the hook mechanism?Hook is the lifecycle method by which a micro-component can register a series of platforms, which are automatically collected by the platform and invoked at key nodes of the platform.

4. Use hook s

stayProps.vueIn a component's mixins, the platformActionHook is used as a mixin to register life cycle methods for each key node.All lifecycle methods automatically inject vue's component instance object, which can be accessed directly through this object, making it easy for the lifecycle method in the hook to get the state and method of the Vue instance.When the prop component is loaded, the platformActionHook invokes the platform's ability to automatically collect internal hook methods.The code example is as follows:

// prop.vue
export default {
  mixins: [
    platformActionHook({
      /*
       * Register Platform for hook s before activity is saved
       */
      beforeSaveTopicHook() {/* TODO Business logic processing such as parameter checking */},
      /**
       * Register platform for hook s after activity is saved
       */
      afterSaveTopicHook() {},
      /**
       * Register the platform to remove the hook for the current plugin
       */
      beforeDeletePluginHook() {},
      /**
       * Register the platform to remove the hook after the current plugin
       */
      afterDeletePluginHook() {}
    })
  ]
};

5. Platform collects all hook s at once

As mentioned above, becauseProp.vueRendering is loaded dynamically as the corresponding widget is selected in the Editor, but we need a mechanism to collect all hook methods in the widget at once.How?Pre-Rendering, yes, the answer is Pre-Rendering.Platform preselects to get all the plug-ins (umd mode) that make up the active page, and through new Function turns the string of the UMD component into an object instance of the Vue, so that all registered hooks can be filtered outAnd then prerender the main interface once (hidden rendering), [attribute component] is prerendered, platformActionHook automatically aggregates hook lifecycle methods to the platform.

6. Pre-Rendering - A Masterpiece of Micro-Components

By designing prerender-Prop.vuePre-rendering attribute components, with the help of vue's powerful dynamic component capabilities, go directly to our pain points.If we don't need error backtracking on the UI, we can also override the render method of the widget so that no dom nodes are generated, thereby reducing the cost of the dom nodes and rendering.In addition, because the property component containing the hook is pre-rendered in advance, we want to prevent the number of hook methods from being re-registered when the component is rendered in the property panel again, for example, the following code can control the platformActionHook when aggregating hooks by injecting different parameters through mixin.

<template>
  <Component :is="prop" :item="item"></Component>
</template>
<script>
// prerender-prop.vue
export default {
  name: "DynamicProp",
  data() {
    return {
      prop: null
    }
  },
  /**
   * distProp: yesProp.vueContent string of packaged umd file
   */
  props: ['distProp', 'item', 'renderIndex'],
  watch: {
    distProp: {
      immediate: true,
      deep: true,
      handler(val) {
        // Get ComponentsUmd.js, pre-execute component objects
        const propComponent = this.preval(val)
        // Get mixin
        const mixins = propComponent.prop.mixins || []
        // Determine if a mixin contains a hook mixin
        // The change property is set in the platformAction
        const hasHook = mixins.filter(item => item.hook).length
        if (hasHook) {
          // pre-render
          this.prop = {
            ...propComponent,
              mixins: [{ beforeCreate () { 
                 this.$options.registerHook = true; 
                 this.$options.renderIndex = this.renderIndex 
                 } 
              }, 
                ...mixins
              ],
            /*
            The render function can be overridden if error information is not required on the UI
            render() { return null }
            */
          }
        }
      }
    }
  },
  methods: {
    preval(js) {
      const mode= {}
      new Function("self", `return ${js}`)(mode)
      return mode.prop
    }
  }
};
</script>

7. How platformActionHook is automatically aggregated

7.1 Platform to Provide Aggregation Capability

Register the hook store module through the top-level store on the platform.Additionally, hook functions cannot be simply saved in a queue during hook collection, and the order of rendering needs to be exactly the same.Because when deleting a component, you need to find exactly the hook function to delete the component based on the index.In addition, our editor supports dragging components to rearrange component rendering.

How do I keep the hook order consistent with the rendering order of the components?This is where renderIndex needs to be passed through to the attribute components, and our data structures need to be designed to be more flexible to meet order, delete, add, and so on.The key data structures are as follows:

// hook-store.js
import Vue from 'vue'

export default {
  state () {
    return {
      // Queue to collect all active hooks
      beforeDeletePluginHook: [],
      // Collection Platform for hook s after activity is saved
      afterSaveTopicHook: [],
      //Collection Platform Removes the hook for the current plugin
      beforeDeletePluginHook: [],
      // The hook after the collection platform deletes the current plugin
      afterDeletePluginHook: [],
      // ...other life cycle approaches
      mapIndex: {
        /* {
         *  // Rendering order
         *  [renderIndex]: {
         *    Index of beforeSaveTopicHook in queue under current rendering order
         *    hookIndex,
         *    // Whether there is an error return after calling the hook function to facilitate error backtracking
         *    err
         *  }
         }*/
        beforeDeletePluginHook: {},
        afterSaveTopicHook: {},
        beforeDeletePluginHook: {},
        afterDeletePluginHook: {},
        // ...other life cycle approaches
      }
    }
  },
  mutations: {
    register (state, { type, fn, registerHook, renderIndex }) {
      // Using nextTick, make sure unregister is executed first when the editor re-renders when adding and removing components
      Vue.nextTick(() => {
        const list = state[type]
        if (registerHook) {
          list.push(fn)
          const hookIndex = list.indexOf(fn)
          state['mapIndex'][type][renderIndex] = {
            hookIndex,
            err: false
          }
        }
      })
    },
    unregister (state, { type, fn }) {
      const list = state[type]
      const i = list.indexOf(fn)
      if (i > -1) list.splice(i, 1)

      const map = state.mapIndex[type]
      for (let renderIndex in map) {
        if (map.hasOwnProperty(renderIndex)) {
          const val = map[renderIndex]
          if (val.hookIndex === i) {
            delete map[renderIndex]
          }
        }
      }
    }
  }
}

7.2 Platform ActionHook Call Platform Capability Collection

Platforms provide aggregation capabilities in the top stores. Platform ActionHook invokes platform capabilities to precipitate key information in the stores of the platform, which is easily accessible through mapState.

// platform-action-hook.js
export default function platformActionHook(params = {}) {
  let {
    beforeSaveTopicHook,
    afterSaveTopicHook,
    beforeDeletePluginHook,
    afterDeletePluginHook,
    // ...other life cycle approaches
  } = params

  return {
    hook: true,
    beforeCreate() {
      // Prerender Incoming - dynamic-props.vue
      const renderIndex = this.$options.renderIndex
      // Collect when platform scheduling harvest, when to cancel collection, to prevent duplicate collection
      const registerHook = this.$options.registerHook
      if (isDef(beforeTopicSave)) {
        beforeSaveTopicHook = beforeSaveTopicHook.bind(this)
        // Call platform store for hook function collection
        store.commit('hook/register', {
          type: 'beforeSaveTopicHook',
          fn: beforeSaveTopicHook,
          registerHook,
          renderIndex
        })

        // Other hook methods are similar
      }
    }
  }
}

7.3 Platform Execution hook

The platform can get the aggregated data in the hook store through mapState to process the corresponding business logic.

export default {
  computed: {
    ...mapState('hook', [
    'showPropHook',
    'mapIndex',
    'beforeSaveTopicHook',
    'afterSaveTopicHook'
   ])
  },
  methods: {
    saveTopic() {
      // Execute the beforeSaveTopic series of hook s
      save()
      // Execute the afterSaveTopic series of hook s
    }
  }
}

8. Summary

With prerendering, we have the ability to complete hook collection.With the assurance of an upper level data structure, we have the flexibility to extend our error backtracking capabilities.Keep in mind in real time that the last incorrect component index The next time this component is rendered properly in the properties panel, an error backtrace is invoked to invoke the internal hook function.As shown above, you can tell the user why the last save activity was unsuccessful.

4. Micro-component data communication across sandboxes

(Fig.2)

1. Background

As illustrated above, the Editor on the left side of the platform shows the current active viewing effect rendered in an iframe sandbox, the Attribute Configuration panel on the right, and the Editor on the left not in a window environment.Our widget plug-ins are plug-in, and if the Editor panel and the Attribute panel are on the same page, it can cause some problems:

  • The CSS style change of the widget plug-in causes the CSS of the entire system page to be modified
  • Plugin Settings JumpLocation.hrefCauses the whole system to jump out
  • Editor panel and preview panel codes need to be maintained separately, inconsistencies are prone, and WYSIWYG effects are designed

2. Data management across iframe s?

As designed in the above context, we need to synchronize data between the main system and the editor. Data streams are shown below for the purpose of synchronizing data:

  • Resolve configurability of components

  • Automatically generate the active UI by synchronizing the configuration data of the active page

  • Decouple active data and UI

(Fig.3)

3. Component State Management for Sandbox Crossing

Because of the iframe sandbox isolation environment, how do I resolve component connections across sandboxes?Yes, the standard solution is postMessage.The API s are as follows:

otherWindow.postMessage(message, targetOrigin, [transfer]);

For a detailed explanation of the specific parameters, see Official Documents

Because we use Vue, we combine the watch method in Vue to monitor data changes so that the data changes of the property panel are passed to the editor's iframe environment via postMessage.

watch: {
  //Listen for changes in dependencies that need to be collected
  'itemWatch': {
    handler: (val, oldVal) => {
      //Discover changes in data postmessage to child iframe
      const win = document.querySelector('.iframe').contentWindow
      win.postMessage({ action: 'syncItemInfo', params: val })
    },
    deep: true
  }
},

Sub iframe listens for events in postMessage in Editor and handles them accordingly once data changes are received.

export default {
  methods: {
    messageListener(ev) {
      if (ev.source != window.top) {
        return
      }
      let data = ev.data
      if (data.action == 'syncItemInfo') {
        this.num = data.params.numInfo.num
      }
    },
  },
  mounted() {
    window.addEventListener('message', this.messageListener, false)
  }
}

4. Disadvantages

PosMessage enables component state management across sandboxes, but it also has some drawbacks.

  1. Be sure to wait until the B page embedded in page A is loaded before postMessage cross-domain communication.
  2. Data transmission is bidirectional, which is prone to inconsistencies. It is difficult to locate the causes. Data merging is painful.

5. Be brave in exploring Vuex's data management across iframe s

We want the overall component state management to go back to one way. Since we all use Vuex, we want to explore a cross-iframe data management scheme with vuex as the core.If the code is as follows, can the parent window expose the store object to the child iframe and get data in the child window to keep the data responsive?

// code.vue
// Run in an iframe
<template>
  <div>{{title}}</div>
</template>
<script>
export default {
   computed: {
     title() {
       // _uStore_uChild Page Gets store Object of Parent Page
       // Can reaction be guaranteed?
       return __store__.state.title
     }
   }
}
</script>

6. Regression Origin

Tests have found that the above code does not maintain the responsiveness of the data.So why?Why does iframe interrupt vuex's responsive data?At this point, we need to go back to the origin to understand the principle of Vue responsive data.As shown below,

 

(Fig. 4)

 

When a Vue component is initialized, it mainly initializes the life cycle, state, etc. In the initialization state, whether data or props, Vue transforms each property of data and props into responsive data through a series of operations such as observe and defineReactive.The defineReactive function is the core function for bidirectional data binding.

Inside the defineReactive function, a Dep object is instantiated, which serves as a bridge between data and Watcher as well as a container for collecting and storing Watchers.Then, through Object.defineProperty Overrides the get and set functions of a data field.When we access vue data data data, the get function is triggered, and Dep objects in defineReactive are referenced both inside the get function and inside the set function.

7. Practice tests truth

(Fig. 5)

Debug found that, as shown above, it is true that when a change in Vue's data triggers a set operation, dep looks for the watcher, triggers its execution, and then updates the UI.Because iframe is related to the parent window's Dep.target Getting a value of null prevents the parent dep object from collecting watchers in the child iframe, blocking the responsiveness, and the key code is as follows:

(Fig.6)

8. Shouzheng is amazing

Can we connect broken parent-child windows by collecting their dependencies?

Artifact Vue.observable To help

By using in child iframes Vue.observable Adding a wrapper to the state of the parent store allows you to retain a responsive Dep collection in the child iframe, so that the parent-child window responds.However, because Vue depends on the collection of array data in different ways, a new array object needs to be returned for array changes. This idea can encapsulate a set of vuex-style api s, so that the whole data management is in the vuex mode.

8.1 Abstract parent-store-mixin

The parent-store-mixin mounts the store of the parent window on the $pstore property of the vue object in the child iframe window, making it easy to get the store of the parent window in the vue component.

// parent-store-mixin.js
// Use mixin to construct store data associations for different instance objects
module.exports = function ParentMixin(store) {
  return function(Vue) {
    Vue.mixin({
      beforeCreate: function ParerntMixin() {
        Vue.observable(store.state)
        this.$pstore = store
      }
    })
  }
}

8.2 Packaging Tool Method

Encapsulate vuex style tool method, get this.$pstore internally

import {mapPrarentMutations, mapParentState} from 'vuex-parent-helper'

export default {
  computed:{
    ...mapParentState(['foo'])
    // ...mapParentGetters...
  },
  methods: {
    ...mapPrarentMutations(['fooChange'])
    // ...mapParentActions...
  }
}

9. Complete chestnuts

<template>
  <div class="hello">
    <p>{{$pstore.state.top.test.hh}}</p>
    <h1>{{ foo }}</h1>
    <div>test:{{ testGetter }}</div>
    <h3 @click="fooChange(Date.now())">update</h3>
    <h3 @click="fooAction(Date.now())">action</h3>
  </div>
</template>

<script>
import Vue from 'vue'
import {
  mapParentMutations,
  mapParentActions,
  mapParentGetters,
  mapParentState,
  parentStoreMixin
} from 'vuex-parent-helper'

Vue.use(parentStoreMixin(window.top._store_))

export default {
  name: 'HelloWorld',
  computed: {
    ...mapParentState('top', ['foo']),
    ...mapParentGetters('top', ['testGetter'])
  },
  methods: {
    ...mapParentMutations('top', { fooChange: 'foo' }),
    ...mapParentActions('top', { fooAction: 'fooTest' })
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

Parent page store

import Vuex from 'vuex'
import Vue from 'vue'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    top: {
      namespaced: true,
      state() {
        return {
          foo: 'foo',
          test: {
            hh: ''
          }
        }
      },
      getters: {
        testGetter(state) {
          return state.test.hh || 'default'
        }
      },
      mutations: {
        foo(state, txt) {
          state.foo = txt
        },
        test(state) {
          Vue.set(state.test, 'hh', Date.now())
        }
      },
      actions: {
        fooTest(context) {
          context.commit('test')
        }
      }
    }
  }
})

todomvc calf knife

 

(Fig. 7)

5. Thinking Outlook

Here we go back to the team's technological exploration of thinking and state management in a cross-sandbox environment between micro-components and platforms.At the same time, as a front-end engineer, I believe that our daily lives are very similar, thinking, learning, practicing, and refining our technology and vision.So what is technology?Perhaps, as stated in the Essence of Technology, [Technology is essentially a collection of phenomena that are captured and used, or, in other words, a programming of phenomena for their existing purposes].There will be a series of thematic articles to share with you, welcome to exchange and discuss.

More please pay attention to vivo Internet technology WeChat Public Number

Note: To reproduce the article, please contact Microsignal: Labs2020 first.

Tags: Front-end Vue Attribute JSON npm

Posted on Wed, 20 May 2020 22:31:44 -0400 by Labbat