0%

#vue config 配置

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
const path = require("path");
const HotHashWebpackPlugin = require("hot-hash-webpack-plugin");
const WebpackBar = require("webpackbar");
const resolve = (dir) => path.join(__dirname, ".", dir);

module.exports = {
productionSourceMap: false,
publicPath: "./",
outputDir: "dist",
assetsDir: "assets",
devServer: {
port: 9999,
host: "0.0.0.0",
https: false,
open: true,
},

chainWebpack: (config) => {
const types = ["vue-modules", "vue", "normal-modules", "normal"];
types.forEach((type) => {
let rule = config.module.rule("less").oneOf(type);
rule
.use("style-resource")
.loader("style-resources-loader")
.options({
patterns: [path.resolve(__dirname, "./lessVariates.less")],
});
});

config.resolve.alias
.set("@", resolve("src"))
.set("api", resolve("src/apis"))
.set("common", resolve("src/common"));

config.module
.rule("images")
.use("url-loader")
.tap((options) => ({
name: "./assets/images/[name].[ext]",
quality: 85,
limit: 0,
esModule: false,
}));

config.module
.rule("svg")
.test(/.svg$/)
.include.add(resolve("src/svg"))
.end()
.use("svg-sprite-loader")
.loader("svg-sprite-loader");

config.plugin("define").tap((args) => [
{
...args,
"window.isDefine": JSON.stringify(true),
},
]);

// 生产环境配置
if (process.env.NODE_ENV === "production") {
config.output.filename("./js/[name].[chunkhash:8].js");
config.output.chunkFilename("./js/[name].[chunkhash:8].js");
config.plugin("extract-css").tap((args) => [
{
filename: "css/[name].[contenthash:8].css",
chunkFilename: "css/[name].[contenthash:8].css",
},
]);
config
.plugin("hotHash")
.use(HotHashWebpackPlugin, [{ version: "1.0.0" }]);
config.plugin("webpackBar").use(WebpackBar);

config.optimization
.minimize(true)
.minimizer("terser")
.tap((args) => {
let { terserOptions } = args[0];
terserOptions.compress.drop_console = true;
terserOptions.compress.drop_debugger = true;
return args;
});
config.optimization.splitChunks({
cacheGroups: {
common: {
name: "common",
chunks: "all",
minSize: 1,
minChunks: 2,
priority: 1,
},
vendor: {
name: "chunk-libs",
chunks: "all",
test: /[\/]node_modules[\/]/,
priority: 10,
},
},
});
}
},
};

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<template>
<div id="app">
<!-- 上传组件 -->
<el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div>
</el-upload>

<!-- 进度显示 -->
<div class="progress-box">
<span>上传进度:{{ percent.toFixed() }}%</span>
<el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button>
</div>

<!-- 展示上传成功的视频 -->
<div v-if="videoUrl">
<video :src="videoUrl" controls />
</div>
</div>
</template>

<script>
import SparkMD5 from "spark-md5"
import axios from "axios"

export default {
name: 'App3',
filters: {
btnTextFilter(val) {
return val ? '暂停' : '继续'
}
},
data() {
return {
percent: 0,
videoUrl: '',
upload: true,
percentCount: 0
}
},
methods: {
async handleChange(file) {
if (!file) return
this.percent = 0
this.videoUrl = ''
// 获取文件并转成 ArrayBuffer 对象
const fileObj = file.raw
let buffer
try {
buffer = await this.fileToBuffer(fileObj)
} catch (e) {
console.log(e)
}

// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
const chunkSize = 2097152,
chunkList = [], // 保存所有切片的数组
chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名

// 根据文件内容生成 hash 值
const spark = new SparkMD5.ArrayBuffer()
spark.append(buffer)
const hash = spark.end()

// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
let curChunk = 0 // 切片时的初始位置
for (let i = 0; i < chunkListLength; i++) {
const item = {
chunk: fileObj.slice(curChunk, curChunk + chunkSize),
fileName: `${hash}_${i}.${suffix}` // 文件名规则按照 hash_1.jpg 命名
}
curChunk += chunkSize
chunkList.push(item)
}
this.chunkList = chunkList // sendRequest 要用到
this.hash = hash // sendRequest 要用到
this.sendRequest()
},

// 发送请求
sendRequest() {
const requestList = [] // 请求集合
this.chunkList.forEach((item, index) => {
const fn = () => {
const formData = new FormData()
formData.append('chunk', item.chunk)
formData.append('filename', item.fileName)
return axios({
url: '/single3',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
data: formData
}).then(res => {
if (res.data.code === 0) { // 成功
if (this.percentCount === 0) { // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
this.percentCount = 100 / this.chunkList.length
}
this.percent += this.percentCount // 改变进度
this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
}
})
}
requestList.push(fn)
})

let i = 0 // 记录发送的请求个数
// 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器
const complete = () => {
axios({
url: '/merge',
method: 'get',
params: { hash: this.hash }
}).then(res => {
if (res.data.code === 0) { // 请求发送成功
this.videoUrl = res.data.path
}
})
}
const send = async () => {
if (!this.upload) return
if (i >= requestList.length) {
// 发送完毕
complete()
return
}
await requestList[i]()
i++
send()
}
send() // 发送请求
},

// 按下暂停按钮
handleClickBtn() {
this.upload = !this.upload
// 如果不暂停则继续上传
if (this.upload) this.sendRequest()
},

// 将 File 对象转为 ArrayBuffer
fileToBuffer(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader()
fr.onload = e => {
resolve(e.target.result)
}
fr.readAsArrayBuffer(file)
fr.onerror = () => {
reject(new Error('转换文件格式发生错误'))
}
})
}
}
}
</script>

<style scoped>
.progress-box {
box-sizing: border-box;
width: 360px;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding: 8px 10px;
background-color: #ecf5ff;
font-size: 14px;
border-radius: 4px;
}
</style>

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 功能:基于数组的顺序栈
*
*/
public class ArrayStack {

private String[] items; // 数组
private int count; // 栈中元素个数
private int n; // 栈的大小

// 初始化数组,申请一个大小为 n 的数组空间
public ArrayStack(int n) {
this.items = new String[n];
this.n = n;
this.count = 0;
}

/**
* 功能:入栈
* 说明:数组入栈的入口为数组尾部
* @param item :入栈数据元素
* @return:是否入栈成功
*/
public boolean push(String item) {
// 数组空间不够了,直接返回 false,入栈失败。
if (count == n) return false;
// 将 item 放到下标为 count 的位置
items[count] = item;
//数组长度+1
++count;
//入栈成功
return true;
}

/**
* 功能:出栈
* @return:返回出栈元素
*/
public String pop() {
// 栈为空,则直接返回 null
if (count == 0) return null;
// 返回下标为 count-1 的数组元素
String tmp = items[count-1];
//数组长度-1
--count;
//返回出栈数据元素
return tmp;
}
}
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 功能:基本链表实现栈,入栈、出栈、输出栈
*
*/ [1,2,3] pop push
public class StackBasedLinkedList {
//定义栈顶指针
private Node top = null;

//定义栈结点
private static class Node {
//栈结点数据域
private int data;
//栈结点指针域
private Node next;
//构造函数
public Node(int data, Node next) {
this.data = data;
this.next = next;
}
//get 获取数据域方法
public int getData() {
return data;
}
}

/**
* 功能:入栈
* @param value:要入栈的数据元素
*/
public void push(int value) {
//创建一个栈结点
Node newNode = new Node(value, null);
// 判断栈是否为空
if (top == null) {
//如果栈为空,就将入栈的值作为栈的第一个元素
top = newNode;
} else {
//否则插入到top栈结点前(所谓的就是单链表的头插法)
newNode.next = top;
top = newNode;
}
}

/**
* 功能 : 出栈
* @return: -1 为栈中没有数据
*/
public int pop() {
// 如果栈的最顶层栈结点为null,栈为空
if (top == null) return -1;

//否则执行出栈操作,现将栈顶结点的数据元素赋值给 Value
int value = top.data;
//将 top 指针向下移动
top = top.next;
//返回出栈的值
return value;
}

/**
* 功能:输出栈中所有元素
*/
public void printAll() {
//将栈顶指针赋值给p
Node p = top;
//循环遍历栈(遍历单链表)
while (p != null) {
System.out.print(p.data + " ");
//指向下一个结点
p = p.next;
}
System.out.println();
}
}


token 刷新,维护队列,已有 token 后刷新

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import axios from "axios";
import { getToken, setToken, getRefreshToken } from "@utils/auth";

// 刷新 access_token 的接口
const refreshToken = () => {
return instance.post(
"/auth/refresh",
{ refresh_token: getRefreshToken() },
true
);
};

// 创建 axios 实例
const instance = axios.create({
baseURL: process.env.GATSBY_API_URL,
timeout: 30000,
headers: {
"Content-Type": "application/json",
},
});

let isRefreshing = false; // 标记是否正在刷新 token
let requests = []; // 存储待重发请求的数组
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (!error.response) {
return Promise.reject(error);
}
if (
error.response.status === 401 &&
!error.config.url.includes("/auth/refresh")
) {
const { config } = error;
if (!isRefreshing) {
isRefreshing = true;
return refreshToken()
.then((res) => {
const { access_token } = res.data;
setToken(access_token);
config.headers.Authorization = `Bearer ${access_token}`;
// token 刷新后将数组的方法重新执行
requests.forEach((cb) => cb(access_token));
requests = []; // 重新请求完清空
return instance(config);
})
.catch((err) => {
console.log("抱歉,您的登录状态已失效,请重新登录!");
return Promise.reject(err);
})
.finally(() => {
isRefreshing = false;
});
} else {
// 返回未执行 resolve 的 Promise
return new Promise((resolve) => {
// 用函数形式将 resolve 存入,等待刷新后再执行
requests.push((token) => {
config.headers.Authorization = `Bearer ${token}`;
resolve(instance(config));
});
});
}
}
return Promise.reject(error);
}
);

// 给请求头添加 access_token
const setHeaderToken = (isNeedToken) => {
const accessToken = isNeedToken ? getToken() : null;
if (isNeedToken) {
// api 请求需要携带 access_token
if (!accessToken) {
console.log("不存在 access_token 则跳转回登录页");
}
instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
}
};

// 有些 api 并不需要用户授权使用,则无需携带 access_token;默认不携带,需要传则设置第三个参数为 true
export const get = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken);
return instance({
method: "get",
url,
params,
});
};

export const post = (url, params = {}, isNeedToken = false) => {
setHeaderToken(isNeedToken);
return instance({
method: "post",
url,
data: params,
});
};

横向匹配,匹配的长度是不固定的

1
2
 abbc abbbc
/ab{2,5}c/

纵向匹配,具体到某一字符,也有多种可能

1
2
3
ab1c ab2c
/ab[1,2,3]c/

字符组

范围表示法

1
[1234][1 - 4];

排除字符组

[^ab] 除 ab 以外的任意字符

常见简写形式

\d [0-9] 数字
\D [^0-9] 除数字以外的字符
\w [a-z0-9A-Z_] 数字、大小写字母、下划线
\W 非单词字符
\s 空格
\S 非空格
. 任意字符

量词

{m,} 至少出现 m
{m} 出现 m
? {0,1} 出现或者不出现

  • {1,} 出现至少一次,加好追加,先得有一次
  • {0,} 出现一次或者任意次

贪婪匹配与惰性匹配

\d{2,5} 数字 2-5
123 12345 123

\d{2,5}? 虽然 2,5 但是有一次就行了
12, 34, 56

惰性匹配只需要给后面加个?

多选分支

a|b|c

abc

good|at 如果匹配 goodat 会匹配到 good,可见也是惰性的,匹配一次,后面就不再匹配

匹配颜色
#af0
#000000
/#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g

匹配时间
23:21
05:59
7:9
/^([01][09]|[2][0-3]):[0-5][0-9]/

/^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[0-5][0-9])/

匹配日期
1995-03-29

/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/g

window 操作路径
F:\study\javascript\regex\regular expression.pdf
F:\study\javascript\regex
F:\study\javascript
F:\

代表不能为空的文件名 _出现 0 或者任意次{0,} ?可有可无{0,1}
/^[a-zA-F]:\([^\:
<>|”?\r\n]+\)([^\:<>|”?\r\n]+)?$/

匹配id

/id=”.*?”/

/id=”(.?)”/
id=”[^”]

正则表达式匹配 匹配位置和匹配字符

/^|$/g

(?=p) (?!p)

(?=p) 表示p前面的位置
“hello”.replace(/(?=l)/g,”#”)

“he#l#lo”

(?!p)

(?!p) 表示非p前面的位置
“hello”.replace(/(?!p)/g,”#”)
#h#ell#o

位置的特性

1
2
"hello" = "" + "" + "hello"
"hello" /^^hello$$$/

千分位

12,345,678

/(?!^)(?=(\d{3})+$)/g

12345678 1234556 12,345,678 123,345

/(?!\b)(?=(\d{3})+\b)/g
\b 表示单词和空格之间的距离
不是\b前的位置 (?!\b) \B
/\B(?=(\d{3})+\b)/g

格式化

1800 $ 1,800.00

1
2
3
4
5
const format = (num) =>{
return num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g,',').replace(/^/,'$$ ')
}


密码

/(?=.*[0-9])^[a-zA-Z0-9]{6,12}$/ 是否包含数字0-9

/(?=.[0-9])(?=.[a-z])^[a-zA-Z0-9]{6,12}$/ 包含两种

/(?=.*[0-9])^/
表示开头前还有个位置,有任意多个字符,且有数字,简言之字符包含数字

?!p 不包含数字 不包含小写字母 也就是不能单独的存在
/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)^[a-zA-Z0-9]{6,12}$/

正则括号的作用

分组

(ab)+

分支结构匹配的所有可能性

(a|b|c)

分组引用

用括号包裹表示【存储到的分组数据】
/(\d{3})-(\d{2})/

【提取数据】
string.match(reg)

reg.exec(string)

$1-$9
RegExp.$1 构造函数的全局属性

替换

‘2021-03-29’.replace(/(\d{3})-(\d{2})-(\d{2})/,$2/$3/$1)
03/29/2021

反向引用

2017-06-12”
2017/06/12”
2017.06.12”
2016-06/12”
/\d{4}(-|/|.)\d{2}\1{d}2/

\1 表示前面(-|/|.)匹配到的字符,表示前后两者一致

分组后面有量词会匹配最后一次结果

12345
(\d)+ 5

非捕获括号

(?:p)

1
2
3
let str = 'I love js java node'
let str = 'I love node'
reg = /^ I love (?:js|node)/

trim 实现

1
2
3
一个是替换 另一个是提取
/^\s+|\s+$/g ''
/^\s*(.*?)\s*$/g $1

每个单词的首页字母大写

1
/(?:^|\s)\w/g $1.toUpperCase 

驼峰

1
2
3
-moz-magic
/[_-\s]+(.)?/g
(.) 首字母 ?字符尾巴可能不是单词字符 '-moz-magic '

逆驼峰化

MozTransform
-moz-transform

1
2
/([a-zA-Z])/g, '-$1' 
/[_-\s]+/g, '-'

转义

1
2
&lt;&gt;div&lt;
/\&([^;]+);/g.replace(match,key) &gt gt

匹配成对标签

regular expression /\<([^>]+)>/ /\<\/[^>]+>/ var regex = /<([^>]+)>[\d\D]*<\/\1>/; \d\D 数字或者不是数字 任意字符

回溯

// 贪婪12345 \d{1,3} 123 \d{1,3} 45

/“.“/ /“[^”]“/减少回溯

// 惰性 ? 尽可能少

// 分支 a|b|c

/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5]).){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/

// 简化
((…).){3} 三位数.三位数.

(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])

0{0,2}\d 匹配一位数,包括补齐0。 9 09 009 分别0、1 、2三次

0?\d{2} 匹配两位数,包括补齐0。00-99 000-099

1\d{2} 100-199

2[0-4]\d 200-249

25[0-5] 250-255

// 提取公共部分
/this|that/ /th(?is|at)/

// 减少分支数量
/red|read/ /rea?d/

var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = “2017-06-26”;
console.log( string.match(regex) );
// =>[“2017-06-26”, “2017”, “06”, “26”, index: 0, input: “2017-06-26”]

CreateElement

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
// 虚拟 DOM 结构
const element = {
type: "div", // 标签名
props: {
// 节点属性,包含 children
title: "foo", // title 属性
children: "hello", // 子节点,注:实际上这里应该是数组结构,帮助我们存储更多子节点
},
};

const createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : createTextElement(child)
),
},
};
};

const createTextElement = (text) => {
return {
type: "TYPE_TEXT",
props: {
nodeValue: text,
children: [],
},
};
};

Render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 虚拟Dom,添加到真实节点
function render(element, container) {
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
// 遍历所有子节点,并进行渲染
element.props.children.forEach((child) => render(child, dom));
const isProperty = (key) => key !== "children";
// 将element的props属性添加到dom
Object.keys(element.props)
.filter(isProperty)
.forEach((name) => {
dom[name] = element.props[name];
});
container.appendChild(dom);
}

并发模式 (requestIdleCallback)

fiber

虚拟 Dom

构建虚拟 Dom

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
console.log(
el("div", { id: "container" }, [
el("h1", { style: "color: red" }, ["simple virtal dom"]),
])
);

function Element(tagName, props = {}, children = []) {
// 标签名
this.tagName = tagName;
// 属性对象
this.props = props;
// 子节点
this.children = children;
// key标志
const { key = void 666 } = this.props;
this.key = key;

// 子节点数量
let count = 0;
this.children.forEach((child, index) => {
if (child instanceof Element) {
count += child.count;
}
count++;
});
this.count = count;
}

虚拟 Dom 转换为真实 Dom

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
Element.prototype.render = function () {
// 根据tag创建一个标签
const el = document.createElement(this.tagName);
const props = this.props;
// 遍历props给标签赋属性
for (const propName in props) {
const propValue = props[propName];
_.setAttr(el, propName, propValue);
}
// 遍历子结点
this.children.forEach((child) => {
let childEl;
// 如果子结点的原型指向El 继续创建标签 属性赋值
if (child instanceof Element) {
// 进行递归子结点 el("h1",{style:"color:red"})
childEl = child.render();
} else {
// 子结点非指向EL,就创建文本标签
childEl = document.createTextNode(child);
}
el.appendChild(childEl);
});

return el;
};

填充页面

1
2
3
4
5
document.body.appendChild(
el("div", { id: "container" }, [
el("h1", { style: "color: red" }, ["simple virtal dom"]),
]).render()
);

对比两棵树的差距

当新旧节点都是字符串类型时,直接替换

1
2
3
if (_.isString(oldNode) && _.isString(newNode)) {
currentNode.push({ type: patch.type, content: newNode });
}

新旧节点标签名和 key 相同,对比 porps 和 children

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// Diff props
var propsPatches = diffProps(oldNode, newNode);
if (propsPatches) {
currentPatch.push({ type: patch.PROPS, props: propsPatches });
}
// Diff children. If the node has a `ignore` property, do not diff children
if (!isIgnoreChildren(newNode)) {
diffChildren(
oldNode.children,
newNode.children,
index,
patches,
currentPatch
);
}
}

对真实 DOM 进行修改

据 diff 结果,对真实 DOM 进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function patch(node, patches) {
var walker = { index: 0 };
dfsWalk(node, walker, patches);
}

function dfsWalk(node, walker, patches) {
var currentPatches = patches[walker.index];

var len = node.childNodes ? node.childNodes.length : 0;
for (var i = 0; i < len; i++) {
var child = node.childNodes[i];
walker.index++;
dfsWalk(child, walker, patches);
}

if (currentPatches) {
applyPatches(node, currentPatches);
}
}

hooks

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
20个请求最多5
// 请求函数
function createTask(i) {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(i);
}, 2000);
});
};
}

class TaskQueue {
constructor() {
this.max = 5;
this.taskList = [];
setTimeout(() => {
this.run();
});
}
addTask(task) {
this.taskList.push(task);
}
run() {
let len = this.taskList.length;

if (!len) return false;

let min = Math.min(this.max, len);
for (let i = 0; i < min; i++) {
// 开始占用一个任务的空间
this.max--;
let task = this.taskList.shift();
task()
.then((res) => {
console.log("result:", res);
})
.catch((error) => {
throw new Error(error);
})
.finally(() => {
// 释放一个任务空间
this.max++;
this.run();
});
}
}
}

const taskQueue = new TaskQueue();
for (let i = 0; i < 20; i++) {
const task = createTask(i + 1);
taskQueue.addTask(task);
}

jsBridge

原理

IOS 有两个拦截方式一个是 UI 另一种是 WKWebview

拦截 URL 实现通信、

1
2
3
4
5
6
window.WKwebviewBridge = {
registerHandle:handleRegister,
callHandler:callHandler
}
const sendMessageQueue = []

jsCallBridge 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bridge.callHandler('Name',data,(data)=>{
console.log('js Receive data')
})
function callHandler(handlerName,data,responseCallBack){
if(arguments.length ===2 && typeof data === 'function'){
responseCallBack = data
data = null
}
doSend({handlerName,data},responseCallBack)
}
function doSend(message,responseCallback){
if(responseCallBack){
const callBackId = '__bridge'+new Date()
responseCallBack[]
}
}

// 调用上传功能
handleUpClick() {
this.uploadFinished // 维护一个上传状态,上传过程中禁用上传按钮
? this.$refs.input.click()
console.warn(‘请等待当前文件全部上传完成’);
},
handleFiles(e) {
const files = e?.target?.files;
this.readFiles(files);
},

// 上传之前将文件处理为对象
readFiles(files) {
if (!files || files.length <= 0) {
return;
}
for (const file of files) {
const url = window.URL.createObjectURL(file);
const obj = {
title: file.name.replace(/(.[^.]*$)|[_]/gi, ‘’), // 去掉文件后缀
url,
file,
fileType: file.fileType,
status: 0, // 状态 -> 0 等待中,1 完成, 2  正在上传,3 上传失败
percent: 0, // 上传进度
};
// 提前在 data 中定义 list,用来保存需要上传的文件
this.list.push(obj);
}
// 在 data 中定义 startIndex 初始值为 0,上传完成后更新,用于追加上传文件
this.startUpload(this.startIndex);
},

//  上传前需要校验文件
checkFile(index) {
const file = this.list[index];

// 如果文件不存在,即全部文件上传完成 
if (!file) {
this.uploadFinished = true;   // 上传完成,向父组件抛出 success 事件
this.$emit(‘success’, this.list);
// 清空上传控件中的值,保证 change 事件能正常触发
this.$refs.input.value = null; this.startIndex = index > 1 ? index - 1 : 0;
return false;
}

// 校验是否已上传
if (${file.status} === “1”) {
this.startUpload(++index);
return false;
}

// 校验文件大小
if (this.maxSize && file.file && file.file.size >= this.maxSize) {
this.startUpload(++index);
return false;
}

return true;
},// 上传单个文件
startUpload(index) {
if (!this.checkFile(index)) { return; }

// 开始上传,维护上传状态
this.uploadFinished = false;
const file = this.list[index].file;
const fileObj = this.list[index];

// 创建 formData 用于提交
const data = new FormData();
data.append(‘userfile’, file);
axios({
url: this.url, // 上传接口,由 props 传入
method: ‘post’,
data,
withCredentials: true,
cancelToken: this.source.token, // 用于取消接口请求
// 进度条
onUploadProgress: e => {
if (fileObj.status === 1) { return; } // 已上传
// 限定最大值为 99%
const p = parseInt((e.loaded / e.total) * 99);
if (e.total) {
fileObj.status = 2; // 正在上传
fileObj.percent = p; // 更新上传进度
} else {
fileObj.status = 3; // 上传失败
}
},
})
.then(response => {
if (${response.code} === “200”) {
fileObj.status = 1;
fileObj.percent = 100;
} else {
fileObj.status = 3;
}
})
.catch(e => {
fileObj.status = 3;
})
.finally(e => {
this.startUpload(++index);
});
// 上传完成
},

data: () => ({
list: [], // 已选择的文件对象
uploadFinished: true, // 上传状态
startIndex: 0, // 开始上传的下标,用于追加文件
source: axios.CancelToken.source(), // axios 取消请求
}),

reset() { // 重置
this.list = [];
this.source.cancel();
this.startIndex = 0;
this.uploadFinished = true;
this.$refs.input && (this.$refs.input.value = null);
},

、拖拽上传

bindEvents() {
const dropbox = this.$refs.pickerArea;
// 防止重复绑定事件,需要在 data 中初始化 bindDrop 为 false
if (!dropbox || this.bindDrop) { return }
// 绑定拖拽事件,在组件销毁时解绑
dropbox.addEventListener(“drop”, this.handleDrop, false);
dropbox.addEventListener(“dragleave”, this.handleDragLeave);
dropbox.addEventListener(“dragover”, this.handleDragOver);
this.bindDrop = true;
},
// 拖拽到上传区域
handleDragOver(e) {
e.stopPropagation();
e.preventDefault();
this.dragging = true;
},
// 离开上传区域
handleDragLeave(e) {
e.stopPropagation();
e.preventDefault();
this.dragging = false;
},
// 拖拽结束
handleDrop(e) {
e.stopPropagation();
e.preventDefault();
this.dragging = false;
const files = e.dataTransfer.files; // 调用 组件的上传功能
this.$refs.uploadBtn && this.$refs.uploadBtn.readFiles(files);
},

beforeDestroy() {
// 组件销毁前解绑拖拽事件
try {
const dropbox = this.$refs.pickerArea;
dropbox.removeEventListener(“drop”, this.handleDrop);
dropbox.removeEventListener(“dragleave”, this.handleDragLeave);
dropbox.removeEventListener(“dragover”, this.handleDragOver);
this.bindDrop = false;
} catch (e) {}
},