axios cancel token

axios cancel request

单个请求

通过CancelToken.source工厂函数创建 cancel token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

通过执行函数创建 cancel token

1
2
3
4
5
6
7
8
9
10
11
12
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});

// cancel the request
cancel();

在路由切换时 cancel request

配置全局变量 source

1
2
3
4
5
6
Vue.prototype.store = {
source: {
token: null,
cancel: null
}
};

配置 router

1
2
3
4
5
6
7
8
9
//router.js

router.beforeEach((to, from, next) => {
console.log(Vue.prototype.store.source);
const CancelToken = axios.CancelToken;
Vue.prototype.store.source.cancel && Vue.prototype.store.source.cancel();
Vue.prototype.store.source = CancelToken.source();
next();
});

配置 axios

1
2
3
4
5
6
7
const http = axios.create()
http.interceptors.request.use(config => {
config.cancelToken = store.source.token
return config
}, err => {
return Promise.reject(err)
})

Map && Set

Map

Methods

Map 是一个键值对的集合,与 Object 很像 ,但是 Map 允许任何数据类型作为键
主要的方法包括:

  • new Map() – 创建 map。
  • map.set(key, value) – 根据键(key)存储值(value)。
  • map.get(key) – 根据键返回值,如果 map 中该键不存在,返回 undefined
  • map.has(key) – 如果键存在,返回 true,否则返回 false
  • map.delete(key) – 移除该键的值。
  • map.clear() – 清空 map
  • map.size – 返回当前元素个数。
1
2
3
4
5
6
7
let map = new Map();
map.set(1,'num');
map.set('1','str');
map.set(true,'bool');
// Object 会将所有的键转化为字符串
map.get(1); // 'num'
map.get('1'); // 'str'

Map 还可以使用对象来作为键

1
2
3
4
let map = new Map();
let obj = {a:1};
map.set(obj,1);
map.get(obj); // 1

将 Object 转化为 Map

在创建 Map 时可以给构造函数传递一个[key,value]键值对数组。

1
let map = new Map([['1','str'],[1,'num']]);

有一个内建方法 Object.entries 可以返回对象的键值对数组。

1
2
3
4
let map = new Map(Object.entries({
name:'Jhon',
age:12
}));

这里 Object.entries 返回了[['name','Jhon'], ['age',12]]

遍历 Map

有三种方法可以循环遍历 map

  • map.keys() – 返回键的迭代器,
  • map.values() – 返回值的迭代器,
  • map.entries() – 返回 [key, value] 迭代器入口,for..of 循环会默认使用它。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// 迭代键(vegetables)
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}
// 迭代值(amounts)
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// 迭代键值对 [key, value]
for (let entry of recipeMap) { // 和 recipeMap.entries() 一样
alert(entry); // cucumber,500(等等)
}

The insertion order is used
和普通 Object 不同,迭代器的迭代顺序和值被插入的顺序一致,Map 会保留这个顺序。
另外,Map 有一个内建的 forEach 方法,和 Array 很像:

1
2
3
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 等等
});

Set

Set 是一个值的集合,集合中所有值只出现一次
主要方法包括:

  • new Set(iterable) – 创建 set,利用数组来创建是可选的(任何可迭代对象都可以)。
  • set.add(value) – 添加值,返回 set 自身。
  • set.delete(value) – 删除值,如果该 value 在调用方法的时候存在则返回 true ,否则返回 false
  • set.has(value) – 如果 set 中存在该值则返回 true ,否则返回 false
  • set.clear() – 清空 set。
  • set.size – 元素个数。

例如,我们有访客登门,我们希望记住所有人。但是重复来访者并不应该有两份记录。一个访客必须只记录一次。
Set 就恰好是可以做到这个的数据结构:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let set = new Set();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
// 访客,一些用户来了多次
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
// set 保证了值的唯一
alert( set.size ); // 3
for (let user of set) {
alert(user.name); // John(然后是 Pete 和 Mary)
}

Set 的替换方案是使用用户数组,每次插入新元素时使用 arr.find 方法检查用户编码是否重复。但是性能就会很差,因为这个方法会遍历整个数组,检查每个元素。而对于唯一性检查,Set 在内部优化得更好。

Set 迭代

我们可以使用 for..of 或者 forEach 来循环查看 set:

1
2
3
4
5
6
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) alert(value);
// 和 forEach 相同:
set.forEach((value, valueAgain, set) => {
alert(value);
});

注意到这里有个有趣得事情。forEach 函数用于 Set 时有三个参数:value,然后又一个 value,之后是目标对象。确实,相同值的 value 在参数中出现了两次。
这是为了兼容 Map,它在使用 forEach 方法时也包括三个参数。
适用于 Map 的迭代方法 set 也同样支持:

  • set.keys() – 返回 set 中值的迭代对象,
  • set.values() – 和 set.keys 一样,为了兼容 Map
  • set.entries() – 返回形如 [value, value] 的迭代对象,为了兼容 Map 而存在。

数组通过 Set 去重

1
let newArr = Array.from(new Set(oldArr));

JS的运行机制

1.为什么JS是单线程的?

JavsScript 的目的就是与用户交互,以及操作 DOM , 假如是多线程的,A线程添加了一个DOM节点,B线程删除了该DOM节点,这时浏览器应该以那个为准呢?
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2.任务队列 task queue

因为JS是单线程的,所以所有的任务都要在主线程里排队,一个一个执行。但是因为 I/O 设备很慢,CPU 很快,所以大多数时间 CPU 都是闲着的。因此 JavaScript 的设计者意识到,这时主线程可以不用管 I/O 设备,先挂起,执行后面的任务,等 I/O 设备返回了结果,再处理。

因此任务分为了两种,同步任务和异步任务。

2.1 同步任务

同步任务是指在主线程排队的任务,他们会按照顺序一个一个的执行。

2.2 异步任务

异步任务是指不在主线程,而是在任务队列(task queue)的任务,只有任务队列向主线程通知,某个异步任务可以执行了,该任务才会进入主线程执行。
异步的执行流程如下:

( 1 ) 所有同步任务都在主线程上执行,形成一个执行栈
( 2 ) 主线程之外还有一个任务队列。只要异步任务返回了结果,就在任务队列之中放置一个事件
( 3 ) 一旦执行栈中的同步任务执行完毕,系统就会读取任务队列,其中的异步任务会结束等待状态,进入执行栈,开始执行
( 4 ) 主线程不断重复以上三个过程

以下是主线程和任务队列的示意图
bg2014100801.jpg
只要主线程空了,就回去读取任务队列。
任务队列是“先进先出”的结构。

##

3.事件和回调函数

任务队列是事件的队列,I/O 设备每执行完一个任务,就会向任务队列中添加一个事件,主线程的执行栈为空时,就会去读取任务队列的事件。
除了 I/O 设备外,还有一些用户的事件(onClick,onScroll等),这些事件需要指定回调函数,进入任务队列,等待主线程读取。
所谓回调函数(callback),就是指被主线程挂起的代码,异步事件必须指定回调函数,主线程在任务队列读取并执行的就是回调函数。

4.Event Loop

主线程执行栈一空,就去读取任务队列,这是一个不断循环的过程,这种运行机制被称为 Event Loop(事件循环)。
示意图:
bg2014100802.png
主线程运行的时候产生堆(heap)和栈(stack),在栈中执行的同步任务会调用 WebAPI,他们会在任务队列中添加各种回调函数,执行栈清空后,主线程读取任务队列,依次执行队列中的回调函数。


因此执行栈中的同步函数,总是在读取任务队列之前执行。

举个例子

1
2
3
4
5
let xml = new XMlHttoRequest()
xml.open('GET',url)
xml.onload = function(){}
xml.onerror = function(){}
xml.send()
1
2
3
4
5
let xml = new XMlHttoRequest()
xml.open('GET',url)
xml.send()
xml.onload = function(){}
xml.onerror = function(){}

上面两组代码的区别在于 xml.send()的位置不同。但是执行起来是没有任何区别的。
因为xml.send()是异步方法,他会被放在任务队列,而指定回调的部分(onload,onerror)在执行栈中,所以两者的执行顺序是可以预见的。

5. setTimeout

setTimeout 的第一个参数是回调函数,第二个参数是 最小延迟时间。

setTimeout(func,0)的意思是指,在主线程空闲后,尽可能早的执行 func。因此 func 会在放入任务队列的尾部执行。

根据上面我们可以知道,必须等待主线程空闲后才可以执行 setTimeout(func,dely),假如主线程有一个任务需要很久才能完成,setTimeout 的执行间隔就不能保证了,因此 dely 是指 最小延迟时间 , 而不是固定延时时间。

使用 Vue Cli 3 打包组件并发布至npm

1.使用 Vue Cli 3 创建项目

vue create vue-test

2.修改目录并配置webpack

修改后的目录
image.png

  • src -> examples 用于放置示例文件
  • 增加 packages 文件夹 用于打包
  • 增加 lib 文件夹 用户放置打包后的文件
  • 增加 vue.config.js 文件 用于配置webpack

vue.config.js 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module.exports = {
// 修改 src 为 examples
pages: {
index: {
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
css: {
extract: false,
},
// 扩展 webpack 配置,使 packages 加入编译
chainWebpack: config => {
config.module
.rule('js')
.include
.add('/packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
// 修改它的选项...
return options;
});
}
};

3.packages 配置

packages/index.js
用于导出组件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 导入组件
import datePicker from './datepicker';

// 存储组件列表
const components = [
datePicker
];

// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
const install = function (Vue) {
// 判断是否安装
if (install.installed) return;
// 遍历注册全局组件
components.map(component => Vue.component(component.name, component));
};

// 判断是否是直接引入文件
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}

export default {
// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
install,
// 以下是具体的组件列表
datePicker
};

packages/datepicker/index.js

1
2
3
4
5
6
7
8
9
import datepicker from './src/date-picker';

// 为组件提供 install 安装方法,供按需引入
datepicker.install = function (Vue) {
Vue.component(datepicker.name, datepicker); //datepiacker.name 下面解释
};

// 默认导出组件
export default datepicker;
  • datepicker.name 为组件内的 name , 未来使用时,需要使用该 name 作为标签,因此不能随意写。

image.png

packages/src

里面包含组件的所有文件

4. package.json

在 scripts 中增加一行命令

1
"lib": "vue-cli-service build --target lib --name vue-simple-datepicker --dest lib packages/index.js",
  • –target lib 表明打包至 lib 文件夹
  • –name vue-simple-d aepicker 表明打包后的文件名
  • –dest lib 指定输出的目标文件

然后我们执行 yarn build
可以看到 lib 文件夹下出现了三个文件

image.png

demo.html 不用管,下面的两个文件才是我们想要的。
一个是 umd 的,一个是 common 的。
在前端内我们一般都用 umd 的,因此可以在 package.json 加上这么一条

1
"main": "lib/vue-simple-datepicker.umd.min.js",

表明入口文件是 lib/vue-simple-datepicker.umd.min.js
**

5. 发布至 npm

首先你要完善你的 package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"name": "vue-simple-datepicker-ln",
"version": "1.0.1",
"private": false, //一定要是 false
"main": "lib/vue-simple-datepicker.umd.min.js",
"repository": {
"type": "git",
"url": "git@github.com:Storm4542/vue-simple-datepicker.git"
},
"bugs": {
"url": "https://github.com/Storm4542/vue-simple-datepicker/issues"
},
"homepage": " https://storm4542.github.io/vue-simple-datepicker/",
"keywords": [
"datepicker",
"vue-datepicker"
],
"license": "MIT",
"scripts": {
"lib": "vue-cli-service build --target lib --name vue-simple-datepicker --dest lib packages/index.js",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"@vue/babel-preset-app": "^3.0.0",
"vue": "^2.6.6"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.5.3",
"@vue/cli-service": "^3.5.3",
"less": "^3.0.4",
"less-loader": "^4.1.0",
"vue-template-compiler": "^2.5.21"
}
}

登录 npm 。
yarn lib 后 npm publish 即可。

Cordova+Vue实现点击两次返回退出应用

Cordova + Vue 实现点击两次退出应用

注册事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//注意在 deviceready 后使用,写在methods中
//点击返回按键
onBackKeyDown() {
this.$toast('再点击一次退出应用');
document.removeEventListener("backbutton", this.onBackKeyDown, false); // 注销返回键
document.addEventListener("backbutton", this.exitApp, false);//绑定退出事件
setInterval(() => {
document.addEventListener("backbutton", this.onBackKeyDown, false);
document.removeEventListener("backbutton", this.exitApp, false);
}, 3000)
}
//关闭APP
exitApp() {
navigator.app.exitApp();
},

启动事件

1
2
3
4
5
created() {
document.addEventListener("backbutton", this.onBackKeyDown, false);
this.refreshTask();
this.refreshNotice();
}

销毁事件

1
2
3
4
beforeDestroy() {
document.removeEventListener("backbutton", this.onBackKeyDown, false); // 注销返回键
document.removeEventListener("backbutton", this.exitApp, false);
}

如果页面使用了<keep-alive>标签,那么销毁事件的时机为页面离开之前。

1
2
3
4
5
6
beforeRouteLeave(to, from, next) {
document.removeEventListener("backbutton", this.onBackKeyDown, false); // 注销返回键
document.removeEventListener("backbutton", this.exitApp, false);
this.$indicator.close()
next()
}

Cordova下载并预览功能

1.前言

近期混合应用开发需要下载和预览的功能,选择方案为先下载到本地,再使用cordova-plugin-file-opener2插件进行预览。

采用此预览方案文件会被先下载到本地,cordova-plugin-file-opener2插件其实可以直接打开网络地址来实现预览,采用此方式是基于以下考虑:

  1. 避免重复下载

  2. 避免有文件格式解析错误的情况,用户可以到本地再次进行查看

  3. 下载目录可控

2.框架

项目采用 Cordova + Vue + MintUI

3.操作

  • 下载
    • fileInfo提供下载地址、文件名称、文件格式(或扩展名)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function downLoad(fileInfo) {
Vue.$toast('正在下载,请稍等');
new FileTransfer().download(
encodeURI(FILES_HOST + "/" + fileInfo.fileid), //uri网络下载路径
cordova.file.dataDirectory + fileInfo.fileid, //文件本地存储路径
function (fileEntry) {
preView(fileEntry, fileInfo);
},
function (error) {
Vue.$toast('下载失败');
console.log(error);
},
false,
{
headers: {'Authorization': `Bearer ${localStorage.getItem('CFA0')}`},
}
);
}
  • 下载完成,预览文件
    • 使用 cordova-plugin-file-opener2 打开文件

    • mineType使用 mime-types获取,提供扩展名即可获取(若后端提供格式则不需要)。

    [地址]: https://github.com/jshttp/mime-types “Mime-types”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function preView(fileEntry, fileInfo) {
Vue.$toast('开始预览');
let url;
let platform = device.platform.toLowerCase();
if (platform === 'android') {
url = fileEntry.toInternalURL() //安卓预览路径
} else {
url = fileEntry.toURL() //ios 预览路径
}
console.log('路径', url);
cordova.plugins.fileOpener2.showOpenWithDialog(
url,
mime.lookup(fileInfo.fileType),
{
error: function (e) {
Vue.$toast('预览失败');
},
success: function () {
Vue.$toast('预览成功');
}
},
);
}

4. 可能遇到的坑

在预览文件的时候 cordova-plugin-file-opener2有可能会报以下错误:

Attempt to invoke virtual method ‘android.content.res.XmlResourceParser android.content.pm.ProviderInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)’ on a null object reference

寻找很久,应该是因为权限问题导致,解决办法如下:

AndroidManifest.xmlapplication标签内增加

1
2
3
4
5
6
7
<provider 
android:name="io.github.pwlin.cordova.plugins.fileopener2.FileProvider" android:authorities="${applicationId}.opener.provider"
android:exported="false" android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/opener_paths" />
</provider>

JS变量提升

1.var

1
2
3
console.log(a) //undefined

var a = 1;

输出 a 为 undefined

说明var在代码执行前就将 创建变量,并初始化为 undefined

2.let

1
2
3
console.log(a) //ReferenceError: a is not defined

let a = 1;

输出 ReferenceError: a is not defined,a 未定义

说明let在代码执行前 创建变量,并未初始化

3.function

1
2
3
console.log(a) // [Function:a]
a()//我是a
function a(){console.log('我是a')}

说明 funtuon 在代码执行前 创建、初始化并赋值

4.const

1
2
3
console.log(a) //ReferenceError: a is not defined

const a = 1;

const 与 let 相同,但是唯一区别是 const 只有 创建和初始化并没有赋值

JS引用类型

1.JS中的类型

  • 基本类型

    String

    Boolean

    Number

    undefined

    Null

    Symbol

  • 引用类型

    Object

对于基本类型,=代表的是值得拷贝,===代表的是值得比较。

对于引用类型,=代表的是引用地址的拷贝,===代表的是引用地址的比较。

2.几个例子深入理解

  • 案例一
1
2
3
4
5
let a = 1;
let b = a;
a === b // true
b = 2;
a === 1 ; //true

基本类型Number,赋值为拷贝,当 b改变的时候不会影响到a

  • 案例二
1
2
3
4
5
let a = {};
let b = {};
let c = a ;
a === b //false
a === c //true

引用类型Object,比较的是引用地址,ab的引用地址明显是不同的,所以他们不相等。

c得到了a的引用的地址,因此a===c

  • 案例三
1
2
3
4
5
let a = {name:'张三',info:{age:11,sex:'男'}};
let b = a;
b.info.age = 22;
a === b //true
a.info.age === 22 //true

因为ab指向同一个地址,所以b做修改的时候,a同样修改了。

  • 案例四
1
2
3
4
5
let a = {name:'张三',info:{age:11,sex:'男'}};
let b = a ; //展开运算符浅拷贝
a === b //false
a.name === b.name //true
a.info === b.info //true

因为展开运算符是浅拷贝,所以两个对象指向不同的地址,即a!==b

但是浅拷贝毕竟是浅拷贝。

name是基本类型,拷贝的值,所以相同。

info为引用类型,拷贝的地址,也相同。

  • 案例五
1
2
3
4
5
6
let a = {name:'张三',info:{age:11,sex:'男'}};
let b = JSON.parse(JSON.stringify) ; //深拷贝
a === b //false
a.name === b.name //true
a.info === b.info //false
a.info.age === b.info.age//true

深拷贝了,其他的不用说。

因为深拷贝过了,info指向的就是各自的地址了,没啥关系了。

但是基本类型该相同相同。

JS中的数组过滤和对象深拷贝

本文借鉴了该文章

原数组

1
2
3
4
5
6
7
let res = [
{name:'Anne', age: 23, gender:'female'},
{name:'Lisa', age: 16, gender:'female'},
{name:'Jay', age: 19, gender:'male'},
{name:'Jay', age: 22, gender:'male'},
{name:'Mark', age: 40, gender:'male'}
]

1.单个条件单数据筛选

1
2
3
4
function findOneByName(res, name) {
return res.filter(item => item.name === name) //注意:filter返回的是数组
}
console.log(findOneByName(res,'Anne')) // [ { name: 'Anne', age: 23, gender: 'female' } ]

2.单个条件多个数据筛选

有时候可能会遇到重名情况

使用 forEach,Mapfor遍历数组,push 进入一个临时数组返回。

1
2
3
4
5
6
7
8
9
10
11
12
function findSomeByName(res, name) {
let result = []
res.forEach(e => {
if (e.name === name) {
result.push(e)
}
});
return result
}
console.log(findSomeByName(res, 'Jay'))
//[ { name: 'Jay', age: 19, gender: 'male' },
// { name: 'Jay', age: 22, gender: 'male' } ]

3.多个条件单个数据筛选

数据中有两个 ‘ Jay ‘,我只想要那个 22 岁的。

1
2
3
4
5
function findOneByNameAge(res, name, age) {
return res.filter(item => item.name === name && item.age === age)
}
console.log(findOneByNameAge(res, 'Jay', 22));
//[ { name: 'Jay', age: 22, gender: 'male' } ]

4.多个条件多个筛选

我想找到所有叫 Jay 和 Anne 的人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function multiFilter(array, filters) {
const filterKeys = Object.keys(filters)
// filters all elements passing the criteria
return array.filter((item) => {
// dynamically validate all filter criteria
return filterKeys.every(key => {
//ignore when the filter is empty Anne
if (!filters[key].length) return true
return !!~filters[key].indexOf(item[key])
})
})
}

let filter = {
name: ['Jay', 'Anne'],
age: []
}
console.log(multiFilter(res, filter));

5. 判断是否有这个人,或者是否全部都为该人

1
2
3
4
// res 里有没有 Anne ?
console.log(res.some(item => item.name === 'Anne'));//true
// res 里是不是全都是 Anne ?
console.log(res.every(item => item.name === 'Anne'));//false

6.知识点1 : Object.keys() 获取数据索引或者对象属性

1
2
3
4
5
6
7
8
9
let arr = [1, 2, 3]
console.log(Object.keys(arr)); // [ '0', '1', '2' ]

let obj = {
a: '1',
b: '2',
c: '3'
}
console.log(Object.keys(obj));//[ 'a', 'b', 'c' ]

7. 对象的深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let newRes = Json.parse(Json.stringify(res)); 
//数据量不大的时候使用,而且必须遵循JSON格式,假如有 function 就无法拷贝

//递归深拷贝 o1:得到的新对象 , o2:原对象
let newRes = {} ; //想得到数组
let newRes = [] ; //想得到对象
function deepClone(o1, o2) {
for (let k in o2) {
if (typeof o2[k] === 'object') {
o1[k] = {};
deepClone(o1[k], o2[k]);
} else {
o1[k] = o2[k];
}
}
}
deepClone(newRes,res)

QS中文文档

只是简要的并且用自己的话翻译了一下,方便本人查阅。

引入 QS 库

1
yarn add qs
1
2
import qs from 'qs'
import assert from 'assert' //用来测试

使用

1.Parse Object
  • 基本使用
1
let obj = qs.parse('a=c'); // {a:'c'}
  • 是否有原型
1
2
let nullObject = qs.parse("a[hasOwnProperty]=b", { plainObjects: true }); //无原型链
var protoObject = qs.parse("a[hasOwnProperty]=b", {allowPrototypes: true});//有原型链
  • 嵌套生成
1
let obj = qs.parse("foo[bar]=baz"); //{foo:{bar:baz}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//嵌套的深度设置
var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } });
//最多只能到五层
var expected = {
a: {
b: {
c: {
d: {
e: {
f: {
'[g][h][i]': 'j'
}
}
}
}
}
}
};
var string = 'a[b][c][d][e][f][g][h][i]=j';
assert.deepEqual(qs.parse(string), expected);
  • 限制数量
1
2
var limited = qs.parse('a=b&c=d', { parameterLimit: 1 }); //限制为1
assert.deepEqual(limited, { a: 'b' });
  • 去除QueryFix
1
2
var prefixed = qs.parse('?a=b&c=d', { ignoreQueryPrefix: true });
assert.deepEqual(prefixed, { a: 'b', c: 'd' });
  • 设置判断分隔的符号
1
2
var delimited = qs.parse('a=b;c=d', { delimiter: ';' });// delimiter可以随意设置,比如 , { |
assert.deepEqual(delimited, { a: 'b', c: 'd' });
  • 使用正则判断分隔
1
2
var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' });
  • 允许 dots
1
2
var withDots = qs.parse('a.b=c', { allowDots: true });
assert.deepEqual(withDots, { a: { b: 'c' } });
2. Parse Array
  • 使用 [] 生成
1
2
var withArray = qs.parse('a[]=b&a[]=c');
assert.deepEqual(withArray, { a: ['b', 'c'] });
  • 可以指定 index(max=20)
1
2
var withIndexes = qs.parse('a[1]=c&a[0]=b');
assert.deepEqual(withIndexes, { a: ['b', 'c'] });

​ 当指定的 index 很大(小于20)的时候,qs 会自动压缩

1
2
var withIndexes = qs.parse('a[1]=c&a[15]=b');
assert.deepEqual(withIndexes, { a: ['b', 'c'] });
  • 允许空值
1
2
3
4
5
var withEmptyString = qs.parse('a[]=&a[]=b');
assert.deepEqual(withEmptyString, { a: ['', 'b'] });

var withIndexedEmptyString = qs.parse('a[0]=b&a[1]=&a[2]=c');
assert.deepEqual(withIndexedEmptyString, { a: ['b', '', 'c'] });
  • 当 index 大于20的时候 会变成对象
1
2
var withMaxIndex = qs.parse('a[100]=b');
assert.deepEqual(withMaxIndex, { a: { '100': 'b' } });

​ 当然这个 max 值你可以自己设定(0~20 ,超过20设不设置都一样了)

1
2
var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 });
assert.deepEqual(withArrayLimit, { a: { '1': 'b' } });

​ 你甚至可以直接不让它生成数组

1
2
var noParsingArrays = qs.parse('a[]=b', { parseArrays: false });
assert.deepEqual(noParsingArrays, { a: { '0': 'b' } });
  • 混合使用,返回的是Object
1
2
var mixedNotation = qs.parse('a[0]=b&a[b]=c');
assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } });
  • 生成 Object
1
2
var arraysOfObjects = qs.parse('a[][b]=c');
assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] });
3. Parse stringify
  • 基本使用,默认encode输出
1
2
3
qs.stringify(object, [options]);
assert.equal(qs.stringify({ a: 'b' }), 'a=b');
assert.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
  • 关闭 encode
1
2
var unencoded = qs.stringify({ a: { b: 'c' } }, { encode: false });
assert.equal(unencoded, 'a[b]=c');
  • 只对 value encode
1
2
3
4
5
var encodedValues = qs.stringify(
{ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
{ encodeValuesOnly: true }
);
assert.equal(encodedValues,'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h');
  • 自定义 encode
1
2
3
4
var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) {
// Passed in values `a`, `b`, `c`
return // Return encoded string
}})
  • 自定义 decode
1
2
3
4
var decoded = qs.parse('x=z', { decoder: function (str) {
// Passed in values `x`, `z`
return // Return decoded string
}})
  • 对 Array 使用 arrayFormat
1
2
3
4
5
6
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'
  • 对 Object 操作默认为 bracket notation
1
2
qs.stringify({ a: { b: { c: 'd', e: 'f' } } });
// 'a[b][c]=d&a[b][e]=f'
也可以设置为  dots notation
1
2
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true });
// 'a.b.c=d&a.b.e=f'
  • 空的字符串或者 null 的时候会省略值,但是等号(=)会保留
1
2
assert.equal(qs.stringify({ a: '' }), 'a=');
assert.equal(qs.stringify({ a: null }), 'a=');
  • 当一个key对应的值为空时(空数组,空对象),没有返回值
1
2
3
4
5
assert.equal(qs.stringify({ a: [] }), '');
assert.equal(qs.stringify({ a: {} }), '');
assert.equal(qs.stringify({ a: [{}] }), '');
assert.equal(qs.stringify({ a: { b: []} }), '');
assert.equal(qs.stringify({ a: { b: {}} }), '');
  • value 为 undefined 的时候也会被忽略
1
assert.equal(qs.stringify({ a: null, b: undefined }), 'a=');
  • 一个 query 字符串可以预添加 (?)
1
assert.equal(qs.stringify({ a: 'b', c: 'd' }, { addQueryPrefix: true }), '?a=b&c=d');
  • 设置分隔字符串
1
assert.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
  • 如果只想重写日期对象的序列化,则可以提供序列化选项
1
2
3
4
5
var date = new Date(7);
assert.equal(
qs.stringify({ a: date }, { serializeDate: function (d) { return d.getTime(); } }),
'a=7'
);
  • 可以使用 sort 选项,对 key 进行排序
1
2
3
4
function alphabeticalSort(a, b) {
return a.localeCompare(b);
}
assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y');
  • 通过对 key 进行 filter ,并对 value 操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function filterFunc(prefix, value) {
if (prefix == 'b') {
// Return an `undefined` value to omit a property.
return;
}
if (prefix == 'e[f]') {
return value.getTime();
}
if (prefix == 'e[g][0]') {
return value * 2;
}
return value;
}
qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc });
// 'a=b&c=d&e[f]=123&e[g][0]=4'
qs.stringify({ a: 'b', c: 'd', e: 'f' }, { filter: ['a', 'e'] });
// 'a=b&e=f'
qs.stringify({ a: ['b', 'c', 'd'], e: 'f' }, { filter: ['a', 0, 2] });
// 'a[0]=b&a[2]=d'
  • 对 null 的处理

在默认下,null 会被当做空字符串处理

1
2
var withNull = qs.stringify({ a: null, b: '' });
assert.equal(withNull, 'a=&b=');

Parse 不区分有没有 = 的参数,他们都会被解析为空字符串

1
2
var equalsInsensitive = qs.parse('a&b=');
assert.deepEqual(equalsInsensitive, { a: '', b: '' });

现在,为了区分 null 和 空字符串的区别,你可以加上 strictNullHandling,他会区分 null 和空字符串

1
2
var strictNull = qs.stringify({ a: null, b: '' }, { strictNullHandling: true });
assert.equal(strictNull, 'a&b=');
1
2
var parsedStrictNull = qs.parse('a&b=', { strictNullHandling: true });
assert.deepEqual(parsedStrictNull, { a: null, b: '' });

如果你想忽略 value === null的键值对,可以使用 skipNulls

1
2
var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true });
assert.equal(nullsSkipped, 'a=b');