当前位置:   article > 正文

大视频上传 分片及显示进度 vue_axios 分片播放视频数据流

axios 分片播放视频数据流
封装一个js文件
/*
 * @Descripttion: 
 * @version: 
 * @Author: xumingcai
 * @Date: 2020-04-15 17:58:03
 * @LastEditors: xumingcai
 * @LastEditTime: 2020-04-20 16:22:45
 */
import SparkMD5 from 'spark-md5';
import axios, { CancelToken, Cancel} from 'axios';
import { getToken } from '@/core/auth.js';

/**
 * 默认配置
 */
var DEFAULT_OPTIONS = {
  url: '',    // 上传地址
  chunkSize: 2 * 1024 * 1024,  // 分块大小
  retryCount: 5,  // 上传失败重试次数
};
/**
 * 状态集合
 */
export var STATE = {
  PREPARE: 0,               // 预备上传
  HASH_CALC_ING: 1001,      // hash值计算中
  HASH_CALC_FAILED: 1002,   // hash值计算失败
  HASH_CALC_SUCCESS: 1003,  // hash值计算成功
  UPLOAD_ING: 2001,          // 上传中
  UPLOAD_CANCEL: 2002,      // 已取消上传
  UPLOAD_STOP: 2003,        // 上传停止
  UPLOAD_FAILED: 2004,      // 上传失败
  UPLOAD_SUCCESS: 2005,     // 上传成功
};

/**
 * 事件类型
 */
var EVENT_TYPES = {
  ERROR: 'error'
};

/**
 * 获取字符串
 * @param {*} o 非字符串
 */
var toString = o => Object.prototype.toString.call(o);

/**
 * 对象数组转数组
 * @param {Object} o 对象数组o
 */
var toArray = o => [].slice.call(o, 0);

/**
 * 获取文件md5加密
 * @param {*} file 文件
 */
export function getFileMd5 (file) {
  if (!file) return Promise.reject(new Error('文件不存在'));
  const blobSlice =
    File.prototype.slice ||
    File.prototype.mozSlice ||
    File.prototype.webkitSlice;
  const chunkSize = 2097152;
  const chunks = Math.ceil(file.size / chunkSize);
  let currentChunk = 0;
  const spark = new SparkMD5.ArrayBuffer();
  const fileReader = new FileReader();
  return new Promise(function (resolve, reject) {
    function loadNext () {
      var start = currentChunk * chunkSize;
      var end = start + chunkSize >= file.size ? file.size : start + chunkSize;
      fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
    }
    fileReader.onload = function (e) {
      spark.append(e.target.result);
      currentChunk += 1;
      if (currentChunk < chunks) {
        loadNext();
      } else {
        resolve(spark.end());
      }
    };
    fileReader.onerror = function (error) {
      reject(error);
    };
    loadNext();
  });
}

/**
 * 分片上传异常类
 * @param {string} message 异常信息
 */
export function ShardUploadError (message, code, stack) {
  this.name = 'ShardUploadError';
  this.code = code || 1;
  this.message = message || '上传失败';
  this.stack = stack || (new Error()).stack;
}

// 继承
ShardUploadError.prototype = Object.create(Error.prototype);
ShardUploadError.prototype.constructor = ShardUploadError;

// 继承异常code
Object.assign(ShardUploadError, {
  FILE_HASH_CALC_FAILED: 1002,        // 文件hash值计算异常
  FILE_CHUNK_UPLOAD_FAILED: 3001,     // 文件块上传失败
  FILE_UPLOAD_FAILED: 2004,           // 上传失败
  FILE_UPLOAD_CORRUPTION: -303,       // 文件上传损坏
  FILE_SPLICING_FAILED: -302,         // 服务器文件上传成功拼接失败
  PARAMETER_ERROR: 4001,              // 参数错误
  CANCEL_FAILED: 5001,                // 取消失败
});

/**
 * 处理参数为正常
 * @param {number} value 参数值
 * @param {number} defaultValue 默认值
 */
function normalizeInteger (value, defaultValue) {
  value = parseInt(value);
  if (isNaN(value)) { // 不是数字
    value = defaultValue;
  }
  return value;
}

/**
 * 分片上传类
 * @param {File} file 文件对象
 * @param {Object} options 配置
 */
export function ShardUpload (file, options = {}) {
  // 非new创建对象
  if (!(this instanceof ShardUpload)) return new ShardUpload(file, options);

  // 校验文件
  if (!file || !(file instanceof File)) throw new ShardUploadError('请传入File对象', ShardUploadError.PARAMETER_ERROR);

  // 处理配置
  this.originOptions = options;
  options = Object.assign({}, DEFAULT_OPTIONS, options);
  // 是否是字符串
  if (typeof options.url !== 'string') options.url = toString(options.url);
  options.chunkSize = normalizeInteger(options.chunkSize, DEFAULT_OPTIONS.chunkSize);
  options.retryCount = normalizeInteger(options.retryCount, DEFAULT_OPTIONS.retryCount);
  
  this.options = options;
  this.file = file;
  this.state = STATE.PREPARE;   // 初始化状态
  this.promise = null;          // 进行中的延迟对象
  this._events = Object.create(null);        // 初始化异常对象
}

var C = ShardUpload.prototype;

/**
 * 注册事件
 * @param {string|Array} 事件
 * @param {Function} 事件回调方法
 */
C.$on = function (event, fn) {
  if (Array.isArray(event)) { // 绑定多个事件
    for (var i = 0, l = event.length; i < l; i++) {
      this.$on(event, fn);
    }
  } else {
    (this._events[event] || (this._events[event] = [])).push(fn);
  }
  return this;
}
/**
 * 注册一次事件
 * @param {string|Array} 事件
 * @param {Function} 事件回调方法
 */
C.$once = function (event, fn) {
  var self = this;
  function on () {
    self.$off(event, fn);
    fn.apply(self, toArray(arguments));
  }
  on.fn = fn;
  this.$on(event, on);
  return this;
}
/**
 * 注销事件
 * @param {string|Array} 事件
 * @param {Function} 注销事件回调方法
 */
C.$off = function (event, fn) {
  var args = toArray(arguments);

  // 无参数,注销所有事件
  if (!args.length) {
    this._events = Object.create(null);
    return this;
  }

  // event是数组,注销多个事件
  if (Array.isArray(event)) {
    for (var i = 0, l = event.length; i < l; i++) {
      this.$off(event, fn);
    }
    return this;
  }

  // 是否有该事件
  var cbs = this._events[event];
  if (!cbs) {
    return this;
  }

  // 只有一个参数,注销回调方法
  if (args.length === 1) {
    this._events[event] = Object.create(null);
    return this;
  }

  if (fn) { // 方法是否为非;
    var cb;
    let i = cbs.length;
    while(i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {  // cb.fn === fn:检测单次绑定
        cbs.splice(i, 1);
        break;
      }
    }
  }
  return this;
}

/**
 * 触发事件
 * @param {string|Array} 事件
 */
C.$emit = function (event) {
  let cbs = this._events[event];
  if (cbs) {
    cbs = cbs.length ? toArray(cbs) : cbs;
    var args = toArray(arguments);
    for (var i = 0, l = cbs.length; i < l; i++) {
      cbs[i].apply(this, args.slice(1));
    }
  }
  return this;
}

/**
 * 开始上传
 */
C.start = function () {
  switch (this.state) {
    case STATE.HASH_CALC_FAILED:  // hash值计算失败
      this.state = STATE.PREPARE; // 标记为上传成功
      this.promise = this.startCalcHash();   // 继续上传
      break;
    case STATE.UPLOAD_STOP:     // 上传停止
    case STATE.UPLOAD_FAILED:   // 上传失败(五次失败)
      this.state = STATE.HASH_CALC_SUCCESS; // 标记为上传成功
      this.promise = this.startUpload(this.blockIndex);   // 继续上传
      break;
    default:
      this.promise = this.startCalcHash();   // 继续上传
      break;
  }
  return this.promise;
}

/**
 * 开始计算hash值
 */
C.startCalcHash = function () {
  // 当前是否是预备上传状态
  if (this.state !== STATE.PREPARE) return Promise.reject(new Error('预备状态才能计算hash值'));
  this.state = STATE.HASH_CALC_ING; // 标记hash值计算中
  this.$emit('start');
  this.promise = new Promise((resolve, reject) => {
    getFileMd5(this.file).then((hash) => {
      // 已取消上传
      if (this.state === STATE.UPLOAD_CANCEL) throw new Cancel('取消上传');
      this.hash = hash;
      this.state = STATE.HASH_CALC_SUCCESS; // 标记hash值计算成功
      resolve(hash);
    }).catch((error) => {
      this.state = STATE.HASH_CALC_FAILED;  // 标记hash值计算失败
      var shardUploadError = error instanceof Cancel ? error : new ShardUploadError(error.message, ShardUploadError.FILE_HASH_CALC_FAILED, error.stack);
      shardUploadError.stack = error.stack;
      this.$emit(EVENT_TYPES.ERROR, shardUploadError);
      reject(shardUploadError);
    });
  }).then(() => {
    return this.startUpload();
  });
  return this.promise;
}

/**
 * 开始上传
 * @param {number} startIndex 第几块开始上传
 */
C.startUpload = function (startIndex = 0) {
  // 检验参数
  startIndex = parseInt(startIndex);
  if (isNaN(startIndex)) startIndex = 0;
  if (startIndex < 0) startIndex = 0;
  if (this.state !== STATE.HASH_CALC_SUCCESS) return Promise.reject(new Error('hash值计算成功才能开始上传'));

  // 保存当前环境
  var resetCount = this.options.retryCount;     // 上传失败重试次数
  var chunkSize = this.options.chunkSize;       // 上传块大小
  var file = this.file;                         // 保存文件
  var fileSize = file.size;                     // 文件大小
  var retryCount = resetCount;                  // 设置重试次数
  var chunkTotal = Math.ceil(fileSize / chunkSize); // 总块数
  var hash = this.hash;   // 文件hash值
  var url = this.options.url;     // 上传文件地址
  var config = {
    headers: {
      'Content-Type': 'multipart/form-data',
      'Authorization': getToken()
    }
  };
  
  var self = this;
  /**
   * 上传块文件
   * @param {File} file 文件对象
   * @param {numbe} index 上传第几块
   */
  function fn (file, index) {
    var formData = new FormData();
    const nextSize = Math.min((index + 1) * chunkSize, file.size);
    const chunkData = file.slice(index * chunkSize, nextSize);
    formData.append('file', chunkData);  // 块文件
    formData.append('fileName', file.name); // 文件名
    formData.append('partSize', chunkData.size);  // 文件块大小
    formData.append('totalSize', file.size);    // 文件大小
    formData.append('md5Code', hash);   // 文件hash值
    formData.append('partNum', index);  // 当前上传的文件块索引
    formData.append('totalPartNum', chunkTotal);  // 总块数
    self.blockIndex = index;  // 正在上传第几个块的索引

    /**
     * 创建异常对象
     * @param {string} message 错误信息
     * @param {number|string} code 错误码
     */
    function createError (error, code) {
      var error = new ShardUploadError(error.message, code, error.stack);
      error.chunkIndex = index;  // 传第几个块
      error.chunkSize = chunkData.size;    // 块大小
      error.chunkTotal = chunkTotal;   // 块总数
      error.chunkFile = chunkData; // 块文件
      error.fileHash = hash;   // 文件hash值
      return error;
    }
    return new Promise((resolve, reject) => {
      axios.post(url, formData, {
        ...config,
        cancelToken: new CancelToken(function executor(c) {
          // executor 函数接收一个 cancel 函数作为参数
          // self.cancel = self.createCancelMethod(c);
          self._cancel = c;
        })
      }).then(res => {
        var data = res.data;
        if (data && (data.status === 200)) { // 上传成功
          if (!data.data) {
            retryCount = resetCount;  // 重置重试次数
            if (index < chunkTotal - 1) {
              fn(file, index + 1).then(resolve).catch(reject);
            } else {
              self.state = STATE.UPLOAD_SUCCESS;  // 标记为上传成功
              // 最后一个文件块上传成功
              resolve(data);
            }
            self.$emit('block-success', index + 1, chunkTotal);
          } else {  // 已上传过
            resolve(data);
            self.state = STATE.UPLOAD_SUCCESS;    // 标记为上传成功
            self.$emit('block-success', chunkTotal , chunkTotal);
          }
        } else {  // 失败
          var status = data.status;
          var error = createError(data, ShardUploadError.FILE_CHUNK_UPLOAD_FAILED);
          switch (status) {
            case -303:  // 文件拼接完成后计算出的md5与前端上传时计算的不一致,需要重新上传
              error.code = ShardUploadError.FILE_UPLOAD_CORRUPTION;
              self.$emit(EVENT_TYPES.ERROR, error);
              self.state = STATE.UPLOAD_FAILED; // 标记为上传失败
              self.blockIndex = 0;  // 重新从0开始
              reject(error);
              self.$emit('block-success', 0, chunkTotal);
              break;
            case -302:  // 文件上传成功,但是文件片拼接时出错,需要重新请求以再次拼接文件片
              error.code = ShardUploadError.FILE_SPLICING_FAILED;
            default:
              if (retryCount <= 1) {
                self.$emit(EVENT_TYPES.ERROR, error);
                self.state = STATE.UPLOAD_FAILED; // 标记为上传失败
                reject(error);
              } else {
                retryCount -= 1;
                fn(file, index).then(resolve).catch(reject);
              }
              break;
          }
          self.state = STATE.UPLOAD_FAILED; // 标记为上传失败
        }
      }).catch(response => {
        if (response instanceof Cancel || retryCount <= 1) {
          var error = response instanceof Cancel ? response : createError(response, ShardUploadError.FILE_CHUNK_UPLOAD_FAILED);
          self.$emit(EVENT_TYPES.ERROR, error);
          self.state = STATE.UPLOAD_FAILED; // 标记为上传失败
          reject(error);
        } else {
          retryCount -= 1;
          fn(file, index).then(resolve).catch(reject);
        }
      });
    });
  }
  this.state = STATE.UPLOAD_ING;  // 标记为开始上传
  this.promise = fn(file, startIndex);
  return this.promise;
}

/**
 * # 废弃
 * 创建取消方法
 * @param {Function} c 取消的调用方法
 */
C.createCancelMethod = function (c) {
  var self = this;
  return function () {
    if (self.state !== STATE.UPLOAD_ING)
      return Promise.reject(new ShardUploadError('上传中才能取消上传', ShardUploadError.CANCEL_FAILED));
    if (!c || typeof c !== 'function')
      return Promise.reject(new ShardUploadError('取消方法为空或类型非方法', ShardUploadError.CANCEL_FAILED));
    c.call(); // 调用取消方法
    self.state = STATE.UPLOAD_CANCEL;   // 标记为上传取消状态
    return Promise.resolve('取消成功');
  };
}

C.cancel = function () {
  typeof this._cancel === 'function' && this._cancel('取消上传');
  this.state = STATE.UPLOAD_CANCEL;   // 标记为上传取消状态
  return Promise.resolve('取消成功');
}
  • 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
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
//html中的代码	

import { ShardUpload } from "@/utils/shard-upload";  //把文件引入
   <el-upload
            ref="uploader"
            action
            class="avatar-uploader"
            :http-request="handleVideoSuccess"  
            :before-upload="beforeUploadVideo"
            :show-file-list="false"
            style="text-align: start;"
          >
            <video
              v-if="preVideoUrl !='' && !videoFlag"   
              :src="preVideoUrl"  //回显视频
              class="avatar video-avatar"
              controls="controls"
              id="videopause"
            >您的浏览器不支持视频播放</video>
            <i
              v-else-if="preVideoUrl =='' && !videoFlag"
              class="el-icon-plus avatar-uploader-icon"
              style=" color: #8c939d;
                      width: 30px;
                      line-height: 30px;
                      text-align: center;
                      height: 30px;
                      border: 1px dashed;"
            ></i>
            <el-progress   //进度条的
              v-if="videoFlag"
              type="circle"
              :percentage="videoUploadPercent"
              style="margin-top: 10px"
            ></el-progress>
          </el-upload>
  • 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
//详细方法的用法
  handleVideoSuccess(res, file) {
      if (this.shardUpload != null) {  //重新定义
        this.shardUpload.cancel();
        this.shardUpload == null;
      }
      // 创建批量上传实例
      // this.isShowUploadVideo = true;
      this.videoFlag = true;    
      this.videoUploadPercent = 0;     //进度的百分比一开始为0
      this.continiures = res;
      this.continiufile = res.file;  //上传失败的重新调用这个方法并且传相对应的值
      var file = res.file;
      var self = this;
      self.shardUpload = new ShardUpload(file, {
        url: process.env.VUE_APP_UPLOAD_PART_URL,
      });
      // 开始上传
      self.shardUpload
        .start()
        .then((res) => {
          this.videoFlag = false;
          var videoTimes = 0;
          this.$message.success("上传成功");
          console.log(res);
          this.Continue = false;
          if (res.data.videoInfo != "" && res.data.videoInfo != null) {
            videoTimes = res.data.videoInfo.duration;
          }
          this.preVideoUrl = res.data.fdfsHost + res.data.resourceUrl;   //回显视频
          const videoPath = res.data.resourceUrl;
          //const videoPath = res.data.fdfsHost + res.data.resourceUrl;
          const fileName = res.data.fileName;
          const fileType = res.data.format;

          var videoItem = {  //视频上传的相关东西传给后端要用的 
            fileType: fileType,
            fileName: fileName,
            videoTimes: videoTimes,
            fileUrl: videoPath,
          };
          this.addtableData.thematicFileRelationList = [];  //因为只要一个视频路径 所以先清空数组 再求赋值 传给后端
          this.addtableData.thematicFileRelationList.unshift(videoItem);
          //       //取最新的上传视频路径
          // this.addtableData.thematicFileRelationList = this.addtableData.thematicFileRelationList[this.addtableData.thematicFileRelationList.length -1]
        })
        .catch((error) => {
          this.statusCode = error.status;
          this.Continue = true;  //失败后出现继续上传的按钮 重新点击调用这个方法
          // 失败
          // this.onFaild(error);
          // throw error;
        });
      // 上传进度事件
      self.shardUpload.$on("block-success", function (index, total) {
        console.log("--------------------------------------");
        console.log("第几块上传成功:" + index);
        console.log("总块数:" + total);
        console.log("进度:" + ((index / total) * 100 + "%"));
        self.videoUploadPercent = +((index / total) * 100).toFixed(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

以上就是所有内容了 可能有的不完整的还需要你们发现出来 这个是在使用增加某些内容的时候使用的 比如是表单的提交 里面有视频 可以用这个 有的可能没有放上去 可以随时发问 我会第一时间告知的

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/267815
推荐阅读
相关标签
  

闽ICP备14008679号