开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

ArkUI 开发实例:快五一了,做一个回家的订票动画界面(上

发表于 2024-04-23

背景

转眼2024年已经快到五一假期了,最近有空一直在看一些鸿蒙原生开发的文档,ArkUI声明式开发可以快速构建复杂高效的界面和动画效果。

想着快订票了,不如我们就来做一个纯血鸿蒙NEXT开发订票页面吧。

订票动画.gif

掌握技能

本实例虽然只有一个小实例,但是你可以学到一些常用的ArkUI组件的知识,希望通过笔者的实例教程,带你更深刻的理解这些组件的功能,也带你进入原生鸿蒙开发的奇妙世界。

  • Column、Row和Stack布局:通过最常用的三种布局方式,快速学习理解AruUI页面构造;
  • DatePicker:日期选择器,实现快速选择指定范围日期的功能;
  • animation:属性动画,通过组件的某些通用属性变化,实现渐变过渡的动画效果;
  • transition:组件内转场动画,通过配置转场参数,在组件插入和删除时显示渐变过渡的动画效果。
  • Button:按钮组件,响应用户的点击等操作。

需求分析

本实例并非用户真实还原购票场景,只是通过简化版的产品流程设计,模拟用户购票流程,简易梳理产品架构图,如下图所示:
image.png

UI设计

通过卡片化的设计,把现实中车票效果与交互界面进行融合,设计订票组件。

image.png

通过“输入信息”与“查询结果”两个页面,实现用户订票流程。

image.png

本实例并未展示用户层输入“地址”和“交通工具”选择功能,主要目的是带领大家了解ArkUI布局和动画功能,故进行功能精简,按照UI导出切图如下。

image.png

开始实例

下面我们打开Deveco Studio开始本实例的教程,首先新建项目,选择OpenHarmony的Empty Ability创建实例。

image.png

本次实例是基于OpenHarmonySDK的api11(当前官方HarmonyOS仅开放到api9,故采用OpenHarmony演示),
软件名称就命名为“Ticketing”,选择Model为Stage(当前主推Model)

image.png

布局分析

层叠布局 (Stack)

页面最底层容器是一个层叠布局 (Stack),由一张背景图和二级内容视图构成。

那么我们了解下第一个知识点Stack:

层叠布局(Stack)通过区域内元素一层层堆叠实现布局。

子元素在容器中依次入栈,后元素覆盖前元素。

其方便设置相对位置信息,可以很便捷制作卡片效果。

image.png

如上图,Stack容器内元素红色矩形位于最上方堆叠在蓝色矩形和绿色矩形之上。

image.png

基于UI设计,背景图是固定图片,其宽高也是固定的,我们的Stack容器内是顶部对齐,此时可以通过Stack组件alignContent参数实现位置的相对对齐。

名称 描述
TopStart 顶部起始端。
Top 顶部横向居中。
TopEnd 顶部尾端。
Start 起始端纵向居中。
Center 横向和纵向居中。
End 尾端纵向居中。
BottomStart 底部起始端。
Bottom 底部横向居中。
BottomEnd 底部尾端。

根据上方表格内容可看出,我们需要为Stack增加顶部横向居中对齐的方式,故alignContent: Alignment.Top是符合的。

此时代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ArkUI复制代码@Entry
@Component
export struct TicketingView {
build() {
Stack({ alignContent: Alignment.Top }) {
Image("/pages/ComponentClassification/ExampleComponents/OneReserve/bg.png")
.height(350)
Column() {
}
.padding(5)
.alignItems(HorizontalAlign.Center)
.height(350)
}
.width("100%")
.height('100%')
}
}

垂直线性布局 (Column)

二级容器为三个子元素组成的垂直线性布局 (Column)

那么我们了解下第二个知识点Column:

垂直线性布局(Column)沿垂直方向布局的容器。

子元素在容器中依次排列。

其方便设置垂直排列信息。

image.png

如上图,Column容器内元素蓝色矩形位于最下方,粉色矩形位于最上方。

image.png

基于UI设计,由于我们根据UI里面的绝对高度,需要给给不同子容器定义固定的height,我们的Column容器内是居中对齐,此时可以通过Column组件alignContent参数的HorizontalAlign属性实现子组件在水平方向上的对齐格式。

名称 描述
Start 起始端对齐。
Center 居中对齐,默认对齐方式。
End 末端对齐。

此时默认值也是HorizontalAlign.Center,故.alignItems(HorizontalAlign.Center)可以省略。上文Column部分可以替换为下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ArkUI复制代码Column() {
Row() {
}
.height(92)
DatePicker({
start: new Date('2024-1-1'),
end: new Date('2025-1-1'),
})
.padding(3)
.height(165)
.width(230)
Button({ type: ButtonType.Normal }) {
Text( "开始查询")
.fontSize(18)
.fontColor(Color.White)
.margin({ left: 80, right: 80, top: 5, bottom: 5 })
}
.margin(20)
.height(35)
.backgroundColor("#FC6A68")
.borderRadius(8)
}
.padding(5)
.height(350)

image.png

至此,我们的布局已经基本完成(DatePick为日期选择器,Button为按钮,我们将在下文详细讲解,暂时只需要知道其实Column内子元素即可),但是很明显Column内的第一个元素Row还是空的,那么接下来我们开始完善该子元素。

水平线性布局 (Row)

垂直线性布局 (Column)内有三个子元素,其中第一个是Row布局的容器,为了实现展示出发地和目的地的功能。

那么我们了解下第三个知识点Row:

水平线性布局(Column)沿水平方向布局的容器。

子元素在容器中依次排列。

其方便设置横向排列信息。

image.png

如上图,Row容器内元素均为水平排列。

image.png

基于UI设计,我们的Row容器内也是五个子元素构成,其中包含三个Column容器和两个高度很低的矩形。我们的Row容器内是居中对齐,此时可以通过Row组件alignContent参数的VerticalAlign属性实现子组件在垂直方向上的对齐格式。

名称 描述
Top 顶部对齐。
Center 居中对齐,默认对齐方式。
Bottom 底部对齐。

和Column类似,此时Row容器默认值也是VerticalAlign.Center,故.alignItems(VerticalAlign.Center)可以省略。上文Row部分可以替换为下面代码,其包含五部分组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ArkUI复制代码Row() {
//1
Column({ space: 5 }) {
Text("信阳")
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor("#132968")
Text("XinYang")
.fontSize(13)
.fontColor("#132968")
}
.width(70)
//2
Rect()
.height(1)
.width(40)
.margin(7)
//3
Column({ space: 5 }) {

}
.width(40)
//4
Rect()
.height(1)
.width(40)
.margin(7)
//5
Column({ space: 5 }) {
Text("深圳")
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor("#132968")
Text("ShenZhen")
.fontSize(13)
.fontColor("#132968")
}
.width(70)

出发地和目的地是都是Column容器的中文和拼音上下组成,上文我们已经学习了Column的使用方式,此时我们可以通过space实现Column和Row内间距的效果。

1
2
3
4
5
6
7
8
9
10
scss复制代码  Column({ space: 5 }) {
Text("信阳")
.fontSize(20)
.fontWeight(FontWeight.Medium)
.fontColor("#132968")
Text("XinYang")
.fontSize(13)
.fontColor("#132968")
}
.width(70)

如上代码,出发地和目的地容器只是内部文本不同。

1
2
3
4
scss复制代码Rect()
.height(1)
.width(40)
.margin(7)

横线部分我们通过高度为1的矩形实现。

image.png

那么接下来是切换目的地的按钮和火车图标,其位于Row容器正中间,既第三个容器。

image.png

分析可知,其也是Column布局,分为上面火车图标和下方切换按钮。

image.png

由于我们要实现切换时火车外圆环图标的转动动画,故火车图标部分需要两张图通过Stack堆叠。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ArkUi复制代码//3
Column({ space: 5 }) {
Stack() {
Image("/pages/ComponentClassification/ExampleComponents/OneReserve/train.png")
.width(20)
Image("/pages/ComponentClassification/ExampleComponents/OneReserve/reverse.png")
.width(40)
}
Button() {
Image("/pages/ComponentClassification/ExampleComponents/OneReserve/buttonArrow.png")
.width(40)
}
.backgroundColor("#DEEBF9")
}
.width(40)

我们将上面Row内第三个容器修改成如上代码,因为我们已经了解了Stack和Column的用法,故这种嵌套容器的使用和理解,我相信已经没有什么难度了。

此时基本布局效果已经完成~

image.png

那么下一章,我们开始实现基本的逻辑,比如点击切换图标可以实现出发地与目的地的修改,日期选择后可以查询选择日期的火车票信息,以及通过按钮点击事件实现用户查询与购票操作。

敬请期待。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android 人脸识别实践 Android 人脸识别实践

发表于 2024-04-23

Android 人脸识别实践

前言

最近写了写设计模式相关的内容,感觉写的好烂,还不如多写点实践的东西,把我练手的项目设计好的一些模块补充完,最近就从Android硬件部分开始补充吧。

前面把相机写好了,正好想试试人脸识别,而Android也默认提供了人脸识别的功能,就试试吧!

效果图

读者可以先看看效果,再往下看代码实现:

677d15fb312932ea53357169f7ed48c3.gif
不好意思,放错了,正确的是下面这个:

7048c2ded203e631fd96b25bbf41c834.gif
Android这人脸识别也是逗,鸡哥表情包居然还confidence有0.5几。。。

人脸识别库选择

查找了一些文章,实际上人脸识别有很多库可以选择,下面就简单介绍下:

  1. Camera1 API的FaceDetectionListener,但是Camera1过时了
  2. Android SDK提供的FaceDetector
  3. Google Play Services Vision API,但是需要手机有Google服务框架
  4. OpenCV,开源计算机视觉库
  5. 第三方库: Face++ Android SDK、Microsoft Face API、百度人脸识别SDK。。。

本着简单方便的期望,又不想导入第三方库,我还是选择了FaceDetector来实现人脸识别。

下面就用CameraX和FaceDetector来实践下吧。

CameraX配置图像分析

关于相机的使用可以参考我前面的文章:

Android Camera各个API拍照实践

Android Camera各个API录像实践

这里就用最简单的CameraX来做人脸识别,比较简单,和拍照相比,就是把ImageCapture换成了ImageAnalysis,下面简单看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
kotlin复制代码/**
* 使用CameraX API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 PreviewView
*/
@SuppressLint("RestrictedApi")
fun startPreview(
activity: ComponentActivity,
view: PreviewView,
callback: Consumer<Bitmap>
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
cameraProviderFuture.addListener({
// 用于将相机的生命周期绑定到生命周期所有者
// 消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力
mCameraProvider = cameraProviderFuture.get()

// 预览
imagePreview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(view.surfaceProvider)
}

// 配置图像分析
imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(mCameraExecutor, FaceAnalyzer(callback))

// 选择摄像头,省去了去判断摄像头ID
val cameraSelector = mSelector

try {
// Unbind use cases before rebinding
mCameraProvider.unbindAll()

// 将相机绑定到 lifecycleOwner,就不用手动关闭了
mCameraProvider.bindToLifecycle(
activity, cameraSelector, imagePreview, imageAnalysis)

} catch(exc: Exception) {
Log.e("TAG", "Use case binding failed", exc)
}

// 回调代码在主线程处理
}, ContextCompat.getMainExecutor(activity))
}

就是在bindToLifecycle的时候传入一个imageAnalysis,imageAnalysis里面设置好我们对图像逐帧处理的FaceAnalyzer。

FaceDetector人脸识别

当CameraX预览进行的同时,FaceAnalyzer就会逐帧识别,调用它的analyze方法来识别人脸,下面看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
kotlin复制代码@SuppressLint("UnsafeOptInUsageError")
override fun analyze(image: ImageProxy) {
// Log.d("TAG", "analyze: ${image.format}")

// 控制人脸检测
if (!isOpenDetector) {
image.close()
return
}

val bitmap = if (image.format == ImageFormat.YUV_420_888) {
val rotation = image.imageInfo.rotationDegrees
imageProxyToBitmap(image.image!!, rotation)
}else {
imageProxyToBitmap(image)
}

// 创建FaceDetector
if (faceDetector == null) {
faceDetector = FaceDetector(bitmap.width, bitmap.height, maxFaces)
}

// 进行人脸检测
val faces = arrayOfNulls<FaceDetector.Face>(maxFaces)
val faceCount = faceDetector!!.findFaces(bitmap, faces)

// 处理人脸检测结果
if (faceCount > 0) {

// 第一张脸信息
val face1 = faces[0]!!
// 人脸的可信度,0 - 1
val confidence = face1.confidence()
// 双眼的间距
// val eyesDistance = face1.eyesDistance()
// 角度
// val angle = face1.pose(FaceDetector.Face.EULER_X)

Log.d("TAG", "analyze: confidence = $confidence")

// 加点判断,传出结果
if (confidence > 0.5){
callback.accept(bitmap)
}else {
bitmap.recycle()
}
}else {
bitmap.recycle()
}

// !!!注意关闭image
image.close()
}

FaceDetector的使用很简单,创建好后,调用它的findFaces方法就可以识别人脸了。

这里需要注意的一点就是,CameraX默认得到的图片格式是yuv格式的,需要做下转换,FaceDetector只支持Bitmap.Config.RGB_565格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kotlin复制代码private fun imageProxyToBitmap(image: Image, rotationDegrees: Int): Bitmap {
val yBuffer = image.planes[0].buffer
val uBuffer = image.planes[1].buffer
val vBuffer = image.planes[2].buffer

val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()

val nv21 = ByteArray(ySize + uSize + vSize)
yBuffer.get(nv21, 0, ySize)
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)

val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
val outputStream = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, image.width, image.height), 100, outputStream)
val jpegArray = outputStream.toByteArray()

val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.RGB_565
val bitmap = BitmapFactory.decodeByteArray(jpegArray, 0, jpegArray.size, options)

val matrix = Matrix()
matrix.postRotate(rotationDegrees.toFloat())

return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

将yuv格式转成rgb_565格式,就能识别了。

使用

我们只要在页面加载完成的时候启动预览,传入一个callback就能拿到识别到的bitmap了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码binding.root.post {
// 开启识别
faceDetectorHelper.startDetector()
// 启动预览
faceDetectorHelper.startPreview(requireActivity(), binding.preview) {
// 注意切换线程
activity?.runOnUiThread {
// 关闭识别
faceDetectorHelper.stopDetector()
// 设置图片
binding.image.setImageBitmap(it)
}
}
}

我这里加了个isOpenDetector来控制识别,识别到了人脸,就不用一直处理bitmap了,很耗资源。

源码及Demo

源码和demo都放在我练手的仓库里面,有兴趣可以参考下:

FaceDetectorHelper

FaceFragment

小结

花了点时间,通过CameraX和FaceDetector实现了一个动态的人脸识别功能,比较简单,仅仅是人脸识别,并不能认证哈。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nextjs v14 的 cookies()、header

发表于 2024-04-23

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

如果说,有一个 Node.js API 是 Next.js 路由和我们日常开发用到的多个 API 的核心,你猜是哪个 API?

答案是 AsyncLocalStorage。

这可不是一个新 API,早在 2020 年就进入了稳定阶段,在日常的开发中,也可放心使用。

本篇就为大家介绍下 AsyncLocalStorage 这个 API,此外本篇我还会写一个可运行的 Demo,用于展示 AsyncLocalStorage 如何在 Next.js 的 cookies()、headers() 等函数中发挥作用,帮助大家理解 cookies()、headers() 函数的实现原理。

Node.js AsyncLocalStorage 介绍

先让我们介绍下 AsyncLocalStorage 这个 API,简单的来说,这是一个解决异步操作中数据存储的 API。

像我们使用 Next.js、Express.js 等 Node.js 框架写路由处理程序时,一个请求中可能会连续嵌套调用多个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码app.get('/', async (req, res, next) => {
const result = await one();
});

async function one() {
setTimeout(() => {
two()
})
}

function two() {
// ....
}

如果我们在请求时声明一个值(就比如用于监控的 traceId),如何保证深层次的函数如上图的 two() 函数准确的获取这个值,而且保证各个请求之间相互独立,不会获取错乱呢?毕竟我们还用了 async/await、setTimeout 等异步方式调用,如果直接声明为全局变量,很容易就获取错误。

一个简单的方法就是将参数透传。用伪代码表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码app.get('/', async (req, res, next) => {
const traceId = uuid();
const result = await one(traceId);
});

async function one(traceId) {
setTimeout(() => {
two(traceId)
})
}

function two(traceId) {
console.log(traceId)
}

这样一层一层传递当然是可以的,就是不够优雅!

Node.js 直接提供了 AsyncLocalStorage 这个 API 用于处理异步操作中的数据存储问题,按照 Node.js 的说法,该 API 高性能且内存安全。而且于 Node.js v16 版本就已进入稳定阶段。所以可以放心使用。

AsyncLocalStorage 的用法也比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
javascript复制代码import { AsyncLocalStorage } from 'node:async_hooks';

const storage = new AsyncLocalStorage();
let id = 0;

function one() {
storage.enterWith({
traceId: id++
});
two()
}

function two() {
setTimeout(() => {
three()
},1000)
}

function three() {
const store = storage.getStore()
console.log(store.traceId, id)
}

one();
one();
one();
one();
one();

在这个例子中,我们声明一个 AsyncLocalStorage 实例,调用 enterWith 方法存储值,调用 getStore 获取值。

输出的效果如下:

image.png

尽管我们用了 setTimeout,但每个请求都获取到了正确的值,而如果我们直接获取外层 id 变量,因为 setTimeout 的异步效果,每次打印的值都是 5。

除了用 enterWith,也可以使用 run 方法,Node.js 提供了官方示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
javascript复制代码import http from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';

const asyncLocalStorage = new AsyncLocalStorage();

function logWithId(msg) {
const id = asyncLocalStorage.getStore();
console.log(`${id !== undefined ? id : '-'}:`, msg);
}

let idSeq = 0;
http.createServer((req, res) => {
asyncLocalStorage.run(idSeq++, () => {
logWithId('start');
// Imagine any chain of async operations here
setImmediate(() => {
logWithId('finish');
res.end();
});
});
}).listen(8080);

http.get('http://localhost:8080');
http.get('http://localhost:8080');
// Prints:
// 0: start
// 1: start
// 0: finish
// 1: finish

run 的第一个参数是 store,表示要存储的值,第二个参数是回调函数,store 只能在回调函数内访问,回调函数内创建的任何异步操作都可以访问该 store。

在这个例子中,我们使用 AsyncLocalStorage 构建了一个简单的 HTTP 请求 traceId,虽然发出了两条请求,但每条请求的 traceId 都是相互独立的,只能在各自的请求中获取到。

但是 AsyncLocalStorage 到底是怎么实现的呢?归根到底还是使用了底层的 API,拿到了异步函数的调用(AsyncResource,每次异步调用,V8都会创建一个对应的 AsyncResource),将 store 存储到这个异步资源上,所以在异步函数中调用也能正常获取到 store。

此外,使用 AsyncLocalStorage 会带来一定的性能损失,但相比它带来的收益,依然是十分值得使用的。

Next.js cookies() 介绍

说完 AsyncLocalStorage,我们说说 Next.js 的 cookies 函数,这是 Next.js 提供的用于获取请求 Cookie 的 API,使用方式如下:

1
2
3
4
5
6
7
javascript复制代码import { cookies } from 'next/headers'

export default function Page() {
const cookieStore = cookies()
const theme = cookieStore.get('theme')
return '...'
}

除了 cookies(),获取标头的做法也是类似的,只是改用 headers() 函数:

1
2
3
4
5
6
7
8
javascript复制代码import { headers } from 'next/headers'

export default function Page() {
const headersList = headers()
const referer = headersList.get('referer')

return <div>Referer: {referer}</div>
}

但是,在使用 cookies()、header() 的时候,有没有想过,为什么可以这样获取呢?为什么调用一下 cookies() 函数就可以获取到请求的 cookie,而不会出现错乱呢?为什么不采用透传 req 的方式来实现呢,就比如这样写:

1
2
3
4
5
javascript复制代码export default function Page(req) {
const headers = req.headers()
const cookies = req.cookies()
return // ...
}

cookeis()、headers() 的背后到底是怎么实现的呢?

这就要说到 AsyncLocalStorage。为了演示 cookies 的工作原理,我们顺便使用 Express 手写一个 React SSR,那就让我们开始吧!

AsyncLocalStorage 实现 cookies()

新建 next-cookies项目目录,运行 npm init初始化项目。

运行以下命令安装用到的依赖项:

1
bash复制代码npm i tsx express react react-dom

其中:

  1. tsx 用于编译运行 jsx 文件(当然你也可以用 bun 或者其他工具替代)
  2. express 用于构建服务
  3. react、react-dom 用于书写 React 代码

新建 index.tsx,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
javascript复制代码import express from "express";
import { AsyncLocalStorage } from 'node:async_hooks';
import { renderToPipeableStream } from 'react-dom/server';
import React from 'react';
import { User } from './user';

const cookiesStorage = new AsyncLocalStorage();

export function cookies() {
return cookiesStorage.getStore();
}

function parseCookies(request) {
const cookiesHeader = request.headers.cookie || '';
if (!cookiesHeader) return {}
return Object.fromEntries(
cookiesHeader.split(';').map(cookie => {
const [name, ...rest] = cookie.trim().split('=');
return [name, rest.join('=')];
})
)
}

const app = express();

app.get("/:route(*)", async (req, res) => {
const cookies = parseCookies(req);
console.log(cookies)
cookiesStorage.run(cookies, async () => {
const { pipe } = renderToPipeableStream(<User />, {
onShellReady() {
res.setHeader('content-type', 'text/html');
pipe(res);
}
});
})
});

app.listen(3000, (err) => {
if (err) return console.error(err);
return console.log(`Server is listening on 3000`);
});

这段代码并不复杂,我们来详细解释一下作用。

当访问 /xxx的时候,首先调用 parseCookies 获取 req 中的 cookies 对象,当然我们也可以直接使用 cookies-parse 等中间件,但这里为了更直观的展示,我们手动读取了 cookies 标头并将其转为对象。

然后调用 cookiesStorage.run,将 parse 后的 cookies 作为 store 传入,这样我们就可以在回调函数中的任何地方获取到该 store。

而在回调函数中,我们调用 renderToPipeableStream 将 React 组件转为流的形式进行返回。

renderToPipeableStream 是标准的 React API, 将一个 React 组件树渲染为管道化(pipeable)的 Node.js 流,具体使用方式可以参考 React 官网,这里我们演示的是一个标准的 renderToPipeableStream 用法。

而在具体的 <User> 组件中,新建 user.tsx,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码import React from 'react';
import { cookies } from '.'

export function User() {
const cookiesStore = cookies()
return (
<html lang="zh">
<body>
<h3>Cookies:</h3>
{JSON.stringify(cookiesStore, null, 2)}
</body>
</html>
)
}

我们调用了 index.tsx 导出的 cookies 函数,而导出的 cookies 函数代码其实很简单:

1
2
3
javascript复制代码export function cookies() {
return cookiesStorage.getStore();
}

就是这样,我们调用 cookies() 获取了请求的 cookies 对象。归根到底还是因为调用 cookies() 函数的时候还是在 cookiesStorage.run 的回调函数中。

修改 package.json,添加脚本命令:

1
2
3
4
5
javascript复制代码{
"scripts": {
"start": "tsx watch ./index.tsx"
}
}

最后运行 npm start,效果如下:

image.png

是不是跟我们在 Next.js 使用 cookies()、headers() 的方式很类似了?

  1. 功能实现:Next.js Cookies 函数
  2. 仓库源码:github.com/mqyqingfeng…
  3. 下载代码:git clone -b nextjs-cookies git@github.com:mqyqingfeng/next-app-demo.git

PS:学习 Next.js,欢迎入手小册《Next.js 开发指南》。基础篇、实战篇、源码篇、面试篇四大篇章带你系统掌握 Next.js!

参考链接

  1. www.youtube.com/watch?v=Jej…
  2. juejin.cn/post/723362…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深度解读《深度探索C++对象模型》之C++虚函数实现分析(一

发表于 2024-04-23

接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。

假如有这样的一段代码,代码中定义了一个Object类,类中有一个成员函数print,通过以下的两种调用方式调用:

1
2
3
4
c++复制代码Object b;
Object* p = new Object;
b.print();
p->print();

请问这两种方式有什么区别吗?在效率上一样吗?答案是不确定。因为得看成员函数print的声明方式,它可能是静态的,可能是非静态的,也可能是一个虚函数。还得看Object类的具体定义,它可能是独立的类,也有可能是经过多重继承来的类,或者继承的父类中有一个虚基类。

静态成员函数和非虚成员函数比较简单,我们在下一小节简单介绍一下即可,本文重点讲解虚函数的实现及其效率。

成员函数种类

  • 非静态成员函数

非静态成员函数和普通的非成员函数是一样的,它也是被编译器放置在代码段中,且可以像普通函数那样可以获取到它的地址。和普通非成员函数的区别是它的调用必须得经由一个对象或者对象的指针来调用,而且可以直接访问类中非公开的数据成员。下面的代码打印出函数的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c++复制代码#include <cstdio>

class Object {
public:
void print() {
printf("a=%d, b=%d\n", a, b);
}
int a = 1;
int b = 2;
};

void printObject(Object* obj) {
printf("a=%d, b=%d\n", obj->a, obj->b);
}

int main() {
printf("Object::print = %p\n", &Object::print);
printf("printObject = %p\n", &printObject);

return 0;
}

程序的输出结果如下,从打印结果来看,两者的地址比较相近,说明它们都是一起放在代码段中的,从生成的汇编代码也可以看出来。

1
2
text复制代码Object::print = 0x1007b3f30
printObject = 0x1007b3e70

非静态成员函数和普通非成员函数的运行效率上也是一样的,普通非成员函数的实现上,对类中成员的访问看起来像是要经过指针的间接访问,如obj->a,非静态成员函数的访问看起来更直接一点,直接可以对类中的成员进行存取,好像是非静态成员函数的效率更高一些,其实不然,非静态成员函数的调用,编译器会隐式的把它转换成另一种形式:

1
2
3
4
5
6
7
8
c++复制代码Object obj;
obj.print();
// 转换成:
print(&obj);
// print的定义转换成:
print(Object* const this) {
printf("a=%d, b=%d\n", this->a, this->b);
}

两者在本质上是一样的,查看生成的汇编代码也是一样的。另外也说明了为什么非静态成员函数要经由一个对象或对象的指针来调用。

  • 静态成员函数

上面提到的非静态成员函数的调用,必须要经由类的对象来调用,是因为需要将对象的地址作为函数的参数,也就是隐式的this指针,这样在函数中访问类的非静态数据成员时将绑定到此地址上,也就是将此地址作为基地址,经过偏移得到数据成员的地址。但是如果函数中不需要访问非静态数据成员的话,是不需要this指针的,但目前的编译器并不区分这种情况。静态成员函数不能访问类中的非静态数据成员,所以是不需要this指针的,如Object类中定义了静态成员函数static int static_func(),通过对象调用:

Object obj;

obj.static_func();

或者通过对象的指针调用:

Object* pobj = new Object;

pobj->static_func();

最终都会转换成员如下的形式:

Object::static_func();

通过对象或者对象的指针来调用只是语法上的便利而已,它并不需要对象的地址作为参数(this指针)。

那么静态成员函数存在的意义是什么?静态成员函数在C++诞生之初是不支持的,是在后面的版本中增加进去的。假设不支持静态成员函数时,类中有一个非公开的静态数据成员,如果外面的代码需要访问这个静态数据,那么就需要写一个非静态成员函数来存取它,而非静态成员函数需要经由对象来调用,但有时候在这个时间点没有创建一个对象或者没有必要创建一个对象,那么就有了以下的变通做法:

1
2
3
4
c++复制代码// 假设定义了get_static_var函数用于返回静态数据成员
((Object*) 0))->get_static_var();
// 编译器会转换成:
get_static_var((Object*) 0));

上面的代码把0强制转换为Object类型的指针,然后经由它来调用非静态成员函数,编译器会把0作为对象的地址传递给函数,但函数中不会使用这个0,所以不会出现问题。由于有这些需求的存在,C++标准委员会增加了支持静态成员函数,静态成员函数可以访问类中的非公开的静态数据成员,且不需要经由类的对象来调用。

静态成员函数和非静态成员函数、普通函数一样都是存储在代码段中的,也可以获取到它的地址,它是一个实际的内存的地址,是一个数据,如上面定义的static_func函数,它的类型为int (*)(),就是一个普通的函数类型。而非静态成员函数,返回的是一个“指向类成员函数的指针”,如上面定义的print函数,返回的类型是:

void (Object::*) ();

静态成员函数基本上等同于普通函数,所以和C语言结合编程时,可以作为回调函数传递给C语言写的函数。

总结一下,静态成员函数具有以下的特性:

    • 静态成员函数不能存取类中的非静态数据成员。
      • 静态成员函数不能被声明为const、volatile或者是virtual。
      • 静态成员不需要经由类的对象来调用。
  • 虚函数

虚函数是否也可以像非虚函数那样获取到它的地址呢?我们写个程序来测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c++复制代码#include <cstdio>

class Object {
public:
virtual void virtual_func1() {
printf("this is virtual function 1\n");
}
virtual void virtual_func2() {
printf("this is virtual function 2\n");
}
};

int main() {
printf("Object::virtual_func1 = %p\n", &Object::virtual_func1);
printf("Object::virtual_func2 = %p\n", &Object::virtual_func2);
return 0;
}

上面程序的输出:

1
2
text复制代码Object::virtual_func1 = 0x0
Object::virtual_func2 = 0x8

程序的输出结果并不是一个内存地址,而是一个数字,其实这是一个偏移值,对应的是这个虚函数在虚函数表中的位置,一个位置占用8字节大小,第一个是0,第二个是8,以此类推,每多一个虚函数,就在这个表中占用一个位置。看起来像是无法获取到虚函数的地址,其实不然,虚函数的地址就存放在虚函数表中,只是我们无法直接获取到它,但是我们记得,如果有虚函数时,对象的前面会被编译器插入一个虚函数表指针,这个指针就是指向类的虚函数表,我们可以通过它来获取到虚函数的地址,下面演示一下通过非常规手段来调用虚函数的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
c++复制代码#include <cstdio>

class Object {
public:
virtual void virtual_func1() {
printf("this is virtual function 1\n");
}
virtual void virtual_func2() {
printf("this is virtual function 2\n");
}
};

int main() {
Object* pobj = new Object;
using Fun = void (*)(void);
Fun** ptr = (Fun**)pobj;
printf("vptr = %p\n", *ptr);
for (auto i = 0; i < 2; ++i) {
Fun fp = *(*ptr + i); //取得虚函数的内存地址
printf("vptr[%d] = %p\n", i, fp);
fp(); //此行调用虚函数
}
delete pobj;

return 0;
}

程序的输出结果:

1
2
3
4
5
text复制代码vptr = 0x100264030
vptr[0] = 0x100263ea4
this is virtual function 1
vptr[1] = 0x100263ecc
this is virtual function 2

可以看到,虚函数的地址不光可以获取得到,而且还可以直接调用它,调用它的前提是函数中没有访问类的非静态数据成员,不然就会出现运行错误。vptr就是写入到对象前面的虚函数表指针,它的值就是虚函数表在内存中的地址,虚函数表中记录了两项内容,对应了两个虚函数的地址,即vptr[0]是虚函数virtual_func1的地址,vptr[1]是虚函数virtual_func2的地址。把他们强制转换成普通函数的类型指针,然后可以直接调用他们,所以这里是没有对象的this指针的,也就不能访问类中的非静态数据成员了。

虚函数的实现

从上一小节中我们已经窥探到虚函数的一般实现模型,每一个类有一个虚函数表,虚函数表中包含类中每个虚函数的地址,然后每个对象的前面会被编译器插入一个指向虚函数表的指针,同一个类的所有对象都共享同一个虚函数表。接下来的内容中将详细分析虚函数的实现细节,包括单一继承、多重继承和虚继承的情况。

多态是C++中最重要的特性之一,也是组成面向对象编程范式的基石,虚函数则是为多态而生。那么何为多态?多态是在基类中定义一组接口,根据不同的业务场景派生出不同的子类,在子类中实现接口,上层代码根据业务逻辑调用接口,不关心接口的具体实现。在代码中,一般是声明一个基类的指针,此指针在运行期间可能指向不同的派生类,然后通过基类的指针调用一个接口,这个接口在不同的派生类中有不同的实现,所以根据基类的指针指向哪个具体的派生类,调用的就是这个派生类的实例。假设有一个名称为print的接口,p是基类类型的指针,那么下面的调用:

p->print();

是如何识别出要实施多态的行为?以及如何调用到具体哪个派生类中的print?如果是在指针类型上增加信息,以指明具体所指对象的类型,那么会改变指针原有的语义,造成和C语言的不兼容,而且也不是每个类型都需要这个信息,这会造成不必要的空间浪费。如果是在每个类对象中增加信息,那么在不需要多态的对象中也需要存放这些信息,也会造成空间上的浪费。因此增加了一个关键字virtual,用于修饰那些需要多态的函数,这样的函数就叫做虚函数,所以识别一个类是否支持多态,就看这个类中是否声明了虚函数。只要类中有虚函数,就说明需要在类对象中存储运行期的信息。

那么在对象中要存储哪些信息才能够保证保证上面代码中print的调用是调用到正确的派生类中的实例呢?要调用到正确的print实例,我们需要知道:

  • p指向具体的对象类型,让我们知道要调用哪个print;
  • print的位置,以便我们可以正确调用它。

要如何实现它,不同的编译器可能有不同的实现方法,通常是使用虚函数表的做法。编译器在编译的过程中,收集到哪些是虚函数,然后将这些虚函数的地址存放一个表格中,这些虚函数的地址在编译期间确定的,运行期间是不会改变的,虚函数的个数也是固定的,在程序的执行期间不能删除或者增加,所以表格的大小也是固定的,这个过程由编译器在编译期间完成。表格中虚函数的位置按照类中声明的顺序,位置是固定不变的,我们在上节中通过虚函数名称打印出来的值就是虚函数在虚函数表中的位置,即相对于表格首地址的偏移值。

有了这个表格,那么如何寻址到这个表格呢?方法就是编译器根据类中是否有虚函数,如果有虚函数,就在类的构造函数里插入一些汇编代码,在构造对象时,在对象的前面插入一个指针,这个指针指向这个虚函数表,所以这个指针也叫做虚函数表指针。下面以具体的代码来看看虚函数是怎么调用的,把上面的例子main函数修改如下,其它地方不变:

1
2
3
4
5
6
7
8
c++复制代码int main() {
Object* pobj = new Object;
pobj->virtual_func1();
pobj->virtual_func2();
delete pobj;

return 0;
}

我们来看下生成的汇编代码,首先来看看虚函数表长什么样:

1
2
3
4
5
text复制代码vtable for Object:
.quad 0
.quad typeinfo for Object
.quad Object::virtual_func1()
.quad Object::virtual_func2()

它是汇编中定义在数据段的一组数据,“vtable for Object”是它的标签,代表了这个数据区的起始地址,每一行定义一条数据,第一列.quad表示数据的大小,占用8字节,第二列表示数据的值,可以是数字,也可以是标签,标签是地址的引用。其实这个完整的表叫做虚表,它包含了虚函数表、RTTI信息和虚继承相关的信息,Clang和Gcc编译器是把它们合在一起了,其它编译器可能是分开的。第一行是虚继承中用到,之前已经讲过了,第二行是RTTI信息,这个以后再讲。第三、四行是两个虚函数的地址。

接着看看Object类的默认构造函数的代码:

1
2
3
4
5
6
text复制代码Object::Object() [base object constructor]: 	# @Object::Object() [base object constructor]
# 略...
lea rcx, [rip + vtable for Object]
add rcx, 16
mov qword ptr [rax], rcx
# 略...

之前已经讲过,有虚函数时编译器会为类生成默认构造函数,在默认构造函数里在类对象的前面设置了虚函数表指针。在这个默认构造函数里,主要的代码就是上面这三行,首先获取虚表(将上面)的起始地址存放在rcx寄存器,然后加上16的偏移值跳过第一、二行,这时指向第三行数据,也就是第一个虚函数的位置,然后将这个地址赋值给[rax],rax是存放的对象的首地址,这就完成了给对象设置虚函数表指针。

接着看main函数中对虚函数的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
text复制代码main:								# @main
# 略...
# 调用构造函数
mov rdi, rax
mov qword ptr [rbp - 32], rdi # 8-byte Spill
call Object::Object() [base object constructor]
mov rax, qword ptr [rbp - 32] # 8-byte Reload
mov qword ptr [rbp - 16], rax
# 调用第一个虚函数
mov rdi, qword ptr [rbp - 16]
mov rax, qword ptr [rdi]
call qword ptr [rax]
# 调用第二个虚函数
mov rdi, qword ptr [rbp - 16]
mov rax, qword ptr [rdi]
call qword ptr [rax + 8]
# 略...

上面汇编代码中的第4行rax是调用new函数后返回来的地址,也就是pobj指针,把它存放到rdi寄存器中作为参数,同时也保存到栈空间rbp - 32中,然后调用构造函数,构造完成之后再拷贝这个地址到栈空间rbp - 16中。接下来的第10到12行是第一个虚函数的调用,将对象的首地址加载到rdi寄存器中,然后对其取内容,也就是是相当于指针的解引用,即 (*pobj),取得的内容即是构造函数中设置的虚函数表的地址,它是一个指向第一个虚函数的地址,然后第12行对其取内容,也即是对这个地址解引用,取得第一个虚函数的地址,然后以rdi寄存器(即对象的首地址)为第一个参数调用它,相当于:virtual_func1(pobj)。第14到16行是对第二个虚函数的调用,流程和第一个基本一样,区别在于将虚函数表的地址加上8的偏移量以指向第二个虚函数。

如果在一个虚函数中调用另一个虚函数又会怎样?第一个虚函数已经决议出是调用哪个对象的实例了,那么在其中调用其它虚函数还需要再动态决议吗?把main函数中对第二个虚函数的调用去掉,在第一个虚函数中增加以下代码:

1
2
c++复制代码virtual_func2();
Object::virtual_func2();

来看下对应生成的汇编代码,其它代码都差不多,主要看virtual_func1函数的代码:

1
2
3
4
5
6
7
8
text复制代码Object::virtual_func1():            # @Object::virtual_func1()
# 略...
mov rdi, qword ptr [rbp - 16] # 8-byte Reload
mov rax, qword ptr [rdi]
call qword ptr [rax + 8]
mov rdi, qword ptr [rbp - 16] # 8-byte Reload
call Object::virtual_func2()
# 略...

rbp - 16保存的是对象的首地址,第3到5行对应的是上面C++代码中第一句的调用,看起来在虚函数中调用另一个虚函数,用的还是动态决议的方法,这里编译器没有识别出已经决议出具体的对象了。从汇编代码的第6、7行看到,通过前面加类名的限定符,是直接调用到这个函数,如果你明确调用的是哪个函数的话,可以直接在函数的前面加上类名,这样就不需要用多态的方式去调用了。

如果不是通过指针类型来调用虚函数,而是通过对象来调用,结果是什么情况?把main函数改成如下:

1
2
3
4
5
6
7
c++复制代码int main() {
Object obj;
obj.virtual_func1();
obj.virtual_func2();

return 0;
}

查看main函数对应的汇编代码:

1
2
3
4
5
6
7
text复制代码main:                           # @main
# 略...
lea rdi, [rbp - 16]
call Object::virtual_func1()
lea rdi, [rbp - 16]
call Object::virtual_func2()
# 略...

可以看到通过对象来调用虚函数,是直接调用到这个对象的函数实例的,没有使用多态的方式,所以通过对象的方式调用是没有多态的行为的,只有通过类的指针或者引用类型来调用虚函数,才会有多态的行为。

单一继承下的虚函数

假设有以下的类定义及继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
c++复制代码class Point {
public:
Point(int x = 0) { _x = x; }
virtual ~Point() = default;
virtual void drawLine() = 0;
int x() { return _x; }
virtual int y() { return 0; }
virtual int z() { return 0; }
private:
int _x;
};
class Point2d: public Point {
public:
Point2d(int x = 0, int y = 0): Point(x) { _y = y; }
virtual ~Point2d() = default;
void drawLine() override { }
virtual void rotate() { }
int y() override { return _y; }
private:
int _y;
};
class Point3d: public Point2d {
public:
Point3d(int x = 0, int y = 0, int z = 0): Point2d(x, y) { _z = z; }
virtual ~Point3d() = default;
void drawLine() override { }
void rotate() override { }
int z() override { return _z; }
private:
int _z;
};

int main() {
Point* p = new Point3d(1, 1, 1);
printf("z = %d\n", p->z());
delete p;
return 0;
}

先来看看生成的汇编代码中的虚函数表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
text复制代码vtable for Point:
.quad 0
.quad typeinfo for Point
.quad Point::~Point() [base object destructor]
.quad Point::~Point() [deleting destructor]
.quad __cxa_pure_virtual
.quad Point::y()
.quad Point::z()

vtable for Point2d:
.quad 0
.quad typeinfo for Point2d
.quad Point2d::~Point2d() [base object destructor]
.quad Point2d::~Point2d() [deleting destructor]
.quad Point2d::drawLine()
.quad Point2d::y()
.quad Point::z()
.quad Point2d::rotate()

vtable for Point3d:
.quad 0
.quad typeinfo for Point3d
.quad Point3d::~Point3d() [base object destructor]
.quad Point3d::~Point3d() [deleting destructor]
.quad Point3d::drawLine()
.quad Point2d::y()
.quad Point3d::z()
.quad Point3d::rotate()

每个类都有一个对应的虚函数表,虚函数表中的内容主要来自于三方面:

  • 改写基类中对应的虚函数,用自己实现的虚函数的地址写入到对应表格中的位置;
  • 从基类中继承而来的虚函数,直接拷贝基类虚函数的地址添加到虚函数表中;
  • 新增的虚函数,基类中没有,子类的虚函数表会增加一行容纳新条目;

基类和子类使用各自的虚函数表,互不干扰,即使子类中没有改写基类的虚函数,也没有新增虚函数,编译器也会为子类新建一个虚函数表,内容从基类中拷贝过来,内容和基类完全一样。

虚函数表中的虚函数的排列顺序是固定的,一般是按照在类中的声明顺序,如C++代码中的这行代码:

p->z();

要寻址到正确的z函数实例的地址,我们首先需要知道p指针所指向的具体对象,然后需要知道z函数在表格中的位置,如上例中,z函数在第5个条目,也就是说虚函数表的起始地址加上32的偏移量就可以寻址到它,这个位置保持不变,无论p指针指向哪个对象,都能找到正确的z函数。如果子类中有新增的虚函数,新增的虚函数声明的位置插在从基类中继承来的虚函数中间,编译器会做调整,把它安排在后面,在原有的顺序上再递增,如上例中的rotate函数。

如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享并关注,以便在内容更新时直接向您推送。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

理解 Gradle 1 Gradle 简介 2 Grad

发表于 2024-04-23

在软件开发领域,构建工具是开发过程中不可或缺的一部分。它们承担着将源代码转换为可执行程序或库的重要任务,并且在自动化构建、依赖管理和持续集成方面发挥着关键作用。在众多构建工具中,Gradle 以其强大的功能和灵活的特性成为了官方推荐的 Android 项目构建工具。

  1. Gradle 简介

Gradle 是一个基于 Apache Ant 和 Apache Maven 概念的构建自动化工具,但它更加灵活和强大。与传统的 XML 配置相比,Gradle 使用 Groovy 或 Kotlin DSL(领域特定语言)编写构建脚本,使得构建逻辑更加直观和易于理解。Gradle 支持多项目构建、依赖管理、增量构建等功能,并且具有良好的扩展性,可以通过插件机制轻松扩展其功能。

  1. Gradle 生命周期

Gradle 构建过程可以分为三个阶段:

  • 初始化(Initialization)
  • 配置(Configuration)
  • 执行(Execution)

在初始化阶段,Gradle 准备执行环境;在配置阶段,Gradle 配置项目结构、任务和依赖关系;在执行阶段,Gradle 执行项目中定义的任务并完成构建过程。

2.1 Gradle 初始化阶段(Initialization)

Gradle 初始化阶段是构建过程的第一步,负责准备执行环境。在初始化阶段,Gradle 会执行用户目录下(C:\Users\%USERNAME%\.gradle\)的 init.d\ 文件夹中(init.d\ 文件夹默认不存在,需要手动创建)的 gradle 文件。这个文件夹中可以放置多个 gradle 文件,这个阶段会按照字母序依次执行这些文件。

例如,可以在 init.d\ 文件夹中创建一个 init.gradle 文件,写入以下内容:

1
2
3
4
5
println("gradle version: ${getGradleVersion()}")

ext.timestamp = {
new Date()
}

getGradleVersion() 是 Gradle 接口中的函数,Gradle 完整方法参见:
docs.gradle.org/current/jav…

在项目每次执行时,就会打印出当前的 gradle 版本,例如:gradle version: 8.2,并且会创建一个 timestamp 的全局属性,在项目中通过 gradle.timestamp() 可以读取此属性。

1
2
// 输出 timestamp: Thu Apr 18 16:06:46 CST 2024
println("timestamp: ${gradle.timestamp()}")

初始化阶段可以用于实现一些全局性的配置和初始化操作。

在多项目构建时,(Multi-Project Build),初始化阶段还会解析 settings.gradle 中的内容,该文件用于定义项目的结构(包含的子项目),以及一些全局配置。

项目的 settings.gradle 示例代码:

1
2
3
4
5
6
7
8
9
10
11
// 指定包含的子项目
include 'subproject1', 'subproject2'
// 指定包含其他文件夹下的子项目
include ':subproject_in_other_folder'
// 指定其他文件夹下的子项目所在的文件夹
project(':subproject_in_other_folder').projectDir = file('<path_of_the_folder_contains_the_subproject>')

// 指定 rootProject 的 name
rootProject.name = "MyProject"
// 指定 rootProject 的构建文件名称,默认为 build.gradle
rootProject.buildFileName = "myCustomName.gradle"

include(), getRootProject() 是 Settings 接口中的函数,Settings 完整方法和属性参见:
docs.gradle.org/current/jav…

2.2 Gradle 配置阶段(Configuration)

Gradle 配置阶段是构建过程的核心,负责定义和配置项目的各个任务。在配置阶段,Gradle 会按照构建脚本逐步解析和配置项目中的任务和依赖关系。通过 DSL(Domain-Specific Language,领域特定语言)提供的丰富功能,开发者可以灵活地定义任务、管理依赖和配置构建流程,为后续的执行阶段做好准备。

项目的 build.gradle 示例代码:

1
2
3
4
5
6
7
8
9
// 添加依赖库仓库
repositories {
mavenCentral()
}

// 添加依赖库
dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1'
}

repositories(), dependencies(), getTasks() 是 Project 接口中的函数,Project 完整方法和属性参见:
docs.gradle.org/current/jav…

2.2.1 Gradle 中的代理模式

  • 所有的 gradle 文件都实现了 Gradle 接口。在一般的 gradle 文件中,上下文是 Gradle 的代理对象,可以使用 Gradle 接口中的方法。
  • build.gradle 额外实现了 Project 接口。在 build.gradle 文件中,上下文是 Project 的代理对象,可以使用 Project 接口中的方法。通过 project.gradle 可以拿到 Gradle 代理对象。
  • settings.gradle 额外实现了 Settings 接口。在 settings.gradle 文件中,上下文是 Settings 的代理对象,可以使用 Settings 接口中的方法。通过 settings.gradle 可以拿到 Gradle 代理对象。

image.png

2.2.2 Gradle 中的属性

gradle 支持定义 key-value 属性,可以将其放到项目的 gradle.properties 文件中,或者 gradleUserHomeDir 中的 gradle.properties 文件中,也可以通过命令行 -Pkey=value 传入。

使用属性前,最好先用 hasProperty() 函数做个检查,更为安全。

2.2.3 Task 和 Action

gradle 的 configuration 阶段用于向 project 中定义、配置好各个 tasks。execution 阶段用于真正执行这些 tasks。一个 project 由 0 个或多个 tasks 构成,一个 task 由 0 个或多个 actions 构成。

task 中有两个非常方便的方法 doFirst 和 doLast,这两个方法可以用于给指定的 task 添加 action。

1
2
3
4
5
6
7
8
9
// 自定义 task
tasks.register('customTask') {
doFirst {
println 'Before customTask'
}
doLast {
println 'After customTask'
}
}

Task 的所有方法参见:
docs.gradle.org/current/jav…

doFirst 有多个时,最后加的会最先执行。doLast 有多个时,最后加的会最后执行。换句话说,doFirst 添加的 action 是加到所有 action 最前端的,doLast 添加的 action 是加到所有 action 最后端的,恰如其名。

通过 dependsOn 可以设置 task 执行顺序:

1
2
3
4
5
6
7
// 修改 Task
clean.doFirst {
println 'Start to clean'
}

// 配置 Task 之间的依赖关系,下面这行代码意味着 clean task 需要等待 customTask 执行完成后才能执行。
clean.dependsOn(customTask)

执行 clean,输出如下:

1
2
3
Before customTask
After customTask
Start to clean

2.2.4 TaskGraph

在 Configuration 阶段定义好 Task,并设置好依赖关系后。在提交阶段前会生成一个 TaskGraph,也就是在提交阶段需要被执行的所有 Task 的拓扑图。通过 project.gradle.taskGraph.getAllTasks() 可以获取 TaskGraph 中的所有 Task,由于 TaskGraph 是在 Configuration 阶段完成后才能知道的,所以不能直接获取到,而是需要在 taskGraph 的 whenReady 方法中读取:

1
2
3
4
5
project.gradle.taskGraph.whenReady {
it.getAllTasks().each {
println("Task: $it")
}
}

执行一次 clean,输出如下:

1
2
Task: task ':customTask'
Task: task ':clean'

TaskGraph 是一个有向无环图,通过 getDependencies() 可以获取某个 Task 的依赖关系:

1
2
3
4
5
project.gradle.taskGraph.whenReady {
it.getDependencies(clean).each {
println("Task: $it")
}
}

执行一次 clean,输出如下:

1
Task: task ':customTask'

getAllTasks() 和 getDependencies() 都是 TaskExecutionGraph 接口中的方法,TaskExecutionGraph 的所有方法参见:
docs.gradle.org/current/jav…

TaskExecutionGraph 还有一个常用的方法 hasTask(),用于判断某个 task 是否在 graph 中。接收的参数可以是 task name,或 task 本身。

2.2.5 使用插件

插件中可以修改配置,添加 task,添加 action 等。相当于将配置阶段的某些操作进行封装,以便更好地复用。

以使用 java 插件为例,添加插件可以写成:

1
2
3
plugins {
id 'java'
}

或者:

1
apply plugin: 'java'

在使用 java 插件后,项目中就引入了其中集成的 task,如生成 java 文档的 javadoc task、打包 jar 文件的 jar task。

可以修改这些 task 的配置,比如修改 jar 文件名称:

1
2
3
jar {
baseName = "customName"
}

Apply java plugin 后,添加的 task 参见:docs.gradle.org/current/use…

也可以自定义插件,将项目中比较独立的、可以复用的 task 和配置进行封装。这个话题可以单独写一篇文章,这里推荐两篇写得不错的文章:

  • 深度探索 Gradle 自动化构建技术(四、自定义 Gradle 插件)
  • Gradle筑基篇(五)-Gradle自定义插件实战

2.2.6 依赖管理和传递性依赖(Transitive Dependencies)

Gradle 提供了强大的依赖管理机制,支持直接依赖(Direct Dependencies)和传递性依赖(Transitive Dependencies)。Gradle 还支持排除特定的传递性依赖,以解决版本冲突和依赖冲突的问题。

引入依赖库仓库:

1
2
3
repositories {
mavenCentral()
}

这里也用到了代理模式,在进入 repository block 之前,当前的 delegate object 是 project,可以调用 Project 接口中的方法。进入之后,当前的 delegate object 变成了 repository 提供的代理对象,相当于切换了 Context。可以直接调用 repository 中的方法,比如 mavenCentral()。

所以引入依赖库仓库也可以写成:

1
2
3
project.repositories {
repositories.mavenCentral()
}

或者:

1
2
3
delegate.repositories {
delegate.mavenCentral()
}

mavenCentral() 引入的依赖库仓库地址是什么呢?查看 Project 接口文档中关于 repository {} 的介绍可以发现,它的上下文中,提供的代理对象是 RepositoryHandler。再查看 RepositoryHandler 的文档
就能找到 mavenCentral() 代表的实际 URL:repo.maven.apache.org/maven2/

MVN Reporitory 地址:mvnrepository.com/ ,可以在这个网站查询 mavenCentral() 中有哪些仓库,如何添加依赖。

同样地,可以找到 google() 代表的实际 URL:dl.google.com/dl/android/…

但貌似 google 没有提供一个网页来展示其所有库,如果有读者找到,望不吝赐教。

添加依赖库仓库只是添加依赖的第一步,通过 dependencies {} 才是真正添加依赖库。

通过同样的方式可以查到 dependencies 的代理对象是 DependencyHandler:docs.gradle.org/current/dsl… ,其中可以找到 dependencies {} 代码块中可以调用的函数。

注:implementation 函数是 Gradle Java Plugin 中的,api 函数是 Gradle Java Library Plugin 中的。

以 mvnrepository.com/artifact/or… 为例,引入依赖库的完整写法:

1
implementation group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1'

简单写法:

1
implementation 'org.apache.commons:commons-math3:3.6.1'

Dependencies 按照引用方式,可以分为 Direct Dependencies 和 Transitive Dependencies。
Direct Dependencies 指的是直接在项目中声明的依赖,Transitive Dependencies 指的是 Direct Dependencies 中带过来的依赖。

查看 app 模块下的依赖:

1
gradlew app:dependencies

如果依赖项较多,不方便在控制台查看的话,可以将其输出到文件中:

1
gradlew app:dependencies >dependencies.txt

还可以利用 project-report 插件生成 report,添加插件:

1
apply plugin: 'project-report'

执行 task:

1
gradlew htmlDependencyReport

执行后,会在 app/build/reports/ 目录中生成一份 report,点击 app/build/reports/project/dependencies/index.html 可以查看 dependency report:

其中,implementation 中包含的是 Direct dependencies,RuntimeClassPath 中包含 Direct dependencies 和 Transitive dependencies。

Gradle project-report plugin doc:docs.gradle.org/current/use…

排除某个 Transitive Dependencies 示例代码:

1
2
3
implementation('commons-beanutils:commons-beanutils:1.9.4') {
exclude group: 'commons-collections', module: 'commons-collections'
}

2.2.7 Multi-Project Build

Multi-Project 由 Root Project 和多个依赖的 Sub Projects 组成,Sub Project 也可以继续依赖其他 Sub Project,Sub Project 之间也可以互相依赖。

在 Root project 执行 clean 或 build 时,所有的 Sub Projects 也会执行 clean 和 build。

Root project 需要有一个 settings.gradle 文件,在 Single project Build 时,这个文件是可选的,但 Multi-Project Build 必须有这个文件。这个文件用于标记此项目包含哪些子项目,也可以用于设置所有子项目中共有的属性。

在 rootProject 的 build.gradle 文件中,可以设置子项目共有的属性:

1
2
3
4
5
6
7
8
9
10
11
12
subprojects {
apply plugin: 'java'
group = 'com.mygroup'
version = '0.1'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
mavenCentral()
}
}

除了 subprojects 方法外,还有个 allProjects 方法,包含 Root Project 和 Sub Projects:

1
2
3
4
5
allprojects {
println("$it")
println("$it.name")
println("$it.path")
}

也可以通过 rootProject.childProjects 来访问 Sub Projects:

1
2
3
4
rootProject.childProjects.each {
println("$it")
println("$it.key, $it.value, $it.value.name")
}

通过 project(path) 可以获取单个 Project:

1
2
3
4
5
6
7
8
9
project(':') {
println("$it")
println("$it.name")
}

project(':app') {
println("$it")
println("$it.name")
}

将某个 project 添加为依赖也是用这种类似的方式:

1
2
3
dependencies {
implementation project(':lib')
}

如果要将 transitive dependencies 也引用进来,需要用 api 替换 implementation。api scope 在 java-library plugin 中,这很容易理解,因为 application 是独立的,不会被复用,而 java-library 是可以被复用的:

1
2
3
4
apply plugin: 'java-library'
dependencies {
api 'org.apache.commons:commons-math3:3.6.1'
}

2.3 Gradle 执行阶段(Execution)

Gradle 执行阶段是构建过程的最后一步,负责真正执行项目中定义的任务。在执行阶段,Gradle 会按照任务之间的依赖关系,逐个执行任务并完成构建过程。执行阶段通常涉及编译、打包、测试和部署等具体操作,Gradle 会根据项目的配置和需求,自动化地执行这些任务。通过执行阶段,开发者可以将项目从源代码编译到最终可执行的产品,实现自动化构建和持续集成。

Gradle 是一个增量构建系统(incremental build system),如果某个 task 没有更改,它会输出 UP-TO-DATE 并跳过执行,这可以让构建更快。

以 compile task 为例,它的 input 是 source code,output 是 class files。如果本次构建时 source code 相对于上次构建而言,没有更改,并且 output 没有被删除,那么 compile task 在本次构建时就会被跳过。简言之,input 和 output 未改变,则输出 UP-TO-DATE 并跳过此 task。

clean task 用于删除 build 文件夹,如果执行了 clean 再执行 compile,由于 output 被删除了,所以 compile 又会被执行一遍。

三、其他

3.1 Kotlin DSL

Gradle 支持使用 Kotlin 语言,使用 Kotlin DSL 时,文件名不一样:

  • settings.gradle -> settings.gradle.kts
  • build.gradle -> build.gradle.kts

使用的语言也由 groovy 换成 kotlin。

3.2 gradle wrapper

gradle wrapper 的作用是提供一份离线的 gradle 程序,这样电脑上无需安装 gradle 也能运行一个 project。而且能避免版本不一致的问题。

项目中有一个 wrapper task,用于生成 gradle-wrapper,其中可以配置 gradleVersion:

1
2
3
wrapper {
gradleVersion = '8.2'
}

使用 gradle wrapper 执行 clean + build 示例代码:

1
gradlew clean build

四、参考文章

  • Gradle 官方文档
  • The Gradle Masterclass
  • 深度探索 Gradle 自动化构建技术(四、自定义 Gradle 插件)
  • Gradle筑基篇(五)-Gradle自定义插件实战

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

⌛ 一文掌握:Redis7的主从复制 一、什么是Redis7

发表于 2024-04-23

一、什么是Redis7的主从复制

Redis 的主从复制是一种数据复制机制,用于在多个 Redis 服务器之间实现数据同步。

  • master以写为主,Slave以读为主
  • 当master数据变化的时候,自动将新的数据异步同步到其它slave数据库

二、主从复制的四种模式

前置操作

①准三台centos虚拟机IP分别为: 129、131、131

image.png

②恢复Redis的配置为原始配置

cd /java/redis/redis-7.0.2/

cp redis.conf /root/myredis/redisNeW.conf

image.png

2.1 一主二仆

配置步骤

①配置 IP:129 的Redis

2.1.1 开启daemonize yes

image.png

2.1.2 注释掉bind 127.0.0.1

image.png

2.1.3 protected-mode no

image.png

2.1.4 指定端口

image.png

2.1.5 指定当前工作目录,dir

image.png

2.1.6. pid文件名字,pidfile

image.png

2.1.7 log文件名字,logfile

image.png

2.1.8 requirepass 123456

image.png

2.1.9 dump.rdb名字

image.png

② 配置 IP:131 的Redis

2.2.1 将129的redis配置拷贝到131里面

scp /root/myredis/redisNew.conf root@192.168.118.131:/root/myredis

image.png

修改文件名为redis131.conf

2.2.2 replicaof

image.png

2.2.3 指定端口 6381

image.png

2.2.4 配置主机密码 masterauth 123456

image.png

③配置 IP:132 的Redis

2.3.1 将129的redis配置拷贝到132里面

scp /root/myredis/redisNew.conf root@192.168.118.132:/root/myredis
修改文件名为redis132.conf

2.3.2 replicaof

image.png

2.3.3 指定端口 6382

image.png

2.3.4 配置主机密码 masterauth 123456

image.png

④具体演示

2.4.1启动129的Redis

cd /usr/local/redis/bin/

./redis-server /root/myredis/redisNew.conf

./redis-cli -a 123456

image.png

2.4.2启动131的Redis

cd /usr/local/redis/bin/

./redis-server /root/myredis/redis131.conf

./redis-cli -a 123456 -p 6381

image.png

2.4.3启动132的Redis

cd /usr/local/redis/bin/

./redis-server /root/myredis/redis132.conf

./redis-cli -a 123456 -p 6382

image.png

查看129.的日志

image.png

image.png

image.png

1.从机可以执行写命令吗?

不可以,从机只有读写命令的去哪里

2.主机shutdown后,从机会上位吗?

不会,会一直等待到主机启动

3.主机shutdown后,重启后主从关系还在吗?从机还能否顺利复制?

还在,能顺利复制

4.某台从机down后,master继续,从机重启后它能跟上大部队吗?

可以

2.2改换门庭

① 将131的主机配置去掉

image.png

②将132的主机配置去掉

image.png

③ 重启Redis,查看三个Redis的主从状态 info replication

info replication

image.png

image.png

image.png

三个Redis都为主机

③ 将131,132重新用命令设定主机129

slaveof 主库IP 主库端口

slaveof 192.168.118.129 6379

image.png

image.png

④ 主机新增数据,查看从机是否同步更新

image.png

image.png

需要注意的是,通过手动命令设置的主从关系,一旦重启就失效了,是临时命令

2.3 薪火相传

Redis的薪火相传是指上一个 Slave(从服务器)可以是下一个 slave 的 Master(主服务器),Slave 同样可以接收其他 slaves 的连接和同步请求,那么该 slave 作为了链条中下一个的 master,可以有效减轻 master 的写压力,去中心化降低风险。

实现步骤

①131配置主机129

./redis-server /root/myredis/redis131.conf

./redis-cli -a 123456 -p 6381

slaveof 192.168.118.129 6379

image.png

②132配置主机131

./redis-server /root/myredis/redis131.conf

./redis-cli -a 123456 -p 6381

slaveof 192.168.118.131 6381

image.png

③ 查看各个Redis的主从状态

info replication

image.png

image.png

image.png

④ 129主机新增数据查看各个从机数据

image.png

image.png

image.png

2.4自立为王

Redis的自立为王是指将当前的 slave(从服务器)数据库停止与其他 master(主服务器)数据库的同步关系,转成主数据库。执行该操作后,该 slave 数据库将不再接收其他 master 数据库的数据同步,而是自己成为一个独立的主数据库,可以接受其他 slave 数据库的连接和数据同步请求。简单来说就是从从机变为主机

实现步骤

SLAVEOF no one

image.png

三、总结

本文主要介绍了什么是主从复制,以及主从复制的四种模式分别为:一主二仆、薪火相传、改换门庭、自立为王的意思以及如何配置和效果演示。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:相比于 class 类组件,hooks 到底解决了什

发表于 2024-04-23

写在前面

相信使用 react 的小伙伴,都使用过 class 组件和函数式组件。

目前来说,函数式组件比较流行,基本上新开发的项目都是以函数式组件为主。

那相比于这两种方式,最直观的就是函数式组件引入了 hooks,在引入 hooks 之后函数式组件变得更加灵活,所以这个问题基本上就是考察你对两种组件的理解,今天就以我个人的理解给大家分享一下,希望可以帮助到大家!

1、状态逻辑复用

class 组件中状态逻辑难以复用,常常需要使用高阶组件或 Render Props。

hooks 引入了 useState、useReducer 等钩子,使得状态逻辑可以更容易地在不同组件之间共享和复用。

下面我们来举个栗子看看:

假设我们有两个组件 Counter1 和 Counter2,它们都需要使用 count 状态和 increment 函数。

类组件:

在这个例子中,两个组件有相同的状态逻辑,但由于类组件的特性,我们无法很好地抽象出这些逻辑进行复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
js复制代码class Counter1 extends React.Component {
state = { count: 0 };

increment = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
};

render() {
return (
<div>
<p>Counter 1: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}

class Counter2 extends React.Component {
state = { count: 0 };

increment = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
};

render() {
return (
<div>
<p>Counter 2: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}

使用hooks:

使用 hooks,我们可以将这些逻辑抽象为一个自定义 hook useCounter,这样在Counter1 和 Counter2中可以复用一个函数,而不需要重写多个相同的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
js复制代码import { useState } from 'react';

function useCounter() {
const [count, setCount] = useState(0);

const increment = () => setCount(prevCount => prevCount + 1);

return { count, increment };
}

function Counter1() {
const { count, increment } = useCounter();

return (
<div>
<p>Counter 1: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}

function Counter2() {
const { count, increment } = useCounter();

return (
<div>
<p>Counter 2: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}

2、生命周期管理

class 组件中的生命周期方法(如 componentDidMount、componentDidUpdate 等)往往包含多个不相关的逻辑,导致代码不够清晰。

hooks 提供了 useEffect 钩子,根据依赖项的不同情况可以模拟这几个生命周期函数,也就是一个 hooks 可以实现多个生命周期。

1.模拟 componentDidMount

componentDidMount() :在组件被挂载到 DOM 后调用,通常用于发送网络请求、订阅事件等初始化操作。

使用useEffect: 依赖项为空数组,初次渲染执行

1
2
3
4
5
js复制代码  useEffect(() => {
console.log('Component mounted');
// 执行数据获取操作
fetchData();
}, []);

2.模拟 componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot): 在组件更新后调用,通常用于处理更新后的操作,比如更新 DOM 或进行网络请求。

使用useEffect: 依赖项为 state,state 变化时执行

1
2
3
4
5
js复制代码  useEffect(() => {
console.log('Component updated');
// 执行其他副作用操作
document.title = `You clicked ${count} times`;
}, [count]);

3.模拟 componentWillUnmount

componentWillUnmount() :在组件即将从 DOM 中移除时调用,用于清理工作,比如取消订阅、清除定时器等。

使用useEffect: 在组件卸载时执行 return,清理副作用

1
2
3
4
5
6
7
js复制代码  useEffect(() => {
return () => {
console.log('Component unmounted');
// 执行清理操作
clearSubscription();
};
}, []);

3、性能优化

hooks 可以帮助避免不必要的重新渲染和函数创建。,通过 useMemo、useCallback 等钩子进行性能优化。

useMemo

用于记忆(缓存)计算结果,并在依赖项变化时重新计算。 这有助于避免不必要的重复计算,提高性能。

1
2
3
4
5
6
7
8
9
10
11
js复制代码    import React, { useMemo } from 'react';

function ExpensiveComponent({ prop1, prop2 }) {
// 使用 useMemo 缓存计算结果
const result = useMemo(() => {
// 进行昂贵的计算或处理
return prop1 + prop2;
}, [prop1, prop2]); // 依赖项数组

return <p>Result: {result}</p>;
}

useCallback

用于缓存回调函数,避免在每次渲染时重新创建。

接收一个回调函数和依赖项数组,返回缓存后的回调函数。

useCallback的依赖项参数用于指定哪些变量的变化会导致生成新的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码   import React, { useState, useCallback } from 'react';

function MemoizedComponent() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
// 使用 count 进行逻辑处理
console.log(count);
}, [count]);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent onClick={handleClick} />
</div>
);
}

function ChildComponent({ onClick }) {
// 使用缓存后的回调函数
return <button onClick={onClick}>Click me</button>;
}

在这个例子中,useCallback缓存了 handleClick回调函数,并且指定了依赖项数组为[count]。这意味着只有当 count发生变化时,handleClick才会生成新的回调函数。

总结

hooks 的出现 让 React 函数组件也能够更好的进行状态复用以及使用 useEffect 模拟生命周期的实现,通过使用useMemo、useCallback 避免不必要的重新渲染和函数创建,还提高了代码的可读性、复用性和可维护性。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

100 行代码打造小而美的 uni-app 请求库

发表于 2024-04-23

一. 前言

在开发 uni-app 项目时,我们经常需要对接后端接口进行数据请求。虽然 uni-app 框架本身提供了uni.request 用于发起请求,但在实际项目中,我们往往会封装一些请求库来简化请求的操作,提高代码复用性和可维护性。

本文将介绍基于 uni.request 实现一款小而美的请求工具,通过大约 100 行代码的实现,为 uni-app 项目打造一个简洁高效的请求库。

二. 请求库的设计思路

1. 了解 uni.request

在 uni-app 中使用 uni.request 来发起请求,但这种直接调用 uni.request 的方式在实际开发中存在一些不足之处,比如请求逻辑过于分散、请求参数拼接繁琐等。因此,我们希望通过封装一个简单的请求库来优化这一过程。

在 uni-app 使用 uni.request 的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码uni.request({
// url 仅为示例,并非真实接口地址。
url: "https://www.example.com/request",
data: {
text: "uni.request",
},
header: {
// 自定义请求头信息
"custom-header": "hello",
},
success: (res) => {
console.log(res.data);
this.text = "request success";
},
});

以上的这种请求方式是在 uni-app 项目中最常见的代码书写方式,很简单也很好理解,但是无法支持 promise API 式的请求,不支持请求和响应拦截器,同时也不支持全局的变量配置,逻辑分散,不便于维护!

不过我最近看官方文档,官方已经对部分 API 进行了 Promise 封装

详情参考:官方 Promise - 封装

2. 请求库的实现目标

image.png

我的目标核心是实现一个基于 Promise 的,轻量且强大的 http 网络库,基于以上这个目标,我的请求库应该具备以下基本功能:

  1. 提供统一的 Promise API
  2. 基于 uni.request,支持多种运行环境,浏览器 H5、小程序、APP 等
  3. 支持发起 GET、POST、PUT、DELETE 请求
  4. 支持请求/响应拦截器
  5. 支持设置请求的 header
  6. 支持处理请求的 loading 状态
  7. 支持对请求结果进行统一处理
  8. 支持链式调用

三. 请求库实现

基于以上的目标特性,我会一步一步实现请求库,而它的实现应该主要包括以下几个核心点:

  1. Config 配置项:设计一个统一的配置项对象 config,其中包含了 baseURL、header、data、method、dataType、responseType 等网络请求的基本配置信息。这样可以确保在发起网络请求时,可以统一管理这些配置项,方便进行全局设置和覆盖。
  2. 拦截器 Interceptors:设计请求拦截器和响应拦截器,提供 use 方法来添加拦截器处理函数。请求拦截器可以在发送请求前对请求参数进行处理,而响应拦截器则可以在收到响应后对响应结果进行处理。通过拦截器可以实现一些通用的网络请求处理逻辑,比如添加请求头、处理请求参数、统一处理响应结果等。
  3. 链式调用:在 setBaseURL、setHeader、setData 等方法中都使用了链式调用的方式,即每一个方法返回当前实例的引用,使得可以连续调用多个方法来设置请求的各个参数,提高代码的可读性和可维护性。
  4. Promise 处理:在 request 方法中使用了 Promise 对网络请求的结果进行处理,包括请求成功和失败的处理逻辑。同时也通过 Promise 来处理拦截器的返回结果,保证请求和拦截器的执行顺序和逻辑。
  5. 错误处理:在请求完成后,根据返回的状态码来判断请求的成功与失败,并通过不同的处理逻辑来处理不同状态下的响应结果。同时,在拦截器和请求的过程中,也会对错误进行处理,保证请求过程的稳定性和可靠性。

接下来我们分别对以上的核心点进行一一实现:

1. 构造函数

  • 在构造函数中,初始化 config 对象和 interceptors 对象,分别用来存储请求的配置信息和拦截器。
  • config 包含了 baseURL、header、data、method、dataType、responseType、success、fail 和 complete 等属性。
  • interceptors 包含了 request 和 response 拦截器,用来处理请求和响应的拦截操作。

通过构造函数,来定义统一的公共变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码class Http {
constructor() {
this.config = {
baseURL: "",
header: { "Content-Type": "application/json;charset=UTF-8" },
data: {},
method: "GET",
dataType: "json",
responseType: "text",
success() {},
fail() {},
complete() {},
};
}
}

2. 设置 BaseURL

定义 setBaseURL 方法,用来设置请求的基础 URL 统一请求前缀,将传入的 baseURL 参数赋值给 config.baseURL 属性。

1
2
3
4
js复制代码function setBaseURL(baseURL) {
this.config.baseURL = baseURL;
return this;
}

3. 设置请求 header

定义 setHeader 方法,用来设置请求的头部信息,将传入的 header 参数与原有的 header 属性合并更新。

1
2
3
4
js复制代码function setHeader(header) {
this.config.header = { ...this.config.header, ...header };
return this;
}

4. 设置请求体

定义 setData 方法,用来设置请求的数据,根据传入的数据类型判断是直接赋值还是合并更新到 config.data 属性中。

1
2
3
4
5
6
7
8
js复制代码function setData(data) {
if (Array.isArray(data)) {
this.config.data = data;
} else {
this.config.data = { ...this.config.data, ...data };
}
return this;
}

5. 设置拦截器

设计请求拦截器和响应拦截器,提供 use 方法来添加拦截器处理函数。请求拦截器可以在发送请求前对请求参数进行处理,而响应拦截器则可以在收到响应后对响应结果进行处理。

通过拦截器可以实现一些通用的网络请求处理逻辑,比如添加请求头、处理请求参数、统一处理响应结果等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码this.interceptors = {
response: {
use(handler, onerror, complete) {
this.handler = handler;
this.onerror = onerror;
this.complete = complete;
},
},
request: {
use(handler) {
this.handler = handler;
},
},
};

6. 基于 Promise 对象来实现

通过以上的配置,其实我们已经完成了一半,接下来,使用这些配置好的变量按需使用 uni.request 就可以了,是不是很简单?

而 request 方法应该包含以下内容:

  • request 方法用来发起请求,根据传入的 URL、数据和选项进行请求配置。
  • 先处理请求的基础配置,包括 URL、baseURL、header、method、dataType 等。
  • 接着处理拦截器,分为 request 和 response 拦截器,根据拦截器的设置进行相应的拦截操作。
  • 最后返回一个 Promise 对象,实现异步请求的链式调用,并根据请求结果执行相应的回调处理。

接下来,我们按部就班的来实现我们最重要的 request 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
js复制代码function request(url, data, options) {
if (!options) options = {};
// 请求URL
options.url = url;
// 请求baseURL:优先级为:实时传递的 > 公共配置的
options.baseURL =
options.baseURL !== undefined ? options.baseURL : this.config.baseURL;
// 请求头:合并公共配置与实时设置的header, 且优先级实时设置会覆盖公共配置的
options.header = { ...this.config.header, ...options.header };
// 请求方式:优先级为:实时传递的 > 公共配置的
options.method = options.method || this.config.method;
// 数据格式:默认json
options.dataType = options.dataType || this.config.dataType;
// 请求体:优先级为:实时传递的 > 公共配置的
if (isArray(data)) {
options.data = data;
} else {
options.data = { ...this.config.data, ...data };
}
// 拦截器处理
let interceptors = this.interceptors;
let requestInterceptor = interceptors.request;
let responseInterceptor = interceptors.response;
let requestInterceptorHandler = requestInterceptor.handler;
// 实现 Promise
return new Promise((resolve, reject) => {
function isPromise(p) {
return p && p.then && p.catch;
}

/**
* 公用方法
* If the request/response interceptor has been locked
*/
function enqueueIfLocked(promise, callback) {
if (promise) {
promise.then(() => {
callback();
});
} else {
callback();
}
}
// 响应回调
function onresult(handler, response, type) {
enqueueIfLocked(responseInterceptor.p, function () {
if (handler) {
// 统一添加请求信息
response.request = options;
let ret = handler.call(responseInterceptor, response, Promise);
response = ret === undefined ? response : ret;
}
if (!isPromise(response)) {
response = Promise[type === 0 ? "resolve" : "reject"](response);
}
response
.then((d) => {
resolve(d.data);
})
.catch((e) => {
reject(e);
});
});
}
// 请求完成回调,不管请求成功或者失败,都会走这个方法
options.complete = (response) => {
let statusCode = response.statusCode;
let type = 0;
if ((statusCode >= 200 && statusCode < 300) || statusCode === 304) {
// 请求成功
type = 0;
onresult(responseInterceptor.handler, response, type);
} else {
// 请求错误
type = -1;
onresult(responseInterceptor.onerror, response, type);
}
// 请求完成,无论请求成功、失败都会走的回调
onresult(responseInterceptor.complete, response, type);
};

// 开始请求
enqueueIfLocked(requestInterceptor.p, () => {
options = Object.assign({}, this.config, options);
options.requestId = new Date().getTime();
let ret = options;
if (requestInterceptorHandler) {
ret =
requestInterceptorHandler.call(
requestInterceptor,
options,
Promise
) || options;
}
if (!isPromise(ret)) {
ret = Promise.resolve(ret);
}
ret.then(
(d) => {
if (d === options) {
// url处理
d.url =
d.url && d.url.indexOf("http") !== 0 ? d.baseURL + d.url : d.url;
// 是否有追加restful url
d.url = d.restURL ? d.url + d.restURL : d.url;
// 使用 uni.request 正式请求, d 为所有的请求参数
uni.request(d);
} else {
resolve(d);
}
},
(err) => {
reject(err);
}
);
});
});
}

综上所述,以上请求库的设计思路是以封装、拓展性和易用性为核心,通过配置项、拦截器、链式调用和 Promise 处理等设计,实现了一个简单但功能完善的网络请求库,适用于处理各种类型的网络请求需求。

总的来说,以上代码实现了一个简洁易用的 HTTP 请求类,结合了配置管理、拦截器功能和异步请求处理,并提供了一些常用的方法来方便进行 HTTP 请求的管理和处理。

四. 使用方式

1. npm 安装

我已经将uni-http的请求库发到了npm上了,并且在多个项目中得到了良好的应用,可直接使用 npm 安装使用,通过如下方式进行安装:

1
2
3
4
5
bash复制代码// 安装
npm install @anyup/uni-http -S

// 更新
npm update @anyup/uni-http

点击查看在 npm 上完整的 uni-http 请求库,欢迎大家使用:npm 地址

2. 快速上手

实例化 Http 类

Http 为一个类,所以在使用前需要实例化 new,通过构造函数实例化一些必备参数

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码import { Http } from "@anyup/uni-http";

const http = new Http();

// 设置统一的头部
const header = { "Content-Type": "application/json" };

// 仅为示例 API 域名,使用时替换自己的接口域名即可
const baseURL = "https://demo.api.com";

// 设置 baseURL 和 header,支持链式调用
http.setBaseURL(baseURL).setHeader(header);

设置拦截器

  • 通过 interceptors.request.use 设置请求拦截器,主要对请求 header 的配置,比如 token 等。
  • 通过 interceptors.response.use 设置响应拦截器,如果需要,可以对所有的请求成功的响应数据做统一的业务处理,以简化代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
js复制代码// 设置请求拦截器
http.interceptors.request.use(
(request) => {
if (request.loading) {
// 如果配置了loading,请求开始需要显示
}
// 设置请求header
request.header["Authorization"] = "";
return request;
},
(error) => Promise.resolve(error)
);
// 设置响应拦截器
http.interceptors.response.use(
(response) => {
// 请求成功
if (!response.data) {
return Promise.reject(new Error("接口请求未知错误"));
}
// 其他业务处理
return Promise.resolve(response);
},
(error) => {
// 请求失败,业务处理
return Promise.reject(error);
},
(complete) => {
// 请求完成
if (complete.request.loading) {
// 如果配置了loading,请求完成需要关闭
}
// 其他业务处理
console.log("complete", complete);
}
);

请求示例

以登录请求为示例,通过传递 url,data,option,分别配置请求的 api 地址,请求参数,以及请求的个性化配置(是否需要请求 loading 等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
js复制代码// 登录请求示例,并配置请求时显示loading
function requestLogin(username, password) {
http
.request(
"/login",
{ username, password },
{ method: "POST", loading: true }
)
.then((res) => {
// 处理 response 响应
if (res.status === 1) {
console.log(res);
}
});
}

// 直接使用 get|post|put|delete 方式请求
function requestLogin1(username, password) {
// 也可以直接使用 post 方法
http.post("/login", { username, password }, { loading: true }).then((res) => {
// 处理 response 响应
if (res.status === 1) {
console.log(res);
}
});
}

// es6 await 风格 登录请求示例
async function requestLogin2(username, password) {
const res = await http.request(
"/login",
{ username, password },
{ method: "POST", loading: true }
);
// 处理 response 响应
if (res.status === 1) {
console.log(res);
}
}

五. 总结

通过以上代码的实现,我们成功构建了一个简洁高效的 uni-app 请求库,实现了一个小而美的 uni-app 请求库,能够满足常见的请求需求,同时除去注释,代码量也被控制在了 100 行左右。而且还实现了统一请求响应拦截器、全局错误处理等功能,使用它可以让我们的项目开发变得更加顺畅和便利。

希望本文能够对大家在 uni-app 项目中封装请求库有所帮助,让我们的开发工作更加高效和便捷。

在实际项目中,如果你有任何疑问或建议,欢迎联系我,我可以根据需求继续扩展这个请求库。

六. 结语

  • 如果你有任何问题,或者想要共同学习交流,欢迎通过沸点联系我:点击查看沸点
  • 剧透:篇幅有限,下篇文章我会继续以此为基础,说明如何实现批量生成 API 请求,简化代码量 99.99%,通过类的工厂模式,搞定繁琐的请求 function 定义,敬请关注!

源码

开源不易,欢迎 Start、Fork

  • Github
  • Gitee
  • npm

本文正在参加金石计划征文活动,欢迎点赞、收藏加关注,欢迎在评论区留言,让我们一起讨论进步!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

App 启动优化

发表于 2024-04-23

1 启动类型

冷启动(Cold Launch)

第一次打开 App 或者 App 已经被完全关闭后再次启动时发生的情况。在冷启动过程中,App 需要重新初始化,并加载必要的资源和数据。

热启动(Warm Launch)

App 已经在后台运行,再次启动 App 时发生的情况。App 在后台的存活时间,就是 App 能够执行热启动的最大时间间隔。

2 启动流程

Apple 官方的《WWDC Optimizing App Startup Time》 将 iOS 应用的启动分为 pre-main 阶段和 main 两个阶段。

2.2 pre-main 阶段

在 “pre-main” 阶段,iOS 应用程序的启动过程的关键步骤:

  1. 加载解析 info.plist 文件

系统会加载并解析应用程序的 Info.plist 文件,该文件包含了应用程序的配置信息,如应用程序的图标、启动图、权限要求等。

  1. 创建沙盒

在 iOS 8 及其之后的版本,每次启动应用程序时都会生成一个新的沙盒路径,用于存储应用程序的数据、设置文件等。这个沙盒是应用程序的私有存储空间。

  1. 权限检查

根据 Info.plist 文件中配置的权限要求,系统会检查应用程序所申请的各种权限状态,如访问相机、定位、通知等权限。

  1. 加载 Mach-O 文件并运行动态连接器(**Dynamic Link Editor,简称** dyld**)**
  1. dyld 寻找合适的 CPU 运行环境;
  2. dyld 加载程序依赖的动态库(系统动态库和第三方动态库)和源代码文件(.h/.m)编译生成目标文件(.o),并对这些动态库进行连接;
  3. 加载所有方法。在这个阶段,初始化 Runtime 并完成 Objective-C 的内存布局,包括加载所有类、方法等;
  4. 加载 C 函数;
  5. 加载类扩展和 Category。在这个阶段,Runtime 会对所有类结构进行初始化,包括加载类的扩展和 category;
  6. 加载 C++ 静态函数和执行 Objective-C 类的 +load 方法;
  7. 最后,调用 main 函数,应用程序的主逻辑开始执行。

Mach-O(Mach Object)是 macOS 和 iOS 等苹果操作系统上使用的一种可执行文件格式,类似于 Windows 上的 PE(Portable Executable)文件格式。

在编译过程中,源代码经过编译器的处理生成目标文件(.o 文件),然后链接器将这些目标文件以及所需的动态库链接成最终的 Mach-O 可执行文件。这个过程通常包括符号解析、重定位、符号表生成等步骤,最终生成一个完整的可执行文件,供操作系统加载和执行。

影响启动时间因素:

  1. 动态库的数量
  2. OC类的数量,Category的数量
  3. C/C++ 中的构造器函数
  4. C++ 静态对象
  5. OC类的 +load 方法

优化方案:

  1. 减少动态库加载,
  • 移除不必要的动态库
  • 合并多个动态库成一个动态库
  1. 减少无用的类或者方法
  • 清理项目中无用的类和方法,包括无用的 Category
  • 清理无用的静态变量,可以通过 AppCode 代码扫描
  1. 减少 C++ 静态对象的数量
  1. 检查 +load() 方法
  • 规避启动时的非必要操作,可以放到首屏渲染完成后再执行
  • 使用 +initialize() 方法替换

打印App的启动时间

在控制台打印对应的时间日志

Project→ Scheme → Edit Scheme → Run → Arguments → Environment Variables

设置参数 DYLD_PRINT_STATISTICS = 1,打印详细信息 DYLD_PRINT_STATISTICS_DETAILS = 1

1
2
3
4
5
6
7
8
9
yaml复制代码Total pre-main time: 770.45 milliseconds (100.0%)
dylib loading time: 150.62 milliseconds (19.5%)
rebase/binding time: 6.02 milliseconds (0.7%)
ObjC setup time: 9.22 milliseconds (1.1%)
initializer time: 604.57 milliseconds (78.4%)
slowest intializers :
libMainThreadChecker.dylib : 45.22 milliseconds (5.8%)
libglInterpose.dylib : 428.37 milliseconds (55.6%)
PuddingPlus : 189.86 milliseconds (24.6%)
  • dylib loading time:加载动态库,包括系统动态库和第三方动态库。
  • rebase/binding time:地址绑定。
  • ObjC setup time:注册 Objc 类,完成 Objc 类名与类的关系映射、维护 SEL-IMP 的关系映射,将 Protocol/Category 等注入宿主类方法列表中等。
  • initializer time:调用 Objc 类的 +load() 方法、调用 C++ 类的构造函数。

2.3 main 阶段

“main” 阶段可拆分为两个小阶段。

第一阶段:从 main 函数执行开始到应用程序代理 didFinishLaunchingWithOptions 函数执行完成。

影响启动时间因素:

  1. 执行 main 函数的耗时
  2. 执行 applicationWillFinishLaunching 函数的耗时

优化方案:

  1. 尽量不要在 main 函数中添加耗时的任务
  2. 按需加载,仅加载 App 启动和首屏渲染必要业务,对于非必要的业务移到首屏渲染完成后加载

第二阶段:首屏渲染阶段。

影响启动时间因素:

  1. 如加载、编辑、存储图片、文件等资源
  2. 运行时方法替换会带来时间消耗,如 +load() 执行有4毫秒的延迟

优化方案:

  1. 采用缓存策略,预加载数据减少用户等待的时间。
  • 如果首页的内容实时性不是特别强的,可以通过接口获取最新数据(并存储到磁盘),如果本次数据与已经渲染的数据不一致,则刷新用户界面。
  1. 延迟请求相关接口
  • 版本升级提醒或者拉取全局配置信息,如果是单独的接口,则可以延迟请求数据。全局配置信息可以在每次请求到远程最新数据时缓存到本地磁盘,应用启动后优先读取本地的数据,并且延迟请求远程数据。
  1. 拆分接或合并接口请求
  • 如果首页的数据是分多个模块(或微服务)且模块之间是弱相关的,把所有数据放在一个接口会增加后端查询量,导致接口响应过慢。可以按照逻辑划分,在子线程发出请求,数据响应后在子线程完成相关预处理,再回到主线程更新UI;哪个请求先回来就先渲染哪个模块的数据。
  • 如果首页的数据相关性很大,可以将有依赖的接口合并为一个接口,以减少接口请求的次数。
  1. 延迟初始化三方服务
  • 第三方服务SDK可以放在子线程完成行初始化,必须放在主线程初始化的,可以延迟几秒再初始化。
  1. 使用骨架屏等方案
  • 在网络请求过程中展示骨架屏会给用户一种数据即将展示出来的感觉。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot如何优雅的进行参数校验(一) Sprin

发表于 2024-04-23

SpringBoot如何优雅的进行参数校验

一.为什么要进行参数校验

在日常的开发过程中,我们常常需要对传入的参数进行校验,比如在web前后端分离项目中,参数校验有两个方面:

  • 前端进行参数校验
  • 后端进行参数校验

那这两种有什么区别呢?只完成一个可不可以呢?

答案是不可以的!

  1. 前端校验

前端校验主要是针对用户输入时,一些基础的错误进行提示,提升用户体验。比如:必填的选项,邮箱,网址的规则,如果前端校验不通过的话就不需要将请求转到后端。

但是:对于某些不走寻常路的用户,前端校验其实形同虚设。
2. 后端校验

后端校验是针对整个系统的业务逻辑进行校验,包含用户的权限,请求的参数等,校验的范围要大于前端.如果不做后端的校验会怎么样呢?比如前端向后端提交了一个只包含邮箱的请求,然后一些心术不正的人将该请求拷贝,改变参数为任意一段字符,然后重新发送请求,那么请求仍然能被处理,数据库就会有一条脏数据,借助此操作,可以完成一些对系统危害性更大的操作.

前端校验是辅助,后端校验是核心。后端校验必不可少。

二.后端参数校验方式

传统的参数校验一般采用大量的if else代码对参数进行一个一个的校验

传统的参数校验方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码public String checkUserDTO(UserDTO user) {

if (StringUtils.isEmpty(user.getName())) {
return "用户名不能为空";
}
if(StringUtils.isEmpty(user.getEmail())) {
return "邮箱不能为空";
}
if(StringUtils.isEmpty(user.getAccount())) {
return "账号不能为空";
}

if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
return "账号长度必须是6-11个字符";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return "邮箱格式不正确";
}
return "success";
}

这样的方式的话,如果参数多了那光参数校验就是一大堆,给人的感觉就是:不优雅,不专业,代码可读性也很差.

那么怎么能简单快捷的进行参数校验呢?

其实在SpringBoot项目中我们可以引入spring-boot-starter-validation来简单的进行参数校验.

三.spring-validation使用

引入依赖

粘贴请去除其中多余空格

1
2
3
4
5
bash复制代码< !--参数校验 -- >
< dependency >
< groupId>org.springframework.boot < /groupId>
< artifactId>spring-boot-starter-validation< /artifactId>
< /dependency>

参数注解列表

spring-validation是以注解的方式完成参数的校验的,而根据springboot官网的介绍,只要有JSR-303实现,例如Hibernate验证器,那么就能进行参数的校验.

这里列一下常用的注解:

  • @Null
+ 说明:被注释的元素必须为 `null`
+ 适用范围:`Object`
  • @NotNull
+ 说明:被注释的元素必须不为 `null`
+ 适用范围:`Object`
  • @AssertTrue
+ 说明:被注释的元素必须为 `true`
+ 适用范围:`boolean`、`Boolean`
  • @AssertFalse
+ 说明:被注释的元素必须为 `false`
+ 适用范围:`boolean`、`Boolean`
  • @Min(value)
+ 说明:被注释的元素必须是一个数字,其值必须大于等于指定的最小值
+ 适用范围:`BigDecimal`、`BigInteger`、`byte`、`Byte`、`short`、`Short`、`int`、`Integer`、`long`、`Long`
  • @Max(value)
+ 说明:被注释的元素必须是一个数字,其值必须小于等于指定的最大值
+ 适用范围:`BigDecimal`、`BigInteger`、`byte`、`Byte`、`short`、`Short`、`int`、`Integer`、`long`、`Long`
  • @DecimalMin(value)
+ 说明:被注释的元素必须是一个数字,其值必须大于等于指定的最小值
+ 适用范围:`BigDecimal`、`BigInteger`、`CharSequence`、`byte`、`Byte`、`short`、`Short`、`int`、`Integer`、`long`、`Long`
  • @DecimalMax(value)
+ 说明:被注释的元素必须是一个数字,其值必须小于等于指定的最大值
+ 适用范围:`BigDecimal`、`BigInteger`、`CharSequence`、`byte`、`Byte`、`short`、`Short`、`int`、`Integer`、`long`、`Long`
  • @Size(max, min)
+ 说明:被注释的元素的大小必须在指定的范围内
+ 适用范围:`CharSequence`、`Collection`、`Map`、`Array`
  • @Digits (integer, fraction)
+ 说明:被注释的元素必须是一个数字,其值必须在可接受的范围内
+ 适用范围:`BigDecimal`、`BigInteger`、`CharSequence`、`byte Byte`、`short Short`、`int Integer`、`long Long`
  • @Past
+ 说明:被注释的元素必须是一个过去的日期
+ 适用范围:`Date`、`Calendar`、`Instant`、`LocalDate`、`LocalDateTime`、`LocalTime`、`MonthDay`、`OffsetDateTime`、`OffsetTime`、`Year`、`YearMonth`、`ZonedDateTime`、`HijrahDate`、`JapaneseDate`、`MinguoDate`、`ThaiBuddhistDate`
  • @Future
+ 说明:被注释的元素必须是一个将来的日期
+ 适用范围:`Date`、`Calendar`、`Instant`、`LocalDate`、`LocalDateTime`、`LocalTime`、`MonthDay`、`OffsetDateTime`、`OffsetTime`、`Year`、`YearMonth`、`ZonedDateTime`、`HijrahDate`、`JapaneseDate`、`MinguoDate`、`ThaiBuddhistDate`
  • @Pattern(value)
+ 说明:被注释的元素必须符合指定的正则表达式
+ 适用范围:`CharSequence`、`null`
  • @Email
+ 说明:被注释的元素必须是电子邮箱地址
+ 适用范围:`CharSequence`
  • @Length
+ 说明:被注释的字符串的大小必须在指定的范围内
  • @NotEmpty
+ 说明:被注释的字符串的必须非空
  • @Range
+ 说明:被注释的元素必须在合适的范围内

具体使用

对于web服务来说,为防止非法参数对业务造成影响,在Controller层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  1. POST、PUT请求,使用requestBody传递参数;
  2. GET请求,使用requestParam/PathVariable传递参数。

下面我们简单介绍下requestBody和requestParam/PathVariable的参数校验

requestBody参数校验

POST、PUT请求一般会使用requestBody传递参数,这种情况下,后端使用DTO对象进行接收。只要给DTO对象加上@Validated注解就能实现自动参数校验。

requestBody参数校验需要两个步骤:

  • 在DTO字段上声明约束注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码public class UserDTO {

private Long Id;

@NotNull
@Length(min = 2, max = 10)
private String name;

@NotNull
@Length(min = 6, max = 20)
private String account;

@NotNull
@Email
private String email;
}
  • 在方法参数上声明校验注解
1
2
3
4
less复制代码public Result addUser(@RequestBody @Validated UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}

requestParam/PathVariable参数校验

GET请求一般会使用requestParam/PathVariable传参

将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解(如@Min等) 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码@Validated
@RestController
public class UserController {
// 路径变量
@GetMapping("{userId}")
public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
// 校验通过,才会执行业务逻辑处理
UserDTO userDTO = new UserDTO();
userDTO.setId(userId);
return Bizmessage.success(userDTO);
}

// 查询参数
@GetMapping("getByAccount")
public BizMessage getByAccount(@Length(min = 6, max = 11) @NotNull String account) {
// 校验通过,才会执行业务逻辑处理
UserDTO userDTO = new UserDTO();
userDTO.setAccount(account);
return Bizmessage.success(userDTO);
}
}

如果校验失败,则会抛出异常,通常会有统一异常处理

Hibernate Validator的功能是非常强大的,它还支持分组校验,嵌套校验,集合校验,自定义校验等多种校验方式,功能非常强大.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…282930…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%