注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端黑科技篇章之scp2,让你一键打包部署服务器

web
scp2是一个使用nodejs对于SSH2的模拟实现,它可以让我们编译之后将项目推送至测试环境,以方便测试。 项目安装scp2 npm i scp2 -D 编写配置文件 创建scp2的配置文件 upload.server.js const serInfo =...
继续阅读 »

scp2是一个使用nodejs对于SSH2的模拟实现,它可以让我们编译之后将项目推送至测试环境,以方便测试。


项目安装scp2


npm i scp2 -D

编写配置文件


创建scp2的配置文件 upload.server.js


const serInfo = JSON.parse(process.env.npm_config_argv).cooked // 获取终端命令
const server = {
host: serInfo[2], // 服务器ip
port: '22', // 端口一般默认22
username: serInfo[3] || 'root', // 用户名
password: serInfo[4] || 'root', // 密码
pathNmae: '', // 上传到服务器的位置
locaPath: './dist/' // 本地打包文件的位置
}

const argv1 = process.argv
console.log(argv1)
console.log(serInfo)
// 引入scp2
const client = require('scp2')
const ora = require('ora')
const spinner = ora('正在发布到服务器...')

const Client = require('ssh2').Client
const conn = new Client()

console.log('正在建立连接')
conn.on('ready', () => {
console.log('已连接')
if (!server.pathNmae) {
console.log('连接已关闭')
conn.end()
return false
}

conn.exec('rm -rf' + server.pathNmae + '/*', (err, stream) => {
console.log(err + '删除文件')
stream.on('close', (code, signal) => {
console.log('开始上传')
spinner.start()
client.scp(server.locaPath, {
'host': server.host,
'port': server.port,
'username': server.username,
'password': server.password,
'path': server.pathNmae
}, err => {
spinner.stop()
if (!err) {
console.log('项目发布完毕')
} else {
console.log('err', err)
}
conn.end()
})
})
})
}).connect({
host: server.host,
port: server.port,
username: server.username,
password: server.password
// privateKey: '' // 私秘钥
})



配置package.json


image.png


两种上传方式,个人比较喜欢第二种哟



  1. build:pub:一键打包并上传服务器

  2. build:打包上传然后执行npm run publish 上传服务器


实战例子


弄好以上配置,在终端上敲 npm run build 然后再敲 npm run publish 得到以下结果。


image.png


已经发布到指定IP知道目录的服务器啦,这样我们就不用通过第三方ftp工具去上传了哦,是不是方便了很多,如果觉得该文章对你有帮助,请点个小赞赞吧。


作者:大码猴
来源:juejin.cn/post/6955070802035228685
收起阅读 »

谈谈外网刷屏的量子纠缠效果

web
大家好,我卡颂。 最近被一段酷炫的量子纠缠效果刷屏了: 原作者是@_nonfigurativ_,一位艺术家、程序员。 今天简单讲讲他的核心原理。 基础概念 首先我们需要知道两个概念: 屏幕坐标系,屏幕左上角就是屏幕坐标系的圆点 窗口坐标系,页面窗口...
继续阅读 »

大家好,我卡颂。


最近被一段酷炫的量子纠缠效果刷屏了:


acda85f4-d21d-407e-b433-b88a4a65468b.gif


原作者是@_nonfigurativ_,一位艺术家、程序员。



今天简单讲讲他的核心原理。


基础概念


首先我们需要知道两个概念:




  • 屏幕坐标系,屏幕左上角就是屏幕坐标系的圆点




  • 窗口坐标系,页面窗口左上角就是窗口坐标系的圆点





如果只用一台电脑,不外接屏幕的话,我们会有:




  • 一个屏幕坐标系




  • 打开几个页面,每个页面有各自的窗口坐标系




如果外接了屏幕(或外接pad),那么就存在多个屏幕坐标系,这种情况的计算需要用到管理屏幕设备的API —— window.getScreenDetails,在本文的讨论中不涉及这种情况。


当我们打开一个新页面窗口,窗口的左上角就是窗口坐标系的圆点,如果要在页面正中间画个圆,那圆心的窗口坐标系坐标应该是(window.innerWidth / 2, window.innerHeight / 2)



对于一个打开的窗口:




  • 他的左上角相对于屏幕顶部的距离为window.screenTop




  • 他的左上角相对于屏幕左边的距离为window.screenLeft





所以,我们可以轻松得出圆的圆心在屏幕坐标系中的坐标:



位置检测


在效果中,当打开两个页面,他们能感知到对方的位置并作出反应,这是如何实现的呢?



当前,我们已经知道圆心在屏幕坐标系中的坐标。如果打开多个页面,就会获得多个圆心的屏幕坐标系坐标


现在需要做的,就是让这些页面互相知道对方的坐标,这样就能向对应的方向做出连接的特效。


同源网站跨页面通信的方式有很多,比如:




  • Window.postMessage




  • LocalStorageSessionStorage




  • SharedWorker




  • BroadcastChannel




甚至Cookie也能用于跨页面通信(可以在同源的所有页面之间共享)。


在这里作者使用的是LocalStorage



只需要为每个页面生成一个唯一ID


const pageId = Math.random().toString(36).substring(2); // 生成一个随机的页面ID

每当将圆心最新坐标存储进LocalStorage时:


localStorage.setItem(
pageId,
JSON.stringify({
x: window.screenX,
y: window.screenY,
width: window.innerWidth,
height: window.innerHeight,
})
);

在另一个页面通过监听storage事件就能获取对方圆心的屏幕坐标系坐标


window.addEventListener("storage", (event) => {
if (event.key !== pageId) {
// 来自另一个页面
const { x, y } = JSON.parse(event.newValue);
// ...
}
});

再将对方圆心的屏幕坐标系坐标转换为自身的窗口坐标系坐标,并在该坐标绘制一个圆,就能达到类似窗口叠加后,下面窗口的画面出现在上面窗口内的效果。


通俗的讲,所有页面都会绘制其他页面的圆,只是有些圆在页面窗口外,看不见罢了。



考虑到页面性能,检测圆心的屏幕坐标系坐标渲染圆相关操作可以放到requestAnimationFrame回调中执行。


后记


上述只是该效果的核心原理。要完全复刻效果,还得考虑:




  • 渲染大量粒子(我们示例中用代替),且多窗口通信时的性能问题




  • 窗口移动时的阻尼效果




  • 当前的实现是在同一个屏幕坐标系中,如果要跨屏幕实现,需要使用window.getScreenDetails




不得不感叹跨界(作者是艺术家 + 程序员)迸发的想象力真的不一般。



作者:魔术师卡颂
来源:juejin.cn/post/7304531203771301923
收起阅读 »

[自定义View]一个简单的渐变色ProgressBar

web
Android原生ProgressBar 原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。 style: style效果@android:style/Widget.ProgressBar.Horizontal水平进...
继续阅读 »

Android原生ProgressBar



原生ProgressBar样式比较固定,主要是圆形和线条;也可以通过style来设置样式。



style:


style效果
@android:style/Widget.ProgressBar.Horizontal水平进度条
@android:style/Widget.ProgressBar.Small小型圆形进度条
@android:style/Widget.ProgressBar.Large大型圆形进度条
@android:style/Widget.ProgressBar.Inverse反色进度条
@android:style/Widget.ProgressBar.Small.Inverse反色小型圆形进度条
@android:style/Widget.ProgressBar.Large.Inverse反色大型圆形进度条
@android:style/Widget.Material**MD风格

原生的特点就是单调,实现基本的功能,使用简单样式不复杂;要满足我们期望的效果就只能自定义View了。


自定义ProgressBar



自定义View的实现方式有很多种,继承已有的View,如ImageView,ProgressBar等等;也可以直接继承自View,在onDraw中绘制需要的效果。
要实现的效果是一个横向圆角矩形进度条,内容为渐变色。
所以在设计时要考虑到可以定义的属性:渐变色、进度等。



<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="progress">
<attr name="progress" format="float" />
<attr name="startColor" format="color" />
<attr name="endColor" format="color" />
</declare-styleable>
</resources>

View实现



这里直接继承子View,读取属性,在onDraw中绘制进度条。实现思路是通过定义Path来绘制裁切范围,确定绘制内容;再实现线性渐变LinearGradient来填充进度条。然后监听手势动作onTouchEvent,动态绘制长度。


同时开放公共方法,可以动态设置进度颜色,监听进度回调,根据需求实现即可。



package com.cs.app.view

/**
*
*/

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import com.cs.app.R

class CustomProgressView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val progressPaint = Paint()
private val backgroundPaint = Paint()
private var progress = 50f
private var startColor = Color.parseColor("#4C87B7")
private var endColor = Color.parseColor("#A3D5FE")
private var x = 0f
private var progressCallback: ProgressChange? = null

init {
// 初始化进度条画笔
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.FILL

// 初始化背景画笔
backgroundPaint.isAntiAlias = true
backgroundPaint.style = Paint.Style.FILL
backgroundPaint.color = Color.GRAY

if (attrs != null) {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.progress)
startColor = typedArray.getColor(R.styleable.progress_startColor, startColor)
endColor = typedArray.getColor(R.styleable.progress_endColor, endColor)
progress = typedArray.getFloat(R.styleable.progress_progress, progress)
typedArray.recycle()
}
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val width = width.toFloat()
val height = height.toFloat()

//绘制Path,限定Canvas边框
val path = Path()
path.addRoundRect(0f, 0f, width, height, height / 2, height / 2, Path.Direction.CW)
canvas.clipPath(path)

//绘制进度条
val progressRect = RectF(0f, 0f, width * progress / 100f, height)
val colors = intArrayOf(startColor, endColor)
val shader = LinearGradient(0f, 0f, width * progress / 100f, height, colors, null, Shader.TileMode.CLAMP)
progressPaint.shader = shader
canvas.drawRect(progressRect, progressPaint)
}

override fun onTouchEvent(event: android.view.MotionEvent): Boolean {
when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
x = event.rawX

//实现点击调整进度
progress = (event.rawX - left) / width * 100
progressCallback?.onProgressChange(progress)
invalidate()
}

android.view.MotionEvent.ACTION_MOVE -> {
//实现滑动调整进度
progress = (event.rawX - left) / width * 100
progress = if (progress < 0) 0f else if (progress > 100) 100f else progress
progressCallback?.onProgressChange(progress)
invalidate()
}

else -> {}
}
return true
}

fun setProgress(progress: Float) {
this.progress = progress
invalidate()
}

fun setOnProgressChangeListener(callback: ProgressChange) {
progressCallback = callback
}

interface ProgressChange {
fun onProgressChange(progress: Float)
}
}

示例


class CustomViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_view)
val progressTv: TextView = findViewById(R.id.progress_textview)
val view: CustomProgressView = findViewById(R.id.progress)
view.setProgress(50f)

view.setOnProgressChangeListener(object : CustomProgressView.ProgressChange {
override fun onProgressChange(progress: Float) {
progressTv.text = "${progress.toInt()}%"
}
})
}
}

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#0E1D3C"
android:layout_height="match_parent">


<com.cs.app.view.CustomProgressView
android:id="@+id/progress"
android:layout_width="200dp"
android:layout_height="45dp"
app:endColor="#A3D5FE"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.274"
app:progress="60"
app:startColor="#4C87B7" />


<TextView
android:id="@+id/progress_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:textColor="#ffffff"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progress" />

</androidx.constraintlayout.widget.ConstraintLayout>

效果如下:


HnVideoEditor_2023_11_23_144034156.gif
作者:LANFLADIMIR
来源:juejin.cn/post/7304531564342837287
收起阅读 »

前端半自动化部署

web
在前端项目部署时,通常会经历以下步骤: 构建项目:在部署之前,需要使用相应的构建工具(如Webpack、Vite等)对项目进行构建,生成生产环境所需的静态文件(如HTML、CSS、JavaScript、图片等)。构建过程中通常会进行代码压缩、打包、资源优化...
继续阅读 »

在前端项目部署时,通常会经历以下步骤:


image.png



  1. 构建项目:在部署之前,需要使用相应的构建工具(如WebpackVite等)对项目进行构建,生成生产环境所需的静态文件(如HTMLCSSJavaScript、图片等)。构建过程中通常会进行代码压缩、打包、资源优化等操作。

  2. 选择部署方式:根据项目的实际需求,选择适合的部署方式。常见的部署方式包括将静态文件部署到静态文件托管服务(如NetlifyVercelGitHub Pages等)、与后端API服务一起部署到云服务器(如AWS、阿里云、腾讯云等)等。

  3. 配置域名和SSL证书:如果你有自定义域名,需要在域名服务商处将域名解析到部署好的静态文件托管服务或云服务器上。同时,为了保障网站的安全性,建议配置SSL证书,使网站能够通过HTTPS协议进行访问。

  4. 持续集成/持续部署(CI/CD :可以考虑使用CI/CD工具(如GitHub ActionsGitLab CITravis CI等)来实现自动化的构建和部署流程,以提高开发效率并确保部署过程的稳定性。

  5. 性能优化:在部署完成后,可以对网站进行性能优化,包括使用CDN加速、资源压缩、缓存配置等,以提高网站的加载速度和用户体验。

  6. 监控和日志:部署完成后,建议设置监控系统以及日志记录系统,及时发现和解决线上问题。


具体的部署流程会因项目和需求的不同而有所差异。


本文从部署方案一步步做实践,最终实现半自动化部署,当然也可以直接使用Docker或其他方案实现自动化部署。


手动化的部署流程是利用xshell连接服务器,利用xftp进行文件传输。操作流程相对比较原始化。


image.png


假如要实现协同开发人员可以实现共同部署,并且可以减去每次部署都要打开xshell。可以写一段脚本实现连接服务器进行文件传输过程,保证打包后能够运行脚本自动化上传文件。


首先要解决连接服务器问题,可以通过ssh实现。(ftpssh是两种常用的远程文件传输协议,可以高效地将代码上传到服务器。)


await ssh.connect({
host: '主机名',
username: '用户名',
password: '密码'
})

服务器连接成功后,可以进行文件传输


await ssh.putDirectory('本地目录路径', '远程目录路径', {
recursive: true, // 上传整个目录
concurrency: 10, // 同时上传的文件数量
tick(localPath, remotePath, error) { // 通过tick回调函数来监听上传过程中的状态
if(error) {
console.log(`无法上传${localPath}${remotePath}${error}`)
} else {
console.log(`${localPath}上传至${remotePath}`)
}
}
})

此时即可实现脚本的基本功能,只需要在每次npm run build结束后,自动执行这段脚本即可。


因此,只需要在package.json文件中scripts命令下添加一行代码即可。


"build:deploy": "vue-cli-service build && node deploy.js"

这段代码因不同的框架版本可能有所不同,只需要在普通npm run build执行内容后面拼接node deploy.jsdeploy.js就是我们所写的脚本文件。


打包完之后,上传文件过程如图所示,非常丝滑:


企业微信截图_17006194685935.png


此时会有一个问题,打包的dist文件每次上传至服务器时,dist文件一直被覆盖,无法实现按版本回滚。


只需要在上传之前,修改服务器旧的dist文件名,这样旧版本就得以保存。


// 判断服务器dist文件是否存在
let newDistExist = await ssh.execCommand(`ls 旧版本`)
while(newDistExist.code === 0) {
i ++
newDistExits = await ssh.execCommand(`ls 旧版本i`)
}
// 重命名旧版本dist文件
await ssh.execCommand(`mv 旧版本 新版本`)

此时即可实现旧版本保存,以便可以按版本实现回滚操作。


上述过程仅仅是针对个人打包上传服务器,如果想要实现协同开发,则要实现代码共享(例如上传git仓库),为了安全性,账号密码不能以明文暴露。


可以采取以下三种方案,当然没有绝对意义上的安全。



  1. terminal实现账号密码输入


可以利用password-prompt插件,它可以帮助你在命令行中以安全的方式提示用户输入密码。


const getUserInfo = async() => {
const username = await passwordPrompt('输入用户名:')
const password = await passwordPrompt('输入密码:', { method: 'hide' })
return { username, password }
}

将输入的usernamepassword传到远程服务器进行校验即可。



  1. 账号密码加密


由于代码要上传git,所以可以在git上传前进行加密处理,调用gitpre-commit钩子,执行加密,每次pull时候进行解密处理,调用post-merge钩子,调用post-merge钩子时候仅仅需要输入解密口令即可。解密口令只需要做到组员共享即可,此时的解密口令同样可以借助password-prompt插件进行输入。相对第一种方案,输入的内容更少了😂。


const crypto = require('crypto') // 密钥和加密算法 
const secretKey = 'your-secret-key' // 这里替换为你自己的密钥
const algorithm = 'aes-256-cbc' // 使用的加密算法
function decryptData(encryptedData) {
const decipher = crypto.createDecipher(algorithm, secretKey)
let decryptedData = decipher.update(encryptedData, 'hex', 'utf8')
decryptedData += decipher.final('utf8')
return decryptedData
}
// 从环境变量或其他安全方式获取加密的敏感信息
const encryptedInfo = process.env.ENCRYPTED_INFO // 这里假设加密的信息存储在环境变量中
// 解密敏感信息
const decryptedInfo = decryptData(encryptedInfo)
// 在这里使用解密后的敏感信息进行后续操作
console.log('Decrypted info:', decryptedInfo)

git的钩子函数可以采用git hooks工具哈士奇(husky)进行配置,具体配置方式不再赘述。


以上就是本文在前端半自动化部署方面的探索,大家可以贡献自己的想法/做法呀!


作者:一颗多愁善感的派大星j
来源:juejin.cn/post/7303862023618805795
收起阅读 »

极简原生js图形验证码

web
       前天接到需求要在老项目登陆界面加上验证码功能,因为是内部项目且无需短信验证环节,那就直接用原生js写一个简单的图形验证码。 示例: 思路:此处假设验证码为4位随机数值,数值刷新满足两个条件①页面新进/刷新。②点击图片刷新。(实际情况下还要考虑...
继续阅读 »

       前天接到需求要在老项目登陆界面加上验证码功能,因为是内部项目且无需短信验证环节,那就直接用原生js写一个简单的图形验证码。


示例:


1700643433840.png



思路:此处假设验证码为4位随机数值,数值刷新满足两个条件①页面新进/刷新。②点击图片刷新。(实际情况下还要考虑登录出错刷新,此处只做样式不写进去)
实现过程为1.先写一个canvas标签做绘图容器。    →     2.将拿到的值绘制到容器中并写好样式。    →     3.点击刷新重新绘制。



写一个canvas标签当容器


<canvas
style="width: 100px;border: 2px solid rgb(60, 137, 209);background-image: url('https://gd-hbimg.huaban.com/aa3c7f23dfdc7b2d317aa4b77bd6c7b8469564d2dfa8b-Btd5c6_fw658webp');"
id="captchaCanvas">
</canvas>

并设置容器宽高背景颜色或图片等样式


写一个数值绘制到canvas的方法


//text为传递的数值
function generateCaptcha(text, callback) {
var canvas = document.getElementById('captchaCanvas');
var ctx = canvas.getContext('2d');
// 设置字体及大小
ctx.font = '100px Comic Sans MS';
// 设置字体颜色
ctx.fillStyle = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整文字图形位置
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 调整阴影范围
ctx.shadowBlur = Math.random() * 20;
// 调整阴影颜色
ctx.shadowColor = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整阴影位置(偏移量)
ctx.shadowOffsetX = Math.random() * 10;
ctx.shadowOffsetY = Math.random() * 10;
// 绘制文字图形及其偏移量
ctx.fillText(text, 25, 35);
// 绘制文字边框及其偏移量
ctx.strokeText(text, Math.random() * 35, Math.random() * 45);

var imgDataUrl = canvas.toDataURL();
callback(imgDataUrl);

}

拿到数值调用绘制方法



此处为样式示例,因此数值我用4位随机数表示,实际情况为你从后端取得的值,并依靠这个值在后端判断验证码是否一致。



// 调用函数生成验证码并显示在页面上  
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });

监听标签点击实现点击刷新



此处要注意一定要先清空canvas中已绘制图像再渲染新数值,因此直接将清除范围设置较大。



 // 监听点击更新验证码
document.getElementById("captchaCanvas").addEventListener("click", function (event) {
// 清空画布
document.getElementById("captchaCanvas").getContext("2d").clearRect(0, 0, 9999, 9999);
// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
})

最后实现效果:


1700645148990.png


完整代码演示


<!DOCTYPE html>
<html>

<head>
<title>String to Captcha</title>
</head>

<body>
<canvas
style="width: 100px;border: 2px solid rgb(60, 137, 209);background-image: url('https://gd-hbimg.huaban.com/aa3c7f23dfdc7b2d317aa4b77bd6c7b8469564d2dfa8b-Btd5c6_fw658webp');"
id="captchaCanvas">
</canvas>


<script>
// 监听点击更新验证码
document.getElementById("captchaCanvas").addEventListener("click", function (event) {
// 清空画布
document.getElementById("captchaCanvas").getContext("2d").clearRect(0, 0, 9999, 9999);
// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
})

function generateCaptcha(text, callback) {
var canvas = document.getElementById('captchaCanvas');
var ctx = canvas.getContext('2d');
// 设置字体及大小
ctx.font = '100px Comic Sans MS';
// 设置字体颜色
ctx.fillStyle = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整文字图形位置
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 调整阴影范围
ctx.shadowBlur = Math.random() * 20;
// 调整阴影颜色
ctx.shadowColor = 'rgb(' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ', ' + Math.floor(Math.random() * 256) + ')';
// 调整阴影位置(偏移量)
ctx.shadowOffsetX = Math.random() * 10;
ctx.shadowOffsetY = Math.random() * 10;
// 绘制文字图形及其偏移量
ctx.fillText(text, 25, 35);
// 绘制文字边框及其偏移量
ctx.strokeText(text, Math.random() * 35, Math.random() * 45);

var imgDataUrl = canvas.toDataURL();
callback(imgDataUrl);

}

// 调用函数生成验证码并显示在页面上
generateCaptcha(Math.floor(Math.random() * 9000) + 1000, function (imgDataUrl) { });
</script>
</body>

</html>

作者:方苕爱吃瓜
来源:juejin.cn/post/7304182005285830693
收起阅读 »

[compose] 仿刮刮乐效果

web
需求 下班路上新开了一家彩-票店,路过时总是心痒,本着小D怡情的心态,偶尔去刮几张,可是随着时间久了,发现也花了不少钱,看网上有人开发电子木鱼,突然奇想,为什么不做一张电子彩-票。 分析 传统View,网上有很多解决方案,大多数是通过混合模式进行两个图层的合并...
继续阅读 »

需求


下班路上新开了一家彩-票店,路过时总是心痒,本着小D怡情的心态,偶尔去刮几张,可是随着时间久了,发现也花了不少钱,看网上有人开发电子木鱼,突然奇想,为什么不做一张电子彩-票。


分析


传统View,网上有很多解决方案,大多数是通过混合模式进行两个图层的合并。


大致思路:

1、使用onDraw()方法中的Canvas绘制底层中奖层

2、在上面绘制一个蒙版层Bitmap, 在蒙版层Bitmap里面,放置一个新的Canvas

3、绘制一个灰色的矩阵,绘制一个path,将paint的Xfermode设置为 PorterDuff.Mode.DST_IN
4、手指移动时,更新Path路径


Compose实现

1、通过实现DrawModifier,重写draw() 方法

2、绘制原始内容层,drawContent()
3、绘制蒙版和手势层,


//配置画笔 blendMode = Xfermode
private val pathPaint = Paint().apply {
alpha = 0f
style = PaintingStyle.Stroke
strokeWidth = 70f
blendMode = BlendMode.SrcIn
strokeJoin = StrokeJoin.Round
strokeCap = StrokeCap.Round
}

drawIntoCanvas {
//设置画布大小尺寸
val rect = Rect(0f, 0f, size.width, size.height)
//从原始画布层,转换一个新的画布层
it.saveLayer(rect, layerPaint)
//设置新画布大小尺寸
it.drawRect(rect, layerPaint)
startPath.lineTo(moveOffset.x, moveOffset.y)
//绘制手指移动path
it.drawPath(startPath, pathPaint)
it.restore()
}

完整代码


fun ScrapeLayePage(){
var linePath by remember { mutableStateOf(Offset.Zero) }
val path by remember { mutableStateOf(Path()) }
Column(modifier = Modifier
.fillMaxWidth()
.pointerInput("dragging") {
awaitEachGesture {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
//按住时,更新起始点
Press -> {
path.moveTo(
event.changes.first().position.x,
event.changes.first().position.y
)
}
//移动时,更新起始点 移动时,记录路径path
Move -> {
linePath = event.changes.first().position
}
}
}
}
}
.scrapeLayer(path, linePath)
) {
Image(
modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.mipmap.cat),
contentDescription = ""
)
Text(text = "这是一只可爱的猫咪~~")
}
}

fun Modifier.scrapeLayer(startPath: Path, moveOffset: Offset) =
this.then(ScrapeLayer(startPath, moveOffset))

class ScrapeLayer(private val startPath: Path, private val moveOffset: Offset) : DrawModifier {

private val pathPaint = Paint().apply {
alpha = 0f
style = PaintingStyle.Stroke
strokeWidth = 70f
blendMode = BlendMode.SrcIn
strokeJoin = StrokeJoin.Round
strokeCap = StrokeCap.Round
}

private val layerPaint = Paint().apply {
color = Color.Gray
}

override fun ContentDrawScope.draw() {
drawContent()
drawIntoCanvas {
val rect = Rect(0f, 0f, size.width, size.height)
//从当前画布,裁切一个新的图层
it.saveLayer(rect, layerPaint)
it.drawRect(rect, layerPaint)
startPath.lineTo(moveOffset.x, moveOffset.y)
it.drawPath(startPath, pathPaint)
it.restore()
}
}
}

图片.png

参考资料



作者:Gx
来源:juejin.cn/post/7303075105390133259
收起阅读 »

微信小程序记住密码,让登录解放双手

web
密码是用户最重要的数据,也是系统最需要保护的数据,我们在登录的时候需要用账号密码请求登录接口,如果用户勾选记住密码,那么下一次登录时,我们需要将账号密码回填到输入框,用户可以直接登录系统。我们分别对这种流程进行说明: 记住密码 在请求登录接口成功后,我们需要判...
继续阅读 »

密码是用户最重要的数据,也是系统最需要保护的数据,我们在登录的时候需要用账号密码请求登录接口,如果用户勾选记住密码,那么下一次登录时,我们需要将账号密码回填到输入框,用户可以直接登录系统。我们分别对这种流程进行说明:


记住密码


在请求登录接口成功后,我们需要判断用户是否勾选记住密码,如果是,则将记住密码状态账号信息存入本地。
下次登录时,获取本地的记住密码状态,如果为true则获取本地存储的账号信息,将信息回填登录表单。

在这里插入图片描述

在这里插入图片描述


密码加密


我在这里例举两种加密方式MD5Base64
MD5:
1、不可逆
2、任意长度的明文字符串,加密后得到的固定长度的加密字符串
3、实质是一种散列表的计算方式


Base64:
1、可逆性
2、可以将图片等二进制文件转换为文本文件
3、可以把非ASCII字符的数据转换成ASCII字符,避免不可见字符
4、实质是 一种编码格式,如同UTF-8


我们这里使用Base64来为密码做加密处理。


npm install --save js-base64

引入Base64


// js中任意位置都可引入
let Base64 = require('js-base64').Base64;

可以通过encodedecode对字符串进行加密和解密


let Base64 = require('js-base64').Base64;

let pwd = Base64.encode('a123456');
console.log(pwd); // YTEyMzQ1Ng==

let pws2 = Base64.decode('YTEyMzQ1Ng==');
console.log(pwd2); // a123456

到这里我们对密码的简单加密和解密就完成了。
需要注意的是,Base64是可以解密的,所以单纯使用Base64进行加密是不安全的,所以我们要对Base64进行二次加密操作,生成一个随机字符串 + Base64的加密字符。


/***
*
@param {number} num 需要生成多少位随机字符
*
@return {string} 生成的随机字符
*/

const randomString = (num) => {
let str = "",
arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
let index = null;
for (let i = 0; i < num; i++) {
index = Math.round(Math.random() * (arr.length - 1));
str += arr[index];
}
return str;
}

调用randomString函数,根据你传入的数字来生成指定长度的随机字符串,然后将随机字符串与Base64生成的随机字符凭借,完成对密码的二次加密。


let pwd = randomWord(11) + Base64.encode(password); // J8ndUzNIPTtYTEyMzQ1Ng==

到这里就完成了密码加密操作。
在用户登录时,将账号密码存入本地,存入本地方式有很多,例如:CookieslocalStoragesessionStorage等,关于使用方法网上有很多,这里我们使用微信小程序的存储方式wx.setStorageSyn


// 我们这里使用微信小程序的存储方式wx.setStorageSync
let account = {
username: 'test‘,
password: pwd
}
wx.setStorageSync('
account', account)

在这里插入图片描述


二次登录


用户勾选记住密码后,第二次进入系统,直接从本地获取账号密码,对密码进行解密后回填到表单。
先判断用户是否勾选记住密码,然后对密码进行解密。


init() {
let state = wx.getStorageSync('rememberMe')
if (state) {
let account = wx.getStorageSync('account')
let Base64 = require('js-base64').Base64;
let pwd = Base64.decode(account.password.slice(11))
this.setData({
username: account.username,
password: pwd
})
}
this.setData({ rememberMe: state })
}

将解密后的数据回显到表单上,用户就可以直接登录了。


最后


关于记住密码业务,需要保证用户的密码是加密存储,这里用的是微信小程序示例,在web上的流程也是如此,你可以在vue项目中使用本文提到的方法。


作者:DCodes
来源:juejin.cn/post/7303739766106472488
收起阅读 »

登录是前端做全栈的必修课

web
如何在前端实现自动或无感化的登录态管理,包括用户注册、登录、接口校验登录态以及实现自动化请求时自动携带访问令牌。我们将探讨两种常见的实现方式:使用 HTTP Cookie 和前端存储和发送访问令牌。 1. 注册和登录 首先,用户需要通过注册和登录来获取访问令牌...
继续阅读 »

如何在前端实现自动或无感化的登录态管理,包括用户注册、登录、接口校验登录态以及实现自动化请求时自动携带访问令牌。我们将探讨两种常见的实现方式:使用 HTTP Cookie 和前端存储和发送访问令牌。


1. 注册和登录


首先,用户需要通过注册和登录来获取访问令牌。


1.1 注册接口


在注册接口中,用户提供必要的注册信息(如用户名和密码),服务器对用户进行验证并创建用户账户。


示例代码(Node.js + Express):


// 注册接口
app.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;

// 检查用户名和邮箱是否已被注册
if (users.some(user => user.username === username)) {
return res.status(400).json({ error: '用户名已被注册' });
}

if (users.some(user => user.email === email)) {
return res.status(400).json({ error: '邮箱已被注册' });
}

// 使用bcrypt对密码进行哈希处理
const hashedPassword = await bcrypt.hash(password, 10);

// 创建新用户对象
const user = {
id: Date.now().toString(),
username,
email,
password: hashedPassword
};

// 将用户信息存储到数据库
users.push(user);

// 创建访问令牌
const token = jwt.sign({ userId: user.id }, 'secretKey');

res.status(201).json({ message: '注册成功', token });
} catch (error) {
res.status(500).json({ error: '注册失败' });
}
});

1.2 登录接口


在登录接口中,用户提供登录凭据(如用户名和密码),服务器验证凭据的正确性并颁发访问令牌。


示例代码(Node.js + Express):


app.post('/login', (req, res) => {
// 获取登录凭据
const { username, password } = req.body;

// 在此处进行用户名和密码的验证,如检查用户名是否存在、密码是否匹配等

// 验证成功,颁发访问令牌
const token = createAccessToken(username);

// 将访问令牌写入 Cookie
res.cookie('token', token, {
httpOnly: true,
secure: true, // 仅在 HTTPS 连接时发送 Cookie
sameSite: 'Strict' // 限制跨站点访问,提高安全性
});

// 返回登录成功的响应
res.status(200).json({ message: '登录成功' });
});

2. 接口校验登录态


在需要校验登录态的受保护接口中,服务器将校验请求中的登录凭据(Cookie 或访问令牌)的有效性。


示例代码(Node.js + Express):


app.get('/protected', (req, res) => {
// 从请求的 Cookie 中提取访问令牌
const token = req.cookies.token;

// 或从请求头部中提取访问令牌,如果采用前端存储和发送访问令牌方式
// const token = req.headers.authorization.split(' ')[1]; // 示例代码,需根据实际情况进行解析

// 检查访问令牌的有效性
if (!token) {
return res.status(401).json({ error: '未提供访问令牌' });
}

try {
// 验证访问令牌
const decoded = verifyAccessToken(token);

// 在此处进行更详细的用户权限校验等操作

// 返回受保护资源
res.status(200).json({ message: '访问受保护资源成功' });
} catch (error) {
res.status(401).json({ error: '无效的访问令牌' });
}
});

3. 自动化登录态管理


要实现自动或无感化的登录态管理,前端需要在每个请求中自动携带访问令牌(Cookie 或请求头部)。


3.1 使用 HTTP Cookie


当使用 HTTP Cookie 时,浏览器会自动将 Cookie 包含在每个请求的头部中,无需手动设置。


示例代码(前端使用 JavaScript):


// 发送请求时,浏览器自动携带 Cookie
fetch('/protected');

3.2 前端存储和发送访问令牌


当使用前端存储和发送访问令牌时,前端需要在每个请求的头部中手动设置访问令牌。


示例代码(前端使用 JavaScript):


// 从存储中获取访问令牌
const token = localStorage.getItem('token');

// 设置请求头部
const headers = {
'Authorization': `Bearer ${token}`
};

// 发送请求时,手动设置请求头部
fetch('/protected', { headers });

在上述示例代码中,我们使用了前端的 localStorage 来存储访问令牌,并在发送请求时手动设置了请求头部的 Authorization 字段。


请注意,无论使用哪种方式,都需要在服务器端进行访问令牌的验证和安全性检查,以确保请求的合法性和保护用户数据的安全。


补充说明:



  • createUser:自定义函数,用于创建用户账户并将其保存到数据库或其他持久化存储中。

  • createAccessToken:自定义函数,用于创建访问令牌。

  • verifyAccessToken:自定义函数,用于验证访问令牌的有效性。


写在最后


文章旨在答疑扫盲,内容简明扼要方便学习了解,请确保在实际应用中采取适当的安全措施来保护用户的登录凭据和敏感数据,保持学习,共勉~


作者:vin_zheng
来源:juejin.cn/post/7303463043249635362
收起阅读 »

你知道为什么template中不用加.value吗?

web
Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。 询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢? proxyRefs Vue3 中有有个方法prox...
继续阅读 »

Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。


询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢?


proxyRefs


Vue3 中有有个方法proxyRefs,这属于底层 API 方法,在官方文档中并没有阐述,但是 Vue 里是可以导出这个方法。


例如:


<script setup>
import { onMounted, proxyRefs, ref } from "vue";

const user = {
name: "wendZzoo",
age: ref(18),
};
const _user = proxyRefs(user);

onMounted(() => {
console.log(_user.name);
console.log(_user.age);
console.log(user.age);
});
</script>

上面代码定义了一个普通对象user,其中age属性的值是ref类型。当访问age值的时候,需要通过user.age.value,而使用了proxyRefs,可以直接通过user.age来访问。



这也就是为何template中不用加.value的原因,Vue3 源码中使用proxyRefs方法将setup返回的对象进行处理。


实现proxyRefs


单测


it("proxyRefs", () => {
const user = {
name: "jack",
age: ref(10),
};
const proxyUser = proxyRefs(user);

expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);

proxyUser.age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);

proxyUser.age = ref(30);
expect(proxyUser.age).toBe(30);
expect(user.age.value).toBe(30);
});

定义一个age属性值为ref类型的普通对象userproxyRefs方法需要满足:



  1. proxyUser直接访问age是可以直接获取到 10 。

  2. 当修改proxyUserage值切这个值不是ref类型时,proxyUser和原数据user都会被修改。

  3. age值被修改为ref类型时,proxyUseruser也会都更新。


实现


既然是访问和修改对象内部的属性值,就可以使用Proxy来处理getset。先来实现get


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {}
});
}

需要实现的是proxyUser.age能直接获取到数据,那原数据target[key]ref类型,只需要将ref.value转成value


使用unref即可实现,unref的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


get(target, key) {
return unref(Reflect.get(target, key));
}

实现set


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
return unref(Reflect.get(target, key));
},
set(target, key, value) {},
});
}

从单侧中可以看出,我们是测试了两种情况,一种是修改proxyUserageref类型, 一种是修改成不是ref类型的,但是结果都是同步更新proxyUseruser。那实现上也需要考虑这两种情况,需要判断原数据值是不是ref类型,新赋的值是不是ref类型。


使用isRef可以判断是否为ref类型,isRef的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


set(target, key, value) {
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value);
}
}

当原数据值是ref类型且新赋的值不是ref类型,也就是单测中第 1 个情况赋值为 10,将ref类型的原值赋值为valueref类型值需要.value访问;否则,也就是单测中第 2 个情况,赋值为ref(30),就不需要额外处理,直接赋值即可。


验证


执行单测yarn test ref



作者:wendZzoo
来源:juejin.cn/post/7303435124527333416
收起阅读 »

直播点赞喷射表情效果实现

web
最近在线看直播年会。有一个点赞的按钮,点击点赞按钮喷射表情,表情在屏幕上向上浮动之后消失。觉得这个效果挺具有代表性,所以想实现一下。 找了一个别人的效果图 就来实现这个效果。 写一个点赞按钮 <style> .like-box { ...
继续阅读 »

最近在线看直播年会。有一个点赞的按钮,点击点赞按钮喷射表情,表情在屏幕上向上浮动之后消失。觉得这个效果挺具有代表性,所以想实现一下。


找了一个别人的效果图


点赞.gif


就来实现这个效果。


写一个点赞按钮


  <style>
.like-box {
width: 48px;
height: 48px;
border-radius: 50%;
background-color: #ddd;
position: relative;
top: 360px;
display: flex;
align-items: center;
justify-content: center;
left: 300px;
cursor: pointer;
}

.like-box i {
font-size: 25px;
}
</style>
<div class="like-box" id="like-box">
<i class="icon-dianzan iconfont"></i>
</div>

1700297207118.png


其中中间的图标用的是阿里巴巴矢量图标库的图标字体。


动态创建表情


动态表情用一个div表示。背景是表情图片。有6个备选表情


image.png


div样式


 .like-box div {
position: absolute;
width: 48px;
height: 48px;
background-image: url("./public/images/bg1.png");
background-size: cover;
z-index: -1;
}

使用js创建表情图标,并插入到点赞div


const likeBox = document.getElementById('like-box') // id为like-box
const createFace = function () {
const div = document.createElement('div')
return div
}

likeBox.addEventListener('click', function () {
const face = createFace()
likeBox.appendChild(face)
})

实现表情动画效果


从最终效果图中可以看出,最终效果是由多个表情组成,更准确的说是由多个不同运动轨迹表情实现。所以关键是表情的轨迹实现。


而在运动轨迹过程中,有大小缩放效果、有淡出效果。所以至少有三个animation。


实现缩放效果


使用animation实现缩放效果,添加animation样式


    @keyframes scale {
0% {
transform: scale(0.3);
}

100% {
transform: scale(1.2);
}
}

当动态创建表情div时,将缩放效果添加到div上,添加后效果


缩放效果.gif


实现淡出效果


使用animation实现淡出效果,添加animation样式


    @keyframes opacity {
0% {
top: 0;
}

10% {
top: -10px;
}

75% {
opacity: 1;
top: -180px;

}

100% {
top: -200px;
opacity: 0;
}
}

当动态创建表情div时,将淡出效果添加到div上,添加后具体效果


淡入淡出.gif


实现不同轨迹效果


单一轨迹

创建单一轨迹样式效果


 @keyframes swing_1 {
0% {}

25% {
left: 0;
}

50% {
left: 8px;
}

75% {
left: -15px;
}

100% {
left: 15px;
}
}

当动态创建表情div时,将单一轨迹样式效果添加到div上,添加后具体效果


单一轨迹.gif


多轨迹

多轨迹有点麻烦,但是也不是很麻烦。具体思路是创建多个轨迹样式,然后在动态创建表情时给表情div随机添加各种轨迹样式,添加后具体效果


多轨迹.gif


最终js代码


const likeBox = document.getElementById('like-box')

const createFace = function () {
// 随机表情
const face = Math.floor(Math.random() * 6) + 1;
// 随机轨迹
const trajectory = Math.floor(Math.random() * 11) + 1; // bl1~bl11

const div = document.createElement('div')

div.className = `face${face} trajectory${trajectory}`;
return div
}


likeBox.addEventListener('click', function () {
const face = createFace()
likeBox.appendChild(face)
})

移除产生的表情div


为了避免一直添加div,乃至最后降低动画性能,需要等animation结束后移除动画div


const likeBox = document.getElementById('like-box')

const createFace = function () {
const face = Math.floor(Math.random() * 6) + 1;
const trajectory = Math.floor(Math.random() * 11) + 1; // bl1~bl11

const div = document.createElement('div')

div.className = `face${face} trajectory${trajectory}`;
// 移除div
div.addEventListener("animationend", () => {
if(likeBox.contains(div)){
likeBox.removeChild(div)
}
});
return div
}

likeBox.addEventListener('click', function () {
const face = createFace()
likeBox.appendChild(face)
})

总结


所有的效果实现都通过css的animation实现。实际还可以使用canvas实现。关键是实现的思路。


核心思路:使用css的animation实现,基于对动画效果的拆分;拆出单一效果,之后所有效果同时发挥作用。


最近想到一件事,初高中学到的数学知识应该可以应用在开发中。有些前端效果实际就是数学知识的一种应用。更准确的说是高中数学函数图形以及几何图形那块,这两块都有曲线的函数或者方程表示。


如果要实现的效果是曲线或者轨迹的话,完全可以考虑它的坐标关系是不是数学中学到的。进而知道关系,进而开发出效果。


我一直想把中学和大学的知识应用,我想上面就是应用的一个点。


代码地址:github.com/zhensg123/r…


(本文完)


参考文章


H5 直播的疯狂点赞动画是如何实现的?(附完整源码)


作者:通往自由之路
来源:juejin.cn/post/7303463043248291874
收起阅读 »

无感刷新,我想说说这三种方案

web
现在当你想去找一个无感刷新的方案的时候,搜出来的大多都是教你在aioxs的相应拦截器里面去截取当前请求的config。然后当token刷新后再去请求失败的接口。首先声明,这个方案完全没有任何问题。只是有可以优化的地方,这个优化的地方可以在我将要写的第二种方案中...
继续阅读 »

现在当你想去找一个无感刷新的方案的时候,搜出来的大多都是教你在aioxs的相应拦截器里面去截取当前请求的config。然后当token刷新后再去请求失败的接口。首先声明,这个方案完全没有任何问题。只是有可以优化的地方,这个优化的地方可以在我将要写的第二种方案中得到解决。


准备工作


接口服务


在实行方案之前需要准备好相关的接口服务,我会用node写一些登录刷新和正常的业务接口, 点这里查看


简单介绍下准备的接口及其作用



  • /login: 模拟登录并返回tokenrefreshToken

  • /refreshToken: 当token过期,请求这个接口会获得新的tokenrefreshToken,接口需要传入通过/login接口或者/refreshToken接口返回的refreshToken。当refreshToken也判断过期,就只能去登陆页。

  • /test1: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401

  • /test2: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401

  • /test3: 模拟正常的业务请求接口,会验证token是否有效及是否过期,过期返回401


token的过期时间设置为5秒。


axios的封装


我们使用axios都会进行二次封装,都会在拦截器里面处理一些逻辑。这里给出一个最基本的封装,后面都会用到。


import axios from "axios";

export const service = axios.create({
  timeout: 1000 * 30,
  baseURL: "http://192.168.0.102:9001",
});

service.interceptors.request.use(
  (config) => {
    let token = localStorage.getItem("token");
    config.headers["Authorization"] = token;
    return config;
  },
  (err) => Promise.reject(err)
);

service.interceptors.response.use(
  (res) => {
    return res.data;
  },
  (err) => {
    return Promise.reject(err);
  }
);

export const get = (url, params) => {
  return new Promise((resolve, reject) => {
    service
      .get(url, { params })
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

export const post = (url, data = {}) => {
  return new Promise((resolve, reject) => {
    service
      .post(url, data)
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

首先还是介绍下最广泛的使用方案


在axios的响应拦截器里面处理


这种方案的工作流程都是在相应拦截器里面处理的,当判断到接口401,则请求刷新token接口,并改变一个状态来表明当前正在执行刷新token,这样可以避免多个请求同时401的时候所导致的一次401就会请求一次刷新token接口。接着将紧随着报401的接口保存起来,等到刷新token接口成功后再去执行这些失败的接口。完整代码,在上文的二次封装的axios基础上进行更改。


import axios from "axios";
----------------------------------------------------------------新增
import { refreshToken } from "@/api/login";
----------------------------------------------------------------新增

export const service = axios.create({
  timeout: 1000 * 30,
  baseURL: "http://192.168.0.102:9001",
});

service.interceptors.request.use(
  (config) => {
    let token = localStorage.getItem("token");
    config.headers["Authorization"] = token;
    return config;
  },
  (err) => Promise.reject(err)
);

----------------------------------------------------------------新增
let inRefreshing = false; // 当前是否正在请求刷新token
let wating = []; // 报401的接口 加入等待列表 刷新接口成功后统一请求
----------------------------------------------------------------新增

service.interceptors.response.use(
  (res) => {
    return res.data;
  },
  (err) => {
----------------------------------------------------------------新增
    let { config } = err.response;

    if (inRefreshing) { // 刷新token正在请求,把其他的接口加入等待数组
      return new Promise((resolve) => {
        wating.push({
          config,
          resolve,
        });
      });
    }

    if (err?.response?.status === 401) {
      inRefreshing = true;

      refreshToken({ refreshToken: localStorage.getItem("refreshToken") }).then(
        (res) => {
          const { success, response } = res;
          if (success) {
            inRefreshing = false;

            const { token, refreshToken } = response;
            localStorage.setItem("token", token);
            localStorage.setItem("refreshToken", refreshToken);

// 刷新token请求成功,等待数据的失败接口重新发起请求
            wating.map(({ config, resolve }) => {
              resolve(service(config));
            });
            wating = []; // 请求完之后清空等待请求的数组

            return service(config); // 当前接口重新发起请求
          } else {
            // 刷新token失败  重新登录
          }
        }
      );
    }
----------------------------------------------------------------新增
    return Promise.reject(err);
  }
);

export const get = (url, params) => {
  return new Promise((resolve, reject) => {
    service
      .get(url, { params })
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

export const post = (url, data = {}) => {
  return new Promise((resolve, reject) => {
    service
      .post(url, data)
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
};

有些同学可能对上面代码有些疑问,感觉wating里面的数据不对,比如同时发送三个接口报401,wating只会加入两条接口数据,还有一条在哪儿。其实就在第60行返回了。


然后再对接口进行简单的封装


import { post } from "./index";

export const login = () => post("/login");
export const refreshToken = (data) => post("/refreshToken", data);

export const test1 = () => post("/test1");
export const test2 = () => post("/test2");
export const test3 = () => post("/test3");

接下来看结果。点击按钮会发送三条接口请求。

第一次点击发送请求token还未过期,第二次点击发送请求token已过期。
1.gif


完美,没啥问题!因为上面给大家展示的是最简单的axios封装,所以在响应拦截器里面加上一些接口重发的逻辑好像没啥问题。但是如果还有对接口数据的加密解密过程呢?还有控制加载的时候loading窗完美展示的问题呢?这样一看在二次封装的axios里面加上接口重发就有点儿太多了。再比如说我用的不是axios怎么办,是不是又得根据请求的插件去改一些东西。


那么与axios拦截器解耦就成了需要做的事了。


与axios拦截器解耦


我们需要创建一个构造函数,这个构造函数会将报401的接口收集起来,等到刷新token接口成功后再去请求


import { refreshToken } from "@/api/login";

function requestCollection() {
  let inRefreshing = false; // 当前是否正在请求刷新token
  let wating = []; // 报401的接口 加入等待列表 刷新接口成功后统一请求

  return (request) => {
    return new Promise((resolve) =>
      request()
        .then((res) => {
          resolve(res);
        })
        .catch(async (err) => {
          if (err.response.status == 401) {
            if (inRefreshing) {
              // 加入等待执行的数组
              return wating.push({
                request,
                resolve,
              });
            }

            inRefreshing = true;

            await RT();

            wating.map(({ resolve, request }) => {
              resolve(request());
            });
            wating = [];

            return resolve(request());
          }
        })
    );
  };
}

const RT = () => {
  return new Promise((resolve) =>
    refreshToken({
      refreshToken: localStorage.getItem("refreshToken"),
    }).then((res) => {
      const { success, response } = res;
      if (success) {
        const { token, refreshToken } = response;
        localStorage.setItem("token", token);
        localStorage.setItem("refreshToken", refreshToken);

        resolve();
      }
    })
  );
};

export default requestCollection;

其实内在的处理逻辑与第一种大同小异,主要在于将这一部分逻辑抽离出来。


使用


import { post } from "./index2";
import requestCollection from "./RequestCollection";

const check = new requestCollection();

export const login = () => check(() => post("/login"));
export const refreshToken = (data) => check(() => post("/refreshToken", data));

export const test1 = () => check(() => post("/test1"));
export const test2 = () => check(() => post("/test2"));
export const test3 = () => check(() => post("/test3"));

效果演示


2.gif


这种方案存在的一个小问题
因为需要对接口进行一层包裹,所以如果你的项目已经运行有一段时间了突然来个需求说想要个无感刷新token,那其实这个方案会随着你的项目的大小而加大你的工作量。


上述两种方案都存在的不足
当我们都在讨论无感刷新token的方案都是如何接口重发的时候,大家都忽略了一个问题,那就是接口重发的过程是发送了原本两倍+1的接口数量。如果这个接口本来就慢,恰好碰上了401,那用户就得花约两倍时间去等一个结果,相信你不愿意等,我也不愿意等。


那么被大家诟病会造成一定性能浪费的定时任务刷新就可以解决这个问题,并且定时任务刷新也是解耦于二次封装的axios。 其实都到了2023年,一个定时任务会对最终用户所使用的页面造成卡顿的影响可以说完全感知不到。但是我也不推荐大家无意识的随便使用定时器或者闭包一类能通过一点点累加所造成的内存泄露的性能问题。


定时任务刷新


这个就不给代码了,主要注意几个点就行



  • 后端需要配合返回一个token的过期时间

  • 定时任务全局存在

  • 刷新token成功后需要更新过期时间重新计算


总结


其实上面三种方案都有自己的好处和缺点,无论你使用哪一种方案,都没有问题。这些需要你结合自己的项目来选择适合你的方案。


作者:谁是克里斯
来源:juejin.cn/post/7302404170412802074
收起阅读 »

为什么我不建议中小企业使用 TypeScript

web
此博客内容,包含【极端的个人主观因素、极端的个人主观因素、极端的个人主观因素】,如不喜欢,请轻喷...... 不知道从什么时候开始,前端开发者中出现了一种 唯 TS 至上论 的思想。 如果你的项目中使用的 是JavaScript 而 不是TypeScript,...
继续阅读 »

此博客内容,包含【极端的个人主观因素、极端的个人主观因素、极端的个人主观因素】,如不喜欢,请轻喷......


不知道从什么时候开始,前端开发者中出现了一种 唯 TS 至上论 的思想。


如果你的项目中使用的 是JavaScript不是TypeScript,那么就会被打上 很low 的标签。同时也会被立刻质疑:“你为什么没有使用 TS?”


我为什么一定要使用 TypeScript 呢?


TypeScript 真的有那么的完美,值得我们在任何的场景下都优先使用吗? 恐怕不是的


任何的一门技术都是一把双刃剑,它在带来一定优势的同时,必然也会带来一定的不便性。


所以,咱们今天就来聊一聊:“为什么我不建议中小企业使用 TypeScript!”


01:不要让 TS 沦为 个人KPI


有很多中小企业的 “技术Leader”,本身并没有对 TS 进行过深入的了解。只不过是因为老板的一句:“我听说人家现在都在用 TS 啦?” 而强行在团队中推行 TS 。完全不考虑团队目前的技术方向以及团队的加班时长,这是 不可取的


所以 不要让 TS 沦为 个人KPI


当你想要在团队中推行 TS 时,你应该首先评估团队中是否有人使用过 TS,研究下大家学习 TS 所需要花费的时间。如果团队中都不熟悉 TS ,并且你也没有令人信服的理由,就不要强制团队使用 TS 啦。


02:大多数的中小企业很难适应它


说真的,“你(中小企业)花了多少钱招人,你自己心里没点 B 数吗?” 怎么着?还真打算拿着买“粉条”的钱去买“鱼翅”不成?


面试的时候,想尽办法的压薪资。工作的时候,又期望大家为你发挥出远超TA当前薪资的能力,好处都让你占了呗?


所以,别那么天真了!你的开发人员现阶段真的很难适应 TS。


如果,你真的想要在团队中推行 TS,并且希望它可以为你带来好的结果。


那么 请先培训你的团队!


拿出一定的时间和金钱,来提升你团队的技术能力和技术深度。为他们提供学习 TS 所需要的时间和课程,询问团队的意见,正确的评估你们团队的技能组合。否则你的 自私 决定,只会损害你们团队的利益,最终也会损害到你自己的利益。


03:容易出现 “伪 TS”


所有以 .ts 结尾的文件,都是使用了 TypeScript 的。


由此,项目中就有可能会出现大量的 “伪 TS”。也就是:“以 .ts 结尾的文件,但是内容都是 js。”


这样的项目,除了可以让你们老板拿出去“吹牛逼”之外,对技术个人是毫无意义的。



当然,让老板可以拿出去 “吹牛逼” 对很多 “所谓的 Leader” 而言,就是TA们价值的体现



04:更容易出现屎山


其实 “伪 TS” 还好,因为它毕竟 不需要 我们花费更多的 时间和头发 来了解它的心路历程。


而比 “伪 TS” 更可怕的是:屎山一样的 TS 代码。


相信我,一旦 TS 屎山起来,那个味道要比 JS 重的多!


不知道大家有没有接手过一些 “所谓的 TS 项目”。我有幸在多个 训练营的同学 那里见到过很多次。


过度的类型声明、过度的类型封装,以及那些明明被定义但是 “好像” 从来都没有使用过的属性们。偏偏你还不敢动它们。就问你晕不晕。


将来当你想要去定义一个属性时,发现好像已经有了一个类似的,但是你又不确定的时候怎么办呢?最安心的办法就是 “再创建一个”。


所以,我曾经有幸在一个接口中见到了这样的代码(以下为伪代码):


interface User {
name: string;
name2: string;
username: string;
username2: any;
}


就问你刺激不刺激。


通常情况下,当我们遇到这样的代码时,根据 “尽量遵守前人代码习惯” 的规范下,很快这里就可能会出现 name3、name4 以及 name-next...


总结


任何的一门技术都是一把双刃剑,它在带来一定优势的同时,必然也会带来一定的不便性。


所以,我们真的没有必要去跟风追逐所谓的强类型。适合自己团队的,才是最好的!



这是第二次发了,虽然我啥也没改......



作者:程序员Sunday
来源:juejin.cn/post/7303413519906963465
收起阅读 »

为什么说https比http安全?

web
前言 在互联网时代,我们每天都在进行着与网络有关的活动,而网络安全问题也因此成为大家越来越关注的话题。http协议作为最常用的网络传输协议之一,其设计缺陷让黑客攻击变得更加容易。相比之下,https协议通过加密通信,能够更有效地保护用户隐私和数据安全。 本文将...
继续阅读 »

前言


在互联网时代,我们每天都在进行着与网络有关的活动,而网络安全问题也因此成为大家越来越关注的话题。http协议作为最常用的网络传输协议之一,其设计缺陷让黑客攻击变得更加容易。相比之下,https协议通过加密通信,能够更有效地保护用户隐私和数据安全。


本文将为您介绍什么是https,为什么它比http更安全,帮助您更好地了解网络安全问题。


什么是https


httpshttp的加强版(HTTP+SSL),因为http特性是明文传输,因此到每个传输的环节中数据都有可能被第三方篡改的可能,也就是我们所说是中间人攻击。为了数据的安全,提出了https这个方案


但它不是一个新的协议,原理上是在httptcp层之间建立一个中间层(也叫安全层),在不像之前http一样,直接进行数据通信,这个中间层会对数据进行加密处理,将加密后的数据给TCPTCP再将数据包进行解密处理才能传给上游的http



http是位于OSI网络模型中的应用层




SSL(Secure Sockets Layer 安全套接字协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议



20230112112331


在采用了SSL后,http就拥有了https的加密,证书,完整性保护功能。


换句话说,安全性是由SSL来保证的


SSL/TLS


SSL 即 安全套接层(Secure Sockets Layer),在OSI模型中处于第5层。在1999SSL更名为TLS(传输层安全),正式标准化。


TLS中使用了两种加密技术,分别是对称加密和非对称加密



提到 TLS ,就要说下 OpenSSL ,它是一个开源密码学程序库和工具包,支持了所有公开的加密算法和协议,许多应用软件都适用它作为底层库来实现 TLS 功能,例如 Apache、Nginx等。



加密技术


对称加密 Symmetric Cryptography


对称加密常见的加密算法有:DESAESIDEA


这个很好理解,对称加密指的是加密和解密的方式都是使用同一把钥匙(密保)


缺点:



  • 服务器端也会把密钥提供给对方进行解密,如果密钥传递的过程中被窃取那就没有加密的意义了


非对称加密 Asymmetric Cryptography


非对称加密常见的算法有:RSADSADH


非对称加密会有两把解密的密钥分别是ABA加密后的数据包只能通过密钥B来解密,反之,B加密的数据包只能通过A来解密


其中,A是公钥,B是私钥,这两把钥匙是不一样,公钥可以给任何人使用,私钥则必须保密。


这样子做可以防止密钥被攻击者窃取后用来获取数据


缺点:



  • 公钥是公开的,攻击者可以截获公钥后解密服务器发送过来的密钥

  • 公钥不包含服务器信息,使用这个方案无法确保服务器身份的合法性,存在中间人攻击风险

  • 使用非对称加密在数据加密解密过程需要消耗一定时间,降低了数据传输效率


hash算法


例如sha256sha1md5这些用来确定数据的完整性,是否有被篡改过,主要用来生成签名信息。


混合加密


HTTPS采用的是混合加密方案(即:对称加密和非对称加密的结合)


非对称加密的安全性比较高,但是解密速度会比较慢。


当数据进行第一次通信时,使用非对称加密算法(解决安全性问题)交互对称密钥,在这之后的每一次通信都采用对称加密来进行交互。这样子性能和安全也可以得到均衡。


混合加密总用了4把钥匙



  • 非对称加密A+私钥B

  • 对称加密私钥C和私钥D



内容传输时使用对称加密,证书验证阶段使用非对称加密



HTTPS工作过程



  1. 客户端发起一个网络请求。

  2. 服务器将自己的信息以数字证书的方式给了客户端(证书里面含有密钥公钥,地址,证书颁发机构等信息),其中的公钥是用来加密信息的。

  3. 当客户端接收到这个信息之后,会验证证书的完整性。(当证书有效继续下一步,否则显示警告信息)

  4. 客户端生成一个对称密钥并用第二步中的证书公钥进行加密发送给服务器端,

  5. 服务器用私钥解密获取对此密钥。(也证明了服务器是私钥的持有者)

  6. 接下来的通话使用该密钥进行通讯。


20230409202418


HTTPS运行原理


浏览器拿到证书后,会先读取issuer(发布机构),然后在操作系统中内置的受信任的发布机构中查找证书,是否匹配,如果没有找到证书,说明证书有问题,如果找到了,就会拿上级证书的公钥去解密本级证书,得到数字指纹hash,然后对本级证书进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。


证书从哪里来



  • 在安装系统的时候,受信任的证书发布机构的数字证书就已经被微软安装在操作系统中


20230409202800


什么时候证书不可信



  • 证书不是权威CA颁发(一些企业贪图便宜使用盗版证书,没有经过CA认证,也就无法通过使用浏览器内置CA公钥进行验证)

  • 证书过期

  • 证书部署错误,例如证书和域名信息不匹配


HTTPS优劣势


优势



  • 提高Web数据安全性

  • 加密用户数据

  • 提高搜索引擎排序

  • 浏览器不会出现非“不安全”警告

  • 提高用户对站点的信赖

  • 增加了中间人攻击成本


缺点



  • https协议在握手时耗时会大一些,影响整体加载速度

  • 客户端和服务器端会使用更大的性能来处理数据加解密

  • SSL证书需要支付一定的费用来获取

  • 也不是绝对的安全,当网站被攻击,服务器被劫持时,HTTPS起不到任何作用

  • SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗


关于数字证书认证


结合了两种加密方式可以实现数据的加密传输,但安全性还远远不够


如果攻击者采用了DNS劫持,将目标地址替换成攻击者指定的地址,之后服务器再伪造一份公钥和私钥,也能对数据进行处理,而客户端此时也不知道正在访问一个危险的服务器


HTTPS在混合加密的基础上,再追加了数字证书认证步骤,目的就是为了让服务器证明自己的身份


在传输过程中,服务器运营者需要向第三方认证机构(CACertificate Authority)获取授权,在认证通过之后CA会给服务器颁发数字证书


这个证书的作用就是用来证明服务器身份,还有就是把公钥传递给客户端


当客户端获取到数字证书后,会读取其明文内容,CA在对数字证书签名时会保存一个hash函数,这个函数是用来计算明文的内容得到数据A,然后用公钥解密明文内容得到数据B,再对这两份数据进行对比,如果一致就代表认证合法。


为什么要使用https?


它们之间有什么区别吗?


通过上面的介绍,我们可以了解到http在传输过程是明文的,数据容易被黑客截取或者篡改,这会导致用户信息泄露,而https通过ssl进行通讯加密处理,就算被截取了,也无法解读数据


另外,除了安全性方面,httpshttp还有以下区别:



  • 由于https需要对数据进行加解密,所以会增加服务器和客户端的消耗更多的性能资源来处理,同时也增加了响应速度

  • https需要申请证书和验证,http则不需要



作者:_island
来源:juejin.cn/post/7220619478979182648
收起阅读 »

从canvas到B站弹幕

web
canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现 Canvas 我们在body中放一个canvas标签,然后在Script中添加属性 <body> <canvas i...
继续阅读 »

canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现



Canvas


我们在body中放一个canvas标签,然后在Script中添加属性


<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.fillStyle = "green"
ctx.fillRect(10,10,55,50)
</script>
</body>

let ctx = canvas.getContext("2d")其实就是对canvas实例化一个对象。先得到一只2维的画笔,接下来的操作都是针对这只画笔


ctx.fillStyle = "green"给这只画笔沾上墨水,否则怎么画都画不出


fillRext用来画填充矩形。其中四个参数分别为左上角坐标,和右下角坐标,此时效果如下


1.png


	<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.strokeRect(10,10,55,55)
</script>

strokeRect是用来画矩形的,只有边框,不会进行填充,stroke这个单词可能大家只知道有中风的意思,其实还有笔画,轻拭的意思,此时效果如下


2.png


再来一个自定义描边


	<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.closePath()
ctx.stroke()
</script>

beginPath就是让画笔落在纸上


moveTo接收的一个起始位置坐标


两个lineTo是终点坐标


closePath将所有点连接起来,stroke开始画,一定要有stroke,否则没有效果


3.png


当然,你也可以对其填充


        ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.fill()

默认颜色黑色


4.png


当然,你也可以画贝塞尔曲线(bezierCurve):不规则的曲线,这个内容我这里不做介绍,方法可以网上自寻搜索


再来画个圆


    let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.arc(50,50,40,0,2 * Math.PI)
ctx.stroke()

arc方法用于画圆或圆弧,前两个参数为圆心坐标,第三个参数为圆的半径,第四个参数是起始角度(通常为0,三点钟方向),最后一个参数为终止角度。


5.png


绘制文本


    let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.font = '50px sans-serif'
ctx.fillText('床前明月光',10, 100)

fillText中后两个参数为起始坐标,strokeText绘制的是空心字


6.png


如果两个fillText的起始坐标一样,就可以重叠在一起,我现在再加一句同其实坐标的文字


7.png


B站弹幕其实就是用的画布,但是实现起来还是比较困难,为了方便文章排版,注释都放在了代码里面


b站弹幕


html部分


<!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>
#canvas {
position: absolute;
}
/* 宽高不在css设置,是因为是和视频一起变化,动态的,用js */
</style>
</head>
<body>
<div class="wrap">
<h1>Peaky Blinders</h1>
<div class="main">
<canvas id="canvas"></canvas>
<video src="./video.mp4" id="video" controls="true" width="720" height="480"></video>
</div>
<div class="content">
<input type="text" id="text">
<input type="button" value="发弹幕" id="btn">
<!-- 取色器 -->
<input type="color" id="color">
<!-- 控制弹幕的大小 -->
<input type="range" id="range" max="40" min="20">
</div>
</div>
<script src="./index.js"></script>
<!-- 执行js 后加载js js可能是本地文件,也可能是在线地址,如果放在开头,执行这个的时候会堵住html的加载,甚至会报错,读取canvas都不知道,如果保证js内容在页面加载完毕后执行也可以放在前面,就是window.onload这个方法 -->
</body>
</html>

js部分


// 这个js代码很难写,一般高级程序员才会这样写
// window.onload = function(){}
// 1.读取用户内容 2.把内容颜色大小放到画布上,绘制
// 历史弹幕,数组(里面放对象)还要接受新的弹幕,到了时间就绘制,要递归
let data = [
{ value: 'By order of the peaky bliears', color: 'red', fontSize: 22, time: 5 },
{ value: 'No Fucking Fighting', color: 'green', fontSize: 30, time: 10},
{ value: 'Fucking Shelby', color: 'black', fontSize: 22, time: 22}
]
// 整理弹幕数据,弹幕的y,历史弹幕问题 形参跟外面的一样没毛病,辨识度更高,代码太多了abc是啥都不知道,都可以 ,形参可以默认值,万一没有传值呢
function CanvasBarrage(canvas, video, opts = {}){
if(!canvas || !video) return
this.video = video
this.canvas = canvas
// 伪代码 canvas 宽高 和 video宽高保持一致
// canvas.width = style.width style读取宽高,js设置宽高
this.canvas.width = video.width
this.canvas.height = video.height
// 获取画布
this.ctx = canvas.getContext("2d")
// 初始化代码
// 没有认为修改弹幕的设置,默认值
let defOpts = {
color: '#e91e63',
fontSize: 20,
speed: 1.5,
// 透明度
opacity: 0.5,
data: []
//value和time不需要默认值
}
Object.assign(this, defOpts, opts)
// 视频播放,弹幕才会进行绘制
this.isPaused = true
// 默认暂停
// 获取到所有的弹幕 map(返回一个新的数组)里面是箭头函数(把item交给一个新的箭头函数) map循环了,每个弹幕都被修饰了一下
this.barrages = this.data.map((item) => new Barrage(item, this))
this.render()
}
Barrage.prototype.init = function(){
// 左边是自己新建的右边是传进来的,如果第一个是没有的,就是给出的默认的颜色
this.color = this.obj.color || this.context.color
this.speed = this.obj.speed || this.context.speed
this.opacity = this.obj.opacity || this.context.opacity
this.fontSize = this.obj.fontSize || this.context.fontSize

let p = document.createElement('p')
// 让字体大小等于设置的大小
p.style.fontSize = this.fontSize + 'px'
p.innerHTML = this.value
document.body.appendChild(p)
// 右边是获取这个容器的宽度
this.width = p.offsetWidth
// 放完之后要删掉
document.body.removeChild(p)
// 设置弹幕的位置
this.x = this.context.canvas.width
// y的高度是随机值
this.y = this.context.canvas.height * Math.random()
// 弹幕可能上下方超出边界
if(this.y < this.fontSize){
this.y = this.fontSize
}else if(this.y > this.context.canvas.height - this.fontSize){
this.y = this.context.canvas.height - this.fontSize
}
}

// Barrage 修饰一条弹幕 为箭头函数那里服务 (实例对象,this对象)
function Barrage(obj, context){
this.value = obj.value
this.time = obj.time
// 挂在构造函数中后面更方便
this.obj = obj
this.context = context
}

CanvasBarrage.prototype.clear = function(){
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
// 将这条弹幕会绘制在画布上
Barrage.prototype.renderEach = function(){
// canvas绘制过程
// 设置画布的文字字体,字号
// 设置画布的文字颜色
// 绘制颜色
this.context.ctx.font = `${this.fontSize}px Arial`
this.context.ctx.fillStyle = this.color
this.context.ctx.fillText(this.value, this.x, this.y)
}
// 将弹幕绘制到画布上
CanvasBarrage.prototype.renderBarrages = function(){
// 伪代码 拿到视频播放的当前时间,根据时间绘制
let time = this.video.currentTime
// 遍历所有的弹幕
this.barrages.forEach(function(barrage){
// 出屏之外之后就不用再操作了
if(time >= barrage.time && !barrage.flag){
// 这属性没有就是false 这个操作就是为了防止放过了的弹幕不需要初始化
if(!barrage.isInit){
barrage.init()
barrage.isInit = true
}
// 控制弹幕左移
barrage.x -= barrage.speed
// rednerEach相当于ctx.fillstyle
barrage.renderEach()
// 弹幕是有长度的
if(barrage.x < -barrage.width){
barrage.flag = true
}
}
})
}
// 这里就是render ,把弹幕弄到画布中
CanvasBarrage.prototype.render = function(){
// 清除画布,习惯问题
this.clear()
// 要先绘制才能操作画笔,并且要向左移动
this.renderBarrages()
// 播放状态才能移动
if(!this.isPaused){
// setInterval这里不用,下面定时器的更高级,16.7ms(内定时间)之后就执行一次,递归之后就是一直循环下去
requestAnimationFrame(this.render.bind(this))
// bind(this)以后再讲
}
}
// 添加新的弹幕
CanvasBarrage.prototype.add = function(obj){
// barrages是终极数组,data修饰之后的
// this.barrages16.7ms之后也会重修渲染一次
this.barrages.push(new Barrage(obj,this))
}
// 传的参数是canvas和video dom结构 opts是一个对象含value color time fontSize 这个会替代掉,合并对象,相同的覆盖,不同的加进去
let canvas = document.getElementById('canvas')
// video知道此时视频多少秒
let video = document.getElementById('video')
// $没有意义,区分罢了
let $text = document.getElementById('text')
let $btn = document.getElementById('btn')
let $color = document.getElementById('color')
let $range = document.getElementById('range')
// 整理弹幕的实例对象
// 对象里key和value可以直接由{data: data}变成{data}
let canvasBarrage = new CanvasBarrage(canvas, video, {data})
// play是播放,处理所有弹幕实例对象
video.addEventListener('play',function(){
canvasBarrage.isPaused = false
// 处理每一条弹幕,canvasBarrage相当于一个管家
canvasBarrage.render()
})

function send(){
// 读取文本内容
let value = $text.value
// video 自带一个属性读取时间
let time = video.currentTime
let color = $color.value
let fontSize = $range.value
// 把上面的内容整理成一个对象,交给函数去操作
let obj = {
value: value,
color: color,
fontSize: fontSize,
time: time
}
// 多么希望add可以把obj放进去,接收新的弹幕,处理弹幕再走一遍send
canvasBarrage.add(obj)
}
$btn.addEventListener('click', send)
$text.addEventListener('keyup',function(e){

console.log(e);
if(e.keyCode === 13){
send()
}

})


  1. 数据结构:

    • data 数组包含表示单个弹幕项的对象。每个对象具有诸如 value(文本内容)、colorfontSizetime(显示时间)等属性。



  2. CanvasBarrage 类:

    • CanvasBarrage 是一个构造函数,用于初始化弹幕系统。

    • 它接受一个画布元素、一个视频元素和可选的配置选项。

    • 默认选项(defOpts)包括诸如 colorfontSizespeedopacitydata 等属性。

    • 根据提供的数据创建了一个 Barrage 对象的数组。

    • render 方法负责在画布上渲染和动画弹幕。

    • clear 方法在渲染之前清除画布。



  3. Barrage 类:

    • Barrage 是用于单个弹幕项的构造函数。

    • 它接受一个对象(obj)和一个上下文(context),即 CanvasBarrage 的实例。

    • init 方法使用属性如 colorspeedopacityfontSizewidthxy 初始化弹幕项。

    • renderEach 方法在画布上渲染单个弹幕项。



  4. 渲染和动画:

    • renderBarrages 方法负责根据当前视频时间渲染所有弹幕项。

    • render 方法使用 requestAnimationFrame 不断调用自身以进行连续动画。

    • add 方法允许向系统添加新的弹幕项。



  5. 事件监听器:

    • 对视频的 play 事件监听器触发在视频播放时渲染弹幕。

    • 对按钮($btn)的 click 事件监听器触发 send 函数以添加新的弹幕。

    • 对文本输入框($text)的 keyup 事件监听器在按下Enter键时触发 send 函数。



  6. 用户输入处理:

    • send 函数读取输入值(文本、时间、颜色、fontSize)并创建一个新的弹幕对象,然后将其添加到弹幕系统中。



  7. 初始化:

    • 使用画布、视频和提供的数据创建了 CanvasBarrage 的实例。



  8. 使用:

    • 当视频播放时,弹幕系统开始渲染,并且用户可以使用提供的输入元素添加新的弹幕




效果如下:


效果.gif




如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]


作者:Dolphin_海豚
来源:juejin.cn/post/7302310196311719988
收起阅读 »

重要提醒!第三方 Cookie 即将被禁用

web
Chrome 浏览器计划从 2024 年第一季度开始禁用 1% 用户的第三方 Cookie,以方便测试,然后在 2024 年第三季度逐步覆盖到 100% 用户。Chrome 推出了一系列API,为诸如身份验证、广告和欺诈检测等用例提供了以隐私为重点的替代方案。...
继续阅读 »

chrome-4.webp


Chrome 浏览器计划从 2024 年第一季度开始禁用 1% 用户的第三方 Cookie,以方便测试,然后在 2024 年第三季度逐步覆盖到 100% 用户。Chrome 推出了一系列API,为诸如身份验证、广告和欺诈检测等用例提供了以隐私为重点的替代方案。


本文将带您了解禁用时间表,建议您立即采取行动,以确保您的网站做好准备。


1.禁用时间表


privacysandbox.com 的时间轴上,可以看到 2023 年第四季度和 2024 年第一季度将有两个里程碑,作为 Chrome 辅助测试模式的一部分。该测试主要针对测试隐私沙盒相关性和测量 API 的组织,但作为测试的一部分,将对 1% 的 Chrome 稳定版用户禁用第三方 cookies。


配图:时间表


这意味着从 2024 年开始,即使您没有积极参与 Chrome 浏览器辅助测试,您也可以预期在您的网站上看到越来越多的 Chrome 浏览器用户禁用了第三方 Cookie。这一测试周期将持续到 2024 年第三季度,届时将禁用所有 Chrome 浏览器用户的第三方 Cookie。


2.需要做哪些准备?


为确保您的网站在没有第三方 cookie 的情况下可以正常运行,需要做好以下准备:



  • 梳理第三方 cookie 的使用情况。

  • 进行破坏测试。

  • 对于存储在每个网站上的跨站点 cookie(例如嵌入式 cookie),请考虑使用 CHIPS 进行分区。

  • 对于在一小组相关联网站之间的跨站点 cookie,请考虑使用相关网站集。

  • 对于其他第三方 cookie 的使用情况,请迁移到相关的 Web API。


2.1.梳理第三方 cookie 的使用情况


Chrome开发者工具的网络面板显示请求中设置和发送的Cookie。在应用程序面板中,在存储下可以看到Cookie标题。您可以浏览每个访问的站点存储的Cookie,作为页面加载的一部分。您可以按照SameSite列进行排序,以将所有 Cookie分组。


第三方 cookie 可以通过其 SameSite= 值来识别。您应该搜索代码以查找将 SameSite 属性设置为此值的实例。如果您在 2020 年左右之前对添加 SameSite= 到您的 Cookie 进行了更改,那么这些更改可能是一个很好的起点。


Chrome DevTools 网络面板显示根据请求设置和发送的 cookie。在 Application 面板中,您可以在 Storage 下看到 Cookie。您可以浏览为页面加载过程中访问的每个站点存储的 cookie。您可以按列 SameSite 排序以对所有 值的 cookie 进行分组。


从 Chrome 118 开始,DevTools 问题选项卡显示了重大更改问题:"跨站点上下文中发送的 Cookie 将在未来的 Chrome 版本中被阻止", 该问题列出了当前页面可能受影响的 cookie。


chrome-issue-cookie.png


如果您发现第三方设置的 cookie,您应该与这些提供商核实,看看他们是否有逐步淘汰第三方 cookie 的计划。例如,您可能需要升级正在使用的库的版本、更改服务中的配置选项,或者如果第三方正在自行处理必要的更改,则不采取任何操作。


2.2.进行破坏测试


您可以使用 --test-third-party-cookie-phaseout 命令行标志或从Chrome 118开始,使用 chrome://flags/#test-third-party-cookie-phaseout 启用。这将设置 Chrome 阻止第三方 cookie,并确保新功能和缓解措施处于活动状态,以最佳模拟淘汰后的状态。


您也可以尝试通过 chrome://settings/cookies 阻止第三方 cookie 进行浏览,但请注意,该标志也确保了新功能和更新功能的启用。阻止第三方cookie是检测问题的好方法,但并不一定能验证您已经修复了问题。


如果您为您的网站保持一个活跃的测试套件,那么您应该进行两次并行运行:一次是使用常规设置运行的Chrome,一次是使用启用--test-third-party-cookie-phaseout 标志启动的相同版本的 Chrome。第二次运行中的任何测试失败而第一次运行中没有的都是需要调查的第三方cookie依赖的好候选项。请确保报告您发现的问题。


一旦您确定了存在问题的cookie并了解了它们的用例,您可以通过以下选项来选择必要的解决方案。


2.3.将 Partitioned cookie 与 CHIPS 结合使用


如果您的第三方 Cookie 在与顶级站点进行 1:1 嵌入的上下文中使用,则可以考虑使用带有独立分区状态的 Cookie(CHIPS)的分区属性,以允许使用每个站点使用的单独的 Cookie 进行跨站点访问。


partitioned.png


要实现 CHIPS,您需要将 Partitioned 属性添加到您的 Set-Cookie 头中:


通过设置 Partitioned,该网站选择将 cookie 存储在由顶级网站分隔的单独的 cookie 存储区。在上面的示例中,cookie 来自store-finder.site,该网站托管了一个店铺地图,用户可以保存他们喜欢的店铺。通过使用 CHIPS,当 brand-a.site 嵌入store-finder.site 时,fav_store cookie 的值为123。然后,当 brand-b.site 也嵌入 store-finder.site 时,他们将设置并发送自己分隔的fav_store cookie 实例,例如值为456。


这意味着嵌入式服务仍然可以保存状态,但没有允许跨站点跟踪的共享跨站点存储。


潜在的使用案例:第三方聊天嵌入、第三方地图嵌入、第三方支付嵌入、子资源内容分发网络(CDN)负载均衡、无头内容管理系统提供商、用于提供不受信任的用户内容的沙盒域名、使用 Cookie 进行访问控制的第三方 CDN、需要在请求中添加 Cookie 的第三方 API 调用、按发布商进行状态范围的嵌入广告。


2.4.使用相关网站集


当仅在一小部分相关网站上使用第三 Cookie 时,您可以考虑使用相关网站集合(RWS),以便在这些定义的网站上上下文中允许跨站点访问该Cookie。


要实施 RWS,您需要定义并提交网站组。为确保这些网站之间存在有意义的关联,有效集合的策略要求按以下方式对这些网站进行分组:具有可见关联的相关网站(如公司产品的变体)、服务域(如 API、CDN)或国家代码域(如 .uk、.jp)。


RWS.png


网站可以使用 Storage Access API 来请求跨站点的 Cookie 访问权限,使用 requestStorageAccess() 方法或使用requestStorageAccessFor() 方法委派访问权限。当网站在同一组中时,浏览器会自动授予访问权限,并且跨站点的 Cookie将可用。


这意味着相关网站的组仍然可以在有限的上下文中使用跨站点的 Cookie,但不会冒着以允许跨站点追踪的方式在不相关的站点之间共享第三方cookie的风险。


潜在的用例包括:特定于应用程序的域,特定于品牌的域,特定于国家的域,用于提供不受信任的用户内容的沙盒域,用于API的服务域,CDN。


2.5.迁移到相关的 Web API


CHIPS 和 RWS 能够在保护用户隐私的同时实现特定类型的跨站点 Cookie访问,但是其他使用第三方 Cookie 的实例必须迁移到以隐私为重点的替代方案。


Privacy Sandbox 提供了一系列针对特定用例的专用API,无需使用第三方cookie:



  • 联合身份管理(FedCM)允许用户登录到站点和服务。

  • 私有状态令牌通过在站点之间交换有限的、非识别信息,实现反欺诈和反垃圾邮件功能。

  • 主题功能实现基于兴趣的广告和内容个性化。

  • 受保护的受众功能实现再营销和自定义受众。

  • 属性报告功能实现广告展示和转化的测量。


此外,Chrome 还支持 Storage Access API (SAA),用于用户交互框架中的使用。SAA 已经在 Edge,Firefox 和 Safari 上得到支持。我们认为它在保持用户隐私的同时,仍然能够实现关键的跨站点功能,并具有跨浏览器的兼容性。


请注意,Storage Access API (SAA) 将向用户显示浏览器权限提示。为了提供最佳的用户体验,只有在调用 requestStorageAccess() 的站点与嵌入页面进行交互并之前在顶级上下文中访问过第三方站点时,才会提示用户。成功授权将允许该站点在 30 天内跨站点访问 Cookie。可能的用例包括认证的跨站点嵌入,如社交网络评论小部件、支付提供商、订阅视频服务。


如果您仍然有未被这些选项覆盖的第三方 Cookie 用例,您应该向 Chrome 团队报告该问题,并考虑是否有不依赖于启用跨站点跟踪的功能的替代实现。


作者:FED实验室
来源:juejin.cn/post/7302330573381156876
收起阅读 »

阁下,您的表单校验规则还维护的动吗?

web
表单校验是前端项目广泛存在的一个功能,因为Ant Design的引入,所谓的表单校验功能其实已经被抽象成了一个又一个的表单校验规则。以前并未注意到表单校验规则的可维护性,哪个组件用到,就在组件内完成即可。直到PM问了我2个问题: “我们的账号名字符数限制区间是...
继续阅读 »

表单校验是前端项目广泛存在的一个功能,因为Ant Design的引入,所谓的表单校验功能其实已经被抽象成了一个又一个的表单校验规则。以前并未注意到表单校验规则的可维护性,哪个组件用到,就在组件内完成即可。直到PM问了我2个问题:


“我们的账号名字符数限制区间是多少?”


“项目中所有涉及命名的字符数都要限制在3-26个,啥时候能改好?”


“好的”,我习惯性的打开了编辑器全局搜索。。。


后来的日子以上对话又重复了好几轮,每次内容略有不同啊!终于无法忍受的我开始动手对这部分内容专门做了重构,经过几个版本的迭代,似乎已经找到了一个好的方案将前端项目中的众多表单校验规则维护得体,便记下此文与各位前端大佬分享探讨。


影响维护性的几个问题



  1. 校验规则靠近并耦合业务组件,散落在项目各处

  2. 校验规则的复用

  3. 校验规则难以理解和可读

  4. 需要传参的校验规则

  5. 异步校验的规则


可维护的方案


image.png



该方案将所有的表单校验规则整理在src/formRules目录下统一管理,带来的收益是明显的:统一管理本身解决了问题1;./rules.ts & ./rulesHooks.tsx 作为所有业务组件消费校验规则的统一输出口,针对了问题1、2;./baseRuleCreator.tsx 针对了问题4;./baseSemiRule.tsx & ./utils.ts 针对了问题5;问题3么写注释就好了!



我们来看具体的内容:


./baseRules.tsx


// 必填的
export const requiredRule: RuleObject = {
required: true,
}

// 输入必须以字母开头
export const startLetterRule: RuleObject = {
pattern: /^[a-zA-Z]/,
message: 'Must start with a letter',
}

可以看到我们将表单校验规则拆成了原子级别,相同的规则我们只会写1遍,意外的收益在于每个校验报错信息也将是精准的。


./rules.ts


// Email
export const emailRules: RuleObject[] = [requiredRule, emailPatternRule]

// 密码
export const passwordRules: RuleObject[] = [requiredRule, length8_32Rule, passwordBanRule]

我们在该文件下自由组合复用原子化的校验规则,并导出给业务组件消费,达到了复用的目的。如果业务校验规则有修改,我们也可以仅在该文件统一做修改,完成了业务逻辑与校验规则的解耦。


./baseRuleCreator.tsx


// 校验是否与目标值匹配
export function createMatchTargetValueRule(targetValue: any): RuleObject {
return {
message: 'Does not match',
validator(rule: Rule, value: string) {
return value === targetValue ? Promise.resolve() : Promise.reject()
},
}
}

很明显该文件放的规则都是需要有入参的,不再赘述。


./baseSemiRule.tsx


// 异步校验邮箱是否重复
export const duplicatingAccountEmailSemiRule: TSemiRule = {
message: 'Email already exists',
callbackValidator: (value, resolve, reject) => {
return fetch(value).then(res => (res.data.exists ? reject() : resolve(undefined)))
},
}

显然该文件放了需要网络异步校验的规则,但为什么被命名为Semi?难道阁下忘了防抖?虽然我们还需要一个防抖函数来统一加工这些校验规则才可实用。但非常棒的地方在于,这些规则在逻辑上已经自洽了,报错信息和校验逻辑都已经完整了。所以我们并不关心防抖函数的具体实现和校验规则在业务组件中的具体应用,如有修改则我们在这里维护即可,做到了关注点的分离。


./utils.ts


export function createDebounceRule(semiRule: TSemiRule): RuleObject {
const { message, callbackValidator, delayTime = 500 } = semiRule
let timeId: NodeJS.Timeout = null

return {
message,
validator(rule, value) {
return new Promise((resolve, reject) => {
clearTimeout(timeId)
timeId = setTimeout(() => {
callbackValidator(value, resolve, reject)
}, delayTime)
})
},
}
}

防抖函数的具体实现,不再赘述。


./rulesHooks.tsx


export function useVpcConnectionNameRules(target: string) {
const rules = useMemo(() => [
requiredRule,
startLetterRule,
createMatchTargetValueRule(target),
createDebounceRule(duplicatingAccountEmailSemiRule),
], [target])

return rules
}

需要入参的校验规则和异步校验规则利用hooks做抽象,实现复用。


千里之堤溃于蚁穴,勿以事小而不为!表单校验规则虽然是代码维护性一个鲜有前端boy关注的微小领域,但如果我们能关注到项目中一砖一瓦的维护性,则可以相信整个项目大厦也将是健壮无比的。



以上方案仅个人在项目中的实践,维护性的提升无可止境,各位前端大佬如有更好的方案,希望留言不吝赐教!



作者:阿佛加德奔
来源:juejin.cn/post/7259638617546637349
收起阅读 »

听别人说Vue的拖拽库都断代了,我第一个不服

web
vue-draggable-plus 前言 前段时间偶然翻掘金的过程中发现有人宣传 vue 的拖拽库断代了,那么我也来蹭一下热度。 Sortablejs Sortablejs 是一个功能强大的 JavaScript 拖拽库,并且提供了 vue 相关的组件 vu...
继续阅读 »

vue-draggable-plus


前言


前段时间偶然翻掘金的过程中发现有人宣传 vue 的拖拽库断代了,那么我也来蹭一下热度。


Sortablejs


Sortablejs 是一个功能强大的 JavaScript 拖拽库,并且提供了 vue 相关的组件 vue-draggable,并且在 vue3 的前期,提供了 vue-draggable-next,但是可能由于作者的生活过于繁忙的原因,这个库已经两年没有更新了,在当前 vue3 的版本中并不适用,于是乎本人突发奇想,写了一个 vue-draggable-plus(其实很久之前就写好了,没有可以宣传),它用于兼容 vue2.7vue3 以上的版本,下面我们来介绍一下它。


vue-draggable-plus


vue-draggable-plus 它用于延续 vue-draggable 核心理念,提供 vue 组件用于双向绑定数据列表,实现拖拽排序、克隆等功能,同时它还支持函数式、指令式的使用方式,让你使用起来更加方便,废话不多生活,我们先上图


2023-11-09 18.47.24.gif


更多演示请参考:demo


安装


npm install vue-draggable-plus

使用


vue-draggable-plus 支持三种使用方式:组件使用、函数式使用、指令式使用


下面我们来一一介绍:



  1. 组件式使用:


它和传统的vue组件的使用方式一样,支持双向绑定数据:


<template>
<VueDraggable
v-model="list"
:animation="150"
ghostClass="ghost"
@start="onStart"
@update="onUpdate"
>

<div
v-for="item in list"
:key="item.id"
>

{{ item.name }}
</div>
</VueDraggable>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { type UseDraggableReturn, VueDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])

</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>



  1. 函数式使用:


<template>
<div
ref="el"
>

<div
v-for="item in list"
:key="item.id"
>

{{ item.name }}
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])
const el = ref()

const { start } = useDraggable(el, list, {
animation: 150,
ghostClass: 'ghost',
onStart() {
console.log('start')
},
onUpdate() {
console.log('update')
}
})
</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>

它就像你使用 vueuse 一样接受一个 element 的引用,和一个响应式列表数据



  1. 指令式使用


由于指令的特殊性,指令只能绑定您在 setup 中绑定的数据,它并不能支持异步绑定数据,如果您的数据来自于异步获取,那么请您使用组件或者函数式实现



<template>
<ul
v-draggable="[
list,
{
animation: 150,
ghostClass: 'ghost',
onUpdate,
onStart
}
]"
>
<li
v-for="item in list"
:key="item.id"
>
{{ item.name }}
</li>
</ul>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { vDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])

function onStart() {
console.log('start')
}

function onUpdate() {
console.log('update')
}
</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>


指定目标容器


在 Sortablejs 官方以往的 Vue 组件中,都是通过使用组件作为列表的直接子元素来实现拖拽列表,当我们使用一些组件库时,如果组件库中没有提供列表根元素的插槽,我们很难实现拖拽列表,vue-draggable-plus 完美解决了这个问题,它可以让你在任何元素上使用拖拽列表,我们可以使用指定元素的选择器,来获取到列表根元素,然后将列表根元素作为 Sortablejs 的 container,我们来看一下用法:



  • Table.vue


<template>
<table>
<thead>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
</thead>
<tbody class="el-table"> <-- 我们会将 .el-table 的选择器传递给 vue-draggable-plus -->
<tr v-for="item in list" :key="item.name" class="cursor-move">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
interface Props {
list: Record<'name' | 'id', string>[]
}
defineProps<Props>()
</script>


  • App.vue


<template>
<section>
<div>
<-- 传递 .el-table 作为根元素,将 .el-table 的子元素作为拖拽项 -->
<VueDraggable v-model="list" animation="150" target=".el-table">
<Table :list="list"></Table>
</VueDraggable>
</div>
<div class="flex justify-between">
<preview-list :list="list" />
</div>
</section>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VueDraggable } from 'vue-draggable-plus'
import Table from './Table.vue'

const list = ref([
{
name: 'Joao',
id: '1'
},
{
name: 'Jean',
id: '2'
},
{
name: 'Johanna',
id: '3'
},
{
name: 'Juan',
id: '4'
}
])
</script>


来看效果:


2023-11-09 19.11.29.gif


结尾


如果您有使用需求,请参考文档:vue-draggable-plus,当然如果您不需要高度定制化,使用 vueuse 中的 useSortable 也是一样的。


如果它对您有用,请帮忙点个star:GitHub


友情链接:svelte-draggable-plus


作者:丶远方
来源:juejin.cn/post/7299353745506615347
收起阅读 »

一个 React 简易网页端音乐播放器

web
前言 这是一个轻量级的 react 音乐播放器,前端使用 UmiJS,后端采用网易云音乐 NODEJS API 制作。项目的 TS 声明写的比较乱,后续有空的话会发布重写 TS 的版本或者直接重构该播放器。 后续计划将右侧播放器抽离为一个单独的组件,可供页面直...
继续阅读 »

图片


前言


这是一个轻量级的 react 音乐播放器,前端使用 UmiJS,后端采用网易云音乐 NODEJS API 制作。项目的 TS 声明写的比较乱,后续有空的话会发布重写 TS 的版本或者直接重构该播放器。


后续计划将右侧播放器抽离为一个单独的组件,可供页面直接使用。


功能


现有功能



  1. 登陆 / 退出个人网易云账号

  2. 获取私人雷达歌单

  3. 播放歌曲

  4. 播放自己已有的网易云音乐歌单 / 订阅的歌单

  5. 单曲播放 / 全部循环 / 随机播放

  6. 搜索歌曲

  7. 背景图切换


计划中功能(先把饼画着)



  1. 音质选择

  2. 歌曲切换 -> 背景图变化

  3. 保存播放列表并同步到网易云

  4. 双语歌词对照

  5. 歌词自定义字体大小

  6. 歌曲查看评论 / 点赞 / 留言

  7. 详情页相似歌曲推荐

  8. 无版权歌曲或加载出错歌曲增加标记

  9. 将右侧播放器抽离成独立组件


比较有特色的地方


图片


1、右侧全局播放栏


播放栏可以清空播放列表,查看当前歌曲歌词,对播放列表的歌曲可以使用拖拽进行顺序调整。


2、主页左上角频谱图的实现


开始构建使用


1.安装项目


npm install

2.设置后台接口地址


第一个:网易云 NODEJS 服务器,到 src/utils/request.ts 将其设置为你的网易云后台 API 地址。


switch (process.env.NODE_ENV) {
case 'production':
// 你的生产环境地址 / Your production mode api
axios.defaults.baseURL = '';
break;

default:
// development
axios.defaults.baseURL = 'http://localhost:3000';
break;
}

第二个:天气地址,到 src/constant/api/weather.ts 进行设置。


然后到 src/redux/modules/Weather/action.ts 下根据你设置的天气接口改变传入数据结构,文件内均有注释。


      const info = {
// 空气质量
airQuailty: dewPt,
// 当前气温
currentTemp: temp,
// 体感气温
feelTemp: feels,
// 湿度
humidity: rh,
// 气压
baro,
// 天气描述,如晴或多云
weatherDescription: cap,
}

如果想高度自定义样式或内容的话可以到 src/pages/IndexPage/topRightWeather 进行调整。


打包发布


npm run build

部分功能预览


图片


图片


图片


作者:程序员Winn
来源:juejin.cn/post/7291960625462198324
收起阅读 »

炫酷的高亮卡片效果

web
前言 无意中在Nuxt官网发现一组高亮卡片元素的效果,发现还挺好看的,就自己试着写了一下,下面是Nuxt官网效果图,边框会随着鼠标移动,并且周围的卡片也会“染上”。 我实现的效果如下 实现过程 写好六个卡片 下面看代码,先用HTML写六个div元素,并...
继续阅读 »

前言



无意中在Nuxt官网发现一组高亮卡片元素的效果,发现还挺好看的,就自己试着写了一下,下面是Nuxt官网效果图,边框会随着鼠标移动,并且周围的卡片也会“染上”。



Video_2023-11-14_180220.gif


我实现的效果如下


Video_2023-11-14_175549-Trim.gif


实现过程


写好六个卡片


下面看代码,先用HTML写六个div元素,并设置好基础样式。


    <div class="box">
<div class="col">
<div class="element">
<div class="mask">
div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
div>

body {
margin: 0;
padding: 0;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
background-color: #0D1428;
}

.box {
width: 1200px;
display: flex;
flex-wrap: wrap;
}

.col {
width: calc((100% - 4 * 20px) / 4);
height: 180px;
padding: 10px;
}
.element {
background: #172033;
height: 100%;
position: relative;
border-radius: 10px;
}


image.png


JS获取卡片坐标距离鼠标坐标的距离


使用JS获取每一个卡片坐标距离鼠标坐标的距离,并将这个值设置到元素的style中作为一个变量。


var elements = document.getElementsByClassName("element");
// 添加鼠标移动事件监听器
document.addEventListener("mousemove", function (event) {
// 获取鼠标位置
var mouseX = event.pageX;
var mouseY = event.pageY;

// 遍历元素并输出距离鼠标的坐标
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var rect = element.getBoundingClientRect();
var elementX = rect.left + window.pageXOffset;
var elementY = rect.top + window.pageYOffset;

var distanceX = mouseX - elementX;
var distanceY = mouseY - elementY;

// 将距离值设置到每一个卡片元素上面
element.style.setProperty('--x', distanceX + 'px');
element.style.setProperty('--y', distanceY + 'px');
}
});

我们检查控制台可以看到,值已经设置上去了,并且随着鼠标的移动,这个值是在不断变化的


image.png


给元素设置径向渐变


随后我们在element这个伪元素上设置一个径向渐变的CSS效果, 径向渐变的圆心坐标为当前元素距离当前鼠标坐标的距离。再使用mask遮罩,只留出3px的距离作为渐变效果展示。


.element::before {
content: '';
position: absolute;
width: calc(100% + 3px);
height: calc(100% + 3px);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 10px;
background: radial-gradient(250px circle at var(--x) var(--y),#00DC82 0,transparent 100%);;
}
.element .mask {
position: absolute;
inset: 3px;
background: #172033;
border-radius: 10px;

}

至此,效果就完全实现啦


image.png



作者:林黛玉倒拔垂杨柳
来源:juejin.cn/post/7301266090750115877
收起阅读 »

深入了解 JavaScript 中 Object 的重要属性

web
JavaScript 中的 Object 是一种非常灵活且强大的数据类型,它允许我们创建和操作键值对。在本文中,我们将深入探讨 Object 的一些重要属性,以便更好地理解和利用这个关键的数据结构。 1. Object.keys() Object.keys()...
继续阅读 »

JavaScript 中的 Object 是一种非常灵活且强大的数据类型,它允许我们创建和操作键值对。在本文中,我们将深入探讨 Object 的一些重要属性,以便更好地理解和利用这个关键的数据结构。


1. Object.keys()


Object.keys() 方法返回一个包含给定对象的所有可枚举属性的字符串数组。这对于获取对象的所有键是非常有用的。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const keys = Object.keys(myObject);
console.log(keys); // ['name', 'age', 'job']

2. Object.values()


Object.values() 方法是 JavaScript 中用于获取对象所有可枚举属性值的一个非常便捷的工具。通过调用该方法,我们可以轻松地将对象的值提取为一个数组,而无需手动遍历对象的属性。这样一来,我们能够更加高效地对对象的值进行处理和操作。这一特性对于处理对象数据非常有用,例如在需要对对象的值进行计算、过滤或展示时,可以直接利用 Object.values() 方法获取到对象的所有值,然后进行进一步的处理。这样不仅能简化代码逻辑,还能提升代码的可读性和可维护性。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const values = Object.values(myObject);
console.log(values); // ['John', 30, 'Developer']

3. Object.entries()


Object.entries() 方法返回一个给定对象自己的所有可枚举属性的键值对数组。这对于遍历对象的键值对非常有用。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const entries = Object.entries(myObject);
console.log(entries);
// [['name', 'John'], ['age', 30], ['job', 'Developer']]

4. Object.assign()


Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。这对于对象的浅拷贝非常有用。


示例:


const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const result = Object.assign({}, target, source);
console.log(result); // { a: 1, b: 4, c: 5 }

5. Object.freeze()


Object.freeze() 方法冻结一个对象,防止添加新属性,删除现有属性或修改属性的值。这对于创建不可变对象非常有用。


示例:


const myObject = {
name: 'John',
age: 30
};

Object.freeze(myObject);

// 下面的操作将无效
myObject.age = 31;
delete myObject.name;
myObject.job = 'Developer';

console.log(myObject); // { name: 'John', age: 30 }

6. Object.defineProperty()


Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性。这对于定义属性的特性非常有用。


示例:


const myObject = {};

Object.defineProperty(myObject, 'name', {
value: 'John',
writable: false, // 不能被修改
enumerable: true, // 可以被枚举
configurable: true // 可以被删除
});

console.log(myObject.name); // 'John'
myObject.name = 'Jane'; // 这里会被忽略,因为属性是不可写的

结论


Object 是 JavaScript 中一个关键的数据类型,通过深入了解其中的一些重要属性,我们可以更灵活地操作和管理对象。以上介绍的方法只是 Object 提供的众多功能之一,掌握这些属性将有助于更好地利用 JavaScript 中的对象。希望本文能够帮助你更深入地理解和使用 Object


作者:_XU
来源:juejin.cn/post/7301976895913951269
收起阅读 »

Vue 中使用 Lottie 动画库详解

web
Lottie 是一个由 Airbnb 开源的动画库,它允许你在 Web、iOS、Android 等平台上使用体积小、高性能的体验丰富的矢量动画。本文将详细介绍在 Vue 项目中如何集成和使用 Lottie。 步骤一:安装 Lottie 首先,需要安装 Lott...
继续阅读 »

Lottie 是一个由 Airbnb 开源的动画库,它允许你在 Web、iOS、Android 等平台上使用体积小、高性能的体验丰富的矢量动画。本文将详细介绍在 Vue 项目中如何集成和使用 Lottie。


步骤一:安装 Lottie


首先,需要安装 Lottie 包。在 Vue 项目中,可以使用 npm 或 yarn 进行安装:


npm install lottie-web
# 或
yarn add lottie-web

步骤二:引入 Lottie


在需要使用 Lottie 的组件中引入 Lottie 包:


// HelloWorld.vue

<template>
<div>
<lottie
:options="lottieOptions"
:width="400"
:height="400"
/>

</div>

</template>

<script>
import Lottie from 'lottie-web';
import animationData from './path/to/your/animation.json';

export default {
data() {
return {
lottieOptions: {
loop: true,
autoplay: true,
animationData: animationData,
},
};
},
mounted() {
this.$nextTick(() => {
// 初始化 Lottie 动画
const lottieInstance = Lottie.loadAnimation(this.lottieOptions);
});
},
};
</script>


<style>
/* 可以添加样式以调整动画的位置和大小 */
</style>


在上述代码中,animationData 是你的动画 JSON 数据,可以使用 Bodymovin 插件将 After Effects 动画导出为 JSON。


步骤三:调整参数和样式


lottieOptions 中,你可以设置各种参数来控制动画的行为,比如是否循环、是否自动播放等。同时,你可以通过样式表中的 CSS 来调整动画的位置和大小,以适应你的页面布局。


/* HelloWorld.vue */

<style>
.lottie {
margin: 20px auto; /* 调整动画的位置 */
}
</style>

四 Lottie 的主要配置参数


Lottie 提供了一系列配置参数,以便你能够定制化和控制动画的行为。以下是 Lottie 的主要配置参数以及它们的使用方法:


1. container


container 参数用于指定动画将被插入到页面中的容器元素。可以是 DOM 元素,也可以是一个用于选择元素的 CSS 选择器字符串。


示例:


// 使用 DOM 元素作为容器
const container = document.getElementById('animation-container');

// 或者使用 CSS 选择器字符串
const container = '#animation-container';

// 初始化 Lottie 动画
const animation = lottie.loadAnimation({
container: container,
/* 其他配置参数... */
});

2. renderer


renderer 参数用于指定渲染器的类型,常用的有 "svg" 和 "canvas"。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
renderer: 'svg', // 或 'canvas'
/* 其他配置参数... */
});

3. loop


loop 参数用于指定动画是否循环播放。设置为 true 时,动画将一直循环播放。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
loop: true,
/* 其他配置参数... */
});

4. autoplay


autoplay 参数用于指定是否在加载完成后自动播放动画。设置为 true 时,动画将在加载完成后立即开始播放。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
autoplay: true,
/* 其他配置参数... */
});

5. path


path 参数用于指定动画 JSON 文件的路径或 URL。可以是相对路径或绝对路径。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
path: 'path/to/animation.json',
/* 其他配置参数... */
});

6. rendererSettings


rendererSettings 参数用于包含特定渲染器的设置选项。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
rendererSettings: {
clearCanvas: true, // 在每一帧上清除画布
},
/* 其他配置参数... */
});

7. animationData


animationData 参数允许你直接将动画数据作为 JavaScript 对象传递给 Lottie。可以用于直接内嵌动画数据而不是从文件加载。


示例:


const animationData = {
/* 动画数据的具体内容 */
};

const animation = lottie.loadAnimation({
container: '#animation-container',
animationData: animationData,
/* 其他配置参数... */
});

8. name


name 参数用于为动画指定一个名称。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
name: 'myAnimation',
/* 其他配置参数... */
});

9. speed


speed 参数用于控制动画的播放速度。1 表示正常速度,0.5 表示一半速度,2 表示两倍速度。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
speed: 1.5, // 播放速度为原来的1.5倍
/* 其他配置参数... */
});

10. 事件回调


Lottie 还支持通过事件回调来执行一些自定义操作,如 onCompleteonLoopCompleteonEnterFrame 等。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
loop: true,
onComplete: () => {
console.log('动画完成!');
},
/* 其他配置参数... */
});

通过灵活使用这些参数,你可以定制化你的动画,使其更好地满足项目的需求。


步骤五:运行项目


最后,确保你的 Vue 项目是运行在支持 Lottie 的环境中。启动项目,并在浏览器中查看效果:


npm run serve
# 或
yarn serve

访问 http://localhost:8080(具体端口可能会有所不同),你应该能够看到嵌入的 Lottie 动画正常播放。


结论


通过这些步骤,我们为 Vue 项目增添了一种引人注目的交互方式,提升了用户体验。Lottie 的强大功能和易用性使得在项目中集成动画变得轻而易举。希望本文对你在 Vue 项目中使用 Lottie 有所帮助。在应用中巧妙地使用动画,让用户感受到更加愉悦的交互体验。


作者:_XU
来源:juejin.cn/post/7301976895913623589
收起阅读 »

惊讶,Vite 原来也可以跑在浏览器

web
为大家介绍一个 vite 的一个终端插件,使之可以运行在浏览器中。它就是# vite-plugin-terminal。 Git 地址:github.com/patak-dev/v… vite-plugin-terminal 这个插件使用起来很简单,首先安装: ...
继续阅读 »


为大家介绍一个 vite 的一个终端插件,使之可以运行在浏览器中。它就是# vite-plugin-terminal


Git 地址:github.com/patak-dev/v…


vite-plugin-terminal


这个插件使用起来很简单,首先安装:


npm i -D vite-plugin-terminal

然后将插件添加到您的 vite.config.ts 配置中:


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal()
]
}

最后,你可以在源代码中像使用 console.log 一样使用它。


import terminal from 'virtual:terminal';
import './module.js';

terminal.log('Hey terminal! A message from the browser');

const json = { foo: 'bar' };

terminal.log({ json });

terminal.assert(true, 'Assertion pass');
terminal.assert(false, 'Assertion fails');

terminal.info('Some info from the app');

terminal.table(['vite', 'plugin', 'terminal']);

看看效果。



体验地址:stackblitz.com/edit/github…


将日志导入终端


如果您希望标准 console 日志出现在终端中,您可以使用以下 console: 'terminal' 选项 vite.config.ts:


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal({
console: 'terminal'
})
]
}

在这种情况下,就不需要导入虚拟终端来使用该插件。


console.log('Hey terminal! A message from the browser')

如果想要更多控制,也可以手动在脑海中覆盖它。


  <script type="module">
// Redirect console logs to the terminal
import terminal from 'virtual:terminal'
globalThis.console = terminal
</script>

双端控制台


如果希望同时控制登录终端和控制台,可以使用 output 选项来定义 terminal 应记录日志的位置。接受 terminal、console 或同时包含两者的数组。


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal({
output: ['terminal', 'console']
})
]
}


其他


这个插件方法非常多,基本和 console 一样。


terminal.log(obj1 [, obj2, ..., objN])
terminal.info(obj1 [, obj2, ..., objN])
terminal.warn(obj1 [, obj2, ..., objN])
terminal.error(obj1 [, obj2, ..., objN])
terminal.assert(assertion, obj1 [, obj2, ..., objN])
terminal.group()
terminal.groupCollapsed()
terminal.groupEnd()
terminal.table(obj)
terminal.time(id)
terminal.timeLog(id, obj1 [, obj2, ..., objN])
terminal.timeEnd(id)
terminal.clear()
terminal.count(label)
terminal.countReset(label)
terminal.dir(obj)
terminal.dirxml(obj)

也可以定制一些配置。
例如上面介绍到的 console,设置为 'terminal' 使其 globalThis.console 等于terminal 应用程序中的对象。设置 output,定义日志的输出位置。设置 strip,terminal.*()生产时捆扎时剥去。还可以设置 includeexclude 用来指定插件在删除生产调用时应在构建中操作的文件和指定插件在删除生产调用时应忽略的构建中的文件。


小结


# vite-plugin-terminal 换种方式颠覆了现在大多人本地开发的模式,如果用来快速做演示 demo,是一个非常不错的选择。但是当前这个插件还是存在不少的问题,不过真的要用在大型商业项目里面时候,就要考虑跟 Devops系统的集成,希望# vite-plugin-terminal完全成熟开源后,能给开发者带来更多的便利。


参考



作者:拜小白
来源:juejin.cn/post/7301909438540333067
收起阅读 »

🔥🔥🔥“异步”是好还是坏?怎么灵活使用?看这边!🔥🔥🔥

web
前言     今天我们来聊一聊JS中的代码“异步”问题。先让我们简单看一看下面的代码: function a() { setTimeout(() => { console.log('写文章'); }, 1000) } f...
继续阅读 »

前言


    今天我们来聊一聊JS中的代码“异步”问题。先让我们简单看一看下面的代码:


function a() {
setTimeout(() => {
console.log('写文章');
}, 1000)
}

function b() {
setTimeout(() => {
console.log('发布');
}, 0)
}

a()
b()

    我们分别设置了ab两个函数;再分别设置一个计时器a函数设定为1秒,b函数设定为0秒;最后分别调用ab两函数。我们都知道JavaScript是从上往下单线程执行的,很明显此处我们想要的效果肯定是先“写文章”,1秒后再“发布”,让我们看看效果:


image.png


    很可惜事与愿违,我们连“写文章”都还没写呢就已经“发布”了。


    那么为什么会这样呢?当代码读取到调用a函数时,确实是是先执行了a函数,但同时浏览器引擎也不会傻傻等待a函数执行完再进行下一步,它会同时也执行b函数。而根据我们的设定,b函数的计算器设定为0秒并不需要等待,所以我们先得到的就是“发布”,而不是预期的“写文章”。这就是“异步”。


正文


异步问题


    在JavaScript中,异步编程是一种处理非阻塞操作的方式,使得代码可以在等待某些操作完成的同时继续执行其他任务。JavaScript是从上往下单线程执行的,但通过异步编程,可以实现在等待一些I/O操作、网络请求或定时器等时不阻塞整个程序的执行。同一时间干多步事情,让JS执行效率更高——这就是异步的优点。但异步有好处也有坏处,举个“栗子”:当b需要拿到a给出的结果才能执行的时候,异步会让还未拿到a结果的b也执行,这就会出问题,也就叫异步问题。就像我们前言中展示的那样,那碰到这种问题该怎么解决呢?


回调(Callback)


    有一种老的解决办法就是回调:把b的执行扔进a,等a执行完自然就轮到了b。让我们简单试一试:


function a() {
setTimeout(() => {
console.log('写文章');
b()
}, 1000)
}

function b() {
setTimeout(() => {
console.log('发布');
}, 0)
}

a()

再看看执行结果:


image.png


    我们在等待了一秒之后完成了“写文章”随后立即“发布”了。这样确实看起来解决了异步问题,但这也同时会带来新的问题。在回调中我们有一种情况叫做“回调地狱”:当回调的数量多起来的时候,执行的链就会非常长,类似于物理中的串联,有一个元件出了问题,整段代码就会崩掉,并且代码维护起来也会非常麻烦,得从头到尾查找问题。以下是一个简单的“回调地狱”例子,使用了多个嵌套的回调函数,模拟了异步操作的情况:


// 模拟异步操作1
function asyncOperation1(callback) {
setTimeout(function() {
console.log("Async Operation 1 completed");
callback();
}, 1000);
}

// 模拟异步操作2
function asyncOperation2(callback) {
setTimeout(function() {
console.log("Async Operation 2 completed");
callback();
}, 1000);
}

// 模拟异步操作3
function asyncOperation3(callback) {
setTimeout(function() {
console.log("Async Operation 3 completed");
callback();
}, 1000);
}

// 嵌套回调地狱
asyncOperation1(function() {
asyncOperation2(function() {
asyncOperation3(function() {
console.log("All async operations completed");
});
});
});

    在上述例子中,asyncOperation1asyncOperation2asyncOperation3 分别代表三个异步操作。它们的回调函数嵌套在彼此之内,形成了回调地狱。当异步操作数量增加时,这种嵌套结构会变得难以理解和维护。因此,使用Promise或更先进的异步处理方式通常更为推荐。这有助于避免回调地狱,提高代码的可读性和可维护性。


Promise


    在JavaScript中,Promise是一种用于处理异步操作的对象,它提供了更优雅的方式来组织和处理异步代码。Promise可以通过.then()链式调用,使得多个异步操作可以依次执行,而不是嵌套在回调中,使得异步代码更易于理解维护,避免了回调地狱(Callback Hell)。还可以通过.then()方法处理Promise成功状态,通过.catch()方法处理Promise失败状态。这种分离成功和失败的处理方式更加清晰。


    下面是一个简单的Promise示例:


// 创建一个Promise对象
let myPromise = new Promise(function(resolve, reject) {
// 异步操作
setTimeout(function() {
let success = true;

if (success) {
resolve("Promise resolved!");
} else {
reject("Promise rejected!");
}
}, 1000);
});

// 处理Promise成功状态
myPromise.then(function(result) {
console.log(result);
})
// 处理Promise失败状态
.catch(function(error) {
console.error(error);
});

    在这个例子中,myPromise表示一个异步操作,通过resolvereject函数表示成功和失败。.then()方法用于处理成功状态,.catch()方法用于处理失败状态。Promise的引入使得异步代码更为结构化,便于阅读维护


结语


    这次文章我们简单介绍了JavaScript中的“异步”、“回调”以及“Promise对象”。当然Promise身为一个对象肯定远不止这么几个方法!JavaScript的世界是那么的广阔,如果关于JS的内容对你有帮助的话,希望能给博主一个免费的小心心♡呀~


作者:Mio_02
来源:juejin.cn/post/7301914624140034083
收起阅读 »

React框架部署实战:打造高效现代化的Web应用

web
React框架部署实战 在前端开发领域,React框架因其高效的组件化架构和出色的性能而备受开发者喜爱。然而,要让React应用真正展现其魅力,一个关键的环节就是部署。 本文将带领你深入了解React框架的部署过程,为你的Web应用提供一个高效、稳定的运行...
继续阅读 »

React框架部署实战



在前端开发领域,React框架因其高效的组件化架构和出色的性能而备受开发者喜爱。然而,要让React应用真正展现其魅力,一个关键的环节就是部署。



本文将带领你深入了解React框架的部署过程,为你的Web应用提供一个高效、稳定的运行环境。


📍 准备工作:配置环境


在开始部署React应用之前,确保你的开发环境已经配置完善。首先,安装Node.js和npm,这是React应用的基础依赖。随后,使用以下命令安装Create React App,这是一个官方推荐的React应用脚手架,简化了项目的初始化和配置过程。


npx create-react-app my-react-app

📍 生产环境构建:优化性能


React应用在开发过程中使用的是开发环境配置,而在部署到生产环境时,我们需要进行一些优化以提升性能。使用以下命令进行生产环境构建:


npm run build

这将生成一个build文件夹,包含了优化后的、用于生产环境的代码。这一步骤将帮助你减小应用的体积,提高加载速度,使其更适合在生产环境中运行。


📍 选择合适的服务器:保障稳定性


选择一个合适的服务器对于React应用的部署至关重要。你可以选择使用传统的Web服务器,比如Nginx或Apache,也可以考虑使用专门为React应用设计的服务器,如Express或Firebase Hosting。确保服务器能够正确配置,以支持React路由和处理单页面应用的特殊需求。


📍 域名与SSL:提升安全性


为你的React应用配置域名,并考虑启用SSL证书以提高安全性。在大多数情况下,你可以通过云服务提供商或第三方SSL证书颁发机构获取免费的SSL证书。使用HTTPS协议不仅有助于提升安全性,还有可能对搜索引擎排名产生积极影响。


📍 自动化部署:提高效率


自动化部署是一个高效的实践,可以减少人为错误并提高开发团队的工作效率。你可以考虑使用持续集成/持续部署(CI/CD)工具,如Jenkins、Travis CI或GitHub Actions,将代码的自动构建和部署流程整合到你的开发工作流中。


📍 监控与日志:保障可维护性


部署完成后,监控和日志记录是必不可少的环节。使用工具如Sentry、New Relic等,实时监测应用的性能和错误,及时发现并解决潜在的问题。同时,记录应用的日志可以帮助你追踪和分析用户行为,为后续的优化提供有力支持。


📍 版本管理:确保灵活性


在生产环境中,灵活地管理React应用的版本是至关重要的。使用工具如Docker,可以打包你的应用及其依赖,确保在不同环境中的一致性。结合版本控制工具如Git,能够轻松地进行回滚和发布新版本。


📍 总结


通过本文的步骤,你可以更好地了解如何部署React应用,确保其在生产环境中高效、稳定地运行。部署不仅仅是一个技术问题,更是一个关乎用户体验和团队效率的重要环节。通过合理的部署流程,你的React应用将能够展现出其设计之美和高效性能,为用户提供卓越的使用体验。


作者:知识浅谈
来源:juejin.cn/post/7301976895913689125
收起阅读 »

历时一个月,6年前端降薪上岸了

web
6年前端,11.15号,拿到了这一个月以来的第一份offer,降薪,教育行业,正如之前在文章中所说的,我就是头铁。 这篇文章就水一下我这差不多一个月的面试旅程吧。 我是在7.14号左右从一家培训公司毕业,然后拿了N+1,我就回家了,一直待到国庆之后才回的北京。...
继续阅读 »

6年前端,11.15号,拿到了这一个月以来的第一份offer,降薪,教育行业,正如之前在文章中所说的,我就是头铁。
这篇文章就水一下我这差不多一个月的面试旅程吧。
我是在7.14号左右从一家培训公司毕业,然后拿了N+1,我就回家了,一直待到国庆之后才回的北京。


第一次面试


第一个面试是我的朋友给我内推的公司,智联招聘,时间大概是在10.08,当时我啥也没有准备,裸面,最后还是被pass了。面试官当时问了我这么几个问题:




  1. 说说你自己想做的技术方向(主要是针对招聘岗位看看你是否合适,他们想找一个做监控平台和业务的)

  2. 你认为什么是好的代码

  3. 对程序设计原则有了解吗

  4. 对于质量保障,前端职能该做什么(前端质检工作)

  5. 小程序从聊天记录选择文件,上传失败,你觉得应该如何去解决和排查这个问题

  6. node是如何处理高并发的



从整个面试来看,涉及到前端技术相关的问题其实也没几个,第1个问题,面试官就已经给我挖坑了,我当时巴拉巴拉说了一堆关于前端基建和自己想做前端底层相关的事情,很明显,他需要的不是这样的人,因为他们现在做的还是以业务为主,还有监控相关的事情正在推进,需要有人着手去做。然后就是第4个问题,这个问题就可以考察出我对于前端整个开发生命周期的质量保证了解多少,从而判断出我是不是适合这个岗位,很明显的,我的回答并不满意。第2个和第3个问题,我确实以前没有考虑过,后来我查了一下,如果对理论知识比较关注的话,这些东西应该都需要了解的。由此考察出我对这方面知识的欠缺。第5个问题,是之前他们在开发遇到的一个问题,然后问我如何解决这个问题,我当时脑子短路了,一直在寻思这个问题到底出在哪里,而他想知道的是,当遇到问题的时候如何排查解决的一个思路,很明显,我走错路了。整个面试过程也很清晰,考察三个点,第一点是不是匹配他们的招聘岗位,第二点是不是理论基础比较扎实,第三点解决问题的能力。


如何复习


经过这次面试,我把上面几个问题总结了,在后面的面试中2、3、4经常被问到,我也是对答如流。由此开始了我的面试旅程。那么接下来就讲讲我是如何来复习和做总结的。


首先,复习基础知识(八股文),一般就是css、js、ts、vue、react、webpack浏览器相关面试题,如何去找面试题呢?我最常用的方式是掘金去看优质的文章,这是最浪费时间也是最能能掌握的一种方式,可能很多面试题手册写的就是针对面试题的那么几句话,并没有讲的很清楚,知其然,不知其所以然,问的详细一点就会懵了。如果需要面试题手册的可以加我WX:xiumubai01。然后就是就是总结,我选择的是xmind写个思维导图,罗列大纲。这样看起来就非常清晰,哪些知识点我复习过了,对应的每个知识点有哪些细节,我都会做出标记,这样,在我的脑海中就形成了一套面试的话术,我面试的时候也会根据这样一个结构去讲,一来我心中有思路,二来面试官听的不迷糊。
大概差不多像下面的这样的:
image.png


以某个知识点为例:
image.png
我把整个浏览器相关的知识点都总结到了一起,这样既可以方便复习,也能让我系统的掌握相关浏览器的知识点。
想要获取思维导图的道友们可以可以加我WX:xiumubai01


关于项目问题,我也提以下。非常重要。一般面试官会根据你写的简历上面的项目问你,让你讲讲做的最拿手的一个项目,做了哪些事?遇到过什么问题?怎么解决的?当你讲项目的时候,面试官就能直观的感受到你平时工作中到底几斤几两。我的项目中写了一个业务(剧本直播),微前端平台,低代码平台。比如我微前端平台面试官会问到的问题:你如何选型?为什么选这个框架(qiankun)?那qiankun当中你使用的时候遇到过什么问题?你觉得这个框架有哪些不足的地方?它是如何实现js沙箱隔离的?(变态一点的直接让你实现一个)。所以项目你一定要吃透。当你讲的过程中,人家会提问各种场景,问你如何解决的,如果你提前没有想到,那只能当场退役。


除了自己复习相关的知识点以外,面试总结特别重要。我养成的习惯是每次面试完了立马总结问到的问题,然后进行复盘,这次面试哪里回答的不好,面试官想要考察的能力是什么?这次回答不好的问题下次我能不能应对?按照我的经验,每次总结完之后大概率后面的面试都会问到同样的问题。以下是我总结的:
image.png


如果你面试完了记不住面试官问的问题,我的做法是掏出纸和笔,在面试官提问的时候,记下问题关键词,这样方便面试完了以后回忆。


如何coding


加下来就是关于coding题,很多人都会恐惧,我也是,没有思路,前一天刚刷的代码第二天就忘了。没有他法,脑子笨,只能靠理解加强记忆,那最好的办法就是你要把这道题目吃透,研究明白,思路清晰,其实不管任何代码,你先得知道这道题怎么解啊,然后才能用代码实现。我这里总结了我面试以来手写的一些coding题,放在github了。大家可以打开链接自取。



github: github.com/xiumubai/co…


gitee: gitee.com/xiumubai/co…



如何消除焦虑


在面试的过程中,难免会有学不进去,面试遭受打击,长时间没有offer心中气垒等等情况的发生。尤其是当你面试3-4轮以后,眼见要拿到offer了,最后杳无音信,或者人事直接通知你pass了,这时候是最打击人的。整个人就像被掏空了一样。


面对以上这种情况,我后来想了一种排解的方式,拉了个微信群,把最近找工作的道友们一起拉进去,大家互相鼓励,讨论遇到的面试题,最近的市场行情,这样可以分解一下压力,转移注意力,有可能有的人比你更惨。有想进群的朋友可以关注一下我的公众号「白哥学前端」,进群,群里有我最新的一些面试题和xmind文件分享。


当然保持积极的心情也很重要,不要做一些沉迷动作(比如打游戏,晚上不睡,早上不起),把自己的时间调整成上班的时间,这个时间点你就做跟学习有关的事情。


这个过程是很痛苦和煎熬的,相信大家都能坚持住,最后,祝大家都能顺利找到满意的工作。


作者:白哥学前端
来源:juejin.cn/post/7301909438540267531
收起阅读 »

前端版本过低引导弹窗方案分享

web
作者:费昀锋 背景 作为 TOB 的业务方,我们偶尔会收到一些如下图所示的反馈。 作为 PC 页面为主的业务方,大多数用户在一天的工作中,可能都不太会刷新或者重新打开我们的页面,导致我们在下午或者白天发布的前端版本,往往需要到几个小时甚至第二天,才能覆盖到...
继续阅读 »

作者:费昀锋



背景


作为 TOB 的业务方,我们偶尔会收到一些如下图所示的反馈。



作为 PC 页面为主的业务方,大多数用户在一天的工作中,可能都不太会刷新或者重新打开我们的页面,导致我们在下午或者白天发布的前端版本,往往需要到几个小时甚至第二天,才能覆盖到 98% 以上的用户。



我们统计了 bscm 平台 5 次下午 2-3 点左右发布的版本,在发布后每个时间段内老版本用户的占比情况。选择这个时间点发布的原因是这个时间点基本是平台用户的上班时间,是最有可能出现用户已经打开了页面同时我们在发布新代码的场景的,比较具有代表性。按平台用户六七点下班来看,我们可以看到还有将近 6% 的用户在当天是会一直访问老版本的前端代码的,按照 bscm 平台 1w+的 uv 来看,约有 600 多人会可能遇到前端版本过低导致的使用问题。


方案


弹窗内容



弹窗的触发条件


首先介绍两个概念,本地版本号和云端版本号。本地版本号是用户请求到的前端页面的代码版本号,是用户访问页面时决定;云端版本号可以理解为最新前端版本号,它是每次开发者发布前端代码时决定的。



判断触发条件的时机


有了弹窗的触发条件,我们还需要去决定什么时候判断弹窗是否满足触发的条件,上面也提到了,出现这类问题的场景多见于用户在使用过程中,开发者进行了前端代码发布,那我们主要可以有两个类型的时机去进行触发条件的判断。




  1. 前端代码去感知什么时候有新版本的代码发布了,去进行条件判断(消息推送)




  2. 前端在一定的条件下主动去判断触发条件(轮询,请求后端接口时,一些中频前端事件的监听)




我们对这些时机在更新是否及时,判断次数多少、实现成本高低等维度进行一个对比。



⭐️ 越多表示这个维度得分越高




根据表格可以看到 websocket 消息推送和前端事件监听这两种方案综合来看是更合适一些的,但是前端事件监听其实它的劣势在实际运用场景中会被弱化(一天的上线数量有限,请求次数一天不会多太多次),但是实现成本远低于 websocket,所以无疑是实际落地场景中比较理想的选择。



根据 can i use 的结果我们也可以发现 visibilitychange 事件也基本符合我们目前 B 端页面对于 PC 浏览器的要求。


版本号的生成


本地版本号


本地版本号是用户访问时决定的,那无疑页面的 html 文件就是这个版本号存在的最佳载体,我们可以在打包时通过 plugin 给 html 文件注入一个版本号。


云端版本号


云端版本号的选择则有很多方式了,数据库、cdn 等等都可以满足需求。不过考虑到实现成本和泳道的情况,想了一下两个思路一个是打包的同时生成一个 version.json 文件,配一个路由去访问;另一个是直接访问对应的 html 代码,解析出注入的版本号,二者各自有适合的场景。


微前端的适配


我们现在的大多数项目都包含了主应用和子应用,那其实不管是子应用的更新还是主应用的更新都应该有相关的提示,而且相互独立,但同时又需要保证弹窗不同时出现。


想要沿用之前的方案其实只需要解决三个问题。



  1. 主子应用的本地版本号标识需要有区分,因为 html 文件只有一个,需要能在 html 文件中区分出哪个应用的版本是什么,这个我们只需在 plugin 中注入标识即可解决。

  2. 云端版本号请求时也要请求对应的云端版本号,这个目前采用的方案是主应用去请求唯一的 version.json 文件,因为主应用路由是唯一的,子应用则去请求最新的 html 资源文件,解析出云端版本号。

  3. 不重复弹窗我们只需要在展示弹窗前,多加一个是否已经有弹窗展示的判断即可了。


具体实现


版本号的写入和读取



监听时机和频控逻辑


正如前文提到的,本身版本发布不是一个高频事件,但是监听事件的频次有时候可能过高了,不希望频繁的去进行触发条件判断。同时如果出现一天内多次发布的场景,也不希望这个弹窗对于用户有过多的打扰,所以需要去添加一个频控逻辑。



具体代码


plugin


/* eslint-disable */
import { CoraWebpackPlugin, WebpackCompiler } from '@ies/eden-web-build';
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');

interface IVersion {
name?: string; // 编译完的文件夹名称
subName?: string; // 子应用的名称,主应用可以不传
}

export class VersionPlugin implements CoraWebpackPlugin {
readonly name = 'versionPlugin'; // 插件必须要有一个名字,这个名字不能和已有插件冲突
private _version: number;
private _name: string;
private _subName: string;
constructor(params: IVersion) {
this._version = new Date().getTime();
this._name = params?.name || 'build';
this._subName = params?.subName || ''
}
apply(compiler: WebpackCompiler): void {
compiler.hooks.afterCompile.tap('versionPlugin', () => {
try {
const filePath = path.resolve(`./${this._name}/template/version.json`);
fs.writeFile(filePath, JSON.stringify({ version: this._version }), (err: any) => {
if (err) {
console.log('@@@err', err);
}
});
const htmlPath = path.resolve(`./${this._name}/template/index.html`);
const data = fs.readFileSync(htmlPath);
const $ = cheerio.load(data);
$('body').append(`
${this._subName}versionTag" style="display: none">${this._version}
`
);
fs.writeFile(htmlPath, $.html(), (err: any) => {
if (err) {
console.log('@@@htmlerr', err);
}
});
} catch (err) {
console.log(err);
}
});
}
}

弹窗组件


import React, { useEffect } from 'react';

import { Modal } from '@ecom/auxo';
import axios from 'axios';
import moment from 'moment';

export interface IProps {
isSub?: boolean; // 是否为子应用
subName?: string; // 子应用名称
resourceUrl?: string; // 子应用的资源url
}

export type IType = 'visibilitychange' | 'popstate' | 'init';

export default React.memo<IProps>(props => {
const { isSub = false, subName = '', resourceUrl = '' } = props || {};

const cb = (latestVersion: number | undefined, currentVersion: number | undefined, type: IType) => {
try {
// 版本落后,提示可以刷新页面
if (latestVersion && currentVersion && latestVersion > currentVersion) {
// 提醒过了就设置一个更新提示过期时间,一天内不需要再提示了,弹窗过期时间暂时全局只需要一个!!
localStorage.setItem(`versionUpdateExpireTime`, moment().endOf('day').format('x'));
if (!document.getElementById('versionModalTitle')) {
Modal.confirm({
title: <div id="versionModalTitle">版本更新提示div>,
content:
'您已经长时间未使用此页面,在此期间平台有过更新,如您此时在页面中没有填写相关信息等操作,请点击刷新页面使用最新版本!',
okText: <div data-text={`前端版本升级引导-立即更新 ${type}`}>刷新页面div>,
cancelText: <div data-text={`前端版本升级引导-我知道了 ${type}`}>我知道了div>,
onCancel: () => {
console.log('fe-version-watcher INFO: 未更新~');
},
onOk: () => {
location.reload();
},
});
}
}
// 不管版本是否落后,半小时内都不需要去重新请求判断
localStorage.setItem(`versionInfoExpireTime`, String(new Date().getTime() + 1000 * 60 * 30));
} catch {}
};

const formatVersion = (text?: string) => (text ? Number(text) : undefined);

useEffect(() => {
try {
const fn = function (type: IType) {
if (document.visibilityState === 'visible') {
/**
*
@desc 为了防止打扰,版本更新每个应用一天只提示一次 所以过期时间设为当天23:59:59,没过期则直接return
*/

if (Number(localStorage.getItem(`versionUpdateExpireTime`) || 0) >= new Date().getTime()) {
return;
}
/**
*
@desc 不需要每次切换页面都去判断资源,每次从服务器获取到的版本信息,给半个小时的缓存时间,需要区分子应用
*/

if (Number(localStorage.getItem(`versionInfoExpireTime`) || 0) > new Date().getTime()) {
return;
}

if (!isSub) {
/**
*
@desc 主应用使用version.json文件来获取最新的版本号
*/

const dom = document.getElementById('versionTag');
const currentVersion = formatVersion(dom?.innerText);
axios.get(`/version?timestamp=${new Date().getTime()}`).then(res => {
const latestVersion = res?.data?.version;
cb(latestVersion, currentVersion, type);
});
} else {
/**
*
@desc 子应用使用最新html中的innerText来获取最新版本号
*/

if (resourceUrl) {
const dom = document.getElementById(`${subName}versionTag`);
const currentVersion = dom?.innerText ? Number(dom?.innerText) : undefined;
axios.get(resourceUrl).then(res => {
/** ignore_security_alert */
try {
const html = res.data;
const doc = new DOMParser().parseFromString(html, 'text/html');
const latestVersion = formatVersion(doc.getElementById(`${subName}versionTag`)?.innerText);
cb(latestVersion, currentVersion, type);
} catch {}
});
}
}
}
};
const visibleFn = () => {
fn('visibilitychange');
};
const routerFn = () => {
fn('popstate');
};
if (isSub) {
// 子应用可能会有缓存,初始化的时候先判断一次
fn('init');
}
document.addEventListener('visibilitychange', visibleFn);
window.addEventListener('popstate', routerFn);
return () => {
document.removeEventListener('visibilitychange', visibleFn);
window.removeEventListener('popstate', routerFn);
};
} catch {}
}, []);

return <div />;
});

如何接入


主应用版本



  1. 安装依赖


npm i @ecom/fe-version-watcher-plugin # 安装plugin 
npm i @ecom/logistics-supply-chain-fe-version-watcher # 安装引导弹窗


  1. 引入 versionPlugin,自动生成 version.json + html 文件中自动注入


import { VersionPlugin } from '@ecom/fe-version-watcher-plugin';

// 有些项目打包后template文件夹下的名字不是build而是build_cn
// 可以根据自己项目的实际情况传入{name: build_cn}

{
...,
plugins: [
...,
[VersionPlugin, {}],
]
}


  1. 引入版本引导弹窗


import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';

<FeVersionWatcher />


  1. goofy 新增路由配置,/version 指向 version.json 文件 (或者其它方式可以使得/version 的路由指向该 version.json 文件)



预告


采用 version.json 的方案,引入 FersionWatcher 组件就不再需要任何参数,目前主应用只支持这种模式。未来也将参考子应用,主应用支持读取 html 中版本标识的能力,将配置路由的工作改成组件 props 传入资源 url,开发者可以根据实际情况自行选择。


子应用版本



  1. 安装依赖


npm i @ecom/fe-version-watcher-plugin # 安装plugin
npm i @ecom/logistics-supply-chain-fe-version-watcher # 安装引导弹窗


  1. 引入 versionPlugin, html 文件中自动注入版本号,需要子应用标识参数(必填)


import { VersionPlugin } from '@ecom/fe-version-watcher-plugin';

// 有些项目打包后template文件夹下的名字不是build而是build_cn
// 可以根据自己项目的实际情况传入{name: build_cn}

{
...,
plugins: [
...,
[VersionPlugin, {subName: 'general-supplier', name: 'build_cn'}],
]
}


  1. 引入版本引导弹窗(subName 和 plugin 中保持一致,resourceUrl 为配置的子应用路由)


import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';

// subName需要和plugin的参数保持一致,resourceUrl为子应用资源的路径(子引用goofy上配置的路由)
<FeVersionWatcher isSub subName="general-supplier" resourceUrl="/webApp/general-supplier" />

resourceUrl一般就是goofy上配置的路由设置,,如果不同平台有区分,可以动态传入。



如何调试/效果展示


发布成功后,可以根据如下步骤测试:




  1. 删除 localstorage 中相关的 value





  2. 修改 html 中的 version,改成一个比较小的数值即可





  3. 切换路由,或者隐藏/打开页面,出现弹窗




收益统计



同样我们截取了 4 次该平台 2-3 点发布的版本情况,可以看到老版本用户的 uv 占比有着明显的下降。



上线至今共计提示 10 万+用户,帮助约 5 万人次及时更新了前端代码。


作者:字节前端
来源:juejin.cn/post/7301530293377843235
收起阅读 »

防抖是回城,节流是攻击

web
前言 防抖和节流是前端开发中常用的函数优化手段,它们可以限制函数的执行频率,提升性能和用户体验。在我们的日常开发中,经常会遇到一些需要对函数进行优化的场景,比如防止表单的重复提交。 一 防抖与节流的区别 我们简单描述下它们的作用 防抖:它限制函数在一段连续的时...
继续阅读 »

前言


防抖节流是前端开发中常用的函数优化手段,它们可以限制函数的执行频率,提升性能和用户体验。在我们的日常开发中,经常会遇到一些需要对函数进行优化的场景,比如防止表单的重复提交。


一 防抖与节流的区别


我们简单描述下它们的作用


防抖:它限制函数在一段连续的时间内只执行一次。当连续触发某个事件时,只有在事件停止触发一段时间后,才会执行函数。


节流:它按照固定的时间间隔执行函数。当连续触发某个事件时,每隔一段时间执行一次函数。


简而言之,防抖是在事件停止触发后延迟执行函数,而节流是按照固定的时间间隔执行函数。


因为防抖节流的作用和应用场景基本相同,也就导致它们容易被人混淆,不好记忆。


之前在网上看到了一个例子非常的有趣形象,和大家分享下。


王者荣耀大家都玩过吧,里面的英雄都有一个攻击间隔,当我们连续的点击普通攻击的时候,英雄的攻速并不会随着我们点击的越快而更快的攻击。这个其实就是节流,英雄会按照自身攻速的系数执行攻击,我们点的再快也没用。


而防抖在王者荣耀中就是回城,在游戏中经常会遇到连续回城嘲讽对手的玩家,它们每点击一次回城,后一次的回城都会打断前一次的回城,只有最后一次点击的回城会被触发,从而保证回城只执行一次,这就是防抖的概念。


自从我看到这个例子后,节流和防抖就再也没记混过了。作为一个8年王者老玩家。


下面是防抖和节流的实现


防抖的实现与使用


防抖的应用场景:



  1. 输入框搜索:当用户在搜索框中输入关键字时,使用防抖可以避免频繁发送搜索请求,而是在用户停止输入一段时间后才发送请求,减轻服务器压力。

  2. 窗口调整:当窗口大小调整时,使用防抖可以避免频繁地触发重排和重绘操作,提高页面性能。

  3. 按钮点击:当用户点击按钮时,使用防抖可以避免用户多次点击造成的多次提交或重复操作。


immediate参数用于控制防抖函数是否立即触发,true立即触发,false过delay时间后触发。


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按钮</button>
<script>
function debounce(func, delay, immediate) {
let timer;
return function () {
let context = this;
let args = arguments;

if (timer) {
clearTimeout(timer)
}

if (immediate && !timer) {
func.apply(context, args)
}

timer = setTimeout(() => {
timer = null
if (!immediate) {
func.apply(context, args)
}
}, delay);
}
}

// 创建一个被防抖的函数
const debouncedFunction = debounce(() => {
console.log("Debounced function executed.");
}, 1000, false);

document.getElementById('btn').addEventListener('click', debouncedFunction)

</script>
</body>

</html>

节流的实现与使用


节流的应用场景:



  1. 页面滚动:当页面滚动时,使用节流可以限制滚动事件的触发频率,减少事件处理的次数,提高页面的响应性能。

  2. 鼠标移动:当鼠标在某个元素上移动时,使用节流可以减少事件处理的次数,避免过于频繁的操作。


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按钮</button>
<script>

function throttle(func, delay, immediate) {
let timer;
return function () {
const context = this
const args = arguments

if (timer) {
return
}

if (!timer && immediate) {
func.apply(context, args)
}

timer = setTimeout(() => {
timer = null

if (!immediate) {
func.apply(context, args)
}
}, delay);
}
}

// 创建一个被节流的函数
const throttledFunction = throttle(() => {
console.log("throttled function executed.");
}, 1000, false);

document.getElementById('btn').addEventListener('click',throttledFunction)
</script>
</body>

</html>

结尾


看完本文章后,希望能够加深大家对防抖和节流的印象,分清二者的区别。


作者:欲买炸鸡同载可乐
来源:juejin.cn/post/7301244391153467431
收起阅读 »

一文带你如何优雅地封装小程序蓝牙模块

web
一. 前言。 蓝牙功能在我们日常软件中的使用率还是蛮高的----譬如各类共享单车/电单车。正因此,我们开发中接触蓝牙功能也是日渐增长。对于很多从未开发过蓝牙功能的童鞋来说,当PM小姐姐扔过来一个蓝牙协议要你接入时,简直一头雾水(我是谁?我在哪?)。只能一翻度娘...
继续阅读 »

一. 前言。


蓝牙功能在我们日常软件中的使用率还是蛮高的----譬如各类共享单车/电单车。正因此,我们开发中接触蓝牙功能也是日渐增长。对于很多从未开发过蓝牙功能的童鞋来说,当PM小姐姐扔过来一个蓝牙协议要你接入时,简直一头雾水(我是谁?我在哪?)。只能一翻度娘和AI,可是网上文章大多水准参差不齐,技术五花八门,没法真正地让你从无到有掌握蓝牙功能/协议对接。


二. 说明。


本文就基于uni-app框架结合微信和支付宝小程序为例,来讲述蓝牙功能在各类型小程序中的整体开发流程和如何“优雅”高效的封装蓝牙功能模块。本文使用到的主要技术栈和环境有:



  • uni-app

  • JavaScript

  • AES加解密

  • 微信小程序

  • 支付宝小程序


三. 蓝牙流程图。


正所谓“知己知彼,百战不殆”,所以在讲述蓝牙模块如何在小程序中开发和封装之前,我们先要了解蓝牙功能模块是如何在小程序中“走向”的,各API是如何交互通讯的。为了让大家看得清楚,学的明白----这里简明扼要地梳理了一份蓝牙核心API流程图(去除了非必要的逻辑走向,只展示了实际开发中最重要的步骤和交互)。



  • uni-app: 蓝牙API

  • 微信小程序:蓝牙API

  • 支付宝小程序:蓝牙API

  • 核心API流程图(注:每家厂商的小程序API大同小异,uni-app的基本通用,具体明细详见各厂商开发文档):


小程序蓝牙流程.png


四. 蓝牙协议。


了解完开发所需的API后,就需要根据实际开发场景中所对接的硬件和其厂家提供的蓝牙对接协议来结合上述的API来编写代码了。每家厂商的蓝牙协议是不一样的,不过“万变不离其宗”。只要知道其中的规则,真正看懂一家,那换其他家的也是可以看懂的。本文以下述协议(蓝牙寻车+蓝牙开锁)为例解释下。


1. 寻车:



  • 协议内容:


image.png



  • 解读:


根据上述图文的描述,我们可以知道想要开启蓝牙锁,那么必须先通过寻车蓝牙指令(7B5B01610060 或 7B5B01610160)写入,然后根据蓝牙响应的信息功能体和错误码判断响应是否正确,如正确,那么就拿到此时的随机数,后根据协议规定对该随机数做相应的处理,最后将处理后得到的结果用于组装开锁的蓝牙写入指令。



  • 案例代码:


image.png
image.png


2. 开锁:



  • 协议内容:


image.png



  • 解读:


根据上述图文的描述,我们可以知道开锁的写入指令是需要自己组装的,组装规则为:7B5B(数据头) 1B(信息体长度) 62(信息功能) 00(秘钥索引)018106053735(补1位0的电话号码)4B大端的时间戳 寻车拿到的随机码补8位0后经AES加密组合得到的16B数据 00(校验码);所以开锁写入的数据就是这种(案例:7B5B1B6200018106053735XXXXXXXXXXXXXXXXXXXX)。响应的话,也是根据信息功能体和错误码来判断开锁失败(9201)还是成功(9200)。



  • 案例代码:


image.png


五.代码编写。


这里为了提高蓝牙模块的代码耦合度,我们会把业务层和蓝牙模块层分离出来----也就是会把蓝牙整体流程交互封装成一个蓝牙模块js,然后根据业务形态,在各个业务层面上通过传参的形式来区分每个组件的蓝牙功能。


1. 业务层:



  • 核心代码:


//引入封装好的蓝牙功能JS模块核心方法函数
import { operateBluetoothYws } from '@/utils/bluetoothYws.js';

//调用蓝牙功能
blueTooth() {
//初始化蓝牙模块,所有的蓝牙API都需要在此步成功后才能调用
uni.openBluetoothAdapter({
success(res) {
console.log('初始化蓝牙成功res', res);
let mac = 'FF8956DEDA29';
let key = 'oYQMt8LFavXZR6sB';
operateBluetoothYws('open', mac, key, flag => {
if (flag) {
console.log('flag存在回调函数--蓝牙成功,可以执行后续步骤了', flag);
} else {
console.log('flag不存在回调函数--蓝牙成功,可以执行后续步骤了', flag);
}
})
},
fail(err) {
console.log('初始化蓝牙失败err', err);
}
})
},


  • 解读:


这里是我们具体业务层需要的写法,一开始就是引入我们封装好的蓝牙JS模块核心方法函数(operateBluetoothYws),然后启用uni.openBluetoothAdapter这个蓝牙功能启动前提,成功后在其success内执行operateBluetoothYws方法,此时的参数根据实际开发业务和相对应的蓝牙协议而定(这里以指令参数、设备编号和AES加密秘钥为例),实际中每个mac和key是数据库一一匹配的,我们按后端童鞋提供的接口获取即可(这里为了直观直接写死)。


2. 蓝牙模块层:



  • 核心代码:


let CryptoJS = require('./crypto-js.min.js'); //引入AES加密
let callBack = null; //回调函数,用于与业务层交互
let curOrder; //指令(开锁还是关锁后取锁的状态)
let curMac; //当前扫码的车辆编码对应的设备mac
let curKey; //当前扫码的车辆编码对应的秘钥secret(用于AES加密)
let curDeviceId; //当前扫码的车辆编码对应的设备的 id
let curServiceId; //蓝牙服务 uuid,需要使用 getBLEDeviceServices 获取
let curCharacteristicRead; //当前设备读的uuid值
let curCharacteristicWrite; //当前设备写的uuid值


//蓝牙调用核心方法(order: 指令;mac:车辆编码;key:秘钥secret;cb:回调)
function operateBluetoothYws(order,mac, key, cb) {
curOrder = order;
curMac = mac;
curKey = key;
callBack = cb
searchBluetooth();
}

//第一步(uni.startBluetoothDevicesDiscovery(OBJECT),开始搜寻附近的蓝牙外围设备。)
function searchBluetooth() {
uni.startBluetoothDevicesDiscovery({
services: ['00000001-0000-1000-8000-00805F9B34FB', '00000002-0000-1000-8000-00805F9B34FB'],
success(res) {
console.log('第一步蓝牙startBluetoothDevicesDiscovery搜索成功res', res)
watchBluetoothFound();
},
fail(err) {
console.log('第一步蓝牙startBluetoothDevicesDiscovery搜索失败err', err)
callBack && callBack(false)
}
})
}

//第二步(uni.onBluetoothDeviceFound(CALLBACK),监听寻找到新设备的事件。)
function watchBluetoothFound() {
uni.onBluetoothDeviceFound(function(res) {
curDeviceId = res.devices.filter(i => i.localName.includes(curMac))[0].deviceId;
stopSearchBluetooth()
connectBluetooth()
})
}

//第三步(uni.createBLEConnection(OBJECT),连接低功耗蓝牙设备。)
function connectBluetooth() {
if (curDeviceId.length > 0) {
// #ifdef MP-WEIXIN
uni.createBLEConnection({
deviceId: curDeviceId,
timeout: 5000,
success: (res) => {
console.log('第三步通过deviceId连接蓝牙设备成功res', res);
getBluetoothServers()
},
fail: (err) => {
console.log('第三步通过deviceId连接蓝牙设备失败err', err);
callBack && callBack(false)
}
});
// #endif
// #ifdef MP-ALIPAY
my.connectBLEDevice({
deviceId: curDeviceId,
timeout: 5000,
success: (res) => {
console.log('第三步通过deviceId连接蓝牙设备成功res', res);
getBluetoothServers()
},
fail: (err) => {
console.log('第三步通过deviceId连接蓝牙设备失败err', err);
callBack && callBack(false)
}
});
// #endif
}
}

//第四步(uni.stopBluetoothDevicesDiscovery(OBJECT),停止搜寻附近的蓝牙外围设备。)
function stopSearchBluetooth() {
uni.stopBluetoothDevicesDiscovery({
success: (res) => {
console.log('第四步停止搜寻附近的蓝牙外围设备成功res', res);
},
fail: (err) => {
console.log('第四步停止搜寻附近的蓝牙外围设备失败err', err);
}
})
}

//第五步(uni.getBLEDeviceServices(OBJECT),获取蓝牙设备所有服务(service)。)
function getBluetoothServers() {
uni.getBLEDeviceServices({
deviceId: curDeviceId,
success(res) {
console.log('第五步获取蓝牙设备所有服务成功res', res);
//这里取res.services中的哪个,这是硬件产商配置好的,不同产商不同,具体看对接协议
if (res.services && res.services.length > 1) {
curServiceId = res.services[1].uuid
getBluetoothCharacteristics()
}
},
fail(err) {
console.log('第五步获取蓝牙设备所有服务失败err', err);
callBack && callBack(false)
}
})
}

//第六步(uni.getBLEDeviceCharacteristics(OBJECT),获取蓝牙设备某个服务中所有特征值(characteristic)。)
function getBluetoothCharacteristics() {
// #ifdef MP-WEIXIN
uni.getBLEDeviceCharacteristics({
deviceId: curDeviceId,
serviceId: curServiceId,
success: (res) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值成功res', res);
curCharacteristicWrite = res.characteristics.filter(item => item && item.uuid.includes('0002'))[
0].uuid
curCharacteristicRead = res.characteristics.filter(item => item && item.uuid.includes('0003'))[
0].uuid
notifyBluetoothCharacteristicValueChange()
},
fail: (err) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值失败err', err);
callBack && callBack(false)
}
});
// #endif
// #ifdef MP-ALIPAY
my.getBLEDeviceCharacteristics({
deviceId: curDeviceId,
serviceId: curServiceId,
success: (res) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值成功res', res);
curCharacteristicWrite = res.characteristics.filter(item => item && item.characteristicId.includes('0002'))[
0].characteristicId
curCharacteristicRead = res.characteristics.filter(item => item && item.characteristicId.includes('0003'))[
0].characteristicId
notifyBluetoothCharacteristicValueChange()
},
fail: (err) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值失败err', err);
callBack && callBack(false)
}
});
// #endif
}

//第七步(uni.notifyBLECharacteristicValueChange(OBJECT),启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值。)
function notifyBluetoothCharacteristicValueChange() {
uni.notifyBLECharacteristicValueChange({
deviceId: curDeviceId,
serviceId: curServiceId,
characteristicId: curCharacteristicRead,
state: true,
success(res) {
console.log('第七步启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值成功res', res);
if(curOrder == 'open'){
//寻车指令
getRandomCode();
}else if(curOrder == 'close'){
//查看锁状态指令
getLockStatus();
}else{

}
//第八步(监听)(uni.onBLECharacteristicValueChange(CALLBACK),监听低功耗蓝牙设备的特征值变化事件。),含下发指令后的上行回应接受
//这里会一直监听设备上行,所以日志等需清除
uni.onBLECharacteristicValueChange((characteristic) => {
// #ifdef MP-WEIXIN
//完整的蓝牙回应数据
let ciphertext = ab2hex(characteristic.value);
//蓝牙回应数据的信息功能体和错误码
let curFeature = ab2hex(characteristic.value).slice(6, 10);
//蓝牙回应数据的错误码
let errCode = ab2hex(characteristic.value).slice(8, 10);
// #endif

// #ifdef MP-ALIPAY
//完整的蓝牙回应数据
let ciphertext = characteristic.value;
//蓝牙回应数据的信息功能体和错误码
let curFeature = characteristic.value.slice(6, 10);
//蓝牙回应数据的错误码
let errCode = characteristic.value.slice(8, 10);
// #endif
if (curFeature.startsWith('91')) { //寻车响应,拿到随机码
//用于给开锁的随机码
getUnlockData(ciphertext)
} else if (curFeature.startsWith('9200')) { //开锁响应(成功)
callBack && callBack(true)
} else if (curFeature.startsWith('98')) { //关锁后APP主动读取后的响应,查看是否已关锁
if (curFeature == '9801') { //关锁成功
callBack && callBack(true)
} else { //关锁失败
callBack && callBack(false)
}
} else {

}
})
},
fail(err) {
console.log('第七步启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值失败err', err);
callBack && callBack(false)
}
})
}

// ArrayBuffer转16进度字符串示例
function ab2hex(buffer) {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function (bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('')
}

//寻车指令,用于拿到开锁所需的随机码
function getRandomCode() {
let str = '7B5B01610060';
writeBLE(str)
}

//开锁指令,获取到开锁所需的数据
function getUnlockData(ciphertext) {
if (ciphertext.length > 16) { //确保寻车后蓝牙响应内容有用于开锁的随机码
//开锁头(固定值)
let headData = '7B5B1B6200';
//用户手机号
let userPhone = '018106053735';
//4B大端秒级时间戳
let timestamp = convertLettersToUpperCase(decimalToHex(getSecondsTimestamp()));
//随机码 + 8个‘0’
let randomVal = convertToLower(ciphertext.slice(16, 24)) + '00000000';
//AES加密后的前32位密文
let aesResult = aesEncrypt(randomVal,curKey).slice(0,32)
//校验码
let checkCode = '00';
//最后用于发指令的内容
let result = headData + userPhone + timestamp + aesResult + checkCode;
writeBLE(result)
} else {
getRandomCode();
}
}

//查看锁状态指令,用于验证用户手工关锁后查询是否真的已关锁
function getLockStatus() {
let str = '7B5B006868';
writeBLE(str)
}

//AES的ECB方式加密,以hex格式(转大写)输出;参数一:明文数据,参数二:秘钥
function aesEncrypt(encryptString, key) {
let aeskey = CryptoJS.enc.Utf8.parse(key);
let aesData = CryptoJS.enc.Utf8.parse(encryptString);
let encrypted = CryptoJS.AES.encrypt(aesData, aeskey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
//将base64格式转为hex格式并转换成大写
let password = encrypted.ciphertext.toString().toUpperCase()
return password;
}

//处理写入数据
function writeBLE(str) {
//如果大于20个字节则分包发送
if (str.length > 20) {
let curArr = splitString(str,20);
// #ifdef MP-WEIXIN
curArr.map(i => writeBLECharacter(hexStringToArrayBuffer(i)))
// #endif

// #ifdef MP-ALIPAY
curArr.map(i => writeBLECharacter(i))
// #endif
} else {
// #ifdef MP-WEIXIN
writeBLECharacter(hexStringToArrayBuffer(str));
// #endif

// #ifdef MP-ALIPAY
writeBLECharacter(str);
// #endif
}
}

//第八步(写入)(uni.writeBLECharacteristicValue(OBJECT),向低功耗蓝牙设备特征值中写入二进制数据。)
function writeBLECharacter(bufferValue){
uni.writeBLECharacteristicValue({
deviceId: curDeviceId,
serviceId: curServiceId,
characteristicId: curCharacteristicWrite,
value: bufferValue,
success(res) {
console.log('第八步(写入)向低功耗蓝牙设备特征值中写入二进制数据成功res', res);
},
fail(err) {
console.log('第八步(写入)向低功耗蓝牙设备特征值中写入二进制数据失败err', err);
callBack && callBack(false)
}
})
}

//将字符串以每length位分割为数组
function splitString(str, length) {
var result = [];
var index = 0;
while (index < str.length) {
result.push(str.substring(index, index + length));
index += length;
}
return result;
}

//字符转ArrayBuffer
function hexStringToArrayBuffer(str) {
// 将16进制转化为ArrayBuffer
return new Uint8Array(str.match(/[\da-f]{2}/gi).map(function(h) {
return parseInt(h, 16)
})).buffer
}

//对字符串中的英文大写转小写
function convertToLower(str) {
var result = '';
for (var i = 0; i < str.length; i++) {
if (/[a-zA-Z]/.test(str[i])) {
result += str[i].toLowerCase();
} else {
result += str[i];
}
}
return result;
}

//对字符串中的英文小写转大写
function convertLettersToUpperCase(str) {
var result = str.toUpperCase(); // 将字符串中的字母转换为大写
return result;
}

//获取秒级时间戳(十进制)
function getSecondsTimestamp() {
var timestamp = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位为秒)
return timestamp;
}

//将十进制时间戳转成十六进制
function decimalToHex(timestamp) {
var hex = timestamp.toString(16); // 将十进制时间戳转换为十六进制字符串
return hex;
}


//抛出蓝牙核心方法
module.exports = {
operateBluetoothYws
};


  • 解读:


这里的步骤和上面流程图中的步骤走向是一样的,不过里面的详情,笔者还是想每一步都拆开来对着实际案例讲述为好,详见下文(这里主要是为了照顾小白,大佬勿怪)。


六. 蓝牙模块层各步骤详解。



  1. 蓝牙功能调用核心方法的定义和导出(operateBluetoothYws)


operateBluetoothYws 这里没啥好特别的,就是将业务层传进来的参数做个中转处理,为后续步骤的api所调用,详见上文代码及其注释。



  1. 第一步(uni.startBluetoothDevicesDiscovery(OBJECT))


uni.startBluetoothDevicesDiscovery 这里主要注意的是services这个参数,这个参数会由硬件厂家提供,一般在其提供的蓝牙协议文档中会标注,作用是要搜索的蓝牙设备主 service 的 uuid 列表。某些蓝牙设备会广播自己的主 service 的 uuid。如果设置此参数,则只搜索广播包有对应 uuid 的主服务的蓝牙设备。建议主要通过该参数过滤掉周边不需要处理的其他蓝牙设备。


image.png



  1. 第二步(uni.onBluetoothDeviceFound(CALLBACK))


uni.onBluetoothDeviceFound 这一步用来确定目标设备id,即后续步骤所需的参数deviceId。 这里主要注意的是其回调函数的devices结果,我们要根据厂家或其提供的蓝牙对接协议规定和我们业务层传进来的mac来匹配筛选目标设备(因为这里会监听到第一步同样的uuid的每一台设备)(这里我就一台设备测试,所以回调函数的devices结果数组中内容就一个;然后之所以用localName.includes(curMac) 来匹配目标设备,这是根据厂商协议文档来做的,每家厂商和每种设备不一样,这里要按实际情况处理,不过万变不离其宗)。


image.png



  1. 第三步(uni.createBLEConnection(OBJECT))


uni.createBLEConnection 这里没啥特别的,主要就是用到第二步中得到的deviceId去连接低功耗蓝牙目标设备。需要注意的是这里支付宝小程序的API不一致,为my.connectBLEDevice


image.png



  1. 第四步(uni.stopBluetoothDevicesDiscovery(OBJECT))


uni.stopBluetoothDevicesDiscovery 这一步主要是为了节省电量和资源,在第三步连接目标设备成功后给停止搜寻附近的蓝牙外围设备。


image.png



  1. 第五步(uni.getBLEDeviceServices(OBJECT))


uni.getBLEDeviceServices 这里通过第二步中得到的deviceId用来获取蓝牙目标设备的所有服务并确定后续步骤所需用的蓝牙服务uuid(serviceId)。这里取res.services中的哪个,这是硬件厂商定好的,不同厂商不同,具体看对接协议(案例中的是固定放在第2个,所以是通过curServiceId = res.services[1].uuid得到)。


image.png



  1. 第六步(uni.getBLEDeviceCharacteristics(OBJECT))


uni.getBLEDeviceCharacteristics 这里通过第二步获取的目标设备IddeviceId和第五步获取的蓝牙服务IdserviceId来得到目标设备的写的uuid读的uuid。这里取characteristics的哪一个也是要根据厂商和其提供的蓝牙协议文档来决定的(案例以笔者这的协议文档为主,所以是这样获取的:curCharacteristicWrite = res.characteristics.filter(item => item && item.uuid.includes('0002'))[0].uuid 和 curCharacteristicRead = res.characteristics.filter(item => item && item.uuid.includes('0003'))[0].uuid)。需要注意的是这里支付宝小程序的API不一致,为my.getBLEDeviceCharacteristics,其res返回值也不一样,curCharacteristicWrite = res.characteristics.filter(item => item && item.characteristicId.includes('0002'))[0].characteristicId 和 curCharacteristicRead = res.characteristics.filter(item => item && item.characteristicId.includes('0003'))[0].characteristicId。


image.png



  1. 第七步(uni.notifyBLECharacteristicValueChange(OBJECT))


uni.notifyBLECharacteristicValueChange 这里就是开启低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值。可以在其的success内执行一些写入操作执行uni.onBLECharacteristicValueChange(CALLBACK)来监听低功耗蓝牙设备的特征值变化事件了。


image.png



  1. 第八步(写入)(uni.writeBLECharacteristicValue(OBJECT))


uni.writeBLECharacteristicValue 这里特别要注意的是参数value必须为二进制值(这里需用注意的是支付宝小程序的参数value可以不为二进制值,可直接传入,详见支付宝小程序开发文档);并且单次写入不得超过20字节,超过了需分段写入


image.png


image.png



  1. 第八步(监听)(uni.onBLECharacteristicValueChange(CALLBACK))


uni.onBLECharacteristicValueChange 这里需根据实际开发的业务场景对CALLBACK 返回参数转16进度字符串后自行处理(支付宝小程序如果写入时未转换,那么这里读取时也不需要转换)(本文以寻车--开锁--检测锁状态为例)。


image.png


七. 总结。


以上就是本文的所有内容,主要分为2部分----业务层蓝牙模块层(封装)。业务层只需要关注目标设备和其对应的密钥(不同厂家和设备不同);蓝牙模块层主要是按蓝牙各API拿到以下四要素并按流程图一步步执行即可。



  1. 蓝牙设备Id:deviceId

  2. 蓝牙服务uuid:serviceId

  3. 蓝牙写操作的uuid

  4. 蓝牙读操作的uuid


至此,如何在小程序中优雅地封装蓝牙模块并高效使用就已经完结了,当然本文只是以最简而易学的案例来讲述蓝牙模块开发,大多只处理了success的后续,至于fail后续可以根据大家实际业务处理。相信看到这,你已经对小程序开发蓝牙功能,对接各种蓝牙协议已经有一定的认识了,再也不虚PM小姐姐的蓝牙需求了。完结撒花~ 码文不易,还请各位大佬三连鼓励(如发现错别之处,还请联系笔者修正)。


作者:三月暖阳
来源:juejin.cn/post/7300929241948422179
收起阅读 »

浏览器的秘密

web
 浏览器架构 1 浏览器的历史 单进程与多进程浏览器 在2007年之前,市面上浏览器都是单进程的,所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。 最新的Chrome浏览器包括:1个浏览器(B...
继续阅读 »

 

浏览器架构


1 浏览器的历史


单进程与多进程浏览器



在2007年之前,市面上浏览器都是单进程的,所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。20210415092040.png




最新的Chrome浏览器包括:1个浏览器(Browser)主进程1个 GPU 进程1个网络(NetWork)进程多个渲染进程多个插件进程
20210415092356.png




  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU进程。其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。

  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响


2 JavaScript的单线程模型



  • 因为JS是与用户互动,以及操作DOM。如果JavaScript是多线程的,会带来很多复杂的问题,假如 JavaScript有A和B两个线程,A线程在DOM节点上添加了内容,B线程删除了这个节点,应该是哪个为准呢? 所以,为了避免复杂性,所以设计成了单线程。

  • H5提供了多线程的方案:Web Worker, 他允许主线程创建worker线程,分配任务给worker进程处理,但是worker线程完全受到主线程控制,也不能操作DOM,没有改变JS的单线程本质。


3 Chrome 打开一个页面需要启动多少进程?分别有哪些进程?


打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。



  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。


4 渲染机制


1. 浏览器如何渲染网页


概述:浏览器渲染一共有五步



  1. 处理 HTML 并构建 DOM 树。

  2. 处理 CSS构建 CSSOM 树。

  3. 将 DOM 与 CSSOM 合并成一个渲染树。

  4. 根据渲染树来布局,计算每个节点的位置。

  5. 调用 GPU 绘制,合成图层,显示在屏幕上



第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染



具体如下图过程如下图所示


img


img


渲染



  • 网页生成的时候,至少会渲染一次

  • 在用户访问的过程中,还会不断重新渲染



重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)




  • 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢

  • 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且CSS也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM


知识点1



  • 由于<script>标签是阻塞解析的,将脚本放在网页尾部会加速代码渲染。

  • deferasync属性也能有助于加载外部脚本。

  • defer使得脚本会在dom完整构建之后执行;

  • async标签使得脚本只有在完全available才执行,并且是以非阻塞的方式进行的


知识点2: 重绘(Repaint)和回流(Reflow)



重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大




  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘

  • 回流是布局或者几何属性需要改变就称为回流。



回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流



以下几个动作可能会导致性能问题



  • 改变 window 大小

  • 改变字体

  • 添加或删除样式

  • 文字改变

  • 定位或者浮动

  • 盒模型


很多人不知道的是,重绘和回流其实和 Event loop 有关



  • 当 Event loop 执行完Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。

  • 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms才会触发一次,并且自带节流功能。

  • 判断是否触发了 media query

  • 更新动画并且发送事件

  • 判断是否有全屏操作事件

  • 执行 requestAnimationFrame 回调

  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好

  • 更新界面

  • 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调


常见的引起重绘的属性



  • color

  • border-style

  • visibility

  • background

  • text-decoration

  • background-image

  • background-position

  • background-repeat

  • outline-color

  • outline

  • outline-style

  • border-radius

  • outline-width

  • box-shadow

  • background-size


3.4 常见引起回流属性和方法



任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子




  • 添加或者删除可见的DOM元素;

  • 元素尺寸改变——边距、填充、边框、宽度和高度

  • 内容变化,比如用户在input框中输入文字

  • 浏览器窗口尺寸改变——resize事件发生时

  • 计算 offsetWidth 和 offsetHeight 属性

  • 设置 style 属性的值


回流影响的范围



由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种




  • 全局范围:从根节点html开始对整个渲染树进行重新布局。

  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局


全局范围回流


<body>
<div class="hello">
<h4>hello</h4>
<p><strong>Name:</strong>BDing</p>
<h5>male</h5>
<ol>
<li>coding</li>
<li>loving</li>
</ol>
</div>
</body>


p节点上发生reflow时,hellobody也会重新渲染,甚至h5ol都会收到影响



局部范围回流



用局部布局来解释这种现象:把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界



3.5 减少重绘和回流



使用 translate 替代 top



<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>


@程序员poetry: 代码已经复制到剪贴板



  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

  • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改100次,然后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量


for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}


@程序员poetry: 代码已经复制到剪贴板



  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

  • CSS选择符从右往左匹配查找,避免 DOM深度过深

  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层。


img


img


作者:vast11
来源:juejin.cn/post/7298893187065659430
收起阅读 »

前端基建有哪些?大小公司的偏重啥?🤨

web
前言 兄弟们可能有的感受 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。 感受一:天天写业务代码,感觉没啥技术含量,没沉淀,成长太慢,架构那边挺有趣的。 感受二:天天写内部工具,觉得没啥提升,感觉要废。 感受三:对一些框架的...
继续阅读 »

前言




兄弟们可能有的感受



  • 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。

  • 感受一:天天写业务代码,感觉没啥技术含量,没沉淀,成长太慢,架构那边挺有趣的。

  • 感受二:天天写内部工具,觉得没啥提升,感觉要废。

  • 感受三:对一些框架的原理、源码、工具 研究较少,无法突破评级,成为leader。


上面的感受都是一些兄弟们的典型感受(也包括我自己)。这时候不妨可以考虑一下,了解了解前端的基础建设,进而 搭建起一个坚实的底座和让自己得到一个提升




正文开始——关于“基建”




1.什么是基建?



  • “技术基建”,就是研发团队的技术基础设施建设,一个团队通用技术能力的沉淀。

  • 小到文档规范,脚手架工具,大到工程化、各个领域工具链,凡是能促进业务效率、沟通成本都可以称作基建。

  • 网上看到的一句话,说的很好, “业务支撑是活在当下,技术基建是活好未来”




2.基建的意义


主要是为了以下几点:



  • 业务复用,提高效率: 基建可以提高单个人的工作产出和工作效率,可以从代码层面解决一些普遍性和常用性的业务问题

  • 规范、优化流程制度: 优异的流程制度必将带来正面的、积极的、有实效的业务支撑。

  • 更好面对未来业务发展: ,像建房子一样,好的地基可以建出万丈高楼。

  • 影响力建设、开源建设:建设结果对于业务的促进,更容易获得内部合作方的认可;沉淀下来的好的经验,可以对外输出分享,也是对影响力的有力帮助。




基建搞什么




1.核心:


下手之前首先得记住总结出的核心概念:



  • 三个落地要素: 公司的团队规模、公司的业务、团队水平。

  • 四大基础特性: 技术的健全性、基建的稳定性、研发的效率性、业务的体验性


根据结合落地和基础特性,来搭建不同"重量"和"复杂度"的基建系统。(毕竟每个公司的情况都不同)




2.方向


基建开始之前,首先得确定建设的策略及步骤,主要是从 拆解研发流程 入手的:


一个基本的研发流程闭环一般是:需求导入 => 需求拆解 => 技术方案制定 => 本地编码 => 联调 => 自测优化 => 提测修复 Bug => 打包 => 部署 => 数据收集&分析复盘 => 迭代优化 。


在研发流程闭环中每一个环节的阻塞点越少,研发效率就越高。基建,就是从这些耽误研发时间的阻塞点入手,按照普遍性 + 高频的优先级标准,挨个突破。




3.搞什么


通用的公式是: 标准化 + 规范化 + 工具化 + 自动化 ,能力完备后可以进一步提升到平台化 + 产品化。在方向上,主要是从下面的 8 个主要方向进行归类和建设,供大家参考:



  • 开发规范:这一部分沉淀的是团队的标准化共识,标准化是团队有效协作的必备前提。

  • 研发流程: 标准化流程直接影响上下游的协作分工和效率,优秀的流程能带来更专业的协作。

  • 工程管理: 面向应用全生命周期的低成本管控,从应用的创建到本地环境配置到低代码搭建到打包部署。

  • 性能体验: 自动化工具化的方式发现页面性能瓶颈,提供优化建议。

  • 安全防控: 三方包依赖安全、代码合规性检查、安全风险检测等防控机制。

  • 统计监控: 埋点方案、数据采集、数据分析、线上异常监控等。

  • 质量保障: 自测 CheckList、单测、UI 自动化测试、链路自动化测试等。


如上是一般性前端基建的主要方向和分区,不论是 PC 端还是移动端,这些都是基础的建设点。业务阶段、团队能力的差异,体现在基建上,在于产出的完整性颗粒度深入度自动化的覆盖范围。




4.大小公司基建重点


小团队的现实问题:考虑到现实,毕竟大多数前端团队不像大厂那样有丰富的团队人员配置,大多数还是很小的团队,小团队在实施基建时就不可避免的遇到很现实的阻力:



  • 最大的阻力应该就是 受限于团队规模小 ,无法投入较多精力处理作用于直接业务以外的事情

  • 其次应该是团队内部 对于基建的必要性和积极性认识不够 (够用就行的思想)


大小公司基建重点:




  • 小公司: 针对一些小团队或者说偏初创期的团队,其建设,往往越偏向于基础的技术收益,如脚手架组件库打包部署工具等;优先级应该排好,推荐初创公司和小团队成立优先搭建好:规范文档、统一开发环境技术栈/方法/工具、项目模板、CI/CD流程 ,把基础的闭环优先搭建起来。




  • 大公司: 越是成熟的业务和成熟沉淀的团队,其建设会越偏向于获取更多的业务收益,如直接服务于业务的系统,技术提效的同时更能直接带来业务收益。搭建起一套坚实的项目底座,能够更好的支持上层建筑的发展,同时也能够提升团队的成长,打开在业界的知名度,获取更好的信任支持。大公司在基础建设上,会更加考虑数据一些监控以及数据的埋点分析和统计,更加的偏重于数据的安全防范,做到质量保证。对于这点,很多前端需要写许多的测试case,有些人感觉很折磨,哈哈哈哈哈哈。






基建怎么搞




下面,会针对一些大家都感兴趣的方向,结合我们团队过去部分的建设产出,为大家列举一些前端基建类的沉淀,以供参考。


1. 规范&文档


规范和文档是最应该先行的,规范意味着标准,是团队的共识,是沟通协作的基础。


文档:



  • 新人文档(公司、业务、团队、流程等)

  • 技术文档、

  • 业务文档、

  • 项目文档(旧的、新的)

  • 计划文档(月度、季度、年度)

  • 技术分享交流会文档


规范:



  • 项目目录规范:比如api,组件,页面,路由,hooks,store等



  • 代码书写规范:组件结构、接口(定义好参数类型和响应数据类型)、事件、工具约束代码规范、代码规范、git提交规范




2. 脚手架


开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。


省事的可能直接拥抱框架选型对应的全家桶,如 Vue 全家桶,或者用 Webpack 撸一个脚手架。能力多一些的会再为脚手架提供一些插件服务,如 Lint 或者 Mock。从简单的一个本地脚手架,到复杂的一个工程化套件系统。




3. 组件


公司项目多了会有很多公共的组件,可以抽离出来,方便自身和其他项目复用,一般可以分为以下几种组件:



  • UI组件:antd、element、vant、uview...

  • 业务组件:表单、表格、搜索...

  • 功能组件:上拉刷新,滚动到底部加载更多,虚拟滚动,拖拽排序,图片懒加载..




4. 工具 / 函数库


前端工具库,如 axios、loadsh、Day.js、moment.js、big.js 等等(太多太多,记不得了)


常见的 方法 / API封装:query参数解析、device设备解析、环境区分、localStorage封装、Day日期格式封装、Thousands千分位格式化、防抖、节流、数组去重、数组扁平化、排序、判断类型等常用的方法hooks抽离出来组成函数库,方便在各个项目中使用




5. 模板


可以提前根据公司的业务需求,封装出各个端对应通用开发模版,封装好项目目录结构,接口请求,状态管理,代码规范,git规范钩子,页面适配,权限,本地存储管理等等,来减少开发新项目时前期准备工作时间,也能更好的统一公司整体的代码规范。



  1. 通用后台管理系统基础模版封装

  2. 通用小程序基础模版封装

  3. 通用h5端基础模版封装

  4. 通用node端基础模版封装

  5. 其他类型的项目默认模版封装,减少重复工作。




6. API管理 / BFF


推荐直接使用axios封装或fetch,项目中基于次做二次封装,只关注和项目有关的逻辑,不关注请求的实现逻辑。在请求异常的时候不返回 Promise.reject() ,而是返回一个对象,只是code改为异常状态的 code,这样在页面中使用时,不用用 try/catch 包裹,只用 if 判断 code 是否正确就可以。再在规定的目录结构、固定的格式导出和导入。


BFF(Backends For Frontends)主要将后端复杂的微服务,聚合成对各种不同用户端(无线/Web/H5/第三方等)友好和统一的API;




7. CI/CD 构建部署


前端具备自己的构建部署系统,便于专业化方面更好的流程控制。很多公司目前,都实现了云打包、云检测和自动化部署,每次 git commit 代码后,都会自动的为你部署项目至 测试环境、预生产环境、生产环境,不用你每次手动的去打包后 cv 到多个服务器和环境。开发新的独立系统之初,也会希望能实现一种 Flow 的流式机制,以便实现代码的合规性静态检测能力。如果可以的话,可以去实现了一套插件化机制,可以按需配置不同的检测项,如某检测项检测不通过,最终会阻塞发布流程。




8. 数据埋点与分析


前端团队可以做的是 Web 数据埋点收集和数据分析、可视化相关的全系统建设。可实现埋点规范、埋点 SDK、数据收集及分析、PV/UV、链路分析、转化分析、用户画像、可视化热图、坑位粒度数据透出等数据化能力,下面给大家细分一些这些数据:



  • 行为数据:时间、地点、人物、交互、交互的内容;

  • 质量数据:浏览器加载情况、错误异常等;

  • 环境数据:浏览器相关的元数据以及地理、运营商等;

  • 运营数据:PV、UV、转化率、留存率(很直观的数据);




9.微前端


将您的大型前端应用拆分为多个小型前端应用,这样每个小型前端应用都有自己的仓库,可以专注于单一的某个功能;也可再聚合成有各个应用组成的一个平台,而各个应用使用的技术栈可以不同,也就是可以将不同技术栈的项目给整合到一块。这点就很不错,在如今电子办公化如此细致的时代,可能许多公司工作中都不止一个平台,平台之间的切换十分的繁琐,这时候平台之间聚合的趋势想来是必然的。(个人浅显的理解)


目前成熟一点的框架有蛮多的,使用的底层思想也各有不同,目前我也在学习qiankun等框架中,期待后面能够给大家分享一篇文章,加油💪




基建之外思考




1. 从当下业务场景出发开始


很多时候我们的建设推不下去,往往不是因为人力的问题,而是 没想清楚/没有方向 。对于研发同学,我们更应该着重于当下,从方案出发找实际场景的问题,也就是从我们项目和团队目前的业务问题、人员问题,一步步出发。还有就是,我们得开这个头。没有一个作家是看小说看成的,同理技术专家也不会是通过看技术书籍养成的。在实践中也就是实际场景中学习,从来都是最快的方式。许多有价值的事从来都是从业务本身的问题出发。到头来你会发现:问题就是机会,问题就是长萝卜的坑




2.基建讲究循序渐进


业界大部分的研发团队,都不是阿里、腾讯、头条这样基础完备沉淀丰富的情况,起步期和快速爬坡期居多,建设滞后。体现在基建上,可能往往只有一个基于 Webpack 搞搞的脚手架,和一个第三方开源的 UI 组件库上封装下自己的业务组件库,除此之外无他。如果兄弟们现在恰好是我说的这种情况,不用焦虑,很多前端也是一样的情况。只要我们一步步建设,慢慢落地基础设施,就一定会取得好的反馈




3. 技术的价值,在于解决业务问题,并且匹配


技术的价值,在于解决业务问题;人的身价,在于解决问题的能力


基建的内容我认为首先是 和业务阶段相匹配 的。不同团队服务的业务阶段不同,基建的内容和广深度也会不同。高下之分不在于多寡,而在于对业务的理解和支持程度。


“业务支撑” 和 “基础建设” 都是同一件事的两个面,这个 “同一件事”,就是帮助业务解决问题。任何脱离解决实际场景而发起的基建,都需要重新审视甚至不应被鼓励。如果时间成本没有那么多的话,建议先搭建好基本的建设底座,想要更好的闭环的想法还是先搁置一下。




4.个人不足


总结了这么多,结果发现自己对于一些知识点还是了解的太浅显了,自身在那些方面能分享的还是不多,也看了一些文章,只能描出个大概,实在是有点不好意思。但回头想想,这何尝也不算个勉励自己的方法,能够鞭策自己。后续,在我学习深入一些基建方面的知识后,会再出一些文章分享给大家,希望能够帮助到大家,共勉!!!☺(发现问题会及时补充)




落尾




大家好,我是 KAIHUA ,一个来自阿卡林省目前在深圳前端区Frank + ikun


从这周开始,我想试试每一两周复盘一次,总结出至少一个知识点,目的是尽快给自己的反馈,将自己产品一样快速迭代上升,希望可以坚持✊。


如果有什么相关错误,望大家指正,感谢感谢!!!(还在学习中,嘿嘿🤭)


下一篇文章应该会是关于 前端思考 方面的,希望早一点归纳出,和大家沟通交流...


各位 彦祖 / 祖贤,fan yin (欢迎) 关注点赞收藏,将泼天的富贵带点给我😭


一起加油!!! giao~~~🐵🙈🙉


作者:KAIHUA
来源:juejin.cn/post/7301150860825133110
收起阅读 »

用剥蒜的方式去学习闭包,原来这么简单!!!

web
对于很多学习js的码友们来说,闭包无疑是最难啃的模块之一,今天我们就用剥蒜的方式一层一层的剥开它。 我们先用一个案例来引入它 大多数肯定会觉得它输出的结果是0到9,那么大多数人都错了,它输出的结果是十个10 那么要怎么让它输出0到9呢?这里我们要先引入一个新...
继续阅读 »

对于很多学习js的码友们来说,闭包无疑是最难啃的模块之一,今天我们就用剥蒜的方式一层一层的剥开它。


我们先用一个案例来引入它



大多数肯定会觉得它输出的结果是0到9,那么大多数人都错了,它输出的结果是十个10
那么要怎么让它输出0到9呢?这里我们要先引入一个新的东西,叫调用栈


调用栈


调用栈是用来管理函数调用关系的一种数据结构


在代码预编译的时候就会产生一个生成一个调用栈,它会先去找到代码中的全局变量、函数声明,形成一个变量环境,这个变量环境和词法环境(这里我们不去探讨)一起放在全局执行上下文中。然后再从上到下去执行代码,每调用一个函数,就会生成一个该函数的执行上下文,然后入栈,函数执行的时候先去它的词法环境去找对应的变量名,找不到再去变量环境中找,再找不到就去它的下一级去寻找直到找到为止,然后函数执行完成后再将其执行上下文销毁,避免栈溢出,我们用一个代码来举例:


iwEcAqNwbmcDAQTRB4AF0QQ4BrAXVDtt0dImQAU9UGjZw-8AB9IZAt15CAAJomltCgAL0gAC84A.png_720x720q90.jpg


执行函数add时,先去add的执行上下文中寻找b,b在add的变量环境中,但是并没有a,于是再去全局执行上下文中按照词法环境和变量环境的顺序去找,找到了a,最终返回a+b=12。


作用域链


调用栈在生成执行上下文时会默认在变量环境中产生一个outer,它指向该函数的外层作用域,函数声明在哪里,哪里就是函数的外层作用域,然后形成一个作用域链。


我们再来看下一个案例



调用foo的时候生成了foo的执行上下文,foo的函数体中有bar的调用,所以又生成了一个bar的执行上下文,bar声明在最外面,所以它的outer指向全局执行上下文,因此当bar在寻找myName这个变量的时候直接跳过foo去了全局执行上下文,所以最终输出的结果是万总


iwEcAqNwbmcDAQTRB4AF0QQ4BrDMeEmBKzv5FAU9V3BEkJoAB9IZAt15CAAJomltCgAL0gADbJY.png_720x720q90.jpg


闭包


了解完调用栈和作用域链之后,就可以进入我们今天的主题闭包了,还是用一个案例来说明



函数a的函数体中声明了一个函数b,并且函数a的结果是返回了函数b


var c = a() 先调用a,并且把a的返回值赋给c,因此c就是一个函数,然后再调用c,这就是整个的执行过程。在调用完a后,a的函数体已经全部执行完毕,应该被销毁,但是在调用c的时候(c就是函数b),需要用到a中的变量,因此在销毁掉a的执行上下文的同时会分出一个区域用来存储b中所需要用到a的变量,这个存储了count的地方就叫做闭包。


iwEcAqNwbmcDAQTRB4AF0QQ4BrCKgRsdcT9JIgU9Y1plkyAAB9IZAt15CAAJomltCgAL0gACzhQ.png_720x720q90.jpg


因此闭包的概念就是:


即使外部函数已经执行完毕,但是内部函数引用了外部函数中的变量依然会保存在内存中,我们把这些变量的集合,叫做闭包


现在我们再回到第一个问题,如何让它输出0到9,很显然,就是在for的内部形成一个闭包,让i每次可以叠加存在内存中,因此代码如下:



这样一层一层把从外到内的去了解闭包,是不是就更容易了呢,你学会了吗?


作者:欣之所向
来源:juejin.cn/post/7300063572074201125
收起阅读 »

前端小练:kiss小动画分享

web
最近的学习中,哈士奇学习了简单的动画的平移和旋转的用法,同时对于z-index有了一部分了解,那么这次就通过学习写了个kiss动画 人狠话不多,哈士奇给大家献上代码先 <!DOCTYPE html> <html lang="en"> ...
继续阅读 »

最近的学习中,哈士奇学习了简单的动画的平移和旋转的用法,同时对于z-index有了一部分了解,那么这次就通过学习写了个kiss动画


image.png


人狠话不多,哈士奇给大家献上代码先


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="container">
<div class="ball" id="l-ball">
<div class="face face-l">
<div class="eye-l eye-ll"></div>
<div class="eye-l eye-lr"></div>
<div class="mouth"></div>
</div>
</div>
<div class="ball" id="r-ball">
<div class="face face-r">
<div class="eye-r eye-rl"></div>
<div class="eye-r eye-rr"></div>
<div class="mouth-r"></div>
<div class="kiss-r">
<div class="kiss"></div>
<div class="kiss"></div>
</div>
</div>
</div>
</div>
</body>
</html>

body{
background-color:#78e08f;
margin: 0;
padding: 0px;
}
.container{
width: 232px;
height: 200px;

position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.ball{
width: 100px;
height: 100px;
border: 6px solid #000;
border-radius: 50%;
background-color: white;
position: relative;
display: inline-block;/*令块元素变为具有行内块元素的特点表现为可以使得div处于同一行*/
}
.face-l{
width: 70px;
height: 30px;
position: absolute;
right: 0px;
top: 30px;
}
.face-r{
width: 70px;
height: 30px;
position: absolute;
top: 30px;
}
.eye-l{
width: 15px;
height: 14px;
border-bottom:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-r{
width: 15px;
height: 14px;
border-top:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-ll{
left: 10px;
}
.eye-lr{
right: 5px;
}
.eye-rl{
left: 10px;
}
.eye-rr{
right: 5px;
}
.mouth{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
}
.face-l::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
right: -8px;
top: 20px;
}
.face-l::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
left:-5px;
top: 20px;

}
.face-r::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius:100%;
background-color: red;
position: absolute;
right: 3px;
top: 20px;
}
.face-r::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
background-color: red;
position: absolute;
top: 20px;
}
#l-ball{
z-index: 2;/*设置元素层级,使得左边的小人可以覆盖在右边的小人脸上*/
animation: close-l 4s ease infinite;/*平移4s*/
}
@keyframes close-l{
0%{
transform: translate(0);
}
20%{
transform: translate(20px);
}
35%{
transform: translate(20px);
}
55%{
transform: translate(0);
}
100%{
transform: translate(0);
}

}
.face-l{
animation:face-l 4s ease infinite;
}
@keyframes face-l{
0%{
transform: translate(0) rotate(0);/*translate 平移 rotate旋转*/
}
10%{
transform: translate(0) rotate(0);
}
20%{
transform: translate(5px) rotate(2deg);
}
28%{
transform: translate(0) rotate(0);
}
35%{
transform: translate(5px) rotate(2deg);/*需要写清楚像素,否则没有动画*/
}
50%{
transform: translate(0) rotate(0);
}
100%{
transform: translate(0) rotate(0);
}
}
.kiss-r{
margin: 0 auto;
position: absolute;
left: 35px;
right: 0;
bottom: -5px;
opacity: 0;
animation: kiss-r 4s ease infinite;
}
.kiss{
width: 13px;
height: 10px;
border-radius: 50%;
border-left: 5px solid #000;
display: block;
}
.mouth-r{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
animation: mouth-r 4s ease infinite;
}
#r-ball{
animation: close-r 4s ease infinite;
}
@keyframes close-r{
0%{
transform: translate(0);
}
50%{
transform: translate(0);
}
70%{
transform: translate(-45px)rotate(15deg);
}
85%{
transform: translate(-45px)rotate(-10deg);
}
100%{
transform: translate(0);
}

}
@keyframes mouth-r{
0%{
opacity: 1;
}
50%{
opacity: 1;
}
50.5%{
opacity: 0;
}
/*70%{
opacity: 1;
}*/

84.9%{
opacity: 0;
}
85%{
opacity: 1;
}
100%{
opacity: 1;
}
}
@keyframes kiss-r{
0%{
opacity: 0;
}
50%{
opacity: 0;
}
50.5%{
opacity: 1;
}
84.9%{
opacity: 1;
}
85%{
opacity: 0;
}
100%{
opacity: 0;
}
}

接下来哈士奇为大家依次聊聊这段代码
首先是html的部分:


html主要是使用div对整个页面做出一个布局,哈士奇此次的小人主要是小人的两张脸和五官,因此我们在html的代码创建的过程中需要留出脸 眼睛 嘴巴的部分进行后面的css代码的操作。


在这里有些同学可能会问到,这里的kiss和mouth怎么回事,稍后我们就知道了!


那么再给大家讲讲css的部分:


首先我们通过整个页面的设置,将整个页面背景设置,也就是body部分,去除之前的默认值。


body{
background-color:#78e08f;
margin: 0;
padding: 0px;
}

接下来就是容器container的设置,我们的设置一个大容器用于放下两个小人,通过position中的absolute对于父容器(此处的是body)进行定位,使用translate函数将容器移到页面的正中心的位置


.container{
width: 232px;
height: 200px;

position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}

将大致的位置确定好了以后,我们就可以开始对于两个kiss小人进行操作了


首先确定两个小球的设置


.ball{
width: 100px;
height: 100px;
border: 6px solid #000;
border-radius: 50%;
background-color: white;
position: relative;
display: inline-block;/*令块元素变为具有行内块元素的特点表现为可以使得div处于同一行*/
}

通过border将外形线条确定,这样一来就可以制造出小人外面一圈的线,通过border-radius确定弧度,最后通过relative的相对定位,针对元素的原本位置进行定位。那么为什么要使用display呢?我们都知道,inlie-block可以使得块级元素div转化为具有行内块的特点的元素,因此div中的两个ball小球就能处于同一行了


确定两个小球的位置以后我们开始确定小球的脸


.face-l{
width: 70px;
height: 30px;
position: absolute;
right: 0px;
top: 30px;
}
.face-r{
width: 70px;
height: 30px;
position: absolute;
top: 30px;
}

通过左右两脸的设置确定他们相对于他们父容器l-ball 和r-ball的位置


接下来设置眼睛的相同元素的设置


.eye-l{
width: 15px;
height: 14px;
border-bottom:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-r{
width: 15px;
height: 14px;
border-top:5px solid #000;
border-radius: 50%;
position: absolute;
}

对于我们来说,这里的两个眼睛其实就是两个弧线,所以我们只需要确定两根线,然后使用boder-radius进行弯曲,就能把眼睛制造出来了,再通过absolute对于自己的父容器进行定位


.eye-ll{
left: 10px;
}
.eye-lr{
right: 5px;
}
.eye-rl{
left: 10px;
}
.eye-rr{
right: 5px;
}

微调设置眼睛的具体位置


再进行嘴巴的设计


.mouth{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
}

也是进行一个弯曲的曲线的设计


接下来难度要升级了,两个脸颊红红的部分应该如何实现呢?
这里我们使用到了伪元素进行创建


.face-l::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
right: -8px;
top: 20px;
}
.face-l::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
left:-5px;
top: 20px;

}
.face-r::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius:100%;
background-color: red;
position: absolute;
right: 3px;
top: 20px;
}
.face-r::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
background-color: red;
position: absolute;
top: 20px;
}

给大家复习一下伪元素的用法,在哈士奇的这个代码中,before和after分别是对于父容器的第一个子元素进行操作,也就是face里面的左眼睛进行操作,针对左眼定位脸颊的位置(记住哦,如果没有给出伪元素的定位,也就是父容器的话,是无法显示伪元素的


这样一来,我们的脸颊也做好了
最后就是属于亲脸颊时的嘴巴部分


.kiss-r{
margin: 0 auto;
position: absolute;
left: 35px;
right: 0;
bottom: -5px;
opacity: 0;
animation: kiss-r 4s ease infinite;
}
.kiss{
width: 13px;
height: 10px;
border-radius: 50%;
border-left: 5px solid #000;
display: block;
}
.mouth-r{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
animation: mouth-r 4s ease infinite;
}

哈士奇在设计中,想要右边的小球能够在最后亲到左边小球,那么光移动嘴巴是不行的,还需要让嘴巴变形成为嘟嘴的样子,因此哈士奇在mouth-r中设置了嘴巴的样式,接下来又在kiss中设置了嘴巴转变后的样式。这就是为什么右边的小球要设置kiss和mouth的原因了!


在kiss-r中大家看到opacity,如果opacity:0; 那么代表着这个块是隐藏状态,如果opacity:1; 那么就是显示的状态


最后就是动画设置的部分了,有些小伙伴已经看出来了哈士奇已经写过的
animation: mouth-r 4s ease infinite;


那么在这给大家讲讲这是个啥意思


animation(声明动画): mouth-r(动画的名字) 4s(时间) ease(规定慢速开始,然后变快,然后慢速结束的过渡效果) infinite(永久执行:动画会循环播放)


先聊聊左脸的动画,哈士奇希望它平移过去,然后做出小鸟依人的蹭一蹭的动作,于是就有了


#l-ball{
z-index: 2;/*设置元素层级,使得左边的小人可以覆盖在右边的小人脸上*/
animation: close-l 4s ease infinite;/*平移4s*/
}
@keyframes close-l{
0%{
transform: translate(0);
}
20%{
transform: translate(20px);
}
35%{
transform: translate(20px);
}
55%{
transform: translate(0);
}
100%{
transform: translate(0);
}

}
.face-l{
animation:face-l 4s ease infinite;
}
@keyframes face-l{
0%{
transform: translate(0) rotate(0);/*translate 平移 rotate旋转*/
}
10%{
transform: translate(0) rotate(0);
}
20%{
transform: translate(5px) rotate(2deg);
}
28%{
transform: translate(0) rotate(0);
}
35%{
transform: translate(5px) rotate(2deg);/*需要写清楚像素,否则没有动画*/
}
50%{
transform: translate(0) rotate(0);
}
100%{
transform: translate(0) rotate(0);
}
}

首先就是在元素中声明需要准备动画,然后在下方使用@keyframes 动画名 写出一个动画的具体内容(我们需要写出什么时候动画要做什么)


比如4s中0%的时候我希望动画开始平移,就写transform:translate()写出平移的位置是多少像素,那么在下一个%出现前,浏览器就会执行你的操作,表示在%~%之间执行动画的操作


rotate()则是进行旋转,使用以后动画将会根据一定的比例进行旋转


最后就是右脸小球的亲亲操作了


#r-ball{
animation: close-r 4s ease infinite;
}
@keyframes close-r{
0%{
transform: translate(0);
}
50%{
transform: translate(0);
}
70%{
transform: translate(-45px)rotate(15deg);
}
85%{
transform: translate(-45px)rotate(-10deg);
}
100%{
transform: translate(0);
}

}
@keyframes mouth-r{
0%{
opacity: 1;
}
50%{
opacity: 1;
}
50.5%{
opacity: 0;
}
/*70%{
opacity: 1;
}*/

84.9%{
opacity: 0;
}
85%{
opacity: 1;
}
100%{
opacity: 1;
}
}
@keyframes kiss-r{
0%{
opacity: 0;
}
50%{
opacity: 0;
}
50.5%{
opacity: 1;
}
84.9%{
opacity: 1;
}
85%{
opacity: 0;
}
100%{
opacity: 0;
}
}

前面还是进行平移操作,到了后面,小球需要进行亲亲,那么哈士奇通过opacity实时操作嘴巴的出现时间,最后在亲的前面的时间把嘟嘴展现出来。


你学会了吗?快去给你的女友写个亲亲动画吧!!


总结与联想


总结


今天哈士奇给大家分享了一个前端小动画的展现,并且逐步为大家解释了一个前端小动画应该如何写出来,在这其中涉及到了transform opacity animation z-index的使用,大家可以简单上手做做哦


联想


那么动画是否还有其他的关键词呢?ease就能解决所有的平移问题吗?我们是否可以通过其他方式展示不同效果呢?


作者:疯犬丨哈士奇
来源:juejin.cn/post/7300460850010734646
收起阅读 »

周爱民:告前端同学书

web
一年前,InfoQ的编辑约请我对前端技术做了些回顾总结,说了三个方面的话题:其一,前端过去的15年大致可以怎样划分;其二,前端的现状以及面临的挑战;其三,前端会有怎样的未来。后来刊发成综述,是《技术15年》。缘于文体变动,访谈的味道十不存一,所以这里再次整理成...
继续阅读 »

一年前,InfoQ的编辑约请我对前端技术做了些回顾总结,说了三个方面的话题:其一,前端过去的15年大致可以怎样划分;其二,前端的现状以及面临的挑战;其三,前端会有怎样的未来。后来刊发成综述,是《技术15年》。缘于文体变动,访谈的味道十不存一,所以这里再次整理成文,是为《告前端同学书》。



作者:周爱民 / aimingoo


各位前端同学,就我的所知来看,每⼀个具体的技术,在其⽅向上都有着不同的标志事件,也因此有着不同的阶段划分。但是我想,如果从我们这个领域对“前端”的认识来观察这件事,⼤概会对不同的前端阶段能有更清晰的认识。


早期前端的从业⼈员⼤多来⾃后端开发者、应⽤软件开发者,或者⽹⻚设计师,⽽并没有专职的前端开发。例如说阿⾥巴巴在 2010 年之前都把前端归在产品部⻔,可⻅前端⼯程师的来源和定位⼀直都很模糊。这个时代,ECMAScript 还陷在 Ed4 的泥坑中没有⾛出来,IE 浏览器带来的标准分裂还没有得到全⾯的修补,源于对这个领域的漠视,⼤⼚优势也没有体现出来,前端开发者们基本上各⾃为战,框架和中间件层出不穷⽽⼜良莠难分,开发⼯具和环境却荒草凄凄以⾄于乏善可陈。但是也正是在这个时代,ES6、CSS3、HTML5 等等都在筑基、渗透与蓄势。


随着专⽤⼯具链和开发流程的成熟,前后端分离的运动从项⽬内部开始蔓延到整个领域,出现了专⻔的前端开发⼯程师、团队,以及随之⽽来的⻆⾊细分,很多独⽴的技术社区就是在这个时代出现的。前后端分离不仅仅是⼀种技术表现,更是⼀种⾏业协作的模式与规范,并且反过来推动了⼯具和框架的⼤发展。信⼼满满的前端不拘于⼀城⼀地,⼀⽅⾯向前、向专业领域推进,从⽽影响到交互与接触层。因此更丰富的界⾯表现,以及从移动设备到⼈机交互界⾯等领域都成了前端的研究⽅向,是所谓“⼤前端”。⽽另⼀⽅⾯则向后、向系统领域渗透,有了所谓⼯程师“全栈化”运动。这个时候的“全栈”,在⼯程上正好符合敏捷团队的需求,在实践上正好⼜叠加上DevOPS、云端开发和⼩应⽤的⼏阵助⼒,前端因此⼀⽚繁华景象。


所以 2008 年左右开始的前后端分离是整个前端第⼆阶段的起点,这场运动改变了软件开发的体系与格局,为随后⼗年的前端成熟期拓开了局⾯。那⼀年的 SD2C 我谈了《VCL 已死、RAD 已死》,⽽⼗年后阿⾥的圆⼼在GMTC 上讲了《前端路上的思考》,可算作对这个时代的预⾔和反思。


相对于之前所说的第⼀、第⼆阶段,我认为如今我们正⾏进在⼀个全新阶段中。这个阶段初起的主要表现是:前端分离为独⽴领域,并向前、后两个⽅向并进之举已然势微。其关键在于,前端这个领域中的内容已经逐渐复杂,⽽其应⽤的体量也将愈加庞⼤,因此再向任何⽅向发展都难尽全⼒、难得全功。


摊⼦铺得⼤了,就需要再分家。所以下⼀个阶段中,将再次发⽣横向的领域分层,⼀些弥合层间差异的技术、⽅法与⼯具也将出现,类似于 Babel 这样的“嵌缝膏”产品将会再次成为⼀时热⻔。但⻓期来说,领域分层带来的是更专精的职业与技能,跨域协作是规约性的、流程化的,以及⼯具适配的。从 ECMAScript 的实践来看,规范的快速更新和迭代已经成为现实,因此围绕规范与接⼝的新的开发技术与⼯程模型,将会在这个阶段中成为主要⼒量,并成为维持系统稳定性的主要⼿段。


这是在⼀个新阶段的前夜。故此,有很多信息并不那么明朗,⽐如说像前后端分离这样的标志性事件并没有出现,亦或者出现了也还没有形成典型影响。我倾向于认为引领新时代的,或者说开启下⼀个阶段的运动将会发⽣在交互领域,也就是说新的交互⽅式决定了前端的未来。之前⾏业⾥在讲的 VR 和 AR(虚拟现实和增强实境)是在这个⽅向上的典型技术,但不唯于此。⼏乎所有在交互⽅式上的变⾰,都会成为⼈们认识与改变这个世界的全新动⼒,像语⾳识别、视觉捕捉、脑机接⼝等等,这些半成熟的或者实验性的技术都在影响着我们对“交互”的理解,从⽽也重新定义了前端。


⾏业⽣态也会重构,如同今天的前端⼤会已经从“XX技术⼤会”中分离出来⼀样,不久之后“交互”也会从前端分化出来,设计、组件化、框架与平台等等也会成体系地分化出来。前端会变得⽐后端更复杂、更多元,以及更加的⽣机勃勃。这样的⽣态起来了,⼀个新的时代也就来临了。简单地说,1、要注重领域与规范,2、要跟进交互与体验,3、要在⽣态中看到机会。


然而,前端的同学们,我们也不要忘记在这背景中回望自身,正视我们前端自己的问题。


其⼀,底⼦还是薄,前端在技术团队与社区的积累上仍然不够。看起来摊⼦是铺开了,但是每每只在“如何应⽤”上下功夫,真正在⽹络、系统、语⾔、编译、机器学习等等⽅⾯有深⼊研究的并不多。⼀直以来,真正有创建性或预⻅性的思想、⽅法与理论鲜⻅于前端,根底薄是⾸要原因。


其⼆,思维转换慢,有些技术与思想抛弃得不够快,不够彻底。不能总是把核⼼放在“三⼤件(JS+CSS+HTML)”上⾯,核⼼要是不变,前端的⾰命也就不会真正开始。要把“Web 前端”前⾯的“Web”去掉,就现实来说,很多⼈连“观望”都没有开始。


其三,还没有找到跟“交互”结合起来的有效⽅法与机制。前端过去⼗年,在 IoT、机器学习、云平台等等每⼀次潮流都卡上了点⼉,但是如果前端的下⼀次转型起于“交互”,那么我们⽬前还没有能⼒适应这样的变化。当然,契机也可能不在于“交互”,但如果这样,我们的准备就更不充分了。


其四,向更多的应⽤领域渗透的动机与动⼒不明确。⻓期以来,前端在各个领域上都只是陪跑,缺乏真正推动这些领域的动机与动⼒。往将来看,这些因素在前端也将持续缺乏。寻求让前端持续发展,甚⾄领跑某些领域的内驱⼒量,任重⽽道远。


同学们,我想我们必须有一种共同的、清醒的认识与认知:浏览器是未来。去操作系统和云化是两个⼤的⽅向,当它们达成⽬标时,浏览器将成为与⽤户接触的唯⼀渠道。研究浏览器,其本质就是研究交互和表现,是前端的“终极私活”。但不要局限于“Web 浏览器”,它必将成为历史,如同操作系统的“⽂件浏览器”⼀样。


要极其关注 JavaScript 的类型化,弱类型是这⻔语⾔在先天条件上的劣势,是它在⼤型化和系统化应⽤中的明显短板。这个问题⼀旦改善,JavaScript 将有⼒量从其它各种语⾔中汲取营养,并得以⾯向更多的开发领域,这是 JavaScript 的未来。


AI 和 WASM 在前端可以成为⻬头并进的技术,⼀个算法,⼀个实现。对于前端来说,性能问题⼀直是核⼼问题,⽽交互与表现必将“⼤型与复杂化”,例如虚拟现实交互,以及模拟反馈等等,⽽ WASM 是应对这些问题的有效⼿段。


所谓交互与表现,本质上都是“空间问题”。亦即是说,前端表现中的所谓布局、块、位置、流等等传统模式与技术,与将来的交互技术在问题上是同源的。就像“盒模型”确定了 CSS 在前端的核⼼地位⼀样,新的空间定位技术,以及与之匹配的表现与交互⽅法是值得关注和跟进的。


前端要有更强的组织⼒,才能应付更⼤规模的系统。这⾥的组织⼒主要是针对⼯程化⽽⾔,所有⼯程化⼯具,其最终的落脚点都在快速、可靠,并以体系化的⽅式来组织⼯程项⽬。这包括⼈、资源、信息、时间、能⼒与关系等等⼯程因素,每个⽅⾯都有问题,都值得投⼊技术⼒量。


相较于新入行的前端的同学们,我能从没有前端走到如今前端的⼤发展,何其幸也。以我⼀路之所⻅,前端真正让我钦佩的是持久的活⼒。前端开发者⼏乎总是⼀个团队中“新鲜⾎液”的代名词,因此前端在业界的每个阶段都⾛在时代的前列。如今看 C 语⾔的⽼迈,操作系统的封闭,后台的保守,以及业务应⽤、产品市场等等各个领域都在筑城⾃守,再看前端种种,便总觉得开放与探索的信念犹在。


曾经与我⼀道的那些早期的前端开发者们,如今有做了主管的,有搞了标准的,有带了团队的,有转了后端的,有做架构做产品做运维等等⼀肩担之,也有开了公司做了顾问从商⼊政的,但也仍然还有在前端⼀线上做着努⼒,仍看好于这⼀个⽅向并在具体事务上勉⼒前⾏的。我曾经说,“任何事情做个⼗年,总会有所成绩的”,如今看来,这个时间还是说少了,得说是:⼏个⼗年地做下去,前端总能做到第⼀。


惟只提醒⼤家,领域分层的潮流之下,层间技术的核⼼不是功能(functional),⽽是能⼒(capabilities)。向应⽤者交付能⼒,需要有体系性的思维,要看向系统的全貌。我们专精于细节没错,专注于⼀城⼀地也没错,然而眼光⾼远⽽脚踏实地,是前端朋友们当有之势。


亦是这个时代予我们的当为之事!


周爱民/aimingoo


初稿于2022.06


此稿于2023.10


作者:裕波
来源:juejin.cn/post/7290751135903236137
收起阅读 »

【微信小程序】 token 无感刷新

web
⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖 一、【实现思路】🚩 小程序端登录时,除了返回用户信息,还需返回两个 token 信息 accessToken:用于验证用户身份 refreshToken:用于刷新 a...
继续阅读 »

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖


一、【实现思路】🚩



  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息

    • accessToken:用于验证用户身份

    • refreshToken:用于刷新 accessToken



  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求

  3. 刷新成功会拿到新的 accessToken 和 refreshToken

  4. 然后使用新的 accessToken 将失败的请求重新发送


二、【流程图】🚩


图中为大致流程,为了看起来简洁直观,略去了与本文内容不相关的步骤


image.png


三、【后端代码】🚩



1. ⌈签发和验证 token⌋🍥




  • 签发 accessToken 时,设置的过期时间是4个小时

  • 签发 refreshToken 时,设置的过期时间是7天

  • 自己在测试的时候,可以把时间改短一点,例如30s

  • 正常实现的效果是:登录时间超过4个小时之后,再次发送需要身份验证的请求,会使用 refreshToken 去请求刷新 accessToken;如果距离上次登录已经超过了7天,则会提示重新登录

  • 这样的话,实现了一定的安全性(因为 accessToken 每隔4个小时就会更新),同时又没有让用户频繁地重新登录



2. ⌈登录⌋🍥




  • 拿到请求参数中的登录凭证(code),以及保存的 appId 和 appSecret

  • 基于上述三个参数发送请求到微信官方指定的服务器地址,获取 openid

  • openid 是小程序登录用户的唯一标识,每次登录时的登录凭证(code)会变,但是获取到的 openid 是不变的

  • 根据 openid 在数据库中查找用户,如果没有查找到,说明本次登录是当前用户的首次登录,需要创建一个新用户,存入数据库中

  • 然后根据用户 id 以及设置的签发密钥进行 accessToken 和 refreshToken 的签发

  • 签发密钥可以是自己随意设置的一段字符串,两个 token 要设置各自对应的签发密钥

  • 这个签发密钥,在进行 token 验证的时候会使用到


四、【前端代码】🚩



1. ⌈封装的登录方法⌋🍥




  • 在创建微信小程序项目时,默认是在根目录下 app.js 的 onLaunch 生命周期函数中进行了登录

  • 也就是说每次在小程序初始化的时候都会进行登录操作

  • 作者这里是把登录操作单独提取出来了,这样可以在个人主页界面专门设置一个登录按钮

  • 当本地存储的用户信息被清除,或者上面提到的 refereshToken 也过期的情况下,我们点击登录按钮进行登录操作


import { loginApi } from '@/api/v1/index'

const login = async () => {
try {
// 登录获取 code
const {code} = await wx.login()
// 调用后端接口,获取用户信息
const {user, accessToken, refreshToken} = await loginApi(code)
wx.setStorageSync('profile', user)
wx.setStorageSync('accessToken', `Bearer ${accessToken}`)
wx.setStorageSync('refreshToken', refreshToken)
} catch (error) {
wx.showToast({
title: '登录失败,请稍后重试',
icon: 'error',
duration: 2000
})
}
}

export default login



2. ⌈封装的请求方法⌋🍥






  • 接收5个参数:url、method、params、needToken、header(点击展开)

    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接

    • method:请求方法,默认值为 'GET'

    • params:请求参数,数据格式为 object,例如: {name: 'test'}

    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false

    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null






  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。在上面的验证 token 代码中,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息




  • 调用 wx.request 发送请求,在 success 回调函数中判断请求返回信息中的状态码,根据状态码的不同做对应的操作,这里只讨论401 token 过期的情况





  • 当 token 过期时,从本地存储中获取到 refreshToken,然后调用对应后端接口刷新 token(点击展开)

    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)

    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求

    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true






  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来




  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送





  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)

    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中





【源码】🚩



【说明】🚩



  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢


作者:Libra0809
来源:juejin.cn/post/7299357353538486291
收起阅读 »

【Taro】【微信小程序】token 无感刷新

web
⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖 一、【实现思路】🚩 小程序端登录时,除了返回用户信息,还需返回两个 token 信息 accessToken:用于验证用户身份 refreshToken:用于刷新 a...
继续阅读 »

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖


一、【实现思路】🚩



  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息

    • accessToken:用于验证用户身份

    • refreshToken:用于刷新 accessToken



  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求

  3. 刷新成功会拿到新的 accessToken 和 refreshToken

  4. 然后使用新的 accessToken 将失败的请求重新发送



👉具体实现可以翻阅作者的上一篇文章:【微信小程序】token 无感刷新



👉实现思路中的后三步,微信小程序中是在请求的 success 回调函数中做的处理,Taro 中则是设置了响应拦截器,在拦截器中做的对应处理,本文仅讨论有区别的这部分



二、【前端代码】🚩



1. ⌈封装的请求方法⌋🍥






  • 接收5个参数:url、method、params、needToken、header(点击展开)

    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接

    • method:请求方法,默认值为 'GET'

    • params:请求参数,数据格式为 object,例如: {name: 'test'}

    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false

    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null






  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。后端接口在验证 token 时,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息




  • 先通过 Taro.addInterceptor 设置拦截器,然后调用 Taro.request 发送请求。这样的话,当请求真正发送之前以及获取到响应信息时,都会先进入到拦截器中,我们就是在这里进行的 token 刷新操作





  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { baseUrl } from '../config'
    import responseInterceptor from '../http/interceptors'

    // 添加拦截器
    Taro.addInterceptor(responseInterceptor)

    // 封装的请求方法
    const request = (url, method = 'GET', params = {}, needToken = false, header = null) => {
    const {contentType = 'application/json'} = header || {}
    if (url.indexOf(baseUrl) === -1) url = baseUrl + url

    const option = {
    url,
    method,
    data: method === 'GET' ? {} : params,
    header: {'Content-Type': contentType}
    }

    // 处理 token
    if (needToken) {
    const token = Taro.getStorageSync('accessToken')

    if (token) {
    option['header']['Authorization'] = token
    } else {
    Taro.setStorageSync('profile', null)
    Taro.showToast({
    title: '请登录',
    icon: 'error',
    duration: 2000
    })

    return
    }
    }

    // 发起请求
    return Taro.request(option)
    }

    export default request





2. ⌈拦截器⌋🍥



拦截器是一个函数,接受 chain 对象作为参数。chain 对象中含有 requestParams 属性,代表请求参数。拦截器最后需要调用 chain.proceed(requestParams) 以调用下一个拦截器或者发起请求。Taro 中的这个拦截器没有请求拦截器和响应拦截器之分,具体看你是在调用 chain.proceed(requestParams) 之前还是之后做的操作。具体说明可查阅官方文档




  • 拦截器中先调用 chain.proceed(requestParams) 发送请求,其返回的是一个 promise 对象,所以可以在 .then 中做响应处理




  • .then 中先判断响应状态码,这里我们只讨论 401 token 过期的情况





  • 当 token 过期时,获取本地存储的 refreshToken,然后调用对应后端接口刷新 token(点击展开)

    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)

    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求

    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true






  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来




  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送





  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)

    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中







  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { statusCode } from '../config'
    import request from './request'

    // 标识 token 刷新状态
    let isTokenRefreshing = false

    // 存储因为等待 token 刷新而挂起的请求
    let failedRequests = []

    // 设置响应拦截器
    const responseInterceptor = chain => {
    // 先获取到本次请求的参数,后面会使用到
    let {requestParams} = chain

    // 发起请求,然后进行响应处理
    return chain.proceed(requestParams)
    .then(res => {
    switch (res.statusCode) {
    // 404
    case statusCode.NOT_FOUND:
    return Promise.reject({message: '请求资源不存在'})
    // 502
    case statusCode.BAD_GATEWAY:
    return Promise.reject({message: '服务端出现了问题'})
    // 403
    case statusCode.FORBIDDEN:
    return Promise.reject({message: '没有权限访问'})
    // 401
    case statusCode.AUTHENTICATE:
    // 获取 refreshToken 发送请求刷新 token
    // 刷新请求发送前,先判断是否有已发送的请求,如果有就挂起,如果没有就发送请求
    if (isTokenRefreshing) {
    const {url: u, method, params, header} = requestParams
    return failedRequests.push(() => request(u, method, params, true, header))
    }

    isTokenRefreshing = true
    const url = '/auth/refresh-token'
    const refreshToken = Taro.getStorageSync('refreshToken')
    return request(url, 'POST', {refreshToken}, false)
    .then(response => {
    // 刷新成功,将新的 accesToken 和 refreshToken 存储到本地
    Taro.setStorageSync('accessToken', `Bearer ${response.accessToken}`)
    Taro.setStorageSync('refreshToken', response.refreshToken)

    // 将 failedRequests 中的请求使用刷新后的 accessToken 重新发送
    failedRequests.forEach(callback => callback())
    failedRequests = []

    // 再将之前报 401 错误的请求重新发送
    const {url: u, method, params, header} = requestParams
    return request(u, method, params, true, header)
    })
    .catch(err => Promise.reject(err))
    .finally(() => {
    // 无论刷新是否成功,都需要将 isTokenRefreshing 重置为 false
    isTokenRefreshing = false
    })
    // 500
    case statusCode.SERVER_ERROR:
    // 刷新 token 失败
    if (res.data.message === 'Failed to refresh token') {
    Taro.setStorageSync('profile', null)
    Taro.showToast({
    title: '请登录',
    icon: 'error',
    duration: 2000
    })
    return Promise.reject({message: '请登录'})
    }

    // 其他问题导致失败
    return Promise.reject({message: '服务器错误'})
    // 200
    case statusCode.SUCCESS:
    return res.data
    // default
    default:
    return Promise.reject({message: ''})
    }
    })
    .catch(error => {
    console.log('网络请求异常', error, requestParams)
    return Promise.reject(error)
    })
    }

    export default responseInterceptor




【源码】🚩



【说明】🚩



  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢


作者:Libra0809
来源:juejin.cn/post/7300592516759306291
收起阅读 »

语雀又崩了?今天咱们玩点花的,手把手教你写出令人窒息的“烂代码”

web
Hello,大家好,我是Sunday。 10月23日 2023年10月23日,语雀崩溃 10 个小时,作为一款知名度极高的产品,这样的一次崩溃可以说对语雀的口碑影响极大。 不过,好在语雀的公关团队处理的还不错,没有甩锅而是及时发布公告,明确是自己的问题。同时在...
继续阅读 »

Hello,大家好,我是Sunday。


10月23日


2023年10月23日,语雀崩溃 10 个小时,作为一款知名度极高的产品,这样的一次崩溃可以说对语雀的口碑影响极大。


不过,好在语雀的公关团队处理的还不错,没有甩锅而是及时发布公告,明确是自己的问题。同时在问题解决之后,给大家 六个月的会员补偿 也可以说是诚意满满(以下为10月24日语雀团队公告)。




毕竟大家都是程序员嘛,这种事也不是不能接受。毕竟:谁还没搞崩过系统呢?😂



本以为这件事就这么过去了,哪知道昨天的一个故障,再次让语雀登上了“风口浪尖”......


11月12日


昨天下午,我在正常使用语雀记录同学学习情况的时候,突然出现了无法保存的情况。心想:“这不会是又崩了吧~~”


看了眼语雀群的微信,果然......




说实话,当时我的第一反应是:“又有瓜可以吃了~~~~~,开心😂”



反正也写不成了,坐等吃瓜就行了。正好恰逢双十一,看看买的硬盘到哪了。


结果打开淘宝才发现,这次不对劲啊,淘宝也崩了!!!




最终我们了解了事情的全貌:



本次事故是由于阿里云 OSS 的故障导致的。钉钉、咸鱼、淘宝、语雀都崩了....



从语雀的公告也体现出了这点:



公告内容如下:



尊敬的客户:您好!北京时间2023年11月12日 17:44起,阿里云监控云产品控制台访问及API调用出现出现使用异常,阿里云工程师正在紧急介入排查。非常抱歉给您的使用带来不便,若有任何问题,请随时联系我们。



可以说,语雀这次有点躺枪了(谁让你刚崩过呢~~~)。


玩点花的!教你写出令人窒息的“烂代码”


好啦,瓜吃完啦。



关于语雀崩溃的反思,网上有很多文章,我就不凑这个热闹了,想要看的同学可以自行搜索~~



“回归正题”,接下来咱们就来看看咱们的文章正题:“如何写出烂代码”。



以下共有十三条烂代码书写准则,可能并没有面面俱到,如果大家发现有一些难以忍受的烂代码习惯,也可以留言发表意见~~



第一条:打字越少越好


  // Good 👍🏻
const a = 18

// Bad 👎
const age = 18

第二条:变量/函数混合命名风格


  // Good 👍🏻
const my_name = 'Suday'
const mName = 'Sunday'
const MnAme = 'Sunday'

// Bad 👎
const myName = 'Sunday'

第三条:不要写注释


  // Good 👍🏻
const cbdr = 666

// Bad 👎
// 666ms 是根据 UX A/B 测试结果进行经验计算的。
// 具体可查看 xxxxxx
const callbackDebounceRate = 666

第四条:使用母语写注释


  // Good 👍🏻
// 666 мс было эмпірычна вылічана на аснове вынікаў UX A/B.
const callbackDebounceRate = 666

// Bad 👎
// 666ms 是根据 UX A/B 测试结果进行经验计算的。
// 具体可查看 xxxxxx
const callbackDebounceRate = 666

第五条:尽可能混合不同的格式


  // Good 👍🏻
const n = 'Sunday';
const a = "18"
const g = "MAN"

// Bad 👎
const name = 'sunday'
const age = '18'
const gender = 'man'

第六条:尽可能把代码写成一行


  // Good 👍🏻
document.location.search.replace(/(^\?)/, '').split('&').reduce(function (o, n) { n = n.split('=') })

// Bad 👎
document.location.search
.replace(/(^\?)/, '')
.split('&')
.reduce((searchParams, keyValuePair) => {
keyValuePair = keyValuePair.split('=')
searchParams[keyValuePair[0]] = keyValuePair[1]
return searchParams
})

第七条:发现错误要保持静默


   // Good 👍🏻
try {
...
} catch () {🤐}

// Bad 👎
try {
...
} catch (error) {
setErrorMessage(error.message)
logError(error)
}

第八条:广泛使用全局变量


  // Good 👍🏻
let count = 1
function addCount() {
count += 1
}

// Bad 👎
function addCount() {
let count = 1
count += 1
}

第九条:构建备用变量


  // Good 👍🏻
let count = 1
function addCount() {
count += 1
}

// Bad 👎
function addCount() {
let count = 1
count += 1
}

第十条:Type 使用需谨慎


  // Good 👍🏻
function sum(a, b) {
return a + b
}

// Bad 👎
function sum(a: number, b: number) {
return a + b
}

第十一条:准备「Plan B」


  // Good 👍🏻
function square(num) {
if (typeof num === 'undefined') {
return undefined
} else {
return num ** 2
}
return null
}

// Bad 👎
function square(num) {
if (typeof num === 'undefined') {
return undefined
}
return num ** 2
}

第十二条:嵌套的三角法则


    // Good 👍🏻
function somFun(num) {
if (condition1) {
if (condition2) {
asyncFunction(param, (result) => {
if (result) {
for (; ;) {
if (condition3) {

}
}
}
})
}
}
}

// Bad 👎
async function somFun(num) {
if (!condition1 || !condition2) {
return;
}
const result = await asyncFunction(params);
if (!result) {
return;
}
for (; ;) {
if (condition3) {

}
}
}

第十三条:混合缩进


      // Good 👍🏻
const f = ['zs'
, 'lisi', 'wangwu']
const d = {
name: 'zs',
age: '18'
}

// Bad 👎
const f = ['zs'
, 'lisi', 'wangwu']
const d = {
name: 'zs',
age: '18'
}

总结


所谓的“烂代码”,是大家一定 不要 照着写的哈。


“教你写出令人窒息的“烂代码”“ 是一个反义,这个大家肯定是可以明白的哈~~~~。



”烂代码“内容参考自:zhuanlan.zhihu.com/p/516564022



作者:程序员Sunday
来源:juejin.cn/post/7300440002999435316
收起阅读 »

Uniapp Record:获取手机号

web
前言:渠道 -> 产品:"我需要收集用户信息"。产品 -> 开发:"那就新增一个功能来获取用户的手机号地址相关信息"。最近项目中增加了获取用户信息相关需求,这个功能怎么说呢,对于我甚至是大部分人来说都是比较抵触的吧,毕竟无缘无故获取个人信息就是感觉...
继续阅读 »

前言:渠道 -> 产品:"我需要收集用户信息"。产品 -> 开发:"那就新增一个功能来获取用户的手机号地址相关信息"。最近项目中增加了获取用户信息相关需求,这个功能怎么说呢,对于我甚至是大部分人来说都是比较抵触的吧,毕竟无缘无故获取个人信息就是感觉不爽,哈哈!但是没也没法,身为打工人的无奈,照做呗。



由于目前项目技术栈是 uniapp,所以先去官方文档查阅相关资料,了解到目前有是三种方式涉及手机号相关的,自然也是能够获取到手机号。


1. uni一键登录


uni一键登录是DCloud公司联合个推公司推出的,整合了三大运营商网关认证能力的服务。实现流程如下:



  1. App 界面弹出请求授权,询问用户是否同意该App获取手机号。这个授权界面是运营商SDK弹出的,可以有限定制;

  2. 用户同意授权后,SDK底层访问运营商网关鉴权,获得当前设备access_token等信息;

  3. 在服务器侧通过 uniCloud 将access_token等信息 置换为当前设备的真实手机号码。然后服务器直接入库,避免手机号传递到前端发生的不可信情况。


对该方法大致了解了下,其中流程相对比较简单,但是结合当前项目来说:



  1. 每次验证需要收费,虽然很便宜(2分)

  2. 需要开通uni一键登录服务,uniCloud 服务


因为项目不涉及云开发,而且不考虑产品使用时产生的额外费用,所以暂时pass掉。


2. OAuth 登录鉴权


App端OAuth(登录鉴权)模块封装了市场上主流的三方登录SDK,提供JS API统一调用登录鉴权功能。也看下实现流程:



  1. 向三方登录平台申请开通,有些平台(如微信登录)申请成功后会获取appid;

  2. 在HBuilder中配置申请的参数(如appid等),提交云端打包生成自定义基座;

  3. 在App项目中用API进行登录,成功后获取到授权标识提交到业务服务器完成登录操作。


该方式需要在项目 mainifest.json 中去开启 OAuth 鉴权模块:


uni02.png


可以看到里面除了前面提到的 一键登录,还包含 苹果登录、微信登录、QQ登录等三方登录平台,因为要涉及开通相关服务,并且当前登录业务鉴权逻辑比较简单(手机号、密码验证),并且app也为上架应用市场,所以这种相对繁琐的方式也就不考虑了。


3. 微信小程序登录


前面两种方式都pass掉了,意味着要获取手机号相关信息在APP中是行不通了的,但是不慌,不是还有微信小程序版嘛,正好产品也包含小程序平台,前段时间做公众号网页开发时也是包含登录授权,所以小程序的授权登录应该也差不多,而且小程序对比APP来说相对便捷(缺点是某些涉及原生插件相关的功能暂时无法使用)。


同样,先去微信官方文档查阅,看到有两种方式可以获取:


uni03.png


下面具体介绍下实现方案:


3-1. 纯前端实现

<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" plain="true">获取手机号</button>

这个 button 里面的一些属性及事件的具体用法说明可以去看文档说明:uniapp button 用法,文档解释的很清楚,写法也是固定的。


这里还需要用到一个加解密插件:WXBizDataCrypt,下载链接如下,


https://res.wx.qq.com/wxdoc/dist/assets/media/aes-sample.eae1f364.zip

可以去下载选择对应的版本,目前有 Java、C++、Node、Python四个版本,我们这里选择Node版本,将 WXBizDataCrypt.js 添加到项目中


完整代码如下:


<!-- testPhone.vue -->
<template>
<view class="wrap">
<view class="box-container">
<input v-model="phone" />
<view class="action-btn">
<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" plain="true">获取手机号</button>
</view>
</view>
</view>
</template>

<script>
import WXBizDataCrypt from '@/utils/WXBizDataCrypt.js'

export default {
data() {
return {
phone: "",
phone_iv: "",
js_code: "",
session_key: "",
phone_encryptedData: null,
}
},
onShow() {
this.initLogin()
},
methods: {
initLogin() {
uni.login({
provider: 'weixin',
success: res => {
this.js_code = res.code
uni.request({
url: 'https://api.weixin.qq.com/sns/jscode2session', // 请求微信服务器
method: 'GET',
data: {
appid: 'xxxxxxxx', // 微信appid
secret: 'xxxxxxxxxxxxx', // 微信秘钥
js_code: this.js_code,
grant_type: 'authorization_code'
},
success: (res) => {
console.log('获取信息', res.data);
this.session_key = res.data.session_key
}
});
}
});
},
getPhoneNumber(res) {
console.log(res, '---手机号回调信息')
this.phone_encryptedData = res.detail.encryptedData;
this.phone_iv = res.detail.iv;
let pc = new WXBizDataCrypt('填写微信appid', this.session_key);
try {
let data = pc.decryptData(this.phone_encryptedData, this.phone_iv);
if (data.phoneNumber !== '') {
this.phone = data.phoneNumber;
}
} catch (error) {
console.error('获取失败:', error);
}
}
}
}
</script>

<style lang="less">
.wrap {
width: 100vw;
height: 100vh;
background-color: #F1F2F6;
display: flex;
align-items: center;
justify-content: center;

.box-container {
width: 70vw;
height: 30vh;

input {
border: 2rpx solid black;
}

.action-btn {
width: 50%;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 40rpx auto;
}
}
}
</style>

大致流程是:


先通过 uni.login 拿到一个 code,用这 code 作为js_code、appid(微信小程序后台设置中获取)、secret(微信公众号后台获取的密钥)、grant_type(固定值:authorization_code) 去请求 https://api.weixin.qq.com/sns/jscode2session 这个地址,返回结果如下:


{"session_key":"zkJJOfHPYHc\/cVK2kydibg==","openid":"oHXOj5NJMH78yWdVcf6loGOL4cno"}

然后点击按钮调起微信手机号授权页:


999.png


@getphonenumber 事件的回调中获取的信息打印结果如下:


888.png


框选的信息就是我们需要的,是一个加密后的数据。


最后使用 WXBizDataCrypt 对信息进行解密,解密后就是我们需要的手机号信息了。


3-2. 前后端实现


前端代码逻辑改了下:


<script>
export default {
data() {
return {
phone: "",
}
},
methods: {
getPhoneNumber(res) {
console.log(res, '---手机号回调信息')
// 注:这里的code和前面登录返回的code是不同的
const { code } = res.detail
// 根据code去请求后端提供的接口,即可从响应数据中拿到手机号
}
}
}
</script>

后端做了哪些事情呢?


首先会去 获取接口调用凭证 ,官方文档描述如下:


777.png


// 参数说明

{
grant_type: client_credential, // 固定值
appid: '', // 填写微信小程序的appid
secret: '', // 填写微信小程序的密钥
}

返回参数为:access_token(接口凭证)expire_in(过期时间,默认为2小时)


然后再去调获取手机号接口(getPhoneNumber),


666.png


参数携带前面返回的 access_token,再加上前端传过来的 code,即可获取到手机号信息。


下面是我用 Postman 对三个接口做了测试验证:


weixin08.png


weixin07.png


weixin06.png


对比两种方式,个人建议还是采用第二种好一点,让相关的业务都在后端去处理,除此之外还有一个原因就是涉及一个安全性相关问题,前面代码中可以看到我们在请求小程序登录接口是将 appid、screct等信息放在请求参数中的,这种极易通过源码拿到,所以存在相关信息泄露问题,事实证明这种方式也是不建议使用的:


555.png


踩坑点




  1. 注意区分登陆时返回的 code 和 button 按钮获取手机号回调返回的 code 是不相同的




  2. @getphonenumber 回调函数的返回信息如果信息为:api scope is not declared in the privacy agreement ,这种是小程序的【隐私保护策略】限制的,排查下你的小程序中用户隐私保护指引设置送是否添加了相关的用户隐私类型(手机号、通讯录、位置信息等)




444.png


以上就是结合项目需求场景对获取手机号的实现做的一个记录!


作者:瓶子丶
来源:juejin.cn/post/7300036605099343926
收起阅读 »

token 和 cookie 还在傻傻分不清?

web
token 概念和作用 Token是一种用于身份验证和授权的令牌。在Web应用程序中,当用户进行登录或授权时,服务器会生成一个Token并将其发送给客户端。客户端在后续的请求中将Token作为身份凭证携带,以证明自己的身份。 Token可以是一个字符串,通常是...
继续阅读 »

token 概念和作用


Token是一种用于身份验证和授权的令牌。在Web应用程序中,当用户进行登录或授权时,服务器会生成一个Token并将其发送给客户端。客户端在后续的请求中将Token作为身份凭证携带,以证明自己的身份。


Token可以是一个字符串,通常是经过加密和签名的,以确保其安全性和完整性。服务器收到Token后,会对其进行解析和验证,以验证用户的身份并授权对特定资源的访问权限。


Token的使用具有以下特点:



  • 无状态:服务器不需要在数据库中存储会话信息,所有必要的信息都包含在Token中。

  • 可扩展性:Token可以存储更多的用户信息,甚至可以包含自定义的数据。

  • 安全性:Token可以使用加密算法进行签名,以确保数据的完整性和安全性。

  • 跨域支持:Token可以在跨域请求中通过在请求头中添加Authorization字段进行传递。


Token在前后端分离的架构中广泛应用,特别是在RESTful API的身份验证中常见。它比传统的基于Cookie的会话管理更灵活,并且适用于各种不同的客户端,例如Web、移动应用和第三方接入等。


cookie 和 token 的关系


Cookie和Token是两种不同的概念,但它们在身份验证和授权方面可以有关联。


Cookie是服务器在HTTP响应中通过Set-Cookie标头发送给客户端的一小段数据。客户端浏览器将Cookie保存在本地,然后在每次对该服务器的后续请求中将Cookie作为HTTP请求的一部分发送回服务器。Cookie通常用于在客户端和服务器之间维护会话状态,以及存储用户相关的信息。


Token是一种用于身份验证和授权的令牌。它是一个包含用户身份信息的字符串,通常是服务器生成并返回给客户端。客户端在后续的请求中将Token作为身份凭证发送给服务器,服务器通过验证Token的有效性来确认用户的身份和权限。


Cookie和Token可以结合使用来实现身份验证和授权机制。服务器可以将Token存储在Cookie中,然后发送给客户端保存。客户端在后续的请求中将Token作为Cookie发送给服务器。服务器通过验证Token的有效性来判断用户的身份和权限。这种方式称为基于Cookie的身份验证。另外,也可以将Token直接存储在请求的标头中,而不是在Cookie中进行传输,这种方式称为基于Token的身份验证。


需要注意的是,Token相对于Cookie来说更加灵活和安全,可以实现跨域身份验证,以及客户端和服务器的完全分离。而Cookie则受到一些限制,如跨域访问限制,以及容易受到XSS和CSRF攻击等。因此,在实现身份验证和授权机制时,可以选择使用Token替代或辅助Cookie。


token 一般在客户端存在哪儿


Token一般在客户端存在以下几个地方:



  • Cookie:Token可以存储在客户端的Cookie中。服务器在响应请求时,可以将Token作为一个Cookie发送给客户端,客户端在后续的请求中会自动将Token包含在请求的Cookie中发送给服务器。

  • Local Storage/Session Storage:Token也可以存储在客户端的Local Storage或Session Storage中。这些是HTML5提供的客户端存储机制,可以在浏览器中长期保存数据。

  • Web Storage API:除了Local Storage和Session Storage,Token也可以使用Web Storage API中的其他存储机制,比如IndexedDB、WebSQL等。

  • 请求头:Token也可以包含在客户端发送的请求头中,一般是在Authorization头中携带Token。


需要注意的是,无论将Token存储在哪个地方,都需要采取相应的安全措施,如HTTPS传输、加密存储等,以保护Token的安全性。


存放在 cookie 就安全了吗?


存放在Cookie中相对来说是比较常见的做法,但是并不是最安全的方式。存放在Cookie中的Token可能存在以下安全风险:



  • 跨站脚本攻击(XSS) :如果网站存在XSS漏洞,攻击者可以通过注入恶意脚本来获取用户的Cookie信息,包括Token。攻击者可以利用Token冒充用户进行恶意操作。

  • 跨站请求伪造(CSRF) :攻击者可以利用CSRF漏洞,诱使用户在已经登录的情况下访问恶意网站,该网站可能利用用户的Token发起伪造的请求,从而执行未经授权的操作。

  • 不可控的访问权限:将Token存放在Cookie中,意味着浏览器在每次请求中都会自动携带该Token。如果用户在使用公共计算机或共享设备时忘记退出登录,那么其他人可以通过使用同一个浏览器来访问用户的账户。


为了增加Token的安全性,可以采取以下措施:



  • 使用HttpOnly标识:将Cookie设置为HttpOnly,可以防止XSS攻击者通过脚本访问Cookie。

  • 使用Secure标识:将Cookie设置为Secure,只能在通过HTTPS协议传输时发送给服务器,避免明文传输。

  • 设置Token的过期时间:可以设置Token的过期时间,使得Token在一定时间后失效,减少被滥用的风险。

  • 使用其他存储方式:考虑将Token存储在其他地方,如Local Storage或Session Storage,并采取加密等额外的安全措施保护Token的安全性。


token 身份验证代码实现


服务端使用 JWT 进行 token 签名和下发


可以参考使用这个库 node-jsonwebtoken


后端代码示例 (Node.js / Express),代码简单实现如下:


const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

const secretKey = 'mysecretkey';

app.use(express.json());

app.post('/api/login', (req, res) => {
// 从请求中获取用户名和密码
const { username, password } = req.body;

// 验证用户名和密码
if (username === 'admin' && password === 'password') {
// 用户名和密码验证成功,生成Token并返回给前端
const token = jwt.sign({ username }, secretKey, { expiresIn: '1h' });
res.json({ token });
} else {
// 用户名和密码验证失败,返回错误信息给前端
res.status(401).json({ error: 'Authentication failed' });
}
});

app.get('/api/protected', verifyToken, (req, res) => {
// Token验证成功,可以访问受保护的路由
res.json({ message: 'Protected API endpoint' });
});

function verifyToken(req, res, next) {
const token = req.headers.authorization;

if (!token) {
return res.status(401).json({ error: 'Missing token' });
}

// 验证Token
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token' });
}

// Token验证通过,将解码后的数据存储在请求中,以便后续使用
req.user = decoded;
next();
});
}

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

在上述后端代码中,我们使用了jsonwebtoken库来生成和验证Token。在登录路由/api/login中,验证用户名和密码成功后,生成一个Token并返回给前端。在受保护的路由/api/protected中,我们使用verifyToken中间件来验证请求中的Token,只有通过验证的请求才能访问该路由。


当然实际开发中, 可以使用中间件来进行 jwt 的验证, 下发方式也因人而异, 可以放在 cookie 中, 也可以作为 response 返回均可, 上述代码仅作参考;


前端代码实现示范如下


前端获取到了Token后将其存储在Cookie中,并在后续请求中自动发送给后端,可以通过以下方式实现前端代码:


import React, { useState, useEffect } from 'react';

function App() {
const [token, setToken] = useState('');

useEffect(() => {
// 检查本地是否有保存的Token
const savedToken = localStorage.getItem('token');
if (savedToken) {
setToken(savedToken);
}
}, []);

const handleLogin = async () => {
// 发送请求到后端进行登录验证
const response = await fetch('http://example.com/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: 'admin', password: 'password' }),
});

if (response.ok) {
// 登录成功,获取Token并保存到前端
const data = await response.json();
setToken(data.token);
// 保存Token到本地
localStorage.setItem('token', data.token);
}
};

const handleLogout = () => {
// 清除保存的Token
setToken('');
// 清除本地保存的Token
localStorage.removeItem('token');
};

return (
<div>
{token ? (
<div>
<p>Token: {token}</p>
<button onClick={handleLogout}>Logout</button>
</div>
) : (
<button onClick={handleLogin}>Login</button>
)}
</div>

);
}

export default App;

作者:晴小篆
来源:juejin.cn/post/7299731897626443785
收起阅读 »

看完还学不会正则,快来锤我!

web
前言 各位同仁在表单验证规则,或者在验证数据的时候,是不是经常都是xxx.xxxx,然后对数据处理来处理去,最后进行后续操作……,不仅费时费力,而且消耗精神,于是乎: ps(我):要不要用用正则,但是想不起来怎么写啊,百度搜一下吧,chatGpt搜一下,贴上去...
继续阅读 »

前言


各位同仁在表单验证规则,或者在验证数据的时候,是不是经常都是xxx.xxxx,然后对数据处理来处理去,最后进行后续操作……,不仅费时费力,而且消耗精神,于是乎:


ps(我):要不要用用正则,但是想不起来怎么写啊,百度搜一下吧,chatGpt搜一下,贴上去


我:欧哟?刚刚好啊,效果还怪好的嘞,哈哈哈,天助我也!


这还是好的情况,刚刚好符合,要是不好……


ps(我):服了,这正则不得行,chatGpt搜的错的,自己写吧又不会,找吧又没有……
我:乖乖写if else吧


又是啃哧啃哧,耗时耗力……可能大家伙都是差不多的哈,谁也别说谁(大佬除外)。本着多一事不如少一事的原则,直接学习一波!


js正则表达式


正则(正则表达式)是一种用于描述文本模式的工具,它通过使用特定的符号和语法规则来定义一个字符串的模式。正则表达式通常由各种字符和特殊元字符组成,用于进行字符串匹配、查找、替换和验证等操作。


使用正则表达式,可以执行以下操作:



  1. 模式匹配:正则表达式可以用于查找和匹配具有特定模式的字符串。通过定义一个模式,可以搜索和识别符合该模式的字符串。

  2. 字符串查找与替换:正则表达式可以用于在文本中进行字符串查找和替换。通过指定要查找或替换的模式,可以对目标字符串进行修改和处理。

  3. 数据验证:正则表达式可以用于验证用户输入或其他数据的格式和有效性。例如,可以使用正则表达式验证电子邮件地址、电话号码、日期等的格式是否符合预期。

  4. 文本提取:在文本处理中,可以使用正则表达式从大量文本数据中提取出所需的信息。例如,可以使用正则表达式从日志文件中提取特定的时间戳或关键字。

  5. 数据清洗与转换:使用正则表达式,可以进行文本数据的清洗和转换。可以根据模式匹配和替换规则,删除非法字符、规范化日期格式、提取关键信息等。


正则表达式提供了一种强大和灵活的文本处理工具,它被广泛应用于编程语言、文本编辑器、数据处理工具等各种软件中。虽然正则表达式的语法可能会显得复杂,但掌握它可以极大地提高对文本模式处理的能力。


应用


正则表达式在计算机科学和文本处理中具有广泛的应用。以下是一些常见的正则表达式应用:



  • 模式匹配:正则表达式可用于检测字符串是否与特定模式匹配。例如,可以使用正则表达式来验证电子邮件地址、检查电话号码的格式、识别日期等。

  • 字符串搜索与替换:正则表达式可以用于在文本中搜索特定的模式,并进行替换或提取。这对于在大量文本中进行批量操作非常有用,如查找和替换文本文件中的特定单词或短语。

  • 表单验证:在前端开发中,可以使用正则表达式验证用户输入的表单数据。例如,验证用户名是否只包含字母和数字、检查密码是否符合指定的复杂度要求等。

  • URL路由:许多Web框架使用正则表达式来解析URL路由和处理动态路由。它们通过正则表达式匹配URL字符串并将其映射到相应的处理程序或控制器。

  • 日志分析:使用正则表达式可以解析和提取日志文件中的有用信息。例如,可以使用正则表达式从服务器日志中提取IP地址、日期时间戳、错误消息等。

  • 数据清洗与转换:正则表达式可用于清洗和转换数据,如从多种格式的文本数据中提取特定字段、规范化日期格式、去除特殊字符等。

  • 编程工具与编辑器:许多编程工具和文本编辑器支持正则表达式搜索和替换功能。这使得开发人员能够更灵活地进行代码重构和批量修改操作。


创建正则


js 中内置了正则表达式对象 RegExp,我们要创建一个正则表达式,可以:



  • 第一种创建方式,接收patternmodifiers两个参数


// 构造函数RegExp
var regex = new RegExp(pattern, modifiers);
// 构造函数创建
var regex = new RegExp('xyz', 'i');
// 等价于 字面量创建
var regex = /xyz/i;

RegExp 构造函数接收两个参数,pattern 描述了表达式的模式,为字符串,modifiers 是正则表达式的修饰符,用于执行区分大小写和全局匹配。



  • 第二种创建方式,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。


var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;

在构造函数创建时 pattern 是正则字符串,字面量创建时,pattern 是一个类似 /正则规则/ 表达式,是放在双斜杠里的。
modifiers有三个值,分别为


描述
i不区分大小写。在匹配时忽略大小写
g全局匹配。查找所有可能的匹配,而不仅仅是第一个匹配。
s单行匹配。启用单行模式,使点号(.)可以匹配包括换行符在内的任意字符。
u启用完整的 Unicode 匹配支持,会正确处理四个字节的 UTF-16 编码。
m多行匹配。启用多行模式,^ 和 $ 可以匹配行的开始和结束。

例如,使用 ig 修饰符创建一个忽略大小写和全局匹配的正则表达式:


javascript
var pattern = new RegExp("pattern", "ig");

或者使用字面量表示法也可以传递修饰符:


var pattern = /pattern/ig;

正则对象的方法


正则表达式对象通常提供一组方法,用于在字符串上执行不同的操作。以下是一些常见的正则表达式对象的方法:




  1. test(string): 检测指定字符串是否与正则表达式匹配。返回一个布尔值,表示是否找到匹配项。一般开发当中,我们使用这种方法较多。


    const regex = /apple/;
    console.log(regex.test("I love apples")); // true
    console.log(regex.test("I prefer oranges")); // false



  2. exec(string): 在给定字符串中搜索匹配项,并返回一个数组或 null。数组包含有关匹配项的详细信息,如匹配的子字符串、捕获组等。


    const regex = /\d+/;
    console.log(regex.exec("I have 100 apples")); // ["100"]
    console.log(regex.exec("There are no numbers here")); // null



  3. match(regexp): 在字符串中查找与正则表达式匹配的内容,并返回一个数组或 null。类似于 exec() 方法,但是 match() 是在字符串上调用,而不是在正则表达式上调用。


    const string = "I have 100 apples";
    const regex = /\d+/;
    console.log(string.match(regex)); // ["100"]



  4. search(string): 在字符串中搜索与正则表达式匹配的内容,并返回匹配项的索引。如果没有找到匹配项,则返回 -1。


    const string = "I prefer oranges";
    const regex = /oranges/;
    console.log(string.search(regex)); // 8



  5. replace(regexp, replacement): 替换字符串中与正则表达式匹配的部分。可以将匹配项替换为指定的字符串或使用函数进行替换。


    const string = "I like cats and dogs";
    const regex = /cats/;
    const replacement = "birds";
    const newString = string.replace(regex, replacement);
    console.log(newString); // "I like birds and dogs"



  6. split(regexp): 将字符串分割为由正则表达式匹配的子字符串组成的数组。正则表达式定义了分隔符。


    const string = "apple,banana,orange";
    const regex = /,/;
    const parts = string.split(regex);
    console.log(parts); // ["apple", "banana", "orange"]



正则规则


分为基本字符匹配;元字符匹配,如\w;锚点匹配指定匹配发生的位置, 如^ 表示匹配行的开头;量词和限定符, 如*; 分组和捕获();零宽断言:正向肯定断言 (?=...):匹配满足断言条件的位置,但不会消耗字符;


接下来一一进行介绍。


基本字符匹配


匹配字面量字符/ /


如果想在javaScript当中直接匹配java,可以直接在我们的字面量当中写入想要匹配的值,即java直接进行匹配。


正则: /java/


可以匹配的不能匹配的
javascriptJavascript
javajaava

字符组[ ]


如果不仅仅想要匹配java还想要匹配Java,那光光/java/是不够的。这时候还需要用到我们的字符组。


正则:/[Jj]ava/


可以匹配的不能匹配的
javascriptjaava
Javascriptjvav

[]匹配规则当中,目标字符可以匹配中括号里面的任意一个字符即可,转为javaScript语言就是 ||的意思。观察两个目标字符串,java与Java的区别也仅仅是首字母不同,那么只需要兼容开头的大小字母即可。


拓展

若是想匹配java Java JAva,正则需要如何编写?通过观察各个字符当中的差别,即前两个字母的可能性都可能为大小写,便得出前两个位置的匹配使用字符组即可。


正则:/[J][Aa]va/


字符组区间 -


如果说只想匹配前缀为123,后面是二十六个字母当中任何一个的字符怎么办?


这简单,刚刚学完字符组,我直接一手/123[a,b,c,d....]/把二十六个字母全部列一遍,话虽如此,但大可不必!


此处若是可选匹配字母过多的话,可直接使用字符组区间连接


正则: /123[a-zA-Z]/


可以匹配的不能匹配的
123a123
123B12345

同时还可以匹配多个数字,比如我只想匹配[3-9]的数字,那么也可以使用连接符


正则123[3-9][a-zA-Z]


可以匹配的不能匹配的
1233a123a9
1236B123B

字符组取反:[^]


有的时候你可能也不想匹配某些字符,比如只晕小写字母,那么这个时候你可以对你所要匹配的字符组进行取反,那就匹配不到了。


正则:/[^a-z]/


可以匹配的不能匹配的
1233ABCDEabcde
12345678adasd
123adasdadasd


注意: 此处需要全部为小写字母test匹配结果才是false,若字符包含其他的字符,test的匹配结果仍然为true。



const pattern = /[^a-z]/ // 表示的意思为所有字符都不是小写
const string = '123adasd' // 此处还有数字
pattern.test(string) // true

元字符匹配


日常开发当中,元字符单独使用的情况并不多,更多的是跟随后续的量词一块使用,最终形成限定字符格式的正则。


单点 .


. 是一个特殊的元字符,可以用于匹配除了换行符 \n(或其他行终止符,如 \r\n)之外的任意单个字符。


正则:/./


可以匹配的不能匹配的
1\n(换行)
a\r(回车)

数字 \d


\d 可以匹配任意一个数字字符,包括 0 到 9 的数字。


字符 \w


用于匹配字母字符、数字和下划线。


具体来说,\w 匹配以下字符:



  • 小写字母(a-z)

  • 大写字母(A-Z)

  • 数字(0-9)

  • 下划线(_)


空白符 \s


用于匹配空白字符



  • 空格符(Space)

  • 制表符(\t)

  • 换行符(\n)

  • 回车符(\r)

  • 垂直制表符(\v)

  • 换页符(\f)



注意:如果说想要匹配正则当中的匹配规则符号,例如只想匹配单点字符.,则需要使用反斜杠进行转义,即/\./ 任何匹配正则当中具有意义的字符都需要进行转义。



量词


量词用于指定模式重复出现的次数。允许你匹配一定数量的字符或子模式,是正则当中见怪不怪的玩意。与上述字符相互搭配,能获得意想不到的结果。


量词 {}


用于匹配前面的字符或子表达式指定的精确的重复次数。


比如需要匹配重复多个字符,如需要匹配出现两次a的字符串。


正则:/a{2}/


可以匹配的不能匹配的
aaabab
aabbabb

但是我只知道会出现a字符,可能是两到三个呢?这个时候就可以使用区间来表示,囊括出现的次数。


正则:/a{2,3}/


可以匹配的不能匹配的
aaabbbb
aabbabb
aaababab

如果只知道出现一次,但是不清楚具体有几次,便直接可以不写右区间,表示至少出现n次,比如下面的正则就表示至少出现3次a


正则:/a{3,}/


可以匹配的不能匹配的
aaabbbb
baaaaaabb

量词 +


用于匹配前面的字符或子表达式至少一次或多次出现。
实际上,+的表现形式,还可以用{1,}来表示


正则: /a+/ 等价于 /a{1,}/


可以匹配的不能匹配的
abbbb
aabbb

量词 *


用于匹配前面的字符或子表达式出现0次或多次出现。实际上,*的表现形式,也可以用{0,}来表示


正则: /a*/ 等价于 /a{0,}/


可以匹配的不能匹配的
abbbb
aabbb

量词 ?


用于匹配前面的字符或子表达式零次或一次。实际上,*的表现形式,也可以用{0,1}来表示


正则: /a?b/ 等价于 /a{0,1}b/


可以匹配的不能匹配的
babcde
bad


正则表达式的贪婪匹配和非贪婪匹配是用来描述匹配模式时的两种不同行为。
贪婪匹配是指正则表达式尽可能地匹配更长的文本片段。它会尽量多地消耗输入字符串,并尝试匹配满足整个正则表达式模式的最长可能结果,是默认的行为,
反之,非贪婪匹配(也称为懒惰匹配或最小匹配) 则是指正则表达式尽可能地匹配更短的文本片段。它会尽量少地消耗输入字符串,并尝试匹配满足整个正则表达式模式的最短可能结果。
通常非贪婪匹配通过在正则字符串后面加?号来表示。



示例
正则表达式 /a+/,它表示匹配一个或多个连续出现的字符 "a"。


对于字符串 "aaa",贪婪匹配将尽量匹配更长的连续的 "a" 字符串,在这种情况下会匹配整个字符串 "aaa"。


使用非贪婪匹配需要在量词后面添加 ?。正则表达式 /a+?/ 表示非贪婪匹配,将匹配一个或多个连续出现的字符 "a",但只尽量匹配最短的结果。非贪婪匹配将尽量匹配最短的连续的 "a" 字符串。在这个例子中,非贪婪匹配会匹配第一个 "a" 字符,因为它是最短的满足正则表达式模式的子串。


锚点匹配


锚点是正则表达式中的特殊字符,用于匹配字符串的位置而不是具体的字符,可用于指定匹配发生的位置,常用的锚点有^$\b


^ 起始位置


表示匹配行的开头。下面正则表示匹配以a为开头的字符


正则:^a


可以匹配的不能匹配的
ada
abbc

$ 结束位置


表示匹配行的结尾。下面正则表示匹配以a为结尾的字符


正则:a$


可以匹配的不能匹配的
aab
dabc

\b 边界


表示匹配单词边界。下面正则表示匹配独立的单词


正则:/\bapple\b/


可以匹配的不能匹配的
I love applepineapple
applepinapple

\b还有很多其他的应用,比如



  • \b\w+\b:匹配一个或多个连续的单词字符,可以用来分割句子为单词数组。

  • \b\d{4}\b:匹配仅包含4位数字的字符串


在转义\b的时候需要使用\\b


分组和捕获:


分组 ()


括号 ( ):用于将一组模式作为单个单元进行匹配,并将其视为一个分组。


比如,我要匹配以jstsjava后缀的文件
正则:/.*\.(js|ts|java)/


可以匹配的不能匹配的
index.js1.png
1.ts2.jpg
calss.java3.mp3

再比如 正则:/(ab){1,}/,可以匹配一个或出现多个连续的ab,利用分组实现的


可以匹配的不能匹配的
abaa
ababba

捕获组


通过圆括号捕获分组内的内容,可以在后续操作中进行引用。


可能这比较难理解,我们举例说明,比如,我们有1-82-2这种类型的数据,我们可以使用正则的分组将两边的数据包裹,并使用exec进行捕获。分组符号的数据就是把这些想要捕获的数据标记出来。


如果我们想要 () 的分组能力,但是又不想捕获数据,可以使用 (?:) 表达式。可以提高正则表达式的性能和简洁性。


image.png


零宽断言



  1. 正向肯定预查(?=...):表示在当前位置后面,如果满足括号内的表达式,则继续匹配成功。

  2. 正向否定预查(?!...):表示在当前位置后面,如果不满足括号内的表达式,则继续匹配成功。

  3. 反向肯定预查(?<=...):表示在当前位置前面,如果满足括号内的表达式,则继续匹配成功。

  4. 反向否定预查(?<!...):表示在当前位置前面,如果不满足括号内的表达式,则继续匹配成功。



  • /(?=\d)\w+/ 匹配由数字紧随其后的单词字符。
    | 可以匹配的 | 不能匹配的 |
    | --- | --- |
    | 1 | w |
    | 1w | ww |


为什么这里能匹配1呢?1首先同样属于字符,其次还是数字,在断言的时候,不消耗字符,符合数字随其后的规则(本身)



  • /(?<!\d)\w+/ 匹配没有数字紧随在前面的单词字符。(js不支持)



js并不支持反向预查,只支持正向预查。这是因为正向预查在匹配时,可以当前位置后面的内容进行断言判断,如果不符合预期,则无法继续匹配成功。这种类型的预查可以通过回溯来实现。


然而,反向否定预查需要从当前位置回溯到前面的位置进行条件判断,这就使得正则引擎需要逆序地扫描前面的内容,增加了匹配的复杂度。因此,实现反向否定预查的算法相对更为复杂,并且可能导致性能下降。


反向否定预查在某些特定情况下可以被其他模式替代,比如使用捕获组结合后续的处理代码来达到类似的效果。



正则表达式大全



  1. 邮箱验证


/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(.\w{2,3})+$/

^\w+匹配以字符开头,([.-]?\w+)* 部分出现两次,品牌包含一个或多个由-或点.连接的部分,(.\w{2,3})+匹配域名




  1. URL 验证:包括 HTTP 和 HTTPS 协议。


/^(https?://)?[\w-]+(.[\w-]+)+[/#?]?.*$/


  1. 身-份-证号码验证:验证中国大陆身-份-证号码的有效性。


低配:
/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/

高配:
身-份-证号匹配
/^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|3[0-1])\d{3}[0-9Xx]$/


  • ^[1-9]\d{5}:匹配 6 位行政区划代码、

  • (19|20)\d{2}:年份,匹配以 19 或 20 开头的四位数字、

  • (0[1-9]|1[0-2]):月份,取值范围为 01 到 12、

  • (0[1-9]|[1-2]\d|3[0-1]):日期,取值范围为 01 到 31、

  • \d{3}:顺序码,任意三位数字、

  • [0-9Xx]:校验码,可以是数字或字母 X 或 x、



  1. 数字验证:用于验证一个字符串是否只由数字组成。


`/^\d+$/`


  1. 字母验证:用于验证一个字符串是否只由字母组成。


`/^[a-zA-Z]+$/`


  1. 小数验证:匹配的数字可包含小数点,此处转义了小数点,


/^\d+(\.\d+)?$/


  1. 整数验证(包括负数):用于验证一个字符串是否为整数,可以包含正负号。


`/^[-+]?\d+$/`


  1. IP 地址验证: 用于验证 IPv4 地址的有效性。


/^((25[0-5]|2[0-4]\d|[01]?\d\d?).){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/


  1. 手机号码验证:


低配版本,仅表示11位数字


```
^\d{11}$ 低配版本,11位数字
```

高配版本


```
/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/

如果不想这么复杂,可以写为
/^1[3-9]d{9}$/


还能匹配*特殊符号的,但是会失去匹配11位数功能
/^1[3-9]\d{1}(?:\*{1,})*\d+$/
```

如果确定符号个数,可改为/^1[3-9]\d{1}((?:\*{4})|\d{4})\d{4}$/,就能匹配固定11位数的号码



  • 可以匹配152702365242

  • 可以匹配152****65242


10.密码复杂度要求




  • 8位任意密码


    /^.{8,}$/



  • 包括至少8个字符,包含大写字母、小写字母和数字


    /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/



?=为正向断言,判断条件是否符合.*\d,即任意字符但是需要出现一个数字,其余类似


这个正则表达式用于强制密码应至少包含一个数字(?=.*\d)、一个小写字母(?=.*[a-z])和一个大写字母(?=.*[A-Z]),并且长度至少为8个字符.{8,}



  • 包括至少8个字符,包含大写字母、小写字母和数字,包括特殊字符
    /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*?]).{8,}$/



11.以8结尾,且位数在6位以内的数字


/^\d{0,5}8$/


  1. 时间匹配,匹配时分,年月日的匹配建议还是按照Date的API,正则在匹配闰年的二月份时候无法匹配


/^(?:[01]\d|2[0-3]):(?:[0-5]\d)$/


  • 可以匹配09:10 12:12 23:01 23:59


/^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$/



  • (?!0000) 表示后面不能跟着四个0,即年份不能为0000。

  • [0-9]{4} 表示匹配四个数字,即年份的格式为四位数字。

  • - 表示匹配“-”字符。

  • (?:…) 表示非捕获型分组,用于提高正则表达式的效率。

  • (?:0[1-9]|1[0-2]) 表示匹配01-12月份,其中0[1-9]表示01-09月份,1[0-2]表示10-12月份。

  • (?:0[1-9]|1[0-9]|2[0-8]) 表示匹配01-28日,其中0[1-9]表示01-09日,1[0-9]表示10-19日,2[0-8]表示20-28日。

  • (?:0[13-9]|1[0-2])-(?:29|30) 表示匹配01、03、05、07、08、10、12月份的29或30日。

  • (?:0[13578]|1[02])-31 表示匹配01、03、05、07、08、10、12月份的31日。

  • (?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29 表示匹配闰年的2月29日,其中[0-9]{2}表示匹配两位数字的年份,(?:0[48]|[2468][048]|[13579][26])表示匹配闰年的年份,即能被4整除但不能被100整除,或者能被400整除。

  • $ 表示匹配字符串的结束位置。



  1. 用户名:4-10位的用户名,包含下划线、连接符


/^[a-zA-Z0-9_-]{4,10}$/

总结


以上就是目前能想到的常用的正则,大家如果也有或者说常用的正则,也可以在评论区反馈,谢谢各位!


作者:原野风殇
来源:juejin.cn/post/7299376141451411490
收起阅读 »

耗时七天,我写完了自己的第一个小程序

web
一入红尘深似海。 自2016年参加工作时申请了第一张信用卡,至今已有七年矣。在这七年之中,自己信用卡的数量由1张逐渐增加到12张,后又慢慢减少到如今的5张。七年的卡海浮沉让我从初入社会的一无所有,到如今的负债累累。 当然,这篇文章并不是来记录自己七年的负债之旅...
继续阅读 »

一入红尘深似海。


自2016年参加工作时申请了第一张信用卡,至今已有七年矣。在这七年之中,自己信用卡的数量由1张逐渐增加到12张,后又慢慢减少到如今的5张。七年的卡海浮沉让我从初入社会的一无所有,到如今的负债累累。


当然,这篇文章并不是来记录自己七年的负债之旅,而是在经历了多年“钱都去哪儿了?”的内心呼唤后的心灵觉醒:还是要有个账本记账啊!


我的需求并不复杂:



1、可以快速的知道如今自己卡的总额度是多少,还需要还的欠款是多少(清楚负债情况)


2、可以快速知道每张卡的可用额度是多少,账单日是哪天(便于刷卡时明确知道该刷哪张卡,不至于出现今天刚刷了卡,明天就出了账单要还的现象)


3、可以知道每个月刷卡的总手续费是多少(清楚损益,明白每个月的损耗)


4、记录收支(了解每一分钱都去了哪里)



基于以上四个简单的需求,在尝试现在市面上十几款记账软件后,我惊奇的发现:竟然没有一款合适的软件可以满足我的需求!


于是,我做了一个【XXXX】的决定:自己来写一个工具吧!


然后,就诞生了我发布的第一个小程序:了账


小程序简介


了(liao)账是一款简洁的记账小程序。了账中的了字,是明了之意,清楚明白自己的账目,亦是了结之意,祝愿各位卡友早日上岸。


了账的页面相对简洁,有【未登录】【已登录】两种状态展示,如下图所示:


【未登录】


截屏2023-10-23 08.19.09.png


【已登录】


截屏2023-10-23 08.27.30.png


了账只有账户账单两个tab页,分别用来展示当前账户信息和查看收支记录。


账户页面展示了用户(目前只有作者本人😄)较为关心的几个数据:【当前额度】、【可用额度】、【现金额度】、【信用卡总览】、【当前账户】。


账单页面除了查看每一笔收支记录外,在顶部也展示了当月总出账、总入账信息。


截屏2023-10-29 14.53.05.png


新增账户


用户可通过【新增】按键创建账户,在账户页面,顶部账户信息会随之动态改变。如下图所示:


截屏2023-10-29 15.05.44.png


在新增页面,用户可点击账户类型修改新增账户的类型,目前【了账】共包含【信用卡】、【储蓄卡】、【支付宝】、【微信】、【其他】共五类账户。除信用卡外,其余四类账户额度统一归类为【现金额度】。


信用卡除了【固定额度】之外有时会给一部分【临时额度】,因此,在新增账户页面,除了【固定额度】之外,添加了【当前额度】字段。【当前额度】是包含【固定额度】和【临时额度】的账户总额度。


新增收支


当用户创建过账户后,就可以点击【账户】页面右下角【记一笔】浮块创建收支记录,并在【账单】页面查看。相应的,账户页面所展示的【账户信息】也会随之动态改变。如下图所示:


截屏2023-10-29 15.24.00.png


在记录收支时,不同的账户类型可选的账单类型也不相同。如:信用卡账户下可选择的账单类型为【日常支出】、【个人刷卡】、【账单还款】,储蓄卡账户下可选择的账单类型为【日常支出】、【日常收入】、【转账支出】、【转账收入】,支付宝账户微信账户其他账户则多出【提现】类型可供选择。如下图所示:


截屏2023-10-29 15.41.54.png


当账单类型为【日常支出】时,则须选择支出类型。目前共有【食】、【行】、【衣】、【住】、【娱乐】、【其他支出】六类支出可供选择。如下图所示:


截屏2023-10-29 15.35.20.png


信用卡账户账单类型为【个人刷卡】,以及支付宝账户微信账户其他账户账单类型为【提现】时,则需填写【收款金额】。收款账户为除【信用卡账户】外的其他账户,收款金额为除去手续费之外的实际到账金额。如下图所示:


截屏2023-10-29 15.53.45.png


账单的编辑、删除和账户的编辑、删除


用户可通过左滑对当前账户及当前收支进行编辑、删除。当收支被删除后,账户信息将会回退该笔收支。当账户被删除后,该账户下的所有收支将不可被编辑、删除。如下图所示:


截屏2023-11-11 12.27.15.png


账户详情和账单详情


点击每个账户和账单,可进入详情页,查看详情信息。如下图所示:


截屏2023-11-11 14.25.39.png


写在最后


账本只是工具,最主要的还是要诸位卡友调整好心态,量入为出。祝愿各位早日上岸!!!


写代码用了7天,备案发布将近一个月!!!最后上线认证居然还收了30块巨款!!!至今仍未明白:经历了实名注册小程序号,实名IPC备案后,最后上线认证的意义在哪里?难道只为承袭小马哥一贯的氪金传统?


作者:凡铁
来源:juejin.cn/post/7299733832413069347
收起阅读 »

看明白两个案例,秒懂事件循环

web
事件循环的任务队列包括宏任务和微任务 执行顺序就是:同步代码 -> 第一轮微任务 -> 第一轮宏任务 -> 第二轮微任务 ->... 宏任务有:setTimeout, setInterval, setImmediate, I/O, UI...
继续阅读 »

事件循环的任务队列包括宏任务微任务


执行顺序就是:同步代码 -> 第一轮微任务 -> 第一轮宏任务 -> 第二轮微任务 ->...


宏任务有:setTimeout, setInterval, setImmediate, I/O, UI rendering。


微任务有:process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)


两大原则:



  1. setTimeout和setInterval同源,且均优先于setImmediate执行

  2. nextTick队列会比Promie.then方法里面的代码先执行


简单案例


setTimeout(function() {
console.log('timeout1'); // 5-第一轮宏任务
})

new Promise(function(resolve) {
console.log('promise1'); // 1-同步代码
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2'); // 2-同步代码
}).then(function() {
console.log('then1'); // 4-第一轮微任务
})

console.log('global1'); // 3-同步代码


/*
promise1
promise2
global1
then1
timeout1
*/


综合案例


console.log('golb1'); // 1-同步代码

setTimeout(function() {
console.log('timeout1'); // 3.1-第一轮宏任务
process.nextTick(function() {
console.log('timeout1_nextTick'); // 3.3-第二轮微任务
})
new Promise(function(resolve) {
console.log('timeout1_promise'); // 3.2-第一轮宏任务
resolve();
}).then(function() {
console.log('timeout1_then') // 3.4-第二轮微任务
})
})

setImmediate(function() {
console.log('immediate1'); // 3.1-第一轮宏任务
process.nextTick(function() {
console.log('immediate1_nextTick'); // 3.3-第二轮微任务
})
new Promise(function(resolve) {
console.log('immediate1_promise'); // 3.2-第一轮宏任务
resolve();
}).then(function() {
console.log('immediate1_then') // 3.4-第二轮微任务
})
})

process.nextTick(function() {
console.log('glob1_nextTick');// 2.1-第一轮微任务
})
new Promise(function(resolve) {
console.log('glob1_promise');// 1-同步代码
resolve();
}).then(function() {
console.log('glob1_then') // 2.2-第一轮微任务
})

setTimeout(function() {
console.log('timeout2'); // 3.1-第一轮宏任务
process.nextTick(function() {
console.log('timeout2_nextTick'); // 3.3-第二轮微任务
})
new Promise(function(resolve) {
console.log('timeout2_promise'); // 3.2-第一轮宏任务
resolve();
}).then(function() {
console.log('timeout2_then') // 3.4-第二轮微任务
})
})

process.nextTick(function() {
console.log('glob2_nextTick');// 2.1-第一轮微任务
})
new Promise(function(resolve) {
console.log('glob2_promise');// 1-同步代码
resolve();
}).then(function() {
console.log('glob2_then')// 2.2-第一轮微任务
})

setImmediate(function() {
console.log('immediate2'); // 3.1-第一轮宏任务
process.nextTick(function() {
console.log('immediate2_nextTick'); // 3.3-第二轮微任务
})
new Promise(function(resolve) {
console.log('immediate2_promise'); // 3.2-第一轮宏任务
resolve();
}).then(function() {
console.log('immediate2_then') // 3.4-第二轮微任务
})
})

/*
(1-同步代码)
golb1
glob1_promise
glob2_promise
(2-第一轮微任务)
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
(3-第一轮宏任务)
(setTimeout)
timeout1
timeout1_promise
timeout1_nextTick
timeout1_then
timeout2
timeout2_promise
timeout2_nextTick
timeout2_then
(setImmediate)
immediate1
immediate1_promise
immediate1_nextTick
immediate1_then
immediate2
immediate2_promise
immediate2_nextTick
immediate2_then
*/


注:在Node 11前,Node的事件循环会与浏览器存在差异,以上面案例中的两个setTimeout为例:


//在Node 11前
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
// 在Node11后和浏览器
timeout1
timeout1_promise
timeout1_nextTick
timeout1_then
timeout2
timeout2_promise
timeout2_nextTick
timeout2_then

即在同一类任务分发器(如:多个setTimeout),在Node 11前,会先执行所有的nextTick,再到Promise.then;而在Node11后和浏览器,都是依次执行每个setTimeout,在同一个setTimeout里面先执行所有nextTick,再到Promise.then。


Refs:


mp.weixin.qq.com/s/m3a6vjp8-…


作者:星辰_Stars
来源:juejin.cn/post/7298325881731219496
收起阅读 »

面试题:小男孩毕业之初次面试

web
前言 看到身边的同学渐渐的都有了一些面试之后,我逐渐感到了焦虑,甚至都有些对自己感到不自信,之后在上周三的上午,终于时来运转,先是梭翱打电话来,之后就是美云和琻瑢那边的简历初筛通过,通知我面试,之后按照自己的回忆写下了一些感悟与题目,希望对你们有所帮助。 浙江...
继续阅读 »

前言


看到身边的同学渐渐的都有了一些面试之后,我逐渐感到了焦虑,甚至都有些对自己感到不自信,之后在上周三的上午,终于时来运转,先是梭翱打电话来,之后就是美云和琻瑢那边的简历初筛通过,通知我面试,之后按照自己的回忆写下了一些感悟与题目,希望对你们有所帮助。


浙江杭州(实习 130-160/天)



这是我的第一场面试,面试官问的都是vue的问题。这场面试全程懵逼下来的,因为我前面基本都在准备js和css方面,vue方面也就瞄了几眼,结果就是和面试官疯狂的扯。面试完之后反思,在自我介绍中一定要讲清楚自己使用了是vue2还是vue3,不熟悉或者面试前没准备好的知识点一定不要讲出来,全程懵下来血的教训。然后也是电话面试,所以在听面试官老师的问题方面可能有点费力。在看面试题的时候,不要死记硬背,可以根据自己熟悉的语句自己表达出来就行。



1. 说一下vue2和vue3生命周期的实现和它们的不同点?


每个Vue实例在创建时都会经过一系列的初始化过程,vue的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件


Vue2的生命周期函数




  • create阶段:vue实例被创建


    beforeCreate: 创建前,此时data和methods中的数据都还没有初始化


    created: 创建完毕,data中有值,未挂载




  • mount阶段: vue实例被挂载到真实DOM节点


    beforeMount:可以发起服务端请求,取数据


    mounted: 此时可以操作DOM




  • update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染


    beforeUpdate:更新前


    updated:更新后




  • destroy阶段:vue实例被销毁


    beforeDestroy:实例被销毁前,此时可以手动销毁一些方法


    destroyed:销毁后




上述生命周期钩子函数中,beforeCreate和created钩子函数在组件创建时只会执行一次,而beforeMount、mounted、beforeUpdate和updated钩子函数则会在组件的数据发生变化时多次执行。在组件销毁时,beforeDestroy和destroyed钩子函数也只会执行一次。


Vue3的生命周期函数




  • setup() : 开始创建组件之前,在 beforeCreate 和 created 之前执行,创建的是 data 和 method




  • mount阶段: vue实例被挂载到真实DOM节点


    onBeforeMount() : 组件挂载到节点上之前执行的函数;


    onMounted() : 组件挂载完成后执行的函数;




  • update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染


    onBeforeUpdate(): 组件更新之前执行的函数;


    onUpdated(): 组件更新完成之后执行的函数;




  • unmount阶段:vue实例被销毁


    onBeforeUnmount(): 组件卸载之前执行的函数;


    onUnmounted(): 组件卸载完成后执行的函数;




在Vue3中,beforeDestroy钩子函数被废弃,取而代之的是onUnmounted钩子函数。与Vue2不同,onUnmounted钩子函数在组件卸载之后调用,而不是在组件销毁之前调用。此外,Vue3还新增了一个onErrorCaptured钩子函数,用于处理子孙组件抛出的错误。


不同


1. vue3和vue2的生命周期函数名称


在Vue2中,我们熟悉的生命周期函数有:beforeCreate、created、beforeMountmounted、beforeUpdate、updated、 beforeDestroy、destroyed。而在Vue3中,这些函数名称被进行了重命名,变成了:beforeCreate->setup,created->setup,beforeMount->onBeforeMount,mounted->onMounted,beforeUpdate->onBeforeUpdate,updated->onUpdated,beforeUnmount ->onBeforeUnmount,unmounted ->onUnmounted。


重命名的原因是为了更好地反映生命周期的不同阶段,方便开发者进行理解和使用。


常用生命周期对比如下表所示。


vue2vue3
beforeCreate使用 setup()
created使用 setup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted

2. 新增和废弃生命周期函数


Vue3为我们提供了一些新的生命周期函数,这些函数可以帮助我们更好地管理组件,Vue3废弃了beforeDestroy钩子函数,并且新增了生命周期函数。这些新的生命周期函数分别是:


onRenderTracked:当渲染跟踪或依赖项跟踪时被调用。


onRenderTriggered:当渲染时触发其他渲染时,或者当在当前渲染中延迟调度的作业时被调用。


onErrorCaptured:当子组件抛出未处理的错误时被调用。这些新的生命周期函数可以帮助我们更好地调试、优化组件,提升应用的性能。


3. 使用hook函数代替生命周期函数


Vue3引入了新的API——Composition API,通过这个API可以使用hook函数来代替生命周期函数。 Composition API可以让我们更好地管理代码逻辑,将不同的功能划分为不同的小函数,便于维护和复用。hook函数在组件中的调用顺序与生命周期函数类似,但是更加灵活,可以根据需要进行组合和抽离。


4.v-if和v-for的优先级不同


vue2生命周期执行过程


生命周期.png


vue3生命周期执行过程


image.png


2. Vue2和Vue3数据更新时有什么不一样?


Proxy 替代 Object.defineProperty:在Vue2中,使用Object.defineProperty来拦截数据的变化,但是该方法存在一些缺陷,比如不能监听新增的属性和数组变化等。Vue3中使用了ES6中的Proxy来拦截数据的变化,能够完全监听数据变化,并且能够监听新增的属性。


批量更新:Vue2中,在数据变化时,会立即触发虚拟DOM的重渲染,如果在一个事件循环中连续修改多个数据,可能会造成性能问题。而Vue3中,使用了更高效的批量更新策略,会在下一个事件循环中统一处理数据变化,提高了性能。


更快的响应式系统:Vue3中使用了更快的响应式系统,能够更快地追踪依赖关系,并在数据变化时更快地更新视图。此外,Vue3还对Reactivity API进行了优化,使得开发者能够更灵活地使用响应式数据。


Composition API:Vue3中引入了Composition API,可以更好地组织代码逻辑,也可以更好地处理数据更新。通过使用setup函数和ref、reactive等函数,能够更方便地对数据进行监听和修改。


3. 为什么vue中更改对象和数组时,有时候页面没有进行更新




  1. 对象或数组未在初始时声明为响应式:在Vue中,只有在初始时声明为响应式的对象和数组才能进行监听和更新。如果在初始时没有声明为响应式,那么更改对象或数组时,Vue无法检测到变化,从而无法进行更新。




  2. 直接更改对象或数组的属性或元素:在Vue中,如果直接更改对象或数组的属性或元素,Vue无法检测到变化。因此,应该使用Vue提供的响应式方法来更改对象或数组的属性或元素,例如Vue.setVue.$set方法。




  3. 变异方法不会触发更新:Vue会对一些常用的数组变异方法进行封装,使其成为响应式的,例如pushpopshiftunshiftsplicesortreverse方法。但是,如果使用不在这个列表中的变异方法来更改数组,Vue就无法检测到变化。因此,应该尽可能使用Vue封装过的变异方法。




  4. 异步更新:在Vue中,更新是异步的。当数据发生变化时,Vue会将更新推迟到下一个事件循环中。因此,如果在一个事件循环中进行多次数据更改,Vue只会进行一次更新。如果需要在一次事件循环中进行多次数据更改,请使用Vue.nextTick方法。




总之,为了确保Vue可以正确地监听和更新对象和数组,应该在初始时将它们声明为响应式,避免直接更改对象或数组的属性或元素,尽可能使用Vue提供的响应式方法,避免使用不在Vue封装列表中的变异方法,以及注意异步更新的特性。


4. 你在项目里面是怎么使用vuex/pinia?


在我的项目中我使用的是pinia


首先,先通过npm安装pinia


npm install pinia

其次,在根组件app.vue中创建Pinia实例并将其注册为应用程序的插件


import { createPinia } from 'pinia'
const pinia = createPinia()
createApp(App).use(pinia).mount('#app')

接着,在src目录下创建一个store文件夹中的index.js,而在使用Pinia时,通过引入Pinia中的defineStore来定义一个store实例,类似于Vuex的store。然后我定义了不同的子仓库并暴露(export),来存储对应不同的页面所需的数据与操作,之后再返回(return)数据和操作。而在组件中使用Pinia时,需要通过引入,useStore辅助函数获取store实例,并将状态、操作和获取器映射到组件中,以便使用。


import { defineStore } from "pinia";
import { reactive } from "vue";

export const useUserStore = defineStore('user', () => {
const state = reactive({gridList:[]})
const loadUser = async () => {}
return {
state,
loadUser
}
})

import { useUserStore } from "@/store/user";

const userStore = useUserStore();
const gridList = computed(() => userStore.state.gridList);

上海(实习 100-150/天)



该面试是通过视频面试,面试的时候题目相对比较简单,都是一些基础的问题,这也就给了我极大的自信



1. JS的Event Loop你能给我介绍下吗?


因为JS是单线程的语言,为了防止一个函数执行时间过长阻塞后面的代码,所以就需要Event Loop这个事件环的运行机制。


当执行一段有同步又有异步的代码时,会先将同步任务压入执行栈中,然后把异步任务放入异步队列中等待执行,微任务放到微任务队列,宏任务放到宏任务队列,依次执行。执行完同步任务之后,Event Loop会先把微任务队列执行清空,微任务队列清空后,进入宏任务队列,取宏任务队列的第一个项任务进行执行,执行完之后,查看微任务队列是否有任务,有的话,清空微任务队列。然后再执行宏任务队列,反复微任务宏任务队列,直到所有队列任务执行完毕。


PS: 答完了基本的答案之后,最好可以往下继续延申,不要让面试成为一问一答,这样你的面试就会变的比较丰满,让面试官不至于太枯燥,直到面试官让你停为止。



异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列先执行。


微任务队列的代表就是,Promise.thenMutationObserver,宏任务的话就是setImmediate setTimeout setInterval



2. 渲染页面的重绘回流你能给我讲一下吗?




  • 重排/回流(Reflow):当DOM元素发生了规格大小,位置,增删改的操作时,浏览器需要重新计算元素的几何属性,重新生成布局,重新排列元素。




  • 重绘(Repaint): 当一个DOM元素的外观样式发生改变,但没有改变布局,重新把DOM元素的样式渲染到页面的过程。





重排和重绘它们会破坏用户体验,并且让UI展示非常迟缓,而在两者无法避免的情况下,重排的性能影响更大,所以一般选择代价更小的重绘。


『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。



上海(实习 200-210/天)



这场面试很正常,自我感觉含金量也比较高,通过视频面试能够知道,面试官老师人也长得挺帅的,说话和蔼,讲真,人真的挺好的。不过自己还会犯傻,走进思维误区,没有理解清面试官老师的问题,所以在面试中如果没听清楚问题,千万一定要再问一下面试官。



1. 响应式开发你了解吗?响应式是如何实现的呢?


响应式开发是一种设计和开发网站或应用程序的方法,使其能够在不同设备上以适应性和灵活性的方式呈现。它可以确保网站或应用程序在各种屏幕尺寸、浏览器和设备上都能提供良好的用户体验。


响应式开发的实现基于使用CSS媒体查询、弹性布局和流体网格等技术。以下是一些主要的实现方法:




  1. CSS媒体查询:使用CSS媒体查询可以检测设备的屏幕尺寸、分辨率和方向等特性,并根据这些特性应用不同的样式规则。通过定义不同的CSS样式,可以使网页在不同的设备上以不同的方式呈现。




  2. 弹性布局:即(display:flex),使用弹性布局(flexbox)可以创建灵活的布局结构,使内容能够根据屏幕尺寸进行自动调整。弹性布局使得元素的大小、位置和间距能够根据可用空间进行自适应。




  3. 网格布局:即(display:grid),使用流体网格(fluid grid)可以创建基于相对单位(如百分比)的网格系统,使页面的布局能够根据屏幕大小进行缩放和调整。这样可以确保内容在不同屏幕尺寸上均匀分布和对齐。




2. 媒体查询这个你了解吗?


我在使用less预编译样式中使用过媒体查询(这里提一嘴自己使用过less或者其他的预编译),媒体查询使用@media规则来定义,其语法如下:


@media mediatype and|not|only (media feature) {
/* CSS样式规则 */
}

其中,mediatype指定了媒体类型,如screen表示屏幕媒体、print表示打印媒体等。andnotonly是逻辑运算符,用于组合多个条件。media feature表示设备的特性,如width表示屏幕宽度、orientation表示屏幕方向等。


下面是一些常用的媒体特性:



  • width:屏幕宽度。

  • height:屏幕高度。

  • device-width:设备屏幕宽度。

  • device-height:设备屏幕高度。

  • orientation:屏幕方向(横向或纵向)。

  • aspect-ratio:屏幕宽高比。

  • color:设备的颜色位深。

  • resolution:屏幕分辨率。


通过结合不同的媒体特性和条件,可以根据设备的不同特性来应用不同的CSS样式。例如,可以使用媒体查询来定义在屏幕宽度小于某个阈值时应用的样式,或者根据屏幕方向调整布局等。


以下是一个示例,演示如何使用媒体查询在屏幕宽度小于600px时应用特定的样式:


@media screen and (max-width: 600px) {
/* 在屏幕宽度小于600px时应用的样式 */
body {
font-size: 14px;
}
/* 其他样式规则 */
}

这样,当浏览器窗口宽度小于600px时,body元素的字体大小将被设置为14px。


3. CSS的伪元素你知道是什么东西吗?


伪元素是CSS中的一种特殊选择器,用于向选中的元素的特定部分添加样式,而不需要在HTML结构中添加额外的元素。伪元素使用双冒号::作为标识符,用于区分伪类(pseudo-class)和伪元素。(在旧版本的CSS中,单冒号:也被用作伪元素的标识符,但在CSS3规范中,建议使用双冒号以区分伪类和伪元素。)


以下是一些常用的CSS伪元素:



  1. ::before:在选中元素的内容之前插入一个生成的内容。

  2. ::after:在选中元素的内容之后插入一个生成的内容。


这些伪元素可以与CSS的属性和样式一起使用,例如contentcolorbackground等,以为选中的元素的特定部分添加样式。


以下是一个示例,演示如何使用伪元素为元素的内容之前插入一个生成的内容并应用样式:


p::before {
content: "前缀:";
font-weight: bold;
color: blue;
}

在上述示例中,::before伪元素被应用于<p>元素,它在该段落的内容之前插入了一个生成的文本"前缀:",并为该生成的文本应用了加粗字体和蓝色的颜色。


4. 介绍一下HTML5的特有的标签?



  1. 语义化标签



  • <article>:用于表示独立的、完整的文章内容。

  • <section>:用于表示页面或应用程序中的一个区域,可以包含一个标题。

  • <header>:用于表示页面或应用程序的标题,通常包含logo和导航。

  • <footer>:用于表示页面或应用程序的页脚部分,通常包含版权信息、联系方式等。

  • <nav>:用于表示导航链接的集合,通常包含一组指向其他页面的链接。

  • <aside>:用于表示页面或应用程序的旁边栏,通常包含相关的信息、广告、链接等。



  1. <video>:用于嵌入视频文件,可以使用<source>标签指定多个视频文件,以便在不同的浏览器和设备上播放。

  2. <audio>:用于嵌入音频文件,可以使用<source>标签指定多个音频文件,以便在不同的浏览器和设备上播放。

  3. <canvas>:用于创建绘图区域,可以使用JavaScript在上面绘制图形、动画等。

  4. <progress>:用于显示进度条,表示任务完成的进度。


5. 你如果要做一个搜索引擎比较友好的页面,应该是要做到些什么东西呢?




  1. 使用语义化的HTML标记:使用适当的HTML标签来正确表示页面的结构,如使用<header><nav><article>等。




  2. 使用有意义的标题:使用恰当的标题标签(<h1><h2>等)来突出页面的主题和内容。




  3. 提供关键词和描述:在HTML文档中,可以通过<meta>标签来定义各种属性,比如页面的描述和关键字。


    keywords:向搜索引擎说明你的网页的关键词


     `<meta name="keyword" content="前端,面试,小厂">`

    description:告诉搜索引擎你的站点的主要内容


    <meta name="description" content="页面描述,包含关键字和吸引人的内容">



  4. 使用合适的图像标签:为图片使用适当的alt属性,描述图片内容,方便搜索引擎理解图像。




  5. 使用服务端渲染(SSR)的框架,比如vue中的Nuxtreact中的Next,即在服务端生成完整的 HTML 页面,并将其发送给浏览器。这使得搜索引擎可以更好地理解和索引页面的内容,因为它们可以直接看到渲染后的页面。




6. 介绍一下flex的布局吧?


## 阮一峰老师有一个博客,专门讲解一个flex布局,你可以讲一下flex布局吗?


7. 后端和前端的一些交互,你了解是什么东西?


后端和前端之间的交互通常通过前后端分离的架构来实现,其中前端负责展示界面和用户交互,后端负责处理数据和逻辑操作。


以下是一些常见的后端和前端交互的方式和技术:




  1. RESTful API:使用基于HTTP的RESTful API,前端可以向后端发送请求并获取数据。后端提供API接口,通过GETPOSTPUTDELETE等HTTP方法来处理前端请求,并返回相应的数据。前端可以使用Ajax、Fetch API或axios等工具来发送请求和处理响应。




  2. 数据传输格式前后端交互时需要使用一致的数据传输格式。常见的数据格式包括JSON(JavaScript Object Notation)和XML(eXtensible Markup Language)。前端可以发送数据请求给后端,后端将数据以指定的格式进行封装和返回给前端。




  3. 然后我还使用过nodejs中的koa洋葱模型简单搭建过一个MVC结构的服务器。




8. 那你有遇到过跨域问题吗?实际解决方法?


我分别说了




  • JSONP:在DOM文档中,使用<script>标签,但却缺点只能发 GET 请求并且容易受到XSS跨站脚本攻击




  • CORS:通过在服务器配置响应头,Access-Control-Allow-xxx字段来设置访问的白名单、可允许访问的方式等




  • postMessage




  • html原生的websocket




  • 代理 白嫖即食:构建工具的proxy代理配置区别(解决跨域)




讲了这些东西之后,面试官就让我说一下实际解决方法,像jsonp,postMeassage都不是正常的


然后我就把整个CORS跨域的过程给讲了一遍,包含了浏览器的跨域拦截



首先,浏览器进行了一个跨域请求,向服务器发送了一个预检(options)请求,服务器会在响应头部中设置Access-Control-Allow-Origin和Access-Control-Allow-Methods等配置,告知浏览器是否允许跨域请求。如果该页面满足服务器设置的白名单和可允许访问的方式,那么服务器就允许跨域访问,浏览器就会接受响应,进行真实的跨域请求,否则就会报错。



面试基本必问问题


1. 你有什么想问我的吗?(问到这里一场面试结束了)




  1. 公司团队使用的技术栈有哪些?




  2. 如果我面试通过后,公司是否有人带,主要做些什么




  3. 公司团队提交代码的工具有什么要求吗?




  4. 把之前没答上来的问题可以再问一遍(让面试官感到你很好学)




2. 你写项目的时候碰到过印象里比较深刻的一些bug或困难,你怎么解决的?


其实这部分可以从侧面分析这个问题,问你遇到的bug可能一时半会儿不知道怎么回答,但如果问你是如何实现项目中的某个功能,这时候就好回答了,只需要转换回答成没有这个功能代码会出现什么问题。所以面试官不是问你有什么bug,而是你在项目中有哪些亮点。



前端中常见的一些bug



  1. JavaScript 错误:在应用程序中使用的 JavaScript 代码可能包含语法错误或逻辑错误,这些错误会导致应用程序在执行时出现问题,从而导致性能问题。

  2. DOM 操作错误:通过 JavaScript 操作文档对象模型 (DOM) 可以更新应用程序中的 HTML 元素。但是,如果 DOM 操作不正确或在操作过程中执行了太多的操作,可能会导致性能问题。

  3. 页面重绘:当用户与页面交互时,浏览器会执行重新绘制和重排操作。如果页面包含太多的重绘操作或页面重排操作,则可能导致性能问题。

  4. 图像和资源加载:在加载图像和其他资源时,如果没有正确管理缓存或使用适当的图像格式,则可能导致性能问题。

  5. 前端框架错误:使用前端框架时,可能会出现错误或不良的编码实践,这些问题可能会导致性能问题。



axios响应拦截


遇到bug:我是使用mockjs来模拟后端的接口,当时我在设置端口的返回值时,我返回数据有一个状态码以及把json数据中export出来的detail数据添加到data这个需要返回的数据(代码如下),这导致我在获取接口里的数据时需要多.data(引用一层data),当时我没意识,结果一直获取不到数据。


解决办法:通过使用axios进行请求和响应,并在响应的时候设置一个拦截,对响应进行一番处理之后就可以直接拿到接口返回的值,而不会导致接口返回的值不会有太多的嵌套了。


Mock.mock(/\/detail/, 'get', () => {
return {
code: 0, // 返回状态码
data: detail // 返回数据
}
})

import axios from "axios";
// 响应拦截器
axios.interceptors.response.use((res) => {
return res.data
})

图片和组件的懒加载


遇到的bug:我做的项目使用了很多的组件页面和大量的图片,导致在加载页面时耗时比较久,以及在页面的切换时很多暂时不需要的页面组件一次性全部加载了,导致整个项目的性能非常差。


解决办法


图片懒加载:在App.vue中引入VueLazy并且使用app.use启用它,然后把图片中的src改成v-lazy


<img :src="xxx.png">

改成


<img v-lazy="xxx.png">

页面组件懒加载:在router配置中的component,把直接在代码一开始引入组件页面,改成箭头函数式引入。


    import Home from '@/views/Home/Home.vue' 
{
path: '/',
component: Home
},

改成


    {
path: '/',
component: () => import('@/views/Home/Home.vue')
},

搜索界面节流


遇到的bug:在搜索界面的时候,当我一直点击搜索时,它会频繁的进行请求,造成了不必要的性能损耗。


解决办法:使用loadash库中的节流API,进行对触发搜索事件进行节流,防止用户进行频繁的搜索请求导致性能损耗。



import _ from 'lodash'

const value = ref(null)

const ajax1 = () => {
console.log('开始搜索,搜索内容为' + value.value)
}

let debounceAjax1 = _.debounce(ajax1, 1000)

const onSearch = () => {
if (!value.value) {
showToast('搜索内容为空,请输入内容')
return
}
debounceAjax1()
}

404页面


遇到的bug:当输入url中没有在路由配置中配置过的路径时,页面它会出现空白,并且浏览器发出警告,如果我这个项目上线的话,可能会造成用户的体验不友好和搜索引擎不友好。


解决办法:在路由配置中再配置一个404页面的路径,这样就能使用户不管怎么输入不合规的url后,都会提示用户输错了网址。


    {
path: '/404',
name: 'NotFound',
component: () => import('@/views/NotFound/Index.vue')
},
// 所有未定义路由,全部重定向到404页
{
path: '/:pathMatch(.*)',
redirect: '/404'
}

结语


面试,说到底,迈开第一步其实是最重要的,别想那么多,要抱着反正有那么多家公司,我没必要非要去你这一家的心态去面试,把面试官当作一个久久未联系过的老朋友,突然有一天碰到了聊起天。面试完之后一定及时的整理复盘,不断地让自己变得更加牢固。


作者:吃腻的奶油
来源:juejin.cn/post/7233307834456375353
收起阅读 »

我给项目加了性能守卫插件,同事叫我晚上别睡的太死

web
引言 给组内的项目都在CICD流程上更新上了性能守卫插件,效果也还不错,同事还疯狂夸奖我 接下里进入我们的此次的主题吧 由于我组主要是负责的是H5移动端项目,老板比较关注性能方面的指标,比如首页打开速度,所以一般会关注FP,FCP等等指标,所以一般项目写完...
继续阅读 »



引言


给组内的项目都在CICD流程上更新上了性能守卫插件,效果也还不错,同事还疯狂夸奖我


WX20230708-170807@2x.png


接下里进入我们的此次的主题吧



由于我组主要是负责的是H5移动端项目,老板比较关注性能方面的指标,比如首页打开速度,所以一般会关注FP,FCP等等指标,所以一般项目写完以后都会用lighthouse查看,或者接入性能监控系统采集指标.



WX20230708-141706@2x.png


但是会出现两个问题,如果采用第一种方式,使用lighthouse查看性能指标,这个得依赖开发自身的积极性,他要是开发完就Merge上线,你也不知道具体指标怎么样。如果采用第二种方式,那么同样是发布到线上才能查看。最好的方式就是能强制要求开发在还没发布的时候使用lighthouse查看一下,那么在什么阶段做这个策略呢。聪明的同学可能想到,能不能在CICD构建阶段加上策略。其实是可以的,谷歌也想到了这个场景,提供性能守卫这个lighthouse ci插件


性能守卫



性能守卫是一种系统或工具,用于监控和管理应用程序或系统的性能。它旨在确保应用程序在各种负载和使用情况下能够提供稳定和良好的性能。



Lighthouse是一个开源的自动化工具,提供了四种使用方式:




  • Chrome DevTools




  • Chrome插件




  • Node CLI




  • Node模块




image.png


其架构实现图是这样的,有兴趣的同学可以深入了解一下


这里我们我们借助Lighthouse Node模块继承到CICD流程中,这样我们就能在构建阶段知道我们的页面具体性能,如果指标不合格,那么就不给合并MR


剖析lighthouse-ci实现


lighthouse-ci实现机制很简单,核心实现步骤如上图,差异就是lighthouse-ci实现了自己的server端,保持导出的性能指标数据,由于公司一般对这类数据敏感,所以我们一般只需要导出对应的数据指标JSON,上传到我们自己的平台就行了。


image.png


接下里,我们就来看看lighthouse-ci实现步骤:





    1. 启动浏览器实例:CLI通过Puppeteer启动一个Chrome实例。


    const browser = await puppeteer.launch();




    1. 创建新的浏览器标签页:接着,CLI创建一个新的标签页(或称为"页面")。


    const page = await browser.newPage();




    1. 导航到目标URL:CLI命令浏览器加载指定的URL。


    await page.goto('https://example.com');




    1. 收集数据:在加载页面的同时,CLI使用各种Chrome提供的API收集数据,包括网络请求数据、JavaScript执行时间、页面渲染时间等。





    1. 运行审计:数据收集完成后,CLI将这些数据传递给Lighthouse核心,该核心运行一系列预定义的审计。





    1. 生成和返回报告:最后,审计结果被用来生成一个JSON或HTML格式的报告。


    const report = await lighthouse(url, opts, config).then(results => {
    return results.report;
    });




    1. 关闭浏览器实例:报告生成后,CLI关闭Chrome实例。


    await browser.close();



// 伪代码
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const {URL} = require('url');

async function run() {
// 使用 puppeteer 连接到 Chrome 浏览器
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

// 新建一个页面
const page = await browser.newPage();

// 在这里,你可以执行任何Puppeteer代码,例如:
// await page.goto('https://example.com');
// await page.click('button');

const url = 'https://example.com';

// 使用 Lighthouse 进行审查
const {lhr} = await lighthouse(url, {
port: new URL(browser.wsEndpoint()).port,
output: 'json',
logLevel: 'info',
});

console.log(`Lighthouse score: ${lhr.categories.performance.score * 100}`);

await browser.close();
}

run();

导出的HTML文件


image.png


导出的JSON数据


image.png


实现一个性能守卫插件


在实现一个性能守卫插件,我们需要考虑以下因数:





    1. 易用性和灵活性:插件应该易于配置和使用,以便它可以适应各种不同的CI/CD环境和应用场景。它也应该能够适应各种不同的性能指标和阈值。





    1. 稳定性和可靠性:插件需要可靠和稳定,因为它将影响整个构建流程。任何失败或错误都可能导致构建失败,所以需要有强大的错误处理和恢复能力。





    1. 性能:插件本身的性能也很重要,因为它将直接影响构建的速度和效率。它应该尽可能地快速和高效。





    1. 可维护性和扩展性:插件应该设计得易于维护和扩展,以便随着应用和需求的变化进行适当的修改和更新。





    1. 报告和通知:插件应该能够提供清晰和有用的报告,以便开发人员可以快速理解和处理任何性能问题。它也应该有一个通知系统,当性能指标低于预定阈值时,能够通知相关人员。





    1. 集成:插件应该能够轻松集成到现有的CI/CD流程中,同时还应该支持各种流行的CI/CD工具和平台。





    1. 安全性:如果插件需要访问或处理敏感数据,如用户凭证,那么必须考虑安全性。应使用最佳的安全实践来保护数据,如使用环境变量来存储敏感数据。




image.png


// 伪代码
//perfci插件
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const { port } = new URL(browser.wsEndpoint());

async function runAudit(url) {
const browser = await puppeteer.launch();
const { lhr } = await lighthouse(url, {
port,
output: 'json',
logLevel: 'info',
});
await browser.close();

// 在这里定义你的性能预期
const performanceScore = lhr.categories.performance.score;
if (performanceScore < 0.9) { // 如果性能得分低于0.9,脚本将抛出错误
throw new Error(`Performance score of ${performanceScore} is below the threshold of 0.9`);
}
}

runAudit('https://example.com').catch(console.error);


使用


name: CI
on: [push]
jobs:
lighthouseci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install && npm install -g @lhci/cli@0.11.x
- run: npm run build
- run: perfci autorun


性能审计


const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const nodemailer = require('nodemailer');

// 配置邮件发送器
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com',
pass: 'your-password',
},
});

// 定义一个函数用于执行Lighthouse审计并处理结果
async function runAudit(url) {
// 通过Puppeteer启动Chrome
const browser = await puppeteer.launch({ headless: true });
const { port } = new URL(browser.wsEndpoint());

// 使用Lighthouse进行性能审计
const { lhr } = await lighthouse(url, { port });

// 检查性能得分是否低于阈值
if (lhr.categories.performance.score < 0.9) {
// 如果性能低于阈值,发送警告邮件
let mailOptions = {
from: 'your-email@gmail.com',
to: 'admin@example.com',
subject: '网站性能低于阈值',
text: `Lighthouse得分:${lhr.categories.performance.score}`,
};

transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}

await browser.close();
}

// 使用函数
runAudit('https://example.com');


接下来,我们分步骤大概介绍下几个核心实现


数据告警


// 伪代码
const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const nodemailer = require('nodemailer');

// 配置邮件发送器
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com',
pass: 'your-password',
},
});

// 定义一个函数用于执行Lighthouse审计并处理结果
async function runAudit(url) {
// 通过Puppeteer启动Chrome
const browser = await puppeteer.launch({ headless: true });
const { port } = new URL(browser.wsEndpoint());

// 使用Lighthouse进行性能审计
const { lhr } = await lighthouse(url, { port });

// 检查性能得分是否低于阈值
if (lhr.categories.performance.score < 0.9) {
// 如果性能低于阈值,发送警告邮件
let mailOptions = {
from: 'your-email@gmail.com',
to: 'admin@example.com',
subject: '网站性能低于阈值',
text: `Lighthouse得分:${lhr.categories.performance.score}`,
};

transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}

await browser.close();
}

// 使用函数
runAudit('https://example.com');

处理设备、网络等不稳定情况


// 伪代码

// 网络抖动
const { lhr } = await lighthouse(url, {
port,
emulatedFormFactor: 'desktop',
throttling: {
rttMs: 150,
throughputKbps: 1638.4,
cpuSlowdownMultiplier: 4,
requestLatencyMs: 0,
downloadThroughputKbps: 0,
uploadThroughputKbps: 0,
},
});


// 设备
const { lhr } = await lighthouse(url, {
port,
emulatedFormFactor: 'desktop', // 这里可以设定为 'mobile' 'desktop'
});


用户登录态问题



也可以让后端同学专门提供一条内网访问的登录态接口环境,仅用于测试环境



const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const fs = require('fs');
const axios = require('axios');
const { promisify } = require('util');
const { port } = new URL(browser.wsEndpoint());

// promisify fs.writeFile for easier use
const writeFile = promisify(fs.writeFile);

async function runAudit(url, options = { port }) {
// 使用Puppeteer启动Chrome
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 访问登录页面
await page.goto('https://example.com/login');

// 输入用户名和密码
await page.type('#username', 'example_username');
await page.type('#password', 'example_password');

// 提交登录表单
await Promise.all([
page.waitForNavigation(), // 等待页面跳转
page.click('#login-button'), // 点击登录按钮
]);

// 运行Lighthouse
const { lhr } = await lighthouse(url, options);

// 保存审计结果到JSON文件
const resultJson = JSON.stringify(lhr);
await writeFile('lighthouse.json', resultJson);

// 上传JSON文件到服务器
const formData = new FormData();
formData.append('file', fs.createReadStream('lighthouse.json'));

// 上传文件到你的服务器
const res = await axios.post('https://your-server.com/upload', formData, {
headers: formData.getHeaders()
});

console.log('File uploaded successfully');

await browser.close();
}

// 运行函数
runAudit('https://example.com');

总结


性能插件插件还有很多需要考虑的情况,所以,不懂还是来私信问我吧,我同事要请我吃饭去了,不写了。


作者:linwu
来源:juejin.cn/post/7253331974051823675
收起阅读 »

聊聊深色模式(Dark Mode)

web
什么是深色模式 深色模式(Dark Mode),或者叫暗色模式,黑夜模式,是和日常使用的浅色(亮色)模式(Light Mode)相对应的一种UI主题。 深色模式最早来源于人机交互领域的研究和实践,从2018年左右开始,Apple推出了iOS 13,其中包含了系...
继续阅读 »

什么是深色模式


深色模式(Dark Mode),或者叫暗色模式,黑夜模式,是和日常使用的浅色(亮色)模式(Light Mode)相对应的一种UI主题。


深色模式最早来源于人机交互领域的研究和实践,从2018年左右开始,Apple推出了iOS 13,其中包含了系统级别的深色模式,可以将整个系统的界面切换为暗色调。


Google也在Android 10中推出了类似的深色模式功能,使深色模式得到了更广泛的应用和推广。


iOS官网的深色模式示例


iOS官网的深色模式示例


它不是简单的把背景变为黑色,文字变为白色,而是一整套的配色主题,这种模式相比浅色模式更加柔和,可以减少亮度对用户眼睛造成的刺激和疲劳。


随着越来越多的应用开始支持深色模式,作为开发也理应多了解下深色模式。


首先,怎么打开深色模式


在说怎么实现之前,先来说说我们要怎么打开深色模式,一般来说只需要在系统调节亮度的地方就可以调节深色模式,具体我们可以看各个系统的官方网站即可:
如何打开深色模式



但是在开发调试调试时,不断切换深色模式可能比较麻烦,这时浏览器就提供了一种模拟系统深色模式的方法,可以让当前的Web页面临时变为深色模式,以Chrome为例:
浏览器模拟深色/浅色模式



  1. 打开Chrome DevTools

  2. Command+Shift+P

  3. 输入dark或者light

  4. 打开深色或者浅色模式打开深色模式打开浅色模式


不过要注意的是,浏览器DevTools里开启深色模式,在关闭开发者工具后就会失效。


自动适配 - 声明页面支持深色模式


其实,在支持深色模式的浏览器中,有一套默认的深色模式,只需要我们在应用中声明,即可自动适配深色模式,声明有两种方式:


1. 添加color-schememeta标签


在HTML的head标签中增加color-schememeta标签,如下所示:


<!--
The page supports both dark and light color schemes,
and the page author prefers light.
-->

<meta name="color-scheme" content="light dark">

通过上述声明,告诉浏览器这个页面支持深色模式和浅色模式,并且页面更倾向于浅色模式。在声明了这个之后,当系统切换到深色模式时,浏览器将会把我们的页面自动切换到默认的深色模式配色,如下所示:
左边浅色,右边是浏览器自动适配的深色


左边浅色,右边是浏览器自动适配的深色


2. 在CSS里添加color-scheme属性


/*
The page supports both dark and light color schemes,
and the page author prefers light.
*/

:root {
color-scheme: light dark;
}

通过上面在:root元素上添加color-scheme属性,值为light dark,可以实现和meta标签一样的效果,同时这个属性不只可用于:root级别,也可用于单个元素级别,比meta标签更灵活。


但是提供color-schemeCSS属性需要首先下载CSS(如果通过<link rel="stylesheet">引用)并进行解析,使用meta可以更快地使用所需配色方案呈现页面背景。两者各有优劣吧。


自定义适配


1. 自动适配的问题


在上面说了我们可以通过一些标签或者CSS属性声明,来自动适配深色模式,但是从自动适配的结果来看,适配的并不理想:
左边浅色,右边是浏览器自动适配的深色


左边浅色,右边是浏览器自动适配的深色




  • 首先是默认的黑色字体,到深色模式下变成了纯白色#FFFFFF,和黑色背景(虽然说不是纯黑)对比起来很扎眼,在一些设计相关的文章[1][2]里提到,深色模式下避免使用纯黑和纯白,否则更容易使人眼睛👁疲劳,同时容易在页面滚动时出现拖影:


    滚动时出现拖影,图片来源「即刻」




滚动时出现拖影,图片来源「即刻」




  • 自动适配只能适配没有指定颜色和背景色的内容,比如上面的1、2、3级文字还有背景,没有显式设置colorbackground-color


    对于设置了颜色和背景色(这种现象在开发中很常见吧)的内容,就无法自动适配,比如上面的7个色块的背景色,写死了颜色,但是色块上的文字没有设置颜色。最终在深色渲染下渲染出的效果就是,色块背景色没变,但是色块上的文字变成了白色,导致一些文字很难看清。




所以,最好还是自定义适配逻辑,除了解决上面的问题,还可以加一下其他的东西,比如加一些深浅色模式变化时的过渡动画等。


2. 如何自定义适配


自定义适配有两种方式,CSS媒体查询和通过JS监听主题模式


1). CSS媒体查询


prefers-color-scheme - CSS:层叠样式表 | MDN
我们可以通过在CSS中设置媒体查询@media (prefers-color-scheme: dark),来设置深色模式下的自定义颜色。比如:


.textLevel1 {
color: #404040;
margin-bottom: 0;
}
.textLevel2 {
color: #808080;
margin-bottom: 0;
}
.textLevel3 {
color: #bfbfbf;
margin-bottom: 0;
}

@media (prefers-color-scheme: dark) {
.textLevel1 {
color: #FFFFFF;
opacity: 0.9;
}
.textLevel2 {
color: #FFFFFF;
opacity: 0.6;
}
.textLevel3 {
color: #FFFFFF;
opacity: 0.3;
}
}

通过媒体查询设置元素在深色模式下的1、2、3级文字的颜色,在浅色模式下设置不同的颜色,在深色模式下,增加不透明度:


截屏2023-03-12 下午6.04.18.png左边的是自动适配的浅色深色,右边是自定义适配的浅色深色


左边的是自动适配的浅色深色,右边是自定义适配的浅色深色


对于prefers-color-scheme的兼容性也不必担心,主流浏览器基本都支持了:


截屏2023-03-12 下午6.10.07.png


2). JS监听主题颜色


Window.matchMedia() - Web API 接口参考 | MDN


通过CSS媒体查询适配深色模式已经很方便了,完全不需要修改JS代码逻辑,那为什么还需要JS监听主题颜色呢?


因为通过CSS媒体查询虽然方便,但是只能跟随系统的主题颜色变化,假设用户想要类似于下面的自定义主题颜色,不跟随系统变化,或者业务上想做一些什么高级的操作,那么CSS媒体查询就无能为力了。


7b368843-dd42-4b4b-bc73-6d445de78923.gif


在JS中,可以通过window.matchMedia('(prefers-color-scheme: dark)').match来判断当前的颜色主题:


const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

if (darkModeMediaQuery.matches) {
// dark mode
} else {
// light mode
}

darkModeMediaQuery.addEventListener('change', (event: MediaQueryListEvent) => {
if (event.matches) {
// dark mode
} else {
// light mode
}
});

如果使用的是React,还可以写一个自定义hooks,来监听当前的颜色主题:


export enum EColorScheme {
LIGHT = 'light',
DARK = 'dark',
}

export const useColorScheme = (): EColorScheme => {
const [scheme, setScheme] = useState(
darkModeMediaQuery.matches ? EColorScheme.DARK : EColorScheme.LIGHT
);

useEffect(() => {
const listener = (event: MediaQueryListEvent) => {
setScheme(event.matches ? EColorScheme.DARK : EColorScheme.LIGHT);
};
darkModeMediaQuery.addEventListener('change', listener);
return () => {
darkModeMediaQuery.removeEventListener('change', listener);
};
}, []);

return scheme;
};

一些题外话


我写完这个hooks之后,有一次偶然用Chat GPT让它写一段监听深色模式的代码,写成hooks,最后生成的代码和这个几乎一模一样,Chat GPT还是有点东西的:


import { useState, useEffect } from 'react';

type ThemeType = 'light' | 'dark';

function useTheme(): ThemeType {
const [theme, setTheme] = useState<ThemeType>(
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
);

useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const handleChange = () => setTheme(mediaQuery.matches ? 'dark' : 'light');

mediaQuery.addEventListener('change', handleChange);

return () => mediaQuery.removeEventListener('change', handleChange);
}, []);

return theme;
}

export default useTheme;

window.matchMedia的兼容性也挺好的:


截屏2023-03-12 下午7.03.48.png
通过JS监听颜色主题变化之后,那可玩性就很多了,我们可以通过下面这些方式来适配深色模式:




  • 动态添加类名覆盖样式


    通过判断深色模式来添加一个深色模式的类名,覆盖浅色模式样式:


    <div
    className={classnames(
    style.wrapper,
    scheme === EColorScheme.DARK && style.darkModeWrapper
    )}
    >
    {/* some code here */}
    </div>



  • 对于深色模式直接引用不同的CSS资源文件




  • 用一些第三方的库,比如postcss-darkmode




回到上面话题,通过JS可以监听到系统的颜色主题,那怎么实现用户主动选择颜色主题,不随系统的改变呢?其实也很简单,可以在本地store中设置一个颜色主题的值,用户设置了就优先选用store里的,没有设置就跟随系统,以上面的hooks为例:


export const useColorScheme = (): EColorScheme => {
// 从 store 中取出用户手动设置的主题
const manualScheme = useSelector(selectManualColorScheme);
const [scheme, setScheme] = useState(
darkModeMediaQuery.matches ? EColorScheme.DARK : EColorScheme.LIGHT
);

useEffect(() => {
const listener = (event: MediaQueryListEvent) => {
setScheme(event.matches ? EColorScheme.DARK : EColorScheme.LIGHT);
};
darkModeMediaQuery.addEventListener('change', listener);
return () => {
darkModeMediaQuery.removeEventListener('change', listener);
};
}, []);

// 优先取用户手动设置的主题
return manualScheme || scheme;
};

React Native中的适配


上面说的都是在浏览器里对深色模式的适配,那在React Native里面要怎么适配深色模式呢?


1. 大于等于0.62的版本


Appearance · React Native


在React Native 0.62版本中,引入了Appearance模块,通过这个模块:


type ColorSchemeName = 'light' | 'dark' | null | undefined;

export namespace Appearance {
type AppearancePreferences = {
colorScheme: ColorSchemeName;
};

type AppearanceListener = (preferences: AppearancePreferences) => void;

/**
* Note: Although color scheme is available immediately, it may change at any
* time. Any rendering logic or styles that depend on this should try to call
* this function on every render, rather than caching the value (for example,
* using inline styles rather than setting a value in a `StyleSheet`).
*
* Example: `const colorScheme = Appearance.getColorScheme();`
*/

export function getColorScheme(): ColorSchemeName;

/**
* Add an event handler that is fired when appearance preferences change.
*/

export function addChangeListener(listener: AppearanceListener): EventSubscription;

/**
* Remove an event handler.
*/

export function removeChangeListener(listener: AppearanceListener): EventSubscription;
}

/**
* A new useColorScheme hook is provided as the preferred way of accessing
* the user's preferred color scheme (aka Dark Mode).
*/

export function useColorScheme(): ColorSchemeName;

通过Appearance模块,可以获得当前的系统颜色主题:


const colorScheme = Appearance.getColorScheme();
if (colorScheme === 'dark') {
// dark mode
} else {
// light mode
}

Appearance.addChangeListener((prefer: Appearance.AppearancePreferences) => {
if (prefer.colorScheme === 'dark') {
// dark mode
} else {
// light mode
}
});

同时也提供了一个上面我们自己实现的hooks,useColorScheme


const colorScheme = useColorScheme();

一些坑




  1. Appearance这个接口在Chrome调试模式下,会不生效,永远返回light


    Appearance.getColorScheme() always returns ‘light’




  2. Appearance想要生效,还需要Native做一些配置


    React Native 0.62.2 Appearance return wrong color scheme



    Also make sure you do not have UIUserInterfaceStyle set in your Info.plist. I had it set to 'light' so Appearance.getColorScheme() was always returning 'light'.





2. 小于0.62的版本


对于0.62之前的版本,由于RN没有提供官方接口,需要通过第三方的库react-native-dark-mode来实现:
GitHub - codemotionapps/react-native-dark-mode: Detect dark mode in React Native


它的实现原理感兴趣的可以看下:



react-native-dark-mode 实现原理(这段实现原理其实也是问Chat GPT得到的答案😂)


react-native-dark-mode库的实现原理比较简单,它主要是利用了原生平台的接口来检测当前系统是否处于深色模式。在iOS平台上,它使用了UIUserInterfaceStyle接口来获取当前系统的界面风格,然后判断是否为暗黑模式。在Android平台上,它使用了UiModeManager接口来获取当前系统的 UI 模式,然后判断是否为夜间模式。


具体来说,react-native-dark-mode在React Native项目中提供了一个名为useDarkMode的 React Hooks,用于获取当前系统是否处于深色模式。当使用这个Hooks时,它会首先检测当前平台是否支持暗黑模式,如果支持,就直接调用原生平台的接口获取当前系统的界面风格或UI模式,并将结果返回给调用方。如果不支持,就返回一个默认值(比如浅色模式)。


需要注意的是,由于react-native-dark-mode是一个纯JS库,它无法直接调用原生平台的接口。所以它在Native端编写了一个名为DarkMode的模块,在JS层通过NativeModules.DarkMode来调用。



  • 在iOS上,DarkMode模块会通过RCT_EXPORT_MODULE()宏将自己暴露给RN的JS层。同时,它还会使用RCT_EXPORT_METHOD()宏将检测系统界面风格的方法暴露给JS层,使得在JS中可以直接调用该方法。

  • 在Android上,DarkMode模块同样会通过@ReactModule注解将自己暴露给JS层。然后,它会创建一个名为DarkModeModule的Java类,并在该类中实现检测系统UI模式的方法。最后,它会使用@ReactMethod注解将该方法暴露给JS层,使得在JS中可以直接调用该方法。



参考链接



作者:酥风
来源:juejin.cn/post/7298997940019085366
收起阅读 »

00年菜鸡前端的面试经历分享

web
去年8月份入职的某大厂(外包)今年6月份被通知甲方即将转移去广州。我们外包人员产品和测试被外包公司安排了赔偿,但也赔的很少。前端和后端就是被安排其他甲方的面试,我碰巧很想去旅游,就直接自离了。 出去玩了一个月以后兜里的元子也基本见底了,虽说目前还没有车贷房贷但...
继续阅读 »

去年8月份入职的某大厂(外包)今年6月份被通知甲方即将转移去广州。我们外包人员产品和测试被外包公司安排了赔偿,但也赔的很少。前端和后端就是被安排其他甲方的面试,我碰巧很想去旅游,就直接自离了。


出去玩了一个月以后兜里的元子也基本见底了,虽说目前还没有车贷房贷但也要交房租也要吃饭,就又开始了找工作。不过令人没想到的是今年的行情能这么这么的差,以前每年都说今年环境差但每次我离职基本都能在两周内拿到满意的ofr,但今年算是找了将近两个月才找到个稍微稍微差不多点的(短期,三个月,而且薪资比上家低了3K,好在离家近,办公环境还算敞亮


(图片是面试路上拍的与文章内容没啥关系)


微信图片_20230817085740.jpg

简单记录,问的问题以及我的回答有的记不太清我就从简了。


第一家是一个研究所,面试我的不知道是个大哥还是大姐反正有点中性那种感觉(不过听声音应该是大哥),问了react中的useEffect,我说是用于修改以及监听数据变化,相当于react18之前的componentDidMount、componentDidUpdate和componentWillUnmount,传递的参数分别是要处理的逻辑函数以及数组。大文件上传,我说大文件上传主要的解决方案就是切片处理,和后端定义好key关键值,然后分割file分批通过接口上传文件以及参数后端拿到后再进行合并。第三个问了我性能优化,我说了几个大概方向:图片优化(大图片压缩、雪碧图)、代码优化(组件化减少复用、外部链接)、懒加载预加载、节流防抖。


然后问了我以前的工作亮点,这个问题 其实很多次被问到我也只是挑我觉得业务逻辑稍微难一点的东西说,实在没有个说出来让面试官眼前一亮的答案。


然后回家后hr联系我说给过,但只给到了12,我说我最低接受13,其实不是拉扯她我这次找工作本来给自己定的目标就是13-14,我是觉得这家离家比较近,但办公环境有些压抑,屋里人多 有点阴暗 我说能不能争取到13,hr说尝试一下,过了一会说最高12.5了我说那我再看看吧,其实也是因为心态问题,这是第一家我也只是试水的状态,他真的给到了13我可能也不是说一定就会去。


微信图片_20230801013245.jpg

第二家也是个自研,这家离家距离中规中矩,45分钟地铁。问的都是些基础面试题早就背的滚瓜烂熟那种,什么水平垂直居中 我说了三种 一种弹性盒、一种topleft50%然后margin各负一半、还有一种绝对定位相对定位。什么组件通信、路由传参,但这家吃亏在我没做过GIS和地图,所以结果是也没给过。第三家是个外包,其实我从不介意外包,因为我学历就不太顶,而且现在行情不好有的干就不错了。这家公司位置还挺好,在新街口附近,应该很有钱,问了vue中父子组件生命周期的执行顺序,我说父create-子create-子mount-父mount。然后他又追问我哪个先beforeCreate我说子先


然后问我 v-if和v-for的优先级以及vue2 和 vue3中他们的区别,其实应该是2中for大于if3中if大于for但我回答的时候说反了她还问我确定吗我说确定
然后和我说他们公司主要用的技术栈是react(我纳闷那你问我vue干啥玩楞)而且他们主要是用react native我寻思也行 做一些我没做过的东西也算开拓新领域了,但很遗憾也没给过


微信图片_20230817090227.jpg

第四家 就有意思了,贼拉远。怎么事儿呢? 上午十点半我刚自然醒迷瞪的我就看boss一看有个面试邀请乐呵的就接受了,然后一看是今天的我寻思那起来洗漱换衣服出发吧,结果一出门看路线才看到他娘的两个小时的路程,地铁转三趟,还要做十站公交,还要徒步1.5公里。我寻思这就算面试通过了以后也不好上下班呀,一天四个小时都在路上,我就打算取消了吧,但boss上即将面试的面试还不能取消,我跟hr说 hr说没事我们好多员工也在你那附近,过来吧。其实大概也能察觉到估计是让我过去填她人事kpi的,但我想着在家闲着也是闲着就当打发时间了,就去了。


确实是麻烦,这路程真的就算给我18k我都不想去,然后接我进去的是个花臂小哥,他花臂还挺帅的。我从家出发是十点,到那十二点都午休了,他们让我等到一点半我说我下午有事就联系了人事让面试官这会儿面一下子


问了我关于深浅拷贝 我说就是引用指针的区别,深拷贝就是重新注册一块空间声明变量,常用的方法有递归和json.parse再strfy但后者只能处理基本数据类型。问了我事件执行机制,我就大概往红任务微任务那方向回答的,然后让我手写了个递推和冒泡。就回了,吗的这面试就面了半个小时,来回路程四个半小时


出门十点,回家下午四点了(面完出来在地铁口吃了口饭)


面试官意思说我还可以,但回家以后我也没问hr后续,因为过了也不打算去,而且hr也没主动联系我


然后就搬了个家。。。


微信图片_20230817090228.jpg

最后一家面试(也就是现在入职这家)问了我跨域,我说跨域是出于浏览器的同源策略,当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。然后解决办法 第一个我说的jsonp解决,用script标签括住跨域的部分,第二个是本地代理。他又说线上环境你怎么办呢,我说线上的话那就cors解决,什么w3c标准啊跨源ajax啥的就都忽悠上了,其实正儿八经工作中我基本没用过cors和jsonp,基本全是本地代理


然后问了我数组的一些方法我就可增删改查合并分割这些的说了一些


微信图片_20230817115038.jpg

然后让我手写了一个promise和节流函数还有一个去重,讲实话就去重写出来的比较完整,promise和节流就写出来个大概思路


就让我进了


但就三个月,我想的仨月就仨月吧,干完也就十一月中旬了,再躺一个月过完元旦回家过年了


其实要不是因为刚和小伙伴签了一年的房子合同真有点打算去别的城市了


作者:牛油果好不好吃
来源:juejin.cn/post/7268011328940539939
收起阅读 »

你的代码不堪一击!太烂了!

web
前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
继续阅读 »

前言


小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


一、变量解构一解就报错


优化前


const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



所以当 dataundefinednull 时候,上述代码就会报错。


优化后


const App = (props) => {
const { data } = props;
const { name, age } = data || {};
}

二、不靠谱的默认值


估计有些同学,看到上小节的代码,感觉还可以再优化一下。


再优化一下


const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



所以当 props.datanull,那么 const { name, age } = null 就会报错!


三、数组的方法只能用真数组调用


优化前:


const App = (props) => {
const { data } = props;
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


优化后:


const App = (props) => {
const { data } = props;
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象


优化前:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


优化后:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


二次优化后:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用


优化前:


const App = (props) => {
const { data } = props;
const nameList = Object.keys(data);
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


优化后:


const App = (props) => {
const { data } = props;
const nameList = Object.keys(data || {});
}

二次优化后:


const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props;
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获


优化前:


import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。


优化后:


import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


二次优化后:


import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse


优化前:


const App = (props) => {
const { data } = props;
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


优化后:


const App = (props) => {
const { data } = props;
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据


优化前:


const App = (props) => {
const { data } = props;
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


优化后:


import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props;
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作


优化前:


const App = (props) => {
const { data } = props;
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


优化后:


const App = async (props) => {
const { data } = props;
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御


优化前:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


优化后:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续


以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


作者:红尘炼心
来源:juejin.cn/post/7259007674520158268
收起阅读 »

坏了,CSS真被他们玩出花来了

web
前言 事情是这样子的,本人由于摸鱼过多被临时抽调去支援公司的一个官网开发,其中有个任务是改造侧边栏导航,我心想着很简单嘛,两下搞完继续我的摸鱼大业😁.然后ui就丢给了一个网站让我照着这个做(龟龟现成的都有,这也太好了),网站在这里,让我们来看一下简单的交互. ...
继续阅读 »

前言


事情是这样子的,本人由于摸鱼过多被临时抽调去支援公司的一个官网开发,其中有个任务是改造侧边栏导航,我心想着很简单嘛,两下搞完继续我的摸鱼大业😁.然后ui就丢给了一个网站让我照着这个做(龟龟现成的都有,这也太好了),网站在这里,让我们来看一下简单的交互.


iShot_2023-10-30_11.07.56.gif


看了一下大致就是滚动到特定位置后把导航栏固定,hover的时候显示导航列表,这不是小菜一碟.


滚动到特定位置固定导航


主要逻辑就是下面这段,简单来说就是监听滚动条位置,当滚动到特定高度时改变导航按钮的定位方式,
css部分是用的@emotion/styled,文档可以看一下这里.


const [fixed, setFixed] = useState(false);
useEffect(() => {
const root = document.querySelector('#root');
if (!root) return;
const fn = () => {
if (root.scrollTop > 451) {
setFixed(true);
} else {
setFixed(false);
}
};
root.addEventListener('scroll', fn);
return () => {
root.removeEventListener('scroll', fn);
};
}, []);

const StyledFixed = styled.div<{ fixed?: boolean }>`
width: 0;
position: ${(props) => (props.fixed ? 'fixed' : 'absolute')};
top: ${(props) => (props.fixed ? '80px' : '531px')};
left: 80px;
z-index: 9;
@media (max-width: 1800px) {
left: 12px;
}
`;

效果如下


iShot_2023-10-30_11.24.58.gif


hover显示导航列表


然后就是鼠标移入的时候显示导航列表了,我心想这还不简单,几行css就搞定了(简单描述下就是把导航列表放在按钮里面,给按钮加上hover效果),但是当我研究了一下腾讯网站的代码,我发现事情好像没那么简单.


image.png


导航按钮和列表是平级的,这样的话鼠标移上去列表显示,但列表显示的同时hover效果也没有了,列表就又隐藏了,就会导致闪烁的效果,gif图展示不够明显.


iShot_2023-10-30_11.49.33.gif

现在问题来了,如果是平级元素,那如何控制hover显示呢,答案就在他们的父元素身上,从下面2张图上可以看出,导航按钮以及他的父元素都加上了hover样式


image.png


image.png


默认情况下父元素宽度为0,防止误触发列表展示,hover状态下设置width:auto
image.png


实现效果


到这里,我已经完全清楚了实现原理,完整的代码在下面


const LeftNav = observer(() => {
const { selectedKeys, openKeys } = menuStore;
const [fixed, setFixed] = useState(false);
useEffect(() => {
const root = document.querySelector('#root');
if (!root) return;
const fn = () => {
if (root.scrollTop > 451) {
setFixed(true);
} else {
setFixed(false);
}
};
root.addEventListener('scroll', fn);
return () => {
root.removeEventListener('scroll', fn);
};
}, []);
const onMenuClick = ({ key }: { key: string }) => {
menuStore.selectedKeys = [key];
if (key) {
const dom = document.querySelector(`#${key}`);
menuStore.scrollingKey = key;
dom?.scrollIntoView({
behavior: 'smooth'
});
}
};

const onOpenChange = (keys: string[]) => {
menuStore.openKeys = keys.filter((i) => i !== openKeys[0]);
};

return (
<StyledFixed fixed={fixed}>
<div className='left-nav-btn'>
<RightOutlinedIcon />
</div>
<div className='left-nav-list'>
<StyledMenuWrap>
<Menu selectedKeys={selectedKeys} openKeys={openKeys} mode='inline' items={MENU} onClick={onMenuClick} onOpenChange={onOpenChange} />
</StyledMenuWrap>
</div>
</StyledFixed>

);
});

const StyledFixed = styled.div<{ fixed?: boolean }>`
width: 0;
position: ${(props) => (props.fixed ? 'fixed' : 'absolute')};
top: ${(props) => (props.fixed ? '80px' : '531px')};
left: 80px;
z-index: 9;
&:hover {
width: auto;
.left-nav-btn {
display: none;
}
.left-nav-list {
transform: none;
visibility: visible;
}
}
@media (max-width: 1800px) {
left: 12px;
}
.left-nav-btn {
position: absolute;
top: 80px;
width: 40px;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
padding: 12px;
border-radius: 100px;
border: 1px solid #fff;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0px 4px 30px 0px rgba(12, 25, 68, 0.05);

cursor: pointer;

&::before {
content: '页面导航';
background: linear-gradient(139deg, #c468ef 5.3%, #2670ff 90.91%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}

&:hover {
display: none;
& + .left-nav-list {
transform: none;
visibility: visible;
}
}
@media (min-width: 1799px) {
display: none;
}
}
.left-nav-list {
position: relative;
width: 172px;
z-index: 1;
transition: all 0.3s ease-in;
@media (max-width: 1800px) {
transform: translateX(-200px);
visibility: hidden;
}
}
`
;

最终的实现效果如下
iShot_2023-10-30_14.39.20.gif


至于腾讯网站中的滚动到对应模块高亮菜单的实现可以看看 IntersectionObserver 这个api,好了本次的分享就到此为止了,感谢各位大佬的阅读与点赞😁,你可以说我菜,因为我是真的菜.


作者:hahayq
来源:juejin.cn/post/7295343805020487690
收起阅读 »

JSON慢地要命: 看看有啥比它快!

web
是的, 你没听错! 网络开发中无处不在的数据交换格式JSON, 可能会拖慢你的应用程序. 在这个速度和响应速度至上的世界里, 检查 JSON 的性能影响至关重要, 而我们对此却常常忽略. 在本博客中, 我们将深入探讨 JSON 成为应用程序瓶颈的原因, 并探索...
继续阅读 »


是的, 你没听错! 网络开发中无处不在的数据交换格式JSON, 可能会拖慢你的应用程序. 在这个速度和响应速度至上的世界里, 检查 JSON 的性能影响至关重要, 而我们对此却常常忽略. 在本博客中, 我们将深入探讨 JSON 成为应用程序瓶颈的原因, 并探索更快的替代方案和优化技术, 以确保你的应用程序以最佳状态运行.


JSON 是什么? 为何我要关注这个问题?



JSON 教程 | w3resource


JSON是JavaScript Object Notation的缩写, 是一种轻量级数据交换格式, 已成为Web应用中传输和存储数据的首选. 它的简洁性和人类可读格式使人类和机器都能轻松使用. 但是, 为什么要在Web开发项目中关注 JSON 呢?


JSON 是应用中数据的粘合剂. 它是服务器和客户端之间进行数据通信的语言, 也是数据库和配置文件中存储数据的格式.


JSON 的流行以及人们使用它的原因…


JSON 在Web开发领域的受欢迎程度怎么强调都不为过. 它已成为数据交换的事实标准, 这其中有几个令人信服的原因:


它易于使用!



  1. 人类可读格式: JSON 使用简单明了, 基于文本的结构, 开发人员和非开发人员都能轻松阅读和理解. 这种人类可读的格式增强了协作, 简化了调试.

  2. 语言无关性: JSON 与任何特定的编程语言无关. 它是一种通用的数据格式, 几乎所有现代编程语言都能对其进行解析和生成, 因此它具有很强的通用性.

  3. 数据结构一致性: JSON 使用键值对, 数组和嵌套对象来实现数据结构的一致性. 这种一致性使其具有可预测性, 便于在各种编程场景中使用.

  4. 支持浏览器: 网络浏览器原生支持 JSON, 允许Web应用与服务器进行无缝通信. 这种本地支持极大地促进了 JSON 在Web开发中的应用.

  5. JSON API: 许多网络服务和应用接口默认以 JSON 格式提供数据. 这进一步巩固了 JSON 在Web开发中作为数据交换首选的地位.

  6. JSON Schema: 开发人员可以使用 JSON 模式来定义和验证 JSON 数据的结构, 从而为应用增加了一层额外的清晰度和可靠性.


鉴于这些优势, 难怪全球的开发人员都依赖 JSON 来满足他们的数据交换需求. 然而, 随着我们在本博客的深入探讨, 我们将发现与 JSON 相关的潜在性能挑战, 以及如何有效解决这些挑战.


速度需求


🚀🚀🚀


应用的速度和响应的重要性


在当今快节奏的数字环境中, 应用的速度和响应能力是不可或缺的. 用户希望在Web和移动应用中即时获取信息, 快速交互和无缝体验. 对速度的这种要求是由以下几个因素驱动的:



  1. 用户期望: 用户已习惯于从数字互动中获得闪电般快速的响应. 他们不想等待网页的加载或应用的响应. 哪怕是几秒钟的延迟, 都会导致用户产生挫败感并放弃使用.

  2. 竞争优势: 速度可以成为重要的竞争优势. 反应迅速的应用往往比反应迟缓的应用更能吸引和留住用户.

  3. 搜索引擎排名: 谷歌等搜索引擎将网页速度视为排名因素. 加载速度更快的网站往往在搜索结果中排名靠前, 从而提高知名度和流量.

  4. 转化率: 电子商务网站尤其清楚速度对转化率的影响. 网站速度越快, 转换率越高, 从而增加收入.

  5. 移动性能: 随着移动设备的普及, 对速度的需求变得更加重要. 移动用户的带宽和处理能力往往有限, 因此快速的应用性能是必要的.


JSON 会拖慢我们的应用吗?


现在, 让我们来讨论核心问题: JSON 是否会拖慢我们的应用?


如前所述, JSON 是一种非常流行的数据交换格式. 它灵活, 易用, 并得到广泛支持. 然而, 这种广泛的应用并不意味着它不会面临性能挑战.


某些情况下, JSON 可能是导致应用慢的罪魁祸首. 解析 JSON 数据的过程, 尤其是在处理大型或复杂结构时, 可能会耗费宝贵的毫秒时间. 此外, 低效的序列化和反序列化也会影响应用的整体性能.


在接下来的内容中, 我们将探讨 JSON 成为应用瓶颈的具体原因, 更重要的是, 探讨如何缓解这些问题. 在深入探讨的过程中, 请记住我们的目标不是诋毁 JSON, 而是了解其局限性并发现优化其性能的策略, 以追求更快, 反应更灵敏的应用.



LinkedIn将Protocal Buffers与Rest.li集成以提高微服务性能| LinkedIn工程


JSON 为什么会变慢


尽管 JSON 被广泛使用, 但它也难逃性能挑战. 让我们来探究 JSON 可能会变慢的原因, 并理解为什么 JSON 并不总是数据交换的最佳选择.


1. 解析带来的开销


当 JSON 数据到达应用时, 它必须经过解析过程才能转换成可用的数据结构. 解析过程可能相对较慢, 尤其是在处理大量或深度嵌套的 JSON 数据时.


2. 序列化和反序列化


JSON 要求在从客户端向服务器发送数据时进行序列化(将对象编码为字符串), 并在接收数据时进行反序列化(将字符串转换回可用对象). 这些步骤会带来开销, 影响应用的整体速度.


微服务架构的世界里, JSON 通常用于在服务之间传递消息. 但是, 很关键的是, 我们必须认识到, JSON 消息需要序列化和反序列化, 这两个过程会带来巨大的开销.



在有大量微服务不断通信的场景中, 这种开销可能会增加, 并有可能使应用变慢, 以至于影响用户体验.




我们面临的第二个挑战是, 由于 JSON 的文本性质, 序列化和反序列化的延迟和吞吐量都不理想.
— LinkedIn



1_74sQfiW0SjeFfcTcgNKupw.webp
序列化和反序列化


3. 字符串操作


JSON 基于文本, 在连接和解析等操作中严重依赖字符串操作. 与处理二进制数据相比, 处理字符串的速度会慢一些.


4. 缺乏数据类型


JSON 的数据类型(如字符串, 数字, 布尔值)非常有限. 复杂的数据结构可能需要效率较低的表示法, 从而导致内存使用量增加和处理速度减慢.



5. 冗余


JSON 的人类可读性设计可能会导致冗余. 不需要的键和重复的结构增加了有效载荷的大小, 导致数据传输时间延长.



第一个挑战是 JSON 是一种文本格式, 往往比较冗余. 这导致网络带宽使用量增加, 更高的延迟, 效果并不理想.
— LinkedIn



6. 不支持二进制


JSON 缺乏对二进制数据的本地支持. 在处理二进制数据时, 开发人员通常需要将其编解码为文本, 而这可能会降低效率.


7. 深度嵌套


在某些情况下, JSON 数据可能是深嵌套的, 需要递归解析和遍历. 这种计算复杂性会降低应用的运行速度, 尤其是在没有优化的情况下.


JSON的替代方案


虽然 JSON 是一种通用的数据交换格式, 但由于其在某些情况下的性能限制, 人们开始探索更快的替代格式. 让我们深入探讨其中的一些替代方案, 了解何时以及为何选择它们:


1. Protocol Buffers(protobuf)


Protocal Buffers通常被称为protobuf, 是由谷歌开发的一种二进制序列化格式. 它的设计宗旨是高效, 紧凑和快速. Protobuf 的二进制性质使其在序列化和反序列化方面的速度明显快于 JSON.



  • 何时选择: 当你需要高性能的数据交换时, 尤其是在微服务架构, 物联网应用或网络带宽有限的情况下, 请考虑使用Protobuf.


GitHub - vaishnav-mk/protobuf-example


2. MessagePack


MessagePack 是另一种二进制序列化格式, 以速度快, 结构紧凑而著称. 它比 JSON 更有效率, 同时与各种编程语言保持兼容.



  • 何时选择: 当你需要在速度和跨语言兼容性之间取得平衡时, MessagePack 是一个不错的选择. 它适用于实时应用和对减少数据大小至关重要的情况.


3. BSON (二进制 JSON)


BSON 或二进制 JSON 是一种从 JSON 衍生出来的二进制编码格式. 它保留了 JSON 的灵活性, 同时通过二进制编码提高了性能. BSON 常用于 MongoDB 等数据库.



  • 何时选择: 如果你正在使用 MongoDB, 或者需要一种格式来弥补 JSON 和二进制效率之间的差距, 那么 BSON 是一个很有价值的选择.


4. Apache Avro


Apache Avro 是一个数据序列化框架, 专注于提供一种紧凑的二进制格式. 它基于schema, 可实现高效的数据编解码.



  • 何时选择: Avro 适用于schema演进非常重要的情况, 如数据存储, 以及需要在速度和数据结构灵活性之间取得平衡的情况.


与 JSON 相比, 这些替代方案提供了不同程度的性能改进, 具体选择取决于你的具体使用情况. 通过考虑这些替代方案, 你可以优化应用的数据交换流程, 确保将速度和效率放在开发工作的首位.



JSON, Protobufs, MessagePack, BSON 和 Avro 之间的差异


每个字节都很重要: 优化数据格式


在效率和速度至上的数据交换世界中, 数据格式的选择会产生天壤之别. 本节将探讨从简单的 JSON 数据表示到更高效的二进制格式(如 Protocol Buffers, MessagePack, BSON 和 Avro)的过程. 我们将深入探讨每种格式的细微差别, 并展示为什么每个字节都很重要.


开始: JSON 数据


我们从简单明了的 JSON 数据结构开始. 下面是我们的 JSON 数据示例片段:


{
"id": 1, // 14 bytes
"name": "John Doe", // 20 bytes
"email": "johndoe@example.com", // 31 bytes
"age": 30, // 9 bytes
"isSubscribed": true, // 13 bytes
"orders": [ // 11 bytes
{ // 2 bytes
"orderId": "A123", // 18 bytes
"totalAmount": 100.50 // 20 bytes
}, // 1 byte
{ // 2 bytes
"orderId": "B456", // 18 bytes
"totalAmount": 75.25 // 19 bytes
} // 1 byte
] // 1 byte
} // 1 byte

JSON 总大小: ~ 139 字节


虽然 JSON 用途广泛且易于使用, 但它也有一个缺点, 那就是它的文本性质. 每个字符, 每个空格和每个引号都很重要. 在数据大小和传输速度至关重要的情况下, 这些看似微不足道的字符可能会产生重大影响.


效率挑战: 使用二进制格式减小尺寸



现在, 让我们提供其他格式的数据表示并比较它们的大小:


Protocol Buffers (protobuf):


syntax = "proto3";

message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_subscribed = 5;
repeated Order orders = 6;

message Order {
string order_id = 1;
float total_amount = 2;
}
}

0A 0E 4A 6F 68 6E 20 44 6F 65 0C 4A 6F 68 6E 20 44 6F 65 65 78 61 6D 70 6C 65 2E 63 6F 6D 04 21 00 00 00 05 01 12 41 31 32 33 03 42 DC CC CC 3F 05 30 31 31 32 34 34 35 36 25 02 9A 99 99 3F 0D 31 02 42 34 35 36 25 02 9A 99 99 3F

Protocol Buffers 总大小: ~ 38 bytes


MessagePack:


(注意:MessagePack 是一种二进制格式, 此处的表示法非人工可读.)


二进制表示(十六进制):


a36a6964000000000a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d042100000005011241313302bdcccc3f0530112434353625029a99993f


MessagePack 总大小: ~34 字节


BSON (二进制 JSON):


(注意:BSON 是一种二进制格式, 此处的表示法非人工可读.)


二进制表示法 (十六进制):


3e0000001069640031000a4a6f686e20446f6502656d61696c006a6f686e646f65406578616d706c652e636f6d1000000022616765001f04370e4940

BSON 总大小: ~ 43 字节


Avro:


(注: Avro使用schema, 因此数据与schema信息一起编码.)


二进制表示法 (十六进制):


0e120a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d049a999940040a020b4108312e3525312e323538323539

Avro 总大小: ~ 32 字节



(这些替代方案的实际字节数可能会有所不同, 提供这些数字只是为了让大家有个大致的了解.)


现在你可能会感到奇怪, 为什么我们的程序会有这么多的字节数?


现在你可能想知道为什么有些格式输出的是二进制, 但它们的大小却各不相同. Avro, MessagePack 和 BSON 等二进制格式具有不同的内部结构和编码机制, 这可能导致二进制表示法的差异, 即使它们最终表示的是相同的数据. 下面简要介绍一下这些差异是如何产生的:


1. Avro:



  • Avro 使用schema对数据进行编码, 二进制表示法中通常包含该schema.

  • Avro 基于schema的编码可提前指定数据结构, 从而实现高效的数据序列化和反序列化.

  • Avro 的二进制格式设计为自描述格式, 这意味着schema信息包含在编码数据中. 这种自描述性使 Avro 能够保持不同版本数据模式之间的兼容性.


2. MessagePack:



  • MessagePack 是一种二进制序列化格式, 直接对数据进行编码, 不包含schema信息.

  • 它使用长度可变的整数和长度可变的字符串的紧凑二进制表示法, 以尽量减少空间使用.

  • MessagePack 不包含schema信息, 因此更适用于schema已预先知道并在发送方和接收方之间共享的情况.


3. BSON:



  • BSON 是 JSON 数据的二进制编码, 包括每个值的类型信息.

  • BSON 的设计与 JSON 紧密相连, 但它增加了二进制数据类型, 如 JSON 缺乏的日期和二进制数据.

  • 与 MessagePack 一样, BSON 不包含schema信息.


这些设计和编码上的差异导致了二进制表示法的不同:



  • Avro 包含模式信息并具有自描述性, 这导致二进制大小稍大, 但提供了schema兼容性.

  • MessagePack 因其可变长度编码而高度紧凑, 但缺乏模式信息, 因此适用于已知模式的情况.

  • BSON 与 JSON 关系密切, 包含类型信息, 与 MessagePack 等纯二进制格式相比, 会增加大小.


总之, 这些差异源于每种格式的设计目标和功能. Avro 优先考虑schema兼容性, MessagePack 注重紧凑性, 而 BSON 则在保持类似 JSON 结构的同时增加了二进制类型. 格式的选择取决于具体的使用情况和要求, 如schema兼容性, 数据大小和易用性.


优化 JSON 性能


JSON 虽然用途广泛, 在Web开发中被广泛采用, 但在速度方面也存在挑战. 这种格式的人类可读性会导致数据负载较大, 处理时间较慢. 因此, 问题出现了: 我们能够怎样优化JSON以使得它更快更高效? 在本文中, 我们将探讨可用于提高 JSON 性能的实用策略和优化方法, 以确保 JSON 在提供应用所需的速度和效率的同时, 仍然是现代 Web 开发中的重要工具.


以下是一些优化 JSON 性能的实用技巧以及代码示例和最佳实践:


1. 最小化数据大小:



  • 使用简短, 描述性的键名: 选择简洁但有意义的键名, 以减小 JSON 对象的大小.


// Inefficient
{
"customer_name_with_spaces": "John Doe"
}

// Efficient
{
"customerName": "John Doe"
}


  • 尽可能缩写:  在不影响清晰度的情况下, 考虑对键或值使用缩写.


// Inefficient
{
"transaction_type": "purchase"
}

// Efficient
{
"txnType": "purchase"
}

2. 明智地使用数组:



  • 最小化嵌套: 避免深度嵌套数组, 因为它们会增加解析和遍历 JSON 的复杂性.


// Inefficient
{
"order": {
"items": {
"item1": "Product A",
"item2": "Product B"
}
}
}

// Efficient
{
"orderItems": ["Product A", "Product B"]
}

3. 优化数字表示:



  • 尽可能使用整数:  如果数值可以用整数表示, 请使用整数而不是浮点数.


// Inefficient
{
"quantity": 1.0
}

// Efficient
{
"quantity": 1
}

4. 消除冗余:



  • 避免重复数据: 通过引用共享值来消除冗余数据.


// Inefficient
{
"product1": {
"name": "Product A",
"price": 10
},
"product2": {
"name": "Product A",
"price": 10
}
}

// Efficient
{
"products": [
{
"name": "Product A",
"price": 10
},
{
"name": "Product B",
"price": 15
}
]
}

5. 使用压缩:



  • 使用压缩算法:  如何可行的话, 使用压缩算法, 比如Gzip 或者Brotli, 以在传输过程中减少JSON负载大小.


// Node.js example using zlib for Gzip compression
const zlib = require('zlib');

const jsonData = {
// Your JSON data here
};

zlib.gzip(JSON.stringify(jsonData), (err, compressedData) => {
if (!err) {
// Send compressedData over the network
}
});

6. 采用服务器端缓存:



  • 缓存 JSON 响应:  实施服务器端缓存, 以便高效地存储和提供 JSON 响应, 减少重复数据处理的需要.


7. 剖析与优化:



  • 剖析性能:  使用剖析工具找出 JSON 处理代码中的瓶颈, 然后优化这些部分.



请记住, 你实施的具体优化措施应符合应用的要求和限制.



真实世界的优化: 在实践中加速


在这一部分, 我们将深入探讨现实世界中遇到 JSON 性能瓶颈并成功解决的应用和项目. 我们将探讨企业如何解决 JSON 的局限性, 以及这些优化为其应用带来的切实好处. 从 LinkedIn 和 Auth0 这样的知名平台到 Uber 这样的颠覆性科技巨头*, 这些示例为我们提供了宝贵的见解, 让我们了解在尽可能利用 JSON 的多功能性的同时提高速度和响应能力的策略.


1. LinkedIn集成Protocol Buffers:


挑战: LinkedIn 面临的挑战是 JSON 的冗长以及由此导致的网络带宽使用量增加, 从而导致延迟增加.
解决方案: 他们在微服务通信中采用了二进制序列化格式 Protocol Buffers 来取代 JSON.
影响: 这一优化将延迟降低了60%, 提高了 LinkedIn 服务的速度和响应能力.


2. Uber的H3地理索引:



  • 挑战: Uber 使用 JSON 表示各种地理空间数据, 但解析大型数据集的 JSON 会降低其算法的速度.

  • 解决方法 他们引入了H3地理索引, 这是一种用于地理空间数据的高效六边形网格系统, 可减少 JSON 解析开销.

  • 影响: 这一优化大大加快了地理空间操作, 增强了 Uber 的叫车和地图服务.


3. Slack的消息格式优化:



  • 挑战: Slack 需要在实时聊天中传输和呈现大量 JSON 格式的消息, 这导致了性能瓶颈.

  • 解决方法 他们优化了 JSON 结构, 减少了不必要的数据, 只在每条信息中包含必要的信息.

  • 影响: 这一优化提高了消息渲染速度, 改善了 Slack 用户的整体聊天性能.


4. Auth0的Protocal Buffers实现:



  • 挑战: Auth0 是一个流行的身份和访问管理平台, 在处理身份验证和授权数据时面临着 JSON 的性能挑战.

  • 解决方案: 他们采用Protocal Buffers来替代 JSON, 以编解码与身份验证相关的数据.

  • 影响: 这一优化大大提高了数据序列化和反序列化的速度, 从而加快了身份验证流程, 并增强了 Auth0 服务的整体性能.


这些真实案例表明, 通过优化策略解决 JSON 的性能难题, 可对应用的速度, 响应和用户体验产生重大积极影响. 它们强调了在各种应用场景中考虑使用替代数据格式和高效数据结构来克服 JSON 相关缓慢的问题的重要性.


总结一下


在开发领域, JSON 是数据交换不可或缺的通用工具. 其人类可读格式和跨语言兼容性使其成为现代应用的基石. 然而, 正如我们在本文中所探讨的, JSON 的广泛应用并不能使其免于性能挑战.


我们在优化 JSON 性能的过程中获得的主要启示是显而易见的:



  • 性能至关重要: 在当今的数字环境中, 速度和响应速度至关重要. 用户希望应用能够快如闪电, 即使是微小的延迟也会导致不满和机会的丧失.

  • 尺寸至关重要: 数据有效载荷的大小会直接影响网络带宽的使用和响应时间. 减少数据大小通常是优化 JSON 性能的第一步.

  • 替代格式: 当效率和速度至关重要时, 探索其他数据序列化格式, 如Protocal Buffers, MessagePack, BSON 或 Avro.

  • 真实世界案例: 从企业成功解决 JSON 速度变慢问题的实际案例中学习. 这些案例表明, 优化工作可以大幅提高应用的性能.


在继续构建和增强Web应用时, 请记住要考虑 JSON 对性能的影响. 仔细设计数据结构, 选择有意义的键名, 并在必要时探索其他序列化格式. 这样, 你就能确保你的应用在速度和效率方面不仅能满足用户的期望, 而且还能超越用户的期望.


在不断变化的Web开发环境中, 优化 JSON 性能是一项宝贵的技能, 它能让你的项目与众不同, 并确保你的应用在即时数字体验时代茁壮成长.


作者:bytebeats
来源:juejin.cn/post/7299353265099423753
收起阅读 »

仿写el-upload组件,彻底搞懂文件上传

web
用了那么久的Upload组件,你知道是怎么实现的么,今天就来仿写一个饿了么el-upload vue组件,彻底搞懂前端的文件上传相关知识! 要实现的props 参数说明action必选参数,上传的地址headers设置上传的请求头部multiple是否支持多选...
继续阅读 »

用了那么久的Upload组件,你知道是怎么实现的么,今天就来仿写一个饿了么el-upload vue组件,彻底搞懂前端的文件上传相关知识!


要实现的props


参数说明
action必选参数,上传的地址
headers设置上传的请求头部
multiple是否支持多选文件
data上传时附带的额外参数
name上传的文件字段名
with-credentials支持发送 cookie 凭证信息
show-file-list是否显示已上传文件列表
drag是否启用拖拽上传
accept接受上传的文件类型
on-preview点击文件列表中已上传的文件时的钩子
on-remove文件列表移除文件时的钩子
on-success文件上传成功时的钩子
on-error文件上传失败时的钩子
on-progress文件上传时的钩子
on-change添加文件时被调用
before-upload上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。
before-remove删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 Promise 且被 reject,则停止删除。
list-type文件列表的类型
auto-upload是否在选取文件后立即进行上传
file-list上传的文件列表, 例如: [{name: 'food.jpg', url: 'xxx.cdn.com/xxx.jpg'}]
limit最大允许上传个数
on-exceed文件超出个数限制时的钩子

参考:element.eleme.cn/#/zh-CN/com…


这里面有几个重要的点:



  1. input file 的美化

  2. 多选

  3. 拖拽


一个个实现


创建upload组件文件


src/components/upload/index.vue


<template></template>
<script setup>
  // 属性太多,把props单独放一个文件引入进来
  import property from './props'
  const props = defineProps(property)
</script>
<style></style>

./props.js


export default {
  action: {
    typeString
  },
  headers: {
    typeObject,
    default: {}
  },
  multiple: {
    typeBoolean,
    defaultfalse
  },
  data: {
    typeObject,
    default: {}
  },
  name: {
    typeString,
    default'file'
  },
  'with-credentials': {
    typeBoolean,
    defaultfalse
  },
  'show-file-list': {
    typeBoolean,
    defaulttrue,
  },
  drag: {
    typeBoolean,
    defaultfalse
  },
  accept: {
    typeString,
    default''
  },
  'list-type': {
    typeString,
    default'text' // text、picture、picture-card
  },
  'auto-upload': {
    typeBoolean,
    defaulttrue
  },
  'file-list': {
    typeArray,
    default: []
  },
  disabled: {
    typeBoolean,
    defaultfalse
  },
  limit: {
    typeNumber,
    defaultInfinity
  },
  'before-upload': {
    typeFunction,
    default() => {
      return true
    }
  },
  'before-remove': {
    typeFunction,
    default() => {
      return true
    }
  }

具体的编写upload组件代码


1. 文件上传按钮的样式


我们都知道,<input type="file">的默认样式是这样的: 很丑,并且无法改变其样式。


解决办法:可以把input隐藏,重新写个按钮点击来触发input的文件选择。


<template>
  <input 
     type="file" 
     id="file" 
     @change="handleChange"
  >

  <button 
     class="upload-btn" 
     @click="choose"
  >

    点击上传
  </button>
</template>
<script setup>
  // 触发选择文件
  const choose = () => {
    document.querySelector('#file').click()
  }
  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
  }
</script>
<style scoped>
  #file {
    display: none;
  }
  .upload-btn {
    border: none;
    background-color#07c160;
    color#fff;
    padding6px 10px;
    cursor: pointer;
  }
</style>

效果:



这样也是可以调起文件选择框,并触发input的onchange事件。



2. 多选


直接在input上加一个Booelan属性multiple,根据props中的值动态设置


顺便把accept属性也加上


<template>
  <input 
     type="file" 
     id="file" 
     :multiple="multiple"
     :accept="accept"
     @change="handleChange"
  >
</template>

3. 拖拽


准备一个接收拖拽文件的区域,props传drag=true就用拖拽,否则就使用input上传。


<template>
  <input 
    type="file" 
    id="file" 
    :multiple="multiple"
    :accept="accept"
    @change="handleChange"
  >
  <button 
     class="upload-btn" 
     v-if="!drag" 
     @click="choose"
  >
    点击上传
  </button>
  <div 
    v-else 
    class="drag-box" 
    @dragover="handleDragOver"
    @dragleave="handleDragLeave"
    @drop="handleDrop"
    @click="choose"
    :class="{'dragging': isDragging}"
  >
    将文件拖到此处,或<span>点击上传</span>
  </div>
</template>

dragging用来拖拽鼠标进入时改变样式


<script setup>
  const isDragging = ref(false)
  // 拖放进入目标区域
  const handleDragOver = (event) => {
    event.preventDefault()
    isDragging.value = true
  }
  const handleDragLeave = (event) => {
    isDragging.value = false
  }
  let files = []
  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log(files);
  }
</script>

.drag-box {
    width240px;
    height150px;
    line-height150px;
    text-align: center;
    border1px dashed #ddd;
    cursor: pointer;
    border-radius8px;
  }
  .drag-box:hover {
    border-color: cornflowerblue;
  }
  .drag-box.dragging {
    background-colorrgb(131161216, .2);
    border-color: cornflowerblue;
  }
  .drag-box span {
    color: cornflowerblue;
  }

跟使用input上传效果一样


4. 上传到服务器


并实现on-xxx钩子函数


  const emit = defineEmits()
  const fileList = ref([])
  let files = []

  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  const handleBeforeUpload = (files) => {
    if (files.length > props.limit - fileList.value.length) {
      console.error(`当前限制选择 ${props.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.value.length} 个文件`)
      emit('on-exceed', files, toRaw(fileList.value))
      return
    }
    // 可以把锁哥文件放到一个formData中一起上传,
    // 遍历文件一个个上传,这里一个个上传是为了实现钩子函数回调时返回对应的file对象。
    files.forEach(async file => {
      emit('on-change', file, files)
      if (!props.beforeUpload()) {
        return
      }
      if (props.autoUpload) {
        uploadRequest(file, files)
      }
    })
  }

  // 手动上传已选择的文件
  const submit = () => {
    files.forEach(async file => {
      uploadRequest(file, files)
    })
  }
  
  // 保存xhr对象,用于后面取消上传
  let xhrs = []
  const uploadRequest = async (file, files) => {
    let xhr = new XMLHttpRequest();
    // 调用open函数,指定请求类型与url地址。请求类型必须为POST
    xhr.open('POST', props.action);
    // 设置自定义请求头
    Object.keys(props.headers).forEach(k => {
      xhr.setRequestHeader(k, props.headers[k])
    })
    // 额外参数
    const formData = new FormData()
    formData.append('file', file);
    Object.keys(props.data).forEach(k => {
      formData.append(k, props.data[k]);
    })
    // 携带cookie
    xhr.withCredentials = props.withCredentials
    xhr.upload.onprogress = (e) => {
      emit('on-progress', e, file, files)
    }
    // 监听状态
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        const res = JSON.parse(xhr.response)
        const fileObj = {
          name: file.name,
          percentage: 100,
          raw: file,
          response: res,
          status: 'success',
          size: file.size,
          uid: file.uid,
        }
        fileList.value.push(fileObj)
        if (xhr.status === 200 || xhr.status === 201) {
          emit('on-success', res, fileObj, toRaw(fileList.value))
        } else {
          emit('on-error', res, fileObj, toRaw(fileList.value))
        }
      }
    }
    // 发起请求
    xhr.send(formData);
    xhrs.push({
      xhr,
      file
    })
  }

  const preview = (file) => {
    emit('on-preview', file)
  }

  const remove = (file, index) => {
    if (!props.beforeRemove()) {
      return
    }
    fileList.value.splice(index, 1)
    emit('on-remove', file, fileList.value)
  }

  // 取消上传
  const abort = (file) => {
    // 通过file对象找到对应的xhr对象,然后调用abort
    // xhr.abort()
  }

  defineExpose({
    abort,
    submit
  })

全部代码


<template>
  <input 
    type="file" 
    id="file" 
    :multiple="multiple"
    :accept="accept"
    @change="handleChange"
  >

  <button class="upload-btn" v-if="!drag" @click="choose">
    点击上传
  </button>
  <div 
    v-else 
    class="drag-box" 
    @dragover="handleDragOver"
    @dragleave="handleDragLeave"
    @drop="handleDrop"
    @click="choose"
    :class="{'dragging': isDragging}"
  >

    将文件拖到此处,或<span>点击上传</span>
  </div>
  <template v-if="showFileList">
    <template v-if="listType === 'text'">
      <p class="file-item" v-for="(file, index) in fileList" :key="index" @click="preview(file)">
        <span>{{file.name}}</span>
        <span class="remove" @click.stop="remove(file, index)">×</span>
      </p>
    </template>
  </template>
</template>

<script setup>
  import { ref, toRaw, onMounted } from 'vue'
  import property from './props'
  const props = defineProps(property)
  const emit = defineEmits()

  const fileList = ref([])
  const isDragging = ref(false)

  // 触发选择文件
  const choose = () => {
    document.querySelector('#file').click()
  }

  // 拖放进入目标区域
  const handleDragOver = (event) => {
    event.preventDefault()
    isDragging.value = true
  }

  const handleDragLeave = (event) => {
    isDragging.value = false
  }

  let files = []

  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  const handleBeforeUpload = (files) => {
    if (files.length > props.limit - fileList.value.length) {
      console.error(`当前限制选择 ${props.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.value.length} 个文件`)
      emit('on-exceed', files, toRaw(fileList.value))
      return
    }
    files.forEach(async file => {
      emit('on-change', file, files)
      if (!props.beforeUpload()) {
        return
      }
      if (props.autoUpload) {
        uploadRequest(file, files)
      }
    })
  }

  // 手动上传已选择的文件
  const submit = () => {
    files.forEach(async file => {
      uploadRequest(file, files)
    })
  }

  let xhrs = []
  const uploadRequest = async (file, files) => {
    let xhr = new XMLHttpRequest();
    // 调用open函数,指定请求类型与url地址。请求类型必须为POST
    xhr.open('POST', props.action);
    // 设置自定义请求头
    Object.keys(props.headers).forEach(k => {
      xhr.setRequestHeader(k, props.headers[k])
    })
    // 额外参数
    const formData = new FormData()
    formData.append('file', file);
    Object.keys(props.data).forEach(k => {
      formData.append(k, props.data[k]);
    })
    // 携带cookie
    xhr.withCredentials = props.withCredentials
    xhr.upload.onprogress = (e) => {
      emit('on-progress', e, file, files)
    }
    // 监听状态
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        const res = JSON.parse(xhr.response)
        const fileObj = {
          name: file.name,
          percentage100,
          raw: file,
          response: res,
          status'success',
          size: file.size,
          uid: file.uid,
        }
        fileList.value.push(fileObj)
        if (xhr.status === 200 || xhr.status === 201) {
          emit('on-success', res, fileObj, toRaw(fileList.value))
        } else {
          emit('on-error', res, fileObj, toRaw(fileList.value))
        }
      }
    }
    // 发起请求
    xhr.send(formData);
    xhrs.push({
      xhr,
      file
    })
  }

  const preview = (file) => {
    emit('on-preview', file)
  }

  const remove = (file, index) => {
    if (!props.beforeRemove()) {
      return
    }
    fileList.value.splice(index, 1)
    emit('on-remove', file, fileList.value)
  }

  // 取消上传
  const abort = (file) => {
    // 通过file对象找到对应的xhr对象,然后调用abort
    // xhr.abort()
  }

  defineExpose({
    abort,
    submit
  })
</script>

<style scoped>
  #file {
    display: none;
  }
  .upload-btn {
    border: none;
    background-color#07c160;
    color#fff;
    padding6px 10px;
    cursor: pointer;
  }
  .drag-box {
    width240px;
    height150px;
    line-height150px;
    text-align: center;
    border1px dashed #ddd;
    cursor: pointer;
    border-radius8px;
  }
  .drag-box:hover {
    border-color: cornflowerblue;
  }
  .drag-box.dragging {
    background-colorrgb(131161216, .2);
    border-color: cornflowerblue;
  }
  .drag-box span {
    color: cornflowerblue;
  }
  .file-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top12px;
    padding0 8px;
    border-radius4px;
    cursor: pointer;
  }
  .file-item:hover {
    background-color#f5f5f5;
    color: cornflowerblue;
  }
  .file-item .remove {
    font-size20px;
  }
</style>

如何使用


<template>
<upload
ref="uploadRef"
action="http://localhost:3000/upload"
multiple
show-file-list
drag
auto-upload
upload-folder
:headers="headers"
:data="data"
:limit="3"
:before-upload="beforeUpload"
:before-remove="beforeRemove"
@on-change="handleChange"
@on-success="handleSuccess"
@on-error="handleError"
@on-preview="handlePreview"
@on-remove="handleRemove"
@on-exceed="handleExceed"
@on-progress="handleProgress"
>

</upload>
</template>

<script setup>
import { ref } from 'vue'
import upload from '@/components/upload/index.vue'

const uploadRef = ref(null)

const data = {
name: '张三',
age: 20
}

const headers = {
a: 111
}

const handleChange = (file, fileList) => {
// console.log('onChange', file, fileList)
}
const handleSuccess = (res, file, fileList) => {
console.log(res, file, fileList)
}
const handleError = (err, file, fileList) => {
console.log(err, file, fileList)
}
const handlePreview = (file) => {
console.log('handlePreview', file)
}
const handleRemove = (file, fileList) => {
console.log('handleRemove', file, fileList)
}
const handleExceed = (files, fileList) => {
console.log('文件个数超限', files, fileList)
}
const handleProgress = (e, file, fileList) => {
console.log('上传进度');
if (e.lengthComputable) {
const percentComplete = Math.ceil((e.loaded / e.total) * 100)
console.log('[ percentComplete ] >', percentComplete)
}
}
const beforeUpload = (file) => {
return true
}
const beforeRemove = (file, fileList) => {
return true
}
</script>

<style>

</style>


作者:xintianyou
来源:juejin.cn/post/7292302859964727346
收起阅读 »

前端如何直接上传文件夹

web
前面写了一篇仿写el-upload组件,彻底搞懂文件上传,实现了选择/拖拽文件上传,我们经常看到一些网站支持直接选择整个文件夹上传,例如:宝塔面板、cloudflare托管、对象存储网站等等需要模拟文件路径存储文件的场景。那是怎么实现的呢? 依然从两方面来说:...
继续阅读 »

前面写了一篇仿写el-upload组件,彻底搞懂文件上传,实现了选择/拖拽文件上传,我们经常看到一些网站支持直接选择整个文件夹上传,例如:宝塔面板、cloudflare托管、对象存储网站等等需要模拟文件路径存储文件的场景。那是怎么实现的呢?


依然从两方面来说:



  1. input选择文件夹

  2. 拖拽文件夹


input选择文件夹


在props.js中加一个属性,upload-folder是否支持上传文件夹


export default {
// 前面的省略了...
// 是否支持选择文件夹
'upload-folder': {
type: Boolean,
default: false
}
}

改一下input标签,依然是根据props的值动态判断是否支持上传文件夹。主要是webkitdirectory这个属性,由于不是一个标准属性,需要加浏览器前缀。


<input 
type="file"
id="file"
:multiple="multiple"
:accept="accept"
:webkitdirectory="uploadFolder"
:mozdirectory="uploadFolder"
:odirectory="uploadFolder"
@change="handleChange"
>


注意:支持选择文件夹时就只能选择文件夹,无法选择文件。


那么如何获取选择的文件夹呢?其实我们最终要上传的依然是文件,也就是file对象,文件夹也是一个特殊的文件。


依然是通过inputonchange事件回调拿到上传的event


或者直接获取input这个dom对象,然后拿到files属性,结果是一样的。


// input选择文件回调
const handleChange = (event) => {
console.log('[ files ] >', event.target.files)
const inputDom = document.querySelector('#file')
console.log('[ files ] >', inputDom.files)
}


可以看到,比选择单个文件时,多了一个webkitRelativePath属性,并且它是递归选择的文件夹,拿到这个文件夹及其子文件夹下所有的文件,我们可以通过这个属性拿到上传时文件所在的文件夹名称路径


拖拽文件夹


上篇文章讲过拖拽如何拿到文件,首先要准备一个用于拖拽放置的区域。
调用upload组件时,传入drag=true


<div 
class="drag-box"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>

将文件拖到此处,或<span>点击上传span>
div>

// 拖放进入目标区域
const handleDragOver = (event) => {
event.preventDefault()
}
// 拖拽放置
const handleDrop = (event) => {
event.preventDefault()
console.log('[ event ] >', event)
}

注意:和input上传不同,拖拽时,是可以同时拖拽文件和文件夹的。


因为可以同时拖拽文件和文件夹,我们就不能直接使用event.dataTransfer.files,如果刚好拖拽进来的是一个文件,那可以这么获取,如果是个文件夹呢?那就不行了。


同时拖拽一个文件和一个文件夹


这时候就要用到event.dataTransfer.items


// 拖拽放置
const handleDrop = (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
}

打印一下看看:

得到一个List类型的数据,里面是两个DataTransferItem,控制台无法直接查看它到底是个什么玩意儿。

看MDN,也看不出它具体是个啥。既然是List,遍历一下看看:


const handleDrop = (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
for (const item of event.dataTransfer.items) {
console.log('[ item ] >', item)
}
}


可以看到不管是文件还是文件夹,都被识别成了file,只不过图片是直接能识别出type为image/png


查看MDN,developer.mozilla.org/zh-CN/docs/…


点击查看itemPrototype,发现里面有个webkitGetAsEntry方法,执行它就能拿到item的具体信息。


看方法名,带了个webkit,但是这个方法除了Android Firefox浏览器以外都可以用。


for (const item of event.dataTransfer.items) {
const entry = item.webkitGetAsEntry()
console.log(entry)
}

依然拖动上面那个图片文件和一个文件夹:


可以看出,文件夹里面还有文件和文件夹,但是只显示了一个文件和一个文件夹,看来拖拽和input上传不一样,它不会自动的把里面所有的文件递归列出来。


通过isDirectory属性,就能区分是文件还是文件夹。除了这些基础属性以外,继续查看Prototype,可以看到还有一系列方法:


先看怎么拿到文件


entry是一个文件时,它有两个方法:createWriter()file(),查看MDN,developer.mozilla.org/en-US/docs/…

createWriter()已经废弃了,而且也不是我们今天要用的。

file()才是我们要找的。


这不就是我们熟悉的file对象吗,跟input上传拿到的一毛一样。


再看怎么拿到文件夹


查看MDN的Drop API webkitGetAsEntry()方法,developer.mozilla.org/zh-CN/docs/… 可得,如果是文件夹,可以通过createReader方法创建一个文件目录阅读器,然后通过readEntries方法,重新拿到每个item,这就是event.dataTransfer.items里面的每个item

我们写一下试试

依然是之前那个图片和文件夹

只打印出了跟目录下一级的一个文件和一个文件夹,那下面还有一个文件怎么办呢?


递归呀!


写一个递归读文件的方法。


const readFiles = async (item) => {
if (item.isDirectory) {
// 是一个文件夹
console.log('=======文件夹=======');
const directoryReader = item.createReader();
// readEntries是一个异步方法
const entries = await new Promise((resolve, reject) => {
directoryReader.readEntries(resolve, reject);
});

let files = [];
for (const entry of entries) {
const resultFiles = await readFiles(entry);
files = files.concat(resultFiles);
}
return files;
} else {
// 是一个文件
console.log('=======文件=======');
// file也是一个异步方法
const file = await new Promise((resolve, reject) => {
item.file(resolve, reject);
});
console.log('[ file ] >', file);
return [file];
}
}

handleDrop方法也要改一下


// 拖拽放置
const handleDrop = async (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)

const files = [];
const promises = [];
for (const item of event.dataTransfer.items) {
const entry = item.webkitGetAsEntry();
console.log('[ entry ] >', entry);
promises.push(readFiles(entry));
}

const resultFilesArrays = await Promise.all(promises);
const allFiles = resultFilesArrays.flat();

console.log('[ All files ] >', allFiles);
}

再次拖拽上传看看

三个文件我们都拿到了。


总结


上传文件夹,还是直接使用input比较简单,使用它能直接拿到文件夹下所有的文件,以及每个文件在本地的路径,代码量也少很多。


拖拽的好处是文件和文件夹能一起上传。


作者:xintianyou
来源:juejin.cn/post/7292323606875553843
收起阅读 »