注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的而且消息已读后需要多次刷新会话列表才会清空未读消息

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的
而且消息已读后需要多次刷新会话列表才会清空未读消息

你不常用的 FileReader 能干什么?

web
前言 欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群! 本文灵感源于上周小伙伴遇到一个问题: "一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!" 这会有什么问题呢? 按原逻辑就是调用该接口后,就会一股脑...
继续阅读 »

前言



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



本文灵感源于上周小伙伴遇到一个问题:


"一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!"


1C306E8E.jpg


这会有什么问题呢?


按原逻辑就是调用该接口后,就会一股脑把该接口接返回过来的内容,直接经过 Blob 对象 转换后再通过隐藏的 <a> 标签实现下载。


但是有一个问题,那就是接口也是需要进行各种逻辑处理、判断等等,然后再决定是给前端响应一个正常的 Blob 格式的文件流,还是返回相应 JSon 格式的异常信息 等等。


如果返回了 JSon 格式的异常信息,那前端应该给用户展示信息内容,而不是将其作为下载的内容!


1C3802FC.gif


FileReader 实现 Blob 从 String 到 JSON


复现问题


为了更直观看到对应的效果,我们这里来简单模拟一下前后端的交互过程吧!


前端


由于小伙伴发送请求时使用的是 Axios,并且设置了其对应的 responsetype:blob | arraybuffer,所以这里我们也使用 Axios 即可,具体如下:


    // 发起请求
const request = () => {
axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then((res) => {

// 转换为 bloc 对象
const blob = new Blob([res.data])

// 获取导出文件名,decodeURIComponent为中文解码方法
const fileName = decodeURIComponent(res.headers["content-disposition"].split("filename=")[1])

// 通过a标签进行下载
let downloadElement = document.createElement('a');
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = fileName;
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
});
}

后端


这里我们就简单通过 koa 来实现将一个表格文件响应给前端,具体如下:


    const xlsx = require("node-xlsx");

const Koa = require("koa");
const app = new Koa();

const cors = require("koa2-cors");

// 处理跨域
app.use(
cors({
origin: "*", // 允许来自指定域名请求
maxAge: 5, // 本次预检请求的有效期,单位为秒
methods: ["GET", "POST"], // 所允许的 HTTP 请求方法
credentials: true, // 是否允许发送 Cookie
})
);

// 响应
app.use(async (ctx) => {
// 文件名字
const filename = "人员信息";

// 数据
const data = [
{ name: "赵", age: 16 },
{ name: "钱", age: 20 },
{ name: "孙", age: 17 },
{ name: "李", age: 19 },
{ name: "吴", age: 18 },
];

// 表格样式
const oprions = {
"!cols": [{ wch: 24 }, { wch: 20 }, { wch: 100 }, { wch: 20 }, { wch: 10 }],
};

// JSON -> Buffer
const buffer = JSONToBuffer(data, oprions);

// 设置 content-type
ctx.set("Content-Type", "application/vnd.openxmlformats");

// 设置文件名,中文必须用 encodeURIComponent 包裹,否则会报异常
ctx.set(
"Content-Disposition",
"attachment; filename=" + encodeURIComponent(filename) + ".xlsx"
);

// 文件必须设置该请求头,否则前端拿不到 Content-Disposition 响应头信息
ctx.set("Access-Control-Expose-Headers", "Content-Disposition");

// 将 buffer 返回给前端
ctx.body = buffer;
});

// 将数据转成 Buffer
const JSONToBuffer = (data, options = {}) => {
let xlsxObj = [
{
name: "sheet",
data: [],
},
];

data.forEach((item, idx) => {
// 处理 excel 表头
if (idx === 0) {
xlsxObj[0].data.push(Object.keys(item));
}

// 处理其他 excel 数据
xlsxObj[0].data.push(Object.values(item));
});

// 返回 buffer 对象
return xlsx.build(xlsxObj, options);
};

// 启动服务
app.listen(3000);

正常效果展示


1.gif


异常效果展示


可以看到当返回的内容为 JSON 格式 的内容时,原本逻辑在获取 filename 处就发生异常了,即使这一块没有发生异常,被正常下载下来也是不对的,因为这种情况应该要进行提示。


1.gif


并且此时直接去访问 res.data 得到的也不是一个 JSON 格式 的内容,而是一个 ArrayBuffer


image.png


返回的明明是 JSON ,但是拿到的却是 ArrayBuffer?


responseType 惹的祸


还记得我们在通过 Axios 去发起请求时设置的 responseType:'arraybuffer' 吗?


没错,就是因为这个配置的问题,它会把得到的结果给转成设置的类型,所以看起是一个 JSON 数据,但实际上拿到的是 Arraybuffer



这个 responseType 实际上就是 XMLHttpRequest.responseType,可点击该链接自行查看。



不设置 responseType 行不行?


那么既然是这个配置的问题,那么我们不设置不就好了!


确实可行,如下是未设置 responseType 获取到的结果:


image.png


但也不行,如果不设置 responseType 或者设置的类型不对,那么在 正常情况 下(即 文件被下载)时 会导致文件格式被损坏,无法正常打开,如下:


image.png


FileReader 来救场


实际上还有个比较直接的解决方案,那就是把接收到的 Arraybuffer 转成 JSON 格式不就行了吗?


1CB04D6B.jpg


没错,我们只需要通过 FileReader 来完成这一步即可,请看如下示例:


// json -> blob
const obj = { hello: "world" };

const blob = new Blob([JSON.stringify(obj, null, 2)], {
type: "application/json",
});

console.log(blob) // Blob {size: 22, type: 'application/json'}

// blob -> json
const reader = new FileReader()

reader.onload = () => {
console.log(JSON.parse(reader.result)) // { hello: "world" }
}

reader.readAsText(blob, 'utf-8')

是不是很简单啊!


值得注意的是,并不是任何时候都需要转成 JSON 数据,就像并不是任何时候都要下载一样,我们需要判断什么时候该走下载逻辑,什么时候该走转换成 JSON 数据。


怎么判断当前是该下载?还是该转成 JSON?


这个还是比较简单的,换个说法就是判断当前返回的是不是文件流,下面列举较常见的两种方式。


根据 filename 判断


正常情况来讲,再返回文件流的同时会在 Content-Disposition 响应头中添加和 filename 相关的信息,换句话说,如果当前没有返回 filename 相关的内容,那么就可以将其当做异常情况,此时就应该走转 JSON 的逻辑。


不过需要注意,有时候后端返回的某些文件流并不会设置 filename 的值,此时虽然符合异常情况,但是实际上返回的是一个正常的文件流,因此不太推荐这种方式


208EA3E8.gif


根据 Content-Type 判断


这种方式更合理,毕竟后端无论是返回 文件流 或是 JSON 格式的内容,其响应头中对应的 Content-Type,必然不同,这里的判断更简单,我们直接判断其是不是 JSON 类型即可。


更改后的代码,如下:


axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then(({headers, data}) => {
console.log("FileReader 处理前:", data)

const IsJson = headers['content-type'].indexOf('application/json') > -1;

if(IsJson){
const reader = new FileReader()

// readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob
// 若后端直接返回的就是 blob 类型,则直接使用即可
reader.readAsText(new Blob([data], {type: 'application/json'}), 'utf-8')

reader.onload = () => {
// 将字符内容转为 JSON 格式
console.log("FileReader 处理后:", JSON.parse(reader.result))
}
return
}

// 下载逻辑
download(data)
});

值得注意的是,readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob,若后端直接返回的就是 blob 类型,则直接使用即可。


image.png


FileReader 还能干什么?


以上是使用 FileReader 解决一个实际问题的例子,那么除此之外它还有什么应用场景呢?


不过我们还是先来了解一下 FileReader 的一些相关内容吧!!!


FileReader 是什么?


FileReader 对象允许 Web 应用程序 异步读取 存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。


不过还要注意如下两条规则:



  • FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中按路径名简单地读取文件

  • 要在 JavaScript 中按路径名读取文件,应使用标准 Ajax 解决方案进行 服务器端文件读取


总结起来就是,FileReader 只能读取 FileBlob 类型的文件内容,并且不能直接按路径的方式读取文件,如果需要以路径方式读取,最好要通过 服务端 返回流的形式。


四种读取方式


FileReader 可以如下四种方式读取目标文件:




  • FileReader.readAsArrayBuffer()



    • 开始读取指定的 Blob中的内容,读取完成后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象




  • FileReader.readAsBinaryString() (非标准



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含所读取文件的 原始二进制数据




  • FileReader.readAsDataURL()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 data: URL 格式的 Base64 字符串以表示所读取文件的内容




  • FileReader.readAsText()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 字符串 以表示所读取的文件内容




如上对应的方法命名十分符合顾名思义的特点,因此可以很容易看出来在不同场景下应该选择什么方法,并且如上方法一般都会配合 FileReader.onload 事件FileReader.result 属性 一起使用。


FileReader 的其他应用场景


预览本地文件


通常情况下,前端选择了相应的本地文件(图片、音/视频 等)后,需要通过接口发送到服务端,接着服务端在返回一个相应的预览地址,前端在实现支持预览的操作。


如果说现在有一个需要省略掉中间过程的需求,那么你就可以通过 FileReader.readAsDataURL() 方法来实现,但是要考虑文件大小带来转换时间快慢的问题。


这一部分比较简单,就不贴代码占篇幅了,效果如下:


1.gif


传输二进制格式数据


通常在上传文件时,前端直接将接收到的 File 对象以 FormData 发送给后端,但如果后端需要的是二进制的数据内容怎么办?


此时我们就可以使用 FileReader.readAsArrayBuffer() 来配合,为啥不用 FileReader.readAsBinaryString(),因为它是非标准的,而且 ArrayBuffer 也是原始的 二进制数据


具体代码如下:


// 文件变化
const fileChange = (e: any) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.readAsArrayBuffer(file)

reader.onload = () => {
upload(reader.result, 'http://xxx')
}
}

// 上传
const upload = (binary, url) => {
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.overrideMimeType("application/octet-stream");

//直接发送二进制数据
xhr.send(binary);

// 监听变化
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 响应成功
}
}
}
}

最后



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



上面我们通过 FileReader 解决了一个实际问题,同时也简单介绍了其相应的使用场景,但这个场景具体是否是用于你的需求还要具体分析,不能盲目使用。


以上就是本文的全部内容了,希望本文对你有所帮助!!!


21E0754A.jpg

收起阅读 »

数组去重你想到几种办法呢?

web
前言 你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对...
继续阅读 »

前言


你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对于数组去重这道简单的面试题时,我们可以回答的方法有什么吧。


数组去重


1. 不使用数组API方法


首先我来介绍一种不是用数组身上的API的去重解法,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for(let i = 0; i < array.length; i++){
for( var j = 0; j < res.length; j++){
if(array[i] === res[j]){
break;
}
}
if(j === res.length){
res.push(array[i])
}
}
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]


既然不使用数组自带的API方法,那我们首先考虑的就是用双重for循环了,如上述代码:



  1. 我们准备了一个空的结果数组

  2. 我们对需要去重的数组进行循环

  3. 在第一层数据中再套一层循环,根据下标判断结果数组内是否有重复项。


我们调用该方法,打印结构如上述代码的注解处,成功的实现了对数组的去重。


2. 使用 indexOf


既然有不使用数组API的,那就肯定有使用数组API的,下面看我使用indexOf完成数组的去重,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for (let i = 0; i < array.length; i++) {
if (res.indexOf(array[i]) === -1) { // 返回找到的第一个值得下标
res.push(array[i])
}
}
return res
}
console.log(unique(array))// [ '1', 1, '2', 2 ]


如上述代码, 我们巧妙了使用了indexOf查找结果数组中是否已经存在,如果不存在才向结果数组中添加,实现了数组去重。


在上述代码的基础上,我们还可以转变一下,将for循环内的语句改为


if (array.indexOf((array[i])) == array.lastIndexOf(array[i])) {
i++
} else {
array.splice(array.lastIndexOf(array[i]), 1)
}

不新增其他变量,直接通过indexOf和lastIndexOf判断该值是否在原数组内为唯一值,从而直接修改原数组,实现数组的去重。


3. 使用 sort


对于数组去重,我们除了通过下标找出是否有重复项之外,我们还可以先排序,然后在判断前后项是否相同来实现去重,代码如下:


var  array = [1, 3, 5, 4, 2, 1, 2, 4, 4, 4]
function unique(array) {
let res = []
let sortedArray = array.concat().sort() //concat() 返回新的数组
let seen;
for (let i = 0; i < sortedArray.length; i++) {
if (!i || seen !== sortedArray[i]) {
res.push(sortedArray[i])
}
seen = sortedArray[i]
}
return res
}
console.log(unique(array)); // [ 1, 2, 3, 4, 5 ]

如上述代码, 我们先获取一个排好序的新数组,再对新数组进行循环,判断保存前一个值的seen与当前值是否相同来实现数组去重。


温馨小提示: 由于数组的排序方法不能区分数组和字符串,所以想要使用此方法必须要保证数组的值的类型相同,不然会出bug


4. 使用 filter


既然都用到了sort排序了,那我直接抬出ES6数组新增的filter过滤器API也不过分吧,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = array.filter((item, index, array) => {
return array.indexOf(item) === index
})
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

如上述代码,filter直接使用array.indexOf(item) === index作为过滤条件返回出一个新的数组,实现数组去重。


如上述代码,我们结合了 indexOf方法作为过滤条件,那我们也可以结合一下sort方法吧,直接使用一行代码就解决了数组的去重。代码如下:


function unique(array) {
return array.concat().sort().filter((item, index, array) => !index || item !== array[item - 1])
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

5. 使用Set、Map、或者对象


除了上述的通过数组API和不使用数组API的方法外,我们还能想到的就是借助对象来实现数组的去重。使用Set数据结构是我们最容易想到的办法,使用Map与对象方法的相似,都是以数组的值作为key,再将所有的可以取出来组成一个数组。 我就不给小伙伴们演示代码了,感兴趣的小伙伴可以自己动手试试。


(对于对象的key只能为字符串这个问题,我们可以换个思路,将下标存为key,值存为value,判断不同key的值相不相同来实现数组去重。我们还可以在存key时加上其类型,然后进行一次转换。)


自己封装一个去重API


在介绍上述数组去重的方法后,我们再来总结一下,将其融合成一个有复用性,而且还可以适用不同情况的API方法。


我来介绍一下如下我封装的一个数组去重的API方法,



  1. 该方法可接受三个参数,第一个参数为需要去重的数组,第二个参数为该数组是否为排好序的数组,第三个参数为一个回调函数

  2. 该回调函数也有三个参数,分别为值,下标,需要去重数组。该回调函数的作用是方便用户对数组进行一些额外的处理(例如将大写转为小写)

  3. 第二,三参数可不传递。


var array = [1, 2, '1', 'a', 'A', 2, 1]
var array2 = [1, 1, '1', 2, 2]
function uniq(array, isSorted, iteratee) {
let seen = []
let res = []
for(let i = 0; i < array.length; i++){
let computed = iteratee ? iteratee(array[i], i,array) : array[i]
if(isSorted) {
if(!i || seen !== array[i]){
res.push(array[i])
}
seen = array[i]
}else if(iteratee) {
if(seen.indexOf(computed) === -1){
seen.push(computed)
res.push(computed)
}
}
else {
if(res.indexOf(array[i]) === -1) {
res.push(array[i])
}
}
}
return res
}
let result = uniq(array, false, function(item, index, arr){
return typeof item == 'string' ? item.toLowerCase() : item
})
console.log(result); // [ 1, 2, '1', 'a' ]
console.log(uniq(array2, true)); // [ 1, 2 ]

总结


对于数组的去重,当我们能在面试中说到这个多方法的话,这道面试题也就过了,虽然这道面试不难,但如果我们想要想到这个多方法的话,还是

作者:潘小七
来源:juejin.cn/post/7248835844659970105
需要许多知识储备的。

收起阅读 »

在高德地图实现卷帘效果

web
介绍 今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。 实现思路 1.创建目标图...
继续阅读 »

介绍


今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。


实现思路


1.创建目标图层,这里除了有一个默认的底图,还增加了卫星影像图和路网图层,后两者是可以被掩模的。因此在创建图层时通过设置rejectMapMask(默认值false)让图层是否允许被掩模。


2.提供实时设置掩模的方法renderMask,核心代码只需要map.setMask(mask)。


3.实现拖拽交互逻辑,监听拖拽过程,实时触发 renderMask


实现代码


1.创建目标图层


// 基础底图
const baseLayer = new AMap.TileLayer({
zIndex: 1,
//拒绝被掩模
rejectMapMask: true,
})

map = new AMap.Map('container', {
center:[116.472804,39.995725],
viewMode:'3D',
labelzIndex:130,
zoom: 5,
cursor:'pointer',
layers:[
// 底图,不掩模
baseLayer,
// 路网图层
new AMap.TileLayer.RoadNet({
zIndex:7
}),
// 卫星影像图层
new AMap.TileLayer.Satellite()
]
});

2.提供实时设置掩模的方法


function renderMask(){
// 当前地图范围
const {northEast, southWest} = map.getBounds()
// 地理横向跨度
const width = northEast.lng - southWest.lng
// 拖拽条位置占比例
const dom = document.querySelector('#dragBar')
const ratio = Math.ceil(parseInt(dom.style.left) + 5) / map.getSize().width

let mask = [[
[northEast.lng, northEast.lat],
[southWest.lng+ width * ratio, northEast.lat],
[southWest.lng+ width * ratio, southWest.lat],
[northEast.lng, southWest.lat]
]]

map.setMask(mask)
}

3.实现拖拽交互逻辑


// 拖拽交互
function initDrag(){

const dom = document.querySelector('#dragBar')
dom.style.left = `${map.getSize().width/2}px`

// const position = {x:0, y:0}
interact('#dragBar').draggable({
listeners: {
start (event) {
// console.log(event.type, event.target)
},
move (event) {
// 改变拖拽条位置
const left = parseFloat(dom.style.left)
const targetLeft = Math.min(Math.max(left + event.dx, 0), map.getSize().width - 10)
dom.style.left = `${targetLeft}px`

if(event.dx !== 0){
renderMask()
//必须!强制地图重新渲染
map.render()
}
},
end(event){
// console.log(event.type, event.target)
}
}
})
}


  1. 启动相关方法,完善交互逻辑


initDrag()
renderMask()
map.on('mapmove', renderMask)
map.on('zoomchange', renderMask)
window.addEventListener('resize', renderMask)

相关链接


本文代码演示


jsfiddle.net/gyratesky/z…


maptalks 图层卷帘效果


maptalks.org/examples/cn…


卫星+区域掩模


lbs.amap.com/demo/j

avasc…

收起阅读 »

正则别光想着抄,看懂用法下次你也会写

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一些高级的正则表达式用法。


校验字符串是否包含大小写字母+数字+特殊字符,并且长度为8-12。


如果想要使用单个正则表达式就解决上述问题,就需要稍微学习一下正则的一些高级用法了。


^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$

先行断言(预搜索)


先行断言中不会获取任何内容,只是做一次筛查



  • 正向先行 指在某个位置向右看,该位置必须能匹配该表达式(?=表达式)。

  • 反向先行 指在某个位置往右看,该位置保证不能出现的表达式。

  • 正向后行 指在某个位置向左看,该位置必须能匹配该表达式,但不会获取表达式的内容(?<=表达式)

  • 反向后行 指在某个位置往左看,该位置保证不能出现的表达式(?<!表达式)


这个正则表达式使用了正向先行断言来同时检查字符串中是否包含大小写字母、数字和特殊符号。它的含义如下:



  • ^:匹配字符串的开头。

  • (?=.*[a-z]):正向先行断言,要求字符串中至少包含一个小写字母。

  • (?=.*[A-Z]):正向先行断言,要求字符串中至少包含一个大写字母。

  • (?=.*\d):正向先行断言,要求字符串中至少包含一个数字。

  • (?=.*[!@#$%^&*()_+]):正向先行断言,要求字符串中至少包含一个特殊符号(这里列出了一些常见的特殊符号,你可以根据需要添加或修改)。

  • [a-zA-Z\d!@#$%^&*()_+]:匹配允许的字符集合,包括大小写字母、数字和特殊符号。

  • {8,12}:限定字符串的长度在 8 到 12 位之间。

  • $:匹配字符串的结尾。


使用这个正则表达式可以对目标字符串进行检查,判断是否满足包含大小写、数字和特殊符号,并且长度为 8 到 12 位的要求。例如:


let regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$/;
let str = "Password123!";
let isMatch = regex.test(str);
console.log(isMatch); // 输出: true

获取ip地址


当处理日志文件时,有时需要从日志文本中提取特定的信息。一个常见的场景是提取日志中的 IP 地址。


假设我们有一个日志文件,其中包含了多行日志记录,每行记录的格式如下:


[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home

在上述示例中,我们使用 match 方法来执行正则表达式匹配,并将匹配的结果存储在 match 变量中。如果有匹配结果,我们可以从数组中取得第一个元素 match[0],即提取到的 IP 地址。


let logText = "[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home";
let regex = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
let match = logText.match(regex);
if (match) {
let ipAddress = match[0];
console.log(ipAddress); // 输出: 192.168.0.1
} else {
console.log("No IP address found.");
}

非捕获型分组


非捕获型分组是正则表达式中的一种分组语法,用于对一组子表达式进行逻辑组合,但不会捕获匹配的结果。它以 (?: 开始,并以 ) 结束。


/\b(?:\d{1,3}\.){3}\d{1,3}\b/

解释一下这个正则表达式:



  • \b:单词边界,用于确保 IP 地址被完整匹配。

  • (?:\d{1,3}\.){3}:非捕获型分组,匹配由 1 到 3 个数字和一个点组成的序列,重复匹配 3 次,用于匹配 IP 地址的前三个数字和点的部分。

  • \d{1,3}:匹配由 1 到 3 个数字组成的序列,用于匹配 IP 地址的最后一个数字。

  • \b:单词边界,用于确保 IP 地址被完整
    作者:simple_lau
    来源:juejin.cn/post/7248832185808617509
    匹配。

收起阅读 »

从张鑫旭大佬文章中发现了我前端知识的匮乏

web
最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。 我们先看下页面是怎...
继续阅读 »

最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。


我们先看下页面是怎么样的:


chrome-capture-2023-5-26.gif


功能很简单,就是复制下面的二维码图片,然后粘贴到文本框中,最后点击识别按钮,把识别二维码的结果展示到下面。


源代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>qrcode</title>
<style>
.area {
height: 200px;
border: 1px dashed skyblue;
background-color: #fff;
display: grid;
place-items: center;
margin-top: 20px;
}
.area:focus {
border-style: solid;
}
.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}
.button {
margin: 1rem auto;
width: 160px;
height: 40px;
font-size: 112.5%;
background-color: #eb4646;
color: #fff;
border: 0;
border-radius: 0.25rem;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<div class="container">
<input id="file" class="file" type="file" accept="image/png" />
<div id="area" class="area" tabindex="-1"></div>
</div>
<p align="center">
<button id="button" class="button">识别</button>
</p>

<p id="result" align="center"></p>

<p align="center">
方便大家复制的示意图:<br /><img
src="./qrcode.png"
style="margin-top: 10px"
/>

</p>

<script>
var reader = new FileReader()
reader.onload = function (event) {
area.innerHTML = '<img src="' + event.target.result + '">'
}
document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

file.addEventListener('change', function (event) {
const file = event.target.files && event.target.files[0]
if (file) {
reader.readAsDataURL(file)
}
})

button.addEventListener('click', function () {
if ('BarcodeDetector' in window) {
// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})

const eleImg = document.querySelector('#area img')
if (eleImg) {
barcodeDetector
.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})
} else {
result.innerHTML = `<span class="error">请先粘贴二维码图片</span>`
}
} else {
result.innerHTML = `<span class="error">当前浏览器不支持二维码识别</span>`
}
})
</script>
</body>
</html>

背景交代完成,现在就一点一点的来分析其中代码的精妙之处。


CSS部分


tabindex = -1


<div id="area" class="area" tabindex="-1"></div>

当我看到tabindex这个属性时,完全不知道它的用法,于是我继续在张鑫旭大佬的博客中搜索,找到一篇叫《HTML tabindex属性与web网页键盘无障碍访问》的文章,这里简要说下这个属性的用法和作用。


tabindex属性是一个全局属性,也就是所有 HTML 标签都可以用的属性,比方说idclass属性等。所以,可以在div上使用。同时,这个属性是一个非常老的属性,没有兼容性问题,放心使用。


tabindex属性是一个与键盘访问行为息息相关的属性。平常可能感觉不到它的价值,但是一旦我们的鼠标坏掉了或者没电了,我们就只能使用键盘。亦或者在电视机上,或者投影设备上访问我们的网页的时候,我们只能使用遥控器。就算设备都完全正常,对于资深用户而言,键盘访问可以大大提高我们的使用效率。


当一个元素设置tabindex属性值为-1的时候,元素会变得focusable,所谓focusable指的是元素可以被鼠标或者JS focus,在 Chrome 浏览器下表现为会有outline发光效果,IE浏览器下是虚框,同时能够响应focus事件。默认的focusable元素有<a>, <area>, <button>, <input>, <object>, <select> 以及 <textarea>


但是,tabindex = -1不能被键盘的tab键进行focus。这种鼠标可以focus,但是键盘却不能focus的状态,只要tabindex属性值为负值就可以了。


因此,我们可以设置divfocus的样式,当鼠标点击div时,我们可以改变它的边框,如下:


.area:focus {
border-style: solid;
}

tabindex属性值是一个整数,它来决定被tabfocus的顺序,顺序越小越先被focus,但是 0除外,如下divfocus的顺序依次是:1,2,3。


<div id="area" class="area" tabindex="1"></div>
<div class="area" tabindex="3"></div>
<div class="area" tabindex="2"></div>

tabindex="0"又是怎么回事呢?


元素设置tabindex="-1",可以鼠标和JS可以focus,但键盘不能focus


tabindex="0"tabindex="-1"的唯一区别就是键盘也能focus,但是被focus的顺序是最后的。或者你可以这么理解,<div>设置了tabindex="0",从键盘访问的角度来讲,相对于<div>元素变成了<button>元素。


垂直居中


垂直居中是一个常用的需求了,我经常使用flex来完成:


display: flex;
align-items: center;
justify-content: center;

在大佬的文章中使用了一个新的用法:


display: grid;
place-items: center;

place-items 属性是以下属性的简写:align-itemsjustify-items


:empty::before


div元素没有内容时,.area:empty样式会生效,同时为了显示一段提示内容,使用了伪元素::before,在content写入提示内容。


.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}

JS部分


copy paste 事件


document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

这两个事件都属于ClipboardEvent事件(剪切板事件),还有一个cut剪切事件。


wrap.oncopy = function(event){}
wrap.oncut = function(event){}
wrap.onpaste = function(event) {}

任何软件上的内容,可以被复制粘贴,是因为软件对操作系统复制粘贴操作的实现,软件都会把复制剪切的内容存入操作系统的剪切板上。同样,浏览器也对操作系统的剪切板进行了实现,属于浏览器的自身的实现。


浏览器复制操作的默认行为是触发浏览器的 copy 事件,将 copy 的内容存入操作系统的剪切板中。


那如何干预浏览器的这种默认的复制粘贴操作呢?


可以通过event.preventDefault阻止事件的默认行为,即当触发这三个事件时,阻止对系统剪切板的数据操作。然后,我们对数据进行加工后,重新写入到剪贴板。


比如,当用户复制我们网站的内容时,可以在数据后面加一个版权的相关信息。


<div id="wrap">这是复制的复制内容</div>
<script>
var wrap = document.getElementById('wrap')
wrap.oncopy = function (event) {
// 通过copy事件监听,阻止将选中内容复制到系统剪切板上
event.preventDefault()
// 获取选中内容对象
const selection = document.getSelection()
// selection对象重构了toSring()方法,获取selection对象的选中内容
var selectContent = selection.toString()
var dealContent =
selectContent +
'转载请联系作者,内容地址:xxxxx'
// 把重写后的内容写入到剪贴板
event.clipboardData.setData('text/plain', dealContent)
}
</script>


ClipboardEvent 事件有个最重要的属性clipboardData,该属性值是DataTransfer对象,这个对象在拖拽场景中经常使用,后面会专门写一篇文章来说说这个对象。


new BarcodeDetector解析二维码


// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})
barcodeDetector.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})

浏览器提供了原生的API来解析二维码和条形码,即 Barcode Detection API


formats表示要解析那种码,如下图所示:


image.png


总结


通过学习上面的代码,可以发现自己在 css,js 方面上的不足,原因是缺乏探索性,老是用已有的知识来解决问题,或者直接去 github 上找第三方库,其实可以使

作者:小p
来源:juejin.cn/post/7248874230862233655
用最简单的方式实现。

收起阅读 »

从 0 到 1 实现一个 Terminal 终端

web
前言 之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。 每一步骤后都有对应的 com...
继续阅读 »

前言



之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。



每一步骤后都有对应的 commit 记录;


源码地址:github.com/ljq0226/my-… 欢迎 Star ⭐️⭐️⭐️


体验地址: my-terminal.netlify.app/



搭建环境


我们使用 vite 构建项目,安装所需要的依赖库:



  • @neodrag/react (拖拽)

  • tailwindcss

  • lucide-react (图标)
    步骤:

  • pnpm create vite

  • 选择 React+TS 模版

  • 安装依赖:pnpm install @neodrag/react lucide-react && pnpm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p
    配置 tailwind.config.js:


/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}


仓库代码:commit1


开发流程


搭建页面


image.png


以上是终端的静态页面,样式这里就不在详细展开了,此次代码仓库 commit2 。 接下来我们为该终端添加拖拽效果:


//App.tsx
···
import type { DragOptions } from '@neodrag/react'
import { useRef, useState } from 'react'

function APP(){
const [position, setPosition] = useState({ x: 0, y: 0 })
const draggableRef = useRef(null)
// 初始化 dragable 拖拽设置
const options: DragOptions = {
position,
onDrag: ({ offsetX, offsetY }) => setPosition({ x: offsetX, y: offsetY }),
bounds: { bottom: -500, top: 32, left: -600, right: -600 },
handle: '.window-header',
cancel: '.traffic-lights',
}
useDraggable(draggableRef, options)

}

return (
<div ref={draggableRef}> //将 draggableRef 挂在到节点上

</div>

)
···

这样我们的 Terminal 终端就有了拖拽效果,其它 API 方法在@neodrag/react 官网中,代码仓库 commit3


terminal2.gif


输入命令


一个终端最重要的当然是输入命令了,在这我们使用 input 框来收集收集输入命令的内容。
由于我们每次执行完一次命令之后,都会生成新的行,所以我们将新行封装成一个组件,Row 组件接收两个参数(id:当前 Row 的唯一标识;onkeydown:监听 input 框的操作):


// components.tsx
interface RowProps {
id: number
onkeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void
}
const Row: React.FC<RowProps> = ({ id, onkeydown }) => {

return (
<div className='flex flex-col w-full h-12'>
<div>
<span className="mr-2 text-yellow-400">funnycoder</span>
<span className="mr-2 text-green-400">@macbook-pro</span>
<span className="mr-2 text-blue-400">~{dir}</span>
<span id={`terminal-currentDirectory-${id}`} className="mr-2 text-blue-400"></span>
</div>
<div className='flex'>
<span className="mr-2 text-pink-400">$</span>
<input
type="text"
id={`terminal-input-${id}`}
autoComplete="off"
autoFocus={true}
className="flex-1 px-1 text-white bg-transparent outline-none"
onKeyDown={onkeydown}
/>

</div>

</div>

)
}

一开始的时候,我们通过初始化一个 Row 进行操作,我们所有生成的 Row 通过


//app.tsx
const [content, setContent] = useState<JSX.Element[]>(
[<Row
id={0}
key={key()} // React 渲染列表时需要key
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, 0)}
/>,
])

content 变量来存储,在后续我们经常要修改 content 的值,为了简化代码我们为 setContent 封装成 generateRow 方法:


// 生成内容
const generateRow = (row: JSX.Element) => {
setContent(s => [...s, row])
}

问题来了,当我们获取到了输入的命令时,怎么执行对应的方法呢?


每一个 Row 组件都有 onKeyDown事件监听,当按下按键时就调用 executeCommand 方法,通过 input 框的 id 获取该 input 框 dom 节点, const [cmd, args] = input.value.trim().split(' ') 获取执行命令 cmd 和 参数 args,此时根据 event.key 按键操作执行对应的方法:


 // 执行方法
function executeCommand(event: React.KeyboardEvent<HTMLInputElement>, id: number) {
const input = document.querySelector(`#terminal-input-${id}`) as HTMLInputElement
const [cmd, args] = input.value.trim().split(' ')
if (event.key === 'ArrowUp')
alert(`ArrowUp,Command is ${cmd} Args is ${args}`)

else if (event.key === 'ArrowDown')
alert(`ArrowDown,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Tab')
alert(`Tab,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Enter')
alert(`Enter,Command is ${cmd} Args is ${args}`)
}

接下来我们测试一下,输入cd desktop,按下 Enter 键:
terminal3.gif


代码仓库 commit3


构建文件夹系统


终端的最常用的功能就是操作文件,所以我们需要构建一个文件夹系统,起初,在我的项目中使用的是一个数组嵌套,类似下面这种


image.png


这种数据结构的话,每次寻找子项的都需要递归计算,非常麻烦。在这我们采用 map 进行存储,将数据扁平化:


image.png


代码仓库 commit4


执行命令


准备工作


我们先介绍一下几个变量:



  • currentFolderId :当前文件夹的 id,默认为 0 也就是最顶层的文件夹

  • currentDirectory : 当前路径

  • currentId : input 输入框的 id 标识


  const [currentId, setCurrentId] = useState<number>(0)
const [currentFolderId, setCurrentFolderId] = useState(0)
const [currentDirectory, setCurrentDirectory] = useState<string>('')

并把一些静态组件封装在 components.tsx 文件中:


image.png


核心介绍


我们用一个对象来存储需要执行对应的方法:


  const commandList: CommandList = {
cat,
cd,
clear,
ls,
help,
mkdir,
touch,
}

executeCommand 方法中,如果用户按下的是'Enter' 键,我们首先判断下输入的 cmd 是否在 commandlist 中,如果存在,就直接执行该方法,如果不存在,就生成一个 CommandNotFound
行:


//app.js 
function executeCommand(){
//...
else if (event.key === 'Enter') {
// 将新输入 command 加入 commandHistory 中
const newArr = commandHistory
newArr.push(input.value.trim())
setCommandHistory(newArr)
// 如果输入 command 符合就执行 ⭐️⭐️⭐️
if (cmd && Object.keys(commandList).includes(cmd))
commandList[cmd](args)
else if (cmd !== '')
generateRow(<CommandNotFound key={key()} command={input.value.trim()} />)
// 每次无论 command 符不符合,都需要生成一行新的 Row,并且 curentId++
setCurrentId(id => id + 1)
setTimeout(() => {
generateRow(
<Row
key={key()}
id={commandHistory.length}
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, commandHistory.length)}
/>,
)
}, 100)
}
//...
}

help


当输入的 cmd 识别为'help'时就会调用该方法,生成在 components.tsx 里 Help()中定义好的静态数据:


  // help 命令
const help = () => {
generateRow(<Help key={key()} />)
}

代码仓库:commit5


cd


首先,默认的currentFolderId为 0,也就是指向我们的根文件夹,我们可以通过 folderSysteam.get(currentFolderId) 来获取当前文件夹下的信息,包括该文件夹的 title,子文件的 id 数组 childIds
当我们获取到了参数 arg 时,首先要判断 是否为空或者'..',若是的话,即返回上一层目录,
如果是正常参数的话,通过 folderSysteam.get(currentFolderId) 获取子目录的 childIds 数组,遍历当前目录下的子目录,找到子目录中 title 和 arg 一样的目录并返回该子目录 id,将 currentFolderId 设置为该子目录 id 并且拼接文件路径:


  // cd 命令
const cd = (arg = '') => {
const dir: string = localStorage.getItem(CURRENTDIRECTORY) as string
//判断是否返回上一层目录
if (!arg || arg === '..') {
// 处理文件路径
const dirArr = dir.split('/')
dirArr.length = Math.max(0, dirArr.length - 2)
//区分是否是root层
if (!dirArr.length)
setCurrentDirectory(`${dirArr.join('')}`)
else
setCurrentDirectory(`${dirArr.join('')}/`)
// 将当前目录设置为上一层目录
setCurrentFolderId(folderSysteam.get(`${currentFolderId}`)?.parentId as number)
return
}
//若是正常的跳转子目录
//根据 arg 参数获取需跳转目录的 id
const id = searchFile(arg)
// 如果子目录存在,设置路径、更新当前目录id
if (id) {
const res = `${dir + folderSysteam.get(`${id}`)?.title}/`
setCurrentFolderId(id)
setCurrentDirectory(res)
}
// 否则返回 NoSuchFileOrDirectory
else { generateRow(<NoSuchFileOrDirectory key={key()} command={arg}/>) }
}
const searchFile = (arg: string) => {
// 对输入做一个优化,例如文件夹名为 Desktop,只要我们输入'Desktop'|'desktop'|'DESKTOP'都行
const args = [arg, arg.toUpperCase(), arg.toLowerCase(), arg.charAt(0).toUpperCase() + arg.slice(1)]
// 获取当前目录下子目录
const childIds = getStorage(CURRENTCHILDIDS)
// 遍历子目录,找到title 为 arg 的目录
for (const item of folderSysteam.entries()) {
if (childIds.includes(item[1].id) && args.includes(item[1].title))
return item[1].id
}
}


ls


  // ls 命令
const ls = () => {
let res = ''
// 获取当前目录下所有子目录 id
const ids = getStorage(CURRENTCHILDIDS)
// 遍历 id 进行拼接
for (const id of ids)
res = `${res + folderSysteam.get(`${id}`)?.title} `
if (!res) {
generateRow(<div key={key()} >There are no other folders or files in the current directory.</div>)
}
else {
res.split(' ').map((item: string) =>
generateRow(<div key={key()} className={item.includes('.') ? 'text-blue-500' : ''}>{item}</div>),
)
}
}

terminal6.gif


代码仓库:commit6| commit6.1


mkdir、touch


创建文件或文件夹,我们只需要创建该文件或文件夹对象,新对象的 parentId 指向当前目录,其新 id 加入到当前目录的 childIds 数组中,最后再更新一下 folderSysteam 变量:


  // mkdir 命令
const mkdir = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
childIds: [],
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}
// touch 命令
const touch = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
content: <div ><h1>
This is <span className='text-red-400 underline'>{arg}</span> file!
</h1>
<p>Imagine there's a lot of content here...</p>
</div>
,
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}


terminal7.gif


代码仓库:commit7


cat、clear


cat 命令只需要展示子文件的 content 属性值即可:


  // cat 命令
const cat = (arg = '') => {
//获取当前目录下 childIds 进行遍历
const ids = getStorage(CURRENTCHILDIDS)
ids.map((id: number) => {
const item = folderSysteam.get(`${id}`) as FolderSysteamType
//生成 title 为 arg 文件的 content Row 行
return item.title === arg ? generateRow(<div key={key()}>{item.content}</div> as JSX.Element) : ''
})
}

clear 命令只需要调用 setContent():


  // clear 命令
const clear = () => {
setContent([])
//清空 input 框内容
const input = document.querySelector('#terminal-input-0') as HTMLInputElement
input.value = ''
}

terminal8.gif
代码仓库:commit8


其它操作


准备工作


我们先介绍一下几个变量:



  • commandHistory : 用于存储输入过的 command数组

  • changeCount : 用来切换 command 计数


  const [changeCount, setChangeCount] = useState<number>(0)
const [commandHistory, setCommandHistory] = useState<string[]>([])

上下键切换 command


上面定义的 changeCount 变量默认为 0,当我们按上🔼键时,changeCount-1,当我们按下🔽键时,changeCount+1。
而当 changeCount 变量变化时,获取当前 input dom 节点,设置其值为commandHistory[commandHistory.length + changeCount],这样我们的上下键切换 command 就实现了:


    // 当按下上下键时 获取历史 command
useEffect(() => {
const input = document.querySelector(`#terminal-input-${commandHistory.length}`) as HTMLInputElement
if (commandHistory.length)
input.value = commandHistory[commandHistory.length + changeCount]
if (!changeCount) {
input.value = ''
setChangeCount(0)
}
}, [changeCount])

// 按向上🔼键
function handleArrowUp() {
setChangeCount(prev => Math.max(prev - 1, -commandHistory.length))
}
// 按向下🔽键
function handleArrowDown() {
setChangeCount(prev => Math.min(prev + 1, 0))
}
// 执行方法
function executeCommand(...) {
//...
if (event.key === 'ArrowUp') {
handleArrowUp()
}
else if (event.key === 'ArrowDown') {
handleArrowDown()
}
//...

Tab 键补全 command


根据历史记录补全 command ,利用 Array.filter() 和 String.startsWith() 就行:


  // 匹配历史 command 并补充
const matchCommand = (inputValue: string): string | null => {
// 遍历历史command 返回以当前输入 command 值开头(startsWith)的 command
const matchedCommands = commandHistory.filter(command => command.startsWith(inputValue))
return matchedCommands.length > 0 ? matchedCommands[matchedCommands.length - 1] : null
}


代码仓库:commit9


最后


大家有兴趣的话可以自己再去二次改造或添加一些新玩法,此组件已通过 Netlify 部署上线,地址为 my-terminal.netlify.app/
项目源代码:github.com/ljq0226/my-… 欢迎 S

作者:Aphelios_
来源:juejin.cn/post/7248599585735098405
tar ⭐️⭐️⭐️

收起阅读 »

前端面试题 - 96. hash 和 history 的区别?

web
hash和history是Web开发中常用的两个概念,它们都与浏览器URL相关。 Hash(哈希) URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个ha...
继续阅读 »

hashhistory是Web开发中常用的两个概念,它们都与浏览器URL相关。


Hash(哈希)


URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个hashchange事件。


// 监听 hashchange 事件
window.addEventListener('hashchange', function() {
var currentHash = window.location.hash;

// 根据不同的哈希值执行相应的操作
if (currentHash === '#section1') {
console.log('显示第一部分的内容')
} else if (currentHash === '#section2') {
console.log('显示第二部分的内容')
} else {
console.log('其他操作')
}
});

通过监听此事件,你可以根据哈希的变化来执行相应的操作,例如显示不同的内容或调用特定的函数。哈希可以直接通过JavaScript进行修改,例如window.location.hash = "section2",URL将变为(此时hashchange事件也会触发):


https://example.com/page.html#section2
// 输出 显示第二部分的内容

History(历史记录)


历史记录是浏览器跟踪用户访问过的URL的一种机制。通过history对象,你可以在JavaScript中操作浏览器的历史记录。一些常用的方法包括history.pushState()history.replaceState()history.back()。这些方法允许你添加、替换和移动浏览器的历史记录,并且不会导致页面的实际刷新。当历史记录发生变化时,浏览器不会重新加载页面,但可以通过popstate事件来捕获这些变化并做出响应。


示例:


// 添加新的历史记录
history.pushState({ page: "page2" }, "Page 2", "page2.html");

// 监听 popstate 事件
window.addEventListener('popstate', function(event) {
var state = event.state;
console.log(state)
// 根据历史记录的变化执行相应的操作
if (state.page === "page1") {
console.log('显示第一页的内容')
} else if (state.page === "page2") {
console.log('显示第二页的内容')
} else {
console.log('其他操作')
}
});

需要注意的是,使用pushState()方法修改历史记录并不会触发popstate事件。只有在用户点击浏览器的前进或后退按钮时,或者通过JavaScript代码调用history.back()history.forward()history.go()方法导致历史记录变化时,popstate

作者:总瓢把子
来源:juejin.cn/post/7248608019851755575
e>事件才会被触发。

收起阅读 »

面试官: 既然有了 cookie 为什么还要 localStorage?😕😕😕

web
Web Storage Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端...
继续阅读 »

Web Storage


Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。


Web Storage 规范最新的版本是第 2 版,这一版规范主要有两个目标:



  1. 提供在 cookie 之外的存储会话数据的途径;

  2. 提供跨会话持久化存储大量数据的机制;


Web Storage 定义了两个对象: localStoragesessionStorage。前者是永久存储机制,而后者是跨会话的存储机制。这两个浏览器存储 API 提供了在浏览器中不收页面刷新影响而存储数据的两种方式。


Storage 类型


Storage 类型用于保存 名/值 对数据,直至存储空间上限(由浏览器决定)。Storage 的实例与其他对象一样,但增加了以下方法:



  1. clear(): 删除所有值;

  2. getItem(name): 取得给定 name 值;

  3. key(index): 取得给定数值位置的名称;

  4. removeItem(name): 删除给定 name名/值 对;

  5. setItem(name,value): 设置给定 name 的值;


getItem()removeItem(name)setItem() 方法可以直接或间接通过 Storage 对象调用。因为每个数据项都作为属性存储在该对象上,所以可以使用点或括号操作符访问这些属性,统统同样的操作来设置值,也可以使用 delete 操作符来删除属性。即便如此,通常还是建议使用方法而非属性来执行这些操作,以免意外重写某个已存在的对象成员。


localStorage 对象


在修订的 HTML5 规范里,localStorage 对象取代了 globalStorage,作为在客户端持久存储数据的机制,要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在想用的端口上使用相同的协议。


因为 localStorageStorage 的实例,所以可以像使用 sessionStorage 一样使用 localStorage。具体实例请看下面几个例子:


// 使用方法存储数据
localStorage.setItem("moment", 777);

// 使用属性存储数据
localStorage.nickname = "moment";

// 使用方法获取数据
const name = localStorage.getItem("moment");

// 使用属性获得数据
const nickname = localStorage.nickname;

两种存储方法的区别在于,存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口,标签也或重新启动浏览器而丢失。


存储事件


每当 Storage 对象发生变化时,都会在文档上触发 storage 事件,使用属性或者 setItem() 设置值、使用 deleteremoveItem() 删除值,以及每次调用 clean() 时都会触发这个事件,这个事件的事件对象有如下四个属性:



  1. domain: 存储变化对应的域;

  2. key: 被设置或删除的键;

  3. newValue: 键被设置的新值,若键被删除则为 null;

  4. oldValue: 键变化之前的值。


我们可以使用如下代码监听 storage 事件:


window.addEventListener("storage", function (e) {
document.querySelector(".my-key").textContent = e.key;
});

对于 sessionStoragelocalStorage 上的任何更改都会触发 storage 事件,但 storage 事件不会区分这两者。


这是一道面试题


在不久前,被问到这样一个问题,我们通过后端返回来的 token 为什么是存储在 localStorage 而不是存储在 cookie 中?


考虑这个问题的首先我们应该知道,token 就是一个字符串,而使用 cookie 的话,大小是满足的,所以考察的点就不在这个内存上面了。


之所以使用 localStorage 存储 token,而不是使用 cookie,这可能基于以下几个方面考虑:



  1. 前后端分离架构: 在一些现代的 Web 应用程序中,前端和后端通常是通过 API 进行通信的,而不是使用传统的服务器端渲染。在这种情况下,前端可能是一个独立的应用程序,如基于 JavaScript 的单页应用或移动应用程序。由于前端和后端是分离的,Cookie 在这种架构中不太容易管理,因为跨域请求可能会遇到一些限制。localStorage 提供了一种更方便的解决方案,前端应用程序可以直接访问和管理存储在本地的令牌;

  2. 安全性需求: 在某些情况下,开发者可能认为将令牌存储在 Cookie 中存在一些安全风险,尤其是在面对跨站脚本攻击 XSS 时。使用 localStorage 可以减少某些安全风险,因为 LocalStorage 中的数据不会自动发送到服务器,且可以通过一些安全措施(如加密)来增强数据的安全性;

  3. 令牌过期处理: 使用 localStorage 存储令牌可以让令牌在浏览器关闭后仍然保持有效,这在某些应用场景下是有用的。例如,用户可能关闭了浏览器,然后再次打开时仍然保持登录状态,而不需要重新输入凭据;


值得注意的是,使用 localStorage 存储 token 也不是说百分百安全的,依然会存在一些问题和风险,如容易收到 XSS 攻击、不支持跨域贡献等。因此,在使用 localStorage 存储令牌时,开发者需要采取适当的安全措施,如加密存储数据、定期更新令牌等,以确保令牌的安全性和有效性。


localStorage 如何实现跨域


localStorage 是一直域限制的存储机制,通常只能在同一域名下的页面中访问。这意味着默认情况下,localStorage 的数据在不同域名或跨域的情况下是无法直接访问的。然而,有几种方法可以实现跨域访问 localStorage 中的数据:



  1. 域名映射(Domain Mapping): 将不同域名都指向同一个服务器 IP 地址。这样不同域名下的页面就可以共享同一个 localStorage 中的数据;

  2. postMessage API: postMessage 是一种浏览器提供的 API,用于在不同窗口或跨域的 iframe 之间进行安全的消息传递。你可以在不同域名的页面中使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中;


使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中,实例代码如下:


// 发送消息到目标窗口
window.postMessage(
{ key: "token", value: "1233211234567" },
"https://liangzai.com"
);

在接收消息的窗口中:


// 监听消息事件
window.addEventListener("message", function (event) {
if (event.origin === "https://sourcedomain.com") {
// 存储数据到 LocalStorage
localStorage.setItem(event.data.key, event.data.value);
}
});

这些方法提供了一些途径来实现跨域访问 localStorage 中的数据。具体选择哪种方法取决于你的需求和应用场景,以及你对目标域名的控制程度。需要注意的是,安全性是非常重要。


cookie 和 localStorage 的区别


CookieLocalStorage 是两种用于在浏览器中存储数据的机制,它们在以下方面有一些区别:



  1. 存储容量: Cookie 的存储容量通常较小,每个 Cookie 的大小限制在几 KB 左右。而 LocalStorage 的存储容量通常较大,一般限制在几 MB 左右。因此,如果需要存储大量数据,LocalStorage 通常更适合;

  2. 数据发送: Cookie 在每次 HTTP 请求中都会自动发送到服务器,这使得 Cookie 适合用于在客户端和服务器之间传递数据。而 localStorage 的数据不会自动发送到服务器,它仅在浏览器端存储数据,因此 LocalStorage 适合用于在同一域名下的不同页面之间共享数据;

  3. 生命周期:Cookie 可以设置一个过期时间,使得数据在指定时间后自动过期。而 LocalStorage 的数据将永久存储在浏览器中,除非通过 JavaScript 代码手动删除;

  4. 安全性:Cookie 的安全性较低,因为 Cookie 在每次 HTTP 请求中都会自动发送到服务器,存在被窃取或篡改的风险。而 LocalStorage 的数据仅在浏览器端存储,不会自动发送到服务器,相对而言更安全一些;


总结


Cookie 适合用于在客户端和服务器之间传递数据、跨域访问和设置过期时间,而 LocalStorage 适合用于在同一域名下的不同页面之间共享数据、存储大量数据和永久存储数据。选择使用哪种机制应根据具体的需

作者:Moment
来源:juejin.cn/post/7248623545219825723
求和使用场景来决定。

收起阅读 »

在高德地图中实现降雨图层

web
前言 有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。 需求说明 在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合; 可以结合当地天气预...
继续阅读 »

前言


有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。


需求说明


在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合;


可以结合当地天气预报情况,自动调节风速、风向、降雨量等参数。


需求分析


方案一:全局降雨


在用户视口面前加一层二维的降雨平面层。


优点: 只管二维图层就行了,不需要与地图同步坐标,实现起来比较简单,界面是全局的一劳永逸。


缺点:只适合从某些角度观看,没法再做更多定制了。


Honeycam_2023-06-16_11-10-37.gif


方案二:局部地区降雨


指定降雨范围,即一个三维空间,坐标与地图底图同步,仅在空间内实现降雨。


优点:降落的雨滴有远近关系,比较符合现实场景;可适用各种地图缩放程度。


缺点:需要考虑的参数比较多,比如降雨范围一项就必须考虑这个三维空间是什么形状,可能是立方体、圆柱体或者多边形挤压体;需要外部图层的配合,比如说下雨了,那么天空盒子的云层、建筑图层的明度是否跟着调整。


Honeycam_2023-06-16_11-20-08.gif


实现思路


根据上面利弊权衡,我选择了方案二进行开发,并尽量减少输入参数,降雨影响范围初步定为以地图中心为坐标中心的立方体,忽略风力影响,雨滴采用自由落体方式运动。


降雨采用自定义着色器的方式实现,充分利用GPU并行计算能力,刚好在网上搜到一位大佬写的three演示代码,改一下坐标轴(threejs空间坐标轴y轴朝上,高德GLCustomLayer空间坐标z轴朝上)就可以直接实现最基础的效果。这里为了演示方便增加坐标轴和影响范围的辅助线。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry,该几何体的构成就是在影响范围内随机位置的1000个平面,这些平面与地图底面垂直;


Honeycam_2023-06-24_15-40-31.gif


2.创建雨滴材质,雨滴不受光照影响,这里使用最基础的MeshBasicMaterial材质即可,半透明化且加上一张图片作为纹理;


Honeycam_2023-06-24_15-50-32.gif


3.为实现雨滴随着时间轴降落的动画效果,需要调整几何体的形状尺寸,并对MeshBasicMaterial材质进行改造,使其可以根据当前时间time改变顶点位置;


Honeycam_2023-06-24_16-01-39.gif



  1. 调整顶点和材质,使其可以根据风力风向改变面的倾斜角度和移动轨迹;


Honeycam_2023-06-24_16-16-52.gif



  1. 将图层叠加到地图3D场景中


Honeycam_2023-06-24_16-28-46.gif


基础代码实现


为降低学习难度,本模块只讲解最基础版本的降雨效果,雨滴做自由落体,忽略风力影响。这里的示例以高德地图上的空间坐标轴为例,即z轴朝上,three.js默认空间坐标系是y轴朝上。我把three.js示例代码演示放到文末链接中。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry


createGeometry () {
// 影响范围:只需要设定好立方体的size [width/2, depth/2, height/2]
//
const { count, scale, ratio } = this._conf.particleStyle
// 立方体的size [width/2, depth/2, height/2]
const { size } = this._conf.bound
const box = new THREE.Box3(
new THREE.Vector3(-size[0], -size[1], 0),
new THREE.Vector3(size[0], size[1], size[2])
)

const geometry = new THREE.BufferGeometry()
// 设置几何体的顶点、法线、UV
const vertices = []
const normals = []
const uvs = []
const indices = []

// 在影响范围内随机位置创建粒子
for (let i = 0; i < count; i++) {
const pos = new THREE.Vector3()
pos.x = Math.random() * (box.max.x - box.min.x) + box.min.x
pos.y = Math.random() * (box.max.y - box.min.y) + box.min.y
pos.z = Math.random() * (box.max.z - box.min.z) + box.min.z

const height = (box.max.z - box.min.z) * scale / 15
const width = height * ratio

// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

vertices.push(...rect)

normals.push(
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z
)

uvs.push(1, 1, 0, 1, 0, 0, 1, 0)

indices.push(
i * 4 + 0,
i * 4 + 1,
i * 4 + 2,
i * 4 + 0,
i * 4 + 2,
i * 4 + 3
)
}

// 所有顶点的位置
geometry.setAttribute(
'position',
new THREE.BufferAttribute(new Float32Array(vertices), 3)
)
// 法线信息
geometry.setAttribute(
'normal',
new THREE.BufferAttribute(new Float32Array(normals), 3)
)
// 设置UV属性与顶点顺序一致
geometry.setAttribute(
'uv',
new THREE.BufferAttribute(new Float32Array(uvs), 2)
)
// 设置基本单元的顶点顺序
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1))

return geometry
}

2.创建材质


createMaterial () {
// 粒子透明度、贴图地址
const { opacity, textureUrl } = this._conf.particleStyle
// 实例化基础材质
const material = new THREE.MeshBasicMaterial({
transparent: true,
opacity,
alphaMap: new THREE.TextureLoader().load(textureUrl),
map: new THREE.TextureLoader().load(textureUrl),
depthWrite: false,
side: THREE.DoubleSide
})

// 降落起点高度
const top = this._conf.bound.size[2]

material.onBeforeCompile = function (shader, renderer) {
const getFoot = `
uniform float top; // 天花板高度
uniform float bottom; // 地面高度
uniform float time; // 时间轴进度[0,1]
#include <common>
float angle(float x, float y){
return atan(y, x);
}
// 让所有面始终朝向相机
vec2 getFoot(vec2 camera,vec2 normal,vec2 pos){
vec2 position;
// 计算法向量到点的距离
float distanceLen = distance(pos, normal);
// 计算相机位置与法向量之间的夹角
float a = angle(camera.x - normal.x, camera.y - normal.y);
// 根据点的位置和法向量的位置调整90度
pos.x > normal.x ? a -= 0.785 : a += 0.785;
// 计算投影值
position.x = cos(a) * distanceLen;
position.y = sin(a) * distanceLen;

return position + normal;
}
`

const begin_vertex = `
vec2 foot = getFoot(vec2(cameraPosition.x, cameraPosition.y), vec2(normal.x, normal.y), vec2(position.x, position.y));
float height = top - bottom;
// 计算目标当前高度
float z = normal.z - bottom - height * time;
// 落地后重新开始,保持运动循环
z = z + (z < 0.0 ? height : 0.0);
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 调整坐标参考值
z += bottom;
z += position.z - normal.z;
// 生成变换矩阵
vec3 transformed = vec3( foot.x, foot.y, z );
`

shader.vertexShader = shader.vertexShader.replace(
'#include <common>',
getFoot
)
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
begin_vertex
)
// 设置着色器参数的初始值
shader.uniforms.cameraPosition = { value: new THREE.Vector3(0, 0, 0) }
shader.uniforms.top = { value: top }
shader.uniforms.bottom = { value: 0 }
shader.uniforms.time = { value: 0 }
material.uniforms = shader.uniforms
}

this._material = material

return material
}

3.创建模型



createScope () {
const material = this.createMaterial()
const geometry = this.createGeometry()

const mesh = new THREE.Mesh(geometry, material)

this.scene.add(mesh)

// 便于调试,显示轮廓
// const box1 = new THREE.BoxHelper(mesh, 0xffff00)
// this.scene.add(box1)
}

4.更新参数


// 该对象用于跟踪时间
_clock = new THREE.Clock()

update () {
const { _conf, _time, _clock, _material, camera } = this

// 调整时间轴进度,_time都值在[0,1]内不断递增循环
// particleStyle.speed为降落速度倍率,默认值1
// _clock.getElapsedTime() 为获取自时钟启动后的秒数
this._time = _clock.getElapsedTime() * _conf.particleStyle.speed / 2 % 1

if (_material.uniforms) {
// 更新镜头位置
_material.uniforms.cameraPosition.value = camera.position
// 更新进度
_material.uniforms.time.value = _time
}
}

animate (time) {
if (this.update) {
this.update(time)
}
if (this.map) {
// 叠加地图时才需要
this.map.render()
}
requestAnimationFrame(() => {
this.animate()
})
}

优化调整


修改场景效果


通过对图层粒子、风力等参数进行封装,只需简单地调整配置就可以实现额外的天气效果,比如让场景下雪也是可以的,广州下雪这种场景,估计有生之年只能在虚拟世界里看到了。


Honeycam_2023-06-24_17-00-11.gif


以下是配置数据结构,可供参考


const layer = new ParticleLayer({
map: getMap(),
center: mapConf.center,
zooms: [4, 30],
bound: {
type: 'cube',
size: [500, 500, 500]
},
particleStyle: {
textureUrl: './static/texture/snowflake.png', //粒子贴图
ratio: 0.9, //粒子宽高比,雨滴是长条形,雪花接近方形
speed: 0.04, // 直线降落速度倍率,默认值1
scale: 0.2, // 粒子尺寸倍率,默认1
opacity: 0.5, // 粒子透明度,默认0.5
count: 1000 // 粒子数量,默认值10000
}
})

添加风力影响


要实现该效果需要添加2个参数:风向和风力,这两个参数决定了粒子在降落过程中水平面上移动的方向和速度。



  1. 首先调整一下代码实际那一节步骤2运动的相关代码


const begin_vertex = `
...
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 增加了下面这几行
float x = foot.x+ 200.0 * ratio; // 粒子最终在x轴的位移距离是200
float y = foot.y + 200.0 * ratio; // 粒子最终在y轴的位移距离是200
...
// 生成变换矩阵
vec3 transformed = vec3( foot.x, y, z );


  1. 如果粒子是长条形的雨滴,那么它在有风力影响的运动时,粒子就不是垂直地面的平面了,而是与地面有一定倾斜角度的平面,如图所示。


Untitled.png


我们调整调整一下代码实际那一节步骤1的代码,实现方式就是让每个粒子平面在创建之后,所有顶点绕着平面的法线中心轴旋转a角度。


本示例旋转轴(x, y, 1)与z轴(0,0,1)平行,这里有个技巧,我们在做平面绕轴旋转的时候先把平面从初始位置orgPos移到坐标原点,绕着z轴旋转后再移回orgPos,会让计算过程简单很多。


// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

// 定义旋转轴
const axis = new THREE.Vector3(0, 0, 1).normalize();
//定义旋转角度
const angle = Math.PI / 6;
// 创建旋转矩阵
const rotationMatrix = new THREE.Matrix4().makeRotationAxis(axis, angle);

for(let index =0; index< rect.length; index +=3 ){
const vec = new THREE.Vector3(rect[index], rect[index + 1], rect[index + 2]);
//移动到中心点
vec.sub(new THREE.Vector3(pos.x, pos.y,pos.z))
//绕轴旋转
vec.applyMatrix4(rotationMatrix);
//移动到原位
vec.add(new THREE.Vector3(pos.x, pos.y, pos.z))
rect[index] = vec.x;
rect[index + 1] = vec.y;
rect[index + 2] = vec.z;
}

待改进的地方


本示例中有个需要完善的地方,就是加入了风力影响之后,如果绕垂直轴旋转一定的角度,会看到如下图的异常,雨点的倾斜角度和运动倾斜角度是水平相反的。


Honeycam_2023-06-24_21-06-51.gif


问题的原因是材质着色器中的“让所有面始终朝向相机”方法会一直维持粒子的倾斜状态不变,解决这个问题应该是调整这个方法就可以了。然而作为学渣的我还没摸索出来,果然可视化工程的尽头全是数学Orz。


相关链接


1.THREE.JS下雨进阶版,面只旋转Y轴朝向相机


http://www.wjceo.com/blog/threej…


2.演示代码在线DEMO


jsfiddle.net/gyrate

sky/5…

收起阅读 »

我有个气人的同事......

web
前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。 曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 ...
继续阅读 »

前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。




曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 console-custom。沉寂在个人仓库很久,前段时间看到别人也有类似仓库,也就想着把自己的也发出来。




其实,我个人不是很推荐在代码里 写 console.log 之类的来调试代码,更推荐去浏览器控制台去打断点来调试,更好的理清数据的流转,事件的先后顺序等。



背景


官方背景:



  • 方便大家调试代码的时候,在浏览器控制台输出自定义个性化日志。

  • 防止控制台输出密密麻麻的 console.log,一眼看不到想看的。

  • 防止某个气人的小伙伴老是使用 console.error,强迫症不允许。

  • ......


真实背景:


其实,是我之前有个小伙伴同事——“小白菜”(也是为啥函数名叫 blog 的原因之一,下边会看到),他调试代码,打印输出总是喜欢 console.error(),用完了还不自己清理,大家协同开发的时候,git pull 他的代码后,总是让人就很难受!看着一堆报错,一时半会看不清是程序自己的报错,还是调试的输出!强迫症就犯了!想骂街......


不......不......要冷静!



  • 编码千万行

  • 调试要输出

  • log不规范

  • 同事两行泪


效果


浏览器页面 page


tu1.jpg


浏览器控制台 console


image.png



有个重点、痛点是这个, console.log(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333); 打印的数据多的时候不换行,需要找半天,我专门给处理成 分行, 一行一行展示了,这样好看清数据。



这个工具库有以下几个特点:



  1. 支持输入多个数据,并分行打印出来,并能看出是个整体

  2. 支持自己修改自己的默认样式部分,配置自己的默认部分

  3. 支持额外的自定义部分,拓展用户更多的玩法

  4. ......


其实 console 由于可以自定义,其实会有很多玩法,我个人在此主要的思路是



  1. 一定要简单,因为 console.log 本身就很简单,尽量不要造成使用者心智负担。

  2. 就简单的默认定制一个彩色个性化的部分,能区分出来,解决那个气人同事所谓的痛点就好。

  3. 代码要少,不要侵入,不要影响用户的业务代码


源码


此处源码有借鉴 github 开源代码:github.com/Redstone-1/…


大家如有更多、更丰富的需求场景可去参考使用。


// src/utils/console-custom.js
const GourdBabyColorMap = new Map([
["1", "#FF0000"],
["2", "#FFA500"],
["3", "#FFFF00"],
["4", "#008000"],
["5", "#00FFFF"],
["6", "#0000FF"],
["7", "#800080"],
]);

const createBLog = (config) => {
const logType = config.logType || "default";
const username = config.username || "";
const logName = config.logName || "";
const usernameColor = config.usernameColor || "#41b883";
const logNameColor = config.logNameColor || "#35495e";
const padding = config.padding || 6;
const borderRadius = config.borderRadius || 6;
const fontColor = config.fontColor || "#FFFFFF";
const usernameStyle = config.usernameStyle || "";
const logNameStyle = config.logNameStyle || "";

const logTemplate = (username = "myLog", logName = "") =>
`${username ? '%c' + username : ''} ${logName ? '%c' + logName : ''} `;

const customLog = (...data) => {
console.log(
logTemplate(username, logName),
usernameStyle ? usernameStyle : `background: ${usernameColor}; padding: 6px; border-radius: 6px 0 0 6px; color: #fff`,
logNameStyle ? logNameStyle : `background: ${logNameColor}; padding: 6px; border-radius: 0 6px 6px 0; color: #fff`,
...data
);
};

const defaultLog = (...data) => {
const len = data.length;
if (len > 1) {
data.map((item, index) => {
let firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
let secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
if (index === 0) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: ${borderRadius}px 0 0 0;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: 0 ${borderRadius}px 0 0;
color: ${fontColor}
`
;
} else if (index === len -1) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 0 ${borderRadius}px;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 ${borderRadius}px 0;
color: ${fontColor}
`
;
}
console.log(
logTemplate(username, `数据${index+1}`),
firstStyle,
secondStyle,
item
);
});
} else {
const firstStyle = `
background: ${usernameColor};
padding: ${padding}px;
border-radius: ${borderRadius}px 0 0 ${borderRadius}px;
color: ${fontColor}
`
;

const secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 ${borderRadius}px ${borderRadius}px 0;
color: ${fontColor}
`
;

console.log(
logTemplate(username, logName),
firstStyle,
secondStyle,
...data
);
}
};

const log = (...data) => {
switch(logType) {
case 'custom':
customLog(...data)
break;
default:
defaultLog(...data)
}
};

return {
log,
};
};

export default createBLog

API


唯一API createBLog(对!简单!易用!用起来没有负担!)


import createBLog from '@/utils/console-custom'

const myLog = createBLog(config)

配置 config: Object


一次配置,全局使用。(该部分是借鉴开源代码重构了配置内容)


配置项说明类型默认值
logTypelog 日志类型default、customdefault
usernamelog 的主人,也就是谁打的日志string-
logNamelog 的名字,也就是打的谁的日志string-
usernameColorusername 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#41b883
logNameColorlogName 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#35495e
paddingusername 和 logName 内边距,单位 pxnumber6
borderRadiususername 和 logName 圆角边框,单位 pxnumber6
fontColorusername 和 logName 字体颜色string#FFFFFF
usernameStyleusername 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-
logNameStylelogName 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-

基本用法 default



也是默认用法(default),同时也是最推荐大家用的一种方法。



vue2 版本


// main.js
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

// 不需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$blog = myLog.log;

// 需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$nlog = (logName, ...data) => {
myLog.logName = logName;
myLog.log(...data);
};

// vue2 组件里边使用
// 同时输入多个日志数据,可帮用户按照行的形式分开,好一一对应看清 log
this.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
this.$blog(111231231231231);

this.$nlog("logName", 2212121212122);

vue3 版本


// main.ts
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

app.config.globalProperties.$blog = myLog.log;

// vue3 组件里边使用
import { getCurrentInstance } from 'vue'

export default {
setup () {
const { proxy } = getCurrentInstance()

proxy.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
proxy.$blog(111231231231231);

proxy.$nlog("logName", 2212121212122);
}
}

自定义用法 custom



这部分我没有很多玩法,下边的例子也是借鉴别人的,主要全靠用户自己扩展 css 样式了。做一套自己喜欢的样式。



// main.js

// ....
Vue.prototype.$clog = (logName, ...data) => {
myLog.logType = "custom";
myLog.logName = logName;
myLog.usernameStyle = `text-align: center;
padding: 10px;
background-image: -webkit-linear-gradient(left, blue,
#66ffff 10%, #cc00ff 20%,
#CC00CC 30%, #CCCCFF 40%,
#00FFFF 50%, #CCCCFF 60%,
#CC00CC 70%, #CC00FF 80%,
#66FFFF 90%, blue 100%);`
;
myLog.logNameStyle = `background-color: #d2d500;
padding: 10px;
text-shadow: -1px -1px 0px #e6e600,-2px -2px 0px #e6e600,
-3px -3px 0px #e6e600,1px 1px 0px #bfbf00,2px 2px 0px #bfbf00,3px 3px 0px #bfbf00;`
;
myLog.log(...data);
};

// 提供的其他 css 样式
myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 0px 0px 15px #00FFFF,0px 0px 15px #00FFFF,0px 0px 15px #00FFFF;`
;
myLog.logNameStyle = `background-color: gray;
color: #eee;
padding: 10px;
text-shadow: 5px 5px 0 #666, 7px 7px 0 #eee;`
;

myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 1px 1px 0px #0000FF,2px 2px 0px #0000FF,-1px -1px 0px #E31B4E,-2px -2px 0px #E31B4E;`
;
myLog.logNameStyle = `font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
text-transform: uppercase;/* 全开大写 */
padding: 10px;
color: #f1ebe5;
text-shadow: 0 8px 9px #c4b59d, 0px -2px 1px #fff;
font-weight: bold;
letter-spacing: -4px;
background: linear-gradient(to bottom, #ece4d9 0%,#e9dfd1 100%);`
;
// ....

其中渐变色的玩法


myLog.usernameStyle = `background-image: linear-gradient(to right, #ff0000, #ff00ff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;
myLog.logNameStyle = `background-image: linear-gradient(to right, #66ff00 , #66ffff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;

其中输出 emoji 字符


this.$nlog("😭", 2212121212122);
this.$nlog("🤡", 2212121212122);
this.$nlog("💩", 2212121212122);
this.$nlog("🚀", 2212121212122);
this.$nlog("🎉", 2212121212122);
this.$nlog("🐷", 2212121212122);

小伙伴们你肯定还有什么好玩的玩法!尽情发挥吧!


最后


还是想极力劝阻那些用 console.error() 调试代码的人,同时也能尽量少用 console 来调试,可以选择控制台断点、编译器断点等。还是不是很推荐使用 console 来调试,不过本文也可以让大家知道,其实 console 还有这种玩法。如果写 JS 库的时候也可以使用,让自己

作者:Bigger
来源:juejin.cn/post/7248448028297855035
的库极具自己的特色。

收起阅读 »

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf

web
常见 Node.js 版本管理器比较:nvm、Volta 和 asdf 随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本...
继续阅读 »

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf


随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本管理器:nvm、voltaasdf 来帮助你为你的开发环境选择合适的版本管理器。


nvm


nvm(Node Version Manager)是最古老和最受欢迎的 Node.js 版本管理器之一,至今仍在积极维护。nvm 允许开发人员在一台机器上安装和管理多个版本的 Node.js。它还提供了一个方便的命令行界面,用于在可用版本之间切换。


nvm 的工作原理是将 Node.js 的每个版本安装到下的独立目录中。使用 nvm use 在版本之间切换时,它会更新环境变量以指向相应的目录。所以可以并行安装多个版本的 Node.js,并且每个版本都有自己的一组全局安装的软件包 ~/.nvm/versions/node/$PATH


nvm 的一个缺点是它只支持 Node.js。如果需要管理其他编程语言或工具,则需要使用单独的版本管理器。另外,nvm 需要手动安装和配置,这对于初学者来说可能有点难受。


要安装 nvm,可以使用以下命令:


curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

# or

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

这将从官方 NVM GitHub 仓库下载并运行 NVM 安装脚本,该仓库将在你的系统上安装 NVM。安装完成后,你可以通过运行以下命令来验证是否已安装 NVM:


nvm --version

1.1 如何使用特定版本的 Node.js


要将特定版本的 Node.js 与 nvm 配合使用,你需要执行以下步骤:



  1. 列出可用的 Node.js 版本:要查看可以使用 nvm 安装的所有可用 Node.js 版本的列表,请运行以下命令:


nvm ls-remote


  1. 安装所需版本:要安装特定版本的 Node.js,例如版本 18,请使用以下命令:


nvm install 18


  1. 使用已安装的版本:安装所需的 Node.js 版本后,你可以通过运行以下命令来使用它:


nvm use 16

设置默认版本:如果要使用特定版本的 Node.js 默认情况下,可以使用以下命令将其设置为默认版本:


nvm alias default 18

Volta


volta 是一个较新的 Node.js 版本管理器,旨在简化 Node.js 和其他工具的安装和管理,在 2019 年出世,仍在积极开发中。Volta 采用了与 nvm 不同的方法:它不是管理 Node.js 的多个版本,而是管理项目及其依赖项。当你创建新项目时,volta 会自动检测所需的 Node.js 版本并为你安装它。


volta 还支持其他工具,如 Yarn 和 Rust,开箱即用(不仅仅是 Node.js!对于使用多种编程语言并需要单个工具来管理它们的开发人员来说,这使其成为一个不错的选择。与 nvm 一样,volta 提供了一个命令行界面,用于在 Node.js 版本之间切换,但它通过使用拦截对 node 可执行文件的调用的全局填充程序来实现。


要安装 volta,你可以使用以下命令:


curl https://get.volta.sh | bash

此命令将下载并执行安装 volta 的脚本。


如何使用特定版本的 Node.js



  1. 安装所需版本:安装 volta 后,你可以使用它创建一个新项目并使用 volta install 命令设置所需的 Node.js 版本,以下命令创建一个新项目并将所需的 Node.js 版本设置为 16.0.0:


volta install node@16.0.0


  1. 在该项目的上下文中运行命令:此命令使用所需版本的 Node.js 在项目的上下文中运行 app.js 文件。


volta run node app.js


  1. 切换版本:你也可以使用 Volta 在不同版本的 Node.js 之间切换。例如,要切换到版本 10.0.0,可以使用以下命令:


volta pin node@10.0.0


  1. 设置默认版本:最后,以下命令将你的环境切换为 Node.js 版本 16.0.0 并设置为 Node.js 的默认版本:


nvm alias default 16.0.0

volta 的一个潜在缺点是它仍然是一个相对较新的工具,因此它可能不像 nvm 那样经过实战测试,而且它的社区支持有限,插件和集成也较少。


ASDF


ASDF 是一个版本管理器,旨在成为“通用语言版本管理器”。在 2015 年出世,支持广泛的编程语言和工具,包括 Node.js。ASDF 设计为可扩展,因此可以轻松地添加新语言和工具的支持。


asdf 支持几种流行的编程语言,包括 Node.js,Ruby,Python,Elixir,Java,Rust,PHP,Perl,Haskell,R,Lua 和 Earlang。这意味着你可以在一个地方管理不同的语言版本!如果要在不同语言的项目之间切换,使 asdf 成为一个不错的选择。


与 volta 一样,ASDF 管理项目及其依赖项,而不是同一工具的多个版本。创建新项目时,asdf 会自动检测所需的 Node.js 版本并为你安装。asdf 提供了一个命令行界面,用于在 Node.js 版本以及其他工具之间切换。


asdf 的一个潜在缺点是它的设置可能比 nvm 或 volta 复杂一些。你需要安装多个插件来添加对不同语言和工具的支持,并且可能需要修改 shell 配置以正确使用 asdf。


下面是如何使用 ASDF 安装和使用特定 Node.js 版本的示例:



  1. 安装 ASDF:可以使用以下命令安装 ASDF:


brew install asdf


  1. 将 Node.js 插件添加到 ASDF:你必须安装插件才能将 Node.js 添加到你的项目中


asdf plugin add nodejs


  1. 安装 Node.js 版本 18:使用以下命令使用特定版本的 Node.js:


asdf install nodejs 18


  1. 使用特定版本:


asdf global nodejs 18

nvm,volta 和 asdf 之间的差异



  1. 目的: NVM,Volta 和 ASDF 有不同的用途。NVM 专注于管理多个版本的 Node.js。而 Volta 将 Node.js 版本管理和包管理结合在一个工具中。ASDF 是一个版本管理器,支持多种编程语言,包括 Node.js。

  2. 安装: NVM、Volta 和 ASDF 的安装过程不同。NVM 可以使用 curl 命令安装,而 Volta 要求你手动下载并安装它。ASDF 可以使用 Homebrew 等包管理器安装,也可以直接从 GitHub 下载。

  3. 配置: NVM、Volta 和 ASDF 的配置过程是不同的。NVM 要求你手动更新 shell 配置文件。Volta 不需要任何手动配置。ASDF 要求你手动设置所需的插件。

  4. 自动版本检测: Volta 是唯一通过读取项目的 package.json 文件自动检测项目所需的 Node.js 版本的版本管理器。

  5. 包管理: Volta 是唯一将 Node.js 版本管理和包管理结合在一个工具中的版本管理器。NVM 和 ASDF 仅管理 Node.js 版本。


相似之处



  1. 多节点.js版本: NVM、Volta 和 ASDF 都允许你在同一台机器上管理多个版本的 Node.js。

  2. 全局和本地 node.js 版本: 所有三个版本管理器都允许你全局或本地安装 Node.js 版本。

  3. 简单命令: NVM、Volta 和 ASDF 有简单的命令来管理 Node.js 版本。

  4. 兼容性: 所有三个版本管理器都与 macOS,Linux 和 Windows 操作系统兼容。

  5. 开源: NVM、Volta 和 ASDF 都是开源项目,这意味着它们可以免费使用,并且可以由社区贡献。

  6. 版本锁定: 所有三个版本管理器都允许你锁定特定项目的 Node.js 版本,确保所有团队成员使用相同的版本。


小结


总之,nvmVoltaasdf 都是很棒的 Node.js 版本管理器,可以帮助你更改,管理和更新 Node.js 的多个版本,还可以与新版本保持同步,包括 LTS 版本。Nvm 是最古老和最受欢迎的版本管理器之一,Volta 有不同的方法,它不是管理多个版本的 Node.js,而是管理项目及其依赖项,最后 asdf 管理不同的语言版本,如果你在使用不同语言的项目之间切换,这使得 asdf

作者:夏安君
来源:juejin.cn/post/7247543825535270968
是一个不错的选择。

收起阅读 »

强制缓存这么暴力,为什么不使用协商缓存😡😡😡

web
前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存 和 协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。 强缓存和协商缓存 浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档...
继续阅读 »

前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。


强缓存和协商缓存


浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档,其中浏览器缓存就分为 强缓存协商缓存:



  1. 强缓存: 当浏览器在请求资源时,根据响应头中的缓存策略信息,判断是否使用本地缓存副本而无需发送请求到服务器。如果资源被标记为强缓存,浏览器会直接从本地缓存中加载资源,而不发送请求到服务器,从而提高页面加载速度并减轻服务器负载;

  2. 协商缓存: 协商缓存是一种缓存策略,用于在资源未过期但可能已经发生变化时,通过与服务器进行协商确定是否使用本地缓存。协商缓存通过在请求中发送特定的条件信息,让服务器判断资源是否有更新,并返回相应的状态码和响应头信息,以指示浏览器是否可以使用本地缓存;


所以根据以上所聊到的特点,浏览器缓存有以下几个方面的优点:



  1. 减少冗余的数据传输;

  2. 减少服务器负担;

  3. 加快客户端加载网页的速度;


20230624082050


浏览器会首先获取该资源缓存的 header 信息,然后根据 Cache-Controlexpires 来判断是否过期。


如图,在浏览器第一次发送请求后,需要再次发送请求时,它会经过以下几个步骤:




  1. 首先,浏览器发送请求到服务器,请求的资源可能是一个网页、css 文件、JavaScript 文件或者其他类型的文件;




  2. 当服务器接收到请求后,首先检查请求中的缓存策略,例如请求头中的 Cache-Controlexpires 字段;




  3. 如果资源被标记为强缓存,服务器会进行以下判断:



    • 如果缓存有效,即资源的过期时间未到达或过期时间在当前时间之后,服务器返回状态码为 200 ok,并在响应头中设置适当的缓存策略,例如设置 Cache-ControlExpires 字段,告诉浏览器可以使用本地缓存;

    • 如果缓存无效,即资源的过期时间已过或过期时间在当前时间之前,服务器返回新的资源,状态码为 200 ok,并在响应头中设置适当的缓存策略;




  4. 如果资源未被标记为强缓存或缓存验证失败,服务器进行协商缓存的判断:



    • 如果请求头中包含 If-Modified-Since 字段,表示浏览器之前缓存了该组员并记录了最后修改时间,服务器会根据资源的最后修改时间进行判断;

      • 如果资源的最后修改时间与 If-Modified-Since 字段的值相同或更早,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应头;

      • 如果资源的最后修改时间晚于 If-Modified-Since 字段的值,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 ok,并在响应头中设置新的最后修改时间;



    • 如果请求头中包含 If--Match 字段,表示浏览器之前缓存了该资源并记录资源的 ETag 值,服务器会根据资源的 ETag 进行判断:

      • 如果资源的 ETagIf--Match 字段的值相同,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应体;

      • 如果资源的 ETagIf--Match 字段的值不同,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 OK,并在响应头中设置新的 ETag;






  5. 浏览器接收到服务器的响应之后,根据状态码和响应头信息进行相应的处理:



    • 如果状态码为 200 OK,表示服务器返回了新的资源,浏览器使用新的资源并更新本地缓存;

    • 如果状态码为 304 Not Modified,表示资源未发生变化,浏览器使用本地缓存的副本;

    • 浏览器根据响应头中的缓存策略进行进一步处理:

      • 如果响应头中包含 Cache-Control 字段,浏览器根据其指令执行缓存策略。例如,如果响应头中的 Cache-Control 包含 no-cache,浏览器将不使用本地缓存,而是向服务器发送请求获得最新的资源;

      • 如果响应头中包含 Expires 字段,浏览器将与当前时间比较,判断资源的过期时间。如果过期时间已过,浏览器将不使用本地缓存,而是向服务器发送请求获取最新的资源;

      • 如果响应头中包含其他相关的缓存控制字段(如 ETag),浏览器可以根据这些字段进行更精确的缓存控制和验证;






其中,在上面的流程中,又有几个令人难懂的字段,主要有以下几个:



  1. ETag: 它是通过对比浏览器和服务器资源的特征值来决定是否要发送文件内容,如果一样就只发送 304 Not Modified;

  2. Expires: 设置过期时间,是绝对时间;

  3. Last-Modified: 以时刻作为标识,无法识别一秒内进行多次修改的情况,只要资源修改,无论内容是否发生实质性变化,都会将该资源返回客户端;

  4. If--Match: 当客户端发送 GET 请求时,如果之前已经键相用资源的请求时,并且服务器返回了 ETag,那么客户端可以将 ETag 的值添加到 If--Match 头中,当再次请求该资源时,客户端会将 If--Match 头发送给服务器,服务器收到请求之后,会检查 If--Match 投中的值是否与当前资源的 ETag 值匹配:

    • 如果匹配,则表示客户端所请求的资源没有发生变化,服务器会返回状态码 304 Not Modified,并且不返回实际的资源内容;

    • 如果 If--Match 头中的值与服务器上资源的 ETag 值不匹配,说明资源发生了变化,服务器会正常返回资源,并返回状态码 200 OK;




图解强缓存和协商缓存


在上面的内容中讲了这么多的理论, 你是否还是不太理解什么是 强缓存协商缓存 啊,那么接下来我们就用几张图片来弄清楚这两者的区别。


强缓存


强缓存就是文件直接从本地缓存中获取,不需要发送请求。


首次请求


20230624103449


当浏览器发送初次请求时,浏览器会向服务器发起请求,服务器接收到浏览器的请求后,返回资源并返回一个 Cache-Control 字段给客户端,在该字段中设置一些缓存相关的信息,例如最大过期时间。


再次请求


20230624103906


在前面的基础上,浏览器再次发送请求,浏览器一节接收到 Cache-Control 的值,那么这个时候浏览器它会首先检查它的 Cache-Control 是否过期,如果没有过期则直接从本地缓存中拉取资源,返回割到客户端,则无需再经过服务器。


缓存失效


20230624104233


强缓存有过期时间,那么就意味着总有一天缓存会失效,如果客户端的 Cache-Control 失效了,那么它就会像首次请求中一样,重新向服务器发起请求,之后服务器会再次返回资源和 Cache-Control 的值。


协商缓存


协商缓存也叫做对比缓存,服务端判断客户端的资源是否和服务端的一样,如果一样则返回 304,反之返回 200 和最新的资源。


初次请求


20230624112243


如果客户端是第一次向服务器发出请求,则服务器返回资源和对应的资源标识给浏览器,该资源标识就是对当前所返回资源的唯一标识,可以是 ETag 或者是 Last-Modified


之后如果浏览器再次发送请求是,浏览器就会带上这个资源表,此时服务端就会通过这个资源标识,可以判断出浏览器的资源跟服务器此时的资源是否一致,如果一致则返回 304 Not Modified,如果不一致,则返回 200,并返回资源以及新的资源标识。


不同刷新操作方式,对强制缓存和协商缓存的影响


不同的刷新操作方式对于强制缓存和写上缓存的影响如下:




  1. 普通刷新(F5刷新按钮):



    • 强制缓存的影响: 浏览器忽略强制缓存,直接向服务器发送请求,获取最新的资源,也就是强制缓存失效;

    • 协商缓存的影响: 浏览器放带有缓存验证的字段的请求,浏览器会根据验证结果返回新的资源或者 304 Not Modified;




  2. 强制刷新(Ctrl+F5Shift+刷新按钮):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器发送不带缓存验证字段的请求,服务器返回新的资源,不进行验证,也就是协商缓存失效;




  3. 禁用缓存刷新(DevTools 中的 Disable cacheNetwork 勾选 Disable cache):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器会发送带有缓存验证字段的请求,服务器会根据验证结果返回新的资源或 304 Not Modified;




这玩意也就图一乐,一般出现了问题我都是直接重启......


总结


总的来说,强制缓存是通过在请求中添加缓存策略,判断缓存是否有效,避免发送请求到服务器。而协商缓存是通过条件请求与服务器进行通信,验证缓存是否仍然有效,并在服务器返回适当的响应状态码和缓存策略。


强制缓存可以减少对服务器的请求,加快资源加载速度,但可能无法获取到最新的资源。协商缓存能够验证资源的有效性,并在需要时获取最新的资源,但会增加对服务器的请求。选择使用哪种缓存策略取决于具体的应用场景和资源的特性。


参考资料


你知道 304 吗?图解强缓存和协商缓存


作者:Moment
来源:juejin.cn/post/7248235392284721209
收起阅读 »

你真的会用<a>标签下载文件吗?

web
最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。 <a> 标签 download 这应该是最常见,最受广大人民群众喜闻...
继续阅读 »

最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。


<a> 标签 download


这应该是最常见,最受广大人民群众喜闻乐见的一种下载方式了,搭配上 download 属性, 就能让浏览器将链接的 URL 视为下载资源,而不是导航到该资源。


如果 download 再指定个 filename ,那么就可以在下载文件时,将其作为预填充的文件名。不过名字中的 /\ 会被转化为下划线 _,而且文件系统可能会阻止文件名中的一些字符,因此浏览器会在必要时适当调整文件名。


封装下载方法


贴份儿我常用的下载方法:


const downloadByUrl = (url: string, filename: string) => {
if (!url) throw new Error('当前没有下载链接');

const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
// 使用target="_blank"时,添加rel="noopener noreferrer" 堵住钓鱼安全漏洞 防止新页面window指向之前的页面
a.rel = "noopener noreferrer";
document.body.append(a);
a.click();

setTimeout(() => {
a.remove();
}, 1000);
};

Firefox 不能一次点击多次下载


这里有个兼容性问题:在火狐浏览器中,当一个按钮同时下载多个文件(调用多次)时,只能下载第一个文件。所以,我们可以利用 <a> 标签的 target 属性,将其设置成 _blank 让火狐在一个新标签页中继续下载。


// 检查浏览器型号和版本
const useBrowser = () => {
const ua = navigator.userAgent.toLowerCase();
const re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/;
const m = ua.match(re);
const Sys = {
browser: m[1].replace(/version/, "'safari"),
version: m[2]
};

return Sys;
};

添加一个浏览器判断:


const downloadByUrl = (url: string, filename: string) => {
// 略......

// 火狐兼容
if (useBrowser().browser === "firefox") {
a.target = "_blank";
}

document.body.append(a);
}

download 使用注意点


<a> 标签虽好,但还有一些值得注意的点:


1. 同源 URL 的限制



download 只在同源 URL 或 blob:data: 协议起作用



也就是说跨域是下载不了的......


首先,非同源 URL 会进行导航操作。其次,如果非要下载,那么正如上面的文档所说,可以先将其转换为 blob:data: 再进行下载,至于如何转换会在 Blob 章节中详细介绍。


2. 无法鉴权


使用 <a> 标签下载是带不了 Header 的,因此也不能携带登录态,所以无法进行鉴权。这里我们给出一个解决方案:



  1. 先发送请求获取 blob 文件流,这样就能在请求时进行鉴权;

  2. 鉴权通过后再执行下载操作。


这样是不是就能很好的同时解决问题1和问题2带来的两个痛点了呢😃


顺便提一下,location.hrefwindow.open 也存在同样的问题。


3. download 与 Content-Disposition 的优先级


这里需要关注一个响应标头 Content-Disposition,它会影响 <a>的 download 从而可能产生不同的下载行为,先看一个真实下载链接的 Response Headers


Snipaste_2023-06-20_18-19-21.png


如图所示,Content-Disposition 的 value 值为 attachment;filename=aaaa.bb。请记住,此时Content-Disposition 的 filename 优先级会大于 <a> download 的优先级。也就是说,当两者都指定了 filename 时,会优先使用 Content-Disposition 中的文件名。


接下来我们看看这个响应标头到底是什么。


Content-Disposition



在常规的 HTTP 应答中,Content-Disposition 响应标头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。



Content-Type 不同,后者用来指示资源的 MIME 类型,比如资源是图片(image/png)还是一段 JSON(application/json),而 Content-Disposition 则是用来指明该资源是直接展示在页面上的,还是应该当成附件下载保存到本地的。


当它作为 HTTP 消息主题的标头时,有以下三种写法:


Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"

inline


默认值,即指明资源是直接展示在页面上的。
但是在同源 URL 情况下,<a> 元素的 download 属性优先级比 inline 大,浏览器优先使用 download 属性来处理下载(Firefox 早期版本除外)。


attachment


即指明资源应该被下载到本地,大多数浏览器会呈现一个 “保存为” 的对话框,如果此时有 filename,那么它将其优于 download 属性成为下载的预填充文件名。


<a>标签 VS Content-Disposition


介绍完 Content-Disposition,我们做一个横向比对的归纳一下:




  • download VS inline/attachment


    优先级:attachment > download > inline




  • download 的值 VS filename


    优先级:filename > download 的值




Blob 转换


前文介绍到,在非同源请情况下可以将资源当成二进制的 blob 先拿到手,再进行 <a> 的下载处理。接下来,我们介绍两种 blob 的操作:


方法1. 用作 URL(blob:)


URL.createObjectURL 可以给 FileBlob 生成一个URL,形式为 blob:<origin>/<uuid>,此时浏览器内部就会为每个这样的 URL 存储一个 URL → Blob 的映射。因此,此类 URL 很短,但可以访问 Blob。


那这就好办多了,写成代码就三行:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 生成访问 blob 的 URL
const url = URL.createObjectURL(blob);

// 调用刚刚封装的 a 标签下载方法
downloadByUrl(url, "表格文件.xlsx");

// 删除映射,释放内存
URL.revokeObjectURL(url);
};

不过它有个副作用。虽然这里有 Blob 的映射,但 Blob 本身只保存在内存中的。浏览器无法释放它。


在文档退出时(unload),该映射会被自动清除,因此 Blob 也相应被释放了。但是,如果应用程序寿命很长,那这个释放就不会很快发生。


因此,如果我们创建一个 URL,那么即使我们不再需要该 Blob 了,它也会被挂在内存中。


不过,URL.revokeObjectURL 可以从内部映射中移除引用,允许 Blob 被删除并释放内存。所以,在即时下载完资源后,不要忘记立即调用 URL.revokeObjectURL。


方法2. 转换为 base64(data:)


作为 URL.createObjectURL 的一个替代方法,我们也可以将 Blob 转换为 base64-编码的字符串。这种编码将二进制数据表示为一个由 0 到 64 的 ASCII 码组成的字符串,非常安全且“可读”。


更重要的是 —— 我们可以在 “data-url” 中使用此编码。“data-url” 的形式为 data:[<mediatype>][;base64],<data>。我们可以在任何地方使用这种 url,和使用“常规” url 一样。


FileReader 是一个对象,其唯一目的就是从 Blob 对象中读取数据,我们可以使用它的 readAsDataURL 方法将 Blob 读取为 base64。请看以下示例:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 声明一个 fileReader
const fileReader = new FileReader();

// 将 blob 读取成 base64
fileReader.readAsDataURL(blob);

// 读取成功后 下载资源
fileReader.onload = function () {
downloadByUrl(fileReader.result);
};
};

在上述例子中,我们先实例化了一个 fileReader,用它来读取 blob。


一旦读取完成,就可以从 fileReader 的 result 属性中拿到一个data: URL 格式的 Base64 字符串。


最后,我们给 fileReader 注册了一个 onload 事件,在读取操作完成后开始下载。


两种方法总结与对比


URL.createObjectURL(blob) 可以直接访问,无需“编码/解码”,但需要记得撤销(revoke);


Data URL 无需撤销(revoke)任何操作,但对大的 Blob 进行编码时,性能和内存会有损耗。


总而言之,这两种从 Blob 创建 URL 的方法都可以用。但通常 URL.createObjectURL(blob) 更简单快捷。


responseType


最后,我们回头说一下请求的注意点:如果你的项目使用的是 XHR (比如 axios)而不是 fetch, 那么请记得在请求时添加上 responseType 为 'blob'。


export const fetchFile = async (params) => {
return axios.get(api, {
params,
responseType: "blob"
});
};

responseType 不是 axios 中的属性,而是 XMLHttpRequest 中的属性,它用于指定响应中包含的数据类型,当为 "blob" 时,表明 Response 是一个包含二进制数据的 Blob 对象。


除了 blob 之外,responseType 还有 arraybufferjsontext等其他枚举字符串值。


总结


一言以蔽之,同源就直接使用 <a> download 下载,跨域就先获取 blob,用 createObjectURLreadAsDataURL 读取链接,再用 <a> download 下载。


参考资料


收起阅读 »

那年毕业前,我花了一整个上午的时间走遍整个校园

web
又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情... 其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片。 那些照片 🔼 这张照片...
继续阅读 »

又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情...


其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片


那些照片


木头 2023-06-06 19.29.22.jpeg


🔼 这张照片左边黑白的是一天前拍的,右边是今天拍的,当时毕业典礼刚结束,我们要从体育馆走回计算机学院,人很多,大家边走边笑着,感觉就像... 对,就像是刚入学报到的那一天,那天也像这天一样热闹。今天的路上没有人,天气很好,只有静悄悄的阳光撒在地面上。


木头 2023-06-06 19.35.20.jpeg


🔼 这张照片是宿舍楼旁边的一条小路,路中间是校医院。七天前吃饭的时候路过这里,随手拍了一张照片,今天有风,落了一地小花,还挺好看。


木头 2023-06-06 19.40.15.jpeg


🔼 这是学校操场,28天前正好是在做毕业设计的阶段,在宿舍一坐就是一天,没思路的时候,我就喜欢一个人来看台上坐一会儿,戴着耳机灯歌,看下面跑步的人,等操场上的灯关了,我就回去。当时是一个傍晚,夕阳挺好看的。今天的天气很好,有很多人在跑步。


木头 2023-06-06 19.44.53.jpeg


🔼 老照片拍摄于596天前,快两年了吧... 那时候应该大三,中午刚下课,大家走在南广场上,一块去食堂,那天的云很好看。当时在拍新照片的时候,心里还是有点伤感,明天就要离校了,很多人很可能就再也见不到了...(现在来看,的确是这样)


木头 2023-06-06 19.49.36.jpeg


🔼 上学的时候,每天早晨我都起的很早,六点多就从宿舍出来,其实也不是为了学习或者什么,我就喜欢走在安静的校园里,阳光洒在草丛间,偶尔有鸟叫声,我觉得这种感觉很美好。拍左边图的时候应该是个深秋了吧,树叶落了一地,而右边又正好是一个盛夏,树木生长的正好!这张照片的对比感让我感到无比惊喜!


拍摄心得


其实当时拍的比这些成品照片要多得多,总共拍了20多个地方吧,最终只合成出了78张可以用的,废片率相当高


因为什么呢?


原因是当时的我没有一个准确的参照物,在拍新照片时我一般会历经以下步骤:


1、先拿出手机看看老照片的角度和位置


2、举起ipad,凭感觉走到自己认为准确的位置上


3、拍一张看一下效果


4、满意就再拍两张当备份,不满意就继续重复以上步骤,直到排除满意的


好像一个递归方法!用伪代码实现一下就是:


const takePhoto = () => {
// 1、拿出老照片看角度 + 位置
const { position } = showOldPhoto();

// 2、拿出ipad,走到对应的位置上
walkToPosition(position);

// 3、拍一张看看效果
const { isOK } = takeSomePhoto();

// 4、判断是否满意,满意就结束,不满意就继续递归
!isOk && takePhoto();
}

当然,我也没那么工匠精神,我可能还得再加一个结束条件


const takePhoto = () => {
// 如果拍5次还不满意,就
if(reTryTime > 5) {
return;
}

// others
}

这个过程是比较重复且枯燥的,当然可以适当优化一下比如我可以在ipad上看照片,这样就省掉手机这一步了,另外可以在拍摄时不断地切换照片和相机app,这样就可以稍微快点看到当前位置对不对了...


em... 当时的我真的希望有一个工具能来辅助我拍这些照片!


噢噢噢!


现在的我可以很开心的跟那时候的我说,有了!现在有了!


你可以去微信小程序里搜:历旧弥新


你就可以搜到一个看起来还蛮专业的一个小程序,UI做的也不错,不丑!


它好像提供了一个你非常需要的功能:和旧照片来一场对话


你可以非常轻松的用它来拍一张新旧照片合成的照片,


就像下图:


WechatIMG301.jpeg


你可以将你的旧照片半透明的状态覆盖到相机上(就像左边的图),可以缩放平移,把它放在准确的位置上之后,然后你就可以非常轻易的去拍摄相同角度的照片了!


嗯... 听到这里是不是感觉出来这是一个广告了哈哈哈,没错,那就是了!


打广告!


对,这就是我基于四年前的想法,最近花了几个周末开发的一个小程序,历旧弥新


名字取自 历久弥新 => ,代表一种新旧交替的含义


来看下小程序首页


木头 2023-06-06 21.31.46.jpeg


它一共包含四个功能:


1、与旧照片来一次对话


2、已有关联的照片拼接


3、快速找一个相同的拍照姿势


4、异地也可以来合照


我们也提供了比较好的一些用户拍摄过的照片,放在首页的下半部分:


木头 2023-06-06 21.54.15.jpeg


木头 2023-06-06 21.57.31.jpeg


你可以快速的进行 拍同款 !就可以拍摄类似的照片啦!


当然它或许也存在一些问题希望大家不要吝啬自己的建议,可以评论在下方哈!

作者:木头就是我呀
来源:juejin.cn/post/7242247549511663672

收起阅读 »

Vue KeepAlive 为什么不能缓存 iframe

web
最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下: <router-view v-slot="{ Component ...
继续阅读 »

最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下:


  <router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>

看起来并没有什么问题,并且其他非 iframe 实现的页面都是可以被缓存的,因此可以推断问题出在 iframe 的实现上。


我们先了解下 KeepAlive


KeepAlive (熟悉的可跳过本节)


被 KeepAlive 包裹的组件不是真的卸载,而是从原来的容器搬运到另外一个隐藏容器中,实现“假卸载”, 当被搬运的容器需要再次挂载时,应该把组件从隐藏容器再搬运到原容器,这个过程对应到组件的生命周期就是 activated 和 deactivated


keepAlive 是需要渲染器支持的,在执行 mountComponent 时,如果发现是 __isKeepAlive 组件,那么会在上下文注入 move 方法。


function mountComponent(vnode, container, anchor) {
/**... */
const instance = {
/** ... */
state,
props: shallowReactive(props),
// KeepAlive 实例独有
keepAliveCtx: null
};

const isKeepAlive = vnode.__isKeepAlive;
if (isKeepAlive) {
instance.keepAliveCtx = {
move(vnode, container, anchor) {
insert(vnode.component.subTree.el, container, anchor);
},
createElement
};
}
}

实现一个最基本的 KeepAlive,需要注意几个点



  1. KeepAlive 组件会创建一个隐藏的容器 storageContainer

  2. KeepAlive 组件的实例增加两个方法 _deActive_active

  3. KeepAlive 组件存在一个缓存的 Map,并且缓存的值是 vnode


const KeepAlive = {
// KeepAlive 特有的属性,用来标识
__isKeepAlive: true,
setup() {
/**
* 创建一个缓存对象
* key: vnode.type
* value: vnode
*/

const cache = new Map();
// 当前 keepAlive 组件的实例
const instance = currentInstance;
const { move, createElement } = instance.keepAliveCtx;
// 创建隐藏容器
const storageContainer = createElement('div');

// 为 KeepAlive 组件的实例增加两个方法
instance._deActive = vnode => {
move(vnode, storageContainer);
};
instance._active = (vnode, container, anchor) => {
move(vnode, container, anchor);
};

return () => {
// keepAlive 的默认插槽就是要被缓存的组件
let rawVNode = slot.default();
// 不是组件类型的直接返回,因为其无法被缓存
if (typeof rawVNode !== 'object') {
return rawVNode;
}

// 挂载时,优先去获取被缓存组件的 vnode
const catchVNode = cache.get(rawVNode.type);
if (catchVNode) {
rawVNode.component = catchVNode.component;
// 避免渲染器重新挂载它
rawVNode.keptAlive = true;
} else {
// 如果没有缓存,就将其加入到缓存,一般是组件第一次挂载
cache.set(rawVNode.type, rawVNode);
}
// 避免渲染器真的把组件卸载,方便特殊处理
rawVNode.shouldKeepAlive = true;
rawVNode.keepAliveInstance = instance;
return rawVNode;
};
}
};

从上可以看到,KeepAlive 组件不会渲染额外的内容,它的 render 函数最终只返回了要被缓存的组件(我们称要被缓存的组件为“内部组件”)。KeepAlive 会对“内部组件”操作,主要是在其 vnode 上添加一些特殊标记,从而使渲染器能够据此执行特殊的逻辑。


function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
if (type === 'string') {
/** 执行普通的标签 patch */
} else if (type === Text) {
/** 处理文本节点 */
} else if (type === Fragment) {
/** 处理Fragment节点 */
} else if (typeof type === 'object') {
if (!n1) {
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor);
} else {
mountComponent(n2, container, anchor);
}
} else {
patchComponent(n1, n2, anchor);
}
}
}

function unmount(vnode) {
const { type } = vnode;
if (type === Fragment) {
/**... */
} else if (typeof type === 'object') {
if (vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode);
} else {
unmount(vnode.component.subTree);
}
return;
}
}

从上面的代码我们可以看出,vue 在渲染 KeepAlive 包裹的组件时,如果有缓存过将执行 keepAliveInstance._activate,在卸载时将执行 keepAliveInstance._deActivate


原因


通过上面的了解,我们知道,KeepAlive 缓存的是 vnode 节点,vnode 上面会有对应的真实DOM。组件“销毁”时,会将真实 DOM 移动到“隐藏容器”中,组件重新“渲染”时会从 vnode 上取到真实 DOM,再重新插入到页面中。这样对普通元素是没有影响的,但是 iframe 很特别,当其插入到页面时会重新加载,这是浏览器特性,与 Vue 无关。


解决方案


思路:路由第一次加载时将 iframe 渲染到页面中,路由切换时通过 v-show 改变显/隐。



  1. 在路由注册时,将 component 赋值为一个空组件


  {
path: "/chathub",
name: "chathub",
component: { render() {} }, // 这里写 null 时控制台会出 warning,提示缺少 render 函数
},


  1. 在 router-view 处,渲染 iframe,通过 v-show 来控制显示隐藏


  <ChatHub v-if="chatHubVisited" v-show="isChatHubPage"></ChatHub>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>


  1. 监听路由的变化,改变 iframe 的显/隐


const isChatHubPage = ref(false)
// 这里是个优化,想的是只有页面访问过该路由才渲染,没访问过就不渲染该组件
const chatHubVisited = ref(false)

watch(
() => routes.path,
(value) => {
if (value === '/chathub') {
chatHubVisited.value = true
isChatHubPage.value = true
} else {
isChatHubPage.value = false
}
},
{
immediate: true
}
)
作者:莱米
来源:juejin.cn/post/7246310077233659941

收起阅读 »

uni-app实现微信小程序蓝牙打印

web
打印流程 小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备...
继续阅读 »

打印流程


小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备写入数据


1.初始化蓝牙模块 uni.openBluetoothAdapter


注意:其他蓝牙相关 API 必须在 uni.openBluetoothAdapter 调用之后使用。


uni.openBluetoothAdapter({
success(res) {
console.log(res)
}
})

2.开始搜索附近的蓝牙设备 uni.startBluetoothDevicesDiscovery


此操作比较耗费系统资源,请在搜索并连接到设备后调用 uni.stopBluetoothDevicesDiscovery 方法停止搜索。


uni.startBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

3.获取搜索到的蓝牙列表 uni.getBluetoothDevices


获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备(不是很准确,有时会获取不到)。


uni.getBluetoothDevices({
success(res) {
console.log(res)
}
})

4.监听寻找到新设备的事件 uni.onBluetoothDeviceFound


监听寻找到新设备的事件,跟第三步一起使用,确保能获取附近所有蓝牙设备。


uni.onBluetoothDeviceFound(function (devices) {
console.log(devices)
})

5.连接蓝牙设备 uni.createBLEConnection


若APP在之前已有搜索过某个蓝牙设备,并成功建立连接,可直接传入之前搜索获取的 deviceId 直接尝试连接该设备,避免用户每次都要连接才能打印,省略二三四步减少资源浪费。


uni.createBLEConnection({
deviceId:获取到蓝牙的deviceId,
success(res) {
console.log(res)
}
})

6.关闭搜索蓝牙设备事件 uni.stopBluetoothDevicesDiscovery


停止搜寻附近的蓝牙外围设备。若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。


uni.stopBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

7.获取蓝牙设备的所有服务 uni.getBLEDeviceServices


uni.getBLEDeviceServices({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
success(res) {
console.log('device services:', res.services)
}
})

8.获取服务的所有特征值 uni.getBLEDeviceCharacteristics


uni.getBLEDeviceCharacteristics({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
success(res) {
console.log('device getBLEDeviceCharacteristics:', res.characteristics)
}
})
三种不同特征值的id
for (var i = 0; i < res.characteristics.length; i++) {
if (!notify) {
notify = res.characteristics[i].properties.notify;
if (notify) readId = res.characteristics[i].uuid;
}
if (!indicate) {
indicate = res.characteristics[i].properties.indicate;
if (indicate) readId = res.characteristics[i].uuid;
}
if (!write) {
write = res.characteristics[i].properties.write;
writeId = res.characteristics[i].uuid;
}
if ((notify || indicate) && write) {
/* 获取蓝牙特征值uuid */
success &&
success({
serviceId,
writeId: writeId,
readId: readId,
});
finished = true;
break;
}

9.向蓝牙设备写入数据 uni.writeBLECharacteristicValue


向低功耗蓝牙设备特征值中写入二进制数据。注意:必须设备的特征值支持 write 才可以成功调用。


并行调用多次会存在写失败的可能性。


APP不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。


若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。


安卓平台上,在调用 notifyBLECharacteristicValueChange 成功后立即调用 writeBLECharacteristicValue 接口,在部分机型上会发生 10008 系统错误


// 向蓝牙设备发送一个0x00的16进制数据
const buffer = new ArrayBuffer(1)
const dataView = new DataView(buffer)
dataView.setUint8(0, 0)
uni.writeBLECharacteristicValue({
// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId,
// 这里的value是ArrayBuffer类型
value: buffer,
success(res) {
console.log('writeBLECharacteristicValue success', res.errMsg)
}
})

写在最后


DEMO地址:gitee.com/zhou_xuhui/… (plus可能会报错,demo中注释掉就好,不影响流程)


打印机CPCL编程参考手册(CPCL 语言):http://www.docin.com/p-2160

作者:我是真的菜呀
来源:juejin.cn/post/7246264754141773885
10502…

收起阅读 »

悟了两星期终于悟了,移动端适配核心思想——没讲懂揍我

web
移动端开发与pc端适配的不同 pc端布局常用方案 所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱) 先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的...
继续阅读 »

移动端开发与pc端适配的不同


pc端布局常用方案


所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱)


先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的形式。也就是说所有的内容都写在版心容器盒子里,这个容器盒子设置:margin: 0 auto; & min-width: <版心宽度> & width: <版心宽度>就可以保证:




  • 当用户的屏幕(浏览器)宽度很大,或者缩放浏览器到很小比例时,此时浏览器的宽度大于版心盒子的width,版心容器会自动生成margin-left & margin-right,总会保证版心容器处于页面的正中心。


    这里可以提一嘴pc端浏览器缩放的原理:页面所有元素的css宽高都不会改变,只是css像素的在屏幕上展示的大小缩水了,具体点来说,原本700px * 700px的盒子在浏览器上用10cm * 10cm面积(的物理像素)渲染,但现在用原本<浏览器缩放比率> * 10cm * 10cm面积(的物理像素)渲染。




  • 当用户的屏幕小于版心盒子的width,出现横向滚动条,版心盒子的左右margin为0,width内的内容可滑动滚动条完整查看。




可以参考大淘宝pc端官网就是版心布局的实践。


好了,那么问题来了,移动端为啥不能照搬pc端的这种适配方案呢?


我们有必要先梳理一下移动端对页面进行渲染展示的逻辑:


移动端页面渲染的逻辑


<meta name="viewport">的情况


在html文档里没有<meta name="viewport">标签配置的情况下(通过对比即可理解<meta>标签的意义):


plus:如下整个流程篇口语话主要是梳理核心思路,没有一字一板的细节考究



  1. 我们项目中布局写的所有dom元素的css大小都正常(完全按照css大小的预期)在一个非常大的空间进行渲染,这个空间可能不是无限大,但是为了帮助理解,因为这个空间的大小一般不影响我们项目的正常布局,所以我们可以理解为无限大,这是第一步,即项目页面就像在pc端一样完全按照css写的大小以及布局进行渲染。



  1. 因为我们的移动端设备没有电脑屏幕那么大,所以会把第一步在“很大空间”渲染的页面进行缩小,直至缩小到我们的大页面宽度正好与手机屏幕的宽度一样即可。所以第二步相当于为了让用户把页面看全,手机自动把页面缩小至屏幕内。


为了帮助大家理解,也验证我上面的说法,我写了如下的pc端的版心布局的页面:


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

我把上面的页面部署到osrc(一个国内的免费部署网站,类似于vercel)上了(不可用chrome浏览器的移动端去模拟移动端访问的场景,chrome浏览器只是模拟了屏幕大小,而并没有模拟移动端环境,也就是说根本不会处理<meta>标签,所以这里我们需要部署),大家可以自行用pc端和移动端访问体验(实践一下绝对秒懂我上面的文字),为了照顾没有双端设备的读者,我截一下图(直接喂饭到胃哈哈)


pc端访问:


pc端访问版心布局.png


移动端访问:


移动端访问版心布局.jpg


清晰了吧兄弟们,我们写死的1200px宽的container盒子因为手机本身没这么大,所以缩小之后塞进了手机屏幕中,仔细看手机中的文字,已经被缩小的看不清了。


配置<meta name="viewport">的情况


暂时只给我们的index.html<meta name="viewport">添加一个content="width=device-width, initial-scale=1.0"


<!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>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

重新部署后访问查看效果,有meta的页面,部署地址<meta>标签是针对移动端的,所以pc端完全没影响,跟上面一样。现在我们访问移动端效果如下,我没有缩小图片,注意观察页面底部出现滚动条了(纵向滚动条有滚动所以文字没展示,不重要):


设置meta后移动端访问效果.jpg


解释一下content="width=device-width, initial-scale=1.0"的作用。


解读<meta> & dip & 布局视口(自认为最精华,全网少数不带偏新人的解释)


其实相当于我们在content字段中进行了两项配置,第一项是width=device-width,第一个width是指布局视口的宽度,引出概念,何为布局视口?还记得我们上面说的在没有<meta>时的那个非常大的布局空间嘛,就是它!我们让这个空间的宽度等于device-widthdevice-width就是指dip即设备独立像素,第二个概念,何为dip(device independent piexl设备独立像素)呢?听我的,完全不要被网上各种乱七八糟的解释弄迷糊了,什么dpr,什么物理像素,我只能说那些东西与我们开发者并没有直接关系,笔者读了几户所有能搜到的各种移动端入门文章,一言难尽... ,我来给出对于dip的理解,每一个型号的移动设备都具有的一个大小,这个大小是用设备独立像素dip来描述的,所以它仅仅是一个描述大小的单位,但是这个单位究竟是多大呢,换句话说dip有何特殊性呢?


在移动端不缩放的情况下,一个css像素等于一个设备独立像素dip

(chrome浏览器的移动端开发工具里显示的就是dip大小)也就是说,我们让布局视口的宽度等于设备的dip宽度,这里注意:布局视口由原来的”无限大“现在改为一个具体的数值,并不会影响页面的正常布局,页面还是会完整渲染, 只是最后不用缩小放进屏幕了,因为我们缩小的目的就是让布局视口完整的展现在屏幕中。因为屏幕不能展示完整整个页面的布局,所以底部出现滚动条。用户可以滚动访问页面全部内容。


其实这里initial-scale=1.0的作用就是让移动端浏览器不自行缩放,不然的话浏览器会把如上页面再缩小,然后放到手机屏幕里去。


关于<meta name="viewport">的最佳实践


简简单单如下:


<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

为什么说这是最佳实践,论证它其实还缺少一个关键点,也就是移动端css单位的选取,概括来说,width=device-width配置与移动端css单位的选取两者相辅相成,共同构成了“最佳实践”。


先说一下css单位选取——以vw为例:vw是相对屏幕大小的一个长度单位,1vw等于移动设备屏幕宽度的1%


如何理解“最佳实践”?


首先width=device-width保证了无论何种机型的移动设备,我们开发时写页面的布局视口始终等于屏幕宽度,但看这一点,确实没啥用。如果再来一个条件:页面中所有长度单位用vw来表达。细品!


如何细品?别忘了初心,我们的目标是在不同的移动设备上都能有统一的展示效果,开品:我们用不同dip宽度的设备打开网页,首先布局视口的大小会随着设备dip的不同而不同,也就是始终等于dip宽度:


布局视口宽度 === 设备dip宽度,

并且我们的所有元素大小单位都是vw,也就是说页面中所有元素大小都以屏幕宽度为参照物。最终的效果就是,一个dip宽度超级小的设备打开网页,与一个dip宽度非常大的设备打开网页,看到的页面内容是完全相似的,也就是每个元素在页面中所占的比例不同设备都一样(不同点就在于屏幕本身的大小不一样)!


一般<meta>标签的content中还会设置initial-scale=1.0, maximum-scale=1.0, user-scalable=no,即不让页面进行缩放,感觉这个看需求吧,不让缩小应该是必须的,因为可以想一想,用户缩小完全没有意义呐!(需要大家自己去理解,属于只可意会),至于让不让放大,应该是看情况吧,反正移动端淘宝官网是允许放大的。


移动端适配方案理解


主流的有vw方案、flexible + rem方案,总而言之,把元素的大小用rem来表示或者vw表示,本质都是以手机屏幕宽度为参考,vw比较直接,表达的意思就是1vw等于手机屏幕宽度的百分之一;rem比较间接,通过flexible.js先把得知屏幕宽度是多少px,然后设置<html>font-size,进而所有元素的rem其实还是表达占屏幕宽度的百分之多少。


当然两种方案都有一些技术细节问题需要解决,比如1px问题、安全区域问题等等。这里就不多说了。


相信能一步一步走到这里的同志,对移动端适配绝对有了一个清晰的把握。


2023.6.19,3: 59。

作者:荣达
来源:juejin.cn/post/7246001188448731196
更文不易,点个赞吧!

收起阅读 »

值得学习的JavaScript调试技巧

web
引言 最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各...
继续阅读 »

引言


最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各位。


一:console.dir


在打印dom节点时,普通的console.log是纯文本格式,而dir的打印是以对象的方式。因此在输出dom节点时,务必使用dir打印


<div id="main">
<div class="box1">
<p>p1</p>
</div>

</div>

let oD = document.querySelector('.box1')
console.log(oD)//普通的log输出
console.dir(oD)//dir输出方式

image.png


二:二次发起请求


在调试接口时,通常我们会刷新页面然后观察network的接口信息,如果项目加载时间过长,刷新页面查看接口的效率是十分低的。



  1. 对接口请求右键

  2. 选择Relpy xhr发送请求


image.png


三:接口请求参数修改


借助浏览器控制台可以不用修改代码就可以发送不同参数的新请求了。具体操作如下



  1. 对接口请求右键

  2. 选择copy。

  3. 再选择copy as fetch。

  4. 在console区域粘贴上面的请求信息,然后修改请求体参数。

  5. 然后切换到networkl查看最新请求的结果


效果展示


24.gif


四:css查看伪类hover,active样式


在控制台右侧选择:hov可以选择对应dom各种伪类状态下的css样式,十分的便捷


image.png


五:css样式跳转到对应文件查看


选择css样式,按住alt点击就可以跳到对应文件查看具体代码


25.gif


六:控制台输出选择的dom


首先在页面选择指定的位置dom,然后在在控制台使用$0就表示当前选中的dom了


26.gif


七:展开全部dom


有时候我们在页面查找一个dom时,它嵌套层级特别深。这巨他妈蛋疼一层层展开,这个时候我们就需要找到一键全部展开来帮助我们解决这个问题了。


27.gif


右键选择expand就可以展开选择的dom了。


八:断点调试


断点调试是本节最后一个内容了,它也是最核心的内容了,玩的6的是真的6,老大说我搞懂断点调试和对应的堆栈上下文就可以毕业了。(毕业=辞退?还是。。。)下面我列举的仅仅是入门级别的断点调试,只是说明如何上手操作,里面许多东西还望大家多多探索。


1. 打断点方式


代码中:debugger


在需要断点的地方写入debugger,此时程序运行后代码就会卡在这里,等待主人的安排


let a = 10
debugger
a++

浏览器中:



  1. 选择sources

  2. 在指定代码行左侧单击


image.png


2. 断点间调试


第一种断点调试是十分常用的方式,代码会从当前断点直接运行到下一个断点处执行,中间经过代码都默认被执行且跳过。如下图红色按钮就是断点间调试。


image.png


例子演示


28.gif


我们在上图中打了3个断点,逐个点击,首先从断点15行直接跳到断点17行,最后跳到19行。由于异步最后执行,所以最后又跳到断点15行结束。断点经过的地方鼠标移动到变量上可以查看其内部数据。


3. 逐步调试


逐步调试很明显就是字面意思,从当前断点位置开始一行一行的运行代码,稍微注意的是,遇到函数不进入函数的内部,而是直接执行完函数。


image.png


例子演示


29.gif


4. 进入与进出函数调试


逐步调试遇到函数是不进入函数内部的,因此需要借助进入和进出调试方式控制函数的访问


image.png


例子演示


30.gif


5. 逐步调试详细版


上面讲述了第一种逐步调试方式,其遇到函数是不进入函数内部的,而是直接执行函数。因此下面这种方式是逐步调试的详细版,它也是从断点位置逐步的调试运行,遇到函数也会进入函数的内部进行逐步执行。


image.png


九:React/Vue中尝试


有吊毛说react和vue咋调试?嗯,那个吊毛其实就是我,其实也很简单滴。



  1. 在需要调试的代码位置插入debugger

  2. 在浏览器控制台需要查看变量的地方插入断点

  3. 使用各种调试连招一顿操作就行。


代码例子


例如下面的例子,页面最后显示的num是多少?最后是101,不了解批量setState的开始肯定蒙,我们调试看看


import React,{useEffect, useState} from "react";
const Home = () => {
const [num,setNum] = useState(1)
useEffect(()=>{
debugger
setNum(100)
setTimeout(() => {
setNum(num+100)
}, 0);
},[])
return (
<div>num:{num}</div>
)
}
export default Home;

调试演示
根据调试发现,进入定时器的时候num还未更新,还是1。


31.gif


作者:前端兰博
来源:juejin.cn/post/7246376735838060603
收起阅读 »

前端时钟翻页效果,一看就会,一写就fei

web
最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。 一、元素拆解 从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一...
继续阅读 »

最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。


22.gif


一、元素拆解


动画拆解.png


从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一面显示旧文字的下半部分,另一面显示新文字的上半部分。翻转的动画,我们考虑采用FLIP的思想:



  1. 先实现【动画结束帧】的样式;

  2. 再从【动画开始帧】播放。


二、实现结束帧样式


准备工作:用vue脚手架创建一个模板项目,并添加一个div容器:


image.png


<!-- App.vue -->
<template>
<div id="app">
<test-comp/>
</div>
</template>


<!-- Test.vue -->
<template>
<div class="card"></div>
</template>


<style lang="less" scoped>
.card {
position: relative;
border: solid 4px black;
width: 400px;
height: 400px;
perspective: 1000px;
}
</style>


2.1 实现静止的上半面板


image.png


<template>
<div class="card">
<div class="half-card top-half"></div>
<!-- <div class="half-card bottom-half">财</div> -->
</div>

</template>

<style lang="less" scoped>
/* ... */
.half-card {
position: absolute;
width: 100%;
height: 50%;
overflow: hidden;
background-color: #2c292c;
color: white;
font-size: 320px;
}
.top-half {
line-height: 400px;
}
</style>


我们知道line-height配合font-size可以控制文字在垂直方向的位置,大多数情况下,文字顶部与容器顶部的距离公式为(line-height - font-size) / 2。


记容器高度h,文字大小f,容器只显示文字上半部分的情况下,上述距离的值为h - f / 2,即(line-height - f) / 2 = h - f / 2,所以line-height为2h(400px)。


2.2 实现静止的下半面板


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<div class="half-card bottom-half"></div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.bottom-half {
top: 50%;
line-height: 0;
}
</style>


在容器只显示文字下半部分的情况下,完整的文字顶部距离容器顶部的距离是-f / 2,那么就有(line-height - f) / 2 = - f / 2,即line-height = 0;


2.3 实现旋转面板


2.3.1 旋转面板的正面————新文字的上半部分


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<div class="half-card front-side"></div>
<!-- <div class="half-card back-side">发</div> -->
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.rotating-half {
position: absolute;
width: 100%;
height: 50%;
.half-card {
height: 100%;
}
}
.front-side {
line-height: 400px;
}
</style>


2.3.2 旋转面板的背面————旧文字的下半部分


怎么让一个div背对我们?只要让它绕着自己的腰部横线翻转180度即可(翻跟斗)。


image.png
image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<!-- <div class="half-card front-side">财</div> -->
<div class="half-card back-side"></div>
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.back-side {
line-height: 0;
transform: rotateX(180deg); // !!!!!!!!!!!
}
</style>


现在,如果把正面也加上,会发现这样一个问题:两个面的位置是重叠的,在模板中后声明的背面元素(即使它是背对着我们)会覆盖正面元素。我们想让这两个面在背对我们的状态下都不显示,这就需要到如下的css属性:backface-visibility: hidden。


此外,现在一个旋转面板中带有两个“面”,我们想要这两个面随着父元素面板的3d旋转一起旋转,也就是保持相对静止,这就需要设置旋转面板【将子元素纳入自己的3d变换空间】:transform-style: preserve-3d。


加上css后,让旋转面板简单地旋转一下,看看效果(效果图有点慢):


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-style: preserve-3d;
.half-card {
/* ... */
backface-visibility: hidden;
}
/* to delete */
transition: transform 1s;
&:hover { transform: rotateX(-180deg); }
}
/* ... */
</style>

2.gif


至此,三个面板静态效果已经完成:


image.png


三、播放动画


在第二节已经得到了动画结束时的状态。接下来需要从动画开始的状态进行播放。


3.1 设置好旋转轴


在目标动画中,旋转面板应该是绕着底边进行旋转的。把【变换原点】设置为底边的中点,这样,经过这个点的X轴就和底边所在的直线重合,绕X轴旋转就等价于绕底边旋转:


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-origin: center bottom;
}
/* ... */
</style>

3.2 找到动画开始帧,使用animate播放动画


动画开始时,旋转面板在主面板的下半区域。要从上半区域(无变换状态)到达下半区域,需要绕着底边逆时针旋转180度,因此开始帧所处于的变换状态就是rotateX(-180deg),从而得到动画的关键帧:


【transform: rotateX(-180deg)】->【transform: none】。


我们给旋转面板加上ref,然后在组件挂载完毕时播放即可:


<script>
export default{
mounted() {
this.$refs.rotate?.animate?.(
[
{ offset: 0, transform: 'rotateX(-180deg)' },
// { offset: 1, transform: 'none' },
],
{
duration: 1000,
easing: 'ease-in-out',
},
);
},
};
</script>

2.gif


四、应用


这样的UI组件可能会用于记录时间、比赛分数变化啥的,自然是不能把值写死。考虑如下的应用场景:


<!-- App.vue -->
<template>
<div id="app" class="flex-row">
<test-comp :value="scoreLGD"/>
<h1>VS</h1>
<test-comp :value="scoreLiquid"/>
</div>
</template>


<script>
import TestComp from './Test';
export default {
components: { TestComp },
data() { return {
scoreLGD : 15,
scoreLiquid: 13,
};
},
mounted() {
setInterval(() => {
this.scoreLGD = this.randomInt(99);
this.scoreLiquid = this.randomInt(99);
}, 5000);
},
/* ... */
};

在该场景下,翻页组件需要在更新时而不是挂载时执行动画(因为没有上一个值)。因此我们在组件内部维护一个记录上一个值的状态,然后把动画从挂载阶段移动到更新阶段:


<template>
<div class="card">
<!-- 旧文字上 -->
<div
v-if="staleValue !== undefined"
class="half-card top-half">

{{ staleValue }}
</div>
<!-- 新文字下 -->
<div class="half-card bottom-half">{{ value }}</div>
<!-- 旋转面板 -->
<div ref="rotate" class="rotating-half">
<!-- 新文字上 -->
<div class="half-card front-side">{{ value }}</div>
<!-- 旧文字下 -->
<div
v-if="staleValue !== undefined"
class="half-card back-side">

{{ staleValue }}
</div>
</div>
</div>

</template>

<script>
export default {
props: ['value'],
data() { return { staleValue: undefined }; },
watch: {
value(_, old) { this.staleValue = old; },
},
updated() {
this.$refs.rotate?.animate?.(
[{ offset: 0, transform: 'rotateX(-180deg)' }],
{ duration: 1000, easing: 'ease-in-out' },
);
},
};
</script>


基本完成:


22.gif


总结一下


实现翻页效果 = 实现两块静态面板 + 实现一块双面旋转面板 + 播放旋转动画。


这里用vue写了demo, react应该也差不多,将updated换成layoutEffect等等。


另外,动画也可以用类名加css实现,当元素不在视口可以不播放,一些样式可以改成props配置。总之应该有不少地方还可以迭代优化下。


参考文章如下,分析思路基本一致,代码实现上有差异:

【1】优雅的时钟翻页效果,让你的网页时钟与众不同!

【2】原生JS实现

一个翻页时钟

收起阅读 »

用js脚本下载某书的所有文章

web
前言 在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。 想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是...
继续阅读 »

前言


在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。


想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是下载功能根据日期什么判断了,还是bug了,试了好几次都这样,官方渠道只能放弃了。


手动一篇一篇粘贴的成本太高了,不仅有发布的文章,还有各种没有发布的笔记在里面,各种文章笔记加起来好几百篇呢,既然是工程师,就用工程师思维解决实际问题,能用脚本下载个人账号的下的所有文章吗?


思考.gif


思路梳理


由于是下载个人账号下的所有文章,包含发布的和未发布的,来看下个人账号的文章管理后台


文集模式.png


根据操作以及分析浏览器控制台 网络 请求得知,文章管理后台逻辑是这样的,默认查询所有文集(文章分类列表), 默认选中第一个文集,查询第一个文集下的所有文章,默认展示第一篇文章的内容,点击文集,获取当前文集下的所有文章,默认展示文集中的第一篇文章,点击文章获取当前文章数据,来分析一下相关的接口请求


获取所有文集


https://www.jianshu.com/author/notebooks 这个 Get 请求是获取所有 文集,用户信息是放在 cookie


分析请求模式.png


来看下返回结果


[
    {
        "id": 51802858,
        "name": "思考,工具,痛点",
        "seq": -4
    },
    {
        "id": 51783763,
        "name": "安全",
        "seq": -3
    },
    {
        "id": 51634011,
        "name": "数据结构",
        "seq": -2
    },
    ...
]

接口返回内容很简单,一个 json 数据,分别是:id、文集名称、排序字段。


获取文集中的所有文章


https://www.jianshu.com/author/notebooks/51802858/notes 这个 Get 请求是根据 文集id 获取所有文章,51802858"思考,工具,痛点" 文集的id, 返回数据如下


[
    {
        "id": 103888430, // 文章id
        "slug": "984db49de2c0",
        "shared": false,
        "notebook_id": 51802858, // 文集id
        "seq_in_nb": -4,
        "note_type": 2,
        "autosave_control": 0,
        "title": "2022-07-18", // 文章名称
        "content_updated_at": 1658111410,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    {
        "id": 98082442,
        "slug": "6595bc249952",
        "shared": false,
        "notebook_id": 51802858,
        "seq_in_nb": -3,
        "note_type": 2,
        "autosave_control": 3,
        "title": "架构图",
        "content_updated_at": 1644215292,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    ...
]

接口返回的 json 数据里包含 文章id文集名称,这是接下来需要的字段,其他字段暂时忽略。


获取文章内容


https://www.jianshu.com/author/notes/98082442/content 这个 Get 请求是根据 文章id 获取文章 Markdown 格式内容, 98082442《架构图》 文章的id, 接口返回为 Markdown 格式的字符串


{"content":"![微服务架构图 (3).jpg](https://upload-images.jianshu.io/upload_images/6264414-fa0a7893516725ff.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"}

现在,我们了解清楚了文集,文章,以及文档内容的获取方式,接下来开始脚本实现。


代码实现


由于我是前端攻城狮,优先考虑使用 js 来实现下载,还有一个考虑因素是,接口请求里面的用户信息是通过读取 cookie 来实现的,js 脚本在浏览器的控制台执行发起请求时,会自动读取 cookie,很方便。


如果要下载个人账号下所有文章的话,根据梳理出来的思路编写代码就行


获取所有文集id


fetch("https://www.jianshu.com/author/notebooks")
  .then((res) => res.json())
  .then((data) => {
    // 输出所有文集
    console.log(data);
  })

使用fetch函数进行请求,得到返回结果,上面的代码直接在浏览器控制台执行即可,控制台输出效果如下


输出所有文集.png


根据文集数据获取所有文章


上一步得到了所有文集,使用 forEach 循环所有文集,再根据 文集id 获取对应文集下的所有文章,依然使用 fetch 进行请求


...
let wenjiArr = [];
wenjiArr = data; // 文集json数据
let articleLength = 0;
wenjiArr.forEach((item, index) => {
  // 根据文集获取文章
  fetch(`https://www.jianshu.com/author/notebooks/${item.id}/notes`)
    .then((res2) => res2.json())
    .then((data2) => {
      console.log("输出文集下的所有文章:", data2);
    });
});

根据文章id获取文章内容,并下载 Markdown 文件


有了文章 id, 根据 id 获取内容,得到的内容是一个对象,对象中的 content 属性是文章的 Markdown 字符串,使用 Blob 对象和 a 标签,通过 click() 事件实现下载。


在这里的代码中使用 articleLength 变量记录了一下文章数量,使用循环中的文集名称和文章名称拼成 Markdown 文件名 item.name - 《item2.title》.md


...
console.log(item.name + " 文集中的文章数量: " + data2.length);
articleLength = articleLength + data2.length;
console.log("articleLength: ", articleLength);
data2.forEach(async (item2, i) => {
// 根据文章id获取Markdown内容
fetch(`https://www.jianshu.com/author/notes/${item2.id}/content`)
.then((res3) => res3.json())
.then((data3) => {
console.log(data3);
const blob = new Blob([data.content], {
type: "text/markdown",
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = item.name + " - 《" + item2.title + `》.md`;
link.click();
});
});

代码基本完成,运行


在浏览器控制台中运行写好的代码,浏览器下方的下载提示嗖嗖的显示,由于没有做任何处理,当前脚本执行过程中报错了,文章下载了几十个以后就停止了,提示 429



HTTP 请求码 429 表示客户端发送了太多的请求,服务器无法处理。这种错误通常表示服务器被攻击或过载。



文章内容太多了,意料之中的情况,需要改进代码


思路改进分析


根据问题分析,脚本里的代码是循环套循环发请求的,这部分改造一下试试效果。


把每个循环里面发送 fetch 请求的外面是加个 setTimeout, 第一个循环里面的 setTimeout 延迟参数设置为 1000 * indexindex 为当前循环的索引,第一个请求0秒后执行,后面每一次都加1秒后执行,由于文集的数量不多,大约20个,这一步这样实现是没问题的。


重点是第二个循环,根据文集获取所有文章,每个文集里多的文章超过50篇,少的可能2,3篇,这里面的 setTimeout 延迟参数这样设置 2000 * (i + index)i 为第二个循环的索引,这样保证在后面的请求中避免了某个时间段发送大量请求,导致丢包的问题。


再次执行代码,对比控制台输出的文章数量和下载目录中的文章(项目)数量,如果一致,说明文章都下载好了


390.png


下载.png


改造后的完整代码地址


github.com/gywgithub/F…


思考


整体来看,文章下载脚本的逻辑并不复杂,接口参数也简单明确,两个 forEach 循环,三个 fetch 请求,把获取到的文章内容实用 a 标签下载下来就行了。关于大量请求发送导致 429 或者请求丢失的问题,脚本中使用了一种方案,当时还想到了另外两种方案:


请求同步执行


通过同步的方式先得到所有文集下的所有文章,再根据文章列表数组同步发请求下载,请求一个个发,文章一篇篇下载


Promise.all


使用 Promise.all() 分批发送请求,避免一次请求发送太多



也可能还有其他的解决方案,欢迎大家评论区讨论交流,一起学习共同进步 ^-^



作者:草帽lufei
来源:juejin.cn/post/7245184987531018300

收起阅读 »

这道面试题真的很变态吗?😱

web
最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题 题...
继续阅读 »

最近帮公司招聘,主要负责一面,所以基本上问的基础多一点。但是我在问这样一道面试题的时候,很少有人答对。不少人觉得我问这道题多少有点过分了😭,当然了面试还是奔着相互沟通相互学习的目的,并不是说这道题不会就被刷掉,单纯的觉得这道题有意思。话不多说,我们直接上题


题目


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

这里我会要求面试者从上到下依次说出执行结果。普遍多的面试者给出的答案是:foo1、foo2、foo1、foo2。虽然在我看来这是一道简单的面试题,但是也不至于这么简单吧😱~~~


当然面试本来就是一个相互讨论的过程,那就和面试者沟通下这道题我的理解,万一我理解错了呢😂


解答


拆分函数表达式


首先我会让面试者先看前面两个函数


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

这时候大部分人基本上都可以答对了,是:foo1、foo2。再有很少数的人答不对那就只能”施主,出门右转“😌。接着根据我当时的心情可能会稍作追问(美女除外🙂):


foo()
var foo = function () {
console.log("foo1")
}

这时候又有一部分的人答不上来了。这毫无疑问是肯定会报错的啊


image.png


我们都知道用var定义的变量会变量提升,所以声明会被拿到函数或全局作用域的顶部,并且输出undefined。所以当执行foo()的时候,foo还是undefined,所以会报错。由于js从按照顺序从上往下执行,所以当执行foo = function(){}的时候,才对foo进行赋值为一个函数。我们重新看拆分之后的代码


var foo = function () {
console.log("foo1")
}
foo()

var foo = function () {
console.log("foo2")
}
foo()

foo首先会变量提升,然后进行赋值为function。所以当执行第一个foo的时候,此时foo就是我们赋值的这个函数。接着执行第二个foo的赋值操作,由于函数作用域的特性,后面定义的函数将覆盖前面定义的函数。
由于在调用函数之前就进行了函数的重新定义,所以在调用函数时,实际执行的是最后定义的那个函数。所以上面的代码会打印:foo1、foo2。


这种定义函数的方式,我们称为函数表达式。函数表达式是将函数作为一个值赋给一个变量或属性


函数表达式我们拆分完了,下面就看看函数声明吧。


拆分函数声明


function foo() {
console.log("foo1")
}
foo()

function foo() {
console.log("foo2")
}
foo()

大部分人其实都卡在了这里。函数声明会在任何代码执行之前先被读取并添加到执行上下文,也就是函数声明提升。说到这里其实大多数人就已经明白了。这里使用了函数声明定义了两个foo函数,由于函数声明提升,第二个foo会覆盖第一个foo,所以当调用第一个foo的时候,其实已经被第二个foo覆盖了,所以这两个打印的都是foo2。


当两段代码结合


当开始解析的时候,函数声明就已经提升了,第四个foo会覆盖第三个foo。然后js开始从上往下执行,第一个赋值操作之后执行foo()后,打印了”foo1“,第二个赋值之后执行foo(),打印了"foo2"。下面两个foo的执行其实是第二个赋值了的foo,因为函数声明开始从刚开始就被提升了,而下面的赋值会覆盖foo。


总结


我们整体分析代码的执行过程



  1. 通过函数表达式定义变量foo并赋值为一个匿名函数,该函数在被调用时打印"foo1"。

  2. 接着,通过函数表达式重新定义变量foo,赋值为另一个匿名函数,该函数在被调用时打印"foo2"。

  3. 使用函数声明定义了两个名为foo的函数。函数声明会在作用域中进行提升。后面的会覆盖前面的,由于声明从一开始就提升了,而又执行了两个赋值操作,所以此时foo是第二个赋值的函数。

  4. 然后调用foo(),输出"foo2"。

  5. 再调用foo(),也输出"foo2"。


其实就一个点: 函数表达式相对于函数声明的一个重要区别是函数声明在代码解析阶段就会被提升(函数声明提升),而函数表达式则需要在赋值语句执行到达时才会创建函数对象


小伙伴们,以上是我的理解,欢迎在评论区留言,大家相互讨论相互学习。


之前的描述确实有点不妥,所以做了改动,望大家谅解,还

作者:翰玥
来源:juejin.cn/post/7237051958993469496
是本着相互学习的态度

收起阅读 »

别再无聊地显示隐藏了,Vue 中使用过渡动画让你的网页更有活力

web
Vue 是一款流行的前端框架,支持过渡动画的实现是其中的一项重要特性。在 Vue 中,使用过渡动画可以为用户提供更加友好的用户体验。下面我将为大家介绍一下子如何在 Vue 中实现过渡动画。 1. 你知道什么是过渡动画吗 过渡动画是指在 DOM 元素从一个状态到...
继续阅读 »

Vue 是一款流行的前端框架,支持过渡动画的实现是其中的一项重要特性。在 Vue 中,使用过渡动画可以为用户提供更加友好的用户体验。下面我将为大家介绍一下子如何在 Vue 中实现过渡动画。


1. 你知道什么是过渡动画吗


过渡动画是指在 DOM 元素从一个状态到另一个状态发生变化时,通过添加过渡效果使得这个变化看起来更加平滑自然的动画效果。在 Vue 中,过渡动画可以应用到以下几个场景中:



  • 显示和隐藏元素

  • 动态添加或删除元素

  • 元素位置的变化


2. Vue 过渡动画的实现方法


2.1 CSS 过渡


Vue 提供了 transition 组件来支持过渡动画。我们可以在需要应用过渡动画的元素外层包裹一个 transition 组件,并通过设置 CSS 样式或绑定动态 class 来实现过渡动画的效果。


Vue 的过渡动画通过添加 CSS 类名来实现。我们可以通过为需要过渡的元素添加 v-ifv-show 指令来控制元素的显示和隐藏,然后使用 transition 组件进行动画效果的设置。


下面我写个示例给大家参考一下,我将给按钮添加过渡动画效果:


<template>
<button @click="show=!show">Toggle</button>
<transition name="fade">
<div v-if="show">Hello, World!</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false
};
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

在上面的代码思路中,我们在 transition 包裹的 div 元素上使用了 v-if 指令来控制元素的显示和隐藏。同时,我们给 transition 组件添加了一个 name 属性,并使用 CSS 样式来定义过渡动画的效果。其中,.fade-enter-active.fade-leave-active 分别表示进入和离开时的过渡动画,而 .fade-enter.fade-leave-to 则分别表示进入和离开时元素的样式。


2.2 JS 过渡


除了使用 CSS 过渡外,在 Vue 中也可以使用 JavaScript 过渡来实现动画效果。JS 过渡相比于 CSS 过渡的优势在于它可以更加灵活地控制过渡动画。


它与 CSS 过渡不同,Javascript 过渡可以更加灵活地控制过渡动画,可以实现更加丰富的效果。Vue 提供了事件钩子函数,使得我们可以自定义过渡动画的效果。


image.png


Vue 中提供了以下事件钩子函数:



  • before-enter

  • enter

  • after-enter

  • enter-cancelled

  • before-leave

  • leave

  • after-leave

  • leave-cancelled


我们可以使用 transition 组件的 mode 属性来设置过渡的模式,如果使用了 mode 属性,Vue 将会自动调用对应的钩子函数,我们可以通过这些钩子函数来自定义过渡效果。


下面是我写的一个基于 JS 过渡的演示Demo,我们将为按钮添加自定义的过渡动画:


<template>
<button @click="show=!show">Toggle</button>
<transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave">
<div v-if="show">Hello, World!</div>
</transition>
</template>
<script>
export default {
data() {
return {
show: false
};
},
methods: {
beforeEnter(el) {
el.style.opacity = 0;
el.style.transformOrigin = 'left';
},
enter(el, done) {
anime({
targets: el,
opacity: 1,
translateX: [20, 0],
easing: 'easeInOutQuad',
duration: 500,
complete: done
});
},
leave(el, done) {
anime({
targets: el,
opacity: 0,
translateX: [-20, 0],
easing: 'easeInOutQuad',
duration: 500,
complete: done
});
}
}
};
</script>

在上面的前端页面中,我们通过设置 transition 组件的 css 属性为 false 来禁用 CSS 过渡,然后我们使用了 before-enterenterleave 等钩子函数来自定义过渡动画。在这个示例代码中,我们使用了第三方动画库 Anime.js 来实现元素进入和离开时的动画效果,同时在 anime 动画完成后,我们还需要手动调用 done 函数来告知 Vue 过渡动画已经完成。


3. 小结一下


通过我写的这篇文章的介绍,可以让大家多了解了 Vue 过渡动画的基本概念,并且掌握了如何在 Vue 中实现过渡动画。不论是使用 CSS 过渡还是 JavaScript 过渡,都可以帮助我们为用户提供更加友好的用户体验。我希望本文对您有所帮助,如果您有任何疑问或建议,欢迎在评论区留言。


作者:Cosolar
来源:juejin.cn/post/7241874482574114875
收起阅读 »

for循环的代价

web
for循环的可控性比forEach好,但是它的代价却鲜为人知,这篇文章就是简单的探讨下这背后的代价。 先定一个基调,我们从作用域的角度去分析,也会按需讲一些作用域的知识。 作用域是什么? 要解释这个问题,先从程序的角度来看,几乎所有编程语言上来就会介绍自己的变...
继续阅读 »

for循环的可控性比forEach好,但是它的代价却鲜为人知,这篇文章就是简单的探讨下这背后的代价。 先定一个基调,我们从作用域的角度去分析,也会按需讲一些作用域的知识。


作用域是什么?


要解释这个问题,先从程序的角度来看,几乎所有编程语言上来就会介绍自己的变量类型,因为如果没有变量程序只能执行一些简单的任务。但是引入变量之后程序怎么才能准确的找到自己需要的变量。这就需要建立一套规则让程序能够准确的找到需要的变量,这样的规则被称为作用域。


块级作用域


块级作用域如同全局作用域和函数作用域一样,只不过块级作用域由花括号({})包裹的代码块创建的。在块级作用域内声明的变量只能在该作用域内访问,可以使用 let 或 const 关键字声明变量,可以在块级作用域内创建变量。
所以引擎在编译时是通过花括号({})包裹和声明关键字判断是否创建块级作用域,因此绝大多数的语句是没有作用域的,同时从语言设计的角度来说越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
基于这个原则,switch语句被设计为有且仅有一个作用域,无论它有多少个case语句,其实都是运行在一个块级作用域环境中的。
一些简单的、显而易见的块级作用域包括:


// 例1
try {
// 作用域1
}
catch (e) { // 表达式e位于作用域2
// 作用域2
}
finally {
// 作用域3
}

// 例2
//(注:没有使用大括号)
with (x) /* 作用域1 */; // <- 这里存在一个块级作用域

// 例3, 块语句
{
// 作用域1

除此之外,按上述理解,for语句也可以满足上述的条件。


for循环作用域


并不是所有的for循环都有自己的作用域,有且仅有


for ( <let/const> ...) ...

这个语法有自己的块级作用域。当然,这也包括相同设计的for await和for .. of/in ..。例如:


for await ( <let/const> x of ...) ...
for ( <let/const> x ... in ...)
for ( <let/const> x ... of ...) ...

已经注意到了,这里并没有按照惯例那样列出“var”关键字。简单理解就是不满足创建的条件。Js引擎在编译时,会对标识符进行登记,而为了兼容,将标识符分为了两类varNames 和 lexicalNames。以前 var 声明、函数声明将会登记在varNames,为了兼容varNames只有全局作用域和函数作用域两种,所以编译时会就近登记在全局作用域和函数作用域中且变量有“提升”效果。Es6新增的声明关键词将登记在lexicalNames,编译时会就近创建块级作用或就近登记在函数作用域中。



varNames 和 lexicalNames属性只是一个用于记录标识符的列表,是通过词法作用域分析,在当前作用域中做登记的。它们记录了当前作用域中的变量和函数的名称,以及它们的作用域信息,帮助 JavaScript 引擎在代码执行时正确地解析标识符的作用域。



关于作用域还有一点要说明,JavaScript采用词法作用域,这意味着变量的作用域在代码编写时就确定了,而不是在运行时确定。这与动态作用域不同,动态作用域是根据函数的调用栈来确定变量的作用域。
举个例子:


function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();

词法作用域下log的结果是2,动态作用域下log的是3。



词法作用域是指 JavaScript 引擎在编译时如何确定变量的作用域。在 JavaScript 中,词法作用域是指变量的作用域是由代码的位置(词法)决定的,而不是由运行时的作用域链决定的。



变量的作用域和可见性是由词法作用域和作用域链来决定的,作用域链是基于词法作用域和函数作用域来确定的,这也证明了 JavaScript 采用的是词法作用域。


for循环隐藏作用域


首先,必须要拥有至少一个块级作用域。如之前讲到的,满足引擎创建的条件。但是这一个作用域貌似无法解释下面这段代码


for(let i=0;i<10;i++){
let i=1;
console.log(i) // 1
}

这段代码时可以正常运行的,而我们知道let语句的变量不能重复声明的,所以对for循环来说一个作用域是满足了这个场景的。
但是这段代码依然可以执行,那JS引擎是如何处理的呢?
只能说明循环体又创建了一个块级作用域,事实如你所见,JS引擎确实对for循环的每个循环体都创建了一个块级作用域。
举个栗子,以下代码中使用 let 声明变量 i


for (let i = 0; i < 5; i++) {
console.log(i);
}

在编译时,JavaScript 引擎会将循环体包裹在一个块级作用域中,类似于以下代码:


{
let i;
for (i = 0; i < 5; i++) {
console.log(i);
}
}

每次循环都会创建一个新的块级作用域,因此,在循环中声明的变量 i 只能在当前块级作用域中访问,不会污染外部作用域的变量。而通过作用域链每个循环体内都可以访问外层变量i。
而我们知道从语言设计的角度来说越少作用域的执行环境调度效率也就越高,执行时的性能也就越好,所以这也算是代价之一吧。
就算如此设计还是无法解释下面这段代码。


for (let i = 0; i < 5; i++) {
setTimeout(()=>{console.log(i)})
}

如果按上述的理解,那最后log时访问的都是外层的变量i,最后的结果应该都是4,可事实却并非如此。当定时器被触发时,函数会通过它的闭包来回溯,并试图再次找到那个标识符i。然而,当定时器触发时,整个for迭代都已经结束了。这种情况下,访问i,获取到的也是上层作用域中的i,此刻i的值应该是最后一次赋。
之所能按我们预想的输出1,2,3,4,那是因为JavaScript 引擎在创建循环体作用域的时候,会在该作用域中声明一个新的变量 i,并将其初始化为当前的迭代次数,这个新的变量 i 会覆盖外层的变量 i。这个过程是由 JavaScript 引擎自动完成的,我们并不需要手动

作者:chtty
来源:juejin.cn/post/7245641209913360445
创建或赋值这个变量。

收起阅读 »

前端如何破解 CRUD 的循环

web
据说,西西弗斯是一个非常聪明的国王,但他也非常自负和狂妄。他甚至敢欺骗神灵,并把死者带回人间。为此,他被宙斯(Zeus)惩罚,被迫每天推着一块巨石上山,但在接近山顶时,巨石总是会滚落下来,他不得不重新开始推石头,永远困在这个循环中… 很多开发工作也如此单调而乏...
继续阅读 »

据说,西西弗斯是一个非常聪明的国王,但他也非常自负和狂妄。他甚至敢欺骗神灵,并把死者带回人间。为此,他被宙斯(Zeus)惩罚,被迫每天推着一块巨石上山,但在接近山顶时,巨石总是会滚落下来,他不得不重新开始推石头,永远困在这个循环中…


很多开发工作也如此单调而乏味,比如今天要讲的中后台开发的场景。中后台业务基本上就是一些数据的增删改查、图表,技术含量不高,比较容易范式化。


前端如何破除这种 CRUD 的单调循环呢?








低代码


过去几年前端的低代码很火,这些低代码平台通常支持创建数据模型后,一键生成对应的增删改查页面:




模型驱动生成页面





💡 本文提及的低代码是狭义低代码,你可以认为就是可视化搭建平台





低代码在过去几年就是 「雷声大,雨点小」,跟现在的 AI 颇为相似。


不管是大厂还是小厂都在搞低代码,包括笔者也参与过几个低代码项目,但是小厂支撑不起来这样的资源投入,最后都胎死腹中。我相信很多读者也经历过这种情况。
大部分公司只是尾随市场营销噱头,盲目跟风,压根就没有做这种低代码平台资源准备和沉淀。


作为前端,能参与到低代码项目的开发是一件非常兴奋的事情,毕竟是少数前端能主导的项目,架构、组件设计、编辑器的实现可玩性很高,可以跟同行吹很久。


作为用户(开发者)呢?可能会排斥和质疑,不管怎么说,它并没有发挥市场所期望的价值。




最主要的原因是:它解决不了复杂的问题




低代码直观、门槛低, 前期开发确实很爽,可视化数据建模、拖拉拽生成页面、流程编排,很快就可以把一些简单的业务开发出来。


然而软件编码本身占用研发流程的比例,据 ChatGPT 估算大约只有 20% ~ 30%。而且业务持续变化,代码也需要持续迭代。试想一下如何在这些低代码平台上进行重构和检索?






总的来说,有一些缺点:




  • 复杂的业务逻辑用低代码可能会更加复杂。低代码应该是特定领域问题的简化和抽象,如果只是单纯将原有的编码工作转换为 GUI 的模式,并没有多大意义。


    例如流程编排,若要用它从零搭建一个复杂的流程,如果照搬技术语言去表达它,那有可能是个地狱:


    流程编排


    理想的流程编排的节点应该是抽象程度更高的、内聚的业务节点,来表达业务流程的流转。然而这些节点的设计和开发其实是一件非常有挑战性的事情。




  • 软件工程是持续演进的,在可维护性方面,目前市面上的低代码平台并不能提供可靠的辅助和验证。因此企业很难将核心的稳态业务交给这些平台。




  • 还有很多… 平台锁定,缺乏标准,性能问题、复用、扩展性、安全问题、黑盒,可迁移性,研发成本高,可预测性/可调试性差,高可用,版本管理,不能自动化…








当然,低代码有低代码的适用场景,比如解决特定领域问题(营销活动页面,海报,数据大屏,表单引擎、商城装修、主页),POC 验证。即一些临时的/非核心的敏态业务



💡 目前有些低代码平台也有「出码能力」,让二开有了一定的可行性。




💡 AI 增强后的低代码可能会更加强大。但笔者依旧保持观望的态度,毕竟准确地描述软件需求,本身就是就是软件研发的难题之一,不然我们也不需要 DDD中的各种方法论,开各种拉通会,或许也不需要需求分析师,产品…


非专业用户直接描述需求来产出软件,大多是不切实际的臆想









中间形态


有没有介于可视化低代码平台和专业代码之间的中间形态?既能保持像低代码平台易用性,同时维持代码的灵活性和可维护性。


我想那就是 DSL(domain-specific language) 吧? DSL 背后体现的是对特定领域问题的抽象,其形式和语法倒是次要的。



💡 DSL 的形式有很多,可以创建一门新的微语言(比如 SQL, GraphQL);可以是一个 JSON 或者 YAML 形式;也可以基于一门现有的元语言(比如 Ruby、Groovy,Rust…)来创建,这些元语言,提供的元编程能力,可以简洁优雅地表达领域问题,同时能够复用元语言 本身的语言能力和基础设施。



严格上可视化低代码平台也是一种‘可视化’ 的 DSL,笔者认为它的局限性更多还是来源‘可视化’,相对的,它优点也大多来源’可视化‘



这又牵扯到了持续了半个多世纪的: GUI vs CLI(程序化/文本化) 之争。这个在《UNIX 编程艺术》中有深入的探讨。命令行和命令语言比起可视化接口来说,更具表达力,尤其是针对复杂的任务。另外命令行接口具有高度脚本化的能力。缺点就是需要费劲地记忆,易用性差,透明度低。当问题规模变大、程序的行为日趋单一、过程化和重复时, CLI 也常能发挥作用。

如果按照友好度和问题域的复杂度/规模两个维度来划分,可以拉出以下曲线:

友好曲线


中间会出现一个交叉点,在这个交叉点之后,命令行的简要行和表达力变得要比避免记忆负担更有价值。


《反 Mac 接口》一书中也进行了总结:可视化接口在处理小数量物体简单行为的情况下,工作的很好,但是当行为或物体的数量增加是,直接操作很快就编程机械重复的苦差…



也就是说,DSL 的形式会约束 DSL 本身的表达能力。




正如前文说的,如果‘低代码’仅仅是将原本的编码工作转换为 GUI 形式,其实并没有多大意义,因为没有抽象。


反例:JSON GUI vs JSON


JSON GUI vs JSON






正例: VSCode 案例


setting in json


setting in gui


充分利用 GUI 的优势,提供更好的目录组织、文本提示、数据录入的约束和校验。






我们可能会说 GUI 形式用户体验更好,门槛低更低,不用关心底层的细节。其实并不一定是 GUI 带来的,而是抽象后的结果。GUI 只不过是一种接口形式




回到正题,为了摆脱管理后台 CRUD 的 「西西弗斯之石」: 我们可以创建一个 DSL,这个 DSL 抽象了管理端的各种场景,将繁琐的实现细节、重复的工作封装起来,暴露简洁而优雅的用户接口(User Interface)。



💡 小结。DSL 是可视化低代码与 pro code 之间的中间中间形态,权衡了易用性/灵活性和实现成本。DSL 的形式会直接影响它的表达能力,但比形式更重要的是 DSL 对特定问题域的抽象。


我们不必重新发明一门语言,而是复用元语言的能力和生态,这基本上是零成本。











抽象过程


典型的增删改查页面:


CRUD


分析过程:



  1. 后端增删改查主要由两大组件组成: 表单表格

  2. 而表单和表格又由更原子的’字段’组成。字段的类型决定了存储类型、录入方式、和展示方式

  3. 字段有两种形态:编辑态预览态。表格列、详情页通常是预览态,而表单和表格筛选则使用编辑态。




预览态和编辑态


借鉴低代码平台的组件库/节点库,我们可以将这些‘字段’ 提取出来, 作为表单和表格的‘原子’单位, 这里我们给它取个名字,就叫原件(Atomic)吧。


低代码平台


原件将取代组件库里面的表单组件,作为我们 CRUD 页面的最小组成单位。它有且只有职责:


原件



  • 数据类型和校验。原件代表的是一种数据类型,可以是基础类型,比如数字、字符串、布尔值、枚举;也可以是基础类型上加了一些约束和交互,比如邮件、手机号码、链接;甚至可能有业务属性,比如用户,商品,订单,二维码。

  • 数据的预览。

  • 数据的录入,严格约束为 value/onChange 协议。好处是方便进行状态管理,可能保证原件实现的统一性。






接着组合原件来实现表单和表格组件,满足 CRUD 场景:


CRUD


理想状态下,我们仅需声明式地指定表格的列和原件类型,其余的技术细节应该隐藏起来。表格伪代码示例:


# 创建包含 名称、创建时间、状态三列的表格,其中可以搜索名称和创建时间
Table(
columns(
column(名称,name, queryable=true)
column(创建时间, created, data-range, queryable=true)
column(状态, status, select, options=[{label: 启用,value: 1, {label: 禁用, value: 0}}])
)
)



表单伪代码示例:


# 创建包含 名称、状态、地址的表单
Form(
item(名称,name, required=true)
item(状态,status, select, options=[{label: 启用,value: 1, {label: 禁用, value: 0}}])
item(地址, address, address)
)



如上所示,本质上,开发者就应该只关注业务数据本身,而应该忽略掉前端技术实现的噪音(比如状态管理、展示风格、分页、异常处理等等)。






表格和表单为了适应不同的需求,还会衍生出不同的展现形式:


概览图


原件 + 核心的表单/表格能力 + 场景/展示形式,一套「组合拳」下来,基本就可以满足常见的后台 CRUD 需求了。








约定大于配置


前端的在研发流程中相对下游,如果上游的产品定义,UI 设计,后端协议没有保持一致性,就会苦于应付各种混乱的差异,复用性将无从谈起。


为了最小化样板代码和沟通成本,实现开箱即用的效果。我们最好拉通上下游,将相关的规范确定下来,前端开发者应该扮演好串联的角色。




这些规范包含但不限于:



  • 页面的布局

  • UI 风格

  • 提示语

  • 验证规则

  • 数据的存储格式

  • 通用的接口(比如文件上传,导入导出)



概览图


组件库可以内置这些约定,或者提供全局的配置方式。这些规范固化后,我们就享受开箱即用的快感了。








实现示例


基于上述思想,我们开发了一套组件库(基于 Vue 和 element-ui),配合一套简洁的 DSL,来快速开发 CRUD 页面。





💡 这套组件库耦合了我们自己的约定。因此可能不适用于外部通用的场景。本文的意义更多是想启发读者,去构建适合自己的一套解决方案。



列表页定义:


表格示例


import { defineFatTable } from '@wakeadmin/components'

/**
* 表格项类型
*/

export interface Item {
id: number
name: string
createDate: number
}

export const MyTable = defineFatTable<Item>(({ column }) => {
// 可以在这里放置 Vue hooks
return () => ({
async request(params) {
/* 数据获取,自动处理异常和加载状态 */
},
// 删除操作
async remove(list, ids) {
/*列删除*/
},
// 表格列
columns: [
// queryable 标记为查询字段
column({ prop: 'name', label: '名称', queryable: true }),
column({ prop: 'createDate', valueType: 'date-range', label: '创建时间', queryable: true }),
column({
type: 'actions',
label: '操作',
actions: [{ name: '编辑' }, { name: '删除', onClick: (table, row) => table.remove(row) }],
}),
],
})
})

语法类似于 Vue defineComponent,传入一个’setup’, 。这个 setup 中可以放置一些逻辑和状态或者 Vue hooks,就和 Vue defineComponent 定义一样灵活。


返回关于表格结构的”声明”。最优的情况下,开发者只需要定义表格结构和后端接口,其余的交由组件库处理。


当然复杂的定制场景也能满足,这里可以使用 JSX,监听事件,传递组件支持的任意 props 和 slots。






表单页示例:


表单示例


import { defineFatForm } from '@wakeadmin/components'
import { ElMessageBox } from 'element-plus'

export default defineFatForm<{
// 🔴 这里的泛型变量可以定义表单数据结构
name: string
nickName: string
}>(({ item, form, consumer, group }) => {
// 🔴 这里可以放置 Vue Hooks

// 返回表单定义
return () => ({
// FatForm props 定义
initialValue: {
name: 'ivan',
nickName: '狗蛋',
},

submit: async (values) => {
await ElMessageBox.confirm('确认保存')
console.log('保存成功', values)
},

// 🔴 子节点
children: [
item({ prop: 'name', label: '账号名' }),
item({
prop: 'nickName',
label: '昵称',
}),
],
})
})


💡 和 tailwind 配合食用更香。我们假设整体的页面是符合UI规范的,细微的调整使用 tw 会很方便







全局配置:


import { provideFatConfigurable } from '@wakeadmin/components'
import { Message } from 'element-ui'

export function injectFatConfigurations() {
provideFatConfigurable({
// ...
// 统一处理 images 原件上传
aImagesProps: {
action: '/upload',
},
// 统一 date-range 原件属性
aDateRangeProps: {
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'yyyy-MM-dd',
shortcuts: [
{
text: '最近一周',
onClick(picker: any) {
picker.$emit('pick', getTime(7))
},
},
{
text: '最近一个月',
onClick(picker: any) {
picker.$emit('pick', getTime(30))
},
},
{
text: '最近三个月',
onClick(picker: any) {
picker.$emit('pick', getTime(90))
},
},
],
},
})
}





更多示例和深入讲解见这里








更多实现


前端社区有很多类似的产品,比如:



  • XRender。中后台「表单/表格/图表」开箱即用解决方案

  • Antd ProComponents。ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面

  • 百度 Amis 。 用 JSON 作为 DSL,来描述界面


读者不妨多参考参考。








总结


简单来说,我们就是从提供「毛坯房」升级到了「精装房」,精装房的设计基于我们对市场需求的充分调研和预判。目的是对于 80% 的用户场景,可以实现拎包入住,当然也允许用户在约束的范围内改装。


本文主要阐述的观点:



  • 低代码平台的高效和易用大多来源于抽象,而不一定是 GUI,GUI ≠ 低代码。

  • 摆脱「西西弗斯之石」 考验的是开发者的抽象能力,识别代码中固化/重复的逻辑。将模式提取出来,同时封装掉底层的实现细节。最终的目的是让开发者将注意力关注到业务本身,而不是技术实现细节。

  • 用声明式、精简、高度抽象 DSL 描述业务 。DSL 的形式会约束他的表达能力,我们并不一定要创建一门新的语言,最简单的是复用元语言的生态和能力。

  • 约定大于配置。设计风格、交互流程、数据存储等保持一致性,才能保证抽象收益的最大化。因此规范很重要。这需要我们和设计、产品、后端深入沟通,达成一致。

  • 沉淀原件。低代码平台的效率取决于平台提供的组件能力、数量和粒度。比如前端的组件库,亦或者流程引擎的节点,都属于原件的范畴。

  • 要求不要太高,没有万精油方案,我们期望能满足 80% 常见的场景,这已经是一个很好的成绩。至于那 20% 的个性需求,还是从毛坯房搞起吧。








扩展阅读


收起阅读 »

你的代码着色好看吗?来这里看看吧!

web
如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。 那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。 你只需要使用一个叫做 highlight.js 的第三方...
继续阅读 »

如果你想在网页上展示一些代码,你可能会遇到一个问题:代码看起来很单调,没有任何颜色或格式,这样的代码不仅不美观,也不利于阅读和理解。


那么,有没有什么办法可以让代码变得更漂亮呢?答案是有的,而且很简单。


你只需要使用一个叫做 highlight.js 的第三方库,就可以轻松实现代码着色的效果。



highlight.js 是一个非常强大和流行的库,它可以自动识别和着色超过 190 种编程语言。


它支持多种主题和样式,让你可以根据自己的喜好选择合适的配色方案。


在本文中,子辰将向你介绍如何使用 highlight.js 来为你的代码着色,以及它的基本原理和优势。


让我们开始吧!


如何使用 highlight.js


使用 highlight.js 的方法有两种:一种是通过 npm 下载并安装到你的项目中,另一种是通过 CDN 引入到你的网页中。


这里我们以 CDN 的方式为例,如果你想使用 npm 的方式,可以参考官方文档。


首先,我们需要在网页中引入 highlight.js 的 JS 文件和 CSS 文件。


JS 文件是核心文件,负责识别和着色代码,CSS 文件是样式文件,负责定义代码的颜色和格式。



我们可以从 CDN 中选择一个合适的 JS 文件和 CSS 文件。


highlight.js 提供了多个 CDN 服务商,你可以根据自己的需求选择一个,这里我们以 jsDelivr 为例。


JS 文件的链接如下:


<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js"></script>

CSS 文件的链接则需要根据你想要的主题来选择。


highlight.js 提供了很多主题,你可以在官网上预览每个主题的效果,并找到对应的 CSS 文件名,这里我们以 github-dark 为例。


CSS 文件的链接如下:


<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github-dark.min.css">

将上面两个链接分别放到网页的 head 标签中,就完成了引入 highlight.js 的步骤。


接下来,我们需要在网页中写一些代码,并用 pre 标签和 code 标签包裹起来。


pre 标签用于保留代码的格式,code 标签用于标识代码内容。例如:


<pre>
<code id="code-area">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
height: 100vh;
width: 100vw;
}
</code>
</pre>

注意,我们给 code 标签添加了一个 id 属性,方便后面通过 JS 获取它。


最后,我们需要在网页中添加一些 JS 代码,来调用 highlight.js 的方法,实现代码着色的功能。


highlight.js 提供了两个主要的方法:highlightElement 和 highlight。


这两个方法都可以实现代码着色的效果,但是适用于不同的场景。


highlightElement


highlightElement 方法适用于当你的代码是直接写在网页中的情况。


这个方法接受一个元素作为参数,并将该元素内部的文本内容进行着色处理。例如:


// 获取 code 元素
const codeEle = document.getElementById("code-area");
// 调用 highlightElement 方法,传入 code 元素
hljs.highlightElement(codeEle);

如果一切顺利,你应该能看到类似下图的效果:



代码已经被着色了,并且你可以看到代码被替换成了一个个标签,标签被加上了样式。


在最后的原理里我们在详细的说一下。


highlight


highlight 方法适用于当你的代码是通过 Ajax 请求获取到的纯文本数据的情况。


这个方法接受一个字符串作为参数,并返回一个对象,包含着色后的文本内容和代码的语言。例如:


<script>
const codeEle = document.getElementById('code-area')
// 比如说现在 code 就是 Ajax 返回的数据,lang 就是代码语言,content 就是代码内容
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
// 我们接下来可以使用 hljs.highlight,将代码内容与代码语言传入进去
const result = hljs.highlight(code.content, {
language: code.lang
})
// 它会返回一个结果,我们打印到控制台看看
console.log('result >>> ', result)
</script>


我们可以看到,打印出来的是一个对象,code 是它原始的代码,language 是它的语言,而 value 就是它着色后的代码。


那么现在要做的就是将 value 添加到 code 元素里边去。


<script>
const code = {
lang: 'css',
content: `
* {
margin: 0;
padding: 0;
}`

}
const result = hljs.highlight(code.content, {
language: code.lang
})
const codeEle = document.getElementById('code-area')
codeEle.innerHTML = result.value
</script>


我们可以看到,代码确实被着色了,但是和之前的有所差别,我们看一下是什么原因。



打开控制后我们发现,用这种方式 code 元素就没有办法被自动加上类样式了,所以说我们就需要手动给 code 加上类样式才可以。


// 通过 className 为 code 手动添加类样式,并添加类的语言
codeEle.className = `hljs language-${code.lang}`

highlight.js 的语言支持


无论使用哪种方法,都需要注意指定代码所属的语言。


如果不指定语言,highlight.js 会尝试自动识别语言,并可能出现错误或不准确的结。


指定语言可以通过两种方式:



  • 在 code 标签中添加 class 属性,并设置为 language-xxx 的形式,其中 xxx 是语言名称。

  • 在调用 highlightElement 或 highlight 方法时,在第二个参数中传入一个对象,并设置 language 属性为语言名称。



上图是 highlight.js 支持的语言,可以看到有很多种,需要用其他语言的时候,language 设置成指定的语言名称就可以了。


原理


它的原理你可能已经猜到了,在 highlightElement 里我们简单说了一下,现在再看下图:



之所以可以实现着色,其实就是查找和替换的过程,将原来的纯文本替换为元素标签包裹文本,元素是可以加上样式的,而样式就是我们引入的 css 文件。


这就是它的基本原理了。


总结


其实有时候我们去设置 Mackdown 的自定义样式呢,在代码区域设置的时候也是这样设置的,当然类样式的名字呢,基本上都是标准的格式。


好了,这个库分享介绍给你了,库的原理也为你做了简单的科普,希望对你有所帮助。


如果你有什么问题或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收

作者:子辰Web草庐
来源:juejin.cn/post/7245584147456507965
藏或分享给你的朋友!

收起阅读 »

优雅的使用位运算,省老多事了!!!

web
你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势? 位运算符号的基本了解 首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString 将数字转换...
继续阅读 »

你好,我是泰罗凹凸曼,今天我们来一篇JS中的位运算科普,经常在源码中看到的位运算符,和用其定义的一系列状态到底有什么优势?


位运算符号的基本了解


首先,我们应该要简单了解位运算符,常用的位运算符大概有以下几种,我们可以在JS中使用 toString 将数字转换为二进制查看,也可以通过 0b 开头来手动创建一个二进制数字:


(3).toString(2) // 11
0b00000011 // 3 前面的几位0可以省略,可以简写为 0b11

1. 与 &


按位对比两个二进制数,如果对应的位都为 1,则结果为 1,否则为 0


console.log((1 & 3) == 1) // true

对比图例如下所示:



2. 或 |


按位对比两个二进制数,如果对应的位有一个 1,则结果为 1,否则为 0


console.log((1 | 3) == 3) // true

对比图例如下所示:



3. 异或 ^


按位对比两个二进制数,如果对应的位有且只有一个 1,则结果为 1,否则为 0


console.log((1 ^ 3) == 2) // true

对比图例如下所示:



4. 非 ~


按位对操作的二进制数取反,即 1 变 0,0 变 1,任何数的非运算符计算结果都是 -(x + 1)


const a = -1 // ~a = -(-1 + 1) = 0
console.log(~a) // 0
const b = 5 // ~b = -(5 + 1) = -6
console.log(~b) // -6

一个数和它的取反数相加的结果总为 -1


5. 左移 <<


左移会将二进制值的有效位数全部左移指定位数,被移出的高位(最左边的数字)丢弃,但符号会保留,低位(最右边的数字)会自动补0


console.log(1 << 2) // 4

图例如下所示:



6. 右移 >>


和左移相反的操作,将二进制的操作数右移指定位数,高位补0,低位丢弃!


console.log(4 >> 2) // 1

参考资料均来自 MDN,除了这些常用的符号之外,文档还标注了所有的JS操作符号,感兴趣的同学可以看一看!


有什么用?


说了这么多符号,对于操作符的影响是加深了,但是有什么用呢?二进制数字难理解,位操作符也难理解,二进制和十进制的互转不写个代码都心算不了,相信各位同学肯定有如此费解,我们先来看一段 Vue 的源代码,其中定义了很多状态类的字段!


源码位置戳这里



以及 Vue 中对其的使用,源码位置戳这里



我们可以看到,Vue 定义了一系列状态列标识一个 Dom 是属于什么类型,并用 VNode 中的一个字段 shapeFlag 来完成存储和判断,对状态的存储只用到了一个字段一个数字,就可以进行多种状态的判断!


我们尝试着设计一种类似的判断结构出来如何?


我有N个权限


假设系统中的用户我们规定其有增删改查四个权限,我们可以设计一个枚举类来标识拥有的四个权限:


enum UserPerm {
CREATE = 1 << 0,
DELETE = 1 << 1,
UPDATE = 1 << 2,
SELECT = 1 << 3,
}

我们设计的时候,完全不必在意上述的二进制的十进制值是什么,只需要清楚的是,上述枚举的 1 在二进制位的哪个位置,如 1 的 二进制为 00000001,将其左移 1(1 << 1), 就变成了 00000010, 依次类推,我们用一个二进制串中的每一位来标识一个权限,这样一个字符串中只要出现对应位置的 1, 则该用户就拥有对应位置的权限,如图:



有什么好处呢?


我们知道二进制是可以转换为十进制的,这样子我们就可以用一个数字来表示多个权限,如一个用户完整的拥有四个权限,那他的二进制为 0b1111, 那么其状态为数字 15


如果一个用户只有 CREATESELECT 的权限,那么二进制表达为 0b1001,十进制数字为 9


后端数据库中,前端用户信息中,接口返回都只有一列一个字段就可以表示,那么用户信息应该是下面的形式:


const userInfo = {
name: '泰罗凹凸曼',
phone: '15888888888',
perm: 9, // 代表其只有 CREATE 和 SELECT 两种权限
}

权限的判断


如何判断这个用户是否具备某一个权限呢?那就需要请出我们的 与运算符(&),参考 Vue 的做法:


console.log(userInfo.perm & UserPerm.CREATE) // 9 & (1 << 0) = 1

console.log(userInfo.perm & UserPerm.UPDATE) // 返回 0, 0代表不通过

如果 userInfo.perm 中包含 CREATE,就会返回 CREATE 的值,否则返回 0,在JS中,任何非0的数字都可以通过 if 判断,所以我们只需要一个判断就足够了!


if (userInfo.perm & UserPerm.CREATE) {
console.log('有创建权限')
} else {
console.log('没有创建权限')
}

什么原理?我们之前给过与运算符的图例,接下来我们看一下如上两句代码的图例所示:



我们看到,上下的符号位如果对不上的话,返回的结果都是 0,这样子我们就轻松实现了权限的判断


权限的增删


那么我们如何实现对一个用户的权限更新呢,比如给上面的用户新增一个 UPDATE 权限,这个时候我们就需要 或运算符(|)


比如:


userInfo.perm | UserPerm.UPDATE // 1001 | 0100 = 1101 = 13

这样子我们就对一个用户权限进行了增加,或的规则我们上面也给过图例,这里大家可以自己尝试理解一下,无非是两个二进制数 10010100 之间的或运算,只有其中一位为 1 则为 1,这两个数字计算的结果自然是 1101


那么如何实现权限删除呢?异或运算符(^)给你答案!有且只有一个 1,返回 1,否则为 0,删除对我们刚刚添加的 UPDATE 权限的方法:


userInfo.perm ^ UserPerm.UPDATE // 1101 ^ 0100 = 1001

非常简单是吧?看到这里,相信你已经完全理解位运算符在权限系统的妙用了,如果我这个时候需要添加一个新的权限,如分享权限,那么我只有用第五位的1来表示这个权限就可以啦


enum UserPerm {
SHARE = 1 << 5
}

// 添加分享权限
userInfo.perm | UserPerm.SHARE

以前的方案


我们以前在做用户标识的时候,通常会定义一个数组来表示,然后执行数组判断来进行权限的判断


const userPerm = ['CREATE', 'UPDATE', 'DELETE', 'SELECT']

// 判断有无权限
if (userPerm.includes('CREATE')) {
// ...
}

// 增加权限
user.perm.push('UPDATE')

// 删除权限
user.perm.splice(user.perm.indexOf('UPDATE'), 1)

相信大家也可以看出来,无论是从内存占用,效率,便捷程度来说位运算符的形式都是完胜,这也是会被各大项目使用的原因之一!快去你的项目中实践吧,记得写好注释哦!


结语


今天带大家认识了位运算符在权限系统的妙用,小伙伴们还有什么使用位运算符的巧妙思路,可以在评论中给出来哦!继续加油吧,快去实践少年!


祝大家越来越牛逼!


去探索,不知道的东西还多着呢,我是泰罗凹凸曼,M78星云最爱写代码的,我们下一篇再会!


作者:泰罗凹凸曼
来源:juejin.cn/post/7244809939838844984
收起阅读 »

js的垃圾回收机制

web
概论 对于js的垃圾回收,很多人的理解还停留在引用计数和标记清除的阶段。 有人会说,学习这个,对业务代码开发没啥作用。但是我想说,了解了这些基础的东西之后,才能更好地组织代码,在写代码的时候,才能做到心中能有个框架,知道浏览器到底发生了什么。 我也不是科班出身...
继续阅读 »

概论


对于js的垃圾回收,很多人的理解还停留在引用计数和标记清除的阶段。


有人会说,学习这个,对业务代码开发没啥作用。但是我想说,了解了这些基础的东西之后,才能更好地组织代码,在写代码的时候,才能做到心中能有个框架,知道浏览器到底发生了什么。


我也不是科班出身,很多东西不清不楚的。但我感觉计算机行业有个很好的地方,就是学了的知识很快就能得到验证,就能在生产上应用。这种成就感是我当年干机械的时候所无法体验到的。


前几天就有个报障说有个项目越用越卡,但是排查不出问题,我最近正好在学习垃圾回收内存泄漏,就立马能分析出来是不是内存不足产生的影响,就很开心。


本文我会采用图解的方式,尽量照着js垃圾回收的演变历史讲解。


一,什么是垃圾回收


GCGarbage Collection,也就是我们常说的垃圾回收。


我们知道,js是v8引擎编译执行的,而代码的执行就需要内存的参与,内存往往是有限的,为了更好地利用内存资源,就需要把没用的内存回收,以便重新使用。


比如V8引擎在执行代码的过程中遇到了一个函数,那么我们会创建一个函数执行上下文环境并添加到调用栈顶部,函数的作用域里面包含了函数中所有的变量信息,在执行过程中我们分配内存创建这些变量,当函数执行完毕后函数作用域会被销毁,那么这个作用域包含的变量也就失去了作用,而销毁它们回收内存的过程,就叫做垃圾回收。


如下代码:


var testObj1={
a:1
}
testObj1={
b:2
}

对应的内存情况如下:


1,垃圾的产生.drawio.png
其中堆中的{a:1}就变成了垃圾,需要被GC回收掉。


在C / C++中,需要开发者跟踪内存的使用和管理内存。而js等高级语言,代码在执行期间,是V8引擎在为我们执行垃圾回收。


那么既然已经有v8引擎自动给我们回收垃圾了,为啥我们还需要了解V8引擎的垃圾回收机制呢?这是因为依据这个机制,还有些内存无法回收,会造成内存泄漏。具体的表现就是随着项目运行时间的变成,系统越来越卡滞,需要手动刷新浏览器才能恢复。


了解V8的垃圾回收机制,才能让我们更好地书写代码,规避不必要的内存泄漏。


二,内存的生命周期


如上所说,内存应该存在这样三个生命周期:




  1. 分配所需要的内存:在js代码执行的时候,基本数据类型存储在栈空间,而引用数据类型存储在堆空间。




  2. 使用分配的空间:可能对对应的值做一些修改。




  3. 不需要时将其释放回收。


    如下代码:


    function fn(){
    //创建对象,分配空间
    var testObj={
    a:1
    }
    //修改内容
    testObj.a=2
    }
    fn()//调用栈执行完毕,垃圾回收

    对应的内存示意图:




2,内存的生命周期.drawio.png


三,垃圾回收的策略


当函数执行完毕,js引擎是通过移动ESP(ESP:记录当前执行状态的指针)来销毁函数保存在栈当中的执行上下文的,栈顶的空间会被自动回收,不需要V8引擎的垃圾回收机制出面。


然而,堆内存的大小是不固定的,那堆内存中的数据是如何回收的呢?


这就引出了垃圾回收的策略。


通常用采用的垃圾回收有两种方法:引用计数(reference counting)标记清除(mark and sweep)


3.1,引用计数(reference counting)


如上文第二节中所说,testObj对象存放在堆空间,我们想要使用的时候,都是通过指针来访问,那么是不是只要没有额外的指针指向它,就可以判定为它不再被使用呢?


基于这个想法,人们想出了引用计数的算法。


它工作原理是跟踪每个对象被引用的次数,当对象的引用次数变为 0 时,则判定该对象为无用对象, 可以被垃圾回收机制进行回收。


    function fn(){
//创建对象,分配空间
var testObj1={
a:1
}//引用数:1
var testObj2=testObj1//引用数:2
var testObj3=testObj1//引用数:3
var testObj4={
b:testObj1
}//引用数:4
testObj1=null//引用数:3
testObj2=null//引用数:2
testObj3=null//引用数:1
testObj4=null//引用数:1
}
fn()//调用栈执行完毕,垃圾回收

如上代码,引用次数变成0后,堆内存中的对应内存就会被GC。


如下图,当testObj1-4都变成null后,原来的testObj4引用数变成0,而{a:1}这时候的引用数还为1(有一个箭头指向它),而{b:1002}被回收后,它的引用数就变成0,故而最后也被垃圾回收。


3,引用计数的计数数量.drawio.png


引用计数的优点:


引用计数看起来很简单,v8引擎只需要关注计数器即可,一旦对象的引用数变成0,就立即回收。

但是很明显的,引用计数存在两个缺点:


1,每个对象都需要维护一个计数器去记录它的引用数量。
2,如果存在相互循环引用的对象,因为各自的引用数量无法变成0(除非手动改变),因而无法被垃圾回收。

对于第二点,如下代码:


function fn(){
//创建对象,分配空间
var testObj1={
a:testObj2
}
var testObj2={
b:testObj1
}
}
fn()

当fn执行完毕后的内存情况如下,因为两个对象相互引用,导致引用数到不了0,就无法被GC:


4.循环引用.drawio.png


因为引用计数的弊端,后续的浏览器开始寻找新的垃圾回收机制,从2012年起,所有现代浏览器都使用了标记清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记清除算法的改进。


3.2,标记清除(mark and sweep)


标记清除是另一种常见的垃圾回收机制。


它工作原理是找出所有活动对象并标记它们,然后清除所有未被标记的对象


其实现步骤如下:



  1. 根节点:垃圾回收机制的起点是一组称为根的对象(有很多根对象),根通常是引擎内部全局变量的引用,或者是一组预定义的变量名,例如浏览器环境中的 Window 对象和 Document 对象。

  2. 遍历标记:从根开始遍历引用的对象,将其标记为活动对象。每个活动对象的所有引用也必须被遍历并标记为活动对象。

  3. 清除:垃圾回收器会清除所有未标记的对象,并使空间可用于后续使用。


因为能从根节点开始被遍历到的(有被使用到的),就是有用的活动对象,而剩余不能被链接到的则是无用的垃圾,需要被清除。


对于前文引用计数中循环引用的例子,就因为从根对象触发,无法遍历到堆空间中的那两个循环引用的对象,就会把它判定为垃圾对象,从而回收。


如下代码:


var obj1={
a:{
b:{
c:3
}
}
}
var obj2={
d:1
}
obj2=null

如下图,从根节点无法遍历到obj2了,就会把d垃圾回收。


5,标记清除.png


按照这个思路,标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,会导致空闲内存空间是不连续的,出现了 内存碎片(如下图),并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题:当新对象需要空间存储时,需要遍历空间以找到能够容纳对象大小size的区域:


6,标记清除新增对象.png


这样效率比较低,因而又有了标记整理(Mark-Compact)算法 ,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会先将活动对象向内存的一端移动,然后再回收未标记的垃圾内存:


7,标记整理算法.png


四,V8引擎的分代回收


如上文所说,在每次垃圾回收时都要检查内存中所有的对象,这样的话对于一些占用空间大、存活时间长的对象,要是和占用空间小、存活时间短的对象一起检查,那不是平白浪费很多不必要的检查资源嘛。


因为前者需要时间长并且不需要频繁进行清理,后者恰好相反,那怎么优化呢?


类似于信誉分,信誉分高的,检查力度就应该小一些嘛。把信誉分抽象一下,其实说的就是分层级管理,于是就有了弱分代假设。


4.1,弱分代假设(The Weak Generational Hypothesis)



  1. 多数对象的生命周期短

  2. 生命周期长的对象,一般是常驻对象


V8的GC也是基于假设将对象分为两代: 新生代和老生代。


对不同的分代执行不同的算法可以更有效的执行垃圾回收。


V8 的垃圾回收策略主要基于分代式垃圾回收机制,将堆内存分为新生代和老生代两区域,采用不同的策略来管理垃圾回收。


他们的内存大小如下:


64位操作系统32位操作系统
V8内存大小1.3G(1432MB)0.7g(716MB)
新生代空间32MB16MB
老生代空间1400MB700MB

4.2,新生代的垃圾回收策略


新生代对象是通过一个名为 Scavenge 的算法进行垃圾回收。


在JavaScript中,任何对象的声明分配到的内存,将会先被放置在新生代中,而因为大部分对象在内存中存活的周期很短,所以需要一个效率非常高的算法。Scavenge算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。


Scavenge 算法中将新生代内存一分为二,Semi space FromSemi space To,新生区通常只支持 1~8M 的容量。这块区域使用副垃圾回收器来回收垃圾。


工作方式也很简单:


1,等From空间满了以后,垃圾回收器就会把活跃对象打上标记。
2,把From空间已经被标记的活动对象复制到To空间。
3,将From空间的所有对象垃圾回收。
4,两个空间交换,To空间变成From空间,From变成To空间。以此往复。

而判断是否是活跃对象的方法,还是利用的上文说的从根节点遍历,满足可达性则是活跃对象。


具体流程如下图所示,假设有蓝色指针指向的是From空间,没有蓝色指针指向的是To空间:


8,新生代的垃圾回收策略.drawio.png


从上图可以明显地看到,这种方式解决了上文垃圾回收后内存碎片不连续的问题,相当于是利用空间换时间。


现在新生代空间的垃圾回收策略已经了解,那新生代空间中的对象又如何进入老生代空间呢?


4.3,新生代空间对象晋升老生代空间的条件


1,复制某个对象进入to区域时,如果发现内存占用超过to区域的25%,则将其晋升老生代空间。(因为互换空间后要留足够大的区域给新创建对象)
2,经过两次fromto互换后,还存活的对象,下次复制进to区域前,直接晋升老生代空间。

4.4,老生代空间的垃圾回收策略


老生代空间最初的回收策略很简单,这在我们上文也讲过,就是标记整理算法。


1,先根据可达性,给所有的老生代空间中的活动对象打上标记。
2,将活动对象向内存的一端移动,然后再回收未标记的垃圾内存。

这样看起来已经很完美了,但是我们知道js是个单线程的语言,就目前而言,我们的垃圾回收还是全停顿标记:js是运行在主线程上的,一旦垃圾回收生效,js脚本就会暂停执行,等到垃圾回收完成,再继续执行


这样很容易造成页面无响应的情况,尤其是在多对象、大对象、引用层级过深的情况下。


于是在这基础上,又有了增量标记的优化。


五,V8优化


5.1,增量标记


前文所说,我们给老生代空间中的所有对象打上活动对象的标记,是从一组根节点出发,根据可达性遍历而得。这就是全量地遍历,一次性完成,


但因为js是单线程,为了避免标记导致主线程卡滞。于是人们想出来和分片一样的思路:主线程每次遍历一部分,就去干其他活,然后再接着遍历。如下图:


9,增量标记.png


增量标记就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记,这样就算页面卡滞,因为时间很短,使用者也感受不到,体验就好了很多。


但是这又引发了一个新的问题:每次遍历一部分节点就停下来,下次继续怎么识别到停顿点,然后继续遍历呢?


V8又引入了三色标记法。


5.2,三色标记法


首先要明确初心:三色标记法要解决的问题是遍历节点的暂停与恢复。


使用两个标志位编码三种颜色:白色(00),灰色(10)和黑色(11)。


白色:指的是未被标记的对象,可以回收


灰色:指自身被标记,成员变量(该对象的引用对象)未被标记,即遍历到它了,但是它的下线还没遍历。不可回收


黑色:自身和成员变量都被标记了,是活动对象。不可回收


1,从已知对象开始,即roots(全局对象和激活函数), 将所有非root对象标记置为白色
2,将root对象变黑,同时将root的直接引用对象abc标记为灰色
3,将abc标记为黑色,同时将它们的直接引用对象标记为灰色
4,直到没有可标记灰色的对象时,开始回收所有白色的对象

10,三色标记法.drawio.png


如上图所示,如果第一次增量标记只标记到(2),下次开始时,只要找到灰色节点,继续遍历标记即可。


而遍历标记完成的标志就是内存中不再有灰色的。于是这时候就可以把白色的垃圾回收掉。


那这样就解决了遍历节点的暂停与恢复问题,同时支持增量标记。


(ps:其实这里我有个疑惑,暂停后重新开始的时候,不也要遍历寻找灰色节点嘛,每次恢复都要遍历找灰色节点,不是也耗时嘛?)


5.3,写屏障


按照上文对标记的描述,其实有一个前提条件:在标记期间,代码运行不会变更对象的引用情况。


比如说我采用的是增量标记,前脚刚做好的标记,后脚就被js脚本修改了引用关系,那不是会导致标记结果不可信嘛?如下图:


11,写屏障.drawio.png


就像上图一样,D已经被判定成垃圾了,但是下一个分片的js又引用了它,这时候如果删除,必然不对,所以V8 增量使用 写屏障 (Write-barrier) 机制,即一旦有黑色对象引用白色对象,该机制会强制将引用的白色对象改为灰色,从而保证下一次增量 GC 标记阶段可以正确标记,这个机制也被称作 强三色不变性


那在我们上图的例子中,将对象 B 的指向由对象 C 改为对象 D 后,白色对象 D 会被强制改为灰色。


这样一来,就不会将D判定为垃圾 ,并且图中新增的垃圾C在本轮垃圾回收中也不会回收,而是在下一轮回收了。


5.4,惰性清理


上文的增量标记和三色标记法以及写屏障只是对标记方式的优化。目的是采用分片的思想将标记的流程碎片化。


而清理阶段同样可以利用这个思想。


V8的懒性清理,也称为惰性清理(Lazy Sweeping),是一种垃圾回收机制,用于延迟清理未标记对象所占用的内存空间,以减少垃圾回收期间的停顿时间。


当增量标记结束后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,于是可以将清理的过程延迟一下,让JavaScript逻辑代码先执行;也无需一次性清理完所有非活动对象内存,垃圾回收器可以按需逐一进行清理,直到所有的页都清理完毕。


六,垃圾回收总结


6.1,初始的垃圾回收策略:从引用计数到标记清除


对于js的垃圾回收,最开始的时候,是采用引用计数的算法,但是因为引用计数存在循环引用导致垃圾无法清除,于是又引入了标记清除算法,而标记清除算法存在碎片空间问题,于是又优化成标记整理算法。


随着技术的发展,v8引擎的垃圾回收机制也在不断完善。


6.2,弱分代假设,划分新老生代空间采用不同策略


第一次完善是采用弱分代假设,为了让内存占用大、存活时间长的对象减少遍历,采用分代模型,分成了新分代和老分代空间,垃圾回收采取不同的策略。


新生代空间以空间换时间,拆分成from和to空间互换位置,解决垃圾回收后内存不连续的问题。


将满足条件的对象晋升到老生代空间。而老生代空间采用标记整理算法。


6.3,从全停顿到引入分片思想


因为js是单线程,如果垃圾回收耗时过长,就会阻塞页面响应。


为了解决标记阶段的全停顿问题,引入了增量标记算法。但是非黑即白的标记算法在下一次重新开始标记时无法找到上次的中断点,所以使用三色标记法。此外,为了避免增量标记过程中js脚本变更引用关系,v8又增加了写屏障。


同样的,为了解决清理阶段的全停顿问题,引入了惰性清理。


七,本系列其他文章


最近在整理js基础,下面是已经完成的文章:


js从编译到执行过程 - 掘金 (juejin.cn)


从异步到promise - 掘金 (juejin.cn)


从promise到await - 掘金 (juejin.cn)


浅谈异步编程中错误的捕获 - 掘金 (juejin.cn)


作用域和作用域链 - 掘金 (juejin.cn)


原型链和原型对象 - 掘金 (juejin.cn)


this的指向原理浅谈 - 掘金 (juejin.cn)


js的函数传参之值传递 - 掘金 (juejin.cn)


js的事件循环机制 - 掘金 (juejin.cn)


从作用域链和内存角度重新理解闭包 - 掘金 (juejin.cn)


八,本文参考文章:


「硬核JS」你真的了解垃圾回收机制吗 - 掘金 (juejin.cn)


一文带你快速掌握V8的垃圾回收机制 - 掘金 (juejin.cn)


[深入浅出]JavaScript GC 垃圾回收机制 - 掘金 (juej

in.cn)

收起阅读 »

什么,产品让我实现自动播放?

web
前言 最近,在逛一些技术群时,看到有人在吐槽,这个video媒体标签设置autoplay属性怎么不生效。不生效就算了,为什么我在dom渲染完成时去获取video元素(假设获取到的元素为el),然后执行el.paly()也不生效,why???? 那为什么我通过控...
继续阅读 »

前言


最近,在逛一些技术群时,看到有人在吐槽,这个video媒体标签设置autoplay属性怎么不生效。不生效就算了,为什么我在dom渲染完成时去获取video元素(假设获取到的元素为el),然后执行el.paly()也不生效,why????


那为什么我通过控制台去执行这两行代码的时候,它又生效了!!!!???


带着满脸疑惑,我们来了解一下浏览器的自动播放策略。


1、浏览器的自动播放策略


以谷歌浏览器为例:


在某些特定的情况下,浏览器是允许自动播放的:




  1. 静音状态下始终允许自动播放




  2. 有声音自动播放时:



    • 用户进行了页面点击等与界面发生交互行为后。

    • 达到媒体参与指数, 也就是用户之前在本站播放过有声音的视频。

    • 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA




  3. 主站可以将自动播放权限委托给它们的 iframe,以允许自动播放声音。




2、静音播放。


在静音状态下,浏览器是允许自动播放的,代码如下:


<video controls class="videoItem" width="100%" autoplay muted loop="loop" src="./tets.mp4"></video>

属性说明:


muted: 是否静音播放,默认为false


autoplay:是否自动播放,默认为false


control:控制器是否显示,默认为false(可不写)。


loop:是否循环播放,默认为false(可不写)。


唉,好像可以自动播放了,于是我拿去应付产品,产品给我泼了一盆冷水,说能让它播放起来有声音吗?

于是,又开始了我们的与非静音状态自动播放功能的探索。


3、非静音自动播放


我们在上面有了解到非静音自动播放有四种情况:



  1. 用户进行了页面点击等与界面发生交互行为后。

  2. 达到媒体参与指数MEI.

  3. 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA

  4. 主站可以将自动播放权限委托给它们的 iframe。


3.1 用户进行了页面交互行为


这个就可以理解成用户触碰了页面之后,我们就可以进行有声音的自动播放了。看代码


HTML部分:


<video controls class="videoItem" width="100%"  loop="loop" src="./tets.mp4"></video>

JS部分:


const vdo = document.querySelector('video')
// 播放函数
async function playAudio() {
const res = await vdo.play()
}
// 监听用户点击
document.addEventListener('click',playAudio)
// 监听媒体播放
vdo.addEventListener('play', function () { //播放开始执行的函数
console.log("开始播放");
// 移除点击监听事件
document.removeEventListener("click",playAudio)
})

这里我们就有很大的发挥空间了,比如说视频不完全在可见区,或者用户看视频前给他弹个框?


3.2 达到媒体参与指数MEI


MEI是浏览器根据我们对一些网站的浏览行为打分,越高,就表示我们喜欢观看这个网站的视频,可以通过about://media-engagement来查询,不可更改。
注意:该策略只对PC端浏览器有效。


3.3 用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA


用户将页面添加到移动设备的主屏幕上或者在PC上安装了PWA,这个我们基本可以不用去考虑了,在国内是比较少见的,感兴趣的可以去百度。


3.4 主站可以将自动播放权限委托给它们的 iframe。


这个就是通过iframe来控制媒体的自动播放。代码如下:


    <!-- 允许自动播放 -->
<iframe src="跨源地址" allow="autoplay">

<!-- 允许自动播放和设置全屏 -->
<iframe src="跨源地址" allow="autoplay; fullscreen">

需要注意的是,我们可能需要做一些本地代理,不然可能会出现跨域问题。


4 补充


上面还有一点是没有提到的,就是同源下点击页面跳转后可实现自动播放。


比如:我在http://www.bilibili.com页面点击一个视频跳转到了一个新页面,就可以实现自动播放(注意:不能打开新窗口,否则非

作者:清_秋
来源:juejin.cn/post/7244818202214416443
静音自动播放将失效)

收起阅读 »

面试:(简单粗暴点)百度一面,直接问痛我

web
前言 这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。 百度一面 1. 如何用chatgpt提...
继续阅读 »

前言


这次的百度面试挺紧张的,在写算法题的时候脑子都有点空白,还是按照脑海中那点残存的算法技巧才写出来,不至于太尴尬,以及第一次面试百度这种级别的公司,难免出现了一些平常不至于出现的问题或没注意的缺点,在这里分享给大家。


百度一面


1. 如何用chatgpt提升前端开发效率



因为我嘴贱,平时习惯了使用chatgpt,然后自我介绍说了一句,由于之前面得公司都没问过,导致我没怎么往这方面准备,以至于答得时候牛头不对马嘴,所以说不愧是大厂啊。




  1. 问题解答和指导ChatGPT可以帮助回答与前端开发相关的问题。当你在编写代码的时候,当一时忘记了某个API怎么用,就可以向ChatGPT提问,并获得解答和指导,甚至还会给出一些更加深入且性能更好的应用。这可以帮助更快地解决问题和理解前端开发中的概念。

  2. 代码片段和示例ChatGPT可以帮助你生成常见的前端代码片段和示例。你可以描述你想要实现的功能或解决的问题,然后向ChatGPT请求相关代码片段。这样,您可以更快地获得一些基础代码,从而加快开发速度。

  3. 自动生成文档ChatGPT可以帮助你生成前端代码的文档。你可以描述一个函数、组件或类,并向ChatGPT请求生成相关的文档注释。这可以帮助您更轻松地为你的代码添加文档,提高代码的可读性和可维护性。

  4. 问题排查和调试:在开发过程中,您可能会遇到问题或错误。您可以向ChatGPT描述您遇到的问题,或者直接把代码交给它,并请求帮助进行排查和调试。ChatGPT可以提供一些建议和指导,帮助您更快地找到问题的根本原因并解决它们。

  5. 学习资源和最新信息ChatGPT可以为你提供关于前端开发的学习资源和最新信息。你可以向ChatGPT询问关于前端开发的最佳实践、最新的框架或库、前端设计原则等方面的问题。这可以帮助我们不断学习和更新自己的前端开发知识,从而提高效率。


2. [1, 2, 3, 4, 5, 6, 7, 8, 9] => [[1, 2, 3],[4, 5, 6],[7, 8, 9]],把一个一维数组变成三个三个的二维数组


在JavaScript中,可以使用数组的slice方法和一个循环来将一个一维数组转换为一个二维数组。下面是一个示例代码:


    function convertTo2DArray(arr, chunkSize) {
var result = [];
for (var i = 0; i < arr.length; i += chunkSize) {
result.push(arr.slice(i, i + chunkSize));
}
return result;
}

var inputArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var outputArray = convertTo2DArray(inputArray, 3);

console.log(outputArray);

输出结果将是:


    [[1, 2, 3], [4, 5, 6], [7, 8, 9]]


slice 不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组,不信的话自己可以编译一下。



这段代码中的convertTo2DArray函数接受两个参数:arr表示输入的一维数组,chunkSize表示每个子数组的大小。它使用slice方法来从输入数组中提取每个子数组,并使用循环来遍历整个数组并构建输出二维数组。最后,它返回生成的二维数组。


3. 输出结果,为什么?


    const obj3 = {a: 1};
const obj4 = {b: 2};
console.log(obj3 == obj4); // false
console.log(obj3 === obj4); // false

结果:


false,false


原因:


在这段代码中,obj3obj4分别是两个独立的对象,它们开辟的堆内存地址是完全不一样。==运算符用于比较两个操作数是否相等,而===运算符用于比较两个操作数是否严格相等。


根据对象的比较规则,当使用==运算符比较两个对象时,它们将会进行类型转换后再进行比较。由于obj3obj4是不同的对象,即使它们的属性值相同,它们的引用也不同,因此在进行类型转换后,它们会被视为不相等的对象。因此,console.log(obj3 == obj4);的输出结果将会是false


而在使用===运算符比较两个对象时,不会进行类型转换,而是直接比较两个操作数的值和类型是否完全相同。由于obj3obj4是不同的对象,且类型也不同,即使它们的属性值相同,它们也不会被视为严格相等的对象。因此,console.log(obj3 === obj4);的输出结果同样会是false



总结起来,无论是使用==运算符还是===运算符,obj3obj4都不会被视为相等或严格相等的对象,因为它们是不同的对象。



4. this有关 输出结果,为什么?


    const obj1 = {
fn: () => {
  return this
}
}
const obj2 = {
fn: function(){
  return this
}
}

console.log(obj1.fn());
console.log(obj2.fn());

输出结果:



  1. window || undefined

  2. obj2


原因是:


在箭头函数 fn 中的 this 关键字指向的是定义该函数的上下文,而不是调用该函数的对象。因此,当 obj1.fn() 被调用时,由于箭头函数没有它自己的this,当你调用fn()函数时,this指向会向上寻找,因此箭头函数中的 this 指向的是全局对象(在浏览器环境下通常是 window 对象),因此返回的是 undefined


而在普通函数 fn 中的 this 关键字指向的是调用该函数的对象。在 obj2.fn() 中,函数 fn 是作为 obj2 的方法被调用的,所以其中的 this 指向的是 obj2 对象本身,因此返回的是 obj2


需要注意的是,在严格模式下,普通函数中的 this 也会变为 undefined,因此即使是 obj2.fn() 也会返回 undefined。但在示例中没有明确指定使用严格模式,所以默认情况下运行在非严格模式下。


5. Promise有关输出结果,为什么?


    console.log('1');
function promiseFn() {
return new Promise((resolve, reject) => {
  setTimeout(()=> {
    console.log('2');
  })
  resolve('3');
  console.log('4')
})
}

promiseFn().then(res => {
console.log(res);
});

输出结果: 1 4 3 2


原因是:



  1. 首先,代码从上往下执行,把console.log('1')放入同步任务

  2. 再调用promiseFn(),因为new Promise是同步任务,所以放入同步任务,继续执行

  3. 遇到setTimout这个宏任务,放入宏任务队列中

  4. 遇到resolve('3'),把res返回

  5. 之后再执行.then(),因为promise.then是微任务,所以放入微任务队列

  6. 代码是先执行同步任务,再执行微任务,之后再是宏任务

  7. 所以输出结果为1 4 3 2



这里涉及到了EventLoop的执行机制,如果不是太清楚可以看看我的面试题:小男孩毕业之初次面试第二家公司第一题



6. 实现斐波那契的第N个值(从0开始),要求时间复杂度为O(n)



首先,说到斐波那契第一个想到的肯定是如下的算法,但这可是百度啊,如果只是这种程度的话如何能和同样面相同岗位的人竞争呢,所以我们得想到如下算法有什么缺点,然后如何优化



function fib(n) {
if (n == 0 || n === 1) return 1;
return fib(n - 1) + fib(n - 2);
};

console.log(fib(3)); // 5
console.log(fib(5)); // 8

单纯的使用递归看似没什么问题,也能运算出结果,但是里面有个致命的问题,首先,时间复杂度就不对,递归思想的复杂度为 O(2^n) ,它不为O(n),然后还有会重复计算,比如计算n=3时,会计算fib(1) + fib(2),再次计算fib(4)时,会先算fib(3) = fib(1) + fib(2),然后再计算fib(4) = fib(1) + fib(2) + fib(3),在这里,fib(1)和fib(2)重复计算了两次,对于性能损耗极大。此时的你如果对动态规划敏感的话,就会从中想到动态规划其中最关键的特征——重叠子问题



因此,使用动态规划来规避重复计算问题,算是比较容易想到较优的一种解法,并且向面试官展现了你算法能力中有动态规划的思想,对于在面试中的你加分是极大的。



以下是动态规划思路的算法,状态转移方程为dp[i] = dp[i-1] + dp[i-2]


function fibonacci(n) { 
if (n <= 1) return n;
let fib = [0, 1]; // 保存斐波那契数列的结果
for (let i = 2; i <= n; i++) {
fib[i] = fib[i - 1] + fib[i - 2]; // 计算第i个斐波那契数
}
return fib[n];
}



当然,你可能会说,在面试中怎么可能一下子就能想到动态规划,所以在面试前你需要背一背相关的状态转移方程,当你对算法问题分析到一定程度时,就能够记忆起这些状态转移方程,提高你写算法的速度。



在面试中,动态规划的常用状态转移方程可以根据问题的具体情况有所不同。以下是几个常见的动态规划问题和它们对应的状态转移方程示例:




  1. 斐波那契数列(Fibonacci Sequence):



    • dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示第 i 个斐波那契数。




  2. 爬楼梯问题(Climbing Stairs):



    • dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示爬到第 i 级楼梯的方法数。




  3. 背包问题(Knapsack Problem):



    • dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),其中 dp[i][j] 表示在前 i 个物品中选择总重量不超过 j 的最大价值,weight[i] 表示第 i 个物品的重量,value[i] 表示第 i 个物品的价值。




  4. 最长递增子序列(Longest Increasing Subsequence):



    • dp[i] = max(dp[j] + 1, dp[i]),其中 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度,j0i-1 的索引,且 nums[i] > nums[j]




  5. 最大子数组和(Maximum Subarray Sum):



    • dp[i] = max(nums[i], nums[i] + dp[i-1]),其中 dp[i] 表示以第 i 个元素结尾的最大子数组和。




  6. 最长公共子序列(Longest Common Subsequence):




    • 如果 str1[i] 等于 str2[j],则 dp[i][j] = dp[i-1][j-1] + 1




    • 否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1]),其中 dp[i][j] 表示 str1 的前 i 个字符和 str2 的前 j 个字符的最长公共子序列的长度。






  7. 编辑距离(Edit Distance):




    • 如果 word1[i] 等于 word2[j],则 dp[i][j] = dp[i-1][j-1]




    • 否则,dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。






  8. 打家劫舍(House Robber):



    • dp[i] = max(dp[i-1], dp[i-2] + nums[i]),其中 dp[i] 表示前 i 个房屋能够获得的最大金额,nums[i] 表示第 i 个房屋中的金额。




  9. 最大正方形(Maximal Square):




    • 如果 matrix[i][j] 等于 1,则 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1




    • 否则,dp[i][j] = 0,其中 dp[i][j] 表示以 matrix[i][j] 为右下角的最大正方形的边长。






7. 手写EventBus


当需要手动实现一个简单的 EventBus 时,你可以创建一个全局的事件总线对象,并在该对象上定义事件的订阅和发布方法。


class EventBus {
constructor() {
this.events = {}; // 存储事件及其对应的回调函数列表
}

// 订阅事件
subscribe(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}

// 发布事件
publish(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}

// 取消订阅事件
unsubscribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}
}

使用上述 EventBus 类,你可以执行以下操作:


// 创建全局事件总线对象
const eventBus = new EventBus();

const callback1 = data => {
console.log('Callback 1:', data);
};

const callback2 = data => {
console.log('Callback 2:', data);
};

// 订阅事件
eventBus.subscribe('event1', callback1);
eventBus.subscribe('event1', callback2);

// 发布事件
eventBus.publish('event1', 'Hello, world!');

// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!

// 取消订阅事件
eventBus.unsubscribe('event1', callback1);

// 发布事件
eventBus.publish('event1', 'Goodbye!');

// 输出:
// Callback 2: Goodbye!

在上述示例中,我们创建了一个 EventBus 类,该类具有 subscribepublishunsubscribe 方法。subscribe 方法用于订阅事件,publish 方法用于发布事件并触发相关的回调函数,unsubscribe 方法用于取消订阅事件。我们使用全局的 eventBus 对象来执行订阅和发布操作。


这个简单的 EventBus 实现允许你在不同的组件或模块之间发布和订阅事件,以实现跨组件的事件通信和数据传递。你可以根据需要对 EventBus 类进行扩展,添加更多的功能,如命名空间、一次订阅多个事件等。



当问到EventBus时,得预防面试官问到EvnetEmitter,不过当我在网上查找相关的资料时,发现很多人似乎都搞混了这两个概念,虽然我在这里的手写原理似乎也差不多,但在实际使用中,两者可能在细节上有所不同。因此,在具体场景中,你仍然需要根据需求和所选用的实现来查看相关文档或源码,以了解它们的具体实现和用法。



下面是一个简单的 EventEmitter 类实现的基本示例:


class EventEmitter {
constructor() {
this.events = {}; // 用于存储事件及其对应的回调函数列表
}

// 订阅事件
on(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback); // 将回调函数添加到事件的回调函数列表中
}

// 发布事件
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}

// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); // 过滤掉要取消的回调函数
}
}

// 添加一次性的事件监听器
once(eventName, callback) {
const onceCallback = data => {
callback(data); // 执行回调函数
this.off(eventName, onceCallback); // 在执行后取消订阅该事件
};
this.on(eventName, onceCallback);
}
}

使用上述 EventEmitter 类,你可以执行以下操作:


    const emitter = new EventEmitter();

const callback1 = data => {
console.log('Callback 1:', data);
};

const callback2 = data => {
console.log('Callback 2:', data);
};

// 添加一次性事件监听器
const onceCallback = data => {
console.log('Once Callback:', data);
};

// 订阅事件
emitter.on('event1', callback1);
emitter.on('event1', callback2);
emitter.once('event1', onceCallback);

// 发布事件
emitter.emit('event1', 'Hello, world!');

// 输出:
// Callback 1: Hello, world!
// Callback 2: Hello, world!
// Once Callback: Hello, world!

// 取消订阅事件
emitter.off('event1', callback1);

// 发布事件
emitter.emit('event1', 'Goodbye!');

// 输出:
// Callback 2: Goodbye!

在上述示例中,EventEmitter 类具有 onemitoffonce 方法。on 方法用于订阅事件,emit 方法用于发布事件并触发相关的回调函数,off 方法用于取消订阅事件,once方法用于添加一次性的事件监听器。你可以根据需求对 EventEmitter 类进行扩展,添加更多的功能,比如一次订阅多个事件、取消所有事件订阅等。


eventBus,eventEmitter的区别


EventBusEventEmitter 都是用于实现事件发布-订阅模式的工具,但它们在实现和使用上有一些区别。




  1. 实现方式:



    • EventBusEventBus 是一个全局的事件总线,通常是作为一个单例对象存在,用于在不同组件或模块之间传递事件和数据。在 Vue.js 中,Vue 实例可以充当 EventBus 的角色。

    • EventEmitterEventEmitter 是一个基于类的模块,通常是作为一个实例对象存在,用于在单个组件或模块内部实现事件的发布和订阅。




  2. 使用范围:



    • EventBusEventBus 的作用范围更广泛,可以跨越不同组件、模块或文件进行事件的发布和订阅。它可以实现多个组件之间的通信和数据传递。

    • EventEmitterEventEmitter 主要用于单个组件或模块内部,用于实现内部事件的处理和通信。




  3. 依赖关系:



    • EventBusEventBus 通常需要一个中央管理的实例,因此需要在应用程序的某个地方进行创建和管理。在 Vue.js 中,Vue 实例可以用作全局的 EventBus

    • EventEmitterEventEmitter 可以在需要的地方创建实例对象,并将其用于内部事件的发布和订阅。




  4. 命名空间:



    • EventBusEventBus 可以使用不同的事件名称来进行事件的区分和分类,可以使用命名空间来标识不同类型的事件。

    • EventEmitterEventEmitter 通常使用字符串作为事件的名称,没有直接支持命名空间的概念。





总结起来,EventBus 主要用于实现跨组件或模块的事件通信和数据传递,适用于大型应用程序;而 EventEmitter 主要用于组件或模块内部的事件处理和通信,适用于小型应用程序或组件级别的事件管理。选择使用哪种工具取决于你的具体需求和应用场景。



8. (场景题)在浏览器中一天只能弹出一个弹窗,如何实现,说一下你的思路?


要在浏览器中实现一天只能弹出一个弹窗的功能,可以使用本地存储(localStorage)来记录弹窗状态。下面是一种实现方案:



  1. 当页面加载时,检查本地存储中是否已存在弹窗状态的标记。

  2. 如果标记不存在或者标记表示上一次弹窗是在前一天,则显示弹窗并更新本地存储中的标记为当前日期。

  3. 如果标记存在且表示上一次弹窗是在当天,则不显示弹窗。


以下是示例代码:


    // 检查弹窗状态的函数
function checkPopupStatus() {
// 获取当前日期
const currentDate = new Date().toDateString();

// 从本地存储中获取弹窗状态标记
const popupStatus = localStorage.getItem('popupStatus');

// 如果标记不存在或者标记表示上一次弹窗是在前一天
if (!popupStatus || popupStatus !== currentDate) {
// 显示弹窗
displayPopup();

// 更新本地存储中的标记为当前日期
localStorage.setItem('popupStatus', currentDate);
}
}

// 显示弹窗的函数
function displayPopup() {
// 在这里编写显示弹窗的逻辑,可以是通过修改 DOM 元素显示弹窗,或者调用自定义的弹窗组件等
console.log('弹出弹窗');
}

// 在页面加载时调用检查弹窗状态的函数
checkPopupStatus();

在这个实现中,checkPopupStatus 函数会在页面加载时被调用。它首先获取当前日期,并从本地存储中获取弹窗状态的标记。如果标记不存在或者表示上一次弹窗是在前一天,就会调用 displayPopup 函数显示弹窗,并更新本地存储中的标记为当前日期。


通过这种方式,就可以确保在同一天只能弹出一个弹窗,而在后续的页面加载中不会重复弹窗。


9. 项目中的性能优化?




  1. 对组件和图片进行懒加载对暂时未使用的组件和图片使用懒加载可以显著地减少页面加载时间,比如在我的项目中路由配置中除了需要频繁切换的页面组件外,其他的组件都使用箭头函数引入组件进行懒加载,以及一些没有展现在界面的图片也进行了一个VueLazy的懒加载。




  2. 减少HTTP请求数量由于频繁的请求会对后端服务器造成极大的负担,所以应该减少不必要的请求,比如在我的项目中的搜索界面,对于搜索按钮增加了防抖功能




  3. 使用缓存使用浏览器缓存可以减少资源请求,从而提高页面加载速度。项目中我会把用户的一些需要持久化的信息存入本地存储。




  4. 异步请求使用Promise.all:异步请求可以在后台加载资源,从而避免阻塞页面加载。在请求数据时,我会使用Promise.all一次性并行的请求类似的数据,而不需要一个一个的请求,较少了请求时间。




  5. 图片优化使用适当的图片格式和大小可以减少页面的资源请求和加载时间,项目中我会把图片转化成base64的格式和webp格式,这样可以使图片大小更小




  6. 使用CDN加速:使用CDN可以提高资源的访问速度,从而加快页面加载速度。我项目中的一些第三方资源有时需要请求,因此我会使用CDN内容分发网络来提高访问速度。




  7. 骨架屏(Skeleton Screen):它可以提升用户感知的加载速度和用户体验。虽然骨架屏本身并不直接影响代码性能,但它可以改善用户对应用程序的感知,提供更好的用户体验。




10. 项目中遇到的难点,如何解决


1. 数据状态管理


前端登录状态管理



  • 我在一个练手的项目中做前端登录功能的时候, 碰到了购物车需要登录判断的功能,比如用isLogin来判断有没有登录,当时由于没有深入了解vuex,所以我一开始想着把这个isLogin通过组件与组件的传值方法,把这个值传给相应的组件,然后在需要登录组件中进行判断,但后来发现这个方法太麻烦了

  • 后来通过学习了解,使用了vuex这个全局状态管理的方法, 通过使用createStore这个vuex中的API创建了一个全局的登录状态,再通过actions mutations实现登录判断和登录状态共享


组件数据状态管理



  • 我项目中一开始首页、详情页等其他页面越来越多的状态放在同一个store上,虽然感觉有点乱,但实现了数据流和组件开发的分离,使得我更能够专注于数据的管理

  • 但随着数据的增多,感觉实在太乱了,然后得知vuex中可以使用 modules 来进行分模块,相应的页面放入相应的模块状态中,之后再用actions,mutations,state,getters这四件套, 更好的模块化管理数据,能够知道哪些状态是全局共享的(登录), 哪些状态是模块共享的

  • 然后在新的项目中,也就是现在简历上的项目里,尝试使用pinia来管理,因为我发现它更简单(没有mutations),模块化更好,让我对组件状态管理的更加得心应手,学习起来也更加的方便。


node的错误处理



  • 一开始用node写后端的时候,一堆错误,比如路由没配置,数据库报错。使得后面的代码都无法运行,写着写着就感觉写不下去,经常一个错误就需要反复的在脑海中想最后依靠那一丝的灵光一闪才解决

  • 之后我就在app.js这个后端入口文件的最后,添加一个统一的错误处理的中间件,向前端返回状态码和相应的信息后,直接使用next()向后继续执行,这样虽然服务器报了错,但仍然可以执行后续的代码。


跨域问题



  • 在我写完前端项目的时候,想要提升一下自己,就转去学习了Koa,在搭建了大致的服务器,写了一个简单的接口并运行服务器后,我想当然的就在前端直接请求后端的端口,结果报了一个跨域的错误,由于当时初学后端,不怎么了解跨域,所以找了很多的解答并逐个在项目中进行尝试,比如跨域中的scriptpostMessagehtml本身的Websocket

  • 但发现最实用的还是在服务器中配置Access-Control-Allow-Origin来控制跨域请求的url地址,以及其他一些Access-Control-Allow头来控制跨域请求方法等,然后跨域请求url的白名单我放入了.env这个全局环境变量中。


axios响应拦截



  • 在后端返回数据的时候,我返回数据有一个状态码以及添加到data这个需要返回的数据(代码如下),这导致我在获取接口里的数据时需要多.data(引用一层data),当时我没意识到,结果一直获取不到数据。之后输出获取的数据才发现在数据外面包了一层,虽然这个时候解决了服务器那边数据返回的问题,但后面每次获取数据时都需要在往里再获取,非常的麻烦。

  • 最后在学习了并在项目中使用axios进行请求和响应后,就在响应的时候设置一个拦截器,对响应进行一番处理之后就可以直接拿到后端接口返回的值,而不会导致接口返回的值不会有太多的嵌套了。


11. 如何学习前端的,学了几年?



这个就看个人情况了,但其中,你得展现出你的学习积极性和对前端的热爱,让面试官能够欣赏你



我大致说说我回答的,仅作参考


我从大二开始就对前端很感兴趣,当时正好学校也分了Web前端的方向,于是就跟着学校的课程开始学习基本的html,css,js三剑客,但之后感觉到老师教的很慢,就自己到B站上学习了,之后由于参加过一次蓝桥杯,就看到了蓝桥云课上有相关的基于html,css,js比较基础项目,接着我还学习了一些行内大牛写的一些博客文章,比如阮一峰,张鑫旭,廖雪峰等这些老师。之后又学习了vue并且在GitHub上学习相关的设计理念,根据GitHub上项目中不懂的东西又逐渐学习了各种UI组件库和数据请求方式,最后又学习了Nodejs中的Koa,用Vue和Koa仿写了一个全栈型项目,目前正在学习一些typescript的基本用法并尝试着运用到项目中,并在学习Vue的一些底层源码。


结语及吐槽


大厂的面试终归到底还是和我之前面的公司不一样,它们更加看重的是代码底层的实现和你的算法基础,终归到底,这次面试只是一次小尝试,想要知道自己的水平到底在哪里,并且能够借此完善自己的能力,努力的提升自己,希望能够给

作者:吃腻的奶油
来源:juejin.cn/post/7240751116701728805
大家带来一些正能量。

收起阅读 »

优化图片和视频的加载过程,提升用户体验

web
展示效果 (因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵) 展示都是基于 Slow 3G 弱网下的效果。 优化前 这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。 优化后 图片下载过程中显示模糊的图片占位符,直到图片下...
继续阅读 »

展示效果


(因为掘金不能上传视频,所以转成动图之后分辨率比较低,还望多包涵)


展示都是基于 Slow 3G 弱网下的效果。


优化前


before.gif


这种体验交较差,在图片下载完之前,本应该展示图片的区域会长时间空白。


优化后


eeeee.gif


图片下载过程中显示模糊的图片占位符,直到图片下载完成再切换展示。


原理


首先先贴出页面的代码 index.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>
</head>
<style>
html,
body {
margin: 0;
padding: 0;
}

@keyframes pulse {
0% {
opacity: 0;
}
50% {
opacity: 0.1;
}
100% {
opacity: 0;
}
}

.container {
width: 50vw;
background-repeat: no-repeat;
background-size: cover;
}

.container.loaded::before {
animation: none;
content: none;
}

.container::before {
content: '';
position: absolute;
inset: 0;
opacity: 0;
animation: pulse 2.5s infinite;
background-color: var(--text-color);
}

.container img,
.container video {
opacity: 0;
transition: opacity 250ms ease-in-out;
}

.container.loaded img,
.container.loaded video {
opacity: 1;
}
</style>
<body>
<!-- container容器加载一个体积非常小的低分辨率图片 -->
<div class="container" style="background-image: url(http://localhost:3000/uploads/10007/fox-small.jpeg);">
<!-- 图片延时加载 loading: lazy -->
<img
src="http://localhost:3000/uploads/10007/fox.jpeg"
loading="lazy"
style="width: 50vw"
/>

</div>

<br/>

<video
id="video"
autoplay
controls="controls"
style="width: 50vw"
poster="http://localhost:3000/uploads/10007/big_buck_bunny-small.png"
src="http://localhost:3000/uploads/10007/big_buck_bunny.mp4"
>
</video>
</body>
<script>
const blurredImageDiv = document.querySelector('.container');
const img = blurredImageDiv.querySelector('img');
function loaded() {
// 图片下载完之后 再展示
blurredImageDiv.classList.add('loaded');
}

if (img.complete) {
loaded();
} else {
img.addEventListener('load', loaded);
}

var poster = new Image();
poster.onload = function () {
// 加载完之后替换 poster url 不会重复请求
const video = document.querySelector('#video');
video.poster = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
};
poster.src = 'http://localhost:3000/uploads/10007/big_buck_bunny.png';
</script>
</html>

其实原理就是基于原图片生成出一个低分辨率体积非常小的图片(因为体积小,下载会很快),然后作为占位符显示,直到原图片完全下载之后再替换展示原图片。


那么如何生成一个超低分辨率的占位图片呢,可以使用 ffmpeg,需要本地提前安装,我是用的MacOS系统,所以直接通过 brew install ffmpeg 安装了。


如果是服务使用 Docker 部署的话,可参考:


FROM node:16 AS deps
WORKDIR /app
COPY . .
RUN wget https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-4.4.1-arm64-static.tar.xz &&\
tar xvf ffmpeg-4.4.1-arm64-static.tar.xz &&\
mv ffmpeg-4.4.1-arm64-static/ffmpeg /usr/bin/ &&\
mv ffmpeg-4.4.1-arm64-static/ffprobe /usr/bin/
#RUN apt install -y ffmpeg
RUN yarn install
RUN yarn build
EXPOSE 3000
ENV PORT 3000
CMD [ "node", "dist/index.js" ]

ffmpeg -i sourcePath.jpg -vf scale=width:height outputPath.jpg
// 约束比例压缩
// width/height 为压缩之后图片的宽高 当其中一个值为 -1 的时候将保持原来的尺寸比例压缩

那么我们可以有如下命令:


ffmpeg -i sourcePath.jpg -vf scale=20:-1 outputPath.jpg
// 压缩之后生成 20 像素宽的图片用于做占位符展示

我们可以写个文件上传的服务,上传图片之后,服务端自动生成一个低分辨率的图片版本,然后将两者的地址url都返回过来。比如 Node 中我们可以使用 fluent-ffmpeg,那么以上命令就对应成代码:


import * as ffmpeg from 'fluent-ffmpeg';
import { FfmpegCommand } from 'fluent-ffmpeg';

export const runFfmpegCmd = (command: FfmpegCommand) =>
new Promise<void>((resolve, reject) => {
command
.on('error', (error) => {
reject(error);
})
.on('end', () => {
resolve();
})
.run();
});


public async uploadImage(user: User.UserInfo, file: Express.Multer.File) {
console.log(file);
const path = join(this.uploadPath, `${user.id}`, '/');

await ensureDir(path);

const { originalname, path: filePath } = file;
const finalPath = path + originalname;
const name = originalname.split('.');
const smallPath = path + name[0] + '-small.' + name[1];
console.log(smallPath);
await rename(filePath, finalPath);

// size 对应 scale=20:-1
await runFfmpegCmd(ffmpeg(finalPath).size('20x?').output(smallPath));

return {
statusCode: HttpStatus.OK,
data: {
path: finalPath,
smallPath,
},
};
}

public async uploadVideo(user: User.UserInfo, file: Express.Multer.File) {
console.log(file);
const path = join(this.uploadPath, `${user.id}`, '/');

await ensureDir(path);

const { originalname, path: filePath } = file;
const finalPath = path + originalname;
const name = originalname.split('.');
const shotName = name[0] + '.png';
const smallName = name[0] + '-small.png';

await rename(filePath, finalPath);

// 生成两个不同分辨率的缩略图
await Promise.all([
runScreenShotCmd(
ffmpeg(finalPath).screenshot({
count: 1,
filename: shotName,
folder: path,
}),
),
runScreenShotCmd(
ffmpeg(finalPath).screenshot({
count: 1,
filename: smallName,
folder: path,
size: '20x?',
}),
),
]);

return {
statusCode: HttpStatus.OK,
data: {
path: finalPath,
shotPath: path + shotName,
smallPath: path + smallName,
},
};
}

代码在自己的github上:im_server


自己本地的 swagger 界面的上传截图:


图片
image.png


视频
image.png


那么我们就可以得到一个超低分辨率的图片了,由于体积非常小,所以下载很快(特别是弱网情况下)。


补充


关于 img 标签的 lazy load 可参考:浏览器IMG图片原生懒加载loading=”lazy”实践指南


使用 imgsrcset 属性可实现根据不同屏幕分辨率加载不同尺寸的图片,进一步提升用户体验,而且没必要在小屏幕中加载超大分辨率的图片:响应式图片


结论


通过使用超低分辨率的占位符图片可以优化用户体验,特别是一些图片素材网站,再结合 img 标签的 loading="lazy"

作者:梦想很大很大
来源:juejin.cn/post/7244352006814679100
code> 懒加载。

收起阅读 »

优雅的时钟翻页效果,让你的网页时钟与众不同!

web
你有没有想过给你的网页时钟添加翻页效果,让它更引人注目,更酷炫吗?如果是的话,你来对地方了! 这篇文章将教你如何通过简单的步骤,为你的网页时钟添加翻页效果。 无论你是 web 开发初学者或是有一定经验的开发者,这篇文章都将为你提供有用的实现技巧和原理解释。 ...
继续阅读 »

你有没有想过给你的网页时钟添加翻页效果,让它更引人注目,更酷炫吗?如果是的话,你来对地方了!


13.gif


这篇文章将教你如何通过简单的步骤,为你的网页时钟添加翻页效果。


无论你是 web 开发初学者或是有一定经验的开发者,这篇文章都将为你提供有用的实现技巧和原理解释。


来,跟着子辰一起开始吧!


思考


01.gif


通过上图可以看到,由 3 翻到 4,其实是 3 的上半部分,与 4 的下半部分,一起翻下来的。


为了便于理解将翻页的过程通过侧面角度展示,解析成下图中所示的样子。


02.png


我们先来看一下 3 是如何呈现的。


03.png


那么由 3 的呈现我们可以知道,4 其实一开始是对折的,然后 4B 翻下来后形成完整的 4。


04.png


那么现在我们将 3 与 4 结合在一起看看。


05.png


由上可知,下一个数字都是对折的,在呈现时,都是有由前一个数字的上半部与下一个数字的上半部,翻转得到新的数字。


既然数字翻页的秘密我们知道了,接下来就是实现了。


06.png


实现翻页


容器背景


首先我们要实现一个承载数字的容器,中间使用伪元素做分割线,这就是时钟的底盘。


<div class="card-container"></div>

.card-container {
background: #2c292c;
width: 200px;
height: 200px;
position: relative;
perspective: 500px;
}

.card-container::before {
z-index: 99;
content: " ";
position: absolute;
left: 0;
top: 50%;
background: #120f12;
width: 100%;
height: 6px;
margin-top: -3px;
}

07.png


下层数字上半部分


接下来我们先来实现背后的下一层的 4,因为 4 分为上下两部分,我们先实现上半部分。


<div class="card-container">
<div class="card1 card-item">4</div>
</div>

/* 因为所有的数字都有公共部分,我们提取出来 */
.card-item {
position: absolute;
width: 100%;
/* 因为每个卡片只有半个数字,所以高度只有百分之50 */
height: 50%;
left: 0;
top: 0;
overflow: hidden;
background: #2c292c;
}

.card1 {
line-height: 200px
}

08.png


下层数字下半部分


<div class="card-container">
<div class="card1 card-item">4</div>
<div class="card2 card-item">4</div>
</div>

.card2 {
top: 50%;
}

首先我们写出来是这样的。


09.png


但是我们要求的是 4 的下半部分向上对折覆盖在 4 的上半部分之上。
所以我们看到的应该是 4 下半部分的背面,通过中线向上对折,并且因为是背面,所以我们不应该看到他。


.card2 {
z-index: 2;
top: 50%;
line-height: 0;
/* 变换原点为上边的中部 */
transform-origin: center top;
/* 对折 180 度 */
transform: rotateX(180deg);
/* 通过这个属性让元素的背面隐藏 */
backface-visibility: hidden;
}

08.png


现在看上去好像和只有上半部分没什么区别,所以我们给他加个 hover 加个过渡效果让它翻转下来看看。


11.gif


这样就看出来了。


上层数字


上层数字的原理就比较简单了,我们参考下层数字的逻辑写。


<div class="card-container">
<div class="card1 card-item">4</div>
<div class="card2 card-item">4</div>
<div class="card3 card-item">3</div>
<div class="card4 card-item">3</div>
</div>

.card3 {
line-height: 200px;
transform-origin: center bottom;
backface-visibility: hidden;
z-index: 2
}

.card4 {
top: 50%;
line-height: 0
}

12.png


现在就是这样效果,同样的,我们给它加个 hover,3 的翻页过渡要与 4 的保持同步。


01-13.gif


现在我们就实现了单个数字的过渡。


翻页时钟


时钟无非就是三个翻页效果加上自动翻页,我们去实现一下。


<!-- data-number 用于存储上一次的时间,来和即将改变的时间对比 -->
<div class="card-container flip" id="card-h" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>
<div class="card-container flip" id="card-m" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>
<div class="card-container flip" id="card-s" data-number="00">
<div class="card1 card-item">00</div>
<div class="card2 card-item">00</div>
<div class="card3 card-item">00</div>
<div class="card4 card-item">00</div>
</div>

/* etc... */

.flip .card2 {
transform: rotateX(0);
}

.flip .card3 {
transform: rotateX(-180deg);
}

// 获取 dom
const hour = document.getElementById("card-h");
const minute = document.getElementById("card-m");
const second = document.getElementById("card-s");

function setHTML(dom, time) {
// 下一次要显示的时间
const nextValue = time.toString().padStart(2, "0");
// 上一次的时间
const curValue = dom.dataset.number;
// 如果下次要显示的时间和上一次的一样,直接退出。比如在同一分钟或同一小时内。
if (nextValue === curValue) {
return;
}
// 重置时分秒的 dom
dom.innerHTML = `<div class="card1 card-item">${nextValue}</div>
<div class="card2 card-item">${nextValue}</div>
<div class="card3 card-item">${curValue}</div>
<div class="card4 card-item">${curValue}</div>`
;
// 移除 flip 属性再次添加以触发过渡再次执行
dom.classList.remove("flip");
dom.clientHeight;
dom.classList.add("flip");
// 时间不同时重置 dataset.number
dom.dataset.number = nextValue;
}

// 获取时分秒并分别设置
function setNumbers() {
var now = new Date();
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();
setHTML(hour, h);
setHTML(minute, m);
setHTML(second, s);
}

setNumbers();

setInterval(setNumbers, 1000);

13.gif


至此我们就完成了时钟翻页的效果了,你学会了吗?


总结


子辰详细介绍了如何通过简单的步骤,为网页时钟添加翻页效果。


文章从思考开始,通过分析数字翻页的秘密来解决问题。


接着,详细讲解了实现翻页的具体方法和原理,并给出了相应的代码实现。


最后,通过组合多个翻页效果,实现了完整的时钟翻页效果。


如果你认真读完了这篇文章,那么以下这几点都是你所学到的:



  1. 提高对 CSS3 属性的理解和掌握,例如 perspective、transform、backface-visibility 等。

  2. 掌握实现元素翻转动画的基本方法和技巧,包括旋转轴心、变换原点、背面可见性等。

  3. 了解如何通过数据属性(data-*)存储和比较数据,避免不必要的重复操作。

  4. 学会如何通过 JavaScript 操作 DOM 元素,实现网页中的动态效果。


其实实现的效果并不难,代码也都是基础的代码,难的是思考翻页的过程,好的思维方法,才是前端进阶的基础,更是关键。


多看好的文章,多看好的思考过程,都是

作者:子辰Web草庐
来源:juejin.cn/post/7244351125448458296
提升思维的一种方式。

收起阅读 »

移动端网页开发有感

web
前段时间参与了一个移动端页面开发的需求,开发时明显感觉与 pc 端开发相比,移动端页面的限制会更多😭 需求结束后思考了一下究竟是哪些方面感觉不舒服?有没有对应的解决方法?以便下次开发移动端页面时能提升开发效率和体验。 移动端网页容易出现布局问题 🤦‍♂️ 因为...
继续阅读 »

前段时间参与了一个移动端页面开发的需求,开发时明显感觉与 pc 端开发相比,移动端页面的限制会更多😭


需求结束后思考了一下究竟是哪些方面感觉不舒服?有没有对应的解决方法?以便下次开发移动端页面时能提升开发效率和体验。


移动端网页容易出现布局问题 🤦‍♂️


因为页面空间小,容易出现元素重叠、挤压、换行等样式问题,怎么在不同尺寸的设备上合适地展示页面?


解决办法:




  1. 使用 <meta name="viewport"> 标签 ✨


    这个标签想必做过移动端页面开发的同学都不陌生吧?它就是专门为移动端展示优化而增加的标签。


    先来看看它的作用是什么?


    它可以设置移动端页面宽度、缩放比例、是否允许用户缩放网页等


    它的基本属性有哪些?


    属性名含义取值范围
    width控制视口大小具体数值或 'device-width'
    initial-scale页面初始缩放比例0.0 ~ 10
    minimum-scale控制页面允许被缩小的倍数0.0 ~ 10
    maximum-scale控制页面允许被大的倍数0.0 ~ 10
    user-scalable控制是否允许放大和缩小页面yes 或 no

    需要注意的是在移动设备上默认页面宽度为 980px:




Luban_16853778753692bc31648-c60e-4ebc-845c-3bac272f7393.jpg


假如我们希望页面的视口宽度与设备宽度相同,同时初始缩放比例为 1,可以在 <head> 里增加这个的 meta 标签
<meta name="viewport" content="width=device-width,initial-scale=1">


Luban_1685377875383f6d72d0e-c360-43aa-807a-739399af01fe.jpg


这样页面的展示就符合我们的预期了




  1. 使用 vw、vh 视口单位
    vw、vh 都是视口单位


    简而言之:100vw = 视口宽度,100vh = 视口高度 (够简单吧 😅




  2. 使用 rem 相对单位




rem 在移动端开发中很重要,因为不同移动设备有着不同的尺寸和分辨率,如果我们用固定的 px 作为元素大小单位会发现不够适用


而 rem 是相对单位大小,它相对的是根元素 html 的字体大小,比如:


<html>
<head>
<style>
html {
font-size: 14px; // 这里将 html 的字体大小设为 14px
}
.content {
font-size: 2rem; // 在页面展示时将会被计算成 14 * 2 = 28px
}
</style>
</head>
<body>
<div class="content">rem</div>
</body>
</html>

所以我们可以根据设备大小动态设置根元素大小,从而成比例地更改页面里其它元素的大小


    const BASE_PAGE_WIDTH = 370
const BASE_SIZE = 16

function setRem() {
const scale = document.documentElement.clientWidth / BASE_PAGE_WIDTH
document.documentElement.style.fontSize = `${scale * BASE_SIZE}px`
}

setRem()

真机调试比较麻烦 😌


尽管可以在电脑浏览器上模拟移动设备的展示情况,但并不够真实,在开发和测试阶段仍然需要在真机上调试;


同时可能我们的测试环境需要连接 vpn 或添加特定的请求头才能访问到,所以在手机上需要通过电脑代理才能访问测试环境,从而进行开发测试;


最后,即使能在手机上访问到本地开发和测试环境的页面,你会发现当页面报错的时候你压根就看不到 log 日志输出或网络请求,这种干看着页面有问题却不能进一步排查的感觉就很难受 😖


还好有一些工具可以帮我们化解这些难题 🥳


解决办法:



  1. 首先我们可以使用 whistlecharles 来连接电脑代理,这里以 whistle 为例:

    • 电脑安装并启动 whistle

    • 手机和电脑在同一局域网下

    • 手机设置网络代理

    • 手机安装 https 证书





具体操作可以访问官方文档的详细步骤哈



Done!现在手机会通过电脑作为代理来访问网络,可以直接访问开发地址或测试环境地址啦~



  1. 然后我们可以使用 VConsole 在移动设备上进行调试,它相当于在页面上加了一个控制台,从而让我们可以查看页面上的日志输出、网络请求等,它的用法也很简单:


// 使用 npm 安装
npm install vconsole

import VConsole from 'vconsole'

new VConsole()

// 使用 CDN 安装
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole will be exported to `window.VConsole` by default.
const vConsole = new window.VConsole();
</script>


然后你会发现页面右下角多了一个 vConsole 的按钮:


Screenshot_2023-06-12-23-46-33-090_mark.via.jpg


我们可以测试一下打印日志:


    document.addEventListener('click', handlePageClick)

function handlePageClick() {
console.log('Daniel Yang')
}

在点击页面后再点击 vConsole 按钮会发现在展开的面板里 log 一栏已经显示出 log 的内容:


Screenshot_2023-06-12-23-48-35-778_mark.via.jpg


同时我们也可以在 VConsole 面板上查看页面元素结构、客户端存储、网络请求等,总之非常的 nice 🤗


以上就是自己对一次移动端网页开发过程中遇到的问题小小的总结,如果你在移动端开发中有遇到其它印象深刻的坑,欢迎一起留言讨论哦


006APoFYly8hesgm67dwpj30hs0hbmxv.jpeg

作者:卡布奇诺有点苦
来源:juejin.cn/post/7243757233666195515
th="50%"/>

收起阅读 »

百分百空手接大锅

web
背景 愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅...
继续阅读 »

背景


愉快的双休周末刚过完,早上来忽然被运营通知线上业务挂了,用户无法下单。卧槽,赶紧进入debug模式,一查原来是服务端返回的数据有问题,赶紧问了服务端,大佬回复说是业务部门配置套餐错误。好在主责不在我们,不过赶紧写了复盘文档,主动找自己的责任,扛起这口大锅,都怪我们前端,没有做好前端监控,导致线上问题持续两天才发现。原本以为运营会把推辞一下说不,锅是她们的,可惜人家不太懂人情世故,这锅就扣在了技术部头上。虽然但是,我还是静下心来把前端异常监控搞了出来,下次一定不要主动接锅,希望看到本文的朋友们也不要随便心软接锅^_^


监控


因为之前基于sentry做了埋点处理,基础已经打好,支持全自动埋点、手动埋点和数据上报。相关的原理可以参考之前的一篇文章如何从0-1构建数据平台(2)- 前端埋点。本次监控的数据上报也基于sentry.js。那么如何设计整个流程呢。具体步骤如下:




  1. 监控数据分类




  2. 监控数据定义




  3. 监控数据收集




  4. 监控数据上报




  5. 监控数据输出




  6. 监控数据预警




数据分类


我们主要是前端的数据错误,一般的异常大类分为逻辑异常和代码异常。基于我们的项目,由于涉及营收,我们就将逻辑错误专注于支付异常,其他的代码导致的错误分为一大类。然后再将两大异常进行细分,如下:




  1. 支付异常


    1.1 支付成功


    1.2 支付失败




  2. 代码异常


    2.1 bindexception


     2.1.1  js_error

    2.1.2 img_error

    2.1.3 audio_error

    2.1.4 script_error

    2.1.5 video_error



  3. unhandleRejection


    3.1 promise_unhandledrejection_error


    3.2 ajax_error




  4. vueException




  5. peformanceInfo




数据定义


基于sentry的上报数据,一般都包括事件与属性。在此我们定义支付异常事件为“page_h5_pay_monitor”,定义代码异常事件为“page_monitor”。然后支付异常的属性大概为:



pay_time,

pay_orderid,

pay_result,

pay_amount,

pay_type,

pay_use_coupon,

pay_use_coupon_id,

pay_use_coupon_name,

pay_use_discount_amount,

pay_fail_reason,

pay_platment


代码异常不同的错误类型可能属性会有所区别:



// js_error

monitor_type,

monitor_message,

monitor_lineno,

monitor_colno,

monitor_error,

monitor_stack,

monitor_url

// src_error

monitor_type,

monitor_target_src,

monitor_url

// promise_error

monitor_type,

monitor_message,

monitor_stack,

monitor_url

// ajax_error

monitor_type,

monitor_ajax_method,

monitor_ajax_data,

monitor_ajax_params,

monitor_ajax_url,

monitor_ajax_headers,

monitor_url,

monitor_message,

monitor_ajax_code

// vue_error

monitor_type,

monitor_message,

monitor_stack,

monitor_hook,

monitor_url

// peformanceInfo 为数据添加 loading_time 属性,该属性通过entryTypes获取

try {

const observer = new PerformanceObserver((list) => {

for (const entry of list.getEntries()) {

if (entry.entryType === 'paint') {

sa.store.set('loading_time', entry.startTime)

}
}

})

observer.observe({ entryTypes: ['paint'] })

} catch (err) {

console.log(err)

}


数据收集


数据收集通过事件绑定进行收集,具体绑定如下:


import {

BindErrorReporter,

VueErrorReporter,

UnhandledRejectionReporter

} from './report'

const Vue = require('vue')


// binderror绑定

const MonitorBinderror = () => {

window.addEventListener(

'error',

function(error) {

BindErrorReporter(error)

},true )

}

// unhandleRejection绑定 这里由于使用了axios,因此ajax_error也属于promise_error

const MonitorUnhandledRejection = () => {

window.addEventListener('unhandledrejection', function(error) {

if (error && error.reason) {

const { message, code, stack, isAxios, config } = error.reason

if (isAxios && config) {

// console.log(config)

const { data, params, headers, url, method } = config

UnhandledRejectionReporter({

isAjax: true,

data: JSON.stringify(data),

params: JSON.stringify(params),

headers: JSON.stringify(headers),

url,

method,

message: message || error.message,

code

})

} else {

UnhandledRejectionReporter({

isAjax: false,

message,

stack

})

}

}

})

}

// vueException绑定

const MonitorVueError = () => {

Vue.config.errorHandler = function(error, vm, info) {

const { message, stack } = error

VueErrorReporter({

message,

stack,

vuehook: info

})

}

}

// 输出绑定方法

export const MonitorException = () => {

try {

MonitorBinderror()

MonitorUnhandledRejection()

MonitorVueError()

} catch (error) {

console.log('monitor exception init error', error)

}

}


数据上报


数据上报都是基于sentry进行上报,具体如下:



/*

* 异常监控库 基于sentry jssdk

* 监控类别:

* 1、window onerror 监控未定义属性使用 js资源加载失败问题

* 2、window addListener error 监控未定义属性使用 图片资源加载失败问题

* 3、unhandledrejection 监听promise对象未catch的错误

* 4、vue.errorHandler 监听vue脚本错误

* 5、自定义错误 包括接口错误 或其他diy错误

* 上报事件: page_monitor

*/


// 错误类别常量

const ERROR_TYPE = {

JS_ERROR: 'js_error',

IMG_ERROR: 'img_error',

AUDIO_ERROR: 'audio_error',

SCRIPT_ERROR: 'script_error',

VIDEO_ERROR: 'video_error',

VUE_ERROR: 'vue_error',

PROMISE_ERROR: 'promise_unhandledrejection_error',

AJAX_ERROR: 'ajax_error'

}

const MONITOR_NAME = 'page_monitor'

const PAY_MONITOR_NAME = 'page_h5_pay_monitor'

const MEMBER_PAY_MONITOR_NAME = 'page_member_pay_monitor'

export const BindErrorReporter = function(error) {

if (error) {

if (error.error) {

const { colno, lineno } = error

const { message, stack } = error.error

// 过滤

// 客户端会有调用calljs的场景 可能有一些未知的calljs

if (message && message.toLowerCase().indexOf('calljs') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else if (error.target) {

const type = error.target.nodeName.toLowerCase()

const monitorType = type + '_error'

const src = error.target.src

sa.track(MONITOR_NAME, {

//属性

})

}

}

}

export const UnhandledRejectionReporter = function({

isAjax = false,

method,

data,

params,

url,

headers,

message,

stack,

code

}
) {

if (!isAjax) {

// 过滤一些特殊的场景

// 1、自动播放触发问题

if (message && message.toLowerCase().indexOf('user gesture') !== -1) {

return

}

sa.track(MONITOR_NAME, {

//属性

})

} else {

sa.track(MONITOR_NAME, {

//属性

})

}

}

export const VueErrorReporter = function({ message, stack, vuehook }) {

sa.track(MONITOR_NAME, {

//属性

})

}

export const H5PayErrorReport = ({

isSuccess = true,

amount = 0,

type = -1,

couponId = -1,

couponName = '',

discountAmount = 0,

reason = '',

orderid = 0,

}
) => {

// 事件名:page_member_pay_monitor

sa.track(PAY_MONITOR_NAME, {

//属性

})

}


以上,通过sentry的sa.track进行上报,具体不作展开


输出与预警


数据被上报到大数据平台,被存储到hdfs中,然后我们直接做定时任务读取hdfs进行一定的过滤通过钉钉webhook输出到钉钉群,另外如果有需要做数据备份可以通过hdfs到数据仓库再到kylin进行存储。


总结


数据监控对于大的,特别是涉及营收的平台是必要的,我们在设计项目的时候一定要考虑到,最好能说服服务端,让他们服务端也提供相应的代码监控。ngnix层或者云端最好也来一层。严重的异常可以直接给你打电话,目前云平台都有相应支持。这样有异常及时发现,锅嘛,接到手里

作者:CodePlayer
来源:juejin.cn/post/7244363578429030459
就可以精准扔出去了。

收起阅读 »

最近遇到的奇葩进度条

web
前言 本文将介绍几个我最近遇到的奇葩进度条,需求看似简单,但是我们可以用平常巧妙的属性来解决,再也不用复杂的html结构和颜色渐变算法。 “奇葩”的环形渐变进度条 需求描述:需要环形渐变的进度条让人快速理解进度实现程度,10-20%是青绿色,20%到30%是黄...
继续阅读 »

前言


本文将介绍几个我最近遇到的奇葩进度条,需求看似简单,但是我们可以用平常巧妙的属性来解决,再也不用复杂的html结构和颜色渐变算法。


“奇葩”的环形渐变进度条


需求描述:需要环形渐变的进度条让人快速理解进度实现程度,10-20%是青绿色,20%到30%是黄色.....


乍一看是不是很容易,但是我思来想去用了echarts的svg渲染,但是只要到了90%,一定会渐变到青绿色,从红色渐变到青绿色,做实让我心一凉。


image.png


思路一:径向渐变分割


网上思路很多,稍微复杂的比如分割区域做大量的颜色的径向渐变。原理是将rgba转为16进制计算颜色插值。这样我们通过计算step步长就可以根据细分做渐变了。但是好像无法很好满足我们的指定区域10%-20%是某种颜色,虽然可以但是也太麻烦了。


  function gradientColor(startRGB, endRGB, step) {
let startR = startRGB[0]
let startG = startRGB[1]
let startB = startRGB[2]
let endR = endRGB[0]
let endG = endRGB[1]
let endB = endRGB[2]
let sR = (endR - startR) / step // 总差值
let sG = (endG - startG) / step
let sB = (endB - startB) / step
var colorArr = []
for (var i = 0; i < step; i++) {
let color = 'rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')'
colorArr.push(color)
}
return colorArr
}

思路二:CSS结合svg


我们可以用css的background: conic-gradient


background: conic-gradient(#179067, #62e317, #d7f10f, #ffc403, #fcc202, #ff7327, #ff7327, #FF5800, #ff5900, #f64302, #ff0000, #ff0000);

image.png


看着好像不错,那么接下来只要我们做个遮罩,然后用svg的strokeDashoffset来形成我们的环状进度条就可以了。至于百分之几到百分之几我们可以将conic-gradient内部属性做个百分比的拆分就可以了


image.png


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.circle {
width: 300px;
height: 300px;
background: conic-gradient(#179067, #62e317, #d7f10f, #ffc403, #fcc202, #ff7327, #ff7327, #FF5800, #ff5900, #f64302, #ff0000, #ff0000);
border-radius: 50%;
position: relative;
}

#progress-circle circle {
stroke-dasharray: 880;
stroke: #f2f2f2;
}

#progress-circle {
transform: rotate(-90deg);
}

.circle-mask {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
width: 260px;
height: 260px;
background: #fff;
border-radius: 50%;
}
</style>

<body>
<div class="circle">
<svg id="progress-circle" width="300" height="300">
<circle r="140" cx="150" cy="150" stroke-width="21" fill="transparent" />
</svg>
<div class="circle-mask"></div>
</div>
</body>
<script>
const circle = document.querySelector('#progress-circle circle');
const radius = circle.r.baseVal.value;
const circumference = radius * 2 * Math.PI;
function setProgress(percent) {
const progress = circumference - (percent / 100) * circumference;
circle.style.strokeDashoffset = -progress;
}
let prog = 40
let val = 100 - prog
setProgress(val); //设置初始进度

</script>

</html>

这里简单讲下逻辑,我们通过计算环的周长,总长其实就是stroke-dasharray,通过strokeDashoffset来偏移我们的虚线线段,那么开始的就是我们的实线线段。其实就是一个蚂蚁线。让这个线长度等于我们的环长度,通过api让实线在开始的位置。


最终效果


image.png


"奇葩"的横向进度条


在我们平常需求用用组件库实现进度条很容易,但是我们看看这个需求的进度条的场景,文字要能被裁剪成黑白两色。


image.png


image.png


思路一: overflow:hidden


具体就不演示了,内部通过两个副本的文案,一套白色一套黑色,通过定位层级的不同,overflow:hidden来隐藏,缺点是相对繁琐的dom结构。


思路二: background-clip 裁剪


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
:root {
--d: 20%
}

.inverted {
padding: 0 8px;
display: flex;
justify-content: space-between;
background: linear-gradient(-90deg, #000 var(--d), #fff 0) no-repeat, linear-gradient(-90deg, #0000 var(--d), rgb(192, 23, 23) 0) no-repeat;
-webkit-background-clip: text, padding-box;
background-clip: text, padding-box;
color: #0000;
font-weight: bold;
cursor: pointer;
}

.box {
background: #ebebeb;
width: 300px;
border-radius: 24px;
overflow: hidden;
}
</style>

<body>
<div class="box">
<div class="inverted">
<div class="inverted-item">888w/12</div>
<div class="inverted-item">100%/10s</div>
</div>
</div>
</body>

<script>
function modifyProg(prog) {
let val = 100 - prog
document.documentElement.style.setProperty('--d', val + '%')
}
modifyProg(6)
</script>

</html>

这里我们主要用了background-clip的text和padding-box两个裁剪,一个裁剪文本,一个裁剪背景延伸至内边距padding外沿。不会绘制到边框处。在js中我们通过setProperty修改css变量即可。


最终效果


image.png


附录



  1. 卷出新高度,几百行的Canvas游戏秘籍 - 掘金 (juejin.cn)

  2. 为什么我的WebGL开发这么丝滑 🌊

  3. Echarts无法实现这个曲线图😭,那我手写一个 - 掘金 (juejin.cn)

  4. Redux已死,那就用更爽的Zustand - 掘金 (juejin.cn)<
    作者:谦宇
    来源:juejin.cn/post/7244172094547492923
    /a>

收起阅读 »

IntelOne Mono 英特尔编程字体

web
一、简介 IntelOne Mono 是一个被设计为富有表现力的字体,在设计之初就充分的考虑到 开发者的需求:清晰度/易读性,以解决视觉的疲劳, 减少编码错误等问题,并且 IntelOne Mono 已经得到 低视力 等人群的反馈,证明确实得到一些正面反馈效果...
继续阅读 »

IntelOne Mono.png


一、简介


IntelOne Mono 是一个被设计为富有表现力的字体,在设计之初就充分的考虑到 开发者的需求:清晰度/易读性,以解决视觉的疲劳, 减少编码错误等问题,并且 IntelOne Mono 已经得到 低视力 等人群的反馈,证明确实得到一些正面反馈效果。



开源协议:SIL Open Font License 1.1 商用需要注意协议内容。



二、字体粗细


Untitled.png


IntelOne Mono 按照字体粗细可以分为四种字体:细体/常规字体/中等粗细/粗体 以及斜体效果


三、字体格式支持


font-images.png


官方仓库 中,给出四种格式的样式字体,就安装体验:



  • Windows/MacOS 桌面端使用 ttf/otf,这两种字体都具有跨平台性。

  • Web 端使用 woff/woff2 字体。


四、下载并使用字体


1. Git clone 下载


cd <dir> & git clone https://github.com/intel/intel-one-mono.git

2. Github Release 下载



intel/intel-one-mono 根据需要下载即可。



3、在 VS Code 中配置


位置设置示例
1、配置Settings(设置) -> User(用户) -> Text Editor (文本编辑器) -> Font Family (字体家族) -> IntelOne Monovs-code-setting-font.png
2、集成终端Settings(设置) -> User(用户) -> Features(特性) -> Terminal (终端) -> Intergrated Font Family (字集成字体家族) -> IntelOne Monointel-one-inter.png

4、在 WebStrome 中配置


编辑器配置设置位置示例
1、配置编辑器Settings(设置) -> Editor(编辑器) -> Font -> IntelOne Monowebstrom-use-intel-one-mono-font.png

5、在 Sublime Text 中配置


覆盖 json 数据 font_face 属性值:


编辑器配置设置位置示例
font_face"font_face": "IntelOne Mono"sublime-intel-one-mono-screen-cut.png


其他环境字体,例如:终端中配置也比较简单,就不再复述。



五、字体显示效果


不同大小字体展示效果.png


在渲染字体方面,IntelOne Mono 字体推荐使用大号字体,ttf 文件格式字体,已经对大号字体进行了优化,尤其是在 Windows 平台。



注意:在 VSCode 中,当字体大小发生变化的时候,字体/建议窗口/终端的行高最好一起配置。



六、好编程字体的特点


要素说明
1、清晰易读避免过度装饰,准确还原字体,易读易懂。
2、等宽字体编程对排版的整齐度有较高的要求,排版整齐的代码更加容易阅读和理解。
3、字符与符号层次封面字符中,字母,数字等都具有应该具有不同的展示高度,凸显不同的层级的内容,使得编码更具有层次感。便于快速理解代码字符。
4、特殊字符逗号、句号、括号等等编程中常用的字符,应该突出、便于识别。

七、不同编辑器显示效果


编辑器展示
VS Codeshow-vscode.png
WebStromewebstrom-intelOne-mono-show.png
Sublimeshow-sublime-intel-one-mono-screen-cut.png

八、社区反馈


IntelOne Mono 字体自 4 月 22 发布第一个版本,到今天 Github 社区 Star 数目目前 已经达到 5.5K+ star 数目,足以证明字体的受欢迎程度。


九、与其他字体对比



对比是为了找到更加 适合自己 的字体。



字体名称效果展示
IntelOne Monocompare-intelone-mono.png
JetBrainsMono Nerd FontJetBrainsMono Nerd Font.png
Input Monocompare-Input Mono.png
InconsolataGo Nerd Font Monocompare-InconsolataGo Nerd Font Mono.png
Cascadia Monocompare-cascadiamono.png

十、从 IntelOne Mono 看字体设计


1. UFO


logo.svg

  • 全名:(The Unified Font Object) 统一字体对象

  • UFO 3 (unifiedfontobject.org)
    一种跨平台、跨应用程序、人类可读、面向未来的格式,用于存储字体数据。

  • UFO3 文件的目录结构是这样的。


2. glif


全名:(glyph interchage format) 描述字型文件格式,glif 存储了单个字形的 (TrueType、PostScript)轮廓、标记点、参考线,使用 xml 语法。


3. plist


属性列表格式包含一系列以 XML 编码的键值对。


4. fea


.fea文件名扩展名主要与 Adobe OpenType Feature File ( .fea)文件类型相关联,该文件类型与 OpenType 一起使用,OpenType 是一个开放的标准规范,用于可扩展的排版字体,最初由微软和 Adobe Systems 开发。


5. 软件


RoboFont


robotfont.png


仅限 MacOS 支持,推荐使用。


FontForge


fontforge.png


适用于 Windows/Mac。


6. PostScript 字体


是由 Adobe Systems 为专业数字排版桌面排版开发的轮廓字体计算机字体规范编码的字体文件。


7. TrueType 字体


TrueType 是由美国苹果公司和微软公司共同开发的一种电脑轮廓字体(曲线描边字)类型标准。这种类型字体文件的扩展名是.ttf,类型代码是 tfil。


十一、小结


本文主要介绍 IntelOne Mono 字体在当前主流编辑器中使用方法和展示效果,了解好的编程字体的优秀特带你。并且探索其背后实现的直奔知识点涉及字体的设计方法和工具软件,如果感兴趣,可以自己设计一套字体。IntelOne Mono 字体在 GitHub 上 Star 数量反映了其被人快速关注且喜欢的特点。但也有不足,没有提供 Nerd Font 字体,对于喜欢用终端的小伙伴

作者:进二开物
来源:juejin.cn/post/7244174500785373241
,暂时可能会受欢迎。

收起阅读 »

还有多少公司在使用H5?不怕被破解吗?

web
H5还有人在用吗 近几天,老板让我调查一下现在市面上H5的使用现状。粗略地调查了一下,发现现在使用H5的真不多了,但是还是有人在用H5的,原因无非一是成本低,相比户外广告,H5制作费用根本不值一提;二是效果好,转化率高,好一点的H5大概有10W浏览量,这也反映...
继续阅读 »

H5还有人在用吗


近几天,老板让我调查一下现在市面上H5的使用现状。粗略地调查了一下,发现现在使用H5的真不多了,但是还是有人在用H5的,原因无非一是成本低,相比户外广告,H5制作费用根本不值一提;二是效果好,转化率高,好一点的H5大概有10W浏览量,这也反映H5数据统计精准,企业知道钱花在哪个地方,心里就踏实。


但是,H5的安全性会让企业非常头疼,不知道大家还记不记得几年前某App H5 页面被植入色情内容广告在安全圈引起了轰动。后来排查才基本确定为用户当地运营商http劫持导致H5页面被插入广告,给该App 造成极大影响。


为什么H5是黑灰产高发区?


从顶象多年的防控经验来看,H5面临的风险相对较多是有其原因的。


1、JavaScript代码特性。


H5平台开发语言是JavaScript,所有的业务逻辑代码都是直接在客户端以某种“明文”方式执行。代码的安全防护主要依靠混淆,混淆效果直接决定了客户端的安全程度。不过对于技术能力较强的黑产,仍然可以通过调试还原出核心业务处理流程。




2、企业营销推广需求追求简单快捷。


首先,相比其他平台,很多公司在H5平台的开放业务往往会追求简单,快捷。比如在营销推广场景,很多企业的H5页面只需从微信点击链接直接跳转到一个H5页面,点击页面按钮即可完成活动,获取积分或者小红包。


一方面确实提升了用户体验,有助于拉新推广;但另一方面简便的前端业务逻辑,往往也会对应简单的代码,这也给黑灰产提供了便利,相比去破解App,H5或者小程序的破解难度要低一些。


数据显示,如果企业在营销时不做风险控制,黑产比例一般在20%以上,甚至有一些高达50%。这就意味着品牌主在营销中相当一部分费用被浪费了。


3、H5平台自动化工具众多。


核心流程被逆向后,攻击者则可以实现“脱机”,即不再依赖浏览器来执行前端代码。攻击者可以自行构造参数,使用脚本提交请求,即实现完全自动化,如selenium,autojs,Puppeteer等。这些工具可以在不逆向JS代码的情况下有效实现页面自动化,完成爬虫或者薅羊毛的目的。


4、防护能力相对薄弱。


从客观层面来看,H5平台无论是代码保护强度还是风险识别能力,都要弱于App。这是现阶段的框架导致,并不是技术能力问题。JavaScript数据获取能力受限于浏览器,出于隐私保护,浏览器限制了很多数据获取,这种限制从某种程度上也削弱了JavaScript在业务安全层面的能力。


以电商App为例,出于安全考虑,很多核心业务只在App上支持。如果H5和App完全是一样的参数逻辑和加密防护,对于攻击者,破解了H5也就等于破解了App。


5、用户对H5缺乏系统认识。


最后,大部分用户对H5的安全缺乏系统性的认识,线上业务追求短平快,没有在H5渠道构建完善的防护体系情况下就上线涉及资金的营销业务。


H5代码混淆


基于上面这些问题,我们可以采取H5代码混淆的方式来稍微解一下困境。


一、产品简介



  • H5代码混淆产品,通过多层加密体系,对H5文件进行加密、混淆、压缩,可以有效防止H5源代码被黑灰产复制、破解。


二、混淆原理



  • 对代码中的数字,正则表达式、对象属性访问等统一转为字符串的表示形式

  • 对字符串进行随机加密(多种加密方式,倒序/随机密钥进行轮函数加密等)

  • 对字符串进行拆分,并分散到不同作用域

  • 打乱函数顺序

  • 提取常量为数组引用的方式


举个简单的例子来说明一下流程
(1)变量和函数重命名:


// 混淆前
function calculateSum(a, b) {
var result = a + b;
return result;
}

// 混淆后
function a1xG2b(c, d) {
var e = c + d;
return e;
}


(2)代码拆分和重新组合:


// 混淆前
function foo() {
console.log('Hello');
console.log('World');
}

// 混淆后
function foo() {
console.log('Hello');
}

function bar() {
console.log('World');
}


(3)控制流转换:


// 混淆前
if (condition) {
console.log('Condition is true');
} else {
console.log('Condition is false');
}

// 混淆后
var x = condition ? 'Condition is true' : 'Condition is false';
console.log(x);

(4)添加无用代码:


// 混淆前
function foo() {
console.log('Hello');
}

// 混淆后
function foo() {
console.log('Hello');
var unusedVariable = 10;
for (var i = 0; i < 5; i++) {
unusedVariable += i;
}
}

结语


当然,实际的代码混淆技术可能更加复杂。而且,代码混淆并不能完全阻止源代码的泄露或逆向工程,但可以增加攻击者分析和理解代码的难度。


H5现在的使用场景其实更多可能偏向日常的投票场景、活动场景以及游戏营销等等,其实使用场景很少了,但是一旦被攻击,尤其是对于运营商这种大厂来说,危害性还是很大的,企业或者说公司还是需

作者:昀和
来源:juejin.cn/post/7244004118222078010
要注意这方面的安全。

收起阅读 »

AnyScript:前端开发的最佳良药!

web
不以繁琐为名,更以简洁为声! 作为一名Api调用工程师,深知在前端开发中的各种挑战和痛点。在我们开发过程中,代码的可维护性和可扩展性是至关重要的因素。TypeScript(简称TS)作为JavaScript的超集,为我们带来了更好的开发体验和更高的代码质量。 ...
继续阅读 »

cover.png


不以繁琐为名,更以简洁为声!


作为一名Api调用工程师,深知在前端开发中的各种挑战和痛点。在我们开发过程中,代码的可维护性和可扩展性是至关重要的因素。TypeScript(简称TS)作为JavaScript的超集,为我们带来了更好的开发体验和更高的代码质量。


1. 类型系统:保驾护航


1.1 强大的类型检查


TypeScript引入了静态类型检查,这是它最吸引人的特点之一。通过在代码中定义变量的类型,TypeScript可以在编译时发现潜在的错误,大大减少了在运行时遇到的意外错误。例如,在JavaScript中,我们可以将一个字符串类型的变量传递给一个预期接收数字类型的函数,这将导致运行时错误。而在TypeScript中,编译器将会提示我们这个潜在的类型不匹配错误,使得我们可以在开发过程中及早发现并修复问题。


举个例子,假设我们有以下的TypeScript代码:


function add(x: number, y: number): number {
return x + y;
}

const result = add(3, '5');
console.log(result);

在这个例子中,我们本应该传递两个数字给add函数,但是错误地传递了一个字符串。当我们尝试编译这段代码时,TypeScript编译器会提示错误信息:


Argument of type 'string' is not assignable to parameter of type 'number'.

通过这种类型检查,我们可以在开发过程中发现并解决类型相关的问题,避免了一些常见的错误。


1.2 类型推断的魅力


在TypeScript中,我们不仅可以显式地定义变量的类型,还可以利用类型推断的功能。当我们没有明确指定类型时,TypeScript会根据上下文和赋值语句自动推断变量的类型。这个特性不仅减少了我们编写代码时的工作量,还提供了代码的简洁性。


举个例子,考虑以下的代码:


const name = 'John';

在这个例子中,我们没有显式地指定name的类型,但TypeScript会自动推断它为字符串类型。这种类型推断让我们在编写代码时更加灵活,减少了类型注解的需求。


2. 更好的代码编辑体验


2.1 智能的代码补全和提示


使用TypeScript可以带来更好的代码编辑体验。由于TypeScript具备了静态类型信息,编辑器可以提供智能的代码补全和提示功能,减少我们编写代码时的出错几率。当我们输入一个变量名或者函数名时,编辑器会根据类型信息推断可能的属性和方法,并展示给我们。


例如,当我们有一个对象,并想获取它的属性时,编辑器会给出属性列表供我们选择。这在大型项目中尤其有用,因为我们可以快速了解某个对象的可用属性,而不必查阅文档或者浏览源代码。


2.2 重构的艺术


在大型项目中进行代码重构是一项棘手的任务。TypeScript提供了强大的重构能力,使得我们能够更轻松地重构代码而不担心破坏现有功能。在进行重构操作时,TypeScript会自动更新相关的类型注解,帮助我们在整个重构过程中保持代码的一致性。


举个例子,假设我们有一个函数:


function multiply(x: number, y: number): number {
return x * y;
}

现在我们决定将multiply函数的参数顺序调换一下。在传统的JavaScript中,我们需要手动修改所有调用multiply函数的地方。而在TypeScript中,我们只需要修改函数本身的定义,TypeScript会自动检测到这个变化,并指示我们需要更新的地方。


3. 生态系统的繁荣


3.1 类型定义文件


TypeScript支持类型定义文件(.d.ts),这些文件用于描述 JavaScript 库的类型信息。通过使用类型定义文件,我们可以在TypeScript项目中获得第三方库的类型检查和智能提示功能。这为我们在开发过程中提供了极大的便利,使得我们能够更好地利用现有的JavaScript生态系统。


例如,假设我们使用著名的React库进行开发。React有一个官方提供的类型定义文件,我们只需要将其安装到项目中,就能够获得对React的类型支持。这使得我们可以在编写React组件时,获得相关属性和方法的智能提示,大大提高了开发效率。


3.2 社区的支持


TypeScript拥有庞大而活跃的社区,开发者们不断地分享自己的经验和资源。这意味着我们可以轻松地找到许多优秀的库、工具和教程,帮助我们更好地开发和维护我们的前端项目。无论是遇到问题还是寻找最佳实践,社区都会给予我们及时的支持和建议。


4. 面向未来的技术栈


4.1 ECMAScript的最新特性支持


ECMAScript是JavaScript的标准化版本,不断更新以提供更多的语言特性和功能。TypeScript紧密跟随ECMAScript标准的发展,支持最新的语法和特性。这意味着我们可以在TypeScript项目中使用最新的JavaScript语言功能,而不必等待浏览器的支持。


例如,当ECMAScript引入了Promiseasync/await等异步编程的特性时,TypeScript已经提供了对它们的完整支持。我们可以在TypeScript项目中使用这些特性,而无需担心兼容性问题。


4.2 渐进式采用


对于已有的JavaScript项目,我们可以渐进式地引入TypeScript,而无需一次性对整个项目进行重写。TypeScript与JavaScript是高度兼容的,我们可以逐步将JavaScript文件改写为TypeScript文件,并为每个文件逐渐添加类型注解。这种渐进式的采用方式可以降低迁移的风险和成本,并让我们享受到TypeScript带来的好处。


结语


推荐使用TypeScript来提升我们的开发体验和代码质量。它强大的类型系统、智能的代码编辑体验、丰富的生态系统以及面向未来的技术栈,都使得TypeScript成为当今前端开发的首选语言之一。


但是,我们也需要明确TypeScript并非万能的解决方案。在某些特定场景下,纯粹的JavaScript可能更加合适。我们需要根据具体项目的需求和团队的情况,权衡利弊并做出适当的选择。



示例代码仅用于说明概念,可能不符合最佳实践。在实际开发中,请根据具体情况进行调整。


作者:ShihHsing
来源:juejin.cn/post/7243413799347798072

收起阅读 »

如果让你设计一个弹幕组件,你会怎么做???

web
大家好,我是前端小张同学,这次给大家更新一个开发中常见的需求,接下来我会将我的弹幕实现以及设计思路一步一步描述出来,并分享给大家,希望大家喜欢。 今天我们的主题是 ,用vue手写一个弹幕 1:关于弹幕设计思想 1.1 : 业务层 | 视图层(全局组件) 1.1...
继续阅读 »

大家好,我是前端小张同学,这次给大家更新一个开发中常见的需求,接下来我会将我的弹幕实现以及设计思路一步一步描述出来,并分享给大家,希望大家喜欢。


今天我们的主题是 ,用vue手写一个弹幕


1:关于弹幕设计思想


1.1 : 业务层 | 视图层(全局组件)


1.1.1 : 从业务角度来说,如果你设计的是全局弹幕组件,你要考虑以下几点。



  1. 容器的高度?

  2. 容器层次结构划分?

  3. 渲染弹幕的方式,使用组件的人应该传递什么数据?

  4. 是否支持全屏弹幕?

  5. 是否支持弹幕关闭和开启?

  6. 是否需要重置弹幕?

  7. 是否支持暂停弹幕?

  8. 是否需要集成发送功能?


设计方案考虑完整了以后,你将可以开始考虑 数据层的设计


1.2 数据层


1.2.1 : 从数据角度来说每一条弹幕无非是一个element,然后把弹幕内容放到这个element元素中,并且给 element 添加动画,那接下来,你应该这样考虑。




  1. 弹幕是JS对象?它的属性有哪些?




  2. 谁去管理这些弹幕?如何让他能够支持暂停和关闭?




  3. 你如何把后台的数据,与你前台的一些静态数据进行合并,创造出一个完整对象?




  4. 你怎么去渲染这些弹幕?




  5. 你想要几秒创建一次弹幕并在容器内显示和运行?




  6. 弹幕具备哪些灵活的属性?

    运行动画时间 , 用户自己发布的弹幕样式定制?
    又或者,弹幕需要几条弹道内运行等等这些你都需要考虑。




数据设计方案考虑完整了以后,你将可以开始考虑 数据管理层的设计


1.3 数据管理层


1.3.1 从管理的角度来说,外界调用某些方法,你即可快速的响应操作,例如外界调用 open 方法,你就播放弹幕,调用Stop方法,你就关闭弹幕 接下来,你应该考虑以下几点。



  1. 面向对象设计,应该提供哪些方法,具备哪些功能?

  2. 调用了指定的方法,应该怎么对数据进行操作。

  3. 如何对弹幕做性能优化?


到这里 , 我们设计方案基本完成,接下来我们可以开始编写代码。


2: 代码实现


2.1 : 数据层设计方案实现


我们需要构建一个 Barrage 类 ,我们每次去创建一个弹幕的时候都会 new Barrage,让他帮助我们生成一些弹幕属性。


export class Barrage {
constructor(obj) {
// 每次 new Barrage() 传入一个 后台返回的数据对象 obj
const { barrageId, speed, level, top, jumpUrl, barrageContent, animationPlayState, ...args } = obj
this.barrageId = barrageId; // id : 每条弹幕的唯一id
this.speed = speed; // speed : 弹幕运行的速度,由外界控制
this.level = level; // level : 弹幕的层级 --> 弹幕可分为设计可分为 上下 1 , 1 两个层级 ,可决定弹幕的显示是全屏还是半屏显示
this.top = top; // top :弹幕生成的位置相对于 level 的层级 决定 ,相对于 Level 层级 盒子距离顶部的位置
this.jumpUrl = jumpUrl; // jumpUrl :点击弹幕需要跳转的链接
this.barrageContent = barrageContent; // barrageContent : 弹幕的内容
this.animationPlayState = ''; // 设计弹幕 是否可 点击暂停功能
this.color = '#FFF' // 弹幕颜色
this.args = args // 除去Barrage类之外的一些数据属性全部丢到这里,例如后台返回的数据
}
}

2.1 : 数据管理层设计方案实现


2.1.1 :我们在这里实现了 , 弹幕的 增加删除初始化重置关闭开启功能


1. 实现弹幕开启功能.


BarrageManager.js


export class BarrageManager {

constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}
init(barrages) {
this.sourceBarrages = barrages
this.deleteCount = parseInt(this.sourceBarrages.length / deleteQuantity.FIFTY) // 计算可删除数量
this.lastDeleteCount = this.sourceBarrages.length % deleteQuantity.FIFTY // 计算 最后一次可删除数量
}
/**
*
* @param {*} barrages 接收一个弹幕数组数据
* @description 循环创建 弹幕对象 ,将后台数据与 创建弹幕的属性结合 存入弹幕数组
*/

loopCreateBarrage(barrages) {
const { rows, createTime, crearteBarrageObject } = this.barrageVue
let maxRows = rows / 2 // 最大的弹幕行数
this.timer = setInterval(() => {
for (let i = 0; i < 1; i++) {
let barrageItem = barrages[this.count]
if (this.row >= maxRows) { this.row = 0 } // 如果当前已经到了 最大的弹幕行数临界点则 回到第0 行弹道继续 创建
if (!barrageItem) return clearInterval(this.timer) // 如果取不到了则证明没数据了 , 结束弹幕展示
const item = crearteBarrageObject({ row: this.row, ...barrageItem }) // 添加对象到 弹幕数组中
this.addBarrage(item)
this.count++ // 用于取值 ,取了多少条
this.row++ // 用于弹道
}
}, createTime * 1000);
}
/**
* @param {*} barrages 传入一个弹幕数组数据
* @returns 无返回值
* @description 调用 该方法 开始播放弹幕
*/

open(barrages) {
if (barrages.length === 0) return
this.init(barrages)
this.loopCreateBarrage(this.sourceBarrages)
}
}

在这里我们初始化了一个 open 方法,并接收一个数组 ,并调用了 init 方法 去做初始化操作,并调用了 循环创建的方法,没 createTime 秒创建一条弹幕,加入到弹幕数组中。



  1. 连接视图层


2.1 : 视图层 | 业务层设计方案实现


index.vue


<template>
<div class="barrage">
<div class="barrage-container" ref="barrageContainer">
<div class="barrage-half-screen" ref="halfScreenContainer">
<template v-for="item in barrageFiltering.level1">
<barrage-item
:item="item" :class="{pausedAnimation : paused }"
:options='barrageTypeCallback(item)'
@destory="destoryBraageItem" :key="item.barrageId">

</barrage-item>
</template>
</div>
<div class="barrage-full-screen" v-if="fullScreen">
<template v-for="item in barrageFiltering.level2">
<barrage-item
:item="item" :class="{pausedAnimation : paused }"
:options='barrageTypeCallback(item)'
@destory="destoryBraageItem" :key="item.barrageId">

</barrage-item>
</template>
</div>
</div>
<user-input ref="publishBarrage" v-if="openPublishBarrage" @onBlur="handleBlur">
<template #user-operatio-right>
<!-- 处理兼容性问题 ios 和 安卓 触发点击事件 -->
<div class="send" @click="sendBarrage($event)" v-if="IOS">
<slot name="rightRegion"></slot>
</div>
<div class="send" @mousedown="sendBarrage($event)" v-else>
<slot name="rightRegion"></slot>
</div>
</template>
</user-input>
</div>

</template>
export default {
created () {
this.barrageManager = new BarrageManager(this)
},
mounted() {
// 初始化弹幕渲染数据
this.initBarrageRenderData();
},
data() {
return {
barrageManager : null,
isClickSend: false,
paused : false
};
},
methods : {
initBarrageRenderData() {
this.barrageManager.open(this.barrages);
},
},
computed : {
barrageFiltering() {
return {
level1:
this.barrageManager.barrages.filter(
item => item.level === barrageLevel.LEVEL1
) || [],
level2:
this.barrageManager.barrages.filter(
item => item.level === barrageLevel.LEVEL2
) || []
};
},
}
}

视图层知识点回顾


在这里我们在弹幕组件创建的时候去创建了一个 弹幕管理对象,并且在挂载的时候去初始化了以下 弹幕渲染的数据,于是我们调用了 弹幕管理类open方法,这样当组件挂载时,就会去渲染 barrageFiltering 数据,这里我们是在管理类中拿到了管理类中循环创建的数据。


open 方法实现


到这里我们的弹幕的开启基本上已经完成了,可以看得出,如果你是这样设计的,你只需要在组件中调用管理类的一些方法,它就能帮你完成一些功能。


3: 实现弹幕关闭功能


barrageManager.js


 class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}

/**
* @return 无返回值
* @description 调用close 方法 关闭弹幕
*/

close() {
clearInterval(this.timer)
this.removeAllBarrage()
}
/**
* @description 删除全部的弹幕数据
*/

removeAllBarrage() {
this.barrages = []
}
}


关闭功能知识点回顾


在这里我们可以看到,关闭弹幕的功能其实很简单,你只需要把开启弹幕时的定时器关闭,并且把弹幕数组数据清空就可以了


4: 实现弹幕添加功能


index.vue



addBarrage(barrageContent) {
// 获取当前 定时器正在创建的 一行
let currentRow = this.barrageManager.getRow();
let row = currentRow === this.rows / 2 ? 0 : currentRow + 1;
if (row === this.rows / 2) {
row = 0;
}
let myBarrage = {
row,
barrageId: '1686292223004',
barrageContent,
style: this.style,
type: "mySelf", // 用户自己发布的弹幕类型
barrageCategory: this.userBarrageType
};

const item = this.crearteBarrageObject(myBarrage);

this.barrageManager.addBarrage(item); // 数据准备好了 调用添加方法

console.info("发送成功")

this.barrageManager.setRow(row + 1);
},

barrageManager.js


 class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}
/**
*
* @param {*} obj 合并完整的的弹幕对象
* @param {...any} args 开发者以后可能需要传递的剩余参数
*/

addBarrage(obj, ...args) {
const barrage = new Barrage(obj, ...args)
this.barrages.push(barrage)
}
}

添加功能知识点回顾


在这里我们可以看到,添加的时候,我们 组件 只需要去调用 addBarrage 方法进行弹幕添加,并且在调用的过程中我们去 new Barrage 这个类 , 也就是我们之前准备好的 弹幕数据类 | 数据层设计


5: 实现弹幕删除功能


class BarrageManager {
constructor(barrageVue) {
this.barrages = []; // 填弹幕的数组
this.barragesIds = [] // 批量删除弹幕的数组id
this.sourceBarrages = [] // 源弹幕数据
this.timer = null //控制弹幕的开启和关
this.barrageVue = barrageVue // 弹幕组件实例
this.deleteCount = 0, // 销毁弹幕的总数
this.lastDeleteCount = 0, // 最后可销毁的数量
this.row = 0,
this.count = 0
}

/**
*
* @param {*} barrageId // 入参 弹幕id
* @returns 无返回值
* @description 添加需要批量删除的 id 到 批量删除的栈中 barragesIds
*/

addBatchRemoveId(barrageId) {
this.barragesIds.push(barrageId)
this.batchRemoveHandle()
}
/**
*
* @param {*} start 你需要从第几位开始删除
* @param {*} deleteCount // 删除的总数是多少个
* @returns 无返回值
*/

batchRemoveBarrage(start, deleteCount) {
if (this.barrages.length === 0) return
this.barrages.splice(start, deleteCount)
}
batchRemoveId(start, deleteCount) {
if (this.barragesIds.length === 0) return
this.barragesIds.splice(start, deleteCount)
}
/**
* @param {*} barrageId 弹幕 id 针对单个删除弹幕时 使用
*/

removeBarrage(barrageId) {
let index = this.barrages.findIndex(item => item.barrageId === barrageId)
this.barrages.splice(index, 1)
}
/**
* @description 删除全部的弹幕数据
*/

removeAllBarrage() {
this.barrages = []
}
// 批量移除逻辑处理
batchRemoveHandle() {
if (this.deleteCount === 0 || this.deleteCount === 0) {
if (this.barragesIds.length === this.lastDeleteCount) {
this.batchRemoveBarrage(0, this.lastDeleteCount)
this.batchRemoveId(0, this.lastDeleteCount)
}
} else {
if (this.barragesIds.length === deleteQuantity.FIFTY) {
this.batchRemoveBarrage(0, deleteQuantity.FIFTY)
this.batchRemoveId(0, deleteQuantity.FIFTY)
this.deleteCount--
}
}
}
}

删除功能知识点回顾


在这里我们可以看到,删除的时候我们把每一个弹幕id加入到了一个数组中 , 当 弹幕id数组长度达到我想要删除的数量的时候, 调用 splice 方法 执行批量删除操作,当数据发生更新,视图也会更新,这样我们只需要执行一次dom操作,不需要每一次删除弹幕更新dom,造成不必要的性能消耗。


5: 实现弹幕重置功能


到这里,我相信你已经明白了我的设计,如果现在让你实现一个 重置弹幕方法 你会怎么做 ? 是不是只需要,调用一下 close 方法 , 然后再去 调用 open方法就可以了,ok 接下来我会将完整版代码 放入我的github仓库,小伙伴们可以去拉取 仓库链接,具体代码还需要小伙伴们自己从头阅读一次,这里只是说明了部分内容 , 阅读完成后 , 你就会彻底理解。


关于 barrageTypeCallback 函数


这个方法主要是可以解决弹幕样式定制的问题,你可以根据每个弹幕的类型 做不同的样式对象返回,我们会自动帮你渲染。


barrageTypeCallback ( {args} ) {

const { barrageCategary } = args

if(barrageCategary === 'type1'){

retun {
className : 'classOne',
children : {
show : false
i : {
showIcon : false,
style : {
color : 'red'
}
}
}
}
}
else{

return { className : 'default' }
}
}




结束语


前面的所有代码只是想告诉大家这个设计思想,当你的思维模型出来以后,其实很轻松。


我是 前端小张同学

作者:前端小张同学
来源:juejin.cn/post/7243680440694980668
期待你的关注,谢谢。

收起阅读 »

为什么推荐用svg而不用icon?

web
为什么要用svg而没有用icon? 使用背景: 1.因为svg图标在任何设备下都可以高清显示,不会模糊。而icon会在显卡比较低的电脑上有显示模糊的情况 2.svg图标在页面render时 速度会比icon稍微快一点 3.实现小程序换肤功能 ;方案见:ht...
继续阅读 »

为什么要用svg而没有用icon?


使用背景:


图片.png



1.因为svg图标在任何设备下都可以高清显示,不会模糊。而icon会在显卡比较低的电脑上有显示模糊的情况


2.svg图标在页面render时 速度会比icon稍微快一点
3.实现小程序换肤功能 ;方案见:http://www.yuque.com/lufeilizhix…



// svg在html里的使用示例01
<div>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>home</title>
<path d="M32 18.451l-16-12.42-16 12.42v-5.064l16-12.42 16 12.42zM28 18v12h-8v-8h-8v8h-8v-12l12-9z"></path>
</svg>
</div>


SVG基础可参考:http://www.yuque.com/lufeilizhix…


Svg-inline的使用


//示例02

import iconShop from '../assets/menuIcon/shop.svg?inline'
import iconCustomer from '../assets/menuIcon/customer.svg?inline'
import iconCustomerService from '../assets/menuIcon/customerService.svg?inline'
import iconNuCoin from '../assets/menuIcon/nuCoin.svg?inline'
import iconBanner from '../assets/menuIcon/banner.svg?inline'
import iconAccount from '../assets/menuIcon/account.svg?inline'
import iconDataReport from '../assets/menuIcon/dataReport.svg?inline'
import iconVera from '../assets/menuIcon/banner_01.svg?inline'

inline svg是目前前端图标解决方案的最优解(当然不仅限于图标),而且使用方式也及其简单,只要将svg图标代码当成普通的html元素来使用即可,如:


<!-- 绘制右箭头 -->
<svg viewBox="0 0 1024 1024" height="1em" width="1em" fill="currentColor">
<path d="M665.6 512L419.84 768l-61.44-64 184.32-192L358.4 320l61.44-64 184.32 192 61.44 64z" />
</svg>

<!-- 绘制边框 -->
<svg viewBox="0 0 20 2" preserveAspectRatio="none" width="100%" height="2px">
<path d="M0 1L20 1" stroke="#000" stoke-width="2px"></path>
</svg>

注意: 新版chrome不支持 # , 需要改成%23 ;stroke="%23000"

作为图片或背景使用时


 icon: https://www.baidu.com+ '/icons/icon_01.svg' 
<image class="headIcon" src="data:image/svg+xml,{{icon}}"></image>
**特别注意 需要把img标签换成image标签**

将上面的代码插入html文档即可以很简单地绘制出一些图标。
正常情况下会将svg保存在本地,具体的页面中导入,参考示例02 作为组件使用;目的是可复用
一般来说,使用inline svg作为图标使用时,想要保留svg的纵横比,可以只指定width属性,但是一般为了清晰都同时指定height属性。但如果是像上面绘制边框这种不需要保留纵横比的情形,可将preserveAspectRatio设置为none


优势与使用方式


从示例01可以看到,将svg直接作为普通html元素插入文档中,其本质和渲染出一个div、span等元素无异,天生具有渲染快、不会造成额外的http请求等优势,除此之外还有以下优势之处:


样式控制更加方便;
inline svg顶层的元素会设置以下几个属性:


height=“1em” width=“1em” 可以方便地通过设置父元素的font-size属性控制尺寸


fill=“currentColor” 可以方便地根据父元素或自身的color属性控制颜色


但是我们也可以为其内部的子元素单独设置样式 参考


注意事项


如需对svg中各部分分别应用样式,则在设计svg时最好不要将各部分都编于一组,可以将应用相同样式的部分进行分别编组,其他不需要设置样式的部分编为一组,这样我们在应用样式时,只需为对应的标签设置class属性即可。


一般在拿到svg文件后,推荐使用svgo优化svg代码,节省体积,但是如果我们需要针对性设置样式时则需要谨慎使用,因为优化代码会进行路径合并等操作,可能我们想要设置的子元素已经不是独立的了。


inline svg的复用及组件化


同一个inline svg必须能够进行复用,将需要复用inline svg封装成组件


// 使用inline svg组件
import AnySvgIcon from './inline-svg-component'
<AnySvgIcon width="16px" height="16px" />

参考:


inline svg和字体图标的对比


字体图标

的使用与设计

收起阅读 »

这几个让代码清新的魔法,让领导追着给我涨薪

web
清晰易维护的代码对于任何软件项目的长期成功和可扩展性至关重要。它提升团队成员之间的协作效率,减少错误的可能性,并使代码更易于理解、测试和维护。在本博文中,我们将探讨一些编写清晰易维护的 JavaScript 代码的最佳实践,并提供代码示例以阐明每个实践方法。 ...
继续阅读 »

清晰易维护的代码对于任何软件项目的长期成功和可扩展性至关重要。它提升团队成员之间的协作效率,减少错误的可能性,并使代码更易于理解、测试和维护。在本博文中,我们将探讨一些编写清晰易维护的 JavaScript 代码的最佳实践,并提供代码示例以阐明每个实践方法。


1. 一致的代码格式:


一致的代码格式对于可读性非常重要。它有助于开发人员更快地理解代码,提升协作效果。使用一致且被广泛接受的代码风格指南,比如 ESLint 提供的指南,并配置你的编辑器或 IDE 以自动格式化代码。
示例:


// 错误的格式化
function calculateSum(a,b){return a+b; }

// 正确的格式化
function calculateSum(a, b) {
return a + b;
}

2. 有意义的变量和函数命名:


为变量、函数和类使用有意义且描述性的名称。避免使用单个字母或容易引起他人困惑的缩写。这种做法提高了代码的可读性,并减少了对注释的需求。
示例:


// 错误的命名
const x = 5;

// 正确的命名
const numberOfStudents = 5;

3. 模块化和单一职责原则:


遵循单一职责原则,为函数和类设定单一、明确的职责。这种做法提高了代码的可重用性,并使其更易于测试、调试和维护。
示例:


// 错误的做法
function calculateSumAndAverage(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
const average = sum / numbers.length;
return [sum, average];
}

// 正确的做法
function calculateSum(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}

function calculateAverage(numbers) {
const sum = calculateSum(numbers);
const average = sum / numbers.length;
return average;
}

4. 避免全局变量:


尽量减少使用全局变量,因为它们可能导致命名冲突,并使代码更难以理解。相反,封装你的代码到函数或模块中,并尽可能使用局部变量。
示例:


// 错误的做法
let count = 0;

function incrementCount() {
count++;
}

// 正确的做法
function createCounter() {
let count = 0;

function incrementCount() {


count++;
}

return {
incrementCount,
getCount() {
return count;
}
};
}

const counter = createCounter();
counter.incrementCount();

5. 错误处理和鲁棒性:


优雅地处理错误,并提供有意义的错误信息或适当地记录它们。验证输入,处理边界情况,并使用正确的异常处理技术,如 try-catch 块。
示例:


// 错误的做法
function divide(a, b) {
return a / b;
}

// 正确的做法
function divide(a, b) {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}

try {
const result = divide(10, 0);
console.log(result);
} catch (error) {
console.error(error.message);
}

6. 避免重复代码:


代码重复不仅会导致冗余代码,还会增加维护和修复错误的难度。将可重用的代码封装到函数或类中,并努力遵循 DRY(Don't Repeat Yourself)原则。如果发现自己在复制粘贴代码,请考虑将其重构为可重用的函数或模块。
示例:


// 错误的做法
function calculateAreaOfRectangle(length, width) {
return length * width;
}

function calculatePerimeterOfRectangle(length, width) {
return 2 * (length + width);
}

// 正确的做法
function calculateArea(length, width) {
return length * width;
}

function calculatePerimeter(length, width) {
return 2 * (length + width);
}

7. 明智地使用注释:


干净的代码应该自解释,但有些情况下需要注释来提供额外的上下文或澄清复杂的逻辑。谨慎使用注释,并使其简洁而有意义。注重解释“为什么”而不是“如何”。
示例:


// 错误的做法
function calculateTotalPrice(products) {
// 遍历产品
let totalPrice = 0;
for (let i = 0; i < products.length; i++) {
totalPrice += products[i].
price;
}
return totalPrice;
}

// 正确的做法
function calculateTotalPrice(products) {
let totalPrice = 0;
for (let i = 0; i < products.length; i++) {
totalPrice += products[i].
price;
}
return totalPrice;
// 总价格通过将数组中所有产品的价格相加来计算。
}

8. 优化性能:


高效的代码提升了应用程序的整体性能。注意不必要的计算、过度的内存使用和潜在的瓶颈。使用适当的数据结构和算法来优化性能。使用类似 Chrome DevTools 的工具对代码进行性能分析和测量,以识别并相应地解决性能问题。


示例:


// 错误的做法
function findItemIndex(array, target) {
for (let i = 0; i < array.length; i++) {
if (array[i] === target) {
return i;
}
}
return -1;
}

// 正确的做法
function findItemIndex(array, target) {
let left = 0;
let right = array.length - 1;

while (left <= right) {
const mid = Math.floor((left + right) / 2);

if (array[mid] === target) {
return mid;
}

if (array[mid] < target) {
left = mid +
1;
}
else {
right = mid -
1;
}
}

return -1;
}

9. 编写单元测试:


单元测试对于确保代码的正确性和可维护性非常重要。编写自动化测试以覆盖不同的场景和边界情况。这有助于尽早发现错误,便于代码重构,并对修改现有代码充满信心。使用像 Jest 或 Mocha 这样的测试框架来编写和运行测试。
示例(使用 Jest):


// 代码
function sum(a, b) {
return a + b;
}

// 测试
test('sum 函数正确地相加两个数字', () => {
expect(sum(2, 3)).toBe(5);
expect(sum(-1, 5)).toBe(4);
expect(sum(0, 0)).toBe(0);
});

10. 使用函数式编程概念:


函数式编程概念,如不可变性和纯函数,可以使代码更可预测且更易于理解。拥抱不可变数据结构,并尽量避免对对象或数组进行突变。编写无副作用且对于相同的输入产生相同输出的纯函数,这样更容易进行测试和调试。
示例:


// 错误的做法
let total = 0;

function addToTotal(value) {
total += value;
}

// 正确的做法
function addToTotal(total, value) {
return total + value;
}

11. 使用 JSDoc 文档化代码:


使用 JSDoc 来为函数、类和模块编写文档。这有助于其他开发人员理解你的代码,并使其更易于维护。


/**
* 将两个数字相加。
* @param {number} a - 第一个数字。
* @param {number} b - 第二个数字。
* @returns {number} 两个数字的和。
*/

function add(a, b) {
return a + b;
}

12. 使用代码检查工具和格式化工具:


使用 ESLint 和 Prettier 等工具来强制执行一致的代码风格,并在问题出现之前捕获潜在问题。


// .eslintrc.json
{
"extends": ["eslint:recommended", "prettier"],
"

plugins"
: ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}

// .prettierrc.json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}

结论:


编写清晰且易于维护的代码不仅仅是个人偏好的问题,而是一项专业责任。通过遵循本博文中概述的最佳实践,您可以提高 JavaScript 代码的质量,使其更易于理解、维护和协作,并确保软件项目的长期成功。在追求清晰且易于维护的代码时,请牢记一致性、可读性、模块化和错误处理这些关键原则。祝你编码愉快!

原文:dev.to/wizdomtek/b…
翻译 / 润色:ssh

作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7243680592192782393

收起阅读 »

大专前端,三轮面试,终与阿里无缘

web
因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
继续阅读 »

因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


进入到面试环节,首先是两道笔试题,算是前置面试:


第一道题目是算法题:


提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:


const result = query(list)
.where(item => item.age > 18)
.sortBy('id')
.groupBy('name')
.execute();

console.log(result);

具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


第二道题目是场景题:


要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


过了几分钟,对面直接回复笔试过了,然后约了面试。


一面:




  • 自我介绍


    这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 七层网络模型、和 DNS 啥的


    计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。




  • 然后问了一些 host 相关的东西



    • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。




  • React 代码层的优化可以说一下么?



    • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。




  • 说一下 useMemouseCallback 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 说一下 useEffectuseLayoutEffect 有什么区别



    • 很基础的问题,这里就不展开说了。




  • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



    • 很基础的问题,这里就不展开说了。




  • 如果在 if 里面写 useEffect 会有什么表现?



    • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。




  • 说一下 React 的 Fiber 架构是什么




    • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:




      1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。




      2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。




      3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。




      (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)






  • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



    • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。




  • 个人方面有什么规划吗?



    • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。




  • 对未来的技术上有什么规划呢?



    • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。




  • 能不能从技术的角度讲一下你工作中负责业务的复杂度?




    • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


      协同文档在前端的难点主要有两个方面:




      1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:




        • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。




        • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。




        • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。






      2. 性能问题




        • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。




        • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。










  • 可以讲一下你在工作中技术上的建设吗?



    • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……




  • 你有什么想问我的吗?



    • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



二面:




  • 自我介绍



    • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。




  • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



    • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。




  • object 的原型指向谁?



    • 回答了 null。(我也不知道对不对,瞎说的)




  • 能说一下原型链的查找过程吗?



    • 磕磕绊绊背了一段八股文,这里省略吧。




  • node 的内存管理跟垃圾回收机制有了解过吗?




    • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:




    • 首先分两种情况:V8 将内存分成 新生代空间老生代空间




      • 新生代空间: 用于存活较短的对象




        • 又分成两个空间: from 空间 与 to 空间




        • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



          • 存活的对象从 from space 转移到 to space

          • 清空 from space

          • from space 与 to space 互换

          • 完成一次新生代 GC






      • 老生代空间: 用于存活时间较长的对象




        • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



          • 经历过一次以上 Scavenge GC 的对象

          • 当 to space 体积超过 25%




        • 标记清除算法:标记存活的对象,未被标记的则被释放



          • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

          • 并发标记:不阻塞 js 执行










  • js 中的基础类型和对象类型有什么不一样?



    • 基础类型存储在栈中,对象类型存储在堆中。




  • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



    • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。




  • 简单说一下 useEffect 的用法:



    • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。




  • 说一下 useEffect 的返回值用来做什么?



    • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。




  • 你知道 useEffect 第二个参数内部是怎么比较的吗?



    • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)




  • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



    • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。




  • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



    • 磕磕绊绊回答出来了。




  • 追问:http 是在哪一层实现的?



    • 应用层。




  • 说一下 getpost 有什么区别?



    • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……




  • 说一下浏览器输入 url 到页面加载的过程:




    • 输入网址发生以下步骤:



      1. 通过 DNS 解析域名的实际 IP 地址

      2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

      3. 强缓存协商缓存都没有命中,则返回请求结果

      4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

      5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

      6. 接下来是浏览器解析 HTML,开始渲染页面




    • 顺便说了渲染页面的过程:



      1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

      2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

      3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

      4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

      5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。




    • 渲染完成之后,开始执行其它任务:



      1. dom 操作

      2. ajax 发起的 http 网络请求等等……

      3. 浏览器处理事件循环等异步逻辑等等……






  • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



    • 比较经典的面试题,说了 flexfloat 两种方式。




  • 项目难点



    • 和一面一样,说了协同文档的两大难点,这里就不重复了。




  • 你有什么想问我的吗?



    • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。




  • 最后问了期望薪资什么的,然后就结束了。




二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ


今天已经是第二天了,目前没有下文,看起来二面是过了,但是因为学历不够,中止了三面的流程,基本上是失败了,我也不会报有什么希望了,所以写个面经记录一下。


最后,给自己打个广告!求职求职求职!!!


社交信息:



个人优势:



  • antd 团队成员、ahooks 团队成员,活跃于 github 开源社区,给众多知名大型开源项目提交过 PR,拥有丰富的 React + TS 实战经验

  • 熟悉前端性能优化的实现,例如代码优化、打包优化、资源优化,能结合实际业务场景进行优化

  • 熟悉 webpack / vite 等打包工具的基本配置, 能够对以上工具进行二次封装、基于以上工具搭建通用的开发环境

  • 熟悉 prettier / eslint 基本配置,有良好且严格的编码习惯,唯客户论,实用主义者

  • 熟悉代码开发到上线全流程,对协同开发分支管理项目配置等都有较深刻的最佳实践



可内推的大佬们麻烦联系我!在 github 主页有联系方式,或者直接在掘金私聊我也可,谢谢!!


作者:三年没洗澡
来源:juejin.cn/post/7239715208792342584

收起阅读 »

vue打包脚本:打包前判定某个npm link依赖库是否是指定分支

web
1. 需求场景 有一个项目A,它依赖项目B 项目B是本地开发的,也在本地维护 项目A通过npm link链接到了项目B 随着项目A的不断迭代,功能升级较大创建了新的分支项目A-dev 项目A-dev分支也要求项目B也要创建出项目B-dev分支与之对应 项目A...
继续阅读 »

1. 需求场景



  • 有一个项目A,它依赖项目B

  • 项目B是本地开发的,也在本地维护

  • 项目A通过npm link链接到了项目B

  • 随着项目A的不断迭代,功能升级较大创建了新的分支项目A-dev

  • 项目A-dev分支也要求项目B也要创建出项目B-dev分支与之对应

  • 项目A项目A-dev都在产品同时运行,遇到问题都要在各自分支同步修复缺陷


在启动或者打包的时候需要特别小心项目B处在什么分支,错配分支就会导致项目启动失败或报错,于是需要有一个脚本帮我在项目启动时,检查依赖脚本是否在正确的分支上,如果不在,就自动将依赖分支切换到对应需要的分支上。


2. 脚本编写


2.1 脚本思路:



  • 先去指定项目B的文件夹中查看该项目处于哪一个分支

  • 通过动态参数获得本地启动或打包的是哪一个分支,和当前项目分支进行比对

  • 如果不是当前分支,就切换到要求的分支, 切到对应分支后,再执行install操作

  • 如果是当前分支则直接跳过分支切换操作,直接往下走


2.2 脚本创建


在vue项目根目录创建一个脚本文件check-dependency.js


下面脚本中分支名@tiamaes/t4-framework就对应项目B


const t4FrameworkFilePath = "D:/leehoo/t4-framework"; //本地@tiamaes/t4-framework目录地址

const { spawnSync } = require("child_process");

const branchName = process.argv[2];//获取参数分支名,打包时需要传递进来
if (!branchName) {
console.error("Branch name is not specified.");
process.exit(1);
}

// 获取当前分支
const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: t4FrameworkFilePath });
if (result.status !== 0) {
console.error("Failed to get current branch:", result.stderr.toString());
process.exit(1);
}
const currentBranch = result.stdout.toString().trim();

// 判断分支是否需要切换
if (currentBranch !== branchName) {
console.log(`@tiamaes/t4-framework is not in ${branchName} branch. Switching to ${branchName} branch...`);
const checkoutResult = spawnSync("git", ["checkout", branchName], { cwd: t4FrameworkFilePath });
if (checkoutResult.status !== 0) {
console.error(`Failed to switch to ${branchName} branch:`, checkoutResult.stderr.toString());
process.exit(1);
}

// 安装依赖包
console.log("Installing dependencies...");
const installResult = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install"], { cwd: t4FrameworkFilePath });
if (installResult.status !== 0) {
console.error("Failed to install dependencies:", installResult.stderr.toString());
process.exit(1);
}
console.log("Dependencies installed successfully.");
}

console.log(`@tiamaes/t4-framework is in ${branchName} branch. Proceeding to build...`);

process.exit(0);


3. package.json引用


在该脚本的script中增加引用方式,在项目启动或打包的时候都要执行一次node check-dependency.js,其后跟随的是项目B的分支名,我这里是erp-dev和erp-m1两个分支


"scripts": {
"serve:erp": "node check-dependency.js erp-dev && npm run link:local && vue-cli-service serve --mode development.erp",
"serve:m1": "node check-dependency.js erp-m1 && npm run link:local && vue-cli-service serve --mode development.m1",
"build:erp": "node check-dependency.js erp-dev && npm run link:local && vue-cli-service build --report --mode production.erp",
"build:m1": "node check-dependency.js erp-m1 && npm run link:local && vue-cli-service build --mode production.m1",
"link:local": "npm link @tiamaes/t4-framework",
},

下面是执行效果


Video_2023-06-09_202602.gif


image.png

收起阅读 »

Vue和React权限控制的那些事

web
自我介绍 看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。 前言 无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。 ...
继续阅读 »

自我介绍


看官们好,我叫JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。


前言


无论是后台管理系统,还是面向C端的产品,权限控制都是日常工作中常见的需求。在此梳理一下权限控制的那些逻辑,以及在Vue/React框架下是有什么样的解决方案。


什么是权限控制?


现在基本上都是基于RBAC权限模型来做权限控制


一般来说权限控制就是三种




  • 页面权限:说白了部分页面是具备权限的,没权限的无法访问




  • 操作权限:增删改查的操作会有权限的控制




  • 数据权限:不同用户看到的、数据是不一样的,比如一个列表,不同权限的查看这部分数据,可能有些字段是**脱敏的,有些条目无法查看详情,甚至部分条目是无法查看




那么对应到前端的维度,常见的就4种




  • 权限失效(无效)(token过期/尚未登录)




  • 页面路由控制,以路由为控制单位




  • 页面上的操作按钮、组件等的权限控制,以组件/按钮为最小控制单位




  • 动态权限控制,比如1个列表,部分数据可以编辑,部分部分不可编辑




image.png


⚠️注意: 本文一些方案 基于 React18 React-Router V6 以及 Vue3 Vue-Router V4


⚠️Umi Max 这种具备权限控制系统的框架暂时不在讨论范围内~~


前置条件


由于市面上各家实现细节不一样,这里只讨论核心逻辑思路,不考虑细节实现


无论框架如何,后端根据RABC角色权限这套逻辑下来的,会有如下类似的权限标识信息,可以通过专门的接口获取,或者跟登录接口放在一起。


image.png


然后根据这些数据,去跟路由,按钮/组件等,比对产生真正的权限


像这种权限标识一般都存在内存当中(即便存在本地存储也需要加密,不过其实真正的权限控制还是需要后端来控),一般都是全局维护的状态,配合全局状态管理库使用。


权限失效(无效)


image.png


这种场景一般是在发送某些请求,返回过期状态


或者跟后端约定一个过期时间(这种比较不靠谱)


通常是在 全局请求拦截 下做,整理一下逻辑


路由级别权限控制


通常前端配好的路由可以分为 2 种:


一种是静态路由:即无论什么权限都会有的,比如登录页、404页这些


另一种是动态路由:虽然叫动态路由,其实也是在前端当中定义好了的。说它是动态的原因是根据后端的权限列表,要去做动态控制的


vue实现


在vue体系下,可以通过路由守卫以及动态添加路由来实现


动态路由


先配置静态路由表 , 不在路由表内的路由重定向到指定页(比如404)


在异步获取到权限列表之后,对动态部分的路由进行过滤之后得到有权限的那部分路由,再通过router.addRoute()添加到路由实例当中。


流程为:


(初始化时) 添加静态路由 --> 校验登录态(比如是否有token之类的) --> 获取权限列表(存到vuex / pinia) --> 动态添加路由(在路由守卫处添加)



rightsRoutesList // 来自后端的当前用户的权限列表,可以考虑存在全局状态库
dynamicRoutes // 动态部分路由,在前端已经定义好, 直接引入

// 对动态路由进行过滤,这里仅用path来比较
// 目的是添加有权限的那部分路由,具体实现方案自定。
const generateRoute = (rightsRoutesList)=>{
//ps: 这里需要注意下(如果有)嵌套路由的处理
return dynamicRoutes.filter(i=>
rightsRoutesList.some(path=>path === i.path)
)
}

// 拿到后端返回的权限列表
const getRightsRoutesList = ()=>{
return new Promise(resolve=>{
const store = userStore()
if(store.rightsRoutesList){
resolve(store.rightsRoutesList)
}else{
// 这里用 pinia 封装的函数去获取 后端权限列表
const rightsRoutesList = await store.fetchRightsRoutesList()
resolve(rightsRoutesList)
}
}
}

let hasAddedDynamicRoute = false
router.beforeEach(async (to, from) => {
if(hasAddedDynamicRoute){
// 获取
const rightsRoutesList = await getRightsRoutesList()

// 添加到路由示例当中
const routes = generateRoute(rightsRoutesList)
routes.forEach(route=>router.addRoute(route))
// 对于部分嵌套路由的子路由才是动态路由的,可以
router.addRoute('fatherName',route)
hasAddedDynamicRoute = true
}
// 其他逻辑。。。略


next({...to})
}


踩坑

通过动态addRoute去添加的路由,如果你F5刷新进入这部分路由,会有白屏现象。


image.png


因为刷新进入的过程经历了 异步获取权限列表 --> addRoute注册 的过程,此时跳转的目标路由就和你新增的路由相匹配了,需要去手动导航。


因此你需要在路由守卫那边next放行,等下次再进去匹配到当前路由


你可以这么写


router.beforeEach( (to,from,next) => {
// ...其他逻辑

// 关键代码
next({...to})
})


路由守卫


一次性添加所有的路由,包括静态和动态。每次导航的时候,去对那些即将进入的路由,如果即将进入的路由是在动态路由里,进行权限匹配。


可以利用全局的路由守卫


router.beforeEach( (to,from,next) => {
// 没有访问权限,则重定向到404
if(!hasAuthorization(to)){
// 重定向
return '/404'
}
})

也可以使用路由独享守卫,给 权限路由 添加


    // 路由表
const routes = [
//其他路由。。。略

// 权限路由
{
path: '/users/:id',
component: UserDetails,
// 定义独享路由守卫
beforeEnter: (to, from) => {
// 如果没有许可,则
if(!hasAuthorization(to)){
// 重定向到其他位置
return '/404'
}
},
},
]


react实现


在react当中,一般先将所有路由添加好,再通过路由守卫来做权限校验


局部守卫loader


React-router 当中没有路由守卫功能,可以利用v6版本的新特性loader来做,给权限路由都加上对应的控制loader


import { redirect, createBrowserRouter, RouterProvider } from 'react-router-dom'


const router = createBrowserRouter([
{
// it renders this element
element: <Team />,

// when the URL matches this segment
path: "teams/:teamId",

// with this data loaded before rendering
loader: async ({ request, params }) => {
// 拿到权限
const permission = await getPermission("teams/:teamId")
// 没有权限则跳到404
if(!permission){
return redirect('/404')
}
return null
},

// and renders this element in case something went wrong
errorElement: <ErrorBoundary />,
},
]);

// 使用
function RouterView (){
return (
<RouterProvider router={router}/>
)
}



包装路由(相当于路由守卫)


配置路由组件的时候,先渲染包装的路由组件


image.png


在包装的组件里做权限判断


function RouteElementWrapper({children, path, ...props }: any) {
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(()=>{
// 判断登录态之类的逻辑

// 如果要获取权限,则需要setIsLoading,保持加载状态

// 这里判断权限
if(!hasAccess(path)){
navigate('/404')
}
},[])
// 渲染routes里定义好的路由
return isLoading ? <Locading/> : children
}

按钮(组件)级别权限控制


组件级别的权限控制,核心思路就是 将判断权限的逻辑抽离出来,方便复用。


vue 实现思路


在vue当中可以利用指令系统,以及hook来实现


自定义指令


指令可以这么去使用


<template>
<button v-auth='/site/config.btn'> 编辑 </button>
</template>

指令内部可以操作该组件dom和vNode,因此可以控制显隐、样式等。


hook


同样的利用hook 配合v-if 等指令 也可以实现组件级颗粒度的权限控制


<template>
<button v-if='editAuth'> 权限编辑 </button>
<div v-else>
无权限时做些什么
</div>

<button v-if='saveAuth'> 权限保存 </button>
<button v-if='removeAuth'> 权限删除 </button>
</template>
<script setup>
import useAuth from '@/hooks/useAuth'
// 传入权限
const [editAuth,saveAuth,removeAuth] = useAuth(['edit','save','remove'])
</script>


hook里的实现思路: 从pinia获取权限列表,hook里监听这个列表,并且匹配对应的权限,同时修改响应式数据。


react 实现思路


在React当中可以用高阶组件和hook的方式来实现


hook


定义一个useAuth的hook


主要逻辑是: 取出权限,然后通过关联响应式,暴露出以及authKeys ,hasAuth函数


export function useAuth(){
// 取出权限 ps: 这里从redux当中取
const authData = useSelector((state:any)=>state.login)
// 取出权限keys
const authKeys = useMemo(()=>authData.auth.components ?? [],[authData])
// 是否拥有权限
const hasAuth = useCallback(
(auths:string[]|string)=>(
turnIntoList(auths).every(auth=>authKeys.includes(auth))
),
[authKeys]
)
const ret:[typeof authKeys,typeof hasAuth] = [authKeys,hasAuth]
return ret
}

使用


const ProductList: React.FC = () => {
// 引入
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth("edit"), [hasAuth]);

// ...略
return (
<>
{ authorized ? <button> 编辑按钮(权限)</button> : null}
</>

)
};


权限包裹组件


可以跟进一步,依据这个权限hook,封装一层包裹组件


const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
// 控制显隐
return authorized ? children : null
}

使用


<AuthWrapper auth='edit'>
<button> 编辑按钮(AuthWrapper) </button>
</AuthWrapper>

还可以利用renderProps特性


const AuthWrapper:React.FC<{auth:string|string[],children:JSX.Element}> = ({auth, children})=>{
const [, hasAuth] = useAuth();
// 计算是否有权限
const authorized = useMemo(() => hasAuth(auth), [hasAuth]);
+ if(typeof children === 'function'){
+ return children(authorized)
+ }
// 控制显隐
return authorized ? children : null
}

<AuthWrapper auth='edit'>
{
(authorized:boolean)=> authorized ? <button> 编辑按钮(rederProps) </button> : null
}
</AuthWrapper>

动态权限控制


这种主要是通过动态获取到的权限标识,来控制显隐、样式等。可以根据特定场景做特定的封装优化。主要逻辑其实是在后端处理。


结尾


可以看到在两大框架下实现权限控制时,思路和细节上还是稍稍有点不一样的,React给人的感觉是手上的积木更加零碎的一点,有些功能需要自己搭起来。相反Vue给人的感觉是面面俱到,用起来下限会更高。


最后


如果大家有什么想法和思考,欢迎在评论区留言~~。


另外:本人经验有限,

作者:JetTsang
来源:juejin.cn/post/7242677017034915899
如果有错误欢迎指正。

收起阅读 »

Vue3项目实现图片实现响应式布局和懒加载

web
Element实现响应式布局 分享一下,在Vue3项目中实现响应式布局(一行显示7列)。在这个例子中,我参考了Element官方的Layout布局,使用el-card来放置图片。 利用分栏布局,el-row行上设置每列的间隔gutter,el-col上设置响应...
继续阅读 »

Element实现响应式布局


分享一下,在Vue3项目中实现响应式布局(一行显示7列)。在这个例子中,我参考了Element官方的Layout布局,使用el-card来放置图片。
利用分栏布局,el-row行上设置每列的间隔gutter,el-col上设置响应式的栅格布局,Element官方预设了5个响应式尺寸,官方给出了详细的属性解释。这个例子中我设置了4个尺寸。
在这里插入图片描述
栅格默认的占据的列数是24,设置24就是一列,设置12就显示两列,设置8就显示3列,设置6就显示4列,设置4显示6列......可以根据自己的场景需求来进行布局。这个例子中我设置的响应式布局如下:



:xs="12" 当浏览器宽度<768px时,一行展示2列

:sm="8" 当浏览器宽度>=768px时,一行展示3列

:md="6" 当浏览器宽度>=992px时,一行展示4列

:lg="{ span: '7' }" 当浏览器宽度>=1200px时,一行展示7列 这个需要在css样式中设置一下。



这里例子中的图片都是放在el-card中的,并且图片都是一样大小的。修改图片可以利用图片处理工具,分享一个自己常用的工具:轻量级图片处理工具photopea
Element的Card组件由 header 和 body 组成。 header 是可选的,卡片可以只有内容区域。可以配置 body-style 属性来自定义body部分的style。
:body-style="{padding:10px}" ,这个其实是对el-card头部自动生成的横线下方的body进行边距设置。也就是除了el-card的header部分,其余都是body部分了。
这里例子中没有头部,就是给卡片的body部分设置内边距。
在这里插入图片描述
具体代码如下所示:
在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
图片效果如下所示:
当浏览器宽度>=1200px时,一行展示7列:
图片14.png


当浏览器宽度>=992px时,一行展示4列:


图片15.png
当浏览器宽度>=768px时,一行展示3列:


图片16.png
当浏览器宽度<768px时,一行展示2列:


图片17.png
接下来,优化一下页面,对图片进行懒加载处理。


图片懒加载


看下上面没有用于懒加载方式的情况,F12---NetWork---Img可以看到页面加载就会显示这个页面用到的所有图片。


图片18.png
可以利用vue-lazyload,它是一个Vue.js 图片懒加载插件,可以在页面滚动至图片加载区域内时按需加载图片,进一步优化页面加载速度和性能。采用懒加载技术,可以仅在需要加载的情况下再进行加载,从而减少资源的消耗。也就是在页面加载时仅加载可视区域内的图片,而对于网页下方的图片,我们滑动到该图片时才会进行加载。


下载、引入vue-lazyload


npm install vue-lazyload --save


在package.json中查看:


图片19.png
在main.ts中引入:


图片20.png


使用vue-lazyload


在需要使用懒加载的图片中使用v-lazy指令替换src属性。


图片21.png
也可以是下面的写法:


图片22.png
这样,就实现图片的懒加载了。
验证一下懒加载是否生效,F12---NetWork---Img,可以看到图片的加载情况。


一开始没有滑动到图片区域,就不会加载图片,可以在Img中看到loding占位图片在显示。


图片23.png
滑动到了对应的照片才会显示对应的图片信息。


图片24.png


图片25.png


图片26.png


作者:YM13140912
来源:juejin.cn/post/7242516121769033787
>这就实现了懒加载。

收起阅读 »

一个大龄小前端的年终悔恨

web
今年都做什么了? 刷视频 打王者 空余时间维护了一个项目 就这样吧 仔细想了想今年也没有做什么呀! 真是年纪越大时间越快 为什么有大有小啊? 95的够大了吧 步入前端也才不到3年 So一个大龄的小前端 技术有长进么? 一个PC端项目 用了 react a...
继续阅读 »

image.png




今年都做什么了? 刷视频 打王者 空余时间维护了一个项目 就这样吧



仔细想了想今年也没有做什么呀! 真是年纪越大时间越快




为什么有大有小啊?


95的够大了吧


步入前端也才不到3年


So一个大龄的小前端


技术有长进么?


一个PC端项目 用了 react antd redux-toolkit react-router ahooks axios 也就这样吧,就一点简单的项目,react熟练了么?有点会用了,可是我工作快3年了,写项目还是要来回查文档,antd用的熟练的时候倒是可以不用去查文档,可是过了就忘了,今天写项目就有点想不起来怎么用了,查了文档才可以继续写下去


有长进么?




  1. react熟练了一些,可以自己看源码了




  2. 自己解决问题的能力有了一点提升




  3. 技术的广度认识有了(23年目标是深度)




  4. 数据结构了解一点了 二叉树 队列 链表 队列 (还学了一点算法,不过忘了🤣)




  5. 写代码喜欢封装组件了




  6. node学了一点又忘了




  7. ts会的多了一点




  8. antd也好一点了,以前在群里问一些小白问题,还好有个大哥经常帮我




  9. css 还是不咋地 不过我刚买了一个掘金小册 s.juejin.cn/ds/hjUap4V[…




生活上有什么说的呢?


生活很好 吃喝不愁


就是太久没有回家了 老家黑龙江 爷爷奶奶年纪大了 有时候想不在杭州了 回哈尔滨吧 这样可以多陪陪他们 可是回哈尔滨基本就是躺平了 回去我能做什么? 继续做前端? 好好补补基础去做一个培训讲师?


回去的好处是房子压力小 可以买一个车 每天正常上班 下班陪家人 到家有饭吃 想想也挺好


不过女朋友想在杭州,所以我还会在杭州闯一下的,毕竟我们在杭州买房子也是可以努力一下的


女朋友对我很好 我们在一起也快3年了 我刚步入前端的时候我们刚在一起 2020-05-20 她把我照顾的很好 她很喜欢我我感觉的到 我平时不太会表达 其实我是想跟她结婚的我也喜欢她 我对她耐心少了一点 这一点我会改的 以后我想多跟她分享我每天发生的事 我想这样她会更开心一点吧


今年她给我做了好多的饭,有段时间上班都是她晚上下班回来做的(她下班的早 离家近) 第二天我们好带去(偶尔我们吃一段时间的轻食) 可是我还是胖了




image.png


2023要怎么做?


我想成为大佬 我想自律一些 还有工资也要多一点吧



  • 开年主要大任务 两个字 搞钱 咱们不多来 15万可以吧 嗯 目标攒15W

  • 紧接上条 要是买 20W-30W的车 那你可以少攒点 8万到10万 (买车尽量贷款10W)

  • MD 减肥可以吧 你不看看你多胖了呀 175的身高 快170斤了减到140斤 (总觉得不胖,壮)

  • 技术一定要提升 你不能再这样下去了 要被清除地~





技术我们来好好的捋一下,该怎么提升




  1. 现有项目自己codeReview(改改你的垃圾代码吧)

  2. css多学点

    1. css in js

    2. Tailwindcss

    3. css Module less 写法好好研究一下

    4. css 相关配置要会



  3. react源码要搞一下

    1. fiber

    2. hooks

    3. diff

    4. 一些相关的库的源码 (router,redux等)



  4. webpack vite (要能写出来插件)

  5. node 这个一定要学会 (最起码能自己写接口和工具)

  6. 文章要搞起来 (最起码要写20篇,前5篇要一周一篇文章)


2023 搞一个 pc端 H5 小程序 后台接口 要齐全 必须搞出来一个 加

作者:奈斯啊小刘超奈斯_
来源:juejin.cn/post/7174789490580389925
油💪🏻

收起阅读 »

变“鼠”为“鸭”——为SVG Path制作FIFO路径变换动画,效果丝滑

web
一个月前我曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》,介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础不妨畅想一下,用动画将一个汉字变为另一个汉字,听上去是不是很简单呢?下面动手实践一下: 我随便找了一个字体Aa剑豪...
继续阅读 »

一个月前我曾撰文《使用batik在kotlin中将TTF字体转换为SVG图像》,介绍了如何将汉字转为SVG Path路径进行展示和变换,以此为基础不妨畅想一下,用动画将一个汉字变为另一个汉字,听上去是不是很简单呢?下面动手实践一下:


我随便找了一个字体Aa剑豪体,然后随机选取了两个汉字:,再用上文提到的文章介绍的提取整体字形区块方法取出了SVG:


image.png


image.png


可以看到很简单就提取出了两个字整体的字形,下面用D3做一个简单的变换动画展示:


初始变换


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>鼠鼠我鸭</title>
</head>
<body style="text-align: center"></body>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script type="module">
const _svg = d3
.select("body")
.append("svg")
.attr("width", "1000")
.attr("height", "1000")
.style("zoom", "0.3")
.style("transform", "rotateX(180deg)");
_svg.append("g")
.attr("transform", "translate(0, 160)")
.append("path")
.attr("fill", "#3fd")
.attr("stroke", "black")
.attr("stroke-width", "4")
.attr("d", 上面提到的鼠的SVG_Path...)
.transition().delay(1000).duration(1200)
.attr("fill", "#ef2")
.attr("d", 上面提到的鸭的SVG_Path...);
</script>
</html>

这里调整了svg尺寸以及zoomtransform等属性更好的适应画面,还做了个g标签套住并将定位交给它,动画效果如下图所示:


Animation.gif


很明显的看到,效果非常奇怪,怎么一开始就突然变得很乱?一开始乱这一下显得很突兀,这是因为两段图像的path长度就相差很多,绘进方式也完全不一样,很难真正的渐变过去,我试了一个有优化此过程的库d3-interpolate-path,用上去效果也没有什么差别,而且它用的还是d3@v5版本的,不知道怎么path中还会穿插一些NaN的值,很怪异,看来只能自己做了。


想真正的点对点的渐移过去,可能还是有些难的,所以我想出了一个较为简单的方案,实现一种队列式的效果,的笔画慢慢消失,而则跟随在后面逐步画出,实现一种像队列中常说的FIFO(先进先出)的效果


首先就是拆解,做一个while拆分两个字所有的节点,然后再一步步绘上拆出来的节点以验证拆的是否完整,再才能进行后面的处理。


事先要将“鸭鼠”各自的path定义为常量sourceresult,将二者开头的M与结尾的Z都去掉(中间的M不要去掉),因为动画中字形是流动的,起止点不应提前定义。


拆分路径点


const source = 鼠的SVG_Path(没有MZ)...
const result = 鸭的SVG_Path(没有MZ)...
const actionReg = new RegExp(/[a-z]/, "gi");
const data = new Array();
let match;
let lastIndex;
while ((match = actionReg.exec(result))) {
data.push(result.substring(lastIndex, match.index));
lastIndex = match.index;
}
data.push(result.substring(lastIndex));

就这样就能把的部分拆开了,先直接累加到试验一下是否成功:


叠加试验


let tran = g
.append("path")
.attr("fill", "red")
.attr("stroke", "black")
.attr("stroke-width", "4")
.attr("d", "M" + source + "Z")
.transition()
.delay(800);

let step = "L";
data.map(item => {
step += item + " ";
tran = tran.transition().attr("d", "M" + source + step.trimEnd() + "Z").duration(20);
});

首先是把上面path独立出来改一改,变成红色的利于观看,然后下面慢慢的拼合上每个节点,效果如下:


Animation.gif


是理想中的效果,那么下一步就是加速FIFO先进先出的变换了:


FIFO先进先出


这一步是不能用SVG动画的,要用setInterval定时器进行动画调节,SVG始终还是只能处理很简单的path变化,效果不如直接变来的好,这里设计成把每一帧的动画存进一个方法数组然后交给setInterval计时器循环执行(写起来比较方便),先是改一下tran的定义,因为不是动画了,所以现在改叫path就好了,border也不需要了:


let path = g
.append("path")
.attr("fill", "red")
.attr("d", "M" + source + "Z");

就这样简单的初始化一下就好了,然后就是最核心的一个过程,path的绘制循序就像一个FIFO队列:


let step = "";
let pre = source;
const funs = new Array();
data.map(async function (item, i) {
step += item + " ";
match = pre && actionReg.exec(source);
if (!match) {
pre = "";
} else if (~["M", "L", "T"].indexOf(match[0])) {
pre = source.substring(match.index + 1);
}
const d = "M" + pre + (pre ? "L" : "") + step.trimEnd() + "Z";
funs.push(() => path.attr("d", d));
});

首先是pre负责的字形,这个字形是要慢慢消失的前部,这个前部不是所有的节点都能用的,而是"M", "L", "T"这种明确有点位的动作才行,毕竟这是动画的起始点。然后step就是代表,要一步一步累加。循环结束funs数组也就累计好了所有的帧(方法),然后用定时器执行这些带参方法即可:


const animation = setInterval(() => {
if (!funs.length) {
clearInterval(animation);
return;
}
funs.shift()();
}, 20);

这种方式虽然非常少见,不过这个定时器流程还是很好理解的过程,效果如下:


Animation.gif


是想象中的效果,但稍微有些单调,可以加上一段摇摆的动画配合变换:


摇摆动画


let pathTran = path;
Array(8)
.fill(0)
.map(function () {
pathTran = pathTran
.transition()
.attr("transform", "skewX(10)")
.duration(300)
.transition()
.attr("transform", "skewX(-10)")
.duration(300);
});
pathTran.transition().attr("transform", "").duration(600);

这段动画要不断赋值才能形成连贯动画,所以直接用path处理动画是不行的,因为上面计时器也是用到这个path对象,所以要额外定义一个pathTran专门用于动画,这段摇摆动画效果如下:


Animation.gif


时间掐的刚刚好,那边计时器停掉,这边摇摆动画也缓停了。


写的十分简便,一点小创

作者:lyrieek
来源:juejin.cn/post/7241826575951200293
意,供大家参考观赏。

收起阅读 »

什么是 HTTP 长轮询?

web
什么是 HTTP 长轮询? Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。 为了克服这个缺陷,Web 应用...
继续阅读 »

什么是 HTTP 长轮询?


Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。


为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。


什么是 HTTP 长轮询?


那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。


长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。


HTTP 长轮询如何工作?


要了解长轮询,首先要考虑使用 HTTP 的标准轮询。


“标准”HTTP 轮询


HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。


一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。


HTTP 轮询


上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。


这种“标准”HTTP 轮询有缺点:



  • 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。

  • 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。


HTTP 长轮询解决了使用 HTTP 进行轮询的缺点



  1. 请求从浏览器发送到服务器,就像以前一样

  2. 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送

  3. 客户端等待服务器的响应。

  4. 当数据可用时,服务器将其发送给客户端

  5. 客户端立即向服务器发出另一个 HTTP 长轮询请求


HTTP 长轮询


上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。


这比常规轮询更有效率。



  • 浏览器将始终在可用时接收最新更新

  • 服务器不会被永远无法满足的请求所搞垮。


长轮询有多长时间?


在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。


当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。


通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。


使用长轮询时的注意事项


在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。



  • 随着使用量的增长,您将如何编排实时后端?

  • 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?

  • 通过长轮询,您能否管理消息队列并如何处理丢失的消息?

  • 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?


在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。


服务器性能和扩展


使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?


这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?


消息排序和排队


在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。


服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。


HTTP 长轮询 MQ


然后出现几个明显的问题:



  • 服务器应该将数据缓存或排队多长时间?

  • 应该如何处理失败的客户端连接?

  • 服务器如何知道同一个客户端正在重新连接,而不是新客户端?

  • 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?


所有这些问题都需要 HTTP 长轮询解决方案来回答。


设备和网络支持


如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。


众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良

作者:demo007x
来源:juejin.cn/post/7240111396869161020
好的代理环境中挣扎。

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


const isEmptyObject = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object;



作者:shichuan

来源:juejin.cn/post/7230810119122190397

收起阅读 »

低代码的那些事

web
在当今数字化的时代,前端开发已成为构建出色用户体验的重要领域。然而,传统的前端开发过程往往需要耗费大量时间和精力,对于那些没有技术背景或时间有限的人来说,这无疑是一个巨大的挑战。然而,随着技术的不断进步,低代码开发正迅速崛起,为我们提供了一种简化开发流程的全新...
继续阅读 »

在当今数字化的时代,前端开发已成为构建出色用户体验的重要领域。然而,传统的前端开发过程往往需要耗费大量时间和精力,对于那些没有技术背景或时间有限的人来说,这无疑是一个巨大的挑战。然而,随着技术的不断进步,低代码开发正迅速崛起,为我们提供了一种简化开发流程的全新方法。


终端概念


终端 = 前端 + 客户端


在讲低代码之前,我们先来聊一聊终端概念,这个终端不是指敲命令行的小窗口,而是 终端 = 前端 + 客户端。乍一听,这个不是和大前端的概念类似吗?为什么又提出一个重复的名词,实际上它俩还是有很大区别的,在大前端里面,岗位是有不同区分的,比如前端开发工程师、客户端开发工程师,每个岗位的分工是不一样的,但是你可以把终端看成一个岗位。


image.png


下面是阿里巴巴终端开发工程师招聘的 JD,因为内容较长,我将他分成了三张图片,我们从上到下依次看。


第一张图片:
2024届实习生的招聘,招聘岗位为终端开发工程师





第二张图片:
这是他们对终端开发工程师的描述,大家主要看标了特殊颜色的字体就行



它包括原有的“前端工程师”和“移动端工程师” 相较过去,我们强调面向最终的用户进行交付,不局限于“前端〞、“移动端〞,这将显著拓宽工程师的职责、 能力边界。






第三张图片:
这是他们对终端开发工程师的岗位要求,可以从要求的第1、2、3项看到,这个岗位更侧重于基础技术、终端界面,而不是在于要求你会使用某某框架。





大家对终端概念有了一定了解之后,那么这个终端概念是谁提出的呢?没错,就是阿里巴巴。

阿里巴巴公众号改名史


这个公众号可能会有一些朋友之前关注过,它会发布前端和客户端相关的文章,但是之前的名字并不叫阿里巴巴终端技术。


image.png


我们来看看他的改名史:



  • 2019年05月10日注册 "Alibaba FED"(FED:Front-end Developer 前端开发者)

  • 2019年06月12日 "Alibaba FED" 认证 Alibaba F2E"(F2E:Front-end Engineer 前端工程师)

  • 2022年07月08日 "Alibaba F2E" 帐号迁移改名"阿里巴巴终端技术"


所以是从此又多了一个终端开发工程师的岗位吗,显然不是的,终端开发工程师最终是要取代前端开发工程师和客户端开发工程师的,最终的目的是达到降本增效。


那如何让前端开发工程师和客户端开发工程师过渡成为终端开发工程师。


终端走向荧幕


在阿里 2022 年举办的第 17 届 D2 终端技术大会上,当然他们是这一届将大会名字改成了终端,其中有一篇卓越工程的主题简介如下:


image.png



在过去十年,不管是前端的工具链还是客户端的版本交付效能等都在快速演进,面向未来,我们升级工程体系走向终端工程一体化,覆盖前端及客户端工程师从研发交付到运维的全生命周期,利用低代码、极速构建、全链路运维、Serverless 等新型的工程技术,在卓越工程标准推动下引领终端工程师走向卓越。



可以看到,低代码是可以作为实践终端的一种技术方案,并且将其放在了第一位,那么什么是低代码,低代码能做什么事情,为什么使用低代码可以让终端开发工程师变的更加卓越?低代码对我们普通的一线开发又能带来什么改变或者赋能?
好,接下来,我们就来聊一聊低代码。


什么是低代码


Low-Code


低代码来源于英语翻译——Low-Code,当然,此“Low”非彼“Low”,它意指一种快速开发的方式,使用最少的代码、以最快的速度来交付应用程序。


低代码的定义是什么


虽然低代码已经是个老生常谈的话题了,但关于它的定义我觉得还是有必要描述一遍(来自ChatGPT):


低代码是一种软件开发方法,旨在通过最小化手动编码的工作量,利用可视化工具和组件来快速构建应用程序。它提供了一个图形化的界面,使开发者能够以图形化方式设计和创建应用程序的用户界面、业务逻辑和数据模型,而无需编写大量的传统代码。


低代码它作为一种软件的开发方法,他不仅仅可以作为终端的解决方案,也可以在后端、IOT、大数据等领域上进行使用,并且这些领域也都有现成的低代码开源工具或者平台。


传统前端开发 VS 低代码开发


传统的前端开发方式,我们需要使用 HTML + CSS 描绘出页面的骨架,再使用 JAVASCRIPT 完成页面的功能逻辑。


image.png


可以看到图片上,我们定义了三个 div 元素,并且给每个 div 元素加上了背景颜色,并且加上了点击事件。每一块的元素,都需要我们使用相应的代码去描述。页面越复杂,描述的代码量就会越多。页面的代码量越多,相应的维护的成本就会越高。


我们在来看下如何使用低代码进行开发:


Untitled2.png


左侧物料区,中间画布区,右侧物料配置区,这种区域划分也是比较常见的低代码平台布局。选择物料以后,将物料拖进画布区,接下来我们就可以对物料进行属性配置。


相较于故枯燥难懂的代码,直观的拖拉拽显得更加简单,也更加容易理解。


背后的原理是什么


通过简单的托拉拽后我们可以看到一份表格页面,那么页面实际上是如何生成的呢?


背后实际对应的一份 Schema 描述,主要由物料描述和布局描述组成。


10921685966317_.pic.jpg


我们从左到右依次看,componentsMap 记录了我们当前页面使用的组件,可以看到我们使用了Form.Item、Input、Table,以及这些组件来自的 package 包信息。


componentsTree 里面记录了整个页面布局的信息,最外层由Page组件包裹,然后依次是 Form.Item组件,label 标签为姓名,里面还有一个 input 作为子元素,后面还有两个 Form.Item,为年龄和地址,最后的元素是 Table 组件,通过这些信息,我们就可以布局出一份简单的表格页面。


componentsMap 描述了我们当前页面所需的物料,componentsTree 描述了我们当前页面的布局顺序,将这两份数据组合,通过特定的解析器,就可以得到一份页面。低代码的页面渲染是通过我们事先约定好的数据结构进行生成的。


Schema ⇒ 页面,会不会使我的页面加载变慢


可能会有一些同学心中会有疑问,通过 Schema 生成页面,应该是需要一层 runtime 层吧,通过一段运行时的代码,将 Schema 转换为页面。


那在将 Schema 的内容转换为页面的时候,难免会产生性能上的损耗吧?


性能 VS 可维护性


这里就涉及到了性能 和 可维护性的取舍了,平台的意义在于为你掩盖底层代码的操作。让你用更直观的方式来描述你的目的,其实这里可以牵扯出另外一个相似的话题。


真实DOM VS 虚拟DOM


10931685966489_.pic.jpg


现代化的前端框架都会使用虚拟 DOM,那大家觉得真实DOM更快还是虚拟DOM更快?


框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。


没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。


针对任何一处基准,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。


你会发现低代码的 Schema 和 虚拟 DOM 是比较相似的,都是通过对象的形式去描述 DOM 节点,
虚拟 DOM 的另外一个优势,是在于实现跨端,将底层对 DOM 操作的 API 更换为对 Android 或者 IOS 的 UI 操作。同理低代码的 Schema 也可以比较好的实现跨端,思路基本是一致的,Schema 只是一份组件 + 页面的描述,你可以根据不同的场景,进行不同平台的组件的渲染。


有没有办法可以优化页面渲染的性能吗?


10941685966667_.pic.jpg


那么有没有解决方案呢?


是有的,我们可以通过 Schema 的方式对页面进行出码,出码后是一套完整的应用工程,基于应用工程在去对应用进行一次构建,将这部分负担转移到编译时去完成。


生成的应用工程和我们平常开发的应用工程基本相似:


10951685966768_.pic.jpg


什么是低代码?


如果之前你没有听过低代码,到这里你还是没有明白低代码是什么。没关系,你可以暂时的把他理解一个可视化编辑器,通过拖拽元素就能生成页面,页面背后实际对应的是一串 Schema JSON 数据。到最后,我们会重新对低代码进行一个定义。


低代码发展趋势


低代码发展时间线


image.png


我们来看下低代码发展的时间线:



  • 1980年代:出现了第四代编程语言(ABAP, Unix Shell, SQL, PL/SQL, Oracle Reports, R)第四代编程语言指的是非过程的高级规范语言,包括支持数据库管理、报告生成、数学优化、图形用户界面(GUI)开发和 web 开发。

  • 2000年:出现了 VPL 语言(即 visual programming language 可视化变成语言)

  • 2014年:提出了低代码 / 零代码概念

  • 2016年:国内独立的低代码开发平台开始相继发布

  • 2021年:中国市场逐渐形成完整的低代码、无代码生态体系


发展趋势


image.png


这是 OSS Insight 上关于低代码的一组统计数据,他们分析了50亿的 Github event数据,得出了这些报告,我从里面摘选了和低代码相关的部分。


首先,在2022年热门话题的开源存储库活跃度,LowCode 以76.3%活跃度位居第一,其次是Web3、Github Actions、Database、AI,可见大部分低代码开源仓库都处于一个开发或者维护的状态。


image.png


我们在来看下,低代码发展的趋势图,从2016年到2020年,低代码整体处于上升趋势,并且新增仓库在2020年达到最高点,新增低代码相关的仓库高达了300%以上。


在2020年野蛮生长后,2021年的新增仓库趋近于0,但是在2021低代码相关仓库的start数量将近增长了200%,2022年的数据开始趋于平缓,并且新增仓库数量减少,标志着低代码技术沉淀趋于平稳,百花齐放的时代已经过去。


没有规矩,不成方圆


百花齐放下其实是关于低代码标准的缺失,每套低代码平台都有自己的行为标准,各个平台之间的物料是有隔阂的,无法通用的。就有点像现在百花齐放的小程序,比如微信小程序、支付宝小程序、百度小程序等等,从一定程度上来讲,标准的缺失,会给用户和开发者带来了一定的困扰。


如果有行业组织或者技术社区可以积极推动低代码标准化的倡议,制定统一的行为标准和规范,标准物料的
定义,那么对于低代码的未来发展是一件非常有利的事情。


低代码产品矩阵


我们看来下国外的低代码产品矩阵,种类和平台还是非常多的。
10981685967331_.pic.jpg


可以看到关于低代码的落地场景其实有非常多,并且已经有了大量成熟应用流入市场,所以低代码作为一种开发软件的方法,可以将其灵活运用在各个场景。


而且每个场景的低代码,在其下还可以细分领域,比如 Web 应用程序开发,可以细分为中后台管理搭建、活动推广页面、图表大盘页面等。


一款低代码平台,通常都会有它自己的定位,是为了解决某种特定领域下的特定业务而生。所以一个公司内部有十几个低代码平台是很正常的,他们在细分下的业务场景有不同的分工。


我们正在做什么


这一章节分为三个小块来讲,为什么要做低代码、怎么去做低代码、现在做的怎么样了


为什么要做低代码


我们为什么要做低代码,低代码其实能解决的问题和场景有非常多,那么低代码能帮我们研发团队解决哪些问题?


1.由繁去简


image.png


通常一个需求下来,



  1. 产品会先对需求进行规划,产出一份原型图交付给 UI 设计

  2. UI 通过产品提供的原型图,对原型进行美化,产出一份设计稿

  3. 前端对照设计稿进行页面开发,交付高保真的页面

  4. 最后进行接口联调,将静态的数据更改为接口获取的数据


做程序本质上也是在做交流,假设我们现在是一位前端或者客户端的开发工程师,



  1. 我们需要先和产品 battle 原型图

  2. 和 UI 讨论设计稿

  3. 交付高保真的页面

  4. 和后端进行接口联调


可以看到绝大部分的时间都花在了如何去做页面上,在加上关于各个环节对页面的讨论和修改,这中间会产生大量的浸没成本。


如果说,现在有一个工具,可以做到产品交付的就是高保真页面,你会选择用还是不用?


image.png


这个是使用低代码后的开发流程,由产品直接生成高保真页面交付给前端,极大提高了开发生产力。那么,这个时候前端可以更加聚焦于业务逻辑,聚焦于工程体系,而不是页面本身。


2. 我不想在去改样式了


好像所有的产品经理都喜欢在项目即将上线前,对页面的样式进行调整,没错,既不是测试阶段,也不是预发阶段,而是即将发布前,改完一个,在改一个,好像总是也改不完。


而使用低代码平台后,将页面生成的权利递到产品经理手中,他们可以随心所欲的修改,尽情的展示自己的创造力,我们也不在需要反复的修改样式,反复的重新构建应用发布,你可以专心的去做其它事情。


3. 真正的所见即所得


真正的所见即所得,相比于黑盒子的代码,低代码平台显得更加直观,操作的是页面,而不是代码,你可以在平台上尽情的组装,就像是搭积木一样。


怎么去做低代码


image.png


在能够协调足够多的资源的情况下,选择自研是比较好的一条路,因为一切都是可控的。


但是在资源有限的情况下,选择开源或许是一种最快最便捷的方法了。我们在低代码发展趋势中,可以发现低代码平台和开源技术已经趋于稳定。


使用稳定的开源框架可以更快的帮助我们创建低代码平台,并且有足够多懂低代码底层的人,去帮助我们维护基础设施,站在巨人的肩膀上出发,往往会事半功倍。


我们选择的是阿里开源的 lowcode-engine,在其基础上进行二次开发,选择它的理由有很多:


10991685967710_.pic.jpg


现在做的怎么样了


下面是平台的真实演示,目前已经支持开发以及发布预览了。
_d.gif


低代码架构图:
image.png


平台使用流程的步骤图:


image.png



  • 第一步是创建应用

  • 第二步是创建页面,当然一个应用下面可能会有多个页面,每个页面都会是相互独立的,

  • 第三步是布局调整,可以在选中的页面进行拖拽物料进行布局

  • 第四步是属性配置,配置物料的属性,配置物料的事件或者样式,提供自定义样式和逻辑的功能支持

  • 第五步是保存发布,将当前各个页面的schema进行保存上传,存储到数据库

  • 第六步是页面渲染,可以直接通过平台生成的页面地址,打开页面


被误解的低代码


我相信是会有一大部分的程序员从内心抵制低代码的,一方面,作为一个技术工种,对自己的技术是有底气的,有傲骨的,人为写的代码都不怎么样,还指望低代码平台上的代码吗,另一方面,在低代码平台的代码上维护,那简直就是在屎山上维护,维护的成本会更大吧


出码 VS 不出码


这里的痛点是在于需不需要维护低代码产出的代码,前面我们讲到过出码,出码可以用于产物构建。但构建这一块,是平台去做的,用户并不会感知到背后实际的应用工程。


出码同时也可以用于用户的自定义需求,如果平台的物料完全覆盖了你的业务场景,你是不需要去出码的。但是,如果平台的物料无法满足你的业务场景,你需要的组件又具备足够的特殊性,这个时候你可能需要使用出码功能,在出码的应用工程基础下,添加自己的业务代码,那么这个时候,你是需要维护整个应用工程的。


对低代码的分歧往往是这个时候产生的,每个人心中都有自己的标准代码,都会本能的去抵触和自己标准不同的代码。


不出码如何自定义开发需求?


那有没有既不出码,又可以满足自定义开发的需求呢?


因为我们更多的是希望平台去维护工程,而不是通过人为方式去维护。像我们平时开发页面,本质上就是在写组件,通过拼装组件,形成页面。


我们的思想上可以做个转变,既然 80%~90% 的工作平台可以承接,剩余的平台无法实现,可以自己实现自定义组件进行发布,发布的组件可以沉淀到市场,你的其它项目可以使用自己的业务组件,其他同事也可以使用你的组件。


低代码会不会导致前端岗位变少?


其实完全可以将低代码看成提升工作效率的一个工具,低代码解决页面视图,页面逻辑和数据联调需要开发者在平台上进行完成,开发者的关注点更多的聚焦于业务逻辑,聚焦于如何去做好工程化体系。


AI Code 不是更好吗?


那好像 AI Code 也可以做到低代码能做的地步?


在今年3月份 GPT-4 的发布会上,只需要在草稿本上用纸笔画出一个非常粗糙的草图,再拍照告诉GPT-4需要一个这样的网站,AI 就可以在10秒钟左右根据草图,生成一个网站完整的前端 HTML 代码。


GPT-4发布:一张草图,一秒生成网站


image.png


这简直就是低代码 plus,回归我第一次使用 GPT 的时候,我确实是被惊讶到,特别是他能衔接上下文关系,给出你想要的答案。


我们开发应用,其实本身就是一个庞大的上下文,版本一直迭代,需求一直新增,通过人本身去记住业务上下文,在一个足够复杂的应用下,他的上下文会足够大,足够的冗余,我们去抽离组件,抽离函数,使用数据结构优化代码,实际上就是在优化上下文,写代码并不难,难的是如何梳理页面的组件和那些难以理解的业务以及那些人与人的沟通。


至少现在看来,GPT 无法做到承接复杂应用的上下文,现在的他只能帮助你快速产出一个 demo 应用,前提你需要做到甄别代码的能力,以及还需要面临后续版本迭代更新的窘境问题。


或者说,如果 AI 真的能做到独立开发复杂应用,程序员就真的会被取代吗,做程序本身就是一个相对复杂的活,需要持续学习,持续精进。如果AI真的能做到独立开发这一步,那我觉得离真正的无人驾驶也不远了,出租车司机全部都得失业,因为做一个程序相比于驾驶车辆,会难上不少,当然还包括其它行业,80% 以上的职业都极有可能面临下岗危机。


这个是政客、政府不允许的,虽然科技进步是好事,但是 AI 并没有带来实际的增量工作岗位,反而导致失业率变高,失业率若变高,整体社会的稳定性结构就会变差。
所以,我们更多的将 AI 看成工具,关注点在于,如何用 AI 去做成更多的事情。


什么是低代码?


讲到这里,基本上今天的分享就已经进入尾声了,最后我们在来确定下什么是低代码?



低代码是一种软件开发方法,旨在通过最小化手动编码的工作量,利用可视化工具和组件来快速构建应用程序。它提供了一个图形化的界面,使开发者能够以图形化方式设计和创建应用程序的用户界面、业务逻辑和数据模型,而无需编写大量的传统代码。



一千个人眼中,有一千个哈姆雷特,每个人对低代码的理解都会有些许不同,首先低代码是一种软件开发的方法,这套方法可以用在很多场景。如果一个平台提供了可视化的工具和组件并且又提供部分手动编码的能力,它就可以是一个低代码平台。


在前端低代码的方案中,并不是不再使用 HTML、CSS、JAVASCRIPT 进行开发,而是大大减少他们的使用频率,通过少量的代码,就可以完成一个页面的开发。


参考


收起阅读 »