The problem

Two days ago, the business side threw me a piece of code, which omitted the complicated logic and simplified the code as follows:

// Code example 1
import { Prompt, Link } from 'react-router-dom';

export const App = () = > {
  return (
    <>
      <Prompt message="Jump to another same micro application route" />
      <Link to="/detail">Jump to the detail</Link>
    </>)}Copy the code

When used in combination with icestark, a jump to other routes of the same micro-application will cause an exception: Prompt popups twice.

Faced with this mistake, I became deeply reflective. Next, I try to unravel the mystery of this mistake, and in the process, it involves:

  • The React Router implementation principle
  • <Prompt />The underlying implementation of
  • And the dilemma faced by the micro-front-end framework after hijacking the route

How does the React Router DOM implement single-page application routing

Let’s take BrowserHistory as an example:

// Code example 2
import { BrowserRouter, Route } from 'react-router-dom';

ReactDOM.render(
  <BrowserRouter>
    <Route exact path="/">
      <Home />
    </Route>
  </BrowserRouter>
)
Copy the code

The code above initializes an instance of BrowserHistory and triggers BrowserHistory’s Listen method. This method does two things:

  1. Listen for global POPState events
  2. Subscription History changes

In this way, the corresponding page component is rendered dynamically each time the route changes (or popState events are triggered) through history.push or browser forward and back. The general process is as follows:

The route-jacking logic of the micro front end

The micro front-end framework (with its runtime capabilities) is similar to the React Router DOM, essentially hijacking the pushState and replaceState methods of Window.history, and listening for popState and hashChange events. And dynamically render the successful micro-application according to the current URL. Taking icestark as an example, the simplified logic is as follows:

// Code example 3
const originPush = window.history.pushState;
const originReplace = window.history.replaceState;

const urlChange = () = > {
	// Match the corresponding micro application according to the URL
}

// Hijack the pushState method of history
const hajackHistory = () = > {
	window.history.pushState = (. rest) = > {
  	originPush.apply(window.history, [...rest]);
  	urlChange();
  }
  
  window.addEventListener('popstate', urlChange, false);
}
Copy the code

But that won’t solve the whole problem

Microapplications have independent routing when the framework application and microapplication do not share the same history instance. When the framework uses switched routes, or other micro-applications switch routes, how can micro-applications sense the route changes?

For example, when switching different routes to the same micro-app through the history.push of the framework app, the micro-app will not render the correct page.

Of course, problems always have solutions. According to our analysis of the React Router DOM, microapplications match pages in two ways.

  1. Push method through the history instance of the microapplication
  2. The PopState event is triggered

For method one, if the page frame application intrudes into the micro-application, it is not reasonable, the main application and micro-application should be as independent as possible, rather than coupling. Thus, ICestark solves this problem by hijacking all listeners for popState events and actively firing all listeners for PopState after the route changes.

// Code example 4
const popstateCapturedListeners = [];

const hijackEventListener = () = > {
  window.addEventListener = (eventName, fn, ... rest) = > {
  	if (typeof fn === 'function' && eventName === 'popstate') {
    	// Hijack the popState listenerpopstateCapturedListeners.push(fn); }}};// Execute the captured PopState event listener
const callCapturedEventListeners = () = > {
  if (popstateCapturedListeners.length) {
    popstateCapturedListeners.forEach(listener= > {
      listener.call(this, historyEvent)
    })
  }
};

reroute()
	// Trigger the listener after matching the corresponding microapplication
	.then(() = > {
		callCapturedEventListeners();
});
Copy the code

Side effects

There is an additional side effect that needs to be noted. When history.push is executed inside the microapplication, the popState listener mounted by the microapplication is repeated once.

For now, this is an expected behavior.

Further examine the implementation of Prompt

Sensing something, let’s dive into the implementation of a Prompt to see what causes it to trigger twice. The React Router DOM Prompt code can be found here:

// Code example 5
function Prompt({ message, when = true }) {
  return( <RouterContext.Consumer> {context => { invariant(context, "You should not use <Prompt> outside a <Router>"); if (! when || context.staticContext) return null; const method = context.history.block; return ( <Lifecycle onMount={self => { self.release = method(message); }} onUpdate={(self, prevProps) => { if (prevProps.message ! == message) { self.release(); self.release = method(message); } }} onUnmount={self => { self.release(); }} message={message} /> ); }} </RouterContext.Consumer> ); }Copy the code

The code is relatively simple. When the Prompt component loads, the history.block method is called; During the unloading, some recycling was done. Further into the implementation of history.block:

// Code example 5
function block(prompt = false) {
  const unblock = transitionManager.setPrompt(prompt);

  if(! isBlocked) { checkDOMListeners(1);
    isBlocked = true;
  }

  return () = > {
    if (isBlocked) {
      isBlocked = false;
      checkDOMListeners(-1);
    }

    return unblock();
  };
}
Copy the code

History. The block call here the transitionManager. SetPrompt global method. What is the logic behind this?

// Code example 6
function createTransitionManager() {
  let prompt = null;

  function setPrompt(nextPrompt) {
    warning(prompt == null.'A history supports only one prompt at a time');

    prompt = nextPrompt;

    return () = > {
      if (prompt === nextPrompt) prompt = null;
    };
  }

  function confirmTransitionTo(location, action, getUserConfirmation, callback) {
    if(prompt ! =null) {
      const result =
        typeof prompt === 'function' ? prompt(location, action) : prompt;

      if (typeof result === 'string') {
        if (typeof getUserConfirmation === 'function') {
          getUserConfirmation(result, callback);
        } else {
          warning(
            false.'A history needs a getUserConfirmation function in order to use a prompt message'
          );

          callback(true); }}else {
        // Return false from a transition hook to cancel the transition.callback(result ! = =false); }}else {
      callback(true); }}...return {
    setPrompt,
    confirmTransitionTo,
  };
}
Copy the code

The setPrompt method simply saves a prompt when history.push is called or in response to a popState change, Will call createTransitionManager. ConfirmTransitionTo judge whether there is a Prompt currently. The processing logic is as follows:

Based on the above analysis, the Prompt component relies entirely on the contents of the Prompt to determine whether to display the Confirm pop-up. From the analysis in the previous section, since ICestark repeated the execution logic of the route, is the culprit “it”? Sure enough, when icestark remove callCapruteEventListeners (see the code example 4) code, Prompt bounced back to normal.

How to solve

I guess I found the reason. So, how do we solve this problem? Further analysis of the Prompt implementation shows that the Prompt component calls the function returned by history.block (see code example 5) to clear the contents of the Prompt after uninstallation.

If it is because in the Prompt component is not uninstall, callCapruteEventListeners has been carried out. Verify the way is very simple, need only in executed the location of the position and Prompt unload callCapruteEventListeners breakpoint. The results are in line with our expectations. The final solution, we through asynchronous invocation callCapruteEventListeners, ensure implementation in the Prompt components after unloading.

conclusion

The React Router DOM and ICestARK hijacked routes, and the design considerations at the time, to help you understand some of the core operating principles of the micro front-end. Finally, if you want to know icestark source code and are interested in micro front-end implementation, do not miss it:

Icestark – Micro front end solution for large systems