- 微信左上角分享成功是否有回调?
[图片] 左上角分享 成功后有回调吗?
2023-10-08 - web-view打开嵌入腾讯视频的页面提示iframe不支持引用非业务域名
现在的策略是允许公众号文章内嵌iframe,对于第三方的iframe会检查业务域名。可理解为: 1、打开公众号文章,文章内嵌iframe含有腾讯视频这种情况就是支持的。 2、打开开发者自己的业务域名,网页内嵌iframe含有腾讯视频这种情况就是不支持的。
2019-11-20 - 代码按需注入与初始渲染缓存
[视频] 你好,我是李艺。 上节课我们主要学习了自定义组件的优化技巧,这节课我们学习按需注入与初始渲染缓存。 在第一课关于启动流程的介绍中我们已经得知,第二阶段代码注入在冷启动中是不可或缺的,只有逻辑层和视图层代码全部注入,并且时间点对齐以后才会开始第三阶段首屏渲染的工作。如果这个小程序的这个代码它很大、代码很复杂,又或者用户的设备是低端机,性能不太好,这一阶段便会显著影响启动性能。 针对这个问题,小程序提供了懒加载机制,允许首屏渲染之前按需注入仅这个首屏运行所需要的一个代码,其他的代码可以在稍后用到的时候再加载和注入。下面看项目实践。 首先看实践一:按需注入 启动按需注入只需要在app.json配置文件里面添加一个lazyCodeLoading的配置,添加配置之前,小程序在首屏渲染之前,它页面所在的代码包以及主包的所有页面代码小程序都会加载并且注入,添加这个配置以后虽然相关的代码包仍在下载,但仅会加载和注入当前这个页面,它所需要用到的那些组件以及代码启用懒加载以后,在微信开发者工具的调试区可以看到相应的关于懒加载的提示,有一点我们需要注意,如果这个页面本身它比较复杂,用到了很多自定义的组件,这些自定义组件在开启按需注入这种模式以后仍然是会加载的,如果我们想进一步减少这个首屏需要注入的代码,可以在启用按需注入以后同时启用占位组件,关于占位组件,稍后我们会再详加讲解。 下面我们看实践一的代码演示。 在小程序里面启用按需注入,也就是俗称的懒加载,很简单,只需要在这个项目的app.json这个里边加一条配置就可以了,然后其他的这些工作全部是由微信完成的,我们只需要加一条配置就可以了。写一个叫做lazyLoading,当我们打一个lazy的时候它自动提示它应该填写的正确的内容, lazyCodeLoading等于requiredComponents,包括它这个值也会自动帮我们完成,其他的我们都不需要做,这个已经添加完了。现在我们单击编译按钮看一下它的一个表现,注意看在我们调试区现在多了一条打印信息:Lazy code loading is enabled告诉我们现在懒加载已经启动了,只是注入我们需要的一些组件,它有这样的一条提示,代码演示就到这里。 下面我们看实践二:静态初始渲染缓存。 前面我们使用过骨架屏,骨架屏是在这个页面完全加载以后,由微信开发者工具负责生成的一个色块状的页面结构,在运行时与骨架屏对应的还有一项技术,就是初始渲染缓存。初始渲染缓存是第一次页面运行的时候由微信客户端负责将这个页面在本地的某个区域缓存起来,下次真正的页面未加载之前,先展示缓存过的页面。初始渲染缓存又分为静态和动态两种,在这个页面配置里面只需要添加一个叫做initialRenderingCache这样的一个配置就可以启用静态缓存。静态初始缓存以页面初始的data数据与页面里面的wxml标签代码共同渲染成一个页面在本地缓存,在下一次用户访问页面的时候,不必等逻辑层代码初始化完毕它就会将缓存的页面内容先发给用户展示,在一定程度上,初始渲染缓存的页面相当于是一个静态化的本地化的骨架屏页面。 现在看实践二的代码演示。 启用渲染缓存很简单,也是加一条配置就可以了。我们打开商品详情页,它的json配置文件,我们只需要在这个里边加一条initialRenderingCache,它的值有两个,然后下面这个static代表的就是静态初始缓存,把这条配置加上然后就可以了。然后接下来我们再调一下我们编译设置,以商品详情页为启动页面进行一个测试,我们把警告信息打开以后可以看到这个地方会有一个提示,无效的page.json initialRenderingCache,提示我们这个地方有一个无效的页面配置,这个配置它其实只是在我们的微信开发者工具里面会有,如果是我们用手机测试的话,刚才我们提到了,它其实会有一条关于初始信息在这个页面成功加载之前就进行渲染的这样的一条提示。那个是我们的初始渲染缓存机制在起作用,对于我们开发者工具里面这样的一条提示,其实可以无视它,这个无关紧要的 没有关系。代码演示就到这里。 下面我们看实践三,动态初始渲染缓存。 动态初始渲染缓存与静态初始渲染缓存的不同在于配置节点的值不同,在这个页面配置里面我们添加一个initialRenderingCache等于dynamic这样的一个配置就可以启用动态缓存了,与静态初始缓存不同的是动态缓存可以指定缓存的内容。例如我们举个例子:在页面加载完成以后,通过调用setInitialRenderingCache的一个方法设置需要动态缓存的数据,这个方法的一个参数是一个dymamicData,也就是动态数据,它将与页面初始的data一起混合,然后与这个页面的wxml源码共同生成一个缓存的页面,以便下一次用户访问的时候直接去使用,并不是在本次这个页面展示的时候使用的。设置动态缓存的代码我们一定要放在 onReady周期函数里面,不能比这个时间点要早,早的话其实会影响这个页面的整体的一个渲染效率的。在微信开发者工具的模拟器里面,初始渲染缓存的一个缓存功能,刚才我们提到了会有一条黄色的无效的initialRenderingCache这样的一个配置警告信息,这个信息它不单在静态缓存里面有,在动态缓存里面其实它也有,但对于这条信息其实我们没有必要在意,我们在手机上进行测试的时候其实是没有这样一条提示的,我们可以在手机上预览商品详情页,然后关掉小程序,怎么关掉,就是在微信顶部下拉这个屏幕,在最近使用过的小程序列表里面将测试版本的小程序下拉到下方的红色区域内进行删除,当我们下一次在手机中再次预览的话就能看到这个缓存页面,但是如果你这个页面足够简单,由于它加载太快的话,这个缓存页面可能还是看不到。当然我们在调试区会看到一条提示就是Update view with init data,这个提示它其实就是我们的初始渲染缓存机制在发挥作用,并且这条提示它会在我们这个页面的onLoad事件之前就会显示出来。现在我们在屏幕上看到的这个截图就是我们手机上测试的一个截图效果。 下面我们进行实践三的代码演示。 启用动态初始渲染缓存也很简单,主要也是改配置,在我们商品详情页的json配置文件里面将它的initialRenderingCache这个节点,它的值由static改成dynamic,如果这个我们记不清的话,其实还可以借助它的自动提示功能,然后回车就可以了,这是第一步。第二步就是我们如果想加一点动态的数据作为缓存的一部分的话,可以在这个里面有一个关于swipers数据的加载,加载完成以后,在最后这个地方可以再调用它的一个叫做setInitial,这个地方没有提示对吧,第一个方法可以查文档,第二个方法可以看最终源码,我们就看一下最终源码。 这个源码里面序号跟我们实践是对应的,比如说我们第三讲的实践三对应的源码就是3.3 setInitialRenderingCache这个代码,这个地方我们可以看到 这个名字它用的其实是我们data对象里面默认的有的这个名字,然后这个数据是我们新加载到的数据,但是我们设置跟setData不一样,setData是我们设置这个页面里面,给这个页面用的,这个是给我们动态渲染缓存机制去使用的,代码设置完以后我们可以再测试一下。在调试区刚才我们提到了,仍然会看到无效的page.json initialRenderingCache这样的一个黄色的配置提示,这个没有关系 其实可以无视它。在手机上测试它这个提示其实是没有的,这个代码演示就到这里。 最后我们总结一下,按需注入是小程序的一项优化机制,只需要开发者在app.json配置文件里面加一条配置,不需要再做其他的任何事情就可以显著提升小程序的启动性能了,这条配置可以作为项目的一个默认配置进行保留,初始渲染缓存它相当于之前PC Web 2.0时代在后台自动为动态页面生成的HTML静态页面,当用户访问的时候,它发给用户的是静态页面,动态内容有更新的时候,静态页面在后台它会重新再生成一次,这是一种以空间换时间的一种优化策略,多占用一点点的内存和硬盘以争取启动时间上的一个减少。 下面我们再说一下初始渲染缓存的它的一个工作原理,在小程序启动页面的时候,尤其是小程序冷启动进入第一个页面的时候,由于逻辑层初始化的时间比较长,等到逻辑层与这个视图层全面初始化完毕再渲染出这个页面可能会看到白屏现象,这是我们不想给用户看到的,启用初始渲染缓存可以使视图层不需要等待逻辑层初始化完毕,也就是这个时候我们可以跳过逻辑层与视图层初始化完成时间点的这样的一个对齐的限制,直接提前将一个缓存的页面渲染结果展示给用户,具体的实现的流程它是这样的,在小程序页面的第一次被打开的时候,微信就将这个页面的初始渲染的结果记录下来,然后写入到一个临时的缓存区域里面,在这个页面第二次打开的时候,微信它查看缓存里面有没有这个页面,如果是有 直接就把这个页面展示给用户,但是我们要知道缓存的页面它是无法响应用户的交互事件的,需要等到这个页面真实渲染完成以后这个页面才可以正常访问,现在我们在屏幕上看到的这两个链接是我们本节课涉及到的文档链接。这节课我们就讲到这里。 这节课我们主要学习了代码的按需注入,俗称懒加载,开启方式是在app.json配置文件里面添加一个值等于requiredComponents一个lazyCodeLoading的配置节点,关于初始静态缓存,这是一种空间换时间的优化策略,它可以减少用户等待页面加载的时间,但这个缓存的页面是不能进行事件交互的,并且并不是所有的组件都支持静态缓存的,基本上我们在文档上看到所有的原生组件它都不在这个缓存之列,不在缓存之列也不会给我们带来一些错误,它只是这些组件它无法在缓存以后呈现给用户,所以初始渲染缓存只适合这个页面节点数量比较少、比较简单、内容不经常变化、用户又经常访问同时wxml节点的结构又非常简单的这样的一些入口页面去使用。 初始渲染缓存与骨架屏是同一种优化策略,本质上它们都是以空间换时间,只不过是两个不同方向的一个优化,一般而言我们用了骨架屏以后就不要再使用初始渲染缓存了。反之也是一样的,用于提供导航功能的一个首页或者是二级页面,一般适合使用初始渲染缓存,而这个页面经常变化的时候,它时效性较高的动态详情页面,这种页面它适合使用骨架屏。当然了如果这个页面想让用户尽早看到内容的话,下节课我们学习如何使用分包,独立分包以及分包预下载。 这里有一个问题请你思考一下,在多地图游戏开发里面,当用户从一个地图跳入到另外一个地图的时候,我们总是能看到一个资源加载进度条,这是游戏对地图资源加载的一种优化,这类游戏它并没有在启动的时候就开始加载所有的地图资源,而仅是在加载当前这个游戏运行所需要的一个最小的资源包,在小程序开发里面有没有这样的一种机制可以实现相似的资源加载优化效果?下节课我们就一起来探讨一下这个问题。 点击查看开放文档: 初始渲染缓存按需注入和用时注入
2022-07-14 - 微信小程序真机调试报错fail-109:net::ERR_ADDRESS_UNREACHABLE ?
在已配置https的情况下,开发者工具中运行正常,真机调试及体验版 部分接口出现 fail-109:net::ERR_ADDRESS_UNREACHABLE 错误, 部分接口可以正常调用。求大佬告知原因及解决方法!
2020-01-15 - 使用分包异步化组件实现可变Tab页面
背景和需求 众所周知,在微信小程序内,TabBar 页面必须放主包内,这固然是为了用户体验做出的限制,但是也限制了开发者,如果想要实现不同的客户可以定制不同的TabBar页面,而很多页面又是分散到不同分包内的,那我们能选择的方案也就是在所有可作为TabBar页面上放置自定义TabBar组件,而后根据客户的不同配置,展示不同的TabBar 选项,当客户点击Tab时,使用[代码]navigateTo[代码]或[代码]redirectTo[代码]进行切换页面。 但这个方案存在明显的问题,首先如果使用[代码]navigateTo[代码]进行切换,会有很明显的页面切换动画,很容易到达10层页面栈限制(当然这个可以使用无限路由方案进行缓解,但是无限路由是一种万不得已且体验很差的路由方案),且由于页面未进行销毁,内存占用会比较大,容易造成卡顿;如果使用[代码]redirectTo[代码]进行切换,页面节点状态无法保存(如滚动位置),页面数据倒是可以使用全局状态管理库进行保存,但是每次在切换 Tab 都会有明显的数据重新加载的动画效果。 曙光 在微信小程序支持分包异步化之前,对于上面的问题一直没有好的解决方案,支持分包异步化之后,我们可以将一些组件放入分包内异步加载,这一定程度上解决了主包过大的问题。同时也让我们看到了希望,我们可以将很多组件放入分包内进行异步加载,主包空间空了出来,可以放更多的页面,但不是所有页面都能放入主包,那还有其他方案吗? 我们想,既然组件能从分包异步加载,那页面可以吗? 我们知道,在微信小程序内,通常都会使用Page进行声明页面,但也可以用Component声明页面,也就是说 Component 声明的组件可以当成页面用,那反过来,Page 声明的页面可以当成组件用吗? 答案是可以,但是当这样使用的时候,页面的生命周期方法不会被执行,且实例对象上不存在options(页面路由参数),route(当前页面路由地址)等数据,那我们就不能愉快地玩耍了吗? No! 没有页面该有的属性?那我们就拿到实例对象给他补上去! 生命周期方法不执行?那我们就拿到实例对象后自己去调用! 解决思路 要将现有页面作为组件加载,那我们必须要有一个容器页面,去承载真实页面,在容器页面中去补上已经作为组件的真实页面缺失的属性,在对应的生命周期方法中调用真实页面的生命周期钩子。 我们第一步就需要创建一个容器页面出来,我们可以选择手动创建,也可以自动化创建, 但是已有项目来说,手动创建太费时,且每增加新页面都要修改容器页面代码,故此不考虑。 自动化构建容器页面包含如下步骤: 读取 app.json,获取所有分包页面路径 读取分包页面对应的json文件,将其中内容记录到 [代码]tab-bar-page-config.js[代码] 中,因为我们需要在运行时读取真实页面的标题,背景色等信息,而微信小程序不支持从js中读取json文件,故需要将json内容提前读取出来,为了减少数据量,记录时可以将[代码]usingComponents[代码]等无需运行时使用的数据去掉。效果如下: [代码]// tab-bar-page-config.js module.exports = { "/pack_a/page_1": { "navigationBarTitleText": "页面标题", "navigationBarBackgroundColor": "#ffffff" }, "/pack_b/page_2": { "navigationBarTitleText": "页面标题", "navigationBarBackgroundColor": "#ffffff" } /* 其他页面信息 */ } [代码] 生成 wxml 文件,效果如下: [代码]<pack_a_page_1 id="pack_a_page_1" wx:if="{{ pagePath === '/pack_a/page_1' }}" /> <pack_b_page_2 id="pack_b_page_2" wx:elif="{{ pagePath === '/pack_b/page_2' }}" /> <!-- 其他页面节点 --> [代码] 生成容器页面 json 文件,效果如下: [代码]{ "usingComponents": { "pack_a_page_1": "/pack_a/page_1", "pack_b_page_2": "/pack_b/page_2", /* 其他页面 */ }, "componentPlaceholder": { "pack_a_page_1": "view", "pack_b_page_2": "view", /* 其他页面 */ } } [代码] 编写容器页面 js 逻辑,大体如下: [代码]Page({ data: { // 当前真实页面的路径 pagePath: '', }, // 真实页面的实例 pageInstance: null, onLoad() { // 根据网络接口返回数据,得到当前容器页面应当显示的真实页面路径 this.setData({ pagePath: someDataFromNet.pagePath, }); }, onShow() { this.pageInstance?.onShow?.(); }, onReady() { this.pageInstance?.onReady?.(); }, /* 其他生命周期 */ }) [代码] 我们现在面临一个问题,那就是我们是使用分包异步化组件进行加载真实页面,那真实页面是什么时候加载成功的呢?我们知道当组件加载成功后,会执行组件的 [代码]lifetimes.attached[代码] 生命周期, 那既然页面可以当成组件用,那页面是否也有这个生命周期呢?通过查阅文档,我们知道了可以在页面中使用[代码]Behavior[代码], 我们可以通过[代码]Behavior[代码]中定义 [代码]lifetimes.attached[代码],在其中通过 [代码]this.triggerEvent('pageattached')[代码] 去通知容器页面,现在我们的 wxml 需要做一些修改,如下: [代码]<pack_a_page_1 wx:if="{{ pagePath === '/pack_a/page_1' }}" bind:pageattached="onPageAttached" /> <pack_b_page_2 wx:elif="{{ pagePath === '/pack_b/page_2' }}" bind:pageattached="onPageAttached" /> <!-- 其他页面节点 --> [代码] js 中也要增加 [代码]onPageAttached[代码] 方法,如下: [代码]Page({ /* 其他生命周期方法 */ onPageAttached() { const route = this.data.pagePath.slice(1); const id = route.replace(/\//g, '_'); const page = this.selectComponent(`#${route}`); page.route = route; page.options = {}; // 补全其他信息, // 调用对应生命周期方法, page.onLoad?.(page.options); // 由于组件可能加载得比较晚,容器页面的 onShow 和 onReady 已经执行过了,这里需要手动执行一遍真实页面的 onShow 和 onReady // 还需要额外做一些判断,避免 onShow 连续执行多遍 page.onShow?.(page.options); page.onReady?.(page.options); }, }) [代码] 好了,准备工作基本上做完,现在就差给所有页面加上我们之前写的[代码]Behavior[代码]了,如果项目一开始就封装了[代码]BasePage[代码]之类的方法,我们只需要在 BasePage 将这个[代码]Behavior[代码]加到[代码]BasePage[代码]中就行,如果没有的话,可以通过改写[代码]Page[代码]去实现,这里就不举例了。 现在我们按照上面的步骤生成5个容器页面并且加入到 [代码]app.json[代码] 中了,然后开始下一步了, 等等。。。5个容器页面?生成的代码有5份!不行,这样会平白占用很多主包空间的,我们需要做一些优化:将生成5个容器页面优化成生成一个容器组件,然后在5个容器页面内去引用该组件,并修改上面的一些逻辑,这样生成的代码就基本上少了1/5,还是很可观的。 现在还差封装[代码]switchTab[代码]方法了,在其中将[代码]url[代码]替换成容器页面的地址,然后记录该容器页面需要展示的真实页面地址,在容器页面中加载对应的真实页面即可。亦可改写 [代码]wx.switchTab[代码] 去调用我们封装的 [代码]switchTab[代码] 方法,在此就不举例了。 好了,现在基础步骤已完成,就差看效果了。 咦,好像还差某些东西,页面标题呢?怎么不能下拉刷新了?这个页面好像是没有顶部导航栏的呀。 我们一个一个来。 标题及背景颜色等 还记得之前生成的 [代码]tab-bar-page-config.js[代码] 吗?我们在其中记录了页面的一些信息,现在,我们需要在运行时去调用微信API设置标题,颜色等信息。解决。 下拉刷新 微信没有提供是否启用下拉刷新的API,所以我们只能给所有容器页面都加上下拉刷新,然后 [代码]onPullDownRefresh[代码] 中判断如果当前真实页面没有启用下拉刷新,就调用[代码]wx.stopPullDownRefresh[代码]停止下拉刷新,否则就调用真实页面的[代码]onPullDownRefresh[代码]钩子。额。。。勉强算解决吧。 顶部导航栏 微信同样没提供是否启用顶部导航栏的API,故只能将5个容器页面分成2类,2个是不带顶部导航的,剩下3个是带顶部导航的,在我们封装的 [代码]switchTab[代码] 中增加判断要跳转的页面是否是包含顶部导航的,分别落到不同的容器页面上即可。解决。 至此,动态Tab页面基本上实现了,还有些样式上的兼容问题,如:某个页面的wxss声明了 [代码]page { backgroud: 'red'; } [代码] 那容器页面内的所有页面都会被影响,对此我们只能在页面的[代码]wxss[代码]中不使用[代码]标签选择器[代码],实际上在微信开发者工具中,使用[代码]标签选择器[代码]是会报警告的,但是口头约束是没有用的,还是会有人会写,故我们引入了[代码]postcss[代码],编写插件使在构建时将标签选择器去掉,并且报出警告。 至此,功能基本完成,需要做的就是验证哪些功能出现了问题,做出相应的修改。 结尾 分包异步化作为一个新出现的特性,还存在一些不稳定,如在开发者工具中,经常出现加载失败的问题,ios 真机调试报错等问题,且要求的最低SDK版本为[代码]2.17.3[代码],要在生产环境中使用还需要做很多的验证工作,也希望微信官方能尽早修改开发者工具中的问题。
2021-11-29 - 分包异步化,分包难题不用怕
原文来自「微信开发者」公众号。 本文主要介绍了“分包异步化”新能力的原理、组件、方法和兼容性要求。 在小程序开发过程中,你是否对分包问题感到困扰? 多业务的分包难以划分分包体积膨胀下载并注入无用代码插件无法实现分包处理……为解决上述问题,微信团队提供【分包异步化】新能力,实现跨分包组件、跨分包方法,成功解决分包难、分包不合理等问题。 • • 分包异步化原理 • • 原有的分包隔离机制导致各分包之间无法引用自定义组件或逻辑代码,因此导致分包难等一系列问题。分包异步化能力打通不同分包的引用关系,解决小程序代码包合理化的问题,支持跨分包组件、跨分包方法。 [图片] • • 跨分包组件 • • 当使用其他分包组件时,代码包需要增加占位组件 (component placeholder),实现页面高效配置。例如页面展示时,分包 (subpackageB) 仍未下载,进行以下操作实现跨分包组件: 1. 使用组件 <simple-list> 代替 <list>,使用 <view> 代替 <card>,完成页面渲染 2. 完成渲染后,开始下载和注入分包 3. 完成分包下载和注入后,将占位组件替换成真正的组件 // subPackageA/pages/index.json { "usingComponents": { "button": "../../commonPackage/components/button", "list": "../../subPackageB/components/full-list", "simple-list": "../components/simple-list" }, "componentPlaceholder": { "button": "view", "list": "simple-list" } } • • 跨分包方法 • • 在小程序开发过程中,通过require回调函数或requireAsync异步调用2种方法,分包异步化能够引用其他分包的逻辑代码。具体操作如下: // subPackageA/index.js // 使用回调函数风格的调用 require('../subPackageB/utils.js', utils => { console.log(utils.whoami) // Wechat MiniProgram }) // 或者使用 Promise 风格的调用 require.async('../commonPackage/index.js').then(pkg => { pkg.getPackageName() // 'common' }) • • 兼容性要求 • • 分包异步化能力要求基础库版本 2.17.3 及以上(正式发布需在 mp 设置最低版本基础库 2.17.3)。平台能力兼容安卓微信、iOS 微信、1.05.2104272 及以上版本的微信开发者工具。更低版本的基础库兼容工作预计在一个月后完成。 • • 总结 • • 实现分包异步化能力后,主包的「公有」性质被削弱,「前置」性质显得更重要(优先于所有分包注入运行且默认注入运行)。开发者可以根据自身业务诉求,结合分包异步化,进行小程序调优,实现更快的启动速度、按需下载和注入代码包、合理处理公有组件等效果。 如有其他小程序相关问题,可在 微信小程序交流专区 中发帖互动,将有技术专员为大家解答及进行深度交流。
2022-03-24 - 微信小程序对接企业微信客服
考虑到用户会在企业的小程序里联系客服,为此支持在小程序里接入微信客服。微信小程序打开微信客服的功能已向非个人的全体小程序开放,小程序开发者在小程序管理后台处绑定同主体的微信客服(企业ID)后即可调用小程序相关接口,接入微信客服。 接入方式:https://developer.work.weixin.qq.com/document/path/94739、https://kf.weixin.qq.com/api/doc/path/94772 在微信客服管理后台获取对外的企业ID和客服链接。在小程序管理后台的【功能】【客服】【微信客服】处,填写同一主体的微信客服对应的企业ID,完成小程序和微信客服的绑定。调用「小程序打开微信客服」接口,完成接入。 注:仅可正常接入已在小程序管理后台绑定的企业ID下的微信客服 小程序管理后台-关联企业微信客服注意:企业ID必须跟该小程序的企业主体一致; 在小程序中的接入流程:https://work.weixin.qq.com/nl/act/p/a733314375294bdd 详情如下: [图片] 登录企业微信管理后台-开启微信客服功能参考:https://baijiahao.baidu.com/s?id=1735577920604728565&wfr=spider&for=pc、 https://blog.csdn.net/weixin_42065713/article/details/126137884?from_wecom=1 (1)登录【企业微信管理后台】选择【应用管理】【微信客服】,开启【微信客服】旁边的按钮; 注意:若需要后台对接客服信息的话需要开启“通过API管理微信客服”的,若不需要则不开启。 [图片] (2)然后在【客服账号】一栏点击【创建账号】来指定接待人员;创建客服账号时,企业管理员可以选择展示的视频号,设置接待人员、接待规则、接待上限、接待时间、智能回复、超时结束聊天等内容。 [图片] [图片] (3)选择【接入场景】 在这里我们选择【在微信内其他场景接入】,进入页面后点击【去接入】。企业管理员可以选择需要配置的客服账号,复制客服链接后可以配置到以下场景:在网页接入、在公众号菜单接入、在小程序接入、在搜一搜品牌官方区接入、点击微信支付凭证接入。接入后,客户点击客服入口即可发起咨询。 [图片] [图片] (4)可以在【服务工具】找到相应的配置设置客服的自动回复 [图片] 在小程序中添加打开微信客服的点击事件,调用「wx.openCustomerServiceChat」接口完成接入。 export default { methods: { // 跳转微信客服 jumpToWeChatCustomerService() { openWeChatCustomerService("https://work.weixin.qq.com/xxxxx", "wwed1ca4d3597eXXXX"); }, // 打开微信客服 openWeChatCustomerService ( weiXinCustomerServiceUrl = "", corpId = "", showMessageCard = false, sendMessageTitle = "", sendMessagePath = "", sendMessageImg = "" ) { if (!weiXinCustomerServiceUrl || !corpId) return Toast("请配置好客服链接或者企业ID"); // eslint-disable-next-line no-undef wx.openCustomerServiceChat({ // 客服信息 extInfo: { url: weiXinCustomerServiceUrl, // 客服链接 https://work.weixin.qq.com/xxxxxxxx }, corpId, // 企业ID wwed1ca4d3597eXXXX showMessageCard, // 是否发送小程序气泡消息 sendMessageTitle, // 气泡消息标题 sendMessagePath, // 气泡消息小程序路径(一定要在小程序路径后面加上“.html”,如:pages/index/index.html) sendMessageImg, // 气泡消息图片 success(res) { console.log("success", JSON.stringify(res)); }, fail(err) { console.log("fail", JSON.stringify(err)); // eslint-disable-next-line no-undef return wx.showToast({ title: err.errMsg, icon: "none" }); }, }); }, }, } 常见错误:(1)"fail" "{"errCode":1,"errMsg":"openCustomerServiceChat:fail invalid param: url"}" [图片] 原因是:属性extInfo拼错了。 解决方法:将extInfo属性名写对即可。 (2)sendMessagePath属性设置的小程序绝对路径后,在微信客服消息的气泡消息点击打开会提示“页面不存在”。 在小程序内正常访问路径如:“pages/index/index”是可以访问成功的,但如果在sendMessagePath属性设置该路径的话,在微信客服消息的气泡消息点击打开会提示“页面不存在”。 [图片] [图片] [图片] 解决方法:在小程序文件路径后面加上“.html”即可,如“pages/index/index.html”或者“/pages/index/index.html”都可 [图片]
2023-09-05 - 微信小程序自动化框架minium实践
一、背景需求精选小程序发生了一次线上问题,测试阶段的小程序开发码测试ok,但是小程序正式码由于打包问题,"我的订单"页面文件打包失败,导致线上用户访问我的页面白屏。 当前并不能避免该打包问题,为了规避异常版本发布至线上,需要在预发、体验码发布、正式码发布等各阶段进行主流程回归。手动回归测试非常耗时,在发布前的各阶段,测试人员须重复执行大量测试用例,以确保本次上线功能OK且对其他功能无影响。 一遍又一遍执行相同的测试用例,不仅要花费更多的时间,而且还会降低整体测试效率,因此引入微信小程序自动化以解放重复人力。 二、调研1.Jest+小程序SDK优点: 小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的,小程序自动化 SDK 本身不提供测试框架。这意味着你可以将它与市面上流行的任意 Node.js 测试框架结合使用;jest 是facebook推出的一款测试框架,集成了 Mocha,chai,jsdom,覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架;缺点: 语言仅支持JavaScript 编写;使用中遇到问题,网上相关资料比较少;2.minium框架优点: 微信小程序官方推出的小程序自动化框架,是为小程序专门开发的自动化框架, 提供了 Python 和 JavaScript 版本。支持一套脚本,iOS & Android & 模拟器,三端运行提供丰富的页面跳转方式,看不到也能去得到可以获取和设置小程序页面数据,让测试不止点点点支持往 AppSerive 注入代码片段可以使用 minium 来进行函数的 mock, 可以直接跳转到小程序某个页面并设置页面数据, 做针对性的全面测试缺点: 暂不支持H5页面的调试;暂不支持插件内wx接口调用;3.选型精选小程序主要是原生页面,minium和Jest均能满足需求。minium支持Python 和 JavaScript 版本,而且有专门的团队定期维护,遇到问题可以在微信开发者社区进行提问,因此选择了minium。 三、minium介绍minium提供一个基于unittest封装好的测试框架,利用这个简单的框架对小程序测试可以起到事半功倍的效果。 测试基类Minitest会根据测试配置进行测试,minitest向上继承了unittest.TestCase,并做了以下改动: 加载读取测试配置在合适的时机初始化minium.Minium、minium.App和minium.Native根据配置打开IDE,拉起小程序项目和或自动打开真机调试拦截assert调用,记录检验结果记录运行时数据和截图,用于测试报告生成使用MiniTest可以大大降低小程序测试成本。 Properties: 名称类型默认值说明appminium.AppNoneApp实例,可直接调用minium.App中的方法miniminium.MiniumNoneMinium实例,可直接调用minium.Minium中的方法nativeminium.NativeNoneNative实例,可直接调用minium.Native中的方法 代码示例: #!/usr/bin/env python3 import minium class FirstTest(minium.MiniTest): def test_get_system_info(self): sys_info = self.mini.get_system_info() self.assertIn("SDKVersion", sys_info) 四、环境搭建安装minium-doc,这个是小程序安装和使用的文档介绍,或者不用自己本地安装直接访问官方文档安装python 3.8及以上安装微信开发者工具(我本机使用的版本是1.05.2103200),并打开安全模式: 设置 -> 安全设置 -> 服务端口: 打开在工具栏菜单中点击设置,选择项目设置,切换到“本地设置”,将调试基础库选择大于2.7.3的库; [图片] 下载minium安装包并安装,地址参考官网安装命令:pip3 install minium-latest.zip 或者python3 setup.py install 安装完成后,可执行以下命令查看版本:minitest -v 开启微信工具安全设置中的 CLI/HTTP (提供了命令行和HTTP两种调用方式)调用功能。在开发者工具的设置 -> 安全设置中开启服务端口。 [图片] 开启微信工具安全设置中的 CLI/HTTP (提供了命令行和HTTP两种调用方式)调用功能。在开发者工具的设置 -> 安全设置中开启服务端口。开启被测试项目的自动化端口号"path/to/cli" auto --project "path/to/project" --auto-port 9420 默认的命令行工具所在位置: macOS: <安装路径>/Contents/MacOS/cli Windows: <安装路径>/cli.bat 五、小程序脚本编写思路:使用Page Object 架构,使系统架构分层,每一个页面设计为一个Class,包含了页面需要测试的元素,测试用例只要关心测试的数据即可;1.目录结构[图片] cases/: 存放测试脚本和用例case/base/:页面公共方法case/pages/:页面对象模型outputs/:测试报告test/:测试脚本route.py:小程序页面操作的路径2.自动化脚本BasePage是页面基类,封装所有页面会用到的公用方法class BasePage: def __init__(self, mini): self.mini = mini def navigate_to_open(self, route): """以导航的方式跳转到指定页面,不允许跳转到 tabbar 页面,支持相对路径和绝对路径, 小程序中页面栈最多十层""" self.mini.app.navigate_to(route) def redirect_to_open(self, route): """关闭当前页面,重定向到应用内的某个页面,不允许跳转到 tabbar 页面""" self.mini.app.redirect_to(route) def switch_tab_open(self, route): """跳转到 tabBar 页面,会关闭其他所有非 tabBar 页面""" self.mini.app.switch_tab(route) @property def current_title(self) -> str: """获取当前页面 head title, 具体项目具体分析,以下代码仅用于演示""" return self.mini.page.get_element("XXXXXX").inner_text def current_path(self) -> str: """获取当前页面route""" return self.mini.page.path HomePage是要测试的精选首页页面from case.base.basepage import BasePage from case.base import route class HomePage(BasePage): """小程序首页公共方法""" locators = { "BASE_ELEMENT": "view", "BASE_BANNER": "首页banner元素选择器XXX" } # 首页点击官方补贴的"更多"按钮 subsidy_more_button = ("跳转页面的元素选择器XXX", "更多") """ 校验页面路径 """ def check_homepage_path(self): self.mini.assertEqual(self.current_path(), route.homepage_route) """ 校验页面的基本元素 """ def check_homepage_base_element(self): # 校验页面是否包含view元素 self.mini.assertTrue(self.mini.page.element_is_exists(HomePage.locators['BASE_ELEMENT'])) # 校验页面banner位置 self.mini.assertTrue(self.mini.page.element_is_exists(HomePage.locators['BASE_BANNER'])) """ 获取官方补贴,点击"更多"按钮跳转 """ def get_subsidy_element(self): self.mini.page.get_element(str(self.subsidy_more_button[0]), inner_text=str(self.subsidy_more_button[1])).click() BaseCase是测试用例基类,用于设置用例输出路径和清理工作,项目的测试用例都继承此类from pathlib import Path import minium class BaseCase(minium.MiniTest): """测试用例基类""" @classmethod def setUpClass(cls): super(BaseCase, cls).setUpClass() output_dir = Path(cls.CONFIG.outputs) if not output_dir.is_dir(): output_dir.mkdir() @classmethod def tearDownClass(cls): super(BaseCase, cls).tearDownClass() cls.app.go_home() def setUp(self): super(BaseCase, self).setUp() def tearDown(self): super(BaseCase, self).tearDown() 3.元素定位的方法minium 通过 WXSS 选择器来定位元素的,目前小程序仅支持以下的选择器: [图片] 参考例子: 假如要查找像上面这一个元素的话,他的选择器会像是下面这样: tageName + #id + .className view#main.page-section.page-section-gap tagName :类型选择器,标签名称,view、checkbox 等等,选择所有指定类型的最简单方式。id:ID 选择器,自定义给元素的唯一 ID,使用时前面跟着 # 号,这是选择单个元素的最有效的方式。className:类选择器,由一个点.以及类后面的类名组成,存在多个类的时候可以以点为间隔一直拼接下4.编写精选首页的测试用例被测试的有赞精选小程序首页如下图:[图片] HomePageTest # coding=utf-8 from case.base import loader from case.base.basecase import BaseCase from case.pages.homepage import HomePage """ 小程序首页测试 """ class HomePageTest(BaseCase): def __init__(self, methodName='runTest'): super(HomePageTest, self).__init__(methodName) self.homePage = HomePage(self) """ case1:测试首页的跳转路径是否正确,跳转路径要使用绝对路径,小程序默认进入就是首页,所以不用再切换进入的路径 """ def test_01_home_page_path(self): self.homePage.check_homepage_path() """ case2:页面的基本元素是否存在 """ def test_02_page_base_element(self): self.homePage.check_homepage_base_element() """ case3:检查首页的"官方补贴"模块存在 """ def test_03_live_sale(self): self.assertTexts(["官方补贴"], "view") self.assertTexts(["轻松赚回早餐钱"], "view") """ case4:从首页点击"更多"跳转到直播特卖页面,页面包含"推荐"模块 """ def test_04_open_live_sale(self): # 点击首页的"更多"按钮的元素 self.homePage.get_subsidy_element() self.page.wait_for(2) result = self.page.wait_for("页面元素选择器xxx") # 等待页面渲染完成 if result: category = self.page.data['categoryList'] self.assertEquals("美食", category[0]['title'], "接口返回值包含美食模块") self.assertEquals("美妆", category[1]['title'], "接口返回值包含美妆模块") self.page.wait_for(2) self.app.go_home() if __name__ == "__main__": loader.run(module="case.homepage_test", config="../config.json", generate_report=True) 5.编辑配置文件config.json{ "project_path": "XXXXX", "dev_tool_path": "/Applications/wechatwebdevtools.app/Contents/MacOS/cli", "debug_mode": "debug", "test_port": 9420, "platform": "ide", "app": "wx", "assert_capture": false, "request_timeout":60, "remote_connect_timeout": 300, "auto_relaunch": true } 6.minitest 命令行minium安装时执行的setup.py文件,指定了minitest命令运行的方法入口为:minium.framework.loader:main [图片] loader.py文件解释了运行的命令行的含义 [图片] -h, --help: 使用帮助。-v, --version: 查看 minium 的版本。-p PATH/--path PATH: 用例所在的文件夹,默认当前路径。-m MODULE_PATH, --module MODULE_PATH: 用例的包名或者文件名--case CASE_NAME: test_开头的用例名-s SUITE, --suite SUITE:测试计划文件-c CONFIG, --config CONFIG:配置文件名,配置项目参考配置文件-g, --generate: 生成网页测试报告--module_search_path [SYS_PATH_LIST [SYS_PATH_LIST ...]]: 添加 module 的搜索路径-a, --accounts: 查看开发者工具当前登录的多账号, 需要通过 9420 端口,以自动化模式打开开发者工具--mode RUN_MODE: 选择以parallel(并行)或者fork(复刻)的方式运行用例7.suite测试计划文件{ "pkg_list": [ { "case_list": [ "test_*" ], "pkg": "case.*_test" } ] } suite.json的pkglist字段说明要执行用例的内容和顺序,pkglist 是一个数组,每个数组元素是一个匹配规则,会根据pkg去匹配包名,找到测试类,然后再根据case_list里面的规则去查找测试类的测试用例。可以根据需要编写匹配的粒度。注意匹配规则不是正则表达式,而是通配符。 8.命令行运行脚本minitest -m case.homepage_test --case test_07_open_live_sale -c config.json -g #运行执行class文件中的指定用例test_07_open_live_sale minitest -s suite.json -c config.json -g #按照suite配置去执行用例 9.生成测试报告生成报告之后,在对应的目录下面有index.html文件,但是我们不能直接用浏览器打开这个 文件,需要把这个目录放到一个静态服务器上 测试结果存储在outputs下,运行命令python3 -m http.server 12345 -d outputs然后在浏览器上访问http://localhost:12345即可查看报告 六、遇到的问题1.需要开启被测试小程序应用的自动化测试端口9420 [图片] 开启被测试工程的自动化端口 "path/to/cli" auto --project "path/to/project" --auto-port 9420 2.打开微信开发者工具超时 [图片] 微信开发者工具:设置-代理设置,关闭ide的代理 [图片] 3.连接开发者工具后报错 原因:可能是微信开发者工具和minium的版本不一致; 我测试使用ok的匹配版本为: Minium版本:1.0.5 开发者工具版本:1.05.2102010 python版本:3.8.8 4.出现以下报错,可能是登陆的开发者工具的账号,没有被测试小程序的开发者权限; [图片] 5.运行过程中,发现调用截图的方法比较耗时,但是在config文件设置了"assert_capture": false,配置没生效,仍然会去调用截图的方法; [图片] ps:猜测是一个bug,然后给微信社区留言了,最新版本1.0.6修复了这个问题 原因:是框架的minitest.py文件调用setup和TearDown方法的时候,没有判断配置文件"assert_capture": false这个条件 [图片] 可以修改minitest.py文件,增加配置文件的判断条件,修改如下: if self.test_config.assert_capture: self.capture("setup") 6.命令行执行的时候加了-p xxx参数,运行时报引入的包不存在 [图片] 原因:命令行运行时默认是当前路径,加-p xxx, 这样会导致脚本运行的PYTHONPATH变了(不是当前目录了),这样会导致包不存在 [图片] 解决方法: 命令行运行的时候,用-m 指定运行的包路径,不用-p把-p xxx用到的路径都加入到PYTHONPATH中七、参考资料 微信官方文档简书上Rethink的相关文章介绍
2022-06-23 - iOS的safari浏览器中怎样拉起小程序?
现在的方案: 尝试使用URL Link的方式,后端返回wxaurl,前端 location.href = 'https://wxaurl.cn/*TICKET*' 表现: 现在safari会提示 'Safari浏览器打不开该网页,因为网址无效'。 但是如果手机上安装的有我们的某个应用(就叫做应用A吧),会提示要不要打开该应用,然后可以打开微信小程序。 这个wxaurl的地址,在iOS的扫码或者短信里边点击都是可以直接打开小程序的。 想请问下有朋友知道这是什么问题,怎么解决吗? 也尝试了URL Scheme的方式,按照文档,location.href = 'weixin://dl/business/?t= *TICKET*',是一样的表现
2022-01-14 - 【技巧】利用canvas生成朋友圈分享海报
前言 大家好,上次给大家讲了函数防抖和函数节流 https://developers.weixin.qq.com/community/develop/article/doc/000a645d8b8ba0d8722863ef45bc13 今天给大家分享一下利用canvas生成朋友圈分享海报 由于小程序的限制,我们不能很方便地在微信内直接分享小程序到朋友圈,所以普遍的做法是生成一张带有小程序分享码的分享海报,再将海报保存到手机相册,有两种方法可以生成分享海报,第一种是让后台生成然后返回图片链接,这一种方法比较简单,只需要传后台所需要的参数就行了,今天给大家介绍的是第二种方法,用canvas生成分享海报。 效果 [图片] 主要步骤 把海报样式用标签先写好,方便画图时可以比对 用canvas进行画图,canvas要注意定好宽高 canvas利用wx.canvasToTempFilePath这个api将canvas转化为图片 将转化好的图片链接放入image标签里 再利用wx.saveImageToPhotosAlbum保存图片 坑点 用canvas进行画图的时候要注意画出来的图的大小一定要是你用标签写好那个样式的两倍大小,比如你的海报大小是400600的大小,那你用canvas画的时候大小就要是8001200,宽高可以写在样式里,如果你画出来的图跟你海报图是一样的大小的话生成的图片是会很模糊的,所以才需要放大两倍。 画图的时候要注意尺寸的转化,如果你是用rpx做单位的话,就要对单位进行转化,因为canvas提供的方法都是经px为单位的,所以这一点要注意一下,px转rpx的公式是w/750z2,w是手机屏幕宽度screenWidth,可以通过wx.getSystemInfo获取,z是你需要画图的单位,2就是乘以两倍大小。 图片来源问题,因为canvas不支持网络图片画图,所以你的图片要么是固定的,如果不是固定的,那就要用wx.downloadFile下载后得到一个临时路径才行 小程序码问题,小程序需要后台请求接口后返回一个二进制的图片,因为二进制图片canvas也是不支持的,所以也是要用wx.downloadFile下载后得到一个临时路径,或者可以叫后台直接返回一个小程序码的路径给你 这里保存的时候是有个授权提醒的,如果拒绝的话再次点击就没有反应了,所以这里我做了一个判断是否有授权的,如果没有就弹窗提醒,确认的话会打开设置页面,确认授权后再次返回就行了,这里有个坑注意下,就是之前拒绝后再进入设置页面确认授权返回页面时保存图片会不成功,官方还没解决,我是加了个setTimeOut处理的,详情可以看这里 https://developers.weixin.qq.com/community/develop/doc/000c46600780f0fa68d7eac345a400 代码实现 [图片] 这里图片我先用的是网上的链接,实际项目中是后台返回的数据,这个可以自行处理,这里只是为了演示方便,生成临时路径的方法我这里是分别定义了一个方法,其实可以合成一个方法的,只是生成小程序码时如果要传入参数要注意一下。 绘图方法是drawImg,这里截一部分,详细的可以看代码片段 [图片] 不足 由于在实际项目中返回的图片宽高是不固定的,但是canvas画出来的又需要固定宽高,所以分享图会有图片变形的问题,使用drawImage里的参数也不能解决,如果各位有比较好的方案可以一起讨论一下。 代码片段 https://developers.weixin.qq.com/s/3pcsjDmS7M5Y
2019-02-22 - 边框 border设置1rpx 在iPhoneX上面显示不完整
边框 border设置1rpx 在iPhoneX上面显示不完整 [图片] .count input { width: 260rpx; height: 70rpx; line-height: 70rpx; font-size: 26rpx; border: solid 1rpx #dbdbdb; padding: 0rpx 25rpx; color: #626262; text-align: center; } 另外1rpx在iOS和android端显示的宽度不一致的问题什么时候能解决? 麻烦官方给个解决方案
2018-05-09 - web-view 跳到外部链接提示 不支持打开非业务域名?
web-view组件,跳到新浪新闻提示 不支持打开非业务域名。按官方的指引,怎么把校验文件放到服务器域名根目录下,那可是人家的服务器。莫非,微信不允许跳到别人的链接,文档也没清楚说明。
2022-05-07 - Painter 一款轻量级的小程序海报生成组件
生成海报相信大家有的人都做过,但是canvas绘图的坑太多。大家可以试试这个组件。然后附上楼下大哥做的可视化拖拽生成painter代码的工具:链接地址https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813
2019-09-27 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 小程序性能和体验优化方法
[图片] 小程序应避免出现任何 JavaScript 异常 出现 JavaScript 异常可能导致小程序的交互无法进行下去,我们应当追求零异常,保证小程序的高鲁棒性和高可用性 小程序所有请求应响应正常 请求失败可能导致小程序的交互无法进行下去,应当保证所有请求都能成功 所有请求的耗时不应太久 请求的耗时太长会让用户一直等待甚至离开,应当优化好服务器处理时间、减小回包大小,让请求快速响应 避免短时间内发起太多的图片请求 短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术或在屏幕外的图片使用懒加载 避免短时间内发起太多的请求 短时间内发起太多请求会触发小程序并行请求数量的限制,同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等 避免 setData 的数据过大 setData工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 常见的 setData 操作错误 频繁的去 setData Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层 染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时 每次 setData 都传递大量新数据 由setData的底层实现可知,数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程 后台态页面进行 setData 当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行 避免 setData 的调用过于频繁 setData接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 避免将未绑定在 WXML 的变量传入 setData setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗 合理设置可点击元素的响应区域大小 我们应该合理地设置好可点击元素的响应区域大小,如果过小会导致用户很难点中,体验很差 避免渲染界面的耗时过长 渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 避免执行脚本的耗时过长 执行脚本的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要确认并优化脚本的逻辑 对网络请求做必要的缓存以避免多余的请求 发起网络请求总会让用户等待,可能造成不好的体验,应尽量避免多余的请求,比如对同样的请求进行缓存 wxss 覆盖率较高,较少或没有引入未被使用的样式 按需引入 wxss 资源,如果小程序中存在大量未使用的样式,会增加小程序包体积大小,从而在一定程度上影响加载速度 文字颜色与背景色搭配较好,适宜的颜色对比度更方便用户阅读 文字颜色与背景色需要搭配得当,适宜的颜色对比度可以让用户更好地阅读,提升小程序的用户体验 所有资源请求都建议使用 HTTPS 使用 HTTPS,可以让你的小程序更加安全,而 HTTP 是明文传输的,存在可能被篡改内容的风险 不使用废弃接口 使用即将废弃或已废弃接口,可能导致小程序运行不正常。一般而言,接口不会立即去掉,但保险起见,建议不要使用,避免后续小程序突然运行异常 避免过大的 WXML 节点数目 建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长 避免将不可能被访问到的页面打包在小程序包里 小程序的包大小会影响加载时间,应该尽量控制包体积大小,避免将不会被使用的文件打包进去 及时回收定时器 定时器是全局的,并不是跟页面绑定的,当页面因后退被销毁时,定时器应注意手动回收 避免使用 css ‘:active’ 伪类来实现点击态 使用 css ‘:active’ 伪类来实现点击态,很容易触发,并且滚动或滑动时点击态不会消失,体验较差 建议使用小程序内置组件的 ‘hover-*’ 属性来实现 滚动区域可开启惯性滚动以增强体验 惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 [代码]-webkit-overflow-scrolling: touch[代码] 的样式
2019-03-15 - 云开发入门
重磅打造的小程序学习路径课,从微信小程序到微信云开发体系化的学习,带来更加顺畅的学习体验。
2021-11-19 - 组件page-container的基本使用和遇到的一些问题
page-container是微信小程序的一个视图容器,主要的功能就是实现一个弹窗的功能,具体效果如下 [图片] 该组件所包含的属性不多,比较关键的属性就是显示容器和容器位置的属性以及各种事件触发函数。如下 [图片][图片][图片] show主要通过一些page-container外的事件触发后更改将show的值改为true,从而显示page-container组件。 position属性控制着组件弹出的位置,但是属性的值bottom,top与right,center的显示效果不同。 bottom和top只是在当前页面中显示出一个弹窗,而right和center是相当于直接载入了一个子页面,具体效果如下 [图片] [图片] [图片] right和center的最终效果一样只是position="right"下的页面是从右往左弹出而已。 组件的几个绑定函数说明也比较通俗易懂,主要就是在进入这个组件以及离开组件的过程中触发的函数,在编写多个触发函数时,需要注意所需要使用的数据有没有被后面执行的触发函数覆盖掉的问题。 .js Page({ /** * 页面的初始数据 */ data: { show: false, text: '文本', }, showPage: function(e){ this.setData({show: true}); }, beforeEnter: function(e){ this.setData({text: '函数beforeEnter'}); }, enter: function(e) { this.setData({text: '函数enter'}); }, afterEnter: function(e){ this.setData({text: '函数afterEnter'}); }, }) .wxml <view class="test" bindtap="showPage">展示page-container</view> <page-container show="{{show}}" round="true" bind:beforeenter="beforeEnter" bind:enter="enter" bind:afterenter="afterEnter"> <view class="test"> {{text}} </view> </page-container> [图片] 最后输出的是"函数afterEnter"是因为事件触发函数的执行顺序问题,离开的触发函数同理 最后说一说我在使用该组件进行开发中遇到的一些问题及总结。 我在实际开发过程中遇到了一个需求,从一个页面跳转到另一个页面进行操作,操作完成后使用page-container组件弹出操作成功的信息,随后使用wx.navigateBack这个API回到上一界面,起初我是想在触发这个组件显示的那个函数中实现这个功能,具体的操作大致可以简化成这样(还是以上面的代码片段为例) .js showPage: function(e){ this.setData({show: true}); setTimeout(function(){ wx.navigateBack({ delta: 1, }); },2000); }, 这里使用定时器的原因是如果我直接进行路由的跳转将不会显示出page-container的弹窗,但是使用了定时器后会出现跳转失败,如果定时器结束后还没有关闭弹窗则会跳转失败,只会关闭弹窗但是无法回到上一页面,无论delta等于几都是一样,只有在定时器结束前关闭组件的弹窗才可以跳转成功。 针对这一问题,我的解决方法是写一个函数绑定bind:afterleave,在用户自己关闭弹窗后由程序判断是否需要跳转回上一界面。 .js Page({ /** * 页面的初始数据 */ data: { show: false, text: '文本', exit: false,//判断是否推出 }, showPage: function(e){ this.setData({show: true, exit: true}); }, afterLeave: function(e){ if(this.data.exit){ wx.navigateBack({ delta: 1, }) } }, }) .wxml <view class="test" bindtap="showPage">展示page-container</view> <page-container show="{{show}}" round="true" bind:afterleave="afterLeave"> <view class="test"> {{text}} </view> </page-container> 大致代码就是这样,根据不同的需求具体的逻辑也会不同。补充一句,使用wx.redirectTo等路由API不会出现问题。 以上就是我这几天对page-container的学习以及遇到的问题,如有不足和错误还请不吝赐教。
2021-11-11 - wx.enableAlertBeforeUnload只有在非自定义标题栏的时候才会触发?
wx.enableAlertBeforeUnload 在非自定义标题栏时(navigationStyle为custom) 不会触发弹框 请官方帮忙看看 急!!!
2022-01-05 - 自定义tabbar 【恋爱小清单开发总结】
看官方demo的小伙伴知道,自定义tabbar需要在小程序根目录底下建一个名叫custom-tab-bar的组件(我有试过,如果放在components目录里面小程序会识别不了),目前我自己实现的效果是:通过在配置可以切换tab,也可以点击tab后重定向到新页面,支持隐藏tabbar,同时也可以显示右上角文本和小红点。 官方demo里面用的是cover-view,我改成view,因为如果页面有弹窗的话我希望可以盖住tabbar 总结一下有以下注意点: 1、tabbar组件的目录命名需要是custom-tab-bar 2、app.json增加自定义tabbar配置 3、wx.navigateTo不允许跳转到tabb页面 4、进入tab页面时,需要调用tabbar.js手动切换tab 效果图: [图片] 可以扫码体验 [图片] 代码目录如下: [图片] 代码如下: app.json增加自定义tabbar配置 "tabBar": { "custom": true, "color": "#7A7E83", "selectedColor": "#3cc51f", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/love/love", "text": "首页" }, { "pagePath": "pages/tabbar/empty", "text": "礼物说" }, { "pagePath": "pages/tabbar/empty", "text": "恋人圈" }, { "pagePath": "pages/me/me", "text": "我" } ] }, 自定义tabbar组件代码如下 index.js //api.js是我自己对微信接口的一些封装 const api = require('../utils/api.js'); //获取应用实例 const app = getApp(); Component({ data: { isPhoneX: false, selected: 0, hide: false, list: [{ showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/love/love", iconPath: "/images/tabbar/home.png", selectedIconPath: "/images/tabbar/home-select.png", text: "首页" }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/gifts/giftList", iconPath: "/images/tabbar/gift.png", selectedIconPath: "/images/tabbar/gift-select.png", text: "礼物说", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/moments/moments", iconPath: "/images/tabbar/lover-circle.png", selectedIconPath: "/images/tabbar/lover-circle-select.png", text: "恋人圈", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/me/me", iconPath: "/images/tabbar/me.png", selectedIconPath: "/images/tabbar/me-select.png", text: "我" }] }, ready() { // console.error("custom-tab-bar ready"); this.setData({ isPhoneX: app.globalData.device.isPhoneX }) }, methods: { switchTab(e) { const data = e.currentTarget.dataset; console.log("tabBar参数:", data); api.vibrateShort(); if (data.hideTabBar) { api.navigateTo(data.navigatePath); } else { /*this.setData({ selected: data.index }, function () { wx.switchTab({url: data.path}); });*/ /** * 改为直接跳转页面, * 因为发现如果先设置selected的话, * 对应tab图标会先选中,然后页面再跳转, * 会出现图标变成未选中然后马上选中的过程 */ wx.switchTab({url: data.path}); } }, /** * 显示tabbar * @param e */ showTab(e){ this.setData({ hide: false }, function () { console.log("showTab执行完毕"); }); }, /** * 隐藏tabbar * @param e */ hideTab(e){ this.setData({ hide: true }, function () { console.log("hideTab执行完毕"); }); }, /** * 显示小红点 * @param index */ showRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = true; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏小红点 * @param index */ hideRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = false; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 显示tab右上角文本 * @param index * @param text */ showBadge(index, text, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: true, badgeText: text}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏tab右上角文本 * @param index */ hideBadge(index, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: false, badgeText: ""}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } } } }); index.html <view class="footer-tool-bar flex-center {{isPhoneX? 'phx_68':''}}" hidden="{{hide}}"> <view class="tab flex-full {{selected === index ? 'focus':''}}" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" data-navigate-path="{{item.navigatePath}}" data-hide-tab-bar="{{item.hideTabBar}}" data-open-ext-mini-program="{{item.openExtMiniProgram}}" data-ext-mini-program-app-id="{{item.extMiniProgramAppId}}" bindtap="switchTab"> <view class="text"> <view class="dot" wx:if="{{item.showRedDot}}"></view> <view class="badge" wx:if="{{item.showBadge}}">{{item.badgeText}}</view> <image class="icon" src="{{item.selectedIconPath}}" hidden="{{selected !== index}}"></image> <image class="icon" src="{{item.iconPath}}" hidden="{{selected === index}}"></image> </view> </view> </view> index.json { "component": true, "usingComponents": {} } index.wxss @import "/app.wxss"; .footer-tool-bar{ background-color: #fff; height: 100rpx; width: 100%; position: fixed; bottom: 0; z-index: 100; text-align: center; font-size: 24rpx; transition: transform .3s; border-radius: 30rpx 30rpx 0 0; /*padding-bottom: env(safe-area-inset-bottom);*/ box-shadow:0rpx 0rpx 18rpx 8rpx rgba(212, 210, 211, 0.35); } .footer-tool-bar .tab{ color: #242424; height: 100%; line-height: 100rpx; } .footer-tool-bar .focus{ color: #f96e49; font-weight: 500; } .footer-tool-bar .icon{ width: 44rpx; height: 44rpx; margin: 18rpx auto; } .footer-tool-bar .text{ line-height: 80rpx; height: 80rpx; position: relative; display: inline-block; padding: 0rpx 40rpx; box-sizing: border-box; margin: 10rpx auto; } .footer-tool-bar .dot{ position: absolute; top: 16rpx; right: 16rpx; height: 16rpx; width: 16rpx; border-radius: 50%; background-color: #f45551; } .footer-tool-bar .badge{ position: absolute; top: 8rpx; right: 8rpx; height: 30rpx; width: 30rpx; line-height: 30rpx; border-radius: 50%; background-color: #f45551; color: #fff; text-align: center; font-size: 20rpx; font-weight: 450; } .hide{ transform: translateY(100%); } app.wxss(这里的样式文件是我用来存放一些公共样式) /**app.wxss**/ page { background-color: #f5f5f5; height: 100%; -webkit-overflow-scrolling: touch; } .container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box; } .blur { filter: blur(80rpx); opacity: 0.65; } .flex-center { display: flex; align-items: center; justify-content: center; } .flex-column { display: flex; /*垂直居中*/ align-items: center; /*水平居中*/ justify-content: center; flex-direction: column; } .flex-start-horizontal{ display: flex; justify-content: flex-start; } .flex-end-horizontal{ display: flex; justify-content: flex-end; } .flex-start-vertical{ display: flex; align-items: flex-start; } .flex-end-vertical{ display: flex; align-items: flex-end; } .flex-wrap { display: flex; flex-wrap: wrap; } .flex-full { flex: 1; } .reset-btn:after { border: none; } .reset-btn { background-color: #ffffff; border-radius: 0; margin: 0; padding: 0; overflow: auto; } .loading{ opacity: 0; transition: opacity 1s; } .load-over{ opacity: 1; } .phx_68{ padding-bottom: 68rpx; } .phx_34{ padding-bottom: 34rpx; } 另外我还对tabbar的操作做了简单的封装: tabbar.js const api = require('/api.js'); /** * 切换tab * @param me * @param index */ const switchTab = function (me, index) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { console.log("切换tab:", index); me.getTabBar().setData({ selected: index }) } }; /** * 显示 tabBar 某一项的右上角的红点 * @param me * @param index */ const showRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showRedDot(index, success, fail); } }; /** * 隐藏 tabBar 某一项的右上角的红点 * @param me * @param index */ const hideRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideRedDot(index, success, fail); } }; /** * 显示tab右上角文本 * @param me * @param index * @param text */ const showBadge = function (me, index, text, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showBadge(index, text, success, fail); } }; /** * 隐藏tab右上角文本 * @param me * @param index */ const hideBadge = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideBadge(index, success, fail); } }; /** * 显示tabbar * @param me * @param success */ const showTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showTab(success); } }; /** * 隐藏tabbar * @param me * @param success */ const hideTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideTab(success); } }; module.exports = { switchTab, showRedDot, hideRedDot, showBadge, hideBadge, showTab, hideTab }; 最后,进入到tab对应页面的时候要手动调用一下swichTab接口,然tabbar聚焦到当前tab /** * 生命周期函数--监听页面显示 */ onShow: function () { tabbar.switchTab(this, this.data.tabIndex);//tabIndex是当前tab的索引 }
2021-11-09 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 关于video控件播放在线视频黑屏,MEDIA_ERR_NETWORK错误原因的可能性
背景: 业务场景存在后台上传视频至华为云私有云上,小程序端中video控件的src调用接口对在线资源请求文件流; IOS端表现:video控件播放在线视频时,资源体积约在10M时,界面黑屏无法加载资源; Android端表现:video控件播放在线视频无异常; 可能性列表: 视频压缩率和编码的可能性;视频大小的可能性;视频资源路径中文的可能性;后台资源请求失败的可能性;经过排查视频压缩率符合ios能接受的范围、视频转换编码、第三方大型在线视频、确认文件在私有云的链接不包含中文。 在对第四个可能性排查过程中,我也发现我的视频获取方式和论坛的同类型的例子中的不大一样。 1)论坛中多为上传到公有云,直接调用的视频的网络路径,或是云上传后进行调用; 问题可能与私有云有关,在请教后端的同事后得知,我们是通过Tomcat对私有云资源进行缓存,再讲缓存的资源返回到客户端,在Tomcat缓存的过程中,由于文件过大,文件流出现异常,这也解释了为何小体积的视频可以正常播放,而约10M时就出现了问题。 解决方案: 1、最直接的解决方案是将资源上传到公有云中,直接调用网络地址; 2、对于基于私有云的解决方案,后端同事提出了一种解决方案:对私有云的资源授权,获取文件的临时路径; 由于业务存在敏感数据,我们后面选择了两种方案混合。 如果你也遇到了MEDIA_ERR_NETWORK这一报错问题,希望我的上面的文章能帮助到你;
2021-03-18 - 如何做微信小程序开发前期准备
本文分享如何微信小程序开发前期准备,给新接触的开发者做开发前期指引。小程序有别于我们普通网站或自己公司的app,不能产品的需求来了就开始直接研发,前期需要不少准备工作要处理。要求小程序开发者本身不仅要动技术,还需要懂平台运营规范。公司需求分享如何做开发一个小程序前期准备工作,顺便分享到社区。涉及注册到前期基础的技术准备的思维方式,人员分工。 1、注册 说明 (1) 有两种方式注册一个小程序,第一种通过已有公众号快速关联注册,第二种通过线上常规流程完成注册。如果有认证的公众号,建议优先使用快速关联注册。 (2) 填写未注册过公众平台、开放平台、企业号、未绑定个人号的邮箱。 (3) 快速关联注册会复用公众号的主体信息,主体迁移麻烦。 (4) 一个主体可以注册的账号个数是50。主子公司主体的小程序可以做关联来打通信息,但是会占用名额。 (5) 小程序账号和公众号是独立的账号。 (6) 快速注册只能针对已认证的小程序。 快速注册 (1) 登录公众号》小程序》小程序管理》添加》快速注册并认证 (2) 同意快速创建协议》管理员扫码 (3) 资质确认》管理员扫码确认 (4) 输入邮箱》激活 (5) 管理员可以用邮箱登录 正常注册流程 (1) 选择【小程序】账号类型 (2) 输入邮箱,然后登录邮箱,点激活链接 (3) 选择【企业】类型的主体 (4) 选择认证方式【对公账户小额打款】或【微信支付300元认证费】 (5) 【对公账户小额打款】,提交后有打款信息,10天内向指定账号打款 (6) 【微信支付300元认证费】 (7) 注册完成,邮箱及管理员微信扫码即可登录 参考文档: https://developers.weixin.qq.com/community/business/doc/000200772f81508894e94ec965180d 2、 信息完善及开发准备 基本信息 (1)名称、logo和简介:相互之间应存在关联,避免相互之间缺乏相关表达的联系,造成用户对该小程序实际提供的功能或服务范围的混淆。 (2)名称、logo:不得混有商业化用语、热门公众号或小程序名称、热门应用名称、流行用语、“国家级”、“最高级”等广告法律法规明令禁止的用语、水印标识等与小程序功能或内容无关的内容; 参考:https://developers.weixin.qq.com/miniprogram/product/ 服务类目 (1)类目:所实际提供的服务和内容,需与小程序的简介一致,且不存在隐藏类目。 (2)内容:服务范围需与实际填写的类目和标签一致,也需和自身所提供的服务一致,且不应超出小程序平台的类目库范围。需要注意的是,一旦你选择了游戏类目,该类目将不可修改变更为其他小程序类目。 参考:https://developers.weixin.qq.com/miniprogram/product/material/ 绑定开发者和运营中 (1)由管理员绑定开发者:开发者可以登录后台和开发调试,并使用【小程序助手】 (2)有管理员绑定运营者或体验中:运营者可以看【小程序助手】,体验者可以扫体验二维码使用体验版 (3)开发版,体验版,审核版,灰度发布版,线上版本,上一版本 (4)人数上线说明 登录微信公众平台小程序,进入用户身份-开发者,新增绑定开发者。 个人主体小程序最多可绑定5个开发者,10个体验者。 未认证的组织类型小程序最多可绑定10个开发者,20个体验者 已认证的小程序最多可绑定20个开发者,40个体验者。 产品评审 (1)审核产品内容页面包含的内容类目归属,及是否涉及提交特殊资质证明的证件 (2)案例类目1:新闻、资讯、新闻=>提供信息浏览服务=>(2选1):1、新闻服务商:《互联网新闻信息服务许可证》 (3)案例类目2:交友、聊天=>《增值电信业务经营许可证》 (4)社区/论坛:=>(2选1)1、《非经营性互联网信息服务备案核准》 (5)反面案例1:客户小程序提供房产交易需求=》暂不支持房产在线交易,房产在线交易服务属平台尚未开放的服务类目,建议去除 (6)反面案例2:小程序上线采集房产咨询展示功能=>如包含房地产政策新闻,需补充:时政信息类目 =》(2选1):①、新闻服务商:《互联网新闻信息服务许可证》 ②、政府或监管机构:《非经营性互联网信息服务备案核准》与《组织机构代码证》 技术准备 (1)获取AppID和Secrect,并获取token及存储刷新token (2)筛选项目里所有涉及用到的微信的能力,一一列出来,并思考参数的准备和请求交互的数据存储,从而设计交互方式和数据存储方式 (3)域名配置,支持请求、上传、下载、socket 3、开发及审核 开发 (1)全局缓存参数的生命期管理,场景值值处理,转发卡片,封面处理 (2)完成满足交互的API (3)完成功能后,运行小程序体验评分 (4)推荐的人员分工模式 a、后端或者项目经理或架构师(必须要全栈)负责全局数据量交互,包含自己公司和微信服务器交互的设计及存储 b、前端写UI及调API c、一人负责实现后端数据流API的实现 d、更大型的项目可以,b和c模块可以增加人即可,建议需要一共全栈工程师做全局的进度和项目管理工作 审核与发布 (1)提交发布,审核,手动上线 [图片] 近期上线项目落地案例 (1)产品完成注册账号 (2)产品和技术一起完成名称类目的基本资料填写,通读prd原型和官方类目:定义为【信息查询】 (3)研发准备:发现文档仅涉及获取小程序码,那么就需要获取参数码,进而发现需要去准备获取token (4)分工 a、前端:UI、交互API、本次存储管理、小程序识别处理、转发参数处理、转发卡片封面处理、前端渲染处理 b、后端:token准备、获取参数码及参数码与前端解析对接、后端获取参数码并上传后保存url、自身小程序的数据交互API c、联调:参数码,转发场景,本地缓存,优化渲染和接口返回速度,测速并一起优化建议项 后记: hello world:https://developers.weixin.qq.com/ebook?action=get_post_info&docid=0002c8979d0678bb00862b8595b00a
2020-04-17