前端写文件、格式转换等小结(含Electron,Web,Uniapp端)

774 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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。浏览器基于它扩展了windowdocument、navigator等对象。小程序也基于标准js扩展了各种wx.xx、my.xx、swan.xxAPI。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无法访问此目录

image-20220422163358936.png

在 应用公共目录 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路径下,下图是通过华为手机助手来查看的

image-20220422153056013.png

  • 应用公共下载目录,对应常量plus.io.PUBLIC_DOWNLOADS,多应用时都可读写,常用于保存下载文件。Number类型,固定值4,对应相对路径URL为"_downloads"开头的地址。 安装包存在多个5+ App或uni-app环境时(如小程序SDK),所有5+ App或uni-app都可进行读写操作。

android端测试:将 data.json 文件下载到 PUBLIC_DOWNLOADS 路径下,下图是通过华为手机助手来查看的

image-20220422153133023.png

调用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…

uniapp.dcloud.io/api/file/fi…

长按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('用户点击取消')
              }
            },
          })
        },
      })
    },

持续更新中。。。