陌上

陌上 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

陌上 赞了文章 · 2020-01-03

vue-cli 打包后提交到线上出现 "Uncaught SyntaxError:Unexpected token <" 报错

前言: 项目使用vue-cli版本2.9.3 ,vue-router使用webpackChunkName实现按需加载.
图片描述

BUG描述:该报错在项目上线一段时间后,有用户反映页面无法正常游览 (后面以问题1/问题2区分)

问题1.导航点击无法正常跳转,刷新后恢复正常. console打印:Error:Loading chunk {n} failed.

报错截图图片描述

问题2.页面全白,并且刷新仍然无效. console打印:Uncaught SyntaxError:Unexpected token <

报错截图: 图片描述

经过一番折腾,初步定位问题1在经过build/webpack.prod.conf.jschunkhash打包后的JS文件hash值会有变更,因为每次更新代码到线上都会删除旧的dist目录,将最新的dist目录copy上传提供后台更新. 在更新代码的这个过程用户停留在页面上,当用户在更新完后重新操作就会导致报错

图片描述

问题1解决方法:捕获路由报错. (思路来源:https://segmentfault.com/a/11... )

routers.onError((err) => {
  const pattern = /Loading chunk (\d)+ failed/g;
  const isChunkLoadFailed = err.message.match(pattern);
  if (isChunkLoadFailed) {
    let chunkBool = sessionStorage.getItem('chunkError');
    let nowTimes = Date.now();
    if (chunkBool === null || chunkBool && nowTimes - parseInt(chunkBool) > 60000) {//路由跳转报错,href手动跳转
      sessionStorage.setItem('chunkError', 'reload');
      const targetPath = routers.history.pending.fullPath;
      window.location.href = window.location.origin + targetPath;
    }else if(chunkBool === 'reload'){ //手动跳转后依然报错,强制刷新
      sessionStorage.setItem('chunkError', Date.now());
      window.location.reload(true);
    }
  }
})

问题2在Network查看js文件加载的时候发现某个js文件Response Headercontent-type异常,正常情况返回content-type: application/javascript. 但是有一个js响应的内容为HTML, js无法识别<符号导致抛出报错
图片描述
图片描述

问题2解决方法: 经过问题排查发现,vue-cli默认build后的文件名格式为js/[name].[chunkhash].js,每次npm run build后有改动的文件hash值都会改变,上传后Nginx无法找到最新上传的文件,所以返回了默认index.html里的内容,我们的文件后缀名是.js自然无法识别<html>这种标签符号,导致console抛出Uncaught SyntaxError:Unexpected token <,我尝试修改build/webpack.prod.conf.jsoutput输出文件名格式,目前问题已得到解决
图片描述

查看原文

赞 24 收藏 13 评论 15

陌上 收藏了文章 · 2020-01-03

vue-cli 打包后提交到线上出现 "Uncaught SyntaxError:Unexpected token <" 报错

前言: 项目使用vue-cli版本2.9.3 ,vue-router使用webpackChunkName实现按需加载.
图片描述

BUG描述:该报错在项目上线一段时间后,有用户反映页面无法正常游览 (后面以问题1/问题2区分)

问题1.导航点击无法正常跳转,刷新后恢复正常. console打印:Error:Loading chunk {n} failed.

报错截图图片描述

问题2.页面全白,并且刷新仍然无效. console打印:Uncaught SyntaxError:Unexpected token <

报错截图: 图片描述

经过一番折腾,初步定位问题1在经过build/webpack.prod.conf.jschunkhash打包后的JS文件hash值会有变更,因为每次更新代码到线上都会删除旧的dist目录,将最新的dist目录copy上传提供后台更新. 在更新代码的这个过程用户停留在页面上,当用户在更新完后重新操作就会导致报错

图片描述

问题1解决方法:捕获路由报错. (思路来源:https://segmentfault.com/a/11... )

routers.onError((err) => {
  const pattern = /Loading chunk (\d)+ failed/g;
  const isChunkLoadFailed = err.message.match(pattern);
  if (isChunkLoadFailed) {
    let chunkBool = sessionStorage.getItem('chunkError');
    let nowTimes = Date.now();
    if (chunkBool === null || chunkBool && nowTimes - parseInt(chunkBool) > 60000) {//路由跳转报错,href手动跳转
      sessionStorage.setItem('chunkError', 'reload');
      const targetPath = routers.history.pending.fullPath;
      window.location.href = window.location.origin + targetPath;
    }else if(chunkBool === 'reload'){ //手动跳转后依然报错,强制刷新
      sessionStorage.setItem('chunkError', Date.now());
      window.location.reload(true);
    }
  }
})

问题2在Network查看js文件加载的时候发现某个js文件Response Headercontent-type异常,正常情况返回content-type: application/javascript. 但是有一个js响应的内容为HTML, js无法识别<符号导致抛出报错
图片描述
图片描述

问题2解决方法: 经过问题排查发现,vue-cli默认build后的文件名格式为js/[name].[chunkhash].js,每次npm run build后有改动的文件hash值都会改变,上传后Nginx无法找到最新上传的文件,所以返回了默认index.html里的内容,我们的文件后缀名是.js自然无法识别<html>这种标签符号,导致console抛出Uncaught SyntaxError:Unexpected token <,我尝试修改build/webpack.prod.conf.jsoutput输出文件名格式,目前问题已得到解决
图片描述

查看原文

陌上 收藏了文章 · 2020-01-01

免费API接口汇总(不定时更新)

声明:如果API涉及到调用次数限制或收费问题,请自行辨别处理;另外,有的API接口并非官方以官网渠道公示,仅适用于开发调试使用。

平台

阿里云市场
https://market.aliyun.com/data
聚合数据
https://www.juhe.cn/docs
京东万象
https://wx.jdcloud.com/api
神箭手
https://www.shenjianshou.cn
阿凡达数据
https://www.avatardata.cn/Docs
apishop
https://www.apishop.net
iDataAPI
http://www.idataapi.cn
HaoService
http://www.haoservice.com

地图

高德地图
https://lbs.amap.com
百度地图
http://lbsyun.baidu.com
腾讯地图
https://lbs.qq.com
搜狗地图
http://map.sogou.com/api

天气

丫丫天气
http://www.yytianqi.com
和风天气
http://www.heweather.com
心知天气
https://www.seniverse.com
彩云天气
http://wiki.swarma.net/index....
小米天气
https://github.com/jokermonn/...
魅族天气
https://github.com/jokermonn/...
免费天气查询
https://www.sojson.com/blog/3...

快递

快递100
https://www.kuaidi100.com/ope...
快递网
http://www.kuaidi.com/openapi...
TrackingMore(国际快递)
https://www.trackingmore.com/...

技术

开源中国
https://www.oschina.net/openapi
CNode
https://cnodejs.org/api
V2EX
https://www.v2ex.com/p/7v9TEc53
Ruby China
https://www.yuque.com/ruby-ch...
diycode
https://www.diycode.cc/api
Coding
https://open.coding.net
玩Android
http://www.wanandroid.com/blo...

电影

豆瓣电影
https://github.com/jokermonn/...
时光网
https://github.com/jokermonn/...

视频

开眼
https://github.com/jokermonn/...

图片

干货集中营
https://gank.io/api
来福岛
http://www.laifudao.com/api.asp
图虫
https://github.com/jokermonn/...

音乐

QQ音乐
https://y.qq.com/m/api/api.html
QQ空间音乐
https://www.sojson.com/api/qq...
百度音乐
https://my.oschina.net/skiden...
豆瓣一刻
https://github.com/ZongweiBai...

文章

今日头条
https://github.com/jokermonn/...
每日一文
https://github.com/jokermonn/...
知乎专栏
https://github.com/TonnyL/Zhi...
知乎日报
https://github.com/izzyleung/...

翻译

有道词典
https://github.com/jokermonn/...
金山词霸
https://github.com/jokermonn/...

壁纸

360壁纸
https://github.com/jokermonn/...
安卓壁纸
https://github.com/jokermonn/...

域名

域名备案查询
https://www.sojson.com/api/be...
公安备案查询
https://www.sojson.com/api/be...

AI

百度AI市场
http://ai.baidu.com/market
智能机器人
https://www.sojson.com/api/se...

日历

农历查询
https://www.sojson.com/api/lu...

金融

通联数据
https://m.datayes.com
开彩网
http://www.opencai.net/apifree

其他

小白接口
https://www.okayapi.com

查看原文

陌上 赞了文章 · 2020-01-01

免费API接口汇总(不定时更新)

声明:如果API涉及到调用次数限制或收费问题,请自行辨别处理;另外,有的API接口并非官方以官网渠道公示,仅适用于开发调试使用。

平台

阿里云市场
https://market.aliyun.com/data
聚合数据
https://www.juhe.cn/docs
京东万象
https://wx.jdcloud.com/api
神箭手
https://www.shenjianshou.cn
阿凡达数据
https://www.avatardata.cn/Docs
apishop
https://www.apishop.net
iDataAPI
http://www.idataapi.cn
HaoService
http://www.haoservice.com

地图

高德地图
https://lbs.amap.com
百度地图
http://lbsyun.baidu.com
腾讯地图
https://lbs.qq.com
搜狗地图
http://map.sogou.com/api

天气

丫丫天气
http://www.yytianqi.com
和风天气
http://www.heweather.com
心知天气
https://www.seniverse.com
彩云天气
http://wiki.swarma.net/index....
小米天气
https://github.com/jokermonn/...
魅族天气
https://github.com/jokermonn/...
免费天气查询
https://www.sojson.com/blog/3...

快递

快递100
https://www.kuaidi100.com/ope...
快递网
http://www.kuaidi.com/openapi...
TrackingMore(国际快递)
https://www.trackingmore.com/...

技术

开源中国
https://www.oschina.net/openapi
CNode
https://cnodejs.org/api
V2EX
https://www.v2ex.com/p/7v9TEc53
Ruby China
https://www.yuque.com/ruby-ch...
diycode
https://www.diycode.cc/api
Coding
https://open.coding.net
玩Android
http://www.wanandroid.com/blo...

电影

豆瓣电影
https://github.com/jokermonn/...
时光网
https://github.com/jokermonn/...

视频

开眼
https://github.com/jokermonn/...

图片

干货集中营
https://gank.io/api
来福岛
http://www.laifudao.com/api.asp
图虫
https://github.com/jokermonn/...

音乐

QQ音乐
https://y.qq.com/m/api/api.html
QQ空间音乐
https://www.sojson.com/api/qq...
百度音乐
https://my.oschina.net/skiden...
豆瓣一刻
https://github.com/ZongweiBai...

文章

今日头条
https://github.com/jokermonn/...
每日一文
https://github.com/jokermonn/...
知乎专栏
https://github.com/TonnyL/Zhi...
知乎日报
https://github.com/izzyleung/...

翻译

有道词典
https://github.com/jokermonn/...
金山词霸
https://github.com/jokermonn/...

壁纸

360壁纸
https://github.com/jokermonn/...
安卓壁纸
https://github.com/jokermonn/...

域名

域名备案查询
https://www.sojson.com/api/be...
公安备案查询
https://www.sojson.com/api/be...

AI

百度AI市场
http://ai.baidu.com/market
智能机器人
https://www.sojson.com/api/se...

日历

农历查询
https://www.sojson.com/api/lu...

金融

通联数据
https://m.datayes.com
开彩网
http://www.opencai.net/apifree

其他

小白接口
https://www.okayapi.com

查看原文

赞 33 收藏 28 评论 1

陌上 发布了文章 · 2019-12-21

Express + Three.js 抽奖程序

抽奖程序

年会抽奖程序,3D 球体抽奖,支持奖品信息配置,参与抽奖人员信息Excel导入,抽奖结果Excel导出

github地址: https://github.com/moshang-xc/lottery

技术

技术:Node + Express + Three.js

后台通过Express实现

前端抽奖界面通过Three.js实现 3D 抽奖球,引用了Three.js的官方 3D 示例

功能描述:

  1. 可将抽奖结果进行保存实时下载到 excel 中
  2. 已抽取人员不在参与抽取,抽中的人员不在现场可以重新抽取
  3. 刷新或者关掉服务器,会保存当前已抽取的数据,不会进行数据重置,只有点击界面上的重置按钮,才能重置抽奖数据
  4. 每次抽取的奖品数目可配置
  5. 抽取完所有奖品后还可以继续抽取特别奖(例如:现在抽取红包,追加的奖品等),此时默认一次抽取一个

预览

lottery.gif

f.jpg

s.jpg

t.jpg

安装

git clone https://github.com/moshang-xc/lottery.git

cd lottery

# 服务端插件安装
cd server
npm install

# 前端插件安装
cd ../product
npm install

# 打包
npm run build

# 运行
npm run serve

# 开发调试
npm run dev

目录结构

Lottery
├── product
│   ├── src
│   │   ├── lottery
│   │   │   └── index.js
│   │   ├── lib
│   │   ├── img
│   │   ├── css
│   │   └── data
│   ├── package.json
│   └── webpack.config.js
├── server
│   ├── config.js
│   ├── server.js
│   └── package.js
  1. product 为前端页面目录
  2. package.josn web 项目配置文件
  3. webpack.config.js 打包配置文件
  4. server 为服务器目录
  5. config 为奖品信息的配置文件

配置信息

  1. 抽奖用户信息,按指定的格式填写在server/data/user.xlsx文件中,不能修改文件名
  2. 奖品的配置信息填写在server/config.js文件中,不能修改文件名
// 奖品信息,第一项为预留项不可修改,其他项可根据需要修改
let prizes = [{
        type: 0,
        count: 1000,
        title: '特别奖',
        img: ''
    }, {
        type: 1,
        count: 1,
        title: '华为Mate 20X',
        img: '../img/huawei.png'
    }
    ...
];

/**
 * 一次抽取的奖品个数
 * 顺序为:[特别奖,一等奖,二等奖,三等奖,四等奖,五等奖]
 */
const EACH_COUNT = [1, 1, 1, 1, 1, 5];
// 公司名称,用于显示在抽奖名单的title部分
const COMPANY = 'MoShang';
查看原文

赞 25 收藏 15 评论 6

陌上 发布了文章 · 2019-09-09

JS浮点数也没那么复杂

前言

工作中经常会遇到浮点数的操作,所以对一些常见的"bug"比如浮点数的精度丢失,0.1+0.2!==0.3的问题也有所了解,但是都不深入,对于Number的静态属性MAX_SAFE_INTEGER知道它的存在,但是并不知道为什么这样定义范围。刚好最近有空就带着这些疑惑深入的了解了一下,发现网上也有一些文章,有对这些知识的梳理,要么是太晦涩,需要一定的基础才能看懂,要么就是太散,没有全面的进行分析。所以想着,写一篇这方面的文章,一是对自己学习结果的总结和检验,另一方面通过通俗易懂的方式分享给跟我一样有困惑的同学,大家互相学习,共同进步,有问题欢迎指正。

本文首先会介绍一些概念,然后深入分析IEEE浮点数精度丢失的问题,最后解释为什么最大安全数MAX_SAFE_INTEGER的取值是$2^{53} - 1$。

浮点数

首先来介绍一下浮点数,JavaScript中所有的数字,无论是整数还是小数都只有一种类型Number。遵循 IEEE 754 的标准,在程序内部Number类型实质是一个64位固定长度的浮点数,也就是标准的double双精度浮点数。

IEEE浮点数格式使用科学计数法表示实数。科学计数法把数字表示为尾数(mantissa),和指数 (exponent)两部分。比如 25.92 可表示为 $ 2.592\times10^1 $,其中2.592是尾数,值 $10^1$ 是指数。*指数的基数为 10,指数位表示小数点移动多少位以生成尾数。每次小数点向前移动时,指数就递增;每次小数点向后移动时,指数就递减。再比如 $ 0.00172 $可表示为 $1.72\times10^-3$。科学计数法对应到二进制里也是一个意思。

计算机系统使用二进制浮点数,这种格式使用二进制科学计数法的格式表示数值。数字按照二进制格式表示,那么尾数指数都是基于二进制的,而不是十进制,例如 $1.0101\times2^2$。 在二制里表示,1.0101 左移两位后,生成二进制值 101.01,这个值表示十进制整数 5,加上小数$(0\times2^{-1}+1\times2^{-2}=0.25)$,生成十进制值 5.25。

浮点数的组成

前面已经介绍了IEEE浮点数使用科学计数法表示实数,IEEE浮点数标准会把一个二进制串分成3部分,分别用来存储浮点数的尾数阶码以及符号位。其中

  • 符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数
  • 指数位E:中间的 11 位存储指数(exponent),用来表示次方数
  • 尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零,二进制默认整数位为1舍去
指数表示浮点数的指数部分,是一个无符号整数,因为长度是11位,取值范围是 0~2047。因为指数值可以是正值,也可以是负值,所以需要通过一个偏差值对它进行置偏,即指数的真实值=指数部分的整数—偏差值。对于64位浮点数,取中间值,则偏差值=1023,[0,1022]表示为负,[1024,2047] 表示为正

通过公式计算来表示浮点数的值话,如下所示:

$$ \begin{gather} V = (-1)^S\times2^{E-1023}\times(1.M) \end{gather} $$

公式看起来可能还是有点抽象,那我们拿一个具体的十进制数字8.75来举例,分析对应公式中各变量的值。首先将8.75转成二进制,其中整数部分8对应的二进制为1000。小数转二进制具体步骤为:将该数字乘以2,取出整数部分作为二进制表示的第1位;然后再将小数部分乘以2,将得到的整数部分作为二进制表示的第2位;以此类推,直到小数部分为0。 故0.75转二进制的过程如下:

0.75 * 2 = 1.5 // 记录1
0.5 * 2 = 1 // 记录1
// 0.75对应的二进制为11

最终8.75对应的二进制为1000.11,通过科学计数法表示为$1.00011\times2^3$,其中舍去1后,M=00011E = 3。故E=3+1023=1026。最终的公式变成:$8.75 = (-1)^0\times2^{1026-1023}\times(1.00011)$。

在尾数的定义上,有一个概念超出的部分自动进一舍零不知道大家有没有注意到,IEEE754浮点数的舍入规则与我们了解的四舍五入相似,但也存在一些区别。

IEEE754规范的舍入规则

IEEE754采用的浮点数舍入规则有时被称为最近偶数

  • 首先判断精度损失(优先级最高),向上和向下都计算,精度损失最小者获胜,也就是"最近"原则.
  • 如果距离相等(即精度损失相等),那么将执行偶数判断,偶数胜出.

我们来举个例子,假定二进制小数1.01101,舍入到小数点后4位。首先往上和往下损失的精度都是0.00001(二进制),这时候根据第二条规则保证舍入后的最低有效位是偶数,所以执行向下舍入,结果为1.0110。如果将其舍入到小数点后2位,则执行向上舍入,精度丢失0.00011,向下舍入,精度丢失0.00101,所以结果为1.10。再来思考下看看下面的这些例子,原因后面会解释。

 Math.pow(2,53) // 9007199254740992
 Math.pow(2,53) + 1 // 9007199254740992
 Math.pow(2,53) + 2 // 9007199254740994
 Math.pow(2,53) + 3 // 9007199254740996

了解了浮点数的组成,以及尾数的舍入规则后,我们就来看看为什么浮点数会存在精度丢失的问题。

精度丢失问题

通过浮点数的尾数接受,也许机智的你就已经发现了为什么会丢失精度。就是因为舍入规则的存在,才导致了浮点数的精度丢失。

浮点数的组成部分,我们已经了解了如何将一个十进制的小数转成二进制。不知道大家有没有注意到我们只说了将该数字乘以2,取出整数部分作为二进制表示的第1位,以此类推,直到小数部分为0,但还存在另一种特殊情况就是小数部分出现循环,无法停止,这个时候用有限的二进制位就无法准确表示一个小数,这也就是精度丢失的原因了。

我们按照乘以 2 取整数位的方法,把 0.1 表示为对应二进制:

// 0.1二进制演算过程如下
0.1 * 2 = 0.2 // 取整数位 记录0
0.2 * 2 = 0.4 // 取整数位 记录00
0.4 * 2 = 0.8 // 取整数位 记录000
0.8 * 2 = 1.6 // 取整数位 记录0001
0.6 * 2 = 1.2 // 取整数位 记录00011
0.2 * 2 = 0.4 // 取整数位 记录000110
0.2 * 2 = 0.4 // 取整数位 记录0001100
0.4 * 2 = 0.8 // 取整数位 记录00011000
0.8 * 2 = 1.6 // 取整数位 记录000110001
0.6 * 2 = 1.2 // 取整数位 记录0001100011
... // 如此循环下去
0.1 = 0.0001100110011001...

最终我们得到一个无限循环的二进制小数 0.0001100110011001...,按照浮点数的公式,$0.1=1.100110011001..\times2^{-4}$,$E=1023-4=1019$,舍去首位的1,通过舍入规则取52位M=00011001100...11010,转化成十进制后为 0.100000000000000005551115123126,因此就出现了精度丢失。同时通过上面的转化过程可以看到0.2,0.4,0.6,0.8都无法精确表示,0.1 到 0.9 的 9 个小数中,只有 0.5 可以用二进制精确的表示。

让我们继续看个问题:

0.1 + 0.2 === 0.3 // false
var s = 0.3 
s === 0.3 // true

为什么0.3 === 0.3 而 0.1 + 0.2 !== 0.3

// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

// 转成十进制正好是 0.30000000000000004

可以看出,因为0.1和0.2都无法被精确表示,所以在进行加法运算之前,0.1和0.2的精度就已经丢失了。 浮点数的精度丢失在每一个表达式,而不仅仅是表达式的求值结果。

我们可以拿个简单的数学加法来类比一下,计算1.7+1.6的结果,四舍五入保留整数:

1.7 + 1.6 = 3.3 = 3

换种方式,先进行四舍五入,再进行求值:

1.7 + 1.6 = 2 + 2 = 4

通过两种运算,我们得到了两个结果3 和4。同理,在我们的浮点数运算中,参与运算的两个数 0.1 和 0.2 精度已经丢失了,所以他们求和的结果已经不是 0.3了。

既然0.3无法精确表示为什么又能得到0.3呢

let i = 0.3;
i === 0.3 // true

为什么x=0.3能得到0.3

首先,你看到的0.3并不是你认为的0.3。因为尾数的固定长度是 52 位,再加上省略的一位,最多可以表示的数是 $2^{53}=9007199254740992$,这与16个十进制位表示的精度十分接近。

例如,0.3000000000000000055与0.30000000000000000051是相同的都是0.1,这两个数按照64位双精度浮点格式存储与0.1是一样的。

0.3000000000000000055 === 0.3 // true
0.3000000000000000055 === 0.3000000000000000051 // true

由上面可以看到,在双精度的浮点下,整数部分+小数部分的位数一共有 17 位。

当尾数长度是 16时,可以使用 toPrecision(16) 来做精度运算,超过的精度会自动做凑整处理。例如:

(0.10000000000000000555).toPrecision(16) // 返回 0.1

(0.1).toPrecision(21) // 0.100000000000000005551

为什么[-(2^53-1), 2^53-1]为安全的整数区域

在JavaScript中Number有两个静态属性MAX_SAFE_INTEGERMIN_SAFE_INTEGER,分别表示最大的安全的整数型数字 ($2^{53} - 1$)和最小的安全的整数型数字 ($-(2^{53} - 1)$)。

安全的整数意思就是说在此范围内的整数和双精度浮点数是一一对应的,不会存在一个整数有多个浮点数表示的情况,当然也不会存在一个浮点数对应多个整数的情况。那这两个数值是怎么来的呢?

我们先不考虑符号位和指数位,浮点数的尾数位为52位,不包括省略的1,则可以表示的最大的二进制小数为1.11111...(52个1),推算一下这个数的值,其中整数位为1对应的十进制的值为$2^0\times1=1$,小数位的值为$1/2+1/4+1/8...$是一个公比为$\frac{1}{2}$的等比数列,我们知道等比数列的求和公式为(不会的回去翻翻高中课本)

$$ S_n = \frac{a_nq-a_1}{q-1},(q\neq1) $$

根据求和公式算出小数位的结果接近0.9999999999999998,加起来就是1.9999999999999998无限的接近2。

再来看指数位,前面已经说过指数位表示小数点移动多少位以生成尾数,每次小数点向前移动时,指数就递增,当指数递增到52时,这时取满了小数位,对应的值为2^52*(1.111111...(52个))对应的十进制整数数为无限的接近$2\times2^{52}$即为$2^{53} - 1$。

同时指数位为23时也能明确的表明一个整数,对应的表达式为$2^{53}\times1.0$,那最大的安全整数明明可以到$2^{53}$,不是上面所说的$2^{53} - 1$呀。不要着急,我们继续往下看,我们来看看$2^{53} + 1$的值。首先将其转成对应的二进制,这时的尾数为1.000...(52个0)1,由于bit-64浮点数只能存储52位尾数,最后一位1,根据IEEE浮点数舍入规则,向下舍入,此时丢失了精度。最后$2^{53}$和这两个数$2^{53} + 1$按照64位双精度浮点格式存储结果是一样的。

Math.pow(2,53) // 9007199254740992
Math.pow(2,53) === Math.pow(2,53) + 1  // true

前面说过安全的整数意思就是说在此范围内的整数和双精度浮点数是一一对应的,而此时不是一一对应的关系,故 $[-(2^{53} - 1), 2^{53} - 1]$为安全的整数区域。

最后考虑符号位的话最小的安全整数就是$-(2^{53} - 1)$。

我们继续,上面说的只是安全区域,并不代表浮点数能精确存储的最大整数就是$-(2^{53} - 1)$,这是两个概念。我们接下来看看$2^{53} + 2$的64位双精度浮点格式存储结果,这时的尾数是1.000..(51个0)1,可以完全存储没有丢失精度,继续往下看$2^{53} + 3$,对应的二进制尾数为1.00..(51个0)11,根据舍入规则,向上舍入,结果为1.00..(50个0)10。也就对应了上面提到的结果:

Math.pow(2,53) + 1 // 9007199254740992
Math.pow(2,53) + 2 // 9007199254740994
Math.pow(2,53) + 3 // 9007199254740996

有兴趣的话,还可以继续研究,指数位为54的情况,以此类推。由此可以看出,IEEE能表示的整数的最大值不止$2^{53} - 1$,超过这个值也可以表示,只是需要注意精度的问题,使用的时候需要小心。

后续

对于浮点数的缺陷和对应的解法,可以看看这篇文章JavaScript 浮点数陷阱及解法

附录

JavaScript 浮点数陷阱及解法

代码之谜

IEEE754规范的舍入方案

查看原文

赞 1 收藏 0 评论 1

陌上 赞了文章 · 2019-08-22

从0到1:PostCSS 插件开发最佳实践

本文原始来源:http://devework.com/postcss-p...。转载请提供原始来源,谢谢!

clipboard.png

前阵子为了满足工作上的一个需求开发了一个PostCSS 插件,后来也将这个插件提交给PostCSS 官方并得到认可。在这篇文章中笔者将记录开发过程中遇到的一些问题,且斗胆将之称为“最佳实践”,希望对有兴趣尝试PostCSS 插件开发的您有所帮助。

简介篇

开发成果展示

首先先上成果:https://github.com/Jeff2Ma/postcss-lazyimagecss (欢迎给个star 哦~)

postcss-lazyimagecss 插件实现的功能是为 CSS 中的background-image 对应的图片自动添加widthheight 属性。简单形象化的效果展示如下:

/* Input ./src/index.css */
.icon-close {
    background-image: url(../slice/icon-close.png); //icon-close.png - 16x16
}

.icon-new {
    background-image: url(../slice/icon-new@2x.png); //icon-new@2x.png - 16x16
}

/* Output ./dist/index.css */
.icon-close {
    background-image: url(../slice/icon-close.png);
    width: 16px;
    height: 16px;
}

.icon-new {
    background-image: url(../slice/icon-new@2x.png);
    width: 8px;
    height: 8px;
    background-size: 8px 8px;
}

为什么重复造一个轮子

开发这个PostCSS 插件的起因是原先工作流中使用的gulp-lazyimagecss 插件在加入SourceMap 功能后运行不正常,多次尝试修复均告失败。后来笔者想到,PostCSS 本身天然支持SourceMap,那如果将这个功能开发成PostCSS 插件岂不是也完美支持SourceMap 了?

于是笔者便在gulp-lazyimagecss 的基础上开发出了这么一个轮子。在此也感谢原开发者hzlzhlittledu 的大力帮助与支持。对笔者而言,更像是站在巨人的肩膀上开发出来这个插件。

准备篇

原理

关于PostCSS 的原理,官方有这么一个图:

clipboard.png

简单解释,PostCSS 会将上一步传入的 CSS 按照一条条样式规则(rule)进行解析(Parser)得到一个节点树;然后借助一系列插件在节点树上进行转换操作,并最终通过Stringifier 进行拼接。source map则记录了前后的对应关系。

当然,在实际的开发中其实不必深究原理,最重要的是看其提供的API 来调用即可。

工欲善其事必先利其器

开发一个PostCSS 插件也是开发一个Node 模块,想到后面要发布到NPM 跟PostCSS 官方,那么作为一个开源项目的可维护性、可扩展性也是很重要的。因此在进入正式的开发之前,笔者做了如下的工作:

1、配置 editorconfig

editorconfig 作为一套统一代码格式的解决方案,已经在团队不少项目中使用,其很好地解决了因为团队协作中因不同代码编辑器及不同的代码习惯产生的潜在风险。这里是最终的配置文件

2、基础的开发工作流

在整个开发插件过程前,笔者根据需求配了个基于Gulp 的开发工作流,主要配备如下功能(任务):

  • 代码质量监控ESlint

优秀的开源代码必然是有着标准化的JavaScript 代码风格,因此在整个开发过程中借助ESlint 来严格控制自己的代码质量。这里是本项目的ESlint 配置文件。

var eslint = require('gulp-eslint');
gulp.task('lint', function () {
    return gulp.src(files)
        .pipe(eslint())
        .pipe(eslint.format())
        .pipe(eslint.failAfterError());
});
  • 基础的CSS 转换

这个任务其实就是本PostCSS 插件实现的功能,之所以在开发过程中也要配置是为了下面的单元测试任务的调用。

  • 单元测试

秉承TDD(测试驱动开发)的开发理念,单元测试的任务是必不可少的。

gulp.task('test', function () {
    return gulp.src('test/*.js', { read: false })
        .pipe(mocha({ timeout: 1000000 }));
}); 
  • watch 任务

gulp watch 任务是上面任务的集体调用,实现的功能是在开发过程中,每当按下保存键就自动运行ESlint 代码质量监控及进行单元测试任务。有效保障了整个开发过程中的质量。

clipboard.png

3、托管到 Github 并配置Travis-ci 持续集成

整个开发过程使用Github 托管源代码并通过Travis-ci 持续集成。PostCSS 官方建议最低需要支持Node.js 0.12 的版本,所以整个Travis-ci 的配置文件如下:

sudo: false
language: node_js
node_js:
  - "0.12"
  - "4"
  - "5"
  - "6"
  - "stable"
before_script:
  - npm install -g mocha

相应的在Travis-ci 管理后台配置push 操作作为动作钩子,这样每次有commit push 上去就会自动进行测试并在log 上展示出结果:

clipboard.png

开发篇

从最小开始

一个PostCSS 插件最基础的构成如下:

var postcss = require('postcss');
module.exports = postcss.plugin('PLUGIN_NAME', function (opts) {
    opts = opts || {};
    // 传入配置相关的代码
    return function (root, result) {
        // 转化CSS 的功能代码
    };
});

然后就是不同的需求情况来决定是否引入第三方模块,是否有额外配置项,然后在包含root,result 的匿名函数中进行最为核心的转换代码功能编写。

root(css),rule, nodes, decl, prop, value

如本文一开头的PostCSS 原理解析,CSS 文件在经过Parser 转化后的递归单个子单位可以归为如下:

  • root(css) :也是整个CSS 代码段,包含多个rule。

  • rule: 包含一个CSS class 范围内的代码段

.icon-close {
    background-image: url(../slice/icon-close.png);
    font-size: 14px;
}
  • nodes: 代指rule 中{}中间的多个 decl 部分。

  • decl: 单行CSS ,即有属性与值的部分

background-image: url(../slice/icon-close.png);
  • prop,value

相应的CSS 属性与值,如上面 propbackground-image,valueurl(../slice/icon-close.png)

伪代码实现

根据postcss-lazyimagecss 插件要实现的内容,涉及到CSS 转化的有如下情景:

  • 增加 width 属性及获取到真实值

  • 增加 height 属性及获取到真实值

  • 二倍图情况下增加 background-size 属性并计算出值

结合上一小节,可以先写出如下简洁版伪代码:

css.walkRules(function (rule) { // 遍历所有 CSS
    rule.walkDecls(/^background(-image)?$/, function (decl) { // 遍历每条 CSS 规则,找出目标 rule
        // 一些传参等代码
        nodes.forEach(function (node) { // 遍历其它 rules
            ...
        });

        ... // 其它代码实现,如找出图片真实width 等

        rule.append({prop: 'width', value: valueWidth}); // 在该decl 追加width 属性
    });
});

细化代码

接下来就是考虑不同情况增加一些逻辑判断:

  • 判断url 中是否为网络地址或Base64 的data 形式:imageRegex.exec(value).indexOf('data:')

  • 判断该rule 下是否已经有width 等属性,在nodes 循环中:

if (node.prop === 'width') {
    CSSWidth = true;
}
  • 判断2倍图图片宽高是否为偶数:

value.indexOf('@2x') > -1 && (info.width % 2 !== 0 || info.height % 2 !== 0

再具体的不再详述,完整的代码实现可以见这里

难点解决

postcss-lazyimagecss 插件使用了第三方模块fast-image-size 来进行图片数据(文件类型、宽高)的获取,大大提高了开发效率。然而在寻找图片绝对路径的这个实现上还是绕了不少弯路。

插件的思路是需要获取CSS 中background-image属性对应值中url()的相对图片路径,以此来找到图片的绝对路径,之后用fast-image-size 模块获取到相应的数据。

然而在一些特殊情况并不能准确找到绝对路径。

在CSS 预处理器(如Less 或Sass)中,常借助@import来组件化CSS 代码,然而在层层@import 下路径可能已经被产生变化。举个例子,有如下结构:

.
├── css
├── html
├── img
│   └── icon.png
└── scss
    ├── index.scss
    └── second
        └── _import.scss

上面的文件树中展示的 scss/index.scss@import 了二级目录下的 _import.scss,在_import.scss中有一个类需要用到img/icon.png

因为同时也配置了local server(以上面的./目录作为server 的根目录),那么在 url 中可以写成../../img/icon.png../img/icon.png,甚至写成../../../../../img/icon.png(N个../)——这些情况下Sass 编译后的index.css 均可正常读取。原因相信也知道,因为root url的存在,上面的路径写法均相当于/img/icon.png

在这个情况下于用户而言是感受不到错误的,但在插件中可就找不到真实绝对路径了。笔者对于这个情况是采用了如下方式进行解决:

借助Node.js 中的fs.existsSync 函数检测绝对路径对应的文件是否存在。第一次为正常fs.existsSync,如果找到就跳出;如果没有则先对路径的字符串执行replace('../', '');然后再次执行fs.existsSync。如果两次均没有找到则在终端进行提示,但这种情况下并不会报错破坏进程的运行。

function fixAbsolutePath(dir, relative) {
    // find the first time
    var absolute = path.resolve(dir, relative);

    // check if is a image file
    var reg = /\.(jpg|jpeg|png|gif|svg|bmp)\b/i;
    if (!reg.test(absolute)) {
        pluginLog('Not a image file: ', absolute);
        return;
    }

    if (!fs.existsSync(absolute) && (relative.indexOf('../') > -1)) {
        relative = relative.replace('../', '');
        // find the second time
        absolute = path.resolve(dir, relative);
    }

    return absolute;
}

不敢说这是一种最好的处理方式,但至少是一种可行的处理方式。

单元测试

单元测试上采用Mocha 测试工具, should.js 做断言库。在笔者看来,结合TDD 进行开发,单元测试仅作为一种开发的辅助手段,规避开发过程中一些产生致命的报错。本文不展开如何写单元测试,具体实现可点击这里

优化篇

在Postcss 官方Github Repo,有一个Plugin Guidelines。对于其提倡的“Do one thing, and do it well” 深感认同,因此在基本完成插件功能后笔者又做了如下优化工作。

更友好的log 提示

官方其实是建议用内置的result.warn来代替console.logconsole.warn来展示log 信息(原因据说是一些PostCSS 处理器会忽略这类console log 输出)。不过笔者尝试后发现官方函数下提示的信息会非常长,后面采用了借助chalk 模块封装了console.log的形式增加了高亮态信息展示。

锦上添花

“找不到图片文件”的场景处理

用户在写CSS 代码的时候,background-image 的url 可能会有如下情况:

  • 输入的是目录

  • 输入的非图片路径

  • 输入了一半就保存了

  • 根本就是瞎输入

场景很多,但对于插件而言仅仅是能否找到与否的结果。在处理这些错误场景的情况下也给出的细分到“File does not exist” 或 “Not a image file”的情况,让这类错误提醒更加友好一些。

提示二倍图不正确

如果用户引用的二倍图(类似xxx@2x.png)的宽度高度为非偶数的话,也会有相应的提醒。

以上的报错提示在实际运行效果如下:

clipboard.png

英文版 README

PostCSS 官方建议是README.md用英文写,其余语种采用类似README.zh.md的方式。

维护一份 changelog

按照建议,也将更新历史等数据放在了一个名为CHANGELOG.md文件上,并采用语义化的版本号

其它

根据自己的开发习惯,在Github 上的Repo 也放置了一份LICENSE 文件。

发布篇

发布到NPM 官方

发布到NPM 官方的步骤在这里就不再详述。仅分享一个不错的版本号增加方式(告别packup.json 的手动改版本数字)。

npm version patch => z+1
npm version minor => y+1 && z=0
npm version major => x+1 && y=0 && z=0

与上文所讲的语义化的版本号相关,vX.Y.Z(主版本号.次版本号.修订号)三个选项分别对应三部分的版本号,每次运行命令会导致相应的版本号递增一,同时子版本号清零。记得运行上面命令前先将文件变动提交到git 上去。

之后运行npm publish命令即可。

发布到PostCSS 官方

Postcss 官方主页上有个plugin list 文件展示了所有的第三方插件,提交的话Fork 一份然后在该文件增加自己的插件详细然后提交合并,等作者允许即可。

发布到postcss.part

postcss.parts 是一个非官方的PostCSS 插件搜索平台。提交自己插件可按照这个说明。其实本质也是Fork 然后加信息在Pull request 的方式,在此不累述。

结束篇

效果

在开发完postcss-lazyimagecss 插件后,笔者按照上面的发布方式提交了给官方。后面效果还不错,PostCSS 作者也提了个star 跟issue。PostCSS 官方推特上的推荐也带来了第一批Stargazers。

clipboard.png

因为这个缘故,在第三届中国CSS 大会上也有幸与PostCSS 作者ai 大神勾搭了下,并得到了大神赠送的俄罗斯巧克力。

思考

在笔者看来,PostCSS 的作为一个CSS 转换引擎,其不参与细分功能实现仅交于第三方插件的设计理念,让其产生了一个非常的开放的生态。但对于个开放机制下的一些情况笔者并不是很赞同,如一些用中文写CSS 的插件(当然这个更多是for fun),一些自定义CSS 属性如用size: 10px 2px 等代替width/height的插件——在笔者看来PostCSS 插件应该更多在遵从CSS 标准语法的基础上进行扩展。

但无论如何,还是挺佩作者开发出了这么个造福前端届的工具;也因为认同作者,笔者写了这篇文章为推广PostCSS 做了一点微小的工作;也希望对看到文末的您有所帮助,积极参与到开源创作的事业中。

参考文章:

http://ai.github.io/postcss-way/

https://github.com/postcss/po...

https://css-tricks.com/want-m...

查看原文

赞 13 收藏 20 评论 6

陌上 发布了文章 · 2019-08-19

前端跨域分析及解决办法

为什么有跨域问题

因为浏览器的同源策略,导致了跨域问题的出现。

一. 同源策略

1. 什么是同源策略

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

2. 为什么需要同源策略

出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求。 例如XMLHttpRequest遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非使用CORS头文件或其他跨域方法。

没有同源策略的两大安全隐患:

  1. 针对接口请求
    常见的场景为:cookie获取,CSRF攻击:(Cross-site request forgery)跨站请求伪造

One Day,当你兴致勃勃在某宝上准备双11的买买买,网购物车里面各种加,这个时候弹出了你爱豆的新闻,那必须得关注,看一看你家爱豆是不是也要介绍一下另一半,于是就点击连接进去看了,你看的过程中,也许这个网站暗地里就做了些什么不可描述的事情!

比如说由于没有同源策略的限制,它向某宝发起了请求!因为你在登录某宝时“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!

  1. 针对Dom
    假设一个场景需要用户先填写用户名密码等登录信息进行身份验证,才能进行接下来的操作,当你输入用户名密码登录成功后,你的账号密码就被盗了,那酸爽~~~
    那这个钓鱼网站做了什么呢?
    // HTML
    <iframe name="qq" data-original="www.qq.com"></iframe>
    // JS
    // 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
    let iframe = window.frames['qq']
    let userInput = iframe.document.getElementById('账号输入框'),
        passInput = iframe.document.getElementById('密码输入框');
    
    //dom都拿到了,账号和密码不就是一件很容易的事情了么~~~

因此同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。

3. 怎么判断URL是否同源

如果两个页面的协议端口(如果有指定)和域名都相同,则两个页面具有相同的源。

同源的判定:
http://www.example.com/dir/page.html为例,以下表格指出了不同形式的链接是否与其同源:(原因里未申明不同的属性即说明其与例子里的原链接对应的属性相同)

链接结果原因
http://www.example.com/dir/page2.html同协议同域名同端口
http://www.example.com/dir2/other.html同协议同域名同端口
http://www.example.com:81/dir/other.html端口不同
https://www.example.com/dir/other.html协议不同端口不同
http://en.example.com/dir/other.html域名不同
http://example.com/dir/other.html域名不同(要求精确匹配)
http://v2.www.example.com/dir/other.html域名不同(要求精确匹配)
http://www.example.com:80/dir/other.html不确定取决于浏览器的实现方式

tips: 主域名与子域名的区别

主域名:由两个或两个以上的字母构成,中间由点号隔开,整个域名只有1个点号,唯一的
子域名:是在主域名之下的域名,域名内容会有多个点号

例如:https://www.baidu.com/
协议:https://
服务器名称:www
主域名:baidu.com
子域名(子域名包括服务器名称www + 主域名baidu.com):www.baidu.com

二. 接口跨域的正确打开方式

目前常用的解决跨域问题的三种方式:

  • jsonp:只能发送GET请求
  • iframe + form
  • CORS:跨域资源共享(Cross-origin resource sharing);

1. JSONP

在HTML标签里,一些标签比如script、img这样的获取资源的标签是没有跨域限制的,利用这一点,我们可以这样干:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script type='text/javascript'>
      // 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
      window.jsonpCb = function (res) {
        console.log(res)
      }
    </script>
    <script data-original='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
  </body>
</html>

由上可知JSNOP只能发送GET请求,不能发送POST,本质上通过script标签去加载资源就是GET

2. Iframe + Form

看代码

const requestPost = ({url, data}) => {
  // 首先创建一个用来发送数据的iframe.
  const iframe = document.createElement('iframe')
  iframe.name = 'iframePost'
  iframe.style.display = 'none'
  document.body.appendChild(iframe)
  const form = document.createElement('form')
  const node = document.createElement('input')
  // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
  iframe.addEventListener('load', function () {
    console.log('post success')
  })

  form.action = url
  // 在指定的iframe中执行form的action url
  form.target = iframe.name
  form.method = 'post'
  for (let name in data) {
    node.name = name
    node.value = data[name].toString()
    form.appendChild(node.cloneNode())
  }
  // 表单元素需要添加到主文档中.
  form.style.display = 'none'
  document.body.appendChild(form)
  form.submit()

  // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
  document.body.removeChild(form)
}
// 使用方式
requestPost({
  url: 'http://localhost:9871/api/iframePost',
  data: {
    msg: 'helloIframePost'
  }
})

3. CORS

CORS全称跨域资源共享(Cross-origin resource sharing),该机制允许Web应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。浏览器支持在API容器中(例如XMLHttpRequest或Fetch)使用CORS,以降低跨域HTTP请求所带来的风险。CORS需要客户端和服务器同时支持,目前,所有浏览器都支持该机制(微企即采用这种方式)。

IE 10+ 提供了对规范的完整支持,但在较早版本(8 和 9)中,CORS机制是借由 XDomainRequest 对象完成的。

CORS规范要求,对那些可能对服务器数据产生副作用的HTTP请求方法(特别是GET以外的HTTP请求,或者搭配某些 MIME 类型的POST请求),浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的HTTP请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括Cookies和HTTP认证相关数据)。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信

CORS标准允许在下列场景中使用跨域http请求:

  • 由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。
  • Web 字体 (CSS 中通过 @font-face 使用跨域字体资源), 因此,网站就可以发布 TrueType
  • 字体资源,并只允许已授权网站进行跨站调用。
  • WebGL 贴图
  • 使用 drawImage 将 Images/video 画面绘制到 canvas
  • 样式表(使用 CSSOM)
  • Scripts (未处理的异常)

浏览器将CORS请求分成两类:简单请求(simple request)非简单请求(not-so-simple request)

使用下列方法之一:

GET
HEAD
POST 

Fetch 规范定义了对 CORS安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type (需要注意额外的限制)
  • Content-Type 的值仅限于下列三者之一:

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
      只要同时满足以下两大条件,就属于简单请求。

凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。

1. 简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com //必填
Access-Control-Allow-Credentials: true //非必填
Access-Control-Expose-Headers: FooBar //非必填
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

withCredentials 属性
上面说到,CORS请求默认不发送CookieHTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

另一方面,开发者必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials

xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

2. 非简单请求(预检请求)

2.1 预检请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一段浏览器的JavaScript脚本。

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

2.2 预检请求的回应

服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

Access-Control-Allow-Origin: *

如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

(2)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

2.3 浏览器的正常请求和回应

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是"预检"请求之后,浏览器的正常CORS请求。

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

HTTP 响应首部字段

1. Access-Control-Allow-Origin

响应首部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:

Access-Control-Allow-Origin: <origin> | *

其中,origin 参数的值指定了允许访问该资源的外域URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。

例如,下面的字段值将允许来自 http://mozilla.com 的请求:

Access-Control-Allow-Origin: http://mozilla.com

如果服务端指定了具体的域名而非“*”,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

DOM级别跨域

document.domain

页面可能会因某些限制而改变他的源。脚本可以将document.domain的值设置为其当前域或其当前域的超级域。如果将其设置为其当前域的超级域,则较短的域将用于后续源检查。
假设http://store.company.com/dir/other.html文档中的一个脚本执行以下语句:

document.domain = "company.com";

这条语句执行之后,页面将会成功地通过对http://company.com/dir/page.html的同源检测(假设http://company.com/dir/page.html将其document.domain设置为company.com,以表明它希望允许这样做 - 更多有关信息,请参阅 document.domain)。然而,company.com不能设置document.domainothercompany.com,因为它不是 company.com 的超级域

canvas中getImageData,toDataURL跨域

通过添加cross-origin属性即可解决getImageDatatoDataURL跨域问题,具体参见canvas跨域

查看原文

赞 0 收藏 0 评论 0

陌上 发布了文章 · 2019-08-19

聊一聊http状态码,缓存机制,cookie

Http状态码

状态码含义
1XX请求正在被处理
2XX请求被成功处理
3XX请求需要附加操作,常见如重定向
4XX客户端出错导致请求无法被处理
5XX服务端处理出错

2XX

状态码原因短语含义
200OK请求被正常处理
204NO CONTENT请求成功,但是响应的报文中不含实体主体
206Partial Content只返回了请求资源的部分

204: 例如option请求,通常被用来做正式请求的预请求,这个请求只需要确认后续的请求能不能通过,即只需要一个结果,而不需要返回其他内容。

206: 在 http的请求中,头部添加Range用来表示范围请求,例如

'Range': byte=5001-10000 // 表示本次要请求资源的5001-10000字节的部分

这种情况下,如果服务器接受范围请求并且成功处理,就会返回 206,并且在响应的头部返回:

'Content-Range':bytes 5001-10000/10000// 表示整个资源有10000字节,本次返回的范围为 5001-10000字节

3XX

状态码原因短语含义
301Moved Permanently资源被永久重定向了
302Found资源临时重定向到location
303See Other资源使用GET访问临时重定向的location
304Not Modified资源未改变,可直接使用缓存
307Temporary Redirect严格限制重定向不允许从POST转为GET
301302的唯一区别就在于一个是临时,一个是永久。

4XX

状态码原因短语含义
400Bad Request请求中有语法错误
401Unauthorized未经过认证
403Forbidden禁止访问也就是无权限访问
404Not Found服务端没有找到所请求的资源

5XX

状态码原因短语含义
500Internal Server Error服务器故障
503Service Unavailable服务器暂时无法使用

缓存机制

浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

强缓存

强缓存不会向服务器发送请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回200的状态码,并且Size显示from disk cachefrom memory cache。强缓存可以通过设置两种 HTTPHeader 实现:ExpiresCache-Control

1. Expires

缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。Expires 是 Web 服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。Expires: Wed, 22 Oct 2018 08:41:00GMT 表示资源会在Wed, 22 Oct 2018 08:41:00 GMT后过期,需要再次请求。

2. Cache-Control

在 HTTP/1.1 中,Cache-Control是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的 5 分钟内再次加载资源,就会命中强缓存。
Cache-Control可以在请求头或者响应头中设置,并且可以组合使用多种指令:

指令作用
public响应可被客户端和代理服务器缓存
private响应只可被客户端缓存
max-age=30缓存 30 秒后过期,需要重新请求
s-maxage=30覆盖max-age,作用一样,只是在代理服务器中生效
no-store不缓存任何响应
no-cache资源被缓存,但是立即失效,下次会发起请求验证资源是否过期,配合协商缓存使用
max-stale=3030 秒内,即使缓存过期也使用该缓存
min-fresh=30希望在 30 秒内获取最新的响应

详细说明:

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存)。具体来说响应可被任何中间节点缓存,如 Browser<--proxy1<--proxy2<--Server,中间的proxy可以缓存资源,比如下次再请求同一资源proxy1直接把自己缓存的东西给Browser而不再向proxy2要。
  • private:所有内容只有客户端可以缓存,Cache-Control默认取值。具体来说,表示中间节点不允许缓存,对于Browser<--proxy1<--proxy2<--Serverproxy会老老实实把Server返回的数据发送给proxy1,自己不缓存任何数据。当下次Browser再次请求时proxy会做好请求转发而不是自作主张给自己缓存的数据。
  • no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用Cache-Control的缓存控制方式做前置验证,而是使用Etag或者Last-Modified字段来控制缓存。

    需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。
  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
  • max-agemax-age=xxx(xxx is numeric)表示缓存内容将在 xxx 秒后失效
  • s-maxage(单位为 s):同max-age作用一样,只在代理服务器中生效(比如 CDN 缓存)。比如当s-maxage=60时,在这 60 秒中,即使更新了 CDN 的内容,浏览器也不会进行请求。max-age用于普通缓存,而s-maxage用于代理缓存。s-maxage的优先级高于max-age。如果存在 s-maxage,则会覆盖掉max-ageExpiresheader。
  • max-stale:能容忍的最大过期时间。max-stale指令标示了客户端愿意接收一个已经过期了的响应。如果指定了max-stale的值,则最大容忍时间为对应的秒数。如果没有指定,那么说明浏览器愿意接收任何 age 的响应(age 表示响应由源站生成或确认的时间与当前时间的差值)。
  • min-fresh:能够容忍的最小新鲜度。min-fresh标示了客户端不愿意接受新鲜度不多于当前的 age 加上 min-fresh 设定的时间之和的响应。

3. Expires 和 Cache-Control 两者对比

其实这两者差别不大,区别就在于 Expires 是 http1.0 的产物,Cache-Control 是 http1.1 的产物,两者同时存在的话,Cache-Control 优先级高于 Expires;在某些不支持 HTTP1.1 的环境下,Expires 就会发挥用处。所以 Expires 其实是过时的产物,现阶段它的存在只是一种兼容性的写法。

强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

  • 协商缓存生效,返回304Not Modified
  • 协商缓存失效,返回200和请求结果

协商缓存可以通过设置两种 HTTP 头ModifiedETag

1. Last-Modified 和 If-Modified-Since

浏览器在第一次访问资源时,服务器返回资源的同时,在 response header 中添加Last-Modified的 header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和 header;

Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT

浏览器下一次请求这个资源,浏览器检测到有Last-Modified这个 header,于是添加If-Modified-Since这个 header,值就是Last-Modified中的值;服务器再次收到这个资源请求,会根据If-Modified-Since中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回 304 和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200.

但是 Last-Modified 存在一些弊端:

如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源.因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源.既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP/1.1 出现了ETagIf-None-Match;

2. ETag 和 If-None-Match

Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的If-None-Match里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回304知会客户端直接使用本地缓存即可。

3. 两者之间对比

  • 首先在精确度上,Etag 要优于 Last-Modified。

    Last-Modified 的时间单位是秒,如果某个文件在 1 秒内改变了多次,那么他们的 Last-Modified 其实并没有体现出来修改,但是 Etag 每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的 Last-Modified 也有可能不一致。

  • 第二在性能上,Etag 要逊于 Last-Modified,毕竟 Last-Modified 只需要记录时间,而 Etag 需要服务器通过算法来计算出一个 hash 值。
  • 第三在优先级上,服务器校验优先考虑 Etag。

强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。

实际场景应用缓存策略

1. 频繁变动的资源

Cache-Control: no-cache

对于频繁变动的资源,首先需要使用Cache-Control: no-cache使浏览器每次都请求服务器,然后配合ETag或者Last-Modified来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

2. 不常变化的资源

Cache-Control: max-age=31536000

通常在处理这类资源时,给它们的Cache-Control配置一个很大的max-age=31536000(一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash,版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效(其实并未立即失效,只是不再使用了而已)。

内容参考:浏览器的缓存机制

Cookie的实现机制

Cookie是由客户端保存的小型文本文件,其内容为一系列的键值对。 Cookie是由HTTP服务器设置的,保存在浏览器中, 在用户访问其他页面时,会在HTTP请求中附上该服务器之前设置的Cookie。

  1. 浏览器向某个URL发起HTTP请求(可以是任何请求,比如GET一个页面、POST一个登录表单等)
  2. 对应的服务器收到该HTTP请求,并计算应当返回给浏览器的HTTP响应。
  3. 在响应头加入Set-Cookie字段,它的值是要设置的Cookie。
  4. 浏览器收到来自服务器的HTTP响应。
  5. 浏览器在响应头中发现Set-Cookie字段,就会将该字段的值保存在内存或者硬盘中。
Set-Cookie字段的值可以是很多项Cookie,每一项都可以指定过期时间Expires。 默认的过期时间是用户关闭浏览器时。
  1. 浏览器下次给该服务器发送HTTP请求时, 会将服务器设置的Cookie附加在HTTP请求的头字段Cookie中。
浏览器可以存储多个域名下的Cookie,但只发送当前请求的域名曾经指定的Cookie, 这个域名也可以在Set-Cookie字段中指定)。
  1. 服务器收到这个HTTP请求,发现请求头中有Cookie字段, 便知道之前就和这个用户打过交道了。
  2. 过期的Cookie会被浏览器删除。

总之,服务器通过Set-Cookie响应头字段来指示浏览器保存Cookie, 浏览器通过Cookie请求头字段来告诉服务器之前的状态。 Cookie中包含若干个键值对,每个键值对可以设置过期时间。

查看原文

赞 0 收藏 0 评论 0

陌上 收藏了文章 · 2019-07-11

异步Promise及Async/Await可能最完整入门攻略

此文只介绍Async/Await与Promise基础知识与实际用到注意的问题,将通过很多代码实例进行说明,两个实例代码是setDelaysetDelaySecond

tips:本文系原创转自我的博客异步Promise及Async/Await最完整入门攻略,欢迎前端大神交流,指出问题


一、为什么有Async/Await?

我们都知道已经有了Promise的解决方案了,为什么还要ES7提出新的Async/Await标准呢?

答案其实也显而易见:Promise虽然跳出了异步嵌套的怪圈,用链式表达更加清晰,但是我们也发现如果有大量的异步请求的时候,流程复杂的情况下,会发现充满了屏幕的then,看起来非常吃力,而ES7的Async/Await的出现就是为了解决这种复杂的情况。

首先,我们必须了解Promise

二、Promise简介

2.1 Promise实例

什么是Promise,很多人应该都知道基础概念?直接看下面的代码(全文的例子都是基于setDelaySecondsetDelay两个函数,请务必记住):

const setDelay = (millisecond) => {
  return new Promise((resolve, reject)=>{
      if (typeof millisecond != 'number') reject(new Error('参数必须是number类型'));
      setTimeout(()=> {
        resolve(`我延迟了${millisecond}毫秒后输出的`)
      }, millisecond)
  })
}

我们把一个Promise封装在一个函数里面同时返回了一个Promise,这样比较规范。

可以看到定义的Promise有两个参数,resolvereject

  • resolve:将异步的执行从pending(请求)变成了resolve(成功返回),是个函数执行返回。
  • reject:顾名思义“拒绝”,就是从请求变成了"失败",是个函数可以执行返回一个结果,但我们这里推荐大家返回一个错误new Error()
上述例子,你可以reject('返回一个字符串'),随便你返回,但是我们还是建议返回一个Error对象,这样更加清晰是“失败的”,这样更规范一点

2.2 Promise的then和catch

我们通过Promise的原型方法then拿到我们的返回值:

setDelay(3000)
.then((result)=>{
    console.log(result) // 输出“我延迟了2000毫秒后输出的”
})

输出下列的值:“我延迟了2000毫秒后输出的”。

如果出错呢?那就用catch捕获:

setDelay('我是字符串')
.then((result)=>{
    console.log(result) // 不进去了
})
.catch((err)=>{
    console.log(err) // 输出错误:“参数必须是number类型”
})

是不是很简单?好,现在我增加一点难度,如果多个Promise执行会是怎么样呢?

2.3 Promise相互依赖

我们在写一个Promise:

const setDelaySecond = (seconds) => {
  return new Promise((resolve, reject)=>{
      if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10'));
      setTimeout(()=> {
        console.log(`先是setDelaySeconds函数输出,延迟了${seconds}秒,一共需要延迟${seconds+2}秒`)
        resolve(setDelay(2000)) // 这里依赖上一个Promise
      }, seconds * 1000)
  })
}

在下一个需要依赖的resolve去返回另一个Promise,会发生什么呢?我们执行一下:

setDelaySecond(3).then((result)=>{
  console.log(result)
}).catch((err)=>{
  console.log(err);
})

你会发现结果是先执行:“先是setDelaySeconds输出,延迟了2秒,一共需要延迟5秒”

再执行setDelayresolve“我延迟了2000毫秒后输出的”。的确做到了依次执行的目的。

有人说,我不想耦合性这么高,想先执行setDelay函数再执行setDelaySecond,但不想用上面那种写法,可以吗,答案是当然可以。

2.4 Promise链式写法

先改写一下setDelaySecond,拒绝依赖,降低耦合性

const setDelaySecond = (seconds) => {
  return new Promise((resolve, reject)=>{
      if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10'));
      setTimeout(()=> {
        resolve(`我延迟了${seconds}秒后输出的,是第二个函数`)
      }, seconds * 1000)
  })
}

先执行setDelay在执行setDelaySecond,只需要在第一个then的结果中返回下一个Promise就可以一直链式写下去了,相当于依次执行

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(3)
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
}).catch((err)=>{
  console.log(err);
})

发现确实达到了可喜的链式(终于脱离异步嵌套苦海,哭),可以看到then的链式写法非常优美。

2.5 链式写法需要注意的地方

这里一定要提到一点:

then式链式写法的本质其实是一直往下传递返回一个新的Promise,也就是说then在下一步接收的是上一步返回的Promise,理解这个对于后面的细节非常重要!!

那么并不是这么简单,then的返回我们可以看出有2个参数(都是回调):

  • 第一个回调是resolve的回调,也就是第一个参数用得最多,拿到的是上一步的Promise成功resolve的值。
  • 第二个回调是reject的回调,用的不多,但是求求大家不要写错了,通常是拿到上一个的错误,那么这个错误处理和catch有什么区别和需要注意的地方呢?

我们修改上面的代码:

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(20)
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
}, (_err)=> {
  console.log('我出错啦,进到这里捕获错误,但是不经过catch了');
})
.then((result)=>{
  console.log('我还是继续执行的!!!!')
})
.catch((err)=>{
  console.log(err);
})

可以看到输出结果是:进到了then的第二个参数(reject)中去了,而且最重要的是!不再经过catch了。

那么我们把catch挪上去,写到then错误处理前:

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(20)
})
.catch((err)=>{ // 挪上去了
  console.log(err); // 这里catch到上一个返回Promise的错误
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
}, (_err)=> {
  console.log('我出错啦,但是由于catch在我前面,所以错误早就被捕获了,我这没有错误了');
})
.then((result)=>{
  console.log('我还是继续执行的!!!!')
})

可以看到先经过catch的捕获,后面就没错误了。

可以得出需要注意的:

  • catch写法是针对于整个链式写法的错误捕获的,而then第二个参数是针对于上一个返回Promise的。
  • 两者的优先级:就是看谁在链式写法的前面,在前面的先捕获到错误,后面就没有错误可以捕获了,链式前面的优先级大,而且两者都不是break, 可以继续执行后续操作不受影响。

2.5 链式写法的错误处理

上述已经写好了关于then里面三个回调中第二个回调(reject)会与catch冲突的问题,那么我们实际写的时候,参数捕获的方式基本写得少,catch的写法会用到更多。

既然有了很多的Promise,那么我需不需要写很多catch呢?

答案当然是:不需要!,哪有那么麻烦的写法,只需要在末尾catch一下就可以了,因为链式写法的错误处理具有“冒泡”特性,链式中任何一个环节出问题,都会被catch到,同时在某个环节后面的代码就不会执行了

既然说到这里,我们把catch移到第一个链式的返回里面会发生什么事呢?看下面代码:

setDelay('2000')
.then((result)=>{
  console.log('第一步完成了');
  console.log(result)
  return setDelaySecond(3)
})
.catch((err)=>{ // 这里移到第一个链式去,发现上面的不执行了,下面的继续执行
  console.log(err);
})
.then((result)=>{
  console.log('第二步完成了');
  console.log(result);
})

惊喜的发现,链式继续走下去了!!输出如下(undefined是因为上一个then没有返回一个Promise):

clipboard.png

重点来了!敲黑板!!链式中的catch并不是终点!!catch完如果还有then还会继续往下走!不信的话可以把第一个catch在最后面的那个例子后面再加几个then,你会发现并不会跳出链式执行

如果顺序执行setDelay,setDelay1,setDelaySecond,按照上述的逻辑,流程图可以概括如下:

clipboard.png

catch只是捕获错误的一个链式表达,并不是break!

所以,catch放的位置也很有讲究,一般放在一些重要的、必须catch的程序的最后。**这些重要的程序中间一旦出现错误,会马上跳过其他后续程序的操作直接执行到最近的catch代码块,但不影响catch后续的操作!!!!

到这就不得不体一个ES2018标准新引入的Promise的finally,表示在catch后必须肯定会默认执行的的操作。这里不多展开,细节可以参考:Promise的finally

2.5 Promise链式中间想返回自定义的值

其实很简单,用Promise的原型方法resolve即可:

setDelay(2000).then((result)=>{
  console.log('第一步完成了');
  console.log(result);
  let message = '这是我自己想处理的值'; 
  return Promise.resolve(message) // 这里返回我想在下一阶段处理的值
})
.then((result)=>{
  console.log('第二步完成了');
  console.log(result); // 这里拿到上一阶段的返回值
  //return Promise.resolve('这里可以继续返回')
})
.catch((err)=>{
  console.log(err);
})

2.7 如何跳出或停止Promise链式

不同于一般的functionbreak的方式,如果你是这样的操作:func().then().then().then().catch()的方式,你想在第一个then就跳出链式,后面的不想执行了,不同于一般的break;return null;return false等操作,可以说,如何停止Promise链,是一大难点,是整个Promise最复杂的地方。

1.用链式的思维想,我们拒绝掉某一链,那么不就是相当于直接跳到了catch模块吗?

我们是不是可以直接“拒绝“掉达到停止的目的?

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(1)
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
  console.log('我主动跳出循环了');
  return Promise.reject('跳出循环的信息') // 这里返回一个reject,主动跳出循环了
})
.then((result)=>{
  console.log('我不执行');
})
.catch((mes)=>{
  console.dir(mes)
  console.log('我跳出了');
})

但是很容易看到缺点:有时候你并不确定是因为错误跳出的,还是主动跳出的,所以我们可以加一个标志位:

return Promise.reject({
    isNotErrorExpection: true // 返回的地方加一个标志位,判断是否是错误类型,如果不是,那么说明可以是主动跳出循环的
}) 

或者根据上述的代码判断catch的地方输出的类型是不是属于错误对象的,是的话说明是错误,不是的话说明是主动跳出的,你可以自己选择(这就是为什么要统一错误reject的时候输出new Error('错误信息')的原因,规范!)

当然你也可以直接抛出一个错误跳出:

throw new Error('错误信息') // 直接跳出,那就不能用判断是否为错误对象的方法进行判断了

2.那有时候我们有这个需求:catch是放在中间(不是末尾),而同时我们又不想执行catch后面的代码,也就是链式的绝对中止,应该怎么办?

我们看这段代码:

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(1)
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
  console.log('我主动跳出循环了');
  return Promise.reject('跳出循环的信息') // 这里直接调用Promise原型方法返回一个reject,主动跳出循环了
})
.then((result)=>{
  console.log('我不执行');
})
.catch((mes)=>{
  console.dir(mes)
  console.log('我跳出了');
})
.then((res)=>{
    console.log('我不想执行,但是却执行了'); // 问题在这,上述的终止方法治标不治本。
})

这时候最后一步then还是执行了,整条链都其实没有本质上的跳出,那应该怎么办呢?

敲黑板!!重点来了!我们看Promise/A+规范可以知道:

A promise must be in one of three states: pending, fulfilled, or rejected.

Promise其实是有三种状态的:pending,resolve,rejected,那么我们一直在讨论resolve和rejected这2个状态,是不是忽视了pending这个状态呢?pending状态顾名思义就是请求中的状态,成功请求就是resolve,失败就是reject,其实他就是个中间过渡状态。

而我们上面讨论过了,then的下一层级其实得到的是上一层级返回的Promise对象,也就是说原Promise对象与新对象状态保持一致。那么重点来了,如果你想在这一层级进行终止,是不是直接让它永远都pending下去,那么后续的操作不就没了吗?是不是就达到这个目的了??觉得有疑问的可以参考Promise/A+规范。

我们直接看代码:

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(1)
})
.then((result)=>{
  console.log(result);
  console.log('我主动跳出循环了');
  // return Promise.reject('跳出循环的信息')
  // 重点在这
  return new Promise(()=>{console.log('后续的不会执行')}) // 这里返回的一个新的Promise,没有resolve和reject,那么会一直处于pending状态,因为没返回啊,那么这种状态就一直保持着,中断了这个Promise
})
.then((result)=>{
  console.log('我不执行');
})
.catch((mes)=>{
  console.dir(mes)
  console.log('我跳出了');
})
.then((res)=>{
  console.log('我也不会执行')
})

这样就解决了上述,错误跳出而导致无法完全终止Promise链的问题。

但是!随之而来也有一个问题,那就是可能会导致潜在的内存泄漏,因为我们知道这个一直处于pending状态下的Promise会一直处于被挂起的状态,而我们具体不知道浏览器的机制细节也不清楚,一般的网页没有关系,但大量的复杂的这种pending状态势必会导致内存泄漏,具体的没有测试过,后续可能会跟进测试(nodeJS或webapp里面不推荐这样),而我通过查询也难以找到答案,这篇文章可以推荐看一下:从如何停掉 Promise 链说起。可能对你有帮助在此种情况下如何做。

当然一般情况下是不会存在泄漏,只是有这种风险,无法取消Promise一直是它的痛点。而上述两个奇妙的取消方法要具体情形具体使用。

2.8 Promise.all

其实这几个方法就简单了,就是一个简写串联所有你需要的Promise执行,具体可以参照阮一峰的ES6Promise.all教程

我这上一个代码例子

Promise.all([setDelay(1000), setDelaySecond(1)]).then(result=>{
  console.log(result);
})
.catch(err=>{
  console.log(err);
})
// 输出["我延迟了1000毫秒后输出的", "我延迟了1秒后输出的,注意单位是秒"]

输出的是一个数组,相当于把all方法里面的Promise并行执行,注意是并行。
相当于两个Promise同时开始执行,同时返回值,并不是先执行第一个再执行第二个,如果你想串行执行,请参考我后面写的循环Promise循环串行(第4.2小节)

然后把resolve的值保存在数组中输出。类似的还有Promise.race这里就不多赘述了。

三、Async/await介绍

3.1 基于Promise的Async/await

什么是async/await呢?可以总结为一句话:async/await是一对好基友,缺一不可,他们的出生是为Promise服务的。可以说async/await是Promise的爸爸,进化版。为什么这么说呢?且听我细细道来。

为什么要有async/await存在呢?

前文已经说过了,为了解决大量复杂不易读的Promise异步的问题,才出现的改良版。

这两个基友必须同时出现,缺一不可,那么先说一下Async

async function process() {
}

上面可以看出,async必须声明的是一个function,不要去声明别的,要是那样await就不理你了(报错)。

这样声明也是错的!

const async demo =  function () {} // 错误

必须紧跟着function。接下来说一下它的兄弟await

上面说到必须是个函数(function),那么await就必须是在这个async声明的函数内部使用,否则就会报错。

就算你这样写,也是错的。

let data = 'data'
demo  = async function () {
    const test = function () {
        await data
    }
}

必须是直系(作用域链不能隔代),这样会报错:Uncaught SyntaxError: await is only valid in async function

讲完了基本规范,我们接下去说一下他们的本质。

3.2 async的本质

敲黑板!!!很重要!async声明的函数的返回本质上是一个Promise

什么意思呢?就是说你只要声明了这个函数是async,那么内部不管你怎么处理,它的返回肯定是个Promise。

看下列例子:

(async function () {
    return '我是Promise'
})()
// 返回是Promise
//Promise {<resolved>: "我是Promise"}

你会发现返回是这个:Promise {<resolved>: "我是Promise"}

自动解析成Promise.resolve('我是Promise');

等同于:

(async function () {
    return Promise.resolve('我是Promise');
})()

所以你想像一般function的返回那样,拿到返回值,原来的思维要改改了!你可以这样拿到返回值:

const demo = async function () {
    return Promise.resolve('我是Promise');
    // 等同于 return '我是Promise'
    // 等同于 return new Promise((resolve,reject)=>{ resolve('我是Promise') })
}
demo.then(result=>{
    console.log(result) // 这里拿到返回值
})

上述三种写法都行,要看注释细节都写在里面了!!像对待Promise一样去对待async的返回值!!!

好的接下去我们看await的干嘛用的.

3.3 await的本质与例子

await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖

这一句咋一看很别扭,好的不急,我们从例子开始看:

const demo = async ()=>{
    let result = await new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('我延迟了一秒')
      }, 1000)
    });
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
}
// demo的返回当做Promise
demo().then(result=>{
  console.log('输出',result);
})

await顾名思义就是等待一会,只要await声明的函数还没有返回,那么下面的程序是不会去执行的!!!。这就是字面意义的等待一会(等待返回再去执行)。

那么你到这测试一下,你会发现输出是这个:输出 undefined。这是为什么呢?这也是我想强调的一个地方!!!

你在demo函数里面都没声明返回,哪来的then?所以正确写法是这样:

const demo = async ()=>{
    let result = await new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('我延迟了一秒')
      }, 1000)
    });
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    return result;
}
// demo的返回当做Promise
demo().then(result=>{
  console.log('输出',result); // 输出 我延迟了一秒
})

我推荐的写法是带上then,规范一点,当然你没有返回也是没问题的,demo会照常执行。下面这种写法是不带返回值的写法:

const demo = async ()=>{
    let result = await new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('我延迟了一秒')
      }, 1000)
    });
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
}
demo();

所以可以发现,只要你用await声明的异步返回,是必须“等待”到有返回值的时候,代码才继续执行下去。

那事实是这样吗?你可以跑一下这段代码:

const demo = async ()=>{
    let result = await setTimeout(()=>{
      console.log('我延迟了一秒');
    }, 1000)
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    return result
}
demo().then(result=>{
  console.log('输出',result);
})

你会发现,输出是这样的:

我由于上面的程序还没执行完,先不执行“等待一会”
输出 1
我延迟了一秒

奇怪,并没有await啊?setTimeout是异步啊,问题在哪?问题就在于setTimeout这是个异步,但是不是Promise!起不到“等待一会”的作用。

所以更准确的说法应该是用await声明的Promise异步返回,必须“等待”到有返回值的时候,代码才继续执行下去。

请记住await是在等待一个Promise的异步返回

当然这种等待的效果只存在于“异步”的情况,await可以用于声明一般情况下的传值吗?

事实是当然可以:

const demo = async ()=>{
    let message = '我是声明值'
    let result = await message;
    console.log(result); 
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    return result
}
demo().then(result=>{
  console.log('输出',result);
})

输出:

我是声明值
我由于上面的程序还没执行完,先不执行“等待一会”
输出 我是声明值

这里只要注意一点:then的执行总是最后的。

3.4 async/await 优势实战

现在我们看一下实战:

const setDelay = (millisecond) => {
  return new Promise((resolve, reject)=>{
      if (typeof millisecond != 'number') reject(new Error('参数必须是number类型'));
      setTimeout(()=> {
        resolve(`我延迟了${millisecond}毫秒后输出的`)
      }, millisecond)
  })
}
const setDelaySecond = (seconds) => {
  return new Promise((resolve, reject)=>{
      if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10'));
      setTimeout(()=> {
        resolve(`我延迟了${seconds}秒后输出的,注意单位是秒`)
      }, seconds * 1000)
  })
}

比如上面两个延时函数(写在上面),比如我想先延时1秒,在延迟2秒,再延时1秒,最后输出“完成”,这个过程,如果用then的写法,大概是这样(嵌套地狱写法出门右拐不送):

setDelay(1000)
.then(result=>{
    console.log(result);
    return setDelaySecond(2)
})
.then(result=>{
    console.log(result);
    return setDelay(1000)
})
.then(result=>{
    console.log(result);
    console.log('完成')
})
.catch(err=>{
    console.log(err);
})

咋一看是不是挺繁琐的?如果逻辑多了估计看得更累,现在我们来试一下async/await

(async ()=>{
  const result = await setDelay(1000);
  console.log(result);
  console.log(await setDelaySecond(2));
  console.log(await setDelay(1000));
  console.log('完成了');
})()

看!是不是没有冗余的长长的链式代码,语义化也非常清楚,非常舒服,那么你看到这里,一定还发现了,上面的catch我们是不是没有在async中实现?接下去我们就分析一下async/await如何处理错误?

3.5 async/await错误处理

因为async函数返回的是一个Promise,所以我们可以在外面catch住错误。

const demo = async ()=>{
  const result = await setDelay(1000);
  console.log(result);
  console.log(await setDelaySecond(2));
  console.log(await setDelay(1000));
  console.log('完成了');
}
demo().catch(err=>{
    console.log(err);
})

在async函数的catch中捕获错误,当做一个Pormise处理,同时你不想用这种方法,可以使用try...catch语句:

(async ()=>{
  try{
    const result = await setDelay(1000);
    console.log(result);
    console.log(await setDelaySecond(2));
    console.log(await setDelay(1000));
    console.log('完成了');
  } catch (e) {
    console.log(e); // 这里捕获错误
  }
})()

当然这时候你就不需要在外面catch了。

通常我们的try...catch数量不会太多,几个最多了,如果太多了,说明你的代码肯定需要重构了,一定没有写得非常好。还有一点就是try...catch通常只用在需要的时候,有时候不需要catch错误的地方就可以不写。

有人会问了,我try...catch好像只能包裹代码块,如果我需要拆分开分别处理,不想因为一个的错误就整个process都crash掉了,那么难道我要写一堆try...catch吗?我就是别扭,我就是不想写try...catch怎嘛办?下面有一种很好的解决方案,仅供参考:

我们知道await后面跟着的肯定是一个Promise那是不是可以这样写?

(async ()=>{
  const result = await setDelay(1000).catch(err=>{
      console.log(err)
  });
  console.log(result);
  const result1 = await setDelaySecond(12).catch(err=>{
      console.log(err)
  })
  console.log(result1);
  console.log(await setDelay(1000));
  console.log('完成了');
})()

这样输出:

我延迟了1000毫秒后输出的
Error: 参数必须是number类型,并且小于等于10
    at Promise (test4.html:19)
    at new Promise (<anonymous>)
    at setDelaySecond (test4.html:18)
    at test4.html:56
undefined
我延迟了1000毫秒后输出的
完成了

是不是就算有错误,也不会影响后续的操作,是不是很棒?当然不是,你说这代码也忒丑了吧,乱七八糟的,写得别扭await又跟着catch。那么我们可以改进一下,封装一下提取错误的代码函数:

// to function
function to(promise) {
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]); // es6的返回写法
}

返回的是一个数组,第一个是错误,第二个是异步结果,使用如下:

(async ()=>{
   // es6的写法,返回一个数组(你可以改回es5的写法觉得不习惯的话),第一个是错误信息,第二个是then的异步返回数据,这里要注意一下重复变量声明可能导致问题(这里举例是全局,如果用let,const,请换变量名)。
  [err, result] = await to(setDelay(1000)) 
   // 如果err存在就是有错,不想继续执行就抛出错误
  if (err) throw new Error('出现错误,同时我不想执行了');
  console.log(result);
  [err, result1] = await to(setDelaySecond(12))
   // 还想执行就不要抛出错误
  if (err) console.log('出现错误,同时我想继续执行', err);
  console.log(result1);
  console.log(await setDelay(1000));
  console.log('完成了');
})()

3.6 async/await的中断(终止程序)

首先我们要明确的是,Promise本身是无法中止的Promise本身只是一个状态机,存储三个状态(pending,resolved,rejected),一旦发出请求了,必须闭环,无法取消,之前处于pending状态只是一个挂起请求的状态,并不是取消,一般不会让这种情况发生,只是用来临时中止链式的进行。

中断(终止)的本质在链式中只是挂起,并不是本质的取消Promise请求,那样是做不到的,Promise也没有cancel的状态。

不同于Promise的链式写法,写在async/await中想要中断程序就很简单了,因为语义化非常明显,其实就和一般的function写法一样,想要中断的时候,直接return一个值就行,null,空,false都是可以的。看例子:

let count = 6;
const demo = async ()=>{
  const result = await setDelay(1000);
  console.log(result);
  const result1 = await setDelaySecond(count);
  console.log(result1);
  if (count > 5) {
      return '我退出了,下面的不进行了';
    // return; 
    // return false; // 这些写法都可以
    // return null;
  }
  console.log(await setDelay(1000));
  console.log('完成了');
};
demo().then(result=>{
  console.log(result);
})
.catch(err=>{
  console.log(err);
})

实质就是直接return返回了一个Promise,相当于return Promise.resolve('我退出了下面不进行了'),当然你也可以返回一个“拒绝”:return Promise.reject(new Error('拒绝'))那么就会进到错误信息里去。

async函数实质就是返回一个Promise!

四、实战中异步需要注意的地方

我们经常会使用上述两种写法,也可能混用,有时候会遇到一些情况,这边举例子说明:

4.1 Promise获取数据(串行)之then写法注意

并行的不用多说,很简单,直接循环发出请求就可以或者用Promise.all。如果我们需要串行循环一个请求,那么应该怎么做呢?

我们需要实现一个依次分别延迟1秒输出值,一共5秒的程序,首先是Promise的循环,这个循环就相对来说比较麻烦:

我们经常会犯的错误!就是不重视函数名与函数执行对程序的影响

先不说循环,我们先举一个错误的例子,现在有一个延迟函数

const setDelay = (millisecond) => {
  return new Promise((resolve, reject)=>{
      if (typeof millisecond != 'number') reject(new Error('参数必须是number类型'));
      setTimeout(()=> {
        resolve(`我延迟了${millisecond}毫秒后输出的`)
      }, millisecond)
  })
}

我们想做到:“循环串行执行延迟一秒的Promise函数”,期望的结果应该是:隔一秒输出我延迟了1000毫秒后输出的,一共经过循环3次。我们想当然地写出下列的链式写法:

arr = [setDelay(1000), setDelay(1000), setDelay(1000)]
arr[0]
.then(result=>{
  console.log(result)
  return arr[1]
})
.then(result=>{
  console.log(result)
  return arr[2]
})
.then(result=>{
  console.log(result)
})

但是很不幸,你发现输出是并行的!!!也就是说一秒钟一次性输出了3个值!。那么这是什么情况呢?其实很简单。。。就是你把setDelay(1000)这个直接添加到数组的时候,其实就已经执行了,注意你的执行语句(1000)

这其实是基础,是语言的特性,很多粗心的人(或者是没有好好学习JS的人)会以为这样就把函数添加到数组里面了,殊不知函数已经执行过一次了。

那么这样导致的后果是什么呢?也就是说数组里面保存的每个Promise状态都是resolve完成的状态了,那么你后面链式调用直接return arr[1]其实没有去请求,只是立即返回了一个resolve的状态。所以你会发现程序是相当于并行的,没有依次顺序调用。

那么解决方案是什么呢?直接函数名存储函数的方式(不执行Promise)来达到目的

我们这样改一下程序:

arr = [setDelay, setDelay, setDelay]
arr[0](1000)
.then(result=>{
  console.log(result)
  return arr[1](1000)
})
.then(result=>{
  console.log(result)
  return arr[2](1000)
})
.then(result=>{
  console.log(result)
})

上述相当于把Promise预先存储在一个数组中,在你需要调用的时候,再去执行。当然你也可以用闭包的方式存储起来,需要调用的时候再执行。

4.2 Promise循环获取数据(串行)之for循环

上述写法是不优雅的,次数一多就GG了,为什么要提一下上面的then,其实就是为了后面的for循环做铺垫。

上面的程序根据规律改写一下:

arr = [setDelay, setDelay, setDelay]
var temp
temp = arr[0](1000)
for (let i = 1; i <= arr.length; i++) {
    if (i == arr.length) {
      temp.then(result=>{
        console.log('完成了');
      })
      break;
    }
    temp = temp.then((result)=>{
        console.log(result);
        return arr[i-1](1000)
    });
}

错误处理可以在for循环中套入try...catch,或者在你每个循环点进行.then().catch()、都是可行的。如果你想提取成公共方法,可以再改写一下,利用递归的方式:

首先你需要闭包你的Promise程序

function timeout(millisecond) {
  return ()=> {
    return setDelay(millisecond);
  }
}

如果不闭包会导致什么后果呢?不闭包的话,你传入的参数值后,你的Promise会马上执行,导致状态改变,如果用闭包实现的话,你的Promise会一直保存着,等到你需要调用的时候再使用。而且最大的优点是可以预先传入你需要的参数

改写数组:

arr = [timeout(2000), timeout(1000), timeout(1000)]

提取方法,Promise数组作为参数传入:

const syncPromise = function (arr) {
  const _syncLoop = function (count) {
    if (count === arr.length - 1) { // 是最后一个就直接return
      return arr[count]()
    }
    return arr[count]().then((result)=>{
      console.log(result);
      return _syncLoop(count+1) // 递归调用数组下标
    });
  }
  return _syncLoop(0);
}

使用:

syncPromise(arr).then(result=>{
  console.log(result);
  console.log('完成了');
})
// 或者 添加到Promise类中方法
Promise.syncAll = function syncAll(){
  return syncPromise
}// 以后可以直接使用
Promise.syncAll(arr).then(result=>{
  console.log(result);
  console.log('完成了');
})

还有大神总结了一个reduce的写法,其实就是一个迭代数组的过程:

const p = arr.reduce((total, current)=>{
    return total.then((result)=>{
        console.log(result);
        return current()
    })
}, Promise.resolve('程序开始'))
p.then((result)=>{
    console.log('结束了', result);
})

都是可行的,在Promise的循环领域。

4.3 async/await循环获取数据(串行)之for循环

现在就来介绍一下牛逼的async/await实战,上述的代码你是不是要看吐了,的确,我也觉得好麻烦啊,那么如果用async/await能有什么改进吗?这就是它出现的意义:

模拟上述代码的循环:

(async ()=>{
    arr = [timeout(2000), timeout(1000), timeout(1000)]
    for (var i=0; i < arr.length; i++) {
        result = await arr[i]();
        console.log(result);
    }
})()

。。。这就完了?是的。。。就完了,是不是特别方便!!!!语义化也非常明显!!这里为了保持与上面风格一致,没有加入错误处理,所以实战的时候记得加入你的try...catch语句来捕获错误。

四、后记

一直想总结一下Promiseasync/await,很多地方可能总结得不够,已经尽力扩大篇幅了,后续有新的知识点和总结点可能会更新(未完待续),但是入门这个基本够用了。

我们常说什么async/await的出现淘汰了Promise,可以说是大错特错,恰恰相反,正因为有了Promise,才有了改良版的async/await,从上面分析就可以看出,两者是相辅相成的,缺一不可。

想学好async/await必须先精通Promise,两者密不可分,有不同意见和改进的欢迎指导!

前端小白,大家互相交流,peace!

查看原文

认证与成就

  • 获得 26 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-05-17
个人主页被 510 人浏览