WXInlinePlayer的loader部分。
loader下载器主要暴露一个方法Promise read(void)
,外部用法是:
1
2
3
4
5
|
this.loader.read().then(data => {
if (data.length) {
this.processor.process(data);
}
});
|
也就是说,我们自定义的下载器实现这个方法,也能代替默认的下载器。自带的下载器有两种,一种是chunk.js,一种是stream.js。
chunk加载器
我们先来看看chunk.js的代码。
1
2
3
4
5
6
7
|
read() {
if (!this.done) {
return this._request();
}
return Promise.resolve(new Buffer(0));
}
|
这里只是简单调用this._request
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
|
_request() {
let isSuccess = false;
let promise = this._fetch();
for (let i = 0; i < MAX_REQ_RETRY; i++) {
promise = promise
.then(buffer => {
if (!isSuccess) {
isSuccess = true;
this.downloadSize += buffer.length;
if (buffer.length < this.chunkSize) {
this.done = true; // 获取到的数据不足chunk大小,表明文件下载完了
}
}
if (supportSharedBuffer) { // 支持sharedBuffer的浏览器就用,避免后续多次拷贝
let sharedBuffer = new SharedArrayBuffer(buffer.byteLength);
let result = new Uint8Array(sharedBuffer);
result.set(buffer);
buffer = result;
}
return buffer;
})
.catch(e => {
if (i >= MAX_REQ_RETRY - 1) {
if (!this.emitted) { // 只会抛出事件一次
this.emitted = true;
this.emit('loadError', e); // 失败次数超过限制,抛出事件
}
throw e;
} else {
return this._fetch();
}
});
}
return promise;
}
|
this._request
会调用this._fetch
去获取数据,并在失败的时候重试MAX_REQ_RETRY
次。
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
|
_fetch() {
return new Promise((resolve, reject) => {
const endIndex = this.startIndex + this.chunkSize;
this.xhr = new XMLHttpRequest();
this.xhr.open('GET', this.url);
this.xhr.responseType = 'arraybuffer';
this.xhr.setRequestHeader(
'Range',
`bytes=${this.startIndex}-${endIndex}` //通过range控制每次只下载chunk大小的数据
);
this.xhr.onerror = e => {
reject({
status: -1,
statusText: 'unknown error',
detail: e
});
};
this.xhr.onload = () => {
if (this.xhr.readyState == 4) {
if (this.xhr.status >= 200 && this.xhr.status <= 299) {
if (!this.emitted) { // 只会抛出事件一次
this.emitted = true;
this.emit('loadSuccess');
}
this.startIndex = endIndex + 1;
resolve(new Uint8Array(this.xhr.response));
} else {
reject({
status: this.xhr.status,
statusText: this.xhr.statusText,
detail: String.fromCharCode.apply(
null,
new Uint8Array(this.xhr.response)
)
});
}
}
};
this.xhr.send();
});
}
|
可以看到,chunk.js的方法是通过range方法每次下载chunk大小的数据,失败的时候会重试一定次数。
stream加载器
看stream.js之前,我们发现如下
1
2
3
4
5
|
function Loader({ type = 'chunk', opt }) {
return type == 'chunk'
? new ChunkLoader(opt)
: new (Util.workerify(StreamLoader, ['read', 'cancel', 'hasData']))(opt);
}
|
从Util.workerify
我们猜测,这是启动了一个web worker,下面我们求证一下这个想法,注意这里我们调整了一下代码顺序,方便理解:
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
|
getFuncBody(funcStr) {
return funcStr
.trim()
.match(/^function\s*\w*\s*\([\w\s,]*\)\s*{([\w\W]*?)}$/)[1];
}
workerify(func, methods = []) {
// StreamLoader是将全部代码都放到一个function里面,getFuncBody则从这个函数的
// 字符串中,提取函数内部的代码,从而拿到一个完整的js文件
const funcStr = this.getFuncBody(func.toString());
function __Worker__(data) {
EventEmitter.call(this);
this.id = 0;
this.resolves = [];
const blob = new Blob([funcStr], { type: 'text/javascript' }); // 用函数体代码创建一个Blob
this.url = URL.createObjectURL(blob); // 给这个blob创建对象URL
this.worker = new Worker(this.url); // 创建web worker
this.worker.onmessage = message => { // worker完成工作后,返回结果
const { id, data, destroy, type } = message.data;
if (destroy) {
this.resolves = [];
URL.revokeObjectURL(this.url);
this.worker.terminate();
this.worker = null;
} else if (type == 'event') {
this.emit(data.type, data.data); // 对于Streamloader,是转发loadError和loadSuccess事件
} else {
for (let i = this.resolves.length - 1; i >= 0; i--) {
if (id == this.resolves[i].id) {
this.resolves[i].resolve(data); // 在这里resolve之前调用method返回的promise
this.resolves.splice(i, 1);
break;
}
}
}
};
this.worker.postMessage({ type: 'constructor', id: this.id++, data });
}
inherits(__Worker__, EventEmitter);
for (let i = 0; i < methods.length; i++) {
const type = methods[i];
__Worker__.prototype[type] = function(data) { // 让本对象实现methods方法
return new Promise((resolve, reject) => { // 返回的是Promise,因为是在worker中完成的,无法立刻返回结果
const id = this.id++;
this.resolves.push({ id, resolve, reject });
if (this.worker) {
this.worker.postMessage({ type, id, data }); // 通过postmessage让worker去执行具体动作
}
});
};
}
return __Worker__;
}
|
下面我们看一下stream.js的原理。
1
2
3
4
5
6
7
8
9
10
11
|
StreamLoader.prototype.read = function() {
if (this.data.length < this.chunkSize) { // 目前的缓冲数据不满足chunk大小
if (this.done) {
return this._getChunk(); // 如果是文件到EOF了,那么也只能直接返回了
}
return this._request().then(() => {
return this._getChunk(); // 如果文件没结束,那么请求后再返回chunk数据
});
}
return this._getChunk(); // 目前的缓冲数据大于chunk大小
};
|
看起来本地维护了一些数据,不用每次从网络请求。
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
|
function slice(buffer, startIndex, endIndex) {
if (!endIndex || endIndex - startIndex > buffer.length) {
endIndex = buffer.length;
}
if(supportSharedBuffer){ // 支持sharedArray的时候用它减少后续拷贝
let sharedBuffer = new SharedArrayBuffer(endIndex - startIndex);
let result = new Uint8Array(sharedBuffer);
result.set(buffer.subarray(startIndex, endIndex));
return result;
}else{
return buffer.subarray(startIndex, endIndex);
}
}
StreamLoader.prototype._getChunk = function() {
return new Promise(resolve => {
const buffer = slice(this.data, 0, this.chunkSize);
this.data = slice(
this.data,
this.data.length <= this.chunkSize ? this.data.length : this.chunkSize
);
resolve(buffer);
});
};
|
从this.data
中拿出chunk大小的数据,如果数据不足,就全部拿出来。取数据出来的时候和chunk.js一样,如果支持sharedArrayBuffer,就传sharedArrayBuffer。
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
|
function concat(i, j) {
const buffer = new Uint8Array(i.length + j.length);
buffer.set(new Uint8Array(i), 0);
buffer.set(new Uint8Array(j), i.length);
return buffer;
}
StreamLoader.prototype._request = function() {
if (this.reader) { // 看之前请求过没,请求过就能拿到reader
return this.reader.read().then(result => {
let { value, done } = result;
value = new Uint8Array(value ? value : 0);
this.data = concat(this.data, value); // 将数据拼到this.data里面
if (done) {
this.done = true; // 是否遇到EOF
} else if (this.data.length < this.chunkSize) {
return this._request(); // 读到数据,但是数据不足,递归调用自己
}
});
} else {
return fetch(this.url, { // 只在第一次调用fetch,拿到reader
method: 'GET'
})
.then(resp => {
const { status, statusText } = resp;
if (status < 200 || status > 299) {
return resp.text().then(text => {
self.postMessage({
type: 'event',
data: {
type: 'loadError', // 状态码不是2xx就报错,是就返回加载成功
data: { status, statusText, detail: text }
}
});
});
}
self.postMessage({ type: 'event', data: { type: 'loadSuccess' } });
this.reader = resp.body.getReader();
return this._request();
})
.catch(e => {
self.postMessage({
type: 'event',
data: {
type: 'loadError', // fetch失败也报错
data: { status: -1, statusText: 'unknown error', detail: e }
}
});
});
}
};
|
总结
chunk.js是每次获取一块数据都是通过发起HTTP请求,通过Range字段控制下载的范围;而stream.js则是通过fetch的流API,只发起一次http请求,后续不断读取块数据。
文章作者
shandowc
上次更新
2020-07-15
(7a1c413)