稍微整了整微信小程序,还是有不少问题的,做个小总结吧
和以往一样,本次项目也放到了 Github 中,欢迎围观 star ~
一、微信公众平台 - 小程序后台
1.申请小程序账号
注册时所用的邮箱有限制(未注册过公众平台、开放平台、企业号、未绑定个人号的邮箱)
选择主体为个人或企业,按照流程注册
小程序的信息填写有次数限制,需要注意不要乱填(服务类目,名称,头像,介绍 等)
登录
2.设置
记住这两项 AppID 和 AppSecret,在开发时需要用到
大部分操作都需要管理员扫码验证
配置好开发者,体验用户的相关权限
设置 -> 基本设置 中配置好 基础库最低版本的设置
设置 -> 开发者设置 -> 服务器域名中 配置好服务器域名,小程序才能正确地发起异步请求
3.小程序社区
常见的问题可以 在 小程序社区 及 小程序论坛 及 小程序文档 查找答案
二、小程序开发者工具
1.安装
注意,小程序开发者工具与公众号开发者工具不能同时运行,不能安装在同一个目录中
为了能够兼容两者的使用,需要一些额外的操作
比如需要同时使用三个工具,先修改文件夹命名
内部的exe可执行程序修改成对应的名称
在开始菜单中才能正确找到三个程序的快捷入口
一般来说,开发者工具仅作为编译及调试使用,代码编辑建议使用自己的编辑器或IDE
开发者工具bug多多,如果发现失效的奇怪的问题,请重启开发者工具
常用快捷键:
Ctrl+B 编译
Ctrl+R 刷新
Ctrl+Shift+P 远程预览(项目需要提供AppID)
2.其他说明
2.1. 打开开发者工具,选择项目文件夹,自动识别为已有项目还是新建的项目
填写AppID 才能使用真机预览
2.2. 小程序布局以iPhone6为基础
所以在移动设备模拟中 选用 iPhone6作为基准,再兼容其他设备
设备模拟与真机预览某些时候差异很大,别忘了在手机上调试
2.3 建议的设置项
默认开发者工具中发起的异步请求仅在开发者工具中看到,如需使用Fiddler进行调试,需要手动设置代理
2.4 项目配置
项目的设置,会自动同步到 project.config.json文件中
点开右侧详情,选中比较高的调试基础库版本
选用自带的ES6转ES5,代码补全压缩
开发时记得勾选不校验域名(防止访问失败),上线阶段记得取消勾选(校验域名可更安全)
可点击切换编辑器的显示隐藏,快速调整右下开发者调试的窗口
2.5 代码上传
2.5.1 先进行编译并远程预览
2.5.2 再进行上传,管理员登录微信公众平台选择启用为体验版本,相关人员进行体验
2.5.3 管理员选用体验版本上传作为审核
不可随意上传审核,多次审核不通过有惩罚机制
2.5.4 管理员选择审核通过的版本,发布为线上版本
三、小程序开发框架、代码结构
1.基础结构
tabbar中的路径以及页面中的跳转路径,都必须在app.json的pages中配置好,其中分包的pages也要配置好
2.项目结构
小程序支持大部分ES6语法,但为了支持完整的ES6+语法,以及对原生的回调做优化,引入 wxPromise
基本支持小程序的所有API转换成 then catch finally 的便捷语法,加上 wx.pro.即可 如
// 演示 wxPromise 的能力 wx.request({ url: \'test.php\', //仅为示例,并非真实的接口地址 data: { x: \'\' , y: \'\' }, header: { \'content-type\': \'application/json\' // 默认值 }, success: function(res) { console.log(res.data) }, fail: function(e) { console.log(e) }, complete: function(e) { console.log(e) } }) wx.pro.request({ url: \'test.php\', data: {}, method: \'GET\', header: {\'content-type\': \'application/json\'} }).then(res => { console.log(res) }).catch(err => { console.log(err) }).finally(() => { wx.hideLoading() })
小程序不支持SCSS预编译的CSS,为了支持SCSS,所有需要引入构建工具进行编译
小程序自带了代码压缩,不过基本上只是对JS做了压缩,还需要压缩其他资源文件(WXSS、WXML等)
引入Gulp构建工具
项目参考 此Demo
目录结构
src目录为源代码,gulpfile为 Gulp的任务配置,经过处理猴,将生成dist目录,在小程序开发者工具中打开此 dist目录即可
在sign目录下,执行 npm i 安装依赖包,然后运行 npm start 即可执行并监听,更新dist目录的内容,然后在 开发者工具中重新编译 Ctrl+B 即可看到效果(有时开发者工具出现奇怪问题的,就重启工具)
src目录中主要为项目根文件,pages页面目录,assets资源目录
pages目录中的 嵌入的subPages目录用作分包目录,页面之外的其他资源文件或其他模块在assets目录中
app.json配置示例
{ "pages": [ "pages/index/index", "pages/user/user", "pages/wait/wait" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#1296db", "navigationBarTitleText": "我来也", "navigationBarTextStyle": "white" }, "subPackages": [{ "root": "pages/subPages", "pages": [ "signed/signed" ] }], "tabBar": { "selectedColor": "#1296db", "borderStyle": "white", "list": [ { "pagePath": "pages/index/index", "text": "我要签到", "iconPath": "./assets/imgs/sign.png", "selectedIconPath": "./assets/imgs/sign_active.png" }, { "pagePath": "pages/wait/wait", "text": "等候大厅", "iconPath": "./assets/imgs/wait.png", "selectedIconPath": "./assets/imgs/wait_active.png" }, { "pagePath": "pages/user/user", "text": "个人中心", "iconPath": "./assets/imgs/user.png", "selectedIconPath": "./assets/imgs/user_active.png" } ] }, "debug": true }
页面配置user.json配置示例
{ "navigationBarTitleText": "个人中心" }
Gulp编译示例
运行示例
3. 项目框架
小程序开发,一般有三种方式
3.1 纯原生开发
3.2 基本使用原生开发,并使用构建工具进行一些简单构建
3.3 使用小程序开发框架
两者都需要vue语法基础,无vue基础则有些障碍,相对来说 mpvue能更纯粹地使用以往开发Web的经验,wepy在小程序之上又有自己的一套规则
使用框架,并不是不再需要使用小程序原生的语法API
鉴于此,为了弥补原生不支持SCSS的不足,使用 3.2 做简单构建方案
不采用webpack的原因是webpack基于JS模块,通过配置入口模块做依赖打包,在小程序项目中Gulp工作流的方案更为合适
四、小程序基础语法、常见问题
1.基础用法
框架为基础的语法结构
组件类似web中的HTML标签,web中的component
API为小程序中的可用JS方法
部分组件与API有版本兼容问题,需要注意
2. 常见问题
2.1 input组件中上方的图标按钮在真机中点击失效
input组件为原生组件,层级最高。focus 聚焦状态下点击上方的按钮时,会先触发失焦,再次点击按钮才生效
解决办法,按钮置于input后面,背景做一些处理,与input区域的背景协调一致
另外要注意,需要将按钮可点区域扩大,可以获得更好的使用体验
2.2 image组件的大小适应问题
小程序中image默认为完全拉伸至填满image容器的scaleToFill 属性,有时会导致图片扭曲变形
常用的 mode 模式为 widthFix 宽度不变,高度自动变化,保持原图宽高比不变。需要先设置好宽度
另外,在引入小图标时,也要注意小图标受到image组件默认宽高320px*240px 的影响,会被拉伸。可手动设置宽高解决
2.3 部分小图标可使用自带的 icon组件
2.4 如需要提供可选择可复制的文本 ,需使用 text组件 ,而不是 view 组件
2.5 Flex布局的使用
小程序中会大量使用到 Flex布局,要掌握好。优先使用此布局方案
2.6 rpx自适应单位的使用
小程序中引入了新的CSS单位 rpx(responsive pixel), 相对于px,它提供了自适应的能力,优先使用这个单位
规定了以iPhone6屏幕(375px*667px)为基准,其设备像素比DPR为2,所以定义其屏幕宽度为 375px * 2 = 750rpx,相当于 1px == 2rpx 、1rpx == 0.5px
在以往web开发中,我们需要根据不同机型的屏幕宽度与DPR,手动计算不同的rpx值
但在小程序中,我们只需要以iPhone6为基准开发即可(设计稿也需要设计成iPhone6的750px宽度),设置好750px下某个元素应该是多少rpx,其他的转换工作无需考虑
2.7 文本垂直居中的处理
在小程序中文本和图片之前的垂直居中可能需要特殊处理, 以往web页面中可用 vertical-align: middle; 实现居中效果
但在小程序中的 image组件 和 text 组件的表现不太一样,看着不居中
可以使用Flex布局居中的方式,父元素设置 display: flex; align-items: center; 即可
2.8 底部 tabbar图标与文本大小的自适应
小程序自带的 底部tabbar可自定义的程度不是很高,不同机型下可能会出现图标太大或文字太小的不同现象
解决方案:
使用自带tabbar,图标选用 81px * 81px ,注意图标不要撑满大小,需要留白,否则会偏大
自定义tabbar,需要模拟原生tabbar的一些跳转时的固定效果,尽量保持和自定义的体验一致
可参考 一些Demo
2.9 页面跳转使用绝对路径,且需要在 pages 中配置好
如 跳转到某个页面
wx.navigateTo({ url: \'/pages/subPages/signed/signed\' });
2.10 tabbar中配置的页面,需要使用 switchTab 才能跳转
同样的 switchTab 也只能跳转到 tabbar设置的页面
wx.switchTab({url:\'/pages/user/user\'}); <navigator class=\'goto-sign\' open-type="switchTab" url=\'/pages/index/index\'> <image src="../../assets/imgs/sign_active.png" /> <text>前往签到</text> </navigator> wx.redirectTo({url:\'/pages/subPages/signed/signed\'});
2.11 在WXSS中的背景图片不能设置为本地路径
仅支持作为 base64引入图片,或者使用网络地址的图片,或者使用 image组件引入图片
2.12 请求时的url参数需要设置为带 scheme(如 http)的绝对地址
且需要在微信工作平台设置好服务器域名
2.13 小程序的代码包限制为 2M
可使用分包策略,单个包限制为2M,所有包限制为8M
能使用网络资源的就使用网络资源(比如图片、音频等),能压缩的就压缩,精简优化代码
分包方法:
假设对不需要初次就加载的页面 signed.wxml进行打包加载,对其进行分包
在 app.json中配置 subPackages
"subPackages": [{ "root": "pages/subPages", "pages": [ "signed/signed" ] }], "tabBar": { ... }
将该页面包放在 pages主页面目录下的 subPages 子页面目录即可
跳转时记得把 这里的 subPages 路径也加上 /pages/subPages/signed/signed
2.14 过多地进行 setData 操作可能会导致性能下降
2.15 登录授权的兼容方案
以下仅涉及获取用户基本信息的部分,如要获取openID 手机号等 还需要发起请求让后端来解密处理 更多步骤
旧版本中一般直接使用JS进行调用
wx.getUserInfo({ success: function(res) { console.log(res.userInfo) } })
新版本中已逐渐不支持,直接调用会失效,应该改用 button组件引导用户启用授权
点击按钮后再获取信息
<view class=\'user__info\'> <button wx:if="{{!hasUserInfo}}" open-type=\'getUserInfo\' bindgetuserinfo=\'getUserInfo\'> <image class=\'user__wx\' src="../../assets/imgs/wx.png" mode=\'widthFix\' /> <text>微信授权登录</text> </button> <block wx:else> <image class="user__avatar" src="{{avatar}}"/> <view >{{nickName}}</view> </block> </view>
有信息的就直接显示信息,否则提示用户点击按钮授权
点击后将触发 getUserInfo回调,e.detail中携带了 与 wx.getUserInfo 相同的返回信息
将用户信息保存到本地缓存
data: { hasUserInfo: app.globalData.userInfo.hasUserInfo, avatar: app.globalData.userInfo.avatar, nickName: app.globalData.userInfo.nickName }, ... getUserInfo(e) { if (!e.detail.userInfo) { return; } app.globalData.userInfo = { hasUserInfo: true, avatar: e.detail.userInfo.avatarUrl, nickName: e.detail.userInfo.nickName } wx.setStorage({ key: \'userInfo\', data: app.globalData.userInfo }); this.setData({ hasUserInfo: app.globalData.userInfo.hasUserInfo, avatar: app.globalData.userInfo.avatar, nickName: app.globalData.userInfo.nickName }); }
2.16 上传文件
目前仅支持上传图片及小视频
仅支持一次上传一个文件,多个文件需循环上传
需考虑全部上传成功才继续操作,可使用 promise.all 结合 await/async 异步处理
如 上传图片成功后才提交数据
async uploadFaces() { let p = []; (this.data.face || []).forEach(item => { p.push(new Promise((resolve, reject) => { wx.pro.uploadFile({ url: \'http://xxx\', filePath: item, name: \'cover\', formData: { _csrf: \'RktCQ3daWDJ3Sk1aSE9ucikRJRYcLhoFODo1Yx4AACgLBDsTBSwAYS59fGsxJ1offf\' }, header: { \'Cookie\': \'PHPSESSID=95nno109b17gbfb852tpf4mke455\', \'content-type\': \'multipart/form-data\' } }).then(rs => { let data = typeof rs.data === \'string\' ? JSON.parse(rs.data) : rs.data; if (!data.status) { reject(rs); } else { resolve(rs); } console.log(rs); }).catch(e => { reject(e); console.log(e); }); })); }); await Promise.all(p); }, submitData(data) { let that = this; console.log(data); ... } ... if (rs.confirm) { let uploadFaceloading = wx.showLoading({ title: \'上传中\' }); // 先上传图片 that.uploadFaces() .then((rs) => { console.log(\'uploadSuccess\', rs); // 再提交数据 that.submitData(e.detail.value); }).catch(e => { wx.showToast({ title: \'上传失败\', icon: \'none\' }); console.log(\'uploadFail\', e); }).finally(rs => { wx.hideLoading(uploadFaceloading); }); }
2.17 获取本地图片 wx.chooseImage 等相关的API,会触发页面的 onHide/onShow
开始选择,直到选择完毕,相关的触发流程
所以在onShow或onHide中需要处理一些数据时,要关注这个问题
在app.globalData中设置一个标志位,由其他页面跳转进来onShow时获取到正确的标志位(即其他页面卸载/隐藏时 将该标志为置位),chooseImage 选择后不置位
使用一些获取页面栈的API,来判断当前页面的信息(如 getCurrentPages() ,只是能够判断出来的几率不是很大)
2.18 获取用户坐标位置
使用 wx.getLocation 能够获取用户坐标的经纬度,我们还需要将其转换为可识别的城市信息,需要使用第三方API(比如腾讯地图,百度地图等)来解析坐标
使用腾讯位置服务,先注册登录,生成属于自己的 KEY值用于传值,需要启用 WebServiceAPI
在获取到经纬度之后,调用
// app.js let QQMapWX = require(\'./assets/js/qqmap-wx-jssdk.min.js\'); App({ onLaunch: function() { let userInfo = wx.getStorageSync(\'userInfo\'); this.globalData.qqmapsdk = new QQMapWX({ // 输入对应的key key: \'...\' }); }, globalData: { qqmapsdk: null } }); // index.js let app = getApp(); Page({ onReady() { wx.pro.getLocation({ type: \'wgs84\' }).then(res => { console.log(res); // 获取当前位置 app.globalData.qqmapsdk.reverseGeocoder({ location: { latitude: res.latitude, longitude: res.longitude }, success: (addressRes) => { let city = addressRes.result.address_component.city; console.log(addressRes.result.address_component, city); // 匹配当前坐标,设置城市 if (city) { let groupSelected = this.data.group.find(item => { return city.includes(item); }); groupSelected && this.setData({ groupSelected: groupSelected }); } }, fail: (e) => { console.log(e); } }); }).catch(e => { console.log(e); }); } })
也可以手动设置经纬度进行测试,在开发者工具中
可以使用百度地图拾取坐标系统 查询某地的坐标,用作测试
首次调用时会询问用户授权,如果用户允许了,接下去就不再询问。如果拒绝,则将一直拒绝。
大部分授权的询问机制是差不多的。
2.19 关于授权
微信小程序涉及很多授权相关的东西,基础的信息可以直接使用API获取,敏感数据需要通过后端发起API来解析
在微信开发者工具中,可以在这里清除授权信息(远程调试的手机上也会被清除),用以反复测试授权的情况
在手机上,可以点击小程序右上角的.. ,查看关于小程序
即可在这个页面的授权列表中开启或关闭
如果用户之前已经拒绝授权,调用相关API时将直接失败,不会再弹出授权窗口
这时,可以考虑使用 wx.openSetting ,将会跳转到这个授权列表页,让用户先手动设置
// 打开授权设置窗口,进行手动授权 wx.pro.openSetting({}) .then(rs => { console.log(rs); }).catch(e => { console.log(e); });
也可以使用 wx.authorize 来指定哪种授权,不过如果之前用户拒绝过,将在一段时间之内一直返回失败的信息,所以还是很鸡肋的..
// 获取用户的授权状态列表 wx.pro.getSetting({}) .then(rs => { console.log(rs); // 查询位置信息是否已允许授权 if (!rs.authSetting[\'scope.userLocation\']) { // 请求授权获取用户位置 wx.pro.authorize({ scope: \'scope.userLocation\' }) .then(rs => { console.log(rs); }).catch(e => { console.log(e); }); } }).catch(e => { console.log(e); });
其他相关的API
获取用户的登录状态,在调用某些API时,需要先调用此API,目前还不得而知
// 获取用户的登录状态 wx.pro.login({}) .then(rs => { console.log(rs); }).catch(e => { console.log(e); });