Author: Tujia Liang Yahui

When I just joined the company, the technical Leader told me that he hoped to implement front-end automation and that I could come up with a feasible solution. At the time, the front end team was very young, but the growth of the business resulted in the team more than doubling in size. Coupled with the ever-changing toC business, all sorts of bugs fly around. The cost of front-end automated testing is very high, and it is almost impossible to implement script development for automated testing in the case of long-term overtime demands. How to find a low-cost and feasible automated testing technology solution is my original intention to make this mini Version of Redux. It’s called mini. On the one hand, the technical implementation of a complete set of small program version of REdux cannot be realized due to the lack of their own ability. On the other hand, the principle of gradual iteration is adopted to gradually optimize according to the subsequent use of basic needs. Or maybe Dan will have one of his own soon.

1. The pain of Internet toC application research and development

Lack of people, lack of people, we lack a quality front end, which is probably what most technology managers want. In the face of the system of all over the sky bugs, cockroaches like endless low-level errors, whether it is always so powerless. Although we have many testing tools and libraries for automated testing, we still wonder why front-end automated testing is so difficult to implement.

During National Day, I developed a mini version of Redux to solve this problem, and the solution was feasible after the internal discussion of the team this week, and I began to prepare to apply it to the project for verification. Therefore, I would like to share my thoughts with you

2. Current confusion in the implementation of front-end application automation:

  • The front-end Javacript language is a generic scripting language, and many errors are found only at runtime. As a result, the quality of the front-end code is difficult to guarantee. As a result, some teams will go for TS, and TS has greatly improved the situation.

  • The modern front-end is not very good at layering code, especially in Vue projects. Exports are all UI interaction logic and business logic coupled together. Also leads to the front end of a current dilemma, serious coupling. Coupling means that each place has a particularly large sphere of influence. It often results in the same business being changed in one place and affected in another where it shouldn’t be. Angular already provides its own standards and rules for module division, React has its own Flux architecture approach to guide people, and Vue’s absence in this area makes it confusing in increasingly complex systems. Uvu also said that Vuex is not suitable for large applications. This is reflected in some ways.)

  • The inability of most young front ends to design business models has also led to a state of confusion in the current project code. Although many good front-end engineers can implement MVVM, VDOM, bidirectional binding, single data flow, and even their own can write a set of implementation for you on the spot. In practice, however, there is a lack of understanding of business data modeling. Chaos often occurs. This is one of the biggest puzzles many teams encounter when implementing React-Redux

  • Once the UI logic and business logic are coupled, testing can only be done through the virtual page DOM, but the development costs of this automated testing approach can be significant for a UI whose toC business changes face to face every two or three months. But the changes to the business logic are minimal.

Therefore, in view of the above problems, we should find a way to separate the business layer from the view layer and test the business layer alone, which can greatly reduce the cost of toC application automation test development and greatly improve the accuracy of the business in the project. As for the view layer, since most mVVV-based front-end applications are data-driven systems, the robustness of the system can be greatly guaranteed as long as the business data model is correct.

3. How to decouple view layer from business logic layer

In fact, module split has always been a difficult problem in the field of software development, how to decouple each module, although we have various methodologies to guide us to implement, but the methodology is only a theory after all. There are a lot of factors that come into play when it comes to actually doing it, and not every team has the talent to do it.

In the front end field, MVC is too complex and too heavy for the front end, and most people are lack of design ability in the long-term focus on the presentation of views. In this respect, MVVM provides a direction for the front end:

Flux: the original

Flux eschews MVC in favor of a unidirectional data flow. When a user interacts with a React view, the view propagates an action through a central dispatcher, to the various stores that hold the application’s data and business logic, which updates all of the views that are affected. This works especially well with React’s declarative programming style, which allows the store to send updates without specifying how to transition views between states.

The translation:

Flux avoids MVC and adopts one-way data flow. When the user interacts with the React view, the view passes an action object to each store of data and business logic through the Dispatcher method. Data changes in these stores affect all views and cause them to be updated. This is related to the React programming style, which allows views to be changed by data changes without specifying how to switch views by state.

throughFluxWe can clearly realize that in the view, in response to various user behaviors, the dispatcher (dispatch) to take new business data into action (action) for the carrier into the processing and storage of business data model (store). The diagram below:

Among them, Redux derived from the architecture methodology based on Flux is widely known, and Vuex is also widely recognized with the popularity of Vue.

But Redux and Vuex play the same role in development, but are fundamentally different, which is why Vuex is not suitable for large front-end applications:

  • Redux is framework independent, and Vuex relies on the responsive properties of Vue

  • Redux is pure native JS and view-neutral, which means it makes it easy to peel off business into Redux. This makes it easy to reuse into any front-end technology. Vuex has a hard time doing that.

  • Redux emphasizes that Reduce must be a pure function, which means that the same parameters will lead to the same result, that is, the result is predictable and has very good testability, which also meets our demand for automated testing of business. Vuex relies on mutations for parameter references, and Actions support asynchrony, resulting in the uncertainty of the return value.

For all these reasons, a small application version of Redux is what we need.

4. How to build a small program versionRedux

I’m sure most of you have read the Redux source code, and OF course I’ve written an article about the Redux source code. I’m sure the principles are well understood, but the difficulty with how to implement a small program version of Redux is how to implement something like React-Redux that combines Redux into a small program.

We are faced with the following technical problems:

  • Where does store exist?

  • How do I expose interfaces?

  • How can the data in store be responsive to the data in Page?

In fact, it is to make a publish and subscribe model implementation, but we need to ensure that our store internal data can not be arbitrarily modified, so as to ensure the stability of our business.

When you think of data sharing within applets, globalData comes to mind. GlobalData, however, relies on global app objects, and the effects of global variables are well known, not necessarily by a novice. This makes state changes untraceable.

So how do you avoid using global variables and still solve the data storage problem? The answer is —- sandbox mode

Sandbox mode, JS is a very common design patterns, it is through the principle of closure to keep the data within a function role in China, and by the return value function reference this function within the package variable in the body, forming a closure, and only through this function returns the function can access and modify data in the closure, thus up to the data protection role.


     
  1. Function initMpState () {// mp-redux initializes the function, forming a separate scope here

  2. const reducers = {}; // Data in the scope of this function

  3. const finalState = {}; // Data in the scope of this function

  4. const listeners = []; // Data in the scope of this function

  5. let injectMethod = null; // Data in the scope of this function

  6. Function getStore() {// Interface for accessing data in sandbox

  7.    return finalState;

  8.  }

  9. Function createStore(modules, injectFunc) {// Interface for initializing data in the sandbox

  10. .

  11.  }

  12. Function dispatch(action) {// Interface for manipulating data in sandbox

  13. .

  14.  }

  15. Function connect(mapStoreToState, Component) {// High-level API for associating applets with Page objects

  16. .

  17.  }

  18.  return {

  19.    createStore,

  20.    dispatch,

  21.    connect,

  22.    getStore

  23.  }

  24. }

  25. module.exports = initMpState();

Copy the code

With sandbox mode, we protect our data well and provide limited means of operation to safely and reliably store the data in our business data model.

5. How to initialize our appletstore?

Since you want to expose the interface, you want to keep the closure inside the function. It’s so complicated. But CommonJS is a great help here:

When we require a module, CommonJS keeps the module in a separate scope. And it’s always been there. A typical application scenario is Nodejs

So with commonjs, we use module.exports to expose the set of apis returned by our mp-redux initialization function to the caller:

                                                                    
     
  1. // mp-redux/index.js

  2. function initMpState () {

  3. .

  4.   }

  5. module .exports = initMpState ();

Copy the code

This allows us to do things from anywhere without fear (using apis to manipulate store data).

6. Initialize the business model

The data in the Store is based on the business, and how to preserve the business model will be our focus. These business models, in turn, involve a lot of business logic data processing. At the same time, we need to ensure that the business is testable.

Therefore, Redux’s Reduce approach is the perfect choice for our needs, so each model must be a pure function that returns a pure object, the business data model, after each operation.

                                                                                
     
  1. / *

  2. Modules, refer to Redux, we can split many business modules, each business module has its own business model, so modules is an object, key is the name of the business module and value is a pure function that deals with the business model.

  3. An injectFunc is provided mainly because the applet is initialized after the system loads,

  4. So we need to hijack the specific API in order to synchronize the store data to the currently displayed page in this API. Why not write onShow as a small program? Mainly after considering Baidu small program, alipay small program. It's more flexible.

  5. * /

  6. function createStore (modules , injectFunc ) {

  7. if (injectFunc && typeof injectFunc === 'string') {

  8. injectMethod = injectFunc ;

  9.     }

  10. // We saved our user-defined business models into the reducers in the sandbox

  11. if (modules && typeof modules === 'object') {

  12. const keys = Object. keys( modules);

  13. const len = keys .length ;

  14. for (let i = 0; i < len; i++) {

  15. const key = keys [i ];

  16. if (modules .hasOwnProperty (key ) && typeof modules [key ] === 'function') {

  17. reducers [key ] = modules [key ];

  18.         }

  19.       }

  20.     }

  21. // Initialize the store

  22. dispatch ({type : '@MPSTATE/INIT'});

  23.   }

Copy the code

7. How to relatestoreData into the applet page, and responsive processing?

The applets automatically subscribe to the data object in the Page parameter, so we simply provide a wrapper function that reflects the data model in the store we want to subscribe to into the parameters needed to build the Page function of the applets. The Dispatch method is injected, along with the data mapping function mapStoreToState.

Because each page subscribes only to the business data states it cares about, we can’t just throw the entire store at them. So we need mapStoreToState to inject only the business data state that the user needs into the page.


     
  1. / *

  2. *mapStoreToState, which allows users to subscribe to their own pages with the state of business data they care about

  3. * /

  4.  function connect(mapStoreToState, component) {

  5. if (! component || typeof component ! == 'object') {

  6. throw new Error('mpState[connect]: Component must be a Object! ');

  7.    }

  8. if (! mapStoreToState || typeof mapStoreToState ! == 'function') {

  9. throw new Error('mpState[connect]: mapStoreToState must be a Function! ');

  10.    }

  11. // We need to inject redux-related functions and states into the user's page definition

  12. const newComponent = { ... component };

  13. // Get the data defined by the user on the page, we need to keep the original state

  14.    const data = component.data || {};

  15. // Get the state of the store to which the user subscribed

  16.    const extraData = mapStoreToState(finalState);

  17. if (! extraData || typeof extraData ! == 'object') {

  18. throw new Error('mpState[connect]: mapStoreToState must return a Object! ');

  19.    }

  20. // Merge the state in the user's own page with the state in the store injected through Connect. My implementation here is a bit poor

  21.    let newData = null;

  22.    if (typeof data === 'function') {

  23.      newData = {

  24. . data(),

  25. . extraData

  26.      }

  27.    } else {

  28.      newData = {

  29. . data,

  30. . extraData

  31.      }

  32.    }

  33. // Inject into the Page object

  34.    if (newData) {

  35.      newComponent.data = newData;

  36.    }

  37. // Get the life cycle hooks that need to be hijacked. Since every page does not have to be hijacked the same life cycle, there is a method that can be customized for each page to modify the hijacked hook

  38.    const injectFunc = component.getInjectMethod;

  39.    const methods = component.methods || {};

  40.    const newLiftMethod = injectFunc && injectFunc() || injectMethod;

  41.    const oldLiftMethod = component[newLiftMethod];

  42. // Inject the Dispatch API

  43.    methods.dispatch = dispatch;

  44.    newComponent.methods = methods;

  45.    newComponent.dispatch = dispatch;

  46.    newComponent.mapStoreToState = mapStoreToState;

  47. // Lifecycle hook hijacking

  48.    if (newLiftMethod) {

  49.      newComponent[newLiftMethod] = function() {

  50.        if (this) {

  51. // Synchronize store data to page in hijacked hook

  52.          this.dispatch({});

  53.          oldLiftMethod && oldLiftMethod.call(this, arguments);

  54.        }

  55.      }

  56.    }

  57. // Returns a new Page object

  58.    return newComponent;

  59.  }

Copy the code

     
  1. // Connect is used to inject the subscribed state, and MP-redux automatically injects the Dispatch method into the page object

  2. const mpState = require('./.. /.. /mp-redux/index.js');

  3. const util = require('.. /.. /utils/util.js');

  4. const logActions = require('./.. /.. /action/logs.js');

  5.  Page(mpState.connect((state) => {

  6.    return {

  7.      userInfo: state.userInfo.userInfo,

  8.      logs: state.logs.logs

  9.    }

  10.  },

  11. {// All business data is stored in the store, so pages with only business data do not need the data attribute.

  12.    clearLogs() {

  13. This.dispatch ({// Issue an action via the dispatch method to update data in the store

  14.        type: logActions.clearLogs

  15.      })

  16.    }

  17.  }))

Copy the code

8. How do I distribute updatesstoreAnd reflect the data in the applet’s page?

Since the state update of the applet is done through the setData API, we need to synchronize the state of the data in the Store through the API at Dispatch

                                                                                                
     
  1. / *

  2. * It is important to note that action is a native JS object, not a function, and Redux asynchrony is implemented via redux-thunk, but my appeal was to make the business logic in our application easier to test, so I didn't provide support, which is actually quite simple to implement. Can refer to [vue - with - I do redux source] (https://github.com/ryouaki/vue-with-redux/blob/master/src/index.js)

  3. * /

  4. function dispatch (action ) {

  5. // debugger

  6. const keys = Object. keys( reducers);

  7. const len = keys .length ;

  8. // This loop is used to iterate over the model to recalculate the new store

  9. for (let i = 0; i < len; i++) {

  10. const key = keys [i ];

  11. const currentReduce = reducers [key ];

  12. const currentState = finalState [key ];

  13. const newState = currentReduce (currentState , action );

  14. finalState [key ] = newState ;

  15.     }

  16. if (this ) {

  17. // The new data model is injected into the page via setData based on the subscription rules within the component

  18. const componentState = this. mapStoreToState( finalState) || {};

  19. React and Vue support is provided, so there are a few extra lines of code, which are still being tested.

  20. If (this.setData) {// applet

  21. this. setData({ ... componentState })

  22. } else if (this.setState) {// react

  23. this. setState({ ... componentState })

  24. } else { // VUE

  25. const propKeys = Object. keys( componentState);

  26. for ( let i = 0; i < propKeys. length; i++) {

  27. this[ propKeys[ i]] = componentState[ propKeys[ i]];

  28.         }

  29.       }

  30.     }

  31.   }

Copy the code

In fact, with the above code we have basically done a simple publish subscription.

9. actionandmodel(I thinkmodelthanreduceEasier to understand, so I callmodel, ha ha)

But there’s nothing to say here. Same as Redux, right

                                                                                                        
     
  1. const actions = require( './.. /action/logs.js');

  2. const initState = {

  3. logs : []

  4. }

  5. module .exports = function (state = initState , action = {}) {

  6. const newState = { ... state };

  7. switch (action .type ) {

  8. case actions .addLogs :

  9. const now = new Date();

  10. newState .logs .push ({

  11. time : now .getHours () + ":" + now .getMinutes () + ":" + now .getSeconds (),

  12. value : action .data

  13. });

  14. return newState ;

  15. case actions .clearLogs :

  16. newState .logs = [];

  17. return newState ;

  18. default:

  19. return newState ;

  20.   }

  21. }

Copy the code

Examples and source code

Examples and source code

Currently the code upload of the log page failed and was lost. Make it up next week. I can’t open applets on Ubuntu at home.

The last

With this MP-Redux, the business logic, data and view are separated, and the business logic and data are stored in pure JS code. Convenient multi-platform transplantation, but to do is to do a platform data responsive adaptation.

The greater benefit is that the pain point of coupling the view to the business layer is addressed and the data business is stripped into pure functions, greatly improving the testability of the business code.

By providing an independent way to test business data, it also reduces the overall cost of testing.

In addition

At present, the front end team is very young. We have many demands that cannot be met by the existing library, so we have many opportunities for technological innovation. Let me know if you want to mess around.

In addition, I am engaged in front-end micro service practice, and has been successful, interested must contact me.

Moreover, our technical requirement is not high, I don’t care what the Vue how deep source principle research, algorithm and he didn’t care how cattle, JS with slip, I’m looking forward to those who love technology, like to study technology, like digging through the pain points in the group’s business development technology innovations, improve the productivity of the team as a whole person to join our (this is my point of view, Doesn’t mean the boss approves of it. ).