diff --git a/ .gitattributes b/ .gitattributes new file mode 100644 index 0000000..fcadb2c --- /dev/null +++ b/ .gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c819e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +.deno_plugins +**/*.rs.bk +.idea/ +.DS_Store +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..01f8c9b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 manyuanrong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5bc21d2 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# deno_es + +> **deno_es** is a **Elastic Search** database driver developed for deno + +## Examples + +```ts +import { Client } from "../mod.ts"; +const client = new Client(); +await client.connect("http://localhost:9200/"); + +const ajax = async () => { + try { + const count = await client.count({ + index: "myindex2", + // method: "post", + }); + console.log(count); + } catch (error) { + console.error(error); + } +}; + +console.time("ajax"); +// await Array.from(new Array(100)).map(ajax); +await ajax(); +console.timeEnd("ajax"); + +``` diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..eebeeb3 --- /dev/null +++ b/deps.ts @@ -0,0 +1,7 @@ +export { deferred } from "https://deno.land/std@0.83.0/async/deferred.ts"; +export type { Deferred } from "https://deno.land/std@0.83.0/async/deferred.ts"; +export { + assert, + assertEquals, +} from "https://deno.land/std@0.83.0/testing/asserts.ts"; +export { urlParse } from "https://deno.land/x/url_parse/mod.ts"; diff --git a/examples/conn.ts b/examples/conn.ts new file mode 100644 index 0000000..05a6585 --- /dev/null +++ b/examples/conn.ts @@ -0,0 +1,23 @@ +import { Client } from "../mod.ts"; + +const client = new Client(); + +await client.connect("http://localhost:9200/"); + +const ajax = async () => { + try { + const names = await client.count({ + index: "myindex2", + method: "post", + }); + + console.log(names); + } catch (error) { + console.error(error); + } +}; + +console.time("ajax"); +// await Array.from(new Array(100)).map(ajax); +await ajax(); +console.timeEnd("ajax"); diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..4e4396f --- /dev/null +++ b/mod.ts @@ -0,0 +1 @@ +export { Client } from "./src/client.ts"; diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..414b1fd --- /dev/null +++ b/src/client.ts @@ -0,0 +1,82 @@ +import { assert, urlParse } from "../deps.ts"; +import { Ajax, ajax, Method } from "./utils/ajax.ts"; + +const DENO_DRIVER_VERSION = "0.0.1"; + +export class Client { + // cache db + #dbCache = new Map(); + + #connectionCache = new Map(); + + db: string | undefined; + + private conn: Deno.Conn | undefined; + + public connectedCount = 0; + + connectDB(db: string) { + this.db = db; + Ajax.defaults.baseURL = db; + const options = urlParse(db); + return Deno.connect({ + hostname: options.hostname, + port: Number(options.port), + }); + } + + connect( + cacheKey: string, + ): Promise { + try { + if (this.#connectionCache.has(cacheKey)) { + return this.#connectionCache.get(cacheKey); + } + const promise = this.connectDB(cacheKey); + this.connectedCount++; + this.#connectionCache.set(cacheKey, promise); + return promise.then((conn) => { + this.conn = conn; + }); + } catch (e) { + throw new Error(`Connection failed: ${e.message || e}`); + } + } + + count(options: { + method?: Method; + body?: string; + index: string; + }) { + assert(this.conn); + let path = ""; + + let { index, body, method } = options; + + if (index != null) { + if (method == null) method = body == null ? "GET" : "POST"; + path = "/" + encodeURIComponent(index) + "/" + "_count"; + } else { + if (method == null) method = body == null ? "GET" : "POST"; + path = "/" + "_count"; + } // build request object + return ajax({ + url: path, + method, + data: body, + }); + } + + close() { + if (this.conn) { + this.conn.close(); + } + this.#dbCache.clear(); + this.#connectionCache.clear(); + this.connectedCount = 0; + } + + get version() { + return DENO_DRIVER_VERSION; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..249aed8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,499 @@ +import { Bson } from "../deps.ts"; + +export type Document = Bson.Document; + +export interface Server { + host: string; + port: number; + domainSocket?: string; +} + +export interface ConnectOptions { + compression?: string[]; + certFile?: string; + keyFile?: string; + keyFilePassword?: string; + tls?: boolean; + safe?: boolean; + credential?: Credential; + db: string; + servers: Server[]; + retryWrites?: boolean; + appname?: string; +} + +export interface CountOptions { + limit?: number; + skip?: number; + hint?: Document | string; + comment?: Document; + readConcern?: Document; + collation?: Document; +} + +export interface FindOptions { + findOne?: boolean; + skip?: number; + limit?: number; + projection?: Document; + sort?: Document; + noCursorTimeout?: boolean; +} + +export interface ListDatabaseInfo { + name: string; + sizeOnDisk?: number; + empty?: false; +} + +export interface InsertOptions { + /** + * Optional. If true, then when an insert of a document fails, return without inserting any remaining documents listed in the inserts array. + * If false, then when an insert of a document fails, continue to insert the remaining documents. Defaults to true. + */ + ordered?: boolean; + + /** + * Optional. A document that expresses the write concern of the insert command. Omit to use the default write concern. + * Do not explicitly set the write concern for the operation if run in a transaction. To use write concern with transactions, see Transactions and Write Concern. + */ + writeConcern?: Document; + + /** + * Optional. Enables insert to bypass document validation during the operation. This lets you insert documents that do not meet the validation requirements. + */ + bypassDocumentValidation?: boolean; + + /** + * Optional. A user-provided comment to attach to this command. + */ + comment?: Document; +} +export interface UpdateOptions { + /** + * Optional. A document expressing the write concern of the update command. Omit to use the default write concern. + */ + writeConcern?: Document; + + /** + * Optional. If true, then when an update statement fails, return without performing the remaining update statements. + * If false, then when an update fails, continue with the remaining update statements, if any. Defaults to true. + */ + ordered?: boolean; + + /** + * Optional. If true, updates all documents that meet the query criteria. + * If false, limit the update to one document that meet the query criteria. Defaults to false. + */ + multi?: boolean; + + /** + * optional list of array filters referenced in filtered positional operators + */ + arrayFilters?: Document[]; + + /** + * Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields). + */ + collation?: Document; + + /** + * Allow driver to bypass schema validation in MongoDB 3.2 or higher + */ + bypassDocumentValidation?: boolean; + + /** + * An optional hint for query optimization. See the update (https://docs.mongodb.com/manual/reference/command/update/#update-command-hint) command reference for more information. + */ + hint?: Document; + + /** + * When true, creates a new document if no document matches the query. + */ + upsert?: boolean; + + /** + * The write concern timeout. + */ + wtimeout?: number; + + /** + * If true, will throw if bson documents start with $ or include a . in any key value + */ + checkKeys?: boolean; + + /** + * Serialize functions on any object. + */ + serializeFunctions?: boolean; + + /** + * Specify if the BSON serializer should ignore undefined fields. + */ + ignoreUndefined?: boolean; + + /** + * Optional. A user-provided comment to attach to this command. + */ + comment?: Document; + + /** + * optional session to use for this operation + */ + // session?: ClientSession +} + +export interface DeleteOptions { + /** + * Optional. If true, then when a delete statement fails, return without performing the remaining delete statements. + * If false, then when a delete statement fails, continue with the remaining delete statements, if any. Defaults to true. + */ + ordered?: boolean; + + /** + * Optional. A document expressing the write concern of the delete command. Omit to use the default write concern. + */ + writeConcern?: Document; + + /** + * Optional. Specifies the collation to use for the operation. + * See https://docs.mongodb.com/manual/reference/command/delete/#deletes-array-collation + */ + collation?: Document; + + /** + * Optional. A user-provided comment to attach to this command. + */ + comment?: Document; + + /** + * The number of matching documents to delete. Specify either a 0 to delete all matching documents or 1 to delete a single document. + */ + limit?: number; + + /** + * Optional. A document or string that specifies the index to use to support the query predicate. + * The option can take an index specification document or the index name string. + * If you specify an index that does not exist, the operation errors. + */ + hint?: Document | string; +} + +export interface DropOptions { + /** + * Optional. A document expressing the write concern of the drop command. Omit to use the default write concern. + */ + writeConcern?: Document; + + /** + * Optional. A user-provided comment to attach to this command. + */ + comment?: any; +} + +export interface DistinctOptions { + /** + * The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). + */ + readPreference?: string; + /** + * Number of milliseconds to wait before aborting the query. + */ + maxTimeMS?: number; + /** + * pecify collation settings for operation. See aggregation documentation(https://docs.mongodb.com/manual/reference/command/aggregate). + */ + collation?: Document; + /** + * optional session to use for this operation + */ + // session?:ClientSession; +} + +export interface AggregateOptions { + /** + * The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). + */ + readPreference?: string; + /** + * @default 1000 + * The number of documents to return per batch. See aggregation documentation(https://docs.mongodb.com/manual/reference/command/aggregate). + */ + batchSize?: number; + /** + * @default false + * Explain returns the aggregation execution plan (requires mongodb 2.6 >). + */ + explain?: boolean; + /** + * @default false + * allowDiskUse lets the server know if it can use disk to store temporary results for the aggregation (requires mongodb 2.6 >). + */ + allowDiskUse?: boolean; + /** + * maxTimeMS specifies a cumulative time limit in milliseconds for processing operations on the cursor. MongoDB interrupts the operation at the earliest following interrupt point. + */ + maxTimeMS?: number; + /** + * @default false + * Allow driver to bypass schema validation in MongoDB 3.2 or higher. + */ + bypassDocumentValidation?: boolean; + /** + * @default false + *Return document results as raw BSON buffers. + */ + raw?: boolean; + /** + * @default true + * Promotes Long values to number if they fit inside the 53 bits resolution. + */ + promoteLongs?: boolean; + /** + * @default true + * Promotes BSON values to native types where possible, set to false to only receive wrapper types. + */ + promoteValues?: boolean; + /** + * @default false + * Promotes Binary BSON values to native Node Buffers. + */ + promoteBuffers?: boolean; + /** + * Specify collation settings for operation. See aggregation documentation(https://docs.mongodb.com/manual/reference/command/aggregate). + */ + collation?: Document; + /** + * Add a comment to an aggregation command + */ + comment?: string; + /** + * Add an index selection hint to an aggregation command + */ + hint?: string | Document; + /** + * optional session to use for this operation + */ + // session?:ClientSession; +} + +export interface CreateUserOptions { + /** + * The name of the new user. + */ + username?: string; + + /** + * The user’s password. The pwd field is not required if you run createUser on the $external database to create users who have credentials stored externally to MongoDB. + */ + password?: string; + + /** + * Optional. Any arbitrary information. This field can be used to store any data an admin wishes to associate with this particular user. For example, this could be the user’s full name or employee id. + */ + customData?: Document; + + /** + * The roles granted to the user. Can specify an empty array [] to create users without roles. + */ + roles?: (string | { + role: string; + db: string; + })[]; + + /** + * Optional. Indicates whether the server or the client digests the password. + * See https://docs.mongodb.com/manual/reference/command/createUser/#dbcmd.createUser + */ + digestPassword?: boolean; + + /** + * Optional. The level of write concern for the creation operation. The writeConcern document takes the same fields as the getLastError command. + */ + writeConcern?: Document; + + /** + * Optional. The authentication restrictions the server enforces on the created user. Specifies a list of IP addresses and CIDR ranges from which the user is allowed to connect to the server or from which the server can accept users. + */ + authenticationRestrictions?: Document[]; + + /** + * Optional. Specify the specific SCRAM mechanism or mechanisms for creating SCRAM user credentials. + */ + mechanisms?: ("SCRAM-SHA-1" | "SCRAM-SHA-256")[]; + + /** + * Optional. A user-provided comment to attach to this command. + */ + comment?: Document; +} + +export interface Credential { + /** + * The username to authenticate with. This applies to all mechanisms but may be omitted when authenticating via MONGODB-X509. + */ + username?: string; + + /** + * The password to authenticate with. This does not apply to all mechanisms. + */ + password?: string; + + /** + * The database used to authenticate. This applies to all mechanisms and defaults to "admin" in SCRAM authentication mechanisms and "$external" for GSSAPI, MONGODB-X509 and PLAIN. + */ + db?: string; + + /** + * Which authentication mechanism to use. If not provided, one will be negotiated with the server. + */ + mechanism?: "SCRAM-SHA-1" | "SCRAM-SHA-256" | "MONGODB-X509"; +} + +export interface IndexOptions { + /** + * Specifies the index’s fields. For each field, specify a key-value pair in which + * the key is the name of the field to index and the value is either the index direction + * or index type. If specifying direction, specify 1 for ascending or -1 for descending. + */ + key: Document; + + /** + * A name that uniquely identifies the index. + */ + name: string; + + /** + * Optional. Deprecated in MongoDB 4.2. + */ + background?: boolean; + + /** + * Optional. Creates a unique index so that the collection will not accept insertion + * or update of documents where the index key value matches an existing value in the index. + * Specify true to create a unique index. The default value is false. + */ + unique?: boolean; + + /** + * Optional. If specified, the index only references documents that match the filter expression. + * See Partial Indexes for more information. + */ + partialFilterExpression?: Document; + + /** + * Optional. If true, the index only references documents with the specified field. + * These indexes use less space but behave differently in some situations (particularly sorts). + * The default value is false. See Sparse Indexes for more information. + */ + sparse?: boolean; + + /** + * Optional. Specifies a value, in seconds, as a TTL to control how long MongoDB retains + * documents in this collection. See Expire Data from Collections by Setting TTL for + * more information on this functionality. This applies only to TTL indexes. + */ + expireAfterSeconds?: number; + + /** + * Optional. A flag that determines whether the index is hidden from the query planner. + * A hidden index is not evaluated as part of query plan selection. Default is false. + */ + hidden?: boolean; + + /** + * Optional. Allows users to configure the storage engine on a per-index basis when creating an index. + */ + storageEngine?: Document; + + /** + * Optional. For text indexes, a document that contains field and weight pairs. + * The weight is an integer ranging from 1 to 99,999 and denotes the significance + * of the field relative to the other indexed fields in terms of the score. + * You can specify weights for some or all the indexed fields. + * See Control Search Results with Weights to adjust the scores. + * The default value is 1. + */ + weights?: Document; + + /** + * Optional. For text indexes, the language that determines the list of + * stop words and the rules for the stemmer and tokenizer. + * See Text Search Languages for the available languages and Specify a Language + * for Text Index for more information and examples. The default value is english. + */ + default_language?: string; + + /** + * Optional. For text indexes, the name of the field, in the collection’s documents, + * that contains the override language for the document. The default value is language. + * See Use any Field to Specify the Language for a Document for an example. + */ + language_override?: string; + + /** + * Optional. The text index version number. Users can use this option to override the default version number. + */ + textIndexVersion?: number; + + /** + * Optional. The 2dsphere index version number. Users can use this option to override the default version number. + */ + "2dsphereIndexVersion"?: number; + + /** + * Optional. For 2d indexes, the number of precision of the stored geohash value of the location data. + * The bits value ranges from 1 to 32 inclusive. The default value is 26. + */ + bits?: number; + + /** + * Optional. For 2d indexes, the lower inclusive boundary for the longitude and latitude values. The default value is -180.0. + */ + min?: number; + + /** + * Optional. For 2d indexes, the upper inclusive boundary for the longitude and latitude values. The default value is 180.0. + */ + max?: number; + + /** + * For geoHaystack indexes, specify the number of units within which to group the location values; + * i.e. group in the same bucket those location values that are within the specified number + * of units to each other. The value must be greater than 0. + */ + bucketSize?: number; + + /** + * Optional. Specifies the collation for the index. + */ + collation?: Document; + + /** + * Allows users to include or exclude specific field paths from a wildcard index using + * the { "$**" : 1} key pattern. This option is only valid if creating a wildcard index + * on all document fields. You cannot specify this option if creating a wildcard index + * on a specific field path and its subfields, e.g. { "path.to.field.$**" : 1 } + */ + wildcardProjection?: Document; +} + +export interface CreateIndexOptions { + /** + * Specifies the indexes to create. Each document in the array specifies a separate index. + */ + indexes: IndexOptions[]; + + /** Optional. A document expressing the write concern. Omit to use the default write concern. */ + writeConcern?: Document; + + /** + * Optional. The minimum number of data-bearing voting replica set members (i.e. commit quorum), + * including the primary, that must report a successful index build before the primary marks the indexes as ready. + * A “voting” member is any replica set member where members[n].votes is greater than 0. + */ + commitQuorum?: number | string; + + /** Optional. A user-provided comment to attach to this command. Once set */ + comment?: Document; +} diff --git a/src/utils/ajax.ts b/src/utils/ajax.ts new file mode 100644 index 0000000..2f4e839 --- /dev/null +++ b/src/utils/ajax.ts @@ -0,0 +1,539 @@ +import { join } from "https://deno.land/std@0.99.0/path/mod.ts"; + +export type Method = + | "get" + | "GET" + | "delete" + | "DELETE" + | "head" + | "HEAD" + | "options" + | "OPTIONS" + | "post" + | "POST" + | "put" + | "PUT" + | "patch" + | "PATCH" + | "purge" + | "PURGE" + | "link" + | "LINK" + | "unlink" + | "UNLINK"; + +export type Credentials = "omit" | "include" | "same-origin"; + +export type Mode = "same-origin" | "cors" | "no-cors"; + +export type AbortResult = { + promise: Promise; + abort: () => void; +}; + +export type CacheResult = { + promise: Promise; + config: AjaxConfig; + controller?: AbortController; +}; + +export interface RequestConfig { + url?: string; + + keepalive?: boolean; + + method?: Method; + baseURL?: string; + headers?: any; + data?: any; + timeout?: number; + timeoutErrorMessage?: string; + timeoutErrorStatus?: number; + /** + * omit:忽略cookie的发送 + * + * same-origin: 表示cookie只能同域发送,不能跨域发送 + * + * include: cookie既可以同域发送,也可以跨域发送 + */ + credentials?: Credentials; + /** + * same-origin:该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个error告知不能跨域;其对应的response type为basic。 + * + * cors: 该模式支持跨域请求,顾名思义它是以CORS的形式跨域;当然该模式也可以同域请求不需要后端额外的CORS支持;其对应的response type为cors。 + * + * no-cors: 该模式用于跨域请求但是服务器不带CORS响应头,也就是服务端不支持CORS;这也是fetch的特殊跨域请求方式;其对应的response type为opaque。 + */ + mode?: Mode; + + stoppedErrorMessage?: string; +} + +export interface AjaxExConfig extends RequestConfig { + isFile?: boolean; // 是否要传递文件 + isNoAlert?: boolean; // 是否要提示错误信息,默认提示 + isUseOrigin?: boolean; // 为true时,直接返回response,不再处理结果 + isEncodeUrl?: boolean; //get请求时是否要进行浏览器编码 + isOutStop?: boolean; + /** + * 主动控制取消请求时可传递此参数,或者直接使用ajaxAbortResult方法。例如: + * + * const controller = new AbortController(); + * const {signal} = controller; + */ + signal?: AbortSignal; + /** + * 如果是-1,代表不清除缓存 + * + * 如果是0,代表不使用缓存 + */ + cacheTimeout?: number; +} + +export interface AjaxConfig extends AjaxExConfig { + url: string; + method: Method; + data?: FormData | any; +} + +type RequestCallback = (config: AjaxConfig) => AjaxConfig; + +type ErrorCallback = (error: Error) => Promise; + +type ResponseCallback = (data: any) => Promise; + +class Interceptors { + public chain: any[]; + + constructor() { + this.chain = []; + } + + use(callback: T, errorCallback: ErrorCallback) { + this.chain.push(callback, errorCallback); + return this.chain.length - 2; + } + + eject(index: number) { + this.chain.splice(index, 2); + } +} + +export class BaseAjax { + static defaults: AjaxExConfig = { + credentials: "include", + mode: "cors", + timeout: 1000 * 60 * 2, + timeoutErrorMessage: "timeout", + timeoutErrorStatus: 504, + stoppedErrorMessage: "Ajax has been stopped! ", + method: "get", + keepalive: true, + }; + + public interceptors = { + request: new Interceptors(), + response: new Interceptors(), + }; + + public caches = new Map(); // 缓存所有已经请求的Promise,同一时间重复的不再请求 + private IS_AJAX_STOP = false; + + /** + * 停止ajax + */ + stopAjax() { + this.IS_AJAX_STOP = true; + } + + isAjaxStopped() { + return this.IS_AJAX_STOP; + } + + getUniqueKey(config: AjaxConfig) { + return (config.baseURL || "") + config.url + config.method + + (config.data ? JSON.stringify(config.data) : ""); + } + + /** + * 取消接口请求 + * @param controller 取消控制器 + */ + abort(controller: AbortController | undefined) { + if (controller) { + controller.abort(); + } + } + + /** + * 取消所有接口请求 + */ + abortAll() { + for (const cache of this.caches.values()) { + if (!cache.config.isOutStop) { // 如果是要跳出停止处理的,就不能给取消了 + this.abort(cache.controller); + } + } + } + + /** + * 提示错误,可以配置不提示 + */ + private showMessage(msg: string, config?: AjaxConfig) { + if (config && config.isNoAlert) { + return; + } + if (!msg) { + console.error("No message available"); + return; + } + this.handleMessage(msg); + } + + /** + * 处理消息,具体实现可以覆盖此项 + */ + protected handleMessage(msg: string) { + console.error(msg); + } + + private handleGetUrl(url: string, data: any, isEncodeUrl?: boolean) { + let tempUrl = url; + if (typeof data === "object") { + const exArr = []; + for (const key in data) { + exArr.push(key + "=" + data[key]); + } + if (exArr.length > 0) { + const exUrl = isEncodeUrl + ? encodeURI(encodeURI(exArr.join("&"))) + : exArr.join("&"); //这里怎么加密,与后台解密方式也有关。如果不是这样的格式,就自己拼接url + if (!tempUrl.includes("?")) { + tempUrl += "?" + exUrl; + } else { + tempUrl += "&" + exUrl; + } + } + } else { + if (data) { + if (!tempUrl.includes("?")) { + tempUrl += "?" + data; + } else { + tempUrl += "&" + data; + } + } + } + return tempUrl; + } + + private handleBaseUrl(url: string, baseURL?: string) { + return join(baseURL || "", url); + } + + private handlePostData(data: any, isFile?: boolean) { + let obj = data; + if (typeof data === "object") { + if (isFile) { //文件上传 + const formData = new FormData(); //构造空对象,下面用append方法赋值。 + for (const key in data) { + const value = data[key]; + if (key == "files" && Array.isArray(value)) { + value.forEach((file) => formData.append(key, file)); + } else { + formData.append(key, value); //例:formData.append("file", document.getElementById('fileName').files[0]); + } + } + obj = formData; + } else { + obj = JSON.stringify(data); + } + } + return obj; + } + + /** + * 进行fetch请求 + * @param config 配置 + */ + async request(config: AjaxConfig) { + const { + url, + baseURL, //接着的前缀url + data, + headers = {}, + method, + credentials, + isFile, + isUseOrigin, + isEncodeUrl, //get请求时是否要进行浏览器编码 + ...otherParams + } = config; + + let tempUrl = this.handleBaseUrl(url, baseURL); + let body: any; + if (method.toUpperCase() === "GET") { + body = null; //get请求不能有body + tempUrl = this.handleGetUrl(tempUrl, data, isEncodeUrl); + } else { + body = this.handlePostData(data, isFile); + if (isFile) { + if (!headers["Content-Type"]) { + headers["Content-Type"] = "application/x-www-form-urlencoded"; + } + } else { + if (!headers["Content-Type"]) { + headers["Content-Type"] = "application/json"; + } + } + } + try { + const response = await fetch(tempUrl, { + headers, + body, + method, + credentials, + ...otherParams, + }); + if (!response.ok) { //代表网络请求失败,原因可能是token失效,这时需要跳转到登陆页 + this.handleErrorResponse(response); + return Promise.reject(response); + } + if (isUseOrigin) { + return response; + } + //以下处理成功的结果 + return response.json(); + } catch (err) { //代表网络异常 + if (!this.isAbortError(err)) { //不属于主动取消的,需要进行提示 + this.showMessage(err, config); + } + return Promise.reject(err); + } + } + + /** + * 处理错误请求 + */ + protected handleErrorResponse(response: Response) { + console.error( + `HTTP error, status = ${response.status}, statusText = ${response.statusText}`, + ); + } + + isAbortError(err: Error) { + return err.name === "AbortError"; + } + + /** + * 实现fetch的timeout 功能 + * @param fecthPromise fetch + * @param controller 取消控制器 + * @param config + **/ + private fetch_timeout( + fecthPromise: Promise, + controller: AbortController | undefined, + config: AjaxConfig, + ) { + let tp: any; + const timeout = config.timeout; + const abortPromise = new Promise((resolve, reject) => { + tp = setTimeout(() => { + this.abort(controller); + reject({ + code: config.timeoutErrorStatus, + message: config.timeoutErrorMessage, + }); + }, timeout); + }); + + return Promise.race([fecthPromise, abortPromise]).then((res) => { + clearTimeout(tp); + return res; + }); + } + + private mergeAbortConfig( + config: AjaxConfig, + signal?: AbortSignal, + ): AbortController | undefined { + let controller; + if (typeof AbortController === "function" && signal === undefined) { // 如果要自己控制取消请求,需要自己传递signal,或者使用isReturnAbort参数 + controller = new AbortController(); + config.signal = controller.signal; + } + return controller; + } + + mergeConfig(cfg: AjaxConfig) { + const config = Object.assign({}, BaseAjax.defaults, cfg); // 把默认值覆盖了 + const chain = this.interceptors.request.chain; + let callback; + let errCallback; + while (callback = chain.shift()) { + try { + errCallback = chain.shift(); + callback(config); + } catch (e) { + console.error(e); + errCallback(e); // TODO 这个作用没想好 + break; + } + } + return config; + } + + mergeResponse(promise: Promise) { + const chain = this.interceptors.response.chain; + while (chain.length) { + promise = promise.then(chain.shift(), chain.shift()); + } + return promise; + } + + clearCacheByKey(uniqueKey: string, cacheTimeout?: number) { + if (cacheTimeout !== undefined) { + if (cacheTimeout >= 0) { // 如果小于0,不清除 + setTimeout(() => { + this.caches.delete(uniqueKey); + }, cacheTimeout); + } + } else { + this.caches.delete(uniqueKey); + } + } + + /** + * 缓存请求,同一时间同一请求只会向后台发送一次 + */ + private cache_ajax(cfg: AjaxConfig): CacheResult { + const config = this.mergeConfig(cfg); + const { signal, cacheTimeout } = config; + if (cacheTimeout === 0) { // 不缓存结果 + const controller = this.mergeAbortConfig(config, signal); + const promise = this.request(config); + return { + promise: this.mergeResponse(promise), + config, + controller, + }; + } + const uniqueKey = this.getUniqueKey(config); + const caches = this.caches; + if (!caches.has(uniqueKey)) { + const controller = this.mergeAbortConfig(config, signal); + const temp = this.request(config).then((result) => { + this.clearCacheByKey(uniqueKey, config.cacheTimeout); + return result; + }, (err) => { + this.clearCacheByKey(uniqueKey, config.cacheTimeout); + return Promise.reject(err); + }); + const promise = this.fetch_timeout(temp, controller, config); + caches.set(uniqueKey, { + promise: this.mergeResponse(promise), + config, + controller, + }); + } + return caches.get(uniqueKey); + } + + /** + * ajax主方法,返回promise + */ + ajax(cfg: AjaxConfig): Promise { + const { isOutStop } = cfg; + if (!isOutStop && this.isAjaxStopped()) { + return Promise.reject(BaseAjax.defaults.stoppedErrorMessage); + } + const result = this.cache_ajax(cfg); + return result.promise; + } + + /** + * 调用ajax的同时,返回取消ajax请求的方法 + */ + ajaxAbortResult(cfg: AjaxConfig): AbortResult { + const { isOutStop } = cfg; + if (!isOutStop && this.isAjaxStopped()) { + const promise = Promise.reject(BaseAjax.defaults.stoppedErrorMessage); + return { + promise, + abort: () => { + }, + }; + } + const result = this.cache_ajax(cfg); + return { + promise: result.promise, + abort: () => { + return this.abort(result.controller); + }, + }; + } + + get(url: string, data?: any, options?: AjaxExConfig) { + return this.ajax({ + url, + method: "get", + data, + ...options, + }); + } + + /** + * 调用ajax的get请求的同时,返回取消ajax请求的方法 + */ + getAbortResult(url: string, data?: any, options?: AjaxExConfig) { + return this.ajaxAbortResult({ + url, + method: "get", + data, + ...options, + }); + } + + post(url: string, data?: any, options?: AjaxExConfig) { + return this.ajax({ + url, + method: "post", + data, + ...options, + }); + } + + /** + * 调用ajax的post请求同时,返回取消ajax请求的方法 + */ + postAbortResult(url: string, data?: any, options?: AjaxExConfig) { + return this.ajaxAbortResult({ + url, + method: "post", + data, + ...options, + }); + } +} + +export class Ajax extends BaseAjax { + /** + * 处理错误请求 + */ + protected handleErrorResponse(response: Response) { + console.error( + `HTTP error, status = ${response.status}, statusText = ${response.statusText}`, + ); + if (response.status === 401) { //权限问题 + this.stopAjax(); + this.abortAll(); + // toLogin(); + } + } +} + +const instance = new Ajax(); + +export const ajax = instance.ajax.bind(instance); +export const get = instance.get.bind(instance); +export const post = instance.post.bind(instance);