preface

Before a page is presented to users, it needs to go through three processes: static resource loading, back-end interface request and rendering. What we need to do is to defend against possible abnormal situations in each process, maintain a smooth user experience, and also deal with external attacks.

The network

At present, the mainstream research and development model is the front and rear end separation, take React for example

function App() {
  const [data, setData] = useState(null);
  useEffect((a)= >{(async() = > {const data = awaitrequest(); setData(data); }) (); });if(! data)return null;
  return (
    <div className="App">
      <h1>Hello {data.name}</h1>
    </div>
  );
}
Copy the code

Poor network data return is slow, the page does not render, always show blank page, poor experience, generally we will add a transition like this:

function App() {
  ...
  if (!data) return <Loading />; . }Copy the code

Check out demo: CodeSandbox

This solves the problem of the page being white before the data is returned, but ignores the length of time the static resource is loaded, during which the page is still white, so there should also be a transition effect before loading the static resource. Try modifying the example above:

<html>
<head>
  <title>Home page</title>
  <style>.loading { ... }</style>
</head>
<body>
  <div id="app">
    <div class="loading"><span></span></div>
  </div>
  <script src="manifest.js"></script>
  <script src="vendor.js"></script>
  <script src="main.js"></script>
</body>
</html>
Copy the code

Check out demo: CodeSandbox

Loading the loading segment first and then loading the resources seems to solve the overall transition problem. However, if you observe carefully, you will find that the animation starts again after playing for a while, causing serious breakage. I believe you are also familiar with the reason. Shouldn’t let React take over the page and try to remake it again:

/* render.js */
import React from "react";
import ReactDOM from "react-dom";
export default function render(Component, props) {
  const rootElement = document.getElementById("root");
  ReactDOM.render(<Component {. props} / >, rootElement); } /* index.js */ import render from "./render"; import request from "./request"; import App from "./App"; (async () => { const data = await request(); render(App, { data }); }) ();Copy the code

Check out demo: CodeSandbox

Before the page content is presented to the user, the loading animation effect is kept to avoid the interruption of the user experience due to network reasons.

The interface

Once the static resource is loaded and we start talking to the back end to get the page data, we first need to deal with the following possible exceptions.

timeout

The user can tolerate a waiting time of about 3~5s from the time of visiting a web page to the time of presenting it, and the interface request should return the result within 3s excluding the time of loading static resources.

If the user’s network is poor and we do not set the interface timeout, the page will always be in loading state, and the user will directly leave without effective feedback. So we need to set a reasonable timeout period and give the user feedback when the timeout is triggered.

We choose to use native FETCH to initiate the request. Unfortunately, FETCH does not support timeout parameter setting, so we need to manually wrap it:

async function request(url, options = {}) {
  const{ timeout, ... restOptions } = options;const response = await Promise.race([
    fetch(url, restOptions),
    timeoutFn(timeout),
  ]);
  const { data } = await response.json();
  return data;
}
function timeoutFn(ms = 3000) {
  return new Promise((resolve, reject) = > {
    setTimeout((a)= > {
      reject(Error('timeout'));
    }, ms);
  });
}
Copy the code

Then prompt in case of timeout:

async function request(url, options = {}) { const { timeout, ... restOptions } = options; try { const response = await Promise.race([ baseRequest(url, restOptions), timeoutFn(timeout), ]); const { data } = await response.json(); return data; } catch (error) {if (error. Message === 'timeout') {render(() => <span> request timeout, please try </span>); } throw error; }}Copy the code

The timeout prompt function is available, but the user can either refresh the page or exit and reenter. The experience is still not friendly.

Ideally, the user should be able to retry directly on the current page, without a page refresh or pop-up. Let’s tweak the code again to simulate a relatively complete example:

Check out demo: CodeSandbox

Error handling

General error handling

After receiving the result of the request, we first handle the network related error:

const statusText = {
  401: 'Please log in again'.403: 'No operation permission'.404: 'Request does not exist'.500: 'Server exception'. };function request(url, options = {}, callback) {
  const{ timeout, ... restOptions } = options;try {
    const response = await Promise.race([
      fetch(url, restOptions),
      timeoutFn(timeout),
    ]);
    const { status } = response;
    if (status === 200) {
      const { data } = await response.json();
      callback(data);
      return;
    }
    render(
      PageError, 
      { 
        children: statusText[status] || 'System exception, please try again later'}); }catch (error) {
    if (error.message === 'timeout') {
      render(PageError, {
        key: Math.random(),
        onFetch() {
          request(url, options, callback);
        },
        children: 'Request timed out, hit retry'
      });
    }
    throwerror; }}Copy the code

Business error handling

Next, we will handle the normal business error returned by the back-end, and the data structure returned by the convention of the first and the back-end:

{
  success: true/false.data: {                Return if success is true
    id: '69887645366'.desc: 'Here's the product description.',},errorCode: 'E123456'.// Return when success is false
  errorMsg: 'Product ID cannot be empty'.// Return when success is false
}
Copy the code

Processing error:

if (status === 200) {
  const { success, data, errorMsg } = await response.json();
  if (success) {
    callback(data);
    return;
  }
  render(PageError, { children: errorMsg });
}
Copy the code

Check out demo: CodeSandbox

The request to cancel

If you’ve been writing React SPA pages, you’ve probably come across this error:

Component A is destroyed. When the request comes back, setState returns an error. Let’s see A simple example:

Check out demo: CodeSandbox

Unmount the component and cancel the request. Fetch is not supported.

function request(url, options = {}, callback) {
  const fetchPromise = fetch(url, options)
    .then(response= > response.json());
  let abort;
  const abortPromise = new Promise((resolve, reject) = > {
    abort = (a)= > {
      reject(Error('abort'));
    };
  });
  Promise.race([fetchPromise, abortPromise])
    .then(({ data }) = > {
      callback(data);
    }).catch((a)= >{});return abort;
}
useEffect((a)= > {
  const abort = request('https://cnodejs.org/api/v1/topic/5433d5e4e737cbe96dcef312', {}, setData);
  return (a)= > {
    abort();
  };
});
Copy the code

Check out demo: CodeSandbox

So far, we have basically solved the various cases of interface exceptions, and now we can move on to writing the business logic.

It is recommended that you choose an AXIos-like Http request library in a production environment, as the native FETCH capability is too weak

The rendering

Exception handling

Let’s say I have a page that shows the user’s balance, and it looks something like this

The normal data structure returned by the backend looks like this:

{ rest: { amount: "10"}}Copy the code

The front-end rendering logic generally looks like this:

<div>
  <strong>Balance:</strong>
  <span className="highlight">${rest. Amount}</span>
</div>
Copy the code

Error: Cannot read property ‘amount’ of undefined (error: Cannot read property ‘amount’ of undefined)

Perhaps some people’s approach is to judge empty:

<span className="highlight">{rest && rest. Amount} yuan</span>
Copy the code

This approach creates two problems

  • Many fields need to be nulled, a lot of redundant code, poor readability
  • Core data display is not clear, misleading users, easy to cause customer complaints

In React, we can use ErrorBoundary for unified processing:

class ErrorBoundary extends Component {
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  state = {
    hasError: false}; componentDidCatch(error, info) {// reportError(error, info);
  }
  render() {
    const { hasError } = this.state;
    const { children } = this.props;
    if (hasError) {
      return <div>The system is abnormal. Please try again later</div>;
    }
    returnchildren; }}function render(Component, props) {
  const rootElement = document.getElementById("root");
  ReactDOM.render(
    <ErrorBoundary>
      <Component {. props} / >
    </ErrorBoundary>,
    rootElement
  );
}
Copy the code

Check out demo: CodeSandbox

But the drop

One day, the business asked for a banner AD at the bottom of the balance page and added the interface for obtaining ads:

function requestAd(callback) {
  callback({ ad: { desc: "This is an advertisement."}}); }Copy the code

Unfortunately, the advertising interface is unstable and the returned data often has problems. According to the above example, it will go directly to the ErrorBoundary and display an exception, which is obviously not in line with our expectations.

Therefore, we need to downgrade the non-core business:

<div>
  <strong>Balance:</strong>
  <span className="highlight">${rest. Amount}</span>
  <ErrorBoundary fallback>
    <Ad />
  </ErrorBoundary>
</div>
Copy the code

Check out demo: CodeSandbox

The heavy ones

Form submission is a very common scenario, and there are two ways to prevent repeated clicks

Button against the heavy

Add anti-weight to buttons, for example:

function App() {
  const [applying, setApplying] = useState(false);
  const handleSubmit = async() = > {if (applying) return;
    setApplying(true);
    try {
      await request();
    } catch (error) {
      setApplying(false); }};return (
    <div className='App'>
      <button onClick={handleSubmit}>{applying ? 'Submitting... ':' submit '}</button>
    </div>
  );
}
Copy the code

The advantage is that it does not affect the user’s overall page operation, and the disadvantage is that it requires page management state.

Global heavy proof

Cover the entire page, for example:

function request(url) {
  Loading.show('Requested... ');
  try {
    await fetch(url);
  } catch (error) {
    // show error
  } finally{ Loading.hide(); }}function App() {
  const handleSubmit = (a)= > {
    request();
  };
  return (
    <div className='App'>
      <button onClick={handleSubmit}>submit</button>
    </div>
  );
}
Copy the code

The advantage is that you don’t have to use the page to manage your state. The disadvantage is that the prompt is heavy and will block other user operations.

The attack

xss

Script injection attack, for example, to leave a message under a post, content injection of a script to obtain the cookie of the currently logged in user:

<script>report(document.cookie)</script>
Copy the code

If the site does not escape the output of the message, it will be injected with a script, and all users who access the post will be victims.

If the website does the output escape, you will see the contents of this pile:

&lt; script&gt; report(document.cookie)&lt; /script&gt;Copy the code

Currently, mainstream libraries or frameworks do escape output for us by default, like React. If we must render HTML fragments, we need to use dangerouslySetInnerHTML.

csrf

Cross-site scripting, such as leaving a comment on a post on www.a.com with a phishing link that leads to a page developed by the attacker, www.b.com, that simply initiates a request for a reply to the post

<form action="http://www.a.com/replay">
  <input type="text" name="content" value="This is an automatic response.">
</form>
Copy the code

Users browsing the post inadvertently click on the link, which automatically replies. The common defense method is token verification. www.a.com delivers a token through a cookie, reads the token in the cookie and places it in the request header for server authentication during write operations. Due to the same-origin policy of the browser, website B cannot read the token of website A.

Another option is to add referer to verify that only whitelisted domains are allowed to be written. Generally, the two methods are used together to ensure website security.

CSRF is defended at the network request level. Only frameworks provide full functionality, such as Angular, which generally requires our own integration.

summary

The exceptions listed above account for less than 1% of the estimated actual cases, but almost 99% of our base code is written for them. The first consideration of qualified programmers in the coding process is how to prevent extreme exceptions, only do well in this 1% exception handling, in order to better serve the remaining 99%.

About us:

We are ant Insurance experience technology team, from the Insurance business group of Ant Financial. We are a young team (no historical technology stack baggage), current average age is 92 years (remove a highest score 8x years – team leader, remove a lowest score 97 years – intern young brother). We support almost all of alibaba group’s insurance businesses. 18 years our output of mutual treasure sensational insurance industry, 19 years we have a number of heavyweight projects in preparation for mobilization. With the rapid development of the business group, the team is also expanding rapidly. Welcome to join us

We want you to be: technically grounded, deep in a specific field (Node/ interactive marketing/data visualization, etc.); Good at precipitation, continuous learning; Personality optimistic, cheerful, lively and outgoing.

If you are interested in joining us, please send your resume to [email protected]