注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一次接手外包公司前端代码运行踩坑过程

web
背景 外包项目结束后,代码交给我公司需要存起来,因为后期还会有迭代开发任务,以后的事情肯定是我们公司内部来维护了,那就需要把代码运行起来,这过程中运行前端项目遇到的几个问题和处理过程简单记录下。 主要问题是,外包公司建有自己UI组件库,所有里面很多包是他们np...
继续阅读 »

背景


外包项目结束后,代码交给我公司需要存起来,因为后期还会有迭代开发任务,以后的事情肯定是我们公司内部来维护了,那就需要把代码运行起来,这过程中运行前端项目遇到的几个问题和处理过程简单记录下。


主要问题是,外包公司建有自己UI组件库,所有里面很多包是他们npm私有仓库的托管,我们无法访问到他们的私服仓库,思路是从 node_modules中 把私有包迁移到我们公司自己内网仓库


代码


我拿到的两个项目代码,共有两个项目代码,下面这是web的代码,处理思路是一样的


image.png


第一步运行看是否正常


因为观察到项目中有 node_modules ,因为外包公司是把整个项目文件都拷贝过来了,里面还包括 .git 目录,如果能直接运行起来,那万事大吉。


显示看下图,是运行报错的,缺少包和相关命令,所以我们还是得自己来重新安装 node_modules ,但是问题是私有包如何解决?


image.png


第二次尝试重新安装包


我们尝试重新直接安装包,安装失败,因为访问不到私有仓库域名


image.png


正式迁移包


我们公司也是用verdaccio搭建过私有仓库的,所以要把外包项目的私有包上传到我们公司内部



  • package.json中找到私有包

  • 拷贝私有包成独立项目

  • 推送到我们公司内部verdaccio仓库(没有私有仓库就传到npm上也一样,但是外包公司自己的包还是别外传)

  • 项目中配置.npmrc锁定包来源

  • 锁定项目中版本号


package.json中找到私有包


通过判定看到下图的包在 http://www.npmjs.com/ 中查找不到,所以下面这些 @iios前缀的包是需要迁移到包


image.png


拷贝私有包成独立项目



我们从 node_modules 中拷贝出来这些文件夹



image.png



观察到所有包都是完整的,都有package.json文件



image.png


推送到我们公司内部verdaccio仓库


这里这么多包,如果简化可以使用lerna或者shell脚本来统一处理版本问题,但是我们简化就按个包执行推送命令即可


image.png


后续所有包同理操作即可


image.png


后面就不一一列举了,检查verdaccio是否推送成功


image.png


项目中配置.npmrc锁定包来源


现在私有包都上传完成了,所以需要回到主项目,安装包就行了,但是因为有私有包,于是需要执行 .npmrc 规定各种包的安装路径


image.png


锁定项目中版本号


这一步是我习惯,在package.json中,版本号固定写死,而不是 ^前缀开头自动更新此版本


而且更重要的是,外包项目已经在线上运行,万一以后要三方包变化导致一些莫名其妙问题就很麻烦,锁定版本号是非常有必要的,才能以后很久之后打开发布代码也是没有问题的


image.png


image.png


删除node_modules 和 yarn.lock ,重新安装包


image.png


image.png


重新运行


一切都搞完了,重新运行成功


image.png


image.png


作者:一个不会重复的id
来源:juejin.cn/post/7348090716578824230
收起阅读 »

弱智吧成最好中文AI训练数据:大模型变聪明,有我一份贡献

web
在中文网络上流传着这样一段话:弱智吧里没有弱智。百度「弱智吧」是个神奇的地方,在这里人人都说自己是弱智,但大多聪明得有点过了头。最近几年,弱智吧的年度总结文章都可以顺手喜提百度贴吧热度第一名。所谓总结,其实就是给当年吧里的弱智发言排个名。各种高质量的段子在这里...
继续阅读 »


在中文网络上流传着这样一段话:弱智吧里没有弱智。

百度「弱智吧」是个神奇的地方,在这里人人都说自己是弱智,但大多聪明得有点过了头。最近几年,弱智吧的年度总结文章都可以顺手喜提百度贴吧热度第一名。所谓总结,其实就是给当年吧里的弱智发言排个名。

各种高质量的段子在这里传入传出,吸引了无数人的围观和转载,这个贴吧的关注量如今已接近 300 万。你网络上看到的最新流行词汇,说不定就是弱智吧老哥的杰作。

随着十几年的发展,越来越多的弱智文学也有了奇怪的风格,有心灵鸡汤,有现代诗,甚至有一些出现了哲学意义。

最近几天,一篇人工智能领域论文再次把弱智吧推上了风口浪尖。

引发 AI 革命的大模型因为缺乏数据,终于盯上了弱智吧里无穷无尽的「数据集」。有人把这些内容拿出来训练了 AI,认真评测对比一番,还别说,效果极好。

接下来,我们看看论文讲了什么。
最近,大型语言模型(LLM)取得了重大进展,特别是在英语方面。然而,LLM 在中文指令调优方面仍然存在明显差距。现有的数据集要么以英语为中心,要么不适合与现实世界的中国用户交互模式保持一致。 
为了弥补这一差距,一项由 10 家机构联合发布的研究提出了 COIG-CQIA(全称 Chinese Open Instruction Generalist - Quality Is All You Need),这是一个高质量的中文指令调优数据集。数据来源包括问答社区、维基百科、考试题目和现有的 NLP 数据集,并且经过严格过滤和处理。
此外,该研究在 CQIA 的不同子集上训练了不同尺度的模型,并进行了深入的评估和分析。本文发现,在 CQIA 子集上训练的模型在人类评估以及知识和安全基准方面取得了具有竞争力的结果。
研究者表示,他们旨在为社区建立一个多样化、广泛的指令调优数据集,以更好地使模型行为与人类交互保持一致。
本文的贡献可以总结如下:

提出了一个高质量的中文指令调优数据集,专门用于与人类交互保持一致,并通过严格的过滤程序实现;

探讨了各种数据源(包括社交媒体、百科全书和传统 NLP 任务)对模型性能的影响。为从中国互联网中选择训练数据提供了重要见解;

各种基准测试和人工评估证实,在 CQIA 数据集上微调的模型表现出卓越的性能,从而使 CQIA 成为中国 NLP 社区的宝贵资源。


  • 论文地址:https://arxiv.org/pdf/2403.18058.pdf
  • 数据地址:https://huggingface.co/datasets/m-a-p/COIG-CQIA
  • 论文标题:COIG-CQIA: Quality is All You Need for Chinese Instruction Fine-tuning


COIG-CQIA 数据集介绍

为了保证数据质量以及多样性,本文从中国互联网内的优质网站和数据资源中手动选择了数据源。这些来源包括社区问答论坛、、内容创作平台、考试试题等。此外,该数据集还纳入了高质量的中文 NLP 数据集,以丰富任务的多样性。具体来说,本文将数据源分为四种类型:社交媒体和论坛、世界知识、NLP 任务和考试试题。


社交媒体和论坛:包括知乎、SegmentFault 、豆瓣、小红书、弱智吧。

世界知识:百科全书、四个特定领域的数据(医学、经济管理、电子学和农业)。

NLP 数据集:COIG-PC 、COIG Human Value 等。

考试试题:中学和大学入学考试、研究生入学考试、逻辑推理测试、中国传统文化。
表 1 为数据集来源统计。研究者从中国互联网和社区的 22 个来源总共收集了 48,375 个实例,涵盖从常识、STEM 到人文等领域。

图 2 说明了各种任务类型,包括信息提取、问答、代码生成等。

图 3 演示了指令和响应的长度分布。

为了分析 COIG-CQIA 数据集的多样性,本文遵循先前的工作,使用 Hanlp 工具来解析指令。

实验结果

该研究在不同数据源的数据集上对 Yi 系列模型(Young et al., 2024)和 Qwen-72B(Bai et al., 2023)模型进行了微调,以分析数据源对模型跨领域知识能力的影响,并使用 Belle-Eval 上基于模型(即 GPT-4)的自动评估来评估每个模型在各种任务上的性能。
表 2、表 3 分别显示了基于 Yi-6B、Yi-34B 在不同数据集上进行微调得到的不同模型的性能。模型在头脑风暴、生成和总结等生成任务中表现出色,在数学和编码方面表现不佳。


下图 4 显示了 CQIA 和其他 5 个基线(即 Yi-6B-Chat、Baichuan2-7B-Chat、ChatGLM2-6B、Qwen-7B-Chat 和 InternLM-7B-Chat)的逐对比较人类评估结果。结果表明,与强基线相比,CQIA-Subset 实现了更高的人类偏好,至少超过 60% 的响应优于或与基线模型相当。这不仅归因于 CQIA 能够对人类问题或指令生成高质量的响应,还归因于其响应更符合现实世界的人类沟通模式,从而导致更高的人类偏好。

该研究还在 SafetyBench 上评估了模型的安全性,结果如下表 4 所示:

在 COIG Subset 数据上训练的模型性能如下表 5 所示:





作者:APPSO
来源:mp.weixin.qq.com/s/BN52IrDg-xNosxkJ6MbNvA
收起阅读 »

Geoserver:小程序巨丝滑渲染海量点位

web
文章最后有效果图 需求 在小程序上绘制 40000+ 的点位。 难点 众所周知小程序的 map 组件性能低下,同时渲染几百个 marker 就会卡顿,一旦加上 callout 弹窗,这个数量可能会降到几十个,如果使用了 自定义弹窗(custom-callou...
继续阅读 »

文章最后有效果图



需求


在小程序上绘制 40000+ 的点位。


难点


众所周知小程序的 map 组件性能低下,同时渲染几百个 marker 就会卡顿,一旦加上 callout 弹窗,这个数量可能会降到几十个,如果使用了 自定义弹窗(custom-callout) 会更卡,所以渲染 4w+ 的点,用常规方法是不可能实现的。


方案


按需加载


按需加载即按屏幕坐标加载,只显示视野范围内的点位,需要后端配合在接口中新增 bbox(Bounding box) 参数,再从数据库中查出范围内的点。


小程序端需要使用视野变化监听方法实时更新,虽然请求和渲染频繁,但是在缩放等级较大时,有很高的性能:


<map bindregionchange="regionChanged" markers="{{markers}}">

regionChanged(e){
this.data.bbox = [     [e.detail.region.southwest.longitude, e.detail.region.southwest.latitude],
    [e.detail.region.northeast.longitude, e.detail.region.northeast.latitude],
  ]
   // 执行获取点、渲染点的操作
}

需要注意的是,目前的微信版本(8.0.47),基础库3.3.4该方法不可用,见 微信开放社区


如果遇到 bindregionchange 不可用时,可以用 bind:touchend 方法代替,手动获取范围


    setBbox() {
     mapCtx = wx.createMapContext('map', this)
     mapCtx.getRegion({
       success: (res) => {
         let bbox = [
          [res.southwest.longitude, res.southwest.latitude],
          [res.northeast.longitude, res.northeast.latitude],
        ]
         // 执行获取点、渲染点的操作
        })
    })
  }

使用了按需渲染后,在缩放等级较大时,已经可以有很好的效果,移动屏幕时基本可以秒加载出新的点,同时清除掉屏幕范围外的点。


然而,在点位多的时候,我们收到了 setData 长度超出的报错,页面也异常卡顿。


优化渲染方式


小程序的 setData 方法最多只能更新 1M 的数据,超过这个数据会报错,并严重卡顿,即使不超过,在数据量较大时,也会非常卡顿,为了解决这个问题,我们不能再使用 setData 去渲染数据。


小程序提供了专门渲染点的方法: addMarkers


// 执行获取点、渲染点的操作处,使用该方法,并设置 clear: true 。这样就达到了上面说的,更新点时,旧的点会被清除。


然而,这并没有解决根本问题,我们现在可以做到渲染远远大于1M的数据,并渲染时不会报错,但是由于小程序 map 组件的渲染策略,我们的点会一个一个渲染上去,我们知道更新 canvas 代价是很大的,尤其是像 marker 这种携带很多必要信息的东西。


这里我们尝试将 marker 携带的参数压缩到极致,仅保留经纬度、颜色状态信息、id、callout,效果依然差强人意。


并且,由于小程序 marker 的 callout 不是互斥的,且没有给我们预留参数去设置这一点,所以在我们切换 marker 选中状态时,需要把 marker 数组完全遍历一遍,移除其他的 callout , 并添加新的 callout,这个开销也是巨大不可接受的。


优化选中策略


为了解决切换 marker 选中状态时的开销问题,我们想了一个绝妙的主意,就是将 marker 数组中的 callout 完全移除,只保留 id 等必要字段,在点击时,添加一个新的带 callout 点上去,盖住原来的点,这样看起来就是原有的点被选中了,这样既压缩了 marker 携带参数,又解决了切换选中时必须遍历 marker 数组的问题。


height: 20,
width: 17,
iconPath: this.data.markerIcons[this.getMarkerType(item)],
latitude: item.point[1],
longitude: item.point[0],
id: this.getUniqueNumber(item.uid), //id 必须是数字
storeCode: item.uid,
//callout:{...} // 不要此项
customCallout: {} //必须加,不然会有一个没有内容的弹窗,这个可以阻止默认弹窗弹出

优化海量点渲染策略


经过上面的优化,我们的小程序已经可以高性能的显示点位了,但是当缩放等级低时(12以下),点位多起来了,我们目前的方法就显得力不从心了。


如果点位无限多,我们又该如何优化呢?


聚合


聚合指的是将临近的点位聚合成一个大点,从而达到渲染点数变少、提高性能的方法。


此方法经过实测,发现当点达到一定量级的时候,用了反而比不用还卡,因为每当你缩放地图时,都需要计算聚合,当计算压力大于渲染压力时,聚合反而成了一种负担,而不是优化了。


所以我们不用聚合。


小程序个性化图层


小程序提供了付费功能:个性化图层,可以上传海量数据并生成一个小程序支持加载的图层。遗憾的是这种方法只适合静态数据,对于经常需要变动的数据,这种方法的实时性得不到保证,只能通过手动在后台更新数据。


所以此方案也不可用。


瓦片


小程序 map 是不支持瓦片(个性化图层除外)加载的,但是我们知道,瓦片就是一张图片而已,那么小程序可以在地图上放图片吗,答案是可以:addGroundOverlay


我们决定朝着此方案努力,请看下文。


搭建 geoserver


首先到 geoserver官网 下载geoserver本体,geoserver是为数不多几个推荐 windows 平台的大型工具软件,下载前注意,geoserver对 jdk 版本有要求,版本不一致会导致 geoserver 启动失败等问题。


image.png
我们的服务器是 linux ,所以下载了linux版本,到服务器找个位置 直接 unzip 就可以了。


安装完之后,需要先编辑 start.ini 调整一个合适的空闲端口,作为后面web端管理页面的地址端口。别忘了在防火墙开启此端口。


最后在 bin 中有一个 startup.sh , 使用 nohup 命令设置后台运行。


此时在浏览器输入服务器地址和你刚刚设置的端口号,最后加上 /geoserver,即可看到geoserver的管理页面。


image.png
初始用户名密码:admin geoserver


登录完成后可以看到全部功能


image.png
点击数据存储 -> 添加新的数据存储,即可添加数据并发布图层。


可以看到支持 PostGis,使用 PostGis 作为数据源,图层会实时更新,也就是说,当数据变化时,无需任何代码和人工干预。


当数据源添加完成后,需要新建一个图层,并指定为刚刚新建的数据源。


此时,在图层预览页面即可看到刚刚创建的图层了,当然此时的图层使用的是默认样式,需要编写SLD(xml格式)的样式文件去指定样式,这对于我们来说无疑是一种负担。


好在 geoserver 有 css 插件,安装此插件并重启geoserver,即可使用 css 编写图层样式。


* {
 mark-size:8px;
}
[control_sts == 1] {
 mark:url("https://entropy.xxx.cn/xx/dotgreen.png");
}
[control_sts == 0] {
 mark:url("https://entropy.xxx.cn/xx/dotgray.png");
}

可以看到,它与标准css还是有一些差异的,像mark、mark-size在标准css中是不存在的。


指定样式后,在图层预览页面,可以看到效果


image.png


打开控制台,可以看到网络请求中的地址长这样:


http://xxx:8089/geoserver/cite/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=cite%3Axc_store_geo&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4326&WIDTH=670&HEIGHT=768&BBOX=114.4720458984375%2C37.7874755859375%2C118.1524658203125%2C42.0062255859375

放到浏览器窗口打开,发现是一张png图片,那么我们刚好可以使用小程序的 addGroundOverlay 添加到地图上。


SERVICE: WMS
VERSION: 1.1.1
REQUEST: GetMap
FORMAT: image/png
TRANSPARENT: true
STYLES:
LAYERS: xx:xxxx
exceptions: application/vnd.ogc.se_inimage
SRS: EPSG:4326
WIDTH: 670
HEIGHT: 768
BBOX: 114.4720458984375,37.7874755859375,118.1524658203125,42.0062255859375

看一下这些参数,出了 BBOX ,其他的写固定值就可以了。


这里注意,宽高值,需要设置为小程序中地图元素的大小,单位是 px。


在小程序中拼装WMS地址


比较简单,直接看代码:


    setTileImage(params: { LAYERS: string[], BBOX: string, SCREEN_WIDTH: number, SCREEN_HEIGHT: number, CQL_FILTER: string }) {
     mapCtx = wx.createMapContext('map', this)
     this.removeTileImage().then(() => {
       for (let index in params.LAYERS) {
         let id = +(9999 + index)
         !this.data.groundOverlayIds.includes(id) && this.data.groundOverlayIds.push(id)
         let data: any = {
           id: +(9999 + index),
           zIndex: 999,
           src: `http://xxx:8089/geoserver/cite/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=${params.LAYERS[index]}&STYLES=&exceptions=application/vnd.ogc.se_inimage&FORMAT=image/png&TRANSPARENT=true&FORMAT_OPTIONS=antialias:full&SRS=EPSG:4326&BBOX=${params.BBOX}&WIDTH=${params.SCREEN_WIDTH * 2}&HEIGHT=${params.SCREEN_HEIGHT * 2}&CQL_FILTER=${params.CQL_FILTER}`,
           bounds: {
             southwest: {
               latitude: +params.BBOX.split(',')[1],
               longitude: +params.BBOX.split(',')[0]
            },
             northeast: {
               latitude: +params.BBOX.split(',')[3],
               longitude: +params.BBOX.split(',')[2]
            }
          }
        }
         mapCtx.addGroundOverlay({
           ...data,
        })
      }
    })
  },

我这里封装了一个可以接受多个图层的方法,这里值得注意的是,我没有使用 updateGroundOverlay 方法去更新图层,而是先使用 removeGroundOverlay 移除,再重新添加的,这是因为updateGroundOverlay有一个bug,我不说,你可以自己试试。


完成


f42f89c10e3c044f3b8e0200d7dfa52a.webp
至此已经完全实现了小程序的海量点的渲染,无论点有多少,我们都只需要渲染一张图片而已,性能好的一批。


作者:德莱厄斯
来源:juejin.cn/post/7348363874965028864
收起阅读 »

统一公司的项目规范

web
初始化项目 vscode 里下好插件:eslint,prettier,stylelint 官网模版创建项目:pnpm create vite react-starter --template react-swc-ts 安装依赖:pnpm i 后面有可能遇到 ...
继续阅读 »

初始化项目



  • vscode 里下好插件:eslint,prettier,stylelint

  • 官网模版创建项目:pnpm create vite react-starter --template react-swc-ts

  • 安装依赖:pnpm i

  • 后面有可能遇到 ts 类型错误,可以提前安装一个pnpm i @types/node -D


配置 npm 使用淘宝镜像



  • 配置npmrc


    registry = "https://registry.npmmirror.com/"



配置 node 版本限制提示



  • package.json 中配置


    "engines": {
    "node": ">=16.0.0"
    },



配置 eslint 检查代码规范



eslint 处理代码规范,prettier 处理代码风格
eslint 选择只检查错误不处理风格,这样 eslint 就不会和 prettier 冲突
react 官网有提供一个 hook 的 eslint (eslint-plugin-react-hooks),用处不大就不使用了




  • 安装:pnpm i eslint -D

  • 生成配置文件:eslint --init(如果没eslint,可以全局安装一个,然后使用npx eslint --init)


    - To check syntax and find problems  //这个选项是eslint默认选项,这样就不会和pretter起风格冲突
    - JavaScript modules (import/export)
    - React
    - YES
    - Browser
    - JSON
    - Yes
    - pnpm


  • 配置eslintrc.json->rules里配置不用手动引入 react,和配置不可以使用 any

  • 注意使用 React.FC 的时候如果报错说没有定义 props 类型,那需要引入一下 react


    "rules": {
    //不用手动引入react
    "react/react-in-jsx-scope": "off",
    //使用any报错
    "@typescript-eslint/no-explicit-any": "error",
    }


  • 工作区配置.vscode>settings.json,配置后 vscode 保存时自动格式化代码风格


    比如写了一个 var a = 100,会被自动格式化为 const a = 100


    {
    "editor.codeActionsOnSave": {
    // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.eslint": true,
    //自动格式化
    "editor.formatOnSave": true
    }
    }


  • 配置.eslintignore,eslint 会自动过滤 node_modules


    dist


  • 掌握eslint格式化命令,后面使用 lint-staged 提交代码的时候需要配置


    为什么上面有 vscode 自动 eslint 格式化,还需要命令行: 因为命令行能一次性爆出所有警告问题,便于找到位置修复


    npx eslint . --fix//用npx使用项目里的eslint,没有的话也会去使用全局的eslint
    eslint . --fix //全部类型文件
    eslint . --ext .ts,.tsx --fix //--ext可以指定文件后缀名s

    eslintrc.json 里配置



  • "env": {
    "browser": true,
    "es2021": true,
    "node": true // 因为比如配置vite的时候会使用到
    },



配置 prettier 检查代码风格



prettier 格式化风格,因为使用 tailwind,使用 tailwind 官方插件




  • 安装:pnpm i prettier prettier-plugin-tailwindcss -D

  • 配置.prettierrc.json


    注释要删掉,prettier 的配置文件 json 不支持注释


    {
    "singleQuote": true, // 单引号
    "semi": false, // 分号
    "trailingComma": "none", // 尾随逗号
    "tabWidth": 2, // 两个空格缩进
    "plugins": ["prettier-plugin-tailwindcss"] //tailwind插件
    }


  • 配置.prettierignore


    dist
    pnpm-lock.yaml


  • 配置.vscode>settings.json,配置后 vscode 保存时自动格式化代码风格


    {
    "editor.codeActionsOnSave": {
    // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.eslint": true
    },
    //自动格式化
    "editor.formatOnSave": true,
    //风格用prettier
    "editor.defaultFormatter": "esbenp.prettier-vscode"
    }


  • 掌握prettier命令行


    可以让之前没有格式化的错误一次性暴露出来


    npx prettier --write .//使用Prettier格式化所有文件



配置 husky 使用 git hook



记得要初始化一个 git 仓库,husky 能执行 git hook,在 commit 的时候对文件进行操作




  • 安装


    sudo pnpm dlx husky-init


    pnpm install


    npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"',commit-msg 使用 commitlint


    npx husky add .husky/pre-commit "npm run lint-staged",pre-commit 使用 lint-staged



配置 commitlint 检查提交信息



提交规范参考:http://www.conventionalcommits.org/en/v1.0.0/




  • 安装pnpm i @commitlint/cli @commitlint/config-conventional -D

  • 配置.commitlintrc.json


    { extends: ['@commitlint/config-conventional'] }



配置 lint-staged 增量式检查



  • 安装pnpm i -D lint-staged

  • 配置package.json


    "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "prepare": "husky install",
    "lint-staged": "npx lint-staged"//新增,对应上面的husky命令
    },


  • 配置.lintstagedrc.json


    {
    "*.{ts,tsx,json}": ["prettier --write", "eslint --fix"],
    "*.css": ["stylelint --fix", "prettier --write"]
    }



配置 vite(代理/别名/drop console 等)



如果有兼容性考虑,需要使用 legacy 插件,vite 也有 vscode 插件,也可以下载使用




  • 一些方便开发的配置


    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react-swc'
    import path from 'path'

    // https://vitejs.dev/config/
    export default defineConfig({
    esbuild: {
    drop: ['console', 'debugger']
    },
    css: {
    // 开css sourcemap方便找css
    devSourcemap: true
    },
    plugins: [react()],
    server: {
    // 自动打开浏览器
    open: true
    proxy: {
    '/api': {
    target: 'https://xxxxxx',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^\/api/, '')
    }
    }
    },
    resolve: {
    // 配置别名
    alias: { '@': path.resolve(__dirname, './src') }
    },
    //打包路径变为相对路径,用liveServer打开,便于本地测试打包后的文件
    base: './'
    })


  • 配置打包分析,用 legacy 处理兼容性


    pnpm i rollup-plugin-visualizer -D


    pnpm i @vitejs/plugin-legacy -D,实际遇到了再看官网用


    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react-swc'
    import { visualizer } from 'rollup-plugin-visualizer'
    import legacy from '@vitejs/plugin-legacy'
    import path from 'path'
    // https://vitejs.dev/config/
    export default defineConfig({
    css: {
    // 开css sourcemap方便找css
    devSourcemap: true
    },
    plugins: [
    react(),
    visualizer({
    open: false // 打包完成后自动打开浏览器,显示产物体积报告
    }),
    //考虑兼容性,实际遇到了再看官网用
    legacy({
    targets: ['ie >= 11'],
    additionalLegacyPolyfills: ['regenerator-runtime/runtime']
    })
    ],
    server: {
    // 自动打开浏览器
    open: true
    },
    resolve: {
    // 配置别名
    alias: { '@': path.resolve(__dirname, './src') }
    },
    //打包路径变为相对路径,用liveServer打开,便于本地测试打包后的文件
    base: './'
    })


  • 如果想手机上看网页,可以pnpm dev --host

  • 如果想删除 console,可以按h去 help 帮助,再按c就可以 clear console


配置 tsconfig



  • tsconfig.json 需要支持别名


    {
    "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "./",
    "paths": {
    "@/*": ["src/*"]
    }
    },
    "include": ["src"],
    "references": [{ "path": "./tsconfig.node.json" }]
    }



配置 router



  • 安装:pnpm i react-router-dom

  • 配置router->index.ts


    import { lazy } from 'react'
    import { createBrowserRouter } from 'react-router-dom'
    const Home = lazy(() => import('@/pages/home'))
    const router = createBrowserRouter([
    {
    path: '/',
    element: <Home></Home>
    }
    ])
    export default router


  • 配置main.tsx


    import { RouterProvider } from 'react-router-dom'
    import ReactDOM from 'react-dom/client'
    import './global.css'
    import router from './router'

    ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
    <RouterProvider router={router} />
    )



配置 zustand 状态管理



  • 安装pnpm i zustand

  • store->index.ts


    import { create } from 'zustand'

    interface appsState {
    nums: number
    setNumber: (nums: number) => void
    }

    const useAppsStore = create<appsState>((set) => ({
    nums: 0,
    setNumber: (num) => {
    return set(() => ({
    nums: num
    }))
    }
    }))

    export default useAppsStore


  • 使用方法


    import Button from '@/comps/custom-button'
    import useAppsStore from '@/store/app'
    const ZustandDemo: React.FC = () => {
    const { nums, setNumber } = useAppsStore()
    const handleNum = () => {
    setNumber(nums + 1)
    }
    return (
    <div className="p-10">
    <h1 className="my-10">数据/更新</h1>
    <Button click={handleNum}>点击事件</Button>
    <h1 className="py-10">{nums}</h1>
    </div>

    )
    }

    export default ZustandDemo



配置 antd



  • 新版本的 antd,直接下载就可以用,如果用到它的图片再单独下载pnpm i antd

  • 注意 antd5 版本的 css 兼容性不好,如果项目有兼容性要求,需要去单独配置


配置 Tailwind css


pnpm i tailwindcss autoprefixer postcss


tailwind.config.cjs


// 打包后会有1kb的css用不到的,没有影响
// 用了antd组件关系也不大,antd5的样式是按需的
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
// colors: {
// themeColor: '#ff4132',
// textColor: '#1a1a1a'
// },
// 如果写自适应布局,可以指定设计稿为1000px,然后只需要写/10的数值
// fontSize: {
// xs: '3.3vw',
// sm: '3.9vw'
// }
}
},
plugins: []
}

postcss.config.cjs


module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

我喜欢新建一个 apply.css 引入到全局


@tailwind base;
@tailwind components;
@tailwind utilities;

.margin-center {
@apply mx-auto my-0;
}

.flex-center {
@apply flex justify-center items-center;
}

.absolute-center {
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
}

封装 fetch 请求



这个封装仅供参考,TS 类型有点小问题



// 可以传入这些配置
interface BaseOptions {
method?: string
credentials?: RequestCredentials
headers?: HeadersInit
body?: string | null
}

// 请求方式
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'

// 第一层出参
interface ResponseObject {
ok: boolean
error: boolean
status: number
contentType: string | null
bodyText: string
response: Response
}

// 请求头类型
type JSONHeader = {
Accept: string
'Content-Type': string
}

// 创建类
class Request {
private baseOptions: BaseOptions = {}

// 根据传入的 baseOptions 做为初始化参数
constructor(options?: BaseOptions) {
this.setBaseOptions(options || {})
}

public setBaseOptions(options: BaseOptions): BaseOptions {
this.baseOptions = options
return this.baseOptions
}

// 也提供获取 baseOption 的方法
public getBaseOptions(): BaseOptions {
return this.baseOptions
}

// 核心请求 T 为入参类型,ResponseObject 为出参类型
public request<T>(
method: HttpMethod,
url: string,
data?: T, //支持使用get的时候配置{key,value}的query参数
options?: BaseOptions //这里也有个 base 的 method
): Promise<ResponseObject> {
// 默认 baseOptions
const defaults: BaseOptions = {
method
// credentials: 'same-origin'
}

// 收集最后要传入的配置
const settings: BaseOptions = Object.assign(
{},
defaults,
this.baseOptions,
options
)

// 如果 method 格式错误
if (!settings.method || typeof settings.method !== 'string')
throw Error('[fetch-json] HTTP method missing or invalid.')

// 如果 url 格式错误
if (typeof url !== 'string')
throw Error('[fetch-json] URL must be a string.')

// 支持大小写
const httpMethod = settings.method.trim().toUpperCase()

// 如果是GET
const isGetRequest = httpMethod === 'GET'

// 请求头
const jsonHeaders: Partial<JSONHeader> = { Accept: 'application/json' }

// 如果不是 get 设置请求头
if (!isGetRequest && data) jsonHeaders['Content-Type'] = 'application/json'

// 收集最后的headers配置
settings.headers = Object.assign({}, jsonHeaders, settings.headers)

// 获取query参数的key
const paramKeys = isGetRequest && data ? Object.keys(data) : []

// 获取query参数的值
const getValue = (key: keyof T) => (data ? data[key] : '')

// 获取query key=value
const toPair = (key: string) =>
key + '=' + encodeURIComponent(getValue(key as keyof T) as string)

// 生成 key=value&key=value 的query参数
const params = () => paramKeys.map(toPair).join('&')

// 收集最后的 url 配置
const requestUrl = !paramKeys.length
? url
: url + (url.includes('?') ? '&' : '?') + params()

// get没有body
settings.body = !isGetRequest && data ? JSON.stringify(data) : null

// 做一层res.json()
const toJson = (value: Response): Promise<ResponseObject> => {
// 拿到第一次请求的值
const response = value

const contentType = response.headers.get('content-type')
const isJson = !!contentType && /json|javascript/.test(contentType)

const textToObj = (httpBody: string): ResponseObject => ({
ok: response.ok,
error: !response.ok,
status: response.status,
contentType: contentType,
bodyText: httpBody,
response: response
})

const errToObj = (error: Error): ResponseObject => ({
ok: false,
error: true,
status: 500,
contentType: contentType,
bodyText: 'Invalid JSON [' + error.toString() + ']',
response: response
})

return isJson
? // 如果是json,用json()
response.json().catch(errToObj)
: response.text().then(textToObj)
}

// settings做一下序列化
const settingsRequestInit: RequestInit = JSON.parse(
JSON.stringify(settings)
)

// 最终请求fetch,再通过then就能取到第二层res
return fetch(requestUrl, settingsRequestInit).then(toJson)
}

public get<T>(
url: string,
params?: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('GET', url, params, options)
}

public post<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('POST', url, resource, options)
}

public put<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('PUT', url, resource, options)
}

public patch<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('PATCH', url, resource, options)
}

public delete<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('DELETE', url, resource, options)
}
}

const request = new Request()

export { request, Request }


如果用 axios 请求


request.ts


import axios from 'axios'
import { AxiosInstance } from 'axios'
import { errorHandle, processData, successHandle } from './resInterceptions'
import { defaultRequestInterception } from './reqInterceptions'
const TIMEOUT = 5 * 1000

class Request {
instance: AxiosInstance
constructor() {
this.instance = axios.create()
this.init()
}

private init() {
this.setDefaultConfig()
this.reqInterceptions()
this.resInterceptions()
}
private setDefaultConfig() {
this.instance.defaults.baseURL = import.meta.env.VITE_BASE_URL
this.instance.defaults.timeout = TIMEOUT
}
private reqInterceptions() {
this.instance.interceptors.request.use(defaultRequestInterception)
}
private resInterceptions() {
this.instance.interceptors.response.use(processData)
this.instance.interceptors.response.use(successHandle, errorHandle)
}
}

export default new Request().instance

reqInterceptions.ts


import type { InternalAxiosRequestConfig } from 'axios'

const defaultRequestInterception = (config: InternalAxiosRequestConfig) => {
// TODO: 全局请求拦截器: 添加token
return config
}

export { defaultRequestInterception }

resInterceptions.ts


import { AxiosError, AxiosResponse } from 'axios'
import { checkStatus } from './checkStatus'

const processData = (res: AxiosResponse) => {
// TODO:统一处理数据结构
return res.data
}

const successHandle = (res: AxiosResponse) => {
// TODO:处理一些成功回调,例如请求进度条
return res.data
}

const errorHandle = (err: AxiosError) => {
if (err.status) checkStatus(err.status)
else return Promise.reject(err)
}

export { processData, successHandle, errorHandle }

checkStatus.ts


export function checkStatus(status: number, msg?: string): void {
let errMessage = ''

switch (status) {
case 400:
errMessage = `${msg}`
break
case 401:
break
case 403:
errMessage = ''
break
// 404请求不存在
case 404:
errMessage = ''
break
case 405:
errMessage = ''
break
case 408:
errMessage = ''
break
case 500:
errMessage = ''
break
case 501:
errMessage = ''
break
case 502:
errMessage = ''
break
case 503:
errMessage = ''
break
case 504:
errMessage = ''
break
case 505:
errMessage = ''
break
default:
}
if (errMessage) {
// TODO:错误提示
// createErrorModal({title: errMessage})
}
}

api.ts


import request from '@/services/axios/request'
import { ReqTitle } from './type'

export const requestTitle = (): Promise<ReqTitle> => {
return request.get('/api/一个获取title的接口')
}

type.ts


export type ReqTitle = {
title: string
}

配置 mobx(可不用)



  • 安装pnpm i mobx mobx-react-lite

  • 配置model->index.ts


    import { makeAutoObservable } from 'mobx'

    const store = makeAutoObservable({
    count: 1,
    setCount: (count: number) => {
    store.count = count
    }
    })

    export default store


  • 使用方法举个 🌰


    import store from '@/model'
    import { Button } from 'antd'
    import { observer, useLocalObservable } from 'mobx-react-lite'
    const Home: React.FC = () => {
    const localStore = useLocalObservable(() => store)
    return (
    <div>
    <Button>Antd</Button>
    <h1>{localStore.count}</h1>
    </div>

    )
    }

    export default observer(Home)



配置 changelog(可不用)


pnpm i conventional-changelog-cli -D


第一次先执行conventional-changelog -**p** angular -**i** CHANGELOG.md -s -r 0全部生成之前的提交信息


配置个脚本,版本变化打 tag 的时候可以使用


"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
}

配置 editorConfig 统一编辑器(可不用)



editorConfig,可以同步编辑器差异,其实大部分工作 prettier 做了,需要下载 editorConfig vscode 插件
有编辑器差异的才配置一下,如果团队都是 vscode 就没必要了




  • 配置editorconfig


    #不再向上查找.editorconfig
    root = true
    # *表示全部文件
    [*]
    #编码
    charset = utf-8
    #缩进方式
    indent_style = space
    #缩进空格数
    indent_size = 2
    #换行符lf
    end_of_line = lf



配置 stylelint 检查 CSS 规范(可不用)



stylelint 处理 css 更专业,但是用了 tailwind 之后用处不大了




  • 安装:pnpm i -D stylelint stylelint-config-standard

  • 配置.stylelintrc.json


    {
    "extends": "stylelint-config-standard"
    }


  • 配置.vscode>settings.json,配置后 vscode 保存时自动格式化 css


    {
    "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true, // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.stylelint": true //自动格式化stylelint
    },
    "editor.formatOnSave": true, //自动格式化
    "editor.defaultFormatter": "esbenp.prettier-vscode" //风格用prettier
    }


  • 掌握stylelint命令行


    npx stylelint "**/*.css" --fix//格式化所有css,自动修复css



下面是 h5 项目(可不用)


配置vconsole(h5)



  • 安装pnpm i vconsole -D

  • main.tsx里新增


    import VConsole from 'vconsole'
    new VConsole({ theme: 'dark' })



antd 换成 mobile antd(h5)



  • pnpm remove antd

  • pnpm add antd-mobile


配置 postcss-px-to-viewport(废弃)



  • 把蓝湖设计稿尺寸固定为 1000px(100px我试过蓝湖直接白屏了),然后你点出来的值比如是 77px,那你只需要写 7.7vw 就实现了自适应布局,就不再需要这个插件了

  • 安装:pnpm i postcss-px-to-viewport -D

  • 配置postcss.config.cjs


    module.exports = {
    plugins: {
    'postcss-px-to-viewport': {
    landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
    landscapeUnit: 'vw', // 横屏时使用的单位
    landscapeWidth: 568, // 横屏时使用的视口宽度
    unitToConvert: 'px', // 要转化的单位
    viewportWidth: 750, // UI设计稿的宽度
    unitPrecision: 5, // 转换后的精度,即小数点位数
    propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
    viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
    fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
    selectorBlackList: ['special'], // 指定不转换为视窗单位的类名,
    minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
    mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
    replace: true, // 是否转换后直接更换属性值
    exclude: [/node_modules/] // 设置忽略文件,用正则做目录名匹配
    }
    }
    }



作者:imber
来源:juejin.cn/post/7241875166887444541
收起阅读 »

如何及时发现网页的隐形错误

web
在上一篇文章前端监控究竟有多重要?大家了解了前端监控系统的重要性以及前端监控的组成部分、常见的监控指标、埋点方式。 接下来这篇文章我们就来详细学习一下前端监控系统中的,异常监控。 想要进行异常监控之前,肯定先要了解有哪些异常才能进行监控。 异常的类型 一般来说...
继续阅读 »

在上一篇文章前端监控究竟有多重要?大家了解了前端监控系统的重要性以及前端监控的组成部分、常见的监控指标、埋点方式。


接下来这篇文章我们就来详细学习一下前端监控系统中的,异常监控


想要进行异常监控之前,肯定先要了解有哪些异常才能进行监控。


异常的类型


一般来说,浏览器端的异常分为两种类型:



  • JavaScript 错误,一般都是来自代码的原因。

  • 静态资源错误,一般都是来着资源加载的原因


而这里面我们又有各自的差异


JavaScript 错误


先来说说JavaScript的错误类型,ECMA-262 定义了 7 种错误类型,说明如下:



  • EvalError :eval() 函数的相关的错误

  • RangeError :使用了超出了 JavaScript 的限制或范围的值。

  • ReferenceError: 引用了未定义的变量或对象

  • TypeError: 类型错误

  • URIError: URI操作错误

  • SyntaxError: 语法错误 (这个错误WebIDL中故意省略,保留给ES解析器使用)

  • Error: 普通异常,通常与 throw 语句和 try/catch 语句一起使用,利用属性 name 可以声明或了解异常的类型,利用message 属性可以设置和读取异常的详细信息。


如果想更详细了解可以看详细错误罗列这篇文章


静态资源错误



  • 通过 XMLHttpRequest、Fetch() 的方式来请求的 http 资源时。

  • 利用
收起阅读 »

CSS弹性布局:Flex布局及属性完全指南,点击解锁新技能!

Flex 布局是一种新型的 CSS 布局模式,它主要用于弹性盒子布局。相比于传统的布局方式,它更加灵活,易于调整,也更加适应不同的设备和屏幕尺寸。下面我们就来详细解析 Flex 布局及其属性,帮助大家深入理解和运用 Flex 布局。一、什么是Flex布局?在介...
继续阅读 »

Flex 布局是一种新型的 CSS 布局模式,它主要用于弹性盒子布局。相比于传统的布局方式,它更加灵活,易于调整,也更加适应不同的设备和屏幕尺寸。

下面我们就来详细解析 Flex 布局及其属性,帮助大家深入理解和运用 Flex 布局。

一、什么是Flex布局?

在介绍Flex布局之前,我们不得不提到它的前辈——浮动和定位。它们曾是布局的主力军,但随着响应式设计的兴起,它们的局限性也愈发明显。

Flex布局的出现,正是为了解决这些局限性,它允许我们在一个容器内对子元素进行灵活的排列、对齐和空间分配。

Description

Flex全称为 “Flexible Box Layout”,即 “弹性盒布局”,旨在提供一种更有效的方式来布局、对齐和分配容器中项目之间的空间,即使它们的大小未知或动态变化。

声明定义

容器里面包含着项目元素,使用 display:flex 或 display:inline-flex 声明为弹性容器。

.container {
display: flex | inline-flex;
}

flex布局的作用

  • 在父内容里面垂直居中一个块内容。

  • 使容器的所有子项占用等量的可用宽度/高度,而不管有多少宽度 / 高度可用。

  • 使多列布局中的所有列采用相同的高度,即使它们包含的内容量不同。

二、Flex布局的核心概念

要理解Flex布局,我们必须先了解几个核心概念:

2.1 容器与项目

容器(Container):设置了display: flex;的元素成为Flex容器。容器内的子元素自动成为Flex项目。

.container{
display: flex;
}
<div class="container">
<div class="item"> </div>
<div class="item">
<p class="sub-item"> </p>
</div>
<div class="item"> </div>
</div>

上面代码中, 最外层的 div 就是容器,内层的三个 div 就是项目。

注意: 项目只能是容器的顶层子元素(直属子元素),不包含项目的子元素,比如上面代码的 p 元素就不是项目。flex布局只对项目生效。

2.2 主轴(Main Axis)和交叉轴(Cross Axis)

主轴是Flex项目的排列方向,交叉轴则是垂直于主轴的方向。

Description

主轴(main axis)

沿其布置子容器的从 main-start 开始到 main-end ,请注意,它不一定是水平的;这取决于 flex-direction 属性(见下文), main size 是它可放置的宽度,是容器的宽或高,取决于 flex-direction。

交叉轴(cross axis)

垂直于主轴的轴称为交叉轴,它的方向取决于主轴方向,是主轴写满一行后另起一行的方向,从 cross-start 到 cross-end , cross size 是它可放置的宽度,是容器的宽或高,取决于 flex-direction。

三、Flex布局的基本属性

3.1容器属性

Description

容器的属性主要包括:

  • flex-direction:定义了主轴的方向,可以是水平或垂直,以及其起始和结束的方向。

  • flex-wrap:决定了当容器空间不足时,项目是否换行。

  • flex-flow:这是flex-direction和flex-wrap的简写形式。

  • justify-content:设置项目在主轴上的对齐方式。

  • align-items:定义了项目在交叉轴上的对齐方式。

  • align-content:定义了多根轴线时,项目在交叉轴上的对齐方式。

  • gap row-gap、column-gap:设置容器内项目间的间距。

3.1.1 主轴方向 flex-direction

定义主轴的方向,也就是子项目元素排列的方向。

  • row (默认):从左到右 ltr ;从右到左 rtl

  • row-reverse :从右到左 ltr ;从左到右 rtl

  • column: 相同, row 但从上到下

  • column-reverse: 相同, row-reverse 但从下到上

.container {
flex-direction: row | row-reverse | column | column-reverse;
}

Description

Description

3.1.2 换行 flex-wrap

设置子容器的换行方式,默认情况下,子项目元素都将尝试适合一行nowrap。

  • nowrap (默认)不换行

  • wrap 一行放不下时换行

  • wrap-reverse 弹性项目将从下到上换行成多行

.container {
flex-wrap: nowrap | wrap | wrap-reverse;
}

Description

3.1.3 简写 flex-flow

flex-direction 和 flex-wrap 属性的简写,默认值为 row nowrap。

.container {
flex-flow: column wrap;
}

取值情况:

Description

3.1.4 项目群对齐 justify-content与align-items

justify-c ontent 决定子元素在主轴方向上的对齐方式,默认是 flex-start。

.container {
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly | start | end | left | right ... + safe | unsafe;
}

Description

align-items 决定子元素在交叉轴方向上的对齐方式,默认是 stretch。

.container {
align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline | start | end | self-start | self-end + ... safe | unsafe;
}

Description

3.1.5多行对齐 align-content

align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

  • flex-start:与交叉轴的起点对齐。

  • flex-end:与交叉轴的终点对齐。

  • center:与交叉轴的中点对齐。

  • space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。

  • space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。

  • stretch(默认值):轴线占满整个交叉轴。

.container {
align-content: flex-start | flex-end | center | space-between | space-around | space-evenly | stretch | start | end | baseline | first baseline | last baseline + ... safe | unsafe;
}

Description

3.1.6 间距 gap row-gap column-gap

设置容器内项目之间的间距,只控制项目与项目的间距,对项目与容器的间距不生效。

.container {
display: flex;
...
gap: 10px;
gap: 10px 20px; /* row-gap column gap */
row-gap: 10px;
column-gap: 20px;
}

Description
这设置的是最小间距,因为 just-content 导致的间距变大。

3.2项目属性

Description

项目item 的属性包括:

  • order:指定了项目的排列顺序。

  • flex-grow:定义了在有可用空间时的放大比例。

  • flex-shrink:定义了在空间不足时的缩小比例。

  • flex-basis:指定了项目在分配空间前的初始大小。

  • flex:这是flex-grow、flex-shrink和flex-basis的简写形式。

  • align-self:允许单个项目独立于其他项目在交叉轴上对齐。

3.2.1 排序位置 order

  • 每个子容器的order属性默认为0

  • 通过设置order属性值,改变子容器的排列顺序

  • 可以是负值,数值越小的话,排的越靠前

.item1 {
order: 3; /* default is 0 */
}

Description

3.2.2 弹性成长 flex-grow

在容器主轴上存在剩余空间时, flex-grow才有意义。

定义的是可放大的能力,0 (默认)禁止放大,大于 0 时按占的比重分放大,负数无效。

.container{
border-left:1.2px solid black;
border-top:1.2px solid black;
border-bottom: 1.2px solid black;
width: 100px;
height: 20px;
display: flex;
}
.item{
border-right:1.2px solid black;
width: 20px;height: 20px;
}
.item1{
/* 其他的都是0,这一个是1,1/1所以能所有剩下的空间都是item1的 */
flex-grow: 1; /* default 0 */
}
<div>
<div class="item item1" style="background-color: #7A42A8;"></div>
<div style="background-color: #8FAADC;"></div>
<div style="background-color: #DAE3F3;"></div>
</div>

Description

3.2.3 弹性收缩 flex-shrinik

当容器主轴 “空间不足” 且 “禁止换行” 时, flex-shrink才有意义。

定义的是可缩小的能力,1 (默认)等大于 0 的按比例权重收缩, 0 为禁止收缩,负数无效。

.container{
width: 100px;
height: 20px;
display: flex;
flex-wrap: nowrap;
}
.item{
width: 50px;height: 20px;
}
.item1{/*收缩权重1/3,总空间50,所以它占33.33,为原本的2/3*/
flex-shrink: 1; /* default 1 */
}
.item2{/*收缩权重2/3,总空间50,所以它占16.67,为原本的1/3*/
flex-shrink: 2; /* default 1 */
}
.item3{
flex-shrink: 0; /* default 1 */
}
<div>
<div class="item item1" style="background-color: #7A42A8;"></div>
<div class="item item2" style="background-color: #8FAADC;"></div>
<div class="item item3" style="background-color: #DAE3F3;"></div>
</div>

Description

3.2.4 弹性基值 flex-basis

flex-basis 指定了 flex 元素在主轴方向上的初始尺寸,它可以是长度(例如 20% 、 5rem 等)或关键字。felx-wrap根据它计算是否换行,默认值为 auto ,即项目的本来大小。它会覆盖原本的width 或 height。

.item {
flex-basis: <length> | auto; /* default auto */
}

3.2.5 弹性简写flex

flex-grow , flex-shrink 和 flex-basis 组合的简写,默认值为 0 1 auto。

.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}

取值情况:
Description

3.2.6自我对齐 align-self

这允许为单个弹性项目覆盖默认的交叉轴对齐方式 align-items。

.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

Description

注意: flexbox布局和原来的布局是两个概念,部分css属性在flexbox盒子里面不起作用,eg:float , clear 和 vertical-align 等等。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

四、实战演练

让我们通过一个简单的例子来实践一下Flex布局的魅力。假设我们有6张图片,我们希望在不同的屏幕尺寸下,它们能够自适应排列。

1、设置容器属性:

对于包含图片的容器,首先将其display属性设置为flex,从而启用Flex布局。

2、确定排列方向:

根据设计需求,可以通过设置flex-direction属性来确定图片的排列方向。例如,如果希望图片在小屏幕上水平排列,可以设置flex-direction: row;如果希望图片垂直排列,则设置flex-direction: column。

3、调整对齐方式:

使用justify-content和align-items属性来调整图片的对齐方式。例如,如果想让图片在主轴上均匀分布,可以设置justify-content: space-around;如果想让图片在交叉轴上居中对齐,可以设置align-items: center。

4、允许换行显示:

如果需要图片在小屏幕上换行显示,可以添加flex-wrap: wrap属性。

5、优化空间分配:

通过调整flex-grow、flex-shrink和flex-basis属性来优化空间分配。例如,可以设置图片的flex-basis为calc(100% / 3 - 20px),这样每张图片会占据三分之一的宽度减去20像素的间距。

示例代码如下:

<!DOCTYPE html>
<html>
<head>
<style>
.image-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.image-grid img {
flex-basis: calc(100% / 3 - 20px);
}
@media screen and (max-width: 800px) {
.image-grid img {
flex-basis: calc(100% / 2 - 20px);
}
}
@media screen and (max-width: 400px) {
.image-grid img {
flex-basis: calc(100% - 20px);
}
}
</style>
</head>
<body>
<div>
<img src="image1.jpg" alt="Image 1">
<img src="image2.jpg" alt="Image 2">
<img src="image3.jpg" alt="Image 3">
<img src="image4.jpg" alt="Image 4">
<img src="image5.jpg" alt="Image 5">
<img src="image6.jpg" alt="Image 6">
</div>
</body>
</html>

将上述代码保存为一个HTML文件,并将image1.jpg、image2.jpg等替换为你自己的图片路径。然后在浏览器中打开该HTML文件,你将看到一个响应式的图片网格布局,图片会根据屏幕尺寸自适应排列。

Flex布局以其简洁明了的属性和强大的适应性,已经成为现代网页设计不可或缺的工具。掌握了Flex布局,你将能够轻松应对各种复杂的页面布局需求,让你的设计更加灵活、美观。现在,就打开你的代码编辑器,开始你的Flex布局之旅吧!

收起阅读 »

前端可玩性UP项目:大屏布局和封装

web
前言 autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。 这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。 分析设计稿 分...
继续阅读 »

前言


autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。


这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。


分析设计稿


分析设计稿之前先吐槽一下大屏这种展现形式,这简直就是自欺欺人、面子工程的最直接的诠释,是吊用没有,只为了好看,如果设计的再不好看啊,这就纯纯是屎。在我的理解中,这就像把PPT放到了web端,仅此而已。



但是王哥告诉我:"你看似没有用的东西,其实都有用,很多想真正做出有用的产品的企业,没钱,就要先把面子工程做好,告诉别人他们要做一件什么事,这样投资人才会看到,后面才有机会发展。"



布局方案


image.png
上图展示了一个传统意义上且比较普遍的大屏形态,分为四个部分,分别是


头部


头部经常放标题、功能菜单、时间、天气


左右面板


左右面板承载了各种数字和报表,还有视频、轮播图等等


中间


中间部分一般放地图,这其中又分假地图(一张图片)、图表地图(如echarts)、地图引擎(如:leaflet、mapbox、高德、百度)。或者有的还会放3D场景,一般有专门的同事去做3D场景,然后导入到web端。


大屏的设计通常的分辨率是 1920*1080 的,这也是迄今为止应用最广泛的显示器配置,当然也有基于客户屏幕做的异形分辨率,这就五花八门了。


但是万变不离其宗,分辨率的变化不会影响它的基本结构,根据上面的图,我们可以快速构建结构代码


  <div class='Box'>
   <div class="header"></div>
   <div class="body">
     <div class="leftPanel"></div>
     <div class="mainMap"></div>
     <div class="rightPanel"></div>
   </div>
 </div>

上面的代码实现了最简单的上下(Box)+左右(body)的布局结构,完全不需要任何定位策略。


要实现上图的效果,只需最简单的CSS即可完成布局。


组件方案


大屏虽然是屎,但是是一种可玩性很强的项目,想的越复杂,做起来就越复杂,想的越简单,做起来就越简单。


可以疯狂封装炫技,因为大屏里面的可玩组件简直太多了,且涵盖的太全了,想怎么玩都可以,包括但不限于 各类图表库的封装(echarts、highCharts、vChart)、轮播图(swiper)、地图引擎、视频库(包括直播流)等等。


如果想简单,甚至可以不用封装,可以看到结构甚至简单到不用CSS几行就可以搭建出基本框架,只把header、leaftPanel、rightPanel、map封装一下就可以了。


这里还有一个误区,就是大家都喜欢把 大型的组件库 拉到大屏里来用,结果做完了发现好像只用了一个 toast 和一个下拉组件,项目打包后却增大了几十倍的体积,其实像这种简单的组件,完全可以手写,或者找小的独立包来用,一方面会减小体积,不至于让项目臃肿,另一方面可以锻炼自己的手写能力,这才是有必要的封装。


适配


目前主流的适配方案,依然是 rem 方案,其原理就是根据根元素的 font-size 自动计算大小,但是此方法需要手动计算 rem 值,或者使用第三方插件如postcss等,但是此方案还有一个弊端,就是无法向下兼容,因为浏览器中最小的文字大小是12px。


vh/vw方案就不再赘述了,原理基本和 rem/em 相似,都涉及到单位的转换。


autofit.js


主要讲一下使用 autofit.js 如何快速实现适配。


不支持的场景


首先 autofit.js 不支持 elementUI(plus)、ant-design等组件库,具体是不支持包含popper.js的组件,popper.js 在计算弹出层位置时,不会考虑 scale 后的元素的视觉大小,所以会造成弹出元素的位置偏移。


其次,不支持 百度地图,百度地图对窗口缩放事件没有任何处理,有同学反馈说,即使使用了resize属性,百度地图在和autofit.js共同使用时,也会有事件热区偏移的问题。而且百度地图使用 bd-09 坐标系,和其他图商不通用,引擎的性能方面也差点意思,个人不推荐在开发中使用百度地图。


然后一些拖拽库,如甘特图插件,可能也不支持,他们在计算鼠标位置时同样没有考虑 scale 后的元素的视觉大小。


用什么单位


不支持的单位:vh、vw、rem、em


让我诧异的是,老有人问我该用什么单位,主要徘徊在 px 和 % 之间,加群的同学多数是因为用了相对单位,导致留白了。不过人各有所长,跟着各位大佬我也学到了很多。


看下图


image.png
假如有两个宽度为1000的元素,他们内部都有一个子元素,第一个用百分比设置为 width:50%;left:1% , 第二个设置为 wdith:500px;left:10px 。此时,只要外部的1000px的容器宽度不变,这两个内部元素在视觉上是一模一样的,且在实际数值上也是一模一样的,他们宽度都为500px,距离左侧10px。


但是如果外部容器变大了,来看一下效果:


image.png
在样式不变的情况下,仅改变外部容器大小,差异就出来了,由上图可知,50%的元素依然占父元素的一半,实际宽度变成了 1000px,距离左侧的实际距离变成了 20px。


这当然不难理解,百分比单位是根据 最近的、有确定大小的父级元素计算的。


所以,应该用什么单位其实取决于想做什么,举个例子:在1920*1080基础上开发,中间的地图写成了宽度为 500px ,这在正常情况下,看起来没有任何问题,它大概占屏幕的 26%,当屏幕分辨率达到4096*2160时,它在屏幕上只占 12%,看起来就是缩在一角。而当你设置宽度为26%时,无论显示器如何变化,它始终占屏幕26%。


autofit.js 所干的事,就是把1000px 变成了 2000px或者把2000px变成了1000px,并给它设置了一个合适的缩放大小。


图表、图片拉伸


背景或各种图片按需设置 object-fit: cover;即可


图表如echarts一般推荐使用百分比,且监听窗口变化事件做resize()


结语


再次感慨,大道至简,事情往往没有那么复杂,祝各位前程似锦。


作者:德莱厄斯
来源:juejin.cn/post/7344625554530779176
收起阅读 »

js检测网页空闲状态(一定时间内无操作)

web
1. 背景 最近开发项目时,常碰到“用户在一定时间内无任何操作时,跳转到某个页面”的需求。 网上冲浪后,也没有找到一个比较好的js封装去解决这个问题,从而决定自己实现。 2. 如何判断页面是否空闲 首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何...
继续阅读 »

1. 背景


最近开发项目时,常碰到“用户在一定时间内无任何操作时,跳转到某个页面”的需求。


网上冲浪后,也没有找到一个比较好的js封装去解决这个问题,从而决定自己实现。


2. 如何判断页面是否空闲


首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何操作,则当前网页为空闲状态。


用户操作网页,无非就是通过鼠标键盘两个输入设备(暂不考虑手柄等设备)。因而我们可以监听相应的输入事件,来判断网页是否空闲(用户是否有操作网页)。



  1. 监听鼠标移动事件mousemove

  2. 监听键盘按下事件mousedown

  3. 在用户进入网页后,设置延时跳转,如果触发以上事件,则移除延时器,并重新开始。


3. 网页空闲检测实现


3.1 简易实现


以下代码,简单实现了一个判断网页空闲的方法:


const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;

const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};
const onStartTimer = () => {
onClearTimer();
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
};
const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
};
const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

也许你注意到了,我并没有针对onStartTimer事件进行防抖,那这是不是会对性能有影响呢?


是的,肯定有那么点影响,那我为啥不添加防抖呢?


这是因为添加防抖后,形成了setTimeout嵌套,嵌套setTimeout会有精度问题(参考)。


或许你还会说,非活动标签页(网页被隐藏)的setTimeout的执行和精度会有问题(参考非活动标签的超时)。


确实存在以上问题,接下来我们就来一一解决吧!


3.2 处理频繁触发问题


我们可以通过添加一个变量记录开始执行时间,当下一次执行与当前的时间间隔小于某个值时直接退出函数,从而解决这个问题(节流思想应用)。


const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;
// 记录开始时间
let beginTime = 0;
const onStartTimer = () => {
// 触发间隔小于100ms时,直接返回
const currentTime = Date.now();
if (pageTimer && currentTime - beginTime < 100) {
return;
}

onClearTimer();
// 更新开始时间
beginTime = currentTime;
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};
const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
};
const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
};
const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

3.3 处理页面被隐藏的情况(完整实现)


我们可以监听visibilitychange事件,在页面隐藏时移除延时器,然后页面显示时继续计时,从而解决这个问题。


/**
* 网页空闲检测
* @param {() => void} callback 空闲时执行,即一定时长无操作时触发
* @param {number} [timeout=15] 时长,默认15s,单位:秒
* @param {boolean} [immediate=false] 是否立即开始,默认 false
* @returns
*/

const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;
let beginTime = 0;
const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};
const onStartTimer = () => {
const currentTime = Date.now();
if (pageTimer && currentTime - beginTime < 100) {
return;
}

onClearTimer();
beginTime = currentTime;
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};

const onPageVisibility = () => {
// 页面显示状态改变时,移除延时器
onClearTimer();

if (document.visibilityState === 'visible') {
const currentTime = Date.now();
// 页面显示时,计算时间,如果超出限制时间则直接执行回调函数
if (currentTime - beginTime >= timeout * 1000) {
callback();
return;
}
// 继续计时
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000 - (currentTime - beginTime));
}
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
document.addEventListener('visibilitychange', onPageVisibility);
};

const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
document.removeEventListener('visibilitychange', onPageVisibility);
};

const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

通过以上代码,我们就完整地实现了一个网页空闲状态检测的方法。


4. 扩展阅读


chrome浏览器其实提供了一个Idle DetectionAPI,来实现网页空闲状态的检测,但是这个API还是一个实验性特性,并且Firefox与Safari不支持。API参考


作者:求知若饥
来源:juejin.cn/post/7344670957405405223
收起阅读 »

灰度发布策略在前端无损升级中的应用

web
为了提升浏览器加载页面资源的性能,对于js、css、图片等静态资源,web服务器往往会通过Cache-Control、ETag/If--Match、Last-Modified/If-Modified-Since、Pragma、Expires、Date、Age等...
继续阅读 »

为了提升浏览器加载页面资源的性能,对于js、css、图片等静态资源,web服务器往往会通过Cache-Control、ETag/If--Match、Last-Modified/If-Modified-Since、Pragma、Expires、Date、Age等头部来控制、管理、检测这类资源对缓存机制的使用情况。同时,为了使新版本的js、css等资源立即生效,一种比较通行的做法是为js、css这些文件名添加一个hash值。这样当js、css内容发生变化时,浏览器获取的是不同的js、css文件。在这种情况下,旧版本的index.html文件可能是这样的:


<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>测试</title>
<meta http-equiv=X-UA-Compatible content="IE=Edge">
<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
<link href=/static/css/main.4656e35c1e8d4345f5bf.css rel=stylesheet>
</head>
<style>
html, body {
width: 100%;
}
</style>
<body>
<div id=newMain></div>
<script type=text/javascript src=/static/js/main-4656e35c1e8d4345f5bf.js></script>
</body>
</html>

当项目的js、css内容发生了变化时,新版本的index.html文件内容变成这样的(js和css文件名携带了新的hash值1711528240049):


<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>测试</title>
<meta http-equiv=X-UA-Compatible content="IE=Edge">
<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
<link href=/static/css/main.1711528240049.css rel=stylesheet>
</head>
<style>
html, body {
width: 100%;
}
</style>
<body>
<div id=newMain></div>
<script type=text/javascript src=/static/js/main-1711528240049.js></script>
</body>
</html>

因为index.html文件一般会设置为不缓存,这样用户每次访问首页时,都会从web服务器重新获取index.html,然后根据index.html中的资源文件是否变化,从而决定是否使用缓存的文件。这样既能让用户立即获取最新的js、css等静态资源文件,又能充分地使用缓存。index.html的响应头大概长这样:


847fa51ec16bbfa14a5865482b23f396.png


但是为了保证系统的高可用,web后端往往由多个实例提供服务,用户请求会在多个服务实例间进行负载均衡。而系统升级过程中,会存在多个版本共存的现象。这时,如果用户从旧版本实例上获取了index.html文件,然后再去获取旧版本的js、css文件(main-4656e35c1e8d4345f5bf.jsmain.4656e35c1e8d4345f5bf.css),但是请求却分发到了新版本服务实例上,这时因为新版本服务实例只有main-1711528240049.jsmain.1711528240049.css文件,就会导致访问失败。反过来,如果从新版本实例上获取了index.html文件,在请求相应的js、css文件时,也可能被分发到旧版本实例上,也导致访问失败。


解决方法:


1)首先,改造一下index.html文件中引用js、css等静态资源的路径,添加一个版本号,如v1、v2,这样index.html文件对js、css的引用变为:


<link href=/static/v1/css/main.1711528240049.css rel=stylesheet>
<script type=text/javascript src=/static/v1/js/main-1711528240049.js></script>

2)使用灰度发布策略升级系统,具体步骤如下(假设系统包含A、B两个服务实例)



  1. 升级前(稳态),在应用网关(代理)上配置路由策略V1,该路由策略的功能为:匹配路径前缀/static/v1的请求负载均衡分发到A、B两个服务实例

  2. 将待升级的服务实例A从路由策略V1中摘除掉,这时用户请求只会发送给实例B

  3. 待实例A上所有进行中的请求都处理完后,就可以安全的停掉旧的服务,替换为新的服务,这时还不会有请求分发到实例A

  4. 待实例B测试功能正常后,在应用网关(代理)上新增一条路由策略V2,该路由策略的功能为:匹配路径前缀/static/v2的请求分发到服务实例A。这时,从服务实例A上获取的index.html文件引发的后续js、css请求,都会分发到服务实例A,从服务实例B上获取的index.html文件引发的后续js、css请求,都会分发到服务实例B

  5. 继续将实例B从路由策略V1中摘掉,然后升级实例B,将实例B添加到路由策略V2中

  6. 所有的流量都切换到了路由策略V2中,下线路由策略V1。完成整个升级过程,实现了前端的无损升级


作者:movee
来源:juejin.cn/post/7353069220827856946
收起阅读 »

特效炸裂:小米 SU7 在线特效网站技术不完全揭秘!!!

web
哈喽,大家好 我是 xy👨🏻‍💻。用 Three.js 实现 小米 SU7 在线体验,特效相当炸裂!!! 前言 最近一位叫 @GameMCU的大佬用 Webgl、Three.js 等技术实现了一个 小米 SU7 在线体验网站:https://gamemcu....
继续阅读 »

哈喽,大家好 我是 xy👨🏻‍💻。用 Three.js 实现 小米 SU7 在线体验,特效相当炸裂!!!



前言


最近一位叫 @GameMCU的大佬用 WebglThree.js 等技术实现了一个 小米 SU7 在线体验网站:https://gamemcu.com/su7/被广大网友疯传,效果相当炸裂!


网站首发当天由于访问量过大导致奔溃, 后来可能获得了某里云官方支持!!! 这一波真的要给某里云点赞!



更有网友评论: 这效果和交互完全可以吊打官方和各种卖车的网站了啊



并且 @小米汽车官方:求求了,收编了吧,这能极大提升小米su7的逼格,再用到公司其他产品,能提升整体公司的逼格



废话不多说,直接上效果!!!


效果展示



  • 模拟在汽车在道路行驶特效,宛如身临其境




  • 流线型车身设计,彰显速度与激情的完美融合。每一处细节都经过精心打磨,只为给你带来最纯粹的驾驶体验。




  • 在高速行驶的过程中,风阻是影响车速的重要因素。我们的特效模拟器通过先进的算法,真实还原了风阻对车辆的影响。当你长按鼠标,感受那股扑面而来的气流,仿佛置身于真实的驾驶环境中。




  • 雷达实时探测功能可以帮你轻松掌握周围车辆的情况,让你在驾驶过程中更加安心



视频


是怎么实现的


在线体验完@GameMCU大佬的网站之后, 我很好奇大佬是使用什么技术去实现的, 身为前端开发的我, 第一步当然是 F12 打开控制台查看



发现使用的是 Three.js r150 版本开发, 并且还用了一个叫 xviewer.js 的插件,


于是乎我找到了@GameMCU大佬的 github 主页, 在主页中介绍了 xviewer.js:



xviewer.js是一个基于 three.js 的插件式渲染框架,它对 three.js 做了一层简洁优雅的封装,包含了大量实用的组件和插件,目标是让前端开发者能更简单地应用webgl技术。



比较遗憾的是 xviewer.js 目前还没有开源, 不过按照作者的意思是可能会在近期开源。


虽然目前 小米 SU7 在线体验网站没有开源, 但是作者主页开源了另外一个项目: three.js复刻原神启动, 也是一个基于 xviewer.js 开发的在线网站。


通过源码发现作者在项目中写了大量的 Shader, Shader 对于实现复杂的视觉效果和图形渲染技术至关重要,它们使得开发者能够创建出令人印象深刻的3D场景动画



Shader 是一种在计算机图形学中使用的程序,它运行在图形处理单元(GPU)上,用于处理渲染过程中的光照、颜色、纹理等视觉效果。


Shader 通常被用于 3D 图形渲染中,以增强视觉效果,使得图像更加逼真和吸引人。


在 Three.js 中, Shader 通常分为两类:顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。


顶点着色器负责处理顶点数据,如位置颜色纹理坐标等,而片元着色器则负责处理像素颜色,包括光照材质属性


总之,Shader 在 Three.js 中扮演着至关重要的角色,它们为开发者提供了强大的工具来创建丰富、动态和引人入胜的 3D 图形内容。通过学习和掌握 Shader 编程,开发者可以极大地扩展 Three.js 的应用范围和创作能力。


那么作为一名前端开发人员, 应该怎么快速入门 Shader, 并且用 Shader 创造令人惊叹的交互体验呢???


三个学习 Shader 网站推荐


1. The Book of Shaders



网址: https://thebookofshaders.com/?lan=ch


The Book of Shaders 是一个在线学习 Shader 的网站(电子书),它提供了一系列关于 Shader 的基础教程和示例代码,堪称入门级指南


2.Shadertoy



网址:https://www.shadertoy.com/


Shadertoy 是一个基于 WebGL 的在线实时渲染平台,主要用于编辑分享查看 shader 程序及其实现的效果。


在这个平台上,用户可以创作和分享自己的 3D 图形效果。它提供了一个简单方便的环境,让用户可以轻松编辑自己的片段着色器,并实时查看修改的效果。


同时,Shadertoy 上有许多大佬分享他们制作的酷炫效果的代码,这些代码是完全开源的,用户可以在这些代码的基础上进行修改和学习。


除此之外,Shadertoy 还允许用户选择声音频道,将当前帧的声音信息转变成纹理(Texture),传入 shader 当中,从而根据声音信息来控制图形。这使得 Shadertoy 在视觉和听觉的结合上有了更多的可能性。


3.glsl.app



网址:https://glsl.app/


glsl.app 是一个在线的 GLSL (OpenGL Shading Language) 编辑器。GLSL 是一种用于图形渲染的着色语言,特别是在 OpenGL 图形库中。这种语言允许开发者为图形硬件编写着色器程序,这些程序可以运行在 GPU 上,用于计算图像的各种视觉效果。


在 glsl.app 上,你可以:



  • 编写和编辑着色器代码:直接在网页上编写顶点着色器、片元着色器等。

  • 实时预览:当你编写或修改着色器代码时,可以立即在右侧的预览窗口中看到效果。

  • 分享你的作品:完成你的着色器后,你可以获得一个链接,通过这个链接与其他人分享你的作品。

  • 学习:如果你是初学者,该网站还提供了很多示例和教程,帮助你了解如何编写各种着色器效果。


参考连接:



作者:前端开发爱好者
来源:juejin.cn/post/7352797634556706831
收起阅读 »

提升你的CSS技能:深入理解伪类选择器和伪元素选择器!

在CSS的世界里,有些选择器并不像它们的名字那样直接。今天,我们要探索的是两种特殊的选择器:伪类选择器和伪元素选择器。它们虽然名字相似,但功能和用途却大有不同。下面就让我们一起来了解一下它们是如何在我们的页面布局中扮演着不可或缺的角色的吧。一、伪类选择器1、什...
继续阅读 »

在CSS的世界里,有些选择器并不像它们的名字那样直接。今天,我们要探索的是两种特殊的选择器:伪类选择器和伪元素选择器。它们虽然名字相似,但功能和用途却大有不同。

下面就让我们一起来了解一下它们是如何在我们的页面布局中扮演着不可或缺的角色的吧。

一、伪类选择器

1、什么是伪类选择器

伪类选择器,顾名思义,是一种特殊的选择器,它用来选择DOM元素在特定状态下的样式。这些特定状态并不是由文档结构决定的,而是由用户行为(如点击、悬停)或元素的状态(如被访问、被禁用)来定义的。

例如,我们可以用伪类选择器来改变链接在不同状态下的颜色,从而给用户以视觉反馈。

2、伪类选择器的语法

selector:pseudo-class {
property: value;
}

a:link {
color: #FF0000;
}

input:focus {
background-color: yellow;
}

注意:伪类名称对大小写不敏感。

3、常用的伪类选择器

下面分别介绍一下比较常用几类伪类选择器:

3.1 动态伪类选择器

这类选择器主要用于描述用户与元素的交互状态。例如:

1):hover: 当鼠标悬停在元素上时的样式。

代码示例:将链接的文本颜色改为红色

a:hover {
color: red;
}

2):active:当元素被用户激活(如点击)时的样式。

代码示例:将按钮的背景色改为蓝色

button:active {
background-color: blue;
}

3):focus: 当元素获得焦点(如输入框被点击)时的样式。

代码示例:将输入框的边框颜色改为绿色

input:focus {
border-color: green;
}

4):visited: 用于设置已访问链接的样式,通常与:link一起使用来区分未访问和已访问的链接。

代码示例:将已访问链接的颜色改为紫色

a:visited {
color: purple;
}

3.2 UI元素状态伪类选择器

这类选择器用于描述元素在用户界面中的状态。例如:

1):enabled和:disabled: 用于表单元素,表示元素是否可用。

示例:将禁用的输入框的边框颜色改为灰色

input:disabled {
border-color: gray;
}

2):checked: 用于单选框或复选框,表示元素是否被选中。

示例:将选中的单选框的背景色改为黄色

input[type="radio"]:checked {
background-color: yellow;
}

3):nth-child(n): 选取父元素中第n个子元素。

示例:将列表中的奇数位置的项目的背景色改为蓝色:

li:nth-child(odd) {
background-color: blue;
}

3.4 否定伪类选择器

这类选择器用于排除符合特定条件的元素。例如:

:not(selector): 选取不符合括号内选择器的所有元素。

示例:将不是段落的元素的背景色改为灰色:

*:not(p) {
background-color: gray;
}

4、常见应用

  • 设置鼠标悬停在元素上时的样式;

  • 为已访问和未访问链接设置不同的样式;

  • 设置元素获得焦点时的样式;


// 示例:a 标签的四种状态,分别对应 4 种伪类;

/* 未访问的链接 */
a:link {
color: blue;
}

/* 已访问的链接 */
a:visited {
color: red;
}

/* 鼠标悬停链接 */
a:hover {
color: orange;
}

/* 已选择的链接(鼠标点击但不放开时) */
a:active {
color: #0000FF;
}

注意:

  • a 标签的 4 个伪类(4种状态)必须按照一定顺序书写,否则将会失效;

  • a:hover 必须在 CSS 定义中的 a:link 和 a:visited 之后,才能生效;

  • a:active 必须在 CSS 定义中的 a:hover 之后才能生效;

  • 书写顺序为:a:link、a:visited、a:hover、a:active;

  • 记忆方法:love hate - “爱恨准则”;

二、伪元素选择器

1、什么是伪元素选择器

与伪类选择器不同,伪元素选择器是用来选择DOM元素的特定部分,而不是整个元素。它们通常用于处理那些不是由HTML标签直接表示的内容,比如首行文字、首字母或者生成的内容(如内容前面的编号)。

伪元素选择器允许我们对页面上的某些部分进行精确的样式控制,而这些部分在HTML结构中并不存在。

2、伪元素选择器语法

selector::pseudo-element {
property: value;
}

p::first-line {
color: #ff0000;
}

h1::before {
content: '♥';
}

3、常用伪元素选择器

伪元素选择器并不是针对真正的元素使用的选择器,而是针对CSS中已经定义好的伪元素使用的选择器,CSS中有如下四种常用伪元素选择器:first-line、 first-letter、 before、after。

3.1 ::first-line

::first-line表示第一行(第一行内容根据屏幕大小来决定显示多少字),例如:p::first-line{}。
代码示例:

    <style>
p::first-line{
color: blue;
}
</style>

Description

3.2 ::first-letter

::first-letter表示第一个字母,例如:p::first-letter{}。

代码示例:

<style>
p::first-letter{
font-size: 30px;
color: blueviolet;
}
</style>

Description

3.3 ::before和::after

::before表示元素的开始,::after表示元素的最后,before和after必须结合content属性来使用。

代码示例:

 <style>
p::after{
content: "hahaha";
color: red;
}
p::before{
content: "hehehe";
color: coral;
}
</style>

Description

注意:

  • before和after创建一个元素,但是属于行内元素。
  • 新创建的这个元素在文档中是找不到的,所以我们称为伪元素。
  • before在父元素内容的前面创建元素,after在父元素内容的后面插入元素。
  • 伪元素选择器和标签选择器一样,权重为1。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

三、伪类与伪元素选择器的区别

CSS中的伪类选择器和伪元素选择器都是用来选取DOM中特定元素的选择器。具体区别如下:

伪类的操作对象是文档树中已有的元素,而伪元素则创建了一个文档数外的元素。因此,伪类与伪元素的区别在于:有没有创建一个文档树之外的元素;

  • 伪类本质上是为了弥补常规CSS选择器的不足,以便获取到更多信息;

  • 伪元素本质上是创建了一个有内容的虚拟容器;

  • CSS3 中伪类和伪元素的语法不同;

  • 在 CSS3 中,已经明确规定了伪类用一个冒号来表示,而伪元素则用两个冒号来表示;

  • 可以同时使用多个伪类,而只能同时使用一个伪元素。

总的来说,伪类选择器关注的是元素在特定状态下的样式变化,而伪元素选择器则是通过创建新的元素来实现特定的样式效果。两者都是CSS中非常强大的工具,可以帮助开发者实现复杂的页面布局和动态效果。

伪类选择器和伪元素选择器虽然不是真正的元素,但它们在CSS中扮演着极其重要的角色。了解并熟练运用它们,可以让你的网页更加生动、互动性更强,同时也能更好地控制页面的布局和内容的表现。

收起阅读 »

JSON非常慢:这里有更快的替代方案!

web
是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探讨 JSON 可能成为应用程序瓶颈的原因,并探索更快的替代方法和优化技术,使您...
继续阅读 »

是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探讨 JSON 可能成为应用程序瓶颈的原因,并探索更快的替代方法和优化技术,使您的应用程序保持最佳运行状态。


JSON 是什么,为什么要关心?


image.png


JSON 是 JavaScript Object Notation 的缩写,一种轻量级数据交换格式,已成为应用程序中传输和存储数据的首选。它的简单性和可读格式使开发者和机器都能轻松使用。但是,为什么要在项目中关注 JSON 呢?


JSON 是应用程序中数据的粘合剂。它是服务器和客户端之间进行数据通信的语言,也是数据库和配置文件中存储数据的格式。从本质上讲,JSON 在现代网络开发中起着举足轻重的作用。


JSON 的流行以及人们使用它的原因...


主要有就下几点:



  1. 人类可读格式:JSON 采用简单明了、基于文本的结构,便于开发人员和非开发人员阅读和理解。这种人类可读格式增强了协作,简化了调试。

  2. 语言无关:JSON 与任何特定编程语言无关。它是一种通用的数据格式,几乎所有现代编程语言都能对其进行解析和生成,因此具有很强的通用性。

  3. 数据结构一致性:JSON 使用键值对、数组和嵌套对象来实现数据结构的一致性。这种一致性使其具有可预测性,便于在各种编程场景中使用。

  4. 浏览器支持:浏览器原生支持 JSON,允许应用程序与服务器进行无缝通信。这种本地支持极大地促进了 JSON 在开发中的应用。

  5. JSON API:许多服务和应用程序接口默认以 JSON 格式提供数据。这进一步巩固了 JSON 在网络开发中作为数据交换首选的地位。

  6. JSON 模式:开发人员可以使用 JSON 模式定义和验证 JSON 数据的结构,从而为其应用程序增加一层额外的清晰度和可靠性。


鉴于这些优势,难怪全球的开发人员都依赖 JSON 来满足他们的数据交换需求。不过,随着我们深入探讨,会发现与 JSON 相关的潜在性能问题以及如何有效解决这些挑战。


对速度的需求


应用速度和响应速度的重要性


在当今快节奏的数字环境中,应用程序的速度和响应能力是不容忽视的。用户希望在网络和移动应用中即时获取信息、快速交互和无缝体验。对速度的这种要求是由多种因素驱动的:



  1. 用户期望:用户已习惯于从数字互动中获得闪电般快速的响应。他们不想等待网页加载或应用程序响应。哪怕是几秒钟的延迟,都会导致用户产生挫败感并放弃使用。

  2. 竞争优势:速度可以成为重要的竞争优势。与反应慢的应用程序相比,反应迅速的应用程序往往能更有效地吸引和留住用户。

  3. 搜索引擎排名:谷歌等搜索引擎将页面速度视为排名因素。加载速度更快的网站往往在搜索结果中排名靠前,从而提高知名度和流量。

  4. 转换率:电子商务网站尤其清楚速度对转换率的影响。网站速度越快,转换率越高,收入也就越高。

  5. 移动性能:随着移动设备的普及,对速度的需求变得更加重要。移动用户的带宽和处理能力往往有限,因此,快速的应用程序性能必不可少。


JSON 会拖慢我们的应用程序吗?


在某些情况下,JSON 可能是导致应用程序运行速度减慢的罪魁祸首。解析 JSON 数据的过程,尤其是在处理大型或复杂结构时,可能会耗费宝贵的毫秒时间。此外,低效的序列化和反序列化也会影响应用程序的整体性能


JSON 为什么会变慢


1.解析开销


JSON 数据到达应用程序后,必须经过解析过程才能转换成可用的数据结构。解析过程可能相对较慢,尤其是在处理大量或深度嵌套的 JSON 数据时。


2.序列化和反序列化


JSON 要求在从客户端向服务器发送数据时进行序列化(将对象编码为字符串),并在接收数据时进行反序列化(将字符串转换回可用对象)。这些步骤会带来开销并影响应用程序的整体速度。


在微服务架构的世界里,JSON 通常用于在服务之间传递消息。但是,JSON 消息需要序列化和反序列化,这两个过程会带来巨大的开销。



在众多微服务不断通信的情况下,这种开销可能会累积起来,有可能会使应用程序减慢到影响用户体验的程度。



image.png


3.字符串操作


JSON 以文本为基础,主要依靠字符串操作来进行连接和解析等操作。与处理二进制数据相比,字符串处理速度较慢。


4.缺乏数据类型


JSON 的数据类型(如字符串、数字、布尔值)有限。复杂的数据结构可能需要效率较低的表示方法,从而导致内存使用量增加和处理速度减慢。


image.png


5.冗长性


JSON 的人机可读设计可能导致冗长。冗余键和重复结构会增加有效载荷的大小,导致数据传输时间延长。


6.不支持二进制


JSON 缺乏对二进制数据的本地支持。在处理二进制数据时,开发人员通常需要将其编码和解码为文本,这可能会降低效率。


7.深嵌套


在某些情况下,JSON 数据可能嵌套很深,需要进行递归解析和遍历。这种计算复杂性会降低应用程序的运行速度,尤其是在没有优化的情况下。


JSON 的替代品


虽然 JSON 是一种通用的数据交换格式,但由于其在某些情况下的性能限制,开发者开始探索更快的替代格式。我们来看呓2其中的一些替代方案。


1.协议缓冲区(protobuf)


协议缓冲区(通常称为 protobuf)是谷歌开发的一种二进制序列化格式。其设计宗旨是高效、紧凑和快速。Protobuf 的二进制特性使其在序列化和反序列化时比 JSON 快得多。


何时使用:当你需要高性能数据交换时,尤其是在微服务架构、物联网应用或网络带宽有限的情况下,请考虑使用 protobuf


2. MessagePack 信息包


MessagePack 是另一种二进制序列化格式,以速度快、结构紧凑而著称。其设计目的是在保持与各种编程语言兼容的同时,提高比 JSON 更高的效率。


何时使用:当你需要在速度和跨语言兼容性之间取得平衡时,MessagePack 是一个不错的选择。它适用于实时应用程序和对减少数据量有重要要求的情况。


3. BSON(二进制 JSON)


BSON 或二进制 JSON 是一种从 JSON 衍生出来的二进制编码格式。它保留了 JSON 的灵活性,同时通过二进制编码提高了性能。BSON 常用于 MongoDB 等数据库。


何时使用:如果你正在使用 MongoDB,或者需要一种能在 JSON 和二进制效率之间架起桥梁的格式,那么 BSON 就是一个很有价值的选择。


4. Apache Avro(阿帕奇 Avro)


Apache Avro 是一个数据序列化框架,专注于提供一种紧凑的二进制格式。它基于模式,可实现高效的数据编码和解码。


何时使用:Avro 适用于模式演进非常重要的情况,如数据存储,以及需要在速度和数据结构灵活性之间取得平衡的情况。


与 JSON 相比,这些替代方案在性能上有不同程度的提升,具体选择取决于您的具体使用情况。通过考虑这些替代方案,您可以优化应用程序的数据交换流程,确保将速度和效率放在开发工作的首位。


image.png


每个字节的重要性:优化数据格式


JSON 数据


下面是我们的 JSON 数据示例片段:


{
"id": 1, // 14 bytes
"name": "John Doe", // 20 bytes
"email": "johndoe@example.com", // 31 bytes
"age": 30, // 9 bytes
"isSubscribed": true, // 13 bytes
"orders": [ // 11 bytes
{ // 2 bytes
"orderId": "A123", // 18 bytes
"totalAmount": 100.50 // 20 bytes
}, // 1 byte
{ // 2 bytes
"orderId": "B456", // 18 bytes
"totalAmount": 75.25 // 19 bytes
} // 1 byte
] // 1 byte
}

JSON 总大小: ~139 字节


JSON 功能多样,易于使用,但也有缺点,那就是它的文本性质。每个字符、每个空格和每个引号都很重要。在数据大小和传输速度至关重要的情况下,这些看似微不足道的字符可能会产生重大影响。


效率挑战:使用二进制格式减少数据大小


现在,我们提供其他格式的数据表示并比较它们的大小:


协议缓冲区 (protobuf)


syntax = "proto3";

message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_subscribed = 5;
repeated Order orders = 6;

message Order {
string order_id = 1;
float total_amount = 2;
}
}

0A 0E 4A 6F 68 6E 20 44 6F 65 0C 4A 6F 68 6E 20 44 6F 65 65 78 61 6D 70 6C 65 2E 63 6F 6D 04 21 00 00 00 05 01 12 41 31 32 33 03 42 DC CC CC 3F 05 30 31 31 32 34 34 35 36 25 02 9A 99 99 3F 0D 31 02 42 34 35 36 25 02 9A 99 99 3F

协议缓冲区总大小: ~38 字节


MessagePack


二进制表示法(十六进制):


a36a6964000000000a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d042100000005011241313302bdcccc3f0530112434353625029a99993f

信息包总大小: ~34 字节


Binary Representation (Hexadecimal):


3e0000001069640031000a4a6f686e20446f6502656d61696c006a6f686e646f65406578616d706c652e636f6d1000000022616765001f04370e4940

BSON 总大小: ~43 字节


Avro


二进制表示法(十六进制):


0e120a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d049a999940040a020b4108312e3525312e323538323539

Avro 总大小: ~32 字节


image.png


现在,你可能想知道,为什么这些格式中的某些会输出二进制数据,但它们的大小却各不相同。Avro、MessagePack 和 BSON 等二进制格式具有不同的内部结构和编码机制,这可能导致二进制表示法的差异,即使它们最终表示的是相同的数据。下面简要介绍一下这些差异是如何产生的:


1. Avro



  • Avro 使用模式对数据进行编码,这种模式通常包含在二进制表示法中。

  • Avro 基于模式的编码通过提前指定数据结构,实现了高效的数据序列化和反序列化。

  • Avro 的二进制格式设计为自描述格式,这意味着模式信息包含在编码数据中。这种自描述性使 Avro 能够保持不同版本数据模式之间的兼容性。


2. MessagePack



  • MessagePack 是一种二进制序列化格式,直接对数据进行编码,不包含模式信息。

  • 它使用长度可变的整数和长度可变的字符串的紧凑二进制表示法,以尽量减少空间使用。

  • MessagePack 不包含模式信息,因此更适用于模式已提前知晓并在发送方和接收方之间共享的情况。



3. BSON



  • BSON 是 JSON 数据的二进制编码,包括每个值的类型信息。

  • BSON 的设计与 JSON 紧密相连,但它增加了二进制数据类型,如 JSON 缺乏的日期和二进制数据。

  • 与 MessagePack 一样,BSON 不包括模式信息。


这些设计和编码上的差异导致了二进制表示法的不同:



  • Avro 包含模式信息并具有自描述性,因此二进制文件稍大,但与模式兼容。

  • MessagePack 的编码长度可变,因此非常紧凑,但缺乏模式信息,因此适用于已知模式的情况。

  • BSON 与 JSON 关系密切,并包含类型信息,与 MessagePack 等纯二进制格式相比,BSON 的大小会有所增加。


总之,这些差异源于每种格式的设计目标和特点。Avro 优先考虑模式兼容性,MessagePack 侧重于紧凑性,而 BSON 在保持类似 JSON 结构的同时增加了二进制类型。格式的选择取决于您的具体使用情况和要求,如模式兼容性、数据大小和易用性。


优化 JSON 性能


下面是一些优化 JSON 性能的实用技巧以及代码示例和最佳实践:


1.最小化数据大小



  • 使用简短的描述性键名:选择简洁但有意义的键名,以减少 JSON 对象的大小


// Inefficient
{
"customer_name_with_spaces": "John Doe"
}

// Efficient
{
"customerName": "John Doe"
}


  • 尽可能缩写:在不影响清晰度的情况下,考虑对键或值使用缩写。


// 效率低
{
"transaction_type": "purchase"
}

// 效率高
{
"txnType": "purchase"
}

2.明智使用数组



  • 尽量减少嵌套:避免深度嵌套数组,因为它们会增加解析和遍历 JSON 的复杂性。


// 效率低
{
"order": {
"items": {
"item1": "Product A",
"item2": "Product B"
}
}
}

// 效率高
{
"orderItems": ["Product A", "Product B"]
}

3.优化数字表示法


尽可能使用整数:如果数值可以用整数表示,就用整数代替浮点数。


// 效率低
{
"quantity": 1.0
}

// 效率高
{
"quantity": 1
}

4.删除冗余


避免重复数据:通过引用共享值来消除冗余数据。


// 效率低
{
"product1": {
"name": "Product A",
"price": 10
},
"product2": {
"name": "Product A",
"price": 10
}
}

// 效率高
{
"products": [
{
"name": "Product A",
"price": 10
},
{
"name": "Product B",
"price": 15
}
]
}

5.使用压缩


应用压缩算法:如果适用,在传输过程中使用 Gzipor Brotlito 等压缩算法来减小 JSON 有效负载的大小。


// 使用 zlib 进行 Gzip 压缩的 Node.js 示例
const zlib = require('zlib');

const jsonData = {
// 在这里填入你的 JSON 数据
};

zlib.gzip(JSON.stringify(jsonData), (err, compressedData) => {
if (!err) {
// 通过网络发送 compressedData
}
});


6.采用服务器端缓存:


缓存 JSON 响应:实施服务器端缓存,高效地存储和提供 JSON 响应,减少重复数据处理的需要。


7.配置文件和优化


剖析性能:使用剖析工具找出 JSON 处理代码中的瓶颈,然后优化这些部分。


实际优化:在实践中加快 JSON 的处理速度


在本节中,我们将探讨实际案例,这些案例在使用 JSON 时遇到性能瓶颈并成功克服。我们会看到诸如 LinkedIn、Auth0、Uber 等知名技术公司如何解决 JSON 的限制并改善他们应用的性能。这些案例为如何提升应用处理速度和响应性提供了实用的策略。


1.LinkedIn 的协议缓冲区集成:



  • 挑战:LinkedIn 面临的挑战是 JSON 的冗长以及由此导致的网络带宽使用量增加,从而导致延迟增加。

  • 解决方案:他们采用了 Protocol Buffers,这是一种二进制序列化格式,用以替换微服务通信中的 JSON。

  • 影响:这一优化将延迟降低了 60%,提高了 LinkedIn 服务的速度和响应能力。


2.Uber 的 H3 地理索引:


挑战:Uber 使用 JSON 来表示各种地理空间数据,但解析大型数据集的 JSON 会降低其算法速度。


解决方案:他们引入了 H3 Geo-Index,这是一种用于地理空间数据的高效六边形网格系统,可减少 JSON 解析开销。


影响:这一优化大大加快了地理空间业务的发展,增强了 Uber 的叫车和地图服务。


3.Slack 的信息格式优化:


挑战:Slack 需要在实时聊天中传输和呈现大量 JSON 格式的消息,这导致了性能瓶颈。


解决方案:他们优化了 JSON 结构,减少了不必要的数据,只在每条信息中包含必要的信息。


影响:这项优化使得消息展现更快,从而提高了 Slack 用户的整体聊天性能。


4.Auth0 的协议缓冲区实现:


挑战:Auth0 是一个流行的身份和访问管理平台,在处理身份验证和授权数据时面临着 JSON 的性能挑战。


解决方案:他们采用协议缓冲区(Protocol Buffers)来取代 JSON,以编码和解码与身份验证相关的数据。


影响:这一优化大大提高了数据序列化和反序列化的速度,从而加快了身份验证流程,并增强了 Auth0 服务的整体性能。


这些现实世界中的例子展示了通过优化策略解决 JSON 的性能挑战如何对应用程序的速度、响应速度和用户体验产生实质性的积极影响。它们强调了考虑替代数据格式和高效数据结构的重要性,以克服各种情况下与 JSON 相关的速度减慢问题。


结论


在不断变化的网络开发环境中,优化 JSON 性能是一项宝贵的技能,它能让你的项目与众不同,并确保你的应用程序在即时数字体验时代茁壮成长。


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

解锁前端难题:亲手实现一个图片标注工具

web
本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究! 业务中涉及图片的制作和审核功能,审核人员需要在图片中进行标注,并说明存在的问题,标注过程中需要支持放大缩小,移动等交互,将业务剥离,这个需求,可以定义为实现一个图...
继续阅读 »

本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究!



业务中涉及图片的制作和审核功能,审核人员需要在图片中进行标注,并说明存在的问题,标注过程中需要支持放大缩小,移动等交互,将业务剥离,这个需求,可以定义为实现一个图片标注功能。


实现这个功能并不容易,其涉及的前端知识点众多,本文带领大家从零到一,亲手实现一个,支持缩放,移动,编辑的图片标注功能,文字描述是抽象的,眼见为实,实现效果如下所示:


Kapture 2024-03-20 at 18.43.56.gif


技术方案


这里涉及两个关键功能,一个是绘制,包括缩放和旋转,一个是编辑,包括选取和修改尺寸,涉及到的技术包括,缩放,移动,和自定义形状的绘制(本文仅实现矩形),绘制形状的选取,改变尺寸和旋转角度等。


从大的技术选型来说,有两种实现思路,一种是 canvas,一种是 dom+svg,下面简单介绍下两种思路和优缺点。


canvas 可以方便实现绘制功能,但编辑功能就比较困难,当然这可以使用库来实现,这里我们考虑自己亲手实现功能。



  • 优点

    • 性能较好,尤其是在处理大型图片和复杂图形时。

    • 支持更复杂的图形绘制和像素级操作。

    • 一旦图形绘制在 Canvas 上,就不会受到 DOM 的影响,减少重绘和回流。



  • 缺点

    • 交互相对复杂,需要手动管理图形的状态和事件。

    • 对辅助技术(如屏幕阅读器)支持较差。



  • 可能遇到的困难

    • 实现复杂的交互逻辑(如选取、移动、修改尺寸等)可能比较繁琐。

    • 在缩放和平移时,需要手动管理坐标变换和图形重绘。




dom+svg 也可以实现功能,缩放和旋转可以借助 css3 的 transform。



  • 优点

    • 交互相对简单,可以利用 DOM 事件系统和 CSS。

    • 对辅助技术支持较好,有助于提高可访问性。



  • 缺点

    • 在处理大型图片和复杂图形时,性能可能不如 Canvas。

    • SVG 元素数量过多时,可能会影响页面性能。



  • 可能遇到的困难

    • 在实现复杂的图形和效果时,可能需要较多的 SVG 知识和技巧。

    • 管理大量的 SVG 元素和事件可能会使代码变得复杂。




总的来说,如果对性能有较高要求,或需要进行复杂的图形处理和像素操作,可以选择基于 Canvas 的方案。否则可以选择基于 DOM + SVG 的方案。在具体实现时,可以根据项目需求和技术栈进行选择。


下面我们选择基于 canvas 的方案,通过例子,一步一步实现完成功能,让我们先从最简单的开始。


渲染图片


本文我们不讲解 canvas 基础,如果你不了解 canvas,可以先在网上找资料,简单学习下,图片的渲染非常简单,只用到一个 API,这里我们直接给出代码,示例如下:


这里我们提前准备一个 canvas,宽高设定为 1000*700,这里唯一的一个知识点就是要在图片加载完成后再绘制,在实战中,需要注意绘制的图片不能跨域,否则会绘制失败。


<body>
<div>
<canvas id="canvas1" width="1000" height="700"></canvas>
</div>
<script>
const canvas1 = document.querySelector('#canvas1');
const ctx1 = canvas1.getContext('2d');
let width = 1000;
let height = 700;

let img = new Image();
img.src = './bg.png';
img.onload = function () {
draw();
};

function draw() {
console.log('draw');
ctx1.drawImage(img, 0, 0, width, height);
}
</script>
</body>

现在我们已经成功在页面中绘制了一张图片,这非常简单,让我们继续往下看吧。


缩放


实现图片缩放功能,我们需要了解两个关键的知识点:如何监听缩放事件和如何实现图片缩放。


先来看第一个,我用的是 Mac,在 Mac 上可以通过监听鼠标的滚轮事件来实现缩放的监听。当用户使用鼠标滚轮时,会触发 wheel 事件,我们可以通过这个事件的 deltaY 属性来判断用户是向上滚动(放大)还是向下滚动(缩小)。


可以看到在 wheel 事件中,我们修改了 scale 变量,这个变量会在下面用到。这里添加了对最小缩放是 1,最大缩放是 3 的限制。


document.addEventListener(
'wheel',
function (event) {
if (event.ctrlKey) {
// detect pinch
event.preventDefault(); // prevent zoom
if (event.deltaY < 0) {
console.log('Pinching in');
if (scale < 3) {
scale = Math.min(scale + 0.1, 3);
draw();
}
} else {
console.log('Pinching out');
if (scale > 1) {
scale = Math.max(scale - 0.1, 1);
draw();
}
}
}
},
{ passive: false }
);

图片缩放功能,用到了 canvas 的 scale 函数,其可以修改绘制上下文的缩放比例,示例代码如下:


我们添加了clearRect函数,这用来清除上一次绘制的图形,当需要重绘时,就需要使用clearRect函数。


这里需要注意开头和结尾的 save 和 restore 函数,因为我们会修改 scale,如果不恢复的话,其会影响下一次绘制,一般在修改上下文时,都是通过 save 和 restore 来复原的。


let scale = 1;

function draw() {
console.log('draw');
ctx1.clearRect(0, 0, width, height);
ctx1.save();
ctx1.scale(scale, scale);
ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

这里稍微解释一下 scale 函数,初次接触,可能会不太好理解。在 Canvas 中使用 scale 函数时,重要的是要理解它实际上是在缩放绘图坐标系统,而不是直接缩放绘制的图形。当你调用 ctx.scale(scaleX, scaleY) 时,你是在告诉 Canvas 之后的所有绘图操作都应该在一个被缩放的坐标系统中进行。


这意味着,如果你将缩放比例设置为 2,那么在这个缩放的坐标系统中,绘制一个宽度为 50 像素的矩形,实际上会在画布上产生一个宽度为 100 像素的矩形。因为在缩放的坐标系统中,每个单位长度都变成了原来的两倍。


因此,当我们谈论 scale 函数时,重点是要记住它是在缩放整个绘图坐标系统,而不是单独的图形。这就是为什么在使用 scale 函数后,所有的绘图操作(包括位置、大小等)都会受到影响。


现在我们已经实现了图片的缩放功能,效果如下所示:


Kapture 2024-03-21 at 15.20.58.gif


鼠标缩放


细心的你可能发现上面的缩放效果是基于左上角的,基于鼠标点缩放意味着图片的缩放中心是用户鼠标所在的位置,而不是图片的左上角或其他固定点。这种缩放方式更符合用户的直觉,可以提供更好的交互体验。


为了实现这种效果,可以使用 tanslate 来移动原点,canvas 中默认的缩放原点是左上角,具体方法是,可以在缩放前,将缩放原点移动到鼠标点的位置,缩放后,再将其恢复,这样就不会影响后续的绘制,实现代码如下所示:


let scaleX = 0;
let scaleY = 0;

function draw() {
ctx1.clearRect(0, 0, width, height);
ctx1.save();
// 注意这行1
ctx1.translate(scaleX, scaleY);
ctx1.scale(scale, scale);
// 注意这行2
ctx1.translate(-scaleX, -scaleY);
ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

scaleX 和 scaleY 的值,可以在缩放的时候设置即可,如下所示:


// zoom
document.addEventListener(
'wheel',
function (event) {
if (event.ctrlKey) {
if (event.deltaY < 0) {
if (scale < 3) {
// 注意这里两行
scaleX = event.offsetX;
scaleY = event.offsetY;
scale = Math.min(scale + 0.1, 3);
draw();
}
}
// 省略代码
}
},
{ passive: false }
);

现在我们已经实现了图片的鼠标缩放功能,效果如下所示:


3.gif


移动视口


先解释下放大时,可见区域的概念,好像叫视口吧
当处于放大状态时,会导致图像只能显示一部分,此时需要能过需要可以移动可见的图像,
这里选择通过触摸板的移动,也就是 wheel 来实现移动视口


通过 canvas 的 translate 来实现改变视口


在图片放大后,整个图像可能无法完全显示在 Canvas 上,此时只有图像的一部分(即可见区域)会显示在画布上。这个可见区域也被称为“视口”。为了查看图像的其他部分,我们需要能够移动这个视口,即实现图片的平移功能。


在放大状态下,视口的大小相对于整个图像是固定的,但是它可以在图像上移动以显示不同的部分。你可以将视口想象为一个固定大小的窗口,你通过这个窗口来观察一个更大的图像。当你移动视口时,窗口中显示的图像部分也会相应改变。


为了实现移动视口,我们可以通过监听触摸板的移动事件(也就是 wheel 事件)来改变视口的位置。当用户通过触摸板进行上下或左右滑动时,我们可以相应地移动视口,从而实现图像的平移效果。


我们可以使用 Canvas 的 translate 方法来改变视口的位置。translate 方法接受两个参数,分别表示沿 x 轴和 y 轴移动的距离。在移动视口时,我们需要更新图片的位置,并重新绘制图像以反映新的视口位置。


代码改动如下所示:


let translateX = 0;
let translateY = 0;

function draw() {
// 此处省略代码
// 改变视口
ctx1.translate(translateX, translateY);

ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

// translate canvas
document.addEventListener(
"wheel",
function (event) {
if (!event.ctrlKey) {
// console.log("translate", event.deltaX, event.deltaY);
event.preventDefault();
translateX -= event.deltaX;
translateY -= event.deltaY;
draw();
}
},
{ passive: false }
);

在这个示例中,translateXtranslateY 表示视口的位置。当用户通过触摸板进行滑动时,我们根据滑动的方向和距离更新视口的位置,并重新绘制图像。通过这种方式,我们可以实现图像的平移功能,允许用户查看图像的不同部分。


现在我们已经实现了移动视口功能,效果如下所示:


4.gif


绘制标注


为了便于大家理解,这里我们仅实现矩形标注示例,实际业务中可能存在各种图形的标记,比如圆形,椭圆,直线,曲线,自定义图形等。


我们先考虑矩形标注的绘制问题,由于 canvas 是位图,我们需要在 js 中存储矩形的数据,矩形的存储需要支持坐标,尺寸,旋转角度和是否在编辑中等。因为可能存在多个标注,所以需要一个数组来存取标注数据,我们将标注存储在reacts中,示例如下:


let rects = [
{
x: 650,
y: 350,
width: 100,
height: 100,
isEditing: false,
rotatable: true,
rotateAngle: 30,
},
];

下面将 rects 渲染到 canvas 中,示例代码如下:


代码扩机并不复杂,比较容易理解,值得一提的rotateAngle的实现,我们通过旋转上下文来实现,其旋转中心是矩形的图形的中心点,因为操作上线文,所以在每个矩形绘制开始和结束后,要通过saverestore来恢复之前的上下文。


isEditing表示当前的标注是否处于编辑状态,在这里编辑中的矩形框,我们只需设置不同的颜色即可,在后面我们会实现编辑的逻辑。


function draw() {
// 此处省略代码
ctx1.drawImage(img, 0, 0, width, height);

rects.forEach((r) => {
ctx1.strokeStyle = r.isEditing ? 'rgba(255, 0, 0, 0.5)' : 'rgba(255, 0, 0)';

ctx1.save();
if (r.rotatable) {
ctx1.translate(r.x + r.width / 2, r.y + r.height / 2);
ctx1.rotate((r.rotateAngle * Math.PI) / 180);
ctx1.translate(-(r.x + r.width / 2), -(r.y + r.height / 2));
}
ctx1.strokeRect(r.x, r.y, r.width, r.height);
ctx1.restore();
});

ctx1.restore();
}

现在我们已经实现了标注绘制功能,效果如下所示:


5.png


添加标注


为了在图片上添加标注,我们需要实现鼠标按下、移动和抬起时的事件处理,以便在用户拖动鼠标时动态地绘制一个矩形标注。同时,由于视口可以放大和移动,我们还需要进行坐标的换算,确保标注的位置正确。


首先,我们需要定义一个变量 drawingRect 来存储正在添加中的标注数据。这个变量将包含标注的起始坐标、宽度和高度等信息:


let drawingRect = null;

接下来,我们需要实现鼠标按下、移动和抬起的事件处理函数:


mousedown中我们需要记录鼠标按下时,距离视口左上角的坐标,并将其记录到全局变量startXstartY中。


mousemove时,需要更新当前在绘制矩形的数据,并调用draw完成重绘。


mouseup时,需要处理添加操作,将矩形添加到rects中,在这里我做了一个判断,如果矩形的宽高小于 1,则不添加,这是为了避免在鼠标原地点击时,误添加图形的问题。


let startX = 0;
let startY = 0;
canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

console.log('mousedown', e.offsetX, e.offsetY, x, y);

drawingRect = drawingRect || {};
});
canvas1.addEventListener('mousemove', (e) => {
// 绘制中
if (drawingRect) {
drawingRect = computeRect({
x: startX,
y: startY,
width: e.offsetX - startX,
height: e.offsetY - startY,
});
draw();
return;
}
});
canvas1.addEventListener('mouseup', (e) => {
if (drawingRect) {
drawingRect = null;
// 如果绘制的矩形太小,则不添加,防止原地点击时添加矩形
// 如果反向绘制,则调整为正向
const width = Math.abs(e.offsetX - startX);
const height = Math.abs(e.offsetY - startY);
if (width > 1 || height > 1) {
const newrect = computeRect({
x: Math.min(startX, e.offsetX),
y: Math.min(startY, e.offsetY),
width,
height,
});
rects.push(newrect);
draw();
}
return;
}
});

下面我们来重点讲讲上面的computexycomputeRect函数,由于视口可以放大和移动,我们需要将鼠标点击时的视口坐标换算为 Canvas 坐标系的坐标。


宽高的计算比较简单,只需要将视口坐标除以缩放比例即可得到。但坐标的计算并不简单,这里通过视口坐标,直接去推 canvas 坐标是比较困难的,我们可以求出 canvas 坐标计算视口坐标的公式,公式推导如下:


vx: 视口坐标
x: canvas坐标
scale: 缩放比例
scaleX: 缩放原点
translateX: 视口移动位置

我们x会在如下视口操作后进行渲染成vx:
1: ctx1.translate(scaleX, scaleY);
2: ctx1.scale(scale, scale);
3: ctx1.translate(-scaleX, -scaleY);
4: ctx1.translate(translateX, translateY);

根据上面的步骤,每一步vx的推演如下:
1: vx = x + scaleX
2: vx = x * scale + scaleX
3: vx = x * scale + scaleX - scaleX * scale
4: vx = x * scale + scaleX - scaleX * scale + translateX * scale

通过上面 vx 和 x 的公式,我们可以计算出来 x 和 vx 的关系如下,我在这里走了很多弯路,导致计算的坐标一直不对,不要试图通过 vx 直接推出 x,一定要通过上面的公式来推导:


x = (vx - scaleX * (1 - scale) - translateX * scale) / scale

理解了上面坐标和宽高的计算公式,下面的代码就好理解了:


function computexy(x, y) {
const xy = {
x: (x - scaleX * (1 - scale) - translateX * scale) / scale,
y: (y - scaleY * (1 - scale) - translateY * scale) / scale,
};
return xy;
}
function computewh(width, height) {
return {
width: width / scale,
height: height / scale,
};
}
function computeRect(rect) {
const cr = {
...computexy(rect.x, rect.y),
...computewh(rect.width, rect.height),
};
return cr;
}

最后,我们需要一个函数来绘制标注矩形:


function draw() {
// 此处省略代码
if (drawingRect) {
ctx1.strokeRect(
drawingRect.x,
drawingRect.y,
drawingRect.width,
drawingRect.height
);
}
ctx1.restore();
}

现在我们已经实现了添加标注功能,效果如下所示:


6.gif


选取标注


判断选中,将视口坐标,转换为 canvas 坐标,遍历矩形,判断点在矩形内部
同时需要考虑点击空白处,清空选中状态
选中其他元素时,清空上一个选中的元素
渲染选中状态,选中状态改变边的颜色,为了明显,红色变为绿色
要是先选取元素的功能,关键要实现的判断点在矩形内部,判断点在矩形内部的逻辑比较简单,我们可以抽象为如下函数:


function poInRect({ x, y }, rect) {
return (
x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height
);
}

在点击事件中,我们拿到的是视口坐标,首先将其转换为 canvas 坐标,然后遍历矩形数组,判断是否有中选的矩形,如果有的话将其存储下来。


还需要考虑点击新元素时,和点击空白时,重置上一个元素的选中态的逻辑,代码实现如下所示:


canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

const pickRect = rects.find((r) => {
return poInRect({ x, y }, r);
});

if (pickRect) {
if (editRect && pickRect !== editRect) {
// 选择了其他矩形
editRect.isEditing = false;
editRect = null;
}
pickRect.isEditing = true;
editRect = pickRect;
draw();
} else {
if (editRect) {
editRect.isEditing = false;
editRect = null;
draw();
}
drawingRect = drawingRect || {};
}
});

现在我们已经实现了选取标注功能,效果如下所示:


7.gif


移动


接下来是移动,也就是通过拖拽来改变已有图形的位置
首先需要一个变量来存取当前被拖拽元素,在 down 和 up 时更新这个元素
要实现拖拽,需要一点小技巧,在点击时,计算点击点和图形左上角的坐标差,在每次 move 时,用当前坐标减去坐标差即可
不要忘了将视口坐标,换算为 canvas 坐标哦


接下来,我们将实现通过拖拽来改变已有标注的位置的功能。这需要跟踪当前被拖拽的标注,并在鼠标移动时更新其位置。


首先,我们需要一个变量来存储当前被拖拽的标注:


let draggingRect = null;

在鼠标按下时(mousedown 事件),我们需要判断是否点击了某个标注,并将其设置为被拖拽的标注,并在鼠标抬起时(mouseup 事件),将其置空。


要实现完美的拖拽效果,需要一点小技巧,在点击时,计算点击点和图形左上角的坐标差,将其记录到全局变量shiftXshiftY,关键代码如下所示。


let shiftX = 0;
let shiftY = 0;
canvas1.addEventListener('mousedown', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

if (pickRect) {
// 计算坐标差
shiftX = x - pickRect.x;
shiftY = y - pickRect.y;
// 标记当前拖拽元素
draggingRect = pickRect;
draw();
}
});
canvas1.addEventListener('mouseup', (e) => {
if (draggingRect) {
// 置空当前拖拽元素
draggingRect = null;
return;
}
});

在鼠标移动时(mousemove 事件),如果有标注被拖拽,则更新其位置,关键代码如下所示。


canvas1.addEventListener('mousemove', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

// 当前正在拖拽矩形
if (draggingRect) {
draggingRect.x = x - shiftX;
draggingRect.y = y - shiftY;
draw();
return;
}
});

现在我们已经实现了移动功能,效果如下所示:


8.gif


修改尺寸


为了实现标注尺寸的修改功能,我们可以在标注的四个角和四条边的中点处显示小方块作为编辑器,允许用户通过拖拽这些小方块来改变标注的大小。


首先,我们需要实现编辑器的渲染逻辑。我们可以在 drawEditor 函数中添加代码来绘制这些小方块。


在这里,我们使用 computeEditRect 函数来计算标注的八个编辑点的位置,并在 drawEditor 函数中绘制这些小方块,关键代码如下所示:


在这个例子中,我们只展示了上边中间编辑点的处理逻辑,其他编辑点的处理逻辑类似。


function computeEditRect(rect) {
let width = 10;
let linelen = 16;
return {
t: {
type: "t",
x: rect.x + rect.width / 2 - width / 2,
y: rect.y - width / 2,
width,
height: width,
},
b: {// 代码省略},
l: {// 代码省略},
r: {// 代码省略},
tl: {// 代码省略},
tr: {// 代码省略},
bl: {// 代码省略},
br: {// 代码省略},
};
}
function drawEditor(rect) {
ctx1.save();
const editor = computeEditRect(rect);
ctx1.fillStyle = "rgba(255, 150, 150)";

// 绘制矩形
for (const r of Object.values(editor)) {
ctx1.fillRect(r.x, r.y, r.width, r.height);
}
ctx1.restore();
}
function draw() {
rects.forEach((r) => {
// 添加如下代码
if (r.isEditing) {
drawEditor(r);
}
});
}

接下来,我们需要实现拖动这些编辑点来改变标注大小的功能。首先,我们需要在鼠标按下时判断是否点击了某个编辑点。


在这里,我们使用 poInEditor 函数来判断鼠标点击的位置是否接近某个编辑点。如果是,则设置 startEditRect, dragingEditor, editorShiftXY 来记录正在调整大小的标注和编辑点。


let startEditRect = null;
let dragingEditor = null;
let editorShiftX = 0;
let editorShiftY = 0;
function poInEditor(point, rect) {
const editor = computeEditRect(rect);
if (!editor) return;

for (const edit of Object.values(editor)) {
if (poInRect(point, edit)) {
return edit;
}
}
}
canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

if (editRect) {
const editor = poInEditor({ x, y }, editRect);
if (editor) {
// 调整大小
startEditRect = { ...editRect };
dragingEditor = editor;
editorShiftX = x - editor.x;
editorShiftY = y - editor.y;
return;
}
}
});

然后,在鼠标移动时,我们需要根据拖动的编辑点来调整标注的大小。


在这个例子中,我们只展示了上边中间编辑点的处理逻辑,其他编辑点的处理逻辑类似。通过拖动不同的编辑点,我们可以实现标注的不同方向和维度的大小调整。


canvas1.addEventListener('mousemove', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

// 如果存在编辑中的元素
if (editRect) {
const editor = poInEditor({ x, y }, editRect);
// 调整大小中
if (dragingEditor) {
const moveX = (e.offsetX - startX) / scale;
const moveY = (e.offsetY - startY) / scale;

switch (dragingEditor.type) {
case 't':
editRect.y = startEditRect.y + moveY;
editRect.height = startEditRect.height - moveY;
break;
}
draw();
return;
}
}
});

现在我们已经实现了修改尺寸功能,效果如下所示:


9.gif


旋转


实现旋转编辑器的渲染按钮,在顶部增加一个小方块的方式来实现,


旋转图形会影响选中图形的逻辑,即点在旋转图形里的判断,这块的逻辑需要修改


接下来实现旋转逻辑,会涉及 mousedown 和 mousemove


接下来介绍旋转,这一部分会有一定难度,涉及一些数学计算,而且旋转逻辑会修改多出代码,下面我们依次介绍。


旋转涉及两大块功能,一个是旋转编辑器,一个是旋转逻辑,我们先来看旋转编辑器,我们可以在标注的顶部增加一个用于旋转的小方块作为旋转编辑器,如下图所示:


image.png


下面修改我们的drawEditorcomputeEditRect函数,增加渲染逻辑,涉及一个方块和一条线的渲染。


其中rotr就是顶部的方块,rotl是那条竖线。


function computeEditRect(rect) {
let width = 10;
let linelen = 16;
return {
...(rect.rotatable
? {
rotr: {
type: 'rotr',
x: rect.x + rect.width / 2 - width / 2,
y: rect.y - width / 2 - linelen - width,
width,
height: width,
},
rotl: {
type: 'rotl',
x1: rect.x + rect.width / 2,
y1: rect.y - linelen - width / 2,
x2: rect.x + rect.width / 2,
y2: rect.y - width / 2,
},
}
: null),
};
}
function drawEditor(rect) {
ctx1.save();
const editor = computeEditRect(rect);
ctx1.fillStyle = 'rgba(255, 150, 150)';
const { rotl, rotr, ...rects } = editor;

// 绘制旋转按钮
if (rect.rotatable) {
ctx1.fillRect(rotr.x, rotr.y, rotr.width, rotr.height);
ctx1.beginPath();
ctx1.moveTo(rotl.x1, rotl.y1);
ctx1.lineTo(rotl.x2, rotl.y2);
ctx1.stroke();
}

// 绘制矩形
// ...
}

在实现旋转逻辑之前,先来看一个问题,如下图所示,当我们在绿色圆圈区按下鼠标时,在我们之前的逻辑中,也会触发选中状态。


image.png


这是因为我们判断点在矩形内部的逻辑,并未考虑旋转的问题,我们的矩形数据存储了矩形旋转之前的坐标和旋转角度,如下所示。


let rects = [
{
x: 650,
y: 350,
width: 100,
height: 100,
isEditing: false,
rotatable: true,
rotateAngle: 30,
},
];

解决这个问题有两个思路,一个是将旋转后矩形的四个点坐标计算出来,这种方法比较麻烦。另一个思路是逆向的,将要判断的点,以矩形的中点为中心,做逆向旋转,计算出其在 canvas 中的坐标,这个坐标,可以继续参与我们之前点在矩形内的计算。


关键代码如下所示,其中rotatePoint是计算 canvas 中的坐标,poInRotRect判断给定点是否在旋转矩形内部。


// 将点绕 rotateCenter 旋转 rotateAngle 度
function rotatePoint(point, rotateCenter, rotateAngle) {
let dx = point.x - rotateCenter.x;
let dy = point.y - rotateCenter.y;

let rotatedX =
dx * Math.cos((-rotateAngle * Math.PI) / 180) -
dy * Math.sin((-rotateAngle * Math.PI) / 180) +
rotateCenter.x;
let rotatedY =
dy * Math.cos((-rotateAngle * Math.PI) / 180) +
dx * Math.sin((-rotateAngle * Math.PI) / 180) +
rotateCenter.y;

return { x: rotatedX, y: rotatedY };
}

function poInRotRect(
point,
rect,
rotateCenter = {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
},
rotateAngle = rect.rotateAngle
) {
if (rotateAngle) {
const rotatedPoint = rotatePoint(point, rotateCenter, rotateAngle);
const res = poInRect(rotatedPoint, rect);
return res;
}
return poInRect(point, rect);
}

接下来实现旋转逻辑,这需要改在 mousedown 和 mousemove 事件,实现拖动时的实时旋转。


在 mousedown 时,判断如果点击的是旋转按钮,则将当前矩形记录到全局变量rotatingRect


canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

if (editRect) {
const editor = poInEditor({ x, y }, editRect);
if (editor) {
// 调整旋转
if (editor.type === 'rotr') {
rotatingRect = editRect;
prevX = e.offsetX;
prevY = e.offsetY;
return;
}
// 调整大小
}
}
});

在 mousemove 时,判断如果是位于旋转按钮上,则计算旋转角度。


canvas1.addEventListener('mousemove', (e) => {
// 绘制中
const { x, y } = computexy(e.offsetX, e.offsetY);
// 当前正在拖拽矩形

// 如果存在编辑中的元素
if (editRect) {
const editor = poInEditor({ x, y }, editRect);
console.log('mousemove', editor);

// 旋转中
if (rotatingRect) {
const relativeAngle = getRelativeRotationAngle(
computexy(e.offsetX, e.offsetY),
computexy(prevX, prevY),
{
x: editRect.x + editRect.width / 2,
y: editRect.y + editRect.height / 2,
}
);
console.log('relativeAngle', relativeAngle);
editRect.rotateAngle += (relativeAngle * 180) / Math.PI;
prevX = e.offsetX;
prevY = e.offsetY;
draw();
return;
}

// 调整大小中
}
});

将拖拽移动的距离,转换为旋转的角度,涉及一些数学知识,其原理是通过上一次鼠标位置和本次鼠标位置,计算两个点和旋转中心(矩形的中心)三个点,形成的夹角,示例代码如下:


function getRelativeRotationAngle(point, prev, center) {
// 计算上一次鼠标位置和旋转中心的角度
let prevAngle = Math.atan2(prev.y - center.y, prev.x - center.x);

// 计算当前鼠标位置和旋转中心的角度
let curAngle = Math.atan2(point.y - center.y, point.x - center.x);

// 得到相对旋转角度
let relativeAngle = curAngle - prevAngle;

return relativeAngle;
}

现在我们已经实现了旋转功能,效果如下所示:


10.gif


总结


在本文中,我们一步一步地实现了一个功能丰富的图片标注工具。从最基本的图片渲染到复杂的标注编辑功能,包括缩放、移动、添加标注、选择标注、移动标注、修改标注尺寸、以及标注旋转等,涵盖了图片标注工具的核心功能。


通过这个实例,我们可以看到,实现一个前端图片标注工具需要综合运用多种前端技术和知识,包括但不限于:



  • Canvas API 的使用,如绘制图片、绘制形状、图形变换等。

  • 鼠标事件的处理,如点击、拖拽、滚轮缩放等。

  • 几何计算,如点是否在矩形内、旋转角度的计算等。


希望这个实例能够为你提供一些启发和帮助,让你在实现自己的图片标注工具时有一个参考和借鉴。


更进一步


站在文章的角度,到此为止,下面让我们站在更高的维度思考更进一步的可能,我们还能继续做些什么呢?


在抽象层面,我们可以考虑将图片标注工具的核心功能进行进一步的抽象和封装,将其打造成一个通用的开源库。这样,其他开发者可以直接使用这个库来快速实现自己的图片标注需求,而无需从零开始。为了实现这一目标,我们需要考虑以下几点:



  • 通用性:库应该支持多种常见的标注形状和编辑功能,以满足不同场景的需求。

  • 易用性:提供简洁明了的 API 和文档,使得开发者能够轻松集成和使用。

  • 可扩展性:设计上应该留有足够的灵活性,以便开发者可以根据自己的特定需求进行定制和扩展。

  • 性能优化:注重性能优化,确保库在处理大型图片或复杂标注时仍能保持良好的性能。


在产品层面,我们可以基于这个通用库,进一步开发成一个功能完备的图片标注工具,提供开箱即用的体验。这个工具可以包括以下功能:



  • 多种标注类型:支持矩形、圆形、多边形等多种标注类型。

  • 标注管理:提供标注的增加、删除、编辑、保存等管理功能。

  • 导出和分享:支持导出标注结果为各种格式,如 JSON、XML 等,以及分享给他人协作编辑。

  • 用户界面:提供友好的用户界面,支持快捷键操作,提高标注效率。

  • 集成与扩展:支持与其他系统或工具的集成,提供 API 接口和插件机制,以便进行功能扩展。


通过不断地迭代和优化,我们可以使这个图片标注工具成为业界的标杆,为用户提供高效便捷的标注体验。


感谢您的阅读和关注!希望这篇文章能够为您在前端开发中实现图像标注功能提供一些有价值的见解和启发。如果您有任何问题、建议或想要分享自己的经验,欢迎在评论区留言交流。让我们一起探索更多前端技术的可能性,不断提升我们的技能和创造力!


本文示例源码:github.com/yanhaijing/…


本文示例预览:yanhaijing.com/imagic/demo…


作者:颜海镜
来源:juejin.cn/post/7350954669742768147
收起阅读 »

Window.print() 实现浏览器打印

web
前言 由于在公司项目中有打印的具体业务场景,在查询相关资料后,找到了 Window.print() 是用来打印的方法,写下这篇文章供自己和大家查漏补缺。 语法 window.print(); 该方法没有参数和返回值,在页面中直接调用,将直接打印整个页面,具体...
继续阅读 »

前言


由于在公司项目中有打印的具体业务场景,在查询相关资料后,找到了 Window.print() 是用来打印的方法,写下这篇文章供自己和大家查漏补缺。


语法


window.print();

该方法没有参数和返回值,在页面中直接调用,将直接打印整个页面,具体使用如下面代码所示



在点击打印该页面按钮后,会触发浏览器的打印对话框,对话框里有一些配置项,可以设置打印的相关参数。


image.png


根据上面的方法,我们就可以实现在浏览器中打印页面。


但是实际的开发中,我们的业务场景比这更加复杂。例如:只打印某个 DOM 元素,需要根据用户需求调整纸张的大小和形状,调整打印的布局,字体大小,缩放比例等等。这些都是常见的情况,那我们应该怎么做呢?


使用 @media 媒体查询


媒体查询 MDN解释:
@media CSS @ 规则可用于基于一个或多个媒体查询的结果来应用样式表的一部分。使用它,你可以指定一个媒体查询和一个 CSS 块,当且仅当该媒体查询与正在使用其内容的设备匹配时,该 CSS 块才能应用于该文档。


简单来说就是使用媒体查询根据不同的条件来决定应用不同的样式。具体到我们的需求就是,在打印时,使用专门的打印样式,隐藏其他元素,实现只打印某个元素的效果



使用样式表


在你的 标签中添加 link 元素,倒入专门供打印使用的样式表。你可以在样式表中编写打印是的具体样式。


<link href="/media/css/print.css" media="print" rel="stylesheet" />

打印页面时常用的一些样式


利用 print 设置打印页面的样式,利用 page 设置打印文档的纸张配置


 /* print.css */
@media print {
* {
-webkit-print-color-adjust: exact !important; /* 保证打印出来颜色与页面一致 */
}
.no-break {
page-break-inside: avoid; /* 避免元素被剪切 */
}
.no-print {
display: none; /* 不想打印的元素设置隐藏 */
}

@page {
size: A4 prtrait; /* 设置打印纸张尺寸及打印方向:A4,纵向打印 */
margin-top: 3cm /* 利用 margin 设置页边距 */
}

}

使用 iframe 实现更加精细的打印


我们也可以将想要打印的内容在 iframe 中渲染出来并打印,通过创建一个隐藏的 iframe 元素,将想要打印的内容放入其中,然后触发 iframe 的打印功能,实现更加灵活的打印。伪代码如下所示:


<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Use ifame print example</title>
<script>
function printPage() {
// 新建一个 iframe 元素并隐藏
const iframe = document.createElement("iframe");
iframe.style.position = "fixed";
iframe.style.right = "0";
iframe.style.bottom = "0";
iframe.style.width = "0";
iframe.style.height = "0";
iframe.style.border = "0";
// 将它添加到 document 中
document.body.appendChild(hideFrame);

// 拿到新创建的 iframe 的文档流
const iframeDocument = iframe.contentDocument;

// 在 iframe 中新建一个 link 标签,引入专门用于打印的样式表
const printCssLink = iframeDocument.createElement('link')
printCssLink.rel = 'stylesheet';
printCssLink.type = 'text/css';
printCssLink.media = 'print';
printCssLink.href = `medis/css/print.css`;
iframeDocument.head.appendChild(printCssLink);

// 在 iframe 中新建一个容器,用来存放你想要打印的元素
let printContainer = iframeDocument.createElement('div');
iframeDocument.body.appendChild(printContainer);

iframeDocument.close(); // 关闭 iframe 文档流

// 获取 iframe 的 window 对象
const iframeWindow = iframe.contentWindow;
// 当 iframe 内容加载完成后触发打印功能。
iframeWindow.onload = function() {
iframeWindow.print();
}
// 打印完后移除 iframe 元素
document.body.removeChild(iframe);
}
</script>
</head>
<body>
<p>
<span onclick="printPage();">
Print external page!
</span>
</p>
</body>
</html>

以上就是关于我在使用 Window.print() 打印页面的一些总结,欢迎大家有问题在评论区讨论学习。


作者:It_Samba
来源:juejin.cn/post/7267091417021628475
收起阅读 »

如何正确编写一个占满全部可视区域的组件(hero component)?

web
什么是 hero component hero component 或者 hero image 是一个网页设计术语,用于描述欢迎访问者访问网页的全屏视频、照片、插图或横幅。该图像始终位于网页顶部附近的显着位置,通常会在屏幕上延伸整个宽度。 我们经常见到这种...
继续阅读 »

什么是 hero component



hero component 或者 hero image 是一个网页设计术语,用于描述欢迎访问者访问网页的全屏视频、照片、插图或横幅。该图像始终位于网页顶部附近的显着位置,通常会在屏幕上延伸整个宽度。



我们经常见到这种 hero component,在视觉上占据了整个视口的宽度和高度。比如特斯拉的官网:


image.png


如何实现一个 hreo component?


很多人可能会不假思索的写出下面的 css 代码:


.hero {
width: 100vw;
height: 100vh;
}

你写完了这样的代码,满意地提测后。测试同学走过来告诉你:你的代码在苹果手机上不好使了!


image.png


如图所示,hreo component 被搜索栏挡住了。


又是 safari 的问题!我们可以先停止关于 Is Safari the new Internet Explorer? 的辩论,看看问题的成因和解决办法。


什么是视口单位


vh 单位最初存在时被定义为:等于初始包含块高度的 1%。



  • vw = 视口尺寸宽度的 1%。

  • vh = 视口大小高度的 1%。


将元素的宽度设置为 100vw,高度设置为 100vh,它就会完全覆盖视口。这就是上面 hero component 实现的基本原理:


image.png
这看起来非常完美,但在移动设备上。苹果手机的工程师觉得我们应该最大化利用手机浏览器的空间,于是在 Safari 上引入了动态工具栏,动态工具栏会随着用户的滑动而收起。


页面会表现为:高度为 100vh 的元素将从视口中溢出


image.png


当页面向下滚动时,动态工具栏会收起。在这种状态下,高度设为 100vh 的元素将覆盖整个视口。


image.png



图片来自于:大型、小型和动态视口单元



svh / lvh / dvh


为了解决上面提到的问题,2019 年,一个新的 CSS 提案诞生了。



上面的解释来自于 MDN,读起来有点拗口,其实顾名思义,再结合下面的动图就很好理解:
dvh.gif



在 tailwindcss 的文档中,对 dvh 有一个漂亮的动画演示:tailwindcss.com/blog/tailwi…



用以上的属性可以完美解决上面的 safari 中视口大小的问题,值得一提的是,有的人会建议始终使用 dvh 代替 vh。从上面的动图可以看到,dvh 在手机上其实会有个延迟和跳跃。所以是否使用dvh还是看业务的实际场景,就我而言,上面的 hero component 的例子使用 lvh 更合适。


兼容性


你可以在 can I use 中查看兼容性:


image.png


可以看到,这三个 css 属性还算是比较新的特性,如果为了兼容旧浏览器,最好是把 vh 也加上:


.hero {
width: 100vw;
height: 100vh;
height: 100dvh;
}

这样,即使在不支持新特性的浏览器中,也会降级到 vh 的效果。


感谢阅读本文~


作者:李章鱼
来源:juejin.cn/post/7352079427863592971
收起阅读 »

如何完成一个完全不依赖客户端时间的倒计时

web
前言 最近在做一个调查问卷系统,其中有一个需求就是倒计 40 分钟以后自动提交问卷。由于 UI 库使用的是 antd,所以我第一反应是使用 antd 的 CountDown 组件。 于是我就愉快的写出以下代码: import { Statistic } fro...
继续阅读 »

前言


最近在做一个调查问卷系统,其中有一个需求就是倒计 40 分钟以后自动提交问卷。由于 UI 库使用的是 antd,所以我第一反应是使用 antdCountDown 组件。
于是我就愉快的写出以下代码:


import { Statistic } from 'antd';
const { Countdown } = Statistic;

const TOTAL_TIME = 40;
const deadline = dayjs(startTime).add(TOTAL_TIME, 'minute').valueOf();


function TitleAndCountDown() {
useEffect(() => {
if (currentTime >= deadline) {
onFinish();
}
}, []);

return (
<Countdown
value={deadline}
onFinish={onFinish}
format="mm:ss"
prefix={<img src={clock} style={{ width: 25, height: 25 }} />
}
/>

);
}

其中 startTimecurrentTime 是服务端给我返回的开始答题时间以及现在的时间,onFinish 是提交问卷的函数。测试一切正常,并且看起来好像没有依赖客户端时间,于是我就愉快的提交了代码。


antd 的问题


上线后,有客户反映倒计时不正常,进入系统后直接显示 9000 多秒,导致业务直接进行不下去。这个时候我就懵了,我的代码中并没有依赖任何客户端时间,问题肯定是出现在 antdCountDown 组件上。于是我就去看了一下 antdCountDown 组件的源码,果不其然


 // 30帧
const REFRESH_INTERVAL= 1000 / 30;

const stopTimer = () => {
onFinish?.();
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};

const syncTimer = () => {
const timestamp = getTime(value);
if (timestamp >= Date.now()) {
countdown.current = setInterval(() => {
forceUpdate();
onChange?.(timestamp - Date.now());
if (timestamp < Date.now()) {
stopTimer();
}
}, REFRESH_INTERVAL);
}
};

React.useEffect(() => {
syncTimer();
return () => {
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};
}, [value]);

核心代码就是这段,本质 CountDown 并不是一个倒计时,而是根据客户端时间算出来的一个时间差值,这也能解释为啥这个倒计时相对比较准确。


但是依赖了客户端时间,就意味客户的本地时间会影响这个倒计时的准确性,甚至可以直接通过修改本地时间来绕过倒计时。一开始我的方案是加入 diff 值修正客户端时间,我也给 antd 官方提了一个 PR,但是被拒绝了。后来想了一下 CountDown 组件可以直接传入 diff 后的 value,确实没有必要新增 props


这个方案后来也是被否了,因为还是依赖了客户端时间。客户的机房条件比较复杂,可能一开始时间不对,但是做题途中时间会校正回来。因为我们这个调查系统短时间有几十万人参加调查,为了不给服务器过多的压力,查询服务器时间接口的频率是 1 分钟一次,所以会有很长时间的倒计时异常。


完全不依赖客户端时间的倒计时


倒计时的方案大致有 4 种, setTimeoutsetIntervalrequestAnimationFrameWeb WorkerrequestAnimationFrameWeb Worker 因为兼容性问题暂时放弃。


setInterval 实现倒计时是比较方便的,但是 setInterval 有两个缺点



  1. 使用 setInterval 时,某些间隔会被跳过;

  2. 可能多个定时器会连续执行;


每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。


可以看到,主线程的渲染都会对 setTimeoutsetInterval 的执行时间产生影响,但是 setTimeout 的影响小一点。所以我们可以使用 setTimeout 来实现倒计时.


const INTERVAL = 1000;

interface CountDownProps {
restTime: number;
format?: string;
onFinish: () => void;
key: number;
}
export const CountDown = ({ restTime, format = 'mm:ss', onFinish }: CountDownProps) => {
const timer = useRef<NodeJS.Timer | null>(null);
const [remainingTime, setRemainingTime] = useState(restTime);

useEffect(() => {
if (remainingTime < 0 && timer.current) {
onFinish?.();
clearTimeout(timer.current);
timer.current = null;
return;
}
timer.current = setTimeout(() => {
setRemainingTime((time) => time - INTERVAL);
}, INTERVAL);
return () => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
};
}, [remainingTime]);

return <span>{dayjs(remainingTime > 0 ? remainingTime : 0).format(format)}</span>;
};

为了修正 setTimeout 的时间误差,我们需要在 聚焦页面的时候 以及 定时一分钟请求一次服务器时间来修正误差。这里我们使用 swr 来轻松实现这个功能。


const REFRESH_INTERVAL = 60 * 1000;

export function useServerTime() {
const { data } = useSWR('/api/getCurrentTime', swrFetcher, {
// revalidateOnFocus 默认是开启的,但是我们项目中给关了,所以需要重新激活
revalidateOnFocus: true,
refreshInterval: REFRESH_INTERVAL,
});
return { currentTime: data?.currentTime };
}

最后我们把 CountDown 组件和 useServerTime 结合起来


function TitleAndCountDown() {
const { currentTime } = useServerTime();

return (
<Countdown
restTime={deadline - currentTime}
onFinish={onFinish}
key={deadline - currentTime}
/>

);
}

这样,就完成了一个完全不依赖客户端时间的倒计时组件。


总结



  • 上面方案中的 setTimeout 其实换成 requestAnimationFrame 计时会更加准确,也解决了 requestAnimationFrame未被激活的页面中 中不会执行的问题。

  • setIntervalsetTimeout 的时间误差是由于主线程的渲染时间造成的,所以如果我们的页面中有很多的动画,那么这个误差会更大。

  • 未激活的页面,setTimeout 的最小执行间隔是 1000ms


作者:xinglee
来源:juejin.cn/post/7229898205256417341
收起阅读 »

JavaScript作用域详解

web
作用域可分为词法作用域和动态作用域,JavaScript 使用词法作用域,也称为静态作用域。 词法作用域是指变量的作用域在代码写好的时候就确定了,而不是在运行时确定。函数在定义的时候就决定了其作用域,而不是在调用的时候。 JavaScript 的作用域(S...
继续阅读 »

作用域可分为词法作用域和动态作用域,JavaScript 使用词法作用域,也称为静态作用域。


词法作用域是指变量的作用域在代码写好的时候就确定了,而不是在运行时确定。函数在定义的时候就决定了其作用域,而不是在调用的时候。


JavaScript 的作用域(Scope)是指在代码中定义变量时,这些变量在哪里以及在哪些地方可以被访问。作用域控制着变量的可见性和生命周期。在 JavaScript 中,有全局作用域和局部作用域的概念,作用域的规则由函数定义和代码块定义来决定。


1. 全局作用域(Global Scope)


全局作用域是指在整个 JavaScript 程序中都可访问的范围。在全局作用域中定义的变量和函数可以被任何地方访问,包括代码文件、函数内部、循环块等。例如:


var globalVariable = "I am global";

function globalFunction({
  console.log(globalVariable);
}

globalFunction(); // 输出: I am global

2. 局部作用域(Local Scope)


局部作用域是指在函数内部或代码块内部定义的变量,其可见性仅限于该函数或代码块内部。这种作用域遵循 "变量提升" 的规则,即变量在声明之前就可以被访问,但其值为 undefined。例如:


function localScopeExample({
  var localVariable = "I am local";
  console.log(localVariable);
}

localScopeExample(); // 输出: I am local
console.log(localVariable); // 错误,localVariable 不在此处可见

3. 块级作用域(Block Scope)


在 ES6 引入块级作用域概念,可以通过 letconst 关键字在代码块内定义变量,这使得变量在块级范围内有效。在此之前,JavaScript 只有函数作用域,使用 var 关键字定义的变量在整个函数范围内有效。


if (true) {
  let blockVariable = "I am in a block";
  console.log(blockVariable);
}

console.log(blockVariable); // 错误,blockVariable 不在此处可见

总结


作用域是 JavaScript 中重要的概念,理解作用域有助于正确使用变量、避免命名冲突,提高代码的可维护性。


作者:MasterBao
来源:mdnice.com/writing/c771e23f7b014afbbe42499a1b32b0f7
收起阅读 »

【CSS定位属性】用CSS定位属性精确控制你的网页布局!

CSS定位属性是用于控制网页中元素位置的一种方式,它能够让元素在页面上精准地落在我们想要的位置。在CSS中,定位(Positioning)是控制元素在页面上如何定位和显示的一种机制。它主要包括四种属性:静态定位(static)、相对定位(relative)、绝...
继续阅读 »

CSS定位属性是用于控制网页中元素位置的一种方式,它能够让元素在页面上精准地落在我们想要的位置。

在CSS中,定位(Positioning)是控制元素在页面上如何定位和显示的一种机制。它主要包括四种属性:静态定位(static)、相对定位(relative)、绝对定位(absolute)、固定定位(fixed)。

每种定位方式都有其独特的特点和使用场景,下面将分别介绍这几种定位属性。

一、Static(静态定位)

静态定位是元素的默认定位方式,元素按照正常的文档流进行排列。在静态定位状态下,不能配合top、bottom、left、right来改变元素的位置。

  • 可以用于取消元素之前的定位设置。

代码示例:

<!DOCTYPE html>
<html>
<head>
<style>
.static {
background-color: lightblue;
padding: 100px;
}
</style>
</head>
<body>


<div>这是一个静态定位的元素。</div>


</body>
</html>

Description

二、Fixed(固定定位)

固定定位使元素相对于浏览器窗口进行定位,即使页面滚动,元素也会保持在固定的位置。

  • 固定定位的元素会脱离正常的文档流。

示例代码:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{
margin: 0;
padding: 0;
}
body{
/* 给整个页面设置高度,出滚动条以便观察 */
height: 5000px;
}
div{
width: 100px;
height: 100px;
background-color: blue;
/* 固定定位 */
position: fixed;
right: 100px;
bottom: 100px;
}
</style>
</head>
<body>
<div></div>
</body>
</html>

运行结果:

移动前

Description

移动后

Description

比如我们经常看到的网页右下角显示的“返回到顶部”,就可以用固定定位来实现。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
position: relative;
}
.content {
/* 页面内容样式 */
}
#backToTop {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #333;
color: #fff;
border: none;
padding: 10px;
cursor: pointer;
}
</style>
</head>
<body style="height: 5000px;">
<div>

</div>
<button id="backToTop" onclick="scrollToTop()">返回顶部</button>
<script>
function scrollToTop() {
window.scrollTo({top: 0, behavior: 'smooth'});
}
</script>
</body>
</html>

运行结果:

Description

三、Relative(相对定位)

相对定位是将元素对于它在标准流中的位置进行定位,通过设置边移属性top、bottom、left、right,使指定元素相对于其正常位置进行偏移。如果没有定位偏移量,对元素本身没有任何影响。

不使元素脱离文档流,空间会保留,不影响其他布局。

代码示例:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box1{
width:200px;
height:100px;
background:skyblue;
margin:10px;
}
.box2{
width:200px;
height:100px;
background:pink;
margin:10px;
position:relative;/*相对定位*/
left:100px;/*向右偏移100px*/
top:-50px;/*向上偏移50px*/
}
.box3{
width:200px;
height:100px;
background:yellowgreen;
margin:10px;
}
</style>
</head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
</body>
</html>

运行结果:

没使用相对定位之前是这样的:

Description

使用相对定位后:相对于原来的位置向右偏移了100px,向上偏移50px。
Description

虽然它的位置发生了变化,但它在标准文档流中的原位置依然保留。

四、Absolute(绝对定位)

绝对定位使元素相对于最近的非 static 定位祖先元素进行定位。如果没有这样的元素,则相对于初始包含块(initial containing block)。绝对定位的元素会脱离正常的文档流。

  • 如果该元素为内联元素,则会变成块级元素,可直接设置其宽和高的值(让内联具备快特性);

  • 如果该元素为块级元素,使其宽度根据内容决定。(让块具备内联的特性)

<style>
.wrap{
width:500px;
height:400px;
border: 2px solid red;
}
.box1{
width:200px;
height:100px;
background:skyblue;
margin:10px;
}
.box2{
width:200px;
height:100px;
background:pink;
margin:10px;
position:absolute;/*绝对定位*/
left:100px;/*向右偏移100px*/
top:30px;/*向下偏移30px*/
}
.box3{
width:200px;
height:100px;
background:yellowgreen;
margin:10px;


}
</style>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>

将第二个设置为绝对定位后,它脱离了文档流可以定位到页面的任何地方,在标准文档流中的原有位置会空出来,所以第三个会排到第一个下面。

Description

第二个相对于它的父元素向右偏移100,向下偏移30。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

五、z-index(层级顺序的改变)

层叠顺序决定了元素之间的堆叠顺序。z-index 属性用于设置元素的层叠顺序。具有较高 z-index 值的元素会覆盖具有较低 z-index 值的元素。

注意:

  • 默认值是0
  • 数值越大层越靠上
  • 不带单位
  • 没有最大值和最小值
  • 可以给负数

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div:nth-of-type(1){
width: 300px;
height: 300px;
background-color: skyblue;
position: absolute;
}
div:nth-of-type(2){
width: 200px;
height: 200px;
background-color: pink;
position: absolute;
}
div:nth-of-type(3){
width: 100px;
height: 100px;
background-color: yellowgreen;
position: absolute;
z-index: -1;
}
</style>
</head>
<body>

<div></div>
<div></div>
<div></div>


</body>
</html>

运行结果:

Description

可以看到,最后一个div依然存在,但是看不见了,原因就是我们改变了z-index属性值。

Description

以上就是CSS定位属性的介绍了,通过这些定位属性,可以灵活地控制网页中元素的位置和堆叠顺序。

在实际应用中,CSS定位属性的使用需要考虑到整体布局和用户体验。合理运用这些定位技巧,可以让你的网页不仅美观,而且易于使用和维护。记住,好的设计总是细节和功能的完美结合。

收起阅读 »

解锁 JSON.stringify() 5 个鲜为人知的功能

web
作为一名前端开发者,你可能熟悉JSON.stringify()方法,通常用于调试。但是很多只是简单使用一下接下来,让我们深入了解其实用性。 考虑一个对象如果想把她转成字符串打印出来: const obj = { name: 'San Shang Y...
继续阅读 »

u=142040142,590010156&fm=253&fmt=auto&app=138&f=JPEG.webp


作为一名前端开发者,你可能熟悉JSON.stringify()方法,通常用于调试。但是很多只是简单使用一下接下来,让我们深入了解其实用性。


考虑一个对象如果想把她转成字符串打印出来:


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(obj.toString()); // Result: [object Object]

如果你想这样打印你所看到的只能是 [object Object]


我们可以借助JSON.stringify()方法


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj));
// Result: {"name":"San Shang You Ya","age":18}

大多数开发者直接使用 JSON.stringify(),但我即将揭示一些隐藏的技巧。


1. 第二个参数(Array)


-JSON.stringify() 接受第二个参数,它是一个你想在控制台中显示的对象的键的数组。例如:


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj, ['name']));
// Result: {"name": "San Shang You Ya"}

这样而不是将整个 JSON 对象混乱地显示在控制台中,可以通过将所需的键作为数组传递给第二个参数来选择性地打印。


2. 第二个参数(Function)



  • 第二个参数也可以是一个函数,根据函数内的逻辑输出键值对。

  • 如果返回 undefined,则该键值对将不会被打印出来。


const obj = {  
name: 'San Shang You Ya',
age: 18
};

console.log(JSON.stringify(obj, (key, value) => (key === "age" ? value : undefined)));
// Result: {"age": 18}

3. 第三个参数作为数字



  • 第三个参数控制最终字符串中的间距。如果是一个数字,字符串化的每个级别将相应缩进。


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj, null, 2));

image.png


4. 第三个参数作为字符串


如果第三个参数是一个字符串,它将替换为空格字符


image.png


5. toJSON 方法


对象可以拥有一个 toJSON 方法。
JSON.stringify() 返回该方法的结果,并对其进行字符串化,而不是转换整个对象。


const superhero= {  
firstName: "San Shang",
lastName: "You Ya",
age: 21,
toJSON() {
return {
fullName: `${this.firstName} + ${this.lastName}`
};
}
};

console.log(JSON.stringify(superhero));
// Result: "{ "fullName" : "San Shang You Ya"}"

作者:StriveToY
来源:juejin.cn/post/7329164061390798883
收起阅读 »

拯救强迫症!前端统一代码规范

web
1. 代码格式化 1.1 工具介绍 ESLint 是一款用于查找并报告代码中问题的工具 Stylelint 是一个强大的现代 CSS 检测器 Prettier 是一款强大的代码格式化工具,支持多种语言 lint-staged 是一个在 git 暂存文件上运...
继续阅读 »

1. 代码格式化


1.1 工具介绍


Untitled 1.png



  • ESLint 是一款用于查找并报告代码中问题的工具

  • Stylelint 是一个强大的现代 CSS 检测器

  • Prettier 是一款强大的代码格式化工具,支持多种语言

  • lint-staged 是一个在 git 暂存文件上运行 linters 的工具

  • husky 是 Git Hook 工具,可以设置在 git 各个阶段触发设定的命令


1.2 配置说明


1.2.1 ESLint 配置


在项目根目录下增加 .eslintrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D eslint eslint-plugin-vue eslint-plugin-import eslint-import-resolver-typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-plugin-prettier eslint-config-prettier

module.exports = {
// 此项是用来告诉 eslint 找当前配置文件不能往父级查找
root: true,
// 全局环境
env: {
browser: true,
node: true,
},
// 指定如何解析语法,eslint-plugin-vue 插件依赖vue-eslint-parser解析器
parser: "vue-eslint-parser",
// 优先级低于parse的语法解析配置
parserOptions: {
// 指定ESlint的解析器
parser: "@typescript-eslint/parser",
// 允许使用ES语法
ecmaVersion: 2020,
// 允许使用import
sourceType: "module",
// 允许解析JSX
ecmaFeatures: {
jsx: true,
},
},
extends: [
"eslint:recommended", // 引入 ESLint的核心功能并且报告一些常见的共同错误
"plugin:import/recommended", // import/export语法的校验
"plugin:import/typescript", // import/export 语法的校验(支持 TS)
// 'plugin:vue/essential' // vue2 版本使用
// 'plugin:vue/recommended', // vue2 版本使用
"plugin:vue/vue3-essential", // vue3 版本使用
"plugin:vue/vue3-recommended", // vue3 版本使用
"plugin:@typescript-eslint/recommended",
"prettier", // prettier 要放在最后!
],
plugins: ["prettier"],
rules: {
"prettier/prettier": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"no-undef": "off",
// 更多规则详见:http://eslint.cn/docs/rules/
},
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
};

💡当 ESLint 同时使用 prettier 的时候,prettier 和 ESLint 可能存在一些规则冲突,我们需要借助 eslint-plugin-prettiereslint-config-prettier 进行解决,在安装完依赖包后在 .eslintrc.js 配置文件中进行添加如下内容:


module.exports = {
"extends": [
// 其他扩展内容...
"prettier" // prettier 要放在最后!
],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
},
}

1.2.2 StyleLint 配置


在项目根目录下增加 .stylelintrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D stylelint stylelint-config-standard stylelint-order stylelint-config-rational-order prettier stylelint-prettier stylelint-config-prettier postcss-html postcss-less stylelint-config-recommended-vue

module.exports = {
extends: [
'stylelint-config-standard', // 官方 stylelint 规则
'stylelint-config-rational-order', // 属性排列顺序规则
/*
* 通过安装 stylelint-prettier,设置 'stylelint-prettier/recommended',其包含了三个操作
plugins: ['.'],
extends: ['stylelint-config-prettier'], // 需要安装 stylelint-config-prettier
rules: {'prettier/prettier': true},
*/

'stylelint-prettier/recommended',
],
plugins: [
'stylelint-order', // CSS 属性排序
],
rules: {
// 更多规则详见:https://stylelint.io/user-guide/rules/list
},
};

💡当 StyleLint 同时使用 prettier 的时候,prettier 和 StyleLint 可能存在一些规则冲突,我们需要借助 stylelint-prettierstylelint-config-prettier 进行解决,在安装完依赖包后在 .stylelintrc.js 配置文件中进行添加如下内容:


module.exports = {
extends: [
/*
* 通过安装 stylelint-prettier,设置 'stylelint-prettier/recommended',其包含了三个操作
plugins: ['.'],
extends: ['stylelint-config-prettier'], // 需要安装 stylelint-config-prettier
rules: {'prettier/prettier': true},
*/

'stylelint-prettier/recommended',
],
};

1.2.3 Prettier 配置


在项目根目录下增加 .prettierrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D prettier

module.exports = {
// 更多规则详见:https://prettier.io/docs/en/options.html
printWidth: 120, // 单行长度
tabWidth: 2, // 缩进长度
useTabs: false, // 使用空格代替tab缩进
semi: true, // 句末使用分号
singleQuote: true, // 使用单引号
bracketSpacing: true, // 在对象前后添加空格-eg: { foo: bar }
quoteProps: 'consistent', // 对象的key添加引号方式
trailingComma: 'all', // 多行时尽可能打印尾随逗号
jsxBracketSameLine: true, // 多属性html标签的‘>’折行放置
arrowParens: 'always', // 单参数箭头函数参数周围使用圆括号-eg: (x) => x
jsxSingleQuote: true, // jsx中使用单引号
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'ignore', // 对HTML全局空白不敏感
};

1.2.4 husky 和 lint-staged 配置


step1. 初始化 husky


npx husky-init && npm install

step2. 在 .husky/pre-commit 文件中进行修改(注意区别 husky@7 与 husky@4 的设置方式)


#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

step3. 安装 lint-statged 并在 package.json 中进行设置


npm i -D lint-staged

{
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{css,less,vue}": [
"stylelint --fix",
"prettier --write",
"git add"
]
}
}

1.3 使用参考



  1. 代码提交:根据上述工具配置,代码在提交仓库时进行检查和格式化,实现代码风格统一;

  2. 本地保存:在 VSCode 中进行配置,使得代码在保存的时候即按照相应的规则进行格式化;



如何在 VSCode 中进行配置使得能够自动按照相应的规则进行格式化呢?接下来进入第二章《编辑器配置》。



2. 编辑器配置


2.1 VSCode 配置


2.1.1 配置内容


Untitled.png


所有 VSCode 配置自定义的内容(包括插件部分)都在 setting.json 文件中,以下为参考配置:


{
"editor.tabSize": 2,
"window.zoomLevel": 0,
"editor.fontSize": 14,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.multiCursorModifier": "ctrlCmd",
"editor.snippetSuggestions": "top",
"eslint.codeAction.showDocumentation": {
"enable": true
},
"eslint.run": "onSave",
"eslint.format.enable": true,
"eslint.options": {
"extensions": [
".js",
".vue",
".ts",
".tsx"
]
},
"eslint.validate": [
"javascript",
"typescript",
"vue"
],
"stylelint.validate": [
"css",
"less",
"postcss",
"scss",
"sass",
"vue",
],
// 保存时按照哪个规则进行格式化
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.fixAll.eslint": true
},
"files.autoSave": "afterDelay", // 文件自动保存
"files.autoSaveDelay": 2000, // 2s 后文件自动保存
}

参考资料: VS Code 使用指南VS Code 中 Vetur 与 prettier、ESLint 联合使用


2.1.1 插件推荐



  1. Eslint: Integrates ESLint JavaScript int0 VS Code

  2. stylelint: Official Stylelint extension for Visual Studio Code

  3. Prettier: Code formatter using prettier

  4. EditorConfig: EditorConfig Support for Visual Studio Code

  5. Npm Intellisense: VS Code plugin that autocompletes npm modules in import statements

  6. Path Intellisense: VS Code plugin that autocompletes filenames

  7. Auto Rename Tag: Auto rename paired HTML/XML tag

  8. Auto Close Tag: Automatically add HTML/XML close tag

  9. Code Spelling Checker: Spelling checker for source code

  10. Volar / Vetur: Language support for Vue 3 / Vue tooling for VS Code


2.2 EditorConfig 配置


EditorConfig 的优先级高于编辑器自身的配置,因此可用于维护不同开发人员、不同编辑器的编码风格。在项目根目录下增加 .editorconfig 文件进行配置即可,以下为参考配置:


# Editor configuration, see http://editorconfig.org

# 表示是最顶层的 EditorConfig 配置文件
root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
insert_final_newline = true # 始终在文件末尾插入一个新行
trim_trailing_whitespace = true # 去除行尾的任意空白字符

3. Commit Message 格式化


3.1 工具介绍


Conventional Commits 约定式提交规范是一种用于给提交信息增加人机可读含义的规范,可以通过以下工具来进行检查、统一和格式化:



  • commitlint:检查您的提交消息是否符合 conventional commit format

  • commitizen:帮助撰写规范 commit message 的工具

  • cz-customizable:自定义配置 commitizen 工具的终端操作

  • commitlint-config-cz:合并 cz-customizable 的配置和 commitlint 的配置


3.2 配置说明


3.2.1 格式化配置


step1. 安装 commitizen 和 cz-customizable


npm install -D commitizen cz-customizable

step2. 在 package.json 添加以下内容:


{
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
}

step3. 在项目根目录下增加 .cz-config.js 文件进行配置即可,以下为参考配置:


module.exports = {
// type 类型
types: [
{ value: 'feat', name: 'feat: 新增功能' },
{ value: 'fix', name: 'fix: 修复 bug' },
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'style', name: 'style: 代码格式改变(不影响功能)' },
{ value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 添加或修改测试用例' },
{ value: 'build', name: 'build: 构建流程或外部依赖变更' },
{ value: 'ci', name: 'ci: 修改 CI 配置或脚本' },
{ value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name: 'revert: 回滚 commit' },
],
// scope 类型
scopes: [
['components', '组件相关'],
['hooks', 'hook 相关'],
['utils', 'utils 相关'],
['styles', '样式相关'],
['deps', '项目依赖'],
// 如果选择 custom,后面会让你再输入一个自定义的 scope。也可以不设置此项,把后面的 allowCustomScopes 设置为 true
['custom', '以上都不是,我要自定义'],
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`,
};
}),
// 交互提示信息
messages: {
type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:',
scope: '选择一个 scope(可选):\n',
customScope: '请输入自定义的 scope:\n', // 选择 scope: custom 时会出现的提示
subject: '填写简短精炼的变更描述:\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED(可选):\n',
confirmCommit: '是否确认提交?',
},
// 设置只有 type 选择了 feat 或 fix,才询问 breaking message
allowBreakingChanges: ['feat', 'fix'],
// subject 限制长度
subjectLimit: 100,
};

step4. 新增 husky 配置,使得提交 commit message 时触发 commitizen,快捷命令如下:


npx husky add .husky/prepare-commit-msg "exec < /dev/tty && node_modules/.bin/cz --hook || true"

注意,commitizen 如果是全局安装,则使用下面的快捷命令:


npx husky add .husky/prepare-commit-msg "exec < /dev/tty && git cz --hook || true"

3.2.2 格式检查配置


step1. 安装 commitlint 和 commitlint-config-cz ****依赖:


npm install --save-dev @commitlint/{config-conventional,cli} commitlint-config-cz

step2. 在项目根目录下增加 commitlint.config.js 文件进行配置即可,以下为配置内容:


module.exports = {
extends: ['@commitlint/config-conventional', 'cz'],
rules: {},
};

step3. 新增 husky 配置,使得提交 commit message 时触发 commitlint 检验,配置内容如下:


npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

3.3 使用参考


在命令行输入 git commit,然后根据命令行提示输入相应的内容,完成之后则会自动生成符合规范的 commit message,从而实现提交信息的统一。


4. 代码规范参考


4.1 JS/TS 规范


社区类代码风格:



工具类代码风格:



4.2 CSS 规范


社区类代码风格:



工具类代码风格



4.3 VUE 规范


推荐阅读 Vue 官方风格指南:Vue2 版本Vue3 版本,其他可参考 eslint-plugin-vue


作者:植物系青年
来源:juejin.cn/post/7278575483909799947
收起阅读 »

时间格式化,显示昨天、今天

web
时间格式化的需求: 今天的数据显示“时分”,HH:mm 10:00 昨天的数据显示“昨天 时分”, 昨天 10:00 今年的数据,显示 “月日 时分”, 05-01 10:00 不是今年的数据,显示“年月日 时分”, 2022-05-01 10:00 代...
继续阅读 »

时间格式化的需求:



  • 今天的数据显示“时分”,HH:mm 10:00

  • 昨天的数据显示“昨天 时分”, 昨天 10:00

  • 今年的数据,显示 “月日 时分”, 05-01 10:00

  • 不是今年的数据,显示“年月日 时分”, 2022-05-01 10:00


代码展示



在 ios中 用new Date("2022-05-01 10:00").getTime()会有兼容性问题,跟日期格式的连字符有关系,这里使用moment插件



const moment = require("moment");

// 判断日期是不是今天、昨天, 0:今天 -1:昨天 1-明天
// str: 2023-02-07 14:09:27.0
export function isWhichDay(str) {
const date = new Date();
const that = moment(moment(str).format("YYYY-MM-DD")).valueOf();
const today = moment(moment(date).format("YYYY-MM-DD")).valueOf();
const timeStampDiff = that - today;
const obj = {
"-86400000": "-1",
0: "0",
86400000: "1",
};
return obj[timeStampDiff] || null;
}

// 判断是不是当年
export function isCurYear(str) {
return moment().format("YYYY") === moment(str).format("YYYY");
}

/**
* 格式化时间 YYYY-MM-DD HH:mm:ss
* 1、当天时间显示如 10:00
* 2、昨天显示如 昨天10:00
* 3、昨天之前且当年的,显示如,05-01 10:00
* 4、昨天之前且跨年的,显示如, 2022-05-01 10:00
*
@param {string} time "2022-05-01 10:00:01.0"
*
@returns {string}
*/

export function formatTime(time) {
const t = isWhichDay(time);
if (t === "0") {
return moment(time).format("HH:mm");
} else if (t === "-1") {
return `昨天 ${moment(time).format("HH:mm")}`;
} else if (
isCurYear(time) &&
moment(time).valueOf() < moment(new Date()).valueOf()
) {
return moment(time).format("MM-DD HH:mm");
} else {
return moment(time).format("YYYY-MM-DD HH:mm");
}
}



作者:甜点cc
来源:juejin.cn/post/7226300253921558583
收起阅读 »

环信WEB端单群聊 UIKit 快速集成与消息发送指南

写在前面:千呼万唤始出来,环信web端终于出uikit了,环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库。该组件库提供了聊天相关的组件,包括会话列表、聊天界面、联系人列表和群组设置等组件,组件内部集成了...
继续阅读 »

写在前面:

千呼万唤始出来,环信web端终于出uikit了,环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库。该组件库提供了聊天相关的组件,包括会话列表、聊天界面、联系人列表和群组设置等组件,组件内部集成了 IM SDK,可以帮助开发者不关心内部实现和数据管理就能根据实际业务需求快速搭建包含 UI 界面的即时通讯应用。现在就让我们一起探索如何集成吧!本文介绍如何快速实现在单聊会话中发送消息


准备工作:

  1. React 环境:需要 React 16.8.0 或以上版本;React DOM 16.8.0 或以上版本。

  2. 即时通讯 IM 项目:已在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了 App Key

  3. 环信用户:在环信控制台创建 IM 用户,并获取用户 ID 和密码或 token。


  4. 好友关系:双方需要先添加好友才可以聊天







集成uikit:

准备工作完成就开始集成!在此先奉上uikit源码

第一步:创建一个uikit项目

# 安装 CLI 工具。
npm install create-react-app
# 构建一个 my-app 的项目。
npx create-react-app my-app
cd my-app

第二步:安装 easemob-chat-uikit

cd my-app
  • 使用 npm 安装 easemob-chat-uikit 包
npm install easemob-chat-uikit --save
  • 使用 yarn 安装 easemob-chat-uikit 包
yarn add easemob-chat-uikit

第三步:引入uikit组件

在你的 React 项目中,引入 UIKit 提供的组件和样式:

// 导入组件
import {
UIKitProvider,
Chat,
ConversationList,
// ...
} from "easemob-chat-uikit";

// 导入样式
import "easemob-chat-uikit/style.css";

第四步:初始化配置

easemob-chat-uikit 提供 UIKitProvider 组件管理数据。UIKitProvider 不渲染任何 UI, 只用于为其他组件提供全局的 context,自动监听 SDK 事件, 在组件树中向下传递数据来驱动组件更新。单群聊 UIKit 中其他组件必须用 UIKitProvider 包裹。

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
</UIKitProvider>
</div>
);
}

export default App;


第五步:引入组件

根据自己的项目引入所需组件,组件文档,本文只介绍如何快速实现在单聊会话中发送消息,为了方便快速体验,一定要确保准备工作的第四条双方已经互为好友

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
<div style={{ display: "flex" }}>
<div style={{ width: "40%", height: "100%" }}>
<ContactList
onItemClick={(data) => {
rootStore.conversationStore.addConversation({
chatType: "singleChat",
conversationId: data.id,
lastMessage: {},
unreadCount: "",
});
}}
/>
</div>//联系人组件,点击某个好友通过‘rootStore.conversationStore.addConversation’创建会话
<div style={{ width: "30%", height: "100%" }}>
<ConversationList />//会话列表组件
</div>
<div style={{ width: "30%", height: "100%" }}>
<Chat />//聊天消息组件
</div>
</div>
</UIKitProvider>
</div>
);
}

export default App;


第六步:运行并测试

1、运行项目

npm run start

2、点击好友并发送一条消息


总结:

通过以上步骤,你已经成功集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧


收起阅读 »

前端如何用密文跟后端互通?原来那么简单!

web
后端:密码得走密文哇! 我:base64?md5? 后端:这次不行哇,新来的测试不好糊弄呢!必须要国密sm2加密捏 我: 好吧,看我的。 我这边使用的是sm-crypto,当然也有很多优秀的库如:forge,我在业务上搭配jszip做过上传zip文件内藏加密后...
继续阅读 »

后端:密码得走密文哇!

我:base64?md5?

后端:这次不行哇,新来的测试不好糊弄呢!必须要国密sm2加密捏

我: 好吧,看我的。


我这边使用的是sm-crypto,当然也有很多优秀的库如:forge,我在业务上搭配jszip做过上传zip文件内藏加密后的私钥进行证书登录,还是不错的支持,但是文档社区不是很友好,所以推荐sm-crypto,接下来让我们一起使用它吧。


sm-crypto是一个基于Node.js的密码学库,专为与国密算法(中国密码算法标准)兼容而设计。它提供了各种加密、解密、签名和验证功能。


sm-crypto包含多种密码算法的实现,例如:



  • SM1:对称加密算法,其加密强度与AES相当,但该算法不公开,调用时需要通过加密芯片的接口进行调用。

  • SM2:非对称加密算法,基于ECC(椭圆曲线密码学)。该算法已公开,且由于基于ECC,其签名速度与秘钥生成速度都快于RSA。此外,ECC 256位(SM2采用的就是ECC 256位的一种)的安全强度比RSA 2048位高,但运算速度快于RSA。

  • SM3:消息摘要算法,可以用MD5作为对比理解,其校验结果为256位。

  • SM4:无线局域网标准的分组数据算法,属于对称加密,密钥长度和分组长度均为128位。


sm-crypto内部方法介绍


1.SM2加密与解密


SM2是一种基于椭圆曲线密码学的非对称加密算法。sm-crypto提供了SM2的密钥生成、加密、解密等功能。通过调用相关方法,开发者可以轻松地生成SM2密钥对,并使用公钥进行加密、私钥进行解密。


const { sm2 } = require('sm-crypto');  
const keyPair = sm2.generateKeyPairHex(); // 生成密钥对
const publicKey = keyPair.publicKey; // 公钥
const privateKey = keyPair.privateKey; // 私钥

const message = 'Hello, SM2!'; // 待加密的消息
const encrypted = sm2.doEncrypt(message, publicKey, { hash: true }); // 使用公钥加密
const decrypted = sm2.doDecrypt(encrypted, privateKey, { hash: true, raw: true }); // 使用私钥解密

console.log('加密结果:', encrypted);
console.log('解密结果:', decrypted.toString()); // 输出原始消息

image.png


2.SM3摘要算法


SM3是一种密码杂凑算法,用于生成消息的摘要值。sm-crypto提供了SM3的摘要计算功能,开发者可以通过调用相关方法计算任意消息的SM3摘要值。


const { sm3 } = require('sm-crypto');  
const message = 'Hello, SM3!'; // 待计算摘要的消息
const digest = sm3(message); // 计算SM3摘要值

console.log('SM3摘要值:', digest);

image.png


3.SM4分组加密算法


SM4是一种分组密码算法,适用于无线局域网等场景。sm-crypto提供了SM4的加密与解密功能,开发者可以使用SM4密钥对数据进行加密和解密操作。


const sm4 = require('sm-crypto').sm4;                                               |
const sm4 = require('sm-crypto').sm4;
const key = '0123456789abcdeffedcba9876543210'; // 16字节的SM4密钥
const message = 'Hello, SM4!'; // 待加密的消息
const encrypted = sm4.encrypt(Buffer.from(message), Buffer.from(key, 'hex')); // 加密
const decrypted = sm4.decrypt(encrypted, Buffer.from(key, 'hex')); // 解密

console.log('加密结果:', encrypted.toString('hex'));
console.log('解密结果:', decrypted.toString()); // 输出原始消息

image.png


4、签名/验签


签名(Sign)


const { sm2 } = require('sm-crypto'); 
const keyPair = sm2.generateKeyPairHex(); // 生成密钥对
const publicKey = keyPair.publicKey; // 公钥
const privateKey = keyPair.privateKey; // 私钥

const message = '这是要签名的消息'; // 替换为实际要签名的消息
// 使用私钥对消息进行签名
let sigValueHex = sm2.doSignature(message, privateKey);
console.log('签名结果:', sigValueHex);

image.png
验签(Verify Signature)


const message = '这是要验证签名的消息'; // 应与签名时使用的消息相同  
const sigValueHex = '签名值'; // 替换为实际的签名值字符串,即签名步骤中生成的sigValueHex

// 使用公钥验证签名是否有效
let verifyResult = sm2.doVerifySignature(message, sigValueHex, publicKey);

console.log('验签结果:', verifyResult); // 如果验证成功,应输出true;否则输出false


image.png


实战例子


登录注册,对用户密码进行加密



注意:前端是不储存任何涉及安全的密钥(公钥是直接拿后端生成的)。



新建个工具文件,专门存放加密逻辑,我这用的是SM2


// smCrypto.js
import { sm2 } from 'sm-crypto' // 引入加密库

export const doEncrypt = ( // 加密
data,
pKey = publicKey,
cipherMode = 0
) =>
sm2.doEncrypt(
typeof data === 'object'
? JSON.stringify(data) : data,
pKey,
cipherMode
)
export const encryptionPwd = async data => { // 加密密码高阶
let servePublicKey = ''
await user.getSm2Pkeys()
.then(res => {
servePublicKey = res.data.content
})
return doEncrypt(
data,
servePublicKey
)
}

sm-crypto作为一款基于Node.js的国密算法库,为开发者提供了丰富的密码学功能。通过调用sm-crypto的内部方法,开发者可以轻松地实现SM2加密与解密、SM3摘要计算以及SM4分组加密等操作。这些功能在保障数据安全、构建安全应用等方面发挥着重要作用。同时,开发者在使用sm-crypto时,也需要注意遵循最佳的安全实践,确保密钥的安全存储和管理,以防止潜在的安全风险。


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

浏览器无痕模式就真的无痕了吗?不一定哦!

web
概述 无痕模式,有些浏览器也叫隐身模式,隐私模式。该模式下所有cookie、缓存是失效的,也就是所有原来的登录信息都会消失,那么是否你打开一个网站,网站平台就真的不确定你是谁了吗? 不一定哦。这个世界上有一种技术叫浏览器指纹技术,不需要你登录,它就可以根据你的...
继续阅读 »

概述


无痕模式,有些浏览器也叫隐身模式,隐私模式。该模式下所有cookie、缓存是失效的,也就是所有原来的登录信息都会消失,那么是否你打开一个网站,网站平台就真的不确定你是谁了吗? 不一定哦。这个世界上有一种技术叫浏览器指纹技术,不需要你登录,它就可以根据你的特定标志来区分,从而跟踪你的所有操作记录。今天我们就来看看这种技术的原理,还有用途。


一、原理


浏览器指纹可以在用户没有任何登录的情况下仍然知道你谁,比如你在登录了一下网站A,现在开启无痕模式,再次打开网站A, 那么网站A大概率还是能区分现在操作网站的人是谁。 因为在这个世界上,用户的浏览器环境极小概率才能相同,考虑的因素包括浏览器版本、浏览器型号、屏幕分辨率、系统语言、本地时间、CPU架构、电池状态、网络信息、已安装的浏览器插件等等各种各样。浏览器指纹技术就是用这些因素来综合计算一个哈希值,这个哈希值大概率是唯一的


二、展示


今天我们就展示一下单单利用Canvas画布这个功能来确定用户的唯一标识,因为Canvas API专为通过JavaScript和HTML绘制图形而设计,而图形在画布上的呈现方式可能因Web浏览器、操作系统、显卡和其他因素而不一样,从而产生可用于创建指纹的唯一图像,我们利用下方代码展示一个图像。


// 输入一个带有小写、大写、标点符号的文本
var txt = "BrowserLeaks,com <canvas> 1.0";
ctx.textBaseline = "top";
ctx.font = "14px 'Arial'";
ctx.textBaseline = "alphabetic";
ctx.fillStyle = "#f60";
ctx.fillRect(125,1,62,20);
ctx.fillStyle = "#069";
ctx.fillText(txt, 2, 15);
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
ctx.fillText(txt, 4, 17);

下面动画GIF图展示了虽然JavaScript代码相同,但是由于图像在不同系统上的呈现方式不同,因此每一个用户所显示的图像都有细微的差别。这种差别大多数时候人眼通常无法识别,但通过对生成图像的数据进行分析就能发现不一样。



我们直接看成品,利用普通模式和无痕模式打开这个测试网站,就能发现2个哈希值是完全一样的,并且是每15万个用户中有2个人可能是相同的哈希值,唯一性达到了99.99%。


三、用途


1、广告


广告联盟通过浏览器指纹技术,不需要你登录,就可以知道你看过哪些记录,然后在其他站点给你推送你感兴趣的广告。所以有时候我们经常会碰到在淘宝或者百度搜索了一个商品,然后去到其他网站,马上给你推送你搜索过的商品的广告。


2、防刷


针对一些商品的浏览量、内容的阅读量、投票等等各种类似平台,往往会针对用户做一个限制,比如只能浏览一次。这种时候往往也都是用浏览器指纹技术,虽然你可能注册了很多用户账号,但是你的浏览器指纹都是相同的,可以判断你就是同一个人。不过上有政策,下有对策,有个东西叫做指纹浏览器,这类浏览器里面可以随意的切换用户的指纹。 所以这个技术针对广大的普通用户防刷还是有效的,对专业的刷量工作室就没什么效果了。


作者:明同学
来源:juejin.cn/post/7347958786050637875
收起阅读 »

H5唤起APP路子

web
前一段时间在做一些H5页面,需求中落地页占比较大,落地页承担的职责就是引流。引流有两种形式,同时也是我们对唤端的定义:引导已下载用户打开APP,引导未下载用户下载APP。 引导已下载用户打开APP,从数据上说用户停留在APP中的时间更多了,是在提高用户粘性;从...
继续阅读 »

前一段时间在做一些H5页面,需求中落地页占比较大,落地页承担的职责就是引流。引流有两种形式,同时也是我们对唤端的定义:引导已下载用户打开APP,引导未下载用户下载APP。


引导已下载用户打开APP,从数据上说用户停留在APP中的时间更多了,是在提高用户粘性;从体验上说,APP体验是要比H5好的。引导未下载用户下载APP,可以增加我们的用户量。


上面其实分别解释了 什么是唤端 以及 为什么要唤端,也就是 3W法则 中的 What 和 Why,那么接下来我们就要聊一聊 How 了,也就是 如何唤端


我们先来看看常见的唤端方式以及他们适用的场景:


唤端媒介


URL Scheme


来源


我们的手机上有许多私密信息,联系方式、照片、银彳亍卡信息...我们不希望这些信息可以被手机应用随意获取到,信息泄露的危害甚大。所以,如何保证个人信息在设备所有者知情并允许的情况下被使用,是智能设备的核心安全问题。


对此,苹果使用了名为 沙盒 的机制:应用只能访问它声明可能访问的资源。但沙盒也阻碍了应用间合理的信息共享,某种程度上限制了应用的能力。


因此,我们急需要一个辅助工具来帮助我们实现应用通信, URL Scheme 就是这个工具。


URL Scheme 是什么


我们来看一下 URL 的组成:

[scheme:][//authority][path][?query][#fragment]

我们拿 https://www.baidu.com 来举例,scheme 自然就是 https 了。


就像给服务器资源分配一个 URL,以便我们去访问它一样,我们同样也可以给手机APP分配一个特殊格式的 URL,用来访问这个APP或者这个APP中的某个功能(来实现通信)。APP得有一个标识,好让我们可以定位到它,它就是 URL 的 Scheme 部分。


常用APP的 URL Scheme


APP微信支付宝淘宝微博QQ知乎短信
URL Schemeweixin://alipay://taobao://sinaweibo://mqq://zhihu://sms://

URL Scheme 语法


上面表格中都是最简单的用于打开 APP 的 URL Scheme,下面才是我们常用的 URL Scheme 格式:

     行为(应用的某个功能)    
|
scheme://[path][?query]
| |
应用标识 功能需要的参数

Intent


安卓的原生谷歌浏览器自从 chrome25 版本开始对于唤端功能做了一些变化,URL Scheme 无法再启动Android应用。 例如,通过 iframe 指向 weixin://,即使用户安装了微信也无法打开。所以,APP需要实现谷歌官方提供的 intent: 语法,或者实现让用户通过自定义手势来打开APP,当然这就是题外话了。


Intent 语法

intent:
HOST/URI-path // Optional host
#Intent;
package=[string];
action=[string];
category=[string];
component=[string];
scheme=[string];
end;

如果用户未安装 APP,则会跳转到系统默认商店。当然,如果你想要指定一个唤起失败的跳转地址,添加下面的字符串在 end; 前就可以了:

S.browser_fallback_url=[encoded_full_url]

示例


下面是打开 Zxing 二维码扫描 APP 的 intent。

intent:
//scan/
#Intent;
package=com.google.zxing.client.android;
scheme=zxing;
end;

打开这个 APP ,可以通过如下的方式:

 <a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;S.browser_fallback_url=http%3A%2F%2Fzxing.org;end"> Take a QR code a>

Universal Link


Universal Link 是什么


Universal Link 是苹果在 WWDC2015 上为 iOS9 引入的新功能,通过传统的 HTTP 链接即可打开 APP。如果用户未安装 APP,则会跳转到该链接所对应的页面。


为什么要使用 Universal Link


传统的 Scheme 链接有以下几个痛点:



  • 在 ios 上会有确认弹窗提示用户是否打开,对于用户来说唤端,多出了一步操作。若用户未安装 APP ,也会有一个提示窗,告知我们 “打不开该网页,因为网址无效”

  • 传统 Scheme 跳转无法得知唤端是否成功,Universal Link 唤端失败可以直接打开此链接对应的页面

  • Scheme 在微信、微博、QQ浏览器、手百中都已经被禁止使用,使用 Universal Link 可以避开它们的屏蔽( 截止到 18年8月21日,微信和QQ浏览器已经禁止了 Universal Link,其他主流APP未发现有禁止 )


如何让 APP 支持 Universal Link


有大量的文章会详细的告诉我们如何配置,你也可以去看官方文档,我这里简单的写一个12345。



  1. 拥有一个支持 https 的域名

  2. 开发者中心 ,Identifiers 下 AppIDs 找到自己的 App ID,编辑打开 Associated Domains 服务。

  3. 打开工程配置中的 Associated Domains ,在其中的 Domains 中填入你想支持的域名,必须以 applinks: 为前缀

  4. 配置 apple-app-site-association 文件,文件名必须为 apple-app-site-association不带任何后缀

  5. 上传该文件到你的 HTTPS 服务器的 根目录 或者 .well-known 目录下


Universal Link 配置中的坑


这里放一下我们在配置过程中遇到的坑,当然首先你在配置过程中必须得严格按照上面的要求去做,尤其是加粗的地方。



  1. 跨域问题


    IOS 9.2 以后,必须要触发跨域才能支持 Universal Link 唤端。


    IOS 那边有这样一个判断,如果你要打开的 Universal Link 和 当前页面是同一域名,ios 尊重用户最可能的意图,直接打开链接所对应的页面。如果不在同一域名下,则在你的 APP 中打开链接,也就是执行具体的唤端操作。


  2. Universal Link 是空页面


    Universal Link 本质上是个空页面,如果未安装 APP,Universal Link 被当做普通的页面链接,自然会跳到 404 页面,所以我们需要将它绑定到我们的中转页或者下载页。



如何调用三种唤端媒介


通过前面的介绍,我们可以发现,无论是 URL Scheme 还是 Intent 或者 Universal Link ,他们都算是 URL ,只是 URL Scheme 和 Intent 算是特殊的 URL。所以我们可以拿使用 URL 的方法来使用它们。


iframe

<iframe src="sinaweibo://qrcode">

在只有 URL Scheme 的日子里,iframe 是使用最多的了。因为在未安装 app 的情况下,不会去跳转错误页面。但是 iframe 在各个系统以及各个应用中的兼容问题还是挺多的,不能全部使用 URL Scheme。


a 标签

<a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"">扫一扫</a>

前面我们提到 Intent 协议,官方给出的用例使用的就是使用的 a 标签,所以我们跟着一起用就可以了


使用过程中,对于动态生成的 a 标签,使用 dispatch 来模拟触发点击事件,发现很多种 event 传递过去都无效;使用 click() 来模拟触发,部分场景下存在这样的情况,第一次点击过后,回到原先页面,再次点击,点击位置和页面所识别位置有不小的偏移,所以 Intent 协议从 a 标签换成了 window.location。


window.location


URL Scheme 在 ios 9+ 上诸如 safari、UC、QQ浏览器中, iframe 均无法成功唤起 APP,只能通过 window.location 才能成功唤端。


当然,如果我们的 app 支持 Universal Link,ios 9+ 就用不到 URL Scheme 了。而 Universal Link 在使用过程中,我发现在 qq 中,无论是 iframe 导航 还是 a 标签打开 又或者 window.location 都无法成功唤端,一开始我以为是 qq 和微信一样禁止了 Universal Link 唤端的功能,其实不然,百般试验下,通过 top.location 唤端成功了。


判断唤端是否成功


如果唤端失败(APP 未安装),我们总是要做一些处理的,可以是跳转下载页,可以是 ios 下跳转 App Store... 但是Js 并不能提供给我们获取 APP 唤起状态的能力,Android Intent 以及 Universal Link 倒是不用担心,它们俩的自身机制允许它们唤端失败后直接导航至相应的页面,但是 URL Scheme 并不具备这样的能力,所以我们只能通过一些很 hack 的方式来实现 APP 唤起检测功能。

// 一般情况下是 visibilitychange 
const visibilityChangeProperty = getVisibilityChangeProperty();
const timer = setTimeout(() => {
const hidden = isPageHidden();
if (!hidden) {
cb();
}
}, timeout);

if (visibilityChangeProperty) {
document.addEventListener(visibilityChangeProperty, () => {
clearTimeout(timer);
});

return;
}

window.addEventListener('pagehide', () => {
clearTimeout(timer);
});

APP 如果被唤起的话,页面就会进入后台运行,会触发页面的 visibilitychange 事件。如果触发了,则表明页面被成功唤起,及时调用 clearTimeout ,清除页面未隐藏时的失败函数(callback)回调。


当然这个事件是有兼容性的,具体的代码实现时做了事件是否需要添加前缀(比如 -webkit- )的校验。如果都不兼容,我们将使用 pagehide 事件来做兜底处理。


没有完美的方案


透过上面的几个点,我们可以发现,无论是 唤端媒介调用唤端媒介 还是 判断唤端结果 都没有一个十全十美的方法,我们在代码层上能做的只是在确保最常用的场景(比如 微信、微博、手百 等)唤端无误的情况下,最大化的兼容剩余的场景。


好的,我们接下来扯一些代码以外的,让我们的 APP 能够在更多的平台唤起。




  • 微信、微博、手百、QQ浏览器等。


    这些应用能阻止唤端是因为它们直接屏蔽掉了 URL Scheme 。接下来可能就有看官疑惑了,微信中是可以打开大众点评的呀,微博里面可以打开优酷呀,那是如何实现的呢?


    它们都各自维护着一个白名单,如果你的域名在白名单内,那这个域名下所有的页面发起的 URL Scheme 就都会被允许。就像微信,如果你是腾讯的“家属”,你就可以加入白名单了,微信的白名单一般只包含着“家属”,除此外很难申请到白名单资质。但是微博之类的都是可以联系他们的渠道童鞋进行申请的,只是条件各不相同,比如微博的就是在你的 APP 中添加打开微博的入口,三个月内唤起超过 100w 次,就可以加入白名单了。




  • 腾讯应用宝直接打开 APP 的某个功能


    刚刚我们说到,如果你不是微信的家属,那你是很难进入白名单的,所以在安卓中我们一般都是直接打开腾讯应用宝,ios 中 直接打开 App Store。点击腾讯应用宝中的“打开”按钮,可以直接唤起我们的 APP,但是无法打开 APP 中的某个功能(就是无法打开指定页面)。


    腾讯应用宝对外开放了一个叫做 APP Link 的申请,只要你申请了 APP Link,就可以通过在打开应用宝的时候在应用宝地址后面添加上 &android_schema={your_scheme} ,来打开指定的页面了。




开箱即用的callapp-lib


信息量很大!各种问题得自己趟坑验证!内心很崩溃!


不用愁,已经为你准备好了药方,只需照方抓药即可😏 —— npm 包 callapp-lib


你也可以通过 script 直接加载 cdn 文件:

<script src="https://unpkg.com/callapp-lib"></script>

它能在大部分的环境中成功唤端,而且炒鸡简单啊,拿过去就可以用啊,还支持很多扩展功能啊,快来瞅瞅它的 文档 啊~~~


作者:阳光多一些
链接:juejin.cn/post/7348249728939130907
收起阅读 »

华为自研的前端框架是什么样的?

web
大家好,我卡颂。 最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力: 响应式API 兼容ReactAPI 官方提供6大核心组件 并且,在官方宣传视频里提到 —— 这是款大模型驱动的智能框架。 ...
继续阅读 »

大家好,我卡颂。


最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力:



  1. 响应式API




  1. 兼容ReactAPI




  1. 官方提供6大核心组件



并且,在官方宣传视频里提到 —— 这是款大模型驱动智能框架


那么,这究竟是款什么样的前端框架呢?我在第一时间体验了Demo,阅读了框架源码,并采访了框架核心开发者。本文将包括两部分内容:



  1. 对框架核心开发者陈超涛的采访

  2. 卡颂作为一个老前端,阅读框架源码后的一些分析

采访核心开发者


开发Inula的初衷是?


回答:


华为内部对于业务中强依赖的软件,考虑到竞争力,通常会开发一个内部使用的版本。


Inula在华为内部,从立项到现在两年多,基本替换了公司内绝大部分React项目。



卡颂补充背景知识:Inula兼容React 95% API,最初开发的目的就是为了替换华为内部使用的React。为了方便理解,你可以将Inula类比于华为内部的React



为什么开源?


回答:


华为对于自研软件的公司策略,只要是公司内部做的,觉得还ok的自研都会开源。



接下来的提问涉及到官网宣传的内容



宣传片提到的大模型赋能、智能框架是什么意思?


回答:


这主要是Inula团队与其他外部团队在AI低代码方向的一些探索。比如:



  1. 团队与上海交大的一个团队在探索大模型赋能chrome调试业务代码方面有些合作,目的是为了自动定位问题

  2. 团队与华为内部的大模型编辑器团队合作,探索框架与编辑器定制可能性


以上还都属于探索阶段。


Inula未来有明确的发展方向么?


回答:


团队正在探索引入响应式API,相比于React的虚拟DOM方案,响应式API能够提高运行时性能。24年可能会从Vue composition API中寻求些借鉴。


新的发展方向会在项目仓库以RFC的形式展开。



补充:RFCRequest for Comments的缩写。这是一种协作模式,通常用于提出新的特性、规范或者改变现有的一些规则。RFC的目的是收集不同的意见和反馈,以便在最终确定一个决策前,考虑尽可能多的观点和影响。



为什么要自研核心组件而不用社区成熟方案?



卡颂补充:所谓核心组件,是指状态管理、路由、国际化、请求库、脚手架这样的框架生态相关的库。既然Inula兼容React,为什么不直接用React生态的成熟产品,而要自研呢?毕竟,这些库是没有软件风险的。




回答:


主要还是丰富Inula生态,根据社区优秀的库总结一套Inula官方推荐的最佳实践。至于开发者怎么选择,我们并不强求。


卡颂的分析


以上是我对Inula核心开发者陈超涛的采访。下面是我看了Inula源码后的一些分析。


要分析一款前端框架,最重要的是明白他是如何更新视图的?这里我选择了两种触发时机来分析:



  1. 首次渲染


触发的方式类似如下:


Inula.render(<App />, document.getElementById("root"));


  1. 执行useState的更新方法触发更新


触发的方式类似如下:


function App() {
const [num, update] = useState(0);
// 触发更新
update(xxx);
// ...
}

顺着调用栈往下看,他们都会执行两步操作:



  1. 创建名为update的数据结构

  2. 执行launchUpdateFromVNode方法


比如这是首屏渲染时:



这是useState更新方法执行时:



launchUpdateFromVNode方法会向上遍历到根结点(源码中遍历的节点叫VNode),再从根节点开始遍历树。由此可以判断,Inula的更新机制与React类似。


所有主流框架在触发更新后,都不会立刻执行更新,中间还有个调度流程。这个流程的存在是为了解决:



  1. 哪些更新应该被优先执行?

  2. 是否有些更新是冗余的,需要合并在一块执行?


Vue中,更新会在微任务中被调度并统一执行,在React中,同时存在微任务(promise)与宏任务(MessageChannel)的调度模式。


Inula中,存在宏任务的调度模式 —— 当宿主环境支持MessageChannel时会使用它,不支持则使用setTimeout调度:



同时,与这套调度机制配套的还有个简单的优先级算法 —— 存在两种优先级,其中:



  • ImmediatePriority:对应正常情况触发的更新

  • NormalPriority:对应useEffect回调


每个更新会根据更新的ID(一个自增的数字)+ 优先级对应的数字 作为优先级队列中的排序依据,按顺序执行。


假设先后触发2次更新,优先级分别是ImmediatePriorityNormalPriority,那么他们的排序依据分别是:



  1. 100(假设当前ID到100了)- 1(ImmediatePriority对应-1) = 99

  2. 101(100自增到101)+ 10000(NormalPriority对应10000)= 10101


99 < 10101,所以前者会先执行。


需要注意的是,Inula中对更新优先级的控制粒度没有React并发更新细,比如对于如下代码:


useEffect(function cb() {
update(xxx);
update(yyy);
})

React中,控制的是每个update对应优先级。在Inula中,控制的是cb回调函数与其他更新所在回调函数之间的执行顺序。


这意味着本质来说,Inula中触发的所有更新都是同步更新,不存在React并发更新中高优先级更新打断低优先级更新的情况。


这也解释了为什么Inula兼容 95% 的React API,剩下 5% 就是并发更新相关API(比如useTransitionuseDeferredvalue)。


现在我们已经知道Inula的更新方式类似React,那么官网提到的响应式API该如何实现呢?这里存在三条路径:



  1. 一套外挂的响应式系统,类似ReactMobx的关系

  2. 内部同时存在两套更新系统(当前一套,响应式一套),调用不同的API使用不同的系统

  3. 重构内部系统为响应式系统,通过编译手段,使所有API(包括当前的React API与未来的类 Vue Composition API)都走这套系统



其中第一条路径比较简单,第二条路径应该还没框架使用,第三条路径想象空间最大。不知道Inula未来会如何发展。


总结


当前,Inula是一款类React的框架,功能上可以类比为React并发更新之前的版本


下一步,Inula会引入响应式API,目的是提高渲染效率。


对于未来的发展,主要围绕在:



  • 探索类 Vue Composition API的可能性

  • 迭代官方核心生态库


对于华为出的这款前端框架,你怎么看?


现在开放原子开源基金会搞了个开源大赛,奖金有35w,有两个选题:



  1. 基于openInula实现社区生态库,比如组件库、图表库、Rust基建、SSR、跨平台、高性能响应式更新方案...

  2. 基于openInula实现的AI应用


由于 openInulaReact API基本一致,说白了只要你把自己的 React 项目改下依赖适配下就能报名,有奖金拿,还有华为背书,这波属于稳赚不赔。感兴趣的朋友可以搜openInula前端框架生态与AI创新挑战赛报名。


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

告别混乱布局:用CSS盒子模型为你的网页穿上完美外衣!

在网络设计的世界里,盒子模型是构建网页布局的基石,只有理解了盒子模型,我们才能更好的进行网页布局。HTML中的每一个元素都可以看成是一个盒子,拥有盒子一样的外形和平面空间,它不可见、不直观,但无处不在,所以初学者很容易在这上面出问题。今天就让我们来深入了解一下...
继续阅读 »

在网络设计的世界里,盒子模型是构建网页布局的基石,只有理解了盒子模型,我们才能更好的进行网页布局。

HTML中的每一个元素都可以看成是一个盒子,拥有盒子一样的外形和平面空间,它不可见、不直观,但无处不在,所以初学者很容易在这上面出问题。今天就让我们来深入了解一下盒子模型。

一、盒子模型是什么?

首先,我们来理解一下什么是CSS盒子模型。

简单来说,CSS盒子模型是CSS用来管理和布局页面上每一个元素的一种机制。每个HTML元素都可以被想象成一个矩形的盒子,这个盒子由内容(content)、内边距(padding)、边框(border)和外边距(margin)四个部分组成。

Description

这四个部分共同作用,决定了元素在页面上的最终显示效果。

二、盒子模型的组成部分

一个盒子由外到内可以分成四个部分:margin(外边距)、border(边框)、padding(内边距)、content(内容)。

Description

其中margin、border、padding是CSS属性,因此可以通过这三个属性来控制盒子的这三个部分。而content则是HTML元素的内容。

下面来一一介绍盒子模型的各个组成部分:

2.1 内容(Content)

内容是盒子模型的中心,它包含了实际的文本、图片等元素。内容区域是盒子模型中唯一不可或缺的部分,其他三部分都是可选的。

内容区的尺寸由元素的宽度和高度决定,但可以通过设置box-sizing属性来改变这一行为。

下面通过代码例子来了解一下内容区:

<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 200px;
height: 100px;
background-color: lightblue;
border: 2px solid black;
padding: 10px;
margin: 20px;
box-sizing: content-box; /* 默认值 */
}
</style>
</head>
<body>


<div>这是一个盒子模型的例子。</div>


</body>
</html>

Description

在这个例子中,.box类定义了一个具有特定样式的<div>元素。这个元素的宽度为200px,高度为100px,背景颜色为浅蓝色。边框为2像素宽的黑色实线,内边距为10像素,外边距为20像素。

由于我们设置了box-sizing: content-box;(这是默认值),所以元素的宽度和高度仅包括内容区的尺寸。换句话说,元素的宽度是200px,高度是100px,不包括内边距、边框和外边距。

如果我们将box-sizing属性设置为border-box,则元素的宽度和高度将包括内容区、内边距和边框,但不包括外边距。这意味着元素的总宽度将是234px(200px + 2 * 10px + 2 * 2px),高度将是124px(100px + 2 * 10px + 2 * 2px)。

总之,内容区是CSS盒子模型中的一个核心概念,它表示元素的实际内容所在的区域。通过调整box-sizing属性,您可以控制元素尺寸是否包括内容区、内边距和边框。

2.2 内边距(Padding)

内边距是内容的缓冲区,它位于内容和边框之间。通过设置内边距,我们可以在内容和边框之间创建空间,让页面看起来不会太过拥挤。

内边距是内容区和边框之间的距离,会影响到整个盒子的大小。

  • padding-top: ; 上内边距
  • padding-left:; 左内边距
  • padding-right:; 右内边距
  • padding-bottom:; 下内边距

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style type="text/css">
/*
1、 padding-top: ; 上内边距
padding-left:; 左内边距
padding-right:; 右内边距
padding-bottom:; 下内边距
2、padding简写 可以跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右


*/

.box1 {
width: 200px;
height: 200px;
background-color: #bfa;
/* padding-top:30px ;
padding-left: 30px;
padding-right: 30px;
padding-bottom: 30px; */

padding: 40px;
border: 10px transparent solid;
}
.box1:hover {
border: 10px red solid;
}

/*
* 创建一个子元素box2占满box1,box2把内容区撑满了
*/

.box2 {
width: 100%;
height: 100%;
background-color: yellow;
}
</style>
</head>
<body>
<div>
<div></div>
</div>
</body>
</html>

Description

2.3 边框(Border)

边框围绕在内边距的外围,它可以是实线、虚线或者其他样式。边框用于定义内边距和外边距之间的界限,同时也起到了美化元素的作用。

边框属于盒子边缘,边框里面属于盒子内部,出了边框都是盒子的外部,设置边框必须指定三个样式 边框大小、边框的样式、边框的颜色

  • 边框大小:border-width
  • 边框样式:border-style
  • 边框颜色:border-color

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<style type="text/css">


.box {
width: 0px;
height: 0px;
/* background-color: rgb(222, 255, 170); */
/* 边框的大小 如果省略,有默认值,大概1-3px ,不同的浏览器默认大小不一样
border-width 后可跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右

单独设置某一边的边框宽度
border-bottom-width
border-top-width
border-left-width
border-right-width
*/

border-width: 20px;
/* border-left-width:40px ; */
/*
边框的样式
border-style 可选值
默认值:none
实线 solid
虚线 dashed
双线 double
点状虚线 dotted
*/

border-style: solid;
/* 设置边框的颜色 默认值是黑色
border-color 也可以跟多个值
四个值 上 右 下 左
三个值 上 左右 下
二个值 上下 左右
一个值 上下左右
对应的方式跟border-width是一样
单独设置某一边的边框颜色
border-XXX-color: ;
*/

border-color: transparent transparent red transparent ;
}
.box1{
width: 200px;
height: 200px;
background-color: turquoise;
/* 简写border
1、 同时设置边框的大小,颜色,样式,没有顺序要求
2、可以单独设置一个边框
border-top:; 设置上边框
border-right 设置右边框
border-bottom 设置下边框
border-left 设置左边框
3、去除某个边框
border:none;
*/

border: blue solid 10px;
border-bottom: none;
/* border-top:10px double green ; */

}
</style>
</head>
<body>
<div></div>
<div></div>
</body>
</html>

Description

2.4 外边距(Margin)

外边距是元素与外界的间隔,它决定了元素与其他元素之间的距离。通过调整外边距,我们可以控制元素之间的相互位置关系,从而影响整体布局。

  • margin-top:; 正值 元素向下移动 负值 元素向上移动
  • margin-left:; 正值 元素向右移动 负值 元素向左移动
  • margin-bottom:; 正值 元素自己不动,其靠下的元素向下移动,负值 元素自己不动,其靠下的元素向上移动
  • margin-right: ; 正值负值都不动

代码示例:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style type="text/css">
/* 外边距 不会影响到盒子的大小
可以控制盒子的位置
margin-top:; 正值 元素向下移动 负值 元素向上移动
margin-left:; 正值 元素向右移动 负值 元素向左移动
margin-bottom:; 正值 元素自己不动,其靠下的元素向下移动,负值 元素自己不动,其靠下的元素向上移动
margin-right: ; 正值负值都不动
简写 margin 可以跟多个值
规则跟padding一样
*/

.box1 {
width: 200px;
height: 200px;
background-color: #bfa;
border: 10px solid red;
/* margin-top: -100px;
margin-left: -100px;
margin-bottom: -100px;
margin-right: -100px; */

margin: 40px;
}


.box2 {
width: 200px;
height: 200px;
background-color: yellow;
}
</style>
</head>
<body>
<div></div>
<div></div>
</body>
</html>

Description

三、盒子的大小

盒子的大小指的是盒子的宽度和高度。大多数初学者容易将宽度和高度误解为width和height属性,然而默认情况下width和height属性只是设置content(内容)部分的宽和高。

盒子真正的宽和高按下面公式计算

  • 盒子的宽度 = 内容宽度 + 左填充 + 右填充 + 左边框 + 右边框 + 左边距 + 右边距
  • 盒子的高度 = 内容高度 + 上填充 + 下填充 + 上边框 + 下边框 + 上边距 + 下边距

我们还可以用带属性的公式表示:

  • 盒子的宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right
  • 盒子的高度 = height + padding-top + padding-bottom + border-top + border-bottom + margin-top + margin-bottom

上面说到的是默认情况下的计算方法,另外一种情况下,width和height属性设置的就是盒子的宽度和高度。盒子的宽度和高度的计算方式由box-sizing属性控制。

Description

box-sizing属性值

content-box:默认值,width和height属性分别应用到元素的内容框。在宽度和高度之外绘制元素的内边距、边框、外边距。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

border-box:为元素设定的width和height属性决定了元素的边框盒。就是说,为元素指定的任何内边距和边框都将在已设定的宽度和高度内进行绘制。通过从已设定的宽度和高度分别减去 边框 和 内边距 才能得到内容的宽度和高度。

  • 当box-sizing:content-box时,这种盒子模型成为标准盒子模型;
  • 当box-sizing: border-box时,这种盒子模型称为IE盒子模型。

四、盒子模型应用技巧

掌握了盒子模型的基本概念后,我们就可以开始创造性地应用它来设计网页。以下是一些技巧:

  • 使用内边距来创建呼吸空间,不要让内容紧贴边框,这样可以让页面看起来更加舒适。

  • 巧妙运用边框来分隔内容区域,或者为特定的元素添加视觉焦点。

  • 利用外边距实现元素间的对齐和分组,保持页面的整洁和组织性。

  • 考虑使用负边距来实现重叠效果,创造出独特的层次感和视觉冲击力。

CSS盒子模型是前端开发的精髓之一,它不仅帮助我们理解和控制页面布局,还为我们提供了无限的创意空间。现在,你已经掌握了盒子模型的奥秘,是时候在你的项目中运用这些知识,创造出令人惊叹的网页设计了。

记住,每一个细节都可能是打造卓越用户体验的关键。开启你的CSS盒子模型之旅,让我们一起构建更加精彩、更加互动的网页世界!

收起阅读 »

爱猫程序员给自家小猪咪做了一个上门喂养小程序

web
🐱前言 每次一到节假日,都好想和好朋友们一起出去玩,但是心里总放不下家里的小猪咪,于是心想能不能找一个喂养师上门喂养呢。于是找了几个上门喂养的平台,并最终下单了服务。 真的不得不说,上门喂养的小姐姐真的非常用心和专业。她们来到我家期间,全情投入地照顾着我的毛孩...
继续阅读 »

🐱前言


每次一到节假日,都好想和好朋友们一起出去玩,但是心里总放不下家里的小猪咪,于是心想能不能找一个喂养师上门喂养呢。于是找了几个上门喂养的平台,并最终下单了服务。


真的不得不说,上门喂养的小姐姐真的非常用心和专业。她们来到我家期间,全情投入地照顾着我的毛孩子,让它吃得饱饱的,看起来胖乎乎的。更令我感动的是,她们还全程录像了照料过程,并将视频发送给我,让我能够时刻了解小猪咪的情况。


分享下小猪咪美照👇



🤔️思考


我也是程序员,为什么我不能自己也做一个上门喂养的小程序呢,于是经过调研,发现了其他的几个平台各有各的弊端和优点,然后诞生出了最初的想法如何做一个把其他平台的弊端去除掉,做一个最好的上门喂养平台。


🎨开始设计


于是开始琢磨figma~~~


因为任何c端都是通过首页再去衍生出其他页面整体样式的,所以先着手制作首页,只要首页定好了其他页面都好说。
image.png


一周后....开始着手设计🤔️...思考...参考....初版定稿


由于刚入门设计一开始丑丑的,不忍直视~~~



再经过几天的琢磨思考...改版...最终确定首页


经过不断的练习琢磨参考最后定稿,给大家推荐下我经常参考我素材网站花瓣



N天之后......其他页面陆续出炉


由于页面太多了就不一一展示啦~~~


image.png


最满意的设计页面


给各大掘友分享一下我最满意的设计页面


签到


结合了猫咪的元素统一使用了同一只猫咪素材~整体效果偏向手绘风格。


image.png


抽奖扭蛋


这个扭蛋机真是一笔一画画了一天才出来的,真的哭😭啦~,由于AE动画太过麻烦所以每一个扭蛋球球的滚动都用代码去实现~~


image.png



💻编程


技术选型


uniapp + nestjs + mysql


NestJS是一个基于Node.js的开发框架,它提供了一种构建可扩展且模块化的服务端应用程序的方式,所以对于前端而言是最好上手的一门语法。


Nestjs学习推荐


给各大掘友推荐一下本人从0到1上手nestjs的教程,就是一下小册就是以下这本,初级直接上手跟着写一遍基本就会啦


image.png


建议学习到 61章左右就可以开始写后端项目啦



小程序端基本使用逻辑



  • 用户下单-服务人员上门服务完成-用户检查完成后确认订单完成-订单款项打款到服务人员钱包

  • 用户注册成为服务人员-设置服务范围-上线开始服务-等待用户给服务人员下单


下单流程


选择服务地点-选择服务人员-点击预约-添加服务宠物-付款


image.png


服务人员认证流程


根据申请流程逐步填写,由于服务人员是平台与用户产生信任的标准,所以我们加大了通过审核的门槛,把一些只追求利益,而不是真正热爱宠物的人员拒之门外,保护双方利益。


image.png


后端Nestjs部署


后端代码写完之后我们需要把服务部署到腾讯云,以下是具体步骤


1.腾讯云创建镜像仓库


前往腾讯云创建容器镜像服务,这样我们就可以把本地docker镜像推送到腾讯云中了,这个容器镜像服务个人版是免费的


image.png


2.打包Nestjs


通过执行docker命令部署到本地的docker


image.png


👇以下是具体docker代码


FROM --platform=linux/amd64 node:18-alpine3.14 as build-stage

WORKDIR /app

COPY package.json .
COPY cert .
COPY catdogship.com_nginx .
COPY ecosystem.config.js .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

# 第一个镜像执行 build
RUN npm run build

# FROM 继承 node 镜像创建一个新镜像
FROM --platform=linux/amd64 node:18-alpine3.14 as production-stage

# 通过 COPY --from-build-stage 从那个镜像内复制 /app/dist 的文件到当前镜像的 /app 下
COPY --from=build-stage /app/package.json /app/package.json
COPY --from=build-stage /app/ecosystem.config.js /app/ecosystem.config.js

COPY --from=build-stage /app/dist /app/src/
COPY --from=build-stage /app/cert /app/cert/
COPY --from=build-stage /app/public /app/public/
COPY --from=build-stage /app/static /app/static/
COPY --from=build-stage /app/catdogship.com_nginx /app/catdogship.com_nginx/

WORKDIR /app

# 切到 /app 目录执行 npm install --production 只安装 dependencies 依赖
RUN npm install --production

RUN npm install pm2 -g

EXPOSE 443

CMD ["pm2-runtime", "/app/ecosystem.config.js"]

3.推送到腾讯云


本地打包完成之后我们需要把本地的docker推送到腾讯云中,所以我们本地写一个sh脚本执行推送


#!/bin/bash

# 生成当前时间
timestamp=$(date +%Y-%m-%d-%H-%M)

# Step 1: 构建镜像
docker build -t hello:$timestamp .

# Step 2: 查找镜像的标签
image_id=$(docker images -q hello:$timestamp)

# Step 3: 为镜像添加新的标签
docker tag $image_id 你的腾讯云镜像地址:$timestamp

docker push 你的腾讯云镜像地址:$timestamp

4.部署到服务器


由于我使用的是轻量级应用服务器,所以直接使用自动化助手去进行部署(PS:可能有一些小伙伴会问为什么用轻量级应用服务器呢,因为目前用户量不是很多,轻量级应用服务器足够支撑,后面用户量起来会考虑转为k8s集群


image.png


然后我们去创建一个自动化执行命令,去执行服务器的docker部署


image.png


创建命令


image.png


执行命令


image.png


👇以下是命令代码


# 停止服务
docker stop hello

# 删除容器
docker rm hello

# 拉取镜像
docker pull 你的腾讯云镜像地:{{key}}

#读取image名称
image_id=$(docker images -q 你的腾讯云镜像地:{{key}})

# 运行容器
docker run -d -p 443:443 -e TZ=Asia/Shanghai --name hello $image_id

5.部署完成


命令返回执行结果显示执行完成,说明已经部署成功了


image.png


6.Nestjs服务器端的管理


由于node是一个单线程,所以我们使用的是pm2去进行管理node,它可以把node变成一个多线程并进行管理


由于nestjs中使用到了定时任务,而定时任务只需要开一条线程去做就好了,所以我增加了一个环境变量NODE_ENV来对定时任务进行管理


module.exports = {
apps: [
{
name: 'wx-applets',
// 指定要运行的应用程序的入口文件路径
script: '/app/src/main.js',
exec_mode: 'cluster',
// 集群模式下的实例数-启动了2个服务进程
instances: 4,
// 如果设置为 true,则避免使用进程 ID 为日志文件添加后缀
combine_logs: true,
// 如果服务占用的内存超过300M,会自动进行重启。
// max_memory_restart: '1000M',
env: {
NODE_ENV: 'production',
},
},
{
name: 'wx-applets-scheduled-tasks',
script: '/app/src/main.js',
instances: 1,
// 采用分叉模式,创建一个单独的进程
exec_mode: 'fork',
env: {
NODE_ENV: 'tasks',
},
},
],
};

后端总结


到目前为止前台的业务接口都写完了做了个统计一共有179个接口


image.png


后期版本更新


预计这个月上线毛孩子用品盲盒抽奖,有兴趣的友友们也可关注下哦


Frame 2608921.png


后期展望,帮助更多的流浪动物有一个温暖的家


image.png

小程序上线


目前小程序已经上线啦~,友友们可以前往小程序搜索 喵汪舰 前往体验,
或者扫描一下二维码前往


开业海报.jpg

我的学习路线


因为我是一个前端开发,所以对于设计感觉还是挺好的,所以上手比较快。
一条学习建议路线:前端-后端-设计-产品,最终形成了一个完整的产品产出。


以下的链接是这个项目中我经常用到的素材网站:


freepik国外素材网站-可以找到大部份的插画素材


figma自带社区-获取参考的产品素材


花瓣国内素材参考网站-涵盖了国内基本的产品素材


pinterest国外大型素材网-你想到的基本都有


总结


一个产品的产出不仅仅依靠代码,还要好的用户体验,还需要不断的优化迭代,


最后给一起并肩前行的创业者们的一段话:


在创业的道路上,我们正在追逐梦想,挑战极限,为自己和世界创造新的可能性。这个旅程充满了风险和不确定性,但也蕴藏着无限的机遇和成就,不要害怕失败,勇于面对失败,将其视为成功的必经之路。


作者:热爱vue的小菜鸟
来源:juejin.cn/post/7348363812948033575
收起阅读 »

歌词滚动效果

web
需求 实现歌词滚动效果,类似网易云音乐播放界面。 实现 1.准备数据 后台一般会返回这样一个歌词数据,每个时间都对应当前这个歌词。 因为是字符串不方便直接使用,我们把他转化为对象格式。封装一个utils工具传入歌词把lyric转化为对象。 // 处理后端返...
继续阅读 »

需求


实现歌词滚动效果,类似网易云音乐播放界面。



实现


1.准备数据

后台一般会返回这样一个歌词数据,每个时间都对应当前这个歌词。



因为是字符串不方便直接使用,我们把他转化为对象格式。封装一个utils工具传入歌词把lyric转化为对象。


// 处理后端返回歌词
export const HandleLyric = (lyric) => {
    function convertToSeconds(timeArray) {
        // 获取分钟和秒(包括毫秒)  
        const minutes = parseFloat(timeArray[0]); // 使用parseFloat来确保即使分钟有小数部分也能正确处理  
        const secondsWithMilliseconds = parseFloat(timeArray[1]);
        // 计算总秒数  
        const totalSeconds = minutes * 60 + secondsWithMilliseconds;
        return totalSeconds;
    }
    let line = lyric.split('\n')
    let value1 = []
    for (let i = 0; i < line.length; i++) {
        let str = line[i]
        // 把字符串分割为数组
        let part = str.split(']')
        let timestr = part[0].substring(1)
        let parts = timestr.split(':')
        let obj = {
            time: convertToSeconds(parts),
            word: part[1]
        }
        value1.push(obj)
    }
    return value1
}


2.计算偏移量

准备audio标签


  <div class="audio">
      <el-button>立即播放/暂停</el-button>
      <audio :src="audio_info['url']" ref="audio" class="audio-sty">11</audio>
  </div>

调用audio标签的currentTime可以获取当前歌曲播放到第几秒,遍历歌词的时间和当前时间(currentTime)比较,返回的i就是当前歌词的在第几行。


const changeplay = () => {
    let audio = document.querySelector('.audio-sty')
     // 找到当前这一句歌词的索引
        function FindIndex() {
            let currentTime = audio.currentTime
            for (var i = 0; i < store.lyicWords.length; i++) {
                if (currentTime < store.lyicWords[i].time) {
                    return i - 1
                }
            }
            return store.lyicWords.length - 1
        } 
}

计算偏移量


        // 计算偏移量 
        /**
         * 偏移量
         * @containerHeight //容器高度
         * @PHeight   //单个歌词高度
         */
        function Setoffset() {
            var index = FindIndex()
            var offset = index * store.PHeight + store.PHeight / 2 - store.containerHeight / 2
            if (offset < 0) {
                offset = 0
            }
            store.index = index
            store.Offset = offset
        }


用当前歌词所偏移的大小加上单个歌词Height1/2的大小,就可以实现歌词偏移,如果想让每次高亮的歌词在中间,需要再减去container盒子自身Height的一半。




调用audio的timeupadte时间触发计算偏移函数


  // audio 时间变化事件
audio.addEventListener('timeupdate', Setoffset)

3.添加样式

<div class="center" ref="center">
            <h1>歌词</h1>
<el-card class="box-center">
      <div class="center-ci" style="overflow: auto;" ref="lyricHeight">
      <p v-for="(item, index) in  songList['txt']" v-if="songList['txt'].length != 0"
                        :class="[index == store.index ? 'active' : '']">
{{item }}</p>
       <p v-else>纯音乐,无歌词</p>
       </div>
</el-card>
</div>

使用transform: translateY(-50px);控制偏移

使用transform: scale(1.2);控制文字大小


     .center-ci {
                transformtranslateY(-50px);
                display: block;
                height100%;
                width100%;

                p {
                    transition0.2s;
                    font-size20px;
                    text-align: center;
                }

                .active {
                    transformscale(1.2);
                    color: goldenrod;
                }
            }

给歌词容器设置transform就可以实现歌词偏移了



// 根据偏移量滚动歌词 
lyricHeight.value.style.transform = `translateY(-${store.Offset}px)`

4.效果


~谢谢观看


作者:remember_me
来源:juejin.cn/post/7336538738749849600
收起阅读 »

微信小程序:优雅处理分页功能

web
背景 在公司的项目中,分页功能十分常见,以前就是简单的复制粘贴,每次来回切换切面,还经常复制错位置或复制到其他页面去了,半天找不到原因(内心:#*! $%^&@ 1)。 核心思路 分页原理: 属性 page来控制分页数,初始值为1;moreFlag有更多数...
继续阅读 »

背景



在公司的项目中,分页功能十分常见,以前就是简单的复制粘贴,每次来回切换切面,还经常复制错位置或复制到其他页面去了,半天找不到原因(内心:#*! $%^&@ 1)。



核心思路


分页原理:


属性 page来控制分页数,初始值为1;moreFlag有更多数据的标志,初始值为true;pageSize页面自行定义,可以默认设置为10


组件 原生scroll-viewrecycle-view,但内容页面定制化(循环detailList)


方法 bindscrolltolowerbindscrolltoUpper 调取接口获取更多数据,若结果集中的数据量小于pageSize时,moreFlag更新为false,界面激活“暂无更多”标识;bindrefresherrefresh时刷新重置变量为初始值。


后端接口对接 一般接口都会用Promise异步封装,但一般页面中的请求和处理都各不相同,所以不能作为共同点


graph TD
getListChangeStatus初始化 --> getList加载数据 --> getRemoteList调接口输出处理好的结果集 --> getList合并结果集 --> 下拉刷新重置pulldownRefresh

混入


根据上面的分页原理,大都为逻辑处理,所以考虑使用类似Vue里的混入,而在小程序中有一个类似的语法:Behavior

Behavior


image.png


behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins” 或 “traits”。


每个 behavior 可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。  每个组件可以引用多个 behavior ,behavior 也可以引用其它 behavior


定义Behavior


和组件的配置参数差不多,点击查看详情


注意 即使运用在Page里,方法也需放在methods里;同时注意一下同名字段和生命周期的规则,如下图所示:


image.png
以 scroll-view 按照分页原理来定义分页的Behavior


module.exports = Behavior({
data: {
page: 1,
moreFlag: true,
detailList: [],
refreshFlag: false
},
methods: {
// 初始化
async getListChangeStatus() {
this.setData({
page: 1,
detailList: [],
moreFlag: true
})
await this.getList()
},
// 获取列表
async getList() {
if (!this.data.moreFlag) {
... // 可以增加处理,例如吐司等
return
}
let { detailList,fun } = await this.getRemoteList()
if (detailList) {
let detailListTem = this.data.detailList
if (this.data.page == 1) {
detailListTem = []
}
if (detailList.length < this.data.pageSize) {
//表示没有更多数据了
this.setData({
detailList: detailListTem.concat(detailList),
moreFlag: false
})
} else {
this.setData({
detailList: detailListTem.concat(detailList),
moreFlag: true,
page: this.data.page + 1
})
}
// 可能需要一些处理,例如获取容器的高度之类的,又或者scroll-int0-view对应id
if(fun && fun instanceof Function) fun()
}
},
// 到达底部
reachBottom() {
console.log('--reachBottom--')
this.getList()
},
// 下拉刷新重置
pullDownRefresh() {
this.setData({
page: 1,
hasMoreData: true,
detailList: []
})
this.getList()
setTimeout(() => {
this.setData({
refreshFlag: false
})
}, 500)
}
}
})

引入方法


在页面或组件中(如果页面多个需要分页的地方建议用组件)使用behaviors: [myBehavior]


import pagination from '../../behaviors/pagination.js'
Page({
data: {
pageSize: 10
},
behaviors: [pagination],
onShow() {
this.getListChangeStatus()
},
async getRemoteList() {
let { page, pageSize } = this.data
const returnObj = {}
const res = await XXX // 请求接口
... // 处理数据
returnObj.detailList = data
return returnObj
}
})


<scroll-view scroll-y refresher-enabled refresher-triggered="{{refreshFlag}}" bindrefresherrefresh="pullDownRefresh" bindscrolltolower="reachBottom" style="height:100%;">

scroll-view>



作者:HuaHua的世界
来源:juejin.cn/post/7267417634478719036
收起阅读 »

微信小程序商城分类滚动列表锚点

web
一、需求背景最近接了个商城小程序的项目,在做商品分类页面的时候,一开始是普通分类列表,但是客户觉得效果不理想,想要滚动列表的效果,需要实现以下功能:列表滑动效果;滑动切换分类;点击分类跳转到相应的分类位置。思路是用使用官方组件scroll-view,给每个分类...
继续阅读 »

一、需求背景

最近接了个商城小程序的项目,在做商品分类页面的时候,一开始是普通分类列表,但是客户觉得效果不理想,想要滚动列表的效果,需要实现以下功能:

  1. 列表滑动效果;
  2. 滑动切换分类;
  3. 点击分类跳转到相应的分类位置。

思路是用使用官方组件scroll-view,给每个分类(子元素)添加锚点,然后记录每个分类项的高度,监听scroll-view组件滚动事件,计算分类的跳转

二、效果演示

录制_2023_04_18_11_25_56_701.gif

三、核心代码实现

下面要使用到的方法都来自于查阅微信小程序官方文档

  1. 创建一个scoll-view 并配置需要用到的属性scroll-int0-view 根据文档描述此属性是子元素的id,值为哪个就跳到那个子元素。为了使跳转不显得生硬,再添加scroll-with-animation属性,然后创建动态生成分类的dom元素并为每个子元素添加相应的id

image.png

        <view class="content">

<scroll-view scroll-y scroll-with-animation class="left" style="height:{{height}}rpx;" scroll-int0-view='{{leftId}}'>
<view id='left{{index}}' class="left-item {{activeKey===index?'active':''}}" wx:for="{{navData}}" data-index='{{index}}' wx:key='id' bindtap="onChange">
<text class='name'>{{item.name}}text>
view>
scroll-view>

<scroll-view class="right" scroll-y scroll-with-animation scroll-int0-view="{{selectedId}}" bindscroll="changeScroll" style='height:{{height}}rpx;'>

<view class="item" wx:for="{{goodslist}}" wx:key="id" id='type{{index}}'>

<view class="type">【{{item.name}}】view>

<view class="item-list">
<navigator class="list-item" wx:for="{{item.list}}" wx:for-item='key' wx:key="id" url='/pages/goods/goods?id={{key.id}}'>
<image style="width: 100%; height: 180rpx;" src="{{key.imgurl}}" />
<view class="item-name">{{key.goods_name}}view>
navigator>
view>
<view wx:if="{{item.list.length===0}}" class="nodata">
暂无商品
view>
view>
scroll-view>
view>

css部分

这里用到了吸顶效果position: sticky;

        .content {
width: 100%;
height: calc(100% - 108rpx);
overflow-y: hidden;
display: flex;

.left {
height: 100%;
overflow-y: scroll;
.left-item {
width: 100%;
padding: 20rpx;
box-sizing: border-box;

.name {
word-wrap: break-word;
font-size: 28rpx;
color: #323233;
}
}

.active {
border-left: 6rpx #ee0a24 solid;
background-color: #fff;
}
}

.right {
flex: 1;

.item {
position: relative;
padding: 20rpx;

.type {
margin-bottom: 10rpx;
padding: 5rpx;
position: sticky;
top: 0;
background-color: #fff;
}

.item-list {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20rpx;
text-align: center;

.item-name {
color: #3a3a3a;
font-size: 26rpx;
margin-top: 10rpx;
}
}

.nodata{
padding: 20rpx;
color: #ccc;
}
}
}
}

2. 在列表渲染完成之后计算出每个分类的高度并且保存成一个数组

// 用到的data
data:{
// 分类列表
navData:[],
// 商品列表
goodslist:[],
// 左侧分类选中项 分类列表数组的下标
activeKey:0,
// 计算出的锚点的位置
heightList:[],
// 右侧子元素的锚点
selectedId: 'type0',
// 左侧分类的锚点
leftId:'left0',
// scroll-view 的高度
height:0
},
onShow() {
let Height = 0;
wx.getSystemInfo({
success: (res) => {
Height = res.windowHeight
}
})
const query = wx.createSelectorQuery();
query.selectAll('.search').boundingClientRect()
query.exec((res) => {
// 计算滚动列表的高度 视口高度减去顶部高度 *2是因为拿到的是px 虽然也可以 但是我们通常使用的是rpx
this.setData({
height: (Height - res[0][0].height) * 2
})
})
},

//计算右侧每个锚点的高距离顶部的高
selectHeight() {
let h = 0;
const query = wx.createSelectorQuery();
query.exec((res) => {
console.log('res', res)
let arr=res[0].map((item,index)=>{
h+ = item.height
return h
})
this.setData({
heightList: arr,
})
console.log('height', this.data.heightList)
})
},

使用到的相关API image.png

  1. 监听scroll-view的滚动事件,通过滚动位置计算当前是哪个分类。
changeScroll(e) {
// 获取距离顶部的距离
let scrollTop = e.detail.scrollTop;
// 当前分类选中项,分类列表下标
let {activeKey,heightList} = this.data;
// 防止超出分类 判断滚动距离是否超过当前分类距离顶部高度
if (activeKey + 1 < heightList.length && scrollTop >= heightList[activeKey]) {
this.setData({
// 左侧分类选中项改变
activeKey: activeKey + 1,
// 左侧锚点对应位置
leftId: `left${activeKey + 1}`
})
}
if (activeKey - 1 >= 0 && scrollTop < heightList\[activeKey - 1]) {
this.setData({
activeKey: activeKey - 1,
leftId: `left${activeKey - 1}`
})
}
},

4. 监听分类列表点击事件,点击分类跳转相应的分类商品列表

onChange(event) {
let index = event.currentTarget.dataset.index
this.setData({
activeKey: index,
selectId: "item" + index
});
},

四、总结

左侧分类一开始是用的vantUI的滚动列表,但是分类过多就不会随着滑动分类滚动到可视位置,所以改成自定义组件,反正也很简单。

最初是想根据右侧滚动位置给左侧的scroll-view添加scroll-top,虽然实现,但是有时会有一点小问题目前没有想好怎么解决,改用右侧相同方法实现可以解决。

css部分使用scss编写,使用的是vscode的easy scss插件,具体方法百度一下,很简单。


作者:丝网如风
来源:juejin.cn/post/7223211123960660028

收起阅读 »

如何防止网站信息泄露(复制/水印/控制台)

web
一、前言 中午休息的时候,闲逛公司内网,看到一个url,就想复制一下url,看看url对应的内容是啥,习惯性使用ctrl+c,然后ctrl+v,最后得到是 禁止复制,哦,原来是禁用了复制。这能难倒一个前端开发吗?当然不能。于是打开了控制台,这时,发现页面自动执...
继续阅读 »

一、前言


中午休息的时候,闲逛公司内网,看到一个url,就想复制一下url,看看url对应的内容是啥,习惯性使用ctrl+c,然后ctrl+v,最后得到是 禁止复制,哦,原来是禁用了复制。这能难倒一个前端开发吗?当然不能。于是打开了控制台,这时,发现页面自动执行了一段立即执行函数,函数体里面是debugger代码,然后手动跳过debugger后,页面已经变成一个空白页面了。


本文将简单讲解禁止复制、水印和禁止打开控制台三个功能点的实现。


前面几节是分功能讲解,最后一节将会写出完整的代码,如何防止网站信息泄露。


二、禁止复制


现在有的复制网页(常规网页用户,不开控制台的情况)的内容方式有:



  1. 选择 -> ctrl+c(command + c)

  2. 选择 -> 鼠标右键 -> 复制


js拦截


相比user-select无法做到某一些内容可以被选中


document.addEventListener('contextmenu', function(e) {  
e.preventDefault();
}, false);

document.addEventListener('selectstart', function(e) {
e.preventDefault();
}, false);


user-select


不难发现,当我们复制内容的时候,首选是选择目标内容,那我们可以禁用用户的选择目标内容。


css属性user-select用于控制用户是否能够选择(即复制)文本和其他页面元素。它的作用是指定用户在浏览网页时是否能够选择和复制页面上的文本和其他元素。


<h3>user-select: none;</h3>
<div style="user-select: none;">我是捌玖,欢迎关注我哟,这儿是利用css样式,测试能否禁用复制</div>
<div style="user-select: none;">哈哈哈,当然,这种方式是无效的,我只是玩下</div>

那user-select和pointer-event的区别是啥?
pointer-events  指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target (en-US)。通俗一点讲,例如我们给某个元素设置了pointer-events: none,当我们点击这个元素的时候,是不会触发click事件,包括我们即使通过鼠标也无法选中该元素。

user-select 用于控制用户是否可以选择文本。这不会对作为浏览器用户界面(即 chrome)的一部分的内容加载产生任何影响,除非是在文本框中。



注意:user-select是无法拦截input中的选中(鼠标/ctrl+a)



拦截ctrl + a


每个人(系统)使用使用组合键进行复制,可能键是存在差异的,就拿我电脑是mac,默认是command + a为复制,当然是可以修改的,看个人使用习惯,所以我们要同时拦截掉ctrl + a 和 command + a。


document.keyDown = function(event) {
const {ctrlKey, metaKey, keyCode} = event;
if((ctrlKey || metaKey) && keyCode === 65) {
return false;
}
}

拦截ctrl+c(command + c)


每个人(系统)使用使用组合键进行复制,可能键是存在差异的,就拿我电脑是mac,默认是command + c为复制,当然是可以修改的,看个人使用习惯,所以我们要同时拦截掉ctrl + c 和 command + c的。不可以直接拦截c键的输入,会影响到input框的输入


document.keyDown = function(event) {
const {ctrlKey, metaKey, keyCode} = event;
if((ctrlKey || metaKey) && keyCode === 67) {
return false;
}
}

直接拦截鼠标右键


该方法直接拦截了右键菜单的打开,主要用于拦截图片的复制,菜单中的复制图片的方法有多种(复制图片、复制图片地址等),暂时没找到合适的方法拦截菜单中具体的某一项。
image.png


document.oncontextmenu = function(event){
if(event.srcElement.tagName=="IMG"){
alert('图片直接右键');
return false;
}
};

三、水印


网站水印的主要作用是版权保护和网站标识展示。具体来说,它可以在图片上加上作者的信息或标识,防止他人未经授权使用,有助于保护图片的版权安全。同时,它也可以在网页中展示特定的标识或信息,如网站的名称、网址、版权信息等,有助于提高网站的知名度和品牌形象。


此外,网站水印还可以用于追踪网站的非法使用和侵权行为。当发现某个网站上出现了未经授权的水印,就可以通过水印的信息追踪到该网站的使用者,从而采取相应的法律措施。


// 创建Canvas元素  
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

// 设置Canvas尺寸和字体样式
canvas.width = 100;
canvas.height = 100;
context.font = '30px Arial';
context.fillStyle = 'rgba(0,0,0,0.1)';

// 绘制文字到Canvas上
context.fillText('捌玖', 10, 50);

// 生成水印图像的URL
const watermarkUrl = canvas.toDataURL();

// 在页面上显示水印图像(或进行其他操作)
const divDom = document.createElement('div');
divDom.style.position = 'fixed';
divDom.style.zIndex = '99999';
divDom.style.top = '-10000px';
divDom.style.bottom = '-10000px';
divDom.style.left = '-10000px';
divDom.style.right = '-10000px';
divDom.style.transform = 'rotate(-45deg)';
// 避免对用户的交互产生影响
divDom.style.pointerEvents = 'none';
divDom.style.backgroundImage = `url(${watermarkUrl})`;
document.body.appendChild(divDom);

四、禁止打开控制台


禁止用户打开控制台



  1. 防止代码被窃取:在控制台中,用户可以查看和修改网页的源代码,这可能会导致恶意用户窃取您的代码或敏感信息。通过禁止打开控制台,可以保护您的代码和数据不被未经授权的用户访问或篡改。

  2. 提高安全性:控制台是网页与用户之间进行交互的主要渠道之一,但也是潜在的安全风险之一。恶意用户可以利用控制台执行恶意代码、进行钓鱼攻击等。禁止用户打开控制台可以减少这些潜在的安全风险。

  3. 保护系统资源:在控制台中,用户可以执行各种操作,例如创建新文件、删除文件等,这可能会对系统资源造成不必要的占用和破坏。禁止打开控制台可以限制用户的操作范围,保护系统资源不被滥用。

  4. 防止误操作:控制台允许用户进行各种操作,但这也增加了误操作的风险。禁止打开控制台可以减少用户误操作的可能性,避免不必要的损失和风险。


let firstTime;
let lastTime;
setInterval(() => {
firstTime = Date.now()
debugger
lastTime = Date.now()
if (lastTime - firstTime > 10) {
window.location.href = "about:blank";
}
}, 100)

五、总结


    // 防止用户选中
function disableSelect() {
// 方式一,js拦截
// 缺点: 无法做到某一些内容可以选中
document.onselectstart = function(event){
e.preventDefault();
};


// 方式:给body设置样式
document.body.style.userSelect = 'none';


// 禁用input的ctrl + a
document.keyDown = function(event) {
const {ctrlKey, metaKey, keyCode} = event;
if((ctrlKey || metaKey) && keyCode === 65) {
return false;
}
}
};

// 禁用键盘的复制
function disableCopy() {
const {ctrlKey, metaKey, keyCode} = event;
if((ctrlKey || metaKey) && keyCode === 67) {
return false;
}
}

// 禁止复制图片
function disableCopyImg() {
document.oncontextmenu = function(event){
if(event.srcElement.tagName=="IMG"){
alert('图片直接右键');
return false;
}
};
};

// 生成水印
function generateWatermark(keyword = '捌玖') {
// 创建Canvas元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');

// 设置Canvas尺寸和字体样式
canvas.width = 100;
canvas.height = 100;
context.font = '30px Arial';
context.fillStyle = 'rgba(0,0,0,0.1)';

// 绘制文字到Canvas上
context.fillText(keyword, 10, 50);

// 生成水印图像的URL
const watermarkUrl = canvas.toDataURL();

// 在页面上显示水印图像(或进行其他操作)
const divDom = document.createElement('div');
divDom.style.position = 'fixed';
divDom.style.zIndex = '99999';

// 因为div旋转了45度,所以div需要足够的大
divDom.style.top = '-10000px';
divDom.style.bottom = '-10000px';
divDom.style.left = '-10000px';
divDom.style.right = '-10000px';
divDom.style.transform = 'rotate(-45deg)';

// 防止对用户的交互产生影响
divDom.style.pointerEvents = 'none';
divDom.style.backgroundImage = `url(${watermarkUrl})`;
document.body.appendChild(divDom);
}

// 禁止打开控制台
function disbaleConsole() {
let firstTime
let lastTime
setInterval(() => {
firstTime = Date.now()
debugger
lastTime = Date.now()
if (lastTime - firstTime > 10) {
window.location.href = "about:blank";
}
}, 100);
}

disableSelect();
disableCopy();
disableCopyImg();
generateWatermark();
disbaleConsole();

作者:捌玖ki
来源:juejin.cn/post/7292416512333332534
收起阅读 »

ES2024即将发布!5个可能大火的JS新方法

web
Hello,大家好,我是 Sunday。 ECMAScript 2024(ES15) 即将发布(2024年6月),新的版本带来了非常多全新的特性。其中有 5 个全新的 JS 方法,可以大幅度提升我们的工作效率,从而让我们得到更多的摸鱼时间。咱们一起来看看吧! ...
继续阅读 »

Hello,大家好,我是 Sunday。


ECMAScript 2024(ES15) 即将发布(2024年6月),新的版本带来了非常多全新的特性。其中有 5 个全新的 JS 方法,可以大幅度提升我们的工作效率,从而让我们得到更多的摸鱼时间。咱们一起来看看吧!


01:Promise.withResolvers


这个功能引入了一个新方法来创建一个 promise,直接返回 resolve 和 reject 的回调。使用 Promise.withResolvers ,我们可以创建直接在其执行函数之外 resolve 和 reject


const [promise, resolve, reject] = Promise.withResolvers();

setTimeout(() => resolve('Resolved after 2 seconds'), 2000);

promise.then(value => console.log(value));

02:Object.groupBy()


Object.groupBy() 方法是一项新添加的功能,允许我们按照特定属性将数组中的 对象分组,从而使数据处理变得更加容易。


const pets = [
{ gender: '男', name: '张三' },
{ gender: '女', name: '李四' },
{ gender: '男', name: '王五' }
];

const res = Object.groupBy(pets, pet => pet.gender);
console.log(res);
// 输出:
// {
// 女: [{ gender: '女', name: '李四' }]
// 男: [{ gender: '男', name: '张三' }, { gender: '男', name: '王五' }],
// }

03:Temporal


Temporal提案引入了一个新的API,以更直观和高效的方式 处理日期和时间。例如,Temporal API提供了新的日期、时间和持续时间的数据类型,以及用于创建、操作和格式化这些值的函数。


const today = Temporal.PlainDate.from({ year: 2023, month: 11, day: 19 });
console.log(today.toString()); // 输出: 2023-11-19

const duration = Temporal.Duration.from({ hours: 3, minutes: 30 });
const tomorrow = today.add(duration);
console.log(tomorrow.toString()); // 输出: 2023-11-20

04:Records 和 Tuples


Records 和 Tuples 是全新的数据结构,提供了一种更简洁和类型安全的方式来创建对象和数组。



  • Records 类似于对象,但具有具体类型的固定属性集。

  • Tuples 类似于数组,但具有固定长度,每个元素可以具有不同类型。


let record = #{
id: 1,
name: "JavaScript",
year: 2024
};
console.log(record.name); // 输出: JavaScript

05:装饰器(Decorators)


装饰器(Decorators)是一种提议的语法,用于添加元数据或修改类、函数或属性的行为。装饰器可用于实现各种功能,如日志记录、缓存和依赖注入。


function logged(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
return descriptor;
}

class Example {
@logged
sum(a, b) {
return a + b;
}
}

const e = new Example();
e.sum(1, 2); // 输出:[1, 2]

其他


ES15 还提供了很多其他的新提案,比如:新的正则v、管道符|>String.prototype.isWellFormed()ArrayBuffer.prototype.resize 等等。大家有兴趣的同学可以额外到 mdn 网站上进行了解~~



前端训练营:1v1私教,终身辅导计划,帮你拿到满意的 offer 已帮助数百位同学拿到了中大厂 offer。欢迎来撩~~~~~~~~



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

答应我,在vue中不要滥用watch好吗?

web
前言 上周五晚上8点,开开心心的等着产品验收完毕后就可以顺利上线。结果产品突然找到我说要加需求,并且维护这一块业务的同事已经下班走了,所以只有我来做。虽然内心一万头草泥马在狂奔,但是嘴里还是一口答应没问题。由于这一块业务很复杂并且我也不熟悉,加上还饿着肚子,在...
继续阅读 »

前言


上周五晚上8点,开开心心的等着产品验收完毕后就可以顺利上线。结果产品突然找到我说要加需求,并且维护这一块业务的同事已经下班走了,所以只有我来做。虽然内心一万头草泥马在狂奔,但是嘴里还是一口答应没问题。由于这一块业务很复杂并且我也不熟悉,加上还饿着肚子,在梳理代码逻辑的时候我差点崩溃了。需要修改的那个vue文件有几千行代码,迭代业务对应的ref变量有10多个watch。我光是梳理这些watch的逻辑就搞了很久,然后小心翼翼的在原有代码上面加上新的业务逻辑,不敢去修改原有逻辑(担心搞出线上bug背锅)。


滥用watch带来的问题


首先我们来看一个例子:


<template>
{{ dataList }}
</template>

<script setup lang="ts">
import { ref, watch } from "vue";

const dataList = ref([]);
const props = defineProps(["disableList", "type", "id"]);
watch(
() => props.disableList,
() => {
// 根据disableList逻辑很复杂同步计算出新list
const newList = getListFromDisabledList(dataList.value);
dataList.value = newList;
},
{ deep: true }
);
watch(
() => props.type,
() => {
// 根据type逻辑很复杂同步计算出新list
const newList = getListFromType(dataList.value);
dataList.value = newList;
}
);
watch(
() => props.id,
() => {
// 从服务端获取dataList
fetchDataList();
},
{ immediate: true }
);
</script>

上面这个例子在template中渲染了dataList,当props.id更新时和初始化时从服务端异步获取dataList。当props.disableListprops.type更新时,同步的计算出新的dataList。


代码逻辑流程图是这样的:


bad-code.png


乍一看上面的代码没什么问题,但是当一个不熟悉这一块业务的新同学接手这一块代码时问题就出来了。


我们平时接手一个不熟悉的业务首先要找一个切入点,对于前端业务,切入点肯定是浏览器渲染的页面。在 Vue 中,页面由模板渲染而来,找到模板中使用的响应式变量和他的来源,就能理解业务逻辑。以 dataList 变量为例,梳理dataList的来源基本就可以理清业务逻辑。


在我们上面的这个例子dataList的来源就是发散的,有很多个来源。首先是watchprops.id从服务端异步获取。然后是watchprops.disableListprops.type,同步更新了dataList。这个时候一个不熟悉业务的同学接到产品需求要更新dataList的取值逻辑,他需要先熟悉dataList多个来源的取值逻辑,熟悉完逻辑后再分析我到底应该是在哪个watch上面去修改业务逻辑完成产品需求。


但是实际上我们维护别人的代码时(特别是很复杂的代码)一般都不愿意去改代码,而是在原有代码的基础上再去加上我们的代码。因为去改别人的复杂代码很容易搞出线上bug,然后背锅。所以在这里我们的做法一般都是再加一个watch,然后在这个watch中去实现产品最新的dataList业务逻辑。


watch(
() => props.xxx,
() => {
// 加上产品最新的业务逻辑
const newList = getListFromXxx(dataList.value);
dataList.value = newList;
}
);

迭代几次业务后这个vue文件里面就变成了一堆watch,屎山代码就是这样形成的。当然不排除有的情况是故意这样写的,为的就是稳定自己在团队里面的地位,因为离开了你这坨代码没人敢动。


关注公众号:前端欧阳,解锁我更多vue干货文章,并且可以免费向我咨询vue相关问题。


使用computed解决问题


我们看了上面的反例,那么一个易维护的代码是怎么样的呢?我认为应该是下面这样的:


line.png


dataListtemplate中渲染,然后同步更新dataList,最后异步从服务端异步获取dataList,整个过程能够被穿成一条线。此时新来一位同学要去迭代dataList相关的业务,那么他只需要搞清楚产品的最新需求是应该在同步阶段去修改代码还是异步阶段去修改代码,然后在对应的阶段去加上对应的最新代码即可。


我们来看看上面的例子应该怎么优化成易维护的代码,上面的代码中dataList来源主要分为同步来源和异步来源。异步来源这一块我们没法改,因为从业务上来看props.id更新后必须要从服务端获取最新的dataList。我们可以将同步来源的代码全部摞到computed中。优化后的代码如下:


<template>
{{ renderDataList }}
</template>

<script setup lang="ts">
import { ref, computed, watch } from "vue";

const props = defineProps(["disableList", "type", "id"]);
const dataList = ref([]);

const renderDataList = computed(() => {
// 根据disableList计算出list
const newDataList = getListFromDisabledList(dataList.value);
// 根据type计算出list
return getListFromType(newDataList);
});

watch(
() => props.id,
() => {
// 从服务端获取dataList
fetchDataList();
},
{
immediate: true,
}
);
</script>

我们在template中渲染的不再是dataList变量,而是renderDataListrenderDataList是一个computed,在这个computed中包含了所有dataList同步相关的逻辑。代码逻辑流程图是这样的:


good-code.png


此时一位新同学接到产品需求要迭代dataList相关的业务,因为我们的整个业务逻辑已经变成了一条线,新同学就可以很快的梳理清楚业务逻辑。再根据产品的需求看到底应该是修改同步相关的逻辑还是异步相关的逻辑。下面这个是修改同步逻辑的demo:


const renderDataList = computed(() => {
// 加上产品最新的业务逻辑
const xxxList = getListFromXxx(dataList.value);
// 根据disableList计算出list
const newDataList = getListFromDisabledList(xxxList);
// 根据type计算出list
return getListFromType(newDataList);
});

总结


这篇文章介绍了watch主要分为两种使用场景,一种是当watch的值改变后需要同步更新渲染的dataList,另外一种是当watch的值改变后需要异步从服务端获取要渲染的dataList。如果不管同步还是异步都一股脑的将所有代码都写在watch中,那么后续接手的维护者要梳理dataList相关的逻辑就会非常痛苦。因为到处都是watch在更新dataList的值,完全不知道应该在哪个watch中去加上最新的业务逻辑,这种时候我们一般就会再新加一个watch然后在新的watch中去实现最新的业务逻辑,时间久了代码中就变成了一堆watch,维护性就变得越来越差。我们给出的优化方案是将那些同步更新dataListwatch代码全部摞到一个名为renderDataListcomputed,后续维护者只需要判断新的业务如果是同步更新dataList,那么就将新的业务逻辑写在computed中。如果是要异步更新dataList,那么就将新的业务逻辑写在watch中。


作者:前端欧阳
来源:juejin.cn/post/7340573783744102435
收起阅读 »

H5、小程序商品加入购物车的抛物线动画如何实现

web
H5、小程序商品加入购物车的抛物线动画如何实现 电商类 H5、小程序把商品加入到购物车时,常常有一个抛物线动画。比如麦当劳小程序,当你点击加购按钮时,会看到有一个小汉堡从卡片汉堡上抛出,然后掉落到购物袋里。 这种动画该怎么做呢?如果你也想实现它,看完这篇文章...
继续阅读 »

H5、小程序商品加入购物车的抛物线动画如何实现


电商类 H5、小程序把商品加入到购物车时,常常有一个抛物线动画。比如麦当劳小程序,当你点击加购按钮时,会看到有一个小汉堡从卡片汉堡上抛出,然后掉落到购物袋里。


mcdonald's.gif


这种动画该怎么做呢?如果你也想实现它,看完这篇文章,你一定有所收获。我会先说明抛物线动画的原理,再解释实现它的关键代码,最后给出完整的代码示例。代码效果如下:


parabola.gif


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的前端武学。


抛物线动画的原理


高中物理告诉我们,平抛运动、斜抛运动可以分解为水平方向的匀速直线运动、竖直方向自由落体运动(匀加速直线运动)。


principle.png


同理,我们可以把抛物线动画分解为水平的匀速动画、竖直的匀加速动画。


水平匀速动画很容易实现,直接 animation-timing-function 取值 linear 就行。


竖直的匀加速直线运动,严格实现非常困难,我们可以近似实现。因为匀加速直线运动,速度是越来越快的,所以我们可以用一个先慢后快的动画替代,你可能立刻就想到给 animation-timing-function 设置 ease-in。不过 ease-in 先慢后快的效果不是很明显。针对这个问题,张鑫旭大佬提供了一个贝塞尔曲线 cubic-bezier(0.55, 0, 0.85, 0.36);1。当然,你也可以用 cubic-bezier 自己调一个先慢后快的贝塞尔曲线。


关键代码实现


我们把代码分为两部分,第一部分是布局代码、第二部分是动画代码。


布局代码


首先是 HTML 代码,代码非常简单。下图中小球代表商品、长方形代表购物车。


ball-and-cart.png


<div class="ball-box">
<div class="ball"></div>
</div>
<div class="cart"></div>

你可能比较好奇,小球用一个 ball 元素就可以实现,为什么我要用 ball 和 ball-box 两个元素呢?因为 animation 只能给一个元素定义一个动画效果,而我们需要给小球添加两个动画效果。于是我们将动画分解,给 ball-box 添加水平运动的动画,给 ball 添加竖直运动的动画。


动画代码


再看动画代码,moveX 是水平运动动画,moveY 是竖直动画,动画时间均为 1s。为了让效果更加明显,我还特意将动画设置成无限循环的动画。


.ball-box {
/* ... */
animation: moveX 1s linear infinite;
}

.ball {
/* ... */
animation: moveY 1s cubic-bezier(0.55, 0, 0.85, 0.36) infinite;
}

@keyframes moveX {
to {
transform: translateX(-250px);
}
}

@keyframes moveY {
to {
transform: translateY(250px);
}
}

代码示例



总结


本文我们介绍了抛物线动画的实现方法,我们可以将抛物线动画拆分为水平匀速直线动画和竖直从慢变快的动画,水平动画我们可以使用 linear 来实现,竖直动画我们可以使用一个先慢后快的贝塞尔曲线替代。设置动画时,我们还需要注意,一个元素只能设置一个动画。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的前端武学。


Footnotes




作者:小霖家的混江龙
来源:juejin.cn/post/7331607384933220390
收起阅读 »

面试官:在连续请求过程中,如何取消上次的请求?

web
前言 这个问题想必很多朋友都遇到过,我再详细说一下场景! 如 Boss 搜索框所示: 先输入1 再输入2 再输入3 再输入123  请求参数依次为:1 12 123 123123 请求参数通过右侧的 query 参数也可以看到,一共请求了...
继续阅读 »

前言


这个问题想必很多朋友都遇到过,我再详细说一下场景!


如 Boss 搜索框所示:




先输入1




再输入2




再输入3




再输入123 




请求参数依次为:1 12 123 123123



请求参数通过右侧的 query 参数也可以看到,一共请求了四次。


不难发现,这里已经做了基本的防抖,因为我们连续输入123的时候,只发了一次请求。


好了,现在看完基本场景,我们回到正题!


从上面的演示中不难发现我们一共发送了4次请求,顺序依次为1、12、123、123123。


问题


面试官现在问题如下:



我先输入的 1,已经发送请求了,紧接着输入了 2,3,123,如果在我输入最后一次 123 的时候,我第一次输入的 1 还没有请求成功,那请求 query 为 1 的这个接口将会覆盖 query 为 123123 的搜索结果,因为当 1 成功的时候会将最后一次请求的结果覆盖掉,当然这个概率很小很小,现在就这个bug,说一下你的解决思路吧!



解决


看到这个问题我们首先应该思考的是如何保证后面的请求不被前面的请求覆盖掉,首先说一下防抖是不行的,防抖只是对连续输入做了处理,并不能解决这个问题,上面的演示当中应该不难发现。


如何保证后面的请求不被前面的请求覆盖掉?


我们思路是否可以转化为:只需要保证后面的每次接口请求都是最新的即可?


简单粗暴一点就是,我们后续请求接口时直接把前面的请求干掉即可!


那如何在后续请求时,直接干掉之前的请求?


关键:使用 AbortController


MDN 解释如下:



AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。


AbortController.abort(),中止一个 尚未完成 的 Web(网络)请求。



MDN 文档如下:



AbortController - Web API 接口参考 | MDN (mozilla.org)



我们可以借助 AbortController 直接终止还 未完成 的接口请求,注意这里说的是还未完成的接口,如果接口已经请求成功就没必要终止了。


代码实现


参考代码如下:


    let currentAbortController = null;
function fetchData(query) {
// 取消上一次未完成的请求
if (currentAbortController) {
currentAbortController.abort();
}

// 创建新的 AbortController
currentAbortController = new AbortController();

return fetch(`/api/search?q=${query}`, {
signal: currentAbortController.signal
})
.then(response => response.json())
.then(data => {
// 处理请求成功的数据
updateDropdownList(data);
})
.catch(error => {
// 只有在请求未被取消的情况下处理错误
if (!error.name === 'AbortError') {
handleError(error);
}
});
}

借用官方的解释:



当 fetch 请求初始化时,我们将 AbortSignal 作为一个选项传递进入请求的选项对象中(下面的 {signal: currentAbortController.signal})。这将 signal 和 controller 与 fetch 请求相关联,并且允许我们通过调用 AbortController.abort() 去中止它!



这就意味着我们将 signal 作为参数进行传递,当我们调用 currentRequest.abort() 时就可以终止还未完成的接口请求,从而达到我们的需要。


我们在每次重新调用这个接口时,判断是否存在 AbortController 实例,如果存在直接中止掉该实例即可,这样就可以保证我们每次的请求都可以拿到最新的数据。


    if (currentAbortController) {
currentAbortController.abort();
}

总结


我们再来理一下这个逻辑:


首先是第一次调用时为接口请求添加 AbortSignal 参数


之后在每次进入都判断是否存在 AbortController 实例,有的话直接取消掉


取消只会针对还未完成的请求,已经完成的不会取消


通过这样就可以达到我们每次都会使用最新的请求接口作为数据来源,因为后面的接口会将前面的干掉


如果这道面试题这样回答,是不是还不错?


作者:JacksonChen
来源:juejin.cn/post/7347395265836924938
收起阅读 »

面试官:为什么不用 index 做 key?

web
Holle 大家好,我是阳阳羊,在前两天的面试中,面试官问了这样一个问题:“在 Vue 中,我们在使用 v-for 渲染列表的时候,为什么要绑定一个 key?能不能用 index 做 key?” 在聊这个问题之前我们还得需要知道 Vue 是如何操作 DOM 结...
继续阅读 »

Holle 大家好,我是阳阳羊,在前两天的面试中,面试官问了这样一个问题:“在 Vue 中,我们在使用 v-for 渲染列表的时候,为什么要绑定一个 key?能不能用 indexkey?”


在聊这个问题之前我们还得需要知道 Vue 是如何操作 DOM 结构的。


虚拟DOM


我们知道,Vue 不可以直接操作 DOM 结构,而是通过数据驱动、指令等机制来间接操作 DOM 结构。当我们修改模版中的数据时,Vue 会触发重新渲染过程,调用render函数,它会返回一个 虚拟 DOM 树,它描述了整个组件模版的结构。


举个栗子🌰:


<template>
<ul class="list">
<li v-for="item in list" :key="item.index" class="item">{{ item }}</li>
</ul>
</template>

<script setup>
import { ref } from 'vue';
const list = ref(['html', 'css', 'js'])
</script>

Vue 在渲染这个列表时,就会调用render函数,它会返回一个类似下面这个虚拟 DOM 树。


let VDom = {
tagName: 'ul',
props: {
class: 'list'
},
chilren: [
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['html']
},
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['css']
},
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['js']
}
]
}

虚拟 DOM 的每个节点对应于真实 DOM 树中的一个节点。


当我们修改数据时,Vue 又会触发重新渲染的过程。


const list = ref(['html', 'css', 'vue']) //修改列表第三项'js'->'vue'

Vue 又会生成一个新的虚拟DOM树:


let VDom = {
tagName: 'ul',
props: {
class: 'list'
},
chilren: [
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['html']
},
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['css']
},
{
tagName: 'li',
props: {
class: 'item'
},
chilren: ['vue']
}
]
}

注意观察,这里最后一个节点的子节点为'vue',发生了数据变化,Vue内部又会返回一个新的虚拟 DOM。那么 Vue 是如何将这个变化响应给页面的呢?


摆在面前的有两条路


要么重新渲染这个新的虚拟 DOM ,要么只新旧虚拟 DOM 之间改变的地方。


显而易见,只渲染修改了的地方是不是会更节省性能。


巧了,尤雨溪也是这样想的,于是便有了“ Diff 算法 ”。


Diff 算法


Vue 将新生成的新虚拟 DOM 与上一次渲染时生成的旧虚拟 DOM 进行比较,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点。


我自己总结了一下Diff算法的过程,由于代码过多,就不在此展示了:




  1. 新旧虚拟DOM对比的时候,Diff 算法比较只会在同层级进行,不会跨层级比较。

  2. 首先比较两个节点的类型,如果类型不同,则废弃旧节点并用新节点替代。

  3. 对于相同类型的节点,进一步比较它们的属性。记录属性差异,以便生成相应的补丁。

  4. 如果两个节点相同,继续递归比较它们的子节点,直到遍历完整个树。

  5. 如果节点有唯一标识,可以通过这些标识来快速定位相同标识的节点。

  6. 如果节点的相同,只是顺序变化,不会执行不必要的操作。



面试官:为什么不用 index 做 key?


平常v-for循环渲染的时候,为什么不建议用 index 作为循环项的 key 呢?


举个栗子🌰:


<div id="app">
<ul>
<li v-for="item in list" :key="item.index">{{item}}</li>
</ul>
<button @click="add">添加</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(['html', 'css', 'js']);
const add=()=> {
list.value.unshift('阳阳羊');
}
return {
list,
add
}
}
}).mount('#app')
</script>

这里用 indexkey渲染这个列表,我们通过 add 方法在列表的前面添加一项。


GIF 2024-3-5 23-57-41.gif


我们发现添加操作导致的整个列表的重新渲染,按道理来说,Diff 算法会复用后面的三项,因为它们只是位置发生了变化,内容并没有改变。但是我们回过头来发现,我们在前面添加了一项,导致后面三项的 index 变化,从而导致 key 值发生变化。Diff 算法失效了?


那我们可以怎么解决呢?其实我们只要使用一个独一无二的值来当做key就行了


<div id="app">
<ul>
<li v-for="item in list" :key="item.id">{{item.name}}</li>
</ul>
<button @click="add">添加</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const list = ref(
[
{ name: "html", id: 1 },
{ name: "css", id: 2 },
{ name: "js", id: 3 },
]);
const add=()=> {
list.value.unshift({ name: '阳阳羊', id: 4 });
}
return {
list,
add
}
}
}).mount('#app')
</script>

GIF 2024-3-6 0-09-39.gif


这样,key就是永远不变的,更新前后都是一样的,并且又由于节点的内容本来就没变,所以 Diff 算法完美生效,只需将新节点添加到真实 DOM 就行了。


最后


看到这里,希望你已经对Diff 算法有了初步的了解,想要深入了解,可以自行查看Diff 源码。总的来说,Diff 算法是一项关键的技术,为构建响应式和高效的用户界面提供了基础。最后,祝你面试顺利,学习进步!



如果你正在面临春招,或者对面试有所疑惑,欢迎评论 / 私信,我们报团取暖!




技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 “点赞 收藏+关注” ,感谢支持!!



作者:阳阳羊
来源:juejin.cn/post/7342793254096109583
收起阅读 »

完美解决html2canvas + jsPDF导出pdf分页echarts内容截断问题

web
想直接看解决方案的可跳过我的絮絮叨叨 有段时间没有更新内容了,一方面是自己在沉淀,二是前段时间学着剪vlog想着把自己上下班及中午锻炼的碎片整理出来发到网上,咱也做一个自媒体博主,实现时间自由,财富自由,走上巅峰,登上福布斯,弹劾小日子,哈哈哈,当然有想法是好...
继续阅读 »

想直接看解决方案的可跳过我的絮絮叨叨


有段时间没有更新内容了,一方面是自己在沉淀,二是前段时间学着剪vlog想着把自己上下班及中午锻炼的碎片整理出来发到网上,咱也做一个自媒体博主,实现时间自由,财富自由,走上巅峰,登上福布斯,弹劾小日子,哈哈哈,当然有想法是好的,别管最后咋样先动起来,在行动的这个过程中总会有意想不到的收获,沉淀的过程中我也尝试着写过一些无厘头的文,巴拉巴拉我在说什么,先搞正事,完事在絮叨。


image.png


事件起因


像往常一样,我在霹雳吧啦的敲着26个字母,产品大佬过来说,小帅咱们客户反映线上导出的数据统计有问题,我想不对啊,数据有问题?不可能吧,数据有问题,应该是去找后端吧,找我干啥,是需要我跟进这个问题嘛?原来是历史问题啊~!页面数据涉及到 柱状图、饼状图、折线图和一些数据的展示呈现出现了中间断裂/截断问题,导致导出的pdf格式打印出来不美观,影响用户体验


image.png


简单分析



  1. html2canvas + jsPDF现状导出pdf是一个整体,以a4的高度进行分页,问题的主要原因

  2. 需要对页面元素进行计算,1 + 2大于 a4的高度就另起一页 简单说干就干

  3. 打开百度一搜,why?为啥都没有完美的解决方法,倔友的一些方法也都试了,多少都存在问题不能解决。得,还是得自己搞。


核心代码 代码经过测试 可直接使用


我知道大家进来都想直接找解决问题的方法,因为我带着问题去找答案也一样,先解决了再听他们絮叨。上才艺展示,如果能帮到你请回来看我絮叨。


import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf'

// pdfDom 页面dom , spacingHeight 留白间距 fileName 文件名
export function html2Pdf(pdfDom,spacingHeight,fileName){


// 获取元素的高度
function getElementHeight(element) {
return element.offsetHeight;
}

// A4 纸宽高
const A4_WIDTH = 592.28,A4_HEIGHT = 841.89;
// 获取元素去除滚动条的高度
const domScrollHeight = pdfDom.scrollHeight;
const domScrollWidth = pdfDom.scrollWidth;

// 保存当前页的已使用高度
let currentPageHeight = 0;
// 获取所有的元素 我这儿是手动给页面添加class 用于计算高度 你也可以动态添加 这个不重要,主要是看逻辑
let elements = pdfDom.querySelectorAll('.element');
// 代表不可被分页
let newPage = 'new-page'

// 遍历所有内容的高度
for (let element of elements) {
let elementHeight = getElementHeight(element);
console.log(elementHeight, '我是页面上的elementHeight'); // 检查
// 检查添加这个元素后的总高度是否超过 A4 纸的高度
if (currentPageHeight + elementHeight > A4_HEIGHT) {
// 如果超过了,创建一个新的页面,并将这个元素添加到新的页面上
currentPageHeight = elementHeight;
element.classList.add(newPage);
console.log(element, '我是相加高度大于A4纸的元素');
}
currentPageHeight += elementHeight
}
// 根据 A4 的宽高等比计算 dom 页面对应的高度
const pageWidth = pdfDom.offsetWidth;
const pageHeight = (pageWidth / A4_WIDTH) * A4_HEIGHT;
// 将所有不允许被截断的子元素进行处理
const wholeNodes = pdfDom.querySelectorAll(`.${newPage}`);
console.log(wholeNodes, '将所有不允许被截断的子元素进行处理')
// 插入空白块的总高度
let allEmptyNodeHeight = 0;
for (let i = 0; i < wholeNodes.length; i++) {
// 判断当前的不可分页元素是否在两页显示
const topPageNum = Math.ceil(wholeNodes[i].offsetTop / pageHeight);
const bottomPageNum = Math.ceil((wholeNodes[i].offsetTop + wholeNodes[i].offsetHeight) / pageHeight);

// 是否被截断
if (topPageNum !== bottomPageNum) {
// 创建间距
const newBlock = document.createElement('div');
newBlock.className = 'spacing-node';
newBlock.style.background = '#fff';

// 计算空白块的高度,可以适当留出空间,根据自己需求而定
const _H = topPageNum * pageHeight - wholeNodes[i].offsetTop;
newBlock.style.height = _H + spacingHeight + 'px';

// 插入空白块
wholeNodes[i].parentNode.insertBefore(newBlock, wholeNodes[i]);

// 更新插入空白块的总高度
allEmptyNodeHeight = allEmptyNodeHeight + _H + spacingHeight;
}
}
pdfDom.setAttribute(
'style',
`height: ${domScrollHeight + allEmptyNodeHeight}px; width: ${domScrollWidth}px;`,
);

}


以上我们就完成 dom 层面的分页,下面就进入常规操作转为图片进行处理


 return html2Canvas(pdfDom, {
width: pdfDom.offsetWidth,
height: pdfDom.offsetHeight,
useCORS: true,
allowTaint: true,
scale: 3,
}).then(canvas => {

// dom 已经转换为 canvas 对象,可以将插入的空白块删除了
const spacingNodes = pdfDom.querySelectorAll('.spacing-node');

for (let i = 0; i < spacingNodes.length; i++) {
emptyNodes[i].style.height = 0;
emptyNodes[i].parentNode.removeChild(emptyNodes[i]);
}

const canvasWidth = canvas.width,canvasHeight = canvas.height;
// html 页面实际高度
let htmlHeight = canvasHeight;
// 页面偏移量
let position = 0;

// 根据 A4 的宽高等比计算 pdf 页面对应的高度
const pageHeight = (canvasWidth / A4_WIDTH) * A4_HEIGHT;

// html 页面生成的 canvas 在 pdf 中图片的宽高
const imgWidth = A4_WIDTH;
const imgHeight = 592.28 / canvasWidth * canvasHeight
// 将图片转为 base64 格式
const imageData = canvas.toDataURL('image/jpeg', 1.0);

// 生成 pdf 实例

const PDF = new jsPDF('', 'pt', 'a4', true)

// html 页面的实际高度小于生成 pdf 的页面高度时,即内容未超过 pdf 一页显示的范围,无需分页
if (htmlHeight <= pageHeight) {

PDF.addImage(imageData, 'JPEG', 0, 0, imgWidth, imgHeight);

} else {

while (htmlHeight > 0) {
PDF.addImage(imageData, 'JPEG', 0, position, imgWidth, imgHeight);

// 更新高度与偏移量
htmlHeight -= pageHeight;
position -= A4_HEIGHT;

if (htmlHeight > 0) {
// 在 PDF 文档中添加新页面
PDF.addPage();
}
}

}
// 保存 pdf 文件
PDF.save(`${fileName}.pdf`);
}).catch(err => {
console.log(err);
}
);


})



到这儿 htmlToPdf.js这个文件逻辑就处理完毕了,页面引入就可以正常使用了。


import  { html2Pdf }  from '@/utils/htmlToPdf'

// this.$refs 或 id
html2Pdf(this.$refs.viewReportCon)


如果能帮到你那最好不过了,最近天气回暖,换季期间干燥,多方因素易发生感冒,请各位 彦祖 务必保重身体。


作者:攀登的牵牛花
来源:juejin.cn/post/7346808829298262050
收起阅读 »

如何从Button.vue到Button.js

web
Vue的插件系统提供了一种灵活的方式来扩展Vue。Element UI作为一个基于Vue的UI组件库,其使用方式遵循Vue的插件安装模式,允许通过Vue.use()方法全局安装或按需加载组件。本文以Button组件为例,深入探讨Vue.use()方法的工作原理...
继续阅读 »

Vue的插件系统提供了一种灵活的方式来扩展Vue。Element UI作为一个基于Vue的UI组件库,其使用方式遵循Vue的插件安装模式,允许通过Vue.use()方法全局安装或按需加载组件。本文以Button组件为例,深入探讨Vue.use()方法的工作原理,以及如何借助这一机制实现Element UI组件的动态加载。

1. Vue.use()的工作原理

Vue.use(plugin)方法用于安装Vue插件。其基本工作原理如下:

  1. 参数检查Vue.use()首先检查传入的plugin是否为一个对象或函数,因为一个Vue插件可以是一个带有install方法的对象,或直接是一个函数。
  2. 安装插件:如果插件是一个对象,Vue会调用该对象的install方法,传入Vue构造函数作为参数。如果插件直接是一个函数,Vue则直接调用此函数,同样传入Vue构造函数。
  3. 避免重复安装:Vue内部维护了一个已安装插件的列表,如果一个插件已经安装过,Vue.use()会直接返回,避免重复安装。

Vue.use方法是Vue.js框架中用于安装Vue插件的一个全局方法。它提供了一种机制,允许开发者扩展Vue的功能,包括添加全局方法和实例方法、注册全局组件、通过全局混入来添加全局功能等。接下来,我们深入探讨Vue.use的工作原理。

1.1 详细步骤

Vue.use(plugin, ...options)方法接受一个插件对象或函数作为参数,并可选地接受一些额外的参数。Vue.use的基本工作流程如下:

  1. 检查插件是否已安装:Vue内部维护了一个已安装插件的列表。如果传入的插件已经在这个列表中,Vue.use将不会重复安装该插件,直接返回。
  2. 执行插件的安装方法

    • 如果插件是一个对象,Vue将调用该对象的install方法。
    • 如果插件本身是一个函数,Vue将直接调用这个函数。

    在上述两种情况中,Vue构造函数本身和Vue.use接收的任何额外参数都将传递给install方法或插件函数。

1.2 插件的install方法

插件的install方法是实现Vue插件功能的关键。这个方法接受Vue构造函数作为第一个参数,后续参数为Vue.use提供的额外参数。在install方法内部,插件开发者可以执行如下操作:

  • 注册全局组件:使用Vue.component注册全局组件,使其在任何新创建的Vue根实例的模板中可用。
  • 添加全局方法或属性:通过直接在VueVue.prototype上添加方法或属性,为Vue添加全局方法或实例方法。
  • 添加全局混入:使用Vue.mixin添加全局混入,影响每一个之后创建的Vue实例。
  • 添加Vue实例方法:通过在Vue.prototype上添加方法,使所有Vue实例都能使用这些方法。

1.3 示例代码

考虑一个简单的插件,它添加了一个全局方法和一个全局组件:

const MyPlugin = {
install(Vue, options) {
// 添加一个全局方法
Vue.myGlobalMethod = function() {
// 逻辑...
}

// 添加一个全局组件
Vue.component('my-component', {
// 组件选项...
});
}
};

// 使用Vue.use安装插件
Vue.use(MyPlugin);

Vue.use(MyPlugin)被调用时,Vue会执行MyPlugininstall方法,传入Vue构造函数作为参数。MyPlugin利用这个机会向Vue添加一个全局方法和一个全局组件。

1.4 小结

Vue.use方法是Vue插件系统的核心,它为Vue应用提供了极大的灵活性和扩展性。通过Vue.use,开发者可以轻松地将外部库集成到Vue应用中,无论是UI组件库、工具函数集合,还是提供全局功能的插件。理解Vue.use的工作原理对于有效地利用Vue生态系统中的资源以及开发自定义Vue插件都至关重要。

2. Element UI的动态加载

Element UI允许用户通过全局方式安装整个UI库,也支持按需加载单个组件以减少应用的最终打包体积。按需加载的实现,本质上是利用了Vue的插件安装机制。

以按需加载Button组件为例,步骤如下:

  1. 安装babel插件:首先需要安装babel-plugin-component,这个插件可以帮助我们在编译过程中自动将按需加载的组件代码转换为完整的导入语句。
  2. 配置.babelrc或babel.config.js:在.babelrcbabel.config.js配置文件中配置babel-plugin-component,指定需要按需加载的Element UI组件。
{
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
  1. 在Vue项目中按需加载:在Vue文件中,可以直接导入Element UI的Button组件,并使用Vue.use()进行安装。
import Vue from 'vue';
import { Button } from 'element-ui';

Vue.use(Button);

上述代码背后的实现逻辑如下:

  • babel-plugin-component插件处理这段导入语句时,它会将按需加载的Button组件转换为完整的导入语句,并且确保相关的样式文件也被导入。
  • Button组件对象包含一个install方法。这个方法的作用是将Button组件注册到全局,使其在Vue应用中的任何位置都可使用。
  • Vue.use(Button)调用时,Vue内部会执行Buttoninstall方法,将Button组件注册到Vue中。

在Vue中,如果一个组件(如Element UI的Button组件)需要通过Vue.use()方法进行按需加载,这个组件应该提供一个install方法。这个install方法是Vue插件安装的核心,它定义了当使用Vue.use()安装插件时Vue应该如何注册这个组件。接下来,我们来探讨一个具有install方法的Button组件应该是什么样的。

3. 仿Button组件

一个设计得当的Button组件,用于按需加载时,大致应该遵循以下结构:

// Button.vue


<script>
export default {
name: 'ElButton',
// 组件的其他选项...
};
script>

为了使上述Button组件可以通过Vue.use(Button)方式安装,我们需要在组件外层包裹一个对象或函数,该对象或函数包含一个install方法。这个方法负责将Button组件注册为Vue的全局组件:

// index.js 或 Button.js
import Button from './Button.vue';

Button.install = function(Vue) {
Vue.component(Button.name, Button);
};

export default Button;

这里,Button.install方法接收一个Vue构造函数作为参数,并使用Vue.component方法将Button组件注册为全局组件。Button.name用作全局注册的组件名(在这个例子中是ElButton),确保了组件可以在任何Vue实例的模板中通过标签来使用。

使用场景

当开发者在其Vue应用中想要按需加载Button组件时,可以这样实现加载:

import Vue from 'vue';
import Button from 'path-to-button/index.js'; // 或者直接指向包含`install`方法的文件

Vue.use(Button);

通过这种方式,Button组件就被注册为了全局组件,可以在任何组件的模板中直接使用,而无需在每个组件中单独导入和注册。

小结

拥有install方法的Button组件使得它可以作为一个Vue插件来按需加载。这种模式不仅优化了项目的打包体积(通过减少未使用组件的引入),还提供了更高的使用灵活性。开发者可以根据需要,选择性地加载Element UI库中的组件,而无需加载整个UI库。这种按需加载的机制,结合Vue的插件安装系统,极大地增强了Vue应用的性能和可维护性。

结尾

通过上述分析,我们可以看到,Vue.use()方法为Vue插件和组件的安装提供了一种标准化的方式。Element UI通过结合Vue的插件系统和Babel插件,实现了组件的按需加载,既方便了开发者使用,又优化了应用的打包体积。


作者:慕仲卿
来源:juejin.cn/post/7346134710132129830
收起阅读 »

faceApi-人脸识别和人脸检测

web
需求:浏览器通过模型检测前方是否有人(距离和正脸),检测到之后拍照随机保存一帧 实现步骤: 获取浏览器的摄像头权限 创建video标签并通过video标签展示摄像头影像 创建canvas标签并通过canvas标签绘制摄像头影像并展示 将canvas的当前帧转...
继续阅读 »

需求:浏览器通过模型检测前方是否有人(距离和正脸),检测到之后拍照随机保存一帧


实现步骤:



  1. 获取浏览器的摄像头权限

  2. 创建video标签并通过video标签展示摄像头影像

  3. 创建canvas标签并通过canvas标签绘制摄像头影像并展示

  4. 将canvas的当前帧转成图片展示保存


pnpm install @vladmandic/face-api 下载依赖


pnpm install @vladmandic/face-api


下载model模型


将下载的model模型放到项目的public文件中 如下图


image.png


创建video和canvas标签


      <video ref="videoRef" style="display: none"></video>
<template v-if="!picture || picture == ''">
<canvas ref="canvasRef" width="400" height="400"></canvas>
</template>
<template v-else>
<img ref="image" :src="picture" alt="" />
</template>
</div>

  width: 400px;
height: 400px;
border-radius: 50%;
overflow: hidden;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

.video_box {
position: fixed;
width: 400px;
height: 400px;
border-radius: 50%;
overflow: hidden;
}

@keyframes moveToTopLeft {
0% {
right: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

100% {
right: -68px;
top: -68px;
transform: scale(0.5);
}
}

.video_box {
animation: moveToTopLeft 2s ease forwards;
}


介绍分析

video 类选择器 让视频流居中

picture变量 判断是否转成照片

video_box视频流的某一帧转成照片后 动态移动到屏幕右上角



主要逻辑代码 主要逻辑代码 主要逻辑代码!!!


import * as faceApi from '@vladmandic/face-api'

const videoRef = ref()
const options = ref(null)
const canvasRef = ref(null)
let timeout = null
// 初始化人脸识别
const init = async () => {
await faceApi.nets.ssdMobilenetv1.loadFromUri("/models") //人脸检测
// await faceApi.nets.tinyFaceDetector.loadFromUri("/models") //人脸检测 人和摄像头距离打开
await faceApi.nets.faceLandmark68Net.loadFromUri("/models") //特征检测 人和摄像头距离必须打开
// await faceApi.nets.faceRecognitionNet.loadFromUri("/models") //识别人脸
// await faceApi.nets.faceExpressionNet.loadFromUri("/models") //识别表情,开心,沮丧,普通
// await faceApi.loadFaceLandmarkModel("/models");

options.value = new faceApi.SsdMobilenetv1Options({
minConfidence: 0.5, // 0.1 ~ 0.9
});
await cameraOptions()
}

// 打开摄像头
const cameraOptions = async() => {
let constraints = {
video: true
}
// 如果不是通过loacalhost或者通过https访问会将报错捕获并提示
try {
if (navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia(constraints).then((MediaStream) => {
// 返回参数
videoRef.value.srcObject = MediaStream;
videoRef.value.play();
recognizeFace()
}).catch((error) => {
console.log(error);
});
} else {
console.log('浏览器不支持开启摄像头,请更换浏览器')
}

} catch (err) {
console.log('非https访问')
}
}

// 检测人脸
const recognizeFace = async () => {
if (videoRef.value.paused) return clearTimeout(timeout);
canvasRef.value.getContext('2d', { willReadFrequently: true }).drawImage(videoRef.value, 0, 0, 400, 400);
// 直接检测人脸 灵敏较高
// const results = await new faceApi.DetectAllFacesTask(canvasRef.value, options.value).withFaceLandmarks();
// if (results.length > 0) {
// photoShoot()
// }
// 计算人与摄像头距离和是否正脸
const results = await new faceApi.detectSingleFace(canvasRef.value, options.value).withFaceLandmarks()
if (results) {
// 计算距离
const { positions } = results.landmarks;
const leftPoint = positions[0];
const rightPoint = positions[16];
// length 可以代替距离的判断 距离越近 length值越大
const length = Math.sqrt(
Math.pow(leftPoint.x - rightPoint.x, 2) +
Math.pow(leftPoint.y - rightPoint.y, 2),
);
// 计算是否正脸
const { roll, pitch, yaw } = results.angle
//roll水平角度 pitch上下角度 yaw 扭头角度
console.log(roll, pitch, yaw, length)
if (roll >= -10 && roll <= 10 && pitch >= -10 && pitch <= 10 && yaw>= -20 && yaw <= 20 && length >= 90 && length <= 110) {

photoShoot()

}

}


timeout = setTimeout(() => {
return recognizeFace()
}, 0)
}
const picture = ref(null)
const photoShoot = () => {
// 拿到图片的base64
let canvas = canvasRef.value.toDataURL("image/png");
// 停止摄像头成像
videoRef.value.srcObject.getTracks()[0].stop()
videoRef.value.pause()
if(canvas) {
// 拍照将base64转为file流文件
let blob = dataURLtoBlob(canvas);
let file = blobToFile(blob, "imgName");
// 将blob图片转化路径图片
picture.value = window.URL.createObjectURL(file)

} else {
console.log('canvas生成失败')
}
}
/**
* 将图片转为blob格式
* dataurl 拿到的base64的数据
*/
const dataURLtoBlob = (dataurl) => {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {
type: mime
});
}
/**
* 生成文件信息
* theBlob 文件
* fileName 文件名字
*/
const blobToFile = (theBlob, fileName) => {
theBlob.lastModifiedDate = new Date().toLocaleDateString();
theBlob.name = fileName;
return theBlob;
}

// 判断是否在区间
const isInRange = (number, start, end) => {
return number >= start && number <= end
}
export { init, videoRef, canvasRef, timeout, picture }

作者:发量浓郁的程序猿
来源:juejin.cn/post/7346121373113647167
收起阅读 »

领导:我有个需求,你把我们项目的技术栈升级一下

web
故事的开始 在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着掘金摸着🐟,一切的一切都是这么的美好。 霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了? 呸,是”帅哥,领导叫你去开会“。 ”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走...
继续阅读 »

故事的开始


在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着掘金摸着🐟,一切的一切都是这么的美好。


霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了?


呸,是”帅哥,领导叫你去开会“。


”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走向了办公室。


胖虎00002-我真的很不错.gif


会议室情节


”咦,不是开会吗,怎么就只有领导一个人“。


昏暗的灯光,发着亮光的屏幕,在幽闭的空间里气氛显得有那么一丝的~~~暧昧,不对,是紧张。


”帅哥你来了,那我直接说事情吧“,领导说到。


突然我察觉到那么一丝不安,但是现在走好像来不及了,房门紧闭,领导又有三头六臂,凭着我这副一米八五,吴彦祖的颜值的身躯根本就逃不了。


”是这样的,上面有个新需求,我看完之后发现我们目前的项目技术包袱有点重,做起来比较麻烦,看看你做个项目技术栈升级提高的方案吧“。


听到这里我松了一口气,还好只是升级提高的方案,不是把屁股抬高的方案。


哆啦A梦00009-噢那你要试试看么.png


进入正题


分析了公司项目当前的技术栈,确实存在比较杂乱的情况,一堆的技术包袱,惨不忍睹,但还是能跑的,人和项目都能跑。



技术栈:vue2全家桶 + vuetify + 某位前辈自己开发的组件库 + 某位前辈搞的半成品bff



我用了一分钟的时间做足了思想功课,这次的升级决定采用增量升级的方案,为什么用增量升级,因为线上的项目,需求还在开发,为了稳定和尽量少的投入人力,所以采取增量升级。



这里会有很多小伙伴问,什么是增量升级啊,我会说,自己百度



又经过了一分钟的思想斗争,决定使用微前端的方案进行技术升级,框架选择阿里的qiankun



这里又会有小伙伴说,为什么用qiankun啊,我会说,下面会说



胖虎00004我大雄今天就是要刁难你胖虎.gif


微前端---qiankun


为什么使用微前端,最主要是考虑到他与技术栈无关,能够忽略掉一些历史的包袱。


为什么要用qiankun,最主要还是考虑到稳定问题,qiankun目前的社区比较大,方案也多,出了问题能找到方案,本人之前也有使用过的经验,所以这次就决定是它。



技术为业务服务,在面对技术选型的时候要考虑到现实的问题,不能一味的什么都用新的,稳是第一位



那么应该如何在老项目中使用微前端去升级,我给出了我的方案步骤



  1. 对老项目进行改造(路由,登录,菜单)

  2. 编写子应用的开发模板

  3. 逐个逐个模块进行重构


下面会和大家分析一下三个步骤的内容


66b832f0c3f84bc99896d7f5c4367021_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


老项目的改造(下面大多数为代码)


第一步,我们要将老项目改造成适合被子应用随便进入的公交车。🚗


先对老项目进行分析,老项目是一个后台项目,大多数的后台项目布局都是上中左布局,头部导航栏,左边菜单栏,中间内容。那么我们只需要当用户选择的菜单属于微前端模块时,将中间内容变成微前端容器就好了。


image.png


那我们现在制作一个layout,里面的UI库我懒得换了,讲究看吧🌈


// BasicLayout.vue
<a-layout>
<a-layout-sider collapsible>
//菜单
</a-layout-sider>

<a-layout>
<a-layout-header>
//头部
</a-layout-header>
<a-layout-content>
//内容
<router-view/>
<slot></slot>
</a-layout-content>
</a-layout>

</a-layout>

然后对App.vue进行一定的修改


// App.vue
<a-config-provider locale="zh-cn">
<component v-if="layout" :is="layout">
<!-- 微前端子应用的容器,插槽紧跟着view-router -->
<div id="SubappViewportWrapper"></div>
</component>

// 初始化子应用时传入容器,这个容器不能后续修改,目前方案是将下面的容器动态append到SubappViewportWrapper
<div id="SubappViewport"></div>
</a-config-provider>

import { BasicLayout, UserLayout } from '@/layouts'
import { MICRO_APPS } from './qiankun'
import { start } from 'qiankun';
export default defineComponent({
components: {
BasicLayout,
UserLayout
},
data () {
return {
locale: zhCN,
layout: ''
}
},
methods: {
isMicroAppUrl (url) {
let result = false;
MICRO_APPS.forEach(mUrl => {
if (url.includes(mUrl)) {
result = true;
}
});
return result;
},
checkMicroApp (val) {
if (isMicroAppUrl(val.fullPath)) {
// 展示微前端容器
console.log('是微前端应用....');
document.body.classList.toggle(cName, false);
console.log(document.body.classList);
} else {
// 隐藏微前端容器
console.log('不是微前端应用');
document.body.classList.toggle(cName, true);
}
const oldLayout = this.layout;
this.layout = val.meta.layout || 'BasicLayout';
if (oldLayout !== this.layout) {
const cNode = document.getElementById('SubappViewport');
this.$nextTick(function () {
const pNode = document.getElementById('SubappViewportWrapper');
if (pNode && cNode) {
pNode.appendChild(cNode);
}
});
}
}
},
watch: {
$route (val) {
this.checkMicroApp(val);
}
},
mounted () {
start()
}
})
</script>

<style lang="less">
</style>



修改目的,判断路由中是否为微前端模块,如果是的话,就插入微前端模块容器。


然后新建一个qiankun.js文件


// qiankun.js
import { registerMicroApps, initGlobalState } from 'qiankun';
export const MICRO_APPS = ['test-app']; // 子应用列表
const MICRO_APPS_DOMAIN = '//localhost:8081'; // 子应用入口域名
const MICRO_APP_ROUTE_BASE = '/test-app'; // 子应用匹配规则

const qiankun = {
install (app) {
// 加载子应用提示
const loader = loading => console.log(`加载子应用中:${loading}`);
const registerMicroAppList = [];
MICRO_APPS.forEach(item => {
registerMicroAppList.push({
name: item,
entry: `${MICRO_APPS_DOMAIN}`,
container: '#SubappViewport',
loader,
activeRule: `${MICRO_APP_ROUTE_BASE}`
});
});
// 注册微前端应用
registerMicroApps(registerMicroAppList);

// 定义全局状态
const { onGlobalStateChange, setGlobalState } = initGlobalState({
token: '', // token
});
// 监听全局变化
onGlobalStateChange((value, prev) => {
console.log(['onGlobalStateChange - master'], value);
});
}
};
export default qiankun;

这个文件我们引入了qiankun并对使用它的API进行子应用的注册,之后直接在main.js注册,


// main.js
...
import qiankun from './qiankun'
Vue.use(qiankun)
...

然后我们只需要对路由做一点点的修改就可以用了


新建RouteView.vue页面,用于接受微前端模块的内容


//RouteView.vue
<template>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>

</template>

修改路由配置


//router.js
const routes = [
{
path: '/home',
name: 'Home',
meta: {},
component: () => import('@/views/Home.vue')
},
// 当是属于微前端模块的路由, 使用RouteView组件
{
path: '/test-app',
name: 'test-app',
meta: {},
component: () => import('@/RouteView.vue')
}
]

最后就新增一个名为test-app的子应用就可以了。


关于子应用的内容,这次先不说了,码字码累了,下次再说吧。


哆啦A梦00007-嘛你慢慢享受吧.png


下集预告(子应用模板编写)


你们肯定会说,不要脸,还搞下集预告。


哼,我只能说,今天周五,准备下班,不码了。🌈


胖虎00014-怎么了-我胖虎说的有毛病吗.gif


作者:小酒星小杜
来源:juejin.cn/post/7307469610423664655
收起阅读 »

H5、小程序中四个反向圆角的图片如何实现

web
H5、小程序中四个反向圆角的图片如何实现 最近我逛热风小程序(一个卖鞋的小程序)时,发现了一个奇特的图片样式。图片的四个圆角是反向的,和常规图片不一样。 思索一番后,我发现想实现这个效果,需要的 CSS 知识还挺多,于是整理这篇文章。 下面我会先介绍如何实现...
继续阅读 »

H5、小程序中四个反向圆角的图片如何实现


最近我逛热风小程序(一个卖鞋的小程序)时,发现了一个奇特的图片样式。图片的四个圆角是反向的,和常规图片不一样。


hotwind.jpg


思索一番后,我发现想实现这个效果,需要的 CSS 知识还挺多,于是整理这篇文章。


下面我会先介绍如何实现四个反向圆角的矩形,再介绍如何把特殊矩形作为遮罩、得到和热风小程序的图片效果。接着,我会给出完整的代码。最后,我会给做一个简单的总结。


拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的前端武学。


如何实现四个反向圆角的矩形


不难想到,四个反向圆角的矩形,就是四个边角都被圆遮挡的矩形。


rect-and-circle.png


径向渐变


知道圆遮挡矩形的原理后,我们最容易想到的办法是 —— 先用 50% 的 border-radius 得到圆,再改变圆形的定位去遮挡矩形。


不过这种方法需要多个元素,并不优雅,我将介绍另一种更巧妙的办法。


我们需要先了解一个 CSS 函数 —— radial-gradientradial-gradient 中文名称是径向渐变,它可以指定渐变的中心、形状、颜色和结束位置。语法如下:


/*
* 形状 at 位置
* 渐变颜色 渐变位置
* ...
* 渐变颜色 渐变位置
*/

background-image:
radial-gradient(
circle at center,
transparent 0,
transparent 20px,
#ddd 20px
);

利用径向渐变在矩形中心画圆


光看语法比较抽象,我们用代码写一个实际的例子。如图所示,我们要在矩形中心画圆:


rect0.png


下面是关键代码:


background: radial-gradient(
circle at center,
transparent 0,
transparent 20px,
#ddd 20px
);

其中:



  • radial-gradient 函数用于创建渐变效果。

  • circle at center 指定了圆形渐变,并且渐变的中心在矩形的中心。

  • transparent 0 指定了第一个渐变颜色为透明,位置是从中心开始。

  • transparent 20px 指定了第二个渐变颜色也为透明,位置距离中心 20px。

  • #ddd 20px 指定了第三个渐变颜色为淡灰色,位置距离中心 20px,第三个渐变颜色之后的颜色都是淡灰色。


通过 radial-gradient,我们成功让矩形中心、半径为 20px 的圆变透明,超过半径 20px 的地方颜色都变为灰色,这样看起来就是矩形中心有一个圆。


矩形左上角画圆


不难想到,只要我们把圆的中心从矩形中心移动到矩形的左上角,就可以让圆挡住矩形左上角,得到一个反向圆角。


rect1.png


关键代码如下,我们可以把 circle at center 改写为 circle at left top


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
);

矩形四个角画圆


我们已经知道 radial-gradient 如何实现 1 个反向圆角,接下来再看如何实现 4 个反向圆角。继续之前的思路,我们很容易想到给 background 设置多个反向渐变。


多个渐变之间可以用逗号分隔、且它们会按照声明的顺序依次堆叠。于是我们会写出如下关键代码:


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
),
/* ... */
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
#ddd 20px
);

遗憾的是,上述代码运行后我们看不到四个反向圆角,而是看到一个矩形。


rect.png


这是因为四个矩形互相堆叠,反而把反向圆角给遮住了。


overlay.png


遮挡怎么解决呢?我们可以分四步来解决。


设置背景宽度和高度


我们先单独看一个径向渐变。


第一步是设置 background-size: 50% 50%;,它设置了背景图像的大小是容器宽度和高度的 50%。代码运行后效果如下:


background-size.png


可以看到左上角有反向圆角的矩形重复出现了四次。


设置不允许重复


第二步是设置 background-repeat: no-repeat;。我们需要去除第一步中出现的重复。代码运行后效果如下:


no-repeat.png


给每个径向渐变设置位置


第三步是设置不允许重复时,应该保留的背景图像位置。


第一步中左上角是反向圆角的矩形出现了四次,第二步不允许重复时默认保留了左上角的矩形。事实上我们可以选择保留四个矩形中的任何一个,比如我们可以选择保留右下角的矩形。


background: 
radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
)
right bottom;

代码运行后效果如下:


no-repeat-right-bottom.png


组合前三个步骤


第四步是组合前三个步骤的语法。


看完第三步后,一个很自然的想法,就是用四个渐变分别形成四个特殊矩形,然后把四个特殊矩形分别放在左上、右上、左下和右下角,最后得到有四个反向圆角的矩形。


关键代码如下,为了区分四个径向渐变,我给左上、右上、左下、右下分别设置了红、绿、蓝、黄四种颜色:


background: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
red 20px
)
left top,
radial-gradient(
circle at right top,
transparent 0,
transparent 20px,
green 20px
)
right top,
radial-gradient(
circle at left bottom,
transparent 0,
transparent 20px,
blue 20px
)
left bottom,
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
yellow 20px
)
right bottom;
background-repeat: no-repeat;
background-size: 50% 50%;

代码运行效果如下:


rect2.png


不难想到,只要把红、绿、蓝、黄都换为灰色,就可以得到一个全灰、有四个反向圆角的矩形。


rect2-gray.png


把背景改写为遮罩


知道四个反向圆角的矩形如何实现后,我们可以:



  • background-size 改写为 mask-size

  • background-repeat 改写为 mask-repeat;

  • background 改写为 mask


这样就可以得到四个反向圆角的矩形遮罩。


mask: radial-gradient(
circle at left top,
transparent 0,
transparent 20px,
#ddd 20px
)
left top,
radial-gradient(
circle at right top,
transparent 0,
transparent 20px,
red 20px
)
right top,
radial-gradient(
circle at left bottom,
transparent 0,
transparent 20px,
red 20px
)
left bottom,
radial-gradient(
circle at right bottom,
transparent 0,
transparent 20px,
red 20px
)
right bottom;
mask-size: 50% 50%;
mask-repeat: no-repeat;

我们可以把四个反向圆角的矩形覆盖在一张背景图片上,就得到了和热风小程序一样的效果:


rect3.png


需要注意的是,部分手机浏览器不支持 mask 语法,所以我们有必要再设置一份 -webkit-mask-webkit-mask-size-webkit-mask-repeat


代码示例



总结


本文我们介绍了如何实现四个反向圆角的图片。


我们可以利用径向渐变,实现四个反向圆角的矩形。然后我们把这个矩形作为遮罩,覆盖在背景图片上,这样就实现了四个反向圆角的图片。




作者:小霖家的混江龙
来源:juejin.cn/post/7338280070303350834
收起阅读 »

H5、小程序 Tab 如何滚动居中

web
H5、小程序 Tab 如何滚动居中 Tab 在 PC 端、移动端应用都上很常见,不过 Tab 在移动端 比 PC 端更复杂。为什么呢?移动端设备屏幕较窄,一般仅能展示 4 ~ 7 个 Item。考虑到用户体验,UI 往往要求程序员实现一个功能——点击 Item...
继续阅读 »

H5、小程序 Tab 如何滚动居中


Tab 在 PC 端、移动端应用都上很常见,不过 Tab 在移动端 比 PC 端更复杂。为什么呢?移动端设备屏幕较窄,一般仅能展示 4 ~ 7 个 Item。考虑到用户体验,UI 往往要求程序员实现一个功能——点击 Item 后,Item 滚动到屏幕中央,拼多多的 Tab 就实现了这个功能。


pdd.gif


如果你也想实现这个功能,看了这篇文章,你一定会有所收获。我会先说明 Tab 滚动的本质,分析出滚动距离的计算公式,接着给出伪代码,最后再给出 Vue、React 和微信小程序的示例代码。


Tab 滚动的本质


Tab 滚动,本质是包裹着 Item 的容器在滚动。


如下图,竖着的虚线长方形代表手机屏幕,横着的长方形代表 Tab 的容器,标着数字的小正方形代表一个个 Tab Item。


左半部分中,Tab 容器紧贴手机屏幕左侧。右半部分中,Item 4 位于屏幕中央,两部分表示 Item 4 从屏幕右边滚动到屏幕中央。


scroll-left.png


不难看出,Item 4 滚动居中,其实就是容器向左移动 distance。此时容器滚动条到容器左边缘的距离也是 distance。


换句话说,让容器向左移动 distance,Item 4 就能居中。 因此只要我们能找出计算 distance 的公式,就能控制某个 Item 居中。


计算 distance 的公式


该如何计算 distance 呢?我们看下方这张更细致的示意图。


屏幕中央有一条线,它把 Item 4 分成了左右等宽的两部分,也把手机屏幕分成了左右等宽的两部分。你可以把 Item 4 一半的宽度记为 halfItemWidth,把手机屏幕一半的宽度记为 halfScreenWidth。再把 Item 4 左侧到容器左侧的距离记为 itemOffsetLeft


calculate-scroll-left.png


不难看出,这四个值满足如下等式:


distance + halfScreenWidth = itemOffsetLeft + halfItemWidth

简单推导一下,就得到了计算 distance 的公式。


distance = itemOffsetLeft + halfItemWidth - halfScreenWidth

公式的伪代码实现


现在开始解释公式的代码实现。


先看下 itemOffsetLefthalfItemWidthhalfScreenWidth 如何获取。



  • itemOffsetLeft 是 Item 元素到容器左侧的距离,你可以用 HTMLElement.offsetLeft 作它的值。

  • halfItemWidth 是 Item 元素一半的宽度。HTMLElement.offsetWidth 是元素的整体宽度,你可以用 offsetWidth / 2 作它的值,也可以先用 Element.getBoundingClientRect() 获取一个 itemRect 对象,再用 itemRect.width / 2 作它的值。

  • halfScreenWidth 是手机屏幕一半的宽度。 window.innerWidth 是手机屏幕的整体宽度,你可以用 innerWidth / 2 作它的值。


再看下如何把 distance 设置到容器上。


在 HTML 中,我们可以使用 Element.scrollLeft 来读取和设置元素滚动条到元素左边的位置。因此,你只需要容器的 scrollLeft 赋值为 distance,就可以实现 Item 元素滚动居中。


现在给出点击 tab 的函数的伪代码:


const onClick = () => {
const itemOffsetLeft = item.offsetLeft;
const halfItemWidth = item.offsetWidth / 2;
const halfScreenWidth = window.innerWidth / 2;
tabContainer.scrollLeft = itemOffsetLeft + halfItemWidth - halfScreenWidth
}

代码示例


Vue


Tab 滚动居中 | Vue


React


Tab 滚动居中 | React


微信小程序


Tab 滚动居中 | 微信小程序


小程序的 API 和浏览器的 API 有差异。



  • itemOffsetLeft ,你需要从点击事件的 event.currentTarget 中获取。

  • halfItemWidth,你需要先用 wx.createSelectorQuery() 选取到 Item 后,从 exec() 的执行结果中获取到 Item 整体宽度,然后再除以 2。

  • halfScreenWidth,你需要先用 wx.getSystemInfoSync() 获取屏幕整体宽度,然后再除以 2。


至于把 distance 设置到容器上,微信小程序 scroll-view 组件中,有 scroll-left 这个属性,你可以把 distance 赋值给 scroll-left




作者:小霖家的混江龙
来源:juejin.cn/post/7322730720732921867
收起阅读 »

写个 Mixin 来自动维护 loading 状态吧

web
Vue 中处理页面数据有两种交互方式: 骨架屏:加载时提供骨架屏,加载失败展示错误页面和重试按钮,需要维护加载状态数据,适用于注重用户体验的精细页面 消息弹窗:加载过程中展示 loading 遮罩,失败时弹出错误消息提示,不需要维护加载状态数据,适用于后台管...
继续阅读 »

Vue 中处理页面数据有两种交互方式:



  • 骨架屏:加载时提供骨架屏,加载失败展示错误页面和重试按钮,需要维护加载状态数据,适用于注重用户体验的精细页面

  • 消息弹窗:加载过程中展示 loading 遮罩,失败时弹出错误消息提示,不需要维护加载状态数据,适用于后台管理系统等不太看重用户体验的页面,或者提交数据的场景


本文适用于骨架屏类的页面数据加载场景。


痛点描述


我们日常加载页面数据时,可能需要维护 loading 状态,就像这样:


<template>
<el-table v-loading="loading" :data="tableData"></el-table>
</template>
<script>
export default {
data() {
return {
tableData: [],
loading: false,
}
},
methods: {
async getTableData() {
this.loading = true
try {
this.tableData = await this.$http.get("/user/list");
} finally {
this.loading = false;
}
},
},
}
</script>

其实加载函数本来可以只有一行代码,但为了维护 loading 状态,让我们的加载函数变得复杂。如果还要维护成功和失败状态的话,加载函数还会变得更加复杂。


export default {
data() {
return {
tableData: [],
loading: false,
success: false,
error: false,
errmsg: "",
}
},
methods: {
async getTableData() {
this.loading = true;
this.success = false;
this.error = false;
try {
this.user = await this.$http.get("/user/list");
this.success = true;
} catch (err) {
this.error = true;
this.errmsg = err.message;
} finally {
this.loading = false;
}
},
},
}

如果页面有多个数据要加载,比如表单页面中有多个下拉列表数据,那么这些状态属性会变得特别多,代码量会激增。


export default {
data() {
return {
yearList: [],
yearListLoading: false,
yearListLoaded: false,
yearListError: false,
yearListErrmsg: "",
deptList: [],
deptListLoading: false,
deptListLoaded: false,
deptListError: false,
deptListErrmsg: "",
tableData: [],
tableDataLoading: false,
tableDataLoaded: false,
tableDataError: false,
tableDataErrmsg: ""
}
}
}

其实我们可以根据加载函数的状态来自动维护这些状态数据,这次我们要实现的目标就是自动维护这些状态数据,并将它们放到对应函数的属性上。看看这样改进后的代码:


<template>
<div v-if="tableData.success">
<!-- 显示页面内容 -->
</div>
<div v-else-if="tableData.loading">
<!-- 显示骨架屏 -->
</div>
<div v-else-if="tableData.error">
<!-- 显示失败提示 -->
</div>
</template>
<script>
export default {
data() {
return {
tableData: []
}
},
methods: {
async getTableData() {
this.tableData = await this.$http.get("/user/list");
},
}
}
</script>

加载函数变得非常纯净,data 中也不需要定义一大堆状态数据,非常舒适。


Mixin 设计


基本用法


我们需要指定一下 methods 中的哪些方法是用来加载数据的,我们只需要对这些加载数据的方法添加状态属性。根据我之前在文章《我可能发现了Vue Mixin的正确用法——动态Mixin》中的看法,可以使用函数形式的 mixin 来指定。


export default {
mixins: [asyncStatus('getTableData')],
methods: {
async getTableData() {},
},
}

指定多个方法


也可以用数组指定多个方法名。


export default {
mixins: [asyncStatus([
'getDeptList',
'getYearList',
'getTableData'
])],
}

自动扫描所有方法


如果不传参数,则通过遍历的方式,给所有组件实例方法加上状态属性。


export default {
mixins: [asyncStatus()]
}

全局注入


虽然给所有的组件实例方法加上状态属性是没必要的,但也不影响。而且这有个好处,就是可以注册全局 mixin。


Vue.mixin(asyncStatus())

默认注入的属性


我们默认注入的状态字段有4个:



  • loading 是否正在加载中

  • success 是否加载成功

  • error 是否加载失败

  • exception 加载失败时抛出的错误对象


指定注入的属性名


当然,为了避免命名冲突,可以传入第二个参数,来指定添加的状态属性名。


export default {
mixins: [asyncStatus('getTableData', {
// 注入的加载状态属性名是 isLoading
loading: 'isLoading',
// 注入的错误状态属性名是 hasError
error: 'hasError',
// 错误对象的属性名是 errorObj
exception: 'errorObj',
// 不注入 success 属性
success: false,
})]
}

随意传入参数


由于第一个参数和第二个参数的形式没有重叠,所以省略第一个参数也是可行的。


export default {
mixins: [asyncStatus({
loading: 'isLoading',
error: 'hasError',
exception: 'errorObj',
success: false,
})]
}

总结


总结一下,我们需要使用函数形式来实现 mixin,函数接收两个参数,并且两个参数都是可选的。


/**
* @/mixins/async-status.mixin.js
* 维护异步方法的执行状态,为组件中的指定方法添加如下属性:
* - loading {boolean} 是否正在执行
* - success {boolean} 是否执行成功
* - error {boolean} 是否执行失败
* - exception 方法执行失败时抛出的异常
* @param {string|string[]} [methods] 方法名,可指定多个
* @param {Alias} [alias] 为注入的属性指定属性名,或将某个属性设置成false跳过注入
*
* @typedef Alias
* @type {object}
* @prop {boolean|string} [loading=true]
* @prop {boolean|string} [success=true]
* @prop {boolean|string} [error=true]
* @prop {boolean|string} [exception=true]
*/

export function asyncStatus(methods, alias) {}

函数返回真正的 mixin 对象,为组件中的异步方法维护并注入状态属性。


Mixin 实现


注入属性的时机


实现这个 mixin 是有一定难度的,首先要找准注入属性的时机。我们希望尽可能早往方法上注入属性,至少在执行 render 函数之前,以便在加载状态变化时可以重现渲染,但又需要在组件方法初始化之后。


所以,你需要熟悉 Vue 的组件渲染流程。在 Vue2 的源码中有这样一段组件初始化代码:


Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
// ...
};

而其中的 initState 方法的源码如下:


export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

所以总结一下 Vue 组件的初始化流程:



  • 执行 beforeCreate

  • 挂载 props

  • 挂载 methods

  • 执行并挂载 data

  • 挂载 computed

  • 监听 watch

  • 执行 created


我们必须在 methods 初始化之后开始注入属性,否则方法还没挂载到组件实例上。可以选择的是 data 或者 created。为了尽早注入,我们应该选择在 data 中注入。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias) {
return {
data() {
// 在这里为方法注入状态属性
return {}
}
}
}

处理参数


由于参数的形式比较自由,我们需要处理并统一一下参数形式。我们把 methods 处理成数组形式,并取出 alias 中指定注入的状态属性名。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
// 只传入 alias 的情况
if (typeof methods === 'object' && !Array.isArray(methods)) {
alias = methods
}
// 将 methods 规范化成数组形式
if (typeof methods === 'string') {
methods = [methods]
}
if (!Array.isArray(methods)) {
// TODO: 这里应该换成遍历出的所有方法名
methods = []
}
// 获取注入的状态属性名
const getKey = (name) =>
typeof alias[name] === 'string' || alias[name] === false
? alias[name]
: name
const loadingKey = getKey('loading')
const successKey = getKey('success')
const errordKey = getKey('error')
const exceptionKey = getKey('exception')
}

遍历组件方法


没有传入 methods 的时候,需要遍历组件上定义的所有方法。办法是遍历 this.$options.methods 上的所有属性名,这样遍历出的结果会包含从 mixins 中引入的方法。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
if (!Array.isArray(methods)) {
// 遍历出的所有方法名,注意这段代码需要在 data 中执行
methods = Object.keys(this.$options.methods)
}
return {}
}
}
}

维护加载状态


需要注意的是,只有响应式对象上的属性才会被监听,也就是说,只有响应式对象上的属性值变化才能引起组件的重新渲染。所以我们必须创建一个响应式对象,把加载状态维护进去。这可以通过 Vue.observable() 这个API来创建。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
// 存储状态值
const status = Vue.observable({})
loadingKey && Vue.set(status, loadingKey, false)
successKey && Vue.set(status, successKey, false)
errorKey && Vue.set(status, errorKey, false)
exceptionKey && Vue.set(status, exceptionKey, false)
// 设置状态值
const setStatus = (key, value) => key && (status[key] = value)
}
}
return {}
}
}
}

我们把加载状态维护到 status 中。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
const fn = this[method]
// 用于识别是否最后一次调用
let loadId = 0
// 替换原始方法
this[method] = (...args) => {
// 生成本次调用方法的标识
const currentId = ++loadId
setStatus(loadingKey, true)
setStatus(successKey, false)
setStatus(errorKey, false)
setStatus(exceptionKey, null)
try {
// 这里调用原始方法,this 为组件实例
const result = fn.call(this, ...args)
// 兼容同步和异步方法
if (result instanceof Promise) {
return result
.then((res) => {
// 最后一次加载完成时才更新状态
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(successKey, true)
}
return res
})
.catch((err) => {
// 最后一次加载完成时才更新状态
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
}
throw err
})
}
setStatus(loadingKey, false)
setStatus(successKey, true)
return result
} catch (err) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
throw err
}
}
}
}
return {}
}
}
}

注入状态属性


其实需要注入的属性都在 status 中,可以把它们作为访问器属性添加到对应的方法上。


// @/mixins/async-status.mixin.js
export function asyncStatus(methods, alias = {}) {
return {
data() {
for (const method of methods) {
if (typeof this[method] === 'function') {
// 存储状态值
const status = Vue.observable({})
// 替换原始方法
this[method] = (...args) => {}
// 注入状态值到方法中
Object.keys(status).forEach((key) => {
Object.defineProperty(this[method], key, {
get() {
return status[key]
}
})
})
Object.setPrototypeOf(this[method], fn)
}
}
return {}
}
}
}

完整代码


最后整合一下完整的代码。


import Vue from 'vue'

/**
* @/mixins/async-status.mixin.js
* 维护异步方法的执行状态,为组件中的指定方法添加如下属性:
* - loading {boolean} 是否正在执行
* - success {boolean} 是否执行成功
* - error {boolean} 是否执行失败
* - exception 方法执行失败时抛出的异常
* @param {string|string[]|Alias} [methods] 方法名,可指定多个
* @param {Alias} [alias] 为注入的属性指定属性名,或将某个属性设置成false跳过注入
*
* @typedef Alias
* @type {object}
* @prop {boolean|string} [loading=true] 加载状态的属性名
* @prop {boolean|string} [success=true] 加载成功状态的属性名
* @prop {boolean|string} [error=true] 加载失败状态的属性名
* @prop {boolean|string} [exception=true] 加载失败时存储错误对象的属性名
*
* @example
* <template>
* <el-table v-loading="getTableData.loading" />
* </template>
* <script>
* export default {
* mixins: [
* asyncMethodStatus('goFetchData')
* ],
* methods: {
* async getTableData() {
* this.tableData = await this.$http.get('/user/list');
* }
* }
* }
* </script>
*/

export default function asyncMethodStatus(methods, alias = {}) {
// 规范化参数
if (typeof methods === 'object' && !Array.isArray(methods)) {
alias = methods
}
if (typeof methods === 'string') {
methods = [methods]
}
const getKey = (name) =>
typeof alias[name] === 'string' || alias[name] === false
? alias[name]
: name
const loadingKey = getKey('loading')
const successKey = getKey('success')
const errorKey = getKey('error')
const exceptionKey = getKey('exception')
return {
data() {
if (!Array.isArray(methods)) {
// 默认为所有方法注入属性
methods = Object.keys(this.$options.methods)
}
for (const method of methods) {
if (typeof this[method] === 'function') {
const fn = this[method]
let loadId = 0
const status = Vue.observable({})
loadingKey && Vue.set(status, loadingKey, false)
successKey && Vue.set(status, successKey, false)
errorKey && Vue.set(status, errorKey, false)
exceptionKey && Vue.set(status, exceptionKey, false)
const setStatus = (key, value) => key && (status[key] = value)
this[method] = (...args) => {
const currentId = ++loadId
setStatus(loadingKey, true)
setStatus(successKey, false)
setStatus(errorKey, false)
setStatus(exceptionKey, null)
try {
const result = fn.call(this, ...args)
if (result instanceof Promise) {
return result
.then((res) => {
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(successKey, true)
}
return res
})
.catch((err) => {
if (loadId === currentId) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
}
throw err
})
}
setStatus(loadingKey, false)
setStatus(successKey, true)
return result
} catch (err) {
setStatus(loadingKey, false)
setStatus(errorKey, true)
setStatus(exceptionKey, err)
throw err
}
}
Object.keys(status).forEach((key) => {
Object.defineProperty(this[method], key, {
get() {
return status[key]
}
})
})
Object.setPrototypeOf(this[method], fn)
}
}
return {}
}
}
}

作者:cxy930123
来源:juejin.cn/post/7249724085147254845
收起阅读 »

移动端vh适配短屏幕手机,兼容一屏显示问题

web
rem适配 在日常的移动端开发中,设计稿一般为750物理像素,而我平时开发的时候,习惯以屏幕宽度为375,高度为724为标准( iPhone X 在微信内置浏览器的宽高) ,如下图所示: 该页面是使用 rem 进行适配,此时该图片宽度为 533px,正常我们...
继续阅读 »

rem适配


在日常的移动端开发中,设计稿一般为750物理像素,而我平时开发的时候,习惯以屏幕宽度为375,高度为724为标准( iPhone X 在微信内置浏览器的宽高) ,如下图所示:


1710350110240.png


该页面是使用 rem 进行适配,此时该图片宽度为 533px,正常我们需要设置其宽度为5.33rem ,当屏幕高度为724时,可以正常一屏显示完全。


 <body>
   <div class="content">其他内容</div>
   <img class="pic" src="./images/1.png" />
 </body>

 * {
   margin: 0;
   padding: 0;
 }
 body {
   background-color: skyblue;
 }
 .content {
   margin-top: 8rem
 }
 .pic {
  width: 5.33rem;
 }

短屏手机显示


而当我模拟短屏幕手机进行预览时,设置屏幕高度为667,此时屏幕宽度没有变化,那么根元素 htmlfont-size 也不会发生变化,那么造成的结果就是短屏幕手机上会出现滚动条,无法一屏显示。


1710350616617.png


但是需求是要求内容一屏能显示完全,此时 rem 适配已经没法做到了,在屏幕宽度不变,但是高度变化的情况下,这该怎么进行适配呢?


没错,这里我想到的是 vh 单位,不使用百分比是因为百分比适配是根据父级的宽高进行计算,而 vh 是根据整个屏幕的高度进行计算。


修改 css 如下所示:


 .content {
   margin-top: 55.249vh;
   /* margin-top: 8rem; */
 }
 .pic {
   /* width: 5.33rem; */
   width: auto;
   height: 28.66vh;
   max-height: 4.15rem;
 }

vh高度适配


利用 vh 对高度进行适配,但是这个 55.249vh28.66vh 是如何这算出来的呢?


首先我是基于 375*724 进行布局,在724的高度下,图片宽度 5.33rem,高度没设置,那就是使用了图片533px 时的高度,为 415px


1710351128534.png


724的高度下,图片高度使用了 415px,那么在屏幕上显示的应该是207.5px,那如果使用 415px 进行vh 换算,应该是 415 / (724x2) x 100,得出的结果约为28.66,这个就是对应的 vh 高度。


那么 55.249vh 同理,原来设置的 8rem,也就是相当于 800px,经过换算后得出结果。


而对图片设置 max-height 是为了不让图片一直随着高度变大得拉伸,以免造成图片变形。


此时在短屏幕手机上显示的效果如下图所示,当然 font-size 我这里没处理,有时候 font-size 也可以使用 vh 适配。


1710351669836.png


作者:一如彷徨
来源:juejin.cn/post/7345729950458724389
收起阅读 »

为什么可以通过process.env.NODE_ENV来区分环境

web
0.背景 通常我们在开发中需要区分当前代码的运行环境是dev、test、prod环境,以便我们进行相对应的项目配置,比如是否开启sourceMap,api地址切换等。而我们区分环境一般都是通过process.env.NODE_ENV,那么为什么process....
继续阅读 »

0.背景


通常我们在开发中需要区分当前代码的运行环境是dev、test、prod环境,以便我们进行相对应的项目配置,比如是否开启sourceMap,api地址切换等。而我们区分环境一般都是通过process.env.NODE_ENV,那么为什么process.env.NODE_ENV可以区分环境呢?是我们给他配置的,还是他可以自动识别呢?


1.什么是process.env.NODE_ENV


process.env属性返回一个包含用户环境信息的对象。


在node环境中,当我们打印process.env时,发现它并没有NODE_ENV这一个属性。实际上,process.env.NODE_ENV是在package.json的scripts命令中注入的,也就是NODE_ENV并不是node自带的,而是由用户定义的,至于为什么叫NODE_ENV,应该是约定成俗的吧。


2.通过package.json来设置node环境中的环境变量


如下为在package.json文件的script命令中设置一个变量NODE_ENV


{
"scripts": {
"dev": "NODE_ENV=development webpack --config webpack.dev.config.js"
}
}

执行对应的webpack.config.js文件


// webpack.config.js
console.log("【process.env】", process.env.AAA);

但是在index.jsx中也就是浏览器环境下的文件中打印process.env就会报错,如下:
image.png
可以看到NODE_ENV被赋值为development,当执行npm run dev时,我们就可以在 webpack.dev.config.js脚本中以及它所引入的脚本中访问到process.env.NODE_ENV,而无法在其它脚本中访问。原因就是前文提到的peocess.env是Node环境的属性,浏览器环境中index.js文件不能够获取到。


3.使用webpack.DefinePlugin插件在业务代码中注入环境变量


这个时候我们就存在一个解决方法,通过webpack中的DefinePlugin来设置一个全局变量,这样所有的打包的js文件都可以访问到这个全局变量了。


const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"'
})
]
}



使用DefinePlugin注意点
webpack.definePlugins本质上是打包过程中的字符串替换,比如我们刚才定义的__WEBPACK__ENV:JSON.stringify('packages')
在打包过程中,如果我们代码中使用到了__WEPBACK__ENVwebpack会将它的值替换成为对应definePlugins中定义的值,本质上就是匹配字符串替换,并不是传统意义上的环境变量process注入。

如下图所示:
image.png
由上图可知:仔细对比这两段代码第一个问题的答案其实已经很明了了,针对definePlugin这个插件我们使用它定义key:value全局变量时,他会将value进行会直接替换文本。所以我们通常使用JSON.stringify('pacakges')或者"'packages'"



作者:会飞的特洛伊
来源:juejin.cn/post/7345760019319390248
收起阅读 »

HTML常用基础标签:图片与超链接标签全解!

HTML图片标签和超链接标签是网页开发中常用的两种标签,它们分别用于在网页中插入图片和创建超链接。我们每天都在互联网世界中与各种形式的信息打交道。你是否好奇过,当你点击一篇文章中的图片或链接时,是什么神奇的力量让你瞬间跳转到另一个页面?今天,就让我们一起揭开H...
继续阅读 »

HTML图片标签和超链接标签是网页开发中常用的两种标签,它们分别用于在网页中插入图片和创建超链接。

我们每天都在互联网世界中与各种形式的信息打交道。你是否好奇过,当你点击一篇文章中的图片或链接时,是什么神奇的力量让你瞬间跳转到另一个页面?

今天,就让我们一起揭开HTML图片标签和超链接标签的神秘面纱。

一、HTML图片标签

HTML图片标签是一种特殊的标记,它可以让网页显示图像。通过使用图片标签,我们可以在网页上展示各种图片,从而让网页更加生动有趣。

Description

1、语法结构

HTML图片标签的语法结构非常简单,只需要使用标签,并在其中添加src属性,指定图片的路径即可。例如:

<img src="image.jpg" alt="描述图片的文字">

2、图片格式

HTML支持多种图片格式,包括JPEG、PNG、GIF等。不同的图片格式具有不同的特点,可以根据需要选择合适的格式。

3、图片属性

除了src属性外,HTML图片标签还有其他一些常用的属性,如:

  • alt属性用于描述图片的内容,当图片无法显示时,会显示该属性的值;
  • width和height属性用于设置图片的宽度和高度;
  • title属性用于设置鼠标悬停在图片上时显示的提示信息。

4、网络图片的插入

当需要插入网络上的图片时,可以将图片的URL地址作为src属性的值。例如:

<img src="https://www.example.com/images/pic.jpg" alt="示例图片">

5、本地图片的插入

当需要插入本地图片时,可以将图片的相对路径或绝对路径作为src属性的值。

6、相对路径与绝对路径

在这里再给大家介绍两个概念,相对路径与绝对路径,搞懂它们,我们在插入本地图片时也能得心应手。

Description

相对路径:
相对于当前HTML文件所在目录的路径,包含Web的相对路径(HTML中的相对目录)。例如,如果图片文件位于与HTML文件相同的目录中,可以直接使用文件名作为路径:

<img src="pic.jpg" alt="本地图片">

绝对路径:
图片文件在计算机上的完整路径(URL和物理路径)。例如:

<img src="C:/Users/username/Pictures/pic.jpg" alt="本地图片">

二、HTML超链接标签

超链接标签是HTML中另一个重要的元素,它可以实现网页之间的跳转。通过使用超链接标签,我们可以将文本、图片等内容设置为可点击的链接,方便用户在不同页面之间自由切换。

Description

1、语法结构

超链接标签使用<a>标签表示,需要在href属性中指定链接的目标地址。

<a href="目标地址" title="标题">文本内容</a>

例如:

<a href="https://www.ydcode.cn/">点击访问示例网站</a>

示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>示例网站</title>
</head>
<body>
    <h1>欢迎来到云端源想!</h1>
    <p>这是一个简单的HTML页面,用于展示一个网站的结构和内容。</p>
    <a href="https://www.ydcode.cn/">点击访问示例网站</a>
</body>
</html>

Description

2、链接目标

超链接可以链接到不同的目标,包括其他网页、电子邮件地址、文件下载等。通过设置href属性的值,可以实现不同的链接目标。

3、链接属性

超链接标签还有一些其他常用的属性,如:

  • target属性用于设置链接打开的方式,可以选择在新窗口或当前窗口打开链接;
  • title属性用于设置鼠标悬停在链接上时显示的提示信息;
  • rel属性用于设置链接的关系,例如设置nofollow值可以告诉搜索引擎不要跟踪该链接。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

4、锚点链接标签

锚点标签用于在网页中创建一个可以点击的锚点,以便用户可以通过点击锚点跳转到页面中的其他部分。如下图中电子书的章节切换。

Description

锚点标签的语法为:

<a name="锚点名称"></a>

例如,可以在页面中的一个段落前添加一个锚点:

<a name="section1"></a>
<p>这是一个段落。</p>

然后,可以在页面的其他位置创建一个指向该锚点的超链接:

<a href="#section1">跳转到第一节</a>

当用户点击“跳转到第一节”链接时,页面将滚动到名为“section1”的锚点所在的位置。

示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>示例网站</title>
</head>
<body>
    <h1>欢迎来到云端源想!</h1>
    <p><a href="#section1">跳转到第一节</a></p>
    <p>这是一个段落。</p>
    <p>这是另一个段落。</p>
    <p>这是第三个段落。</p>
    <a name="section1"></a>
    <p>这是第一节的内容。</p>
</body>
</html>

Description

三、总结

HTML图片标签和超链接标签是构建网页的两个重要元素,它们不仅丰富了网页的内容,还为网页添加了动态和互动性。

通过学习和掌握这两个标签的使用方法,我们可以创建更加丰富和互动的网页,为用户提供更好的浏览体验。无论是展示精美的图片,还是实现页面之间的跳转,HTML图片标签和超链接标签都能帮助我们实现更多的创意和功能。

让我们一起探索HTML的奇妙世界,创造出更加精彩的网页吧!

收起阅读 »

有效封装WebSocket,让你的代码更简洁!

web
前言 在现代 Web 应用中,实时通信已经成为越来越重要的一部分。而 WebSocket 技术的出现,使得实时通信变得更加高效和便捷。 WebSocket 协议是一种基于 TCP 协议的双向通信协议,它能够在客户端和服务器之间建立起持久性的连接,从而实现实时通...
继续阅读 »

前言


在现代 Web 应用中,实时通信已经成为越来越重要的一部分。而 WebSocket 技术的出现,使得实时通信变得更加高效和便捷。


WebSocket 协议是一种基于 TCP 协议的双向通信协议,它能够在客户端和服务器之间建立起持久性的连接,从而实现实时通信。


在前端开发中,为了更好地利用 WebSocket 技术,我们通常会对其进行封装,以便于全局调用并根据自己的业务做不同的预处理。


本文将介绍如何有效封装一个 WebSocket 供全局使用,并根据自己的业务做不同的预处理,实现更方便的调用,减少重复代码。


具体实现


我们将基于 Web API 提供的 WebSocket 类,封装一个 Socket 类,该类将提供以下功能:



  1. 建立 WebSocket 连接,并支持发送 query 参数。

  2. 发送、接收消息,支持对 WebSocket 的事件进行监听。

  3. 断开 WebSocket 连接。

  4. 支持心跳检测。

  5. 可以根据业务需要,对发送和接收的消息进行预处理。


下面是实现代码:


// socket.js
import modal from '@/plugins/modal'
const baseURL = import.meta.env.VITE_APP_BASE_WS;
const EventTypes = ['open', 'close', 'message', 'error', 'reconnect'];
const DEFAULT_CHECK_TIME = 55 * 1000; // 心跳检测的默认时间
const DEFAULT_CHECK_COUNT = 3; // 心跳检测默认失败重连次数
const DEFAULT_CHECK_DATA = { Type: 1, Parameters: ['alive'] }; // 心跳检测的默认参数 - 跟后端协商的
const CLOSE_ABNORMAL = 1006; // WebSocket非正常关闭code码

class EventMap {
deps = new Map();
depend(eventType, callback) {
this.deps.set(eventType, callback);
}
notify(eventType, event) {
if (this.deps.has(eventType)) {
this.deps.get(eventType)(event);
}
}
}

class Socket extends WebSocket {
heartCheckData = DEFAULT_CHECK_DATA;
heartCheckTimeout = DEFAULT_CHECK_TIME;
heartCheckInterval = null;
heartCheckCount = DEFAULT_CHECK_COUNT
constructor(options, dep, reconnectCount = 0) {
let _baseURL = baseURL
const { url, protocols, query = {}, greet = null, customBase = null } = options;
const _queryParams = Object.keys(query).reduce((str, key) => {
if (typeof query[key] !== 'object' && typeof query[key] !== 'function') {
return str += str.length > 0 ? `&${key}=${query[key]}` : `${key}=${query[key]}`;
} else {
return str;
}
}, '');
if (customBase) {
_baseURL = customBase
}
super(`${_baseURL}${url}?${_queryParams}`, protocols);
this._currentOptions = options;
this._dep = dep;
this._reconnectCount = reconnectCount;
greet && Object.assign(this, {
heartCheckData: greet
})
this.initSocket();
}

// 初始化WebSocket
initSocket() {
// 监听webSocket的事件
this.onopen = function (e) {
this._dep.notify('open', e);
this.heartCheckStart();
}
this.onclose = function (e) {
this._dep.notify('close', e);
// 如果WebSocket是非正常关闭 则进行重连
if (e.code === CLOSE_ABNORMAL) {
if (this._reconnectCount < this.heartCheckCount) {
this._reconnectCount++;
const _socket = new Socket(this._currentOptions, this._dep, this._reconnectCount);
this._dep.notify('reconnect', _socket);
} else {
return modal.msgError('WebSocket重连失败, 请联系技术客服!');
}
}
}
this.onerror = function (e) {
this._dep.notify('error', e);
}
this.onmessage = function (e) {
// 如果后端返回的是二进制数据
if (e.data instanceof Blob) {
const reader = new FileReader()
reader.readAsArrayBuffer(e.data)
reader.onload = (ev) => {
if (ev.target.readyState === FileReader.DONE) {
this._dep.notify('message', ev.target?.result);
}
}
} else {
// 处理普通数据
try {
const _parseData = JSON.parse(e.data);
this._dep.notify('message', _parseData);
} catch (error) {
console.log(error)
}
}
}

}

// 订阅事件
subscribe(eventType, callback) {
if (typeof callback !== 'function') throw new Error('The second param is must be a function');
if (!EventTypes.includes(eventType)) throw new Error('The first param is not supported');
this._dep.depend(eventType, callback);
}

// 发送消息
sendMessage(data, options = {}) {
const { transformJSON = true } = options;
let result = data;
if (transformJSON) {
result = JSON.stringify(data);
}
this.send(result);
}

// 关闭WebSocket
closeSocket(code, reason) {
this.close(code, reason);
}

// 开始心跳检测
heartCheckStart() {
this.heartCheckInterval = setInterval(() => {
if (this.readyState === this.OPEN) {
let transformJSON = typeof this.heartCheckData === 'object'
this.sendMessage(this.heartCheckData, { transformJSON });
} else {
this.clearHeartCheck();
}
}, this.heartCheckTimeout)
}

// 清除心跳检测
clearHeartCheck() {
clearInterval(this.heartCheckInterval);
}

// 重置心跳检测
resetHeartCheck() {
clearInterval(this.heartCheckInterval);
this.heartCheckStart();
}
}
// 默认的配置项
const defaultOptions = {
url: '',
protocols: '',
query: {},
}

export const useSocket = (options = defaultOptions) => {
if (!window.WebSocket) return modal.msgWarning('您的浏览器不支持WebSocket, 请更换浏览器!');
const dep = new EventMap();
const reconnectCount = 0;
return new Socket(options, dep, reconnectCount);
}

接下来我们从实际使用的角度解释一下上面的代码,首先我们暴露了一个 useSocket 函数,该函数接收一个 options 配置项参数,支持的参数有:



  • url:要连接的 WebSocket URL;

  • protocols:一个协议字符串或者一个包含协议字符串的数组;

  • query:可以通过 URL 传递给后端的查询参数;

  • greet:心跳检测的打招呼信息;

  • customBase:自定义的 baseURL ,否则默认使用环境变量中定义的 env.VITE_APP_BASE_WS


在调用该函数后,我们首先会判断当前用户的浏览器是否支持 WebSocket,如果不支持给予用户提示。


然后我们实例化了一个 EventMap 类的实例对象 dep,你可以把它当作是一个依赖收集桶,当用户订阅了某个 WebSocket 事件时,我们将收集这个事件对应的回调作为依赖,在事件触发时,再通知该依赖,然后调用该事件对应的回调函数。


接下来我们定义了一个初始的重连次数记录值 reconnectCount 为 0,每当这个 WebSocket 重连时,该值会自增。


之后我们实例化了自己封装的 Socket 类,并传入了我们上面的三个参数。
Socket 类的构造函数 constructor 中,我们先取出配置项,把 query 内的参数拼接在 URL 上,然后使用 super 调用父类的构造函数进行建立 WebSocket 连接。


之后我们缓存了当前 Socket 实例化时的参数,再调用 initSocket() 方法去进行 WebSocket 事件的监听:



  • onopen:触发 depopen 对应的回调函数并且打开心跳检测;

  • onclose:触发 depclose 对应的回调函数并且对关闭的 code 码进行判断,如果是非正常关闭连接,将会进行重连,如果重连次数达到阈值,则通知给用户;

  • onerror:触发 deperror 对应的回调函数;

  • onmessage:接收到服务端返回的数据,可以先根据自身业务做一些预处理,比如我就根据不同的数据类型进行了数据解析的预处理,之后再触发 depmessage 对应的回调函数并传入处理过后的数据。


我们也暴露了一些成员方法以供实例对象使用:



  • subscribe:订阅 WebSocket 事件,传入事件类型并须是 EventTypes 内的类型之一,第二个参数则是回调函数;

  • sendMessage:同样的,我们在给服务端发送数据之前也可以根据自身业务做一些预处理,比如我将需要转成 JSON 的数据,在这里统一转换后再发送给服务端;

  • closeSocket:关闭 WebSocket 连接;

  • heartCheckStart:开始心跳检测,会创建一个定时器,在一定时间之后(默认是55s)给服务端发送信息确认连接是否正常;

  • clearHeartCheck:清除心跳检测定时器(如果当前 WebSocket 连接已经关闭,则自动清除);

  • resetHeartCheck:重置心跳检测定时器。


如何使用


让我们看下如何使用这个封装好的 useSocket 函数,以在 Vue3中使用为例:


// xx.jsx or xx.vue
import { useSocket } from './socket.js'
const socket = ref(null) // WebSocket实例
const initWebSocket = () => {
const options = {
url: '/<your url>',
query: {
// something params
},
}
socket.value = useSocket(options)
socket.value.subscribe('open', () => {
console.log('WebSocket连接成功!')
const greet = 'hello'
// 发送打招呼消息
socket.value.sendMessage(greet)
})
socket.value.subscribe('close', reason => {
console.log('WebSocket连接关闭!', reason)
})
socket.value.subscribe('message', result => {
console.log('WebSocket接收到消息:', result)
})
socket.value.subscribe('error', err => {
console.log('WebSocket捕获错误:', err)
})
socket.value.subscribe('reconnect', _socket => {
console.log('WebSocket断开重连:', _socket)
socket.value = _socket
})
}
initWebSocket()

最后,如果想 debug 我们的心跳检测是否有效,可以使用下面这段代码:


// 测试心跳检测重连 手动模拟断开的情况
if (this._reconnectCount > 0) return;
const tempTimer = setInterval(() => {
this.close();
if (this._reconnectCount < 3) {
console.log('重连');
this._reconnectCount++;
const _socket = new Socket(this._currentOptions, this._dep, this._reconnectCount);
this._dep.notify('reconnect', _socket);
} else {
return clearInterval(tempTimer);
}
}, 3 * 1000)

initSocket() 方法中的 this.onopen 事件的回调函数内的最后添加上面这段代码即可。


总结


至此,我们实现了一个 WebSocket 类的封装,提供了连接、断开、消息发送、接收和心跳检测等功能,并可以根据业务需要对消息进行预处理。同时,我们还介绍了如何使用封装好的 useSocket 函数。


WebSocket 封装的好处在于可以让我们在全局范围内方便地使用 WebSocket,提高代码的可读性和可维护性,降低代码的复杂度和重复性。在实际开发过程中,我们可以结合自己的业务需求,对封装的 WebSocket 类进行扩展和优化,以达到更好的效果。


尽管我在文中尽可能地详细介绍了每一个步骤和细节,但是难免会存在一些错误和不足之处。如果您在使用本文中介绍的方法时发现了任何错误或者有更好的方法,非常欢迎您指正并提出建议,以便我能够不断改进和提升文章的质量。


我是荼锦,一个兴趣使然的开发者。非常感谢您阅读本文,希望本文对您有所帮助!


作者:荼锦
来源:juejin.cn/post/7231481633671757861
收起阅读 »

在 vite 工程化中手动分包

web
项目搭建我们使用 vite 搭建一个 vue3 工程,执行命令:pnpm create vite vue3-demo --template vue-ts 安装 lodash 依赖包,下载依赖:pnpm...
继续阅读 »

项目搭建

  1. 我们使用 vite 搭建一个 vue3 工程,执行命令:
pnpm create vite vue3-demo --template vue-ts
  1. 安装 lodash 依赖包,下载依赖:
pnpm add lodash

pnpm install
  1. 完成后的工程目录结构是这样的: 


业务场景

我们先首次构建打包,然后修改一下代码再打包,对比一下前后打包差异:


可以看到,代码改动后,index-[hash].js 的文件指纹发生了变化,这意味着每次打包后,用户就要重新下载新的 js,而这个文件里面包含了这些东西:vuelodash业务代码,其中像 vuelodash 这些依赖包是固定不变的,有变动的只是我们的业务代码,基于这个点我们就可以在其基础上打包优化。

打包优化

我们需要在打包上优化两个点:

  1. 把第三方依赖库单独打包成一个 js 文件
  2. 把我们的业务代码单独打包成一个 js 文件

这块需要我们对 vite 工程化知识有一定的了解,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源,可以通过配置 build.rollupOptions.output.manualChunks 来自定义 chunk 分割策略。

更改 vite 配置

  1. 打开 vite.config.ts,加入配置项:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor':['vue', 'lodash'], // 这里可以自己自定义打包名字和需要打包的第三方库
}
}
}
}
})
  1. 执行打包命令,我们可以看到打包文件中多了一个 verdor-[hash].js 的文件,这个就是刚才配置分包的文件: 


  1. 这样的好处就是,将来如果我们的业务代码有改动,打包的第三方库的文件指纹就不会变,用户就会直接读取浏览器缓存,这是一种比较优的解决办法: 


  1. 但这样需要我们每次都手动填写第三方库,那也显得太呆了,我们可以把 manualChunks 配置成一个函数,每次去加载这个模块的时候,它就会运行这个函数,打印看下输出什么: 


  1. 我们会发现依赖包都是在 node_modules 目录下,接下来我们就修改一下配置:
 import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
}
}
}
}
})
  1. 我们再看下打包结果: 


总结

分包(Code Splitting)是一种将应用程序代码拆分为不同的块或包的技术,从而在需要时按需加载这些包。这种技术带来了许多优势,特别是在构建大型单页应用(Single Page Application,SPA)时。

  • 减小初始加载时间: 将应用程序分成多个小块,可以减少初始加载时需要下载的数据量。这意味着用户可以更快地看到初始内容,提高了用户体验。
  • 优化资源利用: 分包可以根据用户的操作行为和需要进行按需加载。这样,在用户访问特定页面或功能时,只会加载与之相关的代码,而不会加载整个应用程序的所有代码,从而减少了不必要的资源消耗。
  • 并行加载: 分包允许浏览器并行加载多个包。这可以加速页面加载,因为浏览器可以同时请求多个资源,而不是等待一个大文件下载完成。
  • 缓存优化: 分包使得缓存管理更加灵活。如果应用程序的一部分发生变化,只需重新加载受影响的分包,而不必重新加载整个应用程序。
  • 减少内存占用: 当用户访问某个页面时,只有该页面所需的代码被加载和执行。这有助于减少浏览器内存的使用,尤其是在应用程序变得复杂时。
  • 按需更新: 当应用程序的某个部分需要更新时,只需要重新发布相应的分包,而不必重新发布整个应用程序。这可以减少发布和部署的复杂性。
  • 代码复用和维护: 分包可以按功能或模块来划分,从而鼓励代码的模块化和复用。这样,不同页面之间可以共享相同的代码块,减少了重复编写和维护代码的工作量。


作者:白雾茫茫丶
来源:juejin.cn/post/7346031272919072779
收起阅读 »

H5推送,为什么都用WebSocket?

web
       大家好,我是石头~        最近大盘在3000点附近磨蹭,我也随大众去网上浏览了下感兴趣的几只股票,看下行情怎样。        看了一会,还是垃圾行情,不咋地,不过看着页面上的那些实时刷新分时图和五档行情,倒是想起公司以前就因为这个实时数...
继续阅读 »

       大家好,我是石头~


       最近大盘在3000点附近磨蹭,我也随大众去网上浏览了下感兴趣的几只股票,看下行情怎样。
       看了一会,还是垃圾行情,不咋地,不过看着页面上的那些实时刷新分时图和五档行情,倒是想起公司以前就因为这个实时数据刷新的问题,差点引起一次生产事故。


HTTP轮询差点导致生产事故


       那是一个给用户展示实时数据的需求,产品的要求是用户数据发生变动,需要在30秒内给客户展示出来。


       当时由于数据展示的页面入口较深,负责的后端开发就让H5通过轮询调用的方式来实现数据刷新。


       然而,由于客户端开发的失误,将此页面在APP打开时就进行了初始化,导致数据请求量暴涨,服务端压力大增,差点就把服务端打爆了。


fa7049166c79454eb87f3890d1aa6f4b.webp


H5推送,应该用什么?


       既然用HTTP做实时数据刷新有风险,那么,应该用什么方式来实现?


       一般要实现服务端推送,都需要用到长连接,而能够做到长连接的只有WebSocket、UDP和TCP,而且,WebSocket是在TCP之上构建的一种高级应用层协议。大家觉得我们应该用哪一种?


       其实,大家只要网上查一下,基本都会被推荐使用WebSocket,那么,为什么要用WebSocket?


u=2157318451,827303453&fm=253&fmt=auto&app=138&f=JPEG.webp


为什么要用WebSocket?


       这个我们可以从以下几个方面来看:



  • 易用性与兼容性:WebSocket兼容现代浏览器(HTML5标准),可以直接在H5页面中使用JavaScript API与后端进行交互,无需复杂的轮询机制,而且支持全双工通信。而TCP层级的通信通常不适合直接在纯浏览器环境中使用,因为浏览器API主要面向HTTP(S)协议栈,若要用TCP,往往需要借助Socket.IO、Flash Socket或其他插件,或者在服务器端代理并通过WebSocket、Comet等方式间接与客户端通信。

  • 开发复杂度与维护成本:WebSocket已经封装好了一套完整的握手、心跳、断线重连机制,对于开发者而言,使用WebSocket API相对简单。而TCP 开发则需要处理更多的底层细节,包括但不限于连接管理、错误处理、协议设计等,这对于前端开发人员来说门槛较高。

  • 资源消耗与性能:WebSocket 在建立连接之后可以保持持久连接,减少了每次请求都要建立连接和断开连接带来的资源消耗,提升了性能。而虽然TCP连接也可以维持长久,但如果是自定义TCP协议,由于没有WebSocket的标准化复用和优化机制,可能在大规模并发场景下,资源管理和性能控制更为复杂。

  • 移动设备支持:WebSocket在移动端浏览器上的支持同样广泛,对于跨平台的H5应用兼容性较好。若采用原生TCP,移动设备上的兼容性和开发难度会进一步加大。


websocket01.jpg


结论


       综上所述,H5实时数据推送建议使用WebSocket,但是在使用WebSocket的时候,大家对其安全机制要多关注,避免出现安全漏洞。


作者:石头聊技术
来源:juejin.cn/post/7345404998164955147
收起阅读 »