diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0048e1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ + +node_modules/ +dist/ + +.vscode +*.sublime-project +*.sublime-workspace +package-lock.json + + +._* + +.Spotlight-V100 +.Trashes +.DS_Store +.AppleDouble +.LSOverride \ No newline at end of file diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..b007fb1 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,10 @@ +jsxBracketSameLine: true +jsxSingleQuote: true +semi: false +singleQuote: true +printWidth: 80 +useTabs: false +tabWidth: 2 +trailingComma: none +bracketSpacing: true +arrowParens: avoid \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab60297 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 + +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..8128506 --- /dev/null +++ b/Readme.md @@ -0,0 +1,83 @@ +## node-fetch的增强版 +> node-fetch的增强版, 增加注入及数据处理, 支持多实例。 + + +## Node.js 兼容性 + +因为需要支持 ESM。所以需要 Node.js >= v12.0.0, + + + +### 示例 + +```js +import fetch from '@bytedo/node-fetch' + + +fetch('/get_list', {body: {page: 1}}) + .then(r => r.json()) + .then(list => { + console.log(list) + }) + + +// 创建一个新的fetch实例, 可传入新的基础域名, 和公共参数等 +var f1 = fetch.create('//192.168.1.101', {headers: {token: 123456}}) + +f1('/get_list', {body: {page: 1}}) + .then(r => r.json()) + .then(list => { + console.log(list) + }) + + +``` + + + +### APIs + +#### 1. fetch(url[, options]) +> 发起一个网络请求, options的参数如下。 同时支持配置公共域名, 公共参数。 + + + method`` 默认GET, 可选GET/POST/PUT/DELETE... + + body`` 要发送的数据, 如果是不允许有`body`的方式, 会被自动拼接到url上 + + cache`` 是否缓存, + + credentials`` 是否校验 + + signal`` 网络控制信号, 可用于中断请求 + + timeout`` 超时时间, 默认30秒, 单位毫秒 + + +```js +fetch.BASE_URL = '//192.168.1.100' +// 1.2.0开始支持注入 +fetch.inject.request(function(conf) { + // 无需返回值, 但需要注意这是引用类型,不要对带个conf赋值 + conf.headers.token = 123456 +}) + +// 响应注入, 需要有返回值 +fetch.inject.response(function(res) { + return res.json() +}) +``` + + +#### 2. fetch.create() +> 创建一个新的fetch实例, 可以无限创建多个实例(用于同一个项目中有多组不同的接口)。 + +```js +var another = fetch.create() +another.BASE_URL = '//192.168.1.101' +// 新创建的实例, 也支持注入 +another.inject.request(function(conf) { + conf.headers.token = 123456 +}) + +another.inject.response(function(res) { + return res.json() +}) + +``` + + diff --git a/build.js b/build.js new file mode 100644 index 0000000..170e068 --- /dev/null +++ b/build.js @@ -0,0 +1,18 @@ +/** + * {build} + * @author yutent + * @date 2021/08/09 11:59:41 + */ + +import Es from 'esbuild' + +const mode = process.argv.slice(2).shift() + +Es.build({ + entryPoints: ['src/index.js'], + bundle: true, + watch: mode === 'dev' ? true : false, + minify: mode === 'dev' ? false : true, + format: 'esm', + outdir: 'dist' +}) diff --git a/package.json b/package.json index d271fa7..88fa0d2 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { + "type": "module", "name": "@bytedo/node-fetch", - "version": "1.0.0", - "description": "", - "main": "index.js", + "version": "0.0.1", + "description": "node-fetch的增强版, 增加注入及数据处理", + "main": "dist/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node build.js dev", + "build": "node build.js" }, "repository": { "type": "git", @@ -15,5 +17,8 @@ "fetch" ], "author": "", + "dependencies": { + "node-fetch": "^3.3.0" + }, "license": "MIT" } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..d883169 --- /dev/null +++ b/src/index.js @@ -0,0 +1,221 @@ +/** + * 新一代版本 + * @author yutent + * @date 2020/07/31 18:59:47 + */ + +import nativeFetch from 'node-fetch' +import { Format, toS } from './lib/format.js' + +const NOBODY_METHODS = ['GET', 'HEAD'] +const FORM_TYPES = { + form: 'application/x-www-form-urlencoded; charset=UTF-8', + json: 'application/json; charset=UTF-8', + text: 'text/plain; charset=UTF-8' +} +const ERRORS = { + 10001: 'Argument url is required', + 10012: 'Parse error', + 10100: 'Request canceled', + 10104: 'Request pending...', + 10200: 'Ok', + 10204: 'No content', + 10304: 'Not modified', + 10500: 'Internal Server Error', + 10504: 'Connected timeout' +} + +class _Request { + constructor(url = '', options = {}, owner) { + if (!url) { + throw new Error(ERRORS[10001]) + } + + // url规范化 + url = url.replace(/#.*$/, '') + + if (owner.BASE_URL) { + if (!/^([a-z]+:|\/\/)/.test(url)) { + url = owner.BASE_URL + url + } + } + + options.method = (options.method || 'get').toUpperCase() + + this._owner = owner + + this.options = { + headers: { + 'content-type': FORM_TYPES.form + }, + body: null, + cache: 'default', + signal: null, // 超时信号, 配置该项时, timeout不再生效 + timeout: 30000 // 超时时间, 单位毫秒, 默认30秒 + } + + if (!options.signal) { + this.control = new AbortController() + options.signal = this.control.signal + } + + if (options.headers) { + let headers = this.options.headers + Object.assign(headers, options.headers) + options.headers = headers + } + + Object.assign(this.options, options, { url }) + + if (owner._inject_req) { + owner._inject_req(this.options) + } + + return this.__next__() + } + + __next__() { + var options = this.options + var hasAttach = false // 是否有附件 + var noBody = NOBODY_METHODS.includes(options.method) + + /* -------------------------- 1»» 请求的内容 --------------------- */ + if (options.body) { + var type = typeof options.body + switch (type) { + case 'number': + case 'string': + this.__type__('text') + break + case 'object': + // 如果是一个 FormData对象,且为不允许携带body的方法,则直接改为POST + if (options.body.constructor === FormData) { + hasAttach = true + // 修正请求类型 + if (noBody) { + options.method = 'POST' + } + } else { + for (let k in options.body) { + if (toS.call(options.body[k]) === '[object File]') { + hasAttach = true + break + } + } + // 有附件,则改为FormData + if (hasAttach) { + if (noBody) { + options.method = 'POST' + } + options.body = Format.mkFormData(options.body) + } + } + break + } + } + if (hasAttach) { + delete options.headers['content-type'] + } + + /* -------------------------- 2»» 处理跨域 --------------------- */ + + /* ------------- 3»» 根据method类型, 处理表单数据 ---------------- */ + + // 拼接到url上 + if (noBody) { + let tmp = Format.param(options.body) + if (tmp) { + options.url += (~options.url.indexOf('?') ? '&' : '?') + tmp + } + delete options.body + } else { + if (!hasAttach) { + if (~options.headers['content-type'].indexOf('json')) { + options.body = JSON.stringify(options.body) + } else { + options.body = Format.param(options.body) + } + } + } + + /* ----------------- 4»» 超时处理 -----------------------*/ + if (options.timeout && options.timeout > 0) { + this.timer = setTimeout(_ => { + this.abort() + }, options.timeout) + + delete options.timeout + } + + /* ----------------- 5»» 构造请求 ------------------- */ + var url = options.url + delete options.url + for (let k in options) { + if ( + options[k] === null || + options[k] === undefined || + options[k] === '' + ) { + delete options[k] + } + } + return nativeFetch(url, options) + .then(r => { + clearTimeout(this.timer) + let isSucc = r.status >= 200 && r.status < 400 + let _type + if (this._owner._inject_res) { + r = this._owner._inject_res(r) + _type = toS.call(r) + } + if (isSucc) { + return r + } else { + if (_type === '[object Promise]') { + return r.then(_ => Promise.reject(_)) + } else { + return Promise.reject(r) + } + } + }) + .catch(e => { + clearTimeout(this.timer) + return Promise.reject(e) + }) + } + + abort() { + this.control.abort() + } + + __type__(type) { + this.options.headers['content-type'] = FORM_TYPES[type] + } +} + +function inject(target) { + target.inject = { + request(callback) { + target._inject_req = callback + }, + response(callback) { + target._inject_res = callback + } + } +} + +const fetch = function (url, options) { + return new _Request(url, options, fetch) +} + +fetch.create = function () { + var another = function (url, options) { + return new _Request(url, options, another) + } + inject(another) + return another +} + +inject(fetch) + +export default fetch diff --git a/src/lib/format.js b/src/lib/format.js new file mode 100644 index 0000000..f14facf --- /dev/null +++ b/src/lib/format.js @@ -0,0 +1,76 @@ +/** + * + * @authors yutent (yutent.io@gmail.com) + * @date 2016-11-26 16:35:45 + * + */ + +export const toS = Object.prototype.toString +export const encode = encodeURIComponent +export const decode = decodeURIComponent + +/** + * 表单序列化 + */ +function serialize(p, obj, query) { + var k + if (Array.isArray(obj)) { + obj.forEach(function (it, i) { + k = p ? `${p}[${Array.isArray(it) ? i : ''}]` : i + if (typeof it === 'object') { + serialize(k, it, query) + } else { + query(k, it) + } + }) + } else { + for (let i in obj) { + k = p ? `${p}[${i}]` : i + if (typeof obj[i] === 'object') { + serialize(k, obj[i], query) + } else { + query(k, obj[i]) + } + } + } +} + +export const Format = { + mkFormData(data) { + let form = new FormData() + for (let i in data) { + let el = data[i] + if (Array.isArray(el)) { + el.forEach(function (it) { + form.append(i + '[]', it) + }) + } else { + form.append(i, data[i]) + } + } + return form + }, + param(obj) { + if (!obj || typeof obj === 'string' || typeof obj === 'number') { + return obj + } + + let arr = [] + let query = function (k, v) { + if (/native code/.test(v)) { + return + } + + v = typeof v === 'function' ? v() : v + v = toS.call(v) === '[object File]' ? v : encode(v) + + arr.push(encode(k) + '=' + v) + } + + if (typeof obj === 'object') { + serialize('', obj, query) + } + + return arr.join('&') + } +}