- 小程序app.globalData属性值改变时其它页面的引用响应更新
前提说明 小程序app.js的globalData中定义了userInfo属性,并且在首页和我的tab中引用了,当在其它页面更新userInfo后,在首页和我的中引用的userInfo未更新。 解决思路 利用发布-订阅的设计模式,app.js中的userInfo用Object.defineProperty实现数据劫持,当监听到userInfo值改变时,通知每个订阅者。在首页和我的页面调用app.js中的订阅方法,将更新数据的方法追加到userInfo的订阅者列表中。 实现代码 [代码]// app.js onLaunch: async function () { this.initObserve(); }, // 监听globalData中属性变化 initObserve() { const obj = this.globalData; const keys = ['userInfo']; keys.forEach(key => { let value = obj[key]; obj[`${key}SubscriberList`] = []; Object.defineProperty(obj, key, { configurable: true, enumerable: true, set(newValue) { obj[`${key}SubscriberList`].forEach(watch => { watch(newValue); }); value = newValue; }, get() { return value; } }); }); }, // 订阅globalData中某个属性变化 subscribe(key, watch) { watch(this.globalData[key]); this.globalData[`${key}SubscriberList`].push(watch); }, // 首页和我的page页 onLoad() { app.subscribe('userInfo', (userInfo) => { this.setData({ userInfo, }); }); }, [代码] 遇到的问题 小心Object.defineProperty中的set方法死循环导致栈溢出。在set用obj[key] = value时将会导致死循环,因为给属性赋值后,会再次调用set方法。解决的办法是利用闭包的原理,定义临时变量为obj[key],在set方法中对临时变量赋值。或者在obj中声明一个变量的副本,set中对变量副本赋值,get中返回变量副本。
2021-01-16 - app.onLaunch与page.onLoad异步问题
问题:相信很多人都遇到过这个问题,通常我们会在应用启动app.onLaunch() 去发起静默登录,同时我们需要在加载页面的时候,去调用一个需要登录态的后端 API 。由于两者都是异步,往往page.onload()调用API的时候,app.onLaunch() 内调用的静态登录过程还没有完成,从而导致请求失败。 解决方案:1. 通过回调函数// on app.js App({ onLaunch() { login() // 把hasLogin设置为 true .then(() => { this.globalData.hasLogin = true; if (this.checkLoginReadyCallback) { this.checkLoginReadyCallback(); } }) // 把hasLogin设置为 false .catch(() => { this.globalData.hasLogin = false; }); }, }); // on page.js Page({ onLoad() { if (getApp().globalData.hasLogin) { // 登录已完成 fn() // do something } else { getApp().checkLoginReadyCallback = () => { fn() } } }, }); ⚠️注意:这个方法有一定的缺陷(如果启动页中有多个组件需要判断登录情况,就会产生多个异步回调,过程冗余),不建议采用。 2. 通过Object.defineProperty监听globalData中的hasLogin值 // on app.js App({ onLaunch() { login() // 把hasLogin设置为 true .then(() => { this.globalData.hasLogin = true; }) // 把hasLogin设置为 false .catch(() => { this.globalData.hasLogin = false; }); }, // 监听hasLogin属性 watch: function (fn) { var obj = this.globalData Object.defineProperty(obj, 'hasLogin', { configurable: true, enumerable: true, set: function (value) { this._hasLogin = value; fn(value); }, get: function () { return this._hasLogin } }) }, }); // on page.js Page({ onLoad() { if (getApp().globalData.hasLogin) { // 登录已完成 fn() // do something } else { getApp().watch(() => fn()) } }, }); 3. 通过beautywe的状态机插件(项目中使用该方法) // on app.js import { BtApp } from '@beautywe/core/index.js'; import status from '@beautywe/plugin-status/index.js'; import event from '@beautywe/plugin-event/index.js'; const app = new BtApp({ onLaunch() { // 发起静默登录调用 login() // 把状态机设置为 success .then(() => this.status.get('login').success()) // 把状态机设置为 fail .catch(() => this.status.get('login').fail()); }, }); // status 插件依赖于 beautywe-plugin-event app.use(event()); // 使用 status 插件 app.use(status({ statuses: [ 'login' ], })); // 使用原生的 App 方法 App(app); // on page.js Page({ onLoad() { // must 里面会进行状态的判断,例如登录中就等待,登录成功就直接返回,登录失败抛出等。 getApp().status.get('login').must().then(() => { // 进行一些需要登录态的操作... }) }, }); 具体实现 具体实现可以参考我的商城小程序项目 项目体验地址:体验 代码:代码
2021-05-20 - 小程序页面(Page)扩展,为所有页面添加公共的生命周期、事件处理等函数
背景 在小程序的原生开发中,页面中经常会用到一些公共方法,例如在页面onLoad中验证权限、所有页面都需要onShareAppMessage设置分享等 假设我们在编码时每个页面都写一遍,显然不是一个高级程序员会干的事情,太Low了。如果我们定义一个公共文件,导出这些公共方法,每个页面都引入,然后再生命周期或者事件处理函数中调用,虽然看起来很方便,但不够优雅,达不到我们最终的目的(偷懒)。 下面给大家介绍一种相对比较优雅的实现方式,扩展Page来实现以上的操作。 Page(页面) 需要传入的是一个 [代码]object[代码] 类型的参数,那么我们重载一个 [代码]Page[代码] 函数,将这个 [代码]object[代码] 参数拦截改掉就可以了,下面直接上代码。 实现 1、在根目录新建一个 [代码]page-extend.js[代码] 文件,公共的逻辑都写在这里面 [代码]/** * * Page扩展函数 * * @param {*} Page 原生Page */ const pageExtend = Page => { return object => { // 导出原生Page传入的object参数中的生命周期函数 // 由于命名冲突,所以将onLoad生命周期函数命名成了onLoaded const { onLoaded } = object // 公共的onLoad生命周期函数 object.onLoad = function (options) { // 在onLoad中执行的代码 ... // 执行onLoaded生命周期函数 if (typeof onLoaded === 'function') { onLoaded.call(this, options) } } // 公共的onShareAppMessage事件处理函数 object.onShareAppMessage = () => { return { title: '分享标题', imageUrl: '分享封面' } } return Page(object) } } // 获取原生Page const originalPage = Page // 定义一个新的Page,将原生Page传入Page扩展函数 Page = pageExtend(originalPage) [代码] 2、在 [代码]app.js[代码] 中引入 [代码]page-extend.js[代码] 文件 [代码]require('./page-extend') App({ // 其他代码 ... }) [代码] 代码片段 https://developers.weixin.qq.com/s/Cyx8iGmV7Ldp 本文内容及评论未经允许,禁止任何形式的转载与复制(代码可在程序中使用)
2019-12-24 - 生成JSON Line小工具更新记录
我一直在维护一个生成JSON Line的小工具,目前已上线半年,运行良好,也得到了很多朋友的欢迎和支持 更新记录2020-11-26 修复之前每天可导入一次的问题,目前可支持无限次数导入; 本文内容 在这个小工具在使用过程中一直存在一个问题,那就是同一个集合每天只可以导入一次,当时开发这个工具的时候是基于我答题小程序的场景,所以没有考虑太多 在生成_id的时候用了年月日做为前缀,这便是上面问题的根源, 本次修改将前缀改为年月日时分秒 工具截图https://www.xiaomutong.com.cn/index20200501.html [图片] 使用方法1)点击上面的选择文件按钮,将文件是上传; 2)点击下面的开始生产按钮 这时候会自动生成一个JSON文件,如下图所示 [图片] 然后就可以按照对应的集合格式进行修改并导入了
2020-11-26 - [有点炫]自定义navigate+分包+自定义tabbar
自定义navigate+分包+自定义tabbar,有需要的可以拿去用用,可能会存在一些问题,根据自己的业务改改吧 大家也可以多多交流 代码片段:在这里 {"version":"1.1.5","update":[{"title":"修复 [复制代码片段提示] 无法使用的问题","date":"2020-06-15 09:20","imgs":[]}]} 更新日志: 2019-11-25 自定义navigate 也可以调用wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 2019-11-25 页面滚动条显示在自定义navigate 和 自定义tabbar上面的问题(点击“体验custom Tabbar” [图片] [图片] 其他demo: 云开发之微信支付:代码片段
2020-06-15 - 微信小程序路由实战
欢迎来到我博客阅读:BlueSun - 微信小程序路由实战 0. 目录 1. 前言 2. 智能路由跳转 — Navigator 模块 3. 虚拟路由策略 — Router 模块 4. 落地中转策略 — LandTransfer 模块 4.1. 对于要解决的第一个问题:统一的落地页 4.2. 对于第二个要解决的问题:短链参数 4.3. LandTransfer 模块设计 5. 更好的开发体验 5.1. Typescript + Router 5.2. 智能生成路由配置 5.3. 自定义组件跳转 6. 整体架构图 7. 最后的最后 1. 前言 在微信小程序由一个 [代码]App()[代码]实例,和众多[代码]Page()[代码]组成。而在小程序中所有页面的路由全部由框架进行管理,框架以栈的形式维护了所有页面,然后提供了以下 API 来进行路由之间的跳转: [代码]wx.navigateTo[代码] [代码]wx.redirectTo[代码] [代码]wx.navigateBack[代码] [代码]wx.switchTab[代码] [代码]wx.reLaunch[代码] 但是,对于一个企业应用,把这些问题留给了开发者: 原生 API 使用了 [代码]Callback[代码] 的函数实现形式,与我们现代普遍的 [代码]Promise[代码] 和 [代码]async/await[代码] 存在 gap。 基于小程序路由的设计,暴露给外部的是真实路由(如扫码,公众号链接等方式),对后续项目重构留下历史包袱。 小程序页面栈最多十层, 在超过十层后 [代码]wx.navigateTo[代码] 失效,需要开发者判断使用 [代码]wx.redirectTo[代码] 或其他API 小程序页面栈存在一种特殊的页面:Tab 页面,需要使用 [代码]wx.switchTab[代码] 才能跳转。需要开发者主动判断,不方便后期改动 Tab 页面属性。 额外的,对于小程序码,要使用无数量限制 API wxacode.getUnlimited ,存在参数长度限制32位以内。需要开发者自行解决。 而本文,期望能对这若干问题,逐个提供解决方案。 2. 智能路由跳转 — Navigator 模块 在这里我们一起解决: 原生 API 非 Promsie 页面栈突破十层时特殊处理 特殊页面 Tab 的跳转处理 我们的思路是,希望能设计一种逻辑,根据场景来自动判断使用哪个微信路由 API,然后对外只提供一个函数,例如: [代码]gotoPage('/pages/goods/index') [代码] 具体逻辑如下: 当跳转的路由为小程序 tab 页面时,则使用 [代码]wx.switchTab[代码]。 当页面栈达到 10 层之后,如果要跳转的页面在页面栈中,使用 [代码]wx.navigateBack({ delta: X })[代码] 出栈到目标页面。 当页面栈达到 10 层之后,目标页面不存在页面栈中,使用 [代码]wx.redirectTo[代码] 替换栈顶页面。 其他情况使用 [代码]wx.navigateTo[代码] 顺带的,我们把这个函数以 Promise 形式实现,以及支持参数作为 [代码]object[代码]传入,例如: [代码]gotoPage('/pages/goods/index', { name: 'jc' }).then(...).catch(...); [代码] 大部分场景下,只要使用[代码]gotoPage[代码]就能满足。 那肯定也会有特定的情况,需要显式的指定使用 [代码]navigateTo/switchTab/redirectTo/navigateBack[代码]的哪一个。 那么我们也按照类似的实现,满足相同模式的 API [代码]navigateTo('/pages/goods/index', { name: 'jc' }).then(...).catch(...); switchTab('/pages/goods/index', { name: 'jc' }).then(...).catch(...); redirectTo('/pages/goods/index', { name: 'jc' }).then(...).catch(...); navigateBack('/pages/goods/index', { name: 'jc' }).then(...).catch(...); [代码] 这些函数都可以内聚到同一个模块,我们称其为:Navigator [代码]const navigator = new Navigator(); navigator.gotoPage(...); navigator.navigateTo(...); navigator.switchTab(...); navigator.redirectTo(...); navigator.navigateBack(...); [代码] 模块设计: [图片] 3. 虚拟路由策略 — Router 模块 在这里,我们解决: 对外暴露了真实路由,导致历史包袱沉重的问题。 在许多应用开发中,我们经常需要把某种模式匹配到的所有路由,全都映射到同个页面中去。 例如,我们有一个 Goods 页面,对于所有 ID 各不相同的商品,都要使用这个页面来承载。 [图片] 那么在代码层面上,期望能实现这样的调用方式: [代码]// 创建路由实例 const router = new Router(); // 注册路由 router.register({ path: '/goods/:id', // 虚拟路由 route: '/pages/goods/index', // 真实路由 }); // 跳转到 /pages/goods/index,参数: onLoad(options) 的 options = { id: '123' } router.gotoPage('/goods/123'); // 跳转到 /pages/goods/index,参数: onLoad(options) 的 options = { id: '456' } router.gotoPage('/goods/456'); [代码] Class Router 的核心逻辑是完成: 路由的注册,完成「虚拟路径」和「真实路径」关系的存储。 满足「虚拟路径」到「真实路径」的转换,并且识别「动态路径参数」(dynamic segment)。 路由跳转。 对于「路由的注册」,我们在其内部存储一个 map 就能完成。 而对于「路径的转换」, [代码]vue-router[代码] 有类似的实现,通过其源码发现,内部是使用 path-to-regexp 作为路径匹配引擎,我们可以拿来用之。 然后对于「路由的跳转」,我们可以直接复用上面提到的 Navigator 模块,通过输入真实路径,来完成路由的跳转。 模块设计: [图片] 其中: RouteMatcher:提供动态路由参数匹配功能,内部使用 path-to-regexp 作为路径匹配引擎。 Route: 为每个路径创建路由器,存储每个路由的虚拟路径和真实路由的关系。 Router:整合内部各模块,对外提供统一且优雅的调用方式。 4. 落地中转策略 — LandTransfer 模块 在这里,我们解决: 小程序扫码、公众号链接等场景下的落地页统一。 小程序码,对于无限量API wxacode.getUnlimited ,突破参数32位长度限制。 4.1. 对于要解决的第一个问题:统一的落地页 我们把如:扫小程序码、公众号菜单、公众号文章等方式打开小程序某个页面的路径称为「外部路由」。 根据小程序的设计,暴露给外部的连接是真实的页面路径,如:[代码]/pages/home/index[代码],该设计在实践中存在的弊端:各个落地页分散,后期修改真实文件路径难度大。 在 「中长生命周期」 产品中,随着产品的迭代,我们难免会遇到项目的重构。如果分发出去的都是没经过处理的真实路径的话,我们重构时就会束手束脚,要做很多的兼容操作。因为你不知道,分发出去的小程序二维码, 有多少被打印到实体物料中。 那么,「虚拟路由」+「落地中转」 的策略就显得基本且重要了。 「虚拟路由」的功能,**Router **模块给我们提供了支持了,我们还需要对外提供一个统一的落地页面,让它来完成对内部路由的中转。 基本逻辑: 分发出去的真实路由,指向到唯一的落地页面,如:[代码]$LAND_PAGE: /pages/land-page/index[代码] 由这个落地页面,进行内部路由的重定向转发,通过接收 参数,如:[代码]path=/user&name=jc&age=18[代码] [图片] 在代码层面上,我们希望能实现这样的使用: [代码]// /pages/land-page/index.ts const landTransfer = new LandTransfer(landTransferOptions); Page({ onLoad(options) { landTransfer .run(options) .then(() => {...}) .catch(() => {...}); } }); [代码] 然后针对 TS,我们还可以使用装饰器版本,更加简便: [代码]import { landTransferDecorator } from 'wxapp-router'; Page({ @landTransferDecorator(landTransferOptions) onLoad(options) { // ... }, }); [代码] 4.2. 对于第二个要解决的问题:短链参数 微信小程序主要提供了两个接口去生成小程序码: wxacode.get: 获取小程序码,适用于需要的码数量较少的业务场景。通过该接口生成的小程序码,永久有效,数量限制为 100,000 个 wxacode.getUnlimited: 获取小程序码,适用于需要的码数量极多的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制。 第一种方式,[代码]wxacode.get[代码] 数量限制为 10w 个,虽然量很大了,绝大多数的小程序可能用不到这个量。 但如果我们运营的是一个中大型电商小程序的话,假如:1w 种商品 x 10 种商品规格,那就会超过这个数量。到时候再进行改造,就困难了。 所以,如果抱着是运营一个 「中长生命周期」 的产品的话,我们会使用第二种方式:[代码]wxacode.getUnlimited[代码] 不尽人意的是,虽然它没有数量限制,但是对参数会有 32 个字符的限制,显然是不够用的(一个 uuid 就 32 字符了)。 对于这种情况,我们可以使用「短链参数」的形式解决,由于wxacode.getUnlimited 会通过 [代码]scene[代码]字段作为 query 参数传递给小程序的,那么我们可以通过 [代码]scene[代码]参数来实现短链服务,这需要后端配合。 前后端交互如下: [图片] 当小程序需要生成小程序码的时候,请求后端提供的接口,例如:[代码]/api/encodeShortParams[代码] 后端把内容转换为 32 字符内的字符串,存储到数据库中。 后端通过 wxacode.getUnlimited 接口,以短链字符串作为 [代码]scene[代码]的值,以商定好的统一落地页 [代码]$LAND_PAGE[代码]作为 [代码]page[代码]值,生成小程序码。 当通过小程序码进入小程序,小程序获取到 [代码]scene[代码]参数,请求后端提供的接口,例如:[代码]/api/decodeShrotParams[代码] 小程序理解内容,跳转到目标页面中去。 而前端对于统一落地页的逻辑处理,我们只需要在第一个问题的基础上,增加一个转换短链参数内容的逻辑就行了: [图片] 代码层面上,我们我们只需要多定义转换短链参数的方式:[代码]convertScenePrams[代码] [代码]// in /pages/land-page/index.js import { landTransferDecorator } from 'wxapp-router'; const landTransferOptions = { // 此处接收 onLoad(options) 中的 options.scene convertSceneParams: (sceneParams) => { return API.convertScene({ sceneParams }).then((content) => { // 假如后端存的是 JSON 字符串,前端decode // 要求 content = { path: '/home', a: 1, b:2 } return JSON.parse(content); }); }, }; Page({ @landTransferDecorator(landTransferOptions) onLoad(options) { // ... }, }); [代码] 而其中的 [代码]API.convertScene[代码] 就对接服务端提供 HTTP 接口服务来完成。 4.3. LandTransfer 模块设计 [图片] 5. 更好的开发体验 5.1. Typescript + Router 对于小程序内部的路由跳转,我们除了指定一个字符串的路由,我们是否也可以通过链式调用,像调用函数那样去跳转页面呢?类似这样; [代码]routes.pages.user.go({ name: 'jc' }); [代码] 这样做的好处是: 更自然的调用方式。 能结合 TS,来做到类型提示和联想。 由于事先 [代码]wxapp-router[代码] 并不知道开发者需要注册的路由是什么样的,所以路由的 TS 声明文件,需要开发者来定义。 例如,我们在项目中维护一份路由文件: [代码]// config/routes.ts // 创建路由实例 const router = new Router(); const routesConfig = [{ path: '/user', route: '/pages/user/index', }, { path: '/goods', route: '/pages/goods/index', }]; type RoutesType { paegs: { user: Route<{name: string}>, goods: Route, } } // 注册路由 router.batchRegister(routesConfig); // 获取 routes const routes: RoutesType = router.getRoutes(); export default routes; [代码] 然后在别的地方使用它: [代码]import routes from './routes.ts'; routes.pages.user.go({ name: 'jc' }); [代码] 5.2. 智能生成路由配置 如果路由变多的时候,我们还需要对每个路由手动去编写 [代码]RoutesType[代码] 的话,就有点难受了。 在小程序中,我们把正式路由都配置到 [代码]app.json[代码] ,那么在遵循既定的项目结构情况下,我们可以通过自动构建,完成大部分工作,例如: 智能注册路由 智能识别页面入参声明 5.3. 自定义组件跳转 以上都是脚本层面的使用,小程序中还有 [代码]wxml[代码], 我们希望能在有个组件快速使用: [代码]<Router path="/pageA" query="{{pageAQuery}}"></Router> <Router path="/pageB" query="{{pageBQuery}}" type="redirectTo"></Router> <Router path="/pageC/katy"></Router> [代码] 那么,实现一个自定义组件,然后把 Router模块包装一下,问题就不大了。 示例代码: [代码]// components/router.wxml <view class="wxapp-router" bind:tap="gotoPage"> <slot /> </view> [代码] [代码]// components/router.ts Component({ properties: { path: String, type: { type: String, value: 'gotoPage' }, route: String, query: Object, delta: Number, setData: Object, }, methods: { gotoPage(event) { const router = getApp().router; const { path, route, type, query} = this.data; const toPath = route || path; if (['gotoPage', 'navigateTo', 'switchTab', 'redirectTo'].includes(type)) { (router as any)[type](toPath, query); } if (type === 'navigateBack') { const { delta, setData } = this.data; router.navigateBack({ delta }, { setData }) } } } }) [代码] 6. 整体架构图 最后,我们来整体回顾一下各模块的设计 [图片] Navigator:封装微信原生路由 API,提供智能跳转策略。 LandTransfer:提供落地页中转策略。 RouteMatcher:提供动态路由参数匹配功能。 Route: 为每个路径创建路由器。 Router:整合内部各模块,对外提供优雅的调用方式。 Logger:内部日志器。 Path-to-regexp: 开源社区的路由匹配引擎。 7. 最后的最后 鉴于写过很多的实战类的文章,会有不少同学想要到整体的示例代码,这次我就索性写了一个工具,Enjoy it! wxapp-router: 🛵 The router for Wechat Miniprogram
2021-03-31 - 浅谈小程序路由的封装设计
微信官方提供了基础的路由能力,在日常的开发中虽已够用,但随着开发的深入,会遇到许多值得思考提炼的问题。本文将探讨作者在微信小程序(以下简称小程序)开发当中遇到的问题,以及解决方案设计。 参考“WHY-HOW-WHAT"黄金圈思维法则,首先讲述为什么小程序的路由需要封装设计,也就是存在哪些问题,需要封装处理? 存在的问题 路由跳转的路径与文件路径耦合 小程序的路由跳转使用的是真实文件路径,因此若文件的结构发生变化,必会影响到所有的页面的跳转路径。 笔者在实际开发中就遇到这个问题,以小程序分包举例。 小程序的分包是以文件夹为单位的。如果要将一系列的页面拆分成分包,则需要将这些文件移至同个目录之下,因此必然导致路由的跳转路径发生变更。如果此时路由跳转均是直接通过文件路径跳转的话,则需要全局改动,导致的工作量不少。 另外,当开发团队比较庞大时,不同的业务之间总会存在互相跳转的情况。当其中一个页面地址发生变更时,其他业务跳转到该页面的路径都需要手动变更。若此时通知不及时,或者遗漏了一些地方,导致跳转失败,终会酿成大错。 路由传参 目前小程序支持的传参方式,即通过跳转路径上的query查询参数。 通过query传参的问题,与在Web上URL传参是一致的,比如: query的参数长度有限 query只能传递可序列化的数据 导航前需手动序列化,到达目标页面后需反序列化 条件导航 在日常业务中,会存在一些页面需要一定条件才允许进入的。 举个例子,会员服务是一种很常见的能力,而会员中心的进入条件是: 该用户已经完成登录 该用户是本产品的会员 一般情况下,这有特定准入资格的页面的导航逻辑是这样的: [图片] 这种方式简单明了,但存在一个问题:需要每次跳转前主动判断,逻辑冗余以外,还可能被遗漏。 思路 由于小程序本身已提供了基础的路由导航能力,不像react、vue.js那样需要从底层进行封装,从而提供路由能力。但是,本质上小程序可以理解成类vue.js这样的框架,因此可以从vue.js的路由库vue-router上找到灵感,从而解决以上问题。 命名路由 使用 命名路由 的方式可以解决前文提及的跳转路径和文件真实路径耦合问题。 通过[代码]Map[代码]来映射 页面ID 和 页面地址,路由跳转时,仅能使用 页面ID 进行路由跳转。 下面以导航至首页举例: [代码]// before wx.switchTab('pages/home/index') // after router.go('home') [代码] 由于小程序有tab页面和普通页面之分,因此导航至tab页时需使用switchTab 细心的读者可能会发现上文使用了[代码]go[代码]方法,而不是[代码]switchTab[代码]。其实,具体哪些页面属于tab页面,在[代码]app.json[代码]已经明确配置。对于使用者来说,不需要关心跳转的页面是属于哪种类型,这些细节都应该统一在底层封装好。下面罗列[代码]Router[代码]与官方API的对应关系: [图片] Router API的设计原则是保持简单,以及尽量保持与web规范一致 传递参数 微信官方提供的query方式传参,若参数是普通数据类型(如[代码]Number[代码]、[代码]String[代码])时可以直接使用;但若是涉及到复杂数据类型(如[代码]Array[代码]、[代码]Object[代码])时,需要先做序列化处理,当数据较为庞大时,性能的损耗还是比较明显的。 因此,在内存上传递参数是比较便利且容易想到的办法。 利用数据字典,将[代码]页面ID[代码]作为[代码]key[代码]、传递的参数作为[代码]value[代码],写入[代码]Router[代码]的[代码]state[代码]: [代码]router.go = function(pageID, params) { // do something... router.state[pageID] = params } [代码] 在目标页面上,可以通过[代码]router.getParams()[代码]方法,获取传递的参数。 由于采用了命名路由的方式,可以使用[代码]页面ID[代码]作为[代码]key[代码],避免了使用跳转路径做[代码]key[代码]时,涉及到的绝对与相对路径问题。 条件导航 条件导航可以使用类似vue-router的导航守卫来解决问题。 由于路由的能力是微信官方提供的,因此无法像 vue-router 那样提供多类型的导航守卫,但仅有全局导航守卫也足够使用。 以下仍以“会员中心”的进入逻辑举例,并简要介绍实现思路: [图片] 其中,to和from目前是pageID,其实可以封装更多信息,以保证导航守卫可以尽可能拥有更多的信息。因此to可以理解成是即将进入的页面路由对象,而from则是当前正要离开的路由对象。 路由对象可以包含以下信息: [代码]pageID[代码]:页面ID [代码]path[代码]:页面ID对应的path [代码]params[代码]:传递的参数 [代码]query[代码]: URL的查询参数 配置信息 由前文提到的 命名路由 做法需要一个配置文件来关联[代码]页面ID[代码]与[代码]页面路径[代码]的关系。 页面的配置信息,则是使用[代码]router.config.js[代码]设置,然后通过构建工具编译转成[代码]app.json[代码]。 以下是[代码]route.config.js[代码]: [图片] 其中,跳转首页则是[代码]router.go('home')[代码];而跳转分包[代码]health[代码]的首页则是使用[代码]router.go('health.home')[代码] 通过以上的配置文件,使用构建工具转换成微信官方可识别的[代码]app.json[代码]配置: [图片] 辅助函数 在日常开发当中,经常会用到一些和路由相关的通用辅助函数,如获取当前页面,获取上个页面等。这些辅助函数都应该统一抽象封装,避免代码冗余。 [代码]router.utils = { getCurPage() { // 获取当前页面信息 let pages = getCurrentPages() let len = pages.length return pages[len - 1] }, getPrePage() {}, // 获取上个页面信息 getParams() {}, // 获取传递的参数 getPageID(path) {} // 通过path找到pageID } [代码] navigator组件 微信官方除了提供[代码]API[代码]用于导航以外,还提供了[代码]navigator[代码]组件。 另外还有[代码]functional-page-navigator[代码]是用于插件当中,不能在小程序包使用,因此本文暂且将其忽略。 由于[代码]navigator[代码]的跳转参数仍是使用[代码]path[代码],因此笔者将其进行二次封装,改造成可以通过[代码]pageID[代码]跳转: [图片] 总结 由于小程序相对比较封闭,因此在路由上能做的东西比较有限。 但路由又与许多概念有千丝万缕的关系。比如路由与文件结构关联,而文件结构又影响到分包的设计,环环相扣,影响到的地方则会越来越多。 因此,能提前看到本文提到的可能出现的问题,也许对后续的小程序开发有一定的参考意义。 另外,前文提到的很多问题,在早期开发,或者没有深入开发之前,都不会遇到。但是当你开始经历前文提到的那些问题时,往往此时的改造成本已经很大了。因此希望本文能给你带了一些启发,在早期规避这些问题,那本文的使命就达到了。
2020-06-15 - 小程序流量主、广告位类型和广告收益分析
小程序流量主、广告位类型和广告收益分析 ## 本文介绍 最近在小程序的几个微信群,经常有朋友问到以下几个问题 1、小程序怎么盈利 2、小程序流量主是什么以及怎么开通 3、小程序广告有哪些类型,哪种广告类型相对收益最大 4、 ## 小程序如何盈利 目前对个人小程序开发者而言,只有通过开通流量主,并且按照官方规范要求添加广告位,才能获取收益,当然打赏除外。 ## 什么是流量主如何开通 流量主是微信对外提供的一个服务,通过开通流量主,就可以在小程序合适的位置引入广告位,进而实现收益 登录公众号后台( https://mp.weixin.qq.com/ )在左侧菜单中,找到 推广-流量主,点击进去会看到如下截图 [图片] 小程序流量主: 1.开通条件:小程序累计独立访客(UV)1000以上,且无违规记录,即可开通流量主功能。 温馨提示:如满足条件仍无法开通,可能是数据同步问题,建议等待1-2个工作日后再试。 2.申请方法:进入微信公众平台小程序后台,点击左侧面板“流量主”,满足开通门槛的小程序开发者点击“开通”,提交财务资料,待审核通过后成功开通流量主功能,即可创建相应的广告位。 3.广告接入指引: 广告接入可查看: 微信小程序广告接入指引 在开通流量主的过程中,会绑定个人银行卡,以方便进行后续的广告收益结算,目前结算每月两次,具体官方公告可以查阅 [流量主结算周期及开票规则调整说明][2019-12-03发布] https://mp.weixin.qq.com/promotion/readtemplate?t=notice/detail_page&time=1575340587¬ice_id=634169 ## 广告类型有哪些 Banner激励式视频插屏视频广告前贴视频 以下为各广告类型,截图示例, [banner广告] [图片] [插屏广告] [图片] 视频广告 [图片] 由于插屏广告会影响用户体验,所以不建议放太多场景使用。 具体不同类型广告体验,可以扫码 [图片] 首页模块-->>插屏广告使用说明-->>视频广告关于我们-->>banner广告 ## 哪种广告类型收益相对最大 [图片] 在10月30号,将banner广告同一替换为激励式视频广告和视频广告,收益很明显从30元上升到90元、150元 可以看到视频广告相对于banner广告,对于收益增加是有用的。 下图是某小程序12月4号一天的收益数据 [图片] 12月4号一天,不同广告类型,收益分析 总收益 194.74+23.27+147.82=365.83 具体分拆来看 广告类型点击量总收益单个点击收益(元)banner1956194.740.099插屏广告6223.270.375激励式视频广告152147.820.972 通过上图我们对比分析,不难得出以下结论:激励式视频广告单个点击的收益最大、 当然我们不能通过单一维度来了解哪种收益最好,还要综合考虑,比如哪种广告对用户影响最小,毕竟不管哪种方式,广告的接入肯定会带来交互体验上的障碍, 我们必须在交互体验和广告收益这两者之间做好权衡。 ## 系统公告 激励式广告于7月31日支持30秒视频素材,广告流量将逐步放开,MP后台-广告位管理模块可支持选择6-15秒视频或6-30秒视频素材的功能,请流量主根据产品进行调整。程序视频广告已于9月4日正式全量上线,开通后即按广告曝光获得分成收入,进一步提升流量变现收益。小程序视频前贴广告组件已于8月30日正式全量上线,开通后即按广告曝光获得分成收入,进一步提升流量变现收益。## 官方文档 小程序广告组件流量主操作指引https://wximg.qq.com/wxp/pdftool/get.html?id=BJSyDkLqz&pa=14&name=miniprogramAds_supplier_manual应用规范https://wxa.wxs.qq.com/mpweb/delivery/legacy/pdftool/get.html?id=rynYA8o3f&pa=10&name=miniprogramAds_supplier_guidance小程序流量主应用规范https://wximg.qq.com/wxp/pdftool/get.html?id=rynYA8o3f&pa=10&name=miniprogramAds_supplier_guidance处罚标准https://wxa.wxs.qq.com/mpweb/delivery/legacy/pdftool/get.html?id=BkTGkbs2G&pa=1&name=miniprogramAds_supplier_regulation小程序视频广告流量主指引https://wximg.qq.com/wxp/pdftool/get.html?post_id=1317小程序视频前贴广告流量主指引https://wximg.qq.com/wxp/pdftool/get.html?post_id=1318## 总结三点 从纯收益的角度来讲,在各种广告类型中,视频广告(包含激励式视频广告、视频广告、视频前贴广告)要比banner广告要好,而且好很多从用户体验来讲,插屏广告是首次打开带插屏广告的页面强制弹出的,但是广告过后,在页面是不占空间的,这是区分与其他广告的地方,banner广告、激励式视频广告、视频广告、视频前贴广告都是在页面中占固定的空间的,这一点要小程序运营同学权衡。Banner广告是按点击,激励式视频、视频广告、插屏广告都是按照曝光来收取广告费用的,这一点非常重要,难怪我每次手工点击我的视频广告没有见流量的增加[哭脸.jpg]。[感谢 @ 仙森 补充于2019年12月9号] 虽然对个人开发者而言,我们开发小程序的目的是为了收益(当然也有为了情怀而开发),在了解如何收益的情况下,我们还是应该尽量把精力放在小程序本身的开发上面。 感谢 在此特别感谢,小程序运营讨论群的两位小伙伴,微信号中间两位已打码 1、@迭戈 (yang_##chun) 2、@风猫 (cs##26)
2020-12-25 - JavaScript常用设计模式示例与应用
JavaScript常用设计模式实例与应用 前言 1. 什么是设计模式 小时候打游戏,我们总是追求快速完美通关;上下班交通,我们总是会选择最方便便捷乘车路线。我们总是追求一件事情的最优美便捷的解决方案,也就是其所谓的最佳实践。 一个设计模式就是一个可重用的方案,可应用于在软件设计中常见的问题,在本次分享主题中,就是编写JavaScript的web应用程序中常见的问题,设计模式的另一种解释就是一个我们如何解决问题的模板。那些在许多不同但类似的情况下使用的模板。 2. 为什么要学习设计模式 JavaScript是一门以原型为基础,面向对象的,动态数据类型语言。在把函数视为第一公民,支持函数式编程的同时也不排斥面向对象的开发方式,甚至在ES6+的标准中还引入了面向对象的一些原生支持。这使得JavaScript成为一门功能强大的语言同时也导致了编程风格的碎片化,同一个功能实现的多样性。对于一些传统的、强面向对象的设计模式会有各种类型的实现,有时候会让人感觉牵强。但是这些并不妨碍我们使用JavaScript来表达设计模式的理念、所要解决的问题以及它的核心思想,这才是我们所要关注的核心。 设计模式可以让我们站在巨人的肩膀上,获得前人的经验,保证我们以优雅的方式组织我们的代码并满足我们解决问题所需要的条件。 内容 一、设计原则 设计原则是指导思想,是我们在程序设计中尽可能要遵守的准则。设计模式就是这些设计原则的一些具体实现,所要达到的目标就是高内聚低耦合。在这里我简单介绍一些六大设计原则中的单一职责原则(SPR)、开放封闭原则(OCP)、最少知识原则(LKP)。 1. 单一职责原则 单一职责原则指的是一个类应该仅有一个引起它变化的原因,也就是说一个对象只做一件事情。这样做可以让我们对对象的维护变得简单,如果一个对象拥有多种职责,职责之间相互耦合,对一个职责的修改势必会影响到其他职责。也就是说,一个对象负责的职责越多,耦合越强,对模块的修改就越危险。 2. 开放封闭原则 开放封闭原则指的是一个模块应该在对扩展开放,而对修改封闭。当需要修改增加需求的时候,应该尽量通过扩展新代码的方式,而不是修改已有的代码。因为修改已有代码会给依赖原有代码的模块带来隐患,从而需要把依赖原有代码的模块重新测试一遍,加重测试成本。 3. 最少知识原则 最少知识原则指的是一个类应该对自己需要耦合或调用的类了解得尽可能少,调用者或依赖着仅需要知道他所需要的方法即可,其他的概不关心。因为类与类之间的关系越密切,耦合性越高,当一个类发生改变时,对另一个类的影响也越大。通常我们减少对象之间的联系的方法是引入一个第三者来帮助通信,阻隔对象之间的直接通信,从而减少耦合。 二、设计模式的分类 设计模式可以被分成几个不同的种类: 创建型设计模式 创建型设计模式关注的是对象创建的机制方法,一般会把对象的创建和使用分离,从而帮助创建类的实例对象。属于这一类的设计模式主要有:构造器模式、工厂模式、单例模式、建造者模式等。 结构型设计模式 结构型设计模式关注对象组成以及不同对象之间的关系。这类模式有助于在系统的某一部分发生变化时减少对整个系统结构的改变。主要包括:代理模式、享元模式、外观模式、适配器模式、装饰者模式等。 行为型设计模式 行为型设计模式关注对象之间的通信,描述对象之间如何相互协作。主要包括:发布订阅模式,策略模式,状态模式,迭代器模式,命令模式,职责链模式,中介者模式等。 三、设计模式示例 1. 单例模式 单例模式(Singleton Pattern)属于创建型设计模式,它限制一个类只能有一个实例化对象,并提供一个访问它的全局访问点。 单例模式可能是最简单的设计模式了,虽然简单,但在实际项目开发中是很常用的一种模式。 单例模式中有几个需要知道的概念: Singleton:特定的类,也就是我们需要访问的类,访问者要拿到的就是它的实例。 Instance: 单例,是特定类的唯一实例。 getInstance: 获取单例的方法。 代码示例 [代码]var GameManager = (function () { // 单例 var instance; function init() { // 私有变量和方法 var _saveData = { name: 'glenn', level: 1 }; function _privateMethod(){ console.log( "I am private function" ); } return { // 公有变量和方法 levelUp: function(){ _saveData.level ++; }, getCurLevel: function(){ return _saveData.level; }, getName: function(){ return _saveData.name; }, publicProperty: "this is a public prop", }; }; return { // 如果存在获取此单例实例,如果不存在创建一个单例实例 getInstance: function () { if ( !instance ) { instance = init(); } return instance; } }; })(); // 使用: var singleA = GameManager.getInstance(); singleA.levelUp(); var singleB = GameManager.getInstance(); console.log( singleA.getCurLevel() === singleB.getCurLevel() ); // true [代码] 在本例中,GameManager是一个单例类,我们首先使用立即调用函数IIFE把希望隐藏的单例示例instance隐藏起来,在init方法中定义该单例类的公有和私有方法变量,然后返回一个对象,把获取单例实例的方法getInstance暴露出去。在getInstance方法中,通过JavaScript的闭包特性把单例实例instance存进闭包中,在第一次获取实例时才初始化单例,并在之后的获取操作中返回的都是这个相同的实例。 可以看到,在使用单例的代码中,我们调用了两次getInstance获取的两个对象singleA和singleB指向的是同一个对象。 源码中的单例模式 以 ElementUI 为例,ElementUI中的全屏Loading蒙层使用服务的形式调用的使用方式示意: [代码]Vue.prototype.$loading = service; this.$loading({ fullscreen: true }); [代码] 我们可以看看这个loading在ElementUI2.9.2源码中是如何实现的。 下面是为了方便观看省略了部分代码后的源码 [代码]import Vue from 'vue'; import loadingVue from './loading.vue'; const LoadingConstructor = Vue.extend(loadingVue); //... //单例 let fullscreenLoading; LoadingConstructor.prototype.originalPosition = ''; LoadingConstructor.prototype.originalOverflow = ''; LoadingConstructor.prototype.close = function() { //... }; const addStyle = (options, parent, instance) => { //... }; const Loading = (options = {}) => { //... //判断示例是否已经初始化 if (options.fullscreen && fullscreenLoading) { return fullscreenLoading; } //一系列的初始化操作 let parent = options.body ? document.body : options.target; let instance = new LoadingConstructor({ el: document.createElement('div'), data: options }); addStyle(options, parent, instance); if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') { addClass(parent, 'el-loading-parent--relative'); } if (options.fullscreen && options.lock) { addClass(parent, 'el-loading-parent--hidden'); } parent.appendChild(instance.$el); Vue.nextTick(() => { instance.visible = true; }); //把初始化出来的实例缓存下来 if (options.fullscreen) { fullscreenLoading = instance; } return instance; }; export default Loading; [代码] 这里的单例是fullscreenLoading,缓存在闭包中。当用户调用时传入的options中fullscreen为true且之前已经创建并初始化过单例的情况下直接返回之前创建的单例,否则继续执行后面的初始化操作,并把创建的单例赋值给闭包中的fullscreenLoading后返回新创建的单例。 这是一个典型的单例模式应用,通过复用之前创建的全屏加载蒙层单例,不仅减少了重复实例化过程带来的额外开销,还保证了页面中不会出现重复的全屏加载蒙层。 单例模式的应用场景 当项目中需要一个公共的状态管理时,我们可以引入单例模式来确保访问的一致性。 当项目中存在一些同一时间只会出现一个且会重复出现的对象时,我们可以引入单例模式避免重复创建对象产生的多余开销,例如项目中的弹窗,消息框提醒等。 2. 外观模式 外观模式又叫门面模式,属于结构型模式,它将子系统的一系列复杂的接口集成起来组成一个更高级别的更舒适的高层接口,从而隐藏其真正的潜在复杂性,对外提供一个一致的外观。 外观模式让外界减少对子系统的直接交互,从而降低耦合,让外界可以轻松使用子系统,其本质是封装交互,简化调用。 代码示例 [代码]var module = (function() { var _sportsman = { speed: 5, height: 10, set : function(key, val) { this[key] = val; }, run : function() { console.log('运动呀正在以'+this.speed+'米每秒的速度向前跑着。'); }, jump: function(){ console.log( "运动员往上跳了"+this.height+'米'); } }; return { facade : function( args ) { args.speed != undefined && _sportsman.set('speed', args.speed); args.height != undefined && _sportsman.set('height', args.height); args.run && _sportsman.run(); args.jump && _sportsman.jump(); } }; }()); // Outputs: 运动呀正在以10米每秒的速度向前跑着。 // 运动员往上跳了5米 module.facade( {run: true, speed: 10, jump: true, height: 5} ); [代码] 这是表达外观模式一个简单的例子。在本例中,调用module的门面方法facede会触发运动员对象_sportsman中的一系列私有方法。但在这一次,用户不需要关心运动员对象内部方法的实现,就可以让运动员动起来。 源码中的外观模式 当我们使用Jquery的$(document).ready()来给浏览器加载完成添加事件回调时,Jquery会调用源码中的私有方法: [代码]// ... bindReady: function() { //... // Mozilla, Opera and webkit nightlies currently support this event if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", jQuery.ready, false ); // If IE event model is used } else if ( document.attachEvent ) { // ensure firing before onload, // maybe late but safe also for iframes document.attachEvent( "onreadystatechange", DOMContentLoaded ); // A fallback to window.onload, that will always work window.attachEvent( "onload", jQuery.ready ); // If IE and not a frame // continually check to see if the document is ready var toplevel = false; try { toplevel = window.frameElement == null; } catch(e) {} if ( document.documentElement.doScroll && toplevel ) { doScrollCheck(); } } } // ... [代码] 由于IE9之前的IE版本浏览器以及Opera7.0之前的Opera浏览器不支持addEventListener方法,在需要适配这些浏览器的项目中,我们需要自己手动判断浏览器版版本来决定使用什么事件绑定方法以及事件。而如果使用了Jquery库中提供的这个外观方法,用户则不需要关心浏览器的兼容问题,使用一致的外观接口$(document).ready()就可以实现监听浏览器加载完成事件的功能,从而简化了使用。 除了抹平浏览器的兼容性问题之外,Jquery还有一些其他的外观模式的应用: 比如设置或获取dom结点的内容和属性时使用的text()、html()和val()方法时,Jquery判断调用方法是否有传参数来确定是设置还是获取操作。这里Jquery把设置和获取操作对外提供了同一个外观接口,使调用简化了不少。 再比如Jquery的ajax的API[代码]$.ajax(url[,settings])[代码],当我们需要设置以JSONP的形式发送请求时,只需要传入[代码]dataType: 'jsonp'[代码]设置,jquery会进行额外的操作帮我们启动JSONP流程,而不需要调用者添加额外的代码。 外观模式的适用场景 维护设计粗糙和难以理解的上古系统,或者非常复杂的一些系统时,可以为这些系统设置一个外观模块,给外界提供清晰的接口,以后的新系统只需要与外观接口交互即可。 构建多层系统时,可以使用外观模式来将系统分层,让外观接口成为每一层的入口,简化层间调用,给层间松耦。 团队协作时,可以将各自负责的模块建立合适的外观,简化其他同事的使用,节约沟通时间。 发布订阅者模式 发布 - 订阅模式(Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),属于行为型模式,它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。 发布 - 订阅模式有几个主要概念: Publisher:发布者,当消息发生时负责通知对应订阅者 Subscriber:订阅者,当消息发生时被通知的对象 SubscriberMap:以type为主键存储数组,每个数组存储所有对应type的订阅者 type: 消息类型,订阅者可以订阅的不同消息类型 subscribe:该方法可以将订阅者添加到SubscriberMap中对应的数组中 unSubscribe:该方法为SubscriberMap中删除订阅者 notify:该方法遍历通知SubscriberMap中对应type的所有订阅者 代码示例 [代码]var Publisher = (function() { var _subsMap = {} // 存储订阅者 return { /* 消息订阅 */ subscribe(type, cb) { if(_subsMap[type]){ if (!_subsMap[type].includes(cb)){ _subsMap[type].push(cb); } }else{ _subsMap[type] = [cb]; } }, /* 消息退订 */ unsubscribe(type, cb) { if(!_subsMap[type] || !_subsMap[type].includes(cb))return; var idx = _subsMap[type].indexOf(cb); _subsMap[type].splice(idx, 1); }, /* 消息发布 */ notify(type) { if (!_subsMap[type])return; var args = Array.prototype.slice.call(arguments, 1); _subsMap[type].forEach(function(cb){ cb.apply(this, args); }) } } })() Publisher.subscribe('运动鞋', function(message){console.log('111' + message)}); // 订阅运动鞋 Publisher.subscribe('运动鞋', function(message){console.log('222' + message)}); Publisher.subscribe('帆布鞋', function(message){console.log('333' + message)}); // 订阅帆布鞋 Publisher.notify('运动鞋', ' 运动鞋到货了 ~') // 打电话通知买家运动鞋消息 Publisher.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息 [代码] 这是一个发布-订阅模式的通用代码实现,Publisher就是一个发布者,这里使用了立即调用函数IIFE方式来将不希望被外界调用的_subsMap隐藏。订阅者采用回调函数的形式,在消息发布时使用JavaScript的apply、call函数使发布的消息参数可以传到订阅者回调函数中去。 源码中的发布-订阅模式 我们使用Jquery的API可以轻松实现消息的订阅、发布以及退订操作: [代码]function eventHandler() { console.log('自定义方法') } /* ---- 事件订阅 ---- */ $('#app').on('myevent', eventHandler) // 发布 $('#app').trigger('myevent') // 输出:自定义方法 /* ---- 取消订阅 ---- */ $('#app').off('myevent') $('#app').trigger('myevent') // 没有输出 [代码] 对应api源码参见: event.js 其中add方法为on接口的内部直接绑定方法,remove方法对应off接口的内部实现。 发布-订阅模式的优缺点 发布-订阅模式最大优点就是解耦: 时间上的解耦:注册事件后,订阅者不需要持续关注发布者的动态,当事件触发时,发布者会通知对应的订阅者,调用对应的回调函数。 对象间的解耦: 发布者不需要提前知道事件的订阅者有哪些,当事件发生时直接遍历对应的订阅者回调函数来通知订阅者,从而解耦了发布者和订阅者之间的联系,使它们之间互不持有。 发布-订阅模式也有一些缺点: 增加消耗:创建结构和缓存订阅者两个过程都会消耗计算和内存资源,即时订阅后没有触发过,订阅者使用会存在内存中。 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的那堆 deps/subs/watchers 们… 总结 设计模式能够让我站在巨人的肩膀上,享受其他开发者们长期以来在一些有挑战性问题上的解决方案以及优秀的架构。 对我们来讲,知道有这些设计模式是很重要的,但更重要的是应该知道怎样以及什么时候去使用它们。遵守设计原则,使用设计模式是好事,但是过犹不及,在实际项目中我们不能刻板的遵守这些设计原则以及使用设计模式,在想使用每个模式前先去了解下它的优缺点。要真正的理解模式能给你带来什么好处需要花时间去尝试,以实际情况中模式给你的程序带来的好处作为标准来选择。
2019-10-31 - 小程序读取excel表格数据,并存储到云数据库
最近一直比较忙,答应大家的小程序解析excel一直没有写出来,今天终于忙里偷闲,有机会把这篇文章写出来给大家了。 老规矩先看效果图 [图片] 效果其实很简单,就是把excel里的数据解析出来,然后存到云数据库里。说起来很简单。但是真的做起来的时候,发现其中要用到的东西还是很多的。不信。。。。 那来看下流程图 流程图 [图片] 通过流程图,我看看到我们这里使用了云函数,云存储,云数据库。 流程图主要实现下面几个步骤 1,使用wx.chooseMessageFile选择要解析的excel表格 2,通过wx.cloud.uploadFile上传excel文件到云存储 3,云存储返回一个fileid 给我们 4,定义一个excel云函数 5,把第3步返回的fileid传递给excel云函数 6,在excel云函数里解析excel,并把数据添加到云数据库。 可以看到最神秘,最重要的就是我们的excel云函数。 所以我们先把前5步实现了,后面重点讲解下我们的excel云函数。 一,选择并上传excel表格文件到云存储 这里我们使用到了云开发,使用云开发必须要先注册一个小程序,并给自己的小程序开通云开发功能。这个知识点我讲过很多遍了,还不知道怎么开通并使用云开发的同学,去翻下我前面的文章,或者看下我录的讲解视频《5小时入门小程序云开发》 1,先定义我们的页面 页面很简单,就是一个按钮如下图,点击按钮时调用chooseExcel方法,选择excel [图片] 对应的wxml代码如下 [图片] 2,编写文件选择和文件上传方法 [图片] 上图的chooseExcel就是我们的excel文件选择方法。 uploadExcel就是我们的文件上传方法,上传成功以后会返回一个fildID。我们把fildID传递给我们的jiexi方法,jiexi方法如下 3 把fildID传递给云函数 [图片] 二,解下来就是定义我们的云函数了。 1,首先我们要新建云函数 [图片] 如果你还不知道如何新建云函数,可以翻看下我之前写的文章,也可以看我录的视频《5小时入门小程序云开发》 如下图所示的excel就是我们创建的云函数 [图片] 2,安装node-xlsx依赖库 [图片] 如上图所示,右键excel,然后点击在终端中打开。 打开终端后, 输入 npm install node-xlsx 安装依赖。可以看到下图安装中的进度条 [图片] 这一步需要你电脑上安装过node.js并配置npm命令。 3,安装node-xlsx依赖库完成 [图片] 三,编写云函数 我把完整的代码贴出来给大家 [代码]const cloud = require('wx-server-sdk') cloud.init() var xlsx = require('node-xlsx'); const db = cloud.database() exports.main = async(event, context) => { let { fileID } = event //1,通过fileID下载云存储里的excel文件 const res = await cloud.downloadFile({ fileID: fileID, }) const buffer = res.fileContent const tasks = [] //用来存储所有的添加数据操作 //2,解析excel文件里的数据 var sheets = xlsx.parse(buffer); //获取到所有sheets sheets.forEach(function(sheet) { console.log(sheet['name']); for (var rowId in sheet['data']) { console.log(rowId); var row = sheet['data'][rowId]; //第几行数据 if (rowId > 0 && row) { //第一行是表格标题,所有我们要从第2行开始读 //3,把解析到的数据存到excelList数据表里 const promise = db.collection('users') .add({ data: { name: row[0], //姓名 age: row[1], //年龄 address: row[2], //地址 wechat: row[3] //wechat } }) tasks.push(promise) } } }); // 等待所有数据添加完成 let result = await Promise.all(tasks).then(res => { return res }).catch(function(err) { return err }) return result } [代码] 上面代码里注释的很清楚了,我这里就不在啰嗦了。 有几点注意的给大家说下 1,要先创建数据表 [图片] 2,有时候如果老是解析失败,可能是有的电脑需要在云函数里也要初始化云开发环境 [图片] 四,解析并上传成功 如我的表格里有下面三条数据 [图片] 点击上传按钮,并选择我们的表格文件 [图片] 上传成功的返回如下,可以看出我们添加了3条数据到数据库 [图片] 添加成功效果图如下 [图片] 到这里我们就完整的实现了小程序上传excel数据到数据库的功能了。 再来带大家看下流程图 [图片] 如果你有遇到问题,可以在底部留言,我看到后会及时解答。后面我会写更多小程序云开发实战的文章出来。也会录制本节的视频出来,敬请关注。
2019-11-12 - 一个奇葩思路实现的瀑布流双列布局
传统的瀑布流布局实现一般关键是去计算每一列的高度,从而判断下一个元素应该插入到哪一列(当然是最短的那列)。 这个奇葩思路没有任何计算,主要思路如下: 在瀑布流容器底部加入一根细线 利用微信小程序的IntersectionObserver,为每一列和细线添加监听 逐个加入要插入的item元素 根据监听相交变化结果判断下一个item应该插入哪一列(简单来说就是插入到当前不与细线相交的那一列,因为比较短) 这个思路实际上就是把计算高度换成了监听判断哪列更高,因此也不必知道每个元素的高度。 目前只能支持两列布局的情况,如果列数更多我没办法不通过计算来知道哪列最短,如果有思路或想法的童鞋欢迎交流~ 实现过程也比较简单,就分享个思路,不贴代码了(问就是懒!) 感兴趣的童鞋可以看代码片段,里面有完整的实现代码: https://developers.weixin.qq.com/s/nH5pg4mE78dG
2019-11-23 - 小程序顶部导航栏,可滑动,可动态选中放大
最近在研究小程序顶部导航栏时,学到了一个不错的导航栏,今天就来分享给大家。 老规矩,先看效果图 [图片] 可以看到我们实现了如下功能 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 - 将小程序原生异步函数promisify后,在async/await中使用
目前,小程序中支持使用async/await有三种模式: 1、不勾选es6转es5,不勾选增强编译;该模式是纯es7的async/await,需要基础库高版本。 2、勾选es6转es5,勾选增强编译;一般是因为调用了第三方的es5插件,通过增强编译支持async/await。 3、勾选es6转es5,不勾选增强编译;手工引入runtime.js支持async/await。 据最近更新情况,原生的函数已经大部分同时原生支持同步化了,不需要本方案转化了,直接加上await即可;比如wx.chooseImage、wx.showModal。。。具体有哪些,可以自己试。 如果只是wx.request的同步化,可参考: https://developers.weixin.qq.com/community/develop/article/doc/0004cc839407a069f77a416c056813 app.js代码: function promisify(api) { return (opt, ...arg) => { return new Promise((resolve, reject) => { api(Object.assign({}, opt, { success: resolve, fail: reject }), ...arg) }) } } App({ globalData: {}, chooseImage: promisify(wx.chooseImage), request: promisify(wx.request), getUserInfo: promisify(wx.getUserInfo), onLaunch: function () { }, }) 某page的index.js代码: const app = getApp() testAsync: async function(){ let res = await app.chooseImage() console.log(res) res = await app.request({url:'url',method:'POST',data:{x:0,y:1}}) console.log(res) }, [图片]
2020-10-20 - 路由的封装
小程序提供了路由功能来实现页面跳转,但是在使用的过程中我们还是发现有些不方便的地方,通过封装,我们可以实现诸如路由管理、简化api等功能。 页面的跳转存在哪些问题呢? 与接口的调用一样面临url的管理问题; 传递参数的方式不太友好,只能拼装url; 参数类型单一,只支持string。 alias 第一个问题很好解决,我们做一个集中管理,比如新建一个[代码]router/routes.js[代码]文件来实现alias: [代码]// routes.js module.exports = { // 主页 home: '/pages/index/index', // 个人中心 uc: '/pages/user_center/index', }; [代码] 然后使用的时候变成这样: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { wx.navigateTo({ url: routes.uc, }); }, }); [代码] query 第二个问题,我们先来看个例子,假如我们跳转[代码]pages/user_center/index[代码]页面的同时还要传[代码]userId[代码]过去,正常情况下是这么来操作的: [代码]const routes = require('../../router/routes.js'); Page({ onReady() { const userId = '123456'; wx.navigateTo({ url: `${routes.uc}?userId=${userId}`, }); }, }); [代码] 这样确实不好看,我能不能把参数部分单独拿出来,不用拼接到url上呢? 可以,我们试着实现一个[代码]navigateTo[代码]函数: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, query }) { const queryStr = Object.keys(query).map(k => `${k}=${query[k]}`).join('&'); wx.navigateTo({ url: `${url}?${queryStr}`, }); } Page({ onReady() { const userId = '123456'; navigateTo({ url: routes.uc, query: { userId, }, }); }, }); [代码] 嗯,这样貌似舒服一点。 参数保真 第三个问题的情况是,当我们传递的参数argument不是[代码]string[代码],而是[代码]number[代码]或者[代码]boolean[代码]时,也只能在下个页面得到一个[代码]string[代码]值: [代码]// pages/index/index.js Page({ onReady() { navigateTo({ url: routes.uc, query: { isActive: true, }, }); }, }); // pages/user_center/index.js Page({ onLoad(options) { console.log(options.isActive); // => "true" console.log(typeof options.isActive); // => "string" console.log(options.isActive === true); // => false }, }); [代码] 上面这种情况想必很多人都遇到过,而且感到很抓狂,本来就想传递一个boolean,结果不管传什么都会变成string。 有什么办法可以让数据变成字符串之后,还能还原成原来的类型? 好熟悉,这不就是json吗?我们把要传的数据转成json字符串([代码]JSON.stringify[代码]),然后在下个页面把它转回json数据([代码]JSON.parse[代码])不就好了嘛! 我们试着修改原来的[代码]navigateTo[代码]: [代码]const routes = require('../../router/routes.js'); function navigateTo({ url, data }) { const dataStr = JSON.stringify(data); wx.navigateTo({ url: `${url}?jsonStr=${dataStr}`, }); } Page({ onReady() { navigateTo({ url: routes.uc, data: { isActive: true, }, }); }, }); [代码] 这样我们在页面中接受json字符串并转换它: [代码]// pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(options.jsonStr); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这里其实隐藏了一个问题,那就是url的转义,假如json字符串中包含了类似[代码]?[代码]、[代码]&[代码]之类的符号,可能导致我们参数解析出错,所以我们要把json字符串encode一下: [代码]function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } // pages/user_center/index.js Page({ onLoad(options) { const json = JSON.parse(decodeURIComponent(options.encodedData)); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] 这样使用起来不方便,我们封装一下,新建文件[代码]router/index.js[代码]: [代码]const routes = require('./routes.js'); function navigateTo({ url, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { routes, navigateTo, extract, }; [代码] 页面中我们这样来使用: [代码]const router = require('../../router/index.js'); // page home Page({ onLoad(options) { router.navigateTo({ url: router.routes.uc, data: { isActive: true, }, }); }, }); // page uc Page({ onLoad(options) { const json = router.extract(options); console.log(json.isActive); // => true console.log(typeof json.isActive); // => "boolean" console.log(json.isActive === true); // => true }, }); [代码] route name 这样貌似还不错,但是[代码]router.navigateTo[代码]不太好记,[代码]router.routes.uc[代码]有点冗长,我们考虑把[代码]navigateTo[代码]换成简单的[代码]push[代码],至于路由,我们可以使用[代码]name[代码]的方式来替换原来[代码]url[代码]参数: [代码]const routes = require('./routes.js'); function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const url = routes[name]; wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } function extract(options) { return JSON.parse(decodeURIComponent(options.encodedData)); } module.exports = { push, extract, }; [代码] 在页面中使用: [代码]const router = require('../../router/index.js'); Page({ onLoad(options) { router.push({ name: 'uc', data: { isActive: true, }, }); }, }); [代码] navigateTo or switchTab 页面跳转除了navigateTo之外还有switchTab,我们是不是可以把这个差异抹掉?答案是肯定的,如果我们在配置routes的时候就已经指定是普通页面还是tab页面,那么程序完全可以切换到对应的跳转方式。 我们修改一下[代码]router/routes.js[代码],假设home是一个tab页面: [代码]module.exports = { // 主页 home: { type: 'tab', path: '/pages/index/index', }, uc: { path: '/pages/a/index', }, }; [代码] 然后修改[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; if (route.type === 'tab') { wx.switchTab({ url: `${route.path}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${route.path}?encodedData=${dataStr}`, }); } [代码] 搞定,这样我们一个[代码]router.push[代码]就能自动切换两种跳转方式了,而且之后一旦页面类型有变动,我们也只需要修改[代码]route[代码]的定义就可以了。 直接寻址 alias用着很不错,但是有一点挺麻烦得就是每新建一个页面都要写一个alias,即使没有别名的需要,我们是不是可以处理一下,如果在alias没命中,那就直接把name转化成url?这也是阔以的。 [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : name; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 在页面中使用: [代码]Page({ onLoad(options) { router.push({ name: 'pages/user_center/a/index', data: { isActive: true, }, }); }, }); [代码] 注意,为了方便维护,我们规定了每个页面都必须存放在一个特定的文件夹,一个文件夹的当前路径下只能存在一个index页面,比如[代码]pages/index[代码]下面会存放[代码]pages/index/index.js[代码]、[代码]pages/index/index.wxml[代码]、[代码]pages/index/index.wxss[代码]、[代码]pages/index/index.json[代码],这时候你就不能继续在这个文件夹根路径存放另外一个页面,而必须是新建一个文件夹来存放,比如[代码]pages/index/pageB/index.js[代码]、[代码]pages/index/pageB/index.wxml[代码]、[代码]pages/index/pageB/index.wxss[代码]、[代码]pages/index/pageB/index.json[代码]。 这样是能实现功能,但是这个name怎么看都跟alias风格差太多,我们试着定义一套转化规则,让直接寻址的name与alias风格统一一些,[代码]pages[代码]和[代码]index[代码]其实我们可以省略掉,[代码]/[代码]我们可以用[代码].[代码]来替换,那么原来的name就变成了[代码]user_center.a[代码]: [代码]Page({ onLoad(options) { router.push({ name: 'user_center.a', data: { isActive: true, }, }); }, }); [代码] 我们再来改进[代码]router/index.js[代码]中[代码]push[代码]的实现: [代码]function push({ name, data }) { const dataStr = encodeURIComponent(JSON.stringify(data)); const route = routes[name]; const url = route ? route.path : `pages/${name.replace(/\./g, '/')}/index`; if (route.type === 'tab') { wx.switchTab({ url: `${url}`, // 注意tab页面是不支持传参的 }); return; } wx.navigateTo({ url: `${url}?encodedData=${dataStr}`, }); } [代码] 这样一来,由于支持直接寻址,跳转home和uc还可以写成这样: [代码]router.push({ name: 'index', // => /pages/index/index }); router.push({ name: 'user_center', // => /pages/user_center/index }); [代码] 这样一来,除了一些tab页面以及特定的路由需要写alias之外,我们也不需要新增一个页面就写一条alias这么麻烦了。 其他 除了上面介绍的navigateTo和switchTab外,其实还有[代码]wx.redirectTo[代码]、[代码]wx.navigateBack[代码]以及[代码]wx.reLaunch[代码]等,我们也可以做一层封装,过程雷同,所以我们就不再一个个介绍,这里贴一下最终简化后的api以及原生api的映射关系: [代码]router.push => wx.navigateTo router.replace => wx.redirectTo router.pop => wx.navigateBack router.relaunch => wx.reLaunch [代码] 最终实现已经在发布在github上,感兴趣的朋友可以移步了解:mp-router。
2019-04-26 - [填坑手册]小程序目录结构和component组件使用心得
[图片] 小程序目录结构 关于小程序的目录结构,可以说一开始大家都有各自的开发习惯和命名规则,但一旦项目变得复杂庞大的时候,你就发现管理起来和后期维护变得很麻烦,如果是 协同开发 的话,更容易出现 “互坑” 的情况。 智库君在一年多的小程序开发中也跳过不少的坑,总结了一套还算好维护的目录结构跟大家分享(仅供参考,觉得好拿去,觉得不好欢迎提出意见),以下是实战项目中的结构示例: [代码]├─ app.js --- 小程序加载时优先加载的入口JS ├─ app.json ---入口文件和公共配置 ├─ app.wxss ---公共样式表 ├─ project.config.json ---小程序全局配置文件 ├─ sitemap.json ---允许微信索引文件 │ ├─cloud-functions ---云函数 │ └─setCrypto ---数据加密模块,用户加密一些数据 │ index.js │ package.json │ ... │ ... │ ├─components ---小程序自定义组件 │ ├─plugins --- (重点)可独立运行的大型模块,可以打包成plugins │ │ ├─comment ---评论模块 │ │ │ │ index.js │ │ │ │ index.json │ │ │ │ index.wxml │ │ │ │ index.wxss │ │ │ │ services.js ---(重点)用来处理和清洗数据的service.js,配套模板和插件 │ │ │ │ │ │ │ └─submit ---评论模块子模块:提交评论 │ │ │ index.js │ │ │ index.json │ │ │ index.wxml │ │ │ index.wxss │ │ │ │ │ └─canvasPoster ---canvas海报生成模块 │ │ index.js │ │ index.json │ │ index.wxml │ │ index.wxss │ │ services.js ---(重点)用来处理和清洗数据的service.js,配套模板和插件 │ │ ... │ │ ... │ │ │ └─templates ---(重点)模板,通过外部传参的容器,不做过多的数据处理 │ │ │ ├─slideshow ---滚屏切换模板 │ │ index.js │ │ index.json │ │ index.wxml │ │ index.wxss │ │ service.js ---(重点)用来处理和清洗数据的service.js,配套模板和插件 │ │ │ └─works ---作品模板 │ │ index.js │ │ index.json │ │ index.wxml │ │ index.wxss │ │ service.js │ │ │ ├─articlePlugin ---作品模板中的文章类型 │ │ index.js │ │ index.json │ │ index.wxml │ │ index.wxss │ │ │ ├─galleryPlugin ---作品模板中的九宫格类型 │ │ index.js │ │ index.json │ │ index.wxml │ │ index.wxss │ │ │ └─videoPlugin ---作品模板中的视频类型 │ index.js │ index.json │ index.wxml │ index.wxss │ ... │ ... │ ├─config ---自定义配置文件 │ config.js ---存放基础配置 │ constants.js ---存储常量 │ weui.wxss ---第三方文件wxss,js等 │ ... │ ... │ ├─pages ---小程序页面 │ ├─user ---用户页面 │ │ index.js │ │ index.json │ │ index.wxml │ │ index.wxss │ ├─news ---新闻页面 │ │ index.js │ │ index.json │ │ index.wxml │ │ index.wxss │ │ │ └─home ---首页 │ index.js │ index.json │ index.wxml │ index.wxss │ ... │ ... │ ├─request ---https请求管理(根据switch tab分类会比较好) │ common.js ---一些公共请求获取,如兑换openId,unionId 等 │ news.js │ uri.js --- (重点)总的URI请求管理,方便切换和配置DEV,QA,PROD环境 │ user.js │ ... │ ... │ └─utils ---功能组件 logger.js ---日志管理 util.js ---公共小组件库 ... ... [代码] 例如微信自己的wepy的官方文档,现在也添加了目录结构说明: [图片] 为什么一定要写这个目录结构呢? 不知道大家有没有发现,在以往的老项目交接和多人协同开发中,容易遇到别人写的模块,变量命名不准确,或者资料缺损,一次十来个方法/组件间的互相调用,直接把接(盘)手的人整懵逼了,所以智库君觉得,无论是独立开发,还是协同开发,留一份完整的目录说明文档是很有必要的,勿坑 他人 OR 未来的自己~~~ component使用心得 大家在开发过程中肯定会去看官方文档,但不可能全看完才开始写代码,大多数情况都是用到了再看,本人也是,所以下面抽一些开发中遇到的重点来讲: 一、引用组件模板页面的自定义 组件模板的写法与页面模板相同。组件模板与组件数据结合后生成的节点树,将被插入到组件的引用位置上。 在组件模板中可以提供一个 <slot> 节点,用于承载组件引用时提供的子节点。 [代码]<!-- 组件模板 --> <view class="wrapper"> <view>这里是组件的内部节点</view> <slot></slot> </view> [代码] [代码]<!-- page页/父页面引用组件的页面模板 --> <view> <component-tag-name> <!-- 这部分内容将被放置在组件 <slot> 的位置上 --> <view>这里是插入到组件slot中的内容</view> <view>在加载组件的页面里自定义内容,将没有复用性的内容写在这里</view> </component-tag-name> </view> [代码] 页面自定义部分默认是加载在组件上方。 为什么要在引用组件的页面添加这些内容呢? 因为组件其中一个重要的特点是复用性,但是有的时候可能要根据不同场景做一些自定义,如果在组件中写大量的场景/逻辑判断,会增加组件的冗余,而且这些方法只是被复用一次的话,完全可以不写到组件里。 二、“一键换肤”根据不同场景给组件引入外部样式 [代码]<!-- 外部引用组件的页面传入样式 --> <WorkComponent extra-class="style1" j-data="{{workData}}"></WorkComponent> [代码] [代码]//组件中js Component({ /** * 引入外部样式,可传多个class */ externalClasses: ['extra-class','extra-class2'], }) [代码] extra-class 从外部引入父级css,可用根据不同场景配置不同的样式方案,这样使得组件自定义能力更强。 三、数据清洗与容错 [代码]//service.js 思路示例 module.exports = { /** * 功能:处理作者列表 * @param list * @returns {Array} */ authorList: function (list = []) { let result = []; list.forEach(item => { result.push({ guid: item.recommend_obj_id || '', type: item.recommend_type || '', logo: (item.theme_pic || '').trim() || '', title: item.title || '' }); }); return result; } }; [代码] 如果外部传入的数据要分别导入多个组件中,可以在组件中建立一个对应的service.js,有2个作用: 清洗数据,避免setData()的时有过多的脏数据 错误数据的兼容,添加数据缺省值,增加代码健壮性 四、canvas在component组件中无法选中的问题 [代码] //这里只需要在后面 添加this对象 let ctx = wx.createCanvasContext('myCanvas', this); [代码] 其他一些默认组件,遇到类似的问题,一般只要引用时传入this对象即可解决。 五、组件之间的通讯 在实际生产环境中,我们常常需要控制各个组件之间的互相通信/传参,下面介绍下具体的用法: WXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(自基础库版本 2.0.9 开始,还可以在数据中包含函数)。具体在 组件模板和样式 章节中介绍。 事件:用于子组件向父组件传递数据,可以传递任意数据。 如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。 设置监听事件: [代码]<!-- wxml 中 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 --> <component-tag-name bindmyevent="setMyEvent" /> <!-- 或者可以写成 --> <component-tag-name bind:myevent="setMyEvent" /> [代码] [代码]// index.js 父页面中 Page({ setMyEvent: function(e){ let self = this; if (e.detail) { // 自定义组件触发事件时提供的detail对象 switch (e.detail) { case "hidden": //隐藏 悬浮框上的评论 this.setData({ isFixCommentShow: false }); break; case "fixRefresh": //刷新悬浮框 this.setData({ fixRefresh: true }); break; case "commentRefresh": //刷新评论 this.setData({ commentRefresh: Math.random() }); break; case "createPoster": //生成海报组件 self.setPosterSave(); break; } } } }) [代码] 父页面引用子组件,子组件发送的信息,可以通过bind的方法监听到,来获取到具体的传参值。 触发事件 自定义组件触发事件时,需要使用 triggerEvent方法,指定事件名、detail对象和事件选项: [代码]<!-- 页面 page.wxml --> <another-component bindcustomevent="pageEventListener1"> <my-component bindcustomevent="pageEventListener2"></my-component> </another-component> <!-- 组件 another-component.wxml --> <view bindcustomevent="anotherEventListener"> <slot /> </view> <!-- 组件 my-component.wxml --> <view bindcustomevent="myEventListener"> <slot /> </view> [代码] [代码]//组件中js Component({ properties: {}, methods: { onTap: function(){ var myEventDetail = {} // detail对象,提供给事件监听函数 var myEventOption = {} // 触发事件的选项 this.triggerEvent('myevent', myEventDetail, myEventOption) //myEventOption的一些配置: this.triggerEvent('customevent', {}, { bubbles: true }) // 会依次触发 pageEventListener2 、 pageEventListener1 this.triggerEvent('customevent', {}, { bubbles: true, composed: true }) // 会依次触发 pageEventListener2 、 anotherEventListener 、 pageEventListener1 } } }); [代码] myEventOption 的配置: bubbles(Boolean):事件是否冒泡 composed(Boolean):事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部 capturePhase(Boolean):事件是否拥有捕获阶段 需要强调一点:建议大家不要在组件上bind太多的监听,一方面以后管理起来会比较麻烦,另一方面首次加载如果调用过多方法会引起数据渲染的卡顿。 Component官方文档: https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/ 往期回顾: [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - 小程序跳转页面加载优化
适应场景: 小程序页面跳转redirect/navigate/其它方式 分析: 从用户触发跳转行为到下一个页面onload生命周期函数内时间差会有500ms左右,如果在页面跳转之后进行onload函数内才开始去加载页面数据,那么这500ms左右的时间就浪费了。 改进: 在页面触发跳转行为的处理函数里结合promise预先加载下个页面的数据,并将promise对象缓存,此时页面跳转和加载数据同时进行,到了目标页面再取出缓存的promise对象进行判断和取数据操作。 效果: 跳转页面加载速度提高了600ms。 示例: 代码结构 [图片] pageManager.js [代码]// 写在utils里的公用方法 const pageList = {}; module.exports = { putData:function(pageName, data){ pageList[pageName] = data; }, getData:function(pageName){ return pageList[pageName]; } } [代码] util.js [代码]const myPromise = fn => obj => { return new Promise((resolve, reject) => { obj.complete = obj.success = (res) => { resolve(res); } obj.fail = (err) => { reject(err); } fn(obj); }) } module.exports = { myPromise : myPromise } [代码] index.js [代码]// 跳转页面 const {myPromise} = require('../../utils/util'); const pageManager = require('../../utils/pageManager'); page({ data: { }, onLoad:function(){ }, gotoPageA:function(){ const PromisePageA = myPromise(wx.request)({ url : '' }).then((res)=>{ return res.data; }) pageManager.putData('pageA',promisePageA); wx.navigateTo({ url: 'pages/pageA/pageA' }) } }) [代码] pageA.js [代码]// 被跳转页面 const util = require('../../utils/util.js'); const pageManager = require('../../utils/pageManager'); const {myPromise} = require('../../utils/util'); Page({ data:{ logs:[] }, onLoad: function(){ const promisePageA = pageManager.getData('pageA'); if(promisePageA){ const resData = promisePageA.then( function(data){ }, function(){ console.log("err"); } ) } } }) [代码]
2019-10-31 - 对 “微信开放社区” 的一些思考和建议
[图片] 上周参加了“上海微信开放社区”的沙龙活动,看到现场大家回答问题都很踊跃,回去自己也思考了下,先看下10月24日(1024如此重要的节日发布。。。emmm。。。)官方公布的内容: 《社区突出贡献者激励计划》。 首先有点必须值得肯定,官方正在努力做出改变,并且把平台建设的更好,基于“人人都是产品经理”的思想,我这里也给到一些整理后的建议。 免责申明:由于可以拿到的关键数据有限,作为一个数据控,略感无奈,很多时候做决定或给建议只能通过个人主观认知来判断,但本人会尽力秉承理性和客观来给出一些看法。 1、奖励机制 1.1、“全周期”评价,积分系统 积分是一个累加的概念,它表明你对这个平台付出的总额。个人觉得这个是重要的奖励机制,为什么呢? 打个比方,现在的奖励机制是按照月度来的,展示的是单月的贡献,那么就会有个问题: A同学:5月份非常积极,为平台贡献了1000,获得月度突出贡献奖,但后面就再没有贡献了,一年贡献总额为1000 B同学:每个月都有贡献,每月100点,坚持了一年,那么总额就是1200 可以看到,其实是B同学对整个平台总体贡献大,但按照现在的机制只有A同学能获得奖励~ 这样或多或少都会打击B同学这类贡献者,他们或许因为工作满或者没有那么多连续的大量时间,平台就忽略他们的贡献~~~ 结论:建立全周期评价体系,加入积分机制、年度,季度贡献者的评选。 1.2、贡献者评价的一些基础数据 用户点赞 用户收藏 评论 浏览次数&分享 投币:类型于B站的投币机制,可以每个月给用户5个币,投完为止,限量才能突出用户选择的谨慎性和投币对象的价值。 投诉:这类反馈要更加谨慎和细致 关于是否需要赞对应的“踩” ? 回答问题类:一定需要,因为有这种情况,一个回答能解决当前问题,但可能引起“次生灾害”导致其他的问题,但由于先前点赞数过高,一直排名靠前,可能出现误导~ 文章类:不建议有“踩”,因为写文章的成本较高,更多的是提供一个全流程解决方案,而非简单的对错判定,当然也不需要所有人满意~ 结论:这里 每个数据的权重 是最为关键的,官方到时候是否公布,也是要细致考虑,个人认为:投币 = 投诉 > 收藏 > 点赞 = 评论 > 浏览次数 1.3、最终奖励 建立周边商城,利用获得的积分或者虚拟货币兑换各类物品(开放社区都是实名制的,不用担心刷子薅羊毛) 1.3.1、虚拟奖励 特有头像、头环 专属徽章,如“九月社区突出贡献个人开发者” 加快小程序审核时间 发表的文章/回答的问题 能优先被“置顶、推荐、加精”等 云函数、云服务等器免费使用额度 兑换一些腾讯系的虚拟物品 个人小程序开发,给予更大的权限,比如个人小程序可以使用web-view 优先参与“内测”的机会,观看付费教学视频等~ 核心:可以在回答问题和发文章一眼看出此类“专业用户”,要有一定区分度 1.3.2、实物奖励 特色周边,比如企鹅毛绒玩具,手机壳等 徽章、勋章 里程碑奖品:类似B站,10W粉丝会给一个银色奖杯牌,100w会给一个金色的 可以优先参加一些分享会或者现场培训 google的做法,参与互动就有奖励,如图: [图片] 实物奖励核心:一定要突出实用性并且可反复展示,人一定是会有虚荣心的,但这不一定是负面的,而可能成为持续性的鼓励,比如每当你看到桌面上的“奖杯”。。。想起。。。~~~ 结论:专家级用户一定是少数,但他们反而是最重要的,他们提供了正确率和解决问题效率!奖励的核心是让真正的贡献者获得“荣耀感”,建立“忠诚度”,让他们能为建立强大的微信开放社区持续性投入。 2、社区交互和界面优化 2.1、页面交互优化 2.1.1、将“我的关注”修改成“我的” [图片] “我的”我的模块可以加入三个标签: 我的提问(默认第一个显示),根据观察,这个平台上用户更关心自己的问题是否得到解决,围绕“我的问题”是第一需求。 我的关注:保持不变 我的状态/积分:包括评论,点赞等信息,现在是最上面,且单独打开窗口,不太友好,切换起来也不方便~ 建议去掉头部的“全部、文章、问答”这一栏,改成“推荐,我的,疑问/BUG”,将文章和问答加入到筛选Label中。 2.1.2、搜索和筛选排序优化 [图片] 添加其他几个筛选选项,比如点赞数最高的回答,收藏数最高的文章等等 2.1.3、个人提醒小优化 比如这种情况,有十几个未读信息,但是点击进去后,需要一个个点开,才能去掉红色的“未读状态”,逼死强迫症啊~ 建议:点进去,就直接把16个未读状态归零,但是可以在“详情页面”中看到标记~ [图片] 2.1.4 小程序核心 小程序发布文章肯定不方便,个人觉得更多的是简单回答问题和评论,所以小程序端的核心应该是“资讯的接收和BUG修复的提醒”,这里小程序的订阅功能会变得更加强大,比如,某某BUG官方已修复的订阅推送~~~ 2.2、奖励栏优化+ 荣誉墙页面 [图片] 个人贡献者只有10个能入选太少了!!! 建议增加至20/50个奖励名额,让更多的人可以上榜,一定不会是件坏事,这里可以设置前1-3名特殊的ICON,前4-10名一个标签,首页隐藏其他11-50名,但是有一个荣誉墙页面,里面可以查到以往所有的“优秀贡献者”~~~ 荣誉墙未来可以添加其他排行榜,作为小程序开发者的一种荣耀~ 2.3、BUG的整理、追踪和反馈 从目前看来用户最关心的是: BUG的处理方法和是否解决!!!!!! 同类问题的归类合并,避免反复提问,消耗回答者精力 BUG解决追踪和及时反馈,这里建议添加“关注或者订阅”功能,解决后能及时反馈给用户。(这里对应小程序的优化) [图片] 顶部建议添加 “疑问/BUG” 选项,为用户提供的处理方法(包括绕过BUG或者hack的方法)和分类。 侧面的“已知问题与需求反馈”,个人觉得很棒,没有问题~ [图片] 3、提问模板+降低沟通成本 3.1、增加提供代码片段的Tips 我之前提了一个问题,就遇到一个情况,官方回答需要我提供 代码片段,当时是一脸懵逼,好在看完教程,知道如何使用了,从此对这个功能喜欢的不得了。 [图片] 个人认为官方给到的代码片段对于用户反馈和解决问题非常有用,所以在官方模板中建议加上代码片段使用方法的tips 小问号ICON~ 3.2、引导/教会 提问者 很多时候“专家用户”想回答一些提问者的问题,但是发现没看明白问题的意思或者误解了问题的内容,这里我认为官方有必要引导提问者更好的描述问题。 一个好的提问者,会让回答者更加省心,所以官方如果能提供一个提问的demo,必然大大降低沟通成本,减少用户抱怨。 [图片] 4、“微信学院”的传道受业解惑 4.1、官方视频指导 首先,需要肯定一点,微信学院这个项目感觉很棒,可以帮助到很多小程序的参与者。 说下问题,现在的界面部分略感凌乱,不同岗位的人进来找于自己相关的教程比较吃力,建议添加分类功能Tab选项~ [图片] 现有的行业分类可以保持(根据你们官方自己的计划来) 功能性分类:产品,开发,运营来区分现在不同种类的视频,因为比如PM过来,他们完全对技术这块视频不敢兴趣,目前就会产生干扰+阅读成本。 建立专辑和连续性:官方的教学视频做的更加“线性”,比如一期二期这种,有规律且连续,类似于专辑/系列视频,这样用户学起来更加持续且扎实,官方教学的效果也更好 增加一些实战类视频(包括技术和运营),目前的视频更多讲的是概念,有点虚,实战较少,比如如何通过云函数生成一个二维码,其实很多人都在问这类问题,这个可以帮助中小企业减低开发成本且更好的社交裂变,对于平台方是双赢的~ 4.2、“文章和问答”的思考 个人觉得文章是传道受业,问答更多是解惑,哪个更重要呢? 短期肯定是问答,长期的话文章的价值更大,文章凸显的是持续性的知识输出,给开发者提供一个完整的学习路径,比起做到哪里遇到问题再问来说,文章的引导意义更加彻底。 个人觉得官方未来可以考虑鼓励更多的用户去写一些“项目实战和技术贴”,做持续性的内容输出和知识科普~ 5、服务商/插件 5.1、插件接入标准模板 现阶段第三方开发的插件,接入方式和教程参差不齐,有的甚至需要二次开发,但是往往说明不清,给使用者造成很大的困惑。反正本人之前想用一个插件,搜了下各家文档写得各种风骚外加“脑补”,完全被整懵逼~ [图片] 建议官方提供一个标准化的接入模板或者教程,最好带示例,而不是开发者自己想着填,这样可以帮助使用者更方便的接入和使用。 5.2、收不收费的问题 5.2.1、收费 提供“人工服务”的一定要收费,以保持服务的品质 持续性的支持,偏向于收费 需要占用其他资源的,比如地图,服务器,AI算力等 收费的初衷一定是为了提供更好的服务,但希望官方提供一个标准,避免乱定价,或者一锤子买卖坑人的问题。 5.2.2、免费 一次性使用的组件类,不需要其他资源支持,纯粹的代码提供 开源项目 插件,这个有争议,但是插件可能很多情况下,没有“售后”,恐怕出现收费后,撒手不管的情况,容易造成纠纷~ 5.2.3、微信官方支持/支付费用 那些长期维护的项目,或者优秀的开源框架/插件,为了鼓励开发者坚持优化和改进,官方可以提供一笔基金支持日常维护迭代,类似 apache基金会、Nginx社区、React/VUE项目这种,由机构/大公司/基金会维持的项目~ 5.3、建立评价体系 简单的评分和赞踩系统 提供试用/DEMO,上来试用,可以避免买了后发现和自己的需求不匹配的问题 退货/钱功能 谁都不想上来就做小白鼠嘛,花了钱还踩坑~~~ 所以必须建立一个有效的评价体系,也能督促和规范服务商/插件开发者。 以上就是本人的一些拙见,写的比较仓促,如有内容错误和阅读疑惑欢迎各位同行留言评论,觉得不错也可点个赞,让更多人看见,感谢 ^_^ ~
2019-10-29 - 微信小程序开发UI组件样式库推荐
做为微信开发的程序员来说,写一些WXSS页面样式最头疼了。往往做出来的界面虽然功能一个不少,但显示的效果简陋而达不到用户满意。 我们推荐了以下几款微信小程序的组件库,可以让你不用懂WXSS也不用设计感,照样能做出很漂亮的小程序。 一、WeUIWEUI是一套基于样式库weui-wxss开发的小程序扩展组件库,同微信原生视觉体验一致的UI组件库,由微信官方设计团队和小程序团队为微信小程序量身设计,令用户的使用感知更加统一。 [图片] [图片] 官方组件库能够满足基础的界面需求,但是,如果你想要更加饱满的视觉,更加活泼的动效,恐怕 WeUI 就满足不了你的需要了。 GitHub 地址:https://github.com/Tencent/weui 二、ColorUI 组件库ColorUI 是一款高颜值组件库,侧重于视觉交互。比起 WeUI 的低调克制,ColorUI 色彩鲜亮,样式繁多。除了拥有非常丰富的原生组件的自定义样式,它还提供一些常见的页面元素,比如时间轴、步骤条、聊天页、模态窗口等等。 [图片] 这些页面元素通常应用在哪些场景下呢? 如果你想做一款诸如日记类、记账类、博客类、Vlog 类的小程序,这时就需要用到「时间轴」。 如果你想做一款涉及流程的小程序,比如物流跟踪,工作审批等,「步骤条」就可以派上用场了。 如果你想做一款社交类小程序,那么,当然少不得要用到「聊天」的界面。 而「模态窗口」则可以应用于各类小程序中出现弹框、侧边栏的地方。 [图片] 此外,ColorUI 还引入了插件扩展,也就是更为复杂的组件。目前已有的扩展包括索引列表、微动画、全屏抽屉以及垂直导航。引用这几项扩展,只需编写少量代码,就能实现较炫的视觉交互,进一步简化了开发工作。 [图片] 前面我们已经提到,ColorUI 是侧重于视觉交互的组件库,这方面的表现,还在于它为用户提供了色彩的搭配方案。打开「背景」,可以看到深色、淡色、渐变等多种配色。 [图片] ColorUI 还有许多值得推荐的地方。多样化的示例就是其中之一,它详尽地向用户展示了各种情况下,开发者可能需要编写的样式。 比如,打开「头像」,就会看到被一一列举的圆形头像、圆角矩形头像、各种尺寸头像、默认头像、文字头像、彩色头像、头像组、贴标签头像等等。一个这么简单的组件,也可以有许多种不同的呈现方式。 [图片] 又比如,打开「列表」,不仅可以看到宫格列表、菜单列表、消息列表、左滑列表等基本的样式,还可以设置一些可选项,像边框、箭头等,在细节处也有多种可选样式。 [图片] ColorUI 给大家提供了高度自定义的组件,一些比较麻烦的样式,开发者只需调用其组件就能得以实现。不过,ColorUI 也不是万能的,比如,它尚未涉及购物类小程序所需的组件。 GitHub 地址:https://github.com/weilanwl/ColorUI [图片] 三、Vant 组件库演示Vant 是由有赞发布的,轻量的小程序 UI 组件库。如果你想制作一款电商、餐饮、外卖平台、票务预订等购物类小程序,选用 Vant 是较为合适的。为什么这么说呢? [图片] 首先,我们来看「业务组件」这一块。可以看到,「商品卡片」与「提交订单栏」两个组件可以构成一个基本的「购物车」页面;而「商品卡片」与「商品导航」二者又可以组成一个简单的商店页面。 [图片] 我们再看看其他琐碎的组件,比如「表单组件」中的「评分」、「搜索」、「步进器」,都属于购物类小程序需要用到的组件。 [图片] 「导航组件」中的「徽章」与「展示组件」中的「分类选择」,都可以用于商品品类的选择切换。 [图片] 「展示组件」中的「折叠面板」与「面板」可以用作详细介绍商品的组件,「步骤条」则可以用于显示物流跟踪信息。 [图片] 使用 Vant 组件库,除了可以用常用的 Toast 方法,向用户弹出提醒消息,还可以引用「反馈组件」中的「消息通知」以及「展示组件」中的「通告栏」,向用户输出通知信息。 [图片] 除了以上可用于购物类小程序的组件,Vant 组件库当然还有那些比较通用基本元素、弹出层、Transition 动画等。值得一提的是,Vant 还支持自定义 Actionsheet,在「反馈组件」的「上拉菜单」中,有三种不同的自定义 Actionsheet。 [图片] Vant 对开发者非常友好,文档可以说是事无巨细了,而且在文档右侧,还可以预览样式哦。 开发文档:https://youzan.github.io/vant-weapp/#/intro GitHub 地址:https://github.com/youzan/vant-weapp [图片] 四、iViewUIiViewUI 是由 TalkingData 发布的组件库。作为一款好用的组件库,布局、面板、列表、表单、顶部导航栏、底部导航栏等组件当然必不可少,那么 iViewUI 除了具备这些标配的组件,还有哪些亮点呢? [图片] 在「导航」分类下,「分页」、「索引选择器」以及「吸顶容器」都是比较实用的组件。 其中,「索引选择器」与 ColorUI 中的「索引列表」是同类组件,不同的是,ColorUI 的「索引列表」中每一项可以包含图片、名字与描述,且支持搜索,而 iViewUI 的「索引选择器」中每一项只包含名字,且不支持搜索。 而「吸顶容器」在上文中尚未提及,这一组件适合用于分级长列表的显示。 [图片] 在「视图」分类下的「倒计时」一项中,提供了多种倒计时的显示格式。 [图片] iViewUI 同样有详细的文档,但是不支持网页预览,只能打开小程序预览。 开发文档:https://weapp.iviewui.com/docs/guide/start GitHub 地址:https://github.com/TalkingData/iview-weapp [图片] 五、MinUI 组件库MinUI 是由蘑菇街发布的组件库。与其他组件库不同的是,MinUI 更注重一些细节的处理。 [图片] 调用「基础元件」中的「文本截断」,可以控制长文本的显示行数,文本超长的用省略号结尾。「页底提示」可以用在上拉加载中的过程中。而「价格」则提供了各种样式的价格及货币符号。 [图片] 「功能组件」的「异常流展示」为开发者提供了各种异常状态下,向用户展示的界面。「遮罩层」则提供了各种效果的遮罩层,及其显示、隐藏方式。 [图片] 相比其他组件库,MinUI 将各种组件拆分得更细,真正使用时,需要开发者更多的对各个组件进行再次结合,但也因此 MinUI 显得更加通用。 开发文档:https://meili.github.io/min/docs/minui/index.html#README GitHub 地址:https://github.com/meili/min-cli [图片] 六、TaroUITaroUI 是由京东·凹凸实验室发布的多端 UI 组件库。这套组件库,可以在 H5、微信小程序、支付宝小程序、百度小程序多端适配运行。TaroUI 的整体风格简约、清新、统一,适合工具、读书、资讯、教育、商务等类型的小程序。 [图片] 除了拥有上文所提及的组件之外,TaroUI 还有几个特别的组件。在「表单」中有一项「范围选择器」,可以通过滑动条指定数值范围。在「高阶组件」中,可以显示「日历」,并且支持多种日期选择样式。 [图片] TaroUI 同样拥有健全的开发文档,也支持在网页中预览手机效果。 开发文档:https://taro-ui.aotu.io/#/docs/introduction GitHub 地址:https://github.com/NervJS/taro-ui [图片] 七、WuxUI这套组件库所包含的组件最为丰富。不仅我们前文提到的各类组件都可以在 Wux 中找到,而且还有进度环、骨架屏、筛选栏、数字键盘、结果页等实用工具类组件。如果你想开发一款工具类小程序,Wux 是个不错的选择。 [图片] 开发文档:https://wux-weapp.github.io/wux-weapp-docs/#/introduce GitHub 地址:https://github.com/wux-weapp/wux-weapp/ [图片] 这 7 款 UI 组件库各有所长,适合不同的小程序类型,Vant 适合电商类的,TaroUI 与 Wux 适合工具类的,而蘑菇街的 MinUI 当然更适合社区类的了。 大家可以根据自己的需求来选择相应的UI组件库来创建制作微信小程序。有微信小程序开发需求也可以联系云梁网络(https://www.yunliangwang.com)
2019-10-29 - 自定义下拉选择组件Select
背景 项目需求要实现类似html标签 <Select/>效果,所以写了一个类似效果的自定义组件。 效果图 [图片] wxml [代码]<view class="view-item"> <text class='item-key'>{{title}}<text style="color:red" wx:if="{{isRequired}}">*</text></text> <view class="view-select-container"> <view class='select-value' bindtap="selectToggle"> <input value="{{value}}" name='{{name}}' disabled="{{true}}" /> <image class='img-arrow' style="width:40rpx;height:40rpx" src='/images/drop_down.png' /> </view> <view class="view-options" wx:if="{{showOptions}}"> <cover-view class='option-item' wx:for="{{options}}" data-index="{{index}}" bindtap="selectItem">{{item[showkey]}}</cover-view> </view> <view class="view-out" wx:if="{{showOptions}}" bindtap="hideSelect"></view> </view> </view> [代码] js [代码]Component({ behaviors: ['wx://form-field'], //支持表单获取组件值 properties: { //组件的名称 title: { type: String }, //通过form获取组件的值 name: { type: String }, //下拉显示的数据集合 options: { type: Array }, //表单组件是否必填 isRequired: { type: Boolean }, //外部传递的动态变量 showkey: { type: String } }, data: { showOptions: false //组件默认的展开状态 }, /** * 组件的方法列表 */ lifetimes: { attached: function() { let key = this.properties.showkey this.setData({ value: this.properties.options[0][key] //默认选中第一个 }) }, }, methods: { selectToggle: function(e) { this.setData({ showOptions: !this.data.showOptions }) }, hideSelect: function(e) { this.setData({ showOptions: false }) }, selectItem: function(e) { let optionList = this.properties.options //外部传进来的数组对象 let nowIdx = e.currentTarget.dataset.index //当前点击的索引 let selectItem = optionList[nowIdx] //当前点击的内容 this.setData({ showOptions: false, value: selectItem[this.properties.showkey] }); let eventOption = {} // 触发事件的选项 this.triggerEvent("mySelectItem", selectItem) //组件选中回调 } } }) [代码] wxss [代码].view-item { display: flex; align-items: center; padding: 2% 0%; /* border-bottom: 1px solid #eee; */ } .item-key { font-size: 31rpx; color: #666; width: 25%; } .view-select-container { width: 75%; height: 80rpx; position: relative; } .select-value { width: 100%; height: 100%; display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; border: 1px solid #eee; font-size: 31rpx; position: relative; } .img-arrow { width: 28rpx; height: 26rpx; padding-right: 10rpx; } .view-options { background-color: #fff; display: flex; width: 100%; z-index: 3; position: absolute; flex-direction: column; justify-content: center; align-items: center; } .view-out { position: fixed; z-index: 2; width: 100%; left: 0; top: 0; height: 100%; } .option-item { font-size: 28rpx; width: 100%; display: flex; justify-content: center; align-items: center; margin-top: -1px; text-align: center; box-sizing: border-box; border: 1px solid #eee; line-height: 80rpx; height: 80rpx; } input { padding-left: 3%; font-size: 30rpx; } [代码] 使用 json中引入自定义组件 [代码]{ "usingComponents": { "Select": "/components/select/select" } } [代码] js [代码]Page({ data: { optionArry: [{ "name": "香蕉", "id": "1" }, { "name": "苹果", "id": "2" }, { "name": "橘子", "id": "3" }, { "name": "雪梨", "id": "4" }], }, onLoad: function() {}, }) [代码] wxml中使用 [代码]<Select title="类别" options="{{optionArry}}" isRequired="true" bind:mySelectItem='onSelectItem' name='formkey' showkey='name' /> [代码] 总结:可以动态传递对象数组在组件中显示的属性名,类似picker的range-key;使用cover-view解决当组件展开时遮住原生组件时的点击击穿问题。 最后代码片段以供学习和参考: https://developers.weixin.qq.com/s/RwZLwImG7kcE
2019-11-08 - 使用 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 - 小程序顶部自定义导航组件实现原理及坑分享
为什么使用自定义导航 对比默认导航栏,我们会更需要: 统一Android、IOS手机对于页面title的展示样式及位置 更丰富的导航栏定制效果,如添加home图标等 左上角返回事件的监听处理 统一实现双击返回顶部功能 自定义导航组件实现思路 自定义导航组件实现的核心是需要计算导航栏的真实高度 这里以官方文档->扩展能力中的Navigation组件为例分析实现思路。当使用"navigationStyle": "custom"时,默认导航被移除,页面的开始位置变成了屏幕顶部,这时我们需要实现的导航栏是在状态栏下面。 导航栏的真实高度=状态栏高度+导航栏内容。 [图片] 使用wx.getSystemInfo获取到statusBarHeight便是导航栏的高度,但是导航栏内容高度呢? 有人可能觉得导航栏内容高度顾名思义就是导航栏内容高度啊,内容撑起还用管嘛!要,必须要! 因为右上角胶囊按钮是原生加载的,我们的导航栏内容需要正好贴在胶囊的下方且垂直居中。 导航栏内容高度=(胶囊按钮的顶部距离 - 状态高度)*2 + 胶囊高度 [图片] 如何计算胶囊的数据呢?幸运的是我们有 wx.getMenuButtonBoundingClientRect() 获取胶囊按钮的布局位置信息,那么动态计算导航栏的内容高度就很方便啦。 好了,以上就是动态计算的核心思路,我们再来看官方Navigation组件高度是怎么实现的 [代码]page{--height:44px;--right:190rpx;} .weui-navigation-bar .android{--height:48px;--right:222rpx} .weui-navigation-bar__inner{ position:fixed;top:0;left:0;z-index:5001;display:flex;align-items:center; height:var(--height);padding-right:var(--right);width:calc(100% - var(--right)) } [代码] 导航栏内容的高度是通过- -height这个css变量提前声明好的,安卓机型会重新覆盖为新的css变量值,目前没发现有适配问题。 官方就是官方啊,具体尺寸都知道,那就不用一番计算周折啦,直接拿来主义即可。 导航的布局位置已经搞定啦,剩下就是写具体的内容,不同业务实现需求不同这里就不一一赘述了。 完善官方顶部导航组件 本着拿来主义,直接使用官方Navigation组件,但在实际业务开发中还是遇到不少需要自定义的需求,就比如: loadding样式没实现 标题内容超出没有出现省略号 和原生顶部的样式不兼容,导致单个页面引入时跳转有明显差异出现 没有双击返回顶部功能开关功能 引入页面需要获取导航栏的高度,来控制其他元素距离顶部的位置, 不能根据页面栈数据动态显示隐藏back按钮, 针对以上需求,我们对官方的组件进行二次完善开发,满足常规的自定义需求绰绰有余,直接引入开箱即用。 源码使用示例 https://github.com/YuniorZen/minicode-debug/tree/master/minicode02 [图片] 使用说明 [代码]/*自定义头部导航组件,基于官方组件Navigation开发。*/ <navigation-bar title="会员中心" bindgetBarInfo="getBarInfo"></navigation-bar> [代码] 组件属性列表 属性 类型 描述 bindgetBarInfo function 组件实例载入页面时触发此事件,首参为event对象,event.detail携带当前导航栏信息,如导航栏高度 event.detail.topBarHeight bindback function 点击back按钮触发此事件响应函数 backImage string back按钮的图标地址 homeImage string home按钮的图标地址 ext-class string 添加在组件内部结构的class,可用于修改组件内部的样式 title string 导航标题,如果不提供为空 background string 导航背景色,默认#ffffff color string 导航字体颜色 dbclickBackTop boolean 是否开启双击返回顶部功能,默认true border boolean 是否显示顶部导航下边框分割线,默认false loading boolean 是否显示标题左侧的loading,默认false show boolean 显示隐藏导航,隐藏的时候navigation的高度占位还在,默认true left boolean 左侧区域是否使用slot内容,默认false center boolean 中间区域是否使用slot内容,默认false Slot name 描述 left 左侧slot,在back按钮位置显示,当left属性为true的时候有效 center 标题slot,在标题位置显示,当center属性为true的时候有效 自定义顶部导航目前存在的坑 弹窗的背景蒙层无法覆盖原生胶囊按钮 页面下拉刷新的圆点会被自定义导航遮盖 如果要自定义顶部导航,以上问题避免不了,只能忍着接受。 目前还没遇到完美的解决方案,针对下拉刷新圆点被遮挡的问题微信官方还在需求开发中,如果你有好的想法欢迎留言反馈,一起学习交流。
2019-10-31 - 小程序生成分享图片方案分享
小程序生成图片分享朋友圈 小程序开发者都希望自己的小程序得以广泛传播,因为不少小程序都设计了很多转发激励行为,但分享小程序到朋友圈(或其他外部平台)一直是一个难题。一个常见的方案就是生成分享海报、分享图片。但生成分享图片在技术上却也是一个难题。 技术选型 目前常用技术方案基本分为三种: 使用 canvas 绘图并生成 使用后端绘图库进行绘制,返回给小程序端 使用服务端开一个浏览器进行 HTML 渲染,并截图返回给小程序端 第一种方案:要求较高,canvas 和纯 html 布局相去甚远,零基础学习成本较高,而且在不同的微信浏览器中效果不可预期,想短时间内做出精美可控的生成图片不容易。实操的时候发现了一个非常麻烦的事情:网络图片或者 base64 图片都无法直接在 canvas 里渲染显示,要先下载好传进去。 第二种方案:后端库可以完成较为简单的需求,但字体加载、阴影、圆角、透明等方案效果需要精调,如果文字需要截断或动态伸缩长度时并不容易处理。图片的截取和伸缩自适应也不灵活。而且选用这种方案相当于需要把 UI 布局的工作丢给后端工程师去解决,这不是他们擅长的范围,效果未必会好。 第三种方案:页面的绘制方面,纯前端技术即可完成,难度低,完成度高,但是需要在后端起一个 node 服务开启 puppeteer 去控制服务端 Chrome 浏览器。这种方案的缺点就是成本太高,我们和业界同行都测算过,结果差不多:4 核 16G 的服务器生成图片的 QPS 大概只有 10-20,相当于一秒钟较差情况只能生成 10 张图片,这对于突发的大量分享需求并不能满足,而且这种配置的服务器,不能部署其他服务,只跑这个服务就会用尽大部分资源。 费用上:只单单算 5M 带宽的服务器费用一个月就要 700+ 人民币,流量和图片托管费用另算。此方案的最小化实现:至少需要 1 核 2G 的服务器才能较为顺畅地完成一次顺利截图,但是还是要处理浏览器无响应假死等情况,较为复杂。但综合来看,这种方案是效果最好最为灵活的。 快海报小程序分享图生成服务 快海报 kuaihaibao.com 是专门提供小程序分享海报生成服务的,技术上用的就是上面所述的第三种方案,但是只需要调用他的 API 就可以完成,不需要开发者维护 puppeteer 和 headless Chrome,而且成本较低,一张分享图的最低生成成本是 0.01 元。 其实真正集成到自己的服务中时,平均成本要比这个低,因为有些生成的图片的二维码,如果不带用户个人信息(不给分享的用户返利)时,可以生成一次之后永久缓存起来,其他用户再分享同一个东西都用缓存好的图片,综合成本就降下来了。 算一下成本: 比方说一个刚起步的小程序日活 5000(对于刚起步的小程序其实已经很高了吧) 假设有 5% 的用户生成分享图 也就是每天生成 250 张分享图,一个月会生成 7500 张分享图 这样的话每个月成本就是 75 元人民币左右,相比 700+ 人民币的服务器成本省太多了。这是测算比较高的指标,而且是完全不应用缓存方案的情况。 如果你的小程序还处于冷启动的阶段: 日活 500 假设有 5% 的用户生成分享图 也就是每天生成 25 张分享图,一个月会生成 750 张分享图 每月成本 7.5 元。比 1 核 2G 的最小化自部署方案也要便宜。但带来的收益是无穷的,750 张分享图发到朋友圈,每张分享图 1000 受众浏览,一个月就是将近 750000 人次分享受众。 调用 API 首先去 https://kuaihaibao.com/ 注册账号,验证邮箱激活之后,其实就可以先测试用了,每个账号有 100 次测试额度,测试生成的图片带水印。 网站左侧的 [文档] 页面能找到集成文档,非常简单,一共就只有一个核心 API,通过 HTTP 调用的。 先在【开发】->【设置】中激活 token [图片] 目前支持三种生成方式: 直接传 URL 进行渲染 传 HTML 渲染 使用内置的模板进行选择 这里演示使用模板渲染,因为比较简单 打开 【开发】->【模板】中,找到自己喜欢的模版。因为我只想生成一个简单的分享图片,所以最简单的方式就是使用网站内置的模版,内置模板目前有 8 款,应该能满足大部分小程序的需求了,抽奖、打卡、图文、文字、电商都有,改一改文案和图片就可以了。 我选了这个抽奖模板: [图片] 按照 https://kuaihaibao.com/doc/docs/template/kzccda95.html 文档描述的 JSON 改成我需要的: [代码]{ "backgroundColor": "#fafafa", "backgroundImage": "", "user": { "avatar": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/girl_2.jpg", "nickname": "我是测试账号", "color": "#666" }, "tip": "邀请你来抽奖", "qrcode": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/sample_qr_0.png", "records": [ { "title": "一等奖", "desc": "2019 年 11 月 16 日 10:00 开奖", "image": "https://s3.cn-northwest-1.amazonaws.com.cn/res.weiyidan.com/production/10000002/4109f8e51a8f43b9816dbc8fe636e22a.jpeg" } ], "brand": "我的测试抽奖小程序", "slogan": "快来和我一起抽吧!", "metaColor": "#999" } [代码] 然后打开 Terminal 做一次请求试试: [代码]curl -X "POST" "https://api.kuaihaibao.com/services/screenshot" \ -H 'Authorization: Bearer 这里写你自己的 token' \ -H 'Content-Type: application/json; charset=utf-8' \ -d /pre>{ "template": "kzccda95", "data": { "qrcode": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/sample_qr_0.png", "records": [ { "title": "一等奖", "desc": "2019 年 11 月 16 日 10:00 开奖", "image": "https://s3.cn-northwest-1.amazonaws.com.cn/res.weiyidan.com/production/10000002/4109f8e51a8f43b9816dbc8fe636e22a.jpeg" } ], "tip": "邀请你来抽奖", "slogan": "快来和我一起抽吧!", "metaColor": "#999", "brand": "我的测试抽奖小程序", "backgroundImage": "", "backgroundColor": "#fafafa", "user": { "avatar": "https://khb-sample.oss-cn-shanghai.aliyuncs.com/sample/girl_2.jpg", "nickname": "我是测试账号", "color": "#666" } } }' [代码] 返回了结果: [代码]{ "success": true, "data": { "name": "iPhone 5", "image": "https://khb-test-oss.oss-cn-shanghai.aliyuncs.com/screenshot/4fa63f2a3605cbdece90c659cbccea619d9cf9fa?x-oss-process=style/test_watermark" } } [代码] 打开图片地址看看: [图片] 速度很快,图片很漂亮,只是中间带水印,充值后成为付费用户,再生成的图片水印就自动取掉了。 后端集成 这里参考快海报官方给的最佳实践的逻辑参考图: [图片] 所以后端只需要做一件事,就是提供一个 API 给客户端用,这个 API 被调用的时候去请求快海报的服务器,再把结果返回给小程序就好了。
2020-10-20 - recycle-view 商品长列表应用
recycle-view 在商品长列表中的应用 参考官方文档 WXML: [代码]<recycle-view class="list" batch="{{batchSetRecycleData}}" id="recycleId" bindscrolltolower="bindscrolltolower" scroll-y="true"> <recycle-item class="record" wx:for="{{recycleList}}" wx:key="id"> <view class="record_image" style="background:url( {{item.images[0] }}&x-oss-process=image/resize,s_320 )"></view> <view class="record_view">{{index}} . {{item.title}}</view> </recycle-item> <view slot="after">加载中...</view> </recycle-view> [代码] JS: [代码]const app = getApp() const createRecycleContext = require('miniprogram-recycle-view'); Page({ pageNum:1,//页码 listobj: Object,//RecycleContext对象 postflg:true,//是否可以加载列表,用户误触控制 windowWidth:0,//系统页面可视宽度 data: {}, onReady: function () { var than = this; //获取系统参数 wx.getSystemInfo({ success: function(res) { than.windowWidth = res.windowWidth; //创建RecycleContext对象来管理 recycle-view 定义的的数据 than.listobj = createRecycleContext({ id: 'recycleId', dataKey: 'recycleList', page: than, itemSize: than.itemSizeFunc, }) than.getlist();//请求接口 }, }) }, //设置item宽高信息,样式所设必须与之相同 itemSizeFunc: function (item, idx) { var than = this; return { width: than.windowWidth * 0.47, height: than.windowWidth * 0.61 } }, //滚动到底部监听,分页加载 bindscrolltolower(e) { console.log('滚动到底部----'); if(this.postflg){ this.postflg = false;//请求完成前不再更改页码请求接口 this.pageNum++; this.getlist(); } }, //数据请求 getlist(){ var than = this; wx.request({ url: 'https://w.taopaitang.com/api/discover?page=1&pagenum=10', data:{ page: than.pageNum, pagenum:10, }, method: 'get', success(res){ console.log('数据请求成功----' + than.pageNum +'---',res); if(res.data.message){ //append RecycleContext 对象提供的方法:在当前的长列表数据上追加list数据 than.listobj.append(res.data.data.items); than.postflg = true; } } }) } }) [代码] 全部代码 -------> 代码片段 <--------- 我想把此列表改成瀑布流展示,求路过的大神指点下!!!!!!
2019-10-07 - 纯云开发二手书商城的全开源demo
这是为母校写的一个纯粹的公益小程序,原生+云开发,写文章太累了,所以所有代码我都写了注释,还是很适合入门学习的,特别是云开发 [图片] [图片] [图片] 程序本身来说,我认为没啥多大的亮点,只不过把很多单个案例综合起来了,云开发方面,比如:支付、提现、获取用户手机号、发短信、发邮箱。。。。。。。界面上,清一色的flex布局。 和完整版得商城小程序,还差了一丢丢–购物车,因为思考了一下,这个小程序着实用不着,用来学习还是可以了滴 源码和使用教程发在Github: https://github.com/xuhuai66/used-book-pro
2019-09-18