前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >webpack 项目 css/js主域重试

webpack 项目 css/js主域重试

作者头像
IMWeb前端团队
发布2019-12-04 09:58:52
1K0
发布2019-12-04 09:58:52
举报
文章被收录于专栏:IMWeb前端团队IMWeb前端团队

本文作者:IMWeb elvin 原文出处:IMWeb社区 未经同意,禁止转载

为了提高网站的访问速度,现在一般会将静态资源放在 CDN 下,而不是放在网站的域名之下。以腾讯课堂为例,其域名为 ke.qq.com,打开控制台,访问 ke.qq.com,我们可以看到 js 文件放在了 CDN 7.url.cn 下,css 文件放在了 CDN 8.url.cn 下。尽管 CDN 的服务可用性一般宣称 99.9% 甚至 99.999%,然而实际上监测结果比该数值要小一些。为了应对这种情况,需要做到当发现 css 或 js 文件从 CDN 加载失败时,能再次从网站的域名加载。

可以将“发现 css 或 js 文件从 CDN 加载失败时,能再次从网站的域名加载“”这个目标分解成四个问题来解决:

  1. 如何判断 css 文件加载失败?
  2. 如何从主域再次加载 css 文件?
  3. 如何判断 js 文件加载失败?
  4. 如何从主域再次加载 js 文件?

接下来将会就这四个问题,对使用 webpack 打包的项目进行具体的讨论。

不过在具体讨论之前,先补充一个知识点:webpack 打包生成所有文件后,会触发 'done' 事件。我们可以通过监听 'done' 事件,然后对 css 和 js 文件做包装,对 html 做 js 代码注入等。

css 主域重试

css 的作用就是改变元素的样式,从这一点出发,我们可以想出如下的方案:

  1. 首先向 css 文件添加一条规则;
  2. 接着向 html 文件中添加一个元素,最后通过 js 判断第一步中添加的规则是否起作用:
    • 若起作用,则说明 css 加载成功;
    • 若未起作用,则说明 css 加载失败,需要从主域重试。

这种方案可以形象地叫做“埋点”:向 html 文档中埋入一个检查点。

通过一个具体的例子来看看如何实现。首先,假设有如下 css 代码:

代码语言:javascript
复制
/* old css_example1.css */
.elvin {
  font-size: 16px;
}

然后,向其中添加一条规则:

代码语言:javascript
复制
/* new css_example1.css */
#css_example1 {
  display: none;
}

.elvin {
  font-size: 16px;
}

最后,向 html 文件中添加一个 id 为 css_example1 的 div,并通过 js 来做判断:

代码语言:javascript
复制
<div id="example1"></div>
<script>
  var div = document.getElementById('example1');
  if (getComputedStyle(div).display !== 'none') {
    // 说明 css 加载失败
    var newLink = document.createElement('link');
    newLink.rel = 'stylesheet';
    newLink.href = newUrl; // 主域下该 css 对应的地址
    document.head.appendChild(newLink);
  }
</script>

插入的 js 逻辑为:当判断出 css 文件加载失败后,创建一个新的 link 标签,然后将其地址指向相应的主域地址,最后将其添加到 html 的头部即可。

需要说明的是,上述向 css 添加规则和向 html 注入代码都是通过监听 webpack 的 'done' 事件进行的自动操作,并不需要手动的去插入这些代码。

js 主域重试

js 主域重试比 css 主域重试要复杂很多,因为 js 之间往往会存在依赖关系,所以对 js的执行顺序有着要求。举例来说,假设在 html 中依次引入了 1.js, 2.js, 3.js,那么我们希望最终能实现如下效果:

  • 若文件1.js,2.js,3.js 正常加载,则每一个 js 加载成功后立即执行;
  • 若文件1.js,3.js 正常加载, 2.js 加载失败,则 1.js 加载成功后立即执行,待 2.js 重试成功,再按序运行2.js,3.js;
  • 若文件1.js,2.js,3.js 均加载失败,则全部重试,若都成功,再按序运行 1.js,2.js,3.js。

也就是说,认为 2.js 是 依赖于 1.js,3.js 是依赖于 1.js 和 2.js,所以必须保证按照 1.js,2.js 和 3.js 的顺序来执行。这一想法是符合用 webpack 打包的项目的实际情况的:使用 webpack 打包的项目每个页面一般引入三个 js 文件:

  1. vendor.js:整个项目的基础库打包成该文件;
  2. common.js:将多个(大于等于3)页面公用的库打包成该文件;
  3. xxx.js:页面涉及的不包含在前面两个文件中的代码。

上述三个文件必须按照 vendor.js,common.js 和 xxx.js 的顺序来执行。

理想情况

在不需要考虑兼容性的情况下,js 主域重试其实也很好实现:监听 script标签的 onerror 事件,假若发现 js 加载失败,则通过 () 方法,立即写入一个新的 script标签,该标签的 src 指向主域地址。这种方法的神奇之处在于 (),通过它写入的 script新标签,会阻塞后续 script脚本的执行,直到新标签加载并执行完毕。

这种方法简直完美,实现代码也不超过 10行,然而现实是它不仅仅在 IE 上不能正常工作,在 Edge 上也不行:对于 windows 家的浏览器,哪怕 document.readystate 是 loading,在事件响应函数中调用 () 也会造成整个 html 的清空覆盖。所以,必须另寻它法。

接下来将具体讲一讲我所想到的 webpack 项目中 js 主域重试的解决方案,和大家一起讨论。

js 加载成功的判断

网上现有的资料大部分都是通过 script标签的 onload/onerror 事件来判断 js 的加载成功与否,有时为了兼容低版本的 IE,还需要通过 script标签的 onreadystatechange 事件来判断。感谢 webpack 提供了在不修改源文件的情况下对打包出来的 js 做注入的功能,所以类似于 css 埋 div 的方法,也可以在 webpack 构建的时候,向 js 文件埋入变量,然后尝试访问该变量,若得到值,则说明 js 文件加载成功;若未得到值,则说明 js 文件加载失败。

假设有一个 js 文件 js_exampl1.js 如下:

代码语言:javascript
复制
// old js_example1
console.log('js_example1');
...

那么可以根据文件名,埋入一个唯一的变量:

代码语言:javascript
复制
// new(1) js_example1
IMWEB_WEBPACK.js_example1 = true;
console.log('in js_exampl1');
...

最后,再根据这个变量来进行加载成功与否的判断:

代码语言:javascript
复制
if (!IMWEB_WEBPACK['js_example1']) {
  // 说明 js 加载失败
  var newScript= document.createElement('script');
  newScript.src = newSrc; // 主域下该 js 对应的地址
  document.body.appendChild(newScript);
}

上述代码有两点需要说明一下:

  1. 用文件名作为变量埋入是因为 webpack 打包后的文件名都含 md5 值,可以保证唯一性;
  2. 埋入的变量都放在 IMWEB_WEBPACK 下是为了避免污染全局变量。

js 避免立即执行

本节一开始有谈到,假如引入了1.js, 2.js, 3.js,若文件1.js,3.js 正常加载, 2.js 加载失败,则希望 3.js 在 2.js 从主域加载成功后再执行。为了实现这个需求,需要 3.js 在加载成功后,原代码不立即执行,我的实现方式是将原来的代码用函数体包裹起来避免立即执行,然后再调用一个事先写好的函数进行判断。

还是举例来进行具体说明。对于上一小节的 js_example1.js 文件,继续做如下改造:

代码语言:javascript
复制
// new(2) js_example1
IMWEB_WEBPACK.js_example1 = true;
function IMWEB_WEBPACK_js_exampl1() {
  console.log('in js_exampl1');
  …
}
IMWEB_WEBPACK_JS_ONLOAD('js_exampl1');

原 jsexample1 的代码被包裹在函数 IMWEB_WEBPACK_js_exampl1 中从而避免了立即执行。该函数名可在构建时自动生成,具体规则为 IMWEB_WEBPACK + 文件名。然后将文件名传入 IMWEB_WEBPACK_JS_ONLOAD,做下一步操作。

js 执行顺序保证

为了实现 js 主域重试,还需要向 webpack 生成的 html 文件插入两段 js 代码,第一段代码需要插入在所有外联的 js 代码之前,具体如下:

代码语言:javascript
复制
IMWEB_WEBPACK.JSARRAY = [
  { name: 'js_example1', url: '//7.url.cn/js_example1.js'},
  // ...
];
IMWEB_WEBPACK.firstLoad = true; // 标志是否是从页面本身的 script标签加载
IMWEB_WEBPACK.jsRunCnt = 0; // 计数器:统计已运行的 JS

function IMWEB_WEBPACK_JS_ONLOAD (name) {
  if (IMWEB_WEBPACK.firstLoad) {
    // 从本有的 script标签加载的 JS 
    var jsFile = IMWEB_WEBPACK.JSARRAY[IMWEB_WEBPACK.jsRunCnt];
    if (jsFile.name === name) {
      jsFile.isLoad = true;
      window['IMWEB_WEBPACK_' + jsFile.name]();
      IMWEB_WEBPACK.jsRunCnt++;
    }
  }
  else {
    // 从新添加的 script标签加载的 JS
    IMWEB_WEBPACK.jsLoadedCnt++;
    if (IMWEB_WEBPACK.jsLoadedCnt === IMWEB_WEBPACK.JSARRAY.length) {
      IMWEB_WEBPACK_RunScripts();
    }
  }
}

上述代码逻辑较为简单,做一点说明:

  • IMWEB_WEBPACK.JSARRAY 是扫描 html 文件得到的,它记录了该 html 所引入的所有外联 js 的文件名和链接;
  • IMWEB_WEBPACK.firstLoad 用于记录整个页面的 js 加载状态:当所有外联 script标签还未尝试加载完时,值为 true;当已尝试加载完时(无论成功与否),值为 false;
  • IMWEB_WEBPACK.jsRunCnt 用于统计已经加载并成功运行的 js 文件个数;
  • IMWEB_WEBPACK_JS_ONLOAD 每一个外联的 js 加载成功后都会调用这个函数,当所有外联 script标签还未尝试加载完时,若尚未有 js 加载失败,则每一个 js 加载成功后函数体都会立即执行;否则不执行。

第二段代码需要插入在所有外联的 js 代码之后,具体如下:

代码语言:javascript
复制
IMWEB_WEBPACK.firstLoad = false;
IMWEB_WEBPACK.jsLoadedCnt = IMWEB_WEBPACK.jsRunCnt; // 计数器:统计已加载的 JS

for (var i = IMWEB_WEBPACK.jsLoadedCnt; i < IMWEB_WEBPACK.JSARRAY.length; i++) {
  var name = IMWEB_WEBPACK.JSARRAY[i].name;
  if (!IMWEB_WEBPACK[name]) {
   var newScript= document.createElement('script');
    newScript.src = newUrl; // 主域下该 js 对应的地址
    document.body.appendChild(newScript);
  }
  else {
    IMWEB_WEBPACK.jsLoadedCnt++;
  }
}

function IMWEB_WEBPACK_RunScripts() {
  // 所有从 CDN 加载失败的 js 从主域加载成功后调用本函数
  for (var i = IMWEB_WEBPACK.jsRunCnt; i < IMWEB_WEBPACK.JSARRAY.length; i++) {
    var name = IMWEB_WEBPACK.JSARRAY[i].name; 
    window['IMWEB_WEBPACK_' + name]();
  }
}

上述代码会在所有外联 script标签尝试加载后(无论成功与否)执行,它主要负责重试从 CDN 加载失败的 js,并在所有主域重试的 js 加载成功后执行尚未执行的 js 脚本。

效果演示

在上图中,可以看见 common_md5.css 从 8.url.cn 加载失败后,自动从主域再次尝试拉取。

在上图中,可以看见 vendor_md5.js 从 7.url.cn 加载失败后,自动从主域再次尝试拉取。需要注意的 vendor_md5.js 从 7.url.cn 尝试拉取了两次,这应该是 Chrome (版本 60)本身的失败重试机制。

总结

css 主域重试较为简单,核心概念就是埋点;js 主域重试则较为复杂,因为涉及到了依赖的解决问题,核心在于埋变量和通过 jsRunCnt、jsLoadedCnt 两个计数器进行相应的判断。

有说的不清楚的地方或者读者认为有待商榷的地方欢迎在评论区指出,大家一起来进行讨论。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-08-27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
内容分发网络 CDN
内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档