- 微信小程序签字板、手写签名、涂鸦组件
bao-wecom 小程序自定义签字板、手写签名、涂鸦组件。 背景 在一次小程序项目中,需要开发一个用户签字的功能,所以就开发这个签字板组件。 案例demo [图片] 使用方法 1. 安装组件 [代码]npm install --save bao-wecom [代码] 2. 构建 npm 点击开发者工具中的菜单栏:工具 --> 构建 npm [图片] 3. 签字板使用 在页面的 json 配置文件中添加 bao-wecom-signature 自定义组件的配置 [代码]{ "usingComponents": { "bao-wecom-signature": "bao-wecom/signature/index" }, "pageOrientation": "landscape",//横屏 "navigationStyle": "custom" //自定义头 } [代码] WXML 文件中引用 bao-wecom-signature [代码]<bao-wecom-signature></bao-wecom-signature> [代码] bao-wecom-signature 的属性介绍如下: 字段名 类型 必填 描述 title String 否 签字版标题 color String 否 设置笔画线条的颜色,默认为#1A1A1A ,hex格式 boardColor String 否 设置画板的颜色,默认为#ffffff ,hex格式 backgroundColor String 否 设置背景的颜色,默认为#ffa500 ,hex格式 size Number 否 设置笔画线条的粗细,默认为8 fileType String 否 设置导出图片的格式,默认为png quality Number 否 生成图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。 buttonList Array 否 设置显示的按钮,默认为[“cancel”, “setting”, “reset”, “preview”, “save”, “confirm”],cancel:取消按钮,setting:设置按钮,reset:重写按钮,preview:预览按钮,save:保存按钮,confirm:确认按钮。 bao-wecom-color-picker 的事件介绍如下: 事件名 描述 confirm 点击确定时触发,返回值{ path: 图片本地路径, width: 宽度, height: 高度 } cancel 点击取消时触发 如果觉得作者不易,打赏一下呗 [图片]
10-30 - 小程序自定义tabbar,根据权限显示不同的tabbar
在小程序开发者,自定义tabbar,根据权限角色不同,显示不同的tabbar,经常遇到,这里给出一个解决方案。 1、修改app.json [代码]"tabBar": { "custom":true } [代码] 2、在项目根目录创建一个文件夹 custom-tab-bar,组件,如图 [图片] custom-tab-bar/index.js [代码]Component({ /** * 组件的属性列表 */ properties: { }, /** * 组件的初始数据 */ data: { active: -1, list: [ { value: 0, show: true, label: '首页', icon: 'home', url: '/pages/index/index' }, { value: 1, show: true, label: '订单', icon: 'task', url: '/pages/order/index/index' }, { value: 2, show: true, label: '我的', icon: 'user', url: '/pages/my/index/index' }, ], }, /** * 组件的方法列表 */ methods: { onChange({detail: {value}}) { const {list} = this.data; console.log('value', value) this.setData({ active: value, }); wx.switchTab({ url: list[value].url, }) }, init() { const page = getCurrentPages().pop(); let urls = this.data.list.map(v => v.url); let active = urls.findIndex(v => v === `/${page.route}`); console.log('active',active) this.setData({ active }); }, toggleMenu(role) { // 在这里处理具体的权限业务逻辑 // 下面是伪代码 let {list} = this.data; if (role == 1) { list[1].show = false; this.setData({list}); } }, } }) [代码] custom-tab-bar/index.json [代码]{ "component": true, "usingComponents": { "t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar", "t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item" } } [代码] custom-tab-bar/index.wxml [代码]<t-tab-bar t-class="t-tab-bar" value="{{active}}" bindchange="onChange" theme="tag" split="{{false}}"> <block wx:for="{{list}}"> <t-tab-bar-item wx:if="{{item.show}}" wx:key="index" value="{{item.value}}" icon="{{item.icon}}"> {{item.label}} </t-tab-bar-item> </block> </t-tab-bar> [代码] 3、在tabbar关联页面初始化tabbar组件的init方法 上面我们在tabbar写了三个路由,那么在这三个相对应的页面,都要调用一下下面的代码 [代码]onShow() { this.getTabBar().init() }, [代码] 通过以上步骤已经实现了自定义tabbar的逻辑。 关于角色权限下的tabbar 在有些场景下,我们需要权限来控制显示tabbar,这个时候,我们可以在首页,请求数据,然后调用[代码]this.getTabBar().toggleMenu();[代码] 来改变要显示的菜单,这样我们根据角色权限的tabbar就完成了。
06-26 - 手把手教你备案微信小程序(非个人主体备案)
备案材料准备 在提交备案前,请务必提前准备好备案所需材料,以免由于材料更新问题,导致备案需延期提交。下面将会带大家详细了解备案材料的要求,这样后续在提交时就能避免因为材料问题而导致失败。 材料示例及注意事项 [图片] 注:所有上传材料大小应不超过2M,分辨率不低于720* 1280 ,仅支持JPG、JPEG、PNG 格式 若想查看更多小程序备案材料示例,详情可查看文档 备案信息填写 备案材料准备好后,就可以前往【小程序管理后台-设置-小程序备案】提交备案申请了。下面将会详细教大家如何进行备案信息的填写,一共分为五个部分:主办单位信息填写、主体负责人信息填写、小程序信息填写、小程序管理员信息填写和上传其他信息材料。 1.主办单位信息填写 [图片] [图片] 填写说明 常见报错/问题 解决方案 ①选择地区:选择与证件地址相一致的省市区信息 该主体已在XX完成备案,请修改备案省份或注销备案主体重新备案 请核实该主体是否有在其他省份备案过,由于同主体在所有平台的备案省份必须保持一致,需修改备案省份或注销备案主体重新备案 ②主办者性质:默认与小程序主体认证信息相一致 / / ③证件类型:默认与小程序主体认证信息相一致 / / ④上传证件:按要求提供最新版证件 营业执照有效期不足 请联系工商部门更新证件有效期 ⑤企业名称:填写证件相对应名称信息 主办者与小程序主体不一致 请核实填写企业名称是否与小程序主体名称、上传营业执照名称相一致 ⑤企业名称:填写证件相对应名称信息 营业执照名称为空或者* 号 请联系工商部门更新企业名称信息 ⑥证件住所:填写证件相对应经营场所信息 【主体证件住所】工商数据对比不通过 请参考文档进行排查 ⑦证件号码:填写证件相对应统一社会信用代码信息 未查询到企业信息,请检查主体证件号是否有误 请核实填写的是否为统一社会信用代码,若无,请联系工商部门更新证件信息,不能填写其他如工商注册号等 ⑧通讯地址:填写当前主体所在的实际通讯地址(无需填写省、市、区) 通讯地址未能精确到门牌号 若无具体门牌号,需要在备注中说明情况 ⑨备注(选填):针对主体信息进行补充说明,如有可填写 / / 注:若为新建企业或近期有做信息变更,可能会存在企业工商数据更新延迟的情况,建议过段时间(5~15个工作日)再进行重试,否则无法正常发起验证流程。 2.主体负责人信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择主体负责人证件类型信息 / / ②上传证件:按要求提供最新版证件 / / ③负责人名称:通过上传证件自动识别,有误可自行修改 主体负责人与法定代表人不一致,且备案所在地不支持法定代表人授权 请核实填写的主体负责人是否为法人,需与营业执照信息一致,由于所属地区不支持授权,只能填写法人信息 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【主体负责人证件号码】企业工商四要素核验失败 请核实填写的主体负责人名称、证件号信息是否正确 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:主体负责人手机号码 【主体负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:主体负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:主体负责人的应急电话 【主体负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:主体负责人的电子邮箱 / / 3.小程序信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①服务内容标识:根据小程序实际运营内容选择合适的即可 小程序服务内容类型数目不能超过5个 服务内容标识是通信管局对各个行业的分类,平台部分行业类目与管局行业类目名称不完全不一致,建议根据备案小程序实际运营内容尽可能选择对应的服务内容标识,最多选择5个。 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 如从事XXX业务,请上传前置审批文件 小程序实际运营内容涉及前置审批项,需上传对应的审批文件 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 前置审批项必须选择“以上都不涉及” 小程序实际运营内容不涉及前置审批项,需要选择"以上都不涉及” ③备注(必填):具体描述小程序实际经营内容,主要服务内容 请在小程序备注按格式填写 请核实是否有根据备注格式进行填写,仅自行补充带星号内容即可。 4.小程序管理员信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择小程序负责人证件类型信息(目前仅支持身份证) / / ②上传证件:按要求提供最新版证件(目前仅支持身份证) / / ③负责人名称:通过上传证件自动识别,有误可自行修改 【小程序负责人姓名】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【小程序负责人证件号码】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:小程序负责人手机号码 【小程序负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:小程序负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:小程序负责人的应急电话 【小程序负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:小程序负责人的电子邮箱 / / ⑩负责人人脸核身:小程序管理员(小程序负责人)需使用微信APP扫码,完成人脸核身 当前场景仅支持居民身份证 核实小程序管理员证件是否为大陆居民身份证,目前港澳台管理员无法进行人脸核身,建议先更换小程序管理员为中国大陆地区的人员,作为备案小程序负责人。 5.上传其他信息材料 [图片] 填写说明 常见报错/问题 解决方案 互联网信息服务承诺书:1. 广东地区:下载页面提供的模版文件,填写完整后上传提交;2. 非广东地区:点击阅读确认后提交 承诺书需加盖公章,但个体户没有公章 若个体工商户无公章,需要主体负责人手写日期+签名+盖手印+身份证号码,同时请在主体备注处备注“个体工商户无公章”。注:江苏、宁夏、福建地区,须刻章后提交备案,不接受负责人手印。 以上信息都填写完毕,就可点击提交,后续的备案审核流程可参考: [图片] 如有其他相关疑问,欢迎随时参与社区讨论。
05-28 - 原生小程序自定义导航栏
微信原生小程序导航栏 navigation-bar 使用说明 [图片][图片] 1、安装插件,并在小程序开发工具中构建 npm [代码]npm i navigationbar-wx [代码] 2、app.js 中得 globalData 内写上如下: [代码]import { navigationbarWx } from "navigationbar-wx/config.js"; navigationbarWx.init({ title: "小程序默认名称", homePath: "/pages/index/index",//默认路径,如果相同可以忽略这个参数 }) //备注:init 可初始化的字段如下字段说明 [代码] 3、app.json 中配置插件 [代码]"usingComponents": { "navigationbar-wx": "navigationbar-wx" } [代码] 4、页面使用-传参如下说明 [代码]<!-- 可直接使用 --> <navigationbar-wx /> <!-- 也可传参使用 --> <navigationbar-wx title="" logo="" color="" bgColor="" bgImg="" iconColor="" hideHome="" hideSeat="" /> [代码] 字段说明 字段 必传 类型 默认值 说明 title 否 String 小程序默认标题 当前页面标题 logo 否 String 无 可传入一个图片 URL 作为标题,logo 优先级大于 title color 否 String #000000 字体颜色,默认黑色 bgColor 否 String #ffffff 导航栏颜色,默认白色 bgImg 否 String 无 导航栏整体图片,背景图优先级大于背景颜色 iconColor 否 String black 左边按钮颜色,只支持 black、white 两种类型 hideHome 否 Boolean false 当没有上一级页面时,是否隐藏小房子 hideSeat 否 Boolean false 导航栏底部占位,默认占位不隐藏 homePath 否 String /pages/index/index 页面如果设置这个参数优先级高于 init 时传入的路径 github 地址 github 地址
06-17 - input adjust-position 键盘弹起 + 自定义导航 解决固定定位偏移top方案
input adjust-position 键盘弹起 + 自定义导航 解决flex定位问题 关键词:adjust-position,键盘弹起 ;"navigationStyle": "custom"; 解决方案:当键盘弹起时,通过已知值计算top偏移; 主要获取数值 键盘高度,窗口高度,获取焦点的输入框的top偏移(主要); 窗口高度 - 键盘高度 = 可视的高度 输入框Top偏移(主要是这个,在不同位置) - 可视的高度 + 输入框高度 = 相对窗口顶部(top)偏移 wxml 部分 <view class="navigation-bar" style="{{keyboardHeight > 0?'top:'+OffsetTop +'px':''}}"> <navigation-bar /> <!--自定义导航--> </view> .navigation-bar{ position: fixed; top:0; left:0; widt:100%; } <view> <view style="height:600px"></view> <!-- 非必需,看个人需要;cursor-spacing 指定光标与键盘的距离,取 input 距离底部的距离和 cursor-spacing 指定的距离的最小值作为光标与键盘的距离 --> <!-- 必须 id --> <!-- 必须 bindfocus bindblur 指向同一个事件--> <input cursor-spacing="20" id="input-phone" bindfocus="getTelIptHeight" bindblur="getTelIptHeight" type="text" value="{{phone}}" bindinput="phoneInput" placeholder="请输入手机号" placeholder-class="input-placeholder" /> </view> js事件部分 const selectDom = (createSelectorQuery, select) => { return new Promise((resolve, reject) => { try { createSelectorQuery.select(select).boundingClientRect((e) => { resolve(e) }).exec(); } catch (error) { console.log(error); } }) } { data:{ OffsetTop:0, }, getTelIptHeight(e) { console.log("height---------", e); let keyboardHeight = e?.detail?.height || 0 this.setData({ keyboardHeight: keyboardHeight }) // 自定义导航栏逻辑计算 const windowHeight = wx.getSystemInfoSync().windowHeight; // 获取窗口高度 const query = wx.createSelectorQuery(); // selectDom 自己疯装的 Promise.all([selectDom(query, '#' + e.currentTarget.id)]).then((rect) => { console.log(rect[0], windowHeight) const inputTop = rect[0]?.top; // 获取输入框的 top偏移 const bottomOffs = windowHeight - keyboardHeight; // 可视窗口高度 //输入框偏移位置 - 可视窗口高度 + 自身高度 = 相对窗口顶部(top)偏移 const Offset = (inputTop - bottomOffs) + rect[0]?.height; console.log('Offset', Offset) this.setData({ OffsetTop: Offset }) }) } }
04-16 - css实现无缝滚动字幕
直接看代码片段吧:https://developers.weixin.qq.com/s/Ae0lsgme7SOG 代码不复杂,就不多解释了 js主要是在一开始设置一下css,后续全都靠css来实现效果 为什么要用css来实现,而不是js。主要是实际使用下来,css比js在时间控制上更精准,而且不需要用到setTimeout,也就没必要考虑clearTimeout 同样原理还可以做出纵向的滚动字幕,代码片段:https://developers.weixin.qq.com/s/9p1H6gmk75OM
01-16 - 深入理解小程序——生命周期
生命周期类型 不像其他框架,小程序是有页面(page) 和组件(Component) 两个概念,所以可以理解有两种生命周期。不过,你也可以使用组件也注册页面,然后在 [代码]lifetimes[代码] 字段里声明组件的生命周期,在 [代码]pageLifetimes[代码] 里声明页面的生命周期。 页面的生命周期有: onLoad onShow onReady onHide onUnload 而组件的生命周期有: created attached ready moved detached error 这里其实还有一个 linked 生命周期,存在父子关系时会触发 条件渲染的影响 组件可以通过 [代码]wx:if[代码] 和 [代码]hidden[代码] 来控制渲染的,这里对生命周期的触发也有影响。 如定义这么一个组件 [代码]log[代码]: [代码]Component({ lifetimes: { attached() { console.log('log attached') } } }) [代码] 使用 wx:if 然后在页面中使用 [代码]wx:if[代码] 条件渲染: [代码]<view wx:if="{{false}}"> <log /> </view> [代码] 此时不会触发 [代码]attached[代码],因此控制台没有输出。 使用 hidden 反而如果使用 [代码]hidden[代码] 条件渲染: [代码]<view hidden="{{true}}"> <log /> </view> [代码] 此时反而控制台会输出 [代码]log attached[代码]。 两者差异 其实两者的差异在于,[代码]hidden[代码] 会正常渲染 DOM,而 [代码]wx:if[代码] 则不会渲染。 如果组件的父元素使用 [代码]hidden[代码] 进行隐藏,那么此时 [代码]created[代码]、[代码]attached[代码]、[代码]ready[代码] 生命周期均会正常触发。如果在这些生命周期里获取子元素的尺寸,则所有值均返回 0。 如 TDesign 里面的 [代码]swipe-cell[代码] 需要计算 left 和 right 区域的大小;[代码]tabs[代码] 需要计算下划线的位置。 解决方案 比较简单的处理方式:建议用户使用 [代码]wx:if[代码] 而不是 [代码]hidden[代码],不过这明显是治标不治本的方案。 前文也提到了,问题的根本是没有正确地获取到元素的尺寸,因此可以在获取元素尺寸的地方做兼容处理。异常触发的条件则是 [代码]width == 0 && right == 0[代码] 知道在哪里需要兼容处理之后,需要解决的则是:如何在可以获取到正确的尺寸的时候重新获取尺寸呢? 此时可以使用 [代码]wx.createIntersectionObserver[代码] 这个 API。当 [代码]hidden = false[代码] 的时候,组件会重新出现在视图里,Observer 就会被触发,此时重新获取尺寸就能得到正确的尺寸信息了。以下是简易的封装: [代码]const getObserver = (context, selector) => { return new Promise((resolve, reject) => { wx.createIntersectionObserver(context) .relativeToViewport() .observe(selector, (res) => { resolve(res); }); }); }; const getRect = function (context:, selector) { return new Promise((resolve, reject) => { wx.createSelectorQuery() .in(context) .select(selector) .boundingClientRect((rect) => { if (rect) { resolve(rect); } else { reject(rect); } }) .exec(); }); }; export const getRectFinally = (context, selector) => { return new Promise((resolve, reject) => { getRect(context, selector).then(rect => { if (rect.width === 0 && rect.height === 0) { getObserver(context, selector).then(res => { resolve(res) }).catch(reject) } else { resolve(rect) } }).catch(reject) }) } [代码] 父子组件的影响 当存在父子组件的时候,可能很多人根本不知道各种生命周期的触发顺序。 之前 TDesign 的 [代码]cell-group[代码] 有个错误的实现,在 linked 生命周期里获取子元素进行操作: [代码]Component({ relations: { '../cell/cell': { type: 'child', linked() { this.updateLastChid(); }, }, }, updateLastChid() { const items = this.$children; items.forEach((child, index) => child.setData({ isLastChild: index === items.length - 1 })); }, }) [代码] 其实,存在父子组件的时候,生命周期是这么触发的: 父组件 created 子组件 created 父组件 attached 子组件 attached 父组件 linked(触发多次,次数 = 子组件数量) 子组件 linked 父组件 ready 子组件 ready 因此如果是这么使用 [代码]t-cell-group[代码]: [代码]<t-cell-group> <t-cell title="cell1" /> <t-cell title="cell2" /> <t-cell title="cell3" /> </t-cell-group> [代码] 那么子组件 cell 的 [代码]setData[代码] 触发次数为:1 + 2 + 3 = 6 次。 但其实开发者的预期应该是 1 次,所以 [代码]updateLastChid[代码] 应该放在父组件的 ready 方法里才符合预期。 总结 以上是在小程序开发的过程中,常遇到的问题。但如果没有像 TDesign 组件库这样深入开发小程序,可能并不会去深入钻研生命周期的细节。但在日常的业务开发当中,如果开发者能够清晰地理解各种生命周期的本质,在遇到其他问题的时候,也能比较快速地定位问题的关键点。 如果能到达这样的效果,也是笔者写下这篇文章的初心。 更多内容关注:https://github.com/LeeJim
2023-08-18 - 🎆我们开源啦 | 基于Skyline开发的组件库🚀
我们开源啦,希望可以给大家的开发之旅带来一些灵感。我后溪的小程序也都会基于这个组件库开发,并且会保持组件库的更新与维护。 我是第一次进行开源,肯定会有错漏,欢迎大家指正,我会以最快的时间响应修改。 Skyline UI 组件库 前言 Skyline 是微信小程序推出的一个类原生的渲染引擎,其使用更精简高效的渲染管线,性能比 WebView 更优异,并且带来诸多增强特性,如 Worklet 动画、手势系统、自定义路由、共享元素等。 使用这个组件库的前提是:通过微信小程序原生+skyline框架开发,所以目前我们不保证兼容webview框架(也就是电脑端与低版本的微信),但后续会进行系统性的兼容。 使用 Skyline UI前,请确保你已经学习过微信官方的 微信小程序开发文档 和 Skyline 渲染引擎文档 。 背景 随着Skyline 渲染引擎 1.1.0 版本发布,我们所运营的小程序也平稳的渡过了阵痛期,团队使用Skyline也越来得心应手,所以接下来,团队的开发重心全面偏向Skyline渲染框架,考虑有大量的UI交互重复,我们决定基于Skyline开发了这个UI组件库。 但团队力量有限,这个新生的组件可能有很多的不尽如人意,所以希望能以开源的方式吸引更多开发者使用Skyline框架,如果这个框架不适合你,也可以借鉴其思路。 Gitee Gitee仓库 在线预览 以下是目前两个使用该框架的小程序 SkylineUI组件库 [图片] NONZERO COFFEE [图片] 开始使用 UI库结构 Skyline UI组件库 依赖于以下四部分,具体使用参考以下的具体说明 utils工具库: 其中包含了UI库自定义的一个工具类SkyUtils,它包含了组件中所含的各种函数,非常重要。 各组件元素:sky-*(组件名) skywxss样式库:其中包含深浅色色彩、文字字体、布局等样式wxss 在小程序中引入 UI库 一、直接下载引入 点击下载组件包 将src下所有文件复制到您项目根目录下的components文件夹中,没有的话请自行新建。 二、npm引入 1.在小程序项目中,可以通过 npm 的方式引入 SkylineUI组件库 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 2.安装组件库 [代码]npm install jieyue-ui-com [代码] 3.npm 命令执行完后,需要在开发者工具的项目中点菜单栏中的 工具 - 构建 npm 两种引入方式的不同可能导致后续使用时,引用组件的路径不同,请注意区别 1.直接引入components文件夹内,引用地址通常是 ‘./components/‘ 2.npm引入,组件引用地址通常是’./miniprogram_npm/jieyue-ui-com/’ 如何使用 1.在app.js文件中初始化工具类,并且添加两个全局变量 [代码]// app.js App({ onLaunch() { ;(async ()=>{ // 全局注册工具类SkyUtils // 这里默认npm引用,地址为'./components/utils/skyUtils',如果是直接引用组件,地址可能是'./components/utils/skyUtils',后面不再说明 const SkyUtils = await import('./components/utils/skyUtils'); wx.SkyUtils = SkyUtils.default; // 初始化设备与系统数据 wx.SkyUtils.skyInit() // 小程序自动更新方法 wx.SkyUtils.versionUpdate() })() }, globalData: { sky_system:{}, sky_menu:{} }, }) [代码] 2.在app.wxss文件中引入样式文件 [代码]//wxss * _dark.wxss 是适配深色模式的色彩变量 @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skycolor_dark.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfontline.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyfont.wxss'; @import '/miniprogram_npm/jieyue-ui-com/skywxss/skyother.wxss'; [代码] 3.page.json中引用组件 [代码]//page.json { "usingComponents": { "sky-text":"/miniprogram_npm/jieyue-ui-com/sky-text/sky-text" } } [代码] 4.页面中使用 [代码] // wxml <sky-text content="文本内容" max-lines="2" fade></sky-text> [代码] 5.其他组件具体使用请参考组件包中的redeme.md 适配深色模式 如果您在开发时,全部使用我们预设好的颜色变量,那么可以自动适配深色模式。 [代码].page{ background-color: var(--bg-l0); } [代码] [代码] <view style="background-color: var(--bg-l0)"></view> <view style="background-color: {{color}}"></view> [代码] [代码] Page({ data: { color: "var(--bg-l0)" } }) [代码]
01-09 - 用小程序wxs做一个时间格式化的过滤器
为什么用wxs格式化时间呢? 使用小程序的wxs格式化时间可以使代码更简洁、易读,并且可以避免在多个地方使用相同的时间格式化代码,提高了代码的可维护性和可重用性。此外,wxs运行在渲染层,可以减少渲染层和逻辑层之间的通信开销,提高小程序的性能。 只需要三步,就可以做一个格式化时间的过滤器(代码亲测可用) 1 在小程序的工程目录中,创建一个名为“filters”的文件夹,然后在该文件夹下创建一个名为“timeFilter.wxs”的文件,用于实现时间格式化过滤器。 2 在“timeFilter.wxs”文件中,定义一个名为“timeFormat”的函数,用于格式化时间。该函数接收一个时间参数(可以是当前时间、时间戳、毫秒时间戳或Date格式的时间),以及一个格式化字符串参数(例如“yyyy-MM-dd HH:mm:ss”),返回格式化后的时间字符串。 function timeFormat(timestamp, format) { if (!format) { format = "yyyy-MM-dd hh:mm:ss"; } var realDate if ((timestamp + '').length == 10 || (timestamp + '').length == 13) { if ((timestamp + '').length == 10) timestamp = timestamp * 1000; timestamp = parseInt(timestamp); realDate = getDate(timestamp); } else { timestamp = timestamp.replace(getRegExp("T", 'g'), ' '); timestamp = timestamp.replace(getRegExp("-", 'g'), '/'); realDate = timestamp; } var realDate = getDate(timestamp); function timeFormat(num) { return num < 10 ? '0' + num : num; } var date = [ ["M+", timeFormat(realDate.getMonth() + 1)], ["d+", timeFormat(realDate.getDate())], ["h+", timeFormat(realDate.getHours())], ["m+", timeFormat(realDate.getMinutes())], ["s+", timeFormat(realDate.getSeconds())], ["q+", Math.floor((realDate.getMonth() + 3) / 3)], ["S+", realDate.getMilliseconds()], ]; var reg1 = regYear.exec(format); if (reg1) { format = format.replace(reg1[1], (realDate.getFullYear() + '').substring(4 - reg1[1].length)); } for (var i = 0; i < date.length; i++) { var k = date[i][0]; var v = date[i][1]; var reg2 = getRegExp("(" + k + ")").exec(format); if (reg2) { format = format.replace(reg2[1], reg2[1].length == 1 ? v : ("00" + v).substring(("" + v).length)); } } return format; } var regYear = getRegExp("(y+)", "i"); module.exports = { timeFormat: timeFormat }; 3 在需要使用过滤器的页面的.wxml文件中,引入“timeFilter.wxs”文件,使用<wxs>标签将其定义为一个模块,然后在需要格式化时间的地方使用过滤器。 <wxs module="timeFilter" src="../test/fimeFilter.wxs"></wxs> <!-- 格式化时间戳 --> <view>{{timeFilter.timeFormat(1613509857, 'yyyy-MM-dd hh:mm:ss')}}</view> <!-- 格式化毫秒时间戳 --> <view>{{timeFilter.timeFormat(1613509857123, 'yyyy-MM-dd hh:mm:ss')}}</view> <!-- 格式化Date格式的时间 --> <view>{{timeFilter.timeFormat('2022-02-12T12:00:00', 'yyyy-MM-dd hh:mm:ss')}}</view> <view>{{timeFilter.timeFormat('2022/02/12 12:00:00', 'yyyy-MM-dd hh:mm:ss')}}</view> 当然,小程序的wxs不仅仅可以格式化时间,还可以用于实现一些数据的计算、格式化、过滤等功能,主要包括以下方面: 数据的格式化:可以将时间、货币、数字等数据进行格式化,例如将时间转换为指定格式的字符串,将数字保留指定位数的小数等。数据的计算:可以进行简单的数学运算,例如加减乘除、求余数等。数据的过滤:可以实现对数据的筛选和过滤,例如过滤出符合条件的数组元素、去除字符串中的空格等。数据的操作:可以对数据进行一些简单的操作,例如获取数组的长度、获取字符串的子串等。 需要注意的是,小程序的wxs并不是完整的JavaScript语言,它是在JavaScript的基础上进行了一定的限制和扩展。因此,在使用wxs时需要遵守小程序的规范和限制,例如不能使用全局变量、不能直接访问页面的DOM元素等。 更多关于wxs的详细说明,可以移步官方文档 《WXS介绍》
2023-02-15 - 微信小程序this.setData如何修改对象、数组中的值
在微信小程序的前端开发中,使用this.setData方法修改data中的值,其格式为 this.setData({ '参数名1': 值1, '参数名2': 值2 )} 需要注意的是,如果是简单变量,这里的参数名可以不加引号。 经过测试,可以使用3种方式对data中的对象、数组中的数据进行修改。 假设原数据为: data: { user_info:{ name: 'li', age: 10 }, cars:['nio', 'bmw', 'wolks'] }, 方式一: 使用['字符串'],例如 this.setData({ ['user_info.age']: 20, ['cars[0]']: 'tesla' }) 方式二: 构造变量,重新赋值,例如 var temp = this.data.user_info temp.age = 30 this.setData({ user_info: temp }) var temp = this.data.cars temp[0] = 'volvo' this.setData({ cars: temp }) 方式三: 直接使用字符串,此种方式之前不可以,现在可以了,估计小程序库升级了。 注意和第一种方法的对比,推荐还是使用第一种方法。 this.setData({ 'user_info.age': 40, 'cars[0]': 'ford' }) 完整代码: Page({ /** * 页面的初始数据 */ data: { user_info:{ name: 'li', age: 10 }, cars:['nio', 'bmw', 'wolks'] }, change_data: function(){ console.log('对象-修改前:', this.data.user_info) this.setData({ ['user_info.age']: 20 }) console.log('对象-修改后1:', this.data.user_info) var temp = this.data.user_info temp.age = 30 this.setData({ user_info: temp }) console.log('对象-修改后2:', this.data.user_info) this.setData({ 'user_info.age': 40 }) console.log('对象-修改后3:', this.data.user_info) console.log('数组-修改前:', this.data.cars) this.setData({ ['cars[0]']: 'tesla' }) console.log('数组-修改后1:', this.data.cars) var temp = this.data.cars temp[0] = 'volvo' this.setData({ cars: temp }) console.log('数组-修改后2:', this.data.cars) this.setData({ 'cars[0]': 'ford' }) console.log('数组-修改后3:', this.data.cars) } }) 效果: [图片]
2020-08-26 - Skyline 转场动画轻松实现
在之前的 Skyline|小程序页面转场动画 文章中,Skyline 支持了自定义路由,开发者可以根据业务的需求来自行编写页面转场动画。 文章发布之后,我们收到不少开发者的反馈,对于“半屏”打开的转场动画是十分常见的,希望官方可以内置该能力。 为了降低开发成本,从基础库 v3.1.0 开始,Skyline 预设了一些常见的路由动画效果~ routeType 动画效果 wx://bottom-sheet 向上半屏弹窗,前一个页面不变 wx://upwards 向上进入页面,前一个页面不变 wx://zoom 放大进入页面,前一个页面不变 wx://cupertino-modal 向上打开页面至胶囊下面的位置,前一个页面收缩下沉 wx://cupertino-modal-inside 被 wx://cupertino-modal 打开的页面需要继续使用 wx://cupertino-modal 打开效果 wx://modal-navigation 被 wx://cupertino-modal 或 wx://modal 打开的页面向左进入页面的效果,前一个页面不变 wx://modal 向上打开页面至胶囊下面的位置,前一个页面不变 对于以上的 routeType,使用起来非常简单,因为动画效果已经由基础库内置了,所以开发者直接使用即可 [代码]// 演示使用 wx://modal 进入页面 wx.navigateTo({ url: 'xxx', routeType: 'wx://modal' }) [代码] 让我们来看看动画效果吧~ 1、向上半屏弹窗 & 向上进入页面 & 放大进入页面 除了默认的向左进入页面外,基础库内置了向上半屏弹窗 & 向上进入页面 & 放大进入页面 这几个动画效果,开发者可以根据业务自身情况来使用 [图片] 2、向上打开页面至胶囊下面的位置 这个动画效果在原生 APP 中的使用十分常见,对于前一个页面的处理和后面页面打开的动画效果略有不同 我们先来看 wx://cupertino-modal 的路由效果(图左)向上打开页面至胶囊下面的位置,前一个页面收缩下沉 在使用 wx://cupertino-modal 打开的页面之后,有两种页面打开形式 · wx://cupertino-modal-inside(图中):新页面打开方式同 wx://cupertino-modal 一致 · wx://modal-navigation(图右):新页面向左进入,前一个页面不变 [图片] 前一个页面的效果除了下沉收缩,也支持保持不变,使用起来就比较简单 使用 wx://modal 向上打开页面至胶囊下面的位置,前一个页面不变。新页面需要使用 wx://modal-navigation 向左进入,前一个页面不变 [图片] 预设路由使用简单,直接在 wx.navigateTo 配置参数即可,如果想要更丰富、更多的自定义的路由效果,大家可以使用 自定义路由 来自行定制~
2023-10-23 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: 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 - 微信小程序-图片宽高自适应设置
微信小程序中,有很做组件都是有默认宽高的,比如,image组件默认宽度320px、⾼度240px,这些默认设置常常会对我们的页面布局造成影响 <!-- mode="widthFix" 让图片占满宽度,等比例缩放高度 --> <image src="xxxx" mode="widthFix"></image> 所以,在使用image标签的时候,最常用的方法就是设置其 mode,例如用widthFix,实现图片占满宽度,同时按照宽度来拉伸图片高度,这个mode是最常用的image模式。 有时候需要根据图片的宽高来计算出image标签的高度,这就需要用到一个公式了(目的是为了让展示图片根据原图进行等比例的缩放) [图片] 而且,容器.width基本都会设置为100%(100vw),再加上能查看到图片的宽高,就可以根据下面的公式计算出应该设置的容器的高度,进而展现出最完整的图片 [图片] 例如:设置容器的高度就能在wxss样式中通过 【 calc 】 来计算出了 [图片] 这样展示出来的图片就是按照原图的比例缩放的,不会造成超出或者拉伸。
2023-10-17 - 微信小程序-封装请求API——promise方式
目录 第一步:在app.js同级目录下,创建一个文件夹 第二步:封装wx.request方法成promise对象 第三步:页面中引用封装的请求API 1.设置基础请求路径 2.解构传入的参数 3.根据不同的url接口添加不同的header 4.添加请求发起时页面loading效果 完整的封装的API 的 js文件 微信小程序原生的请求API就是wx.request [代码]wx.request({ url: 'example.php', //仅为示例,并非真实的接口地址 data: { x: '', y: '' }, header: { 'content-type': 'application/json' // 默认值 }, success (res) { console.log(res.data) } }) [代码] 有时候不能很好的适配我们的开发需求,比如我们要加一些基础url路径、请求前后的loading效果、不同接口名称下的header。而且,在success回调方法里写请求成功后的操作,看起来代码不太清晰。接下来讲一下封装的逻辑,完整代码放在最后。 第一步:在app.js同级目录下,创建一个文件夹 [图片] 在utils文件夹里新建一个service.js文件,用来放封装的wx.request方法 第二步:封装wx.request方法成promise对象 使用promise对象能很好的解决回调地狱,在.then(res=>{}).catch(err=>{})中能很清晰地看出代码的逻辑 [代码]export const 返回出去的方法名 = (parmas) => { // 返回一个promise对象 return new Promise((resolve, reject) => { wx.request({ url: parmas.url, //仅为示例,并非真实的接口地址 data:parmas.data, header: { 'content-type': 'application/json' // 默认值 }, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) } [代码] 注意:success: (result) => {} 使用箭头函数,防止出现this指向错误 这样,就算是封装了一个最简单、最基础(简陋)的请求API了,在需要使用这个方法的页面的js文件中,引入它 第三步:页面中引用封装的请求API [代码]/** * 小程序中要引用方法,哪个页面要用,就在哪个页面引入 */ import { cjRequest } from "../../utils/service"; Page({ /** * 页面的初始数据 */ data: { .......... .......... getGoodsList () { //因为返回的是promise对象,所以通过.then来获取resolve出来的请求成功的返回数据 cjRequest({ url: "https://xxxtest/goods/search", data: this.QueryParams }) .then((res) => { console.log(res); }) } [代码] 现在,来详细扩展一下封装的请求API 1.设置基础请求路径 [代码]// 基础url const baseUrl = "https://xxxtest" [代码] 这样,就可以简化调用这个方法时url的参数内容了,也方便统一修改开发环境地址、生产环境地址 [代码]// url: "/goods/search" ==》 url中不用再写前面的一长串了 cjRequest({ url: "/goods/search", data: this.QueryParams }).then((res) => { console.log(res); }) [代码] 2.解构传入的参数 我们可以通过ES6中的扩展运算符,将传入到封装方法里的参数直接全部解构出来,不用再一个个获取赋值给对应的键值对了 …parmas 直接解构出传入的参数 [代码]export const 返回出去的方法名 = (parmas) => { //设置基础请求头 const baseUrl = "https://xxxtest" // 返回一个promise对象 return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其解构出的,传入的url:xxx参数 url: baseUrl + parmas.url, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) } [代码] 3.根据不同的url接口添加不同的header header中的Authorization字段一般存储token,用来做身份验证,但有些请求不需要携带token,所以这个封装的API中需要能根据传入的url来判断什么时候该给请求添加上token,什么时候不用可以添加。 [代码]``` export const 返回出去的方法名 = (parmas) => { /** * 根据不同的url接口,来设置不同的header请求头 ** 判断 url中是否带有 /my/ 请求的是私有的路径 带上header token ** { ...parmas.header } ==> 先解构出传进来的header对象,然后再往这个对象里面添加 Authorization字段数据,这样即使有传入header的其他字段也能保留下来 * 如果传入的parmas参数中没有header,那myHeader就是个空的对象 {} 因为啥都没有 */ let myHeader = { ...parmas.header }; //通过includes方法查找字符串中是否包含指定内容,进而判断是否要添加token if (parmas.url.includes("/neddToken/")) { // 往myHeader这个对象里插入键值对 带上Storage中存储的token myHeader["Authorization"] = wx.getStorageSync("token"); } //设置基础请求头 const baseUrl = "https://xxxtest" // 返回一个promise对象 return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其解构出的,传入的url:xxx参数 url: baseUrl + parmas.url, /** * !可以设置上默认的content-type,然后再扩展出传入的myHeader,如果传入的myHeader 为空,那header就还是默认的content-type一个键值对 * !{ 'content-type': 'application/json', ...myHeader } ==》 扩展出myHeader这 个对象中的键值对; */ header: { 'content-type': 'application/json', ...myHeader }, success: (res)=> { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, }) }) [代码] } [代码][代码] 此时,在使用这个封装的API的时候,就可以对header进行设置了,例如: [代码] ``` cjRequest({ url: '/neddToken/home/swiperdata', // 使用的时候也可以传入一些header的字段 header: { 'content-type': 'application/json', 'Date': 'Tue, 15 Nov 2021 08:12:31 GMT' }, method: 'GET', }).then((result) => { console.log(result) }) [代码] [代码][代码] 此时,因为请求url中含有【neddToken】,就会在header中插入token [图片] 此时,这个请求的header中就会添加上token(也就是Authorization这个字段了) [图片] 4.添加请求发起时页面loading效果 当页面在加载数据的时候,最好要有一个loading的提示,同时有遮罩,防止用户乱点 所以,就需要在封装的API中加入微信小程序的wx.showLoading遮罩层了 [代码] // 显示加载中loading效果 wx.showLoading({ title: "加载中", mask: true //开启蒙版遮罩 }); ... ... // 关闭正在等待loading效果 wx.hideLoading(); [代码] 如果直接在封装的API的开始加上loading,在请求结束加上隐藏loading效果,那乍看一下,好像没错,但是细想一下,如果一个页面同时触发了多个请求呢?比如打开一个页面,同时加载多个模块,需要从不同的接口请求数据,那就会使用多次这个封装的API。 此时,就会出现,第一个请求结束,直接关闭了loading效果,而后面几个请求就没有loading效果的遮罩了。 所以,需要在封装的js文件中设置一个全局变量,每次调用这个封装的文件时,就对这个变量++,每次请求结束,返回数据出去的时候,就对这个变量–,最后判断一下这个变量是否为0(也就是所有请求的结束了),在决定是否关闭loading效果 [图片] [图片] 完整的封装的API 的 js文件 [代码]// 同时发送异步代码的次数 let ajaxTimes = 0; export const cjRequest = (parmas) => { // 当有地方调用请求方法的时候,就增加全局变量,用于判断有几个请求了 ajaxTimes++; // 显示加载中loading效果 wx.showLoading({ title: "加载中", mask: true //开启蒙版遮罩 }); /** * 根据不同的url接口,来设置不同的header请求头 ** 判断 url中是否带有 /my/ 请求的是私有的路径 带上header token ** { ...parmas.header } ==> 先解构出传进来的header对象,然后再往这个对象里面添加Authorization字段数据,这样即使有传入header的其他字段也能保留下来 *? 如果传入的parmas参数中没有header,那myHeader就是个空的对象 {} 因为啥都没有 */ let myHeader = { ...parmas.header }; if (parmas.url.includes("/neddToken/")) { // 往myHeader这个对象里插入键值对 带上Storage中存储的token myHeader["Authorization"] = wx.getStorageSync("token"); } // 基础url const baseUrl = "https://api-hmugo-web.itheima.net/api/public/v1" return new Promise((resolve, reject) => { /** * ...parmas ===>就是将传进来的参数扩展开,一行行展示在这里面 * 比如:传进来 * { * url:'xx', * data:{key1:val1,key2:val2} * } * 那么通过 ...parmas 就会把这些内容展示到这里了 */ wx.request({ ...parmas, // 注意,此行必须放在 ...parmas 之下,才能覆盖其传入的url:xxx参数 url: baseUrl + parmas.url, /** * !可以设置上默认的content-type,然后再扩展出传入的myHeader,如果传入的myHeader为空,那header就还是默认的content-type一个键值对 * !{ 'content-type': 'application/json', ...myHeader } ==》 扩展出myHeader这个对象中的键值对; */ header: { 'content-type': 'application/json', ...myHeader }, success: (result) => { // 请求成功,就将成功的数据返回出去 resolve(result) }, fail: (err) => { reject(err) }, // 不管请求成功还是失败,都会触发 complete: () => { /** * !loading效果同时被多个请求触发是可以显示一个的,但是关闭loading一旦被第一个请求完成后关闭,后面的请求触发的loading效果就没了 * !所以,需要通过全局设置一个变量,来监听同时触发了几个请求,当最后一个请求完成后,再关闭loading * ?每次结束请求后,就减少全局变量,当为0时,就表示这是最后一个请求了 */ ajaxTimes--; // 此时就可以关闭loading效果了 if (ajaxTimes === 0) { // 关闭正在等待loading效果 wx.hideLoading(); } } }); }) } [代码] 如果需要按照原生微信请求的格式来封装(调用时要传各种方法、参数),请参考: 微信小程序-封装请求API——原生方式_五速无头怪的博客-CSDN博客[图片]https://blog.csdn.net/black_cat7/article/details/120697607 参考:黑马微信商场小程序 黑马程序员微信小程序开发前端教程_零基础玩转微信小程序_哔哩哔哩_bilibili[图片]https://www.bilibili.com/video/BV1nE41117BQ?p=3
2023-10-18 - Skyline | 它超赞耶!Skyline 商用初体验 -- NONZERO COFFEE
抛开软件功能不谈,在交互体验上,打造一款能和原生APP相媲美的微信小程序,是我这样的微信开发者的梦想。想在技术上想实现原生的功能,开发成本是非常巨大的。但感谢微信小程序推出的渲染引擎Skyline,让我可以轻松实现原生app的交互体验。 目前,小程序已上线运行半月,虽有bug,但瑕不掩瑜。如果您对于使用webView开发还是Skyline开发还有疑虑的话,希望以下介绍对于有选择困难症的同学们有所帮助。接下来,我想借助自己开发的小程序,向大家推荐与介绍Skyline渲染引擎(当然也希望向大家推荐我所开发的小程序 -- 「 NONZERO COFFEE 」)。 如有错误或遗漏,欢迎在评论区批评指正,不胜感激。 小程序效果演示 [图片] 对小程序感兴趣的也可以加我好友讨论技术: [图片] 开发文档中对于Skyline的介绍,有以下几个重要的增强特性:worklet 动画、手势系统、自定义路由(预设路由)、共享元素动画。对于他们的使用和说明,我就不过多说明了,具体可以参照:https://mp.weixin.qq.com/s/dRz2PnkwHxYVL2kCexQ7WQ。 接下来我会介绍这些特性在小程序中,是怎么变成我的奇淫巧技,让小程序也可以更加优美。 worklet 动画:丝滑与复杂的页面元素展示 当页面滚动时,我们通常会希望根据页面不同的滚动状态或位置,灵活的展示或改变某些元素的状态,那么借助scroll-view的worklet:onscrollupdate时间和worklet动画可以完美实现效果~ [图片] 1、展示中可以看到背景图片会随着下拉放大,但是上滑时却是跟随上滑,完美实现不同状态 2、为了优化顾客点单体验,主要菜单栏(点单、外卖、商城)需要在上滑至离开屏幕时,顶部需要弹出备用菜单栏. 以上效果如果通过简单的wx:if判断,那么动画效果势必会很突兀,如果使用worklet 动画的timing()与spring()函数,可以轻松实现动画的流畅效果。 附上简单代码: 这里是置顶的点单菜单栏 这里是页面内容和 onLoad(options) { // 背景图片的高度和缩放 this.bgimgTop = shared(0) this.bgimgSca = shared(1) // 顶部点单栏透明度 this.topfunOp = shared(0) }, onReady() { // 背景图片动画监听 this.applyAnimatedStyle('.bgimg', () => { 'worklet' return {top: `${this.bgimgTop.value}px`,transform:`scale(${this.bgimgSca.value},${this.bgimgSca.value})`} }) // 顶部点单栏动画监听 this.applyAnimatedStyle('.fixtopview', () => { 'worklet' return {opacity: `${this.topfunOp.value}`} }) }, scrolling(e){ "worklet" let scrollTop = e.detail.scrollTop if(scrollTop <= 0){ //向下滚动 this.bgimgTop.value = 0 this.bgimgSca.value = 1 + (-scrollTop / 800) this.topfunOp.value = 0 }else{ //向上滑动 this.bgimgTop.value = - scrollTop this.bgimgSca.value = 1 let opTemp = (scrollTop / 200) this.topfunOp.value = opTemp >= 1 ? 1 : opTemp } }, Tip:由于代码量较大,以下说明不会附上详细代码,待后续整理好源码,会开源的 手势系统:提升用户留存率,神之一手 [图片] 这个实例呢,和第一个非常相似,是因为其中也 用到了worklet动画,换句话说,如果使用了Skyline框架,那么worklet将萦绕整个开发进展,它即代替了wss和css动画。 另外,更重要的是这个实例运用了手势系统,概而述之,就是使用<pan-gesture-handler><vertical-drag-gesture-handler>去更精细化的代理使用和监听<scroll-view>组件。 一、首页由<pan-gesture-handler>判断页面状态,状态1:往上拖动,不处理滚动事件,而是负责顶部搜索按钮和搜索框的动画效果、背景图片的缩放效果、以及整个容器的高度变化;状态2:向下滑动,则处理滚动事件 二、当判断向下滑动,处理滚动事件时,先由<vertical-drag-gesture-handler>判断<scroll-view>是否触顶,接下来也有两种状态,状态1:触顶,则改变由<vertical-drag-gesture-handler>接管,处理容器下滑的动画;状态2:没有触顶,则是<scroll-view>的正常滑动。 自定义路由 :小程序焕然一新,媲美原生 示例一: [图片] 这个详情弹出页的效果和webview下的 <page-container>组件的效果基本相似,如果这里简单的使用跳转新页面,则不符合UI设计稿,使用页面弹出组件时,又会发生用户的返回操作直接退出商品列表页,所以这里使用的自定义路由,并且还利用了worlklet和手势系统,实现了手势下滑与左滑时,如果下滑距离和下滑速度足够,则退出,不足够,则回弹恢复。 示例二: [图片] 这里的自定义路由,效果不明显,但是恰恰就需要这种效果。简单来说就是用户点击搜索框,需要跳转到搜索页面,但是设计稿中需要这种跳转是无感的,而自定义路由中将transitionDuration跳转时间设置为10或者更短,并且将handlePrimaryAnimation跳转效果设置成空值,即可完美实现。以下是简单的自定义路由配置代码: const ShopSearchRouteBuilder = (customRouteContext) => { console.info('skyline: half page route build') /** * 1. 手势拖动时采用原始值 * 2. 页面进入时采用 curve 曲线生成的值 * 3. 页面返回时采用 reverseCurve 生成的值 */ const handlePrimaryAnimation = () => { 'worklet' return { } } return { opaque: false, handlePrimaryAnimation, transitionDuration: 10, reverseTransitionDuration: 10, barrierDismissible:true, canTransitionTo: false, canTransitionFrom: false, } } 共享元素 :我的奇奇怪怪的使用 [图片] 如果没有看出这个例子的共享元素是如何使用的,那么我用webview的页面来分解一下。 [图片] [图片] 这里的商品列表页和购物车其实是两个页面,但是底部的购物车按钮元素,又需要在两个页面跳转时,无感展示,所以这里是最适合使用共享元素的,我个人认为,这比跳转页面时使详情图片在不同页面间飞跃更加高级和实用。 但是既然说到了图片飞跃的效果,下面我也来展示几个实用例子: [图片] [图片] 这里的两个例子也都是列表页跳转至详情页时,实例图片的飞跃。 当然小程序中的动画效果可不止以上罗列的这么几个,总而言之,使用了Skyline之后,以上的动画效果在小程序中随处可见,如果不是本人的代码能力有限,我想小程序的整体效果完全可以媲美APP的原生效果。 希望以上这些能够为即将进行微信小程序开发,而苦恼于是否使用Skyline的同学们,带来新的思路。 Skyline的bug:背刺与惊喜 新技术需要有勇气去使用,去克服遇到的一切问题我遇到了很多bug,虽然他总会在我意料不到的地方背刺我一刀,但是Skyline的效果实在让我欲罢不能,万分庆幸的是我真的要非常感谢微信团队的老师们,给了我很多建议与帮助。 还没有经过老师同意上图,暂时先打个码。 [图片] [图片] 好了,接下来说一下,现在还没有解决的bug,希望微信团队能尽快修复。 1、小程序使用的是自定义tabbar,在ios下,相隔较长时间再重新打开小程序,会出现白屏的现象,是完全不显示任何元素,但vconsole是可以看到各生命周期已经正常允许了的。这里实在不好上传相关代码,因为首页代码很复杂。还关联了全局状态管理。 与其他同学沟通过,发现并不是我的单一案例,也有人复现了。 解决方案:万幸我使用的是自定义tabbar,小程序启动首页被我设置成了某一个单页面,然后从单页面跳转到tabbar首页,并且这个单页面还能设置成宣传海报首页,这也算是一个自洽的解决方法。 2、wx.switchTab()方法:前提依旧是自定义tabbar,假设tabbar有A、B、C、D首先进入了tabbar首页A页面,然后跳转到其他页面,如果这时使用wx.switchTab()跳转至B\C\D页面,会出现tabbar栏不出现的情况。 解决方案:全部跳转至A首页,或者tabbar页面底部安置一个假的tabbar栏。 3、最麻烦的问题:如果我不考虑兼容的问题,那么即代表我需要抛弃一部分没有升级微信版本习惯的用户,因为低版本的微信无法正常使用Skyline框架开发的小程序。 解决方案:如果您的开发成本和时间足够,完全可以慢慢兼容、如果还在纠结,建议您单页面慢慢升级Skyline,Skyline支持单页面渲染(这可真是太棒了),但是对于我这种有代码强迫症的人来说,这是硬伤,我不能忍受一个页面是webview,另一个是skyline,我只能慢慢熬过用户更新的阵痛期。 最后,我还想说几句话: 1、开源这件事,我已经做好了准备,但是开源成本太大,我还没考虑好是否花时间进行开源,总之,有需要的同学,可以写个评论支持下。 2、开发: (1)前端:有80%以上的图片与文字,都是通过后台数据渲染的,也就是说部署后,基本不需要修改小程序代码,但是自由度极高。 (2)后台框架:考虑到是小项目,只使用了java的springboot,数据库使用的是mySql,缓存则也是通过代码实现的(不使用redis,是因为云托管不支持,单独买太贵了),存储使用的腾讯的COS。 (3)后台部署:我抛弃了传统服务器,拥抱了微信云托管。 (4)管理admin页面:使用的是antd admin(vue2),部署方面同样也是使用的云托管的静态资源存储。
2023-10-10 - 小程序隐私协议开发指南仿微信读书授权DEOM示例
一:效果预览图 [图片] 二:代码片段 https://developers.weixin.qq.com/s/UuaRwcm874LW 三:参考文献 https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html 一、功能介绍涉及处理用户个人信息的小程序开发者,需通过弹窗等明显方式提示用户阅读隐私政策等收集使用规则。 为规范开发者的用户个人信息处理行为,保障用户合法权益,微信要求开发者主动同步微信当前用户已阅读并同意小程序的隐私政策等收集使用规则,方可调用微信提供的隐私接口。 特别注意: 以下指南中涉及的 getPrivacySetting、onNeedPrivacyAuthorization、requirePrivacyAuthorize 等接口,目前仍处于 Beta 调试阶段,目前并未按预期返回正确的结果。开发者目前可以先阅读本指南文档和接口文档进行理解,平台将会尽快正式上线这些接口以及调试方法,上线后会在本指南文档和相关公告中进行说明。 2023.08.22更新: 以下指南中涉及的 getPrivacySetting、onNeedPrivacyAuthorization、requirePrivacyAuthorize 等接口目前可以正常接入调试。调试说明: 在 2023年9月15号之前,在 app.json 中配置 [代码]__usePrivacyCheck__: true[代码] 后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。在 2023年9月15号之后,不论 app.json 中是否有配置 [代码]__usePrivacyCheck__[代码],隐私相关功能都会启用。 <view wx:if="{{showPrivacy}}"> <view>隐私弹窗内容....</view> <button bindtap="handleOpenPrivacyContract">查看隐私协议</button> <button id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyAuthorization">同意</button> </view> 四:参考示例 [图片] 五:注意事项 如果使用了 button 组件 open-type 设置了 getPhoneNumber 属性, bindgetphonenumber 。当你新增了属性功能 "__usePrivacyCheck__": true, 那么 授权 wx.getPrivacySetting 调用成功,但是当你未授权或取消时, 切换使用 button 组件 open-type = getPhoneNumber ,可能调用失败,可以参考如下设置代码 ,避免出现 {errno: 104, errMsg: "privacy permission is not authorized"} openType:'getPhoneNumber|agreePrivacyAuthorization' // 示例 if(wx.getPrivacySetting) { this.setData({ openType:'getPhoneNumber|agreePrivacyAuthorization' }) }else { this.setData({ openType:'getPhoneNumber' }) }
2023-09-13 - 【日常杂记】小程序源生icon不垂直居中问题
[图片] 原生icon不居中怎么解决? <icon class="icon-small" type="circle" size="40rpx"></icon> 你会发现 无论是 flex布局 还是 vertical-align: middle;都无法解决莫名其妙多出来的高度 这时候可以这样:直接写死高度 [图片] [图片]
2023-09-18 - 体验skyline模式,写一个国庆头像生成小程序
小程序演示 [图片] 截图 [图片] [图片] 上手指南 该小程序使用了微信的Skyline渲染引擎,效率更高体验更好,可以实现类似于flutter Hero动画(见上图效果)。头像生成使用的是Snapshot进行合成,这个东西很方便,不用使用canvas进行繁琐的绘制处理,只要把布局写好就行,类似html2canvas。 官方[代码]onChooseAvatar[代码]接口获取头像的接口实在太模糊没法使用,因此换成了[代码]wx.chooseMedia[代码] 注意:由于使用了一些新特效所以项目无法在WebView下运行,需使用Skyline引擎,如果考虑到用户兼容性的慎用。 开发前的配置要求 微信版本库 3.0.0 安装步骤 克隆或下载本项目到目录中使用微信开发者工具导入即可 [代码]git clone https://github.com/QQOQ/mp-avatar.git [代码] 修改根目录下的[代码]env.config.js[代码]文件,里面配置你的接口地址 在[代码]app.js[代码]处有一个请求素材的接口,可以修改此处为你的接口,返回格式如下: [代码]{ "default_template": "https://51porn.oss-cn-hangzhou.aliyuncs.com/hat5.png", "default_avatar": "https://51porn.oss-cn-hangzhou.aliyuncs.com/demo.jpg", "template_list": [ "https://51porn.oss-cn-hangzhou.aliyuncs.com/hat0.png", "https://51porn.oss-cn-hangzhou.aliyuncs.com/hat1.png" ] } [代码] [代码]default_template: 默认模板 default_avatar: 默认头像 template_list: 模板列表 [代码] 特别说明 该项目素材均来自于互联网,如有侵权请联系本人删除。
2023-09-27 - 推荐 7 款小程序产品运营必备的官方小程序
前言 接触小程序从2018年到现在已经5年多了,从最开始进入公司管理小程序团队,到后来全职做一名小程序独立开发,再到现在创业组建一个小团队,大大小小的小程序累计加起来差不多100多个,在这个过程中这 7 个官方小程序一直在用。 小程序 以下均为微信官方小程序,直接在微信搜一搜名字即可查到。 微信指数 [图片] 微信指数小程序对我来说使用频率最高了,无论是小程序方向的决策还是公众号文章内容方向的决策我都会先查找一下微信指数,如果内容有热度才能去做,否则做了也白做。 [图片] 使用非常简单,只需要输入关键词即可。能够查看这个词的指数趋势以及数据来源。 [图片] 小程序助手 [图片] 可以看作简化版的小程序管理后台,我用的最多的是审核管理。很多时候代码提交审核了,版本通过了,我人在外面,不方便用电脑,这个时候我就直接用这个小程序进行版本发布,非常方便。 [图片] We分析 [图片] 当小程序上线后,数据分析是必不可少的。从最开始的小程序数据助手到现在的We分析,我几乎每天都会查看数据分析数据来源,这样可以及时的去调整我的运用策略和发现程序问题。 [图片] 微信开放社区 [图片] 只要涉及到微信相关的问题,大部分都能在这个社区得到解决,在这里有很多常见问题的解决方案也有官方最新动态以及提出问题后能得到官方人员和贡献者的解答。 [图片] 我也是贡献者之一,截止到现在回答了3000多个问题和分享了100多篇文章。 [图片] 公众平台助手 [图片] 我主要是用它来查看公众号的数据统计。 [图片] 如果你有发布公众号的需求可以下载官方的【订阅号助手】App。 流量主服务助手 [图片] 小程序盈利模式无法就是广告模式或者付费模式,如果是广告那就用这个小程序。 [图片] 微信支付商家助手 [图片] 付费模式就用这个,在这里还可以直接解决客户投诉问题。 [图片] 最后 你在小程序开发过程中还有什么实用的小程序?欢迎留言一起讨论
2023-09-24 - Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - 小程序主体经银保监会批准,与保险中介公司合作开展保险业务,是否可以通过审核?
2019年11月7日,行运伞小程序版本在发布审核后,系统后台一直提示“你好,你的小程序涉及在线办理保险业务,请补充选择:金融业-保险类目” 现就该小程序说明如下: 行运伞为上海昱熹网络科技公司旗下微信小程序。上海昱熹公司与中盛国际保险经纪有限责任公司在银保监会进行了互联网保险销售资质备案,并在中保协进行了信息披露,披露结果及合作协议,合作模式如图 [图片] [图片] [图片] [图片] 目前银保监会已允许中盛国际在该平台进行保险销售,中盛是专业的保险中介公司,中盛的保险经纪业务许可证等保险销售资质一应俱全,中盛与上海昱熹进行合作,利用行运伞开展互联网保险销售。这种情况, 怎么可以审核通过?如需提供其他证明资料,还请指明。已反馈邮件至miniprogram@tencent.com。
2019-11-08 - 2023品牌新媒体矩阵营销洞察报告:流量内卷下,如何寻找增长新引擎?
[图片] 近年来,随着移动互联网的发展渗透,短视频、直播的兴起,新消费/新零售、兴趣电商/社交电商等的驱动下,布局线上渠道已成为绝大多数品牌的必然选择。 2022年,越来越多的品牌加入到自运营、自播的行列中,并且从单一的平台逐渐拓展到多平台,矩阵化打造,实现从品牌曝光、内容种草到消费转化、用户沉淀的营销闭环。 各短视频、电商平台也陆续推出诸多政策与活动,鼓励、扶持品牌商家自播,助推品牌在社媒平台长效经营。 然而面对线上流量红利的逐渐消退、获客成本的不断上涨,品牌如何通过布局矩阵,完成渠道建设,实现全域经营、业绩增长?从流量到留量,如何深挖用户价值,实现高效转化?面对海量的新媒体运营资产,如何科学、智能、精细化管控,赋能数字化转型、提升运营效率、降低运营成本,从而实现品牌企业社媒运营的持续发展? 为此,果集·云略推出《2023品牌新媒体矩阵营销洞察报告》,希望能给各品牌企业带来些许参考。 【云略Pro】公众号回复“品牌矩阵” 获取完整版报告 1、行业发展概况 (1)用户规模不断增长 电商市场前景可观 据中国互联网络信息中心(CNNIC)发布的第51次《中国互联网络发展状况统计报告》显示,截至2022年12月,我国网民规模达10.67亿,其中,短视频用户规模达10.12亿,同比增长7770万,占网民整体的94.8%。网络直播用户规模达7.51亿,同比增长4728万,占网民整体的70.3% 另一方面,据国家统计局数据显示,2022年,全国网上零售额137853亿元,比上年增长4.0%。其中,实物商品网上零售额119642亿元,增长6.2%,占社会消费品零售总额的比重增长至27.2%。 2022年中国直播电商市场规模超过3.4万亿,年增长率为53%,预计2023年规模将超4.9万亿。眼下疫情全面放开,经济逐步恢复,而伴随着短视频、直播用户规模的逐年增长,未来社媒电商市场前景可期。 (2)电商市场规模同比上涨84% 社媒平台持续火热 2022年,社媒平台持续火热。抖音、快手整体交易规模超万亿,同比上升84%,销量同比增长91%。 随着疫情的全面放开,2023年Q1再创新高,交易规模同比上升60%。视频号直播带货规模同样保持高速增长,销售额同比增长超8倍。小红书电商主播数量同比增长 337%,直播场次同比增长 214%。 (3)AI / 5G / VR 等新技术 为行业发展注入新的动力 得益于新技术的发展,给社媒电商行业带来了更多可能。如可运用在文案内容、模特图片制作、短视频制作、智能客服等方面,更好地实现降本增效。 此外,AI 与虚拟主播的结合,也日渐成为一种趋势。此前爆火的柳夜熙就深受大家的喜欢,也证明了其可能性。 2023年,AI 浪潮风起云涌,而社媒电商与 AI 等新技术的结合,也将为行业带来新的发展点与机遇,打破行业壁垒,实现新的突破。 [图片] 2、品牌布局矩阵的必要性 (1)用户消费时长增长 短视频直播成品牌营销新阵地 短视频、直播行业飞速发展,用户规模和消费时长不断攀升。尤其是短视频,近年来持续领跑用户时长占比,且持续保持较高增长率。短视频、直播日益成为企业树立品牌形象、内容营销种草、获取用户心智、业绩提效增长的重要渠道,从而实现可持续发展。 [图片] (2)品牌加速入场 多平台矩阵运营成共 随着互联网行业不断向纵深发展,内容形态与营销场景也更加多元化。越来越多的品牌跑步入场,深耕社媒营销,建立多平台营销矩阵,借助社媒平台的全域态势助力品牌增长。 据果集数据显示,目前有近23%的品牌选择跨平台运营。其中抖音与其他平台的组合是品牌首选的。 [图片] (3)品牌自播成风口 直播场次与销售业绩稳定增长 电商直播日益成为品牌社媒运营的重点。越来越多的品牌并不单纯地依赖和追求头部主播的强带货效应,而是逐渐发力品牌自播,通过店铺的常态化运营,做好客群关系的维护,及时获取用户反馈,以便更好优化选品与内容策略,打造品牌长效经营的阵地。 据果集数据显示,2022年全年抖音、快手品牌自播场次较2021年增长62.48%,品牌自播GMV增长56.68%。 (4)多渠道品牌营销推广 实现从种草到购买闭环 在移动互联网时代,市场细分越来越垂直,充分借助社媒平台,拓展多渠道营销,多触点发力,构建多维度营销矩阵,沉淀与品牌调性相符的渠道价值,实现销量增长、营销破圈、用户沉淀等核心诉求,才能更好做到品效合一。 3、品牌矩阵营销洞察 品牌案例—— YAYA 鸭鸭 (1)建立品牌直播矩阵 承接流量圈定精准人群 2020年6月,鸭鸭品牌入驻抖音,依靠与达人直播合作的模式快速实现冷启动。2020年11月,开始尝试品牌自播。2021年5月打造品牌店铺矩阵,通过裂变式店群,圈定精准人群,发力品牌自播。同年9月,入驻快手并开始直播。到了2022年,觉察到视频号电商的机会后,鸭鸭在视频号也开设了品牌自播。 [图片] (2)平台差异化定位 品牌自播贡献主要销售额 YAYA鸭鸭在社媒平台采取的是“自播+达播”的模式,但也会根据各平台的属性差异,运营的侧重点也略有不同。 比如抖音平台,前期依靠达人直播分销冷启动,但到了后期品牌自播的投入不断增加;而在快手上,“老铁”的私域特征更为突出,在信任电商下,达人分销比重会更多。 (3)多元场域联动提升品牌认知 主推明星单品打造超级爆款 [图片] 品牌案例—— Midea 美的 (1)线上数字化转型 实现品牌全域协同经营 近年来,随着社媒平台的快速发展,用户的注意力和消费习惯也逐渐往线上迁移。不少传统商家开始线上化、数字化转型,由原来单一的线下门店经营转向线上线下双驱动的模式,加速全渠道布局,努力寻找生意增长的新引擎。 从全网布局来看,美的品牌账号覆盖主要社媒平台、主要品类,“种+拔”一体化全链路布局,通过跨平台矩阵、账号矩阵的多种营销场景,实现品牌全域协同经营。 [图片] (2)加速布局品牌自运营体系 探索生意增长新引擎 高速发展的直播电商,让不少商家找到了新的发展机会。近年来,美的品牌加速布局直播电商自运营体系搭建,发力内容场景、货架场景、营销场景3大锚点,整合全场景,打造流量闭环渠道,实现深度转化与长效增长。 2022年全年同比2021年全年,美的品牌在抖快GMV增长66.64%,尤其是品牌号自播带来的销售明显增长,抖音渠道品牌号自播占比从37%增加至82%。 [图片] (3)破圈营销打造年轻潮流生活方式 节点营销引爆热点话题带动销量提升 [图片] 4、品牌矩阵搭建策略 与解决方案 什么是品牌新媒体矩阵? 能够触达目标群体,满足品牌销量增长、营销破圈、用户沉淀等核心诉求的多种新媒体渠道组合。 (1)常见的账号矩阵搭建结构 [图片] [图片] (2)品牌矩阵搭建策略 [图片] (3)品牌自播体系搭建策略 [图片] (4)各社媒平台特点 [图片] (5)组合拳出击 实现品效域合一 [图片] (6)品牌矩阵布局存在的问题 及解决方案 [图片] [图片] 因为篇幅有限,推文仅展示部分数据,【云略Pro】公众号后台回复“品牌矩阵”,可获取完整版PDF报告。
2023-06-30 - We分析 · 小程序推广分析 能力更新
推广分析是We分析推出的一项经营工具。可以帮助商户监测带参数的小程序页面路径数据,并便捷生成带参数推广素材。将推广素材投放至不同的推广渠道和应用场景后,可在We分析上查看不同推广计划的引流效果数据。 功能路径:【We分析-经营工具-推广分析】 [图片] 应用场景 [图片] 能力更新 1. 推广交易数据监测:访问数据基础上支持统计通过推广引流人群的交易数据,商户可自定义归因区间,确定合理的统计口径。例如,归因区间设定的为7天,该用户在访问推广计划后7天内所发生的交易转化都会归功于该推广计划。(仅开通支付的商户支持该能力) [图片] 2.素材类型拓展:创建推广计划后,根据需求可生成以下类型素材,支持批量生成。 [图片] 3.推广标签设置:可对推广计划所属标签进行标记(例如,为所有朋友圈广告渠道的计划打标),每个推广最多添加3个标签,可在推广看板中根据标签进行数据查看,对比分析不同标签的推广效果。 [图片] 4. 人群分析优化: (1)推广人群画像优化,支持查看引流整体人群、引流新用户、交易用户的基础画像; (2)可快捷创建推广计划的引流人群包,在【画像洞察】、【行为分析】中对引流群体做进一步的分析。 [图片] 详细可查看开放文档:使用指引。【推广分析】持续迭代中,关于【推广分析】的需求与反馈可加入下方交流群与我们反馈。 [图片]
2023-06-14 - 如何实现动态生成好友分享图
本文章采用官方最新的 Canvas.createImage() 来实现下动态生成好友分享图,可以拿来即用。展示效果如下(其中蓝框中文案和红框头像为插入的文本、图像。背景图也支持动态更换): [图片] 小程序demo案例:https://developers.weixin.qq.com/s/nJtr4QmL7RD3 一、市面案例缺陷:翻阅了目前市面上的小程序动态生成好友分享图,大部分还是使用已废弃的『wx.createSelectorQuer』接口来实现。目前小程序已无法很好支持。 二、主要有以下几个关键点需要注意下: 做好友分享图要考虑5:4的比例使用 wx.getImageInfo 一定要考虑图片失败的场景,然后采用兜底图片。相同的逻辑在complete中执行。要区分 wx.createImage ,这个是小游戏用来创建图片对象的。小程序要用Canvas.createImage()。也不是使用 new Image!!!使用的时候一定要在 canvas 类型中注明 type 是 2d 的 canvas[图片] 三、优化知识点: 如何用 async in image loading:https://stackoverflow.com/questions/46399223/async-await-in-image-loading ----- 采用await img.decode() 或者 img.onload = () => resolve() 如何隐藏Canvas:https://developers.weixin.qq.com/community/develop/doc/1aadfacdd9f38584881e0c50db2bcda1 ----- position:fixed;left:100%;
2023-06-19 - 我是如何从网瘾少年变成程序员的?
从网瘾少年到从网瘾少年到流水线工人流水线工人从网瘾少年到流水线工人 2008年的我很顽皮是一名典型的网瘾少年。还记得当时为了做每天的游戏任务,早上5点钟就从被窝里爬起来去网吧上网,上两个小时到7点钟再去学校上课。 [图片] 当时的我学习成绩特别差,初中升高中的时候,没有考上当中的重点高中。那个时候就萌生不想读书的想法,不管家人怎么说都铁了心不愿意再读书,于是老爸提出,只要我乖乖听话去上学,就买一台电脑给我。当时拥有一台电脑可以说是我最大的梦想,所以我选择了一所职中,专业是电子电工。 [图片] 现在还依稀记得学习内容是:认识元器件,焊接,排线,修电视机,修电冰箱,制作电路图等等。 [图片] 学习两年之后,2010年开始进入工厂实习,工资一月600元,当时觉得还挺多的。因为自身玩心太重导致干了半个月就没干了。后来去了东莞,由于没有成年的原因,进工厂只能交中介费,交了200元才能找到一份工作,做一名流水线工人,一条线十多个工作位,我负责的是用电烙铁将一个电阻焊到一块电路板上面。 [图片] 由于刚进工厂,工作时候动作不够熟悉,做起来很慢,导致后面的人没有事情做,影响到了整条线的生产速度,那段时间经常被小组长骂。 在工厂做了快一年,直到有一天,我无意中问了问带我做事情的师傅。 [图片] 就这段无意的对话使我陷入了思考,那年我正好17岁,3年后就是这位同事的状态,仿佛看到了自己的未来,这并不是我想要的。接着我就拿起了手机给我爸打了一通电话,说自己想回家继续读书。 我爸很支持我,没过多久我就提了辞职报告收拾东西回了家。现在聊起当年的经历我爸说当年很多亲戚其实都是不支持我回来的,在我家那个小地方,17岁能赚到3000多已经很不错了。加上从小学习成绩就不好,再读书肯定不行,指不定学费白花了,还不如好好在工厂打工。 [图片] 从流水线工人到程序员 2011年回到家后,我去长沙找了一家学校,学习了自己想学的编程。 说句题外话,我为什么想学编程? 小时候非常喜欢玩游戏,一名准网瘾少年。玩过很多游戏,如:泡泡糖,传奇,私服,魔兽争霸,魔兽世界,跑跑卡丁车,冒险岛,梦幻西游,QQ炫舞,DNF ..., 用过一些游戏外挂,觉得好牛,以为学习编程就能做外挂了。(结果你懂得!) [图片] 经过两年的学习之后,我2013年来到了上海。那一天是2013年6月22号,当年的想法是做架构师,踏上长达16个小时的火车来到了上海,一路坎坷事情虽然已过去几年了,但现在我仍然记忆犹新。 [图片] 从学校到职场 21岁-23岁(2013-2015),从Android开发到移动端负责人,开始带团队。 在去上海找工作之前,我加了很多技术交流群,问过里面参加工作的人,面试的常见问题,还请在里面关系不错的给我做了模拟面试。 2013年6月,我从长沙到上海来的时候,面试的情况还算比较不错,用了 3 天面试 7 家公司,拿到了4个offer。 我选择了一家小型公司,公司的产品是一款语言助手APP,上千万用户量。 在这家公司我参与了备忘录模块,天气播报模块,帮助事件,查找命令,来电播报,短信播报,应用管理模块,联系人模块,摇一摇功能,换肤功能,参与酒店模块以及重构开发。 [图片] 虽然经常加班、但是感觉特别充实,学到了很多!感觉自己就像一块海绵一样,在知识的海洋里贪婪地吸收着。平时我的小组长分配的任务,我都会用最快速度去执行并完成,每次都能够提前完成,再主动找毛毛要新任务,如果没有新的任务则会主动问同事有没有需要帮忙的地方。 我认为,在这个阶段让我懂得了。 无论做什么任务都要全力以赴,虽然每次安排任务会越来越多,但是不要怕做的越快任务越给越多,要知道实践的越多学习的越多,对自己成长越大! 从技术到管理 23岁-25岁(2015-2018),进入一家创业公司,从0到1搭建研发团队。 那个时候公司一共就30多人,一个开发也没有,大部分都是销售,业务是做高端婚礼会馆,老板想做自己的系统,觉得系统能赋能业务。我的想法就是试一试,大不了重新找工作,于是拿着身份证就办理了入职手续。 当时办公场地在南京步行街和公司租的婚礼场地在一个地方,于是老板随便找了一个小办公室,搞了网线,我自带了电脑。这就是我的办公场所了。 初期最难的是招人,最开始就是叫朋友过来看看聊一聊,前几个人都是朋友叫朋友的,开发圈子里面都有些熟人。花了一个多月团队也初步成型,移动端,后端,设计都有1-2人了(早期只做App所以没有招聘前端人员)。 由于没有产品经理就自己每天和老板沟通需求,自己找了个原型图绘制软件把想法画出来,然后不断的和老板探讨。开始开发,由于团队刚开始磨合花了不少时间,所以开发时间也评估少了,只能加班搞。花1个月的时间,做出了系统的第一个版本。上线后天天加班改功能,因为需求只和老板讨论过,而没有和实际使用的业务讨论过,很多功能与业务场景不符合,想法过于美好,考虑不完善。在没有招到产品之前,我承担了产品的角色,前往业务一线天天和业务人员一起讨论,最后也做出了业务人员满意的产品功能。 [图片] 过了两年后,公司从原来的30多人到了300多人。开发团队也跟着不断扩大,从原来的几个人变成了三十多人,管理难度也大幅度提升了。从原来的几个人都坐在一张桌子上,一起吃饭聊天到现在坐满了一个办公室。这个时候制度就很重要了,所以开始制定了一些制度,还有一些奖惩机制,以及找老板要了每个月都活动经费。这个时候我开始看很多管理方面的书籍、还上了很多管理到课程(线上、线下都有)、以及还请身边带团队的朋友吃饭,请教一些带团队的问题。 老板也给了我了足够大的自由空间,让我搭建了整个研发团队的制度、流程、福利。打破了我固有的技术思维,让我学习了商业思维,带我去上线下管理课。 我认为,在这个阶段有两点对我成长帮助很大。 自己当产品的经历。一定要去了解业务一线,老板看到是方向。业务一线才能提出真实需求。要学会提取符合业务产品的需求然后往老板的方向去做,这样才是对的。否则做出来业务人员不用,就是白做了。 思考问题的思维的转化。我的思考维度从“如何做,能不能做?”到“为什么做,带来什么样的价值?”让我从技术人员的思维转换成了管理者思维,带我思考产品的商业价值。 从上海到长沙 25-28岁(2018-2021),进入在线教育公司,担任事业部负责人,带领30多人团队做公司核心业务。 从上海带了一支小团队谈好条件一起来到了长沙,第二天就办理入职手续进入工作状态。在这家公司成立了一个新部门,起初是做微信小程序做用户留存,由于上面给的压力较大加班非常疯狂,从白天干到黑夜,累了回去就睡,起来就继续干。 期间我们尝试了很多种不同的类型:答题、直播、视频、社区、工具矩阵等等,大大小小做了几十款小程序。后来又从to C 转向 to B业务,虽然尝试了很多类型的to C的小程序产品,但是从数据的角度来说并没有被公司认可,反而在to B内部系统中获得了不错的评价。从2018年到2019年,一年多的时间过去了,当时做的业务稍微有些起色的时候,被组织架构调动了。 组织架构大调整,资源重新分配,从最开始一个人带着5个人搭建团队用了一年多的时间到了20多人,后来资源重新整合两个部门合并。我又重新回到了一年多钱多状态又从5个人开始带起,做了一个公司的创新业务,主要和广告投放相关的业务。 从2019年到2021年,经过一年多的不断迭代,反复的和业务摩擦,所负责的团队业务越来越广,团队也随之越来越大从5人增长到了30多人,业务也从边缘做到核心,覆盖100%公司的流量入口。 [图片] 在这家公司做了3年多,从2018年3月14号入职到2021年6月22号离职。目前在创业中,围绕微信生态探索更多的可能性。 在这家公司我学习到了: 目前是流量为王的时代,有流量就能产生收益,流量是一家公司的生死线,核心中的核心。 有了流量之后,需要去提高流量利用率,做好用户留存,分析用户多个维度的需求,去满足产生新的垂直业务场景。 在一个行业做了几年之后,自己一定要让去理解这个行业,技术人的技术只是基础,行业认知才是放大器,基于行业特性做出来的解决方案才是最有价值的。 业余时间 除了以上的工作经历,我还是一个喜欢用业余时间折腾。 1. 2016年写出知名Android开源库 BRVAH 22k+ stars,同类框架排名第一。 [图片] 2. 2017年启动公众号「码个蛋」,公众号矩阵10w关注。 [图片] 3. 2018年探索微信小程序,目前工具矩阵百万用户,用户访问数千万次。 [图片] 改变自己 [图片] 回想在这些年的过程,其实本质上就是在不断的改变自己。 以前的我,只会一味的阅读技术书籍,沉迷于技术。 以前的我,不喜欢与人沟通,甚至有些偏内向。 以前的我,只会用最快的速度完成安排下来的任务。 以前的我,只关注自我成长。 以前的我,只关注当前要做的事情,只想着怎么做。 以前的我,只关注项目内容的交付。 现在的我,阅读的书籍类型有管理,心理,商业,人文。 现在的我,喜欢和人聊天,倾听,讨论,分享自己的想法。 现在的我,在接到任务之前更多的是思考是否合理,有没有更好的解决方案。 现在的我,关注团队每个人的成长。 现在的我,站在用户角度思考,多想产品价值,先会思考为什么这么做。 现在的我,会从产品到研发再到运营,全面思考上下游的衔接。 这些经历让我懂得了。 1.积极主动做事情,得失不要太看重。 2.要深耕某个行业,不要做全要做精。 3.坚持每天复盘,从反思中不断成长。 以上是我从一名流水线工人到程序员再到一名创业者。一路走来的思考,希望能对你有所启发。 [图片] 最后,再送上一段我很喜欢的话 我喜欢程序员,他们单纯、固执、容易体会到成就感;面对压力,能够挑灯夜战不眠不休;面对困难,能够迎难而上挑战自我。他们也会感到困惑与傍徨,但每个程序员的心中都有一个比尔盖茨或是乔布斯的梦想“用智慧开创属于自己的事业”。我想说的是,其实我是一只程序猿*
2021-09-10 - 如何策划一场运营活动?
前言 对于一款产品来说,想要快速提高一些指标,那么做活动肯定是一种必备的运营手段,那么做运营活动需要怎么做呢? 今天主要讲解做活动的 3 个步骤:活动策划、活动实施、活动复盘,让大家来了解做活动的整体框架。 活动策划 不要为了做活动而做好活动,第一步是搞清楚活动目标,常见的活动目标无非就是这 3 点: 拉新:让更多新的用户知道你的产品 促活:让用户提高产品使用频率和时长 转化:让用户付费,提高当前产品收益 比如:要推广一个课程,价格是 100 元,销售额目标达到 10w。 先来拆一下这个活动目标,销售额达到 10w,属于转化变现环节。量化活动目标,按照课程的售价,要达到 10w 的销售额,需要有 1000 个付费学员,按以往 10% 的付费转化率来算,需要拉新 10000 左右的目标用户。 再根据拉新用户数量=渠道用户数*拉新转化率,拉新转化率的平均数据是 5~10%,那么反推得出,我们要投放的种子流量池需要在 10~15w。 最终得出 3 个关键环节数据,我们需要投放 10~15w 的流量池,拉新 10000 用户,转化 1000 用户,才能完成销售额 10w 的目标。 确定要目标接下就是确定活动方案包含但不局限于,活动目标、活动主题、用户参与路径、目标受众类型、活动亮点等。 确定完活动方案,接下来就要输出更为细节的内容了。如文案内容、海报设计等。在写文案、做海报之前,多看竞品、看头部机构、看大平台同类主题,他们的文案是怎样写的,海报是怎样设计的,整理出来再优化。 最后,做一个时间进度排期,确定每天的工作内容,每天确认是否完成,进行检查。 活动实施 当方案确认完后,那就需要找资源合作方,确保流量是足够的,找合作进行联合推广。确定完合作方,确定宣发日期以及要宣发的内容,避免到活动宣发的时候,对方没有排期而耽误了宣发工作。 活动正式推广宣发时,先不要所有渠道全部推送,建议先推送到某个渠道,看看用户的反馈,如果发现问题,则及时优化,再推广至其他渠道。 使用AB测试,分别让组成成分相同或相似的用户,随机访问 AB 两个版本活动,收集用户数据,最后分析评估出最好的版本,投入使用。 如:将两张海报分别发布到 4 个社群,看不同海报的活动参与人数、拉新人数、推广人数、活动裂变率等,各个数据,来敲定最终的活动海报。 当用户决定付费时,会经历一段犹豫纠结的周期。作为运营,我们要做的就是缩短用户的决策周期,让用户尽快完成付费转化动作。 如:推出了一个 40 元的限时优惠券,同时为了让付费后的用户再分享,又结合了分销,这样用户领取优惠券低价购买后,还可以继续分销挣佣金。 等到 40 元限时优惠券到期后,接着再推出20元限时券,很多用户这个时候发现原来优惠券的确有时间限制,且金额在减少,所以就会赶快购买。 限时优惠券,是很多商家都会用的策略,特别是零售和电商行业,优惠只会让用户感受到低价,但是限时却可以触发用户的厌恶损失心理,从而加快完成付费环节。 活动复盘 活动策划前的第一步是要对目标进行拆解,所以我们在复盘活动时,第一步就需要先回顾目标拆解的每一个指标,然后看每一指标的实际结果,包括完成率、目标差异等。在这一步尽可能根据实际情况来统计。 当我们把最终的数据结果对应到目标拆解的每一环节后,我们就能知道哪一步没有完成目标,哪一步超额完成,以及各自的差异点,然后我们就要来分析这其中的原因。 这一步需要将前面分析的结论沉淀记录下来,提炼有规律的因素,后续做活动策划时做参考。将好的环节做成可执行的规则模版,不足的环节总结教训避免下次再出现。 小结 想要做好一次运营活动需要做到以下 3 个步骤: 活动策划:明确目的、确认方案、准备材料 活动实施:准备资源、观察过程、提高转化 活动复盘:回顾目标、数据分析、总结经验
2022-02-12 - 微信小程序如何实现页面传参?
前言 只要你的小程序超过一个页面那么可能会需要涉及到页面参数的传递,下面我总结了 4 种页面方法。 路径传递 通过在url后面拼接参数,参数与路径之间使用 ? 分隔,参数键与参数值用 = 相连,不同参数用 & 分隔;如 ‘path?key=value&key2=value2’。 案例:A页面带参数跳转到B页面 A页面跳转代码 [代码]goB(){ wx.navigateTo({ url: '/pages/B/index?id=value', }) }, [代码] B页面接收代码 [代码]onLoad: function (options) { console.log('id', options.id) } [代码] 上面的案例是字符串参数,但是很多情况下需要传递对象,如下方代码。 [代码]Page({ data: { userInfo:{ name:'cym', age:16 } }, goB(){ wx.navigateTo({ url: '/pages/B/index?id='+this.data.userInfo, }) }, }) [代码] 如果使用上面同样的方式结构,输出的结果是:[object Object] 这个时候需要先把对象通过JSON.stringify(obj)将 object 对象转换为 JSON 字符串进行参数传递,再到接收页面通过JSON.parse解析使用。 A页面跳转代码 [代码] goB(){ let userStr = JSON.stringify(this.data.userInfo) wx.navigateTo({ url: '/pages/B/index?id='+userStr, }) } [代码] B页面接收代码 [代码]onLoad: function (options) { console.log('id', JSON.parse(options.id)) } [代码] 全局变量 通过App全局对象存放全局变量。 app.js代码 [代码]App({ // 存放对象的全局变量 globalData:{}, }) [代码] A页面跳转代码 [代码]// 获取App对象 const app = getApp() Page({ /** * 页面的初始数据 */ data: { userInfo: { name: 'cym', age: 16 } }, goB() { app.globalData.userInfo = this.data.userInfo wx.navigateTo({ url: '/pages/B/index', }) }, }) [代码] B页面接收代码 [代码]// 获取全局对象 const app = getApp() Page({ onLoad: function (options) { console.log(app.globalData.userInfo) } }) [代码] 存放在 App 全局变量里面,可以被多个页面使用,直接从 App 对象获取即可。这个数据是保持在内测中,每次小程序销毁就没有了。 数据缓存 通过存储到数据缓存中。 A页面跳转代码 [代码] goB() { wx.setStorageSync('userInfo', this.data.userInfo) wx.navigateTo({ url: '/pages/B/index', }) } [代码] B页面接收代码 [代码] onLoad: function (options) { let userInfo = wx.getStorageSync('userInfo', this.data.userInfo) console.log(userInfo) } [代码] 存放在数据缓存里面,可以被多个页面使用,直接用 getStorageSync 获取即可。这个数据是保持在数据缓存中,除非清楚数据缓存或者删除小程序否则一直存在。 事件通信 通过事件通信通道。 A页面跳转代码 [代码]goB() { wx.navigateTo({ url: '/pages/B/index', success:(res)=>{ // 发送一个事件 res.eventChannel.emit('toB',{ userInfo: this.data.userInfo }) } }) } [代码] B页面接收代码 [代码]onLoad: function (options) { // 获取所有打开的EventChannel事件 const eventChannel = this.getOpenerEventChannel(); // 监听 index页面定义的 toB 事件 eventChannel.on('toB', (res) => { console.log(res.userInfo) }) } [代码] 总结 大家可以针对具体业务场景来进行选择合适自己的传参方式。
2022-02-19 - 做微信小程序产品,需要注意这 3 点!
前言 微信小程序已经成为了越来越多企业和个人的选择,因为它可以为用户提供便捷、快速的服务和体验。然而,要想在微信小程序市场上脱颖而出,需要考虑以下 3 点。 1.明确目标受众 在开发微信小程序产品之前,需要确定你的目标受众是谁,并了解他们的需求和行为模式。这有助于在设计和开发产品时提供更好的用户体验和满足他们的需求。 在明确受众的同时需要还需要明确市场需求大小,可以通过【微信指数】小程序来查询需求量。 例:在微信指数小程序输入「微信小程序」查看指数趋势。 [图片] 2.界面与交互 微信小程序的界面设计非常重要,因为它是用户与你的产品进行交互的第一印象。一个好的界面设计应该清晰、简洁、易于导航,,同时提供有用的功能和信息。可以学习微信设计指南,以及参考官方小程序的界面设计,如:微信指数,微信支付,腾讯系列小程序。 微信小程序产品的用户体验至关重要。在开发产品时,要考虑到用户使用的便捷性以及整体交互,尽量减少用户在使用过程中遇到的障碍和问题。 3.最小可行性产品 做微信小程序我不推荐做的像App那样功能全面,一个小程序最好只解决一个问题。第一个版本不需要很完善,做个最小可行性产品(mvp)就可以上线快速验证。 [图片] 在此需要注意安全性和稳定性,微信小程序产品的安全性和稳定性是至关重要的。在开发产品时,要考虑到数据的安全性和系统的稳定性,并采取措施来确保用户的数据不会被恶意攻击或泄露,这个是最基本的保障。 通过上线后用户数据反馈产品需要不断的更新优化,持续提高其性能和用户体验。 推荐使用PDCA循环:Plan(计划)、Do(执行)、Check(检查)和Act(处理) P(Plan)计划,包括方针和目标的确定,以及活动规划的制定。 D(Do)执行,根据已知的信息,设计具体的方法、方案和计划布局;再根据设计和布局,进行具体运作,实现计划中的内容。 C(Check)检查,总结执行计划的结果,分清哪些对了,哪些错了,明确效果,找出问题。 A(Act)处理,对总结检查的结果进行处理,对成功的经验加以肯定,并予以标准化;对于失败的教训也要总结,引起重视。对于没有解决的问题,应提交给下一个PDCA循环中去解决。 以上四个过程不是运行一次就结束,而是周而复始的进行,一个循环完了,解决一些问题,未解决的问题进入下一个循环,这样阶梯式上升的。
2023-05-03 - Skyline|电商小程序 留住用户秘诀
你是否也收到这样的用户反馈? 商品列表滚动区域太小,很难找到想要的商品。头部的搜索广告占据了半个屏幕,挤占了实际空间。在我手机这样小的屏幕上,展示区域太小了,能否把它放大点?[图片] 在电商页面中,我们需要向用户展示众多的商品、广告等信息。 然而,如何在有限的屏幕空间中更好地展示它们,是一个需要我们深入思考的问题。 由于小程序 webview 渲染框架在技术存在一定的局限性,我们需要在不同的设计之间进行抉择。 当广告具有较高的优先级时,我们会考虑突出广告的展示,同时减小商品列表在界面中所占比例。当商品列表具有较高的优先级时,我们会考虑优先展示商品,而放弃广告的展示。[图片] 但是当广告和商品列表同样重要的情况下,要怎么办呢? 常见的一种设计方式是设计一个隐藏按钮,当用户不想看广告的时候把广告隐藏掉,隐藏之后商品列表就有更多展示空间 [图片] 但是这种情况也只是针对愿意手动点击隐藏按钮的情况下,还是有一定的局限性。 那么,有没有办法做到在无形中隐藏广告呢? [图片] 说到这里,当然是可以的啦✌️ 1、吸顶布局 + worklet 轻松实现 在常见的电商小程序首页,通常是顶部展示类目、接着展示商品详情,商品详情顶部也有热门等等的分类。 当页面滚动的时候,我们希望商品详情热门分类可以吸在顶部,便于切换。 [图片] 这里我们用到了 scroll-view 的 sticky-header、sticky-section 吸顶布局容器即可轻松实现。 ... ... ... 当 scroll-view 滚动的时候,根据滚动位置把搜索框放到标题的位置,可以再节省一点空间 attached() { // nav-bar 隐藏或展示 this.applyAnimatedStyle('.nav-bar', () => { 'worklet' return { opacity: this.navBarOpactiy.value } }) // 改变搜索框宽度 this.applyAnimatedStyle('.search', () => { 'worklet' return { width: `${this.searchBarWidth.value}%`, } }) }, // scroll-view 监听函数 handleScrollUpdate(evt) { 'worklet' const maxDistance = 60 const scrollTop = clamp(evt.detail.scrollTop, 0, maxDistance) const progress = scrollTop / maxDistance const EasingFn = Easing.cubicBezier(0.4, 0.0, 0.2, 1.0) this.searchBarWidth.value = lerp(100, 70, EasingFn(progress)) this.navBarOpactiy.value = lerp(1, 0, progress) }, 2、手势 + worklet 操作更灵活 小程序新渲染框架支持了手势系统,手势在这里可以发挥大作用~ 我们可以使用手势协商让小程序页面中的广告、商品等无缝切换和更好的展示 [图片] // .wxml ... // .js handlePan(e) { 'worklet' ... if (e.state === GestureState.ACTIVE) { if (this._interactionState.value === InteractionState.UNFOLD) { // 展开状态下,往上滑才折叠起来 if (e.absoluteY - this._startY.value < 0) { this._interactionState.value = InteractionState.ANIMATING this._translY.value = timing(0.0, { duration: 250 }, () => { 'worklet' this._interactionState.value = InteractionState.RESET }) } } else { // 其它情况,跟随手指滑动 this._translY.value = e.absoluteY - this._startY.value } } // 其他状态下的处理 ... }, 加入手势之后,除了可以隐藏广告,我们还可以将一些头部信息隐藏 在用户查看商品列表时,隐藏大部分无用信息,将商品列表展示区域最大化 // 最外层 .page 往上挪 this.applyAnimatedStyle('.page', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) return { transform: `translateY(${translY}px)` } }) // 改变 .navigation-bar 背景色 this.applyAnimatedStyle('.navigation-bar', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) const opacity = translY / -this._tabsTop.value return { backgroundColor: `rgba(255, 255, 255, ${opacity})` } }) // 输入框:改变宽度并且展示 this.applyAnimatedStyle('.search-input', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) const percentage = translY / -this._tabsTop.value return { width: `${percentage * 60 + 40}%`, opacity: percentage, } }) 除此之外,我们这里可以利用手势来展示商家的一些信息 当在商品列表往下拉到顶部时,触发整个列表下拉展示出商家信息 再往上滑动则商品列表重新展示~ [图片] // 商品详情往下拉 this.applyAnimatedStyle('.main', () => { 'worklet' const translY = clamp(this._translY.value, 0, Number.MAX_VALUE) console.log(222, translY) return { transform: `translateY(${translY}px)` } }) // 简单的 header 渐隐 // 商品详情展示时,仅显示简单的 header:学堂名称和几个标签 this.applyAnimatedStyle('.header-shop-info-simple', () => { 'worklet' const min = 50 const max = 100 const translY = clamp(this._translY.value, min, max) - min return { opacity: 1 - (translY / (max - min)) } }) // 复杂的 header 渐显 // 商品详情下拉,显示复杂 header:展示热门活动、公告等等信息 this.applyAnimatedStyle('.header-shop-info-detail', () => { 'worklet' const min = 100 const max = 150 const translY = clamp(this._translY.value, min, max) - min return { opacity: translY / (max - min) } }) 加入手势动画之后,我们的页面展示对比之前有了以下的优势: 更加自然:更符合用户操作习惯,用户自然滚动屏幕时不会感到突兀更节省空间:滚动隐藏更为灵活、省空间,使页面更清爽更高效的展示:可以将要展示的内容更好的展示,无需做取舍 借助手势动画,我们可以优化小程序界面展示、提升用户体验,从而获得更高的商业价值。 如果你也想更好的留住用户,mark 下这个 源码 [ 瀑布流页面 / 分类页面 ] 直接接到到你的小程序吧~
2023-08-03 - 小程序自定义tabbar以及隐藏tabbar
[图片] 分析: 选中时显示背景和文字,图标变换为选中状态的图标 默认状态下,进入小程序显示的是中间的 “地图” 选中 “预约”时,隐藏tabbar ,且导航栏显示 返回 按钮,点击返回 “地图” 整体效果 [图片][图片][图片] 整体目录 [图片] 设置 app.json 在 [代码]app.json[代码]中 配置tabbar 中的 “custom”: true [代码]{ "pages": [ "custom-pages/custom-active/index", "custom-pages/custom-order/index", "custom-pages/custom-my/index" ], "entryPagePath": "custom-pages/custom-active/index", "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "自定义tabbar", "navigationBarTextStyle": "black" }, "tabBar": { "custom": true, "list": [ { "pagePath": "custom-pages/custom-my/index", "text": "我的", "iconPath": "/images/tabbar/my.png" }, { "pagePath": "custom-pages/custom-active/index", "text": "活动" }, { "pagePath": "custom-pages/custom-order/index", "text": "预约" } ] }, "usingComponents": {}, } [代码] 在项目根目录下,添加 custom-tab-bar custom-tab-bar为component 编写 tabBar 代码 用自定义组件的方式编写即可,该自定义组件完全接管 tabBar 的渲染。另外,自定义组件新增 getTabBar 接口,可获取当前页面下的自定义 tabBar 组件实例 在根目录下新建文件夹名为 custom-pages ,存放自定义tabbar的页面 custom-pages -> custom-active -> custom-my -> custom-order [图片] custom-tab-bar代码 [代码]Component({ data: { // tabbar 的列表 tabbarLists:[ { pagePath: "/custom-pages/custom-my/index", text: "我的", iconPath: "/images/tabbar/my.png", selectedIconPath: "/images/tabbar/my_active.png" }, { pagePath: "/custom-pages/custom-active/index", text: "地图", iconPath: "/images/tabbar/map.png", selectedIconPath: "/images/tabbar/map_active.png" }, { pagePath: "/custom-pages/custom-order/index", text: "预约", iconPath: "/images/tabbar/order.png", selectedIconPath: "/images/tabbar/order_active.png" } ], active:null, //设为数字,会产生tabbar闪烁 isShow:true //控制显示隐藏tabbar }, methods: { switchTab(e){ const { index,url } = e.currentTarget.dataset; wx.switchTab({url}) } } }) [代码] [代码]<view class="tab-bar" wx:if="{{isShow}}"> <block wx:for="{{tabbarLists}}" wx:key="item"> <view class="tab-bar-item" data-index="{{index}}" data-url="{{item.pagePath}}" bindtap="switchTab" > <image class="icon" src="{{active == index?item.selectedIconPath:item.iconPath}}"></image> <view class="text {{active == index?'active':''}}">{{item.text}}</view> <!-- 设置选中状态下的背景 --> <view wx:if="{{active == index}}" class="bg-item"></view> </view> </block> </view> [代码] [代码].tab-bar{ height: 48px; background-color: #ffffff; display: flex; border-top: 1rpx solid #f9f9f9; box-shadow: 0 0 5rpx #f8f8f8; } .tab-bar-item{ display: flex; flex: 1; justify-content: center; align-items: center; position: relative; } .icon{ width: 27px; height: 27px; } .text{ font-size: 26rpx; padding: 0 5rpx; letter-spacing: 0.1rem; display: none; } .active{ color: #10B981; display: block; font-weight: 800; } .bg-item{ position: absolute; width: 80%; top: 15rpx; bottom: 15rpx; border-radius: 40rpx; background-color:rgba(52, 211, 153,0.15); } [代码] tabbar的page页 一共3个tabbar的page页:custom-active、custom-my、custom-order custom-active 页 核心代码 [代码]Page({ onShow() { if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ active: 1 }) } } }) [代码] custom-my 核心代码 [代码]Page({ onShow() { if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ active: 0 }) } } }) [代码] custom-order 该page页要隐藏 tabbar ,所以要将 isShow设置为false; 使用 custom-navigator 自定义组件 显示 头部导航 [代码]<!-- 使用自定义组件导航头 --> <custom-navigator title="{{title}}"> </custom-navigator> [代码] [代码]Page({ data: { title:"预约" }, onShow() { if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ active: 2, isShow:false }) } } }) [代码] [代码]{ "usingComponents": { "custom-navigator":"/components/custom-navigator/custom-navigator" }, "navigationStyle": "custom" } [代码] custom-navigator 自定义组件 [代码]<view class="status"></view> <view class="navigator"> <view class="icon"> <image bindtap="gobackTap class="icon-image" src="/images/back.png"></image> </view> <view class="text">{{title}}</view> <view class="right"></view> </view> [代码] [代码].status{ height: 20px; } .navigator{ height: 44px; background-color: #ffffff; display: flex; flex-direction: row; align-items: center; justify-content: center; } .icon{ width: 40px; height: inherit; display: flex; align-items: center; justify-content: center; } .icon-image{ width: 27rpx; height: 27rpx; border: 1rpx solid #cccccc; padding: 8rpx; border-radius: 20rpx; } .text{ color: #333333; flex: 1; text-align: center; font-size: 28rpx; } .right{ color: #333333; width: 40px; } [代码] [代码]Component({ properties: { title:{ type:String, value:"标题" } }, methods: { gobackTap(e){ wx.switchTab({ url: '/custom-pages/custom-active/index', }) } } }) [代码]
2023-05-12 - 用做App的思路去做微信小程序可行吗?
前言 最近在和一位创业的朋友沟通做一款产品,这位朋友一直用app的思路想做个微信小程序。我的建议是用做App的思路去做微信小程序是不可行的,我们要考虑两边的区别,利用不同生态的优势才行。 微信小程序和App的区别 入口不同:微信小程序是通过二维码、搜索、公众号等方式触达用户,而App则是通过应用商店等方式独立下载和安装。 开发语言不同:App通常使用Object-C、Java、Swift等专门的编程语言,而微信小程序则是基于JavaScript开发的,所以开发门槛相对较低。App通常是由公司内部的团队或者外包给第三方开发公司进行开发,而微信小程序是由微信官方提供的开发工具和开发文档,个人或者小团队也可以进行开发。 用户群体不同:App面向的用户群体更广泛,而微信小程序主要面向微信用户,同时也可以吸引一些不使用微信的用户。 功能和体验不同:App通常具有更丰富的功能和更好的用户体验,而微信小程序则更注重轻量化和快速触达用户。 推广方式:微信小程序可以通过微信公众号、小程序卡片、朋友圈等多种方式进行推广,而App则需要通过应用商店等渠道进行推广。app的营销难度大,用户很少会主动去分享,但是小程序可以通过分享直接转发,并且小程序可以通过一些营销的方式来进行裂变,极大的提高了营销的效率。 生态环境:微信小程序在微信生态可以围绕公众号、视频号、企业微信、微信群去做,而App生态更开放。 功能限制:微信小程序的功能相对来说比较有限,不能与手机系统进行深度交互。而APP则可以实现更多的功能,与手机系统进行深度交互。 总结 微信小程序:开发成本更低,获客成本更低,使用成本更低 App:留存更高,生态更开放,体验可以做到更好,限制更少 做微信小程序就要充分利用微信生态的优势,围绕公众号、视频号、企业微信、微信群来做用户留存裂变。 总的来说,微信小程序和App都有自己的优势和特点,创业者可以根据自己资源/产品属性/用户人群等多方面的情况来选择适合自己的平台。
2023-05-05 - 答题积分小程序云开发实战-界面交互篇:积分排名页布局样式与逻辑交互开发
微信小程序云开发实战-答题积分赛小程序 界面交互篇:积分排名页布局样式与逻辑交互开发积分排名页效果图[图片] 积分排名布局与样式实现这个积分排名页的页面布局,设计上是比较美观的,主要展示排名、用户头像昵称、积分信息。我曾搭建的消防安全知识答题、网络安全知识答题、安全生产知识答题等,都是使用这种方式实现的。 页面布局在rank.wxml中,编写布局代码: <view class="mw-page"> <view class="menu menu-avatar cu-list"> <view class="cu-item"> <view class="cu-avatar lg round"> <image class="avatar" src="/images/0.png" mode="widthFix"></image> </view> <view class='content'> <view class='text-gray'> <text class="icon-upstagefill text-yellow"></text> 第<text class="text-yellow text-xl">1</text>名 </view> <view class='text-sm text-grey'>姑苏洛言</view> </view> <view class='action'> <view class='text-xl text-yellow'>20积分</view> </view> </view> </view> </view> 页面样式在rank.wxss中,编写样式代码: page{ background-color: #fff; } .mw-page .menu.cu-list { padding: 20rpx; } .mw-page .menu.cu-list .cu-item { border: 2rpx solid #ddd; border-radius: 10rpx; margin-bottom: 20rpx; } 页面预览保存后,可以在模拟器预览效果或者手机微信扫码后预览。 [图片] 列表渲染觉得太少了,看不出效果? 在组件上使用 wx:for 控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。 [图片] <view class="mw-page"> <view class="menu menu-avatar cu-list"> <view class="cu-item" wx:for="{{6}}" wx:key="index"> <view class="cu-avatar lg round"> <image class="avatar" src="/images/0.png" mode="widthFix"></image> </view> <view class='content'> <view class='text-gray'> <text class="{{index+1 <= 3?'icon-upstagefill text-yellow':'icon-medalfill text-gray'}}"></text> 第<text class="{{index+1 <= 3?'text-yellow':'text-gray'}} text-xl">{{index+1}}</text>名 </view> <view class='text-sm text-grey'>姑苏洛言</view> </view> <view class='action'> <view class='text-xl text-yellow'>20积分</view> </view> </view> </view> </view> 注意:默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item。 列表效果预览保存后,可以在模拟器预览效果或者手机微信扫码后预览。 [图片]
2023-05-06 - 火遍全网3个月,淄博如何打好“从流量到留量”的关键一战?
[图片] 淄博,从3月火到了5月,在“五一”前热度达到顶峰,且还在持续爆火中。 在抖音上,#淄博烧烤 的话题播放量高达200多亿,单日最高播放量近18亿,也有不少3、4月份爆火的话题,播放量超10亿。 [图片] ▲ 图片来源:果集 · 飞瓜数据 小红书近30天与“淄博”相关的笔记也在增长,#淄博烧烤、#淄博话题浏览量均在4亿左右。 [图片] [图片] ▲ 图片来源:果集 · 千瓜数据 公众号上与“淄博烧烤”相关的文章,据不完全统计,阅读10W+至少有250多篇。 名副其实火遍全网。 而在线下,“进淄赶烤”的人蜂拥而至、络绎不绝。 相关数据显示,3月淄博共计接待游客480万人次,而淄博人口才470万。 刚刚过去的“五一”假期,淄博站客运累计发送旅客超24万人次,较2019年同期增长8.5万人次,增幅55%。旅客到达量约24万人次。 “木鸟”民宿发布的《2023五一假期民宿消费报告》显示,在民宿订单增幅Top10城市榜单中,淄博、威海、福州分列前三位,“顶流”淄博民宿同比2019年订单增幅超78倍。 据微信5月4日发布的《2023“五一”游玩井喷数据报告》,“五一”期间,淄博旅游业消费额环比4月增长73%,而游客在淄博本地中小商户日均消费金额环比4月增长也达到了近40%。 [图片] ▲ 图片来源:「微信派」公众号 说淄博顶流,大概也不为过。即便你的身体没有在“进淄赶烤”的路上,心灵也被网上“小饼烤炉加蘸料,灵魂烧烤三件套”刷屏。 一个烧烤带火了一个工业城市,淄博如何出圈?为什么能成为“顶流”?它的爆火带给互联网人、营销人哪些启发? 1、短视频造神 社交平台种草打卡 淄博烧烤的爆火,要从一个温暖的故事讲起。 去年疫情期间,山东大学1万多名大学生被安排在淄博隔离,淄博政府好吃好喝招待他们。临走前,还请所有学生吃了一顿烧烤,并且邀请他们来年再来。今年春天,这些学生回来了。 更重要的是,这些大学生在各种社交平台上分享、打卡淄博烧烤。#大学生组团到淄博吃烧烤 登上抖音同城热搜,目前话题播放量高达3.4亿,还有各种与之相关的话题。 [图片] ▲ 图片来源:抖音截图 随后央视新闻等各大媒体纷纷报道,淄博烧烤“灵魂三件套”开始走红。 将淄博烧烤推上更高热度的,则是千万粉美食博主“B太”的打卡视频。他专注美食餐饮行业的打假。但4月8日发布的视频中,“B太”去了10家美食店,没有一家缺斤少两,并且视频中还展现了好客山东的热情。目前该视频的点赞有368万。 [图片] ▲ 图片来源:B太抖音截图 从百度搜索指数和微信指数趋势也可以明显看到,自4月8日开始,淄博的热度一路狂飙。随着淄博各种宠粉政策的推出以及热情真诚的服务,淄博被“烤”红了。 [图片] ▲ 图片来源:微信指数 央视新闻记者到淄博采访的时候提到:印象最深的不是烧烤,而是一种不同以往的采访体验。当我们在记录别人的同时,也在被别人记录着。 正如卢克文所说:淄博烧烤的红火,是网络时代独有的,小城经济发掘爆炸式现象。 最新数据显示,截至2022年12月,我国短视频用户规模达10.12亿,网络直播用户规模达7.51亿。 短视频直播时代,无论是普普通通的网民,还是拥有一定话语权、粉丝量的网红博主;无论是蹭流量,还是随手记录,这些KOL、KOC的主动传播,有力推动淄博的走红。 2、全盘布局 精准营销 淄博政府的流量承接可谓“快、准、狠”。 首先是政府部门的联动。不仅仅是文旅局,商务局、公安局、市场监管局等各政府部门“总动员”,并且“五一”期间,党政机关全员不休假,充当服务志愿者,做好一条龙服务。 淄博烧烤高铁专列、21条定制公交、成立淄博市烧烤协会、发布淄博烧烤地图、举办淄博烧烤节、72小时昼夜施工改造道路……各种高效“宠粉”举措接连推出。 此次爆火因大学生而起,而淄博政府也面向这批受众,推出了专门的举措,精准营销。 全市38处青年驿站为符合条件的来淄求职、就业的青年学生提供每年3次、每次2晚的免费入住,来淄实习、游玩、访友的青年学生可享受每年4次、每次5天的半价入住。 大学生作为年轻群体代表,也是在网上最为活跃的群体,且近来大学生拉练式的特种兵旅游走红,为淄博的流量传播创造了条件。 [图片] ▲ 图片来源:抖音截图 淄博政府很好地抓住了关键目标用户,不仅吸引他们来旅游,更是吸引外来人才,将流量充分沉淀转化。 当然,有的网友提出要185的小哥接待,淄博政府真的满足了用户的个性化需求。 普通民众也在努力。本地人不吃烧烤让位、出门不打车还为游客免费接送、商铺为游客提供免费手机充电,还有0.85米志愿者喝着奶帮看行李,甚至有夫妻连架都不敢吵了,生怕影响淄博形象。 [图片]▲ 图片来源:网络 有网友调侃:470万人的淄博,有400万人都是客服。全城都在“为淄博的荣誉而战”。 从上至下,由点到面,淄博不仅承接住了流量,还很好地维护和提升了品牌形象,将“淄博烧烤”城市名片和淄博城市形象的良好一面,展现地淋漓尽致。 有的营销活动做不好,可能活动方案是一方面,各部门执行到位、默契配合也很重要,一个环节出错,就有可能满盘皆输。 清晰了解用户画像,针对目标受众战略布局,实施精准营销,并且“用户至上”,想用户所想,把服务做到极致,做好口碑营销。 3、从流量到留量 从爆红到长红 当下全国不少景区都出现宰客现象,今年“五一”前夕,也爆出了不少“酒店刺客”的新闻,而淄博不宰客、不缺斤少两、酒店不涨价反而降价退钱给用户。 淄博所做的不过是回归服务的初心与本质,只有真诚不辜负真诚,而真诚是永远的“必杀技”。 [图片] ▲ 图片来源:网络 是淄博烧烤特别好吃吗?当然也并不全是。全国各地的人民千里迢迢赶去淄博,为的可能不是烧烤,而是感受这座真诚、热情好客、充满烟火气的城市带给人们不一样体验。 正如吴晓波所提到的,淄博烧烤正在兑现人们对自由市场的平民式想象:物美价平的商品、畅快淋漓的消费体验、童叟无欺的市场环境、谦卑和气的“小政府”。 烧烤带火了淄博,成为了这座城市的“引流”利器。但淄博不止于烧烤。 淄博在做的,是穿透烧烤,激活整座城市的活力,努力将“流量”变为“留量”,将“爆红”转化为“长红”。 无论是淄博的烧烤、旅游,还是我们所处的行业,所做的从来都不是一次性买卖,不能为了一时的蝇头小利,而失去初心与本质,做出有损品牌形象、用户满意度大打折扣的行为。 一夜爆红是偶然,但是昙花一现还是勇立潮头,要靠正向内容的持续输出、产品与服务的不断打磨、品牌形象的持续打造、用户口碑的不断积累,只有这样才能带来充分的长尾效应,稳定可持续发展。
2023-05-06 - 小程序静默登录设计
简介 本文主要分享个人平时开发微信小程序时登录设计的思路,目前微信小程序的登录机制与以前的小程序登录机制或网页的登录机制不一样 一. 静默登录 小程序目前的登录方式采用的是静默登录的形式,即用户不需要进行任何相关授权或其他操作就可以很流畅的体验整个应用,不像以前的小程序应用很多功能都需要手动去判断用户是否已经登录过再去开放某些入口或功能(当然目前也有不少小程序改成绑定手机号的形式去搞这种需求) 静默登录目前有以下几点好处: 不需要特意去增加一个登录界面或者登录弹窗去实现登录功能 减少前端对一些特定按钮进行登录状态判断的工作量 用户不需要进行额外登录操作才能完美体验整个小程序 更好的保障用户隐私安全,当然这是对 TX 有好处(毕竟很多小程序喜欢采用强制登录来获取用户信息) 二. 静默登录流程 前端调用 [代码]wx.login[代码] 这个 [代码]api[代码] 获取临时登录凭证([代码]code[代码]) 前端调用服务端登录接口传递 [代码]code[代码] 给服务端 服务端通过小程序 [代码]appId[代码] 和小程序秘钥向微信服务器获取 [代码]openId[代码] 和 [代码]session_key[代码] 服务端将 [代码]openId[代码] 和 [代码]session_key[代码] 进行关联同时产生一个带有默认名称和默认头像的新用户 服务端将自定义登录的信息返回给前端,下次请求服务端接口时把登录信息带上 三. 静默登录设计思路 前端调用 [代码]wx.login[代码] 是不需要进行任何相关操作的,所以触发登录完全是由前端去决定的,而进行登录不能以指定页面作为参考点,需要考虑到用户分享从其他页面进入的情况 现在需要一种,目前个人思考出有两种方案: 每个页面都套用一个 [代码]layout[代码] 自定义组件,在该组件的生命周期进行登录触发,然后触发完毕后在组件抛出一个登录后的回调函数,接着页面接收这个回调函数然后在这个函数调用业务 [代码]api[代码] 在每次请求服务端 [代码]api[代码] 之前,先进行登录,等待登录处理完毕之后再进行请求服务端 两者比较之后目前是采用第二种方式去进行登录流程设计,因为静默登录完全跟用户没有任何关系,只是单纯与服务端进行交流,所以不应该与页面有关系,应该是跟请求服务端 [代码]api[代码] 有关系 四. 静默登录设计实现 由于采用 [代码]api[代码] 拦截方式实现登录设计,登录的代码都放在 [代码]api[代码] 请求模块去实现,以下是实现思路: 在页面中调用服务端 [代码]api[代码],首先判断是否已经登录过,如果没有登录则先触发登录再请求服务端 [代码]api[代码],如果登录则直接请求 考虑到一个页面可能会同时调用多个服务端 [代码]api[代码],需要避免同时触发多次登录 [代码]// 登录后的信息 const token = { // ... } // 请求封装 const _request = <T = unknown>(apiLink: string, params?: Record<string, any>): Promise<T | null> => { return new Promise((resolve, reject) => { wx.request({ url: apiLink, data: params, success: (res) => { resolve(res.data) }, fail: (err) => { reject(null) } }) }) } // 记录登录状态 let _loginRecord: any = null // 请求拦截 const apiRequest = async <T = unknown>(apiLink: string, params?: Record<string, any>): Promise<T | null> => { // 如果一个页面同时触发多个 api 请求,那么只公用一个登录的 Promise 状态,这样就不会多次触发登录 if (!_loginRecord) { _loginRecord = _request('xxxxx/login', { code: 'xxxx' }) } const loginResult = await _loginRecord /** * 这里根据业务需求去定制登录报错时的情况,我目前是 * 先弹窗提示,然后用户确认后再重新请求 api * if (!loginResult) { _loginRecord = null return null } return await _request(apiLink, params) } export default apiRequest [代码] 五. 最后 以上是我个人常用的微信小程序静默登录设计思路,如果有小伙伴有更好的设计思路,希望能够分享出来学习一下
2023-05-09 - Skyline|原生级卡片转场,小程序轻松实现
在上一篇文章《在小程序中实现原生相册》中,我们学习了自定义路由搭配共享元素实现的原生相册效果,共享元素可以让用户在体验小程序时视觉关联性更强。 除了相册实现之外,常见的卡片转场也非常适合。 [图片] ⬆️ 演示效果:默认动画 vs 卡片转场动画 👇 下面我们来看看卡片转场中通过 共享元素 + 自定义路由 来实现无痕跳转。 [图片] 这里的转场稍微有点复杂,涉及到以下 3 个点 旧卡片:图片放大、内容渐隐新页面:按比例放大、页面渐显手势搭配1、旧卡片:图片放大、内容渐隐 在本示例中,列表页采用的是 scroll-view 瀑布流布局的实现。 [图片] 这里我们的共享元素是卡片,即 grid-view 中的内容 card,卡片包括 图片、内容描述。 [图片] 默认情况下,共享元素是整个节点进行飞跃的,由于前后页面的图片元素一致但文本内容不一致, 导致在第一帧或者最后一帧会有跳动的效果。 为了让转场动画更加自然,我们需要在飞跃的过程中渐隐旧卡片的内容描述。 [图片] 在这里,我们需要先用 this.applyAnimatedStyle 来给对应的节点绑定 worklet 驱动动画。 .card_wrap 节点:整个卡片按比例放大.card_desc 节点:内容描述渐隐[图片] 关于动画执行的时机,我们可以通过配置项修改。 immediate:设置是否立即执行驱动动画flush:shareValue 更新时,applyAnimatedStyle 的 updater 函数刷新时机在本例中,需要保证共享元素的图片与目标页面图片位置重叠,所以 flush 设置 sync 在当前时间片刷新。 [图片] 绑定完驱动动画之后,我们需要给共享元素绑定帧回调事件,根据当前动画进度改变共享变量的值来驱动共享动画 [图片] 2、新页面:按比例放大、页面渐显 新页面在路由中的动画,需要在自定义路由中进行配置。关于自定义路由的更多介绍,可参考《小程序页面转场动画》 在路由动画过程中,我们将上一步的共享元素帧回调拿到 begin、end 的值,然后结合动画进度 t 计算得出新页面的位置、缩放比例。 还有根据动画进度,设置页面渐显,与前面的卡片渐隐承接。 [图片] 3、手势搭配 学习过我们前面的文章的同学都知道,自定义路由经常需要结合页面手势,来实现手势返回,关于手势的基础知识可参考《小程序页面转场动画》 [图片] 这里我们希望手势缩小整个当前页面,所以这里手势返回时只在当前页面做手势动画即可。 在页面详情页的最外层,嵌套一个手势组件 pan-gesture-handler,当手势拖动时根据手势的位置改变整个页面(通过 #fake-host 控制)的位置和大小来达到拖动的效果。 [图片] 同样绑定页面驱动动画,通过 applyAnimatedStyle 给 #fake-host 绑定驱动动画,当共享变量 transX、transY 等变化时则自动改变 transform 来驱动 #fake-host 缩小。 [图片] 接着绑定手势事件,根据手势拖动时拿到位置信息改变共享变量 transX、transY 的值。 [图片] 最后我们需要设置背景颜色透明,来达到类似把卡片拖回列表的视觉效果,更好的减少页面切换感~ [图片] 一个自定义路由的页面会有 3 层可以设置到背景色,要做到透明的效果需要将 3 个背景色都设置为透明。更多自定义路由背景色的详情参考官方文档。 [图片] 想要试试卡片转场的无恒效果~扫描 ⬇️ 下方小程序码即可体验。 如果你也想在小程序中实现卡片转场动画,mark 下这个 源码 直接接到到你的小程序吧~ [图片]
2023-08-03 - 日历组件WeCalendar;支持折叠周模式;滑动;自定义标记
miniprogram-wecalendar 一个支持滑动、自定义折叠、标记日期、轻快的小程序日期组件 使用ESbuild 构建,现在的快速响应的 [图片] [图片] [图片] [图片] [图片] 英文 README 展示 [图片] 安装 [代码]npm i miniprogram-wecalendar [代码] or [代码]yarn add miniprogram-wecalendar [代码] 使用 在使用地方 [代码]page.json[代码] 或者 [代码]app.json[代码] 中使用 👇🏻 [代码]{ "usingComponents": { "WeCalendar": "miniprogram-wecalendar" } } [代码] 在 wxml 加入使用 👇🏻 [代码]<WeCalendar markCalendarList="{{markCalendarList}}" isToday="{{true}}" bind:onRangeDate="onRangeDate" bind:onSelect="onSelect" /> [代码] WeCalendar的参数 Property Type Default required Description isToday Boolean False 0 是否展示今天俺妞icon markCalendarList [代码]Array[{ date: YYYY-MM-DD pointColor: #ccc }][代码] [] 0 标记日历的数组,支持自定义颜色 defaultDate String: YYYY-MM-DD Null 0 默认时间 showFolding Boolean True 0 日历折叠功能 weeekLayer Number 1 0 日历折叠的等级 | 行数 WeCalendar 的方法 Property Type Description onSelect Function Callback 日历每天的点击事件 onRangeDate Function Callback 日历渲染的日期范围 举个例子 🌰 onSelect [代码]onSelect: (e) => { const {day} = e.detail // ... } [代码] onRangeDate [代码]onRangeDate: (e) => { const {beginTime, endTime} = e.detail // ... } [代码] 开发启动 [代码]npm run dev [代码] 用微信小程序开发工具打开[代码]demo[代码]文件夹即可,更改[代码]src[代码]下面的文件会自动构建
2023-04-19 - setData动态key的使用以及使用建议
相信每个小程序开发者用的最多的函数非setData莫属,setData是小程序开发中使用最频繁、也是最容易引发性能问题的接口。 本篇内容主要讲小程序setData的用法、注意事项、使用建议以及动态key的写法。 setData介绍 Page.prototype.setData(Object data, Function callback) [代码]setData 函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data 的值(同步)。 [代码] 字段 类型 必填 描述 最低版本 data Object 是 这次要改变的数据 callback Function 否 setData引起的界面更新渲染完毕后的回调函数 1.5.0 Object 以 key: value 的形式表示,将 this.data 中的 key 对应的值改变成 value。 注意事项 直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。 仅支持设置可 JSON 化的数据。 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。 请不要把 data 中任何一项的 value 设为 undefined ,否则这一项将不被设置并可能遗留一些潜在问题。 示例代码: 在开发者工具中预览效果 使用建议 1. data 应只包括渲染相关的数据 setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。 ✅ 页面或组件的 d- ata 字段,应用来存放和页面或组件渲染相关的数据(即直接在 wxml 中出现的字段); ✅ 页面或组件渲染间接相关的数据可以设置为「纯数据字段」,可以使用 setData 设置并使用 observers 监听变化; ✅ 页面或组件渲染无关的数据,应挂在非 data 的字段下,如 this.userData = {userId: ‘xxx’}; ❌ 避免在 data 中包含渲染无关的业务数据; ❌ 避免使用 data 在页面或组件方法间进行数据共享; ❌ 避免滥用 纯数据字段 来保存可以使用非 data 字段保存的数据。 2. 控制 setData 的频率 每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。过于频繁(毫秒级)的调用 setData,会导致以下后果: 逻辑层 JS 线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换; 视图层 JS 线程持续处于忙碌状态,逻辑层 -> 视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟; 视图层无法及时响应用户操作,用户滑动页面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。 因此,开发者在调用 setData 时要注意: ✅ 仅在需要进行页面内容更新时调用 setData; ✅ 对连续的 setData 调用尽可能的进行合并; ❌ 避免不必要的 setData; ❌ 避免以过高的频率持续调用 setData,例如毫秒级的倒计时; ❌ 避免在 onPageScroll 回调中每次都调用 setData。 3.setData 应只传发生变化的数据 setData 的数据量会影响数据拷贝和数据通讯的耗时,增加页面更新的开销,造成页面更新延迟。 ✅ setData 应只传入发生变化的字段; ✅ 建议以数据路径形式改变数组中的某一项或对象的某个属性,如 This.Setdata({‘Array[2].Message’: ‘Newval’, ‘A.B.C.D’: ‘Newval’}),而不是每次都更新整个对象或数组; ❌ 不要在 setData 中偷懒一次性传所有data:this.setData(this.data)。 修改数组动态key的写法以及修改对象的某个属性 在开发者工具中预览效果 动态修改数组的某个元素 用一个中括号修饰表达式即可 [代码]clickItem(e){ let item = e.currentTarget.dataset.item let index = e.currentTarget.dataset.index // 第一种 可以只改某个数组的元素 // item.selected =!this.data.list[index].selected // this.setData({ // ['list['+ index + ']']: item // }) //第二种 也可以直接修改某个数组的某个属性 this.setData({ ['list['+ index + '].selected']: !this.data.list[index].selected }) }, [代码] 以数据路径形式改变数组中的某一项或对象的某个属性 [代码]clickObject(e){ this.setData({ 'user.clickNum': this.data.user.clickNum + 1 }) }, [代码] 更多内容可以仔细查看官方文档: setData 函数详解(在文档的底部) 合理使用 setData
2023-04-24 - 运用小程序Skyline技术构建无缝用户体验 —— 同程旅行酒店最佳实践分享
[图片] 动效衔接设计与小程序渲染框架 1、什么是动效衔接设计? 随着互联网技术和设计理念的不断发展,动效设计成为现代 UI 设计中不可或缺的一部分。其中,动效衔接是非常重要的一环。动效衔接设计是指通过巧妙的动效设计,将不同的 UI 元素在动画过程中自然、流畅地衔接起来,从而增强用户的交互体验和视觉感受。在实际应用中,动效衔接设计主要应用于界面转场、信息提示、状态变化等方面,通过顺畅的衔接,降低用户因白屏等待而产生的焦虑。 2、动效衔接设计的意义 (1)极大提高用户体验,让用户感受到界面的流畅和自然,从而增加用户对产品的好感度。 (2)降低用户的操作认知成本,帮助用户更好地理解执行操作后所带来的结果,从而减少用户对产品的困惑。 (3)强化视觉层,让用户更好地区分不同的信息和元素,从而增强视觉层次感。 (4)增加界面的美感度,让界面更加生动有趣,从而提升整体的美感和设计价值。 (5)提升品牌的认知度,让产品更加具有特色和独特性,从而提高品牌的认知度和市场竞争力。 3、什么是小程序渲染框架Skyline? 为了进一步优化小程序性能,小程序在原 webview 渲染引擎之外最新推出 小程序渲染框架Skyline,其使用更精简高效的渲染管线,并拥有诸多增强特性,让它拥有更接近原生渲染的性能体验。新的增强特性有 worklet 动画系统、手势系统、自定义路由、共享元素动画,而且许多常用的组件如 scroll-view、swiper 都有了更高性能的实现。 [图片] [图片] 实践理念和场景拆解 1、动效衔接设计的核心原则 简单而清晰的动效设计,需要遵守以下几个原则: (1)一致性:动效衔接应该与整体设计风格保持一致。包括颜色、字体、动画速度等方面。 (2)可预测性:用户能够感知动画元素的变化关联性,从而增加用户对产品的掌控感和对界面的理解。 (3)反馈性:动效衔接与用户操作相响应,从而帮助用户理解他们的操作所带来的结果。 (4)视觉层次:动效衔接遵循视觉层次原则,让用户区分页面中的上下关系以及三维物理世界的关系层次,给用户清晰的层级区分感知,提高用户体验。 (5)自然性:动效衔接符合物理规律,例如重力、加速度等,从而增强动画的真实感和用户体验。 2、理念孵化与使用场景拆解 以提炼的动态感受为出发点,理性的层面给予了我们大致的产品体验感知,为我们动效理念的建成提供了框架。对此我们将继续从感性层面出发,找寻可传递真实感受的运动现象并加以组合提炼。 本次 同程旅行小程序 以酒店预订链路中核心的相册页面进行应用场景,在用户操作图片的过程中运用小程序渲染框架承接。 (1)将整个过程进行了拆解,首先为了退出时行动的路径更加清晰,做了一个响应设计,当界面向右滑动退出的过程中,相册图片进行缩小,在缩放过程中,会根据位移距离控制缩放的比例,同时蒙层的透明度以及毛玻璃效果也跟随手指移动变化,和相册列表页在视觉上呈现 XY 轴以及上下层级的空间关系,在缩小到一定的比例时,触发震动效果,松手退出到相册列表页。 (2)在交互结束时,图片退回相册列表页原始位置,在返回路径的过程中,根据交互结束时的定位点,来判断运动的方向和距离,计算运动加速度,以及模拟运动加速度带来的惯性回弹的方向和角度变化,加强与模拟真实物理世界的运动定律,和视觉上的动态感知。 结合自然世界的运动规律来看,把页面进入的元素比作是行驶的汽车,用户当作是正在斑马线上行驶的人,将马路作为页面空间。若汽车采用的是缓入运动(加速)的话,马路上的行人则看到的是一辆不断加速向他行驶过来的车辆。因为担心车辆高速的逼近导致刹车不及时的情况,行人便会本能的作出躲闪的反应。其实页面也是一个道理,进入的元素使用加速运动出现过冲的运动感知会让用户体验时产生不适。 [图片] 小程序渲染框架技术开发实践过程剖析 1、开发自定义路由实现此交互,需要 自定义路由动画,因为小程序渲染框架的页面支持自定义跳转动画。当使用自定义路由后,页面跳转时指定路由类型,就会触发自定义路由动画,而不再是默认的从右往左的动画,此处的实现可以使得页面跳转时,没有默认的路由动画,页面将直接以透明的方式渲染在屏幕上,由开发者自己控制页面内元素的动画展示方式,具体实现如下: (1)在图片查看页面配置文件 index.json 中声明 { "backgroundColor": "#00000000", "backgroundColorContent": "#00000000", // 设置客户端页面背景为透明 "navigationStyle": "custom", "renderer": "skyline", // skyline渲染引擎 "disableScroll": true, "usingComponents": { } } (2)在 wxss 中,设置图片查看页面的 page 节点为透明背景 page { background: transparent; } (3)在 js 中,使用 wx.router.addRouteBuilder(routeType, fn) 来声明自定义路由动画 wx.router.addRouteBuilder('myCustomRoute', function (params) { const handlePrimaryAnimation = () => { 'worklet'; return { // 可在此处,根据 params.primaryAnimation.value 的值,来设置页面的动画效果 backgroundColor: `rgba(0,0,0,${ params.primaryAnimation.value })` }; }; return { opaque: false, handlePrimaryAnimation, barrierColor: '', barrierDismissible: false, transitionDuration: 320, reverseTransitionDuration: 250, canTransitionTo: true, canTransitionFrom: false }; }) (4)在图片列表页面中,使用 x.navigateTo 来跳转页面,并且设置 routeType 为 myCustomRoute wx.navigateTo({ url: '/pages/skyline-image-viewer/index?index=0', routeType: 'myCustomRoute' }) [图片] 需配置页面的渲染引擎为 Skyline,并且在跳转时使用 routeType 就可以实现让页面在跳转时没有默认的路由动画。 2、共享元素穿越在连续的页面跳转时,页面间 key 相同的 share-element 节点将产生飞跃特效,还可自定义插值方式和动画曲线,通常作用于图片。为保证动画效果,前后页面的 share-element 子节点结构应该尽量保持一致 <share-element key="share-key"> <view> you code here </view> <!-- 需要注意,share-element 内要求只有一个根节点 --> </share-element> [图片] 这时,界面的表现像上面视频一样,是一个连续的动画状态,这完全是由 share-element 来控制的,share-element 的动画原理如下图所示: [图片] 3、接入手势组件,实现图片放大、缩小、平移在图片查看页面有如下结构: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <share-element key="{{ shareKey }}" class="current-item"> <image src="{{ src }}"/> </share-element> </scale-gesture-handle> 这里,我们使用小程序渲染框架提供的 手势组件 <scale-gesture-handle>,来实现图片的放大、缩小、平移等手势交互。 注意,所有声明为 worklet 指令的方法它们运行在UI线程,不要在方法中修改普通的变量,因为跨线程的关系,只能修改使用 wx.worklet.shared 声明的变量。 const GestureState = { POSSIBLE: 0, // 此时手势未识别 BEGIN: 1, // 手势已识别 ACTIVE: 2, // 连续手势活跃状态 END: 3, // 手势终止 CANCELLED: 4 // 手势取消 }; Component({ attached() { this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.sharScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.sharScale.value})` } }); // 页面所需的数据,需要在 attached 事件里初始化完毕,使其可以参与首帧渲染 this.setData({ src: '...', shareKey: '...' }); }, methods: { // 当手势组件识别到手势时,触发此回调 onScaleGestureHandle(e) { 'worklet'; const { state } = e; // 在worklet函数里,不要使用 const {} = this 对this解构 const shareX = this.shareX; const shareY = this.shareY; const sharScale = this.sharScale; if (state === GestureState.BEGIN) { // 手势已经识别,此时,可以获取到手势的初始值 } else if (state === GestureState.ACTIVE) { // 手势活跃状态,此时,可以获取到手势的变化值,如平移的距离、缩放的比例等 // 将当前变化的值,设置到 `shared` 变量,就可以改变元素的样式,类似于vue3的数据驱动 shareX.value += e.focalDeltaX; shareY.value += e.focalDeltaY; sharScale.value = e.scale; } else if (state === GestureState.END || state === GestureState.CANCELLED) { // 手势终止或取消,此时,可以获取到手势的最终值 } } } }) [图片] 4、手势协商(解决手势冲突) 上面的 demo 简单演示如何使用手势组件来做图片交互,但是在图片查看页面中,我们还有其他的手势交互,如图片的左右滑动切换等,一般我们会使用 <swiper> 组件来实现,但是 <swiper>组件的内部实现和 <scale-gesture-handle> 组件,都会监听手势事件,手势组件的事件不支持冒泡的,就会导致下面结构横时: <scale-gesture-handle worklet:ongesture="onScaleGestureHandle"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </scale-gesture-handle> 使用手势横向滑动时,会优先触发 swiper 的横向切换事件,而无法触发 <scale-gesture-handle> 的手势事件了,这在图片放大时的图片横向移动产生了冲突。此时就需要使用手势协商来解决手势冲突。 什么是手势协商? 手势协商指的是:当页面同时有多个手势交互时,需通过一定的约定来决定哪些手势事件应该被执行,哪些需要被忽略。 小程序渲染框架解决手势冲突的方式,主要是通过手势组件的 tag、simultaneous-handlers、native-view 和 should-response-on-move 来实现 tag:手势组件的标识,用于区分不同的手势组件simultaneous-handlers:手势组件的协商者,表示需要同时触发事件的手势组件的标识should-response-on-move:参与手势时间的派发过程,返回 false时,表示该手势时间不会继续派发native-view:用当前手势组件来代理原生组件内部的手势事件,如<swiper>组件内部的手势事件<swiper> 的内部也是使用了 <horizontal-drag-gesture-handler>手势组件,但是我们不能直接在<swiper>上设置tag来使其参与手势协商,需要用相同的手势组件通过native-view=swiper将其内部的事件代理出来,使其可以参与协商<!-- <scale-gesture-handle> 缩放手势 --> <!-- <horizontal-drag-gesture-handler> 横向拖动手势 --> <!-- 通过 simultaneous-handlers=tag 来声明多个手势应该同时触发 --> <scale-gesture-handle tag="scale" simultaneous-handlers="{{['swiper']}}" worklet:ongesture="onScaleGestureHandle"> <!-- 此处使用 native-view=swiper 代理内部的手势组件 --> <!-- 通过 should-response-on-move=fn 来参与`事件派发`过程,决定手势的事件是否应该派发 --> <horizontal-drag-gesture-handler tag="swiper" native-view="swiper" simultaneous-handlers="{{['scale']}}" worklet:should-response-on-move="shouldResponseOnMove"> <swiper> <swiper-item wx:for="{{ imgs }}"> <share-element key="{{ item.shareKey }}" class="current-item"> <image src="{{ item.src }}"/> </share-element> </swiper-item> </swiper> </horizontal-drag-gesture-handler> </scale-gesture-handle> const GuestureMode = { INIT: 0, SCALE: 1, SWIPE: 2, MOVE: 3 // ... }; Component({ attached() { this.GuestureModeShared = wx.worklet.shared(GuestureMode.INIT); this.shareX = wx.worklet.shared(0); this.shareY = wx.worklet.shared(0); this.shareScale = wx.worklet.shared(1); // 声明共享变量,并且给需要变化的dom,绑定动画 this.applyAnimatedStyle('.current-item', () => { 'worklet'; return { transform: `translate3d(${this.shareX.value}px, ${this.shareY.value}px, 0) scale(${this.shareScale.value})` } }); // ... }, methods: { onScaleGestureHandle(e) { 'worklet'; const { state } = e; if (state === GestureState.BEGIN) { this.GuestureModeShared.value = GuestureMode.INIT; } else if (state === GestureState.ACTIVE) { if(this.GuestureModeShared.value === GuestureMode.INIT) { this.gestureBefore(e); // 手势类型未知时,判断手势类型 } else { this.gestureHandle(e); // 手势类型已知时,处理手势事件 } } else if (state === GestureState.END || state === GestureState.CANCELLED) { this.GuestureModeShared.value = GuestureMode.INIT; } }, // 判断手势类型 gestureBefore(e) { 'worklet'; const { focalDeltaX, focalDeltaY, scale } = e; if (Math.abs(focalDeltaX) > Math.abs(focalDeltaY)) { this.GuestureModeShared.value = GuestureMode.SWIPE; } else if (scale > 1) { this.GuestureModeShared.value = GuestureMode.SCALE; } else { this.GuestureModeShared.value = GuestureMode.MOVE; } }, // 处理手势事件 gestureHandle(e) { 'worklet'; if (this.GuestureModeShared.value === GuestureMode.SCALE) { this.shareScale.value = e.scale; } else if (this.GuestureModeShared.value === GuestureMode.SWIPE) { // swiper 切换模式时,这里什么都不用做 } else if (this.GuestureModeShared.value === GuestureMode.MOVE) { this.shareX.value += e.focalDeltaX; this.shareY.value += e.focalDeltaY; } }, // 用于判断手势事件是否应该派发 shouldResponseOnMove(e) { 'worklet'; return this.GuestureModeShared.value === GuestureMode.SWIPE; // 当模式为SWIPE时,才响应手势事件 } } }) [图片] 通过上面的代码,我们实现了手势协商,当用户在图片上进行滑动的操作时,总是会触发 <scale-gesture-handler> 的手势事件,通过对图片当前状态的判断来决定应该触发哪种手势,我们通过此种协商让 <horizontal-drag-gesture-handle> 手势在合适的时机触发,以此避免手势冲突。 5、使用小程序渲染框架时需要注意的一些地方作为一款新的渲染优化方式,开发者使用小程序渲染框架需要注意以下内容,以保证渲染的效果和性能。 (1)自定义路由时首帧渲染&首帧性能优化 小程序渲染框架的首帧渲染对共享元素动画非常重要,若共享元素节点的key 错过首帧设置的话,可能会丢失飞跃动画,所以在使用小程序渲染框架时,共享元素的 key 应该尽量在 attached 中或之前设置到页面,并且在首帧渲染时,应尽可能的减少 UI 层的渲染工作 如下: 1)所需要的数据应尽可能使用提前计算好,避免构建页面时等待太久影响响应速度 2)首次设置的数据应该尽可能的少,避免首次渲染时,页面上的元素过多,导致首帧渲染时间过长,导致动画卡顿(如:不要同时初始化太多的 <swiper-item>) 3)确保首帧渲染时,共享元素的 key 正确的设置,避免在首帧渲染时,由于找不到对应的共享元素,导致动画丢失,看不到飞跃动画 4)由于手势事件触发频繁,应尽量避免大量需要的计算的逻辑高频执行,容易导致机器发烫,或者导致动画卡顿 **worklet 函数的使用** worklet 函数的使用有一些限制,主要是由于它是在 UI 线程执行的,所以 worklet 函数中的 this 并非是页面的 this 实例, 里面所使用到的变量也是通过特殊的 babel 插件转换到UI线程的,需要与逻辑层共用的变量都需要用 wx.worklet.shared 将它声明成共享变量,在 UI 线程调用逻辑层的函数需要使用 wx.worklet.runOnJS (2)与 web 规范的差异 虽然小程序渲染框架尽可能的与 web 规范保持一致,但是由底层渲染引擎的限制,还是有一些差异,如: 1)display: flex 的默认朝向是 column,而不是 row,这需要开发者注意,官方后续会支持 block 布局方式 2)暂不支持 css 伪元素,如 ::after、::before,官方正在支持中 3)position 仅支持 absolute、relative,不支持 sticky,实现滚动吸附的效果需用 sticky-* 组件来配合 scroll-view 实现 ** <share-element> 在非小程序渲染框架运行环境里的表现是什么** 在非小程序渲染框架的运行环境内,<share-element> 组件会被视为一个 <view> 组件,需要做好布局的兼容 6、何时使用小程序渲染框架开发时,请确保小程序开发者工具版本是 最新版 nightly,sdk 版本在 2.30.2+,具体限制可参考 文档。 这些新特性的引入,使得小程序渲染框架在小程序开发中的优势更加明显,开发者可以更加便捷地实现各种复杂的交互效果,并且达到接近原生APP的体验。 [图片] 未来展望 1、个性化产品形态:将会根据不同的用户需求和场景,设计出更加符合用户喜好和习惯的动效衔接,进行组件化调用。 2、更加自然和真实的动效衔接:动效衔接将会更加贴近自然规律和真实物理效应,从而增强动画的真实感和用户体验。 3、更加智能化和自适应的动效衔接:动效衔接将会根据用户的操作行为和使用习惯,自适应调整动画效果,从而提高用户体验和产品效果。 4、扩大产品、设计与开发的协作效应:设计对动效的把控、产品对用户的洞察以及开发对新技术的应用,才可以发挥最大化的协作效应。 附1:本文作者 同程旅行研发工程师 同程旅行体验设计师 同程旅行产品经理 附2:代码片段 相册小程序代码片段(请使用 PC 端浏览器打开):https://developers.weixin.qq.com/s/E979jCmP7oHG 附3:UE标注 [图片] 附4:AB 实验效果 AB 实验显著win0.23% [图片]
2023-04-28 - We分析 · 小程序数据分析平台
内含平台介绍、接入指引及系列课程,帮助您快速了解、上手平台,精细化分析小程序数据,驱动业务决策。
2022-11-21 - 昨天开源了一个前端样式库-Vaa,希望能减少代码量(MIT协议,免费商用)
想法 2022年12月份的时候萌生了这个想法,到现在开源,花了两个月时间。 自己也在一边使用一边完善种,直到昨天感觉第一个版本应该是差不多了,于是开始开源,希望能够给大家带来些许帮助。 小巧精致,仅45.8kb 地址:https://github.com/vaaagle/VaaWxss 核心 Flex是这个样式库的核心,我们将一些常用样式通过比较规范的规则,包成了‘类’ 我们经常需要定义宽高,于是重复的定义类然后在类中设置,至少是3行代码 .demo{ height:50px; } 现在vaa里面预设了常用宽高和其它常用样式的类,如 fs-12、fw-600、z-index-1、radius-16、w-100p、h-60、f-c-c等,您只需要了解命名规则然后去使用即可,无需再去定义大量属性重复的类。 我们的预想是,您在看页面的时候,在脑海里设计实现方式的时候,就可以通过拼凑这些类的方式直接实现您设计的效果,比如: 现在有个需要实现一个按钮,那怎么设计? 构思:文本元素(text)+ 字符串居中 + 手机屏幕70%的宽度 + 40px高度 + 圆角 + 文字颜色白色 + 背景颜色蓝色 + 字体大小14px + 距离顶部10px 实现: <text class="w-70p h-40 radius-20 c-fff c-bg-blue2 fs-14 mt10">提交</text> [图片] 使用前提 需要您对flex布局有些了解 [图片]
2023-02-17 - 微盟移动端组件库 Titian Mobile 对外开源
消费互联网时代,纷繁的应用需求催生了产品和功能的指数级增长。在复杂多元的研发场景下,高效快速、保质保量、可持续交付的研发模式,方能支撑高速扩张的业务需求。 01. Titian 是什么? 近日,微盟重磅推出多渠道移动端组件库——Titian。作为移动端前台的“底层”能力,它伴随了“微盟 WOS -新商业操作系统”的研发历程,经历了从无序到有序,从稚嫩到成熟的蜕变,并助力了需要不断实现的“产品一体化、体验一体化、服务一体化、数据一体化和开放一体化”的 WOS 终极形态。 [图片] 经过一年多的不断进化,Titian 取得了大量核心业务应用及有效性验证。Titian 提供了清晰统一的 API ,满足多渠道小程序与多前端框架能力,极大的降低了研发成本与提升需求流转效率。同时,Titian 也是体验良好的重要保障,遵循用户习惯、维持用户心智,为业务提供了有序一致的产品体验。 [图片] 02.设计理念体验设计远不止于核心任务,更需要沉淀与延续设计模式和理念,为后续的设计产出质量与效率提升做基奠。为此,微盟Titian在完成核心任务的同时并行落地了“移动端设计体系”。包含以下三部分:分别为“设计理念”、“视觉语言”、“设计规范”。体系是设计在执行时的规则、标准与指导。 [图片] 针对微盟产研、微盟生态开发者、外部行业开发者3类用户群体,微盟Titian在提升效率、普适通用的核心思路下,基于“有序、普适、多元”的设计理念与价值观,打造具有自身特色的移动端组件库。 · 体验有序一致 即内外的有序,内在拥有统一的标准、规则及模式。外在有统一的设计语言让用户体验有序一致。 · 场景应用普适性 沉淀于微盟核心的 SaaS 业务,对“交易场景”拥有良好的普适性。既能满足当下需求,也能拓展延伸至更广泛的应用场景。 · 品牌调性多元化 丰富多元的场景选择性,满足不同品牌调性展现。贴合用户心智,维持品牌认知。在赋能品牌的同时,开发者更能探索出无限可能。 03.Titian 提供的能力 3.1 完善的基础组件 Titian 目前提供了 60+ 移动端基础组件,足以支撑绝大多数的业务需求。 [图片] 3.2 更丰富的多端小程序支持 借力微盟自研的小程序多渠道转码工具(即将开放),Titian小程序组件库可在微信、支付宝、小红书、快手等渠道的小程序顺畅运行。 3.3 React 和 Vue 同步支持 Titian H5 基于 Web Components 能力,Titian H5 提供 React 和 Vue 3.0 两套组件库。该两套组件库底层互通, API 一致,满足业务方丰富的使用场景。 3.4 H5 与小程序 API 统一小程序与 H5 组件库两者采用统一的 API 设计,方便开发者接入。 3.5 二次开发能力 在 Titian 丰富的基础组件上,用户可以快速开发满足自身需求的定制组件。在微盟已经有大量的案例落地,满足了丰富的业务需求。 [图片] 3.6 多维度主题切换 在确保品牌风格统一的前提下,Titian基于品牌调性提炼了具有共性的视觉特征,分别为颜色、图标、圆角。并用这些特征组合为“通用”、“潮流”、“亲和”套风格(后续会持续增加),让开发者随心选择,让产品视觉更贴合品牌调性,保持品牌识别度,维持C端用户对品牌的心智认知。 3.7 开箱即用的体验 Titian 提供交互式的示例,对于组件最常用的功能,可以直接配置相关属性。业务方可以直接上手操作,简单直观。 04. 开发工具 · 自研打包工具基于 esbuild , Titian 自研了小程序打包工具,让编译流程更加快速,开发体验更加友好。 · VS Code 提示插件 帮助用户在 VS Code 中快捷使用 Titian 组件。 · WXML 格式化工具一个 prettier 格式化插件,方便统一代码风格。 · Figma 设计组件Titian提供了一整套完整的设计组件资源,接入引用即可立即使用,方便快速搭建页面,设计组件覆盖代码能实现的所有能力,做到了设计-研发闭环。 · UED 验收工具对于 Titian 组件,打开验收工具配置后,长按即可变得半透明。可以帮助 UI 快速分辨出 Titian 组件。 05. 优秀案例 经过1年的迭代与不断调优,Titian已为微盟企业内部及微盟商城、CRM、CMS、商户助手、微盟客等客户,累积提供了5000+的移动端场景的应用及验证。 [图片] [图片] [图片] [图片] [图片] [图片] [图片] 未来,微盟将在 Titian 组件库的基础上做更多的探索,开放优化可视化建站平台、D2C 设计图转代码工具、物料市场等新工具新平台,敬请大家多多关注~ 反馈和共建请访问Titian UI - 多渠道移动端组件库PC端官网,进行体验! https://titian.design.weimob.com/ github链接:https://github.com/weimob-tech/titian-design 微盟云开发者可通过微盟云开发者工具中的Titian使用文档指引,查找使用方式! https://doc.weimobcloud.com/word?menuId=53&childMenuId=58&tag=3764
2023-02-07 - Skyline|在小程序实现原生相册的效果
相册在日常生活中经常使用到,如手机自带相册、朋友圈、商品展示图、评论贴图等等,都经常用到相册的能力。 👇下面演示 iOS 原生相册、朋友圈等相册使用效果,我们可以看到图片切换非常顺滑,视觉焦点不变。 [图片] 😭 但是在小程序中,页面切换会有明显的切换感。用户焦点会丢失,缺少视觉关联性。 [图片] 共享元素🔥 为了丰富用户交互效果、提升用户体验、增强视觉关联性,小程序支持了页面间的共享元素 下图展示有无共享元素的页面切换效果,可以看出使用共享元素之后,转场动画更灵活 [图片] 共享元素 经常作用在图片上,例如上面示例中的相册效果,是那么共享元素动画要怎么实现呢? 在页面跳转时,两个页面 key 相同的 share-element 组件则会产生飞跃的过渡效果 [图片] 在上一篇文章中,我们学习了 页面转场动画,共享元素动画跟页面转场动画是类似的,同样是在页面切换间的动画。 动画进度、时间 与 路由进度、时间保持一致(非自定义路由也支持共享元素动画) 在共享元素飞跃的过程中,前后页面图片的裁剪方式(mode) 可能不一致 这种情况下容易导致图片突然跳变,所以我们需要在飞跃的过程中改变图片的大小来保证平滑飞跃 [图片] 在共享元素动画进行的过程中,share-element 可以收到 onFrame 表示动画帧回调 我们可以在帧回调中处理内部元素的显示 例如:我们这里通过在帧回调中改变图片宽高来达到平滑飞跃的效果 // .wxml // .js // 初始化 attached() { this.aspectRatio = shared(0) this.curRect = shared(undefined) // 绑定 worklet 动画 this.applyAnimatedStyle('.img', () => { 'worklet' const curRect = this.curRect.value return { left: `${curRect.left}px`, top: `${curRect.top}px`, width: `${curRect.width}px`, height: `${curRect.height}px` } }) }, // 获取图片初始宽高比 onImageLoad(e) { const { width, height } = e.detail this.aspectRatio.value = width / height }, // 动画帧回调,调整图片大小 onFrame(data) { 'worklet' // 当前帧容器的宽高、进度等信息 const { begin, end, progress, direction } = data ... // 根据图片初始宽高比、共享元素容器、动画进度等计算出变化过程中的值 this.curRect.value = { left = lerp(begin.left, end.left, t), top = lerp(begin.top, end.top, t), width = lerp(begin.width, end.width, t), height = lerp(begin.height, end.height, t), } } 更多共享元素动画原理请查看 官方文档 手势搭配打开图片之后,我们经常需要用到手势来操作图片,如缩放、移动、双击等等 [图片] 我们上次学过的 手势系统 又派上用场啦 通过监听手势事件配合 worklet 函数即可在小程序实现图片预览效果 👇 下面演示缩放手势的处理,除了缩放之外,相册在手势处理上还有很多复杂的逻辑,包括惯性、边界逻辑判断等 点击查看更多相册相关的手势操作 // .wxml // 绑定缩放手势 let sharedValues = this.sharedValues ?? [] // .js // 绑定缩放 this.applyAnimatedStyle('#image', () => { 'worklet' // worklet 函数,sharedValues 变化时,函数会立即执行 return { transform: `scale(${sharedValues[SCALE].value})` } }) // 监听缩放 onScale(evt) { 'worklet' // 连续的手势状态 && 双指放缩 if (evt.state === GestureState.ACTIVE && evt.pointerCount === 2) { // 计算出当前真正的缩放值 sharedValues[SCALE].value = evt.scale / sharedValues[TEMP_LAST_SCALE].value sharedValues[TEMP_LAST_SCALE].value = evt.scale } } 最后,我们来看下小程序实现出来的相册跟原生相册的使用对比,在小程序也可以顺滑的实现类原生的效果啦~ [图片] 目前,同程旅行 已经上线了共享元素结合手势的相册效果,mark这个 相册源码 直接接入到你的小程序吧~ [视频]
2023-08-03 - 微信小程序可视化电影选座组件
推荐一款可视化电影选座组件,具体使用方法请看原文链接:https://juejin.cn/post/6996913047725932575 [图片] gitee地址:https://gitee.com/jensmith/source-coding 原文地址:https://juejin.cn/post/6996913047725932575
2022-04-12 - 分享小程序在app.js中全局管理websocket方案
社区有网友提问怎么在路由切换时保持websocket连接不中断?,我在回答中分享了我在实际项目中使用websocket的方案,这边整理一下。 主要思路是在app.js中全局处理websocket的连接和接收消息,收到消息后再把消息转发到页面,实际用到消息的页面接收消息做后续处理。具体代码如下 要引入mitt.js,百度一下,一个很小的文件(具体代码在文章最后) app.js [代码]const mitt = require('./utils/mitt').mitt ... App({ ... onLaunch: function () { let that = this that.globalData.bus = mitt() ... //连接socket ... //收到消息的回调中 if (msg.length > 0) { that.globalData.bus.emit('_socketMsg', msg) } ... } ... }) [代码] 要用到消息的页面 [代码]const app = getApp() ... Page({ ... socketMsg: function(msg){ //实际处理收到的消息 }, onShow: function () { let that = this app.globalData.bus.on('_socketMsg', that.socketMsg) ... }, onHide: function () { let that = this app.globalData.bus.off('_socketMsg', that.socketMsg) ... }, ... }) [代码] 附:mitt.js [代码]function mitt(all) { all = all || Object.create(null); return { on(type, handler) { (all[type] || (all[type] = [])).push(handler); }, off(type, handler) { if (all[type]) { all[type].splice(all[type].indexOf(handler) >>> 0, 1); } }, emit(type, evt) { (all[type] || []).slice().map((handler) => { handler(evt); }); (all['*'] || []).slice().map((handler) => { handler(type, evt); }); } }; } module.exports = { mitt: mitt } [代码]
2021-07-08 - 如何接入微信公众平台开发
在微信开放社区发现了不少同学都卡在token验证失败了,很多都是代码写的有问题,本人没碰到过代码写对还失败的 ---- 本文只介绍验证服务器地址,其它功能不做描述。---- 接入概述 接入微信公众平台开发,开发者需要按照如下步骤完成: 填写服务器配置(不做描述) 验证服务器地址的有效性 依据接口文档实现业务逻辑(不做描述) 校验规则: 将token、timestamp、nonce三个参数进行字典序排序 将三个参数字符串拼接成一个字符串进行sha1加密 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信 详细文档链接,请参考:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html 校验规则很简单,照着规则来就行了。 项目都是本地的,服务器URL需要外网80端口,所以用了natapp或者花生壳,具体怎么使用百度一下就可以了。 写了两个版本的java、nodejs,问题大多数都是java同学。为了方便演示没有提取工具类,token也是没有写在配置文件中,仅供参考。 java spring boot示例代码 修改application.yml [代码]server.port=80[代码] [图片] nodejs egg示例代码 修改config.default.js [代码]config.cluster = { listen: { path: "", port: 80, hostname: "" } }[代码] port改成80端口 egg https://eggjs.org/zh-cn/intro/quickstart.html [图片] 代码经过验证的,都是可以通过token验证的,有问题还没解决或者准备接入认证同学,可以参考一下代码,少走弯路 补充:golang版本 [图片] © 2019 BINNIE 吉祥 严禁做笔记
2019-12-31 - 小打卡小程序自动化构建及发布流程的工程化实践
原文地址及ppt: https://www.yuque.com/jinxuanzheng/gvhmm5/uy4qu9 目录 这次的分享大概分成三个模块, 分别是为什么做这件事,解决过程中的探索与实践,和我个人对于小程序开发的一些思考 为什么要做这件事? [图片] 首先说下为什么做这件事情,左图这个场景是我们之前的测试场景,首先在一个版本群里,@一下相关人员,丢一张二维码在群里,然后再发一个check list 发布阶段 场景描述 然后会发生些什么呢? 研发会问: 测试通过了么?”稍等“ 隔个5分钟后… 研发再问:”好了么““好了”, 但是很尴尬的有可能跳出一个产品b说我这边还有个功能没测完, 既然有产品A,产品B,那么也有可能有测试A,后端B,这就陷入了一个沟通小循环 [图片] 每天在这个小群里,历史记录里频次最高的发言就是就是"测好了么",“过审了么”,“发版了么”,被我们戏称为现实版的“三次握手”协议,也是这个小群里最“暖心”的问候 发布时间统计概况 [图片] 大概统计了下,每次发版的时长在20分钟左右,按照每天3次发布来计算,每天要花1小时的时间用来进行发布,而且发版并不是一版接一版,它是有间隔的,中间还要计算参与发版流程的同学,例如后端,测试,设计,产品等角色工作被“打断”的成本,作为相关方很难有集中的块状时间去做事情 这样会导致大家对发版这件事情有抵触情绪,我记得当时听到最多的一句话就是,“啊,又发版了?” 这样整体迭代是速度快不起来的, 然而作为创业公司来说,快速迭代,快速试错是一个非常重要的能力,这种状态下明显是一件不太能接受的事情 时间花在了哪? [图片] 可以看左图,这是最开始小打卡最开始的一个发布流程,即修改发布环境 -> 点击上传 -> 填写版本号/版本描述 -> 发布体验码测试 -> 提交审核 -> 点击发布 这是一个流程简洁么?在理想状态下是的,然而现实情况往往要更复杂一些 那么复杂在哪里? 第一点就是check关键信息 提审/提测前是一定要去check关键信息的,为什么?因为是修改发布环境,更改的版本信息是人工来进行修改的,不论多么严谨的人都是有出错的可能性(ps: 其实挺不好意思的,我就干过这个事,而且两次) 第二点是信息同步的问题, 从左图可知,我们是有两个需要经过确认环节的,一个是测试环节,一个是审核环节,这里通常会发生什么事呢? 这里可以一句话概括为“产品/运营等其他相关方很难在第一时间获得发布信息,缺乏有效的通知手段”,其实就是信息同步的问题 查询场景 场景描述 [图片] 以为这就结束了么,在日常工作中,“hey咱们上个版本发了哪些需求”这是一个再寻常不过的问题了,大部分情况下我们会很快给出答案 然而会问这个问题人可就多了,它的上游对接的是客服,技术,产品,市场bd等等部门 根据不同的职责属性,和人数,这个询问次数会被乘以个n次 下面是,根据职责划分,可能会询问这个问题的部门,产品,运营,技术,客服…貌似已经覆盖了大部分的公司职位了 可见是一个长期且重复性相当强的沟通行为 很头痛,但是实际上就是缺了一份更新日志或者说是文档的问题 更新日志需求 [图片] 左图是微信小程序的更新日志文档,大概包含了: 版本号更新日期需求列表可以看到每个迭代发布了什么,什么时候发布的一目了然,自然不需要再来进行询问这个动作 但是我们刚起步没有这份文档的时候怎么办呢?一是根据tag标签去查对应的git log,二是手动去记这么一份wiki 两种做法都存在一定问题: 第一种的问题是除当前项目的开发人员,无法准确获取信息,例如产品/运营/客服同学,首先并不能指望每个人都可以使用git,另一个方面源码私密性也是一个问题 第二种的问题在于 完整性的问题,人为记录更多会选择性的记录,例如里程碑式的业务需求信任度问题,当存在不完整的可能性时,就变成了参考而非答案需要付出额外的人力成本(当然还有可以利用commit message直接生成,但是和团队的提交风格相冲) [图片] 很可惜,没有,所以,很痛 总结 减少无效沟通,避免重复性时间损耗,提高团队效能 --从团队效率上来说,我们并不希望“三次握手”事件的发生保障信息同步能力,确保组织内部信息的一致性和即时性 – 作为公司的主要产品迭代信息是一定要透明化的,不同部门之间合作才不会出现偏差提高版本稳定性,杜绝潜在的发布风险 – 这个下一话会讲最后一个是满足一天多次的发布需求 – 如果按照现在的发布效率来看,1天1-2次就是极限了,不能解决沟通成本问题反而是降效解决方案 概况 [图片] 就小打卡目前的解决方案来说,将问题大概分为了3个模块 自动化构建 - 主要负责打包编译,具备发布体验版的能力更新日志 - 提供版本记录,版本快照能力通讯机制 - 主要负责提供有效的通知手段这里面有一个简单的依赖关系,更新日志依赖构建能力提供原始数据集,通讯机制主要功能是同步消息,但是内容依赖更新日志 总体流程图 [图片] 通过自动化构建,发布体验版并上传到git,git再通过webhook能力打到我们自己的服务器,处理log信息,上传版本快照,将处理后的数据存储到mysql,通过邮件服务,通知到各个方向,最后是视图层进行展现 自动化构建 构建流程需要具备哪些能力? [图片] 首先解决构建问题,首先做事情之前我们要先想一个问题,构建流程需要具备哪些能力? 打包编译的能力 - 解决语法转换/条件编译/代码检查等一系列问题,同时可以在这里定义一些全局常量,例如之前遇到的切换环境的问题发布能力 - 提交体验版的通用做法是点击小程序开发工具的右上角“上传按钮”,其实除了这个方式之外微信提供了另外两种提交方式,命令行和http服务,具体情况可以翻下微信文档版本管理的能力 - 主要是帮助我们规划相关的版本号和版本描述,可以类比npm的version manager最后是要保证一定的扩展性,例如小程序目前支持npm构建功能,如果没有流出扩展空间,是需要去改源代码添加的,并且这套构建流程在一定程度上是需要保障多项目适配的[图片] 这个gif是基于我们自己的一个脚手架工具实现的一个发布流程,可以看到这是一个交互式的导航,输入 xdk-cli publish 这个命令后会询问我们 是否发布正式环境设置一个版本号(这里填写版本号有一个增长逻辑,默认阶段版本号 + 1)填写一个版本描述其实就是一个 切换环境 + 填写版本信息的过程,之后便是进入发布环境 具体方案 [图片] 那么输入 xdk-cli publish 到底做了些什么事呢? 读取项目的本地配置文件,包括脚手架配置和版本管理文件,然后弹出询问命令,填写需要确认的相关信息执行发布钩子上传体验版,执行微信提供的上传命令执行发布后钩子上传体验版成功,返回一个回调这里为了保障扩展性,cli只保留了上传和版本管理的功能,预留了两个钩子函数,分别是发布前后执行,针对于项目定制化的一些task都放在了钩子内,例如babel,lint,sass,小程序的npm功能,也包括git commit, git tag 的提交 好奇cli具体实现的可以微信扫下面这个码,是我之前写的一篇关于搭建cli的文章 带来的收益 左图是优化后的流程,可以看到通过 cli , 我们合并了“修改发布环境 + 点击上传 + 填写版本信息三个环境“,通过直接通过交互命令进行发布, 单从流程角度讲,我们的收益是什么 杜绝了切换线上线下环境问题导致的发布错误版本号填写错误的问题成为历史合并了发布环节,整个提交流程只需要在编辑器中进行即可更新日志 关于更新日志,之前背景中有介绍我们的目标是:**帮助非项目组开发人员快速了解每个迭代的更新信息 ** 三个问题 [图片] 上面是我归纳的三个核心问题,在做这件事之前时一定要想清楚的: 哪些字段需要收录怎么看?在哪里看?什么时候更新日志记录关于第一个问题,哪些字段需要收录? 最基本的有:版本号,版本描述,上线时间,需求列表其次是一些需求的开发人员,审核人员,并且该需求关联的Pr 和 Prd是什么,以及当前版本的状态 怎么看?在哪里看?我们是放在自己内部的线上管理平台上,利用表格的形式展示 什么时候更新日志记录?版本状态出现变动的时候,例如提测,提审,发布,回滚的时候 产出的需求 [图片] 上面的三个问题其实也总结出我们要准备要做些什么。分别是 数据采集,数据存储,状态管理,事件钩子,视图层 数据采集很好理解,收集一些字段数据采集完需要存储到一个线上可访问的数据库,方便我们视图层和后续的一些应用事件钩子,什么时候通知我们需要更新数据,变更状态视图层,承载内容的地方这时候发现本地构建已经很难满足这些需求了,需要一台稳定的线上服务器的来处理这些任务,我们这里是用node来搭建 现有流程 [图片] 这个是我们关于更新日志的现有流程,上面这一层整个是node服务 先是通过构建流程上传代码到git, git通过webhook能力,通知到node服务,收到通知后更新服务器本地代码,提取中间的提交信息和版本信息,压缩上传oss是为了做版本快照,最后将准备好的数据插入mysql 视图层可以根据存储好的数据展示相应内容 案例展示 [图片] 这个图是截的我们管理系统的一张图, 从左到右分别是:版本号,描述,状态,发布时间,还有两个功能选项: 修改状态,下载代码包(其实就是做过处理后的快照) 下面是项目包含的信息:需求类型,需求列表,需求列表的单个item里又有开发人员,审核人员,审核时间,关联pr,和关联的prd 可以看到整个版本信息结构是一目了然的 关键点 [图片] 整个流程最关键的点是怎么整合这样一份完整信息,实际上一份信息是通过 git Msg,Pr Msg, 版本信息,和oss存储信息 合并而成的 为什么git Msg和Pr Msg同属git相关的能力要分开说? [图片] 主要是因为生命周期不同, git Msg依赖于tag hook,tag hook的定义是新的版本诞生,主要是从git log中提取相关版本信息,但是统计维度没办法具象到PR节点,得到更详细的数据,例如Pr标题,开发人员等等 pr Msg依赖于Pr hook,每次提交Pr都会记录相关信息并入库 实际上走的是两套不同的hook服务,入库时也分属不同的两张表,取数时根据prId进行表关联, 看下左图是实际的version信息,这里根据log信息提取出pr_id,推入数组,右图可以理解为Pr表,里面有需求相关的详细信息 具体怎么解析commit msg?npm上很多第三方的包,非常方便 通讯机制 再说最后一个通讯模块,做这个东西的意义是为了解决 “产品/运营等其他相关方很难在第一时间获得发布信息,缺乏有效的通知手段” 这一问题 提取关键词 [图片] 这里可以从这句话里可以提出几个关键词,第一时间,获得发布信息,通知手段,这三个关键词也是我们后续要解决的重点方向 [图片] 首先是关键时间节点,因为具备这个特性所以需要确定几个关键的时间节点进行推送,我这里列的是 测试状态,提审状态,发布状态,回滚状态 这四项 其次是内容详情,肯定是需要有质量的版本信息,关于版本信息刚才讲到的更新日志是一个现成的服务,我们这里直接将它作为数据源,提供必要数据 其次是通知方式,这个就多种多样了,邮件,钉钉,微信等等,我们这里因为用的是企业邮箱而且开发成本比较低,直接用 nodemailer 第三方库搭了个邮件服务 [图片] 关于状态流转 分为两步,手动变更 和 自动变更 手动变更 [图片] 目前阶段来说,手变更是主要变更方式,同样也是兜底方式,大概形式可以看右图,提供了各种状态的按钮,每种状态变更都会通知到node服务中的邮件模块,帮助我们进行消息群发 另一种hack方式 [图片] 自动变更算是一个比较hack的操作,本质上是因为缺少小程序公众平台暂时没有提供hook能力,所以自己这边利用google浏览器的一个叫做油猴的插件在网页上加了这样一个钩子 可以看左图点击提交的时候,会触发脚本绑定的自定义事件,这个事件直接调用我们服务商状态流转的接口帮助我们进行修改,不需要再去后台进行变更 非常期待微信团队后续开放这样一个hook能力,帮助我们不依赖本地客户端实现功能闭环 当前的发布流程 [图片] 这是最新的一套流程,首先是各个组内自测,合并pr,通过命令直接上传体验版,并提交审核,同时发送邮件提醒已经审核,各小组收到审核后进行集成测试,最后邮件提醒当前已发布 新的流程每次发版消耗时间控制在5分钟左右,最主要的是他是一个非阻塞的形式,在发布期间完全可以做自己的事情 对开发人员来说需要关注的环节只有,合并pr,本地执行一次命令,最后在微信公众平台操作提审和发布时的扫码工作对其他人来说,在接到邮件时,直接执行对应需要完成的任务即可[图片] 这套新的流程执行之后,这个测试小群里“三次握手”的场景成为了历史,从最暖的小群,变成了最冷的小群 价值提现 [图片] 首先是解决了我们先前提到一些问题,例如: 拒绝任何形式的“三次握手“,沟通成本降低,无论产品/研发更聚焦于业务本身 - 减少无效沟通具有稳定可靠可追溯的版本记录,确保组织内部信息的一致性邮件服务的送达机制,不再需要人力去传递相关信息,且及时性,完整性得到保障节省下来的资源,足以支撑一天发布多次版本的需求也可以帮助团队小伙伴扩展技术边界 - 接触一些不一样的东西 未来的一些规划 [图片] 一些个人的思考 [图片] 之前有不少同学问我:“小程序开发天花板低,对职业成长不利怎么办?”,最早我也有过类似的想法,经历过一些事情后,总结出了下面的一些点: 在任何一个领域做到“精通”都并不容易,提出这个问题之前首先需要看到“天花板”我们并不是在做小程序,而是在“解决问题”,千万不要要自我设限,认为做小程序开发只能做小程序善于探索边界,主动推动并解决问题形成闭环,解决边界问题才是最有价值的事情工作更重要的是学习做事方法,积累方法论,形成自己的思维框架最后一点追求卓越非常重要,我认识的很多非常厉害并且优秀的人都有这么一项特质 什么是追求卓越?就是做完一件事情,都是要总结复盘思考怎么才能做得更好,哪怕当前的资源并不能支持你在现在的阶段去实现,但是一定要确保自己是有延展性的 当能够把上面5点想清楚的时候我觉得对自己的定位应该不仅仅是小程序开发者了,而是一个互联网从业者
2019-12-24 - 个人项目学习笔记
前言:看完了比赛项目,感觉像是经历了一场头脑风暴,项目的起名、涉足行业、内容、UI、架构,以及业务设计等,感到都有很多学习的地方。打算做一个学习笔记贴。 一、分类 *项目有点多,我自己做了一下分类,以便查找。 记账与备忘: 《大学生记账本》 《咸鱼记账》 《KrisQin记账本》 《MY备忘》 《家庭多用记事本》 《ygjtools》 《乐考吧》 《日程管家》 《YAccount记账助手》 《随变记账》 《待办事项工具》 《小婷和小天一起记账》 《初心日历》 《寝记账》 《智慧账本》 失物招领: 《爱心收发室》 《帮寻小站》 《月见》 《长大寻物》 《悦寻失物招领》 《校园寻回》 《失全拾美》 健身类: 《健身助手力量日记》 《活力健身房》 《RedPoint红点》 《健身小程序简介》 音乐影视: 《云享Music》 《king电影》 《無音不泉》 《电影周周荐》 AI识别: 《鹦鹉AI端侧识别》 《ai视觉测试》 《图文识别》 《AI物以类聚》 《AI写诗》 医疗健康: 《人体生理指标》 《吃药小助》 《己目》 《体重MM》 《菲特日记》 《医医查》 《全国核酸检测资质医院查询》 《糖友饮食助手》 《每日戒糖》 《自助心理成长》 《预约挂号小程序》 《蓝医先生》 社团活动: 《阮薇薇点名啦》 《山大clubs》 《素拓百分百》 《小小微距》 《娱乐投票小程序》 《活动栏》 《薇科技弹幕墙》 《头马报名》 《滑伴》 《科联答题》 《招新Plus》 《文艺比赛小行家》 《布告》 《BJUT活动助手》 《准到聚餐》 学习工具: 《错题小本本》 《为高考加分》 《分录英雄》 《口算助手》 《答案sou》 《教资易取》 《快刷题库answer question》 《拾一英语》 《魅力单词》 《魔方训练计时器》 《focusair》 《Y计算器》 《高级工匠心录》 《成长课程表》 《“倾听者”综合型语音评价系统》 《小青考证》 《IAI CDS》 教育培训: 《来这儿学》 《微学堂(在线学习平台)》 《大学生资源共享平台》 《袋鼠培培》 《宝贝积分管理》 时间管理: 《西瓜清单》 《语音倒计时器》 《西红柿时间管理》 《Do More打卡小程序》 《倒计时》 《tomato clock》 《step by step》 《BT清单》 《tusake Today》 《叮咚倒数日》 《FTodoList》 校园管理: 《班级价值分》 《校园缺勤录》 《教务小助手》 《工程课表》 《云迎新》 《CAN课程表》 《简单的课表小程序》 《运动会管理系统》 《重邮课后小程序》 《高校信息共享平台》 《校园简单易》 《中北请假助手》 《知侬》 《ITD智慧校园》 《We广油》 《江大电服》 校园介绍: 《阿里嘻嘻》 《民大新生助手》 《北邮宣讲通》 《志愿校园》 《浙里淘志愿》 《云校知》 《校拍》 校园社区: 《北院守夜人》 《校园墙》 《AIB校友会》 《校园小唤》 《xcu许院生活》 物流快递: 《高校联盟-快递代取》 《速派递》 《物流小程序》 预约与邀请: 《天翊图书馆预约》 《农大饭食》 《课室助手》 《会议邀请函》 《weSport》 《哪天约》 《定约》 《易约行》 《自闭间预定》 《简约约拍》 《实验室设备预约助手》 《QSCamera》 《预约班车》 《心暖农侬》 《书香长大》 《私约团课》 情侣婚礼: 《趣婚礼》 《小酒馆》 《恋人小清单》 《云表白》 《情侣券》 《旅梦恋爱》 《快表白》 《恋爱空间》 购物商城: 《微信云商城》 《吃否CHIFOU》 《云端商城小程序》 《购物》 《柠檬商城》 《武冈微商城》 《狗头的店,狗头管理》 《林林的妙妙屋》 《芳甸鲜花商城》 《优鲜配送联盟》 《汇尤e家》 《云开发带后台商城系统》 《预付费机票销售小程序》 《微购收单》 知识普及: 《科普小程序》 《百词百科》 《BOSS百科》 《球员搜搜》 《火查查》 《急速查病》 《趣答星球》 《铁路生涯》 《趣酿》 《吃吃等你》 《男人买菜》 《诗华社》 《天天诗词》 《心跑道》 程序员: 《sentry 小程序客户端》 《GitPark》 《码农SHOW营》 《OTP动态验证码》 《微源库》 《LE编程》 《一起来学计组叭》 《统一运维平台》 《见字如面》 图像处理: 《莉龙美颜工具》 《图像复原微信小程序》 《Hi头像》 《我是主角》 《修补匠》 《祥云》 《抽屉表情》 《人脸识别虚拟仿真实验》 《照片时光机》 语言翻译: 《CEnews》 《多源在线翻译》 《汉泰小词典》 《识译小程序》 社区周边: 《社区速修》 《虚拟社区》 《简物业》 《租户在线》 《美今管家》 《顾家》 《雨中送伞》 《盲小鹿》 树洞与留言: 《海豚时光瓶》 《树洞》 《苦海匿舟》 《深大小树洞》 《LMSH7TH》 简历与工作: 《个人简历Plus》 《快速找工作》 《猿宝典》 《云线名片》 《InterviewHub》 《企业招聘》 《JF校园云招聘平台》 《普罗名特》 《校园招聘》 资讯与娱乐: 《Killkinfe》 《开心小杜》 《旅小布短视频》 《心灵鸡汤大全》 《轨道nighty night》 《拯救不开心》 《大宗交易数据查询分析助手》 《糗事》 旅行: 《我的旅行箱》 《云航助手》 《宝塔出行》 《PicGo图旅》 城市宣传与服务: 《数字余杭》 《哏儿通》 《联系群众客户端》 《城市预警系统》 《郑州限行查询》 《佤山行》 商业工具: 《软著助手》 《义思丽代办平台》 《契约farm》 办事工具: 《省计数字监理》 《OA外勤管家》 《报工小助手》 《make的测评程序》 《实验室管理小程序》 《星河意见箱》 《梦凡云OA》 《微助helper》 《群消息公示》 《安全帽智慧监控小程序》 地图打卡: 《高级打卡鸡》 《摄影地图游客版》 《同学在哪儿》 《每日步数打卡》 《生活智打卡》 《打卡日历》 《Mayday Online》 《嘿!我在这儿!》 《心里有树》 《地图留言》 《印纪》 天气与日历: 《一眼天气》 《历史日历》 《7日天气》 《历史上的今天TIH》 《实用小工具》 游戏娱乐: 《趣味游乐城》 《假如生命很短暂》 《磁力积木3D预览》 《MusicColorBlock-Detail》 《消灭癌细胞》 《红小包抽奖》 《大师请提笔》 《画画的北鼻》 《东方小游戏》 存储与分享: 《酷传CoolTran》 《我存》 《次元乌托邦云网盘》 《闪加》 《悦分享》 《云享坊》 生活工具: 《WiFi生成码》 《古老的API小工具》 《日常工具box》 《柠檬收纳》 《买它or not》 《我车呢》 《微信小程序工具箱》 《小记易》 《电魔方智能家居》 《缸中之鱼导购系统》 《格式转换工厂》 《小神助手》 《我家的WIFI》 二手交易与租赁: 《大学校园闲置物品交易平台》 《宝宝约玩》 《精简之校园二手交易平台》 《学辰ing》 《二手市场》 《虾麦》 《校园二手购》 《零工哥》 《易珠》 《瓜大e拼车》 外卖点单收银: 《来一杯a》 《美食屋》 《外卖系统》 《便利下单助手》 《云智慧收银》 《超市Boss助手(零售助手)》 《为特餐饮助手》 《Holly食刻》 《微信自助点餐小程序》 《餐饮流水记账》 《seven取餐小程序》 《校内外卖》 《秀食餐饮小程序》 《校云通》 日记博客论坛: 《博客系统》 《天天读书》 《myVlog》 《红推》 《论坛小程序》 《一瞬相册》 《席博》 《一只书匣》 《社交平台》 《图迹圈》 《MallBook》 《青存纪》 《酒肆 家谱》 《比斯兔u》 《校园书友》 《CC交个朋友》 《点滴互助》 《广大搜搜》 《社交点评》 《迷你论坛》 《Simple Note 短记》 垃圾分类: 《垃圾问问》 《垃圾分类小程序》 《垃圾分类赢好礼》 宠物: 《萌宠创造营》 《宠幸治疗》 《宠物营地》 《流浪猫速查手册》 《泊宠》 物联网: 《lononiot》 《LoRa智能家居管理》 《温湿度实时监控及开关控制小demo的设计》 《HomeAssistant》 《流量计设备性能测试平台》 疫情防控: 《行程助手Plus》 《每天都要上报体温》 《校园疫情管理小程序》 《CUMTB疫情管控期间学生外出申请系统》 《疫简签》 地摊: 《地摊生活》 《逛逛地摊》 《迷你小摊》 二、学习笔记 (一)名词解释 1,分录: 会计分录亦称“记账公式”,简称“分录”。它根据复式记账原理的要求,对每笔经济业务列出相对应的双方账户及其金额的一种记录。 2,文玩: 指的是文房四宝及其衍生出来的各种文房器玩。这些文具造型各异,雕琢精细,可用可赏,使之成为书房里、书案上陈设的工艺美术品。 3,sentry: Sentry是一个开源的实时错误追踪系统,可以帮助开发者实时监控并修复异常问题。 4,GitHub: 是一个面向开源及私有软件项目的托管平台,因为只支持Git作为唯一的版本库格式进行托管,故名GitHub。 5,软著: 全称是计算机软件著作权,是指软件的开发者或者其他权利人依据有关著作权法律的规定,对于软件作品所享有的各项专有权利。 6,打卡: 网络流行词,原指上下班时刷卡记录考勤。现衍生指到了某个地方或拥有某个事物(一般会向他人展示)。网红、圣地打卡。 7,番茄工作法: 是简单易行的时间管理方法。使用番茄工作法,选择一个待完成的任务,将番茄时间设为25分钟,专注工作,中途不允许做任何与该任务无关的事,直到番茄时钟响起,然后进行短暂休息一下(5分钟就行),然后再开始下一个番茄。每4个番茄时段多休息一会儿。 8,码农: 可以指在程序设计某个专业领域中的专业人士,或是从事软体撰写,程序开发、维护的专业人员。但一般Coder特指进行编写代码的编码员。 9,树洞: 来源于童话故事《皇帝长了驴耳朵》,意思是一个可以袒露心声的地方,是指可以将秘密告诉它而绝对不会担心会泄露出去的地方。 10,AI: 全程人工智能(Artificial Intelligence),英文缩写为AI。它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。 11,OA: 办公自动化(Office Automation,简称OA)是将现代化办公和计算机技术结合起来的一种新型的办公方式。办公自动化没有统一的定义,凡是在传统的办公室中采用各种新技术、新机器、新设备从事办公业务,都属于办公自动化的领域。 12,素拓: “素质拓展训练”的简称。素质拓展起源于国外风行了几十年的户外体验式训练,通过设计独特的富有思想性、挑战性和趣味性的户外活动,培训人们积极进取的人生态度和团队合作精神,是一种现代人和现代组织全新的学习方法和训练方式。 13,磁力积木: 由若干个不同形状的积木单体组成。在各个单体的边沿嵌有磁铁或磁片,磁铁上履盖有一层搪瓷,利用磁力使各单体紧密连接在一起。 14,教资: 指教师资格证考试,是由教育部考试中心官方设定的教师资格考试。 15,Instagram: 又叫照片墙,是一款运行在移动端上的社交应用,以一种快速、美妙和有趣的方式将你随时抓拍下的图片彼此分享。 16,Redpoint: 攀岩术语,是指事前曾练习爬过该路线,以先锋攀登的方式、无坠落地完攀该路线。 17,头马: 是Toastmasters的中文简称,于1924年在美国加州成立。是一个非盈利性的、由会员自行管理的组织,目前已在全球一百多个国家成立了上万个俱乐部。 *上述名词介绍来自百度百科与知乎。 (二)出现的学校 河北科技大学(河北-石家庄) 电子科技大学(四川-成都) 重庆邮电大学(重庆) 哈尔滨理工大学(黑龙江-哈尔滨) 桂林航天工业学院理学院(广西-桂林) 河南理工大学(河南-焦作) 河北北方学院(河北-张家口) 北方民族大学(宁夏-银川) 山西大学(山西-太原) 西安交通大学(陕西-西安) 西安电子科技大学(陕西-西安) 华中科技大学(湖北-武汉) 深圳大学(广东-深圳) 浙江大学(浙江-杭州) 湖南大学(湖南-长沙) 武汉大学(湖北-武汉) 广东技术师范大学(广东-广州) 西北民族大学(甘肃-兰州) 北京邮电大学(北京) 中国民航大学(天津) 广东机电职业技术学院(广东-广州) 长江大学(湖北-荆州) 华南理工大学(广东-广州) 包头铁道职业技术学院(内蒙古-包头) 重庆工程职业技术学院(重庆) 江苏大学(江苏-镇江) 南京邮电大学(江苏-南京) 华南理工大学广州学院(广东-广州) 长安大学(陕西-西安) 泉州师范学院(福建-泉州) 桂林电子科技大学(广西-桂林) 广西医科大学(广西-南宁) 华南农业大学(广东-广州) 山西农业大学(山西-晋中、太原) 南京大学金陵学院(江苏-南京) 广东石油化工学院(广东-茂名) 兰州交通大学(甘肃-兰州) 东华理工大学(江西-南昌、抚州) 中山大学南方学院(广东-广州) 广州大学(广东-广州) 中国矿业大学(北京) 南京工业大学(江苏-南京) 中北大学(山西-太原) 华中农业大学(湖北-武汉) 东莞理工学院(广东-东莞) 广东工业大学(广东-广州) 上海电机学院(上海) 南宁职业技术学院(广西-南宁) 台州职业技术学院(浙江-台州) 福州大学(福建-福州) 厦门理工学院(福建-厦门) 美国纽约大学(美国) 英国曼彻斯特大学(英国) 昆明理工大学(云南-昆明) 天津城建大学(天津) 北京工业大学(北京) 广东建设职业技术学院(广东-广州) 湖北师范大学(湖北-黄石) 许昌学院(河南-许昌) 西北工业大学(陕西-西安) (三)个人认为的特别题材 动物保护-鹦鹉AI端侧识别 健康管理-吃药小助 学习工具-口算助手 商业工具-软著助手 图像处理-摄影地图游客版 AI换脸-我是主角 个性服务-雨中送伞 情侣生活-恋人小清单 心情宣泄-苦海匿舟 走失找回-月见 AR躲猫猫-萌宠创造 文艺共鸣-轨道nighty night 心里健康-心暖农侬 模拟红包-红小包抽奖 攀岩健身-RedPoint 职校沟通-铁路生涯 酿酒乐趣-趣酿 盲人助力-盲小鹿 历史日历-历史上的今天 消防检查-火查查 情侣福音-情侣券 家庭互助-顾家 宠物关注-流浪猫速查手册 停车助手-我车呢 诗歌创作-AI写诗 智能家居-LoRa智能家居管理 智能招领-悦寻失物招领 你画我猜-画画的北鼻 随手反馈-城市预警系统 仿真识别-人脸识别虚拟仿真实验 智慧校园-ITD智慧校园 活动协调-哪天约 家庭物联-HomeAssistant 地热监测-流量计设备性能测试平台 (四)官方公布的复赛名单 校园赛道: [图片] 职业赛道: [图片] (五)官方公布的决赛名单 校园赛道: [图片] 职业赛道: [图片] (六)最终决赛成绩 校园赛道: [图片] 职业赛道: [图片]
2020-11-14 - 如何使用painter实现一个海报编辑工具——以taro为例
文章开始前先做个简单的声明:这篇文章主要面向刚了解到 painter 的开发者,文中使用的框架、实现的方式只作为一种参考,并不一定是最佳实践。而使用 painter 能够做的扩展不止下文提到的这些能力,只是以下样例更为方便理解。欢迎各位酌情阅读。 自动态模版功能发布后,陆续有开发者开始尝试使用动态模版能力,我们也收集到了大家反馈的一些问题。这一系列文章的主要内容是从头开始实现一个简单的、基于 painter 动态模版能力的海报编辑工具。希望能通过这一过程,让大家了解为什么我们推出了动态模版能力,以及如何快速上手。同时在文中,也会统一回答一下关于动态模版使用的一些问题。文章中实现的编辑器代码,可以在https://github.com/Kujiale-Mobile/Taro-Painter-Demo/tree/2.x获取 先期准备 本次我们使用 2.2.15 版本的 taro 创建一个空项目 [代码]$ taro init [代码] painter 组件是使用了 mina-painter 包(https://www.npmjs.com/package/mina-painter),这是我们封装的 taro 风格组件,供 taro 1.x/2.x 版本使用,支持 base64 图片与 canvas2d 模式。 [代码]$ yarn add mina-painter [代码] 创建空页面,并引入 painter 组件。 [代码]// pages/index/index.tsx import Painter from 'mina-painter'; ... render() { return ( ... <Painter customStyle={`margin-top:5vh;`} customActionStyle={customActionStyle} dancePalette={danceTemplate} palette={outputTemplate} action={action} clearActionBox={clearActionBox} onImgOK={this.handleImgOk} onDidShow={this.handleDidShow} onTouchEnd={this.handleTouchEnd} onViewClicked={this.handleViewClick} onViewUpdate={this.handleViewUpdate} /> ... ) } [代码] 写一个简单的海报模版,包含 painter 内各种 view 类型。 [代码]// palette/index.ts const template = { width: '750rpx', height: '1334rpx', background: '#FFFFFF', views: [ { id: 'rect_10', type: 'rect', css: { scalable: true, color: '#F5F2EC', height: '348rpx', width: '750rpx', bottom: '0rpx', left: '0rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'rect_9', type: 'rect', css: { scalable: true, color: '#CBBD9F', height: '646rpx', width: '388rpx', left: '0rpx', top: '456rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'rect_8', type: 'rect', css: { scalable: true, color: '#EBE5D7', height: '160rpx', width: '360rpx', top: '222rpx', right: '0rpx', minWidth: '80rpx', minHeight: '80rpx', }, }, { id: 'qrcode', type: 'image', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/jpeg/1623053391518/3EF9BB7ABE024959EB2A0E81078B40FA.jpeg', css: { width: '202rpx', height: '202rpx', bottom: '76rpx', right: '40rpx', borderRadius: '8rpx', borderColor: '#FFFFFF', borderWidth: '4rpx', }, }, { id: 'worker_type', type: 'text', text: '门店店长', css: { scalable: true, deletable: true, left: '156rpx', bottom: '76rpx', fontSize: '24rpx', color: '#656c75', lineHeight: '34rpx', }, }, { id: 'worker_name', type: 'text', text: 'tester', css: { scalable: true, deletable: true, fontSize: '30rpx', fontWeight: 'bold', color: '#333', left: '156rpx', bottom: '114rpx', width: '280rpx', lineHeight: '42rpx', maxLines: 1, }, }, { id: 'avatar', type: 'image', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/png/1623053110600/BDA064C5ECDCB7DD50DEB466C70E2EB0.png', css: { width: '80rpx', height: '80rpx', borderRadius: '40rpx', left: '52rpx', bottom: '76rpx', }, }, { type: 'image', id: 'image-main', url: 'https://qhstaticssl.kujiale.com/newt/100082/image/jpeg/1623053489433/54EE335A9C385A3D99D8664CB9135F84.jpg', css: { width: '672rpx', height: '672rpx', mode: 'aspectFill', right: '0rpx', top: '314rpx', scalable: true, minWidth: '120rpx', }, }, { type: 'rect', css: { width: '666rpx', height: '2rpx', top: '144rpx', right: '0rpx', color: '#EBEFF5', }, }, { id: 'name', type: 'text', text: '一个蒙着红色布的——球?', css: { scalable: true, deletable: true, fontSize: '32rpx', color: '#383c42', maxLines: 1, width: '480rpx', left: '76rpx', top: '74rpx', lineHeight: '44rpx', }, }, { id: 'product', type: 'text', text: '¥9999', css: { scalable: true, deletable: true, fontSize: '80rpx', lineHeight: '90rpx', fontWeight: 'bold', color: '#383C42', textAlign: 'center', left: '76rpx', top: '170rpx', }, }, ], } [代码] 以上种种准备好之后,我们就能得到这样的一个页面: [图片] 这个页面有最基础的点选、拖动能力,通过配置 view.css 的 scalable 和 deletable 属性,可以使用 painter 内置提供的缩放功能。 怎么样,是不是已经功能很完备,好像可以满足需求了啊~好,今天的分享就到此为止(并不是) [图片] 接下来,我们主要会为 text 、image 提供一些能力拓展,并实现基本的撤销、恢复功能。能力拓展的方式是相似的,相信在看完文章后,你就可以熟练地为任意 view 类型拓展能力了。 通过刷新整个 palette 方式进行的操作 [代码]// pages/index/index.tsx refreshPalette = (palette?: IPalette) => { this.setState({ dancePalette: palette || { ...this.currentPalette }, }); }; [代码] 最简单的刷新海报的方式就是直接刷新整个 palette 了,这种操作即便是不使用动态模版也一样可以用。这种方式开销大,速度相对慢,但是可以完全改变海报的结构 删除 View [图片] 虽然 painter 提供了自定义删除 icon 的方法,但是点击删除按钮,你会发现 view 并没有被删除。这是因为我们希望这种修改 palette 的操作能够让外部主动操作,而不是将删除操作也内置——那可能会导致你对自己写的海报模版失去掌控。要想实现删除逻辑非常简单,当用户点击删除按钮时,我们可以从 onTouchEnd 处监听到一个 type = ‘delete’ 的事件。 [图片] 从 palette 中找出对应的 view 并删除,然后更新 palette 就能完成删除操作了。 [代码]// pages/index/index.tsx this.currentPalette.views.splice(detail.index, 1); this.refreshPalette(); [代码] 修改背景 修改背景更为简单——直接改 palette 的 background 属性,然后刷新模版即可 [图片] [代码]// pages/index/index.tsx this.currentPalette.background = color; this.refreshPalette(); [代码] 添加新 View —— 以 text 为例 [图片][图片] 准备一个预先定义好样式的 text 类型的 view ,将输入内容填充后塞入模版的 views 中,最后刷新模版即可 [代码]// common/index.ts export function getBlankTextView(text?: string): IView { return { type: 'text', text: text || '', id: `text_${new Date().getTime()}${Math.ceil(Math.random() * 10)}`, css: { scalable: true, deletable: true, width: '384rpx', fontSize: '36rpx', color: '#000', textAlign: 'center', padding: '0 8rpx 8rpx 8rpx', top: '50%', left: '50%', align: 'center', verticalAlign: 'center', }, }; } // pages/index/index.tsx this.currentPalette.views.push(getBlankTextView(inputValue)); this.refreshPalette(); [代码] 通过刷新 action 方式进行的操作 [代码]// pages/index/index.tsx refreshSelectView = (view?: IView) => { this.setState({ action: { view: view || this.currentView }, }); }; [代码] painter 动态模版功能的一大改动就是增加了 action 属性。当我们向 action 传入一个 view ,painter 会去寻找与其匹配的 view 并刷新状态。通过这种方式,我们最小化了需要修改的内容,从而减少了 painter 所需要的渲染时间。 刷新选中view的样式——以 text 为例 通过监听 onViewClick 事件,我们能够获取当前点击的 view ,在确定当前 view 后,我们就可以通过修改改 view 的 css ,然后刷新 action 来修改样式了。具体表现如下: [图片][图片][图片][图片][图片][图片] [代码]// pages/index/index.tsx this.currentView.css = newCss; this.refreshSelectView(); [代码] 除了 text ,其他各类 view 也都可以做类似操作,比如修改 rect 的尺寸、修改图片链接、基于替换图片链接实现图片裁剪等等。这里只是抛砖引玉,欢迎大家向我们分享你做出了什么炫酷的功能。 同时使用上述两种方法实现撤销与恢复操作 上面介绍了两种刷新海报的方式,而接下来,我们实现一个简单的撤销与恢复功能。这个功能的核心没有什么特殊的,就是同时维持撤销栈和恢复栈两个栈,通过记录之前所做的操作,做反向操作。 [图片] [代码]// pages/index/index.tsx interface ITimeStackItem { view?: IView; palette?: IPalette; index?: number; type?: string; } pushToHistory = (item: ITimeStackItem) => { this.future.length = 0; while (this.history.length > 19) { this.history.shift(); } this.history.push(item); this.refreshTop(); }; handleTimeMachine = (type: 'revert' | 'recover') => { let popStack: ITimeStackItem[]; let pushStack: ITimeStackItem[]; if (type === 'revert') { popStack = this.history; pushStack = this.future; } else { pushStack = this.history; popStack = this.future; } const pre = popStack.pop(); if (!pre) { return; } if (pre.type === 'delete') { this.currentView = undefined; if (this.currentPalette.views[pre.index!] && this.currentPalette.views[pre.index!].id === pre.view!.id) { this.currentPalette.views.splice(pre.index!, 1); } else { this.currentPalette.views.splice(pre.index!, 0, pre.view!); } pushStack.push(pre); this.refreshPalette(); } else if (pre.palette) { pushStack.push({ palette: JSON.parse(JSON.stringify(this.currentPalette)), }); this.currentPalette = pre.palette; this.currentView = undefined; this.refreshPalette(); } else { for (let i = 0; i < this.currentPalette.views.length; i++) { if (this.currentPalette.views[i].id === pre.view!.id) { pushStack.push({ view: JSON.parse(JSON.stringify(this.currentPalette.views[i])), }); this.currentPalette.views[i] = pre.view!; this.currentView = this.currentPalette.views[i]; this.refreshSelectView(pre.view); break; } } } this.setState({ editState: this.currentView && this.currentView.type === 'text' ? EditState.TEXT : EditState.NORMAL, }); this.refreshTop(); }; [代码] 保存生成的海报 在操作动态模版时,是不会触发 onImgOk 的,因为动态模版的内容渲染在四个不同层级的 canvas 上,无法实时生成完善的海报图片,所以需要手动设置 palette 使用静态模版生成对应的海报 [代码]// pages/index/index.tsx this.setState({ palette: JSON.parse(JSON.stringify(this.currentPalette)), }); handleImgOk = path => { ... }; [代码] 总结 经过上述的一个流程,是不是对如何使用 painter 的动态模版有一些新的想法了呢?欢迎大家基于 painter 开发出更多有趣的功能并在评论区与我们分享。
2021-06-16 - 【插件已经停止维护】【微信小程序-插件开发实战】学校选择器 11.9更新
插件已经停止维护 插件使用效果图: [图片] 学校选择器,起源于我个人开发的项目中的一个实际需求:从列表中选择高校。本质上只是一个选择列表,有很多种呈现方法,而我想让这一环节呈现的更舒适一点(就是带图片咯~),于是开发了这个组件。但我觉得应该有其他开发者会用到同样的需求,那何不分享出来?把它插件化并开源。 一、插件的配置 首先新建一个插件,而不是小程序,如下图。 [图片] 微信提供的插件模板分为两个模块,一个是miniprogram,是用于模拟业务环境的,你可以在这里模拟下使用插件的小程序业务页面;另一个模块是plugin,这就是插件的实际开发环境了,最新的基础库已经支持插件里面有自定义组件和多个page页面。 [图片] 改动改动一些模板的变量名,如plugin.json的参数(如要插件的页面、自定义组件等)后,后面就可以开始在plugin里面写插件代码! { "pages": { "chooseSchool": "pages/chooseSchool" }, "main": "index.js" } 在插件的配置方面,官方文档介绍的非常详细,我就不再赘述了:https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/development.html#%E5%88%9B%E5%BB%BA%E6%8F%92%E4%BB%B6%E9%A1%B9%E7%9B%AE 二、学校选择器 下面开始写插件的业务代码。 学校选择器主要由三部分组成:一个固定的搜索框、懒加载的列表、搜索失败的提示框。 1、固定的搜索框 input配置了bindinput,让每次键入字符时,都将包含该字符的学校名加入渲染的列表里 /** * 搜索学校 */ search_school(e){ var value = e.detail.value var schoolList = this.data.schoolList var schoolList_now = [] for(var i = 0; i < schoolList.length; i++){ if(schoolList[i].name.indexOf(value) != -1){ schoolList_now.push(schoolList[i]) } } this.setData({ schoolList_now:schoolList_now }) } 2、懒加载的列表 {{item.name}} 985 211 其中的image配置了lazy-load以允许懒加载,这样可以加快渲染速度。 3.搜索失败的提示框 没有搜索到结果 可能是尚未收录该学校 你可以自己输入学校名字 配置了wx:if,当待渲染列表长度为0时就渲染出现。 三、插件的调用 如何调用插件呢(这是你必须要告知插件使用者的一件事),微信小程序提供的插件模板已经为你配置好了:你只需要在小程序端的app.json中配置plugin的值如下:"choose-school"是插件的名字,provider填写插件的appId,version是版本号(开发时使用的是dev,但正式版需要正式的版本号) "plugins": { "choose-school": { "version": "dev", "provider": "wxddd52601ccb94739" } } 如果插件中还使用了自定义组件,小程序端的对应页面的json中也需要配置对应的usingComponents,我这个插件是没有用到自定义组件的。 小程序端(也就是插件的使用者)该如何进入插件页面呢?进入插件有两种方法,一种是wxml中的navigator组件,模板中自带,你只需要改动一下跳转的路径即可,如下图: Go to Plugin page 另一种方法是使用wx.navigateTo,我比较推荐这一种方法,因为我是用这种方法才能拿到回传。如下所示,你只要把navigateTo绑定在任意一个组件的事件上就行,它需要配置两个参数,url需要按插件的规则配置:plugin: // + 插件名 + 插件页面。events需要配置acceptDataFromOpenedPage这个参数以获取返回值。 wx.navigateTo({ url: 'plugin://choose-school/chooseSchool', events: { // 监听返回数据 acceptDataFromOpenedPage: function(res) { console.log('回调成功:',res) that.setData({ school:res.data }) }, }, }) 到这一步,插件的调用就结束了,下面是获取它的返回值。 四、插件的返回 当插件的业务逻辑和调用都准备就绪时,这个时候最关键的一步来了,小程序调用你的插件后,总是要获取返回值的,那如何传递这个值呢? 微信官方给我们提供了EventChannel这个路由方法,它是页面间事件通信通道。我们在插件页面的学校view上绑定一个点击事件choose,点击后获取该学校的数据并返回给上一个页面,如下图所示: choose(e){ var item = e.currentTarget.dataset.item var school = this.data.school school.name = item.name school.is985 = item.is985 school.is211 = item.is211 this.setData({ school:school }) const eventChannel = this.getOpenerEventChannel() eventChannel.emit('acceptDataFromOpenedPage', {data: school}); wx.navigateBack({ delta: 1, }) }, 想要将值返回,就新建一个通信信道eventChannel,并且用它发射(emit)一次数据给小程序端开放的信道监控事件acceptDataFromOpenedPage,并把数据school作为data的值传入。然后再调用navigateBack返回小程序端,传值完毕。 五、本插件的实机调用 11.8号我的插件就通过审核啦,我用另一款我已上线的小程序作为例子,给大家分享一下如何调用“学校选择器”。 第一步:登录小程序后台——设置——第三方设置 [图片] 第二步:点击添加插件,搜索“高校选择器”,点击添加,即可发送申请[图片] 第三步:等待申请审核通过(是我审核,如果我没有及时通过,你可以直接私信我) [图片] 第四步:点击“详情”——开发文档,查看插件的使用说明 [图片] 第五步:插件申请通过后,在小程序app.json里面如下配置:(version需要填写1.0.1)(provider一定是填我的插件id:wxddd52601ccb94739,不是小程序的appId),插件名字写“choose-school”(我甚至忘了是在哪里设置这个名字的,以至于我都改不了) "plugins": { "choose-school": { "version": "1.0.1", "provider": "wxddd52601ccb94739" } }, [图片] 第六步:打开开发者工具的详情,查看插件信息里是否有“高校选择器”,如果没有就重启一下开发者工具 [图片] 第七步:在任意地方绑定如图函数,函数触发后就会跳转到插件,选择学校后数据可回调到res里。 wx.navigateTo({ url: 'plugin://choose-school/chooseSchool', events: { // 监听返回数据 acceptDataFromOpenedPage: function(res) { console.log('回调成功:',res) }, }, }) [图片] 第八步:测试是否能拿到回调。 [图片] [图片] 第九步:上线使用~,有任何建议都可以在任何联系到我的地方反馈,我会及时回复! 六、项目源码 组件的源码放在了Gitee上,欢迎下载。 https://gitee.com/cao-mengliang98/school/tree/master 七、其他 如果你想用自己的呈现方式,甚至不使用在微信小程序上,你可以通过这个url来获得学校的logo:http://www.ming13.cn:5000/schoolImages/学校的名字.jpg 不要漏掉.jpg,这是我个人的服务器,个人开发者可以直接使用。以及从源码仓库中获取学校名单.xlxs。 (logo和名字均暂时只有985211高校,后续会补充完整国内高校)
07-02 - 【笔记】横向滑动列表的渲染
前言 今天在学习列表渲染的时候,尝试实现了支持横向滑动的列表,但是遇到了很多问题,做一个小小的总结。 组件scroll-view scroll-view是一种视图容器,指定可滚动视图区域。通过设置属性scroll-y=true并给给scroll-view一个固定高度height,可以实现竖向滚动;通过设置属性scroll-x=true可以实现横向滚动,其他的属性可以参考官方文档。 列表渲染 列表渲染是一种很基础的渲染方法,在组件上使用 wx:for 绑定一个数组,即可使用数组中各项的数据重复渲染该组件。在组件中,使用 wx:for-item 可以指定数组当前元素的变量名,默认为item;使用 wx:for-index 可以指定数组当前下标的变量名,默认为index;使用 wx:key 来指定列表中项目的唯一的标识符,可以提高渲染效率(没有特殊需求的话可以直接用index指定)。 例如,在.js中声明一个含有六个元素的数组list,可以用以下代码循环地渲染list中的所有元素 [代码]<!-- index.wxml 列表渲染 --> <view wx:for="{{list}}" wx:key="{{index}}" class="view-parent"> <view class="view-item">{{item.txt}}</view> </view> [代码] 显示效果如下: [图片] 横向滑动列表 将组件scroll-view和列表渲染结合,就可以实现横向滑动列表了,样例代码如下: [代码]<!-- index.wxml 横向滑动列表 --> <text style="margin-left: 40%;">横向滑动列表</text> <scroll-view scroll-x="true" class="scroll-x-list"> <view wx:for="{{list}}" wx:key="{{index}}" class="view-parent"> <view class="view-item">{{item.txt}}</view> </view> </scroll-view> /* index.wxss */ .scroll-x-list{ height:150px; } .view-item{ width:100px; height:100px; background:#1bf891; margin:10px; } [代码] 显示效果如下: [图片] 可以看到结果和预期差很多,不仅没有实现横向滑动,还没有显示出所有的元素。原因是代码虽然在组件中设置了需要的属性,但是在样式上没有做对应的调整,我们必须在wxss中设置布局才可以达到预期效果。最容易想到的就是我们常用的flex布局,关于flex布局的内容比较多,这里就不展开了,推荐看官方文档学习。在进行下一步修改前,先声明几个必须要知道的小细节: 组件scroll-view是不支持flex布局的,要想在scroll-view中使用flex布局,必须嵌套一个其他的支持flex布局的容器,如view。 scroll-view 中的需要滑动的元素不可以用 float 浮动。 scroll-view 中在需要装载滑动元素的父容器中开启flex布局是没有作用的,应该使用dislay:inline-block来进行元素的横向编排。 第一种方法,由于要实现的是横向滑动列表,那么容器中的元素一定是不允许换行的,刚刚提到,scroll-view是不支持flex布局的,所以开启flex布局并设置flex-wrap=nowrap是无效的行为。我们选择在类scroll-x-list中设置white-space: nowrap来处理元素中的空白,让容器内的换行无效。同时,还应设置装载滑动元素的父容器——view-parent的dislay为inline-block,代码如下: [代码]/* index.wxss */ .scroll-x-list{ height:150px; white-space: nowrap; } .view-parent{ display:inline-block; } [代码] 显示效果如下,已经可以横向滚动列表了。 [图片] 第二种方法,可以在scroll-view中嵌套一个view,在这个view中开启flex布局并设置flex-wrap=nowrap来阻止换行,代码如下: [代码]<!-- index.wxml 另一种横向滑动列表 --> <text style="margin-left: 40%;">横向滑动列表</text> <scroll-view scroll-x="true" class="scroll-x-list"> <view class='flex-view'> <view wx:for="{{list}}" wx:key="{{index}}" class="view-parent"> <view class="view-item">{{item.txt}}</view> </view> </view> </scroll-view> /* index.wxss */ .scroll-x-list{ height:150px; } .flex-view{ display:flex; flex-wrap: nowrap; } .view-parent{ display:inline-block; } [代码] 显示效果与刚刚相同: [图片] 一些改进 在第一种方法中,我们没有使用到flex布局,就很容易遇到一些对齐的问题,假设我们设置数组中第三个元素为空,就会出现下面的情况: [图片] 原因很简单,inline-block的属性中在某个元素没有内容的情况下,它的基线对齐方式是基于这个元素的底边的,解决方式是设置一个垂直的对齐方式: [代码]/* index.wxss */ .view-parent{ display:inline-block; vertical-align: top; } [代码] 显示效果如下: [图片] 同时,你会发现第二种开启flex布局方法的横向列表不会有这种对齐的问题,我们还可以在装载滑动元素的容器中开启flex布局来让内容更加美观: [代码].view-item{ width:100px; height:100px; background:#1bf891; margin-right: 20px; align-items:center; display:flex; justify-content:center; } [代码] 显示效果如下: [图片] 总结 scroll-view是一个十分常见实用的组件,但是使用时也有一些需要注意的问题,比如不支持直接使用flex布局。总体来看,比起设置inline-block的布局,更推荐在scroll-view中嵌套一层view再开启flex布局的方法,可以更灵活的摆放控制滑动元素。
2021-11-15 - 这个库能轻松解决99%的异步和逻辑加载时机问题(异步篇)
[图片] 你是否纠结过底层业务逻辑(登陆、获取用户信息等)到底是放app.js的onLaunch还是page的onLoad里比较好,或者因为异步问题被迫放在了onload,我们来分析一下优劣 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 分析 onLaunch处理 优点:底层业务逻辑集中并且只需写一次,比较好维护 缺点:目前没有一个理想的方案来解决onLaunch和onLoad的异步问题,包括注册回调、重写onLoad、请求拦截等。 onLoad处理 优点:因为不涉及跨页面通知,因此异步逻辑比较好处理 缺点:每个页面都得写一次底层业务逻辑,非常繁琐,而且既然是公用的底层业务逻辑,分散在每个页面的onLoad里,好像也不大对劲。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 抉择 按照高内聚低耦合的原则,那逻辑和数据放onLaunch里肯定的,不应该和普通page逻辑耦合在一起,通用的数据和逻辑应该在入口去处理,执行一次到处使用,就像vue的main.js一样,会注册一些技术层的基础设施(路由、状态管理等插件),那业务层的基础设施不就是token、用户信息、所在位置等逻辑吗? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - 想象中的最佳实践 那我们的目标就是如何满足两者的优点,避免两者的缺点,做到真正的“高内聚低耦合” 1.保持底层业务逻辑写在入口app.js,避免耦合page里的逻辑 2.能在任何page里第一时间拿到globalData数据 3.使用方便,做到在业务开发中无感知,不需要写额外的调用、通知等代码 4.无任何副作用,不会影响其他功能,比如重写阻塞onLoad 5.灵活可配,适用以后此类任何业务 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 梦想成真先看一段代码 ⬇️ // page.js export default { name: 'Home', onLoadLogin(){ //登录成功(拿到token) && 页面初始化完成 //Tips:适用于某页面发送的请求依赖token的场景 }, onLoadUser(){ //页面初始化完成 && 获取用户信息完成 //Tips:适用于页面初始化时需要用到用户信息去做判断再走页面逻辑的场景 }, onReadyUser(){ //dom渲染完成 && 获取用户信息完成 //Tips:适用于首次进入页面需要在canvas上渲染头像的类似场景 }, onReadyShow(){ //小程序内页面渲染完成 && 页面显示 //Tips:适用于需要获取小程序组件或者dom,并且每次页面显示都会执行的场景 }, } 应该懂什么意思了吧?是不是你理想中的样子,使用起来跟没有似的 ⬆️ 这段示例代码满足了上面的第2、3、4条目标 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 再来看一段 ⬇️ // app.js // 配置自定义钩子,所有钩子都可以随意组合搭配使用,执行机制类似于Promise.all(但不是用Promise实现的) CustomHook.install({ 'Login':{ // 自定义钩子名称、必须大写字母开头 name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ // 自定义钩子名称 name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } //依赖globalData中数据 }, globalData) 怎么样,是不是很棒,依赖globalData,名字可配,连触发规则都可配,而且还附加了可随意组合的功能(意外还解决了页面内逻辑执行时机问题,在下篇讲) ⬆️ 这段示例代码满足了上面的第1、5条目标。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -我是分割线 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 是不是跃跃欲试了,那就赶紧试试,好用回来告诉我! ⬇️(公司内部已接入两年了很稳定) GitHub:https://github.com/1977474741/spa-custom-hooks [图片]
2023-07-07 - 小程序直播 前端调用直播间列表接口 上线后不能进入直播间?
前端调用了获取直播间列表 并判断是否是直播中 直播中就进入直播间 不在就显示静态页 上线后不能进入直播间 体验版在打开开发调试下是可以进入直播间的 [图片][图片][图片]
2020-12-11 - 微信小程序怎么运营,日常运营工作内容有哪些
微信小程序怎么运营,日常运营工作内容有哪些,下面木鱼小铺(https://www.muyu007.cn)就和大家分享一下: 一、如何让精准用户知道我们的小程序? 1、移动互联网依靠流量的时代在渐消,精准营销应该引起每一位运营营销人员的关注,运营之前需要对自身业务适用用户进行用户画像描述。 2、门店类:小程序二维码以创意设计的方式将用户眼球吸引过来;门店活动,形式不等;对于用户生活必须类的还可以以优惠方式将用户圈进自己的微信群,后续属于社群运营暂不做展开。 3、微信群、好友精准转发,最好分享小程序页面,不要直接分享小程序。 4、对于网商和工具类可以在官网、公众号以及其他网络渠道进行合理公布投放。 二、如何避免用户玩完就走 1、用户第一次触达小程序的不是上图的列表页,而是首页。小程序的首页要简洁明了,直接了当的让用户接触核心业务功能,解决他的某方面需求。 2、小程序列表直观看,小程序图标和小程序名称。图标有创意,名称简短精炼一个词说明该应用是干什么的。 3、不做过轻型工具。比如,有开发者在做日历,时钟、计算器是不可取的,或许可以将轻型工具商业化。 4、做图文推广时,将小程序做好详细介绍,辅助第一点。 三、如何让用户形成裂变 1、优惠活动吸引已有用户邀请新用户。 2、线上线下服务:线上提供客服功能,及时有效回复用户消息,解决用户需求;线下服务员贴心指导或小程序二维码摆放使用流程指导图。 3、优质产品:再好的服务也是依赖于自身产品。 4、优质产品+线上线下服务促成口碑,用户水到渠成。 四、如何通过小程序为企业发展提供实际参考 1、补充开发中需要对用户数据、行为的记录,运营做日、周、月、季整理报表。 2、通过对数据进行抽象化描述,还原真实用户画像和行为,根据特定数据与其对应的日期阶段、产品类型做出运营导向。 3、这一点说白了就是靠数据说话,要求运营人员具备基本的数据分析和运营能力。 最后,运营的顺利进行需要部门配合,运营的结果取决于执行力。而一份真正小程序运营需要对上述每一项进行深加工。
2020-10-23 - app.js中操作wx.login在首页中获取值为空处理.
app.js页面操作wx.login wx.login({ success: function(res) { if (res.code) { wx.request({ method: "POST", url: getApp().globalData.url + '/v1/wechat/userId', data: { js_code: res.code }, header: { "Content-Type": "application/x-www-form-urlencoded", }, success: function(res) { wx.setStorageSync("openid", res.data.data.openid) wx.setStorageSync("access_token", res.data.data.access_token) wx.setStorageSync("status", res.data.data.status) if (that.callBack){ that.callBack(res); } } }) } else { console.log('获取用户登录态失败:' + res.errMsg); } } }) 首页获取值 const app = getApp(); onLoad(query) { let that = this; //判断onLaunch是否执行完毕 if (wx.getStorageSync('access_token')) { } else { app.callBack = res => { }; } },
2020-07-10 - 手机号解密失败?扫盲帖+解决方案
一、之前的解密流程,会偶现“解密失败”,步骤如下: 1、点击 ‘getPhoneNumber’ 的按钮,弹出授权界面 2、调用 wx.checkSession 进行检查,success 的话就发送授权手机号解密接口;fail 的话就进行 wx.login 获取 code 后请求后端获取 openid/sessionKey,并将两个值缓存到本地,然后再进行手机号解密。 二、粗暴方式解密流程,会偶现“解密失败”,步骤如下: 1、点击 ‘getPhoneNumber’ 的按钮,弹出授权界面 2、调用 wx.login 获取 code 后请求后端获取 openid/sessionKey,然后进行手机号解密 探讨微信解密机制(个人理解,如有不对,请指出哟),如下: 1、点击 ‘getPhoneNumber’ 的按钮,弹出授权界面时,在微信后台会将数据进行加密,此时用于加密的 sessionKey-0 是微信后台已存的 sessionKey,这个 sessionKey 有可能是之前其他业务生成的,比如 wx.login(会刷新微信后台的 sessionKey)。 [图片] 2、按照如上错误的步骤,进行 wx.login 获取 code 再获取 sessionKey-1,由于 wx.login 会刷新微信后台的登录状态,此时新生成的 sessionKey-1 和上一步中用于加密的 sessionKey-0 不是同一个,于是解密失败了。 3、重点:必须保持操作前后的 sessionKey 都是同一个,如果 wx.login 刷新了后台的 sessionKey,而之前的操作还是原来的 sessionKey,则会失败。 必现方式一(解密失败),如下: 1、正常授权解密后,关闭小程序一段时间,测试的时候大概 30-40 分钟左右,后台 sessionKey 过期了 2、重新打开小程序走之前的步骤,出现“解密失败”,然后第二次进行授权解密时,又成功了 必现方式二(解密失败),如下: 1、清理缓存后,点击授权登录(此时后台的sessionKey为空,所以无论怎么做登录和检查,都会解密失败),弹出界面后调用 wx.login 进行登录获取 sessionKey 2、授权解密,失败 3、再次点击授权登录(此时会获取第一步中登录生成的 sessionKey,与未过期的sessionKey一致),则进行解密,成功。 解决方案,如下: 基于对上面机制的理解,wx.login 方式获取的 sessionKey 必须在唤起授权弹窗前进行,必须保证授权弹窗时获取的 sessionKey 和 本地未过期的sessionKey是同一个。 1、在 app.js 的 onLaunch 中,先进行 wx.checkSession。success,则进行后续代码执行;如果 fail,则进行 wx.login 重新获取 sessionKey,后端同学将 sessionKey 存储至数据库,openid 是唯一的,可以作为主键。此时,会先于任何操作之前生成一个 sessionKey。 2、唤起授权界面,点击“允许”后,将 openid+iv+encryptedData 传给后端进行解密。 代码如下(IDE复制后没发改样式,const 等变量可能看不清,可以复制到自己的编辑器中查看哈): 然后在 onLaunch 中调用 onLogin.checkSession 进行前置 session 检查和获取 export const onLogin = { // 检查 session 是否有效 checkSession(cb = () => {}) { const _this = this; // 检查登录态是否可用 wx.checkSession({ success () { console.log('session success'); // session_key 未过期,并且在本生命周期一直有效 const { openid, session_key } = wx.getStorageSync('session'); cb(openid, session_key); }, // 登录态失效了 fail () { console.log('session fail'); _this._wxLogin((openid, session_key) => { cb(openid, session_key); }); } }); }, // 微信获取 code 进行解密 _wxLogin(cb = () => {}) { // 将 js_code 发送给后端获取 openId 和 sessionKey,并进行缓存 wx.showLoading({ mask: true }); wx.login({ success: ({ code }) => { // 将 js_code 发送给后端获取 openId 和 sessionKey,并进行缓存 request({ url: `/wx/open/getSessionKey.json?code=${code}&clientIdentity=${CONFIG.clientIdentity}`, onSuccess: ({ openid, session_key }) => { wx.setStorageSync('session', { openid, session_key }); cb(openid, session_key); }, complete: () => { wx.hideLoading(); } }) } }); }, }
2020-11-05 - 什么小程序需要教育相关类目?
什么内容或服务的小程序需要补充教育相关类目? [图片] 1、培训机构:1-职业培训 2-课外辅导,小程序涉及以学历教育或成人教育为目的、有线下教学场景的教育培训机构,提供职业培训、课外辅导、语言培训等教育培训业务,需补充教育-培训机构 所需资质:需提供区、县级教育部门颁发的《民办学校办学许可证》及营业执照(或事业单位法人证书、民办非企业单位登记证书)(18年新法规要求,仅针对新增帐号) 案例:下图小程序涉及提供在线教育培训服务,需补充教育-培训机构。 [图片] 2、教育信息服务:小程序内提供教育政策、考试通知等教育信息服务,需补充教育-教育信息服务。 案例:小程序涉及提供高考成绩查询服务,需补充教育-教育信息服务。 [图片] 3、学历教育(学校):适用于小程序提供初等、中等和高等学历教育等服务,需补充教育-学历教育(学校)类目。 所需资质(2选1): 1)公立学校:由教育行政部门出具的审批设立证明或《事业单位法人证书》 2)私立学校:《民办学校办学许可证》 案例:如下图小程序涉及提供专升本学历教育服务,需补充教育-学历教育(学校类目。 [图片] 4、驾校培训:适用于小程序涉及提供驾校在线付费培训服务,需补充教育-驾校培训类目。 所需资质:《机动车驾驶员培训备案》 案例:如下图小程序涉及提供付费学车培训套餐服务,需补充教育-驾校培训类目。 [图片] 5、驾校平台:适用于小程序涉及为驾校主体提供入驻渠道,提供多家驾校在线培训等服务,需补充教育-驾校平台类目。 所需资质(同时提供): 1、两家或以上的驾校培训公司的合作协议 2、驾校培训机构的《机动车驾驶员培训备案》 3、《非经营性互联网信息服务备案核准》 4、《小程序开发者承诺函》(注:《小程序开发者承诺函》在申请类目详情页处含示例模板) 6、教育平台:适用于小程序涉及为学历教育、成人教育的培训机构提供入驻渠道、提供多家教育培训等服务,需补充教育-教育平台类目。 所需资质(二选一): 1、《非经营性互联网信息服务备案核准》、两家或以上的培训机构合作协议、及培训机构的《民办学校办学许可证》 2、《小程序开发者承诺函》、及其他平台良好经营情况的证明材料(如app store 评分超过50万人及app内提供该类目的服务内容截图) 7、素质教育:适用于小程序涉及艺术培训、语言培训等为目的、有线下教学场景的培训机构,及为艺术、语言培训机构提供入驻渠道等服务 注:若提供多家艺术教育培训等服务,需补充“教育-素质教育”类目。 8、婴幼儿教育:适用于小程序0~6岁年龄阶段的婴幼儿教育服务,需补充教育-婴幼儿教育类目。 案例:小程序涉及提供婴幼儿教育服务,需补充教育-婴幼儿教育类目。 [图片] 9、教育-在线教育:小程序涉及提供教育答题类服务,需补充教育-在线教育类目。 案例:下图小程序涉及提供教育类答题服务,需补充教育-在线教育类目。 [图片] 10、教育装备:小程序提供教育教学活动所需的教具、学具、器材、设施、场所及其配置服务,需补充教育-教育装备类目 案例:下图小程序涉及提供科学实验、电子演示器教育设备配置服务,需补充教育-教育装备类目 [图片] 11、出国留学:小程序内涉及提供境外教育服务,需补充教育-出国留学类目。 案例:如下图小程序涉及提供国外留学签证服务,需补充教育-出国留学类目。 [图片] 12、特殊人群教育:小程序内提供特殊人群方面相关的教育服务,需补充教育-特殊人群教育类目。 案例:如图小程序涉及提供自闭症儿童教育服务,需补充教育-特殊人群教育类目。 [图片] 13、在线视频课程:小程序内提供教育行业提供,网课、在线培训、讲座等教育类直播,需补充教育-在线视频课程类目。 所需资质(5选1): 1、《事业单位法人证书》(适用公立学校) 2、区、县级教育部门颁发的《民办学校办学许可证》(适用培训机构) 3、《信息网络传播视听节目许可证》 4、全国校外线上培训管理服务平台备案 5、教育部门的批准文件 案例:如下图小程序涉及提供小学生作文思维提升在线视频课程培训服务,需补充教育-在线视频课程类目。 [图片] 14、素质教育:小程序内提供以艺术培训、语言培训等为目的、有线下教学场景的培训机构,及为艺术、语言培训机构提供入驻渠道等服务,需补充教育素质教育类目。 案例:如下图小程序涉及提供线下门店的拉丁舞蹈培训服务,需补充教育素质教育类目。 [图片] 15、教育平台:小程序内涉及为学历教育、成人教育的培训机构提供入驻渠道、提供多家教育培训等服务,需补充教育-教育平台服务类目。 所需资质(需同时提供): 1)《非经营性互联网信息服务备案核准》 2)两家或以上的培训机构合作协议,及培训机构的《民办学校办学许可证》 案例:如下小程序涉及为职业资格、技能培训、电脑课程等培训机构提供入住渠道,需补充教育-教育平台服务类目。 [图片]
2021-12-03 - 小程序内完美实现吸顶粘附功能(媲美 美团小程序的吸顶效果)如丝滑一般
利用css属性完美解决吸顶粘附效果(ios&android都适用),分享一下心得: tips:不需要调用onPageScroll 方法 去监听滚动距离!!! 1、给吸顶元素添加css属性 position: sticky;2、给元素设置距离顶部吸附的距离 top: 0px; (这个top值你可以根据实际情况动态设置)3、然后完事了啦啦啦啦啦啦啦啦~by the way:附上简单的代码片段------>https://developers.weixin.qq.com/s/pR8QKjmE7igq
2020-04-02 - 基于科目维度的在线答题小程序,细节整理
本文背景关于开发这个小程序有点历史了,但是由于逻辑相对复杂,每次捡起来都要看代码重新熟悉逻辑,鉴于这个原因,我计划通过该文,把一些逻辑细节通过文字的形式记录下来。 本文内容本文主要围绕科目维度小程序来讲一个答题小程序的逻辑,所谓科目维度就是该小程序是针对具体某个科目,或者考研或者从业资格考试 按科目维度,就有下一级章节 科目、章节,这是按科目维度的常见划分,但是本文总结的小程序还有一级便是天,把章节里面的考题分成许多天 比如一个科目有10个章节,第一章节下面有100道题,可以把这100道题拆分到10天里面,每天10道题 这样就存在三级目录,这种深层次的目录划分,按照常规用户选择的方式是非常不方便的,本文讨论的小程序,将章节隐藏 科目下面就是天,具体如下图所示 [图片][图片] 这样,只要选择了科目,具体多少天便可以通过逻辑来控制,相当于只选一次就可以直接到达刷题的题目,交互非常友好。 前期准备由于这种 科目 --- 天 这种特殊的二级目录组织形式,前期要求我们在录入的时候便要拆分好,科目下每一章节具体划分为多少天,为直观展示,我截图下 大家看下面截图的时候,要按照行来看,每一行是一章节,每一章节拆分到哪一天都是固定的, 每一天对应的题库要单独维护,这是前期的一个不小工作量 [图片] 技术难点看似我这么叙述好像没有太难的地方,但是加上积分,和支付,答题消耗积分,9元解锁所有答题,所以说,有了这些细节,每个逻辑都相对复杂 逻辑1、 我给关键词叫匹配 我们点击科目,我们要通过该科目所有天和当前已刷题的天,做差集,差集里面的便是我们要本次答题的天 比如,我们拿上图的运动生理学为例,该科目有12章节,共20天, 01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17,18,19,20 我们当前完成了5天的刷题,分别是01,02,03,04,05 我们选择科目时,就要从上面连个数组求差集,取第一个06 逻辑2、 我给关键词叫解锁 我们点击科目的时候,要判断当前科目是否解锁,所谓的解锁是指,是否已经解锁了科目下的某天,也就是是否已消耗过积分,但是题目还没有刷完 通过上述逻辑1,我们看到下一个刷题的是06,但是06是否已消耗过积分呢,这一点我们便是在逻辑2来完成 如果已消耗过积分,那么直接进入答题 如果未消耗过几分,那么首先核对积分是否充裕,如果积分充裕,再扣掉积分,然后去答题 逻辑3、 我给的关键词叫记忆 所谓记忆就是每次去答题,可能做到中途就提前退出,这个时候,下次进去刷题的时候,是从第一题开始,还是从上次刷题的地方开始 由于这次刷题没有得分的概念,所以把该问题降低了难度,只要记录每次答题的序号就能在下次进入答题的时候,从下一题开始作答 逻辑4、 我给的关键词叫统计分析 在答题的时候有两个维度掌握率、正确率 所谓掌握率就是 已答题数量/总题目数量 所谓正确率是 答题正确数量/已答题数量 还有一个逻辑:当我们在复习错题的时候,是可以移除错题,这个时候相当于正确答题数量+1了 逻辑5、 我给的关键词叫积分 该逻辑看似跟答题无关,但是在答题小程序里面,这个细节是仅此于答题核心逻辑的, 积分系统是一个完善小程序必不可少的,积分获取的规则,积分消耗的规则,这些都是细节中的细节 [图片] 逻辑6、 我给的关键词叫支付, 该小程序里面涉及9.9解锁所有答题的功能,单次讲支付很简单,但是讲支付融合到业务里面,便复杂一些,支付的优先级高于积分,所以每次判断首先判断是否已支付, 如果没有支付,我们才会走到积分扣减的逻辑里面 备注通过开发这个小程序,让我从传统答题小程序思维里面跳出来,对我而言,这算是一种全新的模式,需要我不断的理解、消化和吸收。
2020-07-04 - 利用coolui scroll组件和百度地图api实现下拉刷新展示天气效果
前言 想给自己写的下拉刷新增加一些效果,突发奇想的想增加一个天气预报的效果,然后就去寻找天气接口。查到接口以后我发现我天真了- -,那个天气真是多到难以想象啊。。什么晴转阴。。晴转多云转大雨。。。动画实在写不过来。但是为了 我的设想,还是写了一些主要的天气的动画(如:晴,多云,小雨,雪,雷阵雨),感兴趣的朋友可以继续添加。 组件 coolui scroll https://developers.weixin.qq.com/community/develop/article/doc/000a00925744f06af6ca00a7651c13 百度地图天气api http://lbs.baidu.com/index.php?title=wxjsapi/guide/getweather 演示Demo https://developers.weixin.qq.com/s/KmYdwMmX7hjV 效果图 晴 [图片] 多云 [图片] 雨 [图片] 雪 [图片] 雷阵雨 [图片]
2020-08-24 - 使用 MobX 来管理小程序的跨页面数据
在小程序中,常常有些数据需要在几个页面或组件中共享。对于这样的数据,在 web 开发中,有些朋友使用过 redux 、 vuex 之类的 状态管理 框架。在小程序开发中,也有不少朋友喜欢用 MobX ,说明这类框架在实际开发中非常实用。 小程序团队近期也开源了 MobX 的辅助模块,使用 MobX 也更加方便。那么,在这篇文章中就来介绍一下 MobX 在小程序中的一个简单用例! 在小程序中引入 MobX 在小程序项目中,可以通过 npm 的方式引入 MobX 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 引入 MobX : [代码]npm install --save mobx-miniprogram mobx-miniprogram-bindings [代码] (这里用到了 mobx-miniprogram-bindings 模块,模块说明在这里: https://developers.weixin.qq.com/miniprogram/dev/extended/functional/mobx.html 。) npm 命令执行完后,记得在开发者工具的项目中点一下菜单栏中的 [代码]工具[代码] - [代码]构建 npm[代码] 。 MobX 有什么用呢? 试想这样一个场景:制作一个天气预报资讯小程序,首页是列表,点击列表中的项目可以进入到详情页。 首页如下: [图片] 详情页如下: [图片] 每次进入首页时,需要使用 [代码]wx.request[代码] 获取天气列表数据,之后将数据使用 setData 应用到界面上。进入详情页之后,再次获取指定日期的天气详情数据,展示在详情页中。 这样做的坏处是,进入了详情页之后需要再次通过网络获取一次数据,等待网络返回后才能将数据展示出来。 事实上,可以在首页获取天气列表数据时,就一并将所有的天气详情数据一同获取回来,存放在一个 数据仓库 中,需要的时候从仓库中取出来就可以了。这样,只需要进入首页时获取一次网络数据就可以了。 MobX 可以帮助我们很方便地建立数据仓库。接下来就讲解一下具体怎么建立和使用 MobX 数据仓库。 建立数据仓库 数据仓库通常专门写在一个独立的 js 文件中。 [代码]import { observable, action } from 'mobx-miniprogram' // 数据仓库 export const store = observable({ list: [], // 天气数据(包含列表和详情) // 设置天气列表,从网络上获取到数据之后调用 setList: action(function (list) { this.list = list }), }) [代码] 在上面数据仓库中,包含有数据 [代码]list[代码] (即天气数据),还包括了一个名为 [代码]setList[代码] 的 action ,用于更改数据仓库中的数据。 在首页中使用数据仓库 如果需要在页面中使用数据仓库里的数据,需要调用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中,然后就可以在页面中直接使用仓库数据了。 [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad() { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 actions: ['setList'], // 将 this.setList 绑定为仓库中的 setList action }) // 从服务器端读取数据 wx.showLoading() wx.request({ // 请求网络数据 // ... success: (data) => { wx.hideLoading() // 调用 setList action ,将数据写入 store this.setList(data) } }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,可以在 wxml 中直接使用 list : [代码]<view class="item" wx:for="{{list}}" wx:key="date" data-index="{{index}}"> <!-- 这里可以使用 list 中的数据了! --> <view class="title">{{item.date}} {{item.summary}}</view> <view class="abstract">{{item.temperature}}</view> </view> [代码] 在详情页中使用数据仓库 在详情页中,同样可以使用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中: [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad(args) { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 }) // 页面参数 `index` 表示要展示哪一条天气详情数据,将它用 setData 设置到界面上 this.setData({ index: args.index }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,这个页面 wxml 中也可以直接使用 list : [代码]<view class="title">{{list[index].date}}</view> <view class="content">温度 {{list[index].temperature}}</view> <view class="content">天气 {{list[index].weather}}</view> <view class="content">空气质量 {{list[index].airQuality}}</view> <view class="content">{{list[index].details}}</view> [代码] 完整示例 完整例子可以在这个代码片段中体验: https://developers.weixin.qq.com/s/YhfvpxmN7HcV 这个就是 MobX 在小程序中最基础的玩法了。相关的 npm 模块文档可参考 mobx-miniprogram-bindings 和 mobx-miniprogram 。 MobX 在实际使用时还有很多好的实践经验,感兴趣的话,可以阅读一些其他相关的文章。
2019-11-01 - 小程序自定义组件之下拉菜单
[图片] 支持 配置化设置弹层内容 支持动态刷新弹层内容 支持动态修改分类标题 支持遮罩层 支持api关闭弹层 详细配置文章请移步 https://juejin.im/post/5ea3f0b2e51d4546c349f95c DEMO [图片]
2020-05-05 - 在线拼团小程序开发功能与方案
随着小程序行业的发展,越来越多的企业和个人想开发一款属于自己的小程序,那么在小程序设计开发、运营推广等过程需要注意什么呢?下面千优小编收集的“在线拼团小程序开发功能与方案”的干货,希望对大家有所帮助! 需要小程序开发的咨询,吴生:微电18319783819 [图片] 相信大家对于网购并不陌生,如今愈来愈多的人都在加入到这个大家庭之中。而随着拼多多的加入,让原本的网购方法获得了新的销售方法。在线拼团一时之间成为广大商家喜爱的获客方法,那么开发一款在线拼团小程序有什么好处呢? 一、在线拼团小程序的开发有什么好处呢? 1、获客更加简单:通过小程序可以有效的实现迅速获客,不必大量付出大量的宣传费用也获取一样的效果。 2、获客更加准确:凡是会进入小程序之中的用户,都是有需求的用户即门店的准确用户。 3、减少获客费用:拼团模式能够有效的刺激顾客的自发性,用户的自发性也能有效的减少获客费用。 4、增加推广效果:在线推广能够有效的实现迅速宣传、大范围的宣传等效果。从而有效的突出推广效果。 二、在线拼团小程序有什么功能呢? 1、产品展示:为用户提供产品展示功能,让用户能够更加直观的感觉自己想要购买的产品。 2、产品抢购:通过在线抢购的功能,不仅能够有效的激发用户对于产品的消费欲望,更能有效的提升平台的活跃度。 3、在线拼团:作为拼团小程序的核心,在线拼团自然是不可缺少的功能。通过在线拼团能够取得更多实惠的价钱。 4、积分功能:积分功能不仅能够为用户带来更多实惠,也能成功的增加用户的粘度,拉近与用户之间的距离。 5、购物车:非常多人在购物时都会有踌躇的变现,而购物车的功能是能够让他们在下一次购物时不再踌躇。 6、个人中心:作为一款购物类平台,提供相关的个人中心是非常有必要的。 关于“在线拼团小程序开发功能与方案”的干货知识,千优小编就分享到此了,小程序的设计开发、运营推广等知识也不是一篇文章能够说清楚的,想学习小程序的更多技术干货,请继续关注千优网,一个集小程序开发、发布、推广的综合性平台!
2020-05-09 - 分享用户头像叠加循环渲染
[代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"flex-Center avatorBox"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"avatorBox-a"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{images}}"[代码] [代码]wx:key [代码][代码]style[代码][代码]=[代码][代码]'transform:translateX({{-index*25}}rpx)'[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]image[代码] [代码]class[代码][代码]=[代码][代码]"avator"[代码] [代码]src[代码][代码]=[代码][代码]'{{item}}'[代码] [代码]mode[代码][代码]=[代码][代码]'aspectFill'[代码][代码]></[代码][代码]image[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]view[代码][代码]>[代码][代码]</[代码][代码]view[代码][代码]>[代码][图片]
2019-12-02 - 基于JetBrains平台的微信小程序插件
wechat-miniprogram-plugin 基于JetBrains平台的微信小程序插件 主要功能 wxml/wxss/wxs文件支持 语法解析 代码完成 代码高亮 wxml嵌入表达式支持 wxml <wxs> 标签支持 创建微信小程序组件以及页面 相关文件导航 微信小程序自定义组件支持 自动注册自定义组件 组件配置解析 微信小程序配置文件支持 代码检查以及自动修复 JetBrains产品兼容性: 支持内核版本在[代码]193.2723[代码]之后的所有产品 在Wiki中浏览更多功能 安装 通过IDE搜索并找到Wechat mini program support插件安装(目前未发布至插件库) 下载发行版,在IDE中选择从磁盘安装 使用 通过IDE打开微信小程序项目即可使用全部功能 欢迎大家对此项目提出想法和建议 Gitee源码地址https://gitee.com/zxy_c/wechat-miniprogram-plugin
2019-12-31 - 小程序顶部导航栏,可滑动,可动态选中放大
最近在研究小程序顶部导航栏时,学到了一个不错的导航栏,今天就来分享给大家。 老规矩,先看效果图 [图片] 可以看到我们实现了如下功能 1,顶部导航栏 2,可以左右滑动的导航栏 3,选中条目放大 原理其实很简单,我这里把我研究后的源码发给大家吧。 wxml文件如下 [代码]<!-- 导航栏 --> <scroll-view scroll-x class="navbar" scroll-with-animation scroll-left="{{scrollLeft}}rpx"> <view class="nav-item" wx:for="{{tabs}}" wx:key="id" bindtap="tabSelect" data-id="{{index}}"> <view class="nav-text {{index==tabCur?'tab-on':''}}">{{item.name}}</view> </view> </scroll-view> [代码] wxss文件如下 [代码]/* 导航栏布局相关 */ .navbar { width: 100%; height: 90rpx; /* 文本不换行 */ white-space: nowrap; display: flex; box-sizing: border-box; border-bottom: 1rpx solid #eee; background: #fff; align-items: center; /* 固定在顶部 */ position: fixed; left: 0rpx; top: 0rpx; } .nav-item { padding-left: 25rpx; padding-right: 25rpx; height: 100%; display: inline-block; /* 普通文字大小 */ font-size: 28rpx; } .nav-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; letter-spacing: 4rpx; box-sizing: border-box; } .tab-on { color: #fbbd08; /* 选中放大 */ font-size: 38rpx !important; font-weight: 600; border-bottom: 4rpx solid #fbbd08 !important; } [代码] js文件如下 [代码]// pages/test2/test2.js Page({ data: { tabCur: 0, //默认选中 tabs: [{ name: '等待支付', id: 0 }, { name: '待发货', id: 1 }, { name: '待收货', id: 2 }, { name: '待签字', id: 3 }, { name: '待评价', id: 4 }, { name: '五星好评', id: 5 }, { name: '差评订单', id: 6 }, { name: '编程小石头', id: 8 }, { name: '小石头', id: 9 } ] }, //选择条目 tabSelect(e) { this.setData({ tabCur: e.currentTarget.dataset.id, scrollLeft: (e.currentTarget.dataset.id - 2) * 200 }) } }) [代码] 代码里注释很明白了,大家自己跟着多敲几遍就可以了。后面会更新更多小程序相关的知识,请持续关注。
2019-11-22 - 小程序中如何正确删除数组中的指定索引的数据
适应场景:购物车清空、删除某一商品及类似此种操作。 主要使用 JavaScript数组的一个方法 array.splice() 具体使用为: 示例: var array = [“张三”,“李四”,“王五”,“赵云”]; //删除王五(索引值为:2) array.splice(2,1); 输出数组array的结果为: [“张三”,“李四”,“赵云”] 注释: 此方法的第一个参数的含有为:删除数组元素的起始索引 第二个参数的含义为:删除数组元素从起始索引开始需要删除的长度 示例的意义参照上述描述,该语句 array.splice(2,1)含义为 从索引2开始,删除长度为1。 下面阐述在小程序中如何操作。 1、先看代码【其中index的值为“删除数组元素的起始索引”】 [图片] 2、执行结果【其中index的值为“删除数组元素的起始索引”】 [图片] 执行数组删除时,此种方案最好,不需要再向后台重复请求数据。
2019-11-18 - 用__wxConfig.envVersion区分小程序体验版,开发板,正式版
在开发过程中,通常测试版和正式版的api的根路径不同,需要在发布时手动去更改路径,这就显得很繁琐,然后官方也没有给出相应的判断环境的api,其实小程序是预设了这个api的,只是不知道为什么没有公布出来,这个api就是 __wxConfig 关键点 — __wxConfig 在控制台中打印__wxConfig可以得到一下数据 [图片] 其中的envVersion为运行环境,有以下几个值 envVersion: ‘develop’, //开发版 envVersion: ‘trial’, //体验版 envVersion: ‘release’, //正式版 其中的platform为运行的平台 有Android ios devtools 等 之前一直不知道微信小程序可以用__wxConfig.envVersion区分小程序体验版,开发板,正式版 目前在官方文档没有查到相关资料,但是亲测可用 [代码] envVersion 类型为字符串 envVersion: 'develop', //开发版 envVersion: 'trial', //体验版 envVersion: 'release', //正式版 [代码] 具体代码可参考如下截图 [图片] 20191120 其实在我们的开发过程中是不需要这个变量的,因为我们开发版、体验版、和生产版是三个不同的小程序,所以不需要根据环境变量来区分 20191121摘自社区帖子 [代码]const env = typeof __wxConfig !== "undefined" ? __wxConfig.envVersion || "release" : "release"; const isProd = env === "release"; const protocol = isProd ? "https://" : "http://"; const baseApi = { develop: "testapi.com", trial: 'readyapi.com', release: "api.com" }; export const api = protocol + baseApi[env]; [代码]
2019-11-20 - 一些精品小程序项目分享(原代码)
小程序有着微信近十亿的用户作为基础,加上公众号、小程序码、微信群转发等多种传播途径,非常容易就获取到直接的用户,跟地推等传统获客方式相比,它的成本是非常低的,低成本高转换的获客,对品牌营销帮助非常大。 给大家分享一下精品小程序的demo,代码仅供学习。 这些我是平时用来学习的demo,看看别人写的代码和自己有什么不一样,多方思考后受益良多 地址:http://t.cn/ExJMe95 [图片][图片] 地址:http://t.cn/ExJMe95 地址:http://t.cn/ExJMe95 地址:http://t.cn/ExJMe95
2019-10-10 - 自定义导航栏与position: fixed
公司UI要实现 从别的页面点到商品页时会有返回和首页按钮; 注:除tabBar页面页面其余页面都有所以开全局样式 但是写着写着发现如果我开启了一个 position: fixed; 定位的话会被覆盖掉顶部导航覆盖掉, 所以top值需要获取状态栏的和导航栏的高度 在页面里如果不是position: fixed;定位的话还好,但是里面的元素一旦使用position: fixed; top:0;left:0;就会定到屏幕最顶端 [图片] 就像是这样,所以说这个顶部的距离还要动态设定px;但是一个小程序不可能只有一两个position: fixed;每个都要动态获取顶部的px也太麻烦了,所以我想了想放到了全局变量就像是这样(APP.globalData.BarHeight = this.data.navbarHeight)把计算好的px放到了全局里面,然后每个页面都要引这个全局变量还是麻烦,所以我干脆抽了一个组件里面专门放position: fixed;的东西,里面不再使用position: fixed; 代码如下 wxml: [代码] <view class="box" style="top:{{topHight}}px"> <slot></slot> </view> [代码] js: [代码] const APP = getApp() Component({ /** * 组件的属性列表 */ properties: {}, lifetimes: { attached() { this.setData({ topHight: APP.globalData.BarHeight }) } }, /** * 组件的初始数据 */ data: { topHight: 0 //自定义导航栏的的高度 }, /** * 组件的方法列表 */ methods: {} }) [代码] wxss: [代码] .box{ width: 100%; position: fixed; left: 0; z-index:999; } [代码] 组件全局注册用的时候直接那它包住position: fixed;元素就好了。 对了前两天看小程序官网有封装的自定义导航栏了,(但是同样position: fixed;也会出现同样现象)有兴趣的自己可以去看下:https://developers.weixin.qq.com/miniprogram/dev/extended/weui/navigation.html 上述是我的方法,有更好的解决办法,请评论我谢谢。
2019-09-20 - 小程序之日历签到积分
[图片] 该示例为纯手写代码,暂无插件,不多说直接上代码 我们的实现思路: JS部分 1、获取当前年月 const date = new Date(); cur_year = date.getFullYear(); cur_month = date.getMonth() + 1; const weeks_ch = [‘日’, ‘一’, ‘二’, ‘三’, ‘四’, ‘五’, ‘六’]; this.setData({ cur_year, cur_month, weeks_ch, }) 2、获取当月共多少天 getThisMonthDays: function (year, month) { return new Date(year, month, 0).getDate() }, 3、获取当月第一天星期几 getFirstDayOfWeek: function (year, month) { return new Date(Date.UTC(year, month - 1, 1)).getDay(); }, 4、计算当月1号前空了几个格子,把它填充在days数组的前面 calculateEmptyGrids: function (year, month) { var that = this; //计算每个月时要清零 that.setData({ days: [] }); const firstDayOfWeek = this.getFirstDayOfWeek(year, month); if (firstDayOfWeek > 0) { for (let i = 0; i < firstDayOfWeek; i++) { var obj = { date: null, isSign: false } that.data.days.push(obj); } this.setData({ days: that.data.days }); //清空 } else { this.setData({ days: [] }); } }, 5、绘制当月天数占的格子,并把它放到days数组中 calculateDays: function (year, month, sign) { var that = this; var isSign; const thisMonthDays = this.getThisMonthDays(year, month); for (var i = 1; i <= thisMonthDays; i++) { var obj = { date: i, isSign: ‘’ } for (var j = 0; j < sign.length; j++) { if (i == parseInt(sign[j].create_time.substr(8, 2))) { obj.isSign = true; break; } } that.data.days.push(obj); } this.setData({ days: that.data.days }); }, 6、切换控制年月,上一个月,下一个月 handleCalendar: function (e) { const handle = e.currentTarget.dataset.handle; const cur_year = this.data.cur_year; const cur_month = this.data.cur_month; if (handle === ‘prev’) { let newMonth = cur_month - 1; let newYear = cur_year; if (newMonth < 1) { newYear = cur_year - 1; newMonth = 12; } this.signRecord(newYear, newMonth); this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘cnext.png’ }) } else { if (cur_month + 1 > month) { this.setData({ imgType: ‘next.png’ }) } else { let newMonth = cur_month + 1; let newYear = cur_year; if (newMonth > 12) { newYear = cur_year + 1; newMonth = 1; } this.signRecord(newYear, newMonth); if (cur_month + 1 == month) { this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘next.png’ }) } else { this.setData({ cur_year: newYear, cur_month: newMonth, imgType: ‘cnext.png’ }) } } } }, wxml部分: <view class=‘all’> <view class=“bar”> <!-- 上一个月 --> <view class=“previous” bindtap=“handleCalendar” data-handle=“prev”> <image src=‘https://www.***.com/weChatImg/pre.png’></image> </view> <!-- 显示年月 --> <view class=“date”>{{cur_year || “–”}} / {{filter.fill(cur_month) || “–”}}</view> <!-- 下一个月 --> <view class=“next” bindtap=“handleCalendar” data-handle=“next”> <image src=‘https://www.***.com/weChatImg/{{imgType}}’></image> </view> </view> <view class=‘xxian’> <image src=‘weChatImg/huan.png’></image> <image src=‘weChatImg/huan.png’></image> </view> <!-- 显示星期 --> <view class=“week”> <view wx:for="{{weeks_ch}}" wx:key="{{index}}" data-idx="{{index}}">{{item}}</view> </view> <view class=‘days’> <!-- 列 --> <view class=“columns” wx:for="{{days.length/7}}" wx:for-index=“i” wx:key=“i”> <view wx:for="{{days}}" wx:for-index=“j” wx:key=“j”> <!-- 行 --> <view class=“rows” wx:if="{{j/7 == i}}"> <view class=“rows” wx:for="{{7}}" wx:for-index=“k” wx:key=“k”> <!-- 每个月份的空的单元格 --> <view class=‘cell’ wx:if="{{days[j+k].date == null}}"> <text decode="{{true}}"> </text> </view> <!-- 每个月份的有数字的单元格 --> <view class=‘cell’ wx:else> <!-- 当前日期已签到 --> <view wx:if="{{days[j+k].isSign == true}}" style=‘color:#acacac’ class=‘cell’> {{days[j+k].date}} <image src=‘https://www.***.com/weChatImg/sgin.png’></image> </view> <!-- 当前日期未签到 --> <view wx:else> <text>{{days[j+k].date}}</text> </view> </view> </view> </view> </view> </view> </view> </view> 相信大家通过以上思路,再结合自己的需求应该可以自己做出符合自己心目中的日历插件或者签到
2019-09-11 - BookChat v2.3 发布,通用书籍阅读小程序,增加分享海报和广告功能
BookChat 介绍 BookChat - 面向程序员的开源书籍和文档阅读学习小程序,同时也是一款基于 Apache 2.0 开源协议进行开源的通用书籍阅读微信小程序。 它非常轻量,200KB 左右的大小,麻雀虽小五脏俱全,该有的功能一个没少;同时参照了腾讯官方的 微信小程序设计指南 进行设计,拥有简洁美观的页面和良好的用户体验。 升级日志 抽屉兼容优化:部分安卓手机机型,阅读页面抽屉间距不合理,以致书籍目录和阅读偏好设置部分被遮挡 阅读体验优化:打开书籍阅读,自动跳转到上次阅读的位置,再也不用担心忘记上次阅读到了哪里 增加广告功能:微信小程序如需添加微信小程序广告,只需修改[代码]config.js[代码]的配置项 [代码]// 横幅广告id,如果申请了腾讯小程序的广告,则创建一个横幅广告,把广告的AdUnitId粘贴进来即可,不投放广告,则把该值设置为空 const bannerAdUnitId = '' [代码] 增加分享海报:分享到朋友圈,也没有那么困难重重了 使用了海报生成组件库: https://github.com/kuckboy1994/mp_canvas_drawer [图片] 其它若干小细节优化 相关地址 BookChat 开源地址 Gitee(码云): https://gitee.com/truthhun/BookChat Github: https://github.com/truthhun/BookChat BookChat 后端程序 BookStack 开源地址 Gitee(码云): https://gitee.com/truthhun/BookStack Github: https://github.com/truthhun/BookStack BookChat 体验码 [图片]
2019-08-14 - 如何实现一个自定义导航栏
自定义导航栏在刚出的时候已经有很多实现方案了,但是还有大哥在问,那这里再贴下代码及原理: 首先在App.js的 onLaunch中获取当前手机机型头部状态栏的高度,单位为px,存在内存中,操作如下: [代码]onLaunch() { wx.getSystemInfo({ success: (res) => { this.globalData.statusBarHeight = res.statusBarHeight this.globalData.titleBarHeight = wx.getMenuButtonBoundingClientRect().bottom + wx.getMenuButtonBoundingClientRect().top - (res.statusBarHeight * 2) }, failure() { this.globalData.statusBarHeight = 0 this.globalData.titleBarHeight = 0 } }) } [代码] 然后需要在目录下新建个components文件夹,里面存放此次需要演示的文件 navigateTitle WXML 文件如下: [代码]<view class="navigate-container"> <view style="height:{{statusBarHeight}}px"></view> <view class="navigate-bar" style="height:{{titleBarHeight}}px"> <view class="navigate-icon"> <navigator class="navigator-back" open-type="navigateBack" wx:if="{{!isShowHome}}" /> <navigator class="navigator-home" open-type="switchTab" url="/pages/index/index" wx:else /> </view> <view class="navigate-title">{{title}}</view> <view class="navigate-icon"></view> </view> </view> <view class="navigate-line" style="height: {{statusBarHeight + titleBarHeight}}px; width: 100%;"></view> [代码] WXSS文件如下: [代码].navigate-container { position: fixed; top: 0; width: 100%; z-index: 9999; background: #FFF; } .navigate-bar { width: 100%; display: flex; justify-content: space-around; } .navigate-icon { width: 100rpx; height: 100rpx; display: flex; justify-content: space-around; } .navigate-title { width: 550rpx; text-align: center; line-height: 100rpx; font-size: 34rpx; color: #3c3c3c; font-weight: bold; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /*箭头部分*/ .navigator-back { width: 36rpx; height: 36rpx; align-self: center; } .navigator-back:after { content: ''; display: block; width: 22rpx; height: 22rpx; border-right: 4rpx solid #000; border-top: 4rpx solid #000; transform: rotate(225deg); } .navigator-home { width: 56rpx; height: 56rpx; background: url(https://qiniu-image.qtshe.com/20190301home.png) no-repeat center center; background-size: 100% 100%; align-self: center; } [代码] JS如下: [代码]var app = getApp() Component({ data: { statusBarHeight: '', titleBarHeight: '', isShowHome: false }, properties: { //属性值可以在组件使用时指定 title: { type: String, value: '青团公益' } }, pageLifetimes: { // 组件所在页面的生命周期函数 show() { let pageContext = getCurrentPages() if (pageContext.length > 1) { this.setData({ isShowHome: false }) } else { this.setData({ isShowHome: true }) } } }, attached() { this.setData({ statusBarHeight: app.globalData.statusBarHeight, titleBarHeight: app.globalData.titleBarHeight }) }, methods: {} }) [代码] JSON如下: [代码]{ "component": true } [代码] 如何引用? 需要引用的页面JSON里配置: [代码]"navigationStyle": "custom", "usingComponents": { "navigate-title": "/pages/components/navigateTitle/index" } [代码] WXML [代码]<navigate-title title="青团社" /> [代码] 按上面步骤操作即可实现一个自定义的导航栏。 如何实现通栏的效果默认透明以及滚动更换title为白色背景,如下图所示: [图片] [图片] [图片] [图片] 最后代码片段如下: https://developers.weixin.qq.com/s/wi6Pglmv7s8P。 以下为收集到的社区老哥们的分享: @Yunior: 小程序顶部自定义导航组件实现原理及坑分享 @志军: 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能 @✨o0o有脾气的酸奶💤 [有点炫]自定义navigate+分包+自定义tabbar @安晓苏 分享一个自适应的自定义导航栏组件
2020-03-10 - 小程序弯低轮播图制作
[图片] wxml [代码]<swiper class='banner' indicator-dots="{{indicatorDots}}" autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}"> <block wx:for="{{imgUrls}}" wx:key=""> <swiper-item> <image src="{{item}}" class="slide-image" /> </swiper-item> </block> </swiper> <view class='wanbox'> <image class='wan' src='http://wechatpx.oss-cn-beijing.aliyuncs.com/qixing/ceng.png'></image> </view> [代码] wxss [代码].slide-image { width: 750rpx; height: 345rpx; } .banner { width: 750rpx; height: 345rpx; } .wanbox{position: relative;z-index: 99} .wan { display: block; width: 750rpx; height: 35rpx; margin-top: -34rpx; } [代码]
2019-07-31 - 有赞Flutter插件开发与发布
一、Flutter插件简介 一种专用的Dart包,其中包含用Dart代码编写的API,以及针对Android(使用Java或Kotlin)和针对iOS(使用OC或Swift)平台的特定实现(另外也可以包含Native的组件代码),也就是说插件包括原生代码与Dart代码。插件开发完成后,将上传到dart插件管理服务仓库,类似于maven、pod库,然后在flutter开发过程中可以通过pubspec.yaml(dart包管理配置文件)来获取插件服务。 二、为什么要开发Flutter插件 随着Flutter生态越来越完善,以及Flutter在性能上的高光表现,越来越多的模块将会通过Flutter来进行实现。为了更方便的与原生工程进行对接以及降低整体工程的耦合,Flutter的开发模式也需要做成组件化的模式,拥有独立调试以及可拆卸的特性。原生工程在接入Flutter模块时,只需要在gradle(pod)中添加依赖,即可与Flutter模块进行交互。 在Flutter不同的模块开发过程中,我们不想重复的去搭建一些基础的flutter组件,比如埋点组件、网络通信组件、图片处理组件等,同时我们也希望在不同的Flutter模块开发过程中,保持Flutter 整体的视觉风格一致,所以我们需要抽离出一些Flutter通用插件,来保证风格的统一以及整体工程的简洁、清晰。 总结一下,Flutter插件化开发的好处: 组件独立维护,降低工程耦合 降低开发Flutter新模块的成本 保持整体风格统一 上面讲了Flutter插件包括原生模块与Dart模块,Dart模块很好理解,就是用dart写一些通用UI、通用IO等。那原生模块应该怎么理解? 首先,虽然Flutter的生态现在已经越来越完善了,但是相比于Android跟iOS原生的生态体系,还是远远不够。很多在Android跟iOS原生上有的很酷炫的库,在Flutter中还没有或者是并没有那么的完善。其次,想必大家在原生工程里都有一套用了多年的稳定基础组件,包括网络组件、数据组件等,要重新在Flutter中用dart来搭建一套,时间成本、风险成本、组件兼容性等都是不可控的。所以,最理想的方式就是Flutter的基础组件可以对我们现有原生的组件做一层包装,然后提供接口给Flutter模块进行调用,这样一来什么时间、风险、兼容性都不是问题。我们只要维护一套原生组件就好,Flutter组件只是一层包装,并不在意内部如何去实现。那么Flutter跟原生怎么进行交互呢? 三、Flutter如何与原生交互 Flutter与原生的交互模型,类似于一种C-S模型。其中Flutter为Client层,原生为Server层,两者通过MethodChannel进行消息通信,原生端向Flutter提供已有的Native组件功能。 在客户端,MethodChannel允许发送与方法调用相对应的消息。 在平台方面,Android上的MethodChannel和iOS上的FlutterMethodChannel启用接收方法调用并返回结果。 这些类允许你使用非常少的“样板”代码开发平台插件。 Flutter与原生的消息传递采用标准信息编解码器,是一种相对高效的二进制序列化与反序列化。当接收跟发送消息时,这些值在消息中会自动进行序列化与反序列化。 [图片] 1.什么是MethodChannel Flutter定义了3种Channel模型,分别是: BasicMessageChannel:用于传递字符串和半结构化的信息 MethodChannel:用于传递方法调用(method invocation) EventChannel: 用于数据流(event streams)的通信 3种channel之间既有共性,也有各自的特性,下面我们就MethodChannel进行展开 MethodChannel有3个重要的成员变量: [代码]- String name [代码] 在Flutter中会存在多个Channel,一个Channel对象通过name来进行唯一的标识,所以在Channel的命名上一定要独一无二,推荐采用组件名_Channel名 组合来进行命名 [代码]- BinaryMessenger messenger [代码] BinaryMessenger是Platform端与Flutter端通信的工具,其通信使用的消息格式为二进制格式数据。当我们初始化一个Channel,并向该Channel注册处理消息的Handler时,实际上会生成一个与之对应的BinaryMessageHandler,并以channel name为key,注册到BinaryMessenger中。当Flutter端发送消息到BinaryMessenger时,BinaryMessenger会根据其入参channel找到对应的BinaryMessageHandler,并交由其处理。 Binarymessenger在Android端是一个接口,其具体实现为FlutterNativeView。而其在iOS端是一个协议,名称为FlutterBinaryMessenger,FlutterViewController遵循了它。 Binarymessenger并不知道Channel的存在,它只和BinaryMessageHandler打交道。而Channel和BinaryMessageHandler则是一一对应的。由于Channel从BinaryMessageHandler接收到的消息是二进制格式数据,无法直接使用,故Channel会将该二进制消息通过Codec(消息编解码器)解码为能识别的消息并传递给Handler进行处理。 当Handler处理完消息之后,会通过回调函数返回result,并将result通过编解码器编码为二进制格式数据,通过BinaryMessenger返回。 [代码]- MethodCodec codec [代码] 消息编解码器Codec主要用于将二进制格式的数据转化为Handler能够识别的数据 MethodCodec主要是对MethodCall中这个对象进行序列化与反序列化 MethodCall是Flutter向Native发起调用产生的对象,其中包含了方法名以及一个参数集合(map或者是Json) 介绍完3个重要的变量,我们把整个流程连起来,看一下完成的交互流程是怎么样的 2.Flutter与原生通信整体流程 首先从dart层调用_channel.invokeMethod(“方法名”,参数),invoke方法会将传入的方法名与参数封装成MethodCall对象,然后通过MethodCodec对MethodCall对象进行编码,形成二进制格式。然后通过BinaryMessenger的send方法,将二进制格式的数据进行发送,我们继续看一下send方法是如何实现的 [代码]Future<dynamic> invokeMethod(String method, [dynamic arguments]) async { assert(method != null); ///send messenge final dynamic result = await BinaryMessages.send( name, codec.encodeMethodCall(MethodCall(method, arguments)), ); if (result == null) throw MissingPluginException('No implementation found for method $method on channel $name'); return codec.decodeEnvelope(result); } [代码] 这里截取了send方法里关键代码, dart层最终通过调用了native方法 Window_sendPlatformMessage,将序列化后的对象通过c层进行发送 [代码]static Future<ByteData> send(String channel, ByteData message) { final _MessageHandler handler = _mockHandlers[channel]; if (handler != null) return handler(message); return _sendPlatformMessage(channel, message); } String _sendPlatformMessage(String name, PlatformMessageResponseCallback callback, ByteData data) native 'Window_sendPlatformMessage'; [代码] 我们在Flutter engine的native代码中可以找到上述native方法的对应实现,这里截取关键部分,可以看到最后是交给了WindowClient的handlePlatformMessage方法进行实现,我们继续往下跟 [代码]... dart_state->window()->client()->HandlePlatformMessage( fml::MakeRefCounted<PlatformMessage>(name, response)); ... [代码] (这里以Android举例,iOS同理)可以看到,在Android平台HandlePlatformMessage方法中,调用到了JNI方法,将c层收到的信息向java层抛 [代码]void PlatformViewAndroid::HandlePlatformMessage( fml::RefPtr<blink::PlatformMessage> message) { JNIEnv* env = fml::jni::AttachCurrentThread(); fml::jni::ScopedJavaLocalRef<jobject> view = java_object_.get(env); auto java_channel = fml::jni::StringToJavaString(env, message->channel()); if (message->hasData()) { fml::jni::ScopedJavaLocalRef<jbyteArray> message_array(env, env->NewByteArray(message->data().size())); env->SetByteArrayRegion( message_array.obj(), 0, message->data().size(), reinterpret_cast<const jbyte*>(message->data().data())); message = nullptr; // This call can re-enter in InvokePlatformMessageXxxResponseCallback. FlutterViewHandlePlatformMessage(env, view.obj(), java_channel.obj(), message_array.obj(), response_id); } else { message = nullptr; // This call can re-enter in InvokePlatformMessageXxxResponseCallback. FlutterViewHandlePlatformMessage(env, view.obj(), java_channel.obj(), nullptr, response_id); } } [代码] 看一下JNI对应的java方法,最终通过handler.onMessage(),完成了本次dart信息的传递。方法中的handler,就是我们前面提到的MethodHandler,也是我们插件的Native模块注册的MethodHandler [代码] private void handlePlatformMessage(final String channel, byte[] message, final int replyId) { this.assertAttached(); BinaryMessageHandler handler = (BinaryMessageHandler)this.mMessageHandlers.get(channel); if (handler != null) { try { ByteBuffer buffer = message == null ? null : ByteBuffer.wrap(message); handler.onMessage(buffer, new BinaryReply() { // ... }); } catch (Exception var6) { // ... } } else { Log.e("FlutterNativeView", "Uncaught exception in binary message listener", var6); nativeInvokePlatformMessageEmptyResponseCallback(this.mNativePlatformView, replyId); } } [代码] MethodHandler 接口有2个回调参数 MethodCall 、Result [代码]public interface MethodCallHandler { void onMethodCall(MethodCall var1, MethodChannel.Result var2); } [代码] 其中MethodCall就是我们前面说的,由dart端传递过来通过序列化、反序列化的对象。 Platform端可以从MethodCall中取出方法名以及参数,然后进行实现。 Result是一个回调接口,最终的结果会通过另一个序列化、反序列化的过程返回给dart,过程就跟上述的一致,如果无需任何返回的,可以不用这个参数。 [代码]public interface Result { void success(@Nullable Object var1); void error(String var1, @Nullable String var2, @Nullable Object var3); void notImplemented(); } [代码] MethodHandler 是在什么时候注册的? 在插件运行的时候,我们会调用插件的registerWith方法,在生成MethodChannel对象时,同时向MethodChannel注册了一个MethodHandler,MethodHandler对象跟MethodChannel对象是一一对应的。 以上就是整个Flutter与Native的交互流程,消息的传递是通过跨平台的c来实现。以下是Flutter到原生的消息传递流程图,Native到Flutter也是类似的。 [图片] 讲完了通信流程,下面开始正式进入插件开发。 四、创建插件工程 推荐通过命令行来创建,因为通过IDE来创建有时候会卡住,而且会比较慢 [代码]flutter create --org com.qima.kdt --template=plugin -i swift -a kotlin flutter_plugin [代码] 创建好以后的目录结构如下 rootProject lib dart模块 android android模块 ios ios模块 example 示例测试工程可用于插件的调试 pubspec.yaml flutter项目的配置文件 …. 1.什么是pubspec.yaml dart生态下的包管理配置文件类似 Android中的gradle、iOS中的Podfile,在这里可以统一管理整个flutter工程的dart依赖包,以及管理整个插件的发布属性。 2.创建过程可能会遇到的问题 IDE 一直卡在 creating Flutter Project…… 原因: Flutter工程在创建过程中需要下载需要的插件,因为网络原因导致需要的插件无法下载成功会导致该问题 解决: 切换网络,或者搭一个梯子 通过命令行来创建插件 编译Android模块遇到Invoke-customs are only supported starting with Android O (–min-api 26) 在app.gradle中增加 [代码]groovy compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } [代码] 创建完插件工程后,分别对原生端与Flutter端进行开发 3.原生端开发 实现MethodCallHandler接口,注册MethodChannel对象,MethodChannel在创建时一定要保证name唯一 将MethodHandler接口注册到MethodChannel中 包装原生端组件,包括一些二方库、三方库,将包好的方法通过MethodCallHandler暴露给Flutter端 4.Flutter端开发 找到MethodChannel对象,通过唯一标识name,注意(name一定要与原生端注册的一致) 定义dart方法,因为要保证方法的执行不产生阻塞,所以推荐用Future async await .相关的语法见dart语法 调用methodChannel.invokeMothed()与原生进行通信 以上就完成了整个插件部分的开发,开发完成后,先不急着将插件发布。可以先在本地的example中对所开发的插件进行验证,验证无误后,再进行发布 五、插件测试 在example/lib/main.dart下调用插件中的方法,然后直接通过命令将工程跑起来查看输出 [代码]flutter run [代码] 1.插件都还没有发布,为什么example工程可以直接引用? 看一下example目录下的pubspec.yaml文件,里面有一句 [代码]yaml xxxxx(插件名): path: ../ [代码] pubspec.yaml 不但可以引用服务器上的插件,也可以引用本地路径下的插件。如此我们可以在插件未发布的情况下,直接在本地的测试工程里对插件进行测试 后续的所有flutter模块的单独调试,也是同样的模式。开发完flutter模块后,直接在example工程中引入调试,不必与host工程进行耦合,可以提供整体的开发效率。测试没有问题后,在进行插件发布,集成开发。 六、插件发布 1.私有Flutter服务器环境搭建 Flutter插件默认是上传到Flutter社区的公共仓库中,实际开发中,我们会有很多暂时不想要开源,只供团队内部使用的插件。因此将这些插件发布到Flutter社区中明显是不合适的,所以需要搭建一个团队内私有的flutter插件管理环境。官方提供了接入文档,这里不展开了。 dart环境配置 服务器搭建 1)官方代码结构简要说明 [图片] example.dart 程序入口,负责各种数据配置,及服务启动 shelf_pubserver.dart 定义了当前dart服务支持的所有接口 获取某个插件的信息 /api/packages/ 获取某个插件特定版本的信息 /api/packages//versions/ 下载插件 /api/packages//versions/.tar.gz 上传插件 /api/packages/versions/new 删除插件 /api/packages//uploaders/ 因为上传的插件文件都是存储在Linux服务器上的,并且已经提供以上这些接口,因此后期也可以简单搭建个flutter web网站,查看私有服务器上的插件包信息,方便开发使用。 启动服务 [代码]dart example/example.dart -s 是否fetch官方仓库 -h ${ip / domain} -p 端口 -d 上传上来的插件包在服务器上的存储地址 [代码] 完成了私有flutter插件管理服务环境后,准备开始插件的上传,首先需要检查本地插件的发布配置信息 2.完善pubspec.yaml文件 [代码]name: 插件名称 description: 插件描述 version: 0.0.1 版本号 author: xxxx<xx@xxx.com> homepage: 项目主页地址 publish_to: 填写私有服务器的地址(如果是发布到flutter pub则不用填写,插件默认是上传到flutter pub) [代码] 3.检验是否满足上传条件 [代码]flutter packages pub publish --dry-run [代码] –dry-run 参数表示本次执行会检查插件的配置信息是否有效,插件是否满足上传条件。如果成功的话并不会真正的将插件上传,而是会显示本次要发布插件的信息,并提示成功。一般在插件的正式发布前,建议先执行该命令,避免在上传过程中出现错误 当插件符合上传条件后,可以开始进行正式发布 4.正式发布 发布至pub平台 [代码]flutter packages pub publish [代码] 发布至私有服务器 [代码]flutter packages pub publish --server $服务器地址 [代码] pubspec.yaml文件中列出的包作者与授权发布该包的人员列表不同。发布某个软件包的第一个版本的人自动成为第一个也是唯一一个有权上传其他版本软件包的人。要允许或禁止其他人上载版本,请使用pub uploader命令。 最终出现如下内容,代表上传成功 [代码].... |-- local.properties |-- pubspec.yaml |-- test | '-- xxxx.dart '-- xxxx.iml Looks great! Are you ready to upload your package (y/n)? y Uploading... Successfully uploaded package. [代码] 七、插件引用 开发上传完成后,就可以在后续的任何Flutter模块中,在pubspec.yaml中添加依赖进行引用 pub仓库插件 [代码]#插件名:版本号 flutter_boost: ^0.0.411 [代码] 私有仓库引用 [代码]${library name}: hosted: name: ${library name} url: xxxxx version: ^1.0.0 [代码] ok,以上就是完整的Flutter插件开发、发布、引用的流程。 八、有赞路由插件开发实践 有赞路由插件第一版的开发思路是对开源项目flutter-boost做一层包装,然后接入到flutter业务中。后期用有赞自己的flutter路由组件替换flutter-boost。 我们按照上述流程,在pubspec.yaml中引入了flutter-boost插件,然后进行二次包装。在包装dart接口时很顺利,没有遇到什么阻碍。然而在Native模块,却一直不能引用到flutter-boost中的native code。不仅仅是android如此,iOS的同学也遇到同样的问题。 是不是插件引用插件,宿主插件就无法引用接入插件的native代码呢?我们又试了试,创建了一个flutter module 以及一个一个flutter application来接入flutter-boost插件,看看能否引用到flutter-boost中的原生代码,最后发现都可以引用,唯独flutter plugin无法引用。 看来应该是插件工程的特殊性导致。于是,我们开始对比插件工程与其他工程的区别,最终发现,module工程以及application工程比插件工程多了一个include_flutter.groovy文件 [代码]rootProject.name = 'android_generated' setBinding(new Binding([gradle: this])) evaluate(new File('include_flutter.groovy')) [代码] iOS多了一个 podhelper.rb [代码]flutter_application_path = '../my_flutter/' eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding) end [代码] 看一看这个文件到底做了什么,以android举例 [代码]def scriptFile = getClass().protectionDomain.codeSource.location.toURI() def flutterProjectRoot = new File(scriptFile).parentFile.parentFile gradle.include ':flutter' //获取项目的根目录 gradle.project(':flutter').projectDir = new File(flutterProjectRoot, '.android/Flutter') def plugins = new Properties() //在根目录下找到一个叫 .flutter-plugins的文件,然后逐行读入; def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins') if (pluginsFile.exists()) { pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } } //.flutter-plugins的内容如下,存放了对应原生模块的名字以及路径 flutter_boost=/Users/xxx/Downloads/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_boost-0.0.415/ xservice_kit=/Users/xxx/Downloads/flutter/.pub-cache/hosted/pub.flutter-io.cn/xservice_kit-0.0.29/ //如果是android工程的,则通过gradle引用到工程中,完成对插件原生lib的引用 plugins.each { name, path -> def pluginDirectory = flutterProjectRoot.toPath().resolve(path).resolve('android').toFile() gradle.include ":$name" gradle.project(":$name").projectDir = pluginDirectory } ... [代码] ok,到这里就很清楚了。一个dart插件不仅仅提供的是dart层的功能,其原生层的功能也可以直接给宿主的原生层去引用。dart插件在完成打包后,其原生部分的代码也会被打成一个依赖包。插件工程默认是不能够引用三方插件的原生依赖包,只能引用到dart部分。当然如果想要引用到三方插件的native功能,需要自己写一个类似于flutter module工程自动创建的依赖包收集脚本。 九、总结 目前Flutter生态越来越完善,后续不可避免的会越来越多的与Flutter进行交互。为了更好的与Native项目的兼容,减少原生工程与Flutter业务的耦合,Flutter插件化是一个不错的选择。目前有赞Flutter插件化项目已经封装了网络、埋点、路由等基础插件,后续将在线上应用进行接入尝试,希望能给正在探索Flutter的同学一些灵感。
2019-07-26