When building web apps, we can’t do without testing in browsers, especially those with powerful debugging tools like Chrome and Firefox. And a lot of times the test is related to the back-end interface.

Clever front – ends invent mocks in order not to be held back by the erratic interfaces of their back – end friends. Today, we use native mocks, such as Mockon; Mock your own Node server; Useful interface collaboration tools, such as YAPI; Also use Service Worker, such as MSW… In fact, there is another way – browser extensions. Browser extensions allow us to reduce the time wasted in window switching and greatly improve debugging efficiency.

Today, let’s talk about how to use Chrome to block a request from a web page and return mock data if it meets the preset criteria.

Sounds simple enough, but Chrome no longer provides official API support (it could have been changed in webRequest) after discovering some security concerns with this type of API. So we have to take a detour: insert a piece of code into the page that proxies the requested JS API.

Why would you want to insert code into the page when an extension can’t

The answer is no! Let’s take a look at the architectural design of Chrome extensions:

The developer needs to consider five parts: the open page (which can be any TAB), the content_script of the extension running on each page, the popup window for the extension, and a background script and setup interface for the extension. Each part runs in its own black box, communicating with each other only through a few interfaces provided by Chrome. Like sendMessage, storage.

If we want to retrieve or manipulate the content on a user’s page, we rely on content_script. It is the only place in the extension where you can get the page document document. However, this is limited to document. To protect the user, the window object in content_script is not the same as the window object in the page. That is, the JS apis they run are isolated. Therefore, we had to “sneak” a piece of code into the user page to help manipulate the interface on the page.

Intercept the fetch

Intercepting fetch is actually quite simple, just need to change the original API package:

// Save the old fetch
const f = window.fetch
// Make it your own
window.fetch = (req, config?) = > {
  The hjack method checks if there is any mock and returns a Response Object if there is
  return hijack(req, config)
    // If not, throw it back to the original fetch
    .catch(() = > f(req, config))
}
Copy the code

Intercept XHR

In addition to intercepting FETCH, you need to consider intercepting XHR when your application has code to convert to ES5 standards. Unlike FETCH, intercepting XHR is more cumbersome.

Let’s review the use of XHR:

var get = new XMLHttpRequest();
get.open('GET'.'/api'.true);

get.onload = function () {
  // End of request
};
get.send(null);


var post = new XMLHttpRequest();
post.open("POST".'/api'.true);
post.setRequestHeader("Content-Type"."application/json; charset=UTF-8");
post.onreadystatechange = function() {
    if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
        // End of request
    }
}
post.send(JSON.stringify({}));
Copy the code

XHR’s request setting, sending, and return result processing are distributed in different methods. Like FETCH, “sudden change” has to be taken care of each one.

Fortunately, I found a friend to open source a package called Ajax-hook, to write these codes well! With Ajax-hook, we just need to do this:

import { proxy } from 'ajax-hook'
proxy({
  onRequest: (config, handler) = >
    hijack(config)
      .then(({ response }) = > {
        // Find the mock and ask handler to help pass the mock data to the next step
        return handler.resolve({
          config,
          status: 200.headers: [],
          response,
        })
      })
  		// If the mock is not found, you can use handler.next to let XHR handle it
      .catch(() = > handler.next(config)),
  onResponse: (response, handler) = > {
    // Handler helps respond to data (whether it is returned online or mock)
    handler.resolve(response)
  },
})
Copy the code

Configure the Chrome extension to insert this blocking code

To insert this code we need at least three files:

intercept.js
content_script.js
manifest.json
Copy the code

We’ll put the above code in intercept. Js:

import { proxy } from 'ajax-hook'

function hijack(url, { method }) {
  return new Promise((resolve, reject) = > {
    // Replace this code later
    console.log('Request interception${method} ${url}`)
    reject();
  })
}

proxy({
  onRequest: (config, handler) = >
    hijack(config.url, config)
      .then(({ response }) = > {
        return handler.resolve({
          config,
          status: 200.headers: [],
          response,
        })
      })
      .catch(() = > handler.next(config)),
  onResponse: (response, handler) = > {
    handler.resolve(response)
  },
})

if (window.fetch) {
  const f = window.fetch
  window.fetch = (req, config?) = > {
    return hijack(req, config)
      .then(({ response }) = > {
        return new Response(response, {
          headers: new Headers([]),
          status: 200,
        })
      })
      .catch(() = > f(req, config))
  }
}
Copy the code

Note that we refer to a third-party package here, and you need to consider packaging it with a packaging tool.

Then insert the file into the document in content_script.js:

const interceptScript = document.createElement('script')
interceptScript.src = chrome.runtime.getURL('intercept.js')
document.head.prepend(interceptScript)
Copy the code

Then remember to declare the corresponding permissions in manifest.json:

{
  "name": "My Chrome Extension"."description": ""."manifest_version": 2."version": "1.0.0"."permissions": []."content_scripts": [{"js": ["content_script.js"]."matches": ["*://*.example.com/*"]."all_frames": true}]."content_security_policy": "script-src 'self' https://*.example.com/*; object-src 'self'"."web_accessible_resources": ["intercept.js"]}Copy the code

Content_scripts and content_security_policy are used to prepare our content_script.js to run in a browser. You need to specify the domain name that you want to access. In addition, web_accessibLE_resources must declare the code file we are about to insert, otherwise the page will not be able to access it.

Edit and store mock data

Now we need a simple interface to set up mock data. This page can be in the pop-up box in the upper right corner of Chrome, in the Debug tool box, or on a separate page, depending on where you like it. I’ll skip the UI part here, assuming that the following interface is eventually called on the page to save the data to Chrome Storage:

chrome.storage.local.set({ key: value }, callback)
Copy the code

Note that There are several storage interfaces for Chrome. The main ones we can use for storage are Local and sync. In addition to the size difference, it’s also important to note that as the name suggests, one is local storage and the other is storage that allows Google accounts to be synchronized. In general, we prefer local storage as much as possible.

Suppose we store mock in data with key values of mock, including the matching full URL, method, and mock response data, as follows:

chrome.storage.local.get(['mock'].function(result) {
  console.log(result.mock);
  / * print: {' GET http://www.example.com/api/test: {response: '{" MSG ":" This is a mock "}'}} * /
});
Copy the code

So how does the code inserted into the page get this information? It belongs to the page environment and cannot access the extended storage.

Here we can take advantage of Chrome’s page-to-extension communication messaging mechanism.

The page sends messages to the specified extension through the following interface:

chrome.runtime.sendMessage(
  EXTENSION_ID,
  message,
  options,
  (response) = > {
    // If there is a response, it can be handled in the callback})Copy the code

The received message is processed through the following interface:

chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) = > {
  // Process the received message and send a response to the sender via sendResponse
})
Copy the code

Let’s send a message to the extension earlier in intercept. Js. In the extension, you can process messages, check to see if we have the corresponding mock Settings, and then respond to requests from the page.

Before we do that, we first discover a question: what is EXTENSION_ID?

All Chrome extensions have an ID. The published extension ID can be found in the URL. Take this last part of the Google Translate URL:

https://chrome.google.com/webstore/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb
Copy the code

For locally installed extensions, type Chrome :// Extensions in your browser and press Enter to see the extension ID under the icon.

However, careful users will soon notice that local debugging updates the ID every time it is reloaded. So what to do?

We can get the current ID in the extension chrome.runtime.id. Insert a global variable into the page for intercept. Js to read.

Modify content_script.js as follows:

+ const extensionGlobals = document.createElement('script')
+ extensionGlobals.innerText = `window.__EXTENTION_ID__ = "${chrome.runtime.id}"; `
+ document.head.prepend(extensionGlobals)
const interceptScript = document.createElement('script')
interceptScript.src = chrome.runtime.getURL('intercept.js')
document.head.prepend(interceptScript)
Copy the code

You can then tell Intercept. Js to send a message to the extension:

import { proxy } from 'ajax-hook' function hijack(url, { method }) { return new Promise((resolve, Reject) => {console.log(' intercept request ${method} ${url} ')+ chrome.runtime.sendMessage(
+ window.__EXTENSION_ID__,
+ {
+ type: 'request_mock',
+ url,
+ method
+},
+ {},
+ (response) => {
+ if (response) resolve(response);
+ else reject();
+}
    )
  })
}

proxy({
  onRequest: (config, handler) =>
    hijack(config.url, config)
      .then(({ response }) => {
        return handler.resolve({
          config,
          status: 200,
          headers: [],
          response,
        })
      })
      .catch(() => handler.next(config)),
  onResponse: (response, handler) => {
    handler.resolve(response)
  },
})

if (window.fetch) {
  const f = window.fetch
  window.fetch = (req, config?) => {
    return hijack(req, config)
      .then(({ response }) => {
        return new Response(response, {
          headers: new Headers([]),
          status: 200,
        })
      })
      .catch(() => f(req, config))
  }
}
Copy the code

The extension is best handled in background_script because, unlike pop-ups and debuggers, it is initialized without the user opening it and runs in the background. To open background_script, declare it in manifest.json:

{"name": "My Chrome Extension", "description": ", "Manifest_version ": 2, "version": "1.0.0", "permissions": [+ "background",
+ "*://*.example.com/*"].+ "background": {
+ "scripts": ["background_script.js"],
+ "persistent": true
+},
+ "externally_connectable": {
+ "matches": [
+ "*://*.example.com/*"
+]
+},
  "content_scripts": [
    {
      "js": ["content_script.js"],
      "matches": ["*://*.example.com/*"],
      "all_frames": true
    }
  ],
  "content_security_policy": "script-src 'self' https://*.example.com/*; object-src 'self'",
  "web_accessible_resources": ["intercept.js"]
}

Copy the code

Create background_script.js to handle request_mock we received:

chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) = > {
   if (message && message.type === 'request_mock') {
    const { method, url } = message;
    chrome.storage.local.get(['mock'].function({ mock }) {
      const matchedMock= mock[`${method.toUpperCase()} ${url}`]; sendResponse(matchedMock); }); }});Copy the code

There you have it, a mock plug-in.

Insert a bug for code interception

Inserting code to intercept a request like this gets almost all the information about the original request, but it has a fatal flaw — it can’t intercept requests made during document loading.

This is because our insert behavior is a DOM operation and we wait for the document to load before we can start the insert operation. I haven’t found a good way around it yet.

If you want complete coverage, you may need to use Chrome’s proxy interface and an external mock server to handle it.