个人案例
- 帮小忙工具箱
基于Ai深度学习,对质量较低的图片进行多种优化处理,重建高清图像。
图像效果增强小程序扫码体验
- 小程序链接生成与使用规则调整公告
各位开发者: 为确保小程序链接合理使用,自 2022 年 4 月 11 日起,URL Scheme 和 URL Link (以下统称为 “链接” )接口能力规则将进行以下调整: 每个 URL Scheme 或 URL Link 有效期最长 30 天,均不再支持永久有效的链接、不再区分短期有效链接与长期有效链接;链接生成后,若在微信外打开,用户可以在浏览器页面点击进入小程序。每个独立的链接被用户访问后,仅此用户可以再次访问并打开对应小程序,其他用户无法再次通过相同链接打开该小程序;单个小程序每天生成链接数(URL Scheme 和 URL Link 总数)上限为 50 万条。 对于上述 1,在开发层面,相应的服务端接口 urlscheme.generate 和 urllink.generate 将进行以下调整: is_expire 值固定为 true,可不再传该值,若传值为 false 也与 true 一样会生成到期失效链接;若 expire_type 传值为 0,需注意 expire_time 传值的时间戳不超过 30 天,即该参数最长传值有效期为 30 天;若 expire_type 传值为 1,需注意 expire_interval 传值范围为 [1, 30],即该参数最长传值间隔天数为 30。详细对比见下表: [图片] 已使用该后端接口的开发者可以不进行任何修改,不会出现返回异常。若传值超过新规则合法值,或声明使用永久有效的链接,则均会被赋最长有效期值(30天);需注意以上新规则生效后的有效期和访问规则变化。 在本次规则调整生效前已经生成的链接,也将自动生效以下规则: 如果有效期超过30天或长期会被降级为30天有效,开始时间从调整日期开始计算;在调整生效后,只能被1个用户访问。 当前已使用微信云开发 静态网站H5跳小程序 与 短信跳小程序、微信服务平台短信服务为用户提供链接的功能不受影响,但同样适用以上规则。 微信团队 2022年3月9日 相关QAQ1:每天下发的短信量级超过50万条,不够用怎么办? A1:可将生成 scheme 的时机改为在用户打开 H5 时再生成: [图片]
2023-09-26 - 紧急wx.cloud.uploadFile 返回 fail url not in domain?
过去代码运行正常,最近两天发现所有的wx.cloud.uploadFile 运行都不正常 该函数在开发环境和真机测试环境都正常,但在真机运行环境下出现错误: fail url not in domain list? 代码片段: wx.cloud.uploadFile({ cloudPath: 'owner_sound/share/' + that.data.news_id + '/share.jpg', filePath: that.data.upload_local_url, // 本地文件路径 success: res => { that.getImgUrl(res.fileID) }, fail: err => { that.setData({ publisher_change: false, }) wx.hideLoading({ success: (res) => { wx.showModal({ title: '封面图片上传失败!', content: err.errMsg, showCancel: false, }) }, }) } })
2021-01-09 - 解决事件委托无法拿到dataset问题。
解决思路:给子节点加个遮罩层。代码片段:https://developers.weixin.qq.com/s/Zlpl6wmq7TsZ效果图:[图片]后续规划:使用事件委托是为了提升性能,目前GC(垃圾回收)的资料还没看完,后面应该会出一篇对比:长列表使用事件委托前后性能对比,敬请期待。核心代码/* item-mask 用层级遮盖,使事件委托获取dataset */ .item-mask { position: relative; } .item-mask::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; top: 0; }
2021-08-30 - 教你怎么监听小程序的返回键
更新:2020年7月28日08:51:11 基础库2.12.0起,可以调用wx.enableAlertBeforeUnload监听原生右上角返回、物理返回以及wx.navigateBack时弹框提示 AIP详情请看: https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.enableAlertBeforeUnload.html //======================================== 怎么监听小程序的返回键? 应该有很多人想要监听用户的这个动作吧,但是很遗憾,小程序不会给你这个API的,那是不是就没辙了? 幸好我们还可以自定义导航栏,这样一来我们就可以监听用户的这一动作了。 什么?这你已经知道啦? 那好咱们就不说自定义导航栏的返回监听了,说一下物理返回和左滑?右滑?(不管了,反正是滑)返回上一页怎么监听。 监听物理返回 首先说一下这个监听方法的缺点,虽说是监听,但是还是无法真正意义上的监听并拦截来阻止页面跳转,页面还是会返回上一页,而后重新载入刚刚的页面,如果这不是你想要的,那可以不用往下看了 其次说一下用到什么东西: wx.onAppRoute、wx.showModal 最后是一些主要代码: 重写wx.showModal,主要是加个confirmStay参数和使wx.showModal Promise化 [代码]const { showModal } = wx; Object.defineProperty(wx, 'showModal', { configurable: false, // 是否可以配置 enumerable: false, // 是否可迭代 writable: false, // 是否可重写 value(...param) { return new Promise(function (rs, rj) { let { success, fail, complete, confirmStay } = param[0] param[0].success = (res) => { res.navBack = (res.confirm && !confirmStay) || (res.cancel && confirmStay) wx.setStorageSync('showBackModal', !res.navBack) success && success(res) rs(res) } param[0].fail = (res) => { fail && fail(res) rj(res) } param[0].complete = (res) => { complete && complete(res) (res.confirm || res.cancel) ? rs(res) : rj(res) } return showModal.apply(this, param); // 原样移交函数参数和this }.bind(this)) } }); [代码] 使用wx.onAppRoute实现返回原来的页面 [代码]wx.onAppRoute(function (res) { var a = getApp(), ps = getCurrentPages(), t = ps[ps.length - 1], b = a && a.globalData && a.globalData.pageBeforeBacks || {}, c = a && a.globalData && a.globalData.lastPage || {} if (res.openType == 'navigateBack') { var showBackModal = wx.getStorageSync('showBackModal') if (c.route && showBackModal && typeof b[c.route] == 'function') { wx.navigateTo({ url: '/' + c.route + '?useCache=1', }) b[c.route]().then(res => { if (res.navBack){ a.globalData.pageBeforeBacks = {} wx.navigateBack({ delta: 1 }) } }) } } else if (res.openType == 'navigateTo' || res.openType == 'redirectTo') { if (!a.hasOwnProperty('globalData')) a.globalData = {} if (!a.globalData.hasOwnProperty('lastPage')) a.globalData.lastPage = {} if (!a.globalData.hasOwnProperty('pageBeforeBacks')) a.globalData.pageBeforeBacks = {} if (ps.length >= 2 && t.onBeforeBack && typeof t.onBeforeBack == 'function') { let { onUnload } = t wx.setStorageSync('showBackModal', !0) t.onUnload = function () { a.globalData.lastPage = { route: t.route, data: t.data } onUnload() } } t.onBeforeBack && typeof t.onBeforeBack == 'function' && (a.globalData.pageBeforeBacks[t.route] = t.onBeforeBack) } }) [代码] 改造Page [代码]const myPage = Page Page = function(e){ let { onLoad, onShow, onUnload } = e e.onLoad = (() => { return function (res) { this.app = getApp() this.app.globalData = this.app.globalData || {} let reinit = () => { if (this.app.globalData.lastPage && this.app.globalData.lastPage.route == this.route) { this.app.globalData.lastPage.data && this.setData(this.app.globalData.lastPage.data) Object.assign(this, this.app.globalData.lastPage.syncProps || {}) } } this.useCache = res.useCache res.useCache ? reinit() : (onLoad && onLoad.call(this, res)) } })() e.onShow = (() => { return function (res) { !this.useCache && onShow && onShow.call(this, res) } })() e.onUnload = (() => { return function (res) { this.app.globalData = Object.assign(this.app.globalData || {}, { lastPage: this }) onUnload && onUnload.call(this, res) } })() return myPage.call(this, e) } [代码] 在需要监听的页面加个onBeforeBack方法,方法返回Promise化的wx.showModal [代码]onBeforeBack: function () { return wx.showModal({ title: '提示', content: '信息尚未保存,确定要返回吗?', confirmStay: !1 //结合content意思,点击确定按钮,是否留在原来页面,confirmStay默认false }) } [代码] 运行测试,Oj8K 是不是很简单,马上去试试水吧,效果图就不放了,静态图也看不出效果,动态图懒得弄,想看效果的自己运行代码片段吧 代码片段 https://developers.weixin.qq.com/s/hc2tyrmw79hg
2020-07-28 - 如何用小程序开发app
背景 都知道小程序的体验要比app里面直接嵌入h5的体验要好,都知道小程序其实也是运行在app上的。那么我们为什么不能用小程序来开发app呢?这样不仅可以小程序和app只要开发一次,小程序和app都有了。还可以实现app动态更新不需要提交应用市场审核,我们只要做个小程序载体的app壳(类微信端小程序 sdk),而且体验效果也接近原生。做一个类似小程序平台把我们现在的app项目框架从组件化改成为小程序平台构架。 [图片] 每个业务程序都是一个个小程序,原生提供原生能力。 什么是小程序 首先我们要知道小程序是啥?现在市场上的小程序有很多,微信小程序、百度小程序、支付宝小程序、字节跳动小程序等但都差不多。与传统app相比,小程序无需安装、卸载,运行在微信、百度、支付宝等这样大型app载体上。虽然每种小程序都差不太多,但都定义了自己的开发语言和规范,这对开发者来说也是不少的麻烦。 小程序是介于web网页应用和原生应用的一种产物; [图片] 小程序和Hybrid APP的关系 原以为Hybrid APP就是用app的webview去加载一个h5文件,然后webview通过js桥梁和原生通信,实现js调用h5方法,h5方法调原生方法。来弥补h5无法拍照、打电话等不足。但是做出来的效果h5都会有短暂白屏,体验也无法达到原生效果(提供一个简单的demo)。后来接触了小程序,觉得小程序的体验和原生差不了多少了,可以说不是专业人员基本是看不出区别的,原先以为小程序是类似RN、weex这样的原生渲染,后面才知道它也是webview渲染。竟然也是hybird app,那它的体验是什么上去的?都是webview渲染,为什么小程序的体验会比普通的h5好?这让我非常感兴趣,于是就开始了小程序的深入了解。 初步了解用h5做Hybrid APP和小程序的区别: 相同点: 1.都是webview渲染 2.都是js通过桥接和原生通信 3.都可以调用原生组件 4.都可以把资源文件下载本地加载渲染 不同点: 1.Hybrid是html 有dom操作,小程序是虚拟dom 屏蔽了直接对dom操作 2.小程序有服务层,负责处理业务逻辑和数据处理 3.小程序页面有原生页面的生命周期管理 4.小程序tab和bar是原生控件 5.小程序类web不是h5 6.小程序基于微信跨平台 小程序原理 下面以微信小程序为例,进一步展开小程序原理 都知道微信小程序有自己的开发语言,wx开头的方法也不少,那它是什么转化为微信app能识别的语言呢?微信开发工具开发完提交审核,审核通过下发到微信端的是什么样的文件呢?带着这些问题我查阅了很多资料,小程序在技术架构上非常清晰易懂。JS负责业务逻辑的实现,而表现层则WXML和WXSS来共同实现,前者其实就是一种微信定义的模板语言,而后者类似CSS。但是语法毕竟是自定义的,所以要么在下发的之前进行编译,要么就是在渲染的时候进行转化成webview能够识别的语法。我们发现这2个节点微信都做了处理,拿到下发到微信端的wxapkg格式的小程序包,解开后都是js和html已经不是我们开发的WXML和WXSS格式了。但是这一个个html直接用浏览器打开却是空白的。没错<body></body>里面是空的,渲染的时候动态加进入内容的。 1.小程序是如何编译的 我们先来看看打包编译这层,微信都做了些啥呢?微信的打包和编译都在服务端进行,我这边找了个类似的来描述下,不一定准确,只能参考下。 检测app.json文件是否存在 清空并创建指定的输出目录 根据service.html模板,带上版本信息输出到指定的目录中 读取配置文件app.json,将其注入到app-config.js中,输出到指定的目录中 读取所有小程序代码中所有的JS文件,同时判断其是否在app.json中定义,如果其没被定义也不是app.js,说明其为引入的module, 将这些JS路径名存入一个数组中,并确保app.js和页面文件放置在数组尾部 遍历JS文件数组并读取它们,根据用户设置项判断是否使用Babel将其转换为es5的代码 把js模块封装成CommonJS模块,并合并成app-service.js这个文件输出 根据app.json里的pages配置,遍历每个页面根据页面wxml,wxss生成相应的页面文件并合成page-frame.html 其他步骤应该都不难理解,我认为最难的应该是wxml,wxss生成相应的页面文件,这个页面不是普通的html文件,前面也说过它的body是空的。如果你安装了微信的开发工具的化你可以找下是否有wcc和wcsc这2个小工具。wxss 转换成了css,wxml转换成了inject_js,实际上就是virtual_dom。openVendor命令可以在小程序中获取到构建脚本wcc和wcsc,以及各个版本小程序的执行SDK***.wxvpkg,这个SDK也可以用Wechat-app-unpack解开,解开后里面就有WAService.js和WAWebview.js等代码。 根据 /Users/***/Library/Application Support/微信web开发者工具/WeappVendor 路径来找到微信开发者工具目录,以及查看工具集成的核心类。可以看到我们和熟悉的也很重要的WAService.js和WAWebview.js2个文件也在里面。不过代码都是加密混淆的,没有可读性。 [图片] 2.编译好的小程序包如何下发解析 再看下下面这张图,微信下发的wxapkg格式的文件(每个小程序都是这样的一个包),这个文件可以通过从越狱的iPhone或者root的安卓手机上拿到。有部分人用charles通过https抓包拿到了下载链接,也拿到了包。解压出来就是这样的目录格式。简单解释下这每个文件的作用以及是什么来的: [图片] app-config.json:小程序的整体配置文件,里面是一个json 主要包括page、entryPagePath、pages、global、tabBar、ext、extAppid等 { “page”: { “pages/shop/index.html”: { “window”: { “enablePullDownRefresh”: false } }, “pages/goods/detail.html”: { “window”: { “navigationBarTitleText”: “商品详情”, “enablePullDownRefresh”: false } }, “pages/order/address/list.html”: { “window”: { “navigationBarTitleText”: “地址列表”, “enablePullDownRefresh”: false, “backgroundTextStyle”: “light” } }, … }, “entryPagePath”: “pages/shop/index.html”, “pages”: [“pages/shop/index”, “pages/order/detail/logisticsmap”,…], “global”: { “window”: { “backgroundTextStyle”: “light”, “navigationBarBackgroundColor”: “#f1f1f1”, “navigationBarTitleText”: " ", “navigationBarTextStyle”: “black” } }, “ext”: { “api”: { }, “form”: { }, “name”: “小店” }, “extAppid”: “########” } page节点:管理每个页面的整体设置,比如原生navigationBar的标题样式、是否需要下拉刷新等可以说这些设置都是对原生的ViewController的一个设置。没错一个小程序页面都有一个对应的原生页面,所以它可以封装原生页面的生命周期暴漏给小程序页面使用,包括原生的navigationBar和tabBar、下拉刷新控件等。这也是为什么小程序如果屏蔽调自带的navigationBar,自己定义navigationBar 下拉刷新也要自己重新做的原因。自定义navigationBar就不再是原生的了,它默认下拉刷新效果是在原生navigationBar下的整个webview做动画效果,这显然满足不了我们的需求。 entryPagePath:这个节点就简单了,小程序的入口页面的配置 pages节点:页面路径数组 对应小程序源代码里面的app.json文件里面的pages节点 global节点:全局设置和全局变量等;window 整个小程序的私有页面都起作用,也就是说每个页面没有自己单独设置都直接用这个全局设置的效果。 ext、extAppid节点:对应的就是ext.json文件 第三方平台部署小程序才需要 tabBar节点:对原生tabBar的样式设置和页面路径和图标等配置 app-service.js文件:这个是小程序一个很重要的文件,小程序体验好一个很重要的环节。从下图截取的代码片段很容易看出这个文件就是小程序里面全部js文件内容的集合。通过__wxRoute来路由,wxRouteBegin=true来标记启始页面。 var wxAppData = wxAppData || {}; ** var wxRoute = wxRoute || “”; var wxRouteBegin = wxRouteBegin || “”; var wxAppCode = wxAppCode || {}; var global = global || {}; var WXML_GLOBAL=WXML_GLOBAL || {}; var wxAppCurrentFile=wxAppCurrentFile||""; ** var Component = Component || function(){}; var definePlugin = definePlugin || function(){}; var requirePlugin = requirePlugin || function(){}; var Behavior = Behavior || function(){}; [代码]define("app.js", function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,XMLHttpRequest,WebSocket,Reporter,webkit,WeixinJSCore){ [代码] “use strict”;App({onLaunch:function(){var e=this,o=wx.getStorageSync(“logs”)||[];o.unshift(Date.now()),wx.setStorageSync(“logs”,o),wx.login({success:function(e){}}),wx.getSetting({success:function(o){o.authSetting[“scope.userInfo”]&&wx.getUserInfo({success:function(o){e.globalData.userInfo=o.userInfo,e.userInfoReadyCallback&&e.userInfoReadyCallback(o)}})}})},globalData:{userInfo:“hello world”,text:“hello world”}}); ** }); require(“app.js”); wxRoute = ‘pages/page2/page2’;wxRouteBegin = true; define(“pages/page2/page2.js”, function(require, module, exports, window,document,frames,self,location,navigator,localStorage,history,Caches,screen,alert,confirm,prompt,XMLHttpRequest,WebSocket,Reporter,webkit,WeixinJSCore){ “use strict”;Page({data:{},onLoad:function(n){},onReady:function(){},onShow:function(){},onHide:function(){},onUnload:function(){},onPullDownRefresh:function(){},onReachBottom:function(){},onShareAppMessage:function(){}}); }); require(“pages/page2/page2.js”);**** 3.小程序如何渲染 除了小程序每个页面的js还包含了app.js 这个包含小程序的整个生命周期管理逻辑的js文件内容也都在这个文件里面。在微信端里面小程序的sdk会有一个单独的webview来加载app-service.js文件当作这个小程序的服务层,负责每个页面逻辑处理,而且这个服务在这个小程序的整个生命周期它是一直在的,每个页面的js文件都已经压缩在这个文件里面,并在小程序服务启动的时候已经加载到内存中,所以在点击按钮需要做逻辑交互的时候体验会那么快。 [图片] App Service(逻辑层)主要就是由app-service.js文件和集成在微信app里面的WAService.js组成,如一个页面加载需要网络请求就是由逻辑层处理请求参数并交给原生来进行请求,原生把请求到的数据返回给App Service(逻辑层)进行数据处理,最后把处理好的数据通过原生JSBridge传给view(试图层)进行渲染。对 逻辑层和视图层没有直接的交互,逻辑服务层和视图层也不在一个线程里面,2个webview 只能通过原生来进行通信。 几个文件夹没啥特别的 就是和你微信小程序开发的目录是一样,放的都是你的页面和组件的html文件。但是值得一提的是这里面的html文件内容很少,它不是一个完整的页面,可以说这个页面的样式,静态内容都不在这个html里面。里面放的是这个页面css、路由路径和加载入口方法的调用generateFunc: $gwx(’./pages/order/rights/index.wxml’)。可以看下我的hello world页面代码,就更加清晰了。 <!–pages/page2/page2.wxml–> <text class=“pageText”>hello world page2</text> /* pages/page2/page2.wxss */ .pageText { background-color: red; width: 100%; height: 100rpx; display: flex; align-items: center; justify-content: center; } 这是一个很简单的小程序页面代码,那它编译后的html页面代码是什么样的呢?下面我展示出来给大家看。 <style> </style> <page></page> <script> var setCssStartTime = Date.now(); setCssToHead([".",[1],“pageText { background-color: red; width: 100%; height: “,[0,100],”; display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center; }\n”,],undefined,{path:"./pages/page2/page2.wxss"})() var setCssEndTime = Date.now(); document.dispatchEvent(new CustomEvent( “generateFuncReady”, { detail: { generateFunc: $gwx( ‘./pages/page2/page2.wxml’ ) }})) </script> 发现了啥?“hello world page2“ 这样页面关键内容不见了,那它是如何渲染的?懂js的人估计很容易就可以看到这个页面的入口方法generateFunc: $gwx 是的 这个方法是一个很重要的方法。那么这个方法在哪里?“hello world page2“ 这样页面关键元素又在哪里?肯定是跟着这个资源包一起下发的。对的所以的页面标签、元素 内容都在page-frame.html文件里面。也就是wxml文件的代码都编译压缩到page-frame.html文件里面,而对应页面的html文件只放对应的wxss文件代码和入口js代码。而入口方法的触发是由原生app调用js"generateFuncReady"事件触发调用。原生sdk这块后面可以以OC为例贴出对应代码,再展开说明下。 page-frame.html文件:这个文件应该是一个小程序包里面最大一个文件,所以里面的代码也不好都贴出来,简单介绍下里面的组成和作用。第一大块:模版 View(视图)层如何绘制每个页面都是公共的,如何把vdom渲染到webview上。$gwx方法就在这个模版里面,一个页面的入口。 [图片] 再找找我们刚才html页面消失的关键元素“hello world page2“在哪里?把每个元素都转化为Z数组了,每个页面都是一个类似这样的代码块。由于混淆和压缩加大了我们阅读的难度,但是如果你以为看完这个文件就完了,那就错了。View(视图)层除了这几个html文件外 还需要一个很重要的文件,那就是放在微信app包里面的WAWebview.js文件。和服务层里面也有一个WAService.js文件配合使用达到和原生交互的效果。 [图片] 和原生交互这块是属于小程序框架,所以肯定不会在下发的小程序包里面,除了原生sdk代码之外,还有2个很重要的js文件就是上面提到的WAService.js文件和文件。接下来我们就开始简单了解下: 为了方便理解我整理了js和原生的一些API,有app(小程序)级别的、有页面级别的、有原生组件级别的。可见js和原生交互是非常频繁的,可以说每个操作都是需要提供View(视图)层的WAWebview.js调用原生的桥梁需要原生处理就原生处理后再调用app Service(逻辑)层的WAService.js 由WAService.js通知 逻辑层处理对应逻辑,再把处理结果返回到原生。 [图片] 小程序在App中执行时的时候分为三个不同的模块,View/Service/Native,各司其职。View和Service都在WKWebView中执行,互相无法调用,不直接操作DOM。他们之间通过Native层通信。Native和WebView之间通过webkit.messagehandler和evaluateJavascript互相调用。小程序借助的是JSBridge实现了对底层API接口的调用,所以在小程序里面开发,开发者不用太多去考虑IOS,安卓的实现差异的问题,安心在上层的视图层和逻辑层进行开发即可。 WeixinJSBridge.publish: view和service之间的透传,在WKWebView之间传递消息。 WeixinJSBridge.subscribe: 注册监听,监听view和service之间的消息调用。 WeixinJSBridge.invoke: View或者Service传递消息到Native,然后Native使用逻辑调用js callback。 WeixinJSBridge.on:监听Native的事件。 [图片] 启动小程序服务startAppWithAppInfo根据appid等基本信息判断小程序是否已经下载到本地,没有的话下载解压加载配置信息等。然后进入manager,manager其实也是分为几部分一个是小程序级别的管理,一个是单个小程序的管理。具体的可以通过下面的类图更加直观。 [图片] 下图显示了小程序启动时会从CDN和服务器校验和下载资源。也就是是小程序启动的时候会有点慢的主要原因,还有一些时间就是需要初值化小程序本地服务。 [图片] 4.小程序的生命周期 关于小程序的生命周期,可以两个部分来理解:应用生命周期(左侧蓝色部分)和页面生命周期(右侧绿色部分)。 其中应用的生命周期是这样一个流程:1、用户首次打开小程序,触发 onLaunch(全局只触发一次)。2、小程序初始化完成后,触发onShow方法,监听小程序显示。3、小程序从前台进入后台,触发 onHide方法。4、小程序从后台进入前台显示,触发 onShow方法。5、小程序后台运行一定时间,或系统资源占用过高,会被销毁。 页面生命周期是这样的一个流程:1、小程序注册完成后,加载页面,触发onLoad方法。2、页面载入后触发onShow方法,显示页面。3、首次显示页面,会触发onReady方法,渲染页面元素和样式,一个页面只会调用一次。4、当小程序后台运行或跳转到其他页面时,触发onHide方法。5、当小程序有后台进入到前台运行或重新进入页面时,触发onShow方法。6、当使用重定向方法wx.redirectTo(OBJECT)或关闭当前页返回上一页wx.navigateBack(),触发onUnload。同时,应用生命周期会影响到页面生命周期。 [图片] 用Page 实例说明的页面的生命周期 [图片] 由上图可知,小程序由两大线程组成:负责界面的视图线程(view thread)和负责数据、服务处理的服务线程(appservice thread),两者协同工作,完成小程序页面生命周期的调用。 视图线程有四大状态: 初始化状态:初始化视图线程所需要的工作,初始化完成后向 “服务线程”发送初始化完成信号,然后进入等待状态,等待服务线程提供初始化数据。 首次渲染状态:当收到服务线程提供的初始化数据后(json和js中的data数据),渲染小程序界面,渲染完毕后,发送“首次渲染完成信号”给服务线程,并将页面展示给用户。 持续渲染状态:此时界面线程继续一直等待“服务线程”通过this.setdata()函数发送来的界面数据,只要收到就重新局部渲染,也因此只要更新数据并发送信号,界面就自动更新。 结束状态:页面被回收或者销毁、应用被系统回收、销毁时触发。 服务线程五大状态: 初始化状态:此阶段仅启动服务线程所需的基本功能,比如信号发送模块。系统的初始化工作完毕,就调用自定义的onload和onshow,然后等待视图线程的“视图线程初始化完成”号。onload是只会首次渲染的时候执行一次,onshow是每次界面切换都会执行,简单理解,这就是唯一差别。 等待激活状态:接收到“视图线程初始化完成”信号后,将初始化数据发送给“视图线程”,等待视图线程完成初次渲染。 激活状态:收到视图线程发送来的“首次渲染完成”信号后,就进入激活状态既程序的正常运行状态,并调用自定义的onReady()函数。此状态下就可以通过 this.setData 函数发送界面数据给界面线程进行局部渲染,更新页面。 后台运行状态:如果界面进入后台,服务线程就进入后台运行状态,从目前的官方解读来说,这个状态挺奇怪的,和激活状态是相同的,也可以通过setdata函数更新界面的。毕竟小程序的框架刚推出,应该后续会有很大不同吧。 结束状态:页面被回收或者销毁、应用被系统回收、销毁时触发。 小程序在App中的应用场景 说了这么多技术理论,最后说下小程序在项目中如何应用。整个项目都是小程序不现实,毕竟小程序的定义是轻量级的,像IM、消息等用原生肯定比小程序更加适合,所以用小程序和原生混合开发是不可少的。还有一个让你不得不混合开发的一个重要原因,你的app不是一个新项目,是一个现有的原生app,一次性用小程序重新做一次不现实,所以混合会是最好的选择。我这边做的混合开发不是技术层面的,我们都知道小程序是很原生通信很频繁的,它需要原生提供各种能力才能到达接近原生的体验,所以本身就是一个混合。而我这里说的混合是指业务层面的混合开发,打破我们以往对小程序的认知。不管是百度小程序还是微信小程序都是运行在他们生态下的一个独立应用程序。比如一个商城小程序它不会有部分页面是原生部分页面是小程序,也只会有一个入口,一个出口。而我们要用小程序来开发app,我们app有自己的需求我们需要让小程序看起来像原生页面一样,对用户来说它还是一个app,不存在哪个页面是原生哪个页面是原生的。所以一切都是从技术层面来说,就是小程序和原生进行混合开发的业务app。你可以理解为你的app里面嵌入h5一样的开发模式,只是小程序页面比一般的h5页面交互体验要好一些而已。 [图片] 我可以从原生页面跳转到小程序的任何页面,如果必要的话也可以从小程序页面跳转到原生页面。所以小程序服务不能在进入小程序页面的时候才启动,也不能因为回到原生页面而销毁。必须根据你的业务场景来调用控制。 以上是个人对与小程序开发app的一些浅薄看法,期待和业界同仁共同探讨。你有什么想法呢?欢迎评论交流。 最后感谢业界各位大佬的贡献,在这里附上我的参考文献: https://blog.csdn.net/ListenToSennTyou/article/details/53258163 https://www.jianshu.com/p/92c6a75c2323 https://blog.csdn.net/xiangzhihong8/article/details/66521459 https://yq.aliyun.com/articles/72825?t=t1 https://github.com/weidian-inc/hera-cli https://www.cnblogs.com/viaiu/p/9935602.html https://www.jianshu.com/p/51ac882ea9f4 http://lrdcq.com/me/read.php/66.htm https://github.com/wdfe/weweb
2019-05-30 - 从源码看微信小程序启动过程
一、写作背景 接触小程序一年多,真实体验就是小程序开发门槛相对而言确实比较低。不过小程序的开发方式,一直是开发者吐槽的,如习惯了 Vue,React 开发的开发者经常会吐槽小程序一个 Page 必须由多个文件组成,组件化支持不完善或者说不能非常愉快的开发组件。在以前小项目中没太大感觉,从加入有赞,参与有赞微商城小程序的开发,是真切的体会到对于大型小程序项目开发的复杂性。 有赞从微信小程序内测就开始开发小程序,在不支持自定义组件的时代,只能通过 import 的形式拆分模块或实现组件。在业务复杂的页面,可能会 import 非常多的模块,而相应的 wxss 也需要 import 样式,除了操作繁琐,有时候也难免遗漏。 作为开发者,我们当然希望可以让工作更简单,更愉快,也希望改善我们的开发方式。所以希望能够更了解微信小程序框架,减少不必要的试错,于是有了一次对小程序框架的 debug 之旅。(基础库 1.9.93) 通过三周空余时间的 debug,也算对小程序框架有了一些浅显的认识,达到了最初的目的;对小程序启动,实例,运行等有了真切的体会。这篇文章记录了小程序框架的基本代码结构,启动流程,以及程序实例化过程。 本文的目的是希望把我看到的分享给对小程序感兴趣或者正在开发小程序的读者,主要解答“框架对传入的对象等到底做了什么”。 二、从启动流程一窥小程序框架细节 在开发者工具中使用 help() 方法,可以查看一些指令和方法。使用其中的 openVendor 方法可以打开微信开发者工具在小程序框架所在目录。其中以包括以基础库命名的目录和其他帮助文件,如其中有两个工具 wcc,wcsc。wcc 可把 wxml 转换为对应的 JS 函数 —— $gwx(path, global),wcsc 可将 wxss 转换为 css。而基础库目录包括 WAService.js 和 WAWebview.js 文件。小程序框架在开发者工具中以 WAService.js 命名(WAWebview.js 不知其作用,听说在真机环境使用该文件)。 在开发中工具命令行使用 document.head 可以查看到小程序的启动流程大致如下: [图片] 以小节的方式分别介绍这些流程,小程序是如何处理的(小节编号与图中编号相同)。 1、初始化全局变量 下图是小程序启动是初始化的一些全局的变量: [图片] 那些使用“__”开头,未在文档中提及可使用变量是不建议使用的,wxAppCode 在开发者工具中分为两类值,json 类型和 wxml 类型。以 .json 结尾的,其 key 值为开发者代码中对应的 json 文件的内容,.wxml 结尾的,其 key 值为通过调用 $gwx(’./pages/example/index.wxml’) 将得到一个可执行函数,通过调用这个函数可得到一个标识节点关系的 JSON 树。 [图片] 2、加载框架(WAService.js) 使用工具对 WAService.js 进行格式化后进行 debug。可以发现小程序框架大致由: WeixinJSBridge、 NativeBuffer、 wxConsole、 WeixinWorker、 JavaScript兼容(这部分为猜测)、 Reporter、 wx、 exparser、 virtualDOM、 appServiceEngine 几部分组成。 其中除了 wx 和 WeixinJSBridge 这两个基础 API 集合, exparser, virtualDOM, appServiceEngine 这三部分作为框架的核心, appServiceEngine 提供了框架最基本的接口如 App,Page,Component; exparser 提供了框架底层的能力,如实例化组件,数据变化监听,view 层与逻辑层的交互等;而 virtualDOM 则起着链接 appServiceEngine 和 exparser 的作用,如对开发者传入 Page 方法的对象进行格式化再传入 exparser 的对应方法处理。 框架对外暴露了以下API:Behavior,App,Page,Component,getApp,getCurrentPages,definePlugin,requirePlugin,wx。 3、业务代码的加载 在小程序中,开发者的 JavaScript 代码会被打包为 [代码]define('xxx.js', function(require, module, exports, window, document, frames, self, location, navigator, localStorage, history, Caches, screen, alert, confirm, prompt, fetch, XMLHttpRequest, WebSocket, webkit, WeixinJSCore, Reporter, print, WeixinJSBridge) { 'use strict'; // your code }) [代码] 这里的 define 是在框架中定义的方法,在框架中提供了两个方法:require 和 define 用来定义和使用业务代码。其方式有些像 AMD 规范接口,通过 define 定义一个模块,使用 require 来应用一个模块。但是也有很大区别,首先 define 限制了模块可使用的其他模块,如 window,document;其次 require 在使用模块时只会传入 require 和 module,也就是说参数中的其他模块在定义的模块中都是 undefined,这也是不能在开发者工具中获取一些浏览器环境对象的原因。 在小程序中,JavaScript 代码的加载方式和在浏览器中也有些不同,其加载顺序是首先加载项目中其他 js 文件(非注册程序和注册页面的 js 文件),其次是注册程序的 app.js,然后是自定义组件 js 文件,最后才是注册页面的 js 代码。而且小程序对于在 app.js 以及注册页面的 js 代码都会加载完成后立即使用 require 方法执行模块中的程序。其他的代码则需要在程序中使用 require 方法才会被执行。 下面详细介绍了 app.js,自定义组件,页面 js 代码的处理流程。 4、加载 app.js 与注册程序 在 app.js 加载完成后,小程序会使用 require(‘app.js’) 注册程序,即对 App 方法进行调用,App 方法是对 appServiceEngine.App 方法的引用。 下图是框架对于 App 方法调用时的处理流程: [图片] App 方法根据传入的对象实例化一个 app 实例,其生命周期函数 onLaunch 和 onShow 因为使用不同的方式获取 options的参数。在有些需要根据场景值来实现需求的,或许使用 onShow 中的场景值更合适。 在实际开发过程中发现,在微信顶部唤起小程序和在小程序列表唤起的 options 也是不一样的。在该案例中通过点击分享的小程序进入后,关闭小程序,再通过不同方式进入小程序,通过顶部唤起的还是 options 的 path 属性还是分享出来的 path,但是通过列表中打开直接回到了首页,这里 App 中的 onShow 就会获取到不同的 options。 5、加载自定义组件代码以及注册自定义组件 自定义组件在 app.js 之后被加载,小程序会在这个过程中加载完所有的自定义组件(分包中自定义组件没有有测试过),并且是加载完成后自动注册,只有注册完成后才会加载下一个自定义组件的代码。 下图是框架对于 Component 方法处理流程: [图片] 图中介绍了框架如何对传入 Component 方法的对象的处理,其后面还有很多深入的对于组件实例化的步骤没有在图中表示出来,具体可以在文章最后的附件中查看。 自定义组件在小程序中越来越完善,其拥有的能力也比 Page 更强大,而后面会提到在使用自定义组件的 Page 中,Page 实例也会使用和自定义组件一样的实例化方式,也就是说,他拥有和自定义组件一样的能力。 6、加载页面代码和注册页面 加载页面代码的处理流程和加载自定义组件一样,都是加载完成后先注册页面,然后才会加载下一个页面。 下图是注册一个页面时框架对于 Page 方法的处理流程: [图片] Page 方法会根据是否使用自定义组件做不同的处理。使用自定义组件的 page 对象会被处理为和自定义组件的结构,并在页面实例化时使用不同的处理流程进行实例化。当然对于开发而言没任何不同。 从图中可以发现 Page 传入的(生命周期)代码并不会在这里被执行,可以通过下面小节了解 Page 实例化的详细过程。 7、等待页面 Ready 和 Page 实例化 还记得上面介绍的启动流程中最后一步等待页面 Ready?严格来讲是等待浏览器 Ready,小程序虽然有部分原生的组件,不过本质上还是一个 web 程序。 在小程序中切换页面或打开页面时会触发 onAppRoute 事件,小程序框架通过 wx.onAppRoute 注册页面切换的处理程序,在所有程序就绪后,以 entryPagePath 作为入口使用 appLaunch 的方式进入页面。 下图是处理导航的程序流程: [图片] 从图中可以看出页面的实例化是在进入页面时进行,下图是具体的实例化过程: [图片] 下图是最终可得到 Page 实例: [图片] 可以发现其中多了 onRouteEnd API,实际该接口不会被调用。其中以 component 标记的表示只有在使用了自定义组件时才会有的方法和属性。在前面第 5 小节提到了对于使用自定义组件的页面会按照自定义组件方式解析,这些属性和方法与自定义组件表现一致。 8、关于 setData 小程序框架是一个以数据驱动的框架,当然不能少了对他如何实现数据绑定的探索,下图是 Page 实例的 setData 执行流程: [图片] 其中 component:setData 表示使用自定义组件的 Page 实例的 setData 方法。 三、写在最后 这是一次不完全的小程序框架探索,是在微信开发工具中 debug 的结果。虽然对于实际开发没有什么太大的帮助,但是对框架如何对开发的 js 代码进行处理有了一个很明确的认识,在使用一些 js 特性时可以有明确的感知。如果你还疑惑“小程序框架对传入的对象等到底做了什么”那一定是我表达能力太差,说声对不起。 通过这一次 debug ,也给我引入了新的问题,还希望能够有更多的讨论: · 自定义组件太多启动时会耗时处理自定义组件 · 文件太多会耗时读文件 · 合理的设计分包很重要 当然最后对于框架中已有的能力,还是非常希望微信可以开放更多稳定的接口,并在文档中告知开发者,让开发变得简单一些。
2019-03-05 - 善用wxs,让代码更简洁、易维护!(类computed)
场景: 在wxml里做运算时得到一大串小数点,虽然使用js也能解决,但在多层条件下使用js处理数值就很难受(亲身体会),比如计算多层规格的商品价格,需要判断当前有多少层规格,并索引到该规格再结合数量计算。 用途: 在wxml里处理使用js处理数据的展示。 好处: 简洁展示,不需繁琐的js处理逻辑复用、维护方便只展示,不破坏源数据 常用tools.wxs: function toFixed(num, bit = 0, isNum = false) { if (isNaN(num)) { return '' } else if (isNum) { return (num.toFixed(bit) - 0) } else { return num.toFixed(bit) } } function splice(str = '', start, end) { return str.slice(start, end) } module.exports = { toFixed: toFixed, splice: splice }; 效果展示: [图片] 代码片段:https://developers.weixin.qq.com/s/t2ApAUm57nfZ
2020-03-17 - 给你的点击区域加个触控热区
一、背景 给图标(按钮、标签)做事件点击时,常常会因为元素面积太小导致用户很难点中,影响用户体验,产品大佬就会让我们做大点击区域,一般给元素加个padding:6rpx就可以了,但这时UI就会偏差。 二、无入侵扩大热区的方法 给需要增加热区的元素加上add_touch类,即可在原来的基础上,增加1.5倍触控热区。 .add_touch { position: relative; } .add_touch::after { content: ''; width: 100%; height: 100%; position: absolute; left: 0; top: 0; transform: scale(1.5); } 三、效果 [图片] 四、代码片段 https://developers.weixin.qq.com/s/ROoPyNme7dfK
2020-03-12 - 社区的知识库有没有入口可以进?
如题:社区的知识库有没有入口可以进? [图片]
2019-12-10 - 那些年你没权限调用的API
api 顾名思义 wx.openMiniProgramHistoryList wx.openMiniProgramProfile wx.openMiniProgramSearch wx.openMiniProgramStarList 不再多BB直接上图 1.隐藏/显示右上角胶囊按钮 [图片] 2.截屏 [图片] 最近玩console有点上头 现在的小程序api已经达到了373个了 我是怎么知道的?看图 [图片] 往下拉一看 373 [图片] 这里边有很多的api 是文档中没写的(也可能永远不会写) 先说下 普通的小程序里边是没有权限的调用会提示 {errMsg: “openUserProfile:fail:access denied”} 或者 {errMsg: “getABTestConfig:fail permission denied”, errCode: 1} 代码片段:https://developers.weixin.qq.com/s/FgIssdmP7jdv 可用的api wx.navigateBackH5 --webview 通过这个api可以返回上一个web页面 更多的BUG等你们去处理 我吃饭去咯 end
2019-11-28 - 小程序页面(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 - 关于云开发新服务“实时数据推送”,你需要了解的全在这了!
“微信小程序工程师邓坤力带你了解如何利用千呼万唤始出来的云开发实时数据推送服务打造生动的小程序和小游戏!” 在数据库在小程序·云开发中的应用一文中,我们了解到实时数据推送作为云开发即将上线的一项新能力,主要指客户端使用官方SDK发起socket连接建立对一个集合的监听,目标集合中如果有符合过滤条件的数据发生变更,将会直接推送到建立监听的客户端。 [图片] 简单来说,使用实时数据推送可以更有效率的拉取数据,帮你把你的应用变成实时有状态,场景会非常有用,比如可以用来做弹幕,做实时排名更新,做实时刷新,或者实时推送一些通知给到用户。 那么,实时数据推送具体是如何为小程序与小游戏赋能,提升开发效率的?让我们跟随微信小程序工程师邓坤力一起,深入了解这项新服务诞生的来龙去脉。 为什么要做实时数据推送? 介绍实时数据推送服务之前,弄懂一个直击灵魂问题将有助于我们的理解,那就是“为什么要做实时数据推送?” 想要更回答这个问题,需要从即时通信服务说起。 [图片] 我们都知道建立一个简单、常规的即时通信服务需要长连作为实现实时性的基础,需要足够的存储来保证消息与文件的持久化,还需要实时推送功能来实现主动同步客户端的能力。在此过程中,开发者往往需要面临: (1)需从零自建完整服务,无法聚焦在原型和核心业务开发上。 (2)开发成本高。由于前后端逻辑复杂,开发者往往需要经历设计基础设施搭建,长连管理、数据库开发、安全管理等琐碎繁杂的步骤。 (3)维护成本高。开发者还需负责维护,完成基础设施管理、异常处理等。 (4)微信能力集成。自建服务器的开发者如要基于微信用户登录态进行操作并让小程序安全运行,就不可避免地需要接入微信鉴权体系,整合accesstoken和 sessionkey流程并保证其安全性。 看到这里,可能有很多读者已经想到了具有开箱即用、集成原生微信能力、自带云数据库、云函数、云存储的云开发,那么云开发的这些优势能否有效解决开发者在建立即时通信服务中常常会遇到的难题呢? 答案是不完全能,由于云开发不支持长连,并且不具备主动同步客户端的能力,因此只能通过短轮询以次级长连和推送的次级替代方案,在即时通讯服务构建时往往需要面临短轮询带来的资源浪费、成本与体验难平衡以及实时性差等问题。 [图片] 可能又有小伙伴要问了,让云开发支持长连不就可以弥补这些缺陷?答案也是否定的,因为若云开发支持长连,整个即时通讯服务的实现仍避免不了对长连的开发和管理,并且需要接受和处理消息,导致流程仍较为复杂,而这恰恰有悖于云开发作为高效率、轻量级解决方案的理念。 由此,云开发的实时推送服务应运而生,它将即时通讯服务所需的能力与云开发独有的优势串联起来,让开发者可以更便捷地使用并快速实现需求。 [图片] 实时数据推送有哪些能力? 能力概述 实时数据推送是云开发数据库新增的服务,通过这项服务,小程序端可实时监听数据库变更,即它支持根据开发者给定的查询语句进行监听,每当查询语句的结果发生变化时,小程序端就会收到包含更新内容的推送,并对实时数据变化做出响应。 总体来说,使用云开发的实时数据推送能力相比起自建服务可以享受以下便利,从而使其更专注于业务逻辑的设计: 原生能力,开箱即用 无需管理长连 无需编写服务端代码 无需搭建和管理基础设施 自动收到更新推送 丰富的应用场景 实时数据推送的应用场景十分丰富。 在即时通信方面,实时数据推送支持小程序直播聊天室、弹幕等以及小游戏的区服聊天、房间聊天、私信等功能的实现。 在状态同步方面,小程序可以使用实时数据推送来保持应用最新状态的同步,以信息流为例,可以支持实时提示有新的文章、评论、点赞,从而达到更好的用户体验;对小游戏来说,可以支持使用状态同步的模型开发的小游戏,比如棋牌类小游戏。 而在实时协作方面,实时数据推送可以为在线共享文档、项目管理协作工具等提供支持。 [图片] 简单易用的API 实时数据推送提供简洁易用的API,调用方便,并且可以完整描述整个维度的数据变化,以便开发者对具体业务逻辑做出响应。 [图片] 自动处理异常 SDK在异常时会尽可能自动恢复状态,并且此恢复为开发者无感知,开发者仅需处理 SDK 无法自动恢复的错误。具体来说: 实时数据推送在断网、网络切换、NAT 地址刷新等情况时均能自动检测异常和恢复连接,并且在更新事件推送失败或丢失时有机制保障会成功拉取,而在更新事件乱序时有机制保障开发者收到的是顺序事件。 [图片] 云开发新能力矩阵 云调用:云函数免鉴权调用微信服务端开放接口,获取微信开放数据,接收微信服务端消息推送。 HTTP API:小程序外访问云开发资源。 数据库聚合:分组查询、统计查询、流水线批处理。 控制台数据库高级查询:控制台中批量数据库增删查改。 云开发Network面板:小程序Network面板支持展示云开发请求。 实时数据推送
2019-08-28 - 小程序毫秒级倒计时的一种优雅实现
在小程序上用毫秒级定时器来刷新页面或组件,会引发性能问题。 使用 [代码]canvas[代码] 又对字体样式无法进行很好的控制。 一种实现方案 使用秒级的定时器刷新页面或组件实现秒级的倒计时,制作毫秒倒计时的 GIF 并把图放到秒级倒计时后面。 毫秒倒计时 GIF:[图片] 小程序效果如下图所示: [图片] HAVE FUN 😃
2019-09-20 - 几行代码实现小程序云开发提现功能
先看效果: [图片] 纯云开发实现,下面说使用步骤: 一:开通商户的企业付款到领取功能 说明地址: https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1 使用条件 1、商户号(或同主体其他非服务商商户号)已入驻90日 2、截止今日回推30天,商户号(或同主体其他非服务商商户号)连续不间断保持有交易 使用条件是第一难,第二难在下面这里 [图片] 在网上找了很多,感觉是云开发这里的一个不完善地方,如果不填ip,会报这种错 [代码]{"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"NO_AUTH"} [代码] [代码]<xml> <return_code><![CDATA[SUCCESS]]></return_code> <return_msg><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></return_msg> <mch_appid><![CDATA[wx383426ad9ffe1111]]></mch_appid> <mchid><![CDATA[1536511111]]></mchid> <result_code><![CDATA[FAIL]]></result_code> <err_code><![CDATA[NO_AUTH]]></err_code> <err_code_des><![CDATA[此IP地址不允许调用接口,如有需要请登录微信支付商户平台更改配置]]></err_code_des> </xml> [代码] 云开发没有ip这个概念,所以这里有些无从下手,不过这里我采用了个替代方案,参考了社区帖子: https://developers.weixin.qq.com/community/develop/doc/00088cff3a40d87d80f7267b65b800 之后我也亲自验证了,基本上就是这几个,当然肯定不够,但是可以自己在逻辑上进行处理,ip以下: [代码]172.81.207.12 172.81.212.74 172.81.236.99 172.81.235.12 172.81.245.51 212.64.65.131 212.64.84.22 212.64.85.35 212.64.85.139 212.64.87.134 [代码] 接着,可以动手了 二、云开发部分 1、设置云存储 证书配置地址: [图片] 下载后有三个文件,我们只需要p12结尾的那个 [图片] 然后,将这个apiclient_cert.p12文件上传到你的云存储 [图片] 这里处理完了,我们只需要一个东西,就是fileID也就是常说的云存储ID(上图红框内容) 2、配置云函数 新建云函数ref云函数 [图片] 代码如下: [代码]const config = { appid: 'wx383426ad9ffe1111', //小程序Appid envName: 'zf-shcud', // 小程序云开发环境ID mchid: '1111111111', //商户号 partnerKey: '1111111111111111111111', //此处填服务商密钥 pfx: '', //证书初始化 fileID: 'cloud://zf-shcud.11111111111111111/apiclient_cert.p12' //证书云存储id }; const cloud = require('wx-server-sdk') cloud.init({ env: config.envName }) const db = cloud.database(); const tenpay = require('tenpay'); //支付核心模块 exports.main = async(event, context) => { //首先获取证书文件 const res = await cloud.downloadFile({ fileID: config.fileID, }) config.pfx = res.fileContent let pay = new tenpay(config,true) let result = await pay.transfers({ //这部分参数含义参考https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 partner_trade_no: 'bookreflect' + Date.now() + event.num, openid: event.userinfo._openid, check_name: 'NO_CHECK', amount: parseInt(event.num) * 100, desc: '二手书小程序提现', }); if (result.result_code == 'SUCCESS') { //如果提现成功后的操作 //以下是进行余额计算 let re=await db.collection('user').doc(event.userinfo._id).update({ data: { parse: event.userinfo.parse - parseInt(event.num) } }); return re } } [代码] 需安装的依赖:wx-server-sdk、tenpay 这里只是实现了简单原始的提现操作,关于提现后,比如防止重复提交,提现限额这些,在开源二手书商城上有完整流程,地址: https://github.com/xuhuai66/used-book-pro 这种办法,不是每次都能成功提现,小概率遇到ip未在白名单情况,还是希望,云开发团队能尽快出一个更好的解决方案吧
2019-09-21 - 全自动、全面化收集 formid 思路,开启脑洞时刻
formid 基础知识 formid 是啥 小程序给用户发消息的唯一途径,所以前端要尽量多收集 formid 保存到服务端; formid 怎么收集 小程序要求必须用户提交表单才能得到formid; form 增加 report-submit 属性,并通过 button submit,前端 formSubmit 事件就能获得 formid ,如下方代码所示: [代码]<form bindsubmit="formSubmit" report-submit class="layout"> <button formType="submit" class="button" hover-class="none"> 。。。 </button> </form> [代码] formid 组件封装(初期思路) 我们将form与button 封装成组件(fomids),通过slot,可以显示任意元素,模拟form submit; 把页面上需要点击的元素用fomids包起来,fomids 将收集的 formid 放入cache,然后通过网络请求将 fomid 传给服务端; 大数小程序都是这么做的,这样做也没有问题,就是略微麻烦,form 与button 组合的样式有时不太好控制 formid 组件封装(进阶思路) 此思路是上面思路的延申,我们把 formids 这个组件做成layout级,把整个页面包在里面,利用事件自动冒泡的特性,只要有点击事件,就能收集到 formid 新建组件layout [代码]<!--components/layout.wxml--> <form bindsubmit="formSubmit" report-submit class="layout"> <button formType="submit" class="button" hover-class="none"> <view class="fixed"><slot></slot></view> </button> </form> [代码] [代码]/* components/layout.wxss */ .layout { display: inline-block; padding-left: 0; padding-right: 0; box-sizing: border-box; font-size: inherit; text-align: left; text-decoration: none; line-height: inherit; -webkit-tap-highlight-color: transparent; color: inherit; width: 100%; position: relative; } .layout .button{ display: inline-block; padding-left: 0; padding-right: 0; box-sizing: border-box; font-size: inherit; text-align: left; text-decoration: none; line-height: inherit; -webkit-tap-highlight-color: #000; color: inherit; width: 100%; position: relative; } .layout .button .fixed{ position:relative; z-index: 9999; width: 100%; } .layout .button:before,.layout .button:after{ border-width: 0; } [代码] [代码]// components/layout.js Component({ methods:{ formSubmit: function(e) { console.log('layout.formids',e.detail.formId) if("the formId is a mock one"!=e.detail.formId){ let formids=wx.getStorageSync('formids') || []; formids.push(e.detail.formId); formids=[...new Set(formids)]; wx.setStorage({key:'formids',data:formids}); } }, }, }) [代码] 将 layout 添加为全局组件 app.json 中增加 [代码]"usingComponents":{ "layout":"/components/layout" }, [代码] 在页面wxml中使用 [代码]<layout> <view class="pages">...</view> </layout> [代码] 怎么将 formid 提给服务端 首先你必须将wx.requxest 进行封装为myRequxest,页面上都是用myRequxest 进行网络请求; myRequxest 中 header 增加formids,从cache中获得放到header中;并删除formid缓存;(需要服务端从header中获得formids并存起来) [代码]function myRequxest(....){ let formids = wx.getStorageSync('formids'); if(formids){ wx.removeStorage({key:'formids'}); } let openid='';//用户openid wx.request({ ... header: { formids:formids.toString(), openid:openid, }, ... }) } [代码] 延申思考 从 写 form 收集 -> 封装 formids 组件 -> 封装 layout(formids) 组件,跳出思维固化,可以做更多可能; 希望这个思路能给你一些触发
2019-07-31 - 跟“张小龙”学做小程序
[图片] 张小龙,微信的缔造者,创造了微信并让微信成为如今生活的一部分,被称为最成功产品经理。本人是这次小程序大赛心语listen的队员,就我们小程序设计过程中受到张小龙先生的启发,所做的一些改进和大家进行分享。 简单就是美,人性化、简单(或原始)的东西往往就是体验最好的。 Windows 时代,多任务是怎么体现出来的?我们要摁“ALT+Tab”键。在 iPhone 里我们只要摁两次底下这个按钮就可以了,这个简单很多。在 iPad 里,4个指头把它撸上去就可以了,它就可以把多任务给切换过来。这是一个从复杂到简单的演化过程。实际上 ALT+Tab 非常复杂,很不人性化,所以我们说 Windows 体验不好,MacOS 体验好,判断依据是哪个更人性化或者更简单,或者更原始,它就是好的。我们买一个 iPhone 或 iPad 给一个 4 岁小孩都会用,这体现的是它的原始或者简单。 在一个小程序中,UI是影响用户第一体验的最大因素,那么用户需要的UI究竟是什么样的呢? [图片] 上图是去年小程序大赛获奖作品“双生小日记”的首页,我们可以发现界面非常简洁,甚至于什么都没有只有一个加号,仿佛在等待你的开启,一点开就知道这个小程序是做什么的,怎么用。 需要做用户教育的产品通常都是失败的,最好的做法是通过功能本身让用户一看就知道。 很多产品人喜欢在程序里加一些 Tips,觉得这是一个很好的教育手段,可如果你需要有 Tips 去教育用户,证明也很失败,你没有办法通过功能本身让用户一看就知道。 [图片] 同样以去年特等奖作品“参观清华”为例,这个小程序简单小巧,一个界面把功能区分的很清晰,用户一眼就可明了,预约操作业务流程短,整个操作都不会超过3级页面,用户体验非常好。 选取另一个获奖作品“数独大乱斗”,说实话我很不喜欢这个小程序的新手指导,用户点进新游戏时需要接近20个步骤的引导,真的去使用会很烦,本来用户被你的标题吸引进来了,可是很有可能被这复杂的新手指导劝退,本来是想进来娱乐的结果要学习一半天,会导致用户体验下降;从另一个方向说,用户真的会认真看完每条指导吗,看完真的能学会怎么用吗,我觉得答案应该是NO。 做产品要隐藏技术性,要提供新体验、要传达情感、要体现人性化。 我们总把用户当做技术专家来看待、当做机器人来看待,但是用户要的不是这个东西。所以在产品里,我们一直要坚持的一个原则是,尽可能不要把技术指标暴露给用户:会禁止显示正在下载,每秒钟多少K的这些数字,“下载”两个字样尽可能不显示。 同样,我们在做一个“what’s new”的时候,为什么要做一个新功能介绍?用户真的对你的功能、特性感兴趣吗?虽然这是大家的日常工作,但用户不需要。用户要的不是了解你的参数、特性、技术指标这些东西,用户要的是你给他提供了什么新的体验。 比如微信第一次你可以使用透明背景动画表情来表达你此刻的心情,我们告诉用户的是这可以表达你的心情,而不是说我们做了一个很牛B的动画表情。用户要的不是动画,用户要的是我可以表达心情,这点是最重要的。虽然我们这里面包含了很复杂的技术,但我们把它隐藏起来了。 但我们即使做了这么强的技术,我们也不会跟哪个用户说,你看我们这个技术很牛。我们会告诉什么才是他要的,是表达他的心情。我们宁愿把那个隐藏起来,告诉用户说你可以和朋友玩剪刀、石头、布的游戏,它是可以玩的。 对于用户只关心能不能用,好不好用,从不关心你怎么实现,可能两行代码就能实现但是很符合用户需要,对这就是好功能。PS很强大但是美图秀秀才是大众使用的神器,美图秀秀在给你美颜的过程中从来不会讲曝光度,对比度之类的专业名词,给你看到的只有变白,变瘦,变大眼,整个过程不会展示任何技术细节,前几天小米cc9发布会一出大家才知道,原来美图背后有这么专业的团队,专业实验室。 有的时候我们程序里肯定有 bug,当产品做得很好的时候,我就容忍这种 bug。我说有 bug 也是人性的体现。 其实产品是可以表达产品经理自己情感的地方,我们以前也放过迈克尔·杰克逊的这一句话:“如果你说我是错的,那你最好证明你是对的。”为什么放这一句话?其实当时很多评论家老是批评我们,你们这里做得不好,那里做得不好。我觉得用这句话来回应这些评论家挺好。 再举一个例子说人性化的用户交流——如何把邮箱系统管理员改为具象的产品经理。以前是系统管理员这样的邮件,把它去掉,改成我们具体的一个产品经理人的名字和他的图象。这样的话,用户每收到一个邮件,全部是一个邮箱的产品经理发过来的,并且有他自己的图象、名字、落款和日期。这就像有一个服务专员给你发邮件,而不是一个系统管理员。 产品要人性化:举一个例子,在 Windows 里有一个专门的“程序管理器”来管理要卸载哪些程序、安装哪些程序,普通用户还专门去卸载,这个挺有难度,但是我们在 iPhone 里是怎么卸载程序的?常按并删了就没有了。你不会看到有一个“程序管理器”在那里,不会看到“卸载”两个字。而且你常按的时候,会发现很多图表在那里晃动,为什么在晃动?表示不稳定状态,表示你可以操作。我后来看那个解释,解释得非常好,他说表示那些表情在哀求你不要删掉我。如果你把那个图标画成一张脸,你会发现确实是这样,在抖啊抖。 产品经理应知道用户的欲望,给他们制定简单的规则,让他们按规则运转和演化。 我们喜欢简单,因为上帝创造宇宙的时候,定下来的规则也非常简单。 为什么说产品经理是站在上帝身边的人?一是我想奉承一下产品经理,赞美一下大家,另外一点是说大家很像上帝。上帝是什么样的人?他建立了简单的规则,然后让这个世界演化。 我想表达的是,产品经理和上帝一样也会俯瞰芸芸众生,知道他们的欲望,然后给他们制定一些规则,让他们按照这个规则来运转。当你做到这一点,你就会像上帝一样,会有上帝的成就感。 首先你要了解人们的欲望,然后通过你的产品去满足,并且他们使用的过程是按照你的预期来的。 小程序的一个事例“跳一跳”,就一个规则跳过去就继续,过不去就结束,本来无比简单的应用为什么获取到这么大流量,因为他充分的了解用户需求和欲望。上手快,一旦开始玩了,就停不下来,输了分分钟再来一局新的,反正复活快,又不需要匹配队友,充分利用用户闲散时间,减压娱乐;排行榜激发了人们的攀比心态,总是想比别人分高。本来简单的规则在更新过程中进行丰富。这个游戏还有一个加分技巧,也就是所谓的游戏规则: 跳到音乐盒上等一会,播放音乐,加30分; 跳到便利店等一会,开门后,加15分 跳到魔方上等一会,魔方旋转,加10分; 跳到污水井盖停一会,冲水后,加5分 [图片] 隐藏的这些规则提高了游戏可玩性,同时又不明确说明需要用户自己探索。 甚至于后来用户们自己探索出新的用法,酒桌喝酒用小游戏,谁死了谁喝酒。可能很多人不知道“跳一跳”后来就新增了多人玩法。每次每人跳一步,轮到谁跳,谁就开始表演;个人失败后,就只能围观其他人战斗了;下一个人可以从失败基座继续,看谁坚持到跳到最后;游戏结束后,以跳的次数先后排名,是时候见证你的实力了。 [图片] 作为微信的“亲儿子”,“跳一跳”真的是一款成功的小程序,充分体现了段落标题的思想。 产品要研究人(用户或自己)的心理。 刚才我提到其实我们的产品经理工作里很多是研究人的心理,其实研究客户心理就是研究自我,很多时候我们是瞄准自我的需求来做产品的。当我们研究不到用户需求时,我们就会说只要让我们自己用得爽,这个是比较容易做到的一点。怎么样让用户用得爽呢?如果光凭一些体验的话,其实是有一个比较简单的方法,把自己当作一个傻瓜来用产品,傻瓜心态。 什么是一个“傻瓜化”的过程?把你自己当傻瓜的过程是说你要放下你脑袋里面所有装下的事,这个时候你就想你就是一个很初级、什么都不懂的用户,然后你来用这个东西,这个非常难做到。如果做不到,你就拉一个用户过来,你看着他用。 好的产品是用完就走。 用完就走其实是 Google 的哲学,但是我们很多产品的考核指标是用户在这里停留的时长有多少。这是一个选择,你是选择给用户自由一些,还是把他拉到这里。我们的选择是说用完就走了,而不是说一定要让他黏在这里,因为他下次还会回来。 [图片] 最后:就像张小龙先生所说的那样,未来微信不是一个软件,而是一种生活方式,小程序将进入生活中方方面面,方便生活,融入生活,改变生活。腾讯用心创造快乐,小程序用心改变生活,代码编译未来,未来掌握在们手中。
2019-07-29 - 小程序实现人脸识别动效
[图片] wxml [代码]<view class='voice'></view> [代码] css [代码].voice { display: block; width: 212rpx; height: 238rpx; margin: 0 auto; background: url(http://clients-80105.oss-cn-hangzhou.aliyuncs.com/%E8%90%8D%E8%90%8D%E4%BA%BA%E8%84%B8%E8%AF%86%E5%88%AB/voice.png) 0 0 no-repeat; background-size: 1272rpx 238rpx; animation: step 2s steps(6) infinite; } @keyframes step { 100% { background-position: -1272rpx 0; } } [代码]
2019-07-31 - 关于组件open-data的建议
[代码]<open-data type="userAvatarUrl"></open-data>[代码]现在能展示当前用户头像,能不能实现多一个属性openId,则可以展示对应openId的头像,这样就能实现用户可以不授权公开信息也能做排行榜。如果觉得openId在网络传输不安全,可以提示开发者做一层openId加密或多个类似openId的标识字段。
2018-09-28 - 如何做左上角返回拦截弹框?
定制左上角返回按钮一般有两个目的: 引导按钮:页面作为小程序启动后第一个页面时,左上角没有操作按钮,用户只能点击在右边胶囊更多回到首页(入口太深)。定制顶部可以引导用户很方便去其他页面。 拦截弹框:比如当用户点击返回时,弹出弹框,挽留用户。 效果: 点击查看没定制顶部录屏,发现没定制时,用户只能点击右边回到首页,考虑入口太深,故在左边做了个很奇怪的返回icon。点击可自己体验效果 点击查看有定制顶部录屏,虽然也是启动后第一个页面,但左上角有按钮返回首页,并当用户抽到红包后,返回会弹框挽留用户。点击可自己体验效果 开发顶部组件知识: 主要知识是组件开发、 wx.getMenuButtonBoundingClientRect()的使用。 代码下载说明: 由于在文章里贴代码导致文章比较乱,所以示例放在代码块里,大家可在开发工具输入链接直接获取,或直接在链接下载文件。 顶部组件代码块: https://developers.weixin.qq.com/s/A3Ki7Bmi7V9Z。 或点击下载示例代码 备注: 代码里有对应注释,我也不确定大家能不能看懂?虽然是很小的组件,代码量也不多,但如果有超过5位同学留言有必要观看视频来熟悉自定义顶部组件的开发过程,我也可录个视频。 推荐阅读 亲测有效隐藏scroll-view滚动条方法 实现自定义等间隔的tab
2019-07-04 - 实现自定义等间隔的tab
在开发小程序时,不知大家有没遇过顶部自定义tab?我经常遇到。 效果图如下: [图片] 刚开始想用movable-view,但无法判断每个item具体的宽度,只能固定item宽并居中,但这样字数不同时,间距则不等宽,最终使用scroll-view。 实现代码 //index.wxml <scroll-view class=‘tab_list’ scroll-x> <view class=‘tab_item’ wx:for="{{tabList}}" wx:key="{{index}}">{{item}}</view> </scroll-view> [代码]//index.wxss .tab_list { width: 750rpx; height: 90rpx; white-space: nowrap; } .tab_item { height: 90rpx; line-height: 90rpx; padding: 0 30rpx; background-color: lightgray; display: inline-block; margin-right: 2rpx; } ::-webkit-scrollbar { display: none; } [代码] //index.js Page({ data: { tabList: [‘图像去雾’, ‘图像增强对比度’, ‘图片无损放大’, ‘黑白图片上色’, ‘拉伸图片恢复’] }, onLoad: function(options) { } }) 代码效果图: [图片] 若想看线上产品样式戳 识图取字tab 或 多多提顶部tab
2019-06-14 - 亲测有效隐藏scroll-view滚动条方法
在有scroll-view滚动条页面的wxss里,例如在首页index.wxss,添加 [代码]::-webkit-scrollbar { display: none; width: 0; height: 0; color: transparent; } [代码] 不用选择器,以及不能在app.wxss直接添加。
2019-06-11 - CSS 火焰?不在话下
正文从下面开始。 今天的小技巧是使用纯 CSS 生成火焰,逼真一点的火焰。 嗯,长什么样子?在 CodePen 上输入关键字 [代码]CSS Fire[代码],能找到这样的: [图片] 或者这样的: [图片] 我们希望,仅仅使用 CSS ,效果能再更进一步吗?能不能是这样子: [图片] 如何实现 嗯,我们需要使用 [代码]filter[代码] + [代码]mix-blend-mode[代码] 的组合来完成。 很多 CSS 华而不实的效果都是 [代码]filter[代码] + [代码]mix-blend-mode[代码],很有意思,但是业务中根本用不上,当然多了解了解总没坏处。 如上图,整个蜡烛的骨架, 除去火焰的部分很简单,掠过不讲。主要来看看火焰这一块如何生成,并且如何赋予动画效果。 Step 1: filter blur && filter contrast 模糊滤镜叠加对比度滤镜产生的融合效果。 单独将两个滤镜拿出来,它们的作用分别是: [代码]filter: blur()[代码]: 给图像设置高斯模糊效果。 [代码]filter: contrast()[代码]: 调整图像的对比度。 但是,当他们“合体”的时候,产生了奇妙的融合现象。 先来看一个简单的例子: [图片] 仔细看两圆相交的过程,在边与边接触的时候,会产生一种边界融合的效果,通过对比度滤镜把高斯模糊的模糊边缘给干掉,利用高斯模糊实现融合效果。 利用上述 [代码]filter blur & filter contrast[代码],我们要先生成一个类似火焰形状的三角形。(略去过程) 这里类似火焰形状的三角形的具体实现过程,在这篇文章有详细的讲解:你所不知道的 CSS 滤镜技巧与细节 [图片] 父元素添加 [代码]filter: blur(5px) contrast(20)[代码],会变成这样: [图片] Step 2: 火焰粒子动画 看着已经有点样子了,接下来是火焰动画,我们先去掉父元素的 [代码]filter: blur(5px) contrast(20)[代码] ,然后继续 。 这里也是利用了 [代码]filter[代码] 的融合效果,我们在上述火焰中,利用 SASS 随机均匀分布大量大小不一的圆形棕色 div ,隐匿在火焰三角内部,大概是这样: [图片] 接下来,我们再利用 SASS,给中间每个小圆赋予一个从下往上逐渐消失的动画,并且均匀赋予不同的 [代码]animation-delay[代码],看起来会是这样: [图片] OK,最重要的一步,我们再把父元素的 [代码]filter: blur(5px) contrast(20)[代码] 打开,神奇的火焰效果就出来了: [图片] Step 3: mix-blend-mode 润色 当然,上述效果已经很不错了。经过各种尝试,调整参数,最后我发现加上 [代码]mix-blend-mode: screen[代码] 混合模式,效果更好,得到头图上面的最终效果如下: [图片] 完整源码在我的 CodePen 上:CodePen Demo – CSS Fire 另外一些效果 当然,掌握了这种方法后,这种生成火焰的技巧也可以迁移到其他效果去。下图是我鼓捣到另外一个小 Demo,当 hover 到元素的时候,产生火焰效果: [图片] CodePen Demo – Hover Fire 嗯,这些其实都是对滤镜及混合模式的一些搭配运用。按照惯例,肯定有人会留言喷了,整这些花里胡哨的有什么用,性能又不好,业务中敢上不把你的腿给打骨折。 [图片] 于我而言,虚心接受各种批评质疑及各种不同的观点,当然我是觉得搞技术一方面是实用,另一方面是兴趣使然,自娱自乐。希望喷子绕道~ 回到正题,了解了这种黏糊糊湿答答的技巧后,还可以折腾出其他很多有意思的效果,当然可能需要更多的去尝试,如下面使用一个标签实现的滴水效果: [图片] CodePen Demo – 单标签实现滴水效果 值得注意的细节点 动画虽然美好,但是具体使用的过程中,仍然有一些需要注意的地方: CSS 滤镜可以给同个元素同时定义多个,例如 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] ,但是滤镜的先后顺序不同产生的效果也是不一样的; 也就是说,使用 [代码]filter: blur(5px) contrast(150%) brightness(1.5)[代码] 和 [代码]filter: brightness(1.5) contrast(150%) blur(5px)[代码] 处理同一张图片,得到的效果是不一样的,原因在于滤镜的色值处理算法对图片处理的先后顺序。 滤镜动画需要大量的计算,不断的重绘页面,属于非常消耗性能的动画,使用时要注意使用场景。记得开启硬件加速及合理使用分层技术; [代码]blur()[代码] 混合 [代码]contrast()[代码] 滤镜效果,设置不同的颜色会产生不同的效果,这个颜色叠加的具体算法暂时没有找到很具体的规则细则,使用时比较好的方法是多尝试不同颜色,观察取最好的效果; 细心的读者会发现上述效果都是基于黑色底色进行的,动手尝试将底色改为白色,效果会大打折扣。 最后 本文只是简单的介绍了整个思路过程,许多 CSS 代码细节,调试过程没有展现出来。主要几个 CSS 属性默认大家已经掌握了大概,阅读后可以自行去了解补充更多细节: [代码]filter[代码] [代码]mix-blend-mode[代码] 更多精彩 CSS 技术文章汇总在我的 Github – iCSS ,持续更新,欢迎点个 star 订阅收藏。 好了,本文到此结束,希望对你有帮助 😃 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。 最后,新开通的公众号求关注,形式希望是更短的篇幅,质量更高一些的技巧类文章,包括但不局限于 CSS: [图片]
2019-04-26