打通前后端逻辑,客户端Flutter代码一天上线

一、前沿 随着闲鱼的业务快速增长,运营类的需求也越来越多,其中不乏有很多界面修改或运营坑位的需求。闲鱼的版本现在是每2周一个版本,如何快速迭代产品,跳过窗口期来满足这些需求?另外,闲鱼客户端的包体也变的很大,企业包的大小,iOS已经到了94.3M,Android也到了53.5M。Android的包体大小,相比2016年,已经增长了近1倍,怎么能将包体大小降下来?首先想到的是如何动态化的解决此类问题。 对于原生的能力的动态化,Android平台各公司都有很完善的动态化方案,甚至Google还提供了Android App Bundles让开发者们更好地支持动态化。由于Apple官方担忧动态化的风险,因此并不太支持动态化。因此动态化能力就会考虑跟Web结合,从一开始基于 WebView 的 Hybrid 方案 PhoneGap、Titanium,到现在与原生相结合的 React Native 、Weex。 但Native和JavaScript Context之间的通讯,频繁的交互就成了程序的性能瓶颈。于此同时随着闲鱼Flutter技术的推广,已经有10多个页面用Flutter实现,上面提到的几种方式都不适合Flutter场景,如何解决这个问题Flutter的动态化的问题?二、动态方案我们最初调研了Google的动态化方案CodePush。2.1 CodePush CodePush是谷歌官方推出的动态化方案,目前只有在Android上面实现了。Dart VM在执行的时候,加载isolate_snapshot_data 和isolate_snapshot_instr 2个文件,通过动态更改这些文件,就达到动态更新的目的。官方的Flutter源码当中,已经有相关的提交来做动态更新的内容,具体内容可以参考 ResourceExtractor.java。 根据官方给出的Guide,我们这边也做了相关的测试,patch的包体大小会很大(939kb)。为了降低包体大小,还可以通过增量的修改snapshot文件的方式来更新。通过bsdiff生成的snapshot的差异文件,2个文件分别可以缩小到48kb和870kb。 目前看来,CodePush还不能做到很好的工程化。而且如何管理patch文件,需要制定baseline和patch文件的规则。2.2 动态模板 动态模板,就是通过定义一套DSL,在端侧解析动态的创建View来实现动态化,比如LuaViewSDK、Tangram-iOS和Tangram-Android。这些方案都是创建的Native的View,如果想在Flutter里面实现,需要创建Texture来桥接;Native端渲染完成之后,再将纹理贴在Flutter的容器里面,实现成本很高,性能也有待商榷,不适合闲鱼的场景。 所以我们提出了闲鱼自己的Flutter动态化方案,前面已经有同事介绍过方案的原理:《做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现》,下面看下具体的实现细节。三、模板编译自定义一套DSL,维护成本较高,怎么能不自定义DSL来实现模板下发?闲鱼的方案就是直接将Dart文件转化成模板,这样模板文件也可以快速沉淀到端侧。3.1 模板规范 先来看下一个完整的模板文件,以新版我的页面为例,这个是一个列表结构,每个区块都是一个独立的Widget,现在我们期望将“卖在闲鱼”这个区块动态渲染,对这个区块拆分之后,需要3个子控件:头部、菜单栏、提示栏;因为这3部分界面有些逻辑处理,所以先把他们的逻辑内置。内置的子控件分别是MenuTitleWidget、MenuItemWidget和HintItemWidget,编写的模板如下:@overrideWidget build(BuildContext context) { return new Container( child: new Column( children: <Widget>[ new MenuTitleWidget(data), // 头部 new Column( // 菜单栏 children: <Widget>[ new Row( children: <Widget>[ new MenuItemWidget(data.menus[0]), new MenuItemWidget(data.menus[1]), new MenuItemWidget(data.menus[2]), ], ) ], ), new Container( // 提示栏 child: new HintItemWidget(data.hints[0])), ], ), );}中间省略了样式描述,可以看到写模板文件就跟普通的widget写法一样,但是有几点要注意:每个Widget都需要用new或const来修饰数据访问以data开头,数组形式以[]访问,字典形式以.访问 模板写好之后,就要考虑怎么在端上渲染,早期版本是直接在端侧解析文件,但是考虑到性能和稳定性,还是放在前期先编译好,然后下发到端侧。3.2 编译流程 编译模板就要用到Dart的Analyzer库,通过parseCompilationUnit函数直接将Dart源码解析成为以CompilationUnit为Root节点的AST树中,它包含了Dart源文件的语法和语义信息。接下来的目标就是将CompilationUnit转换成为一个JSON格式。 上面的模板解析出来build函数孩子节点是ReturnStatementImpl,它又包含了一个子节点InstanceCreationExpressionImpl,对应模板里面的new Container(…),它的孩子节点中,我们最关心的就是ConstructorNameImpl和ArgumentListImpl节点。ConstructorNameImpl标识创建节点的名称,ArgumentListImpl标识创建参数,参数包含了参数列表和变量参数。定义如下结构体,来存储这些信息:class ConstructorNode { // 创建节点的名称 String constructorName; // 参数列表 List<dynamic> argumentsList = <dynamic>[]; // 变量参数 Map<String, dynamic> arguments = <String, dynamic>{};}递归遍历整棵树,就可以得到一个ConstructorNode树,以下代码是解析单个Node的参数:ArgumentList argumentList = astNode;for (Expression exp in argumentList.arguments) { if (exp is NamedExpression) { NamedExpression namedExp = exp; final String name = ASTUtils.getNodeString(namedExp.name); if (name == ‘children’) { continue; } /// 是函数 if (namedExp.expression is FunctionExpression) { currentNode.arguments[name] = FunctionExpressionParser.parse(namedExp.expression); } else { /// 不是函数 currentNode.arguments[name] = ASTUtils.getNodeString(namedExp.expression); } } else if (exp is PropertyAccess) { PropertyAccess propertyAccess = exp; final String name = ASTUtils.getNodeString(propertyAccess); currentNode.argumentsList.add(name); } else if (exp is StringInterpolation) { StringInterpolation stringInterpolation = exp; final String name = ASTUtils.getNodeString(stringInterpolation); currentNode.argumentsList.add(name); } else if (exp is IntegerLiteral) { final IntegerLiteral integerLiteral = exp; currentNode.argumentsList.add(integerLiteral.value); } else { final String name = ASTUtils.getNodeString(exp); currentNode.argumentsList.add(name); }}端侧拿到这个ConstructorNode节点树之后,就可以根据Widget的名称和参数,来生成一棵Widget树。四、渲染引擎端侧拿到编译好的模板JSON后,就是解析模板并创建Widget。先看下,整个工程的框架和工作流:工作流程:开发人员编写dart文件,编译上传到CDN端侧拿到模板列表,并在端侧存库业务方直接下发对应的模板id和模板数据Flutter侧再通过桥接获取到模板,并创建Widget树对于Native测,主要负责模板的管理,通过桥接输出到Flutter侧。4.1 模板获取模板获取分为2部分,Native部分和Flutter部分;Native主要负责模板的管理,包括下载、降级、缓存等。程序启动的时候,会先获取模板列表,业务方需要自己实现,Native层获取到模板列表会先存储在本地数据库中。Flutter侧业务代码用到模板的时候,再通过桥接获取模板信息,就是我们前面提到的JSON格式的信息,Flutter也会有缓存,已减少Flutter和Native的交互。4.2 Widget创建Flutter侧当拿到JSON格式的,先解析出ConstructorNode树,然后递归创建Widget。创建每个Widget的过程,就是解析节点中的argumentsList和arguments 并做数据绑定。例如,创建HintItemWidget需要传入提示的数据内容,new HintItemWidget(data.hints[0]),在解析argumentsList时,会通过key-path的方式从原始数据中解析出特定的值。解析出来的值都会存储在WidgetCreateParam里面,当递归遍历每个创建节点,每个widget都可以从WidgetCreateParam里面解析出需要的参数。/// 构建widget用的参数class WidgetCreateParam { String constructorName; /// 构建的名称 dynamic context; /// 构建的上下文 Map<String, dynamic> arguments = <String, dynamic>{}; /// 字典参数 List<dynamic> argumentsList = <dynamic>[]; /// 列表参数 dynamic data; /// 原始数据} 通过以上的逻辑,就可以将ConstructorNode树转换为一棵Widget树,再交给Flutter Framework去渲染。至此,我们已经能将模板解析出来,并渲染到界面上,交互事件应该怎么处理?4.3 事件处理在写交互的时候,一般都会通过GestureDector、InkWell等来处理点击事件。交互事件怎么做动态化? 以InkWell组件为例,定义它的onTap函数为openURL(data.hints[0].href, data.hints[0].params)。在创建InkWell时,会以OpenURL作为事件ID,查找对应的处理函数,当用户点击的时候,会解析出对应的参数列表并传递过去,代码如下:…final List<dynamic> tList = <dynamic>[];// 解析出参数列表exp.argumentsList.forEach((dynamic arg) { if (arg is String) { final dynamic value = valueFromPath(arg, param.data); if (value != null) { tList.add(value); } else { tList.add(arg); } } else { tList.add(arg); }});// 找到对应的处理函数final dynamic handler = TeslaEventManager.sharedInstance().eventHandler(exp.actionName);if (handler != null) { handler(tList);}…五、 效果新版我的页面添加了动态化渲染能力之后,如果有需求新添加一种组件类型,就可以直接编译发布模板,服务端下发新的数据内容,就可以渲染出来了;动态化能力有了,大家会关心渲染性能怎么样。5.1 帧率在加了动态加载逻辑之后,已经开放了2个动态卡片,下图是新版本我的页面近半个月的的帧率数据:从上图可以看到,帧率并没有降低,基本保持在55-60帧左右,后续可以多添加动态的卡片,观察下效果。注:因为我的页面会有本地的一些业务判断,从其他页面回到我的tab,都会刷新界面,所以帧率会有损耗。 从实现上分析,因为每个卡片,都需要遍历ConstructorNode树来创建,而且每个构建都需要解析出里面的参数,这块可以做一些优化,比如缓存相同的Widget,只需要映射出数据内容并做数据绑定。5.2 失败率现在监控了渲染的逻辑,如果本地没有对应的Widget创建函数,会主动抛Error。监控数据显示,渲染的流程中,还没有异常的情况,后续还需要对桥接层和native层加错误埋点。六、展望 基于Flutter动态模板,之前需要走发版的Flutter需求,都可以来动态化更改。而且以上逻辑都是基于Flutter原生的体系,学习和维护成本都很低,动态的代码也可以快速的沉淀到端侧。 另外,闲鱼正在研究UI2Code的黑科技,不了解的老铁,可以参考闲鱼大神的这篇文章《重磅系列文章!UI2CODE智能生成Flutter代码——整体设计篇》。可以设想下,如果有个需求,需要动态的显示一个组件,UED出了视觉稿,通过UI2Code转换成Dart文件,再通过这个系统转换成动态模板,下发到端侧就可以直接渲染出来,程序员都不需要写代码了,做到自动化运营,看来以后程序员失业也不是没有可能了。 基于Flutter的Widget,还可以拓展更多个性化的组件,比如内置动画组件,就可以动态化下发动画了,更多好玩的东西等待大家来一起探索。参考文献https://github.com/flutter/flutter/issues/14330https://www.dartlang.org/https://mp.weixin.qq.com/s/4s6MaiuW4VoHr_7f0S_vuQhttps://github.com/flutter/engine本文作者:闲鱼技术-景松阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 4, 2019 · 2 min · jiezi

uni-app项目保存图片到相册

github地址,喜欢的可以star下哦插件预览图功能介绍1.长按保存图片 2.右下角图片点击保存图片3.点击图片可以隐藏图片使用教程1.插件代码拷贝下载后把components目录下saveFile.vue文件拷贝到自己项目目录下2.插件全局配置在项目里main.js中配置如下代码import savefile from ‘./components/saveFile.vue’Vue.component(‘savefile’,savefile)3.插件使用vue页面使用<template> <view> <!– 预览图片 –> <savefile v-if=“isShowPhoto” :url=“qrUrl” @hide=“hidePhoto”></savefile> </view></template><script>export default { data() { return { qrUrl:’/static/img/img1.jpg’, isShowPhoto:true, }; }, onLoad() {}, methods: { hidePhoto(){ this.isShowPhoto = false; uni.showToast({ title:‘图片已隐藏’, icon:’none’ }) } }};</script>兼容性uni-app项目中使用都兼容 除了H5

March 29, 2019 · 1 min · jiezi

码上用它开始Flutter混合开发——FlutterBoost

开源地址:https://github.com/alibaba/flutter_boost为什么要混合方案具有一定规模的App通常有一套成熟通用的基础库,尤其是阿里系App,一般需要依赖很多体系内的基础库。那么使用Flutter重新从头开发App的成本和风险都较高。所以在Native App进行渐进式迁移是Flutter技术在现有Native App进行应用的稳健型方式。闲鱼在实践中沉淀出一套自己的混合技术方案。在此过程中,我们跟Google Flutter团队进行着密切的沟通,听取了官方的一些建议,同时也针对我们业务具体情况进行方案的选型以及具体的实现。官方提出的混合方案1基本原理Flutter技术链主要由C++实现的Flutter Engine和Dart实现的Framework组成(其配套的编译和构建工具我们这里不参与讨论)。Flutter Engine负责线程管理,Dart VM状态管理和Dart代码加载等工作。而Dart代码所实现的Framework则是业务接触到的主要API,诸如Widget等概念就是在Dart层面Framework内容。一个进程里面最多只会初始化一个Dart VM。然而一个进程可以有多个Flutter Engine,多个Engine实例共享同一个Dart VM。我们来看具体实现,在iOS上面每初始化一个FlutterViewController就会有一个引擎随之初始化,也就意味着会有新的线程(理论上线程可以复用)去跑Dart代码。Android类似的Activity也会有类似的效果。如果你启动多个引擎实例,注意此时Dart VM依然是共享的,只是不同Engine实例加载的代码跑在各自独立的Isolate。2官方建议引擎深度共享在混合方案方面,我们跟Google讨论了可能的一些方案。Flutter官方给出的建议是从长期来看,我们应该支持在同一个引擎支持多窗口绘制的能力,至少在逻辑上做到FlutterViewController是共享同一个引擎的资源的。换句话说,我们希望所有绘制窗口共享同一个主Isolate。但官方给出的长期建议目前来说没有很好的支持。多引擎模式我们在混合方案中解决的主要问题是如何去处理交替出现的Flutter和Native页面。Google工程师给出了一个Keep It Simple的方案:对于连续的Flutter页面(Widget)只需要在当前FlutterViewController打开即可,对于间隔的Flutter页面我们初始化新的引擎。例如,我们进行下面一组导航操作:我们只需要在Flutter Page1和Flutter Page3创建不同的Flutter实例即可。这个方案的好处就是简单易懂,逻辑清晰,但是也有潜在的问题。如果一个Native页面一个Flutter页面一直交替进行的话,Flutter Engine的数量会线性增加,而Flutter Engine本身是一个比较重的对象。多引擎模式的问题冗余的资源问题.多引擎模式下每个引擎之间的Isolate是相互独立的。在逻辑上这并没有什么坏处,但是引擎底层其实是维护了图片缓存等比较消耗内存的对象。想象一下,每个引擎都维护自己一份图片缓存,内存压力将会非常大。插件注册的问题。插件依赖Messenger去传递消息,而目前Messenger是由FlutterViewController(Activity)去实现的。如果你有多个FlutterViewController,插件的注册和通信将会变得混乱难以维护,消息的传递的源头和目标也变得不可控。Flutter Widget和Native的页面差异化问题。Flutter的页面是Widget,Native的页面是VC。逻辑上来说我们希望消除Flutter页面与Naitve页面的差异,否则在进行页面埋点和其它一些统一操作的时候都会遇到额外的复杂度。增加页面之间通信的复杂度。如果所有Dart代码都运行在同一个引擎实例,它们共享一个Isolate,可以用统一的编程框架进行Widget之间的通信,多引擎实例也让这件事情更加复杂。因此,综合多方面考虑,我们没有采用多引擎混合方案。现状及思考前面我们提到多引擎存在一些实际问题,所以闲鱼目前采用的混合方案是共享同一个引擎的方案。这个方案基于这样一个事实:任何时候我们最多只能看到一个页面,当然有些特定的场景你可以看到多个ViewController,但是这些特殊场景我们这里不讨论。我们可以这样简单去理解这个方案:我们把共享的Flutter View当成一个画布,然后用一个Native的容器作为逻辑的页面。每次在打开一个容器的时候我们通过通信机制通知Flutter View绘制成当前的逻辑页面,然后将Flutter View放到当前容器里面。这个方案无法支持同时存在多个平级逻辑页面的情况,因为你在页面切换的时候必须从栈顶去操作,无法再保持状态的同时进行平级切换。举个例子:有两个页面A,B,当前B在栈顶。切换到A需要把B从栈顶Pop出去,此时B的状态丢失,如果想切回B,我们只能重新打开B之前页面的状态无法维持住。如在pop的过程当中,可能会把Flutter 官方的Dialog进行误杀。而且基于栈的操作我们依赖对Flutter框架的一个属性修改,这让这个方案具有了侵入性的特点。具体细节,大家可以参考老方案开源项目地址:https://github.com/alibaba-flutter/hybridstackmanager新一代混合技术方案 FlutterBoost1重构计划在闲鱼推进Flutter化过程当中,更加复杂的页面场景逐渐暴露了老方案的局限性和一些问题。所以我们启动了代号FlutterBoost(向C++ Boost库致敬)的新混合技术方案。这次新的混合方案我们的主要目标有:可复用通用型混合方案支持更加复杂的混合模式,比如支持主页Tab这种情况无侵入性方案:不再依赖修改Flutter的方案支持通用页面生命周期统一明确的设计概念跟老方案类似,新的方案还是采用共享引擎的模式实现。主要思路是由Native容器Container通过消息驱动Flutter页面容器Container,从而达到Native Container与Flutter Container的同步目的。我们希望做到Flutter渲染的内容是由Naitve容器去驱动的。简单的理解,我们想做到把Flutter容器做成浏览器的感觉。填写一个页面地址,然后由容器去管理页面的绘制。在Native侧我们只需要关心如果初始化容器,然后设置容器对应的页面标志即可。2主要概念3Native层概念Container:Native容器,平台Controller,Activity,ViewControllerContainer Manager:容器的管理者Adaptor:Flutter是适配层Messaging:基于Channel的消息通信4Dart层概念Container:Flutter用来容纳Widget的容器,具体实现为Navigator的派生类-Container Manager:Flutter容器的管理,提供show,remove等ApiCoordinator: 协调器,接受Messaging消息,负责调用Container Manager的状态管理。Messaging:基于Channel的消息通信5关于页面的理解在Native和Flutter表示页面的对象和概念是不一致的。在Native,我们对于页面的概念一般是ViewController,Activity。而对于Flutter我们对于页面的概念是Widget。我们希望可统一页面的概念,或者说弱化抽象掉Flutter本身的Widget对应的页面概念。换句话说,当一个Native的页面容器存在的时候,FlutteBoost保证一定会有一个Widget作为容器的内容。所以我们在理解和进行路由操作的时候都应该以Native的容器为准,Flutter Widget依赖于Native页面容器的状态。那么在FlutterBoost的概念里说到页面的时候,我们指的是Native容器和它所附属的Widget。所有页面路由操作,打开或者关闭页面,实际上都是对Native页面容器的直接操作。无论路由请求来自何方,最终都会转发给Native去实现路由操作。这也是接入FlutterBoost的时候需要实现Platform协议的原因。另一方面,我们无法控制业务代码通过Flutter本身的Navigator去push新的Widget。对于业务不通过FlutterBoost而直接使用Navigator操作Widget的情况,包括Dialog这种非全屏Widget,我们建议是业务自己负责管理其状态。这种类型Widget不属于FlutterBoost所定义的页面概念。理解这里的页面概念,对于理解和使用FlutterBoost至关重要。6与老方案主要差别前面我们提到老方案在Dart层维护单个Navigator栈结构用于Widget的切换。而新的方案则是在Dart侧引入了Container的概念,不再用栈的结构去维护现有的页面,而是通过扁平化key-value映射的形式去维护当前所有的页面,每个页面拥有一个唯一的id。这种结构很自然的支持了页面的查找和切换,不再受制于栈顶操作的问题,之前的一些由于pop导致的问题迎刃而解。也不需要依赖修改Flutter源码的形式去进行页面栈操作,去掉了实现的侵入性。实际上我们引入的Container就是Navigator的,也就是说一个Native的容器对应了一个Navigator。那这是如何做到的呢?7多Navigator的实现Flutter在底层提供了让你自定义Navigator的接口,我们自己实现了一个管理多个Navigator的对象。当前最多只会有一个可见的Flutter Navigator,这个Navigator所包含的页面也就是我们当前可见容器所对应的页面。Native容器与Flutter容器(Navigator)是一一对应的,生命周期也是同步的。当一个Native容器被创建的时候,Flutter的一个容器也被创建,它们通过相同的id关联起来。当Native的容器被销毁的时候,Flutter的容器也被销毁。Flutter容器的状态是跟随Native容器,这也就是我们说的Native驱动。由Manager统一管理切换当前在屏幕上展示的容器。我们用一个简单的例子描述一个新页面创建的过程:创建Native容器(iOS ViewController,Android Activity or Fragment)。Native容器通过消息机制通知Flutter Coordinator新的容器被创建。Flutter Container Manager进而得到通知,负责创建出对应的Flutter容器,并且在其中装载对应的Widget页面。当Native容器展示到屏幕上时,容器发消息给Flutter Coordinator通知要展示页面的id.Flutter Container Manager找到对应id的Flutter Container并将其设置为前台可见容器。这就是一个新页面创建的主要逻辑,销毁和进入后台等操作也类似有Native容器事件去进行驱动。总结目前FlutterBoost已经在生产环境支撑着在闲鱼客户端中所有的基于Flutter开发业务,为更加负复杂的混合场景提供了支持,稳定为亿级用户提供服务。我们在项目启动之初就希望FlutterBoost能够解决Native App混合模式接入Flutter这个通用问题。所以我们把它做成了一个可复用的Flutter插件,希望吸引更多感兴趣的朋友参与到Flutter社区的建设。在有限篇幅中,我们分享了闲鱼在Flutter混合技术方案中积累的经验和代码。欢迎兴趣的同学能够积极与我们一起交流学习。扩展补充1性能相关在两个Flutter页面进行切换的时候,因为我们只有一个Flutter View所以需要对上一个页面进行截图保存,如果Flutter页面多截图会占用大量内存。这里我们采用文件内存二级缓存策略,在内存中最多只保存2-3个截图,其余的写入文件按需加载。这样我们可以在保证用户体验的同时在内存方面也保持一个较为稳定的水平。页面渲染性能方面,Flutter的AOT优势展露无遗。在页面快速切换的时候,Flutter能够很灵敏的相应页面的切换,在逻辑上创造出一种Flutter多个页面的感觉。2Release1.0的支持项目开始的时候我们基于闲鱼目前使用的Flutter版本进行开发,而后进行了Release 1.0兼容升级测试目前没有发现问题。3接入只要是集成了Flutter的项目都可以用官方依赖的方式非常方便的以插件形式引入FlutterBoost,只需要对工程进行少量代码接入即可完成接入。详细接入文档,请参阅GitHub主页官方项目文档。4现已开源目前,新一代混合栈已经在闲鱼全面应用。我们非常乐意将沉淀的技术回馈给社区。欢迎大家一起贡献,一起交流,携手共建Flutter社区。项目开源地址:https://github.com/alibaba/flutter_boost本文作者:福居阅读原文本文来自云栖社区合作伙伴“闲鱼技术”,如需转载请联系原作者。

March 18, 2019 · 1 min · jiezi

uniapp运行模拟器错误java.io.EOFException

java.io.EOFException at java.io.DataInputStream.readUnsignedShort(DataInputStream.java:340) at java.io.DataInputStream.readUTF(DataInputStream.java:589) at com.pandora.console.core.ConsoleLauncher.main(ConsoleLauncher.java:47)07:14:28.989 已停止运行…1.出问题原因:开启了翻墙软件,端口冲突了2.解决方法关闭翻墙软件即可

March 15, 2019 · 1 min · jiezi

uni-app项目数字滚动

uni-app项目数字滚动github地址,喜欢的可以star下哦插件预览图使用教程1.插件代码拷贝下载后把components目录下countUp.vue文件拷贝到自己项目目录下2.插件全局配置在项目里main.js中配置如下代码import countUp from ‘./components/countUp.vue’Vue.component(‘countup’,countUp)3.插件使用vue页面使用<template> <view> <countup :num=“num” color="#ff9e50" width=‘13’ height=‘23’ fontSize=‘23’></countup> <button @tap=“add”>Add</button> <button @tap=“reduce”>Reduce</button> </view></template><script>export default { data() { return { num:123.453 }; }, onLoad() {}, methods: { add() { this.num=++this.num; }, reduce(){ this.num=–this.num; } }};</script>兼容性uni-app项目中使用都兼容

February 20, 2019 · 1 min · jiezi

uni-app自定义验证码、密码输入框

Getting started插件预览图使用教程1.插件代码拷贝下载后把components目录下validCode.vue文件拷贝到自己项目目录下2.插件全局配置在项目里main.js中配置如下代码import validCode from ‘./components/validCode.vue’Vue.component(‘validcode’,validCode)3.插件使用vue页面使用<template> <view class=“content”> <view>验证码:</view> <validcode :maxlength=“4” :isPwd=“false” @finish=“getCode”></validcode> <view>密码:</view> <validcode :maxlength=“6” :isPwd=“true” @finish=“getPwd”></validcode> </view></template><script>export default { data() { return {}; }, onLoad() {}, methods: { getCode(val) { console.log(val); }, getPwd(val){ console.log(val); } }};</script><style>.content { height: 400upx;}</style>

February 19, 2019 · 1 min · jiezi

在Flutter中嵌入Native组件的正确姿势是...

引言在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一个很好的选择。本文希望向大家介绍AndroidView的使用方式以及在此基础之上拓展的双端嵌入Native组件的解决方案。1. 使用教程1.1. DemoRun嵌入地图这一场景可能在很多App中都会存在,但是现在的地图SDK都没有提供Flutter的库,而自己开发一套地图显然不太现实。这种场景下,使用混合栈的形式是一个比较好的选择。我们可以直接在Native的绘图树中嵌入一个Map,但是这个方案嵌入的View并不在Flutter的绘图树中,是一种比较暴力且不优雅的方式,使用起来也很费劲。这时候,使用Flutter官方提供的控件AndroidView就是一种比较优雅的解决方案了。这里做了一个简单的嵌入高德地图的demo,就让我们跟着这个应用场景,看一下AndroidView的使用方式和实现原理。1.2. AndroidView使用方式AndroidView的使用方式和MethodChannel类似,比较简单,主要分为三个步骤:第一步:在dart代码的相应位置使用AndroidView,使用时需要传入一个viewType,这个String将用于唯一标识该Widget,用于和Native的View建立关联。第二步:在native侧添加代码,写一个PlatformViewFactory,PlatformViewFactory的主要任务是,在create()方法中创建一个View并把它传给Flutter(这个说法并不准确,但是我们姑且可以这么理解,后续会进行解释)第三步:使用registerViewFactory()方法注册刚刚写好的PlatformViewFactory,该方法需要传入两个参数,第一个参数需要和之前在Flutter端写的viewType对应,第二个参数是刚刚写好的的PlatformViewFactory。配置高德地图的部分这里就省略不说了,官方有比较详细的文档,可以去高德开发者平台进行查阅。以上便是使用AndroidView的所有操作,总体看起来还是比较简单的,但是真正要用起来,还是有两个无法忽视的问题:View最终的显示尺寸由谁决定?触摸事件是如何处理的?下面就让小闲鱼来给各位一一解答。2. 原理讲解想要解决上面的两个问题,首先必须得理解所谓"传View"的本质是什么?2.1. 所谓"传View"的本质是什么?要解决这个问题,自然避免不了的需要去阅读源码,从更深的层面去看这个传递的整个过程,可以整理出一张这样的流程图:我们可以看到,Flutter最终拿到的是native层返回的一个textureId。根据native的知识ky h这个textureId是已经在native侧渲染好了的view的绘图数据对应的ID,通过这个ID可以直接在GPU中找到相应的绘图数据并使用,那么Flutter是如何去利用这个ID的呢?在之前的深入了解Flutter界面开发中,也给大家介绍了Flutter的绘图流程。我这里也给大家再简单整理一下Flutter的Framework层最后会递交给Engine层一个layerTree,在管线中会遍历layertree的每一个叶子节点,每一个叶子节点最终会调用Skia引擎完成界面元素的绘制,在遍历完成后,在调用glPresentRenderBuffer(IOS)或者glSwapBuffer(Android)按完成上屏操作。Layer的种类有很多,而AndroidView则使用的是其中的TextureLayer。TextureLayer在之前的《Flutter外接纹理》中有更为详细的介绍,这里就不再赘述。TextureLayer在被遍历到时,会调用一个engine层的方法SceneBuilder::addTexture() 将textureId作为参数传入。最终在绘制的时候,skia会直接在GPU中根据textureId找到相应的绘制数据,并将其绘制到屏幕上。那么是不是谁拿到这个ID都可以进行这样的操作呢?答案当然是否定的,Texture数据存储在创建它的EGLContext对应的线程中,所以如果在别的线程进行操作是无法获取到对应的数据的。这里需要引入几个概念:显示屏对象(Display):提供合理的显示器的像素密度和大小的信息Presentation:它给Android提供了在对应的上下文(Context)和显示屏对象(Display)上绘制的能力,通常用于双屏异显。这里不展开讲解Presentation,我们只需要明白Flutter是通过Presentation实现了外接纹理,在创建Presentation时,传入FlutterView对应的Context和创建出来的一个虚拟显示屏对象,使得Flutter可以直接通过ID找到并使用Native创建出来的纹理数据。2.2. View最终的显示尺寸由谁决定?通过上面的流程大家应该都能想到,显示尺寸看起来像是由两部分决定的:AndroidView的大小,Android端View的大小。那么实际上到底是有谁来决定的呢,让我们来做一个实验?直接新建一个Flutter工程,并把中间改成一个AndroidView。//Flutterclass _MyHomePageState extends State<MyHomePage> { double size = 200.0; void _changeSize() { setState(() { size = 100.0; }); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text(widget.title), ), body: Container( color: Color(0xff0000ff), child: SizedBox( width: size, height: size, child: AndroidView( viewType: ’testView’, ), ), ), floatingActionButton: new FloatingActionButton( onPressed: _changeSize, child: new Icon(Icons.add), ), ); }}在Android端也要加上对应的代码,为了更好地看出裁切效果,这里使用ImageView。//Android@Overridepublic PlatformView create(final Context context, int i, Object o) { final ImageView imageView = new ImageView(context); imageView.setLayoutParams(new ViewGroup.LayoutParams(500,500)); imageView.setBackground(context.getResources().getDrawable(R.drawable.idle_fish)); return new PlatformView() { @Override public View getView() { return imageView; } @Override public void dispose() { } };}首先先看AndroidView,AndroidView对应的RenderObject是RenderAndroidView,而一个RenderObject的最终大小的确定是存在两种可能,一种是由父节点所指定,还有一种是在父节点指定的范围中根据自身情况确定大小。打开对应的源码,可以看到其中有个很重要的属性sizedByParent = true,也就是说AndroidView的大小是由其父节点所决定的,我们可以使用Container、SizedBox等控件控制AndroidView的大小。AndroidView的绘图数据是Native层所提供的,那么当Native中渲染的View的实际像素大小大于AndroidView的大小时,会发生什么呢?通常情况下,这种情况的处理思路无非就两种选择,一种是裁切,另一种是缩放。Flutter保持了其一贯的做法,所有out of the bounds的Widget统一使用裁切的方式进行展示,上面所描述的情况就被当作是一种out of the bounds。当这个View的实际像素大小小于AndroidView的时候,会发现View并不会相应地变小(Container的背景色并没有显露出来),没有内容的地方会被白色填充。这其中的原因是SingleViewPresentation::onCreate中,会使用一个FrameLayout作为rootView。2.3. 触摸事件如何传递Android的事件流大家应该都很熟悉了,自顶向下传递,自底向上处理或回流。Flutter同样是使用这一规则,但是其中AndroidView通过两个类来去处理手势:MotionEventsDispatcher:负责将事件封装成Native的事件并向Native传递;AndroidViewGestureRecognizer:负责识别出相应的手势,其中有两个属性:cachedEvents和forwardedPointers,只有当PointerEvent的pointer属性在forwardedPointers中时才会去进行分发,否则会存在cacheEvents中。这里的实现主要是为了解决一些事件的冲突,比如滑动事件,可以通过gestureRecognizers来进行处理,这里可以参考官方注释。/// For example, with the following setup vertical drags will not be dispatched to the Android view as the vertical drag gesture is claimed by the parent [GestureDetector]./// /// GestureDetector(/// onVerticalDragStart: (DragStartDetails d) {},/// child: AndroidView(/// viewType: ‘webview’,/// gestureRecognizers: <OneSequenceGestureRecognizer>[],/// ),/// )/// /// To get the [AndroidView] to claim the vertical drag gestures we can pass a vertical drag gesture recognizer in [gestureRecognizers] e.g:/// /// GestureDetector(/// onVerticalDragStart: (DragStartDetails d) {},/// child: SizedBox(/// width: 200.0,/// height: 100.0,/// child: AndroidView(/// viewType: ‘webview’,/// gestureRecognizers: <OneSequenceGestureRecognizer>[ new VerticalDragGestureRecognizer() ],/// ),/// ),/// )所以总结起来,这部分流程总结起来其实也很简单:事件最初从Native到Flutter这一阶段不在本文的讨论范围之内,Flutter按照自己的规则去处理事件,如果AndroidView赢得了事件,事件就会被封装成相应的Native端的事件并且通过方法通道传回Native,Native再根据自己的处理事件的规则去处理。3. 总结3.1. 方案局限性往大里说:这套方案是Google为了解决开发者日益增长的业务需求与落后的生态环境之间的矛盾而产生的,这一矛盾是一个新生态必然需要去面对的主要矛盾。为了解决这一个问题,最简单的方式当然就是允许开发者使用老生态中已经非常成熟的控件。当然,这样是可以临时解决Flutter生态发展不全面的问题,但是使用这套方案不可避免的需要去编写双端代码(甚至现在iOS还没有对应的控件,当然之后肯定会更新),不能做到真正的跨端。往小里说:这套方案存在着性能上的缺陷,在AndroidView这个类的第三句注释中,官方就已经提到了这是一套比较昂贵的方案,避免在使用Flutter控件也能实现的情况下去使用它。如果之前有看过《Flutter外接纹理》这一文章的同学应该知道,Flutter实现外接纹理的方案中,数据从GPU->CPU->GPU的过程代价是比较大的,在大量使用的场景会造成明显的性能缺陷。我们通过一些手段绕过了中间CPU这一步,并且将这项技术在APP中落地,用于处理图片资源。3.2. 实际应用目前闲鱼从Native向Flutter的迁移工作遇到了Native的本地图片资源在Flutter侧无法访问的问题,在现在Flutter和Native必将长期共存的情况下,重新拷贝一份资源以Flutter的规则来存储当然可以,但是不可避免地增大了包体积,而且不好管理。面对这个问题,我们的解法便是借鉴了AndroidView使用Texture的思路并在将其优化。实现了Native和Flutter的图片资源归一化。除了用于加载位于Native资源目录下的本地图片之外,还可以利用Native的图片库来加载网络图片。我们这么去做的原因是我们在Native侧的图片库较为完善并且经受过大量的线上考验,现在这一阶段,我们不希望将过多的精力投入到重复造轮子这一件事上,而处理网络图片资源和处理本地图片资源的思路其实是一样的,所以我们选择将图片资源进行了统一地整合,在与官方的团队进行沟通并完善后会和大家同步,敬请关注我们的公众号。3.4. 引用高德地图SDK文档万万没想到——Flutter外接纹理Android7.1 Presentation双屏异显原理分析本文作者:闲鱼技术-尘萧阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 16, 2018 · 1 min · jiezi