diff --git a/FakeServer/ajax/DEMO.js b/FakeServer/ajax/DEMO.js index 82a20e9..df2af7e 100644 --- a/FakeServer/ajax/DEMO.js +++ b/FakeServer/ajax/DEMO.js @@ -1,7 +1,12 @@ import { mockFetch } from './fetch/fetch'; mockFetch({ - request() {}, - response() {}, + // request 阶段进行的拦截验证,并向 FakeServer 进行请求 + async proxy(url, options) { + const localCallback = await fakeServer.getServerResult(url, options); + if (localCallback) { + return localCallback; + } + }, silent: false, }); diff --git a/FakeServer/ajax/XMLHttpRequest/defineGetterAndSetter.js b/FakeServer/ajax/XMLHttpRequest/defineGetterAndSetter.js index 659bc55..2d3e2cb 100644 --- a/FakeServer/ajax/XMLHttpRequest/defineGetterAndSetter.js +++ b/FakeServer/ajax/XMLHttpRequest/defineGetterAndSetter.js @@ -6,18 +6,22 @@ export function defineGetAndSet(XHR) { // 将这些 键值对 映射到 $data 属性对象的对应值上去 const array = ['readyState', 'status', 'response', 'responseText', 'statusText']; - Object.defineProperties( - XHR, - array.reduce((col, cur) => { - col[cur] = { - get() { - return this.$data[cur]; - }, - set(state) { - this.$data[cur] = state; - }, - }; - return col; - }, {}), - ); + const auto = array.reduce((col, cur) => { + col[cur] = { + get() { + return this.$data[cur]; + }, + set(state) { + this.$data[cur] = state; + }, + }; + return col; + }, {}); + Object.defineProperties(XHR, Object.assign(auto)); + XHR.getResponseHeader = function (name) { + return this._responseHeaders[name]; + }; + XHR.getAllResponseHeaders = function () { + return this._responseHeaders; + }; } diff --git a/FakeServer/ajax/XMLHttpRequest/xhr.js b/FakeServer/ajax/XMLHttpRequest/xhr.js index f01c6a6..b5326f1 100644 --- a/FakeServer/ajax/XMLHttpRequest/xhr.js +++ b/FakeServer/ajax/XMLHttpRequest/xhr.js @@ -3,56 +3,48 @@ import { pick } from 'lodash-es'; import { defineGetAndSet } from './defineGetterAndSetter.js'; import HTTP_STATUS_CODES from './constant.js'; -let XMLHttpRequest; +let XHR; const config = { request: null, response: null, silent: false, }; -function makeResponse() {} - // ! 虽然 XMLHttpRequest 不能够修改,但是可以通过设置 getter 和 setter 将属性映射到 $属性上,这样的话,原生 XHR 会将数据写入和读取的位置更改为新的对象属性上,这样就可以被我们修改了。 -class MockXMLHttpRequest extends XMLHttpRequest { +class MockXMLHttpRequest extends window.XMLHttpRequest { constructor(...args) { super(...args); } + _responseHeaders = {}; $mock = true; // 标识是否打开拦截 open(method, url, _, username, password) { // 不进行同步操作 - XMLHttpRequest.prototype.open.call(this, method, url, true, username, password); + XHR.prototype.open.call(this, method, url, true, username, password); this.$data.url = url; this.$data.method = method.toLowerCase(); } send(body) { if (this.$mock) { - const options = pick(this.$data, ['headers']); - const result = config.request(this.$data.url, options); - if (result) { + const options = pick(this.$data, ['headers', 'method']); + // ! 这里的 proxy 中的参数固定为 fetch 中的标准。 + const result = config.proxy(this.$data.url, options); + + if (result.body) { defineGetAndSet(this); this.dispatchEvent(new Event('loadstart')); - setTimeout(this.$done.bind(this), this.timeout || 100); + setTimeout(() => this.$done.bind(this)(result), this.timeout || 100); return null; } // 这里穿透下去 } - XMLHttpRequest.prototype.send.call(this, body); + XHR.prototype.send.call(this, body); } setRequestHeader(key, value) { this.$data.headers[key] = value; - return XMLHttpRequest.prototype.setRequestHeader.call(this, key, value); - } - get $mock() { - return this.$mock; - } - set $mock(value) { - if (typeof value === 'boolean') { - this.$mock = value; - return true; - } - return false; + return XHR.prototype.setRequestHeader.call(this, key, value); } + $data = { // 原生属性的 getter 和 setter readyState: 0, @@ -65,18 +57,16 @@ class MockXMLHttpRequest extends XMLHttpRequest { method: 'get', }; - $done() { + $done({ body, headers, status }) { // 伪造 XHR 返回事件 this.readyState = this.HEADERS_RECEIVED; this.dispatchEvent(new Event('readystatechange')); this.readyState = this.LOADING; this.dispatchEvent(new Event('readystatechange')); - - this.status = 200; - this.statusText = HTTP_STATUS_CODES[200]; - // ! 传入创建函数,给与使用者开放权限 - const response = config.response(makeResponse); - this.response = response; + this._responseHeaders = headers; + this.status = status; + this.statusText = HTTP_STATUS_CODES[status]; + this.response = body; this.responseText = typeof this.response === 'string' ? this.response : JSON.stringify(this.response); this.readyState = this.DONE; this.dispatchEvent(new Event('readystatechange')); @@ -84,15 +74,17 @@ class MockXMLHttpRequest extends XMLHttpRequest { this.dispatchEvent(new Event('loadend')); } } -export function mockXHR({ request: req, response: res, silent = false }) { - if (req instanceof Function) config.request = req; - if (res instanceof Function) config.response = res; +export function mockXHR({ proxy, silent = false }) { + if (proxy instanceof Function) config.request = proxy; + config.silent = silent; - XMLHttpRequest = window.XMLHttpRequest; + + // 这个文件中的 XHR + // 代理 fetch 的初始化函数 if (window.XMLHttpRequest && !window.XMLHttpRequest.$mock) { - window.XMLHttpRequest = MockXMLHttpRequest; + [XHR, window.XMLHttpRequest] = [window.XMLHttpRequest, MockXMLHttpRequest]; window.XMLHttpRequest.$mock = true; - if (!silent) console.warn('fetch 已经被代理'); + if (!silent) console.warn('XHR 已经被代理'); } } diff --git a/FakeServer/ajax/fetch/fetch.js b/FakeServer/ajax/fetch/fetch.js index 4cb7d73..95f9066 100644 --- a/FakeServer/ajax/fetch/fetch.js +++ b/FakeServer/ajax/fetch/fetch.js @@ -1,31 +1,28 @@ -const config = { request: null, response: null, silent: false }; +const config = { proxy: null, silent: false }; let realFetch; // 假的 Response 对象 import fakeResponse from './src/response.js'; -function makeResponse(data, options) { - return new fakeResponse(data, options); -} async function fakeFetch(url, options = {}) { // 只有在 $mock 标记为 true 时才进行代理 if (window.fetch.$mock === true) { // ! 传入初始参数 - const result = config.request(url, options); + const result = await config.proxy(url, options); if (result) { - // ! 传入创建函数,给与使用者开放权限 - const response = config.response(makeResponse); if (!silent) console.warn('fetch: mock代理中'); - return response; + const { body, options = {} } = result; + return new fakeResponse(body, options); } } - if (!silent) console.warn('这次 fetch 未使用 mockjs'); + if (!config.silent) console.warn('这次 fetch 未使用 mockjs'); return realFetch(url, options); } -function mockFetch({ request: req, response: res, silent = false }) { - if (req instanceof Function) config.request = req; - if (res instanceof Function) config.response = res; + +// 代理出口 +export function mockFetch({ proxy, silent = false }) { + if (proxy instanceof Function) config.proxy = proxy; config.silent = silent; // 代理 fetch 的初始化函数 if (window.fetch && !window.fetch.$mock) { @@ -35,4 +32,3 @@ function mockFetch({ request: req, response: res, silent = false }) { if (!silent) console.warn('fetch: 已经被代理'); } } -export { fakeFetch, mockFetch }; diff --git a/FakeServer/dist/index.js b/FakeServer/dist/index.js index bf3d498..4ee57b6 100644 --- a/FakeServer/dist/index.js +++ b/FakeServer/dist/index.js @@ -367,73 +367,2277 @@ function parseURL(url, strictMode = false) { return parseUri(url, strictMode); } -function createRegexp(path, config = {}) { - return pathToRegexp(path, [], config); -} class Request { - constructor(path) { - Object.assign(this, parseURL(path)); + constructor(path, options) { + Object.assign(this, parseURL(path), options); } } -class Response { - constructor(path, options = {}) {} - send() {} +class Response$2 { + constructor() {} + headers = { + 'Access-Control-Allow-Origin': '*', + Age: 1200, // 单位为秒 + Allow: 'GET, POST', + 'Content-Encoding': 'gzip', + 'Content-Type': 'text/html; charset=utf-8', + }; + status = 200; + body = null; + send(body) { + this.body = body; + } + setHeaders(headers) { + Object.entries(headers).forEach(([key, value]) => { + this.headers[key] = value; + }); + } } // 每一个 Route 只有一个 main 函数进行数据的返回 class Route { - #callback; - constructor(pathRegexp, cb) { - this.#callback = cb; + constructor({ matcher, name, path, callback, redirect = '' }) { + Object.assign(this, { + matcher, + name, + path, + callback, + redirect, + }); } - query(req, res) { - const response = this.#callback(req, res); - if (response instanceof Response) return response; - return res; + async request(req, res) { + const response = await this.callback(req, res); + if (response instanceof Response && response.data) return response; + return null; } } -class Router { - constructor(Routers) { - this.#initRouterMap(Routers); + +class RouteMap extends Map { + constructor(...args) { + super(...args); } - #RouterMap = new Map(); #RouteMatchers = []; - #initRouterMap(Routers) { - Object.entries(Routers).forEach(([key, value]) => { - if (value instanceof Function) this.addRoute(key, value); - }); - console.log(this.#RouterMap); + addRoute(Route) { + this.#RouteMatchers.push(Route.matcher); + return this.set(Route.matcher, Route); } - #findRoute(path) { + matchRoute(path) { let target; this.#RouteMatchers.some((reg) => { let result = path.match(reg); if (result) { - target = this.#RouterMap.get(reg); + target = this.get(reg); + return true; } return false; }); + if (target?.redirect) return this.matchRoute(target.redirect); return target; } - addRoute(path, mainCallback) { - const route = new Route(path, mainCallback); - const pathRegexp = createRegexp(path); - this.#RouterMap.set(pathRegexp, route); - this.#RouteMatchers.push(pathRegexp); - return route; +} + +/** + * Check if `obj` is a URLSearchParams object + * ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143 + * + * @param {*} obj + * @return {boolean} + */ +const isURLSearchParameters = (object) => { + return ( + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + typeof object.sort === 'function' && + object[Symbol.toStringTag] === 'URLSearchParams' // 获取类名的方式哦 + ); +}; + +const BODY = Symbol('Body internals'); +const RESPONSE = Symbol('Response internals'); + +/** + * Consume and convert an entire Body to a Buffer. + * + * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body + * + * @return Promise + */ +async function consumeBody(data) { + // 标记为已经使用 + if (data.disturbed) { + throw new TypeError(`body used already for: ${data.url}`); + } + data.disturbed = true; + + // 报错 + if (data.error) { + throw data.error; + } + + if (data.body instanceof FormData) { + return new Blob([Object.fromEntries(data.body.entries())]); + } + return data.body; +} + +/** + * Body.js + * + * Body interface provides common methods for Request and Response + */ +/** + * Body mixin + * + * Ref: https://fetch.spec.whatwg.org/#body + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +class Body { + constructor(body, { size = 0 } = {}) { + if (body === null) { + // Body is undefined or null + body = new Blob([]); + } else if (isURLSearchParameters(body)) { + // Body is a URLSearchParams + body = new Blob([body]); + } else if (ArrayBuffer.isView(body)) { + // Body is ArrayBufferView + body = new Blob([body]); + } else { + body = new Blob([JSON.stringify(body)]); + } + + this[BODY] = { + body, + boundary: null, + disturbed: false, + error: null, + }; + this.size = size; + } + + get body() { + return this[BODY].body; + } + + get bodyUsed() { + return this[BODY].disturbed; + } + + /** + * Decode response as ArrayBuffer + * + * @return Promise + */ + async arrayBuffer() { + const blob = await consumeBody(this[BODY]); + return blob.arrayBuffer(); + } + + /** + * Return raw response as Blob + * + * @return Promise + */ + async blob() { + return consumeBody(this[BODY]); + } + + /** + * Decode response as json + * + * @return Promise + */ + async json() { + const text = await this.text(); + return JSON.parse(text || '{}'); + } + + /** + * Decode response as text + * + * @return Promise + */ + async text() { + const blob = await consumeBody(this[BODY]); + return blob.text(); + } + + /** + * Decode response as buffer (non-spec api) + * + * @return Promise + */ +} + +// In browsers, all properties are enumerable. +Object.defineProperties(Body.prototype, { + body: { enumerable: true }, + bodyUsed: { enumerable: true }, + arrayBuffer: { enumerable: true }, + blob: { enumerable: true }, + json: { enumerable: true }, + text: { enumerable: true }, +}); + +/** + * Performs the operation "extract a `Content-Type` value from |object|" as + * specified in the specification: + * https://fetch.spec.whatwg.org/#concept-bodyinit-extract + * + * This function assumes that instance.body is present. + * + * @param {any} body Any options.body input + * @return {string | null} + */ +const extractContentType = (body, request) => { + // Body is null or undefined + if (body === null) { + return null; + } + + // Body is string + if (typeof body === 'string') { + return 'text/plain;charset=UTF-8'; + } + + // Body is a URLSearchParams + if (isURLSearchParameters(body)) { + return 'application/x-www-form-urlencoded;charset=UTF-8'; + } + + // Body is blob + if (body instanceof Blob) { + return body.type || null; + } + + if (body instanceof FormData) { + return `multipart/form-data; boundary=${request[BODY].boundary}`; + } + + // Body constructor defaults other things to string + return 'text/plain;charset=UTF-8'; +}; + +const redirectStatus = new Set([301, 302, 303, 307, 308]); + +/** + * Redirect code matching + * + * @param {number} code - Status code + * @return {boolean} + */ +const isRedirect = (code) => { + return redirectStatus.has(code); +}; + +/** + * Response.js + * + * Response class provides content decoding + */ +const Response$1 = (globalThis.window && globalThis.window.Response) || class Null {}; +/** + * Response class + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +class fakeResponse extends Body { + constructor(body = null, options = {}) { + super(body, options); + const status = options.status != null ? options.status : 200; + const headers = new Headers(options.headers); + if (body !== null && !headers.has('Content-Type')) { + const contentType = extractContentType(body); + if (contentType) { + headers.append('Content-Type', contentType); + } + } + this[RESPONSE] = { + type: 'default', + url: options.url, + status, + statusText: options.statusText || '', + headers, + counter: options.counter, + }; + } + + get type() { + return this[RESPONSE].type; + } + + get url() { + return this[RESPONSE].url || ''; + } + + get status() { + return this[RESPONSE].status; + } + + /** + * Convenience property representing if the request ended normally + */ + get ok() { + return this[RESPONSE].status >= 200 && this[RESPONSE].status < 300; + } + + get redirected() { + return this[RESPONSE].counter > 0; + } + + get statusText() { + return this[RESPONSE].statusText; + } + + get headers() { + return this[RESPONSE].headers; + } + + /** + * Clone this response + * + * @return Response + */ + clone() { + // Don't allow cloning a used body + if (this.bodyUsed) { + throw new Error('cannot clone body after it is used'); + } + return new Response$1(this.body, { + type: this.type, + url: this.url, + status: this.status, + statusText: this.statusText, + headers: this.headers, + ok: this.ok, + redirected: this.redirected, + size: this.size, + }); + } + + /** + * @param {string} url The URL that the new response is to originate from. + * @param {number} status An optional status code for the response (e.g., 302.) + * @return {Response} A Response object. + */ + static redirect(url, status = 302) { + if (!isRedirect(status)) { + throw new RangeError('Failed to execute "redirect" on "response": Invalid status code'); + } + + return new Response$1(null, { + headers: { + location: new URL(url).toString(), + }, + status, + }); + } + + static error() { + const response = new Response$1(null, { status: 0, statusText: '' }); + response[RESPONSE].type = 'error'; + return response; + } + + get [Symbol.toStringTag]() { + return 'Response'; + } +} + +Object.defineProperties(Response$1.prototype, { + type: { enumerable: true }, + url: { enumerable: true }, + status: { enumerable: true }, + ok: { enumerable: true }, + redirected: { enumerable: true }, + statusText: { enumerable: true }, + headers: { enumerable: true }, + clone: { enumerable: true }, +}); + +const config$1 = { proxy: null, silent: false }; +let realFetch; + +async function fakeFetch(url, options = {}) { + // 只有在 $mock 标记为 true 时才进行代理 + if (window.fetch.$mock === true) { + // ! 传入初始参数 + const result = await config$1.proxy(url, options); + if (result) { + if (!silent) console.warn('fetch: mock代理中'); + const { body, options = {} } = result; + return new fakeResponse(body, options); + } + } + + if (!config$1.silent) console.warn('这次 fetch 未使用 mockjs'); + return realFetch(url, options); +} + +// 代理出口 +function mockFetch({ proxy, silent = false }) { + if (proxy instanceof Function) config$1.proxy = proxy; + config$1.silent = silent; + // 代理 fetch 的初始化函数 + if (window.fetch && !window.fetch.$mock) { + // 交叉赋值 + [realFetch, window.fetch] = [window.fetch, fakeFetch]; + window.fetch.$mock = true; + if (!silent) console.warn('fetch: 已经被代理'); + } +} + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +/** Built-in value references. */ +var Symbol$1 = root.Symbol; + +/** Used for built-in method references. */ +var objectProto$6 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$5 = objectProto$6.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString$1 = objectProto$6.toString; + +/** Built-in value references. */ +var symToStringTag$1 = Symbol$1 ? Symbol$1.toStringTag : undefined; + +/** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ +function getRawTag(value) { + var isOwn = hasOwnProperty$5.call(value, symToStringTag$1), + tag = value[symToStringTag$1]; + + try { + value[symToStringTag$1] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString$1.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag$1] = tag; + } else { + delete value[symToStringTag$1]; + } + } + return result; +} + +/** Used for built-in method references. */ +var objectProto$5 = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto$5.toString; + +/** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ +function objectToString(value) { + return nativeObjectToString.call(value); +} + +/** `Object#toString` result references. */ +var nullTag = '[object Null]', + undefinedTag = '[object Undefined]'; + +/** Built-in value references. */ +var symToStringTag = Symbol$1 ? Symbol$1.toStringTag : undefined; + +/** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag && symToStringTag in Object(value)) + ? getRawTag(value) + : objectToString(value); +} + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return value != null && typeof value == 'object'; +} + +/** `Object#toString` result references. */ +var symbolTag = '[object Symbol]'; + +/** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ +function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && baseGetTag(value) == symbolTag); +} + +/** + * A specialized version of `_.map` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ +function arrayMap(array, iteratee) { + var index = -1, + length = array == null ? 0 : array.length, + result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; +} + +/** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ +var isArray = Array.isArray; + +/** Used as references for various `Number` constants. */ +var INFINITY$1 = 1 / 0; + +/** Used to convert symbols to primitives and strings. */ +var symbolProto = Symbol$1 ? Symbol$1.prototype : undefined, + symbolToString = symbolProto ? symbolProto.toString : undefined; + +/** + * The base implementation of `_.toString` which doesn't convert nullish + * values to empty strings. + * + * @private + * @param {*} value The value to process. + * @returns {string} Returns the string. + */ +function baseToString(value) { + // Exit early for strings to avoid a performance hit in some environments. + if (typeof value == 'string') { + return value; + } + if (isArray(value)) { + // Recursively convert values (susceptible to call stack limits). + return arrayMap(value, baseToString) + ''; + } + if (isSymbol(value)) { + return symbolToString ? symbolToString.call(value) : ''; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY$1) ? '-0' : result; +} + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); +} + +/** + * This method returns the first argument it receives. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Util + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'a': 1 }; + * + * console.log(_.identity(object) === object); + * // => true + */ +function identity(value) { + return value; +} + +/** `Object#toString` result references. */ +var asyncTag = '[object AsyncFunction]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + proxyTag = '[object Proxy]'; + +/** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ +function isFunction(value) { + if (!isObject(value)) { + return false; + } + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 9 which returns 'object' for typed arrays and other constructors. + var tag = baseGetTag(value); + return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; +} + +/** Used to detect overreaching core-js shims. */ +var coreJsData = root['__core-js_shared__']; + +/** Used to detect methods masquerading as native. */ +var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; +}()); + +/** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ +function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); +} + +/** Used for built-in method references. */ +var funcProto$1 = Function.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString$1 = funcProto$1.toString; + +/** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to convert. + * @returns {string} Returns the source code. + */ +function toSource(func) { + if (func != null) { + try { + return funcToString$1.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; +} + +/** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ +var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + +/** Used to detect host constructors (Safari). */ +var reIsHostCtor = /^\[object .+?Constructor\]$/; + +/** Used for built-in method references. */ +var funcProto = Function.prototype, + objectProto$4 = Object.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty$4 = objectProto$4.hasOwnProperty; + +/** Used to detect if a method is native. */ +var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty$4).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); + +/** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ +function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = isFunction(value) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); +} + +/** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ +function getValue(object, key) { + return object == null ? undefined : object[key]; +} + +/** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ +function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; +} + +/** + * A faster alternative to `Function#apply`, this function invokes `func` + * with the `this` binding of `thisArg` and the arguments of `args`. + * + * @private + * @param {Function} func The function to invoke. + * @param {*} thisArg The `this` binding of `func`. + * @param {Array} args The arguments to invoke `func` with. + * @returns {*} Returns the result of `func`. + */ +function apply(func, thisArg, args) { + switch (args.length) { + case 0: return func.call(thisArg); + case 1: return func.call(thisArg, args[0]); + case 2: return func.call(thisArg, args[0], args[1]); + case 3: return func.call(thisArg, args[0], args[1], args[2]); + } + return func.apply(thisArg, args); +} + +/** Used to detect hot functions by number of calls within a span of milliseconds. */ +var HOT_COUNT = 800, + HOT_SPAN = 16; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeNow = Date.now; + +/** + * Creates a function that'll short out and invoke `identity` instead + * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN` + * milliseconds. + * + * @private + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new shortable function. + */ +function shortOut(func) { + var count = 0, + lastCalled = 0; + + return function() { + var stamp = nativeNow(), + remaining = HOT_SPAN - (stamp - lastCalled); + + lastCalled = stamp; + if (remaining > 0) { + if (++count >= HOT_COUNT) { + return arguments[0]; + } + } else { + count = 0; + } + return func.apply(undefined, arguments); + }; +} + +/** + * Creates a function that returns `value`. + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Util + * @param {*} value The value to return from the new function. + * @returns {Function} Returns the new constant function. + * @example + * + * var objects = _.times(2, _.constant({ 'a': 1 })); + * + * console.log(objects); + * // => [{ 'a': 1 }, { 'a': 1 }] + * + * console.log(objects[0] === objects[1]); + * // => true + */ +function constant(value) { + return function() { + return value; + }; +} + +var defineProperty = (function() { + try { + var func = getNative(Object, 'defineProperty'); + func({}, '', {}); + return func; + } catch (e) {} +}()); + +/** + * The base implementation of `setToString` without support for hot loop shorting. + * + * @private + * @param {Function} func The function to modify. + * @param {Function} string The `toString` result. + * @returns {Function} Returns `func`. + */ +var baseSetToString = !defineProperty ? identity : function(func, string) { + return defineProperty(func, 'toString', { + 'configurable': true, + 'enumerable': false, + 'value': constant(string), + 'writable': true + }); +}; + +/** + * Sets the `toString` method of `func` to return `string`. + * + * @private + * @param {Function} func The function to modify. + * @param {Function} string The `toString` result. + * @returns {Function} Returns `func`. + */ +var setToString = shortOut(baseSetToString); + +/** Used as references for various `Number` constants. */ +var MAX_SAFE_INTEGER$1 = 9007199254740991; + +/** Used to detect unsigned integer values. */ +var reIsUint = /^(?:0|[1-9]\d*)$/; + +/** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ +function isIndex(value, length) { + var type = typeof value; + length = length == null ? MAX_SAFE_INTEGER$1 : length; + + return !!length && + (type == 'number' || + (type != 'symbol' && reIsUint.test(value))) && + (value > -1 && value % 1 == 0 && value < length); +} + +/** + * The base implementation of `assignValue` and `assignMergeValue` without + * value checks. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ +function baseAssignValue(object, key, value) { + if (key == '__proto__' && defineProperty) { + defineProperty(object, key, { + 'configurable': true, + 'enumerable': true, + 'value': value, + 'writable': true + }); + } else { + object[key] = value; + } +} + +/** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ +function eq(value, other) { + return value === other || (value !== value && other !== other); +} + +/** Used for built-in method references. */ +var objectProto$3 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$3 = objectProto$3.hasOwnProperty; + +/** + * Assigns `value` to `key` of `object` if the existing value is not equivalent + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ +function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty$3.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + baseAssignValue(object, key, value); + } +} + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max; + +/** + * A specialized version of `baseRest` which transforms the rest array. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @param {Function} transform The rest array transform. + * @returns {Function} Returns the new function. + */ +function overRest(func, start, transform) { + start = nativeMax(start === undefined ? (func.length - 1) : start, 0); + return function() { + var args = arguments, + index = -1, + length = nativeMax(args.length - start, 0), + array = Array(length); + + while (++index < length) { + array[index] = args[start + index]; + } + index = -1; + var otherArgs = Array(start + 1); + while (++index < start) { + otherArgs[index] = args[index]; + } + otherArgs[start] = transform(array); + return apply(func, this, otherArgs); + }; +} + +/** Used as references for various `Number` constants. */ +var MAX_SAFE_INTEGER = 9007199254740991; + +/** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ +function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; +} + +/** `Object#toString` result references. */ +var argsTag = '[object Arguments]'; + +/** + * The base implementation of `_.isArguments`. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + */ +function baseIsArguments(value) { + return isObjectLike(value) && baseGetTag(value) == argsTag; +} + +/** Used for built-in method references. */ +var objectProto$2 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$2 = objectProto$2.hasOwnProperty; + +/** Built-in value references. */ +var propertyIsEnumerable = objectProto$2.propertyIsEnumerable; + +/** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ +var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) { + return isObjectLike(value) && hasOwnProperty$2.call(value, 'callee') && + !propertyIsEnumerable.call(value, 'callee'); +}; + +/** Used to match property names within property paths. */ +var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, + reIsPlainProp = /^\w*$/; + +/** + * Checks if `value` is a property name and not a property path. + * + * @private + * @param {*} value The value to check. + * @param {Object} [object] The object to query keys on. + * @returns {boolean} Returns `true` if `value` is a property name, else `false`. + */ +function isKey(value, object) { + if (isArray(value)) { + return false; + } + var type = typeof value; + if (type == 'number' || type == 'symbol' || type == 'boolean' || + value == null || isSymbol(value)) { + return true; + } + return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || + (object != null && value in Object(object)); +} + +/* Built-in method references that are verified to be native. */ +var nativeCreate = getNative(Object, 'create'); + +/** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ +function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + this.size = 0; +} + +/** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function hashDelete(key) { + var result = this.has(key) && delete this.__data__[key]; + this.size -= result ? 1 : 0; + return result; +} + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED$1 = '__lodash_hash_undefined__'; + +/** Used for built-in method references. */ +var objectProto$1 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$1 = objectProto$1.hasOwnProperty; + +/** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED$1 ? undefined : result; + } + return hasOwnProperty$1.call(data, key) ? data[key] : undefined; +} + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function hashHas(key) { + var data = this.__data__; + return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key); +} + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ +function hashSet(key, value) { + var data = this.__data__; + this.size += this.has(key) ? 0 : 1; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; +} + +/** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Hash(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `Hash`. +Hash.prototype.clear = hashClear; +Hash.prototype['delete'] = hashDelete; +Hash.prototype.get = hashGet; +Hash.prototype.has = hashHas; +Hash.prototype.set = hashSet; + +/** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ +function listCacheClear() { + this.__data__ = []; + this.size = 0; +} + +/** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; +} + +/** Used for built-in method references. */ +var arrayProto = Array.prototype; + +/** Built-in value references. */ +var splice = arrayProto.splice; + +/** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + --this.size; + return true; +} + +/** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; +} + +/** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; +} + +/** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ +function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + ++this.size; + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; +} + +/** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function ListCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `ListCache`. +ListCache.prototype.clear = listCacheClear; +ListCache.prototype['delete'] = listCacheDelete; +ListCache.prototype.get = listCacheGet; +ListCache.prototype.has = listCacheHas; +ListCache.prototype.set = listCacheSet; + +/* Built-in method references that are verified to be native. */ +var Map$1 = getNative(root, 'Map'); + +/** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ +function mapCacheClear() { + this.size = 0; + this.__data__ = { + 'hash': new Hash, + 'map': new (Map$1 || ListCache), + 'string': new Hash + }; +} + +/** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ +function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); +} + +/** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ +function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; +} + +/** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function mapCacheDelete(key) { + var result = getMapData(this, key)['delete'](key); + this.size -= result ? 1 : 0; + return result; +} + +/** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function mapCacheGet(key) { + return getMapData(this, key).get(key); +} + +/** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function mapCacheHas(key) { + return getMapData(this, key).has(key); +} + +/** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ +function mapCacheSet(key, value) { + var data = getMapData(this, key), + size = data.size; + + data.set(key, value); + this.size += data.size == size ? 0 : 1; + return this; +} + +/** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function MapCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `MapCache`. +MapCache.prototype.clear = mapCacheClear; +MapCache.prototype['delete'] = mapCacheDelete; +MapCache.prototype.get = mapCacheGet; +MapCache.prototype.has = mapCacheHas; +MapCache.prototype.set = mapCacheSet; + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided, it determines the cache key for storing the result based on the + * arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is used as the map cache key. The `func` + * is invoked with the `this` binding of the memoized function. + * + * **Note:** The cache is exposed as the `cache` property on the memoized + * function. Its creation may be customized by replacing the `_.memoize.Cache` + * constructor with one whose instances implement the + * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) + * method interface of `clear`, `delete`, `get`, `has`, and `set`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] The function to resolve the cache key. + * @returns {Function} Returns the new memoized function. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * var other = { 'c': 3, 'd': 4 }; + * + * var values = _.memoize(_.values); + * values(object); + * // => [1, 2] + * + * values(other); + * // => [3, 4] + * + * object.a = 2; + * values(object); + * // => [1, 2] + * + * // Modify the result cache. + * values.cache.set(object, ['a', 'b']); + * values(object); + * // => ['a', 'b'] + * + * // Replace `_.memoize.Cache`. + * _.memoize.Cache = WeakMap; + */ +function memoize(func, resolver) { + if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var memoized = function() { + var args = arguments, + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; + + if (cache.has(key)) { + return cache.get(key); + } + var result = func.apply(this, args); + memoized.cache = cache.set(key, result) || cache; + return result; + }; + memoized.cache = new (memoize.Cache || MapCache); + return memoized; +} + +// Expose `MapCache`. +memoize.Cache = MapCache; + +/** Used as the maximum memoize cache size. */ +var MAX_MEMOIZE_SIZE = 500; + +/** + * A specialized version of `_.memoize` which clears the memoized function's + * cache when it exceeds `MAX_MEMOIZE_SIZE`. + * + * @private + * @param {Function} func The function to have its output memoized. + * @returns {Function} Returns the new memoized function. + */ +function memoizeCapped(func) { + var result = memoize(func, function(key) { + if (cache.size === MAX_MEMOIZE_SIZE) { + cache.clear(); + } + return key; + }); + + var cache = result.cache; + return result; +} + +/** Used to match property names within property paths. */ +var rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; + +/** Used to match backslashes in property paths. */ +var reEscapeChar = /\\(\\)?/g; + +/** + * Converts `string` to a property path array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the property path array. + */ +var stringToPath = memoizeCapped(function(string) { + var result = []; + if (string.charCodeAt(0) === 46 /* . */) { + result.push(''); + } + string.replace(rePropName, function(match, number, quote, subString) { + result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match)); + }); + return result; +}); + +/** + * Converts `value` to a string. An empty string is returned for `null` + * and `undefined` values. The sign of `-0` is preserved. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + * @example + * + * _.toString(null); + * // => '' + * + * _.toString(-0); + * // => '-0' + * + * _.toString([1, 2, 3]); + * // => '1,2,3' + */ +function toString(value) { + return value == null ? '' : baseToString(value); +} + +/** + * Casts `value` to a path array if it's not one. + * + * @private + * @param {*} value The value to inspect. + * @param {Object} [object] The object to query keys on. + * @returns {Array} Returns the cast property path array. + */ +function castPath(value, object) { + if (isArray(value)) { + return value; + } + return isKey(value, object) ? [value] : stringToPath(toString(value)); +} + +/** Used as references for various `Number` constants. */ +var INFINITY = 1 / 0; + +/** + * Converts `value` to a string key if it's not a string or symbol. + * + * @private + * @param {*} value The value to inspect. + * @returns {string|symbol} Returns the key. + */ +function toKey(value) { + if (typeof value == 'string' || isSymbol(value)) { + return value; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; +} + +/** + * The base implementation of `_.get` without support for default values. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to get. + * @returns {*} Returns the resolved value. + */ +function baseGet(object, path) { + path = castPath(path, object); + + var index = 0, + length = path.length; + + while (object != null && index < length) { + object = object[toKey(path[index++])]; + } + return (index && index == length) ? object : undefined; +} + +/** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ +function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; +} + +/** Built-in value references. */ +var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined; + +/** + * Checks if `value` is a flattenable `arguments` object or array. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is flattenable, else `false`. + */ +function isFlattenable(value) { + return isArray(value) || isArguments(value) || + !!(spreadableSymbol && value && value[spreadableSymbol]); +} + +/** + * The base implementation of `_.flatten` with support for restricting flattening. + * + * @private + * @param {Array} array The array to flatten. + * @param {number} depth The maximum recursion depth. + * @param {boolean} [predicate=isFlattenable] The function invoked per iteration. + * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks. + * @param {Array} [result=[]] The initial result value. + * @returns {Array} Returns the new flattened array. + */ +function baseFlatten(array, depth, predicate, isStrict, result) { + var index = -1, + length = array.length; + + predicate || (predicate = isFlattenable); + result || (result = []); + + while (++index < length) { + var value = array[index]; + if (depth > 0 && predicate(value)) { + if (depth > 1) { + // Recursively flatten arrays (susceptible to call stack limits). + baseFlatten(value, depth - 1, predicate, isStrict, result); + } else { + arrayPush(result, value); + } + } else if (!isStrict) { + result[result.length] = value; + } + } + return result; +} + +/** + * Flattens `array` a single level deep. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to flatten. + * @returns {Array} Returns the new flattened array. + * @example + * + * _.flatten([1, [2, [3, [4]], 5]]); + * // => [1, 2, [3, [4]], 5] + */ +function flatten(array) { + var length = array == null ? 0 : array.length; + return length ? baseFlatten(array, 1) : []; +} + +/** + * A specialized version of `baseRest` which flattens the rest array. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @returns {Function} Returns the new function. + */ +function flatRest(func) { + return setToString(overRest(func, undefined, flatten), func + ''); +} + +/** + * The base implementation of `_.hasIn` without support for deep paths. + * + * @private + * @param {Object} [object] The object to query. + * @param {Array|string} key The key to check. + * @returns {boolean} Returns `true` if `key` exists, else `false`. + */ +function baseHasIn(object, key) { + return object != null && key in Object(object); +} + +/** + * Checks if `path` exists on `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @param {Function} hasFunc The function to check properties. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + */ +function hasPath(object, path, hasFunc) { + path = castPath(path, object); + + var index = -1, + length = path.length, + result = false; + + while (++index < length) { + var key = toKey(path[index]); + if (!(result = object != null && hasFunc(object, key))) { + break; + } + object = object[key]; + } + if (result || ++index != length) { + return result; + } + length = object == null ? 0 : object.length; + return !!length && isLength(length) && isIndex(key, length) && + (isArray(object) || isArguments(object)); +} + +/** + * Checks if `path` is a direct or inherited property of `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + * @example + * + * var object = _.create({ 'a': _.create({ 'b': 2 }) }); + * + * _.hasIn(object, 'a'); + * // => true + * + * _.hasIn(object, 'a.b'); + * // => true + * + * _.hasIn(object, ['a', 'b']); + * // => true + * + * _.hasIn(object, 'b'); + * // => false + */ +function hasIn(object, path) { + return object != null && hasPath(object, path, baseHasIn); +} + +/** + * The base implementation of `_.set`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @param {Function} [customizer] The function to customize path creation. + * @returns {Object} Returns `object`. + */ +function baseSet(object, path, value, customizer) { + if (!isObject(object)) { + return object; + } + path = castPath(path, object); + + var index = -1, + length = path.length, + lastIndex = length - 1, + nested = object; + + while (nested != null && ++index < length) { + var key = toKey(path[index]), + newValue = value; + + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + return object; + } + + if (index != lastIndex) { + var objValue = nested[key]; + newValue = customizer ? customizer(objValue, key, nested) : undefined; + if (newValue === undefined) { + newValue = isObject(objValue) + ? objValue + : (isIndex(path[index + 1]) ? [] : {}); + } + } + assignValue(nested, key, newValue); + nested = nested[key]; + } + return object; +} + +/** + * The base implementation of `_.pickBy` without support for iteratee shorthands. + * + * @private + * @param {Object} object The source object. + * @param {string[]} paths The property paths to pick. + * @param {Function} predicate The function invoked per property. + * @returns {Object} Returns the new object. + */ +function basePickBy(object, paths, predicate) { + var index = -1, + length = paths.length, + result = {}; + + while (++index < length) { + var path = paths[index], + value = baseGet(object, path); + + if (predicate(value, path)) { + baseSet(result, castPath(path, object), value); + } + } + return result; +} + +/** + * The base implementation of `_.pick` without support for individual + * property identifiers. + * + * @private + * @param {Object} object The source object. + * @param {string[]} paths The property paths to pick. + * @returns {Object} Returns the new object. + */ +function basePick(object, paths) { + return basePickBy(object, paths, function(value, path) { + return hasIn(object, path); + }); +} + +/** + * Creates an object composed of the picked `object` properties. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The source object. + * @param {...(string|string[])} [paths] The property paths to pick. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.pick(object, ['a', 'c']); + * // => { 'a': 1, 'c': 3 } + */ +var pick = flatRest(function(object, paths) { + return object == null ? {} : basePick(object, paths); +}); + +// 不可以在原生的 XMLHttpRequest 上直接定义 getter 和 setter, +// 也不可以在 XHR 实例上定义 +// 这样的话会导致无法接收到数据 +// 但是确认为是 mockjs 内的数据返回就可以直接修改 XHR 实例了 + +function defineGetAndSet(XHR) { + // 将这些 键值对 映射到 $data 属性对象的对应值上去 + const array = ['readyState', 'status', 'response', 'responseText', 'statusText']; + const auto = array.reduce((col, cur) => { + col[cur] = { + get() { + return this.$data[cur]; + }, + set(state) { + this.$data[cur] = state; + }, + }; + return col; + }, {}); + Object.defineProperties(XHR, Object.assign(auto)); + XHR.getResponseHeader = function (name) { + return this._responseHeaders[name]; + }; + XHR.getAllResponseHeaders = function () { + return this._responseHeaders; + }; +} + +var HTTP_STATUS_CODES = { + 100: 'Continue', + 101: 'Switching Protocols', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 300: 'Multiple Choice', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 422: 'Unprocessable Entity', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', +}; + +// 使用不完全覆盖的方式,使用继承方式继承所有的属性 +let XHR; +const config = { + request: null, + response: null, + silent: false, +}; + +// ! 虽然 XMLHttpRequest 不能够修改,但是可以通过设置 getter 和 setter 将属性映射到 $属性上,这样的话,原生 XHR 会将数据写入和读取的位置更改为新的对象属性上,这样就可以被我们修改了。 + +class MockXMLHttpRequest extends window.XMLHttpRequest { + constructor(...args) { + super(...args); + } + _responseHeaders = {}; + $mock = true; // 标识是否打开拦截 + open(method, url, _, username, password) { + // 不进行同步操作 + XHR.prototype.open.call(this, method, url, true, username, password); + this.$data.url = url; + this.$data.method = method.toLowerCase(); + } + send(body) { + if (this.$mock) { + const options = pick(this.$data, ['headers', 'method']); + // ! 这里的 proxy 中的参数固定为 fetch 中的标准。 + const result = config.proxy(this.$data.url, options); + + if (result.body) { + defineGetAndSet(this); + this.dispatchEvent(new Event('loadstart')); + setTimeout(() => this.$done.bind(this)(result), this.timeout || 100); + return null; + } + // 这里穿透下去 + } + XHR.prototype.send.call(this, body); + } + setRequestHeader(key, value) { + this.$data.headers[key] = value; + return XHR.prototype.setRequestHeader.call(this, key, value); + } + + $data = { + // 原生属性的 getter 和 setter + readyState: 0, + status: 200, + response: '', + responseText: '', + statusText: '', + headers: {}, + url: '', + method: 'get', + }; + + $done({ body, headers, status }) { + // 伪造 XHR 返回事件 + this.readyState = this.HEADERS_RECEIVED; + this.dispatchEvent(new Event('readystatechange')); + this.readyState = this.LOADING; + this.dispatchEvent(new Event('readystatechange')); + this._responseHeaders = headers; + this.status = status; + this.statusText = HTTP_STATUS_CODES[status]; + this.response = body; + this.responseText = typeof this.response === 'string' ? this.response : JSON.stringify(this.response); + this.readyState = this.DONE; + this.dispatchEvent(new Event('readystatechange')); + this.dispatchEvent(new Event('load')); + this.dispatchEvent(new Event('loadend')); + } +} +function mockXHR({ proxy, silent = false }) { + if (proxy instanceof Function) config.request = proxy; + + config.silent = silent; + + // 这个文件中的 XHR + + // 代理 fetch 的初始化函数 + if (window.XMLHttpRequest && !window.XMLHttpRequest.$mock) { + [XHR, window.XMLHttpRequest] = [window.XMLHttpRequest, MockXMLHttpRequest]; + window.XMLHttpRequest.$mock = true; + if (!silent) console.warn('XHR 已经被代理'); + } +} + +function createRegexp(path, config = {}) { + return pathToRegexp(path, [], config); +} +class FakeServer { + constructor({ Routers, plugins = [] }) { + this.#initRouteMap(Routers); + if (plugins.length) this.#initAjaxProxy(plugins); + } + #RouteMap = new RouteMap(); + #initRouteMap(Routers) { + Routers.forEach((item) => { + item.matcher = createRegexp(item.path); + const route = new Route(item); + this.#RouteMap.addRoute(route); + }); + } + #initAjaxProxy(plugins) { + plugins.forEach((func) => func({ proxy: this.getServerResult.bind(this) })); } + // 传入的参数为 fetch 的传入参数 + // 返回的数据为 Response getServerResult(path, options = {}) { - const req = new Request(path); - const res = new Response(path, options); - const target = this.#findRoute(path); - if (target instanceof Route) { + const req = new Request(path, options); + const res = new Response$2(path, options); + const target = this.#RouteMap.matchRoute(path); + // target 为一个 router + if (target) { console.log(target); - return target.query(req, res); + target.request(req, res); + return res; } else { return null; } } } -export { Router }; +export { FakeServer, mockFetch, mockXHR }; diff --git a/FakeServer/fakeServer.js b/FakeServer/fakeServer.js index e2ff655..686d676 100644 --- a/FakeServer/fakeServer.js +++ b/FakeServer/fakeServer.js @@ -1,30 +1,37 @@ -import { pathToRegexp } from 'path-to-regexp/dist.es2015'; +import { pathToRegexp } from 'path-to-regexp'; import { Request, Response } from './src/Server.js'; import { Route, RouteMap } from './src/Route.js'; function createRegexp(path, config = {}) { return pathToRegexp(path, [], config); } - +export * from './ajax/index.js'; export class FakeServer { - constructor(Routers, {} = {}) { + constructor({ Routers, plugins = [] }) { this.#initRouteMap(Routers); + if (plugins.length) this.#initAjaxProxy(plugins); } #RouteMap = new RouteMap(); #initRouteMap(Routers) { Routers.forEach((item) => { - item.matcher = createRegexp(path); + item.matcher = createRegexp(item.path); const route = new Route(item); this.#RouteMap.addRoute(route); }); } - + #initAjaxProxy(plugins) { + plugins.forEach((func) => func({ proxy: this.getServerResult.bind(this) })); + } + // 传入的参数为 fetch 的传入参数 + // 返回的数据为 Response getServerResult(path, options = {}) { const req = new Request(path, options); const res = new Response(path, options); const target = this.#RouteMap.matchRoute(path); + // target 为一个 router if (target) { console.log(target); - return target.request(req, res); + target.request(req, res); + return res; } else { return null; } diff --git a/FakeServer/index.html b/FakeServer/index.html index 8575718..953e7e2 100644 --- a/FakeServer/index.html +++ b/FakeServer/index.html @@ -12,16 +12,19 @@ diff --git a/FakeServer/src/Route.js b/FakeServer/src/Route.js index ebbb67e..525d11c 100644 --- a/FakeServer/src/Route.js +++ b/FakeServer/src/Route.js @@ -11,8 +11,8 @@ export class Route { } async request(req, res) { const response = await this.callback(req, res); - if (response instanceof Response) return response; - return res; + if (response instanceof Response && response.data) return response; + return null; } } @@ -30,11 +30,13 @@ export class RouteMap extends Map { this.#RouteMatchers.some((reg) => { let result = path.match(reg); if (result) { - target = this.#RouteMap.get(reg); + target = this.get(reg); + return true; } return false; }); + if (target?.redirect) return this.matchRoute(target.redirect); return target; } } diff --git a/FakeServer/src/Server.js b/FakeServer/src/Server.js index 040b4df..2dcbb35 100644 --- a/FakeServer/src/Server.js +++ b/FakeServer/src/Server.js @@ -1,13 +1,26 @@ -import { parseURL } from './src/parseURL.js'; +import { parseURL } from './parseURL.js'; export class Request { constructor(path, options) { - Object.assign(this, parseURL(path), { - headers: options.headers, - }); + Object.assign(this, parseURL(path), options); } } export class Response { - constructor(path, options = {}) {} - send() {} - setHeaders() {} + constructor() {} + headers = { + 'Access-Control-Allow-Origin': '*', + Age: 1200, // 单位为秒 + Allow: 'GET, POST', + 'Content-Encoding': 'gzip', + 'Content-Type': 'text/html; charset=utf-8', + }; + status = 200; + body = null; + send(body) { + this.body = body; + } + setHeaders(headers) { + Object.entries(headers).forEach(([key, value]) => { + this.headers[key] = value; + }); + } }