持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
1 Electron 桌面端
1.1 实例:项目根目录下写入错误日志示例
要求:项目根目录下写入日志,文件夹和文件不存在时先创建,文件存在时在追加写入日志。
主进程js文件中:渲染进程触发主进程 saveLog 事件绑定的监听函数,从而将错误日志写入到项目目录
const { ipcMain, app } = require('electron')
const fs = require('fs')
const path = require('path')
import process from 'process'
function getPath() {
let filePath
// 开发环境写入路径
if (process.env.NODE_ENV === 'development') {
filePath = 'C:/Users/DELL/Desktop/data'
// 生产环境写入路径
} else {
filePath = path.join(app.getAppPath(), '../../data')
}
return filePath
}
function writeFile(savePath, data, e) {
fs.appendFile(savePath, data, (err) => {
if (err) {
// 向渲染进程发送消息通知失败
e.reply('monitorMain', err)
} else {
// 向渲染进程发送消息通知成功
e.reply('monitorMain', 'reply写入成功')
}
})
}
// 保存日志
// data: 记录的日志数据
ipcMain.on('saveLog', (e, data) => {
let filePath = getPath()
let fileName = 'error.log'
let writePath = `${filePath}\${fileName}`
if (fs.existsSync(writePath)) {
writeFile(writePath, data, e)
} else {
fs.mkdir(filePath, { recursive: true }, (err) => {
if (err) {
throw err
} else {
fs.writeFile(writePath, data, (err) => {
if (err) {
console.log(err)
}
})
}
})
}
})
渲染进程js文件中:触发主进程saveLog事件,接收主进程写入成功或失败的消息
const { ipcRenderer } = require('electron')
// 监听主进程错误,打印在渲染进程控制台
ipcRenderer.on('monitorMain', (e, data) => {
console.log(data);
})
// 触发主进程 saveLog 时间,写入错误日志
let errText = 'some error data'
ipcRenderer.send('saveLog', errText)
1.2 实例:将接口请求的blob,写入到public文件夹
本例要求:以pdf为例。分别向开发和生产环境下指定的路径写入pdf文件。文件夹不存在时先创建再写入pdf,pdf存在时先删除再写入。
工具函数中:将blob写入到指定路径下
const fs = require('fs')
const path = require('path')
const { app } = require('electron').remote
function writeToPreview(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.readAsDataURL(blob)
reader.addEventListener('loadend', function () {
let base64 = reader.result.split('base64,')[1]
// 将截取后的字符转化为 nodejs中的Buffer
let data = Buffer.from(base64, 'base64')
function getDirPath() {
let dirPath
// 开发环境写入路径:C:\Users\DELL\Desktop\项目名\public`\data
if (process.env.NODE_ENV === 'development') {
dirPath = path.join(__static, 'data')
// 生产环境写入路径:C:\Program Files (x86)\项目名\resources\app\data
} else {
dirPath = path.join(app.getAppPath(), 'data')
}
return dirPath
}
function writeFile(writePath, data, resolve, reject) {
fs.writeFile(writePath, data, 'binary', (err) => {
if (err) {
reject(err)
} else {
// 写入成功返回写入的路径
resolve({ writePath })
}
})
}
let dirPath = getDirPath()
fs.stat(dirPath, (err, stats) => {
let fileName = 'demo.pdf'
let writePath = `${dirPath}\${fileName}`
if (!err) {
// 走到这里, 表示目录存在
if (fs.existsSync(writePath)) {
fs.unlinkSync(writePath)
// 先删除pdf再写入
writeFile(writePath, data, resolve, reject)
} else {
// 直接写入
writeFile(writePath, data, resolve, reject)
}
} else {
// 只要目录不存在,就会走到这里, 先创建目录, 再写pdf
fs.mkdir(dirPath, { recursive: true }, (err) => {
if (err) {
throw err
} else {
writeFile(writePath, data, resolve, reject)
}
})
}
})
})
})
}
vue 组件中:
let blob = await this.$api.getBlob() // 请求 blob 文件
let {writePath} = await writeToPreview(blob) // pdf 写入成功返回写入路径
本例要点:通过 FileReader 转化成 base64,再将 base64 转化为 buffer,然后通过 fs.writeFile 写入到指定目录中。
1.3 踩坑记录
1.3.1 路径常量
__static:
electron 11 开发环境下的值:C:\Users\DELL\Desktop\项目名\public
electron 12 开发环境下的值:C:\Users\DELL\Desktop\项目名\public
electron 11 生产环境下的值:C:\Program Files (x86)\项目名\resources
electron 12 生产环境下的值:C:\Program Files (x86)\项目名\resources\app.asar
由此可见,__static 生产环境下的值变了,导致我的项目中的错误日志写入失败。
解决方法:app.getAppPath() 来获取项目路径,如下:
app.getAppPath():
开发环境下的值:C:\Users\DELL\Desktop\项目名\dist_electron
生产环境下的值:C:\Program Files (x86)\项目名\resources\app
1.3.2 自己坑自己
之前做项目时碰到了个奇怪的现象,那就是明明日志都打印出了写入成功了,可项目文件夹中就是没有日志文件。搞了我老半天,后面才发现是没有管理员权限导致的。其实本来我在 electron-builder 中的配置项都写对了,代码块如下:
win: {
requestedExecutionLevel: 'highestAvailable', // 应用程序请求执行的安全级别,highestAvailable 表示请求父进程可用的最高权限
},
这样设置打包后应用快捷图标右下角会出现一个小盾牌,双击打开应用时会出现一个弹框提示,点击确定就会以管理员权限来运行程序了。其实这样就可以很轻松的在项目中写入文件了,但是后来我看这个小盾牌不太顺眼,就百度查了下通过修改注册表的方式把小盾牌去掉了,然后就写不进去了,关键我刚开始还没意识到这是我改注册表导致的,后来好不容易想到可能是权限问题,试了下右键以管理员方式打开程序就可以了。才意识到是我原先改了注册表导致的,然后百度想改回来发现死活找不到改注册表的那个方法了。。。
作为开发人员来说,windows 系统中运行程序,尽量都用管理员方式运行,以免因为一些权限问题导致不可预料的问题出现。
1.4 常用文件操作记录
1.4.1 删除当前文件夹及子文件夹中所有文件:
// 删除文件夹中所有内容
function rmdir(filePath, callback) {
// 先判断当前filePath的类型(文件还是文件夹,如果是文件直接删除, 如果是文件夹, 去取当前文件夹下的内容, 拿到每一个递归)
fs.stat(filePath, function (err, stat) {
if (err) return console.log(err)
if (stat.isFile()) {
fs.unlink(filePath, callback)
} else {
fs.readdir(filePath, function (err, data) {
if (err) return console.log(err)
let dirs = data.map((dir) => path.join(filePath, dir))
let index = 0
!(function next() {
if (index === dirs.length) {
// 此行代码会将子文件夹和当前文件夹也一并删除,注释掉则只会删除所有文件不会删除文件夹
// fs.rmdir(filePath, callback)
} else {
rmdir(dirs[index++], next)
}
})()
})
}
})
}
持续更新中。。。
2 Web 端
2.1 格式转换
2.1.1 blob 转 base64
原理:利用fileReader的readAsDataURL,将blob转为base64
blobToBase64(blob) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => {
resolve(e.target.result);
};
// readAsDataURL
fileReader.readAsDataURL(blob);
fileReader.onerror = () => {
reject(new Error('blobToBase64 error'));
};
});
}
调用
blobToBase64(blob).then(res => {
// 转化后的base64
console.log('base64', res)
})
2.1.2 base64 转 blob
原理:利用URL.createObjectURL为 Blob 对象创建临时的URL
/**
* base64 转 URL, 原理:利用URL.createObjectURL为 Blob 对象创建临时的URL
* @param {String} b64data base64 数据
* @param {String} contentType 要转化的数据类型
* @param {Number} sliceSize 《有待学习》
*/
base64ToBlob({ b64data = '', contentType = '', sliceSize = 512 } = {}) {
return new Promise((resolve, reject) => {
// 使用 atob() 方法将数据解码
let byteCharacters = atob(b64data)
let byteArrays = []
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
let slice = byteCharacters.slice(offset, offset + sliceSize)
let byteNumbers = []
for (let i = 0; i < slice.length; i++) {
byteNumbers.push(slice.charCodeAt(i))
}
// 8 位无符号整数值的类型化数组。内容将初始化为 0。
// 如果无法分配请求数目的字节,则将引发异常。
byteArrays.push(new Uint8Array(byteNumbers))
}
let result = new Blob(byteArrays, {
type: contentType,
})
result = Object.assign(result, {
// 这里一定要处理一下 URL.createObjectURL
// 该方法生成的 URL格式: blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1
preview: window.URL.createObjectURL(result),
name: `XXX.png`,
})
resolve(result)
})
}
调用
let base64 = base64.split(',')[1]
base64ToBlob({b64data: base64, contentType: 'image/png'}).then(res => {
// 转后后的blob对象
console.log('blob', res)
})
2.1.3 图片转 base64
原理:用 CanvasRenderingContext2D.drawImage() 方法可以将图像文件写入画布,然后用 HTMLCanvasElement.toDataURL() 将 canvas 转化为 base64。
图片写入画布参考链接:juejin.cn/post/706822…。
canvas 转化为 base64:<canvas>元素的toDataURL()方法,可以将 Canvas 数据转为 base64 格式的图像。
canvas.toDataURL(type, quality)
toDataURL()方法接受两个参数。
- type:字符串,表示图像的格式。默认为
image/png,另一个可用的值是image/jpeg,Chrome 浏览器还可以使用image/webp。 - quality:浮点数,0到1之间,表示 JPEG 和 WebP 图像的质量系数,默认值为0.92。
该方法的返回值是一个 base64 格式的字符串。
调用:将<canvas>元素,转化成PNG base64
function convertCanvasToBase64(canvas) {
var image = new Image();
image.src = canvas.toDataURL('image/png');
return image;
}
调用:将 <canvas>元素转成高画质、中画质、低画质三种 JPEG 图像
var fullQuality = canvas.toDataURL('image/jpeg', 0.9);
var mediumQuality = canvas.toDataURL('image/jpeg', 0.6);
var lowQuality = canvas.toDataURL('image/jpeg', 0.3);
2.2 实例:将请求到的 Blob 下载到本地
原理:这里以原生的ajax请求为例,指定xhr.responseType = 'blob',接收到Blob 对象。将 Blob 对象 通过 window.URL.createObjectURL(blob) 转化为 URL,再将 URL 赋值给 a 标签的 link属性,调用 a.click() 即可下载。
方法和属性解释:
- baseURL:请求的基础路径
- QS.stringify 格式化请求参数的方法
调用 download即可:
// url:接口地址,
// query:请求参数, query格式: { testId: xxx, formatId: xxx}
function download(url, query) {
let xhr = new XMLHttpRequest()
xhr.open('GET', `${baseURL}${url}?${QS.stringify(query)}`, true)
// AJAX 请求时,如果指定responseType属性为blob,下载下来的就是一个 Blob 对象
xhr.responseType = 'blob'
xhr.setRequestHeader('Access-Token', storage.session.get('token'))
xhr.onload = function() {
if (this.status == 200) {
let blob = this.response
//生成URL
let href = window.URL.createObjectURL(blob)
let link = document.createElement('a')
link.download = '波形.pdf'
link.href = href
link.click()
window.URL.revokeObjectURL(href)
}
}
xhr.send(null)
}
2.3 实例:input 控件选中图片后 实现图片本地预览
方法一原理:window.URL.createObjectURL 将 blob 转化为 url 后赋值给 img.src
<!-- html部分 -->
<input type="file" id='f' />
<img id='img' style="width: 200px;height:200px;" />
<!-- js部分 -->
<script>
document.getElementById('f').addEventListener('change', function (e) {
var file = this.files[0];
const img = document.getElementById('img');
const url = window.URL.createObjectURL(file);
img.src = url;
img.onload = function () {
// 释放一个之前通过调用 URL.createObjectURL创建的 URL 对象
window.URL.revokeObjectURL(url);
}
}, false);
</script>
方法二原理:通过 2.1.1 中 blob 转 base64 后赋值给 img.src
blobToBase64(blob).then(base64 => {
const img = document.getElementById('img');
img.src = base64;
})
3 Uni-app 端
Uniapp 在国内为什么会这么火,想必也不用多做解释吧,没错就是上手快+兼容多端,即便性能和文档有点差强人意,也是叫人欲罢不能~下面就让我们进入 uniapp 的 js 世界吧。
3.1 Uni-app 中的 文件操作
在Uniapp 中操作文件要用到 h5+ app API,因为 uni-app的 js 中是不能使用 nodejs 相关的api的。且看 uniapp 官网中的解释:
uni-app的js API由标准ECMAScript的js API 和 uni 扩展 API 这两部分组成。
标准ECMAScript的js仅是最基础的js。浏览器基于它扩展了window、document、navigator等对象。小程序也基于标准js扩展了各种wx.xx、my.xx、swan.xx的API。node也扩展了fs等模块。
uni-app基于ECMAScript扩展了uni对象,并且API命名与小程序保持兼容。
uni-app 中提供了 uni.saveFile(OBJECT) 等操作文件的方法,但有时候对我们来说是不够的,比如说我们想写日志到项目目录中时,这些API 就不够我们用的了,这时候就需要 h5+ app API 登场了。在此之前先说下分区存储的概念。
3.2 分区存储(沙盒存储)
3.2.1 概念
分区存储是一种安全机制,用于防止应用读取其他应用的数据
详情:
- 每个应用程序都有自己的存储空间。
- 应用程序不能翻过自己的目录,去访问系统公共目录。
- 应用程序请求的数据都要通过权限检测,不符合要求不会被放行。
3.2.2 如果没有分区存储会导致什么
- 乱占空间 :各种各样的文件散布在磁盘的各个地方,当用户卸载应用之后,这些被遗弃的文件被滞留在原地,无人管理,占用了磁盘空间,最终结果就会导致磁盘不足
- 随意读取用户的数据
- 随意读取应用的数据
3.2.3 分区存储机制下uni-app/5+ 开发者的影响
android 9及以下
- 系统未做分区存储,除其他应用的内部存储空间不可以读写,其他任意存储目录下的资源文件都可以正常读写操作
android 10
- 仅对targetSdkVersion>=29则会开启分区存储。targetSdkVersion<29则不会有任何限制与android9及以下同理
andorid 11
- 强制执行分区存储。不允许应用读写操作非应用沙盒目录和系统公共目录下的资源文件
3.2.4 目录分析
一:系统公共目录:主要包括Downloads、Documents、Pictures 、DCIM、Movies、Music等
系统公共目录特点:
- 公共目录的文件在App卸载后,不会删除
- 如系统相册可以通过
plus.gallery.pick获取 - 拥有权限,也能通过路径直接访问
系统公共目录缺陷:
- 仅支持读取媒体文件 如:音频文件、视频文件、图片文件。其他类型文件不支持!!!!
- 创建的文件是公用的,你需要确保你的文件命名是惟一的。否则会出现名称对应不上的问题。多数重命名会文件名尾部(i++)处理
- 不可随意删减。该文件谁创建的谁才有权限删除修改。如果不是当前应用创建的文件是无权权限删改的。删减文件会被系统认为恶意操作。将会弹通知栏。告诫手机用户当前XXX应用删除了什么文件
二:应用沙盒存储(分区存储)目录:分区存储主要包括应用的公有目录和应用的私有目录
应用的公有目录:只能Dcloud的APP才能访问,其他框架的APP和原生APP无法访问此目录
在 应用公共目录 documents 中创建 testCommonEntry 目录代码:
plus.io.requestFileSystem(plus.io.PUBLIC_DOCUMENTS, (fileSystem) => {
fileSystem.root.getDirectory('testCommonEntry', {
create: true,
exclusive: false,
})
})
应用的私有目录:仅应用自身可读/写
- plus.io.PRIVATE_WWW:应用所有资源保存到此目录,仅本应用可访问。 为了确保应用资源的安全性,通常此目录只可读。
- plus.io.PRIVATE_DOC:应用私有文档目录,仅本应用可读写
3.2.5 如何让第三方读取我们开发的APP的文件
第三方 是 5+APP和5+APP 时:
- 可以通过plusAPI读取对方应用的应用公共目录,所以将图片等资源拷贝到应用公共目录下。第三方5+APP应用才有权读取该文件。进行操作业务逻辑
第三方是非 5+APP 时:
- 将图片等资源拷贝到系统公共目录下。别的三方应用才有权读取该文件。进行操作业务逻辑
3.3 h5+ app API 的 io 模块
IO模块管理本地文件系统,用于对文件系统的目录浏览、文件的读取、文件的写入等操作。通过plus.io可获取文件系统管理对象。
文档地址:www.html5plus.org/doc/zh_cn/i…
3.3.1 io 模块介绍
为了安全管理应用的资源目录,规范对文件系统的操作,5+ API在系统应用目录的基础设计了应用沙盒目录, 分为私有目录和公共目录两种类型,私有目录仅应用自身可以访问,公共目录在多应用环境时(如小程序SDK)所有应用都可访问。
- 应用私有资源目录,对应常量plus.io.PRIVATE_WWW,仅应用自身可读
- 应用私有文档目录,对应常量plus.io.PRIVATE_DOC,仅应用自身可读写
- 应用公共文档目录,对应常量plus.io.PUBLIC_DOCUMENTS,多应用时都可读写,常用于保存应用间共享文件。Number类型,固定值3,对应相对路径URL为"_documents"开头的地址。 安装包存在多个5+ App或uni-app环境时(如小程序SDK),所有5+ App或uni-app都可进行读写操作。
android端测试:将 data.json 文件下载到PUBLIC_DOCUMENTS路径下,下图是通过华为手机助手来查看的
- 应用公共下载目录,对应常量plus.io.PUBLIC_DOWNLOADS,多应用时都可读写,常用于保存下载文件。Number类型,固定值4,对应相对路径URL为"_downloads"开头的地址。 安装包存在多个5+ App或uni-app环境时(如小程序SDK),所有5+ App或uni-app都可进行读写操作。
android端测试:将 data.json 文件下载到 PUBLIC_DOWNLOADS 路径下,下图是通过华为手机助手来查看的
调用5+ API时通常需要传入文件路径,为了方便理解,分为以下类型:
- 相对路径URL,对应类型plus.io.RelativeURL,以“_”开头,用于访问5+ API定义的应用沙盒目录
- 本地绝对路径URL,对应类型plus.io.LocalURL,以“file://”开头,后面跟随系统的绝对路径,用于访问应用沙盒外的目录,如系统相册等
- 网络路径URL,对应类型plus.io.RemoteURL,以“http://”或“https://”开头,用于访问网络资源
3.3.2 requestFileSystem 方法
plus.io.requestFileSystem( type, succesCB, errorCB ): www.html5plus.org/doc/zh_cn/i…
说明:获取指定的文件系统,可通过type指定获取文件系统的类型。 获取指定的文件系统对象成功通过succesCB回调返回,失败则通过errorCB返回。
参数:
-
type:
( Number ) 必选
本地文件系统常量
可取plus.io下的常量,如plus.io.PRIVATE_DOC、plus.io.PUBLIC_DOCUMENTS等。
-
succesCB:
( FileSystemSuccessCallback ) 必选
请求文件系统成功的回调
-
errorCB: ( FileErrorCallback ) 可选 请求文件系统失败的回调
返回值: 无
3.3.3 向 应用公共下载目录 追加写入日志
// 写入文件
// writeData: 写入的内容
// fileName: 写入的文件名
fileWriter(writeData, fileName = 'data.json') {
// 请求本地系统文件对象
// plus.io.PRIVATE_DOC: 应用私有文档目录,仅应用自身可读写
// fobject: 请求到的目录或文件对象
plus.io.requestFileSystem(
plus.io.PUBLIC_DOWNLOADS,
(fobject) => {
fobject.root.getFile(fileName, { create: true }, (fileEntry) => {
//获取当前文件的路径
// let path = fileEntry.toURL()
// console.log(path)
fileEntry.createWriter(
(data) => {
//对文件进行写入操作
if (writeData) {
// 设置 writeData.length-1 后 写入操作就会从文件的末尾开始
data.seek(data.length)
data.write(writeData)
console.log('写入成功')
}
},
(e) => {
console.log('写入失败:', e)
}
)
})
},
(e) => {
console.log('requestFileSystem 调用失败:', e)
}
)
},
持续更新中。。。
3.3.4 uniapp 相关 api
图片相关操作:uniapp.dcloud.io/api/media/i…
文件相关操作:uniapp.dcloud.io/api/media/f…
长按canvas图像保存到相册:
// 将canvas图像长按保存到相册
handleLongpressWaveform() {
// console.log('handleLongpressWaveform')
uni.canvasToTempFilePath({
canvasId: 'waveforms',
fileType: 'jpg',
success: (res) => {
// 在H5平台下,tempFilePath 为 base64
// console.log(res.tempFilePath)
uni.showModal({
title: '提示',
content: '是否保存图片到相册',
success: (modalRes) => {
if (modalRes.confirm) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
duration: 2000,
})
},
})
} else if (modalRes.cancel) {
console.log('用户点击取消')
}
},
})
},
})
},
持续更新中。。。