History is a JavaScript library that lets you easily manage session history anywhere your JavaScript is running

1. Introduction

While History is maintained by Facebook, the React-router relies on history, as opposed to the browser’s window.history, which contains window.history. An API that allows developers to use History in any environment (e.g. Node, React Native, etc.).

This reading is divided into five parts, respectively for the introduction, use, analysis, demo, summary, five parts are not connected to each other can be separated according to the need to see.

Foreword for introduction, use for the use of the library, parsing for the source code of the analysis, demo is the core of the source code of the small demo, summed up as blowing water, learn to apply.

It is recommended to read with the source code combined with this article, so that it is easier to understand!

  1. history
  2. History Resolves the Github address
  3. Take you hand in hand on the React-Router History car

2. Use

There are three different ways to create a history object, depending on your code environment:

  1. createBrowserHistorySupport:HTML5 history apiModern browsers (e.g./index);
  2. createHashHistory: Traditional browsers (e.g./#/index);
  3. createMemoryHistory: environment without Dom (for example:Node,React Native).

Note: this article only resolves createBrowserHistory. All three constructs are similar


      
<html>
  <head>
    <script src="./umd/history.js"></script>
    <script>
      var createHistory = History.createBrowserHistory
      // var createHistory = History.createHashHistory

      var page = 0
      // createHistory Creates the required history object
      var h = createHistory()

      // h.lock triggers to inform the user that the address bar is about to change before the address bar changes
      h.block(function (location, action) {
        return 'Are you sure you want to go to ' + location.path + '? '
      })

      // h.listen listens for changes to the current address bar
      h.listen(function (location) {
        console.log(location, 'lis-1')})</script>
  </head>
  <body>
    <p>Use the two buttons below to test normal transitions.</p>
    <p>
      <! -- h.paush -->
      <button onclick="page++; h.push('/' + page, { page: page })">history.push</button>
      <! -- <button onclick="page++; h.push('/#/' + page)">history.push</button> -->

      <button onclick="h.goBack()">history.goBack</button>
    </p>
  </body>
</html>
Copy the code

Block is used to intercept addresses before they change, listener is used to listen for changes in the address bar, and push, replace, and go(n) are used to jump

3. The parsing

Post out the source code I will delete to understand the principle of not important parts!! ! If you want to see the complete source please download ha

You can see the Modules folder from history’s source library directory, which contains several files:

  1. Createbrowserhistory. js creates the history object of createBrowserHistory;
  2. Createhashhistory.js creates the history object of createHashHistory;
  3. Creatememoryhistory. js creates the history object of createMemoryHistory;
  4. CreateTransitionManager. Js transition management (e.g., treatment in the block function play frame, the listener queue);
  5. Domutils.js Dom utility classes (e.g., pop-ups, judging browser compatibility);
  6. Index.js entry file;
  7. Locationutils.js handles the Location tool;
  8. Pathutils.js handles the Path tool.

Entry file index.js

export { default as createBrowserHistory } from "./createBrowserHistory";
export { default as createHashHistory } from "./createHashHistory";
export { default as createMemoryHistory } from "./createMemoryHistory";
export { createLocation, locationsAreEqual } from "./LocationUtils";
export { parsePath, createPath } from "./PathUtils";
Copy the code

To separate out all the methods that need to be exposed by filename, let’s start with the createBrowserHistory constructor for history.

3.1 createBrowserHistory

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  // Browser history
  const globalHistory = window.history;
  // Initialize location
  const initialLocation = getDOMLocation(window.history.state);
  // Create the address
  function createHref(location) {
    returnbasename + createPath(location); }... const history = {// window. History property length
    length: globalHistory.length,

    // history Current behavior (PUSH- enter, POP- POP, REPLACE- REPLACE)
    action: "POP".// Location object (address related)
    location: initialLocation,

    // Current address (including pathname)
    createHref,

    // The jump method
    push,
    replace,
    go,
    goBack,
    goForward,

    / / interception
    block,

    / / to monitor
    listen
  };

  return history;
}

export default createBrowserHistory;
Copy the code

The createBrowserHistory function returns the history object. The history object provides many properties and methods. The biggest problem is initialLocation. That is the history. The location. Our parsing order is as follows:

  1. The location;
  2. CreateHref;
  3. Block;
  4. Listen;
  5. A push.
  6. The replace.

3.2 the location

The location property stores information about the address bar. Let’s compare the return value of createBrowserHistory, history.location, with window.location

// history.location
history.location = {
  hash: ""
  pathname: "/history/index.html"
  search: "? _ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  state: undefined
}

// window.location
window.location = {
  hash: ""
  host: "localhost:63342"
  hostname: "localhost"
  href: "http://localhost:63342/history/index.html? _ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  origin: "http://localhost:63342"
  pathname: "/history/index.html"
  port: "63342"
  protocol: "http:"ƒ reload()"? _ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
}
Copy the code

History. location is window.location. Let’s look at how the author deals with it.

const initialLocation = getDOMLocation(window.history.state)
Copy the code

The initialLocation function is equal to the return value of the getDOMLocation function. (getDOMLocation will be called a lot in history, so it’s important to understand this function well.)

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  // Process basename (relative address, e.g., index, if basename is /the/base, then /the/base/index)
  const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "";
  
  const initialLocation = getDOMLocation(window.history.state);

  // Handle the state argument and window.location
  function getDOMLocation(historyState) {
    const { key, state } = historyState || {};
    const { pathname, search, hash } = window.location;

    let path = pathname + search + hash;

    // Ensure that path does not contain basename
    if (basename) path = stripBasename(path, basename);

    // Create the history.location object
    return createLocation(path, state, key);
  };

  const history = {
    // Location object (address related)
    location: initialLocation,
    ...
  };

  return history;
}
Copy the code

A large project will split a function into at least two functions, a function that handles parameters and a function that receives parameters to implement the function:

  1. Processing parameters:getDOMLocationFunction main processingstateandwindow.locationThese two parameters return customhistory.locationObject, the main constructhistory.locationThe object iscreateLocationFunctions;
  2. Structural function:createLocationImplementation concrete constructionlocationThe logic.

Next we look at the createLocation function in the locationutils.js file

// LocationUtils.js
import { parsePath } from "./PathUtils";

export function createLocation(path, state, key, currentLocation) {
  let location;
  if (typeof path === "string") {
    // Two arguments such as push(path, state)

    // The parsePath function is used to dishash addresses for example: parsePath('www.aa.com/aa?b=bb') => {pathName: 'www.aa.com/aa', search: '? B =bb', hash: '}
    location = parsePath(path);
    location.state = state;
  } else {
    // a parameter such as push(location)location = { ... path }; location.state = state; }if (key) location.key = key;

  // location = {
  // hash: ""
  // pathname: "/history/index.html"
  // search: "? _ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  // state: undefined
  // }
  return location;
}

// PathUtils.js
export function parsePath(path) {
  let pathname = path || "/";
  let search = "";
  let hash = "";

  const hashIndex = pathname.indexOf("#");
  if(hashIndex ! = =- 1) {
    hash = pathname.substr(hashIndex);
    pathname = pathname.substr(0, hashIndex);
  }

  const searchIndex = pathname.indexOf("?");
  if(searchIndex ! = =- 1) {
    search = pathname.substr(searchIndex);
    pathname = pathname.substr(0, searchIndex);
  }

  return {
    pathname,
    search: search === "?" ? "" : search,
    hash: hash === "#" ? "" : hash
  };
}
Copy the code

CreateLocation returns a formatted location based on the path or location value passed in. The code is simple.

3.3 createHref

Is createHref function returns the current path name, such as address, http://localhost:63342/history/index.html? A =1, call h.createhref (location) and return /history/index.html? a=1

// createBrowserHistory.js
import {createPath} from "./PathUtils";

function createBrowserHistory(props = {}){
  // Process basename (relative address, e.g., index, if basename is /the/base, then /the/base/index)
  const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "";

  function createHref(location) {
    return basename + createPath(location);
  }
  
  const history = {
    // Current address (including pathname)
    createHref,
    ...
  };

  return history;
}

// PathUtils.js
function createPath(location) {
  const { pathname, search, hash } = location;

  let path = pathname || "/";
  
  if(search && search ! = ="?") path += search.charAt(0) = = ="?" ? search : `?${search}`;

  if(hash && hash ! = ="#") path += hash.charAt(0) = = ="#" ? hash : ` #${hash}`;

  return path;
}
Copy the code

3.4 listen

Here we can imagine the general monitoring process:

  1. Bind the listener function we set up;
  2. The listener function is triggered to listen for changes to history entries.

In Chapter 2, using the code, you create the History object and use the h.listine function.

// index.html
h.listen(function (location) {
  console.log(location, 'lis-1')
})
h.listen(function (location) {
  console.log(location, 'lis-2')})Copy the code

Visible listen can bind multiple monitoring function, we see the author’s first createTransitionManager. Js is how to realize the binding multiple monitoring function.

CreateTransitionManager is a transition manager (e.g., processing pop-ups in block functions, processing queues for listeners). CreateBrowserHistory (createBrowserHistory, createBrowserHistory, createBrowserHistory)

// createTransitionManager.js
function createTransitionManager() {
  let listeners = [];

  // Set the listener function
  function appendListener(fn) {
    let isActive = true;

    function listener(. args) {
      // good
      if(isActive) fn(... args); } listeners.push(listener);/ / remove
    return (a)= > {
      isActive = false;
      listeners = listeners.filter(item= >item ! == listener); }; }// Execute the listener function
  function notifyListeners(. args) {
    listeners.forEach(listener= >listener(... args)); }return {
    appendListener,
    notifyListeners
  };
}
Copy the code
  1. Setting up the Listener functionappendListener:fnThis is a user-set listener that stores all listeners in thelistenersIn the array;
  2. Execute listener functionnotifyListeners: Only loop execution is required.

This feels like a good lesson: add state management when adding queue functions (as in the code above)isActive) to determine whether to enable it.

With that in mind, let’s look at the listen source code.

// createBrowserHistory.js
import createTransitionManager from "./createTransitionManager";
const transitionManager = createTransitionManager();

function createBrowserHistory(props = {}){
  function listen(listener) {
    // Add a listener to the queue
    const unlisten = transitionManager.appendListener(listener);

    // Add a listener for history entries
    checkDOMListeners(1);

    // Remove the listener
    return (a)= > {
      checkDOMListeners(- 1);
      unlisten();
    };
  }

  const history = {
    / / to monitor
    listen
    ...
  };

  return history;
}


Copy the code

History. listen is a callback listener that is triggered when a history entry changes. So there are two steps:

  1. transitionManager.appendListener(listener)Add callback listeners to the queue.
  2. checkDOMListenersListen for changes to historical entries

Here are checkDOMListeners of historical record entries.

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  let listenerCount = 0;

  function checkDOMListeners(delta) {
    listenerCount += delta;
    
    // Whether it has been added
    if (listenerCount === 1 && delta === 1) {
      // Add bindings when the history entry changes
      window.addEventListener('popstate', handlePopState);
    } else if (listenerCount === 0) {
      // Unbind
      window.removeEventListener('popstate', handlePopState); }}// getDOMLocation(event.state) = location = {
  // hash: ""
  // pathname: "/history/index.html"
  // search: "? _ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  // state: undefined
  // }
  function handlePopState(event) {
    handlePop(getDOMLocation(event.state));
  }
  
  function handlePop(location) {
    const action = "POP";
    setState({ action, location })
  }
}
Copy the code

Although the author has written a lot of very detailed callback functions, which can be a little confusing, it makes sense to look at them closely:

  1. checkDOMListenersThere can be only one function globally that listens for history entries (listenerCountTo control);
  2. handlePopState: The listener function must be extracted, or it cannot be untied;
  3. handlePop: Core function that listens for history entries and executes after successful listeningsetState.

SetState ({action, location}) updates history based on the current location.

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  function setState(nextState) {
    / / update the history
    Object.assign(history, nextState);
    history.length = globalHistory.length;

    // Execute the listener function listen
    transitionManager.notifyListeners(history.location, history.action);
  }

  const history = {
    / / to monitor
    listen
    ...
  };

  return history;
}
Copy the code

Here, when changing the history entry successfully:

  1. Update the history;
  2. Execute the listener function listen;

This is the main process of H. Stine, isn’t it quite simple?

3.5 block

The function of history.block isto trigger a prompt when a history entry changes. Here we can imagine the general interception process:

  1. Bind the interception function we set;
  2. Listen for changes to history entries, triggering interceptor functions.

Does this feel similar to the listen function? In fact, the code for listening to the change of the history entry is the same as that for H. listen and H. block (of course, only one function can be bound to listen to the change of the history entry). 3.1.3 I have modified part of the code for easy understanding, the following is the complete source code.


In Chapter 2, you use the h.block function after creating the History object (only one block function can be bound).

// index.html
h.block(function (location, action) {
  return 'Are you sure you want to go to ' + location.path + '? '
})
Copy the code

The same. Let’s look at the author’s createTransitionManager js is how to realize the prompt.

CreateTransitionManager is a transition manager (e.g., processing pop-ups in block functions, processing queues for listeners). CreateBrowserHistory (createBrowserHistory, createBrowserHistory, createBrowserHistory)

// createTransitionManager.js
function createTransitionManager() {
  let prompt = null;

  // Set the prompt
  function setPrompt(nextPrompt) {
    prompt = nextPrompt;

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

  /** * @param location: address * @param action: action * @param getUserConfirmation: callback: The block function returns the value as argument */
  function confirmTransitionTo(location, action, getUserConfirmation, callback) {
    if(prompt ! =null) {
      const result = typeof prompt === "function" ? prompt(location, action) : prompt;

      if (typeof result === "string") {
        Callback (window.confirm(result)) getUserConfirmation(result, callback);
        callback(window.confirm(result))
      } else{ callback(result ! = =false); }}else {
      callback(true); }}return {
    setPrompt,
    confirmTransitionTo
    ...
  };
}
Copy the code

SetPrompt and confirmTransitionTo

  1. SetPrompt: Stores user-set prompt functions in the prompt variable;
  2. Implement the prompt confirmTransitionTo:
    1. Get the prompt: execute the prompt variable;
    2. Prompt callback: Execute callback to return the prompt as a result.

Here is the source code for H.block.

// createBrowserHistory.js
import createTransitionManager from "./createTransitionManager";
const transitionManager = createTransitionManager();

function createBrowserHistory(props = {}){
  let isBlocked = false;

  function block(prompt = false) {
    // Set the prompt
    const unblock = transitionManager.setPrompt(prompt);

    // Whether block is set
    if(! isBlocked) { checkDOMListeners(1);
      isBlocked = true;
    }

    // Remove the block function
    return (a)= > {
      if (isBlocked) {
        isBlocked = false;
        checkDOMListeners(- 1);
      }

      // Eliminate the hint
      return unblock();
    };
  }

  const history = {
    / / interception
    block,
    ...
  };

  return history;
}
Copy the code

The function of history.block isto trigger a prompt when a history entry changes. So there are two steps:

  1. transitionManager.setPrompt(prompt)Setting prompt;
  2. checkDOMListenersChanges to listen for history entry changes.

It feels like there’s a lesson here: callhistory.blockIt returns an unlisten method that can be unlisten or unlisten by calling the return function (interesting).


We see listening history entries change function checkDOMListeners (1) (note: transitionManager. ConfirmTransitionTo).

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  function block(prompt = false) {
    // Set the prompt
    const unblock = transitionManager.setPrompt(prompt);

    // Whether block is set
    if(! isBlocked) { checkDOMListeners(1);
      isBlocked = true;
    }

    // Remove the block function
    return (a)= > {
      if (isBlocked) {
        isBlocked = false;
        checkDOMListeners(- 1);
      }

      // Eliminate the hint
      return unblock();
    };
  }

  let listenerCount = 0;

  function checkDOMListeners(delta) {
    listenerCount += delta;
    
    // Whether it has been added
    if (listenerCount === 1 && delta === 1) {
      // Add bindings when the address bar changes
      window.addEventListener('popstate', handlePopState);
    } else if (listenerCount === 0) {
      // Unbind
      window.removeEventListener('popstate', handlePopState); }}// getDOMLocation(event.state) = location = {
  // hash: ""
  // pathname: "/history/index.html"
  // search: "? _ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  // state: undefined
  // }
  function handlePopState(event) {
    handlePop(getDOMLocation(event.state));
  }
  
  function handlePop(location) {
    // No need to refresh the page
    const action = "POP";

    // Implement hints
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (ok) {
          / / sure
          setState({ action, location });
        } else {
          / / cancelrevertPop(location); }}); }const history = {
    / / interception
    block
    ...
  };

  return history;
}
Copy the code

Is in the trigger transitionManager handlePop function. ConfirmTransitionTo (3.1.3 I have modified to here in order to facilitate understanding).


TransitionManager. ConfirmTransitionTo callback function callback has two branches, the user clicks on the determination of prompt box button or cancel button:

  1. When the user clicks OK in the prompt box, executesetState({ action, location });
  2. Execute when the user clicks cancel in the prompt boxrevertPop(location)(Ignore).

Here already know the h.b lock function, h.l isten and createTransitionManager js. Let’s move on to another important function, h.ush.

3.6 push

function createBrowserHistory(props = {}){
  function push(path, state) {
    const action = "PUSH";
    / / tectonic location
    const location = createLocation(path, state, createKey(), history.location);

    // Execute the block function, pop-up box
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if(! ok)return;

        // Get the current pathname
        const href = createHref(location);
        const { key, state } = location;

        // Add a history entry
        globalHistory.pushState({ key, state }, null, href);
        
        if (forceRefresh) {
          // Force refresh
          window.location.href = href;
        } else {
          / / update the historysetState({ action, location }); }}); }const history = {
    / / jump
    push,
    ...
  };

  return history;
}
Copy the code

The most important thing here is the globalHistory.pushState function, which directly adds a new history entry.

3.7 the replace

function createBrowserHistory(props = {}){
  function replace(path, state) {
    const action = "REPLACE";
    / / tectonic location
    const location = createLocation(path, state, createKey(), history.location);

    // Execute the block function, pop-up box
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if(! ok)return;
        // Get the current pathname
        const href = createHref(location);
        const { key, state } = location;

        globalHistory.replaceState({ key, state }, null, href);

        if (forceRefresh) {
          window.location.replace(href);
        } else{ setState({ action, location }); }}); }const history = {
    / / jump
    replace,
    ...
  };

  return history;
}
Copy the code

The difference between push and replace is the difference between history.pushState and history.replaceState.

3.8 the go

function createBrowserHistory(props = {}){
   function go(n) {
    globalHistory.go(n);
  }

  function goBack() {
    go(- 1);
  }

  function goForward() {
    go(1);
  }

  const history = {
    / / jump
    go,
    goBack,
    goForward,
    ...
  };

  return history;
}
Copy the code

It’s a use of history.go.

4.demo

Take you hand in hand on the React-Router History car

5. To summarize

In general, if blocks are not required, the native method will suffice. PushState, history.replaceState, history.go(n), popState. Company overtime is serious, use the remaining time to expand their knowledge, the best way is to read the source code. Start always a little difficult, the first time to read a face meng force, the second time to read two face meng force, the third time to read a little meng force, the fourth time to read this B cow force ~. Just keep writing more test cases to understand it. Come on!