webpack

1 webpack流程

1.1 合并参数

1.2 创建 compiler

1.2.1 注册plugin

if(Array.isArray(options.plugins)){
  for(const plugin of options.plugins){
    if(typeof plugin === 'function'){
      // 可以为纯函数
      plugin.call(compiler, compiler)
    }else{
      // 也可以是 new 出来的对象
      plugin.apply(compiler)
    }
  }
}

1.2.2 触发事件

compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();

1.2.3 加载内部plugin

new WebpackOptionsApply().process(options, compiler);

1.3 compiler.run(callback)

// beforeRun --> run --> done
const run = () => {
  this.hooks.beforeRun.callAsync(this, err => {
    if (err) return finalCallback(err);

    this.hooks.run.callAsync(this, err => {
      if (err) return finalCallback(err);

      this.readRecords(err => {
        if (err) return finalCallback(err);
        // 正式编译
        this.compile(onCompiled);
      });
    });
  });
};

1.3.1 compiler.compile(callback)

// beforeCompile --> compile --> make --> afterCompile
compile(callback){
  const params = this.newCompilationParams()
  this.hooks.beforeCompile.callAsync(params, err => {
    this.hooks.compile.call(params)
    // 创建 compilation;记录本次编译作业的环境信息 
    const compilation = new Compilation(this)
    // 调用之前注册的 make 钩子,添加 entry 等
    this.hooks.make.callAsync(compilation, err => {
      compilation.finish(err => {
        compilation.seal(err => {
          this.hooks.afterCompile.callAsync(compilation, err => {
            return callback(null, compilation)
          })
        })
      })
    })
  })
}

1.3.2 module.doBuild(options, compilation, resolver, fs, callback)

doBuild(options, compilation, resolver, fs, callback) {
	// 创建 loader 调用的上下文
	const loaderContext = this.createLoaderContext(
		resolver,
		options,
		compilation,
		fs
	);
	runLoaders()

1.3.3 runLoaders()

2 编写loader

获取compiler参数:loader-utils

// loaderContext ==== webpack中的this
function getOptions(loaderContext) {
  const query = loaderContext.query;

  if (typeof query === 'string' && query !== '') {
    return parseQuery(loaderContext.query);
  }

  if (!query || typeof query !== 'object') {
    // Not object-like queries are not supported.
    return null;
  }

  return query;
}

3 loader

3.1 类型

3.1.1 normalLoader

3.1.2 pitchLoader 阻断常规流程

function aLoader(resource){}
aLoader.pitch = function(){}

pitchLoader的阻断:
在这里插入图片描述

3.2 loadLoader

3.2.1 createLoaderObject
function createLoaderObject(loader) {
	var obj = {
		path: null,
		query: null,
		fragment: null,
		options: null,
		ident: null,
		normal: null,
		pitch: null,
		raw: null,
		data: null,
		pitchExecuted: false,
		normalExecuted: false
	};
	Object.defineProperty(obj, "request", {
		enumerable: true,
		get: function() {
			return obj.path + obj.query;
		},
		set: function(value) {
			if(typeof value === "string") {
				var splittedRequest = parsePathQueryFragment(value);
				obj.path = splittedRequest.path;
				obj.query = splittedRequest.query;
				obj.fragment = splittedRequest.fragment;
				obj.options = undefined;
				obj.ident = undefined;
			} else {
				if(!value.loader)
					throw new Error("request should be a string or object with loader and options (" + JSON.stringify(value) + ")");
				obj.path = value.loader;
				obj.fragment = value.fragment || "";
				obj.type = value.type;
				obj.options = value.options;
				obj.ident = value.ident;
				if(obj.options === null)
					obj.query = "";
				else if(obj.options === undefined)
					obj.query = "";
				else if(typeof obj.options === "string")
					obj.query = "?" + obj.options;
				else if(obj.ident)
					obj.query = "??" + obj.ident;
				else if(typeof obj.options === "object" && obj.options.ident)
					obj.query = "??" + obj.options.ident;
				else
					obj.query = "?" + JSON.stringify(obj.options);
			}
		}
	});
	obj.request = loader;
	if(Object.preventExtensions) {
		Object.preventExtensions(obj);
	}
	return obj;
}
3.2.2 iteratePitchingLoaders
function iteratePitchingLoaders(options, loaderContext, callback) {
	// abort after last loader
	if(loaderContext.loaderIndex >= loaderContext.loaders.length)
		return processResource(options, loaderContext, callback);

	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

	// iterate
	if(currentLoaderObject.pitchExecuted) {
		loaderContext.loaderIndex++;
		return iteratePitchingLoaders(options, loaderContext, callback);
	}

	// load loader module
	loadLoader(currentLoaderObject, function(err) {
		if(err) {
			loaderContext.cacheable(false);
			return callback(err);
		}
		var fn = currentLoaderObject.pitch;
		currentLoaderObject.pitchExecuted = true;
		if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);

		runSyncOrAsync(
			fn,
			loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
			function(err) {
				if(err) return callback(err);
				var args = Array.prototype.slice.call(arguments, 1);
				// Determine whether to continue the pitching process based on
				// argument values (as opposed to argument presence) in order
				// to support synchronous and asynchronous usages.
				var hasArg = args.some(function(value) {
					return value !== undefined;
				});
				if(hasArg) {
					loaderContext.loaderIndex--;
					iterateNormalLoaders(options, loaderContext, args, callback);
				} else {
					iteratePitchingLoaders(options, loaderContext, callback);
				}
			}
		);
	});
}
3.2.3 processResource
function processResource(options, loaderContext, callback) {
	// set loader index to last loader
	loaderContext.loaderIndex = loaderContext.loaders.length - 1;

	var resourcePath = loaderContext.resourcePath;
	if(resourcePath) {
		loaderContext.addDependency(resourcePath);
		options.readResource(resourcePath, function(err, buffer) {
			if(err) return callback(err);
			options.resourceBuffer = buffer;
			iterateNormalLoaders(options, loaderContext, [buffer], callback);
		});
	} else {
		iterateNormalLoaders(options, loaderContext, [null], callback);
	}
}
3.2.4 loadLoader
function loadLoader(loader, callback) {
	if(loader.type === "module") {
		try {
			if(url === undefined) url = require("url");
			var loaderUrl = url.pathToFileURL(loader.path);
			var modulePromise = eval("import(" + JSON.stringify(loaderUrl.toString()) + ")");
			modulePromise.then(function(module) {
				handleResult(loader, module, callback);
			}, callback);
			return;
		} catch(e) {
			callback(e);
		}
	} else {
		try {
			var module = require(loader.path);
		} catch(e) {
			// it is possible for node to choke on a require if the FD descriptor
			// limit has been reached. give it a chance to recover.
			if(e instanceof Error && e.code === "EMFILE") {
				var retry = loadLoader.bind(null, loader, callback);
				if(typeof setImmediate === "function") {
					// node >= 0.9.0
					return setImmediate(retry);
				} else {
					// node < 0.9.0
					return process.nextTick(retry);
				}
			}
			return callback(e);
		}
		return handleResult(loader, module, callback);
	}
};
3.2.5 handleResult
function handleResult(loader, module, callback) {
	if(typeof module !== "function" && typeof module !== "object") {
		return callback(new LoaderLoadingError(
			"Module '" + loader.path + "' is not a loader (export function or es6 module)"
		));
	}
	loader.normal = typeof module === "function" ? module : module.default;
	loader.pitch = module.pitch;
	loader.raw = module.raw;
	if(typeof loader.normal !== "function" && typeof loader.pitch !== "function") {
		return callback(new LoaderLoadingError(
			"Module '" + loader.path + "' is not a loader (must have normal or pitch function)"
		));
	}
	callback();
}
3.2.6 readResource
readResource: (resource, callback) => {
	const scheme = getScheme(resource);
	if (scheme) {
		hooks.readResourceForScheme
			.for(scheme)
			.callAsync(resource, this, (err, result) => {
				if (err) return callback(err);
				if (typeof result !== "string" && !result) {
					return callback(new UnhandledSchemeError(scheme, resource));
				}
				return callback(null, result);
			});
	} else {
		fs.readFile(resource, callback);
	}
}
3.2.7 iterateNormalLoaders
function iterateNormalLoaders(options, loaderContext, args, callback) {
	if(loaderContext.loaderIndex < 0)
		return callback(null, args);

	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

	// iterate
	if(currentLoaderObject.normalExecuted) {
		loaderContext.loaderIndex--;
		return iterateNormalLoaders(options, loaderContext, args, callback);
	}

	var fn = currentLoaderObject.normal;
	currentLoaderObject.normalExecuted = true;
	if(!fn) {
		return iterateNormalLoaders(options, loaderContext, args, callback);
	}

	convertArgs(args, currentLoaderObject.raw);

	runSyncOrAsync(fn, loaderContext, args, function(err) {
		if(err) return callback(err);

		var args = Array.prototype.slice.call(arguments, 1);
		// 将结果的后(三个)参数作为 下一个loader 的三个参数
		iterateNormalLoaders(options, loaderContext, args, callback);
	});
}
3.2.8 convertArgs
function convertArgs(args, raw) {
	// raw === loader.raw
	if(!raw && Buffer.isBuffer(args[0]))
		args[0] = utf8BufferToString(args[0]);
	else if(raw && typeof args[0] === "string")
		args[0] = Buffer.from(args[0], "utf-8");
}
3.2.9 runSyncOrAsync
function runSyncOrAsync(fn, context, args, callback) {
	var isSync = true;
	var isDone = false;
	var isError = false; // internal error
	var reportedError = false;
	// 支持异步返回数据,调用时返回一个函数
	context.async = function async() {
		if(isDone) {
			if(reportedError) return; // ignore
			throw new Error("async(): The callback was already called.");
		}
		// 修改为非同步
		isSync = false;
		return innerCallback;
	};
	var innerCallback = context.callback = function() {
		if(isDone) {
			if(reportedError) return; // ignore
			throw new Error("callback(): The callback was already called.");
		}
		isDone = true;
		isSync = false;
		try {
			callback.apply(null, arguments);
		} catch(e) {
			isError = true;
			throw e;
		}
	};
	try {
		var result = (function LOADER_EXECUTION() {
			return fn.apply(context, args);
		}());
		if(isSync) {
			isDone = true;
			if(result === undefined)
				return callback();
			// 支持 Promise
			if(result && typeof result === "object" && typeof result.then === "function") {
				return result.then(function(r) {
					callback(null, r);
				}, callback);
			}
			return callback(null, result);
		}
	} catch(e) {
		if(isError) throw e;
		if(isDone) {
			// loader is already "done", so we cannot use the callback function
			// for better debugging we print the error on the console
			if(typeof e === "object" && e.stack) console.error(e.stack);
			else console.error(e);
			return;
		}
		isDone = true;
		reportedError = true;
		callback(e);
	}
}
3.2.10 processResult
const processResult = (err, result) => {
			if (err) {
				if (!(err instanceof Error)) {
					err = new NonErrorEmittedError(err);
				}
				const currentLoader = this.getCurrentLoader(loaderContext);
				const error = new ModuleBuildError(err, {
					from:
						currentLoader &&
						compilation.runtimeTemplate.requestShortener.shorten(
							currentLoader.loader
						)
				});
				return callback(error);
			}

			const source = result[0];
			const sourceMap = result.length >= 1 ? result[1] : null;
			const extraInfo = result.length >= 2 ? result[2] : null;

			if (!Buffer.isBuffer(source) && typeof source !== "string") {
				const currentLoader = this.getCurrentLoader(loaderContext, 0);
				const err = new Error(
					`Final loader (${
						currentLoader
							? compilation.runtimeTemplate.requestShortener.shorten(
									currentLoader.loader
							  )
							: "unknown"
					}) didn't return a Buffer or String`
				);
				const error = new ModuleBuildError(err);
				return callback(error);
			}

			this._source = this.createSource(
				options.context,
				this.binary ? asBuffer(source) : asString(source),
				sourceMap,
				compilation.compiler.root
			);
			if (this._sourceSizes !== undefined) this._sourceSizes.clear();
			this._ast =
				typeof extraInfo === "object" &&
				extraInfo !== null &&
				extraInfo.webpackAST !== undefined
					? extraInfo.webpackAST
					: null;
			return callback();
		};

4 plugin

4.1 Tapable

4.1.1 interceptor

4.1.1.1 type.d.ts
interface HookInterceptor<T, R> {
	name?: string;
	// 是否需要把context传入进行更改
	context?: boolean;
	// 每个 监听回调执行前 执行(可以操作context)
	tap?: (tap: Tap) => void;
	// 在所有 监听函数执行器 调用(可以操作context)
	call?: (...args: any[]) => void;
	loop?: (...args: any[]) => void;
	error?: (err: Error) => void;
	result?: (result: R) => void;
	// 每个 监听回调结束后 触发
	done?: () => void;
	// 插入监听回调时处理 options(返回undefined则不改变ooptions)
	register?: (options: any) => any
}
4.1.1.2 intercept
intercept(interceptor) {
	this._resetCompilation();
	this.interceptors.push(Object.assign({}, interceptor));
	if (interceptor.register) {
		// 对之前注册的回调的 option 重新处理
		for (let i = 0; i < this.taps.length; i++) {
			this.taps[i] = interceptor.register(this.taps[i]);
		}
	}
}
4.1.2.1 _tap
_tap(type, options, fn) {
	if (typeof options === "string") {
		options = {
			name: options.trim()
		};
	} else if (typeof options !== "object" || options === null) {
		throw new Error("Invalid tap options");
	}
	if (typeof options.name !== "string" || options.name === "") {
		throw new Error("Missing name for tap");
	}
	if (typeof options.context !== "undefined") {
		deprecateContext();
	}
	options = Object.assign({ type, fn }, options);
	// 通过 interceptor.register 处理
	options = this._runRegisterInterceptors(options);
	this._insert(options);
}
4.1.2.2 插入回调 _insert
_insert(item) {
	// 重置 call 函数(在调用了 call 函数后插入)
	this._resetCompilation();
	let before;
	if (typeof item.before === "string") {
		// 用 Set 去重复
		before = new Set([item.before]);
	} else if (Array.isArray(item.before)) {
		before = new Set(item.before);
	}
	let stage = 0;
	if (typeof item.stage === "number") {
		stage = item.stage;
	}
	let i = this.taps.length;
	while (i > 0) {
		i--;
		const x = this.taps[i];
		// 将 已有的 tap 复制给后一位;如果后面顺序不变,就再覆盖这复制的一位
		this.taps[i + 1] = x;
		const xStage = x.stage || 0;
		if (before) {
			// 如果 before 包含当前 tap 就继续向前(类似插入排序)
			if (before.has(x.name)) {
				before.delete(x.name);
				// 继续往前寻找
				continue;
			}
			// before为空时结束
			if (before.size > 0) {
				continue;
			}
		}
		if (xStage > stage) {
			continue;
		}
		i++;
		break;
	}
	this.taps[i] = item;
}

4.1.2 创建 call 函数 _createCall

4.1.2.1 惰性函数

function addEvent (type, element, fun) {
    if (element.addEventListener) {
        addEvent = function (type, element, fun) {
            element.addEventListener(type, fun, false);
        }
    }    else if(element.attachEvent){
        addEvent = function (type, element, fun) {
            element.attachEvent('on' + type, fun);
        }
    }    else{
        addEvent = function (type, element, fun) {
            element['on' + type] = fun;
        }
    }   
    return addEvent(type, element, fun);
}
_createCall(type) {
	return this.compile({
		taps: this.taps,
		interceptors: this.interceptors,
		args: this._args,
		type: type
	});
}

4.1.3 factory.create

create(options) {
	// 初始化从Hook传入的 options
	this.init(options);
	let fn;
	switch (this.options.type) {
		case "sync":
			fn = new Function(
				this.args(),
				'"use strict";\n' +
					this.header() +
					this.contentWithInterceptors({
						onError: err => `throw ${err};\n`,
						onResult: result => `return ${result};\n`,
						resultReturns: true,
						onDone: () => "",
						// 是否将异常抛出,否则将回调放在 try each 语句中
						rethrowIfPossible: true
					})
			);
			break;
		case "async":
			fn = new Function(
				this.args({
					after: "_callback"
				}),
				'"use strict";\n' +
					this.header() +
					this.contentWithInterceptors({
						onError: err => `_callback(${err});\n`,
						onResult: result => `_callback(null, ${result});\n`,
						onDone: () => "_callback();\n"
					})
			);
			break;
		case "promise":
			let errorHelperUsed = false;
			const content = this.contentWithInterceptors({
				onError: err => {
					errorHelperUsed = true;
					return `_error(${err});\n`;
				},
				onResult: result => `_resolve(${result});\n`,
				onDone: () => "_resolve();\n"
			});
			let code = "";
			code += '"use strict";\n';
			code += "return new Promise((_resolve, _reject) => {\n";
			if (errorHelperUsed) {
				code += "var _sync = true;\n";
				code += "function _error(_err) {\n";
				code += "if(_sync)\n";
				code += "_resolve(Promise.resolve().then(() => { throw _err; }));\n";
				code += "else\n";
				code += "_reject(_err);\n";
				code += "};\n";
			}
			code += this.header();
			code += content;
			if (errorHelperUsed) {
				code += "_sync = false;\n";
			}
			code += "});\n";
			fn = new Function(this.args(), code);
			break;
	}
	// 清除在 init 初始化的属性
	this.deinit();
	return fn;
}

4.1.4 header

header() {
	let code = "";
	// 任意监听的context为true,this.needContext()都为true
	if (this.needContext()) {
		// 最初绑定的context都为空对象,但是可以再intercetor中操作
		code += "var _context = {};\n";
	} else {
		code += "var _context;\n";
	}
	code += "var _x = this._x;\n";
	if (this.options.interceptors.length > 0) {
		// 绑定 taps
		code += "var _taps = this.taps;\n";
		// 绑定 interceptors
		code += "var _interceptors = this.interceptors;\n";
	}
	// 可以在这里操作_context
	for (let i = 0; i < this.options.interceptors.length; i++) {
		const interceptor = this.options.interceptors[i];
		if (interceptor.call) {
			code += `${this.getInterceptor(i)}.call(${this.args({
				before: interceptor.context ? "_context" : undefined
			})});\n`;
		}
	}
	return code;
}

4.1.5 contentWithInterceptors

contentWithInterceptors(options) {
	if (this.options.interceptors.length > 0) {
		const onError = options.onError;
		const onResult = options.onResult;
		const onDone = options.onDone;
		// 调用 intercetor 的其他回调
		return this.content(
			Object.assign(options, {
				onError:
					onError &&
					(err => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.error) {
								code += `${this.getInterceptor(i)}.error(${err});\n`;
							}
						}
						code += onError(err);
						return code;
					}),
				onResult:
					onResult &&
					(result => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.result) {
								code += `${this.getInterceptor(i)}.result(${result});\n`;
							}
						}
						code += onResult(result);
						return code;
					}),
				onDone:
					onDone &&
					(() => {
						let code = "";
						for (let i = 0; i < this.options.interceptors.length; i++) {
							const interceptor = this.options.interceptors[i];
							if (interceptor.done) {
								code += `${this.getInterceptor(i)}.done();\n`;
							}
						}
						code += onDone();
						return code;
					})
			})
		);
	} else {
		return this.content(options);
	}
}

4.1.5 callTapsSeries

callTapsSeries({
	onError,
	onResult,
	resultReturns,
	onDone,
	doneReturns,
	rethrowIfPossible
}) {
	if (this.options.taps.length === 0) return onDone();
	const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
	const somethingReturns = resultReturns || doneReturns;
	let code = "";
	let current = onDone;
	let unrollCounter = 0;
	// 从最后一个loader开始创建函数字符串
	for (let j = this.options.taps.length - 1; j >= 0; j--) {
		const i = j;
		const unroll =
			current !== onDone &&
			(this.options.taps[i].type !== "sync" || unrollCounter++ > 20);
		if (unroll) {
			unrollCounter = 0;
			code += `function _next${i}() {\n`;
			code += current();
			code += `}\n`;
			current = () => `${somethingReturns ? "return " : ""}_next${i}();\n`;
		}
		const done = current;
		const doneBreak = skipDone => {
			if (skipDone) return "";
			return onDone();
		};
		const content = this.callTap(i, {
			onError: error => onError(i, error, done, doneBreak),
			onResult:
				onResult &&
				(result => {
					return onResult(i, result, done, doneBreak);
				}),
			onDone: !onResult && done,
			rethrowIfPossible:
				rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
		});
		current = () => content;
	}
	code += current();
	return code;
}

4.1.5 callTap

callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
	let code = "";
	let hasTapCached = false;
	for (let i = 0; i < this.options.interceptors.length; i++) {
		const interceptor = this.options.interceptors[i];
		if (interceptor.tap) {
			if (!hasTapCached) {
				code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
				hasTapCached = true;
			}
			code += `${this.getInterceptor(i)}.tap(${
				interceptor.context ? "_context, " : ""
			}_tap${tapIndex});\n`;
		}
	}
	code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
	const tap = this.options.taps[tapIndex];
	switch (tap.type) {
		case "sync":
			// 是否需要抛出异常
			if (!rethrowIfPossible) {
				code += `var _hasError${tapIndex} = false;\n`;
				code += "try {\n";
			}
			// 是否需要保留结果
			if (onResult) {
				code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
					before: tap.context ? "_context" : undefined
				})});\n`;
			} else {
				code += `_fn${tapIndex}(${this.args({
					before: tap.context ? "_context" : undefined
				})});\n`;
			}
			// 闭合 try
			if (!rethrowIfPossible) {
				code += "} catch(_err) {\n";
				code += `_hasError${tapIndex} = true;\n`;
				code += onError("_err");
				code += "}\n";
				code += `if(!_hasError${tapIndex}) {\n`;
			}
			if (onResult) {
				code += onResult(`_result${tapIndex}`);
			}
			if (onDone) {
				code += onDone();
			}
			if (!rethrowIfPossible) {
				code += "}\n";
			}
			break;
		case "async":
			let cbCode = "";
			if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
			else cbCode += `_err${tapIndex} => {\n`;
			cbCode += `if(_err${tapIndex}) {\n`;
			cbCode += onError(`_err${tapIndex}`);
			cbCode += "} else {\n";
			if (onResult) {
				cbCode += onResult(`_result${tapIndex}`);
			}
			if (onDone) {
				cbCode += onDone();
			}
			cbCode += "}\n";
			cbCode += "}";
			code += `_fn${tapIndex}(${this.args({
				before: tap.context ? "_context" : undefined,
				after: cbCode
			})});\n`;
			break;
		case "promise":
			code += `var _hasResult${tapIndex} = false;\n`;
			code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
				before: tap.context ? "_context" : undefined
			})});\n`;
			code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
			code += `  throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;
			code += `_promise${tapIndex}.then(_result${tapIndex} => {\n`;
			code += `_hasResult${tapIndex} = true;\n`;
			if (onResult) {
				code += onResult(`_result${tapIndex}`);
			}
			if (onDone) {
				code += onDone();
			}
			code += `}, _err${tapIndex} => {\n`;
			code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
			code += onError(`_err${tapIndex}`);
			code += "});\n";
			break;
	}
	return code;
}

4.1.6 SyncHookCodeFactory

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		// 忽略了 intercetor 里的result
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值