The opening words

Start shamelessly writing technical articles this year, averaging one every two weeks. Production is not high, one is because usually work is still very busy, two is not to write and write, if you do not feel interesting things, it is difficult to write down.

As the saying goes, good comments install force to add suffix at the end, the article niu force title to add prefix, so always want to write what series, you can add a prefix to the article title. But there are a lot of books and documentation that are really good on basic concepts like closures and anti-shaking, and I don’t think I can write anything new.

Actually front end into the line of a few years later, feel is a significant change, from a novice when a lot of things will not do, the most common concern is what do not to come out, now what can do it, is the choice of different techniques combination, the most common concern is implemented is the best, what is the common practice in the industry. Therefore, I would like to write a series of articles about solving a certain problem or realizing a certain function in my daily work, so as to provide ideas for friends who have similar function development needs, and also to share and communicate with everyone to see if there is a better implementation plan.

specifications

In order to improve the user access to experience, the company’s APP and embedded H5 pages use a CDN to accelerate, but a few days ago some of the problem, because the CDN service provider server goes down, the secondary node to the normal parts of mobile users can’t access, because is only a matter of CDN server, its actual source address or can be accessed. Later, a similar thing happened again, and we were required to make a CDN switch function. If some users have problems accessing CDN, they can switch to other CDN or directly access the source station. For our front end, it’s actually quite simple if the HTML file is put together with the imported static resources. But the problem is that our HTML is on one server, while static resources are on a dedicated Object Storage Service (OSS), which is a bit troublesome.

Introduction to relevant knowledge

What is the CDN

Content Delivery Network (English: Content Delivery Network or Content Distribution Network) CDN) is a computer network system connected to each other through the Internet, using the server closest to each user, faster and more reliable to send music, pictures, movies, applications and other files to users, to provide high performance, scalability and low-cost network content to users.

The above text is taken from Wikipedia, perhaps in writing, and is briefly described as follows:

You have a file A.js on a server in Hangzhou, and then you have a source station address hangzhou.oss.com/a.js, if you request this address is from the source station to get the A.js file, but this server jiangsu, Zhejiang and Shanghai may access quickly, but in pingdingshan (my hometown, So I spent some money to configure the CDN acceleration and gave me a CDN acceleration address cdn.oss.com/a.js. When I visited this address in Pingdingshan, I requested a CDN server in Zhengzhou, the nearest to me, and found that this server has this A.JS. There is no expiration (cache hit), so it can be returned directly. If the a.js expires or does not exist, an optimal route will be planned to find the next CDN server with a.js or directly back to the source station to take and save, and the resources can be directly returned to the next access.

CDN services ali, Tencent, Huawei big factories are doing, there are some such as network, and paiyun, seven cattle, etc., overseas CDN acceleration is said to be Akamai (Akamai) quite good. In order to take care of their face, we will not name them. Welcome to comment and recommend more excellent CDN service providers.

Resource reference path

Generally, there are two kinds of reference paths of resources in web pages, relative path and absolute path.

Relative paths import files like this:

 <! -- html -->
 <! -- Relative paths start with./ or direct path names -->
 <link href="./style.css" rel="stylesheet" /> 
Copy the code

Relative path Relative to the path of the current page, assuming that the HTML access address is www.demo.com, the path is the root path /, and the imported CSS network request address is www.demo.com/style.css. If this HTML to visit www.demo.com/login, path is/login, CSS network request address as www.demo.com/login/style.css. The last network request for a relative path to a resource can be seen as location.host + location. pathName + file path.

The absolute path introduces files like this:

 <! -- html -->
 <! -- the absolute path starts with a /, you may have seen //www.demo.com/a.js, which indicates that the import resource protocol is consistent with the current page -->
 <link href="/style.css" rel="stylesheet" /> 
Copy the code

Absolute paths introduce resources that are independent of the current page path, starting at the root, regardless of whether your HTML is accessed at www.demo.com or www.demo.com/login. The network requests for CSS style files introduced by the absolute path above are all www.demo.com/style.css. Relative path The last network request to import a resource can be seen as location.host + file path

It is generally not recommended to use relative path to introduce resources in web pages, especially nowadays, many SPA applications control the route to change the path, and relative path may cause a lot of confusion and trouble. The relative path can be used in normal project development, and then use a packaging tool such as Webpack to configure the common path and replace the relative path when packaging. The default public path for webpack is /, and all resource paths in the final packaged file start with an absolute path starting with /.

Public path

In webpack, publicPath is used to configure the publicPath. The publicPath is the basic path of the packaged resources of the project. It can also be understood as a prefix, usually a /, indicating the absolute path of the current access address. But sometimes, we will be js, CSS, images and other static resources stored on another server, then you can set the public path to the corresponding domain name. For example we set publicPath for https://oss.demo.com, so like the style. The CSS reference path would be https://oss.demo.com/style.css complete network address.

Now think about why it’s so easy to switch between HTML files and imported static resources. Obviously, using absolute paths together, static resources follow the access address. If you access HTML with cdn.demo.com, all resource requests in HTML will start with CDn.demo.com. If you switch to oss.demo.com, that’s oss.demo.com. But when our static resources and HTML on a different server, introducing path has written dead prefix address, like https://oss.demo.com/style.css, you no matter from any address access to HTML, The CSS request will always be https://oss.demo.com/style.css.

For projects like create-react-app, you can modify the public path by creating or modifying the homepage field in package.json before the Webpack configuration file is ejected using NPM run eject.

Solution ideas

1.APP proxy request

At that time, THE first solution I came up with was APP proxy request, because our page was embedded in APP, and APP could intercept all web requests of the page. After switching CDN, APP would replace all intercepted web requests with switched CDN addresses, as shown in the schematic diagram below.

Before the switch:

The advantage of this scheme is that there is no need to modify the front-end web page. If other CDN is added in the future, the APP side only needs to configure several proxy addresses, and the client side decides to do it after evaluating the feasibility. However, they proposed to intercept the network request of processing web pages, which may affect the performance of APP. I hope our front-end can realize this switch by itself in the future.

Before I put forward this solution, I checked it specially. The difficulty of implementing it on Android, Apple and Windows is different, and the interception of POST request in WKWebView of ios will lead to the loss of body. There are solutions but some troubles. It is suggested that API call address switching front-end own control is simple, APP is only responsible for proxy interception of resource loading.

2. Multiple front-end builds

So we need to think of another plan, that is, the front end itself to switch, now let’s take a look at our project:

  • React single-page application, packaged with Webpack.
  • The online address is https://www.demo.com
  • The static resource server address for the web page is https://oss.demo.com
  • The CDN acceleration address of the static resource server is https://cdn.demo.com
  • The common path for all static resources in the web page was set to https://cdn.demo.com at build time

A simple and crude way is for me to build twice, once with a public path set to https://oss.demo.com and once with a public path set to https://cdn.demo.com, and place it under a different path, https://oss.demo.com. Two HTML files are placed at https://www.demo.com, by listening on different ports or by distinguishing between arguments that respond to different HTML.

The advantage of this method is that it is simple and there is no need to deal with the project. The disadvantage is that the whole project needs two sets, and one more set will be required to add a CDN line in the future, and the cooperation of the middle layer is required. Therefore, I have ruled out this scheme.

3. Dynamically switch public paths

Since we do not want to build many times, what we need to achieve is that there is only one HTML, and the public path in the project can be dynamically switched by some means. The change of the public path means the change of the request to access resources, so as to achieve the purpose of CDN switching.

First, dynamic. The simplest way to do this is to pass parameters through a URL, where different parameters correspond to different addresses. For example, if we set a CDN parameter, if the access address is https://www.demo.com?cdn=1, we use https://cdn.demo.com as the public path, If https://www.demo.com?cdn=0 is used, https://oss.demo.com is used as the public path.

What is more difficult is the switch of the public path. It has been said above that the public path has been written into the project through the configuration when Webpack is built and packaged, that is, the public path has been determined at the time of packaging. Looking at the packed JS code, you will find such code.

// __webpack_public_path__
__webpack_require__.p = "https://oss.demo.com";

(function(module, exports, __webpack_require__) {
    eval("module.exports = __webpack_require__.p + \"a.f58ad020.jpg\"; \n\n//# sourceURL=webpack:///./a.jpg?");
 }),
Copy the code

We can see that the public path is assigned to __webpack_require__.p. If we want to switch the public path dynamically, it means that we need to change __webpack_require__. Webpack provides a unique variable __webpack_public_path__.

Webpack-specific variables, that is, webpack packages our code with a layer of functions, some of which are passed in as arguments so that we can use them in our code, even if they are not available in the host environment (such as the browser) (require, import, export, etc.).

To change the public path, we simply assign __webpack_public_path__ to the top of the entry file, as follows:

// publicConfig.js
__webpack_public_path__ = 'https://cdn.demo.com';

// import file index.js
import './publicConfig.js'   
import React from 'react';
import ReactDOM from 'react-dom';
Copy the code

When this code is packaged, it will have something like this.

(function(module, exports, __webpack_require__) {
    eval("__webpack_require__.p = \"https://cdn.demo.com\"; \r\n\n\n//# sourceURL=webpack:///./src/publicConfig.js?");
}),
Copy the code

No doubt __webpack_require__.p is reassigned, and although __webpack_require__ is passed in as an argument, the p on the source object is changed because of the reference type.

This appears to be an easy fix, but the change only applies to public paths in JS files. It does not apply to CSS and JS file addresses in HTML, nor to images introduced through urls () in CSS style files.

One thing to note is that if you don’t separate the CSS style file as we did in our project, in the form of CSS in JS, dynamic changes to the CSS style file in the public path will work.

If a project is created by create-react-app, changing __webpack_public_path__ may not take effect. You need to delete the publicPath configuration item to resolve the problem. However, the cause has not been thoroughly analyzed.

Final plan

It may be difficult to solve all of our problems in one step just by changing the WebPack configuration, and we may have to do different things for different files.

  1. Js and CSS: two sets, each set of files in the public path is different.
  2. HTML: An HTML file, but because there are two sets of JS and CSS, you need to be able to dynamically load different JS and CSS files in THE HTML.
  3. Pictures, audio and video resources: due to the use of the same resources, a set.

Because webPack packaging can be time consuming (depending on the size of the project), you want to complete all the steps in one package.

Plan implementation

No need to deal with number three, just pack normally, starting with number two.

I’m sure this can be done by modifying the WebPack configuration or a Webpack plugin, but I’ve decided to use the simplest and most unwieldy-looking method, text substitution.

We first package a copy with https://oss.demo.com as the public path, then copy the JS and CSS folders, and conduct text search and replace for all files in the copied two folders. Replace https://oss.demo.com with https://cdn.demo.com.

Of course, we cannot do these things manually. Write a Node script. It is recommended to configure the CDN information into a special JSON file for easy management and later addition and deletion.

{
  "cdnList": ["Cdn"."Cdn2"]."cdnUrl": {
    "Default": "oss.demo.com"."Cdn": "cdn.demo.com"."Cdn2": "cdn2.demo.com",}}Copy the code
// scripts/replace.js
const path = require("path");
const fs = require("fs-extra"); //fs enhanced version with copy folder
const replace = require("replace-in-file"); // Replace the text in the file
const { cdnList, cdnUrl } = require(".. /project.json");

Create a replacement task oss.demo.com -> cdn.demo.com
const createReplaceOptions = (dir, cdn) = > {
  return {
    files: `${path.resolve(__dirname, `.. /build/static/${dir}${cdn}/ `)}/ *. * `.from: new RegExp(`${cdnUrl.Default}`."g"),
    to: cdnUrl[cdn]
  };
};

// Create a copy task
const createCopy = (dir, cdn) = > {
  return fs
    // Copy the folder
    .copy(
      path.resolve(__dirname, `.. /build/static/${dir}`),
      path.resolve(__dirname, `.. /build/static/${dir}${cdn}`))// Replace all files in the copied folder with text
    .then((a)= > {
      const options = createReplaceOptions(dir, cdn);
      return replace(options)
        .then(results= > {
          console.log("Result of substitution:", results); })}); };// Create a copy and replace task based on the CDN list
cdnList.forEach(item= > {
  const jsCopy = createCopy("js", item);
  const cssCopy = createCopy("css", item);
  Promise.all([jsCopy, cssCopy])
    .then((a)= > console.log("处理完成!"))
    .catch(err= > console.error("Processing failure:",err));
});

Copy the code

In package.json, modify the build command to perform a copy and replace operation after the Webpack is done.

{
  "scripts": {
    "build_cdn": "node scripts/build.js && node scripts/replace.js",}}Copy the code

Tips: Use && to connect two commands. The previous command is executed before the next command is executed, and the previous command is executed concurrently. Therefore, pay attention to the command execution sequence.

Let’s take a look at the effect:

This step solves the common path problem that URL () in CSS introduces resources and JS directly introduces resources through network addresses. Next, it dynamically introduces corresponding CSS and JS files. The method is to create link tags and script tags according to conditions and insert them into HTML.

// cdn.js
var query = parseQueryString(window.location.href); // Format url parameters without writing detailed code here
var cdn = query.cdn; 
var cdnList = {
  Default: "https://oss.demo.com/".Cdn: "https://cdn.demo.com/".Cdn2: "https://cdn2.demo.com/"
};
// Store the judged public path on the window.
if (cdnList[cdn]) {
  window.publicPath = cdnList[cdn];
} else {
  cdn = "";
  window.publicPath = cdnList.Default;
}
// Load CSS and JS dynamically
function asyncAppendNode(tagName, fileName) {
  // CSS, js file address
  function createUrl(type) {
    return window.publicPath + "static/" + type + cdn + "/" + fileName;
  }
  var node = document.createElement(tagName);
  if (tagName === "link") {
    node.type = "text/css";
    node.rel = "stylesheet";
    node.href = createUrl("css");
    document.head.appendChild(node);
  } else {
    node.src = createUrl("js");
    document.body.appendChild(node); }}Copy the code

We introduce the CDn.js file in the HTML head (or write it directly in HTML) to ensure that it takes precedence after the page loads. However, this introduction will result in the file not being packaged with WebPack and compiled without Babel, so it is recommended not to use too new JS features in order to be compatible with more browsers.

First, webpack will process CSS and JS and generate a packed file with hash values in the file name (for version control). The packaged files are then introduced into HTML through the htML-webpack-plugin plugin.

For example, if you have two files, A.css and B.js, packaged and inserted in HTML, this will look like this.

<head>
  <! -- This file is added directly in HTML so webpack is not packaged -->
  <script src="/cdn.js"></script>
  <link href="https://oss.demo.com/a.388e587e.css" rel="stylesheet">
</head>
<body>
  <script src="https://oss.demo.com/b.6b602746.js"></script>
</body>
Copy the code

We want the generated HTML to look like this.

<head>
  <script src="/cdn.js"></script>
  <script>
      asyncAppendNode("link"."a.388e587e.css");
  </script>
</head>
<body>
   <script>
      asyncAppendNode("script"."b.6b602746.js");
  </script>
</body>
Copy the code

Since we have already written the method asyncAppendNode for dynamic loading in cdn.js, we can call it directly here and pass in the necessary parameters. I can modify it manually after packaging, but it is better to package it directly in the form we want.

Now there is only one last problem, how to change from the original CSS, JS file introduction method to function call, no need to use htML-webpack-plugin to do the work, but only through configuration is not able to meet our needs, fortunately htML-webpack-plugin provides us with plug-in extensions, We can write our own custom plugins for htML-webpack-plugin to implement the desired functionality.

const HtmlWebpackPlugin = require('html-webpack-plugin');

class DynamicLoadHtmlWebpackPlugin {
    constructor(options = {}) {
        CallbackName is the name of the dynamically loaded function
        // cdnVariableName is the name of the variable stored in the publicPath we discussed above. In cdn.js, it is stored in window.publicpath.
        const { callbackName = 'callback', cdnVariableName } = options;
        this.callbackName = callbackName;
        this.cdnVariableName = cdnVariableName;
    }
    // Rewrite the generated data of the HTml-webpack-plugin
    rewriteData(node, data, fnName, publicPath) {
        // Insert the CSS reference instead of the script of the function call.
        if (node === 'script') {
            const fileNames = data.map((item) = >
                item.attributes.href.split('/').pop(),
            );
            const styleHtml = fileNames
                .map((item) = > `${fnName}('${node}', '${item}'); `)
                .join(' ');
            return[{tagName: 'script'.voidTag: false.innerHTML: styleHtml },
            ];
        } else {
            // There are two types of js inserts. One is a js file reference. We insert the script of the function call instead. The other type is inline script code, which we don't have to change to insert function calls. But for the project created by create-react-app, the assignment of the environment variable __webpack_require__.p = XXX is written here, so we'll deal with it and replace the public path with the variable name we passed in.
            const inlineScript = [];
            const srcScript = [];
            data.forEach((item) = > {
                if (item.innerHTML) {
                    if (
                        typeof publicPath === 'string' &&
                        this.cdnVariableName
                    ) {
                        const html = item.innerHTML;
                        const newHtml = html.replace(
                            ` ="${publicPath}"`.` =The ${this.cdnVariableName}`,); item.innerHTML = newHtml; } inlineScript.push(item); }else {
                    srcScript.push(item.attributes.src.split('/').pop()); }});const scriptHtml = srcScript
                .map((item) = > `${fnName}('${node}', '${item}'); `)
                .join(' ');
            return [
                ...inlineScript,
                { tagName: 'script'.closeTag: true.innerHTML: scriptHtml }, ]; }}HtmlWebpackPlugin in the packaging process, different life cycle callback, detailed can refer to the official documentation, different life cycle, data content is different.
    apply(compiler) {
        compiler.hooks.compilation.tap(
            'DynamicLoadHtmlWebpackPlugin',
            (compilation) => {
                HtmlWebpackPlugin.getHooks(
                    compilation,
                ).beforeAssetTagGeneration.tapAsync(
                    (data, cb) = > {
                        // Get the publicPath of the WebPack configuration in this lifecycle, save it.
                        this.publicPath = data.assets.publicPath;
                        cb(null, data); }); HtmlWebpackPlugin.getHooks( compilation, ).afterTemplateExecution.tapAsync((data, cb) = > {
                        // In this life cycle, the names of the js and CSS files are confirmed, and the information about the labels to be inserted is placed in an array object, which is easy to handle, so we override it.
                        const newStyleData = this.rewriteData(
                            'link',
                            data.headTags,
                            this.callbackName,
                        );
                        data.headTags = newStyleData;
                        const newScriptData = this.rewriteData(
                            'script',
                            data.bodyTags,
                            this.callbackName,
                            this.publicPath,
                        );
                        data.bodyTags = newScriptData;
                        cb(null, data); }); }); }}module.exports = DynamicLoadHtmlWebpackPlugin;

Copy the code

After the plugin is written, it can be used in webpack.config.js.

const HtmlWebpackPlugin = require("html-webpack-plugin");
const DynamicLoadHtmlWebpackPlugin = require("./dynamicLoadHtmlWebpackPlugin");
module.exports = {
  ...
  plugins:[
    new HtmlWebpackPlugin(),
    new DynamicLoadHtmlWebpackPlugin({
          callbackName: "asyncAppendNode".cdnVariableName: "window.publicPath"}),]... }Copy the code

Then take a look at the packaged HTML.

Results show

So far, the function of dynamically switching the public path of the project has been developed. We can control the network prefix of resources in the whole project by changing URL parameters. Finally, let’s simulate the actual situation, the website address is localhost:3000. The CDN addresses are localhost:3001 and localhost:3002 respectively.

The demo address

conclusion

In fact, later I had a thought, in the “slash-and-burn” front-end development period, we were in complete control of the whole project, like the above thing is simple. Later, front-end projects are gradually engineered and automated, which brings us convenience, while some special and individual needs often take more time and thought to deal with. Just like the original handmade things, if you want to change some things, you can change them. But now they are all produced with machine molds. If you want to change things, you have to start from molds and machines, which may be more troublesome. This reminds me of the villain’s point of view in the Unabomber, an American TV series I watched some time ago. If you are interested, you can watch that American TV series.

Of course, this is just a free time and everyone blowing water, I am not against the front-end engineering and automation, on the contrary, I enjoy the benefits it brings.

Finally again, this is not a complete tutorial and recommend you to do this, but only put forward a train of thought, which use some of the solution you can reference, but does not guarantee that combination is the optimal solution, the purpose of this article in this series even more hope to cause people thinking and discussion. Paper come zhongjue shallow, must know this to practice, work practice is the best way to test technology, I hope this article can bring you help.