最近在开发 IM 产品时,需要改造聊天输入框并参考飞书的交互效果。初看到这个自动换行变高的效果,第一反应是需要通过 JavaScript 动态计算来实现。然而使用 AI 辅助开发后,效果并不理想。于是我查看了飞书网页版的源码,意外发现这个看似复杂的效果竟然是通过纯 CSS 的 Flex 布局实现的。

通过巧妙运用 flex-wrapflex-automargin-left: auto 这三个关键属性,不仅代码更加简洁优雅,性能表现也远超 JavaScript 方案。

效果动图

效果说明

当输入框内容较少时,输入框和按钮组在同一行显示:

1
[输入框........................] [按钮组]

当输入框内容增多需要换行时,由于 flex-wrap 的作用,按钮组会自动换到下一行,并通过 margin-left: auto 保持右对齐:

1
2
3
[输入框内容第一行.....................]
[输入框内容第二行.....................]
[按钮组]

效果体验

核心原理

这个效果的核心在于巧妙地运用了 Flex 布局的三个关键属性:

  1. flex-wrap: 允许 flex 项目换行
  2. flex-auto: 让输入框自动占据可用空间
  3. margin-left: auto: 将按钮组推到右侧

完整代码实现

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Flex 自动变高输入框</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="p-10">
<div class="flex flex-row flex-wrap border rounded-md w-[600px] pb-2">
<!-- 输入框 -->
<div
contenteditable
class="relative overflow-y-scroll flex-auto max-h-[100px] has-[div+div]:w-full m-2 mb-0 outline-none"
placeholder="请输入消息..."
></div>

<!-- 按钮组 -->
<div class="flex flex-nowrap items-center ml-auto mr-3 mt-2 gap-3">
<button title="插入图片">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
</svg>
</button>
<button title="插入视频">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M7 3v18" />
<path d="M3 7.5h4" />
<path d="M3 12h18" />
<path d="M3 16.5h4" />
<path d="M17 3v18" />
<path d="M17 7.5h4" />
<path d="M17 16.5h4" />
</svg>
</button>
<button title="发送消息">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"
/>
<path d="m21.854 2.147-10.94 10.939" />
</svg>
</button>
</div>
</div>
</body>
</html>

如何下载QQ音乐里面收藏的歌曲

发布在 技术

免责声明

  • 此文章具有非常强的时效性。
  • 本文只是技术思路分享,未实现完整程序,更不存在传播相关程序。
  • 测试时下载的歌曲,也在测试完成后立即删除,并未进行传播保留。

前言

这几天失业了,QQ 音乐也正好提醒会员即将到期,显然是不会续费了。那么,有没有可能将已有的歌曲保存到本地,以便在失业潮里也能…。打住,版权,版权,还是版权,我们只做技术分析学习。

从哪里开始?

QQ 音乐有多种版本:PC/Mac 版、网页版、移动端版,显然,网页版是最容易进行技术分析的。

获取歌曲列表

在浏览器打开 https://y.qq.com/ 后登录,发现我的音乐音乐列表有限制,仅显示前 10 首。

查看 Network 接口,可以看到相关请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// POST
// https://*.*.qq.com/cgi-bin/musics.fcg?_=**&sign=**

// Body
{
"comm": {
// 省略其他参数
},
"req_1": {
"module": "**",
"method": "**",
"param": {
"song_begin": 0,
"song_num": 10
// 省略其他参数
}
}
}

很容易看出,req_1.param.song_num 是控制返回歌曲数量的参数。

尝试通过 Postman 修改 song_num 参数值为 1000 后返回接口会报错。

通过多次测试确定是 sign=** 参数的原因,看样子页面在发送请求前会对请求参数进行一个签名计算,服务端进行验证。

尝试在 DevTools 中操作,在相关代码修改了song_num: 10 的值,后面代码再进行签名、发出请求,就可以成功获取到歌曲列表了。

下载歌曲

歌曲列表 API 并不包含歌曲文件地址,因此需要进入歌曲播放页面,通过 DevTools 查看网络请求,发现一个 GET 请求:

1
https://**.stream.qqmusic.qq.com/**.m4a?guid=**&vkey=**&uin=**&fromtag=**

返回了歌曲文件内容,保存为 m4a 文件后可以播放。

观察这个请求参数 发现关键难点参数是 vkey,他也是类似上面的 sign 参数,也是通过一些参数加算法处理得到的,服务端会进行验证。

实现思路

如果通过 httpClient 去实现这个程序,虽然执行效率会更高,但需要对诸多参数进行分析,还需要从压缩的 js 代码中扒取到相关算法代码,还可能遭遇验证码问题。考虑到这类需求功能通常只会用一两次,也不在乎这点执行效率,所以这个方案性价比太低。

那么选择使用 Selenium 或者 Puppeteer 来实现,性价比是最高的,只需要少量代码,少量调试即可快速搞定需求。

下面实例代码是 Puppeteer 实现的。

步骤

  1. 登录授权

Puppeteer 打开 QQ 音乐,用户再手动登录。

  1. 获取歌曲列表

可以通过 request 事件,对指定 js 文件进行拦截,修改为本地 js 文件,本地文件对 song_num: 10 参数进行了修改。下面是示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const likePage = await browser.newPage();
likePage.setRequestInterception(true);
likePage.on("request", (request) => {
const url = request.url();
if (url.includes("xxxxxx.js")) {
request.respond({
status: 200,
contentType: "application/javascript; charset=utf-8",
body: fs.readFileSync("xxxxxx.js", { encoding: "utf-8" }),
});
} else {
request.continue();
}
});
await likePage.goto("https://**.qq.com/n/**/profile/like/song");
  1. 下载歌曲

页面渲染出歌曲列表后,我们通过以下代码,点击一首歌曲的播放按钮,打开播放页面。

1
2
3
await likePage.evaluate(
`document.querySelector('.songlist__item:nth-child(${index}) .list_menu__play').click()`
);

我们在播放页面的 request 事件中对请求的 url 进行判断,如果是歌曲文件,再通过 httpClient 下载保存到本地即可。保存成功再切换到下一首,直到歌曲列表全部下载完成。

以上就整个程序的实现思路了。

可能会遇到的坑

  • 播放过的歌曲可能会缓存在浏览器的 IndexedDB 中,缓存过的歌曲不再进行网络请求。
  • 歌曲列表中可能会遇到一些无版权的歌曲,需要判断进行跳过或先从列表中移除。
  • 歌曲名、歌手名等可能存在特殊字符,用作文件名时需要进行处理。
  • 第 1 页 共 1 页
作者的图片

YaoWorld

热爱编程,热爱生活。

前端工程师

中国-成都