注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

App跨平台框架VS原生开发深度评测之2023版

App跨平台框架历史悠久,从cordova、react native、flutter,直到最近的uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。 过去的问题到底在哪里? 我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差...
继续阅读 »

App跨平台框架历史悠久,从cordovareact nativeflutter,直到最近的uni-app x。江山代有才人出,每个都试图颠覆原生,但过去却一直未成功。


过去的问题到底在哪里?


我们先捋一捋各种技术路线,分析这些跨平台开发框架和原生应用的差别具体在哪里。


逻辑层渲染层类型代表作
webviewwebview弱类型5+App、cordova
js引擎webview弱类型uni-app之app-vue 、小程序(dount)
js引擎原生渲染弱类型react native、uni-app之app-nvue、weex
dart引擎flutter渲染引擎强类型flutter
js引擎flutter渲染引擎弱类型微信skyline、webF、ArkUI-x
kotlin原生渲染强类型uni-app x
kotlin原生渲染强类型原生应用

上面的表格,除了行尾的原生应用外,各个跨平台框架按出现时间排序,可以看到跨平台框架是如何演进的。


上表中,uni-app x和原生应用是一样的,逻辑层和渲染层都是原生,都是强类型;而其他跨平台框架或者在逻辑层、或者在渲染层与原生不一致。


webview不行已经是业内常识了,启动慢、渲染慢、内存占用高。这块本文不再详述。


但那些非web-view的框架到底哪里不如原生?


1. js逻辑+ 原生渲染


react nativeweex等抛弃webview,改由原生渲染的跨平台方案,2014年就推出了。
如今手机硬件也越来越好了,为什么性能还达不到原生?


js+原生渲染的方案主要有2点缺陷:



  • JS引擎自身的性能问题

  • JS和原生之间的通信延迟


1.1 js引擎慢,启动速度和运行速度都弱于原生


所以很多开发者即便使用这类方案,首页也还是原生来写。


React Native的Hermes引擎和华为的arkUI,提供了js编译为字节码的方案,这是一种空间换时间的方案,启动速度有了一定优化,但仍然比不过原生。


弱类型在编译期可优化的幅度有限,还是需要一个运行时来跑,无法像强类型那样直接深入底层。


以数字运算为例,js的number运算确实比强类型的int慢,内存开销也更大。


1.2 js语言与原生之间通信卡顿


每个语言有自己的内存空间,跨语言通信都有折损,每次通信几十到几百毫秒不等,视手机当时的状态。一旦频繁通信,就会明显卡顿。


逻辑层的js,即要和原生渲染层通信,还要和原生API通信:


1.2.1 js与原生ui通信


举个简单的场景例子,在js里监听滚动,根据滚动变化实时调整界面上某些元素的高度变化。这个问题能难倒一大批跨平台开发框架。


如果全部在webview里,js操作ui还好一些,所以uni-app的app-vue里的renderjs操作UI性能高,就是这个道理。同理还有微信小程序的wsx


虽然小程序和uni-app都是js,但实际上逻辑层在独立js引擎里,通过原生桥来控制web-view,通信成本很高。


weex提供了bindingx技术,这是一种弱编程,渲染层预先定义了一些操作UI的方式,调用时全部在渲染层运行,不会来回与逻辑层通信。但这种预定义方式的适应面有限,无法做到在js里高性能、自由的操作所有UI。


1.2.2 js操作原生api


操作系统和三方SDK的API都是原生的,js调用这些能力也需要跨语言通信。比如js调用原生的Storage或IO,数据较多时遍历的性能非常差。


当然在js API的封装上可以做些优化,比如微信的storage提供了wx.batchGetStorageSync这种批量读取的API,既然遍历性能差,那干脆一次性从原生读出来再传给js。


这也只能是无奈的方案,如果在遍历时想用js做什么判断就实现不了了,而且一次性读出很大的数据后传给js这一下,也需要通信时间。


2. flutter方案


flutter在2018年发布,第一次统一了逻辑层和渲染层,而且使用了强类型。


它没有使用原生渲染,而是使用由dart驱动的渲染引擎,这样逻辑层的dart代码操作UI时,再也没有延时了!bindingx、wxs这种补丁方案再也不需要了。


并且dart作为强类型,编译优化很好做,启动速度和运行速度都胜过js。


在这个开源项目下gitcode.net/dcloud/test…,提供了一个flutter编写的100个slider同时滑动的示例, 项目下有源码也有打包好apk,可以直接安装体验。


100个slider同时滑动,非常考验逻辑和UI的通信。如果在webview内部,html和js写100个这样的slider,在新的手机上表现也还ok。但在小程序和react native这种逻辑和UI分离的模式下,100个slider是灾难。


下载安装apk后可以看到dart操作flutter的UI真的没有通信折损,100个slider的拖动非常流畅。


flutter看起来很完美。但为什么也没有成为主流呢?很多大厂兴奋的引入后为何又不再扩大使用范围呢?


2.1 dart与原生API的通信


别忘了上面1.2.2提到的原生API通信。flutter虽然在逻辑层和渲染层都是dart,但要调用原生API时,还是要通信。


操作系统和三方SDK的API是原生的,让dart调用需要做一层封装,又落到了跨语言通信的坑里。


gitcode.net/dcloud/test…这是一个开源测试项目,来测试原生的claas数据与dart的通信耗时。


项目里面有源码,大家可自行编译;根目录有打包好的apk,也可以直接安装体验。


这个项目首先在kotlin中构建了包含不同数据量的class,传递到dart然后渲染在界面上,并且再写回到原生层。


有0.1k和1k两种数据量(点击界面上的1k数字可切换),有读和读并写2个按钮,各自循环1000次。


以下截图的测试环境是华为mate 30 5G,麒麟990。手机上所有进程杀掉。如下图:



  • 1k数据从原生读到dart并渲染


flutter_1k_read.jpeg



  • 1k数据从原生读到dart并渲染再写回


flutter_1k_readwrite.jpeg



  • 0.1k数据从原生读到dart并渲染


flutter_0.1k_read.jpeg



  • 0.1k数据从原生读到dart并渲染再写回


flutter_0.1k_readwrite.jpeg


通信损耗非常明显。并且数据量从1k降低到0.1k时,通信时间并没有减少10倍,这是因为通信耗时有一个基础线,数据再小也降不下去。


为什么会这样?因为dartkotlin不是一种编程语言,不能直接调用kotlinclass,只能先序列化成字符串,把字符串数据从原生传到dart,然后在dart层再重新构造。


当然也可以在原生层为dart封装API时提供wx.batchGetStorageSync这类批处理API,把数据一次读好再给dart,但这种又会遇到灵活性问题。


而在uni-app x中,这种跨语言通信是不存在的,不需要序列化,因为uni-app x使用的编程语言uts,在android上就编译为了kotlin,它可以直接调用kotlin的class而无需通信和封装。示例如下,具体uni-app x的原理后续章节会专题介绍。


<template>
template>
<script lang="uts">
import Build from 'android.os.Build';
export default {
onLoad() {
console.log(Build.MODEL); //uts可以直接导入并使用原生对象,不需要封装,没有跨语言通信折损
}
}
script>

再分享一个知识:


很多人都知道iPhone上跨平台框架的应用,表现比android好。但大多数人只知道是因为iPhone的硬件好。


其实还有一个重要原因,iOS的jscore是c写的,OS的API及渲染层也都是ObjectC,js调用原生时,某些类型可以做共享内存的优化。但复杂对象也还是无法直接丢一个指针过去共享使用内存。


而android,不管java还是kotlin,他们和v8、dart通信仍然需要跨语言通信。


2.2 flutter渲染和原生渲染的并存问题


flutter的自渲染引擎,在技术上是不错的。但在生态兼容上有问题。


很多三方软件和SDK是原生的,原生渲染和flutter自渲染并存时,问题很多。


flutter开发者都知道的一个常见坑是输入法,因为输入法是典型的原生UI,它和flutter自绘UI并存时各种兼容问题,输入框被遮挡、窗体resize适应,输入法有很多种,很难适配。


混合渲染,还有信息流广告、map、图表、动画等很多三方sdk涉及。这个时候内存占用高、渲染帧率下降、不同渲染方式字体不一致、暗黑主题不一致、国际化、无障碍、UI自动化测试,各种不一致。。。


这里没有提供开源示例,因为flutter官方是承认这个问题的,它提供了2种方式:混合集成模式和虚拟显示模式模式。


但在渲染速度、内存占用、版本兼容、键盘交互上都各自有各自的问题。详见flutter官网:docs.flutter.dev/platform-in…。这个是中文翻译:flutter.cn/docs/platfo…


在各大App中,微信的小程序首页是为数不多的使用flutter UI的界面,已经上线1年以上。


下面是微信8.0.44(此刻最新版),从微信的发现页面进入小程序首页。


视频中手机切换暗黑主题后,这个UI却还是白的,而且flutter的父容器原生view已经变黑了,它又在黑底上绘制了一个白色界面,体验非常差。


这个小程序首页界面很简单,没有输入框,规避了混合渲染,点击搜索图标后又跳转到了黑色的原生渲染的界面里。


假使这个界面再内嵌一个原生的信息流SDK,那会看到白色UI中的信息流广告是黑底的,更无法接受。


当然这不是说flutter没法做暗黑主题,重启微信后这个界面会变黑。这里只是说明渲染引擎不一致带来的各种问题。



注:如何识别一个界面是不是用flutter开发的?在手机设置的开发者选项里,有一个GPU呈现模式分析,flutter的UI不触发这个分析。且无法审查布局边界。



flutter的混合渲染的问题,在所有使用原生渲染的跨平台开发框架中都不存在,比如react native、weex、uni-app x。


总结下flutter:逻辑层和UI层交互没有通信折损,但逻辑层dart和原生api有通信成本,自绘UI和原生ui的混合渲染问题很多。


3. js+flutter渲染


flutter除了上述提到的原生通信和混合渲染,还有3个问题:dart生态、热更新、以及比较难用的嵌套写法。


一些厂商把flutter的dart引擎换成了js引擎,来解决上述3个问题。比如微信skyline、webF、ArkUI-x。


其实这是让人困惑的行为。因为这又回到了react native和weex的老路了,只是把原生渲染换成了flutter渲染。


flutter最大的优势是dart操作UI不需要通信,以及强类型,而改成js,操作UI再次需要通信,又需要js运行时引擎。


为了解决js和flutter渲染层的通信问题,微信的skyline又推出了补丁技术worklet动画,让这部分代码运行在UI层。(当然微信的通信,除了跨语言,还有跨进程通信,会更明显)


这个项目gitcode.net/dcloud/test…, 使用ArkUI-x做了100个slider,大家可以看源码,下载apk体验,明显能看到由于逻辑层和UI层通信导致的卡顿。



上述视频中,注意看手指按下的那1个slider,和其他99个通过数据通讯指挥跟随一起行动的slider,无法同步,并且界面掉帧。


不过自渲染由于无法通过Android的开发者工具查看GPU呈现模式,所以无法从条状图直观反映出掉帧。



注意ArkUI-x不支持Android8.0以下的手机,不要找太老的手机测试。



很多人以为自渲染是王道,但其实自渲染是坑。因为flutter的UI还会带来混合渲染问题。


也就是说,js+flutter渲染,和js+原生渲染,这2个方案相比,都是js弱类型、都有逻辑层和渲染层的通信问题、都有原生API通信问题,而js+flutter还多了一个混合渲染问题。


可能有的同学会说,原生渲染很难在iOS、Android双端一致,自渲染没有这个问题。


但其实完全可以双端一致,如果你使用某个原生渲染框架遇到不一致问题,那只是这个框架厂商做的不好而已。


是的,很遗憾react native在跨端组件方面投入不足,官方连slider组件都没有,导致本次评测中未提供react native下slider-100的示例和视频。


4. uni-app x


2022年,uts语言发布。2023年,uni-app x发布。


uts语言是基于typescript修改而来的强类型语言,编译到不同平台时有不同的输出:



  • 编译到web,输出js

  • 编译到Android,输出kotlin

  • 编译到iOS,输出swift


而uni-app x,是基于uts语言重新开发了一遍uni-app的组件、API以及vue框架。


如下这段示例,前端的同学都很熟悉,但它在编译为Android App时,变成了一个纯的kotlin app,里面没有js引擎、没有flutter、没有webview,从逻辑层到UI层都是原生的。


<template>
<view class="content">
<button @click="buttonClick">{{title}}button>
view>
template>

<script> //这里只能写uts
export default {
data() {
return {
title: "Hello world"
}
},
onLoad() {
console.log('onLoad')
},
methods: {
buttonClick: function () {
uni.
showModal({
"showCancel": false,
"content": "点了按钮"
})
}
}
}
script>

<style>
.content {
width: 750rpx;
background-color: white;
}
style>

这听起来有点天方夜谭,很多人不信。DCloud不得不反复告诉大家,可以使用如下方式验证:



  • 在编译uni-app x项目时,在项目的unpackage目录下看看编译后生成的kt文件

  • 解压打包后的apk,看看里面有没有js引擎或flutter引擎

  • 手机端审查布局边界,看看渲染是不是原生的(flutter和webview都无法审查布局边界)


但是开发者也不要误解之前的uni-app代码可以无缝迁移。



  • 之前的js要改成uts。uts是强类型语言,上面的示例恰好类型都可以自动推导,不能推导的时候,需要用:as声明和转换类型。

  • uni-app x支持css,但是css的子集,不影响开发者排版出所需的界面,但并非web的css全都兼容。


了解了uni-app x的基本原理,我们来看下uni-app x下的100个slider效果怎么样。


项目gitcode.net/dcloud/test…下有源码工程和编译好的apk。


如下视频,打开了GPU呈现模式,可以看到没有一条竖线突破那条红色的掉帧安全横线,也就是没有一帧掉帧。



uni-app x在app端,不管逻辑层、渲染层,都是kotlin,没有通信问题、没有混合渲染问题。不是达到了原生的性能,而是它本身就是原生应用,它和原生应用的性能没差别。


这也是其他跨平台开发框架做不到的。


uni-app x是一次大胆的技术突破,分享下DCloud选择这条技术路线的思路:


DCloud做了很多年跨平台开发,uni-app在web和小程序平台取得了很大的成功,不管规模大小的开发者都在使用;但在app平台,大开发者只使用uni小程序sdk,中小开发者的app会整体使用。


究其原因,uni-app在web和小程序上,没有性能问题,直接编译为了js或wxml,uni-app只是换了一种跨平台的写法,不存在用uni-app开发比原生js或原生wxml性能差的说法。


但过去基于小程序架构的app端,性能确实不及原生开发。


那么App平台,为什么不能像web和小程序那样,直接编译为App平台的原生语言呢?


uni-app x,目标不是改进跨平台框架的性能,而是给原生应用提供一个跨平台的写法。


这个思路的转换使得uni-app x超越了其他跨平台开发框架。


在web端编译为js,在小程序端编译为wxml等,在app端编译为kotlin。每个平台都只是帮开发者换种一致的写法而已,运行的代码都是该平台原生的代码。


然而在2年前,这条路线有2个巨大的风险:



  1. 从来没有人走通过

  2. 即便能走通,工作量巨大


没有人确定这个产品可以做出来,DCloud内部争议也很多。


还好,经历了无数的困难和挑战,这个产品终于面世了。


换个写法写原生应用,还带来另一个好处。


同样业务功能的app,使用vue的写法,比手写纯原生快多了。也就是uni-app x对开发效率的提升不只是因为跨平台,单平台它的开发效率也更高。


其实google自己也知道原生开发写法太复杂,关于换种更高效的写法来写原生应用,他们的做法是推出了compose UI。


不过遗憾的是这个方案引入了性能问题。我们专门测试使用compose UI做100个slider滑动的例子,流畅度也掉帧。


源码见:gitcode.net/dcloud/test…, 项目下有打包后的apk可以直接安装体验。


打开GPU呈现模式,可以看到compose ui的100个slider拖动时,大多数竖线都突破那条红色的掉帧安全横线,也就是掉帧严重。


既然已经把不同开发框架的slider-100应用打包出来了,我们顺便也比较了不同框架下的包体积大小、内存占用:


包体积(单位:M)内存占用(单位:Kb)
flutter18141324.8
ArtUI-x45.7133091.2
uni-app x8.5105451.2
compose ui4.498575.2

包体积数据说明:



  • 包含3个CPU架构:arm64、arm32、x86_64。

  • flutter的代码都是编译为so文件,支持的cpu类型和包体积是等比关系,1个cpu最小需要6M体积,业务代码越多,cpu翻倍起来越多。

  • ArtUI-x的业务代码虽然写在js里,但除了引用了flutter外还引用了js引擎,这些so库体积都不小且按cpu分类型翻倍。

  • uni-app x里主业务都在kotlin里,kotlin和Android x的兼容库占据了不少体积。局部如图片引用了so库,1个cpu最小需要7M体积。但由于so库小,增加了2个cpu类型只增加了不到1M。

  • compose ui没有使用so库,体积裁剪也更彻底。

  • uni-app x的常用模块并没有裁剪出去,比如slider100的例子其实没有用到图片,但图片使用的fesco的so库还是被打进去了。实际业务中不可能不用图片,所以实际业务中uni-app x并不会比compose ui体积大多少。


内存占用数据说明:



  • 在页面中操作slider数次后停止,获取应用内存使用信息VmRSS: 进程当前占用物理内存的大小

  • 表格中的内存数据是运行5次获取的值取平均值

  • 自渲染会占据更多内存,如果还涉及混合渲染那内存占用更高


5. 后记


跨语言通信、弱类型、混合渲染、包体积、内存占用,这些都是过去跨平台框架不如原生的地方。


这些问题在uni-app x都不存在,它只是换了一种写法的原生应用。


各种框架类型逻辑层与UI通信折损逻辑层与OS API通信折损混合渲染
react native、nvue、weex
flutter
微信skyline、webF、ArkUI-x
uni-app x
原生应用

当然,作为一个客观的分析,这里需要强调uni-app x刚刚面世,还有很多不成熟的地方。比如前文diss微信的暗黑模式,其实截止到目前uni-app x还不支持暗黑模式。甚至iOS版现在只能开发uts插件,还不能做完整iOS应用。


需求墙里都是uni-app x该做还未做的。也欢迎大家投票。


另外,原生Android中一个界面不能有太多元素,否则性能会拉胯。flutter的自渲染和compose ui解决了这个问题。而原生中解决这个问题需要引入自绘机制来降低元素数量,这个在uni-app x里对应的是draw自绘API。


uni-app x这个技术路线是产业真正需要的东西,随着产品的迭代完善,它能真正帮助开发者即提升开发效率又不牺牲性能。


让跨平台开发不如原生,成为历史。


欢迎体验uni-app x的示例应用,感受它的启动速度,渲染流畅度。


源码在:gitcode.net/dcloud/hell…; 


这个示例里有几个例子非常考验通信性能,除了也内置了slider-100外,另一个是“模版-scroll-view自定义滚动吸顶”,在滚动时实时修改元素top值始终为一个固定值,一点都不抖动。


我们不游说您使用任何开发技术,但您应该知道它们的原理和差别。


欢迎指正和讨论。


作者:CHB
来源:juejin.cn/post/7317091780826497075
收起阅读 »

Kotlin魔法——优雅实现多函数回调

补充 写完这篇文章并发布了之后才发现,这个写法已经有人发布过了,也可以参考参考~ 如何让你的回调更具Kotlin风味 Kotlin DSL回调 写在前面 在网络请求时,经常面临一类情况:网络请求有可能成功,也有可能失败,这就需要两个回调函数来分别对成功和失败...
继续阅读 »

补充


写完这篇文章并发布了之后才发现,这个写法已经有人发布过了,也可以参考参考~


如何让你的回调更具Kotlin风味


Kotlin DSL回调


写在前面



在网络请求时,经常面临一类情况:网络请求有可能成功,也有可能失败,这就需要两个回调函数来分别对成功和失败的情况来进行处理,那么,在Kotlin这门无比强大的语言中,有没有一种“魔法”,能够优雅地实现这一类同时可能需要多个回调的场景呢?



场景


问题的场景已经提出,也就是当某一个行为需要有多个回调函数的时候,并且这些回调并不一定都会触发。


例如,网络请求的回调场景中,有时候是onSuccess触发,有时候是onFailure触发,这两个函数的函数签名也不一定相同,那么怎么实现这个需求呢?


接下来我们以一个具体的问题贯穿全文:


假设我们现在要写一个网络请求框架,在封装上层回调的时候,需要封装两个回调(onSuccess/onFailure)供上层(就假设是UI层吧,不搞什么MVVM架构了)调用,以便UI层能知道网络请求成功/失败了,并进行相应的UI更新。



注: 标题所说的“魔法”是指实现方式三,方式一和二只是为了三铺垫的引子,如果想直奔主题那么建议直接跳转实现方式三!



实现方式一:直接传参


最直接的当然是直接传参嘛,把这两个回调写成函数参数,直接传进去,这当然可以实现目标,简单的示例代码如下。


网络请求层


data class RequestConfig(val api: String, val bodyJson: String, val method: String = "POST")
data class Data(val myData1: Int, val myData2: Boolean)

//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig, onSuccess: (data: Data) -> Unit = {}, onFailure: (errorMsg: String) -> Unit = {}) {
//假设调用更底层如Retrofit等模块,成功拿到数据后调用
onSuccess(Data(1, true))

//或者,失败后调用
onFailure("断网啦")
}

UI层


@Composable
fun MyView() {
Button(onClick = {
fetchData(requestConfig = RequestConfig("/user/info", ""), onSuccess = {
//更新UI
}, onFailure = {
//弹Toast提示用户
})
}) { }
}

在网络请求层,通过把fetchData的回调参数设一个默认值,我们也能实现“回调可选”这一需求。


这似乎并没有什么问题,那么还有没有什么别的实现方式呢?


实现方式二:链式调用


简单的思考过后,发现链式调用似乎也能满足我们的需求,实现如下。


网络请求层


在网络请求层,我们预先封装一个表示请求结果的类MyResult,然后让fetchData返回这个结果。


data class MyResult(val code: Int, val msg: String, val data: Data) {
fun onSuccess(block: (data: Data) -> Unit) = this.also {
if (code == 200) { //判断交给MyResult,若code==200,则认为成功
block(data)
}
}

fun onFailure(block: (errorMsg: String) -> Unit) = this.also {
if (code != 200) { //判断交给MyResult,若code!=200,则认为失败
block(msg)
}
}
}

//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig): MyResult {
return retrofitRequest(requestConfig)
}

UI层


此时的UI层调用fetchData时,则是通过MyResult这个返回值进行链式调用,并且链式调用也是自由可选的。


@Composable
fun MyView() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")).onSuccess {
//更新UI
}.onFailure {
//弹Toast提示用户
}
}) { }
}

这也似乎并没有什么问题,但是,总感觉不够Kotlin!


其实写多了Kotlin就会发现,Kotlin似乎非常喜欢花括号{},也就是作用域或者lambda这个概念。


而且Kotlin还喜欢把最后一个花括号放在最后一个参数,以便提到最外层去。


那么!有没有一种办法,能够以Kotlin常见的作用域的方式,优雅地完成上述场景需求呢?


锵锵!主角登场!


实现方式三:继承+扩展函数=魔法!


不多说,让我们先来看看这种实现方式的效果!


用这种方式,上述UI层将会变成这样!



  • 如果什么也不需要处理


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", ""))
}) { }
}


  • 如果需要处理onSuccess


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")) {
onSuccess {
//更新UI
}
}
}) {

}
}


  • 如果需要同时能处理onSuccess和onFailure


@Composable
fun MyView2() {
Button(onClick = {
//点击按钮后发送网络请求
fetchData(requestConfig = RequestConfig("/user/info", "")) {
onSuccess {
//更新UI
}
onFailure {
//弹Toast提示用户
}
}
}) {

}
}

看到了吗!!!非常自由,而且没有任何多余的->.或者,,只有非常整齐的花括号!


真的太神奇啦!


那么,这是怎么做到的呢?


揭秘时刻


在网络请求层,我们需要先定义一个接口,用于定义我们需要的多个回调函数!


interface ResultScope {
fun onSuccess(block: (data: Data) -> Unit)
fun onFailure(block: (errorMsg: String) -> Unit)
}

接着我们自己在内部实现这个接口!


internal class ResultScopeImpl : ResultScope {
var onSuccessBlock: (data: Data) -> Unit = {}
var onFailureBlock: (errorMsg: String) -> Unit = {}

override fun onSuccess(block: (data: Data) -> Unit) {
onSuccessBlock = block
}

override fun onFailure(block: (errorMsg: String) -> Unit) {
onFailureBlock = block
}
}

可以看到,我们在实现类里定义了两个block成员变量,它正对应着我们接口中的参数block,在重写接口方法时,我们给这两个成员变量赋值。


其实就是把这个block先暂时记录下来啦。


最后就是我们的fetchData函数了。


//模拟网络请求,获取数据
fun fetchData(requestConfig: RequestConfig, resultScope: ResultScope.() -> Unit = {}) {
val result = retrofitRequest(requestConfig)
val resultScopeImpl = ResultScopeImpl().apply(resultScope)

resultScopeImpl.run {
if (result.code == 200) onSuccessBlock(result.data) else onFailureBlock(result.msg)
}
}

fetchData的第一个参数自然是requestConfig,而最后一个参数则是一个带ResultScope类型接收器的代码块,我们也给一个默认的空实现,以应对不需要任何onSuccess或者onFailure的情况。




那么首先就有第一个问题了!resultScope: ResultScope.() -> Unit这个参数怎么理解?


我们首先要理解什么是lambda,或者说理解什么是接口!



重要!精髓! 如何理解lambda的意义?


当面对一堆lambda,甚至是嵌套lambda的时候,你是否感觉到阅读困难,非常无力?如果是的话,其实有一个很简单的方法,lambda也就是一个函数表达式嘛~既然是函数,那么我们就只需要盯紧三件事!



  • 函数的签名(包括参数列表和返回值)

  • 函数的方法体(也就是函数的实现)

  • 谁来负责在什么时候调用这个函数


只要盯紧这三件事,那么lambda的绝大部分理解上的障碍,都会一扫而光


例如


我们经常所说的回调,比如这个网络请求回调,那不就是:



  • 网络请求框架负责约定函数的签名,其中

    • 参数列表代表待会儿我框架层拿到结果以后需要告诉你UI层哪些信息

    • 返回值代表你UI层在知道我框架给的信息,并处理完之后,需要再返回给我框架层什么结果



  • UI层负责这个lambda的具体实现,也就是

    • 怎么去处理刚刚从框架层传来的信息(即参数)

    • 告知框架层处理完毕后的结果(即返回值)



  • 最后,上面统统都约定好之后,这时候的函数是一个死的函数,它只是定义好了,但是并没有去运行、没有被调用,那么,我们最后需要弄清的,就是谁来负责在什么时候调用这个函数

  • 无疑是框架层来调用,框架层在从更下层获取到请求结果后,就会调用这个函数,并且按之前所约定、所定义好的一切去执行它


又例如


Android开发中,RecyclerView这一列表组件会使用适配器,其中abstract void onBindViewHolder(@NonNull VH holder, int position)这个方法就也可以看成是一个所谓的lambda



  • 这个方法的签名和返回值由抽象类Adapter所定义

  • 这个方法的实现由Adapter的子类完成,即我们自己写的适配器

  • 这个方法的调用由RecyclerView控件负责调用


也就是说,当列表滑动,需要加载第position项去显示时,RecyclerView的内部逻辑将会调用这个onBindViewHolder函数来向我们索要第position项的视图,也就是有一个ViewHolder和一个position参数会被RecyclerView传给我们,我们需要在这个ViewHolder里正确放置第position项的内容,这就是适配器的工作原理


小结


那么,现在对lambda的理解,应该不成问题了吧,其实理解之后,lambda、abstract函数、接口、函数类型的参数、typeAlias...等等都是一个意思,我们需要关注的是,它的定义、实现以及调用者和调用时机



回到正题,如何理解resultScope: ResultScope.() -> Unit呢?



ResultScope.() -> Unit 表示一个带ResultScope类型接收器的函数代码块,说通俗一点,就是:



  • 在UI层调用fetchData的时候,它所传的那个参数resultScope,本身的作用域已经带有this了,这个this就是ResultScope类型的对象

    • 再说通俗一点就是,resultScope那个代码块内,能直接访问ResultScope的方法或者属性,这也就是为什么在上面的示例代码里,我们能直接在花括号里写 onSuccess {} 的原因,因为那个花括号已经被ResultScope对象统治了,我们能在里面直接调用ResultScope类的方法onSuccess



  • 然后,在网络请求层,当请求有结果后,我们会调用ResultScope的实例的对应block方法

    • 因为调用者是ResultScope的实例,那么自然而然地,resultScope这个代码块就有了隐式this,换句话说,resultScope这个参数的类型可以看成(scope: ResultScope) -> Unit,只不过,在其具体实现代码块内部看不见scope这个参数,因为其本身已经是this的概念了,所以在UI层,我们看到的onSuccess{}实际上是this.onSuccess{}





好,下一个问题。


在刚刚如何理解resultScope参数的解读里,有一句粗体“我们会调用ResultScope的实例的对应block方法”,那么,下一个问题就是,ResultScope的实例是怎么来的


ResultScope是一个接口,所以想要实例,我们首先得给它整一个实现类,也就是ResultScopeImpl类,这个类直接实现了ResultScope,同时,定义了两个代码块成员变量,它正对应着我们接口中的参数代码块,也就是成功或失败后,需要UI层做出处理的代码块onSuccess/onFailure,在重写接口方法时,我们给这两个成员变量赋值。


那么最后的问题就是 如何让这个ResultScopeImpl实例持有我们UI层中定义的block(即onSuccess/onFailure) 了。


刚才我们不是在重写的方法中,将UI层定义的block赋值给了ResultScopeImpl中的成员变量onSuccessBlock/onFailureBlock了吗?


那我们只要触发赋值,也就是ResultScopeImpl中override fun onSuccess的调用就行了。


办法就是这个!ResultScopeImpl().apply(resultScope)


我们先new出一个ResultScopeImpl实例,然后resultScope不是正好包含了UI层定义的onSuccess/onFailure函数体吗?那我们apply应用/赋值/设置属性)一下就可以了呗~


什么?你不知道为什么apply一下就能赋值了


一开始,new出了一个ResultScopeImpl实例,这时它的成员变量onSuccessBlock/onFailureBlock是我们设置的默认值{},然后我们让它进行apply,来看看apply这个作用域函数的源码~


public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}

发现了吗?apply的参数正好就是T.() -> Unit类型,这里的T不就是ResultScopeImpl吗?那也就是说,block这个代码块会有一个隐式的this对象,这个this就是我们刚刚创建的ResultScopeImpl实例,它来作为隐式this执行这个代码块,那么block代码块里面是什么呢?对啦,就是我们在UI层写的onSuccess和onFailure嘛!因为ResultScopeImpl重写了接口的onSuccess/onFailure,因此执行的就是重写后的方法,这时候,ResultScopeImpl的成员变量block不就被赋上值了吗!over!


那么,完整的流程就是~



  • UI层的Button触发onClick,进而触发fetchData调用

  • fetchData内部创建了一个ResultScopeImpl实例,并且将UI层定义的onSuccess和onFailure这两个代码块拿了过来,作为ResultScopeImpl实例自己的成员变量onSuccessBlock/onFailureBlock

  • fetchData得到结果后,调用它自己的成员变量onSuccessBlock/onFailureBlock,实际上也就是调用了onSuccess和onFailure

  • UI层得到响应,onSuccess/onFailure被调用,触发UI更新


结语


实现方式就介绍到这里啦,当然,第三种方式并不是没有缺点,如果说,需要多次实现onSuccess回调,那么第三种方式,以上面的代码就不方便做到啦,只能把override里改成add,然后成员变量block们用一个List存起来,然后依次触发~


而如果是链式调用的实现方式,就不会有这个问题啦!


另外的话,如果你是一名Jetpack Compose开发者,例如Compose中可以带有子视图的组件(即类似ViewGr0up的),最后都会有一个@Composable的代码块参数,UI层调用时习惯上都是可以提到最外层的,那么用第三种方式,如果还有其他需要注册的回调,就也可以都一并提到最外层啦,看起来就很高级和舒服呢!


就写到这里叭~


作者:彭泰强
来源:juejin.cn/post/7220220246506192952
收起阅读 »

公司敏感数据被上传Github,吓得我赶紧改提交记录

大家好,我是小富~ 说个事吧!最近公司发生了一个事故,有同事不小心把敏感数据上传到了GitHub上,结果被安全部门扫描出来了。这件事导致公司对所有员工进行了一次数据安全的培训。对于这个事我相信,有点工作经验的人都不会故意去上传这些敏感文件,多数应该是误操作导致...
继续阅读 »

大家好,我是小富~


说个事吧!最近公司发生了一个事故,有同事不小心把敏感数据上传到了GitHub上,结果被安全部门扫描出来了。这件事导致公司对所有员工进行了一次数据安全的培训。对于这个事我相信,有点工作经验的人都不会故意去上传这些敏感文件,多数应该是误操作导致的。


这个事件也给了提了个醒,我平时会写博客用GitHub比较多,吓得我赶紧对自己所有的GitHub仓库进行了排查,庆幸没有提交过敏感信息的记录。但我注意到在过往的提交记录中,有使用公司的Git账号信息提交过代码,TMD这就很难受了。


图中信息均为假数据,切勿当真


避免后续产生不必要的麻烦,我决定修改一下提交记录中涉及公司的信息。



注意:以下操作只限于用在自己的Git仓库,别在公司的项目里秀,切记!



设置用户信息


Git进行版本控制的时候,每次的代码提交记录中都包含用户的用户名和邮箱,这些信息在你进行每一次提交时都会被记录下来。我们保不齐会错误地使用了错误的信息,或者需要改用另一个邮箱地址。那这种情况,我们就需要更改我们提交记录中的用户名和邮箱。


可以通过全局设置或者特定仓库设置两种方式来修改我们提交时的用户信息。


全局


全局设置可以影响所有的代码提交。如果你在全局范围内设置了用户名和邮箱后,除非你在特定的项目中覆盖这个设置,否则这个设置会作为默认设置应用于所有的提交。


git config --global user.name "程序员小富"
git config --global user.email "邮箱信息"

你可以通过如下的命令来查看Git的全局配置:


git config --global -l

特定仓库


如果你只想修改某个特定仓库的用户信息,可以在特定仓库的根目录下进行如下操作,Git会将设置得用户名和邮箱仅应用于当前仓库。


git config user.name "程序员小富"
git config user.email "邮箱信息"

篡改提交记录


单条修改


Git提供了amend命令,可以用来修改最新的提交记录。注意,这个命令只会修改最近一次的提交,它能实现以下的功能:



  • 修改提交信息

  • 添加漏掉的文件到上一次的提交中

  • 修改之前提交的文件


用法


它的使用方法比较简单,直接替换用户名、邮箱信息,或者如果已经修改了仓库的用户信息,直接执行命令重置。


# 替换用户名、邮箱信息
git commit --amend --author="{username} <{email}>" --no-edit

#
如果已经修改了仓库的用户信息,直接执行命令重置
git commit --amend --reset-author --no-edit

看到最近一次提交的用户名是xiaofu,不是我的个人信息程序员小富,使用amend命令修改一下。



效果


执行命令后最近一次的提交信息从xiaofu变更到了程序员小富,更改成功和预期的效果一致。


git commit --amend --author="程序员小富 <515361725@qq.com>" --no-edit


修改完成之后,别忘了推送到远程仓库。


 git push origin master

批量修改


Git官网提供了很多种修改提交记录信息的方法,这里主要介绍下filter-branch,它可以通过脚本的方式批量修改历史提交记录信息。


filter-branch 它能实现如下的功能,正好符合我们要批量修改历史提交记录中用户、邮箱的需求。



  • 全局修改邮箱地址;

  • 从每一个提交中移除一个文件;

  • 使一个子目录做为新的根目录


用法


历史提交记录中有很多用户名xiaofu提交的记录,现在使用filter-branch批量将他们改写成程序员小富



以下是官网提供的脚本,其逻辑很简单:如果遇到用户名为xiaofu的提交记录,将该提交记录的用户名和邮箱修改为程序员小富515361725@qq.com


git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_NAME" = "xiaofu" ];
then
GIT_AUTHOR_NAME="程序员小富";
GIT_AUTHOR_EMAIL="515361725@qq.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi'
HEAD

为了方便操作,创建一个脚本modifyCommit.sh,放在项目的根目录执行。


chmod +x modifyCommit.sh
sh modifyCommit.sh

执行脚本后稍作等待,出现如下的输出说明已经在执行修改操作了。



执行完毕看到历史提交记录中的用户名xiaofu全都变更成了程序员小富,说明脚本生效了。



如果没有修改成功,可以再次执行,但会出现错误提示A previous backup already exists in refs/original/,说明已经执行过了,执行以下命令清除缓存即可再次执行。



git filter-branch -f --index-filter 'git rm --cached --ignore-unmatch Rakefile' HEAD

修改完成之后,别忘了推送到远程仓库。


 git push origin master

GitHub工具


管理GitHub项目,我推荐大家使用GitHub官方的Git客户端工具GitHub Desktop,这个工具专门用来管理GitHub仓库,洁面简洁使用也很方便,主打一个轻量。



而且在提交代码时,如果用户信息与当前账号GitHub信息不一致,还会有提示这样就不怕误用其他信息提交了。



总结


如果大家平时会维护自己的GitHub仓库,建议一定一定要仔细的检查提交的代码,像注释里的公司邮箱信息、代码包路径中的公司标识,凡事涉及公司信息的数据一概去除,不要惹一些不必要的麻烦,数据泄漏这种重可大可小不是闹着玩的。


还有GitHub别留太多的个人信息,手机号邮箱就别放了,头像也别傻乎乎的放个自己大头贴,给自己留点回旋的余地。核心思工作和生活要隔离!!!


我是小富~ 下期见


作者:程序员小富
来源:juejin.cn/post/7309784902311870516
收起阅读 »

flutter chat UI again flutter 漂亮聊天UI界面实现

flutter 漂亮聊天UI界面实现 flutter chat UI  之前写了一个聊天界面,但是只是花架子,并不能使用,无法点击,无法活动,并且由于时间问题也没有完全完成,右侧的聊天界面没有实现。现在,我准备完成一个比较美观且能使用的聊天界面。 寻找聊天界面...
继续阅读 »

flutter 漂亮聊天UI界面实现 flutter chat UI


 之前写了一个聊天界面,但是只是花架子,并不能使用,无法点击,无法活动,并且由于时间问题也没有完全完成,右侧的聊天界面没有实现。现在,我准备完成一个比较美观且能使用的聊天界面。


寻找聊天界面模板


 先找一个美观的模板来模仿吧。找模板的标准是简介、美丽、大方、清新。


1.png


 这次选的是一个比较简洁的界面,淡蓝色为主色,横向三个大模块排列开来,有设置界面、好友列表、聊天界面,就选定用这个了。


chatUI聊天界面实现


整体分析


 最外层使用横向布局,分别放置三个大组件,每个组件里面使用竖向布局来放置各种按钮、好友列表、聊天界面。每个组件里面的细节我们边实现边学习。


外层框架


 我们先实现最外边的框架。用SelectionArea包裹所有后续组件,实现所有文字可以选定。Selection现在有了官方的正式支持,该功能补全了Flutter长时间存在Selection异常等问题,尤其是在Web框架下经常会有选择文本时与预期的行为不匹配的情况。接着用Row水平布局组件来包裹三大块细分功能组件,代码里先用text组件代替。这样框架就设置好了。


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, //去掉右上角debug标识
theme: ThemeData(
//主题设置
primarySwatch: Colors.blue,
),
home: const SelectionArea(
//子组件支持文字选定 3.3新特性
child: Scaffold(
//子组件
body: MyAppbody(),
),
),
);
}
}

class MyAppbody extends StatelessWidget {
const MyAppbody({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Row(
//水平布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: Text("按钮组件"), ),

Expanded(
flex: 1, //空间占比
child: Text("好友列表组件"), ),

Expanded(
flex: 3, //空间占比
child: Text("聊天框组件"), ),

],
),
);
}
}

 效果图:


2.png


第一个模块设计


 新建一个fistblock文件夹放置我们的第一个模块代码,实现代码分块抽离。还是先写大框架,外围放置竖向排列组件Column,然后再依次放进去头像模块和设置模块。Column是垂直布局,在Y轴排列,也就是纵轴上的排列方式,可以使其包含的子控件按照垂直方向排列,Column是Widget的容器,存放的是一组Widget,而Container里面一次只能存放一个child。


import 'package:flutter/material.dart';
class FistBlockMain extends StatelessWidget {
const FistBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
////竖直布局
children: const <Widget>[
//子组件
Expanded(
flex:1, //空间占比
child: Text("头像"),
),

Expanded(
flex: 1, //空间占比
child: Text("设置"),
),

Expanded(
flex:1, //空间占比
child: Text("帮助"),
),

],
),
);
}
}

 效果图:


3.png


头像模块实现


 头像模块我们之前也实现过,现在可以直接拿来用,例子里在线状态小圆点在右上角,这里我们依旧利用Badge实现小圆点,同时圆点位置可以自由设置,我比较习惯放在右下角,当然,你也可以通过设置Badge的position参数改变位置。Badge是flutter的插件,flutter也有很多其他的优秀的插件可以使用,有了插件的帮忙,我们可以很方便的实现各种功能。


class User extends StatelessWidget {
const User({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Badge(
//头部部件
//通知小圆点
badgeColor: Colors.green, //小圆点颜色
position: const BadgePosition(
start: 35, top: 35, end: 0, bottom: 0), //小圆点显示位置
borderSide:
const BorderSide(color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const CircleAvatar(
//图片圆形剪裁
radius: 25, //圆形直径,(半径)?
backgroundColor: Colors.white, //背景颜色设置为白色
backgroundImage: AssetImage(
"images/5.jpeg", //图片
),
),
),
title: const Text(//标题
"George",
style: TextStyle(
fontSize: 15, //字体大小
fontWeight: FontWeight.bold, //字体加粗
),
),
);
}
}

 效果图:


4.png


第一模块蓝色背景模块实现


 写完头像模块突然想起来,第一模块的蓝色背景还没实现呢,现在来实现一个蓝色的背景。因为是背景,所以应该用层叠Stack组件。背景颜色用Container的decoration来设置,实际使用BoxDecoration实现背景颜色盒子的设置,同时还需要设置阴影。BoxDecoration类提供了多种绘制盒子的方法,这个盒子有边框、主体、阴影组成,盒子的形状可能是圆形或者长方形。如果是长方形,borderRadius属性可以控制边界的圆角大小。


class FistBlockMain extends StatelessWidget {
const FistBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Stack(children: <Widget>[
const Backgroud(),
Column(
//竖直布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: User(),
),

Expanded(
flex: 1, //空间占比
child: Text("设置"),
),
Expanded(
flex: 1, //空间占比
child: Text("帮助"),
),
],
),
]);
}
}

class Backgroud extends StatelessWidget {
const Backgroud({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color.fromARGB(220, 100, 149, 237),
boxShadow: [
BoxShadow(
color: Color.fromARGB(220, 100, 149, 237),
blurRadius: 30, //阴影模糊程度
spreadRadius: 1 //阴影扩散程度
)
],
),
);
}
}

 效果图:


5.png


 模板的这个颜色我找了半天也没找到,后来就找个相似的先用着,但是总是看起来没有原来的好看。当个程序员难道还需要懂美术和艺术吗。。。


按钮模块实现


 接着要实现若干带图标的按钮了。模板是一个带图标的按钮,我们用TextButton.icon组件实现。按钮能被选定会影响操作体验,这里使用SelectionContainer是他不能被选中。外层使用Column布局依次放置按钮组件。使用Padding调整间距,是他更好看一些。图标和文字大小都是可以设置的。通过Text组件的TextStyle设置文字的颜色、大小,这里我们使用白色的文字。图标使用Icon组件实现,直接使用Icons.lock_clock内置的icon图标。按钮的onPressed和autofocus需要设置,这样的话点击按钮才会有动画显示。Padding组件再一次使用,这个组件我感觉很好用,可以通过他进一步调整部件的位置,进行美化。


class Buttonblock extends StatelessWidget {
const Buttonblock({super.key});
@override
Widget build(BuildContext context) {
return SelectionContainer.disabled(//选定失效
child: Column(
children: <Widget>[
//子组件
Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.lock_clock,
color: Colors.white,
), //白色图标
label: const Text(
"Timeline",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: (){},//点击事件
autofocus: true,
),
),

Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.message,
color: Colors.white,
), //白色图标
label: const Text(
"Message",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: () {
},
autofocus: true,
),
),
],
),
);
}
}


 效果图:


6.png


按钮点击弹窗showDialog实现


 是按钮当然需要被点击,点击之后我们可以弹一个窗给用户进行各种操作。这里用showDialog实现弹窗。在TextButton.icon的onPressed下实现一个点击弹窗操作。在Flutter里有很多的弹出框,比如AlertDialog、SimpleDialog,调用函数是showDialog。对话框也是一个UI布局,通常会包含标题、内容,以及一些操作按钮。这里实现一个最简单的对话框,如果有需求可以在这个基础上进行修改。


 Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: TextButton.icon(
icon: const Icon(
size: 22,
Icons.message,
color: Colors.white,
), //白色图标
label: const Text(
"Message",
style: TextStyle(
fontSize: 14, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white //白色文字
),
),
onPressed: () {//点击弹框
showDialog<void>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('选择'),
children: <Widget>[
SimpleDialogOption(
child: const Text('选项 1'),
onPressed: () {
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: const Text('选项 2'),
onPressed: () {//点击事件
Navigator.of(context).pop();
},
),
],
);
},
).then((val) {
});
},
autofocus: true,
),
),

 效果图:


7.png


第二个模块设计


 第二个模块是两部分,上边部分是一个在线状态展示区域,下边部分是好友列表,中间有一道分隔线。所以第二部分外层使用Column竖直布局组件,结合Stack组件做一个背景色。Stack可以容纳多个组件,以叠加的方式摆放子组件,后者居上,覆盖上一个组件。Stack也是可以存放一组Widget的组件。


class SecondBlockMain extends StatelessWidget {
const SecondBlockMain({super.key});
@override
Widget build(BuildContext context) {
return
Stack(children: <Widget>[
const Backgroud(),
Column(
//竖直布局
children: const <Widget>[
//子组件
Expanded(
flex: 1, //空间占比
child: Text("上边"),
),
Expanded(
flex:4, //空间占比
child: Text("下边"),
),
],
),
]);
}
}

第二个模块灰色背景颜色实现


 仔细看第二部分发现也是有背景颜色的和阴影的,只不过很浅,不容易看出来。刚才已经实现了带阴影的背景,稍微改一下颜色就可以了,依旧要结合Stack组件。BoxShadow的两个参数blurRadius和spreadRadius经常使用,其中blurRadius是模糊半径,也就是阴影半径,SpreadRadius是阴影膨胀数值,也就是阴影面积扩大几倍。


class Backgroud extends StatelessWidget {
const Backgroud({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 238, 235, 235).withOpacity(0.6),
boxShadow: [
BoxShadow(
color: const Color.fromARGB(255, 204, 203, 203).withOpacity(0.5),
blurRadius: 20, //阴影模糊程度
spreadRadius: 20 ,//阴影扩散程度
offset:const Offset(20,20), //阴影y轴偏移量
)
],
),
);
}
}

 效果图:


8.png


在线状态展示区域实现


 本来想着放一个图片在这个位置就好了,这样简单。但是如果拖动界面,改变小大,那么图片就会变形,很不美观。所以利用横向布局组件Row放在外层,里面包裹Badge组件实现小圆点,通过position、badgeColor等组件调整圆点位置和颜色。


class Top extends StatelessWidget {
const Top({super.key});
@override
Widget build(BuildContext context) {
return Row(children: [
Expanded(
flex: 1,
child: ListTile(
leading: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
child: Badge(
//小圆点
badgeColor: Colors.orange, //小圆点颜色
position: const BadgePosition(
start: -70, top: 0, end: 0, bottom: 0), //小圆点显示位置
borderSide: const BorderSide(
color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const Text(
//标题
"Family",
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.black),
),
),
),
)),
Expanded(
flex: 1,
child: ListTile(
leading: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 0, 0),
child: Badge(
//小圆点
badgeColor: Colors.cyan, //小圆点颜色
position: const BadgePosition(
start: -70, top: 0, end: 0, bottom: 0), //小圆点显示位置
borderSide: const BorderSide(
color: Colors.white, width: 1.5), //外层白色圆圈框框
child: const Text(
//标题
"Friend",
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.black),
),
),
),
)),
]);
}
}

 效果图:


9.png


好友列表实现


 好友列表之前也实现过,这次在以前的基础上修改。我们使用ListView组件实现列表,ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件。底层使用Column结合ListTile组件,ListTile结合CircleAvatar可以实现圆形头像效果,同时也可以设置主副标题,设置 focusColor改变鼠标悬停时列表颜色,


List listData = [  {"title": 'First', "imageUrl": "images/1.jpg", "description": '09:15'},  {"title": 'Second', "imageUrl": "images/2.jpg", "description": '13:10'},];

class FriendList extends StatelessWidget {
const FriendList({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: listData.map((value) {//重复生成列表
return Column(
children: <Widget>[
ListTile(
onTap: (){},
hoverColor: Colors.black,// 悬停颜色
focusColor: Colors.white,//聚焦颜色
autofocus:true,//自动聚焦
leading: CircleAvatar(//头像
backgroundImage: AssetImage(value["imageUrl"]),
),
title: Text(
value["title"],
style: const TextStyle(
fontSize: 25, //字体大小
color: Colors.black),
),
subtitle: Text(value["description"])),
const Padding(
padding: EdgeInsets.fromLTRB(70, 10, 0, 30),
child: Text(
maxLines: 2,
"There are moments in life when you miss someone so much that you just want to pick them from your dreams and hug them for real!",
style: TextStyle(
fontSize: 12,
height: 2, //字体大小
color: Colors.grey),
),
)
],
);
}).toList(), //注意这里要转换成列表,因为listView只接受列表
);
}
}


 效果图:


10.png


第三个模块设计


 现在来第三个模块,聊天界面。分析模板布局,从上到下依次是一个搜索框,分隔线,聊天主界面,输入框,表情、视频、语音工具栏和发送按钮。我们,从上到下把他分成四个小部分来实现,外层使用Column组件。


class ThirdBlockMain extends StatelessWidget {
const ThirdBlockMain({super.key});
@override
Widget build(BuildContext context) {
return Stack(children: <Widget>[
Column(
//竖直布局
children: const <Widget>[
//子组件
Text("1"),
Divider(
height: 0.5,
indent: 20.0,
color: Colors.grey,
),
Text("2"),
Divider(
height: 0.5,
indent: 20.0,
color: Colors.grey,
),
Text("3"),
Text("4"),

],
),
]);
}
}


 效果图:


11.png


搜索框实现


 之前实现过搜索框,直接拿过来改一改。外层添加一个SizedBox组件来控制一下搜索框的大小和位置。


class SearchWidget extends StatefulWidget {

const SearchWidget(
{Key? key,
this.height,
this.width,
this.hintText,
this.onEditingComplete})
: super(key: key);

@override
State<SearchWidget> createState() => _SearchWidgetState();
}

class _SearchWidgetState extends State<SearchWidget> {
var controller = TextEditingController();
@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constrains) {
return SizedBox(
width: 400,
height: 40,
child: TextField(
controller: controller, //控制器
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search), //头部搜索图标
filled: true,
fillColor: Colors.grey.withAlpha(50), // 设置输入框背景色为灰色,并设置透明度
hintText: "Search people",
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
contentPadding: const EdgeInsets.only(bottom: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15), //圆角边框
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
//尾部叉叉图标
icon: const Icon(
Icons.close,
size: 17,
),
onPressed: clearKeywords, //清空操作
splashColor: Theme.of(context).primaryColor,
)),
),
);
});
}
}

 效果图:


12.png


聊天,信息发送界面实现


 因为我的这个并不能真的实现聊天,所以就先放text组件在这把吧,后边再进一步完善。这里简单做一些美化操作,输入框不需要背景颜色,图标需要设置成蓝色,同时调节两个模块的长宽高来适应屏幕。输入框使用TextField,与搜索框使用一致。这里要用到StatefulWidget来完成情况输入框的操作。


class ChatUi extends StatelessWidget {
const ChatUi({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
width: 100,
height: 400,
child: Text(""),
);
}
}

class InPutUi extends StatefulWidget {

const InPutUi(
{Key? key,
this.height,
this.width,
this.hintText,
this.onEditingComplete})
: super(key: key);

@override
State<InPutUi> createState() => _InPutUi();
}

class _InPutUi extends State<InPutUi> {
var controller = TextEditingController();
@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constrains) {
return TextField(
controller: controller, //控制器
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withAlpha(50), // 设置输入框背景色为灰色,并设置透明度
hintText: "Write something...",
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
contentPadding: const EdgeInsets.only(bottom: 20),
border: const OutlineInputBorder(
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
color: Colors.blue,
//尾部叉叉图标
icon: const Icon(
Icons.send,
size: 16,
),
onPressed: clearKeywords, //清空操作
splashColor: Theme.of(context).primaryColor,
)),

);
});
}
}

 效果图:


13.png


底部工具界面实现


 最后来实现底部工具栏。外层使用横向布局来依次放入带图标按钮。这里用到IconButton、MaterialButton两种组件来实现按钮,一种是图标按钮,一种是普通按钮,之前已经实现过,拿来就可以用了。外围使用Padding组件进行填充,方便后期调整每个组件的位置,使它更好看一点。



class Bottom extends StatelessWidget {
const Bottom({super.key});
@override
Widget build(BuildContext context) {
return Row(children: [
Padding(
padding: const EdgeInsets.fromLTRB(30, 0, 0, 0),
child: IconButton(
icon: const Icon(Icons.mood),
tooltip: 'click IconButton',
onPressed: () {},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(580, 20, 0, 22),
child: MaterialButton(
height: 35,
color: Colors.blue,
onPressed: () {}, //点击事件
autofocus: true,
child: const Text(
'Send',
style: TextStyle(
fontSize: 12, //字体大小
fontWeight: FontWeight.bold, //字体加粗
color: Colors.white),
),
),
),
]);
}
}



 效果图:


14.png


总结


 到这里基本上就完成了, 当然,他是不能实际使用的,因为点击、数据交互等功能还没实现,因为我还不会。后期再边学边写吧。


 模板图:


1.png


 完成图:
14.png
 自己实现的与模板还是差距很大的。自己的看起来就没那么美观,我应该去学学美术了,一点艺术细胞都没有。


作者:头好晕呀
来源:juejin.cn/post/7232274061283115045
收起阅读 »

uniapp日常总结--uniapp页面传值

uniapp日常总结--uniapp页面传值在Uniapp中,不同页面之间传值可以通过以下几种方式实现:URL参数传递:可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.params或this.$route.query来获取传递的参数。 ...
继续阅读 »

uniapp日常总结--uniapp页面传值

在Uniapp中,不同页面之间传值可以通过以下几种方式实现:

  1. URL参数传递:

    可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.paramsthis.$route.query来获取传递的参数。


    <uni-link :url="'/pages/targetPage/targetPage?param1=' + value1 + '¶m2=' + value2">跳转到目标页面uni-link>
    // 在目标页面获取参数
    export default {
    mounted() {
    const param1 = this.$route.params.param1;
    const param2 = this.$route.params.param2;
    console.log(param1, param2);
    }
    }
  2. 使用页面参数(Query):

    1. 在触发页面跳转的地方,例如在一个按钮的点击事件中:
    // 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage',
    // 传递的参数,可以是一个对象
    success(res) {
    console.log(res);
    },
    fail(err) {
    console.error(err);
    },
    // 参数传递方式,query 表示通过 URL 参数传递
    // params 表示通过 path 参数传递
    // 一般情况下使用 query 就可以了
    // 使用 params 时,目标页面的路径需要定义成带参数的形式
    // 如 '/pages/targetPage/targetPage/:param1/:param2'
    method: 'query',
    // 要传递的参数
    query: {
    key1: 'value1',
    key2: 'value2'
    }
    });



    //简写 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage?key1=value1&key2=value2',
    });
    1. 在目标页面中,可以通过this.$route.query来获取传递的参数:
export default {
onLoad(query) {
// 获取传递的参数
const key1 = this.$route.query.key1;
const key2 = this.$route.query.key2;

console.log(key1, key2);
},
// 其他页面生命周期或方法等
};

在目标页面的onLoad生命周期中,this.$route.query可以获取到传递的参数。key1key2就是在跳转时传递的参数。如果使用uni.switchTab方法进行页面跳转,是无法直接传递参数的。因为uni.switchTab用于跳转到 tabBar 页面,而 tabBar 页面是在底部显示的固定页面,不支持传递参数。如果需要在 tabBar 页面之间传递参数,可以考虑使用全局变量、本地存储等方式进行参数传递。

  • Vuex状态管理:

    使用Vuex进行全局状态管理,可以在一个页面中修改状态,而在另一个页面中获取最新的状态。

    适用于需要在多个页面之间共享数据的情况。

    如果你的应用使用了Vuex,可以在一个页面的computed属性或methods中触发commit,然后在另一个页面通过this.$store.state获取值。

    在第一个页面:

    // 在页面中触发commit
    this.$store.commit('setValue', value);

    在第二个页面:

    // 在另一个页面获取值
    const value = this.$store.state.value;
    console.log(value);
  • 使用本地存储(Storage):

    使用本地存储(localStorage或uni提供的存储API)将数据存储到本地,然后在另一个页面中读取。适用于需要持久保存数据的情况。如果数据不大,你也可以将数据存储在本地存储中,然后在目标页面读取。

    其中根据使用情景可以使用同步StorageSync或者异步Storage来实现。

    两者存在一定的区别,简单介绍可以查看下方链接:

    uniapp日常总结--setStorageSync和setStorage区别

    同步:使用uni.setStorageSyncuni.getStorageSync等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorageSync('key', value);
    // 在页面B中从本地存储中读取数据
    const value = uni.getStorageSync('key');
    console.log(value);

    异步:使用uni.setStorageuni.getStorage等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorage({
    key: 'yourDataKey',
    data: yourData,
    });
    // 在页面B中从本地存储中读取数据
    uni.getStorage({
    key: 'yourDataKey',
    success: function (res) {
    const pageData = res.data;
    },
    });
  • 事件总线:

    使用uni提供的API进行页面传值,如uni.$emituni.$on

    通过事件触发和监听的方式在页面之间传递数据。

    使用Uniapp的事件总线来进行组件之间的通信。在发送组件中,使用uni.$emit触发一个自定义事件,并在接收组件中使用uni.$on监听这个事件。

    在发送组件:

    uni.$emit('customEvent', data);

    在接收组件:

    uni.$on('customEvent', (data) => {
    console.log(data);
    });
  • 应用全局对象:

    通过uni.$app访问应用全局对象,从而在不同页面之间共享数据。

    在发送页面:

    uni.$app.globalData.value = data;

    在接收页面:

    const value = uni.$app.globalData.value;
    console.log(value);
  • URL参数传递对于简单的场景比较方便。Vuex适用于较大的应用状态管理。本地存储适用于需要在页面刷新后仍然保持的简单数据。事件总线方法适用于简单的组件通信。页面参数相对常用于跳转。根据具体需求和应用场景,选择合适的方式进行数据传递。不同的场景可能需要不同的方法。


    作者:狐说狐有理
    来源:juejin.cn/post/7310786618390855717

    收起阅读 »

    Flutter 日记APP-开篇

    序言 在跟着wendux大佬的书学习flutter后,开始着手写个app进行实战。考虑到没有服务器,所以主要写工具类,无网络交互的app。之前看了《小狗钱钱》这本书,里面的梦想笔记让我印象深刻,便开始着手写一个记录自己梦想笔记的app。 App 构想 创建自...
    继续阅读 »

    序言


    在跟着wendux大佬的书学习flutter后,开始着手写个app进行实战。考虑到没有服务器,所以主要写工具类,无网络交互的app。之前看了《小狗钱钱》这本书,里面的梦想笔记让我印象深刻,便开始着手写一个记录自己梦想笔记的app。


    App 构想



    1. 创建自己的梦想

      1.1 梦想内容和描述

      1.2 梦想日记提醒时间,开启后会设置闹钟定时提醒

    2. 创建梦想日记

      2.1 日记标题和内容

      2.2 为了方便日记输入,接入苹果的文本扫描功能

      2.3 日记每天可多次添加或修改

    3. 日记走势

      3.1 根据每天记录的日记数量进行统计,展示一个charts图

    4. 设置功能

      4.1 支持日夜模式

      4.2 支持国际化语言切换


    目前大概就这些后面准备持续更新日记内容,比如新增记账日记,记录每一笔开销和收入,然后统计每月的开销和收入,让自己对于自己的账目管理更加一目了然;还有行程记录,比如出行提醒,旅游日记等等。为了后面更好的兼容,在开始构建的时候会预留相应的字段。


    App 三方选择



    1. get

      状态管理、国际化、皮肤管理于一体的三方库。当然还有其他功能,目前app比较简单仅使用这些。在选择的时候也在犹豫,要不要用BlocProvider,相对来说,另外两个三方要更加轻量一些,provider的侵入性也没有那么强。最后选择get是考虑到国际化管理和换肤等,使用get一步到位。比如国际化通常会用intl

    2. sqflite

      用于数据存储,把日记都保存到本地数据库进行缓存。

    3. shared_preferences
      本地轻量数据缓存,主要是用来存语言国际化等配置信息。

    4. easy_refresh

      上拉刷新,下拉加载

    5. fluttertoast

      Toast 弹窗,需要注意如果兼容其他平台(window)的话需要传入context。


    剩下的就是更新库到本地,传统技艺:put get


    基本上就是用了这些,可以说麻雀虽小,五脏俱全。后面会持续分享app的开发进度,和一些开发中遇到的问题。


    作者:WhiteMonkey
    来源:juejin.cn/post/7309158214481772553
    收起阅读 »

    Flutter 实现登录 UI

    本文,我将解析怎么前构建一个用户交互的登录页面。这里,我使用 TextField 挂件,这方便用户输入用户名和密码。还使用 FlatButton 挂件,来处理一些动作。当然,我还使用了 Image 挂件来设定登录页面的 logo。 效果图如下: 第一步: m...
    继续阅读 »

    本文,我将解析怎么前构建一个用户交互的登录页面。这里,我使用 TextField 挂件,这方便用户输入用户名和密码。还使用 FlatButton 挂件,来处理一些动作。当然,我还使用了 Image 挂件来设定登录页面的 logo


    效果图如下:




    第一步: main() 函数

    import 'package:flutter/material.dart';void main() {
    runApp(MyApp());
    }class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    debugShowCheckedModeBanner: false,
    home: LoginDemo(),
    );
    }
    }

    这个 main() 函数也就是应用的入口。MyApp 类中添加了一个 LoginDemo 类作为 home 属性的参数。


    第二步:class LoginDemo


    • 设定脚手架的 appBar 属性来作为应用的标题,如下:
    appBar: AppBar(
    title: Text('Login Page'),
    ),

    • 在本次的 UI 布局中,所有的挂件都会放在 Column 挂件中,然后存放在脚手架的 body 中。Column 中的第一个是存放 Container 挂件,用来处理 Image 挂件。
    Container(
    height: 150.0,
    width: 190.0,
    padding: EdgeInsets.only(top: 40),
    decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(200),
    ),
    child: Center(
    child: Image.asset('asset/images/flutter-logo.png'),
    ),
    ),

    flutter-logo.png 文件存放在 asset/images 文件夹中。我们需要在 pubspec.yaml 文件中配置路径。

    # To add assets to your application, add an assets section, like this:
    assets:
    - asset/images/



    添加完资源之后,我们可以运行应用了。


    • 然后,使用 TextField 挂件处理用户名和密码。 TextField 挂件是一个输入挂件,帮助我们处理用户的输入信息。
    Padding(
    padding: EdgeInsets.all(10),
    child: TextField(
    decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'User Name',
    hintText: 'Enter valid mail id as abc@gmail.com'
    ),
    ),
    ),
    Padding(
    padding: EdgeInsets.all(10),
    child: TextField(
    obscureText: true,
    decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'Password',
    hintText: 'Enter your secure password'
    ),
    ),
    ),

    这里的 Padding 挂件能够帮助你设定 TextField 挂件的内边距。



    obscureText 属性值为 true 的时候,帮助我们对 TextField 展示特殊的字符,而不是真正的文本。



    • 我们使用 FlatButton 挂件来处理忘记密码
    FlatButton(
    onPressed: (){
    //TODO FORGOT PASSWORD SCREEN GOES HERE
    },
    child: Text(
    'Forgot Password',
    style: TextStyle(color: Colors.blue, fontSize: 15),
    ),
    ),

    onPressed() 这个函数中,我们可以处理页面跳转或者其他的点击逻辑。


    • 对于登录按钮,我们使用 FlatButton 挂件,但是我们得装饰一下,这里我们使用 Container 进行包裹。
    Container(
    height: 50,
    width: 250,
    decoration: BoxDecoration(
    color: Colors.*blue*, borderRadius: BorderRadius.circular(20),
    ),
    child: FlatButton(
    onPressed: () {
    Navigator.push(
    context,
    MaterialPageRoute(builder: (_) => HomePage()),
    );
    },
    child: Text(
    'Login',
    style: TextStyle(color: Colors.*white*, fontSize: 25),
    ),
    ),
    ),

    上面我们设定了 Container 挂件的 heightwidth 属性,所以 flatbutton 也会获取到相同的高度和宽度。


    decoration 属性允许我们设计按钮,比如颜色 colorColors.blueborderRadiusBorderRadius.circular(20) 属性。


    • 最后指定 Text 挂件以为新用户创建账号

    这里我们可以通过 GestureDetector 挂件的 onTap() 功能进行导航操作。或者创建类似忘记密码按钮的 onPressed() 事件。


    这里是整个项目的完整代码:

    // lib/HomePage.dart

    import 'package:flutter/material.dart';

    class HomePage extends StatefulWidget {
    @override
    _HomePageState createState() => _HomePageState();
    }

    class _HomePageState extends State<HomePage> {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('Home Page'),
    ),
    body: Center(
    child: Container(
    height: 80,
    width: 150,
    decoration: BoxDecoration(
    color: Colors.blue, borderRadius: BorderRadius.circular(10)),
    child: FlatButton(
    onPressed: () {
    Navigator.pop(context);
    },
    child: Text(
    'Welcome',
    style: TextStyle(color: Colors.white, fontSize: 25),
    ),
    ),
    ),
    ),
    );
    }
    }
    // lib/main.dart
    import 'package:flutter/material.dart';

    import 'HomePage.dart';

    void main() {
    runApp(MyApp());
    }

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    debugShowCheckedModeBanner: false,
    home: LoginDemo(),
    );
    }
    }

    class LoginDemo extends StatefulWidget {
    @override
    _LoginDemoState createState() => _LoginDemoState();
    }

    class _LoginDemoState extends State<LoginDemo> {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    backgroundColor: Colors.white,
    appBar: AppBar(
    title: Text("Login Page"),
    ),
    body: SingleChildScrollView(
    child: Column(
    children: <Widget>[
    Padding(
    padding: const EdgeInsets.only(top: 60.0),
    child: Center(
    child: Container(
    width: 200,
    height: 150,
    /*decoration: BoxDecoration(
    color: Colors.red,
    borderRadius: BorderRadius.circular(50.0)),*/
    child: Image.asset('asset/images/flutter-logo.png')),
    ),
    ),
    Padding(
    //padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
    padding: EdgeInsets.symmetric(horizontal: 15),
    child: TextField(
    decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'Email',
    hintText: 'Enter valid email id as abc@gmail.com'),
    ),
    ),
    Padding(
    padding: const EdgeInsets.only(
    left: 15.0, right: 15.0, top: 15, bottom: 0),
    //padding: EdgeInsets.symmetric(horizontal: 15),
    child: TextField(

    obscureText: true,
    decoration: InputDecoration(
    border: OutlineInputBorder(),
    labelText: 'Password',
    hintText: 'Enter secure password'),
    ),
    ),
    FlatButton(
    onPressed: (){
    //TODO FORGOT PASSWORD SCREEN GOES HERE
    },
    child: Text(
    'Forgot Password',
    style: TextStyle(color: Colors.blue, fontSize: 15),
    ),
    ),
    Container(
    height: 50,
    width: 250,
    decoration: BoxDecoration(
    color: Colors.blue, borderRadius: BorderRadius.circular(20)),
    child: FlatButton(
    onPressed: () {
    Navigator.push(
    context, MaterialPageRoute(builder: (_) => HomePage()));
    },
    child: Text(
    'Login',
    style: TextStyle(color: Colors.white, fontSize: 25),
    ),
    ),
    ),
    SizedBox(
    height: 130,
    ),
    Text('New User? Create Account')
    ],
    ),
    ),
    );
    }
    }


    本文采用意译的方式翻译。原文 levelup.gitconnected.com/login-page-…



    推荐阅读

    作者:Jimmy
    链接:https://juejin.cn/post/7147865530869907487
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    className 还能这么用,你学会了吗

    抛出问题 className大家都用过吧,用它在react项目中设置样式。它的用法很简单,除了可以设置一个样式外,react中也可以使用className引入多个类样式。 这次在写项目的时候,碰到一个非常小但是当时却一直解决不了的问题。后面在复盘的时候将它解决...
    继续阅读 »

    抛出问题


    className大家都用过吧,用它在react项目中设置样式。它的用法很简单,除了可以设置一个样式外,react中也可以使用className引入多个类样式。


    这次在写项目的时候,碰到一个非常小但是当时却一直解决不了的问题。后面在复盘的时候将它解决了。问题大致是这样的:


    有两个活动页,每个活动页上都有一个活动规则图标来弹出活动规则,活动规则图标距离顶部会有一个值。现在问题就是这个活动规则在这两个活动页距离顶部的这个值是不一样的,但是我已经将这个活动规则图标做成了组件,并在这两个活动页里都调用了它,从而导致两个页面的样式会相同。如下图所示:




    解决问题


    这个问题不算很大,但是属于细节问题。就和我的组长所说的一样,一个项目应该要做到先完成再完美。所以我当时的解决方法是再写一个活动规则组件,只是将距离顶部的值做出修改即可。效果确实是达到了,不过在最后复盘代码的时候,组长注意到了这两个组件,并开始询问我为什么这样做。


    组长:Rule_1Rule_2这两个组件是什么意思,我看它们没有很大的区别呀。


    我便简单说了一下缘由。


    组长接着说:你忘了组件是什么吗?一个CSS样式值不同就大费周章地新增一个组件,这岂不是太浪费了。再去想想其他方案。


    通过这一番谈话我想起了组件化思想的运用,发现之前解决的这个小问题解决的并不够好。于是,我就带着组件化思想又来重新完善它。


    我重新写了一个demo代码,将主要内容和问题在demo代码中体现出来。下面是原版活动规则组件demo代码,之后的代码都是基于demo代码完成的

    import React from "react";
    import "./index.css";
    const Header = ({ onClick }) => {
    return (
    <>
    <div className="container_hd">
    <div
    className='affix'
    onClick={onClick}
    ></div>
    </div>
    </>
    );
    };
    export default Header;

    组件化思想


    我自己问自己:既然已经写好了一个活动规则组件,为什么仅仅因为一个样式值的不同而去新增一个功能一样的组件?很显然,这种方法是最笨的方案。既然是组件,那就应该要有复用性,或者说只需在原有的基础上稍加改动就可达到效果。


    这是样式的问题,因此要从根本上解决问题。单纯地修改 CSS 样式肯定不行,因为两个页面两个不同的样式。


    className 运用


    className 就不用多介绍了,经常能使用,咱们直接来看如何解决问题。在这里我定义了一个 Value 值,用来区分是在哪个页面的,比如分别有提交页和成功页,我在成功页设置一个 Value 值,,然后将 Value 值传入到活动规则组件,那么在活动规则组件里只需要判断 Value 值是否等于成功页的 Value 值即可。在 className 处做一个三元判断,如下所示:

    className={`affix_${Value === "0" ? "main" : "submit"}`}

    相当于如果Value等于0的时候类名为affix_main,否则为affix_submit。最后再css将样式完善即可。完整代码可以参考如下:

    • 成功页组件
    import Header from "./components/Header";

    const Success = () => {
    const Value = "0";
    return (
    <div style={{ backgroundColor: "purple", width: "375px", height: "670px" }}>
    <Header Value={Value}></Header>
    </div>
    );
    };

    export default Success;

    • 活动规则组件
    import React from "react";
    import "./index.css";
    const Header = ({ onClick, Value }) => {
    return (
    <>
    <div className="container_hd">
    <div
    className={`affix_${Value === "0" ? "main" : "submit"}`}
    onClick={onClick}
    ></div>
    </div>
    </>
    );
    };
    export default Header;

    • 活动规则组件样式
    .container_hd {
    width: 100%;
    }
    .affix_main {
    position: absolute;
    top: 32px;
    right: -21px;
    z-index: 9;
    width: 84px;
    height: 26px;
    background: url('./assets/rule.png');
    background-size: contain;
    background-repeat: no-repeat;
    }
    .affix_submit {
    position: absolute;
    top: 12px;
    right: -21px;
    z-index: 9;
    width: 84px;
    height: 26px;
    background: url('./assets/rule.png');
    background-size: contain;
    background-repeat: no-repeat;
    }



    通过对比效果图可以看出,两者的效果确实发生变化。完成之后,我心里在想:为什么当时就没想出这个简单易行的方案呢?动态判断并设置类名,至少比最开始的新增一个组件的方法高级多了。


    总结问题


    对于这个问题的解决就这样告一段落了,虽然看起来比较简单(一个动态设置类名),但是通过这个className的灵活使用,让我对className的用法有了更进一步的掌握,也不得不感叹组件化思想的广泛运用,这里最大程度地将组件化思想通过className 发挥出来。


    因此,希望通过这个问题,来学会className的灵活用法,并理解好组件化思想。当然如果大家还有更好的解决方案的话,欢迎在评论区告诉我。


    作者:一条会coding的Shark
    链接:https://juejin.cn/post/7177042554666352697
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    手机网站支付(在uniapp同时支持H5和app!)

    前言 uniapp开发项目时,遇到对接支付宝手机网站支付。如果仅仅是H5端,那分分钟搞定的(不就是调用后端接口,提交返回表单即可调起支付)。然而,这次需求是H5和App都使用该支付。这倒是新奇了,App中能使用支付宝手机网站支付吗?那它怎么提交表单,怎么处理...
    继续阅读 »

    前言



    uniapp开发项目时,遇到对接支付宝手机网站支付。如果仅仅是H5端,那分分钟搞定的(不就是调用后端接口,提交返回表单即可调起支付)。然而,这次需求是H5和App都使用该支付。这倒是新奇了,App中能使用支付宝手机网站支付吗?那它怎么提交表单怎么处理支付成功时的回调页面跳转




    若你仅H5使用支付宝手机网站支付参考我的文章



    一、使用技术



    1. 解决app如何提交表单:

      renderjs: app-vue 中调用在视图层操作dom,运行for web的js库
      参考文章

    2. 解决app处理支付成功时的回调页面跳转:

      uni.webview.1.5.4.js: 引入该js,使得普通的H5支持uniapp路由跳转接口参考uniapp文档


    二、思路描述



    注意:此处会详细描述思路,请根据自身项目需要自行更改



    step1|✨用户点击支付


    async aliPhonePay() {
    let urlprefix = baseUrl == '/api' ?
    'http://192.168.105.43'
    :
    baseUrl;

    let params = {
    /**1. 支付成功回调页面-中转站*/
    // #ifdef H5
    frontUrl: `${urlprefix}/middle_html/h5.html?type=${this.formartOrderType(this.orderInfo.orderSn)}`,
    // #endif
    // #ifdef APP
    frontUrl: `${urlprefix}/middle_html/app.html?type=${this.formartOrderType(this.orderInfo.orderSn)}`,
    // #endif


    goodsDesc: this.orderInfo.itemName,
    goodsTitle: this.orderInfo.itemName,
    orderSn: this.orderInfo.orderSn,
    orderType: this.formartOrderType(this.orderInfo.orderSn),
    paymentPrice: (this.orderInfo.paymentPrice*1).toFixed(2),
    payChannel: this.paymentType,
    // 快捷支付必传
    bizProtocolNo: this.bankInfo.bizProtocolNo, //用户业务协议号 ,
    payProtocolNo: this.bankInfo.payProtocolNo, //支付协议号
    }

    this.$refs.dyToast.loading()
    let { data } = await PayCenterApi.executePayment(params)
    this.$refs.dyToast.hide()

    /**2. 保存请求得到的表单到strorage,跳转页面*/
    uni.setStorageSync('payForm', data.doPost);
    uni.redirectTo({
    url:`/pages/goods/goodsOrderPay/new-pay-invoke`
    })
    },

    /pages/goods/goodsOrderPay/new-pay-invoke: h5和app都支持的提交表单调起支付方式


    <template>
    <view class="new-pay-invoke-container">
    <view :payInfo="payInfo" :change:payInfo="pay.openPay" ref="pay"></view>
    <u-loading-page loading loading-text="调起支付中"></u-loading-page>
    </view>
    </template>

    <script>
    export default {
    name: 'new-pay-invoke',

    data() {
    return {
    payInfo: ''
    }
    },

    onLoad(options) {
    this.payInfo = uni.getStorageSync('payForm');
    }
    }
    </script>

    <script module="pay" lang="renderjs">
    export default {
    methods: {
    /**h5和app都支持的提交表单调起支付方式*/
    openPay(payInfo, oldVal, ownerInstance, instance) {
    // console.log(payInfo, oldVal, ownerInstance, instance);
    if(payForm) {
    document.querySelector('body').innerHTML = payInfo
    const div = document.createElement('div')
    div.innerHTML = payForm
    document.body.appendChild(div)
    document.forms[0].submit()
    }
    }
    }
    }
    </script>

    <style lang="scss" scoped>

    </style>

    step2|✨支付成功回调页面


    app.html: 作为一个网页,放到线上服务器,注意需要与传递给后端回调地址保持一致


    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <title>app支付成功回调页面-中转站</title>
    </head>
    <body>
    <!-- uni 的 SDK -->
    <!-- 需要把 uni.webview.1.5.4.js 下载到自己的服务器 -->
    <script type="text/javascript" src="https://gitee.com/dcloud/uni-app/raw/dev/dist/uni.webview.1.5.4.js"></script>
    <script type="text/javascript">
    // 待触发 `UniAppJSBridgeReady` 事件后,即可调用 uni 的 API。
    document.addEventListener('UniAppJSBridgeReady', function() {
    /**引入uni.webview.1.5.4.js后,就支持uni各种路由跳转,使得该H5页面能控制uniapp App页面跳转*/
    /**这里做的事是判断订单类型,跳转到app对应的订单支付成功页面 */
    uni.reLaunch({
    url: '对应支付成功页面?payCallback=1'
    // 加payCallback=1参数原因:支付成功页面有时是订单记录,而订单
    // 记录不用走支付流程,用户也能进入。这时就需要该参数判断点击
    // 返回是 返回上一级 还是 返回首页了
    });
    });
    </script>
    </body>
    </html>


    h5.html:与app.html做法一致,但不需要用到uni.webview.1.5.4.js,这里就不赘述了


    以上就是app和h5使用支付宝手机网站支付的全部流程了。
    app有点小瑕疵(app提交表单页面后,支付宝页面导航栏会塌陷到状态栏,用户体验稍微差点)
    我的猜想:
    h5按正常表单提交走,而app利用<webview src="本地网页?表单参数" />本地网页,获取表单参数并拼接表单提交
    还没具体去实现这个猜想,或者大家有更好的解决方式,欢迎评论区展示!!!

    作者:爆竹
    来源:juejin.cn/post/7276692859967864891
    收起阅读 »

    React的并发悖论

    大家好,我卡颂。 当一个React应用逻辑变得复杂后,组件render花费的时间会显著增长。如果从组件render到视图渲染期间消耗的时间过长,用户就会感知到页面卡顿。 为了解决这个问题,有两个方法:让组件render的过程从同步变为异步,这样render过程...
    继续阅读 »

    大家好,我卡颂。


    当一个React应用逻辑变得复杂后,组件render花费的时间会显著增长。如果从组件render视图渲染期间消耗的时间过长,用户就会感知到页面卡顿。


    为了解决这个问题,有两个方法:

    1. 组件render的过程从同步变为异步,这样render过程页面不会卡死。这就是并发更新的原理

    2. 减少需要render的组件数量,这就是常说的React性能优化


    通常,对于不同类型组件,我们会采取以上不同的方法。比如,对于下面这样的有耗时逻辑的输入框,方法1更合适(因为并发更新能减少输入时的卡顿):

    function ExpensiveInput({onChange, value}) {
    // 耗时的操作
    const cur = performance.now();
    while (performance.now() - cur < 20) {}

    return <input onChange={onChange} value={value}/>;
    }

    那么,能不能在整个应用层面同时兼顾这2种方式呢?答案是 —— 不太行。


    这是因为,对于复杂应用,并发更新与性能优化通常是相悖的。就是本文要聊的 —— 并发悖论。


    欢迎加入人类高质量前端交流群,带飞


    从性能优化聊起


    对于一个组件,如果希望他非必要时不render,需要达到的基本条件是:props的引用不变。


    比如,下面代码中Child组件依赖fn props,由于fn是内联形式,所以每次App组件render时引用都会变,不利于Child性能优化:

    function App() {
    return <Child fn={() => {/* xxx */}}/>
    }

    为了Child性能优化,可以将fn抽离出来:

    const fn = () => {/* xxx */}

    function App() {
    return <Child fn={fn}/>
    }

    fn依赖某些props或者state时,我们需要使用useCallback

    function App({a}) {
    const fn = useCallback(() => a + 1, [a]);
    return <Child fn={fn}/>
    }

    类似的,其他类型变量需要用到useMemo


    也就是说,当涉及到性能优化时,React的代码逻辑会变得复杂(需要考虑引用变化问题)。


    当应用进一步复杂,会面临更多问题,比如:

    • 复杂的useEffect逻辑

    • 状态如何共享


    这些问题会与性能优化问题互相叠加,最终导致应用不仅逻辑复杂,性能也欠佳。


    性能优化的解决之道


    好在,这些问题有个共同的解决方法 —— 状态管理。


    上文我们聊到,对于性能优化,关键的问题是 —— 保持props引用不变。


    在原生React中,如果a依赖bb依赖c。那么,当a变化后,我们需要通过各种方法(比如useCallbackuseMemo)保持bc引用的稳定。


    做这件事情本身(保持引用不变)对开发者来说就是额外的心智负担。那么,状态管理是如何解决这个问题的呢?


    答案是:状态管理库自己管理所有原始状态以及派生状态。


    比如:

    • Recoil中,基础状态类型被称为Atom,其他派生状态都是基于Atom组合而来

    • Zustand中,基础状态都是create方法创建的实例

    • Redux中,维护了一个全局状态,对于需要用到的状态通过selector从中摘出来


    这些状态管理方案都会自己维护所有的基础状态与派生状态。当开发者从状态管理库中引入状态时,就能最大限度保持props引用不变。


    比如,下例用Zustand改造上面的代码。由于状态a和依赖afn都是由Zustand管理,所以fn的引用始终不变:

    const useStore = create(set => ({
    a: 0,
    fn: () => set(state => ({ a: state.a + 1 })),
    }))


    function App() {
    const fn = useStore(state => state.fn)
    return <Child fn={fn}/>
    }

    并发更新的问题


    现在我们知道,性能优化的通用解决途径是 —— 通过状态管理库,维护一套逻辑自洽的外部状态(这里的外部是区别于React自身的状态),保持引用不变。


    但是,这套外部状态最终一定会转化为React的内部状态(再通过内部状态的变化驱动视图更新),所以就存在状态同步时机的问题。即:什么时候将外部状态与内部状态同步?


    在并发更新之前的React中,这并不是个问题。因为更新是同步、不会被打断的。所以对于同一个外部状态,在整个更新过程中都能保持不变。


    比如,在如下代码中,由于List组件的render过程不会打断,所以list在遍历过程中是稳定的:

    function List() {
    const list = useStore(state => state.list)
    return (
    <ul>
    {list.map(item => <Item key={item.id} data={item}/>}
    </ul>
    )
    }

    但是,对于开启并发更新的React,更新流程可能中断,不同的Item组件可能是在中断前后不同的宏任务中render,传递给他们的data props可能并不相同。这就导致同一次更新,同一个状态(例子中的list)前后不一致的情况。


    这种情况被称为tearing(视图撕裂)。


    可以发现,造成tearing的原因是 —— 外部状态(状态管理库维护的状态)与React内部状态的同步时机出问题。


    这个问题在当前React中是很难解决的。退而求其次,为了让这些状态库能够正常使用,React专门出了个hook —— useSyncExternalStore。用于将状态管理库触发的更新都以同步的方式执行,这样就不会有同步时机的问题。


    既然是以同步的方式执行,那肯定没法并发更新啦~~~


    总结


    实际上,凡是涉及到自己维护了一个外部状态的库(比如动画库),都涉及到状态同步的问题,很有可能无法兼容并发更新。


    所以,你会更倾向下面哪种选择呢:

    1. care并发更新,以前React怎么用,现在就怎么用

    2. 根据项目情况,平衡并发更新与性能优化的诉求


    作者:魔术师卡颂
    链接:https://juejin.cn/post/7223787990950232125
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    2个奇怪的React写法

    大家好,我卡颂。 虽然React官网用大量篇幅介绍最佳实践,但因JSX语法的灵活性,所以总是会出现奇奇怪怪的React写法。 本文介绍2种奇怪(但在某些场景下有意义)的React写法。也欢迎大家在评论区讨论你遇到过的奇怪写法。 欢迎加入人类高质量前端交流群,带...
    继续阅读 »

    大家好,我卡颂。


    虽然React官网用大量篇幅介绍最佳实践,但因JSX语法的灵活性,所以总是会出现奇奇怪怪的React写法。


    本文介绍2种奇怪(但在某些场景下有意义)的React写法。也欢迎大家在评论区讨论你遇到过的奇怪写法。


    欢迎加入人类高质量前端交流群,带飞


    ref的奇怪用法


    这是一段初看让人很困惑的代码:

    function App() {
    const [dom, setDOM] = useState(null);

    return <div ref={setDOM}></div>;
    }

    让我们来分析下它的作用。


    首先,ref有两种形式(曾经有3种):

    1. 形如{current: T}的数据结构

    2. 回调函数形式,会在ref更新、销毁时触发


    例子中的setDOMuseStatedispatch方法,也有两种调用形式:

    1. 直接传递更新后的值,比如setDOM(xxx)

    2. 传递更新状态的方法,比如setDOM(oldDOM => return /* 一些处理逻辑 */)


    在例子中,虽然反常,但ref的第二种形式和dispatch的第二种形式确实是契合的。


    也就是说,在例子中传递给refsetDOM方法,会在div对应DOM更新、销毁时执行,那么dom状态中保存的就是div对应DOM的最新值。


    这么做一定程度上实现了感知DOM的实时变化,这是单纯使用ref无法具有的能力。


    useMemo的奇怪用法


    通常我们认为useMemo用来缓存变量propsuseCallback用来缓存函数props


    但在实际项目中,如果想通过缓存props的方式达到子组件性能优化的目的,需要同时保证:

    • 所有传给子组件的props的引用都不变(比如通过useMemo

    • 子组件使用React.memo


    类似这样:

    function App({todos, tab}) {
    const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]);

    return <Todo data={visibleTodos}/>;
    }

    // 为了达到Todo性能优化的目的
    const Todo = React.memo(({data}) => {
    // ...省略逻辑
    })

    既然useMemo可以缓存变量,为什么不直接缓存组件的返回值呢?类似这样:

    function App({todos, tab}) {
    const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]);

    return useMemo(() => <Todo data={visibleTodos}/>, [visibleTodos])
    }

    function Todo({data}) {
    return <p>{data}</p>;
    }

    如此,需要性能优化的子组件不再需要手动包裹React.memo,只有当useMemo依赖变化后子组件才会重新render


    总结


    除了这两种奇怪的写法外,你还遇到哪些奇怪的React写法呢?


    作者:魔术师卡颂
    链接:https://juejin.cn/post/7210048692623114298
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    Kotlin注解探秘:让代码更清晰

    快速上手 @Target(   AnnotationTarget.CLASS,   AnnotationTarget.FUNCTION,   AnnotationTar...
    继续阅读 »

    快速上手


    @Target(  
     AnnotationTarget.CLASS,  
     AnnotationTarget.FUNCTION,  
     AnnotationTarget.VALUE_PARAMETER,  
     AnnotationTarget.EXPRESSION,  
     AnnotationTarget.CONSTRUCTOR  
    )

    @Retention(AnnotationRetention.SOURCE)
    @Repeatable
    @MustBeDocumented
    annotation class MyAnnotation

    @MyAnnotation @MyAnnotaion class Test @MyAnnotation constructor(val name: String) {
        @MyAnnotation fun test(@MyAnnotation num: Int)Int = (@MyAnnotation 1)
    }

    注解的声明


    注解使用关键字annotation来声明,比如快速上手中的例子,使用annotation class MyAnnotation就声明了一个注解,我们可以按照定义的规则将其放在其他元素身上


    元注解


    下面的注解了解过Java的肯定不陌生,元注解就是可以放在注解上面的注解





    • @Target: 用来指定注解可以应用到哪些元素上,有以下可选项



      • CLASS: 可以应用于类、接口、枚举类



      • ANNOTATION_CLASS: 可以应用于注解



      • TYPE_PARAMETER



      • PROPERTY



      • FIELD



      • LOCAL_VARIABLE



      • VALUE_PARAMETER: 可以应用于字面值



      • CONSTRUCTOR: 可以应用于构造函数



      • FUNCTION: 可以应用于函数



      • PROPERTY_GETTER



      • PROPERTY_SETTER



      • TYPE



      • EXPRESSION: 可以应用于表达式



      • FILE



      • TYPEALIAS





    • @Retention: 用来指定注解的生命周期



      • SOURCE: 仅保存在源代码中



      • BINARY: 保存在字节码文件中,但是运行是无法获取



      • RUNTIME: 保存在字节码文件中,运行时可以获取





    • @Repeatable: 允许此注解可以在单个元素上多次使用


    拿上方的代码来简单介绍几个元注解


    @Target


    可以看一下@Target的源码


     * This meta-annotation indicates the kinds of code elements which are possible targets of an annotation.
     *
     * If the target meta-annotation is not present on an annotation declaration, the annotation is applicable to the following elements:
     * [CLASS], [PROPERTY], [FIELD], [LOCAL_VARIABLE], [VALUE_PARAMETER], [CONSTRUCTOR], [FUNCTION], [PROPERTY_GETTER], [PROPERTY_SETTER].
     *
     * @property allowedTargets list of allowed annotation targets
     */
    @Target(AnnotationTarget.ANNOTATION_CLASS)
    @MustBeDocumented
    public annotation class Target(vararg val allowedTargets: AnnotationTarget)

    在源码中可以看到,Target注解中要传入的参数为allowedTargets,使用了vararh关键字,可传入多个参数,参数的类型为AnnotationTarget,它是一个枚举类,再进入AnnotationTarget的源码就可以看到它有上方元注解中列出的那些。


    在快速上手的示例中我们的@Target中传入了Class FUNCTION VALUE_PARAMETER EXPRESSION CONSTRCTOR,表示此注解可以放在类、接口、枚举、函数、字面值、表达式和构造函数上


    @Retention


    此注解就是指定它什么时候失效 默认是RUNTIME, 快速上手中是用的SOURCE,表示它仅存在于源码中,在编译成字节码后将会消失,如果指定了BINARY,则可以存在于字节码文件中,但是运行时无法获取,反射无法获取


    注解的属性


    注解可在主构造参数内传值


    annotation class MyAnnotation2(val effect: String)

    class Test2 {
        @MyAnnotation2("Test")
        fun test() {
            println("Run test")
        }
    }

    比如上面的例子,可以在主构造函数内传入一个参数,参数支持的类型有以下几种





    • Kotlin中的八种“基本数据类型”(Byte, Short, Int, Long, Float, Double, Boolean, Char)



    • String类型



    • 引用类型(Class)



    • 枚举类型



    • 注解类型



    • 以上类型的数组类型 需要注意的是,官网中特别说明参数不可以传入可空类型,比如"String?",因为JVM不支持null存储在注解的属性中


    注解的作用


    如果是熟悉Java的开发者对注解的作用肯定是非常熟悉。 注解可以提供给编译器、运行时环境、其他代码库以及框架提供很多可用信息。 可用作标记,可供第三方技术库、框架识别信息,比如大家熟悉的SpringBoot,很多事情就是通过注解和反射来实现 可用来提供更多的上下文信息,比如方法的类型参数、返回值类型、错误处理


    后面可结合反射来深入理解Kotlin在开发中的用途


    作者:AB-style
    来源:mdnice.com/writing/5b8eb45e3b1e4b23a57926bd58b7f540
    收起阅读 »

    原生应用要亡了!

    iOS
    跨平台混合应用(及替代方案)取代了性能优先的原生应用 纯粹的原生应用通常是一种依赖于平台的GUI程序, 它使用特定操作系统的本地开发语言和GUI框架. 例如, Gedit 是一个原生应用, 因为它使用 C 和 GTK 作为实现依赖. Notepad++ 是一...
    继续阅读 »

    跨平台混合应用(及替代方案)取代了性能优先的原生应用




    纯粹的原生应用通常是一种依赖于平台的GUI程序, 它使用特定操作系统的本地开发语言和GUI框架. 例如, Gedit 是一个原生应用, 因为它使用 C 和 GTK 作为实现依赖. Notepad++ 是一个原生应用, 因为它使用 C/C++ 和 Win32 GUI API. 这些原生应用还保留了操作系统特有的UI/UX原则和本地功能. 因此, 电脑用户可以轻松上手并与其他内置原生应用一起使用这些应用. 这些传统的原生应用即使在低端硬件上也能流畅运行, 因为它们没有使用中间消息传递模块或嵌入式渲染/代码执行引擎--它们只是触发内置SDK功能的二进制文件. 原生桌面应用和移动应用开发的情况都是一样的.


    混合应用开发运动结束了原生应用开发的黄金时代, 但却创造了一种新的方式, 可以在创纪录的时间内构建类似原生的跨平台应用. 此外, 混合应用的性能问题导致了另一种使用自定义渲染表面和代码执行环境的类原生应用的发展.


    让我们来谈谈传统原生应用开发的弊端.


    Why Native Apps Are the Best 为什么原生应用是最好的


    每个操作系统通常都预装了通用的GUI软件程序. 例如, Ubuntu提供了原生终端, 文本编辑器, Settings应用, 文件管理器等. 这些内置应用无疑遵循了相同的UI/UX原则, 而且由于出色的软件设计和原生SDK的使用, 占用的磁盘空间, 内存和CPU处理能力更低. 第三方原生应用的工作原理也与内置操作系统应用相同. 它们不会过度使用系统资源, 而是根据为用户提供的功能公平地使用计算能力.


    从所有面向用户的角度来看, 原生应用都非常出色. 它们绝不会拖慢低端电脑的运行速度. 此外, 它们也不会敦促用户改变操作系统特有的UI/UX做法. 看看Remmina RDP(原生GUI程序)与Ubuntu内置终端的对比:



     Remmina和Ubuntu上的终端


    每个移动操作系统都提供了原生SDK, 用于开发特定平台的应用捆绑包. 例如, 您可以使用Android SDK构建高性能, 轻量级和用户友好的移动应用. 看看著名的VLC媒体播放器的Android版本是如何通过XML布局实现"关于"视图的:



     VLC Android项目实现了原生应用视图.


    混合应用: 类似本地的Web应用


    即使原生应用为用户提供了最好的GUI程序, 为什么现代开发人员还是开始开发混合应用呢? 从应用用户的角度来看, 原生应用是非常好的, 但它们却给应用开发人员带来了一个关键问题. 尽管一些操作系统提供了与POSIX标准类似的底层应用接口, 但大多数内置的应用开发SDK都提供了不同编程语言的不同应用接口. 因此, 应用开发人员不得不为一个软件产品维护多个与平台相关的代码库. 这种情况增加了跨平台原生应用的开发难度, 因为一个新功能需要多个特定平台的实现.


    混合应用开发通过提供统一的SDK和语言来为多个平台开发应用, 从而解决了这一问题. 开发人员开始使用Electron, NW.js, Apache Cordova和类似Ionic的框架, 利用Web技术构建跨平台应用. 这些框架在Web浏览器组件内呈现基于HTML的类原生应用GUI, 并通过本地-JavaScript接口和桥接器调用基于JavaScript封装的特定平台本地API. 看看Skype如何在Ubuntu上用HTML呈现类似本地的屏幕:



     Skype的首选项窗口.


    桌面应用配有Web浏览器和Node.js运行模块. 移动应用则使用现有的特定平台浏览器视图(即Android Webview).


    混合应用解决方案解决了开发人员的问题, 却给用户带来了新的麻烦. 由于基于Web的解析和渲染, 混合应用的运行速度比原生应用慢数百倍. 一个简单的跨平台计算器应用可能会占用数百兆字节的存储空间. 运行多个跨平台应用窗口就像运行多个重型Web浏览器. 不幸的是, 大多数用户甚至感觉不到这些问题, 因为他们使用的是功能强大的现代硬件组件.


    混合替代方案的兴起


    一些开发人员仍然非常关注应用的性能--他们需要应用在低端机器上也能使用. 因此, 他们开始开发更接近原生应用的跨平台应用, 而不使用Web视图驱动方法. 开发人员开始使用Flutter和类似React Native的框架. 与基于网页视图的方法相比, 这些框架为跨平台应用开发提供了更好的解决方案, 但它们无法像真正的原生应用那样进行开发.


    Flutter没有使用原生的, 特定平台的UI/UX原则. React Native在每个应用中嵌入了JavaScript引擎, 性能不如原生应用. 与基于网页视图的方法相比, 这些混合替代方案无疑提供了更好的跨平台开发解决方案, 但在应用大小和性能方面仍无法与真正的原生应用相媲美.


    你可以从以下报道中了解Flutter如何与混合应用开发(Electron)竞争:


    拜拜Electron, 你好Flutter


    混合(和替代方案)赢得了软件市场!


    每个商业实体都试图通过开发网站和Web应用进入互联网. 与独立的应用相比, 计算机用户更愿意使用在线服务. 因此, Web浏览器开始改进, 增加了各种以开发者为中心的功能, 如新的Web API, 可访问性支持, 离线支持等. 对开发人员友好的JavaScript鼓励每个开发人员在任何情况下都使用它.


    借助混合应用开发技术, 开发人员可以在最短时间内将现有的Web应用转化为桌面应用(如WhatsApp, Slack 等). 他们将React, Vue和Svelte应用与本地窗口框架封装在一起, 创建了功能齐全的跨平台桌面应用. 这种方法节省了数千开发人员的工时和开发成本. 因此, Electron成为了现代桌面应用的开发解决方案. 然后, 一个只需几兆内存和存储空间的代码编辑器程序就变成了现在这样:



     Visual Studio Code占用约600M内存.


    一般用户不会注意到这一点, 因为每个人都至少使用8或16GB内存. 此外, 他们的存储设备也不会让他们感受到 500M字节代码编辑器的沉重(TauriNeutralinojs解决了应用大小的问题, 但它们仍在制作混合应用).


    同样, 如果应用变得缓慢, 典型的移动用户往往会将责任归咎于设备. 现代用户经常升级设备, 以解决应用开发人员造成的性能问题. 因此, 在当今的软件开发行业, 混合应用开发比本地应用开发更受欢迎. 此外, 混合替代方案(如 Flutter, React Native等)也变得更加流行.


    总结一下


    混合应用开发框架和其他替代框架为构建跨平台应用提供了一个高效, 开发人员优先的环境. 但是, 从用户的角度来看, 这些开发方法会产生一些隐藏的性能和可用性问题. 现代强大的硬件组件处理能力可以掩盖这些开发方法中的技术问题. 此外, 与依赖平台的原生应用开发相比, 这些方法提供了更富有成效, 开发人员优先的开发环境. 编程新手开始学习桌面应用的Electron开发, 移动应用的Flutter开发和React Native开发, 就像他们跳过C作为他们的第一门编程语言一样.


    因此, 原生应用的黄金时代走到了尽头. 幸运的是, 程序员仍在维护旧的原生应用代码库. 操作系统永远不会将其预先包含的应用迁移到混合应用中. 与此同时, 一些开发人员使用类似SDL的跨平台, 高性能原生绘图库构建轻量级跨平台应用. 尽管现代混合应用开发和替代方法已成为软件行业的默认方式, 但我们仍可以保留现有的纯原生定位.


    作者:bytebeats
    链接:https://juejin.cn/post/7273024681631858749
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    flutter有哪些架构的框架?该怎么选择

    flutter有哪些架构的框架? Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:BLoC (Business Logic Comp...
    继续阅读 »

    flutter有哪些架构的框架?


    Flutter是一种新兴的跨平台移动应用开发框架,它提供了丰富的UI组件和工具,使得应用开发更加容易。在Flutter中,有很多架构框架可供选择,以下是几个比较常用的架构框架:

    1. BLoC (Business Logic Component):BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。在Flutter中,可以使用flutter_bloc库来实现BLoC架构。 Provider:Provider是Flutter中的一个轻量级状态管理库,它使用InheritedWidget实现状态共享,可以有效地解决Flutter应用中的状态管理问题。
    2. MobX:MobX是一种基于响应式编程的状态管理库,它使用可观察对象来管理应用程序的状态,并自动更新与之相关的UI组件。在Flutter中,可以使用mobx库来实现MobX架构。
    3. Redux:Redux是一种流行的状态管理模式,在Flutter中也有相应的实现库redux_flutter。Redux通过单一数据源管理应用程序的状态,并使用纯函数来处理状态的更新,可以有效地解决Flutter应用中的状态管理问题。 以上是常用的Flutter架构框架,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。

    除了上面提到的框架之外,还有以下几个Flutter架构框架:

    1. GetX:GetX是一种轻量级的Flutter架构框架,它提供了路由管理、状态管理和依赖注入等功能,可以大大简化Flutter应用的开发。
    2. MVC:MVC是一种经典的软件架构模式,它将应用程序分为模型、视图和控制器三个部分,可以有效地分离关注点,使得应用程序更易于维护和扩展。
    3. MVP:MVP是一种衍生自MVC的架构模式,它将应用程序分为模型、视图和Presenter三个部分,Presenter负责处理业务逻辑,将模型数据展示到视图上。
    4. MVVM:MVVM是一种流行的架构模式,它将应用程序分为模型、视图和视图模型三个部分,视图模型负责处理业务逻辑,将模型数据展示到视图上。

    总之,Flutter中有很多架构框架可供选择,每个框架都有其优点和适用场景,开发者可以根据自己的需求选择合适的架构框架。


    Flutter BLoC


    Flutter BLoC是一种状态管理模式,它将应用程序中的业务逻辑和UI分离,使得应用程序更易于维护和测试。BLoC这个缩写代表 Business Logic Component,即业务逻辑组件。
    BLoC的核心思想是将UI层和业务逻辑层分离,通过Stream或者Sink等异步编程方式,将UI层和业务逻辑层连接起来。具体来说,BLoC模式包含以下三个部分:
    Events:事件,即UI层的用户操作或其他触发条件,例如按钮点击,网络请求完成等等。
    Bloc:业务逻辑层,用于处理Events,处理业务逻辑,并向UI层提供状态更新。
    State:状态,即UI层的显示状态,例如页面的loading状态,数据请求成功或失败状态等等。


    BLoC的核心是Bloc和State之间的联系,Bloc接收Events,并根据业务逻辑处理后,通过Stream向UI层提供状态更新。UI层监听Bloc的Stream,获取最新的State,并根据State更新UI状态。
    在Flutter中,可以使用StreamController来创建BLoC。StreamController是一个异步数据流控制器,可以创建一个Stream用于事件流,创建一个Sink用于事件的注入。
    Flutter框架提供了一个非常好的BLoC框架flutter_bloc,它封装了BLoC的核心逻辑,使得开发者可以更加方便地使用BLoC模式进行状态管理。使用flutter_bloc框架,只需要定义好Events、Bloc和State,然后将它们组合起来,就可以实现状态管理,从而将UI层和业务逻辑层分离。


    总之,Flutter BLoC是一种状态管理模式,它通过将UI层和业务逻辑层分离,使用Stream或Sink等异步编程方式将它们连接起来,实现了应用程序的业务逻辑和UI分离。


    如何选择使用


    选择使用哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。以下是一些常见的应用场景和推荐的架构框架:
    对于小型应用程序,可以考虑使用轻量级的状态管理库,如Provider或GetX。
    对于需要处理复杂业务逻辑的应用程序,可以使用BLoC、MobX或Redux等状态管理框架。
    对于需要实现高度可测试性的应用程序,可以考虑使用MVC、MVP或MVVM等经典的软件架构模式。
    总之,选择哪种Flutter架构框架取决于具体的应用程序需求和开发团队的技术水平。开发者应该根据自己的需求和技能水平选择最适合的架构框架,以提高开发效率和代码质量。


    GetX和BLoC的优缺点


    GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点,下面是它们的主要特点和优缺点的比较:


    GetX


    优点:


    简单易用:GetX是一种轻量级的Flutter框架,提供了简单易用的依赖注入、路由管理和状态管理等功能,可以大大简化Flutter应用的开发。
    性能优秀:GetX使用原生的Dart语言构建,不需要任何代码生成,因此运行速度非常快,同时也具有很好的内存管理和性能优化能力。
    功能完备:GetX提供了路由管理、依赖注入、状态管理、国际化、主题管理等功能,可以满足大多数应用程序的需求。


    缺点:


    社区相对较小:相比其他流行的Flutter框架,GetX的社区相对较小,相关文档和教程相对较少,需要一定的自学能力。
    不适合大型应用:由于GetX是一种轻量级框架,不适合处理大型应用程序的复杂业务逻辑和状态管理,需要使用其他更加强大的框架。


    BLoC


    优点:


    灵活可扩展:BLoC提供了灵活的状态管理和业务逻辑处理能力,可以适应各种应用程序的需求,同时也具有良好的扩展性。
    可测试性强:BLoC将UI和业务逻辑分离,提高了代码的可测试性,可以更容易地编写和运行测试代码。
    社区活跃:BLoC是一种流行的Flutter框架,拥有较大的社区和用户群体,相关文档和教程比较丰富,容易入手。


    缺点:


    学习曲线较陡峭:BLoC是一种相对复杂的框架,需要一定的学习曲线和编程经验,初学者可能需要花费较多的时间和精力。
    代码量较大:由于BLoC需要处理UI和业务逻辑的分离,因此需要编写更多的代码来实现相同的功能,可能会增加开发成本和维护难度。
    总之,GetX和BLoC都是常见的Flutter架构框架,它们各有优缺点。选择哪种框架取决于具体的应用程序需求和开发团队的技术水平。


    作者:某非著名程序员
    链接:https://juejin.cn/post/7230976073495527482
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    一个写了3年半flutter的小伙,突然写了2个月uniapp的感悟!

    前言 因为某些原因,在过去的三年半时间,我除了flutter之外,很少接触其他的框架,期间除了学习了Android(主要是Kotlin、jetpack)、GoLang Gin之外基本上很少接触其他的框架。而在最近的两个月,突然来了一个要求用uniapp实现的项...
    继续阅读 »

    前言


    因为某些原因,在过去的三年半时间,我除了flutter之外,很少接触其他的框架,期间除了学习了Android(主要是Kotlin、jetpack)、GoLang Gin之外基本上很少接触其他的框架。而在最近的两个月,突然来了一个要求用uniapp实现的项目,在接下这个前,我是有些抵触的。第一点是觉得自己短期内去学一个新的框架,学到的东西不足以完成整个项目,第二点是不想脱离舒适圈。当然,最后我还是选择了直面困难,不然您也看不到这篇文章了🤣。


    本文更多的是帮助您解决是否要学习uni-app或flutter框架的这一问题,以及两个框架的一些代码对比。如果您想要判断的是一个新项目该使用哪个框架,那么本文就不是很合适了~


    跨平台层面的对比感悟


    在Flutter刚出来的这几年,经常会在各种跨平台框架对比的文章下,看到将其与uni-app进行比较。当时我也没有在意太多,以为uni-app也是个差不多的“正经”跨平台框架,但当我打开uni-app官网的时候,我震惊了,因为我看到了这样一句话:一套代码编到15个平台,这不是梦想。我瞬间就傻眼了,这么nb?Flutter不也才横跨六大平台 ?在仔细一想,不对啊,这哪来的15个平台?再仔细一看,然后我的心中只剩下一万个省略号了,横跨一堆小程序平台是吧...



    学习成本的对比感悟


    1. 开发语言的不同

    Flutter,要求开发者学习dart,了解dart和flutter的API,最好还会写点原生。而uni-app只需要学Vue.js,没有附加专有技术。所以从学习一个框架来看,很明显uni-app的学习成本很低。而从我个人的角度去分析,当年我只是一个刚入编程世界的菜鸡中的菜鸡,只学了半年的html+css+js和半年的java。抛开学了1个月的SpringBoot,Flutter可以算是我学习的第一个框架,当时我是直接上手学的Flutter,没有去单独学习dart,因为和java很相似。个人觉得学习成本也还好,如果你喜欢这个框架的话~而最近两个月学习uni-app,我也确实是感受到了学习成本很低,基本上看了看文档,就直接上手了,很多组件的名字也是和flutter大差不差。就是写css有点难受🤣,好在flex布局和flutter的rowcolumn用法一样,基本上半小时就能把基本的、简单的页面布局写好了。


    2. 第三方插件&社区氛围

    截至目前2023.7,flutter在github上有155K的star,uni-app有着38.4K的star。从star的数量也可以看出一个框架的热度,很明显,flutter是远高于uni-app的(毕竟uni-app的主要使用场景还是在国内小程序中)。对于第三方插件呢Flutter有着pub.dev,uni-app有插件市场,但相比Flutter呢可能略显不足。


    3. 开发工具的使用

    Flutter可以选择vscode或者android studio等来进行开发,uni-app可以选择HBuilderX,当然也可以使用vscode,用什么开发工具其实大差不差,如果你一直使用vscode,那么你对工具的使用会更加的熟悉,而如果你和我一样,用的是android studio,再去使用HBuilderX,说实话,有点点难受...例如我最常用的Alt+回车(提示),crtl+alt+l(代码格式化)。当然,反过来也是一样的(●'◡'●)


    编码实现对比


    1. 布局区别


    • 代码整体结构:Flutter使用Widget层级嵌套来构建用户界面,也是被很多人所不喜欢的嵌套地狱(这一点因人而异,根据自己的习惯和代码风格)。 uni-app 使用 Vue.js 的组件化布局方式,templatestylescripttemplate 定义了组件的 HTML 结构,style 定义了组件的样式,script 定义了组件的行为。

    • 布局原理区别:Flutter 中的布局是基于约束的,可以使用Constraints来控制小部件的最大和最小尺寸,并根据父级小部件的约束来确定自身的尺寸。uni-app则是,可以使用类似于 CSS 中 Flex 弹性布局的方式来控制组件的排列和布局。通过设置组件的样式属性,如 display: flexflexjustify-content 等,可以实现垂直和水平方向上的灵活布局。当然flutter也有和flex差不多的rowcolumn

    • 自定义布局:Flutter支持自定义布局,可以通过继承 SingleChildLayoutDelegateMultiChildLayoutDelegate 来实现自定义布局,而uni-app目前并没有直接提供类似的专门用于自定义布局的机制,不过uni-app常见的做法是创建一个自定义组件,并在该组件的 template 中使用各种布局方式、样式和组件组合来实现特定的布局效果。


    2. 状态管理的区别

    Flutter 提供了内置的状态管理机制,最常见的就是通过setState来管理小部件的状态,uni-app是利用Vue.js的响应式数据绑定和状态管理,通过 data 属性来定义和管理组件的状态。


    3. 开发语言的区别与联系

    区别:众所周知,JavaScript 是一门弱类型的语言,而 Dart 是强类型的语言(dart也支持一些弱类型,Dart中弱类型有var, Object 以及dynamic)。Dart有类和接口的概念,并支持面向对象编程,如果你喜欢 OOP 概念,那么你会喜欢使用 Dart 进行开发,此外,它还支持接口、Mixin、抽象类和静态类型等,这一点对写过java的朋友很友好,而JavaScript则支持基于原型的面向对象编程。Dart和JavaScript还有一个重要的区别就是:Dart是类型安全的,使用AOT和JIT编译器编译。


    联系:从一个学习这个两个语言的角度去看, 两者都支持异步编程模型,如 Dart 的 async/await和 JavaScript 的 Promiseasync/await,这就非常友好了。


    4. 一个简单的计数器例子,更好的理解他们直接的区别以及相关的地方:


    Flutter代码:


    import 'package:flutter/material.dart';

    void main() {
    runApp(const MyApp());
    }

    class MyApp extends StatelessWidget {
    const MyApp({super.key});

    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    home: const MyHomePage(title: 'Flutter Demo Home Page'),
    )
    ;
    }
    }

    class MyHomePage extends StatefulWidget {
    const MyHomePage({super.key, required this.title});
    final String title;

    @override
    State<MyHomePage> createState() => _MyHomePageState();
    }

    class _MyHomePageState extends State {
    int _counter = 0;

    void _incrementCounter() {
    setState(() {
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    const Text(
    '
    You have pushed the button this many times:',
    )
    ,
    Text(
    '$_counter',
    style:
    Theme.of(context).textTheme.headlineMedium,
    ),
    ],
    ),
    ),
    floatingActionButton:
    FloatingActionButton(
    onPressed: _incrementCounter,
    tooltip: '
    Increment',
    child: const
    Icon(Icons.add),
    ),
    )
    ;
    }
    }

    uniapp代码:


    <template>
    <view class="container">
    <text class="count">{{ count }}text>
    <view class="buttons">
    <button class="btn" @tap="incrementCounter">+button>
    view>
    view>
    template>

    <script>
    export default {
    data() {
    return {
    count: 0,
    };
    },
    methods: {
    incrementCounter() {
    this.count++;
    },
    },
    };
    script>

    <style>
    .container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-between;
    height: 100vh;
    background-color: #f0f0f0;
    }

    .count {
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 48px;
    font-weight: bold;
    height: 100%;
    }

    .buttons {
    display: flex;
    width: 100vw;
    flex-direction: row;
    justify-content: flex-end;
    }

    .btn {
    width: 108rpx;
    height: 108rpx;
    font-size: 24px;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 8px;
    background-color: #2196F3;
    color: #fff;
    border-radius: 50%;
    }
    style>

    总结


    从App开发的角度来看,uni-app的最大价值在于让国内庞大的Vue开发群体也能够轻松地开发“高性能”的App,不用去承担flutter或react native的学习成本,短时间内开发一款简单的偏展示类的app的话,uni-app肯定是首选,小公司应该挺受益的。再加上uni-app可以同时开发多端小程序,就足以保证在国内有足够的市场。但是稍微有点动效或者说有video、map之类的app,那么要慎重考虑,个人觉得挺限制的。不过很多时候技术并不是一个项目选型第一标准,适合才是,uni-app很适合国内,毕竟试错成本低...


    注:本文仅为一个写了几年flutter小伙,突然写了2个月uniapp的感悟,存在一定个人主观,有错误欢迎指出😘

    作者:编程的平行世界
    来源:juejin.cn/post/7261162911615926331

    收起阅读 »

    像支付宝那样“致敬”第三方开源代码

    前言 通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源...
    继续阅读 »

    前言


    通常我们在App中会使用第三方的开源代码,按照许可协议,我们应该在App中公开使用的开源代码并且附上对应的开源协议。当然,实际上只有少部分注重合规性的大厂才会这么干,比如下图是支付宝的关于界面的第三方信息。当然,对于小企业,基本上都不会放使用的第三方开源代码的任何信息。 



    不过,作为一个有“追求”的码农,我们还是想对开源软件致敬一下的,毕竟,没有他们我都不知道怎么写代码。然而,我们的 App 里用了那么多第三方开源插件,总不能一个个找出来一一致敬吧?怎么办?其实,Flutter 早就为我们准备好了一个组件,那就是本篇要介绍的 AboutDialog


    AboutDialog 简介


    AboutDialog 是一个对话框,它可以提供 App 的基本信息,如 Icon、版本、App 名称、版权信息等。 



    同时,AboutDialog还提供了一个查看授权信息(View Licenses)的按钮,点击就可以查看 App 里所有用到的第三方开源插件,并且会自动收集他们的 License 信息展示。所以,使用 AboutDialog 可以让我们轻松表达敬意。怎么使用呢?非常简单,我们点击一个按钮的时候,调用 showAboutDialog 就搞定了,比如下面的代码:

    IconButton(
    onPressed: () {
    showAboutDialog(
    context: context,
    applicationName: '岛上码农',
    applicationVersion: '1.0.0',
    applicationIcon: Image.asset('images/logo.png'),
    applicationLegalese: '2023 岛上码农版权所有'
    );
    },
    icon: const Icon(
    Icons.info_outline,
    color: Colors.white,
    ),
    ),

    参数其实一目了然,具体如下:

    • context:当前的 context
    • applicationName:应用名称;
    • applicationVersion:应用版本,如果要自动获取版本号也可以使用 package_info_plus 插件。
    • applicationIcon:应用图标,可以是任意的 Widget,通常会是一个App 图标图片。
    • applicationLegalese:其他信息,通常会放置应用的版权信息。

    点击按钮,就可以看到相应的授权信息了,点击一项就可以查看具体的 License。我看了一下使用的开源插件非常多,要是自己处理还真的很麻烦。 



    可以说非常简单,当然,如果你直接运行还有两个小问题。


    按钮本地化


    AboutDialog 默认提供了两个按钮,一个是查看授权信息,一个是关闭,可是两个按钮 的标题默认是英文的(分别是VIEW LICENSES和 CLOSE)。 



    如果要改成本地话的,还需要做一个自定义配置。我们扒一下 AboutDialog 的源码,会发现两个按钮在DefaultMaterialLocalizations中定义,分别是viewLicensesButtonLabelcloseButtonLabel。这个时候我们自定义一个类集成DefaultMaterialLocalizations就可以了。

    class MyMaterialLocalizationsDelegate
    extends LocalizationsDelegate<MaterialLocalizations> {
    const MyMaterialLocalizationsDelegate();

    @override
    bool isSupported(Locale locale) => true;

    @override
    Future<MaterialLocalizations> load(Locale locale) async {
    final myTranslations = MyMaterialLocalizations(); // 自定义的本地化资源类
    return Future.value(myTranslations);
    }

    @override
    bool shouldReload(
    covariant LocalizationsDelegate<MaterialLocalizations> old) =>
    false;
    }

    class MyMaterialLocalizations extends DefaultMaterialLocalizations {
    @override
    String get viewLicensesButtonLabel => '查看版权信息';

    @override
    String get closeButtonLabel => '关闭';

    }

    然后在 MaterialApp 里指定本地化localizationsDelegates参数使用自定义的委托类对象就能完成AboutDialog两个按钮文字的替换。

    return MaterialApp(
    debugShowCheckedModeBanner: false,
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    home: const AboutDialogDemo(),
    localizationsDelegates: const [MyMaterialLocalizationsDelegate()],
    );

    添加自定义的授权信息


    虽然 Flutter 会自动收集第三方插件,但是如果我们自己使用了其他第三方的插件的话,比如没有在 pub.yaml 里引入,而是直接使用了源码。那么还是需要手动添加一些授权信息的,这个时候我们需要自己手动添加了。添加的方式也不麻烦,Flutter 提供了一个LicenseRegistry的工具类,可以调用其 addLicense 方法来帮我们添加授权信息。具体使用如下:

    LicenseRegistry.addLicense(() async* {
    yield const LicenseEntryWithLineBreaks(
    ['关于岛上码农'],
    '我是岛上码农,微信公众号同名。\f如有问题可以加本人微信交流,微信号:island-coder。',
    );
    });

    这个方法可以在main方法里调用。其中第一个参数是一个数组,是因为可以允许多个开源代码共用一份授权信息。同时,如果一份开源插件有多个授权信息,可以多次添加,只要名称一致,Flutter就会自动合并,并且会显示该插件的授权信息条数,点击查看时,会将多条授权信息使用分割线分开,代码如下所示:

    void main() {
    runApp(const MyApp());
    LicenseRegistry.addLicense(() async* {
    yield const LicenseEntryWithLineBreaks(
    ['关于岛上码农'],
    '我是岛上码农,微信公众号同名。如有问题可以加本人微信交流,微信号:island-coder。',
    );
    });

    LicenseRegistry.addLicense(() async* {
    yield const LicenseEntryWithLineBreaks(
    ['关于岛上码农'],
    '使用时请注明来自岛上码农、。',
    );
    });
    }



    总结


    本篇介绍了在 Flutter 中快速展示授权信息的方法,通过 AboutDialog 就可以轻松搞定,各位“抄代码”的码农们,赶紧用起来向大牛们致敬吧!


    作者:岛上码农
    链接:https://juejin.cn/post/7246328828837871677
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如何优化 electron 应用在低配电脑秒启动

    背景 古茗门店使用的收银机,有些会因为使用年限长、装了杀毒软件、配置低等原因性能较差,导致进钱宝启动响应较慢。然后店员在双击进钱宝图标后,发现没反应,就会重复点击 因此我们希望优化到即使在这些性能不太好的收银机上,也能让进钱宝有较快的启动体验 优化思路 测...
    继续阅读 »


    背景


    古茗门店使用的收银机,有些会因为使用年限长、装了杀毒软件、配置低等原因性能较差,导致进钱宝启动响应较慢。然后店员在双击进钱宝图标后,发现没反应,就会重复点击


    因此我们希望优化到即使在这些性能不太好的收银机上,也能让进钱宝有较快的启动体验
    lAHPKHtEUt3mDUzM8Mzw_240_240.gif


    优化思路



    • 测量,得到一个大概的优化目标,并发现可优化的阶段

    • 主要方向是优化主进程创建出窗口的时间、让渲染进程页面尽快显示

    • 性能优化好后,尽量让人感觉上更快点

    • 上报各阶段耗时,建立监控机制,发现变慢了及时优化


    测量


    测量主进程


    编写一个 bat文件 放到应用根目录,通过bat启动程序并获取初始启动时间:


    @echo off

    set "$=%temp%\Spring"
    >%$% Echo WScript.Echo((new Date()).getTime())
    for /f %%a in ('cscript -nologo -e:jscript %$%') do set timestamp=%%a
    del /f /q %$%
    echo %timestamp%
    start yourAppName.exe

    pause

    项目内可以使用如下api打印主进程各时间节点:


    this.window.webContents.executeJavaScript(
    `console.log('start', ${start});console.log('onReady', ${onReady});console.log('inCreateWindow', ${inCreateWindow});console.log('afterCreateWindow', ${afterCreateWindow});console.log('beforeInitEvents', ${beforeInitEvents});console.log('afterInitEvents', ${afterInitEvents});console.log('startLoad', ${startLoad});`
    );

    如果发现主进程有不正常的耗时,可以通过v8-inspect-profiler捕获主进程执行情况,最终生成的文件可以放到浏览器调试工具中生成火焰图


    测量渲染进程


    1、可以console打印时间点,可以借助preformance API获取一些时间节点


    2、可以使用preformance工具测白屏时间等


    image.png


    进钱宝测量结果


    以下测量结果中每一项都是时间戳,括号里是距离上一步的时间(ms)


    最简单状态(主进程只保留唤起主渲染进程窗口的逻辑):


    执行exe(指双击应用图标)开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
    16776661416191677666142152(+533)1677666142224(+72)1677666142364(+140)1677666142375(+11)

    未优化状态:


    执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
    16776694148861677669417742(+2856)1677669417856(+114)1677669418043(+187)1677669418061(+18)

    通过上述数据,能看出主进程最大的卡点是执行exe到开始执行代码之间


    渲染进程的白屏时间,最初测试大概是1000ms


    那么我们的优化目标,就是往最简单应用的时间靠齐,优化重点就是主进程开始执行代码时间,和渲染进程白屏时间


    优化步骤


    一、让主进程代码尽快执行


    使用常见的方式,打包、压缩、支持tree-shaking,让代码体积尽可能的小;


    可以把一些依赖按需加载,减少初始包体积


    代码压缩


    使用electron的一个好处是:chrome版本较高,不用pollyfill,可以直接使用很新的es特性


    直接编译目标 ecma2020!!


    优化tree-shaking


    主进程存在global对象,但一些配置性的变量尽量不要挂载在global上,可以放到编译时配置里,以支持更好的tree-shaking


    const exendsGlobal = {
    __DEV__,
    __APP_DIR__,
    __RELEASE__,
    __TEST__,
    __LOCAL__,
    __CONFIG_FILE__,
    __LOG_DRI__,
    GM_BUILD_ENV: JSON.stringify(process.env.GM_BUILD_ENV),
    };

    // 这里把一些变量挂载在global上,这样不利于tree-shaking
    Object.assign(global, exendsGlobal);

    慎用注册快捷方式API


    实测这样的调用是存在性能损耗的


    globalShortcut.register('CommandOrControl+I', () => {
    this.window.webContents.openDevTools();
    });
    // 这个触发方式,我们改为了在页面某个地方连点三下,因为事件监听基本没性能损耗
    // 或者把快捷方式的注册在应用的生命周期中往后移,尽量不影响应用的启动

    优化require


    因为require在node里是一个耗时操作,而主进程最终是打包成一个cjs格式,里面难免有require


    可以使用 node --cpu-prof --heap-prof -e "require('request')" 获取一个包的引用时长。
    如下是一些在我本机的测量结果:


    时长(ms)
    fs-extra83
    event-kit25
    electron-store197
    electron-log61
    v8-compile-cache29

    具体理论分析可以看这里:
    如何加快 Node.js 应用的启动速度


    因此我们可以通过一些方式优化require



    • 把require的包打进bundle

      • 有两个问题

        • bundle体积会增加,这样还是会影响代码编译和加载时间

        • 有些库是必须require的,像node和electron的原生api;就进钱宝来说,我们可以通过其他方式优化掉require,因此没使用这种方式





    • 按需require

    • v8 code cache / v8 snapshot

    • 对应用流程做优化,通过减少启动时的事务,来间接减少启动时的require量


    按需require


    比如fx-extra模块的按需加载方式:


    const noop = () => {};

    const proxyFsExtra = new Proxy(
    {},
    {
    get(target, property) {
    return new Proxy(noop, {
    apply(target, ctx, args) {
    const fsEx = require('fs-extra');
    return fsEx[property](...args);
    },
    });
    },
    }
    );

    export default proxyFsExtra;

    前面的步骤总是做了没坏处,但这个步骤因为要重构代码,因此要经过验证


    因此我们测量一下:


    执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
    16776740873441677674089485(+2141)167767408960616776740898641677674089934

    可以看出,主进程开始执行时间已经有了较大优化(大概700ms)


    v8-compile-cache


    可以直接用 v8-compile-cache 这个包做require缓存


    简单测试如下:


    image.png


    脚本执行时间从388到244,因此这个技术确实是能优化执行时间的


    但也有可能没有优化效果:


    image.png


    在总require较少,且包总量不大的情况下,做cache是没有用的。实测对进钱宝也是没用的,因为经过后面的流程优化步骤,进钱宝代码的初始require会很少。因此我们没有使用这项技术


    但我们还是可以看下这个包的优化机制,这个包核心代码如下,其实是重写了node的Module模块的_compile函数,编译后把V8字节码缓存,以后要执行时直接使用缓存的字节码省去编译步骤


    Module.prototype._compile = function(content, filename) {
    ...

    // 读取编译缓存
    var buffer = this._cacheStore.get(filename, invalidationKey);

    // 这一步是去编译代码,但如果传入的cachedData有值,就会直接使用,从而跳过编译
    // 如果没传入cachedData,这段代码就会产生一份script.cachedData
    var script = new vm.Script(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true,
    cachedData: buffer,
    produceCachedData: true,
    });

    // 上面的代码会产生一份编译结果,把编译结果写入本地文件
    if (script.cachedDataProduced) {
    this._cacheStore.set(filename, invalidationKey, script.cachedData);
    }

    // 运行代码
    var compiledWrapper = script.runInThisContext({
    filename: filename,
    lineOffset: 0,
    columnOffset: 0,
    displayErrors: true,
    });

    ...
    };

    这里有个可能的优化点:v8-compile-cache 只是缓存编译结果,但require一个模块除了编译,还有加载这个io操作,因此是否可以考虑连io一起缓存


    v8-snapshot


    image.png


    原理是:把代码执行结果的内存,做一个序列化,存到本地,真正执行时,直接加载然后反序列化到内存中


    这样跳过了代码编译和执行两个阶段,因此可以提升应用的初始化速度。


    优化效果:


    image.png


    对react做快照后,代码中获取的react对象如下图,实际上获得的是一份react库代码执行后的内存快照,跟正常引入react库没什么区别:


    image.png


    这个方案看起来很香,但也存在两个小问题:


    1、不能对有副作用的代码做snapshot


    因为只是覆写内存,而没有实际代码执行,因此如果有 读写文件、操作dom、console 等副作用,是不会生效的


    因此这个步骤更多是针对第三方库,而不是业务代码


    2、需要修改打包配置


    目前项目一般通过import引用各种包,最终把这些包打包到bundle中;但该方案会在内存直接生成对象,并挂载在全局变量上,因此要使用snapshot,代码中包引用方式需要修改,这个可以通过对编译过程的配置实现


    这个技术看起来确实能有优化效果,但考虑如下几点,最后我们没有去使用这项技术:



    • 对主进程没用,因为主进程刚进来就是要做打开窗口这个副作用;

    • 对渲染进程性价比不高,因为

      • 我们的页面渲染已经够快(0.2s)

      • 启动时,最大的瓶颈不在前端,而在服务端初始化,前端会长时间停留在launch页面等待服务端初始化,基于这一点,对渲染进程js初始化速度做极限优化带来的收益基本没有,我们真实需要的是让渲染进程能尽快渲染出来一些可见的东西让用户感知

      • 维护一个新模块、修改编译步骤、引入新模块带来的潜在风险




    snapshot具体应用方式可看文尾参考文章


    二、优化主进程流程,让应该先做的事先做,可以后做的往后放


    D2E73602-B81D-4b87-8929-427AB6C51C2A.png
    基于上图的思想,我们对bundle包做了拆分:


    image.png


    新的测量数据:


    执行exe开始执行主进程代码主进程ready事件开始初始化渲染进程窗口开始加载渲染进程资源
    16779113945161677911395044(+528)1677911395133(+89)--

    可以看出,到这里主进程已经跟最简单状态差不多了。而且这一步明显优化非常明显。而这一步做的事情核心就是减少初始事务,从而减少了初始代码量以减少编译和加载负担,也避免了初始时过多比较耗性能的API的执行(比如require,比如new BrowserWindow())。


    那么我们主进程优化基本已经达到目标


    三、让渲染进程尽快渲染


    requestIdleCallback


    程序刚启动的时候,CPU占用会很高(100%),因此有些启动任务可以通过requestIdleCallback,在浏览器空闲时间执行,让浏览器优先去渲染


    去掉或改造起始时调用sendSync以及使用electron-store的代码


    原因是sendSync是同步执行,会阻塞渲染进程


    而electron-store里面初始时会调用sendSync


    只加载首屏需要的css


    对首屏不需要的ui库、components做按需加载,以减少初始css量,首屏尽量只加载首屏渲染所需的css



    因为css量会影响页面的渲染性能


    使用 tailwind 的同学可能会发现一个现象:如果直接加载所有预置css,页面动画会非常卡,因此 tailwind 会提供 Purge 功能自动移除未使用的css



    少用或去掉modulepreload


    我们使用的是vite,他会自动给一些js做modulepreload。但实测modulepreload(不是preload)是会拖慢首屏渲染的,用到的同学可以测测看


    四、想办法让应用在体验上更快


    使用骨架屏提升用户体感


    程序开始执行 -> 页面开始渲染, 这段时间内可以使用骨架屏让用户感知到应用在启动,而不是啥都没有


    我们这边用c++写了个只有loading界面的exe,在进钱宝启动时首先去唤起这个exe,等渲染进程渲染了,再关掉他(我们首屏就是一个很简单的页面,背景接近下图的纯色,因此loading界面也做的比较简单)


    动画.gif


    渲染进程骨架屏


    渲染进程渲染过程:加载解析html -> 加载并执行js渲染


    在js最终执行渲染前,就是白屏时间,可以在html中预先写一点简单的dom来减少白屏时间


    一个白屏优化黑科技


    我们先看两种渲染效果:


    渲染较快的

    image.png


    image.png


    渲染较慢的

    image.png


    image.png


    接下来看下代码区别:


    快的代码:
    <div id="root">
    <span style="color: #000;">哈哈</span> <!-- 就比下面那个多了这行代码 -->
    <div class="container">
    <div class="loading">
    <span></span>
    </div>
    </div>
    </div>

    慢的代码:
    <div id="root">

    <div class="container">
    <div class="loading">
    <span></span>
    </div>
    </div>
    </div>

    就是多了一行文字,就会更快地渲染出来


    从下图可以看到,文字渲染出来的同时,背景色和loading动画(就中间那几个白点)也渲染出来了


    image.png


    有兴趣的可以测一下淘宝首页,如果去掉所有文字,还是会较快渲染,但如果再去掉加载的css中的一个background: url(.....jpg),首次渲染就会变慢了


    我猜啊。。。 这个叫信息优先渲染原则。。。🐶就是文字图片可以明确传递信息,纯dom不知道是否传递信息,而如果页面里有明确能传递信息的东西,就尽快渲染出来,否则,渲染任务就可能排到其他初始化任务后面了。


    当然了,这只是我根据测试结果反推出来的猜测🐶


    好了,现在我们也可以让渲染进程较快的渲染了(至少能先渲染出来一个骨架屏🤣)


    五、其他


    升级electron版本


    electron 官方也是在不断优化bug和性能的


    保证后续的持续优化


    因为经过后续的维护,比如有人给初始代码加了些不该加的重量,是有可能导致性能下降的


    因此我们可以对各节点的数据做上报,数据大盘,异常告警,并及时做优化,从而能持续保证性能


    总结


    本文介绍了electron应用的优化思路和常见的优化方案。并在进钱宝上取得了实际效果,我们在一台性能不太好的机器上,把感官上的启动时间从10s优化到了1s(可能有人会提个问题,上面列的时间加起来没有10s,为啥说是10s。原因是我们最初是在渲染进程的did-finish-load事件后才显示窗口的,这个时间点是比较晚的)


    这其中最有效的步骤是优化流程,让应该先做的事先做,可以往后的就往后排,根据这个原则进行拆包,可以使得初始代码尽可能的简单(体积小,require少,也能减少一些耗性能的动作)。


    另外有些网上看起来很秀的东西,不一定对我们的应用有用,是要经过实际测量和分析的,比如code-cache 和 snapshot


    还有个点是,如果想进一步提升体验,可以先启动骨架屏应用,再通过骨架屏应用启动进钱宝本身,这样可以做到ms级启动体验,但这样会使骨架屏显示时间更长点(这种体验也不好),也需要考虑win7系统会不会有dll缺失等兼容问题


    最后


    关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~


    参考文档


    v8 code cache


    v8.dev/blog/improv…

    v8.dev/blog/code-c…

    fed.taobao.org/blog/taofed…

    blog.csdn.net/szengtal/ar…


    v8 snapshot


    http://www.javascriptcn.com/post/5eedbc…

    blog.inkdrop.app/how-to-make…

    github.com/inkdropapp/…


    其他


    zhuanlan.zhihu.com/p/420238372


    blog.csdn.net/qq_37939251…


    medium.com/@felixriese…


    zhuanlan.zhihu.com/p/376

    638202

    收起阅读 »

    算法基础:归并排序

    上一篇文章介绍了什么是分治思想,今天就来看一下它其中一个继承人-- 归并排序,本章主要介绍归并排序的原理,以及对一个实际问题进行编码。 学习的内容 1. 什么是归并排序 比如我们拿到一个数组,如果想使用归并排序,应该怎么做呢?首先我们将数组从中间切分,分成左...
    继续阅读 »

    上一篇文章介绍了什么是分治思想,今天就来看一下它其中一个继承人-- 归并排序,本章主要介绍归并排序的原理,以及对一个实际问题进行编码。


    学习的内容




    1. 什么是归并排序


    比如我们拿到一个数组,如果想使用归并排序,应该怎么做呢?首先我们将数组从中间切分,分成左右两个部分,然后对左半部分和右半部分进行排序,两边部分又可以继续拆分,直至子数组中只剩下一个数据位置。


    然后就要将拆分的子数组进行合并,合并的时候会涉及到两个数据进行比较,然后按照大小进行排序,以此往上进行合并。


    拆分过程


    image.png
    合并过程


    image.png


    从上面我们可以看出,我们最终将大的数组拆分成只有单个数据的数组,然后进行合并,在合并过程中比较两个长度为1的数组,进行排序合并成新的子数组,然后依次类推,直至全部排序完成,也就意味着原数组排序完成。


    2.代码示例


    public class Solution {
       public static void main(String[] args) {
           int[] arr = {1,4,3,2,11};
           sortArray(arr);
           System.out.println(arr);
      }

       public static int[] sortArray(int[] nums) {
           quickSort(nums, 0, nums.length - 1);
           return nums;
      }

       private static void quickSort(int[] nums, int left, int right) {
           if (left >= right) {
               return;
          }
           int partitionIndex = getPartitionIndex(nums, left, right);
           quickSort(nums, left, partitionIndex - 1);
           quickSort(nums, partitionIndex + 1, right);
      }

       private static int getPartitionIndex(int[] nums, int left, int right) {
           int pivot = left;
           int index = pivot + 1;
           for (int i = index; i <= right; i++) {
               if (nums[i] < nums[pivot]) {
                   swap(nums, i, index);
                   index++;
              }
          }
           swap(nums, pivot, index - 1);
           return index - 1;
      }

       private static void swap(int[] nums, int i, int j) {
           int temp = nums[i];
           nums[i] = nums[j];
           nums[j] = temp;
      }
    }




    总结


    本章简单分析了归并排序的原理以及分享了一个实际案例,无论是归并还是归并算法,对理解递归还是很有帮助的,之前总是靠着想递归流程,复杂点的绕着绕着就晕了,后面会再看一下快速排序,他和本文提到的归并排序都是分治思想,等说完快排,再

    作者:花哥编程
    来源:juejin.cn/post/7250404077712048165
    一起对比两者的区别。

    收起阅读 »

    uniapp开发项目——问题总结

    前言 之前使用过uniapp开发微信小程序,但是没有遇到需要兼容H5页面的。因此在使用uniapp开发微信小程序和H5的过程中,遇到了好些问题。 1. button按钮存在黑色边框 使用button标签,在手机上查看存在黑色的边框,设置了border: non...
    继续阅读 »

    前言


    之前使用过uniapp开发微信小程序,但是没有遇到需要兼容H5页面的。因此在使用uniapp开发微信小程序和H5的过程中,遇到了好些问题。


    1. button按钮存在黑色边框


    使用button标签,在手机上查看存在黑色的边框,设置了border: none;也没有效果。


    原因:uniapp的button按钮使用了伪元素实现边框


    解决方法: 设置button标签的伪元素为display:none或者boder:none;


    button:after{
    boder:none;
    }

    2. 配置反向代理,处理跨域


    微信小程序没有跨域问题,如果当前小程序还没有配置服务器域名出现无法请求接口,只需要在微信开发工具勾选不校验合法域名,就可以请求到了


    在本地开发环境中,H5页面在浏览器中调试,会出现跨域问题。如果后端不处理,前端就需要配置反向代理,处理跨域


    a. 在manifest.json的源码视图中,找到h5的配置位置,配置proxy代理


    image.png


    注: "pathRewrite"是必要的,告诉连接要使用代理


    b.在请求接口中使用


    // '/api'就是manifest.json文件配置的devServer中的proxy
    uni.request({
    url: '/api'+ '接口url',
    ...
    })

    c. 配置完,需要重启项目


    3. 使用uni.uploadFile()API,上传图片文件


    在微信小程序使用该API上传图片没问题,但是在H5页面实现图片上传,后台始终不能获取到上传的文件。


    一开始使用uni.chooseImage()API实现从本地相册选择图片或使用相机拍照,成功之后可以返回图片的本地文件路径列表(tempFilePaths)和图片的本地文件列表(tempFiles,每一项是一个 File 对象)


    tempFilePaths 在微信小程序中得到临时路径图片,而在浏览器中得到 blob 路径图片。微信小程序使用uni.uploadFile()上传该临时路径图片,可以成功上传,但是H5无法成功(浏览器中的传值方式会显示为payload,不是文件流file)


    image.png


    f994e37fce7a5d62763f1c015b9553f.png



    可能原因:



    1. 使用 uni.uploadFile() 上传 blob 文件给服务端,后端无法获取到后缀名,进而上传失败。


    b. uni.uploadFile()上传的文件格式不正确



    解决方法:


    在H5中上传tempFiles文件,而不是tempFilePaths,并更改uni.uploadFile()上传的格式


    H5


    image.png


    微信小程序


    image.png


    4. 打包H5


    问题:打包出来,部署到线上,页面空白,控制台preview中展示please enable javascript tocontinue


    原因:uniapp的打包配置存在问题


    解决方法:


    a. web配置不选择路由模式、运行的基础路径也不填写(一开始都写了)


    image.png


    b. "pathRewrite"设置为空(不知道为啥,可能是不需要配置代理了,网站和接口是同一域名)


    "proxy" : {
    "/api" : {
    "target" : "xxx",
    "changeOrigin" : true,
    "secure" : true,
    "pathRewrite" : {}
    }
    }



    注: 之前接口中的'/api'也需要取消


    作者:sherlockkid7
    来源:juejin.cn/post/7250284959221809209

    收起阅读 »

    某外包面试官:你还不会uniapp?😲😲

    uniapp主要文件夹 pages.json 配置文件,全局页面路径配置,应用的状态栏、导航条、标题、窗口背景色设置等 main.js 入口文件,主要作用是初始化vue实例、定义全局组件、使用需要的插件如 vuex,注意uniapp无法使用vue-router...
    继续阅读 »

    uniapp主要文件夹


    pages.json


    配置文件,全局页面路径配置,应用的状态栏、导航条、标题、窗口背景色设置等


    main.js


    入口文件,主要作用是初始化vue实例、定义全局组件、使用需要的插件如 vuex,注意uniapp无法使用vue-router,路由须在pages.json中进行配置。如果开发者坚持使用vue-router,可以在插件市场找到转换插件。


    App.vue


    是uni-app的主组件,所有页面都是在App.vue下进行切换的,是页面入口文件。但App.vue本身不是页面,这里不能编写视图元素。除此之外,应用生命周期仅可在App.vue中监听,在页面监听无效。


    pages


    页面管理部分用于存放页面或者组件


    manifest.json


    文件是应用的配置文件,用于指定应用的名称、图标、权限等。HBuilderX 创建的工程此文件在根目录,CLI 创建的工程此文件在 src 目录。


    package.json


    配置扩展,详情内容请见官网描述package.json概述


    uni-app属性的绑定


    vue和uni-app动态绑定一个变量的值为元素的某个属性的时候,会在属性前面加上冒号":";


    uni-app中的本地数据存储和接收


    // 存储:
    uni.setStorage({key:“属性名”,data:“值”}) //异步
    ni.setStorageSync(KEY,DATA) //同步
    //接收:
    ni.getStorage({key:“属性名”,success(res){res.data}}) //异步
    uni.getStorageSync(KEY) //同步
    //移除:
    uni.removeStorage(OBJECT) //从本地缓存中异步移除指定 key。
    uni.removeStorageSync(KEY) //从本地缓存中同步移除指定 key。
    //清除:
    uni.clearStorage() //清理本地数据缓存。
    ni.clearStorageSync() //同步清理本地数据缓存。

    页面调用接口



    • getApp() 函数 用于获取当前应用实例,一般用于获取globalData

    • getCurrentPages() 函数 用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。

    • uni.emit(eventName,OBJECT) uni.emit(eventName,OBJECT)uni.emit(eventName,OBJECT) uni.on(eventName,callback) :触发和监听全局的自定义事件

    • uni.once(eventName,callback):监听全局的自定义事件。uni.once(eventName,callback):监听全局的自定义事件。

    • 事件可以由 uni.once(eventName,callback):监听全局的自定义事件。

    • 事件可以由uni.emit 触发,但是只触发一次,在第一次触发之后移除监听器。

    • uni.$off([eventName, callback]):移除全局自定义事件监听器。


    uni-app的生命周期


      beforeCreate(创建前)
    created(创建后)
    beforeMount(载入前,挂载)
    mounted(载入后)
    beforeUpdate(更新前)
    updated(更新后)
    beforeDestroy(销毁前)
    destroyed(销毁后)

    路由与页面跳转



    1. uni.navigateTo 不关闭当前页的情况下跳转其他页面

    2. uni.redirectTo 关闭当前页的情况下跳转其他页面

    3. uni.switchTab 跳转去tabBar,关闭其他非tabBar页面

    4. uni.reLaunch 关闭所有页面,跳转到其他页面

    5. uni.navigateBack 返回

    6. edxit 退出app


    跨端适配—条件编译


    1. #ifdef APP-PLUS
    需条件编译的代码 //app
    #endif
    2. #ifndef H5
    需条件编译的代码 //H5
    endif
    3. #ifdef H5 || MP-WEIXIN
    需条件编译的代码 //小程序
    #endif

    uniapp上传文件时使用的api


    uni.uploadFile({
    url: '要上传的地址',
    fileType:'image',
    filePath:'图片路径',
    name:'文件对应的key',
    success: function(res){
    console.log(res)
    },})

    uniapp选择文件、图片上传


    选择文件


    uni.chooseFile({
    count: 6, //默认100
    extension:['.zip','.doc'],
    success: function (res) {
    console.log(JSON.stringify(res.tempFilePaths));
    }
    });

    选择图片文件


    uni.chooseFile({
    count: 10,
    type: 'image',
    success (res) {
    // tempFilePath可以作为img标签的src属性显示图片
    const tempFilePaths = res.tempFiles
    }
    })

    uni-app的页面传参方式


    第一种:
    直接在跳转页面的URL路径后面拼接,如果是数组或者json格式记得转成字符串格式哦。然后再目的页面onload里面接受即可


    //现页面
    uni.navigateTo({
    url:'/pages/notice/notice?id=1'
    })
    //目的页面接收
    //这里用onshow()也可以
    onLoad(options) {
    var data = options.id;
    console.log(data)
    }

    第二种:
    直接在main.js注册全局变量



    • 例如我用的是vue框架,先在main.js文件注册变量myName

    • Vue.prototype.myName= '玛卡巴卡';

    • 在目标文件读取全局变量,注意全局变量不要与我们在当前页声明的变量名重复

    • let name = this.myName; // 玛卡巴卡


    第三种:设置本地存储也比较方便



    • 这里建议使用uni.setStorageSync这个是同步,不会出现去了目标页面取值取不到的问题

    • uni.setStorage是异步存值,获取值也是一样建议使用uni.getStorageSync


    uniapp实现下拉刷新


    实现下拉刷新需要用到uni.onPullDownRefresh和uni.stopPullDownRefresh这个两个函数,函数与生命周期同等级可以监听页面下拉动作


    uniapp实现上拉加载


    uniapp中的上拉加载是通过onReachBottom()这个生命周期函数实现,当下拉触底时就会触发。我们可以在此函数内调用分页接口请求数据,用以获取更多的数据


    scroll-view吸顶问题



    • 问题:
      scroll-view 是常会用到的一个标签,我们可以使用 position:sticky 加一个边界条件例如top:0
      属性实现一个粘性布局,在容器滚动的时候,如果我们的顶部标签栏触碰到了顶部就不会再滚动了,而是固定在顶部。但是在小程序中如果你在scroll-view元素中直接为子元素使用sticky属性,你给予sticky的元素在到达父元素的底部时会失效。

    • 解决:
      在scroll-view元素中,再增加一层view元素,然后在再将使用了sticky属性的子元素放入view中,就可以实现粘贴在某个位置的效果了


    ios输入框字体移动bug



    • 问题:在IOS端有时,当输入框在输入后没有点击其他位置使输入框失焦的话,如果滚动窗口内部的字体也会跟着滚动

    • 解决:



    1. 尝试了下,发现textarea不会和input一样出现字体随着页面滚动的情况,这是一个兼容方案

    2. 还有个不优雅的方案是输入完成后使用其他事件让其失焦或者disable,例如弹窗或者弹出层出来的时候可以暂时让input禁止,然后弹窗交互完成后再放开


    rpx、px、em、rem、%、vh、vw的区别是什么?



    • rpx 相当于把屏幕宽度分为750份,1份就是1rpx

    • px 绝对单位,页面按精确像素展示

    • em 相对单位,相对于它的父节点字体进行计算

    • rem 相对单位,相对根节点html的字体大小来计算

    • % 一般来说就是相对于父元素

    • vh 视窗高度,1vh等于视窗高度的1%

    • vw 视窗宽度,1vw等于视窗宽度的1%


    uni-app的优缺点



    • 优点:



    1. 一套代码可以生成多端

    2. 学习成本低,语法是vue的,组件是小程序的

    3. 拓展能力强

    4. 使用HBuilderX开发,支持vue语法

    5. 突破了系统对H5条用原生能力的限制



    • 缺点:



    1. 问世时间短,很多地方不完善

    2. 社区不大

    3. 官方对问题的反馈不及时

    4. 在Android平台上比微信小程序和iOS差

    5. 文件命
      作者:margin_100px
      来源:juejin.cn/post/7245936314851622970
      名受限

    收起阅读 »

    uniapp 手机号码一键登录保姆级教程

    背景 通过uniapp来开发App,目前内部上架的App产品现有的登录方式有「账号/密码」 和 「手机号/验证码」两种登录方式;但这两种方式还是不够便捷,目前「手机号一键登录」是替代短信验证登录的下一代登录验证方式,能消除现有短信验证模式等待时间长、操作繁琐和...
    继续阅读 »

    背景


    通过uniapp来开发App,目前内部上架的App产品现有的登录方式有「账号/密码」 和 「手机号/验证码」两种登录方式;但这两种方式还是不够便捷,目前「手机号一键登录」是替代短信验证登录的下一代登录验证方式,能消除现有短信验证模式等待时间长、操作繁琐和容易泄露的痛点。


    因此,结合市面上的主流App应用,以及业务方的需求,我们的App产品也需要增加「手机号一键登录」功能。 DCloud联合个推公司整合了三大运营商网关认证的服务,通过运营商的底层SDK,实现App端无需短信验证码直接获取手机号。


    uni官方提供了对接的方案文档,可自行查阅,也可继续阅读本文


    准备工作


    1 目前支持的版本及运营商



    • 支持版本:HBuilderX 3.0+

    • 支持项目类型:uni-app的App端,5+ App,Wap2App

    • 支持系统平台: Android,iOS

    • 支持运营商: 中国移动,中国联通,中国电信


    2 费用


    2.1 运营商费用

    目前一键登录收费规则为每次登录成功请求0.02元,登录失败则不计费。


    2.2 云空间费用

    开通uniCloud是免费的,其中阿里云是全免费,腾讯云是提供一个免费服务空间。


    阿里云

    选择阿里云作为服务商时,服务空间资源完全免费,每个账号最多允许创建50个服务空间。阿里云目前处于公测阶段,如有正式业务对稳定性有较高要求建议使用腾讯云。


    image.png


    阿里云的服务空间是纯免费的。但为避免资源滥用,有一些限制,见下:


    image.png



    除上面的描述外,阿里云没有其他限制。
    因为阿里云免费向DCloud提供了硬件资源,所以DCloud也没有向开发者收费。如果阿里云后续明确了收费计划,DCloud也会第一时间公布。



    腾讯云

    选择腾讯云作为服务商时,可以创建一个免费的服务空间,资源详情参考腾讯云免费额度;如想提升免费空间资源配额,或创建更多服务空间,则需付费购买。


    image.png


    2.3 云函数费用

    如果你的一键登录业务平均每天获取手机号次数为10000次,使用阿里云正式版云服务空间后,对应云函数每天大概消耗0.139元


    接入


    1 重要前置条件



    • 手机安装有sim卡

    • 手机开启数据流量(与wifi无关,不要求关闭wifi,但数据流量不能禁用。)

    • 开通uniCloud服务(但不要求所有后台代码都使用uniCloud)

    • 开发者需要登录 DCloud开发者中心,申请开通一键登录服务。


    2 开发者中心-开通一键登录服务


    此官方文档详细步骤开通一键登录服务,开通后将当前项目加入一键登录内,审核2-3天;


    3 开通uniCloud


    一键登录在客户端获取 access_token 后,必须通过调用uniCloud中云函数换取手机号码,
    所以需要开通uniCould;


    登录uniCloud中web控制台里,新建服务空间,开通uniCloud


    在uniCloud的云函数中拿到手机号后,可以直接使用,也可以再转给传统服务器处理,也可以通过云函数url化方式生成普通的http接口给5+ App使用。


    4 客户端-一键登录


    当前项目关联云空间

    项目名称点击右键,创建云环境,创建的云环境应与之前开通的云空间类型保持一致,我这里选择腾讯云;


    image.png


    创建好后当前项目下会多个文件夹「uniCloud」,点击右键关联创建好的云空间


    image.png


    image.png


    关联成功


    image.png


    获取可用的服务提供商(暂时作用不大)

    一键登录对应的 provider ID为 'univerify',当获取provider列表时发现包含 'univerify' ,则说明当前环境打包了一键登录的sdk;


    uni.getProvider({
    service: 'oauth',
    success: function (res) {
    console.log(res.provider)// ['qq', 'univerify']
    }
    });

    参考文档


    预登录(可选)

    预登录操作可以判断当前设备环境是否支持一键登录,如果能支持一键登录,此时可以显示一键登录选项;


    uni.preLogin({
    provider: 'univerify',
    success(){ //预登录成功
    // 显示一键登录选项
    },
    fail(res){ // 预登录失败
    // 不显示一键登录选项(或置灰)
    // 根据错误信息判断失败原因,如有需要可将错误提交给统计服务器
    console.log(res.errCode)
    console.log(res.errMsg)
    }
    })

    参考文档


    请求登录授权

    弹出用户授权界面。根据用户操作及授权结果返回对应的回调,拿到 access_token,此时客户端登录认证完成;设置自定义按钮等;后续「需要将此数据提交到服务器获取手机号码」


    uni.login({
    provider: 'univerify',
    univerifyStyle: { // 自定义登录框样式
    //参考`univerifyStyle 数据结构`
    },
    success(res){ // 登录成功 在该回调中请求后端接口,将access_token传给后端
    console.log(res.authResult); // {openid:'登录授权唯一标识',access_token:'接口返回的 token'}
    },
    fail(res){ // 登录失败
    console.log(res.errCode)
    console.log(res.errMsg)
    }
    })

    参考文档


    获取用户是否选中了勾选框

    新增判断是否勾选一键登录相关协议函数;


    uni.getCheckBoxState({
    success(res){
    console.log(res.state) // Boolean 用户是否勾选了选框
    console.log(res.errMsg)
    },
    fail(res){
    console.log(res.errCode)
    console.log(res.errMsg)
    }
    })

    参考文档


    用access_token换手机号

    客户端获取到 access_token 后,传递给uniCloud云函数,云函数中通过uniCloud.getPhoneNumber方法获取真正的手机号。


    换取手机号有三种方式:




    1. 在前端直接写 uniCloud.callFunction ,将 access_token 传给指定的云函数。但需要在「云函数内部」请求服务端接口并将电话号码传到服务器;




    2. 使用普通ajax请求提交 access_token 给uniCloud的云函数(不考虑);




    3. 使用普通ajax请求提交 access_token 给自己的传统服务器,通过自己的传统服务器再转发给 uniCloud 云函数。但uniCloud上的「云函数需要做URL化」;




    我们目前使用的是第三种,防止电话号码暴露到前端,通过java小伙伴去请求uniCloud云函数,返回电话号码给后端;


    // 云函数验证签名,此示例中以接受GET请求为例作演示
    const crypto = require('crypto')
    exports.main = async(event) => {

    const secret = 'your-secret-string' // 自己的密钥不要直接使用示例值,且注意不要泄露
    const hmac = crypto.createHmac('sha256', secret);

    let params = event.queryStringParameters
    const sign = params.sign
    delete params.sign
    const signStr = Object.keys(params).sort().map(key => {
    return `${key}=${params[key]}`
    }).join('&')

    hmac.update(signStr);

    if(sign!==hmac.digest('hex')){
    throw new Error('非法访问')
    }

    const {
    access_token,
    openid
    } = params
    const res = await uniCloud.getPhoneNumber({
    provider: 'univerify',
    appid: 'xxx', // DCloud appid,不同于callFunction方式调用,使用云函数Url化需要传递DCloud appid参数
    apiKey: 'xxx', // 在开发者中心开通服务并获取apiKey
    apiSecret: 'xxx', // 在开发者中心开通服务并获取apiSecret
    access_token: access_token,
    openid: openid
    })
    // 返回手机号给自己服务器
    return res
    }

    res结果


    {
    "data": {
    "code": 0,
    "success": true,
    "phoneNumber": "166xxxx6666"
    },
    "statusCode": 200,
    "header": {
    "Content-Type": "application/json; charset=utf-8",
    "Connection": "keep-alive",
    "Content-Length": "53",
    "Date": "Fri, 06 Nov 2020 08:57:21 GMT",
    "X-CloudBase-Request-Id": "xxxxxxxxxxx",
    "ETag": "xxxxxx"
    },
    "errMsg": "request:ok"
    }

    参考文档


    客户端关闭一键登录授权界面

    请求登录认证操作完成后,不管成功或失败都不会关闭一键登录界面,需要主动调用closeAuthView方法关闭。完成业务服务登录逻辑后通知客户端关闭登录界面。


    uni.closeAuthView()

    参考文档


    错误码

    一键登录相关的错误码


    但其中状态码30006,官方未给出相关的说明,但与相关技术沟通得知,该状态码是运营商返回的,大概率是网络信号不好,或者其它等原因造成的,没办法修复,只能是想办法兼容改错误;


    目前我们的兼容处理方案是:程序检测判断如果出现该状态码,则关闭一键登录授权页面,并跳转到原有的「手机号验证码」登录页面


    参考文档


    5 云函数-一键登录


    自HBuilderX 3.4.0起云函数需启用uni-cloud-verify之后才可以调用getPhoneNumber接口,扩展库uni-cloud-verify


    需要在云函数的package.json内添加uni-cloud-verify的引用即可为云函数启用此扩展,无需做其他调整,因为HbuilderX内部已经集成了该扩展库,只需引入即可,不用安装,代码如下:


    {
    "name": "univerify",
    "extensions": {
    "uni-cloud-verify": {} // 启用一键登录扩展,值为空对象即可
    }
    }

    参考文档


    6 运行基座和打包


    使用uni一键登录,不需要制作自定义基座,使用HBuilder标准真机运行基座即可。在云函数中配置好apiKey、apiSecret后,只要一键登录成功,就会从你的账户充值中扣费。


    在菜单中配置模块权限


    image.png


    参考文档


    需要注意的问题


    1. 开通手机号一键登录是否同时需要开通苹果登录?


    目前只开通手机号一键登录,未开通苹果登录,在我们项目里是可以的,但是App云打包时是会弹框提示的,但是并不影响项目在App Store中发布;


    2. 如果同一个token多次反复获取手机号会重复扣费么?


    不会,这种场景应该仅限于联调测试使用,正式上线每次都应该获取最新token,避免过期报错;


    3. access_token过期时间



    • token过期时间是10分钟

    • 每次请求获取手机号接口时,都应该从客户端获取最新的token

    • 在取号成功时进行扣费,获取token不计费


    4. 预登录有效期


    预登录有效期为10分钟,超过10分钟后预登录失效,此时调用login授权登录相当于之前没有调用过预登录,大概需要等待1-2秒才能弹出授权界面。 预登录只能使用一次,调用login弹出授权界面后,如果用户操作取消登录授权,再次使用一键登录时需要重新调用预登录。


    作者:Wendy的小帕克
    来源:juejin.cn/post/7221422131857506359
    收起阅读 »

    Compose跨平台又来了,这次能开发iOS了

    /   今日科技快讯   /近日,有消息称百度3月将推出ChatGPT风格服务。经百度确认,该项目名字确定为文心一言,英文名ERNIE Bot,三月份完成内测,面向公众开放。目前,文心一言在做上线前的冲刺。百度方面表示,...
    继续阅读 »
    /   今日科技快讯   /

    近日,有消息称百度3月将推出ChatGPT风格服务。经百度确认,该项目名字确定为文心一言,英文名ERNIE Bot,三月份完成内测,面向公众开放。

    目前,文心一言在做上线前的冲刺。百度方面表示,ChatGPT相关技术,百度都有。百度在人工智能四层架构中,有全栈布局。包括底层的芯片、深度学习框架、大模型以及最上层的搜索等应用。文心一言,位于模型层。

    /   作者简介   /

    本篇文章转自黄林晴的博客,文章主要分享了如何使用Compose来进行IOS开发,相信会对大家有所帮助!

    原文地址:
    https://juejin.cn/post/7195770699524751421

    /   前言   /

    在之前,我们已经体验了Compose for Desktop与Compose for Web,目前Compose for IOS已经有尚未开放的实验性API,乐观估计今年年底将会发布 Compose for IOS。同时Kotlin也表示将在2023年发布KMM的稳定版本。



    届时Compose-jb + KMM将实现Kotlin全平台。



    /   搭建项目   /

    创建项目

    因为目前Compose for iOS阶段还在试验阶段,所以我们无法使用Android Studio或者IDEA直接创建Compose支持IOS的项目,这里我们采用之前的方法,先使用Android Studio创建一个KMM项目,如果你不知道如何创建一个KMM项目,可以参照之前的这篇文章KMM的初次尝试~,项目目录结构如下所示。



    创建好KMM项目后我们需要添加Compose跨平台的相关配置。

    添加配置

    首先在settings.gradle文件中声明compose插件,代码如下所示:

    pluginManagement {
        repositories {
            google()
            gradlePluginPortal()
            mavenCentral()
            maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
        }

        plugins {
            val composeVersion = extra["compose.version"as String
            id("org.jetbrains.compose").version(composeVersion)
        }
    }

    这里compose.version的版本号是声明在gradle.properties中的,代码如下所示:

    compose.version=1.3.0

    然后我们在shared模块中的build文件中引用插件:

    plugins {
        kotlin("multiplatform")
        kotlin("native.cocoapods")
        id("com.android.library")
        id("org.jetbrains.compose")
    }

    并为commonMain添加compose依赖,代码如下所示:

    val commonMain by getting {
        dependencies {
            implementation(compose.ui)
            implementation(compose.foundation)
            implementation(compose.material)
            implementation(compose.runtime)
        }
    }

    sync之后,你会发现一个错误警告:uikit还处于试验阶段并且有许多bug....



    uikit就是compose-jb暴露的UIKit对象。为了能够使用,我们需要在gradle.properties文件中添加如下配置:

    org.jetbrains.compose.experimental.uikit.enabled=true

    添加好配置之后,我们先来运行下iOS项目,确保添加的配置是无误的。果然,不运行不知道,一运行吓一跳。



    这个问题困扰了我两三天,实在是无从下手,毕竟现在相关的资料很少,经过N次的搜索,最终解决的方案很简单:Kotlin版本升级至1.8.0就可以了。

    kotlin("android").version("1.8.0").apply(false)

    再次运行项目,结果如下图所示。



    不过这是KMM的iOS项目,接下来我们看如何使用Compose编写iOS页面。

    /   开始iOS之旅   /

    我们替换掉iOSApp.swift中的原有代码,替换后的代码如下所示:

    import UIKit
    import shared

    @UIApplicationMain
    class AppDelegateUIResponderUIApplicationDelegate {
        var window: UIWindow?

        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            window = UIWindow(frame: UIScreen.main.bounds)
            let mainViewController = Main_iosKt.MainViewController()
            window?.rootViewController = mainViewController
            window?.makeKeyAndVisible()
            return true
        }
    }

    上面的代码看不懂没关系,我们只来看获取mainViewController的这一行:

    let mainViewController = Main_iosKt.MainViewController()

    Main_iosKt.MainViewController是通过新建在shared模块iOSMain目录下的main.ios.kt文件获取的,代码如下所示:

    fun MainViewController(): UIViewController = Application("Login") { //调用一个Compose方法 }

    接下来所有的事情就都可以交给Compose了。

    图片实现一个登录页面

    因为页面这部分是公用的,所以我们在shared模块下的commonMain文件夹下新建Login.kt文件,编写一个简单的登录页面,代码如下所示:

    @Composable
    internal fun login() {
        var userName by remember {
            mutableStateOf("")
        }
        var password by remember {
            mutableStateOf("")
        }
        Surface(modifier = Modifier.padding(30.dp)) {
            Column {
                TextField(userName, onValueChange = {
                    userName = it
                }, placeholder = { Text("请输入用户名") })
                TextField(password, onValueChange = {
                    password = it
                }, placeholder = { Text("请输入密码") })
                Button(onClick = {
                    //登录
                }) {
                    Text("登录")
                }
            }
        }
    }

    上述代码声明了一个用户名输入框、密码输入框和一个登录按钮,就是简单的Compose代码。然后需要在main.ios.kt中调用这个login方法:

    fun MainViewController(): UIViewController =
        Application("Login") {
            login()
        }

    运行iOS程序,效果如下图所示:



    嗯~,Compose 在iOS上UI几乎可以做到100%复用,还有不学习Compose的理由吗?

    实现一个双端网络请求功能

    在之前的第1弹和第2弹中,我们分别实现了在Desktop、和Web端的网络请求功能,现在我们对之前的功能在iOS上再次实现。

    添加网络请求配置

    首先在shared模块下的build文件中添加网络请求相关的配置,这里网络请求我们使用Ktor,具体的可参照之前的文章:KMM的初次尝试~

    配置代码如下所示:

    val commonMain by getting {
        dependencies {
            ...
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
            implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
            implementation("io.ktor:ktor-client-core:$ktorVersion")
            implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
            implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
        }
    }
    val iosMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-darwin:$ktorVersion")
        }
    }

    val androidMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-android:$ktorVersion")
        }
    }

    添加接口

    这里我们仍然使用wandroid中的每日一问接口。DemoReqData与之前系列的实体类是一样的,这里就不重复展示了。接口地址如下:
    https://wanandroid.com/wenda/list/1/json

    创建接口地址类,代码如下所示:

    object Api {
        val dataApi = "https://wanandroid.com/wenda/list/1/json"
    }

    创建HttpUtil类,用于创建HttpClient对象和获取数据的方法,代码如下所示。

    class HttpUtil {
        private val httpClient = HttpClient {
            install(ContentNegotiation) {
                json(Json {
                    prettyPrint = true
                    isLenient = true
                    ignoreUnknownKeys = true
                })
            }
        }

        /**
         * 获取数据
         */

        suspend fun getData(): DemoReqData {
            val rockets: DemoReqData =
                httpClient.get(Api.dataApi).body()
            return rockets
        }
    }

    这里的代码我们应该都是比较熟悉的,仅仅是换了一个网络请求框架而已。现在公共的业务逻辑已经处理好了,只需要页面端调用方法然后解析数据并展示即可。

    编写UI层

    由于Android、iOS、Desktop三端的UI都是完全复用的,所以我们将之前实现的UI搬过来即可。代码如下所示:

    Column() {
        val scope = rememberCoroutineScope()
        var demoReqData by remember { mutableStateOf(DemoReqData()) }
        Button(onClick = {
            scope.launch {
                try {
                    demoReqData = HttpUtil().getData()
                } catch (e: Exception) {
                }
            }
        }) {
            Text(text = "请求数据")
        }

        LazyColumn {
            repeat(demoReqData.data?.datas?.size ?: 0) {
                item {
                    Message(demoReqData.data?.datas?.get(it))
                }
            }
        }
    }

    获取数据后,通过Message方法将数据展示出来。这里只将作者与标题内容显示出来,代码如下所示:

    @Composable
    fun Message(dataDemoReqData.DataBean.DatasBean?) {
        Card(
            modifier = Modifier
                .background(Color.White)
                .padding(10.dp)
                .fillMaxWidth(), elevation = 10.dp
        ) {
            Column(modifier = Modifier.padding(10.dp)) {
                Text(
                    text = "作者:${data?.author}"
                )
                Text(text = "${data?.title}")
            }
        }
    }

    分别运行iOS、Android程序,点击请求数据按钮,结果如下图:



    这样我们就用一套代码,实现了在双端的网络请求功能。

    /   一个尴尬的问题   /

    我一直认为存在一个比较尴尬的问题,那就是像上面实现一个完整的双端网络请求功能需要用到KMM + Compose-jb,但是KMM与Compose-jb并不是一个东西,但是用的时候呢基本上都是一起用。Compose-jb很久之前已经发了稳定版本只是Compose-iOS目前还没有开放出来,而KMM当前还处于试验阶段,不过在2023年Kotlin的RoadMap中,Kotlin已经表示将会在23年中发布第一个稳定版本的KMM。而Compose for iOS何时发布,我想也是指日可待的事情。

    所以,这个系列我觉得改名为:Kotlin跨平台系列更适合一些,要不然以后就会存在KMM跨平台第n弹,Compse跨平台第n弹....

    因此,从第四弹开始,此系列将更名为:Kotin跨平台第N弹:~

    /   写在最后   /

    从自身体验来讲,我觉得KMM+Compose-jb对Android开发者来说是非常友好的,不需要像Flutter那样还需要额外学习Dart语言。所以,你觉得距离Kotlin一统“江山”的日子还会远吗?

    该文章转载自:https://mp.weixin.qq.com/s/LfD6AD-gDFdEYQS1X96CGw
    收起阅读 »

    环信 flutter sdk集成IM离线推送及点击推送获取推送信息(iOS版)

    前提条件1.macOS系统,安装了xcode和flutter集成环境2.有苹果开发者账号3.有环信开发者账号(注册地址:https://console.easemob.com/user/register)4.参考这篇文章https://www.imgeek.o...
    继续阅读 »

    前提条件

    1.macOS系统,安装了xcode和flutter集成环境

    2.有苹果开发者账号

    3.有环信开发者账号

    (注册地址:https://console.easemob.com/user/register)


    4.参考这篇文章https://www.imgeek.org/article/825360043,完成推送证书的创建和上传

    集成IM离线推送


    1.创建一个新的项目

    2.导入flutterSDK

    3.初始化环信sdk

    void initSDK() async {

      var options = EMOptions(

        appKey: “你的appkey”,

      );

      options.enableAPNs("EaseIM_APNS_Developer");

      await EMClient.getInstance.init(options);

      debugPrint("has init");

    }

    EaseIM_APNS_Developer是你在环信后台创建的证书名,需要注意,iOS需要上传开发证书和生产证书

    4.可以在 _incrementCounter 这个按钮点击事件中调用一下登录操作,到此flutter层的工作已经完成

    5.打开原生项目,修改包名,添加推送功能

    6.打开AppDelegate 文件 导入im_flutter_sdk,并且在didRegisterForRemoteNotificationsWithDeviceToken方面里面调用环信的registerForRemoteNotifications方法,进行token的绑定

    注:IM离线推送机制:

    1.环信这边需要针对设备deviceToken和环信的username进行绑定,

    2.IMserver 收到消息,会检测接收方是否在线,如果在线直接投递消息,如果不在线,则根据username 取设备的deviceToken

    3.根据设备的deviceToken 和 上传的证书给设备推送消息

    4.当app第一次运行的时候,就会走didRegisterForRemoteNotificationsWithDeviceToken方法,这个时候绑定token信息会报错,这个时候是正常的,因为你并没有登录,此时SDK内部会保存deviceToken,当你调用登录接口成功之后,SDK内部会进行一次绑定token的操作,

    到此,推送功能已经集成完毕,注意测试时建议先把项目杀死,保证该用户已经离线


    点击推送获取推送信息

    第一种方法 自己做桥接,实现原生层与flutter层做交互

    第二种方法 可以利用先有api 实现原生层给flutter层传递消息

    今天主要介绍第二种方法

    1.打开原生层 在didFinishLaunchingWithOptions和didReceiveRemoteNotification 方法里调用EMClientWrapper.shared().sendData(toFlutter: userInfo) 方法,把需要传递的数据传到flutter层

    didFinishLaunchingWithOptions 是在app没有打开的情况下点击推送,从launchOptions里面拿到推送信息

    didReceiveRemoteNotification是在 app已经打开的情况下点击推送,从userInfo里面拿到推送信息

    注意:EMClientWrapper.shared().sendData 这个方法填的参数必须是一个字典

    如下图所示

    2.打开flutter层 调用EMClient.getInstance.customEventHandler方法 需要赋值一个函数,这个函数就是接受来自原生层传递过来的消息

    3.此时 点击推送消息 在flutter层就能获取到信息,如图我测试的结果

    完毕




    收起阅读 »

    微信开放小程序运行SDK,我们的App可以跑小程序了

    前言这几天看到微信团队推出了一个名为 Donut 的小程序原生语法开发移动应用框架,通俗的讲就是将微信小程序的能力开放给其他的企业,第三方的 App 也能像微信一样运行小程序了。其实不止微信,面对潜力越来越大的 B 端市场,阿里早期就开放了这样产品——mPaa...
    继续阅读 »

    前言

    这几天看到微信团队推出了一个名为 Donut 的小程序原生语法开发移动应用框架,通俗的讲就是将微信小程序的能力开放给其他的企业,第三方的 App 也能像微信一样运行小程序了。



    其实不止微信,面对潜力越来越大的 B 端市场,阿里早期就开放了这样产品——mPaas,只不过阿里没有做太多的宣传推广,再加上并没有兼容市面中占比和使用范围最大的微信小程序,所以一直处于不温不火的状态。

    今天就主要对比分析下目前市面上这类产品的技术特点及优劣。

    有这些产品

    目前这类产品有一个统一的技术名称:小程序容器技术

    小程序容器顾名思义,是一个承载小程序的运行环境,可主动干预并进行功能扩展,达到丰富能力、优化性能、提升体验的目的。

    目前我已知的技术产品包括:mPaas、FinClip、uniSDK 以及上周微信团队才推出的 Donut。下面我们就一一初略讲下各自的特点。

    他们的特点

    1、mPaas

    mPaaS是源于支付宝 App 的移动开发平台,为移动开发、测试、运营及运维提供云到端的一站式解决方案,能有效降低技术门槛、减少研发成本、提升开发效率,协助企业快速搭建稳定高质量的移动 App。

    mPaaS 提供了包括 App 开发、H5 开发、小程序开发的能力,只要按照其文档可以开发 App,而且可以在其开发的 App 上跑 H5、也可跑基于支付宝小程序标准开发的的小程序。


    由于行业巨头之间互不对眼,目前 mPaas 仅支持阿里生态的小程序,不能直接兼容例如微信、百度、字节等其他生态平台的小程序。

    2、FinClip

    FinClip是一款小程序容器,不论是移动 App,还是电脑、电视、车载主机等设备,在集成 FinClip SDK 之后,都能快速获得运行小程序的能力。

    提供小程序 SDK 和小程序管理后台,开发者可以将已有的小程序迁移部署在自有 App 中,从而获得足够灵活的小程序开发与管理体验。

    FinClip 兼容微信小程序语法,提供全套的的小程序开发管理套件,开发者不需要学习新的语法和框架,使用 FinClip IDE、小程序管理后台、小程序开发文档、FinClip App就能低成本高质量地完成从开发测试,到预览部署的全部工作。


    3、Donut

    Donut多端框架是支持使用小程序原生语法开发移动应用的框架,开发者可以一次编码,分别编译为小程序和 Android 以及 iOS 应用,实现多端开发。

    基于该框架,开发者可以将小程序构建成可独立运行的移动应用,也可以将小程序构建成运行于原生应用中的业务模块。该框架还支持条件编译,开发者可灵活按需构建多端应用模块,可更好地满足企业在不同业务场景下搭建移动应用的需求。


    4、uniSDK

    Uni-app小程序 SDK,是为原生 App 打造的可运行基于 uni-app 开发的小程序前端项目的框架,从而帮助原生 App 快速获取小程序的能力。uni 小程序 SDK 是原生SDK,提供 Android 版本 和 iOS 版本,需要在原生工程中集成,然后即可运行用uni-app框架开发的小程序前端项目。

    Unisdk是 uni-app 小程序生态中的一部分,开发者 App 集成了该 SDK 之后,就可以在自有 App 上面跑起来利用 uni-app 开发的小程序。

    优劣势对比

    1、各自的优势

    mPaas

    • 大而全,App开发、H5开发、小程序开发一应俱全;

    • 技术产品来源于支付宝,背靠蚂蚁金服有大厂背书;

    • 兼容阿里系的小程序,例如支付宝、钉钉、高德、淘宝等;

    • 拥有小程序管理端、云端服务。

    FinClip

    • 小而巧,只专注小程序集成,集成SDK后体积增加3M左右,提供小程序全生命周期的管理 ;

    • 提供小程序转 App 服务,能够一定程度解决 App 开发难的问题;

    • 几个产品中唯一支持企业私有化部署的,可进行定制化开发,满足定制化需求;

    • 兼容微信小程序,之前开发者已拥有的微信小程序,可无缝迁移至 FinClip;

    • 多端支持:iOS、Android、Windows、macOS、Linux,国产信创、车载操作系统。

    Donut

    • 微信的亲儿子,对微信小程序兼容度有其他厂商无可比拟的优势(但也不是100%兼容微信小程序);

    • 提供小程序转 App 服务,能够一定程度解决 App 开发难的问题;

    • 体验分析支持自动接入功能,无需修改代码即可对应用中的所有元素进行埋点;

    • 提供丰富的登录方法:微信登录、苹果登录、验证码登录等。

    uniSDK

    • 开源社区,众人拾柴火焰高;

    • uniapp 开发小程序可迁移至微信、支付宝、百度等平台之上,如果采用 uni 小程序 SDK,之后采用 uni-app 开发小程序,那么就可以实现一次开发,多端上架;

    • 免费不要钱。

    2、各自的不足

    mPaas

    • 小程序管理略简单,没有小程序全生命周期的管理;

    • App 集成其 SDK 之后,体积会扩大 30M 左右;

    • 不兼容微信小程序,之前微信开发的小程序,需要用支付宝小程序的标准进行重写才可迁移到 mPaaS 上;

    • 目前只支持 iOS 与 Android 集成,不支持其他端。

    FinClip

    • 没有对应的移动应用开发平台,只专注于做小程序;

    • 生态能力相较于其他三者相对偏弱,但兼容微信语法可一定程度补齐;

    • 暂不支持 Serveless 服务;

    • 产品快速迭代,既有惊喜,也有未知。

    Donut

    • 对小程序的数量、并发数、宽带上限等有比较严格的规定;

    • 目前仅处于 beta 阶段,使用过程有一定 bug 感;

    • 集成后体积增加明显,核心 SDK 500 MB,地图 300 MB;

    • 没有小程序全生命周期的管理;

    • 目前仅支持 iOS 与 Android 集成,不支持其他端。

    uniSDK

    • 开源社区,质量由开源者背书,在集成、开发过程当中出现问题,bug解决周期长;

    • uni 小程序 SDK 仅支持使用 uni-app 开发的小程序,不支持纯 wxml 微信小程序运行;

    • 目前 uni 小程序 SDK 仅支持在原生 App 中集成使用,暂不支持 HBuilderX 打包生成的 App 中集成;

    • 目前只支持 iOS 与 Android 集成,不支持其他端。

    以上就是关于几个小程序容器的测评分析结果,可以看出并没有完美的选择,每个产品都有自己的一些优势和不足,选择适合自己的就是最好的。希望能给需要的同学一定的参考,如果你有更好的选择欢迎交流讨论。

    作者:Finbird
    来源:juejin.cn/post/7181301359554068541

    收起阅读 »

    如何使用 uni-app 30分钟快速开发即时通讯应用|开发者活动

    “一套代码,多端运行”是很多开发团队的梦想,基于 uni-app 跨平台框架支持 iOS、Android、Web以及各种小程序并支持平台间互通,快速实现搭建多端即时通讯功能,降低开发难度,提升开发效率。12月13日 晚 19:00,环信线上公开课《使用 uni...
    继续阅读 »


    “一套代码,多端运行”是很多开发团队的梦想,基于 uni-app 跨平台框架支持 iOS、Android、Web以及各种小程序并支持平台间互通,快速实现搭建多端即时通讯功能,降低开发难度,提升开发效率。
    12月13日 晚 19:00,环信线上公开课《使用 uniapp 30分钟快速开发即时通讯应用》为题,讲解多端 uni-app 基础框架知识及搭建即时通讯功能项目实战技巧,掌握开发步骤及思路,大大增强代码复用率,提升效率。来直播间 get 环信 IM 的正确打开方式!

    一、时间地点

    活动时间:12 月 13 日(星期二)19:00-20:00
    活动地点:线上直播

    二、演讲大纲

    • uni-app 跨平台框架介绍
    • 使用uni-app 生成 Android&iOS 应用
    • 如何搭建自己的即时通讯应用
    • IM实战篇-uni-app 经典问题答疑

    三、活动报名

    报名链接:https://mudu.tv/live/watch/meddae1l





    收起阅读 »

    哈啰 Quark Design 正式开源,下一代跨技术栈前端组件库

    官网:quark-design.hellobike.comQuark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web ...
    继续阅读 »

    Quark Design 是什么?

    官网:quark-design.hellobike.com

    github:github.com/hellof2e/qu…

    Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架中。

    Quark Design 历经一年多的开发时间,已在集团内部大量业务中得到验证,本着“共创、共建、共享”的开源精神,我们于即日起将 Quark 正式对外开源!Github地址:github.com/hellof2e/qu… (求star、求关注~😁)


    注:文档表现/样式参考了HeadlessUI/nutui/vant等。

    Quark Design 与现有主流组件库的区别是什么?

    Quark(夸克)有别于业界主流的移动端组件库,Quark 能同时运行在业界所有前端框架/无框架工程中,做到真正的技术栈无关 !我们不一样,:)

    • 不依赖技术栈(eg. Vue、React、Angular等)

    • 不依赖技术栈版本(eg. Vue2.x、Vue3.x)

    • 全新的Api设计(eg. 弹窗的打开属性由传统的 Visible 调整为符合浏览器原生弹窗的 open等)

    • 公司前端技术生态项目技术栈多时,保持视觉/交互统一

    • 完全覆盖您所需要的各类通用组件

    • 支持按需引用

    • 详尽的文档和示例

    • 支持定制主题

    性能优势-优先逻辑无阻塞

    我们以对 React 组件的 Web Components 化为例,一个普通的 React 组件在初次执行时需要一次性走完所有必须的节点逻辑,而这些逻辑的执行都同步占用在 js 的主线程上,那么当你的页面足够复杂时,一些非核心逻辑就将会阻塞后面的核心逻辑的执行。

    比如首次加载时,你的页面中有一个复杂的交互组件,交互组件中又包含 N多逻辑和按钮等小组件,此时页面的首次加载不应该优先去执行这些细节逻辑,而首要任务应当是优先渲染出整体框架或核心要素,而后再次去完善那些不必要第一时间完成的细节功能。 例如一些图像处理非常复杂,但你完全没必要在第一时间就去加载它们。

    当我们使用 Web Components 来优化 React的时候,这个执行过程将会变得简洁的多,比如我们注册了一个复杂的逻辑组件,在 React 执行时只是执行了一个 createElement 语句,创建它只需要 1-2 微秒即可完成,而真正的逻辑并不在同时执行,而是等到“核心任务”执行完再去执行,甚至你可以允许它在合适的时机再去执行。

    我们也可以简单的理解为,部分逻辑在之后进行执行然后被 render 到指定 id 的 Div 中的,那么为什么传统的组件为什么不能这么做呢?而非得 Web Components 呢?那就不得不提到它所包含的另一个技术特性:Shadow DOM


    组件隔离(Shadow Dom)

    Shadow DOM 为自定义的组件提供了包括 CSS、事件的有效隔离,不再担心不同的组件之间的样式、事件污染了。 这相当于为自定义组件提供了一个天然有效的保护伞。

    Shadow DOM 实际上是一个独立的子 DOM Tree,通过有限的接口和外部发生作用。 我们都知道页面中的 DOM 节点数越多,运行时性能将会越差,这是因为 DOM 节点的相互作用会时常在触发重绘(Repaint)和重排(reflow)时会关联计算大量 Frame 关系。


    而对 CSS 的隔离也将加快选择器的匹配速度,即便可能是微秒级的提升,但是在极端的性能情况下,依然是有效的手段。

    Quark 能为你带来什么?

    提效降本几乎是所有企业的主旋律,Quark 本身除了提供了通用组件之外,我们还为大家提供了开箱即用的 CLI,可以让大家在直接在日常开发中开发横跨多个技术栈/框架的业务组件。比如一个相同样式的营销弹窗,可以做到:

    • 同时运行在不同技术栈(Angular、Vue、React等)的前端工程中

    • 同时运行在不同版本的技术栈中,比如能同时运行在 Vue2.x、Vue3.x 中

    CLI 内部 Beta 版本目前初版已完成,github 地址:github.com/hellof2e/qu…

    适合场景:前端团队想发布一个独立的组件或npm包,让其他各类技术栈的工程使用,从而达到提效降本的目的。

    npm i -g @quarkd/quark-cli
    npx create-quark


    相关链接

    作者:Allan91
    来源:juejin.cn/post/7160483409691672606

    收起阅读 »

    uni-app跨端开发之疑难杂症

    今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说...
    继续阅读 »

    前言

    今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说,uni-app有很多坑,我对其也只是有些许了解,这回的全身心投入,才知道一入深坑愁似海

    这段时间也做了一些成效,头大如斗的路由拦截、必不可少的http请求封装、提高成效的组件库、仿照微信的oAuth 2.0登录、复杂逻辑的离线存储、用户需要的增量更新包

    有成效也踩了一些坑,百思不得解的console.log、烦到吐血的网络调试、爬坑许久的APP与h5通讯、性能极差的微信小程序端uni.canvasToTempFilePath

    今天就要聊聊一些疑难杂症,有些忘记了,有些还没碰到,后续持续更新吧!

    百思不得解的console.log

    移动端框架是采用npm包的方式提供给业务部门使用,其中包含oAuth2.0登录方式,这其中涉及到了h5通过scheme协议唤醒app并且带回code等参数,相应的参数会存放在plus.runtime.arguments,其他情况下,plus.runtime.arguments的值为空。在给同事排查问题时我就简单操作,在node_modules对应的npm包里面写了不是很严谨的如下代码:

    const args = plus.runtime.arguments;
    // 这个是业务部门出错时,我添加的调试代码
    console.log('>>>>>>'args)
    if (args) {
     const isLogout = args.includes('logout');
     if (isLogout) {
       await this.handleSession();
    else {
       await this.handleAuthorization(args);
    }
    }

    我测试是正常的,args是空值,所以是不会执行if内的逻辑的,但是他这边会执行if内的逻辑的,初步判断args由于某个原因导致存在值了,为了简单明了的查看输出内容,然后我就写了毁一生的console.log('>>>>>>', args),这行调试代码的输出内容如下,我一直以为args是空值,但是判断依旧为true,有点颠覆了我的人生观,后来灵机一动,删掉了第一个修饰参数,发现args原来是有值的,经过排查,是因为添加了微信小程序打开指定页面,导致记录当前页面数据。


    烦到吐血的网络调试

    网络调试对于我们的日常开发是很重要的,有助于快速判断资源请求问题,但uni-app在这方面有很大的缺陷,在讨论这个问题时,先来看一下uni-app的真机调试方式。

    终端调试工具

    当项目运行时,点击终端上的调试按钮,会弹出一个调试界面。


    从调试面板中,可以看到仅有ConsoleElementsSources三个选项,期待许久的Network并没有出现,这种调试方式没办法实现网络请求调试。


    webview调试控制台

    点击工具栏的 运行 -> 运行到手机或模拟器 -> 显示webview调试控制台 会出现一个跟谷歌浏览器一样的调试界面,虽然这里有Network,但是很可惜,这个功能存在问题,没办法监听到网络请求。


    Fiddler 抓取网络请求

    在走投无路之下,只能另辟蹊径,借助工具,抓取真机的网络请求,接下来阐述一下怎么使用Fiddler抓取真机的网络请求,配置完需要重启才生效。

    下载Fiddler

    这是一个免费工具,自行在网络上下载即可。

    Fiddler 基础配置

    点击工具栏的tools,选择options就会弹出一个配置界面



    HTTPS 配置

    选择HTTPS选项,勾选选矿中的Capture HTTPS CONNECTsDecrypt HTTPs trfficIgnore server certificate errors


    Connections 配置

    这边配置的端口号后面配置代理的时候需要使用到。


    手机配置代理

    注意需要和电脑连接同一网络,点击进入手机WIFI详情界面,有个代理,选择手动模式,输入电脑的IP地址和Fiddler的监听端口,即可拦截到真机的所有网络请求,包含我们app对应的网络请求。


    过滤

    这边可以选择过滤对应的ip或域名,多个的话通过分号隔开即可。


    爬坑许久的APP与h5通讯

    谈论这个问题时,先描述一下uni-app实现的app怎么和h5通讯

    app端

    对于app端的通讯,.vue.nvue有两点区别,1. 获取webView实例不一致,2. 监听方法不一致。app向h5传递数据时,需要借助webview.evalJS执行h5的全局方法,而h5向app传递参数时,类似于h5发送postMessage,可以在webview的message/onPostMessage监听函数获取数据。

    vue

    获取webView示例

    webView实例的获取,对于vue文件不是特别友好,需要借助于this.$scope.$getAppWebview(),如果是在组件中需要使用this.$parent.$scope.$getAppWebview(),添加延时的原因是,h5页面可能未加载完成,无法获取到对应的全局函数,会提示xxx函数undefined;

    <template>
       <web-view src="http://www.juejin.com"></web-view>
    </template>
    <script>
       export default {
           onReady() {
               const currentWebview = this.$scope.$getAppWebview();
               const account = '清欢bx'
               setTimeout(() => {
                   const webView = currentWebview.children()[0];
                   webView.evalJS(`setAccountInfo(${account})`);
              }, 1000);
          }
      }
    </script>

    监听方法

    vue文件采用@message触发监听函数

    <template>
       <web-view @message="handleMessage" src="http://www.juejin.com"></web-view>
    </template>
    <script>
       export default {
           methods: {
               handleMessage(data) {
                   console.log(data)
              }
          }
      }
    </script>

    nvue

    获取webView示例

    在nvue获取webView实例就很流畅了,直接通过this.$refs.webview就能获取到。

    <template>
       <web-view ref="webview" src="http://www.juejin.com"></web-view>
    </template>
    <script>
       export default {
           onReady() {
               const account = '清欢bx'
               this.$refs.webview.evalJs(`setAccountInfo(${account})`);
          }
      }
    </script>

    监听方法

    nvue文件采用@onPostMessage触发监听函数

    <template>
       <web-view @onPostMessage="handleMessage" src="http://www.juejin.com"></web-view>
    </template>
    <script>
       export default {
           methods: {
               handleMessage(data) {
                   console.log(data)
              }
          }
      }
    </script>

    h5 端

    发送数据

    需要引入一个uni-app的sdk,uni.webview.1.5.4.js,最低版本需要1.5.4,可以在index.html引入,也可以在main.js引入,注意点是传递的参数必须写在data里面,也就是维持这样的数据结构。

    uni.postMessage({
       data: {
         xxxxxx,
         xxxxxx
      }
    });

    如果是页面加载完成时就需要发送数据,需要等待UniAppJSBridgeReady钩子结束后触发postMessage;

    <script>
       export default {
           mounted() {
               document.addEventListener('UniAppJSBridgeReady'function() {
                   uni.webView.getEnv(function(res) {
                       console.log('当前环境:' + JSON.stringify(res));
                  });
                   uni.postMessage({
                       data: {
                         action'message'
                      }
                  });
              });
          }
      }
    </script>

    如果是通过事件点击发送数据,因为这时候页面已经加载完成,不需要再去监听UniAppJSBridgeReady钩子,直接触发uni.postMessage即可。

    <template>
       <view>
           <button @click="handlePostMessage">发送数据</button>
       </view>
    </template>
    <script>
       export default {
           methods: {
               handlePostMessage() {
                   uni.postMessage({
                       data: {
                         action'message'
                      }
                  });
              }
          }
      }
    </script>

    获取数据

    获取数据的函数,需要挂载到window上,可以直接写在main.js里面,数据需要共享到具体页面内,可以使用本地村存储localStorage、事件总线eventBusvuex,根据自己的需求选择。

    window.setAccountInfo = function(data) {
       console.log(data)
    }

    踩坑点

    uni is not defined

    app需要涉及到离线或者内网,索引uni.webview.js下载到本地进行引入,因为uni.webview.js已经被编译成了umd格式,在vue项目中在进行一次打包后,导致this指向不是window,所以没有把uni挂在到全局上,将this指向改为window即可。

    未改造之前的代码


    改造后


    或者


    app向h5传递参数时,无法传递对象,并且传递的参数需要字符串序列化

    在传递参数时,对象传递过去没办法识别,同时传递的参数需要执行JSON.stringify(),多个参数时,可以多个参数传递,也可以把多个参数进行字符串拼接,然后再h5端进行拆分处理。

    const { accountpassword } = accountInfo;
    const _account = JSON.stringify(account);
    const _password = JSON.stringify(password);
    setTimeout(() => {
       const webView = currentWebview.children()[0];
       webView.evalJS(`setAccountInfo(${_account}, ${_password})`);
    }, 1000);

    四、性能极差的canvas转图片

    自定义组件库里包含手写签名组件,需要用到uni.canvasToTempFilePathcanvas转成图片,这个方法的生成基础图片大小是根据当前屏幕分辨率,在模拟器上运行感觉性能还可以,但是在真机上的性能不高,如果笔画多的话,有时需要十几秒时间,这是没办法接受的,不过也有解决方式,可以通过设置destWidthdestHeight来自定义图片生成的大小,牺牲一些图片清晰度,来提高性能。

    uni.canvasToTempFilePath(
      {
         canvasIdthis.canvaId,
         destWidththis.imgWidth,
         destHeightthis.imgHeight,
         success: (res) => {
           console.log('success')
        },
         fail(e) {
           console.error(e);
        },
      },
       this,
    );

    小结

    我目前主要负责公司uni-app移动端框架的开发,包含组件库相应的生态工具多端适配离线存储hybrid,如果你也正在做相同的事,或者在使用uni-app开发,或者在学习uni-app都可以相互探讨,在这踩坑的过程中,我会持续完善此系类文章,帮助大家和自己更好的使用uni-app开发项目,fighting~

    作者:清欢bx
    来源:juejin.cn/post/7156017191169556511

    收起阅读 »

    uniapp热更新

    为什么要热更新热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤...
    继续阅读 »

    为什么要热更新

    热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦,所以这个时候热更新就显得很重要了。

    首先你需要在manifest.json 中修改版本号

    如果之前是1.0.0那么修改之后比如是1.0.1或者1.1.0这样


    然后你需要在HBuilderX中打一个wgt包

    在顶部>发行>原生App-制作移动App资源升级包


    包的位置会在控制台里面输出


    你需要和后端约定一下接口,传递参数


    然后你就可以在app.vue的onLaunch里面编写热更新的代码了,如果你有其他需求,你可以在其他页面的onLoad里面编写。

        // #ifdef APP-PLUS  //APP上面才会执行
    plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
    uni.request({
                                           url:'请求url写你自己的',
    method: "POST",
    data: {
    version: widgetInfo.version, //app版本号
    name: widgetInfo.name    //app名称
    },
    success: (result) => {
    console.log(result)  //请求成功的数据
    var data = result.data.data  
    if (data.update && data.wgtUrl) {
    var uploadTask = uni.downloadFile({ //下载
    url: data.wgtUrl, //后端传的wgt文件
                                       success: (downloadResult) => { //下载成功执行
                             if (downloadResult.statusCode === 200) {
              plus.runtime.install(downloadResult.tempFilePath, {
                                                   force: flase
                                          }, function() {
                                       plus.runtime.restart();
                                                      }, function(e) {});
                                                      }
                                                  },
                                                  })
                             uploadTask.onProgressUpdate((res) => {
    // 测试条件,取消上传任务。
    if (res.progress == 100) { //res.progress 上传进度
    uploadTask.abort();
    }
      });
            }
                                          }
                                      });
                                  });
    // #endif

    不支持的情况

    • SDK 部分有调整,比如新增了 Maps 模块等,不可通过此方式升级,必须通过整包的方式升级。

    • 原生插件的增改,同样不能使用此方式。
      对于老的非自定义组件编译模式,这种模式已经被淘汰下线。但以防万一也需要说明下,老的非自定义组件编译模式,如果之前工程没有 nvue 文件,但更新中新增了 nvue 文件,不能使用此方式。因为非自定义组件编译模式如果没有nvue文件是不会打包weex引擎进去的,原生引擎无法动态添加。自定义组件模式默认就含着weex引擎,不管工程下有没有nvue文件。

    注意事项

    • 条件编译,仅在 App 平台执行此升级逻辑。

    • appid 以及版本信息等,在 HBuilderX 真机运行开发期间,均为 HBuilder 这个应用的信息,因此需要打包自定义基座或正式包测试升级功能。

    • plus.runtime.version 或者 uni.getSystemInfo() 读取到的是 apk/ipa 包的版本号,而非 manifest.json 资源中的版本信息,所以这里用 plus.runtime.getProperty() 来获取相关信息。

    • 安装 wgt 资源包成功后,必须执行 plus.runtime.restart(),否则新的内容并不会生效。

    • 如果App的原生引擎不升级,只升级wgt包时需要注意测试wgt资源和原生基座的兼容性。平台默认会对不匹配的版本进行提醒,如果自测没问题,可以在manifest中配置忽略提示,详见ask.dcloud.net.cn/article/356…

    • http://www.example.com 是一个仅用做示例说明的地址,实际应用中应该是真实的 IP 或有效域名,请勿直接复制粘贴使用。

    关于热更新是否影响应用上架

    应用市场为了防止开发者不经市场审核许可,给用户提供违法内容,对热更新大多持排斥态度。

    但实际上热更新使用非常普遍,不管是原生开发中还是跨平台开发。

    Apple曾经禁止过jspatch,但没有打击其他的热更新方案,包括cordovar、react native、DCloud。封杀jspatch其实是因为jspatch有严重安全漏洞,可以被黑客利用,造成三方黑客可篡改其他App的数据。

    使用热更新需要注意:

    • 上架审核期间不要弹出热更新提示

    • 热更新内容使用https下载,避免被三方网络劫持

    • 不要更新违法内容、不要通过热更新破坏应用市场的利益,比如iOS的虚拟支付要老老实实给Apple分钱

    如果你的应用没有犯这些错误,应用市场是不会管的。

    作者:是一个秃头
    来源:juejin.cn/post/7039273141901721608

    收起阅读 »

    uniapp的骨架屏生成指南

    骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。 使用到的API有uni.createSelectorQuery()uni.getSystemInfoSync()。常规首页的布局一般而言,我们的首页的基础布局是包...
    继续阅读 »

    骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。 使用到的API有uni.createSelectorQuery()uni.getSystemInfoSync()

    常规首页的布局

    一般而言,我们的首页的基础布局是包含的有:顶部搜索、轮播、金刚区、新闻简报、活动魔方。

    <template>
       <view class="content">
           <!-- 顶部搜索 -->
           <headerSerch></headerSerch>
           <!-- 轮播 -->
           <swiperBg></swiperBg>
           <!-- 金刚区 -->
           <menus></menus>
           <!-- 新闻简报 -->
           <news></news>
           <!-- 活动魔方 -->
           <activity></activity>
           <!-- 骨架屏 -->
           <skeleton :show="show"></skeleton>
       </view>
    </template>

    <script>
       import headerSerch from './components/headerSerch.vue'
       import swiperBg from './components/swiperBg.vue'
       import menus from './components/menus.vue'
       import news from './components/news.vue'
       import activity from './components/activity.vue'
       import skeleton from './components/skeleton.vue'
       export default {
           components: {
               headerSerch,
               swiperBg,
               menus,
               news,
               activity,
               skeleton
          },
           data() {
               return {
                   show: true
              }
          },
           mounted() {
               setTimeout(()=>{
                   this.show = false
              },1200)
          }
      }
    </script>

    <style scoped>

    </style>

    skeleton组件的实现

    代码如下,稍后给大家解释


    步骤一 设置骨架屏的基础样式

    我们通过绝对定位的方式把组件的根元素提高层级,避免被父组件的其他组件覆盖掉。使用 uni.getSystemInfoSync()同步获取系统的可使用窗口宽度和可使用窗口高度并赋值给组件根元素的宽高。

    <view :style="{
    width: windowWidth,
    height: windowHeight,
    backgroundColor: bgColor,
    position: 'absolute',
    zIndex: 9999,
    top: top,
    left: left
    }">
    ......
    ......
    </view>

    <script>
       let systemInfo = uni.getSystemInfoSync();
       export default {
           name: 'skeleton',
           props: {
               show: {
                   type: Boolean,
                   default: true
              },
          },
           data() {
               return {
                   windowWidth: systemInfo.windowWidth + 'px',
                   windowHeight: systemInfo.windowHeight + 'px',
                   bgColor: '#fff',
                   top: 0,
                   left: 0,
              }
          }
        }
    </script>

    步骤二 渲染出占位的灰色块

    通过uniapp的uni.createSelectorQuery()接口,查询页面带有指定类名的元素的位置和尺寸, 通过绝对定位的方式,用同样尺寸的灰色块定位到相同的位置。

    在骨架屏中多数用的主要的矩形节点rectNodes 和圆形节点circleNodes。

    首先给这些元素加上相同的skeleton-fade类,这个类的主要为了有一个灰色的背景并使用animate属性使其看到颜色的深浅变化。


    按照官方的API使用说明,我们得在mounted 后进行调用方法。 在uni.createSelectorQuery()的后面加in(this.$parent)在微信小程序才能生效,在H5端不用加也生效。(我们主要是获取指定元素的位置和高度详细并赋值给rectNodes、circleNodes,所以得到之后可以把这两个方法删掉。)

    mounted() {
       // 矩形骨架元素
       this.getRectEls();
       // 圆形骨架元素
       this.getCircleEls();
    },

    methods: {
       getRectEls() {
           let query = uni.createSelectorQuery().in(this.$parent)
           query.selectAll('.skeleton-rect').boundingClientRect(res => {
                   console.log('rect', JSON.stringify(res));
          }).exec(function() {

          })
      },
       getCircleEls() {
           let query = uni.createSelectorQuery().in(this.$parent)
           query.selectAll('.skeleton-circle').boundingClientRect(res => {
                   console.log('circle', JSON.stringify(res));
          }).exec(function() {

          })
      }
    },

    如下图,在控制台上可以得到我们想到的节点信息。


    然后再复制粘贴给data中的rectNodes、circleNodes。 skeleton组件基本上就完成了。我们再做下优化,skeleton组件接收父组件传的show值,默认是true,当父组件的数据接口请求完成之后show设置为false。

    大功告成,以下的在浏览器端和微信小程序端的骨架屏展示:



    作者:清风programmer
    来源:juejin.cn/post/7037476325480742920

    收起阅读 »

    被「羊了个羊」逼疯后,鹅厂程序员怒而自制「必通关版」|GitHub热榜

    「羊了个羊」有多恶心?能逼程序员气到撸起袖子自己上……这两天,GitHub上就出现这么一个仿制版,名曰「鱼了个鱼」。不同于以「极低通关率」肝死玩家的原版,此版作者放出话来——没广告!可自定义关卡和图案!道具无限!。甚至可以定制出这(离)样(谱)的界面:目前,该...
    继续阅读 »

    「羊了个羊」有多恶心?

    能逼程序员气到撸起袖子自己上……

    这两天,GitHub上就出现这么一个仿制版,名曰「鱼了个鱼」


    不同于以「极低通关率」肝死玩家的原版,此版作者放出话来——

    没广告!可自定义关卡和图案!道具无限!

    甚至可以定制出这(离)(谱)的界面:


    目前,该项目已登GitHub热榜,获297个Star。(链接已附在文末)


    比「羊」更让人舒适

    先看看这款「鱼了个鱼」体验如何。

    从最简单模式开启,简直不要太Easy,道具都无需使用。


    再看中等和困难模式,稍有难度,还好有道具!

    原版的洗牌、撤回、移出可无限次使用,还有更多玄妙功能。

    比如透视,能看到最下方两列叠起来图案依次是什么,这感觉,相当于斗地主把最后三张看完了。


    再比如圣光,能把一大堆图案下面的图层从灰变白,看得更清楚。


    最逆天的还是破坏功能,直接消掉3个同样图案:


    也就是说,一直狂按这个道具能直接通关。


    值得一提的是,通关后祝贺画面是这个:


    建议作者优化下前端,直接换成这个:


    怒而自制必通关版

    据作者介绍,自己也是玩「羊了个羊」几十次,其间,他用尽道具,看了几十遍借贷广告,向富家千金反复求婚,仍然过不了第二关——

    他发现事情不对劲。

    由于方块生成完全随机,那越到后期,越来越多方块叠压在一起,可选方块变少,自然越来越难,经常无解也是常事。

    另一方面,正是极低的通关率让每个「自以为必胜」的玩家上头得不行,形成了上瘾感。


    于是……他怒而自制一个必能通关的版本。

    要求嘛,务必无广告,务必道具无限,要能自定义难度和图案,那更是一件美事儿。

    具体到原理,作者提出四大纲领。

    首先,游戏全局参数设置上,需要将槽位数量、层数等变量抽取成统一的全局变量,每当修改一处,系统自动适配,此外,作者还开放了参数自定义——

    嫌槽位不足?可以自己多加一个!


    其次是整体网格设计。

    为了能快速做出游戏,作者直接将画布分为24×24的虚拟网格,类似一个棋盘——

    每个网格又被划分成3×3的小单元,各层图案生成时,会相互错开1-2个单元,形成层层交叠、密密麻麻的样子。


    第三步是设计随机生成块的图案和坐标。

    先根据全局参数计算总块数,游戏难度越高,块数和相应层数也越多,然后作者用shuffle函数打乱存储所有动物图案的数组,再依次,把图案重新填充到方块中。

    至于如何生成方块坐标,直接让程序随机选取坐标范围内的点,同时随层级变深,坐标范围也越来越小,造成一种——

    越往深了去,图案越拥挤,难度相应越高的效果。


    大致分布规律就是越「深」层越「挤」

    最后,设定上下层块与块的关系。

    作者先给每个块指定一个层级属性,随机生成时,给相互重叠的块绑定层级关系,确保消掉上层块,才能点击下层块。

    基于上述思路,作者熬夜爆肝几个小时,就把游戏雏形做出来了,还放到GitHub上将代码开源——

    他感慨道,总算是满足了自己的通关夙愿。


    作者介绍

    事实上,「鱼了个鱼」项目作者「程序员鱼皮」已小有名气。

    据其个人公开资料显示,「程序员鱼皮」98年出生,现在鹅厂,从事全栈应用开发,同时,也是腾讯云开发高级布道师。

    工作之外,鱼皮利用业余时间做了很多入职大厂经验、技术干货和资源分享,据他称,在校期间就带领工作室建设了几十个校园网站。


    最后,附上「鱼了个鱼」在线体验链接,收获通关喜悦(狗头):

    https://yulegeyu.cn

    参考链接:
    [1]https://github.com/liyupi/yulegeyu
    [2]https://www.bilibili.com/video/BV1Pe411M7wh
    [3]https://mp.weixin.qq.com/s/D_I1Tq-ofhKhlp0rkOpaLA

    来源:詹士 发自 凹非寺

    收起阅读 »

    由点汇聚成字的动效炫极了

    前言在引入 fl_chart 绘制图表的时候,看到插件有下面这样的动效,随机散乱的圆点最后组合成了 Flutter 的 Logo,挺酷炫的。本篇我们来探讨类似的效果怎么实现。点阵在讲解代码实现之前,我们先科普一个知识,即点阵。点阵在日常生活中很常见,比如广告屏...
    继续阅读 »

    由点汇聚成字的动效炫极了

    前言

    在引入 fl_chart 绘制图表的时候,看到插件有下面这样的动效,随机散乱的圆点最后组合成了 Flutter 的 Logo,挺酷炫的。本篇我们来探讨类似的效果怎么实现。

    logo 动画.gif

    点阵

    在讲解代码实现之前,我们先科普一个知识,即点阵。点阵在日常生活中很常见,比如广告屏,停车系统的显示,行业内称之为 LED 显示屏。

    image.png

    LED 显示屏实际上就是由很多 LED 灯组合成的一个显示面板,然后通过显示驱动某些灯亮,某些灯灭就可以实现文字、图形的显示。LED 显示屏的点距足够小时,色彩足够丰富时其实就形成了我们日常的显示屏,比如 OLED 显示屏其实原理也是类似的。之前报道过的大学宿舍楼通过控制每个房间的灯亮灯灭来形成文字的原理也是一样的。

    image.png

    现在来看看 LED显示文字是怎么回事,比如我们要 显示岛上码农的“岛”字,在16x16的点阵上,通过排布得到的就是下面的结果(不同字体的排布会有些差别)。

    因为每一行是16个点,我们可以对应为16位二进制数,把黑色的标记为1,灰色的标记为0,每一行就可以得到一个二进制数。比如上面的第一行第8列为1,其他都是0,对应的二进制数就是0000000100000000,对应的16进制数就是0x0100。把其他行也按这种方式计算出来,最终得到的“岛”字对应的是16个16进制数,如下所示。

     [
    0x0100, 0x0200, 0x1FF0, 0x1010,
    0x1210, 0x1150, 0x1020, 0x1000,
    0x1FFC, 0x0204, 0x2224, 0x2224,
    0x3FE4, 0x0004, 0x0028, 0x0010
    ];
    复制代码

    又了这个基础,我们就可以用 Flutter 绘制点阵图形。

    点阵图形绘制

    首先我们绘制一个“LED 面板”,也就是绘制一个有若干个点构成的矩阵,这个比较简单,保持相同的间距,逐行绘制相同的圆即可,比如我们绘制一个16x16的点阵,实现代码如下所示。

    var paint = Paint()..color = Colors.grey;
    final dotCount = 16;
    final fontSize = 100.0;
    var radius = fontSize / dotCount;
    var startPos =
    Offset(size.width / 2 - fontSize, size.height / 2 - 2 * fontSize);
    for (int i = 0; i < dotCount; ++i) {
    var position = startPos + Offset(0.0, radius * i * 2);
    for (int j = 0; j < dotCount; ++j) {
    var dotPosition = startPos + Offset(radius * 2 * j, position.dy);
    canvas.drawCircle(dotPosition, radius, paint);
    }
    }
    复制代码

    绘制出来的效果如下:

    image.png

    接下来是点亮对应的位置来绘制文字了。上面我们讲过了,每一行是一个16进制数,那么我们只需要判断每一行的16进制数的第几个 bit是1就可以了,如果是1就点亮,否则不点亮。点亮的效果用不同的颜色就可以了。 怎么判断16进制数的第几个 bit 是不是1呢,这个就要用到位运算技巧了。实际上,我们可以用一个第 N 个 bit 是1,其他 bit 都是0的数与要判断的数进行“位与”运算,如果结果不为0,说明要判断的数的第 N 个 bit 是1,否则就是0。听着有点绕,看个例子,我们以0x0100为例,按从第0位到第15位逐个判断第0位和第15位是不是1,代码如下:

    for (i = 0 ; i < 16; ++i) {
    if ((0x0100 & (1 << i)) > 0) {
    // 第 i 位为1
    }
    }
    复制代码

    这里有两个位操作,1 << i是将1左移 i 位,为什么是这样呢,因为这样可以构成0x0001,0x0002,0x0004,...,0x8000等数字,这些数字依次从第0位,第1位,第2位,...,第15位为1,其他位都是0。然后我们用这样的数与另外一个数做位与运算时,就可以依次判断这个数的第0位,第1位,第2位,...,第15位是否为1了,下面是一个计算示例,第11位为1,其他位都是0,从而可以 判断另一个数的第11位是不是0。

    位与运算

    通过这样的逻辑我们就可以判断一行的 LED 中第几列应该点亮,然后实现文字的“显示”了,实现代码如下。wordHex是对应字的16个16进制数的数组。dotCount的值是16,用于控制绘制16x16大小的点阵。每隔一行我们向下移动一段直径距离,每隔一列,我们向右移动一段直径距离。然后如果当前绘制位置的数值对应的 bit位为1,就用蓝色绘制,否则就用灰色绘制。这里说一下为什么左移的时候要用dotCount - j - 1,这是因为绘制是从左到右的,而16进制数的左边是高位,而数字j是从小到大递增的,因此要通过这种方式保证判断的顺序是从高位(第15位)到低位(第0位),和绘制的顺序保持一致。

     for (int i = 0; i < dotCount; ++i) {
    var position = startPos + Offset(0.0, radius * i * 2);
    for (int j = 0; j < dotCount; ++j) {
    var dotPosition = startPos + Offset(radius * 2 * j, position.dy);

    if ((wordHex[i] & ((1 << dotCount - j - 1))) != 0) {
    paint.color = Colors.blue[600]!;
    canvas.drawCircle(dotPosition, radius, paint);
    } else {
    paint.color = Colors.grey;
    canvas.drawCircle(dotPosition, radius, paint);
    }
    }
    }
    复制代码

    绘制的结果如下所示。

    image.png

    由点聚集成字的动画实现

    接下来我们来考虑如何实现开篇说的类似的动画效果。实际上方法也很简单,就是先按照文字应该“点亮”的 LED 的数量,先在随机的位置绘制这么多数量的 LED,然后通过动画控制这些 LED 移动到目标位置——也就是文字本该绘制的位置。这个移动的计算公式如下,其中 t 是动画值,取值范围为0-1.

    移动公式

    需要注意的是,随机点不能在绘图过程生成,那样会导致每次绘制产生新的随机位置,也就是初始位置会变化,导致上面的公式实际不成立,就达不到预期的效果。另外,也不能在 build 方法中生成,因为每次刷新 build 方法就会被调用,同样会导致初始位置发生变化。所以,生成随机位置应该在 initState方法完成。但是又遇到一个新问题,那就是 initState方法里没有 context,拿不到屏幕宽高,所以不能直接生成位置,我们只需要生成一个0-1的随机系数就可以了,然后在绘制的时候在乘以屏幕宽高就得到实际的初始位置了。初始位置系数生成代码如下:

    @override
    void initState() {
    super.initState();
    var wordBitCount = 0;
    for (var hex in dao) {
    wordBitCount += _countBitOne(hex);
    }
    startPositions = List.generate(wordBitCount, (index) {
    return Offset(
    Random().nextDouble(),
    Random().nextDouble(),
    );
    });
    ...
    }
    复制代码

    wordBitCount是计算一个字中有多少 bit 是1的,以便知道要绘制的 “LED” 数量。接下来是绘制代码了,我们这次对于不亮的直接不绘制,然后要点亮的位置通过上面的位置计算公式计算,这样保证了一开始绘制的是随机位置,随着动画的过程,逐步移动到目标位置,最终汇聚成一个字,就实现了预期的动画效果,代码如下。

    void paint(Canvas canvas, Size size) {
    final dotCount = 16;
    final fontSize = 100.0;
    var radius = fontSize / dotCount;
    var startPos =
    Offset(size.width / 2 - fontSize, size.height / 2 - fontSize);
    var paint = Paint()..color = Colors.blue[600]!;

    var paintIndex = 0;
    for (int i = 0; i < dotCount; ++i) {
    var position = startPos + Offset(0.0, radius * i * 2);
    for (int j = 0; j < dotCount; ++j) {
    // 判断第 i 行第几位不为0,不为0则绘制,否则不绘制
    if ((wordHex[i] & ((1 << dotCount - j))) != 0) {
    var startX = startPositions[paintIndex].dx * size.width;
    var startY = startPositions[paintIndex].dy * size.height;
    var endX = startPos.dx + radius * j * 2;
    var endY = position.dy;
    var animationPos = Offset(startX + (endX - startX) * animationValue,
    startY + (endY - startY) * animationValue);
    canvas.drawCircle(animationPos, radius, paint);
    paintIndex++;
    }
    }
    }
    }
    复制代码

    来看看实现效果吧,是不是很酷炫?完整源码已提交至:绘图相关源码,文件名为:dot_font.dart

    点阵汇聚文字动画.gif

    总结

    本篇介绍了点阵的概念,以及基于点阵如何绘制文字、图形,最后通过先绘制随机点,再汇聚成文字的动画效果。可以看到,化整为零,再聚零为整的动画效果还是蛮酷炫的。实际上,基于这种方式,可以构建更多有趣的动画效果。

    作者:岛上码农

    来源:juejin.cn/post/7120233450627891237

    收起阅读 »

    uniapp使用canvas实现二维码分享

    实现使用canvas在小程序H5页面进行二维码分享 如下图效果 可以保存并扫码总体思路:使用canvas进行绘制,为了节省时间固定部分采用背景图绘制 只有二维码以及展示图片及标题绘制,绘制完成后调用uni.canvasToTempFilePath将其转为图片展...
    继续阅读 »

    实现使用canvas在小程序H5页面进行二维码分享 如下图效果 可以保存并扫码


    总体思路:使用canvas进行绘制,为了节省时间固定部分采用背景图绘制 只有二维码以及展示图片及标题绘制,绘制完成后调用uni.canvasToTempFilePath将其转为图片展示

    1.组件调用,使用ref调用组件内部相应的canvas绘制方法,传入相关参数 包括名称 路由 展示图片等。

     <SharePoster v-if='showposter' ref='poster' @close='close'/>

    <script>
     import SharePoster from "@/components/share/shareposter.vue"
     export default {
       components: {
          SharePoster,
      },
      methods:{
          handleShare(item){
             this.showposter=true
             if(this.showvote){
               this.showvote=false
            }
             this.$nextTick(() => {
            this.$refs.poster.drawposter(item.name, `/pagesMore/voluntary/video/player?schoolId=${item.id}`,item.cover)
            })
          },
      }
    </script>

    2.组件模板放置canvas容器并赋予id以及宽度高度等,使用iscomplete控制是显示canvas还是显示最后调用uni.canvasToTempFilePath生成的图片

    <div class="poster-wrapper" @click="closePoster($event)">
         <div class='poster-content'>
             <canvas canvas-id="qrcode"
               v-if="qrShow"
              :style="{opacity: 0, position: 'absolute', top: '-1000px'}"
             ></canvas>
             <canvas
               canvas-id="poster"
              :style="{ width: cansWidth + 'px', height: cansHeight + 'px' ,opacity: 0, }"
               v-if='!iscomplete'
             ></canvas>
             <image
               v-if="iscomplete"
              :style="{ width: cansWidth + 'px', height: cansHeight + 'px' }"
              :src="tempFilePath"
               @longpress="longpress"
             ></image>
         </div>
     </div>

    3.data内放置相应配置参数

     data() {
         return {
             bgImg:'https://cdn.img.up678.com/ueditor/upload/image/20211130/1638258070231028289.png', //画布背景图片
             cansWidth:288, // 画布宽度
             cansHeight:410, // 画布高度
             projectImgWidth:223, // 中间展示图片宽度
             projectImgHeight:167, // 中间展示图片高度
             qrShow:true, // 二维码canvas
             qrData: null, // 二维码数据
             tempFilePath:'',// 生成图路径
             iscomplete:false, // 是否生成图片
        }
      },

    4.在created生命周期内调用uni.createCanvasContext创建canvas实例 传入模板内canvas容器id

    created(){
         this.ctx = uni.createCanvasContext('poster',this)
      },

    5.调用对应方法,绘制分享作品

       // 绘制分享作品
         async drawposter(name='重庆最美高校景象',url,projectImg){
              uni.showLoading({
                title: "加载中...",
                mask: true
              })
              // 生成二维码
             await this.createQrcode(url)
             // 背景
             await this.drawWebImg({
               url: this.bgImg,
               x: 0, y: 0, width: this.cansWidth, height: this.cansHeight
            })
             // 展示图
             await this.drawWebImg({
               url: projectImg,
               x: 33, y: 90, width: this.projectImgWidth, height: this.projectImgHeight
            })
             await this.drawText({
               text: name,
               x: 15, y: 285, color: '#241D4A', size: 15, bold: true, center: true,
               shadowObj: {x: '0', y: '4', z: '4', color: 'rgba(173,77,0,0.22)'}
            })
             // 绘制二维码
             await this.drawQrcode()
             //转为图片
             this.tempFilePath = await this.saveCans()
             this.iscomplete = true
             uni.hideLoading()
        },

    6.绘制图片方法,注意 this.ctx.drawImage方法第一个参数不能放网络图片 必须执行下载后绘制

      drawWebImg(conf) {
           return new Promise((resolve, reject) => {
             uni.downloadFile({
               url: conf.url,
               success: (res) => {
                 this.ctx.drawImage(res.tempFilePath, conf.x, conf.y, conf.width?conf.width:"", conf.height?conf.height:"")
                 this.ctx.draw(true, () => {
                   resolve()
                })
              },
               fail: err => {
                 reject(err)
              }
            })
          })
        },

    7.绘制文本标题

     drawText(conf) {
           return new Promise((resolve, reject) => {
             this.ctx.restore()
             this.ctx.setFillStyle(conf.color)
             if(conf.bold) this.ctx.font = `normal bold ${conf.size}px sans-serif`
             this.ctx.setFontSize(conf.size)
             if(conf.shadowObj) {
               // this.ctx.shadowOffsetX = conf.shadowObj.x
               // this.ctx.shadowOffsetY = conf.shadowObj.y
               // this.ctx.shadowOffsetZ = conf.shadowObj.z
               // this.ctx.shadowColor = conf.shadowObj.color
            }
             let x = conf.x
             conf.text=this.fittingString(this.ctx,conf.text,280)
             if(conf.center) {
               let len = this.ctx.measureText(conf.text)
               x = this.cansWidth / 2 - len.width / 2 + 2
            }

             this.ctx.fillText(conf.text, x, conf.y)
             this.ctx.draw(true, () => {
               this.ctx.save()
               resolve()
            })
          })
        },
    // 文本标题溢出隐藏处理
    fittingString(_ctx, str, maxWidth) {
               let strWidth = _ctx.measureText(str).width;
               const ellipsis = '…';
               const ellipsisWidth = _ctx.measureText(ellipsis).width;
               if (strWidth <= maxWidth || maxWidth <= ellipsisWidth) {
                 return str;
              } else {
                 var len = str.length;
                 while (strWidth >= maxWidth - ellipsisWidth && len-- > 0) {
                   str = str.slice(0, len);
                   strWidth = _ctx.measureText(str).width;
                }
                 return str + ellipsis;
              }
            },

    8.生成二维码

          createQrcode(qrcodeUrl) {
           // console.log(window.location.origin)
           const config={host:window.location.origin}
           return new Promise((resolve, reject) => {
             let url = `${config.host}${qrcodeUrl}`
             // if(url.indexOf('?') === -1) url = url + '?sh=1'
             // else url = url + '&sh=1'
             try{
               new qrCode({
                 canvasId: 'qrcode',
                 usingComponents: true,
                 context: this,
                 // correctLevel: 3,
                 text: url,
                 size: 130,
                 cbResult: (res) => {
                   this.qrShow = false
                   this.qrData = res
                   resolve()
                }
              })
            } catch (err) {
               reject(err)
            }
          })
        },

    9.画二维码,this.qrData为生成的二维码资源

      drawQrcode(conf = { x: 185, y: 335, width: 100, height: 50}) {
    return new Promise((resolve, reject) => {
    this.ctx.drawImage(this.qrData, conf.x, conf.y, conf.width, conf.height)
    this.ctx.draw(true, () => {
    resolve()
    })
    })
    },

    10.将canvas绘制内容转为图片并显示,在H5平台下,tempFilePath 为 base64

    // canvs => images
    saveCans() {
    return new Promise((resolve, reject) => {
    uni.canvasToTempFilePath({
    x:0,
    y:0,
    canvasId: 'poster',
    success: (res) => {
    resolve(res.tempFilePath)
    },
    fail: (err) => {
    uni.hideLoading()
    reject(err)
    }
    }, this)
    })
    },

    11.组件全部代码


    作者:ArvinC
    来源:juejin.cn/post/7041087990222815246

    收起阅读 »

    Uniapp 多端开发经验整理

    本文档目的在于帮助基于 Uniapp 进行移动开发的人员 快速上手、规避问题、提升效率。将以流程提纲的方式,整理开发过程各阶段可能出现的问题点以及思路。对官方文档中已有内容,会贴附链接,尽量不做过多阐述以免冗余。使用时可根据需求和自身掌握情况,从目录跳转查看。...
    继续阅读 »

    文档说明:

    本文档目的在于帮助基于 Uniapp 进行移动开发的人员 快速上手、规避问题、提升效率。将以流程提纲的方式,整理开发过程各阶段可能出现的问题点以及思路。对官方文档中已有内容,会贴附链接,尽量不做过多阐述以免冗余。

    使用时可根据需求和自身掌握情况,从目录跳转查看。

    Uniapp 使用 Vue 语法+微信小程序 API,有二者基础可快速上手,开发 APP 还会用到 HTML5+规范 ,有非常丰富的原生能力。在此还是建议尽量安排时间通读官方文档,至少留下既有功能的印象,来增强对 Uniapp 开发的掌握,游刃有余的应对各类开发需求。

    开发准备

    小程序

    后台配置

    • 小程序个别类目需要行业资质,需要一定时间来申请,根据项目自身情况尽早进行 服务类目 的设置以免影响上线时间。

    • 必须在后台进行 服务器域名配置,域名必须 为 https 。否则无法进行网络请求。注意 每月只有 5 次修改机会

      在开发工具中可配置不验证 https,这样可以临时使用非 https 接口进行开发。非 https 真机预览时需要从右上角打开调试功能。

    • 如果有 webview 需求,必须在小程序管理后台配置域名白名单。

    开发工具

    • 下载 微信开发者工具

    • 设置 → 安全 → 打开“服务端口”。打开后方可用 HbuilderX 运行并更新到微信开发者工具。

    APP

    证书文件

    • 准备苹果开发账号

    • ios 证书、描述文件 申请方法

      证书和描述文件分为开发(Development)和发布(Distribution)两种,Distribution 用来打正式包,Development 用来打自定义基座包。

      ios 测试手机需要在苹果开发后台添加手机登录的 Apple 账号,且仅限邮箱方式注册的账号,否则无法添加。

    Uniapp

    创建 Uni-app 项目

    根据 文档 操作即可,新建时建议先不选择模板,因为模板后期也可以作为插件导入。这里推荐一个 UI 框架 uView,兼容 Nvue 的 Uniapp 生态框架。

    路由

    • 配置: 路由的开发方式与 Vue 不同,不再是 router,而是参照小程序原生开发规则在 pages.json 中进行 配置,注意 path 前面不加"/"。

    • 跳转: 路由的 跳转方式,同样参照了小程序 有 navigator 标签API 两种。

      1. navigator 标签: 推荐使用 有助于 SEO(搜索引擎优化)。

      2. API: 常用跳转方式 uni.navigateTo()uni.redirectTo()uni.switchTab(),即可处理大部分路由情况。

      需注意:

      • tabBar 页面 仅能通过 uni.switchTab方法进行跳转。

      • 如需求特殊可以自定义开发 tabBar,即 pages.json 中不要设置 tabBar,这样也就不需要使用 uni.switchTab 了。

      • url 前面需要加"/"

    • 问题点: 小程序页面栈最多 10 层。也就是说使用 uni.navigateTo 最多只能跳转 9 层页面。

      解决: 这里不推荐直接使用 uni.redirectTo 取代来处理,会影响用户体验,除非产品设计如此。建议在会出现同页面跳转的页面(例:产品详情 → 点击底部更多产品 → 产品详情 →...),封装一下页面跳转方法,使用 getCurrentPages() 方法获取当前页面栈的列表,根据列表长度去判断使用什么路由方法。路由方法的选择根据实际情况决定 官方文档

      //页面跳转
      toPage(url){
       let pages=getCurrentPages()
       if(pages.length<9){
         uni.navigateTo({url})
      }else{
         uni.redirectTo({url})//根据实际情况选择路由方法
      }
      }

    分包加载

    提前规划好分包,使代码文件更加规整,全局思路更加清晰。可以根据业务流程或者业务类型来设计分包。官方文档

    • 分包加载的使用场景:

      1. 主包大小超过 2m。

      2. 访问落地页启动较慢(因为需要下载整个主包)。

    • 分包优化:

      除页面可以分包配置,静态文件、js 也可以配置分包。可以进一步优化落地页加载速度。

      manifest.json对应平台下配置 "optimization":{"subPackages":true} 来开启分包优化。开启后分包目录下可以放置 static 内容。

      //manifest.json源码
      {
      ...,
         "mp-weixin" : {//这里以微信为例,如有其他平台需要分别添加
          ...,
             "optimization" : {
                 "subPackages" : true
            }
        }
      }
    • 分包预载

      通过分包进入落地页后,可能会有跳转其他分包页面的需求。开启分包预载,在落地页分包数据加载完后,提前加载后续分包页面,详见 官方文档

    生命周期

    • Uniapp 的页面生命周期建议使用 onLoadonShowonReadyonHide 等,也可以使用 vue 生命周期 createdmounted 等,但是组件的生命周期仅支持vue 生命周期的写法。

    easycom 组件模式

    • 说明: 只要组件安装在项目的 components 目录下或 uni_modules 目录下,并符合 components/组件名称/组件名称.vue 的目录结构,就可以不用引用、注册,直接在页面中使用。

      easycom 为默认开启状态,可关闭。可以根据需求配置其他路径规则。详见 官方文档

    • 代码举例:

      非 easycom 模式

      <template>
       <view>
         <goods-list>goods-list>
       view>
      template>
      <script>
      import goodsList from '@/component/goods-list'; //引用组件
      export default {
       components: {
         goodsList //注册组件
      }
      };
      script>

      使用 easycom 模式

      <template>
       <view>
         
         <goods-list>goods-list>
       view>
      template>
      <script>
      export default {};
      script>

    是否使用 Nvue

    • Nvue 开发

      • 优点:原生渲染,性能优势明显(性能优势主要体现在长列表)、启用纯原生渲染模式( manifest 里设置 app-plus 下的 renderer:"native" ) 可进一步减少打包体积(去除了小程序 webview 渲染相关模块)

      • 缺点:与 Vue 开发存在 差异,上手难度相对较高。并且设备兼容性问题较多。

      • 使用:适合仅开发 APP,并且项目对性能有较高要求、组件有复杂层级需求的情况下使用。

    • Nvue+vue 混合开发

      • 优点:性能与开发难度折中的选择,即大部分页面使用 Vue 开发,部分有性能要求的页面用 Nvue 开发。

      • 缺点:同 Nvue 开发。并且当应用没有长列表时,与 Vue 开发相比性能提升不明显。

      • 使用:适合需要同时开发 APP+小程序或 H5,并且项目有长列表的情况下使用。

    • Vue 开发

      • 优点:直接使用 Vue 语法进行开发,所有开发平台皆可兼容。

      • 缺点:在 APP 平台,使用 webview 渲染,性能比较 Nvue 相对差。

      • 使用:适合除需要 Nvue 开发外的所有情况。如果 APP 没有性能要求可使用 vue 一锅端。

    跨域

    • 如需开发 H5 版本,本地调试会碰到跨域问题。

    • 3 种解决方案:

      1. 使用 HbuilderX 内置浏览器预览。内置浏览器经过处理,不存在跨域问题。

      2. manifest.json 中配置,然后在封装的接口中判断 url

        // manifest.json
        {
         "h5": {
           "devServer": {
             "proxy": {
               "/api": {
                 "target": "https://***.***.com",
                 "pathRewrite": {
                   "^/api": ""
                }
              }
            }
          }
        }
        }
        //判断当前是否生产环境
        let url = (process.env.NODE_ENV == 'production' ? baseUrl : '/api') + api;
      3. 创建一个 vue.config.js 文件,并在里面配置 devServer

        // vue.config.js
        module.exports = {
         devServer: {
           proxy: {
             '/api': {
               target: 'https://***.***.com',
               pathRewrite: {
                 '^/api': ''
              }
            }
          }
        }
        };

        如果 2、3 方法同时使用,2 会覆盖 3。

    一键登录

    • 5+APP 一键登录,顾名思义:使用了 HTML5+规范、仅 APP 能用。官方指南

    • 小程序、H5 没有 HTML5+扩展规范。小程序可以使用

    推送

    既然在 uniapp 生态,就直接使用 UniPush 推送服务。

    • 该服务由个推提供,但必须向 DCloud 重新申请账号,不能用个推账号。

    开发中

    CSS

    • 建议使用 flex 布局开发。因为 flex 布局更灵活高效,且便于适配 Nvue(Nvue 仅支持 flex 布局)。

    • 小程序 css 中 background 背景图不支持本地路径。解决办法改为网络路径或 base64。

    • 图片设置 display:block。否则图片下方会有 3px 的空隙,会影响 UI 效果。

    • 多行文字需要限制行数溢出隐藏时,Nvue 和非 Nvue 写法不同。

      Nvue 写法

      .text {
       lines: 2; //行数
       text-overflow: ellipsis;
       word-wrap: break-word;
      }

      非 Nvue 写法

      .text {
      display: -webkit-box;
      -webkit-line-clamp: 2; //行数
      -webkit-box-orient: vertical;
      overflow: hidden;
      text-overflow: ellipsis;
      }

    图片

    mode

    • Uniapp 的 与传统 web 开发中的 相比多了一个 mode 属性,用来设置图片的裁剪、缩放模式。

    • 在开发中尽量养成每一个 都设置 mode 的习惯。可以规避掉很多 UI 显示异常的问题

    • 一般只需要使用 widthFixaspectFill 这两个属性即可应对绝大多数情况。

      即只需设置宽度自动撑起高度的图片用 widthFix ;需要固定尺寸设置宽高,并保持图片不被拉伸的图片用 aspectFill

      例如:所有 icon、文章详情里、产品详情里的详情图一般会用 widthFix,用户头像、缩略图一般会用 aspectFill

      属性详情见 官方文档

    lazy-load

    • 图片懒加载,小程序支持,只针对 page 与 scroll-view 下的 image 有效。

    图片压缩

    • 静态图片未压缩。该问题不限于 Uniapp 开发,也包括其他开发方式。是非常常见的问题。

    • 图片压缩前后,包体大小可差距 50%甚至更多。对编译和加载速度提升显著!

    • 此处放上两个 在线压缩工具 自行取用:Tinypngiloveimg

    滚动穿透

    • 弹窗遮罩显示时,底层页面仍可滚动。给遮罩最外层 view 增加事件 @touchmove.stop.prevent

    底部安全区

    • 问题: iOS 全面屏设备的屏幕底部有黑色横条显示,会对 UI 造成遮挡,影响事件点击和视觉效果。Android 没有横条,不受影响。

    • 场景: 各页面底部悬浮菜单、相对于底部距离固定的悬浮按钮、长列表的最后一个内容。

    • 解决方案:

      • 使用 css 样式 constant(safe-area-inset-bottom) env(safe-area-inset-bottom) 来处理,兼容 iOS11.2+,根据 iOS 系统版本占比,可放心使用。需注意该方法小程序模拟器不支持,真机正常。


      • 如果使用 nvue,则不支持以上方案。可使用 HTML5+规范 的方法来处理。


    交互反馈

    移动端比 PC 画面小很多,但是要展示的内容并不少,甚至更多。为了让用户正常使用,并获得优良体验。交互反馈的设置是必不可少的。并且在 UI 设计评审时就应该确定好,所有交互反馈是否齐全。

    • 缺省样式: 所有数量可能为空的数据展示,都应添加缺省样式,乃至缺省样式后的后续引导。

      例如:评论区没有评论,不应显示空白,而是显示(具体按 UI 设计):一个 message 的 icon,下方跟一句"快来发表你的高见",下方再跟一个发表按钮。这样不仅体现了评论区的状态,还做了评论的引导,增加了互动概率。

    • 状态提醒: 所有需要时间相应的状态变化,或者逻辑变化。都应对用户提供状态提醒。同样需要在 UI 设计评审时确认。

      例如:无网络时,显示网络异常,点击重试。各种等待、 下拉刷新、上拉加载、上传、下载、提交成功、失败、内容未加载完成时的骨架屏。甚至可以在点赞时加一个 vibrateShort 等等。

    分享

    除非特别要求不分享,或者订单等特殊页面。否则在开发时各个页面中一定要有设置分享的习惯。可以使应用的功能更完整更合理并且有助于搜索引擎优化。是必须考虑但又容易忽略的地方。

    • 在页面的生命周期中添加 onShareAppMessage 并配置其参数,否则点击右上角三个点,分享相关按钮是不可点击状态。

    • 小程序可以通过右上角胶囊按钮或者页面中

    • 代码示例:


    • return 的 Object 中 imageUrl 必须为宽高比例 5:4 的图片,并且图片大小尽量小于 20K。imageUrl 可不填,会自动截取当前页面画面。

    • 另外 button 有默认样式,需要清除一下。


    获取用户手机号

    • 小程序通过点击 button 获取 code 来跟后端换取手机号。在开发者工具中无法获取到 code。真机预览中可以获取到。


    苹果登录

    • APP 苹果登录需要使用自定义基座打包才能获得 Apple 的登录信息进行测试

    • iOS 自定义基座打包需要用开发(Development)版的证书和描述文件

    H5 唤起 App

    两种实现方式:

    1. URL Sheme

      优点:配置简单

      缺点:会弹窗询问“是否打开***”,未安装时网页没有回调,而且会弹窗“打不开网页,因为网址无效”;微信微博 QQ 等应用中被禁用,用户体验一般。

    2. Universal Link

      优点:没有额外弹窗,体验更优。

      缺点:配置门槛更高,需要一个不同于 H5 域名的 https 域名(跨域才出发 UL);iOS9 以上有效,iOS9 一下还是要用 URL Sheme 来解决;未安装 App 时会跳转到 404 需要单独处理。

    打包发布

    摇树优化

    • H5 打包时去除未引用的组件、API。

    • 摇树优化(treeShaking)

      //manifest.json
      "h5" : {
      "optimization":{
      "treeShaking":{
      "enable":true //启用摇树优化
      }
      }
      }

    启动图标

    让 UI 帮忙切一个符合以下标准的图片,在 APP 图标配置中自动生成即可。

    • 格式为 png

    • UI 切图时不要带圆角

    • 分辨率不小于 1024×1024

    启动图

    • 如没有特殊要求,直接使用通用启动页面即可。

    • 如需自定义启动图:

      • Android 可直接使用普通 png,也可配置.9.png,可减少包体积,避免缩放影响清晰度。为了更好的效果和体验建议使用.9 图。

        如何制作.9.png?使用 Android studio、ps。或者找 UI 同事帮忙

      • iOS 需要制作storyboard,如所需效果与 uni 提供的 storyboard 模板类似,可直接使用模板修改代码即可(xml 格式)。否则需要使用 xcode 进行更深度的修改,以实现效果并适配各机型。

    权限配置

    HBuilderX 默认会勾选一些不需要的权限,为避免审核打回,需要注意以下权限配置

    • manifest.json 中的【App 权限配置】取消勾选“Android 自动添加第三方 SDK 需要的权限”,然后在下方配置处根据参考文档取消勾选没有用到的权限,额外注意核对推送、分享等功能的权限需求。

    • manifest.json 中的【App 模块配置】仅勾选所需模块(容易漏掉,也会影响权限)

    补充

    SEO(搜索引擎优化)

    用户流量是衡量产品的重要指标之一,受到很多方面影响,SEO 就是其中之一。在没有额外推广的情况下,搜索引擎带来的流量基本就是产品流量的主要来源。传统 web 开发通过设置 TDK、sitemap 等,现阶段移动开发方法有所变化,但是万变不离其宗,核心还是一样的。

    • 小程序:

      • 被动方式:

        1. 确保 URL 可直接打开,通俗说就是 url 要有效,不能是 404。

        2. 页面跳转优先采用 navigator 组件

        3. 清晰简洁的页面参数

        4. 必要的时候才请求用户进行授权、登录、绑定手机号等

        5. 不收录 web-view,若非不需 seo 内容(用户协议之类)、或已有 H5 页面节省开发,否则尽量不要用 web-view。

        6. 配置sitemap

        7. 设置标题和分享缩略图 类似于传统 web 中设置 TDK。在百度小程序中有专门的接口来传递 SEO 信息。

      • 主动方式:

        1. 使用页面路径推送能力让微信收录内容

      内容详情请查看 优化指南。所有被动方式可以作为开发习惯来养成。

    • H5: 因为 Uniapp 是基于 Vue 语法来开发,这种 SPA 对于 SEO 并不友好。业界有 SSR(服务端渲染) 方法,等了很久 Uniapp 官方也终于提供了 SSR 的方法,但是需要使用 uniCloud。所以如果没有使用 uniCloud,暂时没有更合适的方法来处理该问题。

    • APP: 方式脱离前端范畴,不做讨论。

    作者:Tigger
    来源:juejin.cn/post/7138221718518595621

    收起阅读 »

    uniapp项目优化方式及建议

    1.复杂页面数据区域封装成组件例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新...
    继续阅读 »

    介绍:性能优化自古以来就是重中之重,关于uniapp项目优化方式最全整理,会根据开发情况进行补充

    1.复杂页面数据区域封装成组件

    场景

    例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿

    优化方案

    对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新这个组件

    注:app-nvue和h5不存在此问题;造成差异的原因是小程序目前只提供了组件差量更新的机制,不能自动计算所有页面差量

    2.避免使用大图

    场景

    页面中若大量使用大图资源,会造成页面切换的卡顿,导致系统内存升高,甚至白屏崩溃;对大体积的二进制文件进行 base64 ,也非常耗费资源

    优化方案

    图片请压缩后使用,避免大图,必要时可以考虑雪碧图或svg,简单代码能实现的就不要图片

    3.小程序、APP分包处理pages过多

    前往官网手册查看配置

    4.图片懒加载

    功能描述

    此功能只对微信小程序、App、百度小程序、字节跳动小程序有效,默认开启

    前往uView手册查看配置

    5.禁止滥用本地存储

    不要滥用本地存储,局部页面之间的传参用url,如果用本地存储传递数据要命名规范和按需销毁

    6.可在外部定义变量

    在 uni-app 中,定义在 data 里面的数据每次变化时都会通知视图层重新渲染页面;所以如果不是视图所需要的变量,可以不定义在 data 中,可在外部定义变量或直接挂载在 vue实例 上,以避免造成资源浪费

    7.分批加载数据优化页面渲染

    场景

    页面初始化时,逻辑层一次性向视图层传递很大的数据,使视图层一次性渲染大量节点,可能造成通讯变慢、页面切换卡顿

    优化方案

    以局部更新页面的方式渲染页面;如:服务端返回 100条数据 ,可进行分批加载,一次加载 50条 , 500ms 后进行下一次加载

    8.避免视图层和逻辑层频繁进行通讯

    1. 减少 scroll-view 组件的 scroll 事件监听,当监听 scroll-view 的滚动事件时,视图层会频繁的向逻辑层发送数据

    2. 监听 scroll-view 组件的滚动事件时,不要实时的改变 scroll-top / scroll-left 属性,因为监听滚动时,视图层向逻辑层通讯,改变 scroll-top / scroll-left 时,逻辑层又向视图层通讯,这样就可能造成通讯卡顿

    3. 注意 onPageScroll 的使用, onPageScroll 进行监听时,视图层会频繁的向逻辑层发送数据

    4. 多使用 css动画 ,而不是通过js的定时器操作界面做动画

    5. 如需在 canvas 里做跟手操作, app端 建议使用 renderjs ,小程序端建议使用 web-view 组件; web-view 里的页面没有逻辑层和视图层分离的概念,自然也不会有通信折损

    9.CSS优化

    要知道哪些属性是有继承效果的,像字体、字体颜色、文字大小都是继承的,禁止没有意义的重复代码

    10.善用节流和防抖

    防抖

    等待n秒后执行某函数,若等待期间再次被触发,则等待时间重新初始化

    节流

    触发事件n秒内只执行一次,n秒未过,再次触发无效

    11.优化页面切换动画

    场景

    页面初始化时存在大量图片或原生组件渲染和大量数据通讯,会发生新页面渲染和窗体进入动画抢资源,造成页面切换卡顿、掉帧

    优化方案

    1. 建议延时 100ms~300ms 渲染图片或复杂原生组件,分批进行数据通讯,以减少一次性渲染的节点数量

    2. App 端动画效果可以自定义; popin/popout 的双窗体联动挤压动画效果对资源的消耗更大,如果动画期间页面里在执行耗时的js,可能会造成动画掉帧;此时可以使用消耗资源更小的动画效果,比如 slide-in-right / slide-out-right

    3. App-nvue 和 H5 ,还支持页面预载,uni.preloadPage,可以提供更好的使用体验

    12.优化背景色闪白

    场景

    进入新页面时背景闪白,如果页面背景是深色,在vue页面中可能会发生新窗体刚开始动画时是灰白色背景,动画结束时才变为深色背景,造成闪屏

    优化方案

    1. 将样式写在 App.vue 里,可以加速页面样式渲染速度; App.vue 里面的样式是全局样式,每次新开页面会优先加载 App.vue 里面的样式,然后加载普通 vue 页面的样式

    2. app端 还可以在 pages.json 的页面的 style 里单独配置页面原生背景色,比如在 globalStyle->style->app-plus->background 下配置全局背景色

    "style": { "app-plus": { "background":"#000000" } }
    1. nvue页面不存在此问题,也可以更改为nvue页面

    13.优化启动速度

    1. 工程代码越多,包括背景图和本地字体文件越大,对小程序启动速度有影响,应注意控制体积

    2. App端的 splash 关闭有白屏检测机制,如果首页一直白屏或首页本身就是一个空的中转页面,可能会造成 splash 10秒才关闭

    3. App端使用v3编译器,首页为 nvue页面 时,并设置为fast启动模式,此时App启动速度最快

    4. App设置为纯 nvue项目 (manifest里设置app-plus下的renderer:"native"),这种项目的启动速度更快,2秒即可完成启动;因为它整个应用都使用原生渲染,不加载基于webview的那套框架

    14.优化包体积

    1. uni-app 发行到小程序时,如果使用了 es6 转 es5 、css 对齐的功能,可能会增大代码体积,可以配置这些编译功能是否开启

    2. uni-app 的 H5端,uni-app 提供了摇树优化机制,未摇树优化前的 uni-app 整体包体积约 500k,服务器部署 gzip 后162k。开启摇树优化需在manifest配置

    3. uni-app 的 App端,Android 基础引擎约 9M ,App 还提供了扩展模块,比如地图、蓝牙等,打包时如不需要这些模块,可以裁剪掉,以缩小发行包;体积在 manifest.json-App 模块权限里可以选择

    4. App端支持如果选择纯nvue项目 (manifest里设置app-plus下的renderer:"native"),包体积可以进一步减少2M左右

    5. App端在 HBuilderX 2.7 后,App 端下掉了 非v3 的编译模式,包体积下降了3M

    15.禁止滥用外部js插件

    描述

    有官方API的就不要额外引用js插件增加项目体积

    例如

    url传参加密直接用 encodeURIComponent() 和 decodeURIComponent()

    作者:Panda_HYC
    来源:juejin.cn/post/6997224351346982942

    收起阅读 »

    搞不定移动端性能,全球爆火的 Notion 从 Hybrid 转向了 Native

    7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了...
    继续阅读 »

    7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。

    该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了 3 倍。


    Notion 发布的这条 Twitter 也得到了广泛的关注,几天之内就有了上千条转发。由于前几年 Notion 的技术栈一直没有公开,开发者对此充满了各种猜测,很多人认为 Notion 使用的是 React Native 或 Electron,因此这次 Notion 宣称切换为原生 iOS 和原生 Android,再一次引发了“框架之争”。

    其中有不少人发表了“贬低”跨平台开发的看法,对 React Native 等框架产生了质疑,毕竟现在向跨平台过渡是不可避免的,这些框架是对原生工具包的一个“威胁”,而 Notion 恰恰又切换到了“原生”开发模式。

    实际上,在 2020 年之前 Notion 使用的是 React Native,随后切换到了 Hybrid 混合开发模式:使用 Kotlin/Swift + 运行网络应用程序的 Web 视图。但移动端的性能一直是一个问题,2 年之后,Notion 再次切换到了原生开发模式。

    有网友认为,像 Notion 这样重 UI 和交互的产品,如果不知道如何掌握 Web 技术,那么对他们的产出速度表示担忧。面对这种吵翻天的状况,Notion 的前端工程师也因此再度出面回应这次切换的原因和一些思考。

    Notion 的发展和理念

    Notion 是一款将笔记、知识库和任务管理无缝衔接整合的多人协作平台。Notion 打破了传统的笔记软件对于内容的组合方式,将文档的内容分成一个个 Block,并且能够以乐高式的方式拖动组合这些 Block,让它使用起来十分灵活。

    Notion 由 Ivan Zhao、Simon Last 于 2013 年在旧金山创立。去年底,Notion 获得了 2.75 亿美元的 C 轮融资。截至 2021 年 10 月,Notion 估值 103 亿美元,在全球拥有超 2000 万用户。Notion 的创始人和 CEO Ivan Zhao 是一位 80 后华人。他出生于中国新疆,曾就读于清华附中,中学时随家人移居加拿大,现在被很多人认为将成为硅谷的下一个袁征(Zoom 的创始人)。Ivan 在大学时期主修认知科学,学习的是人的大脑怎么运作,外加对计算机也很感兴趣。



    Ivan 也曾表示“我的很多朋友都是艺术家。我是他们中唯一会编码的人。我想开发一款软件,它不仅可以为人们提供文档或网页。” 因此,在 2012 年大学毕业后,在文档共享初创公司 Inkling 工作期间,他创办了 Notion。原本的目标是构建一个无代码应用构建工具,不过项目很快失败了。随后 Ivan 与 Simon 迁往了日本京都,待了一年左右,小而安静的地方能“让我们专注在写代码”,在相对无压力和与世隔绝的环境下,构思并设计出了现在的 Notion 原型。用 Reddit 论坛上的一条获得高赞的网友总结就是:一个 Notion = Google docs + Evernote + Trello + Confluence + Github + Wiki +……

    “工具应该模仿人脑的工作方式。但由于每个人的思维和工作方式都不同,这意味着工具需要非常灵活。”Ivan 解释道。而 Notion 创建的目的,就是将用户从一堆各式各样的生产力工具之中解放出来,给予一个干净清爽、简便易行的 All in One 工作平台。企业用户也可以在 Notion 上基本实现公司的内部管理所需要涉及到的所有功能。包括公司知识库和资料库的创建与管理、项目进度管理、信息共享、工作日志、内部社交、协作办公等等。


    有人甚至说,Notion 堪比办公软件届的苹果。在 2016 年发布 1.0 版本后,因其独特的设计、专注于将事情做得更好、对投资人的冷淡态度,外加疫情远程办公潮,多方面因素让 Notion 迅速火遍全球。作为一款 All in one 的概念型工具,Notion 一直被众多企业抄作业,但它目前几乎未逢敌手。

    Notion 为什么要两次更换技术栈?

    Notion 在 2017 年、2018 年分别发布了 iOS 客户端和 Android 客户端。在发布 2.0 版本之后,该公司于 2019 年以 8 亿美元的估值筹集了 1000 万美元的资金。但也许和创始人的发展理念相关,Notion 的员工数量一直不多。

    2019 年 3 月的时候,工程团队总共才 4 个人,当时 Notion 用 React Native 来渲染 web 视图。Notion 在 Twitter 上解释说,这是为了更快地部署新功能和进行一些其他修复。

    但如果这个系统适合开发者,那么它对用户来说远非最佳:许多人抱怨移动版本非常缓慢。“即使是新 iPhone 也非常慢 - 大约 6-7 秒后我才能开始输入笔记。到那时我都快忘记了我之前想写什么。它基本上是一个非常重的 web 应用程序视图。”“如果 Notion 不选择改变,那么它将迅速被其它同类产品取代。”......



    2020 年,Notion 第一次因这个问题,更改了技术栈,放弃 React Native,切换到了 Hybrid 开发环境。

    Notion 前端负责人 Jake Teton‑Landis 表示,“React Native 的优势在于允许 Web 开发人员构建手机应用程序。如果我们已经有了 webview,那么 React Native 不会增加价值。对我们来说,它让一切变得更加困难:性能、代码复杂性、招聘等等。用 React Native 快速完成任务的同时,也在跟复杂性战斗,这让我们感觉束手束脚。”

    虽然这次移动端的性能有了一些提升,但也没有根本解决问题,更新之后,Android 端依然是一个相当大的痛点。


    Notion 也曾在 2019 年的时候表示不会很快发布本机应用程序,但他们同时强调“原生开发也是一个选择”。

    7 月 20 日,Notion 发布了版本更新,并表示将从主页选项卡开始,从 webview 逐步一个个地切换到本机应用程序。

    此时 Notion 工程团队也大约只有 100 人, 总共包含 3 位 iOS 工程师、4 位 android 工程师,除主页使用 SwiftUI/Jetpack Compose 进行渲染,其他部分仍然是 webview 进行绘制。

    “似乎这还是招聘不足产生的人员问题。”Jake 解释说,“我们的策略是随着团队的壮大逐步本地化我们应用程序的更多部分。我们这个程序必须使用本机性能,如果它是原生的,则更容易达到这个性能要求。

    凭借我们拥有的经验,以及对问题的了解,我们因此选择了原生 iOS 和原生 Android 开发。虽然出于复杂性的权衡,在可预见的未来,编辑器可能仍然是一个 webview,毕竟 Google Docs、Quip、Dropbox Paper、Coda 都使用原生 shell、webview 编辑器。”

    原生开发才是王道?!

    虽然无论是原生开发还是 Hybrid 都可以完成工作,但原生应用程序是按照操作系统技术和用户体验准则开发的,因此具有更快的性能优势,并能轻松访问和利用用户设备的内置功能(例如,GPS、地址簿、相机等)。

    Hybrid 开发方式,通常是在面对市场竞争需要尽快构建并发布应用程序时候的一种选择。如果期望的发布时间少于六个月,那么混合可能是一个更好的选择,因为可以构建一套源代码,跨平台发布,与原生开发相比,其开发时间和工作量要少得多,但这也意味着需要做出许多性能和功能上的妥协。

    如果有足够时间,那么原生方法最有意义,可以让应用程序具有最佳性能、最高安全性和最佳用户体验。毕竟,用户体验是应用程序成功的关键。互联网正在放缓,人们使用手机的时间越来越长,缓慢的应用程序意味着糟糕的业务。在这种情况下,对 Notion 来说,拥有一个快速应用程序比以往任何时候都更加重要。

    参考链接:

    https://www.notion.so/releases/2022-07-20

    https://twitter.com/jitl/status/1530326516013342723?s=20&t=xT0gfWhFvs0yNvc1GQ3sTQ

    收起阅读 »

    我对 React 实现原理的理解

    React 是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解。react 和 vue 都是基于 vdom 的前端框架,我们先聊下 vdom:vdom为什么 react 和 vue...
    继续阅读 »

    React 是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解。

    react 和 vue 都是基于 vdom 的前端框架,我们先聊下 vdom:

    vdom

    为什么 react 和 vue 都要基于 vdom 呢?直接操作真实 dom 不行么?

    考虑下这样的场景:

    渲染就是用 dom api 对真实 dom 做增删改,如果已经渲染了一个 dom,后来要更新,那就要遍历它所有的属性,重新设置,比如 id、clasName、onclick 等。

    而 dom 的属性是很多的:


    有很多属性根本用不到,但在更新时却要跟着重新设置一遍。

    能不能只对比我们关心的属性呢?

    把这些单独摘出来用 JS 对象表示不就行了?

    这就是为什么要有 vdom,是它的第一个好处。

    而且有了 vdom 之后,就没有和 dom 强绑定了,可以渲染到别的平台,比如 native、canvas 等等。

    这是 vdom 的第二个好处。

    我们知道了 vdom 就是用 JS 对象表示最终渲染的 dom 的,比如:

    {
       type'div',
       props: {
           id'aaa',
           className: ['bbb''ccc'],
           onClickfunction() {}
      },
       children: []
    }

    然后用渲染器把它渲染出来。

    但是要让开发去写这样的 vdom 么?

    那肯定不行,这样太麻烦了,大家熟悉的是 html 那种方式,所以我们要引入编译的手段。

    dsl 的编译

    dsl 是 domain specific language,领域特定语言的意思,html、css 都是 web 领域的 dsl。

    直接写 vdom 太麻烦了,所以前端框架都会设计一套 dsl,然后编译成 render function,执行后产生 vdom。

    vue 和 react 都是这样:


    这套 dsl 怎么设计呢?

    前端领域大家熟悉的描述 dom 的方式是 html,最好的方式自然是也设计成那样。

    所以 vue 的 template,react 的 jsx 就都是这么设计的。

    vue 的 template compiler 是自己实现的,而 react 的 jsx 的编译器是 babel 实现的,是两个团队合作的结果。

    比如我们可以这样写:


    编译成 render function 后再执行就是我们需要的 vdom。

    接下来渲染器把它渲染出来就行了。

    那渲染器怎么渲染 vdom 的呢?

    渲染 vdom

    渲染 vdom 也就是通过 dom api 增删改 dom。

    比如一个 div,那就要 document.createElement 创建元素,然后 setAttribute 设置属性,addEventListener 设置事件监听器。

    如果是文本,那就要 document.createTextNode 来创建。

    所以说根据 vdom 类型的不同,写个 if else,分别做不同的处理就行了。

    没错,不管 vue 还是 react,渲染器里这段 if else 是少不了的:

    switch (vdom.tag) {
     case HostComponent:
       // 创建或更新 dom
     case HostText:
       // 创建或更新 dom
     case FunctionComponent
       // 创建或更新 dom
     case ClassComponent
       // 创建或更新 dom
    }

    react 里是通过 tag 来区分 vdom 类型的,比如 HostComponent 就是元素,HostText 就是文本,FunctionComponent、ClassComponent 就分别是函数组件和类组件。

    那么问题来了,组件怎么渲染呢?

    这就涉及到组件的原理了:

    组件

    我们的目标是通过 vdom 描述界面,在 react 里会使用 jsx。

    这样的 jsx 有的时候是基于 state 来动态生成的。如何把 state 和 jsx 关联起来呢?

    封装成 function、class 或者 option 对象的形式。然后在渲染的时候执行它们拿到 vdom 就行了。

    这就是组件的实现原理:

    switch (vdom.tag) {
     case FunctionComponent
          const childVdom = vdom.type(props);
          
          render(childVdom);
          //...
     case ClassComponent
        const instance = new vdom.type(props);
        const childVdom = instance.render();
        
        render(childVdom);
        //...
    }

    如果是函数组件,那就传入 props 执行它,拿到 vdom 之后再递归渲染。

    如果是 class 组件,那就创建它的实例对象,调用 render 方法拿到 vdom,然后递归渲染。

    所以,大家猜到 vue 的 option 对象的组件描述方式怎么渲染了么?

    {
       data: {},
       props: {}
       render(h) {
           return h('div', {}, '');
      }
    }

    没错,就是执行下 render 方法就行:

    const childVdom = option.render();

    render(childVdom);

    大家可能平时会写单文件组件 sfc 的形式,那个会有专门的编译器,把 template 编译成 render function,然后挂到 option 对象的 render 方法上:


    所以组件本质上只是对产生 vdom 的逻辑的封装,函数的形式、option 对象的形式、class 的形式都可以。

    就像 vue3 也有了函数组件一样,组件的形式并不重要。

    基于 vdom 的前端框架渲染流程都差不多,vue 和 react 很多方面是一样的。但是管理状态的方式不一样,vue 有响应式,而 react 则是 setState 的 api 的方式。

    真说起来,vue 和 react 最大的区别就是状态管理方式的区别,因为这个区别导致了后面架构演变方向的不同。

    状态管理

    react 是通过 setState 的 api 触发状态更新的,更新以后就重新渲染整个 vdom。

    而 vue 是通过对状态做代理,get 的时候收集以来,然后修改状态的时候就可以触发对应组件的 render 了。

    有的同学可能会问,为什么 react 不直接渲染对应组件呢?

    想象一下这个场景:

    父组件把它的 setState 函数传递给子组件,子组件调用了它。

    这时候更新是子组件触发的,但是要渲染的就只有那个组件么?

    明显不是,还有它的父组件。

    同理,某个组件更新实际上可能触发任意位置的其他组件更新的。

    所以必须重新渲染整个 vdom 才行。

    那 vue 为啥可以做到精准的更新变化的组件呢?

    因为响应式的代理呀,不管是子组件、父组件、还是其他位置的组件,只要用到了对应的状态,那就会被作为依赖收集起来,状态变化的时候就可以触发它们的 render,不管是组件是在哪里的。

    这就是为什么 react 需要重新渲染整个 vdom,而 vue 不用。

    这个问题也导致了后来两者架构上逐渐有了差异。

    react 架构的演变

    react15 的时候,和 vue 的渲染流程还是很像的,都是递归渲染 vdom,增删改 dom 就行。

    但是因为状态管理方式的差异逐渐导致了架构的差异。

    react 的 setState 会渲染整个 vdom,而一个应用的所有 vdom 可能是很庞大的,计算量就可能很大。

    浏览器里 js 计算时间太长是会阻塞渲染的,会占用每一帧的动画、重绘重排的时间,这样动画就会卡顿。

    作为一个有追求的前端框架,动画卡顿肯定是不行的。但是因为 setState 的方式只能渲染整个 vdom,所以计算量大是不可避免的。

    那能不能把计算量拆分一下,每一帧计算一部分,不要阻塞动画的渲染呢?

    顺着这个思路,react 就改造为了 fiber 架构。

    fiber 架构

    优化的目标是打断计算,分多次进行,但现在递归的渲染是不能打断的,有两个方面的原因导致的:

    • 渲染的时候直接就操作了 dom 了,这时候打断了,那已经更新到 dom 的那部分怎么办?

    • 现在是直接渲染的 vdom,而 vdom 里只有 children 的信息,如果打断了,怎么找到它的父节点呢?

    第一个问题的解决还是容易想到的:

    渲染的时候不要直接更新到 dom 了,只找到变化的部分,打个增删改的标记,创建好 dom,等全部计算完了一次性更新到 dom 就好了。

    所以 react 把渲染流程分为了两部分: render 和 commit。

    render 阶段会找到 vdom 中变化的部分,创建 dom,打上增删改的标记,这个叫做 reconcile,调和。

    reconcile 是可以打断的,由 schedule 调度。

    之后全部计算完了,就一次性更新到 dom,叫做 commit。

    这样,react 就把之前的和 vue 很像的递归渲染,改造成了 render(reconcile + schdule) + commit 两个阶段的渲染。

    从此以后,react 和 vue 架构上的差异才大了起来。

    第二个问题,如何打断以后还能找到父节点、其他兄弟节点呢?

    现有的 vdom 是不行的,需要再记录下 parent、silbing 的信息。所以 react 创造了 fiber 的数据结构。


    除了 children 信息外,额外多了 sibling、return,分别记录着兄弟节点、父节点的信息。

    这个数据结构也叫做 fiber。(fiber 既是一种数据结构,也代表 render + commit 的渲染流程)

    react 会先把 vdom 转换成 fiber,再去进行 reconcile,这样就是可打断的了。

    为什么这样就可以打断了呢?

    因为现在不再是递归,而是循环了:

    function workLoop() {
     while (wip) {
       performUnitOfWork();
    }

     if (!wip && wipRoot) {
       commitRoot();
    }
    }

    react 里有一个 workLoop 循环,每次循环做一个 fiber 的 reconcile,当前处理的 fiber 会放在 workInProgress 这个全局变量上。

    当循环完了,也就是 wip 为空了,那就执行 commit 阶段,把 reconcile 的结果更新到 dom。

    每个 fiber 的 reconcile 是根据类型来做的不同处理。当处理完了当前 fiber 节点,就把 wip 指向 sibling、return 来切到下个 fiber 节点。:

    function performUnitOfWork() {
     const { tag } = wip;

     switch (tag) {
       case HostComponent:
         updateHostComponent(wip);
         break;

       case FunctionComponent:
         updateFunctionComponent(wip);
         break;

       case ClassComponent:
         updateClassComponent(wip);
         break;
       case Fragment:
         updateFragmentComponent(wip);
         break;
       case HostText:
         updateHostTextComponent(wip);
         break;
       default:
         break;
    }

     if (wip.child) {
       wip = wip.child;
       return;
    }

     let next = wip;

     while (next) {
       if (next.sibling) {
         wip = next.sibling;
         return;
      }
       next = next.return;
    }

     wip = null;
    }

    函数组件和 class 组件的 reconcile 和之前讲的一样,就是调用 render 拿到 vdom,然后继续处理渲染出的 vdom:

    function updateClassComponent(wip) {
     const { typeprops } = wip;
     const instance = new type(props);
     const children = instance.render();

     reconcileChildren(wipchildren);
    }

    function updateFunctionComponent(wip) {
     renderWithHooks(wip);

     const { typeprops } = wip;

     const children = type(props);
     reconcileChildren(wipchildren);
    }

    循环执行 reconcile,那每次处理之前判断一下是不是有更高优先级的任务,就能实现打断了。

    所以我们在每次处理 fiber 节点的 reconcile 之前,都先调用下 shouldYield 方法:

    function workLoop() {
    while (wip && shouldYield()) {
    performUnitOfWork();
    }

    if (!wip && wipRoot) {
    commitRoot();
    }
    }

    shouldYiled 方法就是判断待处理的任务队列有没有优先级更高的任务,有的话就先处理那边的 fiber,这边的先暂停一下。

    这就是 fiber 架构的 reconcile 可以打断的原理。通过 fiber 的数据结构,加上循环处理前每次判断下是否打断来实现的。

    聊完了 render 阶段(reconcile + schedule),接下来就进入 commit 阶段了。

    前面说过,为了变为可打断的,reconcile 阶段并不会真正操作 dom,只会创建 dom 然后打个 effectTag 的增删改标记。

    commit 阶段就根据标记来更新 dom 就可以了。

    但是 commit 阶段要再遍历一次 fiber 来查找有 effectTag 的节点,更新 dom 么?

    这样当然没问题,但没必要。完全可以在 reconcile 的时候把有 effectTag 的节点收集到一个队列里,然后 commit 阶段直接遍历这个队列就行了。

    这个队列叫做 effectList。

    react 会在 commit 阶段遍历 effectList,根据 effectTag 来增删改 dom。

    dom 创建前后就是 useEffect、useLayoutEffect 还有一些函数组件的生命周期函数执行的时候。

    useEffect 被设计成了在 dom 操作前异步调用,useLayoutEffect 是在 dom 操作后同步调用。

    为什么这样呢?

    因为都要操作 dom 了,这时候如果来了个 effect 同步执行,计算量很大,那不是把 fiber 架构带来的优势有毁了么?

    所以 effect 是异步的,不会阻塞渲染。

    而 useLayoutEffect,顾名思义是想在这个阶段拿到一些布局信息的,dom 操作完以后就可以了,而且都渲染完了,自然也就可以同步调用了。

    实际上 react 把 commit 阶段也分成了 3 个小阶段。

    before mutation、mutation、layout。

    mutation 就是遍历 effectList 来更新 dom 的。

    它的之前就是 before mutation,会异步调度 useEffect 的回调函数。

    它之后就是 layout 阶段了,因为这个阶段已经可以拿到布局信息了,会同步调用 useLayoutEffect 的回调函数。而且这个阶段可以拿到新的 dom 节点,还会更新下 ref。

    至此,我们对 react 的新架构,render、commit 两大阶段都干了什么就理清了。

    总结

    react 和 vue 都是基于 vdom 的前端框架,之所以用 vdom 是因为可以精准的对比关心的属性,而且还可以跨平台渲染。

    但是开发不会直接写 vdom,而是通过 jsx 这种接近 html 语法的 DSL,编译产生 render function,执行后产生 vdom。

    vdom 的渲染就是根据不同的类型来用不同的 dom api 来操作 dom。

    渲染组件的时候,如果是函数组件,就执行它拿到 vdom。class 组件就创建实例然后调用 render 方法拿到 vdom。vue 的那种 option 对象的话,就调用 render 方法拿到 vdom。

    组件本质上就是对一段 vdom 产生逻辑的封装,函数、class、option 对象甚至其他形式都可以。

    react 和 vue 最大的区别在状态管理方式上,vue 是通过响应式,react 是通过 setState 的 api。我觉得这个是最大的区别,因为它导致了后面 react 架构的变更。

    react 的 setState 的方式,导致它并不知道哪些组件变了,需要渲染整个 vdom 才行。但是这样计算量又会比较大,会阻塞渲染,导致动画卡顿。

    所以 react 后来改造成了 fiber 架构,目标是可打断的计算。

    为了这个目标,不能变对比变更新 dom 了,所以把渲染分为了 render 和 commit 两个阶段,render 阶段通过 schedule 调度来进行 reconcile,也就是找到变化的部分,创建 dom,打上增删改的 tag,等全部计算完之后,commit 阶段一次性更新到 dom。

    打断之后要找到父节点、兄弟节点,所以 vdom 也被改造成了 fiber 的数据结构,有了 parent、sibling 的信息。

    所以 fiber 既指这种链表的数据结构,又指这个 render、commit 的流程。

    reconcile 阶段每次处理一个 fiber 节点,处理前会判断下 shouldYield,如果有更高优先级的任务,那就先执行别的。

    commit 阶段不用再次遍历 fiber 树,为了优化,react 把有 effectTag 的 fiber 都放到了 effectList 队列中,遍历更新即可。

    在dom 操作前,会异步调用 useEffect 的回调函数,异步是因为不能阻塞渲染。

    在 dom 操作之后,会同步调用 useLayoutEffect 的回调函数,并且更新 ref。

    所以,commit 阶段又分成了 before mutation、mutation、layout 这三个小阶段,就对应上面说的那三部分。

    我觉得理解了 vdom、jsx、组件本质、fiber、render(reconcile + schedule) + commit(before mutation、mutation、layout)的渲染流程,就算是对 react 原理有一个比较深的理解了。


    作者:zxg_神说要有光
    来源:juejin.cn/post/7117051812540055588

    收起阅读 »

    Flutter中的异步

    同步与异步程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用...
    继续阅读 »

    同步与异步

    程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。

    从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。

    同步I/O 与 异步I/O的区别


    为什么使用异步

    用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:

    • I/O操作:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用

    • 并行执行多个操作:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步

    • 长时间运行的基于事件驱动的请求:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应

    计算机中异步的实现方式就是任务调度,也就是进程的切换

    任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。

    计算机系统分为用户空间内核空间,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程


    在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程

    每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换

    Future

    Future<T> 表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error,使用代码实例:

    Future<int> future = getFuture();
    future.then((value) => handleValue(value))
        .catchError((error) => handleError(error))
    .whenComplete(func);

    future可以是三种状态:未完成的返回结果值返回异常

    当一个返回future对象被调用时,会发生两件事:

    • 将函数操作入队列等待执行结果并返回一个未完成的Future对象

    • 函数操作完成时,Future对象变为完成并携带一个值或一个错误

    首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue中事件,最后执行事件队列Event Queue中的事件,示例:

    void main(){
     Future(() => print(10));
    Future.microtask(() => print(9));
     print("main");
    }
    /// 打印结果为:
    /// main
    /// 9
    /// 10

    基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function

    factory Future(FutureOr<T> computation()) {
       _Future<T> result = new _Future<T>();
       Timer.run(() {
         try {
           result._complete(computation());
        } catch (e, s) {
           _completeWithErrorCallback(result, e, s);
        }
      });
       return result;
    }

    Function有多种写法:

    //简单操作,单步
    Future(() => print(5));
    //稍复杂,匿名函数
    Future((){
     print(6);
    });
    //更多操作,方法名
    Future(printSeven);

    printSeven(){
     print(7);
    }
     

    Future.microtask

    此工程方法创建的事件将发送到微任务队列Microtask Queue,具有相比事件队列Event Queue优先执行的特点

    factory Future.microtask(FutureOr<T> computation()) {
       _Future<T> result = new _Future<T>();
    //
       scheduleMicrotask(() {
         try {
           result._complete(computation());
        } catch (e, s) {
           _completeWithErrorCallback(result, e, s);
        }
      });
       return result;
    }

    Future.sync

    返回一个立即执行传入参数的Future,可理解为同步调用

    factory Future.sync(FutureOr<T> computation()) {
       try {
         var result = computation();
         if (result is Future<T>) {
           return result;
        } else {
           // TODO(40014): Remove cast when type promotion works.
           return new _Future<T>.value(result as dynamic);
        }
      } catch (error, stackTrace) {
         /// ...
      }
    }
    Future.microtask(() => print(9));
     Future(() => print(10));
     Future.sync(() => print(11));

    /// 打印结果: 11、9、10

    Future.value

    创建一个将来包含value的future

    factory Future.value([FutureOr<T>? value]) {
       return new _Future<T>.immediate(value == null ? value as T : value);
    }

    参数FutureOr含义为T value 和 Future value 的合集,因为对于一个Future参数来说,他的结果可能为value或者是Future,所以对于以下两种写法均合法:

        Future.value(12).then((value) => print(value));
     Future.value(Future<int>((){
       return 13;
    }));

    这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前

    Future.error

    创建一个执行结果为error的future

    factory Future.error(Object error, [StackTrace? stackTrace]) {
    /// ...
    return new _Future<T>.immediateError(error, stackTrace);
    }

    _Future.immediateError(var error, StackTrace stackTrace)
    : _zone = Zone._current {
    _asyncCompleteError(error, stackTrace);
    }
    Future.error(new Exception("err msg"))
    .then((value) => print("err value: $value"))
    .catchError((e) => print(e));

    /// 执行结果为:Exception: err msg

    Future.delayed

    创建一个延迟执行回调的future,内部实现为Timer加延时执行一个Future

    factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
    /// ...
    new Timer(duration, () {
    if (computation == null) {
    result._complete(null as T);
    } else {
    try {
    result._complete(computation());
    } catch (e, s) {
    _completeWithErrorCallback(result, e, s);
    }
    }
    });
    return result;
    }

    Future.wait

    等待多个Future并收集返回结果

    static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
    {bool eagerError = false, void cleanUp(T successValue)?}) {
    /// ...
    }

    FutureBuilder结合使用:

    child: FutureBuilder(
    future: Future.wait([
    firstFuture(),
    secondFuture()
    ]),
    builder: (context,snapshot){
    if(!snapshot.hasData){
    return CircularProgressIndicator();
    }
    final first = snapshot.data[0];
    final second = snapshot.data[1];
    return Text("data $first $second");
    },
    ),

    Future.any

    返回futures集合中第一个返回结果的值

    static Future<T> any<T>(Iterable<Future<T>> futures) {
    var completer = new Completer<T>.sync();
    void onValue(T value) {
    if (!completer.isCompleted) completer.complete(value);
    }
    void onError(Object error, StackTrace stack) {
    if (!completer.isCompleted) completer.completeError(error, stack);
    }
    for (var future in futures) {
    future.then(onValue, onError: onError);
    }
    return completer.future;
    }

    对上述例子来说,Future.any snapshot.data 将返回firstFuturesecondFuture中第一个返回结果的值

    Future.forEach

    为传入的每一个元素,顺序执行一个action

    static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) {
    var iterator = elements.iterator;
    return doWhile(() {
    if (!iterator.moveNext()) return false;
    var result = action(iterator.current);
    if (result is Future) return result.then(_kTrue);
    return true;
    });
    }

    这里边action是方法作为参数,头一次见这种形式语法还是在js中,当时就迷惑了很大一会儿,使用示例:

    Future.forEach(["one","two","three"], (element) {
    print(element);
    });

    Future.doWhile

    执行一个操作直到返回false

    Future.doWhile((){
    for(var i=0;i<5;i++){
    print("i => $i");
    if(i >= 3){
    return false;
    }
    }
    return true;
    });
    /// 结果打印到 3

    以上为Future中常用构造函数和方法

    在Widget中使用Future

    Flutter提供了配合Future显示的组件FutureBuilder,使用也很简单,伪代码如下:

    child: FutureBuilder(
    future: getFuture(),
    builder: (context, snapshot){
    if(!snapshot.hasData){
    return CircularProgressIndicator();
    } else if(snapshot.hasError){
    return _ErrorWidget("Error: ${snapshot.error}");
    } else {
    return _ContentWidget("Result: ${snapshot.data}")
    }
    }
    )

    Async-await

    使用

    这两个关键字提供了异步方法的同步书写方式,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因此,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。

    两条基本原则:

    • 定义一个异步方法,必须在方法体前声明 async

    • await关键字必须在async方法中使用

    首先,在要执行耗时操作的方法体前增加async:

    void main() async { ··· }

    然后,根据方法的返回类型添加Future修饰

    Future<void> main() async { ··· }

    现在就可以使用await关键字来等待这个future执行完毕

    print(await createOrderMessage());

    例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:

    var list = getCategoryList();
    list.then((value) => value[0].getCategorySubList(value[0].id))
    .then((subCategoryList){
    var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
    print(courseList);
    }).catchError((e) => (){
    print(e);
    });

    现在来看下使用async/await,事情变得简单了多少

    Future<void> main() async {
    await getCourses().catchError((e){
    print(e);
    });
    }
    Future<void> getCourses() async {
    var list = await getCategoryList();
    var subCategoryList = await list[0].getCategorySubList(list[0].id);
    var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
    print(courseList);
    }

    可以看到这样更加直观

    缺陷

    async/await 非常方便,但是还是有一些缺点需要注意

    因为它的代码看起来是同步的,所以是会阻塞后面的代码执行,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。

    这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求

    Future<String> getBannerList() async {
    return await Future.delayed(Duration(seconds: 3),(){
    return "banner list";
    });
    }

    Future<String> getHomeTabList() async {
    return await Future.delayed(Duration(seconds: 3),(){
    return "tab list";
    });
    }

    Future<String> getHomeMsgList() async {
    return await Future.delayed(Duration(seconds: 3),(){
    return "msg list";
    });
    }

    使用await编写很可能会写成这样,打印执行操作的时间

    Future<void> main2() async {
    var startTime = DateTime.now().second;
    await getBannerList();
    await getHomeTabList();
    await getHomeMsgList();
    var endTime = DateTime.now().second;
    print(endTime - startTime); // 9
    }

    在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:

    Future<void> main() async {
    var startTime = DateTime.now().second;
    var bannerList = getBannerList();
    var homeTabList = getHomeTabList();
    var homeMsgList = getHomeMsgList();

    await bannerList;
    await homeTabList;
    await homeMsgList;
    var endTime = DateTime.now().second;
    print(endTime - startTime); // 3
    }

    将三个Future存储在变量中,这样可以同时启动,最后打印时间仅为3s,所以在编写代码时,我们必须牢记这点,避免性能损耗。

    原理

    线程模型

    当一个Flutter应用或者Flutter Engine启动时,它会启动(或者从池中选择)另外三个线程,这些线程有些时候会有重合的工作点,但是通常,它们被称为UI线程GPU线程IO线程。需要注意一点这个UI线程并不是程序运行的主线程,或者说和其他平台上的主线程理解不同,通常的,Flutter将平台的主线程叫做"Platform thread"


    UI线程是所有的Dard代码运行的地方,例如framework和你的应用,除非你启动自己的isolates,否则Dart将永远不会运行在其他线程。平台线程是所有依赖插件的代码运行的地方。该线程也是native frameworks为其他任务提供服务的地方,一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个Platform thread为其提供服务。跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟Android/iOS UI相关的操作都必须在主线程进行相类似。

    Isolates是Dart中概念,本意是隔离,它的实现功能和thread类似,但是他们之间的实现又有着本质的区别,Isolote是独立的工作者,它们之间不共享内存,而是通过channel传递消息。Dart是单线程执行代码,Isolate提供了Dart应用可以更好的利用多核硬件的解决方案。

    事件循环

    单线程模型中主要就是在维护着一个事件循环(Event Loop) 与 两个队列(event queue和microtask queue)当Flutter项目程序触发如点击事件IO事件网络事件时,它们就会被加入到eventLoop中,eventLoop一直在循环之中,当主线程发现事件队列不为空时发现,就会取出事件,并且执行。

    microtask queue中事件优先于event queue执行,当有任务发送到microtask队列时,会在当前event执行完成后,阻塞当前event queue转而去执行microtask queue中的事件,这样为Dart提供了任务插队的解决方案。

    event queue的阻塞意味着app无法进行UI绘制,响应鼠标和I/O等事件,所以要谨慎使用,如下为流程图:


    这两个任务队列中的任务切换在某些方面就相当于是协程调度机制

    协程

    协程是一种协作式的任务调度机制,区别于操作系统的抢占式任务调度机制,它是用户态下面的,避免线程切换的内核态、用户态转换的性能开销。它让调用者自己来决定什么时候让出cpu,比操作系统的抢占式调度所需要的时间代价要小很多,后者为了恢复现场会保存相当多的状态(不仅包括进程上下文的虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态),并且会频繁的切换,以现在流行的大多数Linux机器来说,每一次的上下文切换要消耗大约1.2-1.5μs的时间,这是仅考虑直接成本,固定在单个核心以避免迁移的成本,未固定情况下,切换时间可达2.2μs


    对cpu来说这算一个很长的时间吗,一个很好的比较是memcpy,在相同的机器上,完成一个64KiB数据的拷贝需要3μs的时间,上下文的切换比这个操作稍微快一些


    协程和线程非常相似,是从异步执行任务的角度来看,而并不是从设计的实体角度像进程->线程->协程这样类似于细胞->原子核->质子中子这样的关系。可以理解为线程上执行的一段函数,用yield完成异步请求、注册回调/通知器、保存状态,挂起控制流、收到回调/通知、恢复状态、恢复控制流的所有过程

    多线程执行任务模型如图:


    线程的阻塞要靠系统间进程的切换,完成逻辑流的执行,频繁的切换耗费大量资源,而且逻辑流的执行数量严重依赖于程序申请到的线程的数量。

    协程是协同多任务的,这意味着协程提供并发性但不提供并行性,执行流模型图如下:


    协程可以用逻辑流的顺序去写控制流,协程的等待会主动释放cpu,避免了线程切换之间的等待时间,有更好的性能,逻辑流的代码编写和理解上也简单的很多

    但是线程并不是一无是处,抢占式线程调度器事实上提供了准实时的体验。例如Timer,虽然不能确保在时间到达的时候一定能够分到时间片运行,但不会像协程一样万一没有人让出时间片就永远得不到运行……

    总结

    • 同步与异步

    • Future提供了Flutter中异步代码链式编写方式

    • async-wait提供了异步代码的同步书写方式

    • Future的常用方法和FutureBuilder编写UI

    • Flutter中线程模型,四个线程

    • 单线程语言的事件驱动模型

    • 进程间切换和协程对比

    参考

    dart.cn/tutorials/l…

    dart.cn/codelabs/as…

    medium.com/dartlang/da…

    juejin.cn/post/684490…

    developer.mozilla.org/en-US/docs/…

    http://www.zhihu.com/question/19…

    http://www.zhihu.com/question/50…

    en.wikipedia.org/wiki/Asynch…

    eli.thegreenplace.net/2018/measur…


    作者:QiShare
    来源:juejin.cn/post/6987637272375984165

    收起阅读 »

    Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套

    这次的 Flutter 小技巧是 ListView 和 PageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListView 和 PageView 的三种嵌套模式带大家收获一些不一样的小技巧。正常嵌套最常见的...
    继续阅读 »

    这次的 Flutter 小技巧是 ListViewPageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListViewPageView 的三种嵌套模式带大家收获一些不一样的小技巧。

    正常嵌套

    最常见的嵌套应该就是横向 PageView 加纵向 ListView 的组合,一般情况下这个组合不会有什么问题,除非你硬是要斜着滑

    最近刚好遇到好几个人同时在问:“斜滑 ListView 容易切换到 PageView 滑动” 的问题,如下 GIF 所示,当用户在滑动 ListView 时,滑动角度带上倾斜之后,可能就会导致滑动的是 PageView 而不是 ListView


    虽然从我个人体验上并不觉得这是个问题,但是如果产品硬是要你修改,难道要自己重写 PageView 的手势响应吗?

    我们简单看一下,不管是 PageView 还是 ListView 它们的滑动效果都来自于 Scrollable ,而 Scrollable 内部针对不同方向的响应,是通过 RawGestureDetector 完成:

    • VerticalDragGestureRecognizer 处理垂直方向的手势

    • HorizontalDragGestureRecognizer 处理水平方向的手势

    所以简单看它们响应的判断逻辑,可以看到一个很有趣的方法 computeHitSlop根据 pointer 的类型确定当然命中需要的最小像素,触摸默认是 kTouchSlop (18.0)

    image-20220613103745974

    看到这你有没有灵光一闪:如果我们把 PageView 的 touchSlop 修改了,是不是就可以调整它响应的灵敏度? 恰好在 computeHitSlop 方法里,它可以通过 DeviceGestureSettings 来配置,而 DeviceGestureSettings 来自于 MediaQuery ,所以如下代码所示:

    body: MediaQuery(
     ///调高 touchSlop 到 50 ,这样 pageview 滑动可能有点点影响,
     ///但是大概率处理了斜着滑动触发的问题
     data: MediaQuery.of(context).copyWith(
         gestureSettings: DeviceGestureSettings(
       touchSlop: 50,
    )),
     child: PageView(
       scrollDirection: Axis.horizontal,
       pageSnapping: true,
       children: [
         HandlerListView(),
         HandlerListView(),
      ],
    ),
    ),

    小技巧一:通过嵌套一个 MediaQuery ,然后调整 gestureSettingstouchSlop 从而修改 PageView 的灵明度 ,另外不要忘记,还需要把 ListViewtouchSlop 切换会默认 的 kTouchSlop

    class HandlerListView extends StatefulWidget {
     @override
     _MyListViewState createState() => _MyListViewState();
    }
    class _MyListViewState extends State<HandlerListView> {
     @override
     Widget build(BuildContext context) {
       return MediaQuery(
         ///这里 touchSlop 需要调回默认
         data: MediaQuery.of(context).copyWith(
             gestureSettings: DeviceGestureSettings(
           touchSlop: kTouchSlop,
        )),
         child: ListView.separated(
           itemCount: 15,
           itemBuilder: (context, index) {
             return ListTile(
               title: Text('Item $index'),
            );
          },
           separatorBuilder: (context, index) {
             return const Divider(
               thickness: 3,
            );
          },
        ),
      );
    }
    }

    最后我们看一下效果,如下 GIF 所示,现在就算你斜着滑动,也很触发 PageView 的水平滑动,只有横向移动时才会触发 PageView 的手势,当然, 如果要说这个粗暴的写法有什么问题的话,大概就是降低了 PageView 响应的灵敏度

    xiehuabudong

    同方向 PageView 嵌套 ListView

    介绍完常规使用,接着来点不一样的,在垂直切换的 PageView 里嵌套垂直滚动的 ListView , 你第一感觉是不是觉得不靠谱,为什么会有这样的场景?

    对于产品来说,他们不会考虑你如何实现的问题,他们只会拍着脑袋说淘宝可以,为什么你不行,所以如果是你,你会怎么做?

    而关于这个需求,社区目前讨论的结果是:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理

    如果对实现逻辑分析没兴趣,可以直接看本小节末尾的 源码链接

    看到自己管理先不要慌,虽然要自己实现 PageViewListView 的手势分发,但是其实并不需要重写 PageViewListView ,我们可以复用它们的 Darg 响应逻辑,如下代码所示:

    • 通过 NeverScrollableScrollPhysics 禁止了 PageViewListView 的滚动效果

    • 通过顶部 RawGestureDetectorVerticalDragGestureRecognizer 自己管理手势事件

    • 配置 PageControllerScrollController 用于获取状态

    body: RawGestureDetector(
     gestures: <Type, GestureRecognizerFactory>{
       VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
               VerticalDragGestureRecognizer>(
          () => VerticalDragGestureRecognizer(),
          (VerticalDragGestureRecognizer instance) {
         instance
          ..onStart = _handleDragStart
          ..onUpdate = _handleDragUpdate
          ..onEnd = _handleDragEnd
          ..onCancel = _handleDragCancel;
      })
    },
     behavior: HitTestBehavior.opaque,
     child: PageView(
       controller: _pageController,
       scrollDirection: Axis.vertical,
       ///屏蔽默认的滑动响应
       physics: const NeverScrollableScrollPhysics(),
       children: [
         ListView.builder(
           controller: _listScrollController,
           ///屏蔽默认的滑动响应
           physics: const NeverScrollableScrollPhysics(),
           itemBuilder: (context, index) {
             return ListTile(title: Text('List Item $index'));
          },
           itemCount: 30,
        ),
         Container(
           color: Colors.green,
           child: Center(
             child: Text(
               'Page View',
               style: TextStyle(fontSize: 50),
            ),
          ),
        )
      ],
    ),
    ),

    接着我们看 _handleDragStart 实现,如下代码所示,在产生手势 details 时,我们主要判断:

    • 通过 ScrollController 判断 ListView 是否可见

    • 判断触摸位置是否在 ListIView 范围内

    • 根据状态判断通过哪个 Controller 去生产 Drag 对象,用于响应后续的滑动事件

      void _handleDragStart(DragStartDetails details) {

       if (_listScrollController?.hasClients == true &&
           _listScrollController?.position.context.storageContext != null) {
         ///获取 ListView 的 renderBox
         final RenderBox? renderBox = _listScrollController
             ?.position.context.storageContext
            .findRenderObject() as RenderBox;

         if (renderBox?.paintBounds
                .shift(renderBox.localToGlobal(Offset.zero))
                .contains(details.globalPosition) ==
             true) {
           _activeScrollController = _listScrollController;
           _drag = _activeScrollController?.position.drag(details, _disposeDrag);
           return;
        }
      }

       ///这时候就可以认为是 PageView 需要滑动
       _activeScrollController = _pageController;
       _drag = _pageController?.position.drag(details, _disposeDrag);
    }

    前面我们主要在触摸开始时,判断需要响应的对象时ListView 还是 PageView ,然后通过 _activeScrollController 保存当然响应对象,并且通过 Controller 生成用于响应手势信息的 Drag 对象。

    简单说:滑动事件发生时,默认会建立一个 Drag 用于处理后续的滑动事件,Drag 会对原始事件进行加工之后再给到 ScrollPosition 去触发后续滑动效果。

    接着在 _handleDragUpdate 方法里,主要是判断响应是不是需要切换到 PageView:

    • 如果不需要就继续用前面得到的 _drag?.update(details)响应 ListView 滚动

    • 如果需要就通过 _pageController 切换新的 _drag 对象用于响应

    void _handleDragUpdate(DragUpdateDetails details) {
     if (_activeScrollController == _listScrollController &&

         ///手指向上移动,也就是快要显示出底部 PageView
         details.primaryDelta! < 0 &&

         ///到了底部,切换到 PageView
         _activeScrollController?.position.pixels ==
             _activeScrollController?.position.maxScrollExtent) {
       ///切换相应的控制器
       _activeScrollController = _pageController;
       _drag?.cancel();

       ///参考 Scrollable 里
       ///因为是切换控制器,也就是要更新 Drag
       ///拖拽流程要切换到 PageView 里,所以需要 DragStartDetails
       ///所以需要把 DragUpdateDetails 变成 DragStartDetails
       ///提取出 PageView 里的 Drag 相应 details
       _drag = _pageController?.position.drag(
           DragStartDetails(
               globalPosition: details.globalPosition,
               localPosition: details.localPosition),
           _disposeDrag);
    }
     _drag?.update(details);
    }

    这里有个小知识点:如上代码所示,我们可以简单通过 details.primaryDelta 判断滑动方向和移动的是否是主轴

    最后如下 GIF 所示,可以看到 PageView 嵌套 ListView 同方向滑动可以正常运行了,但是目前还有个两个小问题,从图示可以看到:

    • 在切换之后 ListView 的位置没有保存下来

    • 产品要求去除 ListView 的边缘溢出效果

    7777777777777

    所以我们需要对 ListView 做一个 KeepAlive ,然后用简单的方法去除 Android 边缘滑动的 Material 效果:

    • 通过 with AutomaticKeepAliveClientMixinListView 在切换之后也保持滑动位置

    • 通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Scrollable 的边缘 Material 效果

    child: PageView(
     controller: _pageController,
     scrollDirection: Axis.vertical,
     ///去掉 Android 上默认的边缘拖拽效果
     scrollBehavior:
         ScrollConfiguration.of(context).copyWith(overscroll: false),


    ///对 PageView 里的 ListView 做 KeepAlive 记住位置
    class KeepAliveListView extends StatefulWidget {
     final ScrollController? listScrollController;
     final int itemCount;

     KeepAliveListView({
       required this.listScrollController,
       required this.itemCount,
    });

     @override
     KeepAliveListViewState createState() => KeepAliveListViewState();
    }

    class KeepAliveListViewState extends State<KeepAliveListView>
       with AutomaticKeepAliveClientMixin {
     @override
     Widget build(BuildContext context) {
       super.build(context);
       return ListView.builder(
         controller: widget.listScrollController,

         ///屏蔽默认的滑动响应
         physics: const NeverScrollableScrollPhysics(),
         itemBuilder: (context, index) {
           return ListTile(title: Text('List Item $index'));
        },
         itemCount: widget.itemCount,
      );
    }

     @override
     bool get wantKeepAlive => true;
    }

    所以这里我们有解锁了另外一个小技巧:通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Android 滑动到边缘的 Material 2效果,为什么说 Material2, 因为 Material3 上变了,具体可见: Flutter 3 下的 ThemeExtensions 和 Material3

    000000000

    本小节源码可见: github.com/CarGuo/gsy_…

    同方向 ListView 嵌套 PageView

    那还有没有更非常规的?答案是肯定的,毕竟产品的小脑袋,怎么会想不到在垂直滑动的 ListView 里嵌套垂直切换的 PageView 这种需求。

    有了前面的思路,其实实现这个逻辑也是异曲同工:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理,不同的就是手势方法分发的差异。

    RawGestureDetector(
             gestures: <Type, GestureRecognizerFactory>{
               VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
                       VerticalDragGestureRecognizer>(
                  () => VerticalDragGestureRecognizer(),
                  (VerticalDragGestureRecognizer instance) {
                 instance
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel;
              })
            },
             behavior: HitTestBehavior.opaque,
             child: ListView.builder(
                   ///屏蔽默认的滑动响应
                   physics: NeverScrollableScrollPhysics(),
                   controller: _listScrollController,
                   itemCount: 5,
                   itemBuilder: (context, index) {
                     if (index == 0) {
                       return Container(
                         height: 300,
                         child: KeepAlivePageView(
                           pageController: _pageController,
                           itemCount: itemCount,
                        ),
                      );
                    }
                     return Container(
                         height: 300,
                         color: Colors.greenAccent,
                         child: Center(
                           child: Text(
                             "Item $index",
                             style: TextStyle(fontSize: 40, color: Colors.blue),
                          ),
                        ));
                  }),
          )

    同样是在 _handleDragStart 方法里,这里首先需要判断:

    • ListView 如果已经滑动过,就不响应顶部 PageView 的事件

    • 如果此时 ListView 处于顶部未滑动,判断手势位置是否在 PageView 里,如果是响应 PageView 的事件

      void _handleDragStart(DragStartDetails details) {
       if (_listScrollController.offset > 0) {
         _activeScrollController = _listScrollController;
         _drag = _listScrollController.position.drag(details, _disposeDrag);
         return;
      }
       if (_pageController.hasClients) {
         ///获取 PageView
         final RenderBox renderBox =
             _pageController.position.context.storageContext.findRenderObject()
                 as RenderBox;

         ///判断触摸范围是不是在 PageView
         final isDragPageView = renderBox.paintBounds
            .shift(renderBox.localToGlobal(Offset.zero))
            .contains(details.globalPosition);

         ///如果在 PageView 里就切换到 PageView
         if (isDragPageView) {
           _activeScrollController = _pageController;
           _drag = _activeScrollController.position.drag(details, _disposeDrag);
           return;
        }
      }

       ///不在 PageView 里就继续响应 ListView
       _activeScrollController = _listScrollController;
       _drag = _listScrollController.position.drag(details, _disposeDrag);
    }

    接着在 _handleDragUpdate 方法里,判断如果 PageView 已经滑动到最后一页,也将滑动事件切换到 ListView


    当然,同样还有 KeepAlive 和去除列表 Material 边缘效果,最后运行效果如下 GIF 所示。

    22222222222

    本小节源码可见:github.com/CarGuo/gsy_…

    最后再补充一个小技巧:如果你需要 Flutter 打印手势竞技的过程,可以配置 debugPrintGestureArenaDiagnostics = true;来让 Flutter 输出手势竞技的处理过程

    import 'package:flutter/gestures.dart';
    void main() {
     debugPrintGestureArenaDiagnostics = true;
     runApp(MyApp());
    }

    image-20220613115808538

    最后

    最后总结一下,本篇介绍了如何通过 Darg 解决各种因为嵌套而导致的手势冲突,相信大家也知道了如何利用 ControllerDarg 来快速自定义一些滑动需求,例如 ListView 联动 ListView 的差量滑动效果:




    44444444444444


    作者:恋猫de小郭
    来源:juejin.cn/post/7116267156655833102

    收起阅读 »

    Flutter极简状态管理库Creator

    我之前一直用riverpod来做状态管理,最近发现了一个新发布的库,尝试了一下,非常简洁好用,给大家推荐一下。叫做Creator(地址),刚发布几天就有几十个👍。这个库的API跟riverpod很接近,但是更加简洁清晰,基本上没有什么上手难度。先看一下它的co...
    继续阅读 »

    我之前一直用riverpod来做状态管理,最近发现了一个新发布的库,尝试了一下,非常简洁好用,给大家推荐一下。叫做Creator(地址),刚发布几天就有几十个👍。

    这个库的API跟riverpod很接近,但是更加简洁清晰,基本上没有什么上手难度。

    先看一下它的counter例子:

    // 定义状态
    final counter = Creator.value(0);
    Widget build(BuildContext context) {
    return Column(
      children: [
        // 响应状态
        Watcher((context, ref, _) => Text('${ref.watch(counter)}')),
        TextButton(
          // 更新状态
          onPressed: () => context.ref.update<int>(counter, (count) => count + 1),
          child: const Text('+1'),
        ),
      ],
    );
    }

    它的核心概念极其简单,只提供两种creator:

    • Creator 产生一系列的 T

    • Emitter 产生一系列的 Future<T>

    这里T可以是任何类型,甚至可以是Widget。然后它把所有的creator都组织成一个有向图(叫做Ref)。

    还是举一个官网的例子吧。可以在DartPad上跑,显示摄氏温度或者华氏温度:



    // repo.dart

    // 假装调用一个后端API。
    Future<int> getFahrenheit(String city) async {
    await Future.delayed(const Duration(milliseconds: 100));
    return 60 + city.hashCode % 20;
    }
    // logic.dart

    // 简单的creator
    final cityCreator = Creator.value('London');
    final unitCreator = Creator.value('Fahrenheit');

    // 可以像Iterable/Stream那样使用 map, where, reduce 之类的.
    final fahrenheitCreator = cityCreator.asyncMap(getFahrenheit);

    // 组合不同的creator,产生新的业务逻辑。
    final temperatureCreator = Emitter<String>((ref, emit) async {
    final f = await ref.watch(fahrenheitCreator);
    final unit = ref.watch(unitCreator);
    emit(unit == 'Fahrenheit' ? '$f F' : '${f2c(f)} C');
    });

    // 摄氏华氏温度转换
    int f2c(int f) => ((f - 32) * 5 / 9).round();
    // main.dart

    Widget build(BuildContext context) {
    return Watcher((context, ref, _) =>
        Text(ref.watch(temperatureCreator.asyncData).data ?? 'loading'));
    }
    ... context.ref.set(cityCreator, 'Pairs'); // 会调用后端API
    ... context.ref.set(unitCreator, 'Celsius'); // 不会调用后端API

    可以看出,当用户改变所选城市之后, 状态会沿着图中的箭头传导,一直传到最后的Creator<Widget>,从而更新UI。

    我觉得这个有向图的设计还是非常独特的,很好理解,也很简单。组织比较复杂的业务逻辑的时候非常方便。

    这个库的核心代码才500行,感兴趣的同学可以去看官方文档和代码。

    欢迎讨论!

    作者:Jay_Guo
    来源:juejin.cn/post/7107433326054473736

    收起阅读 »

    在uni-app中使用微软的文字转语音服务

    前言尝试过各种TTS的方案,一番体验下来,发现微软才是这个领域的王者,其Azure文本转语音服务的转换出的语音效果最为自然,但Azure是付费服务,注册操作付费都太麻烦了。但在其官网上竟然提供了一个完全体的演示功能,能够完完整整的体验所有角色语音,说话风格.....
    继续阅读 »

    前言

    尝试过各种TTS的方案,一番体验下来,发现微软才是这个领域的王者,其Azure文本转语音服务的转换出的语音效果最为自然,但Azure是付费服务,注册操作付费都太麻烦了。但在其官网上竟然提供了一个完全体的演示功能,能够完完整整的体验所有角色语音,说话风格...


    但就是不能下载成mp3文件,所以有一些小伙伴逼不得已只好通过转录电脑的声音来获得音频文件,但这样太麻烦了。其实,能在网页里看到听到的所有资源,都是解密后的结果。也就是说,只要这个声音从网页里播放出来了,我们必然可以找到方法提取到音频文件。

    本文就是记录了这整个探索实现的过程,请尽情享用~

    本文大部分内容写于今年年初一直按在手里未发布,我深知这个方法一旦公之于众,可能很快会迎来微软的封堵,甚至直接取消网页体验的入口和相关接口。

    解析Azure官网的演示功能

    使用Chrome浏览器打开调试面板,当我们在Azure官网中点击播放功能时,可以从network标签中监控到一个wss://的请求,这是一个websocket的请求。


    两个参数

    在请求的URL中,我们可以看到有两个参数分别是AuthorizationX-ConnectionId


    有意思的是,第一个参数就在网页的源码里,使用axios对这个Azure文本转语音的网址发起get请求就可以直接提取到


    const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

    const reg = /token: \"(.*?)\"/;

    if(reg.test(res.data)){
      const token = RegExp.$1;
    }

    通过查看发起请求的JS调用栈,加入断点后再次点击播放



    可以发现第二个参数X-ConnectionId来自一个createNoDashGuid的函数

    this.privConnectionId = void 0 !== t ? t : s.createNoDashGuid(),

    这就是一个uuid v4格式的字符串,nodash就是没有-的意思。

    三次发送

    请求时URL里的两个参数已经搞定了,我们继续分析这个webscoket请求,从Message标签中可以看到


    每次点击播放时,都向服务器上报了三次数据,明显可以看出来三次上报数据各自的作用

    第一次的数据:SDK版本,系统信息,UserAgent

    Path: speech.config
    X-RequestId: 818A1E398D8D4303956D180A3761864B
    X-Timestamp: 2022-05-27T16:45:02.799Z
    Content-Type: application/json

    {"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript"},"os":{"platform":"Browser/MacIntel","name":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","version":"5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36"}}}

    第二次的数据:转语音输出配置,从outputFormat可以看出来,最终的音频格式为audio-24khz-160kbitrate-mono-mp3,这不就是我们想要的mp3文件吗?!

    Path: synthesis.context
    X-RequestId: 091963E8C7F342D0A8E79125EA6BB707
    X-Timestamp: 2022-05-27T16:48:43.340Z
    Content-Type: application/json

    {"synthesis":{"audio":{"metadataOptions":{"bookmarkEnabled":false,"sentenceBoundaryEnabled":false,"visemeEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-24khz-160kbitrate-mono-mp3"},"language":{"autoDetection":false}}}

    第三次的数据:要转语音的文本信息和角色voice name,语速rate,语调pitch,情感等配置

    Path: ssml
    X-RequestId: 091963E8C7F342D0A8E79125EA6BB707
    X-Timestamp: 2022-05-27T16:48:49.594Z
    Content-Type: application/ssml+xml

    <speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US"><voice name="zh-CN-XiaoxiaoNeural"><prosody rate="0%" pitch="0%">我叫大帅,一个热爱编程的老程序猿</prosody></voice></speak>

    接收的二进制消息

    既然从前三次上报的信息已经看出来返回的格式就是mp3文件了,那么我们是不是把所有返回的二进制数据合并就可以拼接成完整的mp3文件了呢?答案是肯定的!

    每次点击播放后接收的所有来自websocket的消息的最后一条,都有明确的结束标识符



    turn.end代表转换结束!

    用Node.js实现它

    既然都解析出来了,剩下的就是在Node.js中重新实现这个过程。

    两个参数

    1. Authorization,直接通过axios的get请求抓取网页内容后通过正则表达式提取

    const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

    const reg = /token: \"(.*?)\"/;

    if(reg.test(res.data)){
      const Authorization = RegExp.$1;
    }
    1. X-ConnectionId,直接使用uuid库即可

    //npm install uuid
    const { v4: uuidv4 } = require('uuid');

    const XConnectionId = uuidv4().toUpperCase();

    创建WebSocket连接

    //npm install nodejs-websocket
    const ws = require("nodejs-websocket");

    const url = `wss://eastus.tts.speech.microsoft.com/cognitiveservices/websocket/v1?Authorization=${Authorization}&X-ConnectionId=${XConnectionId}`;
    const connect = ws.connect(url);

    三次发送

    第一次发送

    function getXTime(){
      return new Date().toISOString();
    }

    const message_1 = `Path: speech.config\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript","os":{"platform":"Browser/Linux x86_64","name":"Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0","version":"5.0 (X11)"}}}}`;

    connect.send(message_1);

    第二次发送

    const message_2 = `Path: synthesis.context\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"synthesis":{"audio":{"metadataOptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-16khz-32kbitrate-mono-mp3"}}}`;

    connect.send(message_2);

    第三次发送

    const SSML = `
      <speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US">
          <voice name="zh-CN-XiaoxiaoNeural">
              <mstts:express-as style="general">
                  <prosody rate="0%" pitch="0%">
                  我叫大帅,一个热爱编程的老程序猿
                  </prosody>
              </mstts:express-as>
          </voice>
      </speak>
      `

    const message_3 = `Path: ssml\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/ssml+xml\r\n\r\n${SSML}`

    connect.send(message_3);

    接收二进制消息拼接mp3

    当三次发送结束后我们通过connect.on('binary')监听websocket接收的二进制消息。

    创建一个空的Buffer对象final_data,然后将每一次接收到的二进制内容拼接到final_data里,一旦监听到普通文本消息中包含Path:turn.end标识时则将final_data写入创建一个mp3文件中。

    let final_data=Buffer.alloc(0);
    connect.on("text", (data) => {
      if(data.indexOf("Path:turn.end")>=0){
          fs.writeFileSync("test.mp3",final_data);
          connect.close();
      }
    })
    connect.on("binary", function (response) {
      let data = Buffer.alloc(0);
      response.on("readable", function () {
          const newData = response.read()
          if (newData)data = Buffer.concat([data, newData], data.length+newData.length);
      })
      response.on("end", function () {
          const index = data.toString().indexOf("Path:audio")+12;
          final_data = Buffer.concat([final_data,data.slice(index)]);
      })
    });

    这样我们就成功的保存出了mp3音频文件,连Azure官网都不用打开!

    命令行工具

    我已经将整个代码打包成一个命令行工具,使用非常简单

    npm install -g mstts-js
    mstts -i 文本转语音 -o ./test.mp3

    已全部开源: github.com/ezshine/mst…

    在uni-app中使用

    新建一个云函数

    新建一个云函数,命名为mstts


    由于mstss-js已经封装好了,只需要在云函数中npm install mstts-js然后require即可,代码如下

    'use strict';
    const mstts = require('mstts-js')

    exports.main = async (event, context) => {
      const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');
     
      //res为buffer格式
    });

    下载播放mp3文件

    要在uniapp中播放这个mp3格式的文件,有两种方法

    方法1. 先上传到云存储,通过云存储地址访问

    exports.main = async (event, context) => {
      const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');
     
      //res为buffer格式
      var uploadRes = await uniCloud.uploadFile({
          cloudPath: "xxxxx.mp3",
          fileContent: res
      })
       
      return uploadRes.fileID;
    });

    前端用法:

    uniCloud.callFunction({
      name:"mstts",
      success:(res)=>{
          const aud = uni.createInnerAudioContext();
          aud.autoplay = true;
          aud.src = res;
          aud.play();
      }
    })
    • 优点:云函数安全

    • 缺点:文件上传到云存储不做清理机制的话会浪费空间

    方法2. 利用云函数的URL化+集成响应来访问

    这种方法就是直接将云函数的响应体变成一个mp3文件,直接通过audio.src赋值即可访问`

    exports.main = async (event, context) => {
    const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');

    return {
    mpserverlessComposedResponse: true,
    isBase64Encoded: true,
    statusCode: 200,
    headers: {
    'Content-Type': 'audio/mp3',
    'Content-Disposition':'attachment;filename=\"temp.mp3\"'
    },
    body: res.toString('base64')
    }
    };

    前端用法:

    const aud = uni.createInnerAudioContext();
    aud.autoplay = true;
    aud.src = 'https://ezshine-274162.service.tcloudbase.com/mstts';
    aud.play();
    • 优点:用起来很简单,无需保存文件到云存储

    • 缺点:URL化后的云函数如果没有安全机制,被抓包后可被其他人肆意使用

    作者:大帅老猿
    来源:juejin.cn/post/7103720862221598757

    收起阅读 »

    节日献礼:Flutter图片库重磅开源!

    去年,闲鱼新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完...
    继续阅读 »

    背景:

    去年,闲鱼新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如我们在相册中,需要在图片库之外再搭建图片通道。

    简介:

    PowerImage 是一个充分利用 native 原生图片库能力、高扩展性的flutter图片库。我们巧妙地将外接纹理与 ffi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。

    能力特点:

    • 支持加载 ui.Image 能力。在基于外接纹理的方案中,使用方无法拿到真正的 ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。

    • 支持图片预加载能力。正如原生precacheImage一样。这在某些对图片展示速度要求较高的场景下非常有用。

    • 新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。

    • 支持模拟器。在 flutter-1.23.0-18.1.pre之前的版本,模拟器无法展示 Texture Widget。

    • 完善自定义图片类型通道。解决业务自定义图片获取诉求。

    • 完善的异常捕获与收集。

    • 支持动图。(来自淘特的PR)

    Flutter 原生方案:

    在介绍新方案开始之前,先简单回忆一下 flutter 原生图片方案。


    原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilderloadingBuilder,最终在图片加载成功后,会 rebuildRawImageRawImage 会通过 RenderImage 来绘制,整个绘制的核心是 ImageInfo 中的 ui.Image

    • Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。

    • ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。

    • ImageStream:图片资源加载的对象。

    在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?

    新一代方案:

    我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。

    FFI:

    正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是 ui.Image。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image

    首先 native 侧先获取必要的参数(以 iOS 为例):

    _rowBytes = CGImageGetBytesPerRow(cgImage);
    CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
    CFDataRef rawDataRef = CGDataProviderCopyData(dataProvider);
    _handle = (long)CFDataGetBytePtr(rawDataRef);
    NSData *data = CFBridgingRelease(rawDataRef);
    self.data = data;
    _length = data.length;

    dart 侧拿到后

    @override  FutureOr createImageInfo(Map map) {
    Completer completer = Completer();
    int handle = map['handle'];
    int length = map['length'];
    int width = map['width'];
    int height = map['height'];
    int rowBytes = map['rowBytes'];
    ui.PixelFormat pixelFormat = ui.PixelFormat.values[map['flutterPixelFormat'] ?? 0];
    pointer.asTypedList(length);
    ui.decodeImageFromPixels(pixels, width, height, pixelFormat,(ui.Image image) {
    ImageInfo imageInfo = ImageInfo(image: image);
    completer.complete(imageInfo);     //释放 native 内存
    PowerImageLoader.instance.releaseImageRequest(options);
    }, rowBytes: rowBytes);
    return completer.future;
    }

    我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过 ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。

    这里有两个优化方向:

    1. 解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。

    2. 与 flutter 官方讨论,尝试从内部减少这次内存拷贝。

    FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。

    Texture:

    Texture 方案与原生结合有一些难度,这里涉及到没有 ui.Image 只有 textureId。这里有几个问题需要解决:

    问题一:Image Widget 需要 ui.Image 去 build RawImage 从而绘制,这在本文前面的Flutter 原生方案介绍中也提到了。 问题二:ImageCache 依赖 ImageInfo 中 ui.Image 的宽高进行 cache 大小计算以及缓存前的校验。 问题三:native 侧 texture 生命周期管理

    都有解决方案:

    问题一:通过自定义 Image 解决,透出 imageBuilder 来让外部自定义图片 widget 问题二:为 Texture 自定义 ui.image,如下:

    import 'dart:typed_data';
    import 'dart:
    ui'
    as ui show Image;
    import 'dart:ui';
    class TextureImage implements ui.Image {
        int _width;
        int _height;
        int textureId;
       TextureImage(this.textureId, int width, int height)     : _width = width,       _height = height;
        @override void dispose() {
        // TODO: implement dispose }
         @override int get height => _height;
         @override Future
    toByteData(     {ImageByteFormat format = ImageByteFormat.rawRgba}) {  
             // TODO: implement toByteData  
                 throw UnimplementedError();
         }
         @override int get width => _width;
    }

    这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。 实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。这个问题可以具体看 @皓黯 的 ISSUE 与 PR。

    问题三:关于 native 侧感知 flutter image 释放时机的问题

    修改的 ImageCache 释放如下(部分代码):

    typedef void HasRemovedCallback(dynamic key, dynamic value);

    class RemoveAwareMap<K, V> implements Map<K, V> {
    HasRemovedCallback hasRemovedCallback;
    ...
    }
    //------
    final RemoveAwareMap<Object, _PendingImage> _pendingImages = RemoveAwareMap<Object, _PendingImage>();
    //------
    void hasImageRemovedCallback(dynamic key, dynamic value) {
    if (key is ImageProviderExt) {
    waitingToBeCheckedKeys.add(key);
    }
    if (isScheduledImageStatusCheck) return;
    isScheduledImageStatusCheck = true;
    //We should do check in MicroTask to avoid if image is remove and add right away
    scheduleMicrotask(() {
    waitingToBeCheckedKeys.forEach((key) {
    if (!_pendingImages.containsKey(key) &&
    !_cache.containsKey(key) &&
    !_liveImages.containsKey(key)) {
    if (key is ImageProviderExt) {
    key.dispose();
    }
    }
    });
    waitingToBeCheckedKeys.clear();
    isScheduledImageStatusCheck = false;
    });
    }

    整体架构:

    我们将两种解决方案非常优雅地结合在了一起:


    我们抽象出了 PowerImageProvider ,对于 external(ffi)、texture,分别生产自己的 ImageInfo 即可。它将通过对 PowerImageLoader 的调用,提供统一的加载与释放能力。

    蓝色实线的 ImageExt 即为自定义的 Image Widget,为 texture 方式透出了 imageBuilder。

    蓝色虚线 ImageCacheExt 即为 ImageCache 的扩展,仅在 flutter < 2.2.0 版本才需要,它将提供 ImageCache 释放时机的回调。

    这次,我们也设计了超强的扩展能力。除了支持网络图、本地图、flutter 资源、native 资源外,我们提供了自定义图片类型的通道,flutter 可以传递任何自定义的参数组合给 native,只要 native 注册对应类型 loader,比如「相册」这种场景,使用方可以自定义 imageType 为 album ,native 使用自己的逻辑进行加载图片。有了这个自定义通道,甚至图片滤镜都可以使用 PowerImage 进行展示刷新。

    除了图片类型的扩展,渲染类型也可进行自定义。比如在上面 ffi 中说的,为了降低内存拷贝带来的峰值问题,使用方可以在 flutter 侧进行解码,当然这需要 native 图片库提供解码前的数据。

    数据:

    FFI vs Texture:


    机型:iPhone 11 Pro;图片:300 张网络图;行为:在listView中手动滚动到底部再滚动到顶部;native Cache20 maxMemoryCount; flutter Cache30MBflutter version 2.5.3; release 模式下

    这里有两个现象:

    FFI:   186MB波动Texture: 194MB波动

    在 2.5.3 版本中,Texture 方案与 FFI,在内存水位上差异不大,内存波动上面与 flutter 1.22 结论相反。

    图中棋格图,为打开 checkerboardRasterCacheImages 后所展示,可以看出,ffi方案会缓存整个cell,而texture方案,只有cell中的文字被缓存,RasterCache 会使得 ffi 在流畅度方面会有一定优势。

    滚动流畅性分析:


    设备: Android OnePlus 8t,CPU和GPU进行了锁频。case: GridView每行4张图片,300张图片,从上往下,再从下往上,滑动幅度从500,1000,1500,2000,2500,5轮滑动。重复20次。方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑数据,获取TimeLine数据并分析。
    复制代码

    结论:

    • UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI方式波动比较大。

    • Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize了,其他方式加载的是原图。

    更精简的代码:


    dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。

    FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。

    单测:


    为了保证核心代码的稳定性,我们有着较为完善的单测,行覆盖率接近95%。

    关于开源:

    我们期待通过社区的力量让 PowerImage 更加完善与强大,也希望 PowerImage 能为大家在工程研发中带来收益。

    Issues:

    关于 issue,我们希望大家在使用 PowerImage 遇到问题与诉求时,积极交流,提出 issue 时尽可能提供详细的信息,以减少沟通成本。在提出 issue 前,请确保已阅读 readme。


    对于 bug 的 issue,我们自定义了模板(Bug report),可以方便地填一些必要的信息。其他类型则可以选择 Open a blank issue

    我们每周会花部分时间统一处理 issues,也期待大家的讨论与 PR。

    PR:

    为了保持 PowerImage 核心功能的稳定性,我们有着完善的单测,行覆盖率达到了 95%(power_image库)。

    在提交PR时,请确保所提交的代码被单测覆盖到,并且涉及到的单测代码请同时提交。


    得益于 Github 的 Actions 能力,我们在主分支 push 代码、对主分支进行 PR 操作时,都会触发 flutter test任务,只有单测通过才可合入。

    未来:

    开源是 PowerImage 的开始,而不是结束,PowerImage 可做的事情还有很多,有趣而丰富。比如第一个 issue 中描述的 loadingBuilder 如何实现?比如 ffi 方案如何支持动图?再比如Kotlin和Swift···

    PowerImage 未来将持续演进,在当前 texture 方案与 ffi 方案共存的情况下,伴随着 flutter 本身的迭代,我们将更倾向于向 ffi 发展,正如在上文的对比中, ffi 方案可以天然享用 raster cache 所带来的流畅度的优势。

    PowerImage 也会持续追随 flutter 的脚步,以始终贴合原生的设计理念,不断进步,我们希望更多的同学加入进来,共同成长。

    其他四个Flutter开源项目: 闲鱼技术**公众号-闲鱼开源

    PowerImage相关链接:

    GitHub:(✅star🌟)

    github.com/alibaba/pow…

    Flutter pub:(✅like👍)

    pub.dev/packages/po…

    作者:闲鱼技术——新宿

    收起阅读 »

    为了看Flutter到底有没有人用我竟然

    首先,我在vivo应用市场中,下载了4月11日软件排行榜中的所有App,总计230个,再加上平时用的比较多的一些App,总共270个App,作为我们的统计基数。github.com/zhaobozhen/…github.com/sugood/apka…App列...
    继续阅读 »

    Flutter这个东西出来这么久了,到底市场占有率怎么样呢?为了让大家了解这一真实数据,也为了让大家了解当前Flutter在各大App中的使用情况,我今天下载了几百个App,占了手机将近80G空间,就为了得出一个结论——Flutter,到底有没有人用。

    首先,我在vivo应用市场中,下载了4月11日软件排行榜中的所有App,总计230个,再加上平时用的比较多的一些App,总共270个App,作为我们的统计基数。

    检测方法,我使用LibChecker来查看App是否有使用Flutter相关的so。

    github.com/zhaobozhen/…

    除了使用LibChecker之外,还有其它方案也可以,例如使用shell指令——zipinfo。

    github.com/sugood/apka…

    Apk本质上也是一种压缩包,所以,通过zipinfo指令并进行grep,就可以很方便的获取了,同时,如果配合一下爬虫来爬取应X宝的Apk下载地址,就可以成为一个全自动化的脚本分析工具,这里没这么强的需求,所以就不详细做了。

    App列表

    我们来看下,我都下载了多少App。

    Screenshot_2022-04-12-09-45-44-13_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-47-46_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-49-64_92b64b2a7aa6eb3771ed6e18d0029815
    Screenshot_2022-04-12-09-45-51-75_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-53-78_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-55-92_92b64b2a7aa6eb3771ed6e18d0029815
    Screenshot_2022-04-12-09-45-58-12_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-00-27_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-02-34_92b64b2a7aa6eb3771ed6e18d0029815
    Screenshot_2022-04-12-09-46-04-34_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-06-60_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-09-14_92b64b2a7aa6eb3771ed6e18d0029815

    这些App基本上已经覆盖了应用商店各个排行榜里的Top软件,所以应该还是比较具有代表性和说服力的。

    下面我们就用LibChecker来看下,这些App里面到底有多少使用了Flutter。

    统计结果

    Screenshot_2022-04-12-09-51-25-73_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-34-94_708f76cdf2c7449ff16a8486e0e036f6
    Screenshot_2022-04-12-09-51-39-66_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-44-41_708f76cdf2c7449ff16a8486e0e036f6
    Screenshot_2022-04-12-09-51-49-75_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-58-19_708f76cdf2c7449ff16a8486e0e036f6
    Screenshot_2022-04-12-09-52-04-67_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-52-13-25_708f76cdf2c7449ff16a8486e0e036f6

    已经使用Flutter的App共52个,占全体样本的19.2%,作为参考,统计了下RN相关的App,共有45个,占全体样本的16.6%,可以说,Flutter已经超过RN成为跨平台方案的首选。

    在52个使用Flutter的App中:

    • 腾讯系:QQ邮箱、微信、QQ同步助手、蓝盾、腾讯课堂、QQ浏览器、微视、企业微信、腾讯会议

    • 百度系:百度网盘、百度输入法

    • 阿里系:优酷视频、哈啰出行、淘特、酷狗直播、阿里1688、学习强国、钉钉、淘宝、闲鱼

    • 其它大厂:链家、转转、智联招聘、拍拍贷、哔哩哔哩漫画、网易有道词典、爱奇艺、考拉海购、携程旅行、微博、Soul、艺龙旅行、唯品会、飞猪旅行

    从上面的数据来看,各大厂都对Flutter有使用,头条系未列出的原因是,目前好像只有头条系大规模使用了Flutter的动态化加载方案,所以原始包内找不到Flutter相关的so,所以未检出(猜测是这样,具体可以请头条系的朋友指出,根据上次头条的分享,内部有90+App在使用Flutter)。

    不过这里要注意的 ,这里并不是选取的大家常用的一些APP来做测试的,而是直接选取的排行榜,如果直接用常用APP来测试,那比例可能更高,大概统计了下,估计在60%左右。

    不过大厂里面,京东没有使用Flutter我还是比较意外的,看了下京东的几个App,目前还是以RN为主作为跨平台的方案。这跟其它很多大厂一样,它们不仅使用了Flutter,RN也还可以检出,这也从侧面说明了,各个厂商,对跨平台的方案探索,从未停止。

    所以,总结一下,目前使用Flutter的团队的几个特定:

    • 创业公司:快速试错、快速开发,像Blued、夸克这也的

    • 大厂:大厂的话题永远是效率,如何利用跨平台技术来提高开发效率,是它们引入Flutter的根本原因

    • 创新型业务:例如B漫、淘特、Soul这类没有太多历史包袱的新业务App,可以利用Flutter进行极为高效的开发

    所以,整体在知乎上吵「Flutter被抛弃了」、「Flutter要崛起了」,有什么意义呢?所有的争论都抵不过数据来的真实。

    嘴上说着不要,身体倒是很诚实。

    希望这份数据能给你一些帮助。


    作者:xuyisheng
    来源:juejin.cn/post/7088864824284676110


    收起阅读 »

    【 Flutter 极限测试】连续 1000000 次 setState 会怎么样

    测试描述可能很多人会认为,每次的 State#setState 都会触发当前状态类的 build 方法重新构建。但真的是这样吗,你真的了解 Flutter 界面的更新流程吗?本篇文章将做一个极限测试,看一下连续触发 1000000 次 setState 会发生...
    继续阅读 »
    测试描述

    可能很多人会认为,每次的 State#setState 都会触发当前状态类的 build 方法重新构建。但真的是这样吗,你真的了解 Flutter 界面的更新流程吗?

    本篇文章将做一个极限测试,看一下连续触发 1000000setState 会发生什么?是连续触发 1000000 次屏幕更新,导致界面卡死,还是无事发生?用你的眼睛来见证吧!


    1、测试代码说明

    如下所示,在默认案例基础上添加了两个蓝色文字,点击时分别触发如下的 _increment1_setState1000000 。其中 _setState1000000 是遍历执行 1000000setState


    void _increment1() {
     setState(() {
       _counter++;
    });
    }

    void _setState1000000() {
     for (int i = 0; i < 1000000; i++) {
       setState(() {
         _counter++;
      });
    }
    }

    2、运行结果

    如下是在 profile 模式下,网页调试工具中的测试结果。可以看出即使连续触发了 1000000 次的 steState ,也不会有 1000000 次的帧触发来更新界面。也就是说,并非每次的 steState 方法触发时,都会进行重新构建,所以,你真的懂 State#steState 吗?



    3. 源码调试分析

    如下,在 State#setState 源码中可以看出,它只做了两件事:

    • 触发入参回调 fn 。

    • 执行持有元素的 markNeedsBuild 方法。


    这里 1121 行的 fn() 做了什么,不用多说了吧。就是 setState 入参的那个自加方法。



    此时该 State 中持有的 _element 对象类型是 StatefulEmement ,也就是 MyHomePage 组件创建的元素。



    Elememt#markNeedsBuild 方法中没有一个非常重要的判断,那就是下面 4440 行 中,如果 dirty 已经是 true 时,则直接返回,不会执行接下来的方法。如果 dirtyfalse ,那接下来会置为 true

    另外,owner.scheduleBuildFor 用于收集脏元素,以及申请新帧的触发。这就是为什么连续执行 1000000stateState 时,该元素不会加入脏表 1000000 次,不会触发 1000000 帧的原因。


    总的来说, State#setState 的核心作用就是把持有的元素标脏申请新帧调度。而只有新帧到来,执行完构建之后,元素的 dirty 才会置为 false 。也就是说,两帧之间,无论调用多少次 setState ,都只会触发一次, 元素标脏申请新帧调度 。这就是为什么连续触发 1000000 次,并无大事发生的原因。


    作者:张风捷特烈
    来源:https://juejin.cn/post/7091471603774521352

    收起阅读 »

    【Flutter】Dart语法之List & Map

    【Flutter】学习笔记——Dart中的List & Map的使用 list列表,相当于 OC 中的 NSArray 数组,分为可变和不可变两种。 map键值对,相当于 OC 中的 NSDicti...
    继续阅读 »
    【Flutter】学习笔记——Dart中的List & Map的使用



    • list列表,相当于 OC 中的 NSArray 数组,分为可变不可变两种。

    • map键值对,相当于 OC 中的 NSDictionary 字典,也分为可变不可变两种。



    1. list数组



    list默认都是可变的,列表中可以添加不同数据类型的数据。



    1.1 可变list

    void main() { 
    // 直接 list创建
    List a = ["1", 2, "3.0", 4.0];
    print(a);

    // var 创建
    var list = [1, 2, "zjp", 3.0];
    print(list);
    }

    运行结果如下:


    image.png


    1.2 常用方法

    获取&修改指定下标数据:


    // 直接获取指定下标数据 
    print(list[3]);
    // 直接修改指定下标数据
    list[3] = "reno";

    插入数据:


    list.insert(1, "hellow"); // list.insert(index, element)
    print(list);

    删除数据:


    list.remove(1); // list.remove(element)
    print(list);

    清空所有数据:


    list.clear();
    print(list);

    运行结果如下:


    image.png


    1.3 排序和截取

    void main() {
    List b = [3, 4, 5, 8, 6, 7];
    // 排序
    b.sort();
    print(b);
    // 截取
    print(b.sublist(1, 3));
    }

    运行结果如下:


    image.png


    1.4 不可变list


    不可变的 list 需要使用const修饰。



    void main() {
    List b = const [3, 4, 5, 8, 6, 7];
    b[3] = 10; // 报错
    }

    不可变list不能修改其元素值,否则会报错


    image.png


    2. map键值对



    map默认也是可变的。



    2.1 可变map

    void main() {
    Map a = {"a": 1, "b": 2};
    print(a);

    var a1 = {"a1": 1, "a2": 2};
    print(a1);
    }

    运行结果如下:


    image.png


    2.2 常用方法

    获取&修改指定下标数据:


    // 直接获取指定下标数据 
    print(a["a"]);
    // 直接修改指定下标数据
    a["a"] = "aa";
    print(a["a"]);

    获取map长度


    print(a.length);

    获取map所有的key


    print(a.keys);

    获取map所有的value


    print(a.values);

    运行结果如下:


    image.png


    2.3 不可变map


    不可变的 map 也是使用const修饰。



    void main() {
    Map a = const {"a": 1, "b": 2};
    a["a"] = 10; // 报错
    }

    不可变map不能修改其元素值,否则也会报错


    image.png


    3. list转map


    void main() {
    List b = ["zjp", "reno"];
    print(b.asMap());
    }

    运行结果如下:



    作者:忻凯同学
    链接:https://juejin.cn/post/7085642932761395237
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Flutter实现心碎的感觉

    前言 继续动画探索,今天用Flutter制作一个心碎的感觉,灵感来源于今天的股市,哎,心哇凉哇凉的。废话不多说,开始。 效果图先上: 1、绘制一个心 首先我们使用两段三阶贝塞尔曲线制作一个心型,这里因为需要实现心碎的效果,所以我们需要将心的两段用两段路径pat...
    继续阅读 »

    前言


    继续动画探索,今天用Flutter制作一个心碎的感觉,灵感来源于今天的股市,哎,心哇凉哇凉的。废话不多说,开始。


    效果图先上:


    1、绘制一个心


    首先我们使用两段三阶贝塞尔曲线制作一个心型,这里因为需要实现心碎的效果,所以我们需要将心的两段用两段路径path进行绘制出来,效果:


    image.png

    绘制代码:


    canvas.translate(size.width / 2, size.height / 2);
    Paint paint = Paint();
    paint
    ..style = PaintingStyle.stroke
    ..strokeWidth = 2
    ..color = Colors.black87;
    Path path = Path();
    path.moveTo(0, 0);
    path.cubicTo(-200, -80, -60, -240, 0, -140);
    path.close();
    Path path2 = Path();
    canvas.save();
    canvas.drawPath(
    path,
    paint
    ..color = Colors.red
    ..style = PaintingStyle.stroke);
    canvas.restore();
    path2.cubicTo(200, -80, 60, -240, 0, -140);
    path2.close();
    canvas.drawPath(
    path2,
    paint..color = Colors.black87);

    2、绘制心的裂痕


    我们看到心确实分成两半了,但是中间还缺少裂痕,接下来我们就绘制心碎的裂痕,也很简单,在两段路径path闭合前进行绘制线,效果:


    image.png


    绘制代码:


    path.relativeLineTo(-10, 30);
    path.relativeLineTo(20, 5);
    path.relativeLineTo(-20, 30);
    path.relativeLineTo(20, 20);
    path.relativeLineTo(-10, 20);
    path.relativeLineTo(10, 10);

    path2.relativeLineTo(-10, 30);
    path2.relativeLineTo(20, 5);
    path2.relativeLineTo(-20, 30);
    path2.relativeLineTo(20, 20);
    path2.relativeLineTo(-10, 20);
    path2.relativeLineTo(10, 10);

    OK,我们已经看到心已经有了裂痕,如何心碎,只需将画布进行翻转一定角度即可,这里我们将画布翻转45°,看下效果:

    左边:
    image.png

    右边:
    image.png


    3、加入动画


    已经有心碎的感觉了,接下来加入动画元素让心碎的过程动起来。

    思路: 我们可以想一下,心碎的过程是什么样子,心的颜色慢慢变灰,心然后慢慢裂开,下方的动画运动曲线看起来更符合心碎的过程,里面有不舍,不甘,但最后心还是慢慢的碎了。
    xinsui.gif


    我们把画笔进行填充将这个动画加入进来看下最终效果。

    df5dbcbb-f36b-4f05-9613-0e94149d888f.gif
    是不是心碎了一地。


    知识点: 这里我们需要找到红色和灰色的RGB色值,通过Color.fromRGBO(r, g, b, opacity)方法赋值颜色的色值。然后通过动画值改变RGB的值即可。
    这里我使用的色值是:

    红色:Color.fromRGBO(255, 0, 0, 1)

    灰色:Color.fromRGBO(169, 169, 169, 1)


    最终代码:


    class XinSui extends StatefulWidget {
    const XinSui({Key? key}) : super(key: key);

    @override
    _XinSuiState createState() => _XinSuiState();
    }

    class _XinSuiState extends State<XinSui> with SingleTickerProviderStateMixin {
    late AnimationController _controller =
    AnimationController(vsync: this, duration: Duration(milliseconds: 4000))
    ..repeat();
    late CurvedAnimation cure =
    CurvedAnimation(parent: _controller, curve: Curves.bounceInOut);

    late Animation<double> animation =
    Tween<double>(begin: 0.0, end: 1.0).animate(cure);

    @override
    Widget build(BuildContext context) {
    return Container(
    child: CustomPaint(
    size: Size(double.infinity, double.infinity),
    painter: _XinSuiPainter(animation),
    ),
    );
    }

    @override
    void dispose() {
    _controller.dispose();
    super.dispose();
    }
    }

    class _XinSuiPainter extends CustomPainter {
    Animation<double> animation;

    _XinSuiPainter(this.animation) : super(repaint: animation);

    @override
    void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    Paint paint = Paint();
    paint
    ..style = PaintingStyle.stroke
    ..strokeWidth = 2
    ..color = Colors.black87;
    Path path = Path();
    path.moveTo(0, 0);
    path.cubicTo(-200, -80, -60, -240, 0, -140);
    path.relativeLineTo(-10, 30);
    path.relativeLineTo(20, 5);
    path.relativeLineTo(-20, 30);
    path.relativeLineTo(20, 20);
    path.relativeLineTo(-10, 20);
    path.relativeLineTo(10, 10);
    path.close();
    Path path2 = Path();
    canvas.save();
    canvas.rotate(-pi / 4 * animation.value);
    canvas.drawPath(
    path,
    paint
    ..color = Colors.red
    ..color = Color.fromRGBO(
    255 - (86 * animation.value).toInt(),
    (animation.value * 169).toInt(),
    (animation.value * 169).toInt(),
    1)

    ..style = PaintingStyle.fill);
    canvas.restore();
    path2.cubicTo(200, -80, 60, -240, 0, -140);
    path2.relativeLineTo(-10, 30);
    path2.relativeLineTo(20, 5);
    path2.relativeLineTo(-20, 30);
    path2.relativeLineTo(20, 20);
    path2.relativeLineTo(-10, 20);
    path2.relativeLineTo(10, 10);
    path2.close();
    canvas.rotate(pi / 4 * animation.value);
    canvas.drawPath(
    path2,paint);
    }
    @override
    bool shouldRepaint(covariant _XinSuiPainter oldDelegate) {
    return oldDelegate.animation != animation;
    }
    }

    小结


    动画曲线Curves配合绘制可以实现很多好玩的东西,这个需要勤加练习方能掌握,仅将此心碎献给今天受伤的股民朋友们(ಥ﹏ಥ)


    作者:老李code
    来源:https://juejin.cn/post/7090457954415017991
    收起阅读 »

    Flutter实现一个牛顿摆

    前言牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。 - 知识点:绘制、动画曲线、多动画状态更新 效果图: 1、绘制静态效果 首先我们需要把线和小圆球绘制出来,...
    继续阅读 »

    前言

    • 牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。

    - 知识点:绘制、动画曲线、多动画状态更新


    效果图:


    638bdf30-7b2a-4c3e-ad14-94da128b68f1.gif


    1、绘制静态效果


    首先我们需要把线和小圆球绘制出来,对于看过我之前文章的小伙伴来说这个就很简单了,效果图:

     
    image.png

    关键代码:


    // 小圆球半径
    double radius = 6;

    /// 小球圆心和直线终点一致
    //左边小球圆心
    Offset offset = Offset(20, 60);
    //右边小球圆心
    Offset offset2 = Offset(20 * 6 * 8, 60);

    Paint paint = Paint()
    ..color = Colors.black87
    ..strokeWidth = 2;

    /// 绘制线
    canvas.drawLine(Offset.zero, Offset(90, 0), paint);
    canvas.drawLine(Offset(20, 0), offset, paint);
    canvas.drawLine(
    Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
    canvas.drawLine(
    Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
    canvas.drawLine(
    Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
    canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);

    /// 绘制小圆球
    canvas.drawCircle(offset, radius, paint);
    canvas.drawCircle(Offset(20 + radius * 2, 60), radius, paint);
    canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
    canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
    canvas.drawCircle(offset2, radius, paint);

    2、加入动画


    思路: 我们可以看到5个小球一共2个小球在运动,左边小球运动一个来回之后传递给右边小球,右边小球开始运动,右边一个来回再传递给左边开始,也就是左边运动周期是:0-1-0,正向运动一次,反向再运动一次,这样就是一个周期,右边也是一样,左边运动完传递给右边,右边运动完传递给左边,这样就简单实现了牛顿摆的效果。


    两个关键点


    小球运动路径: 小球的运动路径是一个弧度,以竖线的起点为圆心,终点为半径,那么我们只需要设置小球运动至最高点的角度即可,通过角度就可计算出小球的坐标点。


    运动曲线: 当然我们知道牛顿摆小球的运动曲线并不是匀速的,他是有一个加速减速过程的,撞击之后,小球先加速然后减速达到最高点速度为0,之后速度再从0慢慢加速进行撞击小球,周而复始。

    下面的运动曲线就是先加速再减速,大概符合牛顿摆的运动曲线。我们就使用这个曲线看看效果。

     
    ndb.gif

    完整源码:


    class OvalLoading extends StatefulWidget {
    const OvalLoading({Key? key}) : super(key: key);

    @override
    _OvalLoadingState createState() => _OvalLoadingState();
    }

    class _OvalLoadingState extends State
    with TickerProviderStateMixin
    {
    // 左边小球
    late AnimationController _controller =
    AnimationController(vsync: this, duration: Duration(milliseconds: 300))
    ..addStatusListener((status) {
    if (status == AnimationStatus.completed) {
    _controller.reverse(); //反向执行 1-0
    } else if (status == AnimationStatus.dismissed) {
    _controller2.forward();
    }
    })
    ..forward();
    // 右边小球
    late AnimationController _controller2 =
    AnimationController(vsync: this, duration: Duration(milliseconds: 300))
    ..addStatusListener((status) {
    // dismissed 动画在起始点停止
    // forward 动画正在正向执行
    // reverse 动画正在反向执行
    // completed 动画在终点停止
    if (status == AnimationStatus.completed) {
    _controller2.reverse(); //反向执行 1-0
    } else if (status == AnimationStatus.dismissed) {
    // 反向执行完毕左边小球执行
    _controller.forward();
    }
    });
    late var cure =
    CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
    late var cure2 =
    CurvedAnimation(parent: _controller2, curve: Curves.easeOutCubic);

    late Animation animation = Tween(begin: 0.0, end: 1.0).animate(cure);

    late Animation animation2 =
    Tween(begin: 0.0, end: 1.0).animate(cure2);

    @override
    Widget build(BuildContext context) {
    return Container(
    margin: EdgeInsetsDirectional.only(top: 300, start: 150),
    child: CustomPaint(
    size: Size(100, 100),
    painter: _OvalLoadingPainter(
    animation, animation2, Listenable.merge([animation, animation2])),
    ),
    );
    }

    @override
    void dispose() {
    _controller.dispose();
    _controller2.dispose();
    super.dispose();
    }
    }

    class _OvalLoadingPainter extends CustomPainter {
    double radius = 6;
    final Animation animation;
    final Animation animation2;
    final Listenable listenable;

    late Offset offset; // 左边小球圆心
    late Offset offset2; // 右边小球圆心

    final double lineLength = 60; // 线长

    _OvalLoadingPainter(this.animation, this.animation2, this.listenable)
    : super(repaint: listenable) {
    offset = Offset(20, lineLength);
    offset2 = Offset(20 * radius * 8, lineLength);
    }

    // 摆动角度
    double angle = pi / 180 * 30; // 30°

    @override
    void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
    ..color = Colors.black87
    ..strokeWidth = 2;

    // 左边小球 默认坐标 下方是90度 需要+pi/2
    var dx = 20 + 60 * cos(pi / 2 + angle * animation.value);
    var dy = 60 * sin(pi / 2 + angle * animation.value);
    // 右边小球
    var dx2 = 20 + radius * 8 - 60 * cos(pi / 2 + angle * animation2.value);
    var dy2 = 60 * sin(pi / 2 + angle * animation2.value);

    offset = Offset(dx, dy);
    offset2 = Offset(dx2, dy2);

    /// 绘制线
    canvas.drawLine(Offset.zero, Offset(90, 0), paint);
    canvas.drawLine(Offset(20, 0), offset, paint);
    canvas.drawLine(
    Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
    canvas.drawLine(
    Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
    canvas.drawLine(
    Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
    canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);

    /// 绘制球
    canvas.drawCircle(offset, radius, paint);
    canvas.drawCircle(
    Offset(20 + radius * 2, 60),
    radius,
    paint);

    canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
    canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
    canvas.drawCircle(offset2, radius, paint);
    }
    @override
    bool shouldRepaint(covariant _OvalLoadingPainter oldDelegate) {
    return oldDelegate.listenable != listenable;
    }
    }

    去掉线的效果:

    b6a23e8a-9c4a-4aa8-9518-46a53b756a88.gif


    总结


    本文展示了实现牛顿摆的原理,其实并不复杂,关键点就是小球的运动轨迹和运动速度曲线,如果用到项目中当做Loading还有很多优化的空间,比如加上小球影子、修改小球颜色或者把小球换成好玩的图片等等操作会看起来更好看一点,本篇只展示了实现的原理,希望对大家有一些帮助~


    作者:老李code
    来源:https://juejin.cn/post/7090123854135164935 收起阅读 »

    在Flutter上优雅的请求网络数据

    当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。 解决的问题通用异常处理请求资源状态可见(加载成功,加载中,加载失败)通用重试逻辑 效果展示 为了演示请求失败的处理,特意在wa...
    继续阅读 »

    当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。


    解决的问题

    • 通用异常处理
    • 请求资源状态可见(加载成功,加载中,加载失败)
    • 通用重试逻辑


    效果展示


    为了演示请求失败的处理,特意在wanApi抛了两次错
    LBeZ5Q.gif


    正文


    搜索一下关于flutter网络封装的多半都是dio相关的封装,简单的封装、复杂的封装百花齐放,思路都是工具类的封装。今天换一个思路来实现,引入repository对数据层进行操作,在repository里使用dio作为一个数据源供repository使用,需要使用数据就对repository进行操作不直接调用数据源(在repositoy里是不允许直接操作数据源的)。用WanAndroid的接口写个示例demo


    定义数据源


    使用retrofit作为数据源,感兴趣的小伙伴可以看下retrofit这个库

    class _WanApi implements WanApi {
    _WanApi(this._dio, {this.baseUrl}) {
    baseUrl ??= 'https://www.wanandroid.com';
    }

    final Dio _dio;

    String? baseUrl;

    @override
    Future<BannerModel> getBanner() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    final _result = await _dio.fetch<Map<String, dynamic>>(
    _setStreamType<BannerModel>(
    Options(method: 'GET', headers: _headers, extra: _extra)
    .compose(_dio.options, '/banner/json',
    queryParameters: queryParameters, data: _data)
    .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = BannerModel.fromJson(_result.data!);
    return value;
    }

    @override
    Future<TopArticleModel> getTopArticle() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    final _result = await _dio.fetch<Map<String, dynamic>>(
    _setStreamType<TopArticleModel>(
    Options(method: 'GET', headers: _headers, extra: _extra)
    .compose(_dio.options, '/article/top/json',
    queryParameters: queryParameters, data: _data)
    .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = TopArticleModel.fromJson(_result.data!);
    return value;
    }

    @override
    Future<PopularSiteModel> getPopularSite() async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{};
    final _headers = <String, dynamic>{};
    final _data = <String, dynamic>{};
    final _result = await _dio.fetch<Map<String, dynamic>>(
    _setStreamType<PopularSiteModel>(
    Options(method: 'GET', headers: _headers, extra: _extra)
    .compose(_dio.options, '/friend/json',
    queryParameters: queryParameters, data: _data)
    .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = PopularSiteModel.fromJson(_result.data!);
    return value;
    }

    RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
    if (T != dynamic &&
    !(requestOptions.responseType == ResponseType.bytes ||
    requestOptions.responseType == ResponseType.stream)) {
    if (T == String) {
    requestOptions.responseType = ResponseType.plain;
    } else {
    requestOptions.responseType = ResponseType.json;
    }
    }
    return requestOptions;
    }
    }



    repository封装


    Resource是封装的资源加载状态类,用于包装资源


    enum ResourceState { loading, failed, success }

    class Resource {
    final T? data;
    final ResourceState state;
    final dynamic error;
    Resource._({required this.state, this.error, this.data});

    factory Resource.failed(dynamic error) {
    return Resource._(state: ResourceState.failed, error: error);
    }

    factory Resource.success(T data) {
    return Resource._(state: ResourceState.success, data: data);
    }

    factory Resource.loading() {
    return Resource._(state: ResourceState.loading);
    }

    bool get isLoading => state == ResourceState.loading;
    bool get isSuccess => state == ResourceState.success;
    bool get isFailed => state == ResourceState.failed;
    }

    接下来我们在Repository里使用WanApi来封装,我们通过流的方式返回了资源加载的状态可供View层根据状态展示不同的界面,使用try-catch保证网络请求的健壮性


    class WanRepository extends BaseRepository {
    late WanApi wanApi = GetInstance().find();
    ///获取首页所需的所有数据
    Stream> homeData() async* {
    //加载中
    yield Resource.loading();
    try {
    var result = await Future.wait([
    wanApi.getBanner(),
    wanApi.getPopularSite(),
    wanApi.getTopArticle()
    ]);
    final BannerModel banner = result[0];
    final PopularSiteModel site = result[1];
    final TopArticleModel article = result[2];
    //加载成功
    yield Resource.success(
    HomeDataMapper(site.data, banner.data, article.data));
    } catch (e) {
    //加载失败
    yield Resource.failed(e);
    }
    }
    }

    咋一看感觉没啥问题细思之下问题很多,每一个请求还多了try-catch以外那么多的模板方法,实际开发中只写try包裹的内容才符合摸鱼佬的习惯。ok,我们把模板方法提取出来到一个公共方法里去,就变成了这样:


    class WanRepository extends BaseRepository {
    late WanApi wanApi = GetInstance().find();
    ///获取首页所需的所有数据
    Stream> homeData() async* {
    ///定义加载函数
    loadHomeData()async*{
    var result = await Future.wait([
    wanApi.getBanner(),
    wanApi.getPopularSite(),
    wanApi.getTopArticle()
    ]);
    final BannerModel banner = result[0];
    final PopularSiteModel site = result[1];
    final TopArticleModel article = result[2];
    //加载成功
    yield Resource.success(
    HomeDataMapper(site.data, banner.data, article.data));
    }
    ///将加载函数放在一个包装器里执行
    yield* MyWrapper.customStreamWrapper(loadHomeData);
    }
    }

    得益于Dart中函数可以作为参数传递,所以我们可以定义一个包装方法,入参是具体业务的函数,出参和业务函数一致,在这个方法里可以处理各种异常,甚至可以实现通用的请求重试(只需要在失败的时候弹窗提醒用户重试,获得认可后再次执行function就可以了,更关键的是此时状态管理里对repository的调用依旧是完整的,也就是说这是一个通用的重试功能)
    包装器代码:


    class MyWrapper {
    //流的方式
    static Stream> customStreamWrapper(
    Stream> Function() function,
    {bool retry = false}) async* {
    yield Resource.loading();
    try {
    var result = function.call();
    await for(var data in result)
    {
    yield data;
    }
    } catch (e) {
    //重试代码
    if (retry) {
    var toRetry = await Get.dialog(const RequestRetryDialog());
    if (toRetry == true) {
    yield* customStreamWrapper(function,retry: retry);
    }
    else
    {
    yield Resource.failed(e);
    }
    } else {
    yield Resource.failed(e);
    }
    }
    }
    }

    其实就是把相同的地方封装成一个通用方法,不同的地方单独拎出来编写,然后作为一个参数传到包装器里执行。显然这样的方法却不够优雅,每次在写repository的时候都得创建一个函数在里面编写请求数据的逻辑然后交给包装器执行。我们肯定希望repository里代码长成这个样子:


    @Repo()
    abstract class WanRepository extends BaseRepository {
    late WanApi wanApi = GetInstance().find();

    ///获取首页所需的所有数据
    @ProxyCall()
    @Retry()
    Stream> homeData() async* {
    var result = await Future.wait(
    [wanApi.getBanner(), wanApi.getPopularSite(), wanApi.getTopArticle()]);
    final BannerModel banner = result[0];
    final PopularSiteModel site = result[1];
    final TopArticleModel article = result[2];
    yield Resource.success(
    HomeDataMapper(site.data, banner.data, article.data));
    }
    }

    是的没错,最终的repository就长这个样子,你只需要在类上打个注解@Repo在需要代理调用的方法上注解@ProxyCall,运行 flutter pub run build_runner build 就可以生成对应的包装代码:


    // GENERATED CODE - DO NOT MODIFY BY HAND

    part of 'wan_repository.dart';

    // **************************************************************************
    // RepositoryGenerator
    // **************************************************************************

    class WanRepositoryImpl = WanRepository with _WanRepository;

    mixin _WanRepository on WanRepository {
    @override
    Stream> homeData() {
    return MyWrapper.customStreamWrapper(() => super.homeData(), retry: true);
    }
    }

    结语


    感谢你的阅读,这只是一个网络请求封装的思路不是最优解,但希望给你带来新思考


    附demo地址:gitee.com/cysir/examp…


    flutter版本:2.8


    作者:贼不走空
    链接:https://juejin.cn/post/7088223867017101343
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Flutter bottomSheet 高度自适应及溢出处理

    最近在创建 bottomSheet的时候遇到一个问题:弹窗的高度无法根据其内容自适应 先放上显示弹窗的代码,如下: Future<T?> showSheet<T>( BuildContext context, Widg...
    继续阅读 »

    最近在创建 bottomSheet的时候遇到一个问题:弹窗的高度无法根据其内容自适应



    先放上显示弹窗的代码,如下:


    Future<T?> showSheet<T>(
    BuildContext context,
    Widget body, {
    bool scrollControlled = false,
    Color bodyColor = Colors.white,
    EdgeInsets? bodyPadding,
    BorderRadius? borderRadius,
    }) {
    const radius = Radius.circular(16);
    borderRadius ??= const BorderRadius.only(topLeft: radius, topRight: radius);
    bodyPadding ??= const EdgeInsets.all(20);
    return showModalBottomSheet(
    context: context,
    elevation: 0,
    backgroundColor: bodyColor,
    shape: RoundedRectangleBorder(borderRadius: borderRadius),
    barrierColor: Colors.black.withOpacity(0.25),
    // A处
    constraints: BoxConstraints(
    maxHeight: MediaQuery.of(context).size.height - MediaQuery.of(context).viewPadding.top),
    isScrollControlled: scrollControlled,
    builder: (ctx) => Padding(
    padding: EdgeInsets.only(
    left: bodyPadding!.left,
    top: bodyPadding.top,
    right: bodyPadding.right,
    // B处
    bottom: bodyPadding.bottom + MediaQuery.of(ctx).viewPadding.bottom,
    ),
    child: body,
    ));
    }


    其中,A处、B处的作用就是,让弹窗的内容始终显示在安全区域内



    高度自适应问题


    首先,我们在弹窗中显示点内容:


    showSheet(context, Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: const [
    Text('这是第一行'),
    ],
    ));

    效果如下图所示:




    此时,我们只需要将显示内容的代码改为如下:


    showSheet(context, Column(
    mainAxisSize: MainAxisSize.min, // 这一行是关键所在
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: const [
    Text('这是第一行'),
    ],
    ));

    现在的效果图如下:




    现在我们可以看到,弹窗的高度已经根据内容自适应了。


    内容溢出问题


    前面的解决方式,仅在内容高度小于默认高度时有效。当内容过多,高度大于默认高度时,就会出现溢出警告,如下图所示:




    此时,我们该怎么办呢?


    答案是:运用 showModalBottomSheet 的 isScrollControlled 参数,将其设置为true即可,代码如下:


    showSheet(context, Column(
    mainAxisSize: MainAxisSize.min,
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: const [
    Text('这是第一行'),
    Text('这是很长长..(此处省略若干字)..的一段话,')
    ],
    ), scrollControlled: true); // 这一行用于告诉系统,弹窗的内容完全由我们自己管理

    此时,效果图如下:



    showSheet 补充说明


    对前面showSheet代码中,A处、B处的进一步说明:


    A处:如果不对内容的高度进行限制,则内容会显示在状态栏之后,而引起用户交互问题。如下图所示:



    B处:如果不加 MediaQuery.of(ctx).viewPadding.bottom 这一句,则内容有可能会显示在底部横条的下方,此时也不利于交互


    最终版本图样


    内容较少(高度跟随内容自适应):



    内容很多(顶部、底部均显示在安全区域内):



    作者:JameLee
    链接:https://juejin.cn/post/7081475476765556749
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    2022了,来体验下 flutter web

    前言 flutter从 17年 推出,18年12月 开始发布 1.0 版本,2021年3月 发布 2.0 增加了对桌面和 web 应用的支持。 最大特点是基于skia实现自绘引擎,使用dart语言开发,既支持JIT(just in time: 即时编译)又支持...
    继续阅读 »

    前言


    flutter从 17年 推出,18年12月 开始发布 1.0 版本,2021年3月 发布 2.0 增加了对桌面和 web 应用的支持。
    最大特点是基于skia实现自绘引擎,使用dart语言开发,既支持JIT(just in time: 即时编译)又支持AOT(ahead of time: 提前编译),开发阶段使用JIT模式提高时效性,同时在发布阶段使用AOT模式提高编译性能。
    作为前端的话,还是更关注flutter web的支持情况。为了体验flutter web,特意用flutter写了个小游戏来看编译后的代码在web上运行如何。


    开始之前


    早在3年前的 19年初 1.0 出来没多久的时候就尝试用flutter来写一些常见的菜单注册登录等页面demo来,那时候flutter的生态还在发展中,除了官方提供的一些解决方案,三方的一些包很多都不成体系,应用范围较小,由于当时是抱着前端的固有思路来尝鲜flutter,flutter 刚发展起来,轮子远没有那么多,发现写起来远没有Vue、React 这类生态成熟的框架写起来舒服,除了 widget 组件多,写起UI来可以直接看文档写完很方便外,网络请求,路由管理、状态管理(这些像vue有axios/vue-router/vuex)用官方的方法写起来相当麻烦(也可能是我不会用,对新手不友好),维护起来就更麻烦了。


    过去3年了,再看flutter,2.0版本发布也快一年了,当再次想用flutter写个demo的时候,发现了社区已经出现了一些经过几年发展的provider、getx之类的状态管理框架,能帮助新手快速入门,用了 getx 感觉是个脚手架,又不仅仅是脚手架,简直是大而全的轮子,状态管理、路由管理一应俱全,生成的目录结构清晰,你只需要去填充 UI 和处理数据。用法也很简单,对新手很友好。


    flutter + getx 写一个小游戏


    既然选好了那就用 getx 生成项目目录,开始开发,选用了一个很常见的小游戏:数字华容道,功能也简单。 项目地址


    项目可以打包成原生应用,也可以打包成 web 应用


    数字华容道web版


    flutter web 渲染模式


    不同的渲染器在不同场景下各有优势,因此 Flutter 同时支持以下两种渲染模式:


    HTML 渲染器: 结合了 HTML 元素、CSS、Canvas 和 SVG。该渲染模式的下载文件体积较小。
    CanvasKit 渲染器: 渲染效果与 Flutter 移动和桌面端完全一致,性能更好,widget 密度更高,但增加了约 2MB 的下载文件体积。
    为了针对每个设备的特性优化您的 Flutter web 应用,渲染模式默认设置为自动。这意味着您的应用将在移动浏览器上使用 HTML 渲染器运行,在桌面浏览器上使用 CanvasKit 渲染器运行。官方文档




    使用 HTML 渲染


    flutter run -d chrome --web-renderer html
    复制代码

    使用 HTML,CSS,Canvas 和 SVG 元素来渲染,应用的大小相对较小,元素数量多,请求都是http2


    元素如下



    请求如下



    使用 CanvasKit 渲染


    CanvasKit 是以 WASM 为编译目标的Web平台图形绘制接口,其目标是将 Skia 的图形 API 导出到 Web 平台。


    flutter run -d chrome --web-renderer canvaskit
    复制代码

    默认 CanvasKit 渲染,元素数量比html少很多,就是需要请求 canvaskit.wasm,该文件大小7MB左右、默认在 unpkg.com 国内加载速度慢,可以将文件放到国内 cdn 以提升请求效率



    元素如下



    请求如下,部分还使用了http3



    小结


    flutter web 通过编译成浏览器可运行的代码,经实践来看,性能还是有些问题,不过如果是单单想要写SPA,那恐怕还是js首选。目前来说flutter的生态经过几年的发展已经有了很多开源轮子,但要说稳定性还无法击败js,要不要用 flutter web 就要根据实际需求来决定了。


    作者:c_137Summer
    链接:https://juejin.cn/post/7072280090066812941
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    面试官:知道 Flutter 生命周期?下周来入职!

    作为一名移动端开发工程师,刚接触 Flutter 的时候,一定会有这样的疑问:Flutter 的生命周期是怎么样的?是如何处理生命周期的?我的 onCreate()[Android] 在哪里?viewDidLoad()[iOS] 呢? 我的业务逻辑应该放在哪里...
    继续阅读 »

    作为一名移动端开发工程师,刚接触 Flutter 的时候,一定会有这样的疑问:Flutter 的生命周期是怎么样的?是如何处理生命周期的?我的 onCreate()[Android] 在哪里?viewDidLoad()[iOS] 呢? 我的业务逻辑应该放在哪里处理?初始化数据呢?希望看了这篇文章后,可以对你有一点小小的帮助。


    安卓


    如果你是一名安卓开发工程师,那么对于 Activity 生命周期肯定不陌生



    • onCreate

    • onStart

    • onResume

    • onPause

    • onStop

    • onDestroy


    android_life_cycle


    iOS


    如果你是一名 iOS 开发工程师,那么 UIViewController 的生命周期肯定也已经很了解了。



    • viewDidLoad

    • viewWillAppear

    • viewDidAppear

    • viewWillDisappear

    • viewDidDisappear

    • viewDidUnload


    ios_life_cycle


    Flutter


    知道了 Android 和 iOS 的生命周期,那么 Flutter 呢?有和移动端对应的生命周期函数么?如果之前你对 Flutter 有一点点了解的话,你会发现 Flutter 中有两个主要的 Widget:StatelessWidget(无状态)StatefulWidget(有状态)。本篇文章我们主要来介绍下 StatefulWidget,因为它有着和 Android 和 iOS 相似的生命周期。


    StatelessWidget


    无状态组件是不可变的,这意味着它们的属性不能变化,所有的值都是最终的。可以理解为将外部传入的数据转化为界面展示的内容,只会渲染一次。
    对于无状态组件生命周期只有 build 这个过程。无状态组件的构建方法通常只在三种情况下会被调用:小组件第一次被插入树中,小组件的父组件改变其配置,以及它所依赖的 InheritedWidget 发生变化时。


    StatefulWidget


    有状态组件持有的状态可能在 Widget 生命周期中发生变化,是定义交互逻辑和业务逻辑。可以理解为具有动态可交互的内容界面,会根据数据的变化进行多次渲染。实现一个 StatefulWidget 至少需要两个类:



    • 一个是 StatefulWidget 类。

    • 另一个是 Sate 类。StatefulWidget 类本身是不可变的,但是 State 类在 Widget 生命周期中始终存在。StatefulWidget 将其可变的状态存储在由 createState 方法创建的 State 对象中,或者存储在该 State 订阅的对象中。


    StatefulWidget 生命周期



    • createState:该函数为 StatefulWidget 中创建 State 的方法,当 StatefulWidget 被创建时会立即执行 createState。createState 函数执行完毕后表示当前组件已经在 Widget 树中,此时有一个非常重要的属性 mounted 被置为 true。

    • initState:该函数为 State 初始化调用,只会被调用一次,因此,通常会在该回调中做一些一次性的操作,如执行 State 各变量的初始赋值、订阅子树的事件通知、与服务端交互,获取服务端数据后调用 setState 来设置 State。

    • didChangeDependencies:该函数是在该组件依赖的 State 发生变化时会被调用。这里说的 State 为全局 State,例如系统语言 Locale 或者应用主题等,Flutter 框架会通知 widget 调用此回调。类似于前端 Redux 存储的 State。该方法调用后,组件的状态变为 dirty,立即调用 build 方法。

    • build:主要是返回需要渲染的 Widget,由于 build 会被调用多次,因此在该函数中只能做返回 Widget 相关逻辑,避免因为执行多次而导致状态异常。

    • reassemble:主要在开发阶段使用,在 debug 模式下,每次热重载都会调用该函数,因此在 debug 阶段可以在此期间增加一些 debug 代码,来检查代码问题。此回调在 release 模式下永远不会被调用。

    • didUpdateWidget:该函数主要是在组件重新构建,比如说热重载,父组件发生 build 的情况下,子组件该方法才会被调用,其次该方法调用之后一定会再调用本组件中的 build 方法。

    • deactivate:在组件被移除节点后会被调用,如果该组件被移除节点,然后未被插入到其他节点时,则会继续调用 dispose 永久移除。

    • dispose:永久移除组件,并释放组件资源。调用完 dispose 后,mounted 属性被设置为 false,也代表组件生命周期的结束。


    不是生命周期但是却非常重要的几个概念


    下面这些并不是生命周期的一部分,但是在生命周期中起到了很重要的作用。



    • mounted:是 State 中的一个重要属性,相当于一个标识,用来表示当前组件是否在树中。在 createState 后 initState 前,mounted 会被置为 true,表示当前组件已经在树中。调用 dispose 时,mounted 被置为 false,表示当前组件不在树中。

    • dirty:表示当前组件为脏状态,下一帧时将会执行 build 函数,调用 setState 方法或者执行 didUpdateWidget 方法后,组件的状态为 dirty。

    • clean:与 dirty 相对应,clean 表示组件当前的状态为干净状态,clean 状态下组件不会执行 build 函数。


    stateful_widget_lifecycle 生命周期流程图


    上图为 flutter 生命周期流程图


    大致分为四个阶段



    1. 初始化阶段,包括两个生命周期函数 createState 和 initState;

    2. 组件创建阶段,包括 didChangeDependencies 和 build;

    3. 触发组件多次 build ,这个阶段有可能是因为 didChangeDependencies、 setState 或者 didUpdateWidget 而引发的组件重新 build ,在组件运行过程中会多次触发,这也是优化过程中需要着重注意的点;

    4. 最后是组件销毁阶段,deactivate 和 dispose。


    组件首次加载执行过程


    首先我们来实现下面这段代码(类似于 flutter 自己的计数器项目),康康组件首次创建是否按照上述流程图中的顺序来执行的。



    1. 创建一个 flutter 项目;

    2. 创建 count_widget.dart 中添加以下代码;


    import 'package:flutter/material.dart';

    class CountWidget extends StatefulWidget {
    CountWidget({Key key}) : super(key: key);

    @override
    _CountWidgetState createState() {
    print('count createState');
    return _CountWidgetState();
    }
    }

    class _CountWidgetState extends State<CountWidget> {
    int _count = 0;
    void _incrementCounter() {
    setState(() {
    print('count setState');
    _count++;
    });
    }

    @override
    void initState() {
    print('count initState');
    super.initState();
    }

    @override
    void didChangeDependencies() {
    print('count didChangeDependencies');
    super.didChangeDependencies();
    }

    @override
    void didUpdateWidget(CountWidget oldWidget) {
    print('count didUpdateWidget');
    super.didUpdateWidget(oldWidget);
    }

    @override
    void deactivate() {
    print('count deactivate');
    super.deactivate();
    }

    @override
    void dispose() {
    print('count dispose');
    super.dispose();
    }

    @override
    void reassemble() {
    print('count reassemble');
    super.reassemble();
    }

    @override
    Widget build(BuildContext context) {
    print('count build');
    return Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    Text(
    '$_count',
    style: Theme.of(context).textTheme.headline4,
    ),
    Padding(
    padding: EdgeInsets.only(top: 100),
    child: IconButton(
    icon: Icon(
    Icons.add,
    size: 30,
    ),
    onPressed: _incrementCounter,
    ),
    ),
    ],
    ),
    );
    }
    }

    上述代码把 StatefulWidget 的一些生命周期都进行了重写,并且在执行中都打印了标识,方便看到函数的执行顺序。



    1. 在 main.dart 中加载该组件。代码如下:


    import 'package:flutter/material.dart';

    import './pages/count_widget.dart';

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    visualDensity: VisualDensity.adaptivePlatformDensity,
    ),
    home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
    }
    }

    class MyHomePage extends StatefulWidget {
    MyHomePage({Key key, this.title}) : super(key: key);

    final String title;

    @override
    _MyHomePageState createState() {
    return _MyHomePageState();
    }
    }

    class _MyHomePageState extends State<MyHomePage> {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: CountWidget(),
    );
    }
    }

    这时 CountWidget 作为 MyHomePage 的子组件。我们打开模拟器,开始运行。在控制台可以看到如下日志,可以看出 StatefulWidget 在第一次被创建的时候是调用下面四个函数。


    flutter: count createState
    flutter: count initState
    flutter: count didChangeDependencies
    flutter: count build

    点击屏幕上的 ➕ 按钮,_count 增加 1,模拟器上的数字由 0 变为 1,日志如下。也就是说在状态发生变化的时候,会调用 setStatebuild 两个函数。


    flutter: count setState
    flutter: count build

    command + s 热重载后,日志如下:


    flutter: count reassemble
    flutter: count didUpdateWidget
    flutter: count build

    注释掉 main.dart 中的 CountWidget,command + s 热重载后,这时 CountWidget 消失在模拟器上,日志如下:


    class _MyHomePageState extends State<MyHomePage> {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    // body: CountWidget(),
    );
    }
    }

    flutter: count reassemble
    flutter: count deactivate
    flutter: count dispose

    经过上述一系列操作之后,通过日志打印并结合生命周期流程图,我们可以很清晰的看出各生命周期函数的作用以及理解生命周期的几个阶段。
    相信很多细心的同学已经发现了一个细节,那就是 build 方法在不同的操作中都被调用了,下面我们来介绍什么情况下会触发组件再次 build。


    触发组件再次 build


    触发组件再次 build 的方式有三种,分别是 setStatedidChangeDependenciesdidUpdateWidget


    1.setState 很好理解,只要组件状态发生变化时,就会触发组件 build。在上述的操作过程中,点击 ➕ 按钮,_count 会加 1,结果如下图:


    set_state


    2.didChangeDependencies,组件依赖的全局 state 发生了变化时,也会调用 build。例如系统语言等、主题色等。


    3.didUpdateWidget,我们以下方代码为例。在 main.dart 中,同样的重写生命周期函数,并打印。在 CountWidget 外包一层 Column ,并创建同级的 RaisedButton 做为父 Widget 中的计数器。


    class MyHomePage extends StatefulWidget {
    MyHomePage({Key key, this.title}) : super(key: key);

    final String title;

    @override
    _MyHomePageState createState() {
    print('main createState');
    return _MyHomePageState();
    }
    }

    class _MyHomePageState extends State<MyHomePage> {
    int mainCount = 0;

    void _changeMainCount() {
    setState(() {
    print('main setState');
    mainCount++;
    });
    }

    @override
    void initState() {
    print('main initState');
    super.initState();
    }

    @override
    void didChangeDependencies() {
    print('main didChangeDependencies');
    super.didChangeDependencies();
    }

    @override
    void didUpdateWidget(MyHomePage oldWidget) {
    print('main didUpdateWidget');
    super.didUpdateWidget(oldWidget);
    }

    @override
    void deactivate() {
    print('main deactivate');
    super.deactivate();
    }

    @override
    void dispose() {
    print('main dispose');
    super.dispose();
    }

    @override
    void reassemble() {
    print('main reassemble');
    super.reassemble();
    }

    @override
    Widget build(BuildContext context) {
    print('main build');
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: Column(
    children: <Widget>[
    RaisedButton(
    onPressed: () => _changeMainCount(),
    child: Text('mainCount = $mainCount'),
    ),
    CountWidget(),
    ],
    ),
    );
    }
    }

    重新加载 app,可以看到打印日志如下:


    father_widget_create_state


    flutter: main createState
    flutter: main initState
    flutter: main didChangeDependencies
    flutter: main build
    flutter: count createState
    flutter: count initState
    flutter: count didChangeDependencies
    flutter: count build

    可以发现:



    • 父组件也经历了 createStateinitStatedidChangeDependenciesbuild 这四个过程。

    • 并且父组件要在 build 之后才会创建子组件。


    点击 MyHomePage(父组件)的 mainCount 按钮 ,打印如下:


    flutter: main setState
    flutter: main build
    flutter: count didUpdateWidget
    flutter: count build

    点击 CountWidget 的 ➕ 按钮,打印如下:


    flutter: count setState
    flutter: count build

    可以说明父组件的 State 变化会引起子组件的 didUpdateWidget 和 build,子组件自己的状态变化不会引起父组件的状态改变


    组件销毁


    我们重复上面的操作,为 CountWidget 添加一个子组件 CountSubWidget,并用 count sub 前缀打印日志。重新加载 app。


    注释掉 CountWidget 中的 CountSubWidget,打印日志如下:


    flutter: main reassemble
    flutter: count reassemble
    flutter: count sub reassemble
    flutter: main didUpdateWidget
    flutter: main build
    flutter: count didUpdateWidget
    flutter: count build
    flutter: count sub deactivate
    flutter: count sub dispose

    恢复到注释前,注释掉 MyHomePage 中的 CountWidget,打印如下:


    flutter: main reassemble
    flutter: count reassemble
    flutter: count sub reassemble
    flutter: main didUpdateWidget
    flutter: main build
    flutter: count deactivate
    flutter: count sub deactivate
    flutter: count sub dispose
    flutter: count dispose

    因为是热重载,所以会调用 reassembledidUpdateWidgetbuild,我们可以忽略带有这几个函数的打印日志。可以得出结论:
    父组件移除,会先移除节点,然后子组件移除节点,子组件被永久移除,最后是父组件被永久移除。


    Flutter App Lifecycle


    上面我们介绍的生命周期主要是 StatefulWidget 组件的生命周期,下面我们来简单介绍一下和 app 平台相关的生命周期,比如退出到后台。


    我们创建 app_lifecycle_state.dart 文件并创建 AppLifecycle,他是一个 StatefulWidget,但是他要继承 WidgetsBindingObserver。


    import 'package:flutter/material.dart';

    class AppLifecycle extends StatefulWidget {
    AppLifecycle({Key key}) : super(key: key);

    @override
    _AppLifecycleState createState() {
    print('sub createState');
    return _AppLifecycleState();
    }
    }

    class _AppLifecycleState extends State<AppLifecycle>
    with WidgetsBindingObserver {
    @override
    void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    print('sub initState');
    }

    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
    // TODO: implement didChangeAppLifecycleState
    super.didChangeAppLifecycleState(state);
    print('didChangeAppLifecycleState');
    if (state == AppLifecycleState.resumed) {
    print('resumed:');
    } else if (state == AppLifecycleState.inactive) {
    print('inactive');
    } else if (state == AppLifecycleState.paused) {
    print('paused');
    } else if (state == AppLifecycleState.detached) {
    print('detached');
    }
    }

    @override
    Widget build(BuildContext context) {
    print('sub build');
    return Container(
    child: Text('data'),
    );
    }
    }

    didChangeAppLifecycleState 方法是重点,AppLifecycleState 中的状态包括:resumedinactivepauseddetached 四种。


    didChangeAppLifecycleState 方法的依赖于系统的通知(notifications),正常情况下,App是可以接收到这些通知,但有个别情况下是无法接收到通知的,比如用户关机等。它的四种生命周期状态枚举源码中有详细的介绍和说明,下面附上源码以及简单的翻译说明。


    app_life_cycle_state



    • resumed:该应用程序是可见的,并对用户的输入作出反应。也就是应用程序进入前台。

    • inactive:应用程序处于非活动状态,没有接收用户的输入。在 iOS 上,这种状态对应的是应用程序或 Flutter 主机视图在前台非活动状态下运行。当处于电话呼叫、响应 TouchID 请求、进入应用切换器或控制中心时,或者当 UIViewController 托管的 Flutter 应用程序正在过渡。在 Android 上,这相当于应用程序或 Flutter 主机视图在前台非活动状态下运行。当另一个活动被关注时,如分屏应用、电话呼叫、画中画应用、系统对话框或其他窗口,应用会过渡到这种状态。也就是应用进入后台。

    • pause:该应用程序目前对用户不可见,对用户的输入没有反应,并且在后台运行。当应用程序处于这种状态时,引擎将不会调用。也就是说应用进入非活动状态。

    • detached:应用程序仍然被托管在flutter引擎上,但与任何主机视图分离。处于此状态的时机:引擎首次加载到附加到一个平台 View 的过程中,或者由于执行 Navigator pop,view 被销毁。


    除了 app 生命周期的方法,Flutter 还有一些其他不属于生命周期,但是也会在一些特殊时机被观察到的方法,如 didChangeAccessibilityFeatures(当前系统改变了一些访问性活动的回调)didHaveMemoryPressure(低内存回调)didChangeLocales(用户本地设置变化时调用,如系统语言改变)didChangeTextScaleFactor(文字系数变化) 等,如果有兴趣的话,可以去试一试。


    总结


    本篇文章主要介绍了 Widget 中的 StatefulWidget 的生命周期,以及 Flutter App 相关的生命周期。但是要切记,StatefulWidget 虽好,但也不要无脑的所有 Widget 全都用它,能使用 StatelessWidget 还是要尽量去使用 StatelessWidget(仔细想一下,这是为什么呢?)。好啦,看完本篇文章,你就是 Flutter 初级开发工程师了,可以去面试了(狗头保命)。


    最后


    真正坚持到最后的人,往往靠的不是短暂的激情,而是恰到好处的喜欢和投入。你还那么年轻,完全可以成为任何你想要成为的样子!


    作者:百瓶技术
    链接:https://juejin.cn/post/7056646298073563166
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »