性能优化方法介绍

介绍

近期在学习性能优化的定位,在此总结一下,几种前端常见的性能定位的方式方法。

首先介绍一下显示页面常见的几个量化指标。

FP(First Paint Time)

从页面开始加载到浏览器中检测到渲染(任何渲染)时被触发(例背景改变,样式应用等)。

FCP(First Contentful Paint)

从页面开始加载到页面内容的任何部分呈现在屏幕上的时间。(关注焦点是内容,这个度量可以知道用户什么时候收到有用的信息)

FMP(First Meaning Paint)

表示页面的主要内容,开始出现在屏幕的时间,只是记录加载体验的最开始。如果页面loading,则该时刻毫无意义。

LCP(Largest Contentful Paint)

视窗最大可见图片或者文本块的渲染时间。帮助捕获更多的首次渲染之后的加载性能。

长任务(Long Task)

当一个任务超过50ms耗时,则认定为一个长任务

TTI(Time To Internative)

从页面开始加载主要子资源到能够快速响应用户输入的时间。

FID(First Input Delay)

从用户第一次与页面交互到浏览器实际能够处理事件的时间

总阻塞时间TBT(Total Blocking Time)

衡量从FCP到TTI之间主线程被阻塞时长的总和

DCL(DOMContentLoaded)

当HTML文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,无需等待样式、图像和子框架的完成加载。

L(onLoaded)

当依赖的资源全部加载完毕之后才会出发

CLS(Cumulative Layout Shift)

所有布局偏移分数的汇合,凡是在页面完整生命周期内预料之外的布局偏移都包括。

性能定位

web-vitals

performance API

Performance 是一个浏览器全局对象,提供了一组 API 用于编程式地获取程序在某些节点的性能数据。它包含一组高精度时间定义,以及配套的相关方法。

使用 performance.timing 信息简单计算出网页性能数据

FP:responseStart - navigationStart

重定向耗时:redirectEnd - redirectStart

DNS 查询耗时:domainLookupEnd - domainLookupStart

TCP 链接耗时:connectEnd - connectStart

HTTP 请求耗时:responseEnd - responseStart

解析 dom 树耗时:domComplete - domInteractive

DOM ready 时间:domContentLoadedEventEnd - navigationStart

onload:loadEventEnd - navigationStart

使用performance.getEntries()获取所有资源请求的时间数据

获取所有资源请求的时间数据,这个函数返回一个按 startTime 排序的对象数组

使用performance.getEntriesByName(name)获取特定名称的时间数据

我们可以通过 getEntriesByName(name)提供的 api 去获取 FCP 数据

FCP = performance.getEntriesByName(“first-contentful-paint”)[0].startTime - navigationStart

使用performance.now()精确计算程序执行时间

performance.now方法返回当前网页自从performance.timing.navigationStart到当前时间之间的微秒数(毫秒的千分之一)。也就是说,它的精度可以达到 100 万分之一秒。

使用performance.mark以及performance.measure手动测量性能

一般用于自定义搜集性能数据指标 做前端的性能监控系统

控制台Performance

WX20220107-150213.png

1:工具条,包含录制,刷新页面分析,清除结果等一系列操作

2:总览图,高度概括随时间线的变动,包括 FPS,CPU,NET

3:火焰图,从不同的角度分析框选区域 。例如:Network,Frames, Interactions, Main 等

4:总体报告:精确到毫秒级的分析,以及按调用层级,事件分类的整理

Memory

WX20220107-154730.png

LightHouse

lighthouse03.jpg

参考链接

最全的前端性能定位总结

koa-etag源码解析

介绍

Etag support for Koa responses using etag.

意为使用etag库,设置koa响应头Etag

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const conditional = require('koa-conditional-get');
const etag = require('koa-etag');
const Koa = require('koa');
const app = new Koa();

// etag works together with conditional-get
app.use(conditional());
app.use(etag());

app.use(function (ctx) {
ctx.body = 'Hello World';
});

app.listen(3000);

console.log('listening on port 3000');

源码 koa-etag 4.0.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

'use strict'
const calculate = require('etag')
const Stream = require('stream')
const promisify = require('util').promisify
const fs = require('fs')

const stat = promisify(fs.stat)

/**
* Expose `etag`.
* Add ETag header field.
* @param {object} [options] see https://github.com/jshttp/etag#options
* @param {boolean} [options.weak]
* @return {Function}
* @api public
*/
module.exports = function etag (options) {
return async function etag (ctx, next) {
// 先进行后续的中间件逻辑,之后再处理etag
await next()
const entity = await getResponseEntity(ctx)
setEtag(ctx, entity, options)
}
}

async function getResponseEntity (ctx) {
// 如果没有body,或已存在etag,则return跳出
const body = ctx.body
if (!body || ctx.response.get('etag')) return

// 获取http状态码 第一位数字
const status = ctx.status / 100 | 0

// 2xx 除2xx的非正常情况都跳出
if (status !== 2) return

// 1、如果为流,返回fs.stat调用后的文件信息对象
// 2、如果为字符串或buffer则直接返回
// 3、否则转为字符串返回
if (body instanceof Stream) {
if (!body.path) return
return await stat(body.path)
} else if ((typeof body === 'string') || Buffer.isBuffer(body)) {
return body
} else {
return JSON.stringify(body)
}
}

function setEtag (ctx, entity, options) {
// 如果body,即响应体不存在则不设置tag
if (!entity) return

// 调用etag能力并设置到响应头etag中
ctx.response.etag = calculate(entity, options)
}

参考链接:

copy-to源码解析

介绍

copy an object’s properties to another one, include propertiy, getter and setter.

意思是从另一个对象中复制属性,其中包括属性,getter和setter访问器函数。

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var copy = require('copy-to');

var src = {
_name: 'foo',
set name(val) {
this._name = val;
},
get name() {
return this._name;
},
show: function () {
console.log(this._name);
}
};

var des = {
_name: 'bar'
};

copy(src).to(des);
copy(src).toCover(des);
copy(src).pick('_name', 'name').to(des);
copy(src).pick('_name', 'name').toCover(des);

copy(src).withAccess().and(other).to(des); // 包括构造器属性

源码 copy-to.js 2.0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
'use strict';
var slice = Array.prototype.slice;

/**
* Expose copy
*
* ```
* copy({foo: 'nar', hello: 'copy'}).to({hello: 'world'});
* copy({foo: 'nar', hello: 'copy'}).toCover({hello: 'world'});
* ```
*
* @param {Object} src
* @return {Copy}
*/

module.exports = Copy;

/**
* 核心的copy方法
* @param {Object} src
* @param {Boolean} withAccess
*/
function Copy(src, withAccess) {
// 一种写法的兼容,可以直接调用Copy相当于new Copy
if (!(this instanceof Copy)) return new Copy(src, withAccess);
// 设置源对象和目标对象
this.src = src;
this._withAccess = withAccess;
}

/**
* 复制属性包括 getter 和 setter
* @param {[type]} val [description]
* @return {[type]} [description]
*/
Copy.prototype.withAccess = function (w) {
this._withAccess = w !== false;
return this;
};

/**
* 指定 src 对象的某些属性
* @api: public
*/
Copy.prototype.pick = function (keys) {
if (!Array.isArray(keys)) {
keys = slice.call(arguments);
}
if (keys.length) {
this.keys = keys;
}
return this;
};

/**
* copy src to target,
* do not cover any property target has
* @param {Object} to
* @api: public
*/
Copy.prototype.to = function (to) {
to = to || {};

if (!this.src) return to;
var keys = this.keys || Object.keys(this.src);

// _withAccess默认undefined即不考虑构造器,调用
if (!this._withAccess) {
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (to[key] !== undefined) continue;
to[key] = this.src[key];
}
return to;
}

for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// 如果该属性存在跳过,即不覆盖
if (!notDefined(to, key)) continue;
var getter = this.src.__lookupGetter__(key);
var setter = this.src.__lookupSetter__(key);
if (getter) to.__defineGetter__(key, getter);
if (setter) to.__defineSetter__(key, setter);

if (!getter && !setter) {
to[key] = this.src[key];
}
}
return to;
};

/**
* 从src对象复制到to对象,并且覆盖目标对象已有的属性
* @param {Object} to
* @api: public
*/
Copy.prototype.toCover = function (to) {
var keys = this.keys || Object.keys(this.src);

for (var i = 0; i < keys.length; i++) {
var key = keys[i];
delete to[key];
var getter = this.src.__lookupGetter__(key);
var setter = this.src.__lookupSetter__(key);
if (getter) to.__defineGetter__(key, getter);
if (setter) to.__defineSetter__(key, setter);

if (!getter && !setter) {
to[key] = this.src[key];
}
}
};

Copy.prototype.override = Copy.prototype.toCover;

/**
* 添加其他对象到src中
* @param {Obj} obj
* @return {Copy}
*/
Copy.prototype.and = function (obj) {
var src = {};
this.to(src);
this.src = obj;
this.to(src);
this.src = src;

return this;
};

/**
* 检查 obj[key] 是否存在
* @param {Object} obj
* @param {String} key
* @return {Boolean}
*/
function notDefined(obj, key) {
return obj[key] === undefined
&& obj.__lookupGetter__(key) === undefined
&& obj.__lookupSetter__(key) === undefined;
}

__lookupGetter__

该方法会返回当前对象上指定属性的属性读取访问器函数(getter)。目前已废弃,不推荐使用。

__defineGetter__

该方法可以将一个函数绑定在当前对象的指定属性上,当那个属性的值被读取时,你所绑定的函数就会被调用。目前已废弃,不推荐使用。

参考链接:

koa-bodyparser源码解析

介绍

A body parser for koa, based on co-body. support json, form and text type body.

意思是koa的body解析器,基于co-body,支持jsonformtext类型的请求体。

demo:

1
2
3
4
5
6
7
8
9
10
11
var Koa = require('koa');
var bodyParser = require('koa-bodyparser');

var app = new Koa();
app.use(bodyParser());

app.use(async ctx => {
// the parsed body will store in ctx.request.body
// if nothing was parsed, body will be an empty object {}
ctx.body = ctx.request.body;
});

源码 koa-bodyparser 4.3.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
'use strict';
const parse = require('co-body');
const copy = require('copy-to');

/**
* @param [Object] opts
* - {String} jsonLimit default '1mb'
* - {String} formLimit default '56kb'
* - {string} encoding default 'utf-8'
* - {Object} extendTypes
*/

module.exports = function (opts) {
opts = opts || {};
const { detectJSON } = opts;
const { onerror } = opts;

// 获取可用类型,以及各类型可用的布尔值
const enableTypes = opts.enableTypes || ['json', 'form'];
const enableForm = checkEnable(enableTypes, 'form');
const enableJson = checkEnable(enableTypes, 'json');
const enableText = checkEnable(enableTypes, 'text');
const enableXml = checkEnable(enableTypes, 'xml');

opts.detectJSON = undefined;
opts.onerror = undefined;

// 使 co-body 返回 raw body
opts.returnRawBody = true;

// 默认的 json 类型
const jsonTypes = [
'application/json',
'application/json-patch+json',
'application/vnd.api+json',
'application/csp-report'
];

// 默认的 form 类型
const formTypes = ['application/x-www-form-urlencoded'];

// 默认的 text 类型
const textTypes = ['text/plain'];

// 默认的 xml 类型
const xmlTypes = ['text/xml', 'application/xml'];

// 格式化成对应类型的参数
const jsonOpts = formatOptions(opts, 'json');
const formOpts = formatOptions(opts, 'form');
const textOpts = formatOptions(opts, 'text');
const xmlOpts = formatOptions(opts, 'xml');

// 这里通过参数扩展自定义的type类型
const extendTypes = opts.extendTypes || {};
extendType(jsonTypes, extendTypes.json);
extendType(formTypes, extendTypes.form);
extendType(textTypes, extendTypes.text);
extendType(xmlTypes, extendTypes.xml);

// 返回中间件
return async function bodyParser(ctx, next) {
// 如果请求体为空或禁用则跳出
if (ctx.request.body !== undefined || ctx.disableBodyParser)
return await next();
try {
const res = await parseBody(ctx);
ctx.request.body = 'parsed' in res ? res.parsed : {};
if (ctx.request.rawBody === undefined) ctx.request.rawBody = res.raw;
} catch (err) {
if (onerror) {
// 自定义错误处理函数
onerror(err, ctx);
} else {
throw err;
}
}

await next();
};

// 使用co-body的parse转换请求内容
async function parseBody(ctx) {
if (
enableJson &&
((detectJSON && detectJSON(ctx)) || ctx.request.is(jsonTypes))
) {
return await parse.json(ctx, jsonOpts);
}

if (enableForm && ctx.request.is(formTypes)) {
return await parse.form(ctx, formOpts);
}

if (enableText && ctx.request.is(textTypes)) {
return (await parse.text(ctx, textOpts)) || '';
}

if (enableXml && ctx.request.is(xmlTypes)) {
return (await parse.text(ctx, xmlOpts)) || '';
}

return {};
}
};

function formatOptions(opts, type) {
const res = {};
// 将opts复制到res中并返回
copy(opts).to(res);
res.limit = opts[type + 'Limit'];
return res;
}

function extendType(original, extend) {
if (extend) {
if (!Array.isArray(extend)) {
extend = [extend];
}

extend.forEach(function (extend) {
original.push(extend);
});
}
}

function checkEnable(types, type) {
return types.includes(type);
}

request的is判断:
1
2
3
is (type, ...types) {
return typeis(this.req, type, ...types)
},

​ 可以看到这边bodyparser确实没有过多的逻辑,主要是使用co-body这个库去处理对应类型的数据。

参考链接:

parseurl源码解析

介绍

Parse a URL with memoization.

意思是转化URL对象,使用缓存,基于node的url.parse的能力。

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
parseurl({url:'http://www.baidu.com?q=ni&s=shuo&a=shenme'})

Url {
protocol: 'http:',
slashes: true,
auth: null,
host: 'www.baidu.com',
port: null,
hostname: 'www.baidu.com',
hash: null,
search: '?q=ni&s=shuo&a=shenme',
query: 'q=ni&s=shuo&a=shenme',
pathname: '/',
path: '/?q=ni&s=shuo&a=shenme',
href: 'http://www.baidu.com/?q=ni&s=shuo&a=shenme',
_raw: 'http://www.baidu.com?q=ni&s=shuo&a=shenme'
}

源码 parseurl 1.3.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
'use strict'

var url = require('url')
var parse = url.parse
var Url = url.Url

module.exports = parseurl
module.exports.original = originalurl

/**
* parse转化 `req` 中的 url,同时使用缓存
* @param {ServerRequest} req
* @return {Object}
* @public
*/
function parseurl(req) {
var url = req.url

if (url === undefined) {
// URL 是 undefined 时直接返回
return undefined
}

var parsed = req._parsedUrl

if (fresh(url, parsed)) {
// 如果当前记录的为最新的 url,则直接返回之前parse过的结果
return parsed
}

// 转化 URL
// 转化前url存在_raw,转化后url存在_parsedUrl
// _parsedUrl即为缓存的内容
parsed = fastparse(url)
parsed._raw = url

return (req._parsedUrl = parsed)
};

/**
* 转化 `req`对象的 original url
*
* @param {ServerRequest} req
* @return {Object}
* @public
*/
function originalurl(req) {
var url = req.originalUrl

if (typeof url !== 'string') {
// Fallback
return parseurl(req)
}

var parsed = req._parsedOriginalUrl

if (fresh(url, parsed)) {
// Return cached URL parse
return parsed
}

// Parse the URL
parsed = fastparse(url)
parsed._raw = url

return (req._parsedOriginalUrl = parsed)
};

/**
* Parse the `str` url with fast-path short-cut.
*
* @param {string} str
* @return {Object}
* @private
*/
function fastparse(str) {
// 如果参数不为字符串则使用node中的url.parse去转换
if (typeof str !== 'string' || str.charCodeAt(0) !== 0x2f /* / */) {
return parse(str)
}

var pathname = str
var query = null
var search = null

// This takes the regexp from https://github.com/joyent/node/pull/7878
// Which is /^(\/[^?#\s]*)(\?[^#\s]*)?$/
// And unrolls it into a for loop
// 通过判断 ? 来截取后边的 query字串
for (var i = 1; i < str.length; i++) {
switch (str.charCodeAt(i)) {
case 0x3f: /* ? */
if (search === null) {
pathname = str.substring(0, i)
query = str.substring(i + 1)
search = str.substring(i)
}
break
case 0x09: /* \t */
case 0x0a: /* \n */
case 0x0c: /* \f */
case 0x0d: /* \r */
case 0x20: /* */
case 0x23: /* # */
case 0xa0:
case 0xfeff:
return parse(str)
}
}

var url = Url !== undefined
? new Url()
: {}

url.path = str
url.href = str
url.pathname = pathname

if (search !== null) {
url.query = query
url.search = search
}

return url
}

/**
* 判断之前转化过的 url 是否是最新的
* @param {string} url
* @param {object} parsedUrl
* @return {boolean}
* @private
*/
function fresh(url, parsedUrl) {
// 判断 parsedUrl 为不为空的对象,且为Url的实例,且raw记录的url为当前传入的url
return typeof parsedUrl === 'object' &&
parsedUrl !== null &&
(Url === undefined || parsedUrl instanceof Url) &&
parsedUrl._raw === url
}

参考链接:

object-is源码解析

介绍

这是 es-shims 小组提供的解决方案,用于在不支持es6的环境中使用 Object.is() 方法。众所周知,要优先使用 === 而不是 == ,但三等也存在一定问题,而 Object.is 则弥补了他。

Object.is=== 的区别就在于 +0 不等于 -0NaN 等于 NaN

源码 object-is 1.1.5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use strict';

// 自身不等则一定是NaN
var numberIsNaN = function (value) {
return value !== value;
};

module.exports = function is(a, b) {
// 排除 +0 === -0 为true,因为 +Infinity !== -Infinity
if (a === 0 && b === 0) {
return 1 / a === 1 / b;
}
if (a === b) {
return true;
}
// 排除 NaN === NaN 为false
if (numberIsNaN(a) && numberIsNaN(b)) {
return true;
}
return false;
};


参考链接:

a标签/js下载文件的解决方案

a标签/js下载文件的解决方案

前段时间工作多次遇到过下载文件的问题,这里做个总结。因为浏览器兼容性问题,大体分为三种解决方案。

window.open设置源文件地址

1
window.open("域名/template.xlsx(文件名)");

这个方法一般是后端返回的一个资源地址,它是服务器上的某个位置的静态资源。该方法没有兼容性问题,但整体体验有撕裂感,在现代浏览器不太推荐。

a标签提供下载

1
<a href="域名/template.xlsx(文件名)">下载文件</a>

chrome浏览器会直接下载文件,IE则会弹窗提示保存文件。

ajax + blob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function getBlob(url) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest();

xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response);
}
};

xhr.send();
});
}

function saveAs(blob, filename) {
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, filename);
} else {
const link = document.createElement('a');

link.href = window.URL.createObjectURL(blob);
link.download = filename;
link.click();

window.URL.revokeObjectURL(link.href);
}
}

function download(url, filename) {
getBlob(url).then(blob => {
saveAs(blob, filename);
});
}

// For Example
download('https://github.com/vuejs/vue-router', 'vue-router.html');

这个方案主要使用ajax获取文件内容,转为blob,再利用a标签的点击行为触发下载,但是不管是点击事件还是blob的转换都有一定兼容性问题。

社区下载解决方案

FileSaver.js

FileSaver.js是在客户端保存文件的解决方案,非常适合在客户端生成文件的web应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// 获取当前全局对象
var _global = typeof window === 'object' && window.window === window
? window : typeof self === 'object' && self.self === self
? self : typeof global === 'object' && global.global === global
? global
: this

// 返回blob对象
function bom(blob, opts) {
if (typeof opts === 'undefined') opts = { autoBom: false }
else if (typeof opts !== 'object') {
console.warn('Deprecated: Expected third argument to be a object')
opts = { autoBom: !opts }
}

// 为UTF-8 XML 和 text/* 类型 (包括 HTML)准备 BOM
// 笔记: 浏览器会自动转换 UTF-16 U+FEFF 到 EF BB BF
if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type })
}
return blob
}

// 主要的download下载函数,通过创建XMLHttpRequest发送请求
function download(url, name, opts) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.onload = function () {
// 在拿到响应的数据后,根据不同客户端情况,有不同解决方案
saveAs(xhr.response, name, opts)
}
xhr.onerror = function () {
console.error('could not download file')
}
xhr.send()
}

function corsEnabled(url) {
var xhr = new XMLHttpRequest()
// use sync to avoid popup blocker
xhr.open('HEAD', url, false)
try {
xhr.send()
} catch (e) { }
return xhr.status >= 200 && xhr.status <= 299
}

// `a.click()` 不是在所有浏览器都起作用 (#465)
// 这里是一种兼容写法,dispatchEvent派发click点击事件,若抛错。则创建并执行自定义的鼠标点击事件
function click(node) {
try {
node.dispatchEvent(new MouseEvent('click'))
} catch (e) {
var evt = document.createEvent('MouseEvents')
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
20, false, false, false, false, 0, null)
node.dispatchEvent(evt)
}
}

// Detect WebView inside a native macOS app by ruling out all browsers
// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent)

var saveAs = _global.saveAs || (
// 可能处在web worker中,无法通过window访问,默认不执行其他操作
(typeof window !== 'object' || window !== _global)
? function saveAs() { /* noop */ }

// 尽可能先用download属性 (#193 Lumia mobile) 除非它是macOS类浏览器
: ('download' in HTMLAnchorElement.prototype && !isMacOSWebView)
? function saveAs(blob, name, opts) {
var URL = _global.URL || _global.webkitURL
var a = document.createElement('a')
name = name || blob.name || 'download'

a.download = name
a.rel = 'noopener' // tabnabbing

// TODO: detect chrome extensions & packaged apps
// a.target = '_blank'

if (typeof blob === 'string') {
// Support regular links
a.href = blob
if (a.origin !== location.origin) {
corsEnabled(a.href)
? download(blob, name, opts)
: click(a, a.target = '_blank')
} else {
click(a)
}
} else {
// 支持blobs时
a.href = URL.createObjectURL(blob)
setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s
setTimeout(function () { click(a) }, 0)
}
}

// 使用msSaveOrOpenBlob作为第二方案,兼容IE10以上
: 'msSaveOrOpenBlob' in navigator
? function saveAs(blob, name, opts) {
name = name || blob.name || 'download'

if (typeof blob === 'string') {
if (corsEnabled(blob)) {
download(blob, name, opts)
} else {
var a = document.createElement('a')
a.href = blob
a.target = '_blank'
setTimeout(function () { click(a) })
}
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name)
}
}

// 最后保守方案使用FileReader和弹出窗口
: function saveAs(blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open('', '_blank')
if (popup) {
popup.document.title =
popup.document.body.innerText = 'downloading...'
}

if (typeof blob === 'string') return download(blob, name, opts)

var force = blob.type === 'application/octet-stream'
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)

if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') {
// Safari不支持blob URLs的下载
var reader = new FileReader()
reader.onloadend = function () {
var url = reader.result
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
if (popup) popup.location.href = url
else location = url
popup = null // reverse-tabnabbing #460
}
reader.readAsDataURL(blob)
} else {
var URL = _global.URL || _global.webkitURL
var url = URL.createObjectURL(blob)
if (popup) popup.location = url
else location.href = url
popup = null // reverse-tabnabbing #460
setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s
}
}
)

_global.saveAs = saveAs.saveAs = saveAs

if (typeof module !== 'undefined') {
module.exports = saveAs;
}

1、其中获取全局对象,值得说道得是除了window,self也指向window,还有node环境中的global。

2、因为click()的不兼容,而自行派发鼠标点击事件,node.dispatchEvent(new MouseEvent('click'))

iview

iView中的table导出功能,也是提供下载。兼容方式大体是前文讲到的三种方法,写法比较清晰。

iview/src/components/table/export-csv.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
function has(browser) {
const ua = navigator.userAgent;
if (browser === 'ie') {
const isIE = ua.indexOf('compatible') > -1 && ua.indexOf('MSIE') > -1;
if (isIE) {
const reIE = new RegExp('MSIE (\\d+\\.\\d+);');
reIE.test(ua);
return parseFloat(RegExp['$1']);
} else {
return false;
}
} else {
return ua.indexOf(browser) > -1;
}
}

const csv = {
_isIE11() {
let iev = 0;
const ieold = (/MSIE (\d+\.\d+);/.test(navigator.userAgent));
const trident = !!navigator.userAgent.match(/Trident\/7.0/);
const rv = navigator.userAgent.indexOf('rv:11.0');

if (ieold) {
iev = Number(RegExp.$1);
}
if (navigator.appVersion.indexOf('MSIE 10') !== -1) {
iev = 10;
}
if (trident && rv !== -1) {
iev = 11;
}

return iev === 11;
},

_isEdge() {
return /Edge/.test(navigator.userAgent);
},

_getDownloadUrl(text) {
const BOM = '\uFEFF';
// Add BOM to text for open in excel correctly
if (window.Blob && window.URL && window.URL.createObjectURL) {
const csvData = new Blob([BOM + text], { type: 'text/csv' });
return URL.createObjectURL(csvData);
} else {
return 'data:attachment/csv;charset=utf-8,' + BOM + encodeURIComponent(text);
}
},

download(filename, text) {
if (has('ie') && has('ie') < 10) {
// has module unable identify ie11 and Edge
const oWin = window.top.open('about:blank', '_blank');
oWin.document.charset = 'utf-8';
oWin.document.write(text);
oWin.document.close();
oWin.document.execCommand('SaveAs', filename);
oWin.close();
} else if (has('ie') === 10 || this._isIE11() || this._isEdge()) {
const BOM = '\uFEFF';
const csvData = new Blob([BOM + text], { type: 'text/csv' });
navigator.msSaveBlob(csvData, filename);
} else {
const link = document.createElement('a');
link.download = filename;
link.href = this._getDownloadUrl(text);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
};

export default csv;

参考链接

koa-compose源码解析

介绍

Compose middleware.

compose 函数是 koa 洋葱模型的核心,作为一个 koa 中间件使用,他会创建返回一个递归函数,借由每个中间件的 next 调用 dispatch、传递 context,不断改变内存中的 index 索引,进而做出终止判断。

不难看出,这是闭包的一种使用方式,index 和 dispatch 保存在内存中便于后续调用。

阅读更多...

给阿姨来一杯卡普基诺~

支付宝
微信