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

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


  • 首页

  • 归档

  • 搜索

22个实用的CSS技巧,让你的网站脱颖而出

发表于 2023-08-14

想要让你的网站在激烈的竞争中脱颖而出吗?使用CSS的强大功能可以帮助你实现这一目标。本文将分享22个实用的CSS技巧,帮助你提升网站的外观和用户体验。无论你是一个新手还是有经验的开发者,这些技巧都将为你的网站注入新鲜的设计元素和动感效果。

  1. 自定义字体: 通过使用@font-face规则,你可以在网站中应用自定义字体,增加独特性和品牌识别度。选择适合你网站风格的字体,并确保它们能够正确加载和显示。
  2. 渐变背景色: 使用CSS渐变背景色可以为你的网站添加华丽的外观。尝试不同类型的渐变,如线性渐变、径向渐变或重复渐变。通过指定起始颜色和结束颜色,你可以创建丰富多彩的背景效果。
1
2
3
css复制代码.background {
background: linear-gradient(to right, #ff9900, #ff5500);
}
  1. 动画效果: 利用CSS的过渡和动画属性,为你的网站添加动感效果。创建平滑的过渡、淡入淡出效果或引人注目的动画序列。通过定义动画的持续时间、延迟时间和重复次数,你可以控制动画的表现方式。
1
2
3
4
5
6
7
css复制代码.box {
transition: background-color 0.3s ease-in-out;
}

.box:hover {
background-color: #ff5500;
}
  1. 响应式布局: 使用CSS媒体查询来创建响应式布局,使你的网站在不同设备上都能呈现出良好的用户体验。根据屏幕尺寸和方向,调整元素的大小、位置和样式。使用弹性盒子(Flexbox)或网格布局(Grid Layout)来实现灵活的自适应设计。
1
2
3
4
5
6
7
8
9
10
11
12
13
css复制代码@media screen and (max-width: 768px) {
.container {
flex-direction: column;
}

.sidebar {
order: 2;
}

.main-content {
order: 1;
}
}
  1. 平滑滚动效果: 通过使用CSS的scroll-behavior属性,你可以为网页添加平滑滚动效果,使页面在滚动时更加流畅和舒适。将其应用于html或body元素,即可启用平滑滚动效果。
1
2
3
css复制代码html {
scroll-behavior: smooth;
}
  1. 网格布局: 使用CSS网格布局可以轻松创建复杂的网格结构,实现灵活的页面布局。通过定义网格容器和网格项,你可以精确控制元素的位置和大小。
1
2
3
4
5
6
7
8
9
10
css复制代码.grid-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 20px;
}

.grid-item {
grid-column: span 2;
grid-row: span 1;
}
  1. 自定义滚动条样式: 通过使用CSS的::-webkit-scrollbar伪类选择器,你可以自定义滚动条的样式。调整滚动条的宽度、颜色和形状,以适应你的设计需求。
1
2
3
4
5
6
7
8
9
10
11
ruby复制代码::-webkit-scrollbar {
width: 10px;
}

::-webkit-scrollbar-thumb {
background-color: #ff5500;
}

::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
  1. 响应式字体大小: 使用CSS的vw单位(视窗宽度的百分比)可以创建响应式字体大小。通过设置根元素的字体大小为vw单位,使字体随着屏幕尺寸的变化而自适应。
1
2
3
css复制代码html {
font-size: 4vw;
}
  1. 阴影效果: 通过使用CSS的box-shadow属性,你可以添加阴影效果,为元素增添立体感和深度。调整阴影的颜色、模糊程度和偏移量,以实现不同的效果。
1
2
3
css复制代码.box {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
  1. 自定义滚动条样式: 通过使用CSS的::-webkit-scrollbar伪类选择器,你可以自定义滚动条的样式。调整滚动条的宽度、颜色和形状,以适应你的设计需求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
css复制代码/* Webkit浏览器(Chrome等) */
::-webkit-scrollbar {
width: 8px;
}

::-webkit-scrollbar-track {
background-color: #f1f1f1;
}

::-webkit-scrollbar-thumb {
background-color: #888;
}

::-webkit-scrollbar-thumb:hover {
background-color: #555;
}
  1. 文本溢出省略号: 当文本内容超过容器宽度时,可以使用CSS的text-overflow属性来实现省略号的效果,以便更好地处理长文本。
1
2
3
4
5
css复制代码.container {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
  1. 边框动画效果: 通过使用CSS的transition属性和:hover伪类,可以为元素添加边框动画效果,使其在鼠标悬停时产生过渡效果。
1
2
3
4
5
6
7
8
css复制代码.box {
border: 1px solid #ccc;
transition: border-color 0.3s ease-in-out;
}

.box:hover {
border-color: #ff5500;
}
  1. 图片模糊效果: 通过使用CSS的filter属性中的blur函数,你可以为图片添加模糊效果。调整模糊程度,使图像呈现出柔和的视觉效果。
1
2
3
css复制代码.image {
filter: blur(5px);
}
  1. 渐变背景色: 使用CSS的linear-gradient函数,你可以为元素创建渐变背景色。定义起点和终点的颜色值,以及渐变的方向,实现各种炫丽的背景效果。
1
2
3
css复制代码.container {
background: linear-gradient(to right, #ff5500, #ffd200);
}
  1. 文字阴影效果: 通过使用CSS的text-shadow属性,你可以为文字添加阴影效果,增加文字的可读性和视觉效果。可以调整阴影的颜色、位置和模糊程度。
1
2
3
arduino复制代码.text {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
  1. 渐变边框样式: 使用CSS的border-image属性,你可以创建具有渐变效果的边框样式。定义渐变图像或渐变颜色作为边框的源,以及边框的切片方式和宽度。
1
2
3
4
css复制代码.border {
border: 10px solid;
border-image: linear-gradient(to right, #ff5500, #ffd200) 1;
}
  1. 旋转动画效果: 通过使用CSS的transform属性,你可以为元素创建旋转动画效果。指定旋转角度和过渡时间,在页面中实现各种旋转效果。
1
2
3
4
5
6
7
8
css复制代码.box {
transform: rotate(45deg);
transition: transform 0.3s ease-in-out;
}

.box:hover {
transform: rotate(90deg);
}
  1. 渐变文本效果: 使用CSS的background-clip属性和渐变背景色,可以为文本创建渐变效果。将渐变应用到文本的背景区域,形成独特的渐变文本效果。
1
2
3
4
5
css复制代码.text {
background: linear-gradient(to right, #ff5500, #ffd200);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
  1. 透明度动画效果: 通过使用CSS的opacity属性和transition属性,你可以为元素创建透明度动画效果。控制元素的透明度,使其在过渡期间平滑淡入或淡出。
1
2
3
4
5
6
7
css复制代码.box {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.box:hover {
opacity: 1;
}
  1. 悬浮效果: 通过使用CSS的:hover伪类和transform属性,可以为元素创建各种悬浮效果,如放大、旋转、倾斜等。
1
2
3
4
5
6
7
css复制代码.box {
transition: transform 0.3s ease-in-out;
}

.box:hover {
transform: scale(1.2);
}
  1. 渐变阴影效果: 使用CSS的box-shadow属性,你可以为元素创建渐变阴影效果。定义阴影的颜色和偏移量,使元素呈现出立体感。
1
2
3
css复制代码.box {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.5);
}
  1. 文字动画效果: 通过使用CSS的@keyframes规则和animation属性,可以为文字创建动画效果。定义关键帧和动画属性,使文字在页面中产生动态效果。
1
2
3
4
5
6
7
8
9
10
11
12
css复制代码.text {
animation: rainbow 5s infinite;
}

@keyframes rainbow {
0% { color: red; }
20% { color: orange; }
40% { color: yellow; }
60% { color: green; }
80% { color: blue; }
100% { color: purple; }
}

本文转载自: 掘金

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

JS如何判断文字被ellipsis了?

发表于 2023-08-01

如果想要文本超出宽度后用省略号省略,只需要加上以下的css就行了。

1
2
3
4
5
css复制代码.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

3行css搞定,但是问题来了:如果我们想要当文本被省略的时候,也就是当文本超出指定的宽度后,鼠标悬浮在文本上面才展示popper,应该怎么实现呢?

CSS帮我们搞定了省略,但是JS并不知道文本什么时候被省略了,所以我们得通过JS来计算。接下来,我将介绍几种方法来实现JS计算省略。

createRange

我发现Element-plus表格组件已经实现了这个功能,所以就先来学习一下它的源码。

源码地址:github.com/element-plu…

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
ts复制代码// 仅仅粘贴相关的
const cellChild = (event.target as HTMLElement).querySelector('.cell')
const range = document.createRange()
range.setStart(cellChild, 0)
range.setEnd(cellChild, cellChild.childNodes.length)
let rangeWidth = range.getBoundingClientRect().width
let rangeHeight = range.getBoundingClientRect().height
/** detail: https://github.com/element-plus/element-plus/issues/10790
* What went wrong?
* UI > Browser > Zoom, In Blink/WebKit, getBoundingClientRect() sometimes returns inexact values, probably due to lost
precision during internal calculations. In the example above:
* - Expected: 188
* - Actual: 188.00000762939453
*/
const offsetWidth = rangeWidth - Math.floor(rangeWidth)
if (offsetWidth < 0.001) {
rangeWidth = Math.floor(rangeWidth)
}
const offsetHeight = rangeHeight - Math.floor(rangeHeight)
if (offsetHeight < 0.001) {
rangeHeight = Math.floor(rangeHeight)
}


const { top, left, right, bottom } = getPadding(cellChild) // 见下方
const horizontalPadding = left + right
const verticalPadding = top + bottom
if (
rangeWidth + horizontalPadding > cellChild.offsetWidth ||
rangeHeight + verticalPadding > cellChild.offsetHeight ||
cellChild.scrollWidth > cellChild.offsetWidth
) {
createTablePopper(
parent?.refs.tableWrapper,
cell,
cell.innerText || cell.textContent,
nextZIndex,
tooltipOptions
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ts复制代码// 上面代码17行中的getPadding函数
const getPadding = (el: HTMLElement) => {
const style = window.getComputedStyle(el, null)
const paddingLeft = Number.parseInt(style.paddingLeft, 10) || 0
const paddingRight = Number.parseInt(style.paddingRight, 10) || 0
const paddingTop = Number.parseInt(style.paddingTop, 10) || 0
const paddingBottom = Number.parseInt(style.paddingBottom, 10) || 0
return {
left: paddingLeft,
right: paddingRight,
top: paddingTop,
bottom: paddingBottom,
}
}

document.createRange() 是 JavaScript 中的一个方法,用于创建一个 Range 对象,表示文档中的一个范围。Range 对象通常用于选择文档中的一部分内容,然后对其进行操作。

它可以:

  1. 设置选中文本范围:可以使用 document.createRange() 方法创建一个 Range 对象,并使用 setStart() 和 setEnd() 方法设置选中文本的起始和结束位置。
  2. 插入新元素:可以使用 document.createRange() 方法创建一个 Range 对象,并使用 insertNode() 方法将新元素插入到文档中的指定位置。
  3. 获取特定元素的位置:可以使用 document.createRange() 方法创建一个 Range 对象,并使用 getBoundingClientRect() 方法获取元素在文档中的位置和大小信息。

这边element就是使用range对象的getBoundingClientRect获取到元素的宽高,同时因为得到的宽高值有很多位的小数,所以element-plus做了一个判断,如果小数值小于0.001就舍弃小数部分。

接下来,就让我们进行一下复刻吧,可以通过调整盒子的宽度,在页面中看到是否有省略号的判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
html复制代码<div class="ellipsis box">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</div>

<style>
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.box {
border: 1px solid gray;
padding: 10px;
}

</style>

注意这里,我们需要区分clientWidth和offsetWidth,因为我们现在给了box加了1px的边框,所以offsetWidth = 1 * 2 (左右两边的border宽度) + clientWidth,所以我们这边使用clientWidth来代表box的实际宽度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码const checkEllipsis = () => {
const range = document.createRange();
range.setStart(box, 0)
range.setEnd(box, box.childNodes.length)
let rangeWidth = range.getBoundingClientRect().width
let rangeHeight = range.getBoundingClientRect().height
const contentWidth = rangeWidth - Math.floor(rangeWidth)
const { pLeft, pRight } = getPadding(box)
const horizontalPadding = pLeft + pRight
if (rangeWidth + horizontalPadding > box.clientWidth) {
result.textContent = '存在省略号'
} else {
result.textContent = '容器宽度足够,没有省略号了'
}
}

这种方法div里面放的元素和样式是不受限制的,比如html这样写还是能够正确计算的。

1
2
3
4
5
html复制代码<div class="ellipsis box">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
<span style="font-size: large;">hello world</span>
<span style="letter-spacing: 20px;">hello world</span>
</div>

创建一个div来获取模拟宽度

我们可以还可以通过创建一个几乎相同的div来获取没有overflow:hidden时元素的实际宽度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
html复制代码<div class="ellipsis box">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</div>

<style>
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.box {
border: 1px solid gray;
padding: 10px;
}

</style>
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
js复制代码const checkEllipsis = () => {
const elementWidth = box.clientWidth;
const tempElement = document.createElement('div');
const style = window.getComputedStyle(box, null)
tempElement.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
white-space: nowrap;
padding-left:${style.paddingLeft};
padding-right:${style.paddingRight};
font-size: ${style.fontSize};
font-family: ${style.fontFamily};
font-weight: ${style.fontWeight};
letter-spacing: ${style.letterSpacing};
`;
tempElement.textContent = box.textContent;
document.body.appendChild(tempElement);
if (tempElement.clientWidth >= elementWidth) {
result.textContent = '存在省略号'
} else {
result.textContent = '容器宽度足够,没有省略号了'
}
document.body.removeChild(tempElement);
}

当box元素里面存在多个dom元素的时候,还得进行一个递归创建dom,或者也可以试试cloneNode(true)来试试克隆。

创建一个block元素来包裹inline元素

这种方法从acro design vue中学到的,应该是最简单的办法。要点就是外层一定是block元素,内层是inline元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
html复制代码<div class="ellipsis box">
<span class="content">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing
elit.
</span>
</div>

<style>
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.box {
border: 1px solid gray;
padding: 10px;
}

</style>

通过上面对css和html做的处理,我们可以实现让box元素里面的文字进行ellipisis,同时由于并没有 对span.content进行任何overflow的处理,所以该 span 的offsetWidth还是保持不变。

1
2
3
4
5
6
7
8
9
js复制代码const checkEllipsis = () => {
const { pLeft, pRight } = getPadding(box)
const horizontalPadding = pLeft + pRight
if (box.clientWidth <= content.offsetWidth+horizontalPadding ) {
result.textContent = '存在省略号'
} else {
result.textContent = '容器宽度足够,没有省略号了'
}
}

同样,只要满足外层元素是block,内层元素是inline的话,里面的dom元素其实是随便放的

1
2
3
4
5
6
7
8
9
10
11
12
html复制代码<div class="ellipsis box">
<span class="content">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing
elit.
<span style="font-size: large;">
hello world
</span>
<span style="letter-spacing: 20px;">
hello world
</span>
</span>
</div>

方法比较

  1. 性能(个人主观判断)3>1>2
  2. 省心程度(个人主观判断):1>3>2
  3. 精确度(个人主观判断):3种方法精确度几乎相同,如果硬要比较我觉得是3>1>2

之后我在看看其他组件库有什么好的方法,然后再补充上来,前端总是在做这些很小很小的点,哈哈。

本文转载自: 掘金

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

手把手带你入门 Threejs Shader 系列(三)

发表于 2023-07-25

上篇文章「手把手带你入门 Three.js Shader 系列(二) - 牛衣古柳 - 20230716」古柳教大家如何在片元着色器里使用 uv 纹理坐标,并结合 GLSL 里一些内置函数,制作出颜色渐变、颜色突变、重复条纹等效果,最后带大家看了下不同几何体上的条纹效果是什么样的,相信大家对 shader 编程有了更进一步的了解!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码const vertex = `
varying vec2 vUv;

void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragment = `
varying vec2 vUv;

void main() {
vec3 color = vec3(step(0.5, fract(vUv.x * 3.0)));
gl_FragColor = vec4(color, 1.0);
}
`;

// const material = new THREE.MeshBasicMaterial({ color: 0x0ca678 });
const material = new THREE.ShaderMaterial({
vertexShader: vertex,
fragmentShader: fragment
});

看到有群友认真看了文章并指出几处小笔误,并对古柳的教程加以赞许,还是很开心的。(加古柳微信:xiaoaizhj,备注「可视化加群」即,欢迎进群交流)

绘制圆形

本文将继续介绍些 GLSL 内置函数并结合 uv 来实现些其他效果,首先教教大家如何绘制圆形。

在之前古柳自学 shader 时曾发现不少教程都会有一章(Shaping functions)专门讲如何在 shader 里绘制不同的基本形状,如圆形、矩形、三角形、多边形、线段等,但其实这些代码理解起来还是蛮抽象的而且不少图形相对不常用,因而古柳觉得这部分不太需要那么早以及那么大篇章的讲解,所以在自己的教程中会适当舍弃,这背后的取舍都是为了使该入门教程尽可能简单、大家学起来不觉得晦涩难懂。(如果当初古柳自学时也能有这样的教程想来应该会轻松许多!)

当然相对来说圆形的绘制既简单又常用得多,所以先进行介绍。

通过 length() 内置函数可以获取向量的长度,这里用 vUv 计算每个像素离原点(0.0, 0.0)位置的距离 dist,将其设置到颜色上,会得到圆心在左下角的1/4渐变圆形效果,左上角(0.0, 1.0)和右下角(1.0, 0.0)离原点距离都是1,对应颜色正好是白色,当dist>1后,颜色仍为白色。

1
2
3
4
5
6
7
8
C#复制代码varying vec2 vUv;

void main() {
// 绘制渐变圆形
float dist = length(vUv);
vec3 color = vec3(dist);
gl_FragColor = vec4(color, 1.0);
}

结合上一篇提到的 step() 函数,当 dist<0.5为0,dist>0.5为1,则能得到半径为0.5的1/4圆形。

1
2
3
4
5
6
7
8
C#复制代码varying vec2 vUv;

void main() {
// 绘制圆形
float dist = length(vUv);
vec3 color = vec3(step(0.5, dist));
gl_FragColor = vec4(color, 1.0);
}

通过 vUv - vec2(0.5) 将所有坐标整体移动,也就是将坐标原点移动到 plane 的正中心,然后就能绘制出完整的圆形;另外也可以用 distance() 函数计算两个点的距离来代替 length(),作用相同。

1
2
3
4
5
6
7
8
9
10
C#复制代码varying vec2 vUv;

void main() {
// 先居中,再绘制圆形
float dist = length(vUv - vec2(0.5));
// float dist = distance(vUv, vec2(0.5));
float radius = 0.5; // 0.25
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}

半径动态变化

除了手动改变半径大小 radius,还可以传入时间来动态控制半径大小。

在 ShaderMaterial 里可以通过 uniforms 从主程序 js 里传入所需的变量,其在顶点着色器和片元着色器里都能获取到,且对于每个顶点或片元数值统一相同,比如这里设置 uTime,并且在 animate() 函数里将不断变化的时间数值复制更新上去。注意设置时不是 uTime: 0 这样的格式,后面必须是对象格式 uTime: { value: 0 },其他 uniform 变量同理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码const material = new THREE.ShaderMaterial({
uniforms: {
uTime:
{ value: 0 }
},
vertexShader: vertex,
fragmentShader: fragment,
});

// Animation
let time = 0;
function animate() {
time += 0.05;
material.uniforms.uTime.value = time;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();

在片元着色器里用 uniform 修饰符声明变量,因为 uTime 是不断增大的数值,可以通过 sin() 正弦函数将数值规范到-1-1之间并使其周期变化,再乘以0.5后加上0.5,将范围变化到0-1,确保设置到半径上时不会是负数,于是就能实现圆圈半径大小随时间周期变化的效果。同样的方式还可以用到球体上改变顶点位置,使球体半径大小不断变化,后续讲到顶点着色器时会介绍。

1
2
3
4
5
6
7
8
9
10
11
12
C#复制代码varying vec2 vUv;

uniform float uTime;

void main() {
// 先居中,再绘制圆形
float dist = length(vUv - vec2(0.5));
// 半径大小随时间周期变化
float radius = 0.5 * (sin(uTime) * 0.5 + 0.5);
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}

一组重复圆圈动态变化

接着结合上篇文章所学,对 uv 重复5次 fract(vUv * 5.0),由于每个圆圈都是相对各自中心位置绘制,所以就有一组重复的圆圈效果。

1
2
3
4
5
6
7
8
9
10
11
12
C#复制代码varying vec2 vUv;

uniform float uTime;

void main() {
// 先重复 uv,再居中,再绘制圆形
float dist = length(fract(vUv * 5.0) - vec2(0.5));
// 半径大小随时间周期变化
float radius = 0.5 * (sin(uTime) * 0.5 + 0.5);
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}

但此时每个圆圈都是通过 sin(uTime) 控制动画,半径变化同步进行,很统一也很单调,这里可以通过 sin(uTime + vUv.x) 将不同水平值作为偏差值加进去,于是会有水平波浪起伏的效果

1
2
3
4
5
6
7
8
9
10
11
12
C#复制代码varying vec2 vUv;

uniform float uTime;

void main() {
// 先重复 uv,再居中,再绘制圆形
float dist = length(fract(vUv * 5.0) - vec2(0.5));
// 半径大小随时间周期变化
float radius = 0.5 * (sin(uTime + vUv.x) * 0.5 + 0.5);
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}

如果再把 vUv.y 也一起加上,变化效果更有趣丰富。

1
2
3
4
5
6
7
8
9
10
11
12
C#复制代码varying vec2 vUv;

uniform float uTime;

void main() {
// 先重复 uv,再居中,再绘制圆形
float dist = length(fract(vUv * 5.0) - vec2(0.5));
// 半径大小随时间周期变化
float radius = 0.5 * (sin(uTime + vUv.x + vUv.y) * 0.5 + 0.5);
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}

切换到立方体同样是蛮有趣的效果。

1
2
js复制代码// const geometry = new THREE.PlaneGeometry(1, 1);
const geometry = new THREE.BoxGeometry(1, 1, 1);

需要注意的是,大家自己敲的代码可能动画快慢效果略有不同,可通过 uTime 乘以较大或较小的值进行调整,比如 sin(uTime * 0.1 + vUv.x)。

径向条纹效果

接着让我们回到最初绘制一个完整圆形的例子。

1
2
3
4
5
6
7
8
9
C#复制代码varying vec2 vUv;

void main() {
// 先居中,再绘制圆形
float dist = length(vUv - vec2(0.5));
float radius = 0.5;
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}

当我们有了每个 uv 离中心的距离后,可以对它运用翻倍再取小数的操作进行重复,这样就能做出由里向外一圈圈黑白交错的径向条纹效果。

1
2
3
4
5
6
7
8
9
C#复制代码varying vec2 vUv;

void main() {
// 先居中,后重复,再绘制圆形
float dist = fract(length(vUv - vec2(0.5)) * 5.0);
float radius = 0.5;
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}

有个小细节需要注意,上面我们翻了5倍,但看效果里一黑一白为1组,其实只有3组多点,究其原因是 length(vUv - vec2(0.5)) 一开始的范围并不是0到1,最大值是四个角离中心的距离,也就是 (1.0, 1.0) 离 (0.5, 0.5) 的距离,即 √2/2=0.707,因而我们可以先对其除以 0.707 再去翻倍取小数进行重复,这样就能如愿想有几组就几组、想重复几次就重复几次。

1
C#复制代码float dist = fract(length(vUv - vec2(0.5)) / 0.707 * 5.0);

然后将 uTime 加到变换到0-1范围后的数值上,使得径向条纹动起来。此时 dist 的公式其实已经有些复杂了,大家虽然可能看古柳写得很自然,但如果第一次尝试自己写,没准计算的顺序、括号的位置保不齐花样百出。所以大家务必好好理解每一步的顺序是怎么样的,力求能清楚地自行实现出来。

1
2
3
4
5
6
7
8
9
C#复制代码varying vec2 vUv;

void main() {
// 先居中,后重复,再绘制圆形
float dist = fract((length(vUv - vec2(0.5)) /0.707 + uTime * 0.5) * 5.0);
float radius = 0.5;
vec3 color = vec3(step(radius, dist));
gl_FragColor = vec4(color, 1.0);
}

反向运动可以减去 uTime,运动速率可以通过 uTime 的倍数来控制。

1
C#复制代码float dist = fract((length(vUv - vec2(0.5)) / 0.707 - uTime * 0.5) * 5.0);

切换到立方体上就是这样的效果。

一下子又讲了不少内容,通过这两篇教程的学习,相信大家也见识到结合 GLSL 的内置函数以及简单的 sin 正弦函数,我们就能对 uv 做出不少酷炫的效果。

不知道大家是否越来越觉得 shader 其实蛮有意思的,这些内容并不是特别难,当然教程的背后古柳做了一些取舍,也力求结合更多有趣的例子使大家在学的过程中不觉得枯燥乏味、晦涩难懂。当然更精彩的内容还在后面,希望大家能坚持一起学下去。

如果你喜欢本文内容,欢迎以各种方式支持,这也是对古柳输出教程的一种正向鼓励!

照例

最后欢迎加入「可视化交流群」,进群多多交流,对本文任何地方有疑惑的可以群里提问。加古柳微信:xiaoaizhj,备注「可视化加群」即可。

欢迎关注古柳的公众号「牛衣古柳」,并设置星标,以便第一时间收到更新。

本文转载自: 掘金

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

CSS 滚动驱动动画终于正式支持了~

发表于 2023-07-24

欢迎关注我的公众号:前端侦探

在最新的Chrome 115中,令人无比期待的CSS 滚动驱动动画(CSS scroll-driven animations)终于正式支持了~有了它,几乎以前任何需要JS监听滚动的交互都可以纯 CSS 实现了,就是这么强大,一起了解一下吧

温馨提示:文章略长,建议收藏后反复查阅

一、快速入门 CSS 滚动驱动动画

直接介绍 API 可能不太感兴趣,这里先通过一个最直观的例子感受一下。

下面是一个页面进度指示器,进度随着页面的滚动而变化

Kapture 2023-07-22 at 13.39.36.gif

页面很简单,很多内容和一个进度条

1
2
html复制代码<div class="progress"></div>
...很多内容

进度条是fixed定位

1
2
3
4
5
6
7
8
9
css复制代码.progress{
position: fixed;
top: 0;
left: 0;
right: 0;
height: 10px;
background-color: #F44336;
transform-origin: 0 50%;
}

然后给这个进度条添加一个动画,表示进度从0到100%

1
2
3
4
css复制代码@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}

接着给这个进度条绑定动画

1
2
3
css复制代码.progress{
animation: grow-progress 3s linear;
}

刷新页面,可以看到进度条在3s内从0增长到了100%

Kapture 2023-07-22 at 13.49.38

显然这种动画没什么意义,我们需要在滚动时才触发,并且滚动多少,动画就播放多少。

注意:动画时长不能为0,因为为0表示动画不执行,所以必须写上一个任意非零时间,或者直接为auto

最后,加上最核心的一段,也就是今天的主角animation-timeline

1
2
3
4
css复制代码.progress{
/*...*/
animation-timeline: scroll();
}

这样进度条就乖乖的跟随页面滚动而变化了(注意Chrome 115+)

Kapture 2023-07-22 at 13.39.36.gif

完整代码可以访问:

  • CSS scroll-driven-animations-back (codepen.io)

是不是非常简单?是不是非常神奇?如果你感兴趣,可以接着往下看

二、CSS 滚动驱动动画

大家可能知道,传统 JS 监听滚动有一些问题,如下

  • 现代浏览器在单独的进程上执行滚动,因此只能异步传递滚动事件。
  • 由于是异步传递,因此主线程动画容易出现卡顿

因此,为了解决滚动卡顿的问题,CSS 滚动驱动动画应运而生。那么,什么是 CSS 滚动驱动动画?

默认情况下,动画是随着时间的流逝而播放的。

CSS 滚动驱动动画指的是将动画的执行过程由页面滚动进行接管,也就是这种情况下,动画只会跟随页面滚动的变化而变化,也就是滚动多少,动画就执行多少,时间不再起作用。

如何改变动画的时间线呢? 那就需要用到这个核心概念了:animation-timeline,表示动画时间线(或者叫时间轴),用于控制 CSS 动画进度的时间线,是必不可少的一个属性。

image-20230722182057011

默认值是auto,也是就传统的时间线。下面是它一些关键词

1
2
3
4
5
6
7
8
9
10
11
12
13
css复制代码/* 关键词 */
animation-timeline: none;
animation-timeline: auto;
/* 命名时间线 */
animation-timeline: --timeline_name;

/* 滚动时间线 */
animation-timeline: scroll();
animation-timeline: scroll(scroller axis);

/* 视图时间线 */
animation-timeline: view();
animation-timeline: view(axis inset);

是不是有点混乱?不要慌,实际滚动场景千千万,这里可以分为两大类:一类是滚动进度时间线,也就是上面的关键词scroll(),还有一类是视图进度时间线,也就是关键词view()。

两者形式对应两种不同的应用场景,这是什么意思呢?下面一一介绍

三. CSS 滚动进度时间线

滚动进度时间线(scroll progress timeline)。表示页面或者容器滚动,将滚动进度映射到动画进度上。起始滚动位置代表 0% 进度,结束滚动位置代表 100% 进度,下面是一个可视化演示

scroll-driven-animations.style/tools/scrol…

Kapture 2023-07-22 at 14.05.38.gif

在上面的进度条例子中,我们用到的就是scroll progress timeline,因为我们监听的就是页面的滚动

1
css复制代码animation-timeline: scroll();

这里的scroll()是一个简写,可以传递两个参数,分别是<scroller>和<axis>

<scroller>表示滚动容器,支持以下几个关键值

  • nearest:使用最近的祖先滚动容器*(默认)*
  • root:使用文档视口作为滚动容器。
  • self:使用元素本身作为滚动容器。

<axios>表示滚动方向,支持以下几个关键值

  • block:滚动容器的块级轴方向*(默认)*。
  • inline:滚动容器内联轴方向。
  • y:滚动容器沿 y 轴方向。
  • x:滚动容器沿 x 轴方向。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
css复制代码/* 无参数 */
animation-timeline: scroll();

/* 设置滚动容器 */
animation-timeline: scroll(nearest); /* 默认 */
animation-timeline: scroll(root);
animation-timeline: scroll(self);

/* 设置滚动方向 */
animation-timeline: scroll(block); /* 默认 */
animation-timeline: scroll(inline);
animation-timeline: scroll(y);
animation-timeline: scroll(x);

/* 同时设置 */
animation-timeline: scroll(block nearest); /* 默认 */
animation-timeline: scroll(inline root);
animation-timeline: scroll(x self);

需要注意的是,这里语法容错性比较强,没有顺序要求,会自动识别

因此,如果需要监听横向滚动,可以这样

1
css复制代码animation-timeline: scroll(inline);

不知大家发现没,前面的滚动容器只有三个关键词,并不能通过#id方式任意指定滚动容器,真的能满足所有需求吗?

当然不行!有时候结构稍微复杂一点,自动查找就不适用了,并且这里的最近祖先滚动容器还受到绝对定位的影响,因此,我们还需要手动去指定滚动容器。

官方的解决方式是创建一个带有名称的时间线,具体做法是,在滚动容器上添加一个属性scroll-timeline-name,这个属性值必须以--开头,就像 CSS 变量一样,还可以通过scroll-timeline-axis设置滚动方向,此时的animation-timeline就不用默认的scroll()了,而是改用前面设置的变量,示意如下

1
2
3
4
5
6
7
8
9
10
11
12
css复制代码@keyframes animate-it { … }

/*滚动容器*/
.scroller {
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: inline;
}

.scroller .subject {
animation: animate-it linear;
animation-timeline: --my-scroller;
}

这里的scroll-timeline-axis和scroll-timeline-name还可以简写成一个属性scroll-timeline

1
2
3
4
css复制代码scroll-timeline-name: --my-scroller;
scroll-timeline-axis: inline;
/**可简写为**/
scroll-timeline: --my-scroller inline;

下面来看一个横向滚动的例子,刚好可以把上面的几个新概念都用上。

Kapture 2023-07-22 at 14.37.00.gif

布局还是类似,只是放在了一个可以横向滚动的容器中

1
2
3
4
html复制代码<main>
<div class="progress"></div>
...很多内容...
</main>

给main设置横向滚动,.progress设置fixed定位,还有动画和上个例子一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
css复制代码main{
display: flex;
overflow: scroll;
}
.progress{
position: fixed;
top: 0;
left: 0;
right: 0;
height: 10px;
background-color: #F44336;
transform-origin: 0 50%;
animation:grow-progress 3s linear;
}
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}

由于这里main才是滚动容器,并不是页面,而.progress是fixed定位,如果直接用scroll(nearest)获取到的就是页面根容器,并不是main,所以这里需要用命名scroll-timeline,实现如下

1
2
3
4
5
6
7
8
css复制代码main{
/**/
scroll-timeline: --scrollcontainer inline;
}
.progress{
/**/
animation-timeline: --scrollcontainer;
}

这样就可以将横向滚动进度一一映射到动画上了,而且不受结构限制,非常自由

完整代码可以查看:

  • CSS scroll-driven-animations-inline (codepen.io)

四、CSS 视图进度时间线

视图进度时间线(view progress timeline)。这个名字有些难以理解,其实表示的是一个元素出现在页面视野范围内的进度,也就是关注的是元素自身位置。元素刚刚出现之前代表 0% 进度,元素完全离开之后代表 100% 进度,下面是一个可视化演示

scroll-driven-animations.style/tools/view-…

Kapture 2023-07-22 at 14.51.44.gif

这个概念非常像JS中的Intersection_Observer_API,也就交叉观察者,可以监测到元素在可视区的情况,因此,在这种场景中,无需关注滚动容器是哪个,只用处理自身就行了。

和前面的scroll progress time语法类似,也有一个快捷语法

1
css复制代码animation-timeline: view()

由于无需关注滚动容器,所以它的参数也不一样,分别是<axios>和<inset>

<axios>表示滚动方向,支持以下几个关键值

  • block:滚动容器的块级轴方向*(默认)*。
  • inline:滚动容器内联轴方向。
  • y:滚动容器沿 y 轴方向。
  • x:滚动容器沿 x 轴方向。

<inset>表示调整元素的视区范围,有点类似scroll-padding,支持两个值,表示开始和结束两个范围。

1
2
3
4
5
6
7
css复制代码animation-timeline: view(auto); /* 默认值 */
animation-timeline: view(20%);
animation-timeline: view(200px);
animation-timeline: view(20% 40%);
animation-timeline: view(20% 200px);
animation-timeline: view(100px 200px);
animation-timeline: view(auto 200px);

这里的<inset>还可以用view-timeline-inset单独来表示,不过需要注意的是,这种用法要使用命名的view progress time,如下

1
2
3
css复制代码scroll-timeline: --my-scroller block;
view-timeline-inset: 20% 200px;
animation-timeline: --my-scroller;

按照我的经验,view progress time中使用命名的情况比较少,因为无需知道滚动容器,因此推荐用view()

下面来看一个例子,有一个列表

1
2
3
4
5
6
7
8
html复制代码<div>欢</div>
<div>迎</div>
<div>关</div>
<div>注</div>
<div>前</div>
<div>端</div>
<div>侦</div>
...

简单修饰后效果如下

image-20230722152836026

现在,我们添加一个淡入和缩放的动画

1
2
3
4
5
6
7
8
9
10
css复制代码@keyframes appear {
from {
opacity: 0;
transform: scaleX(0);
}
to {
opacity: 1;
transform: scaleX(1);
}
}

然后通过animation-time绑定在每个元素上,因为我们想做一个元素进入的动画,所以要用到view progress timeline

1
2
3
4
5
css复制代码div{
/**/
animation: appear 1s linear both;
animation-timeline: view();
}

可以得到这样的效果

Kapture 2023-07-22 at 15.48.19.gif

效果是出来了,不过好像有点太过了,太夸张了,可以看到,每个元素在滚动出现到离开的过程中都完整的执行了我们定义的动画。那么,有没有办法让这个范围变小一点呢?默认的范围如下

image-20230722225627354

当然也是可以的,这里就需要用到view的第二个参数<inset>了,比如设置40% 0表示调整视区范围,相当于将滚动容器上边距减少了 40%,当滚动到视区上面40%的时候就完成了动画(默认是滚动到0%,也就是完全离开的时候)

1
2
3
4
css复制代码div{
/**/
animation-timeline: view(40% 0);
}

image-20230723115526561

还可以更加激进一点,设置成100%,相当于元素一旦完全进入,动画就执行完成了,这样元素出现动画会更加和谐

1
2
3
4
css复制代码div{
/**/
animation-timeline: view(100% 0);
}

此时的动画范围就更小了,如下

image-20230723120245407

效果如下,是不是感觉没那么夸张了呢

Kapture 2023-07-22 at 16.21.36.gif

完整代码可以查看:

  • CSS scroll-driven-animations-view (codepen.io)

五、CSS 动画范围区间

默认情况下,动画会根据滚动区间范围一一映射,就比如第一个滚动指示器的例子,滚动多少,指示器的进度就走多少。

image-20230722180458893

但有时候,我们并不需要完整的区间,比如这个例子,右下角的返回顶部按钮

Kapture 2023-07-22 at 17.57.33.gif

像这种情况下,我们其实只需要前面滚动一定距离就可以让返回按钮完全出现了,对应关系应该是这样

image-20230722180944722

那么,如何截取一定的滚动区间呢?这就要涉及一个新的属性,叫做animation-range,也就是“动画范围”。

这里也要分两种场景,也就是前面提到的滚动进度时间线和视图进度时间线

1. 滚动进度时间线

首先来看scroll()场景,由于只是滚动容器的监听,因此比较简单,直接设置范围就行了

1
2
3
css复制代码animation-range: normal; /* 等价于 normal normal */
animation-range: 20%; /* 等价于 20% normal */
animation-range: 100px; /* 等价于 100px normal */

比如上面这个返回顶部的例子,动画其实很简单,就是一个向上的位移动画

1
2
3
4
css复制代码@keyframes back-progress {
from { transform: translateY(150%); }
to { transform: translateY(0%); }
}

如果仅仅添加一个滚动时间轴

1
2
3
4
5
css复制代码.back{
/**/
animation: back-progress 1s linear forwards;
animation-timeline: scroll();
}

那么,这个返回按钮就像滚动进度条那样,慢慢的出来,直到滚动到最底部才完全出来,效果如下

Kapture 2023-07-22 at 22.52.29.gif

这时只需要在[0, 固定距离]的范围内出现就好了,表示只在这个区间范围内触发动画,关键代码如下

1
2
3
4
5
6
css复制代码.back{
/**/
animation: back-progress 1s linear forwards;
animation-timeline: scroll();
animation-range: 0 100px;
}

这样就实现了滚动100px时自动出现的返回顶部按钮,100px后按钮会一直显示

Kapture 2023-07-22 at 17.57.33.gif

完整代码可以查看:

  • CSS scroll-driven-animations-back (codepen.io)

还有一个头部吸顶的例子,原理也是类似的,如下

Kapture 2023-07-23 at 14.13.13.gif

头部是一个高度和字号不断变小的动画,然后需要设置一下animation-range,关键实现如下

1
2
3
4
5
6
7
8
9
10
11
12
css复制代码@keyframes header {
to {
height: 60px;
font-size: 30px;
}
}
.header{
/**/
animation: header 1s linear forwards;
animation-timeline: scroll();
animation-range: 0 calc(100vh - 60px);
}

完整代码可以查看:

  • CSS scroll-driven-animations-header (codepen.io)

2. 视图进度时间线

再来看看view()场景。由于涉及到元素和可视区域的交叉,情况稍微复杂一些,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
css复制代码animation-range: cover; /* 等价于 cover 0% cover 100% */
animation-range: contain; /* 等价于 contain 0% contain 100% */
animation-range: cover 20%; /* 等价于 cover 20% cover 100% */
animation-range: contain 100px; /* 等价于 contain 100px cover 100% */


animation-range: normal 25%;
animation-range: 25% normal;
animation-range: 25% 50%;
animation-range: entry exit; /* 等价于 entry 0% exit 100% */
animation-range: cover cover 200px; /* 等价于 cover 0% cover 200px */
animation-range: entry 10% exit; /* 等价于 entry 10% exit 100% */
animation-range: 10% exit 90%;
animation-range: entry 10% 90%;

有以下关键词

  • cover:元素首次开始进入滚动容器可见范围(0%)到完全离开的过程(100% ),也就是元素只需要和可视范围有交集(默认)
  • contain:元素完全进入滚动容器可见范围(0%)到刚好要离开的过程(100% ),也就是元素必须完全在可见范围才会触发
  • entry:元素进入滚动容器可见范围的过程,刚进入是 0%,完全进入是 100%
  • exit:元素离开滚动容器可见范围的过程,刚离开是 0%,完全离开是 100%
  • entry-crossing:和entry比较类似,暂时没有发现明显差异
  • exit-crossing:和exit比较类似,暂时没有发现明显差异

下面做了一个示意图,表示各自的范围区间

image-20230722223333917

如果还是不太清楚,可以用下面这个工具去对比各自的差异

scroll-driven-animations.style/tools/view-…

image-20230722210228448

比如前面的列表进入时的动画,之前是用view(100% 0)实现的,大家有没有发现,这个效果其实和entry的示意效果一样的?

image-20230723120344950

如果用animation-range就很好理解了,这里需要进入动画,所以可以直接用entry

1
2
3
4
5
css复制代码div{
animation: appear 1s linear forwords;
animation-timeline: view();
animation-range: entry; /*只在进入过程中生效*/
}

同样可以实现相同的效果。

除此之外还可以同时设置进入和离开两种动画,这就需要定义两个动画,然后分别给两个动画定义动画区间,关键实现如下

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
css复制代码div{
animation: appear 1s linear forwards,
disappear 1s linear forwards;
animation-timeline: view();
animation-range: entry,exit; /*进入过程执行appear,离开过程执行disappear*/
}

/*出现动画*/
@keyframes appear {
0% {
opacity: 0;
transform: scaleX(0);
}

100% {
opacity: 1;
transform: scaleX(1);
}
}
/*离开*/
@keyframes disappear {
100% {
opacity: 0;
transform: scaleX(0);
}

0% {
opacity: 1;
transform: scaleX(1);
}
}

这样就得到一个进入和离开均存在动画的滚动列表

Kapture 2023-07-22 at 21.19.34.gif

完整代码可以查看:

  • CSS scroll-driven-animations-range (codepen.io)

另外,还可以将animation-range合并到同一个动画中,在关键帧前面加上entry这些关键词,这样就无需指定animation-range中了,示意代码如下

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
css复制代码div{
animation: animate-in-and-out 1s linear forwards;
animation-timeline: view();
}

@keyframes animate-in-and-out {
entry 0% {
opacity: 0;
transform: scaleX(0);
}

entry 100% {
opacity: 1;
transform: scaleX(1);
}
exit 100% {
opacity: 0;
transform: scaleX(0);
}

exit 0% {
opacity: 1;
transform: scaleX(1);
}
}

六、更多有趣的案例

除了以上一些案例外,CSS 滚动驱动动画还能做更多有趣的事情,这里推荐一个网站

scroll-driven-animations.style/

比如这个 Cover Flow 效果

Kapture 2023-07-23 at 14.34.42.gif

参见:scroll-driven-animations.style/demos/cover…

还有下面的卡片堆叠效果

Kapture 2023-07-23 at 14.45.07.gif

参见:scroll-driven-animations.style/demos/stack…

还有其他的例子就不展示了,大家可以自行体验

七、用一张图总结一下

总的来说,CSS 滚动驱动动画为以后的交互带来了无限可能,下面用一张图总结一下

CSS Scroll-driven Animations.png

最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发❤❤❤

欢迎关注我的公众号:前端侦探

本文转载自: 掘金

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

Android 应用程序如何抓取 Coredump 项目介绍

发表于 2023-07-18

项目介绍

github.com/Penguin38/O…

1
2
3
4
5
6
rust复制代码allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
1
2
3
4
arduino复制代码dependencies {
...
implementation 'com.github.Penguin38:OpenCoreSDK:opencore-1.4.3'
}
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
scss复制代码{
// 初始化组件
Coredump.getInstance().init();

// 设置超时时间 (单位秒)
Coredump.getInstance().setCoreTimeout(Coredump.DEF_TIMEOUT);

// 设置模式
// Coredump.getInstance().setCoreMode(Coredump.MODE_PTRACE | Coredump.MODE_COPY);
// Coredump.getInstance().setCoreMode(Coredump.MODE_PTRACE);
// Coredump.getInstance().setCoreMode(Coredump.MODE_COPY);
Coredump.getInstance().setCoreMode(Coredump.MODE_COPY2);

// 设置 Coredump 文件名规则
Coredump.getInstance().setCoreFlag(Coredump.FLAG_CORE
| Coredump.FLAG_PROCESS_COMM
| Coredump.FLAG_PID
| Coredump.FLAG_THREAD_COMM
| Coredump.FLAG_TID
| Coredump.FLAG_TIMESTAMP);

// 设置过滤条件
Coredump.getInstance().setCoreFilter(Coredump.FILTER_SPECIAL_VMA
// | Coredump.FILTER_FILE_VMA
// | Coredump.FILTER_SHARED_VMA
| Coredump.FILTER_SANITIZER_SHADOW_VMA);

// 设置 Coredump 保存目录
Coredump.getInstance().setCoreDir(...);

// Java Crash 生成 Coredump
Coredump.getInstance().enable(Coredump.JAVA);

// Native Crash 生成 Coredump
Coredump.getInstance().enable(Coredump.NATIVE);

// 设置监听器
Coredump.getInstance().setListener(new Coredump.Listener() {
@Override
public void onCompleted(String path) {
// do anything
}
});

// 主动生成当前时刻 Coredump
Coredump.getInstance().doCoredump();
}

文件格式

UML diagram (16).jpg

名字 用途
ELF Header ELF 头部信息,记录该 ELF 文件类型为 Core,指令集类型等信息,由数据结构 ElfN_Ehdr N=32,64组成,对应的信息可用 readelf -h 查看
Program Headers 记录该 Core 文件所有的段信息,包含每一个段在文件内的偏移,以及对应的虚拟地址偏移,段大小等信息,每一个段为数据结构 ElfN_Phdr 组成,对应的信息可用 readelf -l 查看
PT_NOTE 记录该程序的线程状态信息,线程 TID、寄存器信息等,辅助调试信息 AUXV,对应的信息可用 readelf -n 查看
PT_LOAD 与程序的 /proc/self/maps 一一对应,记录的是程序运行时虚拟内存空间,存放内存元数据
AUXV 记录着执行程序的 PHDR 地址(AT_PHDR)ElfN_Phdr 数据大小(AT_PHENT),以及执行程序共有多少个段(AT_PHENT),这三个数据是调试器所需要的,用于找到 link_map 信息。链接地址(AT_BASE Android 平台则是 /system/bin/linker 地址),此处信息与 /proc/self/auxv 一一对应

工作原理

核心技术依赖 Linux 写时复制机制用于父子进程内存拷贝,以及 ptrace 系统调用,它可用于挂入到目标进程上,并且可访问进程内存、寄存器等信息。程序需访问的文件节点:

节点 项目用途
/proc/<PID>/maps 程序虚拟内存空间,用于解析生成 Core 文件的 Program Headers
/proc/<PID>/auxv 程序辅助调试信息,用于解析生成 Note 段的 AUXV 部分
/proc/<PID/task/* 程序的线程信息,用于解析生成 Note 段的 Register 信息,以及暂停线程
/proc/<PID/mem 用于 COPY2 模式直接复制父进程内存

PTRACE API介绍

API官方文档使用:man7.org/linux/man-p…

#include <sys/ptrace.h>

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

参数类型 描述
PTRACE_ATTACH 挂入到目标PID进程上, 使目标PID进程成为调用进程的tracee,并可向tracee进程发送SIGSTOP信号来终止进程,不必等调用结束再来终止tracee,可使用waitpid(2)来等tracee结束
PTRACE_TRACEME 用于父子进程之间,表示该进程可被父进程跟踪
PTRACE_PEEKTEXTPTRACE_PEEKDATAPTRACE_PEEKUSER 允许跟踪进程读取被跟踪进程的虚拟内存地址
PTRACE_POKETEXTPTRACE_POKEDATAPTRACE_POKEUSER 允许跟踪进程修改被跟踪进程的虚拟内存地址
PTRACE_GETREGSET 允许跟踪进程读取被跟踪进程的寄存器信息
PTRACE_SETREGSET 允许跟踪进程修改被跟踪进程的寄存器信息
PTRACE_CONT 重启已经被终止的被跟踪进程
PTRACE_DETACH 会重启终止的被跟踪进程,并解除跟踪
PTRACE_SYSCALL 重启被跟踪进程,在下一个系统调用开始/退出时终止该进程。如strace工具
PTRACE_SINGLESTEP 重启进程,并且在下一条指令运行结束后切换到终止状态。如单步调试
PTRACE_GETSIGINFO 获取引起进程停止的信号信息,可获取siginfo_t结构信息进行修改,通过SETSIGINFO回传
PTRACE_SETSIGINFO
PTRACE_SETOPTIONS

暂停线程工作

使用 PTRACE_ATTACH 命令即可将目标线程暂停下来,线程进入 T 状态,在内核态上函数将会停留在 ptrace_stop 函数上等待,直到跟踪进程退出或接收到 PTRACE_CONT 或 PTRACE_DETACH 命令恢复状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
C++复制代码void OpencoreImpl::StopAllThread(pid_t pid)
{
char task_dir[32];
struct dirent *entry;
snprintf(task_dir, sizeof(task_dir), "/proc/%d/task", pid);
DIR *dp = opendir(task_dir);
if (dp) {
while ((entry=readdir(dp)) != NULL) {
if (!strncmp(entry->d_name, ".", 1)) {
continue;
}

pid_t tid = atoi(entry->d_name);
if (ptrace(PTRACE_ATTACH, tid, NULL, 0) < 0) {
JNI_LOGI("%s %d: %s\n", __func__ , tid, strerror(errno));
continue;
}
int status = 0;
waitpid(tid, &status, WUNTRACED);
}
}
}

获取线程寄存器

使用 PTRACE_GETREGSET 命令获取进程各个线程的寄存器信息,实际上内核返回的是线程内核态的上下文信息,对应的内核数据结构为 pt_regs。

UML 图 (7).jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
C++复制代码...
int index = 0;
while((entry=readdir(dp)) != NULL) {
if(!strncmp(entry->d_name, ".", 1))
continue;

pid_t tid = atoi(entry->d_name);
prstatus[index].pr_pid = tid;

uintptr_t regset = 1;
struct iovec ioVec;

ioVec.iov_base = &prstatus[index].pr_reg;
ioVec.iov_len = sizeof(core_arm64_pt_regs);

if (ptrace(PTRACE_GETREGSET, tid, regset, &ioVec) < 0) {
JNI_LOGI("%s %d: %s\n", __func__ , tid, strerror(errno));
index++;
continue;
}

index++;
}
...

PTRACE 读取内存

使用 ptrace 读取内存缺点在于非常的慢,原因是每次 syscall 只能访问 4、8 字节,取决于目标操作系统是 32 位还是 64 位,每一段内存的转储过程中都需要进行反复遍历,受限与 ptrace 读取能力,要想完整的保存 Android 应用程序的内存,短则 5 min 起步,长则 20 min 左右。

1
2
3
4
5
6
7
8
9
10
11
12
13
C++复制代码...
while(index < ehdr.e_phnum - 1) {
if (phdr[index].p_filesz > 0) {
switch (mode) {
case MODE_PTRACE: {
Elf64_Addr target = phdr[index].p_vaddr;
while (target < phdr[index].p_vaddr + phdr[index].p_memsz) {
long mem = ptrace(PTRACE_PEEKTEXT, pid, target, 0x0);
fwrite(&mem, sizeof(mem), 1, fp);
target = target + sizeof(Elf64_Addr);
}
}
...

父子进程内存拷贝

segmentfault.com/a/119000003…

在 Linux 系统中,调用 fork 系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程共用相同的内存页,而当子进程或者父进程对内存页进行修改时才会进行复制 —— 这就是著名的 写时复制 机制。

image.png
基于这个机制我们可以直接拷贝子进程虚拟内存空间,尽管子进程后边有修改过内存数据,但与父进程相识度非常的高,大多数情况下可以满足我们对本次内存分析的需求,并且相比纯 ptrace 读取内存能节省大量的抓取时间。

直接内存拷贝

子进程访问 /proc/<PID>/mem 节点获取父进程所有可访问的虚拟内存,性能最高的模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码...
while(index < ehdr.e_phnum - 1) {
if (phdr[index].p_filesz > 0) {
switch (mode) {
...
case MODE_COPY2: {
int count = phdr[index].p_memsz / sizeof(zero);
for (int i = 0; i < count; i++) {
memset(&zero, 0x0, sizeof(zero));
pread(fd, &zero, sizeof(zero), phdr[index].p_vaddr + (i * sizeof(zero)));
uint64_t ret = fwrite(zero, sizeof(zero), 1, fp);
if (ret != 1) {
JNI_LOGE("[%p] write load segment fail. %s %s",
(void *)phdr[index].p_vaddr, strerror(errno), maps[ntfile[index].start].c_str());
}

}
} break;
...

本文转载自: 掘金

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

手把手带你入门 Threejs Shader 系列(二)

发表于 2023-07-16

又是时隔两个月才更新这第二篇教程,原本以为最多一个星期就能更新出来,但写着写着总觉得有些地方解释的不够清楚,于是写到一半就放在一边,终于觉得不能再拖下去才找出来花两天继续写下去。可能依旧有些地方古柳没解释清楚,加上 shader 对初学者而言略有难度、略抽象,所以如果大家对本文任何地方有疑问,可以在评论区或群里咨询。

上篇文章「手把手带你入门 Three.js Shader 系列(一) - 牛衣古柳 - 20230515」里古柳带大家看了下几何体上的顶点属性,介绍了 GLSL 基础、顶点着色器、片元着色器等内容,相信大家现在对下面最简单的 shader 代码应该没有什么疑问了吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码const vertex = `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragment = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;

// const material = new THREE.MeshBasicMaterial({ color: 0x0ca678 });
const material = new THREE.ShaderMaterial({
vertexShader: vertex,
fragmentShader: fragment
});

本文古柳将介绍如何利用顶点上的 UV 纹理坐标并结合 GLSL 里几个常用的内置函数来实现一些简单的效果,带大家初步体验 Shader 编程的神奇之处。

三种修饰符 attribute/uniform/varying

之前提到顶点坐标 position、纹理坐标 uv、法线向量 normal 都是顶点上的数据,在 ShaderMaterial 的顶点着色器里能直接使用,因为里面自动声明了;如果使用的是 RawShaderMaterial 就需要手动声明才能使用。

1
2
3
C#复制代码attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

vec2 vec3 分别是二维、三维向量,特别需要注意的是 GLSL 语言里还需要加上 attribute 修饰符表明这个数据是每个顶点上都不同;此外还可以手动给每个顶点绑定所需的数据,如每个顶点都不同的一个随机值 aRandom,用于对顶点进行不同程度的位置偏移。

1
C#复制代码attribute float aRandom;

与 attribute 相对应的是每个顶点或像素数值都相同时变量使用 uniform 修饰符,比如传入统一的时间 uTime;

1
C#复制代码uniform float uTime;

而如果是顶点着色器里的变量想在片元着色器里使用就需要借助 varying 修饰符表示数据的传递,下面演示了如果将 uv 从顶点着色器传递到片元着色器里,需要借助 vUv 变量,在顶点着色器里声明后并赋值,然后在片元着色器里声明后就可以使用了,比如直接放到颜色里去使用,后续会讲解这里的含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C#复制代码// Vertex Shader
attribute vec2 uv;

varying vec2 vUv;

void main() {
vUv = uv;
}

// Fragment Shader
varying vec2 vUv;

void main() {
gl_FragColor = vec4(vUv, 1.0, 1.0);
}

注意上面不同修饰符对应的变量所采用的命令方式,attribute 的用 a 开头如 aRadom,uniform 的用 u 开头如 uTime,varying 的用 v 开头如 vUv,这是古柳觉得比较直观的一种变量命名方式,后续大家看其他教程时不一定都是这种方式,有的可能不用特定修饰符的打头如 time,有的可能用下划线如 u_time 等等,喜欢用哪种大家可自行选择。

将 uv 设置成颜色的多种方式

上面我们将 uv 纹理坐标传递到片元着色器里,接下来结合 GLSL 里一些内置函数,看看利用 uv 设置颜色会有哪些效果。

在上篇文章「手把手带你入门 Three.js Shader 系列(一) - 牛衣古柳 - 20230515」里古柳讲过 uv 值的范围为(0.0, 0.0)到(1.0, 1.0),原本顶点着色器里只有4个顶点才有 uv 值,但通过 varying 传递到片元着色器的变量都会在每个片元/像素位置得到插值后的数值,所以相当于这个 plane 每个像素都有 uv 数值,用作颜色后每个像素都能设置上颜色。

不过 shader 里无法像 JavaScript 里那样打印变量 console.log(uv) 查看数值情况、也不好 debug,所以大家刚开始接触时会觉得有些难受、有些抽象,无法理解一些数值长啥样、如何分布以及是如何产生作用的……

其实把数值在片元着色器里以颜色显示能有助于理解,虽然不是次次管用,但古柳觉得还是蛮有用的一个技巧。

首先回顾下,片元着色器里设置 gl_FragColor 为红色 vec4(1.0, 0.0, 0.0, 1.0),这段代码会通过 GPU 对所有 plane 上的像素执行,于是呈现出来的就是红色的平面。

1
2
3
4
C#复制代码void main() {
// 红色 rgba
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

渐变效果

如果用 vUv 里的 x 或 y 分量分别设置到 rgba 颜色里的 red 通道并赋值给 gl_FragColor,会有左右或者上下的黑色到红色的渐变效果。

1
2
3
4
5
6
7
C#复制代码varying vec2 vUv;

void main() {
// gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
// vUv.x => 0.0-1.0
gl_FragColor = vec4(vUv.x, 0.0, 0.0, 1.0);
}

1
2
3
4
5
6
C#复制代码varying vec2 vUv;

void main() {
// vUv.y => 0.0-1.0
gl_FragColor = vec4(vUv.y, 0.0, 0.0, 1.0);
}

大家如果刚接触 shader 不知道看到这一行代码有何感受?作为过来人,古柳只能尽量设想大家可能存在的困惑(如果有其他困惑可以评论或群里问),就是照理 position 是每个顶点的坐标,uv 是每个顶点的纹理坐标,按一般 JS 里的惯性思维大家会不会觉得 uv 应该是由所有顶点的纹理坐标所组成的数组,类似 [(0.0,0.0), (0.5, 0.5), (1.0, 0.0), ...] 这样的格式。

但其实并不是,shader 里的代码是对每个顶点或片元单独执行的,这里的 vUv 就只是每个片元其各自的数值(每个片元甚至连周围片元的数值是多少都不知道),比如左下角 vUv 为 (0.0, 0.0) 所以对应的 vUv.x=vUv.y=0.0,颜色为 vec4(0.0, 0.0, 0.0, 1.0) 即黑色,同理把左上角 (0.0, 1.0)、右下角 (1.0, 0.0)、右上角 (1.0, 1.0)、最中间 (0.5, 0.5) 等每个位置的数值分别带入上面的代码,就能得到上图的效果。

上面的例子可能理解起来还不复杂、不抽象,但对于刚接触 shader 的朋友来说,尽早养成将不同位置的数值带入代码然后分析可能呈现的效果,这是古柳在入门 shader 时觉得非常重要的一点学习经验。

如果以 vec3(vUv.x) 或 vec3(vUv.y) 的形式分别设置到 rgb 上,也就是三个颜色通道数值相同时,就会是黑白灰的效果。这里 vec3(0.5,0.5,0.5)=vec3(0.5)。

1
2
3
4
5
6
C#复制代码varying vec2 vUv;

void main() {
// 黑白灰
gl_FragColor = vec4(vec3(vUv.x), 1.0);
}

1
2
3
4
5
6
C#复制代码varying vec2 vUv;

void main() {
// 黑白灰
gl_FragColor = vec4(vec3(vUv.y), 1.0);
}

熟悉的青红、蓝粉效果

如果将 vUv 设置到 red 和 green 通道、blue 通道设为0.0,就是这个非常常见的 uv 青色红色颜色效果,如果大家用过其他一些3D软件,应该对这个图并不陌生。

1
2
3
4
5
C#复制代码varying vec2 vUv;

void main() {
gl_FragColor = vec4(vUv, 0.0, 1.0);
}

如果 blue 通道设为1.0,就是这种蓝色粉色的效果,见多了也会觉得很熟悉。

1
2
3
4
5
C#复制代码varying vec2 vUv;

void main() {
gl_FragColor = vec4(vUv, 1.0, 1.0);
}

颜色突变

除了渐变,我们可以结合 GLSL 的内置函数做出颜色突变的效果,借助 step(edge, x) 函数,其会返回0.0或1.0数值,如果 x<edge 返回0.0,如果 x>edge 返回1.0。step(0.5, vUv.x) 通过 vUv.x 和 0.5 比较,小于0.5的返回0.0,大于0.5的返回1.0,并将该 color 变成转换成 vec3() 格式,于是就是黑白突变的格式。

  • 链接:https://registry.khronos.org/OpenGL-Refpages/gl4/html/step.xhtml
  • 链接:https://docs.gl/sl4/step
1
2
3
4
C#复制代码void main() {
float color = step(0.5, vUv.x);
gl_FragColor = vec4(vec3(color), 1.0);
}

当然大家可能一开始想到的是用 if(vUv.x > 0.5) 条件判断的方式来进行设置,虽然也能成功,但一般 shader 里能用内置函数实现的都会优先用内置函数,一方面性能可能更好,另一方面也更为地道。

改变 step 第一个参数的数值,黑白突变的位置也会相应移动。

1
2
3
4
C#复制代码void main() {
float color = step(0.3, vUv.x);
gl_FragColor = vec4(vec3(color), 1.0);
}

1
2
3
4
C#复制代码void main() {
float color = step(0.7, vUv.x);
gl_FragColor = vec4(vec3(color), 1.0);
}

有时我们喜欢黑白颜色的位置互换,此时设置成 step(vUv.x, 0.5) 或者 step(0.5, 1.0 - vUv.x) 都能起到相同的效果,当然这里古柳用黑白指代只是顺口的,本质还是我们希望有些位置是0.0,有些位置是1.0,当需求不同时可以通过调整参数的位置使颜色突变的顺序改变。

上面讲了这么多效果,如果大家突然犯晕了,请记得古柳前文所说的,多把每个位置的具体数值带入到代码里去理解,相信就豁然开朗了。

1
2
3
4
5
6
7
8
C#复制代码void main() {
// float color = step(0.5, vUv.x);
// 黑白突变顺序互换
// float color = step(0.5, 1.0 - vUv.x);
// 两种方式都行
float color = step(vUv.x, 0.5);
gl_FragColor = vec4(vec3(color), 1.0);
}

重复效果、条纹效果

当我们想实现重复效果时,可以通过对 vUv 乘以一定倍数放大,比如0.0-1.0放大3倍变成0.0-3.0,然后用 fract() 函数取小数使得数值在 0.0-1.0 里循环重复,比如1.1、2.1取小数后都变回0.1,再将该数值转换成 vec3 再设置到颜色上,就会产生重复的黑白渐变效果。

1
2
3
4
5
C#复制代码varying vec2 vUv;

void main() {
gl_FragColor = vec4(vec3(fract(vUv.x * 3.0)), 1.0);
}

将上述重复的0.0-1.0数值先丢给 step() 函数再转换成 vec3,就能产生重复的黑白突变效果。这里不断套娃看着有些复杂,大家可以由里向外一步步理解每步的效果,这种重复的实现方式也是很常见很有用的,有必要熟练掌握,其实也不难。

1
2
3
4
5
C#复制代码varying vec2 vUv;

void main() {
gl_FragColor = vec4(vec3(step(0.5, fract(vUv.x * 3.0))), 1.0);
}

替换几何体

讲着讲着也不少内容了,最初设想是把下图里所有效果都在本文讲完,但怕大家来不及消化,本篇文章就先讲到条纹效果,最后如何绘制圆形图案、如何结合 mix 来混合颜色等内容留到下一篇里继续。

此外,大家可以将平面几何体换成立方体、球体、锥体等等,再对照上图自己尝试不看代码从头敲出每种效果,看看不同几何体、不同 uv 所带来的不同效果,由此加深对本文涉及的简单 shader 代码的理解和掌握。

1
2
3
4
js复制代码// const geometry = new THREE.PlaneGeometry(1, 1);
const geometry = new THREE.BoxGeometry(1, 1, 1);
// const geometry = new THREE.SphereGeometry(1, 32, 16);
// const geometry = new THREE.ConeGeometry(1, 2, 16, 1);

如果你喜欢本文内容,欢迎以各种方式支持,这也是对古柳输出教程的一种正向鼓励!

照例

最后欢迎加入「可视化交流群」,进群多多交流,对本文任何地方有疑惑的可以群里提问。加古柳微信:xiaoaizhj,备注「可视化加群」即可。

欢迎关注古柳的公众号「牛衣古柳」,并设置星标,以便第一时间收到更新。

本文转载自: 掘金

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

感谢钉钉小程序这些年赏我饭吃🍔,嗝~

发表于 2023-07-14

前言

弱弱的问,有多少同学知道钉钉也是有小程序的?那有多少同学参与过开发过钉钉小程序?深度参与过呢?有多深,至少50个页面起吧!一轮筛选过后应该没剩下多少勇士了,古茗应该算是勇士之一了。

在两年前我们决定all in钉钉小程序,因为h5的一些体验问题、业务需要快速迭代,所以小程序是首选;本身公司所有的用户、组织等数据也都是绑定在钉钉上,所以最后敲定钉钉小程序。tob业务目前有6个应用是由钉钉小程序承载的,页面500+,除了小程序还有h5应用也是跑在钉钉容器内,表单引擎、组件库、request、oss、F2、debug工具库等配套基建也是围绕着钉钉小程序卷了一遍,客观来说真的是靠钉钉小程序吃饭啊!

但是在这两年和钉钉小程序的py交易中,总能感受到钉钉小程序承接业务能力的天花板很低,业务稍微发力就能够到,以至于给产品留下了“坏印象”(前端不太行~😒)。所以我们也一直主动保持和钉钉小程序团队的沟通,几乎每周都会反馈问题和建议。但是伤心的是大部分的问题都是要靠自己解决,直到前几天的一个工单让我绷不住了。

批评

准备好了,开始喷了。

诡异的数字键盘

是什么原因让我必须站出来”夸”一下它呢?就是钉钉小程序ios端不能正确弹出数字键盘这件事的让我彻底绷不住了。冷静,我还是一名前端er,让我们回归技术

我们都知道手机浏览器会根据input的 type 类型唤起对应类型的键盘,手机可以直接点击查看

小程序侧也是针对 input 提供了对应的配置。但是你知道吗,钉钉小程序的这个配置就是个摆设,


于是我们怀着揣着疑问去钉钉社区里面提问,得到的回复如下,不知回复的人是否是官方技术,很震惊


得不到结果,接着又去官方提了工单,得到的肯定的回复是 ios webkit存在bug,需要等 apple 修复,我们(钉钉)处理不了 瞬间懵逼

单纯的我想那这个问题肯定很严重吧,去看看微信吧,结果微信是ok的,如图真机验证。


不对,可能微信小程序是同层渲染更高级,钉钉小程序暂时做不到吧。

那我们再去看抖音小程序,结果抖音也是ok的。啊~这…


那我们再去看看同父异母的支付宝小程序吧,咦~虽然展示有点奇怪,但至少能弹出数字键盘可以满足需求。

果然师出同门,看来弹出ios原生数字键盘真的有难度


于是找到钉钉官方希望他们能抄支付宝的方案,至少排期处理一下。得到的回复是 不予处理,你们自己可以写一个数字键盘 我特么…

有bug正常,谁都不能保证程序完美无缺,但是对待bug的态度要正确呀~这态度真的不太行

尤其是作为开放平台对于这么明显的bug说摆烂就摆烂吗?而且真的很难处理吗?真的是 webkit 的锅?第一张图的demo就是用钉钉webview容器打开的,为啥能正确弹出呢?

我这里也打个问号,希望懂的大佬帮忙答疑解惑一下

小程序开发工具2.0,KPI IDE?

不吐不快,接着夸

微信和支付宝都有对应的开发工具,我们开发钉钉小程序实际上用的也是支付宝IDE,但是前段时间偶然间发现钉钉官方出了一个钉钉小程序专用的开发工具,好奇的我当然要尝鲜一下,看看有哪些体验升级,于是下载安装

果然钉钉从不让我失望,帅不过三秒,dd.getAuthCode 无论如何都是请求失败,这个接口是拿钉钉授权token的,最前置的api,拿不到token后续什么都干不了


怎么办?惯例去找官方吧,得到的回复我都看笑了 别用这个,推荐我使用支付宝IDE进行开发 我特么~那你搞这个干嘛,还2.0

其实这个问题5.15我就提的工单,直到发文这天我还特意打开验证,依然没有修复,效率杠杠滴,难道真的只有我在用?

工单群已经被关闭了,无图无证据,这个回复就当我编的,纯当我自己使用不当,ok?反正我也不会再用了

慎用 regeneratorRuntime

大概半年前,小伙伴的电脑本地构建后访问一直报错,前端报错为 regeneratorRuntime_default is not function,但是上CI平台构建测试环境都是正常的,龟龟最烦这种问题,好在能稳定复现。

自己先模拟复现,一步步debug,因为我们中间还套了一层 Taro,重点都在排查 Taro 构建逻辑,排查到最后才往钉钉的构建链路上面想,排查到的结果真的是惊呆了😱😱,居然是钉钉在构建时来了一波偷天换日,甭管你是什么逻辑,只要是命名相同直接给你换掉。

钉钉小程序在编译时会内置一些变量,比如 regeneratorRuntime,应该还有其他的,估计支付宝小程序也这样(我没有验证过)

20230711150104.jpg
我理解和常用的环境变量 process.env.xxx 一样,在构建时会做替换。问题就出现在这,Taro 在特殊场景下会将 await 函数转化成如下

1
2
3
4
5
6
js复制代码// 部分代码只为了展示
...
regeneratorRuntime = webpack_required(20)

regeneratorRuntime_default = wepack_required.n(regeneratorRuntime)
...

碰巧命名撞上了,数据结构变了,页面直接GG。

我们去看了官方文档,只字未提!!! ~这么重要的黑盒不说明一下吗?

又想吐槽了,文档质量真的,想要的总是查不到,垃圾话一堆,有的时候都是直接看支付宝文档。
咱不拿微信小程序开发文档对比,就对标一下竞品飞书开放平台,不说别的,单拿出小程序的开发文档对比真是的被吊打。

更更重要的是当时也给钉钉官方提了意见可以把类似的说明加上去,他们说会加的;对,是的,他们会加的,几个月过去了现在好像还没加,可能今年内会加上?

还有这种编译时变量能定义的稍微 “抽象” 一点吗?别这么轻易就命中,你看一命中,半天的时间都给了钉钉小程序,你说是不是钉钉小程序赏饭吃。

我想上传超过一分钟的视频

看似很简单的需求,和钉钉小程序官方拉扯快俩月了,到现在都没有一个结论。

我们这边有大量上传下达的场景,比如拍出杯操作流程视频,一般都是在三分钟左右,本来门店就很忙没多少时间处理总部管理事项,现在一个视频还得分几段拍。真的每天都有人在吐槽

那有人会问,不能先拍摄再上传吗?答案是不行,钉钉会崩溃,可能大部分内存处理不好的app都会崩溃;首先钉钉小程序不提供分片上传,只能小程序端直推,内存占用会很大;并且大部分的门店工作机配置都不是很高,上传成功率会大大降低

我们也想了几个解决方案:

  1. chooseVideo 增加拍摄时长
  2. chooseVideo 降低分辨率,延长拍摄时长
  3. 分片上传
  4. 先拍摄->门店去压缩->再由钉钉小程序上传
  5. 不在钉钉小程序内处理上传

123钉钉目前还是都不支持,45考虑到操作链路也被否定掉了

恳请钉钉小程序团队考虑下,至少给我们一条路,工单很早就提了,适当的时候排下期?隔壁飞书这点飞的比你们快呀~

除了上传视频,钉钉的 bridge api 在文件的处理上也是有缺失的比如 openDoucemnt、chooseMessageFile、 downloadFile (不能处理文件)。

缺也就算了,有些 api 还不稳定,比如蓝牙相关 api,特定的场景就需要断开重连,而且稳定复现,最后我们只能用提示兜底。

“永远”升不上去的版本

实话实说,钉钉小程序的版本升级真的太不稳定了,刚迁移到钉钉小程序我们线上环境等着验证,它就是不升级,我们就真的傻乎乎的等了一个多小时,是真的!各种清除缓存没有用的,还是太年轻。后面我们成长了,直接卸载钉钉重新安装,这样每次打开小程序只都能同步获取最新版本

别跟我说用咋不用体验版?有些场景本来就只能发版后正式环境验证,比如一些消息类场景。

几乎团队内所有人都问过我一个问题,钉钉小程序这版本是咋么个玩法?怎么还是旧版本?怎么还不升级?

我们如是问了钉钉官方,得到如下回复:

  • 策略1 若检查到本地没有对应的离线包(首次打开), 则客户端会发起同步更新, 下载最新离线包;
  • 策略2 若距离上次发起更新超过 48 小时, 则客户端会发起一次同步更新,下载最新离线包;
  • 策略3 若距离上次打开该小程序超过 10 分钟但小于 48 小时, 则客户端会在展示本地离线包给用户的同时, 发起异步更新, 获取最新离线包信息, 有新版则静默下载更新.(下次启动即为新版本,部分保活小程序需要重启钉钉进程);
  • 策略4 若距离上次打开该小程序不超过 10 分钟, 不发起更新;

说实话,得到版本升级策略后我看了三遍,突然发现有点头疼~(开玩笑)

反正按回复的策略,我们验证的结论是:不能全信

除此之外还有一些疑惑:

  1. 新版本发布48小时后依然有旧版本日志(可能一直没有退出)
  2. 策略3何时触发离线包下载?
  3. 48小时对于紧急发布太久了

小程序的更新机制就不过多做介绍了,有需要复习点击 小程序的更新机制、 小程序更新策略介绍 了解下

无法感知冷启动

钉钉小程序在版本管理这一块感觉还有一个点,就是没有启动页面,所以无法感知冷启动。微信小程序的启动页


为什么会要纠结冷启动呢?因为冷启动会触发版本check,有新版本会异步下载新版本离线包,钉钉文档也是这样描述的。

小程序相对于传统的spa应用中间多了缓存层,小程序也会制定各种策略来管理缓存,好处不言而喻,坏处就是开发者想要精准控制版本相对来是比较困难,所以想要主动控制版本选项之一就是依赖冷启动来下载新版离线包,但是可以被感知的这一环节被阉割了。

那么来致敬下微信,微信是有几个准确的时机会销毁小程序,销毁后下一次打开也必定是冷启动,而且误差可控

  1. 小程序切到后台看不到后,一般 5 分钟内就会被微信客户端主动销毁;
  2. 在微信客户端下拉最近访问的小程序里删除,也会从内存里销毁;
  3. iOS 下如果 5 秒内连续大于等于 2 次的内存告警,会被销毁;

钉钉相应的功能也是有的,比如下拉副屏管理小程序。


但是1、2条我们也都验证过,从结果看并不能准确触发新版离线包的下载。

微信小程序在版本管理最近推出了重磅更新 「优先使用本地版本设置」 和 「小程序最低可用版本设置」,也就是说可以同步升级小程序;咱们钉钉啥时候借鉴一下,这个配置简直一劳永逸,b端应用就需要这样的功能。

header 有坨翔

依然是ios端,有点心疼钉钉小程序了,每次都是ios找麻烦

自定义header的场景,左侧绿色部分不能点击,点击事件不能透传被阻止冒泡,像盖了脱翔,真恶心


本来真实的业务左上角是个头像位,点击弹出操作侧边栏,ios怎么都点出…我们只能硬生生的用 username 现做占位

好在提了工单之后,这么明显的bug不能在不处理了(面子上过不去),答复是8月的版本会修复,我们是六月末提的,可以接受,至少是有盼头了

这是谁的菊花?

看看这是谁的菊花loading,我们花了小半天的时间排查,确定了,这个是钉钉的菊花!


起因是有用户反馈访问钉钉小程序,一直卡死在这个loading页面,看来晚饭又有着落了🍗。

于是对遍了项目内所有的菊花,没有一个跟它长得一样,它太紧致了~ 最后发现特喵的这个 loading 根本就不是我们的,压根就没有进入到我们的小程序,所以我们根本也无法控制。

气不打一处来,于是在反馈群敲下 “**钉钉”,最后离职战胜愤怒😡,一字字退回,最后 “对不起,给你添麻烦了,您在切换到4G试试的,我们也会继续跟进的”,估计客户也在想:”天天就知道道歉,啥也不是”

直到前几天还有用户反馈这个问题,没记错这个问题应该已经一年多了,我们也很无奈,鞭长莫及啊!


惯例我们其实早就联系钉钉,钉钉小程序官方也一直没有回复我们这个loading原因。

自己看现象分析应该是小程序容器初始化失败的loading,而且特定场景能稳定复现,尤其网络条件比较差时。

钉钉小程序官方大大抽空看看吧,至少给个官方话术让我们回复用户也好,敬礼了

maxlength 不生效

在来一个ios端的问题,嘻嘻,趁你病…

textarea maxlength 配置后,再有换行 + 空格的场景下,maxlength限制会失效,可以无限输入;钉钉官网的组件demo都可以复现

反馈给官方后,答复很熟悉,ios系统问题,不予处理

无灰度控制

目前发版还没有控制灰度的功能,所以只能自己处理,我们有两套解决方案

  1. 耦合到代码内,通过配置圈定范围来进行灰度控制
  2. 新创建灰度小程序,在由外层做流量分发

要是支持了就好了,我点一百个赞。估计你们也不会理我,全当我yy好了

长列表性能问题

老生长谈的话题了,同样的场景(一年区间的日历组件平铺)
一些低端机已经很卡了,尤其是在组合了一些动画效果后,但是webview打开毫无压力


也不是钉钉小程序自己的问题,所有小程序应该都是很头疼的场景,中间转换好几层后性能肯定会有丢失。

但是有一说一,对比过微信小程序,同机型同代码,微信小程序流畅度确实好很多~

这里也给ios正名,渲染性能差别不大,主要是安卓的低端机,卡起来很要命

最后的解决方案是我们自己实现懒加载

page-meta

page-meta 是一个小程序的原生组件,可以动态修改一些页面根节点的数据,我们有些需求需要用到这个组件,钉钉还是一句话 不支持

师出同门支付宝小程序已经实现了,适当的时间copy下?

点赞

钉钉小程序也不全是槽点,有些功能设计还是很开放的,有些建议的处理也还是很积极的

webview 通信

微信小程序想要和webview通信,在特定时机(小程序后退、组件销毁、分享等)才能通信;这种机制对h5应用渐进迁移小程序是非常痛苦的,好在钉钉小程序处理webview通信方案还是开放的

钉钉小程序提供 postmessage 方法,只要webview中的应用调用,小程序内就可以接受到回调,没有其他的限制。这种机制就可以做很多事情了,也就是做小程序的api h5应用全部能用

我们基于这点大概做了下面几个事情

  1. 登录授权同步
  2. 数据缓存同步
  3. 常用dd api映射
  4. 统一媒体资源上传
  5. h5落地页重定向到小程序

这套开放的通信机制还是对我们渐进迁移小程序帮了大忙的,当然现在我们已经80%业务已经完成了小程序重构,bridge 用的会越来越少

小程序版本管理

小程序不想IDE构建上传版本怎么办?想要自动化发布体验、正式版本怎么办?
内部小程序管理api钉钉小程序需要用它,现在它完全可以闭环整个钉钉小程序自动化发布流程,这个必须可以吹一波!

大约半年前发布流程

我们想要将钉钉小程序的完整发布流程都托管到内部的CI/CD平台上,但是当时的 dingtalk design cli 只是提供了简单的上传和预览,最后的发布版本还是需要到钉钉管理后台手动点一下,也就说最后一步要靠人工干预。

想象一下当天有两个版本在排队发布发布

  • 在线上回归时要分别发布体验预览版本
  • 验证通过后要分别到管理后台发布正式版本
  • 对应分支要push到master

如果不出意外还好,就怕但是,有人忘记点了正式版本发布呢?分支最后没有推送到master呢?已经推送到master但是线上有问题需要回滚呢?

因为小程序发布正式版本并没有相应的钩子回调导致我们CI/CD平台不能在正确时机的将分支合并到master,这之间有可能存在很多变数

所以我们给钉钉提了需求,希望增加发布预览版本、发布正式版本、回滚版本等cli。好在半年后,6月份功能发布了并且同步了我们

现在发布流程

  • CI/CD 打通gitlab和钉钉管理后台,全程只需在 CI/CD 平台上操作
  • 同时只能有一个条发布流程,允许多分支合并提测(不允许发布正式)
  • 验证回归完成后自动发布正式版本和合并master分支
  • 增加一键自动回滚,同时回滚钉钉小程序版本和代码

剧透下,后续我们负责CI/CD的同学也会做沉浸式的分享 😈😈😈

结尾

和钉钉的故事还很多,毕竟团队30多人或多或少都在和钉钉打交道,就算没bug早晚也得卷出bug。耳边忽然响起了渣渣辉的声音:你知道这几年我们是怎么过的吗,我们要想找bug,耶稣都拦不住

爱之深责之切,能承载我们这么多业务很感谢钉钉平台,说这么多,还是希望钉钉开放平台能做的更好,不只是看数据,更多是注重的开发体验和对待问题的态度,毕竟想要被产品大佬认可还是要靠钉钉小程序给的多不多 😄。

希望有一天能说出,钉钉小程序-你给的太多了。

钉钉小程序的开发者可能真的不多,但遇到疑难杂症也真的痛苦。相比微信小程序那么大的体量,活跃的官方答疑社区,钉钉有问题就只有一个给官方提工单这一个选择。 或许我们可以拉一个钉钉小程序的答疑群?抱团取暖?

最后来个小互动,如果微信小程序开发体验是10分,恳请参与过钉钉小程序开发的勇士评论区给它的开发体验打个分🚀🚀🚀

最后

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

本文转载自: 掘金

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

flutter 状态管理的学习,关于最基本的Inherite

发表于 2023-07-05

使用了flutter很久,觉得自己也很理解状态管理是怎么回事,公司的项目中使用的是Provider这个状态管理框架。但是因为都是封装好且易用始终没有下定决心好好钻研。好了吧,现在因为自己想写一个小的属于自己的项目,才发现自己对状态管理理解的还不是很透彻,所以就从头开始吧!
所有的状态管理都是基于flutter的 InheritedWidget 这个组件来封装的,那就从这个开始吧!
那么假设我们想要共享的是颜色的状态,我们应该怎么管理这个颜色的状态呢?我只需要把要共享的状态放在InheritedWidget 里

image.png

在不同的组件中使用了这个共享状态的颜色

image.png

image.png
这样只要改变颜色,这两句诗的颜色就会改变

image.png

image.png

image.png

这个只是组件间共享状态,跨页面的话把ColorInheritedWidget套在MaterialApp`的上层节点就可以实现跨页面去使用这个共享的颜色了。

image.png

源码 github.com/acsweets/di…

本文转载自: 掘金

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

从零开始uniapp微信小程序项目到发布(超级详细) 一、开

发表于 2023-06-26

我正在参加「掘金·启航计划」

最近微信小程序又掀起一波风潮,本文站在新手的角度出发,比较适合第一次使用uniapp 开发微信小程序的伙伴,或者没有过实战经验的小伙伴参考,从零搭建uniapp小程序项目

一、开发前的准备

1.1 下载工具

hbuilder首页:www.dcloud.io/hbuilderx.h…

下载hbuilder编辑器,选择对应的系统,Windows和mac正式版即可,下载好安装路径不要放在C盘,此编辑器作为主要开发编辑器

微信开发者工具:developers.weixin.qq.com/miniprogram…

下载微信开发者工具,选择对应的系统,正式版即可,下载好安装路径不要放在C盘,此工具主要用于开发是预览小程序

1.2 申请微信小程序

微信公众平台:mp.weixin.qq.com/

进入微信公众平台-> 点击立即注册-> 点击小程序-> 点击前往注册

image.png

填写信息。注意:邮箱需要,未被微信公众平台注册,未被微信开放平台注册,未被个人微信号绑定的邮箱

image.png

注册好后,扫码登陆平台,页面向下滚动,找到左边的开发,选择开发管理,右边页面选择开发设置,这里会有一个AppID,作为绑定开发者工具用(注意不要暴露,使用在后文)

image.png

二、新建项目

打开hbuilder编辑器选择文件->新建->项目

image.png

这里uniapp会提供很多初始化模板,可根据需求选择,此文以默认模板作为演示

  • 填写项目名称
  • 选择项目存放的路径
  • vue版本选择(此文以通俗易懂的2.0作为演示,3.0大同小异)
  • 默认不启用 uniCloud和git
  • 点击创建

image.png

试运行:

可能会出现没有配置微信开发工具情况,此时需要配置微信开发者工具路径

image.png

点击浏览 选择刚才安装微信开发者工具安装路径

image.png

配置微信开发者工具:

设置->安全设置-》开启服务端口

image.png
image.png

运行:

运行->运行到小程序模拟器->微信开发者工具

image.png

运行成功:

微信开发者工具会自动弹出来,可能没登录需要扫码登录微信开发者工具

image.png

三、项目结构

image.png

文件名 用途
pages 用于放置页面文件
static 静态文件位置
App.vue 主文件 同vue
main.js 同vue
manifest.json 项目配置(下文详讲)
pages.json 页面配置(下文详讲)
uni.scss 全局样式文件

3.1 manifest.json配置

此文主要用于开发微信小程序,其他的配置可以暂时不管,选择微信小程序配置

image.png

配置 作用
AppID 填写刚才在微信公共平台获取注册的 AppID
ES6转ES5 可以勾选 兼容
上传代码时样式自动补全 可以勾选 自动补全
上传时自动压缩 勾选 压缩代码
检查安全域名和TLS版本 不勾选 方便开发
位置接口 此配置需要 在微信公众平台申请相应的接口
unPush2.0 消息推送 需要申请
开启云端一体安全网络 加强安全(暂不演示)

3.2 pages.json

注意:此代码为伪代码,只为示例作用,方便查看属性配置使用 无法运行,需要自行配置需要的属性

image.png

pages配置

pages 数组第一项表示启动项 相当于首页,每新增一个页面 要在pages数组新增一个对象
path 文件路径
style 页面样式 下面为style详细配置
navigationBarTitleText 导航栏标题文字
navigationBarTextStyle 导航栏标题颜色,仅支持 black/white
navigationBarBackgroundColor 导航栏背景颜色
backgroundColor tab 的背景色
navigationStyle 导航栏样式,仅支持 default/customm,开启 custom 后,所有窗口均无导航栏,如使用自定义导航栏 需要设置custom
enablePullDownRefresh 是否开启下拉刷新
onReachBottomDistance 页面上拉触底事件触发时距页面底部距离,单位为px

globalStyle全局配置 等同于pages 是单独的对象

tabBar 配置

tabBar 解析
color tab文字默认颜色
selectedColor tab选中时的颜色
borderStyle tab上边框的颜色,仅支持 black/white
backgroundColor tab 的背景色
position 位置 可选值 bottom、top
list 数组 以下为数组对象详解
pagePath 页面路径,必须在 pages 中先定义
iconPath 图片路径,icon 大小限制为40kb,建议尺寸为 81px * 81px,当 postion 为 top 时,此参数无效,不支持网络图片
selectedIconPath 选中时的图片路径,icon 大小限制为40kb,建议尺寸为 81px * 81px ,当 postion 为 top 时,此参数无效
text tab 上按钮文字
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
js复制代码{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app",
"backgroundTextStyle": "black",
"enablePullDownRefresh": "false",
"onReachBottomDistance": 50
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationStyle": "default"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"position": "bottom",
"list": [{
"pagePath": "pages/component/index",
"iconPath": "static/image/icon_component.png",
"selectedIconPath": "static/image/icon_component_HL.png",
"text": "首页"
}, {
"pagePath": "pages/API/index",
"iconPath": "static/image/icon_API.png",
"selectedIconPath": "static/image/icon_API_HL.png",
"text": "购物车"
}]
}
}

四、封装网络请求

4.1配置环境变量

根目录下新增 common文件夹 新建文件operate.js

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码export default {
//接口
api: function() {
let version = wx.getAccountInfoSync().miniProgram.envVersion;
switch (version) {
case "develop": //开发预览版
return "http://xxx.xxx.xxx.xxx:xxx"
case 'trial': //体验版
return "http://xxx.xxx.xxx.xxx:xxx"
case 'release': //正式版
return "http://xxx.xxx.xxx.xxx:xxx"
default: //未知,默认调用正式版
return "http://xxx.xxx.xxx.xxx:xxx"
}
}
}
  • 调用 wx.getAccountInfoSync().miniProgram.envVersion获取微信环境
  • develop-开发预览版 trial-体验版 release-正式版

4.2封装请求

在common文件夹 新建文件request.js

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
js复制代码import operate from './operate.js'
const http = (options) => {
return new Promise((resolve, reject) => {
uni.request({
url: operate.api() + options.url, //接口地址:前缀+方法中传入的地址
method: options.method || 'GET', //请求方法:传入的方法或者默认是“GET”
data: options.data || {}, //传递参数:传入的参数或者默认传递空集合
header: {
'Authorization':"token", //自定义请求头信息
},
success: (res) => {
if(res.data.code!==200){ //自定请求失败的情况
uni.showToast({
title:res.data.msg,
icon: 'none'
});
}
resolve(res.data)//成功
},
// 这里的接口请求,如果出现问题就输出接口请求失败
fail: (err) => {
uni.showToast({
title: "" + err.msg,
icon: 'none'
});
reject(err)
}
})
})
}
export default http
  • 判断环境 不同的url
  • data需要给默认值 {}
  • header可以自定义 比如请求头的token 根据项目配置获取方式,例如uni.getStorageSync(‘Admin-Token’)
  • success 请求成功的情况下 如果code和后端商量不等于200 的情况返回什么 msg是根据后端定义的错误提示字段
  • 可以自行封装需要函数,如全局Loading效果之类

使用:

可统一新建api文件存放不同 页面的api请求

例如:

image.png

例如:user.js 这里统一存放关于用户的请求

1
2
3
4
5
6
7
js复制代码import http from '../common/request.js'
export function getUserInfo() {
return http({
url: '/getUserInfo',
method: 'post',
})
}

单个页面使用: 直接导入方法即可

1
js复制代码    import {getUserInfo} from '../../api/user.js'

或者 全局封装:在main.js里面复制以下函数 个人不是特别喜欢这种方式

1
2
3
4
5
js复制代码import request from './common/request.js'
Vue.prototype.$request = request

//文件内部使用 G
this.$request({url,method,data})

四、引入ui框架

此文以uview2.0为例

uniapp uView2.0插件市场:ext.dcloud.net.cn/plugin?id=1…

uview官网:www.uviewui.com/

点击下载插件并导入HBuilderX

image.png

未登录uniapp的需要先登录

此时hb会弹框,选择项目点击确定

image.png

uview基于sass所以先下载sass

1
2
3
4
5
js复制代码// 安装sass
npm i sass -D

// 安装sass-loader
npm i sass-loader -D

配置main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码// main.js
import App from './App'
import Vue from 'vue'
import uView from '@/uni_modules/uview-ui'

Vue.config.productionTip = false
App.mpType = 'app'
Vue.use(uView)

const app = new Vue({
...App
})
app.$mount()

// main.js

配置uni.scss 全局uview主题文件

1
2
js复制代码//文件最上方
@import '@/uni_modules/uview-ui/theme.scss';

配置App.vue 引入uView基础样式

1
2
3
4
js复制代码<style lang="scss">
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
@import "@/uni_modules/uview-ui/index.scss";
</style>

五、愉快的开始写代码

六、发布

uniapp编译后可以点击微信开发者工具的上传按钮

image.png

打开微信公众平台 选择版本管理 右侧可以看到开发版本 ,如只有一个版本会默认开发版本,如需要发布正式版本点击提交审核就可以,审核周期第一次稍微长一点,后续修改代码就很快

image.png

等待审核后就自动发布成功

注意事项: 正式服需要配置合法请求

入口:开发管理->开发设置

image.png

注意:所有相关请求都需要配置进去,包括使用的第三方接口如百度、阿里,否则正式服无法访问

压缩代码小技巧:

提示:小程序的代码包只能是 2MB,如果主包太大,就要考虑分包处理

运行时勾选 运行是否压缩代码 这样会节约很大空间

image.png

七、常见问题

小程序引入微信api后,很多功能无法使用,例如获取位置信息,获取用户收获地址

小程序部分功能需要申请权限,开发管理->接口设置

按照对应的文档申请所需权限,根据微信文档添加在项目配置内即可

image.png

刚进企业派发小程序任务,如何进行开发?

  • 先用git拉下项目
  • 叫公司在微信公众平台把你的微信加入管理员
  • 重新扫码微信开发者工具
  • 如appId没有自动匹配就手动传入

本文转载自: 掘金

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

WebSocket使用介绍,看这篇就够了

发表于 2023-06-14

一、WebSocket连接的建立、消息的接收和回复

  1. 当涉及到WebSocket框架的深度使用时,一个流行的选择是使用Java的Spring框架来实现。下面是一个基本的示例,演示了如何使用Spring WebSocket框架进行深度使用:

首先,确保你的项目中包含了Spring WebSocket的相关依赖。在pom.xml文件中添加以下依赖:

1
2
3
4
5
6
7
xml复制代码<dependencies>
<!-- Spring WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>

接下来,创建一个WebSocket配置类,用于配置和管理WebSocket连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/websocket").setAllowedOrigins("*");
}
}

在上面的示例中,MyWebSocketHandler是自定义的WebSocket处理程序,它将处理来自客户端的WebSocket连接和消息。

接下来,创建一个WebSocket处理程序类来处理WebSocket连接和消息的逻辑:

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复制代码import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class MyWebSocketHandler extends TextWebSocketHandler {

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 当WebSocket连接建立成功时调用
System.out.println("WebSocket连接已建立");
}

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 处理接收到的WebSocket消息
String payload = message.getPayload();
System.out.println("接收到消息:" + payload);

// 发送回复消息给客户端
String replyMessage = "收到消息:" + payload;
session.sendMessage(new TextMessage(replyMessage));
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 当WebSocket连接关闭时调用
System.out.println("WebSocket连接已关闭");
}
}

在上面的示例中,我们覆盖了afterConnectionEstablished、handleTextMessage和afterConnectionClosed等方法,以处理WebSocket连接的建立、接收消息和连接关闭等事件。

最后,你可以在你的应用程序中使用WebSocket连接,例如在控制器中处理WebSocket请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class WebSocketController {

@MessageMapping("/send")
@SendTo("/topic/messages")
public String sendMessage(String message) {
// 处理接收到的消息,并返回处理结果
String replyMessage = "处理消息:" + message;
return replyMessage;
}
}

在上面的示例中,我们使用@MessageMapping注解来指定处理客户端发送的消息的路径,然后使用@SendTo注解将处理结果发送回指定的订阅路径。

二、广播消息、用户认证

  • 广播消息:除了在handleTextMessage方法中直接发送回复消息给单个客户端外,你还可以使用SimpMessagingTemplate来广播消息给所有订阅了特定主题的客户端。可以在WebSocketConfig中注入SimpMessagingTemplate,然后在处理程序中使用它发送消息。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Autowired
private SimpMessagingTemplate messagingTemplate;

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 处理接收到的WebSocket消息
String payload = message.getPayload();
System.out.println("接收到消息:" + payload);

// 广播消息给所有订阅了特定主题的客户端
messagingTemplate.convertAndSend("/topic/messages", "收到消息:" + payload);
}
  • 用户认证和授权:如果你的应用程序需要对WebSocket连接进行认证和授权,你可以使用Spring Security框架来实现。你可以在WebSocketConfigurer的registerWebSocketHandlers方法中添加适当的拦截器来处理认证和授权逻辑。例如,使用HandshakeInterceptor进行握手阶段的认证,并使用ChannelInterceptor来拦截和处理每个消息的授权逻辑。
  • 处理其他WebSocket事件:除了处理连接建立、消息接收和连接关闭等事件外,你还可以覆盖其他WebSocket事件的处理方法,例如handleTransportError用于处理传输错误,handleBinaryMessage用于处理二进制消息等。根据你的需求,选择合适的方法进行覆盖和处理。
  • 集成其他功能:你可以将WebSocket与其他功能集成,例如数据库访问、消息队列、实时通知等。使用适当的组件和库,将WebSocket与你的应用程序的其他部分无缝集成,以实现更复杂的功能。

三、转换器、存储与过滤器

  1. 自定义消息转换器:Spring WebSocket框架支持使用不同的消息转换器来处理不同类型的消息。你可以自定义消息转换器,以实现自定义的消息格式和处理逻辑。通过实现WebSocketMessageConverter接口,你可以定义自己的消息转换器,并在WebSocketConfig中进行配置。
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
typescript复制代码@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/websocket")
.setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}

@Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
// 添加自定义的消息转换器
messageConverters.add(new MyMessageConverter());
return true;
}
}
  1. 广播消息给特定用户:除了广播消息给所有订阅了特定主题的客户端,你还可以使用SimpMessagingTemplate将消息发送给特定用户。通过将用户标识符作为消息的目的地,你可以确保消息仅发送给特定用户。
1
2
3
4
5
6
typescript复制代码@Autowired
private SimpMessagingTemplate messagingTemplate;

public void sendMessageToUser(String userId, String message) {
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", message);
}
  1. 会话管理和状态存储:WebSocket连接可以建立长时间的会话,你可能需要管理会话状态和存储用户相关的数据。你可以使用WebSocketSession对象来管理会话,并使用适当的存储机制(例如数据库、缓存等)来存储和检索会话状态。
  2. 消息拦截器和过滤器:Spring WebSocket框架提供了拦截器和过滤器机制,允许你在处理消息之前或之后执行额外的逻辑。通过实现HandshakeInterceptor和ChannelInterceptor接口,你可以编写自定义的拦截器和过滤器来处理身份验证、消息转换、日志记录等操作。

四、拦截器、STOMP协议

  1. 处理连接错误:WebSocket连接可能会出现错误,例如连接断开、超时等情况。你可以在handleTransportError方法中处理这些连接错误,并采取适当的措施,例如记录日志、重新连接等。
1
2
3
4
5
6
7
java复制代码@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 处理连接错误
System.out.println("连接错误:" + exception.getMessage());

// 可以根据具体情况采取适当的措施,例如关闭连接、重新连接等
}
  1. 使用WebSocket拦截器:WebSocket拦截器允许你在建立连接之前和之后执行额外的逻辑。你可以实现HandshakeInterceptor接口,并在WebSocketConfig中注册拦截器来处理握手阶段的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/websocket")
.setAllowedOrigins("*")
.addInterceptors(new MyHandshakeInterceptor())
.withSockJS();
}

// ...
}
  1. 使用自定义注解:你可以定义自己的注解,以便在WebSocket处理方法中进行更细粒度的控制。通过创建自定义注解并使用@Target(ElementType.METHOD)将其应用于处理方法,你可以在运行时执行特定的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomWebSocketHandler {
// 添加自定义属性
}




@CustomWebSocketHandler
public void handleWebSocketMessage(WebSocketSession session, TextMessage message) {
// 执行自定义逻辑
}
  1. 使用STOMP协议:STOMP(Simple Text Oriented Messaging Protocol)是一种简单的文本导向消息协议,用于在WebSocket之上进行消息传递。你可以使用Spring的STOMP支持来实现更高级的消息传递功能,例如订阅和发布、消息头、错误处理等。

五、广播管理、定时任务

  1. 消息广播和群组管理:除了向特定用户发送消息,你可能还需要实现消息的广播和群组管理功能。Spring WebSocket框架提供了SimpMessagingTemplate和SimpUserRegistry等工具类来实现这些功能。你可以使用SimpMessagingTemplate发送消息给特定主题或群组,而SimpUserRegistry则用于管理连接的用户和会话。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Autowired
private SimpMessagingTemplate messagingTemplate;

@Autowired
private SimpUserRegistry userRegistry;

public void broadcastMessage(String topic, String message) {
messagingTemplate.convertAndSend(topic, message);
}

public Set<String> getConnectedUsers() {
return userRegistry.getUsers().stream()
.map(SimpUser::getName)
.collect(Collectors.toSet());
}
  1. 定时任务和调度:在WebSocket应用中,你可能需要执行定时任务和调度任务,例如定时发送消息、定时清理会话等。你可以使用Spring框架提供的定时任务调度功能,结合WebSocket框架来实现这些任务。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Component
public class WebSocketScheduler {

@Autowired
private SimpMessagingTemplate messagingTemplate;

@Scheduled(fixedDelay = 5000) // 每5秒执行一次
public void sendScheduledMessage() {
String message = "Scheduled message";
messagingTemplate.convertAndSend("/topic/messages", message);
}
}
  1. 跨域访问控制:如果你的WebSocket应用需要跨域访问控制,你可以配置相应的跨域策略。Spring WebSocket框架提供了setAllowedOrigins方法来设置允许的跨域来源。
1
2
3
4
5
6
7
typescript复制代码@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/websocket")
.setAllowedOrigins("http://example.com")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
}
  1. 安全认证和授权:如果你的WebSocket应用需要安全认证和授权,你可以结合Spring Security框架来实现。通过配置适当的安全规则和认证机制,你可以确保只有经过授权的用户才能建立WebSocket连接和发送消息。
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
scala复制代码@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/websocket")
.setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
}

// ...
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/websocket").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.permitAll()
.and()
.logout()
.permitAll();
}
}

六、存储、多频道管理与部署

  1. 消息存储和持久化:如果你的应用需要存储和持久化消息,可以结合使用WebSocket和消息队列或数据库来实现。当接收到消息时,你可以将消息存储到数据库或消息队列中,并在需要的时候进行读取和处理。
1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码@Autowired
private MessageRepository messageRepository;

public void handleMessage(WebSocketSession session, TextMessage message) {
// 存储消息到数据库或消息队列
messageRepository.save(message.getPayload());
}

public List<String> getMessages() {
// 从数据库或消息队列中读取消息
return messageRepository.findAll();
}
  1. 多频道管理:如果你的应用需要管理多个频道或主题,可以考虑使用WebSocket的订阅和发布模式。你可以创建多个频道或主题,并在客户端订阅感兴趣的频道,以便接收相应的消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码@Autowired
private SimpMessagingTemplate messagingTemplate;

public void sendMessage(String channel, String message) {
messagingTemplate.convertAndSend("/topic/" + channel, message);
}

public void subscribeChannel(String channel, WebSocketSession session) {
messagingTemplate.subscribe("/topic/" + channel, session.getId());
}

public void unsubscribeChannel(String channel, WebSocketSession session) {
messagingTemplate.unsubscribe("/topic/" + channel, session.getId());
}
  1. 跨服务器部署:如果你的应用需要在多个服务器上部署,可以考虑使用分布式消息代理来实现跨服务器的消息传递。一种常见的方案是使用RabbitMQ或Apache Kafka等消息队列作为消息代理,以确保消息在不同服务器之间的可靠传递。
  2. 错误处理和异常处理:在处理WebSocket连接和消息时,可能会出现错误和异常。你可以实现WebSocketHandler接口的handleTransportError和handleMessageException方法来处理连接错误和消息异常,并进行相应的处理,例如记录日志、发送错误消息等。

public class MyWebSocketHandler implements WebSocketHandler {

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码    @Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 处理连接错误
}

@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
try {
// 处理接收到的消息
} catch (Exception e) {
// 处理消息异常
}
}
}

七、会话心跳

  1. 消息编解码:在WebSocket应用中,消息的编码和解码是一个重要的环节。你可以使用Spring提供的消息编解码器来实现消息的转换和处理。例如,可以使用TextMessageEncoder和TextMessageDecoder来进行文本消息的编解码。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class MyTextMessageEncoder implements MessageEncoder<String> {
@Override
public ByteBuffer encode(String message) throws EncodeException {
// 实现消息的编码逻辑
}
}

public class MyTextMessageDecoder implements MessageDecoder<String> {
@Override
public String decode(ByteBuffer message) throws DecodeException {
// 实现消息的解码逻辑
}
}
  1. 心跳检测:为了确保WebSocket连接的稳定性,你可以实现心跳检测机制来监控连接的状态。可以使用定时任务来发送心跳消息,并在一定时间内未收到心跳响应时,断开连接或进行相应的处理。
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
scala复制代码@Component
public class HeartbeatScheduler {

@Autowired
private SimpMessagingTemplate messagingTemplate;

@Scheduled(fixedDelay = 10000) // 每10秒发送一次心跳消息
public void sendHeartbeatMessage() {
messagingTemplate.convertAndSend("/topic/heartbeat", "Heartbeat");
}
}

public class MyWebSocketHandler extends TextWebSocketHandler {

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
if ("Heartbeat".equals(message.getPayload())) {
// 收到心跳消息,进行相应处理
// ...
} else {
// 处理其他消息
// ...
}
}
}
  1. 自定义拦截器:你可以使用自定义拦截器来对WebSocket连接进行拦截和处理。拦截器可以用于认证、授权、日志记录等方面。通过实现HandshakeInterceptor接口,你可以在建立连接之前和之后进行相应的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码public class MyHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 在建立连接之前进行处理
// ...
return true;
}

@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// 在建立连接之后进行处理
// ...
}
}
  1. 会话管理:在WebSocket应用中,你可能需要管理和跟踪每个连接的会话信息。你可以通过维护一个会话管理器来实现会话的创建、销毁和查询等操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码public class SessionManager {
private Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

public void addSession(WebSocketSession session) {
sessions.put(session.getId(), session);
}

public void removeSession(WebSocketSession session) {
sessions.remove(session.getId());
}

public WebSocketSession getSession(String sessionId) {
return sessions.get(sessionId);
}

public List<WebSocketSession> getAllSessions() {
return new ArrayList<>(sessions.values());
}
}

本文转载自: 掘金

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

1…747576…956

开发者博客

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