Recently did a background project, since it is the background management system, the login control is naturally indispensable.

Received demand – background system! The React Router, the React Router, and the basic structure of the React code took almost half a day to figure out. The next step was the so-called login logic.

React V16 has undergone major changes, and I haven’t written React lately. Take this opportunity to familiarize yourself with the React API. React’s new Context API replaces Redux.

The React Context in the root component passes the login status

It’s been a while since React, but its new Context API still looks delicious. Then, in two clicks, a set of Context appears in the entry file:

// Default login information before login
// You can see the data structure and content of the "login information" I designed
const defaultLoginInfo = {
  username: ' '.token: false.ident: false};class Main extends React.Component {
  constructor(props) {
    super(props);
    // Write the login information to this.state
    // Use toStore to determine whether to write Storage as well
    this.updateLogin = (info, toStore = true) = > {
      this.setState(prevState= > {
        letnv = {... prevState.userLoginInfo, ... info}; toStore &&this.storeLoginInfo(nv);
        return {userLoginInfo: nv};
      });
    };

    // Retrieve the login information from sesionStorage
    this.retrieveLoginInfo = (a)= > {
      let stored = sessionStorage.getItem('userLoginInfo');
      // We need a try... catch ... But I removed it to make it easier to read
      return stored ? JSON.parse(stored) : {};
    };
    
    // Save the login information to Storage
    this.storeLoginInfo = (val) = > {
      sessionStorage.setItem('userLoginInfo'.JSON.stringify(val));
    };

    this.state = {
      userLoginInfo: {
        ...defaultLoginInfo,
        update: this.updateLogin,   // Expose methods to child components
        // Generate the actual login information by overwriting the default login information when the main component is first deployed. this.retrieveLoginInfo(),exit: (a)= > this.updateLogin({... defaultLoginInfo}),// Expose methods to child components - log out}}; } render() {}/ /...

  componentDidMount() {
    this.updateLogin(this.retrieveLoginInfo(), false); }}Copy the code

It’s pretty crude — that was my first work replacing Redux with the Context API. But it was concise, and it worked fine — until I wanted to add some new ideas to the login section…

A few words :(in my eyes) the login function of the management system should have some function points

  1. Login state persistence. What user rights ah, identity ah, token ah, all must be stored in Storage.
  2. Login status is globally accessible, including outside the React component.
  3. After the Storage is lost, guide the user to log in again.
  4. You can use a pop-up box instead of a page redirect. (Imagine a user filling out a bunch of forms and the program redirects when it detects that the login is invalid…)

From these points, the original code is useless outside of the React component…

Extract the logic

Global access

Put the code globally…

The window object? (If you have this idea, please think about it.)

So how do you avoid using global variables and still solve the data storage problem? That is “sandbox”. 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.

Well, that “closure” thing again.

But now we have modularity. When we import a module, the module declaration remains in a separate scope and persists. You can use exports for a “sandbox” effect. Except for the derived function, nothing is visible to the outside world. (Webpack modules are also implemented with closures, aren’t they?)

Synchronize everywhere (publish subscribe)

Analyze:

Where do I need to publish?

  • Log out button
  • The backend API tells me, “Login info is wrong.”
  • The front-end proactively detects that the login information is corrupted

Where do I subscribe?

  • UI, which is the state of the root component

Obviously, publish subscriptions are appropriate.

code

// Login expiration detection
const checkExpireTime = info= > {
  return Date.now() > info.expireTime && info.expireTime >= 0;
};

// Take charge of the Storage operation
function retrieveLoginInfo() {
  let stored = sessionStorage.getItem('userLoginInfo');
  if(stored) {
    try {
      letinfo = { ... defaultLoginInfo, ... JSON.parse(stored) };if(checkExpireTime(info) || ! info.token) { exitLogin();return{... defaultLoginInfo}; }return{... defaultLoginInfo, ... info}; }catch(e) {
      return {...defaultLoginInfo};
    }
  } else {
    exitLogin();
    return {...defaultLoginInfo};
  }
}
function storeLoginInfo(val) {
  return sessionStorage.setItem('userLoginInfo'.JSON.stringify(val));
}

/ / radio
function broadcastLoginInfo(info) {
  broadcastList.forEach(curt= > {
    curt(info);
  });
}
/ / store the Listener
let broadcastList = [];
function registerLoginInfoBroadcast(callback) {
  if(!broadcastList.includes(callback)) {
    broadcastList.push(callback);
  }
}

// Update login information - similar to Dispacher
function updateLoginInfo(info) {
  if(checkExpireTime(info)) {
    exitLogin();
    return [false.'Login expired, please log in again'];
  } else {
    storeLoginInfo(info);
    broadcastLoginInfo(info);
    return [true]; }}// Some common actions are extracted (we will reject the sample code)
function exitLogin() { updateLoginInfo({... defaultLoginInfo}); }function syncLoginInfo() {
  broadcastLoginInfo(retrieveLoginInfo());
}

export default { 
  update: updateLoginInfo,
  retrieve: retrieveLoginInfo,
  exit: exitLogin,
  registerBroadcast: registerLoginInfoBroadcast,
  sync: syncLoginInfo,
  storeLoginInfo,
  retrieveLoginInfo,
  defaultLoginInfo,
};

Copy the code

How to use it?

For example, if the background detects a Token error and wants to forcibly clear the login information, what do I do?

import loginInfo from '@/utils/path/to/loginInfoStorage.js';
// ...
function RequestApi (respData) {
    // Do some processing
    if([301.302.303].indexOf(respData.status.code) ! = =- 1) {
        loginInfo.exit(); // Login error? Log out yourself!}}Copy the code

The React root component is a bit more complicated…

Inject the React

In the root component:

Remember Register Listener

// Put it in constructor or componentDidMounted
    loginInfo.registerBroadcast(info= > {
      this.updateLoginState(info, false);
    });
Copy the code

The State of the root component is updated when the Listener fires

    this.updateLoginState = (info, toStore = true) = > {
      this.setState(prevState= > {
        letnv = {... prevState.userLoginInfo, ... info}; toStore &&this.storeLoginInfo(nv);
        let newState = {};
        if(! prevState.useLoginModal || nv.token) { newState.userLoginInfo = {... nv}; }return newState;
      });
    };
Copy the code

The functions passed along with the Context to the child components are also important

    this.state = {
      userLoginInfo: {
        ...this.retrieveLoginInfo(),
        update: this.updateLoginInfo,
        exit: (a)= > {
            // There are other functionsloginInfo.exit(); ,}}};Copy the code

To highlight the essence, this is just my simplified code. The complete code (see below) also features such as login pop-ups.

Pay attention to

  • Update (componentDidUpdate) is not called in the Listener as a subscriber.

By the way: Login pop-up

It’s not the scope of this article, but it’s also a bit of work for me, and it doesn’t work very well. Here’s what it’s all about.

After the Storage is lost, guide the user to log in again. You can use a pop-up box instead of a page redirect.

One tricky problem is that the box can pop up, but the admin UI behind the box can’t be changed.

The idea is that there are two types of loginInfo in state — actualLoginInfo, which really reflects the actual login state, and userLoginInfo, which is for the UI. The Render of the React Router and other UI components makes judgments and renders based on userLoginInfo, whereas the actualLoginInfo is used for login pop-ups. Here is the actual code I used. It’s just not an elegant idea.

A few points to note:

  1. useLoginModal– Use login pop-up or route to jump to a whole login page?
  2. Not login anduseLoginModalIf true, the login popup window is displayed
class Main extends Component {
  constructor(props) {
    super(props);

    this.updateLoginState = (info, toStore = true) = > {
      this.setState(prevState= > {
        letnv = {... prevState.userLoginInfo, ... info}; toStore &&this.storeLoginInfo(nv);
        let newState = {
          actualLoginInfo: {... nv}, };if(! prevState.useLoginModal || nv.token) { newState.userLoginInfo = {... nv}; newState.useLoginModal = !! info.token;// The login popup is used by default after login
        }
        return newState;
      });
    };

    loginInfo.registerBroadcast(info= > {
      // Display login popup without modifying login related UI state
      this.updateLoginState(info, false);
    });

    this.retrieveLoginInfo = loginInfo.retrieve;
    this.updateLoginInfo = loginInfo.update;
    
    // [NOTE] needs to read the login status before rendering 
      
    // Otherwise the URL will be lost after the refresh because Route is not rendered
    this.state = {
      userLoginInfo: {
        ...this.retrieveLoginInfo(),
        // Update the login information
        update: this.updateLoginInfo,
        // Use this function in the UI to log out
        // config.useLoginModal
        // -true Rewrites the login status, does not change the login UI status, and displays the login popup
        // -false Rewrites the login status, changes the LOGIN UI status, and returns to the login page
        exit: (config = {}) = > {
          if(config.useLoginModal || false) {
            this.setState({
              useLoginModal: true,
            }, () => {
              loginInfo.exit();
            });
          } else {
            this.setState({
              useLoginModal: false, }, () => { loginInfo.exit(); }); }}},useLoginModal: false.actualLoginInfo: {}}; render() {return( <UserCtx.Provider value={this.state.userLoginInfo}> <UserCtx.Consumer> {info => ( <main styleName="main-container"> <HashRouter> <LocaleProvider locale={zh_CN}> <> <Switch> {(! info.token) && <Route path="/login" component={withRouterLogin} />} {info.token && <Route path="/admin" component={withRouterAdmin} />} <Redirect to={info.token ? '/admin' : '/login'} /> </Switch> </> </LocaleProvider> </HashRouter> {/* Login popup is displayed when you are not logged in and useLoginModal */} <Modal title=" Please login account first" visible={this.state.useLoginModal && ! this.state.actualLoginInfo.token} footer={false} width={370} closable={false} > <LoginForm /> </Modal> </main> )} </UserCtx.Consumer> </UserCtx.Provider> ); }}Copy the code

TODO

A lot of things are still to be done:

  • Go to storage.get! Can I add a cache?
  • Can be encapsulated as a class or constructor? It’s more versatile!
  • Canceling the Listener function has not been written yet…

conclusion

This is how I, the front-end dog, unknowingly abstracted the login part of the code into a publish-subscribe model.