Wukong Activity Center-Micro Component State Management (Part 1)

This article was first published on the Vivo Internet Technology WeChat public account 
link:https://mp.weixin.qq.com/s/Ka1pjJKuFwuVL8B-t7CwuA
Author: Wukong R & D team in Taiwan

1. Background

Through the technical demystification of "Demystifying vivo how to build a ten-million-level DAU event center-set sail" , I believe we have more understanding of RSC. RSC (remote service component) is a remote service component. Through hot plugging mechanism, visual configuration, plug and play, quickly build active page, is the core component of active page.

RSC is an efficient abstract design scheme for active page constituent units, which maximizes development efficiency and reduces the mental burden of developers. We hope that developers follow the design concept of [high cohesion, weak coupling] in development, and only need to care about the display and logic processing inside the RSC component.

Wukong Activity Center-Micro Component State Management (Part 1)

(figure 1)

But in our actual business development, as shown in Figure 1 above, we have to face a large number of similar scenarios every day. The user obtains the points of the game by participating in the [Monopoly game], and then [Monopoly component] requires the game result Points will be notified to [Card Assembly Component], and then [Card Assembly Component] to obtain the corresponding card, click [Flip Card] to notify [Monopoly Component] to update the remaining number of games. This activity page scenario involves collaboration and data sharing between a large number of components. So if you view the activity as a small front-end system, RSC is just a basic element of the system, and another very important element that cannot be ignored is the connection between RSC components. Of course, this connection is also related to the context of the scene. Therefore, in the process of managing RSC components, the first thing to be solved is the management of the data state between the components in the active page.

Second, the results

Through continuous in-depth thinking about the problem and exploring the essential principles behind the phenomenon, the connection of components in different contexts (state management) is well solved from the architectural design level. E.g:

  • In the activity page, we solved the connection between RSC components and components.

  • Within the platform, we solved the connection between the RSC component and the platform. Business RSC components need to be aware of the key actions of the platform, such as saving activities, deleting components in the editor, etc.

  • In the security sandbox in the editor, we solved the connection between the component and the configuration panel across the sandbox.

3. Architecture evolution

Today we will focus on chatting, in the activity page, the connection between RSC components and components. In the next article, we will talk about the connection between the platform and the RSC components in the sandbox environment.

Because we use Vue as the ui basic framework of our front end, the following technical solutions are all based on Vue.

Four, EventBus event bus

Wukong Activity Center-Micro Component State Management (Part 1)

(figure 2)

A picture is worth a thousand words, as shown in Figure 2. Of course, the simplest solution we have in mind is to record the subscribers in the component by implementing a centralized event processing center, and notify the subscribers in the relevant components through custom events when collaboration is needed. Of course, the notification can carry payload parameter information to achieve the purpose of data sharing. In fact, Vue itself also comes with a custom event system. Custom events between Vue components are implemented based on this. For detailed APIs, please participate in the Vue documentation. We can implement the EventBus mechanism based on Vue itself, without introducing new dependencies and reducing the volume of the bundle. The API uses the following code.

const vm = new Vue()
// 注册订阅者
vm.$on('event-name', (payload) => {/*执行业务逻辑*/})
// 注册订阅者,执行一次后自动取消订阅者
vm.$once('some-event-name', (payload) => {/*执行业务逻辑*/})
// 取消某事件的某个订阅者
vm.$off('event-name',[callback])
// 通知各个订阅者执行相对应的业务逻辑
vm.$emit('event-name',payload)

1. Structural advantages

In practice, the advantages of the data state management model based on EventBus are found:

  • The implementation of the code is relatively simple, and the design scheme is easy to understand
  • The decoupling between components can be accomplished in a very lightweight way, turning the strong coupling between components into a weak coupling to EventBus.

2. Pain points in practice

Of course, the EventBus solution will have some shortcomings:

  • Because business logic is scattered among multiple component subscribers, the processing of business logic becomes fragmented and lacks a coherent context.
  • When reading and maintaining code, it is necessary to constantly find subscribers in the code, resulting in interruption of business process understanding and distraction of attention.

3. Reflection and improvement

Recognizing the shortcomings of EventBus's architectural design, we will also eat our own dog food and implement a set of visual mechanisms. Through the analysis of the abstract syntax tree of the code, we can extract the information of subscribers and senders and display them visually. The relationship between them helps us quickly understand the problem.

In addition, for complex business logic, the [prescript] improvement scheme is designed. For example, although the active page is composed of multiple RSC components, the requested server interface is still one, which contains all the data of the initial state of the page. At this time, we can uniformly handle the logic of obtaining data in the pre-script, and then Re-synchronized to each RSC component. [Front script] is to extract a global object, including shared state and business logic. Multiple components depend on this global object. The architecture design is shown in Figure 3, which is a supplement to the EventBus solution.

Wukong Activity Center-Micro Component State Management (Part 1)

(image 3)

4. Summary

Through the pre-script, it can solve the problem that complex business is difficult to maintain and understand, but it also brings some risk points such as the need to expose global objects, and there is a risk of being overwritten or modified. After the improvement of the pre-script, we more and more clearly feel what the state management mode we need is, that is Vuex. Then let's talk about Vuex.

V. Vuex state management

1. Background

What is Vuex?

Vuex is a state management model developed specifically for Vue.js applications. It uses centralized storage to manage the state of all components of the application, and uses corresponding rules to ensure that the state changes in a predictable way. Vuex is also integrated into Vue's official debugging tool, the devtools extension, which provides advanced debugging functions such as zero-configuration time-travel debugging, state snapshot import and export.

What are the characteristics of Vuex?

  1. Centralized component state management, support dynamic registration store
  2. It has a high degree of matching with Vue, and the bottom layer is implemented based on Vue's responsive data characteristics, maintaining the same data processing characteristics as Vue.
  3. You can quickly get started with Vuex after you are familiar with Vue, and the learning cost is relatively low
  4. Complete development experience, official devtools and time-travel debugging, etc., help developers sort out the predictable changes in data

2. Introduce support for Vuex on the platform

Vuex is a general state management framework, how can it be seamlessly integrated into our RSC component system? We need to introduce the dependency and support for Vuex in the project, and add the dependency on the store in the top-level Vue.

The basic structure of our project:

.
└── src
    ├── App.vue
    ├── app.less
    ├── assets
    ├── component
    ├── directive
    ├── main.js
    ├── stat
    ├── store
    └── utils
├── babel.config.js
├── package.json
├── public

2.1 Add dependencies

According to the specification, first add the dependency on Vuex in the package.json in our project directory

{
  "dependencies": {
    "vuex": "^3.0.1"
  }
}

2.2 Create store object

//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export const store = new Vuex.Store({
  // 状态数据
  state() {
    return {}
  },
  getters: {},
  mutations: {},
  actions: {},
})

2.3 Top-level Vue object injection into the store

Inject the above created store object into the top-level Vue object, so that all Vue components will obtain the top-level store object through this. $ Store. In addition, Vuex also provides easy-to-use tool methods (mapState, mapActions, mapGetters, mapMutations) for data sharing and collaboration.

// App.vue
import { store } from './store'

new Vue({
  // 注入 store
  // 在所有的改 Vue 管理的 vue 对象中都可以通过 this.$store 来获取
  store,
})

3. Use Vuex to develop RSC components

3.1 RSC own store

We still hope that when developing components, developers only focus on their presentation and business logic most of the time, and only share their own state in the top-level store when the component is rendered in the active page. Therefore, the component has its own independent store state management. The state of the module is isolated through the namespace, and then in the beforeCreate lifecycle method of the component, the dynamic store registration is performed through Vuex's registerModule.

3.2 StoreMixin injection

You can simplify this process by extracting the public StoreMixin, and you can also automatically turn on namespaced: true and expand shortcut methods and properties for the current namespace. code show as below:

// store-mixn.js
export default function StoreMixin(ns, store) {
  return beforeCreate() {
    // 保证 namespace 唯一性
    // 开发者可以通过函数生成唯一的namespace
    // 框架可以生成唯一的namespace
    const namespace = isFn(ns) ? ns(this) : gen(ns)
    this.$ns = namespace
    store.namespaced = true
    this.$store.registerModule(namespace, store)

    // 扩展快捷方法和属性
    this.$state = this.$store.state[namespace]
    this.$dispatch = (action, payload) =>
      this.$store.dispatch(`${namespace}/${action}`, payload)
    this.$getter = //...
    this.$commit = //...
  }
}
//store.js
// 当前组件自有store
export default {
  // 组件自身的状态
  state() {
    return {}
  },
  mutations: {},
  getters: {},
  //...other things
}

// code.vue
// 组件对外的入口模块
import store from './store'
export default {
  mixins: [StoreMixin(/*namespace*/ 'hello', /* 组件的 store */ store)],
}

3.3 How to resolve namespace conflicts?

Because an RSC component will be repeatedly loaded multiple times in an activity, all will also cause the store module of the same namespace to be repeatedly loaded, resulting in module coverage. How to ensure the uniqueness of namespace? We can determine whether there is the same namespace when registering the namespace in StoreMixin, and if so, rename the namespace once. For example, when hello has been registered as the store of the command space, registering namspace hello again will automatically become hello1, and the distinction will be made automatically. The simple algorithm is implemented as follows,

// gen.js
// 生成唯一的 namespace
const g = window || global
g.__namespaceCache__ = g.__namespaceCache__ || {}

/**
 * 生成唯一的 moduleName, 同名 name 默认自动增长
 * @param {*} name
 */
export default function genUniqueNamespace(name) {
  let cache = g.__namespaceCache__
  if (cache[name]) {
    cache[name].count += 1
  } else {
    cache[name] = {
      count: 0,
    }
  }
  return name + (cache[name].count === 0 ? '' : cache[name].count)
}

In addition, developers can pass a custom function in store-mixin to generate a unique namespace ID. For example, the following code sets the namespace according to the dynamic routing parameters in vue-router

export default {
  mixins: [StoreMixin((vm) => vm.$router.params.spuId), store],
}

3.4 Challenges of dynamic namespaces

Because the dynamic namespace will bring the problem of uncertainty, the following code example, if hello is renamed to hello1, and in mapue (mapState, mapMutations, etc.) method in Vuex, you need to pass the namespace accurately to get the context of the store in the component .

// code.vue
export default {
  mixins: [StoreMixin('hello', store)],
  computed: {
    ...mapGetters('hello', [
      /* hello namespace store getter */
    ]),
    ...mapState('hello', [
      /* hello namespace state property */
    ]),
  },
  methods: {
    ...mapActions('hello', [
      /* hello namespace actions method */
    ]),
    ...mapMutations('hello', [
      /* hello namespace mutations method */
    ]),
  },
}

3.5 Expand Vuex to support dynamic namespace

How to solve the problem of dynamic namespace in Vuex mapXXX method? First of all, we thought of setting the namespace on the this. $ Ns object of Vue in StoreMixin, so that the components mixed by StoreMixin can dynamically obtain the namespace.

// store-mixn.js
export default function StoreMixin(ns, store) {
  return beforeCreate() {
    // 保证 namespace 唯一性
    const namespace = gen(ns)
    // 将重命名后的 namespace 挂载到当前 vue 对象的$ns 属性上
    this.$ns = namespace
    //...
  }
}

Although we can use this. $ Ns inside the component to get the namespace of the store in the component, imagine that we can:

// code.vue
export default {
  computed: {
    ...mapGetter(this.$ns, [
      /* hello namespace store getter */
    ]),
    ...mapState(this.$ns, [
      /* hello namespace state property */
    ]),
  },
  methods: {
    ...mapActions(this.$ns, [
      /* hello namespace actions method */
    ]),
    ...mapMutations(this.$ns, [
      /* hello namespace mutations method */
    ]),
  },
}

Unfortunately, at this moment this is simply not the current instance of Vue, this. $ Ns is gorgeous and undefined. then what should we do? JS has many features of functional programming. Functions are also values ​​and can be passed as parameters. In fact, in addition to the value characteristics of functions, a very important feature is lazy computed. Based on this thinking, the mapXX method is extended to support dynamic namespace. Then in the mapXXX method, wait until vm is the component instance of the current Vue, and then get the namespace of the current component.

// code.vue
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex-helper-ext'

export default {
  computed: {
    ...mapGetters((vm) => vm.$ns, [
      /* hello namespace store getter */
    ]),
    ...mapState((vm) => vm.$ns, [
      /* hello namespace state property */
    ]),
  },
  methods: {
    ...mapActions((vm) => vm.$ns, [
      /* hello namespace actions method */
    ]),
    ...mapMutations((vm) => vm.$ns, [
      /* hello namespace mutations method */
    ]),
  },
}

3.6 How parent-child components pass dynamic namespaces

I believe you must have discovered one of these problems. This. $ Ns can only be obtained from the StoreMixin component. What about the subcomponents of this component? How to solve the child component gets the parent component's namespace? At this time, we need to use Vue's powerful mixin system to design a global mixin, determine whether the parent component has a $ ns object when the component is created, and if it exists, set the current component's $ ns to be the same as the parent component. Skip if not.

function injectNamespace(Vue) {
  Vue.mixin({
    beforeCreate: function _injectNamespace() {
      const popts = this.$options.parent;
      if (popts && popts.$ns) {
        this.$ns = popts.$ns;
        const namespace = this.$ns;

        // 为组件扩展快捷方法和属性
        this.$state = this.$store.state[namespace]
        this.$dispatch = (action, payload) =>
                            this.$store.dispatch(`${namespace}/${action}`, payload)
        this.$getter = //...
        this.$commit = //...
      }
    }
  });
}
// main.js
Vue.use(injectNamespace);

In this way, the child component will get the namespace set by the parent component by default. With the magic of this mixin, we can expand the design of the mapXXX method more elegantly, because the $ ns attribute can be used as the default namespace in the mapXX method. Be more refreshing and keep the style consistent with the official, so that Vuex can be better integrated into our system.

// code.vue
export default {
  computed: {
    ...mapGetter([
      /* hello namespace store getter */
    ]),
    ...mapState([
      /* hello namespace state property */
    ]),
  },
  methods: {
    ...mapActions([
      /* hello namespace actions method */
    ]),
    ...mapMutations([
      /* hello namespace mutations method */
    ]),
  },
}

3.7 The last complete little chestnut

Through the following small chestnuts, we can see that for developers, as long as they develop according to the standard Vuex development method, it seems that nothing has happened ^ _ ^. In fact, we have made a lot of efforts internally. The purpose of architectural design is to [make simple things simpler and make complex things possible].

store.js RSC component own store

export default {
  state() {
    return { mott: 'hello vue' }
  },
  mutations: {
    changeMott(state) {
      state.mott = 'hello vuex'
    },
  },
}

text.vue text subcomponent, mapState automatically obtains the namespace dynamically

<template>
  <div @click="changeMott">{{ mott }}</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex-helper-ext'
export default {
  computed: {
    ...mapState(['mott']),
  },
  methods: {
    ...mapMutations(['changeMott']),
  },
}
</script>

code.vue

<tempalte>
  <text></text>
</template>
<script>
import store from './store';
import text from './text';

export default {
  mixins: [StoreMixin('hello', store)],
  components: {
    text
  },
  methods: {
    // ....
  }
}
</script>

Sixth, thinking and outlook

This article is written here, and it is coming to an end. Thanks for being with me. We reviewed the RSC componentization plan together, and the road we took in solving the actual business scenario of the middle stage of Wukong activities. The team technically tried to solve the state management between RSC components and components. In the next article, we will talk about the state management on the connection between the RSC component and the platform, and the cross-sandbox environment. Welcome to exchange and discuss together.

Guess you like

Origin blog.51cto.com/14291117/2488640