1

background

Generally speaking, SaaS service providers provide standardized products and services, reflecting the common needs of all customers. However, some customers (especially large customers) will put forward customization requirements in terms of functions, UI, etc. For these customization needs, there are generally two solutions.

The first solution is to provide an application SDK, and the customer's development team can complete the development and deployment of the entire customized application, and the SaaS service provider can provide the necessary technical support. This scenario requires the client's development team to have strong IT expertise.

The second solution is to carry out secondary development and deployment by the development team of the SaaS service provider on the basis of the SaaS application. This solution is mainly for customers with weak IT professional ability, or who only need a small amount of customization on the basis of SaaS applications. However, to support this customization method is equivalent to requiring the SaaS service provider to run different branches of code for different customers in the same application . To achieve this goal, the architecture of the application should also be transformed accordingly. This article mainly describes the transformation scheme and its code implementation.

Program overview

For projects with separated front and back ends, after construction, three code files, html, js, and css, will eventually be generated. Taking a project based on the Vue.js framework as an example, the content of index.html built by it is similar to the following code:

 <!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <link href="https://cdn.my-app.net/sample/assets/css/chunk-0c7134a2.11fa7980.css" rel="prefetch">
  <link href="https://cdn.my-app.net/sample/assets/js/chunk-0c7134a2.02a43289.js" rel="prefetch">
  <link href="https://cdn.my-app.net/sample/assets/css/app.2dd9bc59.css" rel="preload" as="style">
  <link href="https://cdn.my-app.net/sample/assets/js/vendors~app.f1dba939.js" rel="preload" as="script">
  <link href="https://cdn.my-app.net/sample/assets/js/app.f7eb55ca.js" rel="preload" as="script">
  <link href="https://cdn.my-app.net/sample/assets/css/app.2dd9bc59.css" rel="stylesheet">
 </head>
 <body>
   <div id="app"></div>
   <script src="https://cdn.my-app.net/sample/assets/js/vendors~app.f1dba939.js"></script>
   <script src="https://cdn.my-app.net/sample/assets/js/app.f7eb55ca.js"></script>
 </body>
 </html>

In fact, index.html is just an access entry, and its main function is to load css and js resources. In other words: any html page that loads the above css and js resources can run this application .

In this case, just make an application entry page and load the css and js resources constructed by the corresponding code branch according to the customer's configuration . The overall process is shown in the following figure:

整体流程

Build scheme

To load the css and js resources of the corresponding branch on the entry page, you first need a resource list. We can add a step to the build process to extract the js and css references into an assets directory file (index-assets.json) :

 const fs = require('fs');
const content = fs.readFileSync('./dist/index.html', 'utf-8');

// 匹配 html 中的 js 或 css 引用标签
const assetTags = content.match(/<(?:link|script).*?>/gi) || [];

let result = [];
assetTags.forEach((assetTag) => {
  const asset = {
    tagName: '',
    attrs: {}
  };

  // 解析标签名
  if (/<(\w+)/.test(assetTag)) { asset.tagName = RegExp.$1; }

  // 解析属性
  const reAttrs = /\s(\w+)=["']?([^\s<>'"]+)/gi;
  let attr;
  while ((attr = reAttrs.exec(assetTag)) !== null) {
    asset.attrs[attr[1]] = attr[2];
  }

  result.push(asset);
});

// 移除 preload 的资源,并把 prefetch 的资源放到 result 的最后面
const prefetches = [];
for (let i = 0, item; i < result.length;) {
  item = result[i];
  if (item.tagName === 'link') {
    if (item.attrs.rel === 'preload') {
      result.splice(i, 1);
      continue;
    } else if (item.attrs.rel === 'prefetch') {
      prefetches.push(result.splice(i, 1)[0]);
      continue;
    }
  }
  i++;
}
result = result.concat(prefetches);

fs.writeFileSync(
  './dist/index-assets.json',
  JSON.stringify({ list: result }),
  'utf-8'
);

After executing the script, a resource directory file will be generated with the following contents:

 {
  "list": [
    {
      "attrs": {
        "href": "https://cdn.my-app.net/sample/assets/css/app.2dd9bc59.css",
        "rel": "stylesheet"
      },
      "tagName": "link"
    },
    {
      "attrs": {
        "src": "https://cdn.my-app.net/sample/assets/js/vendors~app.f1dba939.js"
      },
      "tagName": "script"
    },
    {
      "attrs": {
        "src": "https://cdn.my-app.net/sample/assets/js/app.f7eb55ca.js"
      },
      "tagName": "script"
    },
    {
      "attrs": {
        "href": "https://cdn.my-app.net/sample/assets/css/chunk-0c7134a2.11fa7980.css",
        "rel": "prefetch"
      },
      "tagName": "link"
    },
    {
      "attrs": {
        "href": "https://cdn.my-app.net/sample/assets/js/chunk-0c7134a2.02a43289.js",
        "rel": "prefetch"
      },
      "tagName": "link"
    }
  ]
}

In the process of fetching resources, the resources preloaded by the link tag are removed, and the prefetched resources are placed at the end of the resource list. The specific reasons will be explained later.

In addition, because the code built by multiple branches must be uploaded to OSS, in order to avoid overlapping each other in the same directory, a branch directory must be added.

 https://cdn.my-app.net/sample/${branch}/

Therefore, the resource directory file path corresponding to the code branch is:

 https://cdn.my-app.net/sample/${branch}/index-assets.json

Load scheme

资源加载时序图

The loading process is shown in the figure above, and each step is described in detail below.

1. Request code branch name

After entering the page, carry the customer information (customer ID, content ID, etc.) to request the back-end interface, which will return the code branch name. The implementation is as follows:

 // id 为客户信息
function getBranch(id) {
  // 如果请求后端接口超时(10s),就加载主分支
  const TIME_OUT = 10000;
  setTimeout(() => {
    loadAssetIndex('main');
  }, TIME_OUT);

  let branch;
  try {
    const response = await fetch(`/api/branch?id=${id}`);
    branch = (await response.json()).branch;
  } catch (e) {
    // 如果后端接口异常,就加载主分支
    branch = 'main';
  }

  // 加载资源目录
  loadIndexAssets(branch);
}

In addition to implementing the basic process, the above code also performs downgrade processing - if the backend interface times out or responds abnormally, load the main branch to avoid a white screen on the page .

2. Load the resource directory

Load the resource directory of the specified branch name. The implementation is as follows:

 // 用于避免重复加载
let status = 0;

function loadIndexAssets(branch) {
  if (status) { return; }
  status = 1;

  let list;
  try {
    const response = await fetch(`https://cdn.my-app.net/sample/${branch}/index-assets.json`);
    list = (await response.json()).list;
  } catch (e) {
    if (branch !== 'main') {
      status = 0;
      loadAssetIndex('main');
    }
    return;
  }
  status = 2;
  loadFiles(list);
}

Similarly, the above code has also been downgraded - if the resource directory file of a specific branch name fails to load, the resource directory file of the main branch will be loaded to avoid a white screen on the page .

3. Load resources

Traverse the resource list and load both css and js onto the page. The code is implemented as follows:

 function loadFiles(list) {
  list.forEach(function(item) {
    const elt = doc.createElement(item.tagName); 
    // 脚本有依赖关系,要按顺序加载
    if (item.tagName === 'script') { elt.async = false; }

    for (const name in item.attrs) {
      elt.setAttribute(name, item.attrs[name]);
    }
    doc.head.appendChild(elt);
  });
}

It should be noted that for dynamically created script nodes, its async attribute defaults to true. That is, these scripts will be requested in parallel, parsed and executed as quickly as possible, and the order of execution is unknown . However, the js in the resource directory has dependencies, and the latter js depends on the former js. Therefore, the script node's async must be set to false to allow it to parse and execute sequentially.

Once the script has successfully executed, the application will initialize.

4. Entry page

In order to allow readers to better understand the whole process, the above code for loading branch resources is written in ES6, and features such as fetch, Promise, async, await and so on will be used. From the perspective of compatibility, this code needs to be translated by Babel, and some extra code will be inserted during the translation process. However, this code blocks subsequent processes and should be as lightweight as possible. Therefore, the actual development is written in ES5, and fetch is also replaced by XMLHttpRequest. In addition, due to the relatively small amount of code, you can also use Webpack's inline-source-webpack-plugin to output the constructed js code to the page in the form of an inline script, reducing one js file request.

 <!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <div id="app"></div>
  <script inline inline-asset="main\.\w+\.js$" inline-asset-delete></script>
</body>
</html>

Other notes

Expiration time of resource catalog files

Since the path of the resource directory file is fixed, this file should disable HTTP strong caching, or configure strong caching for a short time only.

Otherwise, once the browser used by the user caches the file for a long time, during the caching period, no matter how many versions are updated, the user still accesses the cached version.

small acceleration

After all, custom customers are a minority, and most customers still use standard SaaS applications. That is to say, in most cases, the resource directory file of the master branch is loaded. Therefore, this resource can be loaded in advance on the entry page:

 <link href="https://cdn.my-app.net/sample/main/index-assets.json" rel="preload" as="fetch" />

About preloading

The link tag supports preloading in two ways:

  • preload is to load in advance, but does not block onload, mainly used to preload the resources that the current page will use;
  • prefetch is idle loading, mainly used to load resources that may be used in the future.

Taking index.html as an example, the three resources app.2dd9bc59.css, vendors~app.f1dba939.js, app.f7eb55ca.js are all referenced in the page through link or script tags, so they will be loaded in advance through preload . Other resources are resources that may be used in the future (such as resources that will be dynamically imported at a certain time), so they are loaded at idle time through prefetch.

However, when we talked about extracting the css and js resources of the page, we removed the preload resources and moved the prefetch resources to the end. Why do you do that? We analyze this problem from the entry page loading process.

入口页流程图

As shown in FIG:

  • After the loading logic is executed, the page onload has been triggered, and the time for early loading has long passed, so preload is meaningless.
  • After loading the resource directory file, before loading the css and js resources, the page has no other loading tasks and is already in an idle state. If the prefetch link element is inserted into the page at this time, the browser will load this part of the resource immediately. Therefore, in the resource list, the prefetched resources should be placed later, so that those resources required for application initialization can be loaded first.

Summarize

Overall, the scheme described in this paper has the following advantages:

  • Lightweight, no need to rely on third-party libraries or frameworks.
  • There is no need to change the logic of the application, but a layer of entry page is added before entering the application, which is less intrusive.
  • Wide adaptability, no need to change the technology stack of the application.

However, there are certain limitations:

  • It is only suitable for applications with separated front and back ends, and no function can be carried in the html file.
  • The three steps of the entry page - executing the loading logic, loading the resource directory file, and loading the resource are executed serially, and the white screen time of the page will increase. Under conditions, the first two steps can be put into the backend to execute.
  • The multi-branch management scheme of the backend is not considered.

This article was also published on the author's personal blog: https://mrluo.life/article/detail/148/multi-branch-in-runtime


MrLuo
39 声望2 粉丝