Sid

Sid 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 nskip.com 编辑
编辑

JavaScript Developer

个人动态

Sid 赞了文章 · 2020-05-26

基于iframe的移动端嵌套

需求描述

上上周接到了新的项目,移动端需要做一个底部有五个导航,点击不同的导航页面主体显示不同的页面,其中两个页面是自己做,而另外三个页面是引用另外三个网址,其中两个网址为内部项目,另外一个为外部(涉及跨域)。

问题

考虑再三后最省时间成本的就是使用iframe,虽然在移动端使用,我的内心是很拒绝的,不过其他方案调研了下都不太符合现状。每点击一次加载一个新的iframe,比较懒,所以两个新页面也做成了iframe,在做的过程中出现了如下问题,这里总结一下:
1.嵌入的iframe页面无法滚动
2.meta元素的ontent不一致,外部的页面使用width=device-width,而引用的其中一个页面的width=640,这导致那个页面渲染的时候无法全屏缩小
3.ios下其中的一个页面莫名其妙的扩大
4.iframe的页面a标签的锚点失效
5.当我点击a加载了a的iframe页面,在切换到b,这个时候b页面字体莫名的变大
6.导航栏有个样式要求,active的时候icon是为红色的icon,其他状态下则为灰色的。其中的一个需求为返回的时候从哪里点出去返回到哪里
7.某个安卓机后返回无法重新加载iframe

解决

声明嵌入的iframe页面其中4个都是内部项目,同源的,所以大部分处理的问题不存在跨域问题。

1.嵌入的iframe页面无法滚动

在iframe外层包裹一个div,然后将其设置为可滚动

<div style="webkit-overflow-scrolling: touch;overflow-y: scroll;">
    <iframe></iframe>
</div>

并且在禁止浏览器的默认行为,不然类似于微信滑动到底部的时候会和回弹的效果进行冲突

$('body').on('touchmove',function(e){
    e.preventDefault();
});

2.meta元素的ontent不一致

这个暂时没有好办法,iframe渲染的meta也是默认走的最上层的meta,所以他自己的内部meta是失效的,由于该项目属于自己部门,有权限可以修改代码,所以最后我重新设置该页面的meta,重写了这个页面的样式。

3.ios下其中的一个页面莫名其妙的扩大

经排查我发现对于标题这类的设置了white-space:nowrap, 以及iframe页面用了swiper设置的宽度为100%,而移动端为了自适应body也设置的为100%这种情况下,ios下iframe而里面的页面会扩大。
我的解决办法是在原项目下页面html,body依旧设置为100%,而初始化的时候js获取屏幕的宽度再设置body的宽度。

4.iframe的页面a标签的锚点失效

若iframe不涉及跨域,网上有兼容代码可以重新设置a标签,跨域解决不了,因为跨域的情况下,外部页面是无法获取到iframe下的元素的,最后这个导航做了外部跳转。

5.iframe页面切换的时候,切换后的页面样式莫名变大

之前我做页面切换,是用过不重新加载iframe,而是直接修改了iframe的url,但是好像在这种情况下,可能之前上一个页面加载的css没有完全清除掉,所以导致css混乱。所以最后每次切换的时候,豆浆iframe给remove掉,在append加载新的iframe。

6.页面点击跳转之后,返回的状态标记

使用了localStorage记录了url,navIndex

7.某个安卓机后返回无法重新加载iframe

返回后再append的iframe的代码下再让其重新渲染下

$("#iframe").attr("src",url).ready()

8.其他

获取iframe内部元素

document.getElementById('iframe').onload = function(){
let doc = document.getElementById('iframe').contentDocument;
 }

中间涉及到了跨域请求,由于后台接口之前就写了,无法修改,但传过来的中文乱码接口默认的解析为gbk,所以需要设置scriptCharset: 'gbk',

$.ajax({
    type: "GET",
    url: url,
    dataType: "jsonp",
    jsonp: "callback",
    jsonpCallback: "data_callback",
    contentType: "application/x-javascript,charset-type=gbk",
    scriptCharset: 'gbk',
    crossDomain: true,
    success: function (json) {});
});
查看原文

赞 6 收藏 5 评论 0

Sid 赞了文章 · 2019-02-19

text-fill-color:仿苹果官网介绍效果 CSS设置文字渐变效果 文字背景图遮罩

发布iPhone XR的时候 各种心动 去官网看了一遍又一遍。
(贫穷使我节省。。。。。。。。。。)
闲着无聊发现 里面的介绍很用大篇幅的有背景文字来介绍。Like this:

clipboard.png

看着挺酷炫的还不错 就看了下实现方式。
还挺简单的。

附上demo

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>text-fill-color css web前端开发</title>
<style>
html{background: #000;}
.text1{
    margin:60px auto;
    font-size: 120px;
    text-align: center;
    font-weight: bold;
    background:-webkit-gradient(linear,30% 20%,80% 80%,from(#088df3),to(#6a38ec));
    -webkit-background-clip:text;
    -webkit-text-fill-color:transparent;
}
.text2{
    margin:60px auto;
    font-size: 120px;
    text-align: center;
    font-weight: bold;
    background: url(https://www.apple.com/v/iphone-xs/a/images/overview/copy_texture_1_medium.jpg) repeat center center;
    background-size: 100% 100%;//背景的铺排方式修改下效果更明显
    -webkit-background-clip:text;
    -webkit-text-fill-color:transparent;
}
.record{
    color: #737373;text-align: center;font-size: 24px;position: fixed;bottom: 0%;left: 0;width: 100%;padding: 20px 0;
}
</style>
</head>
<body>
    <div class="text1">文字颜色渐变</div>
    <div class="text2">文字遮罩照片</div>
    <p class="record">by Jerry yu </p>
</body>
</html>
            

clipboard.png

clipboard.png

mark一下 仅供参考 欢迎更正补充 end

查看原文

赞 48 收藏 34 评论 4

Sid 关注了专栏 · 2019-02-08

搜云库技术团队

专注于开发技术的研究与知识分享

关注 397

Sid 回答了问题 · 2018-12-29

解决js如何合并对象数组中的某些项?

data.map(d => d.parkId).filter((v, idx, arr) => arr.indexOf(v) === idx).map(parkId => {
  let d = {parkId, data: []}
  d.data = data.filter(item => item.parkId === parkId).map(item => {
    return {
        scheduleId: item.scheduleId,
        hasStock: item.hasStock
    }
  })
  return d
})

不过谈不上优雅。

关注 4 回答 3

Sid 回答了问题 · 2018-12-29

解决这种程序算桌面应用吗

从开发者的角度来看,肯定是不算的。
从使用者的角度来看,那肯定是桌面应用。

使用 Electron 和 NW.js 一类的框架打包的网页应用,可以使开发人员更专注,大部分情况下,和普通网页进行一致开发,涉及到一些底层的比如文件操作之类,又能够比浏览器更强大。如果是纯粹的页面开发,还可以规避掉浏览器的兼容性问题。

像钉钉,就是基于 NW.js 的,不知道现在换了没有。
Github 推出的 Github Desktop 是基于 Electron 的。

关注 4 回答 3

Sid 收藏了文章 · 2018-09-29

你真的会使用XMLHttpRequest吗?

看到标题时,有些同学可能会想:“我已经用xhr成功地发过很多个Ajax请求了,对它的基本操作已经算挺熟练了。” 我之前的想法和你们一样,直到最近我使用xhr时踩了不少坑儿,我才突然发现其实自己并不够了解xhr,我知道的只是最最基本的使用。
于是我决定好好地研究一番xhr的真面目,可拜读了不少博客后都不甚满意,于是我决定认真阅读一遍W3C的XMLHttpRequest标准。看完标准后我如同醍醐灌顶一般,感觉到了从未有过的清澈。这篇文章就是参考W3C的XMLHttpRequest标准和结合一些实践验证总结而来的。

AjaxXMLHttpRequest

我们通常将Ajax等同于XMLHttpRequest,但细究起来它们两个是属于不同维度的2个概念。

以下是我认为对Ajax较为准确的解释:(摘自what is Ajax
AJAX stands for Asynchronous JavaScript and XML. AJAX is a new technique for creating better, faster, and more interactive web applications with the help of XML, HTML, CSS, and Java Script.

AJAX is based on the following open standards:

  • Browser-based presentation using HTML and Cascading Style Sheets (CSS).

  • Data is stored in XML format and fetched from the server.

  • Behind-the-scenes data fetches using XMLHttpRequest objects in the browser.

  • JavaScript to make everything happen.

从上面的解释中可以知道:ajax是一种技术方案,但并不是一种新技术。它依赖的是现有的CSS/HTML/Javascript,而其中最核心的依赖是浏览器提供的XMLHttpRequest对象,是这个对象使得浏览器可以发出HTTP请求与接收HTTP响应。

所以我用一句话来总结两者的关系:我们使用XMLHttpRequest对象来发送一个Ajax请求。

XMLHttpRequest的发展历程

XMLHttpRequest一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了XMLHttpRequest标准XMLHttpRequest标准又分为Level 1Level 2
XMLHttpRequest Level 1主要存在以下缺点:

  • 受同源策略的限制,不能发送跨域请求;

  • 不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;

  • 在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;

那么Level 2Level 1 进行了改进,XMLHttpRequest Level 2中新增了以下功能:

  • 可以发送跨域请求,在服务端允许的情况下;

  • 支持发送和接收二进制数据;

  • 新增formData对象,支持发送表单数据;

  • 发送和获取数据时,可以获取进度信息;

  • 可以设置请求的超时时间;

当然更详细的对比介绍,可以参考阮老师的这篇文章,文章中对新增的功能都有具体代码示例。

XMLHttpRequest兼容性

关于xhr的浏览器兼容性,大家可以直接查看“Can I use”这个网站提供的结果XMLHttpRequest兼容性,下面提供一个截图。

clipboard.png

从图中可以看到:

  • IE8/IE9、Opera Mini 完全不支持xhr对象

  • IE10/IE11部分支持,不支持 xhr.responseTypejson

  • 部分浏览器不支持设置请求超时,即无法使用xhr.timeout

  • 部分浏览器不支持xhr.responseTypeblob

细说XMLHttpRequest如何使用

先来看一段使用XMLHttpRequest发送Ajax请求的简单示例代码。

function sendAjax() {
  //构造表单数据
  var formData = new FormData();
  formData.append('username', 'johndoe');
  formData.append('id', 123456);
  //创建xhr对象 
  var xhr = new XMLHttpRequest();
  //设置xhr请求的超时时间
  xhr.timeout = 3000;
  //设置响应返回的数据格式
  xhr.responseType = "text";
  //创建一个 post 请求,采用异步
  xhr.open('POST', '/server', true);
  //注册相关事件回调处理函数
  xhr.onload = function(e) { 
    if(this.status == 200||this.status == 304){
        alert(this.responseText);
    }
  };
  xhr.ontimeout = function(e) { ... };
  xhr.onerror = function(e) { ... };
  xhr.upload.onprogress = function(e) { ... };
  
  //发送数据
  xhr.send(formData);
}

上面是一个使用xhr发送表单数据的示例,整个流程可以参考注释。


接下来我将站在使用者的角度,以问题的形式介绍xhr的基本使用。
我对每一个问题涉及到的知识点都会进行比较细致地介绍,有些知识点可能是你平时忽略关注的。

如何设置request header

在发送Ajax请求(实质是一个HTTP请求)时,我们可能需要设置一些请求头部信息,比如content-typeconnectioncookieaccept-xxx等。xhr提供了setRequestHeader来允许我们修改请求 header。

void setRequestHeader(DOMString header, DOMString value);

注意点

  • 方法的第一个参数 header 大小写不敏感,即可以写成content-type,也可以写成Content-Type,甚至写成content-Type;

  • Content-Type的默认值与具体发送的数据类型有关,请参考本文【可以发送什么类型的数据】一节;

  • setRequestHeader必须在open()方法之后,send()方法之前调用,否则会抛错;

  • setRequestHeader可以调用多次,最终的值不会采用覆盖override的方式,而是采用追加append的方式。下面是一个示例代码:

var client = new XMLHttpRequest();
client.open('GET', 'demo.cgi');
client.setRequestHeader('X-Test', 'one');
client.setRequestHeader('X-Test', 'two');
// 最终request header中"X-Test"为: one, two
client.send();

如何获取response header

xhr提供了2个用来获取响应头部的方法:getAllResponseHeadersgetResponseHeader。前者是获取 response 中的所有header 字段,后者只是获取某个指定 header 字段的值。另外,getResponseHeader(header)header参数不区分大小写。

DOMString getAllResponseHeaders();
DOMString getResponseHeader(DOMString header);

这2个方法看起来简单,但却处处是坑儿。

你是否遇到过下面的坑儿?——反正我是遇到了。。。

  1. 使用getAllResponseHeaders()看到的所有response header与实际在控制台 Network 中看到的 response header 不一样

  2. 使用getResponseHeader()获取某个 header 的值时,浏览器抛错Refused to get unsafe header "XXX"

经过一番寻找最终在 Stack Overflow找到了答案

"simple response header"包括的 header 字段有:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma;
"Access-Control-Expose-Headers":首先得注意是"Access-Control-Expose-Headers"进行跨域请求时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。

所以getAllResponseHeaders()只能拿到限制以外(即被视为safe)的header字段,而不是全部字段;而调用getResponseHeader(header)方法时,header参数必须是限制以外的header字段,否则调用就会报Refused to get unsafe header的错误。

如何指定xhr.response的数据类型

有些时候我们希望xhr.response返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过xhr.response拿到的直接就是一个 js 对象,我们该怎么实现呢?
有2种方法可以实现,一个是level 1就提供的overrideMimeType()方法,另一个是level 2才提供的xhr.responseType属性。

xhr.overrideMimeType()

overrideMimeTypexhr level 1就有的方法,所以浏览器兼容性良好。这个方法的作用就是用来重写responsecontent-type,这样做有什么意义呢?比如:server 端给客户端返回了一份document或者是 xml文档,我们希望最终通过xhr.response拿到的就是一个DOM对象,那么就可以用xhr.overrideMimeType('text/xml; charset = utf-8')来实现。

再举一个使用场景,我们都知道xhr level 1不支持直接传输blob二进制数据,那如果真要传输 blob 该怎么办呢?当时就是利用overrideMimeType方法来解决这个问题的。

下面是一个获取图片文件的代码示例:

var xhr = new XMLHttpRequest();
//向 server 端获取一张图片
xhr.open('GET', '/path/to/image.png', true);

// 这行是关键!
//将响应数据按照纯文本格式来解析,字符集替换为用户自己定义的字符集
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function(e) {
  if (this.readyState == 4 && this.status == 200) {
    //通过 responseText 来获取图片文件对应的二进制字符串
    var binStr = this.responseText;
    //然后自己再想方法将逐个字节还原为二进制数据
    for (var i = 0, len = binStr.length; i < len; ++i) {
      var c = binStr.charCodeAt(i);
      //String.fromCharCode(c & 0xff);
      var byte = c & 0xff; 
    }
  }
};

xhr.send();

代码示例中xhr请求的是一张图片,通过将 responsecontent-type 改为'text/plain; charset=x-user-defined',使得 xhr 以纯文本格式来解析接收到的blob 数据,最终用户通过this.responseText拿到的就是图片文件对应的二进制字符串,最后再将其转换为 blob 数据。

xhr.responseType

responseTypexhr level 2新增的属性,用来指定xhr.response的数据类型,目前还存在些兼容性问题,可以参考本文的【XMLHttpRequest的兼容性】这一小节。那么responseType可以设置为哪些格式呢,我简单做了一个表,如下:

xhr.response 数据类型说明
""String字符串默认值(在不设置responseType时)
"text"String字符串
"document"Document对象希望返回 XML 格式数据时使用
"json"javascript 对象存在兼容性问题,IE10/IE11不支持
"blob"Blob对象
"arrayBuffer"ArrayBuffer对象

下面是同样是获取一张图片的代码示例,相比xhr.overrideMimeType,用xhr.response来实现简单得多。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
//可以将`xhr.responseType`设置为`"blob"`也可以设置为`" arrayBuffer"`
//xhr.responseType = 'arrayBuffer';
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;
    ...
  }
};

xhr.send();

小结

虽然在xhr level 2中,2者是共同存在的。但其实不难发现,xhr.responseType就是用来取代xhr.overrideMimeType()的,xhr.responseType功能强大的多,xhr.overrideMimeType()能做到的xhr.responseType都能做到。所以我们现在完全可以摒弃使用xhr.overrideMimeType()了。

如何获取response数据

xhr提供了3个属性来获取请求返回的数据,分别是:xhr.responsexhr.responseTextxhr.responseXML

  • xhr.response

    • 默认值:空字符串""

    • 当请求完成时,此属性才有正确的值

    • 请求未完成时,此属性的值可能是""或者 null,具体与 xhr.responseType有关:当responseType"""text"时,值为""responseType为其他值时,值为 null

  • xhr.responseText

    • 默认值为空字符串""

    • 只有当 responseType"text"""时,xhr对象上才有此属性,此时才能调用xhr.responseText,否则抛错

    • 只有当请求成功时,才能拿到正确值。以下2种情况下值都为空字符串"":请求未完成、请求失败

  • xhr.responseXML

    • 默认值为 null

    • 只有当 responseType"text""""document"时,xhr对象上才有此属性,此时才能调用xhr.responseXML,否则抛错

    • 只有当请求成功且返回数据被正确解析时,才能拿到正确值。以下3种情况下值都为null:请求未完成、请求失败、请求成功但返回数据无法被正确解析时

如何追踪ajax请求的当前状态

在发一个ajax请求后,如果想追踪请求当前处于哪种状态,该怎么做呢?

xhr.readyState这个属性即可追踪到。这个属性是只读属性,总共有5种可能值,分别对应xhr不同的不同阶段。每次xhr.readyState的值发生变化时,都会触发xhr.onreadystatechange事件,我们可以在这个事件中进行相关状态判断。

  xhr.onreadystatechange = function () {
    switch(xhr.readyState){
      case 1://OPENED
        //do something
            break;
      case 2://HEADERS_RECEIVED
        //do something
        break;
      case 3://LOADING
        //do something
        break;
      case 4://DONE
        //do something
        break;
    }
状态描述
0UNSENT (初始状态,未打开)此时xhr对象被成功构造,open()方法还未被调用
1OPENED (已打开,未发送)open()方法已被成功调用,send()方法还未被调用。注意:只有xhr处于OPENED状态,才能调用xhr.setRequestHeader()xhr.send(),否则会报错
2HEADERS_RECEIVED (已获取响应头)send()方法已经被调用, 响应头和响应状态已经返回
3LOADING (正在下载响应体)响应体(response entity body)正在下载中,此状态下通过xhr.response可能已经有了响应数据
4DONE (整个数据传输过程结束)整个数据传输过程结束,不管本次请求是成功还是失败

如何设置请求的超时时间

如果请求过了很久还没有成功,为了不会白白占用的网络资源,我们一般会主动终止请求。XMLHttpRequest提供了timeout属性来允许设置请求的超时时间。

xhr.timeout

单位:milliseconds 毫秒
默认值:0,即不设置超时

很多同学都知道:从请求开始 算起,若超过 timeout 时间请求还没有结束(包括成功/失败),则会触发ontimeout事件,主动结束该请求。

【那么到底什么时候才算是请求开始 ?】
——xhr.onloadstart事件触发的时候,也就是你调用xhr.send()方法的时候。
因为xhr.open()只是创建了一个连接,但并没有真正开始数据的传输,而xhr.send()才是真正开始了数据的传输过程。只有调用了xhr.send(),才会触发xhr.onloadstart

【那么什么时候才算是请求结束 ?】
—— xhr.loadend事件触发的时候。

另外,还有2个需要注意的坑儿:

  1. 可以在 send()之后再设置此xhr.timeout,但计时起始点仍为调用xhr.send()方法的时刻。

  2. xhr为一个sync同步请求时,xhr.timeout必须置为0,否则会抛错。原因可以参考本文的【如何发一个同步请求】一节。

如何发一个同步请求

xhr默认发的是异步请求,但也支持发同步请求(当然实际开发中应该尽量避免使用)。到底是异步还是同步请求,由xhr.open()传入的async参数决定。

open(method, url [, async = true [, username = null [, password = null]]])

  • method: 请求的方式,如GET/POST/HEADER等,这个参数不区分大小写

  • url: 请求的地址,可以是相对地址如example.php,这个相对是相对于当前网页的url路径;也可以是绝对地址如http://www.example.com/example.php

  • async: 默认值为true,即为异步请求,若async=false,则为同步请求

在我认真研读W3C 的 xhr 标准前,我总以为同步请求和异步请求只是阻塞和非阻塞的区别,其他什么事件触发、参数设置应该是一样的,事实证明我错了。

W3C 的 xhr标准中关于open()方法有这样一段说明:

Throws an "InvalidAccessError" exception if async is false, the JavaScript global environment is a document environment, and either the timeout attribute is not zero, the withCredentials attribute is true, or the responseType attribute is not the empty string.

从上面一段说明可以知道,当xhr为同步请求时,有如下限制:

  • xhr.timeout必须为0

  • xhr.withCredentials必须为 false

  • xhr.responseType必须为""(注意置为"text"也不允许)

若上面任何一个限制不满足,都会抛错,而对于异步请求,则没有这些参数设置上的限制。

之前说过页面中应该尽量避免使用sync同步请求,为什么呢?
因为我们无法设置请求超时时间(xhr.timeout0,即不限时)。在不限制超时的情况下,有可能同步请求一直处于pending状态,服务端迟迟不返回响应,这样整个页面就会一直阻塞,无法响应用户的其他交互。

另外,标准中并没有提及同步请求时事件触发的限制,但实际开发中我确实遇到过部分应该触发的事件并没有触发的现象。如在 chrome中,当xhr为同步请求时,在xhr.readyState2变成3时,并不会触发 onreadystatechange事件,xhr.upload.onprogressxhr.onprogress事件也不会触发。

如何获取上传、下载的进度

在上传或者下载比较大的文件时,实时显示当前的上传、下载进度是很普遍的产品需求。
我们可以通过onprogress事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的onprogress事件:

  • 上传触发的是xhr.upload对象的 onprogress事件

  • 下载触发的是xhr对象的onprogress事件

xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
    if (event.lengthComputable) {
      var completedPercent = event.loaded / event.total;
    }
 }

可以发送什么类型的数据

void send(data);

xhr.send(data)的参数data可以是以下几种类型:

  • ArrayBuffer

  • Blob

  • Document

  • DOMString

  • FormData

  • null

如果是 GET/HEAD请求,send()方法一般不传参或传 null。不过即使你真传入了参数,参数也最终被忽略,xhr.send(data)中的data会被置为 null.

xhr.send(data)中data参数的数据类型会影响请求头部content-type的默认值:

  • 如果dataDocument 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8

  • 如果dataDOMString 类型,content-type默认值为text/plain;charset=UTF-8

  • 如果dataFormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]

  • 如果data是其他类型,则不会设置content-type的默认值

当然这些只是content-type的默认值,但如果用xhr.setRequestHeader()手动设置了中content-type的值,以上默认值就会被覆盖。

另外需要注意的是,若在断网状态下调用xhr.send(data)方法,则会抛错:Uncaught NetworkError: Failed to execute 'send' on 'XMLHttpRequest'。一旦程序抛出错误,如果不 catch 就无法继续执行后面的代码,所以调用 xhr.send(data)方法时,应该用 try-catch捕捉错误。

try{
    xhr.send(data)
  }catch(e) {
    //doSomething...
  };

xhr.withCredentialsCORS 什么关系

我们都知道,在发同域请求时,浏览器会将cookie自动加在request header中。但大家是否遇到过这样的场景:在发送跨域请求时,cookie并没有自动加在request header中。

造成这个问题的原因是:在CORS标准中做了规定,默认情况下,浏览器在发送跨域请求时,不能发送任何认证信息(credentials)如"cookies"和"HTTP authentication schemes"。除非xhr.withCredentialstruexhr对象有一个属性叫withCredentials,默认值为false)。

所以根本原因是cookies也是一种认证信息,在跨域请求中,client端必须手动设置xhr.withCredentials=true,且server端也必须允许request能携带认证信息(即response header中包含Access-Control-Allow-Credentials:true),这样浏览器才会自动将cookie加在request header中。

另外,要特别注意一点,一旦跨域request能够携带认证信息,server端一定不能将Access-Control-Allow-Origin设置为*,而必须设置为请求页面的域名。

xhr相关事件

事件分类

xhr相关事件有很多,有时记起来还挺容易混乱。但当我了解了具体代码实现后,就容易理清楚了。下面是XMLHttpRequest的部分实现代码:

interface XMLHttpRequestEventTarget : EventTarget {
  // event handlers
  attribute EventHandler onloadstart;
  attribute EventHandler onprogress;
  attribute EventHandler onabort;
  attribute EventHandler onerror;
  attribute EventHandler onload;
  attribute EventHandler ontimeout;
  attribute EventHandler onloadend;
};

interface XMLHttpRequestUpload : XMLHttpRequestEventTarget {

};

interface XMLHttpRequest : XMLHttpRequestEventTarget {
  // event handler
  attribute EventHandler onreadystatechange;
  readonly attribute XMLHttpRequestUpload upload;
};

从代码中我们可以看出:

  1. XMLHttpRequestEventTarget接口定义了7个事件:

    • onloadstart

    • onprogress

    • onabort

    • ontimeout

    • onerror

    • onload

    • onloadend

  2. 每一个XMLHttpRequest里面都有一个upload属性,而upload是一个XMLHttpRequestUpload对象

  3. XMLHttpRequestXMLHttpRequestUpload都继承了同一个XMLHttpRequestEventTarget接口,所以xhrxhr.upload都有第一条列举的7个事件

  4. onreadystatechangeXMLHttpRequest独有的事件

所以这么一看就很清晰了:
xhr一共有8个相关事件:7个XMLHttpRequestEventTarget事件+1个独有的onreadystatechange事件;而xhr.upload只有7个XMLHttpRequestEventTarget事件。

事件触发条件

下面是我自己整理的一张xhr相关事件触发条件表,其中最需要注意的是 onerror 事件的触发条件。

事件触发条件
onreadystatechange每当xhr.readyState改变时触发;但xhr.readyState由非0值变为0时不触发。
onloadstart调用xhr.send()方法后立即触发,若xhr.send()未被调用则不会触发此事件。
onprogressxhr.upload.onprogress在上传阶段(即xhr.send()之后,xhr.readystate=2之前)触发,每50ms触发一次;xhr.onprogress在下载阶段(即xhr.readystate=3时)触发,每50ms触发一次。
onload当请求成功完成时触发,此时xhr.readystate=4
onloadend当请求结束(包括请求成功和请求失败)时触发
onabort当调用xhr.abort()后触发
ontimeoutxhr.timeout不等于0,由请求开始即onloadstart开始算起,当到达xhr.timeout所设置时间请求还未结束即onloadend,则触发此事件。
onerror在请求过程中,若发生Network error则会触发此事件(若发生Network error时,上传还没有结束,则会先触发xhr.upload.onerror,再触发xhr.onerror;若发生Network error时,上传已经结束,则只会触发xhr.onerror)。注意,只有发生了网络层级别的异常才会触发此事件,对于应用层级别的异常,如响应返回的xhr.statusCode4xx时,并不属于Network error,所以不会触发onerror事件,而是会触发onload事件。

事件触发顺序

当请求一切正常时,相关的事件触发顺序如下:

  1. 触发xhr.onreadystatechange(之后每次readyState变化时,都会触发一次)

  2. 触发xhr.onloadstart
    //上传阶段开始:

  3. 触发xhr.upload.onloadstart

  4. 触发xhr.upload.onprogress

  5. 触发xhr.upload.onload

  6. 触发xhr.upload.onloadend
    //上传结束,下载阶段开始:

  7. 触发xhr.onprogress

  8. 触发xhr.onload

  9. 触发xhr.onloadend

发生abort/timeout/error异常的处理

在请求的过程中,有可能发生 abort/timeout/error这3种异常。那么一旦发生这些异常,xhr后续会进行哪些处理呢?后续处理如下:

  1. 一旦发生aborttimeouterror异常,先立即中止当前请求

  2. readystate 置为4,并触发 xhr.onreadystatechange事件

  3. 如果上传阶段还没有结束,则依次触发以下事件:

    • xhr.upload.onprogress

    • xhr.upload.[onabort或ontimeout或onerror]

    • xhr.upload.onloadend

  4. 触发 xhr.onprogress事件

  5. 触发 xhr.[onabort或ontimeout或onerror]事件

  6. 触发xhr.onloadend 事件

在哪个xhr事件中注册成功回调?

从上面介绍的事件中,可以知道若xhr请求成功,就会触发xhr.onreadystatechangexhr.onload两个事件。 那么我们到底要将成功回调注册在哪个事件中呢?我倾向于 xhr.onload事件,因为xhr.onreadystatechange是每次xhr.readyState变化时都会触发,而不是xhr.readyState=4时才触发。

xhr.onload = function () {
    //如果请求成功
    if(xhr.status == 200){
      //do successCallback
    }
  }

上面的示例代码是很常见的写法:先判断http状态码是否是200,如果是,则认为请求是成功的,接着执行成功回调。这样的判断是有坑儿的,比如当返回的http状态码不是200,而是201时,请求虽然也是成功的,但并没有执行成功回调逻辑。所以更靠谱的判断方法应该是:当http状态码为2xx304时才认为成功。

  xhr.onload = function () {
    //如果请求成功
    if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
      //do successCallback
    }
  }

结语

终于写完了......
看完那一篇长长的W3C的xhr 标准,我眼睛都花了......
希望这篇总结能帮助刚开始接触XMLHttpRequest的你。

最后给点扩展学习资料,如果你:

查看原文

Sid 关注了用户 · 2018-02-08

lea_ @lea_

在有限时间,做最好自己.

关注 18

Sid 赞了文章 · 2017-08-25

HTTP缓存机制详解

前言

在请求一个静态文件的时候(图片,css,js)等,这些文件的特点是文件不经常变化,将这些不经常变化的文件存储起来,对客户端来说是一个优化用户浏览体验的方法。那么这个就是客户端缓存的意义了。

Http 缓存机制作为 web 性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系库中的一个基础环节,同时对于有志成为前端架构师的同学来说是必备的知识技能。

但是对于很多前端同学来说,仅仅只是知道浏览器会对请求的静态文件进行缓存,但是为什么被缓存,缓存是怎样生效的,却并不是很清楚。

在此,我会尝试用简单明了的文字,像大家系统的介绍HTTP缓存机制,期望对各位正确的理解前端缓存有所帮助。

缓存规则解析

HTTP缓存有多种规则,根据是否需要重新向服务器发起请求来分类,我将其分为两大类(强制缓存,对比缓存)

在详细介绍这两种规则之前,先通过时序图的方式,让大家对这两种规则有个简单了解。

已存在缓存数据时,仅基于强制缓存,请求数据的流程如下:

clipboard.png

已存在缓存数据时,仅基于对比缓存,请求数据的流程如下:

clipboard.png

对缓存机制不太了解的同学可能会问,基于对比缓存的流程下,不管是否使用缓存,都需要向服务器发送请求,那么还用缓存干什么?

这个问题,我们暂且放下,后文在详细介绍每种缓存规则的时候,会带给大家答案。

我们可以看到两类缓存规则的不同,强制缓存如果生效,不需要再和服务器发生交互,而对比缓存不管是否生效,都需要与服务端发生交互。

两类缓存规则可以同时存在,强制缓存优先级高于对比缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则。

强制缓存

从上文我们得知,强制缓存,在缓存数据未失效的情况下,可以直接使用缓存数据,那么浏览器是如何判断缓存数据是否失效呢?

我们知道,在没有缓存数据的时候,浏览器向服务器请求数据时,服务器会将数据和缓存规则一并返回,缓存规则信息包含在响应header中。

对于强制缓存来说,响应header中会有两个字段来标明失效规则(Expires/Cache-Control)使用chrome的开发者工具,可以很明显的看到对于强制缓存生效时,网络请求的情况。

Expires

Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。

不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。

另一个问题是,到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。

所以HTTP 1.1 的版本,使用Cache-Control替代。

Cache-Control

Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。

private:             客户端可以缓存
public:              客户端和代理服务器都可缓存(前端的同学,可以认为public和private是一样的)
max-age=xxx:   缓存的内容将在 xxx 秒后失效
no-cache:          需要使用对比缓存来验证缓存数据(后面介绍)
no-store:           所有内容都不会缓存,强制缓存,对比缓存都不会触发

(对于前端开发来说,缓存越多越好,so...基本上和它说886)

举个例子:

clipboard.png

图中Cache-Control仅指定了max-age,所以默认为private,缓存时间为31536000秒(365天)也就是说,在365天内再次请求这条数据,都会直接获取缓存数据库中的数据,直接使用。

没懂的话,我们换通俗一点的话来说一遍。当客户端第一次访问资源的时候,服务端在返回资源内容的同时也返回了Expires: Sun, 16 Oct 2016 05:43:02 GMT。

服务端告诉浏览器: 你Y的先把这个文件给我缓存起来,在这个过期时间之前,这个文件都不会变化了,你下次需要这个文件的时候,你就不要过来找我要了,你就去缓存中拿就好了,又快又好。

浏览器回答说:诺。

于是在第二次html页面中又要访问这个资源的时候,并且访问的日期在Sun, 16 Oct 2016 05:43:02 GMT之前,浏览器就不去服务器那边获取文件了,自己从缓存中自食其力了。

但是呢,浏览器毕竟是在客户端的,客户端的时间可是不准确的,用户可以随着自己的喜好修改自己机器的时间,比如我把我机器的时间调成Sun, 16 Oct 2016 05:43:03 GMT,那么呢?我的浏览器就不会再使用缓存了,而每次都去服务器获取文件。于是,服务器怒了:给你个绝对时间,你由于环境被修改没法判断过期,那么我就给你相对时间吧。于是就返回了Cache-Control: max-age:600,浏览器你给我缓存个10分钟去。于是浏览器只有乖乖的缓存10分钟了。

但是问题又来了,如果有的服务器同时设置了Expires和Cache-Control怎么办呢?(不是闲的没事干,而是由于Cache-Controll是HTTP1.1中才有的)那么就是根据更先进的设置Cache-Control来为标准。

好了,现在有个问题,我有个文件可能时不时会更新,服务端非常希望客户端能时不时过来问一下这个文件是否过期,如果没有过期,服务端不返回数据给你,只告诉浏览器你的缓存还没有过期(304)。然后浏览器使用自己存储的缓存来做显示。这个就叫做条件请求。

对比缓存

对比缓存,顾名思义,需要进行比较判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。

再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。

对于对比缓存来说,缓存标识的传递是我们着重需要理解的,它在请求header和响应header间进行传递,一共分为两种标识传递,接下来,我们分开介绍。

Last-Modified / If-Modified-Since

Last-Modified:服务器在响应请求时,告诉浏览器资源的最后修改时间。

clipboard.png

If-Modified-Since:

再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。

服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。

若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码200;若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache。

clipboard.png

Etag / If-None-Match(优先级高于Last-Modified / If-Modified-Since)

第一次客户端访问资源的时候,服务端返回资源内容的同时返回了ETag:1234,告诉客户端:这个文件的标签是1234,我如果修改了我这边的资源的话,这个标签就会不一样了。

第二次客户端访问资源的时候,由于缓存中已经有了Etag为1234的资源,客户端要去服务端查询的是这个资源有木有过期呢?所以带上了If-None-Match: 1234。告诉服务端:如果你那边的资源还是1234标签的资源,你就返回304告诉我,不需要返回资源内容了。如果不是的话,你再返回资源内容给我就行了。服务端就比较下Etag来看是返回304还是200。

clipboard.png

各种刷新

理解了上面的缓存标签之后就很好理解各种刷新了。

刷新有三种

浏览器中写地址,回车
F5
Ctrl+F5

假设对一个资源:

浏览器第一次访问,获取资源内容和cache-control: max-age:600,Last_Modify: Wed, 10 Aug 2013 15:32:18 GMT于是浏览器把资源文件放到缓存中,并且决定下次使用的时候直接去缓存中取了。

浏览器url回车

浏览器发现缓存中有这个文件了,好了,就不发送任何请求了,直接去缓存中获取展现。(最快)

下面我按下了F5刷新

F5就是告诉浏览器,别偷懒,好歹去服务器看看这个文件是否有过期了。于是浏览器就胆胆襟襟的发送一个请求带上If-Modify-since:Wed, 10 Aug 2013 15:32:18 GMT

然后服务器发现:诶,这个文件我在这个时间后还没修改过,不需要给你任何信息了,返回304就行了。于是浏览器获取到304后就去缓存中欢欢喜喜获取资源了。

但是呢,下面我们按下了Ctrl+F5

这个可是要命了,告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求个完整的资源文件下来。于是客户端就完成了强行更新的操作...

还有说一下,那个ETag实际上很少人使用,因为它的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用etag了。

查看原文

赞 49 收藏 234 评论 18

Sid 关注了用户 · 2017-07-25

array_huang @array_huang

关注 4990

Sid 赞了文章 · 2017-07-25

webpack多页应用架构系列(十六):善用浏览器缓存,该去则去,该留则留

本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000010317802
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang

前言

一个成熟的项目,自然离不开迭代更新;那么在部署前端这一块,我们免不了总是要顾及到浏览器缓存的,本文将介绍如何在 webpack (架构)的帮助下,妥善处理好浏览器缓存。

实际上,我很早以前就想写这一part了,只是苦于当时我所掌握的方案不如人意,便不敢献丑了;而自从
webpack 升级到 v2 版本后,以及第三方plugin的日益丰富,我们也有了更多的手段来处理cache。

浏览器缓存简单介绍

下面来简单介绍一下浏览器缓存,以及为何我要在标题中强调“该去则去,该留则留”。

浏览器缓存是啥?

浏览器缓存(Browser Cache),是浏览器为了节省网络带宽、加快网站访问速度而推出的一项功能。浏览器缓存的运行机制是这样的:

  1. 用户使用浏览器第一次访问某网站页面,该页面上引入了各种各样的静态资源(js/css/图片/字体……),浏览器会把这些静态资源,甚至是页面本身(html文件),都一一储存到本地。
  2. 用户在后续的访问中,如果需要再次请求同样的静态资源(根据 url 进行匹配),且静态资源没有过期(服务器端有一系列判别资源是否过期的策略,比如Cache-ControlPragmaETagExpiresLast-Modified),则直接使用前面本地储存的资源,而不需要重复请求。

由于webpack只负责构建生成网站前端的静态资源,不涉及服务器,因此本文不讨论以HTTP Header为基础的缓存控制策略;那我们讨论什么呢?

很简单,由于浏览器是根据静态资源的url来判断该静态资源是否已有缓存,而静态资源的文件目录又是相对固定的,那么重点明显就在于静态资源的文件名了;我们就通过操控静态资源的文件名,来决定静态资源的“去留”。

浏览器缓存,该留不留会怎么样?

每次部署上线新版本,静态资源的文件名若有变化,则浏览器判断是第一次读取这个静态资源;那么,即便这个静态资源的内容跟上一版的完全一致,浏览器也要重新下载这个静态资源,浪费网络带宽、拖慢页面加载速度。

浏览器缓存,该去不去会怎么样?

每次部署上线新版本,静态资源的文件名若没有变化,则浏览器判断可加载之前缓存下来的静态资源;那么,即便这个静态资源的内容跟上一版的有所变化,浏览器也察觉不到,使用了老版本的静态资源。那这会造成什么样的影响呢?可大可小,小至用户看到的依然是老版的资源,达不到上线更新版本的目的;大至造成网站运行报错、布局错位等问题。

如何通过操控静态资源的文件名达到控制浏览器缓存的目的呢?

在webpack关于文件名命名的配置中,存在一系列的变量(或者理解成命名规则也可),通过这些变量,我们可以根据所要生成的文件的具体情况来进行命名,而不必预设好一个固定的名称。在缓存处理这一块,我们主要用到[hash][chunkhash]这两个变量。关于这两个变量的介绍,我在之前的文章 —— 《webpack配置常用部分有哪些?》就已经解释过是什么意思了,这里就不再累述。

这里总结下[hash][chunkhash]这两个变量的用法:

  • [hash]的话,由于每次使用 webpack 构建代码的时候,此 hash 字符串都会更新,因此相当于强制刷新浏览器缓存
  • [chunkhash]的话,则会根据具体 chunk 的内容来形成一个 hash 字符串来插入到文件名上;换句说, chunk 的内容不变,该 chunk 所对应生成出来的文件的文件名也不会变,由此,浏览器缓存便能得以继续利用

有哪些资源是需要兼顾浏览器缓存的?

理论上来说,除了HTML文件外(HTML文件的路径需要保持相对固定,只能从服务器端入手),webpack生成的所有文件都需要处理好浏览器缓存的问题。

js

在 webpack 架构下,js文件也有不同类型,因此也需要不同的配置:

  1. 入口文件(Entry):在webpack配置中的output.filename参数中,让生成的文件名中带上[chunkhash]即可。
  2. 异步加载的chunk:output.chunkFilename参数,操作同上。
  3. 通过CommonsChunkPlugin生成的文件:在CommonsChunkPlugin的配置参数中有filename这一项,操作同上。但需要注意的是,如果你使用[chunkhash]的话,webpack 构建的时候可是会报错的哦;那可咋办呢,用[hash]的话,这common chunk不就每次上线新版本都强制刷新了吗?这其实是因为,webpack 的 runtime && manifest 会统一保存在你的common chunk里,解决的方法,就请看下面关于“webpack 的 runtime && manifest”的部分了。

css

对于css来说,如果你是用style-loader直接把css内联到<head>里的,那么,你管好引入该css的js文件的浏览器缓存就好了。

而如果你是使用extract-text-webpack-plugin把css独立打包成css文件的,那么在文件名的配置上,同样加上[chunkhash]即可加上[contenthash]即可(感谢@FLYiNg_hbt 提醒)。这个[contenthash]是什么东西呢?其实就是extract-text-webpack-plugin为了与[chunkhash]区分开,而自定义的一个命名规则,其实际含义跟[chunkhash]可以说是一致的,只是[chunkhash]已被占用作为 chunk 的内容 hash 字符串了,继续用[chunkhash]会造成下述问题

图片、字体文件等静态资源

《听说webpack连图片和字体也能打包?》里介绍的,处理这类静态资源一般使用url-loaderfile-loader

对于url-loader来说,就不需要关心浏览器缓存了,因为它是把静态资源转化成 dataurl 了,而并非独立的文件。

而对于file-loader来说,同样是在文件名的配置上加上[chunkhash]即可。另外需要注意的是,url-loader一般搭配有降级到file-loader的配置(使用loader加载的文件大于一个你设定的值就降级到使用file-loader来加载),同样需要在文件名的配置上加上[chunkhash]

webpack 的runtime && manifest

所谓的runtime,就是帮助 webpack 编译构建后的打包文件在浏览器运行的一些辅助代码段,换句话说,打包后的文件,除了你自己的源码和npm库外,还有 webpack 提供的一点辅助代码段。

而 manifest,则是 webpack 用以查找 chunk 真实路径所使用的一份关系表,简单来说,就是 chunk 名对应 chunk 路径的关系表。manifest 一般来说会被藏到 runtime 里,因此我们查看 runtime 的时候,虽然能找得到 manifest,但一般都不那么直观,形如下面这一段(仅common chunk部分):

u.type = "text/javascript", u.charset = "utf-8", u.async = !0, u.timeout = 12e4, n.nc && u.setAttribute("nonce", n.nc), u.src = n.p + "" + e + "." + {
    0: "e6d1dff43f64d01297d3",
    1: "7ad996b8cbd7556a3e56",
    2: "c55991cf244b3d833c32",
    3: "ecbcdaa771c68c97ac38",
    4: "6565e12e7bad74df24c3",
    5: "9f2774b4601839780fc6"
}[e] + ".bundle.js";

runtime && manifest被打包到哪里去了?

那么,这runtime && manifest的代码段,会被放到哪里呢?一般来说,如果没有使用CommonsChunkPlugin生成common chunkruntime && manifest会被放在以入口文件为首的chunk(俗称“大包”)里,如果是我们这种多页(又称多入口)应用,则会每个大包一份runtime && manifest;这夸张的冗余我们自然是不能忍的,那么
用上CommonsChunkPlugin后,runtime && manifest就会统一迁到common chunk了。

runtime && manifestcommon chunk带来的缓存危机

虽说把runtime && manifest迁到common chunk后,代码冗余的问题算是解决了,但却造成另一问题:由于我们在上述的静态资源的文件名命名上都采用了[chunkhash]的方案,因此也使得只要我们稍一改动源代码,就会有起码一个 chunk 的命名会产生变化,这就会导致我们的runtime && manifest也产生变化,从而导致我们的common chunk也发生变化,这或许就是 webpack 规定含有runtime && manifestcommon chunk不能使用[chunkhash]的原因吧(反正chunkhash肯定会变的,还不如不用呢是不是)。

要解决上述问题(这问题很严重啊我摔,common chunk怎么能用不上缓存啊,这可是最大的chunk啊),我们就需要把runtime && manifest给独立出去。方法也很简单,在用来打包common chunkCommonsChunkPlugin后,再加一CommonsChunkPlugin

  /* 抽取出所有通用的部分 */
  new webpack.optimize.CommonsChunkPlugin({
    name: 'commons/commons',      // 需要注意的是,chunk的name不能相同!!!
    filename: '[name]/bundle.[chunkhash].js', // 由于runtime独立出去了,这里便可以使用[chunkhash]了
    minChunks: 4,
  }),
  /* 抽取出webpack的runtime代码,避免稍微修改一下入口文件就会改动commonChunk,导致原本有效的浏览器缓存失效 */
  new webpack.optimize.CommonsChunkPlugin({
    name: 'webpack-runtime',
    filename: 'commons/commons/webpack-runtime.[hash].js', // 注意runtime只能用[hash]
  }),

这样一来,runtime && manifest代码段就会被打包到这个名为webpack-runtime的 chunk 里了。这是什么原理呢?据说是在使用CommonsChunkPlugin的情况下, webpack 会把runtime && manifest打包到最后面的一个CommonsChunkPlugin生成的 chunk 里,而如果这个chunk没有其它代码,那么自然就达到了把runtime && manifest独立出去的目的了。

需要注意的是,如果你用了html-webpack-plugin来生成html页面,记得要把这runtime && manifest的 chunk 插入到html页面上,不然页面报错了可不怪我哦。

至此,由于runtime && manifest独立出去成一个chunk了,于是common chunk的命名便可以使用[chunkhash]了,也就是说,common chunk现在也能做到公共模块内容有更新了,才更新文件名;另一方面,这个独立出去的 runtime && manifest chunk,是每次 webpack 打包构建的时候都会更新了。

有必要把 manifest 从 runtime && manifest chunk 中独立出去吗?

是的,不用惊讶,的确是有这么一个骚操作。

把 manifest 独立出去的理由是这样的:manifest 独立出去后,runtime 的部分基本上就不会有变动了;到这里,我们就知道,runtime && manifest里实际上就是 manifest 在变;因此把 manifest 独立出去,也是进一步地利用浏览器缓存(可以把 runtime 的缓存保留下来)。

具体是怎么做的呢?主流有俩方案:

我试用过第二种方案,好使,但最终还是放弃了,为什么呢?

把 manifest 独立出去后,只剩下 runtime 的 chunk 的命名还是只能用[hash],而不能利用[chunkhash],这就导致我们根本没法利用浏览器缓存。后来,我又想出一个折衷的办法,连[hash]也不要了,直接写死一个文件名;这样的话,的确浏览器缓存就能保存下来了。但后来我还是反转了自己,这种方法虽然能留下浏览器缓存,却做不到“该去则去”。或许大家会有疑问,你不是说 runtime 不会变的吗,那留下缓存有什么关系呀?是的,在同一 webpack 环境下 runtime 的确不会变,但难保 webpack 环境改变后,这runtime会怎么样呀。比如说 webpack 的版本升级了、 webpack 的配置改了、loader & plugin 的版本升级了,在这些情况下,谁敢保证 runtime 永远不会变啊?这 runtime 一用错了过期的缓存,那很可能整个系统都会崩溃的啊,这个险我实在是冒不起,所以只能作罢。

不过我看了下Array-Huang/webpack-seedruntime && manifest chunk,也才 2kb 而已嘛,你们管好自己的强迫症和代码洁癖好吗?!

缓存问题杂项

模块id带来的缓存问题

webpack 处理模块(module)间依赖关系时,需要给各个模块定一个 id 以作标识。webpack 默认的 id 命名规则是根据模块引入的顺序,赋予一个整数(1、2、3……)。当你在源码中任意增添或删减一个模块的依赖,都会对整个
id 序列造成极大的影响,可谓是“牵一发而动全身”了。那么这对我们的浏览器缓存会有什么样直接的影响呢?影响就是会造成,各个chunk中都不一定有实质的变化,但引用的依赖模块id却都变了,这明显就会造成 chunk 的文件名的变动,从而影响浏览器缓存。

webpack 官方文档里推荐我们使用一个已内置进 webpack2 里的 plugin:HashedModuleIdsPlugin,这个 plugin 的官方文档在这里

webpack1 时代便有一个NamedModulesPlugin,它的原理是直接使用模块的相对路径作为模块的 id,这样只要模块的相对路径,模块 id 也就不会变了。那么这个HashedModuleIdsPlugin对比起NamedModulesPlugin来说又有什么进步呢?

是这样的,由于模块的相对路径有可能会很长,那么就会占用大量的空间,这一点是一直为社区所诟病的;但这个HashedModuleIdsPlugin是根据模块的相对路径生成(默认使用md5算法)一个长度可配置(默认截取4位)的字符串作为模块的 id,那么它占用的空间就很小了,大家也就可以安心服用了。

To generate identifiers that are preserved over builds, webpack supplies the NamedModulesPlugin (recommended for development) and HashedModuleIdsPlugin (recommended for production).

从上可知,官方是推荐开发环境用NamedModulesPlugin,而生产环境用HashedModuleIdsPlugin的,原因似乎是与热更新(hmr)有关;不过就我看来,仅在生产环境用HashedModuleIdsPlugin就行了,开发环境还管啥浏览器缓存啊,俺开 chrome dev-tool 设置了不用任何浏览器缓存的。

用法也挺简单的,直接加到plugin参数就成了:

plugins: {
  // 其它plugin
  new webpack.HashedModuleIdsPlugin(),  
}

由某些 plugin 造成的文件改动监测失败

有些 plugin 会生成独立的 chunk 文件,比如CommonsChunkPluginExtractTextPlugin(从js中提取出css代码段并生成独立的css文件) 。

这些 plugin 在生成 chunk 的文件名时,可能没料想到后续还会有其它 plugin (比如用来混淆代码的UglifyJsPlugin)会对代码进行修改,因此,由此生成的 chunk 文件名,并不能完全反映文件内容的变化。

另外,ExtractTextPlugin有个比较严重的问题,那就是它生成文件名所用的[chunkhash]是直接取自于引用该css代码段的 js chunk ;换句话说,如果我只是修改 css 代码段,而不动 js 代码,那么最后生成出来的css文件名依然没有变化,这可算是非常严重的浏览器缓存“该去不去”问题了。
2017-07-26 改动:改用[contenthash]便不会出现此问题,上见css部分

有一款 plugin 能解决以上问题:webpack-plugin-hash-output

There are other webpack plugins for hashing out there. But when they run, they don't "see" the final form of the code, because they run before plugins like webpack.optimize.UglifyJsPlugin. In other words, if you change webpack.optimize.UglifyJsPlugin config, your hashes won't change, creating potential conflicts with cached resources.

The main difference is that webpack-plugin-hash-output runs in the last compilation step. So any change in webpack or any other plugin that actually changes the output, will be "seen" by this plugin, and therefore that change will be reflected in the hash.

简单来说,就是这个webpack-plugin-hash-output会在 webpack 编译的最后阶段,重新对所有的文件取文件内容的 md5 值,这就保证了文件内容的变化一定会反映在文件名上了。

用法也比较简单:

plugins: {
  // 其它plugin
  new HashOutput({
    manifestFiles: 'webpack-runtime', // 指定包含 manifest 在内的 chunk
  }),
}

总结

浏览器缓存很重要,很重要,很重要,出问题了怕不是要给领导追着打。另外,这一块的细节特别多,必须方方面面都顾到,不然哪一方面出了纰漏就全局泡汤。

示例代码

诸位看本系列文章,搭配我在Github上的脚手架项目食用更佳哦(笑):Array-Huang/webpack-seedhttps://github.com/Array-Huang/webpack-seed)。

附系列文章目录(同步更新)

本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000010317802
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang
查看原文

赞 58 收藏 140 评论 32

认证与成就

  • 获得 58 次点赞
  • 获得 7 枚徽章 获得 1 枚金徽章, 获得 2 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • MEAN Web开发

    一本介绍使用 JavaScript 进行 Web 全本开发的书籍。

注册于 2011-05-30
个人主页被 2.1k 人浏览