- 试了一下小程序的websocket,提供个简单的代码吧
[代码]var[代码] [代码]util = require([代码][代码]'../../../utils/util.js'[代码][代码]);[代码] [代码]var[代码] [代码]socketOpen = [代码][代码]false[代码][代码];[代码] [代码]var[代码] [代码]frameBuffer_Data, session, SocketTask;[代码] [代码]var[代码] [代码]url = [代码][代码]'wss://wss......./Chat'[代码][代码];[代码] [代码]Page({[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面的初始数据[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]data: {[代码] [代码] [代码][代码]toView: [代码][代码]'green'[代码][代码],[代码] [代码] [代码][代码]windowH: [代码][代码]"1000"[代码][代码],[代码] [代码] [代码][代码]user_input_text: [代码][代码]''[代码][代码], [代码][代码]//用户输入文字[代码] [代码] [代码][代码]inputValue: [代码][代码]''[代码][代码],[代码] [代码] [代码][代码]returnValue: [代码][代码]''[代码][代码],[代码] [代码] [代码][代码]addImg: [代码][代码]false[代码][代码],[代码] [代码] [代码][代码]//格式示例数据,可为空[代码] [代码] [代码][代码]allContentList: [],[代码] [代码] [代码][代码]num: 0,[代码] [代码] [代码][代码]wo: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]ta: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]youImg: [代码][代码]""[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]scrollToTop() {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].setAction({[代码] [代码] [代码][代码]scrollTop: that.windowH[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]bindKeyInput: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]inputValue: e.detail.value[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]// 提交文字[代码] [代码] [代码][代码]submitTo: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]let that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]content = that.data.inputValue;[代码] [代码] [代码][代码]var[代码] [代码]data = {[代码] [代码] [代码][代码]// body: that.data.inputValue,[代码] [代码] [代码][代码]"name"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: content,[代码] [代码] [代码][代码]"type"[代码][代码]: 5,[代码] [代码] [代码][代码]"toName"[代码][代码]: that.data.ta[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//静态显示[代码] [代码] [代码][代码]this[代码][代码].data.allContentList.push({[代码] [代码] [代码][代码]"id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"hx_id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"wo"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"ta"[代码][代码]: that.data.ta,[代码] [代码] [代码][代码]"content"[代码][代码]: content,[代码] [代码] [代码][代码]"voice_url"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"fileurl"[代码][代码]: [代码][代码]null[代码][代码],[代码] [代码] [代码][代码]"create_date"[代码][代码]: util.formatTime[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]allContentList: [代码][代码]this[代码][代码].data.allContentList,[代码] [代码] [代码][代码]inputValue: [代码][代码]''[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]//至底[代码] [代码] [代码][代码]that.bottom();[代码] [代码] [代码][代码]//socket[代码] [代码] [代码][代码]if[代码] [代码](socketOpen) {[代码] [代码] [代码][代码]// 如果打开了socket就发送数据给服务器[代码] [代码] [代码][代码]that.sendSocketMessage(data);[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]//通过 WebSocket 连接发送数据,需要先 wx.connectSocket,并在 wx.onSocketOpen 回调之后才能发送。[代码] [代码] [代码][代码]sendSocketMessage: [代码][代码]function[代码][代码](msg) {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]console.log([代码][代码]'通过 WebSocket 连接发送数据'[代码][代码], JSON.stringify(msg))[代码] [代码] [代码][代码]// debugger[代码] [代码] [代码][代码]SocketTask.send([代码] [代码] [代码][代码]{[代码] [代码] [代码][代码]data: JSON.stringify(msg)[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]function[代码] [代码](res){[代码] [代码] [代码][代码]console.log([代码][代码]'已发送'[代码][代码], res)[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码])[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]addImg: [代码][代码]function[代码][代码](e) {[代码] [代码] [代码][代码]var[代码] [代码]a = [代码][代码]this[代码][代码].data.addImg;[代码] [代码] [代码][代码]if[代码] [代码](a == [代码][代码]true[代码][代码]) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]addImg: [代码][代码]false[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]addImg: [代码][代码]true[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]tap() {[代码] [代码] [代码][代码]for[代码] [代码](let i = 0; i < order.length; ++i) {[代码] [代码] [代码][代码]if[代码] [代码](order[i] === [代码][代码]this[代码][代码].data.toView) {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]toView: order[i + 1],[代码] [代码] [代码][代码]scrollTop: (i + 1) * 200[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]break[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]tapMove() {[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]scrollTop: [代码][代码]this[代码][代码].data.scrollTop + 10[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面加载[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onLoad: [代码][代码]function[代码][代码](options) {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]ta: options.tel,[代码] [代码] [代码][代码]youImg: options.youImg,[代码] [代码] [代码][代码]wo: util.phone[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]// 获取设备高度[代码] [代码] [代码][代码]wx.getSystemInfo({[代码] [代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]var[代码] [代码]clientHeight = res.windowHeight,[代码] [代码] [代码][代码]clientWidth = res.windowWidth,[代码] [代码] [代码][代码]rpxR = 750 / clientWidth;[代码] [代码] [代码][代码]var[代码] [代码]helfH = clientHeight * 1 * rpxR;[代码] [代码] [代码][代码]//var textH = helfH - 100;[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]windowH: helfH[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]//加载历史数据[代码] [代码] [代码][代码]// util.apiHtml +http://localhost:9193[代码] [代码] [代码] [代码] [代码][代码]//修改已读[代码] [代码] [代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面初次渲染完成[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onReady: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码] [代码] [代码][代码]SocketTask.onOpen(res => {[代码] [代码] [代码][代码]socketOpen = [代码][代码]true[代码][代码];[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 连接打开事件。'[代码][代码], res)[代码] [代码] [代码][代码]//发送登陆信息[代码] [代码] [代码][代码]var[代码] [代码]data = {[代码] [代码] [代码][代码]// body: that.data.inputValue,[代码] [代码] [代码][代码]"Name"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: [代码][代码]"login"[代码][代码],[代码] [代码] [代码][代码]"type"[代码][代码]: 4[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]that.sendSocketMessage(data);[代码] [代码] [代码][代码]//循环发送心跳[代码] [代码] [代码][代码]setInterval([代码] [代码] [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]var[代码] [代码]ping = { [代码][代码]"type"[代码][代码]: [代码][代码]"ping"[代码] [代码]};[代码] [代码] [代码][代码]that.sendSocketMessage(ping);[代码] [代码] [代码][代码]}, 20000[代码] [代码] [代码][代码]); [代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onClose(onClose => {[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 连接关闭事件。'[代码][代码], onClose)[代码] [代码] [代码][代码]socketOpen = [代码][代码]false[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].webSocket()[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onError(onError => {[代码] [代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 错误。错误信息'[代码][代码], onError)[代码] [代码] [代码][代码]socketOpen = [代码][代码]false[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]SocketTask.onMessage(onMessage => {[代码] [代码] [代码][代码]console.log([代码][代码]"onMessage:::::"[代码][代码]+onMessage.data);[代码] [代码] [代码][代码]if[代码] [代码](onMessage.data.indexOf([代码][代码]"上线"[代码][代码]) != -1 || onMessage.data.indexOf([代码][代码]"下线"[代码][代码]) != -1) {[代码] [代码] [代码][代码]return[代码][代码];[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]console.log([代码][代码]'监听WebSocket接受到服务器的消息事件。服务器返回的消息'[代码][代码], JSON.parse(onMessage.data))[代码] [代码] [代码][代码]var[代码] [代码]onMessage_data = JSON.parse(onMessage.data)[代码] [代码] [代码][代码]if[代码] [代码](onMessage_data.toName == that.data.wo && onMessage_data.name == that.data.ta) {[代码] [代码] [代码][代码]// addmsglist1(msg1.name, msg1.content)[代码] [代码] [代码][代码]that.data.allContentList.push({[代码] [代码] [代码][代码]"id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"hx_id"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"wo"[代码][代码]: that.data.ta,[代码] [代码] [代码][代码]"ta"[代码][代码]: that.data.wo,[代码] [代码] [代码][代码]"content"[代码][代码]: onMessage_data.content,[代码] [代码] [代码][代码]"voice_url"[代码][代码]: [代码][代码]""[代码][代码],[代码] [代码] [代码][代码]"fileurl"[代码][代码]: [代码][代码]null[代码][代码],[代码] [代码] [代码][代码]"create_date"[代码][代码]: util.formatTime[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]that.setData({[代码] [代码] [代码][代码]allContentList: that.data.allContentList[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]that.bottom();[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]// 获取hei的id节点然后屏幕焦点调转到这个节点 [代码] [代码] [代码][代码]bottom: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]this[代码][代码].setData({[代码] [代码] [代码][代码]scrollTop: 1000000[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面显示[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onShow: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]if[代码] [代码](!socketOpen) {[代码] [代码] [代码][代码]this[代码][代码].webSocket()[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]webSocket: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]// 创建Socket[代码] [代码] [代码][代码]SocketTask = wx.connectSocket({[代码] [代码] [代码][代码]url: url,[代码] [代码] [代码][代码]data: [代码][代码]'data'[代码][代码],[代码] [代码] [代码][代码]header: {[代码] [代码] [代码][代码]'content-type'[代码][代码]: [代码][代码]'application/json'[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]method: [代码][代码]'post'[代码][代码],[代码] [代码] [代码][代码]success: [代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]console.log([代码][代码]'WebSocket连接创建'[代码][代码], res)[代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]fail: [代码][代码]function[代码][代码](err) {[代码] [代码] [代码][代码]wx.showToast({[代码] [代码] [代码][代码]title: [代码][代码]'网络异常!'[代码][代码],[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]console.log(err)[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]})[代码] [代码] [代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面隐藏[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onHide: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]SocketTask.close([代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]console.log(res);[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 生命周期函数--监听页面卸载[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onUnload: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]SocketTask.close([代码][代码]function[代码][代码](res){[代码] [代码] [代码][代码]console.log(res);[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面相关事件处理函数--监听用户下拉动作[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onPullDownRefresh: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 页面上拉触底事件的处理函数[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onReachBottom: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 用户点击右上角分享[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]onShareAppMessage: [代码][代码]function[代码][代码]() {[代码] [代码] [代码][代码]}[代码] [代码]})[代码]
2019-05-16 - 小程序跳转数量限制总结
总结一下小程序的跳转能力和数量限制 表格 来源 目标 方案 数量限制 优点 缺点 小程序 小程序 通过原生Api跳转 10个 用户体验最好 仅限跳转10个小程序,且不可动态替换 官方已放开限制 小程序 小程序 通过预览Api,预览小程序码,长按识别跳转 无 无数量限制 操作复杂,需要先预览图片,然后用户长按识别 小程序 小程序 webview打开H5,在h5中长按识别小程序码 无 无数量限制 需要对接额外开发h5,但相比上一方案,可减少一步预览图片 小程序 h5 webview 20个业务域名 减少小程序页面的开发,直接对接已有的h5 需要配置业务域名,限制为20个 公众号h5 小程序 小程序卡片 10个相同主体,3个不同主体 用户体验最好 需要关联公众号,且仅支持微信文章h5,无法动态进入 公众号h5 小程序 长按识别小程序码 无 无限制,可动态生成二维码 需要用户自己长按图片,用户体验较差 app 小程序 微信sdk 50个相同主体、5个不同主体 就这一个方案,没得选 需要在开放平台上关联app和小程序 小程序 app 小程序原生Api 无 就这一个方案,没得选 只能从小程序返回app,不能主动打开app 小程序 公众号 原生关注公众号组件 1 必须是扫码进入小程序的场景,才会出现公众号关注组件,且需要关联公众号和小程序 小程序 公众号 小程序客服消息发送公众号二维码,用户自己长按识别 无 操作复杂,用户体验差 小程序 公众号 通过预览Api,预览公众号二维码,长按识别跳转 无 无数量限制 操作复杂,需要先预览图片,然后用户长按识别 小程序 公众号 webview打开H5,在h5中长按识别公众号二维码 无 无数量限制 需要对接额外开发h5,但相比上一方案,可减少一步预览图片 思维导图 [图片]
2020-06-24 - [小程序计步] 活力健身房
简介 一款基于手机加速度传感器的跑步记录小程序。 用步伐丈量世界,在活力健身房记录你的跑步轨迹,助你更快达成你的跑步目标。 运动海报,记录每一天的变化,分享好友相互勉励,在活力健身房健身不再是孤独的坚持。 小程序码 [图片] 截图 [图片] [图片] [图片] [图片] 心率测量 最近重写了心率测量算法,使得计算精度更高,效率也变得更高,并且加入了血氧浓度估算,欢迎大家体验。 [图片] PS 这次开发的这个小程序其实就是Lebu的升级版本,算法上升级到了2.0,计算算法更加准确且高效。加入了轨迹图,逐公里的配速曲线以及逐公里的海拔曲线。还支持运动信息海报生成。终于是把在Lebu上没实现的功能都开发完成了,开心owo
2021-07-03 - 小程序特效、看他就够(欢迎大家收藏、点赞)
1、文字跑马灯效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1038 2、触摸水波涟漪效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1350 3、下拉菜单效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1875 4、五星评分效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1876 5、数字累加,动态效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1694 6、星战字幕效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1689 7、动画卡片效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2193 8、列表项左滑删除效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2189 9、图片的滤镜效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=3949 10、黑客帝国metrix效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4670 11、CSS3动画效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4628 12、仿直播点赞气泡效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=2833 13、文字弹幕效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4713 14、仿UC宣传页面的简单动画效果:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=4266 15、发短信验证码倒计时:http://www.wxapp-union.com/portal.php?mod=view&aid=1671 16、弹出菜单特效:http://www.wxapp-union.com/portal.php?mod=view&aid=1659 17、滚动动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1538 18、实时圆形进度条:http://www.wxapp-union.com/portal.php?mod=view&aid=1456 19、遮罩层:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=3617 20、仿Table效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1038 21、操作按钮悬浮固定在底部:http://www.wxapp-union.com/portal.php?mod=view&aid=1029 22、支付倒计时效果:http://www.wxapp-union.com/portal.php?mod=view&aid=890 23、文字单行背景自适应带角标:http://www.wxapp-union.com/portal.php?mod=view&aid=636 24、侧边栏滑动特效;http://www.wxapp-union.com/forum.php?mod=viewthread&tid=1202 25、顶部导航效果:http://www.wxapp-union.com/portal.php?mod=view&aid=1665 26、弹出和隐藏动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1449 27、切换动画:http://www.wxapp-union.com/portal.php?mod=view&aid=1113
2020-07-14 - 微信小商店之申请开通与开张流程
微信小商店-企业/个体店申请流程 微信小商店是小程序团队提供的一项新能力。无需开发、免费开店、帮助商家快速生成卖货小程序。目前开放内测申请,企业/个体工商户均可填写申请,申请通过的商家将会收到由“微信公众平台”下发的邀请内测通知。 点我进入详细介绍 步骤: 点击微信小商店申请链接:https://wj.qq.com/s2/6720859/9c96/ 进入申请界面 [图片] 点击箭头 [图片] 填好相关信息点击下一页 [图片] 选好商品类目,点击下一页 [图片] 点击提交,申请流程就走完了。 审核通过后,大概7天内会收到内测申请! 欢迎大家关注公众号-“五年半”,明天会给大家介绍,内测通过后的注册流程! 微信小商店优势: 接入微信10亿+的流量;售前售后一体化;可以视频直播卖货;借助微信,公众号,小程序扩宽流量;微信后续会开放个人微信小商店功能,开启全民卖货的时代!是不是很期待?笔者认为这是在跟淘宝,京东,拼多多在争夺市场。 我们可以借助这些平台,把我们的产品推广出去。 我们需要做的是: 想好卖什么;产品定位;寻找物美价廉的渠道;确定好合作的物流快递公司,争取更低的快递费用,节约成本; 点击下方文字进入: 1.微信小商店开张注册流程; 2.个人微信公众号申请及运营流程: 希望对大家有所帮助!
2020-07-25 - LouisUI-一套以类名驱动的样式库(by:许十一)
[图片] 码云地址:https://gitee.com/xu_xi/Louis-UI uni-app插件市场地址: https://ext.dcloud.net.cn/plugin?id=2370 main.wxss下载地址: https://vkceyugu.cdn.bspapp.com/VKCEYUGU-unicb9418c/fb116600-d0de-11ea-8bd0-2998ac5bbf7e.wxss 小程序开发可下载main文件改名为main.wxss直接使用全局样式 在app.wxss中 @import './common/main.wxss'; (具体路径依据你引入的位置) [图片] uni-app将louis-ui文件放到指定项目中即可 在App.vue中@import '@/louis-ui/louisui.scss'; (具体路径依据你引入的位置,注意lang=scss) [图片] 具体用法可进入小程序查看,感谢你的使用。 欢迎大家留言反馈提供宝贵意见,谢谢哈。😊 [图片]
2020-07-28 - 小程序加急审核流程上线
为优化小程序审核体验,配合各位开发者解决小程序的紧急迭代需求。平台上线了加急审核流程,开发者可根据自身业务情况进行审核加速申请。 1.加急申请入口 符合条件用户在审核提交页面【审核加急】,选择【加急】并填写【“加急类型”“ 加急说明”】情况后提交审核。 [图片] 2.加急次数说明 (1)非个人主体类型:每个自然年有3次申请加急机会 (2)个人主体类型:每个自然年有1次申请加急机会 注:①提审勾选加急后,如在审核前撤回,机会不被消耗;如加急审核单已被审核,无论审核结果通过/不通过,加急机会都将被消耗。请开发者谨慎合理使用加急机会; ②如勾选加急后,审核单被驳回。开发者在12小时内再次提交审核或者通过驳回站内信内的【前往反馈页面】提交反馈,可获得相对加急的审核队列。 3.加急审核时间段与审核时长 (1)非个人主体类型:00:00-23:59 (2)个人主体类型:9:00-22:00 审核时长:正常加急审核预计2小时内完成。请开发者结合审核工作时间及加急单等待时长综合评估提审加急单的时间。 注意:如遇节假日如春节假期前等加急提审队列拥挤,或小程序代码包含复杂逻辑等特殊情况,将无法保证加急审核在2小时内完成。 4. 以下情形的代码提审单暂不支持加急审核 选择国内主体的以下类目或选择海外主体后首次提交代码审核,需报属地网信部门复核,预计审核时长7天左右,暂不支持加急审核。 [图片] 加急审核机会是根据平台审核资源调配,配合开发者遇重大提审节点快速审核迭代的体验优化。每个小程序的加急额度是有限的,请提交前自行检查,确保加速版本的小程序符合法律法规和平台规则,避免浪费有效加急机会。同时,开发者也可以通过小程序评测达标来获取更多的加急机会。 加急审核机制上线后,我们会根据开发者的使用额度情况及审核资源情况等,对目前加速审核机制进行动态调整与优化。
2022-07-26 - 短时间内快速点击按钮,导致重复提交解决方案总结
本文背景短时间内快速点击按钮,会导致数据重复提交,本文主要针对这种情况进行分析,并参考社区给出三种解决方案 本文内容通过参考社区以及实践操作,我总结以下几种方案 (1)设置状态位,点击状态位,默认可以点击,点击后,状态位为不可点击,参考下面第一篇文章 (2)按钮隐藏,点击按钮后,将按钮隐藏,参考下面第二篇文章 (3)设置模态对话框,具体为wx.showLoading,设置参数mask设为true 参考文章(1) 如何保证按钮不被高频点击?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0002cef10305e8eab28a1bc0951800 (2) button share如何防止多次点击,安卓手机会多次调用转发功能? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/000a88dc6b01f87c3687692035b800 (3) wx.showLoading,加上mask:true,真机上快速点击还是能重复请求?? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/00042665298db841bc6a786fb56c00 本文总结快速点击,重复提交是小程序日常开发经常遇到的细节问题,如果不住啊们处理,是容易造成后患的,比如答题活动场景,重复提交会造成累计得分里面多加了一次 实践反馈目前在党建答题小程序中为防止重复提交,采用上面方案(3),偶尔还有重复提交情况存在,我会在后续跟进,这个问题在参考文章(3)有用户提到。
2020-07-16 - 小程序1rpx边框不完美解决方案
在小程序开发中,1rpx边框随处可见, 像上图UI给的设计稿,如果只是简单使用[代码]border: 1rpx solid red;[代码]的话,在不同的机型上会有不同的表现 [图片] 表现IOS 机型上[图片] Android机型上[图片] 由图片可以看出, IOS机型上会有边框缺失(然而经常出现缺不能稳定复现), 而Android机型上边框比较粗 原因上面这两种表现形式很难联系到一起 首先先看IOS边框缺失的问题,借鉴网络上前辈们的经验 当父元素的高度为奇数,容易出现上下边框缺失,同理宽度为奇数,容易出现左右边框缺失解决办法是在边框内部添加一个1rpx的元素或者伪元素, 撑开内部使父元素的宽高是偶数。 然而我们发现这种方案在Iphone 6等2倍屏可以生效, 但放在如Iphone X等3倍屏下面就很飘了, 还是经常会出现边框缺失的情况, 这种情况下再去把父元素改为2和3共同的倍数就非常不现实了。 再回过头看导致边框缺失的具体原因是啥。 在这之前需要了解下高分屏的物理像素和虚拟像素的概念 简单来说物理像素是设备的实际像素 虚拟像素是设备的坐标点, 可以简单理解为css像素 而rpx类似rem,渲染后实际转换成px之后可能存在小数,在不同的设备上多多少少会存在渲染的问题。而1rpx的问题就更加明显,因为不足1个物理像素的话,在IOS会进行四舍五入,而安卓好像统一向上取整,这也是上面两种设备表现不同的原因。 解决方法我们采用的方法是采用translate:scale(0.5)的方法对边框进行缩放 具体的代码如下 .border1rpx, .border1rpx_before{ position: relative; border-width: 0rpx !important; padding: 0.5rpx; z-index: 0; } .border1rpx::after, .border1rpx_before::before{ content: ""; border-style: inherit; border-color: inherit; border-radius: inherit; box-sizing: border-box !important; position: absolute; border-width: 2rpx !important; left: 0; top: 0; width: 200% !important; height: 200% !important; transform-origin: 0 0; transform: scale(0.5) !important; z-index: -1; } .border1rpx-full { margin: -1rpx; } 给.border1rpx的元素设置边框宽度为0给::after伪元素宽高为两倍,边框设置2rpx,边框其他样式继承元素的设置然后再缩放0.5来达到边框为1rpx的效果 用法基础用法给相应的元素添加border1rpx的class即可, (.borde1rpx说:我们不生产边框,只是边框的搬运工,要显示边框样式的话还需要在元素上自行设置) 圆角边框圆角边框需要自行设置相应伪元素::before 或 ::after的border-raduis值为预期的2倍, 如原本想要设置10rpx的圆角,需要设置[代码].xxx::after{border-raduis: 20rpx;}[代码] 边框内部填充由于设计原因,目标元素会留1rpx的padding用于显示伪元素的边框,如果内部元素是填充的,正常会看到填充元素和目标元素有小部分间隙,此时需要给填充元素添加.border1rpx_full来解决 注意点此方案默认使用::after伪元素实现边框,如果目标元素的after被占用(如iconfont),请使用[代码].border1rpx_before[代码]如单独设置边框(如上边框), [代码]border: 1rpx solid red;border-width: 1rpx 0 0 0;[代码]不能被正确继承,请使用简写[代码]border-top: 1rpx solid red;[代码]由于设计原因,目标元素请最少设置1rpx的padding用于显示边框,(上面的样式已经有了默认的padding,不写也可以, 只是不要用padding:0覆盖)请自行测试点击功能是否正常,防止层级关系导致元素区域被伪元素覆盖
2020-07-23 - 多形态小程序日历组件,轻松搞定项目需求
小程序日历组件 小程序日历组件,支持多种模式,简单易用好上手。 4种日历模式 3种日期选择方式 支持自定义节假日 支持自定义日期内容 懒加载保证渲染性能 支持农历 支持根据指定日期自动生成 支持跨无数据月份 [图片] [图片] [图片] [图片] [图片] 日历组件基础配置 wxml模板 [代码]<ui-calendar dataSource="{{config}}" /> [代码] 配置日历组件 [代码]Pager({ data: { source: { $$id: 'calendar', mode: 1, // 纵向日历 type: 'range', // 区域选择 tap: 'onTap', // page响应事件 total: 365, // 指定日历总天数 data: [], // 按给定日期计算total值,自动构建日历 rangeCount: 28, // 区选区间28天 rangeMode: 2, // 区选模式 rangeTip: ['入住', '离店'], // 区选提示 festival: true, // 开启节假日显示 alignMonth: false, // 月份对齐,swiper切换时 lunar: false, // 是否显示农历 date: [], // 指定日期显示的内容 value: ['2019-12-24', '2020-01-05'], // 默认值 toolbox: { monthHeader: true, // 是否显示月头 discontinue: false, // 自动构建时,是否省略无数据的月份 }, methods: { // 响应 tap事件 onTap(e, param, inst) { // param.date 选中的当前日期 // 当区选模式时 // param.range === 'start' 区选第一天 // param.range === 'end' 区选最后一天 } } } } }) [代码] github地址:https://github.com/webkixi/aotoo-xquery 小程序demo演示 [图片]
2020-06-30 - 微信小程序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 - 微信小程序自定义(裁剪)“小字库”制作与使用
注意:此方法适合制作小点的字库,因为手动制作过程有点繁琐 准备工作: 下载FontCreator工具--->链接:https://pan.baidu.com/s/1sEb16MCoekGnBhnLoWEzMQ 提取码:jt7z 字库格式转换网站:https://transfonter.org/激活FontCreator自行百度()网上下载字库 过程省略.... 裁剪字库 1. 使用FontCreator工具裁剪(或者自定义)字库 2. 先通过https://transfonter.org/网站转换成ttf格式的字库格式。 [图片] 3. 打开FontCreator软件,点击File >> Open选择刚才转换的simsun.ttf文件,然后打开它 裁剪字库 [图片] 4. 新建文件:File >> New [图片] 5. 点击工具栏中“插入新字形” [图片] 6. 从打开的宋体字库中复制一个字形到刚才新建的文件的“新字形”处: 复制 [图片] 拷贝 [图片] 7. 编辑单个字形的属性 [图片] 8. 设置字形编码值 [图片] 9. 根据字形表示的字符来设置,这里我选择的是大写,这个根据你要设置的字符来自行设置 [图片] 10. 重复以上步骤完成所有字形的编码值设置。最后在最前面设置一个空字形(据说是防止字库不被识别) [图片] 11. 保存裁剪后的字库,并导出为ttf格式。 [图片] 12. 将生成的字库转换成base64格式 [图片] 导入到微信小程序目录中 1. 解压将刚才转换成base64的压缩包。 2. 在微信小程序目录中新建一个style目录,并在style文件夹下新建一个wxss文件,并将转换成base64的压缩包中的wxss文件中的内容全部复制到新建的wxss文件中 [图片] [图片] 注意:新建的wxss文件中的css代码中font-family的值,这个后面其他wxss文件中设置字体时需要用到。这里的是“simsun” 3. 在需要使用字体的wxss文件的开头通过import语法来导入字库。然后就可以在wxss文件中使用了。注意路径 [图片] 使用自定义的字库,注意名称。 [图片] 4. 编译小程序,即可查到效果 [图片] 吐槽下:微信社区发布文章的工具真不好用,格式排版不好弄。格式不好,请见谅! 参考文档: https://blog.csdn.net/xiaohui_brook/article/details/51909612 https://blog.csdn.net/qq_24985715/article/details/80857733
2019-12-27 - 纯CSS实现圆环型进度条
以下内容来自于去年的一次案例,随着微信小程序的不断改版,部分条件可能已不再适用,请谨慎参考。内容比较短,主要都在代码片段里。 案例 某个项目中需要用到如下图这样的一个圆环行的进度条。 [图片] 一开始的想法是使用canvas来实现,但是canvas是原生组件,层级最高(当时的情况),实际使用时不方便使用。所以决定尝试用纯CSS来实现这一效果。 实现原理 先上代码:https://developers.weixin.qq.com/s/gjmxwUmm76dG 这里主要用到的是CSS中的clip属性,将一个正方形裁剪后只显示右侧一半,但是仍然以正方形中心为圆心来旋转,来实现需要的角度。 [图片] [代码]clip: rect(0rpx, 46rpx, 92rpx, 0rpx); [代码] 这样最上面那个进度条就可以由以下三部分叠加,在最上面再叠加一个小一号的白色圆形,最外层加上圆角后就可以实现。(下图中红线示例了最外层的圆角以及最上层叠加的白色圆形位置) [图片] 叠加效果 [图片] 用到蓝色圆环小于180度的情况下,需要把背景色和前景色对调。
2019-12-26 - 小程序页面(Page)扩展,为所有页面添加公共的生命周期、事件处理等函数
背景 在小程序的原生开发中,页面中经常会用到一些公共方法,例如在页面onLoad中验证权限、所有页面都需要onShareAppMessage设置分享等 假设我们在编码时每个页面都写一遍,显然不是一个高级程序员会干的事情,太Low了。如果我们定义一个公共文件,导出这些公共方法,每个页面都引入,然后再生命周期或者事件处理函数中调用,虽然看起来很方便,但不够优雅,达不到我们最终的目的(偷懒)。 下面给大家介绍一种相对比较优雅的实现方式,扩展Page来实现以上的操作。 Page(页面) 需要传入的是一个 [代码]object[代码] 类型的参数,那么我们重载一个 [代码]Page[代码] 函数,将这个 [代码]object[代码] 参数拦截改掉就可以了,下面直接上代码。 实现 1、在根目录新建一个 [代码]page-extend.js[代码] 文件,公共的逻辑都写在这里面 [代码]/** * * Page扩展函数 * * @param {*} Page 原生Page */ const pageExtend = Page => { return object => { // 导出原生Page传入的object参数中的生命周期函数 // 由于命名冲突,所以将onLoad生命周期函数命名成了onLoaded const { onLoaded } = object // 公共的onLoad生命周期函数 object.onLoad = function (options) { // 在onLoad中执行的代码 ... // 执行onLoaded生命周期函数 if (typeof onLoaded === 'function') { onLoaded.call(this, options) } } // 公共的onShareAppMessage事件处理函数 object.onShareAppMessage = () => { return { title: '分享标题', imageUrl: '分享封面' } } return Page(object) } } // 获取原生Page const originalPage = Page // 定义一个新的Page,将原生Page传入Page扩展函数 Page = pageExtend(originalPage) [代码] 2、在 [代码]app.js[代码] 中引入 [代码]page-extend.js[代码] 文件 [代码]require('./page-extend') App({ // 其他代码 ... }) [代码] 代码片段 https://developers.weixin.qq.com/s/Cyx8iGmV7Ldp 本文内容及评论未经允许,禁止任何形式的转载与复制(代码可在程序中使用)
2019-12-24 - 单选框radio除了可以传value可以传其他的值吗?
为什么用data-传值在bindChange事件里接收不到,我要传这个data-id的值到radioChange里接收到,但是没能获取到,只能获取到value的值 [图片] [代码]只能是string类型 不过你可以使用wxs处理一下 <wxs module="JSON"> module.exports = { stringify : function(item){ return JSON.stringify(item) } } </wxs> <radio-group ... > .... <radio value="{{JSON.stringify(item)}}" checked="{{item.checked}}" /> </radio-group> ========================= radioChange: function (e) { let d = JSON.parse(e.detail.value) console.log('radio发生change事件,携带value值为:', d) } [代码] 具体帖子来源于 https://developers.weixin.qq.com/community/develop/doc/000886449405400eb579ea0615b400 已亲测
2019-11-29 - wxs中getDate(datestring)在IOS下有BUG
[代码]<[代码][代码]wxs[代码] [代码]module[代码][代码]=[代码][代码]'format'[代码][代码]>[代码][代码] [代码][代码]var date = function(t) {[代码][代码] [代码][代码]var regexp = getRegExp('-', 'g');[代码][代码] [代码][代码]t = t.replace(regexp, '/');[代码][代码] [代码][代码]var now = getDate().getTime();[代码][代码] [代码][代码]var time = getDate(t).getTime();[代码][代码] [代码][代码]if (time > now) {[代码][代码] [代码][代码]return '刚刚';[代码][代码] [代码][代码]} else {[代码][代码] [代码][代码]var e = Math.round((now - time) / 1000);[代码][代码] [代码][代码]if (e < [代码][代码]60[代码][代码]) return '刚刚';[代码][代码] [代码][代码]else if (e < 1800) return Math.round(e / 60) + '分钟前';[代码][代码] [代码][代码]else if (e < 3600) return '半小时前';[代码][代码] [代码][代码]else if (e < 86400) return Math.round(e / 3600) + '小时前';[代码][代码] [代码][代码]else if (e < 86400 * 7) return Math.round(e / 86400) + '天前';[代码][代码] [代码][代码]else if (e < 86400 * 30) return Math.round(e / (86400 * 7)) + '周前';[代码][代码] [代码][代码]else if (e < 86400 * 365) return Math.round(e / (86400 * 30)) + '个月前';[代码][代码] [代码][代码]else return Math.round(e / (86400 * 365)) + '年前';[代码][代码] [代码][代码]}[代码][代码] [代码][代码]return '';[代码][代码] [代码][代码]};[代码][代码] [代码][代码]module.exports = {[代码][代码] [代码][代码]date: date[代码][代码] [代码][代码]};[代码][代码]</wxs>[代码][代码]<[代码][代码]text[代码] [代码]class[代码][代码]=[代码][代码]'time'[代码][代码]>{{format.date(addtime)}}</[代码][代码]text[代码][代码]>[代码]目前我已经通过regexp正则替换掉了“-”为“/”。 默认情况下数据库拿到的日期格式为“2018-08-30 12:00:00”,如果不替换“-”的话,在IOS下是不能通过getDate(datestring)获取到日期对象的。Android下两种格式均表现正常,这是否是个BUG呢?还求官方解答。
2018-08-30 - 做了一个颜色选择器
edit at 11/12 代码传到了:https://github.com/eclipseglory/zasi-components , DEMO演示在文章结尾 小程序没有提供color-picker类似的组件,只能自己做。 可传统的RGB颜色选择器,真的腻了,而且在手机上也不是很操作,就跑网上搜了一圈,发现有一种圆环形的(基于HSV)我很喜欢: [图片] 我自诩对canvas2d和webgl很熟悉,做个这玩意儿很轻松,开始做!没想到痛苦开始了。 从上周5开始,一共做了三个版本: 1.纯canvas版本 2.canvas+组件版本 3.纯组件版本 纯canvas版本这个版本做了整整一天! [图片] 由于canvas绘制性能问题,特别是因为没有requestAnimationFrame可以调用,别说在真机上测试特别不流畅,就是在模拟器上也小卡小卡的。而且,在纯的canvas进行触摸定位等事件响应处理,计算起来太麻烦,bug不断,只能放弃了。 混合版本因为wxs模块是提供requestAnimationFrame接口的,所以我就想,使用canvas作为底部颜色环,上面就直接用view作为指针,这样,事件触发和处理比起纯canvas要简单得多,而且还能利用rAF回调页面接口去绘制其他canvas。 的确,我的想法得到了证实,这个混合版本比起第一个要流畅得多! 可就要完工的时候,我却发现,在真机上,cover-view的鼠标事件有很大问题,坐标值飘忽不定,也就是说拖动指针会发生鬼畜般的抖动!加上我不知道怎么debug到wxs模块中,于是跟个sb一样fix,找了半天也没找到问题在哪儿,直到我搜索时,返现有人也遇到和我一样的问题,我才安心了:这是小程序的问题。 动手改!既然cover-view有不行,那就不用它。 实际上canvas在该组件中的作用无非就是绘制一个圆环而已,如果我利用离屏canvas事先画好,然后保存成图片,再用image加载它,这样就可以避免使用canvas来显示圆环了,也就可以不用cover-view放到其顶部! 想法是好的,可是到了真机上,绘制保存出来的图片时好时坏: [图片] 只能放弃,又耽误我一天。 无canvas版本刚才说了,canvas在该组件中的作用,仅仅是绘制一个颜色环而已,除此之外真没什么用。 那我就用css模拟一个类似圆环就好了,精确到每一度一个颜色一点意义没有。 所以就利用css的background-image属性,做了4个四分之一圆弧,然后拼在一起,得到了一个彩色原版,再用一个小的view遮挡,让它们只露出一部分,圆环就做好了。 之前的代码都不用改,直接用新作的圆环views替换canvas的标签即可。主体框架和功能,不到一天就完成了,不得不说,比起纯的canvas绘制,要方便太多太多。 这是截图: [图片] 代码片段这里是 演示DEMO,要使用的话,复制里面的组件出来用就好。 有些代码我混淆过,但不耽误使用。 有问题找我
2019-11-12 - kbone,十分钟让 Vue 项目同时支持小程序
什么是kbone 微信小程序开发过程中,许多开发者会遇到 小程序 与 Web 端一起的需求,由于 小程序 与 Web 端的运行环境不同,开发者往往需要维护两套类似的代码,这对开发者来说比较耗费力气,并且会出现不同步的情况。 为了解决上述问题,微信小程序推出了同构解决方案 [代码]kbone[代码] 来解决此问题。 那么,[代码]kbone[代码] 要怎么使用呢?这里我们将通过一个 [代码]todo[代码] 的例子来跟大家讲解。 基本结构 首先,我们来看下一个基本的 kbone 项目的目录结构(这里的 [代码]todo[代码] 是基于 [代码]Vue[代码] 的示例,[代码]kbone[代码] 也有 [代码]React[代码],[代码]Preact[代码],[代码]Omi[代码] 等版本,详情可移步 kbone github)。 因为 kbone 是为了解决 小程序 与 Web 端的问题,所以每个目录下的配置都会有两份(小程序 与 Web 端各一份) [图片] 入口 不管是 小程序 端还是 Web 端,都需要入口文件。在 [代码]src/index[代码] 目录下,[代码]main.js[代码] 为 Web 端用主入口,[代码]main.mp.js[代码] 则为 小程序 端用主入口。 当然,Web 端会比 小程序 多一个入口页面,即 [代码]index.html[代码](位于根目录下)。 [图片] 下面两段代码分别是 小程序端 入口与 Web 端入口的代码,可以看到 小程序端的入口代码封装在 [代码]createApp[代码] 函数里面(这里固定即可),内部会比 Web 端多一个创建 [代码]app[代码] 节点的操作,其他的基本就是一致的。 [代码]// 小程序端入口 import Vue from 'vue' import todo from './todo.vue' export default function createApp() { // 创建app节点用于绑定 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) return new Vue({ el: '#app', render: h => h(todo) }) } [代码] [代码]// web端入口 import Vue from 'vue' import todo from './todo.vue' new Vue({ el: '#app', render: h => h(todo) }) [代码] todo.vue 在上面的入口图可以看到,源码目录中,除了入口文件分开之前,页面文件就是共用的了,这里直接使用 Vue 的写法即可,不用做特殊的适应。 配置 写完代码之后,我们要怎么跑项目呢?这时,配置就派上用场啦。 Web 端配置为正常的 Vue 配置,小程序端配置与 Web 端配置的唯一不同就是需要引入 [代码]mp-webpack-plugin[代码] 插件来将 Vue 组件转化为小程序代码。 [图片] 构建代码 接着,我们需要构建代码,让代码可以运行到各自的运行环境中去。构建完成后,生产代码会位于 dist 目录中。 [代码]// 构建 web 端代码 // 目标代码在 dist/web npm run build // 构建小程序端代码 // 目标代码在 dist/mp npm run mp [代码] 小程序端 的构建会比 Web 端的构建多一个步骤,就是 npm 构建。 进入 [代码]dist/mp[代码] 目录,执行 [代码]npm install[代码] 安装依赖,用开发者工具将 [代码]dist/mp[代码] 目录作为小程序项目导入之后,点击工具栏下的 [代码]构建 npm[代码],即可预览效果。 效果 最后,我们来看一下 todo 的效果。kbone 初体验,done~ todo 代码可到 kbone/demo13 自提。 [图片] 最后 如果你想了解更多 kbone 相关的使用及详情,可移步 kbone github。 如有疑问,可到 Kbone小主页 发帖沟通。
2020-04-22 - 答题小程序搭建系列一
答题小程序搭建系列一 后台界面 截图一 [图片] 截图二 [图片] 截图三 [图片] 截图四 [图片] 截图五 [图片] 截图六 [图片] 截图七 [图片] 截图八 [图片] 截图九 [图片] 截图十 [图片] 截图十一 [图片] 截图十二 [图片] 数据设计 [图片] 截图一 [图片] 截图二 [图片] 截图三 [图片] 截图四 [图片] 更新记录 答题小程序 2020-10-12 新增对主观题的支持,具体包括以下几种类型 [代码] "单选"=>"01", "多选"=>"02", "判断"=>"03", "填空"=>"04", "简答"=>"05", "论述"=>"06", "名词解释"=>"07" [代码] 其他
2020-10-12 - 小程序识别身份证,银行卡,营业执照,驾照
最近老是有同学问我小程序ocr识别的问题,就趁机研究了下,实现了小程序识别身份证,银行卡,驾照,营业执照,图片文字的功能。今天来给大家讲讲详细的实现流程。 先画一张流程图出来 [图片] 第一次看到这个流程图,可能有点萌,什么云开发,云函数。。。。 不要着急,我们接下来会一步步带大家实现。 先看下我们的页面和效果图。 [图片] 功能其实很简单,就是我们点对应的按钮后,去拍照或者去相册选择对应的图片。然后把图片上传到云存储,会有一个对应的图片url,然后把这个图片url传递到云函数,然后云函数里使用小程序的开发ocr能力,来识别图片,返回对应的信息回来。如下图所示,我们识别银行卡(身份证什么的就不演示了,涉及到石头哥个人隐私) [图片] 接下来就是代码的实现了。 一,首先要创建一个云开发的小程序项目 这里我前面文章有讲解过,就不再细说了,不会的同学去翻看下我之前的文章。或者看下我录制的 讲解视频 这里有一点需要注意的给大家说下 [图片] 二,创建一个简单的小程序页面 1,index.wxml如下 [图片] 2,index.js完整代码如下 [代码]Page({ //身份证 shenfenzheng() { this.photo("shenfenzheng") }, //银行卡 yinhangka() { this.photo("yinhangka") }, //行驶证 xingshizheng() { this.photo("xingshizheng") }, //拍照或者从相册选择要识别的照片 photo(type) { let that = this wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success(res) { // tempFilePath可以作为img标签的src属性显示图片 let imgUrl = res.tempFilePaths[0]; that.uploadImg(type, imgUrl) } }) }, // 上传图片到云存储 uploadImg(type, imgUrl) { let that = this wx.cloud.uploadFile({ cloudPath: 'ocr/' + type + '.png', filePath: imgUrl, // 文件路径 success: res => { console.log("上传成功", res.fileID) that.getImgUrl(type, res.fileID) }, fail: err => { console.log("上传失败", err) } }) }, //获取云存储里的图片url getImgUrl(type, imgUrl) { let that = this wx.cloud.getTempFileURL({ fileList: [imgUrl], success: res => { let imgUrl = res.fileList[0].tempFileURL console.log("获取图片url成功", imgUrl) that.shibie(type, imgUrl) }, fail: err => { console.log("获取图片url失败", err) } }) }, //调用云函数,实现OCR识别 shibie(type, imgUrl) { wx.cloud.callFunction({ name: "ocr", data: { type: type, imgUrl: imgUrl }, success(res) { console.log("识别成功", res) }, fail(res) { console.log("识别失败", res) } }) } }) [代码] 上面代码注释讲解的很清楚了,再结合我们的流程图,相信你可以看明白。 [图片] 三,重头戏来了,识别的核心代码是下面这个云函数 [图片] 云函数的完整代码也给大家贴出来 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { let { type, imgUrl } = event switch (type) { case 'shenfenzheng': { // 识别身份证 return shenfenzheng(imgUrl) } case 'yinhangka': { // 识别银行卡 return yinhangka(imgUrl) } case 'xingshizheng': { // 识别行驶证 return xingshizheng(imgUrl) } default: { return } } } //识别身份证 async function shenfenzheng(imgUrl) { try { const result = await cloud.openapi.ocr.idcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } //识别银行卡 async function yinhangka(imgUrl) { try { const result = await cloud.openapi.ocr.bankcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } //识别行驶证 async function xingshizheng(imgUrl) { try { const result = await cloud.openapi.ocr.vehicleLicense({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } } [代码] 其实没什么特别的,就是用一个switch方法,根据用户传入的不同的type值,来实现不同的识别效果。 如用传入的type是‘ yinhangka’,我们就调用银行卡识别 [代码]try { const result = await cloud.openapi.ocr.bankcard({ type: 'photo', imgUrl: imgUrl }) return result } catch (err) { console.log(err) return err } [代码] 进而把识别的结果返回给小程序端,如下图 [图片] 到这里我们就完整的实现了,小程序识别身份证,银行卡,行驶证的功能。至于别的更多的ocr识别,可以去看小程序官方文档,结合着我的这篇文章,相信你也可以轻松实现更多的图片识别。 [图片] 源码其实在上面都已经贴给大家了,如果你觉得不完整,想要完整的源码可以在文章底部留言或者私信我。
2019-10-30 - 【开箱即用】分享几个好看的波浪动画css效果!
以下代码不一定都是本人原创,很多都是借鉴参考的(模仿是第一生产力嘛),有些已忘记出处了。以下分享给大家,供学习参考!欢迎收藏补充,说不定哪天你就用上了! 一、第一种效果 [图片] [代码]//index.wxml <view class="zr"> <view class='user_box'> <view class='userInfo'> <open-data type="userAvatarUrl"></open-data> </view> <view class='userInfo_name'> <open-data type="userNickName"></open-data> , 欢迎您 </view> </view> <view class="water"> <view class="water-c"> <view class="water-1"> </view> <view class="water-2"> </view> </view> </view> </view> //index.wxss .zr { color: white; background: #4cb4e7; /*#0396FF*/ width: 100%; height: 100px; position: relative; } .water { position: absolute; left: 0; bottom: -10px; height: 30px; width: 100%; z-index: 1; } .water-c { position: relative; } .water-1 { background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0xPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMTMzLjAwMDAwMCkiIGZpbGwtb3BhY2l0eT0iMC4zIiBmaWxsPSIjRkZGRkZGIj4KICAgICAgICAgICAgPGcgaWQ9IndhdGVyLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMS4wMDAwMDAsIDEzMy4wMDAwMDApIj4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLDcuNjk4NTczOTUgTDQuNjcwNzE5NjJlLTE1LDYwIEw2MDAsNjAgTDYwMCw3LjM1MjMwNDYxIEM2MDAsNy4zNTIzMDQ2MSA0MzIuNzIxMDUyLDI0LjEwNjUxMzggMjkwLjQ4NDA0LDcuMzU2NzQxODcgQzE0OC4yNDcwMjcsLTkuMzkzMDMwMDggMCw3LjY5ODU3Mzk1IDAsNy42OTg1NzM5NSBaIiBpZD0iUGF0aC0xIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-1 3.5s infinite linear; animation: wave-animation-1 3.5s infinite linear; } .water-2 { top: 5px; background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjYwMHB4IiBoZWlnaHQ9IjYwcHgiIHZpZXdCb3g9IjAgMCA2MDAgNjAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM6c2tldGNoPSJodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2gvbnMiPgogICAgPCEtLSBHZW5lcmF0b3I6IFNrZXRjaCAzLjQgKDE1NTc1KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT53YXRlci0yPC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IuaIkSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Ii0iIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMjEuMDAwMDAwLCAtMjQ2LjAwMDAwMCkiIGZpbGw9IiNGRkZGRkYiPgogICAgICAgICAgICA8ZyBpZD0id2F0ZXItMiIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxLjAwMDAwMCwgMjQ2LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTAsNy42OTg1NzM5NSBMNC42NzA3MTk2MmUtMTUsNjAgTDYwMCw2MCBMNjAwLDcuMzUyMzA0NjEgQzYwMCw3LjM1MjMwNDYxIDQzMi43MjEwNTIsMjQuMTA2NTEzOCAyOTAuNDg0MDQsNy4zNTY3NDE4NyBDMTQ4LjI0NzAyNywtOS4zOTMwMzAwOCAwLDcuNjk4NTczOTUgMCw3LjY5ODU3Mzk1IFoiIGlkPSJQYXRoLTIiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMC4wMDAwMDAsIDMwLjAwMDAwMCkgc2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtMzAwLjAwMDAwMCwgLTMwLjAwMDAwMCkgIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat-x; background-size: 600px; -webkit-animation: wave-animation-2 6s infinite linear; animation: wave-animation-2 6s infinite linear; } .water-1, .water-2 { position: absolute; width: 100%; height: 60px; } .back-white { background: #fff; } @keyframes wave-animation-1 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } @keyframes wave-animation-2 { 0% { background-position: 0 top; } 100% { background-position: 600px top; } } .user_box { display: flex; z-index: 10000 !important; opacity: 0; /* 透明度*/ animation: love 1.5s ease-in-out; animation-fill-mode: forwards; } .userInfo_name { flex: 1; vertical-align: middle; width: 100%; margin-left: 5%; margin-top: 5%; font-size: 42rpx; } .userInfo { flex: 1; width: 100%; border-radius: 50%; overflow: hidden; max-height: 50px; max-width: 50px; margin-left: 5%; margin-top: 5%; border: 2px solid #fff; } [代码] 二、第二种效果 [图片] [代码]//index.wxml <view class="waveWrapper waveAnimation"> <view class="waveWrapperInner bgTop"> <view class="wave waveTop" style="background-image: url('https://s2.ax1x.com/2019/09/26/um8g7n.png')"></view> </view> <view class="waveWrapperInner bgMiddle"> <view class="wave waveMiddle" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGZ38.png')"></view> </view> <view class="waveWrapperInner bgBottom"> <view class="wave waveBottom" style="background-image: url('https://s2.ax1x.com/2019/09/26/umGuuQ.png')"></view> </view> </view> //index.wxss .waveWrapper { overflow: hidden; position: absolute; left: 0; right: 0; height: 300px; top: 0; margin: auto; } .waveWrapperInner { position: absolute; width: 100%; overflow: hidden; height: 100%; bottom: -1px; background-image: linear-gradient(to top, #86377b 20%, #27273c 80%); } .bgTop { z-index: 15; opacity: 0.5; } .bgMiddle { z-index: 10; opacity: 0.75; } .bgBottom { z-index: 5; } .wave { position: absolute; left: 0; width: 500%; height: 100%; background-repeat: repeat no-repeat; background-position: 0 bottom; transform-origin: center bottom; } .waveTop { background-size: 50% 100px; } .waveAnimation .waveTop { animation: move-wave 3s; -webkit-animation: move-wave 3s; -webkit-animation-delay: 1s; animation-delay: 1s; } .waveMiddle { background-size: 50% 120px; } .waveAnimation .waveMiddle { animation: move_wave 10s linear infinite; } .waveBottom { background-size: 50% 100px; } .waveAnimation .waveBottom { animation: move_wave 15s linear infinite; } @keyframes move_wave { 0% { transform: translateX(0) translateZ(0) scaleY(1) } 50% { transform: translateX(-25%) translateZ(0) scaleY(0.55) } 100% { transform: translateX(-50%) translateZ(0) scaleY(1) } } [代码] 三、第三种效果 [图片] [代码]//index.wxml <view class="container"> <image class="title" src="https://ftp.bmp.ovh/imgs/2019/09/74bada9c4143786a.png"></image> <view class="content"> <view class="hd" style="transform:rotateZ({{angle}}deg);"> <image class="logo" src="https://ftp.bmp.ovh/imgs/2019/09/d31b8fcf19ee48dc.png"></image> <image class="wave" src="wave.png" mode="aspectFill"></image> <image class="wave wave-bg" src="wave.png" mode="aspectFill"></image> </view> <view class="bd" style="height: 100rpx;"> </view> </view> </view> //index.wxss image{ max-width:none; } .container { background: #7acfa6; align-items: stretch; padding: 0; height: 100%; overflow: hidden; } .content{ flex: 1; display: flex; position: relative; z-index: 10; flex-direction: column; align-items: stretch; justify-content: center; width: 100%; height: 100%; padding-bottom: 450rpx; background: -webkit-gradient(linear, left top, left bottom, from(rgba(244,244,244,0)), color-stop(0.1, #f4f4f4), to(#f4f4f4)); opacity: 0; transform: translate3d(0,100%,0); animation: rise 3s cubic-bezier(0.19, 1, 0.22, 1) .25s forwards; } @keyframes rise{ 0% {opacity: 0;transform: translate3d(0,100%,0);} 50% {opacity: 1;} 100% {opacity: 1;transform: translate3d(0,450rpx,0);} } .title{ position: absolute; top: 30rpx; left: 50%; width: 600rpx; height: 200rpx; margin-left: -300rpx; opacity: 0; animation: show 2.5s cubic-bezier(0.19, 1, 0.22, 1) .5s forwards; } @keyframes show{ 0% {opacity: 0;} 100% {opacity: .95;} } .hd { position: absolute; top: 0; left: 50%; width: 1000rpx; margin-left: -500rpx; height: 200rpx; transition: all .35s ease; } .logo { position: absolute; z-index: 2; left: 50%; bottom: 200rpx; width: 160rpx; height: 160rpx; margin-left: -80rpx; border-radius: 160rpx; animation: sway 10s ease-in-out infinite; opacity: .95; } @keyframes sway{ 0% {transform: translate3d(0,20rpx,0) rotate(-15deg); } 17% {transform: translate3d(0,0rpx,0) rotate(25deg); } 34% {transform: translate3d(0,-20rpx,0) rotate(-20deg); } 50% {transform: translate3d(0,-10rpx,0) rotate(15deg); } 67% {transform: translate3d(0,10rpx,0) rotate(-25deg); } 84% {transform: translate3d(0,15rpx,0) rotate(15deg); } 100% {transform: translate3d(0,20rpx,0) rotate(-15deg); } } .wave { position: absolute; z-index: 3; right: 0; bottom: 0; opacity: 0.725; height: 260rpx; width: 2250rpx; animation: wave 10s linear infinite; } .wave-bg { z-index: 1; animation: wave-bg 10.25s linear infinite; } @keyframes wave{ from {transform: translate3d(125rpx,0,0);} to {transform: translate3d(1125rpx,0,0);} } @keyframes wave-bg{ from {transform: translate3d(375rpx,0,0);} to {transform: translate3d(1375rpx,0,0);} } .bd { position: relative; flex: 1; display: flex; flex-direction: column; align-items: stretch; animation: bd-rise 2s cubic-bezier(0.23,1,0.32,1) .75s forwards; opacity: 0; } @keyframes bd-rise{ from {opacity: 0; transform: translate3d(0,60rpx,0); } to {opacity: 1; transform: translate3d(0,0,0); } } [代码] wave.png(可下载到本地) [图片] 在这个基础上,再加上js的代码,即可实现根据手机倾向,水波晃动的效果 wx.onAccelerometerChange(function callback) 监听加速度数据事件。 [图片] [代码]//index.js Page({ onReady: function () { var _this = this; wx.onAccelerometerChange(function (res) { var angle = -(res.x * 30).toFixed(1); if (angle > 14) { angle = 14; } else if (angle < -14) { angle = -14; } if (_this.data.angle !== angle) { _this.setData({ angle: angle }); } }); }, }); [代码] 四、第四种效果 [图片] [代码]//index.wxml <view class='page__bd'> <view class="bg-img padding-tb-xl" style="background-image:url('http://wx4.sinaimg.cn/mw690/006UdlVNgy1g2v2t1ih8jj31hc0p0qej.jpg');background-size:cover;"> <view class="cu-bar"> <view class="content text-bold text-white"> 悦拍屋 </view> </view> </view> <view class="shadow-blur"> <image src="https://raw.githubusercontent.com/weilanwl/ColorUI/master/demo/images/wave.gif" mode="scaleToFill" class="gif-black response" style="height:100rpx;margin-top:-100rpx;"></image> </view> </view> //index.wxss @import "colorui.wxss"; .gif-black { display: block; border: none; mix-blend-mode: screen; } [代码] 本效果需要引入ColorUI组件库
2019-09-26 - 小程序10行代码实现微信头像挂红旗,国庆节个性化头像
最近朋友圈里经常有看到这样的头像 [图片] 既然这么火,大家要图又这么难,作为程序员的自己当然要自己动手实现一个。 老规矩,先看效果图 [图片] 仔细研究了下,发现实现起来并不难,核心代码只有下面10行。 [代码] wx.canvasToTempFilePath({ x: 0, y: 0, width: num, height: num, destWidth: num, destHeight: num, canvasId: 'shareImg', success: function(res) { that.setData({ prurl: res.tempFilePath }) wx.hideLoading() }, fail: function(res) { wx.hideLoading() } }) [代码] 一,首先要创建一个小程序 至于如何创建小程序,我这里就不在细讲了,我也有写过创建小程序的文章,也有路过相关的学习视频,去翻下我历史文章找找就行。 二,创建好小程序后,我们就开始来布局 布局很简单,只有下面几行代码。 [代码]<!-- 画布大小按需定制 这里我按照背景图的尺寸定的 --> <canvas canvas-id="shareImg"></canvas> <!-- 预览区域 --> <view class='preview'> <image src='{{prurl}}' mode='aspectFit'></image> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="1">生成头像1</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="2">生成头像2</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="3">生成头像3</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="4">生成头像4</button> <button type='primary' bindtap='save'>保存分享图</button> </view> [代码] 实现效果图如下 [图片] 三,使用canvas来画图 其实我们实现微信头像挂红旗,原理很简单,就是把头像放在下面,然后把有红旗的相框盖在头像上面 [图片] 下面就直接把核心代码贴给大家 [代码]let promise1 = new Promise(function(resolve, reject) { wx.getImageInfo({ src: "../../images/xiaoshitou.jpg", success: function(res) { console.log("promise1", res) resolve(res); } }) }); let promise2 = new Promise(function(resolve, reject) { wx.getImageInfo({ src: `../../images/head${index}.png`, success: function(res) { console.log(res) resolve(res); } }) }); Promise.all([ promise1, promise2 ]).then(res => { console.log("Promise.all", res) //主要就是计算好各个图文的位置 let num = 1125; ctx.drawImage('../../'+res[0].path, 0, 0, num, num) ctx.drawImage('../../' + res[1].path, 0, 0, num, num) ctx.stroke() ctx.draw(false, () => { wx.canvasToTempFilePath({ x: 0, y: 0, width: num, height: num, destWidth: num, destHeight: num, canvasId: 'shareImg', success: function(res) { that.setData({ prurl: res.tempFilePath }) wx.hideLoading() }, fail: function(res) { wx.hideLoading() } }) }) }) [代码] 来看下画出来的效果图 [图片] 四,头像加红旗画好以后,我们就要想办法把图片保存到本地了 [图片] 保存图片的代码也很简单。 [代码]save: function() { var that = this wx.saveImageToPhotosAlbum({ filePath: that.data.prurl, success(res) { wx.showModal({ content: '图片已保存到相册,赶紧晒一下吧~', showCancel: false, confirmText: '好哒', confirmColor: '#72B9C3', success: function(res) { if (res.confirm) { console.log('用户点击确定'); } } }) } }) } [代码] 来看下保存后的效果图 [图片] 到这里,我的微信头像就成功的加上了小红旗了。 [图片] 源码我也已经给大家准备好了,有需要的同学在文末留言即可。 [图片] 后面我准备录制一门视频课程出来,来详细教大家实现这个功能,敬请关注。
2019-09-26 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 小程序导航栏出现返回首页按钮
目前返回首页按钮出现的条件为(需同时满足): 1. 使用了默认导航栏样式(非 custom) 2. 不是首页或 tabbar 页面(在 app.json 中定义的) 3. 是页面栈最底层页面 如果是开发者自己手写的 tabbar 导致的问题,需要在页面的 onShow 中调用 wx.hideHomeButton() https://developers.weixin.qq.com/miniprogram/dev/api/ui/navigation-bar/wx.hideHomeButton.html手动隐藏返回首页按钮。
2019-09-27 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - 个人小程序应用开发指南
[图片] 作者:HerryLo 原文永久链接: https://github.com/AttemptWeb/Record... 小程序依附于各大平台,支付宝、微信、头条、百度等小程序,都是在平台上进行开发,平台提供对应的技术支持和用户环境。对于个人开发者,这个还是蛮方便的,它本身带有流量属性,易于传播。 便捷开发最近在弄自己的小程序,所以整一篇文章总结一下。如果你已经开发过小程序,这篇文章可以不用看了。 小程序开发相当的便捷,配合官方文档,微信小程序文档、支付宝小程序文档,即可在本地运行起来,如果只是单纯的展示,那只需要开发完之后上传,等待审核了。只需要一点前端基础,就可开发一个自己的小程序,有想法的同学可以行动起来。 如果在开发中出现问题,也可以在小程序社区求助,微信小程序社区。 不是在给官方推广。如果你希望开发属于自己的应用,小程序是一个可以快速入手的方案,便捷开发,快速发布,自带传播功能。下面我选中微信小程序讲解。 微信原生小程序开发微信小程序文档 |—— component 组件目录 |—— pages page页面目录 | ......more dir | |—— index index页面 | | |—— index.js index.js- js逻辑文件 | | |—— index.json index.json- json配置文件 | | |—— index.wxml index.wxml- wxml页面结构 | | |—— index.wxss index.wxss- wxss页面样式 |—— app.js 全局js文件 |—— app.json 全局json配置文件 |—— app.wxss 全局样式文件 |—— project.config.json 项目配置 |—— sitemap.json 爬虫文件 将项目导入到小程序开发工具中,就可以开始开发了。添加页面,直接在pages目录添加新的文件目录。具体细节参考微信小程序文档 语法支持: 目前最新版本库已经 支持Es6 / Es7,async/await 和 Promise;开发工具会将代码转换成Es5 语法,保证各种机型的样式适配;部分语法类似于vue和react; 组件支持: 提供 template 模板支持, 微信小程序 template模版;Component自定义组件支持,组件内部存在生命周期, Component自定义组件; API支持: 可以调用封装过的系统原生方法,比如蓝牙、网络信息、扫码等API方法;常规的方法如:Storage、Reqeuest、Animation、Canvas等,更多可以查看微信小程序 API; 除小程序原生开发外,当然也可以使用框架了,我个人觉得 Taro框架不错,支持多端小程序开发。 Taro开发Taro开发文档,Taro 支持多端开发,包括微信/百度/支付宝/头条小程序、H5、React-Native,目前我只使用开发过微信小程序和支付宝小程序、H5,其他的还没有尝试过。 |—— config 开发配置文件 |—— dist 目标文件目录 |—— src 开发目录 | |—— actions redux action文件 | |—— constants 常量文件 | |—— pages page页面目录 ......more dir | | |—— index | | | |—— index.scss | | | |—— index.txs | |—— reducers redux reducers文件 | |—— store redux store文件 | |—— app.scss app全局样式 | |—— app.tsx app全局js | |—— index.html html文件 |—— .editorconfig editor配置文件 |—— .eslintrc eslint配置文件 |—— global.d.ts global配置 |—— tsconfig.json ts配置文件 |—— package.json |—— project.config.json |—— readme.md 语法支持: ts 语法支持;React语法规范;支持使用 CSS 预编译器;Es6/Es7 语法支持,如果需要更新的语法,可以在配置config配置; 组件支持: react组件式开发,不过最后还是会编译成原生小程序; npm包: 支持使用 npm/yarn 管理;可以使用 Redux 进行状态管理; API: 对小程序API、H5 API进行一次封装,更加方便; react + ts + npm包,基本就是前端开发正常节奏。不论你开发的那个小程序,只需要运行对应的命令,同时,将dist目录文件导入到开发工具中,即可看见效果。 taro和原生小程序原生小程序:上手快,对于接触过前端开发的同学,可以马上上手,基本不存在技术壁垒。面向微信小程序文档编程。 Taro开发:react语法规范,react组件开发,ts语法支持,支持redux。对于react有偏好的同学,强烈推荐。如果你喜欢react,建议使用Taro开发。(易于团队协作)。 小程序开发问题微信小程序 不可以使用cookie微信小程序 非HTTPS的域名不被支持微信小程序 分享API是同步操作,同时回调成功失败被无法监听微信小程序 wx.setStorageSync和wx.getStorageSync的API会频繁报错微信小程序 setData数据的1M限制,通过数据数据拆分可以解决微信小程序 主包的限制2M,导致无法引入过多的外部文件(使用Taro会依赖npm包)微信小程序 微信开发工具占用CPU过高,导致电脑卡顿 当然问题肯定不止这些,还有微信小程序原生组件、API、官方插件等问题,这里不一一细讲了,对于想做自己小程序的同学,这些基本够用。 不过可能会有人问,框架不是还没有讲嘛?其实使用Tora开发小程序,你只是在使用不同的语法,在编写小程序,最后,命令工具都会将文件转换成原生小程序的文件格式。 个人应用不论你使用哪一种方式,开发你的个人应用,最后都会回归到产品本身上。通过技术完成自己心目中的个人应用,将应用提供给用户,这个才是终极目标。(技术只是手段,产品才是目标)不过我个人使用Taro开发,算是尝尝鲜。 废话不多说,希望个人项目源码可以帮助到大家 github: wxSapp 个人小程序源码 [图片] 目前正在整 github: flutter 个人应用 ,有兴趣可以看看。 ps: 顺便推一下自己的个人公众号:Yopai,有兴趣的可以关注,每周不定期更新,分享可以增加世界的快乐 [图片]
2023-06-18 - 小程序上Typescript啦
期待已久的 Typescript 为什么要用 Typescript 关于 Typescript,可以看看以前写过的这篇《关于Typescript》。文末的故事,便是大多数情况下 Typescript 能帮我们解决的痛点。 过了很久之后,想法还是一样:Typescript 这事情,当你管理大点的应用的时候,就会感受到它的好处了。尤其涉及团队配合的时候! 当然,如果你的项目比较小,或是写个小公(工)举(具)、小 demo 的时候, store 状态管理、typescript 编译这些,除非已经很熟悉、没有额外成本的时候,才勉强适合接入。离了具体场景谈架构,都是耍(xia)流(che)氓(dan)。 为什么要用 Typescript? 变量类型不明确 之前带外包写小程序,除了代码风格不一致之外,还遇到一个会变的变量问题。 [代码]let formGroups = this.currentStep.formGroups; // 猜猜我的 formGroups 是数组数组 [[], [], []],还是对象数组 [{}, {}, {}]? let flattenFields = _.flatten(formGroups); // 不用猜了,我用个 flatten 抹平,它就一定是对象 [{}, {}, {}] 了! flattenFields.forEach(item => { if (item.fields) { // 猜猜我的 item.fields 是数组还是对象? flattenFields.push(..._.values(item.fields)); // 不用猜了,我用个 values 抹平,它就一定是对象了! } }); [代码] 当我帮忙 debug 个问题的时候,打断点看到: [图片] [图片] 喵喵喵??? [代码]# 我和外包童鞋的对话: 我:话说你这些到底是什么类型,从命名和上下文都看不出来。。 我:得去翻更细的代码。。 外包童鞋:values好像可以改一下试试 我:是数组还是对象? 外包童鞋:有的是数组,有的是对象 外包童鞋:一般带复数的是数组 我:(刀.jpg) 我:卧槽 我:你这item.fields,有时候是数组,有时候是对象,这样真的好吗 我:大哥 我:(刀.jpg)* 2 [代码] 接口协议不符合 [代码]前端:帮忙看看这个接口为什么返回失败了? 后台:你这个接口字段少了啊,这个xxx (哼哧哼哧修改) 前端:帮忙看看这个接口为啥又报错了啊? 后台:你这个字段类型不对...我协议里有写的 前端:喔不好意思我改 (哼哧哼哧修改) 前端:(泪光)帮忙看看这个接口为啥还报错? 后台:...你这字段名拼错了啊!!!! [代码] 当然,这个案例里稍微夸张了一点,一般我们都会自己一个个对着协议检查哪里不对,但是很多时候被 bug 光环环绕的时候,你就是发现不了问题。 这个时候,我们就可以用 Typescript 来管理接口啦。 [代码]interface IDemoResponse { date: string; someNumber: number; otherThing: any; } [代码] 1. 使用约定的变量的时候,会有相关提示(请忽略我的强行any)。 [图片] 2. 使用约定以外的属性时候,会报错提示。 [图片] 除此以外,还有很多很棒的用法呢~ 一键调整协议 前端和后台协议约定后,就开始各自开发了。但是,我们总会遇到各种各样的问题,可能导致我们的协议变更。 字段的变更什么的最讨厌了,例如后台要把某个接口下[代码]date[代码]改成[代码]day[代码]。一般来说前端是拒绝的,你不能说让我改我就得改,我得看看我写了多少代码,评估下工作量。 什么,全局替换?你知道使用[代码]date[代码]多普遍吗?万一我替换错了咋办?? 这时候,如果你使用了 Typescript 并定义了协议接口的话,就很好办了~ 依然是这段代码: [代码]interface IDemoResponse { date: string; someNumber: number; otherThing: any; } const demoResponse: IDemoResponse = {} as any; const date = demoResponse.date; [代码] 1. 选中需要重命名的属性。 [图片] 2. 按下F2,重新输入属性名。 [图片] 3. 按下回车,使用到的地方都会更新。 [图片] 是不是很酷~~~ 跨过 Babel 直接使用 ES6/ES7,跨过 eslint 直接使用 prettier 其实小程序工具本身也支持了不少的 ES6 新语法,不过像[代码]async/await[代码]这种,则还是需要自己搞个 Babel 来编译。 现在直接上 Typescript,连 Babel 都可以直接跳过啦。 Prettier 这里重点推荐 prettier 神器,也是团队配合的好工具啊: 项目代码没有配 eslint?导致每次拉下来的代码一大堆冲突? 团队成员使用不同的编辑器?有的没有自动格式化?导致拉下来代码还是一堆冲突? 用 standard?有些规范和实际项目不符合,但是偏偏没得改?? 偷偷地往项目里装个 Prettier,然后所有的矛盾都不见啦。不管你的代码格式多独特,最终在 Git commit 的时候,就被同化啦,而且 Prettier 的格式化也不会影响到 Git 记录。 小程序与 Typescript Typescript 编译下就可以用? 其实小程序它最终运行的还是 Javascript,那不是我们直接自己编译下就好了吗? 少年你太天真了。咱们写 Typescript 最重要的是什么呀?是 Typing 库呀! [图片] 网上开源的关于小程序和 Typescript 的工具或者脚手架也一大堆,为啥不用呢?因为小程序的 API 在不断地变化呀~ 有了官方的支持,即使小程序的 API 变了,我们也可以及时地更新呀(奸笑)~ 开箱即用的尝鲜 既然官方提供支持了,义不容辞地使用呀! 1. 首先,我们更新到最近的工具版本,然后创建项目就能看到了: [图片] 2. 创建模版,我们来看看代码长什么样子。 [图片] 我们可以看到,在 package.json 里面多了俩脚本,其实也就是将 ts 文件原地编译,然后上传代码的时候忽略掉了。 3. 仔细瞧瞧代码。 [图片] 额,好像混入了一些奇怪的东西进去,感叹号是什么鬼??? 后面问了下开发GG,是因为这里比较特殊,目前定义的文件暂时没法兼顾,等后面的版本会兼容。 [图片] 终于用上 Typescript 啦,爽歪歪~ 调整下代码结构 小项目的话,其实也不用带什么编译啦。不过如果你还想用 less,也想用 typescript,还不想看到项目下面乱糟糟的文件: [代码]index.js index.ts index.json index.less index.wxss index.wxml [代码] 我们就简单弄个 gulp,把编译加上吧~ [图片] 然后我们再把 prettier 愉快地加上。这里就不多讲解啦,大家也可以参考我的 demo 项目: wxapp-typescript-demo 对了,目前官方的 typing 库也不是非常完善,如果需要写组件、插件、小游戏的你,可能会面临一大堆的 any 冲击波噢~ [图片] 参考 小程序工具更新 结束语 Typescript 的普及度其实不算高,小程序的确是又一次给到惊喜。反观下我们自己呢?有没有被业务代码冲得找不到方向呢? 很多时候,我们总爱说写业务没啥技术提升,但真的是这样吗?我看过很棒的业务代码,从框架设计到具体的实现,开发者都对自己做了很高的要求。而写技术需求代码的,就一定会写得很好吗? “我们是业务部门,技术肯定不能比” “随便找一些能用的就好了,不要浪费时间在这些上面” “能用就行了,不要在意这些细节” … 写代码是个思考的过程,要对自己有点追求。当然项目急的时候可以理解,事后一定要把欠下的债务给还了。(较真脸)
2019-02-20 - 【周刊-2】三年大厂面试官-前端面试题(偏难)
前言 在阿里和腾讯工作了6年,当了3年的前端面试官,把阿里和腾讯常问的面试题与答案汇总在我的Github中。希望对大家有所帮助,助力大家进入自己理想的企业。 项目地址是:https://github.com/airuikun/Weekly-FE-Interview 如果你在阿里和腾讯面试的时候遇到了什么不懂的问题,欢迎给我提issue,我会把答案和考点都列出来,公布在下一期的面试周刊里。 面试题精选 大家如果去阿里和腾讯面试过,就会发现,在网上刷了很多的前端面试题,但是去大厂面试的时候还是一头雾水,那是因为那些在网上一搜就能搜出来的题,大厂的面试官基本看不上,他们都会问一些开放题,在回答开放题的过程中,就能摸清你知识技能的广度和深度,所以本期会加入几道我在面试候选人常用的开放题,供大家学习和思考。 我把下面每道题的难度高低,和对标了阿里和腾讯的多少职级,都写上去了,大家可以参考一下自己是什么职级。 第 1 题:如何劫持https的请求,提供思路 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 很多人在google上搜索“前端面试 + https详解”,把答案倒背如流,但是问到如何劫持https请求的时候就一脸懵逼,是因为还是停留在https理论性阶段。 想告诉大家的是,就算是https,也不是绝对的安全,以下提供一个本地劫持https请求的简单思路。 模拟中间人攻击,以百度为例 先用OpenSSL查看下证书,直接调用openssl库识别目标服务器支持的SSL/TLS cipher suite [代码] openssl s_client -connect www.baidu.com:443 [代码] 用sslcan识别ssl配置错误,过期协议,过时cipher suite和hash算法 [代码] sslscan -tlsall www.baidu.com:443 [代码] 分析证书详细数据 [代码] sslscan -show-certificate --no-ciphersuites www.baidu.com:443 [代码] 生成一个证书 [代码] openssl req -new -x509 -days 1096 -key ca.key -out ca.crt [代码] 开启路由功能 [代码] sysctl -w net.ipv4.ip_forward=1 [代码] 写转发规则,将80、443端口进行转发给8080和8443端口 [代码] iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443 [代码] 最后使用arpspoof进行arp欺骗 如果你有更好的想法或疑问,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/11 第 2 题:前端如何进行seo优化 难度:阿里p5、腾讯t21 合理的title、description、keywords:搜索对着三项的权重逐个减小,title值强调重点即可;description把页面内容高度概括,不可过分堆砌关键词;keywords列举出重要关键词。 语义化的HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页 重要内容HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,保证重要内容一定会被抓取 重要内容不要用js输出:爬虫不会执行js获取内容 少用iframe:搜索引擎不会抓取iframe中的内容 非装饰性图片必须加alt 提高网站速度:网站速度是搜索引擎排序的一个重要指标 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/12 第 3 题:前后端分离的项目如何seo 难度:阿里p6 ~ p6+、腾讯t22 ~ t23 使用prerender。但是回答prerender,面试官肯定会问你,如果不用prerender,让你直接去实现,好的,请看下面的第二个答案。 先去 https://www.baidu.com/robots.txt 找出常见的爬虫,然后在nginx上判断来访问页面用户的User-Agent是否是爬虫,如果是爬虫,就用nginx方向代理到我们自己用nodejs + puppeteer实现的爬虫服务器上,然后用你的爬虫服务器爬自己的前后端分离的前端项目页面,增加扒页面的接收延时,保证异步渲染的接口数据返回,最后得到了页面的数据,返还给来访问的爬虫即可。 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/13 第 4 题:简单实现async/await中的async函数 难度:阿里p6 ~ p6+、腾讯t22 ~ t23 async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里 [代码]function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch (e) { return reject(e); } if (next.done) { return resolve(next.value); } Promise.resolve(next.value).then( function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); } ); } step(function() { return gen.next(undefined); }); }); } [代码] 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/14 第 5 题:1000-div问题 难度:阿里p5 ~ p6、腾讯t21 ~ t22 一次性插入1000个div,如何优化插入的性能 使用Fragment [代码] var fragment = document.createDocumentFragment(); fragment.appendChild(elem); [代码] 向1000个并排的div元素中,插入一个平级的div元素,如何优化插入的性能 先display:none 然后插入 再display:block 赋予key,然后使用virtual-dom,先render,然后diff,最后patch 脱离文档流,用GPU去渲染,开启硬件加速 如果你有更好的答案或想法,欢迎在这题目对应的github下留言:https://github.com/airuikun/Weekly-FE-Interview/issues/15 第 6 题:(开放题)2万小球问题:在浏览器端,用js存储2万个小球的信息,包含小球的大小,位置,颜色等,如何做到对这2万条小球信息进行最优检索和存储 难度:阿里p7、腾讯t31 你面试阿里和腾讯,能否上p7和t31,就看你对开放题能答有多深和多广。 这题目考察你如何在浏览器端中进行大数据的存储优化和检索优化。 如果你仅仅只是答用数组对象存储了2万个小球信息,然后用for循环去遍历进行索引,那是远远不够的。 这题要往深一点走,用特殊的数据结构和算法进行存储和索引。 然后进行存储和速度的一个权衡和对比,最终给出你认为的最优解。 我提供几个能触及阿里p7和腾讯t31级别的思路: 用ArrayBuffer实现极致存储 哈夫曼编码 + 字典查询树实现更优索引 用bit-map实现大数据筛查 用hash索引实现简单快捷的检索 用IndexedDB实现动态存储扩充浏览器端虚拟容量 用iframe的漏洞实现浏览器端localStorage无限存储,实现2千万小球信息存储 这种开放题答案不唯一,也不会要你现场手敲代码去实现,但是思路一定要行得通,并且是能打动面试官的思路,如果大家有更好的idea,欢迎大家到我的github里补充:https://github.com/airuikun/Weekly-FE-Interview/issues/16 第 7 题:(开放题)接上一题如何尽可能流畅的实现这2万小球在浏览器中,以直线运动的动效显示出来 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 这题考察对大数据的动画显示优化,当然方法有很多种。 但是你有没有用到浏览器的高级api? 你还有没有用到浏览器的专门针对动画的引擎? 或者你对3D的实践和优化,都可以给面试官展示出来。 提供几个思路: 使用GPU硬件加速 使用webGL 使用assembly辅助计算,然后在浏览器端控制动画帧频 用web worker实现javascript多线程,分块处理小球 用单链表树算法和携程机制,实现任务动态分割和任务暂停、恢复、回滚,动态渲染和处理小球 如果大家有更好的idea,欢迎大家到我的github里补充:https://github.com/airuikun/Weekly-FE-Interview/issues/17 第 8 题:(开放题)100亿排序问题:内存不足,一次只允许你装载和操作1亿条数据,如何对100亿条数据进行排序。 难度:阿里p6+ ~ p7、腾讯t23 ~ t31 这题是考察算法和实际问题结合的一个问题 众所周知,腾讯玩的是社交,用户量极大。很多场景的数据量都是百亿甚至千亿级别。 那么如何对这些数据进行高效的操作呢,可以通过这题考察出来。 以前老听说很多人问,前端学算法没有用,考算法都是垃圾,面不出候选人的能力 其实。。。老哥实话告诉你,当你在做前端需要用到crc32、并查集、字典树、哈夫曼编码、LZ77之类东西的时候 已经是涉及到框架实现和极致优化层面了 那时你就已经到了另外一个前端高阶境界了 所以不要抵触算法,可能只是我们目前的眼界和能力,还没触及到那个层级 我前面已经公布了两道开放题的答案了,相信大家已经有所参悟。我觉得在思考开放题的过程中,会有很多意想不到的成长,所以我建议这道题大家可以尝试自己思考一下。本题答案会在周五公布到我的github上。 对应的github地址为:https://github.com/airuikun/Weekly-FE-Interview/issues/18 第 9 题:(开放题)a.b.c.d和a[‘b’][‘c’][‘d’],哪个性能更高 难度:阿里p7 ~ p7+、腾讯t31 ~ t32 别看这题,题目上每个字都能看懂,但是里面涉及到的知识,暗藏杀鸡 这题要往深处走,会涉及ast抽象语法树、编译原理、v8内核对原生js实现问题 直接对标阿里p7 ~ p7+和腾讯t31 ~ t32职级,我觉得这个题是这篇文章里最难的一道题,所以我放在了开放题中的最后一题 大家多多思考,本题答案会在周五公布到我的github上 对应的github地址为:https://github.com/airuikun/Weekly-FE-Interview/issues/19 第 10 题:git时光机问题 难度:阿里p5 ~ p6+、腾讯t21 ~ t23 现在大厂,已经全部都是用git了,基本没人使用svn了 很多面试候选人对git只会commit、pull、push 但是有没有使用过reflog、cherry-pick等等,这些都很能体现出来你对代码管理的灵活程度和代码质量管理。 针对git时光机经典问题,我专门写了一个文章,轻松搞笑通俗易懂,大家可以看一下,放松放松,同时也能学到对git的时光机操作《git时光机》 结语 本人还写了一些前端进阶知识的文章,如果觉得不错可以点个star。 blog项目地址是:https://github.com/airuikun/blog 我是小蝌蚪,腾讯高级前端工程师,跟着我一起每周攻克几个前端技术难点。希望在小伙伴前端进阶的路上有所帮助,助力大家进入自己理想的企业。 交流 欢迎关注我的微信公众号,微信扫下面二维码或搜索“前端屌丝”,讲述了一个前端屌丝逆袭的心路历程,共勉。 [图片]
2019-04-17 - 一个通用request的封装
小程序内置了[代码]wx.request[代码],用于向后端发送请求,我们先来看看它的文档: wx.request(OBJECT) 发起网络请求。使用前请先阅读说明。 OBJECT参数说明: 参数名 类型 必填 默认值 说明 最低版本 url String 是 - 开发者服务器接口地址 - data Object/String/ArrayBuffer 否 - 请求的参数 - header Object 否 - 设置请求的 header,header 中不能设置 Referer。 - method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT - dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse - responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer 1.7.0 success Function 否 - 收到开发者服务成功返回的回调函数 - fail Function 否 - 接口调用失败的回调函数 - complete Function 否 - 接口调用结束的回调函数(调用成功、失败都会执行) - success返回参数说明: 参数 类型 说明 最低版本 data Object/String/ArrayBuffer 开发者服务器返回的数据 - statusCode Number 开发者服务器返回的 HTTP 状态码 - header Object 开发者服务器返回的 HTTP Response Header 1.2.0 这里我们主要看两点: 回调函数:success、fail、complete; success的返回参数:data、statusCode、header。 相对于通过入参传回调函数的方式,我更喜欢promise的链式,这是我期望的第一个点;success的返回参数,在实际开发过程中,我只关心data部分,这里可以做一下处理,这是第二点。 promisify 小程序默认支持promise,所以这一点改造还是很简单的: [代码]/** * promise请求 * 参数:参考wx.request * 返回值:[promise]res */ function requestP(options = {}) { const { success, fail, } = options; return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success: res, fail: rej, }, )); }); } [代码] 这样一来我们就可以使用这个函数来代替wx.request,并且愉快地使用promise链式: [代码]requestP({ url: '/api', data: { name: 'Jack' } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 注意,小程序的promise并没有实现finally,Promise.prototype.finally是undefined,所以complete不能用finally代替。 精简返回值 精简返回值也是很简单的事情,第一直觉是,当请求返回并丢给我一大堆数据时,我直接resolve我要的那一部分数据就好了嘛: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { res(r.data); // 这里只取data }, fail: rej, }, )); }); [代码] but!这里需要注意,我们仅仅取data部分,这时候默认所有success都是成功的,其实不然,wx.request是一个基础的api,fail只发生在系统和网络层面的失败情况,比如网络丢包、域名解析失败等等,而类似404、500之类的接口状态,依旧是调用success,并体现在[代码]statusCode[代码]上。 从业务上讲,我只想处理json的内容,并对json当中的相关状态进行处理;如果一个接口返回的不是约定好的json,而是类似404、500之类的接口异常,我统一当成接口/网络错误来处理,就像jquery的ajax那样。 也就是说,如果我不对[代码]statusCode[代码]进行区分,那么包括404、500在内的所有请求结果都会走[代码]requestP().then[代码],而不是[代码]requestP().catch[代码]。这显然不是我们熟悉的使用方式。 于是我从jquery的ajax那里抄来了一段代码。。。 [代码]/** * 判断请求状态是否成功 * 参数:http状态码 * 返回值:[Boolen] */ function isHttpSuccess(status) { return status >= 200 && status < 300 || status === 304; } [代码] [代码]isHttpSuccess[代码]用来决定一个http状态码是否判为成功,于是结合[代码]requestP[代码],我们可以这么来用: [代码]return new Promise((res, rej) => { wx.request(Object.assign( {}, options, { success(r) { const isSuccess = isHttpSuccess(r.statusCode); if (isSuccess) { // 成功的请求状态 res(r.data); } else { rej({ msg: `网络错误:${r.statusCode}`, detail: r }); } }, fail: rej, }, )); }); [代码] 这样我们就可以直接resolve返回结果中的data,而对于非成功的http状态码,我们则直接reject一个自定义的error对象,这样就是我们所熟悉的ajax用法了。 登录 我们经常需要识别发起请求的当前用户,在web中这通常是通过请求中携带的cookie实现的,而且对于前端开发者是无感知的;小程序中没有cookie,所以需要主动地去补充相关信息。 首先要做的是:登录。 通过[代码]wx.login[代码]接口我们可以得到一个[代码]code[代码],调用后端登录接口将code传给后端,后端再用code去调用微信的登录接口,换取[代码]sessionKey[代码],最后生成一个[代码]sessionId[代码]返回给前端,这就完成了登录。 [图片] 具体参考微信官方文档:wx.login [代码]const apiUrl = 'https://jack-lo.github.io'; let sessionId = ''; /** * 登录 * 参数:undefined * 返回值:[promise]res */ function login() { return new Promise((res, rej) => { // 微信登录 wx.login({ success(r1) { if (r1.code) { // 获取sessionId requestP({ url: `${apiUrl}/api/login`, data: { code: r1.code, }, method: 'POST' }) .then((r2) => { if (r2.rcode === 0) { const { sessionId } = r2.data; // 保存sessionId sessionId = sessionId; res(r2); } else { rej({ msg: '获取sessionId失败', detail: r2 }); } }) .catch((err) => { rej(err); }); } else { rej({ msg: '获取code失败', detail: r1 }); } }, fail: rej, }); }); } [代码] 好的,我们做好了登录并且顺利获取到了sessionId,接下来是考虑怎么把sessionId通过请求带上去。 sessionId 为了将状态与数据区分开来,我们决定不通过data,而是通过header的方式来携带sessionId,我们对原本的requestP稍稍进行修改,使得它每次调用都自动在header里携带sessionId: [代码]function requestP(options = {}) { const { success, fail, } = options; // 统一注入约定的header let header = Object.assign({ sessionId: sessionId }, options.header); return new Promise((res, rej) => { ... }); } [代码] 好的,现在请求会自动带上sessionId了; 但是,革命尚未完成: 我们什么时候去登录呢?或者说,我们什么时候去获取sessionId? 假如还没登录就发起请求了怎么办呢? 登录过期了怎么办呢? 我设想有这样一个逻辑: 当我发起一个请求的时候,如果这个请求不需要sessionId,则直接发出; 如果这个请求需要携带sessionId,就去检查现在是否有sessionId,有的话直接携带,发起请求; 如果没有,自动去走登录的流程,登录成功,拿到sessionId,再去发送这个请求; 如果有,但是最后请求返回结果是sessionId过期了,那么程序自动走登录的流程,然后再发起一遍。 其实上面的那么多逻辑,中心思想只有一个:都是为了拿到sessionId! 我们需要对请求做一层更高级的封装。 首先我们需要一个函数专门去获取sessionId,它将解决上面提到的2、3点: [代码]/** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { login() .then((r1) => { res(r1.data.sessionId); }) .catch(rej); } } else { res(sessionId); } }); } [代码] 好的,接下来我们解决第1、4点,我们先假定:sessionId过期的时候,接口会返回[代码]code=401[代码]。 整合了getSessionId,得到一个更高级的request方法: [代码]/** * ajax高级封装 * 参数:[Object]option = {},参考wx.request; * [Boolen]keepLogin = false * 返回值:[promise]res */ function request(options = {}, keepLogin = true) { if (keepLogin) { return new Promise((res, rej) => { getSessionId() .then((r1) => { // 获取sessionId成功之后,发起请求 requestP(options) .then((r2) => { if (r2.rcode === 401) { // 登录状态无效,则重新走一遍登录流程 // 销毁本地已失效的sessionId sessionId = ''; getSessionId() .then((r3) => { requestP(options) .then(res) .catch(rej); }); } else { res(r2); } }) .catch(rej); }) .catch(rej); }); } else { // 不需要sessionId,直接发起请求 return requestP(options); } } [代码] 留意req的第二参数keepLogin,是为了适配有些接口不需要sessionId,但因为我的业务里大部分接口都需要登录状态,所以我默认值为true。 这差不多就是我们封装request的最终形态了。 并发处理 这里其实我们还需要考虑一个问题,那就是并发。 试想一下,当我们的小程序刚打开的时候,假设页面会同时发出5个请求,而此时没有sessionId,那么,这5个请求按照上面的逻辑,都会先去调用login去登录,于是乎,我们就会发现,登录接口被同步调用了5次!并且后面的调用将导致前面的登录返回的sessionId过期~ 这bug是很严重的,理论上来说,登录我们只需要调用一次,然后一直到过期为止,我们都不需要再去登录一遍了。 ——那么也就是说,同一时间里的所有接口其实只需要登录一次就可以了。 ——也就是说,当有登录的请求发出的时候,其他那些也需要登录状态的接口,不需要再去走登录的流程,而是等待这次登录回来即可,他们共享一次登录操作就可以了! 解决这个问题,我们需要用到队列。 我们修改一下getSessionId这里的逻辑: [代码]const loginQueue = []; let isLoginning = false; /** * 获取sessionId * 参数:undefined * 返回值:[promise]sessionId */ function getSessionId() { return new Promise((res, rej) => { // 本地sessionId缺失,重新登录 if (!sessionId) { loginQueue.push({ res, rej }); if (!isLoginning) { isLoginning = true; login() .then((r1) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().res(r1); } }) .catch((err) => { isLoginning = false; while (loginQueue.length) { loginQueue.shift().rej(err); } }); } } else { res(sessionId); } }); } [代码] 使用了isLoginning这个变量来充当锁的角色,锁的目的就是当登录正在进行中的时候,告诉程序“我已经在登录了,你先把回调都加队列里去吧”,当登录结束之后,回来将锁解开,把回调全部执行并清空队列。 这样我们就解决了问题,同时提高了性能。 封装 在做完以上工作以后,我们都很清楚的封装结果就是[代码]request[代码],所以我们把request暴露出去就好了: [代码]function request() { ... } module.exports = request; [代码] 这般如此之后,我们使用起来就可以这样子: [代码]const request = require('request.js'); Page({ ready() { // 获取热门列表 request({ url: 'https://jack-lo.github.io/api/hotList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 request({ url: 'https://jack-lo.github.io/api/latestList', data: { page: 1 } }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); }, }); [代码] 是不是很方便,可以用promise的方式,又不必关心登录的问题。 然而可达鸭眉头一皱,发现事情并不简单,一个接口有可能在多个地方被多次调用,每次我们都去手写这么一串[代码]url[代码]参数,并不那么方便,有时候还不好找,并且容易出错。 如果能有个地方专门记录这些url就好了;如果每次调用接口,都能像调用一个函数那么简单就好了。 基于这个想法,我们还可以再做一层封装,我们可以把所有的后端接口,都封装成一个方法,调用接口就相对应调用这个方法: [代码]const apiUrl = 'https://jack-lo.github.io'; const req = { // 获取热门列表 getHotList(data) { const url = `${apiUrl}/api/hotList` return request({ url, data }); }, // 获取最新列表 getLatestList(data) { const url = `${apiUrl}/api/latestList` return request({ url, data }); } } module.exports = req; // 注意这里暴露的已经不是request,而是req [代码] 那么我们的调用方式就变成了: [代码]const req = require('request.js'); Page({ ready() { // 获取热门列表 req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); // 获取最新信息 req.getLatestList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); } }); [代码] 这样一来就方便了很多,而且有一个很大的好处,那就是当某个接口的地址需要统一修改的时候,我们只需要对[代码]request.js[代码]进行修改,其他调用的地方都不需要动了。 错误信息的提炼 最后的最后,我们再补充一个可轻可重的点,那就是错误信息的提炼。 当我们在封装这么一个[代码]req[代码]对象的时候,我们的promise曾经reject过很多的错误信息,这些错误信息有可能来自: [代码]wx.request[代码]的fail; 不符合[代码]isHttpSuccess[代码]的网络错误; getSessionId失败; … 等等的一切可能。 这就导致了我们在提炼错误信息的时候陷入困境,到底catch到的会是哪种[代码]error[代码]对象? 这么看你可能不觉得有问题,我们来看看下面的例子: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { console.log(err); }); [代码] 假如上面的例子中,我想要的不仅仅是[代码]console.log(err)[代码],而是想将对应的错误信息弹窗出来,我应该怎么做? 我们只能将所有可能出现的错误都检查一遍: [代码]req.getHotList({ page: 1 }) .then((res) => { if (res.code !== 0) { // 后端接口报错格式 wx.showModal({ content: res.msg }); } }) .catch((err) => { let msg = '未知错误'; // 文本信息直接使用 if (typeof err === 'string') { msg = err; } // 小程序接口报错 if (err.errMsg) { msg = err.errMsg; } // 自定义接口的报错,比如网络错误 if (err.detail && err.detail.errMsg) { msg = err.detail.errMsg; } // 未知错误 wx.showModal({ content: msg }); }); [代码] 这就有点尴尬了,提炼错误信息的代码量都比业务还多几倍,而且还是每个接口调用都要写一遍~ 为了解决这个问题,我们需要封装一个方法来专门做提炼的工作: [代码]/** * 提炼错误信息 * 参数:err * 返回值:[string]errMsg */ function errPicker(err) { if (typeof err === 'string') { return err; } return err.msg || err.errMsg || (err.detail && err.detail.errMsg) || '未知错误'; } [代码] 那么过程会变成: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch((err) => { const msg = req.errPicker(err); // 未知错误 wx.showModal({ content: msg }); }); [代码] 好吧,我们再偷懒一下,把wx.showModal也省去了: [代码]/** * 错误弹窗 */ function showErr(err) { const msg = errPicker(err); console.log(err); wx.showModal({ showCancel: false, content: msg }); } [代码] 最后就变成了: [代码]req.getHotList({ page: 1 }) .then((res) => { console.log(res); }) .catch(req.showErr); [代码] 至此,一个简单的wx.request封装过程便完成了,封装过的[代码]req[代码]比起原来,使用上更加方便,扩展性和可维护性也更好。 结尾 以上内容其实是简化版的[代码]mp-req[代码],介绍了[代码]mp-req[代码]这一工具的实现初衷以及思路,使用[代码]mp-req[代码]来管理接口会更加的便捷,同时[代码]mp-req[代码]也提供了更加丰富的功能,比如插件机制、接口的缓存,以及接口分类等,欢迎大家关注mp-req了解更多内容。 以上最终代码可以在这里获取:req.js。
2020-08-04 - CSS3 Animation动画的十二原则
作为前端的设计师和工程师,我们用 CSS 去做样式、定位并创建出好看的网站。我们经常用 CSS 去添加页面的运动过渡效果甚至动画,但我们经常做的不过如此。 [代码] 动效是一个有助于访客和用户理解我们设计的强有力工具。这里有些原则能最大限度地应用在我们的工作中。 迪士尼经过基础工作练习的长时间累积,在 1981 年出版的 The Illusion of Life: Disney Animation 一书中发表了动画的十二个原则 ([] (https://en.wikipedia.org/wiki/12_basic_principles_of_animation)) 。这些原则描述了动画能怎样用于让观众相信自己沉浸在现实世界中。 [代码] 在本文中,我会逐个介绍这十二个原则,并讨论它们怎样运用在网页中。你能在 Codepen 找到它们[] (https://codepen.io/collection/AxKOdY/)。 挤压和拉伸 (Squash and stretch) [图片] 这是物体存在质量且运动时质量保持不变的概念。当一个球在弹跳时,碰击到地面会变扁,恢复的时间会越来越短。 [代码] 创建对象的时候最有用的方法是参照实物,比如人、时钟和弹性球。 当它和网页元件一起工作时可能会忽略这个原则。DOM 对象不一定和实物相关,它会按需要在屏幕上缩放。例如,一个按钮会变大并变成一个信息框,或者错误信息会出现和消失。 尽管如此,挤压和伸缩效果可以为一个对象增加实物的感觉。甚至一些形状上的小变化就可以创造出细微但抢眼的效果。 HTML [代码] [代码] <h1>Principle 1: Squash and stretch</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle one"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].one .shape { animation: one 4s infinite ease-out; } .one .surface { background: #000; height: 10em; width: 1em; position: absolute; top: calc(50% - 4em); left: calc(50% + 10em); } @keyframes one { 0%, 15% { opacity: 0; } 15%, 25% { transform: none; animation-timing-function: cubic-bezier(1,-1.92,.95,.89); width: 4em; height: 4em; top: calc(50% - 2em); left: calc(50% - 2em); opacity: 1; } 35%, 45% { transform: translateX(8em); height: 6em; width: 2em; top: calc(50% - 3em); animation-timing-function: linear; opacity: 1; } 70%, 100% { transform: translateX(8em) translateY(5em); height: 6em; width: 2em; top: calc(50% - 3em); opacity: 0; } } body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 预备动作 (Anticipation) [图片] 运动不倾向于突然发生。在现实生活中,无论是一个球在掉到桌子前就开始滚动,或是一个人屈膝准备起跳,运动通常有着某种事先的累积。 [代码] 我们能用它去让我们的过渡动画显得更逼真。预备动作可以是一个细微的反弹,帮人们理解什么对象将在屏幕中发生变化并留下痕迹。 例如,悬停在一个元件上时可以在它变大前稍微缩小,在初始列表中添加额外的条目来介绍其它条目的移除方法。 [代码] HTML [代码]<h1>Principle 2: Anticipation</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle two"> <div class="shape"></div> <div class="surface"></div> </article> [代码] CSS [代码].two .shape { animation: two 5s infinite ease-out; transform-origin: 50% 7em; } .two .surface { background: #000; width: 8em; height: 1em; position: absolute; top: calc(50% + 4em); left: calc(50% - 3em); } @keyframes two { 0%, 15% { opacity: 0; transform: none; } 15%, 25% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5,.05,.91,.47); } 28%, 38% { transform: translateX(-2em); } 40%, 45% { transform: translateX(-4em); } 50%, 52% { transform: translateX(-4em) rotateZ(-20deg); } 70%, 75% { transform: translateX(-4em) rotateZ(-10deg); } 78% { transform: translateX(-4em) rotateZ(-24deg); opacity: 1; } 86%, 100% { transform: translateX(-6em) translateY(4em) rotateZ(-90deg); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 演出布局 (Staging) [图片] 演出布局是确保对象在场景中得以聚焦,让场景中的其它对象和视觉在主动画发生的地方让位。这意味着要么把主动画放到突出的位置,要么模糊其它元件来让用户专注于看他们需要看的东西。 [代码] 在网页方面,一种方法是用 model 覆盖在某些内容上。在现有页面添加一个遮罩并把那些主要关注的内容前置展示。 另一种方法是用动作。当很多对象在运动,你很难知道哪些值得关注。如果其它所有的动作停止,只留一个在运动,即使动得很微弱,这都可以让对象更容易被察觉。 [代码] 还有一种方法是做一个晃动和闪烁的按钮来简单地建议用户比如他们可能要保存文档。屏幕保持静态,所以再细微的动作也会突显出来。 HTML [代码]<h1>Principle 3: Staging</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle three"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].three .shape.a { transform: translateX(-12em); } .three .shape.c { transform: translateX(12em); } .three .shape.b { animation: three 5s infinite ease-out; transform-origin: 0 6em; } .three .shape.a, .three .shape.c { animation: threeb 5s infinite linear; } @keyframes three { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 26%, 30% { transform: rotateZ(-40deg); } 32.5% { transform: rotateZ(-38deg); } 35% { transform: rotateZ(-42deg); } 37.5% { transform: rotateZ(-38deg); } 40% { transform: rotateZ(-40deg); } 42.5% { transform: rotateZ(-38deg); } 45% { transform: rotateZ(-42deg); } 47.5% { transform: rotateZ(-38deg); animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 58%, 100% { transform: none; } } @keyframes threeb { 0%, 20% { filter: none; } 40%, 50% { filter: blur(5px); } 65%, 100% { filter: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 连续运动和姿态对应 (Straight-Ahead Action and Pose-to-Pose) [图片] 连续运动是绘制动画的每一帧,姿态对应是通常由一个 assistant 在定义一系列关键帧后填充间隔。 [代码] 大多数网页动画用的是姿态对应:关键帧之间的过渡可以通过浏览器在每个关键帧之间的插入尽可能多的帧使动画流畅。 [代码] 有一个例外是定时功能step。通过这个功能,浏览器 “steps” 可以把尽可能多的无序帧串清晰。你可以用这种方式绘制一系列图片并让浏览器按顺序显示出来,这开创了一种逐帧动画的风格。 HTML [代码]<h1>Principle 4: Straight Ahead Action and Pose to Pose</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle four"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].four .shape.a { left: calc(50% - 8em); animation: four 6s infinite cubic-bezier(.57,-0.5,.43,1.53); } .four .shape.b { left: calc(50% + 8em); animation: four 6s infinite steps(1); } @keyframes four { 0%, 10% { transform: none; } 26%, 30% { transform: rotateZ(-45deg) scale(1.25); } 40% { transform: rotateZ(-45deg) translate(2em, -2em) scale(1.8); } 50%, 75% { transform: rotateZ(-45deg) scale(1.1); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 跟随和重叠动作 (Follow Through and Overlapping Action) [图片] 事情并不总在同一时间发生。当一辆车从急刹到停下,车子会向前倾、有烟从轮胎冒出来、车里的司机继续向前冲。 [代码] 这些细节是跟随和重叠动作的例子。它们在网页中能被用作帮助强调什么东西被停止,并不会被遗忘。例如一个条目可能在滑动时稍滑微远了些,但它自己会纠正到正确位置。 要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗 (View) 过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。 [代码] 在网页方面,这可能意味着让过渡或动画的效果以不同速度来运行。 HTML [代码]<h1>Principle 5: Follow Through and Overlapping Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle five"> <div class="shape-container"> <div class="shape"></div> </div> </article> [代码] CSS [代码].five .shape { animation: five 4s infinite cubic-bezier(.64,-0.36,.1,1); position: relative; left: auto; top: auto; } .five .shape-container { animation: five-container 4s infinite cubic-bezier(.64,-0.36,.1,2); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } @keyframes five { 0%, 15% { opacity: 0; transform: translateX(-12em); } 15%, 25% { transform: translateX(-12em); opacity: 1; } 85%, 90% { transform: translateX(12em); opacity: 1; } 100% { transform: translateX(12em); opacity: 0; } } @keyframes five-container { 0%, 35% { transform: none; } 50%, 60% { transform: skewX(20deg); } 90%, 100% { transform: none; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 缓入缓出 (Slow In and Slow Out) [图片] 对象很少从静止状态一下子加速到最大速度,它们往往是逐步加速并在停止前变慢。没有加速和减速,动画感觉就像机器人。 [代码] 在 CSS 方面,缓入缓出很容易被理解,在一个动画过程中计时功能是一种描述变化速率的方式。 [代码] 使用计时功能,动画可以由慢加速 (ease-in)、由快减速 (ease-out),或者用贝塞尔曲线做出更复杂的效果。 HTML [代码]<h1>Principle 6: Slow in and Slow out</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle six"> <div class="shape a"></div> </article> [代码] CSS [代码].six .shape { animation: six 3s infinite cubic-bezier(0.5,0,0.5,1); } @keyframes six { 0%, 5% { transform: translate(-12em); } 45%, 55% { transform: translate(12em); } 95%, 100% { transform: translate(-12em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 弧线运动 (Arc) [图片] 虽然对象是更逼真了,当它们遵循「缓入缓出」的时候它们很少沿直线运动——它们倾向于沿弧线运动。 我们有几种 CSS 的方式来实现弧线运动。一种是结合多个动画,比如在弹力球动画里,可以让球上下移动的同时让它右移,这时候球的显示效果就是沿弧线运动。 HTML [代码]<h1>Principle 7: Arc (1)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevena"> <div class="shape-container"> <div class="shape a"></div> </div> </article> [代码] CSS [代码].sevena .shape-container { animation: move-right 6s infinite cubic-bezier(.37,.55,.49,.67); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); } .sevena .shape { animation: bounce 6s infinite linear; border-radius: 50%; position: relative; left: auto; top: auto; } @keyframes move-right { 0% { transform: translateX(-20em); opacity: 1; } 80% { opacity: 1; } 90%, 100% { transform: translateX(20em); opacity: 0; } } @keyframes bounce { 0% { transform: translateY(-8em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 15% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 25% { transform: translateY(-4em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 32.5% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 40% { transform: translateY(0em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 45% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 50% { transform: translateY(3em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 56% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 60% { transform: translateY(6em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 64% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } 66% { transform: translateY(7.5em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); } 70%, 100% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] [图片] 另外一种是旋转元件,我们可以设置一个在对象之外的原点来作为它的旋转中心。当我们旋转这个对象,它看上去就是沿着弧线运动。 HTML [代码]<h1>Principle 7: Arc (2)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevenb"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].sevenb .shape.a { animation: sevenb 3s infinite linear; top: calc(50% - 2em); left: calc(50% - 9em); transform-origin: 10em 50%; } .sevenb .shape.b { animation: sevenb 6s infinite linear reverse; background-color: yellow; width: 2em; height: 2em; left: calc(50% - 1em); top: calc(50% - 1em); } @keyframes sevenb { 100% { transform: rotateZ(360deg); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 次要动作 (Secondary Action) [图片] 虽然主动画正在发生,次要动作可以增强它的效果。这就好比某人在走路的时候摆动手臂和倾斜脑袋,或者弹性球弹起的时候扬起一些灰尘。 在网页方面,当主要焦点出现的时候就可以开始执行次要动作,比如拖拽一个条目到列表中间。 HTML [代码]<h1>Principle 8: Secondary Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eight"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码] CSS [代码].eight .shape.a { transform: translateX(-6em); animation: eight-shape-a 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } .eight .shape.b { top: calc(50% + 6em); opacity: 0; animation: eight-shape-b 4s linear infinite; } .eight .shape.c { transform: translateX(6em); animation: eight-shape-c 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; } @keyframes eight-shape-a { 0%, 50% { transform: translateX(-5.5em); } 70%, 100% { transform: translateX(-10em); } } @keyframes eight-shape-b { 0% { transform: none; } 20%, 30% { transform: translateY(-1.5em); opacity: 1; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); } 32% { transform: translateY(-1.25em); opacity: 1; } 34% { transform: translateY(-1.75em); opacity: 1; } 36%, 38% { transform: translateY(-1.25em); opacity: 1; } 42%, 60% { transform: translateY(-1.5em); opacity: 1; } 75%, 100% { transform: translateY(-8em); opacity: 1; } } @keyframes eight-shape-c { 0%, 50% { transform: translateX(5.5em); } 70%, 100% { transform: translateX(10em); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 时间节奏 (Timing) [图片] 动画的时间节奏是需要多久去完成,它可以被用来让看起来很重的对象做很重的动画,或者用在添加字符的动画中。 [代码] 这在网页上可能只要简单调整 animation-duration 或 transition-duration 值。 [代码] 这很容易让动画消耗更多时间,但调整时间节奏可以帮动画的内容和交互方式变得更出众。 HTML [代码]<h1>Principle 9: Timing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle nine"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码] CSS [代码].nine .shape.a { animation: nine 4s infinite cubic-bezier(.93,0,.67,1.21); left: calc(50% - 12em); transform-origin: 100% 6em; } .nine .shape.b { animation: nine 2s infinite cubic-bezier(1,-0.97,.23,1.84); left: calc(50% + 2em); transform-origin: 100% 100%; } @keyframes nine { 0%, 10% { transform: translateX(0); } 40%, 60% { transform: rotateZ(90deg); } 90%, 100% { transform: translateX(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 夸张手法 (Exaggeration) [图片] 夸张手法在漫画中是最常用来为某些动作刻画吸引力和增加戏剧性的,比如一只狼试图把自己的喉咙张得更开地去咬东西可能会表现出更恐怖或者幽默的效果。 在网页中,对象可以通过上下滑动去强调和刻画吸引力,比如在填充表单的时候生动部分会比收缩和变淡的部分更突出。 HTML [代码]<h1>Principle 10: Exaggeration</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle ten"> <div class="shape"></div> </article> [代码] CSS [代码].ten .shape { animation: ten 4s infinite linear; transform-origin: 50% 8em; top: calc(50% - 6em); } @keyframes ten { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.87,-1.05,.66,1.31); } 40% { transform: rotateZ(-45deg) scale(2); animation-timing-function: cubic-bezier(.16,.54,0,1.38); } 70%, 100% { transform: rotateZ(360deg) scale(1); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 扎实的描绘 (Solid drawing) [图片] 当动画对象在三维中应该加倍注意确保它们遵循透视原则。因为人们习惯了生活在三维世界里,如果对象表现得与实际不符,会让它看起来很糟糕。 如今浏览器对三维变换的支持已经不错,这意味着我们可以在场景里旋转和放置三维对象,浏览器能自动控制它们的转换。 HTML [代码]<h1>Principle 11: Solid drawing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eleven"> <div class="shape"> <div class="container"> <span class="front"></span> <span class="back"></span> <span class="left"></span> <span class="right"></span> <span class="top"></span> <span class="bottom"></span> </div> </div> </article> [代码] CSS [代码].eleven .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .eleven .shape .container { animation: eleven 4s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; } .eleven .shape span { display: block; position: absolute; opacity: 1; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; } .eleven .shape span.front { transform: translateZ(3em); } .eleven .shape span.back { transform: translateZ(-3em); } .eleven .shape span.left { transform: rotateY(-90deg) translateZ(-3em); } .eleven .shape span.right { transform: rotateY(-90deg) translateZ(3em); } .eleven .shape span.top { transform: rotateX(-90deg) translateZ(-3em); } .eleven .shape span.bottom { transform: rotateX(-90deg) translateZ(3em); } @keyframes eleven { 0% { opacity: 0; } 10%, 40% { transform: none; opacity: 1; } 60%, 75% { transform: rotateX(-20deg) rotateY(-45deg) translateY(4em); animation-timing-function: cubic-bezier(1,-0.05,.43,-0.16); opacity: 1; } 100% { transform: translateZ(-180em) translateX(20em); opacity: 0; } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码] 吸引力 (Appeal) [图片] 吸引力是艺术作品的特质,让我们与艺术家的想法连接起来。就像一个演员身上的魅力,是注重细节和动作相结合而打造吸引性的结果。 [代码] 精心制作网页上的动画可以打造出吸引力,例如 Stripe 这样的公司用了大量的动画去增加它们结账流程的可靠性。 [代码] HTML [代码]<h1>Principle 12: Appeal</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle twelve"> <div class="shape"> <div class="container"> <span class="item one"></span> <span class="item two"></span> <span class="item three"></span> <span class="item four"></span> </div> </div> </article> [代码] CSS [代码].twelve .shape { background: none; border: none; perspective: 400px; perspective-origin: center; } .twelve .shape .container { animation: show-container 8s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; position: relative; } .twelve .item { background-color: #1f7bb6; position: absolute; } .twelve .item.one { animation: show-text 8s 0.1s infinite ease-out; height: 6%; width: 30%; top: 15%; left: 25%; } .twelve .item.two { animation: show-text 8s 0.2s infinite ease-out; height: 6%; width: 20%; top: 30%; left: 25%; } .twelve .item.three { animation: show-text 8s 0.3s infinite ease-out; height: 6%; width: 50%; top: 45%; left: 25%; } .twelve .item.four { animation: show-button 8s infinite cubic-bezier(.64,-0.36,.1,1.43); height: 20%; width: 40%; top: 65%; left: 30%; } @keyframes show-container { 0% { opacity: 0; transform: rotateX(-90deg); } 10% { opacity: 1; transform: none; width: 4em; height: 4em; } 15%, 90% { width: 12em; height: 12em; transform: translate(-4em, -4em); opacity: 1; } 100% { opacity: 0; transform: rotateX(-90deg); width: 4em; height: 4em; } } @keyframes show-text { 0%, 15% { transform: translateY(1em); opacity: 0; } 20%, 85% { opacity: 1; transform: none; } 88%, 100% { opacity: 0; transform: translateY(-1em); animation-timing-function: cubic-bezier(.64,-0.36,.1,1.43); } } @keyframes show-button { 0%, 25% { transform: scale(0); opacity: 0; } 35%, 80% { transform: none; opacity: 1; } 90%, 100% { opacity: 0; transform: scale(0); } } /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; } h1 { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; } h2 { font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; } a { text-decoration: none; color: #333; } .principle { width: 100%; height: 100vh; position: relative; } .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); } [代码]
2019-03-21