注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

js如何控制一次只加载一张图片,加载完成后再加载下一张

web
今天看到一个面试题,是关于img图片加载方面的,有必要记录一下。其实关于这个问题,只要知道图片什么时候加载完成就能解决了。通过onload事件判断Img标签加载完成实现逻辑:新建一个Image对象实例,为实例对象设置src属性等,在onload事件中添加此实例...
继续阅读 »

今天看到一个面试题,是关于img图片加载方面的,有必要记录一下。其实关于这个问题,只要知道图片什么时候加载完成就能解决了。

通过onload事件判断Img标签加载完成

实现逻辑:新建一个Image对象实例,为实例对象设置src属性等,在onload事件中添加此实例对象到父元素中,然后将图片地址数组中的第一个元素剔除,继续调用此方法直到存储图片地址的数组为空。

代码

const imgArrs = [...]; // 图片地址
const content = document.getElementById('content');
const loadImg = () => {
if (!imgArrs.length) return;
const img = new Image(); // 新建一个Image对象
img.src = imgArrs[0];
img.setAttribute('class', 'img-item');
img.onload = () => { // 监听onload事件
// setTimeout(() => { // 使用setTimeout可以更清晰的看清实现效果
content.appendChild(img);
imgArrs.shift();
loadImg();
// }, 1000);
}
img.onerror = () => {
// do something here
}
}
loadImg();


实现效果

lp_img_load.gif

加上setTimeout后,看到的效果更加明显,我这里加了500毫秒的延迟(录屏软件只支持录制8秒的时间...)

setTimeout_load_img.gif

其实我在网上还看到了一种答案,通过onreadystatechange事件实现监听,于是在我本地调试了一下,发现并不能实现,img实例对象上并没有这个属性方法。查了查MDN,发现目前仅有XmlHttpRequest对象和Document对象中存在onreadystatechange属性,而对于其它元素onreadystatechange此属性并不存在。

因此对于其它元素需要慎用onreadystatechange事件

不过我电脑上目前只有ChormeSafari两种浏览器,对于onreadystatechange测试的覆盖面不全,所以我上面的结论可能还需要进一步验证才行,感兴趣的掘友可以调试一下~。

扩展知识

img标签是什么时候发送图片资源请求的?

  1. HTML文档渲染解析,如果解析到img标签的src时,浏览器就会立刻开启一个线程去请求图片资源。
  2. 动态创建img标签,设置src属性时,即使这个img标签没有添加到dom元素中,也会立即发送一个请求。
// 例1:
const img = new Image();
img.src = 'http://xxxx.com/x/y/z/ccc.png';

上面的代码如果运行起来后,就会发送请求。 如图:

image.png

再看一个例子:创建了一个div元素,然后将存放img标签元素的变量添加到div元素内,而div元素此时并不在dom文档中,页面不会展示该div元素,那么浏览器会发送请求吗?

// 例2:
const img = ``;
const dom = document.createElement('div');
dom.innerHtml = img;

答案:会请求。如图:

image.png

通过设置css属性能否做到禁止发送图片请求资源?

  1. img标签设置样式display:none或者visibility: hidden,隐藏img标签,无法做到禁止发送请求。
"http://xxx.com/x/sdf.png" style="display: none;">
或者
"http://xxx.com/x/sdf.png" style="visibility: hidden;">
  1. 将图片设置为元素的背景图片,但此元素不存在,可以做到禁止发送请求。
DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>title>
<style>
.test {
height: 200px;
background-image: url('http://eb118-file.cdn.bcebos.com/upload/39148b2a545b48bf9b4ee95fd1b7f1eb_1515564089.png?');
}
style>
head>
<body>
<div>div>
body>
html>

dom文档中不存在test元素时,即使设置了背景图片,也不会发送请求,只有test元素存在时才会发送请求。

另外这个例子其实有点不太贴切,img标签background-image二者有着本质的区别。一个属于HTML标签,另一个属于css样式,加载机制和解析顺序也不同。

一个完整的页面是由jshtmlcss组成的,按照解析机制,html元素会优先解析,尽管css样式是放在head标签内的,但也不意味着它会优先加载,它只有等到html文档加载完成后才会执行。而img标签属于网页内容,所以img标签会随着网页解析渲染优先于css样式表加载出来。

文章中若有描述不正确的地方,欢迎掘友们纠正~。

参考文章


作者:娜个小部呀
来源:juejin.cn/post/7340167256267391012
收起阅读 »

小小导出,我大前端足矣!

web
如果你觉得你现在做的事情很累,很难,那一定是你方法用错了。 一、问题剖析 那是一个倾盆大雨的早上,花瓣随风雨落在我的肩膀上,是五颜六色的花朵。 我轻轻抚摸着他,随后拨开第一朵花瓣,她不爱我。 拨开第二朵,她爱我。 正当我沉迷于甜蜜的幻想中,后端小白🙋喊道:...
继续阅读 »



如果你觉得你现在做的事情很累,很难,那一定是你方法用错了。



2.jpg


一、问题剖析


那是一个倾盆大雨的早上,花瓣随风雨落在我的肩膀上,是五颜六色的花朵。


我轻轻抚摸着他,随后拨开第一朵花瓣,她不爱我。


拨开第二朵,她爱我。


正当我沉迷于甜蜜的幻想中,后端小白🙋喊道:这个导出你前端应该就能做的吧!


🙋🏻‍♂️那是自然,有什么功能是我大前端做不了的,必须得让你们大开眼界。


二、为什么导出要前端做?


前端导出的场景:



  1. 轻量级数据:如果要导出的表格数据相对较小,可以直接在前端生成和导出,避免服务器端的处理和通信开销。

  2. 数据已存在于前端:如果表格数据已经以 JSON 或其他形式存在于前端,可以直接利用前端技术将其导出为 Excel、CSV 或其他格式。

  3. 实时生成/计算:如果导出的表格需要根据用户输入或动态生成,可以使用前端技术基于用户操作实时生成表格,并提供导出功能。

  4. 快速响应:前端导出表格可以提供更快的响应速度,避免等待服务器端的处理和下载时间。


后端导出的场景:



  1. 大量数据:如果要导出的表格数据量很大,超过了前端处理能力或网络传输限制,那么在服务器端进行导出会更高效。

  2. 安全性和数据保护:敏感数据不适合在前端暴露,因此在服务器端进行导出可以更好地控制和保护数据的安全。

  3. 复杂的业务逻辑:如果导出涉及复杂的业务逻辑、数据处理或数据查询,使用服务器端的计算能力和数据库访问更合适。

  4. 跨平台支持:如果需要支持多个前端平台(如 Web、移动应用等),将导出功能放在服务器端可以提供一致的导出体验。


三、讲解一下在前端做的导出


xlsx、xlsx-style


如果是只做表格导出:http://www.npmjs.com/package/xls…


如果导出要包含样式:http://www.npmjs.com/package/xls…


import XLSX from "xlsx";

exportData() {
let tableName = '表格'

if(!getVal(this.dataList, 'length')){
this.$message.info("暂时数据");
return
}


// 处理头部
let headers = {
"B2": "字段-B2",
"E2": "字段-E2",
}
const props = [ "B2", "E2" ]
let tmp_dataListFilter = [
{
"B2": "字段-B2",
"E2": "字段-E2",
},
{
"E2": "2",
"B2": "2",
}
]

tmp_dataListFilter.unshift(headers) // 将表头放入数据源前面
let wb = XLSX.utils.book_new();
let contentWs = XLSX.utils.json_to_sheet(tmp_dataListFilter, {
skipHeader: true, // 是否忽略表头,默认为false
origin: "A2" // 设置插入位置
});
// /单独设置某个单元格内容
contentWs["A1"]={
t:"s",
v:tableName,
};
// /设置单元格合并!merges为一个对象数组,每个对象设定了单元格合并的规侧,
// /{s:{r:0,c:},e:{r:0,c:2}为一个规则,s:起始位置,e:结束位置,r:行,c:列
contentWs["!merges"]=[{ s:{r:0,c:0 },e:{r:0,c:props.length - 1 }}]

// 设置单元格的宽度
contentWs["!cols"] = []
props.forEach(p => contentWs["!cols"].push({wch: 35}))
XLSX.utils.book_append_sheet(wb,contentWs,tableName) // 表格内的下面的tab
XLSX.writeFile(wb,tableName + ".xlsx"); // 导出文件名字
},

package.json


"xlsx": "^0.15.5",
"xlsx-style": "^0.8.13"

大概效果如下:


3.png


感觉前端导出也很容易。


哦哦,那你别高兴太早。


四、需求升级:单元格要居中和加粗。


xlsx


尝试使用xlsx-style设样式。


官方文档:github.com/rockboom/Sh…


文档说给单元格设置s为对象


4.png


let contentWs = XLSX.utils.json_to_sheet(tmp_dataListFilter, {
skipHeader: true, // 是否忽略表头,默认为false
origin: "A2", // 设置插入位置
});
// /单独设置某个单元格内容
contentWs["A1"] = {
t: "s",
v: tableName,
s:{ // 这个是关键s
font: { bold: true },
alignment: { horizontal: 'center' }
}
};

发现设置无效。


有人说要改xlsx、xlsx-style源码:


大概的意思是:修改xlsx.extendscript.js、xlsx.full.min.js更改文件变量。


发现仍然无效。


使用binary方式保存



  1. 首先保存的时候 type要改成 binary方式

  2. 保存的时候需要使用 xlsx-style模块


var writingOpt = { 
bookType: 'xlsx',
bookSST: true,
type: 'binary' // <--- 1.改这里
}


/*
2. type:'array'改为'binary' 后因为下面代码会报错, 打不开excel
new Blob([wbout], { type: 'application/octet-stream' }
要文本转换成数组缓存后再生成二进制对象
*/


// 添加String To ArrayBuffer
function s2ab(s) {
var buf = new ArrayBuffer(s.length);
var view = new Uint8Array(buf);
for (var i = 0; i < s.length; i++) {
view[i] = s.charCodeAt(i) & 0xFF;
}
return buf;
}

let blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })

FileSaver.saveAs(blob, exportName)

可以下载了。但依然样式没起作用。


使用 xlsx-style 模块生成文件


首先安装模块


npm install xlsx-style 

在项目里安装报好多错误直接强制安装,不检查依赖。


npm install xlsx-style -force

安装完成后 找不到cptable模块会报错

报错内容如下:


./node_modules/xlsx-style/dist/cpexcel.js Module not found: Error: Can't resolve './cptable' in 

这个问题在vue.config.js里配置一下就可以解决。

其他框架自己找找方法,反正只要不让他报错能启动就行。


module.exports = {
// ...其他配置省略
configureWebpack: {
// ...其他配置省略
externals:{
'./cptable':'var cptable'
},
},

安装完xlsx-style后改代码


import XLSX2 from "xlsx-style";    // 1. 引入模块

// 2. 使用`xlsx-style` 生成。 XLSX.write => XLSX2.write
var wbout = XLSX2.write(wb, writingOpt)

仍然无效。


总结xlsx


大概的意思是说:默认不支持改变样式,想要支持改变样式,需要使用它的收费版本。


本着勤俭节约的原则,很多人使用了另一个第三方库:xlsx-style[4] ,但是使用起来极其复杂,还需要改 node_modules 源码,这个库最后更新时间也定格在了 6年前。还有一些其他的第三方样式拓展库,质量参差不齐。


使用成本和后期的维护成本很高,不得不放弃。


ExcelJS


ExcelJS终于可以了


ExcelJS[5] 周下载量 450k,github star 9k,并且拥有中文文档,对国内开发者很友好。虽然文档是以README 的形式,可读性不太好,但重在内容,常用的功能基本都有覆盖。


最近更新时间是6个月内,试用了一下,集成很简单,再加之文档丰富,就选它了。


npm install exceljs
npm install file-saver // 下载到本地还需要另一个库:file-saver

基本操作


//导入ExcelJS
import ExcelJS from "exceljs";

//下载文件
download_file(buffer, fileName) {
console.log("导出");
let fileURL = window.URL.createObjectURL(new Blob([buffer]));
let fileLink = document.createElement("a");
fileLink.href = fileURL;
fileLink.setAttribute("download", fileName);
document.body.appendChild(fileLink);
fileLink.click();
}

导出xlsx表格的代码


//下面是导出的函数
async export() {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Sheet1");
//这里是数据列表
const data = [
{ id: 1, name: "艾伦", age: 20, sex: "男", achievement: 90 },
{ id: 2, name: "柏然", age: 25, sex: "男", achievement: 86 },
];
// 设置列,这里的width就是列宽
worksheet.columns = [
{ header: "序号", key: "id", width: 10},
{ header: "姓名", key: "name", width: 10 },
];

// 批量插入数据
data.forEach(item => worksheet.addRow(item));

// 写入文件
const buffer = await workbook.xlsx.writeBuffer();
//下载文件
this.download_file(buffer, "填报汇总.xlsx");
}

设置行高和列宽


列宽上面已经有了,这里说明一下行高怎么设置

worksheet.getRow(2).height = 30;


合并单元格


worksheet.mergeCells("B1:C1");


自定义表格样式


//设置样式表格样式,font里面设置字体大小,颜色(这里是argb要注意),加粗
//alignment 设置单元格的水平和垂直居中
const B1 = worksheet.getCell('B1')
B1.font = { size: 20, color:{ argb: 'FF8B008B' }, bold: true }
B1.alignment = { horizontal: 'center', vertical: 'middle' }

ExcelJS实战


import ExcelJS from "exceljs";

//下载文件
download_file(buffer, fileName) {
console.log("导出");
let fileURL = window.URL.createObjectURL(new Blob([buffer]));
let fileLink = document.createElement("a");
fileLink.href = fileURL;
fileLink.setAttribute("download", fileName);
document.body.appendChild(fileLink);
fileLink.click();
},
async exportClick() {
const loading = this.$loading({
lock: true,
text: "数据导出中,请耐心等待!",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});

this.tableData = [
{ a: 1, b:2 }
]
const enterpriseVisitsColumns = [
{
prop: "a",
label: "银行",
},
{
prop: "b",
label: "企业数",
}
]

// 表格数据:this.tableData
if (!(this.tableData && this.tableData.length)) {
this.$message.info("暂无数据");
loading.close();
return;
}

let tableName = this.tableName; // 表格名
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet(tableName);
const props = enterpriseVisitsColumns();
//这里是数据列表
const data = this.tableData;
// 设置列,这里的width就是列宽
let arr = [];
props.forEach((p) => {
arr.push({
header: p.label,
key: p.prop,
width: 25,
});
});
worksheet.columns = arr;

// 插入一行到指定位置,现在我往表格最前面加一行,值为表名
const rowIndex = 1; // 要插入的行位置
const newRow = worksheet.insertRow(rowIndex);
// 设置新行的单元格值
newRow.getCell(1).value = tableName; // 值为表名

// 批量插入数据,上面插一条,这里就是从第二行开始加
data.forEach((item) => worksheet.addRow(item));

//设置样式表格样式,font里面设置字体大小,颜色(这里是argb要注意),加粗
//alignment 设置单元格的水平和垂直居中
// const B1 = worksheet.getCell("B1");
// B1.font = { size: 20, color: { argb: "FF8B008B" }, bold: true };
// B1.alignment = { horizontal: "center", vertical: "middle" };

// 合并单元格,就是把A1开始到J1的单元格合并
worksheet.mergeCells("A1:J1");

// 批量设置所有表格数据的样式
worksheet.eachRow((row, rowNumber) => {
let size = rowNumber == 1 ? 16 : rowNumber == 2 ? 12 : "";
//设置表头样式
row.eachCell((cell) => {
cell.font = {
size,
// color:{ argb: 'FF8B008B' },
bold: true,
};
cell.alignment = { horizontal: "center", vertical: "middle" };
});

//设置所有行高
row.height = 30;
});

// 写入文件
const buffer = await workbook.xlsx.writeBuffer();
//下载文件
this.download_file(buffer, tableName + ".xlsx");

loading.close();
},

后记


导出功能并不是说都是前端或者后端实现,要具体情况,具体分析,我相信哪方都可以做,但谁适合做,这个才是我们需要去思考的。


就如同我们项目中,该例子后面也是前端实现的,大数据分页当然还是得后端同学来实现较好。


如果有其他更好的方法也欢迎评论区见,这里提供的只是诸多方法之一。


最后,祝君能拿下满意的offer。




作者:Dignity_呱
来源:juejin.cn/post/7339814359886348328
收起阅读 »

小程序使用有赞 UI 库

web
引入有赞 UI 库 1、初始化 npm 在小程序 package.json 所在的目录(代码根目录)中执行下面命令,进行初始化: npm init ps. 这里一路按 enter 键就可以了,命令窗口可以在目录下通过 shift + 鼠标右键选择 Powe...
继续阅读 »

引入有赞 UI 库


1、初始化 npm

在小程序 package.json 所在的目录(代码根目录)中执行下面命令,进行初始化:


npm init 

ps. 这里一路按 enter 键就可以了,命令窗口可以在目录下通过 shift + 鼠标右键选择 PowerShell 打开 。


2、安装 Vant 包

在上面的基础上,输入下面命令,安装有赞 UI 库:


npm i vant-weapp -S --production

如果这里报 rollbackFailedOptional 错误,可以试试修改 npm 的资源镜像链接,输入下面命令:


npm config set registry http://registry.npm.taobao.org

然后再执行上面安装命令,应该就可以了。


在这里插入图片描述


3、使用 npm 模块

点击微信开发者工具右上角详情,选择本地设置,勾选上下面的 “使用 npm 模块”


在这里插入图片描述


4、构建 npm

点击开发者工具中的菜单栏:工具 --> 构建 npm


在这里插入图片描述


5、修改 app.json

将 app.json 中的 "style": "v2" 去除


在这里插入图片描述


6、修改 project.config.json

在根目录下的 project.config.json 文件中,通过 ctrl + f 搜索 packNpmManually ,修改配置,使开发者工具可以正确索引到 npm 依赖的位置。


在这里插入图片描述


改成如下图


在这里插入图片描述
代码如下:


        "packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./"
}
],



到这应该就完成安装了,下面看看使用。


使用有赞 UI 库


1、引入控件

在 app.json (或 Page 的 json)中引入控件


"usingComponents": {
"van-button": "@vant/weapp/button/index"
}

2、使用控件

引入组件后,可以在 wxml 中直接使用组件


<van-button type="primary">按钮</van-button>

示例


这里拿一个 Dialog 弹出框作为示例,因为官方文档有问题,在 Page 中引入错了,真的是把我坑到了。


1、引入 Dialog 控件

app.jsonindex.json中引入组件


"usingComponents": {
"van-dialog": "@vant/weapp/dialog/index"
}

2、在 WXML 中设置 Dialog

这里有两种用法,一种是把 Dialog 当布局组件使用,一种是像 wx.showModel 一样弹出对话框,无论哪种都要在 WXML 中写 van-dialog。


这里以后一种用法为例,在 WXML 中随便找个地方,填入下面代码:


<van-dialog id="van-dialog" />

3、在 Page 中使用

先引入组件,就是这里把我坑了,主要就是没有 dist 这个目录了,有赞也不提示。


//import Dialog from 'path/to/@vant/weapp/dist/dialog/dialog';
import Dialog from '../../../miniprogram_npm/@vant/weapp/dialog/dialog';

这里用的相对路径,后面路径是对的,需要直接改下!


在需要的时候像下面一样使用就可以,不过我是觉得还不如微信自带的好看哦!


Dialog.confirm({
title: '标题',
message: '弹窗内容',
})
.then(() => {
// on confirm
})
.catch(() => {
// on cancel
});

4、在 Dialog 中有原生控件

这里提一下,如果 Dialog 中有原生控件,消失的时候原生控件回后消失,很奇怪。例如对话框里面放了一个 canvas 来显示二维码,关闭对话框时,对话框消失了,二维码延迟一会才消失,这时候可以通过变量,先隐藏二维码,再隐藏对话框,有这个思路,就能解决了。


结语


如果不想自己设计各种控件的话,用有赞的 UI 库还是很方便的,但是如果给了设计图,要改这些控件还是有点麻烦。


作者:方大可
来源:juejin.cn/post/7222897518500708407
收起阅读 »

新项目跑不起来,人和项目总得有一个能跑

web
前言 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。 进入新公司或者接手新项目,都会遇到如何将项目跑起,跑不起的话,可能就得人跑了。 每个人的花期不...
继续阅读 »

前言


  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。

  • 进入新公司或者接手新项目,都会遇到如何将项目跑起,跑不起的话,可能就得人跑了。





每个人的花期不同。


就像万特特在《这世界很好,你也不差》中讲的,每个人花期不同,不必焦虑有人比你提前拥有。



1.jpg


一、问题剖析


那是一个风和日丽的早上,我想要去看漫天霞光。


要和心上人手挽手走在街上,


清晨的花香,傍晚的夕阳。


正当我沉迷于甜蜜的幻想中,前同事发来:我遇到一个问题,帮我看看呗!


真是的,慌慌张张的,说吧~


问题是这样子的:


现在有一个新项目,B项目从A项目复制过去,在原有的功能上扩展。


我又多嘴的问了一句,为啥要复制一份过去啊!


他回道:因为现在要做国际化的需求,之前的项目没有考虑到,改起来很麻烦,打算先拷贝一份出来,通过java程序执行一下,先把大部分的先转化一下,这样子不会影响原本项目的开发。


我又对嘴了一下,开新分支不可以嘛!


他说:因为是给两个地区用的,可能在A页面,两个区的需求不同,改起来也麻烦,需求不同,就没必要硬写在一起了。


懂了!


二、gitlab仓库中有两种方式拉取代码仓库:ssh和https


2.png


ssh


首先,确保你已经在 GitLab 上配置了 SSH 密钥。如果没有,你需要生成 SSH 密钥并将公钥添加到 GitLab 的个人设置中。


https


使用 HTTPS 协议拉取代码需要输入你的 GitLab 用户名和密码,或者是访问令牌(Access Token)。


两者



  • 使用 SSH 协议可以避免频繁输入用户名和密码,但需要提前配置好 SSH 密钥。

  • 使用 HTTPS 协议在拉取代码时需要提供用户名和密码或者访问令牌,相对来说更加方便快捷。


一般来说,都是用https拉取仓库,但我怎么拉都拉不下来。


提示:


SSL certificate problem: self signed certificate

翻译:SSL证书问题:自签名证书

于是,我尝试运行ssh的方式拉取。


我们首先需要先生成秘钥


ssh-keygen -t rsa -C “your_email@youremail.com”  
// 命令中的email,就是gitlab中的账号,需要保持一致

直接三个Enter就行,然后会提示输入密码(可输可不输)


3.png


在~/.ssh/下会生成两个文件,id_rsa和id_rsa.pub


4.png


id_rsa是私钥

id_rsa.pub是公钥

在我们c盘的用户里面有个.ssh文件夹


C:\Users\Lenovo\.ssh

gitlab添加秘钥


5.png


这样子就配置好了,可以通过ssh方式拉取了。


git clone 项目远程仓库ssh地址

项目已拉取。


此时要推送的时候,一直让我输入密码,但我输入完之后,仍然要求输入。


我使用的是tortoisegit


连接gitlab,总是弹出git@xxx.com’s password 对话框


然后打开TortoiseGit设置,如下图进入相应页面,选择相对路径的ssh.exe文件


6.png


查找git在哪里,只需要输入 where git


7.png


到这里项目已经拉取,也能推送了。


接下来运行项目。


三、运行的时候,先安装依赖,发现npm安装失败。


发现npm i


8.png


于是,我尝试cnpm i安装,安装是可以安装,但npm run serve的时候还是报错了。


四、于是,我找同事要来了他本地的依赖,想着在我本地看看能不能跑起来。


但因我和同事的环境(npm、node)环境不一样。


于是,我和他保持一致的版本node:12


但又因为还是有些不同,npm run serve的时候,报了:


error  in ./src/styles/element-variables.scss  
Syntax Error: Error: Missing binary. See message above.

看起来是,sass的问题,于是想着重新安装一下


一般来说sass、node-sass问题经常会出现。


npm install --save-dev sass-loader node-sass

仍然不行,那切换一下node的版本,从node10-node14都轮流切换尝试都不行。


我使用的是nvm管理node版本。


9.png


但点进去看只有10.14.1版本有node_modules文件夹依赖


10.png


其他的版本貌似没有,本来想着和同事保持12版本的node,然后把他的依赖复制给我,但我本地的node12没有node_modules文件夹


关于上面的scss文件引起的:Syntax Error: Error: Missing binary. See message above.


下载fibers


运行的时候报错:而且会弹出框说:中止/忽略(其实是fibers缺少二进制文件执行)


Try running this to fix the issue: D:\Program Files\nodejs\node.exe E:\vue-project\node_modules\fibers/build
Error: Cannot find module 'E:\vue-project\node_modules\fibers\bin\win32-x64-83\fibers'
Require stack:

我们发现了fibers引起的错误


有回答说:


项目node_moudules/fibers/bin文件夹中没有win32-x64-83模块,缺少win32-x64-83文件夹下的fibers.node。


11.png


1.在github下载对应系统版本的node文件


win32-x64-83_binding.node 文件下载地址:github-releases


2.下载后的win32-x64-83_binding.node文件改名为fibers.node。


3.保存fibers.node在项目node_moudules/fibers/bin新建的win32-x64-83文件夹中。


4.然后重新执行run serve就可以


rebuild node-sass

执行


npm rebuild node-sass

如果提示 stack Error: EACCES: permission denied, mkdir 错误,则执行命令:


npm rebuild node-sass --unsafe-perm

失败告终!



但我简单粗暴直接把fibers文件夹给删除重新跑就可以了。(√)



五、后面想了一下,项目的背景,是从A项目复制过来的,那我把A项目的依赖复制过来运行下看看。


复制过来后,可以运行了,但因为B新项目装了vue-i18n国际化依赖,于是我安装一下,运行,终于可以了。


但项目打开页面的时候,还是报错了。


12.webp


看起来,vue-router版本和i18n版本冲突了


我的解决办法是把i18n的版本 改为了 8.26.7 ,再启动项目就可以了


npm install vue-i18n@8.26.7 -S

虽然B项目的package.json写着是版本9的,但这不影响我运行。


只不过在页面用到这个依赖的时候可能会有一些不同。


完成搞定。


后记


那晚,我们一起找了家烧烤店,他说:今日之事,都在酒里了,一口闷。


道不清,理还乱,别是一般滋味在心头~


刚开始接手一个项目的时候,依赖啊,版本啊,什么的都很头疼,但一步一步来,见招拆招,无非就是node版本、npm版本、cnpm他们的故事。


如果有其他更好的方法也欢迎评论区见,这里提供的只是诸多方法之一。



作者:Dignity_呱
来源:juejin.cn/post/7339376488028307456
收起阅读 »

前端更新部署后通知用户刷新

web
前言 周五晚上组里说前端有bug,正在吃宵夜的我眉头一紧,立即打开了钉钉(手贱...),看了一下这不是前几天刚解决的吗,果然,使用刷新大法就解决,原因不过是用户一直停留在页面上,新的版本发布后,没有刷新拿不到新的资源。 现在大部分的前端系统都是SPA,用户在使...
继续阅读 »

前言


周五晚上组里说前端有bug,正在吃宵夜的我眉头一紧,立即打开了钉钉(手贱...),看了一下这不是前几天刚解决的吗,果然,使用刷新大法就解决,原因不过是用户一直停留在页面上,新的版本发布后,没有刷新拿不到新的资源。


现在大部分的前端系统都是SPA,用户在使用中对系统更新无感知,切换菜单等并不能获取最新资源,如果前端是覆盖性部署,切换菜单请求旧资源,这个旧资源已经被覆盖(hash打包的文件),还会出现一直无响应的情况。


那么,当前端部署更新后,提示一直停留在系统中的用户刷新系统很有必要。


解决方案



  1. 在public文件夹下加入manifest.json文件,记录版本信息

  2. 前端打包的时候向manifest.json写入当前时间戳信息

  3. 在入口JS引入检查更新的逻辑,有更新则提示更新

    • 路由守卫router.beforeResolve(Vue-Router为例),检查更新,对比manifest.json文件的响应头Etag判断是否有更新

    • 通过Worker轮询,检查更新,对比manifest.json文件的响应头Etag判断是否有更新。当然你如果不在乎这点点开销,可不使用Worker另开一个线程




Public下的加入manifest.json文件


{
"timestamp":1706518420707,
"msg":"更新内容如下:\n--1.添加系统更新提示机制"
}

这里如果是不向用户提示更新内容,可不填,前段开发者也无需维护manifest.json的msg内容,这里主要考虑到如果用户在填长表单的时候,填了一大半,你这时候给用户弹个更新提示,用户无法判断是否影响当前表单填写提交,如果将更新信息展示出来,用户感知更新内容,可判断是否需要立即刷新,还是提交完表单再刷新。


webpack向manifest.json写入当前时间戳信息


	// 版本号文件
const filePath = path.resolve(`./public`, 'manifest.json')
// 读取文件内容
readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('读取文件时出错:', err)
return
}
// 将文件内容转换JSON
const dataObj = JSON.parse(data)
dataObj.timestamp = new Date().getTime()
// 将修改后的内容写回文件
writeFile(filePath, JSON.stringify(dataObj), 'utf8', err => {
if (err) {
console.error('写入文件时出错:', err)
return
}
})
})

如果你无需维护更新内容的话,可直接写入timestamp


// 生成版本号文件
const filePath = path.resolve(`./public`, 'manifest.json')
writeFileSync(filePath, `${JSON.stringify({ timestamp: new Date().getTime() })}`)


检查更新的逻辑


入口文件main.js处引入


我这里检查更新的文件是放在utils/checkUpdate


// 检查版本更新
import '@/utils/checkUpdate'

checkUpdate文件内容如下


import router from '@/router'
import { Modal } from 'ant-design-vue'
if (process.env.NODE_ENV === 'production') {
let lastEtag = ''
let hasUpdate = false
let worker = null

async function checkUpdate() {
try {
// 检测前端资源是否有更新
let response = await fetch(`/manifest.json?v=${Date.now()}`, {
method: 'head'
})
// 获取最新的etag
let etag = response.headers.get('etag')
hasUpdate = lastEtag && etag !== lastEtag
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}

async function confirmReload(msg = '', lastEtag) {
worker &&
worker.postMessage({
type: 'pause'
})
try {
Modal.confirm({
title: '温馨提示',
content: '系统后台有更新,请点击“立即刷新”刷新页面\n' + msg,
okText: '立即刷新',
cancelText: '5分钟后提示我',
onOk() {
worker.postMessage({
type: 'destroy'
})
location.reload()
},
onCancel() {
worker &&
worker.postMessage({
type: 'recheck',
lastEtag: lastEtag
})
}
})
} catch (e) {}
}

// 路由拦截
router.beforeResolve(async (to, from, next) => {
next()
try {
await checkUpdate()
if (hasUpdate) {
worker.postMessage({
type: 'destroy'
})
location.reload()
}
} catch (e) {}
})

// 利用worker轮询
worker = new Worker(
/* webpackChunkName: "checkUpdate.worker" */ new URL('../worker/checkUpdate.worker.js', import.meta.url)
)

worker.postMessage({
type: 'check'
})
worker.onmessage = ({ data }) => {
if (data.type === 'hasUpdate') {
hasUpdate = true
confirmReload(data.msg, data.lastEtag)
}
}
}


这里因为缺换路由本来就要刷新页面,用户可无需感知系统更新信息,直接通过请求头的Etag即可,这里的Fetch方法就用head获取相应头就好了。


checkUpdate.worker.js文件如下


let lastEtag
let hasUpdate = false
let intervalId = ''
async function checkUpdate() {
try {
// 检测前端资源是否有更新
let response = await fetch(`/manifest.json?v=${Date.now()}`, {
method: 'get'
})
// 获取最新的etag和data
let etag = response.headers.get('etag')
let data = await response.json()
hasUpdate = lastEtag !== undefined && etag !== lastEtag
if (hasUpdate) {
postMessage({
type: 'hasUpdate',
msg: data.msg,
lastEtag: lastEtag,
etag: etag
})
}
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}

// 监听主线程发送过来的数据
addEventListener('message', ({ data }) => {
if (data.type === 'check') {
// 每5分钟执行一次
// 立即执行一次,获取最新的etag,避免在setInterval等待中系统更新,第一次获取的etag是新的,但是lastEtag还是undefined,不满足条件,错失刷新时机
checkUpdate()
intervalId = setInterval(checkUpdate,5 * 60 * 1000)
}
if (data.type === 'recheck') {
// 每5分钟执行一次
hasUpdate = false
lastEtag = data.lastEtag
intervalId = setInterval(checkUpdate, 5 * 60 * 1000)
}
if (data.type === 'pause') {
clearInterval(intervalId)
}
if (data.type === 'destroy') {
clearInterval(intervalId)
close()
}
})


如果不使用worker直接讲轮询逻辑放在checkUpdate即可


Worker引入


从 webpack 5 开始,你可以使用 Web Workers 代替 worker-loader


new Worker(new URL('./worker.js', import.meta.url));

以下版本的就只能用worker-loader


也可以逻辑写成字符串,然后通过ToURL给new Worker,如下:


function createWorker(f) {
const blob = new Blob(['(' + f.toString() +')()'], {type: "application/javascript"});
const blobUrl = window.URL.createObjectURL(blob);
const worker = new Worker(blobUrl);
return worker;
}

createWorker(function () {
self.addEventListener('message', function (event) {
// 消费信息
self.postMessage('send message')
}, false);
})


worker数据通信



// 主线程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
 uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);
// Worker 线程
self.onmessage = function (e) {
 var uInt8Array = e.data;
 postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
 postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};


但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。


如果要直接转移数据的控制权,就要使用下面的写法。


// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);


然而,并不是所有的对象都可以被转移。只有那些被设计为可转移的对象(用[ Transferable ] IDL 扩展属性修饰),比如ArrayBuffer、MessagePort,ImageBitmap,OffscreenCanvas,才能通过这种方式来传递。转移操作是不可逆的,一旦对象被转移,原始上下文中的引用将不再有效。转移对象可以显著减少复制数据所需的时间和内存。


作者:Zayn
来源:juejin.cn/post/7329280514628534313
收起阅读 »

用 Puppeteer 把繁琐工作给自动化了,太爽啦!

web
最近在鱼皮的编程导航星球做嘉宾,需要输出一些内容。 而很多内容我之前写过,所以想复制过来。 这时候我就遇到了一个令人头疼的问题: 知识星球的编辑器也太难用了! 比如我在掘金编辑器里这样的 markdown 内容: 复制到星球编辑器是这样的: markdow...
继续阅读 »

最近在鱼皮的编程导航星球做嘉宾,需要输出一些内容。


而很多内容我之前写过,所以想复制过来。


这时候我就遇到了一个令人头疼的问题:


知识星球的编辑器也太难用了!


比如我在掘金编辑器里这样的 markdown 内容:



复制到星球编辑器是这样的:



markdown 语法是识别了,但图片没有自动上传。


如果用富文本格式,格式又不对:



而且 gif 没有识别出来,还是需要手动传一次。


这意味着如果文中有几十张图片,那我需要单独把这几十张图片保存到本地,然后光标定位到对应位置,点击上传图片,把图片插进去。


也就是这样:




把每个图片下载下来,保存为不同的后缀名(png、jpg、gif),然后再定位到对应位置,删除原来的链接,插入图片。


然后这样重复十几次,每篇文章都这样来一遍。


是不是想想都觉得很痛苦。。。


那有什么好的办法解决这个问题呢?


于是我想到了 puppeteer。



它是一个网页自动化的 Node.js 工具,基本所有你手动在浏览器里做的事情,都可以用它来自动化完成。


比如点击、移动光标、输入等等。


那前面那个繁琐的问题自然也可以用 puppeteer 自动化来做,解放我们的生产力。


我们来分析下整个流程:


首先打开星球编辑器页面,如果没登录会跳到登录页:



这一步要扫码,没法自动化。


登录之后进入编辑器页面,输入内容:



这时候我们要把其中的图片链接分析出来,自动下载到本地的目录中。


然后记录每个链接所在的行数,把光标移动到对应的行数,点击上传按钮:



上传这一步也要手动来做,选择之前自动下载的图片就行。


然后光标会自动移动到下一个位置,再点击上传按钮,直到所有图片上传完。


文件浏览器这一步是操作系统的功能,没法自动化。


我们把下载图片、在对应位置插入图片的过程给自动化了。只有登录、选择文件这两步还要还要手动做。


但这样已经方便太多了。


流程理清了,我们就来写下代码吧:


import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 0,
height: 0
}
});

const page = await browser.newPage();

await page.goto('http://www.baidu.com');

await page.focus('#kw');

await page.keyboard.type('hello', {
delay: 200
});

await page.click('#su');

引入 puppeteer,跑一个 chrome 浏览器,创建一个页面,导航到 baidu,输入 hello,点击搜索。


puppeteer 的 api 还是很容易懂的。


其中 defaultViewport 设置宽高为 0 是让网页充满整个窗口。


然后我们把它跑起来,因为用到了 es module、顶层 await,需要在 package.json 声明 type 为 module:



声明 type 为 module 就是所有的模块都是 es module 的意思。


然后把它跑起来:



可以看到脚本正确执行了。


然后我们让它打开星球编辑器的网址:


import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 0,
height: 0
}
});

const page = await browser.newPage();

await page.goto('https://wx.zsxq.com/dweb2/article?groupId=51122858222824');


确实跳到登录了:



扫码登录之后进入星球页面,就可以写文章了。



但是,下次跑脚本还是要再登录。


我们不是登录过了么?为啥还需要登录?


因为 chrome 默认的数据保存在一个目录中,叫 userDataDir,而这个目录默认是临时生成的,所以每次保存数据的目录都不一样。


这就导致了每次都需要登陆。


所以我们指定一个固定的 userDataDir 就好了。


import puppeteer from 'puppeteer';
import os from 'os';
import path from 'path';

const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 0,
height: 0
},
userDataDir: path.join(os.homedir(), '.puppeteer-data')
});

const page = await browser.newPage();

await page.goto('https://wx.zsxq.com/dweb2/article?groupId=51122858222824');

通过 os.homedir() 拿到 home 目录,再下面新建一个 .puppeteer-data 的目录来保存用户数据。


这样登录一次之后,下次就不再需要登录了:



这时候可以看到 userDataDir 下是保存了用户数据的:



接下来就是编辑部分的自动化了。


我们要做的事情有这么两件:



  • 提取文本中的所有链接,自动下载。

  • 光标定位到每个链接的位置,自动点击上传按钮。


执行这俩自动化脚本的过程最好让用户控制,比如输入 download-img 就自动下载图片,输入 upload-next 光标就自动定位到下个位置,点击上传。


所以我们引入 readline 这个内置模块接收用户输入。


import readline from 'readline';

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.on('line', async (command) => {
switch(command) {
case 'upload-next':
await uploadNext();
break;
case 'download-img':
await downloadImg();
break;
default:
break;
}
});

async function uploadNext() {
console.log('------');
}
async function downloadImg() {
console.log('+++++++');
}

调用 creatInterface api,指定 input、output 为标准输入输出。


然后当收到一行的输入的时候,根据内容决定执行什么方法:



我们先实现 download-img 的部分:



可以看到,编辑器部分的内容就是 .ql-editor 下的一个个 p 标签。


那我们只要取出所有的 p 标签,选出 ![]() 格式的内容就好了。


这需要一个正则,我们先把这个正则写出来:


整体格式是这样的:


![]()

但[] 和 () 需要转义:


!\[\]\(\)

中间部分是除了 [] 和 () 的任意字符出现任意次,也就是这样:


[^\[\]\(\)]*

并且 () 里的内容需要提取,需要用小括号包裹。


完整正则就是这样的:


!\[[^\[\]\(\)]*\]\(([^\[\]\(\)]*)\)

我们测试下:



可以看到 () 中的内容被正确提取出来了。


然后在网页里取出所有的 p 标签,根据内容过滤,把链接和行数记录下来:


const links = await page.evaluate(() => {
let links = [];
const lines = document.querySelectorAll('.ql-editor p');
for(let i = 0; i < lines.length; i ++) {
const matchRes =lines[i].textContent.trim().match(/!\[[^\[\]\(\)]*\]\(([^\[\]\(\)]*)\)/)
if (matchRes) {
links.push({
index: i,
link: matchRes && matchRes[1],
});
}
}
return links;
})

用 page.evaluate 方法在网页里远程执行一段 js,拿到它的返回结果。


这里拿到的就是所有的图片链接:



其实严格来说这不叫行数,而是第几个 p 标签,想要定位到对应的 p 标签,只要点击它就好了。



我们记录的下标是从 0 开始,而 nth-child 从 1 开始,所以要加 1。


可以看到,光标定位到了正确的位置:



不过先不着急定位光标,我们先把图片下载给搞定。


下载部分的代码如下:


import https from 'https';
import fs from 'fs';

function downloadFile(url, destinationPath, progressCallback) {
let resolve , reject;
const promise = new Promise((x, y) => { resolve = x; reject = y; });

const request = https.get(url, response => {
if (response.statusCode !== 200) {
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
response.resume();

reject(error);
return;
}
const file = fs.createWriteStream(destinationPath);

file.on('finish', () => resolve ());
file.on('error', error => reject(error));

response.pipe(file);

const totalBytes = parseInt(response.headers['content-length'], 10);
if (progressCallback)
response.on('data', onData.bind(null, totalBytes));
});
request.on('error', error => reject(error));
return promise;

function onData(totalBytes, chunk) {
progressCallback(totalBytes, chunk.length);
}
}

用 https 模块的 get 方法请求 url,然后把 response 用流的方式写入文件,并且通过 content-length 的响应头拿到总长度。


这样,在每次 data 方法里就能根据总长度,当前 chunk 的长度,算出下载进度。


我们测试下:


const url = 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/66399947ea6b45289c8d77b6d4568cc5~tplv-k3u1fbpfcp-watermark.image'

let currentTotal = 0;
downloadFile(url, './1.gif', (totalBytes, chunkBytes) => {
const percent = (currentTotal/totalBytes * 100).toFixed(1);
console.log('总长度:' + totalBytes + 'B', '当前已下载:' + currentTotal + 'B','进度' + percent + '%');
currentTotal += chunkBytes;
})


可以看到,图片下载成功了!


但是,我们现在是知道这是个 gif 才给它加上 .gif 后缀,要是任意一个链接,怎么知道它的格式呢?


这个可以用 image-size 这个包:


import sizeOf from 'image-size';
import fs from 'fs';

const buffer = fs.readFileSync('./1.image');
const dimensions = sizeOf(buffer);

console.log(dimensions);

它能拿到图片的类型和宽高信息:



这样我们在下载完改下名就可以了。


const url = 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/66399947ea6b45289c8d77b6d4568cc5~tplv-k3u1fbpfcp-watermark.image'

let currentTotal = 0;
let filePath = './1.image';
downloadFile(url, filePath, (totalBytes, chunkBytes) => {
const percent = (currentTotal/totalBytes * 100).toFixed(1);
console.log('总长度:' + totalBytes + 'B', '当前已下载:' + currentTotal + 'B','进度' + percent + '%');
currentTotal += chunkBytes;

if(currentTotal >= totalBytes) {
const {type} = sizeOf(fs.readFileSync(filePath));
fs.renameSync(filePath, filePath + '.' + type)
}
})

当下载完之后,拿到图片信息,重命名一下,把后缀名改成新的。


注意下图中文件名字的变化:



这样,下载图片就搞定了。


我们把它集成到自动化流程中。


先指定下文件保存位置和文件名:


我们在 home 目录下创建一个 .img 目录吧,然后文件名是 1.image、2.image 的形式。


const imgPath = path.join(os.homedir(), '.img');

fs.rmSync(imgPath, {
recursive: true
});
fs.mkdirSync(imgPath);

for(let i = 0; i< links.length; i++) {
const filePath = path.join(imgPath, (i+1) + '.image');
fs.writeFileSync(filePath, 'aaaa')
}


每次先清空 .img 目录,再创建。


执行之后,确实在 .img 目录下创建了对应的图片文件:



然后把下载图片和重命名的逻辑集成进来:


fs.rmSync(imgPath, {
recursive: true
});
fs.mkdirSync(imgPath);

for(let i = 0; i< links.length; i++) {
const filePath = path.join(imgPath, (i+1) + '.image');
let currentTotal = 0;
downloadFile(links[i].link, filePath, (totalBytes, chunkBytes) => {
currentTotal += chunkBytes;

if(currentTotal >= totalBytes) {
setTimeout(() => {
const {type} = sizeOf(fs.readFileSync(filePath));
fs.renameSync(filePath, filePath + '.' + type)
console.log(`${filePath} 下载完成,重命名为 ${filePath + '.' + type}`);
}, 1000);
}
})
}

这里加了一个 setTimeout,1s 之后执行重命名的逻辑,保证在文件下载完之后再重命名。


效果是这样的:



在 .img 下可以看到所有的图片都下载并重命名成功了:



有 png 也有 gif



下一步只要在不同的位置插入就好了。


我们再来做光标定位的部分。


这部分前面演示过,就是触发对应 p 标签的 click 就好了。


let cursor = 0;
async function uploadNext() {
if(cursor >= links.length) {
return;
}
await page.click(`.ql-editor p:nth-child(${links[cursor].index + 1})`);
await page.evaluate((index) => {
const p = document.querySelector(`.ql-editor p:nth-child(${index + 1})`);
p.textContent = '';
}, links[cursor].index);
await page.click('.ql-image');
cursor ++;
}

我们定义一个游标,从 0 开始,先点击第一个 link 的 p 标签,把它的内容清空,插入下载的图片。


然后再次执行就是插入下一个。


这样依次插入。


我们来试试:


首先,打开编辑器页面,自己登录和输入 markdown 内容:



然后输入 download-img 来下载图片:



之后执行 upload-next 插入第一张图片:



再执行 upload-next 插入第二张图片:



插入的位置非常正确!



依次 upload-next 就能把所有图片插入完成。


对比下之前的体验:


一张张下载图片,根据不同的格式来重命名,然后一张张找到对应的位置,删除原来的链接,插入图片。


现在的体验:


输入 download-img 自动下载图片,不断执行 upload-next 选择图片,自动插入到正确的位置。


这体验差距很明显吧!


这就是用 puppeteer 自动化以后的工作流。


全部代码如下:


import puppeteer from 'puppeteer';
import os from 'os';
import path from 'path';
import fs from 'fs';
import readline from 'readline';
import sizeOf from 'image-size';
import downloadFile from './download.js';

const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 0,
height: 0
},
userDataDir: path.join(os.homedir(), '.puppeteer-data')
});

const page = await browser.newPage();

await page.goto('https://wx.zsxq.com/dweb2/article?groupId=51122858222824');

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.on('line', async (command) => {
switch(command) {
case 'upload-next':
await uploadNext();
break;
case 'download-img':
await downloadImg();
break;
default:
break;
}
});



let links = [];
async function downloadImg() {
links = await page.evaluate(() => {
let links = [];
const lines = document.querySelectorAll('.ql-editor p');
for(let i = 0; i < lines.length; i ++) {
const matchRes =lines[i].textContent.trim().match(/!\[[^\[\]\(\)]*\]\(([^\[\]\(\)]*)\)/)
if (matchRes) {
links.push({
index: i,
link: matchRes && matchRes[1],
});
}
}
return links;
})

const imgPath = path.join(os.homedir(), '.img');

fs.rmSync(imgPath, {
recursive: true
});
fs.mkdirSync(imgPath);


for(let i = 0; i< links.length; i++) {
const filePath = path.join(imgPath, (i+1) + '.image');
let currentTotal = 0;
downloadFile(links[i].link, filePath, (totalBytes, chunkBytes) => {
currentTotal += chunkBytes;

if(currentTotal >= totalBytes) {
setTimeout(() => {
const {type} = sizeOf(fs.readFileSync(filePath));
fs.renameSync(filePath, filePath + '.' + type)
console.log(`${filePath} 下载完成,重命名为 ${filePath + '.' + type}`);
}, 1000);
}
})
}

console.log(links);
}

let cursor = 0;
async function uploadNext() {
if(cursor >= links.length) {
return;
}
await page.click(`.ql-editor p:nth-child(${links[cursor].index + 1})`);
await page.evaluate((index) => {
const p = document.querySelector(`.ql-editor p:nth-child(${index + 1})`);
p.textContent = '';
}, links[cursor].index);
await page.click('.ql-image');
cursor ++;
}

import https from 'https';
import fs from 'fs';

export default function downloadFile(url, destinationPath, progressCallback) {
let resolve , reject;
const promise = new Promise((x, y) => { resolve = x; reject = y; });

const request = https.get(url, response => {
if (response.statusCode !== 200) {
const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`);
response.resume();

reject(error);
return;
}
const file = fs.createWriteStream(destinationPath);

file.on('finish', () => resolve ());
file.on('error', error => reject(error));

response.pipe(file);

const totalBytes = parseInt(response.headers['content-length'], 10);
if (progressCallback)
response.on('data', onData.bind(null, totalBytes));
});
request.on('error', error => reject(error));
return promise;

function onData(totalBytes, chunk) {
progressCallback(totalBytes, chunk.length);
}
}

总结


星球编辑器不好用,每次都要把图片手动下载下来然后插入对应位置,我们通过 puppeteer 把这个流程自动化了。


puppeteer 是一个自动化测试工具,基本所有浏览器手动的操作都能自动化。


我们用 readline 模块读取用户输入,当输入 download-img 的时候,拿到所有的 p 标签,过滤出链接的内容,把信息记录下来。


自动下载图片并用 image-size 读取图片类型来重命名。


然后输入 upload-next,会通过点击对应 p 标签实现光标定位,然后点击上传按钮来选择图片。


自动化以后的工作流程简单太多了,繁琐的工作都给自动化了,体验爽翻了!


作者:zxg_神说要有光
来源:juejin.cn/post/7230757380819812407
收起阅读 »

js如何实现当文本内容过长时,中间显示省略号...,两端正常展示

web
前一阵做需求时,有个小功能实现起来废了点脑细胞,觉得可以记录一下。 产品的具体诉求是:用户点击按钮进入详情页面,详情页内的卡片标题内容过长时,标题的前后两端正常展示,中间用省略号...表示,并且鼠标悬浮后,展示全部内容。 关于鼠标悬浮展示全部内容的代码就不放在...
继续阅读 »

前一阵做需求时,有个小功能实现起来废了点脑细胞,觉得可以记录一下。


产品的具体诉求是:用户点击按钮进入详情页面,详情页内的卡片标题内容过长时,标题的前后两端正常展示,中间用省略号...表示,并且鼠标悬浮后,展示全部内容。


关于鼠标悬浮展示全部内容的代码就不放在这里了,本文主要写关于实现中间省略号...的代码。


实现思路



  1. 获取标题盒子的真实宽度, 我这里用的是clientWidth;

  2. 获取文本内容所占的实际宽度;

  3. 根据文字的大小计算出每个文字所占的宽度;

  4. 判断文本内容的实际宽度是否超出了标题盒子的宽度;

  5. 通过文字所占的宽度累加之和与标题盒子的宽度做对比,计算出要截取位置的索引;

  6. 同理,文本尾部的内容需要翻转一下,然后计算索引,截取完之后再翻转回来;


代码


html代码


<div class="title" id="test">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</div>

css代码: 设置文本不换行,同时设置overflow:hidden让文本溢出盒子隐藏


.title {
width: 640px;
height: 40px;
line-height: 40px;
font-size: 14px;
color: #00b388;
border: 1px solid #ddd;
overflow: hidden;
/* text-overflow: ellipsis; */
white-space: nowrap;
/* box-sizing: border-box; */
padding: 0 10px;
}

javascript代码:


获取标题盒子的宽度时要注意,如果在css样式代码中设置了padding, 就需要获取标题盒子的左右padding值。 通过getComputedStyle属性获取到所有的css样式属性对应的值, 由于获取的padding值都是带具体像素单位的,比如: px,可以用parseInt特殊处理一下。


获取盒子的宽度的代码,我当时开发时是用canvas计算的,但计算的效果不太理想,后来逛社区,发现了嘉琪coder大佬分享的文章,我这里就直接把代码搬过来用吧, 想了解的掘友可以直接滑到文章末尾查看。


判断文本内容是否超出标题盒子


 // 标题盒子dom
const dom = document.getElementById('test');

// 获取dom元素的padding值
function getPadding(el) {
const domCss = window.getComputedStyle(el, null);
const pl = Number.parseInt(domCss.paddingLeft, 10) || 0;
const pr = Number.parseInt(domCss.paddingRight, 10) || 0;
console.log('padding-left:', pl, 'padding-right:', pr);
return {
left: pl,
right: pr
}
}
// 检测dom元素的宽度,
function checkLength(dom) {
// 创建一个 Range 对象
const range = document.createRange();

// 设置选中文本的起始和结束位置
range.setStart(dom, 0),
range.setEnd(dom, dom.childNodes.length);

// 获取元素在文档中的位置和大小信息,这里直接获取的元素的宽度
let rangeWidth = range.getBoundingClientRect().width;

// 获取的宽度一般都会有多位小数点,判断如果小于0.001的就直接舍掉
const offsetWidth = rangeWidth - Math.floor(rangeWidth);
if (offsetWidth < 0.001) {
rangeWidth = Math.floor(rangeWidth);
}

// 获取元素padding值
const { left, right } = getPadding(dom);
const paddingWidth = left + right;

// status:文本内容是否超出标题盒子;
// width: 标题盒子真实能够容纳文本内容的宽度
return {
status: paddingWidth + rangeWidth > dom.clientWidth,
width: dom.clientWidth - paddingWidth
};
}

通过charCodeAt返回指定位置的字符的Unicode编码, 返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。


截取和计算文本长度


// 计算文本长度,当长度之和大于等于dom元素的宽度后,返回当前文字所在的索引,截取时会用到。
function calcTextLength(text, width) {
let realLength = 0;
let index = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2 * 14; // 14是字体大小
}
// 判断长度,为true时终止循环,记录索引并返回
if (realLength >= width) {
index = i;
break;
}
}
return index;
}

// 设置文本内容
function setTextContent(text) {
const { status, width } = checkLength(dom);
let str = '';
if (status) {
// 翻转文本
let reverseStr = text.split('').reverse().join('');

// 计算左右两边文本要截取的字符索引
const leftTextIndex = calcTextLength(text, width);
const rightTextIndex = calcTextLength(reverseStr, width);

// 将右侧字符先截取,后翻转
reverseStr = reverseStr.substring(0, rightTextIndex);
reverseStr = reverseStr.split('').reverse().join('');

// 字符拼接
str = `${text.substring(0, leftTextIndex)}...${reverseStr}`;
} else {
str = text;
}
dom.innerHTML = str;
}

最终实现的效果如下:


image.png


上面就是此功能的所有代码了,如果想要在本地试验的话,可以在本地新建一个html文件,复制上面代码就可以了。


下面记录下从社区内学到的相关知识:



  1. js判断文字被溢出隐藏的几种方法;

  2. JS获取字符串长度的几种常用方法,汉字算两个字节;


1、 js判断文字被溢出隐藏的几种方法


1. Element-plus这个UI框架中的表格组件实现的方案。


通过document.createRangedocument.getBoundingClientRect()这两个方法实现的。也就是我上面代码中实现的checkLength方法。


2. 创建一个隐藏的div模拟实际宽度


通过创建一个不会在页面显示出来的dom元素,然后把文本内容设置进去,真实的文本长度与标题盒子比较宽度,判断是否被溢出隐藏了。


function getDomDivWidth(dom) {
const elementWidth = dom.clientWidth;
const tempElement = document.createElement('div');
const style = window.getComputedStyle(dom, null)
const { left, right } = getPadding(dom); // 这里我写的有点重复了,可以优化
tempElement.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
white-space: nowrap;
padding-left:${style.paddingLeft};
padding-right:${style.paddingRight};
font-size: ${style.fontSize};
font-family: ${style.fontFamily};
font-weight: ${style.fontWeight};
letter-spacing: ${style.letterSpacing};
`
;
tempElement.textContent = dom.textContent;
document.body.appendChild(tempElement);
const obj = {
status: tempElement.clientWidth + right + left > elementWidth,
width: elementWidth - left - right
}
document.body.removeChild(tempElement);
return obj;
}

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


这种方法是在UI框架acro design vue中实现的。外层套一个块级(block)元素,内部是一个行内(inline)元素。给外层元素设置溢出隐藏的样式属性,不对内层元素做处理,这样内层元素的宽度是不变的。因此,通过获取内层元素的宽度和外层元素的宽度作比较,就可以判断出文本是否被溢出隐藏了。


// html代码
<div class="title" id="test">
<span class="content">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</span>
</div>

// 创建一个block元素来包裹inline元素
const content = document.querySelector('.content');
function getBlockDomWidth(dom) {
const { left, right } = getPadding(dom);
console.log(dom.clientWidth, content.clientWidth)
const obj = {
status: dom.clientWidth < content.clientWidth + left + right,
width: dom.clientWidth - left - right
}
return obj;
}

4. 使用canvas中的measureText方法和TextMetrics对象来获取元素的宽度


通过Canvas 2D渲染上下文(context)可以调用measureText方法,此方法会返回TextMetrics对象,该对象的width属性值就是字符占据的宽度,由此也能获取到文本的真实宽度,此方法有弊端,比如说兼容性,精确度等等。


// 获取文本长度
function getTextWidth(text, font = 14) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")
context.font = font
const metrics = context.measureText(text);
return metrics.width
}

2、JS获取字符串长度的几种常用方法


1. 通过charCodeAt判断字符编码


通过charCodeAt获取指定位置字符的Unicode编码,返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。


function calcTextLength(text) {
let realLength = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2;
}
}
return realLength;
}

2. 采取将双字节字符替换成"aa"的做法,取长度


function getTextWidth(text) {
return text.replace(/[^\x00-\xff]/g,"aa").length;
};

参考文章


1. JS如何判断文字被ellipsis了?


2. Canvas API 中文网


3. JS获取字符串长度的常用方法,汉字算两个字节


4. canvas绘制字体偏上不居中问题、文字垂直居中后偏上问题、measureText方法和TextMetrics对象


作者:娜个小部呀
来源:juejin.cn/post/7329967013923962895
收起阅读 »

uniapp踩坑合集

web
1、onPullDownRefresh下拉刷新不生效 pages.json对应的style中enablePullDownRefresh设置为true,开启下拉刷新 { "path" : "pages/list/list", "style" : ...
继续阅读 »

1、onPullDownRefresh下拉刷新不生效


pages.json对应的style中enablePullDownRefresh设置为true,开启下拉刷新
{
"path" : "pages/list/list",
"style" :
{
"navigationBarTitleText": "页面标题名称",
"enablePullDownRefresh": true
}
}

2、onReachBottom上拉加载不生效


page中css样式设置了height:100%;
修改为height:auto;即可

3、onPageScroll生命周期不触发


最外层css样式设置了以下样式
height: 100%;
overflow: scroll;

4、onBackPress监听页面返回生命周期


使用场景:APP手机左滑返回时控制执行某些操作,不直接返回上一页(例如:弹框打开时关闭弹框)


注意事项


1、onBackPress上不可使用async,会导致无法阻止默认返回


2、支付宝小程序只有真机可以监听到非navigateBack引发的返回事件(使用小程序开发工具时不会触发onBackPress),不可以阻止默认返回行为


3、只有在该函数中返回值为 true 时,才表示不执行默认的返回,自行处理此时的业务逻辑


4、当不阻止页面返回却直接调用页面路由相关接口(如:uni.switchTab)时,可能会导致页面显示异常,可以通过延迟调用路由相关接口解决


5、H5 平台,顶部导航栏返回按钮支持 onBackPress(),浏览器默认返回按键及Android手机实体返回键不支持 onBackPress()


6、暂不支持直接在自定义组件中配置该函数,目前只能是在页面中来处理。


//场景1:弹框打开时,返回执行关闭弹框
//html
"searchPop" type="right" @change="popupChange">
<view class="popup-con">1111view>


//js
export default {
data() {
return {
boxShow: false
}
},
onBackPress(options) {
if( this.boxShow ){
this.$refs.searchPop.close();
return true
}
//其他情况执行默认返回
},
methods: {
popupChange(e) {
this.boxShow = e.show;
},
}
}

//场景2,多级返回时
export default {
data() {
return {
boxShow: false
}
},
onBackPress(options) {
if( this.boxShow ){
this.$refs.searchPop.close();
return true
}else{
if (options.from === 'navigateBack') {
return false;
}
uni.navigateBack({
delta: 2
});
}
},
methods: {
popupChange(e) {
this.boxShow = e.show;
},
}
}


5、遮罩层不能遮底部导航栏


应用场景:APP升级弹框提示


uni文档api界面——交互反馈中uni.showModal可以遮罩底部导航栏;
uni.showToast(OBJECT)、uni.showLoading(OBJECT)都无法遮罩底部导航栏;


目前可以采用两种方式解决:自定义底部导航栏、打开时隐藏底部导航栏


方法一:自定义底部导航栏


1、在app.vue页面的onLaunch生命周期中隐藏原生底部  
onLaunchfunction() {
console.log('App Launch')
uni.hideTabBar();
}

2、自己封装tab组件
<template>
<view class="foot-bar">
<view v-if="hasBorder" class="foot-barBorder">view>
<view class="foot-con">
<view class="foot-list" v-for="(item,index) in tabList" :key="index" @tap="tabJump(index,item.pagePath)">
<img v-if="index!=selectedIndex" class="foot-icon" :src="'/'+item.iconPath" mode="heightFix" />
<img v-else class="foot-icon" :src="'/'+item.selectedIconPath" mode="heightFix" />
<text v-if="index!=selectedIndex" :style="textStyle">{{item.text}}text>
<text v-else :style="textSelectStyle">{{item.text}}text>
view>
view>
view>
template>

<script>
export default {
name: "tabBar",
props: {
hasBorder: {
type: Boolean,
default: false
},
selectedIndex:{
type:[String,Number],
default:0
},
textStyle: {
type: Object,
default () {
return {
color:'#999'
}
}
},
textSelectStyle:{
type: Object,
default () {
return {
color: 'rgb(0, 122, 255)'
}
}
}
},
data() {
return {
tabList: [{
"pagePath": "pages/tabBar/component/component",
"iconPath": "static/component.png",
"selectedIconPath": "static/componentHL.png",
"text": "内置组件"
},
{
"pagePath": "pages/tabBar/API/API",
"iconPath": "static/api.png",
"selectedIconPath": "static/apiHL.png",
"text": "接口"
}, {
"pagePath": "pages/tabBar/extUI/extUI",
"iconPath": "static/extui.png",
"selectedIconPath": "static/extuiHL.png",
"text": "扩展组件"
}, {
"pagePath": "pages/tabBar/template/template",
"iconPath": "static/template.png",
"selectedIconPath": "static/templateHL.png",
"text": "模板"
}
]
};
},
methods:{
tabJump(index,url){
if( index == this.selectedIndex ){
return
}
uni.switchTab({
url: '/' + url
})
}
}
}
script>

<style lang="scss" scoped>
.foot-bar {
position: fixed;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 998;
width: 100vw;
.foot-barBorder {
position: absolute;
left: 0px;
right: 0px;
top: -1px;
width: 100vw;
height: 1px;
background-color: #eee;
}
.foot-con {
background-color: #fff;
width: 100vw;
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
.foot-list {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.foot-icon{
width: auto;
height:30px;
}
text{
font-size: 12px;
}
}
}
}
style>


3、需要底部导航的页面引入组件,当前页面是导航栏第几个,selectedIndex就等于几,从0开始
<template>
<view>
<TabBar :selectedIndex="0">TabBar>
view>
template>
<script>
import TabBar from "@/components/tabBar/tabBar";
export default {
components:{
TabBar
},
data() {
return {}
}
}
script>


方法二:打开时隐藏底部导航栏,关闭时打开导航栏


uni.hideTabBar();uni.showTabBar(); 官方文档


image.png


image.png


6、条件编译的正确写法


语法:以 #ifdef 或 #ifndef 加 %PLATFORM% 开头,以 #endif 结尾



  • #ifdef:if defined 仅在某平台存在

  • #ifndef:if not defined 除了某平台均存在

  • %PLATFORM%:平台名称


//仅出现在 App 平台下的代码  
#ifdef APP-PLUS
需条件编译的代码
#endif

//除了 H5 平台,其它平台均存在的代码  
#ifndef H5
需条件编译的代码
#endif

在 H5 平台或微信小程序平台存在的代码  
#ifdef H5 || MP-WEIXIN
需条件编译的代码
#endif

//css样式中  
page{
padding-top:24rpx;
/* #ifdef  H5 */
padding-top:34rpx;
/* #endif */
}

//.vue页面中  
<template>

<view>NFC扫码view>

template>

//page.json页面中  
//json文件中
//API 的条件编译
//生命周期中
//methods方法中
mounted(){
// #ifdef APP-PLUS
//APP更新
this.checkUpdate();
//#endif
}

7、限制input输入类型,replace不生效


//不生效代码
"Code" type="number" placeholder="请输入号码" clearable trim="all" :inputBorder="false" @input="gunChange" maxlength="11">

methods:{
gunChange(e){
this.addForm.oilGunCode = e.replace(/[^\d]/g, '');
},
}

使用v-model绑定值时,replace回显不生效;将v-model修改为:value即可生效;


//生效代码
all" :inputBorder="false" @input="gunChange" maxlength="11">

限制只能输入数字:/[^\d]/g/\D/g (但无法限制0开头)
限制只能输入大小写字母、数字、下划线:/[^\w_]/g
限制只能输入小写字母、数字、下划线:/[^a-z0-9_]/g
限制只能输入数字和点:/[^\d.]/g
限制只能输入中文:/[^\u4e00-\u9fa5]/g
限制只能输入英文(大小写均可):/[^a-zA-Z]/g
去除空格:/\s+/g

8、常见的登录验证


"name" placeholder="请输入用户名" clearable trim="all" maxlength="11">
<uni-easyinput v-model="tell" placeholder="请输入手机号" clearable trim="all" maxlength="11">uni-easyinput>

methods:{
submitHandle(){
//姓名 2-5为的汉字
var reg0 = /^[\u4e00-\u9fa5]{2,5}$/,
//用户名正则,4到16位(字母,数字,下划线,减号)
var reg = /^[a-zA-Z0-9_-]{4,16}$/;
//密码强度正则,最少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符
var reg2 = /^.*(?=.{6,})(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*? ]).*$/;
//Email正则
var reg3 = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
//手机号正则
var reg4 = /^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$/;
//身-份-证号(18位)正则
var reg5 = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
//车牌号正则
var reg6 = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/;

if( !reg.test(this.name) ){
uni.showToast(
title:'用户名格式不正确!'
)
}
},
}

作者:CRMEB技术团队
来源:juejin.cn/post/7272185503822086203
收起阅读 »

React Server Components引发的分歧与机遇

web
介绍 React Server Components 在以前,当用户访问一个 React 应用时,服务端会返回一个空的 HMTL 文件,里面包含一个或多个 JavaScript 文件,浏览器解析 HTML,然后下载 JavaScript 文件,并在客户端呈现网...
继续阅读 »

介绍 React Server Components


在以前,当用户访问一个 React 应用时,服务端会返回一个空的 HMTL 文件,里面包含一个或多个 JavaScript 文件,浏览器解析 HTML,然后下载 JavaScript 文件,并在客户端呈现网页。


React Server Components(RSC)的出现拓展了 React 的范围。顾名思义,React Server Components 就是 React 的服务端组件,它们只在服务端运行,可以调用服务端的方法、访问数据库等。RSC 每次预渲染后把 HTML 发送到客户端,由客户端进行水合(hydrate)并正式渲染。这种做法的好处是,一部分原本要打包在客户端 JavaScript 文件里的代码,现在可以放在服务端运行了,从而减轻客户端的负担,提升应用的整体性能和响应速度。


「充分利用服务器资源」是发布 RSC 的最大动机,换句话说就是:一切不需要交互的内容都应当放到服务端。React 官方举了一个非常典型的例子——渲染 markdown 内容,


// 客户端组件渲染

import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
const html = sanitizeHtml(marked(text));
return (/* 渲染 */);
}

这个例子中,如果用客户端组件渲染,客户端至少要下载 200 多k的文件才能渲染出内容,但这里的 markdown 内容其实不需要交互,也不会因为用户的操作产生更新信息的需求,非常符合使用 RSC 的理念。如果使用 RSC,


// 服务器组件渲染

import marked from 'marked'; // 零打包大小
import sanitizeHtml from 'sanitize-html'; // 零打包大小

function NoteWithMarkdown({text}) {
// 与之前相同
}

依赖包放在服务端,服务端只返回用户需要看到的内容,客户端包一下子就小了 200 多k。


直到这里,社区主流观点都是积极的,直到 Next.js 基于 RSC 的特性野蛮狂奔,分歧出现了。


社区分歧


出现分歧的最根本原因是 React 引入了服务端的概念,服务端组件和客户端组件有着明显差异:



  • 服务器组件不能使用像 useState 和 useEffect 这样的 React hook;客户端则可以;

  • 服务器组件无权访问浏览器 API;客户端有完整的浏览器 API 权限;

  • 服务端有权限直接访问服务端程序和 API;而客户端组件只能通过请求访问部分程序。


随着 Next.js v13 和 v14 版本发布,React 仍然是金丝雀版本的 RSC 被 Next.js 搬到生产环境,‘use client’‘use server’ 被越来越多人讨论,开发者们说现在有「两个 React」,社区开始争吵 React 这些年在进步还是在退步?


WechatIMG4559.jpeg


社区里反对的声音


首先是知名软件工程师 Cassidy Williams,她指出 React 这两年的发展问题:



  • 「两个 React」带来的新概念对大多数人来说并不是清晰易懂的知识,这种分裂可能导致了额外的混淆和学习障碍。

  • 自 2022 年 6 月以来 React 不仅没有新的发布,还鼓励开发者使用上层框架,而这些上层框架不等 RSC 升级成稳定版,就发布了基于 RSC 的特性(就差点名 Next.js 了)。

  • React 近些年有成员加入其他上层框架的团队,不仅疏于更新版本,还疏于更新文档。


React Query 的开发者 Tanner Linsley 也对 React 的发展表达了担忧和不满:



  • 自从 React 引入 hooks 和 suspense API 以来,React 过分专注于少数几个概念,这些新概念虽然在技术上推动了单线程 UI API 的极限和边界,但对他日常为用户提供价值的工作影响甚微。

  • 从 RSC 发布看出来,React 团队对客户端性能已经没有那么强烈的追求了。


地图技术和可视化技术专家 Tom MacWright 对 React 生态系统的分裂进行了批评:



  • 当前 React 更新缓慢,反而说两个上层框架Remix(由 Shopify 资助)和 Next.js(由 Vercel 资助)在激烈竞争。

  • React 团队和 Next.js 团队交集过多,让 Vercel 获得了领先优势,那些不属于 Vercel 和 Facebook 生态系统的其他框架,如 Remix,它们会受到 React 中已修复但未发布错误的影响。


社区里积极的态度


面对社区里越来越多的反对声音,React 主要贡献者 Dan Abramov 也多次发表里自己看法,他对技术的变革持开放态度:



  • Next.js 的 App Router 有着雄心壮志,但是现在还是发展初期,未来会迭代得更优秀。

  • 客户端组件的工作是 UI = f(state),服务端组件的工作是 UI = f(data),React 希望组合二者的优势,实现 UI = f(data, state),他号召社区共同推动实现这一目标。

  • 对于 Next.js 把 RSC 发布到生产版本,Dan 认为“生产就绪”是一个主观的判断,虽然 RSC 还是金丝雀版本,但是 Facebook 也已经大量使用了。他认为在实践中验证才能更快完善技术,最终达到成熟和稳定。

  • 新技术的发展是一个渐进的过程,涉及到不断的测试、反馈和迭代,社区的力量非常重要。


总的来说,Dan 是希望大家放下偏见,共同在实践中摸索出 React 下一阶段的变革。


我的观点


在 RSC 的讨论中,我比较认同 Dan 提出的开放和包容性的观点。我认为,面对技术的发展,要抛弃个人偏见,可以实践验证,也可以持续观察它们的发展。只有心态上拥抱变革,开发者才能在变革中找到机遇。


RSC 在提升现代 Web 应用开发绝对是有积极意义的,最显而易见的优势是它可以提高大型应用的性能、减少客户端负载、优化数据获取流程等,通过 RSC 完成这些工作会比以往的 SSR 方案要更加方便。


随着 Node v20 的发布和 RSC 的应用,前端和服务端的距离进一步缩小,我们有机会见证前端工作“后端化”——前端工程师会处理更多传统上属于后端的工作,如数据查询优化、服务器资源管理等。这实际上为前端工程师打开了一扇门,让我们有机会更全面地掌握整个 web 应用的开发流程,也就是我们常说的“全栈开发”。这样的转变势必会提高前端的职业天花板和扩大前端工作的广度。


作者:BigYe程普
来源:juejin.cn/post/7330602636934774823
收起阅读 »

React 19 发布在即,抢先学习一下新特性

web
React 上一次发布版本还要追溯到2022年6月14日,版本号是18.2.0。在大前端领域,一项热门技术更新如此缓慢属实罕见。这也引起社区里一些大佬的不满,在我的上一篇文章里有提到,感兴趣的朋友可以点击查看:React 社区里的分歧。 在社区不满的声音越来越...
继续阅读 »

React 上一次发布版本还要追溯到2022年6月14日,版本号是18.2.0。在大前端领域,一项热门技术更新如此缓慢属实罕见。这也引起社区里一些大佬的不满,在我的上一篇文章里有提到,感兴趣的朋友可以点击查看:React 社区里的分歧


在社区不满的声音越来越大的背景下,React 新版本的消息终于来了。


React 团队也回应了迟迟未发布新的正式版本的质疑:此前发布到 Canary 版本的多项特性,因为这些特性是相互关联的,所以 React 团队需要投入大量时间确保它们能够协同工作,然后才能逐步发布到 Stable 版本。


事实也确实如此,虽然在这将近两年的时间里 React 没有发布正式版本,但是 Canary 却有一些重磅更新,例如:useuseOptimistic hook,use clientuse server 指令。这些更新客观上丰富了 React 生态系统,特别是推动了 Next.js 和 Remix 等全栈框架的高速发展。


React 团队已经确定,下一个版本将是大版本号,即版本号会是 19.0.0。


v19 新特性预测


现在,让我们根据 React 团队最新发布的消息,来抢先学习一下 v19 版本可能正式发布的新特性。


自动记忆化


你是否还记得 React Conf 2021 上黄玄介绍的 React Forget?




现在,它来了。


它是一个编译器,目前已经在 instagram 的生产环境中应用,React 团队计划在 Meta 的更多平台中应用,并且未来会进行开源发布。


在使用新编译器以前,我们使用 useMemouseCallbackmemo 来手动缓存状态,以减少不必要的重新渲染,这种实现方式虽然可行,但 React 团队认为这并不是他们认为理想的方式,他们一直寻找让 React 在状态变化时自动且只重新渲染必要部分的方案。经过多年的攻坚,现在新的编译器成功落地了。


新的 React 编译器会是一个开箱即用的特性,对开发者来说是又一次开发范式的改变,这也是 v19 最让人期待的功能。


好玩的是,React 团队在介绍新编译器时完全没有提到“React Forget”,这也让好事的网友爆梗了:They forget React Forget & forget to mentioned Forget in the Forget section.🤣


Actions


React Actions 是 React 团队在探索客户端向服务器发送数据的解决方案过程中发展出来的,这个功能允许开发者向 DOM 元素(如 <form/>)传递一个函数:

<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>

action 函数可以同步或异步操作。使用 action 时,React 将为开发者管理数据提交的生命周期,我们可以通过 useFormStatususeFormState 这两个 hook 来访问表单操作的当前状态和响应。


action 可以在执行数据库变更(如增加、删除、更新数据)和实现表单(如登录表单、注册表单)等客户端到服务器交互的场景中使用。


action 不仅可以与 useFormStatususeFormState 结合使用,还可以用与 useOptimisticuse server 结合使用。详细展开篇幅就会很长了,你可以关注我,很快我会单独写一篇文章介绍 action 的详细用法。


指令:use client 与 use server


use clientuse server 两个指令在 Canary 版本发布已久,终于也要在 v19 版本里加入 Stable 版本了。


此前社区频频有人因为 Next.js 在生产环境使用这两个指令而指责 Next.js 在破坏 React 生态、批评 React 团队纵容 Next.js 超前使用非稳定特性。其实大可不必,因为这两个指令就是为 Next.js 和 Remix 这样的全栈框架设计的,短期内普通开发者使用 React 开发应用几乎不会用到它们。


如果你是使用 React,而不是使用全栈框架,你只需要了解这两个指令的作用即可:use clientuse server 标记了前端和服务端两个环境的“分割点”,use client 指示打包工具生成一个 <script> 标签,而 use server 告诉打包工具生成一个POST端点。这两个指令能够让开发者在一份文件里同时写客户端代码和服务端代码。



💡 如果你对这两个指令感兴趣,可以来看我的另一篇文章:「🌍NextJS v13服务端组件和客户端组件及最佳实践



useOptimistic 乐观更新



💡 乐观更新:是一种在前端开发中常用的处理异步操作反馈的策略。它基于一种“乐观”的假设:即假设无论我们向服务器发送什么请求,这些操作都将成功执行,因此在得到服务器响应之前,我们就提前在用户界面上渲染这些改变。

使用场景:点赞、评论、任务添加编辑等。



useOptimistic 是一个新的 hook,很可能在 v19 版本中被标记为稳定版。useOptimistic 允许你在异步操作(如网络请求)进行时,乐观地更新 UI。它通过接受当前状态和一个更新函数作为参数,返回一个在异步操作期间可能会有所不同的状态副本。你需要提供一个函数,这个函数接收当前状态和操作的输入,并返回在操作等待期间使用的乐观状态。


它的用法定义如下:

const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);

// or

const [optimisticState, addOptimistic] = useOptimistic(
state,
// updateFn
(currentState, optimisticValue) => {
// merge and return new state with optimistic value
}
);

参数



  • state: 初始状态值,以及在没有操作进行时返回的值。

  • updateFn(currentState, optimisticValue) : 一个函数,接收当前状态和传递给 addOptimistic 的乐观值,返回结果乐观状态。updateFn 接收两个参数:currentStateoptimisticValue。返回值将是 currentStateoptimisticValue 的合并值。


返回值



  • optimisticState: 产生的乐观状态。当有操作正在进行,它等于 updateFn 返回的值,没有操作正在进行,它等于 state

  • addOptimistic: 这是在进行乐观更新时调用的调度函数。它接受一个参数 optimisticValue(任意类型),并调用带有 stateoptimisticValueupdateFn


更详细的用法如下:

import { useOptimistic } from 'react';

function AppContainer() {
const [state, setState] = useState(initialState); // 假设有一个初始状态
const [optimisticState, addOptimistic] = useOptimistic(
state,
// updateFn
(currentState, optimisticValue) => {
// 合并返回:新状态、乐观值
return { ...currentState, ...optimisticValue };
}
);

// 假设有一个异步操作,如提交表单
function handleSubmit(data) {
// 在实际数据提交前,使用乐观更新
addOptimistic({ data: 'optimistic data' });

// 然后执行异步操作
fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(realData => {
// 使用实际数据更新状态
setState(prevState => ({ ...prevState, data: realData }));
});
}

return (
// 使用 optimisticState 来渲染 UI
<div>{optimisticState.data}</div>
);
}



useOptimistic 会在异步操作进行时先渲染预期的结果,等到异步操作完成,状态更新后,再渲染真实的返回结果(无论成功和失败)。


其它更新


除此之外,React 团队成员 Andrew Clark 还透露2024年还会有以下变化:



  • forwardRef → ref is a prop:简化对子组件内部元素或组件的引用方式,使 ref 作为一个普通的prop传递

  • React.lazy → RSC, promise-as-child:增强了代码分割和懒加载能力

  • useContext → use(Context):提供一种新的方式来访问 Context

  • throw promise → use(promise):改进异步数据加载的处理方式

  • <Context.Provider> → <Context>:简化了上下文提供者的使用


但目前 React 官网没有对以上潜在更新提供详细的信息。


总结


React 的愿景很大,他们希望打破前端和后端的边界,在维持自身客户端能力优势的基础上,同时为社区的全栈框架提供基建。我非常认可他们的做法,因为打破了端的边界,才能帮助前端工程师打破职业天花板。


React 19 会是引入 hooks 之后又一次里程碑式版本,Andrew Clark 说新版本将在 3 月或 4 月发布,让我们拭目以待!


作者:BigYe程普
来源:juejin.cn/post/7339221543992426559
收起阅读 »

【HTML】交友软件上照片的遮罩是如何做的

web
笑谈 我不知道大家有没有在夜深人静的时候感受到孤苦难耐,🐶。于是就去下了一些交友软件来排遣寂寞。可惜的是,有些交友软件真不够意思,连一些漂亮小姐姐的图片都要进行遮罩,完全不考虑兄弟们的感受,😠。所以今天,我们就一起来看看这些软件的遮罩是如何做的,🐶。 调研...
继续阅读 »

笑谈


我不知道大家有没有在夜深人静的时候感受到孤苦难耐,🐶。于是就去下了一些交友软件来排遣寂寞。可惜的是,有些交友软件真不够意思,连一些漂亮小姐姐的图片都要进行遮罩,完全不考虑兄弟们的感受,😠。所以今天,我们就一起来看看这些软件的遮罩是如何做的,🐶。


调研


市场上这些交友软件比较多,就拿一个我朋友他经常玩的一个软件来研究,叫做《XX之恋》,重申一下,我这里没有任何打广告的嫌疑,毕竟是我朋友玩的,🐶。我们接下来看这软件中遮罩的图片。



注:我实在没有在网上找到该软件这些有遮罩的图片,所以只好从自己的主页上截取了下,如果有当事人认为这是自己的话,请速与我联系,我会及时删除的。




正如上面所见,该软件的遮罩效果还是非常不错的,为什么说非常不错呢?个人认为有两个亮点,🐶保命。



  1. 这个遮罩效果让我们知道对面是女生。

  2. 这个遮罩效果也仅仅只能让我们知道对面是女生。


言归正传,这种效果在我们悠久的前端历史上,有一种专业名词 --> 毛玻璃效果



碎碎念:我看了蛮多毛玻璃的技术文章,这个技术大家说都是为了让能人阅读的时候更赏心悦目,能用来遮小姐姐也算是很不错的创新了。🐶



实现


现在我们只需要两样东西,一个是小姐姐的图片,一个是前端的小知识。我都准备好啦。首先我们先介绍知识点。


backdrop-filter属性


我们看MDN的介绍文档


可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。因为它适用于元素背后的所有元素,为了看到效果,必须使元素或其背景至少部分透明。


backdrop-filter有如下常用属性(带了数值,方便理解)



  • 🌟blur(2px): 对元素背后的背景应用2像素的模糊效果。

  • brightness(60%): 将元素背景的亮度调整为原始亮度的60%。

  • contrast(40%): 将元素背景的对比度调整为原始对比度的40%。

  • ...............等


我们主要使用的便是blur属性,对背景图片模糊,达到类似的效果。


我采用的小姐姐图片如下




思路:使用两个Class。第一个Class的背景图片是上面的小姐姐,第二个Class完全覆盖第一个Class,设置blur10px即可。如下是代码。

效果图如下:





最后,希望大家多多点赞支持,兄弟们的点赞支持是我继续写文章的动力!



作者:鑫宝Code
来源:juejin.cn/post/7333986476030935050
收起阅读 »

面试官:实现一个吸附在键盘上的输入框

web
实现效果 话不多说,先上效果和 demo 地址: demo 地址:codesandbox.io/p/devbox/ke… 体验地址:7fsqr8-5173.csb.app 实现原理 要实现一个吸附在键盘上的 input,可以分为以下步骤: 监听键盘高度...
继续阅读 »

实现效果


话不多说,先上效果和 demo 地址:



demo 地址:codesandbox.io/p/devbox/ke…

体验地址:7fsqr8-5173.csb.app



666.gif


实现原理


要实现一个吸附在键盘上的 input,可以分为以下步骤:



  1. 监听键盘高度的变化

  2. 获取「键盘顶部距离视口顶部的高度」

  3. 设置 input 的位置


第一步:监听监听键盘键盘高度的变化


要监听键盘高度的变化,我们得先看看在键盘展开或收起的时候,分别会触发哪些浏览器事件:



  • iOS 和部分 Android 浏览器


    展开:键盘展示时会依次触发 visualViewport resize -> focusin -> visualViewport scroll,部分情况下手动调用 input.focus 不触发 focusin


    收起:键盘收起时会依次触发 visualViewport resize -> focusout -> visualViewport scroll


  • 其他 Android 浏览器


    展开:键盘展示的时候会触发一段连续的 window resize,约过 200 毫秒稳定


    收起:键盘收起的时候会触发一段连续的 window resize,约过 200 毫秒稳定,但是部分手机上有些异常的 case:键盘收起时 viewport 会先变小,然后变大,最后再变小



总结两者来看,我们要监听键盘高度的变化,可以添加以下监听事件:


if (window.visualViewport) {
 window.visualViewport?.addEventListener("resize", listener);
 window.visualViewport?.addEventListener("scroll", listener);
} else {
 window.addEventListener("resize", listener);
}

window.addEventListener("focusin", listener);
window.addEventListener("focusout", listener);

===========================


📚 题外话: 获取键盘展开和收起状态


===========================


在实际业务中,获取键盘展开和收起的状态,同样很常见,要完成状态的判断,我们可以设定以下规则:


判断键盘展开:当 visualViewport resize/window.reszie、visualViewport scroll、focusin 任意一个事件触发时,如果高度减少,并且屏幕减少的高度(键盘高度)大于 200px 时,判断键盘为展开状态(由于 focusin 部分情况下不触发,所以还需要监听其他事件辅助判断键盘是否为展开状态)


判断键盘收起:当 visualViewport resize/window.reszie、visualViewport scroll、focusout 任意一个事件触发时,如果高度增加,并且屏幕减少的高度(键盘高度)小于 200px,判断键盘为收起状态


// 获取当前视口高度
const height = window.visualViewport
? window.visualViewport.height
: window.innerHeight;

// 获取视口增量:视口高度 - 上次获取的视口高度
const diffHeight = height - lastWinHeight;

// 获取键盘高度:默认屏幕高度 - 当前视口高度
const keyboardHeight = DEFAULT_HEIGHT - height;

// 如果高度减少,且键盘高度大于 200,则视为键盘弹起
if (diffHeight < 0 && keyboardHeight > 200) {
   onKeyboardShow();
} else if (diff > 0) {
   onKeyboardHide();
}

同时,为了避免 “收起时 viewport 会先变小,然后变大,最后再变小” 这种情况,我们需要在展开收起状态发生变化的时候加一个200毫秒的防抖,避免键盘状态频繁改变执行“收起 -> 展开 -> 收起”的逻辑


let canChangeStatus = true;

function onKeyboardShow({ height, top }) {
   if (canChangeStatus) {
     canChangeStatus = false;
     setTimeout(() => {
callback();
        canChangeStatus = true;
    }, 200);
  }
}

第二步:获取键盘顶部距离视口顶部的高度


在 safari 浏览器或者部分安卓手机的浏览器中,在点击输入框的时候,可以看到页面会滚动到输入框所在位置(这是想让被软键盘遮挡的部分展示出来),这个时候,其实是触发了虚拟视口 visualViewport 的 scroll 事件,让页面整体往上顶,即使是 fixed 定位也不例外,因此要获取「键盘顶部距离视口顶部的高度」,我们需要进行如下计算:


键盘顶部距离视口顶部的高度 = 视口当前的高度 + 视口滚动上去高度


// 获取当前视口高度
const height = window.visualViewport ? window.visualViewport.height : window.innerHeight;
// 获取视口滚动高度
const viewportScrollTop = window.visualViewport?.pageTop || 0;
// 获取键盘顶部距离视口顶部的距离,这里是关键
const keyboardTop = height + viewportScrollTop;

第三步:设置 input 的位置


我们先设置 input 的 css 样式


input {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 50px;
transition: all .3s;
}

然后再动态调整 input 的 translateY,让 input 可以配合键盘移动,为了保证 input 能够露出,还需要用上一步计算好的「键盘距离页面顶部高度」再减去「元素高度」,从而获得「当前元素的位移」:


当前元素的位移 = 键盘距离页面顶部高度 - 元素高度


// input 的 position 为 absolute、top 为 0
keyboardObserver.on(KeyboardEvent.PositionChange, ({ top }) => {
 input.style.tranform = `translateY(${top - input.clientHeight}px)`;
});

实现原理是不是很简单?不如来看看完整代码吧~


完整代码


import EventEmitter from "eventemitter3";

// 默认屏幕高度
const DEFAULT_HEIGHT = window.innerHeight;
const MIN_KEYBOARD_HEIGHT = 200;

// 键盘事件
export enum KeyboardEvent {
 Show = "Show",
 Hide = "Hide",
 PositionChange = "PositionChange",
}

interface KeyboardInfo {
 height: number;
 top: number;
}

class KeyboardObserver extends EventEmitter {
 inited = false;
 lastWinHeight = DEFAULT_HEIGHT;
 canChangeStatus = true;

 _unbind = () => {};

 // 键盘初始化
 init() {
   if (this.inited) {
     return;
  }
   
   const listener = () => this.adjustPos();

   if (window.visualViewport) {
     window.visualViewport?.addEventListener("resize", listener);
     window.visualViewport?.addEventListener("scroll", listener);
  } else {
     window.addEventListener("resize", listener);
  }

   window.addEventListener("focusin", listener);
   window.addEventListener("focusout", listener);

   this._unbind = () => {
     if (window.visualViewport) {
       window.visualViewport?.removeEventListener("resize", listener);
       window.visualViewport?.removeEventListener("scroll", listener);
    } else {
       window.removeEventListener("resize", listener);
    }

     window.removeEventListener("focusin", listener);
     window.removeEventListener("focusout", listener);
  };
   
   this.inited = true;
}

// 解绑事件
 unbind() {
   this._unbind();
this.inited = false;
}

 // 调整键盘位置
 adjustPos() {
   // 获取当前视口高度
   const height = window.visualViewport
     ? window.visualViewport.height
    : window.innerHeight;

   // 获取键盘高度
   const keyboardHeight = DEFAULT_HEIGHT - height;
   
   // 获取键盘顶部距离视口顶部的距离
   const top = height + (window.visualViewport?.pageTop || 0);

   this.emit(KeyboardEvent.PositionChange, { top });

   // 与上一次计算的屏幕高度的差值
   const diffHeight = height - this.lastWinHeight;

   this.lastWinHeight = height;

   // 如果高度减少,且减少高度大于 200,则视为键盘弹起
   if (diffHeight < 0 && keyboardHeight > MIN_KEYBOARD_HEIGHT) {
     this.onKeyboardShow({ height: keyboardHeight, top });
  } else if (diffHeight > 0) {
     this.onKeyboardHide({ height: keyboardHeight, top });
  }
}

 onKeyboardShow({ height, top }: KeyboardInfo) {
   if (this.canChangeStatus) {
     this.emit(KeyboardEvent.Show, { height, top });
     this.canChangeStatus = false;
     this.setStatus();
  }
}

 onKeyboardHide({ height, top }: KeyboardInfo) {
   if (this.canChangeStatus) {
     this.emit(KeyboardEvent.Hide, { height, top });
     this.canChangeStatus = false;
     this.setStatus();
  }
}

 setStatus() {
   const timer = setTimeout(() => {
     clearTimeout(timer);
     this.canChangeStatus = true;
  }, 300);
}
}

const keyboardObserver = new KeyboardObserver();

export default keyboardObserver;


使用:


keyboardObserver.on(KeyboardEvent.PositionChange, ({ top }) => {
 input.style.tranform = `translateY(${top - input.clientHeight}px)`;
});

作者:DAHUIAAAAAA
来源:juejin.cn/post/7338335869709385780
收起阅读 »

看完zustand源码后,我的TypeScript水平突飞猛进。

web
前言 过年期间在家里没事,把zustand的源码看了一遍,看完后我最大的收获就是ts水平突飞猛进,再去刷那些类型体操题目就变得简单了,下面和大家分享一下zustand库是怎么定义ts类型的。 ts类型推断 个人认为ts最大的作用有两个,一个是类型约束,另外一个...
继续阅读 »

前言


过年期间在家里没事,把zustand的源码看了一遍,看完后我最大的收获就是ts水平突飞猛进,再去刷那些类型体操题目就变得简单了,下面和大家分享一下zustand库是怎么定义ts类型的。


ts类型推断


个人认为ts最大的作用有两个,一个是类型约束,另外一个是类型推断。



  • 类型约束也叫类型安全,在编译阶段就能发现语法错误,可以有效减少低级错误。

  • 类型推断,当你没有标明变量的类型时,编译器会根据一些简单的规则来推断你定义的变量的类型


这一篇主要和大家分享类型推断,类型推断主要有以下几种情况。


根据变量的值自动推导类型


image.png


image.png


函数返回值自动推断


image.png


函数中如果有条件分支,推导出来的返回值类型是所有分支的返回值类型的联合类型


image.png


ts的类型推导方式是懒推导,也就是说不会实际执行代码。


image.png


上图中如果实际执行了,c的类型是能确认为null的。


使用范型推导


image.png


可以看到按照上面写法,对象合并推导不出来,如果能推导出来u3应该等于 {name: string, age: number}


这时候我们可以借助范型来推导


image.png


可以给上面代码简写为这样,编辑器也能推导出来


image.png


实战


实现pick方法


从一个对象中,返回指定的属性名称。


image.png


上面代码中定义了两个范型T和U,T表示对象,U被限定为T的属性名(U extends keyof T),返回值的类型为{[K in U]: T[K]},in的作用就是遍历U这个数组。


image.png


可以看到数组元素被限制了只能是user对象里的key


image.png


image.png


也正确的推导出来了


实现useRequest


先看一个例子


import { useEffect, useState } from 'react';

// 模拟请求接口,返回用户列表
function getUsers(): Promise<{ name: string }[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
name: 'tom',
},
{
name: 'jack',
},
]);
}, 1000);
})
}

const App = () => {

const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<Awaited<ReturnType<typeof getUsers>>>([]);
const [error, setError] = useState(false);

useEffect(() => {
setLoading(true);
getUsers().then((res) => {
setUsers(res);
}).catch(() => {
setError(true);
}).finally(() => {
setLoading(false);
})
}, []);

if (loading) {
return (
<div>loading...</div>
)
}

if (error) {
return (
<div>error</div>
)
}

return (
<div
>

{users.map(u => (
<div key={u.name}>{u.name}</div>
))}
</div>

);
};

export default App;

上面这个例子实现了从后端请求用户列表,然后渲染出来。为了提高用户体验,在加载数据时,加了一个loading,当请求出错时,告诉用户请求失败。


代码比较简单我就不一一讲解了,有行代码需要注意一下。


 const [users, setUsers] = useState<Awaited<ReturnType<typeof getUsers>>>([]);


  • typeof getUsers 获取getUsers函数类型

  • ReturnType 获取某个函数的返回值

  • Awaited 如果函数返回值为Promise,这个可以获取到最终的值类型。


image.png


image.png


可以看到,正确的获取到了getUsers函数的返回值类型。


然而一个很简单的功能需要写那么多代码,肯定是不合理的,那么我们给简化一下。目前市面上已经有不少库来解决这个问题了,比如react-query或ahooks库里的useRequest,都可以解决这个问题,我这里分享的不是具体代码实现,而是怎么写ts。


封装useRequest


import { useEffect, useState } from 'react';

export function useRequest<T extends () => Promise<unknown>>(
fn: T,
): {
loading: boolean;
error: boolean;
data: Awaited<ReturnType<T>>;
} {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [data, setData] = useState<any>();

useEffect(() => {
setLoading(true);
fn().then(res => {
setData(res);
}).catch(() => {
setError(true);
}).finally(() => {
setLoading(false);
});
}, [fn])

return {
loading,
error,
data,
};
}

改造app.tsx文件,使用useRequest


import { useRequest } from './useRequest';

// 模拟请求接口,返回用户列表
function getUsers(): Promise<{ name: string }[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
name: 'tom',
},
{
name: 'jack',
},
]);
}, 1000);
})
}

const App = () => {

const { loading, data: users, error } = useRequest(getUsers);

if (loading) {
return (
<div>loading...</div>
)
}

if (error) {
return (
<div>error</div>
)
}

return (
<div
>

{users.map(u => (
<div
key={u.name}
>

{u.name}
</div>
))}
</div>

);
};

export default App;

对比最开始的代码,是不是简单了很多。


useRequest.tsx代码也很简单,首先使用了范型限制fn只能是一个函数,返回值还必须是Promise。这个hooks返回值loading和error就不说了,主要是data,这个data要求和传进来的方法返回值一致,前面说过,可以使用Awaited<ReturnType<T>>获取函数的返回类型。


image.png


但是上面代码可能会导致bug,看下面代码,如果请求失败,users应该是空的,直接这样使用就会报错了。改造一下,当error为false的时候data为正常类型,error为true的时候data为null,这里可以使用联合类型。


image.png


image.png


image.png


image.png


加了一个判断后,下面就不会报错了。ts在某些时候,真的可以避免一些低级错误,我相信如果没有这个限制,肯定有人在写代码的时候不加判断直接用users。


如果请求接口的函数需要参数怎么办,下面来实现一下。


image.png


使用Parameters获取传入函数的参数类型


image.png


image.png


多个参数也是支持的


image.png


zustand


zustand是一个react状态管理库,使用起来比较简单没啥心智负担,所以我一直在用。


上面带着大家入门了ts的类型推断,下面给大家分享一下zustand的ts定义。我看完zustand源码后,发现这个库的ts定义比功能实现还复杂,这里我只给大家分享ts,具体实现掘金已经有很多大佬写过了,我就不分享了。


先从一个最简单的例子开始


import { create } from 'zustand';

interface State {
count: number;
}

interface Action {
inc: () => void;
}

export const useStore = create<State & Action>((set) => ({
count: 1,
inc: () => set((state) => ({count: state.count + 1})),
}));

image.png


create方法的定义


type Create = {
<T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): UseBoundStore<Mutate<StoreApi<T>, Mos>>
<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) =>
UseBoundStore<Mutate<StoreApi<T>, Mos>>
/**
* @deprecated Use `useStore` hook to bind store
*/

<S extends StoreApi<unknown>>(store: S): UseBoundStore<S>
}

可以看到create有三个重载方法,最后一个废弃不用了,上面例子使用的是第一个方法,第二个重载方法可以这样使用。


image.png


这样做的意义和中间件有关系,这个后面再说。


 <T, Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
): UseBoundStore<Mutate<StoreApi<T>, Mos>>

我们先看第一个方法,定义了两个范型,T表示返回值类型,对应上面例子中create<State & Action>,Mos是给中间件用的,这个等会再说。


create方法的参数initializer定义


initializer: StateCreator<T, [], Mos>

参数initializer对应的类型是StateCreator


export type StateCreator<
T,
Mis extends [StoreMutatorIdentifier, unknown][] = [],
Mos extends [StoreMutatorIdentifier, unknown][] = [],
U = T,
> = ((
setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>,
getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>,
store: Mutate<StoreApi<T>, Mis>,
) =>
U) & { $$storeMutators?: Mos }

StateCreator定义了4个范型,T还是表示返回值类型,其余三个暂时用不到。


((
setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>,
getState: Get<Mutate<StoreApi<T>, Mis>, 'getState', never>,
store: Mutate<StoreApi<T>, Mis>,
) =>
U) & { $$storeMutators?: Mos }

这段ts表明,initializer是一个函数,并且有三个参数,& { $$storeMutators?: Mos }表示交叉类型,也就是说这个函数可能会有$$storeMutators属性。


举个例子:


image.png


因为函数上没有$$name属性,所以报错了,下面给函数加上属性就可以了


image.png


setState: Get<Mutate<StoreApi<T>, Mis>, 'setState', never>

type Get<T, K, F> = K extends keyof T ? T[K] : F

定义了一个Get类型,表示K如果在T对象的可以中,则返回K属性对应的值类型,如果不在返回F。


看个例子


image.png


因为T对象中没有count属性,所以返回never,never表示不存在的类型。


image.png


因为T对象中有name属性,所以返回name字段对应的类型string。


Mutate<StoreApi<T>, Mis>

Mutate这个类型很复杂,是为了解决中间件类型提示出现的,后面再说,没有使用中间件的情况下可以把这段代码简化为StoreApi<T>


export interface StoreApi<T> {
setState: SetStateInternal<T>
getState: () => T
getInitialState: () => T
subscribe: (listener: (state: T, prevState: T) => void) => () => void
/**
* @deprecated Use `unsubscribe` returned by `subscribe`
*/

destroy: () => void
}

type SetStateInternal<T> = {
_(
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: boolean | undefined,
): void
}['_']

到这里我们就看到前面例子中set的定义了,set方法有两个参数,第一个参数可以是前面范型定义的一个对象,可以是对象中的一些属性,也可以是一个函数。第二个属性表示是否覆盖整个对象。


这里的["_"]让我有点迷惑,不知道有啥作用,也可以写成下面这样。


type SetStateInternal<T> = (
partial: T | Partial<T> | { _(state: T): T | Partial<T> }['_'],
replace?: boolean | undefined,
) =>
void

set竟然可以直接设置值,看完源码后,我才知道可以这样用,一般我都是用函数,然后使用函数返回值更新值。


image.png


create方法的返回值类型定义


UseBoundStore<Mutate<StoreApi<T>, Mos>>

上面说了没有中间件的情况下,可以简化为:UseBoundStore<StoreApi<T>>


export type UseBoundStore<S extends WithReact<ReadonlyStoreApi<unknown>>> = {
(): ExtractState<S>
<U>(selector: (state: ExtractState<S>) => U): U
/**
* @deprecated Use `createWithEqualityFn` from 'zustand/traditional'
*/

<U>(
selector: (state: ExtractState<S>) => U,
equalityFn: (a: U, b: U) => boolean,
): U
} & S

type ExtractState<S> = S extends { getState: () => infer T } ? T : never

create返回值是一个函数,这个函数有三个重载方法,并且方法上还有一些属性,(& S)表示这些属性。


第一个重载方法表示没有参数时直接返回ExtractState<S>,ExtractState其实就是获取S对象中getState的返回值类型。


image.png


第二个重载方法有一个参数,可以返回自定义属性。


image.png


第三个重载方法废弃了,就不说了。


image.png


上图中useStore之所以有setState和getState等属性,就是上面& S的作用。


create第二个重载方法的作用


zustand支持使用中间件和编写中间件,看完官方持久化persist中间件的ts定义后,直接把我CPU干烧了,太复杂。


先看一下前面说过的,为啥create方法加了一个重载方法。


<T>(): <Mos extends [StoreMutatorIdentifier, unknown][] = []>(
initializer: StateCreator<T, [], Mos>,
) =>
Mutate<StoreApi<T>, Mos>

这个重载方法主要是给使用了中间件的情况下使用的,看一个例子。


image.png


image.png


上面例子中使用了官方提供的持久化中间件,如果使用第一种重载方法会报错,使用第二种就会报错,下面我们来分析一下为啥会这样。


先给上面代码简化一下


function a() {
console.log('hello');
}
type Fn = {
<T, U extends any[] = []>(name: U): T;
<T>(): <U extends any[] = []>(name: U) => T;
};

const b = a as Fn;

b(['hello'])

image.png


这时候我们调用第一个重载方法没有报错,加了范型后就报错了。


image.png


这是因为不使用范型的时候,编辑器会自动推导类型,如果传了一个范型,那么 U extends any[] = []会强制使用默认值[],所以传['hello']会报错。传[]就不会报错了。第二个重载方法的意义就是给两个范型拆开,这样设置了T不会应用U。


image.png


回到上面问题再看一下create方法的参数类型


image.png


因为传了一个范型约束,所以第二个参数使用默认值[]了


image.png


然而persist中间件返回值类型Mos不为[],所以报错了


image.png


针对这个问题,有两个解决方案


第一个方案是把范型去掉,把范型写在persist上。


image.png


第二个方案是用第二个重载方法


image.png


中间件返回值的类型定义


前面有个东西没说,create返回值里的Mutate<StoreApi<T>, Mos>是干嘛用的,先看下代码


export type Mutate<S, Ms> = number extends Ms['length' & keyof Ms]
? S
: Ms extends []
? S
: Ms extends [[infer Mi, infer Ma], ...infer Mrs]
? Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>
: never

第一次看这个的时候,直接给我看懵了,这是啥,怎么还有递归,然后恶补了一下ts类型体操知识,顺便把github上类型体操题目刷了一下,然后再回来看这个类型体操就很简单了。


先写一个简单的例子让大家入门一下类型体操,合并数组中的对象类型。



// 写一个类型给a转换为{name: string, age: number}

type a = [{ name: string }, { age: number }];

// infer 可以理解为定义一个变量,
// infer F 表示取出数组中第一个元素,
// ...infer R表示把数组中剩余的元素放到R中,
// S & F 表示把S和F合并,
// C<R, S & F>递归剩余元素也合并S中
// 最后返回S

type C<T extends any[] = [], S = {}> = T extends [infer F, ...infer R] ? C<R, S & F> : S


image.png


理解了这个,那上面Mutate也就好理解了。


number extends Ms['length' & keyof Ms] ? S : : Ms extends [] ? S : ...这段表示如果Ms的类型为any[]则返回S,如果Ms为[]也返回S。


正常我们没有使用中间件的时候,Ms是[],所以直接返回S也就是StoreApi<State & Action>


当使用中间件的时候,我们先看下persist返回值类型。


image.png


persist中间件源码中的类型定义


image.png


根据create方法initializer参数定义Mos被自动推导成了[["zustand/persist", State & Action]],Mos对应Mutate里的Ms。


image.png


Ms extends [[infer Mi, infer Ma], ...infer Mrs]

对比上面的Ms类型,Mi为"zustand/persist",Ma为State & Action,Mrs为[]。


Mutate<StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier], Mrs>

接下来开始递归了,StoreMutators<S, Ma>[Mi & StoreMutatorIdentifier],把Mi替换成"zustand/persist",变成StoreMutators<S, Ma>["zustand/persist" & StoreMutatorIdentifier]


image.png


最开始这段代码让很迷惑,因为StoreMutators在项目里定义的是空对象,上面这种写法取不到任何东西。然后我去persist中间件源码里看了一下,原来在persist里给StoreMutators扩展了。


image.png


这几个类型定义可以简单理解为是给Mutate里S添加了persist属性。而persist属性有下面这些方法。


image.png


type Write<T, U> = Omit<T, keyof U> & U

Write表示合并两个类型,如果有重复的key,用后面的覆盖前面。


image.png


可以看到两个对象合并了key,并且name被覆盖成了number类型。


所以当使用persist中间件时,Mutate<StoreApi<T>, Mos>最终类型为StoreApi<T> & { persist: { ... } },所以我们能create返回的值里调用persist里的方法。


image.png


自定义中间件


模拟per中间件,自己也写一个,没有写具体实现,只写了类型定义。


import { StateCreator, StoreMutatorIdentifier } from 'zustand';

type Test = <
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = [],
U = T
>(
initializer: StateCreator<T, [...Mps, ['test', unknown]], Mcs>
) =>
StateCreator<T, Mps, [['test', U], ...Mcs]>;

type Write<T, U> = Omit<T, keyof U> & U;

declare module 'zustand' {
interface StoreMutators<S, A> {
test: Write<S, {test: {log: () => void}}>;
}
}

function a() {
console.log(444);
}

export const test = a as unknown as Test;

image.png


在中间件中也可以重写setState方法


image.png


image.png


总结


到此终于结束了,最复杂的create方法讲完了,其他都是简单的,就不分享了。说实话ts类型定义比代码实现难理解多了,也有可能是我开始的水平不够,所以看起来比较费劲。为了看懂这些ts,我把ts体操类型刷了一遍,现在我感觉自己ts提升了很多。找个时间看一下zod的源码,学习一下它的ts定义。


我看一些ts教程的文章下面,很多人吐槽说TypeScript没有用,个人觉得公司里的业务代码或者个人小项目确实可以不用,但是如果你要开发一个开源框架或组件库,我觉得ts或jsdoc还是有必要的,类型推断和准确的代码提示可以方便用户使用。


作者:前端小付
来源:juejin.cn/post/7339364757386264612
收起阅读 »

停止使用 localStorage !

web
medium 优秀文章翻译,也增加自己的一些使用体验。 非标题党!本文标题很明确的想表达对 localStorage 的不推荐。 localStorage 的弊病 2009年 localStorage 诞生,简单来说就是 5MB 的字符串格式的存储。让我拆...
继续阅读 »

medium 优秀文章翻译,也增加自己的一些使用体验。




非标题党!本文标题很明确的想表达对 localStorage 的不推荐。


localStorage 的弊病


2009年 localStorage 诞生,简单来说就是 5MB 的字符串格式的存储。让我拆解下定义中的关键点。



  • 字符串集合:它只能存储字符串。如果你想要存储或者检索其他格式数据,你必须进行序列化和反序列化。假如你忘记了这一点,你将会遇到各种各样的网络Bug。例如当你存储 true 和 false 时,你还要注意处理 null、undefined、空字符串等潜在返回值。

  • 非结构化数据:JavaScript 结构化克隆算法用于复制复杂的 JavaScript 对象的算法。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。postMessage, WebWorkers, IndexedDB, Caches API, BroadcastChannel, Channel Messaging API, MessagePortHistory API 都是采用的结构化数据! 采用该结构化数据就是为了解决序列化和反序列化 JSON 带来的问题。很遗憾的是 localStorage 并没有更新该特性,并且未来也没有推进的计划。

  • 安全妥协:你永远不会在任何持久化存储中保存敏感数据,但是开发者依旧会在 localStorage 中保存 Session IDs,JWTs、API keys 等敏感信息。这是个常见的安全隐患,你可以在 window.localStorage 中随意查阅。




  • 性能:localStorage 的性能相对于之前已经有了很好的优化,但是对于超量事务的并发应用程序来说,其性能瓶颈一样要重点考虑。

  • 大小限制:localStorage 有 5MB 的限制,而且可能被浏览器删除。对于现代应用来说,5MB 是很小的容量了,几乎很难存储任何媒体数据。它并不是一成不变的,在一些场景下,浏览器也会主动删除部分持久化存储中的数据,这是个通病,这也是何为常规日志上报会有数据丢失的原因之一。因此有甚至需要主动去管理这部分的数据的生命周期,尽管没人告诉你要做这个。还有个点就是存储剩余容量是无法查询的,因此你无法确定操作是否会以为容量达到上限而无法完整写入。

  • WebWorker 无法访问:localStorage 并不是面向未来的API,也不是适用于并放进程中。

  • 非原子化:localStorage 不保证并行操作中的原子性,也没有任何锁能够保证正在写入的数据不会被覆盖。

  • 无数据隔离:localStorage 仅仅是个字符串的对象,应用下所有数据都被混淆在一起,无法进行数据隔离。

  • 无事务:常规数据库都会支持事务操作,也没办法进行分组。所有操作都是同步的、非独立的、无锁定的。

  • 同步阻塞操作:localStorage 不是异步的,它会阻塞主进程。频繁的读取甚至会影响动画的流畅性,在移动端设备最为明显


WebSQL 何去何从?



WebSQL 目标是为 Web 提供一个简单的 SQL 数据库接口,但是浏览器支持程度确实不好。


你可能好奇它为啥会被抛弃?



  • 单一浏览器厂商实现:WebSQL 主要是 Chrome 和 Safari 实现的,由于 Mozilla 和 Microsoft 不支持,业内开发者几乎不采用它。

  • 非 W3C 标准:这个是至关重要的,W3C 在 2010 年将它从标准中移除了。

  • 与 IndexedDB 的竞争:IndexedDB 主要获得更多的关注,且被设计成标准的跨浏览器解决方案了。

  • 安全问题:一些开发人员和安全专家对WebSQL的安全性表示担忧。他们在很多方面都持怀疑态度,包括缺乏权限控制和SOL风格的漏洞。


最终 IndexedDB 成为浏览器存储的标准,被评价为强壮的、跨浏览器友好。但是大多数经验丰富的开发者都视其为瘟疫,那这种推荐又有什么意义呢?


Cookies 又如何呢?


cookie是1994年由网景公司的网络浏览器程序员卢·蒙图利(Lou Montulli)创建的。


本篇文章的标题实际应该是“停止使用 localStorage 和 Cookie”,但是又不全对,我们应该使用安全的 cookies。



  • 4KB体积限制

  • 默认会被请求传输:非跨域 HTTP 请求会携带 cookie 数据,假如数据不需要被每个请求传输,就会带来带宽开销,导致网络加载速度变慢。

  • 安全隐患:cookie更容易受到XSS的攻击。由于cookie会自动包含在对域的每个请求中,因此它们可能成为恶意脚本的目标。

  • 过期:cookie被设计为在给定日期过期。


IndexedDB 呢?



  • 更好的性能:IndexedDB 操作是异步的,不和阻塞主进程。API 被设计为了事件驱动的。

  • 充足的存储配额:与localStorage的5MB上限相比,IndexedDB提供了更大的存储配额(取决于浏览器、操作系统和可用存储。

  • 可靠且结构化数据:Indexed 减少了强制类型转换,并且采用结构化克隆算法,保证数据的完整。



但是你大概并不想直接使用 IndexedDB。


IndexedDB 大概是避免过多依赖的例外。将 IndexedDB 视为后台数据库,你需要的是 ORM 或者 数据库处理程序来进行查询的管理。由于 IndexedDB 糟糕的 API 设计,你更想要一个 IndexedDB 库。



  • 基于 Promise

  • 更好使用

  • 减少样板代码

  • 关注于更关键的部分


本文比较推荐 dexie.js 和 idb 两个针对 indexedDB 的封装库,其中 idb 的体积是最小的,仅仅 1.19 KB,并不会给程序带来负担。


总结


本文的口号虽然是“停止使用 localStorage”,但是在这个时代实际是难以实现的,但是我们确实应该朝着这个目标出发。


未来开发者应该从 Promise()、async/await 和结构化数据中或者更加清晰且有意义的知识,而不应该关注为何数字“0”在条件语句中会成为“true”,而不应该愤怒与客户获得 null 的返回值。


由于 IndexedDB 的性能优势,你存储各种类型的数据,甚至可以使用游标来遍历所有对象。基于这种技术,你甚至可以构建客户端的搜索引擎,而不会像 localStorage 那样影响动画渲染。



IndexedDB is commonly described as “low-level” . There’s absolutely nothing low-level about IndexedDB, it’s just an API with an old-style and unfriendly syntax. But that doesn't negate it’s underlying capabilities, hence common library usage.



你并不需要直接使用 API,一个体积很小的封装库可以帮助你规避这些。


作者:三省法师
来源:juejin.cn/post/7338422591518457871
收起阅读 »

在高德地图上使用threejs,tweenjs,引入外部模型,实现动画效果

web
init展示地图展示地图-入门教程-地图 JS API 2.0|高德地图API (amap.com) 在vue3项目中使用新版高德地图_vue3使用高德地图-CSDN博客踩坑:KEY异常,错误信息:USERKEY_PLAT_NOMATCH ...
继续阅读 »

init

展示地图

展示地图-入门教程-地图 JS API 2.0|高德地图API (amap.com) 在vue3项目中使用新版高德地图_vue3使用高德地图-CSDN博客

踩坑:KEY异常,错误信息:USERKEY_PLAT_NOMATCH 原因:申请的key和使用的服务不匹配,展示地图使用JS-API的key,地理信息解析是web服务

  1. npm包安装
npm i @amap/amap-jsapi-loader --save
  1. 引入
import AMapLoader from '@amap/amap-jsapi-loader';
  1. 初始化
var AMap, map
window._AMapSecurityConfig = {
securityJsCode: "d0543d6e1c9f40e8272aa30af54e8ded",
};
AMapLoader.load({
key: "d0543d6e1c9f40e8272aa30af54e8ded", //申请好的Web端开发者key,调用 load 时必填
version: "2.0", //指定要加载的 JS API 的版本,缺省时默认为 1.4.15
})
.then((res) => {
//JS API 加载完成后获取AMap对象
AMap = res
initMap()
})
.catch((e) => {
console.error(e); //加载错误提示
});
function initMap () {
map = new AMap.Map("map", {
viewMode: '2D', //默认使用 2D 模式
zoom: 11, //地图级别
center: [116.397428, 39.90923], //地图中心点,背景天安门为例
});
}

此时,一个平平无奇的高德地图跃然纸上

Pasted image 20240221095238.png

结合THREE

自定义图层-GLCustomLayer 结合 THREE-自有数据图层-示例中心-JS API 2.0 示例 | 高德地图API (amap.com)

环境搭建:

  1. 高德地图环境搭建看上一章
  2. three环境搭建 :一定要下载对应版本的three,在官网示例中可以查看其引入的three版本
npm i three@0.142 # 24/2/1日数据

入门小案例-引入外部模型

照搬官网案例就行

我这里做了些许改动

  1. 引入外部模型猴头
  2. 创建mesh,参考官网案例
  3. 添加移动功能,将猴头移动入mesh中

效果如图

Pasted image 20240222161502.png 代码如下:

Pasted image 20240222161509.png

  1. 引入
import AMapLoader from '@amap/amap-jsapi-loader'
import * as THREE from 'three';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import {reactive } from 'vue'
  1. 地图准备
// step map 
var AMap, map

window._AMapSecurityConfig = {
securityJsCode: "d0543d6e1c9f40e8272aa30af54e8ded",
};
AMapLoader.load({
key: "d0543d6e1c9f40e8272aa30af54e8ded", //申请好的Web端开发者key,调用 load 时必填
version: "2.0", //指定要加载的 JS API 的版本,缺省时默认为 1.4.15
})
.then((res) => {
//JS API 加载完成后获取AMap对象
AMap = res
createMap() // 创建地图
createThree() // 创建three
})
.catch((e) => {
console.error(e); //加载错误提示
});
function createMap () {
map = new AMap.Map("map", {
center: [116.54, 39.79],
zooms: [2, 20],
zoom: 14,
viewMode: '3D',
pitch: 50,
});
}

  1. three 准备 核心内容是创建GL图层,里面的 render 基本没什么变化
// step three init
var camera, renderer, scene
var model, monkey, mesh
// 数据转换工具
var customCoords
// 测试用数据
var data
function createThree () {
customCoords = map.customCoords;
data = customCoords.lngLatsToCoords([
[116.52, 39.79],
[116.54, 39.79],
[116.56, 39.79],
])
// 创建 GL 图层
var gllayer = new AMap.GLCustomLayer({
// 图层的层级
zIndex: 10,
// 初始化的操作,创建图层过程中执行一次。
init: (gl) => {
initThree(gl)
},
render: () => {
// 这里必须执行!!重新设置 three 的 gl 上下文状态。
renderer.resetState();
// 重新设置图层的渲染中心点,将模型等物体的渲染中心点重置
// 否则和 LOCA 可视化等多个图层能力使用的时候会出现物体位置偏移的问题
customCoords.setCenter([116.52, 39.79]);
var { near, far, fov, up, lookAt, position } =
customCoords.getCameraParams();

// 这里的顺序不能颠倒,否则可能会出现绘制卡顿的效果。
camera.near = near;
camera.far = far;
camera.fov = fov;
camera.position.set(...position);
camera.up.set(...up);
camera.lookAt(...lookAt);
camera.updateProjectionMatrix();

renderer.render(scene, camera);

// 这里必须执行!!重新设置 three 的 gl 上下文状态。
renderer.resetState();
},
});
map.add(gllayer)
window.addEventListener('resize', onWindowResize);
}
function onWindowResize () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}

然后我们来看initThree

function initThree (gl) {
// 这里我们的地图模式是 3D,所以创建一个透视相机,相机的参数初始化可以随意设置,因为在 render 函数中,每一帧都需要同步相机参数,因此这里变得不那么重要。
// 如果你需要 2D 地图(viewMode: '2D'),那么你需要创建一个正交相机
camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
100,
1 << 30
);

renderer = new THREE.WebGLRenderer({
context: gl, // 地图的 gl 上下文
// alpha: true,
// antialias: true,
// canvas: gl.canvas,
});

// 自动清空画布这里必须设置为 false,否则地图底图将无法显示
renderer.autoClear = false;
scene = new THREE.Scene();

// 环境光照和平行光
var aLight = new THREE.AmbientLight(0xffffff, 3);
var dLight = new THREE.DirectionalLight(0xffffff, 10);
dLight.position.set(1000, -100, 900);
scene.add(dLight);
scene.add(aLight);
// 加载模型、mesh
addModel()
addMesh()
}

以上内容也基本不变,但最后加载模型、mesh按照你需要加载的物体变化。

加载外部模型

function addModel () {
const glftLoader = new GLTFLoader()
glftLoader.load("/public/models/monkeyAndCube.glb", function (gltf) {
model = gltf.scene
model.traverse((child) => {
child.scale.set(500, 500, 500); // 放大模型
child.rotation.x = 0.5 * Math.PI;
child.position.z = 0.8;
console.log(child.name)
if (child.name === "monkey") { monkey = child }
})
monkey.position.set(data[0][0], data[0][1], 500); // 设置位置
scene.add(monkey)
})
}

加载mesh

function addMesh () {
// 这里可以使用 three 的各种材质
var mat = new THREE.MeshLambertMaterial({
side: THREE.DoubleSide,
color: 0x1e2f97,
transparent: true,
opacity: .4,
depthWrite: false
})
var geo = new THREE.BoxBufferGeometry(1200, 1200, 1200);
const d = data[2];
mesh = new THREE.Mesh(geo, mat);
mesh.position.set(d[0], d[1], 500);
scene.add(mesh);
animate()
}
// 动画
function animate () {
mesh.rotateZ((1 / 180) * Math.PI);
map.render();
requestAnimationFrame(animate);
}

移动猴头! 记得自己给这个函数加个按钮

function moveMonkey (checked) {
console.log(checked, 'moveMonkey')
if (checked) {
monkey.position.set(data[2][0], data[2][1], 500);
} else {
monkey.position.set(data[0][0], data[0][1], 500);
}
}

[!TIP] 这里面地图和three好像是一起渲染的。如果你只加载了猴头,没加载mesh,此时是没有动画效果的,所以移动猴头的话,这个效果有延迟,缩放平移一下地图就好了。但是如果添加了动画,由于一直调用 map.render() 函数,因此不会出现此问题

大功告成!

结合tween.js

是不是觉得猴头的移动还不够顺滑,加个tween的动画试试

  1. 引入 npm install @tweenjs/tween.js
import * as TWEEN from '@tweenjs/tween.js'
  1. 更改 moveMonkey函数
function moveMonkey (checked) {
console.log(checked, 'moveMonkey')
if (checked) {
// monkey.position.set(data[2][0], data[2][1], 500);
const tween = new TWEEN.Tween(monkey.position)
.to({ x: data[2][0], y: data[2][1], z: 500 }, 2000)
.start()
} else {
// monkey.position.set(data[0][0], data[0][1], 500);
const tween = new TWEEN.Tween(monkey.position)
.to({ x: data[0][0], y: data[0][1], z: 500 }, 2000)
.start()
}
}
  1. 在 render 函数中加入 ( 这里指Three 函数中的 创建GL图层的 render 函数)
 TWEEN.update()

大功告成!Tween的其他功能也可以使用


作者:写bug的小杜
来源:juejin.cn/post/7338240698703314985

收起阅读 »

简单的 Web 端实时日志实现

web
背景 cron service 在执行定时任务时,需要能够实时查看该任务的执行日志以确保程序正确的工作。为了能够在尽可能短的时间内实现该功能,我们需要一个足够简单的方案。 方案如何选择? 我相信大多数开发者第一个想到的就是 WebSocket ,然后是 HTT...
继续阅读 »

Feb-20-2024 21-40-36.gif


背景


cron service 在执行定时任务时,需要能够实时查看该任务的执行日志以确保程序正确的工作。为了能够在尽可能短的时间内实现该功能,我们需要一个足够简单的方案。


方案如何选择?


我相信大多数开发者第一个想到的就是 WebSocket ,然后是 HTTP/SSE 。WebSocket 在很多情况下是实至名归的万金油选择。但这这里他太重了,对服务端和客户端具有侵入性需要花费额外的时间来集成到项目中。


那么 SSE 呢,为什么不是他?
虽然 SSE 即轻量又实时,但 SSE 无法设置请求头。这导致无法使用缓存和通过请求头设置的 Token。而且作为长链接他还一直占用并发额度。



  • WebSocket:❌

    • 优势:

      • 实时性高

      • 不会占用 HTTP 并发额度



    • 劣势:

      • 复杂度较高,需要在客户端和服务器端都进行特殊的处理

      • 消耗更多的服务器资源。





  • SSE(Server-Sent Events):❌

    • 优势:

      • 基于HTTP协议,不需要在服务端和客户端做额外的处理

      • 实时性高



    • 劣势:

      • 无法设置请求头

      • 占用 HTTP 并发额度





  • HTTP:✅

    • 优势:

      • 简单易用,不需要在服务端和客户端做额外的处理。

      • 支持的功能丰富,如缓存,压缩,认证等功能。



    • 劣势:

      • 实时性差,取决于轮询时间间隔。

      • 每次HTTP请求都需要建立新的连接(仅针对 HTTP/0.x 而言)并可能阻塞同源的其他请求而导致性能问题。12

        • HTTP/1.x 支持持久连接以避免每次请求都重新建立 TCP 连接,但数据通信是串行的。

        • HTTP/2.x 支持持久连接且支持并行的数据通信。









以上列出的优缺点是仅对于本文所讨论的场景(即Web 端的实时日志)而言,这三种数据交互技术在其他场景中的优缺点不在本文讨论范围内。



实现


HTTP 轮询已经是老熟人了,不再做介绍。本文的着重于实时日志的实现及优化。


sequenceDiagram

participant 浏览器

participant 服务器

Note right of 浏览器: 首先,获取日志文件最后 X bytes 的内容

浏览器->>服务器: 最新的日志文件有多大?

服务器->>浏览器: 日志文件大小: Y bytes

浏览器->>服务器: 从 Y - X bytes 处返回内容

服务器->>浏览器: 日志文件大小: Y1 bytes, 日志内容: XXXX



loop 持续轮询获取最新的日志

浏览器->>服务器: 从 Y1 bytes 处返回内容

服务器->>浏览器: 日志文件大小: Y2 bytes, 日志内容: XXXX

end

上方是基本工作原理的流程图。
实现的关键点在于



  • 前端如何知道日志文件当前的大小

  • 服务端如何从指定位置获取日志文件内容


这两点都可以通过 HTTP Header 来解决。Content-Range HTTP 响应头会包含完整文件的大小,而 Range HTTP 请求头可以指示服务器返回指定位置的文件内容。


因此在服务端不需要额外的逻辑,仅通过 Web 端代码就可以实现实时日志功能。


代码实现



篇幅限制,代码不会处理异常情况



首先,根据上述流程图。我们需要获取日志文件的大小。


const res = await fetch(URL, {  
method: "GET",
headers: { Range: "bytes=0-0" }
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 日志文件大小
const total = Number.parseInt(match[3], 10);


Range: "bytes=0-0" 指定仅获取第一个字节的内容,以免较大的日志文件导致响应时间过长。



我们发起了一个 GET 请求并将 Range 请求头设置为 bytes=0-0 如果服务器能够正确处理 Range 请求头,响应中将包含一个 Content-Range 列其中将包含日志文件的完整大小,可以通过正则解析拿到。


现在我们已经拿到了日志文件的大小并存储在名为 total 的变量中。然后根据 total 获取到最后 10 KB 的日志内容。


const res = await fetch(url, {  
method: "GET",
headers: {
Range: `bytes=${total - 1000 * 10}-`
}
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 下一次请求的起始位置
const start = Number.parseInt(match[2], 10) + 1;

// 日志内容
const content = await res.text();

现在我们发起了一个 GET 请求并将 Range 请求头设置为 bytes=${total - 1000 * 10}- 以获取最后 10 KB 的日志内容。并且通过正则解析拿到了下一次请求的起始位置。


现在我们已经拿到了日志文件的大小和最后 10 KB 的日志内容。接下来就是持续轮询去获取最新的日志。轮询代码区别仅在于将 Range 标头设置为 bytes=${start}- 以便获取最新的日志。


const res = await fetch(url, {  
method: "GET",
headers: {
Range: `bytes=${start}-`
}
});

const contentRange = res.headers.get("Content-Range") || "";
const match = /bytes (\d+)-(\d+)\/(\d+)/.exec(contentRange);

// 下一次请求的起始位置
start = Number.parseInt(match[2], 10) + 1;

// 日志内容
const content = await res.text();

以上,基本的功能已经实现了。日志内容保存在名为 content 的变量中。


优化


HTTP 轮询常因其高延迟为人诟病,我们可以通过指数退避的方式来尽可能的降低延时且不会显著的增加服务器负担。


指数退避是一种常见的网络重试策略,它会在每次重试时将等待时间乘以一个固定的倍数。这样做的好处是,当网络出现问题时,重试的时间间隔会逐渐增加,直到达到最大值。


一个简单的实现如下:


/**
* 使用指数退避策略获取日志.
*/

export class ExponentialBackoff {
private readonly base: number;
private readonly max: number;
private readonly factor: number;
private retries: number;

/**
* @param base 基础延迟时间 默认 1000ms (1秒)
* @param max 最大延迟时间 默认 60000ms (1分钟)
* @param factor 延迟时间增长因子 默认 2
*/

constructor(base: number = 1000, max: number = 60000, factor: number = 2) {
this.base = base;
this.max = max;
this.factor = factor;
this.retries = 0;
}

/**
* 获取下一次重试的延迟时间.
*/

next() {
const delay = Math.min(this.base * Math.pow(this.factor, this.retries), this.max);
this.retries++;
return delay;
}

/**
* 重置重试次数.
*/

reset() {
this.retries = 0;
}
}

值得一提的是带有 Range 标头的请求成功时会返回 206 Partial Content 状态码。而在请求的范围超出文件大小时会返回 416 Range Not Satisfiable 状态码。我们可以通过这两个状态码来判断请求是否成功。


成功时调用 reset 方法重置重试次数,失败时调用 next 方法获取下一次重试的延迟时间。


总结


即使再优秀的方案也不是银弹,真正合适的方案需要考虑的不仅仅是技术本身,还有业务场景,团队技术栈,团队技术水平等等。在选择技术方案时,我们需要权衡各种因素,而不是盲目的选择最流行的技术。


当我们责备现有的技术方案为何如此糟糕时,或许在那个时间点上,这个方案是最合适的。


Pasted image 20240220213326.png


感谢以下网友的帮助和建议:齐洛格德


Footnotes




作者:siaikin
来源:juejin.cn/post/7337519776796295177
收起阅读 »

【日常总结】iframe内嵌网站初始路由404解决

web
背景 Vue 项目被嵌入到 <iframe> 中,通过src加载页面,结果打开后直接访问404,但是如果不是通过iframe打开的话是不会出现这个问题的。<iframe src="/dist/index.html"></ifram...
继续阅读 »

背景


Vue 项目被嵌入到 <iframe> 中,通过src加载页面,结果打开后直接访问404,但是如果不是通过iframe打开的话是不会出现这个问题的。

<iframe src="/dist/index.html"></iframe>



问题现象


在首次进入时,Vue Router 没有正确地导航到首页路由,而是到了404页面。




原因


vuerouter内部match当前路径的逻辑在这种情况下有问题,所以取到的是错误的信息,找不到这个path,就跳转到了404。前端路由是要依赖当前路径来做处理的,而路径是依赖location字段,但这个时候location跟我们预期的不一样,就跳转错误。




为什么会这样?


是因为iframe 的加载是异步的,router路径跳转之前iframe还没初始化好,导致router内部获取的当前路径不对。




所以思路有2个:



  1. 路径redirect有问题,那就不依赖它,在url中增加参数设置正确的首次跳转路径

  2. 在 iframe 加载完成时,设置正确的路由信息从而导航过去。所以就从iframe加载完后重新设置导航这个关键点下手。


梳理到这里,我意识到上面两种方案是iframe的通信方式:



  • url传递信息

  • postMessage传递消息


解决方案


方案一:iframe的src的URL 参数中增加初始路由信息



  1. 在 iframe 的 src 属性中通过url参数添加首页的路由信息,
<iframe src="/dist/index.html?route=/dashboard"></iframe>


  1. 再在 Vue 应用程序初始化时,如果读取到了router则跳转过去。
import Vue from 'vue'
import App from './App.vue'
import router from './router'

// 方案1:根据添加的url参数判断
const routeParam = new URLSearchParams(window.location.search).get('route');
if (routeParam) {
router.push(routeParam);
}

new Vue({
router,
render: h => h(App),
}).$mount('#app')

我使用的这种方案。


方案二:使用 postMessage 实现父页面和iframe的通信



  1. 父页面监听iframe加载完后通过postMessage发送消息:
const iframe = document.getElementById('myIframe');
if (iframe) {
iframe.onload = function() {
iframe.contentWindow.postMessage({ route: '/dashboard' }, '*');
};
}


  1. iframe内页面main.ts中监听消息,设置路由
// 方案2:postMessage来实现iframe的通信
window.addEventListener('message', handleMessage, false);

function handleMessage(event: any) {
const { data } = event;
if (data.route) {
console.log('message', event, data.route);
router.push(data.route); // 导航到接收到的路由
}
}

这个方案,也可以fix这个bug,但是会先闪到404,再到首页,原因应该是iframe通信是要放到onmounted钩子里的,因为依赖dom加载完,所以会有个时间差。等到iframe onload时候之前已经redirect到404了,然后又重新route了下,效果不是很好。


方案三:利用路由钩子


利用 Vue Router 的导航守卫,在路由导航前检查 URL 参数中是否存在初始路由信息,如果存在则进行相应的导航处

// 方案3: 路由拦截
router.beforeEach((to, from, next) => {
if (to.matched.length === 0) {
// 如果未匹配到任何路由,说明当前页面可能是初始加载
next('/dashboard/base'); // 或者重定向到默认页面
} else {
next(); // 继续正常导航
}
});

结果


先把今天遇到的问题简单总结下:




  • 最后采用了方案1。




  • 方案2 调试出来了,但是效果会有先闪过404页面,后续又到首页的情况,效果不是很好。




  • 3没调出来,暂时不调了。

































标题优点缺点问题
url加参数简单,直接,只需要在内嵌的项目写代码url结构不好看最终使用
postmenssage不需要修改url需要内嵌iframe的页面和iframe的项目中添加额外代码会有一闪而过的效果,体验不好
beforeEach不需要修改url路由钩子每个都处理了逻辑有问题,没调试出来

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

你不知道的JSON.stringify神操

web
一、前言 在我们面试过程中,面试官经常会提及到深浅拷贝的问题。想必大多数小伙伴会说到JSON.parse(JSON.stringify(obj))。正好今天我就和大家好好唠一唠这个JSON.stringify。 二、概念 JSON.stringify对于我...
继续阅读 »

一、前言



在我们面试过程中,面试官经常会提及到深浅拷贝的问题。想必大多数小伙伴会说到JSON.parse(JSON.stringify(obj))。正好今天我就和大家好好唠一唠这个JSON.stringify



二、概念


JSON.stringify对于我们不陌生,一般用来处理序列化(深拷贝)。就是把我们的对象转换JSON字符串,此方法确实很方便在我们的工作中,但是,这个方法也会有一些弊端,只是我们不怎么遇到。


let obj = {
name: 'iyongbao'
}

console.log(JSON.stringify(obj)); // {"name":"iyongbao"}

三、弊端


1. 对函数不友好


如果我们的对象属性是一个函数,那么在序列化的时候该属性丢失


let obj = {
name: 'iyongbao',
foo: function () {
console.log(`${ this.name }是一个小菜鸟!`)
}
}

console.log(JSON.stringify(obj)); // {"name":"iyongbao"}

2. 对undefined不友好


如果对象的属性值是undefined,转换后会丢失


let obj = {
name: undefined
}

console.log(JSON.stringify(obj)); // {}

3. 对正则表达式不友好


如果对象的属性是一个正则表达式,转换后就会变成一个空的Object


let obj = {
name: 'iyongbao',
zoo: /^i/ig,
foo: function () {
console.log(`${ this.name }是一个小菜鸟!`)
}
}

console.log(JSON.stringify(obj)); // {"name":"iyongbao","zoo":{}}

4. 数组对象


如果是一个数组对象,以上的情况也会发生。


let arr = [
{
name: undefined
}
]

console.log(JSON.stringify(arr)); // [{}]

四、JSON.stringify拓展



说完了JSON.stringify不足,下面我们来说一下你可能没有接触过的其他特性,希望看完会对你有所帮助。



1. 接收一个数组(过滤)


其实JSON.stringify第二参数,可能我们不经常用到。我们可以传入一个数组,值就是对应我们对象key,我称之为过滤。


let obj = {
name: 'iyongbao',
age: 25,
hobby: ['JavaScript', 'Vue']
}

let res = JSON.stringify(obj, ['name']);

console.log(res); // {"name":"iyongbao"}

2. 接收一个函数


第二个参数也可以是一个函数,也是类似过滤效果


let obj = {
name: 'iyongbao',
age: 25,
hobby: ['JavaScript', 'Vue']
}

let res = JSON.stringify(obj, (key, value) => {
if (key === 'age') return undefined;
return value;
});

console.log(res); // {"name":"iyongbao","hobby":["JavaScript","Vue"]}

3. 缩进


第三个参数可以接收一个数字,表示缩进多少字符


let obj = {
name: 'iyongbao',
age: 25,
hobby: ['JavaScript', 'Vue']
}

let res = JSON.stringify(obj, null, 2);

console.log(res);

擷取.PNG


4. 自身toJSON方法


对象可以有一个自身的toJSON属性,是一个返回值方法,用来输出我们自定义的数据样式。


let obj = {
name: 'iyongbao',
age: 25,
toJSON: function () {
return {
message: `${ this.name }的年龄为${ this.age }`
}
}
}

let res = JSON.stringify(obj);

console.log(res); // {"message":"iyongbao的年龄为25"}

五、总结


好了,今天就和大家分享到这吧。一般如果真涉及到深拷贝,我还是首选自己封装一个方法或者是使用第三方插件库来做深拷贝,这样最保险,避免不必要的麻烦。


作者:勇宝趣学前端
来源:juejin.cn/post/7337989768636973083
收起阅读 »

掌握Redis核心:常用数据类型的高效运用秘籍!

在数据驱动的时代,高效地存储和处理数据成为了开发者们的重要任务。Redis,作为一个开源的高性能键值对(key-value)数据库,以其独特的数据结构和丰富的功能,成为了众多项目的首选。今天,我们就来揭开Redis的神秘面纱,看看它是如何通过不同的数据类型,为...
继续阅读 »

在数据驱动的时代,高效地存储和处理数据成为了开发者们的重要任务。Redis,作为一个开源的高性能键值对(key-value)数据库,以其独特的数据结构和丰富的功能,成为了众多项目的首选。

今天,我们就来揭开Redis的神秘面纱,看看它是如何通过不同的数据类型,为我们提供高效、灵活的数据存储和处理能力的。

一、字符串(String):数据的基石

String类型简介

字符串是Redis最基本的数据类型,它可以存储文本、数字或者二进制数据。

使用字符串类型,你可以执行原子性的操作,如追加(APPEND)、设置(SET)和获取(GET)。例如,你可以将用户信息作为字符串存储,并通过键快速检索。

  • 一个key对应一个value。

  • String类型是二进制安全的。只要内容可以使用字符串表示就可以存储到string中。比如jpg图片或者序列化的对象。

  • 一个Redis中字符串value最多可以是512M。

常用命令

set key value: 添加键值对。

Description

get key: 查询key对应的键值。

Description

注意:如果设置了两次相同的key,后设置的就会把之前的key覆盖掉。

append key value: 将给定的value追加到原值的末尾。

Description

strlen key: 获得值的长度。

Description

setnx key value: 只有在key 不存在时 ,才能设置 key 的值。

Description

incr key: 将 key 中储存的数字值增1(只能对数字值操作,如果为空,新增值为1)。

Description

decr key: 将 key 中储存的数字值减1(只能对数字值操作,如果为空,新增值为-1)。

Description

incrby / decrby key 步长: 通过自定义步长方式增减 key 中储存的数字值。

Description

mset key1 value1 key2 value2 …: 同时设置一个或多个键值对。

Description

mget key1 key2 key3 …: 同时获取一个或多个value。

Description

msetnx key1 value1 key2 value2 …: 所有给定 key 都不存在时,同时设置一个或多个 key-value 对。

注意:此操作有原子性,只要有一个不符合条件的key。其他的也都不能设置成功。如下图:
Description

getrange key 起始位置、结束位置: 获得值的范围,类似java中的substring。

Description

setrange key 起始位置 value: 用 value覆写key所储存的字符串值,从起始位置开始(索引从0开始)。

Description

setex key 过期时间 value: 可以在设置键值的同时,设置过期时间,单位秒(前面的expire是给已有的键值设置过期时间,注意区别)。

Description

getset key value: 以新换旧,设置了新值同时获得旧值。

Description

应用场景

存储用户信息: 将用户的姓名、年龄等信息作为字符串存储在Redis中,通过键值对的方式快速检索和更新。

计数器: 使用INCR命令实现访问量、点赞数等计数功能。

二、哈希(Hash):组织数据的框架

Hash类型简介

哈希类型允许你存储字段-值对的集合。这种结构非常适合于存储对象,如用户的个人信息。通过HSET和HGET命令,你可以设置和获取哈希中的字段和值。

哈希类型的优势在于它可以对字段进行原子性操作,而不需要读取整个对象。

常用命令

hset key field value: 给key集合中的 field 键赋值value。
Description

hget key1 field: 从key1集合field取出 value。

Description

hmset key1 field1 value1 field2 value2… : 批量设置hash的值(一次性设置多个数据值)。

Description

hexists key1 field: 查看哈希表 key 中,给定域 field 是否存在。

Description

hkeys key: 列出该hash集合的所有field。

Description

hvals key: 列出该hash集合的所有value。

Description

hincrby key field increment: 为哈希表 key 中的域 field 的值加上增量。

Description

hsetnx key field value: 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在。

Description

应用场景

存储用户对象: 将用户对象的多个属性(如姓名、年龄、性别等)存储在一个哈希结构中,通过HGETALL命令获取整个对象,或使用HGET/HSET针对某个属性进行操作。

存储配置信息: 将应用程序的配置信息以字段-值对的形式存储在哈希中,方便集中管理和修改。

三、列表(List):有序数据的队列

List类型简介

列表类型提供了一种顺序存储数据的方式,它类似于Python中的列表或Java中的LinkedList。

你可以使用LPUSH和RPUSH命令在列表的头部或尾部添加元素。列表还支持范围查询和列表内元素的移除操作,非常适合于实现消息队列等场景。

  • List中单键多值,即一个key对应多个value,其中的多个value值使用List进行存储。

  • Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

  • 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

常用命令

**lpush / rpush key value1 value2 value3 … :**从左边/右边插入一个或多个值(l代表left,r代表right)。

**lrange key start stop:**按照索引下标获得元素(从左到右)。

其中lrange k1 0 -1表示取出k1中全部value值(0表示左边第一个,-1代表右边第一个)。

Description

lpop / rpop key: 从左边/右边吐出一个值。值在键在,值光键亡。
pop表示把值拿出来。

Description
图片从左边取出k1的一个value值。如下图:

Description

从右边取出k2的一个value值。如下图:

Description

value值全部取完的时候,key就没有了。如下图:

Description

rpoplpush key1 key2: 从key1列表右边吐出一个值,插到key2列表左边。

Description

lindex key index: 按照索引下标获得元素(从左到右)。

Description

llen key: 获得列表长度。

Description

linsert key before value newvalue : 在value的前面插入newvalue插入值。

Description

lrem key n value: 从开始删除n个value(从左到右)图片。

Description

lset key index value: 将列表key下标为index的值替换成value。

Description

应用场景

消息队列: 使用LPUSH/RPUSH命令将待处理的消息添加到列表头部/尾部,使用LPOP/RPOP从列表中取出并处理消息。

关注列表: 存储用户关注的其他用户列表,使用LINSERT命令在列表中插入新关注的对象。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!

点这里即可查看!

四、集合(Set):去重数据的集合

Set类型简介

集合类型用于存储无序且唯一的数据集合。当你需要存储不允许重复的元素时,集合是一个很好的选择。

SADD命令用于向集合中添加元素,而SMEMBERS可以获取集合中的所有元素。集合还支持交集、并集和差集等高级操作,非常适合于处理标签、好友关系等场景。

Redis的Set是string类型的无序,不可重复集合。Set底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是0(1)。

常用命令

sadd key value1 value2 …: 将一个或多个 member 元素加入到key对应的集合中,已经存在的 member 元素将被忽略

smembers key: 取出key对应的集合中的所有值。

Description

sismember key value: 判断集合是否为含有该value值。1表示有,0表示没有。

Description

scard key: 返回key对应集合中的元素个数。

Description

srem key value1 value2…: 删除key对应的集合中的某些元素。

Description

spop key: 随机从key对应的集合中吐出一个值。

Description

srandmember key n: 随机从key对应的集合中取出n个值。不会从集合中删除 (rand即为random)。

Description

smove sourceKey destinationKey value: 把集合中一个值从一个集合移动到另一个集合。

Description

sinter key1 key2: 返回两个集合的交集元素

sunion key1 key2: 返回两个集合的并集元素。

sdiff key1 key2: 返回两个集合的差集元素(key1中的,不包含key2中的)。

Description

应用场景

好友关系: 将用户的好友列表存储在一个集合中,使用SADD命令添加好友,使用SISMEMBER判断某个用户是否为好友。

标签系统: 将文章或商品的标签存储在集合中,使用SUNION/SINTER等命令进行标签的交集、并集操作。

五、有序集合(Sorted Set)

Sorted Set类型简介

有序集合是Redis中的一个高级数据类型,它结合了集合的唯一性和列表的排序功能。

每个元素都关联一个分数(score),根据分数对元素进行排序。ZADD命令用于添加元素和分数,ZRANGE则可以获取排序后的元素列表。

注:zset是sorted set的缩写

常用命令

zadd key score1 value1 score2 value2…: 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

Description

zrange key start stop (withscores): 返回有序集 key 中,下标在start和stop之间的元素。(带withscores,可以让分数一起和值返回到结果集)。

zrange rank 0 -1 (withscores): 表示取出全部元素,从小到大排列。如下图:

Description

zrangebyscore key min max (withscores): 返回有序集 key 中所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

Description

zrevrangebyscore key max min (withscores): 同上,改为从大到小排列(其中rev表示reverse)。

Description

zincrby key increment value: 为元素的score加上增量。

Description

zrem key value: 删除该集合下,指定值的元素。

Description

zcount key min max: 统计该集合,分数区间内的元素个数。

Description

zrank key value: 返回该值在集合中的排名。(排在第一位的是0)

Description

应用场景

排行榜系统: 可以使用有序集合来存储用户的得分,并根据得分进行排序。

带权重的集合: 有序集合可以用来实现带权重的集合,即每个元素都有一个对应的权重值。

时间线排序: 有序集合可以用于实现时间线排序,即将事件或消息按照时间顺序进行排序。

六、总结

本篇文章我们探索了Redis的五种常用数据类型及常用命令和使用场景。每一种数据类型都有其独特的优势和适用情况,掌握它们将使你在数据处理的道路上更加得心应手。

无论是构建缓存系统,还是实现复杂的数据结构,Redis都能提供强有力的支持。希望这篇文章能帮助你更好地理解和运用Redis,让你的项目在数据的世界中脱颖而出。

收起阅读 »

前端又又出新框架,这次没有打包了

web
最近,前端开发领域又迎来了一个新框架——ofa.js。它的独特之处在于,不依赖于现有的 nodes/npm/webpack 前端开发工作流程。与jQuery类似,只需引用一个脚本,您就能像使用React/Vue/Angular一样轻松地开发大型应用。 极易上...
继续阅读 »

最近,前端开发领域又迎来了一个新框架——ofa.js。它的独特之处在于,不依赖于现有的 nodes/npm/webpack 前端开发工作流程。与jQuery类似,只需引用一个脚本,您就能像使用React/Vue/Angular一样轻松地开发大型应用。


punch-logo.png


极易上手


如果您要开发简单的项目,想要用一个漂亮的按钮组件,例如 Ant Design 中的 Button组件,你需要学习Node.js、NPM和React等知识,才能开始使用该按钮组件。对于非前端开发者或初学者来说,这将是一个漫长的过程。


如果使用基于ofa.js 开发的组件,就不需要这么复杂了;你只需要了解HTML的基础知识(即使不看ofa.js的文档),也可以轻松使用基于ofa.js开发的组件。以下是使用官方的 punch-logo 代码示例:


<!-- 引入ofa.js到您的项目 -->
<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>

<!-- 加载预先开发的punch-logo组件 -->
<l-m src="https://ofajs.github.io/ofa-v4-docs/docs/publics/comps/punch-logo.html"></l-m>

<!-- 使用punch-logo组件 -->
<punch-logo style="margin: 50px 0 0 100px">
<img
src="https://ofajs.github.io/ofa-v4-docs/docs/publics/logo.svg"
logo
height="90"
/>

<h2>不加班了</h2>
<p slot="fly">下班给我</p>
<p slot="fly">迟点下班</p>
<p slot="fly">周末加班</p>
</punch-logo>

punch-demo.gif


你可以最直接拷贝上面的代码,放到一个空白的html文件内运行试试;这使得ofa.js非常容易与传统的Web开发技术栈相融合。


一步封装组件


封装组件同样非常简单,只需一个HTML文件即可实现。以下是一个官方封装的开关(switch)组件示例:


<!-- my-switch.html -->
<template component>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
background-color: #ccc;
transition: background-color 0.4s;
border-radius: 34px;
cursor: pointer;
}

.slider {
position: absolute;
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: transform 0.4s;
border-radius: 50%;
}

.switch.checked {
background-color: #2196f3;
}

.switch.checked .slider {
transform: translateX(26px);
}
</style>
<div class="switch" class:checked="checked" on:click="checked = !checked">
<span class="slider"></span>
</div>
<script>
export default {
tag: "my-switch",
data: {
checked: true,
},
};
</script>
</template>

在使用时,只需使用 l-m 组件引用它:


<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>
<l-m src="./my-switch.html"></l-m>
<my-switch></my-switch>

switch.gif


示例可以在官方网站下方查看。由于无需打包流程,只需将文件上传到静态服务器即可发布,还可以进行跨域引用,这极大降低了组件共享的成本。


多种模板语法糖


ofa.js与Vue和Angular一样提供了许多模板语法糖,主要包括:



  • 文本渲染

  • 属性绑定/双向绑定

  • 事件绑定

  • 条件渲染

  • 列表渲染

  • ...


具体案例可在官网向下滚动至“提供多样便捷的模板语法”处查看。


天生的状态同步高手


与其他框架不同,ofa.js 使用无感状态同步。这意味着数据不需要通过函数操作,只需设置数据对象即可实现状态同步。以下是一个共享黑夜模式的按钮示例:


// is-dark.js
const isDark = $.stanz({
value: false,
});

export default isDark;

<!-- my-button.html -->
<template component>
<style>
:host {
display: block;
}

.container {
display: inline-block;
padding: 0.5em 1em;
color: white;
border-radius: 6px;
background-color: blue;
cursor: pointer;
user-select: none;
}
.container.dark {
background-color: red;
}
</style>
<div class="container" class:dark="isDark.value">
<slot></slot>
</div>
<script>
import isDark from "./is-dark.js";
export default {
data: {
isDark: {},
},
attached() {
// 共享dark对象数据
this.isDark = isDark;
},
detached() {
// 清除内存记录
this.isDark = {};
},
};
</script>
</template>

sync-state.gif


您可以跳转到 状态同步案例 以查看效果。


最简单的表单操作


表单只需调用formData方法,就能生成自动同步数据的对象:


<form id="myForm">
<input type="text" name="username" value="John Doe" />
<div>
sex:
<label>
man
<input type="radio" name="sex" value="man" />
</label>
<label>
woman
<input type radio="radio" name="sex" value="woman" />
</label>
</div>
<textarea name="message">Hello World!</textarea>
</form>
<br />
<div id="logger"></div>
<script>
const data = $("#myForm").formData();

$("#logger").text = data;
data.watch(() => {
$("#logger").text = data;
});
</script>

form1.gif


您还可以轻松地反向设置表单数据:


<form id="myForm">
<input type="text" name="username" value="John Doe" />
<div>
sex:
<label>
man
<input type="radio" name="sex" value="man" />
</label>
<label>
woman
<input type="radio" name="sex" value="woman" />
</label>
</div>
<textarea name="message">Hello World!</textarea>
</form>
<br />
<div id="logger"></div>
<script>
const data = $("#myForm").formData();

setTimeout(() => {
// 反向设置数据
data.username = "Yao";
data.sex = "man";
data.message = "ofa.js is good!";
}, 1000);
</script>

form2.gif


制作自定义表单组件也没有其他框架那么复杂,只需为组件定义valuename属性即可。


具体效果可跳转至formData API查看。


开发应用


您还可以使用ofa.js开发Web应用,然后直接引用已开发的应用到您的网页上:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>应用演示</title>
<script src="https://cdn.jsdelivr.net/gh/kirakiray/ofa.js/dist/ofa.min.js"></script>
</head>
<body>
<o-app src="https://xxxxx.com/app-config.mjs"> </o-app>
</body>
</html>

具体效果可跳转至使用o-app组件查看。


SCSR


官方提供了一种类似服务端渲染的解决方案,它不仅保证了用户体验,还使页面在静态状态下可被搜索引擎爬取。官网采用了SCSR的方案。



SCSR的全称是Static Client-Side Rendering,又称为静态客户端渲染。它是CSR(Client-Side Rendering)的一种变种,在保留了CSR用户体验的基础上,还能够让页面在静态状态下被搜索引擎爬取。



您可以点击SCSR方案以查看详细信息。


代码简洁


当前版本4.3.29的 ofa.min.js 文件仅有52KB,经过gzip压缩后仅有18KB。


其他


最近升级到了v4版本,目前可用的第三方库还比较有限,但以后将逐渐增加。作者正在准备开发基于ofa.js的UI库。


作者是一位中国开发者,快去给 ofa.js 加星吧!


作者:皮卡怪兽
来源:juejin.cn/post/7295576148364460071
收起阅读 »

千分位分隔?一个vue指令搞定

web
说在前面 🎈对数字进行千分位分隔应该是大部分同学都做过的功能了吧,常规的做法通常是编写一个工具函数来对数据进行转换,那么我们可不可以通过vue指令来实现这一功能呢? 效果展示 实现原理 非输入框 非输入框我们只需要对其展示进行处理,我们可以判断绑定元素...
继续阅读 »

说在前面



🎈对数字进行千分位分隔应该是大部分同学都做过的功能了吧,常规的做法通常是编写一个工具函数来对数据进行转换,那么我们可不可以通过vue指令来实现这一功能呢?



效果展示




实现原理


非输入框


非输入框我们只需要对其展示进行处理,我们可以判断绑定元素的innerHTML是否不为空,不为空的话则直接对其innerHTML内容进行格式化。


export default {
bind: function (el, binding) {
const separator = binding.value || ",";
if (el.innerHTML) {
el.innerHTML = addThousandSeparator(el.innerText, separator);
}
},
};

输入框



对于输入框,我们希望其有以下功能:



1、输入的时候去掉分隔符


这里我们只需要监听元素的聚焦(focus)事件即可,取到元素的值,将其分隔符去掉后重新赋值。


el.addEventListener("focus", (event) => {
const value = event.target.value;
event.target.value = deleteThousandSeparator(value, separator);
});

2、输入完成后添加分隔符


这里我们只需要监听元素的失焦(blur)事件即可,取到元素的值,对其进行添加分隔符处理后重新赋值。


el.addEventListener("blur", (event) => {
const value = event.target.value;
event.target.value = addThousandSeparator(value, separator);
});

千分位分隔函数


function addThousandSeparator(num, separator = ",") {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
}


  • num.toString(): 将输入的数字 num 转换为字符串,以便后续处理。

  • .replace(/\B(?=(\d{3})+(?!\d))/g, separator): 这里使用了正则表达式进行替换操作。具体解释如下:



    • \B: 表示非单词边界,用于匹配不在单词边界处的位置。

    • (?=(\d{3})+(?!\d)): 使用正向预查来匹配每三位数字的位置,但不匹配末尾不足三位的数字。

    • (\d{3})+: 匹配连续的三位数字。

    • separator: 作为参数传入的分隔符,默认为 ,。

    • g: 表示全局匹配,即匹配所有满足条件的位置。




这样,通过正则表达式的替换功能,在数字字符串中的每三位数字之间插入指定的千位分隔符,从而实现千位分隔符的添加。


去掉千分位分隔


function deleteThousandSeparator(numberString, separator = ",") {
return numberString.replace(new RegExp(separator, "g"), "");
}

直接将字符串中的分隔符全部替换为空即可。


完整代码


function addThousandSeparator(num, separator = ",") {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
}
function deleteThousandSeparator(numberString, separator = ",") {
return numberString.replace(new RegExp(separator, "g"), "");
}
export default {
bind: function (el, binding) {
const separator = binding.value || ",";
if (el.innerHTML) {
el.innerHTML = addThousandSeparator(el.innerText, separator);
}
el.addEventListener("focus", (event) => {
const value = event.target.value;
event.target.value = deleteThousandSeparator(value, separator);
});
el.addEventListener("blur", (event) => {
const value = event.target.value;
event.target.value = addThousandSeparator(value, separator);
});
},
};


组件库


组件文档


目前该组件也已经收录到我的组件库,组件文档地址如下:
jyeontu.xyz/jvuewheel/#…


组件内容


组件库中还有许多好玩有趣的组件,如:



  • 悬浮按钮

  • 评论组件

  • 词云

  • 瀑布流照片容器

  • 视频动态封面

  • 3D轮播图

  • web桌宠

  • 贡献度面板

  • 拖拽上传

  • 自动补全输入框

  • 图片滑块验证


等等……


组件库源码


组件库已开源到gitee,有兴趣的也可以到这里看看:gitee.com/zheng_yongt…


作者:JYeontu
来源:juejin.cn/post/7337125978802683956
收起阅读 »

uniapp根据不同的环境配置不同的运行基础路径

web
前言当我们使用uniapp开发同一个项目发布不同的环境二级路径不同时,这时候我们就要根据环境来添加运行的基础路径product:xxx-product-api.xxx.com:9002/productConf…text:xxx-text-api.xxx.com...
继续阅读 »

前言

当我们使用uniapp开发同一个项目发布不同的环境二级路径不同时,这时候我们就要根据环境来添加运行的基础路径

product:xxx-product-api.xxx.com:9002/productConf…

text:xxx-text-api.xxx.com:9002/textConfig/

但是当我们使用HBuilderX开发时你会发现manifest.json手动配置Web配置时只能配置一个像这种情况

image.png

碰到这种情况你会怎么处理?

你是不是会在每次打包发布之前变更该环境对应基础路径?

这样也是一种方法,不过其过程太繁琐,废话不多说,上正文!!!

正文

当我们使用HX创建项目时项目中是没有package.json文件和vue.config.js文件的

  1. 在根目录下创建package.json文件,用于配置多个环境也可用于Hx自定义发行
{
"id": "sin-signature",
"name": "签名组件-兼容H5、小程序、APP",
"version": "1.0.0",
"description": "用于uni-app的签名组件,支持H5、小程序、APP,可导出svg矢量图片。",
"keywords": ["签名,签字,svg,canvas"],

"uni-app": {
"scripts": {
"h5-dev": {
"title": "H5-DEV",
"env": {
"NODE_ENV": "development",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "http://192.168.3.3:8081"
},
"define": {
"H5": true
}
},
"h5-xx": {
"title": "H5-XX",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "http://xxx.xx.xx.xxx:8092"
},
"define": {
"H5": true
}
},
"h5-test": {
"title": "H5-TEST",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "https://beta-text-api.nextopen.cn"
},
"define": {
"H5": true
}
},
"h5-prod": {
"title": "H5-PROD",
"env": {
"NODE_ENV": "production",
"UNI_PLATFORM": "h5",
"VUE_APP_BASE_API": "https://product-api.nextopen.cn"
},
"define": {
"H5": true
}
},

}
}
  1. 在根目录下创建vue.config.js文件,用于处理不同环境配置不同的基础路径
const fs = require('fs')
//此处如果是用HBuilderX创建的项目manifest.json文件在项目跟目录,如果是 cli 创建的则在 src 下,这里要注意
//process.env.UNI_INPUT_DIR为项目所在的绝对路径,经测试,相对路径会找不到文件
const manifestPath = process.env.UNI_INPUT_DIR + '/manifest.json'
let Manifest = fs.readFileSync(manifestPath, { encoding: 'utf-8' })
function replaceManifest(path, value) {
const arr = path.split('.')
const len = arr.length
const lastItem = arr[len - 1]

let i = 0
let ManifestArr = Manifest.split(/\n/)

for (let index = 0; index < ManifestArr.length; index++) {
const item = ManifestArr[index]
if (new RegExp(`"${arr[i]}"`).test(item)) ++i;
if (i === len) {
const hasComma = /,/.test(item)
ManifestArr[index] = item.replace(new RegExp(`"${lastItem}"[\\s\\S]*:[\\s\\S]*`), `"${lastItem}": ${value}${hasComma ? ',' : ''}`)
break;
}
}

Manifest = ManifestArr.join('\n')
}

// 动态修改 h5 路由 base
if (process.UNI_SCRIPT_ENV?.NODE_ENV === 'text'){
//测试的 base
replaceManifest('h5.router.base', '"/textConfig/"')
}else if (process.UNI_SCRIPT_ENV?.NODE_ENV === 'product'){
//生产的 base
replaceManifest('h5.router.base', '"/productConfig/"')
}else {
/其他的 base
replaceManifest('h5.router.base', '""')
}

fs.writeFileSync(manifestPath, Manifest, {
"flag": "w"
})

参考uniapp官方文档:uniapp.dcloud.net.cn/collocation…


作者:快乐是Happy
来源:juejin.cn/post/7337208702201086002
收起阅读 »

新年 10 个面试题,我曾 10 次拷问我的灵魂

web
大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点。 免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 10 Interview Questions Every JavaScript Developer S...
继续阅读 »

大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点



免责声明


本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 10 Interview Questions Every JavaScript Developer Should Know in 2024



本期共享的是 —— 新年里前端面试需要掌握的十大面试题,知识面虽小,但思路清晰。


JS 的世界日新月异,多年来面试趋势也与时俱进。本文科普了新年每个 JS 开发者必知必会十大基本问题,涵盖了从闭包到 TDD(测试驱动开发)的一系列主题,为大家提供应对现代 JS 挑战的知识和信心。


1. 闭包到底是什么鬼物?


闭包让我们有权从内部函数访问外部函数的作用域。当函数嵌套时,内部函数可以访问外部函数作用域中声明的变量,即使外部函数返回后也是如此:


const createCat = cat => {
return {
getCat: () => cat,
setCat: newCat => {
cat = newCat
}
}
}

const myCat = createCat('薛定谔')
console.log(myCat.getCat()) // 薛定谔

myCat.setCat('龙猫')
console.log(myCat.getCat()) // 龙猫

闭包变量是对外部作用域变量的实时引用,而不是拷贝。这意味着,如果变更外部作用域变量,那么变更会反映在闭包变量中,反之亦然,这意味着,在同一外部函数中声明的其他函数将可以访问这些变更。


闭包的常见用例包括但不限于:



  • 数据隐藏

  • 柯里化和偏函数(经常用于改进函数组合,比如形参化 Express 中间件或 React 高阶组件)

  • 与事件处理程序和回调共享数据


数据隐藏


封装是面向对象编程的一个重要特征。封装允许我们向外界隐藏类的实现细节。JS 中的闭包允许我们声明对象的私有变量:


// 数据隐藏
const createGirlFans = () => {
let fans = 0
return {
increment: () => ++fans,
decrement: () => --fans,
getFans: () => fans
}
}

柯里化函数和偏函数:


// 一个柯里化函数一次接受多个参数。
const add = a => b => a + b

// 偏函数是已经应用了某些参数的函数,
// 但没有完全应用所有参数。
const increment = add(1) // 偏函数

increment(2) // 3

2. 纯函数是什么鬼物?


纯函数在函数式编程中兹事体大。纯函数是可预测的,这使得它们比非纯函数更易理解、调试和测试。纯函数遵循两个规则:



  1. 确定性 —— 给定相同的输入,纯函数会始终返回相同的输出。

  2. 无副作用 —— 副作用是在被调用函数外部可观察到的、不是其返回值的任何 App 状态更改。


非确定性函数依赖于以下各项的函数,包括但不限于:



  • 随机数生成器

  • 可以改变状态的全局变量

  • 可以改变状态的参数

  • 当前系统时间


副作用包括但不限于:



  • 修改任何外部变量或对象属性(比如全局变量或父函数作用域链中的变量)

  • 打印到控制台

  • 写入屏幕、文件或网络

  • 报错。相反,该函数应该返回表明错误的结果

  • 触发任何外部进程


在 Redux 中,所有 reducer 都必须是纯函数。如果不是,App 的状态不可预测,且时间旅行调试等功能无法奏效。reducer 函数中的杂质还可能导致难以追踪的错误,包括过时的 React 组件状态。


3. 函数组合是什么鬼物?


函数组合是组合两个或多个函数,产生新函数或执行某些计算的过程:(f ∘ g)(x) = f(g(x))


const compose = (f, g) => x => f(g(x))

const g = num => num + 1
const f = num => num * 2

const h = compose(f, g)

h(20) // 42

React 开发者可通过函数组合来清理大型组件树。我们可以将它们组合,创建一个新的高阶组件,而不是嵌套组件,该组件可以通过附加功能强化传递给它的任何组件。


4. 函数式编程是什么鬼物?


函数式编程是一种使用纯函数作为主要组合单元的编程范式。组合在软件开发中兹事体大,几乎所有编程范式都是根据它们使用的组合单元来命名的:



  • 面向对象编程使用对象作为组合单元

  • 过程式编程使用过程作为组合单元

  • 函数式编程使用函数作为组合单元


函数式编程是一种声明式编程范式,这意味着,程序是根据它们做什么,而不是如何做来编写的。这使得函数式程序比命令式程序更容理解、调试和测试。它们往往更加简洁,这降低了代码复杂性,并使其更易维护。


函数式编程的其他关键方面包括但不限于:



  • 不变性 —— 不可变数据结构比可变数据结构更易推理

  • 高阶函数 —— 将其他函数作为参数或返回函数作为结果的函数

  • 避免共享可变状态 —— 共享可变状态使程序难以理解、调试和测试。这也使得推断程序的正确性更加头大


5. Promise 是什么鬼物?


JS 中的 Promise 是一个表示异步操作最终完成或失败的对象,它充当最初未知值的占位符,通常是因为该值的计算尚未完成。


Promise 的主要特征包括但不限于:



  • 有状态Promise 处于以下三种状态之一:

    • 待定:初始状态,既未成功也未失败

    • 已完成:操作成功完成

    • 拒绝:操作失败



  • 不可变:一旦 Promise 被完成或拒绝,其状态就无法改变。它变得不可变,永久保留其结果。这使得 Promise 在异步流控制中变得可靠。

  • 链接Promise 可以链接起来,这意味着,一个 Promise 的输出可以用作另一个 Promise 的输入。这通过使用 .then() 表示成功或使用 .catch() 处理失败来链接,从而允许优雅且可读的顺序异步操作。链接是函数组合的异步等价物。


const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功!')
// 我们也可以在失败时 reject 新错误。
}, 1000)
})

promise
.then(value => {
console.log(value) // 成功
})
.catch(error => {
console.log(error)
})

6. TS 是什么鬼物?


TS 是 JS 的超集,由微软开发和维护。近年来,TS 的人气与日俱增,如果您是一名 JS 工程师,您最终很可能需要使用 TS。它为 JS 添加了静态类型,JS 是一种动态类型语言。静态类型可以辅助开发者在开发过程的早期发现错误,提高代码质量和可维护性。


TS 的主要特点包括但不限于:



  • 静态类型:定义变量和函数参数的类型,确保整个代码一致性。

  • 给力的 IDE 支持:IDE(集成开发环境)可以提供更好的自动补全、导航和重构,使开发过程更加高效。

  • 编译:TS 代码被转译为 JS,使其与任何浏览器或 JS 环境兼容。在此过程中,类型错误会被捕获,使代码更鲁棒。

  • 接口:接口允许我们指定对象和函数必须满足的抽象契约。

  • 与 JS 的兼容性:Ts 与现有 JS 代码高度兼容。JS 代码可以逐步迁移到 JS,使现有项目能够顺利过渡。


interface User {
id: number
name: string
}

type GetUser = (userId: number) => User

const getUser: GetUser = userId => {
// 从数据库或 API 请求用户数据
return {
id: userId,
name: '人猫神话'
}
}

防范 bug 的最佳方案是代码审查、TDD 和 lint 工具(比如 ESLint)。TS 并不能替代这些做法,因为类型正确性并无法保证程序的正确性。即使应用了所有其他质量措施后,TS 偶尔也会发现错误。但它的主要好处是通过 IDE 支持,提供改进的开发体验。


7. Web Components 是什么鬼物?


WC(Web 组件)是一组 Web 平台 API,允许我们创建新的自定义、可重用、封装的 HTML 标签,在网页和 Web App 中使用。WC 是使用 HTML、CSS 和 JS 等开放 Web 技术构建的。它们是浏览器的一部分,不需要外部库或框架。


WC 对于拥有一大坨可能使用不同框架的工程师的大型团队特别有用。WC 允许我们创建可在任何框架或根本没有框架中使用的可重用组件。举个栗子,Adobe(PS 那个公司)的某个设计系统是使用 WC 构建的,并与 React 等流行框架顺利集成。


WC 由来已久,但最近人气爆棚,尤其是在大型组织中。它们被所有主要浏览器支持,并且是 W3C 标准。


8. React Hook 是什么鬼物?


Hook 是让我们无需编写类即可使用状态和其他 React 功能的函数。Hook 允许我们通过调用函数而不是编写类方法,来使用状态、上下文、引用和组件生命周期事件。函数的额外灵活性使我们更好地组织代码,将相关功能分组到单个钩子调用中,并通过在单独的函数调用中实现不相关功能,分离不相关的功能。Hook 提供了一种给力且富有表现力的方式来在组件内编写逻辑。


重要的 React Hook 包括但不限于:



  • useState —— 允许我们向函数式组件添加状态。状态变量在重新渲染之间保留。

  • useEffect —— 允许我们在函数式组件中执行副作用。它将 componentDidMount/componentDidUpdate/componentWillUnmount 的功能组合到单个函数调用中,减少了代码,并创建了比类组件更好的代码组织。

  • useContext —— 允许我们使用函数式组件中的上下文。

  • useRef —— 允许我们创建在组件的生命周期内持续存在的可变引用。

  • 自定义 Hook —— 封装可重用逻辑。这使得在不同组件之间共享逻辑变得容易。


Hook 的规则:Hook 必须在 React 函数的顶层使用(不能在循环、条件或嵌套函数内),且能且只能在 React 函数式组件或自定义 Hook 中使用。


Hook 解决了类组件的若干常见痛点,比如需要在构造函数中绑定方法,以及需要将功能拆分为多个生命周期方法。它们还使得在组件之间共享逻辑以及重用有状态逻辑,而无需更改组件层次结构更容易。


9. 如何在 React 中创建点击计数器?


我们可以使用 useState 钩子在 React 中创建点击计数器,如下所示:


import React, { useState } from 'react'

const ClickCounter = () => {
const [count, setCount] = useState(0) // 初始化为 0

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count => count + 1)}>Click me</button>
</div>

)
}

export default ClickCounter

粉丝请注意,当我们从现有状态派生新值时,将函数传递给 setCount 是最佳实践,确保我们始终使用最新状态。


10. TDD 是什么鬼物?


TDD(测试驱动开发)是一种软件开发方法,其中测试是在实际代码之前编写的。它围绕一个简短的重复开发周期,旨在确保代码满足指定的要求且没有错误。TDD 在提高代码质量、减少错误和提高开发者生产力方面,可以发挥至关重要的作用。


开发团队生产力最重要的衡量标准之一是部署频率。持续交付的主要障碍之一是对变化的恐惧。TDD 通过确保代码始终处于可部署状态,辅助减少这种恐惧。这使得部署新功能和错误修复更容易,提高了部署频率。


测试先行多了一大坨福利,包括但不限于:



  • 更好的代码覆盖率:测试先行更有可能覆盖所有极端情况。

  • 改进的 API 设计:测试迫使我们在编写代码之前考虑 API 设计,这有助于避免将实现细节泄漏到 API 中。

  • 更少的 bug:测试先行可以辅助在开发过程中尽早发现错误,这样更容易修复。

  • 更好的代码质量:测试先行迫使我们编写模块化、松耦合的代码,这样更容易维护和重用。


TDD 的关键步骤包括但不限于:



  1. 编写测试:此测试最初会失败,因为相应的功能尚不存在。

  2. 编写实现:足以通过测试。

  3. 自信重构:一旦测试通过,就可以自信重构代码。重构是在不改变其外部行为的情况下,重构现有代码的过程。其目的是清理代码、提高可读性并降低复杂性。测试到位后,如果我们犯错了,我们会立即因测试失败而收到警报。


重复:针对每个功能需求重复该循环,逐步构建软件,同时确保所有测试继续通过。


学习曲线:TDD 是一项需要相当长的时间才能培养的技能和纪律。经过大半年的 TDD 体验后,我们可能仍觉得 TDD 难如脱单,且妨碍了生产力。虽然但是,使用 TDD 两年后,我们可能会发现它已经成为第二天性,并且比以前更有效率。


耗时:为每个小功能编写测试一开始可能会感觉很耗时,但长远来看,这通常会带来回报,减少错误并简化维护。我常常告诫大家,“如果你认为自己没有时间进行 TDD,那么你真的没有时间跳过 TDD。”


本期话题是 —— 你遭遇灵魂拷问的回头率最高的面试题是哪一道?


欢迎在本文下方群聊自由言论,文明共享。谢谢大家的点赞,掰掰~


《前端 9 点半》每日更新,坚持阅读,自律打卡,每天一次,进步一点


26-cat.gif


作者:人猫神话
来源:juejin.cn/post/7334653735359348777
收起阅读 »

集帅(美)们,别再写 :key = "index" 啦!

web
浅聊一下 灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞... 开始 在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点...
继续阅读 »

浅聊一下


灵魂拷问:你有没有在v-for里使用过:key = "index",如果有,我希望你马上改正过来并且给我点个赞,如果没有,来都来了,顺手给我点个赞...


开始


在向掘友们解释为什么不能使用 :key = "index" 之前,我想我还得向你们铺垫一点东西


虚拟DOM


什么是虚拟DOM呢?虚拟DOM是一个对象,没想到吧...我们来看看Vue是如何将template模板里面的东西交给浏览器来渲染的


image.png


首先通过 compiler 将 template模板变成一个虚拟DOM,再将虚拟DOM转换成HTML,最后再交给浏览器V8引擎渲染,那么虚拟DOM是什么样的呢?


html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">script>
head>

<body>
<div id="app">
<ul id="item">
<li v-for="item in list" class="item">{{item}}li>
ul>
div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['vue','js','html'])
return {
list
}
}
}).
mount('#app')
script>
body>

html>

在这里,template模板实际上是


 <ul>
<li v-for="item in list">{{item}}li>
ul>

通过v-for循环,渲染出来了3个li


<ul>
<li>vue<li>
<li>js<li>
<li>html<li>
ul>

我们的compiler会将这个模板转化成虚拟DOM


let oldDom = {
tagName = 'ul',
props:{
//存放id 和 class 等
id:'item'
},
children[
{
tagName = 'li',
props:{
class:'item'
},
children:['vue']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['js']
},
{
tagName = 'li',
props:{
class:'item'
},
children:['html']
},
]
}

diff算法


给前面的例子来点刺激的,加上一个按钮和反转函数,点击按钮,list反转


html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">script>
head>

<body>
<div id="app">
<ul>
<li v-for="item in list">{{item}}li>
ul>
<button @click="change">changebutton>
div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['唱','跳','rap','篮球'])
const change = ()=>{
list.
value.reverse()
}
const add = ()=>{
list.
value.unshift('6')
}
return {
list,
change,
}
}
}).
mount('#app')
script>
body>

html>

点击change按钮,此时我们的DOM更改vue又是如何来更新DOM的呢?


image.png


众所周知,回流和重绘会消耗极大的性能,而当DOM发生变更的时候会触发回流重绘(可以去看我的文章(从输入4399.com到页面渲染之间的回流和重绘),那么vue3就有一个diff算法,用来优化性能


image.png


当DOM更改,compiler会生成一个新的虚拟DOM,然后通过diff算法来生成一个补丁包,用来记录旧DOM和新DOM的差异,然后再拿到html里面进行修改,最后再交给浏览器V8进行渲染


简单介绍一下diff算法的比较规则



  1. 同层比较,是不是相同的结点,不相同直接废弃老DOM

  2. 是相同结点,比较结点上的属性,产生一个补丁包

  3. 继续比较下一层的子节点,采用双端对列的方式,尽量复用,产生一个补丁包

  4. 同上


image.png


别再写 :key = "index"


要说别写 :key = "index" ,我们得先明白key是用来干什么的...如果没有key,那么在diff算法对新旧虚拟DOM进行比较的时候就没法比较了,你看这里有两个一样的vue,当反转顺序以后diff算法不知道哪个vue该对应哪个vue了


image.png


如果我们用index来充当key的话来看,当我们在头部再插入一个结点的时候,后面的index其实是改变了的,导致diff算法在比较的时候认为他们与原虚拟DM都不相同,那么diff算法就等于没有用...


image.png


可以用随机数吗?


  • for="item in list" :key="Math.random()">

  • 想出这种办法的,也是一个狠人...当然是不行的,因为在template模板更新时,会产生一个新的虚拟DOM,而这个虚拟DOM里面的key也是随机值,和原虚拟DOM里的key99.99999%是不一样的...


    结尾


    希望你以后再也不会写 :key = "index" 了


    作者:滚去睡觉
    来源:juejin.cn/post/7337513012394115111
    收起阅读 »

    图片转base64,实现图片上传?你学会了吗?

    web
    前言 前段时间在写我的VUE全栈项目的时候,遇到要把前端的照片上传到后端,再由后端存到数据库的问题,通过网上查找资料,看了一些其他写者的解决方法,最终采用转BASE64的方法。本人觉得把上传的图片转为BASE64格式相比其他是比较简单的。 什么是BASE64 ...
    继续阅读 »

    前言


    前段时间在写我的VUE全栈项目的时候,遇到要把前端的照片上传到后端,再由后端存到数据库的问题,通过网上查找资料,看了一些其他写者的解决方法,最终采用转BASE64的方法。本人觉得把上传的图片转为BASE64格式相比其他是比较简单的。


    什么是BASE64


    Base64是一种用64个字符来表示任意二进制数据的方法。它是一种编码方式,而非加密方式,即可以将一张图片数据编码成一串字符串,使用该字符串代替图像地址


    BASE64的优缺点


    优点: 减少一张图片的http请求


    缺点: 导致转换后的css文件体积增大,而CSS 文件的体积直接影响渲染,导致用户会长时间注视空白屏幕,而且转换后的数据是一大串字符串。



    注意:图片转BASE64格式的适合小图片或者极简单图片,大图片不划算。它的格式为:data:image/type;base64,xxxx...



    虽然说这种方式不适用于体积大的图片,但不得不说有时候还挺方便的。由于在我的vue项目中上传的图片都比较小,单一,为了方便我采用了这种方式来实现将前端上传的图片存到数据库中。


    话不多说,进入正题!下面以Vue+Koa框架、数据库为MYSQL为例。


    案例


    前端: 首先在把图片传给后端之前,前端对图片进行格式转换,转换成功后就可以照常调用后端给的接口,传进去就行。



    我这里就是点了提交按钮后,触发编写的点击事件函数,在函数里先对图片转base64,转成功后再调用后端给的接口,把此数据以及其他数据传进去就行。



    在前端编写转base64的函数(很重要)


    export function uploadImgToBase64 (file) {
    return new Promise((resolve, reject) => {
    const reader = new FileReader()//html5提供的一种异步文件读取机制
    reader.readAsDataURL(file)//将文件读取为Base64编码的数据URL
    reader.onload = function () { // 图片转base64完成后返回reader对象
    resolve(reader)
    }
    reader.onerror = reject
    })
    }

    前端要为转为base64的图片的相应操作


    const state = reactive({
    picture: [] //这个是用来装表单里选中的照片
    })


    const imgBroadcastListBase64 = [] //用来存放转base64后的照片,用数组是因为上传的图片可能不止一张
    console.log('图片转base64开始...')
    // 遍历每张图片picture 异步
    const filePromises = state.picture.map(async file => {
    //console.log(file);
    const response = await uploadImgToBase64(file.file) //调用函数 将图片转为base64
    //console.log(response,111);
    return response.result.replace(/.*;base64,/, '') // 去掉data:image/jpeg;base64,
    })
    // 按次序输出 base64图片
    for (const textPromise of filePromises) {
    imgBroadcastListBase64.push(await textPromise)
    }
    console.log('图片转base64结束..., ', imgBroadcastListBase64)


    //判断imgBroadcastListBase64是否<=1,是的话就是上传一张图片,否则上传的是多张图片
    if(imgBroadcastListBase64.length<=1){
    state.imgsStr = imgBroadcastListBase64.join()//转字符串
    }else{
    state.imgsStr = imgBroadcastListBase64.join(',')//转字符串并且每个值用','拼接,这样是为了方便后面从数据库拿到数据,将图片又转为之前的base64格式
    }
    //调用后端提供的接口,传数据到数据库里(这个只是自己编写的后端接口,主要是为了展示传数据)
    const res = await secondGoodsAdd({
    create_time: ti,
    content_goods: state.content,
    color: state.title2,
    price: state.title3,
    tel: state.title4,
    img: state.imgsStr,//转base64后的图片
    concat_num: 0,
    like_num: 0,
    name_goods: state.title1
    })
    if (res.code === '80000') {
    showSuccessToast('发布成功!')
    }
    router.push('/cicle')


    存到数据库中的图片路径是转为base64后的且删除前面data:image/jpeg;base64的字符串。



    这样数据就存到数据库中啦!存进去的是字符串。


    那么问题来了,数据是存进去了,但是我又想拿到这个数据到前端显示出来,好,那我就先直接拿到前端用,结果发现报错了!说是请求头太长?想办法解决下!后面我的解决办法是拿到这个转为了base64且去掉前面data...字段的的图片数据再转为正常的base64的格式。好,来转换吧!


    //我这里是在后端编写的接口,用于展示被添加到数据库中的所有数据
    router.get('/cirleLifeLook',async(ctx,next)=>{
    try {
    const result=await userService.cirleLifeLook()
    for(let i=0;i<result.length;i++){
    var imgData=result[i].img //获取每条数据中的照片字段
    if(imgData){
    if(imgData.indexOf(',')>-1){//存在','的话代表是多张图片的混合的字符串
    let ans=imgData.split(',') //切割 获得多张之前切掉data...后的base64字符串
    let s=[]
    for(let j=0;j<ans.length;j++){
    s.push("data:image/png;base64,"+ans[j])//还原每张图片初始的base64数据
    }
    result[i].img=s
    }else{
    result[i].img="data:image/png;base64,"+imgData //就一张图片直接在前面拼接"data:image/png;base64,"
    }
    }
    }//到此为止,在给前端传数据前就修改了其中每条数据里的照片地址,这样就可以正常显示啦
    if(result.length){
    ctx.body={
    code:'80000',
    data:result,
    msg:'获取成功'
    }
    }else{
    ctx.body={
    code:'80005',
    data:'null',
    msg:'还没有信息'
    }
    }
    } catch (error) {
    ctx.body={
    code:'80002',
    data:error,
    msg:'服务器异常'
    }
    }
    })

    OK,到此就结束啦~现在是不是觉得把图片转base64还是挺简单的?还挺有用的?快去实践下吧。记住此方法只适合小图片类型的,大点的文件可能会崩掉哈!


    结束语


    本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,您的点赞是持续写作的动力,感谢支持。要是您觉得有更好的方法,欢迎评论,提出建议!


    作者:嗯嗯呢
    来源:juejin.cn/post/7255785481119727672
    收起阅读 »

    将一个图片地址转成文件流(File)再上传

    web
    写在开头 最近,小编在业务中遇到一个图片转存的场景。 领导🤠:大概过程就是,接口会给我返回一个图片列表数据,图片路径是全路径,但是路径中的域名是其他系统的,必须要在用户选择图片的时候将图片重新转存到自个的系统上,防止其他系统删除图片对此有影响。 我😃:Em....
    继续阅读 »

    写在开头


    最近,小编在业务中遇到一个图片转存的场景。


    领导🤠:大概过程就是,接口会给我返回一个图片列表数据,图片路径是全路径,但是路径中的域名是其他系统的,必须要在用户选择图片的时候将图片重新转存到自个的系统上,防止其他系统删除图片对此有影响。


    14DBA96E.gif


    我😃:Em...很合理的需求。


    (但是,和有什么关系?我只是一个前端小菜鸡呀,不祥的预感.......)


    我😃:(卑微提问)这个过程不是放后端做比较合理一点?


    后端大哥😡:前端不能做?


    我😣:可以可以,只是...这个好像会跨域?


    后端大哥😠:已经配置了请求头('Access-Control-Allow-Origin': '*')。


    我😖:哦,好的,我去弄一下。(*******此处省略几万字心理活动内容)


    14F03F73.jpg

    第一种(推荐)


    那么,迫于......不,我自愿的,我们来看看前端要如何完成这个转成过程,代码比较简单,直接贴上来瞧瞧:


    async function imageToStorage(path) {
    // 获取文件名
    const startIndex = path.lastIndexOf('/');
    const endIndex = path.indexOf('?');
    const imgName = path.substring(startIndex + 1, endIndex);
    // 获取图片的文件流对象
    const file = await getImgToFile(path, imgName);
    // TODO: 将File对象上传到其他接口中
    }

    /**
    * @name 通过fetch请求文件,将文件转成文件流对象
    * @param { string } path 文件路径全路径
    * @param { string } fileName 文件名
    * @returns { File | undefined }
    */

    function getImgToFile(path, fileName) {
    const response = await fetch(path);
    if (response) {
    const blob = await response.blob();
    const file = new File([blob], fileName, { type: blob.type });
    return file;
    }
    }

    上述方式,在后端配置了允许跨域后,正常是没有什么问题的,也是比较好的一种方式了。😃


    但是,在小编实际第一次编码测试后,却还是遇上了跨域。😓


    image.png


    一猜应该就是后端实际还没配置好,问了一下。


    后端大哥😑:还没部署,一会再自己试试。


    我😤:嗯嗯。


    第二种


    等待的过程,小编又在网上找了找了,找到了第二种方式,各位看官可以瞧瞧:


    /** @name 将图片的网络链接转成base64 **/
    function imageUrlToBase64(imageUrl: string, fileName: string): Promise<File> {
    return new Promise(resolve => {
    const image = new Image();
    // 让Image元素启用cors来处理跨源请求
    image.setAttribute('crossOrigin', 'anonymous');
    image.src = imageUrl + '&v=' + Math.random();
    image.onload = () => {
    const canvas = document.createElement('canvas');
    canvas.width = image.width;
    canvas.height = image.height;
    const context = canvas.getContext('2d')!;
    context.drawImage(image, 0, 0, image.width, image.height);
    // canvas.toDataURL
    const imageBase64 = canvas.toDataURL('image/jpeg', 1); // 第二个参数是压缩质量
    // 将图片的base64转成文件流
    const file = base64ToFile(imageBase64, fileName);
    resolve(file);
    };
    });
    }
    /** @name 将图片的base64转成文件流 **/
    function base64ToFile(base64: string, fileName: string) {
    const baseArray = base64.split(',');
    // 获取类型与后缀名
    const mime = baseArray[0].match(/:(.*?);/)![1];
    const suffix = mime.split('/')[1];
    // 转换数据
    const bstr = atob(baseArray[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
    }
    // 生成文件流
    const file = new File([u8arr], `${fileName}.${suffix}`, {
    type: mime,
    });
    return file;
    }

    这第二种方式由于要先把图片绘制到 canvas 再去转成 base64 再去转成文件流,小编用 console.time 稍微测了一下,每次转化过程都要几百毫秒,图片越大时间越长,挺影响性能的。


    所以,小编还是推荐使用第一种方式,当然,最稳妥的方案是后端去搞最好了。😉



    网上很多都说第二种方式可以直接绕过跨域,各种谈论。😪


    主要就是这个 crossOrigin 属性。MDN解释


    它原理是通过了 CORS


    image.png


    或者可以再看看这个解释:传送门










    至此,本篇文章就写完啦,撒花撒花。


    image.png


    希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

    老样子,点赞+评论=你会了,收藏=你精通了。


    作者:橙某人
    来源:juejin.cn/post/7336756027385872424
    收起阅读 »

    NestJS 依赖注入DI与控制反转IOC

    web
    1. 前言 在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Cont...
    继续阅读 »

    1. 前言


    在《NestJS 快速入门》和《NestJS HTTP 请求与响应 》两篇文章中,主要使用了 NestJS 控制器处理简单的逻辑,对于复杂的业务逻辑,通常使用单独的 Service 服务进行处理,然后可以把 Service 作为依赖注入到 Controller 中。


    2. 概念


    2.1 依赖注入、控制反转、容器


    何为容器?容器是可以装一些资源的。举个例子,普通的容器,生活中的容器比如保温杯,只能用来存储东西,没有更多的功能。而程序中的容器则包括数组、集合Set、Map 等。


    而复杂的容器,生活中比如政府,政府管理我们的一生,生老病死和政府息息相关。


    AB7410FF-F52F-4C58-9171-DFB6303157DD.png


    程序中复杂的容器比如NestJS 容器,它能够管理 Controller、Service 等组件,负责创建组件的对象、存储 组件的对象,还要负责 调用 组件的方法让其工作,并在一定的情况下 销毁 组件。


    依赖注入(Dependency Injection)是实现控制反转的一种方式。控制反转又是什么呢?控制反转(Inversion of Control)是指从容器中获取资源的方式跟以往有所不同。


    2.2 为什么需要控制反转


    2.2.1 依赖关系复杂、依赖顺序约束


    后端系统中有多个对象:



    • Controller 对象: 处理 HTTP 请求,调用 Service,返回响应。

    • Service 对象: 实现业务逻辑。

    • Repository 对象: 实现对数据库的增删改查。


    此外,还包括数据库链接对象 DataSource、配置对象 Config 等等。这些对象之间存在复杂的关系:



    • Controller 依赖 Service 实现业务逻辑。

    • Service 依赖 Repository 进行数据库操作。

    • Repository 依赖 DataSource 建立连接,而 DataSource 则需要从 Config 对象获取用户名密码等信息。


    这导致对象的创建变得复杂,需要理清它们之间的依赖关系,确保正确的创建顺序。例如:


    const config = new Config({ username: 'xxx', password: 'xxx'});
    const dataSource = new DataSource(config);
    const repository = new Repository(dataSource);
    const service = new Service(repository);
    const controller = new Controller(service);

    这些对象需要一系列初始化步骤后才能使用。此外,像 config、dataSource、repository、service、controller 这些对象不需要每次都新建一个,可以保持单例。在应用初始化时,需要明确依赖关系,创建对象组合,并确保单例模式,这是后端系统常见的挑战。


    2.3.2 高层逻辑直接依赖低层逻辑,违反依赖倒置规范



    依赖倒置: 什么是依赖倒置原则(Dependency Inversion Principle)高层模块不应该依赖底层模块,二者都应该依赖抽象(例如接口)。  抽象不应该依赖细节,细节(具体实现)应该依赖抽象。 



    1.举一个工厂例子,初始化时有工人、车间、工厂。


    2FE7E55F-42A4-48FF-9A6B-8E987E386A7F.png


    1.工厂是容器,车间是消费者,依赖工人和工人的服务,工人是依赖,是生产者。


    // 工人
    class Worker {
      manualProduceScrew(){
        console.log('A screw is built')
      }
    }

    // 螺丝生产车间
    class ScrewWorkshop {
      private worker: Worker = new Worker()
     
      produce(){
        this.worker.manualProduceScrew() // 调用工人的方法
      }
    }

    // 工厂
    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    2.现在要把工人制造改为机器制造,需要直接在车间把工人制造改为机器制造,麻烦。


    // 机器
    class Machine {
      autoProduceScrew(){
        console.log('A screw is built')
      }
    }

    class ScrewWorkshop {
      // 改为一个机器实例
      private machine: Machine = new Machine()
     
      produce(){
        this.machine.autoProduceScrew() // 调用机器的方法
      }
    }

    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()


    3.此时考虑依赖倒置原则,通过实现生产者接口来处理(PS:这也是为什么像 Java 语言中,实现业务服务时需要定义接口类和实现类,遵循依赖倒置,方便切换不同的业务逻辑)


    // 定义一个生产者接口
    interface Producer {
      produceScrew: () => void
    }

    // 实现了接口的机器
    class Machine implements Producer {
      autoProduceScrew(){
        console.log('A screw is built')
      }
     
      produceScrew(){
        this.autoProduceScrew()
      }
    }

    // 实现了接口的工人
    class Worker implements Producer {
      manualProduceScrew(){
        console.log('A screw is built')
      }
     
      produceScrew(){
        this.manualProduceScrew()
      }
    }

    class ScrewWorkshop {
      // 依赖生产者接口,可以随意切换啦!!!
      // private producer: Producer = new Machine()
      private producer: Producer = new Worker()
     
      produce(){
        this.producer.produceScrew() // 工人和机器都提供了相同的接口
      }
    }

    class Factory {
      start(){
        const screwWorkshop = new ScrewWorkshop()
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    4.工厂改造后,螺丝生产车间的改造变得更容易了,只需要改变其属性中所新建的遵循Producer接口的实例即可。然而,这并没有完全改善我们与车间主任之间的关系,每次厂里在改造生产机器时我们还是需要麻烦车间主任。这是因为我们还是没有完全遵守依赖倒置原则,ScrewWorkshop 仍然依赖了 Worker/Machine的实例,只不过这种依赖相较之前少了一点罢了。


    要完全遵守依赖倒置原则,需要使用控制反转依赖注入


    2.3 控制反转思想


    2.3.1 获取资源的传统方式



    • 自己做饭:买菜、洗菜、择菜、切菜、炒菜,全过程参与,费时费力,必须了解资源创建整个过程中的全部细节并熟练掌握。

    • 在应用程序中,组件需要获取资源,传统的方式是主动从容器中获取所需资源,这样开发人员需要知道具体容器中特定资源的获取方式,增加了学习成本,也降低了开发效率。


    2.3.2 获取资源的控制反转方式



    • 点外卖:下单、等待、吃外卖,省时省力,不必关心资源创建过程的全部细节。

    • 控制反转的思想改变了应用程序组件获取资源的方式,容器会主动将资源推送给需要的组件,开发人员只需要提供接收资源的方式即可,这样可以降低学习成本,提高开发效率。这种方式被称为查找的被动方式。


    2.4 如何实现控制反转


    起源:许多应用程序的业务逻辑实现需要两个或多个类之间的协作,这种协作使得每个对象都需要获取与其合作的对象(即其所依赖的对象的引用)。如果这种获取过程由对象自身实现,那么将导致代码高度耦合,难以维护和调试。


    技术描述


    在 Class A 中,我们使用了 Class B 的对象 b。通常情况下,我们需要在 A 的代码中显式地使用 new 来创建 B 的对象。但是,如果采用依赖注入技术,A 的代码只需要定义一个 private 的 B 对象,而不需要直接 new 来获取这个对象。相反,我们可以通过相关的容器控制程序来在外部创建 B 对象,并将其注入到 A 类中的引用中。具体获取的方法以及对象被获取时的状态由配置文件(如 XML)来指定。这种方法可以使代码更加清晰和正式。


    loc 也可以理解为把流程的控制从应用程序转移到框架之中。以前,应用程序掌握整个处理流程;现在,控制权转移到了框架,框架利用一个引擎驱动整个流程的执行,框架会以相应的形式提供一系列的扩展点,应用程序则通过定义扩展的方式实现对流程某个环节的定制。“框架Call 应用”。基于 MVC 的 web 应用程序就是如此。


    实现方法


    实现控制反转主要有两种方式:依赖注入和依赖查找。两者的区别在于,前者是被动的接收对象,在类 A 的实例创建过程中即创建了依赖的 B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的事件也可以在代码中自由控制。


    细说


    1.依赖注入:



    • 基于接口。实现特定接口以供外部容器注入所依赖类型的对象

    • 基于set方法。实现特定属性的publicSet方法,来让外部容器调用传入所依赖类型的对象

    • 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象

    • 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。


    2.依赖查找


    依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态。


    2.4.1 工厂例子依赖注入改造


    通过以上学习,现在把工厂例子代码进一步改造,将底层类的依赖,由从中间类直接引用,变为高层类在构造时的依赖注入


    // ......Worker/Machine及其所遵循的接口Producer的实现与此前一致,此处省略

    class ScrewWorkshop
      private producer: Producer
     
      // 通过构造函数注入
      constructor(producer: Producer){
        this.producer = producer
      }
     
      produce(){
        this.producer.produceScrew()
      }
    }

    class Factory {
      start(){
        // 在Factory类中控制producer的实现,控制反转啦!!!
        // const producer: Producer = new Worker()
        const producer: Producer = new Machine()
        // 通过构造函数注入
        const screwWorkshop = new ScrewWorkshop(producer)
        screwWorkshop.produce()
      }
    }

    const factory = new Factory()
    // 工厂开工啦!!!
    factory.start()



    至此,回顾对这个车间的改造三步



    1. 依赖倒置: 解除ScrewWorkshop与Worker/Machine具体类之间的依赖关系,转为全部依赖Producer接口;

    2. 控制反转: 在Factory类中实例化ScrewWorkshop中需要使用的producer,ScrewWorkshop的对依赖项Worker/Machine的控制被反转了;

    3. 依赖注入: ScrewWorkshop中不关注具体producer实例的创建,而是通过构造函数constructor注入;


    3. NestJS 依赖注入


    在Nest的设计中遵守了控制反转的思想,使用依赖注入(包括构造函数注入、参数注入、Setter方法注入)解藕了Controller 与 Provider之间的依赖。


    我们将Nest中的元素与我们自己编写的工厂进行一个类比:



    1. Provider & Worker/Machine:真正提供具体功能实现的低层类。

    2. Controller & ScrewWorkshop:调用低层类来为用户提供服务的高层类。

    3. Nest框架本身 & Factory:控制反转容器,对高层类和低层类统一管理,控制相关类的新建与注入,解藕了类之间的依赖。


    IOC 机制是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。


    Nest 实现了 IOC 容器,会从入口模块开始扫描,分析 Module 之间的引用关系,对象之间的依赖关系,自动把 provider 注入到目标对象。


    Nest 里通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。并且Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。


    provider 一般都是用 @Injectable 修饰的 class:


    2864AB0B-6A5F-4C29-AE1C-87AD610BA635.png


    在 Module 的 providers 里声明:


    27933B1A-306E-43E2-8203-598E1998B6B0.png


    上面是一种简写,完整的写法是这样的


    4CF33A34-F899-4AA0-9DA0-B93D177C9760.png


    构造函数或者属性注入


    E9561C4C-D02B-47DD-8B66-D0076666E2A8.png


    异步的注入对象


    24974844-A158-4FC1-8A14-33A1AF8033FB.png


    通常情况下,提供者通过使用 @Injectable 声明,然后在 @Moduleproviders 数组中注册类来实现。默认的 token 是类本身,因此不需要使用 @Inject 来指定注入的 token。


    但是,也可以使用字符串类型的 token,但在注入时需要单独指定 @Inject。除了可以使用 useClass 指定注入的类,还可以使用 useValue 直接指定注入的对象。如果想要动态生成对象,则可以使用 useFactory,它的参数也注入到 IOC 容器中的对象中,然后动态返回提供者的对象。如果想要为已有的 token 指定一个新的 token,可以使用 useExisting 来创建别名。通过灵活运用这些提供者类型,可以在 Nest 的 IOC 容器中注入任何对象。


    4.实践


    之前部门逻辑都是放在 controller 中的,现在可以把逻辑放 dept.service.ts  上来,添加 @Injectable 装饰器。


    9102DFFB-B32C-4ECE-8A5E-4D34135A6391.png


    在 DeptModule  模块中的 propviders 中引入 DeptService


    1397E621-39A4-4EF7-829A-8500C7B7B0B6.png


    最后在 dep.controller  使用部门服务,通过 @Inject() 装饰器注入。


    C63CF5D6-F08C-440C-9255-5688F35ADA41.png


    小结


    本文我们学习了什么是容器、依赖注入与控制反转,掌握了 Nest 依赖注入使用,并通过部门服务例子进行演示。


    参考资料



    作者:jecyu
    来源:juejin.cn/post/7336055070508843048
    收起阅读 »

    前端最全的5种换肤方案总结

    web
    最近一年很多客户要求更换主题色,而团队的项目基础框架不一,因此几乎使用了所有的更换主题的方案。以下总结了各方案的实现以及优缺点,希望能帮助有需要更换主题色的伙伴少踩坑。 方案一:硬编码 对一些老的前端基础库和业务库样式没有提取公共样式,当有更换主题的需求,实现...
    继续阅读 »

    最近一年很多客户要求更换主题色,而团队的项目基础框架不一,因此几乎使用了所有的更换主题的方案。以下总结了各方案的实现以及优缺点,希望能帮助有需要更换主题色的伙伴少踩坑。


    方案一:硬编码


    对一些老的前端基础库和业务库样式没有提取公共样式,当有更换主题的需求,实现的方法只能是全局样式替换,工作量比较大,需要更改form表单、按钮、表格、tab、容器等所有组件的各种状态,此外还需更换icon图标。


    以下是我们的一个老项目实现主题色更换,全局样式替换接近500行,如下图所示:


    image.png


    image.png


    image.png


    总结: 对于这种老项目只能通过硬编码的方式去更改,工作量较大,好在老项目依赖同一个基础库和业务库,所以在一个项目上实现了也可以快速推广到其它项目。


    方案二:sass变量配置


    团队的基础组件库Link-ui是基于Eelement-ui二次开发,因此可以采取类似于Element-ui的方式进行主题更改,只需要设计师提供6个主题色即可完成主题色的更改,如下所示。


    image.png



    • 配置基础色
      基础色一般需要设计师提供,也可以通过配置化的方式实现,


    $--color-primary-bold: #1846D1 !default;
    $--color-primary: #2664FD !default;
    $--color-primary-light: #4D85FD !default;
    $--color-primary-light-1: #9AC0FE !default;
    $--color-primary-light-2: #C1DBFF !default;
    $--color-primary-lighter: #E8F2FF !default;



    • 从基础库安装包引入基础色和库的样式源文件
      image.png


    @import "./common/base_var.scss";

    /* 改变 icon 字体路径变量,必需 */
    $--font-path: '~link-ui-web/lib/theme-chalk/fonts';

    @import "~link-ui-web/packages/theme-chalk/src/index";


    • 全局引入


    import '@/styles/link-variables.scss';


    • 更换主题色
      只需要更改上面的6个变量即可实现主题色的更改,比如想改成红色,代码如下


    $--color-primary-bold: #D11824 !default;
    $--color-primary: #FD268E !default;
    $--color-primary-light: #D44DFD !default;
    // $--color-primary-light-1: #9AC0FE !default;
    $--color-primary-light-2: #DCC1FF !default;
    $--color-primary-lighter: #F1E8FF !default;

    image.png


    总结: 对于基础库和样式架构设计合理的项目更改主题色非常的简单,只要在配置文件更换变量的值即可。它的缺点是sass变量的更改每次都需要编译,很难实现配置化。


    方案三、css变量+sass变量+data-theme


    代码结构如下:


    image.png



    • 设计三套主题分别定义不同的变量(包含颜色、图标和图片)


      // theme-default.scss
    /* 默认主题色-合作蓝色 */
    [data-theme=default] {
    --color-primary: #516BD9;
    --color-primary-bold: #3347B6;

    --color-primary-light: #6C85E1;
    --color-primary-light-1: #C7D6F7;
    --color-primary-light-2: #c2d6ff;
    --color-primary-lighter: #EFF4FE;

    --main-background: linear-gradient(90deg,#4e68d7, #768ff3);

    --user-info-content-background-image: url('../../assets/main/top-user-info-bg.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg.png');
    ...
    }


      // theme-orange.scss
    // 阳光黄
    [data-theme=orange] {
    --color-primary: #FF7335;
    --color-primary-bold: #fe9d2e;

    --color-primary-light: #FECB5D;
    --color-primary-light-1: #FFDE8B;
    --color-primary-light-2: #fcdaba;
    --color-primary-lighter: #FFF3E8;

    --main-background: linear-gradient(90deg,#ff7335 2%, #ffa148 100%);


    --user-info-content-background-image: url('../../assets/main/top-user-info-bg-1.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg-1.png');
    ...
    }


      // theme-red.scss
    /* 财富红 */
    [data-theme=red] {
    --color-primary: #DF291E;
    --color-primary-bold: #F84323;

    --color-primary-light: #FB8E71;
    --color-primary-light-1: #FCB198;
    --color-primary-light-2: #ffd1d1;
    --color-primary-lighter: #FFEEEE;


    --main-background: linear-gradient(90deg,#df291e 2%, #ff614c 100%);

    --user-info-content-background-image: url('../../assets/main/top-user-info-bg-2.png');
    --msg-tip-content-background-image: url('../../assets/main/top-user-info-bg-2.png');
    ...
    }



    • 把主题色的变量作为基础库的变量


    $--color-primary-bold: var(--color-primary-bold) !default;
    $--color-primary: var(--color-primary) !default;
    $--color-primary-light: var(--color-primary-light) !default;
    $--color-primary-light-1: var(--color-primary-light-1) !default;
    $--color-primary-light-2: var(--color-primary-light-2) !default;
    $--color-primary-lighter: var(--color-primary-lighter) !default;


    • App.vue指定默认主题色


    window.document.documentElement.setAttribute('data-theme', 'default')

    data-theme会注入到全局的变量上,所以我们可以在任何地方获取定义的css变量


    image.png


    实现效果如下:


    image.png


    image.png


    image.png


    总结: 该方案是最完美的方案,但是需对颜色、背景图、icon等做配置,需设计师设计多套方案,工作量相对较大,适合要求较高的项目或者标准产品上面,目前我们的标准产品选择的是该方案。


    方案四:滤镜filter


    filter CSS属性将模糊或颜色偏移等图形效果应用于元素。滤镜通常用于调整图像,背景和边框的渲染。


    它有个属性hue-rotate() 用于改变图整体色调,设定图像会被调整的色环角度值。值为0deg展示原图,大于360deg相当于又绕一圈。
    用法如下:


    body {
    filter: hue-rotate(45deg);
    }


    产品新建UI单元测试运行录制.gif


    总结: 成本几乎为0,实现简单。缺点是对于某些图片或者不想改的颜色需要特殊处理。


    方案五:特殊时期变灰



    • filter还有个属性 grayscale() 改变图像灰度,值在 0% 到 100% 之间,值为0%展示原图,值为100% 则完全转为灰度图像。


    body {
    filter: grayscale(1);
    }

    image.png


    总结: 成本小,可以将该功能做成配置项,比如配置它的生效开始时间和生效结束时间,便于运营维护也不用频繁发布代码。


    总结


    以上就是实现换肤的全部方案,我们团队在实际项目都有使用,比较好推荐的方案是方案一、方案三、方案五,对于要求不高的切换主题推荐方案四,它的技术零成本,对于标准产品推荐方案三。如有更好的方案欢迎评论区交流。


    作者:_无名_
    来源:juejin.cn/post/7329573754987462693
    收起阅读 »

    vue3的宏到底是什么东西?

    web
    前言 从vue3开始vue引入了宏,比如defineProps、defineEmits等。我们每天写vue代码时都会使用到这些宏,但是你有没有思考过vue中的宏到底是什么?为什么这些宏不需要手动从vue中import?为什么只能在setup顶层中使用这些宏? ...
    继续阅读 »

    前言


    vue3开始vue引入了宏,比如definePropsdefineEmits等。我们每天写vue代码时都会使用到这些宏,但是你有没有思考过vue中的宏到底是什么?为什么这些宏不需要手动从vueimport?为什么只能在setup顶层中使用这些宏?


    vue 文件如何渲染到浏览器上


    要回答上面的问题,我们先来了解一下从一个vue文件到渲染到浏览器这一过程经历了什么?


    我们的vue代码一般都是写在后缀名为vue的文件上,显然浏览器是不认识vue文件的,浏览器只认识html、css、jss等文件。所以第一步就是通过webpack或者vite将一个vue文件编译为一个包含render函数的js文件。然后执行render函数生成虚拟DOM,再调用浏览器的DOM API根据虚拟DOM生成真实DOM挂载到浏览器上。


    progress.png


    vue3的宏是什么?


    我们先来看看vue官方的解释:



    宏是一种特殊的代码,由编译器处理并转换为其他东西。它们实际上是一种更巧妙的字符串替换形式。



    宏是在哪个阶段运行?


    通过前面我们知道了vue 文件渲染到浏览器上主要经历了两个阶段。


    第一阶段是编译时,也就是从一个vue文件经过webpack或者vite编译变成包含render函数的js文件。此时的运行环境是nodejs环境,所以这个阶段可以调用nodejs相关的api,但是没有在浏览器环境内执行,所以不能调用浏览器的API


    第二阶段是运行时,此时浏览器会执行js文件中的render函数,然后依次生成虚拟DOM和真实DOM。此时的运行环境是浏览器环境内,所以可以调用浏览器的API,但是在这一阶段中是不能调用nodejs相关的api


    而宏就是作用于编译时,也就是从vue文件编译为js文件这一过程。


    举个defineProps的例子:在编译时defineProps宏就会被转换为定义props相关的代码,当在浏览器运行时自然也就没有了defineProps宏相关的代码了。所以才说宏是在编译时执行的代码,而不是运行时执行的代码。


    一个defineProps宏的例子


    我们来看一个实际的例子,下面这个是我们的源代码:


    <template>
    <div>content is {{ content }}div>
    <div>title is {{ title }}div>
    template>

    <script setup lang="ts">
    import {ref} from "vue"
    const props = defineProps({
    content: String,
    });
    const title = ref("title")
    script>

    在这个例子中我们使用defineProps宏定义了一个类型为String,属性名为contentprops,并且在template中渲染content的内容。


    我们接下来再看看编译成js文件后的代码,代码我已经进行过简化:


    import { defineComponent as _defineComponent } from "vue";
    import { ref } from "vue";

    const __sfc__ = _defineComponent({
    props: {
    content: String,
    },
    setup(__props) {
    const props = __props;
    const title = ref("title");
    const __returned__ = { props, title };
    return __returned__;
    },
    });

    import {
    toDisplayString as _toDisplayString,
    createElementVNode as _createElementVNode,
    Fragment as _Fragment,
    openBlock as _openBlock,
    createElementBlock as _createElementBlock,
    } from "vue";

    function render(_ctx, _cache, $props, $setup) {
    return (
    _openBlock(),
    _createElementBlock(
    _Fragment,
    null,
    [
    _createElementVNode(
    "div",
    null,
    "content is " + _toDisplayString($props.content),
    1 /* TEXT */
    ),
    _createElementVNode(
    "div",
    null,
    "title is " + _toDisplayString($setup.title),
    1 /* TEXT */
    ),
    ],
    64 /* STABLE_FRAGMENT */
    )
    );
    }
    __sfc__.render = render;
    export default __sfc__;

    我们可以看到编译后的js文件主要由两部分组成,第一部分为执行defineComponent函数生成一个 __sfc__ 对象,第二部分为一个render函数。render函数不是我们这篇文章要讲的,我们主要来看看这个__sfc__对象。


    看到defineComponent是不是觉得很眼熟,没错这个就是vue提供的API中的 definecomponent函数。这个函数在运行时没有任何操作,仅用于提供类型推导。这个函数接收的第一个参数就是组件选项对象,返回值就是该组件本身。所以这个__sfc__对象就是我们的vue文件中的script代码经过编译后生成的对象,后面再通过__sfc__.render = renderrender函数赋值到组件对象的render方法上面。


    我们这里的组件选项对象经过编译后只有两个了,分别是props属性和setup方法。明显可以发现我们原本在setup里面使用的defineProps宏相关的代码不在了,并且多了一个props属性。没错这个props属性就是我们的defineProps宏生成的。


    convert.png


    我们再来看一个不在setup顶层调用defineProps的例子:


    <script setup lang="ts">
    import {ref} from "vue"
    const title = ref("title")

    if (title.value) {
    const props = defineProps({
    content: String,
    });
    }
    script>

    运行这个例子会报错:defineProps is not defined


    我们来看看编译后的js代码:


    import { defineComponent as _defineComponent } from "vue";
    import { ref } from "vue";

    const __sfc__ = _defineComponent({
    setup(__props) {
    const title = ref("title");
    if (title.value) {
    const props = defineProps({
    content: String,
    });
    }
    const __returned__ = { title };
    return __returned__;
    },
    });

    明显可以看到由于我们没有在setup的顶层调用defineProps宏,在编译时就不会将defineProps宏替换为定义props相关的代码,而是原封不动的输出回来。在运行时执行到这行代码后,由于我们没有任何地方定义了defineProps函数,所以就会报错defineProps is not defined


    总结


    现在我们能够回答前面提的三个问题了。



    • vue中的宏到底是什么?


      vue3的宏是一种特殊的代码,在编译时会将这些特殊的代码转换为浏览器能够直接运行的指定代码,根据宏的功能不同,转换后的代码也不同。


    • 为什么这些宏不需要手动从vueimport


      因为在编译时已经将这些宏替换为指定的浏览器能够直接运行的代码,在运行时已经不存在这些宏相关的代码,自然不需要从vueimport


    • 为什么只能在setup顶层中使用这些宏?


      因为在编译时只会去处理setup顶层的宏,其他地方的宏会原封不动的输出回来。在运行时由于我们没有在任何地方定义这些宏,当代码执行到宏的时候当然就会报错。



    如果想要在vue中使用更多的宏,可以使用 vue macros。这个库是用于在vue中探索更多的宏和语法糖,作者是vue的团队成员 三咲智子


    作者:欧阳码农
    来源:juejin.cn/post/7335721246931189795
    收起阅读 »

    【如诗般写代码】你甚至连注释都没玩明白

    web
    引言要问我认为最难的事是什么,那只有维护前人的代码。我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行,一头扎进去,闷头写到天昏地暗的什么修复bug,提升性能之类的都不是事,会用调试工具和分时渲染就够了但是看屎山是真的难受注释篇利用注释在编辑器...
    继续阅读 »

    引言

    要问我认为最难的事是什么,那只有维护前人的代码。
    我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行,
    一头扎进去,闷头写到天昏地暗的

    什么修复bug,提升性能之类的都不是事,会用调试工具和分时渲染就够了
    但是看屎山是真的难受


    注释篇

    1. 利用注释在编辑器开启代码提示

      image.png

      看到区别了吗,左边用的是文档注释,鼠标悬浮能看到变量描述
      右边用的是行内注释,没有任何作用

      初步认识了文档注释的好处后,很多人可能还是不用,因为嫌麻烦。
      所以编辑器也为你着想了,以 VSCode为例,输入 /**,就会自动生成文档注释
      如果在函数上面,再按下回车,还能补齐函数参数文档,如下图所示

      comment.gif


    1. 利用文档注释描述函数功能

      当我鼠标悬浮在函数上时,就能看到他的各种描述,从其他文件导入也有效
      image.png


    1. 智能提示当前参数描述,以及类型等

      这里也能用快捷键呼出,详见我上篇文章:# 【最高效编码指南】也许你不会用VSCode | IDEA

      image.png


    1. 添加 JS 类型,实现类似 TS 的效果

      这里我呼出代码提示,但是他并没有给我补全任何方法,因为他不知道你的类型是什么

      image.png

      如果是强类型语言的话,那就会给你补全代码
      那么动态类型如何实现呢?以 JS 为例,使用文档注释即可,也就是前阵子沸沸扬扬的利用 JSDoc 代替 TS

      image.png

      不仅于此,连枚举都能实现,反正 TS 有的,他应该都有,我没有详细研究

      image.png

    2. 文档注释指令

      如下图所示,我想在文档注释里写上用法,但是他的格式十分丑陋,而且没有语法高亮

      image.png

      于是我使用 example 指令,告诉他这是一个示例,这时就有语法高亮了

      image.png

      指令还有很多,你们输入 @ 就会有提示了,比如 deprecated,标记已弃用
      这时你使用它就会有个提示,并且划上一根线

      image.png

    3. MarkDown 文档注释

      有时候,指令可能不够用,这时就可以使用 MarkDown 语法了

      image.png

    4. 结合 TS

      定义类型时,写上文档注释,当你鼠标悬浮时,就能查看对应注释

      image.png

      函数重载情况下,文档注释要写在类型上才行,下面这种无效

      image.png

      要写在类型定义的地方才行

      image.png

    5. 总结

      如果你用的是变量、函数或是 TS 定义类型,你要写注释,那就一定要写 文档注释,我跪下来求求你了 😭

    减少条件分支语句

    1. 策略模式,写个映射表即可。这个有一点开发经验的应该都知道吧

      如果遇到复杂情况,映射表里也可以写函数,执行后返回逻辑

      image.png

    2. 提前返回

      这里第 2 种提前返回就减少了一层嵌套,实际开发中,能减少更多嵌套语句

      image.png

    3. 多个相等判断,使用数组代替

      image.png

    代码七宗罪

    让我来细数一下这坨代码的罪行,然后引出另一个主题,美化代码
    下面这段,这简直是"甲级战犯",

    1. 一堆变量写了或者导入了不用,放那恶心谁呢
    2. 注释了的代码不删 (虽然可能有用,但是真丑)
    3. 都什么年代了,还在用var (坏处下面说)
    4. 用行内注释和没写区别不大,要写就写文档注释 (文档注释的优点上面解释了,不再赘述)
    5. 小学生流水账一般的代码,连个函数入口都没提供,想一句写一句
    6. 连个代码格式化都不会,多按几个回车,你的键盘不会烂掉;每个分段加个注释,你的速度慢不了多少
    7. 硬编码,所有类型用字符串直接区分,你万一要改怎么办?

    image.png

    语义化

    我经常能看见一个文件里,写上百行,没有几个函数,没有注释,没有换行
    一头扎进去,闷头写到天昏地暗的,比如下面这种

    image.png

    这玩意要我一行一行看?我是真的被恶心坏了
    写代码要突出一个重点,看个大概,然后才能快速排查,看第三方库源码也是如此

    我的习惯是写一个主入口,你叫 main | init | start 什么的都行,我只希望你能写上
    然后主入口按照逻辑,给每个函数命名,这样一眼就能看出来你在干什么
    如下图所示,这是我的偏好

    image.png

    我喜欢利用函数全局提升,把初始化函数放在文件顶部。这样每次打开一个文件,就能立刻看到大概逻辑
    所以我很少用匿名函数,像上面那种全部写一坨,还都是匿名函数,我真的很难看出来谁是函数,谁是变量

    这就引出一个新问题,函数的二义性

    函数二义性

    众所周知, JS 的类就是函数,里面有自己的 this,可以 new 一个函数

    image.png

    你要知道他是函数还是类,一般是通过首字母是否大写区分
    但是这仅仅是弱规范,人家爱咋写咋写,所以后来出现了匿名函数(主要还是为了解决 this)

    匿名函数没有自己的 this 指向,没有 arguments,如下图

    image.png

    而且用 const 定义,所以也就没了函数提升,严格来说,匿名函数才是真函数

    不过我觉得直接写匿名函数有点丑,而且写起来似乎繁琐一点,虽然我都是用代码片段生成的
    如果用了匿名函数,那么我就没了函数提升了

    所以我仅仅在以下情况使用匿名函数

    1. 作为回调函数
    2. 不需要 this
    3. 函数重载

    函数重载我来说说吧,应该挺多人不知道。
    比如下图,针对每一种情况,写一遍类型,这样就能更加清楚描述函数的所有参数情况

    image.png

    不过这样好麻烦,而且好丑啊,于是可以用接口,这时你用 function 就实现不了了

    image.png

    var 的坏处

    1. var 会变量提升,你可能拿到 undefined

      image.png

    2. var 没有块级作用域,会导致变量共享

      按照常识,下面代码应该输出 0,1,2,3,4

      image.png

      但是你里面是异步打印,于是等你打印时,i 以及加了5次了,又没有块级作用域,所以你拿到的是同一个东西

      在古时候,是用立即执行函数解决的,如下图。因为函数会把变量存起来传给内部

      image.png

      现在用 let 就行了

      image.png

      所以我求求你别用 var 了

    格式化

    这里可能有争议性,仅仅是我个人喜欢,看着舒服

    大多数写前端的,基本人手一个 Prettier 插件自动格式化,再来个 EsLint
    然后也懒得看配置,默认就是 2 格缩进,回车多了会被删掉什么的

    这样下来,整个文件就相当臃肿,密密麻麻的,我看着很难受

    我的风格如下

    • 用 4 格缩进
    • 代码按照语义类型分块,写上块级文档注释
    • import 语句下面空两行,这样更加直观
    • 每一段,用独特醒目的文档注释划分
    • 定义变量优先使用 const,并且只写一个 const
    • 函数参数过长,则一行放一个参数
    • 写行内样式以及较长字符串时( 比如函数作为字符串 ),用特殊的宽松格式书写,保持类似代码的格式化
    • if 分支语句,要多空一行,看着清爽
    • 三目运算,用三行来写
    • 条件判断尽量提前 return,减少分支缩进

    下面来用图演示一下,不然看着上面的描述抽象

    代码按照语义类型分块,写上块级文档注释

    每一段逻辑写完,用个醒目的、大块的文档注释分开。
    全部执行的逻辑,放在一个 init 函数中

    image.png

    定义变量优先使用 const,并且只写一个 const

    比如声明变量,我喜欢这么写
    按照分类,类型不同则换行,并且写上注释,仅用一个 const

    image.png

    来看看大众写法,可以说 99.9878987%的人都这么写,这种我一看就难受

    image.png

    如果你用 let,并且用 4 格缩进,那么你就刚好对齐了,能少写一个回车
    不过尽量使用 const

    image.png

    函数参数过长,则一行放一个参数

    如果你这么写,那我看完会头晕眼花,属实是又臭又长的参数列表

    image.png

    如果你这么写,我会夸你代码和人一样好看

    image.png

    三目运算格式化

    这俩,你说谁的可读性高,肯定是分三行写的好看啊

    image.png

    字符串以及对象格式化

    这俩,你说谁看得舒服,那肯定是 2 啊
    我看了身边的人和网上的很多代码,大多数都是 1 这种

    image.png

    不管你是用字符串,还是对象等方式表达,你都应该写 2 这种样式

    image.png

    分支语句

    这俩哪种好看还用说吗,肯定是左边的好啊。但是你用 Prettier 格式化的话,应该就变成右边的了
    同理 try catch 之类的也是一样

    image.png

    最后,多用换行,我跪下来求求你了 😭


    作者:寅时码
    来源:juejin.cn/post/7335277377621639219

    收起阅读 »

    前端实现excel_xlsx文件预览

    web
    使用的框架: React 要使用的库: exceljs、handsontable 1. 概述 接到一个任务,是要前端实现文件预览效果,百度了一圈,发现也没有什么好的方法可以完美的将表格渲染出来。在前端中有sheetjs和exceljs可以对xlsx文件进行解...
    继续阅读 »

    使用的框架: React


    要使用的库: exceljs、handsontable



    1. 概述


    接到一个任务,是要前端实现文件预览效果,百度了一圈,发现也没有什么好的方法可以完美的将表格渲染出来。在前端中有sheetjsexceljs可以对xlsx文件进行解析,本来一开始我用的是sheetjs,但是在样式获取上遇到了麻烦,所以我改用了exceljs,不过很难受,在样式获取时同样遇到了不小的麻烦,但是我懒得换回sheetjs了,那就直接使用exceljs吧。


    要实现xlsx文件预览效果,我的想法是使用一个库对xlsx文件进行解析,然后使用另一个库对解析出来的数据在页面上进行绘制,综上,我采用的方案是:exceljs+handsontable


    2. 实现步骤


    2.1 安装库


    使用命令: npm i exceljs handsontable @handsontable/react


    2.2 使用exceljs解析数据并使用handsontable进行渲染


    直接贴代码了:


    import Excel from 'exceljs'
    import { useState } from 'react';

    import { HotTable } from '@handsontable/react';
    import { registerAllModules } from 'handsontable/registry';
    import 'handsontable/dist/handsontable.full.min.css';
    import { textRenderer, registerRenderer } from 'handsontable/renderers';

    // 注册模块
    registerAllModules();

    export default function XLSXPreView() {
    const [data, setData] = useState([]);

    const handleFile = async (e) => {
    const file = e.target.files[0];

    const workbook = new Excel.Workbook();
    await workbook.xlsx.load(file)

    // 第一个工作表
    const worksheet = workbook.getWorksheet(1);

    // 遍历工作表中的所有行(包括空行)
    const sheetData = [];
    worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
    // console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
    // 使用row.values获取每一行的值时总会多出一条空数据(第一条),这里我把它删除
    const row_values = row.values.slice(1);
    sheetData.push(row_values)
    });
    setData(sheetData);
    }

    return (
    <>
    <input type="file" onChange={handleFile}/>
    <div id='table_view'>
    <HotTable
    data={data}
    readOnly={true}
    rowHeaders={true}
    colHeaders={true}
    width="100vw"
    height="auto"
    licenseKey='non-commercial-and-evaluation'// 一定得加这个handsontable是收费的加了这个才能免费用
    />


    </div>
    </>

    )
    }

    到这里,已经实现了从xlsx文件中获取数据,并使用handsontable将表格中的数据渲染出来,示例结果如下,如果只需要将数据显示出来,并不需要将样式什么的一起复现了,那到这里就已经结束了!


    image.png


    但事实上,这并不是我要做到效果,我的xlsx里面还有样式什么的,也需要复现,头疼😔


    3. 其它的杂七杂八


    3.1 单元格样式


    事实上,在exceljs解析xlsx文件时,它顺带一起把样式获取到了,通过worksheet.getCell(1, 1).style可以获取对应单元格的样式,如下,背景色存放在fill.fgColor中,字体颜色存放在font.color中,这样的话只需要将这些样式一一赋值给handsontable组件再添加样式就好了。


    image.png


    但是实际操作的时候却遇到了问题,先说excel中的颜色,在选择颜色时,应该都会打开下面这个选项框吧,如果你选择的是标准色,它获取到的颜色就是十六进制,但是如果你选择主题中的颜色,那就是另一种结果了,并且还会有不同的深暗程度tint,这就很难受了!


    image.png


    随后在控制台中打印了workbook,发现它把主题返回了,可以通过work._themes.theme1获取,不过获取到的是xml格式的字符串,由于xml我没学,我不会,所以我就把它转换成json来进行处理了。


    第一步


    安装xml转json的库: npm i fast-xml-parser


    import {XMLParser} from 'fast-xml-parser'

    // 将主题xml转换成json
    const themeXml = workbook._themes.theme1;
    const options = {
    ignoreAttributes: false,
    attributeNamePrefix: '_'
    }
    const parser = new XMLParser(options);
    const json = parser.parse(themeXml)
    setThemeJson(json);


    其实它的theme好像是固定的,也可以在一些格式转换的网站中直接转换成json然后放到一个json文件中,读取就行,我这里就直接放到一个state中了!



    第二步


    接下来就是重头戏了!设置单元格样式...


    首先安装一个处理颜色的库color,用来根据tint获得不同明暗程度的颜色: npm i color


    下面是获取颜色的函数:


    // 根据主题和明暗度获取颜色
    const getThemeColor = (themeJson, themeId, tint) => {
    let color = '';
    const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme'];
    switch (themeId) {
    case 0:
    color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr'];
    break;
    case 1:
    color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr'];
    break;
    case 2:
    color = themeColorScheme['a:lt2']['a:srgbClr']['_val'];
    break;
    case 3:
    color = themeColorScheme['a:dk2']['a:srgbClr']['_val'];
    break;
    default:
    color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val'];
    break;
    }
    // 根据tint修改颜色深浅
    color = '#' + color;
    const colorObj = Color(color);
    if(tint){
    if(tint>0){// 淡色
    color = colorObj.lighten(tint).hex();
    }else{ // 深色
    color = colorObj.darken(Math.abs(tint)).hex();
    }
    }
    return color;
    }
    // 获取颜色
    const getColor = (obj, themeJson) => {
    if('argb' in obj){ // 标准色
    // rgba格式去掉前两位: FFFF0000 -> FF0000
    return '#' + obj.argb.substring(2);
    }else if('theme' in obj){ // 主题颜色
    if('tint' in obj){
    return getThemeColor(themeJson, obj.theme, obj.tint);
    }else{
    return getThemeColor(themeJson, obj.theme, null);
    }
    }
    }

    然后设置handonsontable的单元格的一些样式:颜色、加粗、下划线、边框balabala...的


    顺带把行高和列宽一起设置了,这个还比较简单,就一笔带过了...


    3.2 合并单元格


    从获取到的sheet中有一个_meages属性,该属性中存放了表格中所有的合并单元格区域,所以只需要将它们重新渲染在handsontable中就好。


    image.png


    然后就实现了表格的一些基本功能的预览,结果如下图:


    image.png


    3. 总结(附全代码)


    其实这个的本质主要就是通过ecxeljs解析表格文件的数据,然后通过handsontable将它们重新绘制在页面上,个人觉得这种方法并不好,因为表格里的操作太多了要把它们一一绘制工作量实在是太大了,而且很麻烦,如有有更好的方案希望大佬们告诉我一下,我这里把表格的一些常用到的功能实现了预览,还有想表格里放图片什么的都没有实现,如果有需要,可以根据需求再进行进行写。



    我写的其实还有一点bug,单元格的边框样式我只设置了solid和dashed,但事实上excel中单元格的边框有12种样式,而且还有对角线边框,设置起来好麻烦,我就不弄了,大家用的时候注意一下哈,有需要的话可以自己修改一下!



    附上全部代码:


    /**
    * exceljs + handsontable
    */

    import Excel from 'exceljs'
    import { useState } from 'react';

    import { HotTable } from '@handsontable/react';
    import { registerAllModules } from 'handsontable/registry';
    import 'handsontable/dist/handsontable.full.min.css';
    import { textRenderer, registerRenderer } from 'handsontable/renderers';

    import {XMLParser} from 'fast-xml-parser'
    import Color from 'color';

    // 注册模块
    registerAllModules();

    // 根据主题和明暗度获取颜色
    const getThemeColor = (themeJson, themeId, tint) => {
    let color = '';
    const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme'];
    switch (themeId) {
    case 0:
    color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr'];
    break;
    case 1:
    color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr'];
    break;
    case 2:
    color = themeColorScheme['a:lt2']['a:srgbClr']['_val'];
    break;
    case 3:
    color = themeColorScheme['a:dk2']['a:srgbClr']['_val'];
    break;
    default:
    color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val'];
    break;
    }
    // 根据tint修改颜色深浅
    color = '#' + color;
    const colorObj = Color(color);
    if(tint){
    if(tint>0){// 淡色
    color = colorObj.lighten(tint).hex();
    }else{ // 深色
    color = colorObj.darken(Math.abs(tint)).hex();
    }
    }
    return color;
    }
    // 获取颜色
    const getColor = (obj, themeJson) => {
    if('argb' in obj){ // 标准色
    // rgba格式去掉前两位: FFFF0000 -> FF0000
    return '#' + obj.argb.substring(2);
    }else if('theme' in obj){ // 主题颜色
    if('tint' in obj){
    return getThemeColor(themeJson, obj.theme, obj.tint);
    }else{
    return getThemeColor(themeJson, obj.theme, null);
    }
    }
    }
    // 设置边框
    const setBorder = (style) =>{
    let borderStyle = 'solid';
    let borderWidth = '1px';
    switch (style) {
    case 'thin':
    borderWidth = 'thin';
    break;
    case 'dotted':
    borderStyle = 'dotted';
    break;
    case 'dashDot':
    borderStyle = 'dashed';
    break;
    case 'hair':
    borderStyle = 'solid';
    break;
    case 'dashDotDot':
    borderStyle = 'dashed';
    break;
    case 'slantDashDot':
    borderStyle = 'dashed';
    break;
    case 'medium':
    borderWidth = '2px';
    break;
    case 'mediumDashed':
    borderStyle = 'dashed';
    borderWidth = '2px';
    break;
    case 'mediumDashDotDot':
    borderStyle = 'dashed';
    borderWidth = '2px';
    break;
    case 'mdeiumDashDot':
    borderStyle = 'dashed';
    borderWidth = '2px';
    break;
    case 'double':
    borderStyle = 'double';
    break;
    case 'thick':
    borderWidth = '3px';
    break;
    default:
    break;
    }
    // console.log(borderStyle, borderWidth);
    return [borderStyle, borderWidth];
    }

    export default function XLSXPreView() {
    // 表格数据
    const [data, setData] = useState([]);
    // 表格
    const [sheet, setSheet] = useState([]);
    // 主题
    const [themeJson, setThemeJson] = useState([]);
    // 合并的单元格
    const [mergeRanges, setMergeRanges] = useState([]);

    registerRenderer('customStylesRenderer', (hotInstance, td, row, column, prop, value, cellProperties) => {
    textRenderer(hotInstance, td, row, column, prop, value, cellProperties);
    // console.log(cellProperties);
    // 填充样式
    if('fill' in cellProperties){
    // 背景颜色
    if('fgColor' in cellProperties.fill && cellProperties.fill.fgColor){
    td.style.background = getColor(cellProperties.fill.fgColor, themeJson);
    }
    }
    // 字体样式
    if('font' in cellProperties){
    // 加粗
    if('bold' in cellProperties.font && cellProperties.font.bold){
    td.style.fontWeight = '700';
    }
    // 字体颜色
    if('color' in cellProperties.font && cellProperties.font.color){
    td.style.color = getColor(cellProperties.font.color, themeJson);
    }
    // 字体大小
    if('size' in cellProperties.font && cellProperties.font.size){
    td.style.fontSize = cellProperties.font.size + 'px';
    }
    // 字体类型
    if('name' in cellProperties.font && cellProperties.font.name){
    td.style.fontFamily = cellProperties.font.name;
    }
    // 字体倾斜
    if('italic' in cellProperties.font && cellProperties.font.italic){
    td.style.fontStyle = 'italic';
    }
    // 下划线
    if('underline' in cellProperties.font && cellProperties.font.underline){
    // 其实还有双下划线,但是双下划綫css中没有提供直接的设置方式,需要使用额外的css设置,所以我也就先懒得弄了
    td.style.textDecoration = 'underline';
    // 删除线
    if('strike' in cellProperties.font && cellProperties.font.strike){
    td.style.textDecoration = 'underline line-through';
    }
    }else{
    // 删除线
    if('strike' in cellProperties.font && cellProperties.font.strike){
    td.style.textDecoration = 'line-through';
    }
    }

    }
    // 对齐
    if('alignment' in cellProperties){
    if('horizontal' in cellProperties.alignment){ // 水平
    // 这里我直接用handsontable内置类做了,设置成类似htLeft的样子。
    //(handsontable)其实至支持htLeft, htCenter, htRight, htJustify四种,但是其是它还有centerContinuous、distributed、fill,遇到这几种就会没有效果,也可以自己设置,但是我还是懒的弄了,用到的时候再说吧
    const name = cellProperties.alignment.horizontal.charAt(0).toUpperCase() + cellProperties.alignment.horizontal.slice(1);
    td.classList.add(`ht${name}`);
    }
    if('vertical' in cellProperties.alignment){ // 垂直
    // 这里我直接用handsontable内置类做了,设置成类似htTop的样子。
    const name = cellProperties.alignment.vertical.charAt(0).toUpperCase() + cellProperties.alignment.vertical.slice(1);
    td.classList.add(`ht${name}`);
    }
    }
    // 边框
    if('border' in cellProperties){
    if('left' in cellProperties.border && cellProperties.border.left){// 左边框
    const [borderWidth, borderStyle] = setBorder(cellProperties.border.left.style);
    let color = '';
    // console.log(row, column, borderWidth, borderStyle);
    if(cellProperties.border.left.color){
    color = getColor(cellProperties.border.left.color, themeJson);
    }
    td.style.borderLeft = `${borderStyle} ${borderWidth} ${color}`;
    }
    if('right' in cellProperties.border && cellProperties.border.right){// 左边框
    const [borderWidth, borderStyle] = setBorder(cellProperties.border.right.style);
    // console.log(row, column, borderWidth, borderStyle);
    let color = '';
    if(cellProperties.border.right.color){
    color = getColor(cellProperties.border.right.color, themeJson);
    }
    td.style.borderRight = `${borderStyle} ${borderWidth} ${color}`;
    }
    if('top' in cellProperties.border && cellProperties.border.top){// 左边框
    const [borderWidth, borderStyle] = setBorder(cellProperties.border.top.style);
    let color = '';
    // console.log(row, column, borderWidth, borderStyle);
    if(cellProperties.border.top.color){
    color = getColor(cellProperties.border.top.color, themeJson);
    }
    td.style.borderTop = `${borderStyle} ${borderWidth} ${color}`;
    }
    if('bottom' in cellProperties.border && cellProperties.border.bottom){// 左边框
    const [borderWidth, borderStyle] = setBorder(cellProperties.border.bottom.style);
    let color = '';
    // console.log(row, column, borderWidth, borderStyle);
    if(cellProperties.border.bottom.color){
    color = getColor(cellProperties.border.bottom.color, themeJson);
    }
    td.style.borderBottom = `${borderStyle} ${borderWidth} ${color}`;
    }
    }

    });

    const handleFile = async (e) => {
    const file = e.target.files[0];

    const workbook = new Excel.Workbook();
    await workbook.xlsx.load(file)

    const worksheet = workbook.getWorksheet(1);

    // const sheetRows = worksheet.getRows(1, worksheet.rowCount);
    setSheet(worksheet)

    // console.log(worksheet.getCell(1, 1).style);

    // 遍历工作表中的所有行(包括空行)
    const sheetData = [];
    worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
    // console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
    // 使用row.values获取每一行的值时总会多出一条空数据(第一条),这里我把它删除
    const row_values = row.values.slice(1);
    sheetData.push(row_values)
    });
    setData(sheetData);

    // 将主题xml转换成json
    const themeXml = workbook._themes.theme1;
    const options = {
    ignoreAttributes: false,
    attributeNamePrefix: '_'
    }
    const parser = new XMLParser(options);
    const json = parser.parse(themeXml)
    setThemeJson(json);

    // 获取合并的单元格
    const mergeCells = [];

    for(let i in worksheet._merges){
    const {top, left, bottom, right} = worksheet._merges[i].model;
    mergeCells.push({ row: top-1, col: left-1, rowspan: bottom-top+1 , colspan: right-left+1})
    }
    setMergeRanges(mergeCells)
    console.log(worksheet);
    }

    return (
    <>
    <input type="file" onChange={handleFile}/>
    <div id='table_view'>
    <HotTable
    data={data}
    readOnly={true}
    rowHeaders={true}
    colHeaders={true}
    width="100vw"
    height="auto"
    licenseKey='non-commercial-and-evaluation'
    rowHeights={function(index) {
    if(sheet.getRow(index+1).height){
    // exceljs获取的行高不是像素值事实上它是23px - 13.8 的一个映射所以需要将它转化为像素值
    return sheet.getRow(index+1).height * (23 / 13.8);
    }
    return 23;// 默认
    }}
    colWidths={function(index){
    if(sheet.getColumn(index+1).width){
    // exceljs获取的列宽不是像素值事实上它是81px - 8.22 的一个映射所以需要将它转化为像素值
    return sheet.getColumn(index+1).width * (81 / 8.22);
    }
    return 81;// 默认
    }}
    cells={(row, col, prop) =>
    {
    const cellProperties = {};
    const cellStyle = sheet.getCell(row+1, col+1).style

    if(JSON.stringify(cellStyle) !== '{}'){
    // console.log(row+1, col+1, cellStyle);
    for(let key in cellStyle){
    cellProperties[key] = cellStyle[key];
    }
    }
    return {...cellProperties, renderer: 'customStylesRenderer'};
    }}
    mergeCells={mergeRanges}
    />

    </div>
    </>

    )
    }

    作者:汤圆要吃咸的
    来源:juejin.cn/post/7264461721279774780
    收起阅读 »

    告别axios,这个库让你爱上前端分页!

    web
    嗨,我们又见面了! 今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了! 那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢...
    继续阅读 »

    嗨,我们又见面了!


    今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了!


    那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢?这时候,alovajs就派上用场了!


    alovajs:轻量级请求策略库


    alovajs是一个轻量级的请求策略库,它可以帮助我们轻松处理分页请求。它支持开发者使用声明式实现各种复杂的请求,比如请求共享、分页请求、表单提交、断点续传等等。使用alovajs,我们可以用很少的代码就实现高效、流畅的请求功能。比如,在Vue中,你可以这样使用alovajs进行分页请求:


    const alovaInstance = createAlova({
    // VueHook用于创建ref状态,包括请求状态loading、响应数据data、请求错误对象error等
    statesHook: VueHook,
    requestAdapter: GlobalFetch(),
    responded: response => response.json()
    });

    const { loading, data, error } = useRequest(
    alovaInstance.Get('https://api.alovajs.org/profile', {
    params: {
    id: 1
    }
    })
    );

    看到了吗?只需要几行代码,alovajs就帮我们处理了分页请求的各种细节,我们再也不用手动维护那些繁琐的状态了!


    对比axios,alovajs的优势


    和axios相比,alovajs有哪些优势呢?首先,alovajs与React、Vue等现代前端框架深度融合,可以自动管理请求相关数据,大大提高了开发效率。其次,alovajs在性能方面做了很多优化,比如默认开启了内存缓存和请求共享,这些都能显著提高请求性能,提升用户体验的同时还能降低服务端的压力。最后,alovajs的体积更小,压缩后只有4kb+,相比之下,axios则有11+kb。


    总之,如果你想在分页加载方面做得更轻松、更高效,alovajs绝对值得一试!



    作者:古韵
    来源:juejin.cn/post/7331924057925533746
    收起阅读 »

    indexOf的第二个参数你用过嘛🤔

    web
    大家好,我是哈默。indexOf 是我们非常熟悉的一个方法,它可以用来获取某一个元素在一个数组里的位置,我们一般就会使用 array.indexOf(element) 的方法来进行使用。 但是,大家有没有使用过 indexOf 的第二个参数呢?第二个参数的使用...
    继续阅读 »

    大家好,我是哈默。indexOf 是我们非常熟悉的一个方法,它可以用来获取某一个元素在一个数组里的位置,我们一般就会使用 array.indexOf(element) 的方法来进行使用。


    但是,大家有没有使用过 indexOf 的第二个参数呢?第二个参数的使用会经常出现在一些优秀的库的源码当中,用于依次分析(或者说扫描)某一个字符串。


    比如命令行美化输出的 chalk 库中就有此应用,因为 chalk 库的原理就是对于我们输出在终端的内容进行处理,然后将处理后的字符串显示在终端上。


    indexOf 基本用法


    首先,我们还是先来回顾一下 indexOf 的最基本用法。


    给定一个数组:[10, 20, 30],寻找这个数组中 30 的位置,是 2


    const arr = [10, 20, 30];
    const element = 30;
    const index = arr.indexOf(element);

    console.log(index); // 2

    indexOf 的第二个参数


    明确了 indexOf 的基本用法以后,它的第 2 个参数有什么用呢?


    其实是起到了一个调整从哪里开始查找的作用。


    我们来看一个例子:


    const arr = [10, 20, 30];
    const element = 10;
    const index = arr.indexOf(element);

    console.log(index); // 0

    const arr2 = [10, 20, 30, 10];
    const element2 = 10;
    const index2 = arr2.indexOf(element2, 1);

    console.log(index2); // 3

    可以看到,同样是查找 [10, 20, 30, 10] 当中 10 的位置,但是因为第一次是从数组第 1 个元素开始查找的,所以得到的结果是 0。


    而第二次是从数组的第 2 个元素开始查找的,所以得到的结果是 3。


    优秀库源码里的使用


    明确了 indexOf 第二个参数的使用之后,我们再来看一下在一些优秀的库的源码里面,它们是如何利用起这个第二个参数的作用的。



    ⚠️注意:我下面会以 String.prototype.indexOf 举例,而上面举的例子是以 Array.prototype.indexOf 为例,但是这两个 API 的第二个参数都是起到一个搜索位置的作用,所以在这里可以一起学习一下



    这里,我们只会分析它的思想,具体的实现在具体的源码里会存在差异,但思想是相同的。


    我们首先定义一个方法,addEmoji,接受三个参数:


    /**
    * 在一个 string 的 targetString 后面,加上一个 emoji
    * @param string 原始 string
    * @param targetString 加 emoji 的那个 string
    * @param emoji 加入的 emoji
    * @returns 处理后的最终结果
    */

    function addEmoji(string, targetString, emoji) {
    let result = "";

    // 一系列处理
    // ...

    return result;
    }

    我们最终会这样调用,在 大家好,我是哈默,今天是一个好天气。 这个字的后面,加上 👍 的 emoji:


    const res = addEmoji("大家好,我是哈默,今天是一个好天气。", "好", "👍");
    console.log(res);

    那么首先我们就可以使用 indexOf 方法来从输入的字符串里找到 的位置:


    function addEmoji(string, targetString, emoji) {
    // 找到 targetString 的位置
    let index = string.indexOf(targetString);

    let result = "";

    // 记录当前扫描到的位置,现在是在参数 string 的开头位置
    // 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
    let currentScanIndex = 0;

    return result;
    }

    如果我们找到了 targetString,即 index !== -1,那么我们就在 targetString 后,加上一个 emoji:


    function addEmoji(string, targetString, emoji) {
    // 找到 targetString 的位置
    let index = string.indexOf(targetString);

    let result = "";

    // 记录当前扫描到的位置,现在是在参数 string 的开头位置
    // 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
    let currentScanIndex = 0;

    // 如果找到了 targetString
    if (index !== -1) {
    // 在 targetString 后面增加 emoji
    result += string.slice(currentScanIndex, index) + targetString + emoji;
    // 将当前扫描位置,移动到 targetString 之后的那个位置上
    currentScanIndex = index + targetString.length;
    }

    // 将 targetString 之后的内容追加到 result 里
    result += string.slice(currentScanIndex);

    return result;
    }

    此时,我们在第一个 字后面,加上了 👍,得到的结果:


    res1.png


    但是,我们这个字符串中,还有一个 好天气,也就是存在多个 targetString,所以我们这里不能是 if 只执行一次,而是要做一个循环。


    我们可以使用一个 while 循环:


    function addEmoji(string, targetString, emoji) {
    // 找到 targetString 的位置
    let index = string.indexOf(targetString);

    let result = "";

    // 记录当前扫描到的位置,现在是在参数 string 的开头位置
    // 因为 string 当中,可能会存在多个 targetString,所以我们会跳着进行扫描,也就是会使用 indexOf 的第二个参数
    let currentScanIndex = 0;

    // 如果找到了 targetString
    while (index !== -1) {
    // 在 targetString 后面增加 emoji
    result += string.slice(currentScanIndex, index) + targetString + emoji;
    // 将当前扫描位置,移动到 targetString 之后的那个位置上
    currentScanIndex = index + targetString.length;
    + // 重点来了!!!我们要从当前扫描的位置开始,去寻找 targetString
    + index = string.indexOf(targetString, currentScanIndex);
    }

    // 将 targetString 之后的内容追加到 result 里
    result += string.slice(currentScanIndex);

    return result;
    }

    此时,我们便成功的给第二个 ,也加上了 emoji:


    res2.png


    这个地方我们就使用到了之前提到的 indexOf 的第二个参数:


    // 重点来了!!!我们要从当前扫描的位置开始,去寻找 targetString
    index = string.indexOf(targetString, currentScanIndex);

    我们是从当前扫描到的位置 currentScanIndex 开始,查找 targetString 的,这样我们就可以找到下一个 targetString 了。


    所以,这里的思想就是通过 indexOf 的第二个参数,帮助我们能够依次扫描一个字符串,依次找到我们想要找的那个元素的位置,然后做相应的处理。


    总结


    indexOf 的第二个参数,叫 fromIndex,看到这里,大家应该也能很好的理解这个 fromIndex 的作用了,就是从哪里开始找嘛!


    作者:我是哈默
    来源:juejin.cn/post/7332858431571230747
    收起阅读 »

    春晚刘谦魔术的模拟程序

    web
    昨晚春晚上刘谦的两个魔术表演都非常精彩,尤其是第二个魔术,他演绎了经典的约瑟夫环问题! 什么是约瑟夫环问题? 约瑟夫环(Josephus problem)是一个经典的数学问题,最早由古罗马历史学家弗拉维奥·约瑟夫斯提出,但它的名字是在19世纪由德国数学家约瑟夫...
    继续阅读 »

    昨晚春晚上刘谦的两个魔术表演都非常精彩,尤其是第二个魔术,他演绎了经典的约瑟夫环问题!


    什么是约瑟夫环问题?


    约瑟夫环(Josephus problem)是一个经典的数学问题,最早由古罗马历史学家弗拉维奥·约瑟夫斯提出,但它的名字是在19世纪由德国数学家约瑟夫·乔瑟夫斯(Josef Stein)命名的。


    问题的描述是这样的:假设有n个人(编号从1到n)站成一个圆圈,从第一个人开始报数,报到某个数字(例如k)的人就被杀死,然后从下一个人开始重新报数并继续这个过程,直到只剩下一个人留下来。


    问题的关键是找出存活下来的那个人的编号。


    结合扑克牌解释约瑟夫环问题


    1、考虑最简单的情况


    假设有2张牌,编号分别是1和2。


    首先将1放到后面,扔掉2。剩下的就是最开始放在最上边的那张1。


    2、稍微复杂一点的情况,牌的张数是2的n次方


    比如有8张牌,编号分别是1、2、3、4、5、6、7、8。


    第一轮会把2、4、6、8扔掉,剩下1、3、5、7按顺序放在后面,又退化成了4张牌的情况。


    第二轮会把3、7扔掉,剩下1、5按顺序放在后面,又退化成了2张牌的情况。


    第三轮把5扔掉,剩下1,就是最初在最前面的那张。


    结论:如果牌的张数是2^n,最后剩下的一定是最开始放在牌堆顶的那张。


    3、考虑任意的情况,牌的张数是2^n+m


    比如牌的张数是11,等于8+3。把1放到后面,把2扔掉,把3放到后面,把4扔掉,把5放到后面,把6扔掉,现在剩下的编号序列是7、8、9、10、11、1、3、5,这又是8张牌的情况!最后一定剩下的是现在牌堆顶的7!


    因此,只要提前知道牌的张数,就一定能马上推导出最终是剩下哪一张牌。一切的魔法都是数学!!都是算法!!


    见证奇迹的时刻!魔术的流程



    1. 4张牌对折后撕开,就是8张,叠放在一起就是ABCDABCD。注意,ABCD四个数字是完全等价的。

    2. 根据名字字数,把顶上的牌放到下面,但怎么放都不会改变循环序列的相对位置。譬如2次,最后变成CDABCDAB;譬如3次,最后换成DABCDABC。但无论怎么操作,第4张和第8张牌都是一样的。

    3. 把顶上3张插到中间任意位置。这一步非常重要!因为操作完之后必然出现第1张和第8张牌是一样的!以名字两个字为例,可以写成BxxxxxxB(这里的x是其他和B不同的牌)。

    4. 拿掉顶上的牌放到一边,记为B。剩下的序列是xxxxxxB,一共7张牌。

    5. 南方人/北方人/不确定,分别拿顶上的1/2/3张牌插到中间,但是不会改变剩下7张牌是xxxxxxB的结果。

    6. 男生拿掉1张,女生拿掉2张。也就是男生剩下6张,女生剩下5张。分别是xxxxxB和xxxxB。

    7. 循环7次,把最顶上的放到最底下,男生和女生分别会是xxxxBx和xxBxx。

    8. 最后执行约瑟夫环过程!操作到最后只剩下1张。当牌数为6时(男生),剩下的就是第5张牌;当牌数为5时(女生),剩下的就是第3张牌。Bingo!就是第4步拿掉的那张牌!


    下面是完整的 JavaScript 代码实现:


    // 定义一个函数,用于把牌堆顶n张牌移动到末尾
    function moveCardBack(n, arr) {
    // 循环n次,把队列第一张牌放到队列末尾
    for (let i = 0; i < n; i++) {
    const moveCard = arr.shift(); // 弹出队头元素,即第一张牌
    arr.push(moveCard); // 把原队头元素插入到序列末尾
    }
    return arr;
    }

    // 定义一个函数,用于把牌堆顶n张牌移动到中间的任意位置
    function moveCardMiddleRandom(n, arr) {
    // 插入在arr中的的位置,随机生成一个idx
    // 这个位置必须是在n+1到arr.length-1之间
    const idx = Math.floor(Math.random() * (arr.length - n - 1)) + n + 1;
    // 执行插入操作
    const newArr = arr.slice(n, idx).concat(arr.slice(0, n)).concat(arr.slice(idx));
    return newArr;
    }

    // 步骤1:初始化8张牌,假设为"ABCDABCD"
    let arr = ["A", "B", "C", "D", "A", "B", "C", "D"];
    console.log("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。");
    console.log("此时序列为:" + arr.join('') + "\n---");

    // 步骤2(无关步骤):名字长度随机选取,这里取2到5(其实任意整数都行)
    const nameLen = Math.floor(Math.random() * 4) + 2;
    // 把nameLen张牌移动到序列末尾
    arr = moveCardBack(nameLen, arr);
    console.log(`步骤2:随机选取名字长度为${nameLen},把第1张牌放到末尾,操作${nameLen}次。`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤3(关键步骤):把牌堆顶三张放到中间任意位置
    arr = moveCardMiddleRandom(3, arr);
    console.log(`步骤3:把牌堆顶3张放到中间的随机位置。`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤4(关键步骤):把最顶上的牌拿走
    const restCard = arr.shift(); // 弹出队头元素
    console.log(`步骤4:把最顶上的牌拿走,放在一边。`);
    console.log(`拿走的牌为:${restCard}`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤5(无关步骤):根据南方人/北方人/不确定,把顶上的1/2/3张牌插入到中间任意位置
    // 随机选择1、2、3中的任意一个数字
    const moveNum = Math.floor(Math.random() * 3) + 1;
    arr = moveCardMiddleRandom(moveNum, arr);
    console.log(`步骤5:我${moveNum === 1 ? '是南方人' : moveNum === 2 ? '是北方人' : '不确定自己是哪里人'},\
    ${moveNum}张牌插入到中间的随机位置。`
    );
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤6(关键步骤):根据性别男或女,移除牌堆顶的1或2张牌
    const maleNum = Math.floor(Math.random() * 2) + 1; // 随机选择1或2
    for (let i = 0; i < maleNum; i++) { // 循环maleNum次,移除牌堆顶的牌
    arr.shift();
    }
    console.log(`步骤6:我是${maleNum === 1 ? '男' : '女'}生,移除牌堆顶的${maleNum}张牌。`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤7(关键步骤):把顶部的牌移动到末尾,执行7次
    arr = moveCardBack(7, arr);
    console.log(`步骤7:把顶部的牌移动到末尾,执行7次`);
    console.log(`此时序列为:${arr.join('')}\n---`);

    // 步骤8(关键步骤):执行约瑟夫环过程。把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。
    console.log(`步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。`);
    while (arr.length > 1) {
    const luck = arr.shift(); // 好运留下来
    arr.push(luck);
    console.log(`好运留下来:${luck}\t\t此时序列为:${arr.join('')}`);
    const sadness = arr.shift(); // 烦恼都丢掉
    console.log(`烦恼都丢掉:${sadness}\t\t此时序列为:${arr.join('')}`);
    }
    console.log(`---\n最终结果:剩下的牌为${arr[0]},步骤4中留下来的牌也是${restCard}`);


    这段代码实现了昨晚春晚上刘谦的第二个魔术表演的过程,并提供了详细的解释。享受魔术的魅力吧!


    image-20240210161329783


    image-20240210161339317


    看到观看的人这么多,除了JavaScript,下面我补充了一些其他语言的实现


    import random

    # 定义一个函数,用于把牌堆顶n张牌移动到末尾
    def move_card_back(n, arr):
       for i in range(n):
           move_card = arr.pop(0)
           arr.append(move_card)
       return arr

    # 定义一个函数,用于把牌堆顶n张牌移动到中间的任意位置
    def move_card_middle_random(n, arr):
       idx = random.randint(n + 1, len(arr) - 1)
       new_arr = arr[n:idx] + arr[0:n] + arr[idx:]
       return new_arr

    # 步骤1:初始化8张牌,假设为"ABCDABCD"
    arr = ["A", "B", "C", "D", "A", "B", "C", "D"]
    print("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤2(无关步骤):名字长度随机选取,这里取2到5(其实任意整数都行)
    name_len = random.randint(2, 5)
    move_card_back(name_len, arr)
    print("步骤2:随机选取名字长度为" + str(name_len) + ",把第1张牌放到末尾,操作" + str(name_len) + "次。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤3(关键步骤):把牌堆顶三张放到中间任意位置
    arr = move_card_middle_random(3, arr)
    print("步骤3:把牌堆顶3张放到中间的随机位置。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤4(关键步骤):把最顶上的牌拿走
    rest_card = arr.pop(0)
    print("步骤4:把最顶上的牌拿走,放在一边。")
    print("拿走的牌为:" + rest_card)
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤5(无关步骤):根据南方人/北方人/不确定,把顶上的1/2/3张牌插入到中间任意位置
    # 随机选择1、2、3中的任意一个数字
    move_num = random.randint(1, 3)
    arr = move_card_middle_random(move_num, arr)
    print("步骤5:我" + ("是南方人" if move_num == 1 else "是北方人" if move_num == 2 else "不确定自己是哪里人") + ",把" + str(move_num) + "张牌插入到中间的随机位置。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤6(关键步骤):根据性别男或女,移除牌堆顶的1或2张牌
    male_num = random.randint(1, 2)
    for i in range(male_num):
       arr.pop(0)
    print("步骤6:我是" + ("男" if male_num == 1 else "女") + "生,移除牌堆顶的" + str(male_num) + "张牌。")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤7(关键步骤):把顶部的牌移动到末尾,执行7次
    for i in range(7):
       move_card = arr.pop(0)
       arr.append(move_card)
    print("步骤7:把顶部的牌移动到末尾,执行7次")
    print("此时序列为:" + ''.join(arr) + "\n---")

    # 步骤8(关键步骤):执行约瑟夫环过程。把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。
    print("步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。")
    while len(arr) > 1:
       luck = arr.pop(0)
       arr.append(luck)
       print("好运留下来:" + luck + "\t\t此时序列为:" + ''.join(arr))
       sadness = arr.pop(0)
       print("烦恼都丢掉:" + sadness + "\t\t此时序列为:" + ''.join(arr))
    print("---\n最终结果:剩下的牌为" + arr[0] + ",步骤4中留下来的牌也是" + rest_card)


    java


    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;

    public class Main {
       public static void main(String[] args) {
           List<String> arr = new ArrayList<>();
           arr.add("A");
           arr.add("B");
           arr.add("C");
           arr.add("D");
           arr.add("A");
           arr.add("B");
           arr.add("C");
           arr.add("D");

           System.out.println("步骤1:拿出4张牌,对折撕成8张,按顺序叠放。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           Random rand = new Random();

           int nameLen = rand.nextInt(4) + 2;
           moveCardBack(nameLen, arr);
           System.out.println("步骤2:随机选取名字长度为" + nameLen + ",把第1张牌放到末尾,操作" + nameLen + "次。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           moveCardMiddleRandom(3, arr);
           System.out.println("步骤3:把牌堆顶3张放到中间的随机位置。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           String restCard = arr.remove(0);
           System.out.println("步骤4:把最顶上的牌拿走,放在一边。");
           System.out.println("拿走的牌为:" + restCard);
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           int moveNum = rand.nextInt(3) + 1;
           moveCardMiddleRandom(moveNum, arr);
           System.out.println("步骤5:我" + (moveNum == 1 ? "是南方人" : moveNum == 2 ? "是北方人" : "不确定自己是哪里人") + ",把" + moveNum + "张牌插入到中间的随机位置。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           int maleNum = rand.nextInt(2) + 1;
           for (int i = 0; i < maleNum; i++) {
               arr.remove(0);
          }
           System.out.println("步骤6:我是" + (maleNum == 1 ? "男" : "女") + "生,移除牌堆顶的" + maleNum + "张牌。");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           for (int i = 0; i < 7; i++) {
               String moveCard = arr.remove(0);
               arr.add(moveCard);
          }
           System.out.println("步骤7:把顶部的牌移动到末尾,执行7次");
           System.out.println("此时序列为:" + String.join("", arr));
           System.out.println("---");

           System.out.println("步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。");
           while (arr.size() > 1) {
               String luck = arr.remove(0);
               arr.add(luck);
               System.out.println("好运留下来:" + luck + "\t\t此时序列为:" + String.join("", arr));
               String sadness = arr.remove(0);
               System.out.println("烦恼都丢掉:" + sadness + "\t\t此时序列为:" + String.join("", arr));
          }
           System.out.println("---\n最终结果:剩下的牌为" + arr.get(0) + ",步骤4中留下来的牌也是" + restCard);
      }

       private static void moveCardBack(int n, List<String> arr) {
           for (int i = 0; i < n; i++) {
               String moveCard = arr.remove(0);
               arr.add(moveCard);
          }
      }

       private static void moveCardMiddleRandom(int n, List<String> arr) {
           Random rand = new Random();
           int idx = rand.nextInt(arr.size() - n - 1) + n + 1;
           List<String> newArr = new ArrayList<>(arr.subList(n, idx));
           newArr.addAll(arr.subList(0, n));
           newArr.addAll(arr.subList(idx, arr.size()));
           arr.clear();
           arr.addAll(newArr);
      }
    }


    以及c++代码


    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <cstdlib>
    #include <ctime>

    void moveCardBack(int n, std::vector<std::string>& arr) {
       for (int i = 0; i < n; i++) {
           std::string moveCard = arr[0];
           arr.erase(arr.begin());
           arr.push_back(moveCard);
      }
    }

    void moveCardMiddleRandom(int n, std::vector<std::string>& arr) {
       int idx = rand() % (arr.size() - n - 1) + n + 1;
       std::vector<std::string> newArr;
       newArr.insert(newArr.end(), arr.begin() + n, arr.begin() + idx);
       newArr.insert(newArr.end(), arr.begin(), arr.begin() + n);
       newArr.insert(newArr.end(), arr.begin() + idx, arr.end());
       arr = newArr;
    }

    int main() {
       srand(time(0));

       std::vector<std::string> arr = {"A", "B", "C", "D", "A", "B", "C", "D"};
       std::cout << "步骤1:拿出4张牌,对折撕成8张,按顺序叠放。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       int nameLen = rand() % 4 + 2;
       moveCardBack(nameLen, arr);
       std::cout << "步骤2:随机选取名字长度为" << nameLen << ",把第1张牌放到末尾,操作" << nameLen << "次。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       moveCardMiddleRandom(3, arr);
       std::cout << "步骤3:把牌堆顶3张放到中间的随机位置。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       std::string restCard = arr[0];
       arr.erase(arr.begin());
       std::cout << "步骤4:把最顶上的牌拿走,放在一边。" << std::endl;
       std::cout << "拿走的牌为: " << restCard << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       moveCardMiddleRandom(rand() % 3 + 1, arr);
       std::cout << "步骤5:我" << (rand() % 2 == 0 ? "是南方人" : "是北方人") << ",把" << rand() % 3 + 1 << "张牌插入到中间的随机位置。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       int maleNum = rand() % 2 + 1;
       for (int i = 0; i < maleNum; i++) {
           arr.erase(arr.begin());
      }
       std::cout << "步骤6:我" << (maleNum == 1 ? "男" : "女") << "生,移除牌堆顶的" << maleNum << "张牌。" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       for (int i = 0; i < 7; i++) {
           std::string moveCard = arr[0];
           arr.erase(arr.begin());
           arr.push_back(moveCard);
      }
       std::cout << "步骤7:把顶部的牌移动到末尾,执行7次" << std::endl;
       std::cout << "此时序列为: ";
       for (const std::string& card : arr) {
           std::cout << card;
      }
       std::cout << std::endl;
       std::cout << "---" << std::endl;

       std::cout << "步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。" << std::endl;
       while (arr.size() > 1) {
           std::string luck = arr[0];
           arr.erase(arr.begin());
           arr.push_back(luck);
           std::cout << "好运留下来: " << luck << "\t\t此时序列为: ";
           for (const std::string& card : arr) {
               std::cout << card;
          }
           std::cout << std::endl;

           std::string sadness = arr[0];
           arr.erase(arr.begin());
           std::cout << "烦恼都丢掉: " << sadness << "\t\t此时序列为: ";
           for (const std::string& card : arr) {
               std::cout << card;
          }
           std::cout << std::endl;
      }
       std::cout << "---\n最终结果: " << arr[0] << ", 步骤4中留下来的牌也是" << restCard << std::endl;

       return 0;
    }

    作者:小u
    来源:juejin.cn/post/7332865125640044556
    收起阅读 »

    async/await 你可能正在将异步写成同步

    web
    前言 你是否察觉到自己随手写的异步函数,实际却是“同步”的效果! 正文 以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。 第一版 思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。 import path fr...
    继续阅读 »

    前言


    你是否察觉到自己随手写的异步函数,实际却是“同步”的效果!


    正文


    以一个需求为例:获取给定目录下的全部文件,返回所有文件的路径数组。


    第一版


    思路很简单:读取目录内容,如果是文件,添加进结果数组,如果还是目录,我们递归执行。


    import path from 'node:path'
    import fs from 'node:fs/promises'
    import { existsSync } from 'node:fs'

    async function findFiles(root) {
    if (!existsSync(root)) return

    const rootStat = await fs.stat(root)
    if (rootStat.isFile()) return [root]

    const result = []
    const find = async (dir) => {
    const files = await fs.readdir(dir)
    for (let file of files) {
    file = path.resolve(dir, file)
    const stat = await fs.stat(file)
    if (stat.isFile()) {
    result.push(file)
    } else if (stat.isDirectory()) {
    await find(file)
    }
    }
    }
    await find(root)
    return result
    }

    机智的你是否已经发现了问题?


    我们递归查询子目录的过程是不需要等待上一个结果的,但是第 20 行代码,只有查询完一个子目录之后才会查询下一个,显然让并发的异步,变成了顺序的“同步”执行。


    那我们去掉 20 行的 await 是不是就可以了,当然不行,这样的话 await find(root) 在没有完全遍历目录之前就会立刻返回,我们无法拿到正确的结果。


    思考一下,怎么修改它呢?......让我们看第二版代码。


    第二版


    import path from 'node:path'
    import fs from 'node:fs/promises'
    import { existsSync } from 'node:fs'

    async function findFiles(root) {
    if (!existsSync(root)) return

    const rootStat = await fs.stat(root)
    if (rootStat.isFile()) return [root]

    const result = []
    const find = async (dir) => {
    const task = (await fs.readdir(dir)).map(async (file) => {
    file = path.resolve(dir, file)
    const stat = await fs.stat(file)
    if (stat.isFile()) {
    result.push(file)
    } else if (stat.isDirectory()) {
    await find(file)
    }
    })
    return Promise.all(task)
    }
    await find(root)
    return result
    }

    我们把每个子目录内容的查询作为独立的任务,扔给 Promise.all 执行,就是这个简单的改动,性能得到了质的提升,让我们看看测试,究竟能差多少。


    对比测试


    console.time('v1')
    const files1 = await findFiles1('D:\\Videos')
    console.timeEnd('v1')

    console.time('v2')
    const files2 = await findFiles2('D:\\Videos')
    console.timeEnd('v2')

    console.log(files1?.length, files2?.length)

    result


    版本二快了三倍不止,如果是并发的接口请求被不小心搞成了顺序执行,差距比这还要夸张。


    作者:justorez
    来源:juejin.cn/post/7332031293877485578
    收起阅读 »

    为什么大家都不想回家过年了

    大家好,我是林家少爷,是一位专注于前端技术,立志成为前端技术专家的热血青年。 2023年,看到身边的朋友多多少少都经历了裁员,降薪,甚至被迫自离。 我还好,2023年唯一值得庆幸的事情就是我工资涨薪2k,不过年终奖依旧无望。 今年年会都取消了,过年礼品也没见影...
    继续阅读 »

    大家好,我是林家少爷,是一位专注于前端技术,立志成为前端技术专家的热血青年。


    2023年,看到身边的朋友多多少少都经历了裁员,降薪,甚至被迫自离。


    我还好,2023年唯一值得庆幸的事情就是我工资涨薪2k,不过年终奖依旧无望。
    今年年会都取消了,过年礼品也没见影。


    坦白讲,自从放开疫情之后,行情不仅没有好转,反而更差了,预计2024年要比之前更难,所以,2024年到2025年,这两年还是多攒钱,多搞钱,然后把钱存下来,尽量多存钱,比什么都强。


    好不容易熬到了过年,本来想回到温暖的港湾,把2023年的事情好的不好的都说出来跟家里人倾诉一下。


    没想到面对的,是家里人的催婚,在一线城市工资那么高,钱都到哪里去了,人家谁谁才比你大三个月都已经二胎了......


    甚至过年还要走亲戚,甚至拉着亲戚说介绍对象。


    这些我都理解,因为我正在经历这一切。


    我的观点是,2023年赚钱是真不容易,打工的面对工作量增加,平时无偿加班,领导PUA(可能领导也被更高级的领导PUA),还有被迫降薪,薪资被压年底年终奖(然后这中间可操作空间很大),而且大城市本来消费就高,有好些同事被裁员还拿不到裁员费,大过年还要跳楼拉横幅争取,以上总总,我都亲眼看过,赚钱太难了。


    我从2020年开始,一直都在做副业,中间经历了一次失败的创业,2021年回归职场,也是行情最差的时候,没想到后面两年越来越差,也明显感觉2023年副业赚钱越来越卷,任何一点赚钱的项目也会被互联网给公开,摊平了信息差,然后大家一窝蜂地涌进来,自己流量也被抢占了,很快这个项目又得放弃了,我还没统计2023年我的副业赚了多少,但肯定不超过10万,离我定的目标(超过主业工资持续3个月以上)还差很远,而且总是被加班,老板周末问话给打断,有时候自己也被气炸了,好几次都想不干了,但是看着自己还没有做起来的副业盘子,贸贸然走人收入立马断了,也是要重新找工作的,就忍了下来。


    大家看到这里就知道我当时内心有多矛盾,但是我都坚持了下来,我相信总有一天我可以真正把副业做起来,真正拥有属于自己的事业,手上有许多现钱,不需要看任何人眼色,我能活成我自己。


    回到过年这个话题,很多老一辈就觉得,大过年的就应该走走亲戚,见见七八姑八大姨,互相聊聊家常,好不热闹。


    但是我身边很多同事,不包括程序员,其实都是偏内向的人(包括我也是),就想着在家里跟家里人倾诉一下,哪怕不倾诉,也是关在家里面,把房间打扫的干净整洁,安静的看书,或者玩玩游戏,或者搞钱,就是不想去见一些八竿子打不着的亲戚,这是一种精神内耗。


    说实话,他们真的只想回家好好休息,啥事都不做,饭店有家人做好的满满一桌饭菜,上班那点屁事就不管他了~


    催婚,算了吧,想当年我们的目标都是考清华北大,985,211,为了高考付出了多少个不眠之夜,但是人人都考得上吗?


    尤其是奔三的女孩子,我能体会她们被家里人各种催婚的痛苦,真的会把人逼疯。


    婚姻大事,岂非儿戏,还是要找到同频人,先谈恋爱,再结婚,感情基础决定上层建筑,只有这样才能长长久久。


    搭伙过日子,那只适合70年代,不适合我们,随意搭伙,闪婚,没有前期磨合阶段,大概率会闪离,现在离婚率高不是没有原因的,所以还需慎重。


    至于面对家里走亲戚,出到社会都知道不过是演戏而已,跟着演,演到底即可。


    不是个好演员做不了一个好销售,不是好销售做不了一个会赚钱的程序员。


    这句话我自己总结出来的,会自我营销就是要会演戏,把自己都骗过了才能骗别人。


    前期会比较累,慢慢就习惯了。


    实在忍受不了,下一年大不了不回家了,图个清静。


    点到为止,祝大家新年快乐。


    作者:林家少爷
    来源:juejin.cn/post/7332593229337100303
    收起阅读 »

    小镇做题家必须要跨过的三道坎

    其实我们大多数人都还称不上小镇做题家,因为我们大多都是来自乡村,只不过在乡镇做了上了几年的学而已。 大多数人的人生轨迹基本上都是从乡村到乡镇再到县城,最终去到了市级以上的城市读大学,没有资源,没人指路,毕业后也是一脸茫然,最终回到原点! 所以大多数小镇做题家的...
    继续阅读 »

    其实我们大多数人都还称不上小镇做题家,因为我们大多都是来自乡村,只不过在乡镇做了上了几年的学而已。


    大多数人的人生轨迹基本上都是从乡村到乡镇再到县城,最终去到了市级以上的城市读大学,没有资源,没人指路,毕业后也是一脸茫然,最终回到原点!


    所以大多数小镇做题家的生活永远都是那么艰难,很难成为出题家,是因为多数人身上都出现了致命的弱点,而这些弱点多数是由原生家庭带来的。


    一.自卑


    自卑可以说是原生家庭带来的第一原罪,也是很难突破的一关,而导致自卑的导火索一定是贫穷。


    因为我们多数人从小都被父母灌输“我们家穷,所以你不应该这样做”。


    所以在进入大学后,多数人的心中都有一个魔咒,我不配拥有,不配出去玩,不配谈恋爱,不配穿好看的衣服,即使是自己打工赚的钱,多花一分都觉得有负罪感。


    因为心里会想着,父母那么辛苦,我就不应该去花钱,而是要节省,当然节省并没有不好。


    但是从逻辑上就已经搞错了,首先如果自己舍不得钱去学习,去投资自己,一个月挣3000能够省下2000,那么这个省钱毫无意义,因为省下的这点钱根本做不了什么,但是会把自己置入一个更深的漩涡,收入无法提升,眼界无法开阔,而是一直盯着一个月省2000这个感人的骗局。


    除了用钱有负罪感,还有不敢向上社交,觉得自己是小山旮旯出来的,不配和优秀的人结伴,因为父母从小就说: 我们家条件没人家好,人家有钱,人家会瞧不起你的。所以很多人内心就已经打了退堂鼓,从而错过了和优秀的人链接的机会。


    但是事实上真正优秀的人,人家从来不会有瞧不起人的心态,只要你身上有优点,你们之间能够进行价值交换,别人比你还谦卑。


    我这几年从互联网行业链接到的人有1000个以上,优秀的人不在少数,我发现越优秀的人越谦虚。


    自卑导致的问题还很多,比如做事扭扭咧咧,不敢上台,不敢发表意见,总是躲在角落里。


    但是这个社会没有人喜欢扭扭咧咧的人,都喜欢落落大方的人,即使你的知识不专业,你普通话不好,这些都不重要,但是只要你表现出自信,大方的态度,那么大家都会欣赏你,因为这就是勇敢。


    二.面子


    有一个事实,越脆弱的人越爱面子,越无能的人越爱面子。


    这句话虽然说起来有点不好听,但是这是事实,我们这种小镇做题家最要先放下面子。


    比如自己不懂很多东西,但是不愿意去问别人,怕被别人说,在学校的时候,有问题不敢问,于是花费很多时间去弄,到最后还是搞不懂。


    进入工作后,不懂的也不好意思问同事和领导,怕被别人说菜,怀疑自己。


    其实真的没几个人有那个闲心去关注你的过往,你的家庭,相反越是伪装,越容易暴露。


    我很喜欢的一段话: 我穷,但是我正在拼命改变、我菜,但是我可以拼命学!


    面子的背后是自负,是错失,是沦陷。


    三.认知


    认知是一个人的天花板,它把人划分了层级。


    有一段话说得很好:你永远无法赚到认知之外的钱,你也无法了解认知之外的世界。


    我们多数小镇做题家的思维永远停留在老师和父母的教导:好好读书,将来就能考一个好工作,就能找一个铁饭碗。

    然后在职场中听老板的:好好工作,任劳任怨,以后给你升职加薪。


    然后多数人就深信不疑,就觉得人生的目标就是铁饭碗,其他的都不行,工作努力就一定能得到升职加薪,实际上这一切是在PUA自己。


    当被这个社会毒打后,才发现自己是那么无知,那么天真。


    而这一切的罪魁祸首就是自己认知不够,总觉得按部就班就一定能顺利过好一生。


    ————


    自卑,面子,认知这三点是我们多数小镇做题家难以跨过的三道坎。


    而这三道坎基本上都是原生家庭和教育造成的。


    跨过这三道坎的方法就是逃离和向上链接。


    施耐庵在几百年前就总结出的一句话:母弱出商贾,父强做侍郎,族望留原籍,家贫走他乡。


    显然我们大多数都是属于第四类,所以远走他乡是唯一的选择,只有脱离原生的环境,才能打开视野。


    事实也是如此,我们观察身边没有背景,没有经济条件的人,他们的谷底反弹一定是逃离。


    绝非留恋原地!


    作者:苏格拉的底牌
    来源:juejin.cn/post/7330295661784875043
    收起阅读 »

    压缩炸弹,Java怎么防止

    一、什么是压缩炸弹,会有什么危害 1.1 什么是压缩炸弹 压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不...
    继续阅读 »

    一、什么是压缩炸弹,会有什么危害


    1.1 什么是压缩炸弹


    压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。


    以下是安全测试几种经典的压缩炸弹


    graph LR
    A(安全测试的经典压缩炸弹)
    B(zip文件42KB)
    C(zip文件10MB)
    D(zip文件46MB)
    E(解压后5.5G)
    F(解压后281TB)
    G(解压后4.5PB)

    A ---> B --解压--> E
    A ---> C --解压--> F
    A ---> D --解压--> G

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


    压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。


    压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。


    压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。



    1.2 压缩炸弹会有什么危害


    graph LR
    A(压缩炸弹的危害)
    B(资源耗尽)
    C(磁盘空间耗尽)
    D(系统崩溃)
    E(拒绝服务攻击)
    F(数据丢失)

    A ---> B
    A ---> C
    A ---> D
    A ---> E
    A ---> F

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px

    压缩炸弹可能对计算机系统造成以下具体的破坏:



    1. 资源耗尽:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。

    2. 磁盘空间耗尽:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。

    3. 系统崩溃:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。

    4. 拒绝服务攻击:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。

    5. 数据丢失:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。



    重要提示:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。



    二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹


    2.1 个人有没有方法可以检测压缩炸弹?


    有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:


    graph LR
    A(个人检测压缩炸弹)
    B(安全软件和防病毒工具)
    C(文件大小限制)
    D(文件类型过滤)

    A ---> B --> E(推荐)
    A ---> C --> F(太大的放个心眼)
    A ---> D --> G(注意不认识的文件类型)

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


    1. 安全软件和防病毒工具(推荐):使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。

    2. 文件大小限制:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。

    3. 文件类型过滤:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。


    2.2 Java怎么防止压缩炸弹


    在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:


    graph LR
    A(Java防止压缩炸弹)
    B(解压缩算法的限制)
    C(设置解压缩操作的资源限制)
    D(使用安全的解压缩库)
    E(文件类型验证和过滤)
    F(异步解压缩操作)
    G(安全策略和权限控制)

    A ---> B
    A ---> C
    A ---> D
    A ---> E
    A ---> F
    A ---> G

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
    style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px


    1. 解压缩算法的限制:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。

    2. 设置解压缩操作的资源限制:使用Java的java.util.zipjava.util.jar等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。

    3. 使用安全的解压缩库:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。

    4. 文件类型验证和过滤:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。

    5. 异步解压缩操作:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。

    6. 安全策略和权限控制:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。


    2.2.1 使用解压算法的限制来实现防止压缩炸弹


    在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制来实现防止压缩炸弹。


    先来看看我们实现的思路


    graph TD
    A(开始) --> B[创建 ZipFile 对象]
    B --> C[打开要解压缩的 ZIP 文件]
    C --> D[初始化 zipFileSize 变量为 0]
    D --> E{是否有更多的条目}
    E -- 是 --> F[获取 ZIP 文件的下一个条目]
    F --> G[获取当前条目的未压缩大小]
    G --> H[将解压大小累加到 zipFileSize 变量]
    H --> I{zipFileSize 是否超过指定的大小}
    I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
    J --> K[抛出 IllegalArgumentException 异常]
    K --> L(结束)
    I -- 否 --> M(保存解压文件) --> E
    E -- 否 --> L

    style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
    style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
    style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
    style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
    style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
    style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
    style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

    实现流程说明如下:



    1. 首先,通过给定的 file 参数创建一个 ZipFile 对象,用于打开要解压缩的 ZIP 文件。

    2. zipFileSize 变量用于计算解压缩后的文件总大小。

    3. 使用 zipFile.entries() 方法获取 ZIP 文件中的所有条目,并通过 while 循环逐个处理每个条目。

    4. 对于每个条目,使用 entry.getSize() 获取条目的未压缩大小,并将其累加到 zipFileSize 变量中。

    5. 如果 zipFileSize 超过了给定的 size 参数,说明解压后的文件大小超过了限制,此时会调用 deleteDir() 方法删除已解压的文件夹,并抛出 IllegalArgumentException 异常,以防止压缩炸弹攻击。

    6. 创建一个 File 对象 unzipped,表示解压后的文件或目录在输出文件夹中的路径。

    7. 如果当前条目是一个目录,且 unzipped 不存在,则创建该目录。

    8. 如果当前条目不是一个目录,确保 unzipped 的父文件夹存在。

    9. 创建一个 FileOutputStream 对象 fos,用于将解压后的数据写入到 unzipped 文件中。

    10. 通过 zipFile.getInputStream(entry) 获取当前条目的输入流。

    11. 创建一个缓冲区 buffer,并使用循环从输入流中读取数据,并将其写入到 fos 中,直到读取完整个条目的数据。

    12. 最后,在 finally 块中关闭 fos 和 zipFile 对象,确保资源的释放。


    实现代码工具类


    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.InputStream;
    import java.util.Enumeration;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipFile;

    /**
    * 文件炸弹工具类
    *
    * @author bamboo panda
    * @version 1.0
    * @date 2023/10
    */

    public class FileBombUtil {

    /**
    * 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
    */

    public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;

    /**
    * 文件超限提示
    */

    public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";

    /**
    * 解压文件(带限制解压文件大小策略)
    *
    * @param file 压缩文件
    * @param outputfolder 解压后的文件目录
    * @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
    * @throws Exception IllegalArgumentException 超限抛出的异常
    * 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
    * 要考虑后面的逻辑,比如告警
    */

    public static void unzip(File file, File outputfolder, Long size) throws Exception {
    ZipFile zipFile = new ZipFile(file);
    FileOutputStream fos = null;
    try {
    Enumerationextends ZipEntry> zipEntries = zipFile.entries();
    long zipFileSize = 0L;
    ZipEntry entry;
    while (zipEntries.hasMoreElements()) {
    // 获取 ZIP 文件的下一个条目
    entry = zipEntries.nextElement();
    // 将解缩大小累加到 zipFileSize 变量
    zipFileSize += entry.getSize();
    // 判断解压文件累计大小是否超过指定的大小
    if (zipFileSize > size) {
    deleteDir(outputfolder);
    throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
    }
    File unzipped = new File(outputfolder, entry.getName());
    if (entry.isDirectory() && !unzipped.exists()) {
    unzipped.mkdirs();
    continue;
    } else if (!unzipped.getParentFile().exists()) {
    unzipped.getParentFile().mkdirs();
    }

    fos = new FileOutputStream(unzipped);
    InputStream in = zipFile.getInputStream(entry);

    byte[] buffer = new byte[4096];
    int count;
    while ((count = in.read(buffer, 0, buffer.length)) != -1) {
    fos.write(buffer, 0, count);
    }
    }
    } finally {
    if (null != fos) {
    fos.close();
    }
    if (null != zipFile) {
    zipFile.close();
    }
    }

    }

    /**
    * 递归删除目录文件
    *
    * @param dir 目录
    */

    private static boolean deleteDir(File dir) {
    if (dir.isDirectory()) {
    String[] children = dir.list();
    //递归删除目录中的子目录下
    for (int i = 0; i < children.length; i++) {
    boolean success = deleteDir(new File(dir, children[i]));
    if (!success) {
    return false;
    }
    }
    }
    // 目录此时为空,可以删除
    return dir.delete();
    }

    }

    测试类


    import java.io.File;

    /**
    * 文件炸弹测试类
    *
    * @author bamboo panda
    * @version 1.0
    * @date 2023/10
    */

    public class Test {

    public static void main(String[] args) {
    File bomb = new File("D:\temp\3\zbsm.zip");
    File tempFile = new File("D:\temp\3\4");
    try {
    FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
    } catch (IllegalArgumentException e) {
    if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
    FileBombUtil.deleteDir(tempFile);
    System.out.println("原始文件太大");
    } else {
    System.out.println("错误的压缩文件格式");
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    }

    三、总结


    文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
    合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。


    文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。


    总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。


    在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:




    1. 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。

    2. 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。

    3. 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。



    作者:独爱竹子的功夫熊猫
    来源:juejin.cn/post/7289667869557178404
    收起阅读 »

    突发奇想(吃饱了撑的) vue3能实现react中的hooks吗?

    web
    前言在前端领域,Vue 和 React 是两个备受欢迎的 JavaScript 框架,它们都为开发者提供了灵活而强大的工具。React 引入了 Hooks 的概念,使得函数组件可以拥有状态和其他 React 特性,而不再需要使用类组件。那么,对于那些突发奇想、...
    继续阅读 »

    前言

    在前端领域,Vue 和 React 是两个备受欢迎的 JavaScript 框架,它们都为开发者提供了灵活而强大的工具。React 引入了 Hooks 的概念,使得函数组件可以拥有状态和其他 React 特性,而不再需要使用类组件。那么,对于那些突发奇想、吃饱了撑的时候,我们是否可以在 Vue 3 中实现类似 React Hooks 的功能呢?

    首先,我们需要了解 React Hooks 的核心思想。React Hooks 允许函数组件拥有状态,副作用和其他 React 特性,而无需使用类组件。这是通过使用 useStateuseEffect 等特殊的函数来实现的。

    Vue 3 也引入了 Composition API,它在一定程度上类似于 React Hooks。Composition API 允许我们在函数组件中组织和重用逻辑。虽然它不是完全相同的实现,但能够达到类似的效果。

    useState

    React 中的 useState:

    useState 是 React 中的一个 Hook,用于在函数组件中引入状态。通过 useState,我们可以在函数组件中保存和更新状态,而不必使用类组件。

    基本语法如下:

    import React, { useState } from 'react';

    function ExampleComponent() {
    // 使用 useState 定义状态变量 count 和更新函数 setCount,并初始化为 0
    const [count, setCount] = useState(0);

    return (
    <div>
    <p>Count: {count}p>
    <button onClick={() => setCount(count + 1)}>
    加一
    button>
    div>
    );
    }

    下面是一个使用 vue3实现类似于 useState 的例子:

    import { ref, UnwrapRef } from "vue";

    type UpdateFunction = (nextState: UnwrapRef) => UnwrapRef;
    function isUpdateFc(
    nextState: UnwrapRef | UpdateFunction
    ): nextState is UpdateFunction {
    return typeof nextState === "function";
    }

    export default function useState(initialState: T) {
    const state = ref(initialState);
    const useState = (nextState: UnwrapRef | UpdateFunction) => {
    // 检测传入的是不是函数,如果是函数就把state传给函数,把函数执行返回值赋给重新state
    if (isUpdateFc(nextState)) {
    state.value = nextState(state.value);
    } else {
    state.value = nextState;
    }
    };
    return [state, useState] as const;
    }

    <template>
    <div>
    <button>{{ count }}button>
    <button @click="() => handerCount()">+button>
    div>
    template>

    useEffect

    React 中的 useEffect:

    useEffect 是 React 中的一个重要 Hook,用于处理副作用操作,比如数据获取、订阅、手动操作 DOM 等。它在函数组件渲染完成后执行,可以用于管理组件的生命周期。

    基本语法如下:

    import React, { useState, useEffect } from 'react';

    function ExampleComponent() {
    const [data, setData] = useState(null);

    useEffect(() => {
    // 在组件渲染完成后执行的副作用操作
    fetchData(); // 例如,发起数据请求
    }, []); // 第二个参数是依赖数组,为空数组表示只在组件挂载和卸载时执行

    return (
    <div>
    {/* 组件渲染的内容 */}
    div>
    );
    }

    在 Vue 3 中使用 onMounted, onUpdated, onUnmounted,watch 实现类似功能:

    在 Vue 3 中,可以使用一系列的生命周期钩子和 watch 函数来实现与 useEffect 类似的效果。

    1. onMounted: 在组件挂载后执行。
    2. onUpdated: 在组件更新后执行。
    3. onUnmounted: 在组件卸载前执行。
    4. watch: 监听特定数据的变化。

    下面是一个使用 vue3实现类似于 useEffect 的例子:

    import { ref, onMounted, watch, onUnmounted, onUpdated } from "vue";

    type EffectCleanup = void | (() => void);
    export default function useEffect(
    setup: () => EffectCleanup,
    dependencies?: readonly unknown[]
    ): void {
    const cleanupRef = ref<EffectCleanup | null>(null);
    const runEffect = () => {
    // 判断下一次执行副作用前还有没有清理函数没有执行
    if (cleanupRef.value) {
    cleanupRef.value();
    }
    // 执行副作用,并赋值清理函数
    cleanupRef.value = setup();
    };
    // 组件挂载的时候执行一次副作用
    onMounted(runEffect);
    // 判断有没有传依赖项,有的话就watch监听
    if (dependencies && dependencies.length > 0) {
    watch(dependencies, runEffect);
    } else if(dependencies === undefined) {
    // 没有传依赖项就组件每次渲染都要执行副作用
    onUpdated(runEffect)
    }
    // 组件销毁的使用如果有清理函数就执行清理函数
    onUnmounted(() => {
    if (cleanupRef.value) {
    cleanupRef.value();
    }
    });
    }

    useReducer

    React 中的 useReducer:

    useReducer 是 React 中的另一个 Hook,用于处理具有复杂状态逻辑的组件。它接受一个包含当前状态和触发状态更新的函数的 reducer,以及初始状态。通过 useReducer,我们可以更好地管理和处理复杂的状态变更逻辑。

    基本语法如下:

    import React, { useReducer } from 'react';

    // 定义 reducer 函数
    const reducer = (state, action) => {
    switch (action.type) {
    case 'increment':
    return { count: state.count + 1 };
    case 'decrement':
    return { count: state.count - 1 };
    default:
    return state;
    }
    };

    function ExampleComponent() {
    // 使用 useReducer,传入 reducer 函数和初始状态
    const [state, dispatch] = useReducer(reducer, { count: 0 });

    return (
    <div>
    <p>Count: {state.count}p>
    <button onClick={() => dispatch({ type: 'increment' })}>加一button>
    <button onClick={() => dispatch({ type: 'decrement' })}>减一button>
    div>
    );
    }

    通过刚刚实现的 useState 来实现类似 useReducer 的功能:

    import { UnwrapRef } from "vue";
    import useState from "./useState";

    type ReducerType = (state: T, action: A) => any;
    export default function useReducer(
    reducer: ReducerType<UnwrapRef, A>,
    initialArg: T,
    init?:
    (value: T) => T
    ) {
    // 根据传没传init函数来初始化state
    const [state, setState] = useState(init ? init(initialArg) : initialArg);
    const dispatch = (action: A) => {
    // 通过reducer函数的返回结果来修改state的值
    setState((state) => reducer(state, action));
    };
    return [state, dispatch] as const;
    }

    <template>
    <div>
    <div>
    <p>Count: {{ state.count }}p>
    <button @click="() => dispatch({ type: 'increment' })">
    加一
    button>
    <button @click="() => dispatch({ type: 'decrement' })">
    减一
    button>
    div>
    div>
    template>

    useCallback

    React 中的 useCallback:

    useCallback 是 React 中的一个 Hook,用于返回一个 memoized 版本的回调函数,避免在每次渲染时都创建新的回调函数。这在防止不必要的渲染和优化性能方面非常有用。

    基本语法如下:

    import React, { useState, useCallback } from 'react';

    function ExampleComponent() {
    const [count, setCount] = useState(0);

    // 使用 useCallback 返回 memoized 版本的回调函数
    const handleClick = useCallback(() => {
    setCount(count + 1);
    }, [count]); // 依赖数组中的值发生变化时,重新创建回调函数

    return (
    <div>
    <p>Count: {count}p>
    <button onClick={handleClick}>加一button>
    div>
    );
    }

    下面是一个使用 useState 和 vue3 中的 watch 模拟实现类似 useCallback 的例子:

    import { watch } from "vue";
    import useState from "./useState";

    type FnType = (...args: T[]) => any;
    export default function useCallback(fn: FnType, dependencies: D[]) {
    const [callback, setCallback] = useState(fn);
    // 如果依赖项有变更就把fn重新赋值没有就直接返回callback
    watch(
    dependencies,
    () => {
    setCallback((cb: FnType) => cb = fn);
    },
    { immediate: false }
    );
    return callback;
    }

    <template>
    <div>
    <button>{{ count }}button>
    <button @click="() => handerCount()">+button>
    div>
    template>

    useMemo

    React 中的 useMemo:

    useMemo 是 React 中的一个 Hook,用于记忆(memoize)计算结果,避免在每次渲染时都重新计算。它对于在渲染期间执行昂贵的计算并确保只在依赖项更改时重新计算结果非常有用。

    基本语法如下:

    import React, { useState, useMemo } from 'react';

    function ExampleComponent() {
    const [count, setCount] = useState(0);

    // 使用 useMemo 记忆计算结果
    const expensiveCalculation = useMemo(() => {
    console.log('计算了一次...');
    return count * 2;
    }, [count]); // 依赖数组中的值发生变化时,重新计算结果

    return (
    <div>
    <p>Count1: {count}p>
    <p>Count2: {expensiveCalculation}p>
    <button onClick={() => setCount(count + 1)}>加一button>
    div>
    );
    }

    下面是一个使用 useStateuseEffect 和 vue3 的 computed 模拟实现类似 useMemo 的例子:

    import { UnwrapRef, computed } from "vue";
    import useEffect from "./useEffect";
    import useState from "./useState";

    export default function useMemo(
    calculateValue: () => R,
    dependencies: T[]
    ) {
    const [cache, setCache] = useStatenull>(null);
    // 判断依赖项有没有变更,没有就直接返回缓存,有的话就重新计算
    useEffect(() => {
    setCache((cache) => {
    return (cache = computed(calculateValue) as UnwrapRef);
    });
    }, dependencies);
    return cache as UnwrapRef;
    }

    <template>
    <div>
    <div>平方: {{ squareSum }}div>
    <div>平方: {{ squareSum }}div>
    <button @click="handelNumbers">更改numbersbutton>
    div>
    template>

    useRef

    React 中的 useRef:

    useRef 是 React 中的一个 Hook,主要用于在函数组件中创建一个可变的对象,该对象的 current 属性被初始化为传入的参数。通常用于获取或存储组件中的引用(reference),并且不会触发组件重新渲染。

    基本语法如下:

    import React, { useRef, useEffect } from 'react';

    function ExampleComponent() {
    const myRef = useRef(null);

    useEffect(() => {
    // 使用 myRef.current 访问引用的 DOM 元素
    console.log(myRef.current);
    }, []);

    return <div ref={myRef}>获取DOMdiv>;
    }

    下面是一个使用 Vue 3 的 ref 模拟实现 useRef 的例子:

    import { ref, Ref } from "vue";

    function isHTMLElement(obj: unknown): obj is HTMLElement {
    return obj instanceof HTMLElement;
    }

    function useRefextends HTMLElement>(initialValue: T | null): Refnull>;
    function useRefextends unknown>(
    initialValue: T extends HTMLElement ? never : T
    ): { current: T };

    function useRef(
    initialValue: unknown
    ): Ref<HTMLElement | null> | { current: unknown } {
    // 判断传入的是不是一个HTML节点
    // 这里可能有点问题就是,或者传入null也会被判定为HTML节点,我没想到怎么解决这个问题
    if (isHTMLElement(initialValue) || initialValue === null) {
    return ref(initialValue);
    } else {
    // 不是就返回一个普通对象
    return {
    current: initialValue,
    };
    }
    }

    export default useRef;

    <template>
    <div>
    <input ref="myInputRef" type="text" />
    <p>Counter: {{ counterRef.current }}p>
    <button @click="incrementCounter">加一button>
    div>
    template>

    补充

    对于react中的createContext,useContext和vue3中的provide,inject很像。

    React 中的 createContext 和 useContext:

    1. createContext: 用于创建一个上下文对象,它包含一个 Provider 组件和一个 Consumer 组件。createContext 接受一个默认值,这个默认值在组件树中找不到对应的 Provider 时被使用。
    const MyContext = React.createContext(defaultValue);
    1. useContext: 用于在函数组件中订阅上下文的变化,获取当前 Provider 提供的值。
    const contextValue = useContext(MyContext);

    Vue3 中的 provide 和 inject:

    1. provide: 用于在父组件中提供数据,被提供的数据可以被子组件通过 inject 访问到。provide 接受一个对象,对象的属性即为提供的数据。

    1. inject: 用于在子组件中注入父组件提供的数据。可以是一个数组,也可以是一个对象,对象的属性为子组件中的变量名,值为从父组件中注入的数据。

    相似之处:

    • 目的相同: 无论是 React 中的上下文和钩子,还是 Vue 3 中的 provide 和 inject,它们都旨在实现组件之间的状态共享,提供一种在组件树中传递数据的方式。
    • 使用方式: 在使用上,它们都在父组件中提供数据,并在子组件中获取数据。
    • 避免了 props 层层传递: 这些机制都避免了将数据通过 props 层层传递的麻烦,特别在深层嵌套的组件树中,可以更方便地进行状态管理。

    总体而言,虽然具体的实现和语法有所不同,但这些机制在概念上非常相似,都是为了解决在组件树中共享数据的问题。

    总结

    本文源于作者的一时灵感,尝试探讨在 Vue 3 中是否能实现类似 React Hooks 的功能。虽然这个想法是出于好奇和娱乐,但在实际的开发中或许并没有太多实际用途。

    通过对比 React Hooks 和 Vue 3 Composition API,我们发现两者在语法和实现上存在一些差异,但本质上都为开发者提供了在函数组件中组织和重用逻辑的方式。这种灵活性是前端技术不断演进的体现,而每一种方式都有其适用的场景。

    在实际项目中,选择使用 React Hooks 还是 Vue3 Composition API 取决于团队和个人的偏好,以及项目的具体需求。技术的发展是不断前行的过程,而我们在其中的探索和实践都是宝贵的经验。

    愿读者在技术的海洋中,既能保持对新鲜事物的好奇心,又能在实际项目中选择合适的工具,取得更好的开发效果。无论是整活还是严肃的技术探讨,都让我们在编码的世界里保持一份热爱和乐趣。


    作者:辛克莱
    来源:juejin.cn/post/7328229830134972425
    收起阅读 »

    换个角度学TS,也许你能熟悉它

    web
    前言 TS绝非为了炫技的存在, 一切为了类型安全, 牢记这句话, 牢记这句话, 牢记这句话!!!感谢林不渡和光神大佬, 本文基本上都是从大佬们那里学来的。 一道开胃菜 function memoize

    前言


    TS绝非为了炫技的存在, 一切为了类型安全, 牢记这句话, 牢记这句话, 牢记这句话!!!感谢林不渡和光神大佬, 本文基本上都是从大佬们那里学来的。


    一道开胃菜


    function memoizeextends (...args: any[]) => any>(fn: T) {
    const cache = new Map()
    return (...args: Parameters<typeof fn>) => {
    const key = JSON.stringify(fn)
    if (cache.has(key)) {
    return cache.get(key)
    }
    const result = fn(...args)
    cache.set(key, result)
    return result
    }
    }

    const add = (a: number, b: number) => a + b
    const memoAdd = memoize(add)
    console.log(memoAdd(1, 2)) // 3
    console.log(memoAdd(1, 2)) // 3

    上面这个缓存函数的有个泛型T, 泛型T被约束为是一个可以是任意类型参数和返回任意类型的函数, 函数被调用时才明确知道T的类型, 比如下面的add函数, 这个时候TS类型就自动推导T是一个a、b参数类型为number返回一个number的函数, 我们看缓存函数内部返回一个函数接收的参数类型应该和T的参数类型一致, 这个时候可以使用TS内置的工具类型Parameters, 它可以返回一个函数类型的参数数组类型, 所以我们typeof fn, 就可以拿到fn的类型, 或者直接使用Parameters


    我们来看看Parameters是怎么实现的:


    type Parametersextends (...args: any) => any> =
    T extends (...args: infer P) => any ? P : never;

    Parameters工具类型接收一个泛型T被约束为可以是任意的函数类型, 后面用了infer类型推导, 可以在类型被使用时推导出具体的类型, T如果是(...args: infer P) => any的子类型, 就返回P, 否则返回never(表示永不可达)。


    不熟悉这种语法没关系, 我们把TS的内置类型都实现一遍,熟能生巧。


    TS内置类型工具


    Awaited


    // 基础用法
    type promise = Promise<string>
    type p = Awaited // string

    // 定义一个返回 Promise 的函数
    function fetchData(): Promise<string> {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve('成功啦啦啦');
    }, 1000);
    });
    }
    // 使用 Awaited 获取 Promise 结果的类型
    type ResultType = Awaited<ReturnType<typeof fetchData>>;

    const result: ResultType = 'Awaited'; // 此处类型会被推断为 string

    async function useResult() {
    const data = await fetchData();
    console.log(data); // 此处 data 的类型已经被推断为 string
    }
    useResult();

    这里的ReturnType和Parameters是配对的, 一个是获取函数类型的参数类型, 一个是获取函数类型的返回值类型, 我们看看ReturnType是怎么实现的


    type ReturnTypeextends (...args: any) => any> =
    T extends (...args: any) => infer R ? R : any;

    我们发现和上面Parameters的实现如出一辙, 只是把infer推断从参数的位置移动到了函数返回值的位置。不过ReturnType如果不满足条件返回的是any而不是never了, 这并没有什么影响。所以ReturnType拿到的类型是定义promise函数的返回类型Promise, 而我们的Awaited就是要拿到Promise里面的类型string


    这里有个思路


    type MyAwait = T extends Promise ? P : never
    type p = MyAwait<Promise<string>> // string

    利用infer推断Promise里面的类型, 好的, 恭喜你, 你已经了解TS了, 可是这并不全面, 如果Promise返回的还是一个Promise呢


    type MyAwait = T extends Promise ? P : never
    type p = MyAwait<Promise<Promise<string>>> // Promise

    递归?如果P还是一个Promise就递归调用MyAwait直到拿到最里面的类型, 没错就是你想的这样, 非常完美


    type MyAwait = T extends Promise // T如果是Promise的子类型
    ? P extends Promise<unknown> // 如果推断出来的P还是一个Promise
    ? MyAwait

    // 递归MyAwait


    : P // 不是Promise就直接返回P
    : T; // 如果泛型传的都不是一个promise直接返回T
    type p = MyAwait<Promise<Promise<string>>>; // string


    我们来看看TS内部是如何实现的


    type Awaited = T extends null | undefined
    ? T // 对于 `null | undefined` 这种特殊情况,当未启用 `--strictNullChecks` 时,直接返回 T
    : T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
    // 仅当类型 T 是一个对象并且具有可调用 `then` 方法时,才会进行下一步处理
    ? F extends (value: infer V, ...args: infer _) => any
    // 如果 `then` 方法的参数是可调用的,提取其第一个参数的类型
    ? Awaited // 递归地解开该值的嵌套异步类型
    : never // `then` 方法的参数不可调用
    : T; // 非对象或不具有 `then` 方法的类型

    Partial


    // 基础用法
    type obj = {
    a: 1,
    b: 2
    }
    type obj2 = Partial
    /**
    * type obj2 = {
    a?: 1 | undefined;
    b?: 2 | undefined;
    }
    */


    我们来实现实现,上面我们讲的是infer提取类型如果有深层的话就是extends配合三元递归,这个Partial就可以用映射成新类型?:表示的就是可选, 可选后除了原来的类型还联合了undefined类型, 这是为了类型安全考虑可选后就可能没有值。


    // 基础用法
    type obj = {
    a: 1,
    b: 2
    }
    type MyPartial = {
    [K in keyof T]?: T[K]
    }
    type obj2 = MyPartial
    /**
    * type obj2 = {
    a?: 1 | undefined;
    b?: 2 | undefined;
    }
    */


    原来的obj类型被构造成了一个新的类型obj2,还是一个对象, 对象里面的每一个键(K)是(in)对象里面所有的键(keyof T)?:(可选) T[K](T对象里面的K键的值),反复想想是不是这回事。


    如果有多个对象嵌套,就递归


    type obj = {
    a: 1,
    b: {
    c: 2
    }
    }
    type DeepPartial = {
    [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]
    }
    type obj2 = DeepPartial
    /**
    * type obj2 = {
    a?: 1 | undefined;
    b?: DeepPartial<{
    c: 2;
    }> | undefined;
    }
    */


    Required


    // 基础用法
    type obj = {
    a: 1,
    b: {
    c: 2
    }
    }
    type MyPartial = {
    [K in keyof T]?: T[K]
    }
    type obj2 = Required<MyPartial>
    /**
    *type obj2 = {
    a: 1;
    b: {
    c: 2;
    };
    }
    */


    Required就是把可选的变成必传的,非常简单,只需要把?去掉


    // 基础用法
    type obj = {
    a: 1,
    b: {
    c: 2
    }
    }
    type MyPartial = {
    [K in keyof T]?: T[K]
    }
    type MyRequired = {
    [K in keyof T]-?: T[K]
    }
    type obj2 = MyRequired<MyPartial>
    /**
    *type obj2 = {
    a: 1;
    b: {
    c: 2;
    };
    }
    */


    直接-?就可以了,神奇吧,那如果对象嵌套了,我想你会举一反三了吧,没错还是它递归


    type DeepRequired = {
    [K in keyof T]-?: T[K] extends object ? DeepRequired : T[K]
    }

    Readonly


    type obj = {
    a: 1,
    b: {
    c: 2
    }
    }
    type obj2 = Readonly
    /**
    * type obj2 = {
    readonly a: 1;
    readonly b: {
    c: 2;
    };
    }
    */


    Readonly就是把对象里面的值变成只读的,看这个obj2的类型,我想你知道怎么做了吧


    type MyReadonly = {
    readonly [K in keyof T]: T[K]
    }

    type DeepReadonly = {
    readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]
    }

    Record


    type obj = Record<string, any>
    /**
    * type obj = {
    [x: string]: any;
    }
    */


    其实根据上面学的,你已经会实现它了


    type MyRecordextends keyof any, T> = {
    [P in K]: T
    }

    type obj = MyRecord<string, any>
    /**
    * type obj = {
    [x: string]: any;
    }
    */


    K extends keyof any: 这里使用了泛型约束,确保 K 是任何类型的键集合的子集。keyof any 表示任何类型的所有键的联合。[P in K]: T: 这是一个映射类型。它表示对于 K 中的每个键 P,都创建一个属性,其值类型是 T


    Pick


    type MyPickextends object, K extends keyof T> = {
    [P in K]: T[K]
    }

    type obj = MyPick<{a: 1, b: 2}, 'a'>
    /***
    * type obj = {
    a: 1;
    }
    */


    Omit


    type MyOmitextends object, K extends keyof T> =
    PickExclude>

    type obj = MyOmit<{ a: 1, b: 2 }, 'a'>
    /***
    * type obj = {
    b: 2;
    }
    */



    • Exclude: 使用 keyof T 获取对象 T 的所有键,然后使用 Exclude 排除其中与 K 相同的键。这样得到的是 T 中除去 K 对应键的键集合。

    • Pick: 使用 Pick 从对象 T 中选择具有特定键集合的属性。在这里,我们选择了 T 中除去 K 对应键的其他所有属性。


    我们来看看Exclude的实现


    Exclude


    type MyExclude = T extends U ? never : T
    type T0 = MyExclude<"a" | "b" | "c", "a">;
    // type T0 = "b" | "c"

    如果T中存在U就剔除(never)否则保留


    Extract


    很明显就是Exclude的反向操作


    type MyExtract = T extends U ? T : never
    type T0 = MyExtract<"a" | "b" | "c", "a">;
    // type T0 = "a"

    NonNullable


    type T0 = NonNullable<string | number | undefined>;
    type T1 = NonNullable<string[] | null | undefined>;
    type NonNullable = T & {};

    T0 的类型是 string | number。这是因为 NonNullable 类型别名被定义为 T & {},这意味着它接受一个类型 T,并返回一个新的类型,该新类型是 T 和空对象类型的交叉类型。由于 TypeScript 中的交叉类型(T & {})会过滤掉 nullundefined,因此 T0 的类型就是排除了 undefinedstring | number


    也可以这样实现


    type MyNonNullable = T extends null | undefined ? never : T;

    ConstructorParameters


    type MyConstructorParametersextends abstract new (...args: any) => any> =
    T extends abstract new (...args: infer P) => any ? P : never;
    class C {
    constructor(a: number, b: string) {}
    }
    type T3 = MyConstructorParameters<typeof C>;
    // type T3 = [a: number, b: string]

    还是老套路infer推断,这个和我们上面那个获取函数参数类型差不多的,换了个形式。



    • T extends abstract new (...args: any) => any: 这是对输入类型 T 进行约束,要求它是一个抽象类的构造函数类型。

    • T extends abstract new (...args: infer P) => any ? P : never: 这是一个条件类型,检查 T 是否符合指定的构造函数模式。如果符合,就返回构造函数的参数类型 P,否则返回 never


    InstanceType


    class C {
    x = 0;
    y = 0;
    }
    type MyInstanceTypeextends abstract new (...args: any) => any> =
    T extends abstract new (...args: any) => infer R ? R : never;
    type T0 = MyInstanceType<typeof C>;
    // type T0 = C

    和我们上面那个实现的获取函数类型返回值类型如出一辙和获取类的构造器类型是配对的。



    • T extends abstract new (...args: any) => any: 这是对输入类型 T 进行约束,要求它是一个抽象类的构造函数类型。

    • T extends abstract new (...args: any) => infer R ? R : any: 这是一个条件类型,检查 T 是否符合指定的构造函数模式。如果符合,就返回构造函数的实例类型 R,否则返回 never


    ThisParameterType


    function toHex(this: Number) {
    return this.toString(16);
    }
    function numberToString(n: ThisParameterType<typeof toHex>) {
    return toHex.apply(n);
    }

    像这种提取类型的都用infer,先猜一猜,内部肯定是判断T是不是一个函数, 第一个参数this infer R是就返回R不是就返回never。
    我们看看答案


    type ThisParameterType =
    T extends (this: infer U, ...args: never) => any
    ? U
    : unknown;

    和我们猜想的差不多,我想你现在应该可以类型编程了吧。


    TS内部还有四个内置类型是通过JS来实现的,我们就不研究了


    `Uppercase`
    `Lowercase`
    `Capitalize`
    `Uncapitalize`

    可以看看我的这篇文章vue里面对于TS的使用 # 突发奇想(吃饱了撑的) vue3能实现react中的hooks吗?


    祝大家TS的编程技术越来越好吧,本人非常菜,大佬轻喷。


    作者:辛克莱
    来源:juejin.cn/post/7332435905926070322

    JS 不写分号踩了坑,但也可以不踩坑

    web
    前言 “所有直觉性的 “当然应该加分号” 都是保守的、未经深入思考的草率结论。” —— 尤雨溪。 重新认识分号在代码中的作用,为什么要写分号?又为什么可以没有分号? 踩的坑 写一个方法将秒数转为“xx天xx时xx分xx秒”的形式 const ONEDAYSEC...
    继续阅读 »

    前言


    “所有直觉性的 “当然应该加分号” 都是保守的、未经深入思考的草率结论。” —— 尤雨溪。

    重新认识分号在代码中的作用,为什么要写分号?又为什么可以没有分号?


    踩的坑


    写一个方法将秒数转为“xx天xx时xx分xx秒”的形式


    const ONEDAYSECOND = 24 * 60 * 60
    const ONEHOURSECOND = 60 * 60
    const ONEMINUTESECOND = 60

    function getQuotientandRemainder(dividend,divisor){
    const remainder = dividend % divisor
    const quotient = (dividend - remainder) / divisor
    return [quotient,remainder]
    }

    function formatSeconds(time){
    let restTime,day,hour,minute
    restTime = time
    [day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)
    [hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)
    [minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)
    return day + '天' + hour + '时' + minute + '分' + restTime + '秒'
    }
    console.log(formatSeconds(time)) // undefined天undefined时undefined分NaN,NaN秒

    按照这段代码执行完后,day、hour、minute这些变量得到的都是 undefined,而 restTime 则好像得到一个数组。

    问题就在于 13、14、15、16 行之间没有添加分号,导致解析时,没有将这三行解析成三条语句,而是解析成一条语句。最终的表达式就是这样的:


    restTime = time[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)

    那执行的过程相当于给 restTime 进行赋值,表达式从左往右执行,最终表达式的值为右值。最右边的值就是 getQuotientandRemainder(restTime,ONEMINUTESECOND),由于在计算过程中 restTime 还没有被赋值,一直是 undefined,所以经过 getQuotientandRemainder 计算后得到的数组对象每个成员都是 NaN,最终赋值给 restTime 就是这样一个数组。


    分号什么时候会“自动”出现


    有时候好像不写分号也不会出问题,比如这种情况:


    let a,b,c
    a = 1
    b = 2
    c = 3
    console.log(a,b,c) // 1 2 3

    这是因为,JS 进行代码解析的时候,能够识别出语句的结束位置并“自动添加分号“,从而能够解析出“正确”的抽象语法树,最终执行的结果也就是我们所期待的。

    JS 有一个语法特性叫做 ASI (Automatic Semicolon Insertion),就是上面说到的”自动添加分号”的东西,它有一定的插入规则,在满足时会为代码自动添加分号进行断句,在我们不写分号的时候,需要了解这个规则,才能不踩坑。(当然这里说的加分号并不是真正的加分号,只是一种解析规则,用分号来代表语句间的界限)


    ASI 规则


    JS 只有在出现换行符的时候才会考虑是否添加分号,并且会尽量“少”添加分号,也就是尽量将多行语句合成一行,仅在必要时添加分号。


    1. 行与行之间合并不符合语法时,插入分号


    比如上面那个自动添加分号的例子,就是合并多行时会出现语法错误。

    a = 1b = 2 这里 1b 是不合法的,因此会加入分号使其合法,变为 a = 1; b = 2


    2. 在规定[no LineTerminator here]处,插入分号


    这种情况很有针对性,针对一些特定的关键字,如 return continue break throw async yield,规定在这些关键字后不能有换行符,如果在这些关键字后有了换行符,JS 会自动在这些关键字后加上分号。
    看下面这个例子🌰:


    function a(){
    return
    123
    }
    console.log(a()) // undefined

    function b(){
    return 123
    }
    console.log(b()) // 123

    在函数a中,return 后直接换行了,那么 return 和 123 就会被分成两条语句,所以其实 123 根本不会被执行到,而 return 也是啥也没返回。


    3. ++、--这类运算符,若在一行开头,则在行首插入分号


    ++ 和 -- 既可以在变量前,也可以在变量后,如果它们在行首,当多行进行合并时,会产生歧义,到底是上一行变量的运算,还是下一行变量的运算,因此需要加入分号,处理为下一行变量的运算。


    a
    ++
    b
    // 添加分号后
    a
    ++b

    如果你的预期是:


    a++ 
    b

    那么就会踩坑了。


    4. 在文件末尾发现语法无法构成合法语句时,会插入分号


    这条和 1 有些类似


    不写分号时需要注意⚠️


    上面的 ASI 规则中,JS 都是为了正确运行代码,必须按照这些规则来分析代码。而它不会做多余的事,并且在遵循“尽量合并多行语句”的原则下,它会将没有语法问题的多行语句都合并起来。这可能违背了你的逻辑,你想让每行独立执行,而不是合成一句。开头贴出的例子,就是这样踩坑的,我并不想一次次连续的对数组进行取值🌚。

    因此我们要写出明确的语句,可以被合并的语句,明确是多条语句时需要加上分号。


    (如果你的项目中使用了某些规范,它不想让你用分号,别担心,它只是不想让你在行尾用分号,格式化时它会帮你把分号移到行首)像这样:


    // before lint
    restTime = time;
    [day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND);
    [hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND);
    [minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND);

    // after lint
    restTime = time
    ;[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND)
    ;[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND)
    ;[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND)

    参考



    作者:用户9787521254131
    来源:juejin.cn/post/7269645636210458635
    收起阅读 »

    基于 localStorage 实现有过期时间的存储方式

    web
    我们知道 localStorage 中的数据是长期保存的,除非手动删除,否则他会一直存在。如果我们想实现一种数据有过期时间的存储方式,该怎么实现呢? 首先应该想到的是 cookie,cookie 本身就有有效期的配置,当某个 cookie 时,浏览器自动清理该...
    继续阅读 »

    我们知道 localStorage 中的数据是长期保存的,除非手动删除,否则他会一直存在。如果我们想实现一种数据有过期时间的存储方式,该怎么实现呢?


    首先应该想到的是 cookie,cookie 本身就有有效期的配置,当某个 cookie 时,浏览器自动清理该 cookie。可是使用 cookie 存储数据,有个不好的地方,很多我们存储的数据,本就是我们前端自己用到的,后端根本用不到。可是存储到 cookie 中后,页面中所有的 cookie 都会随着请求发送给后端,造成传输的 cookie 比较长,而且没有必要。


    低调低调


    因此,我们可以基于 localStorage 来实现一套这样的有过期时间的存储方式。我们在之前的文章 如何重写 localStorage 中的方法 中,也了解了一些重写 localStorage 的方法。这里我们是自己在外层封装一层的方式,来调用 localStorage。


    我这里封装的类名叫: LocalExpiredStorage,即有过期时间的 localStorage。


    1. 实现与 localStorage 基本一致的 api


    我们为了实现跟 localStorage 使用上的一致性体验,这里我们自己的 api 名称和实现方式跟 localStorage 基本一致。


    interface SetItemOptions {
    maxAge?: number; // 从当前时间往后多长时间过期
    expired?: number; // 过期的准确时间点,优先级比maxAge高
    }

    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    constructor(prefix?: string) {
    if (prefix) {
    this.prefix = prefix;
    }
    }

    setItem(key: string, value: any, options?: SetItemOptions) {}
    getItem(key: string): any {}
    removeItem(key: string) {}
    clearAllExpired() {}
    }
    const localExpiredStorage = new LocalExpiredStorage();
    export default localExpiredStorage;

    可以看到我们实现的类里,有三个变化:



    1. setItem()方法新增了一个 options 参数,这里主要是为了配置过期时间,这里有两种配置方式,一种是可以设置多长时间后过期,比如 2 个小时后过期(开发者不用特殊计算 2 个小时后的时间节点);再一种是设置过期的时间节点,该值可以是格式化的时间,也可以是时间戳;

    2. 有一个 prefix 属性,在具体实现中,我们会将 prefix 属性与操作的 key 进行拼接,标识该 key 是具有过期时间特性的,方便我们自己的类进行处理;

    3. 新增了一个 clearAllExpired() 方法,这是为了清理所有已经过期的 key,避免占用缓存;该方法在应用的入口处就应当调用,便于及时清理;


    上面是我们的大致框架,接下来我们来具体实现下这些方法。


    干饭


    2. 具体实现


    接下来我们来一一实现这些方法。


    2.1 setItem


    这里我们新增了一个 options 参数,用来配置过期时间:



    • expired: 固定的过期时间点,比如点击关闭按钮,当天不再展示,那过期时间就是今天晚上的 23:59:59,可以使用该属性;

    • maxAge: 从当前时间起,设置多长时间后过期;比如点击某个提示,3 天内不再展示,使用该属性就比较方便;


    假如两个属性都设置了,我这里约定 expired 属性的优先级更高一些。


    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    constructor(prefix?: string) {
    if (prefix) {
    this.prefix = prefix;
    }
    }

    setItem(key: string, value: any, options?: SetItemOptions) {
    const now = Date.now();
    let expired = now + 1000 * 60 * 60 * 3; // 默认过期时间为3个小时

    // 这里我们限定了 expired 和 maxAge 都是 number 类型,
    // 您也可以扩展支持到 string 类型或者如 { d:2, h:3 } 这种格式
    if (options?.expired) {
    expired = options?.expired;
    } else if (options?.maxAge) {
    expired = now + options.maxAge;
    }

    // 我们这里用了 dayjs 对时间戳进行格式化,方便快速识别
    // 若没这个需要,也可以直接存储时间戳,减少第三方类库的依赖
    localStorage.setItem(
    `${this.prefix}${key}`,
    JSON.stringify({
    value,
    start: dayjs().format("YYYY/MM/DD hh:mm:ss"), // 存储的起始时间
    expired: dayjs(expired).format("YYYY/MM/DD hh:mm:ss"), // 存储的过期时间
    })
    );
    }
    }

    我们在过期时间的实现过程中,目前只支持了 number 类型,即需要传入一个时间戳,参与运算。您也可以扩展到 string 类型(比如'2024/11/23 14:45:34')或者其他格式{ d:2, h:3 } 这种格式。


    设置好过期时间后,我们将 value,存储的起始时间和过期时间,转义成 json string 存储起来。我们这里用了 dayjs 对时间戳进行格式化,方便开发者可以快速地识别。若没有这个需要,也可以直接存储时间戳,减少第三方类库的依赖。


    该方法并没有支持永久存储的设定,若您需要永久存储,可以直接使用 localStorage 来存储。


    2.2 getItem


    获取某 key 存储的值,主要是对过期时间的判断。


    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    constructor(prefix?: string) {
    if (prefix) {
    this.prefix = prefix;
    }
    }

    getItem(key: string): any {
    const result = localStorage.getItem(`${this.prefix}${key}`);
    if (!result) {
    // 若key本就不存在,直接返回null
    return result;
    }
    const { value, expired } = JSON.parse(result);
    if (Date.now() <= dayjs(expired).valueOf()) {
    // 还没过期,返回存储的值
    return value;
    }
    // 已过期,删除该key,然后返回null
    this.removeItem(key);
    return null;
    }
    removeItem(key: string) {
    localStorage.removeItem(`${this.prefix}${key}`);
    }
    }

    在获取 key 时,主要经过 3 个过程:



    1. 若本身就没存储这个 key,直接返回 null;

    2. 已存储了该 key 的数据,解析出数据和过期时间,若还在有效期,则返回存储大数据;

    3. 若已过期,则删除该 key,然后返回 null;


    这里我们在删除数据时,使用了this.removeItem(),即自己实现的删除方法。本来我们也是要实现这个方法的,那就直接使用了吧。


    2.3 clearAllExpired


    localStorage 中的数据并不会自动清理,我们需要一个方法用来手动批量清理已过期的数据。


    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    clearAllExpired() {
    let num = 0;

    // 判断 key 是否过期,然后删除
    const delExpiredKey = (key: string, value: string | null) => {
    if (value) {
    // 若value有值,则判断是否过期
    const { expired } = JSON.parse(value);
    if (Date.now() > dayjs(expired).valueOf()) {
    // 已过期
    localStorage.removeItem(key);
    return 1;
    }
    } else {
    // 若 value 无值,则直接删除
    localStorage.removeItem(key);
    return 1;
    }
    return 0;
    };

    const { length } = window.localStorage;
    const now = Date.now();

    for (let i = 0; i < length; i++) {
    const key = window.localStorage.key(i);

    if (key?.startsWith(this.prefix)) {
    // 只处理我们自己的类创建的key
    const value = window.localStorage.getItem(key);
    num += delExpiredKey(key, value);
    }
    }
    return num;
    }
    }

    在项目的入口处添加上该方法,用户每次进入项目时,都会自动清理一次已过期的 key。


    醒一醒


    3. 完整的代码


    上面我们是分步讲解的,这里我们放下完整的代码。同时,我也在 GitHub 上放了一份:wenzi0github/local-expired-storage


    interface SetItemOptions {
    maxAge?: number; // 从当前时间往后多长时间过期
    expired?: number; // 过期的准确时间点,优先级比maxAge高
    }

    class LocalExpiredStorage {
    private prefix = "local-"; // 用于跟没有过期时间的key进行区分

    constructor(prefix?: string) {
    if (prefix) {
    this.prefix = prefix;
    }
    }

    // 设置数据
    setItem(key: string, value: any, options?: SetItemOptions) {
    const now = Date.now();
    let expired = now + 1000 * 60 * 60 * 3; // 默认过期时间为3个小时

    // 这里我们限定了 expired 和 maxAge 都是 number 类型,
    // 您也可以扩展支持到 string 类型或者如 { d:2, h:3 } 这种格式
    if (options?.expired) {
    expired = options?.expired;
    } else if (options?.maxAge) {
    expired = now + options.maxAge;
    }

    // 我们这里用了 dayjs 对时间戳进行格式化,方便快速识别
    // 若没这个需要,也可以直接存储时间戳,减少第三方类库的依赖
    localStorage.setItem(
    `${this.prefix}${key}`,
    JSON.stringify({
    value,
    start: dayjs().format("YYYY/MM/DD hh:mm:ss"), // 存储的起始时间
    expired: dayjs(expired).format("YYYY/MM/DD hh:mm:ss"), // 存储的过期时间
    })
    );
    }

    getItem(key: string): any {
    const result = localStorage.getItem(`${this.prefix}${key}`);
    if (!result) {
    // 若key本就不存在,直接返回null
    return result;
    }
    const { value, expired } = JSON.parse(result);
    if (Date.now() <= dayjs(expired).valueOf()) {
    // 还没过期,返回存储的值
    return value;
    }
    // 已过期,删除该key,然后返回null
    this.removeItem(key);
    return null;
    }

    // 删除key
    removeItem(key: string) {
    localStorage.removeItem(`${this.prefix}${key}`);
    }

    // 清除所有过期的key
    clearAllExpired() {
    let num = 0;

    // 判断 key 是否过期,然后删除
    const delExpiredKey = (key: string, value: string | null) => {
    if (value) {
    // 若value有值,则判断是否过期
    const { expired } = JSON.parse(value);
    if (Date.now() > dayjs(expired).valueOf()) {
    // 已过期
    localStorage.removeItem(key);
    return 1;
    }
    } else {
    // 若 value 无值,则直接删除
    localStorage.removeItem(key);
    return 1;
    }
    return 0;
    };

    const { length } = window.localStorage;
    const now = Date.now();

    for (let i = 0; i < length; i++) {
    const key = window.localStorage.key(i);

    if (key?.startsWith(this.prefix)) {
    // 只处理我们自己的类创建的key
    const value = window.localStorage.getItem(key);
    num += delExpiredKey(key, value);
    }
    }
    return num;
    }
    }
    const localExpiredStorage = new LocalExpiredStorage();
    export default localExpiredStorage;

    使用:


    localExpiredStorage.setItem("key", "value", { maxAge: 5000 }); // 有效期为5000毫秒
    localExpiredStorage.setItem("key", "value", {
    expired: Date.now() + 1000 * 60 * 60 * 12,
    }); // 有效期为 12 个小时,自己计算到期的时间戳

    // 获取数据
    localExpiredStorage.getItem("key");

    // 删除数据
    localExpiredStorage.removeItem("key");

    // 清理所有过期的key
    localExpiredStorage.clearAllExpired();

    4. 总结


    这个功能本身不难,也有很多开发者自己实现过。这里我也是总结下之前实现的过程。



    作者:小蚊酱
    来源:juejin.cn/post/7215775714417655867
    收起阅读 »

    url请求参数带有特殊字符“%、#、&”时,参数被截断怎么办?

    web
    是的,最近又踩坑了! 事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。 一排查,发现特殊字符“%%%”并未成功传给后端。 我们的这个全局搜索功能是需要跳转页面才能查看到搜索结...
    继续阅读 »

    是的,最近又踩坑了!


    事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。


    一排查,发现特殊字符“%%%”并未成功传给后端。


    我们的这个全局搜索功能是需要跳转页面才能查看到搜索结果的。所以,搜索条件是作为参数拼接在页面url上的。


    正常的传参:


    image.png


    当输入的是特殊字符“%、#、&”时,参数丢失


    image.png


    也就是说,当路由请求参数带有浏览器url中的特殊含义字符时,参数会被截断,无法正常获取参数。


    那么怎么解决这个问题呢?


    方案一:encodeURIComponent/decodeURIComponent


    拼接参数时,利用encodeURIComponent()进行编码,接收参数时,利用decodeURIComponent()进行解码。


    // 编码
    this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(searchValue)}`});

    // 解码
    const text = decodeURIComponent(this.$route.query.text)

    此方法对绝大多数特殊字符都适用,但是唯独输入“%”进行搜索时不行,报错如下。


    image.png


    所以在编码之前,还需进行一下如下转换:



    this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(encodeSpecialChar(searchValue))}`});


    /**
    * @param {*} char 字符串
    * @returns
    */

    export const encodeSpecialChar = (char) => {
    // #、&可以不用参与处理
    const encodeArr = [{
    code: '%',
    encode: '%25'
    },{
    code: '#',
    encode: '%23'
    }, {
    code: '&',
    encode: '%26'
    },]
    return char.replace(/[%?#&=]/g, ($) => {
    for (const k of encodeArr) {
    if (k.code === $) {
    return k.encode
    }
    }
    })
    }


    方案二: qs.stringify()


    默认情况下,qs.stringify()方法会使用encodeURIComponent方法对特殊字符进行编码,以保证URL的合法性。


    const qs = require('qs');

    const searchObj = {
    type: selectValue,
    text: searchValue
    };
    this.$router.push({path: `/crm/global-search/search-result?${qs.stringify(searchObj)}`});


    使用了qs.stringify()方法,就无需使用encodeSpecialChar方法进行转换了。


    作者:HED
    来源:juejin.cn/post/7332048519156776979
    收起阅读 »

    别再只用axios了,试试这个更轻量的网络请求库!

    web
    嗨,我们又见面了!今天我要和大家分享一个前端工程师都会遇到的问题:如何优化移动端网络请求。相信很多人都用过 axios 或 fetch API,但你是否觉得处理分页逻辑、缓存、请求防抖等很麻烦呢?别担心,我找到了一个解决方案——Alova.js。 Alova...
    继续阅读 »

    嗨,我们又见面了!今天我要和大家分享一个前端工程师都会遇到的问题:如何优化移动端网络请求。相信很多人都用过 axios 或 fetch API,但你是否觉得处理分页逻辑、缓存、请求防抖等很麻烦呢?别担心,我找到了一个解决方案——Alova.js。



    Alova.js 是一个轻量级的请求策略库,它可以帮助我们简化网络请求的编写,让我们更专注于业务逻辑。它提供了多种服务端数据缓存模式,比如内存模式和持久化模式,这些都能提升用户体验,同时降低服务端的压力。而且,Alova.js 只有 4kb+,体积是 axios 的 30%,非常适合移动端使用。


    Alova.js 的基础请求功能非常简单,比如你可以这样请求数据:


    const todoDetail = alova.Get('/todo', { params: { id: 1 } });
    const { loading, data, error } = useRequest(todoDetail);

    它还提供了分页请求、表单提交、验证码发送、文件上传等多种请求策略,大大减少了我们的工作量。比如,使用分页请求策略,你只需要这样:


    const {
    loading,
    data,
    isLastPage,
    page,
    pageSize,
    pageCount,
    total,
    } = usePagination((page, pageSize) => queryStudents(page, pageSize));

    怎么样,是不是很简单?Alova.js 还支持 Vue、React、React Native、Svelte 等多种前端框架,以及 Next、Nuxt、SvelteKit 等服务端渲染框架,非常适合现代前端开发。


    感兴趣的话,可以去 Alova.js 的官网看看:Alova.js 官网。也可以在评论区分享你对 Alova.js 的看法哦!嘿嘿,今天就聊到这里,下次见!👋
    有任何问题,你可以加入以下群聊咨询,也可以在github 仓库中发布 Discussions,如果遇到问题,也请在github 的 issues中提交,我们会在最快的时间解决。


    作者:古韵
    来源:juejin.cn/post/7332388389944819748
    收起阅读 »

    记录一次类似页面抽出经历

    web
    一、背景 刚入职了一家新公司,摸鱼式的把每个项目都打开看了下。不够五分钟便惊出了一身冷汗,午饭时分,便和领导聊了下这些项目中的通病。其中就有一个问题就是代码复用的问题,一个Vue文件6000多行代码,多个文件中还有很多重复的代码。领导听了休息了片刻便拍案而起,...
    继续阅读 »

    一、背景


    刚入职了一家新公司,摸鱼式的把每个项目都打开看了下。不够五分钟便惊出了一身冷汗,午饭时分,便和领导聊了下这些项目中的通病。其中就有一个问题就是代码复用的问题,一个Vue文件6000多行代码,多个文件中还有很多重复的代码。领导听了休息了片刻便拍案而起,好的小陈你就把这几个类似的页面抽出来吧!我:。。。。默默扒饭。


    二、问题和方案


    类似登录页这种几百年不变的页面,多个项目不管是逻辑还是UI基本上都是一样的,多个项目要用。虽然CV也挺快,但是如果逻辑一改的话,yi那其实还是挺麻烦的。(领导视角)


    方案一:Iframe嵌入主项目❌


    一开始是想把要引入的页面打包然后通过Iframe引入,但是这样的话会存在域不同的问题,而无法随心所欲的操作本地存储之类的东西。虽然可以用postMessage的方法进行通信传输数据,但是要传输到主页面的信息一多的话,很难分清楚哪个数据是所需要的。在尝试了半天之后,PASS了这个方案。


    方案二:将页面打包成组件,然后在主项目中注册且使用✔


    通过采用lib库模式打包Vue页面为组件,然后在主项目中引入,便可以实现页面的复用。然后引入组件也可以自由的访问本地存储等东西。
    打包命令:


    vue-cli-service build --target lib --name main --dest lib src/components/index.js

    详情可以参考官网的指南
    构建目标 | Vue CLI (vuejs.org)


    接下来便是痛苦且折磨的试错之路😖


    初步实现



    1. 要引入页面的项目结构(components中的About和Main中的文件即为要打包的文件)
      image.png

    2. 配置库打包的文件

      简单说明下这两个文件的作用:

      Main文件夹下面的index.js作用:包含Main的Vue文件注册成全局组件的方法;

      components文件夹下index.js作用:暴露出一个方法可以批量注册components下的组件。

      image.png


    接下来看下这两个文件的具体内容

    Main下面的index.js
    image.png


    components下面的index.js
    image.png


    看了下这两个文件的内容,写过Vue插件的铁铁们应该都很熟悉,对其实就是把页面当成组件了。有的铁铁举手问,小陈那个initRouter是啥呀,小陈后面为铁铁们解答,我们一步一步慢慢实现。

    Main页面如下
    image.png


    接下来便是通过命令行打包成组件的步骤了
    image.png


    现在打包的项目这边的任务就告一段落,后面我们看下主项目要如何引用这个被打包的组件。
    image.png
    只需要在主项目的main中注册我们打包的组件就可以使用了,然后结构出来的Main和About正是刚才我们在components下index.js暴露的两个组件,componentPage则是components下index.js暴露的默认的install方法用于注册。启动下主项目试试就发现我们引入的两个组件都加到主项目路由里面去了。心头一甜但是隐约觉得事情没这么简单。😱


    image.png
    image.png


    三、遇到的问题及解决策略


    问题、组件需要使用主项目的路由


    以登录页为例子,在用户验证完身份之后,需要跳转到主项目中的其他页面。例如跳转home需要跳转到主项目的home页面,在主项目中点击会报错,因为组件路由根本没有这个路由配置,所以需要把主项目的路由引入到组件中,那要怎么做捏?容小陈慢慢解释。


    image.png


    解决:在注册的时候引入主项目的路由


    通过initRouter在注册组件的时候,把主项目的路由引入到组件中,然后在需要使用主项目路由的时候,使用getCurRouter给组件路由赋值成主项目的路由即可。只不过在使用router的js文件都需要使用getCurRouter。Vue文件中则不需要做任何配置,因为this.router/this.route访问的均是主项目的路由。

    组件的路由文件配置
    image.png
    组件下components的index.js配置
    image.png
    Main中跳转的方法
    image.png
    顺便一提:判断生产还是开发环境都是为了开发的时候,不用做额外的配置,只是方法比较笨。如果大佬们有更好的方法麻烦踢一下小陈。


    总结


    这是小陈第一次在掘金上写文章,可能这篇文章的作用不是很大,但也是记录小陈解决问题的载体。文章有啥不清楚的或者不合理的地方还麻烦铁铁们和小陈促膝长谈。但是此次的实践还是让小陈对Vue的一些知识这块有了新的理解。然后日后还请大佬们多多指教。

    Demo的地址: only-for-test: 仅用来测试的仓库 (gitee.com)


    作者:用户1863710796985
    来源:juejin.cn/post/7250667613020291109
    收起阅读 »

    曹贼,莫要动‘我’网站 —— MutationObserver

    web
    前言 本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。 正文 话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子 这么好看的看的小乔,谁看谁不糊,更何...
    继续阅读 »

    前言


    本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。


    正文


    话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子


    image.png
    这么好看的看的小乔,谁看谁不糊,更何况曹老板。这天,曹操在浏览网页的时候,无意间发现了周瑜的这个网站,看着美若天仙的小乔,曹操的眼泪止不住的从嘴角流了下来。赶紧将网站上的照片保存了下来。

    这个消息最后传到了周瑜的耳朵里,他只是想展示小乔,可不是为了让别人下载的。于是在自己的网站上做了一些预防措施。

    为了防止他人直接在网站上直接下载图片,周瑜将右键的默认事件给关闭了,并且为了防止有人打开控制台,并对图片保存,采取了以下方法:


    禁用右键和F12键


    //给整个document添加右击事件,并阻止默认行为
    document.addEventListener("contextmenu", function (e) {
    e.preventDefault();
    return false;
    });

    //给整个页面禁用f12按键 keyCode即将被禁用 不再推荐使用 但仍可以使用
    document.addEventListener("keydown", function (e) {
    //当点了f3\f6\f10之后,即使禁用了f12键依旧可以打开控制台,所以一并禁用
    if (
    [115, 118, 121, 123].includes(e.keyCode) ||
    ["F3", "F6", "F10", "F12"].includes(e.key) ||
    ["F3", "F6", "F10", "F12"].includes(e.code) ||
    //ctrl+f 效果和f3效果一样 点开搜索之后依旧可以点击f12 打开控制台 所以一并禁用
    //缺点是此网站不再能够 **全局搜索**
    (e.ctrlKey && (e.key == "f" || e.code == "KeyF" || e.keyCode == 70))||
    //禁用专门用于打开控制台的组合键
    (e.ctrlKey && e.shiftKey && (e.key == "i" || e.code == "KeyI" || e.keyCode == 73))
    ) {
    e.preventDefault();
    return false;
    }
    });

    当曹操再次想保存小乔照片的时候,发现使用网页的另存了已经没用了。这能难倒曹老板吗,破解方法,在浏览器的右上角进行操作就可打开控制台,这个地方是浏览器自带的,没办法禁用


    image.png
    这番操作之后,曹操可以选择元素保存那个图片了。周瑜的得知了自己的禁用措施被破解后,赶忙连夜加班打补丁,于是又加了一些操作,禁止打开控制台后进行操作


    禁用控制台


    如何判定控制台被打开了,可以使用窗口大小来判定


    function resize() {
    var threshold = 100;
    //窗口的外部减窗口内超过100就判定窗口被打开了
    var widthThreshold = window.outerWidth - window.innerWidth > threshold;
    var heightThreshold = window.outerHeight - window.innerHeight > threshold;
    if (widthThreshold || heightThreshold) {
    console.log("控制台打开了");
    }
    }
    window.addEventListener("resize", resize);

    但是也容易被破解,只要让控制台变成弹窗窗口就可以了


    也可以使用定时器进行无限debugger,因为只有在控制台打开的时候debugger才会生效。关闭控制台的时候,并不会影响功能。当前网页内存占用比较大的时候,定时器的占用并不明显。在当前网页占用比较小的时候,一直开着定时器才会有较为明显的提升


      setInterval(() => {
    (function () {})["constructor"]("debugger")();
    }, 500);

    破解方法一样有,在debugger的位置右键禁用调试就可以了。这样控制台就可以正常操作了


    image.png
    既然有方法破解,就还要做一层措施,既然是要保存图片,那就把img转成canvas,这样即使打开控制台也没办法进行对图片的保存


    //获取dom
    const img = document.querySelector(".img");
    const canvas = document.querySelector("#canvas");
    //img转成canvas
    canvas.width = img.width;
    canvas.height = img.height;
    ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0, img.width, img.height);
    document.body.removeChild(img);

    经过一夜的努力,该加的措施都加上了。周瑜心想这下就没办法保存我的小乔了吧。

    来到曹操这边,再次打开周瑜的小破站,还想故技重施时,发现已经有了各种显示,最后也没难倒曹操,那些阻碍也都被破解了。但是到保存图片的时候傻眼了,竟然已经不是图片格式了,那就没办法下载了呀。但是小乔真的很养神,曹操心有不甘,于是使用了最后一招,既然没办法下载那就截图,虽然有损画质,但是依旧能看。


    得知如此情况的大都督周瑜不淡定了,从未见过如此厚颜无耻之人,竟然使用截图。


    006APoFYly1g2qcclw1frg308w06ox2t.gif
    话说魔高一尺,道高一丈,周瑜再次熬夜加班进行对网站的优化。于是使用了全屏水印+MutationObserver监听水印dom的方法。即使截图也让他看着不舒服。


    MutationObserver


    MutationObserver是一个构造函数,接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

    它接收一个回调函数,每当监听的dom发生改变时,就会调用这个函数,函数传入一个参数,数组包对象的格式,里面记录着dom的变化以及dom的信息。


    image.png
    返回的实例是一个新的、包含监听 DOM 变化回调函数的 MutationObserver 对象。有三个方法observedisconnecttakeRecords



    • observe接收两个参数,第一个为要监听的dom元素,第二个则是一些配置对象,当调用 observe() 时,childListattributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。配置对象如下:

      • subtree:当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false

      • childList:当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false

      • attributes:当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false

      • attributeFilter:一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。

      • attributeOldValue:当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false

      • characterDate:当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false

      • characterDateOldValue:当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false



    • disconnect方法用来停止观察(当被观察dom节点被删除后,会自动停止对该dom的观察),不接受任何参数

    • takeRecords:方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。


    该构造函数监听的dom即使在控制台中被更改属性或值,也会被监听到。


    使用MutationObserver对水印dom进行监听,并限制更改。


    <style>
    //定义水印的样式
    #watermark {
    width: 100vw;
    height: 100vh;
    position: absolute;
    left: 0;
    top: 0;
    font-size: 34px;
    color: #32323238;
    font-weight: 700;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-evenly;
    align-content: space-evenly;
    z-index: 9999999;
    }
    #watermark span {
    transform: rotate(45deg);
    }
    </style>

    <script>
    //获取水印dom
    const watermark = document.querySelector("#watermark");
    //克隆水印dom ,用作后备,永远不要改变
    const _watermark = watermark.cloneNode(true);
    //获取水印dom的父节点
    const d = watermark.parentNode;
    //获取水印dom的后一个节点
    let referenceNode;
    [...d.children].forEach((item, index) => {
    if (item == watermark) referenceNode = d.children[index + 1];
    });
    //定义MutationObserver实例observe方法的配置对象
    const prop = {
    childList: true,//针对整个子树
    attributes: true,//属性变化
    characterData: true,//监听节点上字符变化
    subtree: true,//监听以target为根节点的整个dom树
    };
    //定义MutationObserver
    const observer = new MutationObserver(function (mutations) {
    //在这里每次坚挺的dom发生改变时 都会运行,传入的参数为数组对象格式
    mutations.forEach((item) => {
    //这里可以只针对监听dom的样式来判断
    if (item.attributeName === "style") {
    //获取父节点的所有子节点,因为时伪数组,使用扩展运算符转以下
    [...d.children].forEach((v) => {
    //判断一下,是父节点里的那个节点被改变了,并且删除那个被改变的节点(也就是删除水印节点)
    if (item.target.id && v == document.querySelector(`#${item.target.id}`)) {
    v.remove();
    }
    });
    //原水印节点被删除了,这里使用克隆的水印节点,再次克隆
    const __watermark = _watermark.cloneNode(true);
    //这里的this指向是MutationObserver的实例对象,所以同样可以使用observe监听dom
    //监听第二次克隆的dom
    this.observe(__watermark, prop);
    //因为水印dom被删除了,再将克隆的水印dom添加到原来的位置 就是referenceNode节点的前面
    d.insertBefore(__watermark, referenceNode);
    }
    });
    });
    在初始化的时候监听初始化的水印dom
    observer.observe(watermark, prop);
    </script>



    这样,每当对水印dom进行更改样式的时候,就会删除该节点,并重新添加一个初始的水印dom,即使突破重重困难打开开控制台,用户也是无法对dom 进行操作。


    视频转Gif_爱给网_aigei_com.gif


    隔天曹操再次打开网页,发现网页上的水印,心里不足为惧,心想区区水印能难倒自己?操作到最后却发现,不论如何对水印dom进行操作,都无法改变样式。虽说只是为了保存图片,但是截图有着这样水印,任谁也不舒服呀。曹操大怒,刚吃了两口的饭啪的一下就盖在了桌子上......


    20230508094549_33500.gif
    然而曹操不知道的是,在控制台中,获取dom节点右键是可以只下载获取的那个节点的......


    image.png


    结尾


    文章主要是以鬼畜恶搞的方式讲述了,如何禁止用户打开控制台(还有重写toSring,consloe.log等一些方法,但我并没有没有实现,所以这里并没有写上),并且如何使用MutationObserver构造函数来监听页面中的dom元素。其实大多情况下并没有这方面的项目需求,完全可以当扩展知识看了。


    写的不好的地方可以提出意见,虚心请教!


    作者:iceCode
    来源:juejin.cn/post/7290862554657423396
    收起阅读 »

    前端实现 word 转 png

    web
    在此之前 word 转图片的需求都是在后端实现的,用的是 itext 库,但是 itext 收费的,商用需要付费。近期公司内部安全部门发出侵权警告,要求整改。 所以采用前端实现 word 文档转图片功能。 一、需求 用户在页面上上传 .docx 格式的文件...
    继续阅读 »

    在此之前 word 转图片的需求都是在后端实现的,用的是 itext 库,但是 itext 收费的,商用需要付费。近期公司内部安全部门发出侵权警告,要求整改。


    所以采用前端实现 word 文档转图片功能。



    一、需求



    1. 用户在页面上上传 .docx 格式的文件

    2. 前端拿到文件,解析并生成 .png 图片

    3. 上传该图片到文件服务器,并将图片地址作为缩略图字段


    二、难点


    目前来看,前端暂时无法直接实现将 .docx 文档转成图片格式的需求


    三、解决方案


    既然直接转无法实现,那就采用迂回战术



    1. 先转成 html(用到库 docx-preview

    2. 再将 html 转成 canvas(用到库 html2canvas

    3. 最后将 canvas 转成 png


    四、实现步骤




    1. .docx 文件先转成 html 格式,并插入到目标节点中


      安装 docx-preview 依赖: pnpm add docx-preview --save




    jsx
    复制代码
    import { useEffect } from 'react';
    import * as docx from 'docx-preview';

    export default ({ file }) => {
    useEffect(() => {
    // file 为上传好的 docx 格式文件
    docx2Html(file);
    }, [file]);

    /**
    * @description: docx 文件转 html
    * @param {*} file: docx 格式文件
    * @return {*}
    */
    const docx2Html = file => {
    if (!file) {
    return;
    }
    // 只处理 docx 文件
    const suffix = file.name?.substr(file.name.lastIndexOf('.') + 1).toLowerCase();
    if (suffix !== 'docx') {
    return;
    }
    // 生成 html 后挂载的 dom 节点
    const htmlContentDom = document.querySelector('#htmlContent');
    const docxOptions = Object.assign(docx.defaultOptions, {
    debug: true,
    experimental: true,
    });
    docx.renderAsync(file, htmlContentDom, null, docxOptions).then(() => {
    console.log('docx 转 html 完成');
    });
    };

    return <div id='htmlContent' />;
    };

    此时,在 idhtmlContent 的节点下,就可以看到转换后的 html 内容了( htmlContent 节点的宽高等 css 样式自行添加)




    1. html 转成 canvas


      安装 html2canvas 依赖: pnpm add html2canvas --save




    jsx
    复制代码
    import html2canvas from 'html2canvas';

    /**
    * @description: dom 元素转为图片
    * @return {*}
    */
    const handleDom2Img = async () => {
    // 生成 html 后挂载的 dom 节点
    const htmlContentDom = document.querySelector('#htmlContent');
    // 获取刚刚生成的 dom 元素
    const htmlContent = htmlContentDom.querySelectorAll('.docx-wrapper>section')[0];
    // 创建 canvas 元素
    const canvasDom = document.createElement('canvas');
    // 获取 dom 宽高
    const w = parseInt(window.getComputedStyle(htmlContent).width, 10);
    // const h = parseInt(window.getComputedStyle(htmlContent).height, 10);

    // 设定 canvas 元素属性宽高为 DOM 节点宽高 * 像素比
    const scale = window.devicePixelRatio; // 缩放比例
    canvasDom.width = w * scale; // 取文档宽度
    canvasDom.height = w * scale; // 缩略图是正方形,所以高度跟宽度保持一致

    // 按比例增加分辨率,将绘制内容放大对应比例
    const canvas = await html2canvas(htmlContent, {
    canvas: canvasDom,
    scale,
    useCORS: true,
    });
    return canvas;
    };


    1. 将生成好的 canvas对象转成 .png 文件,并下载


    jsx
    复制代码
    // 将 canvas 转为 base64 图片
    const base64Str = canvas.toDataURL();

    // 下载图片
    const imgName = `图片_${new Date().valueOf()}`;
    const aElement = document.createElement('a');
    aElement.href = base64Str;
    aElement.download = `${imgName}.png`;
    document.body.appendChild(aElement);
    aElement.click();
    document.body.removeChild(aElement);
    window.URL.revokeObjectURL(base64Str);

    五、总结


    前端无法直接实现将 .docx 文档转成图片格式,所以要先将 .docx 文档转换成 html 格式,并插入页面文档节点中,然后根据 html 内容生成canvas对象,最后将 canvas对象转成 .png 文件


    有以下两个缺点:



    1. 只能转 .docx 格式的 word 文档,暂不支持 .doc 格式;

    2. 无法自动获取文档第一页来生成图片内容,需要先将 word 所有页面生成为 html,再通过 canvas 手动裁切,来确定图片宽高。

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

    低成本创建数字孪生场景-开发篇

    web
    介绍 本文承接《低成本创建数字孪生场景-数据篇》,获取到了数据之后就是愉快的开发环节,我们用到了开源项目CesiumJS做为GIS技术框架。 CesiumJS项目从创建至今已经有12年历史,提供了创建3D地球和2D地图的强大功能,它支持多种地图数据源,可以创建...
    继续阅读 »

    介绍


    本文承接《低成本创建数字孪生场景-数据篇》,获取到了数据之后就是愉快的开发环节,我们用到了开源项目CesiumJS做为GIS技术框架。


    CesiumJS项目从创建至今已经有12年历史,提供了创建3D地球和2D地图的强大功能,它支持多种地图数据源,可以创建复杂的3D地形和城市模型。CesiumJS的功能强大,但入门难度比较高,需要提前了解很多概念和设计理念,为方便理解本案例仅仅使用其提供的一些基础功能。


    Guanlianx_5.gif


    需求说明


    为了搭建1个简易的山区小乡镇场景,我们首先梳理下需求,把任务分解好。



    1. 在底图上叠加各种图层

      • 支持叠加地形图层、3DTiles图层、数据图层

      • 支持多种方式分发图层数据



    2. 鼠标与图层元素的交互

      • 鼠标移动时,使用屏幕事件处理器监听事件,获取当前屏幕坐标

      • 如果已经有高亮的元素,将其恢复为正常状态

      • 以当前屏幕坐标为起点发送射线,获取射线命中的元素,如果有命中的元素就高亮它

      • 鼠标点击时使用屏幕事件处理器获取命中元素,如果命中了,就判断元素是否描边状态,有则取消描边,没有则增加描边轮廓



    3. 加载Gltf等其他模型

      • 模型与其他图层元素一样,可以被光标拾取

      • 模型支持播放自带动画




    准备工作


    数据分发服务


    当前案例涉及的图层数据以文件类为主,为方便多处使用,需要将图层的服务独立部署,这里有两个方案:



    1. 自行搭建静态文件服务器,网上搜一个常用的node.js静态服务脚本即可

    2. 把文件放到cesium ion上,如果你要使用cesium ion资产,需要注意配好defaultAccessToken,具体调用方式看下文的代码实现


    安装依赖


    以下为本案例的前端工程使用的核心框架版本


    依赖版本
    vue^3.2.37
    vite^2.9.14
    Cesium^1.112.0

    代码实现



    1. 地图基本场景,本示例使用vite+vue3开发,html非常简单,只需要一个

      标签即可,在cesiumjs中以Viewer为起点调用其他控件,因此我们实例化一个Cesium.viewer, 这里面有非常多配置参数,详细请看开发文档


      import * as Cesium from 'cesium'
      import 'cesium/Build/Cesium/Widgets/widgets.css'

      Cesium.Ion.defaultAccessToken = '可以把一些GIS资产放到Cesium ION上托管,Tokenw为调用凭证'

      // 地图中心
      const center = [1150, 29]

      // cesium实例
      let viewer = null

      // 容器
      const cesiumContainer = ref(null)

      onMounted(async () => {
      await init()
      })

      async function init() {
      viewer = new Cesium.Viewer(cesiumContainer.value, {
      timeline: true, //显示时间轴
      animation: true, //开启动画
      sceneModePicker: true, //场景内容可点击
      baseLayerPicker: true, //图层可点击
      infoBox: false, // 自动信息弹窗
      shouldAnimate: true // 允许播放动画
      })
      // 初始化镜头视角
      restoreCameraView()

      // 开启地形深度检测
      viewer.scene.globe.depthTestAgainstTerrain = true
      // 开启全局光照
      viewer.scene.globe.enableLighting = true
      // 开启阴影
      viewer.shadows = true

      })

      // 设置初始镜头
      function restoreCameraView(){
      viewer.camera.flyTo({
      destination: Cesium.Cartesian3.fromDegrees(center[0], center[1], 0),
      orientation: {
      heading: Cesium.Math.toRadians(0), // 相机的方向
      pitch: Cesium.Math.toRadians(-90), // 相机的俯仰角度
      roll: 0 // 相机的滚动角度
      }
      })
      }

      // 加载地形图层
      async function initTerrainLayer() {
      const tileset = await Cesium.CesiumTerrainProvider.fromUrl(
      'http://localhost:9003/terrain/c8Wcm59W/',
      {
      requestWaterMask: true,
      requestVertexNormals: false
      }
      )
      viewer.terrainProvider = tileset
      }


    2. 在地图上叠加地形图层,图层数据可以自行部署


      // 方法1: 加载本地地形图层
      async function initTerrainLayer() {
      const tileset = await Cesium.CesiumTerrainProvider.fromUrl(
      'http://localhost:9003/terrain/c8Wcm59W/',
      {
      requestWaterMask: true,
      requestVertexNormals: false
      }
      )
      viewer.terrainProvider = tileset
      }

      // 方法2: 加载Ion地形图层
      async function initTerrainLayer() {
      const tileset = await Cesium.CesiumTerrainProvider.fromIonAssetId(1,{
      requestVertexNormals: true
      }
      )
      viewer.terrainProvider = tileset
      }


    3. 加载3DTiles图层,与地形图层类似,换成了Cesium3DTileset类。需要注意使用url加载需要自行解决跨域问题


      const tileset = await Cesium.Cesium3DTileset.fromUrl(
      'http://localhost:9003/model/tHuVnsJXZ/tileset.json',
      {}
      )
      // 将图层加入到场景
      viewer.scene.primitives.add(tileset)

      // 适当调整图层位置
      const translation = getTransformMatrix(tileset, { x: 0, y: 0, z: 86 })
      tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation)

      // 获取变化矩阵
      function getTransformMatrix (tileset, { x, y, z }) {
      // 高度偏差,正数为向上偏,负数为向下偏,根据真实的模型位置不断进行调整
      const heightOffset = z
      // 计算tileset的绑定范围
      const boundingSphere = tileset.boundingSphere
      // 计算中心点位置
      const cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center)
      // 计算中心点位置坐标
      const surface = Cesium.Cartesian3.fromRadians(cartographic.longitude,
      cartographic.latitude, 0)
      // 偏移后的三维坐标
      const offset = Cesium.Cartesian3.fromRadians(cartographic.longitude + x,
      cartographic.latitude + y, heightOffset)

      return Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3())
      }


    4. 鼠标事件交互,鼠标悬浮,在改变选中元素的状态之前,需要将它的当前状态保存下来以便下次可以恢复。


      // 缓存高亮状态
      const highlighted = {
      feature: undefined,
      originalColor: new Cesium.Color()
      }

      // 鼠标与物体交互事件
      function initMouseInteract () {
      // 事件处理器
      const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

      // 鼠标悬浮选中
      handler.setInputAction((event) => {
      // 将原有高亮对象恢复
      if (Cesium.defined(highlighted.feature)) {
      highlighted.feature.color = highlighted.originalColor
      highlighted.feature = undefined
      }
      // 获取选中对象
      const pickedFeature = viewer.scene.pick(event.endPosition)

      if (Cesium.defined(pickedFeature)) {
      // 高亮选中对象
      if (pickedFeature !== moveSelected.feature) {
      highlighted.feature = pickedFeature
      Cesium.Color.clone(pickedFeature.color, highlighted.originalColor)
      pickedFeature.color = Cesium.Color.YELLOW
      }
      }
      }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)


    5. 鼠标事件,鼠标点击,描边轮廓使用了Cesium自带的后期效果处理器,不需要自行编写着色器等操作,因此实现起来很便捷。只需要将选中的元素放到 效果的selected对象数组内就行了。


      // 缓存后期效果
      let edgeEffect = null

      function initMouseInteract(){
      // 鼠标点击选中
      handler.setInputAction((event) => {

      // 获取选中对象
      const pickedFeature = viewer.scene.pick(event.position)

      if (!Cesium.defined(pickedFeature)) {
      return null
      } else {

      // 描边效果:兼容GLTF和3DTiles
      setEdgeEffect(pickedFeature.primitive || pickedFeature)

      // 如果拾取的要素包含属性信息,则打印出来
      if (Cesium.defined(pickedFeature.getPropertyIds)) {
      const propertyNames = pickedFeature.getPropertyIds()
      const props = propertyNames.map(key => {
      return {
      name: key,
      value: pickedFeature.getProperty(key)
      }
      })
      console.info(props)
      }
      }
      }, Cesium.ScreenSpaceEventType.LEFT_CLICK)
      }

      // 选中描边
      function setEdgeEffect (feature) {
      if (edgeEffect == null) {
      // 后期效果
      const postProcessStages = viewer.scene.postProcessStages

      // 增加轮廓线
      const stage = Cesium.PostProcessStageLibrary.createEdgeDetectionStage()
      stage.uniforms.color = Cesium.Color.LIME //描边颜色
      stage.uniforms.length = 0.05 // 产生描边的阀值
      stage.selected = [] // 用于放置对元素

      // 将描边效果放到场景后期效果中
      const silhouette = Cesium.PostProcessStageLibrary.createSilhouetteStage([stage])
      postProcessStages.add(silhouette)

      edgeEffect = stage
      }

      // 选多个元素进行描边
      const matchIndex = edgeEffect.selected.findIndex(v => v._batchId === feature._batchId)
      if (matchIndex > -1) {
      edgeEffect.selected.splice(matchIndex, 1)
      } else {
      edgeEffect.selected.push(feature)
      }

      }


    6. 加载gltf模型, gltf加载后需要进行一次矩阵变换modelMatrix, 加载后启动指定索引的动画进行播放。


      // 加载模型
      async function loadGLTF () {

      let animations = null

      let modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(
      Cesium.Cartesian3.fromDegrees(lng,lat,altitude)
      )

      const model = await Cesium.Model.fromGltfAsync({
      url: './static/gltf/windmill.glb',
      modelMatrix: modelMatrix,
      scale: 30,
      // minimumPixelSize: 128, // 设定模型最小显示尺寸
      gltfCallback: (gltf) => {
      animations = gltf.animations
      }
      })

      model.readyEvent.addEventListener(() => {
      const ani = model.activeAnimations.add({
      index: animations.length - 1, // 播放第几个动画
      loop: Cesium.ModelAnimationLoop.REPEAT, //循环播放
      multiplier: 1.0 //播放速度
      })
      ani.start.addEventListener(function (model, animation) {
      console.log(`动画开始: ${animation.name}`)
      })
      })

      viewer.scene.primitives.add(model)
      }



    部署说明



    1. 场景演示包括前端工程、GIS数据分发服务、服务端接口几个部分

    2. 前端工程使用vue3开发,其中CesiumJs通过NPM依赖包引入

    3. 场景中相关图层均为静态文件,可放入主工程静态目录中,也可以独立部署(需解决跨域访问),或者使用cesiumlab3分发服务便于管理

    4. web端场景对终端设备和浏览器有一定要求,具体配置需要进一步测试


    总结


    在本文中并没有涉及到服务端数据的接入,数据接入进来后,我们可以利用Cesium在GIS开发领域强大功能,与three.js的webGL开发优势,两者相互融合创建更多数据可视化效果。那么关于Cesium和three.js的融合开发还在初步探索阶段,希望下一次有精彩内容分享给大家。


    Hengjiang3.gif


    相关链接


    最新版cesium集成threejs


    Cesium和Three.js结合的5个方案


    Cesium实现更实用的3D描边效果


    作者:gyratesky
    来源:juejin.cn/post/7331626882552872986
    收起阅读 »

    前端将dom转换成图片

    web
    一、问题描述 在工作的过程中会遇到要将dom变成图片并下载的问题,刚开始使用的html2canvas进行转换的,但是我老大告诉我说,咱们做的是面向国外的网页,插件太大会导致页面加载慢的问题(国外部分地区网络没有国内这么发达),就让我用原生dom或一些比较小的插...
    继续阅读 »

    一、问题描述


    在工作的过程中会遇到要将dom变成图片并下载的问题,刚开始使用的html2canvas进行转换的,但是我老大告诉我说,咱们做的是面向国外的网页,插件太大会导致页面加载慢的问题(国外部分地区网络没有国内这么发达),就让我用原生dom或一些比较小的插件,在原生dom下载的时候遇到了context.drawImage(element, 0, 0, width, height)这一方法传入参数要传类型HTMLCanvasElement的问题,所以要将一个HTMLElement转换成HTMLCanvasElement,但是经过一些信息的查找,我发现有个很好用且轻量化的插件,可以完美解决这一问题,所以这里给大家推荐一个轻量级的插件dom-to-image(23kb),这个插件可以不用进行类型转换,直接将dom元素转换成需要的文件格式。


    二、dom-to-image的使用


    2.1 dom-to-image的安装


    在终端输入以下代码进行dom-to-image安装



    npm install dom-to-image



    2.2 dom-to-image引入


    2.2.1 vue项目引入


    在需要使用这个插件的页面使用以下代码进行局部引入


    import domToImage from 'dom-to-image';

    然后就可以通过以下代码进行图片的转换了


    const palGradientGap = document.getElementById('element')
    const canvas = document.createElement('canvas')
    canvas.width = element.offsetWidth
    canvas.height = element.offsetHeight
    this.domtoimage.toPng(element).then(function (canvas) {
    const link = document.createElement('a')
    link.href = canvas
    link.download = 'image.png' // 下载文件的名称
    link.click()
    })

    当然也可以进行全局引入
    创建一个domToImage.js文件写入以下代码


    import Vue from 'vue'; 
    import domToImage from 'dom-to-image';
    const domToImagePlugin = {
    install(Vue) {
    Vue.prototype.$domToImage = domToImage;
    }
    };
    Vue.use(domToImagePlugin);

    然后再入口文件main.js写入以下代码全局引入插件


    import Vue from 'vue'
    import App from './App.vue'
    import './domToImage.js'; // 引入全局插件
    Vue.config.productionTip = false
    new Vue({ render: h => h(App), }).$mount('#app')

    三、dom-to-image相关方法



    1. toSvg(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 SVG 图片,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    2. toPng(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 PNG 图片,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    3. toJpeg(node: Node, options?: Options): Promise<string>:将 DOM 元素转换为 JPEG 图片,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    4. toBlob(node: Node, options?: Options): Promise<Blob>:将 DOM 元素转换为 Blob 对象,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    5. toPixelData(node: Node, options?: Options): Promise<Uint8ClampedArray>:将 DOM 元素转换为像素数据,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。



    6. toCanvas(node: Node, options?: Options): Promise<HTMLCanvasElement>:将 DOM 元素转换为 Canvas 对象,并返回一个 Promise 对象。


      参数说明:



      • node:要转换为图片的 DOM 元素。

      • options:可选参数对象,用于配置转换选项。




    其中,Options 参数是一个可选的配置对象,用于设置转换选项。以下是一些常用的选项:



    • width:输出图像的宽度,默认值为元素的实际宽度。

    • height:输出图像的高度,默认值为元素的实际高度。

    • style:要应用于元素的样式对象。

    • filter:要应用于元素的 CSS 滤镜。

    • bgcolor:输出图像的背景颜色,默认值为透明。

    • quality:输出图像的质量,仅适用于 JPEG 格式,默认值为 0.92。


    作者:crazy三笠
    来源:juejin.cn/post/7331626882553937946
    收起阅读 »

    JS 前端框架的新年预言

    web
    免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 2024 Predictions by JavaScript Frontend Framework Maintainers。 本期共享的是 —— 来自 React/Next...
    继续阅读 »

    免责声明


    本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 2024 Predictions by JavaScript Frontend Framework Maintainers



    本期共享的是 —— 来自 React/Next.js/Angular/Solid 的维护者和创建者科普了它们计划在新年里框架改进的未来规划。


    fe-2024.png


    React:新年预览


    Meta(前脸书)的 React 工程经理 E.W. 表示,React 团队预计在新的一年里会有更多的框架采用 RSC(React 服务服务端组件)。


    “对于大多数人而言,RSC 已经对其所了解的 React 作用域产生了重大变化,从只是一个 UI 层,到对构建 App 的方式产生更大的影响,以享受最佳的用户体验和开发体验,尤其以前对于 SPA(单页应用程序)还不够好,”E.W. 如是说。


    虽然它没有具体爆料来年的任何新进展,但 E.W. 确实表示它们会发布并共享某些去年开始可公开的进展。举个栗子,在 React Advanced 上,该团队向与会者展示了 React Forget,这是 React 的自动记忆编译器。E.W. 表示,React Forget 意味着,开发者不再需要使用 useMemo/useCallback


    “在 React Native EU 上,我们表示,从 0.73 版本开始,我们会把 Web 开发者熟悉的 Chrome 开发工具移植到 React Native 中,”E.W. 补充道。“我们还共享了我们对 Static Hermes 的研究,它是我们的 JS 原生编译器,它不仅有可能加快 React Native App 的速度,还能从根本上改变 JS 的有效用途。”


    Next.js:正在运行的新编译器


    Next.js 推出了一款新的 App 服务器,旨在支持去年的 RSC(React 服务端组件)和 SA(服务端操作)。Vercel 的产品主管 L.R. 表示,它会继续支持旧版的 App 服务器,并且它们的路由系统是可互换的。这种互操作性意味着,开发者可以把时间花在添加新功能上。


    “有些客户已经使用 Next.js 开发了五六年,它们采用这些新功能也需要多年时间,”L.R. 讲道。“我们希望让大家尽可能顺利地度过这段旅程。”


    新的一年里,Next.js 想要解决一大坨问题,但其中一个优先事项可能是简化缓存。它说,就开发体验而言,这可能会更容易。


    “通常情况下,生态系统中的一大坨开发者必须引入一大坨额外软件包,或学习如何使用其他工具来请求、缓存和重新验证,”L.R. 说。“Next.js 现在已经内置了一大坨十分给力的同款功能,但这也意味着,大家需要学习其他东西,目前用户初步的反馈是,‘这很棒棒哒;它十分给力,但如果能更简单一点的话,我们会不吝赞词。’”


    Next.js 团队还会继续关注性能优化,它称之为“我们的持续投资”。


    它补充道,这可能会在新年里以新编译器的形式出现,这将加快在开发者的机器上启动 Next.js 的速度。该编译器已经投入使用了大约一年,Vercel 一直在内部将其用于其产品和 App。它说,由 Rust 提供支持的编译器,在无缓存的情况下比以前有缓存的编译器更快。


    L.R. 说:“我们推出该功能指日可待,大家都可以默认启动它,而且它比现存的 Webpack 编译解决方案更快。” “开发者希望它们的工具更快。它们永远不会抱怨它变得更快。因此,很有趣的是,可以看到工具作者,而不是工具的用户,而是实际的工具开发者转向 Rust 等较低阶的工具,帮助斩获咫尺之间的性能优势。”


    目标三是继续为 Next.js 的未来 10 年奠定基础。


    “你知道的,这个新的路由系统显然让我们十分鸡冻。我们相信这是未来的基础,”它说。“但这也需要时间。大家会尝试,用户会提出功能请求,它们会希望看到事情发生改变。我们认为这是未来五到十年的一项非常长期的投资。”


    它补充说,“有一天”但可能不是今年的目标是,寻求一种更棒的方案来处理 Next.js 内部的内容。


    “今天,它能奏效,我们仍然可以连接到想要的任何内容源,但存在某些方案可以简化开发体验,”它补充道。“与其说这是一项要求,不如说是一种美好的享受,这就是为什么私以为我们无法在新年实现此目标,但我想在未来用它搞点事情。”


    Angular:可选的 Zone.js


    谷歌 Angular DevRel 技术主管兼经理 M.G. 表示,在过去的一年里,Angular 的两大成就是:



    • 引入了 Signal(信号)的细粒度响应性

    • 引入了可延迟视图


    它讲道,明年会在此基础上,进一步关注细粒度响应性,并使 Zone.js 成为可选选项。


    在 Angular 中,Zone 是跨异步任务持续存在的执行上下文。Zone 的 GitHub 仓库对此进行了详细解释,但 Zone 有五大职责,包括但不限于拦截异步任务调度和包装错误处理的回调,以及跨异步操作的 Zone 追踪。Zone.js 可以创建跨异步操作持久存在的上下文,并为异步操作提供生命周期钩子。


    “我们正在探索为现存项目启用可选的 Zone.js,开发者应该可以通过重构现存 App 来利用该功能,”M.G. 如是说。“诉诸可选的 Zone.js,我们期望优化加载时间,并提升初始渲染速度。研究细粒度响应性将其提升到另一个水平,使我们能够只检测组件模板的局部变化。”


    它说,这些功能将带来更快的运行时间。


    在另一个性能游戏中,Angular 正在考虑是否默认启用混合渲染。它补充说,可以选择退出混合渲染,因为它会增加托管要求和成本。


    “我们瞄到了 SSG(静态站点生成)和 SSR(服务端渲染)的巨大价值,凭借我们在 v17 中奠定的坚硬基建,我们正在努力进行最后的润色,以便从一开始就实现这种体验,”M.G. 如是说。


    它补充道,另一个优先事项是落实 Signal 的征求意见。


    开发者还可能会见证 Angular 文档的改进。根据其开发者调查,开发者希望享受进阶的学习体验,其中一部分包括使 Angular.dev 成为 Angular 的全新官网主页。它补充道,开发者还优先考虑了初始加载时间(混合渲染、部分水合和可选的 Zone.js 部分应该解决此问题),以及组件创作(Angular 计划进一步简化组件创作)。


    “我们致力于可持续迭代功能,并与时俱进地渐进增强它们,”M.G. 讲道。“开发者将能够从新年里的所有优化中受益,并将在接下来的几年中享受更好的开发体验和性能。”


    Solid:聚焦原语


    “Solid 之父”R.C. 表示,Solid 开发者可以关注新年的 SolidStart 1.0 和 Solid.js 2.0。SolidStart 是一个元框架,这意味着,它构建于 Solid.js 框架之上。它说,它相相当于 Svelte 的 SvelteKit。


    SolidStart 的官网文档是这样解释的:


    “Web App 通常包含一大坨组件:数据库、服务器、前端、打包器、数据请求/变更、缓存和基建。编排这些组件极具挑战性,并且通常需要跨 App 堆栈大量共享状态和冗余逻辑。进入 SolidStart:一种元框架,它提供了将所有这些组件万法归一的平台。”


    由于 SolidStart 仍处于测试阶段,R.C. 基本上有机会使用生态系统中已有的内容来使其变得更好。


    “其中一个重要的部分是,我们现在不再编写自己的部署适配器,而是使用 Nitro,它也为 Nuxt 框架提供支持,这让我们可以部署到所有不同的平台,”R.C. 讲道。


    另一个例子是,任何 Solid 路由器都可以在 SolidStart 中奏效。


    “这意味着,对路由器的底层部分大量更新,这样它们能够“梦幻联动”,但我非常满意的最终结果是,我们的志愿者小团队需要维护的代码更少了,而且它为开发者提供了很大的灵活性和控制力,”它说。“它们不会被迫采用单一的解决方案,这对我而言兹事体大,因为每个人都有自己的需求。正如我所言,如果您构建正确的基建,并弄清楚这些构建模块是什么,大家可以做更多的事情。”


    它说,最终的结果是一个具有“可交换”部分的元框架,而且不太我行我素。在越来越多的元框架决定开发者技术方案的世界中,Solid 团队一直在思考正确的原语片段的影响。


    “于我而言,它始终是关于构建基元块,这是一个非常工程化的焦点,我认为这是它与众不同的部分原因,”它说。“我一直喜欢提供选择,而且私以为如果我们有正确的原语、正确的片段,我们就可以构建正确的解决方案。”


    它表示,Solid 2.0 应该会在新年中后期的某个时间点发布。它说,目前它们正在设计如何处理异步系统的原型。


    “Solid 2.0 也将是重量级版本,因为我们正在重新审视响应式系统,并研究如何解决异步 Signal 或异步系统,”R.C. 讲道。


    它补充道,Solid 试图平衡控制与性能。


    “我们的社区中有一大坨热心人,它们非常有技术头脑,既关心性能,也关心控制,”它说。“我们确实吸引了一大坨自己真正想要控制构建的方方面面的用户。”


    作者:人猫神话
    来源:juejin.cn/post/7331925629082566707
    收起阅读 »