注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

发送验证码后的节流倒计时丨刷新 & 重新进入页面,还原倒计时状态

web
前言   最近在做一个 H5 工具,需要手机号 + 验证码登录,很自然地,点击发送验证码后需要等待一段时间才能重新发送,用于请求节流,避免用户疯狂点击:     不过这里其实有个隐藏需求——如果仍然在冷却时间内,那么用户无论是刷新或是关闭页面,再次打开登...
继续阅读 »

前言


  最近在做一个 H5 工具,需要手机号 + 验证码登录,很自然地,点击发送验证码后需要等待一段时间才能重新发送,用于请求节流,避免用户疯狂点击:





 

  不过这里其实有个隐藏需求——如果仍然在冷却时间内,那么用户无论是刷新或是关闭页面,再次打开登录弹窗,需要直接展示正确的倒计时状态


解决方案



使用经典的 localStorage




  1. 发送验证码时,将发送时间 (lastSendingTime) 存入 localStorage,并开启 60 秒倒计时。

  2. 倒计时结束后,清除 localStorage 中的 lastSendingTime

  3. 重新进入页面时,若 localStorage 中存有 lastSendingTime,则说明仍处于冷却时间内,那么计算出剩余的倒计时 N,并开启 N 秒倒计时。


Talk is cheap, show me the code!


  const [countdown, setCountdown] = useState(60) // 倒计时
const [canSendCode, setCanSendCode] = useState(true) // 控制按钮文案的状态
const [timer, setTimer] = useState() // 定时器 ID

async function sendVerificationCode() {
try {
// network request...
Toast.show({ content: '验证码发送成功' })
startCountdown()
setCanSendCode(false)
} catch (error) {
setCountdown(0)
setCanSendCode(true)
}
}

function startCountdown() {
const nowTime = new Date().getTime()
const lastSendingTime = localStorage.getItem('lastSendingTime')
if (lastSendingTime) {
// 若 localStorage 中存有 lastSendingTime,则说明仍处于冷却时间内,计算出剩余的 countdown
const restCountdown = 60 - parseInt(((nowTime - lastSendingTime) / 1000), 10)
setCountdown(restCountdown <= 0 ? 0 : restCountdown)
} else {
// 否则说明冷却时间已结束,则 countdown 为 60s,并将发送时间存入 localStorage
setCountdown(60)
localStorage.setItem('lastSendingTime', nowTime)
}

setTimer(
setInterval(() => {
setCountdown(old => old - 1)
}, 1000),
)
}

// 重新进入页面时,若 localStorage 中存有上次的发送时间,则说明还处于冷却时间内,则调用函数计算剩余倒计时;
// 否则什么也不做
useEffect(() => {
const lastSendingTime = localStorage.getItem('lastSendingTime')
if (lastSendingTime) {
setCanSendCode(false)
startCountdown()
}

return () => {
clearInterval(timer)
}
}, [])


// 监听倒计时,倒计时结束时:
// * 清空 localStorage 中存储的上次发送时间
// * 清除定时器
// * 重置倒计时
useEffect(() => {
if (countdown <= 0) {
setCanSendCode(true)
localStorage.removeItem('lastSendingTime')
clearInterval(timer)
setCountdown(60)
}
}, [countdown])

return (
{canSendCode ? (
<span onClick={sendVerificationCode}>
获取验证码
</span>

) : (
<span>
获取验证码({`${countdown}`})
</span>

)}
)

最终效果





总结


  一开始感觉这是个很简单的小需求,可能 20min 就写完了,但实际花了两个多小时才把逻辑全部 cover 到,还是不能太自信啊~


作者:Victor_Ye
来源:juejin.cn/post/7277187894872014848
收起阅读 »

别再用 display: contents 了

web
文章讨论了在网站上使用"display: contents"属性可能导致的潜在问题。作者强调了这种做法可能破坏网页的语义结构,并可能对可访问性产生不利影响。文章还提到了一些潜在的解决方案,以帮助开发人员避免这些问题。 下面是正文~~ display: cont...
继续阅读 »

文章讨论了在网站上使用"display: contents"属性可能导致的潜在问题。作者强调了这种做法可能破坏网页的语义结构,并可能对可访问性产生不利影响。文章还提到了一些潜在的解决方案,以帮助开发人员避免这些问题。


下面是正文~~


display: contents 介绍


CSS(层叠样式表)中的 display: contents 是一个相对较新的属性值,它对元素的布局和可视化有特殊的影响。当你对一个元素应用 display: contents,这个元素本身就像从DOM(文档对象模型)中消失了一样,而它的所有子元素则会升级到DOM结构中的下一个层级。换句话说,该元素的盒模型将被忽略,它的子元素会取而代之,就像直接插入到父元素中一样。


假设我们有这样一个HTML结构:


id="parent">
id="child1">Child 1
id="child2">Child 2

正常情况下,#parent#child1#child2 的父元素,它们在DOM和布局中有一个明确的层级关系。


现在,如果我们对 #parent 应用 display: contents


#parent {
display: contents;
}

在这种情况下,#parent 在页面布局中就像是“消失了”一样。它的所有子元素(这里是 #child1#child2)会直接升级到#parent所在的DOM层级。也就是说,在布局和渲染过程中,#child1#child2 将不再被视为 #parent 的子元素,而是像直接插入到 #parent 的父元素中一样。


这样做的结果是,任何应用于 #parent 的布局和样式都不会影响到页面的渲染,但 #child1#child2 会像正常元素一样被渲染。


主要用途:



  1. 语义改进:能够改进HTML结构,使其更符合语义,但不影响布局和样式。

  2. 布局优化:在某些复杂的布局场景中,它可以简化DOM结构,提高渲染性能。


display: contents 和可访问性的长期问题


从字面上看,这个CSS声明改变了其应用到的元素的显示属性。它使元素“消失”,将其子元素提升到DOM中的下一层级。


这种声明在很多方面都可能是有用的。讽刺的是,其中一个用例就是改善你工作的底层语义。然而,这个声明一开始的效果有点过头了。


CSS和可访问性


不是每个人都意识到这一点,但某些CSS会影响辅助技术的工作方式。就像烧毁你的房子确实会成功地除去其中可能存在的蜘蛛一样,使用 display: contents 可能会完全消除某些元素被辅助技术识别的关键属性。


简而言之,这会导致按钮不被声明为按钮,表格不被声明和导航为表格,列表也是如此,等等。


换句话说:当人们说“HTML默认是可访问的”时,display: contents 彻底破坏了这个“默认”。这不好。


可访问性从业者注意到了这个问题,并提出了完全合理的修复要求。特别值得一提的是Adrian Roselli的勤勉、有条理和实事求是的文档和报告工作。


修复已经完成,浏览器也已经更新,我们得到了一个快乐的结局。对吗?并不是那么简单。


回归问题


在软件开发中,回归可能意味着几件事情。这个词通常用于负面语境,表达更新后的行为不小心恢复到以前,不太理想的工作方式。


对于 display: contents,这意味着每个人的自动或近乎自动更新的浏览器抛弃了非常必要的错误修复,而没有任何警告或通知,就回到了破坏语义HTML与辅助技术交流的基础属性。


这种类型的回归不是一个令人讨厌的 bug,而是破坏了 Web 可访问性的基础方面。


Adrian注意到了这一点。如果你继续阅读我给你链接的部分,他继续注意到这一点。总之,我统计了关于 display: contents 的行为以不可访问的方式回归了16次的更新。


看问题的角度


制作浏览器是一件困难的事情。需要考虑很多、很多不同的事情,那还没考虑到软件的复杂性。


可访问性并不是每个人的首要任务。我可以在这里稍微宽容一些,因为我主要是尝试用我拥有的东西工作,而不是我希望能有的东西。我习惯了应对由于这种优先级而产生的所有小问题、陷阱和杂项。


然而,能够使用Web界面绝非小事。display: contents 的问题对使用它的界面的人们的生活质量有非常真实、非常可量化的影响。


我还想让你考虑一下这种打地鼠游戏是如何影响可访问性从业者的。告诉某人他们不能使用一个闪亮的新玩具永远不会受到欢迎。然后告诉他们你可以,但后来又不能了,这会削弱信任和能力的认知。


别用 display: contents


现在,我不认为我们这个行业可以自信地使用 display: contents。过去的行为是未来行为的良好指标,而走向地狱的道路是由好意铺成的。


我现在认为这个声明是不可预测的。常见的“只需用辅助技术测试其支持情况”的回应在这里也不适用——当前浏览器版本中该声明的期望行为并不能保证在该浏览器的未来版本中持续。


这是一件罕见且令人不安的事情——整个现代Web都是建立在这样的假设之上,即这样的事情不会以这种方式停止工作。这不是互操作性问题,而是由于疏忽造成的伤害。


display: contents 的回归给我们提供了一个小小的窗口,让我们看到浏览器制作的某些方面是如何(或不是如何)被优先考虑和测试的。


人们可以发誓说像可访问性和包容性这样的事情是重要的,但当涉及到这个特定的CSS声明时,很明显大多数浏览器制造商是不可信的。


这个声明在实践中的不稳定性代表了一种非常真实、非常严重的风险,即在你无法控制的情况下,可能会在你的网站或Web应用中引入关键的可访问性问题。


作者:王大冶
来源:juejin.cn/post/7275973778915573772
收起阅读 »

产品:请给我实现一个在web端截屏的功能!

web
一、故事的开始 最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。 作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行! 我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的...
继续阅读 »

一、故事的开始


最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。


作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行!


我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的,微信截图、Snipaste都可以做到的,自己实现的话,一是比较麻烦,而是性能也不会很好,没有必要,把更多的时间放在核心业务更合理!


结果产品跟我说因为公司内部有个可以用来解析图片,生成文本OCR的算法模型,web端需要支持截取网页中部分然后交给模型去训练,微信以及其他的截图工具虽然可以截图,但需要先保存到本地,再上传给模型才行。


网页端支持截图后可以在在截屏的同时直接上传给模型,减少中间过程,提升业务效率。


我一听这产品小嘴巴巴的说的还挺有道理,没有办法,只能接了这个需求,从此命运的齿轮开始转动,开始了我漫长而又曲折的思考。


二、我的思考


在实现任何需求的时候,我都会在自己的脑子中大概思考一下,评估一下它的难度如何。我发现web端常见的需求是在一张图片上截图,这个还是比较容易的,只需要准备一个canvas,然后利用canvas的方法 drawImage就可以截取这个图片的某个部分了。


示例如下:


<!DOCTYPE html>
<html>
<head>
<title>截取图片部分示例</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400"></canvas>
<br>
<button onclick="cropImage()">截取图片部分</button>
<br>
<img id="croppedImage" alt="截取的图片部分">
<br>

<script>
function cropImage() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var image = new Image();

image.onload = function () {
// 在canvas上绘制整张图片
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

// 截取图片的一部分,这里示例截取左上角的100x100像素区域
var startX = 0;
var startY = 0;
var width = 100;
var height = 100;
var croppedData = ctx.getImageData(startX, startY, width, height);

// 创建一个新的canvas用于显示截取的部分
var croppedCanvas = document.createElement('canvas');
croppedCanvas.width = width;
croppedCanvas.height = height;
var croppedCtx = croppedCanvas.getContext('2d');
croppedCtx.putImageData(croppedData, 0, 0);

// 将截取的部分显示在页面上
var croppedImage = document.getElementById('croppedImage');
croppedImage.src = croppedCanvas.toDataURL();
};

// 设置要加载的图片
image.src = 'your_image.jpg'; // 替换成你要截取的图片的路径
}
</script>
</body>
</html>

一、获取像素的思路


但是目前的这个需求远不止这样简单,因为它的对象是整个document,需要在整个document上截取一部分,我思考了一下,其实假设如果浏览器为我们提供了一个api,能够获取到某个位置的像素信息就好了,这样我将选定的某个区域的每个像素信息获取到,然后在一个像素一个像素绘制到canvas上就好了。


我本以为我发现了一个很好的方法,可遗憾的是经过调研浏览器并没有为我们提供类似获取某个位置像素信息的API。


唯一为我们提供获取像素信息的是canvas的这个API。


<!DOCTYPE html>
<html>
<head>
<title>获取特定像素信息示例</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="400"></canvas>
<br>
<button onclick="getPixelInfo()">获取特定像素信息</button>
<br>
<div id="pixelInfo"></div>

<script>
function getPixelInfo() {
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

// 绘制一些内容到canvas
ctx.fillStyle = 'red';
ctx.fillRect(50, 50, 100, 100);

// 获取特定位置的像素信息
var x = 75; // 替换为你想要获取的像素的x坐标
var y = 75; // 替换为你想要获取的像素的y坐标
var pixelData = ctx.getImageData(x, y, 1, 1).data;

// 提取像素的颜色信息
var red = pixelData[0];
var green = pixelData[1];
var blue = pixelData[2];
var alpha = pixelData[3];

// 将信息显示在页面上
var pixelInfo = document.getElementById('pixelInfo');
pixelInfo.innerHTML = '在位置 (' + x + ', ' + y + ') 的像素信息:<br>';
pixelInfo.innerHTML += '红色 (R): ' + red + '<br>';
pixelInfo.innerHTML += '绿色 (G): ' + green + '<br>';
pixelInfo.innerHTML += '蓝色 (B): ' + blue + '<br>';
pixelInfo.innerHTML += 'Alpha (透明度): ' + alpha + '<br>';
}
</script>
</body>
</html>


浏览器之所以没有为我们提供相应的API获取像素信息,停下来想想也是有道理的,甚至是必要的,因为假设浏览器为我们提供了这个API,那么恶意程序就可以通过这个API,不断的获取你的浏览器页面像素信息,然后全部绘制出来。一旦你的浏览器运行这个段恶意程序,那么你在浏览器干的什么,它会一览无余,相当于在网络的世界里裸奔,毫无隐私可言。


二、把DOM图片化


既然不能走捷径直接拿取像素信息,那就得老老实实的把document转换为图片,然后调用canvas的drawImage这个方法来截取图片了。


在前端领域其实99%的业务场景早已被之前的大佬们都实现过了,相应的轮子也很多。我问了一下chatGPT,它立马给我推荐了大名鼎鼎的html2canvas,这个库能够很好的将任意的dom转化为canvas。这个是它的官网。


我会心一笑,因为这不就直接能够实现需求了,很容易就可以写出下面的代码了:


html2canvas(document.body).then(function(canvas) {
// 将 Canvas 转换为图片数据URL
var src = canvas.toDataURL("image/png");
var image = new Image();
image.src = src;
image.onload = ()=>{
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d");
const width = 100;
const height = 100;
canvas.width = width;
canvas.height = height;
// 截取以(10,10)为顶点,长为100,宽为100的区域
ctx.drawImage(image, 10, 10, width, height , 0 , 0 ,width , height);
}
});


上面这段代码就可以实现截取document的特定的某个区域,需求已经实现了,但是我看了一下这个html2canvas库的资源发现并没有那么简单,有两个点并不满足我希望实现的点:


1.大小


当我们将html2canvas引入我们的项目的时候,即便压缩过后,它的资源也有近200kb:


Screen Shot 2023-09-09 at 3.15.10 PM.png


要知道整个react和react-dom的包压缩过后也才不到150kb,因此在项目只为了一个单一的功能引入一个复杂的资源可能并不划算,引入一个复杂度高的包一个是它会增加构建的时间,另一方面也会增加打包之后的体积。


如果是普通的web工程可能情有可原,但是因为我会将这需求做到插件当中,插件和普通的web不一样的一点,就是web工程如果更新之后,客户端是自动更新的。但是插件如果更新了,需要客户端手动的下载插件包,然后再在浏览器安装,因此包的大小尽可能小才好,如果一个插件好几十MB的话,那客户端肯定烦死了。


2.性能


作为业内知名的html2canvas库,性能方面表现如何呢?


我们可以看看它的原理,一个dom结构是如何变成一个canvas的呢!


它的源码在这里:核心的实现是canvas-renderer.ts这个文件。


当html2canvas拿到dom结构之后,首先为了避免副作用给原dom造成了影响,它会克隆一份全新的dom,然后遍历DOM的每一个节点,将其扁平化,这个过程中会收集每个节点的样式信息,尤其是在界面上的布局的几何信息,存入一个栈中。


然后再遍历栈中的每一个节点进行绘制,根据之前收集的样式信息进行绘制,就这样一点点的绘制到提前准备的和传入dom同样大小的canvas当中,由于针对很多特殊的元素,都需要处理它的绘制逻辑,比如iframe、input、img、svg等等。所以整个代码就比较多,自然大小就比较大了。


整个过程其实需要至少3次对整个dom树的遍历才可以绘制出来一个canvas的实例。


这个就是这个绘制类的主要实现方法:


Screen Shot 2023-09-09 at 4.08.30 PM.png


可以看到,它需要考虑的因素确实特别多,类似写这个浏览器的绘制引擎一样,特别复杂。


要想解决以上的大小的瓶颈。


第一个方案就是可以将这个资源动态加载,但是一旦动态加载就不能够在离线的环境下使用,在产品层面是不能接受的,因为大家可以想一想如果微信截图的功能在没有网络的时候就使用不了,这个肯定不正常,一般具备工具属性的功能应该尽可能可以做到离线使用,这样才好。


因此相关的代码资源不能够动态加载。


二、dom-to-image


正当我不知道如何解决的时候,我发现另外了一个库dom-to-image,我发现它打包后的大小只有10kb左右,这其实已经一个很可以接受的体积了。这个是它的github主页。好奇的我想知道它是怎么做到只有这么小的体积就能够实现和html2canvas几乎同样的功能的呢?于是我就研究了一下它的实现。


dom-to-image的实现利用了一个非常灵活的特性--image可以渲染svg


我们可以复习一下img标签的src可以接受什么样的类型:这里是mdn的说明文档


可以接受的格式要求是:



如果我们使用svg格式来渲染图片就可以是这样的方式:


<!DOCTYPE html>
<html>
<head>
<title>渲染SVG</title>
</head>
<body>
<h1>SVG示例</h1>
<img src="example.svg" alt="SVG示例">
</body>
</html>


但是也可以是这样的方式:


<!DOCTYPE html>
<html>
<head>
<title>渲染SVG字符串</title>
</head>
<body>
<div id="svg-container">
<!-- 这里是将SVG内容渲染到<img>标签中 -->
<img id="svg-image" src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /></svg>" alt="SVG图像">
</div>
</body>
</html>


把svg的标签序列化之后直接放在src属性上,image也是可以成功解析的,只不过我们需要添加一个头部:data:image/svg+xml,


令人兴奋的是,svg并不是只支持svg语法,也支持将其他的xml类型的语法比如html嵌入在其中。antv的x6组件中有非常多这样的应用例子,我给大家截图看一下:


Screen Shot 2023-09-09 at 4.49.40 PM.png


在svg中可以通过foreignObject这个标签来嵌套一些其他的xml语法,比如html等,有了这一特性,我们就可以把上面的例子改造一下:


<!DOCTYPE html>
<html>
<head>
<title>渲染SVG字符串</title>
</head>
<body>
<div id="svg-container">
<!-- 这里是将SVG内容渲染到<img>标签中 -->
<img
id="svg-image"
src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /><foreignObject>{ 中间可以放 dom序列化后的结果呀 }</foreignObject></svg>"
alt="SVG图像"
>

</div>
</body>
</html>


所以我们可以将dom序列化后的结构插到svg中,这不就天然的形成了一种dom->image的效果么?下面是演示的效果:


<!DOCTYPE html>
<html>
<head>
<title>渲染SVG字符串</title>
</head>
<body>
<div id="render" style="width: 100px; height: 100px; background: red"></div>
<br />
<div id="svg-container">
<!-- 这里是将SVG内容渲染到<img>标签中 -->
<img id="svg-image" alt="SVG图像" />
</div>

<script>
const perfix =
"data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'>";
const surfix = "</foreignObject></svg>";

const render = document.getElementById("render");

render.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");

const string = new XMLSerializer()
.serializeToString(render)
.replace(/#/g, "%23")
.replace(/\n/g, "%0A");

const image = document.getElementById("svg-image");

const src = perfix + string + surfix;

console.log(src);

image.src = src;
</script>
</body>
</html>


Screen Shot 2023-09-09 at 5.18.12 PM.png


如果你将这个字符串直接通过浏览器打开,也是可以的,说明浏览器可以直接识别这种形式的媒体资源正确解析对应的资源:


data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'><div id="render" style="width: 100px; height: 100px; background: red" xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>

实不相瞒这个就是dom-to-image的核心原理,性能肯定是不错的,因为它是调用浏览器底层的渲染器。


通过这个dom-to-image我们可以很好的解决资源大小性能这两个瓶颈的点。


三、优化


这个库打包后的产物是umd规范的,并且是统一暴露出来的全局变量,因此不支持treeshaking。


Screen Shot 2023-09-09 at 9.13.08 PM.png


但是很多方法比如toJpeg、toBlob、等方法我们其实都用不到,所以打包了很多我们不需要的产物,于是其实我们可以把核心的实现自己写一遍,使用1-2kb的空间就可以做到这一点。


经过以上的思考我们就可以基本上确定方案了:


基于dom-to-image的原理,实现一个简易的my-dom-to-image,大约只需要100行代码左右就可以做到。


然后将document.body转化为image,再从这个image中截取特定的部分。


Screen Shot 2023-09-09 at 9.23.06 PM.png


好了,以上就是我关于这个需求的一些思考,如果掘友也有一些其他非常有意思的需求,欢迎评论区讨论我们一起头脑风暴啊!!!


四、最后的话


以下是我的其他文章,欢迎掘友们阅读哦!


保姆级讲解JS精度丢失问题(图文结合)


shell、bash、zsh、powershell、gitbash、cmd这些到底都是啥?


从0到1开发一个浏览器插件(通俗易懂)


用零碎时间个人建站(200+赞)


另外我有一个自己的网站,欢迎来看看 new-story.cn


创作不易,如果您觉得文章有任何帮助到您的地方,或者触碰到了自己的知识盲区,请帮我点赞收藏一下,或者关注我,我会产出更多高质量文章,最后感谢您的阅读,祝愿大家越来越好。


作者:Story
来源:juejin.cn/post/7276694924137463842
收起阅读 »

谈谈干前端四年的几点感受

19年毕业的我,最开始怀揣着无限憧憬进入这个行业 不知不觉,已经工作4年了,如果算上大四实习的时光,也接近5年了。 4年间换了两家公司。 对于工作,我也有过很多的困扰和迷茫,现在依旧在走一步看一步的状态。 或许一觉起来工作没了,都是概率事件。 为什么会有这篇文...
继续阅读 »

19年毕业的我,最开始怀揣着无限憧憬进入这个行业


不知不觉,已经工作4年了,如果算上大四实习的时光,也接近5年了。


4年间换了两家公司。


对于工作,我也有过很多的困扰和迷茫,现在依旧在走一步看一步的状态。


或许一觉起来工作没了,都是概率事件。


为什么会有这篇文章?


一是与行业内大佬山月交流了一次,解惑答疑,有所感悟,想记录下心中所想;


二是呼应一年前的文章《谈谈干前端三年的几点感受》,对比看看自己想法变化。




前端在一个公司定位


一年前我说,前端在公司的定位是必要不重要,现在的想法依旧不变,只是对象变了。


整个技术开发人员,在一个公司的定位都是处于必要但不重要的角色,可替代性非常高。


可替代的属性越高,价值属性便越低。


也许局部或者短期看,技术开发的薪资是高的,但这对于公司来说,是成本。


如果公司要降本增效,最先压榨的也是这部分人员。


有这样一种说法,“技术傍身,编程改变世界”等等,其实是有些误导人的。


重要的从来都是想法,是渠道,不是技术。


只要能想到,大概率都能实现,实现不了就加班想办法实现。


能够提出想法的人,才处于一个公司重要的地位。


就正如我看到的,一个公司,核心业务人员离职,公司上下极力挽留,一个开发离职,领导回复祝好。




我们要学些什么


学有价值的东西。


何谓价值,价值就是经过时间考验,依旧不变的东西。


我个人极其反对花太多精力深入研究各个技术的源码。


技术说到底是工具,工具最重要的使用,不是本身,而且只要是工具,便都有替代品,


有新的技术,又会有新的源码,学是学不完的。


而计算机行业,不变的什么,有价值的是什么?


是计算机网络,是计算机组成原理,是数据结构与网络,是操作系统,是信息安全,是项目管理,是软件测试。


以上都是大学中计算机类的专业课程,这些年没有变过。


具体一点,与前端不变的什么,有价值的是什么?


是网络请求,是nginx,是性能优化,是前端工程化,是脚手架,是对UI的基本审美。


当然了,如果做可视化,音视频,跨端方向等等也有属于自己的专业壁垒。


以上我提到的都属于目前自己看到,前端通用知识。




如何评价自己的薪资和技术水平


其实,我们学到的99%的知识是无用的,或者学完不用就忘记了。


我学习的目的很单纯,就是为了跳槽涨薪。


让自己的实力能够匹配和市场对我的要求和我自己期望的薪资。


那么,如何得知自己的薪资水平,是否符合年限,技术水平,是否符合市场要求?


需要比较。


我们常说,人要和自己比,不要和别人比,人别人,气死人。但其实这是句自我安慰的鸡汤,不是生活的真相。


人得自知,不比较,如何自知呢?


当然,这种事情很难和同事交流,也不建议问同行。


问同事,同事水平参差不齐,给不了你准确的答案。


问同行,同行也许自己也发展不顺,多半是同病相怜,或是能给你方向,但给不了方案。


我的建议是做付费咨询,向行业内大佬求助。


同行业过来人的经验,更靠谱一些,做不到感同身受,但能明白心中所想。


我想看到这篇文章的各位,都关注过几位技术大佬,那就去主动搭讪,说明来意,付费咨询。


“我的薪资目前和我的工作年限匹配吗?我的技术还应该补充哪部分?我应该如何学习某些知识,有没有推荐的学习路线和文章,等等之类的问题”


而付费是获取能够心安理得的咨询,不要计较那一顿火锅的钱。


但其实只要搭讪成功,大佬一般都不会收费。




2023年了,还要不要往大厂努力?


当然要啊,这个想法和一年前比,没有动摇。


但是大厂今年都在降本增效,门槛更高了,面试更难了,工作更卷了。


但这并不能成为放弃的借口。无论结果与否,人总得有个工作上的目标啊。


正如我的前同事今年初送给我的一句话,


“备考公务员或者向大厂努力,总得找个目标,找件事情去做吧。如果觉得大厂太卷,那就干一年就走,但这个经历会成为你永久的财富。”




前端已死?


今年上半年受chatGPT冲击,这个言论甚嚣尘上。


我这里不讨论死不死的事情,只觉得这个问题很荒谬,多思考思考就会明白这句话,在创造概念,制造焦虑。


仔细想想,这波言论最大的受益方还是 做职业教育的那帮人。




拿多少钱干多少活还是干多少活拿多少钱?


第一家公司一切都很好。


我还是义无反顾的离开了,离开后公司发展得更好了。


离职的直接原因就是,当时要前端使用uni-app做跨端应用,去替换客户端的工作。


这项工作的直接影响就是,整个公司只前端部门加班,我疲惫不堪的同时,uni-app踩不完的坑,也身心俱疲。


每当加班到很晚时,委屈总是涌上心头。


受不了之时,就只剩一个走字。


这时候,小兵心态就出现了,拿多少钱多少活,我就拿这点工资,整这么多活,我无法承担,只能摆烂了。


当然,也有领导心态,你得先努力干,干出成果,我拿着成果才去争取涨薪。


这中间就有一个认知偏差,双方因为角度不同,无法理解对方的心态和想法。


领导觉得小兵不懂他的良苦用心,小兵觉得领导天天画饼。


哪种做法是对的呢?


得就事论事。


如果这件事情,对你有成长,有帮助,比如做一些工程化,脚手架,性能优化的工作,肯定得先干出成果。


如果这件事情,对自己是一种消耗,那还是持小兵心态吧。


如何区分这件事情是对你的帮助还是对自己的消耗呢?


其实自己最清楚。


如果干这项工作时,总是充满期待,充满激情,加班也无怨无悔,那就是帮助。


如果干这项工作时,总是身心俱疲,牢骚满腹,加班会委屈抱怨,那就是消耗。




人都是不愿意被管理的


这句话出自山月,我听后豁然开朗。


今年听闻行业内很多公司严抓考勤,多了很多制度和会议,吐槽随处可见。


新的领导,势必会带来新的管理制度,新的实施方案。


人都是不愿意被管理的,所以会引起各种不适应,但是一般一个月后都会销声匿迹,因为已经适应了。


无法评价这些变化的好与坏。


身处其中的我们只有慢慢适应,打工到哪里都一样,只要被管理着,都需要面临不同的问题。




最后


其实,回顾毕业这些年,19年谣传资本寒冬,然后是防疫三年,到后来前端已死,到现在无法言状的行业颓势。


正应了那句话,“今年是过去十年最差的一年,却可能是未来十年最好的一年。”


然后呢,这句话想表达什么?仅仅是传播了一个情绪,放在近些年都受用。


我想说,


“大环境的整体劣势,不影响个人的局部优势,我们要想办法建立这种个人优势;”


“种一棵树最好的时间是十年前,其次是现在。”


作者:虎妞先生
来源:juejin.cn/post/7258509816691834917
收起阅读 »

突然发现,前端好像没几个做到 CTO 的……

web
大家好,我 ssh,这几天,在推上看到了一个节奏,swyx 小哥发了一篇关于前端天花板的讨论,吸引了 150w 左右的阅读。主要是在讲前端天花板,前端人员被集中捆绑在低级别工程师这个行列中,通往 VP(技术副总裁) 或者 CTO 的大门是朝他们关上的。 而他...
继续阅读 »

大家好,我 ssh,这几天,在推上看到了一个节奏,swyx 小哥发了一篇关于前端天花板的讨论,吸引了 150w 左右的阅读。主要是在讲前端天花板,前端人员被集中捆绑在低级别工程师这个行列中,通往 VP(技术副总裁) 或者 CTO 的大门是朝他们关上的。


微信图片_20230726161930.jpg


而他发出这篇推文的起因,正是 swyx 正文里配的这篇文章截图:



经过我查询,这是 honeycomb.io 的一篇博客 成为工程 VP 里的一段话。他们没有刻意贬低前端工程师,只是客观的描述了统计情况而已,这反而是更加令人悲观的。


其实这个问题我也不止一次想过,尤其是有一些校招的同学特别喜欢思考这个问题,之前一次校招的宣讲会后答疑环节,也有不止一个同学过来问我这个问题。


确实,仔细想想,国内的前端界比较出名的前端出身做到很高职位的,玉伯算是一个代表,后期他基本上已经成为一个产品设计方面的负责人了,脱离了单纯前端的范畴。主导设计了云凤蝶、语雀这些非常 nb 的产品。


image.png


image.png


但是除了玉伯之外,让我们仔细想想,是不是大概率情况下,前端升到更高级别负责人的概率比后端要低很多呢?第一印象是如此,而且我以前在阿里没有隐藏职位的时候,在钉钉上直接搜索 title 来确认过这个问题。


在阿里,资深前端专家则对应前端的 p9,资深技术专家对应后端的 p9,这两个职位的人数在我印象里是相差很悬殊的,很多倍的关系…… 而且我记得 p9 的前端非常稀少。这其实也侧面反应出大家的主观感受是确有其事的。


写到这里,我深感焦虑,赶紧去问问万能的 AI:


ai.sb


卧槽,被辱骂了一通。拿出我大哥 Dan 也没用!


回到正题,swyx 又提到,有人说只要成为全栈就好了。



直接看看这张图:



全栈并不是口头说说那么简单,有一个小型公司的 CTO 也现身说出了自己的看法:


image.png


后端普遍认为前端简单,在国外也一样



前端成为产品总负责人,比成为技术 vp 的路径要概率更大一些,这也符合玉伯的发展路径:


image.png


关于这件事儿,Hacker News 也有一些讨论,不过质量比较差,走偏了:


开始讨论后端的烂代码了


讨论男女平等


我的看法


看完了几乎全部的讨论以后,我感觉国外的开发者对于前端天花板的看法和国内差不多,确实是认为有后端工程背景的人升为 VP/CTO 级别的概率比较高,而前端更倾向于在框架中日复一日的迷失。


以我自己的职业经历来说,假设我在使用 React 技术栈,今天在用 redux,明天出了一个 redux-toolkit 来解决 redux 太烂的问题,你迁移过去了,学到了很多范式很充实。再过几个月,又来了个 recoil,又来个 jotai。好像在很忙碌的学习,但其实都没有脱离状态管理的范畴,就像是被困在小学里反复的读五年级,而后端的人可能去研究更广阔的东西了。比如:




  1. 稳定性:各种灾备方案,限流等操作。




  2. 高并发:延迟,tps。




  3. 一致性:数据正确性。




而前端比较好的处境,就是在一家前端主导产品的公司(比如最近比较火的 AffiNE)参与核心功能的研发,那么可以接触到前端比较深入的一些技术,而且有一帮大牛同事可以陪你玩最新的技术栈。又或者是参与到大型公司的基础架构建设,我了解到的比如性能监控、低代码搭建、serveless 建设、自研 JS 引擎、自研 Rust 编译库,也可以获得比较深入的技术提升。


不过,大部分人的整个职业生涯可能都在做一些 Vue 或者 React 的应用开发,后台管理系统、活动页等等。。。是不是就完了?人生没希望了?


再问问 AI:



我丢,这 AI 吃枪药了吧。


不过他骂的也不无道理,安心做个平庸的前端又怎么样呢?比起很多职业来说,坐在电脑前敲敲你喜欢的代码,当个快乐的小前端,拿个 10-20k 的薪资,不够过日子的嘛?想想土木老哥在烈日下的样子?



我对于平庸人生的看法,把注意力转移到自己的生活中,有一个可以坚持热爱的爱好(比如我自己就喜欢踢足球和健身)。做一个自信阳光的小骚年,不是也很不错吗?


不要高杠杆买房,不要负债太多,保持一定的积蓄习惯,注意资产的合理配置。你肯定能比一般职业的人过得更好,欲望才是万恶之源。


当然,这只是比较悲观的想法,如果你有一颗上进的心,拼到个资深工程师,有点管理能力的话,再争取个前端小 leader 当当,过上小资点的生活也没问题。


我的意思是,人生短短几十年,职业不是生活的全部。假设你全心全意拼在工作上,到了 40 岁挣了一堆钱,落了一身的病。你觉得你真的快乐吗?如果钱是快乐的全部的话,李玟也不会得抑郁症,张朝阳也不会因为抑郁放弃公司管理跑去修行了。


总结


前端确实天花板比较低,不过那又咋样呢?最终能成为 VP 的人也没几个,如果你从小就就是天之骄子,目标是星辰大海,那你考上 985 的计算机系应该没什么问题,在校招的时候就果断选后端吧,确实有几率爬的更高点,但是付出相应的代价也是必要的(后端头发平均值明显低于前端)。


屏幕截图 2023-07-29 052610.jpg


否则,你就做个快乐的小前端,也比其他大多数职业过得舒服。


作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7261807670746513463
收起阅读 »

第一个可以在条件语句中使用的原生hook诞生了

大家好,我卡颂。 在10月13日的first-class-support-for-promises RFC中,介绍了一种新的hook —— use。 use什么?就是use,这个hook就叫use。这也是第一个:可以在条件语句中书写的hook可以在其他hook...
继续阅读 »

大家好,我卡颂。


在10月13日的first-class-support-for-promises RFC中,介绍了一种新的hook —— use


use什么?就是use,这个hook就叫use。这也是第一个:

  • 可以在条件语句中书写的hook

  • 可以在其他hook回调中书写的hook


本文来聊聊这个特殊的hook


欢迎加入人类高质量前端框架研究群,带飞


use是什么


我们知道,async函数会配合await关键词使用,比如:

async function load() {
const {name} = await fetchName();
return name;
}

类似的,在React组件中,可以配合use起到类似的效果,比如:

function Cpn() {
const {name} = use(fetchName());
return <p>{name}</p>;
}

可以认为,use的作用类似于:

  • async await中的await

  • generator中的yield


use作为读取异步数据的原语,可以配合Suspense实现数据请求、加载、返回的逻辑。


举个例子,下述例子中,当fetchNote执行异步请求时,会由包裹NoteSuspense组件渲染加载中状态


当请求成功时,会重新渲染,此时note数据会正常返回。


当请求失败时,会由包裹NoteErrorBoundary组件处理失败逻辑。

function Note({id}) {
const note = use(fetchNote(id));
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
);
}

其背后的实现原理并不复杂:

  1. Note组件首次renderfetchNote发起请求,会throw promise,打断render流程

  2. Suspense fallback作为渲染结果

  3. promise状态变化后重新触发渲染

  4. 根据note的返回值渲染


实际上这套基于promise的打断、重新渲染流程当前已经存在了。use的存在就是为了替换上述流程。


与当前React中已经存在的上述promise流程不同,use仅仅是个原语primitives),并不是完整的处理流程。


比如,use并没有缓存promise的能力。


举个例子,在下面代码中fetchTodo执行后会返回一个promiseuse会消费这个promise

async function fetchTodo(id) {
const data = await fetchDataFromCache(`/api/todos/${id}`);
return {contents: data.contents};
}

function Todo({id, isSelected}) {
const todo = use(fetchTodo(id));
return (
<div className={isSelected ? 'selected-todo' : 'normal-todo'}>
{todo.contents}
</div>
);
}

Todo组件的id prop变化后,触发fetchTodo重新请求是符合逻辑的。


但是当isSelected prop变化后,Todo组件也会重新renderfetchTodo执行后会返回一个新的promise


返回新的promise不一定产生新的请求(取决于fetchTodo的实现),但一定会影响React接下来的运行流程(比如不能命中性能优化)。


这时候,需要配合React提供的cache API(同样处于RFC)。


下述代码中,如果id prop不变,fetchTodo始终返回同一个promise

const fetchTodo = cache(async (id) => {
const data = await fetchDataFromCache(`/api/todos/${id}`);
return {contents: data.contents};
});

use的潜在作用


当前,use的应用场景局限在包裹promise


但是未来,use会作为客户端中处理异步数据的主要手段,比如:


  • 处理context

use(Context)能达到与useContext(Context)一样的效果,区别在于前者可以在条件语句,以及其他hook回调内执行。


  • 处理state

可以利用use实现新的原生状态管理方案:

const currentState = use(store);
const latestValue = use(observable);

为什么不使用async await


本文开篇提到,use原语类似async await中的await,那为什么不直接使用async await呢?类似下面这样:

// Note 是 React 组件
async function Note({id, isEditing}) {
const note = await db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{isEditing ? <NoteEditor note={note} /> : null}
</div>
);
}

有两方面原因。


一方面,async await的工作方式与React客户端处理异步时的逻辑不太一样。


await的请求resolve后,调用栈是从await语句继续执行的(generatoryield也是这样)。


而在React中,更新流程是从根组件开始的,所以当数据返回后,更新流程是从根组件从头开始的。


改用async await的方式势必对当前React底层架构带来挑战。最起码,会对性能优化产生不小的影响。


另一方面,async await这种方式接下来会在Server Component中实现,也就是异步的服务端组件。


服务端组件与客户端组件都是React组件,但前者在服务端渲染(SSR),后者在客户端渲染(CSR),如果都用async await,不太容易从代码层面区分两者。


总结


use是一个读取异步数据的原语,他的出现是为了规范React在客户端处理异步数据的方式。


既然是原语,那么他的功能就很底层,比如不包括请求的缓存功能(由cache处理)。


之所以这么设计,是因为React团队并不希望开发者直接使用他们。这些原语的受众是React生态中的其他库。


比如,类似SWRReact-Query这样的请求库,就可以结合use,再结合自己实现的请求缓存策略(而不是使用React提供的cache方法)


各种状态管理库,也可以将use作为其底层状态单元的容器。


值得吐槽的是,Hooks文档中hook的限制那一节恐怕得重写了。


作者:魔术师卡颂
链接:https://juejin.cn/post/7155673959084589070
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从尤雨溪这两天微博募捐,思考开源如何赚大钱

这两天,尤大在他的微博表示,他打算开启国内开源捐赠计划,截止本文发帖为止,已经有 6k / 月的固定充电了。 这个数额目前还是比较小的,企业级别的 sponsor 应该还没有出现,光靠个人捐赠的话这点钱真的完全不够团队开销的。 正巧我看到了 Ink 作者的...
继续阅读 »

这两天,尤大在他的微博表示,他打算开启国内开源捐赠计划,截止本文发帖为止,已经有 6k / 月的固定充电了。


这个数额目前还是比较小的,企业级别的 sponsor 应该还没有出现,光靠个人捐赠的话这点钱真的完全不够团队开销的。






正巧我看到了 Ink 作者的一篇文章,讲述他在开源软件如何稳定搞钱这方面的思考,觉得他的很多观点非常犀利,值得各位前端开发者同学一起学习,毕竟大家未来可能有搞开源的一天。
接下来是他的这篇 Generating income from open source 的内容:


最近,Ink 的知名度越来越高,并且已经被一些知名公司使用了一段时间。然而,与大多数其他开源项目一样,Ink没有任何收入。


我开始研究各种选项,以改变这种情况,并以某种方式开始收费,这样它就可以支持我以及 Ink 和相关项目(如 Ink UIPastel) 的进一步开发。


本文是我在这个主题上所学到的内容的简要版本。


不起作用的方法


以下是我认为维护者无法从他们的项目中获得收入的原因。


依靠个人捐赠


能够有人愿意支持你是很好的,但是每月 5 美元的捐赠无法维持生活。这是社区对你工作的感激的一种方式,但不应被视为稳定的收入来源。


除非你是社区中极少数非常受欢迎的开发者之一,否则接受事实,不会有足够多的人订阅每月捐赠。


尽管如此,我认为个人捐赠并不是答案。


期望公司捐赠


你构建了很火的项目,并在生产环境中稳定运行,他们从中获益良多。当然,他们肯定知道要回馈一下,毕竟他们赚了那么多钱,是这样的吗?


我们需要最终明白一些简单的道理,改变我们的预期。


经营业务意味着最大化收入和最小化支出。企业不会为了只是为了对你好点,而增加一个长期开支。(万恶的资本家)


企业习惯于以金钱交换价值。开源维护者需要考虑到这一点。你提供价值,他们从中受益并为此付费。


确实有一些拥有强大开源文化的公司可以持续给他们依赖的项目提供重大的每月捐赠,但不幸的是,他们是个例。


完全依赖捐赠或赞助


下面这句话,是不是很耳熟?



请赞助我吧,这样我就可以继续开发我的开源项目。



我们整了一个漂亮的 GitHub 赞助页面,然后坐在那里等待有人注册。你能想象一个企业采用类似的策略吗?它会在一个月内破产倒闭。


我们需要理解我们的项目对公司所提供的价值,并开始收费,就像我们经营一家企业,销售一种有用的产品。


认为没有人愿意付费或者定价不够高


在几家中小型初创公司工作过后,我现在明白几年前自己有多么愚蠢,以为每月 200 块的订阅费是天价,或者公司不愿意为工具付费。纯属扯犊子。


公司为员工解决日常问题和开发产品支付数百万的钞票。如果你的项目解决了他们的问题,使他们的团队不必自己解决,他们会支付比你认为的价值高 10 倍、100 倍甚至 1000 倍的费用。而且,他们会很满意。


公司已经为各种工具和费用支付数万元每月。无论你要求什么,实际上对他们来说都是九牛一毛。把你的产品价格翻倍吧,没毛病。


害怕或者羞于索要信用卡信息


我们不需要为我们的工作收费找理由。没有什么可羞耻的。


你为解决一个问题而付出你的努力。有人为了这个问题请你付费解决,别多虑了。


有效方法


我们喜欢抱怨没人支付维护者的费用,但实际上有很多建立在开源基础上的成功企业。以下是它们持续收入的秘诀:


商业许可证


Dave DeSandro 的Metafizzy提供各种 JavaScript 库,其中包括 Isotope - 用于创建灵活网格布局的库。Isotope 是开源的,但根据你的使用方式有不同的许可证



  1. 开源许可证。


这个许可证允许在个人或开源项目中免费使用 Isotope。



  1. 商业许可证。


这个许可证允许你在几乎任何商业应用中使用 Isotope。实际上,任何希望使用它的公司很可能需要购买商业许可证。


商业许可证的定价根据使用人数而不同:

  • 单个开发者的费用为 25 美元。
  • 8 名开发者团队的费用为 110 美元。
  • 无限数量的开发者的费用为 320 美元。

请注意,这些不是订阅,而是一次性付款。


商业许可证本身是一份 PDF 文件,支付后通过 Gumroad 发送给你。



  1. 商业 OEM 许可证。


该许可证适用于先前的商业许可证未涵盖的其他用途,特别是 UI 构建器、SDK 或工具包。对于商业 OEM 许可证没有公开的定价,这意味着它比前几个等级要贵得多。这些用例可能意味着 Isotope 作为用户界面或产品提供中的关键组成部分,因此公司愿意支付高额费用。


我喜欢这种方法的原因


这看起来是对开源进行收费最简单的方式,因为 Metafizzy 为同一份代码提供了不同的许可证,许可证本身是一个 PDF 文件。没有专业版,没有许可证密钥,也没有其他需要维护的东西。个人开发者可以免费使用同样的工具,而公司则支付合理的价格。


为更多功能收费


Mike Perham 的Sidekiq是一个在 Ruby 应用程序中基于 Redis 的后台作业的著名的库。Sidekiq 提供了 3 种不同的计划:



  1. 开源版。


Sidekiq 免费提供一个有限的开源版本。尽管它被称为“开源”,但 LGPL 许可证似乎允许你在商业应用中使用免费版本。


开源计划不提供任何客户支持,有问题就去提 GitHub Issue 吧。



  1. 专业版。


专业版每月收费 99 美元(或 995 美元/年),提供更多的功能。例如,批处理后台作业、通过更高级的 Redis API 提供的增强可靠性。专业版还包括通过电子邮件提供的客户支持。



  1. 企业版。


企业版根据你运行的 Sidekiq 实例数量,以 229 美元/月或更高的价格提供全部功能。


Sidekiq 的表现非常出色,根据 Mike 在 Hacker News 的最新评论,它现在每年创造 1000 万美元的收入。


有趣的是,他还提到,你可以通过其他开源 Ruby gem 组装 Sidekiq 的大多数付费功能,但是设置和维护起来需要很多时间。最终,你可能会得到一个比经过多次测试的 Sidekiq 还要糟糕的系统,所以购买功能齐全的 Sidekiq 似乎是明智之举。



Sidekiq 的大多数商业功能都可作为开源软件包获得,但是当你将 3-6 个这些功能集成在一起时,复杂性会悄然而至。自己构建往往会导致一个比我精心策划的成熟、经过良好调试的系统还要差的系统。



一旦你注册了 Sidekiq,你将获得访问私有 Ruby gem 服务器的权限,可以从中下载并更新应用程序中的sidekiq gem。他自己构建了这个系统,并表示不用花太多时间维护它。


我喜欢这种方法的原因


Sidekiq 首先是一个很棒的开源项目。在 Ruby 社区中,当你需要后台队列时,它成为了一个明显的选择。这是 Sidekiq 唯一的营销渠道。


然后,开发人员向他们的朋友和公司的管理人员推荐 Sidekiq。随着他们的应用程序扩大,客户有明显的动机支付 Sidekiq 以解锁更多功能。


托管版本


最近,越来越多的企业将其整个产品开源,并提供托管版本以获取收费。

  • Plausible Analytics - 一个注重隐私的 Google Analytics 替代方案。托管版本每月起价 9 美元。
  • PostHog - 产品分析、功能标志、A/B 测试等多个数据工具的组合。托管版本采用按用量计费,前 100 万个事件免费,之后每个事件收费 0.0003068 美元。
  • Metabase - 数据库仪表板。托管版本每月起价 85 美元。

这些只是我能想到的例子,还有许多类似的例子。


我喜欢这种方法的原因


你可以构建一次应用程序,并将相同版本作为开源和托管付费产品提供。你可能会想:“为什么有人愿意为可免费获得的东西付费”。然而,Plausible Analytics 每年收入 100 万美元,所以肯定有很多人愿意支付小额的月费来享受他们的产品,而不用自己搞乱七八糟的服务器啥的。


收费维护和高级材料


Moritz Klack、Christopher Möller、John Robb 和 Hayleigh Thompson 的React Flow是一个用于交互式流程图的 React 库。这是一个可持续的开源项目,与我以前见过的任何项目都不同。React Flow 为公司提供了一个专业版订阅,其中提供以下功能:

  • 访问专业版高级用例示例。
  • 优先解决 GitHub 上的问题。
  • 每月最多 1 小时的电子邮件支持。
  • 最有趣的是,我引用一下,“保持库的运行和维护,采用 MIT 许可证”。

在整个定价页面上,大部分文案都集中在最后一点上。React Flow 不是一个容易用其他东西替代的库,所以公司很可能有兴趣确保它得到良好的维护,并继续使用 MIT 许可。


John 在他们的博客上写了一篇优秀的文章,名为“Dear Open Source: let’s do a better job of asking for money”,我建议你阅读一下。我对此非常着迷,所以给 John 发了一封邮件,提出了一些后续问题,他非常友善地回答了我关于这个话题的许多宝贵的知识。


以下是我从我们的邮件往来中总结出的要点:

  • 包装很重要。公司内部持有信用卡的人希望看到他们一直在看到的“定价”页面。GitHub 赞助页面行不通。React Flow 最初有一个这样的页面,但几乎没有获得任何收入。当他们推出一个类似 SaaS 的产品网站,并提供几个定价层次时,情况改善了。
  • 让大家发现专业版计划。React Flow 组件显示一个指向他们网站的链接,并要求开发人员在订阅专业版计划后将其删除。即使在不这样做的情况下删除它仍然完全合法和可以接受,但它作为一个不会强迫的好方法,可以促使人们查看专业版计划。
  • 公司在有支持的情况下更有安全感。React Flow 每月提供最多 1 小时的电子邮件支持,所以我自然而然地问如果客户花费的时间超过 1 小时会发生什么。John 表示,即使如此,他们还是会继续通过电子邮件提供支持,最后一切都会平衡,因为有很多客户根本不联系他们。他还认为,电子邮件支持会给人一种保险的感觉,因此公司知道如果有需要,他们可以找到他们,即使他们从未这样做过。
  • 为人们提供可以立即购买和访问的东西。我想知道那些对专业版客户可用的高级示例有多重要,因为与其他好处相比,它们似乎只是一种美好的附加功能。令人惊讶的是,John 有不同的看法。他坚信,购买后立即提供一些有价值的东西可以将他们的专业版计划与咨询公司或服务区分开来。这还为客户提供了一个参考点,他们可以在项目中使用并学习。此外,这还有助于吸引那些对 React Flow 感兴趣的公司。

我喜欢这种方法的原因


React Flow 以其出色的开源库而闻名,但他们找到了一种明智的方式在商业上获得收入。他们在定价、包装和支持方面的决策都非常明智,并成功地转化了开源用户为付费客户。


这是我了解到的一些有关将开源项目变为可持续收入的方法。希望这些例子能给你提供一些灵感和启示!


支持包


最后但同样重要的是,你可以围绕你的开源工作建立一家咨询公司,并向依赖于该工作的公司提供专业知识支持。

  • Babel 在他们的Open Collective页面上提供了每年 2.4 万美元的计划,其中公司每月可以获得 2 小时的电子邮件或视频支持。
  • curl 提供商业支持,甚至包括开发定制功能和代码审核以了解你如何使用 curl。
  • Filippo Valsorda向公司提供每年五位数的保留协议。Filippo 与工程师会面,了解他们的需求,并在开发他的开源软件时确保这些需求得到满足。Filippo 是一个密码学专家,所以公司可以签订更昂贵的合同,以获得他在与密码学相关的任何事物上的专业知识,而不仅仅是他自己的项目。

我喜欢这种方法的原因


为公司提供付费支持使你的项目保持完全开源的同时,比 Pro 订阅带来更多的收入。这个过程很难,但对于一个习惯于作为员工工作的人来说,很有吸引力。


结论


偶尔会在 Hacker News 上看到人们讨论开源模式的缺点,护者没有从受益于他们工作的公司那里获得任何收入。


这不公平。他们能做些什么?可以有多种可行的选项可以生成可持续的收入,也有许多成功的例子说明人们今天正在这样做,并且已经持续了很久。这也可能适用于你,快去试试吧,否则你永远不会知道。


作者:ssh_晨曦时梦见兮
链接:https://juejin.cn/post/7245584147456278589
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

图片转换成webp

web
webp的几个问题 1. 什么是webp? 最直接的就是一个图片的后缀是.webp而不是.png/.jpeg等,官方的说法就是由Google开发的一种用于图像压缩的现代格式,目的就是减小图片的大小从而提高网页加载速; 2. 是不是所有浏览器都支持webp图片?...
继续阅读 »

webp的几个问题


1. 什么是webp?


最直接的就是一个图片的后缀是.webp而不是.png/.jpeg等,官方的说法就是由Google开发的一种用于图像压缩的现代格式,目的就是减小图片的大小从而提高网页加载速;


2. 是不是所有浏览器都支持webp图片?如何判断浏览器是否支持webp格式的图片


不是所有的浏览器都支持 WebP 图片格式,但大多数主流的现代浏览器都已经支持了。以下是一些常见的浏览器对 WebP 格式的支持情况:



  • Google Chrome:支持 WebP 格式。

  • Mozilla Firefox:支持 WebP 格式。

  • Microsoft Edge:支持 WebP 格式。

  • Safari:从 Safari 14 开始,支持 WebP 格式
    要判断浏览器是否支持 WebP 格式的图片,可以使用 JavaScript 进行检测。以下是一种常用的方法:


function isWebPSupported() {
var elem = document.createElement('canvas');
if (!!(elem.getContext && elem.getContext('2d'))) {
// canvas 支持
return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
// canvas 不支持
return false;
}

if (isWebPSupported()) {
console.log('浏览器支持 WebP 格式');
} else {
console.log('浏览器不支持 WebP 格式');
}


上述代码通过创建一个 canvas 元素,并尝试将其转换为 WebP 格式的图片。如果浏览器支持 WebP 格式,则会返回一个以 "data:image/webp" 开头的数据 URL。


通过这种方式,你可以在网页中使用 JavaScript 检测浏览器是否支持 WebP 格式,并根据需要提供适当的替代图片


3. 图片转换成webp之后一定会比之前的图片更小吗?


答案是否定的。一般来说,具有大量细节、颜色变化和复杂结构的图像可能会在转换为 WebP 格式后获得更好的压缩效果,反之有些转换后可能会比之前更大;所以最好是图片转换为 WebP 格式之前,建议进行测试和比较不同压缩参数和质量级别的结果,以找到最佳的压缩设置,对最终转换后变成更大的建议不做转换


4. 如何将图片转换成webp



  • 图像编辑软件 如 Adobe Photoshop、GIMP 或在线工具,如 Google 的 WebP 编码器。这些工具可以让你将现有的图像转换为 WebP 格式,并选择压缩质量和压缩类型(有损或无损)

  • 插件转换webp插件文档链接接入


image.png


5. 项目中如何接入??


思路:



  • 第一步肯定是转化将项目中的存储的图片文件通过插件转换出webp格式的图片

  • 判断网页运行的浏览器是否支持webp格式的图片,如果支持,将项目中所有使用png/jpeg的图片的全部替换成webp


6. 转换出项目中图片的webp格式的图片


const imagemin = require("imagemin");
const imageminWebp = require("imagemin-webp");

function transformToWebp(destination, filePaths) {
await imagemin([filePath || `${destination}/*.{jpg,png}`], {
destination: `${destination}/webp/`, // 转换出的webp图片放置在什么目录
plugins: [imageminWebp({quality: 75})] // 使用imageminWebp转换转换质量级别设置多少
})
}

具体到项目中,我们只希望转换我们当前正在开发的文件夹中的图片,而且已经转化的未作修改的就不要再重复转化; 如何知道哪些是新增的或者修改的呢? 想一想🤔️,是不是“git status”可以看到
所以开始做如下调整


// 获取git仓库中发生变更的文件列表
function getGitStatusChangedImgFiles() {
return String(execSync('git status -s'))
.split('\n')
.map(item => item.split(' ').pop()
.filter(path => path.match(/\.(jpg)|(png)/))
);
};

返回一个包含变更图片文件路径的数组['src/example/image/a.png','src/example/image/b.png', '……']


const imgPaths = getGitStatusChangedImgFiles()
async function transformAllChangedImgToWebp() {
const resData = await promise.all(
imgPaths.map(path => {
const imgDir = path.replace(/([^\\/]+)\.([^\\/]+)/i, "") // src/banners/guardian_8/img/95_copy.png => src/banners/guardian_8/img/
return transformToWebp(imgDir, path)
})
)
const allDestinationPaths = resData.map((subArr) => subArr[0].destinationPath)
// 如果这里我们想将生成的webp图片自动的add上去,那么就这样:
execSync(`git add ${allDestinationPaths.join(" ")}`);
}



image.png


什么时候转换成webp最好?


我们在commit的时候进行转换图片,以及自动将转换的图片进行提交
这样我们就可以运用git的钩子函数处理了;


npm install husky --save-dev

// .husky/pre-commit中
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

current_branch=`git rev-parse --abbrev-ref HEAD`

if [[ $current_branch === 'main']]; then
# 生成 webp 图片
npm run webp -- commit
fi

这样在我们commit时就会自动触发pre-commit钩子函数,在package.json中配置webp执行的脚步,执行上述transformAllChangedImgToWebp函数,然后在里面转换出webp图片并将新生成的webp自动git add上去,最后一并commit;


知识点


1. execSync是什么?


execSync 是一个 Node.js 内置模块 child_process 中的方法,用于同步执行外部命令。在 Node.js 中,child_process 模块提供了一组用于创建子进程的函数,其中包括 execSync 方法。execSync 方法用于执行指定的命令,并等待命令执行完成后返回结果。


const { execSync } = require('child_process'); const output = execSync(command, options);

2. git status -s 会显示每个文件的状态信息



  • A:新增文件

  • M:修改文件

  • D:删除文件

  • R:文件名修改

  • C:文件的拷贝

  • U:未知状态


image.png


3. execSync('git status -s')返回值是什么?


image.png


通过String后就可以变成可见的字符串了,然后通过分割等就能拿到具体的修改的文件路径


4. Husky是什么?


Husky 是一个用于在 Git 提交过程中执行脚本的工具。它可以帮助开发人员在代码提交前或提交后执行一些自定义的脚本,例如代码格式化、代码质量检查、单元测试等。Husky 可以确保团队成员在提交代码之前遵循一致的规范和约定。


Husky 的工作原理是通过在 Git 钩子(Git hooks)中注册脚本来实现的。Git 钩子是在特定的 Git 事件发生时执行的脚本,例如在提交代码前执行 pre-commit 钩子,或在提交代码后执行 post-commit 钩子。push代码前执行pre-push的钩子、编写提交信息时执行commit-msg的钩子可用于提交什么规范


小结



  1. 通过execSync('git status -s')从中获取筛选当前新增/修改过的图片;

  2. 调用imagemin和imagemin-webp将图片转换出webp格式的图片

  3. husky的pre-commit中触发上述调用执行,并在里面顺道将新生成的webp一并add上去

  4. 至于后续生成的webp图片怎么使用,这将在下一篇文章中学习


作者:东风t西瓜
来源:juejin.cn/post/7260016275300155449
收起阅读 »

Token到底是什么?!

web
随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web Token(JWT)的解决方案。 JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这种信息可以...
继续阅读 »

随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web TokenJWT)的解决方案。


JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这种信息可以被验证和信任,因为它是数字签名的。


那么JWT中的Token到底是什么?接下来,我们将以登录功能为例进行Token的分析。


登录流程


很多小伙伴对登录的流程已经很熟悉了,我们来看一个最基本的后台系统的登录流程


登录流程图.png


流程图很清楚了,接下来我们使用 V2Koa 实现一个登录过程,来看看Token到底是什么


Vue2 + Koa 实现登录


前端代码


1. 前端点击事件


数据的校验就忽略掉,感兴趣的同学可自行书写或者找我要源码,直接看点击事件


handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
// 这里使用了VueX
this.$store
.dispatch("user/login", this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || "/" });
this.loading = false;
})
.catch(() => {
this.loading = false;
});
} else {
return false;
}
});
}

2. Vuex中的action


校验通过后触发VueXUser模块的Login方法:


async login(context, userInfo) {
const users = {
username: userInfo.mobile,
password: userInfo.password
}
const token = await login(users)
// 在这里大家可以对返回的数据进行更详细的逻辑处理
context.commit('SET_TOKEN', token)
setToken(token)
}

3. 封装的接口


export function login(data) {
return request({
url: '/login',
method: 'post',
data
})
}

以上三步,是我们从前端向后端发送了请求并携带着用户名和密码,接下来,我们来看看Koa中是如何处理前端的请求的


Koa 处理请求


首先介绍一下Koa



Koa 基于Node.js平台,由 Express 幕后的原班人马打造,是一款新的服务端 web 框架



Koa的使用极其简单,感兴趣的小伙伴可以参考官方文档尝试用一下


Koa官网:koa.bootcss.com/index.html#…


1. 技术说明


在当前案例的koa中,使用到了jsonwebtoken的依赖包帮助我们去加密生成和解密Token


2. 接口处理


const { login } = require("../app/controller/user")
const jwt = require("jsonwebtoken")
const SECRET = 'test_';
router.post('/login', async (ctx, next) => {
const { username, password } = ctx.request.body
// 这里是调用Controller中的login方法来跟数据库中的数据作对比,可忽略
const userList = await login(username, password)

if (!userList) {
// 这里的errorModel是自己封装的处理错误的模块
ctx.body = new errorModel('用户名或密码错误', '1001')
return
}

// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓ ※ 重点看这里 ※ ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
const token = jwt.sign({ userList }, SECRET, { expiresIn: "1h" })

ctx.body = {
success: true,
state: 200,
message: 'login success',
data: token
};
return;
})

关于 JWT


上面的重点代码大家看到了,接下来具体给大家解释下JWT



Jwt由三部分组成:headerpayloadsignature



export interface Jwt {
header: JwtHeader;
payload: JwtPayload | string;
signature: string;
}

header头部


里面的包含的内容有很多,比如用于指定加密算法的alg、指定加密类型的typ,全部参数如下所示:


export interface JwtHeader {
alg: string | Algorithm;
typ?: string | undefined;
cty?: string | undefined;
crit?: Array<string | Exclude<keyof JwtHeader, 'crit'>> | undefined;
kid?: string | undefined;
jku?: string | undefined;
x5u?: string | string[] | undefined;
'x5t#S256'?: string | undefined;
x5t?: string | undefined;
x5c?: string | string[] | undefined;
}

payload负载


payload使我们存放信息的地方,里面包含了签发者过期时间签发时间等信息


export interface JwtPayload {
[key: string]: any;
iss?: string | undefined;
sub?: string | undefined;
aud?: string | string[] | undefined;
exp?: number | undefined;
nbf?: number | undefined;
iat?: number | undefined;
jti?: string | undefined;
}

signature签名


signature 需要使用编码后的 headerpayload以及我们提供的一个密钥(SECRET),然后使用 header 中指定的签名算法进行签名


关于 jwt.sign()


jwt.sign()方法,需要三个基本参数和一个可选参数:payloadsecretOrPrivateKeyoptions和一个callback


export function sign(
payload: string | Buffer | object,
secretOrPrivateKey: Secret,
options: SignOptions,
callback: SignCallback,
): void;

payload是我们需要加密的一些信息,这个参数对应上面koa代码中的{ userList },而userList则是我从数据库中查询得到的数据结果


secretOrPrivateKey则是我们自己定义的秘钥,用来后续验证Token时所用


options选项中有很多内容,例如加密算法algorithm、有效期expiresIn等等


export interface SignOptions {
/**
* Signature algorithm. Could be one of these values :
* - HS256: HMAC using SHA-256 hash algorithm (default)
* - HS384: HMAC using SHA-384 hash algorithm
* - HS512: HMAC using SHA-512 hash algorithm
* - RS256: RSASSA using SHA-256 hash algorithm
* - RS384: RSASSA using SHA-384 hash algorithm
* - RS512: RSASSA using SHA-512 hash algorithm
* - ES256: ECDSA using P-256 curve and SHA-256 hash algorithm
* - ES384: ECDSA using P-384 curve and SHA-384 hash algorithm
* - ES512: ECDSA using P-521 curve and SHA-512 hash algorithm
* - none: No digital signature or MAC value included
*/

algorithm?: Algorithm | undefined;
keyid?: string | undefined;
/** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
expiresIn?: string | number | undefined;
/** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
notBefore?: string | number | undefined;
audience?: string | string[] | undefined;
subject?: string | undefined;
issuer?: string | undefined;
jwtid?: string | undefined;
mutatePayload?: boolean | undefined;
noTimestamp?: boolean | undefined;
header?: JwtHeader | undefined;
encoding?: string | undefined;
allowInsecureKeySizes?: boolean | undefined;
allowInvalidAsymmetricKeyTypes?: boolean | undefined;
}

callback则是一个回调函数,有两个参数,默认返回Token


export type SignCallback = (
error: Error | null,
encoded: string | undefined,
) =>
void;

通过以上方法加密之后的结果就是一个Token


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s


总结


在整个的Koa中,用到了jsonwebtoken这个依赖包,里面有sign()方法


而我们前端所得到的数据通过sign()加密出来的包含自定义秘钥的一份用户信息而已


至于用户信息中有什么内容,可以随便处理,比如用户的ID、用户名、昵称、头像等等


那么这个Token后续有什么用呢?


后续我们可以在前端的拦截器中配置这个Token,让每一次的请求都携带这个Token,因为Koa后续需要对每一次请求进行Token的验证


比如登录成功后请求用户的信息,获取动态路由,再通过前端的router.addRoutes()将动态路由添加到路由对象中去即可


作者:半截短袖
来源:juejin.cn/post/7275211391102189628
收起阅读 »

移动端的「基金地图」是怎么做的?

web
🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师芒僧,今年 8 月份开始到 9 月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具,本文具体介绍了它是如何实现的...
继续阅读 »

🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师芒僧,今年 8 月份开始到 9 月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具,本文具体介绍了它是如何实现的。



Kapture 2022-10-19 at 14.12.19.gif


这次在 「支付宝 - 基金」里的【指数专区改版】需求,我们玩了一种很新的东西 🌝


8月份开始到9月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具(如上动图所示)。


简单来说,用户可以在一个散点图上根据 「收益」和「波动」 这两个维度全览对比整个市场里的指数基金,并选出适合自己的指数基金进行投资,这个功能我们愿称其为 「指数图谱」 🐶 。



图谱是这个业务场景上的叫法,实际上图谱应该是关系图而非统计图.



image.pngimage.pngimage.png


功能已发布,页面访问路线如上


先看看有哪些功能点



  1. 精细打磨的移动端手势交互,平移、缩放、横扫不在话下 :


Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.00.49.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.01.38.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.43.17.gif


依次为:缩放、平移、横扫



  1. 底部产品卡和图表的联动交互:


Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.54.40.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.54.40.gif


依次为:点击图表上的气泡、滑动底部卡片



  1. 无惧数据点太多看不到细节,我们有自适应的气泡抽样展示和自动聚焦:


Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.58.03.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 20.03.46.gif


依次为:抽样优化前、抽样优化后


那么,怎么做的呢?


最开始看到这个需求的时候,当时觉得可行性比较低。因为需求里面针对图谱的方案以及细节都特别模糊;不敢承诺各种功能和排期,所以先做了一轮比较完整的系分,增加一些说话的底气🫣


📱 第一步:同类产品调研


因为设计同学的灵感来自于 大众点评APP 上面的「美食地图」,所以第一步就是做了一次「同类产品调研」,仔细去看了一下 「美食地图」上究竟有哪些花样,有哪些体验优化的小细节,不看不知道,一看发现细节原来这么多啊 🤕:


图表和卡片的交互联动点抽样展示列表视图和卡片视图可切换交互时卡片自动折叠散点懒加载上滑直接唤起详情页
21.gif22.gif1658280205580-84108c85-793b-4318-89af-7504f3517613.gif24.gif25.gif1658280765505-5eb8bb05-30c5-45a0-bd81-9f494267b843.gif

做完这一步之后,大概能够知道自己距离“成品”有多远的距离,方便自己评估工期;另外还可以在系分评审的时候把这些细节提出来,防止临近发布了突然发现某个交互逻辑有个致命的漏洞(别问我怎么知道的,要命的)。
这波调研之后,最终我们在实现上致敬了「美食地图」50% 的体验细节优化 (狗头)。


⚙️ 第二步:功能点分析


第二步就是从需求本身的角度做功能点的分析,这样可以方便我们拆分组件,为后续做分层设计打下基础,明白哪些是需要支持可扩展的。这一步大家都熟悉,就不赘述了:
image.png


📦 第三步:通用化设计


有了功能点的分析之后,就可以进行通用化的设计了,这就来到了喜闻乐见的沉淀组件的设计环节 🌝


我们希望这个功能不仅仅是纯业务代码**,期望下次能够复用大部分核心功能 **(理想很丰满),所以在系分的时候是往通用化的方向去设计的,这里主要做了三件事情:分层设计概念标准化核心流程定义



  1. 分层设计


拆的逻辑是按最基础的 M(数据层) - C(控制层) - V(视图层) 拆分的。


image.png


有了分层设计和功能点分析之后,就可以知道哪些应该放到组件内,哪些接口应该被抽象成通用接口,哪些应该保留扩展性供使用者自己来定义,就可以画个表格了,一一决定哪些模块应该放到组件内:
image.png



  1. 概念标准化


下面来到造词环节,把一些常用的概念都定义成一个个名字,这样方便和后端、设计协同的时候效率更高,同时也方便自己定义清楚各个模型(类)。(这里其实取名越贴切越形象越好,有点考验语言能力了属实是)
image.png



  1. 核心流程定义


这一步是脑补环节,在脑子里跑一遍整体的流程,也是整个需求最核心的流程,比如这里会分成四种流程:初始化流程 、散点图交互流程、底部卡片交互流程、顶部tab交互流程


进而可以将四种流程里面的各节点做一些归类,比如都会有图表渲染、数据补全、卡片渲染这些共同的节点,而这些节点就可以实现成具体模型里的具体方法。


image.png


🌝 第四步:难点分析


根据上面拆分的各模块,列出哪些点是实现有困难的,耗时长的。这样就可以在评估工期的时候多 Battle 一下,还能砍砍需求,更可以让底层引擎/SDK来突破这些难点(比如找 F2 的核心开发者) :


image.png
image.png


📃 最后一步:


按照上述的设计进行代码编写。


难点实现


1. 移动端的图表手势交互体验优化


开发之初,F2 只支持单轴(x或者y)的平移缩放,也不支持全方向交互;在 swipe 上的体验也不太好(阻尼感很强),所以在项目开发过程中, F2 完成了很多体验优化,打磨出很多细致入微的良好体验:



  • X轴、Y轴可同时开启平移、缩放

  • swiper 体验效果优化

  • 移出可视区之后的蒙层遮挡能力(view-clip)

  • zIndex 元素层叠渲染

  • 平移缩放性能优化


2. 气泡抽样展示优化


因为散点图上的点在初始化的缩放比例下分布非常密集,所以如果每个点上面都绘制一个气泡的话,就会显得密密麻麻的,根本无从下手(如下图1所示)。针对这样的问题,做了「气泡抽样展示」的优化。


image.png


实现方式上就是渲染前遍历所有的点,如果在这个点周围某个半径距离之内有其他点,那么就认为这个点是脏点(dirty point),最后筛选出所有“干净”的点进行气泡展示。


如下图图1所示,灰色点(右上角)是干净点,而灰白色的点(偏中间的位置)因为其在圆圈半径范围之内有其他点存在,所以这个点是脏点。


image.png



多提一句,这样的过滤方式会使得密集区域的点都不会展示气泡,后续会进行优化。



3. 获取到可视区内的所有点


image.png
由于做了气泡抽样展示,所以上图中的底部卡片只会展示用户可视区内散点图上有气泡的点(细心的盆友可以发现,散点图上有两种点,一种是带气泡的交互点,一种是不带气泡的缩略点)。那么就需要一个获取「可视区内所有的点」,实现思路如下:


- 监听 PanEnd(平移结束)、PinchEnd(缩放结束), SwipeEnd(横扫结束)的事件
- 获取到平移/缩放/横扫之后最新的 scales
- 根据最新的 scales 里面的 x、y 的 range 过滤一遍图表原数据
- 将脏点从上一步的结果过滤出去
- 底部卡片根据上一步的结果进行渲染展示
- 结束



// 根据当前的缩放比例,拿到「可视区」范围内的数据
function getRecordsByZoomScales(scales, data) {
const { x: xScale, y: yScale } = scales;
const { field: xField, min: xMin, max: xMax } = xScale;
const { field: yField, min: yMin, max: yMax } = yScale;

return data.filter((record) => {
const isInView =
record[xField] >= xMin &&
record[xField] <= xMax &&
record[yField] >= yMin &&
record[yField] <= yMax;

return isInView;
});
}


// 使用时
export default props => {
// 图表原数据
const { data } = props;

function handlePanEnd (scales, data) {
// 手动高亮下面这一行
getRecordsByZoomScales(scales, data);
}

return (
<ReactCanvas>
<Chart>
{/* ... */}
<ScrollBar onPanEnd={handlePanEnd}/>
</Chart>
</ReactCanvas>

)

}

4. 数据懒加载


image.pngimage.png
底部卡片的数量是由散点图上点的数量决定的,而每张卡上都有不少的数据量(基金产品信息、指数信息、标签信息),所以不能一次性就把所有点里关联的数据都查询出来(会导致接口返回数据过多)。


这里采取的是懒加载的方式 ,每次只在交互后查询相邻 N+2/N-2 张的卡片数据,并且增加了一份内存缓存来存储已经查询过的卡片数据:


image.png


基本的流程图如下:


- 触发散点图交互/滑动底部卡片
- 读取缓存,过滤出没有缓存过的卡片
- 发起数据调用,获取到卡片的数据
- 写入缓存
- 更新卡片数据,返回
- 更新卡片视图,渲染完成

实际线上效果


项目上线之后,我们发现散点图区域的交互率(包含平移,缩放)非常高,可以看出用户对新类型的选基工具抱有新鲜感,也乐于去进行探索;也有部分用户能够通过工具完成决策或者进行产品之间的详细对比(即点击底部卡片上的详情按钮),起到了一个工具类产品的作用 🌝 。


致谢


感谢 AntV 以及 F2 对移动端图表交互能力的支持。


作者:支付宝体验科技
来源:juejin.cn/post/7176891015112949819
收起阅读 »

Vue3为什么推荐使用ref而不是reactive

web
为什么推荐使用ref而不是reactive reactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代option api的data的替代,可以存放任何数据类型,而reactive声明的数据类...
继续阅读 »

为什么推荐使用ref而不是reactive



reactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代option apidata的替代,可以存放任何数据类型,而reactive声明的数据类型只能是对象;



先抛出结论,再详细说原因:非必要不用reactive! (官方文档也有对应的推荐)


官方原文:建议使用 ref() 作为声明响应式状态的主要 API。


最懂Vue的人都这么说了:推荐ref!!!!!!


image.png


reactiveref 对比


reactiveref
❌只支持对象和数组(引用数据类型)✅支持基本数据类型+引用数据类型
✅在 <script><template> 中无差别使用❌在 <script><template> 使用方式不同(script中要.value)
❌重新分配一个新对象会丢失响应性✅重新分配一个新对象不会失去响应
能直接访问属性需要使用 .value 访问属性
❌将对象传入函数时,失去响应✅传入函数时,不会失去响应
❌解构时会丢失响应性,需使用toRefs❌解构对象时会丢失响应性,需使用toRefs


  • ref 用于将基本类型的数据(如字符串、数字,布尔值等)和引用数据类型(对象) 转换为响应式数据。使用 ref 定义的数据可以通过 .value 属性访问和修改。

  • reactive 用于将对象转换为响应式数据,包括复杂的嵌套对象和数组。使用 reactive 定义的数据可以直接访问和修改属性。


原因1:reactive有限的值类型


reactive只能声明引用数据类型(对象)


let  obj = reactive({
  name: '小明',
  age : 18
})

ref既能声明基本数据类型,也能声明对象和数组;



Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref



//对象
const state = ref({})
//数组
const state2 = ref([])

原因2:reactive使用不当会失去响应:



reactive一时爽,使用不恰当的时候失去响应泪两行,开开心心敲代码过程中,会感叹!!咦?怎么不行?为什么这么赋值失去响应了? 辣鸡reactive!!! 我要用 ref 👉👉yyds



1. 给reactive赋一整个普通对象/reactive对象


通常在页面数据回显时,需要将AJAX请求获取的对象直接赋值给响应式对象,如果操作不当就导致reactive声明的对象失去响应





  • 赋值一个普通对象


    let state = reactive({ count: 0 })
    //这个赋值将导致state失去响应
    state = {count: 1}



  • 赋值一个reactive对象



    如果给reactive的响应式对象赋值普通对象会失去响应,那么给它赋值一个reactive的响应式对象不就行了吗?下面试试看





<template>
{{state}}
</template>    

<stcirpt setup>
const state = reactive({ count: 0 })
//nextTick异步方法中修改state的值
nextTick(() => {
//并不会触发修改DOM ,说明失去响应了
state = reactive({ count: 11 });
});
</stcirpt>

nexTick中给state赋值一个reactive的响应式对象,但是DOM并没有更新!


解决方法:



  1. 不要直接整个对象替换,对象属性一个个赋值


    let state = reactive({ count: 0 })
    //state={count:1}
    state.conut = 1



  2. 使用Object.assign


    let state = reactive({ count: 0 })
    // state = {count:1}   state失去响应
    state = Object.assign(state , {count:1})



  3. 使用ref定义对象



    非必要不用reactive



    let state = ref({ count: 0 })
    state.value={count:1}



为什么同样是赋值对象ref不会失去响应而reactive会?

ref 定义的数据(包括对象)时,返回的对象是一个包装过的简单值,而不是原始值的引用;



就和对象深拷贝一样,是将对象属性值的赋值



reactive定义数据(必须是对象),reactive返回的对象是对原始对象的引用,而不是简单值的包装。



类似对象的浅拷贝,是保存对象的栈地址,无论值怎么变还是指向原来的对象的堆地址;


reactive就算赋值一个新的对象,reactive还是指向原来对象堆地址



2.将reactive对象的属性-赋值给变量(断开连接/深拷贝)


这种类似深拷贝不共享同一内存地址了,只是字面量的赋值;对该变量赋值也不会影响原来对象的属性值



let state = reactive({ count: 0 })
//赋值
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
console.log(state.count) //0

有人就说了,既然赋值对象的属性,那我赋值一整个对象不就是浅拷贝了吗?那不就是上面说的给响应式对象的字面量赋一整个普通对象/reactive对象这种情况吗?这种是会失去响应的


3.直接reactive对象解构时


  • 直接解构会失去响应


let state = reactive({ count: 0 })
//普通解构count 和 state.count 失去了响应性连接
let { count } = state
count++ // state.count值依旧是0

解决方案:



  • 使用toRefs解构不会失去响应



    使用toRefs解构后的属性是ref的响应式数据





const state = reactive({ count: 0 })
//使用toRefs解构,后的属性为ref的响应式变量
let { count } = toRefs(state)
count.value++ // state.count值改变为1

建议: ref一把梭



当使用reactive时,如果不了解reactive失去响应的情况,那么使用reactive会造成很多困扰!



推荐使用ref总结原因如下:




  1. reactive有限的值类型:只能声明引用数据类型(对象/数组)




  2. reactive在一些情况下会失去响应,这个情况会导致数据回显失去响应(数据改了,dom没更新)


    给响应式对象的字面量赋一整个普通对象,将会导致reactive声明的响应式数据失去响应


    <template>
      {{state.a}}
      {{state.b}}
      {{state.c}}
    </template>

    <script>
    let state = reactive({ a:1,b:2,c:3 })
    onMounted(()=>{
        //通AJAX请求获取的数据,回显到reactive,如果处理不好将导致变量失去响应,
       //回显失败,给响应式数据赋值一个普通对象
       state = { a:11,b:22,c:333 }
      //回显成功,一个个属性赋值  
       state.a = 11
       state.b = 22
       state.c = 33
    })
    </script>

    上面这个例子如果是使用ref进行声明,直接赋值即可,不需要将属性拆分一个个赋值


    使用ref替代reactive:


    <template>
      {{state.a}}
      {{state.b}}
      {{state.c}}
    </template>

    <script>
    let state = ref({ a:1,b:2,c:3 })
    onMounted(()=>{
       //回显成功
       state.value = { a:11,b:22,c:333 }
    })
    </script>



  3. ref适用范围更大,声明的数据类型.基本数据类型和引用数据类型都行




虽然使用ref声明的变量,在读取和修改时都需要加.value小尾巴,但是正因为是这个小尾巴,我们review代码的时候就很清楚知道这是一个ref声明的响应式数据;


ref的.value小尾巴好麻烦!


ref声明的响应式变量携带迷人的.value小尾巴,让我们一眼就能确定它是一个响应式变量!虽然使用ref声明的变量,在读取和修改时都需要加.value小尾巴,但是正因为是这个小尾巴,我们review代码的时候就很清楚知道这是一个ref声明的响应式数据;


可能有些人不喜欢这个迷人小尾巴,如果我能自动补全阁下又如何应对?


volar插件能自动补全.value (强烈推荐!!!!!!!)



本人推荐ref一把梭,但是ref又得到处.value ,那就交给插件来完成吧!!!





  • valor 自动补全.value (不是默认开启,需要手动开启)




  • 不会有人不知道Vue3需要不能使用vetur要用valor替代吧?不会不会吧? (必备volar插件)




volar设置自动填充value.gif
可以看到当输入ref声明的响应式变量时,volar插件自动填充.value 那还有啥烦恼呢? 方便!


本文会根据各位的提问和留言持续更新;


@ 别骂了_我真的不懂vue 说(总结挺好的,因此摘抄了):



reactive 重新赋值丢失响应是因为引用地址变了,被proxy代理的对象已经不是原来那个所以丢失响应了,其实ref也是一样的,当把.value那一层替换成另外一个有着.value的对象也会丢失响应 ref定义的属性等价于reactive({value:xxx})

另外说使用Object.assign为什么可以更新模板

Object.assign解释是这样的: 如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。

那个解决方法里不用重新赋值,直接Object.assign(state,{count:1})即可,所以只要proxy代理的引用地址没变,就会一直存在响应性



作者:我要充满正能量
来源:juejin.cn/post/7270519061208154112
收起阅读 »

H5快速上手鸿蒙元服务(前端)

web
一、前言 鸿蒙元服务虽然与h5在很多地方虽然有相似之处,但还是有部分不同的地方,鸿蒙服务开发模式更接近与vue2版本,很多写法与其相似。该篇文章主要用于帮助有h5基础的伙伴能够快速上手鸿蒙元服务,并且对个人在开发过程中遇到的一些坑做个总结。 二、开发相关 项目...
继续阅读 »

一、前言


鸿蒙元服务虽然与h5在很多地方虽然有相似之处,但还是有部分不同的地方,鸿蒙服务开发模式更接近与vue2版本,很多写法与其相似。该篇文章主要用于帮助有h5基础的伙伴能够快速上手鸿蒙元服务,并且对个人在开发过程中遇到的一些坑做个总结。


二、开发相关


项目目录


a51bd7a8cf9c80848926b24be2b8a27.jpg


cd3136d4b9ca22519a4934b79db8d4e.jpg
前端部分主要看js目录下的文件目录即可,除default目录外,其他文件都是与服务卡片相关的。


commom:存放公共配置文件方法等

components:存放公共组件
i18n:i18n相关

media:存放静态文件,图片等

pages:存放页面的目录,包括js,hml,css

utils:存放工具方法,比如网络请求封装等

app.js:全局文件,能够在这个文件中定义全局变量,拥有应用级的生命周期函数


其他关键目录:


supervisual:低代码相关

config.json:项目配置相关,包括路由等


config.json文件


用于给整个项目进行一些关键配置


定义路由


image.png
这种定义路由的方式,可能开发过微信小程序的伙伴会比较熟悉,在微信小程序中,一般第一个路径即是项目打开的页面,可惜在鸿蒙元服务中没有这个便捷的功能,designWidth用于定义页面以多宽的设计图来绘制,autoDesginWidth设为true,即是系统根据手机自动设置。


config.json详细配置请看官方文档: developer.harmonyos.com/cn/docs/doc…


HML


HML是一套类HTML的标记语言,通过组件,事件构建出页面的内容。页面具备数据绑定、事件绑定、列表渲染、条件渲染和逻辑控制等高级能力,由鸿蒙内部实现。


<!-- xxx.hml -->
<div class="container">
<text class="title">{{count}}</text>
<div class="box">
<input type="button" class="btn" value="increase" onclick="increase" />
<input type="button" class="btn" value="decrease" @click="decrease" />
<!-- 传递额外参数 -->
<input type="button" class="btn" value="double" @click="multiply(2)" />
<input type="button" class="btn" value="decuple" @click="multiply(10)" />
<input type="button" class="btn" value="square" @click="multiply(count)" />
</div>
</div>

// xxx.js
export default {
data: {
count: 0
},
increase() {
this.count++;
},
decrease() {
this.count--;
},
multiply(multiplier) {
this.count = multiplier * this.count;
}
};
/* xxx.css */
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
left: 0px;
top: 0px;
width: 454px;
height: 454px;
}
.title {
font-size: 30px;
text-align: center;
width: 200px;
height: 100px;
}
.box {
width: 454px;
height: 200px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.btn {
width: 200px;
border-radius: 0;
margin-top: 10px;
margin-left: 10px;
}

看这段代码是不是就觉得很亲近了,在hml中通过“{{}}”的形式绑定数据,用@和on的方法来绑定事件,同时支持冒泡、捕获等方式。


列表渲染for


<!-- xxx.hml -->
<div class="array-container" style="flex-direction: column;margin: 200px;">
<!-- div列表渲染 -->
<!-- 默认$item代表数组中的元素, $idx代表数组中的元素索引 -->
<div for="{{array}}" tid="id" onclick="changeText">
<text>{{$idx}}.{{$item.name}}</text>
</div>
<!-- 自定义元素变量名称 -->
<div for="{{value in array}}" tid="id" onclick="changeText">
<text>{{$idx}}.{{value.name}}</text>
</div>
<!-- 自定义元素变量、索引名称 -->
<div for="{{(index, value) in array}}" tid="id" onclick="changeText">
<text>{{index}}.{{value.name}}</text>
</div>
</div>


tid等于vue中的key,id即为array每一项中的唯一属性,需要注意的是,与vue不同,在鸿蒙元服务中,tid是必须的,如果没有tid可能会引起运行异常的情况。


条件渲染if和show


<!-- xxx.hml -->
//if
<div class="container">
<button class="btn" type="capsule" value="toggleShow" onclick="toggleShow"></button>
<button class="btn" type="capsule" value="toggleDisplay" onclick="toggleDisplay"></button>
<text if="{{visible}}"> Hello-world1 </text>
<text elif="{{display}}"> Hello-world2 </text>
<text else> Hello-World </text>
</div>


//show
<!-- xxx.hml -->
<div class="container">
<button class="btn" type="capsule" value="toggle" onclick="toggle"></button>
<text show="{{visible}}" > Hello World </text>
</div>


if和show相当于vue中的v-if和v-show,原理也一样。


自定义组件使用(props和emit传值)


<!-- template.hml -->
<div class="item">
<text>Name: {{name}}</text>
<text>Age: {{age}}</text>
<text class="text-style" onclick="childClicked" id="text" ref="animator">点击这里查看隐藏文本</text>
</div>

<!-- template.js -->
export default {
props:{
name,
age
contentList
}
childClicked () {
//获取标签对象
//this.$element("text");
//this.$element("text").currentOffset().y 获取属性;
//通过ref的形式来获取
//this.$refs.animator
this.$emit('eventType1',{text:'123'});
},
};
<!-- index.hml -->
//注册
<element name='comp' src='../../common/template.hml'></element>
<div>
//使用
<comp name="Tony" age="18" content-list="contentList" @event-type1="textClicked"></comp>
</div>

<!-- template.js -->
export default {
textClicked (e) {
//e.detail 拿到传过来的数据 e.detail.text
},
};


注意:组件传递props和emit属性时,强制使用横杆连接的变量名进行传递,接收时,需要使用驼峰名进行接收,通过e.detail拿到emit传过来的参数,通过$element()方法或ref的形式来获取元素对象,其他用法基本和vue2相同。



生命周期和插槽等用法参考官方文档developer.harmonyos.com/cn/docs/doc…


通用事件


developer.harmonyos.com/cn/docs/doc…


内部系统组件


image.png
常用的组件包括:

容器组件:dialog、div、滚动组件用于上拉加载(list、list-item、list-item-group)、popup、轮播组件(swiper)

基础组件:image、text、span、input、label


<div>
<text>123</text>
</div>


注意:

1.div组件内部不能够直接嵌入文字,需要通过text组件进行包裹

2.list组件在相同方向的滚动不能嵌套使用,否则会造成滚动异常

3.image标签有些图片格式不支持,需要转换为可支持的格式



CSS


华为鸿蒙元服务不支持less,sass等预编译语言,只支持css,相对于h5来说,还做了部分阉割,有些属性在h5能用,在鸿蒙元服务确用不了。


元素标签默认样式


需要注意的是,在元服务中,所有的div标签都是一个flex盒子,所以在我们使用div的时候,如果是纵向布局,那我们需要去手动改变flex-direction: column,更改主轴方向。


//hml
<div id="tabBarCon">
<div id="tab1">
</div>

<div id="tab2" onclick="handleJumpToCart">
</div>

<div id="tab3" onclick="handleJumpToMine">
</div>

</div>
//css
.tabBarCon{
flex-direction:column;
}

元素选择器


image.png


image.png
只支持部分选择器和部分伪类选择器,像h5中的伪元素选择器都是不支持的,也不支持嵌套使用,由于不存在伪元素选择器,所以遇到有时候一些特殊场景时,我们只能在hml中去判断元素索引来添加动态样式。


属性与h5中的差异


属性鸿蒙元服务h5
position只支持absolute、relative、fixed支持absolute、relative、fixed、sticky
background渐变linear-gradient(134.27deg, #ff397e 0%, #ff074c 98%),渐变百分比不支持带小数点支持
长度单位只支持px、百分比,不支持rem、em、vw、vhpx、百分比、rem、em、vw、vh
多行文字省略text-overflow: ellipsis; max-lines: 1;(只能用于text组件)单行和多行使用属性不同

JS


特点:

1.支持ES6

2.用法和vue2相似


// app.js
export default {
onCreate() {
console.info('Application onCreate');
},
onDestroy() {
console.info('Application onDestroy');
},
globalData: {
appData: 'appData',
appVersion: '2.0',
},
globalMethod() {
console.info('This is a global method!');
this.globalData.appVersion = '3.0';
}
};
// index.js页面逻辑代码
export default {
data: {
appData: 'localData',
appVersion:'1.0',
},
onInit() {
//获取全局属性
this.appData = this.$app.$def.globalData.appData;
this.appVersion = this.$app.$def.globalData.appVersion;
},
invokeGlobalMethod() {
this.$app.$def.globalMethod();
},
getAppVersion() {
this.appVersion = this.$app.$def.globalData.appVersion;
}
}

data:定义变量

onInit:生命周期函数

getAppVersion:方法,不需要写在methods里面,直接与生命周期函数同级
this.app.app.def:可以拿到全局对象,


导入导出


支持ESmodule


//import
import router from '@ohos.router';
//export
export const xxx=123;

应用级生命周期


image.png


页面级生命周期


image.png


网络请求


使用@ohos.net.http内置模块即可,下面是对网络请求做了一个简单封装,使用的时候直接导入,调用相应请求方法即可,可惜的鸿蒙元服务目前没法进行抓包,所以网络请求调试的时候只能通过打断点的形式进行调试。


import http from '@ohos.net.http';


import { invokeShowLogin } from '../common/invoke_user';

export default {
interceptors(response) {
const result = JSON.parse(response.result || {});
const {code,errno} = result
if (errno === 1024 || code === 1005) {
return invokeShowLogin();
}

return result;
},

get(url, data) {
return http.createHttp().request(
// 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
url,
{
method: http.RequestMethod.GET, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递内容
extraData: data,
connectTimeout: 10*1000,
readTimeout: 10*1000,
}
).then(res=>{
return this.interceptors(res);
});
},

post(url, data) {
return http.createHttp().request(
// 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
url,
{
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': ' application/x-www-form-urlencoded'
},
// 当使用POST请求时此字段用于传递内容
extraData: data,
connectTimeout: 10*1000, // 可选,默认为60s
readTimeout: 10*1000, // 可选,默认为60s
}
).then(res=>{
return this.interceptors(res);
});
},

postJson(url, data) {
return http.createHttp().request(
// 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
url,
{
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递内容
extraData: data,
connectTimeout: 10*1000, // 可选,默认为60s
readTimeout: 10*1000, // 可选,默认为60s
}
).then(res=>{
return this.interceptors(res);
})
}
}

数据存储


只有本地持久化存储这种方式,关闭应用,数据不会丢失。


storage.set({
key: 'loginInfo',
value: JSON.stringify({
uid, skey
}),
});
storage.get({
key: 'userInfo',
value: JSON.stringify(userInfo),
});

路由跳转


<!-- index.hml -->
<div class="container">
<text class="title">This is the index page.</text>
<button type="capsule" value="Go to the second page" class="button" onclick="launch"></button>
</div>

// index.js
import router from '@ohos.router';
export default {
launch() {
router.push ({
url: 'pages/detail/detail',
//携带的参数
params:{a:123}
});
//router.back()
//router.replace()
},
}.
// detail.js
import router from '@ohos.router';
export default {
data:{
a:''
}
onInit(){
//页面携带过来的参数可以直接使用
//this.a
}
}

官方文档链接



  1. config.json:developer.harmonyos.com/cn/docs/doc… &developer.harmonyos.com/cn/docs/doc…

  2. http请求:developer.harmonyos.com/cn/docs/doc…

  3. hml:developer.harmonyos.com/cn/docs/doc…

  4. css:developer.harmonyos.com/cn/docs/doc…

  5. js:developer.harmonyos.com/cn/docs/doc…

  6. 生命周期:developer.harmonyos.com/cn/docs/doc…

  7. 目录结构:developer.harmonyos.com/cn/docs/doc…


作者:前端小萌新y
来源:juejin.cn/post/7275945995609964563
收起阅读 »

我来聊聊面向模板的前端开发

web
在软件开发中,研发效率永远是开发人员不断追求的主题之一。于公司而言,在竞争激烈的互联网行业中,产出得快和慢也许就决定着公司的生死存亡;于个人而言,效率高了就可以少加班,多出时间去提升自己、发展爱好、陪伴家人,工作、生活两不误。 提升效率的途径,无外乎就是「方法...
继续阅读 »

在软件开发中,研发效率永远是开发人员不断追求的主题之一。于公司而言,在竞争激烈的互联网行业中,产出得快和慢也许就决定着公司的生死存亡;于个人而言,效率高了就可以少加班,多出时间去提升自己、发展爱好、陪伴家人,工作、生活两不误。


提升效率的途径,无外乎就是「方法」和「工具」。以一个开发者的思维来想,就是将工作内容进行总结、归纳,从一组相似的工作内容中提炼共同点,抽象出解决这一类问题的方法,从而造出便于在今后的工作中更为快速解决这类问题的工具。这个「工具」可以是个函数、组件、中间件、插件,也可以是 IDE、其他开发工具的扩展,甚至是语言。


面向组件


在现代前端开发中,如果去问一个业务前端开发:「如何提升团队开发效率?」对方所回答的内容中,极有可能会出现「组件库」。没错,在前端工程化趋近完善的今天,在近几年 React、Vue 等组件化库/框架的影响下,面向组件开发的思维方式早已深入人心。


组件库提效有限


现在,组件库已经是一个前端团队的必备设施了,长远来看,团队一定且必须要有自己的组件库。开源的第三方组件库再好,对于一家企业的前端团队来说也只是短期用来充饥的,因为它们无法完全满足一家公司的业务场景,并且出于多终端支持的考虑,必定要进行二次开发或者自研。


组件库有了,团队和公司中推广的效果也不错,绝大多数的人都在用。使用组件开发页面相对 jQuery 时代要每块功能区都得从

等 HTML 标签码起来说确实提升了效率,然而有限;要搞出页面需要反复去引入组件,然后组合拼装出来,就像工厂流水线上的工人拼装零件,仍然要去做很多重复动作。


只要觉得当前的开发方式重复的动作多了,就代表还能继续提效,得想个法子减少重复无意义动作。


面向组件的开发方式,是现代前端页面开发提效的初级阶段,也是一个团队所要必经的阶段。


更高层面的提效


在之前写的文章中有段话——



组件可以很简单,也可以很复杂。按照复杂程度从小到大排的话,可以分为几类:



  1. 基础组件;

  2. 复合组件;

  3. 页面;

  4. 应用。


对,不用揉眼睛,你没有看错!


站在更高的角度去看,「页面」和「应用」也是一种「组件」,只不过它们更为复杂。在这里我想要说的不是它们,而是「基础组件」和「复合组件」。



文中提到了「页面」和「应用」也可以看作是种「组件」。虽然与当时的想法有些差异,但本文的内容就是要在那篇文章的基础上简单聊聊在「页面」层面的提效。


一般来说,「页面」是用户所能看到的最大、最完整的界面,如果能在这个层面有个很好的抽象方案,在做业务开发时与单纯地面向组件开发相比,应该会有更大的提效效果。


GUI 发展了几十年,人机交互的图形元素及布局方式已经相对固定,只要不是出现像 Google Glass 之类的革命性交互设备,就不会发生重大改变。在业务开发中界面形式更是千篇一律,尤其是 web 页面,尤其是中后台系统的 web 页面,一定可以通过什么方式来将这种「千篇一律」进行抽象。


试着来回想下,自己所做过的中后台系统的绝大部分页面是不是我所描述的这样——


页面整体是上下或左右布局。如果是上下布局的话,上面是页头,下面的左侧可能有带页面导航的侧边栏,或者没有侧边栏直接将页面导航全部集中在页头中,剩余区域是页面主体部分,承载着这个页面的主要数据和功能;如果是左右布局,左侧毋庸置疑就是有页面导航的侧边栏,页头跑到了右侧上面,其余是页面主体。


中后台系统的主要功能就是 CRUD,即业务数据的增删改查,相对应的页面展现及交互形式就是列表页、表单页和详情页。列表页汇总了所有业务数据的简要信息,并提供了数据的增、删、改和更多信息查看的入口;表单页肩负着数据新增和修改的功能;详情页能够看到一条业务数据记录最完整的信息。


每新增一个业务模块,就要又写一遍列表页、表单页和详情页……反复做这种事情有啥意思呢?既然这三种页面会反复出现,那干脆封装几个页面级别的组件好了,有新需求的时候就建几个页面入口文件,里面分别引入相应的页面组件,传入一些 props,完活儿!


这种方式看起来不错,然而存在几个问题:



  • 没有描述出页面内容的结构,已封装好的页面组件对于使用者来说算是个黑盒子,页面内容是什么结构不去看源码不得而知;

  • 如果新需求中虽然需要列表页、表单页和详情页,但与已封装好的能够覆盖大部分场景的相关组件所支持的页面有些差异,扩展性是个问题;

  • 每来新需求就要新建页面入口文件然后在里面引入页面组件,还是会有很多无意义重复动作和重复代码,时间长了还是觉得烦。


我需要一种既能看一眼就理解内容结构和关系,又具备较好扩展性,还能减少重复代码和无意义动作的方式——是的,兜了一个大圈子终于要进入正题了——面向模板开发。


面向模板


面向模板的前端开发有三大要素:模板;节点;部件。


富有表达力的模板


我所说的「模板」的主要作用是内容结构的描述以及页面的配置,观感上与 XHTML 相近。它主要具备以下几个特征:



  1. 字符全部小写,多单词用连接符「-」连接,无子孙的标签直接闭合;

  2. 包含极少的具备抽象语义的标签的标签集;

  3. 以特定标签的特定属性的形式支持有限的轻逻辑。


为什么不选择用 JSON 或 JSX 来描述和配置页面?因为模板更符合直觉,更易读,并且中立。用模板的话,一眼就能几乎不用思考地看出都有啥,以及层级关系;如果是 JSON 或 JSX,还得在脑中进行转换,增加心智负担,并且拼写起来相对复杂。Vue 上手如此「简单」的原因之一,就是它「符合直觉」的设计。


要使用模板去描述页面的话,就得自定义一套具有抽象语义的标签集。


页面的整体布局可以用如下模板结构去描述:


<layout>
<header>
<title>欧雷流title>
<navs />
header>
<layout>
<sidebar>
<navs />
sidebar>
<content>...content>
layout>
<footer>...footer>
layout>

看起来是不是跟 HTML 标签很像?但它们并不是 HTML 标签,也不会进行渲染,只是用来描述页面的一段文本。


整体布局可以描述了,但承载整个页面的主要数据和功能的主体部分该如何去描述呢?


在上文中提到,我们习惯将中后台系统中与数据的增删改查相对应的页面称为「列表页」、「表单页」和「详情页」。虽然它们中都带有「页」,但真正有区别的只是整个页面中的一部分区域,通常是页面主体部分。它们可以被分别看成是一种视图形式,所以可以将称呼稍微改变一下——「列表视图」、「表单视图」和「详情视图」。一般情况下,表单视图和详情视图长得基本一样,就是一个能编辑一个不能,可以将它们合称为「表单/详情视图」。


「视图」只描述了一个数据的集合该展示成啥样,并没有也没法去描述每个数据是什么以及长啥样,需要一个更小粒度的且能够去描述每个数据单元的概念——「字段」。这样一来,用来描述数据的概念和模板标签已经齐活儿了:


<view>
<field name="name" label="姓名" />
<field name="gender" label="性别" />
<field name="age" label="年龄" />
<field name="birthday" label="生日" />
view>

虽然数据能够描述了,但还有些欠缺:表单/详情视图中想将字段分组展示没法描述;对数据的操作也没有描述。为了解决这两个问题,再引入「分组」和「动作」。这下,表单/详情视图的模板看起来会是这样:


<view>
<group title="基本信息">
<field name="name" label="姓名" />
<field name="gender" label="性别" />
<field name="age" label="年龄" />
<field name="birthday" label="生日" />
group>
<group title="宠物">
<field name="dogs" label="🐶" />
<field name="cats" label="🐱" />
group>
<action ref="submit" text="提交" />
<action ref="reset" text="重置" />
<action ref="cancel" text="取消" />
view>

模板很好地解决了内容结构描述和配置的问题,但如何去动态地调整结构和更改配置呢?在平常的业务页面开发时也许不会太凸显出问题,但碰到流程表单设计或页面可视化编辑这种灵活性很高的需求时,问题就会被暴露出来了。


充满控制力的节点


在这里,我要将定义好的标签集所拼成的模板解析成节点树,通过更改树的结构和节点的属性去影响页面最终的呈现效果。每个节点都会有节点的基本信息、对应标签的属性和一些节点操作方法:


{
name: "field",
tag: "field",
attrs: {
name: "name",
label: "姓名"
},
parent: {},
children: [],
remove: function() {},
insert: function() {}
}

在页面模板化且节点化之后,理想情况下,页面长啥样已经不受如 React、Vue 等运行时技术栈的束缚,控制权完全在解析模板所生成的节点树上,要想改变页面的视觉效果时只需更改节点即可。


极具表现力的部件


页面内容的描述通过模板来表达了,页面内容的控制权集中到节点树中了,那么页面内容的呈现在这种体系下应该如何去做呢?负责这块的,就是接下来要说的面向模板开发的第三大要素——部件。


「部件」这个词不新鲜,但在我所说的这个面向模板开发的体系中的含义,需要被重新定义一下:「部件」是一个可复用的,显示的信息排列可由用户改变的,可以进行交互的 GUI 元素。


在这个面向模板开发的体系中,模板和节点树完全是中立的,即不受运行时的技术栈所影响;而部件是建立在运行时技术栈的基础之上,但不必限于同一个技术栈。也就是说,可以使用 React 组件,也可以用 Vue 组件。


每个部件在使用前都需要注册,然后在模板中通过 widget 属性引用:


<view widget="form">
<group title="基本信息" widget="fieldset">
<field name="name" label="姓名" widget="input" />
<field name="gender" label="性别" widget="radio" />
<field name="age" label="年龄" widget="number" />
<field name="birthday" label="生日" widget="date-picker" />
group>
<group title="宠物" widget="fieldset">
<field name="dogs" label="🐶" widget="select" />
<field name="cats" label="🐱" widget="select" />
group>
<action ref="submit" text="提交" widget="button" />
<action ref="reset" text="重置" widget="button" />
<action ref="cancel" text="取消" widget="button" />
view>

这样,一个面向模板开发的普通表单页出来了!


思想总结


面向模板的开发方式很好,能够大幅度提高业务前端开发效率,一定程度上减少了业务系统的搭建速度;作为核心的模板和节点树是保持中立的,大大降低了运行时技术栈的迁移成本,且能够应对多端等场景。


面向模板的开发方式初期投入成本很高,标签集、模板解析和部件注册与调用机制等的设计和实现需要较多时间,并且这仅仅是视图层,逻辑层也需要做出相应的变化,不能简单地用 props 和事件绑定进行处理了。


这个体系建成之后,在业务开发上会很简单,但机制理解上会增加部分开发人员的心智负担。


为了效率,一家公司里的业务前端开发到最后一定是面向模板,而非面向组件。


作者:欧雷殿
来源:juejin.cn/post/7274430147126493199
收起阅读 »

血压飙升!记一次关于手机号存储的前后端讨论

起 事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 : 涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。 本来是很简单的表单需求,结果出了幺蛾子。 承 对于前端来说,这就是两...
继续阅读 »


事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 :




涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。


本来是很简单的表单需求,结果出了幺蛾子。



对于前端来说,这就是两个字段,在表单传值和数据逻辑处理时,直接用两个字段无疑是最直接和简单的:

const formData = {
country_code: '86',
phone: '13345431234'
...
}

但是,产品侧有一个强需求:手机号和其他的两个字段要做唯一性校验,不但要求三个至少存在一个,而且还要查别的业务库的数据表来校验这个注册人员的唯一性,有一套复杂的逻辑。总结一下就是,后端分开两个字段来判断很麻烦,这个也好理解,有耦合的逻辑重构要成本的。所以后端是这么要求数据的:

// (86)13345431234
phone: `(${country_code})${phone}`

将国家码放在前缀括号里拼接为一个字符串。这样一来,前端的成本就相对高了一些,不但在表单提交时要拼接,而且在列表查询时,要按照括号匹配解析出前缀来动态显示

const regex = /^\((\d+)\)(\d+)$/;
const matches = phoneNumber.match(regex);
// 如果匹配成功,返回国家码和号码的数组
if (matches) {
const countrycode = matches[1];
const number = matches[2];
return [countrycode, number];
}

就算这样,也可以接受,之前也不止一次挖过这种自己拼接自己解析的坑了。但是随后就有点让人血压上升了。



由于业务扩展,上面的手机号,要传递给一款即时通讯软件 xxx 的 API 来做下发,下发侧则规定了,需要不带括号但是要有国家区号前缀的手机号码

// 8613345431234
phone: `${country_code}${phone}`

这就有问题了,我们直接对接的后端需要括号的,下发侧对接的后端同学不需要括号。


第一阶段


血压上升 20%


讨论让后端换成两个字段存储,这样更灵活,方便拼接;或者给下发侧发数据时去掉括号也可以。后端很为难:”唯一性判断还有其他的逻辑很复杂,有耦合性,拆开字段就要重构,工期又紧,刚完成就要改,误了工期只有我背锅;去掉括号不是不可以,但地方有点多,还要考虑批量数据的性能,而且已经进入测试了,不接受临时需求“。我一想有道理啊,就去问下发侧同学。


第二阶段


血压上升 60%


问下发侧对接的同学,可以处理后再给 API下发吗?果断拒绝。他们也有理由的:”按照软件设计的规范,我们不应该非标处理这个问题的,下发的规范理应就是我们接口的规范,因为规范还有很多,这个地方妥协了,以后肯定还会有更多的非标处理,我们的代码就乱了。“


我草,我一想之前那那么多屎山的由来,觉得更有理了,那怎么办?弄得好像前端这里没有理了。


第三阶段


🔥 血压上升 120% 🔥


下发侧对接的同学反过来给我们前端提了建议,让我们在注册的时候,把前端两个字段就直接不用括号来拼接给后端,在需要解析的时候,使用特别手段解析出国家码。还好心的给我们搜索了一个解析手机号的 js 库:




我只能说,我 TM 谢谢你。😭


前端莫名其妙的凭空多了需求,而且加个库为了给后端擦屁股,还增加了打包体积,出了问题我们得首先背锅,这肯定不行。



这起事件牵涉的技术问题并没有多难,甚至有些幼稚,但是却蕴含了不少上班族哲学,最主要的一点是:如何学会背锅的问题。锅不是不能背,但是背锅换来的应该是产品成功后更多的功劳回报,而不是这种暗箱操作,老板压根不知道,只有扛不住的人吃暗亏。


个人觉得,这个应该是架构师或者技术总监的任务,提前了解上下游业务,并确定好字段与数据结构,而不是等用的时候才发现数据对不上。规划好用什么方案来开发。并将方案设计交于老板开会讨论得出定稿。之后再分派人员开发,就不会有这个事件了。



类似的问题还有很多,前后端与下游计算价格进制与单位不统一,每次都要非标转换;



最后的结局,产品出面调停,软磨硬泡,最终后端妥协了,夹在前端和下发侧之间, 确实为难。针对前端还是原来的逻辑不动,使用括号存储,他在下发侧拉取数据的时候做转换,去掉括号。其中涉及的性能问题和开发成本也申请了延长工期。


Happy Ending !!😁


血压恢复 0%





方案设计注意事项:

  • 注意产品迁移的问题。同类型的产品,如果针对的客户群体不同,其表现形态可能完全不一样,产品迁移过程中,要提前分析出新产品的业务场景的差异,做好风险分析。
  • 提前确定产品上下游生态,做好风险管控。比如下发侧有 API 对接的,需熟悉其 API 文档与使用限制,并在服务不可用时设计告警方案等。

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

现在小厂实习都问的这么难了吗😱「万物心选一面(北京+电商)(vue+小程序)」

h5是怎么和微信小程序通信的?不同H5页面怎么通信的?webview是谁提供的?web-view 是往小程序中嵌入h5的容器。会自动铺满整个小程序页面。微信小程序和 uni-app 都提供了 web-view 组件。由于简历上有一...
继续阅读 »

h5是怎么和微信小程序通信的?不同H5页面怎么通信的?webview是谁提供的?

  1. web-view 是往小程序中嵌入h5的容器。会自动铺满整个小程序页面。微信小程序和 uni-app 都提供了 web-view 组件。
  2. 由于简历上有一段实习是移动端h5开发,让面试官误以为我做过微信小程序的内嵌h5页面,但其实实习中都是公司框架写的代码,页面也是直接嵌入公司的app中的,我并没有微信小程序开发的经验,现在整理答案也是死记硬背,mark一下看到的一篇讲的比较清楚的文章,以后学小程序了再来回顾。
    微信小程序web-view与H5 通信方式探索 - 掘金 (juejin.cn)
  3. h5 页面间通信其实就是前端跨页面通信(吧?)当时第一反应回答的是使用LocalStorage, 面试官又提出用户修改个人信息后返回页面更新信息的情况,回答的是我之前做表单有类似的场景,是向后端提交后在后端更新了数据,回退到原先的页面的时候在 created/activated 的时候获取数据。
    面试官:前端跨页面通信,你知道哪些方法? - 掘金 (juejin.cn)

什么是微任务?什么是宏任务?


如果说哪些操作是宏任务,哪些操作是微任务,那大部分同学都是比较清楚的:

  • 宏任务:script(整体代码)/setTimout/setInterval/setImmediate(node 独有)/requestAnimationFrame(浏览器独有)/IO/UI render(浏览器独有)
  • 微任务:process.nextTick(node 独有)/Promise.then()/Object.observe/MutationObserver

这里面试官还追问了一句,Promise本身是微任务吗


那这两者有具体的定义吗?老规矩,直接 mdn 开搜。mdn 中可以找到微任务(microtask),但是并没有宏任务或者(macrotask)的信息。但是在在 JavaScript 中通过 queueMicrotask() 使用微任务 - Web API 接口参考 | MDN 中我们可以发现,在文档中只有taskmicrotask,对应的就是事件循环中的任务队列task queue和微任务队列microtask queue


文档中还提到了JavaScript 中的 promise 和 Mutation Observer API 都使用微任务队列去运行它们的回调函数,想来面试官的意思就是,Promise本身只是一个代理,Promise()是他的构造函数,真正被放进微任务队列的是Promise的then方法中的回调函数。


文档中对任务和微任务的定义也比较冗长,我想能区分哪些是微任务,哪些是宏任务,说出他们分别会被放在任务队列和微任务队列以及他们的执行顺序(事件循环会持续调用微任务直至队列中没有留存的,再去调用任务队列)应该足够面试了。




遍历对象有哪些方法,如果是对象的原型链上的属性会不会被遍历到?有什么办法可以排除原型链上的属性?


直接上代码测试一波:

Object.prototype.age=18;   // 修改Object.prototype  
const person ={ name: "小明" };

// 输出 name, age
for(key in person){
console.log(key)
}

// 输出 name
Object.keys(person).forEach(key=>{
console.log(key);
})

// 输出 小明, 18
for(key in person){
console.log(person[key])
}

// 输出 小明
Object.values(person).forEach(value=>{
console.log(value);
})

// 输出 name: 小明
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`);
}

很明显,for...in 会遍历到原型链上的属性,Object上的keysvaluesentires方法不会。
看看 mdn 怎么说:



for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性




Object.keys()  静态方法返回一个由给定对象 自身 的可枚举的字符串键属性名组成的数组。
Object.entries()  静态方法返回一个数组,包含给定对象 自有 的可枚举字符串键属性的键值对。


Object.values()  静态方法返回一个给定对象的 自有 可枚举字符串键属性值组成的数组。



那么如果仍然想使用 for...in 来遍历对象,并且不想要原型链上的属性,我们可以使用 Object.hasOwn 过滤掉它们:

for (key in person) {
if (person.hasOwn(key)) {
console.log(key);
}
}


如果指定的对象自身有指定的属性,则静态方法 Object.hasOwn()  返回 true。如果属性是继承的或者不存在,该方法返回 false



组件通信有哪些方法?依赖注入的数据是不是响应式的?有什么办法让他保持响应式的?

  • props / $emit
  • $emit / $on (eventBus)
  • provide / inject
  • $attrs / $listeners
  • ref / $ref
  • $parent / $children
  • vuex / pinia

Vue 组件间通信六种方式(完整版) - 掘金 (juejin.cn)


vue更新dom是异步还是同步?如何不使用nexttick实现nexttick的功能?vue的更新是哪一种微任务?


Vue更新DOM是异步的。这意味着我们在修改完data之后并不能立刻获取修改后的DOM元素。Vue需要通过nextTick方法才能获取最新的DOM。


Vue在调用Watcher更新视图时,并不会直接进行更新,而是把需要更新的Watcher加入到queueWatcher队列里,然后把具体的更新方法flushSchedulerQueue传给nextTick进行调用。nextTick只是单纯通过Promise、setTimeout等方法模拟的异步任务。


如果你想要不使用nextTick实现nextTick的功能,你可以使用Promise、setTimeout等方法来模拟异步任务。例如,你可以使用 Promise.resolve().then(callback) 或者 setTimeout(callback, 0) 来实现类似于nextTick的功能。


至于Vue的更新是哪一种微任务,它取决于浏览器兼容性。Vue会根据浏览器兼容性,选用不同的异步策略。例如,如果浏览器兼容Promise,那么Vue就会使用Promise来实现异步更新。如果浏览器不兼容Promise但兼容MutationObserver,那么Vue就会使用MutationObserver来实现异步更新。如果浏览器既不兼容Promise也不兼容MutationObserver,那么Vue就会使用setImmediatesetTimeout来实现异步更新。


vue能监听到数组的push方法吗?直接给响应式变量赋值一个新的数组会被监听到吗?


这里讨论的都是vue2vue3当中这些问题都已经被proxy解决了。



Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()


替换整个对象或数组就和操作其他类型的响应式数据没区别了,自然是可以检测到的。


深入响应式原理 — Vue.js


如果要把数组api,比如push pop这些都改成async、await的异步函数要怎么做?怎么拿到这些方法?怎么传参?


这一问是我当时最蒙的,我到现在都不确定我是否领悟对了他要问什么,大致上的理解如下,如果有大佬知道的可以在评论区教学一下我这个小菜鸟。

// 保存数组原型上的push方法
const originalPush = Array.prototype.push;
// 重写数组原型上的push方法
Array.prototype.push = async function (...args) {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 500));
return originalPush.apply(this, args);
}
async function test() {
const arr = [1, 2, 3];
await arr.push(4);
console.log(arr);
}
test(); // [1, 2, 3, 4],需要延迟定时器中设定的时间才能打印出来

在沸点一位大佬的提醒下,面试官可能想问的是这个JavaScript 异步数组 - 个人文章 - SegmentFault 思否


总结


总结就是一个字,菜。

虽然是问的有点细致,但基本上都只能回答上来每一问的第一问,后面的深入追问就懵逼了。原因是因为自己基本上都是直接对着面经和八股文准备的,没有实践过,也没有看过相关的文档。之后还是要坚持把JavaScript高级程序设计和vue设计与实现啃完,不说把这些问题记得滚瓜烂熟能对答如流,起码也要在在面试官引导下应该有思路。


作者:炫饭第一名
链接:https://juejin.cn/post/7275141653420425277
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

血压飙升!记一次关于手机号存储的前后端讨论

web
本文是为了探讨技术架构管理的,不要带入实际生活 起 事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 : 涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。 本来是很简单的表单...
继续阅读 »

本文是为了探讨技术架构管理的,不要带入实际生活




事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 :


image.png


涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。


本来是很简单的表单需求,结果出了幺蛾子。



对于前端来说,这就是两个字段,在表单传值和数据逻辑处理时,直接用两个字段无疑是最直接和简单的:


const formData = {
country_code: '86',
phone: '13345431234'
...
}

但是,产品侧有一个强需求:手机号和其他的两个字段要做唯一性校验,不但要求三个至少存在一个,而且还要查别的业务库的数据表来校验这个注册人员的唯一性,有一套复杂的逻辑。总结一下就是,后端分开两个字段来判断很麻烦,这个也好理解,有耦合的逻辑重构要成本的。所以后端是这么要求数据的:


// (86)13345431234
phone: `(${country_code})${phone}`

将国家码放在前缀括号里拼接为一个字符串。这样一来,前端的成本就相对高了一些,不但在表单提交时要拼接,而且在列表查询时,要按照括号匹配解析出前缀来动态显示


const regex = /^\((\d+)\)(\d+)$/;
const matches = phoneNumber.match(regex);
// 如果匹配成功,返回国家码和号码的数组
if (matches) {
const countrycode = matches[1];
const number = matches[2];
return [countrycode, number];
}

就算这样,也可以接受,之前也不止一次挖过这种自己拼接自己解析的坑了。但是随后就有点让人血压上升了。



由于业务扩展,上面的手机号,要传递给一款即时通讯软件 xxx 的 API 来做下发,下发侧则规定了,需要不带括号但是要有国家区号前缀的手机号码


// 8613345431234
phone: `${country_code}${phone}`

这就有问题了,我们直接对接的后端需要括号的,下发侧对接的后端同学不需要括号。


第一阶段


血压上升 20%


讨论让后端换成两个字段存储,这样更灵活,方便拼接;或者给下发侧发数据时去掉括号也可以。后端很为难:”唯一性判断还有其他的逻辑很复杂,有耦合性,拆开字段就要重构,工期又紧,刚完成就要改,误了工期只有我背锅;去掉括号不是不可以,但地方有点多,还要考虑批量数据的性能,而且已经进入测试了,不接受临时需求“。我一想有道理啊,就去问下发侧同学。


第二阶段


血压上升 60%


问下发侧对接的同学,可以处理后再给 API下发吗?果断拒绝。他们也有理由的:”按照软件设计的规范,我们不应该非标处理这个问题的,下发的规范理应就是我们接口的规范,因为规范还有很多,这个地方妥协了,以后肯定还会有更多的非标处理,我们的代码就乱了。“


我草,我一想之前那那么多屎山的由来,觉得更有理了,那怎么办?弄得好像前端这里没有理了。


第三阶段


🔥 血压上升 120% 🔥


下发侧对接的同学反过来给我们前端提了建议,让我们在注册的时候,把前端两个字段就直接不用括号来拼接给后端,在需要解析的时候,使用特别手段解析出国家码。还好心的给我们搜索了一个解析手机号的 js 库:


企业微信截图_1bfc9849-e9f2-4289-a4cb-01098e3dcf2e.png


我只能说,我 TM 谢谢你。😭


前端莫名其妙的凭空多了需求,而且加个库为了给后端擦屁股,还增加了打包体积,出了问题我们得首先背锅,这肯定不行。



这起事件牵涉的技术问题并没有多难,甚至有些幼稚,但是却蕴含了不少上班族哲学,最主要的一点是:如何学会背锅的问题。锅不是不能背,但是背锅换来的应该是产品成功后更多的功劳回报,而不是这种暗箱操作,老板压根不知道,只有扛不住的人吃暗亏。


个人觉得,这个应该是架构师或者技术总监的任务,提前了解上下游业务,并确定好字段与数据结构,而不是等用的时候才发现数据对不上。规划好用什么方案来开发。并将方案设计交于老板开会讨论得出定稿。之后再分派人员开发,就不会有这个事件了。



类似的问题还有很多,前后端与下游计算价格进制与单位不统一,每次都要非标转换;



最后的结局,产品出面调停,软磨硬泡,最终后端妥协了,夹在前端和下发侧之间, 确实为难。针对前端还是原来的逻辑不动,使用括号存储,他在下发侧拉取数据的时候做转换,去掉括号。其中涉及的性能问题和开发成本也申请了延长工期。


Happy Ending !!😁


血压恢复 0%


39df05d4-0146-4fbc-9eda-384298424f19.jpg



方案设计注意事项:



  • 注意产品迁移的问题。同类型的产品,如果针对的客户群体不同,其表现形态可能完全不一样,产品迁移过程中,要提前分析出新产品的业务场景的差异,做好风险分析。

  • 提前确定产品上下游生态,做好风险管控。比如下发侧有 API 对接的,需熟悉其 API 文档与使用限制,并在服务不可用时设计告警方案等。


作者:小肚肚肚肚肚哦
来源:juejin.cn/post/7275576074589880372
收起阅读 »

为什么日本的网站看起来如此不同

web
该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。文章还讨论了日本网站的信息密...
继续阅读 »

该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。
文章还讨论了日本网站的信息密集型布局,这种布局适应了日本语言的特点,使得页面能够容纳大量文字和图像。此外,文章提到了日本网站的功能丰富性,如弹出式窗口和互动元素,以及这些元素在用户体验方面的作用。
作者强调了日本网站在技术和创新方面的进步,尽管在过去存在技术限制。最后,文章提出了一些关于如何将日本网站设计的元素应用到其他文化中的建议。


下面是正文~~~


多年来,我朋友与日本的网站有过许多接触——无论是研究签证要求、计划旅行,还是简单地在线订购东西。而我花了很长时间才适应这些网站上的大段文字、大量使用鲜艳颜色和10多种不同字体的设计,这些网站就像是直接冲着你扔过来的。


image.png


虽然有许多网站都采用了更简约、易于导航的设计,适应了西方网站的用户,但是值得探究的是为什么这种更复杂的风格在日本仍然盛行。


只是为了明确起见,这些不是过去的遗迹,而是维护的网站,许多情况下,它们最后一次更新是在2023年。


image.png


我们可以从几个角度来分析这种设计方法:



  • 字体和前端网站开发限制

  • 技术发展与停滞

  • 机构数字素养(或其缺乏)

  • 文化影响


与大多数话题一样,很可能没有一个正确的答案,而是这个网站设计是随着时间的推移而相互作用的各种因素的结果。


字体和前端网站开发限制


对于会一些基本排版知识、掌握适当软件并有一些空闲时间的人来说,为罗马化语言创造新字体可能是一项有趣的挑战。然而,对于日语来说,这是一个完全不同层次的努力。


要从头开始创建英文字体,需要大约230个字形——字形是给定字母的单个表示(A a a算作3个字形)——或者如果想覆盖所有基于拉丁字母表的语言,则需要840个字形。对于日语而言,由于其三种不同的书写系统和无数的汉字,需要7,000至16,000个字形甚至更多。因此,在日语中创建新字体需要有组织的团队合作和比其拉丁字母表的同行们更多的时间。



这并不令人意外,因此中文和(汉字)韩文字体也面临着类似的工作量,这导致这些语言通常被称为CJK字体所覆盖。



由于越来越少的设计师面对这个特殊的挑战,建立网站时可供选择的字体也越来越少。再加上缺乏大写字母和使用日文字体会导致加载时间较长,因为需要引用更大的库,这就不得不采用其他方式来创建视觉层次。


以美国和日本版的星巴克主页为例:


美国的:


image.png


日本的


image.png


就这样,我们就可以解释为什么许多日本网站倾向于用文字较多的图片来表示内容类别了。有时,你甚至会看到每个磁贴都使用自己定制的字体,尤其是在限时优惠的情况下。


image.png


技术发展/停滞与机构数字素养


如果你对日本感兴趣,你可能对现代与过时技术之间的鲜明对比有所了解。在许多地方,先进的技术与完全过时的技术并存。作为世界机器人领导者之一的国家,在台场人工岛上放置了一座真人大小的高达雕像,却仍然依赖软盘和传真机,面对2022年Windows资源管理器关闭时感到恐慌。


image.png


在德国,前总理安格拉·默克尔在2013年称互联网为“未知领域”后,遭到全国范围的嘲笑。然而,这在2018年被前网络安全部长樱田义孝轻易地超越,他声称自己从未使用过电脑,并且在议会被问及USB驱动器的概念时,他被引述为“困惑不解”(来源)。


对于那些尚未有机会窥探幕后幻象的人来说,这可能听起来很奇怪,但日本在技术素养方面严重落后于更新计划。因此,可以推断这些问题也在阻碍日本网站设计的发展。而具体来说,日本的网页设计正面临着这一挑战——只需在谷歌或Pinterest上搜索日本海报设计,就能看到一个非常不同和现代化的平面设计水平。


image.png


文化影响


在分析任何设计选择时,不应低估文化习俗、倾向、偏见和偏好的影响。然而,“这是文化”的说法可能过于简单化,并被用作为各种差异辩解的借口。而且,摆脱自己的观点偏见是困难的,甚至可能无法完全实现。


因此,从我们的角度来看,看这个网站很容易..


image.png


感觉不知所措,认为设计糟糕,然后就此打住。因为谁会使用这个混乱不堪的网站呢?


这就是因为无知而导致有趣的见解被忽视的地方。现在,我没有资格告诉你日本文化如何影响了这种设计。然而,我很幸运能够从与日本本土人士的交谈中获得启发,以及在日本工作和生活的经验。


与这个分析相关的一次对话实际上不是关于网站,而是关于YouTube的缩略图 - 有时候它们也同样令人不知所措。


image.png


对于习惯了许多西方频道所采用的极简和时尚设计——只有一个标题、重复的色彩搭配和有限的字体——上面的缩略图确实有些难以接受。然而,当我询问一个日本本土人士为什么许多极受欢迎频道的缩略图都是这样设计时,他对这种设计被视为令人困惑的想法感到惊讶。他认为日本的设计方法使视频看起来更加引人入胜,提供了一些信息碎片,从而使我们更容易做出是否有趣的明智决策。相比之下,我给他看的英文视频缩略图在他看来非常模糊和无聊。


也许正是这种寻求信息的态度导致了我们的观念如此不同。在日本,对风险的回避、反复核对和对迅速做出决策的犹豫明显高于西方国家。这与更加集体主义的社会心态紧密相连——例如,在将文件发送给商业伙伴之前进行两次(或三次)检查可能需要更长时间,但错误的风险显著降低,从而避免了任何参与者丢面子的情况发生。


尽管有人认为这只适用于足够高的赌注,而迷惑外国游客似乎不符合条件——搜索一下“Engrish”这个词,然后感谢我吧。


回到网站设计,这种文化角度有助于解释为什么在线购物、新闻和政府网站在外部观察者看来常常是“最糟糕的罪犯”。毕竟,这些正是需要大量细节直接对应于做出良好购买决策、高效地保持最新信息或确保你拥有某个特定程序的所有必要信息的情况。


有趣的是,关于美国人和中国/日本人如何感知信息,也有相当多的研究。几项研究的结果似乎表明,例如,日本人更加整体地感知信息,而美国人倾向于选择一个焦点来引导他们的注意力(来源)。这可能给我们提供了另一个线索,解释为什么即使在日语能力较高的情况下,西方人对这类网站也感到困难。


后但并非最不重要的是,必须说的是,网站并不是在一个在线真空中存在。而且,各种媒体,从小册子或杂志到地铁广告,也使用了尽可能多地压缩信息的布局,人们可能已经习惯了这种无处不在的方式,以至于没有人曾经想过质疑它。


长话短说,这并不是为了找到标题问题的绝对答案,也不是为了加强日本人独特性的观点,就像日本人论一样。相反,尤其是在看到了几次关注一个解释为“真正答案”的讨论之后,我想展示科技、历史和文化影响的广度,这些最终塑造了这种差异。


作者:王大冶
来源:juejin.cn/post/7272290608655941651
收起阅读 »

🤔️《你不知道的JavaScript》到底讲了些什么?

开始之前 在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面...
继续阅读 »

开始之前


在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面,从这本书中受益良多。因此在多次阅读后我选择用内容梗概+案例解析的形式将其精华部分记录下来,以供个人翻阅和与大家分享,那么我们开始吧


上卷


上卷主要针对语言核心的一些关键概念,如作用域、闭包、this等。本文将为笔者阅读过程中所总结和提炼的关键知识点与经典案例


1. 作用域是什么?


内容概览


本章介绍了JavaScript中的作用域概念,解释了变量如何被储存以及如何被引用。


实例分析

var a = 2;

function foo() {
var a = 3;
console.log(a); // 3
}

foo();

console.log(a); // 2

在这个例子中,我们看到a在全局作用域和foo函数的作用域中都有定义。函数内部的a不会影响到全局作用域中的a


2. 词法作用域


内容概览


词法作用域意味着作用域是由函数声明的位置来决定的,而不是函数调用的位置。


实例分析

function foo() {
console.log(a);
}

function bar() {
var a = 3;
foo();
}

var a = 2;

bar(); // 2

尽管foo函数在bar函数内部被调用,但foo函数的词法作用域仍然使其能够访问外部的变量a,所以输出为2。


3. 函数与块作用域


内容概览


介绍了函数作用域和块作用域,以及如何利用它们来避免变量冲突和其他问题。


实例分析

if (true) {
let a = 2;
console.log(a); // 2
}

console.log(a); // ReferenceError

使用let定义的变量具有块作用域,只能在声明它的块中访问。


4. 提升


内容概览


解释了提升(hoisting)现象,即变量和函数声明会被移动到它们所在的作用域顶部。


实例分析

foo(); // "Hello"

function foo() {
console.log("Hello");
}

尽管函数foo在调用之后被声明,但由于提升,它仍然可以正常调用。


5. 作用域闭包


内容概览


解释了闭包是如何工作的,以及它在JavaScript中的重要性。


实例分析

function makeGreeting(greeting) {
return function(name) {
console.log(greeting + ", " + name);
};
}

let sayHello = makeGreeting("Hello");
sayHello("Alice"); // "Hello, Alice"

sayHello函数是一个闭包,它记住了创建它时的作用域,因此能够访问greeting变量。


6. 词法分析和语法分析


实例分析


来看以下代码:

function add(x, y) {
return x + y;
}

let sum = add(5, 7);

在词法分析阶段,这段代码可能被分解为多个词法单元:function, add, (, x, ,, y, ), {, return, +, ;, }, let, =, 5, 7 等。然后,语法分析器会将这些词法单元组合成AST。


7. L查询与R查询


实例分析

function calculateArea(radius) {
const pi = 3.141592653589793;
return pi * radius * radius;
}

let r = 5;
let area = calculateArea(r);

在这个例子中,考虑let area = calculateArea(r);这行代码。对于calculateArea,它是RHS查询,因为我们需要获得这个函数的引用来执行它。而r也是RHS查询,因为我们正在获取它的值来传递给函数。


calculateArea函数内,pi和两次radius的查询都是RHS查询,因为我们获取它们的值来执行乘法操作。而return语句中的计算结果则赋值给了隐式的返回值,这涉及到LHS查询。


对于let r = 5;,这里的r是一个LHS查询,因为我们给它赋值了。


中卷


中卷的内容相比上卷来说更加深入且晦涩,其中包括令初学者头昏脑胀的面向对象编程与this原型链相关的知识,我将以更多的篇幅和更深入的案例来帮助大家进行理解


1. 对象


实例分析 1


使用工厂函数和构造器来创建对象:

function createPerson(name, age) {
return {
name,
age,
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
};
}

const person1 = createPerson('Alice', 30);
person1.greet();

深入分析


这是一个工厂函数的例子,允许我们快速创建具有相似属性和方法的对象。在此,greet方法是每个对象的一部分,这可能导致内存浪费,因为每次创建新对象时,都会为greet方法分配新的内存。


实例分析 2


使用getters和setters:

const book = {
title: 'In Search of Lost Time',
author: 'Marcel Proust',
get description() {
return `${this.title} by ${this.author}`;
},
set description(value) {
[this.title, this.author] = value.split(' by ');
}
};

book.description = '1984 by George Orwell';
console.log(book.title); // Outputs: 1984

深入分析


这个案例展示了如何利用对象的getters和setters来动态地管理对象的属性。通过setter,我们能够同时更新titleauthor,而getter则为我们提供了书的描述。


2. 类


实例分析 1


多态的使用:

class Animal {
makeSound() {
console.log('Some generic sound');
}
}

class Dog extends Animal {
makeSound() {
console.log('Woof');
}
}

const animal1 = new Animal();
const animal2 = new Dog();

animal1.makeSound(); // Outputs: Some generic sound
animal2.makeSound(); // Outputs: Woof

深入分析


多态是面向对象编程中的一个关键概念,允许我们创建能够以多种形式表现的对象。在此,我们看到Dog类重写了Animal类的makeSound方法,实现了多态。


实例分析 2


静态方法的使用:

class MathUtility {
static add(x, y) {
return x + y;
}
}

console.log(MathUtility.add(5, 3)); // Outputs: 8

深入分析


这个案例展示了如何在类中使用静态方法。与实例方法不同,静态方法不需要创建类的实例就可以被调用。它们通常用于执行与类的实例无关的操作。


3. 原型


实例分析


一个动态添加到原型的方法:

function Cat(name) {
this.name = name;
}

Cat.prototype.purr = function() {
console.log(`${this.name} is purring.`);
};

const whiskers = new Cat('Whiskers');
whiskers.purr(); // Outputs: Whiskers is purring.

深入分析


在此例中,我们后期将purr方法添加到Cat的原型中。这意味着即使在添加此方法后创建的所有Cat实例都可以访问它。这展示了原型继承的动态性质:我们可以在任何时候修改原型,这些更改会反映在所有继承了那个原型的对象上。


4. this和对象原型


JavaScript中的this是一个非常深入且经常被误解的主题。this并不是由开发者选择的,它是由函数调用时的条件决定的。


实例分析


考虑以下场景:

function showDetails() {
console.log(this.name);
}

const obj1 = {
name: 'Object 1',
display: showDetails
};

const obj2 = {
name: 'Object 2',
display: showDetails
};

obj1.display(); // Outputs: Object 1
obj2.display(); // Outputs: Object 2

深入分析


在这里,showDetails函数查看this.name。当它作为obj1的方法被调用时,this指向obj1。当它作为obj2的方法被调用时,this指向obj2。这说明了this的动态性质:它是基于函数如何被调用的。


5. 原型链


当试图访问一个对象的属性或方法时,JavaScript会首先在该对象本身上查找。如果未找到,它会在对象的原型上查找,然后是原型的原型,以此类推,直到找到该属性或到达原型链的末尾。


实例分析

function Animal(sound) {
this.sound = sound;
}

Animal.prototype.makeSound = function() {
console.log(this.sound);
}

function Dog() {
Animal.call(this, 'Woof');
}

Dog.prototype = Object.create(Animal.prototype);

const dog = new Dog();
dog.makeSound(); // Outputs: Woof

深入分析


当我们调用dog.makeSound()时,JavaScript首先在dog对象上查找makeSound。未找到后,它会在Dog的原型上查找。还是未找到,然后继续在Animal的原型上查找,最后找到并执行它。


6. 行为委托


行为委托是原型的一种使用模式,涉及到对象之间的关系,而不仅仅是克隆或复制。


实例分析

const Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log(this.id); }
};

const XYZ = Object.create(Task);

XYZ.prepareTask = function(ID, Label) {
this.setID(ID);
this.label = Label;
};

XYZ.outputTaskDetails = function() {
this.outputID();
console.log(this.label);
};

const task = Object.create(XYZ);
task.prepareTask(1, 'create demo for delegation');
task.outputTaskDetails(); // Outputs: 1, create demo for delegation

深入分析


XYZ不是Task的复制,它链接到Task。当我们在XYZ对象上调用setIDoutputID方法时,这些方法实际上是在Task对象上运行的,但this指向的是XYZ。这就是所谓的委托:XYZ在行为上委托给了Task


下卷


下卷的内容相较于中卷就基础了很多,更偏向于实际应用方向


1. 类型和语法


实例分析 - 类型转换


考虑以下的隐式类型转换:

var a = "42";
var b = a * 1;
console.log(typeof a); // "string"
console.log(typeof b); // "number"

深入分析


在这里,变量a是一个字符串,但当我们尝试与数字进行乘法操作时,它会被隐式地转换为一个数字。这是因为乘法操作符期望它的操作数是数字,因此JavaScript会尝试将字符串a转换为一个数字。


2. 异步和性能


实例分析 - Promises

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched!");
}, 2000);
});
}

fetchData().then(data => {
console.log(data); // Outputs: "Data fetched!" after 2 seconds
});

深入分析


Promises 提供了一种更简洁、更具可读性的方式来处理异步操作。在上面的例子中,fetchData函数返回一个Promise。setTimeout模拟了异步数据获取,数据在2秒后可用。当数据准备好后,resolve函数被调用,then方法随后执行,输出数据。


3. ES6及其以上的特性


实例分析 - 使用箭头函数

const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]

深入分析


箭头函数提供了一种更简洁的方式来定义函数,尤其是对于那些简短的、无状态的函数来说。在上述例子中,我们使用箭头函数简洁地定义了一个函数,该函数将其输入值乘以2,并使用map方法将其应用到一个数字数组中。


实例分析 - 使用async/await

async function fetchDataAsync() {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
}

fetchDataAsync().then(data => console.log(data));

深入分析


async/await是ES7引入的特性,允许以同步的方式编写异步代码。在这个案例中,fetchDataAsync函数是一个异步函数,这意味着它返回一个Promise。await关键字使我们能够等待Promise解析,然后继续执行后面的代码。这消除了回调地狱,使异步代码更容易阅读和维护。


4. 迭代器和生成器


实例分析 - 使用生成器函数

function* numbersGenerator() {
yield 1;
yield 2;
yield 3;
}

const numbers = numbersGenerator();

console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3

深入分析


生成器函数使用function*声明,并且可以包含一个或多个yield表达式。每次调用生成器对象的next()方法时,函数都会执行到下一个yield表达式,并返回其值。这使我们能够按需产生值,非常适用于大数据集或无限数据流。


5. 增强的对象字面量


实例分析

const name = "Book";
const price = 20;

const book = {
name,
price,
describe() {
return `${this.name} costs ${this.price} dollars.`;
}
};

console.log(book.describe()); // "Book costs 20 dollars."

深入分析


增强的对象字面量允许我们在声明对象时使用更简洁的语法。在这里,我们直接使用变量名作为键,并使用简短的方法定义形式。这使得对象声明更为简洁和可读。


6. 解构赋值


实例分析

const user = {
firstName: "Alice",
lastName: "Smith"
};

const { firstName, lastName } = user;

console.log(firstName); // Alice
console.log(lastName); // Smith

深入分析


解构赋值允许我们从数组或对象中提取数据,并赋值给新的或已存在的变量。在此例中,我们从user对象中提取了firstNamelastName属性,并将它们赋值给了同名的新变量。


7. 模块


实例分析 - ES6模块导入和导出

// math.js
export function add(x, y) {
return x + y;
}

export function subtract(x, y) {
return x - y;
}

// app.js
import { add, subtract } from './math.js';

console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2

结语


经过对《你不知道的JavaScript》上、中、下三卷的深入探索,我们更加清晰地理解了JavaScript这门语言的复杂性、深度和强大之处。这不仅仅是关于语法或是新特性,更是关于理解其背后的哲学和设计思想。作为开发者,真正的掌握并不只是会用,而是要知其所以然。此书为我们打开了一扇探索JavaScript的大门,但真正的旅程,才刚刚开始。我们的每一步前行,都是为了更好地理解、更精准地应用,为编写出更高效、更优雅的代码而努力。


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

👣 我在语雀做图册 - 更整齐的瀑布流算法

web
🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师亦池。介绍了语雀图册功能背后的更整齐的瀑布流算法,还有一段和 chatGPT 纠缠的故事 🤪,一起来看看~ 🏞️ 介绍一下图册 先来看看我们语雀年前上线的图册功能: 欢迎大家使用图册更好的展示自己的图片,...
继续阅读 »

🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师亦池。介绍了语雀图册功能背后的更整齐的瀑布流算法,还有一段和 chatGPT 纠缠的故事 🤪,一起来看看~



🏞️ 介绍一下图册


先来看看我们语雀年前上线的图册功能:
image.png


image.png



欢迎大家使用图册更好的展示自己的图片,秀秀最近的摄影作品、po一下最近的好吃的好玩的、晒几张靓照~
目前图册只是上了一个基础的初版,还有很多地方在体验和产品设计上也会继续优化,包括针对单张图的删除、排序,图册的尺寸调整,更快捷的把各种来源的图片放进图册里,大家有一些想法也欢迎提建议~



开发故事


🧐 瀑布流能不能再整齐一些


瀑布流是一个不新鲜的布局方式了,看到这个我第一反应自然是使用社区的开源库按需裁剪一下用起来。刚发布时也是这么上线的。扒过代码参考的开源库有:



但第一版本其实回发生下图左侧尴尬的情况:
image.png
肉眼可见我们要的是上图右侧的效果。


常见瀑布流算法的问题



原因:社区主流的瀑布流计算思路都是将已知高度的图片(实现上可以是图片加载完成后获取高度触发重新布局)分发了列容器里,每列记录实时高度,对于每一张新来的图片分发规则是放入最短的那一列。专业点说是贪心算法的思想。



所以当最后一张是长图时就会对布局的齐平性导致很大的冲击。(当然这不是说社区的方案都low,开源产品可能更多考虑了普适情况,譬如可能无法提前知道所有图片尺寸信息,全部加载完再重新布局一次又给用户带来干扰,甚至是懒加载场景更不好优雅的展示处理。)


在语雀编辑器场景,我们对于要布局的那批图片是能拿到宽高信息的,完全可以对所有图片整体考虑,计算一个最优结果再渲染,可以做到不被最后一张长图影响整体。


一开始我觉得这是个单纯的算法问题,可以抽象成将一个数字数组拆分成n个数组,使每个数组的数字和尽量接近,我觉得应该是有一种经典算法来解决这类问题的,譬如动态规划、背包问题之类的。


这么经典的问题不如问chatGPT吧,此处插入一段和chatGPT纠缠的故事。结论是它没给我找到靠谱答案。感兴趣的可以展开后面章节的折叠块看看这个让人哭笑不得的过程🙄。


💁‍♀️ 分析一下


chatGPT没能给我正确答案,我又是个基础算法的渣渣,想先找个方向再进去研究怎么实现,于是请教了一下一个酷爱刷算法题的师妹,得到的方向是:“这是个负载均衡类型问题,具有NP hard复杂度,无法获得最佳解,只能求近似最优解,用动态规划的思想是没错的”。


啥是NP hard复杂度,可以看后面的【基础知识】章节的科普。我也不清楚怎么证明这真的是一个NP hard复杂度的问题,但基础知识告诉我这类复杂度的问题往往复杂度是阶乘级别的,就是不是我们常见的O(n)、O(logn)、O(n^2)这种经典算法的复杂度,他们的复杂度叫做有多项式解。阶乘级别意味着暴力穷举,这往往是计算机无法接受的时间,也毫无算法可言。


咱这个问题,求解最优解时,每一张图片的摆放都影响着后面图片的位置,每张图之间都有关联,想想似乎确实只有穷举才能真正的找到最优解。加上对师妹算法水平的信任,我开始把问题缩减到动态规划领域。


那就拆解子问题,先计算子问题的近似最优解。


🏄‍ ♀️解决方案


核心思想:




  1. 计算平均值,让每一组的和尽量接近均值,最终每组和的差异才会最小

  2. 将原数组arr从大到小排序,降低分组时便利查找的复杂度

  3. 遍历原数组arr,从目标的n个分组的第一组开始装数据,直到它接近均值停止。这里注意接近的意思不是<=avg,而是在均值临界点,加上一个值num1 < avg后,和均值的差值是delta,往前遍历找(意味着num2 > num1)第一个没被分组的数据num2放入当前组后,num2 - avg < delta,如果是的则装num2,否则装num1。确保装的是最接近均值的数。

  4. 对于最后一个分组n-1要装数据时,需要确保arr的每一个数据都被分配完,并且各组结果最小,所以最后一组的策略不参考平均值,而是按和最小的分组去塞arr里的每一个数据。



另外注意,对于已经分好组的数据打个标,以免被重复分组。


这里我们是在拆解子问题



  • 把复杂的分组后每组方差最小的问题,转化为让每组和最接近平均值的问题,将整体的问题拆解成了n个组的问题

  • n个组塞值时,又是一个找数据使它最接近均值的子问题


其中为了降低复杂度不搞遍历的最优,确实只做到了近似最优解。譬如放值前先做了排序,只要当前数据放进去 < avg都先无脑放,就会出现,譬如剩下的数据有[48, 25, 25], 均值是50,本来我们可以放[25,25]得到最接近均值的数据,但现在只放入了48。


🤪 图片场景的特殊考虑因子


当我把一个纯数学解放入瀑布流场景时,发现事情并没有这么简单,算法的最优还是要为图片展示场景效果的最优做一些让步


参差感


譬如你看这个是最优解么?
image.png
因为我们先做了排序,并且按排序的数据顺序做分配,所以长图它它它它都跑到同一列去了。image.png
这个视觉上没有了参差美可受不了。


于是在接近最优的做法上妥协一步。先把排序前n的数据挨个放到n组,让个高的先均匀分布。


结合保留用户本来的顺序,是不是舒服一些:
image.png


这里依旧不是最佳效果,因为只取了前n个,试想我们如果是3组,5个长图,还是有一组全是长图。但长与短的边界实在无法敲定,除非再搞个每张图片高度的均值,大于均值一定阈值的数据先均匀分布到n组,但这种操作的数据越多,越影响到底部整体的平齐效果。所以还是只选了和组数相同的前n张这么处理。我估摸着大多数用户在文档里的图片是个辅助,不会搞出特别大数量级还夹杂很多长短分明的图。当前能保持一定数量级 (<10)展示上不会有太大问题。


排序


尽量得保证用户原图的顺序,所以需要记录原图的顺序,然后在分组完成后:




  1. 每列里按原图顺序重排下顺序

  2. 列与列之间按第一个图的顺序重排下顺序



能做到尽量接近原顺序但不绝对。


纯数字上[[25], [25], [25,25]][[25,25], [25], [25]]的分组没有差别。但是图片场景又不一样了:
image.png
这排列总透着一股奇怪image.png
于是再让步牺牲一下复杂度:



装最后一组数据分配余数之前,先把分配好的分组,先排序,组与组的和相等时优先放入排前面的数组。



当前版本优缺点


目前至少是在最平齐和图片参差感之间谋求的一个较优解,但绝不是最优解,理论上此类问题不穷举遍历获得不了最优解。但我们可以通过优化局部策略,使它更靠近最优解。不过一定是优于贪心算法把每张图放入高度最小列的做法。这里如果有深入研究过瀑布流的小伙伴有更优的方案,欢迎提供,让语雀的瀑布流更整齐~


做事情咱也不能只说好的,对问题缄口不言,目前的问题有:



  • 前面也说过,如果大量图片,并且存在 分组张数n 的与其他图片长度拉开巨大差距的图片,排版还是不够有参差感

  • 先按大小排序,后分组,会对原图顺序造成偏差,很难复原严格的行列顺序,但用户还是能一定程度的干预排序,只是无法满足一定要求图A和图B不放入同一列这种诉求。从这个角度说,顺序上不如贪心算法方案更接近原顺序,贪心方案的最后一张长图问题其实可以通过主动拖拽顺序把长图放到前面来解决掉,但是这对用户的理解力要求太高了。


anyway,以下的数据哪个算法也无法救🥲。目前列数是根据展示区宽度弹性计算的,这种想优雅可能要触发列数的改变规则了。
image.png


chatGPT的插曲


点我展开查看哭笑不得的过程### 第1轮
image.png
一开始它给了我个贪心算法的不是最优解,得让它进阶


第2轮


image.pngimage.png
看上去很高深,但这测试数据结果不对啊。
我换个说法?是不是不能理解什么叫数字加和尽量接近


第3轮


image.png
结果不对,继续让他换个解法


第4轮


image.png
还是肉眼可见的不对,虽然我肉眼分的也不是最优解,最后我的算法告诉我是可以分成三组和都是80的:[80], [32, 32, 12, 3, 1], [30, 21, 20, 9]


那么问题在哪呢,我尝试问了它一个很简单的问题:
image.png
原来加和都求不对,我放弃它了。。。


:::warning
综上:chatGPT能高效省事让你偷懒,但前提是你得能区分出它的答案靠不靠谱,如果你都不知道真相的问题仍给他,就会被忽悠了也不知道。另外别想用它帮你写笔试题了,它只根据语义生成,但并不真的运行代码,给的代码和结果可能完全不匹配。
:::


📔 基础知识


资料:




复杂度被分为两种级别:一种是O(1),O(log(n)),O(n^a)等,我们把它叫做多项式级的复杂度,因为它的规模n出现在底数的位置;另一种是O(a^n)和O(n!)型复杂度,它是非多项式级的,其复杂度计算机往往不能承受




P问题: 如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P问题
NP问题: NP问题不是非P类问题。NP问题是指可以在多项式的时间里验证一个解的问题。NP问题的另一个定义是,可以在多项式的时间里猜出一个解的问题
NPC问题:同时满足下面两个条件的问题就是NPC问题。首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。NPC问题目前没有多项式的有效算法,只能用指数级甚至阶乘级复杂度的搜索。
**NP-Hard问题:**它满足NPC问题定义的第二条但不一定要满足第一条。NP-Hard问题同样难以找到多项式的算法,但它不列入我们的研究范围,因为它不一定是NP问题。即使NPC问题发现了多项式级的算法,NP-Hard问题有可能仍然无法得到多项式级的算法




约化:(Reducibility,有的资料上叫“归约”)。简单地说,一个问题A可以约化为问题B的含义即是,可以用问题B的解法解决问题A,或者说,问题A可以“变成”问题B。通过对某些问题的不断约化,我们能够不断寻找复杂度更高,但应用范围更广的算法来代替复杂度虽然低,但只能用于很小的一类问题的算法。



next:拼图


接下来我们还会上线更灵活的拼图能力。**拼图算法可以实现任何尺寸的图片,保持原比例不裁剪,用户任意摆放位置,最终绘制成整齐的矩形,**这个算法实现也远比瀑布流复杂。


譬如你可以这样:
image.png
也可以拖成这样:
image.png


还可以拖成这样:
image.png


甚至拖成这样:
image.png


等等等等...... 随意组合排序,最终都能整齐。


等上线后我再写写拼图的故事~


作者:支付宝体验科技
来源:juejin.cn/post/7198370695079903291
收起阅读 »

产品经理:实现一个微信输入框

web
近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。 初期认为这应该改动不大,就是把input换...
继续阅读 »


近期在开发AI对话产品的时候为了提升用户体验增强了对话输入框的相关能力,产品初期阶段对话框只是一个单行输入框,导致在文本内容很多的时候体验很不好,所以进行体验升级,类似还原了微信输入框的功能(只是其中的一点点哈🤏)。


初期认为这应该改动不大,就是把input换成textarea吧。但是实际开发过程发现并没有这么简单,本文仅作为开发过程的记录,因为是基于uniapp开发,相关实现代码都是基于uniapp


简单分析我们大概需要实现以下几个功能点:



  • 默认单行输入

  • 可多行输入,但有最大行数限制

  • 超过限制行术后内容在内部滚动

  • 支持回车发送内容

  • 支持常见组合键在输入框内换行输入

  • 多行输入时高度自适应 & 页面整体自适应


单行输入


默认单行输入比较简单直接使用input输入框即可,使用textarea的时候目前的实现方式是通过设置行内样式的高度控制,如我们的行内高度是36px,那么就设置其高度为36px。为什么要通过这种方式设置呢?因为要考虑后续多行输入超出最大行数的限制,需要通过高度来控制textarea的最大高度。


<textarea style="{ height: 36px }" />


多行输入


多行输入核心要注意的就是控制元素的高度,因为不能随着用户的输入一直增加高度,我们需要设置一个最大的行数限制,超出限制后就不再增加高度,内容可以继续输入,可以在输入框内上下滚动查看内容。


这里需要借助于uniapp内置在textarea@linechange事件,输入框行数变化时调用并回传高度和行数。如果不使用uniapp则需要对输入文字的长度及当前行高计算出对应的行数,这种情况还需要考虑单行文本没有满一行且换行的情况。


代码如下,在linechange事件中获取到最新的高度设置为textarea的高度,当超出最大的行数限制后则不处理。


linechange(event) {
const { height, lineCount } = event.detail
if (lineCount < maxLine) {
this.textareaHeight = height
}
}

这是正常的输入,还有一种情况是用户直接粘贴内容输入的场景,这种时候不会触发@linechange事件,需要手动处理,根据粘贴文本后的textarea的滚动高度进行计算出对应的行数,如超出限制行数则设置为最大高度,否则就设置为实际的行数所对应的高度。代码如下:


const paddingTop = parseInt(getComputedStyle(textarea).paddingTop);
const paddingBottom = parseInt(getComputedStyle(textarea).paddingBottom);
const textHeight = textarea.scrollHeight - paddingTop - paddingBottom;
const numberOfLines = Math.floor(textHeight / lineHeight);

if (numberOfLines > 1 && this.lineCount === 1) {
const lineCount = numberOfLines < maxLine ? numberOfLines : maxLine
this.textareaHeight = lineCount * lineHeight
}

键盘发送内容


正常我们使用电脑聊天时发送内容都是使用回车键发送内容,使用ctrlshiftalt等和回车键的组合键将输入框的文本进行换行处理。所以接下来要实现的就是对键盘事件的监听,基于事件进行发送内容和内容换行输入处理。


首先是事件的监听,uniapp不支持keydown的事件监听,所以这里使用了原生JS做监听处理,为了避免重复监听,对每次开始监听前先进行移除事件的监听,代码如下:


this.$refs.textarea.$el.removeEventListener('keydown', this.textareaKeydownHandle)
this.$refs.textarea.$el.addEventListener('keydown', this.textareaKeydownHandle)

然后是对textareaKeydownHandle方法的实现,这里需要注意的是组合键对内容换行的处理,需要获取到当前光标的位置,使用textarea.selectionStart可获取,基于光标位置增加一个换行\n的输入即可实现换行,核心代码如下:


const cursorPosition = textarea.selectionStart;
if(
(e.keyCode == 13 && e.ctrlKey) ||
(e.keyCode == 13 && e.metaKey) ||
(e.keyCode == 13 && e.shiftKey) ||
(e.keyCode == 13 && e.altKey)
){
// 换行
this.content = `${this.content.substring(0, cursorPosition)}\n${this.content.substring(cursorPosition)}`
}else if(e.keyCode == 13){
// 发送
this.onSend();
e.preventDefault();
}

高度自适应


当多行输入内容时输入框所占据的高度增加,导致页面实际内容区域的高度减小,如果不进行动态处理会导致实际内容会被遮挡。如下图所示,红色区域就是需要动态处理的高度。



主要需要处理的场景就是输入内容行数变化的时候和用户粘贴文本的时候,这两种情况都会基于当前的可视行数计算输入框的高度,那么内容区域的高度就好计算了,使用整个窗口的高度减去输入框的高度和其他固定的高度如导航高度和底部安全距离高度即是真实内容的高度。


this.contentHeight = this.windowHeight - this.navBarHeight - this.fixedBottomHeight - this.textareaHeight;

最后


到此整个输入框的体验优化核心实现过程就结束了,增加了多行输入,组合键换行输入内容,键盘发送内容,整体内容高度自适应等。整体实现过程的细节功能点还是比较多,有实现过类似需求的同学欢迎留言交流~


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


作者:南城FE
来源:juejin.cn/post/7267791228872753167
收起阅读 »

微信小程序 折叠屏适配

web
最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后...
继续阅读 »

最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后解决了,因为是甲方内部使用的小程序,这里不便贴图,但有官方案例图片,以供参考


查看了微信官网
大屏适配
响应显示区域变化


启用大屏模式


从小程序基础库版本 2.21.3 开始,在 Windows、Mac、车机、安卓 WMPF 等大屏设备上运行的小程序可以支持大屏模式。可参考小程序大屏适配指南。方法是:在 app.json 中添加 "resizable": true


看到这里我心里窃喜,就加个配置完事了?这也太简单了,但后面证明我想简单了,
主要有两大问题:



  • 1 尺寸不同的情况下内容展示效果兼容问题

  • 2 预览版和体验版 大屏模式冷启动会生效,但热启动 和 菜单中点击重新进入小程、授权操作,会失效变成窄屏


解决尺寸问题


因为css的长度单位大部分用的 rpx,窄屏和宽屏展示差异出入较大,别说客户不认,自己这关就过不了,简直都不忍直视,整个乱成一片,尤其登录页,用了定位,更是乱上加乱。


随后参考了官方的文档 小程序大屏适配指南自适应布局,方案对于微信小程序原生开发是可行的,但这个项目用的 uni-app开发的,虽然uni-app 也有对应的响应式布局组件,再加上我是个比较爱偷懒的人(甲方给的工期事件也有限制),不可能花大量时间把所有也页面重新写一遍布局,这是不现实的。


于是又转战到uni-app官网寻找解决方案 uni-app宽屏适配指南


内容缩放拉伸的处理 这一段中提出了两个策略



  • 1.局部拉伸:页面内容划分为固定区域和长宽动态适配区域,固定区域使用固定的px单位约定宽高,长宽适配区域则使用flex自动适配。当屏幕大小变化时,固定区域不变,而长宽适配区域跟着变化

  • 2.等比缩放:根据页面屏幕宽度缩放。rpx其实属于这种类型。在宽屏上,rpx变大,窄屏上rpx变小。


随后看到这句话特别符合我的需求,哈哈 省事 省事 省事


策略2省事,设计师按750px屏宽出图,程序员直接按rpx写代码即可。但策略2的实际效果不如策略1好。程序员使用策略1,分析下界面,设定好局部拉伸区域,这样可以有更好的用户体验


具体实现


1.配置 pages.json 的 globeStyle


{
"globalStyle": {
"rpxCalcMaxDeviceWidth": 1200, // rpx 计算所支持的最大设备宽度,单位 px,默认值为 960
"rpxCalcBaseDeviceWidth": 375, // rpx 计算使用的基准设备宽度,设备实际宽度超出 rpx 计算所支持的最大设备宽度时将按基准宽度计算,单位 px,默认值为 375
"rpxCalcIncludeWidth": 750 // rpx 计算特殊处理的值,始终按实际的设备宽度计算,单位 rpx,默认值为 750
},
}

2.单位兼容


还有一点官方也提出来了很重要,那就很多时候 会把宽度750rpx 当成100% 使用,这在宽屏的设备上就会有问题, uniapp给了两种解决方案



  • 750rpx 改为100%

  • 另一种是配置rpxCalcIncludeWidth,设置某个特定数值不受rpxCalcMaxDeviceWidth约束


想要用局部拉伸:页面内容划分为固定区域和长宽动态适配区域”的策略,单位必须用px


添加脚本


项目根目录新增文件 postcss.config.js 内容如下。则在编译时,编译器会自动转换rpx单位为px。


// postcss.config.js

const path = require('path')
module.exports = {
parser: 'postcss-comment',
plugins: {
'postcss-import': {
resolve(id, basedir, importOptions) {
if (id.startsWith('~@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
} else if (id.startsWith('@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
} else if (id.startsWith('/') && !id.startsWith('//')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
}
return id
}
},
'autoprefixer': {
overrideBrowserslist: ["Android >= 4", "ios >= 8"],
remove: process.env.UNI_PLATFORM !== 'h5'
},
// 借助postcss-px-to-viewport插件,实现rpx转px,文档:https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
// 以下配置,可以将rpx转换为1/2的px,如20rpx=10px,如果要调整比例,可以调整 viewportWidth 来实现
'postcss-px-to-viewport': {
unitToConvert: 'rpx',
viewportWidth: 200,
unitPrecision: 5,
propList: ['*'],
viewportUnit: 'px',
fontViewportUnit: 'px',
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
replace: true,
exclude: undefined,
include: undefined,
landscape: false
},
'@dcloudio/vue-cli-plugin-uni/packages/postcss': {}
}
}


大屏模式失效问题


下面重头戏来了,这期间经历 蜿蜒曲折 ,到头来发现都是无用功,我自己都有被wx蠢到发笑,唉,


样式问题解决后 开始着重钻研 大屏失效的问题,但看了官方的多端适配示例demo,人家的就是好的,那就应该有解决办法,于是转战github地址 下项目,谁知这项目暗藏机关,各种报错,让你跑不起来。。。。,让我一度怀疑腾讯也这么拉跨


还好issues 区一位大神有解决办法 感兴趣的老铁可以去瞅瞅
image


1693664649860.jpg


另外 微信小程序开发工具需要取消这两项,最后当项目跑起来后我还挺开心,模拟器上没有问题,但用真机预览的时候我啥眼了,还是窄屏,偶尔可以大屏,后面发现 冷启动是大屏,热启动和点击右上角菜单中的重新进入小程序按钮都会自己变成窄屏幕


官方案例.gif批量更新.gif

这是官方的项目啊,为啥人家的可以,我本地跑起来却不可以,让我一度怀疑这里有内幕,经过几轮测试还是不行,于是乎,我开始了各种询问查资料,社区、私聊、评论、github issues,最后甚至 统计出来了 多端适配示例demo 开发者的邮箱 挨个发了邮件,但都结果无一例外,全部石沉大海


1693666642117.jpgwx-github-issues-110.jpg
私聊.jpg评论.jpg
wx-mini-dev.jpgimage.png

结果就是,没有办法了,想看看是不是只有预览和体验版有问题,后面发布到正式版后,再看居然没问题了,就是这么神奇,也是无语!!!! 原来做了这么多无用功。。。。


作者:iwhao
来源:juejin.cn/post/7273764921456492581
收起阅读 »

详解JS判断页面是在手机端还是在PC端打开的方法

web
下面详细介绍一下如何判断页面是在手机端还是在PC端打开,并提供两条示例说明。 方法一:使用UA判断 UA(UserAgent)是指HTTP请求头中的一部分,用于标识客户端的一些信息,比如用户的设备类型、浏览器型号等等。因此,我们可以通过判断UA中的关键字来确定...
继续阅读 »

下面详细介绍一下如何判断页面是在手机端还是在PC端打开,并提供两条示例说明。


方法一:使用UA判断


UA(UserAgent)是指HTTP请求头中的一部分,用于标识客户端的一些信息,比如用户的设备类型、浏览器型号等等。因此,我们可以通过判断UA中的关键字来确定页面访问者的设备类型。下面是实现的代码:


const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

if (isMobile) {
console.log('当前在手机端');
} else {
console.log('当前在PC端');
}

代码解析:


首先,我们使用正则表达式匹配navigator.userAgent中是否包含iPhoneiPadiPodAndroid这些关键字,如果匹配成功,则说明当前是在移动端。如果匹配失败,则说明当前是在PC端。


需要注意的是,该方法并不100%准确,因为用户可以使用PC浏览器模拟手机UA,也有可能使用移动端浏览器访问PC网站。


方法二:使用媒体查询判断


媒体查询是CSS3的一个新特性,可以根据不同的媒体类型(比如设备屏幕的宽度、高度、方向等)来设置不同的CSS样式。我们可以利用媒体查询来判断页面是在手机端还是在PC端打开。下面是实现的代码:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>判断页面是在手机端还是在PC端</title>
<style>
/* 默认样式 */
p {
font-size: 24px;
color: yellow;
}
/* 移动端样式 */
@media (max-width: 767px) {
p {
font-size: 20px;
color: green;
}
}
</style>
</head>
<body>
<p>测试内容</p>
</body>
</html>

代码解析:


在CSS中,我们使用@media关键字定义了一个媒体查询,当浏览器宽度小于等于767px的时候,p元素的字体大小和颜色都会发生改变,从而实现了对移动端的识别。如果浏览器宽度大于767px,则会使用默认样式。


需要注意的是,该方法只能判断设备的屏幕宽度,不能确定设备的真实类型,因此并不太准确。


总的来说,两种方法各有优缺点,具体选择哪种方法要根据自己的需求和场景来决定。一般来说,如果只是想简单地判断页面访问者的设备类型,使用第一种方法即可。如果需要根据设备类型来优化网站的布局和样式,可以使用第二种方法。


作者:RuiRay
来源:juejin.cn/post/7273746154642014262
收起阅读 »

强大的css计数器,你确定不来看看?

web
强大的 css 计数器 css content 属性有很多实用的用法,这其中最为强大的莫过于是计数器了,它甚至可以实现连 javascript 都不能够实现的效果,下面我们一起来研究一下吧。 css 计数器主要有 3 个关键点需要掌握。如下: 首先需要一个计...
继续阅读 »

强大的 css 计数器


css content 属性有很多实用的用法,这其中最为强大的莫过于是计数器了,它甚至可以实现连 javascript 都不能够实现的效果,下面我们一起来研究一下吧。


css 计数器主要有 3 个关键点需要掌握。如下:



  1. 首先需要一个计数器的名字,这个名字由使用者自己定义。

  2. 计数器有一个计数规则,比如是 1,2,3,4...这样的递增方式,还是 1,2,1,2...这样的连续递增方式。

  3. 计数器的使用,即定义好了一个计数器名字和计数规则,我们就需要去使用它。


以上 3 个关键点分别对应的就是 css 计数器的 counter-reset 属性,counter-increment 属性,和 counter()/counters()方法。下面我们依次来介绍这三个玩意儿。


counter-reset 属性


counter-reset 属性叫做计数器重置,对应的就是创建一个计数器名字,如果可以,顺便也可以告诉计数器的计数起始值,也就是从哪个值开始计数,默认值是 0,注意是 0,而不是 1。例如以下一个示例:


html 代码如下:


<p>开始计数,计数器名叫counter</p>
<p class="counter"></p>

css 代码如下:


.counter {
counter-reset: counter;
}

.counter::before {
content: counter(counter);
}

在浏览器中运行以上示例,你会看到如下图所示:


counter-1.png


可以看到计数器的初始值就是 0,现在我们修改一下 css 代码,如下所示:


.counter {
counter-reset: counter 1;
}

在浏览器中运行以上示例,你会看到如下图所示:


counter-2.png


这次我们指定了计数器的初始值 1,所以结果就是 1,计数器的初始值同样也可以指定成小数,负数,如-2,2.99 之类,只不过 IE 和 FireFox 浏览器都会认为是不合法的数值,当做默认值 0 来处理,谷歌浏览器也会直接显示负数,如下图所示:


counter-3.png


低版本谷歌浏览器处理小数的时候是向下取整,比如 2.99 则显示 2,最新版本则当成默认值 0,来处理,如下图所示:


counter-4.png



ps: 当然不推荐指定初始值为负数或者小数。



你以为到这里就完了吗?还没有,计数器还可以指定多个,每一个计数器之间用空格隔开,比如以下代码:


.counter {
counter-reset: counter1 1 counter2 2;
}

.counter::before {
content: counter(counter1) counter(counter2);
}

在浏览器中运行以上示例,你会看到如下图所示:


counter-5.png


除此之外,计数器名还可以指定为 none 和 inherit,也就是取消计数和继承计数器,这没什么好说的。


counter-increment


顾名思义,该属性就是计数器递增的意思,也就是定义计数器的计数规则,值为计数器的名字,可以是一个或者多个,并且也可以指定一个数字,表示计数器每次变化的数字,如果不指定,默认就按照 1 来变化。比如以下代码:


.counter {
counter-reset: counter 1;
counter-increment: counter;
}

得到的结果就是: 1 + 1 = 2。如下图所示:


counter-6.png


再比如以下代码:


.counter {
counter-reset: counter 2;
counter-increment: counter 3;
}

得到的结果就是: 2 + 3 = 5,如下图所示:


counter-7.png


由此可见,计数器的规则就是: 计数器名字唯一,每指定一次计数规则,计数器就会加一,每指定二次计数规则,计数器就会加二,……以此类推。


计数规则不仅可以创建在元素上,也可以创建在使用计数器的元素上,比如以下代码:


.counter {
counter-reset: counter;
counter-increment: counter;
}

.counter::before {
content: counter(counter);
counter-increment: counter;
}

我们不仅在类名为 counter 元素上创建了一个计数器规则,同样的也在 before 伪元素上创建了一个计数器规则,因此最后的结果就是: 0 + 1 + 1 = 2。如下图所示:


counter-8.png


总而言之,无论位置在何处,只要有 counter-increment,对应的计数器的值就会变化, counter()只是输出而已!计数器的数值变化遵循 HTML 渲染顺序,遇到一个 increment 计数器就变化,什么时候 counter 输出就输出此时的计数值。


除此之外,计数器规则也可以和计数器一样,创建多个计数规则,也是以空格区分,比如以下示例代码:


.counter {
counter-reset: counter1 1 counter2 2;
counter-increment: counter1 2 counter2 3;
}

.counter::before {
content: counter(counter1) counter(counter2);
counter-increment: counter1 4 counter2 5;
}

此时的结果就应该是计数器 1: 1 + 2 + 4 = 7,计数器 2: 2 + 3 + 5 = 10。如下图所示:


counter-9.png


同样的,计数器规则的值也可以是负数,也就是递减效果了,比如以下代码:


.counter {
counter-reset: counter1 1 counter2 2;
counter-increment: counter1 -1 counter2 -3;
}

.counter::before {
content: counter(counter1) counter(counter2);
counter-increment: counter1 2 counter2 5;
}

此时的结果就应该是计数器 1: 1 - 1 + 2 = 2,计数器 2: 2 - 3 + 5 = 4。如下图所示:


counter-10.png


同样的计数规则的值也可以是 none 或者 inherit。


counter


counter 方法类似于 calc,主要用于定义计数器的显示输出,到目前为止,我们前面的示例都是最简单的输出,也就是如下语法:


counter(name); /* name为计数器名 */

实际上还有如下的语法:


counter(name,style);

style 参数和 list-style-type 的值一样,意思就是不仅可以显示数字,还可以显示罗马数字,中文字符,英文字母等等,值如下:


list-style-type: disc | circle | square | decimal | lower-roman | upper-roman |
lower-alpha | upper-alpha | none | armenian | cjk-ideographic | georgian |
lower-greek | hebrew | hiragana | hiragana-iroha | katakana | katakana-iroha |
lower-latin | upper-latin | simp-chinese-informal;

比如以下的示例代码:


.counter {
counter-reset: counter;
counter-increment: counter;
}

.counter::before {
content: counter(counter, lower-roman);
}

结果如下图所示:


counter-11.png


再比如以下的示例代码:


.counter {
counter-reset: counter;
counter-increment: counter;
}

.counter::before {
content: counter(counter, simp-chinese-informal);
}

结果如下图所示:


counter-12.png


同样的 counter 也可以支持级联,也就是说,一个 content 属性值可以有多个 counter 方法,如:


.counter {
counter-reset: counter;
counter-increment: counter;
}

.counter::before {
content: counter(counter) '.' counter(counter);
}

结果如下图所示:


counter-13.png


counters


counters 方法虽然只是比 counter 多了一个 s 字母,但是含义可不一样,counters 就是用来嵌套计数器的,什么意思了?我们平时如果显示列表符号,不可能只是单单显示 1,2,3,4...还有可能显示 1.1,1.2,1.3...前者是 counter 做的事情,后者就是 counters 干的事情。


counters 的语法为:


counters(name, string);

name 就是计数器名字,而第二个参数 string 就是分隔字符串,比如以'.'分隔,那 string 的值就是'.',以'-'分隔,那 string 的值就是'-'。来看如下一个示例:


html 代码如下:


<div class="reset">
<div class="counter">
javascript框架
<div class="reset">
<div class="counter">&nbsp;angular</div>
<div class="counter">&nbsp;react</div>
<div class="counter">
vue
<div class="reset">
<div class="counter">
vue语法糖
<div class="reset">
<div class="counter">&nbsp;@</div>
<div class="counter">&nbsp;v-</div>
<div class="counter">&nbsp;:</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

css 代码如下:


.reset {
counter-reset: counter;
padding-left: 20px;
}

.counter::before {
content: counters(counter, '-') '.';
counter-increment: counter;
}

结果如下图所示:


counter-14.png


这种计数效果在模拟书籍的目录效果时非常实用,比如写文档,会有嵌套标题的情况,还有一个比较重要的点需要说明一下,就是显示 content 计数值的那个 DOM 元素在文档流中的位置一定要在 counter-increment 元素的后面,否则是没有计数效果的。


总而言之,content 计数器是非常强大的,以上都只是很基础的用法,真正掌握还需要大量的实践以及灵感还有创意。


作者:夕水
来源:juejin.cn/post/7275176987358265355
收起阅读 »

前端埋点实现方案

前端埋点的简介埋点是指在软件、网站或移动应用中插入代码,用于收集和跟踪用户行为数据。通过在特定位置插入埋点代码,开发人员可以记录用户在应用中的操作,如页面浏览、按钮点击、表单提交等。这些数据可以用于分析用户行为、优化产品功能、改进用户体验等目的。 埋点通常与...
继续阅读 »

前端埋点的简介

  • 埋点是指在软件、网站或移动应用中插入代码,用于收集和跟踪用户行为数据。

  • 通过在特定位置插入埋点代码,开发人员可以记录用户在应用中的操作,如页面浏览、按钮点击、表单提交等。

  • 这些数据可以用于分析用户行为、优化产品功能、改进用户体验等目的。




埋点通常与数据分析工具结合使用,如Google Analytics、Mixpanel等,以便对数据进行可视化和进一步分析。


前端埋点是指在前端页面中嵌入代码,用于收集和跟踪用户行为数据。


通过埋点可以获取用户在网页或应用中的点击、浏览、交互等动作,用于分析用户行为、优化产品体验和进行数据驱动的决策。


在前端埋点中,常用的方式包括:

  1. 页面加载埋点:用于追踪和监测页面的加载时间、渲染状态等信息。
  2. 点击事件埋点:通过监听用户的点击事件,记录用户点击了哪些元素、触发了什么操作,以及相关的参数信息。
  3. 表单提交埋点:记录用户在表单中输入的内容,并在用户提交表单时将这些数据发送到后台进行保存和分析。
  4. 页面停留时间埋点:用于记录用户在页面停留的时间,以及用户与页面的交互行为,如滚动、鼠标悬停等。
  5. AJAX请求埋点:在前端的AJAX请求中添加额外的参数,用于记录请求的发送和返回状态,以及相应的数据。

埋点数据可以通过后端API或第三方数据分析工具发送到服务器进行处理和存储。


在使用前端埋点时,需要注意保护用户隐私,遵守相关法律法规,并确保数据采集和使用的合法性和合规性。


同时,还需设计良好的数据模型和分析策略,以便从埋点数据中获得有价值的信息。


前端埋点设计


前面说过,前端埋点是一种数据追踪的技术,用于收集和分析用户的行为数据。


前端埋点设计方案有哪些?


下面简单介绍一下:

  1. 事件监听:通过监听用户的点击、滚动、输入等事件,记录用户的操作行为。可以使用JavaScript来实现事件监听,例如使用addEventListener()函数进行事件绑定。

  2. 自定义属性:在HTML元素中添加自定义属性,用于标识不同的元素或事件。 例如,在按钮上添加data-*属性,表示不同的按钮类型或功能。当用户与这些元素进行交互时,可以获取相应的属性值作为事件标识。

  3.  发送请求:当用户触发需要追踪的事件时,可以通过发送异步请求将数据发送到后台服务器。 可以使用XMLHttpRequest、fetch或者第三方的数据上报SDK来发送请求。

  4. 数据格式:确定需要采集的数据格式,包括页面URL、时间戳、用户标识、事件类型、操作元素等信息。 通常使用JSON格式来封装数据,方便后续的数据处理和分析。

  5. 用户标识:对于需要区分用户的情况,可以在用户首次访问时生成一个唯一的用户标识,并将该标识存储在浏览器的cookie中或使用localStorage进行本地存储。

  6. 数据上报:将采集到的数据发送到后台服务器进行存储和处理。可以自建后台系统进行数据接收和分析,也可以使用第三方的数据分析工具,例如百度统计、Google Analytics等。

  7.  隐私保护:在进行数据采集和存储时,需要注意用户隐私保护。

  8.  遵守相关的法律法规,对敏感信息进行脱敏处理或加密存储,并向用户明示数据采集和使用政策。


需要注意的是,在进行埋点时要权衡数据采集的成本与收益,确保收集到的数据具有一定的价值和合法性。


同时,要注意保护用户隐私,遵守相关法律法规,尊重用户的选择和权益。


前端埋点示例


以下是一个完整的前端埋点示例


展示了如何在网站上埋点统计页面浏览、按钮点击和表单提交事件

  • 在HTML中标识需要采集的元素或事件:
<button id="myButton" data-track-id="button1">Click Me</button>

<form id="myForm">
  <input type="text" name="username" placeholder="Username">
  <input type="password" name="password" placeholder="Password">
  <button type="submit">Submit</button>
</form>

在按钮和表单元素上添加了data-track-id自定义属性,用于标识这些元素。

  • 使用JavaScript监听事件并获取事件数据:
// 监听页面加载事件
window.addEventListener("load", function() {
  var pageUrl = window.location.href;
  var timestamp = new Date().getTime();
  var userData = {
    eventType: "pageView",
    pageUrl: pageUrl,
    timestamp: timestamp
    // 其他需要收集的用户数据
  };

  // 封装数据格式并发送请求
  sendData(userData);
});

// 监听按钮点击事件
document.getElementById("myButton").addEventListener("click", function(event) {
  var buttonId = event.target.getAttribute("data-track-id");
  var timestamp = new Date().getTime();
  var userData = {
    eventType: "buttonClick",
    buttonId: buttonId,
    timestamp: timestamp
    // 其他需要收集的用户数据
  };

  // 封装数据格式并发送请求
  sendData(userData);
});

// 监听表单提交事件
document.getElementById("myForm").addEventListener("submit", function(event) {
  event.preventDefault(); // 阻止表单默认提交行为

  var formId = event.target.getAttribute("id");
  var formData = new FormData(event.target);
  var timestamp = new Date().getTime();
  var userData = {
    eventType: "formSubmit",
    formId: formId,
    formData: Object.fromEntries(formData.entries()),
    timestamp: timestamp
    // 其他需要收集的用户数据
  };

  // 封装数据格式并发送请求
  sendData(userData);
});

通过JavaScript代码监听页面加载、按钮点击和表单提交等事件,获取相应的事件数据,包括页面URL、按钮ID、表单ID和表单数据等。

  • 发送数据请求:
function sendData(data) {
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "/track", true);
  xhr.setRequestHeader("Content-Type", "application/json");

  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      console.log("Data sent successfully.");
    }
  };

  xhr.send(JSON.stringify(data));
}

使用XMLHttpRequest对象发送POST请求,将封装好的数据作为请求的参数发送到后台服务器的/track接口。

  • 后台数据接收与存储:

后台服务器接收到前端发送的数据请求后,进行处理和存储。


可以使用后端开发语言(如Node.js、Python等)来编写接口逻辑,将数据存储到数据库或其他持久化存储中。


通过监听页面加载、按钮点击和表单提交等事件,并将相关数据发送到后台服务器进行存储和分析。


根据具体项目需求,可以扩展和定制各种不同类型的埋点事件和数据采集。


vue 前端埋点示例


在Vue中实现前端埋点可以通过自定义指令或者混入(mixin)来完成。


下面给出两种常见的Vue前端埋点示例:

  • 自定义指令方式:
// 在 main.js 中注册全局自定义指令 track
import Vue from 'vue';

Vue.directive('track', {
  bind(el, binding, vnode) {
    const { event, data } = binding.value;
    
    el.addEventListener(event, () => {
      // 埋点逻辑,例如发送请求或记录日志
      console.log("埋点事件:" + event);
      console.log("埋点数据:" + JSON.stringify(data));
    });
  }
});

在组件模板中使用自定义指令:

<template>
  <button v-track="{ event: 'click', data: { buttonName: '按钮A' } }">点击按钮A</button>
</template>

  • 1. 混入方式:
// 创建一个名为 trackMixin 的混入对象,并定义需要进行埋点的方法
const trackMixin = {
  methods: {
    trackEvent(event, data) {
      // 埋点逻辑,例如发送请求或记录日志
      console.log("埋点事件:" + event);
      console.log("埋点数据:" + JSON.stringify(data));
    }
  }
};

// 在组件中使用混入
export default {
  mixins: [trackMixin],
  mounted() {
    // 在需要进行埋点的地方调用混入的方法
    this.trackEvent('click', { buttonName: '按钮A' });
  },
  // ...
};

这两种方式都可以实现前端埋点,你可以根据自己的项目需求选择适合的方式。


在实际应用中,你需要根据具体的埋点需求来编写逻辑,例如记录页面浏览、按钮点击、表单提交等事件,以及相应的数据收集和处理操作。


使用自定义指令(Custom Directive)的方式来实现前端埋点


在Vue 3中,你可以使用自定义指令(Custom Directive)的方式来实现前端埋点。


一个简单的Vue 3的前端埋点示例:


  • 创建一个名为analytics.js的文件,用于存放埋点逻辑:
// analytics.js

export default {
  mounted(el, binding) {
    const { eventType, eventData } = binding.value;

    // 发送数据请求
    this.$http.post('/track', {
      eventType,
      eventData,
    })
    .then(() => {
      console.log('Data sent successfully.');
    })
    .catch((error) => {
      console.error('Error sending data:', error);
    });
  },
};

  • 在Vue 3应用的入口文件中添加全局配置:
import { createApp } from 'vue';
import App from './App.vue';
import axios from 'axios';

const app = createApp(App);

// 设置HTTP库
app.config.globalProperties.$http = axios;

// 注册全局自定义指令
app.directive('analytics', analyticsDirective);

app.mount('#app');

  • 在组件中使用自定义指令,并传递相应的事件类型和数据:
<template>
  <button v-analytics="{ eventType: 'buttonClick', eventData: { buttonId: 'myButton' } }">Click Me</button>
</template>

在示例中,我们定义了一个全局的自定义指令v-analytics,它接受一个对象作为参数,对象包含了事件类型(eventType)和事件数据(eventData)。当元素被插入到DOM中时,自定义指令的mounted钩子函数会被调用,然后发送数据请求到后台服务器。


注意,在示例中使用了axios作为HTTP库发送数据请求,你需要确保项目中已安装了axios,并根据实际情况修改请求的URL和其他配置。


通过以上设置,你可以在Vue 3应用中使用自定义指令来实现前端埋点,采集并发送相应的事件数据到后台服务器进行存储和分析。请根据具体项目需求扩展和定制埋点事件和数据采集。


使用Composition API的方式来实现前端埋点


以下是一个Vue 3的前端埋点示例,使用Composition API来实现:

  • 创建一个名为analytics.js的文件,用于存放埋点逻辑:
// analytics.js

import { ref, onMounted } from 'vue';

export function useAnalytics() {
  const trackEvent = (eventType, eventData) => {
    // 发送数据请求
    // 模拟请求示例,请根据实际情况修改具体逻辑
    console.log(`Sending ${eventType} event with data:`, eventData);
  };

  onMounted(() => {
    // 页面加载事件
    trackEvent('pageView', {
      pageUrl: window.location.href,
    });
  });

  return {
    trackEvent,
  };
}

  • 在需要进行埋点的组件中引入useAnalytics函数并使用:
import { useAnalytics } from './analytics.js';

export default {
  name: 'MyComponent',
  setup() {
    const { trackEvent } = useAnalytics();

    // 按钮点击事件
    const handleClick = () => {
      trackEvent('buttonClick', {
        buttonId: 'myButton',
      });
    };

    return {
      handleClick,
    };
  },
};

  • 在模板中使用按钮并绑定相应的点击事件:
<template>
  <button id="myButton" @click="handleClick">Click Me</button>
</template>

在示例中,我们将埋点逻辑封装在了analytics.js文件中的useAnalytics函数中。在组件中使用setup函数来引入useAnalytics函数,并获取到trackEvent方法进行埋点操作。在模板中,我们将handleClick方法绑定到按钮的点击事件上。


当页面加载时,会自动发送一个pageView事件的请求。当按钮被点击时,会发送一个buttonClick事件的请求。


注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


通过以上设置,你可以在Vue 3应用中使用Composition API来实现前端埋点,采集并发送相应的事件数据。请根据具体项目需求扩展和定制埋点事件和数据采集。


react 前端埋点示例


使用自定义 Hook 实现


当然!以下是一个 React 的前端埋点示例,


使用自定义 Hook 实现:

  • 创建一个名为 useAnalytics.js 的文件,用于存放埋点逻辑:
// useAnalytics.js

import { useEffect } from 'react';

export function useAnalytics() {
  const trackEvent = (eventType, eventData) => {
    // 发送数据请求
    // 模拟请求示例,请根据实际情况修改具体逻辑
    console.log(`Sending ${eventType} event with data:`, eventData);
  };

  useEffect(() => {
    // 页面加载事件
    trackEvent('pageView', {
      pageUrl: window.location.href,
    });
  }, []);

  return {
    trackEvent,
  };
}

  • 在需要进行埋点的组件中引入 useAnalytics 自定义 Hook 并使用:
import { useAnalytics } from './useAnalytics';

function MyComponent() {
  const { trackEvent } = useAnalytics();

  // 按钮点击事件
  const handleClick = () => {
    trackEvent('buttonClick', {
      buttonId: 'myButton',
    });
  };

  return (
    <button id="myButton" onClick={handleClick}>Click Me</button>
  );
}

export default MyComponent;

在示例中,我们将埋点逻辑封装在了 useAnalytics.js 文件中的 useAnalytics 自定义 Hook 中。在组件中使用该自定义 Hook 来获取 trackEvent 方法以进行埋点操作。在模板中,我们将 handleClick 方法绑定到按钮的点击事件上。


当页面加载时,会自动发送一个 pageView 事件的请求。当按钮被点击时,会发送一个 buttonClick 事件的请求。


请注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


通过以上设置,你可以在 React 应用中使用自定义 Hook 来实现前端埋点,采集并发送相应的事件数据。根据具体项目需求,你可以扩展和定制埋点事件和数据采集逻辑。


使用高阶组件(Higher-Order Component)实现


当然!以下是一个 React 的前端埋点示例,


使用高阶组件(Higher-Order Component)实现:

  • 创建一个名为 withAnalytics.js 的高阶组件文件,用于封装埋点逻辑:
// withAnalytics.js

import React, { useEffect } from 'react';

export function withAnalytics(WrappedComponent) {
  return function WithAnalytics(props) {
    const trackEvent = (eventType, eventData) => {
      // 发送数据请求
      // 模拟请求示例,请根据实际情况修改具体逻辑
      console.log(`Sending ${eventType} event with data:`, eventData);
    };

    useEffect(() => {
      // 页面加载事件
      trackEvent('pageView', {
        pageUrl: window.location.href,
      });
    }, []);

    return <WrappedComponent trackEvent={trackEvent} {...props} />;
  };
}

  • 在需要进行埋点的组件中引入 withAnalytics 高阶组件并使用:
import React from 'react';
import { withAnalytics } from './withAnalytics';

function MyComponent({ trackEvent }) {
  // 按钮点击事件
  const handleClick = () => {
    trackEvent('buttonClick', {
      buttonId: 'myButton',
    });
  };

  return (
    <button id="myButton" onClick={handleClick}>Click Me</button>
  );
}

export default withAnalytics(MyComponent);

在示例中,我们创建了一个名为 withAnalytics 的高阶组件,它接受一个被包裹的组件,并通过属性传递 trackEvent 方法。在高阶组件内部,我们在 useEffect 钩子中处理页面加载事件的埋点逻辑,并将 trackEvent 方法传递给被包裹组件。被包裹的组件可以通过属性获取到 trackEvent 方法,并进行相应的埋点操作。


在模板中,我们将 handleClick 方法绑定到按钮的点击事件上。


当页面加载时,会自动发送一个 pageView 事件的请求。当按钮被点击时,会发送一个 buttonClick 事件的请求。


请注意,在示例中,我们只是模拟了数据请求操作,请根据实际情况修改具体的发送数据请求的逻辑。


通过以上设置,你可以在 React 应用中使用高阶组件来实现前端埋点,采集并发送相应的事件数据。


当然根据具体项目需求,你还可以扩展和定制埋点事件和数据采集逻辑。


作者:朦胧之
链接:https://juejin.cn/post/7274084216286183460
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

如何突破技术瓶颈(适合P6以下)

前言 最近在更新react组件库源码的文章,其实vue和其他框架都一样,就是我发现自己在一年前直接看这些源码(不调试),还是有点吃力的,然后就放弃了。 可最近不知道为啥,看这些源码对我来说没啥太大难度,直接干就完事了,不需要啥调试。自己好好回想了一下,为什么会...
继续阅读 »

前言


最近在更新react组件库源码的文章,其实vue和其他框架都一样,就是我发现自己在一年前直接看这些源码(不调试),还是有点吃力的,然后就放弃了。


可最近不知道为啥,看这些源码对我来说没啥太大难度,直接干就完事了,不需要啥调试。自己好好回想了一下,为什么会有这样的变化?也算帮助一些想突破自己技术瓶颈的同学。


有新人在下面留言说看到很焦虑,刚进前端领域的同学,你们首要任务是能完成业务开发,此时业务开发带给你的提升是最明显的,文章更多的是帮助业务api用熟之后的想有突破的同学,不用焦虑,哈哈。而且话说回来了,我在平时工作中看到不想突破的人基本占百分90%,无论大小厂,所以不突破也没啥,大部分人只是仅仅当一个普通工作而已。


结论


首先我得出结论是:

  • 最开始不要自己去读源码,看别人的文章和视频即可,目的是先接触比自己能力层次高的代码,为超越现有的能力铺路(后面详细谈怎么做)
  • 平时注意积累一些手写题的思路,网上面经很多,主要不是写出来,是理解原理,理解大于一切,不理解的东西终究会忘记,我们要积累的是能力,能力是第一!(后面详细谈),设计模式里的发布订阅者模式必须要理解!这是写很多库常见的技巧。
  • 最后开始独立去看一些小的代码库,比如腾讯,阿里,字节的组件库,这些库大部分组件难度低。

去哪里看视频和文章学源码


视频


最简易的就是跟着视频学,因为视频会把代码敲一遍,给你思考的时间,讲解也是最细的,很适合刚开始想造轮子的同学了解一些有难度的源码。


举个例子:


我当时看了koa的源码,了解了koa中间件的原理,我自己造了一个自动化发布脚本就利用了这个原理,redux中间件也是类似的原理,在函数式编程领域叫做compose函数,koa是异步compose,redux是同步compose,


简单描述下什么是compose函数


我把大象装进冰箱是不是要
1、打开冰箱门
2、装进去大象
3、关冰箱门


那么很多同学就会写一个函数

function 装大象(){
// 打开冰箱
// 装大象
// 关闭冰箱门
}

compose函数会把这个过程拆开,并且抽象化

// 把装大象抽象为装东西函数
function 装东西();
function 打开冰箱();
function 关闭冰箱();

compose(打开冰箱函数, 装东西函数,关闭冰箱函数)

此时compose把上面三个函数抽象为一个打开冰箱往里面装东西的函数,我们只需要把参数大象穿进去就抽象了整个过程

compose(打开冰箱函数, 装东西函数,关闭冰箱函数)(大象)

具体内容我还写过一篇文章,有兴趣的同学可以去看看:


终极compose函数封装方案!


这个大家应该有自己的去处,我自己的话很简单,视频一般去b站,就是bilibili,有些同学以为这是一个二次元网站是吧,其实里面免费的学习资料一抓一大把呢,啥都有。


比如说我在b站看了很多linux入门教学视频,还有一个培训公开课,讲的都是源码,什么手写react hook,手写webpack,手写xxx,那个时候说实话,听了视频也不是很理解,但是我还是挺喜欢前端的,没咋理解就继续听。


记住,我们需要短时间内提升能力,所以视频算是其中最快的了,其他方法不可能有这个来的快,并且没理解就算了,能理解多少是多少。


学习是一个螺旋上升的过程,不是一下子就全懂或者全不懂的,都是每次比上一次更懂一点。除非你是天才,急不来的。


视频搜索第二大去处就是论坛,一些论坛有各种各样的培训视频,这种论坛太多了,你谷歌或者百度一抓一大把。


对了,谷歌是爸爸,你懂我意思,不要吝啬小钱。在搜索学习资料面前,百度就是个弟弟。


文章


文章一定记住,在精不在多。


切记,每个人都处在不同的学习阶段,不要盲目追求所谓的大神文章,不一定适合你,比如说有些人刚接触前端,你去看有些有深度的文章对你没啥好处,浪费时间,因为你理解不了,理解不了的知识相当于没学,过两天就忘了。


文章选择范围,比如掘金,知乎还有前端公众号,基本上就差不多了,选一两个你觉得你这个阶段能吸收的,好好精读,坚持个一年你会发现不一样的。


额外的知识储备


前端3年前主流的前端书我都读过,什么红宝书,权威指南都读了好几遍了。


但有一本从菜鸟到高级-资深前端很推荐的一本是:JavaScript设计模式与开发实践(图灵出品)(腾讯的一位大哥写的,不是百度的那位,这两本书我都看过)


里面的知识点很干很干,里面有非常多的技巧,比如说你的同事写了一个函数,你不想破坏函数,有什么办法拓展它(其实我觉得我想的这些题就比前端八股文好玩多了,是开放性的)

  • 技巧很多,比如面向切面编程,加个before或者after函数包装一下
  • 比如责任链模式
  • 比如刚才的compose函数
  • 比如装饰器模式

确立自己的发展方向


大家其实最后都要面对一个很现实的问题,就是35以后怎么办,我个人觉得你没有对标阿里P7的能力,落地到中小公司都难。


所以我们看源码,看啥都是为了提升能力,延长职业寿命。


那么如何在短时间内有效的提升,你就需要注意不能各种方向胡乱探索,前端有小游戏方向,数据可视化方向,B端后台系统方向,音视频方向等等


我是做b端,那b端整个链路我就需要打通,组件库是我这个方向,所以我探索这里,还有node端也是,写小工具是必须的,但是你们说什么deno,其他的技术,我根本不在乎,没时间浪费在这些地方,当然除了有些业务上需要,比如之前公司有个ai标注需求,用canvas写了一个类似画板的工具,也算开拓了知识点,但这也不是我重点发展的方向,不深入。


我做组件库是为了后面的低代码,低代码平台的整体设计思路我已经想好了,整体偏向国外开源的appsmith的那种方式,然后打通组件间通信的功能,我认为是能胜任稍微复杂的b端业务场景的,而且可以走很多垂直领域,比如网站建站,微信文章编辑器这种。所以我才开始研究组件库的,因为低代码大多数复杂功能都在组件上。


工作上勇于走出舒适圈


为什么这个跟看源码相关呢,如果你做过比较复杂的项目,你会发现很多现成的第三方库满足不了。比如说我自己遇到过的大型sass项目,ant design就满足不了,所以你才发现,源码看得少加上业务急,代码就烂,时间上就留不出自己偷偷学习的时间,如果你想长期从事软件开发,没有成长是一件很危险的事(钱多当我没说,哈哈),因为无论如何,有本事,总没错的。


当你的业务难度上去的时候,会逼着你去提升能力,所以你如果想前端走的更远,建议不要在自己的舒适区太久,业务上选择一家比较难的公司,后面再跳槽就是沉淀这段时间的知识点了,当你能够有自信说,我现在带团队,从0到1再遇到那么难的业务时,能从容应对,恭喜你,你可以去面下阿里p7,不是为了这个工作啊,可以检验下是不是达到这个职位的标准了,我就喜欢偶尔面一下,也不是换工作,就是看看自己进步没


作者:孟祥_成都
链接:https://juejin.cn/post/7168671474234949662
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

读完React新文档后的8条收获

不久前,React官网进行了重构。新官网将hook作为介绍的重头戏,重新通读了一遍react文档,有了一些自己的感悟。在此分享出来与各位共勉。 1. 换个角度认识Props与State Props与State是React中两个略有相似的概念。在一个React组...
继续阅读 »

不久前,React官网进行了重构。新官网将hook作为介绍的重头戏,重新通读了一遍react文档,有了一些自己的感悟。在此分享出来与各位共勉。


1. 换个角度认识Props与State


PropsState是React中两个略有相似的概念。在一个React组件中,它们作为数据来源可能同时存在,但它们之间有着巨大不同:

  1. Props更像函数中的参数。它主要用于组件之间的信息传递,它可以是任意类型的数据:数字、文本、函数、甚至于组件等等。
  2. State更像组件中的内存。它可以保存组件内部的状态,组件通过跟踪这些状态,从而渲染更新。
import React, { useState } from 'react';

// 父组件
const ParentComponent = () => {
const [count, setCount] = useState(0); // 使用state来追踪count的值

return (
<div>
<ChildComponent age={25} />
<p>Count: {count}</p>
</div>
);
};

// 子组件
const ChildComponent = (props) => {
const { age } = props; // 使用props来获取父组件传递的数据

return (
<div>
<p>Age: {age}</p>
</div>
);
};

2. 不要嵌套定义组件


在一个组件中直接定义其他组件,可以省去很多传递Props的工夫,看上去很好。但我们不应该嵌套定义组件,原因在于**嵌套定义组件会导致渲染速度变慢,也更容易出现BUG**。
我们在嵌套定义组件的情况下,当父组件更新时,内部嵌套的子组件也会重新生成并渲染,子组件内部的state也会丢失。针对这种情况,我们可以通过两种方式来解决:

  1. 为子组件包上useMemo,避免不必要的更新;
  2. 不再嵌套定义,将子组件提至父组件外,通过Props传参。

这里更推荐第二种方法,因为这样代码更少,结构也更清晰,组件迁移维护都会更方便一些。

//🔴 Bad Case
export default function Gallery() {
function Profile() {
// ...
}
// ...
}
//✅ Good Case
function Profile() {
// ...
}

export default function Gallery() {
// ...
}

3. 尽量不要使用匿名函数组件


因为类似export default () => {}的匿名函数组件书写虽然方便,但会让debug变得更困难
如下是两种不同类型组件出错时的控制台的表现:

  1. 具名组件出错时的提示,可直接的指出错的函数组件名称: 


  1. 匿名函数出错时的提示,无法直观确定页面内哪个组件出了错: 



4. 使用逻辑运算符&&编写JSX时,左侧最好不要是数字


运算符&&在JSX中的表现与JS略有不同:

  • 在JS中,只有在&&左侧的值0nullundefinedfalse''等假值时,才会返回右侧值;
  • 在JSX中,React对于falsenullundefined并不会做渲染处理,所以进行&&运算后,如果左侧值为假被返回后,会出现与直觉不同的渲染结果:可能会碰到页面上莫名奇妙出现了一个0的问题。为了避免出现这种情况,可以在书写JSX时,为左侧的值加上!!来进行强制类型转换。
const flag = 0
//🔴 Bad Case
{
flag && <div>123</div>
}
//✅ Good Case 1
{
!!flag && <div>123</div>
}
//✅ Good Case 2
{
flag > 0 && <div>123</div>
}

关于JSX对各种常见假值的渲染,这里进行了总结:

  1. nullundefinedfalse:这些值在JSX中会被视为空,不会渲染任何内容。它们会被忽略,并且不会生成对应的DOM元素。
  2. 0NaN:这些值会在页面上渲染为字符串"0"、"NaN"。
  3. ''[]{}:空字符串会被渲染为什么都没有,不会显示任何内容。


注:这里感谢@小明家的bin的评论提醒,他的见解对我起到了很大的启发作用。



5.全写的 Fragment标签上可以添加属性key


在map循环添加组件时,需要为组件添加key来优化性能。如果待循环的组件有多个时,可以在外层包裹<Fragment>...</Fragment>标签,在其上添加key添加在<Fragment>...</Fragment>来避免创建额外的组件。

const list = [1,2,3]
//🔴 Bad Case
//不能添加key
{
list.map(v=><> <div>1-1</div> <div>1-2</div> </>)
}
//🔴 Bad Case
//创建了额外的div节点
{
list.map(v=><div key={v}> <div>1-1</div> <div>1-2</div> <div/>)
}
//✅ Good Case
{
list.map(v=><Fragment key={v}> <div>1-1</div> <div>1-2</div> </Fragment>)
}



注意简写的Fragment标签<>...</>上不支持添加key



6. 可以使用updater function,来在下一次渲染之前多次更新同一个state


React中处理状态更新时采用了批处理机制,在下一次渲染之前多次更新同一个state,可能不会达到我们想要的效果。如下是一个简单例子:

// 按照直觉一次点击后button中的文字应展示为3,但实际是1
function Demo(){
const [a,setA] = useState(0)

function handler(){
setA(a + 1);
setA(a + 1);
setA(a + 1);
}

return <button onclick={handler}>{a}</button>
}

在某些极端场景下,我们可能希望state的值能够及时发生变化,这时便可以采用setA(n => n + 1)(这种形式被称为updater function)来进行更新:

// 一次点击后a的值会被更新为3
function Demo(){
const [a,setA] = useState(0)

function handler(){
setA(n => n + 1);
setA(n => n + 1);
setA(n => n + 1);
}

return <button onclick={handler}>{a}</button>
}

7. 管理状态的一些原则


更加结构化的状态有助于提高我们组件的健壮性,以下是一些管理组件内状态时可以参考的原则:

  1. 精简相关状态:如果在更新某个状态时总是需要同步更新其他状态变量,可以考虑将它们合并为一个状态变量。
  2. 避免矛盾状态:避免出现两个或多个状态的值互相矛盾的情况。
  3. 避免冗余状态:一个状态可以由其余状态计算而来时,其不应单独作为一个状态来进行管理。
  4. 避免状态重复:当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。尽可能减少重复。
  5. 避免深度嵌套状态:嵌套层级过深的状态更新时相对麻烦许多,尽量保持状态扁平化。

8. 使用useSyncExternalStore订阅外部状态


useSyncExternalStore是一个React18中新提供的一个Hook,可以用于订阅外部状态。
它的使用方式如下:

import { useSyncExternalStore } from 'react';

function MyComponent() {
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
// ...
}

useSyncExternalStore接受三个参数:

  • subscribe:一个订阅函数,用于订阅存储,并返回一个取消订阅的函数。
  • getSnapshot:一个从存储中获取数据快照的函数。在存储未发生变化时,重复调用getSnapshot应该返回相同的值。当存储发生变化且返回值不同时,React会重新渲染组件。
  • getServerSnapshot(可选):一个在服务器端渲染时获取存储初始快照的函数。它仅在服务器端渲染和在客户端进行服务器呈现内容的hydration过程中使用。如果省略该参数,在服务器端渲染组件时会抛出错误。

举一个具体的例子,我们可能需要获取浏览器的联网状态,并期望在浏览器网络状态发生变化时做一些处理。使用useSyncExternalStore可以很大程度上简化我们的代码,同时使逻辑更加清晰:

//🔴 Bad Case
function useOnlineStatus() {
// Not ideal: Manual store subscription in an Effect
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}

updateState();

window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}

// ✅ GoodCase
function useOnlineStatus() {
return useSyncExternalStore(
subscribe, // React won't resubscribe for as long as you pass the same function
() => navigator.onLine, // How to get the value on the client
() => true // How to get the value on the server
);
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}

结语


文章的最后,再来一次无废话总结:

  1. 更清晰地认识了Props与State之间的区别。Props更像是函数的参数,用于组件之间的信息传递;而State更像是组件内部的内存,用于保存组件的状态并进行渲染更新。
  2. 不推荐在一个组件内部嵌套定义其他组件,因为这样会导致渲染速度变慢并容易产生BUG。推荐将子组件提到父组件外部并通过Props传递数据。
  3. 尽量避免使用匿名函数组件,因为在出错时会增加调试的难度。具名组件的出错提示更加直观和准确。
  4. 在使用逻辑运算符&&编写JSX时,左侧最好不要是数字。在JSX中,0会被当作有效的值,而不是假值,为了避免出现问题,可以在左侧的值加上!!进行强制类型转换。
  5. 当在使用全写的Fragment标签时,可以给Fragment标签添加属性key,以优化性能和避免创建额外的组件。
  6. 使用updater function的方式进行状态更新,可以确保在下一次渲染之前多次更新同一个state。这样可以避免批处理机制带来的问题。
  7. 在管理组件内状态时,可以遵循一些原则,如精简相关状态、避免矛盾状态、避免冗余状态、避免状态重复等,以提高组件的健壮性和可维护性。
  8. 使用useSyncExternalStore可以订阅外部状态,它是React 18中新增的Hook。通过订阅函数、获取数据快照的函数以及获取服务器初始快照的函数,我们可以简化订阅外部状态的代码逻辑。

通过对React文档的深入学习和实践,我对React的理解更加深入了解,希望这些收获也能对大家在学习和使用React时有所帮助。


作者:星始流年
链接:https://juejin.cn/post/7233324859932442680
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

不要因bundle size太大责备开发者

前言 大家好我是飞叶,一个前端UP主,最近外网有个新的SSR框架Qwik比较火,GitHub上它的仓库star数以每天100的速度在增长,今天就通过翻译一篇文章来引入QWIK。 当然如果你不想读这些,我也录了一个视频讲这篇文章。讲解视频在这里 原文链接 ht...
继续阅读 »

前言


大家好我是飞叶,一个前端UP主,最近外网有个新的SSR框架Qwik比较火,GitHub上它的仓库star数以每天100的速度在增长,今天就通过翻译一篇文章来引入QWIK。


当然如果你不想读这些,我也录了一个视频讲这篇文章。讲解视频在这里



原文链接 http://www.builder.io/blog/dont-b…



不要因bundle size太大责备开发者


让我们谈谈我们构建 Web 应用程序所必须使用的工具,以及这些工具如何欺骗我们。


开发者们共同的故事


你要创建一个新项目,此时,你信心满满这个新站点会很快很流畅。
在一开始,事情看起来确实如此,但是很快你的应用就变大了变复杂了,应用的开启性能变慢了。
在不知不觉中,您手头上有一个巨大的应用程序,而您却无能为力地修复它。你哪里做错了?


我们用的每个工具/框架都承诺提供更好、更快的结果,
但我们通过访问整个互联网里的应用就知道,结果绝不是一个更好、更快的站点。
谁应该为此负责?开发者吗?


作为开发者,你是否有被告知:"就是你们搞砸了,你们偷工减料,才导致了一个性能差的站点。"


这不是你的错


如果一些网站速度较慢,而另一些网站速度较快,那么当然,责备开发人员可能是有道理的。
但真实情况是:所有网站都很慢!
当没有人成功时,你怎么能责怪开发者呢?问题是系统性的。也许这不是开发者的错。


这是一个关于我们如何构建应用程序、我们使用的工具、工具做出的承诺以及我们最终遇到的缓慢站点的故事。


只有一个结论。这些工具都过度承诺了,这是整个行业的系统性问题。这不仅仅是几个坏苹果,而是整个万维网。


代码太多了


我们都知道问题是什么:代码太多!我们非常擅长创建代码,浏览器无法跟上我们的脚步。
每个人都告诉你你的网站有太多的 JavaScript,但没有人知道如何缩小它。


这就像一个 YouTube 健身频道告诉你减肥所需要做的就是少摄入卡路里。
简单的建议,但成功率令人沮丧。
原因是该建议忽略了食欲。
当你又饿又虚弱并且只想到食物时,又有几个人能做到 减少卡路里摄入的意愿呢?
所以也许减肥成功的秘诀可能不是减少卡路里,而是如何控制你的食欲。


这个例子很类似于 JavaScript 膨胀的情况。
我们知道我们需要更少的 JavaScript,
但是我们有太多需求,除了代码要写,还有太多工具和轮子要用,(才能满足需求)
但是所有这些 代码 和工具 都会源源不断地使我们的应用越来越大。




打包的演变历史


让我们先看看我们是如何陷入这种境地的,然后再讨论前进的道路。


第 0 代:串联


在 ECMAScript 模块之前,什么都没有,只是文件。
打包过程很简单。这些文件被连接在一起并包装在 IIFE 中。


好处是很难向您的应用程序添加更多代码,因此bundle size保持较小。


第 1 代:打包器


ECMAScript 模块来了。
打包器也出现了:WebPack、Rollup 等。


然而,npm install 一个依赖并把它打包进去有点太容易了。很快,bundle size就成了一个问题。


庆幸的是,这些打包器知道如何进行tree shaking和死代码消除。这些功能确保只有用到的的代码才被打包。


第 2 代:延迟加载


意识到bundle size过大的问题, 打包器开始提供延迟加载。
延迟加载很棒,因为它允许将代码分解成许多chunks并根据需要交付给浏览器。
这很棒,因为它允许从最需要的部分开始 分批交付应用程序。


问题在于,在实践中,我们是使用框架来构建应用程序的,而框架对打包程序如何将我们的代码分解为延迟加载的块有很大影响。
问题在于延迟加载块需要引入异步API调用。
如果框架需要对您的代码进行同步引用,则打包器不能引入延迟加载的块。


所以我们需要明白,虽然打包器声称他们可以延迟加载代码,而且这也是真的,
但想做到延迟加载有个前提条件,即我们使用的框架得让开发者使用promise(来懒加载chunk),否则您可能没有太多选择。


第 3 代:延迟加载不在渲染树中的组件


框架迅速争先恐后地利用打包器的延迟加载功能,如今几乎所有人都知道如何进行延迟加载。
但是有一个很大的警告!框架只能延迟加载不在当前渲染树中的组件。




什么是渲染树?它是构成当前页面的一组组件。
应用程序通常具有比当前页面上更多的组件。
通常,渲染树包含视图(这是您当前在浏览器视口中看到的内容)内组件。
和一部分视图之外的组件。


假设一个组件在渲染树中。在这种情况下,框架必须下载组件,因为框架需要重建组件的渲染树,(这是hydration的一部分工作)。
框架只能延迟加载当前不在渲染树中的组件。


另一点是框架可以延迟加载组件,但总是包含行为。
因为组件包含了行为,这个懒加载的单位就太大了。如果可以延迟加载的单位更小会更好。
渲染组件不应要求下载组件的事件处理程序。
框架应该只在用户交互时才下载事件处理程序,而不是作为组件渲染方法的一部分。根
据您正在构建的应用程序的类型,事件处理程序可能代表您的大部分代码。
所以耦合组件的渲染和行为的下载是次优的。


问题的核心


仅在需要重新渲染组件时才延迟加载组件渲染函数,并且仅在用户与事件处理程序交互时才延迟加载事件处理程序。
这样才是最好的!
默认应该是所有内容都是延迟加载的。


但这种方法存在一个大问题。问题是框架需要协调其内部状态与 DOM。
这意味着至少需要一次hydration,来进行完整渲染以重建框架的内部状态。
在第一次渲染之后,框架可以对其更新进行更准确的把控,但问题已经产生了,因为代码已经下载了。所以我们有两个问题:

  • 框架需要下载并执行组件以在启动时重建渲染树。(请参阅hydration 是纯粹的开销)这会强制下载和执行渲染树中的所有组件。
  • 事件处理程序随组件一起提供,即使在渲染时不需要它们。包含事件处理程序会强制下载不必要的代码。

因此,当今框架的现状是,必须急切地下载和执行 SSR/SSG 渲染树中的每个组件(及其处理程序)。
使用当今的框架进行延迟加载有点说谎,因为您并不能在初始页面呈现时进行延迟加载。


值得指出的是,即使开发人员将延迟加载边界引入 SSR/SSG 初始页面,也无济于事。
框架仍需下载并执行 SSR/SSG 响应中的所有组件;因此,只要组件在渲染树中,框架就必须急切地加载开发人员试图延迟加载的组件。


渲染树中组件的急切下载是问题的核心,开发人员对此无能为力。
尽管如此,这并不能阻止开发人员因网站运行缓慢而受到指责。


下一代:细粒度的延迟加载


那么,我们该何去何从?显而易见的答案是我们需要更细粒度。该解决方案既明显又难以实施。我们需要:

  • 更改框架,这样它们就不会在hydration阶段急切地加载渲染树。
  • 允许组件渲染函数 独立于组件事件处理程序 单独下载。

如果您的框架可以完成上述两个部分,那么用户将看到巨大的好处。
应用程序的启动要求很少,因为启动时不需要进行渲染(内容已经在 SSR/SSG 处渲染)。
下载的代码更少:当框架确定需要重新渲染特定组件时,框架可以通过下载渲染函数来实现,而无需下载所有事件处理程序。


细粒度的延迟加载将是网站启动性能的巨大胜利。
它要快得多,因为下载的代码量将与用户交互性成正比,而不是与初始渲染树的复杂性成正比。
您的网站会变得更快,不是因为我们更擅长使代码更小,而是因为我们更擅长只下载我们需要的东西,而不是预先下载所有东西。




入口点 entry point


拥有一个可以进行细粒度延迟加载的框架是不够的。
因为,要利用细粒度的延迟加载,您必须首先拥有要延迟加载的bundles。


为了让打包器创建延迟加载的chunk,打包器需要每个块的入口点。
如果您的应用程序只有单个入口点,则打包器无法创建多个chunks。
如果您的应用程序只有单个入口点,即使你的框架可以进行细粒度的延迟加载,它也没有什么可以延迟加载的。


现在创建入口点很麻烦,因为它需要开发人员编写额外的代码。
在开发应用程序时,我们真的只能考虑一件事,那就是写功能。
让开发人员同时考虑他们正在构建的功能和延迟加载对开发人员来说是不公平的。
所以在实践中,为打包器创建入口点很麻烦。


所需要的是一个无需开发人员考虑就可以创建入口点的框架。
为打包程序创建入口点是框架的责任,而不是开发人员的责任。
开发人员的职责是构建功能。
该框架的职责是考虑应该如何完成该功能的底层实现。
如果框架不这样做,那么它就不能完全满足开发人员的需求。


担心切入点太多?


目标应该是创建尽可能多的入口点。
但是,有些人可能会问,这是不是就会导致下载很多小块而不是几个大块吗?答案是响亮的“不”。


如果没有入口点,打包器就无法创建chunk。
但是打包器可以将多个入口点放入一个chunk中。
您拥有的入口点越多,您以最佳方式组装bundle的自由度就越大。
入口点给了你优化bundle的自由。所以它们越多越好。


未来的框架


下一代框架将需要解决这些问题:

  • 拥有人们喜欢的开发体验DX。
  • 对代码进行细粒度的延迟加载。
  • 自动生成大量入口点以支持细粒度的延迟加载。

开发人员将像现在一样构建他们的网站,但这些网站不会在应用程序启动时用下载和执行一个很大的bundle来压倒浏览器。


Qwik是一个在设计时考虑到这些原则的框架。Qwik细粒度延迟加载是针对每个事件处理程序、渲染函数和effect的。


结论


我们的网站越来越大,看不到尽头。
它们之所以大,是因为这些网站今天比以前做得更多——更多的功能、动画等。并且这种趋势将继续下去。


上述问题的解决方案是对代码进行细粒度的延迟加载,这样浏览器就不会在初始页面加载时不堪重负。


我们的打包工具支持细粒度的延迟加载,但我们的框架不支持。
框架hydration强制渲染树中的所有组件在hydration时加载。(目前的SSR框架唯一的延迟加载是 当前不在渲染树中的组件。)
即使事件处理程序可能是代码的大部分,并且hydration并不需要事件处理器代码,现在的SSR框架还是随组件的下载一并下载了事件处理程序.


因为打包器可以细粒度的延迟加载,但我们的框架不能,我们无法识别其中的微妙之处。
导致的结果就是我们将网站启动缓慢归咎于开发人员,
因为我们错误地认为他们本可以采取一些措施来防止这种情况发生,尽管现实是他们在这件事上几乎没有发言权。


我们需要将细粒度延迟加载设计为框架核心功能的新型框架(例如Qwik )。
我们不能指望开发者承担这个责任;他们已经被各种功能淹没了。
框架需要考虑延迟加载运行时以及创建入口点,以便打包程序可以创建块以进行延迟加载。
下一代框架带来的好处将超过迁移到它们所花费的成本。


作者:飞叶_前端
链接:https://juejin.cn/post/7154948728871190535
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么别人的 hooks 里面有那么多的 ref

前言 最近因为一些原因对useCallback有了一些新的认知,然后遇到一个朋友说他面试的时候被问到如何实现一个倒计时的hooks的时候,想到了这个问题,突然的茅塞顿开,对react 中的hooks有了新的了解,所以有了这篇文章,记录一下我个人的想法。 在...
继续阅读 »

前言



最近因为一些原因对useCallback有了一些新的认知,然后遇到一个朋友说他面试的时候被问到如何实现一个倒计时的hooks的时候,想到了这个问题,突然的茅塞顿开,对react 中的hooks有了新的了解,所以有了这篇文章,记录一下我个人的想法。



在学习一些开源的库的时候,很容易发现开源库中 hooks 里面会写很多的 ref 来存储hooks的参数。


使用了 ref 之后,使用变量的地方就需要 .current 才能拿到变量的值,这比我直接使用变量肯定是变得麻烦了。对于有代码洁癖的人来说,这肯定是很别扭的。


但是在开源库的 hooks 中频繁的使用了 ref,这肯定不是一个毫无原因的点,那么究竟是什么原因,让开源库也不得不使用 .current 去获取变量呢?


useCallback


先跑个题,什么时候我们需要使用 useCallback 呢?


每个人肯定有每个人心中的答案,我来讲讲我的心路历程吧。


第一阶段-这是个啥


刚开始学react的时候,写函数式组件,我们定义函数的时候,肯定是不会有意识的把这个函数使用 useCallback 包裹。就这样写了一段时间的代码。


突然有一天我们遇到了一个问题,useEffect无限调用,找了半天原因,原来是因为我们还不是很清楚useEffect依赖的概念,把使用到的所有的变量一股脑的塞到了依赖数组里面,碰巧,我们这次的依赖数组里面有一个函数,在react每一次渲染的时候,函数都被重新创建了,导致我们的依赖每一次都是新的,然后就触发了无限调用。


百度了一圈,原来使用 useCallback 缓存一下这个函数就可以了,这样useEffect中的依赖就不会每一次都是一个新值了。


小总结: 在这个阶段,我们第一次使用 useCallback ,了解到了它可以缓存一个函数。


第二阶段-可以缓存


可以缓存就遇到了两个点:

  1. 缓存是吧,不会每一次都重新创建是吧,这样是不是性能就能提高了!那我把我所有用到的函数都使用 useCallback缓存一下。
  2. react 每一次render的时候会导致子组件重新渲染,使用memo可以缓存这个子组件,在父组件更新的时候,会浅层的比较子组件的props,所以传给子组件的函数就需要使用缓存useCallback起来,那么父组件中定义函数的时候图方便,一股脑的都使用 useCallback缓存。

小总结: 在这里我们错误的认为了缓存就能够帮助我们做一些性能优化的事情,但是因为还不清楚根本的原因,导致我们很容易就滥用 useCallback


第三阶段-缓存不一定是好事


在这个阶段,写react也有一段时间了,我们了解到处处缓存其实还不如不缓存,因为缓存的开销不一定就比每一次重新创建函数的开销要小。


在这里肯定也是看了很多介绍 useCallback的文章了,推荐一下下面的文章


how-to-use-memo-use-callback,这个是全英文的,掘金有翻译这篇文章的,「好文翻译」


小总结: 到这里我们就大概的意识到了,处处使用useCallback可能并不是我们想象的那样,对正确的使用useCallback有了一定的了解


总结


那么究竟在何时应该使用useCallback呢?

  1. 我们知道 react 在父组件更新的时候,会对子组件进行全量的更新,我们可以使用 memo对子组件进行缓存,在更新的时候浅层的比较一下props,如果props没有变化,就不会更新子组件,那如果props中有函数,我们就需要使用 useCallback缓存一下这个父组件传给子组件的函数。
  2. 我们的useEffect中可能会有依赖函数的场景,这个时候就需要使用useCallback缓存一下函数,避免useEffect的无限调用

是不是就这两点呢?那肯定不是呀,不然就和我这篇文章的标题联系不起来了吗。


针对useEffect这个hooks补充一点react官方文档里面有提到,建议我们使用自定义的 hooks 封装 useEffect

  • 那使用useCallback的第三个场景就出现了,就是我们在自定义hooks需要返回函数的时候,建议使用 useCallback缓存一下,因为我们不知道用户拿我们返回的函数去干什么,万一他给加到他的useEffect的依赖里面不就出问题了嘛。

一个自定义hook的案例



实现一个倒计时 hooks



需求介绍


我们先简单的实现一个倒计时的功能,就模仿我们常见的发短息验证码的功能。页面效果




app.jsx




MessageBtn.jsx



 功能比较简单,按钮点击的时候创建了一个定时器,然后时间到了就清除这个定时器。


现在把 MessageBtn 中倒计时的逻辑写到一个自定义的hooks里面。


useCountdown


把上面的一些逻辑抽取一下,useCountdown主要接受一个倒计时的时长,返回当前时间的状态,以及一个开始倒计时的函数




这里的start函数用了useCallback,因为我们不能保证用户的使用场景会不会出问题,所以我们包一下


升级 useCountdown


现在我们期望useCountdown支持两个函数,一个是在倒计时的时候调用,一个是在倒计时结束的时候调用


预期的使用是这样的,通过一个配置对象传入 countdownCallBack函数和onEnd




改造 useCountdown

  • 然后我们这里count定义 0 有点歧义,0 不能准确的知道是一开始的 0 还是倒计时结束的 0,所以还需要加一个标志位来表示当前是结束的 0
    1. 使用 useEffect监听count的变化,变化的时候触发对应的方法

    实现如下, 新增了红框的内容




    提出问题


    那么,现在就有一个很严重的问题,onEndcountdownCallBack这两个函数是外部传入的,我们要不要把他放到我们自定义hookuseEffect依赖项里面呢


    我们不能保证外部传入的变量一定是一个被useCallback包裹的函数,那么肯定就不能放到useEffect依赖项里面。


    如何解决这个问题呢?


    答案就是使用useRef。(兜兜转转这么久才点题 (╥╯^╰╥))


    用之前我们可以看一下成熟的方案是什么


    比如ahooks里面的useLatestuseMemoizedFn的实现

    • useLatest 源码


    • useMemoizedFn 源码,主要看圈起来的地方就好了,本质也是用useRef记录传入的内容



    ok,我们使用一下 useLatest 改造一下我们的useCountdown,变动点被红框圈起来了




    总结


    其实这篇文章的核心点有两个

    1. 带着大家重新的学习了一下useCallback的使用场景。(useMemo类似)
    2. 编写自定义hooks时候,我们需要注意一下外部传入的参数,以及我们返回给用户的返回值,核心点是决不相信外部传入的内容,以及绝对要给用户一个可靠的返回值。

    作者:既见君子
    链接:https://juejin.cn/post/7271643757640007680
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    移动端的双击事件好不好用?

    web
    前言 2023年了,我不允许还有人不会自己实现移动端的双击事件。 过来,看这里,不足 50 行的代码实现的双击事件。 听笔者娓娓道来。 dblclick js原生有个dblclick双击事件,但是几乎不支持移动端。 而且,该dblclick事件在pc端鼠标双...
    继续阅读 »

    前言


    2023年了,我不允许还有人不会自己实现移动端的双击事件。


    过来,看这里,不足 50 行的代码实现的双击事件。


    听笔者娓娓道来。


    dblclick


    js原生有个dblclick双击事件,但是几乎不支持移动端。


    developer.mozilla.org_zh-CN_docs_Web_API_Element_dblclick_event.png


    而且,该dblclick事件在pc端鼠标双击时,会触发两次click与一次dblclick


    window.addEventListener('click', () => {
    console.log('click')
    });
    window.addEventListener('dblclick', () => {
    console.log('dblclick')
    });

    // 双击页面,打印:click✖️2 dblclick

    我们期望可以在移动端也能有双击事件,并且隔离单击与双击事件,双击时只触发双击事件,只执行双击回调函数,让注册双击事件像注册原生事件一样简单。


    点击穿透


    简单聊聊移动端的点击穿透。



    在移动端单击会依次触发touchstart->touchmove->touchend->click事件。



    有这样一段逻辑,在touchstart时出现全屏弹框,在click弹框时关闭弹框。实际上,在点击页面时,弹框会一闪而过,并没有出现正确的交互。在移动端单击时touchstart早于click,当弹框出现了,后来的click事件就落在了弹框上,导致弹框被关闭。这就是点击穿透的一种表现。


    笔者的业务需求是双击元素,出现全屏弹框,单击弹框时关闭弹框。因此基于这样的业务需求与现实的点击穿透问题,笔者选择采用click事件来模拟双击事件,并且适配pc端使用。大家也可以选择解决点击穿透问题,并采用touchstart模拟双击事件,可以更快地响应用户操作。



    采用touchstart模拟时,可以再考虑排除双指点击的情况。


    在实现上与下文代码除了事件对象获取位置属性有所不同外,其它代码基本一致,实现思路无差别。



    模拟双击事件


    采用click事件来模拟实现双击。


    双击事件定义:2次点击事件间隔小于200ms,并且点击范围小于10px的视为双击。这里的双击事件是自定义事件,为了区分原生的 dblclick,又优先满足移动端使用,则事件名定义为 dbltouch,后续可以使用window.addEventListener('dbltouch', ()=>{})来监听双击事件。



    这个间隔与位移限制大家可以根据自己的业务需求调整。通常采用的是300ms的间隔与10px的位移,笔者业务中发现200ms间隔也可使用。


    自定义事件名大家可以随意设置,满足语义化即可。





    1. 监听click事件,并在捕获阶段监听,目的是为了后续能够阻止click事件传播。


      window.addEventListener('click', handler, true);



    2. 监听函数中,第1次点击时,记录点击位置,并设置200ms倒计时。如果第2次点击在200ms后,则重新派发当前事件,让事件继续传播,使其它的监听函数可以继续处理对应事件。


      // 标识是否在等待第2次点击
      let isWaiting = false;

      // 记录点击位置
      let prevPosition = {};

      function handler(evt) {
      const { pageX, pageY } = evt;
      prevPostion = { pageX, pageY };
      // 阻止冒泡,不让事件继续传播
      evt.stopPropagation();
      // 开始等待第2次点击
      isWaiting = true;
      // 设置200ms倒计时,200ms后重新派发当前事件
      timer = setTimeout(() => {
      isWaiting = false;
      evt.target.dispatchEvent(evt);
      }, 200)
      }

      注意: 倒计时结束时evt.target.dispatchEvent(evt)派发的事件仍是原来的事件对象,即仍是click事件,会触发继续handler函数,进入了循环。


      这里需要破局,已知Event事件对象下有一个 isTrusted 属性,是一个只读属性,是一个布尔值。当事件是由用户行为生成的时候,这个属性的值为 true ,而当事件是由脚本创建、修改、通过 EventTarget.dispatchEvent()派发的时候,这个属性的值为 false 。


      因此,此处脚本派发的事件是希望继续传递的事件,不用handler内处理。


      function handler(evt) {
      // 如果事件是脚本派发的则不处理,将该事件继续传播
      if(!evt.isTrusted){
      return;
      }
      }



    3. 处理完第1次点击后,接着处理在200ms内的第2次点击事件。如果满足位移小于10px的条件,则视为双击。


      // 标识是否在等待第2次点击
      let isWaiting = false;

      // 记录点击位置
      const prevPosition = {};

      function handler(evt) {
      // 如果事件是脚本派发的则不处理,将该事件继续传播
      if(!evt.isTrusted){
      return;
      }
      const { pageX, pageY } = evt;
      if(isWaiting) {
      isWaiting = false;
      const diffX = Math.abs(pageX - prevPosition.pageX);
      const diffY = Math.abs(pageY - prevPosition.pageY);
      // 如果满足位移小于10,则是双击
      if(diffX <= 10 && diffY <= 10) {
      // 取消当前事件传递,并派发1个自定义双击事件
      evt.stopPropagation();
      evt.target.dispatchEvent(
      new PointerEvent('dbltouch', {
      cancelable: false,
      bubbles: true,
      })
      )
      }
      } else {
      prevPostion = { pageX, pageY };
      // 阻止冒泡,不让事件继续传播
      evt.stopPropagation();
      // 开始等待第2次点击
      isWaiting = true;
      // 设置200ms倒计时,200ms后重新派发当前事件
      timer = setTimeout(() => {
      isWaiting = false;
      evt.target.dispatchEvent(evt);
      }, 200)
      }
      }



    4. 以上便实现了双击事件,全局任意地方监听双击。


      window.addEventListener('dbltouch', () => {
      console.log('dbltouch');
      })
      window.addEventListener('click', () => {
      console.log('click');
      })
      // 使用鼠标、手指双击
      // 打印出 dbltouch
      // 而且不会打印有click



    笔者要在这里说句 但是: 由于200ms的延时,虽不多,但是对于操作迅速的用户来讲,还是会有不好的体验。


    优化双击事件


    由于是在window上注册的click函数,虽说注册双击事件像单击事件一样简单了,但却也导致整个产品页面的click事件都会推迟200ms执行。


    因此,我们应该只对需要处理双击的地方添加双击事件,至少只在局部发生延迟情况。稍微调整下代码,将需要注册双击事件的元素由开发决定,通过参数传递。而且事件处理函数也可以通过参数传递,即可以通过监听双击事件,也可以通过回调函数执行。


    以下是完整的代码。


    class RegisterDbltouchEvent {
    constructor(el, fn) {
    this.el = el || window;
    this.callback = fn;
    this.timer = null;
    this.prevPosition = {};
    this.isWaiting = false;

    // 注册click事件,注意this指向
    this.el.addEventListener('click', this.handleClick.bind(this), true);
    }
    handleClick(evt){
    if(this.timer) {
    clearTimeout(this.timer);
    this.timer = null;
    }
    if(!evt.isTrusted) {
    return;
    };
    if(this.isWaiting){
    this.isWaiting = false;
    const diffX = Math.abs(pageX - this.prevPosition.pageX);
    const diffY = Math.abs(pageY - this.prevPosition.pageY);
    // 如果满足位移小于10,则是双击
    if(diffX <= 10 && diffY <= 10) {
    // 取消当前事件传递,并派发1个自定义双击事件
    evt.stopPropagation();
    evt.target.dispatchEvent(
    new PointerEvent('dbltouch', {
    cancelable: false,
    bubbles: true,
    })
    );
    // 也可以采用回调函数的方式
    this.callback && this.callback(evt);
    }
    } else {
    this.prevPostion = { pageX, pageY };
    // 阻止冒泡,不让事件继续传播
    evt.stopPropagation();
    // 开始等待第2次点击
    this.isWaiting = true;
    // 设置200ms倒计时,200ms后重新派发当前事件
    this.timer = setTimeout(() => {
    this.isWaiting = false;
    evt.target.dispatchEvent(evt);
    }, 200)
    }
    }
    }

    只为需要实现双击逻辑的元素注册双击事件。可以通过传递回调函数的方式执行业务逻辑,也可以通过监听dbltouch事件的方式,也可以同时使用,it's up to you.


    const el = document.querySelector('#dbltouch');
    new RegisterDbltouchEvent(el, (evt) => {
    // 实现双击逻辑
    })

    最后


    采用的click事件模拟双击事件,因此在移动端和pc端都可以使用该构造函数。


    作者:Yue栎廷
    来源:juejin.cn/post/7274043371731796003
    收起阅读 »

    为什么我的页面鼠标一滑过,布局就错乱了?

    web
    前言 这天刚到公司,测试同事又在群里@我: 为什么页面鼠标一滑过,布局就错乱了? 以前是正常的啊? 刷新后也是一样 快看看怎么回事 同时还给发了一段bug复现视频,我本地跑个例子模拟下 可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。...
    继续阅读 »

    前言


    这天刚到公司,测试同事又在群里@我:

    为什么页面鼠标一滑过,布局就错乱了?

    以前是正常的啊?

    刷新后也是一样

    快看看怎么回事


    同时还给发了一段bug复现视频,我本地跑个例子模拟下


    GIF 2023-8-28 11-23-25.gif


    可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。


    正文


    首先说下我们的产品逻辑,我们产品的需求是:要滚动的页面,滚动条不应该占据空间,而是悬浮在滚动页面上面,而且滚动条只在滚动的时候展示。


    我们的代码是这样写:


      <style>
    .box {
    width: 630px;
    display: flex;
    flex-wrap: wrap;
    overflow: hidden; /* 注意⚠️ */
    height: 50vh;
    box-shadow: 0 0 5px rgba(93, 96, 93, 0.5);
    }
    .box:hover {
    overflow: overlay; /* 注意⚠️ */
    }
    .box .item {
    width: 200px;
    height: 200px;
    margin-right: 10px;
    margin-bottom: 10px;
    }
    img {
    width: 100%;
    height: 100%;
    }
    </style>
    <div class="box">
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    </div>

    我们使用了overflow属性的overlay属性,滚动条不会占据空间,而是悬浮在页面之上,刚好吻合了产品的需求。


    image.png


    然后我们在滚动的区域默认是overflow:hidden,正常时候不会产生滚动条,hover的时候把overflow改成overlay,滚动区域可以滚动,并且不占据空间。


    简写代码如下:


      .box {
    overflow: hidden;
    }
    .box:hover {
    overflow: overlay;
    }

    然后我们是把它封装成sass的mixins,滚动区域使用这个mixins即可。


    上线后没什么问题,符合预期,获得产品们的一致好评。


    直接这次bug的出现。


    排查


    我先看看我本地是不是正常的,我打开一看,咦,我的布局不会错乱。


    然后我看了我的chrome的版本,是113版本


    然后我问了测试的chrome版本,她是114版本


    然后我把我的chrome升级到最新114版本,咦,滑过页面之后我的布局也乱了。


    初步判断,那就有可能是chrome版本的问题。


    去网上看看chrome的升级日志,看看有没有什么信息。


    image.png


    具体说明:


    image.png


    可以看到chrome114有了一个升级,就是把overflow:overlay当作overflow:auto的别名,也就是说,overlay的表现形式和auto是一致的。


    实锤了,auto是占据空间的,所以导致鼠标一滑过,布局就乱了。


    其实我们从mdn上也能看到,它旁边有个删除按钮,然后滑过有个tips:


    image.png


    解决方案


    第一种方式


    既然overflow:overlay表现形式和auto一致,那么我们得事先预留出滚动条的位置,然后设置滚动条的颜色是透明的,滑过才显示颜色,基本上也能符合产品的需求。


    代码如下:


      // 滚动条
    ::-webkit-scrollbar {
    background: transparent;
    width: 6px;
    height: 6px;
    }
    // 滚动条上的块
    ::-webkit-scrollbar-thumb {
    background-clip: padding-box;
    background-color: #d6d6d6;
    border: 1px solid transparent;
    border-radius: 10px;
    }
    .box {
    overflow: auto;
    }
    .box::-webkit-scrollbar-thumb {
    background-color: transparent;
    }
    .box:hover::-webkit-scrollbar-thumb {
    background-color: #d6d6d6;
    }

    第二种方式


    如果有用element-ui,它内部封装了el-scrollbar组件,模拟原生的滚动条,这个也不占据空间,而是悬浮,并且默认不显示,滑过才显示。



    element-ui没有el-scrollbar组件的文档说明,element-plus才有,不过都可以用。



    总结


    这次算是踩了一个坑,当mdn上面提示有些属性已经要废弃⚠️了,我们就要小心了,尽量不要使用这些属性,就算当前浏览器还是支持的,但是不能保证未来某个时候浏览器会不会废弃这个属性。(比如这次问题,改这些问题挺费时间的,因为用的地方挺多的。)


    因为一旦这些属性被废弃了,它预留的bug就等着你去解决,谨记!


    作者:答案cp3
    来源:juejin.cn/post/7273875079658209319
    收起阅读 »

    JS 获取页面尺寸

    web
    通过 JS 获取页面相关的尺寸是比较常见的操作,尤其是在动态计算页面布局时,今天我们就来学习一下几个获取页面尺寸的基本方法。 获取页面高度 function getPageHeight() { var g = document, a = g.bod...
    继续阅读 »

    通过 JS 获取页面相关的尺寸是比较常见的操作,尤其是在动态计算页面布局时,今天我们就来学习一下几个获取页面尺寸的基本方法。


    获取页面高度


    function getPageHeight() {
    var g = document,
    a = g.body,
    f = g.documentElement,
    d = g.compatMode == "BackCompat" ? a : g.documentElement;
    return Math.max(f.scrollHeight, a.scrollHeight, d.clientHeight);
    }

    获取页面scrollLeft


    function getPageScrollLeft() {
    var a = document;
    return a.documentElement.scrollLeft || a.body.scrollLeft;
    }

    获取页面scrollTop


    function getPageScrollTop() {
    var a = document;
    return a.documentElement.scrollTop || a.body.scrollTop;
    }

    获取页面可视宽度


    function getPageViewWidth() {
    var d = document,
    a = d.compatMode == "BackCompat" ? d.body : d.documentElement;
    return a.clientWidth;
    }

    获取页面可视高度


    function getPageViewHeight() {
    var d = document,
    a = d.compatMode == "BackCompat" ? d.body : d.documentElement;
    return a.clientHeight;
    }

    获取页面宽度


    function getPageWidth() {
    var g = document,
    a = g.body,
    f = g.documentElement,
    d = g.compatMode == "BackCompat" ? a : g.documentElement;
    return Math.max(f.scrollWidth, a.scrollWidth, d.clientWidth);
    }

    ~


    ~ 全文完


    ~


    作者:编程三昧
    来源:juejin.cn/post/7274856158175363126
    收起阅读 »

    一个有意思的点子

    web
    前言 前端基于运行时检测问题、定位原因、预警提示的可行性思考🤔 背景 部门要治理线上故障过多的问题,故障主要分后端异常以及前端的性能问题和业务问题。我们讨论的范围是前端业务故障,经过分析可以把它们大致分为UI、业务、工程技术、日志、三方依赖等。 首先要确定...
    继续阅读 »

    前言


    前端基于运行时检测问题、定位原因、预警提示的可行性思考🤔



    背景


    部门要治理线上故障过多的问题,故障主要分后端异常以及前端的性能问题和业务问题。我们讨论的范围是前端业务故障,经过分析可以把它们大致分为UI、业务、工程技术、日志、三方依赖等。



    首先要确定降低故障的指标,MTTI和MTTD是关键,因为只有及时发现和定位问题才能快速消灭它们。我们暂时没有统计线上MTTI、MTTD的数据,因为缺乏相关的预警所以问题的发现和定位耗时通常很久,下面的一些复盘统计显示发现问题的时间可以持续甚至好几个月。



    这些问题中UI和业务异常占比超过80%,这些问题发现的不及时,一方面是问题本身不会阻碍核心链路,另一方面说明团队对业务的稳定缺少监控手段。
    现有开发流程已经包含了Design QA验收交付的UI是否符合预期;开发工程师会和QA工程师一起执行Test Case验证业务的稳定并且在CI环节还有UT的保障。既然如此那为什么线上还是会不可避免的出现故障呢?


    问题归因


    在Dev和Stage阶段的验收能发现和处理绝显而易见的异常,但是这些验收的场景是有限的



    1. 开发环境数据集的局限

    2. 考虑到AB因素的影响,很难做到全场景全业务覆盖。

    3. 开发过程可能会无意间影响到其他业务,但是我们的注意力集中在现有的业务,也就导致问题难以被发现


    所以归根到底,验收环节中数据和场景的局限以及人治导致一些Edge case被遗漏。


    解决方案


    我们该如何解决数据和场景的局限呢?这个其实通过Monkey和数据流量回放就能解决。
    运行时阶段包含了所有业务和代码的上下文,所有在这个阶段发现问题、分析原因并预警效率是最高的,人治的问题可以得到改善。下面是这种机制执行的思路



    1. 自动化测试时,通过流量回放的形式模拟线上的数据和环境尽可能多的覆盖场景。

    2. 运行时阶段,前端通过UT代码验收业务(🤔UT只能在Unit Test阶段执行,运行时执行的原理后面会讲)

    3. 运行时阶段,分析UI元素间的关系并探测异常问题


    方案实现


    方案实现仅讨论前端的部分。
    UI和业务检测比较通用,因为它们的判别条件是客观的。比如我们完全可以复用UT代码检测业务。


    自动检测、定位原因、预警


    这个机制实现没有困难。考虑到自动检测的范围在不同项目中不尽相同,所以实现的思路可以是插件的形式,模块间通过协议解耦。
    主要功能模块有:



    1. 告警模块

    2. 日志生成模块

    3. 业务注册模块(接收业务自定义的检查日志)

    4. 内嵌的UI检测模块


    UI检测


    业务不同,遇到的UI问题会有差异,这部分需要具体问题具体分析,所以不做过多讨论。针对我们业务的现状Overlap、Truncate、Clip在UI中占比较高。我的做法是对显示的视图按多叉树遍历到叶子节点并分析子节点和兄弟节点间的关系,找到Overlap、Truncate、Clip问题。具体的实现可以参考代码LensWindowGuard.swift:31


    业务检测


    UT代码从逻辑上可以被分为三个部分:



    1. Give

    2. When

    3. Then


    Given表示输入的数据,可以是真实接口也可以是Mock数据。


    When表示调用业务函数,同时这里会产生一个输出结果。


    Then表示检验输入的数据是否和输出的结果匹配,如果不匹配会在UT环节报错。


    业务代码从逻辑上可以被分为两个部分



    1. Give

    2. When


    Given可以是上下文的变量也可以是API调用


    When表示执行业务的代码块


    Blank diagram (23).png
    如果把UT的Then引入到业务的函数里,就可以实现运行时执行UT的效果了😁。


    将UT代码引入到业务函数中,思路是首先把UT按照Given、When、Then三部分拆分,把Given和Then部分独立出去,独立的部分通过代码插桩的方式在UT和业务代码中复用。


    Blank diagram (24).png


    不过到这里遗留了几个问题,暂时还没有太好的思路🧐



    1. 异步回调 - 业务代码或者UT检测逻辑只能执行一个

    2. 业务方法名的修改 - 使用Swift Macro插桩方式等同新增加一个全新的方法,所以运行时执行UT需要更改业务逻辑

    3. UT代码被执行多次 - 上面的图可以看出来,新的UT代码被同时插到旧的UT和业务代码中,所以执行UT时会UT逻辑被执行了两次


    代码插桩


    我们项目基于Swift,且插桩需要有判别逻辑,基于此选用AST的方式在编译期修改FunctionDecliation,把代码通过字符串的形式插到function body的合适位置。正巧2023 WWDC苹果发布了Swift Macro,其中的@Attached(Peer)正好可以满足对FunctionDecliation可编程修改,通过实现自动以@Attached(Peer)以及增加DEBUG宏(Swift Macro不能读取工程配置信息)可以改变UT的流程。真想说Swift太香了。


    最终


    完整的项目的整体架构大致如下,主要分了三部分。



    1. Hubble - 主要职责是提供基础协议、日志、预警和一些协助分析问题的工具集

    2. HubbleCoordinator - 主要职责是作为业务和组件交互的中间层,业务相关的逻辑都放在这里,隔离业务和Hubble

    3. 业务 - 仅处理Hubble初始化工作,对业务代码没有任何侵入
      AR


    作者:tom猪
    来源:juejin.cn/post/7274140856034099252
    收起阅读 »

    刚咬了一口馒头,服务器突然炸了!

    首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。 其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket...
    继续阅读 »

    首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。
    其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket管理器。


    看着群里一群“小可爱”疯狂乱叫,我被吵的头都炸了,赶紧尝试定位问题。

    1. 查看是否存在Jenkins发版 -> 无
    2. 查看最新提交记录 -> 最后一次提交是下午,到晚上这段时间系统一直是稳定的
    3. 查看服务器资源,htop后,发现三台相关的云服务器资源都出现闲置状态
    4. 查看PolarDB后,既MySQL,连接池正常、吞吐量和锁正常
    5. 查看Redis,资源正常,无异常key
    6. 查看前端控制台,出现一些报错,但是这些报错经常会变化
    7. 查看前端测试环境、后端测试环境,程序全部正常
    8. 重启前端服务、后端服务、NGINX服务,好像没用,过了5分钟后,咦,好像可以访问了

    就在我们组里的“小可爱”通知系统恢复正常后,20分钟不到,再一次处于无法打开的状态,沃焯!!!
    完蛋了,找不出问题在哪里了,实在想不通问题究竟出在哪里。


    我不服啊,我不理解啊!


    咦,Nginx?对呀,我瞅瞅访问日志,于是我浏览了一下access.log,看起来貌似没什么问题,不过存在大量不同浏览器的访问记录,刷的非常快。


    再瞅瞅error.log,好像哪里不太对

    2023/04/20 23:15:35 [alert] 3348512#3348512: 768 worker_connections are not enough
    2023/04/20 23:33:33 [alert] 3349854#3349854: *3492 open socket #735 left in connection 1013

    这是什么?貌似是连接池问题,赶紧打开nginx.conf看一下配置

    events {
    worker_connections 666;
    # multi_accept on;
    }

    ???


    运维小可爱,你特么在跟我开玩笑?虽然我们这个系统是给B端用的,还是我们自己组的不到100人,但是这连接池给的也太少了吧!


    另外,前端为什么会开到 1000 个 WS 连接呢?后端为什么没有释放掉FD呢?


    询问后,才知道,前端的Socket管理器,会在连接超时或者其它异常情况下,重新开启一个WS连接。


    后端的心跳配置给了300秒

    Constant::OPTION_OPEN_WEBSOCKET_PROTOCOL => true, // websocket 协议
    Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => 150,
    Constant::OPTION_HEARTBEAT_IDLE_TIME => 300,

    此时修改nginx.conf的配置,直接拉满!!!

    worker_connections 655350;

    重启Nginx,哇绰,好像可以访问了,但是每当解决一个问题,总会产生新的问题。


    此时error.log中出现了新的报错:

    2023/04/20 23:23:41 [crit] 3349767#3349767: accept4() failed (24: Too many open files)

    这种就不怕了,貌似和Linux的文件句柄限制有关系,印象中是1024个。
    至少有方向去排查,不是吗?而且这已经算是常规问题了,我只需小小的百度一下,哼哼~


    拉满拉满!!

    worker_rlimit_nofile 65535;

    此时再次重启Nginx服务,系统恢复稳定,查看当前连接数:

    netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
    # 打印结果
    TIME_WAIT 1175

    FIN_WAIT1 52

    SYN_RECV 1

    FIN_WAIT2 9

    ESTABLISHED 2033

    经过多次查看,发现TIME_WAITESTABLISHED都在不断减少,最后完全降下来。


    本次问题排查结束,问题得到了解决,但是关于socket的连接池管理器,仍然需要优化,及时释放socket无用的连接。


    作者:兰陵笑笑生666
    链接:https://juejin.cn/post/7224314619865923621
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    弃用qiankun!看古茗中后台架构如何破局

    引言 我们团队之前发布过一篇文章,介绍了古茗前端到底在做什么,当然这只包含了我们团队内做的一部分事情。以端侧来划分,主要包括:中后台 web 端、H5 端、4 端小程序、Electron PC 端、Android/Flutter 客户端,我们需要为这些端侧方向...
    继续阅读 »

    引言


    我们团队之前发布过一篇文章,介绍了古茗前端到底在做什么,当然这只包含了我们团队内做的一部分事情。以端侧来划分,主要包括:中后台 web 端、H5 端、4 端小程序、Electron PC 端、Android/Flutter 客户端,我们需要为这些端侧方向做一些通用能力的解决方案,同时也需要深入每个端侧细分领域做它们特有的技术沉淀。本文主要介绍古茗在中后台技术方向的一些思考和技术沉淀。


    业务现状


    古茗目前有大量的中后台业务诉求,包括:权限系统、会员系统、商品中心、拓展系统、营运系统、财务系统、门店系统、供应链系统等多个子系统,服务于内部产品、技术、运营,及外部加盟商等用户群体,这些子系统分别由不同业务线的同学负责开发和维护,而这些子系统还没有系统性的技术沉淀。随着业务体量和业务复杂度的不断增大,在“降本增效”的大背景下,如何保证业务快速且高质量交付是我们面临的技术挑战。


    技术演进




    如上述技术演进路线图所示,我们的中后台技术架构大致经历了 4 个阶段:当业务起步时,我们很自然的使用了单应用开发模式;当业务发展到一定的体量,多人协同开发变得困难时,我们拆分了多个子系统维护,并使用了 systemjs 来加载子系统资源(微前端模式的雏形);当遇到有三方库的多版本隔离诉求时,我们又引入了 qiankun 微前端框架来隔离多个子系统,保证各子系统间互不影响;那是什么原因让我们放弃 qiankun,转向完全自研的一套中后台解决方案?


    弃用 qiankun?


    其实准确来说,qiankun 并不能算是一个完整的中后台解决方案,而是一个微前端框架,它在整个技术体系里面只充当子应用管理的角色。我们在技术演进过程中使用了 qiankun 尝试解决子应用的隔离问题,但同时也带来了一些新的问题:某些场景跳转路由后视图无反应;某些三方库集成进来后导致奇怪的 bug...。同时,还存在一些问题无法使用 qiankun 解决,如:子应用入口配置、样式隔离、运维部署、路由冲突、规范混乱、要求资源必须跨域等。


    探索方向


    我们重新思考了古茗中后台技术探索方向是什么,也就是中后台技术架构到底要解决什么问题,并确定了当下的 2 大方向:研发效率用户体验。由此我们推导出了中后台技术探索要做的第一件事情是“统一”,这也为我们整个架构的基础设计确立了方向。




    架构设计


    我们的整体架构是围绕着“统一”和“规范” 2 大原则来设计,目标是提升整个团队的研发效能。我们认为好的架构应该是边界清晰的,而不是一味的往上面堆功能,所以我们思考更多的是,如果没有这个功能,能否把这件事情做好。


    取个“好”名字


    我老板曾说过一个好的(技术)产品要做的第一件事就是取个“好”名字。我们给这套中后台架构方案取名叫「Mars」,将相关的 NPM 包、组件库、SDK、甚至部署路径都使用 mars 关键字来命名,将这个产品名字深入人心,形成团队内共同的产品认知。这样的好处是可以加强团队对这套技术方案的认同感,以及减少沟通负担,大家一提到 mars,就都知道在说哪一件事。


    框架设计




    正如上述大图所示,我们是基于微前端的思路做的应用框架设计,所以与市面上大多数的微前端框架的设计思路、分层结构大同小异。这里还是稍微介绍一下整个流程:当用户访问网站时,首先经过网关层,请求到基座应用的资源并渲染基础布局和菜单,当监听路由变化时,加载并渲染路由关联的子应用及页面。


    但是,市面上大部分的微前端框架往往需要在基座应用上配置子应用的 nameentryavtiveRule 等信息,因为框架需要根据这些信息来决定什么时机去加载哪一个子应用,以及如何加载子应用的资源。这就意味着每新增一个子应用,都需要去基座维护这份配置。更要命的是,不同环境的 entry 可能还要根据不同的环境做区别判断。如果遇到本地开发时需要跳转至其他子应用的场景,那也是非常不好的开发体验。所以,这是我们万万不能接受的。


    针对这一痛点,我们想到了 2 种解决思路:

    1. 把基座应用的配置放到云端,用 node 作为中间层来维护各子应用的信息,每次新增应用、发布资源后同步更新,问题就是这套方案实现成本比较高,且新增子应用还是需要维护子应用的信息,只是转移到在云端维护了。
    2. 使用约定式路由及部署路径,当我们识别到一个约定的路由路径时,可以反推它的应用 ID 及部署资源路径,完全 0 配置。很明显,我们选择了这种方案。

    约定式路由及部署路径


    路由约定


    我们制定了如下的标准 Mars 路由规范

      /mars/appId/path/some?name=ferret
    \_/ \_/ \_____/ \_______/
    | | | |
    标识 appId path query


    1. 路由必须以 /mars 开头(为了兼容历史路由包袱)

    2. 其后就是 appId ,这是子应用的唯一标识

    3. 最后的 pathquery 部分就是业务自身的路由和参数


    部署路径约定


    我们制定了如下的标准 Mars 子应用部署路径规范

      https://cdn.example.com/mars/[appId]/[env]/manifest.json
    \__________________/ \_/ \___/ \_/ \________/
    | | | | |
    cdn 域名 标识 appId 环境 入口资源清单

    从上述部署路径规范可以看出,整个路径就 appIdenv 2 个变量是不确定的,而 env 可以在发布时确定,因此可由 appId 推导出完整的部署路径。而根据路由约定,我们可以很容易的从路由中解析出 appId,由此就可以拿到完整的 manifest.json 部署路径 ,并以此获取到整个子应用的入口资源信息。


    编译应用


    虽然制定了上述 2 大规范,但是如何保障规范落地,防止规范腐化也是非常重要的一个问题。我们是通过编译手段来强制约束执行的(毕竟“人”往往是靠不住的😄)。


    依赖工程化体系



    提示:Kone 是古茗内部前端工程化的工具产品。



    首先,子应用需要配置一个工程配置文件,并注册 @guming/kone-plugin-mars 插件来完成子应用的本地开发、构建、发布等工程化相关的任务。其中:配置项 appId 就代表约定路由中的 appId 和 部署路径中的 appId,也是子应用的唯一标识。


    工程配置文件:kone.config.json

    {
    "plugins": ["@guming/kone-plugin-mars"],
    "mars": {
    "appId": "demo"
    }
    }

    编译流程


    然后,子应用通过静态化配置式(json 配置)注册路由,由编译器去解析配置文件,注册路由,以及生成子应用 mountunmount 生命周期方法。这样实现有以下 3 个好处:

    • 完整的路由 path 由编译生成,可以非常好的保障约定式路由落地
    • 生命周期方法由编译生成,减少项目中的模板代码,同样可以约束子应用的渲染和卸载按照预定的方式执行
    • 可以约束不规范的路由 path 定义,例如我们会禁用掉 :param 形式的动态路由

    应用配置文件:src/app.json

    {
    "routes": [
    {
    "path": "/some/list",
    "component": "./pages/list",
    "description": "列表页"
    },
    {
    "path": "/some/detail",
    "component": "./pages/detail",
    "description": "详情页"
    }
    ]
    }


    上述示例最终会生成路由:/mars/demo/some/list/mars/demo/some/detail



    webpack-loader 实现


    解析 src/app.json 需要通过一个自定义的 webpack-loader 来实现,部分示例代码如下:

    import path from 'path';
    import qs from 'qs';

    export default function marsAppLoader(source) {
    const { appId } = qs.parse(this.resourceQuery.slice(1));
    let config;
    try {
    config = JSON.parse(source);
    } catch (err) {
    this.emitError(err);
    return;
    }

    const { routes = [] } = config;

    const routePathSet = new Set();
    const routeRuntimes = [];
    const basename = `/mars/${appId}`;

    for (let i = 0; i < routes.length; i++) {
    const item = routes[i];
    if (routePathSet.has(item.path.toLowerCase())) {
    this.emitError(new Error(`重复定义的路由 path: ${item.path}`));
    return;
    }

    routeRuntimes.push(
    `routes[${i}] = { ` +
    `path: ${JSON.stringify(basename + item.path)}, ` +
    `component: _default(require(${JSON.stringify(item.component)})) ` +
    `}`
    );
    routePathSet.add(item.path.toLowerCase());
    }

    return `
    const React = require('react');
    const ReactDOM = require('react-dom');

    // 从 mars sdk 中引入 runtime 代码
    const { __internals__ } = require('@guming/mars');
    const { defineApp, _default } = __internals__;

    const routes = new Array(${routeRuntimes.length});
    ${routeRuntimes.join('\n')}

    // define mars app: ${appId}
    defineApp({
    appId: '${appId}',
    routes,
    });

    `.trim();
    }

    src/app.json 作为编译入口并经过此 webpack-loader 编译之后,将自动编译关联的路由组件,创建子应用路由渲染模板,注册生命周期方法等,并最终输出 manifest.json 文件作为子应用的入口(类似 index.html),根据入口文件的内容就可以去加载入口的 js、css 资源并触发 mount 生命周期方法执行渲染逻辑。生成的 manifest.json 内容格式如下:

    {
    "js": [
    "https://cdn.example.com/mars/demo/prod/app.a0dd6a27.js"
    ],
    "css": [
    "https://cdn.example.com/mars/demo/prod/app.230ff1ef.css"
    ]
    }

    聊聊沙箱隔离


    一个好的沙箱隔离方案往往是市面上微前端框架最大的卖点,我们团队内也曾引入 qiankun 来解决子应用间隔离的痛点问题。而我想让大家回归到自己团队和业务里思考一下:“我们团队需要隔离?不做隔离有什么问题”。而我们团队给出的答案是:不隔离 JS,要隔离 CSS,理由如下:

    1. 不隔离 JS 可能会有什么问题:window 全局变量污染?能污染到哪儿去,最多也就内存泄露,对于现代 B 端应用来说,个别内容泄露几乎可以忽略不计;三方库不能混用版本?如文章开头所提及的,我们要做的第一件事就是统一,其中就包括统一常用三方库版本,在统一的前提下这种问题也就不存在了。当然也有例外情况,比如高德地图 sdk 在不同子系统需要隔离(使用了不同的 key),针对这种问题我们的策略就是专项解决;当然,最后的理由是一套非常好的 JS 隔离方案实现成本太高了,需要考虑太多的问题和场景,这些问题让我们意识到隔离 JS 带来的实际价值可能不太高。
    2. 由于 CSS 的作用域是全局的,所以非常容易造成子应用间的样式污染,其次,CSS 隔离是容易实现的,我们本身就基于编译做了很多约束的事情,同样也可以用于 CSS 隔离方案中。实现方案也非常简单,就是通过实现一个 postcss 插件,将子应用中引入的所有 css 样式都加上特有的作用域前缀,例如:
    .red {
    color: red;
    }

    将会编译成:

    .mars__demo .red {
    color: red;
    }

    当然,某些场景可能就是需要全局样式,如 antd 弹层内容默认就会在子应用内容区外,造成隔离后的样式失效。针对这种场景,我们的解法是用隔离白名单机制,使用也非常简单,在最前面加上 :global 选择器,编译就会直接跳过,示例:

    :global {
    .some-modal-cls {
    font-size: 14px;
    }
    }

    将会编译成:

    .some-modal-cls {
    font-size: 14px;
    }

    除此之外,在子应用卸载的时候,还会禁用掉子应用的 CSS 样式,这是如何做到的?首先,当加载资源的时候,会找到该资源的 CSSStyleSheet 对象:

    const link = document.createElement('link');
    link.setAttribute('href', this.url);
    link.setAttribute('rel', 'stylesheet');
    link.addEventListener('load', () => {
    // 找到当前资源对应的 CSSStyleSheet 对象
    const styleSheets = document.styleSheets;
    for (let i = styleSheets.length - 1; i >= 0; i--) {
    const sheet = styleSheets[i];
    if (sheet.ownerNode === this.node) {
    this.sheet = sheet;
    break;
    }
    }
    });

    当卸载资源的时候,将该资源关联的 CSSStyleSheet 对象的 disabled 属性设置为 true 即可禁用样式:

    if (this.sheet) {
    this.sheet.disabled = true;
    }

    框架 SDK 设计




    框架 SDK 按照使用场景可以归为 3 类,分别是:子应用、基座应用、编译器。同样的遵循我们的一贯原则,如果一个 API 可以满足诉求,就不会提供 2 个 API,尽可能保证团队内的代码风格都是统一的。例如:路由跳转 SDK 只提供唯一 API,并通过编译手段禁用掉其他路由跳转方式(如引入 react-router-dom 的 API 会报错):

    import { mars } from '@guming/mars';

    // 跳转路由:/mars/demo/some/detail?a=123
    mars.navigate('/mars/demo/some/detail', {
    params: { a: '123' }
    });

    // 获取路由参数
    const { pathname, params } = mars.getLocation();
    // pathname: /mars/demo/some/detail
    // params: { a: '123' }

    当然,我们也会根据实际情况提供一些便利的 API,例如:跳转路由要写完整的 /mars/[appId] 路由前缀太繁琐,所以我们提供了一个语法糖来减少样板代码,在路由最前面使用 : 来代替 /mars/[appId] 前缀(仅在当前子应用内跳转有效):

    import { mars } from '@guming/mars';

    // 跳转路由:/mars/demo/some/detail?a=123
    mars.navigate(':/some/detail', {
    params: { a: '123' }
    });

    另外,值得一提的是在基座应用上使用的一个 API bootstrap(),得益于这个 API 的设计,我们可以快速创建多个基座应用(不同域名),还能使用这个 API 在本地开发的时候启动一个基座来模拟本地开发环境,提升开发体验。


    本地开发体验


    开发模拟器


    为更好的支持本地开发环境,我们提供了一套本地开发模拟器,在子应用启动本地服务的时候,会自动启动一个模拟的基座应用,拥有和真实基座应用几乎一样的布局,运行环境,集成的登录逻辑等。除此之外,开发模拟器还提供了辅助开发的「debug 小组件」,比如通过 debug 工具可以动态修改本地开发代理规则,保存之后立即生效。




    IDE 支持


    为了提升开发体验,我们分别开发了 Webstorm 插件 和 VSCode 插件服务于 Mars 应用,目前支持路由组件配置的路径补全、点击跳转、配置校验等功能。此外,我们会为配置文件提供 json schema,配置后将会获得 IDE 的自动补全能力和配置校验能力。




    历史项目迁移


    技术架构演进对于业务项目来说最大的问题在于,如何完成历史项目向新架构的迁移改造?我们团队投入 2 个人花了 2 个月时间将 12 个历史项目全部迁移至最新的架构上,这里分享一些我们在迁移过程中的经验。


    定目标


    首先要确定历史项目迁移这件事情一定是要做的,这是毋庸置疑的,而且要越快越好。所以我们要做的第一件事就是制定迁移计划,最后确定投入 2 个人力,大约花 2 个月时间将历史项目全部迁移至新的架构。第二件事情就是确定改造的范围(确定边界,不做非目标范围内的改造),对于我们的业务现状来说,主要包括:

    • 统一 reactreact-dom 版本为 17.0.2
    • 统一 antd 版本为 4.24.8
    • 统一路由
    • 统一接入 request 请求库
    • 统一接入工程化体系
    • 统一环境变量

    梳理 SOP


    因为迁移的流程不算简单,迁移要做的事情还挺多的,所以接下来要做的一件事就是梳理迁移流程 SOP,SOP 文档要细化到每种可能的场景,以及遇到问题对应的解法,让后续项目的迁移可以傻瓜式的按照标准流程去操作即可。我们的做法是,先以一个项目作为试点,一边迁移一边梳理 SOP,如果在迁移其他项目中发现有遗漏的场景,再持续补充这份 SOP 文档。


    例如:之前项目中使用了 dva 框架,但是它的 routermodel 是耦合的,这样就无法使用我们制定的统一路由方案,对此我们的解法是,通过 hack dva 的源代码,将 model 前置注入到应用中,完成与路由的解耦。


    上线方案


    由于业务迭代频繁,所以我们代码改造持续的时间不能太长,否则要多次合并代码冲突,而我们的经验就是,项目改造从拉分支到发布上线,要在 1 周内完成。当然,整个上线过程还遇到许多需要解决的问题,比如在测试参与较少的情况下如何保障代码质量,包括:业务回归的策略,回滚策略,信息同步等等。


    总结


    之前看到 Umi 4 设计思路文字稿 里面有句话我觉得特别有道理:“社区要开放,团队要约束”,我们团队也在努力践行“团队约束”这一原则,因为它为团队带来的收益是非常高的。


    没有最完美的方案,只有最适合自己的方案,以上这套架构方案只是基于当下古茗前端团队现状做的选择后的结果,可能并不适合每个团队,希望本文的这些思考和技术沉淀能对您有所帮助和启发。


    最后


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


    作者:古茗前端团队
    链接:https://juejin.cn/post/7269352663258284069
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    树形列表翻页,后端: 搞不了搞不了~~

    web
    背景 记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用...
    继续阅读 »

    背景


    记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用户说一进到这个报告页面就卡住不动了,上去一查发现不得了,都是铁杆用户,每年作品都几百个,导致几年下来,这个报告返回了几千个作品,包含上千的图片。


    问题分析


    上千的图片,肯定会卡,首先想到的是做图片懒加载。这个很简单,使用一个vue的全局指令就可以了。但是上线发现,没啥用,dom节点多的时候,懒加载也卡。


    然后就问服务端能不能支持分页,服务端说数据太散,连表太多,树形结构很难做分页。光查询出来就已经很费劲了。


    没办法于是想了一下如何前端来处理掉。


    思路




    1. 由于是app中的嵌入页面,首先考虑通过滚动进行分页加载。




    2. 一次性拿了全部的数据,肯定不能直接全部渲染,我们可以只渲染一部分,比如第一个节点,或者前几个节点。




    3. 随着滚动一个节点一个节点或者一批一批的渲染到dom中。




    实现


    本文仅展示一种基于vue的实现


    1. 容器

    设计一个可以进行滚动翻页的容器 然后绑定滚动方法OnPageScrolling



    <style lang="less" scoped>

    .study-backup {

    overflow-x: hidden;

    overflow-y: auto;

    -webkit-overflow-scrolling: touch;

    width: 100%;

    height: 100%;

    position: relative;

    min-height: 100vh;

    background: #f5f8fb;

    box-sizing: border-box;

    }

    </style>

    <template>

    <section class="report" @scroll="OnPageScrolling($event)">

    </section>

    </template>



    2.初始化数据

    这里定义一下树形列表的数据结构,实现初始化渲染,可以渲染一个树节点,或者一个树节点的部分子节点



    GetTreeData() {

    treeapi

    .GetTreeData({ ... })

    .then((result) => {

    // 处理结果

    const data = Handle(result)

    // 这里备份一份数据 不参与展示

    this.backTreeList = data.map((item) => {

    return {

    id: item.id,

    children: item.children

    }

    })

    // 这里可以初始化为第一个树节点

    const nextTree = this.backTreeList[0]

    const nextTansformTree = nextTree.children.splice(0)

    this.treeList = [{

    id: nextTree.id,

    children: nextTansformTree

    }]

    // 这里可以初始化为第一树节点 但是只渲染第一个子节点

    const nextTree = this.backTreeList[0]

    const nextTansformTree = nextTree.children.splice(0, 1)

    this.treeList = [{

    id: nextTree.id,

    children: nextTansformTree

    }]

    })

    },


    3.滚动加载

    这里通过不断的把 backTreeList 的子节点转存入 treeList来实现分页加载。



    OnPageScrolling(event) {

    const container = event.target

    const scrollTop = container.scrollTop

    const scrollHeight = container.scrollHeight

    const clientHeight = container.clientHeight

    // console.log(scrollTop, clientHeight, scrollHeight)

    // 判断是否接近底部

    if (scrollTop + clientHeight >= scrollHeight - 10) {

    // 执行滚动到底部的操作

    const currentReport = this.backTreeList[this.treeList.length - 1]

    // 检测匹配的当前树节点 treeList的长度作为游标定位

    if (currentReport) {

    // 判断当前节点的子节点是否还存在 如果存在则转移到渲染树中

    if (currentReport.children.length > 0) {

    const transformMonth = currentReport.children.splice(0, 1)

    this.treeList[this.treeList.length - 1].children.push(

    transformMonth[0]

    )

    // 如果不存在 则寻找下一树节点进行复制 同时复制下一节点的第一个子节点 当然如果寻找不到下一树节点则终止翻页

    } else if (this.treeList.length < this.backTreeList.length) {

    const nextTree = this.backTreeList[this.treeList.length]

    const nextTansformTree = nextTree.children.splice(0, 1)

    this.treeList.push({

    id: nextTree.id,

    children: nextTansformTree

    })

    }

    }

    }

    }


    4. 逻辑细节

    从上面代码可以看到,翻页的操作是树copy的操作,将备份树的子节点转移到渲染树中




    1. copy备份树的第一个节点到渲染树,同时将备份树的第一个节点的子节点的第一个节点转移到渲染树的第一个节点的子节点中




    2. 所谓转移操作,就是数组splice操作,从一颗树中删除,然后把删除的内容插入到另一颗树中




    3. 由于渲染树是从长度1开始的,所以我们可以根据渲染树的长度作为游标和备份树进行匹配,设渲染树的长度为当前游标




    4. 根据当前游标查询备份树,如果备份树的当前游标节点的子节点不为空,则进行转移




    5. 如果备份树的当前游标节点的子节点为空,则查找备份树的当前游标节点的下一节点,设为下一树节点




    6. 如果找到了备份树的当前游标节点的下一节点,扩展渲染树,将下一树节点复制到渲染树,同时将下一树节点的子节点的第一节点复制到渲染树




    7. 循环4-6,将备份树完全转移到渲染树,完成所有翻页




    扩展思路


    这个方法可以进行封装,将每次复制的节点数目和每次复制的子节点数目作为传参,一次可复制多个节点,这里就不做展开


    作者:CodePlayer
    来源:juejin.cn/post/7270503053358612520
    收起阅读 »

    创建一个可以循环滚动的文本,可能没这么简单。

    web
    如何创建一个可以向左循环滚动的文本? 创建如上图效果的滚动文本,你能想到几种方式? -------- 暂停阅读,不如你自己先试一下 -------- 方式一: 根据页面宽度,生成多个元素。每个元素通过js控制,每帧向左偏移一点像素。 如果偏移的元素不可见后...
    继续阅读 »

    如何创建一个可以向左循环滚动的文本?


    loop.gif


    创建如上图效果的滚动文本,你能想到几种方式?


    -------- 暂停阅读,不如你自己先试一下 --------


    方式一:


    根据页面宽度,生成多个元素。每个元素通过js控制,每帧向左偏移一点像素。

    如果偏移的元素不可见后,将此元素再移动到屏幕最右边。


    此方式容易理解,实现起来也不困难,但是有性能上的风险,因为每一帧都在修改元素的位置。


    方式二:


    根据页面宽度,生成多个元素。每个元素通过js控制,通过setInterval每一秒向左偏移一些像素。

    然后结合css的transition: all 1s linear;使得偏移更加顺滑。

    如果偏移的元素不可见后,将此元素再移动到屏幕最右边。


    使用此方法可以避免高频率计算元素位置,但是此方式控制起来更复杂,主要是因为,将元素移动到最右边的时候,也会触发transition ,需要额外逻辑控制在元素移到最右边的时候不触发transition

    并且在实际开发中发现。当窗口不可见时候动画实际会暂停,还需要控制当窗口隐藏时候,暂停setInterval


    方式三:


    换一种思路。按顺序排列元素,多个子元素首位相接。将每个子元素通过animation: xxx 10s linear infinite;

    从左到右移动。在一定范围内移动子元素,通过视觉错觉,像是整个大元素(盒子)都在移动。

    此方式简单,并且无需JS,性能较好。


    下面是完整代码(可以控制浏览器宽度,查看不同尺寸屏幕的效果)


    <!doctype html>  
    <html>
    <head>
    <title>LOOP</title>
    <style>
    @keyframes loop {
    0% {
    transform: translateX(0);
    }
    100% {
    transform: translateX(-100%);
    }
    }

    .box {
    white-space: nowrap;
    }

    .scrollContent {
    width: 600px;
    display: inline-block;
    text-align: center;
    animation: loop 3s linear infinite;
    }
    </style>
    </head>
    <body>
    <div class="box">
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    </div>
    </body>

    </html>


    方式四:


    方式三会创建多份一样的文本内容,你可能会说,屏幕上同时出现这么多文本元素,当然要创建这么多一样的内容。

    其实还有一种性能更佳的方式:text-shadow: 600px 0 currentColor,通过此方式创建多份文本副本,达到类似效果。

    此方法性能最佳。但是对非文本无能为力。


    <!doctype html>  
    <html>
    <head>
    <title>LOOP</title>
    <style>
    @keyframes loop {
    0% {
    transform: translateX(0);
    }
    100% {
    transform: translateX(-100%);
    }
    }

    .box {
    white-space: nowrap;
    }

    .scrollContent {
    color: rebeccapurple;
    width: 600px;
    display: inline-block;
    text-align: center;
    animation: loop 3s linear infinite;
    text-shadow: 600px 0 currentColor, 1200px 0 currentColor, 1800px 0 currentColor, 2400px 0 currentColor;
    }
    </style>
    </head>
    <body>
    <div class="box">
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    </div>
    </body>

    </html>

    总结


    方式1:应该是最直接想到的方式。但是出于对性能的担忧。

    方式2:由于方式1性能优化得到,但是方式2过于复杂。
    方式3: 看上去非常易于实现,实际很难想到。
    方式4:如果对text-shadow和css颜色掌握不熟,根本难以实现。


    希望对你有所启发


    作者:wuwei123
    来源:juejin.cn/post/7273026570930257932
    收起阅读 »

    位运算,能不能一次记住!

    web
    写给新手朋友的位运算,你是不是也是总是不记得,记了又忘记,记住了又不知道怎么运用到编程中?那记一次理论+题目的方式理解记忆,搞清楚它吧! 我们接触最多的是十进制数,加减乘除这种四则远算其实就是二进制数据中的一种运算法则,当谈到位运算,它不是用来操作十进制数的,...
    继续阅读 »

    写给新手朋友的位运算,你是不是也是总是不记得,记了又忘记,记住了又不知道怎么运用到编程中?那记一次理论+题目的方式理解记忆,搞清楚它吧!


    我们接触最多的是十进制数,加减乘除这种四则远算其实就是二进制数据中的一种运算法则,当谈到位运算,它不是用来操作十进制数的,我们实际上是在操作二进制数的不同位。位运算在前端开发中可能不常用,但了解它们对你理解计算机底层运作和一些特定情况下的优化是有帮助的。


    接下来我们从几种常见的位运算开始,以及它们的使用场景,好好理解一番。


    1. 二进制转换


    既然是写给新手朋友也能看得明白的,那就顺带提一下二进制数吧(熟悉二进制的可以跳过这段)



    当计算机处理数据时,它实际上是在执行一系列的二进制操作,因为计算机内部使用的是电子开关,这些开关只能表示两个状态:开(表示1)和关(表示0)。因此,计算机中的所有数据最终都被转换为二进制表示。


    二进制(binary)是一种使用两个不同符号(通常是 0 和 1)来表示数字、字符、图像等信息的数字系统。这种二元系统是现代计算机科学的基础。





    • 十进制到二进制的转换:




    将十进制数转换为二进制数的过程涉及到不断地除以2,然后记录余数。最后,将这些余数按相反的顺序排列,就得到了对应的二进制数。


    例如,将十进制数 13 转换为二进制数:



    1. 13 除以 2 得商 6,余数 1

    2. 6 除以 2 得商 3,余数 0

    3. 3 除以 2 得商 1,余数 1

    4. 1 除以 2 得商 0,余数 1


    将这些余数按相反的顺序排列,得到二进制数 1101。


    或者你也可以这么想


    1. (1 || 0) * 2^n + (1 || 0) * 2^(n-1) + ... + (1 || 0) * 2^0 = 13

    2. 只需要满足以上公式,加出来你想要的值

    3. 2 的 4次方大于13,2的3次方小于13,那么就从2的3次方开始依次递减到0次方

    4. 1 * 2^3 + 1 * 2^2 + 1 * 2^1 + 1 * 2^0 显然 8 + 4 + 2 + 1 = 15已经超出了13,所以你得在这个式子中减少2

    5. 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 取该等式中的1,0;所以 13 的二进制是 1101


    以上两种方式都能得出一个数的二进制,看你喜欢




    • 二进制到十进制的转换:




    将二进制数转换为十进制数的过程涉及到将每个位上的数字与2的幂相乘,然后将这些结果相加。


    例如,将二进制数 1101 转换为十进制数:



    1. 第0位(最右边)上的数字是 1,表示 2^0 = 1

    2. 第1位上的数字是 0,表示 2^1 = 0

    3. 第2位上的数字是 1,表示 2^2 = 4

    4. 第3位上的数字是 1,表示 2^3 = 8


    将这些结果相加:1 + 0 + 4 + 8 = 13,得到十进制数 13。


    在编程中,通常会使用不同的函数或方法来实现十进制到二进制以及二进制到十进制的转换,这些转换可以帮助我们在计算机中处理和表示不同的数据。


    2. 按位与(&)


    按位与运算会将两个数字的二进制表示的每一位进行 操作,如果两个相应位都是 1,则结果为 1,否则为 0。


    使用场景: 常用于权限控制和掩码操作。


    image.png


    一道题让你更好的理解它的用法


    题目:判断一个整数是否是2的幂次方。


    问题描述:给定一个整数 n,判断它是否是2的幂次方,即是否满足 n = 2^k,其中 k 是非负整数。


    使用位运算中的按位与操作可以很巧妙地解决这个问题。


    思路:如果一个数 n 是2的幂次方,那么它的二进制表示一定只有一位是1,其他位都是0(例如:8的二进制是 1000)。而 n - 1 的二进制表示则是除了最高位的1之外,其他位都是1(例如:7的二进制是 0111)。如果我们对 nn - 1 进行按位与操作,结果应该是0。


    那我们可以这么写:


    image.png


    在这个示例中,我们巧妙的使用了 (n & (n - 1)) 来检查是否满足条件,如果结果为0,说明 n 是2的幂次方。


    希望这个示例能够帮助你更好地理解按位与运算的应用方式!


    2. 按位或(|)


    按位或运算会将两个数字的二进制表示的每一位进行或操作,如果两个相应位至少有一个是 1,则结果为 1,否则为 0。


    使用场景: 常用于设置选项和权限。


    image.png


    一道题让你更好的理解它的用法


    题目:如何将一个整数的特定位设置为1,而不影响其余位。


    问题描述:给定一个整数 num,以及一个表示要设置为1的位的位置 bitPosition(从右向左,最低位的位置为0),编写一个函数将 num 的第 bitPosition 位设置为1。


    我们可以使用按位或运算来实现这个效果


    image.png


    在这个示例中,我们首先创建了一个掩码 mask(这里用到了另一个位运算,左移,下面会讲到),它只有第 bitPosition 位是1,其他位都是0。然后,我们使用按位或运算 num | masknum 的第 bitPosition 位设置为1,得到了结果。


    这个问题演示了如何使用按位或运算来修改一个整数的特定位,而不影响其他位。希望这个示例能帮助你更好地理解按位或运算的应用方式!


    3. 按位异或(^)


    按位异或运算会将两个数字的二进制表示的每一位进行异或操作,如果两个相应位不相同则结果为 1,相同则为 0。


    使用场景: 常用于数据加密和校验。


    image.png


    一道题让你更好的理解它的用法


    题目:如何交换两个整数的值,而不使用额外的变量


    问题描述:给定两个整数 ab,编写一个函数来交换它们的值,而不使用额外的变量。


    我们可以使用按位异或运算来实现这个效果:


    image.png


    上述代码中,我们首先将 a 更新为 a ^ b,这使得 a 包含了 ab 的异或值。然后,我们使用同样的方法将 b 更新为 a 的原始值,最后,我们再次使用异或运算将 a 更新为 b 的原始值,完成了交换操作。



    此处应该沉思,思考清楚这个问题:(a ^ b) ^ b 得到的是 a 的原始值



    不使用额外的变量来做两个变量值的交换,这还是个面试题哦!


    4. 按位非(~)


    按位非运算会将一个数字的二进制表示的每一位取反,即 0 变成 1,1 变成 0。它将操作数转化为 32 位的有符号整型。


    image.png


    一道题让你更好的理解它的用法


    题目:反转二进制数的位,然后返回其对应的十进制数


    问题描述:给定一个二进制字符串,编写一个函数来反转该字符串的位,并返回其对应的十进制数。


    image.png


    这里你可能会有疑问,为什么13的二进制取反会的到-14,这里就不得不介绍一下 补码 的概念了


    5. 补码小插曲


    假设我们要求 -6 的二进制,那就相当于是求 -6 的补码


    因为负数的二进制表示通常使用二进制补码来表示。要计算-6的二进制补码表示,可以按照以下步骤操作:



    1. 首先,找到6的二进制表示。6的二进制表示是 00000110

    2. 然后,对6的二进制表示进行按位取反操作,即将0变成1,将1变成0。这将得到 11111001

    3. 最后,将取反后的结果加1。11111001 + 1 = 11111010


    所以,-6的二进制补码表示是 11111010。在补码中,最高位表示符号位,0表示正数,1表示负数,其余位表示数值的绝对值。因此,11111010 表示的是-6。


    注意:

    -6的二进制补码表示的位数不一定是8位。位数取决于数据类型和计算机系统的规定。在许多计算机系统中,整数的表示采用固定的位数,通常是32位或64位,但也可以是其他位数,例如16位。


    在常见的32位表示中,-6的二进制补码表示可能是 11111111111111111111111111111010。这是32位二进制,其中最高位是符号位(1表示负数),其余31位表示数值的绝对值。


    在64位表示中,-6的二进制补码表示可能是 1111111111111111111111111111111111111111111111111111111111110。这是64位二进制,同样,最高位是符号位,其余63位表示数值的绝对值。


    因此,-6的二进制补码表示的位数取决于计算机系统和数据类型的规定。不同的系统和数据类型可能采用不同的位数。


    6. 左移(<<)和右移(>>)


    左移运算将一个数字的二进制表示向左移动指定的位数,右移运算将二进制表示向右移动指定的位数。


    image.png



    注意:因为我们的计算可以是32位或者是64位的,所以理论上 5 的二进制应该是 00... 00000101, 整体长度为32或者64。 左移我们只是把有效值 101 向左拖动,右边补0,右移左边补 0, 但是要保证整体32或64位长度不能变,所以,右移会砍掉超出去的值



    一道题让你更好的理解它的用法


    题目: 如何实现整数的乘法和除法,使用左移和右移操作来提高效率。


    问题描述:编写一个函数,实现整数的乘法和除法运算,但是只能使用左移和右移操作,不能使用乘法运算符 * 和除法运算符 /


    这也是一道面试题,实现起来很简单


    image.png



    想清楚,一个数的二进制,每次左移一位的结果会怎么样?


    比如 6 的二进制是 00000110, 左移一次后变成 00001100,


    也就是说 从 2^2 + 2^1 变成了 2^3+ 2^2 。 4 + 2 变成了 8 + 4。


    所以每左移一位,都相当于是原数值本身放大了一倍



    这样你是否更清楚了用左移来实现乘法的效果了呢?


    最后


    以上列举的是常见的位运算方法,还有一些不常见的,比如:



    1. 位清零(Bit Clearing):将特定位设置为0,通常使用按位与运算和适当的掩码来实现。

    2. 位设置(Bit Setting):将特定位设置为1,通常使用按位或运算和适当的掩码来实现。

    3. 位翻转(Bit Flipping):将特定位取反,通常使用按位异或运算和适当的掩码来实现。

    4. 检查特定位:通过使用按位与运算和适当的掩码来检查特定位是否为1或0。

    5. 位计数:计算一个整数二进制表示中1的个数,这通常使用一种称为Brian Kernighan算法的技巧来实现。

    6. 位交换:交换两个整数的特定位,通常使用按位异或运算来实现。


    等等...有兴趣的可以自行摸索了


    作者:一个大蜗牛
    来源:juejin.cn/post/7274188187675902004
    收起阅读 »

    如何告诉后端出身的领导:这个前端需求很难实现

    本文源于一条评论。 有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。” 这位朋友让我写一写,那我就写一写。 反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评...
    继续阅读 »

    本文源于一条评论。




    有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。


    这位朋友让我写一写,那我就写一写。


    反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评论回复吧。愿意看扯淡的同学可以留一下。


    现象分析


    首先,前半句让我很有同感。因为,我就是前端出身,并且在研发管理岗上又待过。这个身份,让我既了解前端的工作流程,又经常和后端平级一起交流经验。这有点卧底的意思。


    有一次,我们几个朋友聚餐闲聊。他们也都是各个公司的研发负责人。大家就各自吐槽自己的项目。


    有一个人就说:“我们公司,有一个干前端的带项目了,让他搞得一团糟”


    另一个人听到后,就叹了一口气:“唉!这不是外行领导内行吗?!


    我当时听了感觉有点别扭。我扫视了一圈儿,好像就我一个前端。因此,我也点了点头(默念:我是卧底)。


    是啊,一般的项目、研发管理岗,多是后端开发。因为后端是业务的设计者,他掌握着基本的数据结构以及业务流转。而前端在他们看来,就是调调API,然后把结果展示到页面上。


    另外,一般项目遇到大事小情,也都是找后端处理。比如手动修改数据、批量生成数据等等。从这里可以看出,项目的“根基”在后端手里。


    互联网编程界流传着一条鄙视链。它是这样说的,搞汇编的鄙视写C语言的,写C的鄙视做Java的,写Java的鄙视敲Python的,搞Python的鄙视写js的,搞js的鄙视作图的。然后,作图的设计师周末带着妹子看电影,前面的那些大哥继续加班写代码。


    当然,我们提倡一个友好的环境,不提倡三六九等。这里只是调侃一下,采用夸张的手法来阐述一种现象。


    这里所谓的“鄙视”,其本质是源于谁更接近原理。


    比如写汇编的人,他认为自己的代码能直接调用“CPU”、“内存”,可以直接操纵硬件。有些前端会认为美工设计的东西,得依靠自己来实现,并且他们不懂技术就乱设计,还有脸要求100%还原。


    所以啊,有些只做过后端的人,可能会认为前端开发没啥东西。


    好了,上面是现象分析。关于谁更有优越感,这不能细究啊,因为这是没有结论的。如果搞一个辩论赛,就比方说”Python一句话,汇编写三年“之类论据,各有千秋。各种语言既然能出现,必定有它的妙用。


    我就是胡扯啊。不管您是哪个工种,请不要对号入座。如果您觉得被冒犯了,可以评论“啥都不懂”,然后离开。千万不要砸自己的电脑。


    下面再聊聊第二部分,面对这种情况,有什么好的应对措施?


    应对方法


    我感觉,后端出身的负责人,就算是出于人际关系,也是会尊重前端开发的


    “小张啊,对于前端我不是很懂。所以请帮我评估下前端的工期。最好列得细致一些,这样有利于我了解人员的工作量。弄完了,发给我,我们再讨论一下!”


    一般都是这么做。


    这种情况,我们就老老实实给他上报就行了,顶多加上10%~30%处理未知风险的时间。


    但是,他如果这样说:“什么?这个功能你要做5天?给你1天都多!


    这时,他是你的领导,对你又有考核,你怎么办?


    你心里一酸:“我离职吧!小爷我受不了这委屈!”


    这……当然也可以。


    如果你有更好的去处,可以这样。就算是没有这回事,有好去处也赶紧去。


    但是,如果这个公司整体还行呢?只是这个直接领导有问题。那么你可以考虑,这个领导是流水的,你俩处不了多长时间。哪个公司每年不搞个组织架构调整啊?你找个看上的领导,吃一顿饭,求个投靠呗。


    或者,这个领导只是因为不懂才这样。谁又能样样精通呢?给他说明白,他可能就想通了。人是需要说服的。


    如果你奔着和平友好的心态去,那么可以试试以下几点:


    第一,列出复杂原因


    既然你认为难以实现,那肯定是难以实现。具体哪里不好实现,你得说说


    记得刚工作时,我去找后端PK,我问他:“你这个接口怎么就难改了?你不改这个字段,我们得多调好几个接口,而且还对应不起来!”


    后端回复我:“首先,ES……;其次,mango……;最后,redis……”


    我就像是被反复地往水缸中按,听得”呜噜呜噜“的,一片茫然。


    虽然,我当时不懂后端,但我觉得他从气势上,就显得有道理


    到后来,还是同样的场景。我变成了项目经理,有一个年轻的后端也是这么回复我的。


    我说:“首先,你不用……;其次,数据就没在……;最后,只需要操作……”。他听完,挠了挠头说,好像确实可以这么做。


    所以,你要列出难以实现的地方。比如,没有成熟的UI组件,需要自己重新写一个。又或者,某种特效,业内都没有,很难做到。再或者,某个接口定得太复杂,循环调组数据,会产生什么样的问题。


    如果他说“我看到某某软件就是这样”。


    你可以说,自己只是一个初级前端,完成那些,得好多高级前端一起研究才行。


    如果你懂后端,可以做一下类比,比如哪些功能等同于多库多表查询、多线程并发问题等等,这可以辅助他理解。不过,如果能到这一步,他的位子好像得换你来坐。


    第二,给出替代方案


    这个方案,适用于”我虽然做不了,但我能解决你的问题“。


    就比如那个经典的打洞问题。需求是用电钻在墙上钻一个孔,我搞不定电钻,但是我用锤子和钉子,一样能搞出一个孔来。


    如果,你遇到难以实现的需求。比如,让你画一个很有特色的扇形玫瑰图。你觉得不好实现,不用去抱怨UI,这只会激化问题。咱可以给他提供几个业界成熟的组件。然后,告诉他,哪里能改,都能改什么(颜色、间距、字体……)。


    我们有些年轻的程序员很直,遇到不顺心的事情就怼。像我们大龄程序员就不这样。因为有房贷,上有老下有小。年龄大的程序员就想,到底是哪里不合适,我得怎样通过我的经验来促成这件事——这并不光彩,年轻人就得有年轻人的样子。


    第二招是给出替代方案。那样难以实现,你看这样行不行


    第三,车轮战,搞铺垫


    你可能遇到了一个硬茬。他就是不变通,他不想听,也不想懂,坚持“怎么实现我不管,明天之前要做完”。


    那他可能有自己的压力,比如老板就是这么要求他的。他转手就来要求你。


    你俩的前提可能是不一样。记住我一句话,没有共同前提,是不能做对比的。你是员工,他是领导,他不能要求你和他有一样的压力,这就像你不能要求,你和他有一样的待遇。多数PUA,就是拿不同的前提,去要求同样的结果。


    那你就得开始为以后扯皮找铺垫了。


    如果你们组有多个前端,可以发动大家去进谏。


    ”张工啊,这个需求,确实不简单,不光是小刘,我看了,也觉得不好实现“


    你一个人说了他不信,人多了可能就信了。


    如果还是不信。那没关系,已经将风险提前抛出了


    “这个需求,确实不好实现。非要这么实现,可能有风险。工期、测试、上线后的稳定性、用户体验等,都可能会出现一些问题”


    你要表现出为了项目担忧,而不是不想做的样子。如果,以后真发生了问题,你可以拿出“之前早就说过,多次反馈,无人理睬”这类的说辞。


    ”你居然不想着如何解决问题,反倒先想如何逃避责任?!“


    因此说,这是下下策。不建议程序员玩带有心机的东西。


    以上都是浅层次的解读。因为具体还需要结合每个公司,每个领导,每种局面。


    总之,想要解决问题,就得想办法


    作者:TF男孩
    链接:https://juejin.cn/post/7235966417564532794
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    京东一面:post为什么会发送两次请求?🤪🤪🤪

    web
    在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
    继续阅读 »

    在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


    那么接下来这篇文章我们就一点一点开始引出这个问题。


    同源策略


    在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


    但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



    • 跨站脚本攻击(XSS)

    • SQL 注入攻击

    • OS 命令注入攻击

    • HTTP 首部注入攻击

    • 跨站点请求伪造(CSRF)

    • 等等......


    如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


    这就引出了最基础、最核心的安全策略:同源策略。


    什么是同源策略


    同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


    如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



    • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

    • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

    • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


    如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


    URL结果原因
    http://store.company.com:80/dir/page.html同源只有路径不同
    http://store.company.com:80/dir/inner/another.html同源只有路径不同
    https://store.company.com:80/secure.html不同源协议不同,HTTP 和 HTTPS
    http://store.company.com:81/dir/etc.html不同源端口不同
    http://news.company.com:80/dir/other.html不同源主机不同

    同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



    • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

    • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

    • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


    出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


    CORS


    对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


    浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



    CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



    例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


    跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


    CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


    简单请求


    不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



    1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

    2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

    3. 请求中没有使用 ReadableStream 对象。

    4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

    5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


    预检请求


    非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


    需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


    例如我们在掘金上删除一条沸点:


    20230822094049


    它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



    • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

    • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

    • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

    • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


    一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


    20230822122441


    上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


    附带身份凭证的请求与通配符


    在响应附带身份凭证的请求时:



    • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

    • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

    • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

    • 对于附带身份凭证的请求(通常是 Cookie),


    这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


    另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


    参考文章



    总结


    预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


    跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


    预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


    使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


    整个完整的请求流程有如下图所示:


    20230822122544


    最后分享两个我的两个开源项目,它们分别是:



    这两个项目都会一直维护的,如果

    作者:Moment
    来源:juejin.cn/post/7269952188927017015
    你也喜欢,欢迎 star 🥰🥰🥰

    收起阅读 »

    网易云音乐 Tango 低代码引擎正式开源!

    web
    📝 Tango 简介 Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供...
    继续阅读 »

    📝 Tango 简介


    Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。


    Tango 低代码引擎开发效果


    如上图所示,Tango 低代码引擎支持可视化视图与源码双向同步,双向互转,为开发者提供 LowCode+ ProCode 无缝衔接的开发体验。


    ✨ 核心特性



    • 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等

    • 基于源码 AST 驱动,无私有 DSL 和协议

    • 提供实时出码能力,支持源码进,源码出

    • 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件

    • 使用 TypeScript 开发,提供完整的类型定义文件


    🏗️ 基于源码的低代码搭建方案


    Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的 基于 Schema 驱动的低代码方案 相比,不受私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。



    📄 源码进,源码出


    由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。


    code in, code out


    🏆 产品优势


    与基于私有 Schema 的低代码搭建方案相比,Tango 低代码引擎具有如下优势:


    对比项基于 Schema 的低代码搭建方案Tango(基于源码 AST 转换)
    适用场景面向特定的垂直搭建场景,例如表单,营销页面等🔥 面面向以源码为中心的应用搭建场景
    语言能力依赖私有协议扩展,不灵活,且难以与编程语言能力对齐🔥 直接基于 JavaScript 语言,可以使用所有的语言特性,不存在扩展性问题
    开发能力LowCode🔥 LowCode + ProCode
    源码导出以 Schema 为中心,单向出码,不可逆🔥 以源码为中心,双向转码
    自定义依赖需要根据私有协议扩展封装,定制成本高🔥 原有组件可以无缝低成本接入
    集成研发设施定制成本高,需要额外定制🔥 低成本接入,可以直接复用原有的部署发布能力

    📐 技术架构


    Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。此外,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。


    具体的技术架构如下图所示:


    low-code engine


    ⏰ 开源里程碑


    Tango 低代码引擎是网易云音乐内部低代码平台的核心构件,开源涉及到大量的核心逻辑解藕的工作,这将给我们正常的工作带来大量的额外工作,因此我们计划分阶段推进 Tango 低代码引擎的开源事项。



    1. 今天我们正式发布 Tango 低代码引擎的第一个社区版本,该版本将会包括 Tango 低代码引擎的核心代码库,TangoBoot 应用框架,以及基于 antd v4 适配的低代码组件库。

    2. 我们计划在今年的 9 月 30 日 发布低代码引擎的 1.0 Beta 版本,该版本将会对核心的实现面向社区场景重构,移除掉我们在云音乐内部的一些兼容代码,并将核心的实现进行重构和优化。

    3. 我们计划在今年的 10 月 30 日 发布低代码引擎的 1.0 RC 版本,该版本将会保证核心 API 基本稳定,不再发生 BREAKING CHANGE,同时我们将会提供完善翔实的开发指南、部署文档、和演示应用。

    4. 正式版本我们将在 2023 年 Q4 结束前 发布,届时我们会进一步完善我们的开源社区运营机制。


    milestones


    🤝 社区建设


    我们的开源工作正在积极推进中,可以通过如下的信息了解到我们的最新进展:



    欢迎大家加入到我们的社区中来,一起参与到 Tango 低代码引擎的开源建设中来。有任何问题都可以通过 Github Issues 反馈给我们,我们会及时跟进处理。


    💗 致谢


    感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。


    感谢 CodeSandbox 提供的 Sandpack 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。

    作者:网易云音乐技术团队
    来源:juejin.cn/post/7273051203562749971

    收起阅读 »

    前端比localStorage存储还大的本地存储方案

    产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。 方案选择既然要存储的数量大,得排除cookielocalStorage,虽然比cookie多,但是同样有上限(5M)左右,备选websql 使用简单,存储量大,兼容性差,备选index...
    继续阅读 »

    产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。


    方案选择

    • 既然要存储的数量大,得排除cookie
    • localStorage,虽然比cookie多,但是同样有上限(5M)左右,备选
    • websql 使用简单,存储量大,兼容性差,备选
    • indexDB api多且繁琐,存储量大、高版本浏览器兼容性较好,备选

    既然罗列了一些选择,都没有十全十美的,那么有没有一种能够集合这多种方式的插件呢?渐进增强 or 优雅降级 的存在
    冲着这个想法,就去github和谷歌找了一下,还真的有这么一个插件。


    那就是 localforage


    localforage


    localForage 是一个 JavaScript 库,只需要通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。 




    关于兼容性


    localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。下面是 indexDB、web sql、localStorage 的一个浏览器支持情况,可以发现,兼容性方面loaclForage基本上满足99%需求


    使用


    解决了兼容性和存储量的点,我们就来看看localforage的基础用法


    安装

    # 通过 npm 安装:
    npm install localforage
    // 直接引用
    <script src="localforage.js"></script>
    <script>console.log('localforage is: ', localforage);</script>

    获取存储


    getItem(key, successCallback)


    从仓库中获取 key 对应的值并将结果提供给回调函数。如果 key 不存在,getItem() 将返回 null。

    localforage.getItem('somekey').then(function(value) {
    // 当离线仓库中的值被载入时,此处代码运行
    console.log(value);
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 回调版本:
    localforage.getItem('somekey', function(err, value) {
    // 当离线仓库中的值被载入时,此处代码运行
    console.log(value);
    });

    设置存储


    setItem(key, value, successCallback)


    将数据保存到离线仓库。你可以存储如下类型的 JavaScript 对象:

    • Array
    • ArrayBuffer
    • Blob
    • Float32Array
    • Float64Array
    • Int8Array
    • Int16Array
    • Int32Array
    • Number
    • Object
    • Uint8Array
    • Uint8ClampedArray
    • Uint16Array
    • Uint32Array
    • String
    localforage
    .setItem("somekey", "some value")
    .then(function (value) {
    // 当值被存储后,可执行其他操作
    console.log(value);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 不同于 localStorage,你可以存储非字符串类型
    localforage
    .setItem("my array", [1, 2, "three"])
    .then(function (value) {
    // 如下输出 `1`
    console.log(value[0]);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 你甚至可以存储 AJAX 响应返回的二进制数据
    req = new XMLHttpRequest();
    req.open("GET", "/photo.jpg", true);
    req.responseType = "arraybuffer";

    req.addEventListener("readystatechange", function () {
    if (req.readyState === 4) {
    // readyState 完成
    localforage
    .setItem("photo", req.response)
    .then(function (image) {
    // 如下为一个合法的 <img> 标签的 blob URI
    var blob = new Blob([image]);
    var imageURI = window.URL.createObjectURL(blob);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });
    }
    });

    删除存储


    removeItem(key, successCallback)


    从离线仓库中删除 key 对应的值。

    localforage.removeItem('somekey').then(function() {
    // 当值被移除后,此处代码运行
    console.log('Key is cleared!');
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    清空存储


    clear(successCallback)


    从数据库中删除所有的 key,重置数据库。


    localforage.clear() 将会删除离线仓库中的所有值。谨慎使用此方法。

    localforage.clear().then(function() {
    // 当数据库被全部删除后,此处代码运行
    console.log('Database is now empty.');
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    localforage是否万事大吉?


    用上了localforage一开始我也以为可以完全满足万恶的产品了,然而。。。翻车了.。


    内存不足的前提下,localforage继续缓存会怎么样?


    在这种状态下,尝试使用localforage,不出意外,抛错了 QuotaExceededError 的 DOMErro


    解决
    存储数据的时候加上存储的时间戳和模块标识,加时间戳一起存储

    setItem({
    value: '1',
    label: 'a',
    module: 'a',
    timestamp: '11111111111'
    })

    • 如果是遇到存储使用报错的情况,try/catch捕获之后,通过判断报错提示,去执行相应的操作,遇到内存不足的情况,则根据时间戳和模块标识清理一部分旧数据(内存不足的情况还是比较少的)
    • 在用户手机上产生脏数据的情况,想要清理的这种情况的 处理方式是:
    • 让后端在用户信息接口里面加上缓存有效期时间戳,当该时间戳存在,则前端会进行一次对本地存储扫描
    • 在有效期时间戳之前的数据,结合模块标识,进行清理,清理完毕后调用后端接口上报清理日志
    • 模块标识的意义是清理数据的时候,可以按照模块去清理(选填)

    作者:前端成长指南
    链接:https://juejin.cn/post/7273028474973012007
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如何消除异步的传染性

    web
    本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。 前言 各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想...
    继续阅读 »

    本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。



    前言


    各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想着来分享一下!好了,接下来笔者会从两个方面来说这个知识点,一方面是概念,另一方面就是如何消除。


    什么是 异步传染性


    笔者通过一个例子来介绍异步传染性的概念。


    CleanShot 2023-08-30 at <a href=10.10.55@2x.png" loading="lazy" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/448eaad1c5f34f319bc3361fcad882ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1234&h=528&s=154964&e=png&b=282a35"/>


    上图中由于m2中的fetch是异步的,导致了使用m2m1变成了async functionmain 又使用了m1,从而main也变成了async function。类似这种现象就叫做异步的传染性。(可能你会觉得,为什么main不直接调m2,我们此处是为了理解这个概念,不要钻牛角尖😁)


    m2就好像病毒🦠,m1明知道到m2有毒,还要来挨着,结果就被传染了,main也是一样。


    那什么是消除传染性呢?就是希望不要 async/await,让mian、m1变成纯函数调用。也就是mian、m1不依赖fetch的状态。期望像下面这样调用:
    CleanShot 2023-08-30 at <a href=10.52.24@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba99c7db117e46319d778002889c51ee~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=924&h=498&s=64284&e=png&b=282a35"/>



    纯函数:



    1. 输入决定输出: 纯函数的输出完全由输入决定,即相同的输入始终产生相同的输出。这意味着函数不依赖于外部状态,也不会对外部状态进行修改。

    2. 没有副作用: 纯函数没有副作用,即在函数的执行过程中不会对除函数作用域外的其他部分产生影响。它不会修改全局变量、改变输入参数或进行文件IO等操作。




    纯函数在函数式编程中具有重要作用,因为它们易于理解、测试和维护。由于不依赖于外部状态,纯函数可以很好地并行执行,也有助于避免常见的错误,例如竞态条件和不确定性行为。



    接下来咱们就分析一下要如何实现消除。


    如何消除


    当我们把async/await去掉之后,就变成了同步调用,那么m2返回的肯定是pending状态的promisemain得到的也是,肯定达不到我们想要的效果。


    那我们能不能等promise变成fulfilled/rejected状态再接着执行main


    可以,第一次调用main,我们直接throw,第一次调用就会终止,然后等promise变成fulfilled/rejected状态,我们将返回结果或错误信息缓存一下,再调用一次main,再次调用时存在缓存,直接返回缓存即可,此时也就变成了同步。流程图如下:


    CleanShot 2023-08-30 at <a href=11.30.26@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc591e6d9bf24b4eb0d3e78fecf5dcc1~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1590&h=1048&s=165391&e=png&b=fdfdfd"/>


    具体实现如下:
    CleanShot 2023-08-30 at <a href=11.34.06@2x.png" loading="lazy" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9b4193e4c6c4a35850c487f1ad0bcbc~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1494&h=1336&s=353606&e=png&b=282a35"/>


    效果如下:
    CleanShot 2023-08-30 at 11.44.35.gif


    到此本次分享的内容就完了,感谢阅读!


    总结


    本文通过简单的例子,描述了什么是异步的传染性,以及如何利用缓存throw重写fetch实现了消除异步的传染性


    如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^<

    作者:Lvzl
    来源:juejin.cn/post/7272751454497996815
    /code>。

    收起阅读 »

    基于 Axios 封装一个完美的双 token 无感刷新

    web
    用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。 标识登录状态的方案有两种: session 和 jwt。 session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出...
    继续阅读 »

    用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。


    标识登录状态的方案有两种: session 和 jwt。


    session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出 cookie 里 id 对应的 session 对象,就可以拿到用户信息。



    jwt 不在服务端存储,会直接把用户信息放到 token 里返回,每次请求带上这个 token,服务端就能从中取出用户信息。



    这个 token 一般是放在一个叫 authorization 的 header 里。


    这两种方案一个服务端存储,通过 cookie 携带标识,一个在客户端存储,通过 header 携带标识。


    session 的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有。



    jwt 的方案天然支持分布式,因为信息保存在 token 里,只要从中取出来就行。



    所以 jwt 的方案用的还是很多的。


    服务端把用户信息放入 token 里,设置一个过期时间,客户端请求的时候通过 authorization 的 header 携带 token,服务端验证通过,就可以从中取到用户信息。


    但是这样有个问题:


    token 是有过期时间的,比如 3 天,那过期后再访问就需要重新登录了。


    这样体验并不好。


    想想你在用某个 app 的时候,用着用着突然跳到登录页了,告诉你需要重新登录了。


    是不是体验很差?


    所以要加上续签机制,也就是延长 token 过期时间。


    主流的方案是通过双 token,一个 access_token、一个 refresh_token。


    登录成功之后,返回这两个 token:



    访问接口时带上 access_token 访问:



    当 access_token 过期时,通过 refresh_token 来刷新,拿到新的 access_token 和 refresh_token



    这里的 access_token 就是我们之前的 token。


    为什么多了个 refresh_token 就能简化呢?


    因为如果你重新登录,是不是需要再填一遍用户名密码?而有了 refresh_token 之后,只要带上这个 token 就能标识用户,不需要传用户名密码就能拿到新 token。


    而 access_token 一般过期时间设置的比较短,比如 30 分钟,refresh_token 设置的过期时间比较长,比如 7 天。


    这样,只要你 7 天内访问一次,就能刷新 token,再续 7 天,一直不需要登录。


    但如果你超过 7 天没访问,那 refresh_token 也过期了,就需要重新登录了。


    想想你常用的 APP,是不是没再重新登录过?


    而不常用的 APP,再次打开是不是就又要重新登录了?


    这种一般都是双 token 做的。


    知道了什么是双 token,以及它解决的问题,我们来实现一下。


    新建个 nest 项目:


     npx nest new token-test


    进入项目,把它跑起来:


    npm run start:dev

    访问 http://localhost:3000 可以看到 hello world,代表服务跑成功了:



    在 AppController 添加一个 login 的 post 接口:



    @Post('login')
    login(@Body() userDto: UserDto) {
    console.log(userDto);
    return 'success';
    }

    这里通过 @Body 取出请求体的内容,设置到 dto 中。


    dto 是 data transfer object,数据传输对象,用来保存参数的。


    我们创建 src/user.dto.ts


    export class UserDto {
    username: string;
    password: string;
    }

    在 postman 里访问下这个接口:



    返回了 success,服务端也打印了收到的参数:



    然后我们实现下登录逻辑:



    这里我们就不连接数据库了,就是内置几个用户,匹配下信息。


    const users = [
    { username: 'guang', password: '111111', email: 'xxx@xxx.com'},
    { username: 'dong', password: '222222', email: 'yyy@yyy.com'},
    ]

    @Post('login')
    login(@Body() userDto: UserDto) {
    const user = users.find(item => item.username === userDto.username);

    if(!user) {
    throw new BadRequestException('用户不存在');
    }

    if(user.password !== userDto.password) {
    throw new BadRequestException("密码错误");
    }

    return {
    userInfo: {
    username: user.username,
    email: user.email
    },
    accessToken: 'xxx',
    refreshToken: 'yyy'
    };
    }

    如果没找到,就返回用户不存在。


    找到了但是密码不对,就返回密码错误。


    否则返回用户信息和 token。


    测试下:


    当 username 不存在时:



    当 password 不对时:



    登录成功时:



    然后我们引入 jwt 模块来生成 token:


    npm install @nestjs/jwt

    在 AppModule 里注册下这个模块:



    JwtModule.register({
    secret: 'guang'
    })

    然后在 AppController 里就可以注入 JwtService 来用了:



    @Inject(JwtService)
    private jwtService: JwtService

    这个是 nest 的依赖注入功能。


    然后用这个 jwtService 生成 access_token 和 refresh_token:



    const accessToken = this.jwtService.sign({
    username: user.username,
    email: user.email
    }, {
    expiresIn: '0.5h'
    });

    const refreshToken = this.jwtService.sign({
    username: user.username
    }, {
    expiresIn: '7d'
    })

    access_token 过期时间半小时,refresh_token 过期时间 7 天。


    测试下:



    登录之后,访问别的接口只要带上这个 access_token 就好了。


    前面讲过,jwt 是通过 authorization 的 header 携带 token,格式是 Bearer xxxx


    也就是这样:



    我们再定义个需要登录访问的接口:


    @Get('aaa')
    aaa(@Req() req: Request) {
    const authorization = req.headers['authorization'];

    if(!authorization) {
    throw new UnauthorizedException('用户未登录');
    }
    try{
    const token = authorization.split(' ')[1];
    const data = this.jwtService.verify(token);

    console.log(data);
    } catch(e) {
    throw new UnauthorizedException('token 失效,请重新登录');
    }
    }

    接口里取出 authorization 的 header,如果没有,说明没登录。


    然后从中取出 token,用 jwtService.verify 校验下。


    如果校验失败,返回 token 失效的错误,否则打印其中的信息。


    试一下:


    带上 token 访问这个接口:



    服务端打印了 token 中的信息,这就是我们登录时放到里面的:



    试一下错误的 token:



    然后我们实现刷新 token 的接口:


    @Get('refresh')
    refresh(@Query('token') token: string) {
    try{
    const data = this.jwtService.verify(token);

    const user = users.find(item => item.username === data.username);

    const accessToken = this.jwtService.sign({
    username: user.username,
    email: user.email
    }, {
    expiresIn: '0.5h'
    });

    const refreshToken = this.jwtService.sign({
    username: user.username
    }, {
    expiresIn: '7d'
    })

    return {
    accessToken,
    refreshToken
    };

    } catch(e) {
    throw new UnauthorizedException('token 失效,请重新登录');
    }
    }

    定义了个 get 接口,参数是 refresh_token。


    从 token 中取出 username,然后查询对应的 user 信息,再重新生成双 token 返回。


    测试下:


    登录之后拿到 refreshToken:



    然后带上这个 token 访问刷新接口:



    返回了新的 token,这种方式也叫做无感刷新。


    那在前端项目里怎么用呢?


    我们新建个 react 项目试试:


    npx create-react-app --template=typescript token-test-frontend


    把它跑起来:


    npm run start


    因为 3000 端口被占用了,这里跑在了 3001 端口。



    成功跑起来了。


    我们改下 App.tsx


    import { useCallback, useState } from "react";

    interface User {
    username: string;
    email?: string;
    }

    function App() {
    const [user, setUser] = useState<User>();

    const login = useCallback(() => {
    setUser({username: 'guang', email: 'xx@xx.com'});
    }, []);

    return (
    <div className="App">
    {
    user?.username
    ? `当前登录用户: ${ user?.username }`
    : <button onClick={login}>登录button>

    }
    div>
    );
    }

    export default App;

    如果已经登录,就显示用户信息,否则显示登录按钮。


    点击登录按钮,会设置用户信息。


    这里的 login 方法因为作为参数了,所以用 useCallback 包裹下,避免不必要的渲染。



    然后我们在 login 方法里访问登录接口。


    首先要在 nest 服务里开启跨域支持:



    在 main.ts 里调用 enbalbeCors 开启跨域。


    然后在前端代码里访问下这个接口:


    先安装 axios


    npm install --save axios

    然后创建个 interface.ts 来管理所有接口:


    import axios from "axios";

    const axiosInstance = axios.create({
    baseURL: 'http://localhost:3000/',
    timeout: 3000
    });

    export async function userLogin(username: string, password: string) {
    return await axiosInstance.post('/login', {
    username,
    password
    });
    }

    async function refreshToken() {

    }
    async function aaa() {

    }

    在 App 组件里调用下:


    const login = useCallback(async () => {
    const res = await userLogin('guang', '111111');

    console.log(res.data);
    }, []);

    接口调用成功了,我们拿到了 userInfo、access_token、refresh_token



    然后我们把 token 存到 localStorage 里,因为后面还要用。


    const login = useCallback(async () => {
    const res = await userLogin('guang', '111111');

    const { userInfo, accessToken, refreshToken } = res.data;

    setUser(userInfo);

    localStorage.setItem('access_token', accessToken);
    localStorage.setItem('refresh_token', refreshToken);
    }, []);


    在 interface.ts 里添加 aaa 接口:


    export async function aaa() {
    return await axiosInstance.get('/aaa');
    }

    组件里访问下:



    const xxx = useCallback(async () => {
    const res = await aaa();

    console.log(res);
    }, []);


    点击 aaa 按钮,报错了,因为接口返回了 401。


    因为访问接口时没带上 token,我们可以在 interceptor 里做这个。


    interceptor 是 axios 提供的机制,可以在请求前、响应后加上一些通用处理逻辑:



    添加 token 的逻辑就很适合放在 interceptor 里:



    axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
    config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
    })

    现在再点击 aaa 按钮,接口就正常响应了:



    因为 axios 的拦截器里给它带上了 token:



    那当 token 失效的时候,刷新 token 的逻辑在哪里做呢?


    很明显,也可以放在 interceptor 里。


    比如我们改下 localStorage 里的 access_token,手动让它失效。



    这时候再点击 aaa 按钮,提示的就是 token 失效的错误了:



    我们在 interceptor 里判断下,如果失效了就刷新 token:


    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    async (error) => {
    let { data, config } = error.response;

    if (data.statusCode === 401 && !config.url.includes('/refresh')) {

    const res = await refreshToken();

    if(res.status === 200) {
    return axiosInstance(config);
    } else {
    alert(data || '登录过期,请重新登录');
    }
    } else {
    return error.response;
    }
    }
    )

    async function refreshToken() {
    const res = await axiosInstance.get('/refresh', {
    params: {
    token: localStorage.getItem('refresh_token')
    }
    });
    localStorage.setItem('access_token', res.data.accessToken);
    localStorage.setItem('refresh_token', res.data.refreshToken);
    return res;
    }

    响应的 interceptor 有两个参数,当返回 200 时,走第一个处理函数,直接返回 response。


    当返回的不是 200 时,走第二个处理函数 ,判断下如果返回的是 401,就调用刷新 token 的接口。


    这里还要排除下 /refresh 接口,也就是刷新失败不继续刷新。


    刷新 token 成功,就重发之前的请求,否则,提示重新登录。


    其他错误直接返回。


    刷新 token 的接口里,我们拿到新的 access_token 和 refresh_token 后,更新本地的 token。


    测试下:


    我手动改了 access_token 让它失效后,点击 aaa 按钮,发现发了三个请求:



    第一次访问 aaa 接口返回 401,自动调了 refresh 接口来刷新,之后又重新访问了 aaa 接口。


    这样,基于 axios interceptor 的无感刷新 token 就完成了。


    但现在还不完美,比如点击按钮的时候,我同时调用了 3 次 aaa 接口:



    这时候三个接口用的 token 都失效了,会刷新几次呢?



    是 3 次。


    多刷新几次也没啥,不影响功能。


    但做的再完美一点可以处理下:



    加一个 refreshing 的标记,如果在刷新,那就返回一个 promise,并且把它的 resolve 方法还有 config 加到队列里。


    当 refresh 成功之后,重新发送队列中的请求,并且把结果通过 resolve 返回。


    interface PendingTask {
    config: AxiosRequestConfig
    resolve: Function
    }
    let refreshing = false;
    const queue: PendingTask[] = [];

    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    async (error) => {
    let { data, config } = error.response;

    if(refreshing) {
    return new Promise((resolve) => {
    queue.push({
    config,
    resolve
    });
    });
    }

    if (data.statusCode === 401 && !config.url.includes('/refresh')) {
    refreshing = true;

    const res = await refreshToken();

    refreshing = false;

    if(res.status === 200) {

    queue.forEach(({config, resolve}) => {
    resolve(axiosInstance(config))
    })

    return axiosInstance(config);
    } else {
    alert(data || '登录过期,请重新登录');
    }
    } else {
    return error.response;
    }
    }
    )

    axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
    config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
    })

    测试下:



    现在就是并发请求只 refresh 一次了。


    这样,我们就基于 axios 的 interceptor 实现了完美的双 token 无感刷新机制。


    总结


    登录状态的标识有 session 和 jwt 两种方案。


    session 是通过 cookie 携带 sid,关联服务端的 session,用户信息保存在服务端。


    jwt 是 token 保存用户信息,在 authorization 的 header 里通过 Bearer xxx 的方式携带,用户信息保存在客户端。


    jwt 的方式因为天然支持分布式,用的比较多。


    但是只有一个 token 会有过期后需要重新登录的问题,为了更好的体验,一般都是通过双 token 来做无感刷新。


    也就是通过 access_token 标识用户身份,过期时通过 refresh_token 刷新,拿到新 token。


    我们通过 nest 实现了这种双 token 机制,在 postman 里测试了一下。


    在 react 项目里访问这些接口,也需要双 token 机制。我们通过 axios 的 interceptor 对它做了封装。


    axios.request.interceptor 里,读取 localStorage 里的 access_token 放到 header 里。


    axios.response.interceptor 里,判断返回的如果是 401 就调用刷新接口刷新 token,之后重发请求。


    我们还支持了并发请求时,如果 token 过期,会把请求放到队列里,只刷新一次,刷新完批量重发请求。


    这样,就是一个基于 Axios 的完美的双 token 无感刷新了。

    作者:zxg_神说要有光
    来源:juejin.cn/post/7271139265442021391

    收起阅读 »

    求求别再叫我切图仔了,我是前端开发!

    web
    ☀️ 前言 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。 群友2: 没有耕坏的田,只有累死...
    继续阅读 »

    ☀️ 前言





    • 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。



      • 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。

      • 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。

      • 群友2: 没有耕坏的田,只有累死的牛啊,老哥!🐮。

      • 群友3: 用CodeFun啊,分分钟解决你这种外包需求。

      • 群友2: 对喔!可以试一下CodeFun,省下来的时间开黑去。




    • 在我印象中智能生成页面代码的工具一般都不这么智能,我抱着怀疑的心态去调研了一下CodeFun看看是不是群友们说的这么神奇,试用了过后发现确实挺强大的,所以这次借此机会分享给大家。




    🤔 什么是 CodeFun



    • 大部分公司中我们前端现在的开发工作流大概是下面这几步。

      • 一般会有UI先根据产品提供的原型图产出设计稿。

      • 前端根据设计稿上的标注(大小,边距等)进行编写代码来开发。

      • 开发完后需要给UI走查来确认是不是他/她想要的效果。

      • 如果发现有问题之后又继续重复上面的工作->修改样式->走查。






    • 我们做前端的都知道,重复的东西都可以封装成组件来复用,而上面这种重复的劳作是我们最不想去做的。

    • 但是因为设计图的精细可能有时候会有1px的差异就会让产品UI打回重新编写代码的情况,久而久之就严重影响了开发效率。

    • 我时常会有这么一种疑惑,明明设计稿上都有样式了,为什么还要我重新手写一遍呢?那么有没有一种可能我们可以直接通过设计稿就自动生成代码呢?

    • 有的!通过我的调研过后发现,发现确实CodeFun在同类产品中更好的解决了我遇到的问题。




    • CodeFun是一款 UI 设计稿智能生成源代码的工具,可以将 SketchPhotoshopFigma 的设计稿智能转换为前端源代码。

    • 8 小时工作量,10 分钟完成是它的slogan,它可以精准还原设计稿,不再需要反复 UI 走查,我觉得在使用CodeFun后可以极大地程度减少工作流的复杂度,让我们的工作流变成以下这样:

      • UI设计稿产出。

      • CodeFun产出代码,前端开发略微修改交付。





    🖥 CodeFun 如何使用



    • 接下来我就演示一下如何快速的根据设计稿来产出前端代码,首先我们拿到一个设计稿,这里我就在网上搜了一套Figma的设计稿来演示。

    • 我们在Figma中安装了一个CodeFun的插件,选择对应CodeFun的项目后点击上传可以看到很轻松的就传到我们的CodeFun项目中,当然除了FigamaCodeFun还支持Sketch,PSD,即时设计等设计稿。

    • 我们随便进入一个页面,引入眼帘的是中间设计稿,而在左侧的列表相当于这个页面的节点,而我们点击一下右上角的生成代码可以看到它通过自己的算法很智能的生成了代码。

    • 我上面选择生成的是React的代码,当然啦,他还有很多种选择微信小程序Vueuni-app等等等等,简直就是多端项目的福音!不止是框架,连Css预处理器都可以选择适合自己的。

    • 将生成的代码复制到编辑器中运行,可以看到对于简单的页面完全不用动脑子,直接就渲染出来我们想要的效果了,如果是很复杂的页面进行一些微调即可,是不是很方便嘿嘿。

    • CodeFun不管是根据你选择的模块进行生成代码还是整页生成代码用户进行复制使用之外,它还提供了代码包下载功能,在下载界面可以选择不同页面,不同框架,不同Css预处理器,不同像素单位

    • 如果是React相关甚至还会帮你把脚手架搭建好,直接下载安装依赖使用即可,有点牛呀。



    🔥 CodeFun 好在哪



    • 笔者在这之前觉得想象中的AI生成前端代码的功能一直都挺简陋,用起来不会到达我的预期,到底能不能解决我的痛点,其实我是有以下固有思想的:

      • 生成代码就是很简单的帮你把HtmlCss写完嘛但是我们不同框架又不能生成。

      • 生成代码的变量名肯定不好看。

      • 生成的代码肯定固定了宽高,不同的手机端看的效果会差很多。

      • 平时习惯了v-for,wx:for,map遍历列表,这种生成代码肯定全部给你平铺出来吧。



    • 但是当我使用过CodeFun之后发现确实他可以解决我们很多的重复编写前端页面代码的场景,而且也打消了我对这类AI生成前端页面代码功能的一些固有思想,就如它的slogan所说:8 小时工作量,10 分钟完成


    多平台、多框架支持



    • 支持 Vue 等主流 Web 开发框架代码输出。

    • 支持微信小程序代码输出,当你选择小程序代码输出时,像素单位会新增一个rpx的选项供大家选择。

    • 使用最简单的复制代码功能,我们可以快速的将我们想要的样式复制到我们的项目中进行使用 。

    • 笔者在使用的过程中一直很好奇下载代码的功能,如果我选择了React难不成还会给我自动生成脚手架?结果一试,还真给我生成了脚手架,只需要安装依赖即可,可以说是很贴心了~。



    循环列表自动输出



    • 我们平时在写一个列表组件的时候都喜欢使用v-for,wx:for,map等遍历输出列表,而CodeFun也做到了这种代码的生成。

    • CodeFun在导入设计稿的时候会自动识别哪些是list组件,当然你也可以手动标记组件为List

    • 然后再开启“将 List 标签输出为循环列表”选项即可自动根据当前选择的框架生成对应的循环遍历语法,确实是很智能了~



    批量数据绑定




    • 在我们平时Coding的过程中都不会把数据写死,而是用变量来代替进行动态渲染,而CodeFun支持批量数据绑定功能,我们可以把任何在页面中看到的元素进行数据绑定和命名修改




    • 就拿上面的循环列表举例吧,在我们一开始识别的Html中,遍历循环了一个typeCards数组,每一个都展示对应的信息,我们可以看到这里一开始是写死的,而我们平时写的时候会将它用变量替代。




    • 我们只需要点击右上角的数据绑定进行可视化修改即可,我们可以看到它的全部写法都改成了变量动态渲染,这就很符合我们平时编码的套路了。





    一键预览功能



    • 有很多同学反馈在之前做小程序的情况下需要将代码编写完整并跑起来的情况下,使用微信的预览功能才可以看到效果,会比较繁琐

    • CodeFun支持直接预览,当我们导入设计稿后,选择右上角的预览功能可以直接生成小程序二维码扫码即可进行预览,好赞!。



    更加舒适的“生成代码”



    • CodeFun生成的代码中是会让人看起来比较舒适的。

      • 变量名可读性会比较强。

      • 布局一般不会固定死宽高,而是使用padding等属性来自适应屏幕百分比

      • 自动处理设计稿中的无用图层、不可见元素、错误的编组乃至不合理的文字排列。

      • 全智能切图,自动分离背景图层、图标元素。




    ✍🏻 一些思考与建议



    • 前端开发不仅仅是一个切图的工具人,如果你一直局限于视图的表现的时候,你的前端水平也就是curd工程师的水平了,我们前端更多的要深入一些性能优化前端插件封装等等有意思的事情🙋🏻。

    • 总之如果你想你的前端水平要更加精进的情况下,可以减少一些在页面上的投入时间,现在的工具越来越成熟,而这些切图工作完全可以交给现有的工具去帮助你完成

    • 在使用体验上来说,CodeFun确实可以解决大部分切图功能,减少大家进行切图的工作时间,大家可以去试一下~但是肯定会有一些小细节不符合自己的想法,表示理解吧,毕竟AI智能生成代码能做成CodeFun这种水平已经很厉害了👍🏻。

    • 在使用建议上来说,我建议大家可以把CodeFun当成一个助手,而不要完全依赖,过度依赖,去找到更合适自己使用CodeFun的使用方法可以大量减少开发时间从而去做👉🏻更有意义的事情。

    • 很多人会很排斥,觉得没自己写的好,但是时代已经变啦~我还是那句话,所有东西都是一个辅助,一个工具,它提供了这些优质的功能而使用的好不好是看使用者本身的,欢迎大家去使用一下CodeFun~支持国产!!




    • 记住我们是前端开发,不是切图仔!做前端,不搬砖!



    作者:快跑啊小卢_
    来源:juejin.cn/post/7145977342861508638

    收起阅读 »

    为了弄清楚几个现象,重新学习了 flex

    web
    flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点: 相对于常规布局(float, position),它具备更高的灵活性; 相对于 grid 布局,它具有更强的兼容性; 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈) ...
    继续阅读 »

    flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点:



    • 相对于常规布局(float, position),它具备更高的灵活性;

    • 相对于 grid 布局,它具有更强的兼容性;

    • 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈)


    但是在开发使用 flex 布局的过程中,也会遇到一些自己难以解释的现象;通俗表述:为什么会是这样效果,跟自己想象的不一样啊?


    那么针对自己提出的为什么,自己有去研究过?为什么是这样的效果?如何解决呢?


    自己也存在同样的问题。所以最近有时间,重新学习了一遍 flex,发现自己对 flex 的某些属性了解少之又少,也就导致针对一些现象确实说不清楚。


    下面我就针对自己遇到的几种疑惑现象进行学习,来对 flex 部分属性深入理解。



    每天多问几个为什么,总有想象不到的意外收获 ---我的学习座右铭



    回顾 flex 模型


    flex 的基本知识和基本熟悉就不介绍了,只需要简单的回顾一下 flex 模型。


    在使用 flex 布局的时候,脑海中就要呈现出清晰的 flex 模型,利于正确的使用 flex 属性,进行开发。


    flex1.png

    理解如下的几个概念:



    • 主轴(main axis)

    • 交叉轴(cross axis)

    • flex 容器(flex container)

    • flex 项(flex item)



    main size 也可以简单理解下,后面内容 flex-basis 会涉及到。



    还顺便理解一下 flex-item 的基本特点



    1. flex item 的布局将由 flex container 属性的设置来进行控制的。

    2. flex item 不在严格区分块级元素和行内级元素。

    3. flex item 默认情况下是包裹内容的,但是可以设置的高度和宽度。


    现象一:flex-wrap 换行引起的间距


    关键代码:


    <!-- css -->
    <style>
     .father {
       width: 400px;
       height: 400px;
       background-color: #ddd;
       display: flex;
       flex-wrap: wrap;
    }
     .son {
       width: 120px;
       height: 120px;
    }
    </style>

    <!-- html -->
    <body>
     <div class="father">
       <div class="son" style="background-color: aqua">1</div>
       <div class="son" style="background-color: blueviolet">2</div>
       <div class="son" style="background-color: burlywood">3</div>
       <div class="son" style="background-color: chartreuse">4</div>
     </div>
    </body>

    具体现象:


    flex2.png

    疑惑:为什么使用 flex-wrap 换行后,不是依次排列,而是排列之间存在间距?



    一般来说,父元素的高度不会固定的,而是由内容撑开的。但是我们也不能排除父元素的高度固定这种情况。



    排查问题:针对多行,并且在交叉轴上,不能想到是 align-content 属性的影响。但是又由于代码中根本都没有设置该属性,那么问题肯定出现在该属性的默认值身上。


    那么通过 MDN 查询:


    flex3.png

    align-content 的默认值为 normal,其解释是按照默认位置填充。这里默认位置填充到底代表什么呢,MDN 上没有明确说明。


    但是在 MDN 上查看 align-items 时,却发现了有用的信息(align-items 是单行,align-content 是多行),normal 在不同布局中有不同的表现形式。


    flex4.png

    可以发现,针对弹性盒子,normal 与 stretch 的表现形式一样。


    自己又去测试 align-content,果然发现 normal 和 stretch 的表现形式一样。那么看看 stretch 属性的解释:


    flex6.png

    那么只需简单的需改,去掉 height 属性,那么 height 属性默认值就为 auto。


    <!-- css -->
    <style>
     .son {
       width: 120px;
       /* 注释掉 height */
       /* height: 120px */
    }
    </style>

    看效果:


    flex5.png

    可以发现,子元素被拉伸了,这是子元素在默认情况下应该占据的空间大小。



    这里就需要理解 flex item 的特点之一:flex item 的布局将由 flex container 属性的设置来进行控制的



    那么当子元素设置高度时,是子元素自己把自己的高度限制了,但是并没有改变 flex container 对 flex item 布局占据的空间大小,所以就会多出一点空间,也就是所谓的间距。


    所以针对上面这个案例,换行存在间隔的现象也就理解了,因为第四个元素本身就排布在弹性盒子的正确位置,只是我们把子元素高度固定了,造成的现象是有存在间隔。



    可以想一下,如果子元素的高度加起来大于父元素的高度,又是什么效果呢?可以自己尝试一下,看自己能够解释不?



    现象二:flex item 拉伸?压缩?


    在使用 flex 时,最常见的现象是这样的:


    flex7.png

    当子元素为 3 个时,不会被拉伸,为什么呢?


    当子元素为 6 个事,会被压缩,又是为什么呢?


    其实上面这两个疑问❓,只需了解两个属性:flex-growflex-shrink。因为这两个属性不常用,所以容易忽略,从而不去了解,那么就会造成疑惑。


    flex-grow 属性指定了 flex 元素的拉伸规则。flex 元素当存在剩余空间时,根据 flex-grow 的系数去分配剩余空间。 flex-grow 的默认值为 0,元素不拉伸


    flex-shrink 属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值。flex-shrink 的默认值为 1,元素压缩



    该两个属性都是针对 主轴方向的剩余空间



    所以



    • 当子元素数量较少时,存在剩余空间,但是又由于 flex-grow 的值为 0,所以子元素宽度不会进行拉伸。

    • 当子元素数量较多时,空间不足,但是又由于 flex-shrink 的值为 1,那么子元素就会根据相应的计算,来进行压缩。


    特殊场景: 当空间不足时,子元素一定会压缩?试试单词很长(字符串很长)的时候呢?


    flex8.png

    现象三:文本溢出,flex-basis?width?


    在布局中,如果指定了宽度,当内容很长的时候,就会换行。但是会存在一种特殊情况,就是如果一个单词很长为内容时,则不会进行换行;跟汉字是一样的道理,不可能从把汉字分成两半。


    那么在 flex 布局中,会存在两种情况:


    flex9.png

    可以发现:



    • 设置了固定的 width 属性,字符串超出宽度之后,就会截取。

    • 而设置了固定的 flex-basis 属性,字符串超出宽度之后,会自动扩充宽度。


    其实在这里可能有人会有疑惑:为什么把 width 和 flex-basis 进行对比?或者说 flex-basis 这个属性到底是干什么?



    其实我也是刚刚才熟悉到这个属性,哈哈哈,不知道吧!!!



    因为 flex-basis 是使用在 flex item 上,而 flex-basis(主轴上的基础尺寸)属性在大多数情况下跟 width 属性是等价的,都是设置 flex-item 的宽度。


    上面的案例就是属于特殊情况,针对单词超长不换行时,flex-basis 就会表现出不一样的形式,自动扩充宽度


    简单学习一下 flex-basis 的基本语法吧。


    flex-basis 属性值:



    • auto: 默认值,参照自身 width 或者 height 属性。

    • content: 自动尺寸,根据内容撑开。

    • <'width'>: 指定宽度。


    当一个属性同时设置 flex-basis(属性值不为 auto) 和 width 时,flex-basis 具有更高的优先级


    现象四:flex 平分


    当相对父容器里面的子元素进行平分时,我们会毫不犹豫的写出:


    .father {
     width: 400px;
     height: 400px;
     background-color: #ddd;
     display: flex;
    }
    .son {
     flex: 1; /* 平分 */
     height: 90px;
    }

    flex10.png

    那么我们是否会想过为什么会平分空间? 其中 flex:1 起了什么作用?


    我们也许都知道 flex 属性是一个简写,是 flex-growflex-shrinkflex-basis 的简写。所以,flex 的属性值应该是三个组合值。


    但是呢,flex 又类似于 font 属性一样,是一个很多属性的简写,其中一些属性值是可以不用写的,采用其默认值。


    所以 flex 的属性值就会分析三种情况:一个值,两个值,三个值。


    MDN 对其做了总结:


    flex11.png

    看图,规则挺多的,如果要死记的话,还是挺麻烦的。


    针对上面的规则,其实只需要理解 flex 的语法形式,还是能够完全掌握(有公式,谁想背呢)。


    flex = none | auto | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]  

    希望你能看懂这个语法,很多 api 都有类似的组合。



    • | 表示要么是 none, 要么是 auto, 要么是后面这一坨,三选一。

    • || 逻辑或

    • ? 可选


    理解上面这种语法之后,总结起来就是如下:


    一个值



    1. none(0 0 auto)auto(1 1 auto) 是需要单独记一下的,这个无法避免。

    2. 无单位,就是 flex-grow,因为存在单位,就是 flex-grow 属性值规定为一个 number 类型的

    3. 有单位,就是 flex-basis,因为类似 width 属性是需要单位的,不然没有效果。


    两个值



    1. 无单位,就是 flex-grow 和 flow-shrink,理由如上

    2. 其中一个有单位,就是 flex-grow 和 flex-basis,因为 flex-shrink 是可选的(这种情况是没有任何实际意义的,flex-basis设置了根本无效)。


    三个值


    三个值不用多说,一一对应。


    理解了上面的语法形式,再来看 flex: 1 的含义就轻而易举了。一个值,没有单位,就是 flex-grow,剩余空间平均分配


    现象五:多行,两边对齐布局


    无论是 app 开发,还是网页开发,遇到最多的场景就是这样的:


    flex12.png

    两边对齐,一行元素之间的间距相同;如果一行显示不下,就换行依次对齐排布。


    那么不难想到的就是 flex 布局,会写下如此代码:


    .father {
     display: flex;
     justify-content: space-between;
     flex-wrap: wrap;
    }
    .son {
     width: 90px;
     height: 90px;
    }

    那么你就会遇到如下情况:


    flex13.png

    其中的第二、三种情况布局是不可以接受,数据数量不齐的问题。但是数据是动态的,所以不能避免出现类似情况。


    你们遇到过这种类似的布局吗?会存在这种情况吗?是怎么解决的呢?


    第一种解决方案:硬算


    不使用 flex 的 justify-content 属性,直接算出元素的 margin 间隔。


    .father {
     width: 400px;
     background-color: #ddd;
     display: flex;
     flex-wrap: wrap;
    }
    .son {
     margin-right: calc(40px / 3); /* 40px 为 父元素的宽度 - 子元素的宽度总和,   然后平分剩余空间*/
     width: 90px;
     height: 90px;
     background-color: #5adacd;
     margin-bottom: 10px;
    }
    /* 针对一行最后一个,清空边距 */
    .son:nth-child(4n) {
     margin-right: 0;
    }

    缺点:只要其中的一个宽度发生变化,又要重新计算。


    第二种解决方案:添加空节点


    为什么要添加空节点呢?因为在 flex 布局中,没有严格的区分块级元素和行内元素。那么就可以使用空节点,来占据空间,引导正确的布局。


    <style>
     .father {
       width: 400px;
       background-color: #ddd;
       display: flex;
       justify-content: space-between;
       flex-wrap: wrap;
    }
     .son {
       width: 90px;
       height: 90px;
       background-color: #5adacd;
       margin-bottom: 10px;
    }
     
     /* height 设置为 0 */
     .father span {
       width: 90px; /*空节点也是 flex-item, width 是必须一致的,只是设置高度为0,不占据空间*/
    }
    </style>
    </head>
    <body>
     <div></div>
     <div class="father">
       <div class="son">1</div>
       <div class="son">2</div>
       <div class="son">3</div>
       <div class="son">4</div>
       <div class="son">5</div>
       <div class="son">6</div>
       <div class="son">7</div>
       <!-- 添加空节点,个数为 n-2 -->
       <i></i>
       <i></i>
     </div>
    </body>

    这样也能解决上面的问题。


    添加空节点的个数:n(一行的个数) - 2(行头和行尾,就是类似第一种情况和第四种情况本身就是正常的,就不需要空间点占据)


    缺点:添加了dom节点


    上面两种方案都解决问题,但是都有着各自的缺点,具体采用哪种方式,就看自己的选择了。


    那么你们还有其他的解决方案吗?


    总结


    其实本篇所解释的现象是自己对 flex 知识掌握不牢而造成的,从而记录此篇,提升熟悉度。也希望能够帮助对这些现象有困惑的码友。


    作者:copyer_xyf
    来源:juejin.cn/post/7273025171111444540
    >如果存在错误解释,评论区留言。

    收起阅读 »

    第一份只干了五天的前端工作

    可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。我当时问了五险一金说是按照7000多交,结果公积金按最低2千多我去的第三天,后端管理A,负责给我分配任...
    继续阅读 »

    可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。

    • 当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。
    • 我当时问了五险一金说是按照7000多交,结果公积金按最低2千多
    • 我去的第三天,后端管理A,负责给我分配任务的,他早上和我讲,类似初创公司领导不愿意看到早下班,晚上也会找我过一下进度啥的,要定制一个学习计划,自己找时间去熟悉代码...

    面试过程倒还顺利,前端A问了些技术,比较偏实际应用的,对我github上做的东西也感兴趣。前端管理B问的有些偏人事的,然后问我下午有没有其它安排,没有的话不走流程叫领导A面一下。领导A就直接说了平薪,试用期3个月80%能不能接受,当时那种场景我就顺势而为说可以,我之前的工作都是少一千。


    我往回走的时候hr给我打电话,前面的没接到,我打过去得知是想问我在哪,想问一下总裁有没有时间,不然就要到周六。


    等到周六面完,下周一打电话说通过,让我周三去报到,我说太快要下周一。然后这家公司每个月月末的周六是组织学习培训的,我也参加了。


    第一天搭建环境,账号权限这些,电脑连不上WiFi,我问前台有没有网线,她说没有我也没去下面找,又换了一台,接口就不同了,又弄双屏的转接线。我找她一次,她都是从前台去存放物品的地方,发现不太对再找她已经回前台了,前后不到一分钟的时间吧。确实我对这个也不太熟,有些不好意思麻烦她,最后少一个转接器我就自己买了,否则屏幕显示不清晰,顺便买了两个增高架。


    前端管理B给我说了几个项目,我看了会代码,管理B找我谈了一会。


    九点上班六点下班,上班第一天我看快到六点半了就准备走,这时候管理B拉着我去展厅看了下公司的产品。


    第二天后端管理A请假了,前端管理B给我说了会代码,后面主要负责的项目,跑了下本地联调。


    基本上一个菜单一个项目,每个项目用iframe嵌套,然后有一些组件库这些,代码之间组件嵌套的比较深,多以component。数据走的是配置,流向很乱。接口的传递和返回都很庞大,有些还是json字符串,20-60多个kb,看结构话要单独复制出来。一些项目调试没有sourcemap,给我的感觉就是把简单的事做复杂了。


    第三天后端管理A给我安排了一些事情,就是口头说了一下,意思这种改动不需要ui、不需要产品自己就可以定,基本上就是我做完让他看一下,他觉得不好在改。这里面就有一个问题,就是到底改哪里没有全部列出来,我对项目又不熟,一般都有一个上下文的概念,可对不上的时候,才发现是另一个地方,他又是没有规划的那种。


    往往走配置基本会出现一种情况,就是一些东西需要单独处理,或者配置选项越加越多,或者当初实现的时候偷懒就给写死了,东改一下西改一下,而且这种封装太笨重了,不好优化,只能说熟悉了更快些,但是维护成本始终会处在一个固定的量级,而且随着功能迭代,补丁会越来越多。


    下午的时候管理B把我叫过去,让我协助后台A排查两个问题,说别的环境没有只有这个环境有,据说问题已经存在很久了,我找到问题A的代码所在地、以及问题的原因就花了很长时间,单从前端看,是因为一个代码报错导致的。因为这些东西要按照业务流程来,我不知道什么是对的,只能关注接口的返回然后找对应的前端组件渲染逻辑,对比差异反馈,本质上就是反推接口返回有哪些不对,找到问题已经晚上10点了,两个问题都是后台在处理用户操作后,前后id不一样导致拿到的数据不对所致。


    这种问题让一个刚入职的人排查显然浪费时间,中间链路太多了,我问了下后端A他之前都是和谁对接,得知前面的人离职了,我就想着这种情况是最难受的,总不可能我一上来就能接手他的工作,巧妇难为无米之炊,哪有那么丝滑的过度,总要有个渐进性的过程吧。


    每天要建tapd,再把tapd的内容复制出来写成日报,然后也要写周报,还要在领导有时间的时候找他汇报进度,或者等他来找你。


    第四天,后端管理A给我说了下今天的任务,有一些历史遗留问题,我处理的还是很快的,直到前端写完对接口的时候,他只是钉钉发了一些字段给我,发现还有另一个项目要改,找到代码熟悉,对好逻辑写好前端代码,我又在本地连了下测试环境,跑了下流程,接口报错了,我看六点半了就走了。


    第四天上午还过了个需求,虽然我也听不太懂,但是管理B直接说这个事情15个自然日还是工作日搞完。


    后端管理A评价我的日报,意思任务完不成要及时上报,晚上要和他汇报进度,我想着我都不知道一天到底有多少任务,也不知道完成任务花费多长时间,更不知道啥样算完成,我咋完成?


    第五天,前端管理B找我聊了一会,说是来了一周,我就把我的感触说了,他问我打算怎么处理,我就说这种强度我就不干了,感觉不值,他说他来处理。


    我对比了下我能够得到的和我将要面对的,平薪80%,三个月,我觉得有些不值得,待遇还不如我两年前,也没到山穷水尽的地步,受这罪干嘛?以前入职也不是没有压力大的时候,但待遇有所增长,看了代码啥的我也觉得对我是一种历练,即便不说我也会主动学习,因为我知道当我很熟悉的时候后面效率更高,算提早付出了,还是在时间不那么紧凑的时候,但这家公司给我一种压榨的感觉。


    后面我把项目分支发给后端管理A,部署发版耽搁了一会,后面是找的前端管理B解决的,我后面了解到走的是自己的搭建的运维系统,两个项目有不同的分支名,我把自己的分支手动合并,再找后端管理A就好了。


    接下来就是他发现一些问题让我改,持续到下午五点半左右,我再次提交代码时,发现gtilab账号已经被注销了,他让我把代码改动发给前端管理B,这个时候我的电脑已经重置,被前台收走了。还是有些遗憾,再次改动时发现轻车熟路了许多,前面还是花了不少精力的。


    公司提供了午休床,我贴了个标签,前端管理B贴心的给我个东西增加区分性,但我用过一次后没再找见,离职也是要交给前台的,找的时候还在想不会所有放午休床的地方都要找一遍吧,还好发现了破碎的标签,应该被别人用的时候弄碎了。


    其实周五不聊的话我可能想着再适应下,也没想到当天就能走完离职,清理tapd的时候发现有五十多个bug挂在我这,看到一个六月份的。


    幸亏我带了包过去,不然东西都不好拿,看着8月份4天32小时...。


    可能是太久没工作了吧,我便抱着试一试的态度,坦白的讲我也想过边干边找,入职的这几天有了新的方向,我github上写的工具依旧发挥稳定,替我节省了很多时间。


    作者:这次假期很长
    链接:https://juejin.cn/post/7263653558385868857
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    某法宝网站 js 逆向思路

    web
    本文章只做技术探讨, 请勿用于非法用途。 目标网站 近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。 本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。 网站分析 文章内容 详情页图片 可以看到下载的方式还蛮多的,...
    继续阅读 »

    本文章只做技术探讨, 请勿用于非法用途。



    目标网站


    近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。


    本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。


    网站分析


    文章内容


    image.png
    详情页图片


    可以看到下载的方式还蛮多的, 尝试复制全文, 得到内容保留原格式, 所以我选择使用复制全文的方式来得到文章内容。


    同时详情页没有看到明显的反扒措施, 不需要特殊处理。


    列表页


    image.png


    最终决定通过专题分类来获取所有的数据, 然后调试分析接口参数, 这里没有什么加密的参数, 确定需要关注的参数如下:


    {
    "Aggs.SpecialType": "", // 专题类型(编号)
    "VerifyCodeResult": "", // 验证码值(后边讲解)
    "Pager.PageIndex": "2", // 页码
    "RecordShowType": "List", // 显示方式(List 方式显示所有数据, 保持该值即可)
    "Pager.PageSize": 100, // 每页的数量(最大 100)
    }


    这里仅列出了需要关注的参数, 其他参数保持原值即可(需要的话可以自己调试对比参数值确定意义), 当 pageSize 设置为 100 时, 第 3 页之后的数据需要验证码才能查看。


    验证码


    为方便分析验证码的校验方式, 推荐使用无痕模式来调试, 获取验证码之前清空一次 cookie。


    image.png


    image.png


    image.png


    可以看到验证码的流程为:



    1. 请求验证码, 并返回 set-cookie 。

    2. 携带该 cookie 信息并校验验证码, 通过后得到 code。

    3. 携带 code 请求数据, 得到数据。


    开整


    确定了问题后, 就可以开始了, 一个一个解决。


    文章内容


    确定了要用复制的方式来得到数据, 那就分析下他的复制干了点啥。


    image.png


    查看他的页面元素, 发现了这两个东西, 全局搜索后很容易定位到处理函数(找不到的话刷新页面)。


    image.png


    然后接着定位这个 toCopyFulltext() 函数。


    image.png


    找到这个就对了, 然后可以看代码他是用的 jQuery 的选择器来做的, 我们只能来仿造一个页面结构来用它, 这里推荐使用 cheerio(nodejs) 库来做(jsdom 应该也可以?)。


    image.png


    伪造后直接调用就大功告成。


    列表页


    这个问题不大, 问题主要在于验证码。


    image.png


    通过查看页面元素可以得到专题编号。


    验证码


    请求


    // 验证码图片请求返回结果
    {
    "errcode": 0,
    "y": 131,
    "array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10",
    "imgx": 300,
    "imgy": 200,
    "small": "data:image/jpg;base64,...", // 滑块图片
    "normal": "data:image/jpg;base64,..." // 背景图片
    }

    image.png
    得到的 base64 可以用 python 的 PIL 库来解析出来。


    image.png
    然后我解析出来的图片就是这个样子, 基本上就确定了这验证码就是老朋友了, 我们先来把他还原。



    还是简单解释一下这个, 图片被切割为了上下两部分, 每个部分又被切割为了 10 等份, 原图为 300x200, 也就是说这里被切割为 20 张 30x100 的图片, 然后打乱顺序拼接后返回, 我们需要先完成切割然后再按正确的顺序来拼接还原。



    "errcode": 0,
    "y": 131,
    "array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10", // 图片的正确顺序
    "imgx": 300, // 图片宽
    "imgy": 200, // 图片高
    "small": "data:image/jpg;base64,...", // 滑块图片
    "normal": "data:image/jpg;base64,..." // 背景图片

    image.png


    image.png


    还原后的图片就是这个样子了。


    校验


    // 校验接口请求参数
    {
    "act": "check",
    "point": "197", // 缺口位置
    "timespan": "1067", // 滑动耗时
    "datelist": "1,1692848585282|10,1692848585288|20,1692848585295|34,1692848585305|50,1692848585312|58,1692848585320|74,1692848585328|90,1692848585338|95,1692848585344|107,1692848585355|117,1692848585360|124,1692848585371|126,1692848585376|130,1692848585388|133,1692848585393|136,1692848585404|137,1692848585409|138,1692848585416|139,1692848585425|139,1692848585433|140,1692848585441|140,1692848585449|140,1692848585457|141,1692848585465|141,1692848585473|141,1692848585481|142,1692848585489|142,1692848585498|142,1692848585506|143,1692848585514|143,1692848585522|143,1692848585530|144,1692848585539|145,1692848585546|146,1692848585554|146,1692848585562|149,1692848585572|151,1692848585579|154,1692848585589|157,1692848585596|160,1692848585604|161,1692848585611|164,1692848585621|167,1692848585627|170,1692848585639|172,1692848585643|174,1692848585655|177,1692848585659|179,1692848585667|181,1692848585676|182,1692848585683|184,1692848585692|185,1692848585699|186,1692848585710|187,1692848585716|188,1692848585724|189,1692848585732|189,1692848585740|190,1692848585748|191,1692848585758|192,1692848585764|192,1692848585773|193,1692848585781|194,1692848585789|195,1692848585797|195,1692848585806|196,1692848585813|196,1692848585821|197,1692848585829|197,1692848585839|197,1692848585845|197,1692848585855|197,1692848586219"
    } // 滑动轨迹(位置,时间戳)

    需要解决的参数为 point(缺口位置) 及 datelist(滑动轨迹)。


    point

    image.png


    推荐使用 ddddocr 库来识别, 准确率还可以吧, 挺方便的。


    datelist

    image.png


    轨迹方面自己设计算法来吧, 这里可以作为一个参考, 他的 datelist 的长度不固定, 一般也就是一百多些轨迹点吧, 可以通过调整参数来达到效果, 反正就是多测试吧, 这个方法大概有 百分之九十 左右的通过率吧, 暂时够用。


    VerifyCodeResult


    // 校验请求成功后返回数据
    {
    "state": 0,
    "info": "正确",
    "data": 197
    }
    // VerifyCodeResult: YmRmYl8xOTc=

    解决了验证码, 惊喜的发现还是不咋对, 这个返回的 data 明显长得和要用的 VerifyCodeResult 不太像, 就接着来找。


    image.png


    全局搜索, 找到两个 js 文件, 都打上断点来调试。


    image.png


    可以看到我们成功断到, 并得到是由 (new Base64).encode("bdfb_" + y) 这种方式来生成的 code 值, 这个 y 值就是上一步返回的 data, 接下来只需要把 Base64 的代码那里扣下来, 或者自己实现就行了, 方便些, 这里直接抠下来了, 然后拿到 code 带上验证码请求返回的 cookie, 就能正常拿到数据了。


    结语


    整体来说不算困难, 没有什么加密啊混淆啊之类的东西, 确定方向之后很快就能搞定了, 用来练手还是很不错的, 有什么问题欢迎交流, 不知道这个得几年啊。。。



    请洒潘江,各倾陆海云

    作者:Glommer
    来源:juejin.cn/post/7270702261293039635
    尔。


    收起阅读 »

    JS长任务(耗时操作)导致页面卡顿,优化方案大比拼!

    web
    抛出问题 前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因: 什么是长任务? 长任务是指JS代码执行耗时超过50ms,能让...
    继续阅读 »

    抛出问题


    前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因:




    • 什么是长任务?


      长任务是指JS代码执行耗时超过50ms,能让用户感知到页面卡顿的代码执行流。




    • 长任务为什么会造成页面卡顿?


      UI界面的渲染由UI线程控制,UI线程和JS线程是互斥的,所以在执行JS代码时,UI线程无法工作,就表现出页面卡死状态。




    我们现在来模拟一个长任务,看看它是怎么影响页面流畅性的:



    • 先看效果(GIF),这里我们给div加了个滚动的动画,当我们开始执行长任务后,页面卡住了,等待执行完后才恢复,总耗时3秒左右,记住总耗时,后面会用到。


    动画.gif



    • 再看代码(有点长,主要看JS部分,后面的优化方案代码只展示优化过的JS函数)


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
    .myDiv {
    width: 100px;
    height: 100px;
    margin: 50px;
    background-color: blue;
    position: relative;
    animation: my-animation 5s linear infinite;
    }
    @keyframes my-animation {
    from {
    left: 0%;
    rotate: 0deg;
    }
    to {
    left: 100%;
    rotate: 360deg;
    }
    }
    </style>
    </head>
    <body>
    <div class="myDiv"></div>
    <button onclick="longTask()">执行长任务</button>

    <script>
    // 模拟耗时操作,大概10ms
    function myFunc() {
    const startTime = Date.now();
    while (Date.now() - startTime < 10) {}
    }

    // 长任务,循环执行myFunc300次,耗时3秒左右
    function longTask() {
    console.log("开始长任务");
    const startTime = Date.now();
    for (let i = 0; i < 300; i++) {
    myFunc();
    }
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    </script>
    </body>
    </html>


    本段代码有一个模拟耗时的函数,有一个模拟长任务的函数(调用300次耗时函数),后面的优化方案都会基于这段代码来进行。


    优化方案


    setTimeout 宏任务方案


    第一个优化方案,我们将长任务拆成多个宏任务来执行,这里我们用setTimeout函数。为什么拆成多个宏任务可以优化卡顿问题呢?


    正如我们上文所说,页面卡顿的原因是因为JS执行线程占用了控制权,导致UI线程无法工作。在浏览器的事件轮询(EventLoop)机制中,每一个宏任务执行完之后会将控制权重新交给UI线程,待UI线程执行完渲染任务后,才会继续执行下一个宏任务。浏览器轮询机制流程图如下所示,想要深入了解浏览器轮询机制,可以参考我的另一篇文章:从进程和线程入手,我彻底明白了EventLoop的原理! image.png



    • 先看效果(GIF),执行长任务的同时,页面也很流畅,没有了先前卡顿的感觉,总耗时4.4秒。


    动画.gif



    • 再看代码


    // setTimeout方案 递归,循环300次
    function timeOutTask(i, startTime) {
    setTimeout(() => {
    if (!startTime) {
    console.log("开始长任务");
    i = 0;
    startTime = Date.now();
    }
    if (i === 300) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    return;
    }
    myFunc();
    timeOutTask(++i, startTime);
    });
    }

    把代码改为多个宏任务之后,解决了页面卡顿的问题,但是总耗时比之前多了1.4秒,主要原因是因为递归调用需要不断地向下开栈,会增加开销。当我们每个任务都不依赖于上一个任务的执行结果时,就可以不使用递归,直接使用循环创建宏任务。



    • 先看效果(GIF),耗时缩短到了3.1秒,但是可以看到明显掉帧。


    动画.gif



    • 再看代码


    // setTimeout不递归方案,循环300次
    function timeOutTask2() {
    console.log("开始长任务");
    const startTime = Date.now();

    for (let i = 0; i < 300; i++) {
    setTimeout(() => {
    myFunc();
    if (i === 300 - 1) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    });
    }
    }

    使用300个循环同时创建宏任务后,虽然耗时降低了,但是div滚动会出现明显掉帧,这也是我们不愿意看到的,那执行代码速度和页面流畅度就没办法兼得了吗?很幸运,requestIdleCallback函数可以帮你解决这个难题。


    requestIdleCallback 函数方案


    requestIdleCallback提供了由浏览器决定,在空闲的时候执行队列任务的能力,从而不会影响到UI线程的正常运行,保证了页面的流畅性。


    它的用法也很简单,第一个参数是一个函数,浏览器空闲的时候就会把函数放到队列执行,第二个参数为options,包含一个timeout,则超时时间,即使浏览器非空闲,超时时间到了,也会将任务放到事件队列。
    下面我们把setTimeout替换为requestIdleCallback



    • 先看效果(GIF),耗时3.1秒,也没有出现掉帧的情况。


    动画.gif



    • 再看代码


    // requestIdleCallback不递归方案,循环300次
    function callbackTask() {
    console.log("开始长任务");
    const startTime = Date.now();

    for (let i = 0; i < 300; i++) {
    requestIdleCallback(() => {
    myFunc();
    if (i === 300 - 1) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    });
    }
    }

    requestIdleCallback解决了setTimeout方案掉帧的问题,这两种方案都需要拆分任务,有没有一种不需要拆分任务,还能不影响页面流畅度的方法呢?Web Worker满足你。


    Web Worker 多线程方案


    WebWorker是运行在后台的javascript,独立于其他脚本,不会影响页面的性能。



    • 先看效果,耗时不到3.1秒,页面也没有受到影响。


    动画.gif



    • 再看代码,需要额外创建一个js文件。(注意,浏览器本地直接运行HTML会被当成跨域,需要开一个服务运行,我使用的http-server)


    task.js 文件代码


    // 模拟耗时
    function myFunc() {
    const startTime = Date.now();
    while (Date.now() - startTime < 10) {}
    }

    // 循环执行300次
    for (let i = 0; i < 300; i++) {
    myFunc();
    }

    // 通知主线程已执行完
    self.postMessage("我执行完啦");

    主文件代码


    // Web Worker 方案
    function workerTask() {
    console.log("开始长任务");
    const startTime = Date.now();
    const worker = new Worker("./task.js");

    worker.addEventListener("message", (e) => {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    });
    }

    WebWorker方案额外增加的耗时很少,也不需要拆分代码,也不会影响页面性能,算是很完美的一种方案了。
    但它也有一些缺点:



    • 浏览器兼容性差

    • 不能访问DOM,即不能更新UI

    • 不能跨域加载JS


    总结


    三种方案中,如果不需要访问DOM的话,我认为最好的方案为WebWorker方案,其次requestIdleCallback方案,最后是setTimeout方案。
    WebWorker和requestIdleCallback属于比较新的特性,并非所有浏览器都支持,所以我们需要先进行判断,代码如下:


    if (typeof Worker !== 'undefined') {
    //使用 WebWorker
    }else if(typeof requestIdleCallback !== 'undefined'){
    //使用 requestIdleCallback
    }else{
    //使用 setTimeout
    }

    希望本文对您有帮助,其他所有代码可在下方直接执行。(WebWorker不支持)

    作者:TuYuHao
    来源:juejin.cn/post/7272632260180377634
    n>

    收起阅读 »

    你看这个圆脸😁,又大又可爱~ (Compose低配版)

    web
    前言 阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新) 在网上看到有人用css写出了下面这种效果;原文链接 我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。 一、Canvas画图 这...
    继续阅读 »


    前言


    阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新)


    在网上看到有人用css写出了下面这种效果;原文链接


    请添加图片描述


    我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。




    一、Canvas画图


    这种笑脸常用的控件肯定实现不了,我们只能用Canvas自己画了


    笑脸


    我们先画脸



    下例当中的size和center都是onDraw 的DrawScope提供的属性,drawCircle则是DrawScope提供的画圆的方法



    Canvas(modifier = modifier
    .size(300.dp),
    onDraw = {

    // 脸
    drawCircle(
    color = Color(0xfffecd00),
    radius = size.width / 2,
    center = center
    )

    })

    属性解释



    • color:填充颜色

    • radius: 半径

    • center: 圆心坐标


    这里我们半径取屏幕宽度一半,圆心取屏幕中心,画出来的脸效果如下


    在这里插入图片描述


    微笑


    微笑是一个弧形,我们使用drawArc来画微笑


    // 微笑
    val smilePadding = size.width / 4

    drawArc(
    color = Color(0xffb57700),
    startAngle = 0f,
    sweepAngle = 180f,
    useCenter = true,
    topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    size = Size(size.width / 2, size.height / 4)
    )

    属性解释



    • color:填充颜色

    • startAngle: 弧形开始的角度,默认以3点钟方向为0度

    • sweepAngle:弧形结束的角度,默认以3点钟方向为0度

    • useCenter :指示圆弧是否要闭合边界中心的标志(上例加不加都无所谓)

    • topLeft :相对于当前平移从0的本地原点偏移,0开始

    • size:要绘制的圆弧的尺寸


    效果如下
    在这里插入图片描述


    眼睛和眼珠子


    眼睛也是drawCircle方法,只是位置不同,这边就不再多做解释


                // 左眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )


    //左眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )



    // 右眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )


    //右眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )


    整个笑脸就画出来了,效果如下


    在这里插入图片描述


    二、跟随手势移动


    在实现功能之前我们需要介绍transformableanimateFloatAsStatetranslate


    transformable


    transformablemodifier用于平移、缩放和旋转的多点触控手势的修饰符,此修饰符本身不会转换元素,只会检测手势。


    animateFloatAsState


    animateFloatAsState 是通过Float状态变化来控制动画 的状态


    知道了这两个玩意过后我们就可以先通过transformable监听手势滑动然后通过translate方法和animateFloatAsState方法组成一个平移动画来实现眼珠跟随手势移动


    完整的代码:


    @Composable
    fun SmileyFaceCanvas(
    modifier: Modifier
    )
    {

    var x by remember {
    mutableStateOf(0f)
    }

    var y by remember {
    mutableStateOf(0f)
    }

    val state = rememberTransformableState { _, offsetChange, _ ->

    x = offsetChange.x
    if (offsetChange.x >50f){
    x = 50f
    }

    if (offsetChange.x < -50f){
    x=-50f
    }

    y = offsetChange.y
    if (offsetChange.y >50f){
    y= 50f
    }

    if (offsetChange.y < -50f){
    y=-50f
    }
    }

    val animTranslateX by animateFloatAsState(
    targetValue = x,
    animationSpec = TweenSpec(1000)
    )

    val animTranslateY by animateFloatAsState(
    targetValue = y,
    animationSpec = TweenSpec(1000)
    )



    Canvas(
    modifier = modifier
    .size(300.dp)
    .transformable(state = state)
    ) {



    // 脸
    drawCircle(
    Color(0xfffecd00),
    radius = size.width / 2,
    center = center
    )

    // 微笑
    val smilePadding = size.width / 4

    drawArc(
    color = Color(0xffb57700),
    startAngle = 0f,
    sweepAngle = 180f,
    useCenter = true,
    topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    size = Size(size.width / 2, size.height / 4)
    )

    // 左眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )

    translate(left = animTranslateX, top = animTranslateY) {
    //左眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )
    }


    // 右眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )

    translate(left = animTranslateX, top = animTranslateY) {
    //右眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )
    }


    }
    }

    为了不让眼珠子从眼眶里蹦出来,我们将位移的范围限制在了50以内,运行效果如下


    在这里插入图片描述




    总结


    通过Canvas中的一些方法配合简单的动画API实

    作者:我怀里的猫
    来源:juejin.cn/post/7272550100139098170
    现了这个眼珠跟随手势移动的笑脸😁

    收起阅读 »

    pdf为什么不能被修改

    web
    PDF简介 PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。 PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进...
    继续阅读 »

    PDF简介



    • PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。

    • PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进行绘制的。

    • PDF基本显示单元包括:文字,图片,矢量图,图片

    • PDF扩展单元包括:水印,电子署名,注释,表单,多媒体,3D

    • PDF动作单元:书签,超链接(拥有动作的单元有很多个,包括电子署名,多媒体等等)


    PDF的优点



    • 一致性:在所有可以打开PDF的机器上,展示的效果是完全一致,不会出现段落错乱、文字乱码这些排版问题。尤其是文档中,本身可以嵌入字体,避免了客户端没有对应字体,而导致文字显示不一致的问题。所以,在印刷行业,绝大多数用的都是PDF格式。

    • 不易修改:用过PDF文件的人,都会知道,对已经保存之后的PDF文件,想要进行重新排版,基本上就不可能的,这就保证了从资料源发往外界的资料,不容易被篡改。

    • 安全性:PDF文档可以进行加密,包括以下几种加密形式:文档打开密码,文档权限密码,文档证书密码,加密的方法包括:RC4,AES,通过加密这种形式,可以达到资料防扩散等目的。

    • 不失真:PDF文件中,使用了矢量图,在文件浏览时,无论放大多少倍,都不会导致使用矢量图绘制的文字,图案的失真。

    • 支持多种压缩方式:为了减少PDF文件的size,PDF格式支持各种压缩方式:asciihex,ascii85,lzw,runlength,ccitt,jbig2,jpeg(DCT),jpeg2000(jpx)

    • 支持多种印刷标准:支持PDF-A,PDF-X


    PDF格式


    根据PDF官方指南,理解PDF格式可以从四个方面下手——Objects(对象)、File structure(物理文件结构)、Document structure(逻辑文件结构)、Content streams(内容流)。


    对象


    物理文件结构




    • 整体上分为文件头(Header)、对象集合(Body)、交叉引用表(Xref table)、文件尾(Trailer)四个部分,结构如图。修改过的PDF结构会有部分变化。




    • 未经修改






    编辑


    img




    • 经修改






    编辑


    img


    文件头



    • 文件头是PDF文件的第一行,格式如下:


    %PDF-1.7

    复制



    • 这是个固定格式,表示这个PDF文件遵循的PDF规范版本,解析PDF的时候尽量支持高版本的规范,以保证支持大多数工具生成的PDF文件。1.7版本支持1.0-1.7之间的所有版本。


    对象集合



    • 这是一个PDF文件最重要的部分,文件中用到的所有对象,包括文本、图象、音乐、视频、字体、超连接、加密信息、文档结构信息等等,都在这里定义。格式如下:


    2 0 obj
    ...
    end obj

    复制



    • 一个对象的定义包含4个部分:前面的2是对象序号,其用来唯一标记一个对象;0是生成号,按照PDF规范,如果一个PDF文件被修改,那这个数字是累加的,它和对象序号一起标记是原始对象还是修改后的对象,但是实际开发中,很少有用这种方式修改PDF的,都是重新编排对象号;obj和endobj是对象的定义范围,可以抽象的理解为这就是一个左括号和右括号;省略号部分是PDF规定的任意合法对象。

    • 可以通过R关键字来引用任何一个对象,比如要引用上面的对象,可以使用2 0 R,需要主意的是,R关键字不仅可以引用一个已经定义的对象,还可以引用一个并不存在的对象,而且效果就和引用了一个空对象一样。

    • 对象主要有下面几种

    • booleam 用关键字true或false表示,可以是array对象的一个元素,或dictionary对象的一个条目。也可以用在PostScript计算函数里面,做为if或if esle的一个条件。

    • numeric


    包括整形和实型,不支持非十进制数字,不支持指数形式的数字。例: 1)整数 123 4567 +111 -2 范围:正2的31次方-1到负的2的31次方 2)实数 12.3 0.8 +6.3 -4.01 -3. +.03 范围:±3.403 ×10的38次方 ±1.175 × 10的-38次方



    • 注意:如果整数超过表示范围将转化成实数,如果实数超过范围就会出错

    • string


    由一系列0-255之间的字节组成,一个string总长度不能超过65535.string有以下两种方式:



    • 十六进制字串 由<>包含起来的一个16进制串,两位表示一个字符,不足两位用0补齐。例: \ 表示AA和BB两个字符 \ 表示AA和B0两个字符

    • 直接字串 由()包含起来的一个字串,中间可以使用转义符"/"。例:(abc) 表示abc (a//) 表示a/ 转义符的定义如下:


    转义字符含义
    /n换行
    /r回车
    /t水平制表符
    /b退格
    /f换页(Form feed (FF))
    /(左括号
    /)右括号
    //反斜杠
    /ddd八进制形式的字符



    • 对象类别(续)




    • name 由一个前导/和后面一系列字符组成,最大长度为127。和string不同的是,name是不可分割的并且是唯一的,不可分割就是说一个name对象就是一个原子,比如/name,不能说n就是这个name的一个元素;唯一就是指两个相同的name一定代表同一个对象。从pdf1.2开始,除了ascii的0,别的都可以用一个#加两个十六进制的数字表示。例: /name 表示name /name#20is 表示name is /name#200 表示name 0




    • array 用[]包含的一组对象,可以是任何pdf对象(包括array)。虽然pdf只支持一维array,但可以通过array的嵌套实现任意维数的array(但是一个array的元素不能超过8191)。例:[549 3.14 false (Ralph) /SomeName]




    • Dictionary 用"<<"和">>"包含的若干组条目,每组条目都由key和value组成,其中key必须是name对象,并且一个dictionary内的key是唯一的;value可以是任何pdf的合法对象(包括dictionary对象)。例: << /IntegerItem 12 /StringItem (a string) /Subdictionary << /Item1 0.4 /Item2 true /LastItem (not!) /VeryLastItem (OK) >> >>




    • stream 由一个字典和紧跟其后面的一组关键字stream和endstream以及这组关键字中间包含一系列字节组成。内容和string很相似,但有区别:stream可以分几次读取,分开使用不同的部分,string必须作为一个整体一次全部读取使用;string有长度限制,但stream却没有这个限制。一般较大的数据都用stream表示。需要注意的是,stream必须是间接对象,并且stream的字典必须是直接对象。从1.2规范以后,stream可以以外部文件形式存在,这种情况下,解析PDF的时候stream和endstream之间的内容就被忽略掉。例: dictionary stream…data…endstreamstream字典中常用的字段如下: 字段名类型值Length整形(必须)关键字stream和endstream之间的数据长度,endstream之前可能会有一个多余的EOL标记,这个不计算在数据的长度中。Filter名字 或 数组(可选)Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。DecodeParms字典 或 数组(可选)一个参数字典或由参数字典组成的一个数组,供Filter使用。如果仅有一个Filter并且这个Filter需要参数,除非这个Filter的所有参数都已经给了默认值,否则的话 DecodeParms必须设置给Filter。如果有多个Filter,并且任意一个Filter使用了非默认的参数, DecodeParms 必须是个数组,每个元素对应一个Filter的参数列表(如果某个Filter无需参数或所有参数都有了默认值,就用空对象代替)。如果没有Filter需要参数,或者所有Filter的参数都有默认值,DecodeParms 就被忽略了。F文件标识(可选)保存stream数据的文件。如果有这个字段, stream和endstream就被忽略,FFilter将会代替Filter, FDecodeParms将代替DecodeParms。Length字段还是表示stream和endstream之间数据的长度,但是通常此刻已经没有数据了,长度是0.FFilter名字 或 字典(可选)和filter类似,针对外部文件。FDecodeParms字典 或 数组(可选)和DecodeParams类似,针对外部文件。




    • Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。且需要被编码。编码算法主要如下:






    编辑切换为居中


    img


    编码可视化主要显示为乱码,所以提供了隐藏信息的机会,如下图的steam内容为乱码。




    编辑切换为居中


    img



    • NULL 用null表示,代表空。如果一个key的值为null,则这个key可以被忽略;如果引用一个不存在的object则等价于引用一个空对象。


    交叉引用表



    • 交叉引用表是PDf文件内部一种特殊的文件组织方式,可以很方便的根据对象号随机访问一个对象。其格式如下:


    xref
    0 1
    0000000000 65535 f
    4 1
    0000000009 00000 n
    8 3
    0000000074 00000 n
    0000000120 00000 n
    0000000179 00000 n

    复制



    • 其中,xref是开始标志,表示以下为一个交叉引用表的内容;每个交叉引用表又可以分为若干个子段,每个子段的第一行是两个数字,第一个是对象起始号,后面是连续的对象个数,接着每行是这个子段的每个对象的具体信息——每行的前10个数字代表这个这个对象相对文件头的偏移地址,后面的5位数字是生成号(用于标记PDF的更新信息,和对象的生成号作用类似),最后一位f或n表示对象是否被使用(n表示使用,f表示被删除或没有用)。上面这个交叉引用表一共有3个子段,分别有1个,1个,3个对象,第一个子段的对象不可用,其余子段对象可用。


    文件尾



    • 通过trailer可以快速的找到交叉引用表的位置,进而可以精确定位每一个对象;还可以通过它本身的字典还可以获取文件的一些全局信息(作者,关键字,标题等),加密信息,等等。具体形式如下:


    trailer
    <<
    key1 value1
    key2 value2
    key3 value3

    >>
    startxref
    553
    %%EOF

    复制



    • trailer后面紧跟一个字典,包含若干键-值对。具体含义如下:


    值类型值说明
    Size整形数字所有间接对象的个数。一个PDF文件,如果被更新过,则会有多个对象集合、交叉引用表、trailer,最后一个trailer的这个字段记录了之前所有对象的个数。这个值必须是直接对象。
    Prev整形数字当文件有多个对象集合、交叉引用表和trailer时,才会有这个键,它表示前一个相对于文件头的偏移位置。这个值必须是直接对象。
    Root字典Catalog字典(文件的逻辑入口点)的对象号。必须是间接对象。
    Encrypt字典文档被保护时,会有这个字段,加密字典的对象号。
    Info字典存放文档信息的字典,必须是间接对象。
    ID数组文件的ID


    • 上面代码中的startxref:后面的数字表示最后一个交叉引用表相对于文件起始位置的偏移量

    • %%EOF:文件结束符


    逻辑文件结构




    编辑切换为居中


    img


    catalog根节点



    • catalog是整个PDF逻辑结构的根节点,这个可以通过trailer的Root字段定位,虽然简单,但是相当重要,因为这里是PDF文件物理结构和逻辑结构的连接点。Catalog字典包含的信息非常多,这里仅就最主要的几个字段做个说明。 字段类型值Typename(必须)只能为Pages 。Parentdictionary(如果不是catalog里面指定的跟节点,则必须有,并且必须是间接对象) 当前节点的直接父节点。Kidsarray(必须)一个间接对象组成的数组,节点可能是page或page tree。Countinteger(必须) page tree里面所包含叶子节点(page 对象)的个数。从以上字段可以看出,Pages最主要的功能就是组织所有的page对象。Page对象描述了一个PDF页面的属性、资源等信息。Page对象是一个字典,它主要包含一下几个重要的属性:

    • Pages字段 这是个必须字段,是PDF里面所有页面的描述集合。Pages字段本身是个字典,它里面又包含了一下几个主要字段:


    字段类型
    Typename(必须)必须是Page。
    Parentdictionary(必须;并且只能是间接对象)当前page节点的直接父节点page tree 。
    LastModifieddate(如果存在PieceInfo字段,就必须有,否则可选)记录当前页面被最后一次修改的日期和时间。
    Resourcesdictionary(必须; 可继承)记录了当前page用到的所有资源。如果当前页不用任何资源,则这是个空字典。忽略所有字段则表示继承父节点的资源。
    MediaBoxrectangle(必须; 可继承)定义了要显示或打印页面的物理媒介的区域(default user space units)
    CropBoxrectangle(可选; 可继承)定义了一个可视区域,当前页被显示或打印的时候,它的内容会被这个区域裁剪。默认值就是 MediaBox。
    BleedBoxrectangle(可选) 定义了一个区域,当输出设备是个生产环境( production environment)的时候,页面显示的内容会被裁剪。默认值是 CropBox.
    Contentsstream or array(可选) 描述页面内容的流。如果这个字段缺省,则页面上什么也不会显示。这个值可以是一个流,也可以是由几个流组成的一个数组。如果是数组,实际效果相当于所有的流是按顺序连在一起的一个流,这就允许PDF生成的时候可以随时插入图片或其他资源。流之间的分割只是词汇上的一个分割,并不是逻辑上或者组织形式的切割。
    Rotateinteger(可选; 可继承) 顺时钟旋转的角度数,这个必须是90的整数倍,默认是0。
    Thumbstream(可选)定义当前页的缩略图。
    Annotsarray(可选) 和当前页面关联的注释。
    Metadatastream(可选) 当前页包含的元数据。


    • 一个简单例子:


    3 0 obj
    << /Type /Page
    /Parent 4 0 R
    /MediaBox [ 0 0 612 792 ]
    /Resources <</Font<<
    /F3 7 0 R /F5 9 0 R /F7 11 0 R
    >>
    /ProcSet [ /PDF ]
    >>
    /
    Contents 12 0 R
    /Thumb 14 0 R
    /Annots [ 23 0 R 24 0 R]
    >>
    endobj

    复制



    • Outlines字段 Outline是PDF里面为了方便用户从PDF的一部分跳转到另外一部分而设计的,有时候也叫书签(Bookmark),它是一个树状结构,可以直观的把PDF文件结构展现给用户。用户可以通过鼠标点击来打开或者关闭某个outline项来实现交互,当打开一个outline时,用户可以看到它的所有子节点,关闭一个outline的时候,这个outline的所有子节点会自动隐藏。并且,在点击的时候,阅读器会自动跳转到outline对应的页面位置。Outlines包含以下几个字段: 字段类型值Typename(可选)如果这个字段有值,则必须是Outlines。Firstdictionary(必须;必须是间接对象) 第一个顶层Outline item。Lastdictionary(必须;必须是间接对象)最后一个顶层outline item。Countinteger(必须)outline的所有层次的item的总数。

    • Outline是一个管理outline item的顶层对象,我们看到的,其实是outline item,这个里面才包含了文字、行为、目标区域等等。一个outline item主要有一下几个字段: 字段类型值Titletext string(必须)当前item要显示的标题。Parentdictionary(必须;必须是间接对象) outline层级中,当前item的父对象。如果item本身是顶级item,则父对象就是它本身。Prevdictionary(除了每层的第一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的前一个item。Nextdictionary(除了每层的最后一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的后一个item。Firstdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的第一个直接子节点。Lastdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的最后一个直接子节点。Destname,byte string, or array(可选; 如果A字段存在,则这个不能被会略)当前的outline item被激活的时候,要显示的区域。Adictionary(可选; 如果Dest 字段存在,则这个不能被忽略)当前的outline item被激活的时候,要执行的动作。

    • URI字段 URI(uniform resource identifier),定义了文档级别的统一资源标识符和相关链接信息。目录和文档中的链接就是通过这个字段来处理的.

    • Metadata字段 文档的一些附带信息,用xml表示,符合adobe的xmp规范。这个可以方便程序不用解析整个文件就能获得文件的大致信息。

    • 其他 Catalog字典中,常用的字段一般有以下一些:


    字段类型
    Typename(必须)必须为Catalog。
    Versionname(可选)PDF文件所遵循的版本号(如果比文件头指定的版本号高的话)。如果这个字段缺省或者文件头指定的版本比这里的高,那就以文件头为准。一个PDF生成程序可以通过更新这个字段的值来修改PDF文件版本号。
    Pagesdictionary(必须并且必须为间接对象)当前文档的页面集合入口。
    PageLabelsnumber tree(可选) number tree,定义了页面和页面label对应关系。
    Namesdictionary(可选)文档的name字典。
    Destsdictionary(可选;必须是间接对象)name和相应目标对应关系字典。
    ViewerPreferencesdictionary(可选)阅读参数配置字典,定义了文档被打开时候的行为。如果缺省,则使用阅读器自己的配置。
    PageLayoutname(可选) 指定文档被打开的时候页面的布局方式。SinglePageDisplay 单页OneColumnDisplay 单列TwoColumnLeftDisplay 双列,奇数页在左TwoColumnRightDisplay 双列,奇数页在右TwoPageLeft 双页,奇数页在左TwoPageRight 双页,奇数页在右缺省值: SinglePage.
    PageModename(可选) 当文档被打开时,指定文档怎么显示Use 目录和缩略图都不显示UseOutlines 显示目录UseThumbs 显示缩略图FullScreen 全屏模式,没有菜单,任何其他窗口UseOC 显示Optional content group 面板UseAttachments显示附件面板缺省值: Use.
    Outlinesdictionary(可选;必须为间接对象)文档的目录字典
    Threadsarray(可选;必须为间接对象)文章线索字典组成的数组。
    OpenActionarray or dictionary(可选) 指定一个区域或一个action,在文档打开的时候显示(区域)或者执行(action)。如果缺省,则会用默认缩放率显示第一页的顶部。
    AAdictionary(可选)一个附加的动作字典,在全局范围内定义了响应各种事件的action。
    URIdictionary(可选)一个URI字典包含了文档级别的URI action信息。
    AcroFormdictionary(可选)文档的交互式form (AcroForm)字典。
    Metadatastream(可选;必须是间接对象)文档包含的元数据流。

    具体组成


    1 Header部分


    PDF文件的第一行应是由5个字符“%PDF-”后跟“1.N”的版本号组成的标题,其中N是0到7之间的数字。例如下面的:


    %PDF–1.0   %PDF–1.1   %PDF–1.2   %PDF–1.3   %PDF–1.4   %PDF–1.5   %PDF–1.6   %PDF–1.7


    从PDF 1.4开始,应使用文档目录字典中的Version 条目(通过文件Trailer部分的Root条目指定版本),而不是标题中指定的版本。


    2 Body部分


    PDF文件的正文应由表示文件内容的一系列间接对象组成,例如字体、页面和采样图像。从PDF 1.5开始,Body还可以包含对象流,每个对象流包含一系列间接对象。例如下面这样:


    1 0 obj
    << /Type /Catalog
      /Outlines 2 0 R
      /Pages 3 0 R
    >>
    endobj
    2 0 obj
    << /Type Outlines
      /Count 0
    >>
    endobj
    3 0 obj
    << /Type /Pages
    /Kids [4 0 R]
    /Count 1
    >>
    endobj
    4 0 obj
    << /Type /Page
      /Parent 3 0 R
      /MediaBox [0 0 612 792]
      /Contents 5 0 R
      /Resources << /ProcSet 6 0 R >>
    >>
    endobj
    5 0 obj
    << /Length 35 >>
    stream
      …Page-marking operators…
    endstream
    endobj
    6 0 obj
    [/PDF]
    endobj

    3 Cross-Reference Table 交叉引用表部分


    交叉引用表包含文件中间接对象的信息,以便允许对这些对象进行随机访问,因此无需读取整个文件即可定位任何特定对象。


    交叉引用表以xref开始,紧接着是一个空格隔开的两个数字,然后每一行就是一个对象信息:


    xref
    0 7
    0000000000 65535 f
    0000000009 00000 n
    0000000074 00000 n
    0000000120 00000 n
    0000000179 00000 n
    0000000300 00000 n
    0000000384 00000 n

    上面第二行中的两个数字“0 7”,0表示下面的对象从0号对象开始,7表示对象的数量,也就是说表示从0到6共7个对象。


    每行一个对象信息的格式如下:


    nnnnnnnnnn ggggg n eol


    • nnnnnnnnnn 长度10个字节,表示对象在文件的偏移地址;

    • ggggg 长度5个字节,表示对象的生成号;

    • n (in-use)表示对象被引用,如果此值是f (free),表示对象未被引用;

    • eol 就是回车换行


    交叉引用表中的第一个编号为0的对象始终是f(free)的,并且生成号为65535;除了编号0的对象外,交叉引用表中的所有对象最初的生成号应为0。删除间接对象时,应将其交叉引用条目标记为“free”,并将其添加到free条目的链表中。下次创建具有该对象编号的对象时,条目的生成号应增加1,最大生成号为65535;当交叉引用条目达到此值时,它将永远不会被重用。


    交叉引用表也可以是这样的:


    xref
    0 1
    0000000000 65535 f
    3 1
    0000025325 00000 n
    23 2
    0000025518 00002 n
    0000025635 00000 n
    30 1
    0000025777 00000 n

    [


    4 Trailer部分


    PDF阅读器是从PDF的尾部开始解析文件的,通过Trailer部分能够快速找到交叉引用表和某些特殊对象。如下所示:


    trailer
    << /Size 7
    /Root 1 0 R
    >>
    startxref
    408
    %%EOF

    文件的最后一行应仅包含文件结束标记%%EOF。关键字startxref下面的数字表示最后一个交叉引用表的xref关键字开头的字节偏移量。trailer和startxref之间是尾部字典,由包含在双尖括号(<<…>>)中的键值对组成。


    为什么不容易被修改



    1. 文件结构和编码:PDF文件采用了一种复杂的文件结构和编码方式,这使得在未经授权的情况下修改PDF文件变得非常困难。PDF文件采用二进制格式存储,而不是像文本文件那样以可读的形式存储。这导致无法直接编辑和修改PDF文件,需要使用特定的软件或工具。

    2. 加密和安全特性:PDF文件可以使用密码进行加密和保护,以确保只有授权的用户才能进行修改。加密可以防止未经授权的访问和修改,使得修改PDF文件变得更加困难和复杂。

    3. 文件签名和验证:PDF文件可以使用数字签名进行验证,以确保文件的完整性和可信性。数字签名可以证明文件的来源和真实性,一旦数字签名验证失败,即表明文件已被篡改。

    4. 版本兼容性和规范:PDF格式被国际标准化组织(ISO)制定为ISO 32000标准。这个标准确保了不同版本和软件之间的PDF文件的兼容性,并定义了丰富的功能和规范,包括页面布局、字体嵌入、图形和图像处理等。这些严格的规范使得对PDF文件进行修改变得复杂和具有挑战性。


    如何修改pdf


    因为pdf的局限性是无法进行修改的,所以我们只能通过将他转换为其他类型的文件进行查看修改,当然,转换的过程不可能是百分百完美的进行转换的。


    下面推荐这俩个可以进行转换的网站


    PDF转换成Word在线转换器 - 免费 - CleverPDF


    Convert PDF to Word for free |

    Smallpdf.com

    收起阅读 »