- 自动更新机制
本文是我的小程序开发日记的其中一篇, GitHub 原文地址 欢迎star,感谢万分! 前言 小程序的更新机制与它的运行机制有关。 为了保证用户能尽可能快得打开小程序,只会在后台更新,不会主动等待更新完毕才进入最新版小程序。 运行机制 首先,先看下小程序的运行机制: [图片] 与APP的概念有些类似,初次打开即为冷启动,若启动之后,在被系统回收之前再次打开,则称之为热启动。 更新机制 小程序的更新机制分为: 未启动时更新 启动时更新 未启动时更新:意味着微信客户端会在用户不在访问小程序期间,主动触发更新,最慢24小时内覆盖所有用户。如果用户在未覆盖期间进入小程序,则触发了启动时更新。 启动时更新:用户冷启动进入小程序时,均会检测小程序是否有更新版本,若有则后台默默更新,准备为下次冷启动时使用。需要注意的是,此时访问的仍是旧版本的小程序。如果此时想手动使用新版小程序,则可以使用官方API: [代码]const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate(function (res) { // 请求完新版本信息的回调 console.log(res.hasUpdate) }) updateManager.onUpdateReady(function () { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success(res) { if (res.confirm) { // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 updateManager.applyUpdate() } } }) }) updateManager.onUpdateFailed(function () { // 新版本下载失败 }) [代码] 如若用户是第一次打开小程序(即新用户),则会直接打开最新版本的小程序。此时不需要考虑更新机制。 根据微信提供的能力,小程序的更新流程大致如下: [图片] 由于官方API没有提供主动下载新版本小程序的能力,仅提供了检测的能力。因此,当新版本下载失败时,没法主动触发重试,只能让用户继续访问旧版本的小程序。 下载失败之后,小程序的重试机制不得而知。可能需要等待小程序被销毁之后,再次冷启动时才会再次主动更新;又或者等待24小时之后。 由于可能存在下载新版本失败的用户,因此小程序的后端服务需要考虑向后兼容。另外,可以在下载失败的回调函数里加入数据统计,用于计算更新失败的概率。 测试 更新机制的测试工作比较麻烦,因为可能要上生产环境测试,风险极大。 笔者尝试在体验版上做测试:先打开[代码]v0.0.1[代码]版本的小程序,然后在开发者工具上传新的版本,再通过最近访问的列表里再次打开小程序,结果发现直接打开的就是[代码]v0.0.2[代码],根本没有还原小程序的更新机制。 因此可以得出结论:体验版无法测试更新机制。 模拟更新 另外,开发者工具的编译模式提供模拟更新: [图片] 兼容处理 由于存在用户访问旧版小程序的可能,因此与后端的接口设计需要特别关注,尤其是在更新接口时,如果没有做到向后兼容,则会出现旧前端访问新后端的现象,从而产生不可预期的后果。 最简单的方式:每次升级接口时,均采用新接口。
2020-05-08 - 优秀开源项目推荐-基于云开发的英文单词对战小程序
在我开始之前,我首先要声明我并不是这个开源项目的开发者/维护者,因此,大家不要太信任我的观点。我确实非常深入地研究了这个项目的代码实现,但是无论如何我也不能保证能跟开发者保持一致。话虽如此,我已经用源码来支持我的观点,并尝试着使我的论点尽可能的真实。 本文背景 是这样的,我最近在开发双人对战答题,在参考git上一些好的开源项目的时候发现了这个小程序,目前这个小程序完全开源的,如果对这个对战模式感兴趣可以学习下。 本文内容 本文介绍一款优秀的开源项目推荐优秀的开源项目推荐基于云开发的英文单词对战小程序 项目介绍 https://juejin.im/post/6844904136215887880 项目地址 https://github.com/arleyGuoLei/wx-words-pk 一下内容摘自开源项目readme 单词天天斗微信小程序云开发实现的单词PK小程序,支持好友对战、随机匹配、人机模式,完整代码,可以直接部署阅览 ~ UI可以披靡市场上所有同类型小程序,体验也是一流的哦 ~ 目前已经有同学在QQ小程序、阿里小程序部署;也有同学修改成了[代码]公务员题库[代码] ~ 期待看到各类优秀产品上线哦 ~ 部署文档: 源码目录下 - 部署文档.md 如果觉得这个文档比较长,可以查看源码目录下 - 精简核心文档.md 上线说明: 源码开源,但上线需要经过作者许可哦 ~ 开发不易、创作不易。需要支付RMB [代码]66+[代码]方可上线,保障作者著作权益 ~ 如果你觉得项目对你有所帮助 ~ 期待得到你的打赏哦 在线体验[图片] UI截图[图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] [图片] 需求概述[图片] 单词对战模式 对战业务需求解析单词对战的游戏核心为:随机生成一定数量的单词列表的单选题类型题目,题目文本为该单词,有 4 个随机中文释义的选项,其中仅有一个为正确释义,双方用户一起选择释义,正确率高且速度快的用户获得对战胜利。 单词对战游戏分为好友对战、随机匹配、人机对战三种对战的形式,均通过上述游戏核心的方式进行对战。 对战设置用户还可以对以下对战信息进行自定义设置 对战的单词书,用户可以选择自己想要背诵的单词类型,包含四级核心词、四级大纲词、六级核心词、六级大纲词、考研真题核心词、考研大纲词、小学必备词、中考大纲词、高考大纲词、雅思大纲词、商务词汇等多种单词书,亦可以选择随机单词书模式,则将从所有的单词中进行随机抽取;设置每一局对战的单词数目为以下任意一种:8、 10(默认)、 12、 15、 20设置切换下一题是否自动播放单词发音设置错词是否加入到生词本开始和错词的时候是否震动设置默认是否播放背景音乐,游戏中也可以随时关闭/开启背景音乐 其他细节优化加入[代码]正在对战过程中[代码]、[代码]对战已结束[代码]、[代码]房间已满[代码]等非正常类型房间,做出相应的交互提示,然后跳转至首页在对战过程中任意用户退出游戏或掉线,则结束本局游戏,进行对战结算对战结束后,房主可以选择再来一局,当房主创建好再来一局的房间后,另外一个用户可以选择再来一局,加入继续对战在对战过程中,选择错误的单词或使用提示卡选择的单词,自动加入到用户生词本,用户可以在生词本中进行复习加入倒计时机制,每一个单词的对战周期为 10s,超时则判断为错选 完整对战流程图[图片] 词汇挑战模式 词汇挑战模式业务解析词汇挑战的核心为:获取随机的一个单词作为单选题题目文本,包含四个中文释义选项,其中一个为正确答案,选择错误则失败,选择正确再获取随机单词,循环下去。 挑战复活机制在词汇挑战的过程中,如果选择错误,可以有两次复活机会 首次复活:通过分享小程序获得复活机会第二次复活:通过观看一个 15s 之内的广告获得复活机会当第三次选择错误,显示再来一局,从零开始记录分数 其他词汇挑战每正确一个词,得分增加 100 分当挑战失败的时候,如果挑战分数高于历史最高分数,则修改历史最高分数为当前分数,用于排行榜排行可以使用提示卡进行选择 完整挑战流程图[图片] 其他功能 生词本用户可以在生词本中查看在单词对战模式、词汇挑战模式中选择错误的单词可以查看单词及单词释义、播放单词发音、删词生词在设置中可以一键清空所有生词 学习打卡当在单词对战模式中,当天对战局数超过 5 局且胜利局数超过 2 局,则打卡成功可以在在打卡页面查看当日进度,可以查看历史的打卡日历 排行榜排行榜包含词力值、词汇挑战分数、签到天数等排名信息每类排行版显示前 20 名的排名头像和昵称以及分数显示自己当前类目下的排名以及分数 用户相关数据库应记录的用户数据包含:昵称、头像、对战局数、胜利局数、选择的单词本、词力值词力值机制:在单词对战模式、单词挑战模式中,每局对战都可以获得相应的词力值分数,作为用户的经验值 其他建议反馈:用户可以在小程序中,反馈意见,然后再后台可以查看用户留言打赏作者:用户可以在小程序中,通过扫码的形式,对小程序进行打赏小程序友情链接:可通过当前小程序跳转至作者的其他小程序中小程序中加入部分广告,不影响用户体验 团队组成整个项目的产品方案、UI 设计、开发、测试、上线运营等皆一个人独立完成。 技术方案 设计设置使用sketch完成,设计稿上传至[代码]蓝湖[代码],作为数据标注。 蓝湖链接链接:https://lanhuapp.com/url/qe2Dl 密码: ydIX 设计图源文件[图片] [图片] [图片] [图片] 下载链接: https://pan.baidu.com/s/1KsZjvlTUbtyYFDcVCy91lg 密码:vylm 开发技术栈前端:原生微信小程序服务端:微信小程序云开发 其他工具ESLintGit + GithubvscodeElectronNodeJSPython 系统架构 项目文件简介├── cloudfunctions # 云开发_云函数目录 | ├── model_auto_sign_trigger # 自动签到定时触发器 | ├── model_book_changeBook # 改变单词书 | ├── model_userWords_clear # 清除用户生词 | ├── model_userWords_get # 获取用户生词 | └── model_user_getInfo # 获取用户信息 ├── db # 数据整理的脚本 ├── design # 设计稿文件、素材文件 | └── words-pk-re.sketch # 设计稿 ├── docs # 项目文档 ├── miniprogram # 小程序前端目录 | ├── app.js # 小程序全局入口 | ├── app.json # 全局配置 | ├── app.wxss # 全局样式 | ├── audios # 选词正确错误的发音 | | ├── correct.mp3 | | └── wrong.mp3 | ├── components # 全局组件 | | ├── header # header组件 | | ├── loading # 全局loading | | └── message # 全局弹窗 | ├── images | | ├── ... 图片素材 | ├── miniprogram_npm # 小程序npm目录 | | └── wxapp-animate # 动画库 | ├── model # 所有的数据库操作 | | ├── base.js # 基类,所有集合继承该基类 | | ├── book.js # 单词书集合 | | ├── index.js # 导出所有数据库操作 | | ├── room.js # 房间集合 | | ├── sign.js # 签到集合 | | ├── user.js # 用户集合 | | ├── userWord.js # 生词表集合 | | └── word.js # 单词集合 | ├── pages # 页面 | | ├── combat # 对战页 | | ├── home # 首页 | | ├── ranking # 排行榜 | | ├── setting # 设置页 | | ├── sign # 签到页 | | ├── userWords # 生词表页 | | └── wordChallenge # 单词挑战 | └── utils | ├── Tool.js # 全局工具类,放了加载、全局store等 | ├── ad.js # 广告 | ├── log.js # 日志上报 | ├── router.js # 全局路由 | ├── setting.js # 全局设置 | └── util.js # 全局工具函数 ├── package.json └── project.config.json # IDE设置、开发设置 云开发数据交互的 Model 层设计在该项目中,将所有的服务端交互、数据库的读取、云函数的调用都放到了 model 目录下,对该目录结构深入解析。 (1) Base.jsbase 基类,所有其他数据集合都继承该类,在构造函数中,用来做数据集合初始化和生命一些可能所需用到的变量。 import $ from './../utils/Tool' const DB_PREFIX = 'pk_' export default class { constructor(collectionName) { const env = $.store.get('env') const db = wx.cloud.database({ env }) this.model = db.collection(`${DB_PREFIX}${collectionName}`) this._ = db.command this.db = db this.env = env } get date() { return wx.cloud.database({ env: this.env }).serverDate() } /** * 取服务器偏移量后的时间 * @param {Number} offset 时间偏移,单位为ms 可+可- */ serverDate(offset = 0) { return wx.cloud.database({ env: this.env }).serverDate({ offset }) } } (2)其他集合文件 (model 目录下,除了 base 和 index 之外的文件)在这些文件中,对应和文件名同名的集合的所有数据操作,比如 book.js 中,包含了所有对 pk_book 集合的所有数据增删改查操作。 import Base from './base' import $ from './../utils/Tool' const collectionName = 'book' /** * 权限: 所有用户可读 */ class BookModel extends Base { constructor() { super(collectionName) } async getInfo() { const { data } = await this.model.get() return data } async changeBook(bookId, oldBookId, bookName, bookDesc) { if (bookId !== oldBookId) { const { result: bookList } = await $.callCloud('model_book_changeBook', { bookId, oldBookId, bookName, bookDesc }) return bookList } } } export default new BookModel() (3)index.js在该文件中,对所有的数据集合操作文件进行引入,然后又导出,之后在其他文件中的的调用,就只需要引入该文件即可,就可以实现调用不同的集合操作。 import userModel from './user' import bookModel from './book' import wordModel from './word' import roomModel from './room' import userWordModel from './userWord' import signModel from './sign' export { userModel, bookModel, wordModel, roomModel, userWordModel, signModel } 环境区分在小程序初始化的时候,对云开发环境进行了全局的初始化,区别开发环境和正式环境。 // app.js initEnv() { const envVersion = __wxConfig.envVersion const env = envVersion === 'develop' ? 'dev-lkupx' : 'prod-words-pk' // 'prod-words-pk' // ['develop', 'trial', 'release'] wx.cloud.init({ env, traceUser: true }) this.store.env = env }, onLaunch() { this.initEnv() this.initUiGlobal() }, 难点解析 难点 1:单词数据 1. 抓包分析和代码实现本课题中使用 MacOS 系统、Charles 抓包软件、安卓手机作为抓包的基本环境。首先在电脑上安装 Charles,然后开启 Proxy 抓包代理,同局域网下配置手机 WiFi 代理实现抓取手机包。 2. 单词数据整理通过爬虫下来的单词数据如下,对于该课题的项目单词数据相对复杂,所以我们对单词数据结构进行简化,只提取项目中需要的字段,以单词 yum 为例: 优化前: {"wordRank":63,"headWord":"yum","content":{"word":{"wordHead":"yum","wordId":"PEPXiaoXue4_2_63","content":{"usphone":"jʌm","ukphone":"jʌm","ukspeech":"yum&type=1","usspeech":"yum&type=2","trans":[{"tranCn":"味道好","descCn":"中释"}]}}},"bookId":"PEPXiaoXue4_2"} 优化后: {"rank":286,"word":"yum","bookId":"primary","_id":"primary_286","usphone":"jʌm","trans":[{"tranCn":"味道好"}]} 通过 NodeJS 编写批量格式整理的程序,整理后导出 JSON 文件 [图片] 3. 数据文件批量导入(传入数据库)由于微信小程序云开发控制台不支持数据文件的批量导入数据库,所以开发了一个支持云开发数据集合批量导入的程序 [图片] [图片] [图片] 数据库批量导入程序更多解析:https://juejin.im/post/5e2bf3e4f265da3e4244ea7f程序代码开源:https://github.com/arleyGuoLei/wxcloud-databases-import 难点 2:单词对战模式本节详细解析单词对战模式的实现,将从创建房间(生成随机词汇、新增房间数据)、对战监听、对战过程(好友对战、随机匹配、人机对战)、对战结算的角度进行分析。 创建对战房间对战房间的创建,分为触发创建房间事件、获取当前选择的单词书、获取单词对战每一局的词汇数量、从数据库 pk_word 集合读取随机单词、格式化获取的随机单词列表、创建房间(使用生成的单词列表、是否好友对战条件)、根据房间的 roomId(主键)跳转至对战页等多个步骤流程组成。 [图片] 房间数据监听单词对战模式中,对 room 数据集合的监听是对战的核心要点,进入对战页面后,调用数据集合的 WatchAPI 对 room 集合中的当前房间记录进行监听,在当前房间记录数据发生变化的时候,将会调用 watch 函数的回调,执行相应的业务,详细流程如下: [图片] 好友对战的实现有了前面创建好的对战房间,也建立好了对当前房间的数据监听,接下来就可以实现有趣的对战交互了。游戏会监听好友用户准备,更新 room 集合中的 right.openid 字段,触发 watch,通知房主可以开始对战;房主点击开始对战,会更新 room 集合中的 state 字段为 PK,watch 回调通知双方开始对战,显示第一道题目,双方用户选择释义的时候,会把选择结果和得分更新至 left/right 中的 grades 和 gradeSum 字段,在 watch 的回调中对双方的选择结果进行显示;当对战到达最后一道题目,且双方都选择完毕,进入结算流程,将房间 state 更新至 finish;如果在对战过程中,有任意用户离开对战,将修改房间 state 为 leave;对战结束之后,房主可以选择再来一局,进行创建房间,更新上一个房间的 nextRoomId 字段,在 watch 回调中通知非房主用户可以加入新的房间,进行再来一局的对战。 [图片] 随机匹配的实现随机匹配对战相对于好友对战的区别在于:好友对战是通过房主将房间链接(roomId)分享到微信好友/微信群,当用户点击分享卡片之后,会跳转至对战页面且房间 Id 为当前分享的房间 roomId,用户进入房间之后就进行上述的监听操作和准备、开始对战等。然而随机匹配的实现原理为,当用户触发随机匹配操作之后,会先在数据库检索有没有符合自己所选择的单词书、目前房主在等待的房间,如果有则加入该房间,如果没有则创建新的随机匹配房间,等待其他用户进入。用户进入之后会自动触发准备操作,房主在 watch 中监听到有用户准备,然后自动触发开始对战操作,后续对战、结算、再来一局流程则和好友对战流程一致。 [图片] 人机对战的实现人机对战的核心思想为:房主用户端随机取一名人机用户,房主端触发人机的自动准备,房主端也自动开始对战,在对战过程中,房主端通过页面 UI 用户手动选词,人机将在 2~5s 或房主选词之后随机完成选词操作,正确率为 75%。 后期可以对正确率进行优化,根据用户的历史正确率进行自动化推算,实现更智能的人机用户,提供更好的用户体验。 [图片] 最后通过 3 个月的开发、功能迭代和运营,目前拥有2600 多的用户量,小程序用户打分为5.0 满分。创建房间且完成对战12000 多局,收录词汇25960个,收录了用户65000多个生词,十分感谢这个项目带给我的成就感。
2020-09-02 - 小程序开发过程中遇到的那些事儿
一、使用wx.getLaunchOptionsSync()这个Api时的注意事项1.该Api获取的是小程序 冷 启动时的参数,与App.onLaunch的回调参数一致。 2.要再更仔细的强调的话,就是说,如果在小程序冷启动后,业务逻辑牵扯到新的启动参数的话,不能用这个Api获取,得拿App.onShow的回调参数。 二、小程序setData给一个数组中的某一对象的一个属性赋值 data: { list: [ { spu_name: "欧莱雅小蜜罐金致臻颜花蜜奢侈痒女补", is_check: true }, { spu_name: "水保湿滋润抗皱面妆欧莱", is_check: false } ] }, let index = 1 let = 'list[' + index + '].is_check' this.setData({ [s]: true }) 三、小程序canvas层级太高,业务逻辑可选的处理方式:方案一、如果要在canvas之上写模块(eg: 弹框,页面顶部的自定义导航栏),该模块所有的view标签换成cover-view标签,这种方法能解决简单的模块兼容,具体cover-view的使用以及坑点可参考微信官方文档:https://developers.weixin.qq.com/miniprogram/dev/component/cover-view.html。 方案二、在canvas之上显示一个弹框时,可以使用wx:if暂时隐藏该canvas,弹框再次隐藏时重新显示canvas。 四、业务逻辑中遇到授权button要阻止冒泡,如何处理授权button是有特定的属性获取授权信息,我们一般阻止冒泡一般用catchtap方法,那么授权类型的button在这种情况下是用不了catchtap的,因此,这个需要一个新的解决方案: [图片] 方案一、用一个view标签将这个button包起来,在这个view标签上写上catchtap,对应的方法写成空的即可。 class="cart-box" catchtap="preventBubble"> if="{{!isAuth}}" class="cart" open-type="getUserInfo" bindgetuserinfo="goAuth"> class="iconfont icon-jiagouwuche font-25 icon"> else class="cart" bindtap="addCart"> class="iconfont icon-jiagouwuche font-25 icon"> 五、使用wx.showTabBarRedDot、wx.hideTabBarRedDot这两个Api时的注意事项 1.开发时得注意,要在tab页面使用这些Api,不要在非tab页面使用,否则console会报错。 六、好玩的小程序开发调试利器之一-AppDataAppData panel 用于显示当前项目运行时小程序 AppData 具体数据,实时地反映项目数据情况,可以在此处编辑数据,并及时地反馈到界面上。 话不多说,体验效果如下: [图片] [图片] [图片] 七、好玩的小程序开发调试利器-自动预览自动预览可以实现编写小程序时快速预览,免去了每次查看小程序效果时都要扫码或者使用小程序助手的麻烦。开发者只需按下快捷键,保持前台运行的微信即可自动唤出或刷新小程序。 要使用自动预览功能,需要配合 6.6.7 及以上的微信客户端版本。 [图片] 八、好玩的小程序开发调试利器-多账号调试通过 菜单 - 工具 - 多帐号调试 可以使用多帐号调试功能,这是做助力活动(需要多人助力)的开发者的福音。 [图片]
2020-08-25 - 小程序登录的演变过程
1. 初版登录存在问题: 并发请求会有多次登录[图片]2. 存储promise[图片] 3. 使用存储promise优化存在问题: wx.checksession个别时候会出现fasle,原因未知无网络提示逻辑[图片] 4. 添加全局控制&校验网络存在问题: 无登录过期处理逻辑[图片] 5. 添加登录过期逻辑还可优化: 页面跳转预加载接口重试逻辑[图片] 6. 添加页面预加载相关[图片] 7. 完整流程[图片]
2020-09-02 - 微信小程序开发技巧总结(二) -- 文件的选取、移动、上传和下载
微信小程序开发技巧总结(二) – 文件的选取、移动、上传和下载 1.不同类型文件的选取 1.1 常用的图片 视频 对于大部分开发者来说,需要上传的文件形式主要为图片,微信为此提供了接口。 [代码]wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success (res) { // tempFilePath可以作为img标签的src属性显示图片 const tempFilePaths = res.tempFilePaths } }) [代码] 其次为视频文件的选取,微信也为此提供了接口。 [代码]wx.chooseVideo({ sourceType: ['album','camera'], maxDuration: 60, //视频长度 单位 s camera: 'back', //选取前置 摄像 还是 后置 摄像 success(res) { console.log(res.tempFilePath) } }) [代码] 也为其提供了视频和图片的二合一接口,这个接口不建议调用,图片和视频的上传建议区分开。 [代码]wx.chooseMedia({ count: 9, mediaType: ['image','video'], sourceType: ['album', 'camera'], maxDuration: 30, camera: 'back', success(res) { console.log(res.tempFilePath)con console.log(res.size) } }) [代码] 这几个接口调用成功的回调函数中,都返回的是文件在文件在本机中的路径。 [代码]res.tempFilePath [代码] 这是一个 数组,存放着选择的所有文件的路径,用于上传。 1.2 其余形式各种文档 那么如果是想要在小程序中上传 doc、pdf、ppt 等类型的文件怎么处理?首先要注意的是微信小程序并没有给用户提供文件管理器接口。 开发者:我想要个文件管理器接口! 官方:不,你不想 聪明的开发者他没有办法,只能另辟蹊径。微信提供了一个选择客户端会话文件的方法。 [代码]wx.chooseMessageFile({ success(res){ console.log(res.tempFilePath) } }) [代码] 与上面两个接口相同,返回的也是文件在本地的存储路径,但是不同的是,这个接口可以选取全部的文件类型。 开发者如果想要上传非图片和视频内容的话,需要两步走。 打开微信文件传输助手,将想要上传的文件发送到文件传输助手 在小程序中调用这个接口,选择文件传输助手,从会话中选择想要上传的文件。 [图片] [图片] 2.文件的上传 2.1 uploadFile方法 所有的文件都是以字节流的形式进行上传,所以上传形式并没有什么本质区别,都是调用相应的接口进行上传。 小程序端写法如下: [代码]wx.uploadFile({ url: '你的服务器函数地址', //仅为示例,非真实的接口地址 filePath: '需要上传的文件路径', //res.tempFilepaths name: 'file', // 文件对应的key ,默认 为 file formData: { 'user': 'test' }, //上传额外携带的参数 success (res){ const data = res.data //do something } }) [代码] 2.2 服务器端如何处理上传的文件 服务端如何接受文件的上传?仅展示Java方式(SpringBoot 框架) [代码]@Controller @ResponseBody public class FileController { //文件上传控制类,是核心配置类,Win <->Linux @RequestMapping(value = "/upload/images") public String uploadimages(HttpServletRequest request,@RequestParam("file") MultipartFile file, @RequestParam("user") String user) throws IOException { //更换服务器,这个值也需要修改 //图片上传写法 //type 是上传图片的类型 if(!file.isEmpty()){ //文件不为空 //String path = "E:"+File.separator+"images"+File.separator+type; // this is windows method String path = "/share"+File.separator+"images"+File.separator+type; // this is Linux method String filename = file.getOriginalFilename(); File filepath = new File(path,filename);//新建文件存储路径 System.out.println(filepath); if(!filepath.getParentFile().mkdirs()){ filepath.getParentFile().mkdirs(); } file.transferTo(new File(path+File.separator+filename)); //想要返回可直接访问的链接还要配置 映射,具体请看下面链接 return "你的IP地址"+"/images/"+type+"/"+filename; }else { return "error"; } } } [代码] 配置访问映射 文件访问映射 2.3 云开发文件上传 微信小程序支持云开发,其文件上传接口有一些差异,但是不需要自己再构建后台。 [代码]wx.cloud.uploadFile({ cloudPath: 'file/' + '你的文件名字(带格式后缀)', // 在云端存储的路径 filePath: '', // 就是选择文件返回的路径 }).then(res => { // get resource ID console.log(res.fileID) }).catch(error => { // handle error }) [代码] 我们可以根据返回的fileID 置换 真实的文件访问地址。 其置换方式参见官方文档: [代码]wx.cloud.getTempFileURL({ fileList: ['cloud://xxx', 'cloud://yyy'], success: res => { // get temp file URL console.log(res.fileList) }, fail: err => { // handle error } }) [代码] TIPS:关于云开发文件上传的建议 如果没有保密需求,为了方便后续开发,存储到数据库中的最好是可以直接访问的文件链接。 置换真实文件地址,不要每次上传一次文件就置换一次,先把返回的fileID 存放在数组中,到该事务所有上传完成后,再使用fileID 数组置换真实文件访问链接数组。 要考虑文件名重复的可能,建议使用时间戳在 wx.cloud.uploadFile 中的 cloudPath 中对存储到云环境中的文件命名进行格式化。 总的来说就是先上传文件,再向数据库中写入记录。 2.4 多文件同时上传的处理方式 uploadFile 每次只能上传一个文件 如何处理这个问题? A.不考虑文件的上传次序问题,可以采用遍历上传的方式,采用精确的时间戳和遍历index对文件名进行格式化。通过定时触发检测函数判断是否全部上传完成。这种方式考虑的是并发能力。 [代码]upSeveralfiles() { wx.showLoading({ title: '上传中~', mask: true }) var that = this; var timecode = sev.vcode(new Date()); // 这是时间戳编码函数 var files = this.data.fileList; // 这是 选择文件中返回的 res.tempFilePath 数组 var len = files.length; var i = 0; for (i = 0; i < len; i++) { var str = files[i].name; wx.cloud.uploadFile({ cloudPath: 'file/' + '(' + sev.getformatTime(new Date()) + ')' + str, filePath: files[i].path, success(res) { console.log(res) that.setData({ cloudlist: that.data.cloudlist.concat([res.fileID]), }) // cloudlist 是存放 文件链接置换id 的数组 , 非云开发存储的就是真实可访问的链接数组 // 如果使用的不是云开发 那么 可以返回真实的 访问地址 }, fail(res) { console.log(res) } }) } // 使用定时器检测文件是否全部上传完成 , 并 判断是否进行下一步 操作 var timer = setInterval(function () { if (that.data.cloudlist.length == len) { // 只有全部上传成功了 长度才会相等 clearInterval(timer); // 继续执行下一步 ,根据 cloudlist 置换真实地址 并存放到数据库 // 如果使用的非云开发,那么就继续执行 存储至数据库的操作 } }, 1000) } [代码] 补充文件编码函数 sev.js 中的根据时间编码部分 , 可以根据实际流量自定义。 [代码]function getformatTime(date) { var year = date.getFullYear() var month = date.getMonth() + 1 var day = date.getDate() var hour = date.getHours() var minute = date.getMinutes() var second = date.getSeconds() return [year, month, day].map(formatNumber).join('-'); }; [代码] B.考虑文件的上传次序问题,采用回调方式进行文件上传(更推荐使用这种方式) [代码]data: { fileList: [], realList: [],//云端地址链接列表 fileid: 0, }, upSeveralfiles() { var that = this; var files = this.data.fileList; // 这是 选择文件中返回的 res.tempFilePath 数组 var len = files.length; var uid = this.data.fileid; wx.uploadFile({ url: '你的服务器文件接收函数请求地址', name: 'file', filePath: files[uid], header: { "Content-Type": "multipart/form-data" }, success(res) { that.setData({ fileid: uid + 1, realList: that.data.realList.concat([res.data]) }, () => { if (that.data.fileid == len) { // 上传完成 ,执行下一步操作 } else { //上传完一个文件 递归执行 下次上传 that.upSeveralfiles(); } }) }, fail(res) { console.log(res.data) } }) }, [代码] 以上是提供的两种思路 , 无论是不是云开发 , 两种思路都是共通的,适用于多文件上传. 文件的下载 这个地方唯一值得注意的是云开发的一种下载方式 可以通过fileID进行download , 当然只能下载存储在自己云环境中的文件. [代码]wx.cloud.downloadFile({ fileID: '', success: res => { console.log(res.tempFilePath) }, fail: err => { } }) [代码] [代码]wx.downloadFile({ url: '', //仅为示例,并非真实的资源 success (res) { //res.tempFilePath }) [代码] 下载进度监控 , 用于下载进度条绘制等功能实现 [代码]const downloadTask = wx.cloud.downloadFile({ *** }) // wx.downloadFile 同理 downloadTask.onProgressUpdate((res) => { //res.progress 为下载进度 }) [代码] 下载下来的文件支持直接打开,就像是在微信聊天中打开一样,需要下载插件。 其使用方式为: [代码]wx.openDocument({ filePath: res.tempFilePath //为文件路径 非数组 , 可使用回调函数 success 等 }) [代码] 文件的移动 这个一般情况下是用不到的, 也不建议使用移动文件的方法作为功能实现手段 ,必然有更好的替代方式,比如修改数据库路径 和 真实文件路径的映射, 效率更高一些. 这里仅讲云开发移动文件方式。 [代码]const cloud = require('wx-server-sdk') const fs = require('fs') const path = require('path') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const fileStream = fs.createReadStream(path.join(__dirname, 'demo.jpg')) //这个可以读取 云存储 中的文件 , 所谓删除 就是复制 然后 删除原位置文件 return await cloud.uploadFile({ cloudPath: 'demo.jpg', fileContent: fileStream, }) } [代码] 具体开发文档 建议在数据库中修改映射路径最好。 需要小程序开发的请联系我QQ : 1025584691
2020-02-17 - 从源码看微信小程序启动过程
一、写作背景 接触小程序一年多,真实体验就是小程序开发门槛相对而言确实比较低。不过小程序的开发方式,一直是开发者吐槽的,如习惯了 Vue,React 开发的开发者经常会吐槽小程序一个 Page 必须由多个文件组成,组件化支持不完善或者说不能非常愉快的开发组件。在以前小项目中没太大感觉,从加入有赞,参与有赞微商城小程序的开发,是真切的体会到对于大型小程序项目开发的复杂性。 有赞从微信小程序内测就开始开发小程序,在不支持自定义组件的时代,只能通过 import 的形式拆分模块或实现组件。在业务复杂的页面,可能会 import 非常多的模块,而相应的 wxss 也需要 import 样式,除了操作繁琐,有时候也难免遗漏。 作为开发者,我们当然希望可以让工作更简单,更愉快,也希望改善我们的开发方式。所以希望能够更了解微信小程序框架,减少不必要的试错,于是有了一次对小程序框架的 debug 之旅。(基础库 1.9.93) 通过三周空余时间的 debug,也算对小程序框架有了一些浅显的认识,达到了最初的目的;对小程序启动,实例,运行等有了真切的体会。这篇文章记录了小程序框架的基本代码结构,启动流程,以及程序实例化过程。 本文的目的是希望把我看到的分享给对小程序感兴趣或者正在开发小程序的读者,主要解答“框架对传入的对象等到底做了什么”。 二、从启动流程一窥小程序框架细节 在开发者工具中使用 help() 方法,可以查看一些指令和方法。使用其中的 openVendor 方法可以打开微信开发者工具在小程序框架所在目录。其中以包括以基础库命名的目录和其他帮助文件,如其中有两个工具 wcc,wcsc。wcc 可把 wxml 转换为对应的 JS 函数 —— $gwx(path, global),wcsc 可将 wxss 转换为 css。而基础库目录包括 WAService.js 和 WAWebview.js 文件。小程序框架在开发者工具中以 WAService.js 命名(WAWebview.js 不知其作用,听说在真机环境使用该文件)。 在开发中工具命令行使用 document.head 可以查看到小程序的启动流程大致如下: [图片] 以小节的方式分别介绍这些流程,小程序是如何处理的(小节编号与图中编号相同)。 1、初始化全局变量 下图是小程序启动是初始化的一些全局的变量: [图片] 那些使用“__”开头,未在文档中提及可使用变量是不建议使用的,wxAppCode 在开发者工具中分为两类值,json 类型和 wxml 类型。以 .json 结尾的,其 key 值为开发者代码中对应的 json 文件的内容,.wxml 结尾的,其 key 值为通过调用 $gwx(’./pages/example/index.wxml’) 将得到一个可执行函数,通过调用这个函数可得到一个标识节点关系的 JSON 树。 [图片] 2、加载框架(WAService.js) 使用工具对 WAService.js 进行格式化后进行 debug。可以发现小程序框架大致由: WeixinJSBridge、 NativeBuffer、 wxConsole、 WeixinWorker、 JavaScript兼容(这部分为猜测)、 Reporter、 wx、 exparser、 virtualDOM、 appServiceEngine 几部分组成。 其中除了 wx 和 WeixinJSBridge 这两个基础 API 集合, exparser, virtualDOM, appServiceEngine 这三部分作为框架的核心, appServiceEngine 提供了框架最基本的接口如 App,Page,Component; exparser 提供了框架底层的能力,如实例化组件,数据变化监听,view 层与逻辑层的交互等;而 virtualDOM 则起着链接 appServiceEngine 和 exparser 的作用,如对开发者传入 Page 方法的对象进行格式化再传入 exparser 的对应方法处理。 框架对外暴露了以下API:Behavior,App,Page,Component,getApp,getCurrentPages,definePlugin,requirePlugin,wx。 3、业务代码的加载 在小程序中,开发者的 JavaScript 代码会被打包为 [代码]define('xxx.js', function(require, module, exports, window, document, frames, self, location, navigator, localStorage, history, Caches, screen, alert, confirm, prompt, fetch, XMLHttpRequest, WebSocket, webkit, WeixinJSCore, Reporter, print, WeixinJSBridge) { 'use strict'; // your code }) [代码] 这里的 define 是在框架中定义的方法,在框架中提供了两个方法:require 和 define 用来定义和使用业务代码。其方式有些像 AMD 规范接口,通过 define 定义一个模块,使用 require 来应用一个模块。但是也有很大区别,首先 define 限制了模块可使用的其他模块,如 window,document;其次 require 在使用模块时只会传入 require 和 module,也就是说参数中的其他模块在定义的模块中都是 undefined,这也是不能在开发者工具中获取一些浏览器环境对象的原因。 在小程序中,JavaScript 代码的加载方式和在浏览器中也有些不同,其加载顺序是首先加载项目中其他 js 文件(非注册程序和注册页面的 js 文件),其次是注册程序的 app.js,然后是自定义组件 js 文件,最后才是注册页面的 js 代码。而且小程序对于在 app.js 以及注册页面的 js 代码都会加载完成后立即使用 require 方法执行模块中的程序。其他的代码则需要在程序中使用 require 方法才会被执行。 下面详细介绍了 app.js,自定义组件,页面 js 代码的处理流程。 4、加载 app.js 与注册程序 在 app.js 加载完成后,小程序会使用 require(‘app.js’) 注册程序,即对 App 方法进行调用,App 方法是对 appServiceEngine.App 方法的引用。 下图是框架对于 App 方法调用时的处理流程: [图片] App 方法根据传入的对象实例化一个 app 实例,其生命周期函数 onLaunch 和 onShow 因为使用不同的方式获取 options的参数。在有些需要根据场景值来实现需求的,或许使用 onShow 中的场景值更合适。 在实际开发过程中发现,在微信顶部唤起小程序和在小程序列表唤起的 options 也是不一样的。在该案例中通过点击分享的小程序进入后,关闭小程序,再通过不同方式进入小程序,通过顶部唤起的还是 options 的 path 属性还是分享出来的 path,但是通过列表中打开直接回到了首页,这里 App 中的 onShow 就会获取到不同的 options。 5、加载自定义组件代码以及注册自定义组件 自定义组件在 app.js 之后被加载,小程序会在这个过程中加载完所有的自定义组件(分包中自定义组件没有有测试过),并且是加载完成后自动注册,只有注册完成后才会加载下一个自定义组件的代码。 下图是框架对于 Component 方法处理流程: [图片] 图中介绍了框架如何对传入 Component 方法的对象的处理,其后面还有很多深入的对于组件实例化的步骤没有在图中表示出来,具体可以在文章最后的附件中查看。 自定义组件在小程序中越来越完善,其拥有的能力也比 Page 更强大,而后面会提到在使用自定义组件的 Page 中,Page 实例也会使用和自定义组件一样的实例化方式,也就是说,他拥有和自定义组件一样的能力。 6、加载页面代码和注册页面 加载页面代码的处理流程和加载自定义组件一样,都是加载完成后先注册页面,然后才会加载下一个页面。 下图是注册一个页面时框架对于 Page 方法的处理流程: [图片] Page 方法会根据是否使用自定义组件做不同的处理。使用自定义组件的 page 对象会被处理为和自定义组件的结构,并在页面实例化时使用不同的处理流程进行实例化。当然对于开发而言没任何不同。 从图中可以发现 Page 传入的(生命周期)代码并不会在这里被执行,可以通过下面小节了解 Page 实例化的详细过程。 7、等待页面 Ready 和 Page 实例化 还记得上面介绍的启动流程中最后一步等待页面 Ready?严格来讲是等待浏览器 Ready,小程序虽然有部分原生的组件,不过本质上还是一个 web 程序。 在小程序中切换页面或打开页面时会触发 onAppRoute 事件,小程序框架通过 wx.onAppRoute 注册页面切换的处理程序,在所有程序就绪后,以 entryPagePath 作为入口使用 appLaunch 的方式进入页面。 下图是处理导航的程序流程: [图片] 从图中可以看出页面的实例化是在进入页面时进行,下图是具体的实例化过程: [图片] 下图是最终可得到 Page 实例: [图片] 可以发现其中多了 onRouteEnd API,实际该接口不会被调用。其中以 component 标记的表示只有在使用了自定义组件时才会有的方法和属性。在前面第 5 小节提到了对于使用自定义组件的页面会按照自定义组件方式解析,这些属性和方法与自定义组件表现一致。 8、关于 setData 小程序框架是一个以数据驱动的框架,当然不能少了对他如何实现数据绑定的探索,下图是 Page 实例的 setData 执行流程: [图片] 其中 component:setData 表示使用自定义组件的 Page 实例的 setData 方法。 三、写在最后 这是一次不完全的小程序框架探索,是在微信开发工具中 debug 的结果。虽然对于实际开发没有什么太大的帮助,但是对框架如何对开发的 js 代码进行处理有了一个很明确的认识,在使用一些 js 特性时可以有明确的感知。如果你还疑惑“小程序框架对传入的对象等到底做了什么”那一定是我表达能力太差,说声对不起。 通过这一次 debug ,也给我引入了新的问题,还希望能够有更多的讨论: · 自定义组件太多启动时会耗时处理自定义组件 · 文件太多会耗时读文件 · 合理的设计分包很重要 当然最后对于框架中已有的能力,还是非常希望微信可以开放更多稳定的接口,并在文档中告知开发者,让开发变得简单一些。
2019-03-05 - 小程序架构设计(二)
接着上篇文章《小程序架构设计(一)》 前边我们说到采用Web+离线包的方式可以解决很多问题,但是遗留了一个安全问题有待解决。 经过了一番讨论,我们决定把开发者的JS逻辑代码放到单独的线程去运行,因为不在Webview线程里,所以这个环境没有Webview任何接口,自然的开发者就没法直接操作Dom,也就没法动态去更改界面,“管控”的问题得以解决。 还存在一个问题:开发者没法操作Dom,如果用户交互需要界面变化的话,开发者就没办法动态变化界面了。所以我们要找到一个办法:不直接操作Dom也能做到界面更新。 其实Facebook早有方案解决这个问题,就是上篇文章提到的React。React引入了Virtual Dom的概念(后文简称VD),业务侧只需要改变数据即可引起界面变化,相关原理后边再写篇文章来分享。 至此小程序双线程的模型就定下来了:渲染层(Webview)+逻辑层(JSCore) [图片] 其中渲染层用了Webview进行渲染,开发者的JS逻辑运行在一个独立的JSCore线程。 渲染层提供了带有数据绑定语法的WXML,逻辑层提供了setData等等API,开发者需要进行界面变化时,只需要通过setData把变化的数据传进去,小程序框架就会进行Dom Diff等流程最后把正确的结果更新在Dom树上。 [图片] 可以看到在开发者的逻辑下层,还需要有一层小程序框架的支持(数据通信、API、VD算法等等),我们把它称为基础库。 我们在两个线程各自注入了一份基础库,渲染层的基础库含有VD的处理以及底层组件系统的机制,对上层提供一些内置组件,例如video、image等等。逻辑层的基础库主要会提供给上层一些API,例如大家经常用到的wx.login、wx.getSystemInfo等等。 解决了渲染问题,我们还要看一下用户在和界面交互时的问题。 [图片] 用户在屏幕点击某个按钮,开发者的逻辑层要处理一些事情,然后再通过setData引起界面变化,整个过程需要四次通信。对于一些强交互(例如拖动视频进度条)的场景,这样的处理流程会导致用户的操作很卡。 对于这种强交互的场景,我们引入了原生组件,这样用户和原生组件的交互可以节省两次通信。 [图片] 正如上图所示,原生组件和Webview不是在同一层级进行渲染,原生组件其实是叠在Webview之上,想必大家都遇到过这个问题,video、input、map等等原生组件总是盖在其他组件之上,这就是这个设计带来的问题。 我们也很重视这个问题,经过了一段时间的努力,我们攻克了这个难题,把原生组件渲染到Webview里,从而实现同层渲染。目前video组件已经完成同层渲染的全量发布,详细可以看我们之前的公告:同层渲染公测。 为了让开发者可以更好的开发小程序,我们在后来还引入了自定义组件和插件的概念,我们后续会有相关的文章再介绍这两块的设计,希望大家关注我们社区的文章板块。 [图片] 以上就是小程序架构设计的历史。
2019-02-27 - 小程序架构设计(一)
在微信早期,我们内部就有这样的诉求,在微信打开的H5可以调用到微信原生一些能力,例如公众号文章里可以打开公众号的Profile页。所以早期微信提供了Webview到原生的通信机制,在Webview里注入JSBridge的接口,使得H5可以通过它调用到原生能力。 [图片] 我们可以通过JSBridge微信预览图片的功能: [代码]WeixinJSBridge.invoke('imagePreview', { current: https://img1.gtimg.com/1.jpg', urls: [ 'https://img1.gtimg.com/1.jpg', 'https://img1.gtimg.com/2.jpg', 'https://img1.gtimg.com/3.jpg' ] }) [代码] 早期微信官方是没有暴露这些接口的,都是腾讯内部业务在使用,很多外部开发者发现后,就依葫芦画瓢地使用了。 从另外一个角度看,JSBridge是微信和H5的通信协议,有一些能力可能要组合不同的能力才能完整调用。如果我们直接开放这套API,相当于所有开发者都要直接理解这样的接口协议,显然是很不合理的。 所以在2015年初的时候,微信就发布了JSSDK,其实就是隐藏了内部一些细节,包装了几十个API给到上层业务直接调用。 [图片] 前边的代码就变成了: [代码]wx.previewImage({ current: https://img1.gtimg.com/1.jpg', urls: [ 'https://img1.gtimg.com/1.jpg', 'https://img1.gtimg.com/2.jpg', 'https://img1.gtimg.com/3.jpg' ] }) [代码] 开发者可以用JSSDK来调用微信的能力,来完成一些以前H5做不到或者难以做到的事情。 能力上得到了更多的支持,但是微信里的H5体验却没有改善。 第一点是加载H5时的白屏。在微信里打开链接后会看到白屏,有一些H5的服务不稳定,这个白屏现象会更严重。 [图片] 第二点是在H5跳转到其他页面时,切换的效果也很不流畅,只能看到顶部绿色进度条在走。 [图片] 随着JSSDK的开放,还出现了更不好对付的问题。 微信上越来越多干坏事的人,有人做假红包,有人诱导分享,有伪造一些官方活动。他们会利用JSSDK的分享能力变相的去裂变分享到各个群或者朋友圈,由于JSSDK是根据域名来赋予api权限的,运营人员封了一个域名后,他们立马用别的域名又继续做坏,要知道注册一个新的域名的成本是很低的。 [图片] [图片] [图片] 龙哥在2016年微信公开课上提出了应用号的概念,我们要重新设计一个新的移动应用开发模式,同时我们要解决刚刚提到的一些问题。 至此,我们回顾一下目前移动应用开发的一些特点: Web开发的门槛比较低,而App开发门槛偏高而且需要考虑iOS和安卓多个平台; 刚刚说到H5会有白屏和页面切换不流畅的问题,原生App的体验就很好了; H5最大的优点是随时可以上线更新,但是App的更新就比较慢,需要审核上架,还需要用户主动安装更新。 我们更想要的一种开发模式应该是要满足一下几点: 像H5一样开发门槛低; 体验一定要好,要尽可能的接近原生App体验; 让开发者可以云端更新,而且我们平台要可以管控。 很多人可能会第一时间想到Facebook的React Native(下边简称RN),是不是RN就能解决这些问题呢? 是的,React Native貌似可以解决刚刚那些问题,我们也曾经想用RN来做。但是仔细分析了一下,我们发现了采用RN这个机制做开放平台还是存在一些问题。 RN只支持CSS的子集,作为一个开放的生态,我们还要告诉外边千千万万的开发者,哪些CSS属性能用,哪些不能用; RN本身存在一些问题,这些依赖RN的修复,同时这样就变成太过依赖客户端发版本去解决开发者那边的Bug,这样修复周期太长。 RN前阵子还搞出了一个Lisence问题,对我们来说也是存在隐患的。 [图片] 所以我们舍弃了这样的方案,我们改用了Hybrid的方式。简单点说,就是把H5所有代码打包,一次性Load到本地再打开。这样的好处是我们可以用一种近似Web的方式来开发,同时在体验上也可以做到不错的效果,并且也是可以做到云端更新的。 [图片] 现在留给我们的最后一个问题就是,平台的管控问题。 怎么理解呢?我们知道H5的界面结构是用HTML进行描述,浏览器进行一系列的解析最终绘制在界面上。 [图片] 同时浏览器提供了可以操作界面的DOM API,开发者可以用这些API进行一些界面上的变动,从而实现UI交互。 [图片] 既然我们要采用Web+离线包的方式,那我们要解决前边说过的安全问题,我们就要禁用掉很多危险的HTML标签,还要禁用掉一些API,我们要一直维护这样的白名单或者黑名单,实现成本太高了,而且未来浏览器内核一旦更新,对我们来说都是很大的安全隐患。 [图片] 这就是小程序一开始遇到的问题,在下篇文章《小程序架构设计(二)》,我们再详细展开一下小程序是如何解决以上这个问题的。
2019-02-26 - view在存在同属性动画时,hover-class无效?
view如果有动画或者有opacity、transform等可动画属性,hover-class无效 同属性情况下 比如animation里有个opacity,那么hover-class中的opacity无效 button暂时没测,估计也一样的 。。。难道是我写的方式不对???
2019-09-06 - [打怪升级]小程序自定义头部导航栏“完美”解决方案
[图片] 为什么要做这个? 主要是在项目中,智酷君发现的一些问题 一些页面是通过扫码和订阅消息访问后,没有直接可以点击去首页的,需要添加一个home链接 需要添加自定义搜索功能 需要自定义一些功能按钮 [图片] 其实,第一个问题,在最近的微信版本更新中已经优化了,通过 小程序模板消息 过来的,系统会自动加上home按钮,但对于其他的访问方式则没有支持~ 一个不大不小的问题:两边ICON不对齐问题 [图片] 智酷君之前尝试了各种解决方法,发现有一个问题,就是现在手机屏幕太多种多样,有 传统头部、宽/窄刘海屏、水滴屏等等,无法八门,很多解决方案都无法解决特殊头部,系统**“胶囊按钮”** 和 自定义按钮在Android屏幕可能有 几像素不对齐 的问题(强迫症的噩梦)。 下面分享下一个相对比较完善的解决方案: [图片] 小程序代码段DEMO Link: https://developers.weixin.qq.com/s/cuUaCimT72cH ID: cuUaCimT72cH 智酷君做了一个demo代码段,方便大家直接用IDE工具查看源码~ [图片] 页面配置 1、页面JSON配置 [代码]{ "usingComponents": { "NavComponent": "/components/nav/common" //以插件的方式引入 }, "navigationStyle": "custom" //自定义头部需要设置 } [代码] 如果需要自定义头部,需要设置navigationStyle为 “custom” 2、页面代码 [代码]<!-- home 类型的菜单 --> <NavComponent v-title="自定义头部" bind:commonNavAttr="commonNavAttr"></NavComponent> <!-- 搜索菜单 --> <NavComponent is-search="true" bind:commonNavAttr="commonNavAttr"></NavComponent> [代码] 可以在自定义导航标签上添加属性配置来设置功能,具体按照实际需要来 3、目录结构 [代码]│ ├─components │ └─nav │ common.js │ common.json │ common.wxml │ common.wxss │ ├─images │ back.png │ home.png │ └─index index.js index.json index.wxml index.wxss search.js search.json search.wxml search.wxss [代码] 仅供参考 插件对应的JS部分 components/nav/common.js部分 [代码]const app = getApp(); Component({ properties: { vTitle: { type: String, value: "" }, isSearch:{ type: Boolean, value: false } }, data: { haveBack: true, // 是否有返回按钮,true 有 false 没有 若从分享页进入则没有返回按钮 statusBarHeight: 0, // 状态栏高度 navbarHeight: 0, // 顶部导航栏高度 navbarBtn: { // 胶囊位置信息 height: 0, width: 0, top: 0, bottom: 0, right: 0 }, cusnavH: 0, //title高度 }, // 微信7.0.0支持wx.getMenuButtonBoundingClientRect()获得胶囊按钮高度 attached: function () { if (!app.globalData.systeminfo) { app.globalData.systeminfo = wx.getSystemInfoSync(); } if (!app.globalData.headerBtnPosi) app.globalData.headerBtnPosi = wx.getMenuButtonBoundingClientRect(); console.log(app.globalData) let statusBarHeight = app.globalData.systeminfo.statusBarHeight // 状态栏高度 let headerPosi = app.globalData.headerBtnPosi // 胶囊位置信息 console.log(statusBarHeight) console.log(headerPosi) let btnPosi = { // 胶囊实际位置,坐标信息不是左上角原点 height: headerPosi.height, width: headerPosi.width, top: headerPosi.top - statusBarHeight, // 胶囊top - 状态栏高度 bottom: headerPosi.bottom - headerPosi.height - statusBarHeight, // 胶囊bottom - 胶囊height - 状态栏height (胶囊实际bottom 为距离导航栏底部的长度) right: app.globalData.systeminfo.windowWidth - headerPosi.right // 这里不能获取 屏幕宽度,PC端打开小程序会有BUG,要获取窗口高度 - 胶囊right } let haveBack; if (getCurrentPages().length != 1) { // 当只有一个页面时,并且是从分享页进入 haveBack = false; } else { haveBack = true; } var cusnavH = btnPosi.height + btnPosi.top + btnPosi.bottom // 导航高度 console.log( app.globalData.systeminfo.windowWidth, headerPosi.width) this.setData({ haveBack: haveBack, // 获取是否是通过分享进入的小程序 statusBarHeight: statusBarHeight, navbarHeight: headerPosi.bottom + btnPosi.bottom, // 胶囊bottom + 胶囊实际bottom navbarBtn: btnPosi, cusnavH: cusnavH }); //将实际nav高度传给父类页面 this.triggerEvent('commonNavAttr',{ height: headerPosi.bottom + btnPosi.bottom }); }, methods: { _goBack: function () { wx.navigateBack({ delta: 1 }); }, bindKeyInput:function(e){ console.log(e.detail.value); } } }) [代码] 解决不同屏幕头部不对齐问题的终极办法是 wx.getMenuButtonBoundingClientRect() 这个方法从微信7.0.0开始支持,通过这个方法我们可以获取到右边系统胶囊的top、height、right等属性,这样无论是水滴屏、刘海屏、异形屏,都能完美对齐右边系统默认的胶囊bar,完美治愈强迫症~ APP.js 部分 [代码]//app.js App({ /** * 加载页面 * @param {*} options */ onShow: function (options) { }, onLaunch: async function () { let self = this; //设置默认分享 this.globalData.shareData = { title: "智酷方程式" } // this.getSysInfo(); }, globalData: { //默认分享文案 shareData: {}, qrCodeScene: false, //二维码扫码进入传参 systeminfo: false, //系统信息 headerBtnPosi: false, //头部菜单高度 } }); [代码] 将获取的参数存储在一个全局变量globalData中,可以减少反复调用的性能消耗。 插件HTML部分 [代码]<view class="custom_nav" style="height:{{navbarHeight}}px;"> <view class="custom_nav_box" style="height:{{navbarHeight}}px;"> <view class="custom_nav_bar" style="top:{{statusBarHeight}}px; height:{{cusnavH}}px;"> <!-- 搜索部分--> <block wx:if="{{isSearch}}"> <input class="navSearch" style="height:{{navbarBtn.height-2}}px;line-height:{{navbarBtn.height-4}}px; top:{{navbarBtn.top+1}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;" maxlength="10" bindinput="bindKeyInput" placeholder="输入文字搜索" /> </block> <!-- HOME 部分--> <block wx:else> <view class="custom_nav_icon {{!haveBack||'borderLine'}}" style="height:{{navbarBtn.height}}px;line-height:{{navbarBtn.height-2}}px; top:{{navbarBtn.top}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;"> <view wx:if="{{haveBack}}" class="icon-back" bindtap='_goBack'> <image src='/images/back.png' mode='aspectFill' class='back-pre'></image> </view> <view wx:if="{{haveBack}}" class='navbar-v-line'></view> <view class="icon-home"> <navigator class="home_a" url="/pages/home/index" open-type="switchTab"> <image src='/images/home.png' mode='aspectFill' class='back-home'></image> </navigator> </view> </view> <view class="nav_title" style="height:{{cusnavH}}px; line-height:{{cusnavH}}px;"> {{vTitle}} </view> </block> </view> </view> </view> [代码] 主要是对几种状态的判断和定位的计算。 插件CSS部分 [代码]/* components/nav/test.wxss */ .custom_nav { width: 100%; background: #3a7dd7; position: relative; z-index: 99999; } .custom_nav_box { position: fixed; width: 100%; background: #3a7dd7; z-index: 99999; border-bottom: 1rpx solid rgba(255, 255, 255, 0.3); } .custom_nav_bar { position: relative; z-index: 9; } .custom_nav_box .nav_title { font-size: 28rpx; color: #fff; text-align: center; position: absolute; max-width: 360rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; top: 0; left: 0; right: 0; bottom: 0; margin: auto; z-index: 1; } .custom_nav_box .custom_nav_icon { position:absolute; z-index: 2; display: inline-block; border-radius: 50%; vertical-align: top; font-size:0; box-sizing: border-box; } .custom_nav_box .custom_nav_icon.borderLine { border: 1rpx solid rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.1); } .navbar-v-line { width: 1px; margin-top: 14rpx; height: 32rpx; background-color: rgba(255, 255, 255, 0.3); display: inline-block; vertical-align: top; } .icon-back { display: inline-block; width: 74rpx; padding-left: 20rpx; vertical-align: top; /* margin-top: 12rpx; vertical-align: top; */ height: 100%; } .icon-home { /* margin-top: 8rpx; vertical-align: top; */ display: inline-block; width: 80rpx; text-align: center; vertical-align: top; height: 100%; } .icon-home .home_a { height: 100%; display: inline-block; vertical-align: top; width: 35rpx; } .custom_nav_box .back-pre, .custom_nav_box .back-home { width: 35rpx; height: 35rpx; vertical-align: middle; } .navSearch { width: 200px; background: #fff; font-size: 14px; position: absolute; padding: 0 20rpx; z-index: 9; } [代码] 总结: 通过微信API: getMenuButtonBoundingClientRect(),结果各类手机屏幕的适配问题 将算好的参数存储在全局变量中,一次计算全局使用,爽YY~ 往期回顾: [填坑手册]小程序PC版来了,如何做PC端的兼容?! [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - 多个input 框,进行输入切换, 键盘频繁收起,隐藏, 可否控制键盘的隐藏于收起?
有没有什么方法可以自由控制键盘的收起与弹出
2020-01-16 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25 - vue使用腾讯位置服务选点组件问题总结
前言 使用地图选点组件引发的一系列问题: 1、选择地址的回调路径不兼容哈希路由 2、回调后腾讯地图返回了完整的地址,而用户需要省市区具体名称 3、获取省市区具体名称后,如何和后端的数据对上对应的 ID 相关文档 地图选点组件 效果图: [图片] 开始使用 采用的是地图 API 的第二种方式,跳转新的页面,通过回调路径把地址信息带回来原先页面 代码如下: [代码]let url = escape( `${window.location.origin}${window.location.pathname}/#/member/address_form?action=${this.$route.query.action}` ) this.$router.go(-1) window.location.href = `https://apis.map.qq.com/tools/locpicker?search=1&type=0&backurl=${url}&key=这里改成你的腾讯地图密钥&referer=这里是腾讯地图应用名称` [代码] url 的处理: 为什么需要这么麻烦的拼接呢? 因为如果每次我都直接拿 [代码]window.location.href[代码]那么如果用户多次选择地址,使用 [代码]escape[代码] 转码的时候,会把路径的中文都转掉,可是我们并不需要每次都把腾讯地图带给我们的参数都重新放到回调地址中,所以就需要自己拼接路径 为什么需要 [代码]escape[代码] 在 vue 中使用的是原来的哈希路由模式。即路径上会带上 #。这时候回调地址就不能识别了,就需要用到 [代码]escape[代码] 做了 url 处理,发现就可以跳转过去了。那第一步就成功了。解决了第一个问题:选择地址的回调路径不兼容哈希路由 问题 2. 需要省市区的名称 这是在地图上随便定的一个点。路径返回大概就是这些参数 包含了 name=印象家园酒店公寓%28广园中路店%29&latng=23.15809,113.27209&addr=广东省广州市白云区广园中路 216 号&city=广州市&module=locationPicker 大概就是这样的: [代码]{ name: "详细的地址名称", latng: "选择的地址的经纬度", addr: "省市区+地方名", city: "城市名", module: "标识" } [代码] 那么我需要处理的就是经纬度,还有省市区名称 [代码]var urlData = this.$route.query // 获取路径上的参数 var latng = urlData.latng.split(",") // 获取经纬度 var reg = /.+?(省|市|自治区|自治州|县|区)/g // 省市区的正则 console.log(latng) // [23.15809,113.27209] 这个数组就是对应的经度纬度了 console.log(urlData.addr.match(reg)) // ['广东省','广州市','白云区'] [代码] 和后端的数据对接 进过上面 2 步。经纬度,详细地址,省市区都已经拿到了。那就差和后端的数据匹配上了(这个操作见仁见智,不一定每个人都有) 后端数据大概长这样: [图片] 我要做的就是拿到中文名称,匹配出对应的 ID [代码] methods:{ /** * 递归方法,获取城市ID等 * @param {Array} list 数据库中的地址列表(每次循环都会拿自己的child来匹配) * @param {Array} param 需要查找的省市区数组 * @param {Number} level 当前遍历的深度 * @param {Array} area_ids 当前已遍历找到的省市区ID数组 * @return 对应的ID数组 */ locationGhosts (list, param, level = 0, areaIds = []) { let child = [] list.some(item => { if (param[level].indexOf(item.area_name) !== -1) { areaIds[level] = item.area_id // 存储ID,已经找到一个 child = item.child return true } }) // 判断不要改三目运算符,详情查看尾递归相关描述 if (level === 2) { return areaIds } else { return this.locationGhosts(child, param, ++level, areaIds) } } } // 调用递归,获取对应的省市区ID let areaIds = this.locationGhosts(this.area_list, urlData.addr.match(reg), 0) areaIds[0] // 省份ID areaIds[1] // 城市ID areaIds[2] // 地区ID [代码] 至此,一个选择地址的功能就算是完美完成了! 以上内容转载自Jioho_的文章《vue中使用腾讯地图选择地址》 来源:CSDN 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2020-08-21 - [打怪升级]小程序评论回复和发贴组件实战(一)
[图片] 在学习成长的过程中,常常会遇到一些自己从未接触的事物,这就好比是打怪升级,每次打倒一只怪,都会获得经验,让自己进步强大。特别是我们这些做技术的,逆水行舟不进则退。下面分享下小程序开发中的打怪升级经历~ [图片] 先来看下实际效果图,小程序开发中有时会要做一些的功能复杂的组件,比如评论回复和发帖功能等,这次主要讲的是关于评论模块的一些思路和实战中的经验,希望能抛砖引玉,给大家一些启发,一同成长~ [代码片段]评论回复组件实战demo demo的微信路径: https://developers.weixin.qq.com/s/oHs5cMma7N9W demo的ID:oHs5cMma7N9W 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] [图片] 根据这个demo.gif,本人做了一个简单的流程图,帮助大家理解。下面罗列一些开发中需要“打的怪”: 1、组件目录结构 [代码]├─components ---小程序自定义组件 │ ├─plugins --- (重点)可独立运行的大型模块,可以打包成plugins │ │ ├─comment ---评论模块 │ │ │ │ index.js │ │ │ │ index.json │ │ │ │ index.wxml │ │ │ │ index.wxss │ │ │ │ services.js ---(重点)用来处理和清洗数据的service.js,配套模板和插件 │ └─submit ---评论模块子模块:提交评论 index.js index.json index.wxml index.wxss [代码] 为什么要单独做个评论页面页面(submit)? 因为如果是当前页面最下面input输入的形式,会出现一些兼容问题,比如: 不同手机的虚拟键盘高度不同,不好绝对定位和完全适配 弹窗输入框过小输入不方便,如果是大的textare时,容易误触下面评论的交。 注:目录结构,仅供参考。 2、NODE端API接口返回结构和页面结构 [代码]//node:API接口返回 { "data": { "commentTotal": 40, "comments": [ { "contentText": "喜欢就关注我", //评论内容 "createTime": 1560158823647, //评论时间 "displayName": "智酷方程式", //用户名 "headPortrait": "https://blz.nosdn.127.net/1/weixin/zxts.jpg", //用户头像 "id": "46e0fb0066666666", //评论ID 用于回复和举报 "likeTotal": 2, //点赞数 "replyContents": [ //回复评论 { "contentText": "@智酷方程式 喜欢就回复我", //回复评论内容 "createTime": 1560158986524, //回复时间 "displayName": "神秘的前端开发", //回复的用户名 "headPortrait": "https://blz.nosdn.127.net/1/2018cosplay/fourth/tesss.jpg", //回复的用户头像 "id": "46e0fb00111111111", //回复评论的ID "likeTotal": 2, //回复评论的点赞数 "replyContents": [], //回复的回复 盖楼 "replyId": "46e0fb001ec222222222", //回复评论的独立ID,用于统计 }, { "contentText": "@智酷方程式: 威武,学习学习", "createTime": 1560407232814, "displayName": "神秘的前端开发", "headPortrait": "https://blz.nosdn.127.net/1/2018cosplay/fourth/tesss.jpg", "id": "46e0fb00111111111", "likeTotal": 0, "replyContents": [], "replyId": "46e0fb001ec222222222", } ], "replyId": "", "topicId": "46e0fb001ec3333333", } ], "curPage": 1, //当前页面 //通过ID 判断 当前用户点赞了 哪些评论 "likes": [ "46e0fb00111111111", "46e0fb001ec222222222", "46e0fb0066666666", ], "nextPage": null, //下一页 "pageSize": 20, //一页总共多少评论 "total": 7, //总共多少页面 }, "msg": "success", "status": "success" } [代码] [代码]<!-- HTML 部分 --> <block wx:if="{{commentList.length>0}}"> <!-- 评论模块 --> <block wx:for="{{commentList}}" wx:for-item="item" wx:for-index="index" wx:key="idx"> <view class="commentItem" catchtap="_goToReply" data-contentid="{{item.id}}" data-replyid="{{item.id}}" data-battle-tag="{{item.displayName}}"> <view class="titleWrap"> <image class="logo" src="{{item.headPortrait||'默认图'}}"></image> <view class="authorWrap"> <view class="author">{{item.displayName}}</view> <view class="time">{{item.createTime}}</view> </view> <view class="starWrap" catchtap="_clickLike" data-index="{{index}}" data-like="{{item.like}}" data-contentid="{{item.id}}" data-topicid="{{item.topicId}}"> <text class="count">{{item.likeTotal||""}}</text> <view class="workSprite icon {{item.like?'starIconHasClick':'starIcon'}}"></view> </view> </view> <view class="text"> {{item.contentText}} </view> </view> <!-- 评论的评论 --> <block wx:for="{{item.replyContents}}" wx:for-item="itemReply" wx:for-index="indexReply" wx:key="idxReply"> <view class="commentItem commentItemReply" catchtap="_goToReply" data-contentid="{{itemReply.id}}" data-replyid="{{item.id}}" data-battle-tag="{{itemReply.displayName}}"> ... 和上面类似 </view> </block> </block> <!-- 加载更多loading --> <block wx:if="{{isOver}}"> <view class="more">评论加载完成</view> </block> </block> [代码] 通过node提供一个API接口,通过用户的openId来判断是否点赞,这里提供一个参考的JSON结构。 JSON尽量做成array循环的结构方便渲染,根据ID来BAN人和管理。底部加上加载更多的效果,同时,记得做一些兼容,比如默认头像等。 3、评论中的一些微信原生交互 这里建议很多交互如果不是必须要特别定制,可以才用微信原生的组件,效果和兼容性都有保障,而且方便简单。 对评论进行回复/举报 [代码]<!-- HTML部分 通过绑定事件:_goToReply 进行交互--> <view class="commentItem" catchtap="_goToReply" data-contentid="{{item.id}}" data-replyid="{{item.id}}" data-battle-tag="{{item.displayName}}"> ... 内部省略 </view> [代码] [代码]//JS部分 微信原生wx.showActionSheet 显示操作菜单交互 _goToReply(e) { // 上面的各种授权判断省略... let self = this; wx.showActionSheet({ itemList: ['回复', '举报'], success: function (res) { if (!res.cancel) { console.log(res.tapIndex); //前往评论 if (res.tapIndex == 0) { //判断是否是 评论的评论 self._goToComment(replyid); } //举报按钮 if (res.tapIndex == 1) { //弹出框 self.setComplain(contentid); } } else { //取消选择 } }, fail(res) { console.log(res.errMsg) } }); } //当选择“举报”的时候,二次调用 wx.showActionSheet 方法 setComplain(contentid){ let complainJson = ["敏感信息", "色情淫秽", "垃圾广告", "语言辱骂", "其它"]; wx.showActionSheet({ itemList: complainJson, success: async res => { if (!res.cancel) { //选择好后,提交举报 try { let complainResult = await request.postComplainReport(complainJson[index], openid, contentid); if (complainResult.msg == "success") { //提交成功后反馈 } else { } } catch (e) { console.log(e) } } } }); } [代码] 显示操作菜单 wx.showActionSheet 方法说明 属性 类型 说明 itemList Array.<string> 按钮的文字数组,数组长度最大为 6 itemColor string 按钮的文字颜色 success function 接口调用成功的回调函数 fail function 接口调用失败的回调函数 complete function 接口调用结束的回调函数(调用成功、失败都会执行) 使用这个方法,即是主流的做法,也能很好的兼容不同机型,同时给予用户“习惯性体验”。 原生评论排序切换 [图片] [代码]<!-- picker组件 html部分--> <picker bindchange="bindPickerChange" value="{{index}}" range="{{array}}"> <view class="picker"> 当前选择:{{array[index]}} </view> </picker> [代码] [代码]// js部分 Page({ data:{ //查看评论类型切换 array: ["最佳", "最新", "只看自己"], //选择数组中的第几个显示 index:0 }, bindPickerChange(e) { console.log('picker发送选择改变,携带值为', e.detail.value) this.setData({ index: e.detail.value }) } }) [代码] picker组件是一个从底部弹起的滚动选择器,这里我们用它来切换不同评论的排序。每次切换都可以通过 bindchange获得对应的变化,通过 e.detail.value获取用户选择的索引值。 官方文档: https://developers.weixin.qq.com/miniprogram/dev/component/picker.html 4、传参跳转写评论页 [代码]let uriData = { logo: "xxx.jpg", type: "commentReply", title: "文章:小程序评论,动态发帖开发指北\n 作者:智酷方程式", openId:"xxxxxxxxxxx", replyId:"aaaaaa" //用户回复的是哪个评论的ID }; wx.navigateTo({ url: `/components/plugins/comment/submit/index?uriData=${encodeURIComponent(JSON.stringify(uriData))}` }); [代码] 这个可以用encodeURIComponent的方式处理下参数中的中文,避免跳转发布评论页接收数据时出现乱码。 5、发表评论页 [图片] 显示和控制评论的字数 [代码]<!-- html部分 关于textarea 的配置 --> <view class='feedback-cont'> <textarea auto-focus="true" value="{{replyName}}" maxlength="200" bindinput="textareaCtrl" placeholder-style="color:#999;" placeholder="留下评论,共同学习,一起进步" /> <view class='fontNum'>{{content.length}}/200</view> </view> <view class='feedback-btn' bindtap='commentSubmit'>提交</view> [代码] [代码]// js部分 Page({ data: { //初始化评论内容,如果是回复则通过传参变成 @xxxx的形式 content: "@xxxx", }, textareaCtrl: function (e) { if (e.detail.value) { this.setData({ content: e.detail.value }) } else { this.setData({ content: "" }) } } }) [代码] textarea 在小程序中改动不大,这个标签原有的一些属性都可以继续使用,通过配置maxlength来控制字数,同时,设置auto-focus="true"可以让用户进到这个发表评论页面时自动弹出虚拟键盘和光标定位在输入的区域。 当然,也可以将发表评论和评论展示区域做在一起,这个就要考虑到要么通过“小程序API”获取键盘高度,要么将“发布评论”置顶区域显示,也是可以做的,只是相对考虑的点会多些。当时开发评论组件的时候,考虑开发时间短和用户体验,权衡后,最终决定以上方案,希望能给到大家一些参考和借鉴,在其他组件开发中触类旁通。 总结,“组件化思想”对于无论做小程序、react/VUE还是其他项目来说,减少重复开发,提高复用性都是一个非常重要的点。评论功能其实只要理清楚整体思路,做起来难度并不大,通过一些原生组件,可以大大提高开发效率,同时保证良好的兼容性。 后面一期还将分享下功能点较多的发帖组件开发。 往期回顾: [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二) [填坑手册]小程序目录结构和component组件使用心得
2021-09-13 - 如何实现一个简单的http请求的封装
好久没发文章了,最近浏览社区看到比较多的请求封装,以及还有在使用原始请求的童鞋。为了减少代码,提升观赏性,我也水一篇吧,希望对大家有所帮助。 默认请求方式,大家每次都这样一些写相同的代码,会不会觉得烦,反正我是觉得头大 😂 [代码]wx.request({ url: 'test.php', //仅为示例,并非真实的接口地址 data: { x: '', y: '' }, header: { 'content-type': 'application/json' // 默认值 }, success (res) { console.log(res.data) } }) [代码] 来,进入正题吧,把这块代码封装下。 首先新建个request文件夹,内含request.js 代码如下: [代码]/** * 网络请求封装 */ import config from '../config/config.js' import util from '../util/util.js' // 获取接口地址 const _getPath = path => (config.DOMAIN + path) // 封装接口公共参数 const _getParams = (data = {}) => { const timestamp = Date.now() //时间戳 const deviceId = Math.random() //随机数 const version = data.version || config.version //当前版本号,自定或者取小程序的都行 const appKey = data.appKey || config.appKey //某个小程序或者客户端的字段区分 //加密下,防止其他人随意刷接口,加密目前采用的md5,后端进行校验,这段里面的参数你们自定,别让其他人知道就行,我这里就是举个例子 const sign = data.sign || util.md5(config.appKey + timestamp + deviceId) return Object.assign({}, { timestamp, sign, deviceId, version, appKey }, data) } // 修改接口默认content-type请求头 const _getHeader = (headers = {}) => { return Object.assign({ 'content-type': `application/x-www-form-urlencoded` }, headers) } // 存储登录态失效的跳转 const _handleCode = (res) => { const {statusCode} = res const {msg, code} = res.data // code为 4004 时一般表示storage里存储的token失效或者未登录 if (statusCode === 200 && (code === 4004)) { wx.navigateTo({ url: '/pages/login/login' }) } return true } /** * get 请求, post 请求 * @param {String} path 请求url,必须 * @param {Object} params 请求参数,可选 * @param {String} method 请求方式 默认为 POST * @param {Object} option 可选配置,如设置请求头 { headers:{} } * * option = { * headers: {} // 请求头 * } * */ export const postAjax = (path, params) => { const url = _getPath(path) const data = _getParams(params) //如果某个参数值为undefined,则删掉该字段,不传给后端 for (let e in data) { if (data[e] === 'undefined') { delete data[e] } } // 处理请求头,加上最近比较流行的jwtToken(具体的自己百度去) const header = util.extend( true, { "content-type": "application/x-www-form-urlencoded", 'Authorization': wx.getStorageSync('jwtToken') ? `Bearer ${wx.getStorageSync('jwtToken')}` : '', }, header ); const method = 'POST' return new Promise((resolve, reject) => { wx.request({ url, method, data, header, success: (res) => { const result = _handleCode(res) result && resolve(res.data) }, fail: function (res) { reject(res.data) } }); }) } [代码] 那么如何调用呢? [代码]//把request的 postAjax注册到getApp()下,调用时: const app = getApp() let postData = { //这里填写请求参数,基础参数里的appKey等参数可在这里覆盖传入。 } app.postAjax(url, postData).then((res) => { if (res.success) { //这里处理请求成功逻辑。 } else { //wx.showToast大家觉得麻烦也可以写到util.js里,调用时:util.toast(msg) 即可。 wx.showToast({ title: res.msg || '服务器错误,请稍后重试', icon: "none" }) } }).catch(err => { //这里根据自己场景看是否封装到request.js里 console.log(err) }) [代码] config.js 主要是处理正式环境、预发环境、测试环境、开发环境的配置 [代码]//发版须修改version, env const env = { dev: { DOMAIN: 'https://dev-api.weixin.com' }, test: { DOMAIN: 'https://test-api.weixin.com', }, pro: { DOMAIN: 'https://api.qtshe.com' } } module.exports = { ...env.pro } [代码] 以上就是简单的一个request的封装,包含登录态失效统一跳转、包含公共参数的统一封装。 老规矩,最后放代码片段,util里内置了md5方法以及深拷贝方法,具体的我也不啰嗦,大家自行查看即可~ https://developers.weixin.qq.com/s/gbPSLOmd7Aft
2020-04-03 - 时间选择器组件之关于table走过的弯路
为了提高开发者的研发效率,提升产品品质,我们提供一套基于饿了么UI实现的UI组件库,TMAP-UI—旨在解决的组件地图场景下应用的问题。 最近在新增[代码]TMAP-UI[代码]组件库的开发过程中,时间选择器是开发者反馈需求较多的一个组件,今天把在开发过程中遇到的一些问题分析给大家。[代码]element-ui[代码]有原生的时间选择器,但是,在我们的交互设计师是根据地图实际应用场景中的特性抽象组件,[代码]element-ui[代码]的样式以及交互操作都无法满足设计需求,需要基于源码进行二次开发,最小成本实现这个通用组件。今天主要聊下开发中遇到的一些关于table的实现问题,对table一探究竟… 关于table特点 首先,我们先来基本了解一下[代码]table[代码]。 [代码]<table> <caption>A summary of the UK's most famous punk bands</caption> <thead> <tr> <th scope="col" class="fixed-width-test2">Band</th> </tr> </thead> <tbody> <tr> <th scope="row">Buzzcocks</th> <td>1976</td> <td>9</td> <td>Ever fallen in love (with someone you shouldn't've)</td> </tr> </tbody> </table> [代码] 一个基本的table布局主要由[代码]table[代码]、[代码]tbody[代码]、[代码]th[代码]、[代码]td[代码]等标签组成。W3C标准中,table的每个标签都自带了[代码]display[代码]样式。例如td标签默认就会有[代码]display:table-cell[代码]的样式,在这个默认样式下,新增margin的设置是不会生效的。更详细标签对应样式可以查看W3C标准。 关于时间选择器 [图片] 时间选择器设计图 根据上图为设计师给出的时间选择器的设计图,选择时间范围是一个重要的新增功能。 实现它的步骤拆分成以下几步: 1.实现基本表格布局 2.添加日期范围选择的样式 实现基本表格布局 首先我们需要按照设计图调整日期间的间距和每个日期的单元格大小。所以,需要对于每个子格设置宽、高和[代码]margin[代码]进行布局。 [图片] [图片] 从上图左侧可以看出,样式在style成功设置的情况下,右侧盒模型的宽和[代码]margin[代码]都没有生效。 我首先猜测是不是哪里的样式压盖了,又或者是样式本身因为某些原因导致没有生效。 于是使用常用的样式压盖方法,在没生效的样式后添加[代码]!important[代码]来提高设置样式的优先级,但还是没有生效。 后来查了W3C的关于table的设置说明,发现了上文提到的td自带的[代码]display:table-cell[代码]样式。于是尝试在样式中新增[代码]display:block[代码]替代原有table自带样式。果然当我设置之后,子td的宽高和margin都生效了。 查阅资料之后发现,一方面td标签不允许设置[代码]margin[代码],另一方面,在table整体的宽度设定后,样式不会按照td标签中设置的宽度执行,会按照整体的table宽度均分给每一个子td。如果希望一些特殊的效果,对宽度设置百分比是可以生效的。这其实能够理解,如果在[代码]table-cell[代码]的模式下允许某个子模块设置宽度和[代码]margin[代码],会导致子模块的宽度和高度不可预期,对同一行的其他子块的宽高造成影响。总结下,为了保证table的样式整体联动性,table宽高和子td的宽高设置不能同时使用绝对数值来设置,只能设置其中一个绝对值,另外一个用相对值来计算划分。 如果我们通过[代码]display:block[代码]完成格式,整体的表格结构就会被打破,产生更多不符合表格逻辑预期的样式问题。于是我们通过每个子单元格均分的特性,设置总的行宽和行高来控制单元格自身的大小和之间的距离,从而达到实现整体表格布局的效果。 添加日期范围和选择样式 盒子宽度问题 那当我们hover态的盒子和背景颜色的盒子大小不一样的时候,我们就需要在td的内部内置一个盒子。这样通过设置颜色我们就可以实现背景。按照上述,只要我们设置好整体的table宽度和背景颜色,就可以得到日期选中范围的设计背景效果,对应的背景颜色得到如下结果。 [图片] 设定整体宽度之后,我们发现部分子td之间存在一条深色的边界线。这是由于他们之间出现了互相压盖,整体的七个td的总宽度设计稿给出是242px,按照预期是应该七个子模块均分宽度。但我查看具体每个子td的宽度发现,有的子td宽度是34、有的是35。这也就是说,在宽度不能够被子模块整除均分的情况下,table自动为我们做了取整,并且为了保持总宽度的不便,有些被向下取整,有些被向上取整。 [图片] [图片] 目前想到的解决办法是,通过计算固定宽度的总宽度使得每一个td能够被整除。 选中态背景问题 接下来,我们需要解决在选中态下起终点日期的样式问题。按照原有的选中样式无论是否添加背景都不能满足需求。见下图,会出现10号日期右侧空白或26号日期多余背景的情况。 [图片] 对于起始日期,要完成上述要求,我们需要将起始框以及它的右侧作为块内容并且仍能保证水平居中。解决办法是,对于10号日期的选择起点,将左侧设置[代码]margin[代码],右侧设置相应宽度的[代码]padding[代码]。 做到这里基本可以认为完成了一个日期范围选择功能。 但实战发现,还没完… 当用户只选择了起点我们会发现选中态导致右侧的边界还会有蓝色背景色。于是,我们还要利用scss的语法特性为样式设置生效条件来区分两种不同的显示策略。 [图片] border-radius问题 设计稿要求,每一行的选择框的起始和末尾需要有圆角。这样一个常见的需求,table果然没让我失望,对tr标签设置无效。原因是因为在[代码]border-collapse[代码]设置为[代码]separate[代码]下,对于所有table标签设置圆角都无法生效。 查阅资料,找到了两种思路的解决方法。 第一种就是对tr的子td, [代码]first-child/last-child[代码]设置圆角。讲到这里就不得不提table的一个属性:[代码]border-collapse[代码]。这个属性用来决定表格的边框是分开的还是合并的。在分隔模式下,相邻的单元格都拥有独立的边框。在合并模式下,相邻单元格共享边框。这里有三个常用值:[代码]inherit[代码], [代码]separate[代码],[代码]collapse[代码]。[代码]separate[代码]表示每个单元格拥有独立的边框,inherit表示相邻的单元格共用同一条边框。 当在separate模式下,我们还可以通过设置border-spacing设置边框的宽度。 [图片] w3c border-spacing配图 第二种当设置[代码]border-collapse:inherit[代码],便可以设置td和tr的圆角了。 总结 在table布局下,有许多自己样式布局,在我们熟练掌握了之后会发现有很多方便的特性。我也把这次遇到的问题沉淀成了表格,希望能对大家以后的开发有点帮助,少走弯路。 [图片] 产品推广 地图组件是专为移动端定制的轻应用产品,支持各手机端主流浏览器,可以实现位置展示、路线规划、地图选点、前端定位等多种场景的下的调用。相比于JS API, 可以通过高度参数化的URL直接调用,极大简化开发的复杂度,降低维护成本。欢迎大家体验!
2020-07-17 - 小程序笔记
1、通常情况下小程序的背景色backgroundColor要和页面的颜色设置成同一颜色。 2、justify-content 设置的是主轴上的对齐方式,而align-items 设置的是交叉轴上的对齐方式。通过观察flex-direction的值来判断 竖直方向还是水平方向哪一条是主轴。若flex-direction: column;那么竖直方向上为主轴,若flex-direction:row,那么水平方向上为主轴。 3、当一行内所有元素的宽度相加超过屏幕的宽度时,flex布局会将每一个元素进行压缩,以保证所有的元素都能显示在同一行内。为了让元素换行,可以使用flex-wrap: wrap。 4、小程序中的像素单位rpx可以根据设备的屏幕进行自适应。若一个字体设置成22px,那么不管设备是IP5还是IP6都会显示同样大的字,但是使用了rpx作为单位之后,在IP5上显示的字会小于IP6上的字。 5、在全局样式表(app.wxss)中定义的样式,只有font和color才会被组件继承,其他的样式都不会被组件继承(这样确保了组件的封闭性)。但是几乎所有的样式都可以被page给继承。 6、在设计组件时,尽量不要让组件产生一些无意义的空白。 7、使用bind:tap="onClick"来为页面元素绑定响应时间,此处为单击事件。 catch:tap可以阻止冒泡事件。 8、组件复用,代码分离。 9、一般不会将请求后台的代码写在组件中。如果组件中需要请求后台了应该是model的文件夹内创建相应的JS文件去请求后台,例如demo中的like.js。 10、在小程序的JS文件中,声明的data对象是该js文件中的私有变量,properties是公开的属性,外部可以访问,如果需要从外部传递进来,那么就需要将属性声明在properties中。不要在data和properties中声明相同的变量,那样会覆盖掉其中的一个变量。 11、修改data对象中的属性要使用this.setData方法来设置。 12、不要在properties的属性中,修改属性本身的值。 index: { type: Number, observer: function(newVal, oldVal, changedPath) { //当组件的值被改变时,会主动的调用observer //newVal:改变之后的值。 //oldVal:改变之前的值。 let val = newVal < 10 ? ‘0’ + newVal : newVal; this.setData({ index:val//错误的写法。会导致内存泄漏。 }); } } 13、在小程序中使用缓存的时候,要确定哪些部分是可以被缓存的,哪些是不能缓存的。(页面会实时发生变化的内容就是不能被缓存的) 14、小程序内置的标签是可以使用hidden=“{{true}}”属性来控制其显示与否的。但是开发者自己编写的组件就无法使用hidden属性来控制其显示或影藏(除非放在自己开发组件的所在WXML文件的view标签内)。 可以使用 wx:if="{{true}}" 来控制自己编写的组件的隐藏与否。 如果需要频繁的切换组件的显示或隐藏,那么微信官方推荐使用hidden,而如果不是频繁切换的话,那么微信官方推荐使用wx:if来控制组件的显示或隐藏。 15、在老板小程序中存在模板template这个概念,在template中可以提取共用的wxml和wxss内容实现组件的元素共用。新的小程序中,可以创建一个common.wxss文件。然后再所要引用的wxss文件中,使用@import “…/common.wxss”;将样式导进来 在小程序中播放音乐有两种方式,老的那种方式存在一定的bug,建议采用新的播放方式:背景音乐播放管理wx.getBackgroundAudioManage()来做。 16、小程序中的behavior可以多继承,当父类中存在一个属性,并且子类中也声明了该属性时,子类中的属性会覆盖掉父类中的那个属性(两个属性的名字相同,但是类型不同)。但是声明周期函数不会覆盖,而是以此执行。 17、使用 @import “…/common.wxss”; 可以为wxml文件引入公用的样式。 18、16和17分别解决了在小程序中复用js和wxss的问题,在wxml也可以通过模板的方式进行复用,但是在组件中复用wxml的话带来的意义并不是特别的大。 19、navi组件和music组件之间的通信,可以通过classic组件进行传递。子组件(navi)通过事件的方式将数据传递给父组件(classic),然后父组件再传递给另外一个子组件(music)。 20、wx:key 如果wx:for后面遍历的是一个object,可以使用object下的某一个属性来作为wx:key的取值,且该属性需要满足不重复且是数字或者是字符串。如果wx:for需要遍历的是一个数组或者字符串的话,那么wx:key后面的取值是*this。 21、在小程序中使用wx.navigateTo()进行小程序页面的跳转。让组件去进行页面的跳转会降低组件的通用性。如果在主页面中进行跳转,需要在组件的js文件中使用triggerEvent将组件内的参数传递到主页面,再在主页面中进行页面跳转。如何取舍?如果编写的组件不会和其他的项目进行共用,那么就可以在组件内部进行页面跳转。 22、组件之间的传参是通过properties中的属性进行传参的。而页面之间的传参是通过onload生命周期函数的options参数中。const id=option.id。就能接收到从外部传入的id了。 23、slot,插槽。感觉上像是一个占位符,可以在组件的外部向组件内部传递一个wxml标签。如果不传递,也不会有任何的显示。 在组件中需要声明属性 options:{ multipleSlots:true }, <view class=‘container’> <text>{{text}}</text> <slot name=“after”></slot> //这里预留一个插槽。 </view> 使用的时候需要将传入的标签,包裹在组件标签的内部: <block wx:for="{{comments}}"> //block用于循环,不是slot的相关知识点。 <v-tag text="{{item.content}}"> <text slot=“after”>{{’+’+item.nums}}</text> //after是插槽的名字。 </v-tag> </block> 24、在v-tag标签中加入属性 tag-class=“ex-tag”,然后在tag组件的js文件中写上externalClasses:[‘tag-class’],然后在再tag.wxml文件的view标签中增加class=‘tag-class’,这样就可以引入外部样式了。如果标签中存在多个样式,那么可能会造成冲突。样式加不上去。如果外部样式想要覆盖普通样式,可以使用!important就可以覆盖普通样式了。 .ex-tag{ background-color:#effbdd !important; } 25、WXS相当于在html标签中直接调用JS代码,可以用来写小程序的过滤器。小程序中的WXS只支持ES5的语法。WXS中文本并不默认解析转义字符例如 。当需要解析这些转义字符时,可以在调用过滤器的标签上添加属性decode。例如:<text class=‘content’ decode=’{{true}}’>{{util.format(book.summary)}}</text> 中的 decode=’{{true}}’ 26、下滑加载更多有两种实现方式:scroll-view和Page的onReachBottom,推荐使用onReachBottom。onReachBottom在组件中无法使用,所有要在Page中使用,并结合12点中的observer来实现下滑后,加载更多内容的动作。(在组件中定义一个属性,然后生成随机字符串或者随机数,在page中每次触发onReachBottom后,更改组件中属性的值,从而使用observer)。 27、小程序中,属性的名字尽量不要用驼峰命名法。在js中声明了一个属性openType,在wxml文件中使用的时候,使用连字符来调用。open-type=“xxxx”。 28、获取授权:老版本使用wx.getUserInfo来弹窗询问是否授权。新版本需要使用小程序中的组件button来获取授权。 在.wxml文件中,使用<button open-type=“getUserInfo” bindgetuserinfo=“getUser”>授权</button>
2020-03-09 - 让小程序页面和自定义组件支持 computed 和 watch 数据监听器
习惯于 VUE 或其他一些框架的同学们可能会经常使用它们的 [代码]computed[代码] 和 [代码]watch[代码] 。 小程序框架本身并没有提供这个功能,但我们基于现有的特性,做了一个 npm 模块来提供 [代码]computed[代码] 和 [代码]watch[代码] 功能。 先来个 GitHub 链接:https://github.com/wechat-miniprogram/computed 如何使用? 安装 npm 模块 [代码]npm install --save miniprogram-computed [代码] 示例代码 [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, }, computed: { sum(data) { return data.a + data.b }, }, }) [代码] [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, sum: 2, }, watch: { 'a, b': function(a, b) { this.setData({ sum: a + b }) }, }, }) [代码] 怎么在页面中使用? 其实上面的示例不仅在自定义组件中可以使用,在页面中也是可以的——因为小程序的页面也可用 [代码]Component[代码] 构造器来创建! 如果你已经有一个这样的页面: [代码]Page({ data: { a: 1, b: 1, }, onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }) [代码] 可以先把它改成: [代码]Component({ data: { a: 1, b: 1, }, methods: { onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }, }) [代码] 然后就可以用了: [代码]const computedBehavior = require('miniprogram-computed') Component({ behaviors: [computedBehavior], data: { a: 1, b: 1, }, computed: { sum(data) { return data.a + data.b }, }, methods: { onLoad: function() { /* ... */ }, myMethod1: function() { /* ... */ }, myMethod2: function() { /* ... */ }, }, }) [代码] 应该使用 [代码]computed[代码] 还是 [代码]watch[代码] ? 看起来 [代码]computed[代码] 和 [代码]watch[代码] 具有类似的功能,应该使用哪个呢? 一个简单的原则: [代码]computed[代码] 只有 [代码]data[代码] 可以访问,不能访问组件的 [代码]methods[代码] (但可以访问组件外的通用函数)。如果满足这个需要,使用 [代码]computed[代码] ,否则使用 [代码]watch[代码] 。 想知道原理? [代码]computed[代码] 和 [代码]watch[代码] 主要基于两个自定义组件特性: 数据监听器 和 自定义组件扩展 。其中,数据监听器 [代码]observers[代码] 可以用来监听数据被 [代码]setData[代码] 操作。 对于 [代码]computed[代码] ,每次执行 [代码]computed[代码] 函数时,记录下有哪些 data 中的字段被依赖。如果下一次 [代码]setData[代码] 后这些字段被改变了,就重新执行这个 [代码]computed[代码] 函数。 对于 [代码]watch[代码] ,它和 [代码]observers[代码] 的区别不大。区别在于,如果一个 data 中的字段被设置但未被改变,普通的 [代码]observers[代码] 会触发,但 [代码]watch[代码] 不会。 如果遇到问题或者有好的建议,可以在 GitHub 提 issue 。
2019-07-24 - 小程序跳转页面加载优化
适应场景: 小程序页面跳转redirect/navigate/其它方式 分析: 从用户触发跳转行为到下一个页面onload生命周期函数内时间差会有500ms左右,如果在页面跳转之后进行onload函数内才开始去加载页面数据,那么这500ms左右的时间就浪费了。 改进: 在页面触发跳转行为的处理函数里结合promise预先加载下个页面的数据,并将promise对象缓存,此时页面跳转和加载数据同时进行,到了目标页面再取出缓存的promise对象进行判断和取数据操作。 效果: 跳转页面加载速度提高了600ms。 示例: 代码结构 [图片] pageManager.js [代码]// 写在utils里的公用方法 const pageList = {}; module.exports = { putData:function(pageName, data){ pageList[pageName] = data; }, getData:function(pageName){ return pageList[pageName]; } } [代码] util.js [代码]const myPromise = fn => obj => { return new Promise((resolve, reject) => { obj.complete = obj.success = (res) => { resolve(res); } obj.fail = (err) => { reject(err); } fn(obj); }) } module.exports = { myPromise : myPromise } [代码] index.js [代码]// 跳转页面 const {myPromise} = require('../../utils/util'); const pageManager = require('../../utils/pageManager'); page({ data: { }, onLoad:function(){ }, gotoPageA:function(){ const PromisePageA = myPromise(wx.request)({ url : '' }).then((res)=>{ return res.data; }) pageManager.putData('pageA',promisePageA); wx.navigateTo({ url: 'pages/pageA/pageA' }) } }) [代码] pageA.js [代码]// 被跳转页面 const util = require('../../utils/util.js'); const pageManager = require('../../utils/pageManager'); const {myPromise} = require('../../utils/util'); Page({ data:{ logs:[] }, onLoad: function(){ const promisePageA = pageManager.getData('pageA'); if(promisePageA){ const resData = promisePageA.then( function(data){ }, function(){ console.log("err"); } ) } } }) [代码]
2019-10-31 - 小程序性能和体验优化方法
[图片] 小程序应避免出现任何 JavaScript 异常 出现 JavaScript 异常可能导致小程序的交互无法进行下去,我们应当追求零异常,保证小程序的高鲁棒性和高可用性 小程序所有请求应响应正常 请求失败可能导致小程序的交互无法进行下去,应当保证所有请求都能成功 所有请求的耗时不应太久 请求的耗时太长会让用户一直等待甚至离开,应当优化好服务器处理时间、减小回包大小,让请求快速响应 避免短时间内发起太多的图片请求 短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术或在屏幕外的图片使用懒加载 避免短时间内发起太多的请求 短时间内发起太多请求会触发小程序并行请求数量的限制,同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等 避免 setData 的数据过大 setData工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 常见的 setData 操作错误 频繁的去 setData Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层 染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时 每次 setData 都传递大量新数据 由setData的底层实现可知,数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程 后台态页面进行 setData 当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行 避免 setData 的调用过于频繁 setData接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 避免将未绑定在 WXML 的变量传入 setData setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗 合理设置可点击元素的响应区域大小 我们应该合理地设置好可点击元素的响应区域大小,如果过小会导致用户很难点中,体验很差 避免渲染界面的耗时过长 渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 避免执行脚本的耗时过长 执行脚本的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要确认并优化脚本的逻辑 对网络请求做必要的缓存以避免多余的请求 发起网络请求总会让用户等待,可能造成不好的体验,应尽量避免多余的请求,比如对同样的请求进行缓存 wxss 覆盖率较高,较少或没有引入未被使用的样式 按需引入 wxss 资源,如果小程序中存在大量未使用的样式,会增加小程序包体积大小,从而在一定程度上影响加载速度 文字颜色与背景色搭配较好,适宜的颜色对比度更方便用户阅读 文字颜色与背景色需要搭配得当,适宜的颜色对比度可以让用户更好地阅读,提升小程序的用户体验 所有资源请求都建议使用 HTTPS 使用 HTTPS,可以让你的小程序更加安全,而 HTTP 是明文传输的,存在可能被篡改内容的风险 不使用废弃接口 使用即将废弃或已废弃接口,可能导致小程序运行不正常。一般而言,接口不会立即去掉,但保险起见,建议不要使用,避免后续小程序突然运行异常 避免过大的 WXML 节点数目 建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长 避免将不可能被访问到的页面打包在小程序包里 小程序的包大小会影响加载时间,应该尽量控制包体积大小,避免将不会被使用的文件打包进去 及时回收定时器 定时器是全局的,并不是跟页面绑定的,当页面因后退被销毁时,定时器应注意手动回收 避免使用 css ‘:active’ 伪类来实现点击态 使用 css ‘:active’ 伪类来实现点击态,很容易触发,并且滚动或滑动时点击态不会消失,体验较差 建议使用小程序内置组件的 ‘hover-*’ 属性来实现 滚动区域可开启惯性滚动以增强体验 惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 [代码]-webkit-overflow-scrolling: touch[代码] 的样式
2019-03-15 - 浅谈小程序运行机制
摘要: 理解小程序原理… 原文:浅谈小程序运行机制 作者:小白 Fundebug经授权转载,版权归原作者所有。 写作背景 接触小程序有一段时间了,总得来说小程序开发门槛比较低,但其中基本的运行机制和原理还是要懂的。“比如我在面试的时候问到一个关于小程序的问题,问小程序有window对象吗?他说有吧”,但其实是没有的。感觉他并没有了解小程序底层的一些东西,归根结底来说应该只能算会使用这个工具,但并不明白其中的道理。 小程序与普通网页开发是有很大差别的,这就要从它的技术架构底层去剖析了。还有比如习惯Vue,react开发的开发者会吐槽小程序新建页面的繁琐,page必须由多个文件组成、组件化支持不完善、每次更改 data 里的数据都得setData、没有像Vue方便的watch监听、不能操作Dom,对于复杂性场景不太好,之前不支持npm,不支持sass,less预编译处理语言。 “有的人说小程序就像被阉割的Vue”,哈哈当然了,他们从设计的出发点就不同,咱也得理解小程序设计的初衷,通过它的使用场景,它为什么采用这种技术架构,这种技术架构有什么好处,相信在你了解完这些之后,就会理解了。下面我会从以下几个角度去分析小程序的运行机制和它的整体技术架构。 了解小程序的由来 在小程序没有出来之前,最初微信WebView逐渐成为移动web重要入口,微信发布了一整套网页开发工具包,称之为 JS-SDK,给所有的 Web 开发者打开了一扇全新的窗户,让所有开发者都可以使用到微信的原生能力,去完成一些之前做不到或者难以做到的事情。 但JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题,比如受限于设备性能和网络速度,会出现白屏的可能。因此又设计了一个增强版JS-SDK,也就是“微信 Web 资源离线存储”,但在复杂的页面上依然会出现白屏的问题,原因表现在页面切换的生硬和点击的迟滞感。这个时候需要一个 JS-SDK 所处理不了的,使用户体验更好的一个系统,小程序应运而生。 快速的加载 更强大的能力 原生的体验 易用且安全的微信数据开放 高效和简单的开发 小程序与普通网页开发的区别 小程序的开发同普通的网页开发相比有很大的相似性,小程序的主要开发语言也是 JavaScript,但是二者还是有些差别的。 普通网页开发可以使用各种浏览器提供的 DOM API,进行 DOM 操作,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。 普通网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。 网页开发者在开发网页的时候,只需要使用到浏览器,并且搭配上一些辅助工具或者编辑器即可。小程序的开发则有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目等等过程方可完成。 小程序的执行环境 [图片] 小程序架构 一、技术选型 一般来说,渲染界面的技术有三种: 用纯客户端原生技术来渲染 用纯 Web 技术来渲染 用客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染 通过以下几个方面分析,小程序采用哪种技术方案 开发门槛:Web 门槛低,Native 也有像 RN 这样的框架支持 体验:Native 体验比 Web 要好太多,Hybrid 在一定程度上比 Web 接近原生体验 版本更新:Web 支持在线更新,Native 则需要打包到微信一起审核发布 管控和安全:Web 可跳转或是改变页面内容,存在一些不可控因素和安全风险 由于小程序的宿主环境是微信,如果用纯客户端原生技术来编写小程序,那么小程序代码每次都需要与微信代码一起发版,这种方式肯定是不行的。 所以需要像web技术那样,有一份随时可更新的资源包放在云端,通过下载到本地,动态执行后即可渲染出界面。如果用纯web技术来渲染小程序,在一些复杂的交互上可能会面临一些性能问题,这是因为在web技术中,UI渲染跟JavaScript的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源。 所以最终采用了两者结合起来的Hybrid 技术来渲染小程序,可以用一种近似web的方式来开发,并且可以实现在线更新代码,同时引入组件也有以下好处: 扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力 体验更好,同时也减轻 WebView 的渲染工作 绕过 setData、数据通信和重渲染流程,使渲染性能更好 用客户端原生渲染内置一些复杂组件,可以提供更好的性能 二、双线程模型 小程序的渲染层和逻辑层分别由 2 个线程管理:视图层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS脚本。 [图片] [图片] 那么为什么要这样设计呢,前面也提到了管控和安全,为了解决这些问题,我们需要阻止开发者使用一些,例如浏览器的window对象,跳转页面、操作DOM、动态执行脚本的开放性接口。 我们可以使用客户端系统的 JavaScript 引擎,iOS 下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境。 这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。 这就是小程序双线程模型的由来: 逻辑层:创建一个单独的线程去执行 JavaScript,在这里执行的都是有关小程序业务逻辑的代码,负责逻辑处理、数据请求、接口调用等 视图层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以视图层存在多个 WebView 线程 JSBridge 起到架起上层开发与Native(系统层)的桥梁,使得小程序可通过API使用原生的功能,且部分组件为原生组件实现,从而有良好体验 三、双线程通信 把开发者的 JS 逻辑代码放到单独的线程去运行,但在 Webview 线程里,开发者就没法直接操作 DOM。 那要怎么去实现动态更改界面呢? 如上图所示,逻辑层和试图层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。 这也就是说,我们可以把 DOM 的更新通过简单的数据通信来实现。 Virtual DOM 相信大家都已有了解,大概是这么个过程:用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。 如图所示: [图片] 1. 在渲染层把 WXML 转化成对应的 JS 对象。 2. 在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。 3. 经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。 我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。 而这样一个完整的框架,离不开小程序的基础库。 四、小程序的基础库 小程序的基础库可以被注入到视图层和逻辑层运行,主要用于以下几个方面: 在视图层,提供各类组件来组建界面的元素 在逻辑层,提供各类 API 来处理各种逻辑 处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑 由于小程序的渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库。 小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端。 这样可以: 降低业务小程序的代码包大小 可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包 五、Exparser 框架 Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。 Exparser的主要特点包括以下几点: 基于Shadow DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。 小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。 内置组件 基于Exparser框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。 六、运行机制 小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台状态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。 小程序没有重启的概念 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁 [图片] 七、更新机制 小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。 八、性能优化 主要的优化策略可以归纳为三点: 精简代码,降低WXML结构和JS代码的复杂性; 合理使用setData调用,减少setData次数和数据量; 必要时使用分包优化。 1、setData 工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 2、常见的 setData 操作错误 频繁的去 setData在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时; 每次 setData 都传递大量新数据由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程, 后台态页面进行 setData当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。 总结 大致从以上几个角度分析了小程序的底层架构,从小程序的由来、到双线程的出现、设计、通信、到基础库、Exparser 框架、再到运行机制、性能优化等等,都是一个个相关而又相互影响的选择。关于小程序的底层框架设计,其实涉及到的还有很多,比如自定义组件,原生组件、性能优化等方面,都不是一点能讲完的,还要多看源码,多思考。每一个框架的诞生都有其意义,我们作为开发者能做的不只是会使用这个工具,还应理解它的设计模式。只有这样才不会被工具左右,才能走的更远!
2019-06-14 - 微信小程序性能优化实践
三个月前接手一个小程序项目,用户量大,交互复杂,性能一般。想法设法做了各种优化,取得了不错的效果,也在过程中收获了一些经验。昨天在公司内部做了一个技术分享,总结了一些优化经验,顺便整理为本文。 提高加载性能 典型的web类的应用,跑起来都需要3步: 第一步,加载运行环境; 第二步,下载代码; 第三步,执行代码,渲染页面。 小程序也如此。第一步加载运行环境,是微信实现的,开发者无需优化。 第二步下载代码,减少首次下载的包的大小,可提高速度。代码压缩、分包加载、预加载等操作,web应用需要借助webpack等打包工具才能实现,而小程序只需修改配置就行了,方便很多。 注意,subpackages配置路径外的目录将被打包到 app(主包)中。原项目在根目录下存了全局组件、样式、图片,导致主包非常大,一度逼近主包大小上线2M。这次把分包里的才会用到的组件,拆到分包所在的文件夹中;把图片资源放到CDN中。这样主包中只留下主包用拿到的代码和组件: [图片] 如果图片资源不多,可以考虑放在代码里,方便管理。但如果图片资源较大,建议使用CDN,这样图片就是在第三步(渲染页面)时才下载,而不是在第二步下载代码时一起下载,能够更快显示骨架屏,优化用户体验。 [图片] 第三步执行代码,优化项包括骨架屏、接口时序调整、懒加载等。其他优化方向还包括,使用数据预拉取,在下载代码阶段提前获取业务数据。这个跟业务高度相关,此处就不展开细讲。 控制setData数据量 setData是小程序开发中使用最频繁的接口,也最容易引发性能问题。每次setData都会触发页面的重新渲染,如果渲染量计算量过大,很容易造成页面卡顿,影响用户体验。原理可以参考官方文档,这里只说实践:不要频繁setData,每次setData数据量不要太大。下面举两个例子。 前端经常会遇到"超长的列表"的难题。因业务需求,后台返回的数据不做分页,一个列表几百甚至上千项,如果直接通过setData写进去,页面一下子就卡死了。优化方案是做前端的分页加载:收到数据后把全部数据存在一个变量里,只取第一页的数据setData更新页面。等用户下拉触发更新的时候,再继续setData下一页的数据。 长度实在太大的情况下,卡顿不再仅仅是setData的问题,而是节点过多导致占用内存过大,这时还可以试下循环列表。 另一个例子是两个列表的数据对比。先说下业务背景:先查商品列表,再查购物车列表,如果该改商品的在购物车列表中有数据,则要更新列表上的数据。之前的做法一直都是循环一遍对比列表,再整个列表数据进行setData,当购物车数量越来越多的时候,就开始卡死了。这次优化引入了Worker,每次购物车更新的时候,在Worker中进行diff运算,算出需要更新列表的哪几项,再进行setData,大大减少了setData的数据量,性能有了明显的改善。 其他细节优化 响应客户滑动事件的需求,使用WXS。之前是把页面touchmove事件绑到js中的逻辑,这样会导致页面的响应不够及时。这次优化把滑动相关的事件都做到wxs里,滑动的体验大大改善。原理涉及小程序的框架视图层(Webview)和逻辑层(App Service)通信机制,官方文档有详细说明。 适当选择同步或异步API。小程序里有很多同步的Api(结尾带Sync的都是),都是阻塞性的。举个例子,常用的wx.getStorageSync()至少消耗50ms运行时间,如果是游戏等性能敏感的项目,卡顿大约3帧。积少成多,用户体验的差异就很明显了。能用异步的地方,尽量不要偷懒用同步的API。 还有一些代码逻辑优化,算法优化等,此处不再展开细讲。推荐一本老书《高性能JavaScript编程》。 总结 以上优化思路,并不局限于微信小程序。小程序的原理和web高度相似,很多优化经验,都可以和web互相借鉴参考。个人总结的三大优化原则:1.使用合理的技术架构;2.养成良好的编码习惯;3.遇到瓶颈之前不要瞎优化。 这两年来小程序生态蓬勃发展,追加了许多实用的功能,开发配套也日益完善。跨页面数据通信可以用Omix,做多端同构用kbone,开发体验比以前有了很多的改善。现在越来越多的产品选择微信小程序平台,体验好、文档完善、兼容性不错、学习曲线平滑、容易找工作,非常推荐新手入坑。 [图片]
2020-07-01 - 如何用小程序开发app
背景 都知道小程序的体验要比app里面直接嵌入h5的体验要好,都知道小程序其实也是运行在app上的。那么我们为什么不能用小程序来开发app呢?这样不仅可以小程序和app只要开发一次,小程序和app都有了。还可以实现app动态更新不需要提交应用市场审核,我们只要做个小程序载体的app壳(类微信端小程序 sdk),而且体验效果也接近原生。做一个类似小程序平台把我们现在的app项目框架从组件化改成为小程序平台构架。 [图片] 每个业务程序都是一个个小程序,原生提供原生能力。 什么是小程序 首先我们要知道小程序是啥?现在市场上的小程序有很多,微信小程序、百度小程序、支付宝小程序、字节跳动小程序等但都差不多。与传统app相比,小程序无需安装、卸载,运行在微信、百度、支付宝等这样大型app载体上。虽然每种小程序都差不太多,但都定义了自己的开发语言和规范,这对开发者来说也是不少的麻烦。 小程序是介于web网页应用和原生应用的一种产物; [图片] 小程序和Hybrid APP的关系 原以为Hybrid APP就是用app的webview去加载一个h5文件,然后webview通过js桥梁和原生通信,实现js调用h5方法,h5方法调原生方法。来弥补h5无法拍照、打电话等不足。但是做出来的效果h5都会有短暂白屏,体验也无法达到原生效果(提供一个简单的demo)。后来接触了小程序,觉得小程序的体验和原生差不了多少了,可以说不是专业人员基本是看不出区别的,原先以为小程序是类似RN、weex这样的原生渲染,后面才知道它也是webview渲染。竟然也是hybird app,那它的体验是什么上去的?都是webview渲染,为什么小程序的体验会比普通的h5好?这让我非常感兴趣,于是就开始了小程序的深入了解。 初步了解用h5做Hybrid APP和小程序的区别: 相同点: 1.都是webview渲染 2.都是js通过桥接和原生通信 3.都可以调用原生组件 4.都可以把资源文件下载本地加载渲染 不同点: 1.Hybrid是html 有dom操作,小程序是虚拟dom 屏蔽了直接对dom操作 2.小程序有服务层,负责处理业务逻辑和数据处理 3.小程序页面有原生页面的生命周期管理 4.小程序tab和bar是原生控件 5.小程序类web不是h5 6.小程序基于微信跨平台 小程序原理 下面以微信小程序为例,进一步展开小程序原理 都知道微信小程序有自己的开发语言,wx开头的方法也不少,那它是什么转化为微信app能识别的语言呢?微信开发工具开发完提交审核,审核通过下发到微信端的是什么样的文件呢?带着这些问题我查阅了很多资料,小程序在技术架构上非常清晰易懂。JS负责业务逻辑的实现,而表现层则WXML和WXSS来共同实现,前者其实就是一种微信定义的模板语言,而后者类似CSS。但是语法毕竟是自定义的,所以要么在下发的之前进行编译,要么就是在渲染的时候进行转化成webview能够识别的语法。我们发现这2个节点微信都做了处理,拿到下发到微信端的wxapkg格式的小程序包,解开后都是js和html已经不是我们开发的WXML和WXSS格式了。但是这一个个html直接用浏览器打开却是空白的。没错<body></body>里面是空的,渲染的时候动态加进入内容的。 1.小程序是如何编译的 我们先来看看打包编译这层,微信都做了些啥呢?微信的打包和编译都在服务端进行,我这边找了个类似的来描述下,不一定准确,只能参考下。 检测app.json文件是否存在 清空并创建指定的输出目录 根据service.html模板,带上版本信息输出到指定的目录中 读取配置文件app.json,将其注入到app-config.js中,输出到指定的目录中 读取所有小程序代码中所有的JS文件,同时判断其是否在app.json中定义,如果其没被定义也不是app.js,说明其为引入的module, 将这些JS路径名存入一个数组中,并确保app.js和页面文件放置在数组尾部 遍历JS文件数组并读取它们,根据用户设置项判断是否使用Babel将其转换为es5的代码 把js模块封装成CommonJS模块,并合并成app-service.js这个文件输出 根据app.json里的pages配置,遍历每个页面根据页面wxml,wxss生成相应的页面文件并合成page-frame.html 其他步骤应该都不难理解,我认为最难的应该是wxml,wxss生成相应的页面文件,这个页面不是普通的html文件,前面也说过它的body是空的。如果你安装了微信的开发工具的化你可以找下是否有wcc和wcsc这2个小工具。wxss 转换成了css,wxml转换成了inject_js,实际上就是virtual_dom。openVendor命令可以在小程序中获取到构建脚本wcc和wcsc,以及各个版本小程序的执行SDK***.wxvpkg,这个SDK也可以用Wechat-app-unpack解开,解开后里面就有WAService.js和WAWebview.js等代码。 根据 /Users/***/Library/Application Support/微信web开发者工具/WeappVendor 路径来找到微信开发者工具目录,以及查看工具集成的核心类。可以看到我们和熟悉的也很重要的WAService.js和WAWebview.js2个文件也在里面。不过代码都是加密混淆的,没有可读性。 [图片] 2.编译好的小程序包如何下发解析 再看下下面这张图,微信下发的wxapkg格式的文件(每个小程序都是这样的一个包),这个文件可以通过从越狱的iPhone或者root的安卓手机上拿到。有部分人用charles通过https抓包拿到了下载链接,也拿到了包。解压出来就是这样的目录格式。简单解释下这每个文件的作用以及是什么来的: [图片] app-config.json:小程序的整体配置文件,里面是一个json 主要包括page、entryPagePath、pages、global、tabBar、ext、extAppid等 { “page”: { “pages/shop/index.html”: { “window”: { “enablePullDownRefresh”: false } }, “pages/goods/detail.html”: { “window”: { “navigationBarTitleText”: “商品详情”, “enablePullDownRefresh”: false } }, “pages/order/address/list.html”: { “window”: { “navigationBarTitleText”: “地址列表”, “enablePullDownRefresh”: false, “backgroundTextStyle”: “light” } }, … }, “entryPagePath”: “pages/shop/index.html”, “pages”: [“pages/shop/index”, “pages/order/detail/logisticsmap”,…], “global”: { “window”: { “backgroundTextStyle”: “light”, “navigationBarBackgroundColor”: “#f1f1f1”, “navigationBarTitleText”: " ", “navigationBarTextStyle”: “black” } }, “ext”: { “api”: { }, “form”: { }, “name”: “小店” }, “extAppid”: “########” } page节点:管理每个页面的整体设置,比如原生navigationBar的标题样式、是否需要下拉刷新等可以说这些设置都是对原生的ViewController的一个设置。没错一个小程序页面都有一个对应的原生页面,所以它可以封装原生页面的生命周期暴漏给小程序页面使用,包括原生的navigationBar和tabBar、下拉刷新控件等。这也是为什么小程序如果屏蔽调自带的navigationBar,自己定义navigationBar 下拉刷新也要自己重新做的原因。自定义navigationBar就不再是原生的了,它默认下拉刷新效果是在原生navigationBar下的整个webview做动画效果,这显然满足不了我们的需求。 entryPagePath:这个节点就简单了,小程序的入口页面的配置 pages节点:页面路径数组 对应小程序源代码里面的app.json文件里面的pages节点 global节点:全局设置和全局变量等;window 整个小程序的私有页面都起作用,也就是说每个页面没有自己单独设置都直接用这个全局设置的效果。 ext、extAppid节点:对应的就是ext.json文件 第三方平台部署小程序才需要 tabBar节点:对原生tabBar的样式设置和页面路径和图标等配置 app-service.js文件:这个是小程序一个很重要的文件,小程序体验好一个很重要的环节。从下图截取的代码片段很容易看出这个文件就是小程序里面全部js文件内容的集合。通过__wxRoute来路由,wxRouteBegin=true来标记启始页面。 var wxAppData = wxAppData || {}; ** var wxRoute = wxRoute || “”; var wxRouteBegin = wxRouteBegin || “”; var wxAppCode = wxAppCode || {}; var global = global || {}; var WXML_GLOBAL=WXML_GLOBAL || {}; var wxAppCurrentFile=wxAppCurrentFile||""; ** var Component = Component || function(){}; var definePlugin = definePlugin || function(){}; var requirePlugin = requirePlugin || function(){}; var Behavior = Behavior || function(){}; [代码]define("app.js", function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,XMLHttpRequest,WebSocket,Reporter,webkit,WeixinJSCore){ [代码] “use strict”;App({onLaunch:function(){var e=this,o=wx.getStorageSync(“logs”)||[];o.unshift(Date.now()),wx.setStorageSync(“logs”,o),wx.login({success:function(e){}}),wx.getSetting({success:function(o){o.authSetting[“scope.userInfo”]&&wx.getUserInfo({success:function(o){e.globalData.userInfo=o.userInfo,e.userInfoReadyCallback&&e.userInfoReadyCallback(o)}})}})},globalData:{userInfo:“hello world”,text:“hello world”}}); ** }); require(“app.js”); wxRoute = ‘pages/page2/page2’;wxRouteBegin = true; define(“pages/page2/page2.js”, function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,XMLHttpRequest,WebSocket,Reporter,webkit,WeixinJSCore){ “use strict”;Page({data:{},onLoad:function(n){},onReady:function(){},onShow:function(){},onHide:function(){},onUnload:function(){},onPullDownRefresh:function(){},onReachBottom:function(){},onShareAppMessage:function(){}}); }); require(“pages/page2/page2.js”);**** 3.小程序如何渲染 除了小程序每个页面的js还包含了app.js 这个包含小程序的整个生命周期管理逻辑的js文件内容也都在这个文件里面。在微信端里面小程序的sdk会有一个单独的webview来加载app-service.js文件当作这个小程序的服务层,负责每个页面逻辑处理,而且这个服务在这个小程序的整个生命周期它是一直在的,每个页面的js文件都已经压缩在这个文件里面,并在小程序服务启动的时候已经加载到内存中,所以在点击按钮需要做逻辑交互的时候体验会那么快。 [图片] App Service(逻辑层)主要就是由app-service.js文件和集成在微信app里面的WAService.js组成,如一个页面加载需要网络请求就是由逻辑层处理请求参数并交给原生来进行请求,原生把请求到的数据返回给App Service(逻辑层)进行数据处理,最后把处理好的数据通过原生JSBridge传给view(试图层)进行渲染。对 逻辑层和视图层没有直接的交互,逻辑服务层和视图层也不在一个线程里面,2个webview 只能通过原生来进行通信。 几个文件夹没啥特别的 就是和你微信小程序开发的目录是一样,放的都是你的页面和组件的html文件。但是值得一提的是这里面的html文件内容很少,它不是一个完整的页面,可以说这个页面的样式,静态内容都不在这个html里面。里面放的是这个页面css、路由路径和加载入口方法的调用generateFunc: $gwx(’./pages/order/rights/index.wxml’)。可以看下我的hello world页面代码,就更加清晰了。 <!–pages/page2/page2.wxml–> <text class=“pageText”>hello world page2</text> /* pages/page2/page2.wxss */ .pageText { background-color: red; width: 100%; height: 100rpx; display: flex; align-items: center; justify-content: center; } 这是一个很简单的小程序页面代码,那它编译后的html页面代码是什么样的呢?下面我展示出来给大家看。 <style> </style> <page></page> <script> var setCssStartTime = Date.now(); setCssToHead([".",[1],“pageText { background-color: red; width: 100%; height: “,[0,100],”; display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center; }\n”,],undefined,{path:"./pages/page2/page2.wxss"})() var setCssEndTime = Date.now(); document.dispatchEvent(new CustomEvent( “generateFuncReady”, { detail: { generateFunc: $gwx( ‘./pages/page2/page2.wxml’ ) }})) </script> 发现了啥?“hello world page2“ 这样页面关键内容不见了,那它是如何渲染的?懂js的人估计很容易就可以看到这个页面的入口方法generateFunc: $gwx 是的 这个方法是一个很重要的方法。那么这个方法在哪里?“hello world page2“ 这样页面关键元素又在哪里?肯定是跟着这个资源包一起下发的。对的所以的页面标签、元素 内容都在page-frame.html文件里面。也就是wxml文件的代码都编译压缩到page-frame.html文件里面,而对应页面的html文件只放对应的wxss文件代码和入口js代码。而入口方法的触发是由原生app调用js"generateFuncReady"事件触发调用。原生sdk这块后面可以以OC为例贴出对应代码,再展开说明下。 page-frame.html文件:这个文件应该是一个小程序包里面最大一个文件,所以里面的代码也不好都贴出来,简单介绍下里面的组成和作用。第一大块:模版 View(视图)层如何绘制每个页面都是公共的,如何把vdom渲染到webview上。$gwx方法就在这个模版里面,一个页面的入口。 [图片] 再找找我们刚才html页面消失的关键元素“hello world page2“在哪里?把每个元素都转化为Z数组了,每个页面都是一个类似这样的代码块。由于混淆和压缩加大了我们阅读的难度,但是如果你以为看完这个文件就完了,那就错了。View(视图)层除了这几个html文件外 还需要一个很重要的文件,那就是放在微信app包里面的WAWebview.js文件。和服务层里面也有一个WAService.js文件配合使用达到和原生交互的效果。 [图片] 和原生交互这块是属于小程序框架,所以肯定不会在下发的小程序包里面,除了原生sdk代码之外,还有2个很重要的js文件就是上面提到的WAService.js文件和文件。接下来我们就开始简单了解下: 为了方便理解我整理了js和原生的一些API,有app(小程序)级别的、有页面级别的、有原生组件级别的。可见js和原生交互是非常频繁的,可以说每个操作都是需要提供View(视图)层的WAWebview.js调用原生的桥梁需要原生处理就原生处理后再调用app Service(逻辑)层的WAService.js 由WAService.js通知 逻辑层处理对应逻辑,再把处理结果返回到原生。 [图片] 小程序在App中执行时的时候分为三个不同的模块,View/Service/Native,各司其职。View和Service都在WKWebView中执行,互相无法调用,不直接操作DOM。他们之间通过Native层通信。Native和WebView之间通过webkit.messagehandler和evaluateJavascript互相调用。小程序借助的是JSBridge实现了对底层API接口的调用,所以在小程序里面开发,开发者不用太多去考虑IOS,安卓的实现差异的问题,安心在上层的视图层和逻辑层进行开发即可。 WeixinJSBridge.publish: view和service之间的透传,在WKWebView之间传递消息。 WeixinJSBridge.subscribe: 注册监听,监听view和service之间的消息调用。 WeixinJSBridge.invoke: View或者Service传递消息到Native,然后Native使用逻辑调用js callback。 WeixinJSBridge.on:监听Native的事件。 [图片] 启动小程序服务startAppWithAppInfo根据appid等基本信息判断小程序是否已经下载到本地,没有的话下载解压加载配置信息等。然后进入manager,manager其实也是分为几部分一个是小程序级别的管理,一个是单个小程序的管理。具体的可以通过下面的类图更加直观。 [图片] 下图显示了小程序启动时会从CDN和服务器校验和下载资源。也就是是小程序启动的时候会有点慢的主要原因,还有一些时间就是需要初值化小程序本地服务。 [图片] 4.小程序的生命周期 关于小程序的生命周期,可以两个部分来理解:应用生命周期(左侧蓝色部分)和页面生命周期(右侧绿色部分)。 其中应用的生命周期是这样一个流程:1、用户首次打开小程序,触发 onLaunch(全局只触发一次)。2、小程序初始化完成后,触发onShow方法,监听小程序显示。3、小程序从前台进入后台,触发 onHide方法。4、小程序从后台进入前台显示,触发 onShow方法。5、小程序后台运行一定时间,或系统资源占用过高,会被销毁。 页面生命周期是这样的一个流程:1、小程序注册完成后,加载页面,触发onLoad方法。2、页面载入后触发onShow方法,显示页面。3、首次显示页面,会触发onReady方法,渲染页面元素和样式,一个页面只会调用一次。4、当小程序后台运行或跳转到其他页面时,触发onHide方法。5、当小程序有后台进入到前台运行或重新进入页面时,触发onShow方法。6、当使用重定向方法wx.redirectTo(OBJECT)或关闭当前页返回上一页wx.navigateBack(),触发onUnload。同时,应用生命周期会影响到页面生命周期。 [图片] 用Page 实例说明的页面的生命周期 [图片] 由上图可知,小程序由两大线程组成:负责界面的视图线程(view thread)和负责数据、服务处理的服务线程(appservice thread),两者协同工作,完成小程序页面生命周期的调用。 视图线程有四大状态: 初始化状态:初始化视图线程所需要的工作,初始化完成后向 “服务线程”发送初始化完成信号,然后进入等待状态,等待服务线程提供初始化数据。 首次渲染状态:当收到服务线程提供的初始化数据后(json和js中的data数据),渲染小程序界面,渲染完毕后,发送“首次渲染完成信号”给服务线程,并将页面展示给用户。 持续渲染状态:此时界面线程继续一直等待“服务线程”通过this.setdata()函数发送来的界面数据,只要收到就重新局部渲染,也因此只要更新数据并发送信号,界面就自动更新。 结束状态:页面被回收或者销毁、应用被系统回收、销毁时触发。 服务线程五大状态: 初始化状态:此阶段仅启动服务线程所需的基本功能,比如信号发送模块。系统的初始化工作完毕,就调用自定义的onload和onshow,然后等待视图线程的“视图线程初始化完成”号。onload是只会首次渲染的时候执行一次,onshow是每次界面切换都会执行,简单理解,这就是唯一差别。 等待激活状态:接收到“视图线程初始化完成”信号后,将初始化数据发送给“视图线程”,等待视图线程完成初次渲染。 激活状态:收到视图线程发送来的“首次渲染完成”信号后,就进入激活状态既程序的正常运行状态,并调用自定义的onReady()函数。此状态下就可以通过 this.setData 函数发送界面数据给界面线程进行局部渲染,更新页面。 后台运行状态:如果界面进入后台,服务线程就进入后台运行状态,从目前的官方解读来说,这个状态挺奇怪的,和激活状态是相同的,也可以通过setdata函数更新界面的。毕竟小程序的框架刚推出,应该后续会有很大不同吧。 结束状态:页面被回收或者销毁、应用被系统回收、销毁时触发。 小程序在App中的应用场景 说了这么多技术理论,最后说下小程序在项目中如何应用。整个项目都是小程序不现实,毕竟小程序的定义是轻量级的,像IM、消息等用原生肯定比小程序更加适合,所以用小程序和原生混合开发是不可少的。还有一个让你不得不混合开发的一个重要原因,你的app不是一个新项目,是一个现有的原生app,一次性用小程序重新做一次不现实,所以混合会是最好的选择。我这边做的混合开发不是技术层面的,我们都知道小程序是很原生通信很频繁的,它需要原生提供各种能力才能到达接近原生的体验,所以本身就是一个混合。而我这里说的混合是指业务层面的混合开发,打破我们以往对小程序的认知。不管是百度小程序还是微信小程序都是运行在他们生态下的一个独立应用程序。比如一个商城小程序它不会有部分页面是原生部分页面是小程序,也只会有一个入口,一个出口。而我们要用小程序来开发app,我们app有自己的需求我们需要让小程序看起来像原生页面一样,对用户来说它还是一个app,不存在哪个页面是原生哪个页面是原生的。所以一切都是从技术层面来说,就是小程序和原生进行混合开发的业务app。你可以理解为你的app里面嵌入h5一样的开发模式,只是小程序页面比一般的h5页面交互体验要好一些而已。 [图片] 我可以从原生页面跳转到小程序的任何页面,如果必要的话也可以从小程序页面跳转到原生页面。所以小程序服务不能在进入小程序页面的时候才启动,也不能因为回到原生页面而销毁。必须根据你的业务场景来调用控制。 以上是个人对与小程序开发app的一些浅薄看法,期待和业界同仁共同探讨。你有什么想法呢?欢迎评论交流。 最后感谢业界各位大佬的贡献,在这里附上我的参考文献: https://blog.csdn.net/ListenToSennTyou/article/details/53258163 https://www.jianshu.com/p/92c6a75c2323 https://blog.csdn.net/xiangzhihong8/article/details/66521459 https://yq.aliyun.com/articles/72825?t=t1 https://github.com/weidian-inc/hera-cli https://www.cnblogs.com/viaiu/p/9935602.html https://www.jianshu.com/p/51ac882ea9f4 http://lrdcq.com/me/read.php/66.htm https://github.com/wdfe/weweb
2019-05-30 - 用开发者工具控制开发环境和生产环境
最近开发过程中遇到一个尴尬的情况,发版前调试一下,API地址改到了测试环境,改完OK发版。。。结果尴尬了,测试地址忘了改回来。审核等待前功尽弃。。。。 这种事情对于匆匆忙忙写bug,匆匆忙忙发版的程序猿来说肯定会发生第二次,第三次。遇到问题,解决问题。 开始翻看找小程序的api文档,于是找到了 wx.getSystemInfo 但是这个API只能区分是pc还是移动端并没有其他有用的信息 接下来去开发社区 https://developers.weixin.qq.com/community/develop/article/doc/000ec87cdd8070c3ba89fe00051813 突然看到这篇文章很有道理,虽然是想直接把作者的方法拿来用,但是想到要在运行时环境通过一个try cath判断控制变量心里有点发虚,再想想。 不过作者提到了灵感来自于node,那是不是可以直接写一个node脚本进行预编译呢。想想小程序的开发者工具还是很open的,兴许可以呢。毕竟还提供了命令行调用,可以考虑一下从这里入手(这太麻烦了。。。。) https://developers.weixin.qq.com/miniprogram/dev/devtools/cli.html 又玩弄了一番开发者工具,终于发现了一个有趣的功能,开发者工具竟然提供了自定义处理命令(编译前的钩子) [图片] 在执行相应的操作前钩子可以执行shell命令,那不是说预览和上传之前都可以调用node脚本了吗。 流程 :预览,上传前》执行调用node脚本命令,带上环境参数》node脚本运行 获取命令行参数》根据参数生成不同环境中使用的配置代码》重新写入api配置文件》预览,上传 [图片] api.template.js 用作config文件模板,需要根据开发环境动态生成的变量可以用占位符的形式写在里面 [代码]var WxApiRoot = '%%API_URL%%'; module.exports=WxApiRoot; [代码] env.config.js 用于配置不同开发环境的配置文件 [代码]const envConfig = { DEV: { API_URL: 'https://dev.com', }, PROD: { API_URL: 'https://pro.com', }, }; module.exports = envConfig; [代码] 接下来是node脚本的入口文件: 首先在项目根目录安装: [代码]cnpm install node-notifier --save-dev [代码] 选择安装,目的是为了编译之后提醒开发者配置文件预编译结果 脚本内容: [代码]const fs = require('fs'); const exec = require('child_process').exec; const envConfig = require('./config/env.config'); const _arguments = process.argv.splice(2); const path = require('path'); const [MINIENV = 'DEV'] = _arguments; const notifier = require('node-notifier'); const templatePath = './config/api.template.js'; const configPath = './config/api.js'; const config = envConfig[MINIENV]; //读取配置文件模板 let templateConfig = fs.readFileSync(templatePath, 'utf8'); //拿到那些变量需要动态编译 const configKeys = Object.keys(config); //通过遍历替换的方式替换模板中的占位符 configKeys.forEach((key) => { const regText = `%%${key}%%`; const regObj = new RegExp(regText, 'g'); templateConfig = templateConfig.replace(regObj, config[key]); toast(`编译环境:${MINIENV}--${key}`, config[key]); }); //把替换后的模板字符串写入api文件中 fs.writeFileSync(configPath, templateConfig); /** * @description 警告提示,方便开发者看到编译过后的配置文件的变量信息 * @author songs * @date 2020-04-27 * @param {*} title * @param {*} content */ function toast(title, content) { console.log(path.join(__dirname, './', 'login-bg.png')); notifier.notify({ title: title, message: content, icon: path.join(__dirname, './', 'login-bg.png'), }); } [代码] 这样 每次预览或者上传之前都可以执行一次node脚本了,config文件预编译完成,再上传。虽然还是没有从根本上解决问题,但是如果配上一个上线流程规范,应该可以应付一大半的环境区分问题吧。 预编译用到的文件不需要上传,可以再project.config里设置忽略上传 [代码] "packOptions": { "ignore": [ { "type": "file", "value": "config/api.template.js" }, { "type": "file", "value": "config/env.config.js" }, { "type": "file", "value": "login-bg.png" } ] }, [代码] 结束!!
2020-04-29 - Lin UI —— 一套设计精良的微信小程序组件库
[图片] 你好,我是小桔,是一个没有感情的代码崽。 今天我想向你推荐一套船新全新的微信小程序组件库:Lin UI,是兄弟就用它! 简介 Lin UI 是由林间有风团队精心打造的一套微信小程序组件库,组件丰富、设计优美,并且拥有完整的商业案例,是您开发微信小程序的不二选择。 官方文档 Github 链接 码云 Gitee 链接 林间有风团队首页 PS:这篇文章不会介绍 Lin UI 如何使用,我仅向你介绍它的优点,如果想要了解使用方法欢迎前往文档官网查看 特点 现在市面上已经有了许多组件库:Vant Weapp、iView Weapp、Color UI、Wuss Weapp、Wux Weapp 等等等等,他们每一套也都能称得上是成熟的组件库。那我为什么还要使用 Lin UI 呢? Lin UI 吸引我的地方主要有如下几点: 使用简单 设计好看 组件丰富 反馈及时 设计好看 Lin UI 的另一大特点就是视觉设计出众,因为林间有风有专业的 UI 设计师对组件样式进行设计,视觉方面甩其他样式靠开发者自己设计的组件库不止一条街。 文章开头,我罗列了一堆组件,其中有一套,主打视觉,那就是 Color UI,也挺好看(Color UI 的作者也曾是一名设计师)。但是 Color 是一套 css 库,并不是组件库,至于具体区别,可以 Google 一下。 我们先来看几个反面例子,这么”朴实无华“的样式,你真的喜欢吗 [图片] 我们再来看一下 Lin Ui 是什么样子 [图片] [图片] 怎么样,哪个好看应该不用我多说了吧 组件丰富 Lin UI 一共包含 54 个组件,不管你用于什么场景,基本都可以满足你 90% 的需求,剩下的 10% ,可以通过提 Issue 的方式及时得到解决 [图片] 使用简单 开发过小程序的开发者可能都知道,小程序相比传统 Web 来说,会简单很多,因为我们不需要去处理多端设备上的屏幕兼容问题,这是小程序的一个优势。但是,如果因为引入一套组件库,让你失去了开发简单这项优势,那还值得去使用它吗? Vant Weapp 是业内非常成熟的一套组件库,它的开发也历经了很长的周期。但是,在我看来,Vant Weapp 的一大缺点就是组件源码非常复杂。这种复杂并不是说他的逻辑有多复杂,而是 Vant Weapp 对代码做了大量的封装,我们看一下下面的代码: [代码]import { VantComponent } from '../common/component'; import { button } from '../mixins/button'; import { openType } from '../mixins/open-type'; VantComponent({ mixins: [button, openType], data: { baseStyle: '' } )} [代码] 这是 Vant Weapp 的 Button 组件部分源码,我们知道,Button 组件几乎是没有业务逻辑的,但是它却引入了 [代码]VantComponent[代码]、[代码]button[代码]、[代码]openType[代码]三个 ts 文件。 [图片] 是不是一脸懵逼?[代码]VantComponent[代码]是啥?小程序原生的[代码]Component[代码]呢?[代码]button[代码]又是啥?[代码]openType[代码]又是什么鬼?甚至源码还是用 TypeScript 写的,我写个小程序还得先去学一遍 TypeScript ? 如果你刚接触小程序和 Vant Weapp,这种高度封装的特性会导致一个非常严重的问题:**当你在开发中遇到一个文档解决不了的问题想翻看源码时,你会发现你看不懂,你必须得理解其整套设计思想才能看懂其中每个组件的逻辑。**很浪费时间对不对? Lin UI 就完全不存在这个问题,只要你懂 js 识字就能看懂源码,不信你看 [代码]Component({ properties: { /** * 折叠面板类型 */ type: { type: String, value: 'normal' }, }, methods: { /** * 关闭所有打开的collapse-item */ foldAllExpandItem(collapseItem) { for (let i = 0; i < this.data._expandItems.length; i++) { if (collapseItem !== this.data._expandItems[i]) { this.data._expandItems[i].foldContent(); } } this.data._expandItems = []; } }); [代码] 这是 Lin UI [代码]IndexList[代码] 组件的源码,因为篇幅问题,只截取了部分。为什么说只要识字就能看懂,因为我们对方法进行了详细的注释,并且没有对微信原生 api 进行二次封装,完全原生的写法,保证你能看懂源码,这么贴心,就问你感不感动! [图片] 总结:如果你非常熟悉小程序的各种 api 、熟悉 ts、并且在遇见 bug 时有耐心去理解高度封装的设计思想,那么你可以使用 Vant Weapp。反之,如果你刚接触小程序,或者不会 ts 、在遇见 bug 时想要以最快的速度解决 bug,那么 Lin UI 会给你最舒适的开发体验。 反馈及时 一个开源项目,其活跃度是非常重要的。反馈的 Bug 能否得到及时的解决,有好的想法能否的项目作者接纳并加入项目之中,以及使用这个项目的人数。这三点是在进行技术选型时必须要考虑的问题。 Lin UI 的反馈效率非常高,Issue 反馈的 Bug 平均能在 24 小时内得到解决,这已经是非常高的效率了。相比 Vant Weapp,因为其维护者都是在职开发者,所以时间并不会太充裕,Issue 处理速度肯定也会慢于 Lin UI 的。 [图片] 结语 Lin UI 从 2019 年 4 月 24 日 发布第一个版本 0.1.0 以来,经过了一年多的迭代更新,600 多次 commit,300 多个 issue,换来的是现在已经足够稳定且易用的 Lin UI 。如果喜欢,也欢迎到 Github 上贡献一颗 Star 。 我是小桔,欢迎关注我的微信公众号,带你了解更多前后端知识。 [图片]
2020-06-12 - 一文带你彻底学会 Git Hooks 配置
[图片] 你好,我是小桔,是一个没有感情的代码崽。 今天给大家介绍一下 Git Hooks,相信 Git 大家都在用吧,Git 除了用作版本控制,还有许多高级功能,Git Hooks 就是其中之一。 本文环境: Git 版本:2.27.0 Husky 版本:4.2.5 Node.js 版本:12.16.2 前言 做过前端的同学对 Hook 这个东西应该很了解吧,后端也是有 Hook 这种概念的,比如 Java 的[代码]@PostConstruct[代码],也是一种 Hook 的体现。简单来说,Hook 就是在执行某个事件之前或之后进行一些其他额外的操作。 举个栗子,张三现在要吃饭,那么吃饭就是一个事件,吃饭前和吃饭后就可以称为两个钩子。现在你想让张三在吃饭前洗下手,那么我们就可以在吃饭前这个钩子这里,设置一个洗手的动作。张三在每次吃饭前,都会检查一下这个钩子,有什么要做的,都会照做。这样,就实现了我们的需求。 Git 也是如此,在 Git 中也有许多的事件(commit、push 等等),每个事件也是对应了有不同的钩子的(如 commit 前,commit 后),那么我们就可以在这些钩子这里配置一些自己需要执行的操作来实现各种各样的需求。 使用 真实场景 可能初次了解 Git Hooks 的同学会有一些疑问:这个东西到底能干嘛?我以前没用过它不也一样好好的吗?我干嘛要用它? 其实这说得很对,**任何技术都是有需求采用,没有需求就别去硬塞,永远记住:技术为业务服务。**但是这并不妨碍你先去了解它,毕竟,只有你先知道了这项技术能解决什么样的问题,日后当你遇到相应的问题时,你才知道该用什么技术去解决。 这里我给出一个真实的场景,是我们团队在开发 Lin UI 的时候遇到的: 我们的 Git 仓库中包含了编译后的代码,所以每次修改了源码,都需要运行一下编译命令,然后把源码和编译后的代码一起提交到 Git 仓库,这个流程没什么问题。但是,人脑不是电脑,总会有疏忽的时候,经常会出现这样一种情况:修改了源码,却忘记了运行编译命令,最后只把源码提交到了 Git 仓库,导致线上仓库的源码和编译产物不一致、 这个问题虽然不是特别严重,但老是出现也总归不好。所以我们就想了一个办法,不再手动编译,把编译任务交给 CI 去做,这样就不存在这样的问题。 但事情总是没那么顺利,因为我们在本地开发调试的时候是需要编译代码的,所以就会生成一部分编译代码,在使用 Git 时,我们经常会使用[代码]git add .[代码]命令,会把所有修改了的代码都提交到仓库,这显示是不行的。因为现在我们已经把编译交给 CI 去做了,并且为了 Code Review 方便,编译代码不应该再提交到仓库了。如果每次手动去把编译代码去除,又非常麻烦,那该怎么办呢? 这种情况,就可以使用 Git Hooks 帮我们在每次提交前自动把编译代码去掉了。 PS:这个场景虽然不那么常见和通用,但确实是在开发中真实遇见的。 Git Hooks 介绍 Git Hooks 的实现其实非常简单,就是就[代码].git/hooks[代码]文件下,保存了一些 shell 脚本,然后在对应的钩子中执行这些脚本就行了。比如下图中,这是一个还没有配置 Git Hooks 的仓库,默认会有很多[代码].sample[代码]结尾的文件,这些都是示例文件 [图片] 我们打开[代码]pre-commit.sample[代码]文件看一下其中的内容,大致意思是说这是一个示例,做了一些格式方面的检测,这个脚本默认是不生效的,如果要生效,把文件名改为[代码]pre-commit.sample[代码]即可 [图片] [代码]pre-commit[代码]这个钩子是在[代码]git commit[代码]命令执行之前触发 Git 支持的所有钩子见下表(加粗的为常用钩子): Git Hook 调用时机 说明 pre-applypatch [代码]git am[代码]执行前 applypatch-msg [代码]git am[代码]执行前 post-applypatch [代码]git am[代码]执行后 不影响[代码]git am[代码]的结果 pre-commit [代码]git commit[代码]执行前 可以用[代码]git commit --no-verify[代码]绕过 commit-msg [代码]git commit[代码]执行前 可以用[代码]git commit --no-verify[代码]绕过 post-commit [代码]git commit[代码]执行后 不影响[代码]git commit[代码]的结果 pre-merge-commit [代码]git merge[代码]执行前 可以用[代码]git merge --no-verify[代码]绕过。 prepare-commit-msg [代码]git commit[代码]执行后,编辑器打开之前 pre-rebase [代码]git rebase[代码]执行前 post-checkout [代码]git checkout[代码]或[代码]git switch[代码]执行后 如果不使用[代码]--no-checkout[代码]参数,则在[代码]git clone[代码]之后也会执行。 post-merge [代码]git commit[代码]执行后 在执行[代码]git pull[代码]时也会被调用 pre-push [代码]git push[代码]执行前 pre-receive [代码]git-receive-pack[代码]执行前 update post-receive [代码]git-receive-pack[代码]执行后 不影响[代码]git-receive-pack[代码]的结果 post-update 当 [代码]git-receive-pack[代码]对 [代码]git push[代码] 作出反应并更新仓库中的引用时 push-to-checkout 当``git-receive-pack[代码]对[代码]git push[代码]做出反应并更新仓库中的引用时,以及当推送试图更新当前被签出的分支且[代码]receive.denyCurrentBranch[代码]配置被设置为[代码]updateInstead`时 pre-auto-gc [代码]git gc --auto[代码]执行前 post-rewrite 执行[代码]git commit --amend[代码]或[代码]git rebase[代码]时 sendemail-validate [代码]git send-email[代码]执行前 fsmonitor-watchman 配置[代码]core.fsmonitor[代码]被设置为[代码].git/hooks/fsmonitor-watchman[代码]或[代码].git/hooks/fsmonitor-watchmanv2[代码]时 p4-pre-submit [代码]git-p4 submit[代码]执行前 可以用[代码]git-p4 submit --no-verify[代码]绕过 p4-prepare-changelist [代码]git-p4 submit[代码]执行后,编辑器启动前 可以用[代码]git-p4 submit --no-verify[代码]绕过 p4-changelist [代码]git-p4 submit[代码]执行并编辑完[代码]changelist message[代码]后 可以用[代码]git-p4 submit --no-verify[代码]绕过 p4-post-changelist [代码]git-p4 submit[代码]执行后 post-index-change 索引被写入到[代码]read-cache.c do_write_locked_index[代码]后 PS:完整钩子说明,请参考官网链接 Husky 配置 从上面的介绍中,我们知道 Git Hook 保存在 .git 文件夹中。不知你有没有发现这会有一个问题?可能细心的同学已经知道了,Git 是一个多人协作工具,那按理说 Git 仓库中的所有文件都应该被跟踪并且上传至远程仓库的。但是有个例外,[代码].git[代码]文件夹不会,这就导致一个问题,我们在本地配置好 Git Hook 后,怎么分享给其他小伙伴儿呢?copy 吗?那未免太 low 了,都用 Git 了,还 copy,也太不优雅了。这时候,就轮到 Husky 出场了。 Husky 是一个让配置 Git 钩子变得更简单的工具(题外话:Husky 是哈士奇的意思,我猜可能是作者养了条二哈) 下面这些流行的项目都在使用 Husky,可见它确实是一个非常好用的工具: webpack babel create-react-app …… Husky 的原理是让我们在项目根目录中写一个配置文件,然后在安装 Husky的时候把配置文件和 Git Hook 关联起来,这样我们就能在团队中使用 Git Hook 了。 下面开始配置 Husky 第一步 使用 npm 初始化你的项目(如果项目已有 package.json,请跳至第二步) [代码]npm init -y [代码] 第二步 安装 Husky [代码]// 注意 Node.js 版本要 >=10 npm install husky -D [代码] 第三步 书写配置文件,4.2.5 版本的 Husky 共支持以下几种格式的配置文件: .huskyrc .huskyrc.json .huskyrc.yaml .huskyrc.yml .huskyrc.js husky.config.js 个人习惯,这里我采用的是[代码].huskyrc[代码],在其中书写 json 格式的配置,如下: [代码]{ "hooks": { "pre-commit": "git restore -W -S dist examples/dist" } } [代码] 是不是很简单,我们来解读一下这个配置文件。[代码]hooks[代码]这个对象中,key 就是钩子名,而 value 就是需要执行的命令。上面这个配置的含义就是,在每次执行 [代码]git commit[代码]之前,都会把[代码]dist[代码]和[代码]examples/dit[代码]目录下的修改回滚(这两个目录就是编译产生的代码),就不用担心误把编译后的代码提交到仓库中了。 上面我们只写了一条命令,如果想执行两条命令怎么办呢?比如我还想在[代码]git commit[代码]之前用 EsLint 检查一下代码质量,我们可以像下面这样写: [代码]{ "hooks": { "pre-commit": "git restore -W -S dist examples/dist && eslint ." } } [代码] 是的,就是这么简单。如果 EsLint 检测不通过,那么[代码]git commit[代码]是会被阻止的,就不用担心"垃圾代码"被提交到线上仓库了。 Husky 注意事项 Husky 让我们可以很方便的配置 Git Hooks,同时,也提供了一些实用方便的小技巧以及一些我们需要注意的点 不支持的钩子 Husky 不支持服务端 Git 的钩子: pre-receive update post-receive 跳过所有钩子 有时你可能不想运行钩子,那么可以像下面这样跳过: [代码]HUSKY_SKIP_HOOKS=1 git rebase ... [代码] 禁用自动安装 如果你不想 Husky 为你自动安装钩子(比如 clone 了一个第三方的库,想要自己开发时),可以这样做: [代码]HUSKY_SKIP_INSTALL=1 npm install [代码] 最后 本文介绍了 Git Hooks 具体有哪些,并讲解了如何用 Husky 便捷的配置 Git Hook。下一篇文章,我会教你如何用 commitlint 结合 Husky 来规范团队的 commit 信息,如果有兴趣的话,记得一定要关注我哦! 我是小桔,欢迎关注我的微信公众号,带你了解更多前后端知识。 [图片]
2020-06-19 - wx.request 经 Promise 封装后,如何拿到requestTask
大家会用 promise 将 wx.request 包装一层。但经过这么一层包装后,就拿到不到 requestTask,从而调用不了 abort 方法。大家都是如何解决的? 代码来自:https://www.kancloud.cn/xiaoyulive/wechat/526990 [代码]class Request {[代码][代码] [代码][代码]constructor (parms) {[代码][代码] [代码][代码]this[代码][代码].withBaseURL = parms.withBaseURL[代码][代码] [代码][代码]this[代码][代码].baseURL = parms.baseURL[代码][代码] [代码][代码]}[代码][代码] [代码][代码]get (url, data) {[代码][代码] [代码][代码]return[代码] [代码]this[代码][代码].request([代码][代码]'GET'[代码][代码], url, data)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]post (url, data) {[代码][代码] [代码][代码]return[代码] [代码]this[代码][代码].request([代码][代码]'POST'[代码][代码], url, data)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]put (url, data) {[代码][代码] [代码][代码]return[代码] [代码]this[代码][代码].request([代码][代码]'PUT'[代码][代码], url, data)[代码][代码] [代码][代码]}[代码][代码] [代码][代码]request (method, url, data) {[代码][代码] [代码][代码]const vm = [代码][代码]this[代码][代码] [代码][代码]return[代码] [代码]new[代码] [代码]Promise((resolve, reject) => {[代码][代码] [代码][代码]wx.request({[代码][代码] [代码][代码]url: vm.withBaseURL ? vm.baseURL + url : url,[代码][代码] [代码][代码]data,[代码][代码] [代码][代码]method,[代码][代码] [代码][代码]success (res) {[代码][代码] [代码][代码]resolve(res)[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail () {[代码][代码] [代码][代码]reject({[代码][代码] [代码][代码]msg: [代码][代码]'请求失败'[代码][代码],[代码][代码] [代码][代码]url: vm.withBaseURL ? vm.baseURL + url : url,[代码][代码] [代码][代码]method,[代码][代码] [代码][代码]data[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]}[代码] [代码]const request = [代码][代码]new[代码] [代码]Request({[代码][代码] [代码][代码]baseURL: [代码][代码]'http://test'[代码][代码],[代码][代码] [代码][代码]withBaseURL: [代码][代码]true[代码][代码]})[代码] [代码]module.exports = request[代码]
2019-03-22 - xquery, 小程序开发工具包
xquery基于原生小程序,是一套类似于jquery的小程序开发工具库,方便引用且无破坏小程序原生模式。 支持组件元素选取 方便的结构嵌套 弱模板化,方便维护 事件方法重新封装,支持query传递参数 支持钩子方法 支持LRU缓存 支持MARKDOWN富文本 支持HTML富文本 [图片] 无侵入的Pager 使用Pager方法替换小程序Page,其他用法一致。无侵入性语法 [代码]const Pager = require('components/aotoo/core/index') Pager({ data: { itemElement: {...} }, onReady(){ ... } }) [代码] 完整的目录结构 [图片] 实例抓取 类似于在web开发中可以使用[代码]getElementsById[代码]抓取html dom元素对象,下列针对item组件对象实现 wxml模板 [代码]<ui-item item="{{itemElement}}" /> [代码] js [代码]const Pager = require('components/aotoo/core/index') Pager({ data: { itemElement: { $$id: 'item-id', title: '这是一个item组件' } }, onReady(){ let $item = this.getElementsById('item-id') $item.addClass('active') } }) [代码] onReay类似于web中的body.onLoad,所有dom元素都已经准备妥当,使用[代码]getElementsById[代码]去抓取实例 事件封装 query传递参数更贴近web前端开发(事件封装是基于Pager及xquery定义的组件才有效,不会影响原生开发) wxml模板 [代码]<ui-item item="{{itemElement}}" /> [代码] js [代码]Pager({ data: { itemElement: { title: '按钮', tap: 'onTap?username=张三' // 允许query参数 // tap => bind:tap的别名 // aim => catch:tap的别名 // longpress => bind:longpress的别名 // longtap => bind:longtap的别名 // touchstart => bind:touchstart 别名 // touchmove => bind:touchmove的别名 // touchend => bind:touchend的别名 // touchcancel => bind:touchcancel的别名 // catchlongpress => catch:longpress的别名 // ...其他catch方法以此类推 } }, onTap(e, param, inst){ // e => 原生event事件对象 // param => {username: '张三'} // inst => <ui-item />组件实例对象,支持update, addClass, removeClass等方法 inst.update({ title: param.username+'的按钮' }, function(){ let $data = inst.getData() console.log($data.title) // 张三的按钮 }) } }) [代码] 数据缓存及数据过期 [代码]const Pager = require('components/aotoo/core/index') const lib = Pager.lib const dataEntity = lib.hooks('DATA-ENTITY', true) // true表示缓存数据到storage onReay(){ // 将用户信息缓存一天 dataEntity.setItem('names', {name: '', sex: ''}, 24*60*60*1000) setTimeout(()=>{ let namesData = dataEntity.getItem('names') console.log(namesData) // {name: '', sex: ''} },3000) } [代码] LRU缓存设置 小程序本地缓存10M,内存128M,因此数据缓存到本地受到很多限制,比如图片,需要使用LRU缓存机制来对图片/文件进行管理 [代码]const Pager = require('components/aotoo/core/index') const lib = Pager.lib const imgEntity = lib.hooks('IMG-ENTITY', { localstorage: true, // 开启本地缓存 expire: 24 * 60 * 60 * 1000, // 过期时间为1天,所有缓存的数据 max: 50 // 最大缓存数为50条,即当前hooks实例只会存储最常用的50条数据 }) onReay(){ // 将用户信息缓存一天 // img-src => 图片的具体地址或者一个唯一地址 // 挂载一个方法,当该钩子存储数据时 imgEntity.setItem('image-src', {img: 'cloude://....'}) saveImgToLocal('cloude://...') setTimeout(()=>{ let imgsData = imgEntity.getItem('image-src') console.log(imgsData) // {img} || undefined if (!imgsData) { imgEntity.setItem('image-src', {img: 'cloude://....'}) saveImgToLocal('cloude://...') } },3000) } [代码] markdown支持 有两种方式支持markdown文本 组件方式 嵌入方式 嵌入方式比较简单,下面示例markdown文本以嵌入方式实现 [代码]const Pager = require('components/aotoo/core/index') Pager({ data: { itemElement: { "@md": ` ## 马克党标题 基于xquery的基类开发的组件可以直接内嵌使用马克党文档 ` } }, onReady(){ ... } }) [代码] html支持 前端从后端拿去富文本数据,直接转化成小程序模板结构输出,下面示例html文本以嵌入方式实现 [代码]const Pager = require('components/aotoo/core/index') Pager({ data: { itemElement: { "@html": ` <div class="container"> <img src="http://..." /> <span>...</span> <br /> <section> ... ... </section> </div> ` } }, onReady(){ ... } }) [代码] github地址:https://github.com/webkixi/aotoo-xquery 小程序demo演示,下列小程序基于xquery的个人开发,公司的就不放了 xquery [图片] saui [图片] 嘟嘟倒计时 [图片]
2019-12-19 - 网页端管理系统在小程序上的实现
接到一个需求,要把一套现有的网页端管理系统完整的复制到小程序上。开始我是拒绝的,想想网页端那些表格要在手机、特别是小程序上复现就头疼。最后甲方给了我一个可以接受的理由:有了小程序就不用做app了啊。 虽然有难度,但是工作还是要做的,有问题就一点一点来解决。先说一个优势,这个网页端是我最近刚重构过的,改成了前后端分离,接口上用了jwt做登录校验(关于jwt的介绍可以移步:【接口相关】聊一聊数据接口的登录态校验以及JWT),可以直接拿到小程序里来用。接下来说一下实际遇到的问题和我的解决方案。 菜单 这个简单,把网页端左侧菜单栏里筛选出常用的放到小程序的tabBar里就可以了,直接用原生的tabBar,没什么花头。 测试的时候发现了一个bug,已经提交给了官方:XS Max真机调试、预览,原生tabBar上的线不显示? 数据表格 网页端数据列表基本都是使用表格来展现,到了小程序端就不适合再用表格了。一方面是小程序没有原生的表格组件,另一方面是手机屏幕不适合展示很宽的表格(横屏什么的从来不在我的考虑范围内)。 我最终采用的解决方案是用卡片列表的形式来展现数据列表。下图是网页端表格和小程序里对应的卡片列表。 [图片] [图片] 模态框+表单 网页端列表中用到的各种表单基本是在弹出模态框里使用表单,这个在小程序上我改成了放到从底部弹出的半屏弹窗。具体效果对比直接看图,小程序里为了防止弹窗内容太多超出屏幕限制,弹窗加了最大高度限制。 [图片] [图片] 日历 由于需求需要,这个项目有一个通过日历展示一个月的日程安排,先看网页端的效果,这里用的是antd的Calendar日历组件。 [图片] 由于手机屏幕的限制,就算能在小程序页面上展现日历,也没办法合适的展现需要的内容。最终决定小程序端只展示某一日的内容,通过从底部弹出的半屏弹窗里显示的日历来切换日期。 [图片] 还是要有取舍 虽然经过各种修改后,绝大多数功能都改成了适合小程序端展现的方式,但是还是有一些功能实在是不适合放到小程序端,或者从功能上来说没必要放到小程序端,这些就只能放弃了。 [图片]
2020-04-10 - 在小程序中实现 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 - 组件封装的思考
本文是我的小程序开发日记的其中一篇, GitHub 原文地址 欢迎star,感谢万分! 前言 在小程序开发的早期,是没有 自定义组件(component),仅有 自定义模板(template) 的。最早接触到组件开发还是在使用 [代码]React[代码]、[代码]Vue[代码] 框架的时候,熟悉以上两个框架的读者,对小程序的组件应该会有熟悉的感觉,机制和写法差不多 为什么要有组件? 对于这个问题,很多人的第一反应也许是:代码复用 的确,代码复用是组件的核心职责,但它还有更大的使命:性能 因为通过组件封装,可以将页面拆分成多个组件,因此较大粒度的页面就被拆分成粒度较小的组件。当一些数据发生变更导致页面变化时,就只需要重新渲染包含该数据的组件即可,而不用渲染整个页面,从而达到了提高渲染性能的效果 [图片] 生命周期 在 [代码]Vue[代码] 中,每个页面是一个 [代码]Vue[代码] 实例,而组件又是可复用的 [代码]Vue[代码] 实例,因此可以理解成,页面和组件是相同的生命周期 而小程序就将页面和组件拆分成两个类:[代码]Page[代码] 和 [代码]Component[代码],因此接收的生命周期函数也是不一样的。比如,[代码]Page[代码] 接收的是:[代码]onLoad[代码]、[代码]onShow[代码]、[代码]onReady[代码]等函数,而 [代码]Component[代码] 则接收 [代码]created[代码]、[代码]attached[代码]、[代码]ready[代码] 等函数 命名风格都不一致,真是让人头大 [图片] 数据传递 Vue [代码]Vue[代码] 的组件间数据传递的机制是这样的:父组件通过[代码]property[代码]传递数据给子组件,而子组件通过事件通知的形式传递数据给父组件 在页面包含的组件结构还比较简单的时候,这样的机制还是比较好用的。但是,随着业务的复杂度逐渐上升,组件嵌套的层数递增,会出现数据层层传递的困境 为了解决这个问题,[代码]Vue[代码] 推出了 [代码]Vuex[代码] 这样的状态管理工具,集中式存储、管理应用的所有组件的状态。并提出了“单向数据流”的理念: [图片] 小程序 小程序同样有类似的机制,[代码]property[代码]和事件。此外还提供了获取 子组件实例 的方法:[代码]selectComponent()[代码] 和 定义组件间关系的字段 [代码]relations[代码] 其中常用的就是获取子组件实例,比如: [代码]<parent-component> <child-component id="child"></child-component> </parent-component> [代码] 此时,在[代码]parent-component[代码]组件中可以直接获取[代码]child-component[代码]的实例: [代码]Component({ attached() { let $child = this.selectComponent('#child') // $child.doSomeThing() } }) [代码] 实战 背景 制作一个 对话框(modal) 组件 也许有的读者会感到困惑,官方不是有提供 [代码]wx.showModal[代码] 可以直接用吗,为什么要重复造轮子 其实,当你的产品想要结合 [代码]Modal[代码] 和 [代码]Button[代码] 的 [代码]open-type[代码] 能力时,你就会明白重复造轮子的必要性以及[代码]wx.showModal[代码]的局限性 属性定义 对话框的常见属性可以参考[代码]wx.showModal[代码] 除此以外,其中关键的一个属性就是 表示对话框当前的显示状态:[代码]visible[代码] 此时,有两种选择,第一种是将这个变量存在页面上,通过[代码]property[代码]传递给[代码]Modal[代码]组件;另外一种,就是作为[代码]Modal[代码]组件[代码]data[代码]中的一员 property传递 通过[代码]property[代码]传递的话,就相当于将 [代码]Modal[代码] 的控制权交到对应的页面,举例: [代码]<!-- home.wxml --> <modal visible="{{visible}}" /> [代码] [代码]// home.js Page({ data: { visible: false }, toggleModal() { this.setData({ visible: !this.data.visible }) } }) [代码] 此时对应的 [代码]Modal[代码]: [代码]// modal.js Component({ properties: { visible: { type: Boolean, value: false, observer(newVal, oldVal) { this.setData({ visible: newVal }) } } } }) [代码] 这里和[代码]Vue[代码]框架有个差异,[代码]Vue[代码]对于传进来的property会自动赋值,而小程序则需要自己手动赋值 问题与办法 当 [代码]visible[代码] 这个变量被 [代码]Modal[代码] 和 [代码]Page[代码] 同时使用时,会出现不显示的问题。 为了便于描述,我通过描述真实场景来讲解: 当页面需要显示对话框时,[代码]Page[代码] 传递 [代码]visible=true[代码] 给 [代码]Modal[代码] 经过一段时间之后,用户关闭了对话框,此时 [代码]Modal[代码] 将自身的 [代码]visible[代码] 设置为 [代码]false[代码] 当页面需要再次出现对话框时,[代码]Page[代码] 继续传递[代码]visible=true[代码] 给 [代码]Modal[代码],此时发现对话框不会显示 通过分析可以发现,由于 [代码]Page[代码] 两次传递相同的 [代码]visible=true[代码] 给 [代码]Modal[代码] ,因此第二次传递的时候,被 [代码]Modal[代码] 直接忽略掉了。 这个问题也很好解决,大致思路就是保证每次传递的值不同即可: 传递的值前面加上时间戳,组件再将时间戳移除(比较直观,但是不方便) 利用对象不相等的机制,数据传递只传对象,不传基础数据类型(比如[代码]{ visible: true } !== { visible: true }[代码]) 组件自身属性 这种是我推荐的方案。将 [代码]visible[代码] 属性交由组件 [代码]Modal[代码] 自行管理: [代码]// modal.js Component({ data: { visible: false }, methods: { show() { this.setData({ visible: true }) } } }) [代码] 由于父组件或者当前页面可以直接获取组件的实例,因此可以直接调用组件的[代码]setData[代码],如: [代码]let $modal = this.selectComponent('#modal') $modal.setData({ visible: true }) [代码] 但是不建议这样使用,而是组件暴露方法让外部调用: [代码]let $modal = this.selectComponent('#modal') $modal.show() [代码] 组件的事件 通常,对话框都会有按钮,一个或两个。 因此 [代码]Modal[代码] 需要与父组件通过 事件(event) 的方式传递信息:当前点击了取消还是确定按钮: [代码]<!-- home.wxml --> <modal id="modal" bind:btntap="handleModalTap" /> [代码] [代码]// home.js Page({ showModal() { let $modal = this.selectComponent('#modal') $modal.show() }, // 其他方法 handleModalTap(e) { let { type } = e.detail // type = cancel or confirm } }) [代码] 在 [代码]Modal[代码] 的构造函数则是这样的: [代码]// modal.js Component({ data: { visible: false } methods: { handleBtnTap(e) { let { type } = e.target.dataset this.triggerEvent('btntap', { type }) } } }) [代码] [代码]<!-- modal.wxml --> <view class="wrapper"> <!-- 省略其他结构 --> <view class="foot" bindtap="handleBtnTap"> <button data-type="cancel">取消</button> <button data-type="confirm">确定</button> </view> </view> [代码] 这样设计 [代码]Modal[代码] 组件,的确可以满足使用,但是不够好用 因为展示对话框时使用的是 [代码]showModal[代码] 而用户操作之后又是通过另外一个方法 [代码]handleModalTap[代码] 反馈的。当一段时间之后回看这样的代码,会发现这种写法存在思维的中断,不利于代码维护 所以,我建议结合 [代码]Promise[代码] 来封装 [代码]Modal[代码] 省略事件 由于展示对话框之后,用户必然要操作,因此可以在 [代码]showModal[代码] 的时候,通过 [代码]Promise[代码] 返回对应的操作信息即可 另外,需要引入发布订阅机制(以下使用 [代码]Node.js[代码] 的 [代码]Events[代码] 举例): [代码]// modal.js const EventEmitter = require('events'); const ee = new EventEmitter(); Component({ data: { visible: false }, methods: { show() { this.setData({ visible: true }) return new Promise((resolve, reject) => { ee.on('cancel', () => { reject() }) ee.on('confirm', () => { resolve() }) }) }, handleBtnTap(e) { let { type } = e.target.dataset ee.emit(type) this.triggerEvent('btntap', { type }) } } }) [代码] 此时,在 [代码]Page[代码] 即可这样展示对话框: [代码]// home.js Page({ onLoad() { let $modal = this.selectComponent('#modal') $moda.show().then(() => { // 当点击确认时 }).catch(() => { // 当点击取消时 }) } }) [代码] 总结 组件是很好用的机制,也是最常用到的能力 因此日常开发中,应该会遇到各种各样组件封装的问题,平时遇到应该多思考总结一下,对团队和自己都很有帮助!
2020-04-08 - 发现开源小程序之美五,营销组件库
发现开源小程序之美一,个人博客小程序 https://developers.weixin.qq.com/community/develop/article/doc/000a40e13ec550274e2a9addd56413发现开源小程序之美二,微慕WordPress小程序 https://developers.weixin.qq.com/community/develop/article/doc/000c44945dc728ab9c2aff2a55b013 发现开源小程序之美三,维修上报小程序发现开源小程序之美四,在线答题小程序发现开源小程序之美五,营销组件库 https://developers.weixin.qq.com/community/develop/article/doc/000c4235c98740a1dc2a1a6045b013wxapp-market小程序营销组件, Marketing components for WeChatApp 支持营销玩法大转盘刮刮乐老虎机水果机九宫格翻纸牌摇一摇手势解锁 git主页 https://github.com/o2team/wxapp-market 码云主页,git经常由于不可抗拒原因无法打开,我在码云备份的,整个仓库未做任何改动,同时该团队目前在码云未创建账号 https://gitee.com/xiaofeiyang3369/wxapp-market 哦,对了,补充下,这个o2team团队,就是京东大名鼎鼎的凹凸团队,小程序框架taro便是出自他们之手,实力非常强悍 件体验二维码[图片] [图片]效果图展示1 [图片] 2 [图片] 3 [图片] 4 [图片] 4 [图片] 更新记录 优化文件目录结构 2017-09-18 手势解锁组件 2017-09-17 摇一摇组件 2017-09-16 九宫格翻纸组件 2017-09-16 增加老虎机组件、水果机组件 2017-09-02 增加刮刮乐组件 2017-08-29 增加大转盘组件 2017-08-27 create wx-market repository 2017-08-26 1 2 3 4
2020-06-09 - JavaScript常用设计模式示例与应用
JavaScript常用设计模式实例与应用 前言 1. 什么是设计模式 小时候打游戏,我们总是追求快速完美通关;上下班交通,我们总是会选择最方便便捷乘车路线。我们总是追求一件事情的最优美便捷的解决方案,也就是其所谓的最佳实践。 一个设计模式就是一个可重用的方案,可应用于在软件设计中常见的问题,在本次分享主题中,就是编写JavaScript的web应用程序中常见的问题,设计模式的另一种解释就是一个我们如何解决问题的模板。那些在许多不同但类似的情况下使用的模板。 2. 为什么要学习设计模式 JavaScript是一门以原型为基础,面向对象的,动态数据类型语言。在把函数视为第一公民,支持函数式编程的同时也不排斥面向对象的开发方式,甚至在ES6+的标准中还引入了面向对象的一些原生支持。这使得JavaScript成为一门功能强大的语言同时也导致了编程风格的碎片化,同一个功能实现的多样性。对于一些传统的、强面向对象的设计模式会有各种类型的实现,有时候会让人感觉牵强。但是这些并不妨碍我们使用JavaScript来表达设计模式的理念、所要解决的问题以及它的核心思想,这才是我们所要关注的核心。 设计模式可以让我们站在巨人的肩膀上,获得前人的经验,保证我们以优雅的方式组织我们的代码并满足我们解决问题所需要的条件。 内容 一、设计原则 设计原则是指导思想,是我们在程序设计中尽可能要遵守的准则。设计模式就是这些设计原则的一些具体实现,所要达到的目标就是高内聚低耦合。在这里我简单介绍一些六大设计原则中的单一职责原则(SPR)、开放封闭原则(OCP)、最少知识原则(LKP)。 1. 单一职责原则 单一职责原则指的是一个类应该仅有一个引起它变化的原因,也就是说一个对象只做一件事情。这样做可以让我们对对象的维护变得简单,如果一个对象拥有多种职责,职责之间相互耦合,对一个职责的修改势必会影响到其他职责。也就是说,一个对象负责的职责越多,耦合越强,对模块的修改就越危险。 2. 开放封闭原则 开放封闭原则指的是一个模块应该在对扩展开放,而对修改封闭。当需要修改增加需求的时候,应该尽量通过扩展新代码的方式,而不是修改已有的代码。因为修改已有代码会给依赖原有代码的模块带来隐患,从而需要把依赖原有代码的模块重新测试一遍,加重测试成本。 3. 最少知识原则 最少知识原则指的是一个类应该对自己需要耦合或调用的类了解得尽可能少,调用者或依赖着仅需要知道他所需要的方法即可,其他的概不关心。因为类与类之间的关系越密切,耦合性越高,当一个类发生改变时,对另一个类的影响也越大。通常我们减少对象之间的联系的方法是引入一个第三者来帮助通信,阻隔对象之间的直接通信,从而减少耦合。 二、设计模式的分类 设计模式可以被分成几个不同的种类: 创建型设计模式 创建型设计模式关注的是对象创建的机制方法,一般会把对象的创建和使用分离,从而帮助创建类的实例对象。属于这一类的设计模式主要有:构造器模式、工厂模式、单例模式、建造者模式等。 结构型设计模式 结构型设计模式关注对象组成以及不同对象之间的关系。这类模式有助于在系统的某一部分发生变化时减少对整个系统结构的改变。主要包括:代理模式、享元模式、外观模式、适配器模式、装饰者模式等。 行为型设计模式 行为型设计模式关注对象之间的通信,描述对象之间如何相互协作。主要包括:发布订阅模式,策略模式,状态模式,迭代器模式,命令模式,职责链模式,中介者模式等。 三、设计模式示例 1. 单例模式 单例模式(Singleton Pattern)属于创建型设计模式,它限制一个类只能有一个实例化对象,并提供一个访问它的全局访问点。 单例模式可能是最简单的设计模式了,虽然简单,但在实际项目开发中是很常用的一种模式。 单例模式中有几个需要知道的概念: Singleton:特定的类,也就是我们需要访问的类,访问者要拿到的就是它的实例。 Instance: 单例,是特定类的唯一实例。 getInstance: 获取单例的方法。 代码示例 [代码]var GameManager = (function () { // 单例 var instance; function init() { // 私有变量和方法 var _saveData = { name: 'glenn', level: 1 }; function _privateMethod(){ console.log( "I am private function" ); } return { // 公有变量和方法 levelUp: function(){ _saveData.level ++; }, getCurLevel: function(){ return _saveData.level; }, getName: function(){ return _saveData.name; }, publicProperty: "this is a public prop", }; }; return { // 如果存在获取此单例实例,如果不存在创建一个单例实例 getInstance: function () { if ( !instance ) { instance = init(); } return instance; } }; })(); // 使用: var singleA = GameManager.getInstance(); singleA.levelUp(); var singleB = GameManager.getInstance(); console.log( singleA.getCurLevel() === singleB.getCurLevel() ); // true [代码] 在本例中,GameManager是一个单例类,我们首先使用立即调用函数IIFE把希望隐藏的单例示例instance隐藏起来,在init方法中定义该单例类的公有和私有方法变量,然后返回一个对象,把获取单例实例的方法getInstance暴露出去。在getInstance方法中,通过JavaScript的闭包特性把单例实例instance存进闭包中,在第一次获取实例时才初始化单例,并在之后的获取操作中返回的都是这个相同的实例。 可以看到,在使用单例的代码中,我们调用了两次getInstance获取的两个对象singleA和singleB指向的是同一个对象。 源码中的单例模式 以 ElementUI 为例,ElementUI中的全屏Loading蒙层使用服务的形式调用的使用方式示意: [代码]Vue.prototype.$loading = service; this.$loading({ fullscreen: true }); [代码] 我们可以看看这个loading在ElementUI2.9.2源码中是如何实现的。 下面是为了方便观看省略了部分代码后的源码 [代码]import Vue from 'vue'; import loadingVue from './loading.vue'; const LoadingConstructor = Vue.extend(loadingVue); //... //单例 let fullscreenLoading; LoadingConstructor.prototype.originalPosition = ''; LoadingConstructor.prototype.originalOverflow = ''; LoadingConstructor.prototype.close = function() { //... }; const addStyle = (options, parent, instance) => { //... }; const Loading = (options = {}) => { //... //判断示例是否已经初始化 if (options.fullscreen && fullscreenLoading) { return fullscreenLoading; } //一系列的初始化操作 let parent = options.body ? document.body : options.target; let instance = new LoadingConstructor({ el: document.createElement('div'), data: options }); addStyle(options, parent, instance); if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') { addClass(parent, 'el-loading-parent--relative'); } if (options.fullscreen && options.lock) { addClass(parent, 'el-loading-parent--hidden'); } parent.appendChild(instance.$el); Vue.nextTick(() => { instance.visible = true; }); //把初始化出来的实例缓存下来 if (options.fullscreen) { fullscreenLoading = instance; } return instance; }; export default Loading; [代码] 这里的单例是fullscreenLoading,缓存在闭包中。当用户调用时传入的options中fullscreen为true且之前已经创建并初始化过单例的情况下直接返回之前创建的单例,否则继续执行后面的初始化操作,并把创建的单例赋值给闭包中的fullscreenLoading后返回新创建的单例。 这是一个典型的单例模式应用,通过复用之前创建的全屏加载蒙层单例,不仅减少了重复实例化过程带来的额外开销,还保证了页面中不会出现重复的全屏加载蒙层。 单例模式的应用场景 当项目中需要一个公共的状态管理时,我们可以引入单例模式来确保访问的一致性。 当项目中存在一些同一时间只会出现一个且会重复出现的对象时,我们可以引入单例模式避免重复创建对象产生的多余开销,例如项目中的弹窗,消息框提醒等。 2. 外观模式 外观模式又叫门面模式,属于结构型模式,它将子系统的一系列复杂的接口集成起来组成一个更高级别的更舒适的高层接口,从而隐藏其真正的潜在复杂性,对外提供一个一致的外观。 外观模式让外界减少对子系统的直接交互,从而降低耦合,让外界可以轻松使用子系统,其本质是封装交互,简化调用。 代码示例 [代码]var module = (function() { var _sportsman = { speed: 5, height: 10, set : function(key, val) { this[key] = val; }, run : function() { console.log('运动呀正在以'+this.speed+'米每秒的速度向前跑着。'); }, jump: function(){ console.log( "运动员往上跳了"+this.height+'米'); } }; return { facade : function( args ) { args.speed != undefined && _sportsman.set('speed', args.speed); args.height != undefined && _sportsman.set('height', args.height); args.run && _sportsman.run(); args.jump && _sportsman.jump(); } }; }()); // Outputs: 运动呀正在以10米每秒的速度向前跑着。 // 运动员往上跳了5米 module.facade( {run: true, speed: 10, jump: true, height: 5} ); [代码] 这是表达外观模式一个简单的例子。在本例中,调用module的门面方法facede会触发运动员对象_sportsman中的一系列私有方法。但在这一次,用户不需要关心运动员对象内部方法的实现,就可以让运动员动起来。 源码中的外观模式 当我们使用Jquery的$(document).ready()来给浏览器加载完成添加事件回调时,Jquery会调用源码中的私有方法: [代码]// ... bindReady: function() { //... // Mozilla, Opera and webkit nightlies currently support this event if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", jQuery.ready, false ); // If IE event model is used } else if ( document.attachEvent ) { // ensure firing before onload, // maybe late but safe also for iframes document.attachEvent( "onreadystatechange", DOMContentLoaded ); // A fallback to window.onload, that will always work window.attachEvent( "onload", jQuery.ready ); // If IE and not a frame // continually check to see if the document is ready var toplevel = false; try { toplevel = window.frameElement == null; } catch(e) {} if ( document.documentElement.doScroll && toplevel ) { doScrollCheck(); } } } // ... [代码] 由于IE9之前的IE版本浏览器以及Opera7.0之前的Opera浏览器不支持addEventListener方法,在需要适配这些浏览器的项目中,我们需要自己手动判断浏览器版版本来决定使用什么事件绑定方法以及事件。而如果使用了Jquery库中提供的这个外观方法,用户则不需要关心浏览器的兼容问题,使用一致的外观接口$(document).ready()就可以实现监听浏览器加载完成事件的功能,从而简化了使用。 除了抹平浏览器的兼容性问题之外,Jquery还有一些其他的外观模式的应用: 比如设置或获取dom结点的内容和属性时使用的text()、html()和val()方法时,Jquery判断调用方法是否有传参数来确定是设置还是获取操作。这里Jquery把设置和获取操作对外提供了同一个外观接口,使调用简化了不少。 再比如Jquery的ajax的API[代码]$.ajax(url[,settings])[代码],当我们需要设置以JSONP的形式发送请求时,只需要传入[代码]dataType: 'jsonp'[代码]设置,jquery会进行额外的操作帮我们启动JSONP流程,而不需要调用者添加额外的代码。 外观模式的适用场景 维护设计粗糙和难以理解的上古系统,或者非常复杂的一些系统时,可以为这些系统设置一个外观模块,给外界提供清晰的接口,以后的新系统只需要与外观接口交互即可。 构建多层系统时,可以使用外观模式来将系统分层,让外观接口成为每一层的入口,简化层间调用,给层间松耦。 团队协作时,可以将各自负责的模块建立合适的外观,简化其他同事的使用,节约沟通时间。 发布订阅者模式 发布 - 订阅模式(Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),属于行为型模式,它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。 发布 - 订阅模式有几个主要概念: Publisher:发布者,当消息发生时负责通知对应订阅者 Subscriber:订阅者,当消息发生时被通知的对象 SubscriberMap:以type为主键存储数组,每个数组存储所有对应type的订阅者 type: 消息类型,订阅者可以订阅的不同消息类型 subscribe:该方法可以将订阅者添加到SubscriberMap中对应的数组中 unSubscribe:该方法为SubscriberMap中删除订阅者 notify:该方法遍历通知SubscriberMap中对应type的所有订阅者 代码示例 [代码]var Publisher = (function() { var _subsMap = {} // 存储订阅者 return { /* 消息订阅 */ subscribe(type, cb) { if(_subsMap[type]){ if (!_subsMap[type].includes(cb)){ _subsMap[type].push(cb); } }else{ _subsMap[type] = [cb]; } }, /* 消息退订 */ unsubscribe(type, cb) { if(!_subsMap[type] || !_subsMap[type].includes(cb))return; var idx = _subsMap[type].indexOf(cb); _subsMap[type].splice(idx, 1); }, /* 消息发布 */ notify(type) { if (!_subsMap[type])return; var args = Array.prototype.slice.call(arguments, 1); _subsMap[type].forEach(function(cb){ cb.apply(this, args); }) } } })() Publisher.subscribe('运动鞋', function(message){console.log('111' + message)}); // 订阅运动鞋 Publisher.subscribe('运动鞋', function(message){console.log('222' + message)}); Publisher.subscribe('帆布鞋', function(message){console.log('333' + message)}); // 订阅帆布鞋 Publisher.notify('运动鞋', ' 运动鞋到货了 ~') // 打电话通知买家运动鞋消息 Publisher.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息 [代码] 这是一个发布-订阅模式的通用代码实现,Publisher就是一个发布者,这里使用了立即调用函数IIFE方式来将不希望被外界调用的_subsMap隐藏。订阅者采用回调函数的形式,在消息发布时使用JavaScript的apply、call函数使发布的消息参数可以传到订阅者回调函数中去。 源码中的发布-订阅模式 我们使用Jquery的API可以轻松实现消息的订阅、发布以及退订操作: [代码]function eventHandler() { console.log('自定义方法') } /* ---- 事件订阅 ---- */ $('#app').on('myevent', eventHandler) // 发布 $('#app').trigger('myevent') // 输出:自定义方法 /* ---- 取消订阅 ---- */ $('#app').off('myevent') $('#app').trigger('myevent') // 没有输出 [代码] 对应api源码参见: event.js 其中add方法为on接口的内部直接绑定方法,remove方法对应off接口的内部实现。 发布-订阅模式的优缺点 发布-订阅模式最大优点就是解耦: 时间上的解耦:注册事件后,订阅者不需要持续关注发布者的动态,当事件触发时,发布者会通知对应的订阅者,调用对应的回调函数。 对象间的解耦: 发布者不需要提前知道事件的订阅者有哪些,当事件发生时直接遍历对应的订阅者回调函数来通知订阅者,从而解耦了发布者和订阅者之间的联系,使它们之间互不持有。 发布-订阅模式也有一些缺点: 增加消耗:创建结构和缓存订阅者两个过程都会消耗计算和内存资源,即时订阅后没有触发过,订阅者使用会存在内存中。 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的那堆 deps/subs/watchers 们… 总结 设计模式能够让我站在巨人的肩膀上,享受其他开发者们长期以来在一些有挑战性问题上的解决方案以及优秀的架构。 对我们来讲,知道有这些设计模式是很重要的,但更重要的是应该知道怎样以及什么时候去使用它们。遵守设计原则,使用设计模式是好事,但是过犹不及,在实际项目中我们不能刻板的遵守这些设计原则以及使用设计模式,在想使用每个模式前先去了解下它的优缺点。要真正的理解模式能给你带来什么好处需要花时间去尝试,以实际情况中模式给你的程序带来的好处作为标准来选择。
2019-10-31 - 基于小程序云开发写了一个代办清单小程序
代码:记我清单 本文基于自己开发的简版todo工具,记我清单项目总结。 小程序云开发提供了云函数、数据库、存储、云调用四大基础能力,弱化了运维和后端的概念,这也是与近些年云服务、serverless等架构的兴起有关。 资源环境配置一个环境对应一整套独立的云开发资源,包括数据库、存储空间、云函数等资源。各个环境是相互独立的,用户开通云开发后即创建了一个环境,默认可拥有最多两个环境。在实际开发中,建议每一个正式环境都搭配一个测试环境,所有功能先在测试环境测试完毕后再上到正式环境。以初始可创建的两个环境为例,建议一个创建为 test 测试环境,一个创建为 release 正式环境。 环境的配置在app.js中 wx.cloud.init({ // env 参数说明: // env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源 // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 // 如不填则使用默认环境(第一个创建的环境) env: 'production-e9bw1', traceUser: true, }) 使用npm的问题云开发的启动项目,根目录下有cloudfunctions和miniprogram两个文件夹目录,这个与小程序单独开发不一样。使用npm依赖需要在miniprogram下安装依赖而不是根目录下。 数据库查询问题小程序端在获取集合数据时服务器一次默认并且最多返回 20 条记录,云函数端这个数字则是 100。微信的解释是 “为了防止误操作以及保护小程序体验”~这,我能说一句体验很差吗,真的会给开发者带来一些不必要的麻烦。 我想你一开始可能会想什么是小程序端与云函数端?难道都是小程序端吗?下面我来介绍一下。 小程序端更像是我们说的前端,就是miniprogram下的代码,你是可以直接拿到wx.cloud的api的。 云函数是指的cloudfunctions下的代码,这个是需要上传到微信的云node环境下才能使用的,调用wx.cloud需要我们使用wx-server-sdk const cloud = require('wx-server-sdk') 注意在小程序端调用查询数据是天然鉴权的,A用户存放的数据,B用户是查询不到的。但是如果你是通过云函数来查询,那么你就要根据用户的openid来作为必要的查询条件了,否则就会出现查询了所有用户数据的问题! const cloud = require('wx-server-sdk') const wxContext = cloud.getWXContext() // 先取出集合记录总数 const countResult = await db.collection('todos').where({ _openid: wxContext.OPENID }).count() 分页查询的代码/** * 根据参数分页查询todo列表 /小程序端一次最多查20个 */ export const queryTodoByPage = async(dbParams = {}, page = 1, pageSize = 20) => { const db = wx.cloud.database() const dbCollection = db.collection('todos') const countResult = await dbCollection.count() // 总条数 const count = countResult.total // 按页查询 const data = await dbCollection.where(dbParams).skip((page - 1) * pageSize).limit(pageSize).get() return new Promise((resolve, reject) => { resolve({ data, count }) }) } 数据状态管理 & computed计算属性很抱歉小程序原生开发并不提供这两个可以让开发者很爽的两个扩展能力,这次开发的todo小程序深有感悟没有这两个特性无疑会让你的代码增加很多,而且会浪费可以减少的数据请求。不过官方提供了扩展库,你可以按照自己的需求去安装。 npm install --save miniprogram-computed npm install --save mobx-miniprogram mobx-miniprogram-bindings 具体可查阅微信小程序官方文档-扩展能力一节。 预览: [图片][图片]
2020-03-20 - 小程序多端框架全面测评:chameleon、Taro、uni-app、mpvue、WePY
作者:coldsnap 原文:小程序多端框架全面测评 Fundebug经授权转载,版权归原作者所有。 最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有: 开发迅速 复用前端生态 易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS 缺点有: 交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 [代码]JS[代码] 和 [代码]Native[代码] 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。 由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。 3. JavaScript 编译型 这类框架就是我们这篇文章的主角们:[代码]Taro[代码]、[代码]WePY[代码] 、[代码]uni-app[代码] 、 [代码]mpvue[代码] 、 [代码]chameleon[代码],它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。 这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 [代码]react-native-web[代码], Weex 原生支持) 另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。 但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。 那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。 如果不管什么 DSL 都能接受,那就可以进入下一个环节: 生态 以下内容均以各框架现在(2019 年 3 月 11 日)已发布稳定版为标准进行讨论。 1. 开发工具 就开发工具而言 [代码]uni-app[代码] 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。 其它的框架都是使用 CLI 命令行工具,但值得注意的是 [代码]chameleon[代码] 有独立的语法检查工具,[代码]Taro[代码] 则单独写了 ESLint 规则和规则集。 在语法支持方面,[代码]mpvue[代码]、[代码]uni-app[代码]、[代码]Taro[代码] 、[代码]WePY[代码] 均支持 TypeScript,四者也都能通过 [代码]typing[代码] 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。 CSS 方面,所有框架均支持 [代码]SASS[代码]、[代码]LESS[代码]、[代码]Stylus[代码],Taro 则多一个 [代码]CSS Modules[代码] 的支持。 所以这一轮比拼的结果应该是: uni-app > Taro > chameleon > WePY、mpvue [图片] 2. 多端支持度 只从支持端的数量来看,[代码]Taro[代码] 和 [代码]uni-app[代码] 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),[代码]chameleon[代码] 少了头条小程序紧随其后。 但值得一提的是 [代码]chameleon[代码] 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。[代码]uni-app[代码] 则有一套独立的条件编译语法,这套语法能同时作用于 [代码]js[代码]、样式和模板文件。[代码]Taro[代码] 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。 在移动端方面,[代码]uni-app[代码] 基于 [代码]weex[代码] 定制了一套 [代码]nvue[代码] 方案 弥补 [代码]weex[代码] API 的不足;[代码]Taro[代码]则是暂时基于 [代码]expo[代码] 达到同样的效果;[代码]chameleon[代码] 在移动端则有一套 SDK 配合多端协议与原生语言通信。 H5 方面,[代码]chameleon[代码] 同样是由多态协议实现支持,[代码]uni-app[代码] 和 [代码]Taro[代码] 则是都在 H5 实现了一套兼容的组件库和 API。 [代码]mpvue[代码] 和 [代码]WePY[代码] 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。 所以最后一轮对比的结果是: chameleon > Taro、uni-app > mpvue > WePY [图片] 3. 组件库/工具库/demo 作为开源时间最长的框架,[代码]WePY[代码] 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。 [代码]uni-app[代码] 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 [代码]WePy[代码] 也是完全不遑多让的。 [代码]Taro[代码] 也有官方维护的跨端 UI 库 [代码]taro-ui[代码] ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 [代码]Taro[代码] 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。 而 [代码]mpvue[代码] 没有官方维护的 UI 库,[代码]chameleon[代码] 第三方的 demo 和工具库也还基本没有。 所以这轮的排序是: WePY > uni-app 、taro > mpvue > chameleon [图片] 4. 接入成本 接入成本有两个方面: 第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 [代码]wxparse[代码]、[代码]echart[代码]、[代码]zan-ui[代码] 等)多是基于原生微信小程序框架语法写成的。目前看来 [代码]uni-app[代码] 、[代码]Taro[代码]、[代码]mpvue[代码] 均有文档或 demo 在框架中直接使用原生小程序组件/库,[代码]WePY[代码] 由于运行机制的问题,很多情况需要小改一下目标库的源码,[代码]chameleon[代码] 则是提供了一个按步骤大改目标库源码的迁移方式。 第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。 而对于两种接入方式 Taro 都提供了 [代码]taro convert[代码] 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。 所以这轮的排序是: Taro > mpvue 、 uni-app > WePY > chameleon 流行度 从 GitHub 的 star 来看,[代码]mpvue[代码] 、[代码]Taro[代码]、[代码]WePY[代码] 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。 [代码]uni-app[代码] 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 [代码]uni-app[代码] 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。 所以从流行程度来看应该是: uni-app > Taro、WePY、mpvue > chameleon [图片] 5. 开源建设 一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。 从第三方贡献者数量来看,[代码]Taro[代码] 在这一方面领先,并且 [代码]Taro[代码] 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 [代码]omi[代码] 框架小程序部分也是基于 Taro 完成的。 [代码]WePY[代码] 在腾讯开源计划的加持下在这一方面也有不错的表现;[代码]mpvue[代码] 由于停滞开发了很久就比较落后了;可能是产品策略的原因,[代码]uni-app[代码] 在开源建设上并不热心,甚至有些部分代码都没有开源;[代码]chameleon[代码] 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。 那么这一轮的对比结果是: Taro > WePY > mpvue > chameleon > uni-app 最后补一个总的生态对比图表: [图片] 未来 从各框架已经公布的规划来看: [代码]WePY[代码] 已经发布了 [代码]v2.0.alpha[代码] 版本,虽然没有公开的文档可以查阅到 [代码]2.0[代码] 版本有什么新功能/特性,但据其作者介绍,[代码]WePY 2.0[代码] 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 [代码]WePY[代码] 的表现。 [代码]mpvue[代码] 已经发布了 [代码]2.0[代码] 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,[代码]mpvue[代码] 要想在未来有作为首先要打消社区对于 [代码]mpvue[代码]不管不顾不更新的质疑。 [代码]uni-app[代码] 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 [代码]uni-app[代码] 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。 [代码]chameleon[代码] 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有: 快应用和端拓展协议 通用组件库和垂直类组件库 面向研发的图形化开发工具 面向非研发的图形化页面搭建工具 如果 [代码]chameleon[代码] 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 [代码]chameleon[代码] 将大有可为。 [代码]Taro[代码] 的未来也一样值得憧憬。Taro 即将要发布的 [代码]1.3[代码] 版本就会支持以下功能: 快应用支持 Taro Doctor,自动化检查项目配置和代码合法性 更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 [代码]只能用 map 创造循环组件[代码] 一条 H5 打包体积大幅精简 同时 [代码]Taro[代码] 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。 结语 那说了那么多,到底用哪个呢? 如果不介意尝鲜和学习 DSL 的话,完全可以尝试 [代码]WePY[代码] 2.0 和 [代码]chameleon[代码] ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。 [代码]uni-app[代码] 和 [代码]Taro[代码] 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 [代码]mpvue[代码] 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 [代码]uni-app[代码] 。 当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交: chameleon: https://github.com/didi/chameleon mpvue: https://github.com/Meituan-Dianping/mpvue Taro: https://github.com/NervJS/taro uni-app: https://github.com/dcloudio/uni-app WePY: https://github.com/Tencent/wepy (按字母顺序排序)
2019-06-18 - 新能力解读:页面间通信接口
在 2019 年 7 月 2 日的小程序基础库版本更新 v2.7.3 中,小程序新增了一个页面间通讯的接口,帮助我们的小程序完成不同页面间数据同步的功能。 页面间通信接口能干嘛? 在 v2.7.3 之前,小程序不同页面间的大批量数据传递主要有两种: 借助诸如 Mobx 、Redux 等工具,来实现不同页面间的数据传递。 借助小程序提供的 storage ,来完成在不同页面间数据的同步。 前者需要引入一些第三方工具库,从而提升了整个应用的大小,同时,引入的工具也带来了学习生本。而后者则是基于小程序提供的存储,先将数据存入存储,再到另外一个页面去读取,如果数据涉及到了多个页面,则可能会导致数据的紊乱。 新的页面间通信接口则直接解决了上述的两个问题,你可以直接使用 API 在两个页面之间传递数据,再也无需担心数据的紊乱。 新增的页面间通信接口应当如何使用? 关于页面间通信接口的使用非常简单。 这里,我们假设存在 A 和 B 两个页面,其中 A 是首页,B是详情页。 A 向 B 传递数据 如果你需要从首页向详情页传递数据,则可以这样操作。 在页面 A 执行代码 [代码]wx.navigateTo({ url: 'test?id=1' success: function(res) { // 通过eventChannel向被打开页面传送数据 res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' }) } }) [代码] 这样,当 A 跳转到 B 时,就会出发 B 当中定义的 acceptDataFromOpenerPage,并将后续的数据传递过去。 在 B 中,你可以在 onLoad 去定义 eventChannel 的相关方法 [代码]Page({ onLoad: function(option){ // 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据 let eventChannel = this.getOpenerEventChannel(); eventChannel.on('acceptDataFromOpenerPage', function(data) { console.log(data) }) } }) [代码] B 向 A 传递数据 如果需要被打开的页面向打开的页面传递数据,则可以使用如下代码: 在 A 中的跳转时,加入 events 的定义,定义你自己的函数,以及对应的处理函数。 [代码]wx.navigateTo({ url: 'test?id=1', events: { someEvent: function(data) { console.log(data) } }, }) [代码] 然后在 B 中,调用如下代码来发信息 [代码]Page({ onLoad: function(option){ const eventChannel = this.getOpenerEventChannel() eventChannel.emit('someEvent', {data: 'test'}); } }) [代码] 这样,就可以在 B 页面将数据传回到 A 页面了。 页面间通信接口使用注意事项? 在使用页面间通信接口时需要注意两点: 该功能从基础库 2.7.3 开始支持,低版本需做兼容处理。
2019-09-21 - 小程序没有 DOM 接口,原因竟然是……?
拥有丰富的 Web 前端开发经验的工程师小赵今天刚刚来到新的部门,开始从事他之前没有接触过的微信小程序开发。在上手的第一天,他就向同办公室的小程序老手老李请教了自己的问题。 小赵:翻了一圈文档,小程序好像并不提供 DOM 接口?我还以为可以像之前一样用我喜欢的前端框架来做开发呢。老李,你说小程序为什么不给我们提供 DOM 接口呀。 老李:要提供 DOM 接口也没那么容易。你知道小程序的双线程模型吗?(小赵漏出了疑惑的表情)小程序是基于 Web 技术的,这你应该知道,但小程序和普通的移动端网页也不一样。你做了很多前端项目了,应该知道在浏览器里,UI 渲染和 JavaScript 逻辑都是在一个线程中执行的? 小赵:这我知道,在同一个线程中,UI 渲染和 JavaScript 逻辑交替执行,JavaScript 也可以通过 DOM 接口来对渲染进行控制。 老李:小程序使用的是一种两个线程并行执行的模式,叫做双线程模型。像我画的这样,两个线程合力完成小程序的渲染:一个线程专门负责渲染工作,我们一般称之为渲染层;而另外有一个线程执行我们的逻辑代码,我们一般叫做逻辑层。这两个线程同时运行,并通过微信客户端来交换数据。在小程序运行的时候,逻辑层执行我们编写的逻辑,将数据通过 setData 发送到渲染层;而渲染层解析我们的 WXML 和 WXSS,并结合数据渲染出页面。一方面,每个页面对应一个 WebView 渲染层,对于用户来说更加有页面的感觉,体验更好,而且也可以避免单个 WebView 的负担太重;另一方面,将小程序代码运行在独立的线程中的模式有更好的安全表现,允许有像 open-data 这样的组件可以在确保用户隐私的前提下让我们展示用户数据。 [图片] 小赵:怪不得所有和页面有关的改动都只能通过 setData 来完成。但是用两个线程来渲染我们平时用单线程来渲染的 Web 页面,会不会有些「浪费」?而且每一个页面有一个对应的渲染层,那页面变多的时候,岂不是会有很大的开销? 老李: 并不浪费,因为界面的渲染和后台的逻辑处理可以在同一时间运行了,这使得小程序整体的响应速度更快了。而在小程序的运行过程中,逻辑层需要常驻,但渲染层是可以回收的。实际上,当页面栈的层数比较高的时候,栈底页面的渲染层是会被慢慢回收的。 小赵: 原来如此。这么说的话,实际的 DOM 树是存在于渲染层的,逻辑层并不存在,所以逻辑层才没有任何的 DOM 接口,我明白了。但是……既然可以实现像 setData 这样的接口,为什么不能直接把 DOM 接口也代理到逻辑层呢?我觉得小程序可以做一个封装,让我们在逻辑层调用 DOM 接口,在渲染层调用接口后再把结果返回给我们呀。 老李:从理论上来说确实是可以的。但是线程之间的通信是需要时间的呀。将调用发送到渲染层,再将 DOM 调用结果发送回来,这中间由于线程通信发生的时间损耗可能会比这个接口本身需要的时间要多得多。如果以此为基础使用基于 DOM 接口的前端框架,大量的 DOM 调用可能会非常缓慢,让这个设计失去意义。 在实际测试中,如果每次 DOM 调用都进行一次线程通信,耗时大约是同等节点规模直接在渲染层调用的百倍以上;如果忽略通信需要的时间,一个实现良好的基于 DOM 代理的框架可以近似地看成一个动态模板的框架,而动态模板和静态模板相比要慢至少 50% 小赵:原来如此,线程通信的时间确实是我没有考虑到的问题。那现在的小程序框架中难道不存在这个问题吗? 老李: 在现在的小程序框架中,这个问题也是存在的,这也是现在的框架基于静态模板渲染的原因。静态模板可以在运行前就做好打包,直接注入到渲染层,省去线程传输的时间。在运行时,逻辑层只和渲染层进行最少的、必要的数据交换:也就是渲染用的数据,或者说 data 。另一方面,静态模板让两个线程都在启动时就拥有模板相关的所有数据,所以框架也充分利用了这一点,进行了很多优化。 小赵: 怪不得我在文档里发现很多和 setData 有关的性能提示,都提醒尽量减少设置不必要的数据,现在总算是知道为什么了。但是具体到实际开发里的时候,还是总觉得很难每次只设置需要的数据啊,像对象里或者数组里的数据怎么办呢? 老李: 如果只改变了对象里或者数组里的一部分数据,可以通过类似 array[2].message , a.b.c.d 这样的 数据路径 来进行「精准设置」。另外,现在自定义组件也支持 纯数据字段 了,只要在自定义组件的选项中设置好名为 pureDataPattern 的正则表达式, data 中匹配这个正则的字段将成为纯数据字段,例如,你可以用 /^_/ 来指定所有 开头的数据字段为纯数据字段。所有纯数据字段仅仅被记录在逻辑层的 this.data 中,而不会被发送到渲染层,也不参与任何界面渲染过程,节省了传输的时间,这样有助于提升页面更新性能。 小赵:小程序还有这样的功能,受教了。不过说来说去,我还是想在小程序里用我顺手的框架来开发,毕竟这样事半功倍嘛。我在网上搜索了一下,发现现在有很多支持用 Web 框架做小程序开发的框架,但好像都是将模板编译成 WXML,最终由小程序来做渲染,但这样的方法好像兼容性也不是很好。我在想,我们能不能在逻辑层仿造一套 DOM 接口,然后在运行时将 DOM 调用适配成小程序调用? 老李: 你的这个脑洞有一些意思。在逻辑层仿造一套 DOM 接口,直接维护一棵 DOM 树,这当然没问题。但是没有代理 DOM 接口,逻辑层的 DOM 树没法反映到渲染层,因为渲染层具体会出现什么样的组件,是运行时才能知道的,这不就没法生成静态模板了? 小赵:静态模板确实是没法生成了,但我看到小程序的框架支持自定义组件,我是不是可以做一个通用的自定义组件,让它根据传入的参数不同,变成不同的小程序内置组件。而且自定义组件还支持在自己的模板中引用自己,那么我只需要一个这个通用组件,然后从逻辑层用代码去控制当前组件应该渲染成什么内置组件,再根据它是否有子节点去递归引用自己进行渲染就可以了。你看这样可行吗? [图片] 老李: 这样的做法确实可行,而且微信官方已经按照这个思路推出小程序和 Web 端同构的解决方案 Kbone 了。Kbone 的原理就像你刚才说的那样,它提供一个 Webpack 插件,将项目编译成小程序项目;同时提供两个 npm 包,分别提供 DOM 接口模拟和你说的那个通用的自定义组件作为运行时依赖。要不你赶紧试试? 小赵:还有这么好的事,那我终于可以用我喜欢的框架开发小程序了!这么好的框架,为什么不直接内置到小程序的基础库里呀? 老李: 因为这样的功能完全可以用现在已有的基础库功能实现出来呀。Kbone 现在是 npm 包的形式,使得它的功能、问题修复可以随着自己的版本来发布,不需要依赖于基础库的更新和覆盖率,不是挺好的吗? 小赵: 好是好,但我担心的是代码包大小限制的问题。除了我们已经写好的业务逻辑之外,现在还得加上 Kbone,会不会装不下呀? 老李: 原来你是担心这个呀,放心,Kbone 现在已经可以在 扩展库 里一键搞定啦。扩展库是帮我们解决依赖的全新功能,只要在配置项中指定 Kbone 扩展库,就相当于引入了 Kbone 相关的最新版本的 npm 包,这样就不占用小程序的代码包体积了,快试试吧! 小赵:哇,那可太爽了,马上就搞起! 最后 如果你对 Kbone 感兴趣或者有相关问题需要咨询, 欢迎加入 Kbone 技术交流 QQ 群:926335938
2020-01-14 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10