文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

在 Web 应用的运行时实现多分支并存和切换

2024-12-01 17:23

关注

第一个方案是提供应用程序 SDK,由客户的开发团队完成整个定制应用的开发和部署,SaaS 服务商提供必要的技术支持即可。此方案要求客户的开发团队具备较强的 IT 专业能力。

第二个方案则是由 SaaS 服务商的开发团队在 SaaS 应用的基础上进行二次开发,并部署。此方案主要面向 IT 专业能力较弱,或者仅需在 SaaS 应用的基础上进行少量定制的客户。然而,要支持这种定制方式,相当于要求 SaaS 服务商在 同一个应用中,针对不同的客户运行不同分支的代码 。要达到这个目的,应用程序的架构也要进行相应的改造。本文主要讲述改造的方案及其代码实现。

方案概览

对于前后端分离的项目来说,经过构建,最终会生成 html、js、css 三种代码文件。以基于 Vue.js 框架的项目为例,其构建出来的 index.html,内容与下面的代码相似:

<!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>

实际上,index.html 只是访问入口,主要作用就是加载 css 和 js 资源。换句话说: 任何的 html 页面,只要加载了上述 css 和 js 资源,都可以运行这个应用 。

既然如此,只要 做一个应用入口页,并根据客户配置加载相应代码分支构建出来的 css 和 js 资源即可 。整体流程如下图所示:

构建方案

入口页要加载对应分支的 css 和 js 资源,首先需要一个资源列表。我们可以在构建流程增加一个步骤,把 js 和 css 的引用提取到一个 资源目录文件(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'
);

执行脚本后,就会生成资源目录文件,其内容为:

{
"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"
}
]
}

在提取资源的过程中,移除了通过 link 标签 preload 的资源,并把 prefetch 的资源放到了资源列表的末尾。具体原因会在后文说明。

此外,因为多个分支构建出来的代码都要上传到 OSS,为了避免放在同一个目录下互相覆盖,就得再加一层分支目录。

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

所以,代码分支对应的资源目录文件路径就是:

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

加载方案

加载流程如上图所示,接下来针对每一步详述。

1.请求代码分支名

进入页面后,携带客户信息(客户标识、内容标识等)请求后端接口,该接口会返回代码分支名。实现如下:

// 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);
}

除了实现基本的流程,以上代码还做了降级处理—— 如果后端接口超时或响应异常,就加载主分支,避免页面白屏 。

2.加载资源目录

加载指定分支名的资源目录。实现如下:

// 用于避免重复加载
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);
}

同样地,以上代码也做了降级处理—— 如果特定分支名的资源目录文件加载失败,就会加载主分支的资源目录文件,避免页面白屏 。

 3.加载资源

遍历资源列表,把 css 和 js 都加载到页面上。代码实现如下:

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);
});
}

需要注意的是,对于动态创建的 script 节点来说,它的 async 属性默认为 true。也就是说,这些 script 会被并行请求,并尽快解析和执行, 执行顺序是未知的 。然而,资源目录中的 js 是有依赖关系的,后面的 js 依赖于前面的 js。因此,必须把 script 节点的 async 设为 false,让其按顺序解析和执行。

脚本顺利执行后,应用就会初始化。

4.入口页

为了让读者更好地理解整个过程,上述加载分支资源的代码是用 ES6 编写的,并且会用到如 fetch、Promise、async、await 等特性。从兼容性的角度考虑,这段代码需要经过 Babel 的转译,转译的过程中会插入一些额外的代码。然而,这段代码会阻塞后续的流程,应尽可能轻量化。因此,实际开发的时候是采用 ES5 编写,fetch 也替换为 XMLHttpRequest。此外,由于代码量比较少,还可以通过 Webpack 的 inline-source-webpack-plugin,把构建后的 js 代码以行内脚本的形式输出到页面上,减少一个 js 文件请求。

<!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>

其他注意点

资源目录文件的过期时间

由于资源目录文件的路径是固定的,所以该文件要禁用 HTTP 的强缓存,或者仅配置短时间的强缓存。

否则,一旦用户使用的浏览器长时间缓存了该文件,那么在缓存期间,不管更新了多少个版本,用户访问的仍然是缓存下来的那个版本。

小小的加速

定制客户毕竟是少数,大部分客户用的仍然是标准的 SaaS 应用。也就是说,大部分情况下加载的是主分支的资源目录文件。因此,可以在入口页提前加载这个资源:

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

关于预加载

link 标签支持两种方式的预加载:

preload 是提前加载,但是不阻塞 onload,主要用于预加载当前页面会用到的资源;

prefetch 是闲时加载,主要用于加载将来可能会用到的资源。

以前文的 index.html 为例,app.2dd9bc59.css、vendors~app.f1dba939.js、app.f7eb55ca.js 这三个资源都在页面中通过 link 或 script 标签引用,所以会通过 preload 去提前加载。而其他资源则是将来可能会用到的资源(比如在某个时机才会动态 import 的资源),所以是通过 prefetch 闲时加载。

然而,在前文讲到提取页面 css 和 js 资源的时候,我们把 preload 的资源移除了,并且把 prefetch 的资源移到了末尾。为什么要这么做呢?我们从入口页加载流程去分析这个问题。

如上图所示:

总结

总地来说,本文所述的方案有以下优势:

然而,也具备一定的局限性:

来源:segmentfault.co内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯