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请求,后续不断读取块数据。