- 微信小程序使用自定义目录(文件路径)进行下载/保存 案例(fail permission denied 解决方案)
场景描述 最近项目中有一个需要把网络文件下载下来保存到本地,然后对下载的文件进行读取,待文件不再使用后把文件进行删除的需求。当然也类似的需求还有很多,比如把小程序中的临时图片/文件永久保存下来等等,都是对文件操作的典型场景。 常见问题 在以上场景的实现过程中可能会遇到各式各样的问题,以下是比较常见的几个: 不清楚文件应该保存到哪个目录下。 fail permission denied 文件权限问题。 使用同步函数不清楚怎么获取执行结果。 API提炼 提到文件操作我们会自然而然地想到了API中的FileSystemManager相关的API,我这里用到的函数有以下几个: 下载函数 wx.downloadFile(Object object) 异步函数: FileSystemManager.access(Object object) FileSystemManager.mkdir(Object object) 同步函数: FileSystemManager.accessSync(string path) FileSystemManager.mkdirSync(string dirPath, boolean recursive) 我对同样的业务逻辑分别分别尝试了异步和同步的两种不同的方案,下面我们用一个最简单的下载保存到本地的案例来切入正题。 案例实践 一:获取正确的文件目录路径 当然在保存文件之前我们先要解决一个小问题,那就是我们要保存到哪里?也就是我们自定义的目录。这里我们简单命名其为 [代码]//自定义缓存文件根路径 var rootPath = "......"; [代码] (当然你也可以命名成其他名字) 变量名字可以随便写,不过自定义路径可不能随便写,也不可以在下载的时候直接给一个path路径,否者就会抛出没有权限或找不到文件的异常。所以开发者并不是可以随意的决定自定义文件的路径,这里就不卖关子了,小程序API中有一个很容易被忽略的API, wx.env.USER_DATA_PATH是专门获取文件系统中的用户目录路径的常量值。 这就是我们在小程序中合法的可操作文件的根目录路径: [代码]rootPath = wx.env.USER_DATA_PATH; [代码] 好了到目前为止我们已经知道了我们该往里存储文件了。 定义一下我们下载文件的缓存目录 [代码]var cachePath = rootPath+"/cache"; [代码] 也就是说之后我们下载的文件都会保存到 /cache 目录下,我们在之后的代码中用到的目录路径均为此路径。 二:异步函数实现方案 我们先来用异步函数来实现一下下载保存的流程。 1 下载文件之前我们首先要判断当前目录是否存在,如果目录不存在我们就直接下载文件到该目录下就会抛出 fail permission denied [图片] 这是判断目录存在的代码 [代码] access() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); fm.access({ path: cachePath, success: function(res) { resolve(); }, fail: function(err) { resolve(err); } }); }); }, [代码] 2 如果目录真实存在那我们当然可以直接使用,如果目录不存在则需要开发者自己创建目录。 [代码] mkdir(){ return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); fm.mkdir({ dirPath: cachePath, recursive: true, success: function(res) { resolve(); }, fail: function(err) { resolve(err); } }); }); }, [代码] 代码执行完之后我可以验证一下目录是否如我们所愿被创建出来。 开发工具右上角的详情–>基本信息–>文件系统–>当前小程序文件系统根目录 [图片] 点击usr文件夹进入根目录 [图片] 执行完上面代码之后我们可以看一下 cache 文件夹确实已经存在了 [图片] 3 目录也存在了,万事具备只欠下载了 [代码] downloadFile() { let fileUrl = 'https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg'; wx.downloadFile({ url: fileUrl, filePath: cachePath + '/temp.png', success: function(res) { console.log('downloadFile success', res); }, fail: function(err) { console.log('downloadFile fail', err); } }); }, [代码] 那执行完下载的代码之后我们再来看看cache目录 [图片] 这就是我们刚刚下载的图片。 ok,异步的整个下载和文件创建流程就走完了。接下来我们来瞅瞅同步流程中有哪些需要我们注意的。 三: 同步方案 同步方案和异步方案的流程大体一致,都是先判断文件目录是否存在,若不存在则创建目录,存在则执行下载逻辑。 使用同步函数需要特别注意的是怎么去判断函数的执行结果,由于 FileSystemManager.accessSync(string path) FileSystemManager.mkdirSync(string dirPath, boolean recursive) 这两个同步函数没有直接给出任何的返回结果。那我们怎么知道目录是否存在、目录是否被创建成功了呢? 这里我们需要剑走偏锋一下,即利用同步函数抛出的异常来判断结果。 我们直接来看代码 [代码] accessSync() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); try { fm.accessSync(cachePath); resolve(); } catch (err) { resolve(err); } }); }, mkdirSync() { return new Promise(function(resolve, reject) { let fm = wx.getFileSystemManager(); try { fm.mkdirSync(cachePath, true); resolve(); } catch (err) { resolve(err); } }); }, [代码] 可以看到我们的代码中多了 try catch 的代码结构,因为同步方法中没有直接返回给我们可用的信息,那我们可以认为同步函数正常执行完的结果为true或success,而进入 catch 后则结果为false或fail亦或根据具体异常具体处理。我们利用同步函数来走一遍下载保存的流程。 [代码] // 同步函数流程 this.accessSync().then(function (err) { if (err) { return that.mkdirSync(); } }).then(function (err) { if (!err) { that.downloadFile() } }); [代码] 可以看到我们利用同步函数下载的图片。 [图片] 总结时刻 我们分别使用异步和同步函数完成了目录的创建和文件的下载等流程。在这个过程中我们特别需要注意几点: 操作文件的根目录是以 wx.env.USER_DATA_PATH 开头的。 使用自定义目录时一定主要不可直接使用,需要增加 判断目录存在、创建目录 两个步骤。 使用同步函数时的执行结果是通过抓取同步函数抛出的异常来进行判断的。在没有给出直接结果的时候要学会利用异常信息来达到目的。 在创建目录时如果该目录存在同样会抛出异常(fail file already exists),这时按照success逻辑继续往下执行即可。 异步方案中介绍了目录查看的步骤和方法,可自行验证。其中usr目录是开发者自定义目录根节点,tmp目录是小程序默认的缓存根节点。 结尾 叙述若有不对或不严谨之处还请不吝指正。
2019-10-31 - 如何判断多行文本溢出达成的条件
需求: 未知字数的一段文本,最多显示4行; 超过四行,则文本溢出,显示展开按钮,展开后显示收起按钮 1.字数较少,不显示展开按钮 [图片] 2.超过四行,文本溢出,显示展开 [图片] 3.超出四行,展开后显示收起 [图片] 多行文本溢出代码: overflow: hidden; -webkit-box-orient: vertical; display: -webkit-box; -webkit-line-clamp: 4; text-overflow: ellipsis; 问题:文本内容达到溢出的条件怎么判断,现在是用文本的字数,比如105个字,超过105个字显示按钮,但显然不合理,求指导,谢谢
2018-03-07 - 在小程序中实现 Mixins 方案
作者:jrainlau 原文:在小程序中实现 Mixins 方案 Fundebug经授权转载,版权归原作者所有。 在原生开发小程序的过程中,发现有多个页面都使用了几乎完全一样的逻辑。由于小程序官方并没有提供 Mixins 这种代码复用机制,所以只能采用非常不优雅的复制粘贴的方式去“复用”代码。随着功能越来越复杂,靠复制粘贴来维护代码显然不科学,于是便寻思着如何在小程序里面实现 Mixins。 什么是 Mixins Mixins 直译过来是“混入”的意思,顾名思义就是把可复用的代码混入当前的代码里面。熟悉 VueJS 的同学应该清楚,它提供了更强大了代码复用能力,解耦了重复的模块,让系统维护更加方便优雅。 先看看在 VueJS 中是怎么使用 Mixins 的。 [代码]// define a mixin object var myMixin = { created: function () { this.hello() }, methods: { hello: function () { console.log('hello from mixin!') } } } // define a component that uses this mixin var Component = Vue.extend({ mixins: [myMixin] }) var component = new Component() // => "hello from mixin!" [代码] 在上述的代码中,首先定义了一个名为 [代码]myMixin[代码] 的对象,里面定义了一些生命周期函数和方法。接着在一个新建的组件里面直接通过 [代码]mixins: [myMixin][代码] 的方式注入,此时新建的组件便获得了来自 [代码]myMixin[代码] 的方法了。 明白了什么是 Mixins 以后,便可开始着手在小程序里面实现了。 Mixins 的机制 Mixins 也有一些小小的细节需要注意的,就是关于生命周期事件的执行顺序。在上一节的例子中,我们在 [代码]myMixin[代码] 里定义了一个 [代码]created()[代码] 方法,这是 VueJS 里面的一个生命周期事件。如果我们在新建组件 [代码]Component[代码] 里面也定义一个 [代码]created()[代码] 方法,那么执行结果会是如何呢? [代码]var Component = Vue.extend({ mixins: [myMixin], created: function () { console.log('hello from Component!') } }) var component = new Component() // => // Hello from mixin! // Hello from Component! [代码] 可以看运行结果是先输出了来自 Mixin 的 log,再输出来自组件的 log。 除了生命周期函数以外,再看看对象属性的混入结果: [代码]// define a mixin object const myMixin = { data () { return { mixinData: 'data from mixin' } } } // define a component that uses this mixin var Component = Vue.extend({ mixins: [myMixin], data () { return { componentData: 'data from component' } }, mounted () { console.log(this.$data) } }) var component = new Component() [代码] [图片] 在 VueJS 中,会把来自 Mixins 和组件的对象属性当中的内容(如 [代码]data[代码], [代码]methods[代码]等)混合,以确保两边的数据都同时存在。 经过上述的验证,我们可以得到 VueJS 中关于 Mixins 运行机制的结论: 生命周期属性,会优先执行来自 Mixins 当中的,后执行来自组件当中的。 对象类型属性,来自 Mixins 和来自组件中的会共存。 但是在小程序中,这套机制会和 VueJS 的有一点区别。在小程序中,自定义的方法是直接定义在 Page 的属性当中的,既不属于生命周期类型属性,也不属于对象类型属性。为了不引入奇怪的问题,我们为小程序的 Mixins 运行机制多加一条: 小程序中的自定义方法,优先级为 Page > Mixins,即 Page 中的自定义方法会覆盖 Mixins 当中的。 代码实现 在小程序中,每个页面都由 [代码]Page(options)[代码] 函数定义,而 Mixins 则作用于这个函数当中的 [代码]options[代码] 对象。因此我们实现 Mixins 的思路就有了——劫持并改写 [代码]Page[代码] 函数,最后再重新把它释放出来。 新建一个 [代码]mixins.js[代码] 文件: [代码]// 保存原生的 Page 函数 const originPage = Page Page = (options) => { const mixins = options.mixins // mixins 必须为数组 if (Array.isArray(mixins)) { delete options.mixins // mixins 注入并执行相应逻辑 options = merge(mixins, options) } // 释放原生 Page 函数 originPage(options) } [代码] 原理很简单,关键的地方在于 [代码]merge()[代码] 函数。[代码]merge[代码] 函数即为小程序 Mixins 运行机制的具体实现,完全按照上一节总结的三条结论来进行。 [代码]// 定义小程序内置的属性/方法 const originProperties = ['data', 'properties', 'options'] const originMethods = ['onLoad', 'onReady', 'onShow', 'onHide', 'onUnload', 'onPullDownRefresh', 'onReachBottom', 'onShareAppMessage', 'onPageScroll', 'onTabItemTap'] function merge (mixins, options) { mixins.forEach((mixin) => { if (Object.prototype.toString.call(mixin) !== '[object Object]') { throw new Error('mixin 类型必须为对象!') } // 遍历 mixin 里面的所有属性 for (let [key, value] of Object.entries(mixin)) { if (originProperties.includes(key)) { // 内置对象属性混入 options[key] = { ...value, ...options[key] } } else if (originMethods.includes(key)) { // 内置方法属性混入,优先执行混入的部分 const originFunc = options[key] options[key] = function (...args) { value.call(this, ...args) return originFunc && originFunc.call(this, ...args) } } else { // 自定义方法混入 options = { ...mixin, ...options } } } }) return options } [代码] Mixins 使用 在小程序的 [代码]app.js[代码] 里引入 [代码]mixins.js[代码] [代码]require('./mixins.js') [代码] 撰写一个 [代码]myMixin.js[代码] [代码]module.exports = { data: { someData: 'myMixin' }, onShow () { console.log('Log from mixin!') } } [代码] 在 [代码]page/index/index.js[代码] 中使用 [代码]Page({ mixins: [require('../../myMixin.js')] }) [代码] [图片] 大功告成!此时小程序已经具备 Mixins 的能力,对于代码解耦与复用来说将会更加方便。
2019-06-28 - 小程序奇技淫巧之 -- globalDataBehavior管理全局状态
Behaviors 自定义组件中,提供了[代码]behaviors[代码]的使用和定义。 从官方文档我们能看到: [代码]behaviors[代码]是用于组件间代码共享的特性,类似于一些编程语言中的“mixins”或“traits”。 每个[代码]behavior[代码]可以包含一组属性、数据、生命周期函数和方法,组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每个组件可以引用多个[代码]behavior[代码]。 简单来说,我们能通过[代码]behaviors[代码]来重构[代码]Component[代码]的能力。Behavior的用处很多,前面也有介绍 computed 计算属性、watch 观察属性的实现,都是使用的 Behavior。 全局状态管理 我们希望全局共享一些数据状态,如果只是通过一个文件的方式进行维护,那么我们无法在状态更新的时候及时地同步到页面。我们需要额外调用 setData 才能更新页面中的 data 数据,才能告诉渲染层这块的数据渲染需要变更,而很多的 Store 状态管理库也是通过这样的方式实现的(事件通知 + setData + 全局状态)。 在小程序 Behavior 能力的支持下,我们可以通过一个全局的 globalData Behavior 注入到每个需要用到的 Component 中,这样就可以在需要的页面中直接引入该 Behavior,就能获取到了。不啰嗦,Behavior的实现如下: [代码]// globalDataStore 用来全局记录 globalData,为了跨页面同步 globalData 用 export let globalDataStore = {}; // 获取本地的 gloabalData 缓存 try { const gloabalData = wx.getStorageSync("gloabalData"); // 有缓存的时候加上 if (gloabalData) { globalDataStore = { ...gloabalData }; } } catch (error) { console.error("gloabalData getStorageSync error", "e =", error); } // globalCount 用来全局记录 setGlobalData 的调用次数,为了在 B 页面回到 A 页面的时候, // 检查页面 __setGlobalDataCount 和 globalCount 是否一致来判断在 B 页面是否有 setGlobalData, // 以此来同步 globalData let globalCount = 0; export default Behavior({ data: { globalData: Object.assign({}, globalDataStore) }, lifetimes: { attached() { // 页面 onLoad 的时候同步一下 globalCount this.__setGlobalDataCount = globalCount; // 同步 globalDataStore 的内容 this.setData({ globalData: Object.assign( {}, this.data.globalData || {}, globalDataStore ) }); } }, pageLifetimes: { show() { // 为了在 B 页面回到 A 页面的时候,检查页面 __setGlobalDataCount 和 globalCount 是否一致来判断在 B 页面是否有 setGlobalData if (this.__setGlobalDataCount != globalCount) { // 同步 globalData this.__setGlobalDataCount = globalCount; this.setGlobalData(Object.assign({}, globalDataStore)); } } }, methods: { // setGlobalData 实现,主要内容为将 globalDataStore 的内容设置进页面的 data 的 globalData 属性中。 setGlobalData(obj: any) { globalCount = globalCount + 1; this.__setGlobalDataCount = this.__setGlobalDataCount + 1; obj = obj || {}; let outObj = Object.keys(obj).reduce((sum, key) => { let _key = "globalData." + key; sum[_key] = obj[key]; return sum; }, {}); this.setData(outObj, () => { globalDataStore = this.data.globalData; }); }, // setGlobalDataAndStorage 实现,先调用 setGlobalData,然后存到 storage 里 setGlobalDataAndStorage(obj: any) { this.setGlobalData(obj); try { let gloabalData = wx.getStorageSync("gloabalData"); // 有缓存的时候加上 if (gloabalData) { gloabalData = { ...gloabalData, ...obj }; } else { gloabalData = { ...obj }; } wx.setStorageSync("gloabalData", gloabalData); } catch (e) { console.error("gloabalData setStorageSync error", "e =", e); } } } }); [代码] 显然,该 Behavior 主要提供了几个能力: 会在小程序 data 添加 globalData 的属性,在 WXML 文件中可以直接通过[代码]{{globalData.xxxx}}[代码]获取到 提供[代码]setGlobalData()[代码]方法,用于更新全局状态 提供[代码]setGlobalDataAndStorage()[代码]方法,用于更新全局状态,同时写入缓存(会在下次启动应用的时候自动获取缓存数据) 这样,我们在初始化 Component 的时候直接引入就可以使用: [代码]Component({ // 在behaviors中引入globalDataBehavior behaviors: [globalDataBehavior], // 其他选项 methods: { test() { // 使用this.setGlobalData可以更新全局的数据状态 this.setGlobalData({ test: "hello world" }); // 使用this.setGlobalDataAndStorage可以更新全局的数据状态,并写入缓存 // 下次globalDataBehavior会默认从缓存中获取 this.setGlobalDataAndStorage({ test: "hello world" }); } } }); [代码] 在引入了 globalDataBehavior 之后,我们的 WXML 就可以直接使用了: [代码]<view>{{ globalData.test }}</view> [代码] 页面如何使用 Behavior [代码]Component[代码]是[代码]Page[代码]的超集,因此可以使用[代码]Component[代码]构造器构造页面。 看看官方文档:事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应[代码]json[代码]文件中包含[代码]usingComponents[代码]定义段。 更详细的使用方法,在 computed 计算属性、watch 观察属性两篇文章中也有描述,大家可以自行参考。 或者直接查看最终的项目代码:wxapp-typescript-demo。 参考 Component构造器 behaviors 结束语 Behavior 其实是很强大的一个能力,我们能用它来对自己的小程序做很多的能力拓展,缺啥补啥,还可以“混入”给每个 Component 每个方法打入日志,就不用每个组件自己手动打印代码拉。
2019-12-10