- 微信小程序结合腾讯地图获取用户所在城市信息
背景 实现小程序进去后会获取用户当前所在城市,然后显示该城市的数据,并且显示在导航栏和 Tab上。 微信小程序中,我们可以通过调用[代码]wx.getLocation()[代码]获取到设备当前的地理位置信息,这个信息是当前位置的经纬度。如果我们想获取当前位置是处于哪个国家,哪个城市等信息,该如何实现呢? 微信小程序中并没有提供这样的API,但是没关系,有[代码]wx.getLocation()[代码]得到的经纬度作为基础就够了,其他的,我们可以使用其他第三方地图服务可以来实现,比如腾讯地图API。 所以整个步骤就是: 在小程序中获取当前的地理位置,涉及小程序API为[代码]wx.getLocation[代码] 把第1步中获得的经纬度信息通过腾讯地图的接口逆地址解析,涉及腾讯地图接口为reverseGeocoder(options:Object) 在小程序中获取当前的地理位置 在小程序中,调用[代码]wx.getLocation[代码],使用前需要用户授权[代码]scope.userLocation[代码],代码如下 [代码]checkAuth(callback) { wx.getSetting({ success(res) { if (!res.authSetting\['scope.userLocation'\]) { wx.authorize({ scope: 'scope.userLocation', success() { wx.getLocation({ type: 'wgs84', success(res) { callback(res.latitude, res.longitude) } }) } }) } } }) } [代码] 其中[代码]type[代码]的取值可以为: [代码]wgs84[代码]意思返回 gps 坐标 [代码]gcj02[代码]返回可用于[代码]wx.openLocation[代码]的坐标 运行后会提示如下信息,还需要在 app.json 中配置permission字段 [图片] 查询文档后得知,得知需要如下配置 [代码]"permission": { "scope.userLocation": { "desc": "你的位置信息将用于小程序位置接口的效果展示" } } [代码] desc 用于在弹出的授权提示框中展示,如下 [图片] 允许后即可获取接口返回的信息,此过程会在右上角胶囊按钮上显示箭头图标 [代码]{ accuracy: 65 errMsg: "getLocation:ok" horizontalAccuracy: 65 latitude: 30.25961 // 纬度,范围为 -90~90,负数表示南纬 longitude: 120.13026 // 经度,范围为 -180~180,负数表示西经 speed: \-1 verticalAccuracy: 65 } [代码] [代码]latitude[代码]和[代码]longitude[代码]即是我们需要的两个字段 腾讯地图接口逆地址解析 以腾讯地图为例,我们可以去腾讯地图开放平台注册一个账号,然后在它的管理后台创建一个密钥(key),以及进行KEY设置,按照微信小程序JavaScript SDK入门及使用限制文档 [图片] 在KEY设置的启用产品中,勾选 WebServiceAPI,选择签名校验方式,因为我是使用云开发的方式,所以没有什么域名也没有授权IP。 [图片] 这部分代码逻辑如下 [代码]import QQMapWX from '../../scripts/qqmap-wx-jssdk.min.js' let qqmapsdk Page({ onLoad: function (options) { // 实例化API核心类 qqmapsdk = new QQMapWX({ key: '开发密钥(key)' // 必填 }); this.checkAuth((latitude, longitude) => { // https://lbs.qq.com/qqmap\_wx\_jssdk/method-reverseGeocoder.html qqmapsdk.reverseGeocoder({ sig: 'KEY设置中生成的SK字符串', // 必填 location: {latitude, longitude}, success(res) { wx.setStorageSync('loca\_city', res.result.ad\_info.city) }, fail(err) { console.log(err) wx.showToast('获取城市失败') }, complete() { // 做点什么 } }) }) } }) [代码] [代码]reverseGeocoder[代码]接口返回的结果,这里面的字段比较多,详细可以看接口文档,里面好几个字段可以取到城市,其中[代码]ad_info[代码]是行政区划信息,我就取这里面的[代码]city[代码]了。 [图片] 以上内容转载自面糊的文章《【实战】小程序中结合腾讯地图获取用户所在城市信息》 链接:https://segmentfault.com/a/1190000021318458#comment-area 来源:segmentfault 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2020-08-28 - 浅谈小程序运行机制
摘要: 理解小程序原理… 原文:浅谈小程序运行机制 作者:小白 Fundebug经授权转载,版权归原作者所有。 写作背景 接触小程序有一段时间了,总得来说小程序开发门槛比较低,但其中基本的运行机制和原理还是要懂的。“比如我在面试的时候问到一个关于小程序的问题,问小程序有window对象吗?他说有吧”,但其实是没有的。感觉他并没有了解小程序底层的一些东西,归根结底来说应该只能算会使用这个工具,但并不明白其中的道理。 小程序与普通网页开发是有很大差别的,这就要从它的技术架构底层去剖析了。还有比如习惯Vue,react开发的开发者会吐槽小程序新建页面的繁琐,page必须由多个文件组成、组件化支持不完善、每次更改 data 里的数据都得setData、没有像Vue方便的watch监听、不能操作Dom,对于复杂性场景不太好,之前不支持npm,不支持sass,less预编译处理语言。 “有的人说小程序就像被阉割的Vue”,哈哈当然了,他们从设计的出发点就不同,咱也得理解小程序设计的初衷,通过它的使用场景,它为什么采用这种技术架构,这种技术架构有什么好处,相信在你了解完这些之后,就会理解了。下面我会从以下几个角度去分析小程序的运行机制和它的整体技术架构。 了解小程序的由来 在小程序没有出来之前,最初微信WebView逐渐成为移动web重要入口,微信发布了一整套网页开发工具包,称之为 JS-SDK,给所有的 Web 开发者打开了一扇全新的窗户,让所有开发者都可以使用到微信的原生能力,去完成一些之前做不到或者难以做到的事情。 但JS-SDK 的模式并没有解决使用移动网页遇到的体验不良的问题,比如受限于设备性能和网络速度,会出现白屏的可能。因此又设计了一个增强版JS-SDK,也就是“微信 Web 资源离线存储”,但在复杂的页面上依然会出现白屏的问题,原因表现在页面切换的生硬和点击的迟滞感。这个时候需要一个 JS-SDK 所处理不了的,使用户体验更好的一个系统,小程序应运而生。 快速的加载 更强大的能力 原生的体验 易用且安全的微信数据开放 高效和简单的开发 小程序与普通网页开发的区别 小程序的开发同普通的网页开发相比有很大的相似性,小程序的主要开发语言也是 JavaScript,但是二者还是有些差别的。 普通网页开发可以使用各种浏览器提供的 DOM API,进行 DOM 操作,小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API。 普通网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。 网页开发者在开发网页的时候,只需要使用到浏览器,并且搭配上一些辅助工具或者编辑器即可。小程序的开发则有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目等等过程方可完成。 小程序的执行环境 [图片] 小程序架构 一、技术选型 一般来说,渲染界面的技术有三种: 用纯客户端原生技术来渲染 用纯 Web 技术来渲染 用客户端原生技术与 Web 技术结合的混合技术(简称 Hybrid 技术)来渲染 通过以下几个方面分析,小程序采用哪种技术方案 开发门槛:Web 门槛低,Native 也有像 RN 这样的框架支持 体验:Native 体验比 Web 要好太多,Hybrid 在一定程度上比 Web 接近原生体验 版本更新:Web 支持在线更新,Native 则需要打包到微信一起审核发布 管控和安全:Web 可跳转或是改变页面内容,存在一些不可控因素和安全风险 由于小程序的宿主环境是微信,如果用纯客户端原生技术来编写小程序,那么小程序代码每次都需要与微信代码一起发版,这种方式肯定是不行的。 所以需要像web技术那样,有一份随时可更新的资源包放在云端,通过下载到本地,动态执行后即可渲染出界面。如果用纯web技术来渲染小程序,在一些复杂的交互上可能会面临一些性能问题,这是因为在web技术中,UI渲染跟JavaScript的脚本执行都在一个单线程中执行,这就容易导致一些逻辑任务抢占UI渲染的资源。 所以最终采用了两者结合起来的Hybrid 技术来渲染小程序,可以用一种近似web的方式来开发,并且可以实现在线更新代码,同时引入组件也有以下好处: 扩展 Web 的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力 体验更好,同时也减轻 WebView 的渲染工作 绕过 setData、数据通信和重渲染流程,使渲染性能更好 用客户端原生渲染内置一些复杂组件,可以提供更好的性能 二、双线程模型 小程序的渲染层和逻辑层分别由 2 个线程管理:视图层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS脚本。 [图片] [图片] 那么为什么要这样设计呢,前面也提到了管控和安全,为了解决这些问题,我们需要阻止开发者使用一些,例如浏览器的window对象,跳转页面、操作DOM、动态执行脚本的开放性接口。 我们可以使用客户端系统的 JavaScript 引擎,iOS 下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境。 这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。 这就是小程序双线程模型的由来: 逻辑层:创建一个单独的线程去执行 JavaScript,在这里执行的都是有关小程序业务逻辑的代码,负责逻辑处理、数据请求、接口调用等 视图层:界面渲染相关的任务全都在 WebView 线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以视图层存在多个 WebView 线程 JSBridge 起到架起上层开发与Native(系统层)的桥梁,使得小程序可通过API使用原生的功能,且部分组件为原生组件实现,从而有良好体验 三、双线程通信 把开发者的 JS 逻辑代码放到单独的线程去运行,但在 Webview 线程里,开发者就没法直接操作 DOM。 那要怎么去实现动态更改界面呢? 如上图所示,逻辑层和试图层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。 这也就是说,我们可以把 DOM 的更新通过简单的数据通信来实现。 Virtual DOM 相信大家都已有了解,大概是这么个过程:用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。 如图所示: [图片] 1. 在渲染层把 WXML 转化成对应的 JS 对象。 2. 在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。 3. 经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。 我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。 而这样一个完整的框架,离不开小程序的基础库。 四、小程序的基础库 小程序的基础库可以被注入到视图层和逻辑层运行,主要用于以下几个方面: 在视图层,提供各类组件来组建界面的元素 在逻辑层,提供各类 API 来处理各种逻辑 处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑 由于小程序的渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库。 小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端。 这样可以: 降低业务小程序的代码包大小 可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包 五、Exparser 框架 Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。 Exparser的主要特点包括以下几点: 基于Shadow DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。 小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。 内置组件 基于Exparser框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。 六、运行机制 小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台状态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。 小程序没有重启的概念 当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是5分钟)会被微信主动销毁 当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁 [图片] 七、更新机制 小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 如果需要马上应用最新版本,可以使用 wx.getUpdateManager API 进行处理。 八、性能优化 主要的优化策略可以归纳为三点: 精简代码,降低WXML结构和JS代码的复杂性; 合理使用setData调用,减少setData次数和数据量; 必要时使用分包优化。 1、setData 工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 2、常见的 setData 操作错误 频繁的去 setData在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时; 每次 setData 都传递大量新数据由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程, 后台态页面进行 setData当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。 总结 大致从以上几个角度分析了小程序的底层架构,从小程序的由来、到双线程的出现、设计、通信、到基础库、Exparser 框架、再到运行机制、性能优化等等,都是一个个相关而又相互影响的选择。关于小程序的底层框架设计,其实涉及到的还有很多,比如自定义组件,原生组件、性能优化等方面,都不是一点能讲完的,还要多看源码,多思考。每一个框架的诞生都有其意义,我们作为开发者能做的不只是会使用这个工具,还应理解它的设计模式。只有这样才不会被工具左右,才能走的更远!
2019-06-14 - iphoneX兼容之自定义底部菜单
当我们需要自定义底部导航栏时 首先要解决iphoneX的底部大横条对这个兼容 通常不设置兼容 都会被挡住 如何编写 在你要编写的底部菜单中插入 样式 [代码]padding-bottom[代码][代码]: calc(env(safe-area-inset-[代码][代码]bottom[代码][代码]) / [代码][代码]2[代码][代码]) 即可兼容 [代码] [代码] 例如:css中插入[代码] [代码]@supports ([代码][代码]bottom[代码][代码]: constant(safe-area-inset-[代码][代码]bottom[代码][代码])) or ([代码][代码]bottom[代码][代码]: env(safe-area-inset-[代码][代码]bottom[代码][代码])) {[代码][代码] [代码][代码].fixed-wrap {[代码][代码] [代码][代码]height[代码][代码]: calc(env(safe-area-inset-[代码][代码]bottom[代码][代码]) / [代码][代码]2[代码][代码]);[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]100%[代码][代码];[代码][代码] [代码][代码]}[代码] [代码] [代码][代码].fixed-pay {[代码][代码] [代码][代码]padding-bottom[代码][代码]: calc(env(safe-area-inset-[代码][代码]bottom[代码][代码]) / [代码][代码]2[代码][代码]);[代码][代码] [代码][代码]}[代码] [代码]}[代码]其中 [代码]env(safe-area-inset-[代码][代码]bottom[代码][代码]) 是计算兼容的高度 通常一半即可 [代码] calc 是计算css 你也可以加入高度 假设有第二层 底部固定栏【即底部导航栏上面还有一层固定栏】 可如下编写 view.footer { bottom: calc(100rpx + env(safe-area-inset-bottom)); } 这样轻轻松松解决兼容 不需要写js代码 <-------------大横条-------------> [图片]
2019-05-28 - 可能是目前最全的koa源码解析指南
本文将按照以下顺序讲解koa,通过初读到精读的方式,一步一步讲解koa涉及的相关知识。 通过阅读完本文,你将了解以下内容: koa框架核心 类继承在koa中的应用 co的实现原理,是如何将generator转为async函数的 洋葱模型中间件实现原理 koa的统一错误处理机制 委托模式在koa中的应用 一、koa是什么 koa是一个精简的node框架,它主要做了以下事情: 基于node原生req和res为request和response对象赋能,并基于它们封装成一个context对象。 基于async/await(generator)的中间件洋葱模型机制。 koa1和koa2在源码上的区别主要是于对异步中间件的支持方式的不同。 koa1是使用generator、yield)的模式。 koa2使用的是async/await+Promise的模式。下文主要是针对koa2版本源码上的讲解。 二、初读koa源码 如果你看了koa的源码,会发现koa源码其实很简单,共4个文件。 [代码]── lib ├── application.js ├── context.js ├── request.js └── response.js [代码] 这4个文件其实也对应了koa的4个对象: [代码]── lib ├── new Koa() || ctx.app ├── ctx ├── ctx.req || ctx.request └── ctx.res || ctx.response [代码] 下面,我们先初步了解koa的源码内容,读懂它们,可以对koa有一个初步的了解。 2.1 application.js application.js是koa的入口(从koa文件夹下的package.json的main字段(lib/application.js)中可以得知此文件是入口文件),也是核心部分。 下面对核心代码进行了注释。 [代码]/** * 依赖模块,包括但不止于下面的,只列出核心需要关注的内容 */ const response = require('./response'); const compose = require('koa-compose'); const context = require('./context'); const request = require('./request'); const Emitter = require('events'); const convert = require('koa-convert'); /** * 继承Emitter,很重要,说明Application有异步事件的处理能力 */ module.exports = class Application extends Emitter { constructor() { super(); this.middleware = []; // 该数组存放所有通过use函数的引入的中间件函数 this.subdomainOffset = 2; // 需要忽略的域名个数 this.env = process.env.NODE_ENV || 'development'; // 通过context.js、request.js、response.js创建对应的context、request、response。为什么用Object.create下面会讲解 this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } // 创建服务器 listen(...args) { debug('listen'); const server = http.createServer(this.callback()); //this.callback()是需要重点关注的部分,其实对应了http.createServer的参数(req, res)=> {} return server.listen(...args); } /* 通过调用koa应用实例的use函数,形如: app.use(async (ctx, next) => { await next(); }); 来加入中间件 */ use(fn) { if (isGeneratorFunction(fn)) { fn = convert(fn); // 兼容koa1的generator写法,下文会讲解转换原理 } this.middleware.push(fn); // 将传入的函数存放到middleware数组中 return this; } // 返回一个类似(req, res) => {}的函数,该函数会作为参数传递给上文的listen函数中的http.createServer函数,作为请求处理的函数 callback() { // 将所有传入use的函数通过koa-compose组合一下 const fn = compose(this.middleware); const handleRequest = (req, res) => { // 基于req、res封装出更强大的ctx,下文会详细讲解 const ctx = this.createContext(req, res); // 调用app实例上的handleRequest,注意区分本函数handleRequest return this.handleRequest(ctx, fn); }; return handleRequest; } // 处理请求 handleRequest(ctx, fnMiddleware) { // 省略,见下文 } // 基于req、res封装出更强大的ctx createContext(req, res) { // 省略,见下文 } }; [代码] 从上面代码中,我们可以总结出application.js核心其实处理了这4个事情: 1. 启动框架 2. 实现洋葱模型中间件机制 3. 封装高内聚的context 4. 实现异步函数的统一错误处理机制 2.2 context.js [代码]const util = require('util'); const createError = require('http-errors'); const httpAssert = require('http-assert'); const delegate = require('delegates'); const proto = module.exports = { // 省略了一些不甚重要的函数 onerror(err) { // 触发application实例的error事件 this.app.emit('error', err, this); }, }; /* 在application.createContext函数中, 被创建的context对象会挂载基于request.js实现的request对象和基于response.js实现的response对象。 下面2个delegate的作用是让context对象代理request和response的部分属性和方法 */ delegate(proto, 'response') .method('attachment') ... .access('status') ... .getter('writable') ...; delegate(proto, 'request') .method('acceptsLanguages') ... .access('querystring') ... .getter('origin') ...; [代码] 从上面代码中,我们可以总结出context.js核心其实处理了这2个事情: 1. 错误事件处理 2. 代理response对象和request对象的部分属性和方法 2.3 request.js [代码]module.exports = { // 在application.js的createContext函数中,会把node原生的req作为request对象(即request.js封装的对象)的属性 // request对象会基于req封装很多便利的属性和方法 get header() { return this.req.headers; }, set header(val) { this.req.headers = val; }, // 省略了大量类似的工具属性和方法 }; [代码] request对象基于node原生req封装了一系列便利属性和方法,供处理请求时调用。 所以当你访问ctx.request.xxx的时候,实际上是在访问request对象上的赋值器(setter)和取值器(getter)。 2.4 response.js [代码]module.exports = { // 在application.js的createContext函数中,会把node原生的res作为response对象(即response.js封装的对象)的属性 // response对象与request对象类似,基于res封装了一系列便利的属性和方法 get body() { return this._body; }, set body(val) { // 支持string if ('string' == typeof val) { } // 支持buffer if (Buffer.isBuffer(val)) { } // 支持stream if ('function' == typeof val.pipe) { } // 支持json this.remove('Content-Length'); this.type = 'json'; }, } [代码] response对象与request对象类似,就不再赘述。 值得注意的是,返回的body支持Buffer、Stream、String以及最常见的json,如上示例所示。 三、深入理解koa源码 通过上面的阅读,相信对koa有了一个初步认识,但毕竟是走马观花,本着追根问底的学术精神,还需要对大量细节进行揣摩,下文会从初始化、启动应用、处理请求等的角度,来对这过程中比较重要的细节进行讲解及延伸,如果彻底弄懂,会对koa以及ES6、generator、async/await、co、异步中间件等有更深一步的了解。 3.1 初始化 koa实例化: [代码]const Koa = require('koa'); const app = new Koa(); [代码] koa执行源码: [代码]module.exports = class Application extends Emitter { constructor() { super(); this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; this.context = Object.create(context); //为什么要使用Object.create? 见下面原因 this.request = Object.create(request); this.response = Object.create(response); if (util.inspect.custom) { this[util.inspect.custom] = this.inspect; } } } [代码] 当实例化koa的时候,koa做了以下2件事: 继承Emitter,具备处理异步事件的能力。然而koa是如何处理,现在还不得而知,这里打个问号。 在创建实例过程中,有三个对象作为实例的属性被初始化,分别是context、request、response。还有我们熟悉的存放中间件的数组mddleware。这里需要注意,是使用Object.create(xxx)对this.xxx进行赋值。 Object.create(xxx)作用: 根据xxx创建一个新对象,并且将xxx的属性和方法作为新的对象的proto。 举个例子,代码在demo02: [代码]const a = { name: 'rose', getName: function(){ return 'rose' } }; const b = Object.create(a); console.log('a is ', a); console.log('b is ', b); [代码] 结果如下: [图片] 可以看到,a的属性和方法已经挂载在b的原型(proto)下了。 所以,当执行完 [代码]this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); [代码] 的时候,以context为例,其实是创建一个新对象,使用context对象来提供新创建对象的proto,并且将这个对象赋值给this.context,实现了类继承的作用。为什么不直接用this.context=context呢?我的理解是,这样会导致两者指向同一片内存,而不是实现继承的目的。 3.2 启动应用及处理请求 在实例化koa之后,接下来,使用app.use传入中间件函数, [代码]app.use(async (ctx,next) => { await next(); }); [代码] koa对应执行源码: [代码] use(fn) { if (isGeneratorFunction(fn)) { fn = convert(fn); } this.middleware.push(fn); return this; } [代码] 当我们执行app.use的时候,koa做了这2件事情: 判断是否是generator函数,如果是,使用koa-convert做转换(koa3将不再支持generator)。 所有传入use的方法,会被push到middleware中。 这里做下延伸讲解,如何将generator函数转为类async函数。 如何将generator函数转为类async函数 koa2处于对koa1版本的兼容,中间件函数如果是generator函数的话,会使用koa-convert进行转换为“类async函数”。(不过到第三个版本,该兼容会取消)。 那么究竟是怎么转换的呢? 我们先来想想generator和async有什么区别? 唯一的区别就是async会自动执行,而generator每次都要调用next函数。 所以问题变为,如何让generator自动执行next函数? 回忆一下generator的知识:每次执行generator的next函数时,它会返回一个对象: [代码]{ value: xxx, done: false } [代码] 返回这个对象后,如果能再次执行next,就可以达到自动执行的目的了。 看下面的例子: [代码]function * gen(){ yield new Promise((resolve,reject){ //异步函数1 if(成功){ resolve() }else{ reject(); } }); yield new Promise((resolve,reject){ //异步函数2 if(成功){ resolve() }else{ reject(); } }) } let g = gen(); let ret = g.next(); [代码] 此时ret = { value: Promise实例; done: false};value已经拿到了Promise对象,那就可以自己定义成功/失败的回调函数了。如: [代码]ret.value.then(()=>{ g.next(); }) [代码] 现在就大功告成啦。我们只要找到一个合适的方法让g.next()一直持续下去就可以自动执行了。 所以问题的关键在于yield的value必须是一个Promise。那么我们来看看co是如何把这些都东西都转化为Promise的: [代码]function co(gen) { var ctx = this; // 把上下文转换为当前调用co的对象 var args = slice.call(arguments, 1) // 获取参数 // we wrap everything in a promise to avoid promise chaining, // 不管你的gen是什么,都先用Promise包裹起来 return new Promise(function(resolve, reject) { // 如果gen是函数,则修改gen的this为co中的this对象并执行gen if (typeof gen === 'function') gen = gen.apply(ctx, args); // 因为执行了gen,所以gen现在是一个有next和value的对象,如果gen不存在、或者不是函数则直接返回gen if (!gen || typeof gen.next !== 'function') return resolve(gen); // 执行类似上面示例g.next()的代码 onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); // 执行每一个gen.next() } catch (e) { return reject(e); } next(ret); //把执行得到的返回值传入到next函数中,next函数是自动执行的关键 } function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a promise. */ function next(ret) { // 如果ret.done=true说明迭代已经完毕,返回最后一次迭代的value if (ret.done) return resolve(ret.value); // 无论ret.value是什么,都转换为Promise,并且把上下文指向ctx var value = toPromise.call(ctx, ret.value); // 如果value是一个Promise,则继续在then中调用onFulfilled。相当于从头开始!! if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }); } [代码] 请留意上面代码的注释。 从上面代码可以得到这样的结论,co的思想其实就是: 把一个generator封装在一个Promise对象中,然后再这个Promise对象中再次把它的gen.next()也封装出Promise对象,相当于这个子Promise对象完成的时候也重复调用gen.next()。当所有迭代完成时,把父Promise对象resolve掉。这就成了一个类async函数了。 以上就是如何把generator函数转为类async的内容。 好啦,我们继续回来看koa的源码。 当执行完app.use时,服务还没启动,只有当执行到app.listen(3000)时,程序才真正启动。 koa源码: [代码]listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } [代码] 这里使用了node原生http.createServer创建服务器,并把this.callback()作为参数传递进去。可以知道,this.callback()返回的一定是这种形式:(req, res) => {}。继续看下this.callback代码。 [代码]callback() { // compose处理所有中间件函数。洋葱模型实现核心 const fn = compose(this.middleware); // 每次请求执行函数(req, res) => {} const handleRequest = (req, res) => { // 基于req和res封装ctx const ctx = this.createContext(req, res); // 调用handleRequest处理请求 return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; // 调用context.js的onerror函数 const onerror = err => ctx.onerror(err); // 处理响应内容 const handleResponse = () => respond(ctx); // 确保一个流在关闭、完成和报错时都会执行响应的回调函数 onFinished(res, onerror); // 中间件执行、统一错误处理机制的关键 return fnMiddleware(ctx).then(handleResponse).catch(onerror); } [代码] 从上面源码可以看到,有这几个细节很关键: 1. compose(this.middleware)做了什么事情(使用了koa-compose包)。 2. 如何实现洋葱式调用的? 3. context是如何处理的?createContext的作用是什么? 4. koa的统一错误处理机制是如何实现的? 下面,来进行一一讲解。 koa-compose和洋葱式调用 先看第一、二个问题。 看看koa-compose的精简源码: [代码]module.exports = compose function compose(middleware) { return function (context, next) { //略 } } [代码] compose函数接收middleware数组作为参数,middleware中每个对象都是async函数,返回一个以context和next作为入参的函数,我们跟源码一样,称其为fnMiddleware。 在外部调用this.handleRequest的最后一行,运行了中间件: [代码]fnMiddleware(ctx).then(handleResponse).catch(onerror); [代码] 我们来看下fnMiddleware究竟是怎么实现的: [代码]function compose (middleware) { return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } [代码] 解释之前,我们通过一个例子来理解,假设加入了两个中间件。源码在demo01: [代码]const Koa = require('koa'); const app = new Koa(); app.use(async (ctx,next) => { console.log("1-start"); await next(); console.log("1-end"); }); app.use(async (ctx, next) => { console.log("2-start"); await next(); console.log("2-end"); }); app.listen(3000); [代码] 我们逐步执行, 0:fnMiddleware(ctx)运行; 0:执行dispatch(0); 0:进入dispatch函数,下图是各个参数对应的值。 [图片] 从参数的值可以得知,最终会执行这段代码: [代码]return Promise.resolve(fn(context, function next() { return dispatch(i + 1) })) [代码] 此时,fn就是第一个中间件,它是一个async函数,async函数会返回一个Promise对象,Promise.resolve()中若传入一个Promise对象的话,那么Promise.resolve将原封不动地返回这个Promise对象。 0:进入到第一个中间件代码内部,先执行“console.log(“1-start”)” 0:然后执行“await next()”,并开始等待next执行返回 1:进入到next函数后,执行的是dispatch(1),于是老的dispatch(0)压栈,开始从头执行dispatch(1),即把第二个中间件函数交给fn,然后开始执行,这就完成了程序的控制权从第一个中间件到第二个中间件的转移。下图是执行dispatch(1)时函数内变量的值: [图片] 1:进入到第二个中间件代码内部,先执行“console.log(“2-start”)”。然后执行“await next()”并等待next执行返回 2:进入next函数后,主要执行dispatch(2),于是老的dispatch(1)压栈,从头开始执行dispatch(2)。下图是执行dispatch(2)时函数内变量的值: [图片] 所以返回Promise.resolve(),此时第二个中间件的next函数返回了。 2:所以接下来执行“console.log(“2-end”)” 1:由此第二个中间件执行完成,把程序控制权交给第一个中间件。第一个中间件执行“console.log(“1-end”)” 0:终于完成了所有中间件的执行,如果中间没有异常,则返回Promise.resolve(),执行handleResponse回调;如有异常,则返回Promies.reject(err),执行onerror回调。 建议用上面例子进行调试源码来理解,会更加清晰。 至此,回答了上面提到的2个问题: compose(this.middleware)做了什么事情(使用了koa-compose包)。 如何实现洋葱式调用的? 单一context原则 接下来,我们再来看第3个问题context是如何处理的?createContext的作用是什么? context使用node原生的http监听回调函数中的req、res来进一步封装,意味着对于每一个http请求,koa都会创建一个context并共享给所有的全局中间件使用,当所有的中间件执行完后,会将所有的数据统一交给res进行返回。所以,在每个中间件中我们才能取得req的数据进行处理,最后ctx再把要返回的body给res进行返回。 请记住句话:每一个请求都有唯一一个context对象,所有的关于请求和响应的东西都放在其里面。 下面来看context(即ctx)是怎么封装的: [代码]// 单一context原则 createContext(req, res) { const context = Object.create(this.context); // 创建一个对象,使之拥有context的原型方法,后面以此类推 const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.state = {}; return context; } [代码] 本着一个请求一个context的原则,context必须作为一个临时对象存在,所有的东西都必须放进一个对象,因此,从上面源码可以看到,app、req、res属性就此诞生。 请留意以上代码,为什么app、req、res、ctx也存放在了request、和response对象中呢? 使它们同时共享一个app、req、res、ctx,是为了将处理职责进行转移,当用户访问时,只需要ctx就可以获取koa提供的所有数据和方法,而koa会继续将这些职责进行划分,比如request是进一步封装req的,response是进一步封装res的,这样职责得到了分散,降低了耦合度,同时共享所有资源使context具有高内聚的性质,内部元素互相能访问到。 在createContext中,还有这样一行代码: [代码]context.state = {}; [代码] 这里的state是专门负责保存单个请求状态的空对象,可以根据需要来管理内部内容。 异步函数的统一错误处理机制 接下来,我们再来看第四个问题:koa的统一错误处理机制是如何实现的? 回忆一下我们如何在koa中统一处理错误,只需要让koa实例监听onerror事件就可以了。则所有的中间件逻辑错误都会在这里被捕获并处理。如下所示: [代码]app.on('error', err => { log.error('server error', err) }); [代码] 这是怎么做到的呢?核心代码如下(在上面提到的application.js的handleRequest函数中): [代码]handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; // application.js也有onerror函数,但这里使用了context的onerror, const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); // 这里是中间件如果执行出错的话,都能执行到onerror的关键!!! return fnMiddleware(ctx).then(handleResponse).catch(onerror); } [代码] 这里其实会有2个疑问: 1.出错执行的回调函数是context.js的onerror函数,为什么在app上监听onerror事件,就能处理所有中间件的错误呢? 请看下context.js的onerror: [代码]onerror(err) { this.app.emit('error', err, this); } [代码] 这里的this.app是对application的引用,当context.js调用onerror时,其实是触发application实例的error事件 。该事件是基于“Application类继承自EventEmitter”这一事实。 2.如何做到集中处理所有中间件的错误? 我们再来回顾一下洋葱模型的中间件实现源码: [代码]function compose (middleware) { return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } [代码] 还有外部处理: [代码]// 这里是中间件如果执行出错的话,都能执行到onerror的关键!!! return fnMiddleware(ctx).then(handleResponse).catch(onerror); [代码] 主要涉及这几个知识点: async函数返回一个Promise对象 async函数内部抛出错误,会导致Promise对象变为reject状态。抛出的错误会被catch的回调函数(上面为onerror)捕获到。 await命令后面的Promise对象如果变为reject状态,reject的参数也可以被catch的回调函数(上面为onerror)捕获到。 这样就可以理解为什么koa能实现异步函数的统一错误处理了。 委托模式 最后讲一下koa中使用的设计模式——委托模式。 当我们在使用context对象时,往往会这样使用: ctx.header 获取请求头 ctx.method 获取请求方法 ctx.url 获取请求url 这些对请求参数的获取都得益于context.request的许多属性都被委托在context上了 [代码]delegate(proto, 'request') .method('acceptsLanguages') ... .access('method') ... .getter('URL') .getter('header') ...; [代码] 又比如, ctx.body 设置响应体 ctx.status 设置响应状态码 ctx.redirect() 请求重定向 这些对响应参数的设置都得益于koa中的context.response的许多方法都被委托在context对象上了: [代码]delegate(proto, 'response') .method('redirect') ... .access('status') .access('body') ...; [代码] 至于delegate的使用和源码就不展开了,有兴趣的同学可以看这里 以上就是对koa源码所涉及的所有知识点的解析啦,初次看可能会有点晕,建议结合源码和例子一起多看几次,就会领会到koa框架的简洁和优雅之美所在了~ 参考文章: https://koajs.com https://zhuanlan.zhihu.com/p/34797505 https://zhuanlan.zhihu.com/p/24559011 https://juejin.im/entry/59e747f0f265da431c6f668e https://www.jianshu.com/p/45ec555a6c83 https://juejin.im/post/5b9339136fb9a05d3634ba13 https://www.jianshu.com/p/feb98591a1e5
2019-03-14 - 跨界 - Omi 发布多端统一框架 Omip 打通小程序与 Web
Omip 今天,Omi 不仅仅可以开发桌面 Web、移动 H5,还可以直接开发小程序!直接开发小程序!直接开发小程序! → omijs.org → Github 地址 Omi 简介 Omi 框架是腾讯研发的下一代前端框架, 基于 Web Components 规范设计的组件化框架,可以开发 PC Web、移动端 H5,也可以直接使用 Omi 开发小程序。Omi 服务于腾讯的 H5 页面, PC 网站以及腾讯内部的一些管理系统和小程序等。自去年年底开源以来,该项目共获得 Star 数 7000+,贡献者 40+。Omi 借助京东 O2Team 优秀的 taro 多端统一框架,以及 Omi 开发团队和社区贡献者近期的共同努力,使 Omi 打通了小程序与 Web。细心的用户会发现,Omi 的 slogan 从 下一代 Web 框架 变更为 下一代前端框架, 因为 Omip 的加入,Omi 生于 Web 却能脱离 Web。 [图片] 同样的语法,同样的书写格式,运行在不同的平台、不同的环境,除了一些平台特有的API,几乎不用任何改动! 老的 Omi 项目做一些极其微小的改动(平台特性相关)就能跑在安卓/IOS的小程序里。 Learn Once, Write Anywhere Write Once, Run Anywhere [图片] Omip 特性 一次学习,多处开发,一次开发,多处运行 使用 JSX,表达能力和编程体验大于模板 支持使用 npm/yarn 安装管理第三方依赖 支持使用 ES6+ 支持使用 CSS 预编译器 小程序 API 优化,异步 API Promise 化 超轻量的依赖包,顺从小程序标签和组件的设计 快速开始 [代码]npm i omi-cli -g omi init-p my-app cd my-app npm start //开发小程序 npm run dev:h5 //开发 h5 npm run build:h5 //发布 h5 [代码] 把小程序目录设置到 dist 目录就可以愉快地调试了! node 版本要求 >= 8 也支持一条命令 [代码]npx omi-cli init-p my-app[代码] (npm v5.2.0+) 当然也支持 TypeScript: [代码]omi init-p-ts my-app [代码] TypeScript 的其他命令和上面一样,也支持小程序和 h5 SPA 开发。 Omip 多端示例 [图片] [图片] Omi 其他 [图片] [图片] 社区化发展,欢迎加入并贡献社区 目前 Omi 的贡献者遍布国内外各大公司(中国、韩国、美国、土耳其),Omi 共接受了快 40 位贡献者的文档和代码提交,核心贡献者共 11 名。欢迎有想法有能力有激情的开发者加入贡献者行列并最终能够进入 Omi Team。 你可以从这几个方面贡献: 1.翻译文档,目前有中文、英文和韩文,欢迎其他语言版本的翻译加入 2.提交补丁代码优化 Omi 3.积极参与 Issue 的讨论,如答疑解惑、提供想法或报告无法解决的错误 4.贡献案例,可以是管理后台、PC 网站、移动端 H5等等 5.完善文档,可以反复修正文档,让其更易懂,上手更快 6.扩展 Omi 生态,编写 Omi 自定义组件 7.分享与 Omi 的故事 8.写 Omi 相关的 blog 我们非常欢迎开发者们为腾讯开源贡献一份力量,相应也将给予贡献者激励以表认可与感谢。参见腾讯贡献者激励计划 Omi 交流群 欢迎加入Omi交流群,群聊号码:256426170,也可扫码加入: [图片] 感谢 感谢京东 O2Team taro 项目 感谢京东 O2Team taro 团队 Github https://github.com/Tencent/omi
2019-03-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 - 叠式轮播图
开发工具和iOS测过,android我没测过。。哈哈哈哈哈 https://developers.weixin.qq.com/s/kh8HhjmA7A4D 注释不知道写啥,简单描述了下 [图片]
2018-11-30