一道面试题引发的思考

从上到下依次说出执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var foo = function () {
console.log("foo1");
};
foo();

var foo = function () {
console.log("foo2");
};
foo();

function foo() {
console.log("foo1");
}
foo();

function foo() {
console.log("foo2");
}
foo();

输出结果:foo1 foo2 foo2 foo2

分析

拆分函数表达式

函数表达式是将函数作为一个值赋给一个变量或属性

1
2
3
4
5
6
7
8
9
10
11
var foo = function () {
console.log("foo1");
};
foo();

var foo = function () {
console.log("foo2");
};
foo();

// foo1 foo2
  • foo 首先会变量提升,然后进行赋值为 function
  • 当执行第一个 foo 的时候,此时 foo 就是我们赋值的这个函数。
  • 接着执行第二个 foo 的赋值操作,由于函数作用域的特性,后面定义的函数将覆盖前面定义的函数。
  • 由于在调用函数之前就进行了函数的重新定义,所以在调用函数时,实际执行的是最后定义的那个函数
  • 打印:foo1、foo2

这种定义函数的方式,我们称为函数表达式

拓展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
foo();
var foo = function () {
console.log("foo1");
};

var foo = function () {
console.log("foo1");
};
foo();

var foo = function () {
console.log("foo2");
};
foo();

上面代码直接报错

  • 用 var 定义的变量会变量提升
  • 声明会被拿到函数或全局作用域的顶部,并且输出 undefined
  • 当执行 foo() 的时候,foo 还是 undefined,所以会报错
  • 由于 js 从按照顺序从上往下执行,所以当执行 foo = function(){} 的时候,才对 foo 进行赋值为一个函数。

拆分函数声明

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log("foo1");
}
foo();

function foo() {
console.log("foo2");
}
foo();

// foo2 foo2
  • 函数声明会在任何代码执行之前先被读取并添加到执行上下文,也就是函数声明提升。
  • 这里使用了函数声明定义了两个 foo 函数
  • 由于函数声明提升,第二个 foo 会覆盖第一个 foo
  • 当调用第一个 foo 的时候,其实已经被第二个 foo 覆盖了,所以这两个打印的都是 foo2。

当两段代码结合

  • 当开始解析的时候,函数声明就已经提升了
  • 第四个 foo 会覆盖第三个 foo
  • 然后 js 开始从上往下执行
  • 第一个赋值操作之后执行 foo()后,打印了 foo1
  • 第二个赋值之后执行 foo(),打印了”foo2”
  • 下面两个 foo 的执行其实是第二个赋值了的 foo,因为函数声明开始从刚开始就被提升了,而下面的赋值会覆盖 foo

总结

整体分析代码的执行过程

  1. 通过函数表达式定义变量 foo 并赋值为一个匿名函数,该函数在被调用时打印foo1
  2. 接着,通过函数表达式重新定义变量 foo,赋值为另一个匿名函数,该函数在被调用时打印foo2
  3. 使用函数声明定义了两个名为 foo 的函数。函数声明会在作用域中进行提升。后面的会覆盖前面的,由于声明从一开始就提升了,而又执行了两个赋值操作,所以此时 foo 是第二个赋值的函数。
  4. 然后调用 foo(),输出foo2
  5. 再调用 foo(),也输出foo2

函数表达式与函数声明的区别

  • 函数声明在代码解析阶段就会被提升(函数声明提升)
  • 函数表达式则需要在赋值语句执行到达时才会创建函数对象

概念

按位取反是指将一个二进制数的每一位都取反,即 0 变为 1,1 变为 0。在很多编程语言中,可以通过使用位运算符~来实现按位取反操作。

运算规则

  1. 将要取反的二进制数转换为二进制补码表示法。
  2. 对二进制补码表示法的每一位进行取反操作。
  3. 将取反后的二进制数转换回十进制表示法。

示例

对二进制数 10110101 按位取反,可以按照以下步骤进行操作:

  1. 将 10110101 转换为二进制补码表示法,因为它是一个有符号数,所以它的最高位为 1,表示负数。其补码为 01001011。
  2. 对补码的每一位进行取反操作,得到 10110100。
  3. 将取反后的二进制数转换回十进制表示法,得到 148。

因此,按位取反操作后,二进制数 10110101 变为了十进制数 148。

应用 indexOf

由于 -1 的取反操作等于 0,而其他数的取反操作不等于 0。所以用 indexOf 操作符可以用~操作来判断某个数组字符串里是否有某个字符。
取反之后不为零,说明 indexOf 的结果不等于-1,数组字符串里包含对应字符
取反之后为零,说明 indexOf 的结果等于-1,数组字符串里不包含对应字符

1
2
3
console.log(~-1); // 0
console.log(~1); // -2
console.log(~0); // -1

数组

1
2
3
4
5
6
let arr = [0, 1, 2, 3, 4, 5, 6];
if (~arr.indexOf(0)) {
console.log("arr里包含0"); // √
} else {
console.log("arr里不包含了0");
}

字符串

1
2
3
4
5
6
let str = "123456";
if (~str.indexOf(0)) {
console.log("str字符串里包含0");
} else {
console.log("str字符串里不包含0"); // √
}

文件传输是一个常见的需求。对于大文件的下载和上传,直接使用传统的方式可能会遇到性能和用户体验方面的问题。

幸运的是,前端技术提供了一些高效的解决方案:文件流操作和切片下载与上传。本文将深入探讨这些技术,帮助你理解它们的原理和实现方法,以优化文件传输效率和提升用户体验。

前端文件流操作

在前端开发中,文件流操作是指通过数据流的方式处理文件,对文件进行读取、写入和展示等操作。下面详细介绍了前端文件流操作的几个基本概念和技术。

数据流和文件处理的基本概念

数据流是指连续的数据序列,可以从一个源传输到另一个目的地。在前端开发中,文件可以被看作数据流的一种形式,可以通过数据流的方式进行处理。 文件处理涉及读取和写入文件的操作,包括读取文件的内容、写入数据到文件,以及对文件进行删除、重命名等操作。

Blob 对象和 ArrayBuffer:处理二进制数据

文件格式转换

在前端处理文件时,经常需要处理二进制数据。Blob(Binary Large Object)对象是用来表示二进制数据的一个接口,可以存储大量的二进制数据。Blob 对象可以通过构造函数进行创建,也可以通过其他 API 生成,例如通过 FormData 对象获取上传的文件。 而 ArrayBuffer 是 JavaScript 中的一个对象类型,用于表示一个通用的、固定长度的二进制数据缓冲区。我们可以通过 ArrayBuffer 来操作和处理文件的二进制数据。

使用 FileReader 进行文件读取

FileReader 是前端浏览器提供的一个 API,用于读取文件内容。通过 FileReader,我们可以通过异步方式读取文件,并将文件内容转换为可用的数据形式,比如文本数据或二进制数据。 FileReader 提供了一些读取文件的方法,例如 readAsText()、readAsArrayBuffer() 等,可以根据需要选择合适的方法来读取文件内容。

将文件流展示在前端页面中

一旦我们成功地读取了文件的内容,就可以将文件流展示在前端页面上。具体的展示方式取决于文件的类型。例如,对于文本文件,可以直接将其内容显示在页面的文本框或区域中;对于图片文件,可以使用 <img> 标签展示图片;对于音视频文件,可以使用 <video><audio> 标签来播放。 通过将文件流展示在前端页面上,我们可以实现在线预览和查看文件内容的功能。

好的,这一部分就基本介绍完毕,总结一下。前端文件操作流是处理大型文件的一种常见方式,他可以通过数据流的方式对文件进行操作。Blob 对象 和 ArrayBuffer 是处理二进制数据的重要工具。而 FileReader 则是读取文件内容的的关键组件。通过这些技术,我们可以方便的在前端页面上进行操作或者文件展示。

文件切片下载

这一步就进入到我们今天文章主题了,先来主要的看下流程

graph LR
A(开始) --> B{选择文件}
B -- 用户选择文件 --> C[切割文件为多个切片]
C --> D{上传切片}
D -- 上传完成 --> E[合并切片为完整文件]
E -- 文件合并完成 --> F(上传成功)
D -- 上传中断 --> G{保存上传进度}
G -- 上传恢复 --> D
G -- 取消上传 --> H(上传取消)

传统文件下载的性能问题

文件切片下载是一种提升文件下载效率的技术,通过将大文件分割成多个小片段(切片),并使用多个并发请求同时下载这些切片,从而加快整体下载速度。

传统的文件下载方式对于大文件来说存在性能问题。当用户请求下载一个大文件时,服务器需要将整个文件发送给客户端。这会导致以下几个问题:

  1. 较长的等待时间:大文件需要较长的时间来传输到客户端,用户需要等待很长时间才能开始使用文件。
  2. 网络阻塞:由于下载过程中占用了网络带宽,其他用户可能会遇到下载速度慢的问题。
  3. 断点续传困难:如果下载过程中出现网络故障或者用户中断下载,需要重新下载整个文件,无法继续之前的下载进度。

利用文件切片提升下载效率

文件切片下载通过将文件分割成多个小片段,每个片段大小通常在几百 KB 到几 MB 之间。然后客户端通过多个并发请求同时下载这些片段。这样做的好处是:

  1. 快速启动:客户端可以快速开始下载,因为只需要下载第一个切片即可。
  2. 并发下载:通过使用多个并发请求下载切片,可以充分利用带宽,并提高整体下载速度。
  3. 断点续传:如果下载中断,客户端只需要重新下载中断的切片,而不需要重新下载整个文件。

切片上传代码示例:

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
const [selectedFile, setSelectedFile] = useState(null);
const [progress, setProgress] = useState(0);

// 处理文件选择事件
function handleFileChange(event) {
setSelectedFile(event.target.files[0]);
}

// 处理文件上传事件
function handleFileUpload() {
if (selectedFile) {
// 计算切片数量和每个切片的大小
const fileSize = selectedFile.size;
const chunkSize = 1024 * 1024; // 设置切片大小为1MB
const totalChunks = Math.ceil(fileSize / chunkSize);

// 创建FormData对象,并添加文件信息
const formData = new FormData();
formData.append("file", selectedFile);
formData.append("totalChunks", totalChunks);

// 循环上传切片
for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
const start = chunkNumber * chunkSize;
const end = Math.min(start + chunkSize, fileSize);
const chunk = selectedFile.slice(start, end);
formData.append(`chunk-${chunkNumber}`, chunk, selectedFile.name);
}

// 发起文件上传请求
axios
.post("/upload", formData, {
onUploadProgress: (progressEvent) => {
const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
setProgress(progress);
}
})
.then((response) => {
console.log("文件上传成功:", response.data);
})
.catch((error) => {
console.error("文件上传失败:", error);
});
}
}

当涉及到切片上传和下载时,前端使用的技术通常是基于前端库或框架提供的文件处理功能,结合后端服务实现。

上面代码里我们提到了文件如何切片上传。

  • 当用户选择文件后,通过 handleFileChange 函数处理文件选择事件,将选择的文件保存在 selectedFile 状态中。
  • 当用户点击上传按钮时,通过 handleFileUpload 函数处理文件上传事件。
  • handleFileUpload 函数中,计算切片数量和每个切片的大小,并创建一个 FormData 对象用于存储文件信息和切片数据。

实现客户端切片下载的方案

实现客户端切片下载的基本方案如下:

  • 服务器端将大文件切割成多个切片,并为每个切片生成唯一的标识符。
  • 客户端发送请求获取切片列表,同时开始下载第一个切片。
  • 客户端在下载过程中,根据切片列表发起并发请求下载其他切片,并逐渐拼接合并下载的数据。
  • 当所有切片都下载完成后,客户端将下载的数据合并为完整的文件。

代码示例:

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
function downloadFile() {
// 发起文件下载请求
fetch("/download", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
})
.then((response) => response.json())
.then((data) => {
const totalSize = data.totalSize;
const totalChunks = data.totalChunks;

let downloadedChunks = 0;
let chunks = [];

// 下载每个切片
for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
fetch(`/download/${chunkNumber}`, {
method: "GET"
})
.then((response) => response.blob())
.then((chunk) => {
downloadedChunks++;
chunks.push(chunk);

// 当所有切片都下载完成时
if (downloadedChunks === totalChunks) {
// 合并切片
const mergedBlob = new Blob(chunks);

// 创建对象 URL,生成下载链接
const downloadUrl = window.URL.createObjectURL(mergedBlob);

// 创建 <a> 元素并设置属性
const link = document.createElement("a");
link.href = downloadUrl;
link.setAttribute("download", "file.txt");

// 模拟点击下载
link.click();

// 释放资源
window.URL.revokeObjectURL(downloadUrl);
}
});
}
})
.catch((error) => {
console.error("文件下载失败:", error);
});
}

首先使用 BLOB 对象创建一共对象 URL,用于生成下载连接,然后创建 a 标签并且设置 href 的属性为刚刚创建的对象 URL,继续设置 a 标签的 download 属性是文件名,方便点击的时候自动下载文件。

显示下载进度和完成状态

为了显示下载进度和完成状态,可以在客户端实现以下功能:

  1. 显示进度条:客户端可以通过监听每个切片的下载进度来计算整体下载进度,并实时更新进度条的显示。
  2. 显示完成状态:当所有切片都下载完成后,客户端可以显示下载完成的状态,例如显示一个完成的图标或者文本。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 处理文件下载事件
function handleFileDownload() {
axios
.get("/download", {
responseType: "blob",
onDownloadProgress: (progressEvent) => {
const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
setProgress(progress);
}
})
.then((response) => {
// 创建一个临时的URL对象用于下载
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "file.txt");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch((error) => {
console.error("文件下载失败:", error);
});
}
1
<button onClick="{handleFileDownload}">下载文件</button> <div>进度:{progress}%</div>
  • 当用户点击下载按钮时,通过 handleFileDownload 函数处理文件下载事件。
  • handleFileDownload 函数中,使用 axios 库发起文件下载请求,并设置 responseType: 'blob' 表示返回二进制数据。
  • 通过监听 onDownloadProgress 属性获取下载进度,并更新进度条的显示。
  • 下载完成后,创建一个临时的 URL 对象用于下载,并通过动态创建 <a> 元素模拟点击下载。

大文件上传的问题与解决方案

传统的文件上传方式存在的问题

  • 大文件上传耗时长,容易导致请求超时。
  • 占用服务器和网络带宽资源,可能影响其他用户的访问速度。
  • 如果上传中断,需要重新上传整个文件,效率低下。
  • 难以实现上传进度的显示和控制。

前端文件切片上传的优势

  • 将大文件分割为更小的文件切片,分多次上传,提高上传效率和稳定性。
  • 提供上传进度的监控和展示,提高用户体验。
  • 充分利用浏览器的并发上传能力,减轻服务器负担。
  • 实现断点续传功能,避免重复上传已上传的部分。

实现前端切片上传的方法

文件切割

使用 JavaScript 的 File API 获取文件对象,并使用 Blob.prototype.slice() 方法将文件切割为多个切片。

  • 使用 FormData 对象将切片数据通过 AJAX 或 Fetch API 发送到服务器。
  • 在后端服务器上接收切片并保存到临时存储中,等待后续合并。
  • 在客户端通过监听上传进度事件,在进度条或提示中展示上传进度。

代码示例:

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
const [file, setFile] = useState(null); //用来存放我本地上传的文件

const chunkSize = 1024 * 1024; // 1MB 切片大小

const upload = () => {
if (!file) {
alert("请选择要上传的文件!");
return;
}

const chunkSize = 1024 * 1024; // 1MB

let start = 0;
let end = Math.min(chunkSize, file.size);

while (start < file.size) {
const chunk = file.slice(start, end);

// 创建FormData对象
const formData = new FormData();
formData.append("file", chunk);

// 发送切片到服务器
fetch("上传接口xxxx", {
method: "POST",
body: formData
})
.then((response) => response.json())
.then((data) => {
console.log(data);
// 处理响应结果
})
.catch((error) => {
console.error(error);
// 处理错误
});

start = end;
end = Math.min(start + chunkSize, file.size);
}
};

return (
<div>
<input type="file" onChange={handleFileChange} />
<button onClick={upload}>上传</button>
</div>
);

在上面的代码中,创建了一个名为 Upload 的函数组件。它使用了 React 的 useState 钩子来管理选中的文件。

通过 onChange 事件监听文件输入框的变化,并在 handleFileChange 函数中获取选择的文件,并更新 file 状态。

点击“上传”按钮时,调用 upload 函数。它与之前的示例代码类似,将文件切割为多个大小相等的切片,并使用 FormData 对象和 fetch 函数发送切片数据到服务器。

实现断点续传的技术:记录和恢复上传状态

  • 在前端,可以使用 localStorage 或 sessionStorage 来存储已上传的切片信息,包括已上传的切片索引、切片大小等。

  • 每次上传前,先检查本地存储中是否存在已上传的切片信息,若存在,则从断点处继续上传。

  • 在后端,可以使用一个临时文件夹或数据库来记录已接收到的切片信息,包括已上传的切片索引、切片大小等。

  • 在上传完成前,保存上传状态,以便在上传中断后能够恢复上传进度。

代码示例:

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
import React, { useState, useRef, useEffect } from "react";

function Upload() {
const [file, setFile] = useState(null);
const [uploadedChunks, setUploadedChunks] = useState([]);
const [uploading, setUploading] = useState(false);
const uploadRequestRef = useRef();

const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
setFile(selectedFile);
};

const uploadChunk = (chunk) => {
// 创建FormData对象
const formData = new FormData();
formData.append("file", chunk);

// 发送切片到服务器
return fetch("your-upload-url", {
method: "POST",
body: formData
})
.then((response) => response.json())
.then((data) => {
console.log(data);
// 处理响应结果
return data;
});
};

const upload = async () => {
if (!file) {
alert("请选择要上传的文件!");
return;
}

const chunkSize = 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);

let start = 0;
let end = Math.min(chunkSize, file.size);

setUploading(true);

for (let i = 0; i < totalChunks; i++) {
const chunk = file.slice(start, end);
const uploadedChunkIndex = uploadedChunks.indexOf(i);

if (uploadedChunkIndex === -1) {
try {
const response = await uploadChunk(chunk);
setUploadedChunks((prevChunks) => [...prevChunks, i]);

// 保存已上传的切片信息到本地存储
localStorage.setItem("uploadedChunks", JSON.stringify(uploadedChunks));
} catch (error) {
console.error(error);
// 处理错误
}
}

start = end;
end = Math.min(start + chunkSize, file.size);
}

setUploading(false);

// 上传完毕,清除本地存储的切片信息
localStorage.removeItem("uploadedChunks");
};

useEffect(() => {
const storedUploadedChunks = localStorage.getItem("uploadedChunks");

if (storedUploadedChunks) {
setUploadedChunks(JSON.parse(storedUploadedChunks));
}
}, []);

return (
<div>
<input type="file" onChange={handleFileChange} />
<button onClick={upload} disabled={uploading}>
{uploading ? "上传中..." : "上传"}
</button>
</div>
);
}

首先,使用 useState 钩子创建了一个 uploadedChunks 状态来保存已上传的切片索引数组。初始值为空数组。

然后,我们使用 useRef 钩子创建了一个 uploadRequestRef 引用,用于存储当前的上传请求。

handleFileChange 函数中,我们更新了 file 状态以选择要上传的文件。

uploadChunk 函数中,我们发送切片到服务器,并返回一个 Promise 对象来处理响应结果。

upload 函数中,我们添加了断点续传的逻辑。首先,我们获取切片的总数,并设置 uploading 状态为 true 来禁用上传按钮。

然后,我们使用 for 循环遍历所有切片。对于每个切片,我们检查 uploadedChunks 数组中是否已经包含该索引,如果不包含,则进行上传操作。

在上传切片之后,我们将已上传的切片索引添加到 uploadedChunks 数组,并使用 localStorage 保存已上传的切片信息。

最后,在上传完毕后,我们将 uploading 状态设为 false,并清除本地存储的切片信息。

在实现大文件上传时要考虑服务器端的处理能力和存储空间,以及安全性问题。同时,为了保障断点续传的准确性,应该尽量避免并发上传相同文件的情况,可以采用文件唯一标识符或用户会话标识符进行区分。

优化用户体验:切片下载与上传的应用场景

后台管理系统中的文件下载和上传

  • 文件下载:在后台管理系统中,用户可能需要下载大型文件,如报表、日志文件、数据库备份等。通过将文件切片下载,可以提高下载速度和稳定性,同时允许用户中断下载并从中断处继续下载。

  • 文件上传:后台管理系统中,用户可能需要上传大型文件,如数据导入、文件备份等。使用切片上传可以提高上传效率,分批上传文件切片,并显示上传进度,使用户能够了解上传的状态。

图片/视频上传和预览

  • 图片上传和预览:在图片上传场景中,用户可以选择多张图片进行上传。通过切片上传,可以加快图片上传速度,并实时显示上传进度。同时,在上传完成后,可以提供预览功能,让用户可以立即查看上传的图片。

  • 视频上传和预览:对于较大的视频文件,切片上传可以确保上传过程可靠且高效。同时,可以实现上传进度的实时展示。上传完成后,通过切片下载技术,用户可以流畅地观看视频,无需等待整个文件下载完成。

云存储和云盘应用中的文件操作

  • 文件分块上传:云存储和云盘应用通常需要处理大量文件的上传。通过切片上传可以提高上传速度和稳定性,并允许用户中断并继续上传。

  • 文件分块下载:当用户需要下载云存储或云盘中的大型文件时,可以使用切片下载技术,加快下载速度并提供中断恢复功能。

  • 文件预览和在线编辑:通过将文件切片并进行预览,在线编辑,可以提供更好的用户体验。用户可以在不需完全下载文件的情况下,直接预览和编辑文件。

保存图片到系统相册

UNI

uni.saveImageToPhotosAlbum

语法

1
uni.saveImageToPhotosAlbum(OBJECT);

平台差异说明

AppH5微信小程序支付宝小程序百度小程序抖音小程序、飞书小程序QQ 小程序快手小程序京东小程序
x

OBJECT 参数说明

参数名类型必填说明
filePathString图片文件路径,可以是临时文件路径也可以是永久文件路径,不支持网络图片路径
successFunction接口调用成功的回调函数
failFunction接口调用失败的回调函数
completeFunction接口调用结束的回调函数(调用成功、失败都会执行)

success 返回参数说明

参数名类型说明
pathString保存到相册的图片路径,仅 App 3.0.5+ 支持
errMsgString调用结果

示例

1
2
3
4
5
6
7
8
9
10
11
12
uni.chooseImage({
count: 1,
sourceType: ["camera"],
success: function (res) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePaths[0],
success: function () {
console.log("save success");
}
});
}
});

PLUS

H5 端可以使用 plus api 来将图片保存到相册
plus.gallery.save

语法

1
plus.gallery.save(path, successCB, errorCB);

条件编译调用 HTML5+

在 uni-app 调用 HTML5+ 的扩展规范时,需要注意使用条件编译。否则运行到 h5、小程序等平台会出现 plus is not defined错误。

1
2
3
4
// #ifdef APP-PLUS
var appid = plus.runtime.appid;
console.log("应用的 appid 为:" + appid);
// #endif

说明

保存文件到系统相册中。 每次仅能保存一个文件,支持图片文件(jpg/jpeg、png、bmp 等格式)和视频文件(3gp、mov 等格式)。 若保存的文件类型当前系统不支持,则通过 errorCB 回调返回错误信息。

参数说明

参数名类型必填说明
pathString要保存到系统相册中的文件文件地址
succesCBFunction保存文件到系统相册中成功的回调函数
errorCBFunction保存文件到系统相册中失败的回调函数

平台支持

Android - 2.2+ (支持)
iOS - 4.3+ (支持)

示例

1
2
3
4
5
6
// 保存图片到相册中
function savePicture() {
plus.gallery.save("_doc/a.jpg", function () {
alert("保存图片到相册成功");
});
}

从相册中选择图片或视频文件

UNI

选择图片

从本地相册选择图片或使用相机拍照
uni.chooseImage

1
uni.chooseImage(OBJECT);

参数说明

OBJECT 参数说明

参数名类型必填说明平台差异说明
countNumber最多可以选择的图片张数,默认 9见下方说明
sizeTypeArrayoriginal 原图,compressed 压缩图,默认二者都有App、微信小程序、支付宝小程序、百度小程序
extensionArray根据文件拓展名过滤,每一项都不能是空字符串。默认不过滤。H5(HBuilder X2.9.9+)
sourceTypeArrayalbum 从相册选图,camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项
cropObject图像裁剪参数,设置后 sizeType 失效App 3.1.19+
successFunction成功则返回图片的本地文件路径列表 tempFilePaths
failFunction接口调用失败的回调函数小程序、App
completeFunction接口调用结束的回调函数(调用成功、失败都会执行)

crop 参数说明

参数名类型必填说明
qualityNumber取值范围为 1-100,数值越小,质量越低(仅对 jpg 格式有效)。默认值为 80。
widthNumber裁剪的宽度,单位为 px,用于计算裁剪宽高比。
heightNumber裁剪的高度,单位为 px,用于计算裁剪宽高比。
resizeBoolean是否将 width 和 height 作为裁剪保存图片真实的像素值。默认值为 true。注:设置为 false 时在裁剪编辑界面显示图片的像素值,设置为 true 时不显示

success 返回参数说明

参数类型说明
tempFilePathsArray图片的本地文件路径列表
tempFilesArray、Array图片的本地文件列表,每一项是一个 File 对象

File 对象结构如下

参数类型说明
pathString本地文件路径
sizeNumber本地文件大小,单位:B
nameString包含扩展名的文件名称,仅 H5 支持
typeString文件类型,仅 H5 支持

示例

1
2
3
4
5
6
7
8
uni.chooseImage({
count: 6, //默认9
sizeType: ["original", "compressed"], //可以指定是原图还是压缩图,默认二者都有
sourceType: ["album"], //从相册选择
success(res) {
console.log(res.tempFilePaths);
}
});

选择视频

拍摄视频或从手机相册中选视频,返回视频的临时文件路径
uni.chooseVideo

1
uni.chooseVideo(OBJECT);

参数说明

OBJECT 参数说明

参数名类型必填说明平台差异说明
sourceTypeArrayalbum 从相册选视频,camera 使用相机拍摄,默认为:[‘album’, ‘camera’]
extensionArray根据文件拓展名过滤,每一项都不能是空字符串。默认不过滤。H5(HBuilder X 2.9.9+)
compressedBoolean是否压缩所选的视频源文件,默认值为 true,需要压缩。微信小程序、百度小程序、抖音小程序、飞书小程序、京东小程序、App(HBuilder X 3.2.7+)
maxDurationNumber拍摄视频最长拍摄时间,单位秒。最长支持 60 秒。APP 平台 1.9.7+(iOS 支持,Android 取决于 ROM 的拍照组件是否实现此功能,如果没实现此功能则忽略此属性。) 微信小程序、百度小程序、京东小程序
cameraString‘front’、’back’,默认’back’APP、微信小程序、京东小程序
successFunction接口调用成功,返回视频文件的临时文件路径,详见返回参数说明。
failFunction接口调用失败的回调函数
completeFunction接口调用结束的回调函数(调用成功、失败都会执行)

success 返回参数说明

参数类型说明平台差异
tempFilePathString选定视频的临时文件路径
tempFileFile选定的视频文件仅 H5(2.6.15+)支持
durationNumber选定视频的时间长度,单位为 sAPP 2.1.0+、H5、微信小程序、京东小程序
sizeNumber选定视频的数据量大小APP 2.1.0+、H5、微信小程序、京东小程序
heightNumber返回选定视频的高APP 2.1.0+、H5、微信小程序、京东小程序
widthNumber返回选定视频的宽APP 2.1.0+、H5、微信小程序、京东小程序
nameString包含扩展名的文件名称仅 H5 支持

PLUS

plus.gallery.pick

语法

1
plus.gallery.pick(successCB, errorCB, options);

参数说明

参数名类型必填说明
optionsGalleryOptions设置选择文件的参数
succesCBFunction保存文件到系统相册中成功的回调函数
errorCBFunction保存文件到系统相册中失败的回调函数
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
interface GalleryOptions {
// 是否显示系统相册文件选择界面的动画
animation: Boolean;
// 确认按钮文字
confirmText: String;
// 是否支持编辑图片
crop: GalleryCropStyles;
// 选择文件保存的路径
editable: Boolean;
// 最多选择的图片数量
filename: String;
// 是否支持多选图片
filter: GalleryFilter;
// 是否检测权限
maximum: Number;
// 已选择的图片路径列表
multiple: Boolean;
// 是否使用系统相册文件选择界面
permissionAlert: Boolean;
// 超过最多选择图片数量事件
popover: PopPosition;
// 配置裁剪图片
selected: Array<String>;
// 相册中选择文件类型过滤器
system: Boolean;
// 相册选择界面弹出指示区域
onmaxed: Function;
}

interface GalleryCropStyles {
// 裁剪后保存图片的质量:取值范围为1-100,数值越小,质量越低(仅对jpg格式有效)。 默认值为80。
quality: Number;
// 裁剪的宽度: 单位为px,用于计算裁剪宽高比。 必须设置此值。
width: Number;
// 裁剪的高度: 单位为px,用于计算裁剪宽高比。 必须设置此值。
height: Number;
// 是否将图片保存为指定的宽高像素: true表示将width和height作为裁剪保存图片的像素值,false表示使用图片编辑操作的真实像素值。
// 默认值为true。设置为false时在裁剪编辑界面显示图片的像素值,设置为true时不显示。
resize: Boolean;
// 裁剪后的图片是否保存到相册中: true表示将裁剪后的图片保存到相册中,false表示不将裁剪后的图片保存到相册中。HBuilder3.3.0+版本支持。
// 默认值为false。
saveToAlbum: Boolean;
}

type GalleryFilter =
| "image" /* 仅可选择图片文件 */
| "video" /* 仅可选择视频文件 */
| "none"; /* 不过滤,可选择图片或视频文件 */

interface PopPosition {
// 指示区域距离容器顶部的距离: 弹出拍照或摄像窗口指示区域距离容器顶部的距离,单位支持像素值(如"100px")和百分比(如"50%"),如不写单位则为像素值值。
top: String;
// 指示区域距离容器左侧的距离: 弹出拍照或摄像窗口指示区域距离容器左侧的距离,单位支持像素值(如"100px")和百分比(如"50%"),如不写单位则为像素值。
left: String;
// 指示区域的宽度: 弹出拍照或摄像窗口指示区域的宽度,单位支持像素值(如"100px")和百分比(如"50%"),如不写单位则为像素值。
width: String;
// 指示区域的高度: 弹出拍照或摄像窗口指示区域的高度,单位支持像素值(如"100px")和百分比(如"50%"),如不写单位则为像素值。
height: String;
}

平台支持

Android - 2.2+ (支持)
iOS - 5.1+ (支持)

示例

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
export function galleryF(list) {
if (!list.length) return;

const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i;
const VIDEO_REGEXP =
/\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i;

return list.map((url) => {
return {
url,
type: IMAGE_REGEXP.test(url)
? "image"
: VIDEO_REGEXP.test(url)
? "video"
: "",
name: url.slice(url.lastIndexOf("/") + 1)
};
});
}

export function gallery(options = {}) {
const { multiple = true, maximum = 9, filter = "none" } = options;

return new Promise((resolve, reject) => {
plus.gallery.pick(
(path) => {
resolve(galleryF(path.files));
},
(error = "取消选择") => {
reject(error);
},
{
multiple,
maximum,
filter,
permissionAlert: true,
onmaxed() {
plus.nativeUI.alert(`最多只能选择${9}张`);
}
}
);
});
}

获取父组件的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 获取父组件的参数
* 因为支付宝小程序不支持provide/inject的写法
this.$parent在非H5中,可以准确获取到父组件,但是在H5中,需要多次this.$parent.$parent.xxx
这里默认值等于undefined有它的含义,因为最顶层元素(组件)的$parent就是undefined,意味着不传name
值(默认为undefined),就是查找最顶层的$parent
* @param {string|undefined} name 父组件的参数名
*/
export function $parent(name = undefined) {
let parent = this.$parent;
// 通过while历遍,这里主要是为了H5需要多层解析的问题
while (parent) {
// 父组件
if (parent.$options && parent.$options.name !== name) {
// 如果组件的name不相等,继续上一级寻找
parent = parent.$parent;
} else {
return parent;
}
}
return false;
}

获取目标元素的节点布局信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 节点布局信息
* @param selector <String> 此参数为元素节点,可以是id或者class,比如"#user-name",".box"
* @param all <Boolean> 是否返回全部节点信息,当页面有多个相同selector的元素时,all为true,会以数组形式返回所有节点的信息(结果为数组,数组元素为对象),否则只返回第一个节点的信息(结果为一个对象)
* @return res = {left: 0,right: 414,top: 323,height: 2597,bottom: 2920,width: 414}
*/
function uGetRect(selector, all) {
return new Promise((resolve) => {
uni
.createSelectorQuery()
.in(this)
[all ? "selectAll" : "select"](selector)
.boundingClientRect((rect) => {
if (all && Array.isArray(rect) && rect.length) {
resolve(rect);
}
if (!all && rect) {
resolve(rect);
}
})
.exec();
});
}

获取用户传递值的 px 值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const isNumber = (v) => /^[+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(v);

/**
* @description 用于获取用户传递值的px值 如果用户传递了"xxpx"或者"xxrpx",取出其数值部分,如果是"xxxrpx"还需要用过uni.upx2px进行转换
* @param {number|string} value 用户传递值的px值
* @param {boolean} unit
* @returns {number|string}
*/
function getPx(value, unit = false) {
if (isNumber(value)) {
return unit ? `${value}px` : Number(value);
}
// 如果带有rpx,先取出其数值部分,再转为px值
if (/(rpx|upx)$/.test(value)) {
return unit
? `${uni.upx2px(parseInt(value))}px`
: Number(uni.upx2px(parseInt(value)));
}
return unit ? `${parseInt(value)}px` : parseInt(value);
}

获取图片信息

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
/**
* 获取图片信息
* @param {Object} filepath
*/
function getImageInfo(filepath) {
return new Promise(function (resolve, reject) {
// #ifdef APP-PLUS
plus.io.getImageInfo({
src: filepath,
success: function (image) {
// console.log('orientation=' + image.orientation);
resolve(image);
},
fail: function (err) {
console.log("getImageInfoErr: " + JSON.stringify(err));
reject(err);
}
});
// #endif

// #ifdef H5 || MP-WEIXIN
uni.getImageInfo({
src: filepath,
success: function (image) {
// console.log('orientation=' + image.orientation);
resolve(image);
},
fail: function (err) {
console.log("getImageInfoErr: " + JSON.stringify(err));
reject(err);
}
});
// #endif
});
}

IO 操作

写入文件内容

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
/**
* 写入文件内容
* @param {String} filePath 文件路径,支持 a/b/file.txt
* @param {String} content 文件存在时是否重写
* @param {Number} [position] 写入日志的位置 0开始 默认结尾
* @returns {Promise}
*/
export function fileWriter(filePath, content, position) {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
// 请求本地系统文件对象 plus.io.PRIVATE_DOC:应用私有文档目录常量
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function (fs) {
// fs.root是根目录操作对象DirectoryEntry
fs.root.getFile(
filePath,
{
create: true
},
function (fileEntry) {
fileEntry.createWriter(
function (writer) {
writer.seek(position == undefined ? writer.length : position);
writer.write(content);
// console.log("写入本地文件日志", writer.fileName);
// console.log(content);
resolve(writer);
},
function (e) {
console.error("本地文件日志写入异常", e);
reject();
}
);
}
);
});
// #endif
// #ifdef H5
resolve();
// #endif
});
}

读取文件内容

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
/**
* 读取文件内容
* @param {String} filePath 文件路径,支持 a/b/file.txt
* @returns {Promise}
*/
export function fileReader(filePath) {
return new Promise((resolve, reject) => {
// 请求本地系统文件对象 plus.io.PRIVATE_DOC 应用私有文档目录常量
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function (fs) {
// fs.root是根目录操作对象DirectoryEntry
fs.root.getFile(filePath, {}, function (fileEntry) {
fileEntry.file(function (file) {
let fileReader = new plus.io.FileReader();
fileReader.readAsText(file, "utf-8");
fileReader.onloadend = function (evt) {
const { result: content, error } = evt?.target || {};
if (error) {
reject();
} else {
resolve({
...file,
content
});
}
};
});
});
});
});
}

读取目录文件列表

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
/**
* 读取目录文件列表
* @param {String} filePath 目录
* @returns {Promise}
*/
export function fileList(filePath) {
return new Promise((resolve, reject) => {
// 请求本地系统文件对象 plus.io.PRIVATE_DOC:应用私有文档目录常量
plus.io.resolveLocalFileSystemURL(
filePath, //指定的目录
(entry) => {
var directoryReader = entry.createReader(); //获取读取目录对象
directoryReader.readEntries(
(entries) => {
resolve(entries);
},
(err) => {
reject("读取失败");
console.error("读取本地目录", filePath, err);
}
);
},
(err) => {
reject("读取失败");
console.error("读取本地目录", filePath, err);
}
);
});
}

读取目录文件列表

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
/**
* 读取目录文件列表(递归)
* @param {String} path 目录
* @returns {Promise}
*/
export async function fileListDeep(path) {
return new Promise(async (resolve) => {
let realFilesObj = {};
await innerCollect(path);
async function innerCollect(path) {
let realFiles = [];
// 获取logs路径下的所有文件夹
let logFolders = await fileList(`${path}`);
for (let i = 0; i < logFolders.length; i++) {
const file = logFolders[i];
if (file.isDirectory) {
await innerCollect(file.toURL());
} else {
realFiles.push(file);
}
}
realFilesObj[path] = realFiles;
return;
}
resolve(realFilesObj);
});
}

压缩目录

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
/**
* 压缩目录
* @param {String} [targetPath] 目录
* @param {String} [name] 目录
* @returns {Promise}
*/
export function zipPath(targetPath = "_doc/", name) {
return new Promise((resolve, reject) => {
let zipfile = `_documents/${
name || targetPath.replace(/_/g, "").replace(/\//g, "_")
}${timeF("MMddhhmmssS")}.zip`;
plus.zip.compress(
targetPath,
zipfile,
() => {
resolve({
filePath: plus.io.convertLocalFileSystemURL(zipfile),
localFile: zipfile
});
},
(error) => {
reject(error);
}
);
});
}

删除单个文件或目录

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
/**
* 删除单个文件或目录
* @param {String} filePath 目录
* @returns {Promise}
*/
export function removeFile(filePath, singleFile = false) {
return new Promise((resolve, reject) => {
plus.io.resolveLocalFileSystemURL(
filePath,
(entry) => {
// 可通过entry对象操作test.html文件
/**
* entry.remove(succesCB, errorCB);
* 以下情况删除目录将会导致失败: 目录中存在文件; 删除根目录; 删除目录成功通过succesCB回调返回,失败则通过errorCB返回。
*/

/**
* entry.removeRecursively(succesCB, errorCB);
* 删除目录将会删除其下的所有文件及子目录 不能删除根目录,如果操作删除根目录将会删除目录下的文件及子目录,不会删除根目录自身。 删除目录成功通过succesCB回调返回,失败则通过errorCB返回。
*/
entry[!singleFile ? "removeRecursively" : "remove"](
() => {
console.log("success");
resolve();
},
() => {
console.log("error");
reject();
}
);
},
() => {
reject();
}
);
});
}

按条件删除文件

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
/**
* 按条件删除文件
* @param {Array} files 文件数组
* @param {Object} [options] 删除条件
* @param {Function} [options.filter] 文件处理函数,返回{ del: 是否直接删除, sort: (不直接删除)文件权重 }
* @param {Number} [options.retainMin] 至少保留的文件数量
* @param {Number} [options.sortMax] 条件权重的最大值
*/
export function removeFileByOptions(files, options) {
if (!options) {
return removeFiles(files);
} else {
const {
filter = (el) => ({ del: true }),
retainMin = 0,
sortMax = 0
} = options;
return files
.map((file) => {
// del=true 直接删除文件
// del=false保留文件,然后进行sort排序后删除sort小于sortMax的文件,但至少保留retainMin个文件
const { sort, del = false } = filter(file);
if (del) {
file.remove();
}
return { file, sort, del };
})
.filter((item) => !item.del)
.sort((a, b) => a.sort - b.sort)
.filter((item, index, self) => {
if (item.sort < sortMax && index < self.length - retainMin) {
item.file.remove();
return false;
}
return true;
});
}
}

删除多个文件或目录

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
/**
* 删除多个文件或目录
* @param {Array} [files] 文件数组
* @returns
*/
export function removeFiles(files = []) {
return new Promise((resolve, reject) => {
let filesLen = files?.length ?? 0;
if (!filesLen) {
resolve();
return;
}
for (let i = 0; i < filesLen; i++) {
const file = files[i];
file.removeRecursively(
() => {
filesLen--;
if (filesLen === 0) {
resolve();
}
},
(err) => {
console.error("remove failed", file.name, err);
reject(`删除失败:${err}`);
}
);
}
});
}

未完,待续。。。

app 端

ux-webview.vue
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
<template>
<web-view
:ref="webviewRefName"
:src="webViewUrl"
:allow="allow"
:sandbox="sandbox"
@message="message"
@onPostMessage="onPostMessage" />
</template>

<script>
import WebView from "./WebView.js";

export default {
props: {
src: {
type: String
},
allow: {
type: String
},
sandbox: {
type: String
},
UniJsBridgeName: {
type: String,
default: "wv"
}
},
data() {
return {
webView: null,
webViewUrl: "",
webviewRefName: null
};
},
created() {
this.webViewUrl = this.src || this.$Route.query.url || "";
this.webviewRefName =
"webview_" + new Date().getTime() + "_" + Math.ceil(Math.random() * 10);
},
mounted() {
let webviewRef = this.$refs[this.webviewRefName];
this.webView = new WebView(webviewRef, this.UniJsBridgeName);
},
methods: {
message() {
this.$emit("message", { ...arguments });
},
onPostMessage(data) {
let message = data.detail;
if (message) {
let { data, callbackId, handlerName } = message.data[0];
/**
* handlerName为response的时候,这个时候的callbackId为客户端的回调函数ID
* 其他的时候,callbackId为H5的回调函数ID
*/
if (handlerName === "response") {
/**
* 找到对应的回调函数
* 执行完毕后立马删除,否则集合中的东西太多了
*/
let responseCallback = this.webView.responseCallbacks[callbackId];
if (responseCallback) {
responseCallback(data);
delete this.webView.responseCallbacks[callbackId];
}
} else {
let messageHandler = this.webView.messageHandlers[handlerName];
let call = (data) => {
this.webView.call(null, data, null, callbackId);
};

if (messageHandler) {
messageHandler(data, call);
}
}
}
this.$emit("onPostMessage", data);
},
call() {
this.webView.call.apply(this.webView, arguments);
},
regist() {
this.webView.regist.apply(this.webView, arguments);
},
init() {
this.webView.init.apply(this.webView, arguments);
},
send() {
this.webView.send.apply(this.webView, arguments);
}
}
};
</script>
webView.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
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
class WebView {
messageHandlers = {};
responseCallbacks = {};
uniqueId = 1;
/**
* webView: uni-app 的webview组件
* UniJsBridgeName: 前端挂载在window下的全局方法,全局变量为wv
*/
constructor(webView, UniJsBridgeName = "wv") {
this.webViewRef = webView;
this.UniJsBridgeName = UniJsBridgeName;
}

/**
* 调用前端JS,都需要通过此函数
*
* @param {String} handlerName 前端注册的函数名
* @param {Object} data 给前端的回调的参数
* @param {Function} callback 调用完前端的回调
*/
call(handlerName, data, responseCallback, responseId) {
let messageJSON = {
handlerName, // 前端注册的函数名
responseId, // 前端对应的自己的回调函数
responseData: data // 给前端的参数
};

/**
* 把回调函数存在集合中
* 等待H5来调用
* @param {Object} responseCallback
*/
if (responseCallback) {
let callbackId =
"uni_app_cb_" + this.uniqueId++ + "_" + new Date().getTime();
this.responseCallbacks[callbackId] = responseCallback;
messageJSON.callbackId = callbackId;
}

messageJSON = JSON.stringify(messageJSON);
this.webViewRef.evalJs(
`${this.UniJsBridgeName}._handleMessageFromNative(${messageJSON})`
);
}

/**
* 注册线程 往数组里面添加值,被动的等待着H5调用的
* @param {Object} handlerName
* @param {Object} handler
*/
regist(handlerName, handler) {
this.messageHandlers[handlerName] = handler;
}

/**
* 设置默认处理函数,如果前端用send函数给客户端发消息,就走此函数
* 其实和regist是一样的,只不过可以省略一个参数
*/
init(handler) {
this.messageHandlers["send"] = handler;
}

/**
* 向H5发消息
* @param {Object} message
*/
send(message, responseCallback) {
this.call(null, message, responseCallback);
}
}

export default WebView;

示例:

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
<template>
<view class="page">
<view class="fromH5Message">
<view class="message">来着H5的消息: {{ fromH5Message }}</view>
</view>
<button type="default" @tap="callH5">调用H5提前注册好的函数</button>
<button type="default" @tap="initH5">调用H5提前注册好的默认处理函数</button>
<ux-webview ref="wv" class="webview" src="http://0.0.0.0:8000" />
</view>
</template>

<script>
export default {
data() {
return {
count: 0,
fromH5Message: ""
};
},
onLoad() {
setTimeout(() => {
/**
* 先注册一个函数给H5调用
*/
this.$refs.wv.regist("nativeShow", (data, cb) => {
this.fromH5Message = JSON.stringify(data);

cb({
data: "nativeShow的消息" + new Date().getTime()
});
});

/**
* 设置一个默认处理函数
*/
this.$refs.wv.init((data, cb) => {
cb({ send: "给H5的" });
});
});
},
methods: {
callH5() {
this.$refs.wv.call(
"test",
{ toH5Data: "给H5的参数" + Date.now() },
(data) => {
this.fromH5Message = JSON.stringify(data);
}
);
},
initH5() {
this.$refs.wv.send({ fff: "1000.013213" }, function (data) {
this.fromH5Message = "send" + JSON.stringify(data);
});
}
}
};
</script>

h5 端

index.nvue
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
<template>
<view>
<h1>这个是H5页面</h1>
<div class="fromNative">来着uniApp的消息: {{ msg }}</div>
<div class="btn-box">
<ux-button type="info" @click="handleNative">
调用Uniapp提前注册好的nativeShow方法
</ux-button>
<ux-button type="primary" @click="handleDefault"
>调用Uniapp提前注册好的默认处理函数</ux-button
>
<ux-button type="success" @click="handleToPage">跳转页面</ux-button>
</div>
</view>
</template>

<script>
export default {
data() {
return {
msg: ""
};
},
onLoad() {
/**
* 注册了一个函数,提供给Uni-app调用
*/
wv.regist("test", (data, cb) => {
// data: 是客户端给H5的参数
this.msg = "来着uniApp的消息111:" + JSON.stringify(data);
// 把参数传递给客户端,如果不执行的话,uni-app是无法执行回调的
cb("我是给uni-app的参数" + new Date().getTime());
});

/**
* 在H5端配置一个默认处理函数,提供给uni-app调用
*/
wv.init((data, cb) => {
alert(JSON.stringify(data));

cb({
initData: "给uni-app使用的"
});
});
},

methods: {
handleNative() {
/**
* 触发uniapp中注册好的函数
*/
wv.call(
"nativeShow",
{
nativeShowData: "当前时间:" + new Date().getTime()
},
(res) => {
this.msg = "来着uniApp的消息2222:" + JSON.stringify(res);
}
);
},
handleDefault() {
wv.send({ sendData: "给uni-app的" }, (res) => {
this.msg = "来着uniApp的消息333:" + JSON.stringify(res);
});
},
handleToPage() {
wv.uni.navigateTo({
url: "/pages/about/index"
});
}
}
};
</script>
uni.webview.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
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
!(function (e, n) {
"object" == typeof exports && "undefined" != typeof module
? (module.exports = n())
: "function" == typeof define && define.amd
? define(n)
: ((e = e || self).uni = n());
})(this, function () {
"use strict";
try {
var e = {};
Object.defineProperty(e, "passive", {
get: function () {
!0;
}
}),
window.addEventListener("test-passive", null, e);
} catch (e) {}
var n = Object.prototype.hasOwnProperty;
function t(e, t) {
return n.call(e, t);
}
var i = [],
a = function (e, n) {
var t = { options: { timestamp: +new Date() }, name: e, arg: n };
if (window.__dcloud_weex_postMessage || window.__dcloud_weex_) {
if ("postMessage" === e) {
var a = { data: [n] };
return window.__dcloud_weex_postMessage
? window.__dcloud_weex_postMessage(a)
: window.__dcloud_weex_.postMessage(JSON.stringify(a));
}
var o = {
type: "WEB_INVOKE_APPSERVICE",
args: { data: t, webviewIds: i }
};
window.__dcloud_weex_postMessage
? window.__dcloud_weex_postMessageToService(o)
: window.__dcloud_weex_.postMessageToService(JSON.stringify(o));
}
if (!window.plus)
return window.parent.postMessage(
{ type: "WEB_INVOKE_APPSERVICE", data: t, pageId: "" },
"*"
);
if (0 === i.length) {
var r = plus.webview.currentWebview();
if (!r) throw new Error("plus.webview.currentWebview() is undefined");
var d = r.parent(),
s = "";
(s = d ? d.id : r.id), i.push(s);
}
if (plus.webview.getWebviewById("__uniapp__service"))
plus.webview.postMessageToUniNView(
{ type: "WEB_INVOKE_APPSERVICE", args: { data: t, webviewIds: i } },
"__uniapp__service"
);
else {
var w = JSON.stringify(t);
plus.webview
.getLaunchWebview()
.evalJS(
'UniPlusBridge.subscribeHandler("'
.concat("WEB_INVOKE_APPSERVICE", '",')
.concat(w, ",")
.concat(JSON.stringify(i), ");")
);
}
},
o = {
navigateTo: function () {
var e =
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
n = e.url;
a("navigateTo", { url: encodeURI(n) });
},
navigateBack: function () {
var e =
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
n = e.delta;
a("navigateBack", { delta: parseInt(n) || 1 });
},
switchTab: function () {
var e =
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
n = e.url;
a("switchTab", { url: encodeURI(n) });
},
reLaunch: function () {
var e =
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
n = e.url;
a("reLaunch", { url: encodeURI(n) });
},
redirectTo: function () {
var e =
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
n = e.url;
a("redirectTo", { url: encodeURI(n) });
},
getEnv: function (e) {
window.plus ? e({ plus: !0 }) : e({ h5: !0 });
},
postMessage: function () {
var e =
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {};
a("postMessage", e.data || {});
}
},
r = /uni-app/i.test(navigator.userAgent),
d = /Html5Plus/i.test(navigator.userAgent),
s = /complete|loaded|interactive/;
var w = window.my && navigator.userAgent.indexOf("AlipayClient") > -1;
var u =
window.swan && window.swan.webView && /swan/i.test(navigator.userAgent);
var c =
window.qq &&
window.qq.miniProgram &&
/QQ/i.test(navigator.userAgent) &&
/miniProgram/i.test(navigator.userAgent);
var g =
window.tt &&
window.tt.miniProgram &&
/toutiaomicroapp/i.test(navigator.userAgent);
var v =
window.wx &&
window.wx.miniProgram &&
/micromessenger/i.test(navigator.userAgent) &&
/miniProgram/i.test(navigator.userAgent);
var p = window.qa && /quickapp/i.test(navigator.userAgent);
for (
var l,
_ = function () {
(window.UniAppJSBridge = !0),
document.dispatchEvent(
new CustomEvent("UniAppJSBridgeReady", {
bubbles: !0,
cancelable: !0
})
);
},
f = [
function (e) {
if (r || d)
return (
window.__dcloud_weex_postMessage || window.__dcloud_weex_
? document.addEventListener("DOMContentLoaded", e)
: window.plus && s.test(document.readyState)
? setTimeout(e, 0)
: document.addEventListener("plusready", e),
o
);
},
function (e) {
if (v)
return (
window.WeixinJSBridge && window.WeixinJSBridge.invoke
? setTimeout(e, 0)
: document.addEventListener("WeixinJSBridgeReady", e),
window.wx.miniProgram
);
},
function (e) {
if (c)
return (
window.QQJSBridge && window.QQJSBridge.invoke
? setTimeout(e, 0)
: document.addEventListener("QQJSBridgeReady", e),
window.qq.miniProgram
);
},
function (e) {
if (w) {
document.addEventListener("DOMContentLoaded", e);
var n = window.my;
return {
navigateTo: n.navigateTo,
navigateBack: n.navigateBack,
switchTab: n.switchTab,
reLaunch: n.reLaunch,
redirectTo: n.redirectTo,
postMessage: n.postMessage,
getEnv: n.getEnv
};
}
},
function (e) {
if (u)
return (
document.addEventListener("DOMContentLoaded", e),
window.swan.webView
);
},
function (e) {
if (g)
return (
document.addEventListener("DOMContentLoaded", e),
window.tt.miniProgram
);
},
function (e) {
if (p) {
window.QaJSBridge && window.QaJSBridge.invoke
? setTimeout(e, 0)
: document.addEventListener("QaJSBridgeReady", e);
var n = window.qa;
return {
navigateTo: n.navigateTo,
navigateBack: n.navigateBack,
switchTab: n.switchTab,
reLaunch: n.reLaunch,
redirectTo: n.redirectTo,
postMessage: n.postMessage,
getEnv: n.getEnv
};
}
},
function (e) {
return document.addEventListener("DOMContentLoaded", e), o;
}
],
m = 0;
m < f.length && !(l = f[m](_));
m++
);
l || (l = {});
var E = "undefined" != typeof uni ? uni : {};
if (!E.navigateTo) for (var b in l) t(l, b) && (E[b] = l[b]);
return (E.webView = l), E;
});
(function () {
if (window.wv) {
throw new Error("wv已经存在了");
}
var uniqueId = 1; // 全局唯一标识,主要用来生产callbackId
var responseCallbacks = {}; // 给客户端发送消息后的回调函数集合
var messageHandlers = {}; // 在H5中注册的函数(就是等待客户端调用的函数)的集合
var receiveMessageQueue = []; // 消息队列,Native给H5发消息的队列,所有的消息都集中在此,依次调用
var wv = {
init: init,
send: send,
call: call,
regist: regist,
uni: { ...uni, ...uni.webView, webView: uni.webView },
_handleMessageFromNative: _handleMessageFromNative
};
// Native调用H5函数必须是要挂载到window下
window.wv = wv;
function init(messageHandler) {
if (wv._messageHandler) {
throw new Error("wv._messageHandler 被调用了两次");
}
wv._messageHandler = messageHandler;
var receiveMessage = receiveMessageQueue;
receiveMessageQueue = null;
for (var i = 0; i < receiveMessage.length; i++) {
_dispatchMessageFromNative(receiveMessage[i]);
}
}
/**
* 发送消息给客户端
* PS:主动发送
* @param data 给客户端的数据
* @param responseCallback 回调函数
*/
function send(data, responseCallback) {
_doSend("send", data, responseCallback);
}
// 调用线程
function call(handlerName, data, responseCallback) {
_doSend(handlerName, data, responseCallback);
}
/**
* 注册一个函数,待客户端调用
* PS:被动等待调用
* @param handlerName
* @param handler: 接收两个参数,data(客户端回复给H5的数据),responseCallback(一个回调函数:调用该函数,客户端才可以执行回调,且可以传参数给客户端)
*/
function regist(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
/**
* 发送给客户端的核心函数,依赖uni-app SDk
* PS:主动发送
* @param handlerName
* @param message
* @param responseCallback: 可接收String和Function,如果为String的话就是给客户端的一个回调ID,这个ID其实本来就是由客户端自己传过来的
* 如果为Function的话就是H5的回调函数
* @private
*/
function _doSend(handlerName, message, responseCallback) {
var callbackId;
if (typeof responseCallback === "string") {
callbackId = responseCallback;
} else if (responseCallback) {
/**
* 把回调函数塞进集合中去,待客户端代码执行完毕就去找到对应的函数开始执行
*/
callbackId = "cb_" + uniqueId++ + "_" + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
} else {
callbackId = "";
}

const postMes = uni.postMessage || uni.webView.postMessage;
if (!postMes) {
return console.error(
"此接口依赖uni.webview,具体参考:https://uniapp.dcloud.io/component/web-view"
);
}
// 给客户端发送消息
postMes({
data: {
data: message,
callbackId: callbackId,
handlerName: handlerName
}
});
}
function _dispatchMessageFromNative(messageJSON) {
if (!messageJSON) {
return console.warn("客户端没有回复任何消息");
}
try {
/**
* responseId: 客户端回复前端的回调函数的ID
* callbackId: 给客户端自己用的回调ID
* handlerName: 前后端统一约定的一个名字[约定不同的名字对应不同的功能,需要后期加,这里只是封装的一个方法]
* responseData: 客户端给前端的参数
*/
var callbackId = messageJSON.callbackId,
responseId = messageJSON.responseId,
handlerName = messageJSON.handlerName,
responseData = messageJSON.responseData;
var responseCallback = void 0;
if (responseId) {
responseCallbacks[responseId] &&
responseCallbacks[responseId](responseData);
delete responseCallbacks[responseId];
} else {
if (callbackId) {
var callbackResponseId_1 = callbackId;
responseCallback = function (responseData) {
_doSend("response", responseData, callbackResponseId_1);
};
}
var handler = wv._messageHandler;
if (handlerName) {
handler = messageHandlers[handlerName];
}
handler(responseData, responseCallback);
}
} catch (e) {
console.log(e);
}
}
/**
* 提供给客户端调用的函数,
* 约定客户端只能通过此函数发消息给H5
* @param messageJSON
* @private
*/
function _handleMessageFromNative(messageJSON) {
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
}
_dispatchMessageFromNative(messageJSON);
}
})();

img

1
<img src="图片来源" alt="图片无法显示时显示图片说明性文字" style="设置样式属性" />
  • img 标签虽然不是块状元素,但是可以设置宽高,占位

  • img 设置 width 后 height 会自适应匹配,如果设置 height 后 width 会自适应匹配,如果同时设置 width,height,img 图片可能会变形

background-image

  • css 的一个样式,并不占位,可以结合 background-size,background-repeat,background-position 等属性来设置图片位置,大小等

  • 所在的元素必须指定 height,否则背景图片显示不出来,可以不指定 width,如果不指定 width,它的宽度则继承父元素的宽度

区别

  • img 是 html 标签,占位,background-image 是 CSS 样式,不占位
  • img 是 document 对象可以操作(比如:从 document 对象中删除),background-image 是不能操作的
  • 在网页加载过程中,img 作为网页结构内容的一部分,会在加载结构的过程中加载,而 background-image 作为 css 样式的一部分,会在结构加载完成后开始加载。(如果你引入了一个很大的图片,在这个图片下载完成之前,img 后面的内容都不会显示,而如果用 css 引入同样的图片,网页结构和内容加载完成之后,才开始加载背景图片,不会影响你浏览网页内容。)
  • img 标签点击会选中或放大图片,background-image 则不会

加载问题

  1. 背景图片会等到 html 结构加载完成才开始加载
  2. img 标签是网页结构的一部分,会在 html 结构加载的时候加载
  3. 在网页加载的过程中,背景图片会等到结构加载完成(网页的内容全部显示以后)才开始加载。
  4. img 标签是网页结构(内容)的一部分会在 html 结构加载的过程中加载,换句话讲,网页会先加载 img 标签的内容,再加载背景图片,如果你用引入了一个很大的图片,那么在这个图片下载完成之前,img 后的内容都不会显示。
  5. 用 css 来引入同样的图片,网页结构和内容加载完成之后,才开始加载背景图片,不会影响你浏览网页内容。(即,img 是内容性的,背景图是修饰性的)

加载失败显示问题

  • 背景图片在加载失败或路径找不到时,不会显示图片标记,

  • img 标签在加载失败或找不到路径时,会显示一个撕裂的小图标标记

数据图与非数据图

所谓数据图就是从后台获取的图片,一般就用 img 标签显示,其他的图片一般就作为背景图展示

何时使用

使用 img

  1. 使用 IMG(alt 文本)图像有一个重要的语义时,比如一个警告图标。这将确保图像的意义可以很好的和 user-agents 沟通,包括屏幕阅读器(宣传、产品图片等)
  2. 作为 html 结构内容的一部分
  3. 展示从后台传过来的数据
  4. 对图片进行缩放操作
  5. 利于搜索引擎搜索时
  6. 有明确的语义化要求
  7. 图片作为页面的组成部分而不是修饰部分
  8. 使用 img 代替有背景图像可以显著提高性能的动画背景
  9. 显示图片只给定宽或高且不能截断图片,并且保证图片宽高比

使用背景图

  1. 不重要的用作装饰、美化的图片
  2. 不是 html 结构的一部分
  3. 图像代替文本使用时使用 backgrond-image(避免出现无语义化标签)
  4. 缩短下载时间通过 CSS sprites 时使用 backgrond-image
  5. 为不同的屏幕分辨率展示不同的图像时(media 查询时使用背景图)
  6. 作为背景图片
  7. 禁止用户保存图片(点击鼠标右键另存为操作)
  8. 允许图片拉伸,平铺,截取等情况

background-size: 100% 100%background-size: cover 之间的区别

background-size: 100% 100%; 按容器 比例撑满,图片会变形
background-size: cover; 把背景图片放大到适合元素容器的尺寸,图片比例不变

特性cookielocalStoragesessionStorageindexDB
数据生命周期⼀般由服务器生成, 可以设置过期时间除非被清理,否则⼀直存在页面关闭就清理除非被清理, 否则⼀直存在
数据存储大小4K5M5M无限
与服务端通信每次都会携带在 header 中,对于请求性能影响不参与不参与不参与

从上表可以看到, cookie 已经不建议用于存储 。如果没有大量数据存储需求的话, 可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储, 否则可以用 sessionStorage 存储。

对于 cookie ,我们还需要注意安全性

属性作用
value如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
http-only不能通过 JS 访问 Cookie ,减少 XSS 攻击
secure只能在协议为 HTTPS 的请求中携带
same-site规定浏览器不能在跨域请求中携带 Cookie ,减少 CSRF 攻击
简述 cookie 和 session 的区别

技术点

  • html2canvas:将网页绘制 base64 的图片
  • plus.nativeObj.Bitmap:下载 base64 的 png 图片,临时存放起来
  • uni.saveImageToPhotosAlbum:将临时存放的 png 图片,存放到手机相册
  • plus.io.resolveLocalFileSystemURL:删除临时存放的图片

运用

安装 html2canvas

1
npm install html2canvas

RenderJs

因为 html2canvas 要进行 dom 操作,所以我们得使用 renderjs。script 就要分为两部分,一个是纯的 script 标签块,一个是带有 module、lang 的 script 标签块

通信

两个 script 块之间的通信

1
$ownerInstance.callMethod("METHOD_NAME", params);

权限

manifest.json 配置 android 的写入权限

1
android.permission.WRITE_EXTERNAL_STORAGE

代码如下

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
<template>
<view class="html2canvas" :prop="domId" :change:prop="html2canvas.create">
<slot></slot>
</view>
</template>

<script>
export default {
name: "Html2canvas",
props: {
// 需要转为图片的父节点
domId: {
type: String,
required: true
}
},
methods: {
success(base64) {
uni.hideToast();
this.saveHeadImgFile(base64);
this.$emit("success", base64);
},
saveHeadImgFile(base64) {
const bitmap = new plus.nativeObj.Bitmap("test");
bitmap.loadBase64Data(
base64,
function () {
const url = "_doc/" + new Date().getTime() + ".png"; // url为时间戳命名方式
console.log("saveHeadImgFile", url);
bitmap.save(
url,
{
overwrite: true // 是否覆盖
// quality: 'quality' // 图片清晰度
},
(i) => {
uni.saveImageToPhotosAlbum({
filePath: url,
success: function () {
uni.showToast({
title: "图片保存成功",
icon: "none"
});
// 图片保存成功后,将临时保存的图片删除
plus.io.resolveLocalFileSystemURL(
url,
function (entry) {
entry.getMetadata(function (metadata) {
// console.log("fileCount=" + metadata.fileCount);
entry.remove(); //删除单个文件 _downloads/ 改文件名
});
},
function (error) {
// uni.showToast({ title: "下载文件" });
console.log("error" + error);
}
);
bitmap.clear();
}
});
},
(e) => {
uni.showToast({
title: "图片保存失败",
icon: "none"
});
bitmap.clear();
}
);
},
(e) => {
uni.showToast({
title: "图片保存失败",
icon: "none"
});
bitmap.clear();
}
);
},
showLoading() {
this.$toast("正在生成海报", {
icon: "none",
mask: true,
duration: 100000
});
}
}
};
</script>

<script module="html2canvas" lang="renderjs">
// 这里使用的是 html2canvas 1.4.1
import html2canvas from "static/js/html2canvas.min.js"; // 将html2canvas文件下载到本地 static/ 目录下
// import html2canvas from "html2canvas"; // 使用npm将html2canvas安装到 node_modules/ 目录下

export default {
methods: {
async create(domId) {
try {
this.$ownerInstance.callMethod("showLoading", true);
const timeout = setTimeout(async () => {
const shareContent = document.querySelector(domId);
const canvas = await html2canvas(shareContent, {
useCORS: true,
width: shareContent.offsetWidth,
height: shareContent.offsetHeight
});

const base64 = canvas.toDataURL("image/png", 1);
this.$ownerInstance.callMethod("success", base64);
clearTimeout(timeout);
}, 500);
} catch (error) {
}
}
}
};
</script>

<style lang="scss"></style>

file、base64 相互转换

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
/**
* file 转为 base64
*/
function fileToBase64(file) {
return new Promise((resolve, reject) => {
///FileReader类就是专门用来读文件的
const reader = new FileReader();
//开始读文件
//readAsDataURL: dataurl 它的本质就是图片的二进制数据, 进行base64加密后形成的一个字符串,
reader.readAsDataURL(file);
// 成功和失败返回对应的信息,reader.result一个base64,可以直接使用
reader.onload = () => resolve(reader.result);
// 失败返回失败的信息
reader.onerror = (error) => reject(error);
});
}

/**
* Base64 转为 File
*/
function base64toFile(base64Data, fileName) {
let data = base64Data.split(","),
mime = data[0].match(/:(.*?);/)[1],
byteString = atob(data[1]),
byteLength = byteString.length,
uInt8Array = new Uint8Array(byteLength);
while (n--) {
uInt8Array[n] = byteString.charCodeAt(n);
}
return new File([uInt8Array], fileName, { type: mime });
}

File、Blob 相互转换

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
/**
* Blob 转为 File
*/
function blobToFile(blob, fileName) {
blob.lastModifiedDate = new Date();
blob.name = fileName;
return blob;
}

/**
* File 转为 Blob
*/
function fileToBlob(file) {
// 创建 FileReader 对象
let reader = new FileReader();

return new Promise((resolve) => {
// FileReader 添加 load 事件
reader.addEventListener("load", (e) => {
let blob;
if (typeof e.target.result === "object") {
blob = new Blob([e.target.result]);
} else {
blob = e.target.result;
}
console.log(Object.prototype.toString.call(blob));
resolve(blob);
});

// FileReader 以 ArrayBuffer 格式 读取 File 对象中数据
reader.readAsArrayBuffer(file);
});
}

base64、Blob 相互转换

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
/**
* Base64 转为 Blob
*/
function baseURLtoBlob(base64) {
let arr = base64.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}

/**
* Blob 转为 Base64
*/
function blobToBase64(blob, callback) {
let reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = function (e) {
callback(e.target.result);
};
}

ArrayBuffer、File 相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* ArrayBuffer 转为 File
*/
function bufferToFile(buffer, filename) {
return new File([buffer], filename);
}

/**
* Blob 转为 ArrayBuffer
*/
function fileToBuffer(file, cb) {
let fr = new FileReader();
let filename = file.name;
fr.readAsArrayBuffer(file);
fr.addEventListener(
"loadend",
(e) => {
let buffer = e.target.result;
cb(buffer, filename);
},
false
);
}

ArrayBuffer、base64 相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* ArrayBuffer 转为 base64
*/
function bufferToBase64(buffer) {
return "data:image/jpeg;base64," + window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
}

/**
* base64 转为 ArrayBuffer
*/
function base64ToBuffer(base64) {
let binary_string = window.atob(base64);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}

ArrayBuffer、Blob 相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* ArrayBuffer 转为 Blob
*/
function bufferToBlob(buffer, mimeType = "") {
return new Blob([buffer], { type: mimeType });
}

/**
* Blob 转为 ArrayBuffer
*/
function blobToBuffer(blob, cb) {
let fr = new FileReader();
let type = blob.type;
fr.readAsArrayBuffer(blob);
fr.addEventListener(
"loadend",
(e) => {
let buffer = e.target.result;
cb(buffer, type);
},
false
);
}

Blob 的应用

  1. 大文件分割
    slice() 方法接受三个参数,起始偏移量,结束偏移量,还有可选的 mime 类型。如果 mime 类型,没有设置,那么新的 Blob 对象的 mime 类型和父级一样。
  2. 当要上传大文件的时候,此方法非常有用,可以将大文件分割分段,然后各自上传,因为分割之后的 Blob 对象和原始的是独立存在的。
    不过目前浏览器实现此方法还没有统一,火狐使用的是 mozSlice() ,Chrome 使用的是 webkitSlice(),其他浏览器则正常的方式 slice()
1
2
3
4
5
6
7
8
9
10
11
// 兼容各浏览器的写法
function sliceBlob(blob, start, end, type) {
type = type || blob.type;
if (blob.mozSlice) {
return blob.mozSlice(start, end, type);
} else if (blob.webkitSlice) {
return blob.webkitSlice(start, end, type);
} else {
throw new Error("This doesn't work!");
}
}

Date对象是 JavaScript 原生的时间库。它以 1970 年 1 月 1 日 00:00:00 作为时间的零点,可以表示的时间范围是前后各 1 亿天(单位为毫秒)。

普通函数的用法

Date对象可以作为普通函数直接调用,返回一个代表当前时间的字符串。

1
2
Date();
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"

注意,即使带有参数,Date作为普通函数使用时,返回的还是当前时间。

1
2
Date(2000, 1, 1);
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"

上面代码说明,无论有没有参数,直接调用Date总是返回当前时间。

构造函数的用法

Date还可以当作构造函数使用。对它使用new命令,会返回一个Date对象的实例。如果不加参数,实例代表的就是当前时间。

1
var today = new Date();

Date实例有一个独特的地方。其他对象求值的时候,都是默认调用.valueOf()方法,但是Date实例求值的时候,默认调用的是toString()方法。这导致对Date实例求值,返回的是一个字符串,代表该实例对应的时间。

1
2
3
4
5
6
7
8
var today = new Date();

today;
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"

// 等同于
today.toString();
// "Tue Dec 01 2015 09:34:43 GMT+0800 (CST)"

上面代码中,todayDate的实例,直接求值等同于调用toString方法。

作为构造函数时,Date对象可以接受多种格式的参数,返回一个该参数对应的时间实例。

1
2
3
4
5
6
7
8
9
10
11
12
// 参数为时间零点开始计算的毫秒数
new Date(1378218728000);
// Tue Sep 03 2013 22:32:08 GMT+0800 (CST)

// 参数为日期字符串
new Date("January 6, 2013");
// Sun Jan 06 2013 00:00:00 GMT+0800 (CST)

// 参数为多个整数,
// 代表年、月、日、小时、分钟、秒、毫秒
new Date(2013, 0, 1, 0, 0, 0, 0);
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)

关于Date构造函数的参数,有几点说明。

第一点,参数可以是负整数,代表 1970 年元旦之前的时间。

1
2
new Date(-1378218728000);
// Fri Apr 30 1926 17:27:52 GMT+0800 (CST)

第二点,只要是能被Date.parse()方法解析的字符串,都可以当作参数。

1
2
3
4
5
6
7
8
9
10
11
new Date("2013-2-15");
new Date("2013/2/15");
new Date("02/15/2013");
new Date("2013-FEB-15");
new Date("FEB, 15, 2013");
new Date("FEB 15, 2013");
new Date("Feberuary, 15, 2013");
new Date("Feberuary 15, 2013");
new Date("15 Feb 2013");
new Date("15, Feberuary, 2013");
// Fri Feb 15 2013 00:00:00 GMT+0800 (CST)

上面多种日期字符串的写法,返回的都是同一个时间。

第三,参数为年、月、日等多个整数时,年和月是不能省略的,其他参数都可以省略的。也就是说,这时至少需要两个参数,因为如果只使用“年”这一个参数,Date会将其解释为毫秒数。

1
2
new Date(2013);
// Thu Jan 01 1970 08:00:02 GMT+0800 (CST)

上面代码中,2013 被解释为毫秒数,而不是年份。

1
2
3
4
5
6
7
8
new Date(2013, 0);
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
new Date(2013, 0, 1);
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
new Date(2013, 0, 1, 0);
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)
new Date(2013, 0, 1, 0, 0, 0, 0);
// Tue Jan 01 2013 00:00:00 GMT+0800 (CST)

上面代码中,不管有几个参数,返回的都是 2013 年 1 月 1 日零点。

最后,各个参数的取值范围如下。

  • 年:使用四位数年份,比如2000。如果写成两位数或个位数,则加上1900,即10代表 1910 年。如果是负数,表示公元前。
  • 月:0表示一月,依次类推,11表示 12 月。
  • 日:131
  • 小时:023
  • 分钟:059
  • 秒:059
  • 毫秒:0999

注意,月份从0开始计算,但是,天数从1开始计算。另外,除了日期的默认值为1,小时、分钟、秒钟和毫秒的默认值都是0

这些参数如果超出了正常范围,会被自动折算。比如,如果月设为15,就折算为下一年的 4 月。

1
2
3
4
new Date(2013, 15);
// Tue Apr 01 2014 00:00:00 GMT+0800 (CST)
new Date(2013, 0, 0);
// Mon Dec 31 2012 00:00:00 GMT+0800 (CST)

上面代码的第二个例子,日期设为0,就代表上个月的最后一天。

参数还可以使用负数,表示扣去的时间。

1
2
3
4
new Date(2013, -1);
// Sat Dec 01 2012 00:00:00 GMT+0800 (CST)
new Date(2013, 0, -1);
// Sun Dec 30 2012 00:00:00 GMT+0800 (CST)

上面代码中,分别对月和日使用了负数,表示从基准日扣去相应的时间。

日期的运算

类型自动转换时,Date实例如果转为数值,则等于对应的毫秒数;如果转为字符串,则等于对应的日期字符串。所以,两个日期实例对象进行减法运算时,返回的是它们间隔的毫秒数;进行加法运算时,返回的是两个字符串连接而成的新字符串。

1
2
3
4
5
6
7
var d1 = new Date(2000, 2, 1);
var d2 = new Date(2000, 3, 1);

d2 - d1;
// 2678400000
d2 + d1;
// "Sat Apr 01 2000 00:00:00 GMT+0800 (CST)Wed Mar 01 2000 00:00:00 GMT+0800 (CST)"

静态方法

Date.now()

Date.now方法返回当前时间距离时间零点(1970 年 1 月 1 日 00:00:00 UTC)的毫秒数,相当于 Unix 时间戳乘以 1000。

1
Date.now(); // 1364026285194

Date.parse()

Date.parse方法用来解析日期字符串,返回该时间距离时间零点(1970 年 1 月 1 日 00:00:00)的毫秒数。

日期字符串应该符合 RFC 2822 和 ISO 8061 这两个标准,即YYYY-MM-DDTHH:mm:ss.sssZ格式,其中最后的Z表示时区。但是,其他格式也可以被解析,请看下面的例子。

1
2
3
4
5
6
Date.parse("Aug 9, 1995");
Date.parse("January 26, 2011 13:51:50");
Date.parse("Mon, 25 Dec 1995 13:30:00 GMT");
Date.parse("Mon, 25 Dec 1995 13:30:00 +0430");
Date.parse("2011-10-10");
Date.parse("2011-10-10T14:48:00");

上面的日期字符串都可以解析。

如果解析失败,返回NaN

1
Date.parse("xxx"); // NaN

Date.UTC()

Date.UTC方法接受年、月、日等变量作为参数,返回该时间距离时间零点(1970 年 1 月 1 日 00:00:00 UTC)的毫秒数。

1
2
3
4
5
6
// 格式
Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])

// 用法
Date.UTC(2011, 0, 1, 2, 3, 4, 567)
// 1293847384567

该方法的参数用法与Date构造函数完全一致,比如月从0开始计算,日期从1开始计算。区别在于Date.UTC方法的参数,会被解释为 UTC 时间(世界标准时间),Date构造函数的参数会被解释为当前时区的时间。

实例方法

Date的实例对象,有几十个自己的方法,除了valueOftoString,可以分为以下三类。

  • to类:从Date对象返回一个字符串,表示指定的时间。
  • get类:获取Date对象的日期和时间。
  • set类:设置Date对象的日期和时间。

Date.prototype.valueOf()

valueOf方法返回实例对象距离时间零点(1970 年 1 月 1 日 00:00:00 UTC)对应的毫秒数,该方法等同于getTime方法。

1
2
3
4
var d = new Date();

d.valueOf(); // 1362790014817
d.getTime(); // 1362790014817

预期为数值的场合,Date实例会自动调用该方法,所以可以用下面的方法计算时间的间隔。

1
2
3
4
var start = new Date();
// ...
var end = new Date();
var elapsed = end - start;

to 类方法

(1)Date.prototype.toString()

toString方法返回一个完整的日期字符串。

1
2
3
4
5
6
var d = new Date(2013, 0, 1);

d.toString();
// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"
d;
// "Tue Jan 01 2013 00:00:00 GMT+0800 (CST)"

因为toString是默认的调用方法,所以如果直接读取Date实例,就相当于调用这个方法。

(2)Date.prototype.toUTCString()

toUTCString方法返回对应的 UTC 时间,也就是比北京时间晚 8 个小时。

1
2
3
4
var d = new Date(2013, 0, 1);

d.toUTCString();
// "Mon, 31 Dec 2012 16:00:00 GMT"

(3)Date.prototype.toISOString()

toISOString方法返回对应时间的 ISO8601 写法。

1
2
3
4
var d = new Date(2013, 0, 1);

d.toISOString();
// "2012-12-31T16:00:00.000Z"

注意,toISOString方法返回的总是 UTC 时区的时间。

(4)Date.prototype.toJSON()

toJSON方法返回一个符合 JSON 格式的 ISO 日期字符串,与toISOString方法的返回结果完全相同。

1
2
3
4
var d = new Date(2013, 0, 1);

d.toJSON();
// "2012-12-31T16:00:00.000Z"

(5)Date.prototype.toDateString()

toDateString方法返回日期字符串(不含小时、分和秒)。

1
2
var d = new Date(2013, 0, 1);
d.toDateString(); // "Tue Jan 01 2013"

(6)Date.prototype.toTimeString()

toTimeString方法返回时间字符串(不含年月日)。

1
2
var d = new Date(2013, 0, 1);
d.toTimeString(); // "00:00:00 GMT+0800 (CST)"

(7)Date.prototype.toLocaleDateString()

toLocaleDateString方法返回一个字符串,代表日期的当地写法(不含小时、分和秒)。

1
2
3
4
5
var d = new Date(2013, 0, 1);

d.toLocaleDateString();
// 中文版浏览器为"2013年1月1日"
// 英文版浏览器为"1/1/2013"

(8)Date.prototype.toLocaleTimeString()

toLocaleTimeString方法返回一个字符串,代表时间的当地写法(不含年月日)。

1
2
3
4
5
var d = new Date(2013, 0, 1);

d.toLocaleTimeString();
// 中文版浏览器为"上午12:00:00"
// 英文版浏览器为"12:00:00 AM"

get 类方法

Date对象提供了一系列get*方法,用来获取实例对象某个方面的值。

  • getTime():返回实例距离 1970 年 1 月 1 日 00:00:00 的毫秒数,等同于valueOf方法。
  • getDate():返回实例对象对应每个月的几号(从 1 开始)。
  • getDay():返回星期几,星期日为 0,星期一为 1,以此类推。
  • getYear():返回距离 1900 的年数。
  • getFullYear():返回四位的年份。
  • getMonth():返回月份(0 表示 1 月,11 表示 12 月)。
  • getHours():返回小时(0-23)。
  • getMilliseconds():返回毫秒(0-999)。
  • getMinutes():返回分钟(0-59)。
  • getSeconds():返回秒(0-59)。
  • getTimezoneOffset():返回当前时间与 UTC 的时区差异,以分钟表示,返回结果考虑到了夏令时因素。

所有这些get*方法返回的都是整数,不同方法返回值的范围不一样。

  • 分钟和秒:0 到 59
  • 小时:0 到 23
  • 星期:0(星期天)到 6(星期六)
  • 日期:1 到 31
  • 月份:0(一月)到 11(十二月)
  • 年份:距离 1900 年的年数
1
2
3
4
5
6
7
var d = new Date("January 6, 2013");

d.getDate(); // 6
d.getMonth(); // 0
d.getYear(); // 113
d.getFullYear(); // 2013
d.getTimezoneOffset(); // -480

上面代码中,最后一行返回-480,即 UTC 时间减去当前时间,单位是分钟。-480表示 UTC 比当前时间少 480 分钟,即当前时区比 UTC 早 8 个小时。

下面是一个例子,计算本年度还剩下多少天。

1
2
3
4
5
6
function leftDays() {
var today = new Date();
var endYear = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999);
var msPerDay = 24 * 60 * 60 * 1000;
return Math.round((endYear.getTime() - today.getTime()) / msPerDay);
}

上面这些get*方法返回的都是当前时区的时间,Date对象还提供了这些方法对应的 UTC 版本,用来返回 UTC 时间。

  • getUTCDate()
  • getUTCFullYear()
  • getUTCMonth()
  • getUTCDay()
  • getUTCHours()
  • getUTCMinutes()
  • getUTCSeconds()
  • getUTCMilliseconds()
1
2
3
4
var d = new Date("January 6, 2013");

d.getDate(); // 6
d.getUTCDate(); // 5

上面代码中,实例对象d表示当前时区(东八时区)的 1 月 6 日 0 点 0 分 0 秒,这个时间对于当前时区来说是 1 月 6 日,所以getDate方法返回 6,对于 UTC 时区来说是 1 月 5 日,所以getUTCDate方法返回 5。

set 类方法

Date对象提供了一系列set*方法,用来设置实例对象的各个方面。

  • setDate(date):设置实例对象对应的每个月的几号(1-31),返回改变后毫秒时间戳。
  • setYear(year): 设置距离 1900 年的年数。
  • setFullYear(year [, month, date]):设置四位年份。
  • setHours(hour [, min, sec, ms]):设置小时(0-23)。
  • setMilliseconds():设置毫秒(0-999)。
  • setMinutes(min [, sec, ms]):设置分钟(0-59)。
  • setMonth(month [, date]):设置月份(0-11)。
  • setSeconds(sec [, ms]):设置秒(0-59)。
  • setTime(milliseconds):设置毫秒时间戳。

这些方法基本是跟get*方法一一对应的,但是没有setDay方法,因为星期几是计算出来的,而不是设置的。另外,需要注意的是,凡是涉及到设置月份,都是从 0 开始算的,即0是 1 月,11是 12 月。

1
2
3
4
5
var d = new Date("January 6, 2013");

d; // Sun Jan 06 2013 00:00:00 GMT+0800 (CST)
d.setDate(9); // 1357660800000
d; // Wed Jan 09 2013 00:00:00 GMT+0800 (CST)

set*方法的参数都会自动折算。以setDate为例,如果参数超过当月的最大天数,则向下一个月顺延,如果参数是负数,表示从上个月的最后一天开始减去的天数。

1
2
3
4
5
6
7
8
9
var d1 = new Date("January 6, 2013");

d1.setDate(32); // 1359648000000
d1; // Fri Feb 01 2013 00:00:00 GMT+0800 (CST)

var d2 = new Date("January 6, 2013");

d.setDate(-1); // 1356796800000
d; // Sun Dec 30 2012 00:00:00 GMT+0800 (CST)

set类方法和get类方法,可以结合使用,得到相对时间。

1
2
3
4
5
6
7
8
var d = new Date();

// 将日期向后推1000天
d.setDate(d.getDate() + 1000);
// 将时间设为6小时后
d.setHours(d.getHours() + 6);
// 将年份设为去年
d.setFullYear(d.getFullYear() - 1);

set*系列方法除了setTime()setYear(),都有对应的 UTC 版本,即设置 UTC 时区的时间。

  • setUTCDate()
  • setUTCFullYear()
  • setUTCHours()
  • setUTCMilliseconds()
  • setUTCMinutes()
  • setUTCMonth()
  • setUTCSeconds()
1
2
3
4
var d = new Date("January 6, 2013");
d.getUTCHours(); // 16
d.setUTCHours(22); // 1357423200000
d; // Sun Jan 06 2013 06:00:00 GMT+0800 (CST)

上面代码中,本地时区(东八时区)的 1 月 6 日 0 点 0 分,是 UTC 时区的前一天下午 16 点。设为 UTC 时区的 22 点以后,就变为本地时区的上午 6 点。

参考链接