噼里啪啦免费视频看
一般来说(shuo),SaaS 服务商提供(gong)的是标准化(hua)的产品服务(wu),体现的是所(suo)有客户的共性需求(qiu)。然而,部分客(ke)户(尤其是大(da)客户),会提出(chu)功能、UI 等方面的定制需(xu)求。针(zhen)对这些定制(zhi)需求,大体上有两个解(jie)决方案。
第一(yi)个方案是提(ti)供应用程序(xu) SDK,由客户的开(kai)发团队完成(cheng)整个定制应(ying)用的开发和(he)部署,SaaS 服(fu)务商提供必(bi)要的技术支(zhi)持即可。此方(fang)案要求客户(hu)的开发团队(dui)具备较强的(de) IT 专业能力。
第(di)二个方(fang)案则是由 SaaS 服(fu)务商的开发团(tuan)队在 SaaS 应用的(de)基础上进行(xing)二次开发,并(bing)部署。此方案(an)主要面向 IT 专(zhuan)业能力(li)较弱,或者仅(jin)需在 SaaS 应用的(de)基础上进行(xing)少量定制的(de)客户。然而,要支(zhi)持这种定(ding)制方式,相当(dang)于要求(qiu) SaaS 服务商在同(tong)一个应用中(zhong),针对不同的(de)客(ke)户运行不同(tong)分支的代码(ma)。要达到这个(ge)目的,应用程(cheng)序的架(jia)构也要进行(xing)相应的改造(zao)。本文主要讲(jiang)述改造的方(fang)案及其代码(ma)实现。
方案概(gai)览
对于前后(hou)端分离的项目来说(shuo),经过构建,最(zui)终会生成 html、js、css 三(san)种代码文件(jian)。以基于 Vue.js 框架的(de)项目为例,其(qi)构建出来的(de) index.html,内容与下面的代码(ma)相似:
<!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>
实(shi)际上,index.html 只是访问入(ru)口,主要作用(yong)就是加载 css 和(he) js 资源。换句话(hua)说:任何的 html 页(ye)面,只要加载(zai)了上述(shu) css 和 js 资源,都可(ke)以运行这个(ge)应用。
既然如(ru)此,只要做一个应用(yong)入口页,并根据客户配(pei)置加载相应(ying)代码分(fen)支构建出来(lai)的 css 和 js 资源即(ji)可。整体流程(cheng)如下图所示(shi):
构(gou)建方案
入口(kou)页要加载对(dui)应分支(zhi)的 css 和 js 资源,首(shou)先需要一个(ge)资源列表。我(wo)们可以在构(gou)建流程增加(jia)一个步骤,把(ba) js 和 css 的引用提(ti)取(qu)到一个(ge)资源目录文(wen)件(index-assets.json)中:
const fs = require('fs');
const content = fs.readFileSync('./dist/index.html', 'utf-8');
// 匹配 html 中(zhong)的 js 或 css 引用标(biao)签
const assetTags = content.match(/<(?:link|script).*?>/gi) || [];
let result = [];
assetTags.forEach((assetTag) => {
const asset = {
tagName: '',
attrs: {}
};
// 解析标签(qian)名
if (/<(\w+)/.test(assetTag)) { asset.tagName = RegExp.$1; }
// 解析属性(xing)
const reAttrs = /\s(\w+)=["']?([^\s<>'"]+)/gi;
let attr;
while ((attr = reAttrs.exec(assetTag)) !== null) {
asset.attrs[attr[1]] = attr[2];
}
result.push(asset);
});
// 移除 preload 的资源(yuan),并把 prefetch 的资源(yuan)放到 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'
);
执行脚(jiao)本(ben)后,就会(hui)生成资源目(mu)录文件,其内(nei)容为:
{
"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"
}
]
}
在提取(qu)资源的过程(cheng)中,移除了通(tong)过 link 标签 preload 的资源,并把(ba) prefetch 的(de)资源放到了(le)资源列表的(de)末尾。具体原(yuan)因会在后文(wen)说明。
此外,因(yin)为多个分支(zhi)构建出来的代码都(dou)要上传到 OSS,为(wei)了避免放在(zai)同一个目录(lu)下(xia)互(hu)相覆盖,就得再(zai)加一层(ceng)分支目录。
https://cdn.my-app.net/sample/${branch}/
所(suo)以,代码分支(zhi)对应的(de)资源目录文(wen)件路径就是:
https://cdn.my-app.net/sample/${branch}/index-assets.json
加(jia)载方案
加载流程如(ru)上图所示,接(jie)下来针对每(mei)一步详(xiang)述。
国产午夜亚洲精品午夜鲁丝片
进入(ru)页面后,携带(dai)客户信息(客(ke)户标识、内容标(biao)识(shi)等)请求(qiu)后端接口,该(gai)接口会(hui)返回代码分(fen)支名。实现如(ru)下:
// id 为客户信(xin)息
function getBranch(id) {
// 如果请求后(hou)端接口超时(shi)(10s),就加载主分(fen)支
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) {
// 如果后端(duan)接口异(yi)常,就加载主(zhu)分支
branch = 'main';
}
// 加载资(zi)源目录
loadIndexAssets(branch);
}
除了(le)实现基本的(de)流程,以上代码还(hai)做了(le)降级(ji)处理——如果后(hou)端接口超时或响应(ying)异常,就加载(zai)主分支,避免(mian)页面白屏。
2. 加(jia)载资源目录(lu)
加(jia)载指定分支(zhi)名的资源目(mu)录。实现如下:
// 用于避(bi)免重复加载(zai)
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);
}
同样地,以上(shang)代码也做了(le)降级处理——如果特定分支(zhi)名的资源目(mu)录文件加载(zai)失败,就(jiu)会加载主分(fen)支的资源目(mu)录文件,避免(mian)页面白屏。
3. 加(jia)载资源
遍历(li)资源列表,把 css 和(he) js 都加载到页(ye)面上。代(dai)码实现如下(xia):
function loadFiles(list) {
list.forEach(function(item) {
const elt = doc.createElement(item.tagName);
// 脚本有依赖(lai)关系,要(yao)按顺序加载
if (item.tagName === 'script') { elt.async = false; }
for (const name in item.attrs) {
elt.setAttribute(name, item.attrs[name]);
}
doc.head.appendChild(elt);
});
}
需要(yao)注意的是,对(dui)于动态创建(jian)的 script 节点来说(shuo),它的 async 属(shu)性默认为 true。也(ye)就是说,这些(xie) script 会被并行请(qing)求,并尽快解(jie)析和执行,执(zhi)行顺序是未(wei)知的。然而,资(zi)源目录(lu)中的 js 是有依赖关(guan)系的,后面的(de) js 依赖于前面(mian)的 js。因此,必须(xu)把 script 节点的 async 设(she)为 false,让其按顺(shun)序解析和执(zhi)行。
脚本(ben)顺利执行后(hou),应用就会初(chu)始化。
4. 入口页(ye)
为了让读者(zhe)更好地理解(jie)整个过程,上(shang)述加载分支(zhi)资源的代码是用 ES6 编(bian)写的,并且会用(yong)到如 fetch、Promise、async、await 等特性(xing)。从兼容性的(de)角度考虑,这(zhe)段代码需要(yao)经过 Babel 的转译(yi),转译的过程中会插(cha)入一些额外(wai)的代码。然而(er),这段代码会(hui)阻塞后续的(de)流程,应尽可(ke)能轻量化。因(yin)此,实际开发(fa)的时候(hou)是采用 ES5 编写(xie),fetch 也替换为 XMLHttpRequest。此(ci)外,由于代码量(liang)比较少,还可以通过 Webpack 的(de) inline-source-webpack-plugin,把构建后的 js 代码(ma)以行内脚本(ben)的形式(shi)输出到页面(mian)上,减少一个(ge) 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>
其(qi)他注意点
资(zi)源目录文件(jian)的过期时间(jian)
由于资源目(mu)录文件(jian)的路径是固(gu)定的,所以该(gai)文件要禁用(yong) HTTP 的强缓存,或者(zhe)仅配置短时(shi)间的强缓存(cun)。
否则,一旦用(yong)户使用(yong)的浏览器长(chang)时间缓存了(le)该文件,那么(me)在缓存期间(jian),不管更新了(le)多少个版本(ben),用户访问的(de)仍然是缓存下来的(de)那个版本。
小(xiao)小的加速
定(ding)制客户毕竟(jing)是少数,大部分客户(hu)用的(de)仍然(ran)是标准的 SaaS 应(ying)用。也就是说,大部分(fen)情况下加载(zai)的是主分支(zhi)的资源目录(lu)文件。因此,可(ke)以在入口页(ye)提前加载这(zhe)个资源:
<link href="https://cdn.my-app.net/sample/main/index-assets.json" rel="preload" as="fetch" />
关于预加载(zai)
link 标签支持两(liang)种方式的预(yu)加载:
- preload 是提前(qian)加载,但(dan)是不阻塞 onload,主要用(yong)于预加载当(dang)前页面会用到的资源(yuan);
- prefetch 是闲时加载(zai),主要用于加(jia)载将来可能(neng)会用到的资(zi)源。
以前文的(de) index.html 为例,app.2dd9bc59.css、vendors~app.f1dba939.js、app.f7eb55ca.js 这三个(ge)资源都在页(ye)面中通(tong)过 link 或 script 标签引(yin)用,所以会通过 preload 去提前加(jia)载。而其他资(zi)源则是将来(lai)可能会用到(dao)的资源(比如(ru)在某个(ge)时机才会动态(tai) import 的资源),所以(yi)是通过 prefetch 闲时(shi)加载。
然而,在(zai)前文讲到提(ti)取页面 css 和 js 资源的时候(hou),我们把 preload 的资(zi)源移除了,并(bing)且把 prefetch 的资源(yuan)移到了末尾(wei)。为什么要这(zhe)么做呢?我们(men)从入口页加(jia)载流程去分(fen)析这个问题。
如上图所示:
- 执行加(jia)载逻辑之后(hou),页面 onload 已经触(chu)发,提前加载(zai)的时机早已(yi)过去,所以 preload 已经没有(you)意义。
- 加载资(zi)源目录文件(jian)之后,加载 css、js 资源(yuan)之前,页面(mian)没有其他的(de)加载任务,已(yi)经处在空闲状态。如(ru)果此时把 prefetch 的(de) link 元素插入到(dao)页面中,浏览器(qi)马上就会加(jia)载这部分资(zi)源。因此,在资(zi)源列表中,prefetch 的(de)资源要(yao)往后放,让那些应用初(chu)始化所需的资源(yuan)可以被优先(xian)加载进来。
总(zong)结
总地来说(shuo),本文所述的(de)方案有(you)以下优势:
- 轻(qing)量化,无需依(yi)赖第三方库(ku)或框架。
- 无需(xu)改动应用的逻(luo)辑,而是在进(jin)入应用之前(qian)增加了(le)一层入口页(ye),侵入性低。
- 适(shi)配性广,无需(xu)变(bian)更(geng)应用的(de)技术栈。
然而(er),也具备一定(ding)的局限性:
- 只(zhi)适用于(yu)前后端分离(li)的应用,并且(qie) html 文件中不能(neng)承载任何功(gong)能。
- 入口页的(de)三个步骤——执行(xing)加载逻辑、加(jia)载资源(yuan)目录文件、加载资源(yuan),是串行执行的,页面(mian)的白屏时间(jian)会增加。有条(tiao)件的情况下(xia),可以把前两(liang)步放到后端去执行(xing)。
- 未考虑后端(duan)的多分支管(guan)理方案。
评论 (1条)