注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

整洁的 Table View 代码

Table view 是 iOS 应用程序中非常通用的组件。许多代码和 table view 都有直接或间接的关系,随便举几个例子,比如提供数据、更新 table view,控制它的行为以及响应选择事件。在这篇文章中,我们将会展示保持 table view 相...
继续阅读 »

Table view 是 iOS 应用程序中非常通用的组件。许多代码和 table view 都有直接或间接的关系,随便举几个例子,比如提供数据、更新 table view,控制它的行为以及响应选择事件。在这篇文章中,我们将会展示保持 table view 相关代码的整洁和良好组织的技术。

UITableViewController vs. UIViewController

Apple 提供了 UITableViewController 作为 table views 专属的 view controller 类。Table view controllers 实现了一些非常有用的特性,来帮你避免一遍又一遍地写那些死板的代码!但是话又说回来,table view controller 只限于管理一个全屏展示的 table view。大多数情况下,这就是你想要的,但如果不是,还有其他方法来解决这个问题,就像下面我们展示的那样。

Table View Controllers 的特性

Table view controllers 会在第一次显示 table view 的时候帮你加载其数据。另外,它还会帮你切换 table view 的编辑模式、响应键盘通知、以及一些小任务,比如闪现侧边的滑动提示条和清除选中时的背景色。为了让这些特性生效,当你在子类中覆写类似 viewWillAppear: 或者 viewDidAppear: 等事件方法时,需要调用 super 版本。

Table view controllers 相对于标准 view controllers 的一个特别的好处是它支持 Apple 实现的“下拉刷新”。目前,文档中唯一的使用 UIRefreshControl 的方式就是通过 table view controller ,虽然通过努力在其他地方也能让它工作(见此处),但很可能在下一次 iOS 更新的时候就不行了。

这些要素加一起,为我们提供了大部分 Apple 所定义的标准 table view 交互行为,如果你的应用恰好符合这些标准,那么直接使用 table view controllers 来避免写那些死板的代码是个很好的方法。

Table View Controllers 的限制

Table view controllers 的 view 属性永远都是一个 table view。如果你稍后决定在 table view 旁边显示一些东西(比如一个地图),如果不依赖于那些奇怪的 hacks,估计就没什么办法了。

如果你是用代码或 .xib 文件来定义的界面,那么迁移到一个标准 view controller 将会非常简单。但是如果你使用了 storyboards,那么这个过程要多包含几个步骤。除非重新创建,否则你并不能在 storyboards 中将 table view controller 改成一个标准的 view controller。这意味着你必须将所有内容拷贝到新的 view controller,然后再重新连接一遍。

最后,你需要把迁移后丢失的 table view controller 的特性给补回来。大多数都是 viewWillAppear: 或 viewDidAppear: 中简单的一条语句。切换编辑模式需要实现一个 action 方法,用来切换 table view 的 editing 属性。大多数工作来自重新创建对键盘的支持。

在选择这条路之前,其实还有一个更轻松的选择,它可以通过分离我们需要关心的功能(关注点分离),让你获得额外的好处:

使用 Child View Controllers

和完全抛弃 table view controller 不同,你还可以将它作为 child view controller 添加到其他 view controller 中(关于此话题的文章)。这样,parent view controller 在管理其他的你需要的新加的界面元素的同时,table view controller 还可以继续管理它的 table view。

- (void)addPhotoDetailsTableView
{
DetailsViewController *details = [[DetailsViewController alloc] init];
details.photo = self.photo;
details.delegate = self;
[self addChildViewController:details];
CGRect frame = self.view.bounds;
frame.origin.y = 110;
details.view.frame = frame;
[self.view addSubview:details.view];
[details didMoveToParentViewController:self];
}

如果你使用这个解决方案,你就必须在 child view controller 和 parent view controller 之间建立消息传递的渠道。比如,如果用户选择了一个 table view 中的 cell,parent view controller 需要知道这个事件来推入其他 view controller。根据使用习惯,通常最清晰的方式是为这个 table view controller 定义一个 delegate protocol,然后到 parent view controller 中去实现。

@protocol DetailsViewControllerDelegate
- (void)didSelectPhotoAttributeWithKey:(NSString *)key;
@end

@interface PhotoViewController ()
@end

@implementation PhotoViewController
// ...
- (void)didSelectPhotoAttributeWithKey:(NSString *)key
{
DetailViewController *controller = [[DetailViewController alloc] init];
controller.key = key;
[self.navigationController pushViewController:controller animated:YES];
}
@end

就像你看到的那样,这种结构为 view controller 之间的消息传递带来了额外的开销,但是作为回报,代码封装和分离非常清晰,有更好的复用性。根据实际情况的不同,这既可能让事情变得更简单,也可能会更复杂,需要读者自行斟酌和决定。

分离关注点(Separating Concerns)

当处理 table views 的时候,有许多各种各样的任务,这些任务穿梭于 models,controllers 和 views 之间。为了避免让 view controllers 做所有的事,我们将尽可能地把这些任务划分到合适的地方,这样有利于阅读、维护和测试。

这里描述的技术是文章更轻量的 View Controllers 中的概念的延伸,请参考这篇文章来理解如何重构 data source 和 model 的逻辑。结合 table views,我们来具体看看如何在 view controllers 和 views 之间分离关注点。

搭建 Model 对象和 Cells 之间的桥梁

有时我们需要将想显示的 model 层中的数据传到 view 层中去显示。由于我们同时也希望让 model 和 view 之间明确分离,所以通常把这个任务转移到 table view 的 data source 中去处理:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PhotoCell"];
Photo *photo = [self itemAtIndexPath:indexPath];
cell.photoTitleLabel.text = photo.name;
NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
cell.photoDateLabel.text = date;
}

但是这样的代码会让 data source 变得混乱,因为它向 data source 暴露了 cell 的设计。最好分解出来,放到 cell 类的一个 category 中。

@implementation PhotoCell (ConfigureForPhoto)

- (void)configureForPhoto:(Photo *)photo
{
self.photoTitleLabel.text = photo.name;
NSString* date = [self.dateFormatter stringFromDate:photo.creationDate];
self.photoDateLabel.text = date;
}

@end

有了上述代码后,我们的 data source 方法就变得简单了。

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier];
[cell configureForPhoto:[self itemAtIndexPath:indexPath]];
return cell;
}

在我们的示例代码中,table view 的 data source 已经分解到单独的类中了,它用一个设置 cell 的 block 来初始化。这时,这个 block 就变得这样简单了:

TableViewCellConfigureBlock block = ^(PhotoCell *cell, Photo *photo) {
[cell configureForPhoto:photo];
};

让 Cells 可复用

有时多种 model 对象需要用同一类型的 cell 来表示,这种情况下,我们可以进一步让 cell 可以复用。首先,我们给 cell 定义一个 protocol,需要用这个 cell 显示的对象必须遵循这个 protocol。然后简单修改 category 中的设置方法,让它可以接受遵循这个 protocol 的任何对象。这些简单的步骤让 cell 和任何特殊的 model 对象之间得以解耦,让它可适应不同的数据类型。

在 Cell 内部控制 Cell 的状态

如果你想自定义 table views 默认的高亮或选择行为,你可以实现两个 delegate 方法,把点击的 cell 修改成我们想要的样子。例如:

- (void)tableView:(UITableView *)tableView
didHighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
}

- (void)tableView:(UITableView *)tableView
didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.photoTitleLabel.shadowColor = nil;
}

然而,这两个 delegate 方法的实现又基于了 view controller 知晓 cell 实现的具体细节。如果我们想替换或重新设计 cell,我们必须改写 delegate 代码。View 的实现细节和 delegate 的实现交织在一起了。我们应该把这些细节移到 cell 自身中去。

@implementation PhotoCell
// ...
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
[super setHighlighted:highlighted animated:animated];
if (highlighted) {
self.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
self.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
} else {
self.photoTitleLabel.shadowColor = nil;
}
}
@end

总的来说,我们在努力把 view 层和 controller 层的实现细节分离开。delegate 肯定得清楚一个 view 该显示什么状态,但是它不应该了解如何修改 view 结构或者给某些 subviews 设置某些属性以获得正确的状态。所有这些逻辑都应该封装到 view 内部,然后给外部提供一个简单的 API。

控制多个 Cell 类型

如果一个 table view 里面有多种类型的 cell,data source 方法很快就难以控制了。在我们示例程序中,photo details table 有两种不同类型的 cell:一种用于显示几个星,另一种用来显示一个键值对。为了划分处理不同 cell 类型的代码,data source 方法简单地通过判断 cell 的类型,把任务派发给其他指定的方法。

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *key = self.keys[(NSUInteger) indexPath.row];
id value = [self.photo valueForKey:key];
UITableViewCell *cell;
if ([key isEqual:PhotoRatingKey]) {
cell = [self cellForRating:value indexPath:indexPath];
} else {
cell = [self detailCellForKey:key value:value];
}
return cell;
}

- (RatingCell *)cellForRating:(NSNumber *)rating
indexPath:(NSIndexPath *)indexPath
{
// ...
}

- (UITableViewCell *)detailCellForKey:(NSString *)key
value:(id)value
{
// ...
}

编辑 Table View

Table view 提供了易于使用的编辑特性,允许你对 cell 进行删除或重新排序。这些事件都可以让 table view 的 data source 通过 delegate 方法得到通知。因此,通常我们能在这些 delegate 方法中看到对数据的进行修改的操作。

修改数据很明显是属于 model 层的任务。Model 应该为诸如删除或重新排序等操作暴露一个 API,然后我们可以在 data source 方法中调用它。这样,controller 就可以扮演 view 和 model 之间的协调者,而不需要知道 model 层的实现细节。并且还有额外的好处,model 的逻辑也变得更容易测试,因为它不再和 view controllers 的任务混杂在一起了。

总结

Table view controllers(以及其他的 controller 对象!)应该在 model 和 view 对象之间扮演协调者和调解者的角色。它不应该关心明显属于 view 层或 model 层的任务。你应该始终记住这点,这样 delegate 和 data source 方法会变得更小巧,最多包含一些简单的样板代码。

这不仅减少了 table view controllers 那样的大小和复杂性,而且还把业务逻辑和 view 的逻辑放到了更合适的地方。Controller 层的里里外外的实现细节都被封装成了简单的 API,最终,它变得更加容易理解,也更利于团队协作。

转自:https://www.jianshu.com/p/22df7a214b49

收起阅读 »

Core Image 和视频

在这篇文章中,我们将研究如何将 Core Image 应用到实时视频上去。我们会看两个例子:首先,我们把这个效果加到相机拍摄的影片上去。之后,我们会将这个影响作用于拍摄好的视频文件。它也可以做到离线渲染,它会把渲染结果返回给视频,而不是直接显示在屏幕上。两个例...
继续阅读 »

在这篇文章中,我们将研究如何将 Core Image 应用到实时视频上去。我们会看两个例子:首先,我们把这个效果加到相机拍摄的影片上去。之后,我们会将这个影响作用于拍摄好的视频文件。它也可以做到离线渲染,它会把渲染结果返回给视频,而不是直接显示在屏幕上。两个例子的完整源代码,请点击这里

总览

当涉及到处理视频的时候,性能就会变得非常重要。而且了解黑箱下的原理 —— 也就是 Core Image 是如何工作的 —— 也很重要,这样我们才能达到足够的性能。在 GPU 上面做尽可能多的工作,并且最大限度的减少 GPU 和 CPU 之间的数据传送是非常重要的。之后的例子中,我们将看看这个细节。

想对 Core Image 有个初步认识的话,可以读读 Warren 的这篇文章 Core Image 介绍。我们将使用 Swift 的函数式 API 中介绍的基于 CIFilter 的 API 封装。想要了解更多关于 AVFoundation 的知识,可以看看本期话题中 Adriaan 的文章,还有话题 #21 中的 iOS 上的相机捕捉。

优化资源的 OpenGL ES

CPU 和 GPU 都可以运行 Core Image,我们将会在 下面 详细介绍这两个的细节。在这个例子中,我们要使用 GPU,我们做如下几样事情。

我们首先创建一个自定义的 UIView,它允许我们把 Core Image 的结果直接渲染成 OpenGL。我们可以新建一个 GLKView 并且用一个 EAGLContext 来初始化它。我们需要指定 OpenGL ES 2 作为渲染 API,在这两个例子中,我们要自己触发 drawing 事件 (而不是在 -drawRect: 中触发),所以在初始化 GLKView 的时候,我们将 enableSetNeedsDisplay 设置为 false。之后我们有可用新图像的时候,我们需要主动去调用 -display。

在这个视图里,我们保持一个对 CIContext 的引用,它提供一个桥梁来连接我们的 Core Image 对象和 OpenGL 上下文。我们创建一次就可以一直使用它。这个上下文允许 Core Image 在后台做优化,比如缓存和重用纹理之类的资源等。重要的是这个上下文我们一直在重复使用。

上下文中有一个方法,-drawImage:inRect:fromRect:,作用是绘制出来一个 CIImage。如果你想画出来一个完整的图像,最容易的方法是使用图像的 extent。但是请注意,这可能是无限大的,所以一定要事先裁剪或者提供有限大小的矩形。一个警告:因为我们处理的是 Core Image,绘制的目标以像素为单位,而不是点。由于大部分新的 iOS 设备配备 Retina 屏幕,我们在绘制的时候需要考虑这一点。如果我们想填充整个视图,最简单的办法是获取视图边界,并且按照屏幕的 scale 来缩放图片 (Retina 屏幕的 scale 是 2)。

完整的代码示例在这里:CoreImageView.swift

从相机获取像素数据

对于 AVFoundation 如何工作的概述,请看 Adriaan 的文章 和 Matteo 的文章 iOS 上的相机捕捉。对于我们而言,我们想从镜头获得 raw 格式的数据。我们可以通过创建一个 AVCaptureDeviceInput 对象来选定一个摄像头。使用 AVCaptureSession,我们可以把它连接到一个 AVCaptureVideoDataOutput。这个 data output 对象有一个遵守 AVCaptureVideoDataOutputSampleBufferDelegate 协议的代理对象。这个代理每一帧将接收到一个消息:

func captureOutput(captureOutput: AVCaptureOutput!,
didOutputSampleBuffer: CMSampleBuffer!,
fromConnection: AVCaptureConnection!) {

我们将用它来驱动我们的图像渲染。在我们的示例代码中,我们已经将配置,初始化以及代理对象都打包到了一个叫做 CaptureBufferSource 的简单接口中去。我们可以使用前置或者后置摄像头以及一个回调来初始化它。对于每个样本缓存区,这个回调都会被调用,并且参数是缓冲区和对应摄像头的 transform:

source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
(buffer, transform) in
...
}

我们需要对相机返回的数据进行变换。无论你如何转动 iPhone,相机的像素数据的方向总是相同的。在我们的例子中,我们将 UI 锁定在竖直方向,我们希望屏幕上显示的图像符合照相机拍摄时的方向,为此我们需要后置摄像头拍摄出的图片旋转 -π/2。前置摄像头需要旋转 -π/2 并且加一个镜像效果。我们可以用一个 CGAffineTransform 来表达这种变换。请注意如果 UI 是不同的方向 (比如横屏),我们的变换也将是不同的。还要注意,这种变换的代价其实是非常小的,因为它是在 Core Image渲染管线中完成的。

接着,要把 CMSampleBuffer转换成 CIImage,我们首先需要将它转换成一个 CVPixelBuffer。我们可以写一个方便的初始化方法来为我们做这件事:

extension CIImage {
convenience init(buffer: CMSampleBuffer) {
self.init(CVPixelBuffer: CMSampleBufferGetImageBuffer(buffer))
}
}

现在我们可以用三个步骤来处理我们的图像。首先,把我们的 CMSampleBuffer 转换成 CIImage,并且应用一个形变,使图像旋转到正确的方向。接下来,我们用一个 CIFilter 滤镜来得到一个新的 CIImage 输出。我们使用了 Florian 的文章 提到的创建滤镜的方式。在这个例子中,我们使用色调调整滤镜,并且传入一个依赖于时间而变化的调整角度。最终,我们使用之前定义的 View,通过 CIContext 来渲染 CIImage。这个流程非常简单,看起来是这样的:

source = CaptureBufferSource(position: AVCaptureDevicePosition.Front) {
[unowned self] (buffer, transform) in
let input = CIImage(buffer: buffer).imageByApplyingTransform(transform)
let filter = hueAdjust(self.angleForCurrentTime)
self.coreImageView?.image = filter(input)
}

当你运行它时,你可能会因为如此低的 CPU 使用率感到吃惊。这其中的奥秘是 GPU 做了几乎所有的工作。尽管我们创建了一个 CIImage,应用了一个滤镜,并输出一个 CIImage,最终输出的结果是一个 promise:直到实际渲染才会去进行计算。一个 CIImage 对象可以是黑箱里的很多东西,它可以是 GPU 算出来的像素数据,也可以是如何创建像素数据的一个说明 (比如使用一个滤镜生成器),或者它也可以是直接从 OpenGL 纹理中创建出来的图像。

从影片中获取像素数据

我们可以做的另一件事是通过 Core Image 把这个滤镜加到一个视频中。和实时拍摄不同,我们现在从影片的每一帧中生成像素缓冲区,在这里我们将采用略有不同的方法。对于相机,它会推送每一帧给我们,但是对于已有的影片,我们使用拉取的方式:通过 display link,我们可以向 AVFoundation 请求在某个特定时间的一帧。

display link 对象负责在每帧需要绘制的时候给我们发送消息,这个消息是按照显示器的刷新频率同步进行发送的。这通常用来做 自定义动画,但也可以用来播放和操作视频。我们要做的第一件事就是创建一个 AVPlayer和一个视频输出:

player = AVPlayer(URL: url)
videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: pixelBufferDict)
player.currentItem.addOutput(videoOutput)

接下来,我们要创建 display link。方法很简单,只要创建一个 CADisplayLink 对象,并将其添加到 run loop。

let displayLink = CADisplayLink(target: self, selector: "displayLinkDidRefresh:")
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)

现在,唯一剩下的就是在 displayLinkDidRefresh: 调用的时候获取视频每一帧。首先,我们获取当前的时间,并且将它转换成当前播放项目里的时间比。然后我们询问 videoOutput,如果当前时间有一个可用的新的像素缓存区,我们把它复制一下并且调用回调方法:

func displayLinkDidRefresh(link: CADisplayLink) {
let itemTime = videoOutput.itemTimeForHostTime(CACurrentMediaTime())
if videoOutput.hasNewPixelBufferForItemTime(itemTime) {
let pixelBuffer = videoOutput.copyPixelBufferForItemTime(itemTime, itemTimeForDisplay: nil)
consumer(pixelBuffer)
}
}

我们从一个视频输出获得的像素缓冲是一个 CVPixelBuffer,我们可以把它直接转换成 CIImage。正如上面的例子,我们会加上一个滤镜。在这个例子里,我们将组合多个滤镜:我们使用一个万花筒的效果,然后用渐变遮罩把原始图像和过滤图像相结合,这个操作是非常轻量级的。

创意地使用滤镜

大家都知道流行的照片效果。虽然我们可以将这些应用到视频,但 Core Image 还可以做得更多。

Core Image 里所谓的滤镜有不同的类别。其中一些是传统的类型,输入一张图片并且输出一张新的图片。但有些需要两个 (或者更多) 的输入图像并且混合生成一张新的图像。另外甚至有完全不输入图片,而是基于参数的生成图像的滤镜。

通过混合这些不同的类型,我们可以创建意想不到的效果。

混合图片

在这个例子中,我们使用这些东西:


上面的例子可以将图像的一个圆形区域像素化。

它也可以创建交互,我们可以使用触摸事件来改变所产生的圆的位置。

Core Image Filter Reference 按类别列出了所有可用的滤镜。请注意,有一部分只能用在 OS X。

生成器和渐变滤镜可以不需要输入就能生成图像。它们很少自己单独使用,但是作为蒙版的时候会非常强大,就像我们例子中的 CIBlendWithMask 那样。

混合操作和 CIBlendWithAlphaMask 还有 CIBlendWithMask 允许将两个图像合并成一个。

CPU vs. GPU

我们在话题 #3 的文章,绘制像素到屏幕上里,介绍了 iOS 和 OS X 的图形栈。需要注意的是 CPU 和 GPU 的概念,以及两者之间数据的移动方式。

在处理实时视频的时候,我们面临着性能的挑战。

首先,我们需要能在每一帧的时间内处理完所有的图像数据。我们的样本中采用 24 帧每秒的视频,这意味着我们有 41 毫秒 (1/24 秒) 的时间来解码,处理以及渲染每一帧中的百万像素。

其次,我们需要能够从 CPU 或者 GPU 上面得到这些数据。我们从视频文件读取的字节数最终会到达 CPU 里。但是这个数据还需要移动到 GPU 上,以便在显示器上可见。

避免转移

一个非常致命的问题是,在渲染管线中,代码可能会把图像数据在 CPU 和 GPU 之间来回移动好几次。确保像素数据仅在一个方向移动是很重要的,应该保证数据只从 CPU 移动到 GPU,如果能让数据完全只在 GPU 上那就更好。

如果我们想渲染 24 fps 的视频,我们有 41 毫秒;如果我们渲染 60 fps 的视频,我们只有 16 毫秒,如果我们不小心从 GPU 下载了一个像素缓冲到 CPU 里,然后再上传回 GPU,对于一张全屏的 iPhone 6 图像来说,我们在每个方向将要移动 3.8 MB 的数据,这将使帧率无法达标。

当我们使用 CVPixelBuffer 时,我们希望这样的流程:


CVPixelBuffer 是基于 CPU 的 (见下文),我们用 CIImage 来包装它。构建滤镜链不会移动任何数据;它只是建立了一个流程。一旦我们绘制图像,我们使用了基于 EAGL 上下文的 Core Image 上下文,而这个 EAGL 上下文也是 GLKView 进行图像显示所使用的上下文。EAGL 上下文是基于 GPU 的。请注意,我们是如何只穿越 GPU-CPU 边界一次的,这是至关重要的部分。

工作和目标

Core Image 的图形上下文可以通过两种方式创建:使用 EAGLContext 的 GPU 上下文,或者是基于 CPU 的上下文。

这个定义了 Core Image 工作的地方,也就是像素数据将被处理的地方。与工作区域无关,基于 GPU 和基于 CPU 的图形上下文都可以通过执行 createCGImage(…),render(, toBitmap, …) 和 render(, toCVPixelBuffer, …),以及相关的命令来向 CPU 进行渲染。

重要的是要理解如何在 CPU 和 GPU 之间移动像素数据,或者是让数据保持在 CPU 或者 GPU 里。将数据移过这个边界是需要很大的代价的。

缓冲区和图像

在我们的例子中,我们使用了几个不同的缓冲区和图像。这可能有点混乱。这样做的原因很简单,不同的框架对于这些“图像”有不同的用途。下面有一个快速总览,以显示哪些是以基于 CPU 或者基于 GPU 的:


结论

Core Image 是操纵实时视频的一大利器。只要你适当的配置下,性能将会是强劲的 —— 只要确保 CPU 和 GPU 之间没有数据的转移。创意地使用滤镜,你可以实现一些非常炫酷的效果,神马简单色调,褐色滤镜都弱爆啦。所有的这些代码都很容易抽象出来,深入了解下不同的对象的作用区域 (GPU 还是 CPU) 可以帮助你提高代码的性能。

转自:https://www.jianshu.com/p/6c4118290a56

收起阅读 »

基于环信 sdk 在uni-app框架中快速集成开发的一款多平台社交Demo

说在前面:此款 demo 是基于 环信sdk 开发的一款具有单聊、群聊、聊天室、音视频等功能的应用。在此之前我们已经开发完 Vue、react(web端)、微信小程序。这三个热门领域的版本,如有需要源码可以留言给我 Git 源码地址: https:/...
继续阅读 »

说在前面:此款 demo 是基于 环信sdk 开发的一款具有单聊、群聊、聊天室、音视频等功能的应用。在此之前我们已经开发完 Vue、react(web端)、微信小程序。这三个热门领域的版本,如有需要源码可以留言给我


Git 源码地址: https://github.com/easemob/webim-uniapp-demo


一、安装开发工具


我们选用微信小程序来用做示例(如果选择百度、支付宝安装对应开发者工具即可)、


微信开发者工具建议还是安装最新版的。uni-app的开发也必须安装HBuilderX工具,这个是捆绑的,没得选择。要用uni-app,你必须得装!


工具安装:


微信开发者工具


HBuilderX


项目demo介绍:



项目demo启动预览:



快速集成环信 sdk:


1、复制整个utils文件



如果你想具体了解主要配置文件 请看这个链接:


https://docs-im.easemob.com/im/web/intro/start


2、如何使用环信的appkey ,可以在环信 console 后台注册一个 账号申请appkey ,可以参考这里 ,获取到 appkey 以后添加到配置文件中 ,如下图所示:



以上两个重要的配置准备完成之后就可以进行一系列的操作了(收发消息、好友申请、进群入群通知等)


在uni-app中 使用环信 sdk 实现添加、删除好友:


1、在全局 App.vue 文件 钩子函数 onLaunch() 中监听各种事件 (好友申请、收到各类消息等)如图:



发送好友请求:



onPresence(message)事件中接收到好友消息申请:



同意好友请求:



拒绝好友请求:



实现收发消息:


1、给好友发送消息:



2、接收到消息:


onTextMessage(message)事件中接收到好友消息,然后做消息上屏处理(具体消息上屏逻辑可看demo中代码示例):



以上展示的仅仅为基本业务场景,更多的业务逻辑详情请看demo示例。api具体详情可以查看 环信sdk 文档


                                  


PS:对于安卓、iOS移动端,我们已经兼容完成。想通过uni-app生成安卓、ios应用的小伙伴们可以愉快的使用起来了~~~


基于uni-app的开发其中也趟了不少坑,在这里就不多赘述了。回归到框架的选型来讲,选用uni-app开发小程序,可同时并行多端小程序,这点是真香,一次开发多端发布。至于审核嘛~ 时快时慢


最后的最后:如果你喜欢,请拒绝白嫖,点赞三连转发!


                                               

收起阅读 »

GCD你会用吗?GCD扫盲之dispatch_semaphore

本文是GCD多线程编程中dispatch_semaphore内容的小结,通过本文,你可以了解到:1、信号量的基本概念与基本使用2、信号量在线程同步与资源加锁方面的应用3、信号量释放时的小陷阱今天我来讲解一下dispatch_semaphore在我们平常开发中的...
继续阅读 »

本文是GCD多线程编程中dispatch_semaphore内容的小结,通过本文,你可以了解到:

1、信号量的基本概念与基本使用
2、信号量在线程同步与资源加锁方面的应用
3、信号量释放时的小陷阱

今天我来讲解一下dispatch_semaphore在我们平常开发中的一些基本概念与基本使用,dispatch_semaphore俗称信号量,也称为信号锁,在多线程编程中主要用于控制多线程下访问资源的数量,比如系统有两个资源可以使用,但同时有三个线程要访问,所以只能允许两个线程访问,第三个应当等待资源被释放后再访问,这时我们就可以使用dispatch_semaphore。

与dispatch_semaphore相关的共有3个方法,分别是dispatch_semaphore_create,dispatch_semaphore_wait,dispatch_semaphore_signal下面我们逐一了解一下这三个方法。

测试代码在这

semaphore的三个方法

dispatch_semaphore_create

/*!
* @function dispatch_semaphore_create
*
* @abstract
* Creates new counting semaphore with an initial value.
*
* @discussion
* Passing zero for the value is useful for when two threads need to reconcile
* the completion of a particular event. Passing a value greater than zero is
* useful for managing a finite pool of resources, where the pool size is equal
* to the value.
*
* @param value
* The starting value for the semaphore. Passing a value less than zero will
* cause NULL to be returned.
*
* @result
* The newly created semaphore, or NULL on failure.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_semaphore_t
dispatch_semaphore_create(long value);

dispatch_semaphore_create方法用于创建一个带有初始值的信号量dispatch_semaphore_t。

对于这个方法的参数信号量的初始值,这里有2种情况:

1、信号量初始值为0时:这种情况主要用于两个线程需要协调特定事件的完成时,即线程同步。
2、信号量初始值为大于0时:这种情况主要用于管理有限的资源池,其中池大小等于这个值,即资源加锁。
上面的2种情况(线程同步、资源加锁),我们在后续的使用篇中会详细讲解。

dispatch_semaphore_wait

/*!
* @function dispatch_semaphore_wait
*
* @abstract
* Wait (decrement) for a semaphore.
*
* @discussion
* Decrement the counting semaphore. If the resulting value is less than zero,
* this function waits for a signal to occur before returning.
*
* @param dsema
* The semaphore. The result of passing NULL in this parameter is undefined.
*
* @param timeout
* When to timeout (see dispatch_time). As a convenience, there are the
* DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants.
*
* @result
* Returns zero on success, or non-zero if the timeout occurred.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

dispatch_semaphore_wait这个方法主要用于等待或减少信号量,每次调用这个方法,信号量的值都会减一,然后根据减一后的信号量的值的大小,来决定这个方法的使用情况,所以这个方法的使用同样也分为2种情况:

1、当减一后的值小于0时,这个方法会一直等待,即阻塞当前线程,直到信号量+1或者直到超时。
2、当减一后的值大于或等于0时,这个方法会直接返回,不会阻塞当前线程。
上面2种方式,放到我们日常的开发中就是下面2种使用情况:

· 当我们只需要同步线程时,我们可以使用dispatch_semaphore_create(0)初始化信号量为0,然后使用dispatch_semaphore_wait方法让信号量减一,这时就属于第一种减一后小于0的情况,这时就会阻塞当前线程,直到另一个线程调用dispatch_semaphore_signal这个让信号量加1的方法后,当前线程才会被唤醒,然后执行当前线程中的代码,这时就起到一个线程同步的作用。

· 当我们需要对资源加锁,控制同时能访问资源的最大数量(假设为n)时,我们就需要使用dispatch_semaphore_create(n)方法来初始化信号量为n,然后使用dispatch_semaphore_wait方法将信号量减一,然后访问我们的资源,然后使用dispatch_semaphore_signal方法将信号量加一。如果有n个线程来访问这个资源,当这n个资源访问都还没有结束时,就会阻塞当前线程,第n+1个线程的访问就必须等待,直到前n个的某一个的资源访问结束,这就是我们很常见的资源加锁的情况。

dispatch_semaphore_signal

/*!
* @function dispatch_semaphore_signal
*
* @abstract
* Signal (increment) a semaphore.
*
* @discussion
* Increment the counting semaphore. If the previous value was less than zero,
* this function wakes a waiting thread before returning.
*
* @param dsema The counting semaphore.
* The result of passing NULL in this parameter is undefined.
*
* @result
* This function returns non-zero if a thread is woken. Otherwise, zero is
* returned.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);

dispatch_semaphore_signal方法用于让信号量的值加一,然后直接返回。如果先前信号量的值小于0,那么这个方法还会唤醒先前等待的线程。

semaphore使用篇

线程同步

这种情况在我们的开发中也是挺常见的,当主线程中有一个异步网络任务,我们需要等这个网络请求成功拿到数据后,才能继续做后面的处理,这时我们就可以使用信号量这种方式来进行线程同步。

我们首先看看完整测试代码:

- (IBAction)threadSyncTask:(UIButton *)sender {

NSLog(@"threadSyncTask start --- thread:%@",[NSThread currentThread]);

//1.创建一个初始值为0的信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

//2.定制一个异步任务
//开启一个异步网络请求
NSLog(@"开启一个异步网络请求");
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url =
[NSURL URLWithString:[@"https://www.baidu.com/" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"%@", [error localizedDescription]);
}
if (data) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
NSLog(@"%@", dict);
}
NSLog(@"异步网络任务完成---%@",[NSThread currentThread]);
//4.调用signal方法,让信号量+1,然后唤醒先前被阻塞的线程
NSLog(@"调用dispatch_semaphore_signal方法");
dispatch_semaphore_signal(semaphore);
}];
[dataTask resume];

//3.调用wait方法让信号量-1,这时信号量小于0,这个方法会阻塞当前线程,直到信号量等于0时,唤醒当前线程
NSLog(@"调用dispatch_semaphore_wait方法");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

NSLog(@"threadSyncTask end --- thread:%@",[NSThread currentThread]);
}

运行之后的log如下:

2019-04-27 17:24:27.050077+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}
2019-04-27 17:24:27.050227+0800 GCD(四) dispatch_semaphore[34482:6102243] 开启一个异步网络请求
2019-04-27 17:24:27.050571+0800 GCD(四) dispatch_semaphore[34482:6102243] 调用dispatch_semaphore_wait方法
2019-04-27 17:24:27.105069+0800 GCD(四) dispatch_semaphore[34482:6105851] (null)
2019-04-27 17:24:27.105262+0800 GCD(四) dispatch_semaphore[34482:6105851] 异步网络任务完成---<NSThread: 0x6000028c6ec0>{number = 6, name = (null)}
2019-04-27 17:24:27.105401+0800 GCD(四) dispatch_semaphore[34482:6105851] 调用dispatch_semaphore_signal方法
2019-04-27 17:24:27.105550+0800 GCD(四) dispatch_semaphore[34482:6102243] threadSyncTask end --- thread:<NSThread: 0x6000028aa7c0>{number = 1, name = main}

从log中我们可以看出,wait方法会阻塞主线程,直到异步任务完成调用signal方法,才会继续回到主线程执行后面的任务。

资源加锁

当一个资源可以被多个线程读取修改时,就会很容易出现多线程访问修改数据出现结果不一致甚至崩溃的问题。为了处理这个问题,我们通常使用的办法,就是使用NSLock,@synchronized给这个资源加锁,让它在同一时间只允许一个线程访问资源。其实信号量也可以当做一个锁来使用,而且比NSLock还有@synchronized代价更低一些,接下来我们来看看它的基本使用

第一步,定义2个宏,将wait与signal方法包起来,方便下面的使用

#ifndef ZED_LOCK
#define ZED_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#endif

#ifndef ZED_UNLOCK
#define ZED_UNLOCK(lock) dispatch_semaphore_signal(lock);
#endif

第二步,声明与创建共享资源与信号锁

/* 需要加锁的资源 **/
@property (nonatomic, strong) NSMutableDictionary *dict;

/* 信号锁 **/
@property (nonatomic, strong) dispatch_semaphore_t lock;
//创建共享资源
self.dict = [NSMutableDictionary dictionary];
//初始化信号量,设置初始值为1
self.lock = dispatch_semaphore_create(1);

第三步,在即将使用共享资源的地方添加ZED_LOCK宏,进行信号量减一操作,在共享资源使用完成的时候添加ZED_UNLOCK,进行信号量加一操作。

- (IBAction)resourceLockTask:(UIButton *)sender {

NSLog(@"resourceLockTask start --- thread:%@",[NSThread currentThread]);

//使用异步执行并发任务会开辟新的线程的特性,来模拟开辟多个线程访问贡献资源的场景

for (int i = 0; i < 3; i++) {

NSLog(@"异步添加任务:%d",i);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

ZED_LOCK(self.lock);
//模拟对共享资源处理的耗时
[NSThread sleepForTimeInterval:1];
NSLog(@"i:%d --- thread:%@ --- 将要处理共享资源",i,[NSThread currentThread]);
[self.dict setObject:@"semaphore" forKey:@"key"];
NSLog(@"i:%d --- thread:%@ --- 共享资源处理完成",i,[NSThread currentThread]);
ZED_UNLOCK(self.lock);

});
}

NSLog(@"resourceLockTask end --- thread:%@",[NSThread currentThread]);
}

在这一步中,我们使用异步执行并发任务会开辟新的线程的特性,来模拟开辟多个线程访问贡献资源的场景,同时使用了线程休眠的API来模拟对共享资源处理的耗时。这里我们开辟了3个线程来并发访问这个共享资源,代码运行的log如下:

2019-04-27 18:36:25.275060+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask start --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
2019-04-27 18:36:25.275312+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:0
2019-04-27 18:36:25.275508+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:1
2019-04-27 18:36:25.275680+0800 GCD(四) dispatch_semaphore[35944:6315957] 异步添加任务:2
2019-04-27 18:36:25.275891+0800 GCD(四) dispatch_semaphore[35944:6315957] resourceLockTask end --- thread:<NSThread: 0x60000130e940>{number = 1, name = main}
2019-04-27 18:36:26.276757+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 将要处理共享资源
2019-04-27 18:36:26.277004+0800 GCD(四) dispatch_semaphore[35944:6316211] i:0 --- thread:<NSThread: 0x6000013575c0>{number = 3, name = (null)} --- 共享资源处理完成
2019-04-27 18:36:27.282099+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 将要处理共享资源
2019-04-27 18:36:27.282357+0800 GCD(四) dispatch_semaphore[35944:6316212] i:1 --- thread:<NSThread: 0x600001357800>{number = 4, name = (null)} --- 共享资源处理完成
2019-04-27 18:36:28.283769+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 将要处理共享资源
2019-04-27 18:36:28.284041+0800 GCD(四) dispatch_semaphore[35944:6316214] i:2 --- thread:<NSThread: 0x600001369280>{number = 5, name = (null)} --- 共享资源处理完成

从多次log中我们可以看出:

添加信号锁之后,每个线程对于共享资源的操作都是有序的,并不会出现2个线程同时访问锁中的代码区域。

我把上面的实现代码简化一下,方便分析这种锁的实现原理:

//step_1
ZED_LOCK(self.lock);
//step_2
NSLog(@"执行任务");
//step_3
ZED_UNLOCK(self.lock);

1、信号量初始化的值为1,当一个线程过来执行step_1的代码时,会调用信号量的值减一的方法,这时,信号量的值为0,它会直接返回,然后执行step_2的代码去完成去共享资源的访问,然后再使用step_3中的signal方法让信号量加一,信号量的值又会回归到初始值1。这就是一个线程过来访问的调用流程。

2、当线程1过来执行到step_2的时候,这时又有一个线程2它也从step_1处来调用这段代码,由于线程1已经调用过step_1的wait方法将信号量的值减一,这时信号量的值为0。同时线程2进入然后调用了step_1的wait方法又将信号量的值减一,这时的信号量的值为-1,由于信号量的值小于0时会阻塞当前线程(线程2),所以,线程2就会一直等待,直到线程1执行完step_3中的方法,将信号量加一,才会唤醒线程2,继续执行下面的代码。这就是为什么信号量可以对共享资源加锁的原因,如果我们可以允许n个线程同时访问,我们就需要在初始化这个信号量时把信号量的值设为n,这样就限制了访问共享资源的线程数。

通过上面的分析,我们可以知道,如果我们使用信号量来进行线程同步时,我们需要把信号量的初始值设为0,如果要对资源加锁,限制同时只有n个线程可以访问的时候,我们就需要把信号量的初始值设为n。

semaphore的释放

在我们平常的开发过程中,如果对semaphore使用不当,就会在它释放的时候遇到奔溃问题。

首先我们来看2个例子:

- (IBAction)crashScene1:(UIButton *)sender {

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

//在使用过程中将semaphore置为nil
semaphore = nil;
}
- (IBAction)crashScene2:(UIButton *)sender {

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

//在使用过程中对semaphore进行重新赋值
semaphore = dispatch_semaphore_create(3);
}

我们打开测试代码,找到semaphore对应的target,然后运行一下代码,然后点击后面2个按钮调用一下上面的代码,然后我们可以发现,代码在运行到semaphore = nil;与semaphore = dispatch_semaphore_create(3);时奔溃了。然后我们使用lldb的bt命令查看一下调用栈。

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
frame #0: 0x0000000111c31309 libdispatch.dylib`_dispatch_semaphore_dispose + 59
frame #1: 0x0000000111c2fb06 libdispatch.dylib`_dispatch_dispose + 97
* frame #2: 0x000000010efb113b GCD(四) dispatch_semaphore`-[ZEDDispatchSemaphoreViewController crashScene1:](self=0x00007fdcfdf0add0, _cmd="crashScene1:", sender=0x00007fdcfdd0a3d0) at ZEDDispatchSemaphoreViewController.m:117
frame #3: 0x0000000113198ecb UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
frame #4: 0x0000000112bd40bd UIKitCore`-[UIControl sendAction:to:forEvent:] + 67
frame #5: 0x0000000112bd43da UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 450
frame #6: 0x0000000112bd331e UIKitCore`-[UIControl touchesEnded:withEvent:] + 583
frame #7: 0x00000001131d40a4 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2729
frame #8: 0x00000001131d57a0 UIKitCore`-[UIWindow sendEvent:] + 4080
frame #9: 0x00000001131b3394 UIKitCore`-[UIApplication sendEvent:] + 352
frame #10: 0x00000001132885a9 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3054
frame #11: 0x000000011328b1cb UIKitCore`__handleEventQueueInternal + 5948
frame #12: 0x0000000110297721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
frame #13: 0x0000000110296f93 CoreFoundation`__CFRunLoopDoSources0 + 243
frame #14: 0x000000011029163f CoreFoundation`__CFRunLoopRun + 1263
frame #15: 0x0000000110290e11 CoreFoundation`CFRunLoopRunSpecific + 625
frame #16: 0x00000001189281dd GraphicsServices`GSEventRunModal + 62
frame #17: 0x000000011319781d UIKitCore`UIApplicationMain + 140
frame #18: 0x000000010efb06a0 GCD(四) dispatch_semaphore`main(argc=1, argv=0x00007ffee0c4efc8) at main.m:14
frame #19: 0x0000000111ca6575 libdyld.dylib`start + 1
frame #20: 0x0000000111ca6575 libdyld.dylib`start + 1
(lldb)

从上面的调用栈我们可以看出,奔溃的地方都处于libdispatch库调用dispatch_semaphore_dispose方法释放信号量的时候,为什么在信号量使用过程中对信号量进行重新赋值或置空操作会crash呢,这个我们就需要从GCD的源码层面来分析了,GCD的源码库libdispatch在苹果的开源代码库可以下载,我在自己的Github也放了一份libdispatch-187.10版本的,下面的源码分析都是基于这个版本的。

首先我们来看一下dispatch_semaphore_t的结构体dispatch_semaphore_s的结构体定义

struct dispatch_semaphore_s {
DISPATCH_STRUCT_HEADER(dispatch_semaphore_s, dispatch_semaphore_vtable_s);
long dsema_value; //当前的信号值
long dsema_orig; //初始化的信号值
size_t dsema_sent_ksignals;
#if USE_MACH_SEM && USE_POSIX_SEM
#error "Too many supported semaphore types"
#elif USE_MACH_SEM
semaphore_t dsema_port; //当前mach_port_t信号
semaphore_t dsema_waiter_port; //休眠时mach_port_t信号
#elif USE_POSIX_SEM
sem_t dsema_sem;
#else
#error "No supported semaphore type"
#endif
size_t dsema_group_waiters;
struct dispatch_sema_notify_s *dsema_notify_head;//链表头部
struct dispatch_sema_notify_s *dsema_notify_tail;//链表尾部
};

这里我们需要关注2个值的变化,dsema_value与dsema_orig,它们分别代表当前的信号值与初始化时的信号值。

当我们调用dispatch_semaphore_create方法创建信号量时,这个方法内部会把传入的参数存储到dsema_value(当前的value)和dsema_orig(初始value)中,条件是value的值必须大于或等于0。

dispatch_semaphore_t
dispatch_semaphore_create(long value)
{
dispatch_semaphore_t dsema;

// If the internal value is negative, then the absolute of the value is
// equal to the number of waiting threads. Therefore it is bogus to
// initialize the semaphore with a negative value.
if (value < 0) {//初始值不能小于0
return NULL;
}

dsema = calloc(1, sizeof(struct dispatch_semaphore_s));//申请信号量的内存

if (fastpath(dsema)) {//信号量初始化赋值
dsema->do_vtable = &_dispatch_semaphore_vtable;
dsema->do_next = DISPATCH_OBJECT_LISTLESS;
dsema->do_ref_cnt = 1;
dsema->do_xref_cnt = 1;
dsema->do_targetq = dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dsema->dsema_value = value;//当前的值
dsema->dsema_orig = value;//初始值
#if USE_POSIX_SEM
int ret = sem_init(&dsema->dsema_sem, 0, 0);//内存空间映射
DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif
}

return dsema;
}

然后调用dispatch_semaphore_wait与dispatch_semaphore_signal时会对dsema_value做加一或减一操作。当我们对信号量置空或者重新赋值操作时,会调用dispatch_semaphore_dispose释放信号量,我们来看看对应的源码

static void
_dispatch_semaphore_dispose(dispatch_semaphore_t dsema)
{
if (dsema->dsema_value < dsema->dsema_orig) {//当前的信号值如果小于初始值就会crash
DISPATCH_CLIENT_CRASH(
"Semaphore/group object deallocated while in use");
}

#if USE_MACH_SEM
kern_return_t kr;
if (dsema->dsema_port) {
kr = semaphore_destroy(mach_task_self(), dsema->dsema_port);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
if (dsema->dsema_waiter_port) {
kr = semaphore_destroy(mach_task_self(), dsema->dsema_waiter_port);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
#elif USE_POSIX_SEM
int ret = sem_destroy(&dsema->dsema_sem);
DISPATCH_SEMAPHORE_VERIFY_RET(ret);
#endif

_dispatch_dispose(dsema);
}

从源码中我们可以看出,当dsema_value小于dsema_orig时,即信号量还在使用时,会直接调用DISPATCH_CLIENT_CRASH让APP奔溃。

所以,我们在使用信号量的时候,不能在它还在使用的时候,进行赋值或者置空的操作。

如果文中有错误的地方,或者与你的想法相悖的地方,请在评论区告知我,我会继续改进,如果你觉得这个篇文章总结的还不错,麻烦动动小手,给我的文章与Git代码样例点个✨

链接:https://www.jianshu.com/p/7981e3357fe9

收起阅读 »

探究iOS鲜为人知的小秘密一一__attribute__运用

Clang Attributes是Clang提供的一种源码注解,方便开发者向编译器表达某种要求,参与控制如Static Analyzer、Name Mangling、Code Generation等过程,一般以attribute(xxx)的形式出现在代码中;为...
继续阅读 »

Clang Attributes是Clang提供的一种源码注解,方便开发者向编译器表达某种要求,参与控制如Static Analyzer、Name Mangling、Code Generation等过程,一般以attribute(xxx)的形式出现在代码中;为方便使用,一些常用属性也被Cocoa定义成宏,比如在系统头文件中经常出现的NS_CLASS_AVAILABLE_IOS(9_0)就是attribute(availability(...))这个属性的简单写法。

※unavailable
#define UNAVAILABLE_ATTRIBUTE __attribute__((unavailable))

可以加在方法声明的后面,告诉编译器该方法不可用

Swift中

@available(*, unavailable)

func foo() {}

@available(iOS, unavailable, message="you can't call this")

func foo2() {}

※availability

#define NS_DEPRECATED_IOS(_iosIntro,_iosDep,...) CF_DEPRECATED_IOS(_iosIntro,_iosDep,__VA_ARGS__)

展开看

#define CF_DEPRECATED_IOS(_iosIntro, _iosDep, ...) __attribute__((availability(ios,introduced=_iosIntro,deprecated=_iosDep,message="" __VA_ARGS__)))

再展开看

__attribute__((availability(ios,introduced=2_0,deprecated=7_0,message=""__VA_ARGS__)))

iOS即是iOS平台

introduced从哪个版本开始使用

deprecated从哪个版本开始弃用

message警告的信息

其实还可以再加一个参数例子:

void f(void) __attribute__((availability(macosx,introduced=10.4,deprecated=10.6,obsoleted=10.7)));

obsoleted完全禁止使用的版本

NS_AVAILABLE_IOS(7_0) iOS7.0或之后才能使用

NS_DEPRECATED_IOS(2_0,6_0) iOS2.0开始使用6.0废弃

Swift中:

@available(iOS 6.0, *)

public var minimumScaleFactor: CGFloat

@available(OSX, introduced=10.4, deprecated=10.6, obsoleted=10.10)

@available(iOS, introduced=5.0, deprecated=7.0)

func foo3() {}

※objc_subclassing_restricted

一句话就是使用这个属性可以定义一个不可被继承的类

__attribute__((objc_subclassing_restricted))

@interface Eunuch : NSObject

@end

@interface Child : Eunuch//如果继承它会报错

@end

在Swift中对原来的很多attribute的支持现在还缺失中,为了达到类似的目的,我们可以使用一个final关键词


※objc_requires_super

使用这个属性标志子类继承这个方法时需要调用super,否则给出编译警告,来让父类中有一些关键代码是在被继承重写后必须执行

#define NS_REQUIRES_SUPER __attribute__((objc_requires_super))


在Switf中也还是可以用final的方法来达到这个目的


Swift equivalent to __attribute((objc_requires_super))?(stackoverflow)

关于Swift中的final的详细讲解

※overloadable

用于C函数,可以定义若干个相同函数名,但参数不同的方法,调用时编译器会自动根据参数选择函数去调用

__attribute__((overloadable))

void logAnything(id obj) {

NSLog(@"%@", obj);

}

__attribute__((overloadable)) void logAnything(int number) {

NSLog(@"%@", @(number));

}

__attribute__((overloadable)) void logAnything(CGRect rect) {

NSLog(@"%@", NSStringFromCGRect(rect));

}

// Tests

logAnything(@[@"1", @"2"]);

logAnything(233);

logAnything(CGRectMake(1, 2, 3, 4));

有兴趣的可以写一个自定义的Log免去用NSLog要写@""等格式的麻烦

※cleanup

__attribute__((cleanup(...))),用于修饰一个变量,在它的作用域结束时可以自动执行一个指定的方法,个人感觉这个真的很方便


一个对象可以这样用,那么block实际也是一个对象,这样就可以写一个宏,实际上就是ReactiveCocoa中神奇的@onExit

#define onExit\

__strong void(^block)(void) __attribute__((cleanup(blockCleanUp), unused)) = ^

有时候我们需要用到锁这个东西,或者一些CoreFoundation的对象有时候最后需要释放,用这个宏就很方便了


为了好看用这个宏的时候加个@就加个释放池就可以了


sunnyxx这篇博客讲的很清楚

Swift中:可以用defer


还有一些format, const, noreturn, aligned , packed, objc_boxable, constructor / destructor, enable_if, objc_runtime_name可以看这两篇文章:

神奇的attribute

Clang Attributes黑魔法小记

链接:https://www.jianshu.com/p/33d7d0596028

收起阅读 »

一步步封装实现自己的网络请求框架 3.0

一、ReactiveHtt协程这个概念已经出现很多年了,但 Kotlin 协程是在 2018 年才发布了 1.0 版本,被 Android 开发者所熟知还要再往后一段时间,协程的意义不是本篇文章所应该探讨的,但如果你去了解下协程能给我们带来的开发效益,我相信你...
继续阅读 »
一、ReactiveHtt

协程这个概念已经出现很多年了,但 Kotlin 协程是在 2018 年才发布了 1.0 版本,被 Android 开发者所熟知还要再往后一段时间,协程的意义不是本篇文章所应该探讨的,但如果你去了解下协程能给我们带来的开发效益,我相信你是会喜欢上它的

闲话说完,这里先贴上 3.0 版本的 GitHub 链接:ReactiveHttp

3.0 版本的技术栈已更新为了 Kotlin + Jetpack + Coroutines + Retrofit,已托管到 jitpack.io,感兴趣的读者可以直接远程导入依赖

allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.leavesC:ReactiveHttp:latest_version'
}


二、能给你带来什么

ReactiveHttp 能够给你带来什么?

  • 更现代化的技术栈。Kotlin + Jetpack + Retrofit 现在应该是大多数 Android 开发者选用的最基础组件了,Kotlin 协程会相对比较少人接触,但我觉得协程也是未来的主流方向之一,毕竟连 Retrofit 也原生支持 Kotlin 协程了,本库会更加符合大多数开发者的现实需求
  • 极简的设计理念。ReactiveHttp 目前仅包含十二个 Kotlin 文件,设计上遵循约定大于配置的理念,大多数配置项都可以通过方法复写的形式来实现自定义
  • 极简的使用方式。只需要持有一个 RemoteDataSource 对象,开发者就可以在任意地方发起异步请求和同步请求了。此外,进行网络请求 Callback 自然是必不可少的,ReactiveHttp 提供了多个事件回调:onStart、onSuccess、onSuccessIO、onFailed、onFailToast、onCancelled、onFinally 等,按需声明即可,甚至可以完全不用实现
  • 支持通用性的自动化行为。对于网络请求来说,像 showLoading、dismissLoading、showToast 等行为是具有通用性的,我们肯定不希望每个网络请求都要来手动调用方法触发以上操作,而是希望能够在发起网络请求的过程中自动完成。ReactiveHttp 就提供了自动完成以上通用性 UI 行为的功能 ,且每个操作均和生命周期相绑定,避免了常见的内存泄漏和 NPE 问题,并提供了交由外部使用者来自定义各种其它行为的入口
  • 极低的接入成本。ReactiveHttp 并不强制要求外部必须继承于任何 BaseViewModel 或者是 BaseActivity/BaseFragment,外部只要通过实现 IUIActionEventObserverIViewModelActionEvent 两个接口即可享受 ReactiveHttp 带来的各个益处。当然,如果你不需要 ReactiveHttp 的自动化行为的话,也可以不实现任何接口
  • 支持多个(两个/三个)接口同时并发请求,在网络请求成功时同步回调,所以理论上多个接口的总耗时就取决于耗时最长的那个接口,从而缩短用户的等待时间,提升用户体验

ReactiveHttp 不能带给你什么?

  • ReactiveHttp 本身的应用领域是专注于接口请求的,所以对于接口的返回值形式带有强制约束,且没有封装文件下载、文件上传等功能
  • 肯定有,但还没想到

ReactiveHttp 目前已经在我司项目上稳定运行一年多了,在这过程中我也是在逐步优化,使其能够更加适用于外部不同环境的需求,到目前为止我觉得也是已经足够稳定了,希望对你有所帮助 😇😇

三、架构说明

现在应该有百分之九十以上的 Android 客户端的网络请求是直接或间接依靠 OkHttp 来完成的吧?本文所说的网络请求框架就是指在 OkHttp 之上所做的一层封装。原生的 OkHttp 在使用上并不方便,甚至可以说是有点繁琐。Retrofit 在易用性上有所提升,但是如果直接使用的话也并不算多简洁。所以我们往往都是会根据项目架构自己来对 OkHttp 或者 Retrofit 进行多一层封装,ReactiveHttp 的实现出发点即是如此

此外,现在大多数项目都引用到了 Jetpack 这一套组件来实现 MVVM 架构的吧?ReactiveHttp 将 Jetpack 和 Retrofit 关联了起来,使得网络请求过程更加符合“响应式”概念,并提供了更加可靠的生命周期安全性和自动化行为

一般的网络请求框架是只专注于完成网络请求并透传出结果,ReactiveHttp 不太一样,ReactiveHttp 在这个基础上还实现了将网络请求和 ViewModel 以及 Activity/Fragment 相绑定的功能,ReactiveHttp 希望做到的是能够尽量完成大多数的通用行为,且每个层次均不强依赖于特定父类

Google 官方曾推出过一份最佳应用架构指南。当中,每个组件仅依赖于其下一级的组件。ViewModel 是关注点分离原则的一个具体实现,是作为用户数据的承载体处理者而存在的,Activity/Fragment 仅依赖于 ViewModel,ViewModel 就用于响应界面层的输入和驱动界面层变化,Repository 用于为 ViewModel 提供一个单一的数据来源及数据存储域,Repository 可以同时依赖于持久性数据模型和远程服务器数据源

ReactiveHttp 的设计思想类似于 Google 推荐的最佳应用架构指南

  • BaseRemoteDataSource 作为数据提供者处于最下层,只用于向上层提供数据,提供了多个同步请求和异步请求方法,和 BaseReactiveViewModel 之间依靠 IUIActionEvent 接口来联系
  • BaseReactiveViewModel 作为用户数据的承载体和处理者,包含了多个和网络请求事件相关的 LiveData 用于驱动界面层的 UI 变化,和 BaseReactiveActivity 之间依靠 IViewModelActionEvent 接口来联系
  • BaseReactiveActivity 包含与系统和用户交互的逻辑,其负责响应 BaseReactiveViewModel 中的数据变化,提供了和 BaseReactiveViewModel 进行绑定的方法

上文有说到,ReactiveHttp 提供了在网络请求过程中自动完成 showLoading、dismissLoading、showToast 等行为的能力。首先,BaseRemoteDataSource 在网络请求过程中会通过 IUIActionEvent 接口来通知 BaseReactiveViewModel 需要触发的行为,从而连锁触发 ShowLoadingLiveData、DismissLoadingLiveData、ShowToastLiveData 值的变化,BaseReactiveActivity 就通过监听 LiveData 值的变化来完成 UI 层操作

四、惯常做法

以下步骤应该是大部分应用目前进行网络请求时的惯常做法了

服务端返回给移动端的数据使用具有特定格式的 Json 来进行通信,用整数 status 来标明本次请求是否成功,在失败时则直接 showToast(msg)data则需要用泛型来声明了,最终就对应移动端的一个泛型类,类似于 HttpWrapBean

{
"status":200,
"msg":"success",
"data":""
}

data class HttpWrapBean(val status: Int, val msg: String, val data: T)

interface ApiService {

@POST("api1")
fun api1(): ObservableInt>>

@GET("api2")
fun api2(): Call>

}


然后在 interface 中声明 Api 接口,这也是使用 Retrofit 的惯常用法。根据项目中的实际情况,开发者可能是使用 Call 或者 Observable 作为每个接口返回值的最外层的数据包装类,然后再使用 HttpWrapBean 来作为具体数据类的包装类

然后项目中使用的是 RxJava,那么就需要像以下这样来完成网络请

val retrofit = Retrofit.Builder()
.baseUrl("https://xxx.com")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val service = retrofit.create(ApiService::class.java)
val call: ObservableInt>> = service.api1()
call.subscribe(object : ConsumerInt>> {
override fun accept(userBean: HttpWrapBean?) {

}

}, object : Consumer {
override fun accept(t: Throwable?) {

}
})


五、简单入门

ReactiveHttp 在使用上会比上面给出的例子简单很多,下面就来看下通过 ReactiveHttp 如何完成网络请求

ReactiveHttp 需要知道网络请求的结果,但不知道外部会使用什么字段名来标识 HttpWrapBean 中的三个值,所以需要外部实现 IHttpWrapBean 接口来进行标明。例如,你可以这样来实现:

data class HttpWrapBean(val status: Int, val msg: String, val data: T) : IHttpWrapBean {

override val httpCode: Int
get() = status

override val httpMsg: String
get() = msg

override val httpData: T
get() = data

//网络请求是否成功
override val httpIsSuccess: Boolean
get() = status == 200

}


suspend来修饰接口方法,且不需要其它的外层包装类。suspend是 kotlin 协程引入的,当用该关键字修饰接口方法时,Retrofit 内部就会使用协程的方式来完成该网络请求

interface ApiService {

@GET("config/district")
suspend fun getProvince(): HttpWrapBean>

}


ReactiveHttp 提供了 RemoteExtendDataSource 交由外部来继承实现。RemoteExtendDataSource 包含了所有的网络请求方法,外部仅需要根据实际情况来实现三个必要的字段和方法即可

  • releaseUrl。即应用的 BaseUrl
  • createRetrofit。用于创建 Retrofit,开发者可以在这里自定义 OkHttpClient
  • showToast。当网络请求失败时,通过该方法来向用户提示失败原因

例如,你可以像以下这样来实现你自己项目的专属 DataSource,当中就包含了开发者整个项目的全局网络请求配置

class SelfRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}

}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = "https://restapi.amap.com/v3/"

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}

}


之后,我们就可以依靠 SelfRemoteDataSource 在任意地方发起网络请求了,按需声明 Callback 方法。此外,由于使用到了扩展函数,所以 SelfRemoteDataSource 中可以直接调用 ApiService 中的接口方法,无需特意引用和导包

六、进阶使用

上述在使用 SelfRemoteDataSource 发起网络请求时虽然调用的是 enqueueLoading 方法,但实际上并不会弹出 loading 框,因为完成 ShowLoading、DismissLoading、ShowToast 等 UI 行为是需要 RemoteDataSource、ViewModel 和 Activity 这三者一起进行配合的,即 SelfRemoteDataSource 需要和其它两者关联上,将需要触发的 UI 行为反馈给 Activity

这可以通过直接继承于 BaseReactiveActivity 和 BaseReactiveViewModel 来实现,也可以通过实现相应接口来完成关联。当然,如果你不需要 ReactiveHttp 的各个自动化行为的话,也可以不做以下任何改动

总的来说,ReactiveHttp 具有极低的接入成本

1、BaseReactiveActivity

BaseReactiveActivity 是 ReactiveHttp 提供的一个默认 BaseActivity,其实现了 IUIActionEventObserver 接口,用于提供一些默认参数和默认行为,例如 CoroutineScope 和 showLoading。但在大多数情况下,我们自己的项目是不会去继承外部 Activity 的,而是会有一个自己实现的全局统一的 BaseActivity,所以如果你不想继承 BaseReactiveActivity 的话,可以自己来实现 IUIActionEventObserver 接口,就像以下这样

@SuppressLint("Registered")
abstract class BaseActivity : AppCompatActivity(), IUIActionEventObserver {

protected inline fun getViewModel(
factory: ViewModelProvider.Factory? = null,
noinline initializer: (VM.(lifecycleOwner: LifecycleOwner) -> Unit)? = null
): Lazy where VM : ViewModel, VM : IViewModelActionEvent {
return getViewModel(VM::class.java, factory, initializer)
}

override val lifecycleSupportedScope: CoroutineScope
get() = lifecycleScope

override val lContext: Context?
get() = this

override val lLifecycleOwner: LifecycleOwner
get() = this

private var loadDialog: ProgressDialog? = null

override fun showLoading(job: Job?) {
dismissLoading()
loadDialog = ProgressDialog(lContext).apply {
setCancelable(true)
setCanceledOnTouchOutside(false)
//用于实现当弹窗销毁的时候同时取消网络请求
// setOnDismissListener {
// job?.cancel()
// }
show()
}
}

override fun dismissLoading() {
loadDialog?.takeIf { it.isShowing }?.dismiss()
loadDialog = nul
}


2、BaseReactiveViewModel

类似地,BaseReactiveViewModel 是 ReactiveHttp 提供的一个默认的 BaseViewModel,其实现了 IViewModelActionEvent 接口,用于接收 RemoteDataSource 发起的 UI 层行为。如果你不希望继承于 BaseReactiveViewModel 的话,可以自己来实现 IViewModelActionEvent 接口,就像以下这样

open class BaseViewModel : ViewModel(), IViewModelActionEvent {

override val lifecycleSupportedScope: CoroutineScope
get() = viewModelScope

override val showLoadingEventLD = MutableLiveData()

override val dismissLoadingEventLD = MutableLiveData()

override val showToastEventLD = MutableLiveData()

override val finishViewEventLD = MutableLiveData()

}


3、关联上

完成以上两步后,开发者就可以像如下所示这样将 RemoteDataSource、ViewModel 和 Activity 这三者给关联起来。WeatherActivity 通过 getViewModel 方法来完成 WeatherViewModel 的初始化和内部多个 UILiveData 的绑定,并在 lambda 表达式中完成对 WeatherViewModel 内部和具体业务相关的 DataLiveData 的数据监听,至此所有自动化行为就都已经绑定上了

class WeatherViewModel : BaseReactiveViewModel() {

private val remoteDataSource by lazy {
SelfRemoteDataSource(this)
}

val forecastsBeanLiveData = MutableLiveData()

fun getWeather(city: String) {
remoteDataSource.enqueue({
getWeather(city)
}) {
onSuccess {
if (it.isNotEmpty()) {
forecastsBeanLiveData.value = it[0]
}
}
}
}

}

class WeatherActivity : BaseReactiveActivity() {

private val weatherViewModel by getViewModel {
forecastsBeanLiveData.observe(this@WeatherActivity, {
showWeather(it)
})
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
weatherViewModel.getWeather("adCode")
}

private fun showWeather(forecastsBean: ForecastsBean) {

}

}


七、其它

1、BaseRemoteDataSource

RemoteExtendDataSource 提供了许多个可以进行复写的方法,既可用于配置 OkHttp 的各个网络请求参数,也用于交由外部进行流程控制。例如,你可以这样来实现自己项目的 BaseRemoteDataSource

class BaseRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}
}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = HttpConfig.BASE_URL_MAP

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

/**
* 如果外部想要对 Throwable 进行特殊处理,则可以重写此方法,用于改变 Exception 类型
* 例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了
* 此时外部就可以实现一个 BaseException 的子类 TokenInvalidException 并在此处返回
* 从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少
*/
override fun generateBaseException(throwable: Throwable): BaseHttpException {
return if (throwable is BaseHttpException) {
throwable
} else {
LocalBadException(throwable)
}
}

/**
* 用于由外部中转控制当抛出异常时是否走 onFail 回调,当返回 true 时则回调,否则不回调
* @param httpException
*/
override fun exceptionHandle(httpException: BaseHttpException): Boolean {
return true
}

/**
* 用于将网络请求过程中的异常反馈给外部,以便记录
* @param throwable
*/
override fun exceptionRecord(throwable: Throwable) {
Log.e("SelfRemoteDataSource", throwable.message ?: "")
}

/**
* 用于对 BaseException 进行格式化,以便在请求失败时 Toast 提示错误信息
* @param httpException
*/
override fun exceptionFormat(httpException: BaseHttpException): String {
return when (httpException.realException) {
null -> {
httpException.errorMessage
}
is ConnectException, is SocketTimeoutException, is UnknownHostException -> {
"连接超时,请检查您的网络设置"
}
else -> {
"请求过程抛出异常:" + httpException.errorMessage
}
}
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}

}


此外,开发者可以直接在自己的 BaseViewModel 中声明一个 BaseRemoteDataSource 变量实例,所有子 ViewModel 都全局统一使用同一份 DataSource 配置。如果有某些特定接口需要使用不同的 BaseUrl 的话,也可以再多声明一个 BaseRemoteDataSource

open class BaseViewModel : BaseReactiveViewModel() {

/**
* 正常来说单个项目中应该只有一个 RemoteDataSource 实现类,即全局使用同一份配置
* 但父类也应该允许子类使用一个独有的 RemoteDataSource,即允许子类复写此字段
*/
protected open val remoteDataSource by lazy {
BaseRemoteDataSource(this)
}

}


2、BaseHttpException

BaseHttpException 是 ReactiveHttp 对网络请求过程中发生的各类异常情况的包装类,任何透传到外部的异常信息均会被封装为 BaseHttpException 类型。BaseHttpException 有两个默认子类,分别用于表示服务器异常和本地异

/**
* @param errorCode 服务器返回的错误码 或者是 HttpConfig 中定义的本地错误码
* @param errorMessage 服务器返回的异常信息 或者是 请求过程中抛出的信息,是最原始的异常信息
* @param realException 用于当 code 是本地错误码时,存储真实的运行时异常
*/
open class BaseHttpException(val errorCode: Int, val errorMessage: String, val realException: Throwable?) : Exception(errorMessage) {

companion object {

/**
* 此变量用于表示在网络请求过程过程中抛出了异常
*/
const val CODE_ERROR_LOCAL_UNKNOWN = -1024520

}

/**
* 是否是由于服务器返回的 code != successCode 导致的异常
*/
val isServerCodeBadException: Boolean
get() = this is ServerCodeBadException

/**
* 是否是由于网络请求过程中抛出的异常(例如:服务器返回的 Json 解析失败)
*/
val isLocalBadException: Boolean
get() = this is LocalBadException

}

/**
* API 请求成功了,但 code != successCode
* @param errorCode
* @param errorMessage
*/
class ServerCodeBadException(errorCode: Int, errorMessage: String) : BaseHttpException(errorCode, errorMessage, null) {

constructor(bean: IHttpWrapBean<*>) : this(bean.httpCode, bean.httpMsg)

}

/**
* 请求过程抛出异常
* @param throwable
*/
class LocalBadException(throwable: Throwable) : BaseHttpException(CODE_ERROR_LOCAL_UNKNOWN, throwable.message?: "", throwable)


有时候开发者需要对某些异常情况进行特殊处理,此时就可以来实现自己的 BaseHttpException 子类。例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了,此时开发者就可以实现一个 BaseHttpException 的子类 TokenInvalidException 并在 BaseRemoteDataSource 中进行返回,从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少

class TokenInvalidException : BaseHttpException(CODE_TOKEN_INVALID, "token已失效", null)

open class BaseRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}
}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = "https://restapi.amap.com/v3/"

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

/**
* 如果外部想要对 Throwable 进行特殊处理,则可以重写此方法,用于改变 Exception 类型
* 例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了
* 此时外部就可以实现一个 BaseException 的子类 TokenInvalidException 并在此处返回
* 从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少
*/
override fun generateBaseException(throwable: Throwable): BaseHttpException {
if (throwable is ServerCodeBadException && throwable.errorCode == BaseHttpException.CODE_TOKEN_INVALID) {
return TokenInvalidException()
}
return if (throwable is BaseHttpException) {
throwable
} else {
LocalBadException(throwable)
}
}

/**
* 用于由外部中转控制当抛出异常时是否走 onFail 回调,当返回 true 时则回调,否则不回调
* @param httpException
*/
override fun exceptionHandle(httpException: BaseHttpException): Boolean {
return httpException !is TokenInvalidException
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}
}


收起阅读 »

我愿赌上一包辣条,这些定位相关你不知道

写码写累了不如来看看这些奇奇怪怪的css,腰不酸腿不疼,一阵轻松加愉快(还能偷我表情包)子元素的绝对定位原点在哪?用了那么久的定位,有注意过子元素的绝对定位原点,是在盒子模型的哪一处,在 padding、border 还是 content?实践出真知。既然不确...
继续阅读 »

写码写累了不如来看看这些奇奇怪怪的css,腰不酸腿不疼,一阵轻松加愉快(还能偷我表情包)

子元素的绝对定位原点在哪?

用了那么久的定位,有注意过子元素的绝对定位原点,是在盒子模型的哪一处,在 padding、border 还是 content?


实践出真知。既然不确定那就实操个例子看看。

<div class="father">
father
<div class="child">child</div>
</div>
body {
background-color: rgb(20, 19, 19);
}

.father {
width: 300px;
height: 300px;
margin: 40px;
border: 20px solid rgb(202, 30, 30);
padding: 40px;
position: relative;
background-color: #eee;
}

.child {
width: 50px;
height: 50px;
position: absolute;
top: 0;
left: 0;
background-color: rgb(228, 207, 17);
}

在 Chrome 90 的版本下的表现。



更换 Edge、火狐、IE 浏览器,以及设置 box-sizing 分别为 border-boxcontent-box,最后的结果都表现一致。


从结果上看,绝对定位的字元素是紧贴着父元素的内边框,绝对定位的原点就是在父元素 padding 的左上角。


如果绝对定位的父亲们都没有设置 relative,那么是将会是定位在哪?

body 下只有一个绝对定位的元素,设置了 bottom:0,那么他的表现将会是如何呢?定位在 body 的底边?

<body>
I am body
<div class="absolute">I am absoluted</div>
</body>
html {
background-color: #fff;
}

body {
height: 50vh;
background-color: #ddd;
}
.absolute {
width: 120px;
height: 50px;
position: absolute;
bottom: 0;
left: 0;
background-color: rgb(0, 0, 0);
color: #fff;
}


从结果上看,绝对定位的元素并不是相对于 body 进行定位的,也不是根据 html 标签,此时的 html 的宽高等同于 body 的宽高,而是根据浏览器视口进行定位的。


所有父元素position 的属性是static 的时候,绝对定位的元素会被包含在初始包含块中,初始包含块有着和浏览器视口一样的大小,所以从表现上来看,就是绝对定位的元素是根据浏览器视口定位。


如果把top和left去掉,那么位置依旧是他原来文档流的位置,只是不占空间了,后面的元素会窜上来。

通过 HTML 结构控制层叠上下文

在使用定位属性时,必不可少的使用 z-index 属性,使用 z-index 属性会创建一个层叠上下文。z-index 值不会在整个文档中进行比较,而只会在该层叠上下文中进行比较。

<div class="bar"></div>
<div class="container">
<div class="bottom-box-1">
<div class="top-box"></div>
</div>
</div>
<div class="container">
<div class="box-container">
<div class="bottom-box-2"></div>
<div class="top-box"></div>
</div>
</div>
body {
display: flex;
justify-content: center;
align-items: center;
height: 90vh;
}
.bar {
position: absolute;
top: 65vh;
z-index: 2;
width: 100vw;
height: 20px;
background: #8bbe6e;
}

.top-box {
position: absolute;
z-index: 3;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50%;
height: 50%;
background: #626078;
filter: brightness(60%);
}

.bottom-box-1 {
position: absolute;
top: 45%;
z-index: 1;
transition: top 1s;
width: 40px;
height: 40px;
background: #626078;
}

.container:hover .bottom-box-1 {
top: 72%;
}

.box-container {
position: absolute;
top: 45%;
transition: top 1s;
}

.bottom-box-2 {
position: relative;
z-index: 1;
width: 40px;
height: 40px;
background: #626078;
}

.container {
border: 2px dashed #626078;
height: 80%;
width: 100px;
margin: 20px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}

.container:hover .box-container {
top: 72%;
}


鼠标分别移入两个虚线框内,我们发现,第二个例子 bar 穿过了两个正方形。


这两者的区别就在于 HTML 结构,在第一个例子中,小正方形在大正方形的里面,大正方形在移动的时候,小正方形也随之移动,但是因为大正方形对决定位且有 z-index 属性不为 auto,因此创建了一个层叠上下文,这导致大正方形内的所有元素都是在这个层叠上下文里层叠。


那么第二个例子是怎么解决的呢?


第二个例子的技巧在于引入了一个新的 div 来包裹这两个正方形,这个新的 div 只负责移动。而里面的大小正方形和 bar 处于同一个层叠上下文中。这样子就可以产生 bar 从两个正方形中穿过的效果。


还没懂的来看图来看图:


总结一下创建层叠上下文的几种情况(别怪我枯燥,就是这么多):



  • 文档根元素<html>;

  • position 值为 relative(相对定位)或 absolute(绝对定位)且 z-index 值不为 auto 的元素;

  • position 值为 fixed(固定定位)或 sticky(粘滞定位)的元素;

  • flex (flexbox) 容器的子元素,且 z-index 值不为 auto;

  • grid 容器的子元素,且 z-index 值不为 auto;

  • opacity 属性值小于 1 的元素;

  • mix-blend-mode 属性值不为 normal 的元素;

  • 以下任意属性值不为 none 的元素:

    • transform

    • filter

    • perspective

    • clip-path

    • mask / mask-image / mask-border



  • isolation 属性值为 isolate 的元素;

  • -webkit-overflow-scrolling 属性值为 touch 的元素;

  • will-change 值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素

  • contain 属性值为 layoutpaint 或包含它们其中之一的合成值(比如 contain: strictcontain: content)的元素;


好了,你学废了嘛~


当定位遇到 Transform

transform 下 absolute 宽度被限制

以前,我们设置 absolute 元素宽度 100%, 则都会参照第一个非static值的position祖先元素计算,没有就window. 现在,=-=,需要把transform也考虑在内了。


默认情况下我们设置 absolute 的宽度 100%,会根据第一个不是static的祖先元素计算,没有就找视口宽度。现在也考虑 CSS3 的 transform 属性了。


<div class="relative">
<div class="transform">
<div class="absolute">i am in transform</div>
</div>
</div>
<div class="relative">
<div class="no-transform">
<div class="absolute">i am not in transform</div>
</div>
</div>
.relative {
position: relative;
width: 400px;
height: 100px;
background-color: rgb(233, 233, 233);
}

.transform {
transform: rotate(0);
width: 200px;
}

.no-transform {
width: 200px;
}

.absolute {
position: absolute;
width: 100%;
height: 100px;
background-color: rgb(137, 174, 255);
}


可以看到绝对定位的宽度是相对 transform 的大小计算了。

transform 对 fixed 的限制

身为大聪明的你,加了一行position:fixed安心上线,结果预览机一瞅,没生效??

position:fixed 正常情况下可以让元素不跟随滚动条滚动,这种行为也无法通过 relative/absolute 来限制。但是遇到 transform,他就被打败了,降级成 absolute
<div class="demo">
<div class="box">
<div class="fixed">
<p>没有transform</p>
<img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
</div>
</div>

<div class="relative box">
<div class="fixed">
<p> 有relative</p>
<img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
</div>
</div>
<div class="transform box">
<div class="fixed">
<p>有transform</p>
<img src="https://gitee.com/zukunft/MDImage/raw/master/img/20210516172759.jpg" alt="">
</div>
</div>
</div>
.box {
height: 250px;
}

.demo {
height: 9999px;
}


.fixed {
position: fixed;
}

.relative {
position: relative;
}

.transform {
transform: scale(1);
}


诶,神奇不,滚起来了,就只有被transform包裹的元素会被滚走。
根据W3C的定义,transform属性值会使元素成为一个包含块,它的后代包括absolute元素,fixed元素受限在其 padding box 区域。所以滚动的时候,transform元素被滚走,其子元素也跟随tranform滚走。


关于包含块

上面提到了包含块,那到底如何形成的包含块,包含块又是个啥子

在 MDN 中的解释

The size and position of an element are often impacted by its containing block. Percentage values that are applied to the width, height, padding, margin, and offset properties of an absolutely positioned element (i.e., which has its positionset to absolute or fixed) are computed from the element's containing block.
即一个元素的尺寸和位置受到它的**包含块(containing block)**的影响。对于一些属性,例如 width, height, padding, margin,绝对定位元素的偏移值 (比如 position 被设置为 absolutefixed),当我们设置百分比值的时候,它的这些值计算,就是通过该元素的包含块的值来计算的。

通常情况下,包含块就是这个元素最近的祖先块元素的内容区域,但实际可能不是;


我们可以通过 position 的属性来确定它的包含块;



  1. 如果 position 属性为 staticrelativesticky,包含块可能由它的最近的祖先块元素(比如说 inline-block, blocklist-item 元素)的内容区的边缘组成,也可能会建立格式化上下文(比如说 table container,flex container, grid container, 或者是 block container 自身)

  2. 如果 position 属性为 **absolute** ,包含块就是由它的最近的 position 的值不是 static (也就是值为fixed, absolute, relativesticky)的祖先元素的内边距区的边缘组成。

  3. 如果 position 属性是 fixed,在连续媒体的情况下(continuous media)包含块是 viewport ,在分页媒体(paged media)下的情况下包含块是分页区域(page area)。

  4. 如果 position 属性是 absolutefixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

    1. transformperspective的值不是 none

    2. will-change 的值是 transformperspective

    3. filter的值不是 nonewill-change 的值是 filter(只在 Firefox 下生效).

    4. contain 的值是 paint (例如: contain: paint;)




需要注意的是根元素(<html>)所在的包含块是一个被称为初始包含块的矩形。他的尺寸是视口 viewport (for continuous media) 或分页媒体 page media (for paged media).


如果所有的父元素都没有显式地定义position属性,那么所有的父元素默认情况下 position 属性都是static。结果,绝对定位元素会被包含在初始包含块中;这个初始块容器有着和浏览器视口一样的尺寸,并且<html>元素也被包含在这个容器里面。简单来说,绝对定位元素会被放在<html>元素的外面,并且根据浏览器视口来定位。

总结

遇到奇奇怪怪的css问题不要慌~硬调不是好办法不如来我这瞅瞅~~ 万一就解决了呢✿✿ヽ(°▽°)ノ✿


链接:https://juejin.cn/post/6963235293587750943

收起阅读 »

做了这个活动,感觉自己成了垂直排版css大师(文字竖排)

前段时间协助完成作家的盘点活动,设计大大给的视觉稿如下图。最初拿到稿子时,以为是一个简单的常规活动,实际在完成这个活动的过程中有一点点小波折,自己也在这个过程中学习到了关于垂直排版的知识点,下面来跟大家描述做这个活动的心路历程,一起探索垂直排版的奥秘~文字竖排...
继续阅读 »

前段时间协助完成作家的盘点活动,设计大大给的视觉稿如下图。最初拿到稿子时,以为是一个简单的常规活动,实际在完成这个活动的过程中有一点点小波折,自己也在这个过程中学习到了关于垂直排版的知识点,下面来跟大家描述做这个活动的心路历程,一起探索垂直排版的奥秘~


文字竖排🤔,立马想到使用 writing-mode,改变文字展示的行进方向


大家都知道的writing-mode


writing-mode 可以将文档流由水平方向调整为垂直方向


以p标签内的一段文本为例,对其添加 writing-mode: vertical-rl,可以快速实现如下图所示的文字竖排效果


p标签示例代码如下:

<p class="text-vertical">爱交流,也爱独处</p>

.text-vertical {
writing-mode: vertical-rl;
}

一顿页面基础布局下来,可以看到页面效果和设计稿还是有差异😔,具体如下图的红色框线内;设计稿中红色框线内的数字是“直立向上”展示的


感觉页面还原的展示也无伤大雅,于是拿着对比图找设计大大沟通;但是设计大大还是比较坚持视觉稿上的数字展示效果,以便更好传达活动内的关键信息(用户不需要旋转手机 or 侧头即可快速看清数据

在纠结更好实现方案的时候,去请教了一下张老师,张老师提示可以使用 text-orientation:upright,即使是较差机型,可以优雅降级展示非竖向的效果,不影响活动


向产品咨询了产品用户机型占比,较差机型的占比很低很低,而且产品也接受较低端机型降级的效果,完美💯

部分人知道都text-orientation

text-orientation: upright 可以将垂直排版的(设置了 writing-mode,且值不为 horizontal-tb)内容均直立向上展示


在之前的示例基础上,添加 text-orientation: upright 即可实现将数字直立向上展示,面效果可以看下方图片中红色线框内数字


p标签示例代码见下方:

<p class="text-vertical">999篇日记,12356万字</p>
.text-vertical {
writing-mode: vertical-rl;
text-orientation: upright;
-webkit-text-orientation: upright;
}

调整完成后,活动大体完成,活动进入测试阶段;活动提测后,测试随口提了一句“这个日期看着有一点点不方便”



虽然测试只是随口一说,但我就放在了心上,思考有没有可能优化一下这个日期显示呢?念念不忘,必有回响,偶然发现 text-combine-upright 的属性可以解决此类竖排横向合并

较少人知道的text-combine-upright


text-combine-upright,可以横向合并显示 2-4 字符,正好特别适合垂直排版中日期横向显示


下图中红色线框内,就是添加 text-combine-upright: all 后日期横向合并的效果,“10” 被合并到一起展示,更利于读者快速获取文字信息


示例代码见下方:

<p class="text-vertical">
<span class="upright-combine">10</span>月
<span class="upright-combine">1</span>日时光日记上线
</p>
.text-vertical {
writing-mode: vertical-rl;
text-orientation: upright;
}
.upright-combine {
/* for IE11+ */
-ms-text-combine-horizontal: all;
/* for Chrome/Firefox */
text-combine-upright: all;
/* for Safari */
-webkit-text-combine: horizontal;
}

1)将待合并元素外包裹一层标签


2)添加 text-combine-upright: all


text-combine-upright 属性支持关键字值和数字值,考虑到数字值的兼容性不佳,此处主要是用使用关键字all实现的


3)兼容处理,对 IE 浏览器和 Safari 浏览器做兼容处理(支持 IE11+),说明及代码见下方:


IE 浏览器使用的是 -ms-text-combine-horizontal 属性,与 text-combine-upright 属性一致 Safari 浏览器使用 -webkit-text-combine 属性,仅支持 none 和 horizontal 属性值


项目中实际代码片段为下方所示,可以兼容移动端项目,日期展示优化完成!

.upright-combine {
/* for IE11+ */
-ms-text-combine-horizontal: all;
text-combine-upright: all;
/* forSafari */
-webkit-text-combine: horizontal;
}

text-combine-upright 使用注意事项:



  • 只用于 2-4 字符的横向合并,例如日期、年份等

  • 由于数字值的兼容性不佳,需要将待横向合并的内容包裹标签


至此,该活动顺利完成并上线了,在完成改活动的过程并非一蹴而就。关于数字垂直排版的实现方案,中间也实践过一些其他方案,可能会对大家未来实现类似场景有参考价值~

数字垂直排版的其他方案

最初在考虑实现数字垂直排版的时候,实践过其他方案,下面跟大家分享一下~


1.JS分割大法


JS 将所有的文本信息均切割为单个标签,将单个文本内容都作为 block 元素,此时无需设置 writing-mode,直接设置外段落文本的最大宽度为 1em,即可达到内容竖向 + 数字直立向上效果


核心代码如下所示:

function text2html(element) {
var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
var node;
let list = []
while ((node = treeWalker.nextNode())) {
list.push(node)
}
// 纯文本节点数组遍历
list.forEach(function(node) {
// 纯文本节点切割,替换为span标签
var replaceArr = node.textContent.split('').map(function(str) {
var newSpan = document.createElement('span');
newSpan.textContent = str;
return newSpan;
});
node.replaceWith.apply(node, replaceArr);
})
}

JS 分割大法,将所有文本内容拆分成单个标签,方案简单粗暴,可能会提高样式布局难度,有潜在风险,故最终未采用该方法,但它较适合对局部个别文案处理、兜底展示或者低端机型处理

2.半角数字转全角

数字为全角时,此时垂直展示的时候,它的展示效果和中文类似,天然“直立向上” ,因而可以将半角数字转成全角数字

数字半角转全角的示例代码如下:

// 数字半转全 0-9 对应 半角对应48-57,全角对应65296-65305
function ToDBC(txtstring) {
var newNumber = "";
for(var i = 0; i < txtstring.length; i++) {
if(txtstring.charCodeAt(i) <= 57 && txtstring.charCodeAt(i) >= 48) {
newNumber = newNumber + String.fromCharCode(txtstring.charCodeAt(i)+65248);
} else {
newNumber = newNumber + txtstring.substring(i, i+1);
}
}
return newNumber;
}

数字半角转全角的方法看似非常完美,但是后续有需要额外考虑哪些不转全角,并且涉及到一些数字计算判断时,还需要把全角转半角(全角数字无法直接参与逻辑运算),展示再转全角,处理起来较为繁琐,故最终没选择此技术方案

数字“直立向上”方案小结



  • CSS的 text-orientation

  • 暴力拆解 DOM

  • 调整数字为全角


关于数字“直立向上”的实现,如果大家有其他好的思考角度或者解决方案,欢迎分享交流~


最终的归纳总结



  • 竖向展示排版通过 writing-mode 可快速实现

  • 对于部分数字需要“直立向上”时,采用 text-orientation:upright 方法

  • 日期类字符(2-4字符)可以通过 text-combine-upright 属性横向合并优化展示



链接:https://juejin.cn/post/6966449320744714277
收起阅读 »

6分钟实现CSS炫光倒影按钮

话不多,先看效果: 回归老本行,继续分享简单有趣的CSS创意特效,放松放松心情~实现过程(完整源码在最后):1 老样子,定义基本样式:*{ margin: 0; padding: 0; box...
继续阅读 »

话不多,先看效果:

 回归老本行,继续分享简单有趣的CSS创意特效,放松放松心情~


实现过程(完整源码在最后):

1 老样子,定义基本样式:

*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'fangsong';
}
body{
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(0, 0, 0);
}

font-family: 'fangsong'; 仿宋字体。 display: flex; align-items: center; justify-content: center; flex布局,让按钮在屏幕居中。

2.定义基本标签:

        
aurora






aurora






aurora




3个a标签就对应3个按钮,每个按钮里4个span就是环绕按钮的4条边。 且都有个公共的选择器 .item 和 只属于自己的选择器。

3.定义每个按钮的基本样式:

    .item{
position: relative;
margin: 50px;
width: 300px;
height: 80px;
text-align: center;
line-height: 80px;
text-transform: uppercase;
text-decoration: none;
font-size: 35px;
letter-spacing: 5px;
color: aqua;
overflow: hidden;
-webkit-box-reflect: below 1px linear-gradient( transparent,rgba(6, 133, 133,0.3));
}
text-align: center;文字对齐方式。
line-height: 80px; 字行高。
text-transform: uppercase; 字母为大写。
text-decoration: none; 去掉a标签默认下划线。
letter-spacing: 5px; 每个字符间的距离。
overflow: hidden;溢出隐藏。
-webkit-box-reflect: below 1px linear-gradient( transparent,rgba(6, 133, 133,0.3)); 这个属性能实现倒影效果。

4. 鼠标经过按钮样式改变:

.item:hover{
background-color: aqua;
box-shadow:0 0 5px aqua,
0 0 75px aqua,
0 0 155px aqua;
color: black;
}

box-shadow:0 0 5px aqua, 0 0 75px aqua, 0 0 155px aqua; 阴影,写多行可以叠加更亮。

5.设置环绕按钮的4根线上面那条的样式:

    .item span:nth-of-type(1){
position: absolute;
left: -100%;
width: 100%;
height: 3px;
background-image: linear-gradient(to left,aqua ,transparent);
animation: shang 1s linear infinite;
}
@keyframes shang{
0%{
left:-100%;
}
50%,100%{
left:100%;
}
}

position: absolute;
left: -100%; 定位在对应位置。
background-image: linear-gradient(to left,aqua ,transparent); 线性渐变颜色。
animation: shang 1s linear infinite; 动画属性,让它动起来。


5.以此类推,设置环绕按钮的其它3根样式:

.item span:nth-of-type(2) {
position: absolute;
top: -100%;
right: 0;
width: 3px;
height: 100%;
background-image: linear-gradient(to top, aqua, transparent);
animation: you 1s linear infinite;
animation-delay: 0.25s;
}
@keyframes you {
0% {
top: -100%;
}
50%,
100% {
top: 100%;
}
}
.item span:nth-of-type(3) {
position: absolute;
right: -100%;
bottom: 0;
width: 100%;
height: 3px;
background-image: linear-gradient(to right, aqua, transparent);
animation: xia 1s linear infinite;
animation-delay: 0.5s;
}
@keyframes xia {
0% {
right: -100%;
}
50%,
100% {
right: 100%;
}
}
.item span:nth-of-type(4) {
position: absolute;
bottom: -100%;
left: 0;
width: 3px;
height: 100%;
background-image: linear-gradient(to bottom, aqua, transparent);
animation: zuo 1s linear infinite;
animation-delay: 0.75s;
}
@keyframes zuo {
0% {
bottom: -100%;
}
50%,
100% {
bottom: 100%;
}
}

animation-delay: 0.75s; 动画延迟执行。每条线对应延迟一段时间,形成时间差,形成环绕效果。

6.给第一,第三个按钮设置其它颜色:

    .item1{
filter: hue-rotate(100deg);
}
.item3{
filter: hue-rotate(250deg);
}

完整代码:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'fangsong';
}

body {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: rgb(0, 0, 0);
}

.item {
position: relative;
margin: 50px;
width: 300px;
height: 80px;
text-align: center;
line-height: 80px;
text-transform: uppercase;
text-decoration: none;
font-size: 35px;
letter-spacing: 5px;
color: aqua;
overflow: hidden;
-webkit-box-reflect: below 1px linear-gradient(transparent, rgba(6, 133, 133, 0.3));
}

.item:hover {
background-color: aqua;
box-shadow: 0 0 5px aqua,
0 0 75px aqua,
0 0 155px aqua;
color: black;
}

.item span:nth-of-type(1) {
position: absolute;
left: -100%;
width: 100%;
height: 3px;
background-image: linear-gradient(to left, aqua, transparent);
animation: shang 1s linear infinite;
}

@keyframes shang {
0% {
left: -100%;
}

50%,
100% {
left: 100%;
}
}

.item span:nth-of-type(2) {
position: absolute;
top: -100%;
right: 0;
width: 3px;
height: 100%;
background-image: linear-gradient(to top, aqua, transparent);
animation: you 1s linear infinite;
animation-delay: 0.25s;
}

@keyframes you {
0% {
top: -100%;
}

50%,
100% {
top: 100%;
}
}

.item span:nth-of-type(3) {
position: absolute;
right: -100%;
bottom: 0;
width: 100%;
height: 3px;
background-image: linear-gradient(to right, aqua, transparent);
animation: xia 1s linear infinite;
animation-delay: 0.5s;
}

@keyframes xia {
0% {
right: -100%;
}

50%,
100% {
right: 100%;
}
}

.item span:nth-of-type(4) {
position: absolute;
bottom: -100%;
left: 0;
width: 3px;
height: 100%;
background-image: linear-gradient(to bottom, aqua, transparent);
animation: zuo 1s linear infinite;
animation-delay: 0.75s;
}

@keyframes zuo {
0% {
bottom: -100%;
}

50%,
100% {
bottom: 100%;
}
}

.item1 {
filter: hue-rotate(100deg);
}

.item3 {
filter: hue-rotate(250deg);
}
</style>
</head>

<body>

<a href="#" class="item item1">
aurora
<span></span>
<span></span>
<span></span>
<span></span>
</a>
<a href="#" class="item item2">
aurora
<span></span>
<span></span>
<span></span>
<span></span>
</a>
<a href="#" class="item item3">
aurora
<span></span>
<span></span>
<span></span>
<span></span>
</a>

</body>

</html>


原文:https://juejin.cn/post/6966482130020859912

收起阅读 »

为了能够摸鱼,我走上了歧路

前言 每天都是重复的工作,这样可不行,已经严重影响我的日常摸鱼,为了减少自己日常的开发时间,我决定走一条歧路,铤而走险,将项目中的各种手动埋点统计替换成自动化埋点。以后再也不用担心没时间摸鱼了~ 作为Android届开发的一员,今天我决定将摸鱼方案分享给大家,...
继续阅读 »

前言


每天都是重复的工作,这样可不行,已经严重影响我的日常摸鱼,为了减少自己日常的开发时间,我决定走一条歧路,铤而走险,将项目中的各种手动埋点统计替换成自动化埋点。以后再也不用担心没时间摸鱼了~


作为Android届开发的一员,今天我决定将摸鱼方案分享给大家,希望更多的广大群众能够的加入到摸鱼的行列中~



  1. 声明打点的接口方法
interface StatisticService {

@Scan(ProxyActivity.PAGE_NAME)
fun buttonScan(@Content(StatisticTrack.Parameter.NAME) name: String)

@Click(ProxyActivity.PAGE_NAME)
fun buttonClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)

@Scan(ProxyActivity.PAGE_NAME)
fun textScan(@Content(StatisticTrack.Parameter.NAME) name: String)

@Click(ProxyActivity.PAGE_NAME)
fun textClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
}




  1. 通过动态代理获取StatisticService接口引用
private val mStatisticService = Statistic.instance.create(StatisticService::class.java)




  1. 在合适的埋点位置进行埋点统计,例如Click埋点
  2. fun onClick(view: View) {
    if (view.id == R.id.button) {
    mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
    } else if (view.id == R.id.text) {
    mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
    }
    }



其中2、3步骤都是在对应埋点的类中使用,这里对应的是ProxyActivity

class ProxyActivity : AppCompatActivity() {

// 步骤2
private val mStatisticService = Statistic.instance.create(StatisticService::class.java)

companion object {
private const val BUTTON = "statistic_button"
private const val TEXT = "statistic_text"
const val PAGE_NAME = "ProxyActivity"
}

override fun onCreate(savedInstanceState: Bundle?) {
//...
title = extraData.title

// 步骤3 => 曝光点
mStatisticService.buttonScan(BUTTON)
mStatisticService.textScan(TEXT)
}

private fun getExtraData(): MainModel =
intent?.extras?.getParcelable(ActivityUtils.EXTRA_DATA)
?: throw NullPointerException("intent or extras is null")

// 步骤3 => 点击点
fun onClick(view: View) {
if (view.id == R.id.button) {
mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
} else if (view.id == R.id.text) {
mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
}
}
}



步骤1是创建新的类,不在代码注入的范围之内。自动生成类可以使用注解+process+JavaPoet来实现。类似于ButterKnifeDagger2Room等。之前我也有写过相关的demo与文章。由于不在本篇文章的范围之内,感兴趣的可以自行去了解。


这里我们需要做的是:需要在ProxyActiviy中将2、3步骤的代码转成自动注入。


自动注入就是在现有的类中自动加入我们预期的代码,不需要我们额外的进行编写。


既然已经知道了需要注入的代码,那么接下的问题就是什么时候进行注入这些代码。


这就涉及到Android构建与打包的流程,Android使用Gradle进行构建与打包,


image.png


在打包的过程中将源文件转化成.class文件,然后再将.class文件转成Android能识别的.dex文件,最终将所有的.dex文件组合成一个.apk文件,提供用户下载与安装。


而在将源文件转化成.class文件之后,Google提供了一种Transform机制,允许我们在打包之前对.class文件进行修改。


这个修改时机就是我们代码自动注入的时机。


transform是由gradle提供,在我们日常的构建过程中也会看到系统自身的transform身影,gradle由各种task组成,transform就穿插在这些task中。


图中高亮的部分就是本次自定义的TraceTransform, 它会在.class转化成.dex之前进行执行,目的就是修改目标.class文件内容。


Transform的实现需要结合Gradle Plugin一起使用。所以接下来我们需要创建一个Plugin


创建Plugin


appbuild.gradle中,我们能够看到以下类似的插件引用方式

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: 'trace_plugin'



这里的插件包括系统自带、第三方的与自定义的。其中trace_plugin就是本次自定义的插件。为了能够让项目使用自定义的插件,Gradle提供了三种打包插件的方式



  1. Build Script: 将插件的源代码直接包含在构建脚本中。这样做的好处是,无需执行任何操作即可自动编译插件并将其包含在构建脚本的类路径中。但缺点是它在构建脚本之外不可见,常用在脚本自动构建中。

  2. buildSrc projectgradle会自动识别buildSrc目录,所以可以将plugin放到buildSrc目录中,这样其它的构建脚本就能自动识别这个plugin, 多用于自身项目,对外不共享。

  3. Standalone project: 创建一个独立的plugin项目,通过对外发布Jar与外部共享使用。


这里使用第三种方式来创建Plugin。所以创建完之后的目录结构大概是这样的


为了让别的项目能够引用这个Plugin,我们需要对外声明,可以发布到maven中,也可以本地声明,为了简便这里使用本地声明。

apply plugin: 'java-gradle-plugin'

dependencies {
implementation gradleApi()
implementation localGroovy()
}

gradlePlugin {
plugins {
version {
// 在 app 模块需要通过 id 引用这个插件
id = 'trace_plugin'
// 实现这个插件的类的路径
implementationClass = 'com.rousetime.trace_plugin.TracePlugin'
}
}
}



Pluginidtrace_plugin,实现入口为com.rousetime.trace_plugin.TracePlugin


声明完之后,就可以直接在项目的根目录下的build.gradle中引入该id

plugins {
id "trace_plugin" apply false
}


为了能在app项目中apply这个plugin,还需要创建一个META-INF.gradle-plugins目录,对应的位置如下


注意这里的trace_plugin.properties文件名非常重要,前面的trace_plugin就代表你在build.gradleapply的插件名称。


文件中的内容很简单,只有一行,对应的就是TracePlugin的实现入口

implementation-class=com.rousetime.trace_plugin.TracePlugin

上面都准备就绪之后,就可以在build.gradle进行apply plugin

apply plugin: 'trace_plugin'


这个时候我们自定义的plugin就引入到项目中了。


再回到刚刚的Plugin入口TracePlugin,来看下它的具体实现

class TracePlugin : Plugin {

override fun apply(target: Project) {
if (target.plugins.hasPlugin(AppPlugin::class.java)) {
val appExtension = target.extensions.getByType(AppExtension::class.java)
appExtension.registerTransform(TraceTransform())
}
val methodVisitorConfig = target.extensions.create("methodVisitor", MethodVisitorConfig::class.java)
LocalConfig.methodVisitorConfig = methodVisitorConfig
target.afterEvaluate {
}
}

}



只有一个方法apply,在该方法中我们打印一行文本,然后重新构建项目,在build输出窗口就能看到这行文本

....
> Configure project :app
Trace Plugin start to apply
mehtodVisitorConfig

Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
Use '--warning-mode all' to show the individual deprecation warnings.
...



到这里我们自定义的plugin已经创建成功,并且已经集成到我们的项目中。


第一步已经完成。下面进入第二步。


实现Transform


TracePluginapply方法中,对项目的appExtension注册了一个TraceTransform。重点来了,这个TraceTransform就是我们在gradle构建的过程中插入的Transform,也就是注入代码的入口。来看下它的具体实现

class TraceTransform : Transform() {

override fun getName(): String = this::class.java.simpleName

override fun getInputTypes(): MutableSet = TransformManager.CONTENT_JARS

override fun isIncremental(): Boolean = true

override fun getScopes(): MutableSet = TransformManager.SCOPE_FULL_PROJECT

override fun transform(transformInvocation: TransformInvocation?) {
TransformProxy(transformInvocation, object : TransformProcess {
override fun process(entryName: String, sourceClassByte: ByteArray): ByteArray? {
// use ams to inject
return if (ClassUtils.checkClassName(entryName)) {
TraceInjectDelegate().inject(sourceClassByte)
} else {
null
}
}
}).apply {
transform()
}
}
}



代码很简单,只需要实现几个特定的方法。



  1. getName: Transform对外显示的名称

  2. getInputTypes: 扫描的文件类型,CONENT_JARS代表CLASSESRESOURCES

  3. isIncremental: 是否开启增量,开启后会提高构建速度,对应的需要手动处理增量的逻辑

  4. getScopes: 扫描作用范围,SCOPE_FULL_PROJECT代表整个项目

  5. transform: 需要转换的逻辑都在这里处理


transform是我们接下来.class文件的入口,这个方法有个参数TransformInvocation,该参数提供了上面定义范围内扫描到的所用jar文件与directory文件。


transform中我们主要做的就是在这些jardirectory中解析出.class文件,这是找到目标.class的第一步。只有解析出了所有的.class文件,我们才能进一步过滤出我们需要注入代码的.class文件。


transform的工作流程是:解析.class文件,然后我们过滤出需要处理的.class文件,写入对应的逻辑,然后再将处理过的.class文件重新拷贝到之前的jar或者directory中。


通过这种解析、处理与拷贝的方式,实现偷天换日的效果。


既然有一套固定的流程,那么自然有对应的一套固定是实现。在这三个步骤中,真正需要实现的是处理逻辑,不同的项目有不同的处理逻辑,


对于解析与拷贝操作,已经有相对完整的一套通用实现方案。如果你的项目中有多个这种类型的Transform,就可以将其抽离出来单个module,增加复用性。


解析与拷贝


下面我们来看一下它的核心实现步骤。

fun transform() {
if (!isIncremental) {
// 不是增量编译,将之前的输出目录中的内容全部删除
outputProvider?.deleteAll()
}
inputs?.forEach {
// jar
it.jarInputs.forEach { jarInput ->
transformJar(jarInput)
}
// directory
it.directoryInputs.forEach { directoryInput ->
transformDirectory(directoryInput)
}
}
executor?.invokeAll(tasks)
}



transform方法主要做的就是分别遍历jardirectory中的文件。在这两大种类中分别解析出.class文件。


例如jar的解析transformJar


如果是增量编译,就分别处理增量的不同操作,主要的是ADDEDCHANGED操作。这个处理逻辑与非增量编译的时候一样,都是去遍历jar,从中解析出对应的.class文件。


遍历的核心代码如下

while (enumeration.hasMoreElements()) {
val jarEntry = enumeration.nextElement()
val inputStream = originalFile.getInputStream(jarEntry)

val entryName = jarEntry.name
// 构建zipEntry
val zipEntry = ZipEntry(entryName)
jarOutputStream.putNextEntry(zipEntry)

var modifyClassByte: ByteArray? = null
val sourceClassByte = IOUtils.toByteArray(inputStream)

if (entryName.endsWith(".class")) {
modifyClassByte = transformProcess.process(entryName, sourceClassByte)
}

if (modifyClassByte == null) {
jarOutputStream.write(sourceClassByte)
} else {
jarOutputStream.write(modifyClassByte)
}
inputStream.close()
jarOutputStream.closeEntry()
}



如果entryName的后缀是.class说明当前是.class文件,我们需要单独拿出来进行后续的处理。


后续的处理逻辑交给了transformProcess.process。具体处理先放一放。


处理完之后,再将处理后的字节码拷贝保存到之前的jar中。


对应的directory也是类似 同样是过滤出.class文件,然后交给process方法进行统一处理。最后将处理完的字节码拷贝保存到原路径中。


以上就是Transform的解析与拷贝的核心处理。


处理


上面提到.class的处理都转交给process方法,这个方法的具体实现在TraceTransformtransform方法中

class TraceAsmInject : Inject {

override fun modifyClassByte(byteArray: ByteArray): ByteArray {
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
val classFilterVisitor = ClassFilterVisitor(classWriter)
val classReader = ClassReader(byteArray)
classReader.accept(classFilterVisitor, ClassReader.EXPAND_FRAMES)
return classWriter.toByteArray()
}

}



process中使用TraceInjectDelegateinject来处理过滤出来的字节码。最终的处理会来到modifyClassByte方法。



这里的ClassWriterClassFilterVisitorClassReader都是ASM的内容,也是我们接下来实现自动注入代码的重点。


ASM


ASM是操作Java字节码的一个工具。


其实操作字节码的除了ASM还有javassist,但个人觉得ASM更方便,因为它有一系列的辅助工具,能更好的帮助我们实现代码的注入。


在上面我们已经得到了.class的字节码文件。现在我们需要做的就是扫描整个字节码文件,判断是否是我们需要注入的文件。


这里我将这些逻辑封装到了ClassFilterVisitor文件中。


ASM为我们提供了ClassVisitorMethodVisitorFieldVisitorAPI。每当ASM扫描类的字节码时,都会调用它的visitvisitFieldvisitMethodvisitAnnotation等方法。


有了这些方法,我们就可以判断并处理我们需要的字节码文件。

class ClassFilterVisitor(cv: ClassVisitor?) : ClassVisitor(Opcodes.ASM5, cv) {

override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) {
super.visit(version, access, name, signature, superName, interfaces)
// 扫描当前类的信息
}

override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor {
// 扫描类中的方法
}


override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor {
// 扫描类中的字段
}

}



这是几个主要的方法,也是接下来我们需要重点用到的方法。


首先我们来看个简单的,这个明白了其它的都是一样的。

fun bindData(value: MainModel, position: Int) {
itemView.content.apply {
text = value.content
setOnClickListener {
// 自动注入这行代码
LogUtils.d("inject success.")
if (position == 0) {
requestPermission(context, value)
} else {
navigationPage(context, value)
}
}
}
}



假设我们需要在onClickListener中注入LogUtils.d这个行代码,本质就是在点击的时候输出一行日志。


首先我们需要明白,setOnClickListener本质是实现了一个OnClickListener接口的匿名内部类。


所以可以在扫描类的时候判断是否实现了OnClickListener这个接口,如果实现了,我们再去匹配它的onClick方法,并且在它的onClick方法中进行注入代码。


而类的扫描与方法扫描分别可以使用visitvisitMetho


visit方法中,我们保存当前类实现的接口;在visitMethod中再对当前接口进行判断,看它是否有onClick方法。



namedesc分别为onClick方法的方法名称与方法参数描述。这是字节码匹配方法的一种规范。



如果有的话,说明是我们需要插入的方法,这个时候返回AdviceAdapter。它是ASM提供的便捷针对方法注入的类。我们重写它的onMethodEnter方法。代表我们将在方法的开头注入代码。


onMethodEnter方法中的代码就是LogUtils.dASM注入实现。你可能会说这个是什么,完全看不懂,更别说写字节码注入了。


别急,下面就是ASM的方便之处,我们只需在Android Studio中下载ASM Bytecode Viewer Support Kotlin插件。


该插件可以帮助我们查看kotlin字节码,只需右键弹窗中选择ASM Bytecode Viewer。稍后就会弹出转化后的字节码弹窗。


在弹窗中找到需要注入的代码,具体就是下面这几行

methodVisitor.visitFieldInsn(GETSTATIC, "com/idisfkj/androidapianalysis/utils/LogUtils", "INSTANCE", "Lcom/idisfkj/androidapianalysis/utils/LogUtils;");
methodVisitor.visitLdcInsn("inject success.");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/idisfkj/androidapianalysis/utils/LogUtils", "d", "(Ljava/lang/String;)V", false);



这就是LogUtils.d的注入代码,直接copy到上面提到的onMethodEnter方法中。这样注入的代码就已经完成。


如果你想查看是否注入成功,除了运行项目,查看效果之外,还可以直接查看注入的源码。


在项目的build/intermediates/transforms目录下,找到自定义的TraceTransform,再找到对应的注入文件,就可以查看注入源码。


其实到这来核心内容基本已经结束了,不管是注入什么代码都可以通过这种方法来获取注入的ASM的代码,不同的只是注入的时机判断。


有了上面的基础,我们来实现开头的自动埋点。


实现


为了让自动化埋点能够灵活的传递打点数据,我们使用注解的方式来传递具体的埋点数据与类型。



  1. TrackClickData: 点击的数据

  2. TrackScanData: 曝光的数据

  3. TrackScan: 曝光点

  4. TrackClick: 点击点


有了这些注解,剩下我们要做的就很简单了


使用TrackClickDataTrackScanData声明打点的数据;使用TrackScanTrackClick声明打点的类型与自动化插入代码的入口方法。


我们再回到注入代码的类ClassFilterVisitor,来实现具体的埋点代码的注入。


在这里我们需要做的是解析声明的注解,拿到打点的数据,并且声明的TrackScanTrackClick方法中插入埋点的具体代码。

override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) {
super.visit(version, access, name, signature, superName, interfaces)
mInterface = interfaces
mClassName = name
}



通过visit方法来扫描具体的类文件,在这里保存当前扫描的类的信息,为之后注入代码做准备

override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor {
val filterVisitor = super.visitField(access, name, desc, signature, value)
return object : FieldVisitor(Opcodes.ASM5, filterVisitor) {
override fun visitAnnotation(annotationDesc: String?, visible: Boolean): AnnotationVisitor {
if (annotationDesc == TRACK_CLICK_DATA_DESC) { // TrackClickData 注解
mTrackDataName = name
mTrackDataValue = value
mTrackDataDesc = desc
createFiled()
} else if (annotationDesc == TRACK_SCAN_DATA_DESC) { // TrackScanData注解
mTrackScanDataName = name
mTrackScanDataDesc = desc
createFiled()
}
return super.visitAnnotation(annotationDesc, visible)
}
}
}



visitFiled方法用来扫描类文件中声明的字段。在该方法中,我们返回并实现FieldVisitor,并重新它的visitAnnotation方法,目的是找到之前TrackClickDataTrackScanData声明的埋点字段。对应的就是mTrackModelmTrackScanData


主要包括字段名称name与字段的描述desc,为我们之后注入埋点数据做准备。


另外一旦匹配到埋点数据的注解,说明该类中需要进行自动化埋点,所以还需要自动创建StatisticService。这是打点的接口方法,具体打点的都是通过StatisticService来实现。


visitField中,通过createFiled方法来创建StatisticService类型的字段

private fun createFiled() {
if (!mFieldPresent) {
mFieldPresent = true
// 注入:statisticService 字段
val fieldVisitor = cv.visitField(ACC_PRIVATE or ACC_FINAL, statisticServiceField.name, statisticServiceField.desc, null, null)
fieldVisitor.visitEnd()
}
}



其中statisticServiceField是封装好的StatisticService字段信息。

companion object {
const val OWNER = "com/idisfkj/androidapianalysis/proxy/StatisticService"
const val DESC = "Lcom/idisfkj/androidapianalysis/proxy/StatisticService;"

val INSTANCE = StatisticService()
}

val statisticService = FieldConfig(
Opcodes.PUTFIELD,
"",
"mStatisticService",
DESC
)



创建的字段名为mStatisticService,它的类型是StatisticService


到这里我们已经拿到了埋点的数据字段,并创建了埋点的调用字段mStatisticService;接下来要做的就是注入埋点代码。


核心注入代码在visitMethod方法中,该方法用来扫描类中的方法。所以类中声明的方法都会在这个方法中进行扫描回调。


visitMethod中,我们找到目标的埋点方法,即之前声明的方法注解TrackScanTrackClick


返回并实现AdviceAdapter,重写它的visitAnnotation方法。


该方法会自动扫描方法的注解,所以可以通过该方法来保存当前方法的注解。


然后在onMethodExit中,即方法的开头处进行注入代码。


在该方法中主要做三件事



  1. 向默认构造方法中,实例化statisticService

  2. 注入TrackClick 点击

  3. 注入TrackScan 曝光


具体的ASM注入代码可以通过之前说的SM Bytecode Viewer Support Kotlin插件获取。


有了上面的实现,再来运行运行主项目,你就会发现埋点代码已经自动注入成功。


我们反编译一下.class文件,来看下注入后的java代码


StatisticService初始化

public ProxyActivity() {
boolean var2 = false;
List var3 = (List)(new ArrayList());
this.mTrackScanData = var3;
// 以下是注入代码
this.mStatisticService = (StatisticService)Statistic.Companion.getInstance().create(StatisticService.class);
}



曝光埋点

@TrackScan
public final void onScan() {
this.mTrackScanData.add(new TrackModel("statistic_button", 0L, 2, (DefaultConstructorMarker)null));
this.mTrackScanData.add(new TrackModel("statistic_text", 0L, 2, (DefaultConstructorMarker)null));
// 以下是注入代码
LogUtils.INSTANCE.d("inject track scan success.");
Iterator var2 = this.mTrackScanData.iterator();

while(var2.hasNext()) {
TrackModel var1 = (TrackModel)var2.next();
this.mStatisticService.trackScan(var1.getName());
}

}



点击埋点

@TrackClick
public final void onClick(@NotNull View view) {
Intrinsics.checkParameterIsNotNull(view, "view");
this.mTrackModel.setTime(System.currentTimeMillis() / (long)1000);
this.mTrackModel.setName(view.getId() == 2131230792 ? "statistic_button" : "statistic_text");
// 以下是注入代码
LogUtils.INSTANCE.d("inject track click success.");
this.mStatisticService.trackClick(this.mTrackModel.getName(), this.mTrackModel.getTime());
}



以上自动化埋点代码就已经完成了。


简单总结一下,所用到的技术有



  1. gradle plugin插件的自定义

  2. gradle transform提供编译中字节码的修改入口

  3. asm提供代码的注入实现


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

因为这几个 TypeScript 代码的坏习惯,同事被罚了 500 块

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。1.不使用 strict 模式这种习惯看起来是什么样的没有用严格模式编写 tsconfi...
继续阅读 »

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。

1.不使用 strict 模式

  • 这种习惯看起来是什么样的

没有用严格模式编写 tsconfig.json。

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}
  • 应该怎样

只需启用 strict 模式即可:

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}


  • 为什么会有这种坏习惯

在现有代码库中引入更严格的规则需要花费时间。

  • 为什么不该这样做

更严格的规则使将来维护代码时更加容易,使你节省大量的时间。

2. 用 ||定义默认值

  • 这种习惯看起来是什么样的

使用旧的 ||处理后备的默认值:

function createBlogPost (text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date()
}
}


  • 应该怎样


使用新的 ??运算符,或者在参数重定义默认值。


function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}


  • 为什么会有这种坏习惯


??运算符是去年才引入的,当在长函数中使用值时,可能很难将其设置为参数默认值。



  • 为什么不该这样做


??与 ||不同,??仅针对 null 或 undefined,并不适用于所有虚值。

3. 随意使用 any 类型

  • 这种习惯看起来是什么样的

当你不确定结构时,可以用 any 类型。

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: any = await response.json()
return products
}
  • 应该怎样


把你代码中任何一个使用 any 的地方都改为 unknown


async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}


  • 为什么会有这种坏习惯


any 是很方便的,因为它基本上禁用了所有的类型检查。通常,甚至在官方提供的类型中都使用了 any。例如,TypeScript 团队将上面例子中的 response.json()的类型设置为 Promise。



  • 为什么不该这样做


它基本上禁用所有类型检查。任何通过 any 进来的东西将完全放弃所有类型检查。这将会使错误很难被捕获到。


4. val as SomeType



  • 这种习惯看起来是什么样的


强行告诉编译器无法推断的类型。


async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}


  • 应该怎样


这正是 Type Guard 的用武之地。


function isArrayOfProducts (obj: unknown): obj is Product[] {
return Array.isArray(obj) && obj.every(isProduct)
}

function isProduct (obj: unknown): obj is Product {
return obj != null
&& typeof (obj as Product).id === 'string'
}

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
if (!isArrayOfProducts(products)) {
throw new TypeError('Received malformed products API response')
}
return products
}


  • 为什么会有这种坏习惯


从 JavaScript 转到 TypeScript 时,现有的代码库通常会对 TypeScript 编译器无法自动推断出的类型进行假设。在这时,通过 asSomeOtherType 可以加快转换速度,而不必修改 tsconfig 中的设置。



  • 为什么不该这样做


Type Guard 会确保所有检查都是明确的。

5. 测试中的 as any



  • 这种习惯看起来是什么样的


编写测试时创建不完整的用例。


interface User {
id: string
firstName: string
lastName: string
email: string
}

test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any

expect(createEmailText(user)).toContain(user.firstName)
}


  • 应该怎样


如果你需要模拟测试数据,请将模拟逻辑移到要模拟的对象旁边,并使其可重用。


interface User {
id: string
firstName: string
lastName: string
email: string
}

class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = 'john@doe.com'
}

test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()

expect(createEmailText(user)).toContain(user.firstName)
}


  • 为什么会有这种坏习惯


在给尚不具备广泛测试覆盖条件的代码编写测试时,通常会存在复杂的大数据结构,但要测试的特定功能仅需要其中的一部分。短期内不必关心其他属性。



  • 为什么不该这样做


在某些情况下,被测代码依赖于我们之前认为不重要的属性,然后需要更新针对该功能的所有测试。


6. 可选属性



  • 这种习惯看起来是什么样的


将属性标记为可选属性,即便这些属性有时不存在。


interface Product {
id: string
type: 'digital' | 'physical'
weightInKg?: number
sizeInMb?: number
}


  • 应该怎样


明确哪些组合存在,哪些不存在。


interface Product {
id: string
type: 'digital' | 'physical'
}

interface DigitalProduct extends Product {
type: 'digital'
sizeInMb: number
}

interface PhysicalProduct extends Product {
type: 'physical'
weightInKg: number
}


  • 为什么会有这种坏习惯


将属性标记为可选而不是拆分类型更容易,并且产生的代码更少。它还需要对正在构建的产品有更深入的了解,并且如果对产品的设计有所修改,可能会限制代码的使用。



  • 为什么不该这样做


类型系统的最大好处是可以用编译时检查代替运行时检查。通过更显式的类型,能够对可能不被注意的错误进行编译时检查,例如确保每个 DigitalProduct 都有一个 sizeInMb。


7. 用一个字母通行天下



  • 这种习惯看起来是什么样的


用一个字母命名泛型


function head<T> (arr: T[]): T | undefined {
return arr[0]
}


  • 应该怎样


提供完整的描述性类型名称。


function head<Element> (arr: Element[]): Element | undefined {
return arr[0]
}


  • 为什么会有这种坏习惯


这种写法最早来源于 C++的范型库,即使是 TS 的官方文档也在用一个字母的名称。它也可以更快地输入,只需要简单的敲下一个字母 T 就可以代替写全名。



  • 为什么不该这样做


通用类型变量也是变量,就像其他变量一样。当 IDE 开始向我们展示变量的类型细节时,我们已经慢慢放弃了用它们的名称描述来变量类型的想法。例如我们现在写代码用 constname ='Daniel',而不是 conststrName ='Daniel'。同样,一个字母的变量名通常会令人费解,因为不看声明就很难理解它们的含义。


8. 对非布尔类型的值进行布尔检查



  • 这种习惯看起来是什么样的


通过直接将值传给 if 语句来检查是否定义了值。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}


  • 应该怎样


明确检查我们所关心的状况。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}


  • 为什么会有这种坏习惯


编写简短的检测代码看起来更加简洁,使我们能够避免思考实际想要检测的内容。



  • 为什么不该这样做


也许我们应该考虑一下实际要检查的内容。例如上面的例子以不同的方式处理 countOfNewMessages 为 0 的情况。


9. ”棒棒“运算符



  • 这种习惯看起来是什么样的


将非布尔值转换为布尔值。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}



  • 应该怎样


明确检查我们所关心的状况。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}


  • 为什么会有这种坏习惯


对某些人而言,理解 !!就像是进入 JavaScript 世界的入门仪式。它看起来简短而简洁,如果你对它已经非常习惯了,就会知道它的含义。这是将任意值转换为布尔值的便捷方式。尤其是在如果虚值之间没有明确的语义界限时,例如 null、undefined 和 ''。



  • 为什么不该这样做


与很多编码时的便捷方式一样,使用 !!实际上是混淆了代码的真实含义。这使得新开发人员很难理解代码,无论是对一般开发人员来说还是对 JavaScript 来说都是新手。也很容易引入细微的错误。在对“非布尔类型的值”进行布尔检查时 countOfNewMessages 为 0 的问题在使用 !!时仍然会存在。


10. != null



  • 这种习惯看起来是什么样的


棒棒运算符的小弟 ! = null 使我们能同时检查 null 和 undefined。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}



  • 应该怎样


明确检查我们所关心的状况。


function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}



  • 为什么会有这种坏习惯


如果你的代码在 null 和 undefined 之间没有明显的区别,那么 != null 有助于简化对这两种可能性的检查。



  • 为什么不该这样做


尽管 null 在 JavaScript 早期很麻烦,但 TypeScript 处于 strict 模式时,它却可以成为这种语言中宝贵的工具。一种常见模式是将 null 值定义为不存在的事物,将 undefined 定义为未知的事物,例如 user.firstName=== null 可能意味着用户实际上没有名字,而 user.firstName=== undefined 只是意味着我们尚未询问该用户(而 user.firstName===的意思是字面意思是 ''。


链接:https://juejin.cn/post/6951934677682749477


收起阅读 »

TypeScript进阶, 如何避免 any

为什么会出现 any不知道如何准确的定义出类型,TS 报错了,用 any 能解决,便用 any 了觉得定义类型浪费时间,项目经理催的紧,工期紧张,any 更方便频繁使用 any&n...
继续阅读 »

为什么会出现 any

  • 不知道如何准确的定义出类型,TS 报错了,用 any 能解决,便用 any 了
  • 觉得定义类型浪费时间,项目经理催的紧,工期紧张,any 更方便

频繁使用 any 的弊端

  • 不利于良好的编码习惯
  • 不利于项目的后续维护
  • 会出现很多本可避免的 bug

非必要不使用 any 的好处

  • 良好的代码提示
  • 强大的静态类型检查
  • 可读性和可维护性

所以,我们要对 AnyScript 说不!

TS 容易出现 any 的场景梳理

给 window 全局对象增加属性

常常能见到这样的写法

;(<any>window).obj = {}(
// 或
window as any
).obj = {}

这样做,在使用时和赋值时都需要断言一次,非常麻烦,并且使用时也不能得到代码提示

正确的做法应该是

  1. 在项目全局的 xxx.d.ts 文件中配置如下代码
interface Window {
obj: {}
}
  1. 在需要给 window 赋值的文件目录下级新建一个 @types 文件夹,并在其中新建 index.d.ts 文件,添加如下代码
interface Window {
obj: {}
}

方法 2 也会在全局的 window 上增加 obj 这一声明,如果新增属性使用的跨度比较大,则推荐放在项目的 index.d.ts 中更利于维护,两种方式都在全局给 window 添加了属性,但方法 1 能一眼看出项目中 window 中添加了什么属性


正确使用可选链、非空断言

错误的理解 typescript 的可选参数,而使用断言导致隐患

const a: {
b: string
c?: {
d: string
}
} = {
b: "123",
}

console.log((<any>a).c.d) // 错误,这样访问会报错,应使用可选链
console.log(a.c!.d) // 错误,ts 不会将错误抛出,但实际访问也会报错

! 非空断言与 as 有相似之处,主要用于断言某个属性的值不为 null 和 undefined,它不会影响最终的代码产物,只会影响代码编译过程中的类型校验

?. 可选链操作符 会影响编译后的代码产物,如:

这段 ts 代码

const a = {
c: undefined,
}

const b = a?.c?.d

会被编译为如下 js 代码

"use strict"

var _a$c

const a = {
c: undefined,
}
const b =
a === null || a === void 0
? void 0
: (_a$c = a.c) === null || _a$c === void 0
? void 0
: _a$c.d

将对象属性类型关联起来

对象中有多个属性是联合类型,其中 a 属性和 b 属性是有关联的,a 为 1 时,b 为 stringa 为 2 时,b 为 number 我们通常是这样定义的

const obj: {
a: 1 | 2
b: string | number
} = {
a: 1,
b: "1.2"
}


那么使用时,会造成需要用断言来再次限定 b 的范围的情况,如下代码段所示

if (obj.a === 1) {
const [left, right] = (obj.b as string).split(".")
}
// 如果你偷懒,那可能又变成了这样的情况
if (obj.a === 1) {
const [left, right] = (obj.b as any).split(".")
}

有没有什么办法能让我们不再 as 一次呢?有

const obj: {
a: 1
b: string
} | {
a: 2
b: number
} = {
a: 1,
b: "1.2"
}
// 你会发现这样定义了以后,不需要再次进行断言限定 obj.b 的范围
if (obj.a === 1) {
const [left, right] = obj.b.split(".") // 校验通过
}

如果我们把这样的方法应用到函数(也可以用重载实现)传参或组件传参,有意思的是它还能限定传参的范围, 函数组件实现:


错误的传参,a 与 b 的类型不匹配,校验不通过

确的传参,校验能通过

注意:你不能将 props 解构出来,会导致两者的关系丢失
const { a, b } = props // 错误,a 和 b 的类型关系丢失

是否使用联合类型需要辩证的看待,在任何时候都用上述方法定义可能会造成一些臃肿


巧用类型保护避免断言


typescript 中,常用的类型保护为 typeofinstanceof、和 in 关键字
掌握上述关键字较为容易,可通过文档了解
还有一个关键字 is (类型谓词)是 typescript 提供的,是另一种“类型保护”(这种说法助于理解)


类型谓词能让我们通过函数的形式做出复杂的类型检验的逻辑,一个使用类型谓词的函数的声明往往是如下形式:

type X = xxxx // 某种类型
function check(params): params is X

理解起来就是如果 check 函数返回了真值,则参数 paramsX 类型,否则不一定是 X 类型


设想一下如下场景,某个项目,既可能运行在微信网页中,也可能运行在其他 webview


在微信网页中,微信客户端向 window 对象中注入了各种 native 方法,使用它的方式就是 window.wx.xxxx()


在其他 webview 中,我们假设也有这样的 native 方法,并且使用它的方式为 window.webviewnative.xxxx()


在 typescript 项目中,window 对象上并不会默认存在 wxwebviewnative 两个属性,参考 给 window 全局对象增加属性,我们能显示地为 wxwebviewnative 两个属性定义类型:

interface Window {
wx?: {
xxxx: Function
}
webviewnative?: {
xxxx: Function
}
}

如果你不会这样做,那可能又会写成断言为 any(window as any).wx.xxxx()


可以看到在上面的代码段中两个属性都被我定义为了可选属性,目的是为了在后续维护(迭代)中,防止不做判断直接链式调用


在微信环境中 window.wx 一定存在,但 webviewnative 一定不存在,反之在其他的 webview 中,(见前文假设)window.webviewnative 一定存在


在接口 interface 中,我们并不能动态的知晓和定义到底哪个存在


你可以这样写

if (typeof window.wx !== 'undefined') {
window.wx.xxxx()
} else {
// not in wx
}

但是直接在 if 中写这样的表达式太过局限,或者 有很多方式都能判断是在微信环境中,会导致项目中充斥着五花八门的判断,类型谓词的好处就出来了


function checkIsWxNativeAPICanUse(win: Window): win is { wx: Exclude<Window['wx'], undefined> } & Window {
return typeof window.wx !== 'undefined'
}
// 使用
if (checkIsWxNativeAPICanUse(window)) {
window.wx.xxxx()
}

总结

非必要少使用 any 既是良好的 ts 代码习惯的养成,也是对自己代码质量的较真

原文:https://juejin.cn/post/6961985123923263525



收起阅读 »

iOS-拍照后裁剪,不可拖动照片的问题

问题在项目中,选择照片或拍照的功能很长见,由于我之前采用系统自带的UIimagePickViewController遇到一点问题:1、使用拍照功能,进行截取时(allowEditing = YES)时,拍照完成的图片无法拖动,没有办法进行选择性的截取图片2、如...
继续阅读 »

问题

在项目中,选择照片或拍照的功能很长见,由于我之前采用系统自带的UIimagePickViewController遇到一点问题:

1、使用拍照功能,进行截取时(allowEditing = YES)时,拍照完成的图片无法拖动,没有办法进行选择性的截取图片
2、如果使用选择相册功能,进入裁剪界面,图片是可以拖动的,唯独拍照之后进入裁剪界面无法拖动
3、微信头像更换拍照好像也无法拖动,初步推测可能使用的系统自带的裁剪界面
所以想来仔细研究一下UIImagePickViewController的属性和使用方法

UIImagePickViewController

UIImagePickerController是iOS系统提供的和系统的相册和相机交互的一个类,可以用来获取相册的照片,也可以调用系统的相机拍摄照片或者视频。该类的继承结构是:

UIImagePickerController-->UINavigationController-->UIViewController-->UIResponder-->NSObject

官方文档中对于该类的说明是:

该类只支持竖屏模式,为了保证该类被原样使用,它不支持子类,并且它的视图层次是私有的不能被修改,只支持自定义cameraOverlayView属性来展示更多信息以及和用户的交互。

由于该类继承自UINavgationController,所以在使用过程中一般实现UIImagePickerControllerDelegate和UINavigationControllerDelegate这两个代理,可以利用navgation的push 和pop操作自定义界面实现更复杂的交互效果。

下面具体分析该类的一些方法和属性.

UIImagePickViewController之常用属性

@property (nullable, nonatomic, weak) id <UINavigationControllerDelegate, UIImagePickerControllerDelegate> delegate;

该对象的代理需要实现UINavigationControllerDelegate和UIImagePickerControllerDelegate协议,nullable是xcode6.3之后引入的nullability annotations特性,主要用于在OC和swift之间的转换。这一特性主要包含两个新的类型注释nullable和nonnull,用于表示对象是否可以是NULL或nil

@property (nonatomic) UIImagePickerControllerSourceType sourceType; // default value is UIImagePickerControllerSourceTypePhotoLibrary.

sourceType用于指定要访问的系统的媒体类型。UIImagePickerControllerSourceType支持以下3种枚举类型,默认值是图片库

typedef NS_ENUM(NSInteger, UIImagePickerControllerSourceType) { UIImagePickerControllerSourceTypePhotoLibrary,//照片库模式。图像选取控制器以该模式显示时会浏览系统照片库的根目录。 UIImagePickerControllerSourceTypeCamera, //相机模式,图像选取控制器以该模式显示时可以进行拍照或摄像。 UIImagePickerControllerSourceTypeSavedPhotosAlbum //相机胶卷模式,图像选取控制器以该模式显示时会浏览相机胶卷目录。};

1、PhotoLibrary代表系统照片应用对应的相薄,包含照片流和其它自定义的相册
2、PhotosAlbum则对应系统照片应用的照片,包含用设备拍摄的所有照片流。
3、Camera则代表相机的摄像头。

@property (nonatomic, copy) NSArray<NSString *> *mediaTypes;

mediaTypes用于设置相机支持的功能,拍照或者是视频,返回值类型可以是kUTTypeMovie视频和kUTTypeImage拍照

1、kUTTypeMovie包含

const CFStringRef kUTTypeAudiovisualContent ;抽象的声音视频
const CFStringRef kUTTypeMovie ;抽象的媒体格式(声音和视频)
const CFStringRef kUTTypeVideo ;只有视频没有声音
const CFStringRef kUTTypeAudio ;只有声音没有视频
const CFStringRef kUTTypeQuickTimeMovie ;
const CFStringRef kUTTypeMPEG ;
const CFStringRef kUTTypeMPEG4 ;
const CFStringRef kUTTypeMP3 ;
const CFStringRef kUTTypeMPEG4Audio ;
const CFStringRef kUTTypeAppleProtectedMPEG4Audio;

2、kUTTypeImage包含

const CFStringRef kUTTypeImage ;抽象的图片类型
const CFStringRef kUTTypeJPEG ;
const CFStringRef kUTTypeJPEG2000 ;
const CFStringRef kUTTypeTIFF ;
const CFStringRef kUTTypePICT ;
const CFStringRef kUTTypeGIF ;
const CFStringRef kUTTypePNG ;
const CFStringRef kUTTypeQuickTimeImage ;
const CFStringRef kUTTypeAppleICNS const CFStringRef kUTTypeBMP;
const CFStringRef kUTTypeICO;
@property (nonatomic) BOOL showsCameraControls NS_AVAILABLE_IOS(3_1);
@property (nonatomic) BOOL allowsEditing NS_AVAILABLE_IOS(3_1); // replacement for -allowsImageEditing; default value is NO.
@property (nonatomic) BOOL allowsImageEditing NS_DEPRECATED_IOS(2_0, 3_1);

1、showsCameraControls用于指定拍照时下方的工具栏是否显示
2、allowImageEditing在iOS3.1就已废弃,取而代之的是allowEditing,
3、allowEditing表示拍完照片或者从相册选完照片后,是否跳转到编辑模式对图片裁剪,只有在showsCameraControls为YES时才有效果。

@property (nonatomic) UIImagePickerControllerCameraCaptureMode cameraCaptureMode NS_AVAILABLE_IOS(4_0); // default is UIImagePickerControllerCameraCaptureModePhoto
@property (nonatomic) UIImagePickerControllerCameraDevice cameraDevice NS_AVAILABLE_IOS(4_0); // default is UIImagePickerControllerCameraDeviceRear
@property (nonatomic) UIImagePickerControllerCameraFlashMode cameraFlashMode
@property (nonatomic) CGAffineTransform cameraViewTransform NS_AVAILABLE_IOS(3_1); // set the transform of the preview view.
@property (nullable, nonatomic,strong) __kindof UIView *cameraOverlayView NS_AVAILABLE_IOS(3_1); // set a view to overlay the preview view.

当sourceType是camera的时候,这几个属性有效,否则抛出异常。

· cameraCaptureMode捕捉模式指定的是相机是拍摄照片还是视频,它的枚举类型如下:

NS_ENUM(NSInteger, UIImagePickerControllerCameraCaptureMode) { 
UIImagePickerControllerCameraCaptureModePhoto,//photo
UIImagePickerControllerCameraCaptureModeVideo//video
};

· cameraDevice指定拍摄的摄像头位置,是使用前置摄像头还是后置摄像头,它的枚举类型有:

typedef NS_ENUM(NSInteger, UIImagePickerControllerCameraDevice) { 
UIImagePickerControllerCameraDeviceRear,//后摄像头(默认)
UIImagePickerControllerCameraDeviceFront//前摄像头
};

· cameraFlashMode用于指定闪光灯模式,它的枚举类型如下:

typedef NS_ENUM(NSInteger, UIImagePickerControllerCameraFlashMode) { 
UIImagePickerControllerCameraFlashModeOff = -1,//关闭闪关灯
UIImagePickerControllerCameraFlashModeAuto = 0,//自动
UIImagePickerControllerCameraFlashModeOn = 1//开启闪关灯
};

· cameraViewTransform该结构体可以用于指定拍摄时View的一些形变属性,如旋转缩放等。当showsCameraControls为NO,系统的工具栏隐藏时,我们可以自定义背景View赋值给cameraOverlayView添加到拍摄时的预览视图之上。

@property(nonatomic) NSTimeInterval videoMaximumDuration NS_AVAILABLE_IOS(3_1); // default value is 10 minutes.
@property(nonatomic) UIImagePickerControllerQualityType videoQuality NS_AVAILABLE_IOS(3_1);

· videoMaximumDuration用于设置视频拍摄模式下最大拍摄时长,默认值是10分钟。
· videoQuality表示拍摄的视频质量设置,默认是Medium即表示中等质量。 videoQuality支持的枚举类型如下:

typedef NS_ENUM(NSInteger, UIImagePickerControllerQualityType) { 
UIImagePickerControllerQualityTypeHigh = 0, // 高清模式
UIImagePickerControllerQualityTypeMedium = 1, //中等质量,适于WIFI传播
UIImagePickerControllerQualityTypeLow = 2, //低等质量,适于蜂窝网络传输
UIImagePickerControllerQualityType640x480 NS_ENUM_AVAILABLE_IOS(4_0) = 3, // VGA 质量
UIImagePickerControllerQualityTypeIFrame1280x720 NS_ENUM_AVAILABLE_IOS(5_0) = 4,//1280*720的分辨率
UIImagePickerControllerQualityTypeIFrame960x540 NS_ENUM_AVAILABLE_IOS(5_0) = 5,//960*540分辨率
};

UIImagePickViewController之类方法

@interface UIImagePickerController : UINavigationController <NSCoding>
+ (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType; // returns YES if source is available (i.e. camera present)
+ (nullable NSArray<NSString *> *)availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)sourceType; // returns array of available media types (i.e. kUTTypeImage)
+ (BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0); // returns YES if camera device is available
+ (BOOL)isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0); // returns YES if camera device supports flash and torch.
+ (nullable NSArray<NSNumber *> *)availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice NS_AVAILABLE_IOS(4_0);

isSourceTypeAvailable用于判断当前设备是否支持指定的sourceType,可以是照片库/相册/相机.
isCameraDeviceAvailable判断当前设备是否支持前置摄像头或者后置摄像头
isFlashAvailableForCameraDevice是否支持前置摄像头闪光灯或者后置摄像头闪光灯
availableMediaTypesForSourceType方法返回所特定的媒体如相册/图片库/相机所支持的媒体类型数组,元素值可以是kUTTypeImage类型或者kUTTypeMovie类型的静态字符串,所以是NSString类型的数组
availableCaptureModesForCameraDevice返回特定的摄像头(前置摄像头/后置摄像头)所支持的拍摄模式数值数组,元素值可以是UIImagePickerControllerCameraCaptureMode枚举里面的video或者photo,所以是NSNumber类型的数组

UIImagePickViewController之对象方法

- (void)takePicture NS_AVAILABLE_IOS(3_1);
- (BOOL)startVideoCapture NS_AVAILABLE_IOS(4_0);
- (void)stopVideoCapture NS_AVAILABLE_IOS(4_0);

1、takePicture可以用来实现照片的连续拍摄,需要自己自定义拍摄的背景视图来赋值给cameraOverlayView,结合自定义overlayView实现多张照片的采集,在收到代理的didFinishPickingMediaWithInfo方法之后可以启动额外的捕捉。
2、startVideoCapture用来判断当前是否可以开始录制视频,当视频正在拍摄中,设备不支持视频拍摄,磁盘空间不足等情况,该方法会返回NO.该方法结合自定义overlayView可以拍摄多部视频
3、stopVideoCapture当你调用此方法停止视频拍摄时,它会调用代理的imagePickerController:didFinishPickingMediaWithInfo:方法

UIImagePickViewController之代理方法

@protocol UIImagePickerControllerDelegate<NSObject>
@optional
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(nullable NSDictionary<NSString *,id> *)editingInfo NS_DEPRECATED_IOS(2_0, 3_0);
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info;
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;
@end

1、imagePickerController:didFinishPickingImage:editingInfo:在iOS3.0中已废弃,不再使用。
2、当用户取消选取的内容时会调用DidCancel方法,默认实现销毁弹出的视图。
3、当完成内容的选取时会调用didFinishPickingMediaWithInfo方法,默认info字典的key值可以是以下类型:

UIKIT_EXTERN NSString *const UIImagePickerControllerMediaType; //指定用户选择的媒体类型
UIKIT_EXTERN NSString *const UIImagePickerControllerOriginalImage; // 原始图片
UIKIT_EXTERN NSString *const UIImagePickerControllerEditedImage; // 修改后的图片
UIKIT_EXTERN NSString *const UIImagePickerControllerCropRect; // 裁剪尺寸
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaURL; // 媒体的URL
UIKIT_EXTERN NSString *const UIImagePickerControllerReferenceURL NS_AVAILABLE_IOS(4_1); // 原件的URL
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaMetadata //当数据来源是相机的时候获取到的静态图像元数据,可以使用phtoho框架进行处理

UIImagePickViewController之C语言函数(保存图片或视频)

UIKIT_EXTERN void UIImageWriteToSavedPhotosAlbum(UIImage *image, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo);
UIKIT_EXTERN BOOL UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath) NS_AVAILABLE_IOS(3_1);
UIKIT_EXTERN void UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, __nullable id completionTarget, __nullable SEL completionSelector, void * __nullable contextInfo) NS_AVAILABLE_IOS(3_1);

1、UIImageWriteToSavedPhotosAlbum用来保存照片到相册,seletor应该设置为- (void)image:(UIImage )image didFinishSavingWithError:(NSError )error contextInfo:(void )contextInfo;当照片保存到相册完成时,会调用该方法通知你。
2、UIVideoAtPathIsCompatibleWithSavedPhotosAlbum会返回布尔类型的值判断该路径下的视频能否保存到相册,视频需要先存储到沙盒文件再保存到相册,而照片是可以直接从代理完成的回调info字典里面获取到。
3、UISaveVideoAtPathToSavedPhotosAlbum用来保存视频到相册,seletor应该设置为- (void)video:(NSString )videoPath didFinishSavingWithError:(NSError )error contextInfo:(void )contextInfo;当视频保存到相册或出错时会调用该方法通知你。
这三个方法一般是在代理的完成方法didFinishPickingMediaWithInfo里面配合使用。
以上便是系统UIImagePickViewController的所有属性和方法的简单介绍

问题解决方案

由于使用设置allowEditing = YES属性后,开启摄像头拍照后直接进入系统自定义的裁剪界面,但是在这个界面,从摄像头拍摄过来的照片无法拖动裁剪,仅仅从相册选择过来的照片是可以拖动进行裁剪的,那么我们就决定裁剪界面不使用系统自带的,而自定义一个裁剪界面.

在UIImagePickViewController的代理方法里

#pragma UIImagePickerController Delegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
UIImage *portraitImg = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
YYImageClipViewController *imgClipVC = [[YYImageClipViewController alloc] initWithImage:portraitImg cropFrame:CGRectMake(0, 100.0f, self.view.frame.size.width, self.view.frame.size.width) limitScaleRatio:3.0];
imgClipVC.delegate = self;
[picker pushViewController:imgClipVC animated:NO];
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[picker dismissViewControllerAnimated:YES completion:nil];
}

HZImageCropperViewController是我自定义的一个裁剪界面,通过这个控制器的两个代理方法,得到裁剪后的照片

- (void)imageClip:(YYImageClipViewController *)clipViewController didFinished:(UIImage *)editedImage 
{
//保存图片
NSString *imageFilePath = [UIImage saveImage:editedImage];
//上传七牛服务器
[self upImageFilePath:imageFilePath];
//隐藏裁剪界面
[clipViewController dismissViewControllerAnimated:YES completion:nil];
}

- (void)imageClipDidCancel:(YYImageClipViewController *)clipViewController
{
[clipViewController dismissViewControllerAnimated:YES completion:nil];
}

下面附上一个demo,里面包含裁剪界面的源代码,可供大家参考
自定义裁剪界面demo

转自:https://www.jianshu.com/p/9474ca73e269

收起阅读 »

【环信大学】深入浅出Runtime(2)

逻辑图终端代码及源码clang 命令行及源码.zip


逻辑图


终端代码及源码

clang 命令行及源码.zip




用ts类型系统实现斐波那契数列

一、我们要做什么我们的目的是想要通过TypeScript的类型声明式语法,编程实现一个斐波那契数列算法。换句话说,类似于用现有的机器码到指令集、二进制到十进制、汇编语言到高级编程语言的过程,让类型定义语法也可以实现编程。最终我们要实现的斐波那契数列代码是这样的...
继续阅读 »

一、我们要做什么

我们的目的是想要通过TypeScript的类型声明式语法,编程实现一个斐波那契数列算法。换句话说,类似于用现有的机器码到指令集、二进制到十进制、汇编语言到高级编程语言的过程,让类型定义语法也可以实现编程。

最终我们要实现的斐波那契数列代码是这样的?

const fib = (n: number): number => n <= 1 ? n : fib(n - 1) + fib(n - 2);

for (let i = 0; i < 10; i++) {
console.log(i, fib(i));
}

运行结果如下:


程序完全没问题,完结撒花!

开玩笑的,上面是只一个用了TypeScript类型定义的JavaScript写法,我们其实真正想这样做↓↓↓, 也就是使用TS Type解决FIbonacci

import { Fib, Add } from './fib-type';

type one = Fib<1>;
type zero = Fib<0>;
type Two = Add<one, one>;
type Five = Add<Two, Add<Two, one>>;
type Fib5 = Fib<Five>;
type Fib9 = Fib<9>;
type r0 = Fib<Zero>; // type r0= 0
type r1 = Fib<One>; // type r1 = 1
type r2 = Fib<Two>; // type r2 = 1
type r3 = Fib<3>; // type r3 = 2
type r4 = Fib<4>; // type r4 = 3
type r5 = Fib<5>; // type r5 = 5
type r6 = Fib<6>; // type r6 = 8
type r9 = Fib<9>; // type r9 = 34
type sum = Add<r9, r6>; // type sum = 42

二、我们该怎么做

要想实现斐波那契数列,参考一开始的代码,有基本的比较, 加法, 循环语法, 所以我们也需要使用类型系统依次实现这三种功能

2.1 加法的实现

为了实现加法, 需要先实现一些工具类型

// 元组长度
type Length<T extends any[]> = T['length'];
type one = 1

// 使用extends实现数字相等的比较
type a111 = 0 extends one ? true : false // type a111 = false
type a112 = 1 extends one ? true : false // type a112 = true

range的实现是递归实现的

// 伪代码
function range(n, list=[]){
if(n<=0) return list.length
return range(n-1, [1, ...list])
}

TypeScript的限制, 没有循环, 只能用递归代替循环, 后面会有几个类似的写法, 记住一点:递归有几个出口, 对象就有几个 key, 每个 key 就是一个条件

// 创建指定长度的元组, 用第二个参数携带返回值
type Range<T extends Number = 0, P extends any[] = []> = {
0: Range<T, [any, ...P]>;
1: P;
}[Length<P> extends T ? 1 : 0];

// 拼接两个元组
type Concat<T extends any[], P extends any[]> = [...T, ...P];

type t1 = Range<3>;
// type t1 = [any, any, any]

type Zero = Length<Range<0>>;
// type Zero = 0
type Ten = Length<Range<10>>;
// type Ten = 10

type Five = Length<Range<5>>;
// type Five = 5

type One = Length<Range<1>>;

有了上面的工具语法,那么实现加法就比较容易了, 只需要求两个元组合并后的长度

type Add<T extends number, P extends number> = Length<
Concat<Range<T>, Range<P>>
>;
type Two = Add<One, One>;
// type Two = 2
type Three = Add<One, Two>;
// type Three = 3

有了加法,该如何实现减法呢?一般减法和除法都比加法难, 所以我们需要更多的工具类型函数

2.2 工具函数

2.2.1 实现一些基本工具类型

  • Shift:删除第一个元素
  • Append:在元组末尾插入元素
  • IsEmpty / NotEmpty:判断列表为空
// 去除元组第一个元素 [1,2,3] -> [2,3]
type Shift<T extends any[]> = ((...t: T) => any) extends (
_: any,
...Shift: infer P
) => any
? P
: [];

type pp = Shift<[number, boolean,string, Object]>
// type pp = [boolean, string, Object]

// 向元组中追加
type Append<T extends any[], E = any> = [...T, E];
type IsEmpty<T extends any[]> = Length<T> extends 0 ? true : false;
type NotEmpty<T extends any[]> = IsEmpty<T> extends true ? false : true;
type t4 = IsEmpty<Range<0>>;
// type t4 = true

type t5 = IsEmpty<Range<1>>;
// type t5 = false

2.2.2 逻辑类型

  • Anda && b
// 逻辑操作
type And<T extends boolean, P extends boolean> = T extends false
? false
: P extends false
? false
: true;
type t6 = And<true, true>;
// type t6 = true

type t7 = And<true, false>;
// type t7 = false

type t8 = And<false, false>;
// type t8 = false

type t9 = And<false, true>;
// type t9 = false

2.2.3 小于等于

伪代码: 主要思想是同时从列表中取出一个元素, 长度先到0的列表比较短

function dfs (a, b){
if(a.length && b.length){
a.pop()
b.pop()
return dfs(a,b)
}else if(a.length){
a >= b
}else if (b.length){
b > a
}
}

思想:将数字的比较转换为列表长度的比较

// 元组的小于等于   T <= P, 同时去除一个元素, 长度先到0的比较小
type LessEqList<T extends any[], P extends any[]> = {
0: LessEqList<Shift<T>, Shift<P>>;
1: true;
2: false;
}[And<NotEmpty<T>, NotEmpty<P>> extends true
? 0
: IsEmpty<T> extends true
? 1
: 2];


// 数字的小于等于
type LessEq<T extends number, P extends number> = LessEqList<Range<T>, Range<P>>;

type t10 = LessEq<Zero, One>;
// type t10 = true
type t11 = LessEq<One, Zero>;
// type t11 = false

type t12 = LessEq<One, One>;
// type t12 = true

2.3 减法的实现

减法有两个思路,列表长度相减求值和数字相减求值

2.3.1 列表减法

默认大减小, 小减大只需要判断下反着来, 然后加个符号就行了, 这里为了简单没有实现,可参考伪代码如下:

// 伪代码
const a = [1, 2, 3];
const b = [4, 5];
const c = [];
while (b.length !== a.length) {
a.pop();
c.push(1);
}// c.length === a.length - b.lengthconsole.log(c.length);

// 元组的减法 T - P, 同时去除一个元素, 长度到0时, 剩下的就是结果, 这里使用第三个参数来携带结果, 每次做一次减法, 向第三个列表里面追加
type SubList<T extends any[], P extends any[], R extends any[] = []> = {
0: Length<R>;
1: SubList<Shift<T>, P, Apped<R>>;
}[Length<T> extends Length<P> ? 0 : 1];
type t13 = SubList<Range<10>, Range<5>>;
// type t13 = 5

2.3.2 数字减法

思想:将数字转成元组后再比较

// 集合大小不能为负数, 默认大减小
// 数字的减法
type Sub<T extends number, P extends number> = {
0: Sub<P, T>;
1: SubList<Range<T>, Range<P>>;
}[LessEq<T, P> extends true ? 0 : 1];

type t14 = Sub<One, Zero>;
// type t14 = 1
type t15 = Sub<Ten, Five>;
// type t15 = 5

我们有了这些工具后, 就可以将一开始用JavaScript实现的斐波那契数列的实现代码,翻译为TypeScript类型编码

三、Fib: JS函数 --> TS类型

在JavaScript中,我们使用函数

const fib = (n: number): number => n <= 1 ? n : fib(n - 1) + fib(n - 2);

在TypeScript中,我们使用类型, 其实只是换了一种写法, 用类型函数描述运算, 万变不离其宗~

由于TypeScript递归限制, 并不能求解非常大的项, 不过好玩就完事了~

export type Fib<T extends number> = {
0: T;
1: Add<Fib<Sub<T, One>>, Fib<Sub<T, Two>>>;
}[LessEq<T, One> extends true ? 0 : 1];

type r0 = Fib<Zero>;
// type r10= 0
type r1 = Fib<One>;
// type r1 = 1

type r2 = Fib<Two>;
// type r2 = 1

type r3 = Fib<3>;
// type r3 = 2

type r4 = Fib<4>;
// type r4 = 3

type r5 = Fib<5>;
//type r5 = 5

type r6 = Fib<6>;
// type r6 = 8

四、总结


看了TypeScript实现斐波纳切数列这一套操作有没有让你有体验到重回“实现流水线CPU”的实验室时光?


IT在最近几十年的发展突飞猛进,越来越多的“程序员”加入到了互联网行业,在一些高级语言以及开发框架下,“程序员”的编码也只需要关注业务逻辑实现,很少有人会再去关注计算机底层是怎么实现加减乘除的,当然社会在进步,技术也在日新月异地迭代,偶尔驻足,回忆刚接触计算机编程,在命令行输出第一行“Hello World!”代码那时欣喜的自己,也许那是我们都回不去的青春...



链接:https://juejin.cn/post/6965320374451961886
收起阅读 »

2021不得不学的Typescript

ts作为一门新技术,这两年是越来越火,经过了一段时间的学习和理解之后,写了这篇文章,通过记录ts的核心知识点来带大家轻松掌握typescript,希望能够打动屏幕面前的你。Typescript基础语法TypeScript支持与JavaScript几乎相同的数据...
继续阅读 »

ts作为一门新技术,这两年是越来越火,经过了一段时间的学习和理解之后,写了这篇文章,通过记录ts的核心知识点来带大家轻松掌握typescript,希望能够打动屏幕面前的你。

Typescript基础语法

TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。接下来我们简单介绍一下这几种类型的用法.

基础类型

// 布尔类型
let isFlag:boolean = true

// 数值类型
let myNumber:number = 24

// 字符串类型
let str:string = 'ykk'

// 数组类型, 有两种表示方式,第一种可以在元素类型后面接上[],表示由此类型元素组成的一个数组
let arr:number[] = [1,2,3]
//当数组存在不同类型时
let arr1: (number | string)[] = [1, '2', 3]

// 数组类型, 使用数组泛型(有关泛型后面会详细分析)
let arr:Array<number> = [4,5,6]

// 元组类型, 允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
let yuan: [string, number];
// 初始化yuan
yuan = ['yuan', 12]; // 正确
yuan = [12, 'yuan']; // 错误

// 枚举类型, 可以为一组数值赋予友好的名字
enum ActionType { online, offline, deleted }
let action:ActionType = ActionType.offline // 1

// any, 表示任意类型, 可以绕过类型检查器对这些值进行检查
let color:any = 1
color = 'red'

// void类型, 当一个函数没有返回值时,通常会设置其返回值类型是 void
function getName(): void {
console.log("This is my name");
}

// object类型, 表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型
let obj:object;
obj = {num: 1}

// 对象类型也可以写成
const t = {
name:string,
age:number
} = {
name:'ykk',
age:18
}

// 函数类型也属于一种对象类型(该实例返回值必须是number类型)
const getTt:() => number = () => {
return 123;
}

//never类型(永远不会执行never之后的逻辑)
function errorEmitter(): never {
throw new Error();
console.log(123)
}

接口(Interface)

interface Person {
name: string;
age: number;
phone: number;
}

let man:Person = {
name: 'ykk',
age: 18,
phone: 13711111111
}

类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。其次我们还可以定义可选属性只读属性。 可选属性表示了接口里的某些属性不是必需的,所以可以定义也可以不定义。·可读属性·使得接口中的某些属性只能读取而不能赋值,如下:

interface Person {
name: string;
age?: number;
readonly phone: number;
}

在实际场景中, 我们往往还会遇到不确定属性名和属性值类型的情况, 这个时候我们可以利用索引签名来设置额外的属性和类型, 如下:

interface Person {
name: string;
[propName: string]: any; //这里表示除了有name之外还可以有其他的任何属性,但是属性名必须是string类型,属性值任意类型都可以
}

当然接口也可以进行继承

//此时human这个interface就拥有Person的所有属性,并且在调用的时候也必须满足Proson的要求,否则就会报错
interface human extends Person {
weight:55
}

和js的class一致, typescript的类有公共(public)私有(private)受保护(protected)的访问类型。

  • public 在TypeScript里,成员都默认为 public,表示允许它在定义类的外部被调用

  • private 表示它能在定义的类内使用,不能在该类的外部使用

  • protected 和private类似, 但是protected允许在类内及继承的子类中使用

class Person {
public name:string = 'ykk';
private age:number = 18;
protected weight:number = 55;
constructor(){

}
}

class Man extends Person {
public say(){
return this.weight
}
}

let p = new Person()
let m = new Man()
console.log(p.name) //ykk
console.log(p.age) //报错age是private属性
console.log(m.say()) //55

由于在js中,getter 和 setter不能直接使用,我们需要通过一个Object.defineProperty来定义触发,那么在ts中就简单多了在类中直接能声明:

class Person {
private _food: string = 'apple'
get food() {
return this._food
}
set food(name: string) {
this._food = name
}
}
let p = new Person()
console.log(p.food) //apple
p.food="cookie" //这里重新设置food

typescript中static这个关键字是把这个方法或者属性直接挂在类上,而不是挂在new出来的实例。设计模式中经典的单例模式,用它最合适不过了!

class Demo {
private static instance: Demo;
peivate contructor(public name:string){}

static getInstance(){
if(!this.instance){
this.instance = new Demo('ykk')
}
return this.instance;
}
}

const demo1 = Demo.getInstance();
const demo2 = Demo.getInstance();
console.log(demo1.name)
console.log(demo2.name)

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法, 这里需要注意的是不能创建一个抽象类的实例

abstract class MyAbstract {
constructor(public name: string) {}
say(): void {
console.log('say name: ' + this.name);
}
abstract sayBye(): void; // 必须在派生类中实现
}

class SubMyAbstract extends MyAbstract {
constructor() {
super('ykk'); // 在派生类的构造函数中必须调用 super()
}
sayBye(): void {
console.log('bye');
}
getOther(): void {
console.log('loading...');
}
}

let department: MyAbstract; // 允许创建一个对抽象类型的引用
department = new SubMyAbstract(); // 允许对一个抽象子类进行实例化和赋值
department.say();
department.sayBye();

department = new MyAbstract(); // 错误: 不能创建一个抽象类的实例
department.getOther(); // 错误: 方法在声明的抽象类中不存在

Typescript进阶语法

联合类型和类型保护

所谓联合类型是用于限制传入的值的类型只能是 | 分隔的每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。例如:

interface person1 {
name: string;
age: number;
}

interface person2 {
hby: string;
age: number;
}
let man:person1 | person2

如果一个值是联合类型,那么我们只能访问它们中共有的部分(共有的属性与方法),由于只能访问共有,导致我们在想要访问某一个的时候ts会提示报错,这时我们就需要类型保护

let man:person1 | person2;
man = {
name: 'ykk',
age: 18,
hby: 'basketball'
}
//使用as直接断言,告诉ts在哪里去找
if((me as person1).name) {
console.log((me as person1).name);
}

if((me as person1).name) {
console.log((me as person2).hby);
}
//使用in
if(('name' in me)) {
console.log(me.name);
}

if('hby' in me) {
console.log(me.hby);
}
//使用typeof
function add(one:number|string,two:number|string){
if(typeof one=="string"||typeof two=="string"){
retrun `${one}${two}`
}
retrun one+two
}
//使用instanceof
class a{
num:1
}
function b(obj:object|a){
if(obj instanceof a){
retrun obj.num
}
}

泛型

什么是泛型呢,我的理解就是泛指的类型,那他在ts中应该怎么写呢?

//定义是用尖括号表示一个变量
function iSay<T>(arg: T): T {
return arg;
}
// 调用的时候去声明类型
let come = iSay<number>(123);

当然了泛型有多种使用方式,接下来咱们一一探索。

函数泛型
//传入一个数组
function say<T>(arr:T[]){
...
};
say<number>([11,22,33]);

//传入多个泛型
function say<T, F>(name:T, age:F){
...
};
say<string, number>('ykk', 18);
类中的泛型
class say<T>{
constructor(name:T){
...
}
}
var t = new say<string>("1")
泛型的继承
class say<T extends number|string>{
constructor(one:T){
...
}
}
var t = new say<string>("1") //这时候表示泛型只能是number类型或者string类型其中的一种,否则会报错
泛型中使用keyof

泛型中使用keyof顾名思义就是遍历一个interface,并且每次的泛型类型就是当前interface的key.interface persons {

    name:string,
age:number,
get:boolean
}
var p = {
name:'ykk',
age:18,
get:false
}
function add<T extends keyof persons>(key:T):persons[T]{
return p[key]
}
//如此一来我们便能知道返回值的准确类型了
var p1 = add('name')
console.log(p1) //ykk

命名空间

ts在我们使用的时候如果用面向对象的方式声明多个类生成实例的时候,你会发现在全局就会多出几个实例,这样就会导致全局污染,如此一来,我们便需要namespace这个关键字,来防止全局污染。

namespace Main{
class circle {
...
}

class rect {
...
}

//如果想要导出给外部使用,需要导出
export class san{
...
}
}
//这样在全局只会有一个Main供我们使用了

声明全局变量

对于使用未经声明的全局函数或者全局变量, typescript往往会报错。最常见的例子就是我们在引入js文件的时候,往往会报下面的错误:


这个时候有两种解决方式:

  • 按照提示执行相对于的npm install @types/xxx的命令。
  • 可以在对应位置添加xxx.d.ts文件, 并在里面声明我们所需要的变量, ts会自动检索到该文件并进行解析,如下:
//定义一个全局类型, 并编写相应的逻辑让ts识别相应的js语法
declare var superagent;
...



原文:https://juejin.cn/post/6966151454914510878

收起阅读 »

iOS 详解socket编程[oc]粘包、半包处理

在做socket编程时,如果是做tcp连接,那就不可避免的会遇到粘包与半包的问题。粘包 就是多组数据被一并接收了,粘在了一起,无法做划分;半包 就是有数据接收不完整,无法处理。要解决粘包、半包的问题,一般在设计数据(消息)格式时会约定好一个字段专门用于描述数据...
继续阅读 »

在做socket编程时,如果是做tcp连接,那就不可避免的会遇到粘包与半包的问题。

粘包 就是多组数据被一并接收了,粘在了一起,无法做划分;

半包 就是有数据接收不完整,无法处理。

要解决粘包、半包的问题,一般在设计数据(消息)格式时会约定好一个字段专门用于描述数据包的长度,这样就使数据有了边界,依靠这个边界,就能把每组数据划分出来,数据不完整时也能获知数据的缺失。

(当然也可以把数据设计成定长数据,但这样不够灵活;或者用\n,\r这类字符作为数据划分依据,但不直观、不明确,同时也不灵活)

举个栗子:

消息=消息头+消息体。消息头用于描述消息本身的基本信息,消息体则为消息的具体内容


如上图所示,假如我们的一个消息是这么定义的

消息头 = msgId(4B)+version(2B)+len(4B),共占用10字节

消息体 =  len中描述的16字节长

所以这条消息的长度就是 26字节

可以看到,要想知道一条完整数据的边界,关键就是消息头中的len字段

假如我们现在接收到的数据是这样的:


这个情况下即包含了粘包,也出现了半包的情况,三个数据包粘在了一起,最后一个数据包没有接收完全,出现了半包的情况,看看代码如何处理

- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag 
{
while (_readBuf.length >= 10)//因为头部固定10个字节,数据长度至少要大于10个字节,我们才能得到完整的消息描述信息
{
NSData *head = [_readBuf subdataWithRange:NSMakeRange(0, 10)];//取得头部数据
NSData *lengthData = [head subdataWithRange:NSMakeRange(6, 4)];//取得长度数据
NSInteger length = [[[NSString alloc] initWithData:lengthData encoding:NSUTF8StringEncoding] integerValue];//得出内容长度
NSInteger complateDataLength = length + 10;//算出一个包完整的长度(内容长度+头长度)
if (_readBuf.length >= complateDataLength)//如果缓存中数据够一个整包的长度
{
NSData *data = [_readBuf subdataWithRange:NSMakeRange(0, complateDataLength)];//截取一个包的长度(处理粘包)
[self handleTcpResponseData:data];//处理包数据
//从缓存中截掉处理完的数据,继续循环
_readBuf = [NSMutableData dataWithData:[_readBuf subdataWithRange:NSMakeRange(complateDataLength, _readBuf.length - complateDataLength)]];
}
else//如果缓存中的数据长度不够一个包的长度,则包不完整(处理半包,继续读取)
{
[_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
return;
}
}
//缓存中数据都处理完了,继续读取新数据
[_socket readDataWithTimeout:-1 buffer:_readBuf bufferOffset:_readBuf.length tag:0];//继续读取数据
}


摘自:https://www.jb51.net/article/105278.htm


收起阅读 »

Socket简析与iOS实现

Socket的基本概念1.定义网络上两个程序通过一个双向通信连接实现数据交互,这种双向通信的连接叫做Socket(套接字)。2.本质网络模型中应用层与TCP/IP协议族通信的中间软件抽象层,是它的一组编程接口(API),也即对TCP/IP的封装。TCP/IP也...
继续阅读 »

Socket的基本概念

1.定义

网络上两个程序通过一个双向通信连接实现数据交互,这种双向通信的连接叫做Socket(套接字)。

2.本质

网络模型中应用层与TCP/IP协议族通信的中间软件抽象层,是它的一组编程接口(API),也即对TCP/IP的封装。TCP/IP也要提供可供程序员做网络开发所用的接口,即Socket编程接口。


3.要素

Socket是网络通信的基石,是支持TCP/IP协议的网络通信的基本操作单元,包含进行网络通信的必须的五种信息:

  • 连接使用的协议
  • 本地主机的IP地址
  • 本地进程的协议端口
  • 远程主机的IP地址
  • 远程进程的协议端口

4.特性

Socket可以支持不同的传输协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接;同理,当使用UDP协议进行连接时,该Socket连接就是一个UDP连接。

多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

5.连接

建立Socket连接至少需要一对套接字,分别运行于服务端(ServerSocket)和客户端(ClientSocket)。套接字直接的连接过程氛围三个步骤:

Step 1 服务器监听

服务端Socket始终处于等待连接状态,实时监听是否有客户端请求连接。

Step 2 客户端请求

客户端Socket提出连接请求,指定服务端Socket的地址和端口号,这时就可以向对应的服务端提出Socket连接请求。

Step 3 连接确认

当服务端Socket监听到客户端Socket提出的连接请求时作出响应,建立一个新的进程,把服务端Socket的描述发送给客户端,该描述得到客户端确认后就可建立起Socket连接。而服务端Socket则继续处于监听状态,继续接收其他客户端Socket的请求。

iOS客户端Socket的实现

1. 数据流方式

- (IBAction)connectToServer:(id)sender {
// 1.与服务器通过三次握手建立连接
NSString *host = @"192.168.1.58";
int port = 1212;

//创建一个socket对象
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self
delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];

NSError *error = nil;

// 开始连接
[_socket connectToHost:host
onPort:port
error:&error];

if (error) {
NSLog(@"%@",error);
}
}


#pragma mark - Socket代理方法
// 连接成功
- (void)socket:(GCDAsyncSocket *)sock
didConnectToHost:(NSString *)host
port:(uint16_t)port {
NSLog(@"%s",__func__);
}


// 断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock
withError:(NSError *)err {
if (err) {
NSLog(@"连接失败");
} else {
NSLog(@"正常断开");
}
}


// 发送数据
- (void)socket:(GCDAsyncSocket *)sock
didWriteDataWithTag:(long)tag {

NSLog(@"%s",__func__);

//发送完数据手动读取,-1不设置超时
[sock readDataWithTimeout:-1
tag:tag];
}

// 读取数据
-(void)socket:(GCDAsyncSocket *)sock
didReadData:(NSData *)data
withTag:(long)tag {

NSString *receiverStr = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];

NSLog(@"%s %@",__func__,receiverStr);
}

2.基于第三方开源库CocoaAsyncSocket

2.1客户端通过地址和端口号与服务端建立Socket连接,并写入相关数据。

- (void)connectToServerWithCommand:(NSString *)command
{
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
[_socket setUserData:command];

NSError *error = nil;
[_socket connectToHost:WIFI_DIRECT_HOST onPort:WIFI_DIRECT_PORT error:&error];
if (error) {
NSLog(@"__connect error:%@",error.userInfo);
}

[_socket writeData:[command dataUsingEncoding:NSUTF8StringEncoding] withTimeout:10.0f tag:6];
}

2.2 实现CocoaAsyncSocket的代理方法

#pragma mark - Socket Delegate

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"Socket连接成功:%s",__func__);
}

-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
if (err) {
NSLog(@"连接失败");
}else{
NSLog(@"正常断开");
}

if ([sock.userData isEqualToString:[NSString stringWithFormat:@"%d",SOCKET_CONNECT_SERVER]])
{
//服务器掉线 重新连接
[self connectToServerWithCommand:@"battery"];
}else
{
return;
}
}

-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
NSLog(@"数据发送成功:%s",__func__);
//发送完数据手动读取,-1不设置超时
[sock readDataWithTimeout:-1 tag:tag];
}

-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
NSString *receiverStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"读取数据:%s %@",__func__,receiverStr);
}

摘自:https://www.jianshu.com/p/8e599ca5dfe8

收起阅读 »

iOS 接入WebSocket

WebSocket是什么WebSocket协议是 基于TCP 的一种网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。WebSocket基本原理帧协议:0 ...
继续阅读 »

WebSocket是什么

WebSocket协议是 基于TCP 的一种网络协议。
它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。


WebSocket基本原理

帧协议:

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+

  • fin(1 bit):指示该帧是否构成该消息的最终帧。大多数情况下,消息适合于一个单一的帧,这一点总是默认设置的。实验表明,Firefox 在 32K 之后创建了第二个帧。
  • rsv1,rsv2,rsv3(1 bit each):必须为0,除非扩展里协商定义了非零值的含义。如果收到一个非零值,并且协商的扩展中没有一个定义这个非零值的含义,那么接收端必须抛出失败连接。

  • opcode(4bits):展示了帧表示什么。以下值目前正在使用:
    0x00:这个帧继续前面的有效载荷。
    0x01:此帧包含文本数据。
    0x02:这个帧包含二进制数据。
    0x08:这个帧终止连接。
    0x09:这个帧是一个 ping 。
    0x0a:这个帧是一个 pong 。
    (正如你所看到的,有足够的值未被使用,它们已被保留供将来使用)。
  • payload:最可能被掩盖的实际数据。它的长度是 payload_len 的长度。
  • masking-key(32 bits):从客户端发送到服务器的所有帧都被帧中包含的 32 位值掩盖。
  • 0-125 表示有效载荷的长度。 126 表示以下两个字节表示长度,127 表示接下来的 8 个字节表示长度。所以有效负载的长度在 〜7bit,16bit 和 64bit 括号内。
  • payload_len(7 bits):有效载荷的长度。 WebSocket 的帧有以下长度括号:
  • mask(1 bit):指示连接是否掩盖。就目前而言,从客户端到服务器的每条消息都必须掩盖,如果规范没有掩盖,规范就会终止连接。

其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

WebSocket常见的使用场景

要求服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。(也可以采用HTTP/2)

iOS端实现WebSocket连接的参考方案

SocketRocket

SocketRocket是facebook封装的websocket开源库,采用纯Objective-C编写。
使用者需要自己实现心跳机制,以及适配断网重连等情况。

SocketIO

SocketIO将WebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口,也就是说,我们在使用SocketIO时,不用担心兼容问题,底层会自动选用最佳的通信方式。因此说,WebSocket是SocketIO的一个子集。

另外,如果后端采用的是原生WebSocket,不建议大家使用SocketIO。
因为SocketIO定制了专有的协议,并不是纯粹的WebSocket,可能会遭遇适配问题。
不过,SocketIO的API极其易用!!!

Starscream

采用Swift编写,不过笔者暂时还没有用过,不发表任何评论。

iOS端利用SocketRocket实现WebSocket连接

示例代码如下,欢迎指正:

import Foundation
import SocketRocket
import Alamofire

/// Websocket连接中的通知
let FSWebSocketConnectingNotification = NSNotification.Name.init("FSWebSocketConnectingNotification")
/// Websocket连接成功的通知
let FSWebSocketDidOpenNotification = NSNotification.Name.init("FSWebSocketDidOpenNotification")
/// Websocket连接收到新消息的通知
let FSWebSocketDidReceiveMessageNotification = NSNotification.Name.init("FSWebSocketDidReceiveMessageNotification")
/// Websocket连接失败的通知
let FSWebSocketFailWithErrorNotification = NSNotification.Name.init("FSWebSocketFailWithErrorNotification")
/// Websocket连接已关闭的通知
let FSWebSocketDidCloseNotification = NSNotification.Name.init("FSWebSocketDidCloseNotification")

/// websocket连接地址,请输入有效的websocket地址!
let WSAddr = "ws://host:port/ws"

/// 心跳包发送间隔,3分钟
let PingDuration = 180

/// Websocket连接对象
class FSWebsocket: NSObject,SRWebSocketDelegate {

static let `default` = FSWebsocket.init()
/// 是否在断开连接后自动重连
var autoReconnect = true

private var reachabilityManager = NetworkReachabilityManager.init(host: "www.baidu.com")
/// websocket连接地址
private var addr = WSAddr
/// websocket连接
private var ws:SRWebSocket?
/// 心跳包定时发送计时器
private var heartbeatTimer:Timer?
/// 重连计数器
private var reconnectCount = 0

override init() {
super.init()

NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)

// 切换网络自动重连
reachabilityManager?.listener = { [weak self] (networkReachabilityStatus) in
// 正在连接中或已经连接成功,不需要重连
if self?.ws?.readyState == .CONNECTING || self?.ws?.readyState == .OPEN{
return
}
// 设置自动重连才会进行自动重连
if let s = self, s.autoReconnect{
self?.reconnect()
}
}
reachabilityManager?.startListening()
}
deinit {
NotificationCenter.default.removeObserver(self)
}

@objc func appDidEnterBackground(){

close()
}
@objc func appWillEnterForeground(){

reconnect()
}

// MARK: - Send Message

@discardableResult
func sendJSON(_ json:[AnyHashable : Any]) -> NSError?{
let jsonData = try? JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted)
return self.sendData(jsonData)
}

@discardableResult
func sendData(_ data:Data?) -> NSError?{

guard ws?.readyState == SRReadyState.OPEN else{
return NSError.init(domain: "FSWebsocket", code: SRStatusCodeGoingAway.rawValue, userInfo: nil)
}
ws?.send(data)
return nil
}

// MARK: - Connection Management

/// 连接websocket服务器
func open(){

let url = URL(string: self.addr)
assert(url != nil)

self.ws = SRWebSocket.init(url: url!)
ws?.delegate = self
ws?.open()
NotificationCenter.default.post(name: FSWebSocketConnectingNotification, object: self)
}

/// 重新连接
private func reconnect(){

if !autoReconnect{
return
}
if reconnectCount > 64 {
return
}

let seconds = DispatchTime.now().uptimeNanoseconds + UInt64(reconnectCount) * NSEC_PER_SEC
let time = DispatchTime.init(uptimeNanoseconds: seconds)
DispatchQueue.main.asyncAfter(deadline: time) {
self.ws?.close()
self.ws = nil

self.open()
}

if reconnectCount == 0 {
reconnectCount = 1
}
reconnectCount *= 2
}

/// 关闭连接
func close(){

self.destroyHeartbeat()

self.ws?.close()
self.ws = nil

resetConnectCount()
}

/// 发送心跳包
@objc func heartbeat(){

guard ws?.readyState == SRReadyState.OPEN else{
return
}
self.ws?.sendPing(nil)
}
/// 启动心跳
private func startHeartbeat(){

let timer = Timer.init(timeInterval: TimeInterval(PingDuration), target: self, selector: #selector(heartbeat), userInfo: nil, repeats: true)
self.heartbeatTimer = timer
RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)
}
/// 停止心跳
private func destroyHeartbeat(){

self.heartbeatTimer?.invalidate()
self.heartbeatTimer = nil
}
/// 重置重连尝试次数
private func resetConnectCount(){

reconnectCount = 0
}


// MARK: - SRWebSocketDelegate

func webSocketDidOpen(_ webSocket: SRWebSocket!) {

self.resetConnectCount()
self.startHeartbeat()
NotificationCenter.default.post(name: FSWebSocketDidOpenNotification, object: self)
}

func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) {

guard let msgString = message as? NSString else{
return
}
guard let msgData = msgString.data(using: String.Encoding.utf8.rawValue) else{
return
}
// 如果传输的是JSON数据,可以使用JSONSerialization将JSON字符串转换为字典
guard let msgJSON = try? JSONSerialization.jsonObject(with: msgData, options: JSONSerialization.ReadingOptions.allowFragments) else{
return
}
NotificationCenter.default.post(name: FSWebSocketDidReceiveMessageNotification, object: self, userInfo: ["message":msgJSON])
}

func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Error!) {

NotificationCenter.default.post(name: FSWebSocketFailWithErrorNotification, object: self, userInfo: ["error": error])
// 连接失败:
if (error as NSError).code == 50{
// 断网,不重连
return
}
// 当前页面不需要使用websocket,不重连
// 重连次数超过限制,不重连
reconnect()
}

func webSocket(_ webSocket: SRWebSocket!, didReceivePong pongPayload: Data!) {
// 心跳包响应回调
}

func webSocket(_ webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) {

// 连接被关闭
NotificationCenter.default.post(name: FSWebSocketDidCloseNotification, object: self, userInfo: ["code": code, "reason": reason, "wasClean": wasClean])

close()
}
}



摘自链接:https://www.jianshu.com/p/934c0d79f75e

收起阅读 »

【性能优化】关键性能指标及测量标准

前言随着时代的发展,业内曾经提出过很多性能优化相关的标准和规范,从最初2007年雅虎提出34条军规,到2020年google开始推广Web Vitals,优化技术已经有将近20多年的发展历史,如下图所示:发展过程中产生了许多的性能指标、测量工具、优化手段等等,...
继续阅读 »

前言

随着时代的发展,业内曾经提出过很多性能优化相关的标准和规范,从最初2007年雅虎提出34条军规,到2020年google开始推广Web Vitals,优化技术已经有将近20多年的发展历史,如下图所示:


发展过程中产生了许多的性能指标、测量工具、优化手段等等,本文主要讲述关键性能指标及测量标准。


性能指标,顾名思义,就是性能优化过程中参考的一些标准。


进行性能优化,指标就是我们的一个抓手,首先你就要确定它的指标,然后才能根据指标去采取措施,否则就会像无头苍蝇一样乱撞,没有执行目标。

什么样的指标值得我们关注?

Web Vitals

Google在2020年推出了一个名为Web Vitals的新概念,着重评估一组页面访问体验钟的一部分关键指标,目的在于简化网络性能。每个指标代表页面体验的一个关键方面:加载、交互和视觉稳定性



Web Vitals在感知的性能,交互性和令人愉悦的方面,可作为用户体验的简化基准。在考虑页面性能时,应该首先关注这一套较小的指标。


此外,Web Vitals代表了访问者在访问您的页面时首先在其视口中看到的内容,即首屏内容。他们首先看到的内容最终会影响他们对页面性能的看法。


首先,专注于这三个指标,可以使您获得可感知的和实际的性能可观的收益,然后再深入研究其他优化。


加载


所谓加载,就是进入页面时,页面内容的载入过程。比如,当你打开一些网站时,你会发现,有的网站首页上的文字、图片出现很缓慢,而有的则很快,这个内容出现的过程就是加载。加载缓慢严重消耗用户的耐心,会让用户离开页面。


这里我们拿淘宝首页的network信息观察一些指标:


如图所示,我们可以看到下方显示网页加载一共有201次请求(requests)、3.3MB资源量(resources),DOM完成的加载时间92ms(DOMContentLoaded)、总资源加载时间479ms(Load),这对一个电商网站来说已经很好了。


瀑布图


再来看瀑布图,nextwork中加载资源的列表右侧的Waterfall一栏显示的就是瀑布图,它可以非常直观的将网站的资源加载用自上而下的方式表达出来。我们可以从横向和纵向两个方向来解读它。


横向来看是具体资源的加载,如下图所示:


当鼠标悬浮在具体的色块上,我们可以看到具体的指标详情。如下图所示,资源下载之前经历了0.46ms的排队(Queueing),DNS查找24.79ms(DNS Lookup)、建立TCP连接(Initial connection)、还有https的网站SSL证书验证的时间37.04ms(SSL),最后发送请求再到资源返回也要经历110.71ms(TTFB)。

下面我们再来纵向看瀑布图,主要看2点:



  1. 资源之间的联系:如果下载发生了阻塞,很多资源的下载就会是串行处理的。如果是并行的,就可以加快下载过程。

  2. 关键时间节点:我们可以看到图中有红蓝两个颜色的两根线,蓝色的是DOM完成的加载时间,红色是页面中所有声明的资源加载完成的时间。

关键指标


那么这么多指标,到底哪些是最值得我们关注的呢?下面我来总结一下:




  1. 白屏时间(FP,First Paint):也叫首次绘制时间,对于应用页面,首次出现视觉上不同于跳转之前内容的时间点,或者说是页面发生第一次绘制的时间点,它的标准时间是 300ms。如果白屏时间过长,用户会认为我们的页面不可用,或者可用性差。如果超过一定时间(如 1s),用户注意力就会转移到其他页面。




  2. 首屏时间(FCP,First Contentful Paint):也叫首次有内容绘制时间,对于所有的网页应用,这是一个非常重要的指标。它是指从浏览器输入地址并回车后,到首屏内容渲染完毕的时间。这期间不需要滚动鼠标或者下拉页面,否则无效。也就是说,它是浏览器完成渲染DOM中第一部分内容(可能是文本、图像或其它任何元素)的时间点,此时用户应该在视觉上有直观的感受。




  3. 首次有意义绘制(FMP,First Meaningful Paint):指页面关键元素的渲染时间。这个概念并没有标准化定义,因为关键元素可以由开发者自行定义究竟什么是“有意义”的内容,只有开发者或者产品经理自己了解。




  4. 速度指数(Speed Index):指的是网页以多快的速度展示内容,标准时间是4s




  5. 总下载时间(Load):页面所有资源加载完成所需要的时间。一般可以统计 window.onload,得到同步加载的资源全部加载完的耗时。如果页面中存在较多异步渲染,那么可以将异步渲染全部完成的时间作为总下载时间。




  6. TTFB(Time To First Byte):是指网络请求被发起到从服务器接收到地一个字节的这段时间。其中包含了TCP连接时间、发送HTTP请求时间和获得相应消息第一个字节的时间。


    TTFB这个参数比较重要,因为它可以给用户最直观的感受,如果TTFB很慢,说明资源一直没有返回,增加白屏时间,如果TTFB很快,资源回来之后就可以进行快速的解析和渲染。


    那么影响TTFB的因素有哪些?



    1. 后台处理能力,服务器响应到底有多快

    2. 资源请求的网络状况,网络是否有延迟

首屏时间 vs 白屏时间


首屏时间 = 白屏时间 + 渲染时间。在加载性能指标方面,相比于白屏时间,首屏时间更重要。为什么?


从重要性角度看,打开页面后,第一眼看到的内容一般都非常关键,比如电商的头图、商品价格、购买按钮等。这些内容即便在最恶劣的网络环境下,我们也要确保用户能看得到。


从体验完整性角度看,进入页面后先是白屏,随着第一个字符加载,到首屏内容显示结束,我们才会认为加载完毕,用户可以使用了。白屏加载完成后,仅仅意味着页面内容开始加载,但我们还是没法完成诸如下单购买等实际操作,首屏时间结束后则可以。


DOMContentLoaded和Load事件的区别


其实从这两个事件的命名就能体会到,DOMContentLoaded 指的是文档中 DOM 加载内容加载完毕的时间,也就是说 HTML 结构已经是完整的了。但是我们知道,很多页面都包含图片、特殊字体、视频、音频等其他资源,由于这些资源由网络请求获取,需要额外的网络请求,因此DOM内容如加载完毕时,这些资源还没有请求或渲染完成。当页面上所有资源加载完成后,Load 事件才会被触发。


因此,在时间线上,Load 事件往往会落后于 DOMContentLoaded 事件

交互/响应


所谓交互,就是用户点击网站或 App 的某个功能,页面给出的回应,也就是浏览器的响应时间。比如我们点击了一个“点赞”按钮,立刻给出了点赞数加一的展示,这就是交互体验好,反之如果很长时间都没回应,这就是交互体验不好。


关于交互指标,有的公司用 FID 指标 (First Input Delay,首次输入延迟), 指标必须尽量小于 100ms,如果过长会给人页面卡顿的感觉。还有的公司使用 PSI(Perceptual Speed Index,视觉变化率),衡量标准是小于20%。


一般来说,主要包括以下几个指标:




  1. 交互动作的反馈时间:也叫用户可交互时间,就是用户可以与应用进行加护的时间,一般来讲,我们认为是 DOMReady 的时间,因为我们通常会在这时绑定事件操作。如果页面中设计交互的脚本没有下载完成,那么当然没有达到所谓的用户可交互时间。那么如何定义 DOMReady 时间呢?这里我推荐大家看司徒正美的文章《何谓DOMReady》。




  2. 刷新率(FPS,Frame Per Second):也叫帧率,标准的刷新率指标是60帧/s,它可以决定画面是否足够流畅。




  3. 异步请求的完成时间:所有的异步请求能在1s中内请求回来。




关于帧率,我们可以用chorme Devtools来查看,打开控制台,点击快捷键command/ctrl+shift+P,弹出下面的弹窗,输入frame,点击FPS一栏,就会在页面左上角看到图2所示的监控台,显示网页交互过程中每一帧的绘制频率。



不同帧率的体验



  • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;

  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;

  • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;

  • 帧率波动很大的动画,亦会使人感觉到卡顿


现在网上很多关于浏览器reflow的文章都说给要少用offsetTop, offsetLeft 等获取布局信息的。因为这些属性需要触发一次浏览器的的Layout。也就是说在一帧内(16ms)会多了一次layout。如果Layout的次数太多,就会导致掉帧。


视觉稳定性


视觉稳定性指标CLS(Cumulative Layout Shift),也就是布局偏移量,它是指页面从一帧切换到另外一帧时,视线中不稳定元素的偏移情况。


比如,你想要购买的商品正在参加抢购活动,而且时间快要到了。在你正要点击页面链接购买的时候,原来的位置插入了一条 9.9 元包邮的商品广告。结果会怎样?你点成了那个广告商品。如果等你再返回购买的时候,你心仪商品的抢购活动结束了,你是不是很气?所以,CLS也非常重要。


一个好的CLS分数是75%以上的用户小于0.1,如图所示:




布局偏移的具体内容


布局偏移是由 Layout Instability API 定义的。这个API会在任意时间上报 layout-shift 的条目,当一个可见元素在两帧之间,改变了它的起始位置(默认的 writing mode 下指的是top和left属性)。这些元素被当成不稳定元素。


需要注意的是,布局偏移只发生在已经存在的元素改变起始位置的时候。如果一个新的元素被添加到dom上,或者已存在的元素改变它的尺寸,除非改变了其他元素的起始位置,否则都不算布局偏移。


布局偏移主要包含以下几项:




  1. 布局偏移分数:布局偏移的分数是两个度量的乘积:影响分数(impact fraction)和距离分数(distance fraction)。如果是一个很大的元素移动了较小的距离,实际影响并不大,所以分数需要依赖两个度量。




  2. 影响分数:影响分数测试的是两帧之间,不稳定元素在视图上的影响范围。




  3. 距离分数:距离分数测试的是两帧之间,不稳定元素在视图上移动的距离(水平和纵向取最大值)。如果有多个不稳定元素,也是取其中最大的一个。




  4. 动画和过渡:动画和过渡,如果做得好,对用户而言是一个不错的更新内容的方式,这样不会给用户“惊喜”。突然出现的或者预料之外的内容,会给用户一个很差的体验。但如果是一个动画或者过渡,用户可以很清楚的知道发生了什么,在状态变化的时候可以很好的引导用户。


    CSS中的 transform 属性可以让你在使用动画的时候不会产生布局偏移。



    • transform:scale() 来替换 widthheight 属性

    • transform:translate() 来替换 top, left, bottom, right 属性




CLS是平时开发很少关注的点,页面视觉稳定性对很多web开发而言,可能没有加载性能那么关注度高,但对用户而言,这确实是很困扰的一点。平时开发中,尽可能的提醒自己,不管是产品交互图出来之后,或者是UI的视觉稿出来之后,如果出现了布局偏移的情况,都可以提出这方面的意见。开发过程中也尽可能的遵循上面提到的一些优化点,给用户一个稳定的视觉体验。


RAIL测量模型


RAIL模型是2015年google提出的一个可以量化的测量标准,通过RAIL模型可以指导性能优化的目标,让良好的用户体验成为可能。




  1. Response 响应:是指用户操作网页的时候浏览器给到用户的反馈时间,其中处理事件应在50ms以内完成。


    为什么是50ms?谷歌向向用户发起调研,将用户的反馈分成了几个组,经过研究得出用户能接受的反馈时间是100ms。


    那么为什么我们要设置在50ms以内,因为100ms是用户输入到反馈的时间,但是浏览器处理反馈也需要时间,所以留给开发者优化处理事件的时间在50ms以内。如下图所示:


  • Animation - 页面中动画特效的流畅度,达到每10ms产生一帧。


    根据研究得出,动画要达到60sps,即每秒60帧给人眼的感觉是流畅的,每一帧大概在16ms,去除浏览器绘制动画的6ms,开发者要保证每10ms产生一帧。


    在这16ms内浏览器要完成的工作有:



    • 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等

    • 样式计算(CSS Object Model):级联地生成每个节点的生效样式。

    • 布局(Layout):计算布局,执行渲染算法

    • 重绘(Paint):各层分别进行绘制(比如 3D 动画)

    • 合成(Composite):将位图发送给合成线程。




  • Idle空闲 - 浏览器有足够的空闲时间,与响应想呼应。尽可能最大化空闲时间,不能让事件处理时间太长,超过50ms。


    例如延迟加载可以用空闲时间去加载。但是如果需要前端做业务计算,就是不合理的。




  • Load - 网络加载时间,在5s内完成内容加载并可以交互。首先加载-解析-渲染的时间在5s,其次网络环境差的情况下,加载也会受到影响。

  • 总结

    至此,性能优化的指标我就介绍完了,现将关键指标总结如下:



    1. 性能优化的三个方向:加载、交互、视觉稳定性

    2. 加载的关键指标有:TTFB(请求等待时间)、FP(白屏时间)、FCP(首屏时间)、Speed Index(4s)

    3. 交互的关键指标:用户可交互时间、帧率(FPS)、异步请求完成时间

    4. 交互稳定性(CLS):布局偏移量中,布局偏移分数 = 影响分数 x 距离分数

    5. RAIL测量模型关注点:响应时间50ms、动画10ms/帧、浏览器空闲时间<50ms、网络加载时间5s

    链接:https://juejin.cn/post/6956583036133572639

    收起阅读 »

    前端的你还不会优化你的图片资源?来看这一篇就够了!

    优质的图片可以有效吸引用户,给用户良好的体验,所以随着互联网的发展,越来越多的产品开始使用图片来提升产品体验。相较于页面其他元素,图片的体积不容忽视。下图是截止 2019 年 6 月 HTTP Archive[1] 上统计的网站上各类资源加...
    继续阅读 »

    优质的图片可以有效吸引用户,给用户良好的体验,所以随着互联网的发展,越来越多的产品开始使用图片来提升产品体验。相较于页面其他元素,图片的体积不容忽视。下图是截止 2019 年 6 月 HTTP Archive[1] 上统计的网站上各类资源加载的体积:


    可以看到,图片占据了半壁江山。同样,在一篇 2018 年的文章中,也提到了图片在网站中体量的平均占比已经超过了 50%[2]。然而,随着平均加载图片总字节数的增加,图片的请求数却再减少,这也说明网站使用的图片质量和大小正在不断提高。


    所以,如果单纯从加载的字节数这个维度来看性能优化,那么很多时候,优化图片带来的流量收益要远高于优化 JavaScript 脚本和 CSS 样式文件。下面我们就来看看,如何优化图片资源。


    1. 优化请求数

    1.1. 雪碧图


    图片可以合并么?当然。最为常用的图片合并场景就是雪碧图(Sprite)[3]


    在网站上通常会有很多小的图标,不经优化的话,最直接的方式就是将这些小图标保存为一个个独立的图片文件,然后通过 CSS 将对应元素的背景图片设置为对应的图标图片。这么做的一个重要问题在于,页面加载时可能会同时请求非常多的小图标图片,这就会受到浏览器并发 HTTP 请求数的限制。我见过一个没有使用雪碧图的页面,首页加载时需要发送 20+ 请求来加载图标。将图标合并为一张大图可以实现「20+ → 1」的巨大缩减。


    雪碧图的核心原理在于设置不同的背景偏移量,大致包含两点:



    • 不同的图标元素都会将 background-url 设置为合并后的雪碧图的 uri;

    • 不同的图标通过设置对应的 background-position 来展示大图中对应的图标部分。


    你可以用 Photoshop 这类工具自己制作雪碧图。当然比较推荐的还是将雪碧图的生成集成到前端自动化构建工具中,例如在 webpack 中使用 webpack-spritesmith,或者在 gulp 中使用 gulp.spritesmith。它们两者都是基于于 spritesmith 这个库,你也可以自己将这个库集成到你喜欢的构建工具中。

    1.2. 懒加载


    我们知道,一般来说我们访问一个页面,浏览器加载的整个页面其实是要比可视区域大很多的,也是什么我们会提出“首屏”的概念。这就导致其实很多图片是不在首屏中的,如果我们都加载的话,相当于是加载了用户不一定会看到图片。而图片体积一般都不小,这显然是一种流量的浪费。这种场景在一些带图片的长列表或者配图的博客中经常会遇到。


    解决的核心思路就是图片懒加载 —— 尽量只加载用户正在浏览或者即将会浏览到的图片。实现上来说最简单的就是通过监听页面滚动,判断图片是否进入视野,从而真正去加载图片:

    function loadIfNeeded($img) {
    const bounding = $img..getBoundingClientRect();
    if (
    getComputedStyle($img).display !== 'none'
    && bounding.top <= window.innerHeight
    && bounding.bottom >= 0
    ) {
    $img.src = $img.dataset.src;
    $img.classList.remove('lazy');
    }
    }

    // 这里使用了 throttle,你可以实现自己的 throttle,也可以使用 lodash
    const lazy = throttle(function () {
    const $imgList = document.querySelectorAll('.lazy');
    if ($imgList.length === 0) {
    document.removeEventListener('scroll', lazy);
    window.removeEventListener('resize', lazy);
    window.removeEventListener('orientationchange', lazy);
    return;
    }
    $imgList.forEach(loadIfNeeded);
    }, 200);

    document.addEventListener('scroll', lazy);
    window.addEventListener('resize', lazy);
    window.addEventListener('orientationchange', lazy);

    对于页面上的元素只需要将原本的 src 值设置到 data-src 中即可,而 src 可以设置为一个统一的占位图。注意,由于页面滚动、缩放和横竖方向(移动端)都可能会改变可视区域,因此添加了三个监听。


    当然,这是最传统的方法,现代浏览器还提供了一个更先进的 Intersection Observer API[4] 来做这个事,它可以通过更高效的方式来监听元素是否进入视口。考虑兼容性问题,在生产环境中建议使用对应的 polyfill


    如果想使用懒加载,还可以借助一些已有的工具库,例如 aFarkas/lazysizesverlok/lazyloadtuupola/lazyload 等。


    在使用懒加载时也有一些注意点:



    • 首屏可以不需要懒加载,对首屏图片也使用懒加载会延迟图片的展示。

    • 设置合理的占位图,避免图片加载后的页面“抖动”。

    • 虽然目前基本所有用户都不会禁用 JavaScript,但还是建议做一些 JavaScript 不可用时的 backup。


    对于占位图这块可以再补充一点。为了更好的用户体验,我们可以使用一个基于原图生成的体积小、清晰度低的图片作为占位图。这样一来不会增加太大的体积,二来会有很好的用户体验。LQIP (Low Quality Image Placeholders)[5] 就是这种技术。目前也已经有了 LQIPSQIP(SVG-based LQIP) 的自动化工具可以直接使用。


    如果你想了解更多关于图片懒加载的内容,这里有一篇更详尽的图片懒加载指南[6]

    1.3. CSS 中的图片懒加载

    除了对于 <img> 元素的图片进行来加载,在 CSS 中使用的图片一样可以懒加载,最常见的场景就是 background-url 

    .login {
    background-url: url(/static/img/login.png);
    }

    对于上面这个样式规则,如果不应用到具体的元素,浏览器不会去下载该图片。所以你可以通过切换 className 的方式,放心得进行 CSS 中图片的懒加载。


    1.4. 内联 base64


    还有一种方式是将图片转为 base64 字符串,并将其内联到页面中返回,即将原 url 的值替换为 base64。这样,当浏览器解析到这个的图片 url 时,就不会去请求并下载图片,直接解析 base64 字符串即可。


    但是这种方式的一个缺点在于相同的图片,相比使用二进制,变成 base64 后体积会增大 33%。而全部内联进页面后,也意味着原本可能并行加载的图片信息,都会被放在页面请求中(像当于是串行了)。同时这种方式也不利于复用独立的文件缓存。所以,使用 base64 需要权衡,常用于首屏加载 CRP 或者骨架图上的一些小图标。


    2. 减小图片大小

    2.1. 使用合适的图片格式


    使用合适的图片格式不仅能帮助你减少不必要的请求流量,同时还可能提供更好的图片体验。


    图片格式是一个比较大的话题,选择合适的格式[7]有利于性能优化。这里我们简单总结一些。


    1) 使用 WebP:


    考虑在网站上使用 WebP 格式[8]。在有损与无损压缩上,它的表现都会优于传统(JPEG/PNG)格式。WebP 无损压缩比 PNG 的体积小 26%,webP 的有损压缩比同质量的 JPEG 格式体积小 25-34%。同时 WebP 也支持透明度。下面提供了一种兼容性较好的写法。

    <picture>
    <source type="image/webp" srcset="/static/img/perf.webp">
    <source type="image/jpeg" srcset="/static/img/perf.jpg">
    <img src="/static/img/perf.jpg">
    </picture>

    2) 使用 SVG 应对矢量图场景:


    在一些需要缩放与高保真的情况,或者用作图标的场景下,使用 SVG 这种矢量图非常不错。有时使用 SVG 格式会比相同的 PNG 或 JPEG 更小。


    3) 使用 video 替代 GIF:


    兼容性允许的情况下考虑,可以在想要动图效果时使用视频,通过静音(muted)的 video 来代替 GIF。相同的效果下,GIF 比视频(MPEG-4)大 5~20 倍Smashing Magazine 上有篇文章[9]详细介绍使用方式。


    4) 渐进式 JPEG:


    基线 JPEG (baseline JPEG) 会从上往下逐步呈现,类似下面这种:


    而另一种渐进式 JPEG (progressive JPEG)[10] 则会从模糊到逐渐清晰,使人的感受上会更加平滑。


    不过渐进式 JPEG 的解码速度会慢于基线 JPEG,所以还是需要综合考虑 CPU、网络等情况,在实际的用户体验之上做权衡。


    2.2. 图片质量的权衡


    图片的压缩一般可以分为有损压缩(lossy compression)和无损压缩(lossless compression)。顾名思义,有损压缩下,会损失一定的图片质量,无损压缩则能够在保证图片质量的前提下压缩数据大小。不过,无损压缩一般可以带来更可观的体积缩减。在使用有损压缩时,一般我们可以指定一个 0-100 的压缩质量。在大多数情况下,相较于 100 质量系数的压缩,80~85 的质量系数可以带来 30~40% 的大小缩减,同时对图片效果影响较小,即人眼不易分辨出质量效果的差异。



    处理图片压缩可以使用 imagemin 这样的工具,也可以进一步将它集成至 webpackGulpGrunt 这样的自动化工具中。


    2.3. 使用合适的大小和分辨率


    由于移动端的发展,屏幕尺寸更加多样化了。同一套设计在不同尺寸、像素比的屏幕上可能需要不同像素大小的图片来保证良好的展示效果;此外,响应式设计也会对不同屏幕上最佳的图片尺寸有不同的要求。


    以往我们可能会在 1280px 宽度的屏幕上和 640px 宽度的屏幕上都使用一张 400px 的图,但很可能在 640px 上我们只需要 200px 大小的图片。另一方面,对于如今盛行的“2 倍屏”、“3 倍屏”也需要使用不同像素大小的资源。


    好在 HTML5 在 <img> 元素上为我们提供了 srcsetsizes 属性,可以让浏览器根据屏幕信息选择需要展示的图片。

    <img srcset="small.jpg 480w, large.jpg 1080w" sizes="50w" src="large.jpg" >

    2.4. 删除冗余的图片信息


    你也许不知道,很多图片含有一些非“视觉化”的元信息(metadata),带上它们可会导致体积增大与安全风险[12]。元信息包括图片的 DPI、相机品牌、拍摄时的 GPS 等,可能导致 JPEG 图片大小增加 15%。同时,其中的一些隐私信息也可能会带来安全风险。


    所以如果不需要的情况下,可以使用像 imageOptim 这样的工具来移除隐私与非关键的元信息。


    2.5 SVG 压缩


    在 2.1. 中提到,合适的场景下可以使用 SVG。针对 SVG 我们也可以进行一些压缩。压缩包括了两个方面:


    首先,与图片不同,图片是二进制形式的文件,而 SVG 作为一种 XML 文本,同样是适合使用 gzip 压缩的。


    其次,SVG 本身的信息、数据是可以压缩的,例如用相比用 <path> 画一个椭圆,直接使用 <ellipse> 可以节省文本长度。关于信息的“压缩”还有更多可以优化的点[13]SVGGO 是一个可以集成到我们构建流中的 NodeJS 工具,它能帮助我们进行 SVG 的优化。当然你也可以使用它提供的 Web 服务


    3. 缓存

    与其他静态资源类似,我们仍然可以使用各类缓存策略来加速资源的加载。




    图片作为现代 Web 应用的重要部分,在资源占用上同样也不可忽视。可以发现,在上面提及的各类优化措施中,同时附带了相应的工具或类库。平时我们主要的精力会放在 CSS 与 JavaScript 的优化上,因此在图片优化上可能概念较为薄弱,自动化程度较低。如果你希望更好得去贯彻图片的相关优化,非常建议将自动化工具引入到构建流程中。


    除了上述的一些工具,这里再介绍两个非常好用的图片处理的自动化工具:SharpJimp


    好了,我们的图片优化之旅就暂时到这了,下面就是字体资源了。


    链接:https://juejin.cn/post/6962800616259190792



    收起阅读 »

    仅使用CSS就可以提高页面渲染速度的4个技巧

    用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。1. Content-visibili...
    继续阅读 »

    用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。

    1. Content-visibility


    一般来说,大多数Web应用都有复杂的UI元素,它的扩展范围超出了用户在浏览器视图中看到的内容。在这种情况下,我们可以使用内容可见性( content-visibility )来跳过屏幕外内容的渲染。如果你有大量的离屏内容,这将大大减少页面渲染时间。


    这个功能是最新增加的功能之一,也是对提高渲染性能影响最大的功能之一。虽然 content-visibility 接受几个值,但我们可以在元素上使用 content-visibility: auto; 来获得直接的性能提升。


    让我们考虑一下下面的页面,其中包含许多不同信息的卡片。虽然大约有12张卡适合屏幕,但列表中大约有375张卡。正如你所看到的,浏览器用了1037ms来渲染这个页面。


    下一步,您可以向所有卡添加 content-visibility 。

    在这个例子中,在页面中加入 content-visibility 后,渲染时间下降到150ms,这是6倍以上的性能提升。


    正如你所看到的,内容可见性是相当强大的,对提高页面渲染时间非常有用。根据我们目前所讨论的东西,你一定是把它当成了页面渲染的银弹。

    然而,有几个领域的内容可视性不佳。我想强调两点,供大家参考。



    • 此功能仍处于试验阶段。 截至目前,Firefox(PC和Android版本)、IE(我认为他们没有计划在IE中添加这个功能)和,Safari(Mac和iOS)不支持内容可见性。

    • 与滚动条行为有关的问题。 由于元素的初始渲染高度为0px,每当你向下滚动时,这些元素就会进入屏幕。实际内容会被渲染,元素的高度也会相应更新。这将使滚动条的行为以一种非预期的方式进行。

    为了解决滚动条的问题,你可以使用另一个叫做 contain-intrinsic-size 的 CSS 属性。它指定了一个元素的自然大小,因此,元素将以给定的高度而不是0px呈现。

    .element{
    content-visibility: auto;
    contain-intrinsic-size: 200px;
    }

    然而,在实验时,我注意到,即使使用 conta-intrinsic-size,如果我们有大量的元素, content-visibility 设置为 auto ,你仍然会有较小的滚动条问题。


    因此,我的建议是规划你的布局,将其分解成几个部分,然后在这些部分上使用内容可见性,以获得更好的滚动条行为。

    2. Will-change 属性

    浏览器上的动画并不是一件新鲜事。通常情况下,这些动画是和其他元素一起定期渲染的。不过,现在浏览器可以使用GPU来优化其中的一些动画操作。



    通过will-change CSS属性,我们可以表明元素将修改特定的属性,让浏览器事先进行必要的优化。



    下面发生的事情是,浏览器将为该元素创建一个单独的层。之后,它将该元素的渲染与其他优化一起委托给GPU。这将使动画更加流畅,因为GPU加速接管了动画的渲染。

    // In stylesheet
    .animating-element {
    will-change: opacity;
    }

    // In HTML
    <div class="animating-elememt">
    Animating Child elements
    </div>

    当在浏览器中渲染上述片段时,它将识别 will-change 属性并优化未来与不透明度相关的变化。



    根据Maximillian Laumeister所做的性能基准,可以看到他通过这个单行的改变获得了超过120FPS的渲染速度,而最初的渲染速度大概在50FPS。


    什么时候不是用will-change


    虽然 will-change 的目的是为了提高性能,但如果你滥用它,它也会降低Web应用的性能。




    • **使用 will-change 表示该元素在未来会发生变化。**因此,如果你试图将 will-change 和动画同时使用,它将不会给你带来优化。因此,建议在父元素上使用 will-change ,在子元素上使用动画。

    .my-class{
    will-change: opacity;
    }
    .child-class{
    transition: opacity 1s ease-in-out;
    }
    • 不要使用非动画元素。 当你在一个元素上使用 will-change 时,浏览器会尝试通过将元素移动到一个新的图层并将转换工作交给GPU来优化它。如果您没有任何要转换的内容,则会导致资源浪费。




    最后需要注意的是,建议在完成所有动画后,将元素的 will-change 删除。

    3.减少渲染阻止时间

    今天,许多Web应用必须满足多种形式的需求,包括PC、平板电脑和手机等。为了完成这种响应式的特性,我们必须根据媒体尺寸编写新的样式。当涉及页面渲染时,它无法启动渲染阶段,直到 CSS对象模型(CSSOM)已准备就绪。根据你的Web应用,你可能会有一个大的样式表来满足所有设备的形式因素。
    <link rel="stylesheet" href="styles.css">

    将其分解为多个样式表后:

    <!-- style.css contains only the minimal styles needed for the page rendering -->
    <link rel="stylesheet" href="styles.css" media="all" />
    <!-- Following stylesheets have only the styles necessary for the form factor -->
    <link rel="stylesheet" href="sm.css" media="(min-width: 20em)" />
    <link rel="stylesheet" href="md.css" media="(min-width: 64em)" />
    <link rel="stylesheet" href="lg.css" media="(min-width: 90em)" />
    <link rel="stylesheet" href="ex.css" media="(min-width: 120em)" />
    <link rel="stylesheet" href="print.css" media="print" />

    如您所见,根据样式因素分解样式表可以减少渲染阻止时间。

    4.避免@import包含多个样式表

    通过 @import,我们可以在另一个样式表中包含一个样式表。当我们在处理一个大型项目时,使用 @import 可以使代码更加简洁。



    关于 @import 的关键事实是,它是一个阻塞调用,因为它必须通过网络请求来获取文件,解析文件,并将其包含在样式表中。如果我们在样式表中嵌套了 @import,就会妨碍渲染性能。

    # style.css
    @import url("windows.css");
    # windows.css
    @import url("componenets.css");


    与使用 @import 相比,我们可以通过多个 link 来实现同样的功能,但性能要好得多,因为它允许我们并行加载样式表。

    总结

    除了我们在本文中讨论的4个方面,我们还有一些其他的方法可以使用CSS来提高网页的性能。CSS最近的一个特性: content-visibility,在未来的几年里看起来是如此的有前途,因为它给页面渲染带来了数倍的性能提升。



    最重要的是,我们不需要写一条JavaScript语句就能获得所有的性能。



    我相信你可以结合以上的一些功能,为终端用户构建性能更好的Web应用。希望这篇文章对你有用,如果你知道什么CSS技巧可以提高Web应用的性能,请在下面的评论中提及。谢谢大家。



    链接:https://juejin.cn/post/6911203296078692359

    收起阅读 »

    任意组合判断还在用Switch?位运算符了解一下~

    情景再现很多时候,当我们写程序都会有这样的情况,就是代码多选操作.例如下面的操作.比如有四个视图View(分别为A,B,C,D);当点击按钮a的时候,视图A,B背景色发生改变;当点击按钮b的时候,视图A,B,D背景色发生改变;当点击按钮c的时候,视图B,C,D...
    继续阅读 »

    情景再现

    很多时候,当我们写程序都会有这样的情况,就是代码多选操作.例如下面的操作.


    比如有四个视图View(分别为A,B,C,D);
    当点击按钮a的时候,视图A,B背景色发生改变;
    当点击按钮b的时候,视图A,B,D背景色发生改变;
    当点击按钮c的时候,视图B,C,D背景色发生改变;
    后续开发中可能有很多按钮和不同的组合形式.

    这时候你会怎么办?

    第一种方案: 所有的按钮就响应一个方法,里面使用if else等模块来区分不同的按钮事件.
    思考问题: 后期如果增加一种按钮.你就需要增加一个if else,代码增加的同时,if else逻辑层级太多也不利于阅读.

    第二种方案: if else性能太低?我们就使用Switch.配合着枚举值或者按钮的Tag值来做区别判断,枚举值可以定义成每一种组合形式都是一个枚举值.不同按钮相同的组合形式进入同样的模块.
    思考问题: 虽然Switch使用break关键词相对于普通的if else有很大的性能提高,但是后期如果增加一种情况,仍然需要添加代码块.还是会增加代码量.

    这时候我们总结一下上面倒是需要干什么,以及出现的问题.

    需求: 组合是具有不确定性的,但是组合中的基本元素是确定的(A,B,C,D中的任意组合).

    问题: 普通的方式不管if else或者Switch都可能会需要罗列出所有的组合形式,代码量很大,不符合代码规范.阅读起来也是相当的困难.

    难道就没有更加优雅的方式来解决这个问题吗?这当然是有的,那就是我们的今天猪脚 位运算符.使用位运算符可以很好的帮助我们解决这一问题.但是在此之前我们需要先了解什么叫做位移运算符.

    位运算符


    按位与和按位或举个例子来看下.

    按位与

    1001
    &
    0101
    =0001

    按位或

    1001
    &
    0101
    =1101

    按位异或

    1001
    ^
    0101
    =1100

    我们了解了位运算符,我们该如何解决最开始的那种问题呢?我们接着看~

    解决问题

    → Demo传送门,对照着看

    解决这种问题我们会用到 << 和& 以及 | 这三种位运算符.

    首先定义一个ColorView视图,继承于UIView,然后在 .h 头文件定义枚举,并且ColorView持有枚举的属性.代码如下所示.

    #import <UIKit/UIKit.h>

    typedef enum : NSUInteger {
    ColorViewStyleA = 1<<0,
    ColorViewStyleB = 1<<1,
    ColorViewStyleC = 1<<2,
    ColorViewStyleD = 1<<3,
    } ColorViewStyle;

    @interface ColorView : UIView

    @property(nonatomic,assign)ColorViewStyle style;
    @property(nonatomic,copy)NSString *title;

    @end

    然后在ViewController导入ColorView,并且声明一个属性needChangeColorStyle.用于判断需要做出修改的视图.

    #import "ColorView.h"
    @interface ViewController ()

    @property(nonatomic,assign)ColorViewStyle needChangeColorStyle;

    @end

    然后在ViewController创建ColorView和Button,这里由于时间原因,我就简写了.主要要给每种视图设置一个枚举值,作为识别码.

    //创建ColorView
    for (int i = 0; i < 4; i++) {

    ColorView *colorView = [[ColorView alloc] initWithFrame:CGRectMake(viewWidth * i + distance *(i+1), 100, viewWidth, 100)];

    ............

    switch (i) {
    case 0:
    colorView.style = ColorViewStyleA;
    break;
    case 1:
    colorView.style = ColorViewStyleB;
    break;
    case 2:
    colorView.style = ColorViewStyleC;
    break;
    case 3:
    colorView.style = ColorViewStyleD;
    break;
    }
    ............
    }

    //创建按钮
    for (int i = 0; i < 4; i++) {
    ............
    }

    然后在按钮的点击方法 buttonAction 里面重置needChangeColorStyle的值,这里就需要使用到按位或进行枚举值的组合了.如下所示.

    - (void)buttonAction:(UIButton *)sender {

    //对 needChangeColorStyle 进行赋值,其实这步操作应该在一开始做的,这里是Demo,所以这么做了.
    //赋值过程中使用了按位或运算符.整合可响应的View类型.
    NSInteger tagIndex = sender.tag - 10000;
    switch (tagIndex) {
    case 0:
    _needChangeColorStyle = ColorViewStyleA|ColorViewStyleB|ColorViewStyleD;
    break;
    case 1:
    _needChangeColorStyle = ColorViewStyleB|ColorViewStyleC|ColorViewStyleD;
    break;
    case 2:
    _needChangeColorStyle = ColorViewStyleA|ColorViewStyleB;
    break;
    case 3:
    _needChangeColorStyle = ColorViewStyleD;
    break;
    }
    [self colorViewsChangAction];
    }

    最后在colorViewsChangAction方法中进行视图的操作选择.使用到了按位与运算.只要视图的style和_needChangeColorStyle有相交部分,那么两者按位与出来的数值一定是大于等于1的.这样就可以做包含操作了.代码日下所示.

    - (void)colorViewsChangAction {

    //遍历ColorView视图数组
    for (ColorView *colorView in self.colorViews) {
    //使用了按位与,查看两者是否具有相交部分.
    if (_needChangeColorStyle & colorView.style) {
    NSLog(@"%@ 做出了响应",colorView.title);
    colorView.backgroundColor = [UIColor redColor];
    } else {
    colorView.backgroundColor = [UIColor orangeColor];
    }
    }
    }

    这样我们就完成了使用位运算做任何组合判断的操作了,后期我们加一种按钮或者组合形式,只需要在 buttonAction 添加三行代码即可.其他都不用了,而且代码结构读起来非常的舒服.

    当然了,在iOS原生框架也是有这样的操作的,例如对于贝塞尔曲线的指定角进行切边操作.枚举值也是带有位移运算的,枚举值如下所示.这时候可以仍然可以使用按位或组合任意形式的角.

    typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft = 1 << 0,
    UIRectCornerTopRight = 1 << 1,
    UIRectCornerBottomLeft = 1 << 2,
    UIRectCornerBottomRight = 1 << 3,
    UIRectCornerAllCorners = ~0UL
    };

    总结

    位运算符还有很多用途,这是最简单的用途而已,在安卓那边的话,如果枚举有性能问题,可以使用定义常量的形式来实现该目的,整体上是一致的,好了就说到这里,如果有任何问题,欢迎批评指导,谢谢.最后再把Demo发一遍.

    转自:https://www.jianshu.com/p/5ed73f85ac37

    收起阅读 »

    一种简单实用的 JS 动态加载方案

    背景 在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。 我们有一些具体的案例,例如: 产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功...
    继续阅读 »

    背景


    在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。


    我们有一些具体的案例,例如:


    产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功能其实只有管理员才能看到,而且最多一周才会使用一次,绝对属于低频操作。


    团队里的小伙伴为了实现这个功能,引入了 XLSX 这个库,JS bundle 体积因而增加了一倍,所有用户的体验都受到影响了。


    XLSX 用来做 Excel 相关的操作是不错的选择,但因为新增低频操作影响全部用户却不值得。


    除了导出 Excel 这种功能外,类似的场景还有使用 html2canvas 生成并下载海报,使用 fabric 动态生成图片等。


    针对这种情况,你觉得该如何优化呢?

    自动分包和动态加载


    机智如你很快就想到使用 JS 动态加载,如果熟悉 React,还知道可以使用 react-loadable 来解决。


    原理就是利用 React Code-Splitting,配合 Webpack 自动分包,动态加载。


    这种方案可以,React 也推荐这么做,但是对于引用独立的第三方库这样的场景,还有更简单的方案。

    更简单的方案


    这些第三方库往往都提供了 umd 格式的 min.js,我们动态加载这些 min.js 就可以了。比如 XLSX,引入其 min.js 文件之后,就可以通过 window.XLSX 来实现 Excel 相关的操作。


    此方案的优点有:



    • 与框架无关,不需要和 React 等框架或 Webpack 等工具绑定

    • 精细控制,React Code-Splitting 之类的方案只能到模块级别,想要在点击按钮后才动态加载较难实现

    具体实现

    我们重点需要实现一个 JS 动态加载器 AsyncLoader,代码如下:

    function initLoader() {
    // key 是对应 JS 执行后在 window 中添加的变量
    const jsUrls = {
    html2canvas: 'https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.7/dist/html2canvas.min.js',
    XLSX: 'https://cdn.jsdelivr.net/npm/xlsx@0.16.9/dist/xlsx.min.js',
    flvjs: 'https://cdn.jsdelivr.net/npm/flv.js@1.5.0/dist/flv.min.js',
    domtoimage: 'https://cdn.jsdelivr.net/npm/dom-to-image@2.6.0/src/dom-to-image.min.js',
    fabric: 'https://cdn.jsdelivr.net/npm/fabric@4.3.1/dist/fabric.min.js',
    };

    const loadScript = (src) => {
    return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.onload = resolve;
    script.onerror = reject;
    script.crossOrigin = 'anonymous';
    script.src = src;
    if (document.head.append) {
    document.head.append(script);
    } else {
    document.getElementsByTagName('head')[0].appendChild(script);
    }
    });
    };

    const loadByKey = (key) => {
    // 判断对应变量在 window 是否存在,如果存在说明已加载,直接返回,这样可以避免多次重复加载
    if (window[key]) {
    return Promise.resolve();
    } else {
    if (Array.isArray(jsUrls[key])) {
    return Promise.all(jsUrls[key].map(loadScript));
    }
    return loadScript(jsUrls[key]);
    }
    };

    // 定义这些方法只是为了方便使用,其实 loadByKey 就够了。
    const loadHtml2Canvas = () => {
    return loadByKey('html2canvas');
    };

    const loadXlsx = () => {
    return loadByKey('XLSX');
    };

    const loadFlvjs = () => {
    return loadByKey('flvjs');
    };

    window.AsyncLoader = {
    loadScript,
    loadByKey,
    loadHtml2Canvas,
    loadXlsx,
    loadFlvjs,
    };
    }

    initLoader();

    使用方式


    以 XLSX 为例,使用这种方式之后,我们不需要在顶部 import xlsx from 'xlsx',只有当用户点击 导出Excel 按钮的时候,才从 CDN 动态加载 xlsx.min.js,加载成功后使用 window.XLSX 即可,代码如下:

    await window.AsyncLoader.loadXlsx().then(() => {
    const XLSX = window.XLSX;
    if (resp.data.signList && resp.data.signList.length > 0) {
    const new_workbook = XLSX.utils.book_new();

    resp.data.signList.map((item) => {
    const header = ['班级/学校/单位', '姓名', '帐号', '签到时间'];
    const { signRecords } = item;
    signRecords.unshift(header);

    const worksheet = XLSX.utils.aoa_to_sheet(signRecords);
    XLSX.utils.book_append_sheet(new_workbook, worksheet, item.signName);
    });

    XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
    } else {
    const new_workbook = XLSX.utils.book_new();
    const header = [['班级/学校/单位', '姓名', '帐号']];
    const worksheet = XLSX.utils.aoa_to_sheet(header);
    XLSX.utils.book_append_sheet(new_workbook, worksheet, '');
    XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
    }
    });

    另一个动态加载 domtoimage 的示例

    window.CommonJsLoader.loadByKey('domtoimage').then(() => {
    const scale = 2;
    window.domtoimage
    .toPng(poster, {
    height: poster.offsetHeight * scale,
    width: poster.offsetWidth * scale,
    style: {
    zoom: 1,
    transform: `scale(${scale})`,
    transformOrigin: 'top left',
    width: `${poster.offsetWidth}px`,
    height: `${poster.offsetHeight}px`,
    },
    })
    .then((dataUrl) => {
    copyImage(dataUrl, liveData?.planName);
    message.success(`${navigator.clipboard ? '复制' : '下载'}成功`);
    });
    });

    AsyncLoader 方案使用方便、理解简单,而且可以很好地利用 CDN 缓存,多个项目可以共用同样的 URL,进一步提高加载速度。而且这种方式使用的是原生 JS,在任何框架中都可以使用。


    注意,如果你用 TypeScript 开发,这种方案或许会丢失一些智能提示,如果引入了对应的 @types/xxx 应该没影响。如果你特别在意开发时的智能提示,也可以在开发的过程中 import 对应的包,开发完成后才换成 AsyncLoader 方案。

    原文:https://juejin.cn/post/6953193301289893901
    收起阅读 »

    Android基于微信 xlog 开源 日志框架

    前言之前写过一个 日志框架LogHelper ,是基于 Logger 开源库封装的,当时的因为项目本身的日志不是很多,完全可以使用,最近和其他公司合作,在一个新的项目上反馈,说在 大量log 的情况下会影响到手机主体功能的使用。从而让我对之前的日志行为做了一个...
    继续阅读 »

    前言

    之前写过一个 日志框架LogHelper ,是基于 Logger 开源库封装的,当时的因为项目本身的日志不是很多,完全可以使用,最近和其他公司合作,在一个新的项目上反馈,说在 大量log 的情况下会影响到手机主体功能的使用。从而让我对之前的日志行为做了一个深刻的反省随后在开发群中咨询了其他开发的小伙伴,如果追求性能,可以研究一下 微信的 xlog ,也是本篇博客的重点

    xlog 是什么

    xlog 是什么 这个问题 我这也是在【腾讯Bugly干货分享】微信mars 的高性能日志模块 xlog得到了答案
    简单来说 ,就是腾讯团队分享的基于 c/c++ 高可靠性高性能的运行期日志组件

    官网的 sample

    知道了他是什么,就要只要他是怎么用的,打开github 找到官网Tencent/mars
    使用非常简单

    下载库

    dependencies {
    compile 'com.tencent.mars:mars-xlog:1.2.3'
    }


    使用

    System.loadLibrary("c++_shared");
    System.loadLibrary("marsxlog");

    final String SDCARD = Environment.getExternalStorageDirectory().getAbsolutePath();
    final String logPath = SDCARD + "/marssample/log";

    // this is necessary, or may crash for SIGBUS
    final String cachePath = this.getFilesDir() + "/xlog"

    //init xlog
    if (BuildConfig.DEBUG) {
    Xlog.appenderOpen(Xlog.LEVEL_DEBUG, Xlog.AppenderModeAsync, cachePath, logPath, "MarsSample", 0, "");
    Xlog.setConsoleLogOpen(true);

    } else {
    Xlog.appenderOpen(Xlog.LEVEL_INFO, Xlog.AppenderModeAsync, cachePath, logPath, "MarsSample", 0, "");
    Xlog.setConsoleLogOpen(false);
    }

    Log.setLogImp(new Xlog());


    OK 实现了他的功能

    不要高兴的太早,后续的问题都头大

    分析各个方法的作用

    知道了最简单的用法,就想看看他支持哪些功能

    按照官网的demo 首先分析一下appenderOpen

    appenderOpen(int level, int mode, String cacheDir, String logDir, String nameprefix, int cacheDays, String pubkey)

    level

    日志级别 没啥好说的 XLog 中已经写得很清楚了

    public static final int LEVEL_ALL = 0;
    public static final int LEVEL_VERBOSE = 0;
    public static final int LEVEL_DEBUG = 1;
    public static final int LEVEL_INFO = 2;
    public static final int LEVEL_WARNING = 3;
    public static final int LEVEL_ERROR = 4;
    public static final int LEVEL_FATAL = 5;
    public static final int LEVEL_NONE = 6;


    值得注意的地方 debug 版本下建议把控制台日志打开,日志级别设为 Verbose 或者 Debug, release 版本建议把控制台日志关闭,日志级别使用 Info.

    public static native void setLogLevel(int logLevel);

    这个在官网的 接入指南

    这里也可以使用

    方法设置

    mode

    写入的模式

    • public static final int AppednerModeAsync = 0;

    异步写入

    • public static final int AppednerModeSync = 1;

    同步写入

    同步写入,可以理解为实时的日志,异步则不是

    Release版本一定要用 AppednerModeAsync, Debug 版本两个都可以,但是使用 AppednerModeSync 可能会有卡顿

    这里也可以使用

    public static native void setAppenderMode(int mode);

    方法设置

    cacheDir 设置缓存目录

    缓存目录,当 logDir 不可写时候会写进这个目录,可选项,不选用请给 "", 如若要给,建议给应用的 /data/data/packname/files/log 目录。

    会在目录下生成后缀为 .mmap3 的缓存文件,

    logDir 设置写入的文件目录

    真正的日志,后缀为 .xlog

    日志写入目录,请给单独的目录,除了日志文件不要把其他文件放入该目录,不然可能会被日志的自动清理功能清理掉。

    nameprefix 设置日志文件名的前缀

    日志文件名的前缀,例如该值为TEST,生成的文件名为:TEST_20170102.xlog。

    cacheDays

    一般情况下填0即可。非0表示会在 _cachedir 目录下存放几天的日志。

    这里的描述比较晦涩难懂,当我设置这个参数非0 的时候 会发现 原本设置在 logDir 目录下的文件 出现在了 cacheDir

    例如 正常应该是

    文件结构

    - cacheDir
    - log.mmap3
    - logDir
    - log_20200710.xlog
    - log_20200711.xlog


    变成这样

    - cacheDir
    - log.mmap3
    - log_20200710.xlog
    - log_20200711.xlog
    - logDir

    全部到了 cacheDir 下面

    cacheDays 的意思是 在多少天以后 从缓存目录移到日志目录

    pubkey 设置加密的 pubkey

    这里涉及到了日志的加密与解密,下面会专门介绍

    setMaxFileSize 设置文件大小

    在 Xlog 下有一个 native 方法

    	public static native void setMaxFileSize(long size);

    他表示 最大文件大小,这里需要说一下,原本的默认设置 是一天一个日志文件在 appender.h 描述的很清楚

    /*
    * By default, all logs will write to one file everyday. You can split logs to multi-file by changing max_file_size.
    *
    * @param _max_byte_size Max byte size of single log file, default is 0, meaning do not split.
    */
    void appender_set_max_file_size(uint64_t _max_byte_size);


    默认情况下,所有日志每天都写入一个文件。可以通过更改max_file_size将日志分割为多个文件。单个日志文件的最大字节大小,默认为0,表示不分割

    当超过设置的文件大小以后。文件会变成如下目录结构

    - cacheDir
    - log.mmap3
    - logDir
    - log_20200710.xlog
    - log_20200710_1.xlog
    - log_20200710_2.xlog


    在 appender.cc 对应的有如下逻辑,

    static long __get_next_fileindex(const std::string& _fileprefix, const std::string& _fileext) {
    ...
    return (filesize > sg_max_file_size) ? index + 1 : index;

    setConsoleLogOpen 设置是否在控制台答应日志

    ···java public static native void setConsoleLogOpen(boolean isOpen); ···

    设置是否在控制台答应日志

    setErrLogOpen

    这个方法是没用的,一开始以为哪里继承的有问题,在查看源码的时候发现 他是一个空方法,没有应用

    使用的话会导致程序异常,在自己编译的so 中我就把它给去掉了

    setMaxAliveTime 设置单个文件最大保留时间

    public static native void setMaxAliveTime(long duration);

    置单个文件最大保留时间 单位是秒,这个方法有3个需要注意的地方,

    • 必须在 appenderOpen 方法之前才有效
    • 最小的时间是 一天
    • 默认的时间是10天

    在 appender.cc 中可以看到

    static const long kMaxLogAliveTime = 10 * 24 * 60 * 60;    // 10 days in second
    static const long kMinLogAliveTime = 24 * 60 * 60; // 1 days in second
    static long sg_max_alive_time = kMaxLogAliveTime;
    ....
    void appender_set_max_alive_duration(long _max_time) {
    if (_max_time >= kMinLogAliveTime) {
    sg_max_alive_time = _max_time;
    }
    }

    默认的时间是10天

    appenderClose

    在 文档中介绍说是在 程序退出时关闭日志 调用appenderClose的方法

    然而在实际情况中 Application 类的 onTerminate() 只有在模拟器中才会生效,在真机中无效的,

    如果在程序退出的时候没有触发 appenderClose 那么在下一次启动的时候,xlog 也会把日志写入到文件中

    所以如何触发呢?

    建议尽可能的去触发他 例如用户双击back 退出的情况下 你肯定是知道的 如果放在后台被杀死了,这个时候也真的没办法刷新,也没关系,上面也说了,再次启动的时候会刷新到日志中,

    appenderFlush

    当日志写入模式为异步时,调用该接口会把内存中的日志写入到文件。

    isSync : true 为同步 flush,flush 结束后才会返回。 isSync : false 为异步 flush,不等待 flush 结束就返回。

    日志文件的加密

    这一块单独拿出来说明,是因为之前使用上遇到了坑

    首先是这个 入参 PUB_KEY,一脸懵,是个啥,

    在 mars/blob/master/mars/log/crypt/gen_key.py 这个就是能够获取到 PUB_KEY 的方法

    运行如下

    $ python gen_key.py
    WARNING: Executing a script that is loading libcrypto in an unsafe way. This will fail in a future version of macOS. Set the LIBRESSL_REDIRECT_STUB_ABORT=1 in the environment to force this into an error.
    save private key
    471e607b1bb3760205f74a5e53d2764f795601e241ebc780c849e7fde1b4ce40

    appender_open's parameter:
    300330b09d9e771d6163bc53a4e23b188ac9b2f5c7150366835bce3a12b0c8d9c5ecb0b15274f12b2dffae7f4b11c3b3d340e0521e8690578f51813c93190e1e

    上面的 private key 自己保存好

    appender_open's parameter: 就是需要的 PUB_KEY

    日志文件的解密

    上面已经知道如何加密了,现在了解一下如何解密

    下载pyelliptic1

    Xlog 加密使用指引中能够看到

    需要下载 pyelliptic1.5.7 然后编译 否则下面的命令会失败

    直接解密脚本

    xlog 很贴心的给我们提供了两个脚本

    使用 decode_mars_nocrypt_log_file.py 解压没有加密的

    python decode_mars_nocrypt_log_file [path]

    使用 decode_mars_crypt_log_file.py 加密的文件

    在使用之前需要将 脚本中的

    PRIV_KEY = "145aa7717bf9745b91e9569b80bbf1eedaa6cc6cd0e26317d810e35710f44cf8"
    PUB_KEY = "572d1e2710ae5fbca54c76a382fdd44050b3a675cb2bf39feebe85ef63d947aff0fa4943f1112e8b6af34bebebbaefa1a0aae055d9259b89a1858f7cc9af9df1"

    改成上面自己获取到的 key 否则是解压不出来的

    python decode_mars_crypt_log_file.py ~/Desktop/log/log_20200710.xlog

    直接生成一个

    - cacheDir
    - log.mmap3
    - logDir
    - log_20200710.xlog
    - log_20200710.xlog.log

    也可以自定义名字

    python decode_mars_crypt_log_file.py ~/Desktop/log/log_20200710.xlog ~/Desktop/log/1.log
    - cacheDir
    - log.mmap3
    - logDir
    - log_20200710.xlog
    - 1.log

    修改日志的格式

    打开我们解压好的日志查看

    ^^^^^^^^^^Oct 14 2019^^^20:27:59^^^^^^^^^^[17223,17223][2020-07-24 +0800 09:49:19]
    get mmap time: 3
    MARS_URL:
    MARS_PATH: master
    MARS_REVISION: 85b19f92
    MARS_BUILD_TIME: 2019-10-14 20:27:57
    MARS_BUILD_JOB:
    log appender mode:0, use mmap:1
    cache dir space info, capacity:57926635520 free:52452691968 available:52452691968
    log dir space info, capacity:57926635520 free:52452691968 available:52452691968
    [I][2020-07-24 +8.0 09:49:21.179][17223, 17223][TAG][, , 0][======================> 1
    [I][2020-07-24 +8.0 09:49:21.180][17223, 17223][TAG][, , 0][======================> 2
    [I][2020-07-24 +8.0 09:49:21.180][17223, 17223][TAG][, , 0][======================> 3
    [I][2020-07-24 +8.0 09:49:21.180][17223, 17223][TAG][, , 0][======================> 4
    [I][2020-07-24 +8.0 09:49:21.181][17223, 17223][TAG][, , 0][======================> 5
    [I][2020-07-24 +8.0 09:49:21.181][17223, 17223][TAG][, , 0][======================> 6
    [I][2020-07-24 +8.0 09:49:21.182][17223, 17223][TAG][, , 0][======================> 7
    [I][2020-07-24 +8.0 09:49:21.182][17223, 17223][TAG][, , 0][======================> 8
    [I][2020-07-24 +8.0 09:49:21.182][17223, 17223][TAG][, , 0][======================> 9
    [I][2020-07-24 +8.0 09:49:21.183][17223, 17223][TAG][, , 0][======================> 10
    [I][2020-07-24 +8.0 09:49:21.183][17223, 17223][TAG][, , 0][======================> 11
    [I][2020-07-24 +8.0 09:49:21.183][17223, 17223][TAG][, , 0][======================> 12
    [I][2020-07-24 +8.0 09:49:21.184][17223, 17223][TAG][, , 0][======================> 13
    [I][2020-07-24 +8.0 09:49:21.184][17223, 17223][TAG][, , 0][======================> 14
    [I][2020-07-24 +8.0 09:49:21.185][17223, 17223][TAG][, , 0][======================> 15
    [I][2020-07-24 +8.0 09:49:21.185][17223, 17223][TAG][, , 0][======================> 16
    [I][2020-07-24 +8.0 09:49:21.185][17223, 17223][TAG][, , 0][======================> 17


    我擦泪 除了我们需要的信息以外,还有这么多杂七杂八的信息,如何去掉,并且自己定义一下格式

    这里就需要自己去编译 so 了,好在 xlog 已经给我们提供了很好的编译代码

    对应的文档 本地编译

    对于编译这块按照文档来就好了 需要注意的是

    • 一定要用 ndk-r20 不要用最新版本的 21
    • 一定用 Python2.7 mac 自带 不用要 Python3

    去掉头文件

    首先我们去到这个头文件,对于一个日志框架来着,这个没啥用

    ^^^^^^^^^^Oct 14 2019^^^20:27:59^^^^^^^^^^[17223,17223][2020-07-24 +0800 09:49:19]
    get mmap time: 3
    MARS_URL:
    MARS_PATH: master
    MARS_REVISION: 85b19f92
    MARS_BUILD_TIME: 2019-10-14 20:27:57
    MARS_BUILD_JOB:
    log appender mode:0, use mmap:1
    cache dir space info, capacity:57926635520 free:52452691968 available:52452691968
    log dir space info, capacity:57926635520 free:52452691968 available:52452691968

    在本机下载好的 mars 下,找到 appender.cc 将头文件去掉

    修改日志格式

    默认的格式很长

    [I][2020-07-24 +8.0 09:49:21.179][17223, 17223][TAG][, , 0][======================> 1

    [日志级别][时间][pid,tid][tag][filename,strFuncName,line][日志内容

    是一个这样结构

    比较乱,我们想要的日志 就时间,级别,日志内容 就行了

    找到 formater.cc

    将原本的

    int ret = snprintf((char*)_log.PosPtr(), 1024, "[%s][%s][%" PRIdMAX ", %" PRIdMAX "%s][%s][%s, %s, %d][",  // **CPPLINT SKIP**
    _logbody ? levelStrings[_info->level] : levelStrings[kLevelFatal], temp_time,
    _info->pid, _info->tid, _info->tid == _info->maintid ? "*" : "", _info->tag ? _info->tag : "",
    filename, strFuncName, _info->line);


    改成

    int ret = snprintf((char*)_log.PosPtr(), 1024,     "[%s][%s]",  // **CPPLINT SKIP**
    temp_time, _logbody ? levelStrings[_info->level] : levelStrings[kLevelFatal] );


    就行了

    然后从新编译,将so 翻入项目 在看一下现在的效果

    [2020-07-24 +8.0 11:47:42.597][I]======================>9

    ok 打完收工

    简单的封装一下

    基本上分析和实现了我们需要的功能,那么把这部分简单的封装一下

    放上核心的 Builder 源码可在下面自行查看

    package com.allens.xlog

    import android.content.Context
    import com.tencent.mars.xlog.Log
    import com.tencent.mars.xlog.Xlog

    class Builder(context: Context) {

    companion object {
    //日志的tag
    var tag = "log_tag"
    }

    //是否是debug 模式
    private var debug = true


    //是否打印控制台日志
    private var consoleLogOpen = true


    //是否每天一个日志文件
    private var oneFileEveryday = true

    //默认的位置
    private val defCachePath = context.getExternalFilesDir(null)?.path + "/mmap"

    // mmap 位置 默认缓存的位置
    private var cachePath = defCachePath

    //实际保存的log 位置
    private var logPath = context.getExternalFilesDir(null)?.path + "/logDir"

    //文件名称前缀 例如该值为TEST,生成的文件名为:TEST_20170102.xlog
    private var namePreFix = "log"

    //写入文件的模式
    private var model = LogModel.Async

    //最大文件大小
    //默认情况下,所有日志每天都写入一个文件。可以通过更改max_file_size将日志分割为多个文件。
    //单个日志文件的最大字节大小,默认为0,表示不分割
    // 最大 当文件不能超过 10M
    private var maxFileSize = 0L

    //日志级别
    //debug 版本下建议把控制台日志打开,日志级别设为 Verbose 或者 Debug, release 版本建议把控制台日志关闭,日志级别使用 Info.
    private var logLevel = LogLevel.LEVEL_INFO

    //通过 python gen_key.py 获取到的公钥
    private var pubKey = ""

    //单个文件最大保留时间 最小 1天 默认时间 10
    private var maxAliveTime = 10

    //缓存的天数 一般情况下填0即可。非0表示会在 _cachedir 目录下存放几天的日志。
    //原来缓存日期的意思是几天后从缓存目录移到日志目录
    private var cacheDays = 0

    fun setCachePath(cachePath: String): Builder {
    this.cachePath = cachePath
    return this
    }

    fun setLogPath(logPath: String): Builder {
    this.logPath = logPath
    return this
    }


    fun setNamePreFix(namePreFix: String): Builder {
    this.namePreFix = namePreFix
    return this
    }

    fun setModel(model: LogModel): Builder {
    this.model = model
    return this
    }

    fun setPubKey(key: String): Builder {
    this.pubKey = key
    return this
    }

    //原来缓存日期的意思是几天后从缓存目录移到日志目录 默认 0 即可
    //如果想让文件保留多少天 用 [setMaxAliveTime] 方法即可
    //大于 0 的时候 默认会放在缓存的位置上 [cachePath]
    fun setCacheDays(days: Int): Builder {
    if (days < 0) {
    this.cacheDays = 0
    } else {
    this.cacheDays = days
    }
    return this
    }

    fun setDebug(debug: Boolean): Builder {
    this.debug = debug
    return this
    }

    fun setLogLevel(level: LogLevel): Builder {
    this.logLevel = level
    return this
    }

    fun setConsoleLogOpen(consoleLogOpen: Boolean): Builder {
    this.consoleLogOpen = consoleLogOpen
    return this
    }


    fun setTag(logTag: String): Builder {
    tag = logTag
    return this
    }


    /**
    * [isOpen] true 设置每天一个日志文件
    * false 那么 [setMaxFileSize] 生效
    */
    fun setOneFileEveryday(isOpen: Boolean): Builder {
    this.oneFileEveryday = isOpen
    return this
    }

    fun setMaxFileSize(maxFileSize: Float): Builder {
    when {
    maxFileSize < 0 -> {
    this.maxFileSize = 0L
    }
    maxFileSize > 10 -> {
    this.maxFileSize = (10 * 1024 * 1024).toLong()
    }
    else -> {
    this.maxFileSize = (maxFileSize * 1024 * 1024).toLong()
    }
    }
    return this
    }

    /**
    * [day] 设置单个文件的过期时间 默认10天 在程序启动30S 以后会检查过期文件
    * 过期时间依据 当前系统时间 - 文件最后修改时间计算
    * 默认 单个文件保存 10
    */
    fun setMaxAliveTime(day: Int): Builder {
    when {
    day < 0 -> {
    this.maxAliveTime = 0
    }
    day > 10 -> {
    this.maxAliveTime = 10
    }
    else -> {
    this.maxAliveTime = day
    }
    }
    return this
    }

    fun init() {

    if (!debug) {
    //判断如果是release 就强制使用 异步
    model = LogModel.Async
    //日志级别使用 Info
    logLevel = LogLevel.LEVEL_INFO
    }

    if (cachePath.isEmpty()) {
    //cachePath这个参数必传,而且要data下的私有文件目录,例如 /data/data/packagename/files/xlog, mmap文件会放在这个目录,如果传空串,可能会发生 SIGBUS 的crash。
    cachePath = defCachePath
    }


    android.util.Log.i(tag, "Xlog=========================================>")
    android.util.Log.i(
    tag,
    "info" + "\n"
    + "level:" + logLevel.level + "\n"
    + "model:" + model.model + "\n"
    + "cachePath:" + cachePath + "\n"
    + "logPath:" + logPath + "\n"
    + "namePreFix:" + namePreFix + "\n"
    + "cacheDays:" + cacheDays + "\n"
    + "pubKey:" + pubKey + "\n"
    + "consoleLogOpen:" + consoleLogOpen + "\n"
    + "maxFileSize:" + maxFileSize + "\n"
    )

    android.util.Log.i(tag, "Xlog=========================================<")
    Xlog.setConsoleLogOpen(consoleLogOpen)
    //每天一个日志文件
    if (oneFileEveryday) {
    Xlog.setMaxFileSize(0)
    } else {
    Xlog.setMaxFileSize(maxFileSize)
    }

    Xlog.setMaxAliveTime((maxAliveTime * 24 * 60 * 60).toLong())

    Xlog.appenderOpen(
    logLevel.level,
    model.model,
    cachePath,
    logPath,
    namePreFix,
    cacheDays,
    pubKey
    )
    Log.setLogImp(Xlog())
    }


    }


    下载

    Step 1. Add the JitPack repository to your build file Add it in your root build.gradle at the end of repositories:

    allprojects {
    repositories {
    ...
    maven { url 'https://www.jitpack.io' }
    }
    }

    Step 2. Add the dependency

    	dependencies {
    implementation 'com.github.JiangHaiYang01:XLogHelper:Tag'
    }

    添加 abiFilter

    android {
    compileSdkVersion 30
    buildToolsVersion "30.0.1"

    defaultConfig {
    ...
    ndk {
    abiFilter "armeabi-v7a"
    }
    }

    ...
    }

    使用

    初始化,建议放在 Application 中

    XLogHelper.create(this)
    .setModel(LogModel.Async)
    .setTag("TAG")
    .setConsoleLogOpen(true)
    .setLogLevel(LogLevel.LEVEL_INFO)
    .setNamePreFix("log")
    .setPubKey("572d1e2710ae5fbca54c76a382fdd44050b3a675cb2bf39feebe85ef63d947aff0fa4943f1112e8b6af34bebebbaefa1a0aae055d9259b89a1858f7cc9af9df1")
    .setMaxFileSize(1f)
    .setOneFileEveryday(true)
    .setCacheDays(0)
    .setMaxAliveTime(2)
    .init()

    XLogHelper.i("======================> %s", i)
    XLogHelper.e("======================> %s", i)


    代码下载:

    收起阅读 »

    作为iOSer,你还不会适配暗黑模式吗 ---- 如何适配暗黑模式(Dark Mode)

    原理1、将同一个资源,创建出两种模式的样式。系统根据当前选择的样式,自动获取该样式的资源2、每次系统更新样式时,应用会调用当前所有存在的元素调用对应的一些重新方法,进行重绘视图,可以在对应的方法做相应的改动资源文件适配1、创建一个Assets文件(或在现有的A...
    继续阅读 »

    原理

    1、将同一个资源,创建出两种模式的样式。系统根据当前选择的样式,自动获取该样式的资源

    2、每次系统更新样式时,应用会调用当前所有存在的元素调用对应的一些重新方法,进行重绘视图,可以在对应的方法做相应的改动

    资源文件适配

    1、创建一个Assets文件(或在现有的Assets文件中)

    2、新建一个图片资源文件(或者颜色资源文件、或者其他资源文件)

    3、选中该资源文件, 打开 Xcode ->View ->Inspectors ->Show Attributes Inspectors (或者Option+Command+4)视图,将Apperances 选项 改为Any,Dark

    4、执行完第三步,资源文件将会有多个容器框,分别为 Any Apperance 和 Dark Apperance. Any Apperance 应用于默认情况(Unspecified)与高亮情况(Light), Dark Apperance 应用于暗黑模式(Dark)

    5、代码默认执行时,就可以正常通过名字使用了,系统会根据当前模式自动获取对应的资源文件

    注意

    同一工程内多个Assets文件在打包后,就会生成一个Assets.car 文件,所以要保证Assets内资源文件的名字不能相同
    如何在代码里进行适配颜色(UIColor)

    如何在代码里进行适配颜色(UIColor)

    + (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
    - (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);

    eg.

    [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull trait) {
    if (trait.userInterfaceStyle == UIUserInterfaceStyleDark) {
    return UIColorRGB(0x000000);
    } else {
    return UIColorRGB(0xFFFFFF);
    }
    }];

    系统调用更新方法,自定义重绘视图

    当用户更改外观时,系统会通知所有window与View需要更新样式,在此过程中iOS会触发以下方法, 完整的触发方法文档

    UIView

    traitCollectionDidChange(_:)
    layoutSubviews()
    draw(_:)
    updateConstraints()
    tintColorDidChange()

    UIViewController

    traitCollectionDidChange(_:)
    updateViewConstraints()
    viewWillLayoutSubviews()
    viewDidLayoutSubviews()

    UIPresentationController

    traitCollectionDidChange(_:)
    containerViewWillLayoutSubviews()
    containerViewDidLayoutSubviews()

    如何不进行系统切换样式的适配

    注意
    苹果官方强烈建议适配 暗黑模式(Dark Mode)此功能也是为了开发者能慢慢将应用适配暗黑模式

    所以想通过此功能不进行适配暗黑模式,预计将会被拒

    全局关闭暗黑模式

    1、在Info.plist 文件中,添加UIUserInterfaceStyle key 名字为 User Interface Style 值为String,

    2、将UIUserInterfaceStyle key 的值设置为 Light

    单个界面不遵循暗黑模式

    1、UIViewController与UIView 都新增一个属性 overrideUserInterfaceStyle
    2、将 overrideUserInterfaceStyle 设置为对应的模式,则强制限制该元素与其子元素以设置的模式进行展示,不跟随系统模式改变进行改变
       1、设置 ViewController 的该属性, 将会影响视图控制器的视图和子视图控制器采用该样式
       2、设置 View 的该属性, 将会影响视图及其所有子视图采用该样式
       3、设置 Window 的该属性, 将会影响窗口中的所有内容都采用样式,包括根视图控制器和在该窗口中显示内容的所有演示控制器(UIPresentationController)

    转自:https://www.jianshu.com/p/7925bd51d2d6

    收起阅读 »

    [Android]使用函数指针实现native层异步回调

    1. 前言 在上篇关于lambda表达式实现方式的文章中,有提到一个概念叫做MethodHandle,当时的解释是类似于C/C++的函数指针,但是文章发出后咨询友人的意见,发现很多人并不清楚函数指针是怎么用的,其实我本人也是只是知道这个概念,但是并没有实际使用...
    继续阅读 »

    1. 前言


    在上篇关于lambda表达式实现方式的文章中,有提到一个概念叫做MethodHandle,当时的解释是类似于C/C++的函数指针,但是文章发出后咨询友人的意见,发现很多人并不清楚函数指针是怎么用的,其实我本人也是只是知道这个概念,但是并没有实际使用过。仿佛冥冥中自有天意,前几天公司的项目正好用到了函数指针来做native层的事件回调,也让我理解了函数指针的妙用。但是关于C/C++我并不是特别熟练,于是将实现过程写了个DEMO,一是为了做个记录熟悉过程,二是以备后续使用。


    2. 概念


    如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。


    那么这个指针变量怎么定义呢?虽然同样是指向一个地址,但指向函数的指针变量同我们之前讲的指向变量的指针变量的定义方式是不同的。例如:


    int(*p)(int, int);


    这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即(p);其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int()(int,int)。


    所以函数指针的定义方式为:


    函数返回值类型 (* 指针变量名) (函数参数列表);


    “函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。


    我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。


    那么怎么判断一个指针变量是指向变量的指针变量还是指向函数的指针变量呢?首先看变量名前面有没有“”,如果有“”说明是指针变量;其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。


    3. 定义函数指针和枚举


    假设native层有个耗时操作需要异步调用,我们在异步调用结束后通过回调通知业务层完成事件,那么这个时候就可以使用函数指针作为回调方法。


    定义方式:



    1. 首先定义事件枚举:


    enum EventEnum {
    eeSleepWake,
    };



    1. 其次,定义一个函数指针:


    typedef void (*onSleepWake)(int code, void* sender);


    这个函数指针可以指向一个返回值为void 参数分别为 int 和void型指针的函数,其中void型指针表示调用方的指针



    1. 定义一个结构体,包含函数指针和调用方的指针


    struct EventData {
    void* eventPointer;
    void* sender;
    };



    1. 注册事件持有类,使其成为单例


    这个操作的部分代码:


    class EventManager {
    public:
    static EventManager& singleton()
    {
    static EventManager sl;
    return sl;
    }
    static EventManager& getInstance()
    {
    return singleton();
    }

    //注册事件
    void addEvent(EventEnum eventEnum, void* event, void* sender);

    EventData getEventData(EventEnum eventEnum);

    private:
    std::map<EventEnum, EventData> eventMap;
    EventManager(){};
    ~EventManager(){};
    };



    1. 实现事件注册函数


    void EventManager::addEvent(EventEnum eventEnum, void* event, void* sender) {
    if(event == nullptr || sender == nullptr) {
    return;
    }
    EventData eventData;
    eventData.eventPointer = event;
    eventData.sender = sender;

    eventMap.insert(std::pair<EventEnum, EventData>(eventEnum, eventData));
    }



    1. 编写函数指针对应函数的具体实现


    void eeSleepWakeCallback(int result, void* sender) {
    JniTester *tester = (JniTester *) sender;
    tester->onResultCallback(result);
    }



    1. 在入口类中注册事件及其对应的枚举和函数


    JniTester::JniTester() {
    EventManager::getInstance().addEvent(eeSleepWake, (void*)eeSleepWakeCallback, this);
    }



    1. 编写异步函数调用


    ···
    void JniTester::getThreadResult() {
    ThreadTest *test = new ThreadTest();
    test->sleepThread();
    }
    ···
    耗时函数的具体实现:


    void ThreadTest::sleepThread() {
    std::thread cal_task(&ThreadTest::makeSleep, this);
    cal_task.detach();
    }

    void ThreadTest::makeSleep() {
    sleep(2);
    }


    这一步我们是通过新建一个线程,并让其等待2S来模拟异步耗时操作


    4. 异步回调的实现



    1. 在java层编写java的回调方法


    private OnResultCallback callback;

    public void setOnResultCallback(OnResultCallback callback) {
    this.callback = callback;
    }

    public interface OnResultCallback {
    void onResult(int result);
    }



    1. 在java曾编写java层回调的触发:


        public void onResult(int result) {
    if (this.callback != null) {
    callback.onResult(result);
    }
    }



    1. native层异步动作完成的通知


    通过向单例的事件持有类获取对应的事件枚举,获取到其对应的函数指针,并调用该函数指针实现:


    void ThreadTest::makeSleep() {
    sleep(2);
    EventData eventData = EventManager::singleton().getEventData(eeSleepWake);
    onSleepWake wake = (onSleepWake)eventData.eventPointer;
    if(wake) {
    wake(12345, eventData.sender);
    }
    }


    因为我们在第三章节第7步注册的函数指针是eeSleepWakeCallback, 因此,这里会调用到这个函数:


    void eeSleepWakeCallback(int result, void* sender) {
    JniTester *tester = (JniTester *) sender;
    tester->onResultCallback(result);
    }


    通过sender确定具体的对象,调用其onResultCallback函数



    1. onResultCallback函数的实现


    void JniTester::onResultCallback(int result) {
    JNIEnv *env = NULL;
    int status = f_jvm->GetEnv((void **) &env, JNI_VERSION_1_4);

    bool isInThread = false;
    if (status < 0) {
    isInThread = true;
    f_jvm->AttachCurrentThread(&env, NULL);
    }

    if (f_cls != NULL) {
    jmethodID id = env->GetMethodID(f_cls, "onResult", "(I)V");
    if (id != NULL) {
    env->CallVoidMethod(f_obj, id, result);
    }
    }

    if (isInThread) {
    f_jvm->DetachCurrentThread();
    }
    }


    这里因为缺少java环境,因此我们需要将该线程挂载到jvm上执行,并获取对应的JNIEnv ,通过jnienv获取java层的回调触发方法onResult并执行。


    5.效果


    编写测试代码:


            JniTester tester = new JniTester();
    Log.d("zyl", "startTime = " + System.currentTimeMillis());
    tester.setOnResultCallback(result -> {
    Log.d("zyl", "endTime = " + System.currentTimeMillis());
    Log.d("zyl", "result = " + result);
    });
    tester.requestData();


    执行结果:
    image.png


    和预期一致,完美。


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

    当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?

    前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了2万条数据,让前端用select组件展示到用户界面里. 我听完之后立马明白了他的困惑, 如果通过硬编码的方式去直接渲染这两万条数据到select中,肯定会卡死....
    继续阅读 »

    前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了2万条数据,让前端用select组件展示到用户界面里. 我听完之后立马明白了他的困惑, 如果通过硬编码的方式去直接渲染这两万条数据到select中,肯定会卡死. 后面他还说需要支持搜索, 也是前端来实现,我顿时产生了兴趣. 当时想到的方案大致如下:



    1. 采用懒加载+分页(前端维护懒加载的数据分发和分页)

    2. 使用虚拟滚动技术(目前react的antd4.0已支持虚拟滚动的select长列表)


    懒加载和分页方式一般用于做长列表优化, 类似于表格的分页功能, 具体思路就是用户每次只加载能看见的数据, 当滚动到底部时再去加载下一页的数据.


    虚拟滚动技术也可以用来优化长列表, 其核心思路就是每次只渲染可视区域的列表数,当滚动后动态的追加元素并通过顶部padding来撑起整个滚动内容,实现思路也非常简单.


    通过以上分析其实已经可以解决朋友的问题了,但是最为一名有追求的前端工程师, 笔者认真梳理了一下,并基于第一种方案抽象出一个实际的问题:


    如何渲染大数据列表并支持搜索功能?


    笔者将通过模拟不同段位前端工程师的实现方案, 来探索一下该问题的价值. 希望能对大家有所启发, 学会真正的深入思考.

    正文

    笔者将通过不同经验程序员的技术视角来分析以上问题, 接下来开始我们的表演.

    在开始代码之前我们先做好基础准备, 笔者先用nodejs搭建一个数据服务器, 提供基本的数据请求,核心代码如下:

    app.use(async (ctx, next) => {
    if(ctx.url === '/api/getMock') {
    let list = []

    // 生成指定个数的随机字符串
    function genrateRandomWords(n) {
    let words = 'abcdefghijklmnopqrstuvwxyz你是好的嗯气短前端后端设计产品网但考虑到付款啦分手快乐的分类开发商的李开复封疆大吏师德师风吉林省附近',
    len = words.length,
    ret = ''
    for(let i=0; i< n; i++) {
    ret += words[Math.floor(Math.random() * len)]
    }
    return ret
    }

    // 生成10万条数据的list
    for(let i = 0; i< 100000; i++) {
    list.push({
    name: `xu_0${i}`,
    title: genrateRandomWords(12),
    text: `我是第${i}项目, 赶快🌀吧~~`,
    tid: `xx_${i}`
    })
    }

    ctx.body = {
    state: 200,
    data: list
    }
    }
    await next()
    })
    以上笔者是采用koa实现的基本的mock数据服务器, 这样我们就可以模拟真实的后端环境来开始我们的前端开发啦(当然也可以直接在前端手动生成10万条数据). 其中genrateRandomWords方法用来生成指定个数的字符串,这在mock数据技术中应用很多, 感兴趣的盆友可以学习了解一下. 接下来的前端代码笔者统一采用react来实现(vue同理).

    初级工程师的方案

    直接从后端请求数据, 渲染到页面的硬编码方案,思路如下:


    代码可能是这样的:

    1. 请求后端数据:
    fetch(`${SERVER_URL}/api/getMock`).then(res => res.json()).then(res => {
    if(res.state) {
    data = res.data
    setList(data)
    }
    })
    1. 渲染页面
    {
    list.map((item, i) => {
    return <div className={styles.item} key={item.tid}>
    <div className={styles.tit}>{item.title} <span className={styles.label}>{item.name}</span></div>
    <div>{item.text}</div>
    </div>
    })
    }
    1. 搜索数据
    const handleSearch = (v) => {
    let searchData = data.filter((item, i) => {
    return item.title.indexOf(v) > -1
    })
    setList(searchData)
    }

    这样做本质上是可以实现基本的需求,但是有明显的缺点,那就是数据一次性渲染到页面中, 数据量庞大将导致页面性能极具降低, 造成页面卡顿.

    中级工程师的方案

    作为一名有一定经验的前端开发工程师,一定对页面性能有所了解, 所以一定会熟悉防抖函数节流函数, 并使用过诸如懒加载分页这样的方案, 接下来我们看看中级工程师的方案:


    通过这个过程的优化, 代码已经基本可用了, 下面来介绍具体实现方案:

    1. 懒加载+分页方案 懒加载的实现主要是通过监听窗口的滚动, 当某一个占位元素可见之后去加载下一个数据,原理如下:

    1. 这里我们通过监听windowscroll事件以及对poll元素使用getBoundingClientRect来获取poll元素相对于可视窗口的距离, 从而自己实现一个懒加载方案.


    在滚动的过程汇总我们还需要注意一个问题就是当用户往回滚动时, 实际上是不需要做任何处理的,所以我们需要加一个单向锁, 具体代码如下:

    function scrollAndLoading() {
    if(window.scrollY > prevY) { // 判断用户是否向下滚动
    prevY = window.scrollY
    if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
    // 请求下一页数据
    }
    }
    }

    useEffect(() => {
    // something code
    const getData = debounce(scrollAndLoading, 300)
    window.addEventListener('scroll', getData, false)
    return () => {
    window.removeEventListener('scroll', getData, false)
    }
    }, [])

    其中prevY存储的是窗口上一次滚动的距离, 只有在向下滚动并且滚动高度大于上一次时才更新其值.


    至于分页的逻辑, 原生javascript实现分页也很简单, 我们通过定义几个维度:



    • curPage当前的页数

    • pageSize 每一页展示的数量

    • data 传入的数据量


    有了这几个条件,我们的基本能分页功能就可以完成了. 前端分页的核心代码如下:

    let data = [];
    let curPage = 1;
    let pageSize = 16;
    let prevY = 0;

    // other code...

    function scrollAndLoading() {
    if(window.scrollY > prevY) { // 判断用户是否向下滚动
    prevY = window.scrollY
    if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
    curPage++
    setList(searchData.slice(0, pageSize * curPage))
    }
    }
    }
    1. 防抖函数实现 防抖函数因为比较简单, 这里直接上一个简单的防抖函数代码:
    function debounce(fn, time) {
    return function(args) {
    let that = this
    clearTimeout(fn.tid)
    fn.tid = setTimeout(() => {
    fn.call(that, args)
    }, time);
    }
    }
    1. 搜索实现 搜索功能代码如下:
    const handleSearch = (v) => {
    curPage = 1;
    prevY = 0;
    searchData = data.filter((item, i) => {
    // 采用正则来做匹配, 后期支持前端模糊搜索
    let reg = new RegExp(v, 'gi')
    return reg.test(item.title)
    })
    setList(searchData.slice(0, pageSize * curPage))
    }

    需要结合分页来实现, 所以这里为了不影响源数据, 我们采用临时数据searchData来存储. 效果如下:


    搜索后


    无论是搜索前还是搜索后, 都利用了懒加载, 所以再也不用担心数据量大带来的性能瓶颈了~

    高级工程师的方案

    作为一名久经战场的程序员, 我们应该考虑更优雅的实现方式,比如组件化, 算法优化, 多线程这类问题, 就比如我们问题中的大数据渲染, 我们也可以用虚拟长列表来更优雅简洁的来解决我们的需求. 至于虚拟长列表的实现笔者在开头已经点过,这里就不详细介绍了, 对于更大量的数据,比如100万(虽然实际开发中不会遇到这么无脑的场景),我们又该怎么处理呢?


    第一个点我们可以使用js缓冲器来分片处理100万条数据, 思路代码如下:

    function multistep(steps,args,callback){
    var tasks = steps.concat();

    setTimeout(function(){
    var task = tasks.shift();
    task.apply(null, args || []); //调用Apply参数必须是数组

    if(tasks.length > 0){
    setTimeout(arguments.callee, 25);
    }else{
    callback();
    }
    },25);
    }

    这样就能比较大量计算导致的js进程阻塞问题了.更多性能优化方案可以参考笔者之前的文章:



    我们还可以通过web worker来将需要在前端进行大量计算的逻辑移入进去, 保证js主进程的快速响应, 让web worker线程在后台计算, 计算完成后再通过web worker的通信机制来通知主进程, 比如模糊搜索等, 我们还可以对搜索算法进一步优化,比如二分法等,所以这些都是高级工程师该考虑的问题. 但是一定要分清场景, 寻找出性价比更高的方案.


    链接:https://juejin.cn/post/6844904184689475592


    收起阅读 »

    一行可以让项目启动快70%以上的代码

    前言 这两天闲来无事,想优化优化项目的启动时间,用了一个下午吧,将项目启动时间从48秒优化到14秒,大约70左右,效果还是有的,而且仅仅用了一行代码。 👇会讲一下找到这行代码的过程,如果没有耐心可以直接跳转到文章底部,直接看结论即可。 项目背景 项目就是简单的...
    继续阅读 »

    前言


    这两天闲来无事,想优化优化项目的启动时间,用了一个下午吧,将项目启动时间从48秒优化到14秒,大约70左右,效果还是有的,而且仅仅用了一行代码。


    👇会讲一下找到这行代码的过程,如果没有耐心可以直接跳转到文章底部,直接看结论即可。


    项目背景


    项目就是简单的Vue项目,不过公司内部给vue-cli包了一层,不过影响不大。


    别的也就没啥了,正常的H5网页,用的插件也不算多,为了控制项目体积。


    项目分析


    既然决定要优化了,首先要分析下项目,先用speed-measure-webpack-pluginwebpack-bundle-analyzer分析下,具体的配置这里就不多说了,很简单,网上一搜一大堆,这里直接看看结论。


    首先是项目运行时间:



    可以看到,基本上耗时大户就是eslint-loadervue-loader了,二者一个耗时40多秒,一个耗时30多秒,非常的占用资源。

    接下来再看看具体的包分析👇


    这一看就很一下子定位到问题到根源了,右侧的chunk-vendors不用看,只看左侧的chunk-page,这里面的页面数量太多了,相应的文件也很多,这也就直接导致了eslint-loadervue-loader耗时很久了,这么多文件,一个个检查耗时当然久了。


    右侧其实还可以继续优化,但感觉没必要,swiper其实并不大。


    那么现在就可以具体定位到问题了,由于项目是多SPA应用,致使.vue文件众多,在项目启动时进行eslint检查和加载耗时过长,导致项目启动时间较久。

    解决方案


    找到问题之后就得解决问题了,初步的解决方案有两个:



    1. 干掉eslint,在本地编译时不检查

    2. 缓存


    解决方案1必然是最简单的,但其实有点不合理,开着eslint就是为了规范代码格式,虽然在提交代码时也有对应的钩子来格式化代码,但在开发过程中进行提示可以更好的帮助我们形成合理的编码方式。


    所以现在剩下的方案就只有进行缓存操作了,接下来笔者就开始找相关插件来更好的进行缓存了。


    尝试解决


    首先是hard-source-webpack-plugin,这插件为模块提供中间缓存步骤,但项目得跑两次,第一次构建时间正常,第二次大概能省去90%左右的时间。


    这插件很多文章都有推荐,感觉很不错的样子,用起来也很简单,只需要👇:

    plugins: [
    new HardSourceWebpackPlugin()
    ]

    这就完事了。

    就这么简单?确实是这么简单,但也不简单,如果到此为止,笔者也不会折腾一下午了。

    就这么简单的一安装:

    npm i hard-source-webpack-plugin -D

    然后像👆一样简单的配置,然后重启项目,您猜怎么着?


    报错了!


    原因是什么呢?


    是因为speed-measure-webpack-plugin或者webpack-bundle-analyzer中的某一个,为什么呢?


    原因笔者其实并不太清楚,因为启动的时候报的错是这样的:

    Cannot find module 'webpack/lib/DependenciesBlockVariable'

    哦呦,这个错有点小意外,怎么会突然报webpack的错呢?


    笔者也是百思不得其解啊,去Google也没有人遇到这种问题。


    不得已,只能去hard-source-webpack-plugin的github上看issue,发现其实有人遇到这个问题的,他们的解决方案就是降低webpack的版本,可笔者这里没办法这么做,因为都集成在vue-cli里了,而且这个还是公司内部包了一层的,这就根本不可能降版本了。


    第一个转机


    那还能怎么办呢?


    实在没有办法了,笔者尝试搜索DependenciesBlockVariable的相关内容,这时事情发生了一丝微妙的变换,原来这个功能在webpack5中被移除了,难道是因为公司内部的vue-cli用的是webpack5.x版本?



    笔者当即在node_modules里面找到了插件,然后查看了package.json文件,结果失望的发现webpack的版本是4.2.6,这就令人绝望了,难道真的不可以么?


    既然打开了webpack的文档,那就好好看看吧。老实说这文档笔者已经看了N次了,真是每次看都有小惊喜,功能真是太多了。


    翻着翻着就看到了这个小功能👇:

    哦呦,还真有点小惊喜呦,这功能简直了,这不就是我想要的么?然后当机立断,往vue.config.js里一家,您猜怎么着?


    成了!


    虽然文档是webpack5.0的,但笔者发现4.x版本中也有这个功能,可能若一弱一些吧,多少能用啊。


    重启了几次项目后发现启动时间已经稳定了,效果真的还不错呦~


    直接给我干到了14秒,虽然有些不太稳定,但这已经是当前状态的最好解决方案了。

    所以最后的代码就是:

    chainWebpack: (config) => {
    config.cache(true)
    }

    chainWebpack的原因是项目中其实没有独立的webpack.config.js文件,所以只能放在vue.config.js文件中,使用chainWebpack来将配置插入到webpack中去。


    你以为事情到这里就结束了么?太简单了。


    第二个转机


    解决完问题后,当然要把speed-measure-webpack-plugin和webpack-bundle-analyzer这两个插件删掉了,然后整理整理代码,推上去完事。


    可笔者还是不死心,为啥hard-source-webpack-plugin不好使呢?不应该啊,为啥别人都能用,自己的项目却用不了呢?


    为了再次操作一手,也是为了更好的优化项目的启动时间,笔者再次安装了hard-source-webpack-plugin,并且对其进行了配置:

    chainWebpack: (config) => {
    config.plugin('cache').use(HardSourceWebpackPlugin)
    }

    这次再一跑,您猜怎么着?


    成了!


    为了避免再次启动失败了,笔者这次没有使用speed-measure-webpack-plugin和webpack-bundle-analyzer这两个插件,所以启动时间也没法具体估计了,但目测时间再10秒以内,强啊。


    所以说hard-source-webpack-plugin失败的原因可能就是那两个统计插件的原因了,得亏再试了一次,要不然就不明不白的GG了。


    结论


    这里的结论就很简单了,有两个版本。


    首先,如果项目能使用hard-source-webpack-plugin就很方便了,用就完事了,啥事也不需要干,所以这一行代码是👇:

    config.plugin('cache').use(HardSourceWebpackPlugin)

    大概真能快90%以上,官方并没有虚报时间。


    其次,如果用不了hard-source-webpack-plugin那就放弃吧,尝试webpack自带的cache功能也是不错的,虽然比不上hard-source-webpack-plugin,但多少也能提升70%左右的启动时间,所以这一行代码是👇:

    config.cache(true)

    并且不需要安装任何插件,一步到位。


    这两种方法其实都是可行了,论稳定和效果的话hard-source-webpack-plugin还是更胜一筹,但cache胜在不用装额外的webpack插件,具体用什么就自己决定吧。


    这里其实还是留了个坑,hard-source-webpack-plugin用不了的具体原因是什么呢?笔者只是猜测和speed-measure-webpack-plugin、webpack-bundle-analyzer这两个插件有关,但却不能肯定,如果有读者知道,欢迎在评论区留言或者私信笔者。


    看了这么久,辛苦了!


    链接:https://juejin.cn/post/6961203055257714702


    收起阅读 »

    SwiftUI-如何创建一个工程

    2019年度WWDC全球开发者大会,更新旗下用于手机、电脑、智能手表和电视机顶盒的软件操作系统。此外还发布了计算机编程语言框架SwiftUI。SwiftUI是基于开发语言Swift建立的框架——SwiftUI。全新的SwiftUI可以用于watchOS、tvO...
    继续阅读 »

    2019年度WWDC全球开发者大会,更新旗下用于手机、电脑、智能手表和电视机顶盒的软件操作系统。此外还发布了计算机编程语言框架SwiftUI。SwiftUI是基于开发语言Swift建立的框架——SwiftUI。全新的SwiftUI可以用于watchOS、tvOS、macOS等苹果旗下系统。

    在本文对于SwiftUI使用做一个简介。😊

    环境:

    1、macOS 15 Beta
    2、Xcode 11.0 Beta
    3、iOS 13.0 Beta

    接下来我们尝试体验一下SwiftUI功能,如何使用SwiftUI实现一个TableView呢?

    import SwiftUI

    struct Hero: Identifiable {
    let id: UUID = UUID()
    let name: String
    }

    struct ContentView : View {

    let heros = [
    Hero(name: "邱少云"),
    Hero(name: "黄继光"),
    Hero(name: "董存瑞"),
    Hero(name: "杨宝山"),
    Hero(name: "毛岸英")
    ]
    var body: some View {
    List(heros) {
    hero in
    Text(hero.name)
    }
    }
    }

    #if DEBUG
    struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
    ContentView()
    }
    }
    #endif


    以上是我们实现的第一个SwiftUI程序是不是很直观?
    接下来我们来了解一下程序的入口,同时解释一下他们之间如何联系的。

    1、创建一个SwiftUI 工程
    在Xcode-Beta 里创建工程和之前Xcode版本是一样的,我们选择 Single View App:


    给工程命名同时选择使用SwiftUI


    2、了解程序的入口

    让我们从项目中删除尽可能多的代码和文件,看到什么程度还可以让它跑起来。刚开始创建工程是这样的:


    我们将AppDelegate.swift和ContentView.swift删除并移进回收站。并在SceneDelegate类的顶部添加@UIApplicationMain,让这个类遵循UIApplicationDelegate,删除SceneDelegate中除func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)以外的方法,func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)是程序的主入口。


    现在如果选择iPhone XR进行运行,会显示黑屏。

    3、创建一个新的Swift File或者直接选择SwiftUI View


    我们将新创建的Swift文件命名为 AwesomeView.swift,内部代码如下,和我们最初删除的ContentView.swift内容一样:

    import SwiftUI

    struct AwesomeView : View {
    var body: some View {
    Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/)
    }
    }

    #if DEBUG
    struct AwesomeView_Previews : PreviewProvider {
    static var previews: some View {
    AwesomeView()
    }
    }
    #endif

    AwesomeView.swift中有一个实现View协议的AwesomeView结构体,根据View协议,我们实现了body属性,Swift5.1中,我们不需要添加return关键字,函数或者闭包最后一行将自动返回。
    这是我们写的第一个SwiftUI试图,接下来选择右上角,点击一个多条线按钮,选择Editor and Canvas


    接下来点击Resume 或者 Try again 查看试图状态。
    预览里将展示AwesomeView_Previews 结构体中闭包返回的所有试图预览。

    在PreviewProvider里我们可以看到这段注释

    Xcode statically discovers types that conform to `PreviewProvider` and
    generates previews in the canvas for each provider it discovers.

    通过Xcode静态发现符合PreviewProvider协议的类型,并在画布中为它发现的每个provider生成预览。所以我们可以随意命名xxx_Previews并遵循PreviewProvider协议,就可以在画布上预览我们的视图。
    我们可以编辑左边的代码,看右侧的画布是不是可以重载。😍

    4、将SwiftUI View 定义为程序启动图

    之前我们的跑起来的程序是黑屏,目前重新启动程序依然是黑屏。如何将我们定义的为根视图呢?其实我们之前删除代码是我们就注意到了在SceneDelegate.swift中有以下代码:

    import UIKit
    import SwiftUI

    @UIApplicationMain
    class SceneDelegate: UIResponder, UIWindowSceneDelegate, UIApplicationDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // 实例化一个手机屏幕大小window
    let window = UIWindow(frame: UIScreen.main.bounds)
    // 实例化一个UIHostingController作为rootViewController
    // UIHostingController保存SwiftUI视图,将AwesomeView作为根视图
    window.rootViewController = UIHostingController(rootView: AwesomeView())
    self.window = window
    window.makeKeyAndVisible()
    }

    }

    现在运行程序我们就可以在模拟器中看到我们写的AwesomeView了。。。。

    Xcode 是如何知道SceneDelegate.swift中的SceneDelegate作为程序启动根视图的类的呢?我们看一下工程中的info.plist


    我们尝试修改一下Delegate Class Name将(PRODUCT_MODULE_NAME).SceneDelegate改为$(PRODUCT_MODULE_NAME).martinlasek,此时我们运行将会出现之前的黑屏情况,因为程序找不到martinlasek,
    现在我们将SceneDelegate.swift中的SceneDelegate类重命名为martinlasek。然后我们再运行一次。在模拟器中我们再次看到了AwesomeView。
    也就是说info.plist中的Delegate Class Name定义了根视图的类。

    通过上面的一段内容你可以很轻松实现一个SwiftUI小程序。从现在开始你可以开启你的SwiftUI之旅了。

    链接:https://www.jianshu.com/p/b4509d3d9766

    收起阅读 »

    哇擦!他居然把 React 组件渲染到了命令行终端窗口里面

    也许你之前听说过前端组件代码可以运行在浏览器,运行在移动端 App 里面,甚至可以直接在各种设备当中,但你有没有见过: 前端组件直接跑在命令行窗口里面,让前端代码构建出终端窗口的 GUI 界面和交互逻辑? 今天, 给大家分享一个非常有意思的开源项目: ink。...
    继续阅读 »

    也许你之前听说过前端组件代码可以运行在浏览器,运行在移动端 App 里面,甚至可以直接在各种设备当中,但你有没有见过: 前端组件直接跑在命令行窗口里面,让前端代码构建出终端窗口的 GUI 界面和交互逻辑?


    今天, 给大家分享一个非常有意思的开源项目: ink。它的作用就是将 React 组件渲染在终端窗口中,呈现出最后的命令行界面。


    本文偏重实战,前面会带大家熟悉基本使用,然后会做一个基于实际场景的练手项目。


    上手初体验

    刚开始上手时,推荐使用官方的脚手架创建项目,省时省心。

    npx create-ink-app --typescript

    然后运行这样一段代码:

    import React, { useState, useEffect } from 'react'
    import { render, Text} from 'ink'

    const Counter = () => {
    const [count, setCount] = useState(0)
    useEffect(() => {
    const timer = setInterval(() => {
    setCount(count => ++count)
    }, 100)
    return () => {
    clearInterval(timer)
    }

    })

    return (
    <Text color="green">
    {count} tests passed
    </Text>
    )
    }

    render(<Counter />);

    会出现如下的界面:


    并且数字一直递增! demo 虽小,但足以说明问题:




    1. 首先,这些文本输出都不是直接 console 出来的,而是通过 React 组件渲染出来的。




    2. React 组件的状态管理以及hooks 逻辑放到命令行的 GUI 当中仍然是生效的。




    也就是说,前端的能力以及扩展到了命令行窗口当中了,这无疑是一项非常可怕的能力。著名的文档生成工具
    Gatsby,包管理工具yarn2都使用了这项能力来完成终端 GUI 的搭建。

    命令行工具项目实战


    可能大家刚刚了解到这个工具,知道它的用途,但对于具体如何使用还是比较陌生。接下来让我们以一个实际的例子来进行实战,快速熟悉。代码仓库已经上传到 git,大家可以这个地址下面 fork 代码: github.com/sanyuan0704…


    下面我们就来从头到尾开发这个项目。


    项目背景


    首先说一说项目的产生背景,在一个 TS 的业务项目当中,我们曾经碰到了一个问题:由于production模式下面,我们是采用先 tsc,拿到 js 产物代码,再用webpack打包这些产物。


    但构建的时候直接报错了,原因就是 tsc 无法将 ts(x) 以外的资源文件移动到产物目录,以至于 webpack 在对于产物进行打包的时候,发现有些资源文件根本找不到!比如以前有这样一张图片的路径是这样—— src/asset/1.png,但这些在产物目录dist却没还有,因此 webpack 在打包 dist 目录下的代码时,会发现这张图片不存在,于是报错了。


    解决思路


    那如何来解决呢?


    很显然,我们很难去扩展 tsc 的能力,现在最好的方式就是写个脚本手动将src下面的所有资源文件一一拷贝到dist目录,这样就能解决资源无法找到的问题。


    一、拷贝文件逻辑


    确定了解决思路之后,我们写下这样一段 ts 代码:

    import { join, parse } from "path";
    import { fdir } from 'fdir';
    import fse from 'fs-extra'
    const staticFiles = await new fdir()
    .withFullPaths()
    // 过滤掉 node_modules、ts、tsx
    .filter(
    (p) =>
    !p.includes('node_modules') &&
    !p.endsWith('.ts') &&
    !p.endsWith('.tsx')
    )
    // 搜索 src 目录
    .crawl(srcPath)
    .withPromise() as string[]

    await Promise.all(staticFiles.map(file => {
    const targetFilePath = file.replace(srcPath, distPath);
    // 创建目录并拷贝文件
    return fse.mkdirp(parse(targetFilePath).dir)
    .then(() => fse.copyFile(file, distPath))
    );
    }))

    代码使用了fdir这个库才搜索文件,非常好用的一个库,写法上也很优雅,推荐大家使用。


    我们执行这段逻辑,成功将资源文件转移到到了产物目录中。


    问题是解决掉了,但我们能不能封装一下这个逻辑,让它能够更方便地在其它项目当中复用,甚至直接提供给其他人复用呢?


    接着,我想到了命令行工具。


    二、命令行 GUI 搭建


    接着我们使用 ink,也就是用 React 组件的方式来搭建命令行 GUI,根组件代码如下:

    // index.tsx 引入代码省略
    interface AppProps {
    fileConsumer: FileCopyConsumer
    }

    const ACTIVE_TAB_NAME = {
    STATE: "执行状态",
    LOG: "执行日志"
    }

    const App: FC<AppProps> = ({ fileConsumer }) => {
    const [activeTab, setActiveTab] = useState<string>(ACTIVE_TAB_NAME.STATE);
    const handleTabChange = (name) => {
    setActiveTab(name)
    }
    const WELCOME_TEXT = dedent`
    欢迎来到 \`ink-copy\` 控制台!功能概览如下(按 **Tab** 切换):
    `

    return <>
    <FullScreen>
    <Box>
    <Markdown>{WELCOME_TEXT}</Markdown>
    </Box>
    <Tabs onChange={handleTabChange}>
    <Tab name={ACTIVE_TAB_NAME.STATE}>{ACTIVE_TAB_NAME.STATE}</Tab>
    <Tab name={ACTIVE_TAB_NAME.LOG}>{ACTIVE_TAB_NAME.LOG}</Tab>
    </Tabs>
    <Box>
    <Box display={ activeTab === ACTIVE_TAB_NAME.STATE ? 'flex': 'none'}>
    <State />
    </Box>
    <Box display={ activeTab === ACTIVE_TAB_NAME.LOG ? 'flex': 'none'}>
    <Log />
    </Box>
    </Box>
    </FullScreen>
    </>
    };

    export default App;

    可以看到,主要包含两大组件: StateLog,分别对应两个 Tab 栏。具体的代码大家去参考仓库即可,下面放出效果图:


    3. GUI 如何实时展示业务状态?


    现在问题就来了,文件操作的逻辑开发完了,GUI 界面也搭建好了。那么现在如何将两者结合起来呢,也就是 GUI 如何实时地展示文件操作的状态呢?


    对此,我们需要引入第三方,来进行这两个模块的通信。具体来讲,我们在文件操作的逻辑中维护一个 EventBus 对象,然后在 React 组件当中,通过 Context 的方式传入这个 EventBus。
    从而完成 UI 和文件操作模块的通信。


    现在我们开发一下这个 EventBus 对象,也就是下面的FileCopyConsumer:

    export interface EventData {
    kind: string;
    payload: any;
    }

    export class FileCopyConsumer {

    private callbacks: Function[];
    constructor() {
    this.callbacks = []
    }
    // 供 React 组件绑定回调
    onEvent(fn: Function) {
    this.callbacks.push(fn);
    }
    // 文件操作完成后调用
    onDone(event: EventData) {
    this.callbacks.forEach(callback => callback(event))
    }
    }

    接着在文件操作模块和 UI 模块当中,都需要做响应的适配,首先看看文件操作模块,我们做一下封装。

    export class FileOperator {
    fileConsumer: FileCopyConsumer;
    srcPath: string;
    targetPath: string;
    constructor(srcPath ?: string, targetPath ?: string) {
    // 初始化 EventBus 对象
    this.fileConsumer = new FileCopyConsumer();
    this.srcPath = srcPath ?? join(process.cwd(), 'src');
    this.targetPath = targetPath ?? join(process.cwd(), 'dist');
    }

    async copyFiles() {
    // 存储 log 信息
    const stats = [];
    // 在 src 中搜索文件
    const staticFiles = ...

    await Promise.all(staticFiles.map(file => {
    // ...
    // 存储 log
    .then(() => stats.push(`Copied file from [${file}] to [${targetFilePath}]`));
    }))
    // 调用 onDone
    this.fileConsumer.onDone({
    kind: "finish",
    payload: stats
    })
    }
    }

    然后在初始化 FileOperator之后,将 fileConsumer通过 React Context 传入到组件当中,这样组件就能访问到fileConsumer,进而可以进行回调函数的绑定,代码演示如下:

    // 组件当中拿到 fileConsumer & 绑定回调
    export const State: FC<{}> = () => {
    const context = useContext(Context);
    const [finish, setFinish] = useState(false);
    context?.fileConsumer.onEvent((data: EventData) => {
    // 下面的逻辑在文件拷贝完成后执行
    if (data.kind === 'finish') {
    setTimeout(() => {
    setFinish(true)
    }, 2000)
    }
    })

    return
    //(JSX代码)
    }

    这样,我们就成功地将 UI 和文件操作逻辑串联了起来。当然,篇幅所限,还有一些代码并没有展示出来,完整的代码都在 git 仓库当中。希望大家能 fork 下来好好体会一下整个项目的设计。


    总体来说,React 组件代码能够跑在命令行终端,确实是一件激动人心的事情,给前端释放了更多想象的空间。本文对于这个能力的使用也只是冰山一角,更多使用姿势等待你去解锁,赶紧去玩一玩吧!


    链接:https://juejin.cn/post/6952673382928220191

    收起阅读 »

    如何应用 SOLID 原则整理 React 代码之单一原则

    SOLID 原则的主要是作为关心自己工作的软件专业人员的指导方针,另外还为那些以经得起时间考验的设计精美的代码库为荣的人。 今天,我们将从一个糟糕的代码示例开始,应用 SOLID 的第一个原则,看看它如何帮助我们编写小巧、漂亮、干净的并明确责任的 React ...
    继续阅读 »

    SOLID 原则的主要是作为关心自己工作的软件专业人员的指导方针,另外还为那些以经得起时间考验的设计精美的代码库为荣的人。


    今天,我们将从一个糟糕的代码示例开始,应用 SOLID 的第一个原则,看看它如何帮助我们编写小巧、漂亮、干净的并明确责任的 React 组件,。

    什么是单一责任原则?

    单一责任原则告诉我们的是,每个类或组件应该有一个单一的存在目的。

    组件应该只做一件事,并且做得很好。

    让我们重构一段糟糕但正常工作的代码,并使用这个原则使其更加清晰和完善。

    让我们从一个糟糕的例子开始

    首先让我们看看一些违反这一原则的代码,添加注释是为了更好地理解:

    import React, {useEffect, useReducer, useState} from "react";

    const initialState = {
    isLoading: true
    };

    // 复杂的状态管理
    function reducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    export const SingleResponsibilityPrinciple = () => {

    const [users , setUsers] = useState([])
    const [filteredUsers , setFilteredUsers] = useState([])
    const [state, dispatch] = useReducer(reducer, initialState);

    const showDetails = (userId) => {
    const user = filteredUsers.find(user => user.id===userId);
    alert(user.contact)
    }

    // 远程数据获取
    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    // 数据处理
    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    // 复杂UI渲染
    return <>
    Users List

    Loading state: {state.isLoading? 'Loading': 'Success'}

    {users.map(user => {
    return
    showDetails(user.id)}>
    {user.name}

    {user.email}


    })}

    }

    这段代码的作用

    这是一个函数式组件,我们从远程数据源获取数据,再过滤数据,然后在 UI 中显示数据。我们还检测 API 调用的加载状态。

    为了更好地理解这个例子,我把它简化了。但是你几乎可以在任何地方的同一个组件中找到它们!这里发生了很多事情:

    1. 远程数据的获取

    2. 数据过滤

    3. 复杂的状态管理

    4. 复杂的 UI 功能

    因此,让我们探索如何改进代码的设计并使其紧凑。

    1. 移动数据处理逻辑

    不要将 HTTP 调用保留在组件中。这是经验之谈。您可以采用几种策略从组件中删除这些代码。

    您至少应该创建一个自定义 Hook 并将数据获取逻辑移动到那里。例如,我们可以创建一个名为 useGetRemoteData 的 Hook,如下所示:

    import {useEffect, useReducer, useState} from "react";

    const initialState = {
    isLoading: true
    };

    function reducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    export const useGetRemoteData = (url) => {

    const [users , setUsers] = useState([])
    const [state, dispatch] = useReducer(reducer, initialState);

    const [filteredUsers , setFilteredUsers] = useState([])


    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    return {filteredUsers , isLoading: state.isLoading}
    }

    现在我们的主要组件看起来像这样:

    import React from "react";
    import {useGetRemoteData} from "./useGetRemoteData";

    export const SingleResponsibilityPrinciple = () => {

    const {filteredUsers , isLoading} = useGetRemoteData()

    const showDetails = (userId) => {
    const user = filteredUsers.find(user => user.id===userId);
    alert(user.contact)
    }

    return <>
    Users List

    Loading state: {isLoading? 'Loading': 'Success'}

    {filteredUsers.map(user => {
    return
    showDetails(user.id)}>
    {user.name}

    {user.email}


    })}

    }

    看看我们的组件现在是多么的小,多么的容易理解!这是在错综复杂的代码库中所能做的最简单、最重要的事情。

    但我们可以做得更好。

    2. 可重用的数据获取钩子

    现在,当我们看到我们 useGetRemoteData Hook 时,我们看到这个 Hook 正在做两件事:




    1. 从远程数据源获取数据




    2. 过滤数据




    让我们把获取远程数据的逻辑提取到一个单独的钩子,这个钩子的名字是 useHttpGetRequest,它把 URL 作为一个参数:

    import {useEffect, useReducer, useState} from "react";
    import {loadingReducer} from "./LoadingReducer";

    const initialState = {
    isLoading: true
    };

    export const useHttpGetRequest = (URL) => {

    const [users , setUsers] = useState([])
    const [state, dispatch] = useReducer(loadingReducer, initialState);

    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch(URL)
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    return {users , isLoading: state.isLoading}

    }

    我们还将 reducer 逻辑移除到一个单独的文件中:

    export function loadingReducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    所以现在我们的 useGetRemoteData 变成了:

    import {useEffect, useState} from "react";
    import {useHttpGetRequest} from "./useHttpGet";
    const REMOTE_URL = 'https://jsonplaceholder.typicode.com/users'

    export const useGetRemoteData = () => {
    const {users , isLoading} = useHttpGetRequest(REMOTE_URL)
    const [filteredUsers , setFilteredUsers] = useState([])

    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    return {filteredUsers , isLoading}
    }

    干净多了,对吧? 我们能做得更好吗? 当然,为什么不呢?

    3. 分解 UI 组件

    看看我们的组件,其中显示了用户的详细信息。我们可以为此创建一个可重用的 UserDetails 组件:

    const UserDetails = (user) => {

    const showDetails = (user) => {
    alert(user.contact)
    }

    return
    showDetails(user)}>
    {user.name}

    {user.email}


    }

    最后,我们的原始组件变成:

    import React from "react";
    import {useGetRemoteData} from "./useGetRemoteData";

    export const Users = () => {
    const {filteredUsers , isLoading} = useGetRemoteData()

    return <>
    Users List

    Loading state: {isLoading? 'Loading': 'Success'}

    {filteredUsers.map(user => )}

    }

    我们把代码从60行精简到了12行!我们创建了五个独立的组成部分,每个部分都有明确而单一的职责。

    让我们回顾一下我们刚刚做了什么


    让我们回顾一下我们的组件,看看我们是否实现了 SRP:




    • Users.js - 负责显示用户列表




    • UserDetails.js ー 负责显示用户的详细资料




    • useGetRemoteData.js - 负责过滤远程数据




    • useHttpGetrequest.js - 负责 HTTP 调用




    • LoadingReducer.js - 复杂的状态管理




    当然,我们可以改进很多其他的东西,但是这应该是一个很好的起点。


    链接:https://juejin.cn/post/6963480203637030926




    收起阅读 »

    React的性能优化(useMemo和useCallback)的使用

    一、业务场景 React是一个用于构建用户界面的javascript的库,主要负责将数据转换为视图,保证数据和视图的统一,react在每次数据更新的时候都会重新render来保证数据和视图的统一,但是当父组件内部数据的变化,在父组件下挂载的所有子组件也会重新...
    继续阅读 »

    一、业务场景



    React是一个用于构建用户界面的javascript的库,主要负责将数据转换为视图,保证数据和视图的统一,react在每次数据更新的时候都会重新render来保证数据和视图的统一,但是当父组件内部数据的变化,在父组件下挂载的所有子组件也会重新渲染,因为react默认会全部渲染所有的组件,包括子组件的子组件,这就造成不必要的浪费。

    1、使用类定义一个父组件
    export default class Parent extends React.Component {
    state = {
    count: 0,
    }
    render() {
    return(
    <div>
    我是父组件
    <button onClick={() => this.setState({count: this.state.count++})}>点击按钮</button>
    <Child />
    </div>
    )
    }
    }

    2、定义一个子组件

    class Child extends React.Component {
    render() {
    console.log('我是子组件');
    return (
    <div>
    我是子组件
    <Grandson />
    </div>
    )
    }
    }

    3、定义一个孙子组件

    class Grandson extends React.Component {
    render() {
    console.log('孙子组件')
    return(<div>孙子组件</div>)
    }
    }
    • 4、上面几个组件是比较标准的react的类组件,函数组件也是类似的,当你在父组件中点击按钮,其实你仅仅是想改变父组件内的count的值,但是你会发现每次点击的时候子组件和孙组件也会重新渲染,因为react并不知道是不是要渲染子组件,需要我们自己去判断。



    一、类组件中使用shouldComponentUpdate生命周期钩子函数

    1、在子组件中使用shouldComponentUpdate来判断是否要更新,

    其实就是根据this.props和函数参数中的nextProps中的参数来对比,如果返回false就不更新,如果返回ture就表示需要更新当前组件


    class Child extends React.Component {
    shouldComponentUpdate (nextProps, nextState) {
    console.log(nextProps, this.props);
    if (nextProps.count === this.props.count) {
    return false;
    } else {
    return true;
    }
    }
    ...
    }
  • **注意点:**这里的count是要父组件给当前组件传递的参数(就是你要监听变化的的来更新当前组件),如果你写一个nextProps.name === this.props.name其实,父组件并没有给当前组件传递name那么下面都是返回false组件不更新




  • 2、当子组件没更新,那么孙组件同样的不更新数据


  • 二、使用PureComponet语法糖

    其实PureComponet就是一个语法糖,只是官方在底层帮你实现了shouldComponentUpdate方法而已,使用的时候只需要子类继承这个类就可以

    • 1、子组件中继承

    class Child extends React.PureComponent {
    render() {
    console.log('我是子组件');
    return (
    <div>
    我是子组件
    <Grandson />
    </div>
    )
    }
    }

    2、在父组件中使用

    // 下面这种情况不会重新渲染子组件
    <Child/>
    // 下面这种情况下会重新渲染子组件
    <Child count={this.state.count}/>

    三、memo的使用



    当你子组件是类组件的时候可以使用shouldComponentUpdate钩子函数或类组件继承PureComponent来实现不渲染子组件,但是对于函数组件来说是不能用这两个方法的,因此react官方给函数组件提供了memo来对函数组件包装下,实现不必要的渲染。





    • 1、组件定义(这里也可以使用类组件)

    function Child () {
    console.log('我是子组件');
    return (
    <div>
    子组件
    </div>
    )
    }

    function Parent () {
    const [count, setCount] = useState(0);
    return (
    <div>
    我是父组件-{count}
    <button onClick={()=>setCount(count + 1)}>点击按钮</button>
    <Child />
    </div>
    )
    }

    2、这里我们父组件内部改变count并没有传递给子组件,但是子组件一样的重新渲染了,这并不是我们希望看到的,因为需要对子组件包装下

    function Child () {
    console.log('我是子组件');
    return (
    <div>
    子组件
    </div>
    )
    }

    const ChildMemo = React.memo(Child);
    function Parent () {
    const [count, setCount] = useState(0);
    return (
    <div>
    我是父组件-{count}
    <button onClick={()=>setCount(count + 1)}>点击按钮</button>
    {/* 这种情况下不会渲染子组件 */}
    <ChildMemo />
    {/* 这种情况下会渲染子组件 */}
    <ChildMemo count={count}/>
    </div>
    )
    }

    四、useMemouseCallback的认识




    • 1、useMemouseCallback都是具有缓存作用的,只是他们缓存对象不一样,一个是属性,一个是缓存函数,特点都是,当缓存依赖的没变,去获取还是获取曾经的缓存




    • 2、useMemo是对函数组件中的属性包装,返回一个具有缓存效果的新的属性,当依赖的属性没变化的时候,这个返回新属性就会从缓存中获取之前的。




    • 3、useCallback是对函数组件中的方法缓存,返回一个被缓存的方法

    五、useMemo的使用(我们依赖借用子组件更新的来做)

    • 1、根据上面的方式我们在父组件更新数据,观察子组件变化

    const Child = (props) => {
    console.log('重新渲染子组件', props);
    return (
    <div>子组件</div>
    )
    }
    const ChildMemo = React.memo(Child);

    const Parent = () => {
    const [count, setCount] = useState(0);
    const [number, setNumber]=useState(0)
    const userInfo = {
    age: count,
    name: 'hello',
    }

    const btnHandler = () => {
    setNumber(number+1);
    }
    return (
    <div>
    {number}-{count}
    <button onClick={btnHandler}>按钮</button>
    <ChildMemo userInfo={userInfo}/>
    </div>
    )
    }
  • 上面发现我们仅仅是更新了number的值,传递给子组件的对象值并没有变化,但是每次子组件都重新更新了,虽然我们在子组件上用了React.memo包装还是不行,这是因为在父组件中每次重新渲染,对于对象来说会是重新一个新的对象了。因此子组件要重新更新,




  • 2、使用useMemo对属性的包装

  • const userInfo = useMemo(() => {
    return {
    age: count,
    name: 'hello',
    };
    }, [count]);
  • 使用useMemo包装后的对象,重新返回一个具有缓存效果的新对象,第二个参数表示依赖性,或者叫观察对象,当被观察的没变化,返回的就是缓存对象,如果被观察的变化了,那么就会返回新的,现在不管你怎么更新number的值,子组件都不会重新更新了




  • 3、注意点:useMemo要配合React.memo来使用,不然传递到子组件也是不生效的

  • 六、useCallback的使用

    前面介绍了,useCallback是对一个方法的包装,返回一个具有缓存的方法,常见的使用场景是,父组件要传递一个方法给子组件

    • 1、在不使用useCallback的时候

    const Child = (props) => {
    console.log('渲染了子组件');
    const { onClick } = props;
    return (
    <button onClick={onClick}>点击按钮获取值</button>
    )
    }

    const ChildMemo = React.memo(Child);

    const Parent = () => {
    const [text, updateText] = useState('');
    const textRef = useRef(text);
    const handleSubmit = () => {
    console.log('当前的值', text);
    }
    return(
    <div>
    我是父组件
    <input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
    <ChildMemo onClick={handleSubmit}/>
    </div>
    )
    }
    • 结果是每次输入框输入值的时候,子组件就会重新渲染一次,其实子组件中仅仅是一个按钮,要获取最终输入的值,每次父组件输入值的时候,子组件就更新,很耗性能的

    2、使用useCallback来包装一个方法

    const Parent = () => {
    const [text, updateText] = useState('');
    const textRef = useRef();

    // useCallback又依赖了textRef的变化,因此可以获取到最新的数据
    const handleSubmit = useCallback(() => {
    console.log('当前输入框的值:', textRef.current);
    }, [textRef])

    // 当text的值变化的时候就会给textRef的current重新赋值
    useEffect(() => {
    textRef.current = text;
    }, [text]);

    return(
    <div>
    我是父组件
    <input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
    <ChildMemo onClick={handleSubmit}/>
    </div>
    )
    }


    原文:https://juejin.cn/post/6965302793242411021

    收起阅读 »

    React22-diff算法

    1.时间复杂度最好的算法都是o(n3) n为元素个数 如果每个元素都去diff的话复杂度很高 所以react做出啦三个限制 1.只对同级元素进行diff,不用对另一个树的所有元素进行diff 2.tag不同的两个元素会产生不同的树,div变为p,react会销...
    继续阅读 »

    1.时间复杂度

    最好的算法都是o(n3) n为元素个数 如果每个元素都去diff的话复杂度很高 所以react做出啦三个限制


    1.只对同级元素进行diff,不用对另一个树的所有元素进行diff


    2.tag不同的两个元素会产生不同的树,div变为p,react会销毁div及其子孙节点,新建p


    3.通过key这个prop来暗示哪些子元素在不同渲染下保持稳定


    举例说明第3点

    // 更新前
    <div>
    <p >ka</p>
    <h3 >song</h3>
    </div>

    // 更新后
    <div>
    <h3 >song</h3>
    <p>ka</p>
    </div>
    这种情况下diff react会把p删除然后新建h3 插入 然后删除h3创建p在传入
    // 更新前
    <div>
    <p key="ka">ka</p>
    <h3 key="song">song</h3>
    </div>

    // 更新后
    <div>
    <h3 key="song">song</h3>
    <p key="ka">ka</p>
    </div>
    //有key的情况,p节点找到后面key相同的p节点所以节点可以复用 h3也可以复用 只需要做一个p append到h3后面的操作

    2.单一节点的diff

    1.diff 就是对比jsx和currentfiber的对象生成workingprogress的fiber


    2.dom节点是否可复用?1.type必须相同 2.key也必须相同 满足这两个才能复用 先检测key是否同再检测type是否同,可复用就复用这个fiber 不过是换属性而已



    3.什么情况不能复用?


    1.key不一样: 这函数一开始就判断key是否相同 不相同直接删除current的fiber,然后找兄弟节点去看是否key相同,为什么找兄弟节点呢 因为可能currentfiber有同级节点而jsx只是单个节点,还是会走到singleelement这个逻辑,所以要看currentfiber同级所有节点,不能旧删除是否可以复用。同时如果某个节点可以复用我们也需要将currentfiber的其他同级fiber删掉。都是为了下面这种情况。


    如果都不一样则创建新的fiber

    //current
    <div></div>
    <p></p>
    //jsx
    <p><p>

    2.type不一样:直接把current的该fiber和兄弟fiber全部删除,因为能判断到type的时候key已经相同啦,其他兄弟节点的key不可能相同,所以直接全部不可以复用。之前key不同还要看兄弟key是否相同。

    3.多节点diff

    什么时候执行多节点diff?当jsx此次为数组即可,无论currentfiber是不是数组

    一共有三种情况

    1.节点更新

    //情况1—— 节点属性变化
    // 之前
    <ul>
    <li key="0" className="before">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="0" className="after">0<li>
    <li key="1">1<li>
    </ul>
    //情况2—— 节点类型更新
    // 之后
    <ul>
    <div key="0">0</div>
    <li key="1">1<li>
    </ul>

    2.节点新增或者减少

    //情况1 —— 新增节点
    // 之前
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    <li key="2">2<li>
    </ul>
    //情况2 —— 删除节点
    // 之后
    <ul>
    <li key="1">1<li>
    </ul>

    3.节点位置变化

    // 之前
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="1">1<li>
    <li key="0">0<li>
    </ul>

    在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。


    虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。


    newChildren[0]fiber比较,newChildren[1]fiber.sibling比较。


    所以无法使用双指针优化。


    多节点diff 最终会产生一条fiber链表,不过最后返回的还是一个fiber(第一个fiber)作为child


    基于以上原因,Diff算法的整体逻辑会经历两轮遍历:


    第一轮遍历:处理更新的节点。 因为更新的概率是最大的


    第二轮遍历:处理剩下的不属于更新的节点

    第一轮遍历



    1. let i = 0,遍历newChildren,将newChildren[i]oldFiber比较,判断DOM节点是否可复用。

    2. 如果可复用,i++,继续比较newChildren[i]oldFiber.sibling,可以复用则继续遍历。

    3. 如果不可复用,分两种情况:



    • key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。(因为key不同是对应节点位置变化不属于更新节点,等到第二轮循环处理)

    • key相同type不同导致不可复用,会将oldFiber标记为DELETION(这样在commit阶段就会删除这个dom),并继续遍历



    1. 如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。

    function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
    ): Fiber | null {
    // This algorithm can't optimize by searching from both ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.

    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.

    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.

    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.

    if (__DEV__) {
    // First, validate keys.
    let knownKeys = null;
    for (let i = 0; i < newChildren.length; i++) {
    const child = newChildren[i];
    knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
    }
    }

    let resultingFirstChild: Fiber | null = null;//!返回的fiber
    let previousNewFiber: Fiber | null = null;//!创建fiber链需要一个临时fiber来做连接

    let oldFiber = currentFirstChild;//!遍历到的current的fiber(旧的fiber)
    let lastPlacedIndex = 0;//!新创建的fiber节点对应的dom节点在页面中的位置 用来节点位置变化的
    let newIdx = 0;//!遍历到的jsx数组的索引
    let nextOldFiber = null;//!oldFiber的下一个fiber
    //!第一轮循环 处理节点更新的情况
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {//!fiber的index标记为当前fiber在同级fiber中的位置
    nextOldFiber = oldFiber;
    oldFiber = null;
    } else {
    nextOldFiber = oldFiber.sibling;
    }
    const newFiber = updateSlot(//!判断节点是否可以复用 这个函数主要看key是否相同 不相同直接返回null 相同则继续判断type是否相同 不相同则创建新的fiber返回 把旧的fiber打上delete的tag 新的fiber打上place的tag type也相同则可以复用fiber返回
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    lanes,
    );
    if (newFiber === null) {
    // TODO: This breaks on empty slots like null children. That's
    // unfortunate because it triggers the slow path all the time. We need
    // a better way to communicate whether this was a miss or null,
    // boolean, undefined, etc.
    if (oldFiber === null) {
    oldFiber = nextOldFiber;
    }
    break;
    }
    if (shouldTrackSideEffects) {
    if (oldFiber && newFiber.alternate === null) {
    // We matched the slot, but we didn't reuse the existing fiber, so we
    // need to delete the existing child.
    deleteChild(returnFiber, oldFiber);
    }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!给newfiber加上place的tag
    if (previousNewFiber === null) {
    // TODO: Move out of the loop. This only happens for the first run.
    resultingFirstChild = newFiber;
    } else {
    // TODO: Defer siblings if we're not at the right index for this slot.
    // I.e. if we had null values before, then we want to defer this
    // for each null value. However, we also don't want to call updateSlot
    // with the previous one.
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
    }
    //!新旧同时遍历完
    if (newIdx === newChildren.length) {
    // We've reached the end of the new children. We can delete the rest.
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
    }
    //!老的遍历完 新的没遍历完 遍历剩下的jsx的newchildren
    if (oldFiber === null) {
    // If we don't have any more existing children we can choose a fast path
    // since the rest will all be insertions.
    for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    if (newFiber === null) {
    continue;
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!直接把新的节点打上place 插入dom
    if (previousNewFiber === null) {
    // TODO: Move out of the loop. This only happens for the first run.
    resultingFirstChild = newFiber;
    } else {
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    }
    return resultingFirstChild;
    }
    //!老的没遍历完 新的也没遍历完 证明key不同跳出啦 要处理节点位置变化的情况 我们要找到key相同的复用 那么为了在o(1)时间内找到 我们用map(key:oldfiber.key->value:oldfiber)数据结构
    // Add all children to a key map for quick lookups.
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    //!遍历剩下的newchldren
    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(//!找到newchildren的key对应的oldfiber 复用/新建fiber返回
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx],
    lanes,
    );
    if (newFiber !== null) {
    if (shouldTrackSideEffects) {
    if (newFiber.alternate !== null) {
    // The new fiber is a work in progress, but if there exists a
    // current, that means that we reused the fiber. We need to delete
    // it from the child list so that we don't add it to the deletion
    // list.
    existingChildren.delete(
    newFiber.key === null ? newIdx : newFiber.key,//!从map中去掉已经找到key的oldfiber
    );
    }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!新的fiber标记为插入 注意位置 (oldindex<lastplaceindex) 移动位置插入 因为老的fiber的index位置比新的页面位置小 肯定要移动插入了
    if (previousNewFiber === null) {
    resultingFirstChild = newFiber;
    } else {
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    }
    }

    if (shouldTrackSideEffects) {
    // Any existing children that weren't consumed above were deleted. We need
    // to add them to the deletion list.
    existingChildren.forEach(child => deleteChild(returnFiber, child));//!删除多余的oldfiber 因为新的children已经遍历完
    }

    return resultingFirstChild;
    }

    原文:https://juejin.cn/post/6964653615256436750

    收起阅读 »

    SwiftUI 入门指引教程

    这是 WWDC2019 发布的 SwiftUI 布局框架的一些官方示例。首先为了保证项目的正常运行,需要升级 Mac OS 至 10.15 beta 版,以及 Xcode 使用 Xcode 11 beta。1.创建项目运行首先创建一个新的项目,模板可以使用第一...
    继续阅读 »

    这是 WWDC2019 发布的 SwiftUI 布局框架的一些官方示例。

    首先为了保证项目的正常运行,需要升级 Mac OS 至 10.15 beta 版,以及 Xcode 使用 Xcode 11 beta。

    1.创建项目运行

    首先创建一个新的项目,模板可以使用第一个Single View App,项目名称官方的Demo叫做Landmarks,勾选上Use SwiftUI如图。


    然后创建项目,点击打开 ContentView.swift,代码如下:

    import SwiftUI

    struct ContentView: View {
    var body: some View {
    Text("Hello World")
    }
    }

    struct ContentView_Preview: PreviewProvider {
    static var previews: some View {
    ContentView()
    }
    }

    目前该类声明了两个 struct,第一个是该 View 的实现,第二个是为了实现该 View 的浏览。

    然后在 canvas 视图上点击 Resume(如果找不到,打开 Editor > Editor and Canvas )。

    然后修改 View 实现的代码,可以实时看到效果

    struct ContentView: View {
    var body: some View {
    Text("Hello SwiftUI!")
    }
    }

    2、定制TextView

    在之前的基础上,按住Command,并单击 Hello SwiftUI!,会弹出菜单,选择Inspect修改属性。


    点击之后


    修改 Font 为 title。

    然后手动修改UI代码,添加颜色为绿色:

    struct ContentView: View {
    var body: some View {
    Text("Turtle Rock")
    .font(.title)
    .color(.green)
    }
    }

    接下来在代码中单击文本的声明Text("Turtle Rock"),可以看到弹出的菜单,点击检查器inspect,把颜色再改回黑色。

    这个时候你会发现,Xcode会删除Text("Turtle Rock")这一行。

    3、使用 Stack 去组合 View


    这一部分会添加几个视图,并使用 Stack去组合。

    单击 Text("Turtle Rock"),弹出的菜单中选择 Embed in VStack。


    单击Xcode窗口右上角的加号按钮(+)打开库,然后在“Turtle Rock”文本视图后将Text视图拖到代码中的位置。

    替换文本为 Joshua Tree National Park,设置字体为.subheadline。

    然后编辑VStack的初始化方法,代码修改为 VStack(alignment: .leading) { 使得它左对齐。

    然后在 canvas 里面 command 并单击 Joshua Tree National Park,选择 Embed in HStack 添加一个新的 textView,输入内容 California,设置字体为 .subheadline。

    通过将 Spacer 添加到包含两个 Text 的水平堆栈,使得布局使用设备的整个宽度,如下:

    struct ContentView: View {
    var body: some View {
    VStack(alignment: .leading) {
    Text("Turtle Rock")
    .font(.title)
    HStack {
    Text("Joshua Tree National Park")
    .font(.subheadline)
    Spacer()
    Text("California")
    .font(.subheadline)
    }
    }
    }
    }

    Spacer 会使用俯视图所有的空间,彻底的展开,不需要通过指定内容大小等属性。

    最后,使用 padding()修饰符,添加到Stack的实现结束的地方,给界面留一些呼吸的空间。

    struct ContentView: View {
    var body: some View {
    VStack(alignment: .leading) {
    Text("Turtle Rock")
    .font(.title)
    HStack {
    Text("Joshua Tree National Park")
    .font(.subheadline)
    Spacer()
    Text("California")
    .font(.subheadline)
    }
    }
    .padding()
    }
    }

    4、Image 添加图片

    这一部分会添加一个独立的原型的自定义图片视图,将遮罩,边框和阴影应用于图像。

    将图片添加到资源 asset 目录下。


    创建一个新的额 SwiftUI 类,命名为 CircleImage.swift,并替换其实现如下,使用Image 的初始化方法 Image(_:)。

    struct CircleImage: View {
    var body: some View {
    Image("turtlerock")
    }
    }

    Image初始化方法之后添加圆形剪裁形状,Circle 可以像这样用做于一个蒙版,或者用作一个试图内的原型的填充。

    struct CircleImage: View {
    var body: some View {
    Image("turtlerock")
    .clipShape(Circle())
    }
    }

    然后添加其余的属性,颜色,线宽和半径为10个单位的阴影:

    struct CircleImage: View {
    var body: some View {
    Image("turtlerock")
    .clipShape(Circle())
    .overlay(
    Circle().stroke(Color.gray, lineWidth: 4))
    .shadow(radius: 10)
    }
    }

    5.组合成详情 View


    在第一个 ContentView 类中插入一个新的 VStack 视图,位置如下:

    struct ContentView: View {
    var body: some View {
    VStack {
    VStack(alignment: .leading) {
    Text("Turtle Rock")
    .font(.title)
    HStack(alignment: .top) {
    Text("Joshua Tree National Park")
    .font(.subheadline)
    Spacer()
    Text("California")
    .font(.subheadline)
    }
    }
    .padding()
    }
    }
    }

    添加一个 MapView,在新添加的 VStack 的下面,设置 MapView 的 Size, 如下:

    struct ContentView: View {
    var body: some View {
    VStack {
    MapView()
    .frame(height: 300)

    VStack(alignment: .leading) {
    Text("Turtle Rock")
    .font(.title)

    仅指定 height 参数的话,View 会自动调整其内容的宽度。在这种情况下,MapView 会扩展以填充可用空间。

    点击 Live Preview 实时预览视图。

    然后在 MapView 的下方,添加我们上一步实现的 CircleImage,并且设置向上的位置偏移量。

    CircleImage()
    .offset(y: -130)
    .padding(.bottom, -130)

    在最外面的 VStack 的底部添加一个 spacer,把内容整个推到屏幕的上面。

    最后:让 MapView 忽略上面的安全距离,在MapView下面插入 .edgesIgnoringSafeArea(.top),完整的类实现代码如下:

    struct ContentView: View {
    var body: some View {
    VStack {
    MapView()
    .edgesIgnoringSafeArea(.top)
    .frame(height: 300)

    CircleImage()
    .offset(y: -130)
    .padding(.bottom, -130)

    VStack(alignment: .leading) {
    Text("Turtle Rock")
    .font(.title)
    HStack(alignment: .top) {
    Text("Joshua Tree National Park")
    .font(.subheadline)
    Spacer()
    Text("California")
    .font(.subheadline)
    }
    }
    .padding()

    Spacer()
    }
    }
    }


    转自:https://www.jianshu.com/p/82524bf00b35

    收起阅读 »

    使用react的7个避坑案例

    React是个很受欢迎的前端框架。今天我们探索下React开发者应该注意的七个点。 1. 组件臃肿 React开发者没有创建必要的足够多的组件化,其实这个问题不局限于React开发者,很多Vue开发者也是。 当然,我们现在讨论的是React 在React中...
    继续阅读 »

    React是个很受欢迎的前端框架。今天我们探索下React开发者应该注意的七个点。


    1. 组件臃肿


    React开发者没有创建必要的足够多的组件化,其实这个问题不局限于React开发者,很多Vue开发者也是。



    当然,我们现在讨论的是React



    React中,我们可以创建一个很多内容的组件,来执行我们的各种任务,但是最好是保证组件精简 -- 一个组件关联一个函数。这样不仅节约你的时间,而且能帮你很好地定位问题


    比如下面的TodoList组件:

    // ./components/TodoList.js

    import React from 'react';

    import { useTodoList } from '../hooks/useTodoList';
    import { useQuery } from '../hooks/useQuery';
    import TodoItem from './TodoItem';
    import NewTodo from './NewTodo';

    const TodoList = () => {
    const { getQuery, setQuery } = useQuery();
    const todos = useTodoList();
    return (
    <div>
    <ul>
    {todos.map(({ id, title, completed }) => (
    <TodoItem key={id} id={id} title={title} completed={completed} />
    ))}
    <NewTodo />
    </ul>
    <div>
    Highlight Query for incomplete items:
    <input value={getQuery()} onChange={e => setQuery(e.target.value)} />
    </div>
    </div>
    );
    };

    export default TodoList;

    2. 直接更改state

    React中,状态应该是不变的。如果你直接修改state,会导致难以修改的性能问题。

    比如下面例子:

    const modifyPetsList = (element, id) => {
    petsList[id].checked = element.target.checked;
    setPetsList(petList)
    }

    上面例子中,你想更改数组对象中checked键。但是你遇到一个问题:因为使用相同的引用更改了对象,React无法观察并触发重新渲染


    解决这个问题,我们应该使用setState()方法或者useState()钩子。


    我们使用useState()方法来重写之前的例子。

    const modifyPetsList = (element, id) => {
    const { checked } = element.target;
    setpetsList((pets) => {
    return pets.map((pet, index) => {
    if (id === index) {
    pet = { ...pet, checked };
    }
    return pet;
    });
    });
    };

    3. props该传数字类型的值却传了字符串,反之亦然

    这是个很小的错误,不应该出现。

    比如下面的例子:

    class Arrival extends React.Component {
    render() {
    return (
    <h1>
    Hi! You arrived {this.props.position === 1 ? "first!" : "last!"} .
    </h1>
    );
    }
    }

    这里===对字符串'1'是无效的。而解决这个问题,需要我们在传递props值的时候用{}包裹。

    修正如下:

    // ❌
    const element = <Arrival position='1' />;

    // ✅
    const element = <Arrival position={1} />;

    4. list组件中没使用key

    假设我们需要渲染下面的列表项:

    const lists = ['cat', 'dog', 'fish’];

    render() {
    return (
    <ul>
    {lists.map(listNo =>
    <li>{listNo}</li>)}
    </ul>
    );
    }

    当然,上面的代码可以运行。当列表比较庞杂并需要进行更改等操作的时候,就会带来渲染的问题。


    React跟踪文档对象模型(DOM)上的所有列表元素。没有记录可以告知React,列表发生了什么改动。


    解决这个问题,你需要添加keys在你的列表元素中keys赋予每个元素唯一标识,这有助于React确定已添加,删除,修改了哪些项目。


    如下:

    <ul>
    {lists.map(listNo =>
    <li key={listNo}>{listNo}</li>)}
    </ul>

    5. setState是异步操作

    很容易忘记React中的state是异步操作的。如果你在设置一个值之后就去访问它,那么你可能不能立马获取到该值。

    我们看看下面的例子:

    handlePetsUpdate = (petCount) => {
    this.setState({ petCount });
    this.props.callback(this.state.petCount); // Old value
    };

    你可以使用setState()的第二个参数,回调函数来处理。比如:

    handlePetsUpdate = (petCount) => {
    this.setState({ petCount }, () => {
    this.props.callback(this.state.petCount); // Updated value
    });
    };

    6. 频繁使用Redux

    在大型的React app中,很多开发者使用Redux来管理全局状态。

    虽然Redux很有用,但是没必要使用它来管理每个状态

    如果我们的应用程序没有需要交换信息的并行级组件的时候,那么就不需要在项目中添加额外的库。比如我们想更改组件中的表单按钮状态的时候,我们更多的是优先考虑state方法或者useState钩子。

    7. 组件没以大写字母开头命名

    在JSX中,以小写开头的组件会向下编译为HTML元素

    所以我们应该避免下面的写法:

    class demoComponentName extends React.Component {
    }

    这将导致一个错误:如果你想渲染React组件,则需要以大写字母开头。

    那么得采取下面这种写法:

    class DemoComponentName extends React.Component {
    }

    后话

    上面的内容提取自Top 10 mistakes to avoid when using React,采用了意译的方式,提取了7条比较实用的内容。

    原文:https://juejin.cn/post/6963032224316784654


    收起阅读 »

    SwiftUI官方教程解读

    SwiftUI简介SwiftUI是wwdc2019发布的一个新的UI框架,通过声明和修改视图来布局UI和创建流畅的动画效果。并且我们可以通过状态变量来进行数据绑定实现一次性布局;Xcode 11 内建了直观的新设计工具canvus,在整个开发过程中,预览可视化...
    继续阅读 »

    SwiftUI简介

    SwiftUI是wwdc2019发布的一个新的UI框架,通过声明和修改视图来布局UI和创建流畅的动画效果。并且我们可以通过状态变量来进行数据绑定实现一次性布局;Xcode 11 内建了直观的新设计工具canvus,在整个开发过程中,预览可视化与代码可编辑性能同时支持并交互,让我们可以体验到代码和布局同步的乐趣;同时支持和UIkit的交互.

    设计工具canvus

    1、开发者可以在canvus中拖拽控件来构建界面, 所编辑的内容会立刻反应到代码上
    2、切换不同的视图文件时canvus会切换到不同的界面
    3、点击左下角的按钮钉我们可以把视图固定在活跃页面
    4、选中canvus中的控件command+click可以调出inspect布局控件的属性
    5、点击右上角的+可以获取新的控件并拖拽到对应的位置
    6、在live状态下我们可以在canvus中调试点击等可交互效果 但不能缩放视图大小

    每次修改或者增加属性需要点击resume刷新canvus


    文件结构

    创建一个SwiftUI文件,默认生成两个结构体。一个实现view的协议,在body属性里描述内容和布局;一个结构体声明预览的view 并进行初始化等信息,预览view是控制器的view时可以显示在多个模拟器设备,是控件view时可以设置frame,预览view是提供给canvus展示的,使用了#if DEBUG 指令,编译器会删除代码,不会随应用程序一起发布

    struct LandmarksList_Previews: PreviewProvider {
    static var previews: some View {
    ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
    LandmarkList()
    .previewDevice(PreviewDevice(rawValue: deviceName))
    .previewDisplayName(deviceName)
    //.previewLayout(.fixed(width: 300, height: 70)) 设置view控件大小
    }
    .environmentObject(UserData())
    }
    }
    #endif

    布局

    普通的view:将多个视图组合并嵌入到堆栈中,这些堆栈将视图水平、垂直或者前后组合在一起

    VStack {  //这里的布局实现的是上图canvus中landMarkDetail的效果
    MapView(coordinate: landmark.locationCoordinate)
    .frame(height: 300)//不传width默认长度为整个界面
    CircleImage(image: landmark.image(forSize: 250))
    .offset(x: 0, y: -130)
    .padding(.bottom, -130)
    VStack(alignment: .leading) {
    Text(landmark.name)
    .font(.title)
    HStack(alignment: .top) {
    Text(landmark.park)
    .font(.subheadline)
    Spacer() //将水平的两个控件撑开
    Text(landmark.state)
    .font(.subheadline)
    }
    }
    .padding()
    Spacer()
    }

    列表的布局:要求数据是可被标识的
    (1)唯一标识每个元素的主键路径

    List(landmarkData.identified(by: \.id)) { landmark in
    LandmarkRow(landmark: landmark)
    }

    (2)数据类型实现Identifiable protocol,持有一个id 属性

    struct Landmark: Hashable, Codable, Identifiable {
    var id: Int //
    var name: String
    fileprivate var imageName: String
    fileprivate var coordinates: Coordinates
    var state: String
    var park: String
    var category: Category
    }
    List(landmarkData) { landmark in
    LandmarkRow(landmark: landmark)
    } //直接传数据源

    导航

    添加导航栏是将其嵌入到NavigationView中,点击跳转的控件包装在navigationButton中,以设置到目标视图的换位。navigationBarTitle设置导航栏的标题,navigationBarItems设置导航栏右边的item

    NavigationView {//显示导航view
    List {
    //SwiftUI里面的类似switch的控件,可以在list中直接组合布局
    Toggle(isOn: $showFavoritesOnly) {
    Text("Favorites only")
    }
    ForEach(landmarkData) { landmark in
    if !self.showFavoritesOnly || landmark.isFavorite {
    //跳转到地标详细页面
    NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
    LandmarkRow(landmark: landmark)
    }
    }
    }
    }
    .navigationBarTitle(Text("Landmarks"))//导航标题
    }
    }

    实现modal出一个view

    .navigationBarItems(trailing:
    //点击navigationBarItems modal出profileHost页面
    PresentationButton(
    Image(systemName: "person.crop.circle")
    .imageScale(.large)
    .accessibility(label: Text("User Profile"))
    .padding(),
    destination: ProfileHost()
    )
    )

    程序运行是从sceneDelegate定义的根视图开始的, UIhostingController 是UIViewController的子类

    动画效果

    SwiftUI包括带有预定义或自定义的基本动画 以及弹簧和流体动画,可以调整动画速度,设置延迟,重复动画等等
    可以通过在一个动画修改器后面添加另一个动画修改器来关闭动画

    1、转场动画

    系统转场动画调用: hikeDetail(hike.hike).transition(.slide)
    自定义的转场动画:把转场动画作为AnyTransition类的类型属性 (方便点语法设置丰富自定义动画)

    extension AnyTransition {
    static var moveAndFade: AnyTransition {
    let insertion = AnyTransition.move(edge: .trailing)
    .combined(with: .opacity)
    let removal = AnyTransition.scale()
    .combined(with: .opacity)
    return .asymmetric(insertion: insertion, removal: removal)
    }
    }

    HikeDetail(hike: hike).transition(.moveAndFade)调用转场动画;move(edge:)方法是让视图从同一边滑出来以及消失;asymmetric(insertion:removal:)设置出现和小时的不同的动画效果

    2、阻尼动画

    var animation: Animation {  //定义成存储属性方便调用
    Animation.spring(initialVelocity: 5)//重力效果,值越大,弹性越大
    .speed(2)//动画时间,值越大动画速度越快
    .delay(0.03 * Double(index))
    }

    3、基础动画

    Button(action: //点击按钮显示一个view带转场的动画效果
    withAnimation {
    self.showDetail.toggle()
    }
    }) {
    Image(systemName: "chevron.right.circle")
    .imageScale(.large)
    //旋转90度
    .rotationEffect(.degrees(showDetail ? 90 : 0))
    //.animation(nil) //关闭前面的旋转90度的动画效果,只显示下面的动画
    //选中的时候放大为原来的1.5倍
    .scaleEffect(showDetail ? 1.5 : 1)
    .padding()
    // .animation(.basic()) 实现简单的基础动画
    //.animation(.spring()) 阻尼动画

    }

    给图片按钮加动画效果, 对应的会有旋转和缩放会有动画;加到action时,即使点击完成后的显示没有给image的可做动画属性加动画效果,全部都有动画,包含旋转缩放和转场动画

    数据流

    利用SwiftUI环境中的存储 ,把自定义数据对象绑定到view ,SwiftUI监视到可绑对象任何影响视图的更改并在更改后显示正确的视图

    1、自定义绑定类型
    声明为绑定类型 BindableObject ,PassthroughSubject是Combine框架的消息发布者, SwiftUI通过这个消息发布者订阅对象,并在数据发生变化的时候更新任何需要刷新的视图

    import Combine
    import SwiftUI
    final class UserData: BindableObject {
    let didChange = PassthroughSubject()

    var showFavoritesOnly = false {
    didSet {
    didChange.send(self)
    }
    }

    var landmarks = landmarkData {
    didSet {
    didChange.send(self)
    }
    }
    }

    当客户机需要更新数据的时候,可绑定对象通知其订阅者
    eg:当其中一个属性发生更改时,在属性的didset里面通过didchange发布者发布更改

    2、绑定属性
    (1)state

    @State var profile = Profile.default

    状态是随时间变化影响页面布局内容和行为的值
    给定类型的持久值,视图通过该持久值读取和监视该值。状态实例不是值本身;它是读取和修改值的一种方法。若要访问状态的基础值,请使用其值属性。

    (2)binding

    @Binding var profile: Profile//向子视图传递数据

    (3)environmentObject :

    @EnvironmentObject var userData: UserData

    存储在当前环境中的数据,跨视图传递,在初始化持有对象的时候使用environmentObject(_:)赋值可以和前面的自定义绑定类型一起使用

    let window = UIWindow(frame: UIScreen.main.bounds)
    window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))

    3、绑定行为

    是对可变状态或数据的引用,用$的前缀访问状态变量或者其属性之一实现绑定控件 也可以访问绑定属性来实现绑定

    与UIkit的交互

    表示UIkit的view和controller 需要创建遵UIViewRepresentable或者UIViewControllerRepresentable协议的结构体,SwiftUI管理他们的生命周期并在需要的时候更新
    实现协议方法:

    //创建展示的UIViewController,调用一次
    func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType
    //将展示的UIViewController更新到最新的版本
    func updateUIViewController(_ uiViewController: Self.UIViewControllerType, context: Self.Context)
    //创建协调器
    func makeCoordinator() -> Self.Coordinator

    在结构体内嵌套定义一个coordinator类。SwiftUI管理coordinator并把它提供给context ,在makeUIView(context:)之前调用这个makeCoordinator()方法创建协调器,以便在配置视图控制器的时候可以访问coordinator对象
    我们可以使用这个协调器来实现常见的Cocoa模式,例如委托、数据源和通过目标操作响应用户事件。

    这里以用UIPageViewController实现轮播图为例,要注意其中的更新页面的逻辑~
    pageview作为主view,组合一个PageControl 和 PageViewController实现图片轮播效果
    PageView: @State var currentPage = 1 定义绑定属性 ,$currentPage实现绑定到PageViewController
    PageViewController: @Binding var currentPage: Int 定义绑定属性,在更新的方法updateUIViewController里面绑定显示,点击pagecontrol的更新页面时pageviewcontroller可以更新到最新的页面
    pagecontrol: @Binding var currentPage: Int定义绑定属性 ,updateUIView 绑定显示,pageview滑动更新页面 pagecontrol可以更新到正确的显示

    struct PageView: View {
    var viewControllers: [UIHostingController]
    @State var currentPage = 1

    init(_ views: [Page]) {//传入的view用SwiftUI的controller包装好后面传给pagecontroller
    self.viewControllers = views.map { UIHostingController(rootView: $0) }
    }

    var body: some View {
    ZStack(alignment: .bottomTrailing) {//将currentpage绑定起来了
    PageViewController(controllers: viewControllers, currentPage: $currentPage)
    PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
    .padding()
    //Text("Current Page: \(currentPage)").padding(.trailing,30)
    }
    }
    }
    import SwiftUI
    import UIKit
    struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator {
    Coordinator(self)
    }
    func makeUIViewController(context: Context) -> UIPageViewController {
    let pageViewController = UIPageViewController(
    transitionStyle: .scroll,
    navigationOrientation: .horizontal)
    pageViewController.dataSource = context.coordinator
    pageViewController.delegate = context.coordinator

    return pageViewController
    }
    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
    //pageviewcontroller绑定currentpage显示当前的页面,pageView变化的时候,page更新页面
    pageViewController.setViewControllers(
    [controllers[currentPage]], direction: .forward, animated: true)

    }
    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
    var parent: PageViewController

    init(_ pageViewController: PageViewController) {
    self.parent = pageViewController
    }
    //左滑显示控制
    func pageViewController(
    _ pageViewController: UIPageViewController,
    viewControllerBefore viewController: UIViewController) -> UIViewController? {
    guard let index = parent.controllers.firstIndex(of: viewController) else {
    return nil
    }
    if index == 0 {
    return parent.controllers.last
    }
    return parent.controllers[index - 1]
    }
    // 右滑动显示控制
    func pageViewController(
    _ pageViewController: UIPageViewController,
    viewControllerAfter viewController: UIViewController) -> UIViewController? {
    guard let index = parent.controllers.firstIndex(of: viewController) else {
    return nil
    }
    if index + 1 == parent.controllers.count {
    return parent.controllers.first
    }
    return parent.controllers[index + 1]
    }
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    if completed,
    let visibleViewController = pageViewController.viewControllers?.first,
    let index = parent.controllers.firstIndex(of: visibleViewController) {
    //当view滑动停止的时候告诉pageview当前页面的index(数据变化 pageview更新pagecontrol的展示)
    parent.currentPage = index
    }
    }
    }
    }
    struct PageControl: UIViewRepresentable {
    var numberOfPages: Int
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator {
    Coordinator(self)
    }

    func makeUIView(context: Context) -> UIPageControl {
    let control = UIPageControl()
    control.numberOfPages = numberOfPages
    control.addTarget(
    context.coordinator,
    action: #selector(Coordinator.updateCurrentPage(sender:)),
    for: .valueChanged)

    return control
    }

    func updateUIView(_ uiView: UIPageControl, context: Context) {
    uiView.currentPage = currentPage
    }

    class Coordinator: NSObject {
    var control: PageControl

    init(_ control: PageControl) {
    self.control = control
    }

    @objc
    func updateCurrentPage(sender: UIPageControl) {
    control.currentPage = sender.currentPage
    }
    }
    }

    : 当我们编辑一部分用户数据的时候,我们不希望在编辑数据完成的时候影响到其他的页面 那么我们需要创建一个副本数据, 当副本数据编辑完成的时候 用副本数据更新真正的数据, 使相关的页面变化 这部分的内容参见demo中profiles的部分;对于画图的部分demo中也有非常酷炫的示例,详情参见 HikeGraph、Badge(徽章)

    参考资料

    Apple官网教程 :https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
    demo下载
    SwiftUI documentation

    作者简介

    就职于甜橙金融(翼支付)信息技术部,负责 iOS 客户端开发

    转自:https://www.jianshu.com/p/ecfdbea7a0ed

    收起阅读 »

    Swift 5—表达通过字符串插值

    Swift的设计 - 首先是 - 是一种安全的语言。检查数字和集合是否有溢出,变量总是在第一次使用之前初始化,选项确保正确处理非值,并且相应地命名任何可能不安全的操作。这些语言功能在很大程度上消除了一些最常见的编程错误,但我们不得不让我们guard失望。今天,...
    继续阅读 »

    Swift的设计 - 首先是 - 是一种安全的语言。检查数字和集合是否有溢出,变量总是在第一次使用之前初始化,选项确保正确处理非值,并且相应地命名任何可能不安全的操作。

    这些语言功能在很大程度上消除了一些最常见的编程错误,但我们不得不让我们guard失望。

    今天,我想谈谈Swift 5中最激动人心的新功能之一:对字符串文字中的值如何通过协议进行插值的大修。很多人都对你可以用它做的很酷的事感到兴奋。(理所当然!我们将在短时间内完成所有这些工作)但我认为重要的是要更广泛地了解这一功能,以了解其影响的全部范围。ExpressibleByStringInterpolation

    格式字符串很糟糕。

    在不正确的NULL处理,缓冲区溢出和未初始化的变量之后, printf/scanf -style格式字符串可以说是C风格编程语言中最有问题的延迟。

    在过去的20年中,安全专业人员已经记录了 数百个与格式字符串漏洞相关的漏洞。它是如此普遍,它被赋予了自己的 Common Weakness Enumeration常见的弱点列举类别。

    他们不仅不安全,而且难以使用。是的,很难用。

    考虑属性on ,它采用 格式字符串。如果我们想要创建一个包含其年份的日期的字符串表示,我们将使用,就像年份一样...... 对吗 dateFormat DateFormatter strftime "Y" "Y"

    import Foundation

    let formatter = DateFormatter()
    formatter.dateFormat = "M/d/Y"

    formatter.string(from: Date()) // "2/4/2019"

    这看起来确实如此,至少在今年的第一个360天。但是当我们跳到今年的最后一天时会发生什么?

    let dateComponents = DateComponents(year: 2019,
    month: 12,
    day: 31)
    let date = Calendar.current.date(from: dateComponents)!
    formatter.string(from: date) // "12/31/2020" (😱)

    啊,啥? 结果"Y"是ISO周编号年的格式 ,它将在2019年12月31日返回2020,因为第二天是新年第一周的星期三。

    我们真正想要的是"y"。

    formatter.dateFormat = "M/d/y"
    formatter.string(from: date) // 12/31/2019 (😄)

    格式化字符串是最难以使用的类型,因为它们很容易被错误地使用。日期格式字符串是最糟糕的,因为可能不清楚你做错了,直到为时已晚。它们是你的代码库中的字面时间炸弹。

    现在花点时间(如果您还没有),"Y"在实际意图使用时,审核您的代码库以使用日期格式字符串"y"。

    到目前为止,问题一直是API必须在危险但富有表现力的特定于域的语言特定领域的语言之间进行选择,例如格式字符串,以及正确但灵活性较低的方法调用。

    Swift 5中的新功能,该协议允许这些类型的API既正确又富有表现力。在这样做的过程中,它推翻了几十年来有问题的行为。ExpressibleByStringInterpolation

    所以不用多说,让我们来看看它是什么以及它是如何工作的:ExpressibleByStringInterpolation

    ExpressibleByStringInterpolation

    符合协议的类型可以自定义字符串文字中的内插值(即,转义的值)。
    ExpressibleByStringInterpolation \(...)

    您可以通过扩展默认String插值类型()或创建符合的新类型来利用此新协议。DefaultStringInterpolation ExpressibleByStringInterpolation

    有关更多信息,请参阅Swift Evolution提议 SE-0228:“Fix ExpressibleByStringInterpolation”

    扩展默认字符串插值

    默认情况下,在Swift 5之前,字符串文字中的所有插值都直接发送到String初始值设定项。现在,您可以指定其他参数,就像调用方法一样(实际上,这就是您在幕后所做的事情)。ExpressibleByStringInterpolation

    作为一个例子,让我们回顾以前的mixup "Y"和"y" ,看看这种混乱可能与避免。ExpressibleByStringInterpolation

    通过扩展String默认插值类型(aptly-named ),我们可以定义一个名为的新方法。第一个未命名参数的类型确定哪些插值方法可用于要插值的值。在我们的例子中,我们将定义一个方法,该方法接受一个参数和一个 我们将用来指定哪个类型的附加参数DefaultStringInterpolation appendingInterpolation appendInterpolation Date component Calendar.Component

    import Foundation

    #if swift(<5)
    #error("Download Xcode 10.2 Beta 2 to see this in action")
    #endif

    extension DefaultStringInterpolation {
    mutating func appendInterpolation(_ value: Date,
    component: Calendar.Component)
    {
    let dateComponents =
    Calendar.current.dateComponents([component],
    from: value)

    self.appendInterpolation(
    dateComponents.value(for: component)!
    )
    }
    }

    现在我们可以为每个单独的组件插入日期:

    "\(date, component: .month)/\(date, component: .day)/\(date, component: .year)"
    // "12/31/2019" (😊)

    这很冗长,是的。但是你永远不会误认为日历组件等同于你真正想要的东西:。.yearForWeekOfYear "Y" .year

    但实际上,我们不应该像这样手工格式化日期。我们应该将责任委托给:DateFormatter

    您可以像任何其他Swift方法一样重载插值,并使多个具有相同名称但不同类型的签名。例如,我们可以formatter为采用相应类型的日期和数字定义插值器。

    import Foundation

    extension DefaultStringInterpolation {
    mutating func appendInterpolation(_ value: Date,
    formatter: DateFormatter)
    {
    self.appendInterpolation(
    formatter.string(from: value)
    )
    }

    mutating func appendInterpolation<T>(_ value: T,
    formatter: NumberFormatter)
    where T : Numeric
    {
    self.appendInterpolation(
    formatter.string(from: value as! NSNumber)!
    )
    }
    }

    这允许与等效功能的一致接口,例如格式化插值日期和数字。

    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .full
    dateFormatter.timeStyle = .none
    "Today is \(Date(), formatter: dateFormatter)"
    // "Today is Monday, February 4, 2019"

    let numberformatter = NumberFormatter()
    numberformatter.numberStyle = .spellOut

    "one plus one is \(1 + 1, formatter: numberformatter)"
    // "one plus one is two"

    实现自定义字符串插值类型

    除了扩展,您还可以在符合的自定义类型上定义自定义字符串插值行为。如果满足以下任何条件,您可以这样做:
    DefaultStringInterpolation ExpressibleByStringInterpolation

    1、您希望区分文字和插值段
    2、您想限制可以插入的类型
    3、您希望支持与默认情况下提供的不同的插值行为
    3、您希望避免使用过多的API表面区域来增加内置字符串插值类型的负担

    对于一个简单的例子,考虑一个转义XML中的值的自定义类型,类似于我们上周描述的一个记录器。我们的目标:提供了一个很好的模板的API,允许我们编写XML / HTML和在自动转义字符一样的方式插入值<和>。

    我们将简单地用一个包含单个String值的包装器开始。

    struct XMLEscapedString: LosslessStringConvertible {
    var value: String

    init?(_ value: String) {
    self.value = value
    }

    var description: String {
    return self.value
    }
    }

    我们在扩展中添加一致性,就像任何其他协议一样。它继承自,需要初始化程序。 本身需要一个初始化程序,它接受所需的关联类型的实例。ExpressibleByStringInterpolation ExpressibleByStringLiteral init(stringLiteral:) ExpressibleByStringInterpolation init(stringInterpolation:) StringInterpolation

    此关联类型负责从字符串文字中收集所有文字段和插值。所有文字段都传递给方法。对于插值,编译器会找到与指定参数匹配的方法。在这种情况下,文字和插值都被收集到一个可变的字符串中。
    StringInterpolation appendLiteral(_:)``appendInterpolation

    这需要一个初始化器; 作为可选的优化,容量和插值计数可用于,例如,分配足够的空间来保存结果字符串。
    StringInterpolationProtocol init(literalCapacity:interpolationCount:)

    import Foundation

    extension XMLEscapedString: ExpressibleByStringInterpolation {
    init(stringLiteral value: String) {
    self.init(value)!
    }

    init(stringInterpolation: StringInterpolation) {
    self.init(stringInterpolation.value)!
    }

    struct StringInterpolation: StringInterpolationProtocol {
    var value: String = ""

    init(literalCapacity: Int, interpolationCount: Int) {
    self.value.reserveCapacity(literalCapacity)
    }

    mutating func appendLiteral(_ literal: String) {
    self.value.append(literal)
    }

    mutating func appendInterpolation<T>(_ value: T)
    where T: CustomStringConvertible
    {
    let escaped = CFXMLCreateStringByEscapingEntities(
    nil, value.description as NSString, nil
    )! as NSString

    self.value.append(escaped as String)
    }
    }
    }

    完成所有这些后,我们现在可以使用自动转义插值的字符串文字进行初始化。(没有XSS漏洞利用给我们,谢谢!)XMLEscapedString

    let name = "<bobby>"
    let markup: XMLEscapedString = """
    <p>Hello, \(name)!</p>
    """
    print(markup)
    // <p>Hello, <bobby>!</p>

    此功能的最佳部分之一是其实现的透明度。对于感觉非常神奇的行为,你永远不会想知道它是如何工作的。

    将上面的字符串文字与下面的等效API调用进行比较:

    var interpolation =
    XMLEscapedString.StringInterpolation(literalCapacity: 15,
    interpolationCount: 1)
    interpolation.appendLiteral("<p>Hello, ")
    interpolation.appendInterpolation(name)
    interpolation.appendLiteral("!</p>")

    let markup = XMLEscapedString(stringInterpolation: interpolation)
    // <p>Hello, <bobby>!</p>

    阅读就像诗歌一样,不是吗?

    有关更高级的示例,请查看Swift Strings Flight School指南示例代码中包含 的 Unicode样式操场

    看看它是如何运作的,很难不去环顾四周,找到无数机会可以使用它:ExpressibleByStringInterpolation

    1、格式化 字符串插值为日期和数字格式字符串提供了更安全,更易于理解的替代方法。
    2、转义 无论是URL中的转义实体,XML文档,shell命令参数还是SQL查询中的值,可扩展字符串插值都可以无缝且自动地进行正确的行为。
    3、装饰 使用字符串插值创建类型安全的DSL,用于为应用程序和终端输出创建属性字符串,使用ANSI控制序列来显示颜色和效果,或填充未加修饰的文本以匹配所需的对齐方式。
    4、本地化 而不是依赖于扫描源代码以查找“NSLocalizedString”匹配的脚本,字符串插值允许我们构建利用编译器查找本地化字符串的所有实例的工具。

    如果您考虑所有这些因素并考虑将来可能支持 编译时常量表达式,那么您发现Swift 5可能只是偶然发现了处理格式化的新方法。

    转自:https://www.jianshu.com/p/14cb3d70d133

    收起阅读 »

    Swift—文本输出流

    print是Swift标准库中最常用的函数之一。实际上,这是程序员在编写“Hello,world!”时学习的第一个函数。令人惊讶的是,我们很少有人熟悉其他形式。例如,您是否知道实际的签名print是 print(_:separator:terminator:)...
    继续阅读 »

    print是Swift标准库中最常用的函数之一。实际上,这是程序员在编写“Hello,world!”时学习的第一个函数。令人惊讶的是,我们很少有人熟悉其他形式。

    例如,您是否知道实际的签名print是 print(_:separator:terminator:)?或者它有一个名为print(_:separator:terminator:to:)?的变体 ?

    令人震惊,我知道。

    这就像了解你最好的朋友“Chaz” 的中间名,并且他的完整法定名称实际上是 “R”。巴克敏斯特小查尔斯拉格兰德“ - 哦,而且,他们一直都有一个完全相同的双胞胎。

    一旦你花了一些时间来收集自己,请继续阅读,找出你之前认为不需要进一步介绍的功能的全部真相。

    让我们首先仔细看看之前的函数声明:

    func print<Target>(_ items: Any...,
    separator: String = default,
    terminator: String = default,
    to output: inout Target)
    where Target : TextOutputStream

    这个重载print 采用可变长度的参数列表,后跟separator和terminator参数 - 两者都有默认值。

    1、separator是用于将每个元素的表示连接items 成单个字符串的字符串。默认情况下,这是一个空格(" ")。
    2、terminator是附加到打印表示的末尾的字符串。默认情况下,这是换行符(\ n "\n")。

    最后一个参数output 采用Target符合协议的泛型类型的可变实例。Text<wbr style="box-sizing: border-box;">Output<wbr style="box-sizing: border-box;">Stream

    符合的类型的实例 可以传递给函数以从标准输出中捕获和重定向字符串。Text<wbr style="box-sizing: border-box;">Output<wbr style="box-sizing: border-box;">Stream``print(_:to:)

    实现自定义文本输出流类型

    由于Unicode的多变性,您无法通过查看字符串来了解字符串中潜伏的字符。在 组合标记, 格式字符, 不支持的字符, 变体序列, 连字,有向图和其他表现形式之间,单个扩展字形集群可以包含远远超过眼睛的东西。

    举个例子,让我们创建一个符合的自定义类型。我们不会逐字地将字符串写入标准输出,而是检查每个组成<dfn style="box-sizing: border-box;">代码点</dfn>。Text<wbr style="box-sizing: border-box;">Output<wbr style="box-sizing: border-box;">Stream

    符合协议只是满足方法要求的问题。Text<wbr style="box-sizing: border-box;">Output<wbr style="box-sizing: border-box;">Stream``write(_:)

    protocol TextOutputStream {
    mutating func write(_ string: String)
    }

    在我们的实现中,我们迭代Unicode.Scalar传递的字符串中的每个值; 的enumerated()收集方法提供当前在每次循环偏移。在方法的顶部,guard如果字符串为空或换行符,则语句会提前解除(这会减少控制台中的噪音量)。

    struct UnicodeLogger: TextOutputStream {
    mutating func write(_ string: String) {
    guard !string.isEmpty && string != "\n" else {
    return
    }

    for (index, unicodeScalar) in
    string.unicodeScalars.lazy.enumerated()
    {
    let name = unicodeScalar.name ?? ""
    let codePoint = String(format: "U+X", unicodeScalar.value)
    print("\(index): \(unicodeScalar) \(codePoint)\t\(name)")
    }
    }
    }

    要使用我们的新类型,请初始化它并将其分配给变量(with ),以便它可以作为参数传递。任何时候我们想要获得字符串的X射线而不是仅仅打印它的表面表示,我们可以在我们的声明中添加一个额外的参数。Unicode<wbr style="box-sizing: border-box;">Logger``var``inout``print

    这样做可以让我们揭示关于表情符号字符的秘密👨👩👧👧:它实际上是 由<abbr title="零宽度木匠" style="box-sizing: border-box;">ZWJ</abbr>字符加入的四个单独表情符号的 序列 - 总共七个代码点!<abbr title="零宽度木匠" style="box-sizing: border-box;"></abbr>

    print("👨‍👩‍👧‍👧")
    // Prints: "👨‍👩‍👧‍👧"

    var logger = UnicodeLogger()
    print("👨‍👩‍👧‍👧", to: &logger)
    // Prints:
    // 0: 👨 U+1F468 MAN
    // 1: U+200D ZERO WIDTH JOINER
    // 2: 👩 U+1F469 WOMAN
    // 3: U+200D ZERO WIDTH JOINER
    // 4: 👧 U+1F467 GIRL
    // 5: U+200D ZERO WIDTH JOINER
    // 6: 👧 U+1F467 GIRL

    在Swift 5.0中,您可以通过其Unicode properties属性访问标量值的名称。与此同时,我们可以使用 字符串变换 来为我们提取名称(我们只需要在两端去掉一些残骸)。

    import Foundation

    extension Unicode.Scalar {
    var name: String? {
    guard var escapedName =
    "\(self)".applyingTransform(.toUnicodeName,
    reverse: false)
    else {
    return nil
    }

    escapedName.removeFirst(3) // remove "\\N{"
    escapedName.removeLast(1) // remove "}"

    return escapedName
    }
    }

    有关更多信息,请参阅 SE-0211:“将Unicode属性添加到Unicode.Scalar”。

    使用自定义文本输出流的想法

    现在我们知道Swift标准库的一个不起眼的部分,我们可以用它做什么?

    事实证明,有很多潜在的用例。为了更好地了解它们是什么,请考虑以下示例:Text<wbr style="box-sizing: border-box;">Output<wbr style="box-sizing: border-box;">Stream

    记录到标准错误

    默认情况下,Swift print语句指向 <dfn style="box-sizing: border-box;">标准输出(stdout)</dfn>。如果您希望改为指向 <dfn style="box-sizing: border-box;">标准error(stderr)</dfn>,则可以创建新的文本输出流类型并按以下方式使用它:

    import func Darwin.fputs
    import var Darwin.stderr

    struct StderrOutputStream: TextOutputStream {
    mutating func write(_ string: String) {
    fputs(string, stderr)
    }
    }

    var standardError = StderrOutputStream()
    print("Error!", to: &standardError)

    将输出写入文件

    前面的写入示例stderr 可以概括为写入任何流或文件,而是通过创建输出流 (可以通过类型属性访问标准错误)。File<wbr style="box-sizing: border-box;">Handle

    import Foundation

    struct FileHandlerOutputStream: TextOutputStream {
    private let fileHandle: FileHandle
    let encoding: String.Encoding

    init(_ fileHandle: FileHandle, encoding: String.Encoding = .utf8) {
    self.fileHandle = fileHandle
    self.encoding = encoding
    }

    mutating func write(_ string: String) {
    if let data = string.data(using: encoding) {
    fileHandle.write(data)
    }
    }
    }

    按照这种方法,您可以自定义print写入文件而不是流。

    let url = URL(fileURLWithPath: "/path/to/file.txt")
    let fileHandle = try FileHandle(forWritingTo: url)
    var output = FileHandlerOutputStream(fileHandle)

    print("\(Date())", to: &output)

    转发流输出

    作为最后一个例子,让我们想象一下你会发现自己经常将控制台输出复制粘贴到某个网站上的表单中的情况。不幸的是,该网站有试图解析无益的行为<,并>就好像它们是HTML。

    每次发布到网站时,您都可以创建一个 自动处理该文本的内容,而不是采取额外的步骤来逃避文本(在这种情况下,我们使用我们发现深埋在Core Foundation中的XML转义函数)。Text<wbr style="box-sizing: border-box;">Output<wbr style="box-sizing: border-box;">Stream

    import Foundation

    struct XMLEscapingLogger: TextOutputStream {
    mutating func write(_ string: String) {
    guard !string.isEmpty && string != "\n",
    let xmlEscaped = CFXMLCreateStringByEscapingEntities(nil, string as NSString, nil)
    else {
    return
    }

    print(xmlEscaped)
    }
    }

    var logger = XMLEscapingLogger()
    print("<3", to: &logger)
    // Prints "<3"

    对于开发人员来说,打印是一种熟悉且便捷的方式,可以了解其代码的行为。它补充了更全面的技术,如日志框架和调试器,并且 - 在Swift的情况下 - 证明它本身就非常强大。

    转自:https://www.jianshu.com/p/4901641b9c38

    收起阅读 »

    90 行代码的webpack,你确定不学吗?

    在前端社区里,webpack 可以说是一个经久不衰的话题。其强大、灵活的功能曾极大地促进了前端工程化进程的发展,伴随了无数前端项目的起与落。其纷繁复杂的配置也曾让无数前端人望而却步,笑称需要一个新工种"webpack 配置工程师"。作为一个历史悠久,最常见、最...
    继续阅读 »
    在前端社区里,webpack 可以说是一个经久不衰的话题。其强大、灵活的功能曾极大地促进了前端工程化进程的发展,伴随了无数前端项目的起与落。其纷繁复杂的配置也曾让无数前端人望而却步,笑称需要一个新工种"webpack 配置工程师"。作为一个历史悠久,最常见、最经典的打包工具,webpack 极具讨论价值。理解 webpack,掌握 webpack,无论是在面试环节,还是在日常项目搭建、开发、优化环节,都能带来不少的收益。那么本文将从核心理念出发,带各位读者拨开 webpack 的外衣,看透其本质。

    究竟是啥


    其实这个问题在 webpack 官网的第一段就给出了明确的定义:



    At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.



    其意为:



    webpack 的核心是用于现代 JavaScript 应用程序的静态模块打包器。 当 webpack 处理您的应用程序时,它会在内部构建一个依赖关系图,该图映射您项目所需的每个模块并生成一个或多个包



    要素察觉:静态模块打包器依赖关系图生成一个或多个包。虽然如今的前端项目中,webpack 扮演着重要的角色,囊括了诸多功能,但从其本质上来讲,其仍然是一个“模块打包器”,将开发者的 JavaScript 模块打包成一个或多个 JavaScript 文件。


    要干什么


    那么,为什么需要一个模块打包器呢?webpack 仓库早年的 README 也给出了答案:



    As developer you want to reuse existing code. As with node.js and web all file are already in the same language, but it is extra work to use your code with the node.js module system and the browser. The goal of webpack is to bundle CommonJs modules into javascript files which can be loaded by <script>-tags.



    可以看到,node.js 生态中积累了大量的 JavaScript 写的代码,却因为 node.js 端遵循的 CommonJS 模块化规范与浏览器端格格不入,导致代码无法得到复用,这是一个巨大的损失。于是 webpack 要做的就是将这些模块打包成可以在浏览器端使用 <script> 标签加载并运行的JavaScript 文件。


    或许这并不是唯一解释 webpack 存在的原因,但足以给我们很大的启发——把 CommonJS 规范的代码转换成可在浏览器运行的 JavaScript 代码


    怎么干的


    既然浏览器端没有 CommonJS 规范,那就实现一个好了。从 webpack 打包出的产物,我们能看出思路。


    新建三个文件观察其打包产物:


    src/index.js

    const printA = require('./a')
    printA()

    src/a.js

    const printB = require('./b')

    module.exports = function printA() {
    console.log('module a!')
    printB()
    }

    src/b.js

    module.exports = function printB() {
    console.log('module b!')
    }

    执行 npx webpack --mode development 打包产出 dist/main.js 文件


    上图中,使用了 webpack 打包 3 个简单的 js 文件 index.js/a.js/b.js, 其中 index.js 中依赖了 a.js, 而 a.js 中又依赖了 b.js, 形成一个完整依赖关系。


    那么,webpack 又是如何知道文件之间的依赖关系的呢,如何收集被依赖的文件保证不遗漏呢?我们依然能从官方文档找到答案:



    When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points, webpack recursively builds a dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, just one - to be loaded by the browser.



    也就是说,webpack 会从配置的入口开始,递归的构建一个应用程序所需要的模块的依赖树。我们知道,CommonJS 规范里,依赖某一个文件时,只需要使用 require 关键字将其引入即可,那么只要我们遇到require关键字,就去解析这个依赖,而这个依赖中可能又使用了 require 关键字继续引用另一个依赖,于是,就可以递归的根据 require 关键字找到所有的被依赖的文件,从而完成依赖树的构建了。


    可以看到上图最终输出里,三个文件被以键值对的形式保存到 __webpack_modules__ 对象上, 对象的 key 为模块路径名,value 为一个被包装过的模块函数。函数拥有 module, module.exports, __webpack_require__ 三个参数。这使得每个模块都拥有使用 module.exports 导出本模块和使用 __webpack_require__ 引入其他模块的能力,同时保证了每个模块都处于一个隔离的函数作用域范围。


    为什么 webpack要修改require关键字和require的路径?我们知道requirenode环境自带的环境变量,可以直接使用,而在其他环境则没有这样一个变量,于是需要webpack提供这样的能力。只要提供了相似的能力,变量名叫 require还是 __webpack_require__其实无所谓。至于重写路径,当然是因为在node端系统会根据文件的路径加载,而在 webpack打包的文件中,使用原路径行不通,于是需要将路径重写为 __webpack_modules__ 的键,从而找到相应模块。


    而下面的 __webpack_require__函数与 __webpack_module_cache__ 对象则完成了模块加载的职责。使用 __webpack_require__ 函数加载完成的模块被缓存到 __webpack_module_cache__ 对象上,以便下次如果有其他模块依赖此模块时,不需要重新运行模块的包装函数,减少执行效率的消耗。同时,如果多个文件之间存在循环依赖,比如 a.js 依赖了 b.js 文件, b.js 又依赖了 a.js,那么在 b.js 使用 __webpack_require__加载 a.js 时,会直接走进 if(cachedModule !== undefined) 分支然后 return已缓存过的 a.js 的引用,不会进一步执行 a.js 文件加载,从而避免了循环依赖无限递归的出现


    不能说这个由 webpack 实现的模块加载器与 CommonJS 规范一毛一样,只能说八九不离十吧。这样一来,打包后的 JavaScript 文件可以被 <script> 标签加载且运行在浏览器端了。

    简易实现


    了解了 webpack 处理后的 JavaScript 长成什么样子,我们梳理一下思路,依葫芦画瓢手动实现一个简易的打包器,帮助理解。


    要做的事情有这么些:



    1. 读取入口文件,并收集依赖信息

    2. 递归地读取所有依赖模块,产出完整的依赖列表

    3. 将各模块内容打包成一块完整的可运行代码


    话不多说,创建一个项目,并安装所需依赖

    npm init -y
    npm i @babel/core @babel/parser @babel/traverse webpack webpack-cli -D

    其中:



    • @babel/parser 用于解析源代码,产出 AST

    • @babel/traverse 用于遍历 AST,找到 require 语句并修改成 _require_,将引入路径改造为相对根的路径

    • @babel/core 用于将修改后的 AST 转换成新的代码输出


    创建一个入口文件 myPack.js 并引入依赖

    const fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const babel = require('@babel/core')

    紧接着,我们需要对某一个模块进行解析,并产出其模块信息,包括:模块路径、模块依赖、模块转换后代码

    // 保存根路径,所有模块根据根路径产出相对路径
    let root = process.cwd()

    function readModuleInfo(filePath) {
    // 准备好相对路径作为 module 的 key
    filePath =
    './' + path.relative(root, path.resolve(filePath)).replace(/\\+/g, '/')
    // 读取源码
    const content = fs.readFileSync(filePath, 'utf-8')
    // 转换出 AST
    const ast = parser.parse(content)
    // 遍历模块 AST,将依赖收集到 deps 数组中
    const deps = []
    traverse(ast, {
    CallExpression: ({ node }) => {
    // 如果是 require 语句,则收集依赖
    if (node.callee.name === 'require') {
    // 改造 require 关键字
    node.callee.name = '_require_'
    let moduleName = node.arguments[0].value
    moduleName += path.extname(moduleName) ? '' : '.js'
    moduleName = path.join(path.dirname(filePath), moduleName)
    moduleName = './' + path.relative(root, moduleName).replace(/\\+/g, '/')
    deps.push(moduleName)
    // 改造依赖的路径
    node.arguments[0].value = moduleName
    }
    },
    })
    // 编译回代码
    const { code } = babel.transformFromAstSync(ast)
    return {
    filePath,
    deps,
    code,
    }
    }

    接下来,我们从入口出发递归地找到所有被依赖的模块,并构建成依赖树

    function buildDependencyGraph(entry) {
    // 获取入口模块信息
    const entryInfo = readModuleInfo(entry)
    // 项目依赖树
    const graphArr = []
    graphArr.push(entryInfo)
    // 从入口模块触发,递归地找每个模块的依赖,并将每个模块信息保存到 graphArr
    for (const module of graphArr) {
    module.deps.forEach((depPath) => {
    const moduleInfo = readModuleInfo(path.resolve(depPath))
    graphArr.push(moduleInfo)
    })
    }
    return graphArr
    }

    经过上面一步,我们已经得到依赖树能够描述整个应用的依赖情况,最后我们只需要按照目标格式进行打包输出即可

    function pack(graph, entry) {
    const moduleArr = graph.map((module) => {
    return (
    `"${module.filePath}": function(module, exports, _require_) {
    eval(\`` +
    module.code +
    `\`)
    }`
    )
    })
    const output = `;(() => {
    var modules = {
    ${moduleArr.join(',\n')}
    }
    var modules_cache = {}
    var _require_ = function(moduleId) {
    if (modules_cache[moduleId]) return modules_cache[moduleId].exports

    var module = modules_cache[moduleId] = {
    exports: {}
    }
    modules[moduleId](module, module.exports, _require_)
    return module.exports
    }

    _require_('${entry}')
    })()`
    return output
    }

    直接使用字符串模板拼接成类 CommonJS 规范的模板,自动加载入口模块,并使用 IIFE 将代码包装,保证代码模块不会影响到全局作用域。

    最后,编写一个入口函数 main 用以启动打包过程

    function main(entry = './src/index.js', output = './dist.js') {
    fs.writeFileSync(output, pack(buildDependencyGraph(entry), entry))
    }

    main()

    执行并验证结果

    node myPack.js

    至此,我们使用了总共不到 90 行代码(包含注释),完成了一个极简的模块打包工具。虽然没有涉及任何 Webpack 源码, 但我们从打包器的设计原理入手,走过了打包工具的核心步骤,简易却不失完整。

    总结


    本文从 webpack 的设计理念和最终实现出发,梳理了其作为一个打包工具的核心能力,并使用一个简易版本实现帮助更直观的理解其本质。总的来说,webpack 作为打包工具无非是从应用入口出发,递归的找到所有依赖模块,并将他们解析输出成一个具备类 CommonJS 模块化规范的模块加载能力的 JavaScript 文件


    因其优秀的设计,在实际生产环节中,webapck 还能扩展出诸多强大的功能。然而其本质仍是模块打包器。不论是什么样的新特性或新能力,只要我们把握住打包工具的核心思想,任何问题终将迎刃而解。



    链接:https://juejin.cn/post/6963820624623960101

    收起阅读 »

    使用vue+element开发一个谷歌插件

    简单功能:点击浏览器右上角插件icon弹出小弹窗,点击设置弹出设置页,并替换背景颜色。开始1.本地创建文件夹testPlugin并新建manifest.json文件{ "name": "testPlugin", "description": "这是...
    继续阅读 »

    简单功能:点击浏览器右上角插件icon弹出小弹窗,点击设置弹出设置页,并替换背景颜色。

    开始
    • 1.本地创建文件夹testPlugin并新建manifest.json文件
    {
    "name": "testPlugin",
    "description": "这是一个测试用例",
    "version": "0.0.1",
    "manifest_version": 2
    }
    • 2.添加插件的小icon
      testPlugin下创建icons文件夹,可以放入一些不同尺寸的icon,测试可以偷懒都放一种尺寸的icon。修改manifest.json为:
    {
    "name": "testPlugin",
    "description": "这是一个测试用例",
    "version": "0.0.1",
    "manifest_version": 2,
    "icons": {
    "16": "icons/16.png",
    "48": "icons/16.png"
    }
    }

    这时候在扩展程序中加载已解压的程序(就是我们创建的文件夹),就可以看到雏形了:


    • 3.选择性地添加点击插件icon浏览器右上角弹出来的框
      manifest.json添加:
    "browser_action": {
    "default_title": "test plugin",
    "default_icon": "icons/16.png",
    "default_popup": "index.html"
    }

    testPlugin创建index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test plugin</title>
    </head>

    <body>
    <input id="name" placeholder="请输入"/>
    </body>
    </html>

    刷新插件,这时候点击浏览器中刚刚添加的插件的icon,就会弹出:


    • 4.js事件(样式同理)
      testPlugin创建js文件夹index.js文件:
    document.getElementById('button').onclick = function() {
    alert(document.getElementById('name').value)
    }

    html中:

    <input id="name" placeholder="请输入"/>
    <input id="button" type="button" value="点击"/>
    <script src="js/index.js"></script>

    刷新插件


    一个嵌入网页中的悬浮框
    上述例子是点击icon浏览器右上角出现的小弹窗,

    引入vue.js、element-ui
        下载合适版本的vue.js和element-ui等插件,同样按照index.js一样的操作引入,如果没有下载单独js文件的地址,可以打开cdn地址直接将压缩后的代码复制。
    manifest.json中添加:

    "content_scripts": [
    {
    "matches": [
    "<all_urls>"
    ],
    "css": [
    "css/index.css"
    ],
    "js": [
    "js/vue.js",
    "js/element.js",
    "js/index.js"
    ],
    "run_at": "document_idle"
    }
    ],

    在index.js文件:
    这里使用在head里插入link 的方式引入element-ui的css,减少插件包的一点大小,当然也可以像引入index.js那样在manifest.json中引入。
    直接在index.js文件中写Vue实例,不过首先得创建挂载实例的节点:

    let element = document.createElement('div')
    let attr = document.createAttribute('id')
    attr.value = 'appPlugin'
    element.setAttributeNode(attr)
    document.getElementsByTagName('body')[0].appendChild(element)

    let link = document.createElement('link')
    let linkAttr = document.createAttribute('rel')
    linkAttr.value = 'stylesheet'
    let linkHref = document.createAttribute('href')
    linkHref.value = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
    link.setAttributeNode(linkAttr)
    link.setAttributeNode(linkHref)
    document.getElementsByTagName('head')[0].appendChild(link)


    const vue = new Vue({
    el: '#appPlugin',
    template:`
    <div class="app-plugin-content">{{text}}{{icon_post_message}}<el-button @click="Button">Button</el-button></div>
    `,
    data: function () {
    return { text: 'hhhhhh', icon_post_message: '_icon_post_message', isOcContentPopShow: true }
    },
    mounted() {
    console.log(this.text)
    },
    methods: {
    Button() {
    this.isOcContentPopShow = false
    }
    }
    })
    让我们来写一个简易替换网页背景颜色工具

    index.js:

    let element = document.createElement('div')
    let attr = document.createAttribute('id')
    attr.value = 'appPlugin'
    element.setAttributeNode(attr)
    document.getElementsByTagName('body')[0].appendChild(element)

    let link = document.createElement('link')
    let linkAttr = document.createAttribute('rel')
    linkAttr.value = 'stylesheet'
    let linkHref = document.createAttribute('href')
    linkHref.value = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
    link.setAttributeNode(linkAttr)
    link.setAttributeNode(linkHref)
    document.getElementsByTagName('head')[0].appendChild(link)


    const vue = new Vue({
    el: '#appPlugin',
    template: `
    <div v-if="isOcContentPopShow" class="oc-move-page" id="oc_content_page">
    <div class="oc-content-title" id="oc_content_title">颜色 <el-button type="text" icon="el-icon-close" @click="close"></el-button></div>
    <div class="app-plugin-content">背景:<el-color-picker v-model="color1"></el-color-picker></div>
    <div class="app-plugin-content">字体:<el-color-picker v-model="color2"></el-color-picker></div>
    </div>
    `,
    data: function () {
    return { color1: null, color2: null, documentArr: [], textArr: [], isOcContentPopShow: true }
    },
    watch: {
    color1(val) {
    let out = document.getElementById('oc_content_page')
    let outC = document.getElementsByClassName('el-color-picker__panel')
    this.documentArr.forEach(item => {
    if(!out.contains(item) && !outC[0].contains(item) && !outC[1].contains(item)) {
    item.style.cssText = `background-color: ${val}!important;color: ${this.color2}!important;`
    }
    })
    },
    color2(val) {
    let out = document.getElementById('oc_content_page')
    let outC = document.getElementsByClassName('el-color-picker__panel')[1]
    this.textArr.forEach(item => {
    if(!out.contains(item) && !outC.contains(item)) {
    item.style.cssText = `color: ${val}!important;`
    }
    })
    }
    },
    mounted() {
    chrome.runtime.onConnect.addListener((res) => {
    if (res.name === 'testPlugin') {
    res.onMessage.addListener(mess => {
    this.isOcContentPopShow = mess.isShow
    })
    }
    })
    this.$nextTick(() => {
    let bodys = [...document.getElementsByTagName('body')]
    let headers = [...document.getElementsByTagName('header')]
    let divs = [...document.getElementsByTagName('div')]
    let lis = [...document.getElementsByTagName('li')]
    let articles = [...document.getElementsByTagName('article')]
    let asides = [...document.getElementsByTagName('aside')]
    let footers = [...document.getElementsByTagName('footer')]
    let navs = [...document.getElementsByTagName('nav')]
    this.documentArr = bodys.concat(headers, divs, lis, articles, asides, footers, navs)

    let as = [...document.getElementsByTagName('a')]
    let ps = [...document.getElementsByTagName('p')]
    this.textArr = as.concat(ps)

    })

    },
    methods: {
    close() {
    this.isOcContentPopShow = false
    }
    }
    })

    index.html:

    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>my plugin</title>
    <link rel="stylesheet" href="css/index.css">
    </head>

    <body>
    <div class="plugin">
    <input id="plugin_button" type="button" value="打开" />
    </div>
    </body>
    <script src="js/icon.js"></script>

    </html>

    新建icon.js:

    plugin_button.onclick = function () {
    mess()
    }
    async function mess () {
    const tabId = await getCurrentTabId()
    const connect = chrome.tabs.connect(tabId, {name: 'testPlugin'});
    connect.postMessage({isShow: true})
    }
    function getCurrentTabId() {
    return new Promise((resolve, reject) => {
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    resolve(tabs.length ? tabs[0].id : null)
    });
    })
    }

    新建index.css:

    .oc-move-page{
    width: 100px;
    height: 200px;
    background: white;
    box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.12);
    border-radius: 8px;
    position: fixed;
    transform: translateY(-50%);
    right: 0;
    top: 50%;
    z-index: 1000001;
    }
    .oc-move-page .oc-content-title{
    text-align: left;
    padding: 12px 16px;
    font-weight: 600;
    font-size: 18px;
    border-bottom: 1px solid #DEE0E3;
    }
    .oc-move-page .app-plugin-content {
    display: flex;
    align-items: center;
    margin-top: 10px;
    }

    .el-color-picker__panel {
    right: 100px!important;
    left: auto!important;
    }


    这样一个小尝试就完成了,当然如果有更多需求可以结合本地存储或者服务端来协作。

    本文链接:https://blog.csdn.net/qq_26769677/article/details/116611072



    收起阅读 »

    手把手教你利用js给图片打马赛克

    效果演示Canvas简介这个 HTML 元素是为了客户端矢量图形而设计的。它自己没有行为,但却把一个绘图 API 展现给客户端 JavaScript 以使脚本能够把想绘制的东西都绘制到一块画布上。HTML5 标签用于绘制图像(通过脚本,通常是 JavaScri...
    继续阅读 »

    效果演示


    Canvas简介

    这个 HTML 元素是为了客户端矢量图形而设计的。它自己没有行为,但却把一个绘图 API 展现给客户端 JavaScript 以使脚本能够把想绘制的东西都绘制到一块画布上。

    HTML5 标签用于绘制图像(通过脚本,通常是 JavaScript)

    不过, 元素本身并没有绘制能力(它仅仅是图形的容器) - 您必须使用脚本来完成实际的绘图任务

    getContext() 方法可返回一个对象,该对象提供了用于在画布上绘图的方法和属性

    本手册提供完整的 getContext(“2d”) 对象属性和方法,可用于在画布上绘制文本、线条、矩形、圆形等等

    标记和 SVG 以及 VML 之间的差异:

    标记和 SVG 以及 VML 之间的一个重要的不同是, 有一个基于 JavaScript 的绘图 API,而 SVG 和 VML 使用一个 XML 文档来描述绘图。

    这两种方式在功能上是等同的,任何一种都可以用另一种来模拟。从表面上看,它们很不相同,可是,每一种都有强项和弱点。例如,SVG 绘图很容易编辑,只要从其描述中移除元素就行。

    要从同一图形的一个 标记中移除元素,往往需要擦掉绘图重新绘制它

    知识点简介

    • 利用js创建图片
    let img = new Image()
    //可以给图片一个链接
    img.src = 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=826495019,1749283937&fm=26&gp=0.jpg'
    //或者本地已有图片的路径
    //img.src = './download.jpg'

    //添加到HTML中
    document.body.appendChild(img)
    • canvas.getContext(“2d”)

    语法:
    参数 contextID 指定了您想要在画布上绘制的类型。当前唯一的合法值是 “2d”,它指定了二维绘图,并且导致这个方法返回一个环境对象,该对象导出一个二维绘图 API

    let ctx = Canvas.getContext(contextID)
    • ctx.drawImage()
    JavaScript 语法 1:
    在画布上定位图像:
    context.drawImage(img,x,y);
    JavaScript 语法 2:
    在画布上定位图像,并规定图像的宽度和高度:
    context.drawImage(img,x,y,width,height);
    JavaScript 语法 3:
    剪切图像,并在画布上定位被剪切的部分:
    context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
    • ctx.getImageData()
    JavaScript 语法
    getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。
    对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:
    R - 红色 (0-255)
    G - 绿色 (0-255)
    B - 蓝色 (0-255)
    A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)
    color/alpha 以数组形式存在,并存储于 ImageData 对象的 data 属性中
    var imgData=context.getImageData(x,y,width,height);
    • ctx.putImageData()
    putImageData() 方法将图像数据(从指定的 ImageData 对象)放回画布上。

    那我们开始搞起来吧

    step-by-step

    准备好我们的图片,并添加上我们的方法

    <body>
    <img src="./download.jpg">
    <button onclick="addCanvas()">生成Canvas</button>
    <button onclick="generateImg()">生成图片</button>
    </body>


    接下来写addCanvas方法

    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image(); //1.准备赋值复制一份图片
    img.src = './download.jpg';
    img.onload = function() { //2.待图片加载完成
    let width = this.width
    let height = this.height

    let canvas = document.createElement('canvas') //3.创建画布
    let ctx = canvas.getContext("2d"); //4.获得该画布的内容
    canvas.setAttribute('width', width) //5.为了统一,设置画布的宽高为图片的宽高
    canvas.setAttribute('height', height)

    ctx.drawImage(this, 0, 0, width, height); //5.在画布上绘制该图片

    document.body.insertBefore(canvas, bt) //5.把canvas插入到按钮前面

    }
    }



    嗯,我们已经成功走出了成功的一小步,接下来是干什么呢?…嗯,我们需要利用原生的onmouseup和onmousedown事件,代表我们按下鼠标这个过程,那么这两个事件添加到哪呢?

    没错,既然我们要在canvas上进行马赛克操作,那我们必然要给canvas元素添加这两个事件

    考虑到我们创建canvas的过程复杂了一点,我们做一个模块封装吧!

    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image();
    img.src = './download.jpg'; //这里放自己的图片
    img.onload = function() {
    let width = this.width
    let height = this.height

    let {
    canvas,
    ctx
    } = createCanvasAndCtx(width, height) //对象解构接收canvas和ctx

    ctx.drawImage(this, 0, 0, width, height);

    document.body.insertBefore(canvas, bt)

    }
    }

    function createCanvasAndCtx(width, height) {
    let canvas = document.createElement('canvas')
    canvas.setAttribute('width', width)
    canvas.setAttribute('height', height)
    canvas.setAttribute('onmouseout', 'end()') //修补鼠标不在canvas上离开的补丁
    canvas.setAttribute('onmousedown', 'start()') //添加鼠标按下
    canvas.setAttribute('onmouseup', 'end()') //添加鼠标弹起
    let ctx = canvas.getContext("2d");
    return {
    canvas,
    ctx
    }
    }

    function start() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = () => {
    console.log('你按下了并移动了鼠标')
    }
    }

    function end() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = null
    }


    嗯,目前来看,我们的代码依然如我们所愿的正常工作

    接下来的挑战更加严峻,我们需要去获取像素和处理像素,让我们再重写start()函数

    function start() {
    let img = document.querySelector('img')
    let canvas = document.querySelector('canvas')
    let ctx = canvas.getContext("2d");
    imgData = ctx.getImageData(0, 0, img.clientWidth, img.clientHeight);
    canvas.onmousemove = (e) => {
    let w = imgData.width; //1.获取图片宽高
    let h = imgData.height;

    //马赛克的程度,数字越大越模糊
    let num = 10;

    //获取鼠标当前所在的像素RGBA
    let color = getXY(imgData, e.offsetX, e.offsetY);

    for (let k = 0; k < num; k++) {
    for (let l = 0; l < num; l++) {
    //设置imgData上坐标为(e.offsetX + l, e.offsetY + k)的的颜色
    setXY(imgData, e.offsetX + l, e.offsetY + k, color);
    }
    }
    //更新canvas数据
    ctx.putImageData(imgData, 0, 0);
    }
    }

    //这里为你提供了setXY和getXY两个函数,如果你有兴趣,可以去研究获取的原理
    function setXY(obj, x, y, color) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    obj.data[4 * (y * w + x)] = color[0];
    obj.data[4 * (y * w + x) + 1] = color[1];
    obj.data[4 * (y * w + x) + 2] = color[2];
    obj.data[4 * (y * w + x) + 3] = color[3];
    }

    function getXY(obj, x, y) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    var color = [];
    color[0] = obj.data[4 * (y * w + x)];
    color[1] = obj.data[4 * (y * w + x) + 1];
    color[2] = obj.data[4 * (y * w + x) + 2];
    color[3] = obj.data[4 * (y * w + x) + 3];
    return color;
    }

    嗯,我们离成功不远拉,最后一步就是生成图片

    好在canavs给我们提供了直接的方法,可以直接将画布导出为Base64编码的图片:

    function generateImg() {
    let canvas = document.querySelector('canvas')
    var newImg = new Image();
    newImg.src = canvas.toDataURL("image/png");
    document.body.insertBefore(newImg, canvas)
    }

    是不是无比轻松呢~,来看看你手写的代码是否和下面一样叭:

    完整代码

    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    </head>

    <body>

    <body>
    <img src="./download.jpg">
    <button onclick="addCanvas()">生成Canvas</button>
    <button onclick="generateImg()">生成图片</button>
    </body>
    <script>
    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image();
    img.src = './download.jpg'; //这里放自己的图片
    img.onload = function() {
    let width = this.width
    let height = this.height

    let {
    canvas,
    ctx
    } = createCanvasAndCtx(width, height)

    ctx.drawImage(this, 0, 0, width, height);

    document.body.insertBefore(canvas, bt)

    }
    }

    function createCanvasAndCtx(width, height) {
    let canvas = document.createElement('canvas')
    canvas.setAttribute('width', width)
    canvas.setAttribute('height', height)
    canvas.setAttribute('onmouseout', 'end()')
    canvas.setAttribute('onmousedown', 'start()')
    canvas.setAttribute('onmouseup', 'end()')
    let ctx = canvas.getContext("2d");
    return {
    canvas,
    ctx
    }
    }

    function start() {
    let img = document.querySelector('img')
    let canvas = document.querySelector('canvas')
    let ctx = canvas.getContext("2d");
    imgData = ctx.getImageData(0, 0, img.clientWidth, img.clientHeight);
    canvas.onmousemove = (e) => {
    let w = imgData.width; //1.获取图片宽高
    let h = imgData.height;

    //马赛克的程度,数字越大越模糊
    let num = 10;

    //获取鼠标当前所在的像素RGBA
    let color = getXY(imgData, e.offsetX, e.offsetY);

    for (let k = 0; k < num; k++) {
    for (let l = 0; l < num; l++) {
    //设置imgData上坐标为(e.offsetX + l, e.offsetY + k)的的颜色
    setXY(imgData, e.offsetX + l, e.offsetY + k, color);
    }
    }
    //更新canvas数据
    ctx.putImageData(imgData, 0, 0);
    }
    }

    function generateImg() {
    let canvas = document.querySelector('canvas')
    var newImg = new Image();
    newImg.src = canvas.toDataURL("image/png");
    document.body.insertBefore(newImg, canvas)
    }

    function setXY(obj, x, y, color) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    obj.data[4 * (y * w + x)] = color[0];
    obj.data[4 * (y * w + x) + 1] = color[1];
    obj.data[4 * (y * w + x) + 2] = color[2];
    obj.data[4 * (y * w + x) + 3] = color[3];
    }

    function getXY(obj, x, y) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    var color = [];
    color[0] = obj.data[4 * (y * w + x)];
    color[1] = obj.data[4 * (y * w + x) + 1];
    color[2] = obj.data[4 * (y * w + x) + 2];
    color[3] = obj.data[4 * (y * w + x) + 3];
    return color;
    }

    function end() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = null
    }
    </script>
    </body>

    </html>

    本文链接:https://blog.csdn.net/JKR10000/article/details/116803023

    收起阅读 »

    微信H5网页跳转小程序,这一篇就够了!

    鉴于微信 开放标签说明文档 写的不是很清楚,大多数开发者看了以后表示:我从哪里来?要到哪里去?所以鄙人记录下这篇文章,以便帮助到一些人。静态网页跳转小程序废话不多说,上才艺!<html><head> <meta charse...
    继续阅读 »

    鉴于微信 开放标签说明文档 写的不是很清楚,大多数开发者看了以后表示:我从哪里来?要到哪里去?

    所以鄙人记录下这篇文章,以便帮助到一些人。

    静态网页跳转小程序

    废话不多说,上才艺!

    <html>
    <head>
    <meta charset="utf-8">
    <meta name = "viewport" content = "width = device-width, initial-scale = 1.0, maximum-scale = 1.0, user-scalable = 0" />
    <title>小程序跳转测试</title>
    </head>
    <body style="text-aligin:center;">
    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html"> <!-- 这里填写跳转对于小程序的页面 注意这里的 .html -->
    <template>
    <style>.btn { padding: 12px width:200px;height:50px;}</style>
    <button class="btn">打开小程序</button>
    </template>
    </wx-open-launch-weapp>

    <script src="/js/jquery-1.12.4.js"></script>
    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> <!-- 至少必须是1.6版本 -->

    <script>

    $(function () {

    //=== 这里仅仅是获取 config 的参数以及签名=== start
    var url = location.href;
    var functions = "updateAppMessageShareData";
    $.get("https://xxx.com/wechat/jssdk/config", {"functions":functions}, function(response){
    if(response.status == 0) {
    var info = response.data;
    wx.config({
    debug: false,
    appId: info.appId,
    timestamp: info.timestamp,
    nonceStr: info.nonceStr,
    signature: info.signature,
    jsApiList: info.jsApiList,
    openTagList: ['wx-open-launch-weapp']//这里直接添加,什么都不用管
    });
    }
    });
    //=== 获取 config 的参数以及签名=== end

    var btn = document.getElementById('launch-btn');
    btn.addEventListener('launch', function (e) {
    console.log('success');
    });
    btn.addEventListener('error', function (e) {
    console.log('fail', e.detail);
    });
    });
    </script>
    </body>
    </html>

    开放对象:

    1、已认证的服务号,服务号绑定“JS接口安全域名”下的网页可使用此标签跳转任意合法合规的小程序。

    2、已认证的非个人主体的小程序,使用小程序云开发的静态网站托管绑定的域名下的网页,可以使用此标签跳转任意合法合规的小程序。

    客户端要求

    微信版本要求为:7.0.12及以上。 系统版本要求为:iOS 10.3及以上、Android 5.0及以上。

    注意:微信开发者工具暂时不支持!所以建议直接使用手机访问进行测试。

    其他说明

    这个功能其实很简单,并没有想象中那么复杂。 实质是在你能够做到自定义分享到朋友圈或朋友的基础上,config多了

    openTagList: ['wx-open-launch-weapp']

    再者需要注意的是,path的页面url 必须带有 .html 带参数的话则参数跟在html的后面。

    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html">

    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html?id=123">

    VUE项目H5跳转

    1、先请求接口配置微信需要的一些参数

    // 需要先请求后端接口 
    let url = window.location.href.split("#")[0];
    let shareConfig = await shareViewAPI.getWechatConfig({url});
    let _this = this;
    // 将接口返回的信息配置
    wx.config({
    debug: false,
    appId: _this.app_id, // 必填,公众号的唯一标识
    timestamp: shareConfig.timestamp, // 必填,生成签名的时间戳
    nonceStr: shareConfig.nonceStr, // 必填,生成签名的随机串
    signature: shareConfig.signature, // 必填,签名
    jsApiList: ["onMenuShareAppMessage"], // 必填,如果只是为了跳转小程序,随便填个值都行
    openTagList: ["wx-open-launch-weapp"] // 跳转小程序时必填
    });

    配置的方法需要放到created、mounted或者beforeRouteEnter里

    2、在页面中添加wx-open-launch-weapp标签

    <!-- 关于username 与 path的值 参考官方文档  -->
    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_***"
    path="/pages/index/index.html"
    @error="handleErrorFn"
    @launch="handleLaunchFn"
    >
    <!-- vue中需要使用script标签代替template插槽 html中使用template -->
    <script type="text/wxtag-template">
    <p class="store-tool_tip">点击进入选基工具</p>
    </script>
    </wx-open-launch-weapp>
    methods: {
    handleLaunchFn(e) {
    console.log("success", e);
    },
    handleErrorFn(e) {
    console.log("fail", e.detail);
    }
    }

    3、好啦

    备注:
    使用该标签的时候可能会报错,在main.js文件中添加上该行代码即可

    // 忽略打开微信小程序的组件
    Vue.config.ignoredElements = ['wx-open-launch-weapp']


    收起阅读 »

    独乐乐不如众乐乐,你的项目还在纠结用日志打印log么?Android开发okhttp3便捷拦截监听

    SimpleInterceptorSimpleInterceptor 是Android OkHttp客户端的的拦截接口工具,为的是方便测试或开发,快速查找问题。 环境要求 Android 4.1+OkHttp 3.x or 4.xandroidx git地址...
    继续阅读 »

    SimpleInterceptor

    SimpleInterceptor 是Android OkHttp客户端的的拦截接口工具,为的是方便测试或开发,快速查找问题。


    在这里插入图片描述
    在这里插入图片描述

    环境要求


    1. Android 4.1+
    2. OkHttp 3.x or 4.x
    3. androidx

    git地址

    github地址 :https://github.com/smartbackme/SimpleInterceptor
    国内访问地址: https://gitee.com/dileber/SimpleInterceptor
    如果觉得不错 github 给个星
    警告



    使用此拦截器时生成和存储的数据可能包含敏感信息,如授权或Cookie头,以及请求和响应主体的内容。
    由此,其只能用于调试过程,不可发布到线上


    配置
    project : build.gradle

    buildscript {
    repositories {
    maven { url 'https://www.jitpack.io' }
    }

    版本于okhttp关联:
    如果app 集成的是okhttp3 3.+版本那么请选用 3.0版本代码
    如果app 集成的是okhttp3 4.+版本那么请选用 4.0版本代码

    okhttp3 3.+
    dependencies {

    debugImplementation 'com.github.smartbackme.SimpleInterceptor:simpleinterceptor-debug:3.0'
    releaseImplementation 'com.github.smartbackme.SimpleInterceptor:simpleinterceptor-release:3.0'
    }
    or

    okhttp3 4.+
    dependencies {

    debugImplementation 'com.github.smartbackme.SimpleInterceptor:simpleinterceptor-debug:4.0'
    releaseImplementation 'com.github.smartbackme.SimpleInterceptor:simpleinterceptor-release:4.0'
    }

    使用:



    OkHttpClient.Builder()
    .addInterceptor(SimpleInterceptor(context))
    .build()

    下载地址:dileber-SimpleInterceptor-master.zip


    收起阅读 »

    ORCharts:环形图、饼状图、扇形图

    本文为ORCharts:环形图、饼状图、扇形图 部分, 做详细说明相关连接GitHubORChartsORCharts:曲线图、折线图效果预览安装pod 'ORCharts/Ring'使用Use Interface Builder1、 在XIB或Storybo...
    继续阅读 »

    本文为ORCharts:环形图、饼状图、扇形图 部分, 做详细说明

    相关连接

    GitHub
    ORCharts
    ORCharts:曲线图、折线图

    效果预览



    安装

    pod 'ORCharts/Ring'

    使用

    Use Interface Builder

    1、 在XIB或Storyboard拖拽一个 UIView 到你需要展示的位置
    2、 修改Class为 ORRingChartView
    3、 设置 dataSource

    代码

    @property (nonatomic, strong) ORRingChartView *ringChartView;
    _ringChartView = [[ORRingChartView alloc] initWithFrame:CGRectMake(0, 0, 375, 375)];
    _ringChartView.dataSource = self;
    [self.view addSubview:_ringChartView];

    在数据改变或是配置改变的时候reloadData

    [_ringChartView reloadData];

    style

    ORRingChartStyleRing:环形图(默认)
    ORRingChartStylePie:饼状图
    ORRingChartStyleFan:扇形图

    _ringChart.style = ORRingChartStylePie;

    代理相关

    ORRingChartViewDatasource

    1、@required
    必须实现方法,数据个数以及对应数据,类似tableView

    - (NSInteger)numberOfRingsOfChartView:(ORRingChartView *)chartView;
    - (CGFloat)chartView:(ORRingChartView *)chartView valueAtRingIndex:(NSInteger)index;

    2、@optional,对应Index数据视图的渐变色,默认为随机色

    - (NSArray <UIColor *> *)chartView:(ORRingChartView *)chartView graidentColorsAtRingIndex:(NSInteger)index;

    对应Index数据视图的线条颜色,默认为白色

    - (UIColor *)chartView:(ORRingChartView *)chartView lineColorForRingAtRingIndex:(NSInteger)index;

    对应Index数据的信息线条颜色,默认为graidentColors的第一个颜色

    - (UIColor *)chartView:(ORRingChartView *)chartView lineColorForInfoLineAtRingIndex:(NSInteger)index;

    中心视图,默认nil,返回的时候需要设置视图大小

    - (UIView *)viewForRingCenterOfChartView:(ORRingChartView *)chartView;

    对应Index数据的顶部信息视图,默认nil,返回的时候需要设置视图大小

    - (UIView *)chartView:(ORRingChartView *)chartView viewForTopInfoAtRingIndex:(NSInteger)index;

    对应Index数据的底部信息视图,默认nil,返回的时候需要设置视图大小

    - (UIView *)chartView:(ORRingChartView *)chartView viewForBottomInfoAtRingIndex:(NSInteger)index;

    配置相关

    以下是配置中部分属性图解


    配置修改方式

    _ringChart.config.neatInfoLine = YES;
    _ringChart.config.ringLineWidth = 2;
    _ringChart.config.animateDuration = 1;
    [_ringChart reloadData];

    以下为配置具体说明

    1、整体
    clockwise:图表绘制方向是否为顺时针,默认YES
    animateDuration:动画时长 ,设置0,则没有动画,默认1
    neatInfoLine:infoLine 两边对齐、等宽,默认NO
    startAngle:图表绘制起始角度,默认 M_PI * 3 / 2
    ringLineWidth:ringLine宽度,默认2
    infoLineWidth:infoLine宽度,默认2

    2、偏移、边距配置
    minInfoInset:infoView的内容偏移,值越大,infoView越宽,默认0
    infoLineMargin:infoLine 至 周边 的距离,默认10
    infoLineInMargin:infoLine 至 环形图的距离,默认 10
    infoLineBreakMargin:infoLine折线距离,默认 15
    infoViewMargin:infoLine 至 infoView的距离,默认5

    3、其他
    pointWidth:infoline 末尾圆点宽度,默认 5
    ringWidth:环形图,圆环宽度, 如果设置了 centerView 则无效,默认60

    文末

    GitHub传送门
    有任何问题,可在本文下方评论,或是GitHub上提出issue
    如有可取之处, 记得 star

    转自:https://www.jianshu.com/p/317a79890984

    收起阅读 »

    Swift手势密码库,用这一个就够了!

    一个轻量级、面对协议编程、高度自定义的 图形解锁/手势解锁 / 手势密码 / 图案密码 / 九宫格密码相比于其他同类三方库有哪些优势:1、完全面对协议编程,支持高度自定义网格视图和连接线视图,轻松实现各类不同需求;2、默认支持多种配置效果,支持大部分主流效果,...
    继续阅读 »

    一个轻量级、面对协议编程、高度自定义的 图形解锁/手势解锁 / 手势密码 / 图案密码 / 九宫格密码

    相比于其他同类三方库有哪些优势:

    1、完全面对协议编程,支持高度自定义网格视图和连接线视图,轻松实现各类不同需求;
    2、默认支持多种配置效果,支持大部分主流效果,引入就可以搞定需求;
    3、源码采用Swift5编写,通过泛型、枚举、函数式编程优化代码,具有更高的学习价值;
    4、后期会持续迭代,不断添加主流效果;

    Github地址

    JXPatternLock

    效果预览

    1. 箭头


    2. 中间点自动链接


    3. 小灰点


    4. 小白点


    5. 荧光蓝


    6. fill白色


    7. 阴影


    8. 图片


    9. 旋转(鸡你太美)


    10. 破折线


    11. 图片连接线(箭头)


    12. 图片连接线(小鱼儿)


    13. 设置密码


    14. 修改密码


    15. 验证密码


    使用

    初始化PatternLockViewConfig

    方式一:使用LockConfig

    LockConfig是默认提供的类,实现了PatternLockViewConfig协议。可以直接通过LockConfig的属性进行自定义。

    let config = LockConfig()
    config.gridSize = CGSize(width: 70, height: 70)
    config.matrix = Matrix(row: 3, column: 3)
    config.errorDisplayDuration = 1

    方式二:新建实现PatternLockViewConfig协议的类

    该方式可以将所有配置细节聚集到自定义类的内部,外部只需要初始化自定义类即可。详情请参考demo里面的ArrowConfig类。这样有个好处就是,多个地方都需要用到同样配置的时候,只需要初始化相同的类,而不用像使用LockConfig那样,复制属性配置代码。

    struct ArrowConfig: PatternLockViewConfig {
    var matrix: Matrix = Matrix(row: 3, column: 3)
    var gridSize: CGSize = CGSize(width: 70, height: 70)
    var connectLine: ConnectLine?
    var autoMediumGridsConnect: Bool = false
    //其他属性配置!只是示例,就不显示所有配置项,影响文档长度
    }

    配置GridView

    config.initGridClosure = {(matrix) -> PatternLockGrid in
    let gridView = GridView()
    let outerStrokeLineWidthStatus = GridPropertyStatus<CGFloat>.init(normal: 1, connect: 2, error: 2)
    let outerStrokeColorStatus = GridPropertyStatus<UIColor>(normal: tintColor, connect: tintColor, error: .red)
    gridView.outerRoundConfig = RoundConfig(radius: 33, lineWidthStatus: outerStrokeLineWidthStatus, lineColorStatus: outerStrokeColorStatus, fillColorStatus: nil)
    let innerFillColorStatus = GridPropertyStatus<UIColor>(normal: nil, connect: tintColor, error: .red)
    gridView.innerRoundConfig = RoundConfig(radius: 10, lineWidthStatus: nil, lineColorStatus: nil, fillColorStatus: innerFillColorStatus)
    return gridView
    }

    配置ConnectLine

    let lineView = ConnectLineView()
    lineView.lineColorStatus = .init(normal: tintColor, error: .red)
    lineView.triangleColorStatus = .init(normal: tintColor, error: .red)
    lineView.isTriangleHidden = false
    lineView.lineWidth = 3
    config.connectLine = lineView

    初始化PatternLockView

    lockView = PatternLockView(config: config)
    lockView.delegate = self
    view.addSubview(lockView)

    结构


    完全遵从面对协议开发。
    PatternLockView依赖于配置协议PatternLockViewConfig。
    配置协议配置网格协议PatternLockGrid和连接线协议ConnectLine。

    转自:https://www.jianshu.com/p/f8aa805057fc

    收起阅读 »

    这是一个围绕SQLite的Objective-C封装

    FMDB这是一个围绕SQLite的Objective-C的封装安装cocoapodsFMDB可以使用CocoaPods安装。如果尚未执行此操作,则可能需要初始化项目,以使其Podfile为您生成模板:$ pod init然后,编辑Podfile,并添加FMDB...
    继续阅读 »

    FMDB

    这是一个围绕SQLite的Objective-C的封装

    安装

    cocoapods

    FMDB可以使用CocoaPods安装

    如果尚未执行此操作,则可能需要初始化项目,以使其Podfile为您生成模板:

    $ pod init

    然后,编辑Podfile,并添加FMDB


    # Uncomment the next line to define a global platform for your project
    # platform :ios, '9.0'

    target 'MyApp' do
    # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
    use_frameworks!

    # Pods for MyApp2

    pod 'FMDB'
    # pod 'FMDB/FTS' # FMDB with FTS
    # pod 'FMDB/standalone' # FMDB with latest SQLite amalgamation source
    # pod 'FMDB/standalone/FTS' # FMDB with latest SQLite amalgamation source and FTS
    # pod 'FMDB/SQLCipher' # FMDB with SQLCipher
    end

    $ pod install

    然后打开.xcworkspace而不是.xcodeproj


    Carthage 安装

    $ echo ' github "ccgus/fmdb" ' > ./Cartfile
    $ carthage update

    您可以在Cocoa项目中使用任何一种样式。FMDB会在编译时确定您正在使用哪个,并做正确的事。

    自定义功能

    过去,编写自定义函数时,通常必须包含自己的@autoreleasepool块,以避免在编写通过大表扫描的函数时出现问题。现在,FMDB将自动将其包装在自动释放池中,因此您不必这样做。

    另外,过去,在检索传递给函数的值时,您必须下拉至SQLite C API并包含您自己的sqlite3_value_XXX调用。现在有FMDatabase方法valueIntvalueString等等,这样你就可以留内斯威夫特和/或Objective-C中,而无需自行调用C函数。同样,指定的返回值时,你不再需要调用sqlite3_result_XXXC API,而是你可以使用FMDatabase方法resultIntresultString等有一个新enumvalueTypeSqliteValueType,可用于检查传递给自定义函数参数的类型。

    queue.inTransaction { db, rollback in
    do {
    guard let db == db else {
    // handle error here
    return
    }

    try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [1])
    try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [2])
    } catch {
    rollback?.pointee = true
    }
    }


    然后,您可以在SQL中使用该函数(在这种情况下,匹配“ Jose”和“José”):

    SELECT * FROM employees WHERE RemoveDiacritics(first_name) LIKE 'jose'

    API变更

    除了makeFunctionNamed上面提到的以外,还有一些其他的API更改。具体来说,

    • 为了与API的其余部分保持一致,方法objectForColumnNameUTF8StringForColumnName已重命名为objectForColumnUTF8StringForColumn

    • 注意,如果将无效的列名/索引传递给它,则objectForColumn(和相关的下标运算符)现在返回nil它曾经返回NSNull

    • 为了避免与混乱FMDatabaseQueue的方法inTransaction,其中执行交易,该FMDatabase方法以确定是否是在交易与否,inTransaction已被替换为只读属性,isInTransaction

    • 几种功能都被转换为性能,即,databasePathmaxBusyRetryTimeIntervalshouldCacheStatementssqliteHandlehasOpenResultSetslastInsertRowIdchangesgoodConnectioncolumnCountresultDictionaryapplicationIDapplicationIDStringuserVersioncountOfCheckedInDatabasescountOfCheckedOutDatabases,和countOfOpenDatabases对于Objective-C用户而言,这几乎没有实质性影响,但是对于Swift用户而言,它带来了更为自然的界面。注意:对于Objective-C开发人员,以前版本的FMDB公开了许多ivars(但是我们希望您无论如何都不要直接使用它们!),但是这些实现的详细信息不再公开。

    URL方法

    为了适应Apple从路径到URL的转变,现在存在NSURL各种init方法的再现形式,以前只接受路径。


    用法

    FMDB中有三个主要类:

    1. FMDatabase-表示单个SQLite数据库。用于执行SQL语句。
    2. FMResultSet-表示在上执行查询的结果FMDatabase
    3. FMDatabaseQueue-如果要在多个线程上执行查询和更新,则需要使用此类。在下面的“线程安全”部分中对此进行了描述。

    数据库创建

    使用FMDatabase指向SQLite数据库文件的路径创建。此路径可以是以下三个路径之一:

    1. 文件系统路径。该文件不必在磁盘上存在。如果它不存在,则会为您创建。
    2. 空字符串(@"")。在临时位置创建一个空数据库。FMDatabase关闭连接后,将删除该数据库
    3. NULL创建一个内存数据库。FMDatabase关闭连接后,该数据库将被销毁

    (有关临时和内存数据库的更多信息,请阅读有关此主题的sqlite文档:https : //www.sqlite.org/inmemorydb.html

    NSString * path = [ NSTemporaryDirectory()stringByAppendingPathComponent:@“ tmp.db  ];
    FMDatabase * db = [FMDatabase
    databaseWithPath: path];

    与数据库进行交互之前,必须先将其打开。如果没有足够的资源或权限打开和/或创建数据库,则打开失败。

    if(![db open ]){
    db = nil
    }

    执行更新

    任何不是该SELECT语句的SQL语句都可以视为更新。这包括CREATEUPDATEINSERTALTERCOMMITBEGINDETACHDELETEDROPENDEXPLAINVACUUM,和REPLACE语句(以及许多其他)。基本上,如果您的SQL语句不是以开头SELECT,则它是一条更新语句。

    执行更新将返回单个值a BOOL返回值YES表示更新已成功执行,返回值NO表示遇到某些错误。您可以调用-lastErrorMessage-lastErrorCode方法来检索更多信息。

    执行查询

    一个SELECT语句是一个查询和通过的一个执行-executeQuery...方法。

    FMResultSet如果成功,则执行查询将返回一个对象,如果nil失败,则返回一个对象您应该使用-lastErrorMessage-lastErrorCode方法来确定查询失败的原因。

    为了遍历查询结果,可以使用while()循环。您还需要从一条记录“步入”到另一条记录。使用FMDB,最简单的方法是这样的:

    FMResultSet *s = [db executeQuery:@"SELECT * FROM myTable"];
    while ([s next]) {
    //retrieve values for each record
    }

    -[FMResultSet next]在尝试访问查询中返回的值之前,必须始终调用它,即使您只希望得到一个值:

    FMResultSet *s = [db executeQuery:@"SELECT COUNT(*) FROM myTable"];
    if ([s next]) {
    int totalCount = [s intForColumnIndex:0];
    }
    [s close]; // Call the -close method on the FMResultSet if you cannot confirm whether the result set is exhausted.

    FMResultSet 有许多方法可以检索适当格式的数据:

    • intForColumn:
    • longForColumn:
    • longLongIntForColumn:
    • boolForColumn:
    • doubleForColumn:
    • stringForColumn:
    • dateForColumn:
    • dataForColumn:
    • dataNoCopyForColumn:
    • UTF8StringForColumn:
    • objectForColumn:

    这些方法中的每一个都还具有一个{type}ForColumnIndex:变体,用于根据结果中列的位置而不是列名来检索数据。

    通常情况下,有没有必要-closeFMResultSet自己,因为当任一结果集耗尽出现这种情况。但是,如果仅提取单个请求或其他没有耗尽结果集的请求,则需要在上调用-close方法FMResultSet

    在数据库上执行完查询和更新后,应-close建立FMDatabase连接,以便SQLite放弃其在操作过程中获取的任何资源。

    [db close];

    更多问题与demo下载:https://github.com/ccgus/fmdb

    源码下载:fmdb-master.zip







    收起阅读 »

    iOS 方便操作 CoreData 的快捷方式

    MagicalRecordMagicalRecord的灵感来自Ruby on Rails的Active Record获取。该代码的目标是:清理我的核心数据相关代码允许清晰,简单的单行读取当需要优化请求时,仍允许修改NSFetchRequest项目状况该项目的活...
    继续阅读 »


    MagicalRecord

    MagicalRecord的灵感来自Ruby on Rails的Active Record获取。该代码的目标是:

    • 清理我的核心数据相关代码
    • 允许清晰,简单的单行读取
    • 当需要优化请求时,仍允许修改NSFetchRequest

    项目状况

    该项目的活动已停止,已由Core Data本身取代。我们提供的最新版本是:

    • MagicalRecord 2.4.0是一个稳定的版本,可从标签'2.4.0'或中获得pod 'MagicalRecord', :git => 'https://github.com/magicalpanda/MagicalRecord'
    • 实验版本MagicalRecord 3.0.0,有两种版本,一种是branch release/3.0,另一种是branch maintenance/3.0

    使用CocoaPods

    在您的项目中集成MagicalRecord的最简单方法之一是使用CocoaPods

    1. 将以下行添加到您的Podfile

      一种。清楚的

       'MagicalRecord'   :GIT中 =>  'https://github.com/magicalpanda/MagicalRecord'

      b。使用CocoaLumberjack作为记录器

       'MagicalRecord / CocoaLumberjack'   :GIT中 =>  'https://github.com/magicalpanda/MagicalRecord'
    2. 在您的项目目录中,运行 pod update

    3. 现在,您应该能够添加#import 到目标的任何源文件中,并开始使用MagicalRecord!

    使用Xcode

    1. 作为Git子模块将MagicalRecord添加到您的项目中:

      $ cd MyXcodeProjectFolder
      $ git submodule add https://github.com/magicalpanda/MagicalRecord.git Vendor/MagicalRecord
      $ git commit -m "Add MagicalRecord submodule"
    2. 拖动Vendor/MagicalRecord/MagicalRecord.xcproj到您现有的Xcode项目

    3. 导航到项目的设置,然后选择要将MagicalRecord添加到的目标

    4. 导航到“构建阶段”,然后展开“使用库链接二进制文件”部分

    5. 单击+,然后找到适合您目标平台的MagicalRecord框架版本

    6. 现在,您应该能够添加#import 到目标的任何源文件中,并开始使用MagicalRecord!

    注意请注意,如果将Xcode的链接框架自动设置为“否”,则可能需要将CoreData.framework添加到iOS上的项目中,因为UIKit默认情况下不包括Core Data。在OS X上,Cocoa包含核心数据。

    类别方法

    //目标C 
    进口 < MagicalRecord / MagicalRecord.h >
    进口 < MagicalRecord / MagicalRecord + ShorthandMethods.h >
    进口 < MagicalRecord / MagicalRecordShorthandMethodAliases.h >

    如果您使用的是Swift,则需要将这些导入添加到目标的Objective-C桥接标头中。

    一旦包含了标题,就应该设置/使用MagicalRecord之前调用+[MagicalRecord enableShorthandMethods]class方法

    // Objective-C- 
    void)theMethodWhereYouSetupMagicalRecord
    {

    [MagicalRecord
    enableShorthandMethods ];

    //按照常规设置MagicalRecord
    }
    //斯威夫特
    func theMethodWhereYouSetupMagicalRecord(){
    MagicalRecord
    enableShorthandMethods()

    //照常设置MagicalRecord

    源码下载:MagicalRecord-master.zip


    收起阅读 »

    在vue项目中使用骨架屏

    vue
    现在的应用开发,基本上都是前后端分离的,前端主流框架有SPA、MPA等,那么解决页面渲染、白屏时间成为首要关注的点webpack可以按需加载,减小首屏需要加载代码的体积;使用CDN技术、静态代码等缓存技术,可以减小加载渲染的时长问题:但是首页依然存在加载、渲染...
    继续阅读 »

    现在的应用开发,基本上都是前后端分离的,前端主流框架有SPA、MPA等,那么解决页面渲染、白屏时间成为首要关注的点

    webpack可以按需加载,减小首屏需要加载代码的体积;

    使用CDN技术、静态代码等缓存技术,可以减小加载渲染的时长

    问题:但是首页依然存在加载、渲染等待时长的问题。那么如何从视觉效果上减小首屏白屏的时间呢?

    骨架屏:举个例子:其实就是在模版文件中id=app容器下面写想要展示的效果,在new Vue(option)之后,该id下的内容就被替换了( 这时候,可能Vue编译生成的内容还没有挂载。因为new Vue的时候会进行一系列的初始化,这也需要耗费时间的)。这样就可以从视觉上减小白屏的时间

    骨架屏的实现方式

    1、直接在模版文件id=app容器下面,写进想要展示的效果html

    2、直接在模板文件id=app容器下面,用图片展示

    3、使用vue ssr提供的webpack插件

    4、自动生成并且自动插入静态骨架屏

    方式1和方式2存在的缺陷:针对不同入口,展示的效果都一样,导致不能灵活的针对不同的入口,展示不同的样式

    方式3可以针对不同的入口展示不同的效果。(实质也是先通过ssr生成一个json文件,然后将json文件内容注入到模板文件的id=app容器下)

    方案一、直接在模版文件id=app容器下面,写进想要展示的效果html

    在根目录的模版文件内写进内容,如红色圈出来的地方


    在浏览器打开项目

    在调用new Vue之前的展示效果(只是做了个简单效果,不喜勿喷):


    可以看到elements中id=app的容器下内容,就是我们写进的骨架屏效果内容


    在看下调了new Vue之后的效果,id=app容器下的内容被vue编译生成的内容替换了



    方案二、直接在模板文件id=app容器下面,用图片展示(这个就不做展示了)

    方案三、使用vue ssr提供的webpack插件:即用.vue文件完成骨架屏

    在方案一的基础上,将骨架屏的代码抽离出来,不在模版文件里面书写代码,而是在vue文件里面书写效果代码,这样便于维护

    1、在根目录下建一个skeleton文件夹,在该目录下创建文件App.vue文件(根组件,类似Vue项目的App.vue)、home.skeleton.vue(首页骨架屏展示效果的代码,类似Vue项目写的路由页面)、skeleton-entry.js(入口文件类似Vue项目的入口文件)、plugin/server-plugin.js(vue-server-renderer包提供了server-plugin插件,从里面将代码拷贝出来)

    home.skeleton.vue(首页骨架屏展示效果的代码)

    <template>
    <div class="skeleton-home">
    <div>加载中...</div>
    </div>
    </template>

    <style>
    .skeleton-home {
    width: 100vw;
    height: 100vh;
    background-color: #eaeaea;
    }
    </style>

    App.vue(根组件)

    <template>
    <div id="app">
    <!-- 根组件 -->
    <home style="display:none" id="homeSkeleton"></home>
    </div>
    </template>
    <script>
    import home from './home.skeleton.vue'
    export default{
    components: {
    home
    }
    }
    </script>
    <style>
    #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    }
    *{
    padding: 0;
    margin: 0;
    }
    </style>

    skeleton-entry.js(入口文件)

    // 入口文件
    import Vue from 'vue'
    import App from './App.vue'
    let skeleton = new Vue({
    render(h) {
    return h(App)
    }
    })
    export default skeleton

    plugin/server-plugin.js(vue-server-renderer包提供了server-plugin插件)

    'use strict';

    /* */

    var isJS = function (file) { return /\.js(\?[^.]+)?$/.test(file); };

    var ref = require('chalk');
    var red = ref.red;
    var yellow = ref.yellow;

    var prefix = "[vue-server-renderer-webpack-plugin]";
    var warn = exports.warn = function (msg) { return console.error(red((prefix + " " + msg + "\n"))); };
    var tip = exports.tip = function (msg) { return console.log(yellow((prefix + " " + msg + "\n"))); };

    var validate = function (compiler) {
    if (compiler.options.target !== 'node') {
    warn('webpack config `target` should be "node".');
    }

    if (compiler.options.output && compiler.options.output.libraryTarget !== 'commonjs2') {
    warn('webpack config `output.libraryTarget` should be "commonjs2".');
    }

    if (!compiler.options.externals) {
    tip(
    'It is recommended to externalize dependencies in the server build for ' +
    'better build performance.'
    );
    }
    };

    var VueSSRServerPlugin = function VueSSRServerPlugin (options) {
    if ( options === void 0 ) options = {};

    this.options = Object.assign({
    filename: 'vue-ssr-server-bundle.json'
    }, options);
    };

    VueSSRServerPlugin.prototype.apply = function apply (compiler) {
    var this$1 = this;

    validate(compiler);

    compiler.plugin('emit', function (compilation, cb) {
    var stats = compilation.getStats().toJson();
    var entryName = Object.keys(stats.entrypoints)[0];
    var entryAssets = stats.entrypoints[entryName].assets.filter(isJS);

    if (entryAssets.length > 1) {
    throw new Error(
    "Server-side bundle should have one single entry file. " +
    "Avoid using CommonsChunkPlugin in the server config."
    )
    }

    var entry = entryAssets[0];
    if (!entry || typeof entry !== 'string') {
    throw new Error(
    ("Entry \"" + entryName + "\" not found. Did you specify the correct entry option?")
    )
    }

    var bundle = {
    entry: entry,
    files: {},
    maps: {}
    };

    stats.assets.forEach(function (asset) {
    if (asset.name.match(/\.js$/)) {
    bundle.files[asset.name] = compilation.assets[asset.name].source();
    } else if (asset.name.match(/\.js\.map$/)) {
    bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source());
    }
    // do not emit anything else for server
    delete compilation.assets[asset.name];
    });

    var json = JSON.stringify(bundle, null, 2);
    var filename = this$1.options.filename;

    compilation.assets[filename] = {
    source: function () { return json; },
    size: function () { return json.length; }
    };

    cb();
    });
    };

    module.exports = VueSSRServerPlugin;

    2、新建一个骨架屏构建配置文件:build/webpack.skeleton.conf.js,这个文件配合vue-server-renderer插件,将App.vue内容构建成单个json格式的文件

    'use strict'

    const path = require('path')
    const nodeExternals = require('webpack-node-externals')
    const VueSSRServerPlugin = require('../skeleton/plugin/server-plugin')

    module.exports = {
    // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
    // 并且还会在编译 Vue 组件时,
    // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
    target: 'node',

    // 对 bundle renderer 提供 source map 支持
    devtool: 'source-map',

    // 将 entry 指向应用程序的 server entry 文件
    entry: path.resolve(__dirname, '../skeleton/skeleton-entry.js'),

    output: {
    path: path.resolve(__dirname, '../skeleton'), // 生成的文件的目录
    publicPath: '/skeleton/',
    filename: '[name].js',
    libraryTarget: 'commonjs2' // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
    },

    module: {
    rules: [
    {
    test: /\.vue$/,
    loader: 'vue-loader',
    options: {
    compilerOptions: {
    preserveWhitespace: false
    }
    }
    },
    {
    test: /\.css$/,
    use: ['vue-style-loader', 'css-loader']
    }
    ]
    },

    performance: {
    hints: false
    },

    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,
    // 并生成较小的 bundle 文件。
    externals: nodeExternals({
    // 不要外置化 webpack 需要处理的依赖模块。
    // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    allowlist: /\.css$/
    }),

    // 这是将服务器的整个输出
    // 构建为单个 JSON 文件的插件。
    // 不配置filename,则默认文件名为 `vue-ssr-server-bundle.json`
    plugins: [
    new VueSSRServerPlugin({
    filename: 'skeleton.json'
    })
    ]
    }

    3、使用webpack-cli运行文件webpack.skeleton.conf.js,生成skeleton.json文件,放置在文件夹skeleton下

    在package.json文件里面书写运行命令:create-skeleton

    "scripts": {
    "create-skeleton": "webpack --progress --config build/webpack.skeleton.conf.js",
    "fill-skeleton": "node ./skeleton/skeleton.js"
    }

    在控制台上运行命令:

    npm run create-skeleton

    文件夹skeleton下就会多出skelleton.json文件


    4、将生成的skeleton.json内容注入到根目录下的index.html(模版文件)

    1)在文件夹skeleton下新建skeleton.js

    // 将生成的skeleton.json的内容填充到模板文件中
    const fs = require('fs')
    const { resolve } = require('path')
    const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

    // 读取skeleton.json,以skeleton/index.html为模版写入内容
    const renderer = createBundleRenderer(resolve(__dirname, '../skeleton/skeleton.json'), {
    template: fs.readFileSync(resolve(__dirname, '../skeleton/index.html'), 'utf-8')
    })
    // 把上一步模版完成的内容写入根目录下的模版文件'index.html'
    renderer.renderToString({}, (err, html) => {
    if (err) {
    return console.log(err)
    }
    console.log('render complete!')
    fs.writeFileSync('index.html', html, 'utf-8')
    })

    2)添加运行命令:fill-skeleton

    "fill-skeleton": "node ./skeleton/skeleton.js"

    3)在控制台上运行该命令,则skeleton.json文件内容被填充至根目录下的模板文件index.html了

    本文链接:https://blog.csdn.net/tangxiujiang/article/details/116832585

    收起阅读 »

    iOS超方便的多样式提示框

    MBProgressHUDMBProgressHUD是一个iOS嵌入式类,在后台线程中完成工作时显示带有指示符和/或标签的半透明HUD。HUD旨在代替未记录的,UIKit UIProgressHUD具有某些附加功能的专用显示器。要求MBProgres...
    继续阅读 »

    MBProgressHUD

    MBProgressHUD是一个iOS嵌入式类,在后台线程中完成工作时显示带有指示符和/或标签的半透明HUD。HUD旨在代替未记录的,UIKit UIProgressHUD具有某些附加功能的专用显示器

    要求

    MBProgressHUD适用于iOS 9.0+。它取决于以下Apple框架,大多数Xcode模板应已包含以下框架:

    • Foundation.framework
    • UIKit.framework
    • CoreGraphics.framework

    您将需要最新的开发人员工具才能进行构建MBProgressHUD较旧的Xcode版本可能会起作用,但不会明确维护兼容性。

    将MBProgressHUD添加到您的项目

    CocoaPods

    1. pod 'MBProgressHUD', '~> 1.2.0'
    2. 通过运行安装pod pod install
    3. 随需包含MBProgressHUD #import "MBProgressHUD.h"

    Carthage

    1. MBProgressHUD添加到您的Cartfile。例如,github "jdg/MBProgressHUD" ~> 1.2.0
    2. run carthage update
    3. 将MBProgressHUD添加到您的项目中。

    SwiftPM / Accio

    .package(url: "https://github.com/jdg/MBProgressHUD.git", .upToNextMajor(from: "1.2.0")),

    .target(name: "App", dependencies: ["MBProgressHUD"]),

    然后在Xcode 11+(SwiftPM)中打开您的项目或运行accio update(Accio)。

    源文件

    或者,您可以直接将MBProgressHUD.hMBProgressHUD.m源文件添加到您的项目中。

    1. 下载最新的代码版本,或将存储库作为git子模块添加到git跟踪的项目中。
    2. 打开Xcode中的项目,然后拖放MBProgressHUD.hMBProgressHUD.m到您的项目(使用“产品导航视图”)。当询问是否从项目外部提取代码存档时,请确保选择复制项目。
    3. 随需包含MBProgressHUD #import "MBProgressHUD.h"

    在运行长时间运行的任务时处理MBProgressHUD时需要遵循的主要原则是使主线程保持无工作状态,因此可以及时更新UI。因此,建议使用MBProgressHUD的方法是在主线程上进行设置,然后将要执行的任务旋转到新线程上。

    [MBProgressHUD showHUDAddedTo:self.view animated:YES];
    dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    // Do something...
    dispatch_async(dispatch_get_main_queue(), ^{
    [MBProgressHUD hideHUDForView:self.view animated:YES];
    });
    });

    MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
    hud.mode = MBProgressHUDModeAnnularDeterminate;
    hud.label.text = @"Loading";
    [self doSomethingInBackgroundWithProgressCallback:^(float progress) {
    hud.progress = progress;
    } completionCallback:^{
    [hud hideAnimated:YES];
    }];

    MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
    hud.mode = MBProgressHUDModeAnnularDeterminate;
    hud.label.text = @"Loading";
    NSProgress *progress = [self doSomethingInBackgroundCompletion:^{
    [hud hideAnimated:YES];
    }];
    hud.progressObject = progress;

    [MBProgressHUD showHUDAddedTo:self.view animated:YES];
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC);
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    // Do something...
    [MBProgressHUD hideHUDForView:self.view animated:YES];
    });

    您应该注意,在该块内完成之前,不会显示任何在上述块内发出的HUD更新。


    更多常见问题:https://github.com/jdg/MBProgressHUD
    源码下载:MBProgressHUD-master.zip



    收起阅读 »