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

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


  • 首页

  • 归档

  • 搜索

深入浅出【强缓存 & 协商缓存】 前言 http内容协商 后

发表于 2024-03-30

前言

你是否被问到过为何第二次打开百度官网速度比之前快,其实这个问题就是http的缓存问题,本期文章就带大家深入认识这两个缓存,让你明白如何实现,以及二者带来的效果,优缺点……

为了方便理解两个缓存,容我这里介绍下内容协商机制,顺便模拟一个情景,让你更好理解请求头响应头

http内容协商

这里我用node的http和url模块来实现,获取前端请求数据时的url,然后判断,若是指定的路径则返回hello world给前端

url模块用于做url路径的解析,类似koa-router路由

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码const http = require('http')
const url = require('url') // url模块 做url路径的解析

const server = http.createServer((req, res) => {
const { pathname } = url.parse(`http://${req.headers.host}${req.url}`)
if (pathname === '/') {
res.end('<h1>hellow world</h1>')
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

这样,我直接访问localhost:3000根路径就可以拿到后端的数据

1.png

或许你会疑问,我向前端返回的不是个html语句吗,怎么被解析了出来?因为这里我并没有设置响应头的格式,浏览器端会默认将html语句解析出来

我可以往头部加个'Content-Type': 'application/json'字段,这样就会读成json格式了

再判断下,若是其他路径返回not found 且404

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码const http = require('http')
const url = require('url')// url模块 做url路径的解析

const server = http.createServer((req, res) => {
const { pathname } = url.parse(`http://${req.headers.host}${req.url}`)
if (pathname === '/') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end('<h1>hellow world</h1>')
} else {
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end('<h1>Not Found</h1>')
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

如图

2.png

好了,这就是很简单的一个http服务

http1.0为何要搞个响应头,请求头?

这就是因为后端返回前端的文件格式不可能是一种,为了方便前端解析数据,请求头中就写入这些字段信息告诉前端应该以何种格式去读取后端返回的数据

内容协商是什么

请求头和响应头就是http的内容协商机制,很好理解,就是前后端协商好如何解析数据

另外,其实前端是可以告诉后端我期望接收到的是什么数据,后端拿到req.headers.accept可以看到前端期望的格式,前端没写就是默认的,如下这样

3.png

因此后端拿到这个数据后可以判断是否有指明的格式,比如json,前端表明需要这个格式,我就返回一个数据给前端

下面我继续改下,如果想要的东西没有json格式,我就返回一个html语句给前端

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
javascript复制代码const http = require('http')
const url = require('url')// url模块 做url路径的解析

const responseData = {
ID: '2003',
Name: '海豚',
RegisterDate: '2024年3月30日'
}

function toHTML(data) { // 将数据转成html语句
return `
<ul>
<li><span>账号:</span><span>${data.ID}</span></li>
<li><span>昵称:</span><span>${data.Name}</span></li>
<li><span>注册时间:</span><span>${data.RegisterDate}</span></li>
</ul>
`
}

const server = http.createServer((req, res) => {
const { pathname } = url.parse(`http://${req.headers.host}${req.url}`)
if (pathname === '/') {
const accept = req.headers.accept
if (accept.indexOf('application/josn') !== -1) {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(responseData)) // 响应数据无法以对象传输,因此要先转成json格式
} else { // 前端想要的数据不是json,那么我就把这个html语句给前端,并告诉前端以html格式加载
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(toHTML(responseData))
}

} else {
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end('<h1>Not Found</h1>')
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

字符串也是有indexOf这个api的,同数组一致,不存在就是-1

前端因此访问根路径就可以拿到html

4.png

现在你对内容协商已经有了更深刻的认识了,就是通过请求头和响应头一起商量着来如何处理数据格式

接下来我们聊聊如何使用http服务向前端返回一个静态资源文件,这就是早期的前后端不分离开发方式,当初的前端工作量很少,就是切图仔,切好页面后给到后端java,后端将java嵌到页面中,然后返回给浏览器用户

不分离开发方式不low,它也可以把页面写的很精致,问题主要是出在前端工作量太少了,导致开发效率很低。

前后端不分离的开发方式也就是服务端渲染

后端返回静态资源文件给前端

这里实现的过程中顺带写了很多用不上的模块,纯粹是为了复习node,不愿意看的小伙伴可以直接跳到缓存那里

接下来,我写一个html文件,里面放上一个本地图片。这个html文件就是一个静态文件,现在我需要前端访问localhost:3000/index.html时,前端可以拿到这个页面

这就需要后端node再引入fs模块,读到html文件,并且需要拿到前端输入的url,这需要用上path模块解析出绝对路径最后读取到整个文件的绝对路径,然后判断资源是否存在

再用fs.statSync拿到文件的详细信息,比如创建时间修改时间等;还可以判断一下前端请求的资源是文件还是文件夹,若是文件夹可以拼接一下,若是文件,就把文件读到返回给前端,文件被读出来是一个十六进制buffer流的形式

前端是不知道buffer流这个格式的,因此后端需要重新写下响应头

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复制代码const http = require('http')
const url = require('url')// url模块 做字符串url路径的解析
const path = require('path')// path 解析路径 解析绝对相对
const fs = require('fs') // 文件模块

const server = http.createServer((req, res) => {
// 将前端请求的地址转换成真实的url,再拼接www这个路径,最后读取整个文件的绝对路径
let filePath = path.resolve(__dirname,path. join('www', url.fileURLToPath(`file:/${req.url}`))) // __dirname 绝对路径 macOS 需要 ///
if (fs.existsSync(filePath)) { // 判断资源是否存在
const stats = fs.statSync(filePath) // statSync读取文件的详细参数,比如创建时间等
console.log(stats);
const isDir = stats.isDirectory() // 是文件(false)还是文件夹(true)
if (isDir) { // 文件夹
filePath = path.join(filePath, 'index.html')
}
if (!isDir || fs.existsSync(filePath)) { // 文件
const content = fs.readFileSync(filePath) // 读取文件

res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
return res.end(content) // node 默认读文件就是 buffer 16进制流 并且返回给前端 前端可以读出来
}
}
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end('<h1>Not Found</h1>')
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

好了,目前前端是可以拿到这个静态资源的,但是有个问题,我们检查接口的时候发现,这个图片的Response是乱码的

5.png

乱码也是可以理解的,刚刚后端的写法是,读到了index.html后返回的格式就是text/html,而这个图片是进入到index.html后再次请求的,这个请求我们并没有写格式

乱码依旧可以看到图片这纯粹是因为谷歌浏览器比较强大,换做是别的浏览器这个图片可能就加载不出了

解决图片乱码

解决这个问题我们可以拿到请求过程中的请求路径,比如这里的图片是jpeg格式,那么就可以拿到jpeg,用上path.parse(filePath)进行解构,拿到ext即可

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
javascript复制代码const http = require('http')
const url = require('url')// url模块 做字符串url路径的解析
const path = require('path')// path 解析路径 解析绝对相对
const fs = require('fs') // 文件模块

const server = http.createServer((req, res) => {
// 将前端请求的地址转换成真实的url,再拼接www这个路径,最后读取整个文件的绝对路径
let filePath = path.resolve(__dirname,path. join('www', url.fileURLToPath(`file:/${req.url}`))) // __dirname 绝对路径
if (fs.existsSync(filePath)) { // 判断资源是否存在
const stats = fs.statSync(filePath) // statSync读取文件的详细参数,比如创建时间等
console.log(stats);
const isDir = stats.isDirectory() // 是文件(false)还是文件夹(true)
if (isDir) { // 文件夹
filePath = path.join(filePath, 'index.html')
}
if (!isDir || fs.existsSync(filePath)) { // 文件
const content = fs.readFileSync(filePath) // 读取文件
const { ext } = path.parse(filePath) // 解析路径

if (ext === '.jpeg') {
res.writeHead(200, {'Content-Type': 'image/jpeg'})
} else {
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'})
}

return res.end(content) // node 默认读文件就是 buffer 16进制流 并且返回给前端 前端可以读出来
}
}
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end('<h1>Not Found</h1>')
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

好了,现在图片没有乱码了,正常情况下就是不会显示任何东西

6.png

但是问题来了,请求到页面后,页面会有很多种格式,难道每个格式都分别判断下然后给特定的格式吗,自己写肯定不现实,这里用轮子

mime-types

mime-types - npm (npmjs.com)

安装mime-types,他可以帮我们自动识别前端请求的文件格式,然后后端写入响应头对应的格式

写法最终如下

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
javascript复制代码const http = require('http')
const url = require('url')// url模块 做字符串url路径的解析
const path = require('path')// path 解析路径 解析绝对相对
const fs = require('fs') // 文件模块
const mime = require('mime-types')

const server = http.createServer((req, res) => {
// 将前端请求的地址转换成真实的url,再拼接www这个路径,最后读取整个文件的绝对路径
let filePath = path.resolve(__dirname,path. join('www', url.fileURLToPath(`file:/${req.url}`))) // __dirname 绝对路径
if (fs.existsSync(filePath)) { // 判断资源是否存在
const stats = fs.statSync(filePath) // statSync读取文件的详细参数,比如创建时间等
console.log(stats);
const isDir = stats.isDirectory() // 是文件(false)还是文件夹(true)
if (isDir) { // 文件夹
filePath = path.join(filePath, 'index.html')
}
if (!isDir || fs.existsSync(filePath)) { // 文件
const content = fs.readFileSync(filePath) // 读取文件
const { ext } = path.parse(filePath) // 解析路径
console.log(ext);

res.writeHead(200, { 'Content-Type': mime.lookup(ext) })

return res.end(content) // node 默认读文件就是 buffer 16进制流 并且返回给前端 前端可以读出来
}
}
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end('<h1>Not Found</h1>')
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

这样,前端请求的后缀是什么,它能自动帮我们识别出对应的响应头字段格式

node-pipe

刚才给前端返回文件的时候是以node读取的buffer格式,然后res.end返回给前端,当然,其实我们也可以用node中的pipe来返回给前端,这个读取到的文件是流类型,不同于buffer

1
2
scss复制代码const fileStream = fs.createReadStream(filePath) // 读文件读成流类型
fileStream.pipe(res) // 将流类型资源汇入到响应体中

不过如今的新版node好像弃用掉了这个方法

好了,现在进入今天的正题,强缓存和协商缓存

强缓存

上面返回静态资源的情景已经差不多实现了,但是有个问题,我这个页面的图片就是不会去变更了,但是我每次刷新页面这个图片都会去请求一下,且耗时5ms,如下

1.gif

这就像是百度的首页,百度的logo基本上不会去变更,因此也非常没有必要重新发请求拿到这个logo

我们重复刷新百度首页,检查图片的请求基本上都是耗时0ms,这就是因为百度已经做好了http缓存

优化这个东西就是http的缓存

我们现在就去看下百度首页,刷新地址,来到result.png的请求,这就是百度的logo图标,查看标头

7.png

里面有个Cache-Control字段,里面有个最大期限为315360000,单位是s,我们算下是多久

8.png

3650天,就是10年,其实这就是百度将这个请求缓存了10年,10年内不会发接口请求,拿到这个logo都是从本地中读取,因此耗时0毫秒

其实这就是强缓存

如何实现

实现起来非常简单,只需要写响应头时加入下面这个字段即可

'Cache-Control': 'max-age=xxx'

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
javascript复制代码const http = require('http')
const url = require('url')// url模块 做字符串url路径的解析
const path = require('path')// path 解析路径 解析绝对相对
const fs = require('fs') // 文件模块
const mime = require('mime-types')

const server = http.createServer((req, res) => {
let filePath = path.resolve(__dirname, path.join('www', url.fileURLToPath(`file:/${req.url}`)))
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath)

const isDir = stats.isDirectory()
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
if (!isDir || fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath)
const { ext } = path.parse(filePath)

res.writeHead(200, {
'Content-Type': mime.lookup(ext),
'Cache-Control': 'max-age=86400' // 一天
})

return res.end(content)
}
}
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end('<h1>Not Found</h1>')
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

这里强缓存了一天,我们先看下效果

2.gif

直接变成了0ms,已经实现了~

缺点

我们现在替换一个图片,命名与之前一致

我们再去刷新,大家可以先猜下浏览器那边是否会刷新?

答案是不会的,那个图片已经被缓存到浏览器本地了,服务器的资源发生变更,强缓存是不知道的

这个时候我们可以强制刷新来获取更改后的图片

强制刷新 === shift + F5

另外,大家是否发现,我后端写的强缓存,前端重新刷新,只缓存住了图片资源,index.html没有被缓存,index.html同样也是静态资源

我们查看index.html的响应头,里面同样有这个缓存字段

9.png

响应头有这个可以理解,请求头我们刚才并没有动啊,请求头是前端设置的,浏览器居然自动给他添加了这个字段并且max-age为0,浏览器特殊对待index.html并不是因为它是个html文件类型,而是因为这个请求是在输入url后发起的

前端发请求可以看成两部分,前部分就是输入url后发起的get请求,之后的请求就是页面加载时碰到需要资源的请求,也就是ajax请求

浏览器这么做其实也可以理解,输入url后的请求怎么能被强缓存起来,请求的东西一定是实时的,最新的

这也是强缓存的缺陷,就是无法缓存输入url后的get请求,只能缓存

输入url的请求一定是get请求,没有post请求

总结

实现:

设置响应头:

'Cache-Control': 'max-age=xxx'

缺点:

  1. 服务器资源命名不变但是文件变了,浏览器不会去更新
  2. 无法缓存输入url后的的get请求

协商缓存

你肯定又会疑惑了,既然百度的logo被缓存了10年,但是每逢过节,那个logo又都会变,这是怎么做到的,说好的10年呢,10年按道理不会重新发请求了

这就要靠协商缓存来解决了

如何实现

在强缓存实现的基础上往响应头添加如下字段

'Last-Modified': stats.mtimeMs

我们再打印下stats,里面就是文件的各种信息,其中mtimeMs是文件的修改时间,ctimeMs是文件的创建时间

10.png

Last-Modified就是上次修改的意思,将这个字段加入到响应头给到前端有何用呢

我们查看index.html的接口请求

11.png

它的请求头多了个If-Modified-Since字段,并且这个字段的值就是mtimeMs,最后的数字都是2793,也就是说浏览器已经拿到了后端修改文件的时间戳

既然前端记录到了这个时间,那么我现在去index.html中添加个标题,那么它的变化就会被操作系统记录到,也就是说后端的mtimeMs就会变更,试着打印下

12.png

果不其然,后端的mtimeMs变了

因此我们如果拿到前端的请求头中的这个字段与后端实时的mtimeMs进行对比就可以判断出文件是否修改,没有修改就将状态码改成304,304的含义就是资源未修改

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
javascript复制代码const http = require('http')
const url = require('url')// url模块 做字符串url路径的解析
const path = require('path')// path 解析路径 解析绝对相对
const fs = require('fs') // 文件模块
const mime = require('mime-types')

const server = http.createServer((req, res) => {
let filePath = path.resolve(__dirname, path.join('www', url.fileURLToPath(`file:/${req.url}`)))
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath)
console.log(stats);
const isDir = stats.isDirectory()
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
if (!isDir || fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath)
const { ext } = path.parse(filePath)
const timeStamp = req.headers['if-modified-since']
let status = 200
if (timeStamp && Number(timeStamp) === stats.mtimeMs) { // 该资源没有被修改
status = 304 // 资源未修改
}

res.writeHead(status, {
'Content-Type': mime.lookup(ext),
'Cache-Control': 'max-age=86400', // 一天
'Last-Modified': stats.mtimeMs // 时间戳 资源修改的时间
})

return res.end(content)
}
}
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end('<h1>Not Found</h1>')
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

好了,协商缓存已经实现了,现在去看下index.html

3.gif

看到没,index.html的状态码变更为304,也就是说资源未修改,并且大小由原来的611B缩小到了203B

html文件不可能被缓存到大小为0

这样,但状态码为304时,浏览器就会自动从缓存中读取这个资源了

现在就解决了强缓存无法缓存输入url发的请求问题

缺点

但是协商缓存依旧无法解决服务器资源命名不变但是文件变了,浏览器不会去更新这个问题

这个问题其实解决办法是通过哈西值,就是文件名最后接一个hash值,只要资源被修改,hash值一定会变更,这样文件名就会被修改,文件名被修改,浏览器就会重新请求

还有个很少见的问题,就是我不小心修改了服务端的资源后立马反悔了,又给改成原样了,mtimeMs依旧会变,因此前端又会重新请求

这个问题的根本原因在于返回给前端的是最近一次的文件修改时间,若这个东西是文件本身就不会有这个问题了

etag

刚才说协商缓存的实现需要加上这个Last-Modified,这里我换成etag来实现,其value是签名,可以完整代表这个文件本身

这需要我们下载checksum依赖

这个依赖可以帮我们判断文件是否被修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码
checksum.file(filePath, (err, sum) => {
const resStream = fs.createReadStream(filePath)
sum = `"${sum}"`
if (req.headers['if-none-match'] === sum) {
res.writeHead(status, {
'Content-Type': mime.lookup(ext),
'Cache-Control': 'max-age=86400',
'etag': sum // 签名(文件资源)也可以做协商缓存
})
} else {
res.writeHead(200, {
'Content-Type': mime.lookup(ext),
'etag': sum
})
return resStream.pipe(res)
}
})

具体实现这里就不再介绍~

总结

过程

后端先将最近修改文件的时间戳mtimeMs给到前端,前端请求头中多出一个If-Modified-Since字段,并且其值就是后端给的mtimeMs,然后后端再拿到前端的请求体中的这个字段与实时的mtimeMs进行比较,如果不一致就是说明资源变更了,正常读取数据返回给前端,如果没有变更,返回状态码304给前端,浏览器此时就会从缓存中读取静态资源

实现:

设置响应头:

'Cache-Control': 'max-age=xxx'

'Last-Modified': stats.mtimeMs

缺点:

服务器资源命名不变但是文件变了,浏览器不会去更新(通过hash值来解决)

优点:

  1. 可以缓存输入url后的请求
  2. 服务器资源变更可以立即拿到这个资源再进行缓存

最后

像是页面的logo,我们肯定需要缓存,每次请求就会很浪费资源,因此强缓存就解决了这个问题,但是强缓存有个缺陷,输入url的get请求是无法强缓存的,一般输入url后碰到需要请求的资源可以再强缓存,强缓存还有个问题就是无法拿到服务端最新的变更,你不可能让用户去强制刷新页面

协商缓存解决了强缓存无法缓存输入url的请求这个问题,就是可以缓存输入url的请求,协商缓存的前端请求头有个if-modified-since字段(先是后端给响应头添加了last-modified字段,里面存放了时间戳),后端可以判断文件的修改时间和这个字段的时间是否一致,一致就是304未修改状态码,不一致说明静态资源发生了变更需要重新请求

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!

本文转载自: 掘金

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

分支管理:master,release,hotfix,sit

发表于 2024-03-30

背景

从一开始的svn到后来接触到git,到目前已经和git打交道比较多了,突然觉得可以把项目中所用到一些分支的管理方式分享出来,希望帮助到大家。

分支介绍

现在使用git一般很少是单个分支来使用,通常是多个分支来进行,接下来我以我最新的项目中所用到的分支,来做一些介绍。

master:

  • master分支代码只能被release分支分支合并,且合并动作只能由特定管理员进行此操作。
  • master分支是保护分支,开发人员不可直接push到远程仓库的master分支

release:

  • 命名规则:release/*,“*”一般是标识项目、第几期、日期等
  • 该分支是保护分支,开发人员不可直接push,一般选定某个人进行整体的把控和push
  • 该分支是生产投产分支
  • 该分支每次基于master分支拉取

dev

  • 这个是作为开发分支,大家都可以基于此分支进行开发
  • 这个分支的代码要求是本地启动没问题,不影响其他人的代码

hotfix

  • 这个分支一般是作为紧急修复分支,当前release发布后发现问题后需要该分支
  • 该分支一般从当前release分支拉取
  • 该分支开发完后需要合并到release分支以及dev分支

feat

  • 该分支一般是一个长期的功能需要持续开发或调整使用
  • 该分支基于release创建或者基于稳定的dev创建也可以
  • 一般开发完后需要合并到dev分支

分支使用

以上是简单介绍了几个分支,接下来我针对以上分支,梳理一些场景,方便大家理解。

首先从master创建一个release分支作为本次投产的分支,然后再从master拉取一个dev分支方便大家开发,dev分支我命名为:dev/soe,然后我就在这个分支上进行开发,其他人也是这样。

然后当我开发完某个任务后,又有一个任务,但是呢,这个任务需要做,只是是否要上这次的投产待定,所以为了不影响到大家的开发,我就不能在dev分支进行开发了,此时我基于目前已经稳定了的dev分支创建了一个feat分支,叫做:feat/sonar,主要是用来修复一些扫描的问题,在此期间,如果我又接到了开发的任务,仍然可以切换到dev来开发,并不影响。

当开发工作完成后,并且也基于dev分支进行了测试,感觉没问题之后,我就会把dev分支的代码合并到release上。

当release投产之后,如果业务验证过也没有问题,那么就可以由专人把release合并到master了,如果发现了问题,那么此时就需要基于release创建一个hotfix分支,开发人员在此分支进行问题的修复,修复完成并测试后,合并到release分支和sit分支。然后再使用release分支进行投产。

总结

以上就是我在项目中,对分支的使用,我觉得关于分支使用看团队以及项目的需要,不必要定死去如何如何,如果有的项目不规定必须要release投产,那么hotfix就不必使用,直接release修改完合并也未尝不可,所以大家在项目中是如何使用的呢?可以评论区一起讨论分享。

致谢

感谢你的耐心阅读,如果我的分享对你有所启发或帮助,就给个赞呗,很开心能帮到别人。

本文转载自: 掘金

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

为啥微信的更新信息大多数都是:修复已知问题,但是开发中不建议

发表于 2024-03-30

背景

可能大家平常有意或无意的注意到,微信的更新日志经常是:解决了一些已知问题。

image.png

但是开发人员日常开发中,提交的信息一般会避免这样,反而会要求把提交信息写的比较详细。

原因

首先,从用户体验的角度来看,频繁地列出所有已知问题及其修复情况可能会让用户感到困惑或担忧,尤其是当这些问题涉及到隐私、安全等敏感话题时。其次,微信作为一个庞大的社交平台,其功能众多,更新日志如果详细到每一项改动,不仅对普通用户来说难以理解,也会增加开发团队的工作量。此外,微信的更新往往伴随着大量的内部优化和结构调整,这些内容对于普通用户而言并不重要,也不易被察觉。

而对于开发人员来说,commit信息一是给自己以后看,通过提交信息就可以知道自己修改了哪些内容,其二就是给其他开发人员查看,从而知道别人修改了哪些地方。

查看提交日志

那么关于微信相关的我们不再赘述,主要针对开发人员的提交信息进行一些讨论,比如如何查看提交信息呢,在idea中可以直接查看git log,也可以通过命令git log来进行查看,或者也可以使用命令git show commitHash针对每一个提交信息进行详细的查看,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码$ git show 32557725d91403ca8e5ae520a5f82a516791f5c0
commit 32557725d91403ca8e5ae520a5f82a516791f5c0
Author: test1 <test1@some.com>
Date: Wed Mar 20 16:53:56 2024 +0800

b5 commit

diff --git a/b.txt b/b.txt
index 86bd041..be62feb 100644
--- a/b.txt
+++ b/b.txt
@@ -2,4 +2,6 @@
22222

33333
-44444
\ No newline at end of file
+44444
+
+55555
\ No newline at end of file

本人代码提交方式

关于代码提交规范,相关的文章有很多,我在此先不多说,只是把我平常所用到的描述一下,大家可以参考。

针对每次的功能涉及到几个方面:代码优化,新功能开发,bug修复等。

  • 针对新功能开发:一般是git commit xxxx.java -m 'feat:用户登录限制只允许特定IP地址来登录管理员账号'
  • 针对bug修复:一般是git commit xxx.java -m 'fix:用户登录后看不到自己的工作任务'
    针对
  • 针对代码优化:,则是git commit xxx.sql -m 'refactor:把原来不存在的用户显示为ID账号,而非null'

总结

其实写好commit信息有较多好处,不单单是上面提到的个人追溯问题容易以及同事协作简单。以下我列出来我想到的。

  • 通过commit信息方便自己进行日报周报的总结
  • 方便进行某些问题的回退
  • 方便快速的梳理功能,以便于线上环境的验证
  • 有益于自己代码的精简提交,如果多个文件一起提,那么commit信息可能就会更复杂

致谢

以上就是从微信的一个更新日志,进而针对开发人员的commit信息进行了一些简单阐述。感谢你的耐心阅读,如果我的分享对你有所启发或帮助,就给个赞呗,很开心能帮到别人。

本文转载自: 掘金

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

并发编程神器CompletableFuture高级用法与实战

发表于 2024-03-29

一、异步任务的异常处理

  • 如果在 supplyAsync 任务中出现异常,后续的 thenApply 和 thenAccept 回调都不会执行,CompletableFuture 将传入异常处理。
  • 如果在第一个thenApply任务中出现异常,第二个 thenApply 和最后的 thenAccept 回调不会被执行,CompletableFuture 将转入异常处理,依次类推。

1、exceptionally

exceptionally 用于处理回调链上的异常, 回调链上出现的任何异常,回调链不继续向下执行,都在exceptionally中处理异常。

1
java复制代码CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

image.png
因为 exceptionally 只处理一次异常,所以常常用在回调链的末端。

2、handle

CompletableFuture API 还提供了一种更通用的方法handle() 表示从异常中恢复 handle() 常常被用来恢复回调链中的一次特定的异常,回调链恢复后可进一步向下传递。

1
java复制代码CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> handle = CompletableFuture.supplyAsync(() -> {
int i = 1 / 0;
return "result";
}).handle((s, e) -> {
if (Objects.nonNull(e)) {
System.out.println("出现异常:" + e.getMessage());
return "unKnown";
}
return s;
});
CommonUtils.printTheadLog("main continue");
String ret = handle.get();
CommonUtils.printTheadLog("ret = " + ret);
CommonUtils.printTheadLog("main end");
}

异步任务不管是否发生异常,handle方法都会执行。所以,handle核心作用在于对上一步异步任务进行现场修复。
案例:对回调链中的一次异常进行恢复处理

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
java复制代码public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> handle = CompletableFuture.supplyAsync(() -> {
int i = 1 / 0;
return "result1";
}).handle((s, e) -> {
if (Objects.nonNull(e)) {
System.out.println("出现异常:" + e.getMessage());
return "unKnown1";
}
return s;
}).thenApply(reuslt -> {
String str = null;
int length = str.length();
return reuslt + "result2";
}).handle((s, throwable) -> {
if (Objects.nonNull(throwable)) {
System.out.println("出现异常" + throwable.getMessage());
return "unknown2";
}
return s;
}).thenApply(s -> s + "result3");
CommonUtils.printTheadLog("main continue");
String ret = handle.get();
CommonUtils.printTheadLog("ret = " + ret);
CommonUtils.printTheadLog("main end");
}

和以往一样,为了提供并行化,异常处理可以方法单独的线程执行,以下是它们的异步回调版本:

1
2
3
4
5
6
7
java复制代码CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
CompletableFuture<T> exceptionallyAsync(Function<Throwable, ? extends T> fn) // jdk17+
CompletableFuture<T> exceptionallyAsync(Function<Throwable, ? extends T> fn, Executor executor) // jdk17+

CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)
CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor)

二、异步任务的交互

异步任务的交互是指在异步任务获取结果的速度相比较中,按一定的规则(先到先得)进行下一步处理。

1、applyToEither

applyToEither() 把两个异步任务做比较,异步任务先得到结果的,就对其获得的结果进行下一步操作。

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
java复制代码public static void main(String[] args) throws ExecutionException, InterruptedException {
//异步任务1
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
int x = new Random().nextInt(3);
CommonUtils.sleepSecond(x);
CommonUtils.printTheadLog("任务1耗时" + x + "秒");
return x;
});

//异步任务2
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
int y = new Random().nextInt(3);
CommonUtils.sleepSecond(y);
CommonUtils.printTheadLog("任务2耗时" + y + "秒");
return y;
});

//哪个异步任务结果先到达,使用哪个异步任务的结果
CompletableFuture<Integer> future3 = future1.applyToEither(future2, result -> {
CommonUtils.printTheadLog("最先到达的是" + result);
return result;
});
CommonUtils.sleepSecond(4);
Integer ret = future3.get();
CommonUtils.printTheadLog("ret ="+ret);
}

以下是applyToEither 和其对应的异步回调版本:

1
2
3
java复制代码CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)
CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn)
CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn,Executor executor)

2、acceptEither

acceptEither()把两个异步任务做比较,异步任务先到结果的,就对先到的结果进行下一步操作(消费使用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public static void main(String[] args) throws ExecutionException, InterruptedException {
//异步任务1
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
int x = new Random().nextInt(3);
CommonUtils.sleepSecond(x);
CommonUtils.printTheadLog("任务1耗时" + x + "秒");
return x;
});

//异步任务2
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
int y = new Random().nextInt(3);
CommonUtils.sleepSecond(y);
CommonUtils.printTheadLog("任务2耗时" + y + "秒");
return y;
});

//哪个异步任务结果先到达,使用哪个异步任务的结果
future1.acceptEither(future2, result -> {
CommonUtils.printTheadLog("最先到达的是" + result);
});
CommonUtils.sleepSecond(4);
}

以下是acceptEither和其对应的异步回调版本:

1
2
3
java复制代码CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action,Executor executor)

3、runAfterEither

如果不关心最先到达的结果,只想在有一个异步任务完成时得到完成的通知,可以使用 runAfterEither()。

1
2
3
java复制代码CompletableFuture<Void> runAfterEither(CompletionStage<?> other,Runnable action)
CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action)
CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public static void main(String[] args) {
//异步任务1
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
int x = new Random().nextInt(3);
CommonUtils.sleepSecond(x);
CommonUtils.printTheadLog("任务1耗时" + x + "秒");
return x;
});
//异步任务2
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
int y = new Random().nextInt(3);
CommonUtils.sleepSecond(y);
CommonUtils.printTheadLog("任务2耗时" + y + "秒");
return y;
});
future1.runAfterEither(future2, () -> {
CommonUtils.printTheadLog("有一个异步任务执行完成");
});
CommonUtils.sleepSecond(4);
}

三、get()和join()的区别

get() 和 join() 都是CompletableFuture提供的以阻塞方式获取结果的方法。
使用时,我们发现,get() 抛出检查时异常,需要程序必须处理;而join() 方法抛出运行时异常,程序可以不处理。所以, join()更适合用在流式编程中。

四、ParallelStream VS CompletableFuture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class MyTask {

private int duration;

public MyTask(int duration) {
this.duration = duration;
}

// 模拟耗时的长任务
public int doWork() {
CommonUtils.printTheadLog("doWork");
CommonUtils.sleepSecond(duration);
return duration;
}
}

1、使用串行流执行并统计总耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码 public static void main(String[] args) {
//需求:创建10个MyTask耗时的任务,统计它们执行完的总耗时
//方案一:在主线程中使用串行执行
//step 1: 创建1日个MyTask对象,每个任务持续1s,存入List集合
IntStream intStream = IntStream.range(0, 10);
List<MyTask> tasks = intStream.mapToObj(item -> {
return new MyTask(1);
}).collect(Collectors.toList());

//step 2: 执行10个MyTask,统计总耗时
long start = System.currentTimeMillis();
List<Integer> results = tasks.stream().map(myTask -> {
return myTask.doWork();
}).collect(Collectors.toList());

long end = System.currentTimeMillis();

double costTime = (end - start) / 1000.0;
System.out.printf("processed %d tasks %.2f second", tasks.size(), costTime);
}

2、使用并行流执行并统计总耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public static void main(String[] args) {

//需求:创建10个MyTask耗时的任务,统计它们执行完的总耗时
//方案二:使用并行流
//step 1: 创建1日个MyTask对象,每个任务持续1s,存入List集合
IntStream intStream = IntStream.range(0, 10);
List<MyTask> tasks = intStream.mapToObj(item -> {
return new MyTask(1);
}).collect(Collectors.toList());

//step 2: 执行10个MyTask,统计总耗时
long start = System.currentTimeMillis();
List<Integer> results = tasks.parallelStream().map(myTask -> {
return myTask.doWork();
}).collect(Collectors.toList());

long end = System.currentTimeMillis();

double costTime = (end - start) / 1000.0;
System.out.printf("processed %d tasks %.2f second", tasks.size(), costTime);
}

3、使用CompletableFutre执行并统计总耗时

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
java复制代码public static void main(String[] args) {

//需求:创建10个MyTask耗时的任务,统计它们执行完的总耗时
//方案三:使用并行流和CompletableFutre组合使用
//step 1: 创建1日个MyTask对象,每个任务持续1s,存入List集合
IntStream intStream = IntStream.range(0, 10);
List<MyTask> tasks = intStream.mapToObj(item -> {
return new MyTask(1);
}).collect(Collectors.toList());

//step 2: 根据MyTask对象构建10个耗时的异步任务
long start = System.currentTimeMillis();
// List<CompletableFuture<Integer>> futures = tasks.parallelStream().map(myTask -> {
// return CompletableFuture.supplyAsync(() -> {
// return myTask.doWork();
// });
// }).collect(Collectors.toList());

List<CompletableFuture<Integer>> futures = tasks.stream().map(myTask -> {
return CompletableFuture.supplyAsync(() -> {
return myTask.doWork();
});
}).collect(Collectors.toList());


//step 3: 当所有任务完成时,获取每个异步任务的执行结果,存入L1st集合中
List<Integer> results = futures.stream().map(future -> {
return future.join();
}).collect(Collectors.toList());
long end = System.currentTimeMillis();

double costTime = (end - start) / 1000.0;
System.out.printf("processed %d tasks %.2f second", tasks.size(), costTime);

}

4、使用串行流和CompletableFutre组合执行并统计总耗时(优化:指定线程数量)

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
java复制代码public static void main(String[] args) {

// CompletableFuture 在流式操作中的优势
// 需求: 创建10个 MyTask 耗时的任务, 统计它们执行完的总耗时
// 方案四:使用CompletableFuture(指定线程数量)

// step 1: 创建10个MyTask对象,每个任务持续1s, 存入List集合
IntStream intStream = IntStream.range(0, 10);
List<MyTask> tasks = intStream.mapToObj(item -> {
return new MyTask(1);
}).collect(Collectors.toList());

// 准备线程池
int N_CPU = Runtime.getRuntime().availableProcessors();
// 设置线程池中的线程的数量至少为10
ExecutorService executor = Executors.newFixedThreadPool(Math.min(tasks.size(),N_CPU * 2));

// step 2: 根据MyTask对象构建10个异步任务
List<CompletableFuture<Integer>> futures = tasks.stream().map(myTask -> {
return CompletableFuture.supplyAsync(()-> {
return myTask.doWork();
},executor);
}).collect(Collectors.toList());

// step 3: 执行异步任务,执行完成后,获取异步任务的结果,存入List集合中,统计总耗时
long start = System.currentTimeMillis();
List<Integer> results = futures
.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
long end = System.currentTimeMillis();

double costTime = (end - start) / 1000.0;
System.out.printf("processed %d tasks %.2f second", tasks.size(), costTime);

// 关闭线程池
executor.shutdown();

/**
* 总结
* CompLetabLeFuture可以控制更多的线程数量,而ParalLelstream不能
*/
}

5、合理配置线程池中的线程数

正如我们看到的,CompletableFuture 可以更好的控制线程池的数量,而 parallelStream 不能。
问题1:如何选用 CompletableFuture 和 ParallelStream?
如果你的任务是IO密集型,你应该使用 CompletableFuture。
如果你的任务是CPU密集型,使用比处理器更多的线程是没有意义的,所以选择 ParallelSteam,因为它不需要创建线程池,更容易使用。
问题2:IO密集型任务和CPU密集型任务的区别?
CPU密集型也叫计算密集型,此时,系统运行时大部分的状况是CPU占用率近乎100%,I/O在很短的时间可以完成,而CPU还有许多运算要处理,CPU使用率很高。比如计算1+2+3…+10万亿、天文计算、圆周率后几十位等,都属于CPU密集型程序。
CPU密集型任务的特点:大量计算,CPU占用率一般都很高,I/O时间很短
IO密集型指大部分的状况是CPU在等I/O(硬盘/内存)的读写操作,但CPU的使用率不高。
简单的说,就是需要大量的输入输出,例如读写文件、传输文件,网络请求。 IO密集型任务的特点:大量网络请求,文件操作,CPU运算少,很多时候CPU在等待资源才能进一步操作。
问题3:既然要控制线程池的数量,多少合适呢?
如果是CPU密集型任务,就需要尽量压榨CPU,参数值可以设为 Ncpu + 1。
如果是IO密集型任务,参考值可以设置为 2 * Ncpu,其中 Ncpu 表示核心数。

五、大数据商品比价

1、需求描述和分析

实现一个大数据比价服务,价格数据可以从京东、天猫、拼多多等平台去获取指定商品的价格、优惠金额,然后计算出实际付款金额(商品价格 -优惠金额),最终返回价格最优的平台与价格信息。

2、构建工具类和实体类

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
java复制代码public class PriceResult {

private int price;
private int discount;
private int realPrice;
private String platform;

public PriceResult() {
}

public PriceResult(String platform) {
this.platform = platform;
}

public PriceResult(int price, int discount, int realPrice, String platform) {
this.price = price;
this.discount = discount;
this.realPrice = realPrice;
this.platform = platform;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}

public int getDiscount() {
return discount;
}

public void setDiscount(int discount) {
this.discount = discount;
}

public int getRealPrice() {
return realPrice;
}

public void setRealPrice(int realPrice) {
this.realPrice = realPrice;
}

public String getPlatform() {
return platform;
}

public void setPlatform(String platform) {
this.platform = platform;
}

@Override
public String toString() {
return "PriceResult{" +
"平台='" + platform + '\'' +
", 平台价=" + price +
", 优惠价=" + discount +
", 最终价=" + realPrice +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码//获取当前时间
private static String getCurrentTime() {
LocalTime now = LocalTime.now();
return now.format(DateTimeFormatter.ofPattern("[HH:mm::ss.SS"));
}

// 打印输出带线程信息的日志
public static void printTheadLog1(String message) {
// 当前时间 | 线程id | 线程名 | 日志信息
String result = new StringJoiner(" | ")
.add(getCurrentTime())
.add(String.format("%2d", Thread.currentThread().getId()))
.add(Thread.currentThread().getName())
.add(message)
.toString();
System.out.println(result);
}

3、构建 HttpRequest

HttpRequest 用于模拟网络请求(耗时的操作)。

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
java复制代码package com.zxh.base.concurrency;

import com.zxh.base.concurrency.domin.PriceResult;
import com.zxh.base.utils.CommonUtils;

public class HttpRequest {

private static void mockCostTimeOperation() {
CommonUtils.sleepSecond(1);
}

// 获取淘宝平台的商品价格
public static PriceResult getTaobaoPrice(String productName) {
CommonUtils.printTheadLog("获取淘宝上" + productName + "价格");
mockCostTimeOperation();
PriceResult priceResult = new PriceResult("淘宝");
priceResult.setPrice(5199);
CommonUtils.printTheadLog("获取淘宝上" + productName + "价格完成:5199");
return priceResult;
}

// 获取淘宝平台的优惠
public static int getTaoBaoDiscount(String productName) {
CommonUtils.printTheadLog("获取淘宝上" + productName + "优惠");
mockCostTimeOperation();
CommonUtils.printTheadLog("获取淘宝上" + productName + "优惠完成:-200");
return 200;
}

// 获取京东平台的商品价格
public static PriceResult getJDongPrice(String productName) {
CommonUtils.printTheadLog1("获取京东上" + productName + "价格");
mockCostTimeOperation();
PriceResult priceResult = new PriceResult("淘宝");
priceResult.setPrice(5299);
CommonUtils.printTheadLog1("获取京东上" + productName + "价格完成:5299");
return priceResult;
}

// 获取京东平台的优惠
public static int getJDongDiscount(String productName) {
CommonUtils.printTheadLog1("获取京东上" + productName + "优惠");
mockCostTimeOperation();
CommonUtils.printTheadLog1("获取京东上" + productName + "优惠完成:-150");
return 150;
}

// 获取拼多多平台的商品价格
public static PriceResult getPDDPrice(String productName) {
CommonUtils.printTheadLog1("获取拼多多上" + productName + "价格");
mockCostTimeOperation();
PriceResult priceResult = new PriceResult("拼多多");
priceResult.setPrice(5399);
CommonUtils.printTheadLog1("获取拼多多上" + productName + "价格完成:5399");
return priceResult;
}

// 获取拼多多平台的优惠
public static int getPDDDiscount(String productName) {
CommonUtils.printTheadLog1("获取拼多多上" + productName + "优惠");
mockCostTimeOperation();
CommonUtils.printTheadLog1("获取拼多多上" + productName + "优惠完成:-5300");
return 5300;
}

}

4、使用串行的方式操作商品比价

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
java复制代码riceResult priceResult;
int discount;

// 获取淘宝平台的商品价格和优惠
priceResult = HttpRequest.getTaobaoPrice(productName);
discount = HttpRequest.getTaoBaoDiscount(productName);
PriceResult taoBaoPriceResult = this.computeRealPrice(priceResult, discount);

// 获取京东平台的商品价格和优惠
priceResult = HttpRequest.getJDongPrice(productName);
discount = HttpRequest.getJDongDiscount(productName);
PriceResult jDongPriceResult = this.computeRealPrice(priceResult, discount);

// 获取拼多多平台的商品价格和优惠
priceResult = HttpRequest.getPDDPrice(productName);
discount = HttpRequest.getPDDDiscount(productName);
PriceResult pddPriceResult = this.computeRealPrice(priceResult, discount);

// 计算最优的平台和价格
Stream<PriceResult> stream = Stream.of(taoBaoPriceResult, jDongPriceResult, pddPriceResult);
Optional<PriceResult> minOpt = stream.min(Comparator.comparing(priceRes -> {
return priceRes.getRealPrice();
}));
PriceResult result = minOpt.get();
return result;

5、使用Future+线程池增加并行

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
java复制代码public PriceResult getCheapestPlatformPrice2(String productName) {
// 线程池
ExecutorService executor = Executors.newFixedThreadPool(4);

// 获取淘宝平台的商品价格和优惠
Future<PriceResult> taobaoFuture = executor.submit(() -> {
PriceResult priceResult = HttpRequest.getTaobaoPrice(productName);
int discount = HttpRequest.getTaoBaoDiscount(productName);
return this.computeRealPrice(priceResult, discount);
});

// 获取京东平台的商品价格和优惠
Future<PriceResult> jdFuture = executor.submit(() -> {
PriceResult priceResult = HttpRequest.getJDongPrice(productName);
int discount = HttpRequest.getJDongDiscount(productName);
return this.computeRealPrice(priceResult, discount);
});


// 获取拼多多平台的商品价格和优惠
Future<PriceResult> pddFuture = executor.submit(() -> {
PriceResult priceResult = HttpRequest.getPDDPrice(productName);
int discount = HttpRequest.getPDDDiscount(productName);
return this.computeRealPrice(priceResult, discount);
});

// 计算最优的平台和价格
PriceResult priceResult = Stream.of(taobaoFuture, jdFuture, pddFuture)
.map(item -> {
try {
//假设延时5s后,就不要它的结果,所以返回一个空
return item.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
return null;
}finally {
executor.shutdown();
}
}).filter(Objects::nonNull)
.min(Comparator.comparing(PriceResult::getRealPrice)).get();

return priceResult;
}

6、使用CompletableFuture进一步增强并行

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
java复制代码public PriceResult getCheapestPlatformPrice3(String productName) {

// 获取淘宝平台的商品价格和优惠
CompletableFuture<PriceResult> taobaofuture = CompletableFuture.supplyAsync(() -> HttpRequest.getTaobaoPrice(productName))
.thenCombine(CompletableFuture.supplyAsync(() -> HttpRequest.getTaoBaoDiscount(productName)), (priceRsult, discount) -> {
return this.computeRealPrice(priceRsult, discount);
});


// 获取京东平台的商品价格和优惠
CompletableFuture<PriceResult> jdfuture = CompletableFuture.supplyAsync(() -> HttpRequest.getJDongPrice(productName))
.thenCombine(CompletableFuture.supplyAsync(() -> HttpRequest.getJDongDiscount(productName)), (priceRsult, discount) -> {
return this.computeRealPrice(priceRsult, discount);
});


// 获取拼多多平台的商品价格和优惠
CompletableFuture<PriceResult> pddfuture = CompletableFuture.supplyAsync(() -> HttpRequest.getPDDPrice(productName))
.thenCombine(CompletableFuture.supplyAsync(() -> HttpRequest.getPDDDiscount(productName)), (priceRsult, discount) -> {
return this.computeRealPrice(priceRsult, discount);
});


// 计算最优的平台和价格
PriceResult priceResult = Stream.of(taobaofuture, jdfuture, pddfuture)
.map(future -> future.join())
.min(Comparator.comparing(item -> item.getRealPrice()))
.get();
return priceResult;
}

7、需求变更:同一个平台比较同款产品(iPhone15)不同色系的价格

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
java复制代码//需求变更:同一个平台比较同款产品(iPhone15)不同色系的价格
public PriceResult batchComparePrice(List<String> products) {
// step 1:遍历每个商品的名字, 根据商品名称开启异步任务获取最终价, 归集到List集合中
List<CompletableFuture<PriceResult>> futureList = products.stream()
.map(productName -> {
return CompletableFuture.supplyAsync(() -> HttpRequest.getTaobaoPrice(productName))
.thenCombine(CompletableFuture.supplyAsync(() -> HttpRequest.getTaoBaoDiscount(productName)), (priceRsult, discount) -> {
return this.computeRealPrice(priceRsult, discount);
});
}).collect(Collectors.toList());

// step 2: 把多个商品的最终价进行排序获取最小值
PriceResult priceResult = futureList.stream()
.map(future -> future.join())
.sorted(Comparator.comparing(item -> item.getRealPrice()))
.findFirst()
.get();
return priceResult;
}

public static void main(String[] args) {
// 异步任务的批量操作
// 测试在一个平台比较同款产品(iPhone15)不同色系的价格
ComparePriceService service = new ComparePriceService();
long start = System.currentTimeMillis();
PriceResult priceResult = service.batchComparePrice(Arrays.asList("iphone15午夜黑","iphone15白色","iphone15淡青"));
long end = System.currentTimeMillis();
double costTime = (end - start)/1000.0;
System.out.printf("cost %.2f second processed\n",costTime);
System.out.println("priceResult = " + priceResult);
}

本文转载自: 掘金

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

🐳 Mybatis 中的动态查询 一、什么的Mybaits的

发表于 2024-03-29

一、什么的Mybaits的动态查询

MyBatis 的动态查询是指根据不同的条件来构建 SQL 查询语句。通常使用动态 SQL 不可能是独立的一部分,MyBatis 使用一种强大的动态 SQL 语言来改进这种情形,这种语言可以被用在任意的 SQL 映射语句中。

二、环境搭建

1.新建数据库、 (mybatisdb) 一张表 、 (t_emp)

image.png

image.png

SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_emp
-- ----------------------------
DROP TABLE IF EXISTS `t_emp`;
CREATE TABLE `t_emp` (
`emp_id` int(0) NOT NULL,
`emp_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
`emp_salary` decimal(10, 2) NULL DEFAULT NULL,
PRIMARY KEY (`emp_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_emp
-- ----------------------------
INSERT INTO `t_emp` VALUES (1, '名字1', 100.00);
INSERT INTO `t_emp` VALUES (2, '名字2', 200.00);

SET FOREIGN_KEY_CHECKS = 1;

2.添加依赖 (mysql , mybatis , junit , lombok)

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
xml复制代码<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
</dependencies>

3.新建实体类 Emp

1
2
3
4
5
6
7
8
java复制代码@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
private Integer empId ;
private String empName ;
private Double empSalary ;
}

4.新建mapper接口 EmpMapper

1
2
3
4
5
6
7
8
java复制代码public interface EmpMapper {
List<Emp> getEmpListByCondition(Emp emp);
void updateEmp(Emp emp);
List<Emp> getEmpListByCondition2(Emp emp);
List<Emp> getEmpListByCondition3(Emp emp);
List<Emp> getEmpListByCondition4(@Param("empIdList") List<Integer> empIdList);
List<Emp> getEmpList();
}

5.在resources目录下新建com.bottom.mybatis.mapper目录,并新建一个mapper.xml文件

image.png

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
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bottom.mybatis.mapper.EmpMapper">
<!-- 1. where - if -->
<select id="getEmpListByCondition" resultType="Emp">
select * from t_emp
<where>
<if test="empId!=null">
or emp_id = #{empId} <!-- 第9行的empId指的是对象中的属性empId。 or后面的emp_id 表示列名,因为此处是SQL语句部分 , #{}里面的是对象属性名 -->
</if>
<if test="empName!=null">
or emp_name like concat('%',#{empName},'%')
</if>
<if test="empSalary!=null">
or emp_salary &lt; #{empSalary}
</if>
</where>
</select>

<!-- 2. set -->
<update id="updateEmp">
update t_emp
<set>
<if test="empName!=null">
emp_name = #{empName},
</if>
<if test="empSalary!=null">
emp_salary = #{empSalary},
</if>
</set>
where emp_id = #{empId}
</update>

<!-- 3. trim -->
<!-- prefix 表示添加前缀
prefixOverrides 动态的删除指定的前缀单词

suffix 表示添加后缀
suffixOverrides 动态的删除指定的后缀单词
-->
<select id="getEmpListByCondition2" resultType="Emp">
select * from t_emp
<trim prefix="where" prefixOverrides="or|and" suffix=";" suffixOverrides="or|and">
<if test="empId!=null">
emp_id = #{empId} or
</if>
<if test="empName!=null">
emp_name like concat('%',#{empName},'%') or
</if>
<if test="empSalary!=null">
emp_salary &lt; #{empSalary} or
</if>
</trim>
</select>

<!-- 4. choose - when - otherwise -->
<select id="getEmpListByCondition3" resultType="Emp">
select * from t_emp
where
<choose>
<when test="empId!=null">
emp_id=#{empId}
</when>
<when test="empName!=null">
emp_name = #{empName}
</when>
<when test="empSalary!=null">
emp_salary = #{empSalary}
</when>
<otherwise>
1=1
</otherwise>
</choose>
</select>

<!-- 5. foreach -->
<select id="getEmpListByCondition4" resultType="Emp">
<!-- select * from t_emp where emp_id in (1,3,5,7,9) -->
select * from t_emp where emp_id in
<foreach collection="empIdList" separator="," open="(" close=")" item="eid">
#{eid}
</foreach>
</select>

<!-- 6. SQL片段 -->
<sql id="allColumns">
select emp_id as empId , emp_name as empName , emp_salary as empSalary
</sql>

<select id="getEmpList" resultType="Emp">
<include refid="allColumns"/> from t_emp
</select>

</mapper>

6.在 resources 目录下新建 mybatis-config.xml、logback.xml
logback.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<appender name="STDOUT"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>

<logger name="com.bottom.mybatis" level="DEBUG" />

</configuration>

mybatis-config.xml

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
xml复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="SLF4J"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.bottom.mybatis.pojo"/>
</typeAliases>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/relation"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.bottom.mybatis.mapper"/>
</mappers>
</configuration>

1、 where-if

image.png

image.png

image.png

image.png

此处的if是动态查询,即输入的条件查询不到结果为空

image.png

如果不输入查询条件,默认查询所有

image.png

image.png

2、trim

在 MyBatis 中,trim 通常用于对字符串进行修剪操作,去除前后的空格或指定的字符。这可以在映射文件的 SQL 语句中使用,以确保数据的一致性和准确性。

trim 函数的属性如下:

  • prefix:在 trim 标签内 SQL 语句加上前缀。
  • suffix:在 trim 标签内 SQL 语句加上后缀。
  • prefixOverrides:指定去除多余的前缀内容。
  • suffixOverrides:指定去除多余的后缀内容。

image.png

image.png

image.png

image.png

3、choose-when-otherwise

MyBatis 的choose-when-otherwise是条件表达式的一种用法,通常用于在 SQL 映射文件中根据不同的条件执行不同的操作。它允许你根据特定的条件选择不同的逻辑或生成不同的 SQL 语句。

image.png

image.png

image.png

4、foreach

foreach是用来构建in条件的,它可以在 SQL 语句中进行迭代一个集合。foreach有List、array、Map三种类型的使用场景。

image.png

image.png

image.png

image.png

5、sql片段

使用 <sql> 标签来定义 SQL 片段。SQL 片段是一段可重用的 SQL 代码块,可以在多个 SQL 语句中引用。SQL 片段的使用可以减少重复编写相同 SQL 代码的工作,提高代码的可维护性和重用性

image.png

image.png

6、分页插件

分页插件是一种用于在 MyBatis 框架中实现分页功能的插件。它可以帮助开发者在查询大量数据时,将结果分成多个页面显示,从而提高查询的效率和用户体验。

image.png

image.png

使用分页插件的步骤如下

  1. 添加依赖:
1
2
3
4
5
xml复制代码<dependency> 
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
  1. 配置分页插件(mybatis-config.xml):

image.png

  1. 在查询功能之前使用PageHelper.startPage(int pageNum, int pageSize)开启分页功能。

image.png

本文转载自: 掘金

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

Docker部署MongoDB+整合Mongo版MyBati

发表于 2024-03-29

👩🏽‍💻个人主页:阿木木AEcru

🔥 系列专栏:《Docker容器化部署系列》 《Java每日面筋》

💹每一次技术突破,都是对自我能力的挑战和超越。

image.png

一、 MongoDB简介

MongoDB是一个开源的NoSQL文档型数据库,它使用灵活的文档模型来存储数据,这些文档可以是嵌套的,类似于JSON对象。MongoDB以其高性能、高可用性和易扩展性而闻名,适用于各种规模的应用,从小型项目到大型企业级应用。

1.1 适用场景

网站数据:MongoDB 非常适合处理网站的实时数据,包括用户会话、页面点击流、用户活动日志等。它支持高并发的读写操作,适合处理大量用户请求和实时数据更新。

缓存:由于其高性能的特性,MongoDB 可以作为应用程序的缓存层,减轻后端数据库的压力。它可以存储大量的缓存数据,提高数据访问速度,优化系统性能。

大数据和分析:MongoDB 可以存储和处理大规模的数据集,适合进行复杂的数据分析和处理。它的聚合框架提供了强大的数据处理能力,可以执行复杂的数据聚合操作。

内容管理系统(CMS):MongoDB 的文档型结构非常适合存储和查询内容相关的数据,如文章、图片、视频等。它的灵活的数据模型和强大的查询语言使得内容管理变得更加高效。

物联网(IoT):MongoDB 可以处理来自传感器和设备的大量时序数据。它支持地理空间索引,适合处理地理位置相关的数据。

移动应用:MongoDB 可以作为移动应用的后端数据库,存储用户数据、应用配置和实时数据。它的可扩展性和高可用性确保了移动应用的稳定运行。

1.2 应用案例

京东:中国著名的电商平台,使用 MongoDB 存储商品信息,支持比价和关注功能。

赶集网:中国著名的分类信息网站,使用 MongoDB 记录页面浏览量(PV)计数。

奇虎360: 著名的病毒软件防护和移动应用平台,使用 MongoDB 支撑的 HULK 平台每天接受 200 亿次的查询。

百度云:使用 MongoDB 管理百度云盘中 500 亿条关于文件源信息的记录。

CERN:著名的粒子物理研究所,欧洲核子研究中心大型强子对撞机的数据使用 MongoDB 存储。

纽约时报:领先的在线新闻门户网站之一,使用 MongoDB 存储和处理新闻内容和用户数据。

二、 Docker部署MongoDB

2.1 拉取MongoDB镜像

1
复制代码 docker pull mongo

2.2 创建持久化文件夹

1
bash复制代码mkdir -p /usr/local/mongodb/data

2.3 启动MongoDB容器

1
scss复制代码docker run  --restart=always -itd --name mongo -v /usr/local/mongodb/data:/data/db -p 27017:27017 mongo:latest --auth

2.4 配置账号密码

进入容器

1
bash复制代码docker exec -it mongo bash

进入MongoDB控制台

1
复制代码mongo admin

创建用户

1
php复制代码db.createUser({user:'root',pwd:'root',roles:[{role:"root", db:"admin"},'readWrite']});

连接MongoDB

1
arduino复制代码db.auth('root', 'root')

2.5 开放防火墙端口

1
2
3
css复制代码sudo firewall-cmd --zone=public --add-port=27017/tcp --permanent
sudo firewall-cmd --reload
注:如果是使用的云服务器,安全组也需要开放此端口。

2.6 工具连接测试

三、SpringBoot整合MongoPlus

3.1 MongoPlus是什么

Mongo-Plus是一个 MongoDB 的操作工具,可和现有mongoDB框架结合使用,为简化开发、提高效率而生。可以理解成Mongo版的MybatisPlus,用法是差不多的。

特性如下:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 5 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持自定义全局通用操作:支持全局通用方法注入

3.2 引入maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!-- <scope>test</scope>-->
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--mongoPlus-->
<dependency>
<groupId>com.gitee.anwena</groupId>
<artifactId>mongo-plus-boot-starter</artifactId>
<version>2.0.8.3</version>
</dependency>
</dependencies>

3.3 yml配置文件

1
2
3
4
5
6
7
8
9
10
11
yml复制代码# mongo配置
mongo-plus:
data:
mongodb:
host: 127.0.0.1 #部署mongodb机器的ip
port: 27017 #端口
database: test #数据库名
username: root #用户名,没有可不填(若账号中出现@,!等等符号,不需要再进行转码!!!)
password: root #密码,同上(若密码中出现@,!等等符号,不需要再进行转码!!!)
authenticationDatabase: admin #验证数据库
connectTimeoutMS: 50000 #在超时之前等待连接打开的最长时间(以毫秒为单位)

3.4 Controller代码

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
java复制代码
@RestController
@RequestMapping("/mini/user")
@RequiredArgsConstructor
public class MiniUserController {

private final MiniUserService miniUserService;

//获取用户列表
@GetMapping("/list")
public R getUserList()
{
List<MiniUser> list = miniUserService.list();
return R.ok(list);
}

//新增用户
@PostMapping("/add")
public R addUser() {
//由于是测试我就随机生成用户信息了
MiniUser user = new MiniUser();
user.setName(RandomUtil.randomString(16));
user.setAge(RandomUtil.randomLong(18,100));
user.setEmail(RandomUtil.randomNumbers(10)+"@qq.com");
boolean save = miniUserService.save(user);
return R.ok(save);
}

//新增用户
@DeleteMapping("/{id}")
public R deleteUser(@PathVariable("id") String id) {
boolean delete = miniUserService.removeById(id);
return R.ok(delete);
}


}

3.5 service代码

1
2
3
java复制代码public interface MiniUserService extends IService<MiniUser> {

}
1
2
3
4
java复制代码@Service
public class MongoServiceImpl extends ServiceImpl<MiniUser> implements MiniUserService {

}

3.6 测试结果

这样一个简单的案例也就完成啦!

四、结尾

感谢您的观看! 如果本文对您有帮助,麻烦用您发财的小手点个三连吧!您的支持就是作者前进的最大动力!再次感谢!

本文转载自: 掘金

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

轻松瘦身:揭秘 Docker 镜像优化之旅 轻松瘦身:揭秘

发表于 2024-03-29

轻松瘦身:揭秘 Docker 镜像优化之旅

引言

在当今快速发展的软件开发领域,Docker 以其轻量级和高效的容器化技术,已经成为开发者和系统管理员的得力助手。然而,随着项目规模的扩大和应用的复杂化,Docker 镜像的体积问题逐渐凸显,成为影响部署效率和成本控制的关键因素。本文旨在分享一次 Docker 镜像体积优化的实践经验,帮助读者轻松实现镜像的“瘦身”。

Docker 镜像基础知识

Docker 镜像是由一系列只读层组成的,这些层通过联合文件系统(UnionFS)叠加在一起,形成一个完整的容器文件系统。每一层都代表了构建过程中的一个步骤,因此,镜像层数越多,其体积往往越大。优化 Docker 镜像的关键在于减少这些层的数量和大小。

优化前的准备工作

在着手优化之前,我们首先对现有的 Docker 镜像进行了彻底的测试和备份。选择一个合适的基础镜像是优化的第一步,因为它直接影响到最终镜像的大小和性能。我们通过分析现有的镜像结构,确定了优化的目标和方向。

优化策略与方法

  1. 清理不必要的文件

我们通过验证程序依赖库、非必要文件拷贝等,如临时文件、编译输出等,从而减少了镜像的体积。
2. 合并镜像层

通过采用多阶段构建的方法,我们将编译和运行阶段分离,大大减少了最终镜像的层数。此外,我们还利用了一些工具,如buildah和dockerSlim,来进一步合并和压缩镜像层。
3. 优化基础镜像和压缩软件包和依赖

在构建过程中,我们使用了像Alpine Linux这样的轻量级基础镜像,并移除了不必要的软件包和依赖,以减少镜像体积。 选择合适的基础镜像可以减小镜像大小,并确保基础镜像的安全性和更新性。Alpine、Ubuntu Minimal 等轻量级基础镜像是常用选择。
4. 优化软件配置

我们还对容器内运行的软件进行了配置优化,关闭了不必要的服务和功能,以减少资源占用和提高安全性。

实践案例分析

在一次针对 Web 应用的 Docker 镜像优化中,我们通过上述方法将后端镜像大小从 464MB 减少到了 315MB,Nginx 镜像从 135MB 减少到了 13.1MB,同时保持了应用的完整性和性能。这个过程中,我们遇到了一些挑战,比如如何确保多阶段构建的正确性和如何保持镜像的安全性。通过不断的测试和调整,我们最终成功地解决了这些问题。

优化前体积

优化前体积

优化前体积

优化前 dockerfile

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
bash复制代码FROM python:3.9.16-slim
RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list && set -ex \
  &&apt-get update\
  &&apt-get install gcc -y\
  &&apt-get install git curl -y
# 设定时区
#ENV TZ=Asia/Shanghai
#RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY backend/ /app
COPY config/ /config

# 再次切换工作目录为Django主目录
WORKDIR /app

# 安装项目所需python第三方库
# 指定setuptools的版本,必须指定,新版本有兼容问题
RUN set -ex \
    &&pip install --upgrade pip \
    &&pip install setuptools_scm -i https://mirrors.aliyun.com/pypi/simple/ \
    &&pip install  --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ \
    && rm -rf /var/cache/yum/* \
    && python manage.py collectstatic --noinput
EXPOSE 8001
EXPOSE 8000
EXPOSE 5555
CMD ["sh", "start.sh", "web"]

优化后体积

第一次优化后体积

第一次优化后体积

优化后 dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码
FROM python:3.9.16-alpine3.16
RUN  apk --update add gcc g++ git curl build-base musl-dev linux-headers
COPY backend/ /app
COPY config/ /config

# 再次切换工作目录为Django主目录
WORKDIR /app

# 安装项目所需python第三方库
# 指定setuptools的版本,必须指定,新版本有兼容问题
RUN set -ex \
    &&pip install --upgrade pip \
    &&pip install setuptools_scm==7.1.0 -i https://mirrors.aliyun.com/pypi/simple/ \
    &&pip install  --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ \
    && rm -rf /var/cache/yum/* \
    && python manage.py collectstatic --noinput
EXPOSE 8001
EXPOSE 8000
EXPOSE 5555
CMD ["sh", "start.sh", "web"]

做了哪些工作?

本次优化过程中,我们仅仅优化了基础镜像,后端从python:3.9.16-slim 改成了 alpine 版本的python:3.9.16-alpine3.16 ,nginx 从nginx:1.20.1改成了 alpine 版本的nginx:stable-alpine3.17-slim

遇到的问题

  1. 因 linux 发行版的不一致,安装命令也做了一些调整,apt 调整成 apk
  2. alpine 发行版的很多 linux 库是精简的,需要安装特殊适配的包

遇到问题的解决方案

遇到问题的解决方案

使用 apk 安装如下包 apk --update add gcc g++ git curl build-base musl-dev linux-headers

优化后的测试与验证

优化完成后,我们对新的镜像进行了详尽的测试,包括功能测试、性能测试和安全测试。这些测试确保了优化后的镜像不仅体积更小,而且运行稳定,满足了生产环境的要求。

持续维护与监控

为了确保镜像的长期健康和性能,我们将优化过程整合到了 CI/CD 流程中,并建立了一套监控系统,用于跟踪镜像的性能指标和安全状态。

总结与展望

通过本次实践,我们不仅学会了如何优化 Docker 镜像,还意识到了持续优化的重要性。随着技术的不断进步,我们相信未来会有更多高效和自动化的工具和方法出现,帮助我们更好地管理和优化 Docker 镜像。

本文转载自: 掘金

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

从零打造一款基于Nextjs+antd50的中后台管理系统

发表于 2024-03-28

hi,大家好,我是徐小夕,最近在研究nextjs, 为了更全面复盘总结nextjs, 我写了一个开箱即用的基于 next 的后台管理系统, 供大家学习参考.

github地址: https://github.com/MrXujiang/next-admin

演示地址:http://next-admin.com

接下来我就和大家介绍一下 Next-Admin 这款中后台管理系统。

为什么要用Nextjs

首先从官网上我们可以了解到 Next.js 提供了先进的服务端渲染(SSR)和静态生成(SSG)能力,使得我们能够在服务器上生成动态内容并将其直接发送给客户端,从而大大减少首次加载的等待时间。这样可以提高网站的性能、搜索引擎优化(SEO)以及用户体验。

在深度使用 next.js 开发应用之后,我总结了以下使用它的优点:

  • 支持高效的服务端渲染和静态页面生成能力
  • 规则化的路由系统(保证页面更有组织层次,能更好的管理多页面)
  • 规范且颗粒度的API开发模式(更好的规范接口和业务调用)
  • 支持复杂系统的搭建(优雅的SPA单页模式和MPA多页面模式)
  • 部署和开发成本很低(前后端同构更优雅)

所以基于以上体验和思考,我决定在后面的产品和系统上都采用 Next 来开发。

Next-Admin 特点

去年值得高兴的事情是 antd5.0 发布了,从组件UI和设计架构上都有了很大的改进,尤其是 Design Token . 有了它我们可以轻松的实时切换网站主题风格, 并且在应用里复用 antd 的设计语言。

所以为了更好的方便国内开发者使用 nextjs 开发中后台系统,我打算使用 antd5.0 作为UI库来开发, 大家也可以在 Next-Admin 的基础上改造成自己的中后台系统。

接下来就来介绍一下 Next-Admin 的特点。

1. 内置基础的登录注册页面

2. 内置可拖拽的数据报表

在内置常用数据看板的同时我还支持了看板拖拽功能, 让用户更高效的消费数据。

3. 内置监控大屏页面

4. 内置常用的搜索列表

5. 支持内嵌第三方系统

上图演示的是内嵌表单搭建引擎 https://turntip.cn/formManager 的案例。

6. 内置空白Landing页面

7. 支持国际化 & 一键换肤

暗模式:

明模式:

同时项目还集成了很多优秀的开发工具,方便大家更高效的开发业务系统。

如果你对 next 开发或者需要开发一套管理系统, 我相信 Next-Admin 会给你开发和学习的灵感。

同时也欢迎和我一起贡献, 让它变得更优秀~

github地址: https://github.com/MrXujiang/next-admin

演示地址:http://next-admin.com

由于服务器在国外, 所以建议大家git到本地体验~

欢迎star + 反馈~

更多推荐

可视化表单&试卷搭建平台技术详解

爆肝1000小时, Dooring零代码搭建平台3.5正式上线

本文转载自: 掘金

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

《HelloGitHub》第 96 期

发表于 2024-03-28

兴趣是最好的老师,HelloGitHub 让你对编程感兴趣!

简介

HelloGitHub 分享 GitHub 上有趣、入门级的开源项目。

github.com/521xueweiha…

这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等,涵盖多种编程语言 Python、Java、Go、C/C++、Swift…让你在短时间内感受到开源的魅力,对编程产生兴趣!


以下为本期内容|每个月 28 号更新

C 项目

1、cosmopolitan:让 C 成为构建一次,可随处运行的语言。这个工具可以将 C 语言编写的程序,编译成可无缝运行在多种操作系统上的可执行文件。它采用自包含式二进制文件的设计,能够将程序所有依赖打包进可执行文件中,实现真正的跨平台运行,支持 Windows、macOS 和 Linux 等主流操作系统。

1
2
3
4
5
6
7
scss复制代码// 编译
cosmocc -o hello hello.c
// 运行
./hello
// 调试
./hello --strace
./hello --ftrace

2、linenoise:一个 C 语言写的命令行编辑库。该项目是 Redis 作者用 C 语言实现的用于提升命令行交互体验的单文件库,整体代码大约 800 多行,轻量且易上手,提供了单/多行编辑模式、左右移动光标、上下回滚输入历史记录、命令补全等功能。来自 @9Ajiang 的分享

3、xxHash:超快的非加密哈希算法。哈希算法是一种将任意长度的输入数据转换为固定长度输出哈希值的算法。xxHash 是一种专为快速计算大型数据集哈希值而设计的非加密哈希算法。它具有出色的速度、零依赖和优秀的分布特性,支持流式计算模式和多种编程语言实现,适用于对计算性能要求很高的数据完整性检查、数据流分析、键值对检索等场景。

1
2
3
4
5
6
7
8
9
10
c复制代码#include <string.h>
#include "xxhash.h"

// Example for a function which hashes a null terminated string with XXH32().
XXH32_hash_t hash_string(const char* string, XXH32_hash_t seed)
{
// NULL pointers are only valid if the length is zero
size_t length = (string == NULL) ? 0 : strlen(string);
return XXH32(string, length, seed);
}

C# 项目

4、reverse-proxy:微软开源的反向代理工具包。该项目是微软团队用 C# 开发的一个提供核心代理功能的工具库,可作为库和项目模板,用于创建反向代理服务器的项目,内含简单的反向代理服务器示例项目。

5、Snap.Hutao:实用的多功能原神工具箱。这是一款专为 Windows 平台设计的原神工具箱,支持多账号切换、自定义帧率上限、祈愿记录、成就管理、签到奖励、查询角色资料、养成计算器等功能。它不对游戏客户端进行任何破坏性修改,只为改善原神桌面端玩家的游戏体验。来自 @Masterain 的分享

C++ 项目

6、ada:快如闪电的 URL 解析利器。该项目是用 C++ 写的符合 WHATWG 规范的 URL 解析器,解析速度是 curl 的数倍,目前已成为 Node.js 默认 URL 解析器(18.16.0 及以上),注意仅仅是 URL 地址解析不是请求。

7、keepassxc:一款开源、安全、跨平台的密码管理器。该项目是采用 C++ 开发的免费、离线、无广告的密码管理工具,它提供了简洁直观的用户界面,可轻松管理各种应用/网站的账号密码,支持多平台、浏览器插件、自动填充、密码生成等功能。

8、TranslucentTB:自定义 Windows 任务栏透明度的小工具。该项目是采用 C++ 开发的用于调整 Windows 任务栏透明度的工具,它体积小、免费、简单易用,支持 5 种任务栏状态、6 种动态模式、Windows 10/11 操作系统。

9、tugraph-db:支付宝背后的分布式图数据库。该项目是由蚂蚁集团和清华大学共同研发的高性能分布式图数据库,支持事务处理、TB 级大容量、低延迟查找和快速图分析等功能。

CSS 项目

10、easings.net:CSS 缓动函数速查表。缓动函数(Easing Functions)是一种用于控制 CSS 动画速度的函数,该项目提供了一系列优雅的缓动函数示例代码和效果展示。

1
2
3
css复制代码.block {
transition: transform 0.6s cubic-bezier(0.7, 0, 0.84, 0);
}

Go 项目

11、codapi:在线运行代码片段的 Go 服务。该项目提供了一个 API 服务,可以在线运行 Python、TypeScript、C、Go 等 30 种编程语言的代码片段,可用于在文档和教程中展示交互式的代码示例。

12、focalboard:开源的项目管理和团队协作工具。这是一款开源、多语言、自托管的项目管理工具,兼容了 Trello 和 Notion 的特点。它支持看板、表格和日历等视图管理任务,并提供评论同步、文件共享、用户权限等功能。该工具还提供了适用于 Windows、macOS、Linux 系统的客户端。

13、go-pretty:美化控制台输出的 Go 库。这是一个用于美化表格、列表、进度条、文本等控制台输出的库,你可以用它输出精美的表格、多层级的列表以及多任务进度条等内容。

14、gopeed:一款由 Go+Flutter 开发的高速下载器。这款下载工具后端用的是 Go 语言,支持 HTTP、BitTorrent、Magnet 等多种协议,并使用协程实现高速并发下载。前端部分采用 Flutter 开发,提供了适用于 Windows、macOS、Linux、Android、iOS 和 Web 等全平台的客户端。来自 @DeShuiYu 的分享

15、teleport:一款 Go 写的企业级开源堡垒机。这是一个专为基础设施提供连接、身份验证、访问控制和安全审计的平台,它支持对内网的 Linux 服务器、Kubernetes 集群、Web 应用、PostgreSQL 和 MySQL 数据库的安全访问。该平台采用自动下发证书的方式进行认证,无需在目标机器上管理密码和 SSH Key。此外,用户可以方便地使用 ssh、mysql、kubectl 等远程连接工具,轻松接入受管理的资源。

Java 项目

16、javers:用于追踪数据历史记录和审计的 Java 库。该项目是将版本管理的想法应用于数据(Java 对象)变更管理的 Java 库,它支持查看复杂的对象结构差异,保留修改数据的历史记录,并能追踪对象变化。来自 @猎隼丶止戈reNo7 的分享

17、source-code-hunter:Spring 全家桶源码解读。该项目提供了一系列互联网主流框架和中间件的源码讲解,包括 Spring 全家桶、Mybatis、Netty、Dubbo 等框架。

JavaScript 项目

18、aspoem:现代化的古诗词学习网站。这是一个更加注重阅读体验和 UI 的诗词网站,采用 TypeScript、Next.js、Tailwind CSS 构建。它拥有简洁清爽的界面和好看的字体,提供了古诗词的拼音、注释、译文以及移动端适配、搜索和一键分享等功能。来自 @meetqyhvkXU 的分享

19、MyIP:好用的 IP 工具箱。该项目的作者是一位产品经理,这是他借助 ChatGPT 完成的第一个 Vue.js 项目。通过该项目,你可以在线查看自己的 IP 信息(多源),并进行网站可用性、网速、MTR、DNS 泄漏、WebRTC 等检测。来自 @Jason Ng 的分享

20、nutui:京东风格的移动端 Vue 组件库。该项目是由京东开源的移动端 Vue 组件库,专为移动端 H5 和小程序开发场景而设计。它内含 80 多个高质量组件,支持按需引用、TypeScript、国际化等特性。

21、pikachu-volleyball:用 JavaScript 实现的皮卡丘排球游戏。该项目通过逆向工程解析原版的皮卡丘排球游戏,并使用 JavaScript 重新实现,包括物理引擎和对战机器人部分。

22、wasp:一个类似 Rails 的 React、Node.js 全栈 Web 框架。该项目是一个面向 Web 开发人员的全栈 Web 框架,开发者只需编写简单的 .wasp 配置文件,就能自动生成基于 React 和 Node.js 构建的 Web 应用,而且内置了数据库、身份验证、路由等功能。

Python 项目

23、marker:将 PDF 转换为 Markdown 文件的项目。这是一个能够将 PDF、EPUB 和 MOBI 格式的文件转换为 Markdown 文件的 Python 项目。相较于 Nougat,它具有更快的速度和更高的准确度,在处理英语类内容时效果最佳,但对中文的处理就要差一些。

24、Paper-Piano:在纸上弹钢琴。该项目使用 Python 和 OpenCV 实现图像处理和识别,通过摄像头捕获手指动作和手指下方的阴影,让用户可以通过触摸纸张来演奏钢琴。

25、pelican:Python 语言的静态网站生成器。这是一个用 Python 编写的静态网站生成器,让你可以通过编写 Markdown、reStructuredText 等格式的文本文件来创建网站,支持生成 RSS、代码语法高亮、插件扩展等功能。

26、posthog:开源的产品分析平台。这是一款基于 Django 构建的产品分析和用户追踪平台,它提供了丰富的功能,包括事件跟踪、漏斗分析、群体分析、A/B 测试等,适用于了解用户行为、改善产品体验的场景。

27、taipy:快速打造数据驱动的 Web 应用。这是一个基于 Python 和 Flask 的项目,结合了 React 等前端技术,为开发者提供了一个简洁、高效的开发框架。它能够简化数据处理、API 开发和用户界面构建的开发过程。不论是数据科学家、机器学习工程师还是 Web 开发者,都能够利用 Taipy 快速完成从原型到 Web 应用的全过程。来自 @刘三非 的分享

Rust 项目

28、genact:假装很忙的摸鱼神器。该项目可以在终端上模拟一些很忙的假象,比如编译、扫描、下载等。这些操作都是假的,实际上什么都没有发生,所以不会影响你的电脑,适用于 Windows、Linux、macOS 操作系统。来自 @39499740 的分享

29、rnote:跨平台的手写笔记和绘图应用。这是一款用 Rust 和 GTK4 编写的绘图应用,可用于绘制草图、手写笔记和注释文档等。它支持导入/导出 PDF 和图片文件,以及无限画布、拖放、自动保存等功能。适用于 Windows、Linux 和 macOS 系统,需要搭配手写板使用。

Swift 项目

30、Applite:Homebrew Cask 的桌面应用。这是一款采用 Swift 开发的免费 macOS 应用,它为 Homebrew Cask 提供了一个图形化界面,实现一键安装、更新和卸载应用。

31、BLEUnlock:使用蓝牙设备解锁你的 Mac 电脑。这款工具是可以在 macOS 上实现通过蓝牙设备解锁/锁定电脑。使用该工具时,蓝牙设备无需安装任何应用程序。当蓝牙设备靠近 Mac 电脑时,可以解锁屏幕并唤醒电脑;而当蓝牙设备远离时,自动锁定屏幕并暂停播放音乐/视频。支持 iPhone、Apple Watch、蓝牙耳机等设备。

其它

32、candle:自制 3D 电子蜡烛。该项目作者使用简单的 LED 板和小型电路板,制作了一个微型电子蜡烛,并通过旋转底座和流体模拟算法,模拟出 3D 的烛光效果。

33、docker-android:运行在 Docker 容器里的 Android。这是一个 Android 模拟器的 Docker 镜像,支持 Android 9-14 版本、VNC(远程桌面)、ADB(Android 调试桥)、日志查看等功能,适用于 Android 客户端测试和调试等场景。

1
2
3
4
5
6
ini复制代码docker run -d -p 6080:6080 \
-e EMULATOR_DEVICE="Samsung Galaxy S10" \
-e WEB_VNC=true \
--device /dev/kvm \
--name android-container \
budtmo/docker-android:emulator_11.0

34、excelCPU:仅用 Excel 构建出一颗 CPU 。该项目是一颗运行在 Excel 文件中的 16 位 CPU 处理器,它具有 3Hz 主频、128KB RAM 和一块 128x128 像素的显示屏,为此作者还创建了一门汇编语言。

35、Mr.-Ranedeer-AI-Tutor:打造你的个性化 AI 老师。该项目通过提示词让 AI 对话机器人充当老师和学习助手的角色,为你生成学习计划、授课解惑、出练习题等,还可以选择不同的授课风格和深度。它可搭配任意大模型,作者推荐 GPT-4 效果最佳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vbnet复制代码===
Author: JushBJJ
Name: "Mr. Ranedeer 提示词"
Version: 2.7
===

[Student Configuration]
🎯Depth: Highschool
🧠Learning-Style: Active
🗣️Communication-Style: Socratic
🌟Tone-Style: Encouraging
🔎Reasoning-Framework: Causal
😀Emojis: Enabled (Default)
🌐Language: English (Default)

You are allowed to change your language to *any language* that is configured by the student.

[Overall Rules to follow]
1. Use emojis to make the content engaging
2. Use bolded text to emphasize important points
3. Do not compress your responses
4. You can talk in any language
...

36、ugly-avatar:丑头像生成器。该项目可以用来随机生成一个很丑的手绘头像,不要怀疑真的很丑、很抽象,仅供娱乐。来自 @puz_zle 的分享

开源书籍

37、Real-Time-Rendering-4th-CN:《Real-Time Rendering 4th》中文翻译版。这是《Real-Time Rendering》第四版的中文翻译项目,该书是实时渲染领域的经典之作,非常适合从事游戏开发、3D 图形、VR/AR 等领域的开发者学习。

机器学习

38、FastChat:用于训练和评估大型语言模型的开放平台。这是一个用于训练、部署和评估大型语言模型的平台,你可以用它在本地部署和评估各种大模型。除此之外,它还提供了一个在线评估大模型的平台,用户可以向两个不同的大模型,问同一个问题,然后根据回答选出你认为更好用的大模型。在此过程中,你可以免费使用 Claude、ChatGPT 等对话机器人。来自 @浮生若夢 的分享

39、generative-ai-for-beginners:面向初学者的生成式人工智能教程。这是由微软开源的面向初学者的生成式 AI 免费课程,课程共 18 节,涵盖了创建生成式 AI 应用所需要了解的一切,包括生成式 AI 和 LLMs 的简介、提示词、构建文本生成应用、聊天应用、图像生成应用、向量数据库等方面的内容。

40、jan:一站式体验 LLMs 的桌面应用。这是一个支持在本地运行开源 LLMs 和连接 ChatGPT 服务的 AI 对话桌面应用,它开箱即用、界面清爽、不挑硬件,支持设置代理、接入 ChatGPT、一键下载/接入适配当前电脑配置的大模型、离线运行等功能,适用于 Windows、Linux、macOS 操作系统。

41、open-interpreter:让 LLM 在你的计算机上运行代码。该项目可以让大语言模型在本地运行代码,支持 Python、JavaScript、Shell 等编程语言。相当于大语言模型是一个解释器,它会理解你的意图,将自然语言转化成相应的代码脚本并运行。安装后,用户就可以在终端通过聊天的方式操作计算机,比如创建和编辑图片、视频和文件,控制 Chrome 浏览器进行搜索等。

最后

感谢参与分享开源项目的小伙伴们,欢迎更多的开源爱好者来 HelloGitHub 自荐/推荐开源项目。如果你发现了 GitHub 上有趣的项目,就点击这里分享给大家伙吧!

本期有你感兴趣的开源项目吗?如果有的话就留言告诉我吧~如果还没看过瘾,可以点击阅读往期内容。

感谢您的阅读,如果觉得本期内容还不错的话 求赞、求分享 ❤️

本文转载自: 掘金

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

提升JavaScript代码质量的最佳实践

发表于 2024-03-28

在JavaScript编程中,代码质量优化是一项重要的技能。它可以帮助我们提高代码的可读性、可维护性和性能。本文将通过一些实际优化过程中的案例,展示如何通过一些技巧和最佳实践,使我们的代码更加优雅。

1. 避免嵌套循环

嵌套循环会增加代码的复杂度,使其难以阅读和维护。我们可以通过将内部循环提取为一个单独的函数来优化代码。

优化前:

1
2
3
4
5
javascript复制代码for (let i = 0; i < array1.length; i++) {
for (let j = 0; j < array2.length; j++) {
// 一些复杂的逻辑
}
}

优化后:

1
2
3
4
5
6
7
8
9
javascript复制代码function processInnerLoop(item) {
for (let j = 0; j < array2.length; j++) {
// 一些复杂的逻辑
}
}

for (let i = 0; i < array1.length; i++) {
processInnerLoop(array1[i]);
}

2. 使用map、filter和reduce替代for循环

在处理数组时,我们经常使用for循环来迭代数组并进行一些操作。然而,使用map、filter和reduce这些高阶函数可以使代码更加简洁和易于理解。

优化前:

1
2
3
4
5
6
javascript复制代码let result = [];
for (let i = 0; i < array.length; i++) {
if (array[i] > 10) {
result.push(array[i] * 2);
}
}

优化后:

1
javascript复制代码let result = array.filter(item => item > 10).map(item => item * 2);

3. 使用解构赋值简化代码

解构赋值是ES6中引入的一个新特性,它允许我们用更简洁的语法从数组或对象中提取数据。

优化前:

1
2
3
javascript复制代码let firstName = person.firstName;
let lastName = person.lastName;
let age = person.age;

优化后:

1
javascript复制代码let { firstName, lastName, age } = person;

4. 多条件if判断

避免重复性的判断某一个变量,可将多个值放在一个数组中,然后调用数组的include方法。

优化前:

1
2
3
javascript复制代码if (a === 'a' || a === 'b' || a === 'c' || a === 'd') {
// 逻辑处理
}

优化后:

1
2
3
javascript复制代码if (['a', 'b', 'c', 'd'].includes(a)) { 
// 逻辑处理
}

5. 使用默认参数值

在函数中,我们经常需要处理未传递的参数。使用默认参数值可以简化这个过程。

优化前:

1
2
3
4
javascript复制代码function greet(name) {
name = name || 'Guest';
console.log('Hello, ' + name);
}

优化后:

1
2
3
javascript复制代码function greet(name = 'Guest') {
console.log(`Hello, ${name}`);
}

6. 简化 if true else 条件表达式

逻辑只是true/false的赋值时,简化不必要的if语句。

优化前:

1
2
3
4
5
javascript复制代码if (a > 100) {  
bool = true;
} else {
bool = false;
}

优化后:

1
javascript复制代码bool = a > 10;

7. indexOf的更简单写法

在数组中查找某个值是否存在可以使用indexOf方法,下面这种写法更简单。

优化前:

1
2
3
4
5
6
7
javascript复制代码if (list.indexOf(item) > -1) {  
// 存在
}

if (list.indexOf(item) === -1) {
// 不存在
}

优化后:

1
2
3
4
5
6
7
javascript复制代码if (~list.indexOf(item)) {  
// 存在
}

if (!~list.indexOf(item)) {
// 不存在
}

8. switch语句简化

将需要执行的条件存储在键值对象中,最后根据条件调用存储的方法。

优化前:

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码switch (type) {  
case 1:
run1();
break;
case 2:
run2();
break;
case 3:
run3();
break;
}

优化后:

1
2
3
4
5
6
7
javascript复制代码const data = {  
1: run1,
2: run2,
3: run3,
};

data[type] && data[type]();

9. 提前return

快速return(也称为提前return或守卫子句)是一种编程模式,特别是在处理多个条件判断时,它可以提高函数的可读性和性能。这种模式通过在函数的开始处检查条件,并在条件满足时立即返回,从而避免执行后续的不必要代码。

优化前:

1
2
3
4
5
6
7
8
9
javascript复制代码function check(number) {
if (number < 0) {
return "Negative";
} else if (number === 0) {
return "Zero";
} else {
return "Positive";
}
}

优化后:

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码function check(number) {
if (number < 0) {
return "Negative";
}

if (number === 0) {
return "Zero";
}

return "Positive";
}

10. 可选链运算符?.

可选链运算符?.提供了一种简洁的方式来安全地访问对象中深层嵌套的属性。它允许开发者在不进行每一步引用校验的情况下读取属性值,如果链中的任何引用是null或undefined,表达式将返回undefined。

1
2
3
4
5
6
javascript复制代码const vacationItinerary = {        
wednesday: {
venue: "Louvre Museum",
expenses: 150,
},
};

使用传统方法来安全地访问一个可能不存在的属性会涉及多个逻辑与操作:

优化前:

1
javascript复制代码const result = vacationItinerary && vacationItinerary.wednesday && vacationItinerary.wednesday.expenses;

优化后:

1
javascript复制代码const result = vacationItinerary?.wednesday?.expenses;

11.多条件&&运算符

当你需要在变量为真时才执行某个函数,可以使用逻辑与&&运算符来简化代码。

优化前:

1
2
3
4
javascript复制代码// 传统的条件判断
if (isValid) {
initiateProcess();
}

优化后:

1
2
javascript复制代码// 简化后的条件执行
isValid && initiateProcess();

12. 使用数字分隔符增强可读性

为了提升大数字的可读性,可以使用下划线_作为数值分隔符,它允许将数字分隔成更易于阅读的形式。

1
2
3
javascript复制代码const number = 1_000_000_000;

console.log(number); // 输出:1000000000

13. 字符串转换数字

虽然可以使用parseInt和parseFloat等内置方法将字符串转换为数字,但还有一种更简洁的方式:在字符串前使用一元加号+运算符。

优化前:

1
2
javascript复制代码let total = parseInt("456");
let average = parseFloat("87.5");

优化后:

1
2
3
4
5
6
javascript复制代码let total = +"456";
let average = +"87.5";

if (+currentState === 0) {
// 执行相关操作
}

使用一元加号+进行转换是一种简单且有效的方法,尤其适合在需要轻量级转换的场景中。

14. 提升控制台输出的清晰度

当你需要在控制台中打印变量的值时,将其包裹在对象字面量中可以同时显示变量名和值,从而提高输出的清晰度。

1
2
3
4
5
6
7
javascript复制代码const username = "Peter";
console.log({ username });

// 控制台输出将会是:
{
"username": "Peter"
}

这种方法不仅让你一目了然地看到变量的名称和对应的值,而且在调试多个变量时尤其有用。它避免了在控制台中查找与变量值对应的变量名的麻烦,使得调试过程更加高效。

15. 数组截断技巧

要快速截断数组至指定长度,只需修改数组的length属性即可。

1
2
3
javascript复制代码let numbers = ['1', '2', '3', '4'];
numbers.length = 2;
console.log(numbers); // 输出:['1', '2']

这个方法简单而直接,能够有效地减少数组的长度,而无需使用额外的函数或方法,尤其在你确切知道需要的数组长度时。

总结

通过以上案例,我们可以看到通过一些简单的技巧和最佳实践,我们可以大大简化我们的JavaScript代码,使其更加优雅。这只是冰山一角,还有许多其他的技巧和最佳实践可以帮助我们优化代码的复杂度。如果你有其他的优化写法欢迎留言交流~

希望本文能够为你的JavaScript编程提供一些启发。


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~

专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)

本文转载自: 掘金

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

1…444546…956

开发者博客

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