注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一些不被人熟知,但又很好用的HTML属性

web
HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。 下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性 contenteditable: 这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的...
继续阅读 »

HTML(超文本标记语言)具有多种属性,可用于增强我们的网页的结构和功能。
下面我就给大家介绍一下,一些很好用的HTML属性,但是不被人熟知的HTML属性


contenteditable:


这个属性使我们的元素变的可编辑。用户可以直接在我们的浏览器中修改元素的内容。


<div contenteditable="true">
这段内容可以被编辑。
</div>

使用场景:
-可以用来创建富文本编辑器,使用户能够在网页中创建、编辑和格式化文本,


spellcheck:


该属性用于启用或禁用元素的拼写检查功能。(如果用户输入的单词拼写有误,浏览器通常会标记出来并提供纠正建议)


<textarea spellcheck="true">
这个文本区域启用了拼写检查。
</textarea>

image.png


使用场景:



  • 可以在文章创作者的富文本编辑器中使用,辅助文章创作


代码演示:


draggable:


该属性使元素可拖动。通常与 JavaScript 结合使用,实现拖放功能。


<img src="image.jpg" draggable="true" alt="可拖动的图片">

使用场景:



  • 在电子商务网站中,用户可以拖动产品图像到购物车区域,以便快速添加商品到购物清单。

  • 在可视化数据分析工具中,用户可以通过拖拽图表或数据元素来定制自己的数据可视化图形。

  • 可以创建一个可拖放的低代码平台


代码演示:


sandbox:


与 元素一起使用,sandbox 属性限制了嵌入内容的行为,如阻止执行脚本或提交表单。

<iframe src="sandboxed-page.html" sandbox="allow-same-origin allow-scripts"></iframe>

使用场景:



  • 可以在电子邮件客户端中,通过使用 sandbox 属性限制电子邮件中嵌入内容的行为,以确保安全性并防止恶意代码执行。

  • 可以在需要嵌入第三方内容(如广告、外部应用程序等)但又需要限制其行为的情况下使用。这可以防止嵌入的内容执行恶意脚本或访问敏感信息。


download:


该属性与 <a>(锚点)元素一起使用,指定用户单击链接时应下载的目标。


<a href="document.pdf" download="my-document">下载 PDF</a>

使用场景:



  • 可用于提供下载链接,例如下载文档、图像或其他文件。这使得用户可以通过单击链接直接下载相关内容而无需离开页面。


hidden:


该属性用于隐藏页面上的元素。这是最初隐藏内容的简单方法,可以通过 CSS 或 JavaScript 在后来显示。


<p hidden>这个段落最初是隐藏的。</p>

使用场景:



  • 在网页中使用弹出式模态框或折叠式面板,可以利用 hidden 属性来最初隐藏它们,并在用户点击或触发特定事件时展现。

  • 在网页表单验证中,可以将错误消息初始隐藏,只有当用户提交表单出现错误时才显示出来。


defer:



<script defer src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


async:


类似于 defer,async 属性与

<script async src="myscript.js"></script>

使用场景:



  • 在网页底部延迟加载 JavaScript 脚本,以确保页面内容首先加载,提升页面加载速度和性能。

  • 对于异步加载的脚本(async),适用于对网页影响较小的辅助性 JavaScript 脚本,例如网页分析工具或跟踪代码。


Accept 属性:


你可以将 accept 属性与 元素(仅适用于文件类型)一起使用,以指定服务器可以接受的文件类型。


<input type="file" accept=".jpg, .jpeg, .png">

使用场景:



  • 在上传图片的社交媒体平台中,限制用户只能上传特定格式(如 JPG、PNG)的图片文件,确保图片质量和页面加载速度。

  • 在在线应用程序中,限制用户只能上传特定类型的文件,例如在云存储服务中只允许上传文档文件。


Translate:


该属性用于指定在页面本地化时,元素的内容是否应该被翻译。


<p translate="no">这段内容不应被翻译。</p>

作者:zayyo
来源:juejin.cn/post/7303789262989443083
收起阅读 »

Celeris Web,一套女生都觉得好看的Vue3模板

web
Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板 一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的? 嗯,女生总是很喜欢漂亮的东...
继续阅读 »

Vue3+Unocss+NaiveUI+Monorepo搭建一套女生觉得好看的前端模板


一年前,我刚刚从后端转入前端的大门,兴奋又迷茫。身边的女性朋友们总是找我帮忙写小工具,但每次都被吐槽UI太丑了。于是,我想,能不能搞点不一样的?


嗯,女生总是很喜欢漂亮的东西,对吧?于是我决定写一款前端开发模板,让开发出来的工具她们用起来不仅方便,还得有点美美哒。Vue 3、Unocss、NaiveUI、Monorepo,这些都是我的秘密武器。我取名它为Celeris Web


这个开发框架采用了最新的技术,包括Vue 3、Vite和 TypeScript。而且,这个项目的设计初衷就用了monorepo的方法使得依赖管理和多个项目的协作变得轻松。这可是一套为开发人员提供了构建现代Web应用程序的全面解决方案哦。


不管你是老手还是新手,Celeris Web都能给你提供一个简化的前端开发流程,利用最新的工具和技术。是不是觉得很吸引人?


Snipaste_2024-01-16_14-27-03.png


Celeris Web的特点



  • ⚡ 闪电般快速:使用Vue 3,Vite和pnpm构建 🔥

  • 💪 强类型:使用TypeScript 💻

  • 📂 单库存储:易于管理依赖项和协作多个项目 🤝

  • 🔥 最新语法:使用新的< script setup >语法 🆕

  • 📦 自动导入组件:自动导入组件 🚚

  • 📥 自动导入API:使用unplugin-auto-import直接导入Composition API和其他API 📨

  • 💡 官方路由器:使用Vue Router v4 🛣️

  • 🎉 加载反馈:使用NProgress提供页面加载进度反馈 🔄

  • 🍍 状态管理:使用Pinia进行状态管理 🗃️

  • 📜 中文字体预设:包含中文字体预设 🇨🇳

  • 🌍 国际化就绪:具备使用本地化的国际化功能 🌎

  • ☁️ Netlify准备就绪:在Netlify上零配置部署 ☁️


有了Celeris Web,你的前端开发之路将更加轻松愉快!🚀


中英文双语注释


在Celeris Web的设计中,我们注重代码的可读性和学习性,为此,我们为每个函数都配备了中英文双语注释,以确保无论您的母语是中文还是英文,都能轻松理解和学习代码。


为什么选择中英文双语注释?



  1. 全球协作: 在多语言团队中,中英文双语注释能够促进更好的沟通和协作,确保团队成员都能准确理解代码的功能和实现。

  2. 学习便捷: 对于新手来说,中英文双语注释提供了更友好的学习环境,帮助他们更快速地掌握代码的逻辑和结构。

  3. 开发者友好: 我们致力于构建一个开发者友好的开发环境,中英文双语注释是我们为实现这一目标而采取的一项关键措施。

  4. 示例:


    /**
    * 打开一个新的浏览器窗口
    * Open a new browser window
    *
    * @param {string} url - 要在新窗口中打开的 URL
    * The URL to open in the new window
    *
    * @param {object} options - 打开窗口的选项
    * Options for opening the window
    * @param {string} options.target - 新窗口的名称或特殊选项,默认为 "_blank"
    * @param {string} options.features - 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    */

    export function openWindow(url: string, { target = "_blank", features = "noopener=yes,noreferrer=yes" }: {
    target?: "_blank" | "_self" | "_parent" | "_top"; // 新窗口的名称或特殊选项,默认为 "_blank"
    features?: string; // 新窗口的特性(大小,位置等),默认为 "noopener=yes,noreferrer=yes"
    } = {}
    ) {
    window.open(url, target, features);
    }

    通过这样的中英文双语注释,我们希望为开发者提供更愉悦、更高效的编码体验,让Celeris Web成为一个真正容易上手和深入学习的前端模板。



Monorepo 设计的好处


1. 依赖管理更轻松: Monorepo 将所有项目的依赖项集中管理,避免了不同项目之间版本冲突的问题,使得整体的依赖管理更加清晰和简便。


2. 代码共享与重用: 不同项目之间可以方便地共享和重用代码,减少重复开发的工作量。这对于保持代码一致性和提高开发效率非常有利。


3. 统一的构建和部署: Monorepo 可以通过统一的构建和部署流程,简化整个开发过程,减少了配置和管理的复杂性,提高了开发团队的协作效率。


4. 统一的版本控制: 所有项目都在同一个版本控制仓库中,使得版本管理更加一致和可控。这有助于团队协同开发时更好地追踪和处理版本问题。 Monorepo设计让Celeris Web不仅是一款后台管理系统模板,同时也是一个快速开发C端产品的前端Web模板。有了Celeris Web,前端开发之路将更加轻松愉快!🚀


设计理念:突破Admin管理的局限性,关注C端用户体验


在市面上,大多数前端模板都着眼于满足B端用户的需求,为企业管理系统(Admin)提供了强大的功能和灵活的界面。然而,很少有模板将C端产品的特点纳入设计考虑,这正是我们Celeris Web的创新之处。


突破Admin管理的局限性:


传统的Admin管理系统更注重数据展示和业务管理,但C端产品更加侧重用户体验和视觉吸引力。我们深知C端用户对于界面美观、交互流畅的要求,因此Celeris Web不仅提供了强大的后台管理功能,更注重让前端界面在用户层面上达到更高水平。


关注C端用户体验:


Celeris Web不仅仅是一个后台管理系统的模板,更是一个注重C端用户体验的前端Web模板。我们致力于打破传统Admin系统的束缚,通过引入崭新的设计理念,使得C端产品在前端呈现上具备更为出色的用户体验。


特色亮点:



  • 时尚美观的UI设计: 我们注重界面的美感,采用现代化设计语言,使得Celeris Web的UI不仅仅是功能的堆砌,更是一种视觉盛宴,让C端用户爱不释手。

  • 用户友好的交互体验: 考虑到C端用户的习惯和需求,Celeris Web注重交互体验的设计,通过流畅的动画效果和直观的操作,使用户感受到前所未有的愉悦和便捷。

  • 个性化定制的主题支持: 我们理解C端产品的多样性,因此提供了丰富的主题定制选项,让每个C端项目都能拥有独一无二的外观,更好地满足产品个性化的需求。


通过这一独特的设计理念,Celeris Web致力于在前端开发领域探索全新的可能性,为C端产品注入更多活力和创意。我们相信,这样的创新将带来更广泛的用户认可和更高的产品价值。在Celeris Web的世界里,前端不再局限于Admin系统,而是融入了更多关于用户体验的精彩元素。


后期发展路线:瞄准AIGC,引领互联网产品变革


随着人工智能与图形计算(AIGC)技术的崛起,我们决定将Celeris Web的发展方向更加专注于推动AIGC相关产品的研发和落地。这一战略决策旨在顺应互联网产品的变革浪潮,为未来的科技创新开辟全新的可能性。


AIGC技术引领变革:


AIGC的兴起标志着互联网产业迎来了一场技术变革,为产品带来更加智能、交互性更强的体验。Celeris Web将积极响应这一变革,致力于为开发者提供更优秀的工具,助力他们在AIGC领域创造更具前瞻性的产品。


模板的研发重心:


在后期的发展中,Celeris Web将更加重视AIGC相关产品的研发需求。我们将推出更多针对人工智能的功能模块,使开发者能够更便捷、高效地构建出色的AIGC应用。


专注产品落地:


除了技术研发,我们将加强对AIGC产品落地的支持。通过提供详实的文档、示例和定制化服务,Celeris Web旨在帮助开发者更好地将AIGC技术融入他们的实际项目中,实现技术创新与商业应用的有机结合。


开放合作生态:


为了推动AIGC技术的更广泛应用,Celeris Web将积极构建开放合作生态。与行业内优秀的AIGC技术提供商、开发者社区保持密切合作,共同推动AIGC技术的发展,携手打造更加繁荣的互联网产品生态圈。


Celeris Web未来的发展将以AIGC为核心,我们期待在这个快速发展的技术领域中,与开发者们一同探索、创新,共同引领互联网产品的未来。通过持续的努力和创新,Celeris Web将成为AIGC领域的引领者,助力开发者创造更加智能、引人入胜的互联网产品。


源码


kirklin/celeris-web (github.com)


作者:KirkLin
来源:juejin.cn/post/7324334380373688371
收起阅读 »

揭秘 "mitt" 源码:为什么作者钟情于 `map` 而放弃 `forEach`

web
故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach, 而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. ...
继续阅读 »

故事是这样的,半年前我提交了一个 Pull Request(PR),想要将作者在代码中使用的 map 改成 forEach


而作者的回应却是:map() is used because it is 3 bytes smaller when gzipped. (使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


咦?为什么会这样呢?


"mitt" 简介


首先,让我们认识一下 "mitt",它是一只小巧灵活的事件发射器(event emitter)库,体积仅有 200 字节,但功能强大。这个小家伙在项目中充当了事件的传播者,有点像是一个小型的邮差,把消息传递给需要它的地方。


developit/mitt: 🥊 Tiny 200 byte functional event emitter / pubsub. (github.com)


作者的选择:map vs forEach


在源码中,我们发现作者选择使用了 Array.prototype.map(),这是一个处理数组每个元素并返回新数组的函数。然而,有趣的地方在于,作者并没有在 map 中返回任何值。这和我对 map 的期望有些出入,因为我们习惯于用它生成一个新的数组。


代码的细微变化


曾经,代码片段是这样的,作者想要用 map 来执行一些操作,但却不生成新数组。


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.map((handler) => {
handler(evt!);
});
}

我希望修改成这样:


if (handlers) {
(handlers as EventHandlerList<Events[keyof Events]>)
.slice()
.forEach((handler) => {
handler(evt!);
});
}

所以我很快就交了个PR:将map改成了forEach,经过了几个月的等待,PR被拒了,作者的回应是:map() is used because it is 3 bytes smaller when gzipped.(使用 map 是因为在 Gzip 压缩时可以减小 3 字节的体积。)


code.png


pr.png


小技巧背后的逻辑


虽然 map 通常用于生成新数组,但作者在这里使用它更像是在借助压缩的优势,让代码更轻量。


大小对比


通过实验验证,使用 map 的打包大小确实稍微小一些:



  • 使用 map 时,打包大小为:


  - 190 B: mitt.js.gz
- 162 B: mitt.js.br
- 189 B: mitt.mjs.gz
- 160 B: mitt.mjs.br
- 268 B: mitt.umd.js.gz
- 228 B: mitt.umd.js.br


  • 而使用 forEach 后,打包大小为:


  - 192 B: mitt.js.gz
- 164 B: mitt.js.br
- 191 B: mitt.mjs.gz
- 162 B: mitt.mjs.br
- 270 B: mitt.umd.js.gz
- 230 B: mitt.umd.js.br

进一步实验


为了深入了解选择的影响,我又进行了一个实验。有趣的是,当我将代码中的一处使用 map 改为 forEach,而另一处保持不变时,结果居然是打包体积更大了。


experiment_results.png


总结


这个故事让我不仅仅关注于代码表面,还开始注重微小选择可能带来的影响。学到了很多平时容易忽略的点,"mitt" 作者的选择展现了在开发中面对权衡时的智慧,通过选择不同的API,以轻松的方式达到减小代码体积的目标。在编写代码时,无处不充满着权衡的乐趣。


如果你对这个故事有更多的想法或者其他技术话题感兴趣,随时和我分享哦!


作者:KirkLin
来源:juejin.cn/post/7327424955037564965
收起阅读 »

使用pixi.js开发一个智慧路口(车辆轨迹追踪)项目

web
项目效果 项目功能: 位置更新、航向角计算。 debug模式。 位置角度线性补帧。 变道、转弯、碰撞检测。 mock轨迹数据 图片效果: 视频效果: 项目启动 项目地址 github:(github.com/huoguozhang…) 线上:todo...
继续阅读 »

项目效果


项目功能:



  • 位置更新、航向角计算。

  • debug模式。

  • 位置角度线性补帧。

  • 变道、转弯、碰撞检测。

  • mock轨迹数据


图片效果:


result.gif


视频效果:



项目启动


项目地址



(如果觉得项目对你有帮助的话, 可以给我一个star 和 赞,❤️)


启动demo项目



  1. cd car-tracking-2d/demos/react-demo

  2. yarn

  3. yarn start


界面使用


debug 模式


浏览器url ?后面(search部分)加入参数debug=1


例如:http://localhost:3000/home?tunnelNo=tunnel1&debug=1


将会展示调试信息:


image.png


如图:车旁边的白色文字信息为debug模式才会展示的内容(由上到下为:里程、车id、车道id、[x,y]、旋转角度)


实现:


技术栈:


ts+pixi.js+任意前端框架


(前端框架使用vuereact或者其他框架都可以。只需要在mounted阶段,实例化我们暴露出来class即可。然后在destroyed或者unmounted阶段destory示例即可,后面会提到。)


pixi.js


官网介绍:



Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.



pixi.js是一个2D的WebGL的渲染库。但是没有three.js知名度高。一个原因是,我们2D的需求技术路线很多,可以是dom、svg、canvas draw api等,包括本项目也可以使用其他技术方案实现,希望通过本文,大家在实现这种频繁更新元素位置的功能,可以考虑一下pixi.js。



API快速讲解

这里只讲我们项目使用到的


Application

import * as PIXI from 'pixi.js';

const app = new PIXI.Application({
view: canvasDom // canvas dom 对象
});


Container

容器,功能为一个组。
当我们设置容器的scale(缩放)、rotation(旋转)、x、y(位置)时。里面的元素都会收到影响。


(ps:app.stage也是一个Container


每个 Container可以通过addChild(增加子节点)、removeChild(删除子节点),也可以设置子元素的zIndex(和css的功能一致)。子原始的scale(缩放)、rotation(旋转)、x、y(位置)是相对于Container的。


Sprite

精灵,渲染图片对象。


carObj = Sprite.from('/car.svg')

Sprite.from(url),url相同的话,只会加载一次图片。纹理对象也只会创建一次。


anchor属性其他对象也有,设置定位点,类似于csstransform-origin


执行下面代码
carObj.anchor.set(0.5, 0.5)


如果x = 10 y =10,carObj的中心点的坐标就是(10,10),旋转原点也是(10,10),缩放也是如此。


Graphics

绘制几何图形,圆弧,曲线、直线等都可以。也支持fill和stroke,canvas draw api支持的,Graphics都支持。


Text

文本,比较简单。字体、颜色、大小,都支持。



  • 值得注意的是文本内容含有换行符时(\n \r),文本会换行。

  • pixi提供测量文本的width height的方法非常好用。


Tick

this.app.ticker.add(() => {})

类似于requestAnimationFrame


具体实现


分三步,vue/react都一样:


1 获取canvas dom通过ref的方式。


2 创建我们封装Stage Road


3 组件销毁时,执行 stage.destroy(注意stage是我封装的,不是pixi的。使用方不需要使用pixi.js的api)


线性插帧

当有一个对象由坐标 点a(0,0)变换到点b(1000,1000),1秒内完成。
中间的变化值为:
dx =1000 dy=1000
记录每帧的时间差t(当前帧距离第0帧的,单位毫秒)


所以第n帧位置信息为(0+dx / 1000 * t, 0+ dy /1000 *t)


角度变换也是这个道理。


位置坐标获取

如果直线长度为1000px,对应的实际里程为100米。


当跑了50米,当前就是直线的中点坐标。
弯道呢,通过弧度可以推算出坐标。
可以把 Road.ts line 70的注释取消。


 // 方便开发观察 绘制车道线 ---begin----
// this.mount(lane.centerLine.paint())

航向角

直线简单,通过Math.atan2可以求出来。
弯道需要通过解析几何,计算出圆弧切线,然后推测出航向角。


转弯

mark.png
可以查看我们标注的一些点


以1到7的弯道举例,相当于是从新创建一次车道,车道的点是车道1和车道7的组合。
我们通过 circle属性配置,在创建Road


{
uid: '1-2',
x: 1072,
y: 1605,
circle: {
// 编号形式 车道序号-第几个点

linkId: '7-3'
}
},

这条信息表示:车道1的第2个点(uid),有圆弧链接到车道7的第3个点(circle.linkId)


碰撞检测

我们这个项目的特点是,前端展示,实际后端返回什么数据,我们就展示什么数据。(一般不需要前端处理)。
这里我们mock的数据就简单处理一下。判断是否存在相交的线段(当前对象的位置和将要到达的点),如果线段相交,车辆暂停移动。


作者:火锅小王子
来源:juejin.cn/post/7327467832866095130
收起阅读 »

微信小程序开发大坑盘点

web
微信小程序开发大坑盘点 起因 前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app...
继续阅读 »

微信小程序开发大坑盘点


起因


前几天心血来潮,想给学校设计个一站式校园小程序,可以查询成绩,考试信息,课表之类的(本来想法里是还想包括一些社交功能的,但这个因为资质问题暂且搁置了)。其实很久以前就有大概了解过微信小程序的一些概念,那个时候试图用 uni-app 做,但是这玩意太难用所以不了了之了。


于是这次打算正经的用微信自己的那套东西做,结果不出意外的是入了深坑......


大坑


微信小程序云函数外部调用异常


微信小程序提供 wx.request 发起 HTTP 请求,由于微信不是浏览器,没有跨域限制,这方便了很多事,但是由于 wx.request 函数只能对 HTTPS 协议的地址发起请求,而我们学校的教务系统又是清一色的 HTTP,因此我需要一个可以用来帮助我发起 HTTP 请求的转发接口。


对于这种简单需求,云函数显然是最好的解决方案,进而我发现微信小程序自带云函数的支持,于是便兴冲冲地写了一段 NodeJS 代码,放上去跑。


结果我发现不知道为什么,请求其他网站都没问题,唯独请求我们教务系统就会原地超时。经过了几个小时的调试,最后以失败告终,转而改用腾讯云的云函数。


代码也十分简单:


const url = require('url')

const express = require('express');
const app = express()
const port = 9000

const rp = require('request-promise')

app.use(express.json());

app.post('/', async (req, res) => {
const jar = rp.jar()

try {
const response = await rp({
...req.body,
resolveWithFullResponse: true,
simple: false,
jar: jar
})
res.json(response)
} catch (e) {
res.json(e)
console.error(e)
}
})

app.listen(port, () => {
console.log("Successfully loaded")
})

其中额外引入了 request-promise 库(express 是默认引入的,腾讯云函数这里做的不错,对 npm 支持很好)。


然后做了一个模仿 wx.request 调用风格的 request 函数,这样我就可以在 wx.request 和我自己的 request 函数中无缝切换(更进阶的是,我自己写的这个还额外支持了以 Promise 风格调用。


export async function request(data) {
try {
const res = await rp({
...data,
uri: data.url,
headers: data.header,
})
let result = {
...res,
data: res.body,
header: res.headers
}
if (result.statusCode != 200) {
throw {
err_msg: "内部错误"
}
}
if (data.dataType === 'json') {
result.body = JSON.parse(result.body)
}
data.success && data.success(result);
data.complete && data.complete({})
return result;
} catch (e) {
data.fail && data.fail(e)
data.complete && data.complete({})
throw e;
}
}

function rp(data) {
return new Promise((resolve, reject) => {
wx.request({
method: 'POST',
url: 'https://service-abcdefg-123456789.gz.apigw.tencentcs.com/release/',
data: data,
success: (res) => {
resolve(res.data)
},
fail: (err) => {
reject(err)
}
})
})
}

ES6 module 和变量作用域支持差


不知道为什么,微信小程序完全不支持 ES6 module,即使它是支持 ES6 语法的。也就是说,你只能使用这种传统的 CommonJS 方式引入:


const module = require('module.js')

而不是 ES6 的 import 语法:


import module from 'module.js'

最离谱的是,微信小程序这个基于 VSCode 的编译器会给你 warn 这段代码,告知你可以转换为使用 import 导入。



于是这又引出了另外一个奇怪的问题:当你在一个界面的逻辑层文件上声明变量时,IDE 会认为这个变量是一个全局变量,因此在其他界面声明同名变量会得到一个 error,即使不会导致任何编译错误。


这导致了,现在我的模块引入必须用一种很奇怪的写法...


const sessionModule = require('../../utils/session');
const tgcModule = require('../../utils/tgc')
const cryptoModule = require('../../miniprogram_npm/crypto-js/index.js')

奇葩的 NPM 支持


在以前,微信小程序是不支持包管理器的,这也就意味着,你得手动把那些库的 JS 复制到你的项目目录里再引用,非常麻烦。但是现在好了,微信可以自动帮你做这件事了。


没错,是自动帮你复制,而不是做了包管理器支持。


怎么说呢...你需要先在你的项目源代码目录中 init 一个 package.jsonadd 你需要的包然后 install,接下来点击 IDE 顶栏的 Tools - Build npm 选项,Weixin Devtools 就会帮你生成一个 miniprogram_npm文件夹,将每个项目各自 combine 到一个 index.js 然后塞到各自名字的文件夹里,然后,你就能通过上面那种方式手动引入使用了。


很奇葩但是... 勉强能用(而且不限制使用的包管理器,比如我用的就是 yarn)。


避免使用双向绑定


微信小程序的 WXML 存在一个有限的双向绑定支持,也是类似 Vue 的那种语法糖:


<input model:value="{{value}}" />

但是这个双向绑定不知道为什么,在某些情况下会认为你没有设置一个 bindinput 事件(但实际上应该是由双向绑定自动设置的),于是不断地在后台刷警告,因此还不如手动实现来的省心。


有限的标准组件支持


如果你觉得微信小程序的开发和前端开发差不多,那就大错特错了。因为微信小程序默认情况下根本不支持任何 HTML 元素,而是套了一层他们自己的元素,比如 view 实际上是 classblock 则和 Vue 的 template 差不多(微信小程序也有 template 元素,只不过那个是给组件用的),不分 h1, h2, span, strong,只有 text 元素等。当然好在 CSS 还是那套,基本都能用。


但是... 微信小程序提供的元素依然太少了,根本没办法满足实际开发需要(比如根本没有表格元素)。于是微信小程序提供了一个 rich-text 元素,可用于渲染 HTML 元素。


但是这个 rich-text 就显得十分鸡肋,他不是通过 slot 传入 HTML 元素,而是通过 string 或者 object。这凭空增加了开发难度,导致我不得不这么写:


<rich-text nodes="{{nodes}}"></rich-text>

this.setData({
nodes: licenses.map(it => {
return `
<div style="margin: 20px 10px;"><strong>${it.projectName}</strong>
is licensed under the <code>${it.licenseName}</code>:</div>
<pre style="overflow: auto; background-color:#F5F6FA;"><code>${it.fullLicense}</code></pre>
${it.sourceRepo?`<div style="margin: 20px 10px;"><span style="color:gray; font-size: 12px;">The source code can be found at: ${it.sourceRepo}</span></div>`:""}
<br/><br/>
`

}).join("")
})

甚至这么写:



完美的回答了知乎有人“为什么不用 JSON 表达页面而是用类似 XML 一样的 HTML”的问题。


最后


虽然吐槽了这么多,但是微信小程序还是有一些不错的点的。除了上面说的宽松的跨域策略以外,微信小程序的 TypeScript 支持很完善,IDE 工具链做的也不错(除了他那个特别容易崩溃的 Simulator),加之微信开放社区的活跃度也不低(问问题一天内就有人回复),也算是能用了。


作者:HikariLan贺兰星辰
来源:juejin.cn/post/7228563544022761509
收起阅读 »

一行代码快速实现全局模糊

web
github 仓库:github.com/astak16/blu…npm 仓库:http://www.npmjs.com/package/blu…页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理敏感数据过滤通常是由后端去做的,有时候...
继续阅读 »

github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232github 仓库:github.com/astak16/blu…

npm 仓库:http://www.npmjs.com/package/blu…

页面在展示时,某些敏感的数据不想展示,可以使用该插件,对敏感数据进行模糊处理

敏感数据过滤通常是由后端去做的,有时候在某些场合不想展示某些数据,这时让后端去改代码,再重新部署,这样的成本太高,所以前端也需要有这样的能力,可以在前端对敏感数据进行模糊处理,这样就不需要后端参与了

前端过滤文本,通常有两种方法:

  1. 拦截响应,对文本进行过滤后在渲染在页面上
  2. 渲染在页面上后,对文本进行过滤

对于方案一,需要遍历所有的响应对象的所有属性,而且只能替换或者删除文本,无法实现个性化的配置

所以我选择了方案二,当页面渲染结束后,遍历所有的 dom,对文本进行过滤,这样可以实现个性化的配置

遍历 dom,你能想到最直接的方法是:

  1. document.querySelectorAll("*")
  2. document.body.getElementsByTagName("*")

然后在遍历找到的所有 dom,对文本进行过滤,这样效率会很差

所以我选择了 TreeWalker 遍历 dom,效率会高很多

技术说明

treeWalker 是通过 document.createTreeWalker() 创建的,传入 NodeFilter.SHOW_TEXT 参数,就可以拿到页面中所有的文本节点

然后用正则匹配文本,如果匹配到了,就对文本进行模糊处理:

const wrapTextWithTag = (node: Node) => {
const oldText = node?.textContent;
if (!oldText) return;
const regexNumber = /[-+]?\d{1,3}(?:,\d{3})*(?:\.\d+)?/;
const regexWord = new RegExp(`(${this.words.join("|")})`);
const mergedRegex = new RegExp(
`(${regexNumber.source}|${regexWord.source})`,
"g"
);

if (mergedRegex.test(oldText)) {
const rep = oldText.replace(mergedRegex, "$1");
const arr = rep.split(/<\/?span>/);
let span;
node.textContent = "";
for (let i = 0; i < arr.length; i++) {
const newText = arr[i];
const isContainsWord = this.isContainsWord(newText);
const isContainsNumber = this.isContainsNumber(newText);
if (!this.isVoid(newText) && (isContainsWord || isContainsNumber)) {
span = this.createElementAndSetBlurred(newText);
} else {
span = document.createTextNode(newText);
}
node.parentElement?.insertBefore(span, node);
}
}
};

如果到此结束的话,只能处理静态的文本,如果是异步数据,就无法处理了,所以还需要监听 dom 的变化,对新增的 dom 进行模糊处理

监听 dom 可以使用 MutationObserver,将 subtree 设置为 true,就可以监听到所有的 dom 变化

const observer = new MutationObserver((mutationList) => {
mutationList.map((mutation) => {
const node = mutation.addedNodes[0];
if (!node) return;
const isStyle = this.hasStyle(node);
if (node.nodeType === 1 && !isStyle && !this.isVoid(node.textContent)) {
this.blurryWordsWithTag(node);
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
this.observer = observer;

这里需要说明一点:默认不开启异步数据模糊处理,因为这样会影响性能,如果需要开启,设置 autoBlur 为 true

推荐使用方式:页面设置个按钮,点击按钮开启模糊,这样可以避免一些性能问题

使用

  1. 安装
npm i blurryjs
# 或
yarn add blurryjs
# 或
pnpm i blurryjs
  1. 引入
import Blur from "blurryjs";
  1. 快速使用
const blur = new Blur(); // 默认全局数字模糊

参数说明

5 个参数

  1. blur?: number:模糊程度,单位:px,默认 2px
  2. blurryWords?: string[]:需要模糊的词,默认数字模糊
  3. operator?: string:默认模糊,可以是其他符号,比如 * 等
  4. autoBlur?: boolean:是否自动模糊,默认 false,只处理静态文本,如果需要处理异步数据,需要设置为 true
  5. excludes?: string[]:排除不需要模糊的 dom,类名,比如 ["ant-input"]
const blur = new Blur({
blur: 2,
blurryWords: ["周杰伦", "陈奕迅"],
operator: "*",
autoBlur: false,
excludes: ["ant-input"],
});

3 个方法:

  1. enableBlur(options):开启模糊,options 参数为:blurblurryWordsoperatorautoBlur

    blur.enableBlur({
    blur: 2,
    blurryWords: ["1", "2", "3"],
    operator: "*",
    autoBlur: false,
    });
  2. disableBlur:关闭模糊

    blur.disableBlur();
  3. destroy:销毁

    blur.destroy();

demo

git clone https://github.com/astak16/blurryjs.git

cd blurryjs

pnpm i

pnpm dev

效果

Kapture 2023-06-30 at 14.48.53.gif


作者:uccs
来源:juejin.cn/post/7250311237107122232
收起阅读 »

同学,请实现一个扫码登录

web
马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦! 即使是年底,打工人的工作量也没有减少,最近leader扔给我一个扫码登录的需求...
继续阅读 »

马上要到春节了,小伙伴们的公司是不是已经可以申请请假调休呢?虽然今年刚入职没有年假(好像国家不是这么规定的,但也不好跟公司硬杠),大小周的我已经攒了7天调休,也可以提前回家过年啦!


即使是年底,打工人的工作量也没有减少,最近leader扔给我一个扫码登录的需求,我一看有点来劲了。一来做了多年前端,类似的需求还没有接触过,平时做的多的页面需求和改改bug对自身能力显然是无法提升的。二来扫码登录的功能很多应用都有做过,常见的微信扫码登录,也挺好奇具体如何实现。我大概看了一遍需求文档,写的挺详细的,流程图也标明了各端的交互流程。由于内网开发,产品流程图也忘记截图了,此处在网上找到的一个大概的流程图:
image.png


主要涉及到的是pc端、手机端和后台服务端。由于听产品同事说手机端由原生端(安卓和IOS)来实现,因此我这边只需要开发pc端就行,工作量直接减半有没有。做过该功能的小伙伴肯定了解,pc端的实现还是比较简单的,主要就是开启轮询查询后台扫码状态,然后做对应的提示或登录成功后跳转首页。


扫码登录的需求在前端主要难点在轮询上


0. 什么叫轮询?


所谓的轮询就是,由后端维护某个状态,或是一种连续多篇的数据(如分页、分段),由前端决定按序访问的方式将所有片段依次查询,直到后端给出终止状态的响应(结束状态、分页的最后一页等)。


1. 轮询的方案?


一般有两种解决方案:一种是使用websocket,可以让后端主动推送数据到前端;还有一种是前端主动轮询(上网查了下细分为长轮询和短轮询),通过大家熟悉的定时器(setIntervalsetTimeout)实现。


由于项目暂未用到websocket,且长轮询需要后台配合,所以直接采用短轮询(定时器)开撸了。


遇到的问题:


1、由于看需求文档上交互流程比较清晰,最开始没去网上查找实现方案,自己直接整了一版setInterval的轮询实现。在跟后台联调的过程中发现定时器每1s请求一次接口,发现很多接口没等响应就开启下一次的请求,很多请求都还在pending中,这样是不对的,对性能是很大消耗。于是想了下,可以通过setTimeout来优化,具体就是用setTimeout递归调用方式模拟setInterval的效果,达到只有上一次请求成功后才开启下一次的请求。


// 开启轮询
async beginPolling() {
if (this.isStop) return;
try {
const status = await this.getQrCodeStatus();
if (!status) return;
this.codeStatus = status;
switch(this.codeStatus) {
case '2':
this.stopPolling();
// 确认登录后,需前端修改状态
this.codeStatus = '5';
this.loading = true;
// 走登录逻辑
this.$emit('login', {
qrcId: this.qrcId,
encryptCSIIStr: this.macAddr
})
break;
case '3':
// 取消登录
this.stopPolling();
await this.getQrCode();
break;
case '4':
// 二维码失效
this.stopPolling();
break;
default:
break;
}
this.timer = setTimeout(this.beginPolling);
} catch(err) {
console.log(err);
this.stopPolling();
}
},

2、在自测了过程中又发现了另外一个问题,stopPolling方法中clearTimeout似乎无法阻止setTimeout的执行,二维码失效后请求仍在不停发出,这就很奇怪了。上网搜索了一番,发现一篇文章(很遗憾,已经找不到是哪篇文章了!)记录了这个问题:大概意思是虽然clearTimeout已经清除了定时器,但此时有请求已经在进行中,导致再次进入了循环体,重新开启了定时器。解决办法就是,需要手动声明一个标识位isStop来阻止循环体的执行。


    stopPolling() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
// 标记终止轮询(仅clearTimeout无法阻止)
this.isStop = true;
}
},

试了下确实达到效果了,但其实这个问题产生的具体原因我还是有些模糊的,希望遇到过相关问题的大佬能指点一下,感激不尽!


3、解决了上面提到的问题,就在以为万事大吉,只待提测的时候。后台同事发现了一个问题(点赞后台同事的尽责之心):他在反复切换登录方式(扫码登录<->账号密码登录)的过程中,发现后台日志有一段时间打印的qrcId不是最新的。然后我这边试了下,确实在切换频率过高时,此时有未完成的请求仍在进行中,导致qrcId被重新赋值了。虽然已经在beforeDestroy里调用了stopPolling清除定时器,但此时请求是未停止的。聪明的小伙伴们肯定想到axioscancelToken可以取消未完成的请求,但我实际也并没有用过,而且项目里也没有可以表演Ctrl+CCtrl+V的地方。于是百度了一番,找到一篇掘友的文章,为了表示尊敬我原封不动的搬到我的代码里了,哈哈!


import axios from "axios";
const CancelToken = axios.CancelToken;

const cancelTokenMixin = {
data() {
return {
cancelToken: null, // cancelToken实例
cancel: null, // cancel方法
};
},
created() {
this.newCancelToken();
},
beforeDestroy() {
//离开页面前清空所有请求
this.cancel("取消请求");
},
methods: {
//创建新CancelToken
newCancelToken() {
this.cancelToken = new CancelToken((c) => {
this.cancel = c;
});
},
},
};
export default cancelTokenMixin;

掘友文章[:](在vue项目中取消axios请求(单个和全局) - 掘金 (juejin.cn))


在组件里引入mixin,另外在请求时传入cancelToken实例,确实达到效果了。此时再次切换登录方式,之前的未完成的请求已被取消,也就无法再篡改qrcId。写到此处,我发现问题2也是未完成的请求导致的,那么是否可以不用isStop标识,直接在stopPolling中调用this.cancel("取消请求");不是更好吗?


完整代码如下:


import sunev from 'sunev'; // 全局公共方法库
import cancelTokenMixin from "@/utils/cancelTokenMixin"; // axios取消请求

export default {
props: {
loginType: {
type: String,
default: 'code'
}
},
mixins: [cancelTokenMixin],
data() {
return {
qrcId: '', // 二维码标识
qrcBase64: '', // 二维码base64图片
macAddr: '', // mac地址
loading: false,
isStop: false,
codeStatus: '0',
qrStatusList: [
{
status: '-1',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '二维码生成失败\n请刷新重试',
refresh: true
},
{ status: '0', icon: '', text: '', refresh: false },
{
status: '1',
icon: 'scan',
color: '#2986ff',
svgClass: 'icon-scan-small',
text: '扫描成功\n请在移动端确认',
refresh: false
},
{
status: '2',
icon: 'confirm',
color: '#2986ff',
svgClass: 'icon-confirm-small',
text: '移动端确认登录',
refresh: false
},
{
status: '3',
icon: 'cancel',
text: '移动端已取消',
refresh: false
},
{
status: '4',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '二维码已失效\n请刷新重试',
refresh: true
},
{
status: '5',
icon: 'success',
color: '#2986ff',
svgClass: 'icon-success-small',
text: '登录成功',
refresh: false
},
{
status: '6',
icon: 'error',
color: '#ed7b2f',
svgClass: 'icon-error-small',
text: '登录失败\n请刷新重试',
refresh: true
}
],
errMsg: ''
}
},
async created() {
try {
await this.getQrCode();
this.beginPolling();
} catch(err) {
console.log(err);
}
},
computed: {
// 当前状态
curQrStatus() {
const statusObj = this.qrStatusList.find(item => item.status === this.codeStatus);
if (this.errMsg) {
statusObj.text = this.errMsg;
}
return statusObj;
}
},
methods: {
// 开启轮询
async beginPolling() {
if (this.isStop) return;
try {
const status = await this.getQrCodeStatus();
if (!status) return;
this.codeStatus = status;
switch(this.codeStatus) {
case '2':
this.stopPolling();
// 确认登录后,需前端修改状态
this.codeStatus = '5';
this.loading = true;
// 走登录逻辑
this.$emit('login', {
qrcId: this.qrcId,
encryptCSIIStr: this.macAddr
})
break;
case '3':
// 取消登录
this.stopPolling();
await this.getQrCode();
break;
case '4':
// 二维码失效
this.stopPolling();
break;
default:
break;
}
this.timer = setTimeout(this.beginPolling);
} catch(err) {
console.log(err);
this.stopPolling();
}
},
// 暂停轮询
stopPolling() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
// 标记终止轮询(仅clearTimeout无法阻止)
this.isStop = true;
}
},
// 获取二维码base64
async getQrCode() {
this.reset();
this.loading = true;
try {
const params = {
encryptCSIIStr: this.macAddr
}
const res = await sunev.$https.post(
'sunev/LoginQRCGen',
{ isLoading: false, cancelToken: this.cancelToken },
params
)
if (res.qrcId) {
this.qrcId = res.qrcId;
this.qrcBase64 = res.qrcBase64;
} else {
this.stopPolling();
}
} catch(err) {
this.errMsg = err.message;
this.stopPolling();
}
},
// 获取二维码状态
async getQrCodeStatus() {
try {
const params = {
encryptCSIIStr: this.macAddr
}
const res = await sunev.$https.post(
'sunev/LoginQRCQry',
{ isLoading: false, cancelToken: this.cancelToken },
params
)
return res.status;
} catch(err) {
this.stopPolling();
}
},
// 刷新二维码
async refresh() {
await this.getQrCode();
this.beginPolling();
},
// 切换登录类型
toggle() {
this.$emit('toggleLoginType');
},
// 重置
reset() {
this.isStop = false;
this.codeStatus = '0';
this.errMsg = '';
},
beforeDestroy() {
this.stopPolling();
}
}
}

ps:


1、由于是老项目了,登录界面逻辑较多,避免臃肿,二维码登录拆分成单独组件实现


2、由于项目组在内网开发,以下代码都是一行行重新手打的,不是很重要的html和css部分就省略了


后记:


由于此需求并不着急上线,暂未提测,所以还不知测试同事会提出怎样的bug。另外掘友们如果发现问题,也欢迎批评指正,感激不尽!


作者:wing98
来源:juejin.cn/post/7326268998490865673
收起阅读 »

前端如何统一开发环境

web
统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。 nodejs 首先推荐使用 fnm 管理多版本 nodejs。 对比 nvm: 支持 brew 安装,...
继续阅读 »

统一不同的同事之间,本地和 CI 之间的开发环境有利于保证运行效果一致和 bug 可复现,本文聚焦于前端最基本的开发环境配置:nodejs 和 包管理器。


nodejs


首先推荐使用 fnm 管理多版本 nodejs。


对比 nvm



  • 支持 brew 安装,更新方便

  • 跨平台,windows 也能用 winget 安装


使用 fnm 一定要记得开启根据当前 .nvmrc 自动切换对应的 nodejs 版本,也就是在在 .zshrc 中加入:


eval "$(fnm env --use-on-cd)"

包管理器


尽管 npm 一直在进步,甚至 7.x 已经原生支持了 workspace。但是我钟爱 pnpm,理由:



  • 安全,避免幽灵依赖,不会将依赖的依赖平铺到 node_modules 下

  • 快,基于软/硬链接,node_modules 下是软连接,硬链接到 .pnpm 文件夹下的硬链接

  • 省磁盘,公司配的 mac 只有 256G

  • pnpm 的可配置性很强,配置不够用还可以用 .pnpmfile.js 编写 hooks

  • yarn2 node_modules 都看不到,分析依赖太麻烦了

  • 公司用的 vue,而 vue3/vite 用 pnpm(政治正确)


推荐使用 Corepack 管理用户的包管理器,其实我一开始知道有 corepack 这个 nodejs 官方的东西的时候,我就在想:为啥不叫 npmm(node package manager manager) 呢?


corepack 目前官方觉得功能没稳定,所以默认没开启,需要用户通过 corepack enable 手动开启,相关的讨论:enable corepack by default


有了 corepack 我们就可以轻松的在 npm/yarn/pnpm 中切换,安装和更新不同的版本。还有一个非常方便的特性就是通过在 package.json 中声明 packageManager 字段例如 "pnpm@8.14.1",当我们开启了 corepack,cd 到该 package.json 所在的 package 的时候,运行 pnpmcorepack 会使用 8.14.1 版本的 pnpm


corepack 是怎样做到的呢?nodejs 安装文件夹有个的 bin 目录,这个目录会被添加到 path 环境变量,其中包含了 corepack 以及 corepack 支持的包管理器的可执行文件:


❯ tree ../../Library/Caches/fnm_multishells/17992_1705553706619/bin
../../Library/Caches/fnm_multishells/17992_1705553706619/bin
├── corepack -> ../lib/node_modules/corepack/dist/corepack.js
├── node
├── npm -> ../lib/node_modules/npm/bin/npm-cli.js
├── npx -> ../lib/node_modules/npm/bin/npx-cli.js
├── pnpm -> ../lib/node_modules/corepack/dist/pnpm.js
├── pnpx -> ../lib/node_modules/corepack/dist/pnpx.js
├── yarn -> ../lib/node_modules/corepack/dist/yarn.js
└── yarnpkg -> ../lib/node_modules/corepack/dist/yarnpkg.js

可以看到 pnpm 被链接到了 corepack 的一个 js 文件,查看 corepack/dist/pnpm.js 内容:


#!/usr/bin/env node
require('./lib/corepack.cjs').runMain(['pnpm', ...process.argv.slice(2)]);

可以看到其实 corepack 相当于劫持了 pnpmyarn 命令,然后根据 packageManager 字段配置自动切换到对应的包管理器,如果已经安装过就使用缓存,没有就下载。


怎样统一 nodejs 和 包管理器


问题


虽然我在项目中配置了 .nvmrc 文件,在 package.json 中声明了 packageManager 字段,但是用户可能没有安装 fnm 以及配置根据 .nvmrc 自动切换对应的 nodejs,还有可能没有开启 corepack,所以同事的环境还是有可能和要求的不一致。我一向认为,不应该依靠人的自觉去遵守规范,通过工具强制去约束才能提前发现问题和避免争论。


解决办法


最开始是看到项目中使用了 only-allow 用于限制同事开发时只能用 pnpm,由此我引发了我一个灵感,为什么我不干脆把事情做绝一点,把 nodejs 的版本也给统一了


于是我写了一个脚本用于检查用户本地的 nodejs 的版本,包管理器的版本必须和要求一致。最近封装成了一个 cli:check-fe-env。使用方式很简单,增加一个 preinstall script:


{
"scripts": {
"preinstall": "npx check-fe-env"
}
}

工作原理



  • 用户在运行 pnpm install 之后,install 依赖之前,包管理器会执行 preinstall 脚本

  • cli 会检测:

    • 用户当前环境的 nodejs 版本和 .nvmrc 中声明的是否一样

    • 用户当前使用的包管理器种类和版本是否和 package.jsonpackageManager 字段一样




获取当前环境的 nodejs 版本很简单,可以用 process.version。想要获取执行脚本时的包管理器可以通过环境变量:process.env.npm_config_user_agent,如果一个 npm script 是通过 pnpm 运行的,那么这个环境变量会被设置为例如 pnpm/8.14.1 npm/? node/v20.11.0 darwin arm64,由此我们可以获取当前使用的包管理器和版本。


为了加快安装速度,我特意把源码和相关依赖给一起打包了,整个 bundle 大小 8k 左右。


局限性


最新的 npmpnpm 目前貌似都有一个 bug,都是安装完依赖才执行 preinstall hooks,具体看这: Preinstall script runs after installing dependencies


这个方案对于 monorepo 项目或者说不需要发包的项目是没啥问题的,但是不适用于一个要发包的项目。原因是 preinstall script 除了会在本地 pnpm install 时执行,别人安装这个包,也会执行这个 preinstall script,就和 vue-demi 用的 postinstall script 一样。主要是确实没找到一个:只会在本地运行 pnpm install 后且在安装依赖前执行的 hook。


作者:余腾靖
来源:juejin.cn/post/7325069743143878697
收起阅读 »

一个指令实现左右拖动改变布局

web
一个指令实现左右拖动改变布局 一、前言 本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了: 实现思路 总结关键技术点 完整 demo 二、实现思路 2.1 外层div布局 首先设置4个div元素,一个作为父容器,一个...
继续阅读 »

一个指令实现左右拖动改变布局


一、前言


本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了:





    1. 实现思路





    1. 总结关键技术点





    1. 完整 demo




二、实现思路


2.1 外层div布局


首先设置4个div元素,一个作为父容器,一个作为左边的容器,一个在中间作为拖动指令承载的元素,最后一个在作为右边容器的元素。


js
复制代码
<div>
<div class="left"></div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right"></div>
</div>

2.2 获取指令元素的父元素和兄弟元素


首先,接收指令传递的各元素的宽,并进行初始赋值和利用 calc 计算右边元素宽度。


js
复制代码
let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

然后,接收指令传递下来的元素 el,并根据该元素 通过 Element.previousElementSibling 获取当前元素前一个兄弟元素,即是 所在的元素。 通过
Element.nextElementSibling 获取当前元素的后一个兄弟元素,即是 所在的元素。 通过 Element.parentElement 获取当前元素的父元素。


js
复制代码
bind: function (el, binding, vnode) {
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement
}

2.3 利用浮动定位,实现浮动布局


接着,给各个容器元素设置浮动定位 float = 'left'。当然,其实其他方式也可以的,只要能达到类似“行内块”的布局即可。


可以提一下的是,设置 float = 'left' 可以创建一个独立的 BFC 区域,具有“独立隔离性”, 即 BFC 区域内部元素的布局,不会“越界”影响外部元素的布局; 外部元素的布局也不会“穿透”,影响 BFC 区域的内部布局。


js
复制代码
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement

box.style.float = 'left'
left.style.float = 'left'
resize.style.float = 'left'
right.style.float = 'left'

2.4 实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标


通过 onpointerdown 监听,实现实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标,这个特定元素即 v-resize 指令所在的元素。


这样,就可以通过获取 v-resize 指令所在的元素的位置属性,来计算出左右的元素,在拖动时需要设置的宽和位置信息。


js
复制代码
resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.setPointerCapture(e.pointerId);
return false
}

2.5 实现鼠标移动时,改变左右的宽度


通过 onpointermove 监听,实现在鼠标指针移动时,获取鼠标事件的位置信息 clientX 等,并由此计算出合适的移动距离 moveLen, resize 的左边距离,left 元素的宽,以及 right
元素的宽。


由此,就实现了每移动一步,就重新计算出新的布局位置信息,并进行了赋值。


js
复制代码
resize.onpointermove = function (e) {
let endX = e.clientX

let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step

resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}

2.6 鼠标抬起时,将鼠标指针从先前捕获的元素中释放


通过监听 onpointerup,实现在鼠标指针抬起时,通过 releasePointerCapture 将鼠标指针从先前捕获的元素中释放,还给鼠标自由。并将 resize 元素的 onpointermove 事件设置为
null。这样,当鼠标被抬起后,再操作就不会携带此前的绑定操作了。


js
复制代码
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}

经过上诉步骤,我们就实现了,从鼠标按下,到移动计算改变布局,然后鼠标抬起释放绑定,操作完成,改变布局的目标达成。


三、总结关键技术点


实现本需求主要的关键技术点有:


3.1 setPointerCapture 和 releasePointerCapture


Element.setPointerCapture() 用于将特定元素指定为未来指针事件的捕获目标。 指针的后续事件将以捕获元素为目标,直到捕获被释放(通过 Element.releasePointerCapture())。


Element.releasePointerCapture() 则用来将鼠标从先前通过 Element.setPointerCapture() 绑定的元素身上释放出来,还给鼠标自由。


需要注意的是,类似的功能事件还有 setCapture() 和 releaseCapture,但它们已经被标记为弃用,且是非标准的,所以不建议使用。


3.2 onpointerdown,onpointermove 和 onpointerup


与上面配套的关键事件还有,onpointerdown,onpointermove 和 onpointerup。其中 onpointermove 是实现主要改变布局的逻辑的地方。


pointerdown:全局事件处理程序,当鼠标指针按下时触发。返回 pointerdown 事件触发对象的事件处理程序。


onpointermove:全局事件处理程序,当鼠标指针移动时触发。返回 targetElement 元素的 pointermove 事件处理函数。


onpointerup:全局事件处理程序,当鼠标指针抬起时触发。返回 targetElement 元素的pointerup事件处理函数。


3.3 注意事项


① Vue.nextTick 的使用。在 vue 指令定义的 bind 中使用了 Vue.nextTick,是为了解决初次运算时,有些 dom 元素未完成渲染,设置元素属性会报警告或错误。


js
复制代码
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
}
})

② position = 'relative' 的设置。给每个元素 left 和 right 元素设置 position = 'relative',是为了解决 z-index 可能会失效的问题,我们知道有时浮动元素会导致这种情形发生。
当然这并不影响本次需求的实现,是为了其他设计考虑才这样做的。


js
复制代码
left.style.position = 'relative'
resize.style.position = 'relative'
right.style.position = 'relative'

③ cursor = 'col-resize' 的设置。为了获得更友好的体验,使得用户一眼鉴别这个功能,我们使用了 cursor 的 col-resize 属性。


js
复制代码
resize.style.cursor = 'col-resize'

四、完整 demo


// 这是定义指令的完整代码:directive.js


js
复制代码

/**
* 自定义调整宽度指令:添加指令后,可以实现拖拽边线改变页面元素的宽度。
* 指令接收两个参数,left 左边元素的宽度,中间 resize 元素的宽度。数据类型均为 number
* 使用示例:
* <div>
* <div></div>
* <div v-resize="{left: 300, resize: 10}" />
* <div></div>
* </div>
*
* 注意:由于是使用 float 布局,所以需要保证有4个元素作为浮动元素的容器,即父容器 1 个,子容器 3 个。
*
*/

import Vue from 'vue'

const resizeDirective = {}
const handler = (el, binding, vnode) => {

let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

if (binding.value?.left && Object.prototype.toString.call(binding.value?.left) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}
if (binding.value?.resize && Object.prototype.toString.call(binding.value?.resize) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}

let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement

box.style.float = 'left'
box.style.height = '100%'
box.style.width = '100%'
box.style.overflow = 'hidden'

left.style.float = 'left'
left.style.width = leftWidth + 'px'
left.style.position = 'relative'

resize.style.float = 'left'
resize.style.cursor = 'col-resize'
resize.style.width = resizeWidth + 'px'
resize.style.height = box.offsetHeight + 'px'
resize.style.position = 'relative'

right.style.float = 'left'
right.style.width = rightWidth
right.style.position = 'relative'
right.style.zIndex = 99

resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.onpointermove = function (e) {
let endX = e.clientX

let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step

resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}
resize.setPointerCapture(e.pointerId);
return false
}
}
resizeDirective.install = Vue => {
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
},
update: function (el, binding) {
handler(el, binding)
},
unbind: function (el, binding) {
el.instance && el.instance.$destroy()
}
})
}

export default resizeDirective



// 在 main.js 中使用


js
复制代码
import resizeDirective from './directive'

Vue.use(resizeDirective)

// 在具体页面中使用:ResizeWidth.vue


html
复制代码

<template>
<div>
<div class="left">left</div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right">right</div>
</div>
</template>

<script>
export default {
name: 'ResizeWidth'
}
</script>

<style scoped>
.left {
background: #42b983;
height: 50vh;
}

.resize {
background: #EEEEEE;
height: 50vh;
}

.right {
background: #1e87f0;
height: 50vh;
}
</style>


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

箭头函数太长了,缩短小窍门来了

web
前言 使用箭头语法,你可以定义比函数表达式短的函数。在某些情况下,你可以完全省略: 参数括号 (param1, param2) return 关键字 甚至大括号 { }。 1. 基本语法 完整版本的箭头函数声明包括: 一对带有参数枚举的括号 (param...
继续阅读 »

前言


使用箭头语法,你可以定义比函数表达式短的函数。在某些情况下,你可以完全省略:



  • 参数括号 (param1, param2)

  • return 关键字

  • 甚至大括号 { }


1. 基本语法


完整版本的箭头函数声明包括:



  • 一对带有参数枚举的括号 (param1, param2)

  • 后面跟随箭头 =>

  • 以函数体 {FunctionBody} 结尾


典型的箭头函数如下所示:


const sayMessage = (what, who) => {
  return `${what}${who}!`;
};

sayMessage('Hello''World'); // => 'Hello, World!'

这里有一点需要注意:你不能在参数 (param1, param2) 和箭头 => 之间放置换行符。


接下来我们看看如何缩短箭头函数,在处理回调时,使它更易于阅读。


2. 减少参数括号


以下函数 greet 只有一个参数:


const greet = (who) => {
  return `${who}, Welcome!`
};

greet('Aliens'); // => "Aliens, Welcome!"

greet 箭头函数只有一个参数 who 。该参数被包装在一对圆括号(who) 中。


当箭头函数只有一个参数时,可以省略参数括号。


可以利用这种性质来简化 greet


const greetNoParentheses = who => {
  return `${who}, Welcome!`
};

greetNoParentheses('Aliens'); // => "Aliens, Welcome!"

新版本的箭头函数 greetNoParentheses 在其单个参数 who 的两边没有括号。少两个字符:不过仍然是一个胜利。


尽管这种简化很容易掌握,但是在必须保留括号的情况下也有一些例外。让我们看看这些例外。


2.1 注意默认参数


如果箭头函数有一个带有默认值的参数,则必须保留括号。


const greetDefParam = (who = 'Martians') => {
  return `${who}, Welcome!`
};

greetDefParam(); // => "Martians, Welcome!"

参数 who 的默认值为 Martians。在这种情况下,必须将一对括号放在单个参数(who ='Martians')周围。


2.2 注意参数解构


你还必须将括号括在已解构的参数周围:


const greetDestruct = ({ who }) => {
  return `${who}, Welcome!`;
};

const race = {
  planet'Jupiter',
  who'Jupiterians'
};

greetDestruct(race); // => "Jupiterians, Welcome!"

该函数的唯一参数使用解构 {who} 来访问对象的属性 who。这时必须将解构式用括号括起来:({who {}})


2.3 无参数


当函数没有参数时,也需要括号:


const greetEveryone = () => {
  return 'Everyone, Welcome!';
}

greetEveryone(); // => "Everyone, Welcome!"

greetEveryone 没有任何参数。保留参数括号 ()


3. 减少花括号和 return


当箭头函数主体内仅包含一个表达式时,可以去掉花括号 {} 和 return 关键字。


不必担心会忽略 return,因为箭头函数会隐式返回表达式评估结果。这是我最喜欢的箭头函数语法的简化形式。


没有花括号 {} 和 return 的 greetConcise 函数:


const greetConcise = who => `${who}, Welcome!`;

greetConcise('Friends'); // => "Friends, Welcome!"

greetConcise 是箭头函数语法的最短版本。即使没有 return,也会隐式返回 $ {who},Welcome! 表达式。


3.1 注意对象文字


当使用最短的箭头函数语法并返回对象文字时,可能会遇到意外的结果。


让我们看看这时下会发生些什么事:


const greetObject = who => { message: `${who}, Welcome!` };

greetObject('Klingons'); // => undefined

期望 greetObject 返回一个对象,它实际上返回 undefined


问题在于 JavaScript 将大括号 {} 解释为函数体定界符,而不是对象文字。message: 被解释为标签标识符,而不是属性。


要使该函数返回一个对象,请将对象文字包装在一对括号中:


const greetObject = who => ({ message: `${who}, Welcome!` });

greetObject('Klingons'); // => { message: `Klingons, Welcome!` }

({ message: `${who}, Welcome!` })是一个表达式。现在 JavaScript 将其视为包含对象文字的表达式。


4.粗箭头方法


类字段提案(截至2019年8月,第3阶段)向类中引入了粗箭头方法语法。这种方法中的 this 总是绑定到类实例上。


让我们定义一个包含粗箭头方法的 Greet 类:


class Greet {
  constructor(what) {
    this.what = what;
  }
  getMessage = (who) => {
    return `${who}${this.what}!`;
  }
}
const welcome = new Greet('Welcome');
welcome.getMessage('Romulans'); // => 'Romulans, Welcome!'

getMessage 是 Greet 类中的一个方法,使用粗箭头语法定义。getMessage 方法中的 this 始终绑定到类实例。


你可以编写简洁的粗箭头方法吗?是的你可以!


让我们简化 getMessage 方法:


class Greet {
  constructor(what) {
    this.what = what;
  }
  getMessage = who => `${who}${this.what}!`
}
const welcome = new Greet('Welcome');
welcome.getMessage('Romulans'); // => 'Romulans, Welcome!'

getMessage = who => `${who}, ${this.what}! 是一个简洁的粗箭头方法定义。省略了其单个参数 who 周围的一对括号,以及大括号 {} 和 return关键字。


5. 简洁并不总是意味着可读性好


我喜欢简洁的箭头函数,可以立即展示该函数的功能。


const numbers = [145];
numbers.map(x => x * 2); // => [2, 8, 10]

x => x * 2 很容易暗示一个将数字乘以 2 的函数。


尽管需要尽可能的使用短语法,但是必须明智地使用它。否则你可能会遇到可读性问题,尤其是在多个嵌套的简洁箭头函数的情况下。


我更喜欢可读性而不是简洁,因此有时我会故意保留大括号和 return 关键字。


让我们定义一个简洁的工厂函数:


const multiplyFactory = m => x => x * m;

const double = multiplyFactory(2);
double(5); // => 10

虽然 multiplyFactory 很短,但是乍一看可能很难理解它的作用。


这时我会避免使用最短的语法,并使函数定义更长一些:


const multiplyFactory = m => { 
  return x => x * m;
};

const double = multiplyFactory(2);
double(5); // => 10

在较长的形式中,multiplyFactory 更易于理解,它返回箭头函数。


无论如何,你都可能会进行尝试。但我建议你将可读性放在简洁性之前。


6. 结论


箭头函数以提供简短定义的能力而闻名。


使用上面介绍的诀窍,可以通过删除参数括号、花括号或 return 关键字来缩短箭头函数。


你可以将这些诀窍与粗箭头方法放在一起使用。


简洁是好的,只要它能够增加可读性即可。如果你有许多嵌套的箭头函数,最好避免使用最短的形式。


作者:河马老师
来源:juejin.cn/post/7326758010523697192
收起阅读 »

爆肝手写 · 一镜到底特效· 龙年大吉 【CSS3】

web
前言 作为一名有多年开发经验的前端技术开发人员, 我最爱的还是用前端技术实现各种炫酷的特效,对于我来说,CSS3不仅仅是一种样式语言,更是一种表达情感、对美好事物追求的一种体现吧, 虽然每天要沉浸在代码的海洋里,但我也要寻找着技术与艺术的交汇点,努力把吃饭的...
继续阅读 »

前言


作为一名有多年开发经验的前端技术开发人员, 我最爱的还是用前端技术实现各种炫酷的特效,对于我来说,CSS3不仅仅是一种样式语言,更是一种表达情感、对美好事物追求的一种体现吧, 虽然每天要沉浸在代码的海洋里,但我也要寻找着技术与艺术的交汇点,努力把吃饭的家伙变成自己的热爱的事情。 龙年来临之际, 通宵写了一个全新的CSS3 一镜到底的特效案例,如下图, 希望能与大家分享这份创意与激情, 祝各位掘友们新年快乐, 龙年行大运!


Video_20240120111316[00_00_12--00_00_15].gif


上源码:



整体实现思路介绍



整个案例使用到CSS3 和 HTML技术, 案例的核心知识点 使用到了 CSS3 中的透视 、3D变换、 动画 、无缝滚动等技术要点, 下面我会逐一进行介绍




  • 知识点1: 一镜到底特效的 案例的整体布局、设计、及动画思路

  • 知识点2:CSS3中的3D坐标系

  • 知识点3:CSS3中的3D变换及案例应用

  • 知识点4:CSS3中的3D透视及案例应用

  • 知识点5:CSS3中的 透视及3d变换的异同点

  • 知识点6:CSS3中的 动画及案例应用


1、一镜到底特效 的整体布局、设计、及动画思路


如下图所示,一镜到底的案例特效 最核心的就是要 构成一套 在3D 空间中, 有多个平行的场景, 然后以摄像机的视角 从前往后 移动,在场景中穿梭, 依次穿过每一个场景的页面即可啦,自己闭上眼睛来体验一下吧;
无标题.png


对应到本案例中效果就是这样啦:


image.png


当然有朋友会说看上图,感觉不到明显的3D 立体效果, 那再来看看下面这个图吧;


消失点.png


上面这张图就是 基于人眼 看不同距离的物体呈现出的结果, 也就是透视效果, 透视效果最核心的特点就是近大远小;而影响看到透视物体大小的一个参数就是消失点距离, 比如消失点越近,最远处的物体会越小, 近大远小的效果越明显, 自己闭上眼睛来体验一下吧;


对应到本案例中效果就是这样啦:


image.png



  • 上述框架对应的HTML源码如下, 其中.sence-in 内部的子元素是素材,可以先忽略:


<div class="sence-box sence-box1">
<div class="sence-in">
<div class="text-left text-box">掘金多多</div>
<div class="text-right text-box">大展鸿图</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
</div>
</div>
<div class="sence-box sence-box2">
<div class="sence-in">
<div class="text-left text-box">步步高升</div>
<div class="text-right text-box">年年有鱼</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box3">
<div class="sence-in">
<div class="text-left text-box">心想事成</div>
<div class="text-right text-box">万事如意</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box4">
<div class="sence-in">
<div class="text-left text-box">蒸蒸日上</div>
<div class="text-right text-box">一帆风顺</div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="denglong-box"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>
<div class="sence-box sence-box5">
<div class="sence-in">
<div class="text-left text-box">自强不息</div>
<div class="text-right text-box">恭喜发财</div>
<div class="sence-block">龙年大吉</div>
<div class="denglong-box"></div>
<div class="long long-left"></div>
<div class="long long-right"></div>
<div class="long2 long2-left"></div>
<div class="long2 long2-right"></div>
<div class="xiangyun"></div>
</div>
</div>

知识点一: CSS3中的坐标系


CSS3中的坐标系,是一切3D 效果的基石, 务必熟练掌握 , 如下图所示:



  • x轴坐标:左边负,右边正

  • y轴坐标:上边负,下边正

  • z轴坐标:里面负,外面正

  • 注意: 坐标系的原点在 浏览器的左上角


image.png


知识点二: 透视(perspective)


perspective属性定义了观察者和Z=0平面之间的距离,从而为3D转换元素创建透视效果。上面也说了, 透视的效果就是 近大远小, 上面的截图中也能看到 。这个属性是用来创建3D转换效果的必要属性,因为当我们进行旋转或其他3D转换时,如果透视效果设置得不正确,元素可能会显得很奇怪或不正常。 透视的语法如下:


在CSS中,我们可以通过在父元素上设置perspective属性来控制子元素的3D效果。例如:


	.container {  
perspective: 1000px;
}

在这个例子中,我们为.container元素设置了perspective属性,值为1000px。这意味着任何在这个元素内部的3D转换都会基于这个视距进行透视。


知识点三:3D 变换的核心属性: transform-style


transform-style属性决定了是否保留元素的三维空间布局。当设置为preserve-3d时,它会保留元素内部的三维空间,即使这个元素本身没有进行任何3D转换。这使得子元素可以相对于父元素进行旋转或其他3D转换,而不会影响其他元素。在我们的案例截图中 也能看出在父元素设置了 transform-style: preserve-3d;属性后, 各个场景在 Z轴方向上,已经有了前后距离上的差异了。 需要注意的点就是, transform-style属性一定要设置给发生3D变换元素的父元素


例如:


 /* 透视属性加给了 最外层的元素, 保证所有子元素的透视效果是一致的,协调的*/
.perspective-box {
transform-style: preserve-3d;
}

在这个例子中,我们为.perspective-box元素设置了transform-style属性为preserve-3d,这意味着任何在这个元素内部的3D转换都会保留其三维空间布局。



  • 小技巧:如果你希望自己做的3D场景,立体效果很真实的话, 可以尽量多的给不同的元素,设置在Z轴方向上 设置不同的偏移量, 这样的效果是 摄像机在穿梭的过程中,每一段距离都能看到不同的风景, 层次感会很强, 当然也不要太疯狂, 不然场景会变得混乱哦


知识点四、perspective和transform-style的差异和注意点(炒鸡重要!)



  • perspective属性定义了观察者和Z=0平面之间的距离,通俗的说 就是屏幕 到消失点的距离,从而影响3D元素的透视效果, 而transform-style属性决定了是否保留元素的三维空间布局

  • 当我们只使用perspective属性时,只有被明确设置为3D转换的元素才会显示透视效果。而当我们使用transform-style: preserve-3d时,即使元素本身没有进行任何3D转换,其子元素也可以进行3D转换并保留三维空间布局。


注意:perspective属性,只能带来近大远小的透视视觉效果,并不能构成真正的3D空间布局。真正的3D布局必须依赖于transform-style: preserve-3d属性来实现


知识点五、animation动画的定义和使用


CSS动画是一种使元素从一种样式逐渐改变为另一种样式的方法。这个过程是通过关键帧(keyframes)来定义的,关键帧定义了动画过程中的不同状态。 在一镜到底的案例中, 整个场景的前后移动,用的就是动画属性。


动画的使用分为两步, 具体使用方式如下:



  • 1.使用@keyframes 来定义动画

  • 2.使用animation属性来调用动画,



@keyframes rotate {
from { transform: rotateX(0deg); }
to { transform: rotateX(360deg); }
}

在这个例子中,我们定义了一个名为“rotate”的关键帧动画,使元素从X轴的0度旋转到360度。然后,我们可以通过将这个动画应用到HTML元素上来使用它:


	.perspective-content {  
animation: rotate 5s infinite linear;
}

在这个例子中,我们将“rotate”动画应用到.cube元素上,设置动画时间为5秒,无限循环,并且线性过渡;


在一镜到底的案例中, 我们定义的动画如下:



@keyframes perspective-content {

0% {
transform: translateZ(0px);
}

50% {
transform: translateZ(6000px);
}

50.1% {
transform: translateZ(-6000px);
}

100% {
transform: translateZ(0px);
}
}


上午动画 其实做了一个无线循环轮播的逻辑, 就是当 在Z轴方向上 从 0 移动到 6000距离以后, 在重置到-6000px, 这样就可以在从-6000继续向前移动, 移动到 0 ,达到一个循环, 再开始下一次的循环;



  • 小技巧: 你可以把动画 单独加给每个场景(可能有10多个子元素, 你的重复写10多遍,会很麻烦的),也可以把动画加给公共的父元素,父元素会带着里面的子元素一起动, 这样只用写一次就行哦;


结束语:


以上就是案例用到的所有知识点啦, 整个案例的代码,可以在顶部源码位置查看,我就不一一解释了, 如有疑问和建议,可以留言,一起探讨学习哦, 本人能力有限, 希望大家多多批评指导;


作者:IT大春哥
来源:juejin.cn/post/7325739662033879090
收起阅读 »

前端实现汉堡菜单

web
如果你曾经在浏览网页时看到三条线堆叠在一起,那么你就遇到了汉堡菜单。它是移动和响应式网页设计中使用的一种流行设计元素,用于创建干净、简约的界面。 单击时,这个小菜单会从屏幕的任一侧滑出,显示导航项或选项列表。当菜单打开时,汉堡菜单也会变成“X”或其他形状。 在...
继续阅读 »

如果你曾经在浏览网页时看到三条线堆叠在一起,那么你就遇到了汉堡菜单。它是移动和响应式网页设计中使用的一种流行设计元素,用于创建干净、简约的界面。


单击时,这个小菜单会从屏幕的任一侧滑出,显示导航项或选项列表。当菜单打开时,汉堡菜单也会变成“X”或其他形状。


在这篇文章中,我们将向您展示如何在 CSS 中创建不同的汉堡菜单动画。让我们开始吧!


创建汉堡菜单


要创建汉堡菜单,我们首先需要创建 HTML 。由一个按钮元素和三个嵌套的 div 元素组成,每个元素代表汉堡图标的一行。


<button class="hamburger">
<div class="hamburger__line"></div>
<div class="hamburger__line"></div>
<div class="hamburger__line"></div>
</button>

接下来,我们将为元素添加一些基本样式。我们将从按钮元素中删除任何默认样式,包括背景和边框颜色。


.hamburger {
background: transparent;
border: transparent;
cursor: pointer;
padding: 0;
}

然后,对于每个线元素,我们将设置背景颜色、高度、宽度和每个直线之间的间距。


.hamburger__line {
background: rgb(203 213 225);
margin: 0.25rem 0;
height: 0.25rem;
width: 2rem;
}

X


是时候使用 CSS 创建一个很酷的汉堡菜单动画了。当用户将鼠标悬停在按钮上时,我们希望线条转换为“X”形。


为了实现这一点,我们将使用  :hover  伪类和  nth-child  选择器来访问每一行。我们将使用  translate() 和  rotate() 函数将线条转换为 X 形状。


第一条线将在 y 轴上向下移动并旋转 45 度以创建一条 X 形状的线。第二行将通过将其不透明度设置为零而消失。最后一条线将在 y 轴上向上移动,并逆时针方向旋转 45 度以完成 X 形状。我们将通过在  translate()rotate()  函数中使用负值,将其转换为与第一行相反的方向。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px) rotate(45deg);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px) rotate(-45deg);
}

若要应用转换,我们将使用该 transition 属性。动画将使用 ease-out 计时功能运行 300 毫秒 (0.3s)。该 all 值表示将对样式更改进行动画处理,包括 transformopacity 属性。


.hamburger__line {
transition: all 0.3s ease-out;
}

通过将鼠标悬停在按钮上来尝试一下。



形成减号


在这种方法中,当按钮悬停在按钮上时,我们会将其变成减号。我们将使用与上一种方法相同的转换,但我们不会旋转第一行和最后一行。


相反,我们将在 y 轴上向下移动第一行,直到它到达第二行。第三条线将向上移动,直到到达第一行。然后,第二行将关闭可见性,就像在前面的方法中一样。


第一行和最后一行的 `transform` 属性将与前面的方法相同,只是我们将不再使用该 `rotate()` 函数。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px);
}

看看它是什么样子的!



要将按钮变成减号,我们可以使用另一种效果,将第一行和最后一行水平移出按钮。我们将使用该 translateX() 函数来指示位置仅在 x 轴上发生了变化。使用 translateX(-100%) ,可以将目标从左向右移出容器,而使用translateX(100%) ,我们可以做相反的事情。


这两种转换都将 opacity 属性设置为零,使第一行和最后一行不可见。因此,动画完成后,只有第二行仍然可见。


.hamburger:hover .hamburger__line:nth-child(1) {
opacity: 0;
transform: translateX(-100%);
}

.hamburger:hover .hamburger__line:nth-child(3) {
opacity: 0;
transform: translateX(100%);
}

看看这如何重现减号。



形成加号


在本节中,我们将向您展示另一种类型的转换。当用户将鼠标悬停在按钮上时,它会变成一个加号。为了达到这种效果,我们将第一条线向下移动,直到它与第二条线相遇,从而形成一条水平线。


然后,我们移动 y 轴上的最后一条线并将其逆时针旋转 90 度,形成加号的垂直线。最后,我们调整 opacity  第二行,使其在动画后不可见。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translateY(9px);
}

.hamburger:hover .hamburger__line:nth-child(2) {
opacity: 0;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translateY(-9px) rotate(-90deg);
}

查看下面的演示,了解这种方法的实际应用。



形成箭头


为了在按钮上创建箭头,我们使用简单的转换技术。第一条线旋转 45 度并沿 x 轴和 y 轴移动,直到它与第二条线的第一个点相交,形成箭头的顶线。然后,我们减小第一行的宽度,使其看起来更时尚。将相同的转换应用于最后一行,以创建箭头的底线。


如果需要调整箭头的位置,请随意调整传递给 translate() 函数的值。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translate(-2px, 4px) rotate(-45deg);
width: 16px;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translate(-2px, -4px) rotate(45deg);
width: 16px;
}

当您将鼠标悬停在按钮上时,箭头的样子如下:



要更改箭头的方向,请调整 translate() 函数的参数。这将确保第一行和最后一行到达第二行的末尾,并且箭头将沿相反方向旋转。


.hamburger:hover .hamburger__line:nth-child(1) {
transform: translate(17px, 4px) rotate(45deg);
width: 16px;
}

.hamburger:hover .hamburger__line:nth-child(3) {
transform: translate(17px, -4px) rotate(-45deg);
width: 16px;
}


原文:phuoc.ng/collection/…


作者:关山月
来源:juejin.cn/post/7325040809698656256
收起阅读 »

龙年到~ 我做了一个龙年红包封面,一大堆人问我教程

web
前言 就在昨天微信公众号给了我一个年终总结赠送了我六百的红包额度,那么我心想白送我? 要知道买额度现在都要一块钱一个红包封面了呢,所以我打算自己做一个红包封面但是听说审核很难过诶~ 没关系我已经踩坑完毕做出来了一个红包封面现在我就把流程分享给大家~ 亲测百分之...
继续阅读 »

前言


就在昨天微信公众号给了我一个年终总结赠送了我六百的红包额度,那么我心想白送我? 要知道买额度现在都要一块钱一个红包封面了呢,所以我打算自己做一个红包封面但是听说审核很难过诶~ 没关系我已经踩坑完毕做出来了一个红包封面现在我就把流程分享给大家~ 亲测百分之百通过!


红包封面展示


img


后台数据


img


这是我做的快去领取吧~


制作的第一个龙年红包上线


制作红包封面


制作红包封面需要 PS 等技术,啊? 我不会啊 我就想到了在线制作海报封面的网站(会 PS 也可以自己画图随便画画都可以只要是原创即可)


我使用的是图怪兽自己在线制作完毕之后喊朋友帮我下载的他有VIP 哈哈哈,也可以进行截图懂我意思吧?


这里我就实现制作了一张海报封面图片,大概话费 30 分钟素材网站上面都有发挥你的想象好吗~


img


紧接着无水印下载,没有 VIP 的按上面说的方法或者评论说一下我帮你~


压缩图片


红包封面它的大小只能是 500kb 的大小


img


丢给熊猫压缩压缩,直接给我压缩到 4 百多 KB


img


打开红包封面平台


微信红包封面开放平台: cover.weixin.qq.com/cgi-bin/mmc…


如果没有注册就注册一个


点击定制封面,进去上传图片


img


上传红包封面进行裁剪到你自己喜欢的感觉即可


img


一些选填的我这里就没准备就没去上传了,接着我们继续上传封面故事


大小不能超过 300 kb 我们继续丢给熊猫压缩压缩可能就没作用了,这下要用到 PS 了


img


打开在线 PS


随便找一个都可以我用的是这个 http://www.tuyitu.com/ps/sources/


点击文件 files 打开你的红包封面图片


img


img


点击图像, 图像大小 我们 宽改为 750 高改为 1250 官方要求的哦



如果把握不住那就用这个裁剪图片网站 tinypng.com/



img


img


修改完毕之后我们进行导出


img


将大小调整到 300kb 如果画质不好那么就去图像修改画布的大小与图片温和即可


img


img


前往红包开放平台上传我们的封面故事,在自己写一段故事描述,我这里就使用混元大模型给我生成一个龙年的祝福语~


img


img


我也祝大家: 龙腾盛世展才华,年贺新春喜洋洋 祝福亲友事事顺,心圆梦圆福满堂!!!


最后一步


证据材料 如果不上传这个 百分之 99 会给退回


img


PSD 源文件


使用在线 PS 打开我们的红包封面图片,在进行另存为 PSD 即可


img


直接进行上传,提交之后等待审核即可,百分之百成功!!


img


img


工作日 10 分钟就审核完毕了,耐心等待~ 如果制作成功 可以贴在评论区一起领取使用呀~




作者:杨不易呀
来源:juejin.cn/post/7325647896047583273
收起阅读 »

Linux操作系统简介:为何成为全球开发者热门选择?

Linux是一种自由和开放源代码的操作系统。这意味着任何人都可以查看、修改和分发Linux的源代码,而不需要支付任何费用。这种开放性使得Linux能够快速地发展和进步,吸引了全球数以万计的开发者共同参与其中,形成了一个庞大的开源社区。那么,Linux究竟是什么...
继续阅读 »

Linux是一种自由和开放源代码的操作系统。这意味着任何人都可以查看、修改和分发Linux的源代码,而不需要支付任何费用。这种开放性使得Linux能够快速地发展和进步,吸引了全球数以万计的开发者共同参与其中,形成了一个庞大的开源社区。

那么,Linux究竟是什么?它又是如何影响我们的生活的呢?让我们一起探索一下。

一、Linux操作系统介绍

在介绍Linux之前,先带大家了解一下什么是自由软件。自由软件的自由(free)有两个含义:第一,是可免费提供给任何用户使用;第二,是指它的源代码公开和自由修改。

所谓自由修改是指用户可以对公开的源代码进行修改,以使自由软件更加完善,还可在对自由软件进行修改的基础上开发上层软件。

Description

下面我们再来看看Linux操作系统的概念:

Linux,一般指GNU/Linux(单独的Linux内核并不可直接使用,一般搭配GNU套件,故得此称呼),是一种免费使用和自由传播的类UNIX操作系统,其内核由林纳斯·本纳第克特·托瓦兹(Linus Benedict Torvalds)于1991年10月5日首次发布。

它主要受到Minix和Unix思想的启发,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统。它支持32位和64位硬件,能运行主要的Unix工具软件、应用程序和网络协议。

二、Linux系统的特点

那么,Linux为什么如此重要呢?这主要得益于它的以下几个特点:

开源免费:

Linux系统是完全免费的,任何人都可以免费使用、修改和分发。这使得Linux得以迅速传播,吸引了大量的开发者参与其中,共同推动其发展。

稳定性高:

Linux系统的稳定性非常高,长时间运行不会出现死机、蓝屏等问题。这也是为什么许多大型企业和政府部门都选择Linux作为服务器操作系统的原因。

兼容性好:

Linux支持几乎所有的硬件平台,包括x86、ARM、PowerPC等。这使得Linux可以在各种不同的设备上运行如个人电脑、手机、路由器等。同时,Linux系统还支持多种编程语言,为开发者提供了广阔的发挥空间。

强大的定制性:

Linux操作系统具有很强的定制性,用户可以根据自己的需求对系统进行深度定制。这使得Linux成为了服务器、嵌入式设备、超级计算机等领域的首选操作系统。

丰富的软件资源:

由于Linux的开源特性,许多优秀的开源软件都选择在Linux平台上发布。这些软件涵盖了从办公应用、图像处理、编程语言到数据库等各种领域,为用户提供了丰富的选择。

社区支持:

Linux拥有一个庞大的开源社区,用户可以在这里寻求帮助、分享经验、讨论问题。这种社区的支持使得Linux用户能够更好地解决问题,提高自己的技能。

三、Linux的应用

Linux的影响力已经远远超出了计算机领域,在服务器、嵌入式、开发、教育等领域都有着广泛应用。

服务器领域:

在服务器领域,Linux已经成为了主流的操作系统。据统计,世界上超过70%的服务器都在运行Linux。

Description
在云计算领域,Linux也占据了主导地位。许多知名的云服务提供商,如Amazon、Google、Microsoft等,都提供了基于Linux的云服务。

嵌入式领域:

由于Linux系统具有高度的可定制性和稳定性,因此在嵌入式领域也有着广泛的应用。

Description

如智能家居设备、无人机、机器人等都使用了Linux作为其操作系统,都离不开Linux系统的支持。这是因为Linux具有高度的可定制性和稳定性,可以满足这些设备的特殊需求。

开发领域:

Linux系统是程序员们的最爱,许多知名的开源项目都是基于Linux系统开发的,如Apache、MySQL、PHP等。

Description

此外,Linux系统还是云计算、大数据等领域的重要基础。

教育领域:

Linux系统在教育领域的应用也日益普及,许多高校和培训机构都开设了Linux相关课程,培养了大量的Linux人才。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是现在还是免费的!点这里立即免费学习!

四、Linux系统的组成

Linux系统一般有4个主要部分:内核,Shell,文件系统和应用程序。

Description

Linux内核: 内核是系统的“内脏“,是运行程序和管理像磁盘及打印机等硬件设备的核心程序。

Linux shell: shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并送入内核中执行。实际上shell是一个命令解释器,解释由用户输入命令并且把他们送到内核。

Linux 文件系统: 文件系统是文件存放在磁盘等存储设备上的组织方法。Linux能支持多种目前流行的文件系统,如XFS、EXT2/3/4、FAT、VFAT、ISO9660、NFS、CIFS等。

Linux应用程序: 标准的Linux系统都有一套称为应用程序的程序集,包括文本编辑器、编程语言、X Window、办公软件、Internet工具、数据库等。

五、总结

总的来说,Linux是一个强大、灵活、稳定和安全的操作系统,它正在改变我们的生活和工作方式。无论你是一名开发者,还是一名普通用户,都应该了解和学习Linux,因为它将会给你带来无尽的可能性和机会。

在未来的日子里,我们将会看到Linux在更多的领域发挥其强大的影响力。无论是在数据中心、云计算、物联网,还是在人工智能、机器学习等领域,Linux都将扮演着重要的角色。

收起阅读 »

Object.assign 这算是深拷贝吗

web
在JavaScript中,Object.assign() 是一个用于合并对象属性的常见方法。然而,对于许多开发者来说,关于它是否执行深拷贝的认识可能存在一些混淆。先说答案Object.assign() 不属于深拷贝,我们接着往下看。 Object.assign...
继续阅读 »

在JavaScript中,Object.assign() 是一个用于合并对象属性的常见方法。然而,对于许多开发者来说,关于它是否执行深拷贝的认识可能存在一些混淆。先说答案Object.assign() 不属于深拷贝,我们接着往下看。


Object.assign() 概览


首先,让我们回顾一下 Object.assign() 的基本用法。该方法用于将一个或多个源对象的属性复制到目标对象,并返回目标对象。这一过程是浅拷贝的,即对于嵌套对象或数组,只是拷贝了引用而非创建新的对象。


const obj = { a: 1, b: { c: 2 } };
const obj2 = { d: 3 };

const mergedObj = Object.assign({}, obj, obj2);

console.log(mergedObj);
// 输出: { a: 1, b: { c: 2 }, d: 3 }

浅拷贝的陷阱


浅拷贝的特性意味着如果源对象中包含对象或数组,那么它们的引用将被复制到新的对象中。这可能导致问题,尤其是在修改新对象时,原始对象也会受到影响。


const obj = { a: 1, b: { c: 2 } };
const clonedObj = Object.assign({}, obj);
clonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 3 } }
console.log(clonedObj); // { a: 1, b: { c: 3 } }

在这个例子中,修改 clonedObj 的属性也会影响到原始对象 obj


因此,如果我们需要创建一个全新且独立于原始对象的拷贝,我们就需要进行深拷贝。而 Object.assign() 并不提供深拷贝的功能。


深拷贝的需求


如果你需要进行深拷贝而不仅仅是浅拷贝,就需要使用其他的方法,如使用递归或第三方库来实现深度复制。以下是几种常见的深拷贝方法:


1. 使用 JSON 序列化和反序列化


const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = JSON.parse(JSON.stringify(obj));
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

这种方法利用了 JSON 的序列化反序列化过程,通过将对象转换为字符串,然后再将字符串转换回对象,实现了一个全新的深拷贝对象。


需要注意的是,这种方法有一些限制,例如无法处理包含循环引用的对象,以及一些特殊对象(如 RegExp 对象)可能在序列化和反序列化过程中失去信息。


2. 使用递归实现深拷贝


function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

const clonedObj = Array.isArray(obj) ? [] : {};

for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}

return clonedObj;
}

const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = deepClone(obj);
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

这是一个递归实现深拷贝的方法。它会递归地遍历对象的属性,并创建它们的副本。这种方法相对灵活,可以处理各种情况。


但需要注意在处理大型对象或深度嵌套的对象时可能会导致栈溢出。


3. 使用第三方库


许多第三方库提供了强大而灵活的深拷贝功能,其中最常用的是 lodash 库中的 _.cloneDeep 方法。


const _ = require('lodash');

const obj = { a: 1, b: { c: 2 } };
const deepClonedObj = _.cloneDeep(obj);
deepClonedObj.b.c = 3;

console.log(obj); // { a: 1, b: { c: 2 } }
console.log(deepClonedObj); // { a: 1, b: { c: 3 } }

使用第三方库的优势在于它们通常经过精心设计和测试,可以处理更多的边界情况,并提供更好的性能。


作者:星光漫步者
来源:juejin.cn/post/7325040809697591296
收起阅读 »

什么,你还不会调试线上 vue 组件?

web
前言 彦祖们,在日常开发中,不知道你们是否遇到过这样的场景 在本地测试开发 vue 组件的时候非常顺畅 一上生产环境,客户说数据展示错误,样式不对... 但是你在本地测试了几次,都难以复现 定位方向 这时候作为老 vuer,自然就想到了 vue devtool...
继续阅读 »

前言


彦祖们,在日常开发中,不知道你们是否遇到过这样的场景


在本地测试开发 vue 组件的时候非常顺畅


一上生产环境,客户说数据展示错误,样式不对...


但是你在本地测试了几次,都难以复现


定位方向


这时候作为老 vuer,自然就想到了 vue devtools


但是新问题又来了,线上环境我们如何开启 vue devtools 呢?


案例演示


让我们以 element-ui 官网为例


先看下此时的 chrome devtools 是没有 Vue 的选项卡的
image.png


一段神奇的代码


其实很简单,我们只需要打开控制台,运行一下以下代码


var Vue, walker, node;
walker = document.createTreeWalker(document.body,1);
while ((node = walker.nextNode())) {
if (node.__vue__) {
Vue = node.__vue__.$options._base;
if (!Vue.config.devtools) {
Vue.config.devtools = true;
if (window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit("init", Vue);
console.log("==> vue devtools now is enabled");
}
}
break;
}
}

image.png


显示 vue devtools now is enabled


证明我们已经成功开启了 vue devtools


功能验证


然后再重启一下 chrome devtool 看下效果


image.png


我们会发现此时多了一个 Vue 选项卡,功能也和我们本地调试一样使用


对于遇到 vue 线上问题调试,真的非常好用!


写在最后


本次分享虽然没有什么技术代码,重在白嫖


感谢彦祖们的阅读


个人能力有限


如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟


作者:前端手术刀
来源:juejin.cn/post/7324643000700502031
收起阅读 »

background简写,真细啊!

web
背景原因 今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒: background: url('./bg.png') no-repeat center contain ; 搞定! 上面设置的依次是 背景图片 背...
继续阅读 »

背景原因


今天写需求,需要使用background简写属性,心想这还不简单吗,真男人写样式只需要两秒:


background:  url('./bg.png') no-repeat center contain ;

搞定!


上面设置的依次是 背景图片 背景平铺模式 背景位置 背景图片是保有其原有的尺寸还是拉伸到新的尺寸。


so easy~


看我ctrl + s 保存代码,编译。


嗯? 怎么不生效? 俺的背景呢?
打开控制台一看,好家伙,压根没生效:


image.png


问题排查


第一反应是这些属性有固定顺序,但是凭我练习两年半的经验,不应该啊,之前也是这样用的啊,遂打开MDN,仔细翻阅....


发现了下面这段话:


image.png


这让我更加确信 写的没毛病啊!!


background-attachment、background-color、background-image、background-position、background-repeat、background-size
这些属性可以以任意顺序书写。


见了鬼了,待我排查两小时(摸鱼...)


原因浮现


在仔细阅读文档后发现,其实在文档的上面,还有另外一段话:


image.png


我恍然大悟,索嘎,以后看文档不能马虎了,得仔细查阅,过于经验主义了,这都是细节啊!


background使用注意事项和总结


其实,使用background时,大部分时候 属性的顺序是可以任意位置书写的,
但是有两个属性有点特殊,那就是background-size和background-position,


当background简写同时有这两个属性时,那么必须background-position在前,background-size在后,且两者只能紧挨着书写并且以 "/"分隔。
例如:


错误: background: url('./bg.png') no-repeat center  contain ; // 没有以 "/"分隔
错误: background: url('./bg.png') center no-repeat contain ; // 没有紧挨着书写
错误: background: url('./bg.png') no-repeat contain / center; //background-size写在了 background-position的前面

正确: background: url('./bg.png') no-repeat center / contain ;


写在最后


其实MDN在关于background的文档最开头的例子中就有写:


image.png


只不过没有用语言描述出来,一般没有认真看很难发现,所以有时候能够静下心来认真查阅文档,真的会发现很多细节(甩锅:这tm是谁写的文档,出来挨打).


作者:可狗可乐
来源:juejin.cn/post/7234825495333158949
收起阅读 »

面试官: forEach怎么停止

web
介绍 在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的...
继续阅读 »

介绍


在准备 JavaScript 面试时,理解数组方法的复杂性至关重要。一个常见的问题是是否可以停止或中断 forEach 循环。本文探讨了 forEach 方法的功能、其局限性以及 JavaScript 中用于突破循环的替代解决方案。我们的目标是通过清晰的解释和实际的代码示例来消除这一概念的神秘感。


在深入探讨之前,请在我的个人网站上探索更多关于 Web 开发的深度文章:


了解 JavaScript 中的 forEach 🤔


JavaScript 的 forEach 方法是迭代数组的流行工具。它为每个数组元素执行一次提供的函数。然而,与传统的 forwhile 循环不同,forEach 旨在为每个元素执行函数,没有内置机制来提前停止或中断循环。


const fruits = ["apple", "banana", "cherry"];
fruits.forEach(function(fruit) {
console.log(fruit);
});

这段代码将输出:


apple
banana
cherry

forEach 的局限性 🚫


1. forEach 中的 break


forEach 的一个关键限制是无法使用传统的控制语句比如 breakreturn 来停止或中断循环。如果您试图在 forEach 内使用 break,将遇到语法错误,因为 break 不适用于回调函数中。


尝试中断 forEach


通常,break 语句用于在满足某个条件时提前退出循环。


const numbers = [1, 2, 3, 4, 5];
numbers.forEach(number => {
if (number > 3) {
break; // 语法错误:非法 break 语句
}
console.log(number);
});

当您试图在 forEach 循环中使用 break 时,JavaScript 抛出一个语法错误。这是因为 break 被设计为在传统循环(如 forwhiledo...while)中使用,在 forEach 的回调函数中不被识别。


2. forEach 中的 return


在其他循环或函数中,return 语句退出循环或函数,如果指定的话返回一个值。


forEach 的上下文中,return 不会跳出循环。相反,它仅仅退出回调函数的当前迭代,并继续下一个数组元素。


尝试返回 forEach


const numbers = [1, 2, 3, 4, 5]; 
numbers.forEach(number => {
if (number === 3) {
return; // 仅退出当前迭代
}
console.log(number);
});

输出


1
2
4
5

在这个例子中,return 跳过了打印 3,但是循环继续剩余的元素。


使用异常中断 forEach 循环 🆕


尽管不建议常规使用,但从技术上来说,通过抛出异常可以停止 forEach 循环。尽管这种方法非正统,一般不建议使用,因为它影响代码的可读性和错误处理,但它可以有效地停止循环。


const numbers = [1, 2, 3, 4, 5];
try {
numbers.forEach(number => {
if (number > 3) {
throw new Error('Loop stopped');
}
console.log(number);
});
} catch (e) {
console.log('Loop was stopped due to an exception.');
}
// 输出: 1, 2, 3, 循环由于异常而停止。

在这个例子中,当满足条件时,抛出一个异常,提前退出 forEach 循环。但是,重要的是要正确处理这些异常,以避免意外的副作用。


用于中断循环的 forEach 替代方法 💡


使用 for...of 循环


for...of 循环是在 ES6(ECMAScript 2015)中引入的,它提供了一种现代的、简洁的和可读的方式来迭代类似数组、字符串、映射、集合等可迭代对象。与 forEach 相比,它的关键优势在于它与 breakcontinue 等控制语句兼容,在循环控制方面提供了更大的灵活性。


for...of 的优点:



  • 灵活性:允许使用 breakcontinuereturn 语句。

  • 可读性:提供清晰简洁的语法,使代码更易读和理解。

  • 通用性:能够迭代各种可迭代对象,不仅仅是数组。


for...of 的实际示例


考虑以下场景,我们需要处理数组的元素,直到满足某个条件:


const numbers = [1, 2, 3, 4, 5];  

for (const number of numbers) {
if (number > 3) {
break; // 成功中断循环
}
console.log(number);
}

输出:


1
2
3

在这个例子中,循环迭代 numbers 数组中的每个元素。一旦遇到大于 3 的数字,它利用 break 语句退出循环。这在 forEach 中是不可能的。


其他方法



  • Array.prototype.some():可以使用它来通过返回 true 来模拟中断循环。

  • Array.prototype.every():当返回 false 值时,此方法停止迭代。


结论 🎓


尽管 JavaScript 中的 forEach 方法提供了直接的数组迭代方式,但它缺乏在循环中段中断或停止的灵活性。理解这个限制对开发人员来说至关重要。幸运的是,像 for...of 循环以及 some()every() 等方法提供了必要的控制来处理更复杂的场景。掌握这些概念不仅可以增强你的 JavaScript 技能,还可以让你为艰巨的面试问题和实际编程任务做好准备。


作者:今天正在MK代码
来源:juejin.cn/post/7324384460136611850
收起阅读 »

html中的lang起到什么作用?

web
今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果…… 在chrome上是这样的 再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……...
继续阅读 »

今天被lang="en"这玩意给坑了,平时看着不起眼的一个小配置,结果在中文换行的时候出现了不一样的效果……


在chrome上是这样的


image.png


再看一下火狐浏览器的效果,加不加en都一样…… 起初还以为是chrome渲染机制的问题,把所有代码都删了才找到问题所在……


image.png


记录一下,避坑~


C3C69257283D24A892D56FA7AD82A2B1.png


代码贴在下面,感兴趣的可以去试一下


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<title>Document</title>
</head>

<body>
<p class="MsoNormal" style="width: 300px;background: yellow;">这一党章内容增写入宪法第一条第二款。<span lang="EN-US"></span>中国特色</p>
</body>

</html>

作者:lwlcode
来源:juejin.cn/post/7324750286329282597
收起阅读 »

MyBatis实战指南(三):相关注解及使用

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。一、什么是注解(Annotation)首先,我们需要明...
继续阅读 »

在前面的两篇文章中,我们已经详细介绍了MyBatis的工作原理和基本使用。今天,我们将深入探讨MyBatis的一个重要特性——注解。如果你对MyBatis的注解还不熟悉,那么这篇文章将为你打开一扇新的大门。

一、什么是注解(Annotation)

首先,我们需要明白什么是注解。注解 Annotation 是从JDK1.5开始引入的新技术。

Description

在Java中,注解是一种用于描述代码的元数据,它可以被编译器、库和其他工具读取和使用。MyBatis的注解就是用来简化XML配置的,它们可以让你的代码更加简洁、易读。

注解的作用:

  • 不是程序本身,对程序作出解释
  • 可以被其他程序读取到

Annotation格式:

注解是以@注解名的方式在代码中实现的,可以添加一些参数值

如:@SuppressWarnings(value=“unchecked”)

注解使用的位置:

package、class、method、field 等上面,相当于给他们添加了额外的辅助信息。

注解的分类:

1.元注解:

  • @Target:用于描述注解的使用范围

  • @Retention:用于描述注解的生命周期

  • @Documented:说明该注解将被包含在javadoc 中

  • @Inherited:说明子类可以继承父类中的该注解

  • @Repeatable:可重复注解

2.内置注解:

  • @Override: 重写检查

  • @Deprecated:过时

  • @SuppressWarnings: 压制警告

  • @FunctionalInterface: 函数式接口

3.自定义注解:

  • public @interface MyAnno{}

二、Mybatis常用注解

首先介绍一下Mybatis注解的使用方法:

第一步,在全局配置文件里的配置映射



    


第二步,在mapper接口的方法的上面添加注解

@Select("select * from user where uid = #{uid}")

    public User findUserById(int uid);

第三步,创建会话调用此方法。

接下来,我们来看看MyBatis中最常用的几个注解:

(1)@Select

作用:标记查询语句。

@Select用于标记查询语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Select注解时,需要在注解中指定SQL语句。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

User getUserById(@Param("id") Long id);

(2)@Insert

作用:标记插入语句。

@Insert用于标记插入语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Insert注解时,需要在注解中指定SQL语句。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

int addUser(User user);

(3)@Update

作用:标记更新语句。

@Update用于标记更新语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Update注解时,需要在注解中指定SQL语句。

示例:

@Update("UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}")

int updateUser(User user);

(4)@Delete

作用:标记删除语句。

@Delete用于标记删除语句。该注解可以在接口方法上使用,也可以在XML文件中使用。使用@Delete注解时,需要在注解中指定SQL语句。

示例:

@Delete("DELETE FROM users WHERE id = #{id}")

int deleteUserById(@Param("id") Long id);

(5)@Results

作用:用于指定多个@Result注解。

@Results用于标记结果集映射,该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@Results注解时,需要指定映射规则。

示例:


@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(6)@Result

作用:用于指定查询结果集的映射关系。

@Result用于标记单个属性与结果集中的列之间的映射关系。该注解可以用于接口方法或XML文件中,通常与@Results注解一起使用。使用@Result注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Results(id = "userResultMap", value = {

    @Result(property = "id", column = "id"),

    @Result(property = "name", column = "name"),

    @Result(property = "age", column = "age")

})


User getUserById(@Param("id") Long id);

(7)@ResultMap

作用:用于指定查询结果集的映射关系。

@ResultMap用于标记结果集映射规则。该注解可以用于接口方法或XML文件中,通常与@Select注解一起使用。使用@ResultMap注解时,需要指定映射规则。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@ResultMap("userResultMap")

User getUserById(@Param("id") Long id);

(8)@Options

作用:用于指定插入语句的选项。

@Options用于指定一些可选的配置项。该注解可以用于接口方法或XML文件中,通常与@Insert、@Update、@Delete等注解一起使用。使用@Options注解时,可以指定一些可选的配置项。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@Options(useGeneratedKeys = true, keyProperty = "id")

int insertUser(User user);

(9)@SelectKey

作用:用于指定查询语句的主键生成方式。

@SelectKey用于在执行INSERT语句后获取自动生成的主键值。该注解可以用于接口方法或XML文件中,通常与@Insert注解一起使用。使用@SelectKey注解时,需要指定生成主键的SQL语句和将主键值赋给Java对象的哪个属性。

示例:

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")

@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", before = false, resultType = Long.class)

int insertUser(User user);

(10)@Param

作用:用于指定方法参数名称。

@Param用于为SQL语句中的参数指定参数名称。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Param注解时,需要指定参数名称。

示例:

@Select("SELECT * FROM users WHERE name = #{name} AND age = #{age}")

List getUsersByNameAndAge(@Param("name") String name, @Param("age") Integer age);

(11)@One

作用:用于指定一对一关联关系。

@One用于在一对一关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@One注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。



  

  

  

  







  

  

  

  


上述代码中,@One注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对一关联查询。在departmentResultMap中,使用@One注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为manager,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@One注解之外,还可以使用@Many注解来指定一对多关联查询的映射方式。

总之,@One注解是MyBatis中用于在一对一关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对一关联查询的结果映射。

(12)@Many

作用:用于指定一对多关联关系。

@Many用于在一对多关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@Many注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性。

示例:



  

  

  

  







  

  

  


上述代码中,@Many注解用于指定查询结果的映射方式,这里使用了嵌套的标签实现了一对多关联查询。在departmentResultMap中,使用@Many注解指定了查询结果映射的Java对象类型为User,查询结果映射的属性为members,ofType参数指定了集合中元素的类型为User,resultMap参数指定了查询结果映射的结果集映射规则为userResultMap。

除了使用@Many注解之外,还可以使用@One注解来指定一对一关联查询的映射方式。

总之,@Many注解是MyBatis中用于在一对多关联查询中指定查询结果的映射方式的注解之一,可以方便地实现一对多关联查询的结果映射。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试【云端源想】!课程视频、知识库、微实战、云实验室、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

(13)@ResultType

作用:用于指定查询结果集的类型。

@ResultType用于指定查询结果的类型。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@ResultType注解时,需要指定查询结果的类型。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

@ResultType(User.class)

User getUserById(Long id);

(14)@TypeDiscriminator

作用:用于指定类型鉴别器,用于根据查询结果集的不同类型映射到不同的Java对象。

@TypeDiscriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@TypeDiscriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:



  

  

  

  

    

    

    

  








  

  







  








  


上述代码中,@TypeDiscriminator注解用于指定不同子类型的映射方式。在vehicleResultMap中,使用@TypeDiscriminator注解指定了类型列的名称为type,javaType参数指定了类型列的Java类型为String,标签中的value属性分别对应不同的子类型(car、truck、bus),resultMap属性用于指定不同子类型的结果集映射规则。

除了使用@TypeDiscriminator注解之外,还可以使用标签来指定不同子类型的映射方式。

总之,@TypeDiscriminator注解是MyBatis中用于在自动映射时指定不同子类型的映射方式的注解之一,可以方便地实现自动映射不同子类型的结果集映射规则。

(15)@ConstructorArgs

作用:用于指定Java对象的构造方法参数。

@ConstructorArgs用于指定查询结果映射到Java对象时使用的构造函数和构造函数参数。该注解可以用于XML文件中,通常与标签一起使用。使用@ConstructorArgs注解时,需要指定构造函数参数的映射关系。

示例:



  

  

    

    

  



(16)@Arg

作用:用于指定Java对象的构造方法参数。

@Arg用于指定查询结果映射到Java对象时构造函数或工厂方法的参数映射关系。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Arg注解时,需要指定参数的映射关系。

示例:

@Select("SELECT name, age FROM users WHERE id = #{id}")

User getUserById(@Arg("name") String name, @Arg("age") int age);

(17)@Discriminator

作用:用于指定类型鉴别器的查询结果。

@Discriminator用于在自动映射时指定不同子类型的映射方式。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Discriminator注解时,需要指定类型列的名称和不同子类型的映射方式。

示例:


@Select("SELECT * FROM vehicle WHERE type = #{type}")

@Discriminator(column = "type", javaType = String.class, cases = {

  @Case(value = "car", type = Car.class),

  @Case(value = "truck", type = Truck.class),

  @Case(value = "bus", type = Bus.class)

})


List getVehiclesByType(String type);

(18)@CacheNamespace

作用:用于指定缓存的命名空间。

@CacheNamespace用于指定Mapper接口中的查询结果是否进行缓存。该注解可以用于Mapper接口上,用于指定Mapper接口中所有方法默认的缓存配置。使用@CacheNamespace注解时,需要指定缓存配置的属性。

示例:

@CacheNamespace(

  implementation = MyBatisRedisCache.class,

  eviction = MyBatisRedisCache.Eviction.LRU,

  flushInterval = 60000,

  size = 10000,

  readWrite = true,

  blocking = true

)


public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(19)@Flush

作用:用于在插入、更新或删除操作之后自动清空缓存。

@Flush是用于在Mapper接口中指定在执行方法前或方法后刷新缓存。该注解可以用于Mapper接口方法上,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Flush注解时,需要指定刷新缓存的时机。

示例:

@Select("SELECT * FROM users WHERE id = #{id}")

@Flush(flushCache = FetchType.AFTER)

User getUserById(Long id);

(20)@MappedJdbcTypes

作用:用于指定Java对象属性与数据库列的映射关系。

@MappedJdbcTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedJdbcTypes注解时,需要指定Java类型和对应的JDBC类型。

示例:

public class User {

  private Long id;

  @MappedJdbcTypes(JdbcType.VARCHAR)

  private String name;

  private Integer age;

  // ...

}

(21)@MappedTypes

作用:用于指定Java对象与数据库类型的映射关系。

@MappedTypes用于将Java类型映射到JDBC类型。该注解可以用于JavaBean属性或ResultMap中,用于指定Java类型对应的JDBC类型。使用@MappedTypes注解时,需要指定Java类型。

示例:

@MappedTypes(User.class)

public interface UserMapper {

  @Select("SELECT * FROM users WHERE id = #{id}")

  User getUserById(Long id);

  // ...

}

(22)@SelectProvider

作用:用于指定动态生成SQL语句的提供者。

@SelectProvider是用于在Mapper接口中动态生成查询SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@SelectProvider注解时,需要指定Provider类和Provider方法。

示例:

@SelectProvider(type = UserSqlProvider.class, method = "getUserByIdSql")

User getUserById(Long id);

(23)@InsertProvider

作用:用于指定动态生成SQL语句的提供者。

@InsertProvider用于在Mapper接口中动态生成插入SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@InsertProvider注解时,需要指定Provider类和Provider方法。

示例:

@InsertProvider(type = UserSqlProvider.class, method = "insertUserSql")

int insertUser(User user);

(24)@UpdateProvider

作用:用于指定动态生成SQL语句的提供者。

@UpdateProvider用于在Mapper接口中动态生成更新SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@UpdateProvider注解时,需要指定Provider类和Provider方法。

示例:

@UpdateProvider(type = UserSqlProvider.class, method = "updateUserSql")

int updateUser(User user);

(25)@DeleteProvider

作用:用于指定动态生成SQL语句的提供者。

@DeleteProvider用于在Mapper接口中动态生成删除SQL语句。该注解可以用于Mapper接口方法上,用于指定一个提供SQL语句的Provider类。使用@DeleteProvider注解时,需要指定Provider类和Provider方法。

示例:

@DeleteProvider(type = UserSqlProvider.class, method = "deleteUserSql")

int deleteUser(Long id);

以上就是MyBatis的相关注解及使用示例了,实际开发中不一定每个都能用到,但是可以收藏起来,有备无患嘛!

总的来说,MyBatis的注解是一个非常强大的工具,它可以帮助你减少XML配置的工作量,让你的代码更加简洁、易读。但是,它也有一定的学习成本,你需要花一些时间去理解和掌握它。希望这篇文章能帮助你更好地理解和使用MyBatis的注解。

收起阅读 »

总是听说 Vue3 选择 Proxy 的原因是性能更好,不如直接上代码对比对比

web
逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.definePropert...
继续阅读 »

逛掘金的时候经常能刷到关于 Vue 响应式原理的文章, 经常能看到 Vue3 弃用 Object.defineProperty 转而使用 Proxy 来实现的原因是 Proxy 性能更好 。看的多了还能刷到一些文章认为 Object.defineProperty 性能更好,因此自己创建了一个小 demo 来对比二者在不同场景下的性能。



以下测试仅在 谷歌浏览器 中进行,不同浏览器内核不同,结果可能有差异。可以访问此 在线地址 测试其他环境下的性能。



封装响应式


本文不会详细解析基于 Object.definePropertyProxy 的封装代码,这些内容在多数文章中已有介绍。Vue3 对嵌套对象的响应式处理进行了优化,采用了一种惰性添加的方式,仅在对象被访问时才添加响应式。相比之下,Vue2 采用了一次性递归处理整个对象的方式添加响应式。为了确保比较的公平性,本文下面的 Object.defineProperty 代码也采用了相同的惰性添加策略。


Object.defineProperty


/** Object.defineProperty 深度监听 */
export function deepDefObserve(obj, week) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
let value = obj[key]

Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
if (
typeof value === "object" &&
value !== null &&
week &&
!week.has(value)
) {
week.set(value, true)
deepDefObserve(value)
}
return value
},
set(newValue) {
value = newValue
},
})
}
return obj
}

Proxy


/** Proxy 深度监听 */
export function deepProxy(obj, proxyWeek) {
const myProxy = new Proxy(obj, {
get(target, property) {
let res = Reflect.get(target, property)
if (
typeof res === "object" &&
res !== null &&
proxyWeek &&
!proxyWeek.has(res)
) {
proxyWeek.set(res, true)
return deepProxy(res)
}
return res
},
set(target, property, value) {
return Reflect.set(target, property, value)
},
})
return myProxy
}

测试性能


测试场景有五个:



  1. 使用两个 API 创建响应式对象的耗时,即 const obj = reactive({}) 的耗时

  2. 测量对已创建的响应式对象的属性进行访问的速度,即 obj.a 的读取时间。

  3. 测量修改响应式对象属性值的耗时,即执行 obj.a = 1 所需的时间。

  4. 创建多个响应式对象,并模拟访问和修改它们属性的操作,以评估在多对象场景下的性能表现。

  5. 针对嵌套对象进行响应式性能测试,以评估在复杂数据结构下的性能表现。


初始化性能


const _0_calling = {
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(data, keys[i], {
get() {},
set() {},
})
}
},
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = new Proxy(data, {
get() {},
set() {},
})
},
}

image.png


很明显,Proxy 的性能优于 Object.defineProperty


读取性能


const readDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const readProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _1_read = {
useObjectDefineProperty() {
readDefData.a
readDefData.b
readDefData.e
},
useProxy() {
readProxyData.a
readProxyData.b
readProxyData.e
},
}

image.png


Object.defineProperty 明显优于 Proxy


写入性能


const writeDefData = deepDefObserve({ a: 1, b: 1, c: 1, d: 1, e: 1 })
const writeProxyData = deepProxy({ a: 1, b: 1, c: 1, d: 1, e: 1 })
export const _2_write = {
count: 2,
useObjectDefineProperty() {
writeDefData.a = _2_write.count++
writeDefData.b = _2_write.count++
},
useProxy() {
writeProxyData.a = _2_write.count++
writeProxyData.b = _2_write.count++
},
}

image.png


Object.defineProperty 优于 Proxy,不过差距不大。


多次创建及读写


export const _4_create_read_write = {
count: 2,
useObjectDefineProperty() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
deepDefObserve(data)
data.a = _4_create_read_write.count++
data.b = _4_create_read_write.count++
data.a
data.c
},
proxyWeek: new WeakMap(),
useProxy() {
const data = { a: 1, b: 1, c: 1, d: 1, e: 1 }
const proxy = deepProxy(data, _4_create_read_write.proxyWeek)
proxy.a = _4_create_read_write.count++
proxy.b = _4_create_read_write.count++
proxy.a
proxy.c
},
}

image.png


Proxy 优势更大,但这个场景并不多见,很少会出现一次性创建大量响应式对象的情况,对属性的读写场景更多。


对嵌套对象的性能


对内部的每个属性都进行读或写操作


const deepProxyWeek = new WeakMap()
const defWeek = new WeakMap()
export const _5_deep_read_write = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
defWeek
),
useObjectDefineProperty() {
_5_deep_read_write.defData.res.code = _5_deep_read_write.count++
_5_deep_read_write.defData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.defData.res.message.error
_5_deep_read_write.defData.res.data[0].id
_5_deep_read_write.defData.res.data[0].name
_5_deep_read_write.defData.res.data[1].id
_5_deep_read_write.defData.res.data[1].name
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
deepProxyWeek
),
useProxy() {
_5_deep_read_write.proxyData.res.code = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.data[0].id = _5_deep_read_write.count++
_5_deep_read_write.proxyData.res.message.error
_5_deep_read_write.proxyData.res.data[0].id
_5_deep_read_write.proxyData.res.data[0].name
_5_deep_read_write.proxyData.res.data[1].id
_5_deep_read_write.proxyData.res.data[1].name
},
}

image.png


Object.defineProperty 会稍好一些,但两者的差距不大。


只读取修改嵌套对象的浅层属性


const _6_deepProxyWeek = new WeakMap()
const _6_defWeek = new WeakMap()
export const _6_update_top_level = {
count: 2,
defData: deepDefObserve(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_deepProxyWeek
),
useObjectDefineProperty() {
_6_update_top_level.defData.res.code = _6_update_top_level.count++
_6_update_top_level.defData.res.message.error
},
proxyData: deepProxy(
{
res: {
code: 200,
message: {
error: null,
},
data: [
{
id: 1,
name: "1",
},
{
id: 2,
name: "2",
},
],
},
},
_6_defWeek
),
useProxy() {
_6_update_top_level.proxyData.res.code = _6_update_top_level.count++
_6_update_top_level.proxyData.res.message.error
},
}

image.png


这个场景 Proxy 略优于 Object.defineProperty


总结


Proxy 在对象创建时的性能明显优于Object.defineProperty。而在浅层对象的读写性能方面,Object.defineProperty 表现更好。但是当对象的嵌套深度增加时,Object.defineProperty 的优势会逐渐减弱。尽管在性能测试中,Object.defineProperty 的读写优势可能更适合实际开发场景,但在 谷歌浏览器 中,Proxy 的性能与 Object.defineProperty 并没有拉开太大差距。因此,Vue3 选择 Proxy 不仅仅基于性能考量,还因为 Proxy 提供了更为友好、现代且强大的 API ,使得操作更加灵活。


作者:clench
来源:juejin.cn/post/7324141201802821672
收起阅读 »

封装v-loading指令 从此释放双手

web
封装v-loading指令 从此释放双手 前言 ​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以...
继续阅读 »

封装v-loading指令 从此释放双手


前言


​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以拍个照片发朋友圈装逼.


​ 但是想象很美好, 坐着地铁到了自习室, 发现大家伙都是在非常安静, 唯一能出声音的就是翻书或者写字的声音. 为了不影响其他人故意挑一个靠窗户的位置,接着对着电脑开始疯狂进攻. 我的键盘声很快传遍了整见屋子. 虽然别人没有说什么, 但是自己觉得好像是故意来捣乱的, 今天的键盘声音显得格外的大声. 没多久我知趣的溜溜球. 美团体验卷直接gg, 哈哈哈, 问题不大~


​ 言归正传, 今天给大家分享的是利用 VUE3 实现v-loading的加载效果 , 先看一下实现效果吧~


2023-09-24-00-48-48.gif


这一类效果在使用组件库, 例如饿了么中出现的频率很高, 使用方法也很简单, 给对应的结构添加上


v-loading="布尔值"即可, 是不是很好奇是怎么实现的? 那么就和旋风冲锋小瑜开始冲!


实现思路



  • loading肯定也是一个组件, 其中包含加载效果还有提示文字, 并且使用的时候可以去修改文字以及开启或者关闭加载动画

  • 实现的周期是在 异步开始前, 开启loading, 在异步处理[数据加载]完成后 关闭loading

  • 既然是在模版中通过 v-xxx来实现的, 那么肯定就是一个自定义指令, Vue提供指令, 也就是去操作DOM[组件实例]


那么按照以上的实现思路, 一步一步去完成, 首先搭设一个Demo的模版结构和样式


搭设基本模版


利用Vue3搭设demo架子, 头部tab栏, 切换路由 , main区域的显示内容


App.vue


<script setup lang="ts"></script>

<template>
<div class="container">
// Tab栏
<Tabs></Tabs>
// 一级路由出口
<router-view></router-view>
</div>
</template>


<style lang="scss">
.container {
width: 100vw;
height: 100vh;
background-color: #1e1e1e;
}
</style>


router路由


 routes: [
{
path: '/',
redirect: '/huawei'
},
{
path: '/huawei',
component: () => import('@/views/Huawei/index.vue'),
meta: {
title: '华为'
}
},
{
path: '/rongyao',
component: () => import('@/views/Rongyao/index.vue'),
meta: {
title: '荣耀'
}
},
{
path: '/xiaomi',
component: () => import('@/views/Xiaomi/index.vue'),
meta: {
title: '小米'
}
},
{
path: '/oppo',
component: () => import('@/views/Oppo/index.vue'),
meta: {
title: 'oppo'
}
}
]

Tabs组件


<script setup lang="ts">
import { ref } from 'vue'
const tabList = ref([
{ id: 1, text: '华为', path: '/huawei' },
{ id: 2, text: '荣耀', path: '/rongyao' },
{ id: 3, text: '小米', path: '/xiaomi' },
{ id: 4, text: 'oppo', path: '/oppo' }
])
const activeIndex = ref(0)
</script>

<template>
<div class="tabs-box">
<router-link
class="tab-item"
:to="item.path"
v-for="(item, index) in tabList"
:key="item.id"
>

<span
class="tab-link"
:class="{ active: activeIndex === index }"
@click="activeIndex = index"
>

{{ item.text }}
</span>
</router-link>
</div>
</template>


<style lang="scss" scoped>
.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
.tab-item {
padding: 10px;
&.router-link-active {
.tab-link {
transition: border 0.3s;
color: gold;
padding-bottom: 5px;
border-bottom: 2px solid gold;
&.active {
border-bottom: 2px solid gold;
color: gold;
}
}
}
}
}
</style>


按照路由去创建4个文件夹,这里按照huawei做举例


<script setup lang="ts">
const src = ref('')
</script>

<template>
<div class="box" >
<div class="img-box">
<img :src="src" alt="" />
</div>
</div>
</template>


创建Loading组件


首先按照最直接的方式, 利用 v-if 以及组件通讯, 实现组件方式的实现


注意 这里是通过 position: absolute; 通过定位的方式进行垂直水平居中, 先埋下伏笔


<script setup lang="ts">

defineProps({
title: {
type: String,
default: '正在加载中...'
}
})

</script>

<template>
<div class="loading-box">
<div class="loading-content">
// loading 动图
<img src="./loading.gif" />
// 底部提示文字
<p class="desc">{{ title }}</p>
</div>
</div>
</template>


<style lang="scss" scoped>
.loading-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
.loading-content {
text-align: center;
img {
width: 35px !important;
height: 35px !important;
}
.desc {
line-height: 20px;
font-size: 12px;
color: #fff;
position: relative;
}
}
}
</style>


在对应组件中使用Loading组件, 利用延时器模拟异步操作


<script>
const src = ref('')
const title = ref('华为加载中...')
const showLoading = ref(true) // 控制loading的显示和隐藏

onMounted(() => {
showLoading.value = true
// 模拟异步请求
window.setTimeout(() => {
src.value =
'https://ms.bdimg.com/pacific/0/pic/-1284887113_-1109246585.jpg?x=0&y=0&h=340&w=510&vh=340.00&vw=510.00&oh=340.00&ow=510.00'
showLoading.value = false
}, 1000)
})
</script>

<template>
<div class="box">
<div class="img-box" v-if="!showLoading">
<img :src="src" alt="" />
</div>
</div>
<Loading v-if="showLoading"></Loading>
</template>


效果一样可以出来, 接下来就利用指令的方式来优化


011.png


V-loading 指令实现


思路:



  • 在dom挂载完成后, 创建Loading实例, 需要挂载到写在具体指令结构上

  • loading需要知道传递的显示文字, 这里通过指令动态的参数传递

  • 当loading组件参数更新后卸载, 关闭loading


1. 使用自定义指令的参数


和内置指令类似,自定义指令的参数也可以是动态的, 下面是Vue官网的截图


动态指令参数.png


在模版中使用 v-loading:[title]="showLoading"


const title = ref('华为加载中...')

<template>
<div class="box" v-loading:[title]="showLoading">
...
</div>

<!-- <Loading v-if="showLoading"></Loading> -->
</template>

2. 利用插件注册指令


在Loading文件下创建js文件


import Loading from './index.vue' // 导入.vue文件
const loadingDirective = {
}
export default loadingDirective

components下创建index.js文件


import loading from '@/components/Loading/index'
export default {
install: (app: App) => {
app.directive('loading', loading)
}

入口文件中注册插件


import MyLoading from '@/components/index'
app.use(MyLoading)

3. 指令 - 节点都挂载完成后调用



  • createApp 作用: 创建一个应用实例 - 创建loading

  • app.mount 作用: 将应用实例挂载在一个容器元素中

  • mounted 参数 el=>获取dom

  • mounted 参数 binding.value => 控制开启和关闭loding 也就是 showLoading

  • mounted 参数 binding.arg => loading显示的文字 [例如华为加载中...]


const loadingDirective = {
/* 节点都挂载完成后调用 */
mounted(el: any, binding: DirectiveBinding) {
/*
value 控制开启和关闭loding
arg loading显示的文字
*/

const { value, arg } = binding
/* 创建loading实例,并挂载 */
const app = createApp(Loading)
// 这一步 instance === loading.vue
// 此时就可以视同loading.vue 也就是组件实例的方法和属性
const instance = app.mount(document.createElement('div'))
/* 为了让elAppend获取到创建的div元素 */
el.instance = instance
/* 如果传入了自定义的文字就添加title */
if (arg) {
instance.setTitle(arg)
}
/* 如果showLoading为true将loading实例挂载到指令元素中 */
if (value) {
// 添加方法方法, 看下文
handleAppend(el)
}
},
}

可以从控制台查看binding中的title以及showLoading的值


02.png


instance.setTitle(arg) 这里既然使用到了组件实例的setTitle方法, 就需要在loading中对应的方法


注意: 在vue3中需要利用defineExpose抛出事件, 让外界可以访问或使用


Loading.vue


const title = ref('')
const setTitle = (val: string) => {
title.value = val
}
// defineProps({ 组件通讯就使用不到了, 注释即可
// title: {
// type: String,
// default: '正在加载中...'
// }
// })

defineExpose({
setTitle,
title
})

<template>
<div class="loading-box">
<div class="loading-content">
<img src="./loading.gif" />
<p class="desc">{{ title }}</p>
</div>
</div>

</template>

4. 指令 - handleAppend(el)方法实现


/* 将loading添加到指令所在DOM */
const handleAppend = (el: any) => {
console.log(el.instance.$el, 'el.instance.$el')
el.appendChild(el.instance.$el)
}

04.png


5. 指令 - updated() 更新后挂载还是消除的逻辑


在第四步中, loading已经可以通过指令显示了, 此时还需要让showLoading为false的时候, 或者这么理解: 当新的值不等于老值的是够关闭loading


此时就可以利用指令中updated钩子去执行这一段关闭的逻辑, 一下是官网的说明


05.png


  /* 更新后调用 */
updated(el: any, binding: DirectiveBinding) {
const { value, oldValue, arg } = binding
if (value !== oldValue) {
/* 更新标题 */
if (arg) {
el.instance.setTitle(arg)
}
// 是显示吗? 如果是就添加 : 如果不是就删除
value ? handleAppend(el) : c(el)
}
}

6. 指令 - handleRemove()方法实现


/* 将loading在DOM中移除 */
const handleRemove = (el: any) => {
removeClass(el, relative as any)
el.removeChild(el.instance.$el)
}

此时基本已经完成了需求, 但是上文我提到了坑点, 原因是loading是通过绝对定位的方式进行水平居中, 那么比如我要在图片中显示loading呢? 我们来实现下这个坑点


7. 坑点的说明


<template>
<div class="box" //关闭 v-loading:[title]="showLoading">
<div class="img-box" v-loading:[title]="showLoading">
<img :src="src" alt="" />
</div>
</div>

<!-- <Loading v-if="true"></Loading> -->
</template>

06.png


很明显发现, 执行现在图片这个盒子上, 并没有水平居中, 审查元素其实也很明显, css样式中是根据子绝父相, 但是此时大盒子并没有提供相对定位, 自然就无法水平居中


那么如何修改呢? 其实只要给绑定指令的盒子添加position: relative;属性即可, 当然absolute或者fixed效果一样可以居中


问题已找到了, 那么在appendChild时判断当前是否存在relative | absolute | fixed 的其中一个, 如果没有就需要classList.add进行添加, 同时在removeChild删除添加的relative | absolute | fixed 即可


8. 完善坑点, 实现水平居中


getComputedStyle() 在MDN上的说明:


方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性的值。私有的 CSS 属性值可以通过对象提供的 API 或通过简单地使用 CSS 属性名称进行索引来访问。


/* 将loading添加到指令所在DOM */
const relative = 'relative'
const handleAppend = (el: any) => {
const style = getComputedStyle(el)
if (!['absolute', 'relative', 'fixed'].includes(style.position)) {
addClass(el, relative as any)
}

el.appendChild(el.instance.$el)
}
// 添加relative
const addClass = (el: any, className: string) => {
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
// 删除relative
const removeClass = (el: any, className: string) => {
el.classList.remove(className)
}

结尾


夜深人静又是卷到凌晨1点, 只管努力, 其他交给天意~ 旋风冲锋手动撒花 ✿✿ヽ(°▽°)ノ✿


demo地址: gitee.com/tcwty123/v-…


作者:不知名小瑜
来源:juejin.cn/post/7281825352530296843
收起阅读 »

前端对接电子秤、扫码枪设备serialPort 串口使用教程

web
因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。 Serialport 官网地址:serialport.io/ Github:g...
继续阅读 »

因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。



Serialport


官网地址:serialport.io/


Github:github.com/serialport/…


官方描述:使用 JavaScript 访问串行端口。Linux、OSX 和 Windows。



SerialPort是什么?



SerialPort 是一个用于在 Node.js 环境中进行串口通信的库。它允许开发者通过 JavaScript 或 TypeScript 代码与计算机上的串口设备进行交互。SerialPort 库提供了丰富的 API,使得在串口通信中能够方便地进行设置、监听和发送数据。



一般我们的设备(电子秤/扫码枪)会有一根线插入到电脑的USB口或者其他口,电脑上的这些插口就是叫串口。设备上的数据会通过这根线传输到电脑里面,比如电子秤传到电脑里的就是重量数值。那么我们前端怎么接收解析到这些数据的呢?SerialPort的作用就是用来帮我们接收设备传输过来的数据,也可以向设备发送数据。


简单概括一下:SerialPort就是我们前端和设备之间的翻译官,可以接收设备传输过来的数据,也可以向设备发送数据。


SerialPort怎么用?


SerialPort可以在Node项目中使用,也可以在Electron项目中使用,我们一般都是用在Electron项目中,接下来讲一下在Electron项目中SerialPort怎么下载和引入


1、创建Electron项目


mkdir my-electron-app && cd my-electron-app
npm init -y
npm i --save-dev electron

网上有很多Electron教程,这里不再详细说了


在package.json中看一下自己的Electron的版本,下一步会用到


2、下载SerialPort


这里先看一下自己使用的Electron对应的Node版本是什么,打开下面electron官网看表格中的Node那一列


Electron发行时间表:http://www.electronjs.org/zh/docs/lat…


image-20240113215800193.png


如果你Electron对应的Node版本高于v12.0.0,直接下载就行


npm install serialport

如果你Electron对应的Node版本低于或等于v12.0.0,请用对应的Node版本对应下面的serialport版本下载



serialport.io/docs/next/g…




  • 对于 Node.js 版本0.100.12,最后一个正常运行的版本是serialport@4

  • 对于 Node.js 版本4.0,最后一个正常运行的版本是serialport@6.

  • 对于 Node.js 版本8.0,最后一个正常运行的版本是serialport@8.

  • 对于 Node.js 版本10.0,最后一个正常运行的版本是serialport@9.

  • 对于 Node.js 版本12.0,最后一个正常运行的版本是serialport@10.



我项目的Electron版本是11.5.0,对应的Node版本号是12.0,对应的serialport版本号是serialport@10.0.0



3、编译Serialport



  • 安装node-gyp 用于调用其他语言编写的程序(如果已安装过请忽略这一步)


    npm install -g node-gyp




  • 进入@serialport目录


    cd ./node_modules/@serialport/bindings


  • 进行编译,target后面换成当前Electron的版本号


    node-gyp rebuild --target=11.5.0



如果编译的时候报错了就将自己电脑的Node版本切换成当前Electron对应的版本号再编译一次


查看Electron对应Node版本号:http://www.electronjs.org/zh/docs/lat…


编译成功以后就可以在代码里使用Serialport了


4、使用Serialport



serialport官网使用教程:serialport.io/docs/next/g…



4.1、引入Serialport


const { SerialPort } = require('serialport')
// or
import { SerialPort } from 'serialport'

4.2、创建串口(重点!)


创建串口有两种写法,新版本是这样写法new SerialPort(params, callback)


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

旧版本是下面这样的写法new Serialport(path, params, callback),我用的是serialport@10.0.0版本就是这样的写法


const port = new Serialport('COM1', {
 baudRate: 9600,
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

创建串口的时候需要传入两个重要的参数是path和baudRate,path是串口号,baudRate是波特率。最后一个参数是回调函数



不知道怎么查看串口号和波特率看这篇文章


如何查看串口号和波特率?



4.3、手动打开串口


如果autoOpen参数是false,需要使用port.open()方法手动打开


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: false,  // 是否自动打开端口, 默认true
})
// autoOpen参数是false,需要使用port.open()方法手动打开
port.open(function (err) {
 if (err) {
   return console.log('打开失败', err.message)
}
 console.log('打开成功')
})

4.4、接收数据(重点!)


接收到的data是一个Buffer,需要转换为字符串进行查看


port.on('data', function (data) {
 // 接收到的data是一个Buffer,需要转换为字符串进行查看
 console.log('Data:', data.toString('utf-8'))
})

接收过来的data就是设备传输过来的数据,转换后的字符串就是我们需要的数据,字符串里面可能有多个数据,我们把自己需要的数据截取出来就可以了


假设通过电子秤设备获取到的数据就是"205 000 000",中间是四个空格分割的,第一个数字205就是获取的重量,需要把这个重量截取出来。下面是我的示例代码


port.on('data', function (data) {
 try {
     // 获取的data是一个Buffer
     // 1.将 Buffer 转换为字符串 dataString.toString('utf-8')
     let weight = data.toString('utf-8')
     // 2.将字符串分割转换成数组,取数组的第一个值.split('   ')[0]
     weight = weight.split('   ')[0]
     // 3.将取的值 去掉前后空格
     weight = weight.trim()
     // 4.最后转换成数字,获取到的数字就是重量
     weight = Number(weight)
     console.log('获取到重量:'+ weight);
} catch (err) {
   console.error(`
     重量获取报错:${err}
     获取到的Buffer: ${data}
     Buffer转换后的值:${data.toString('utf-8')}
   `
);
}
})

4.5、写入数据


port.write('Hi Mom!')
port.write(Buffer.from('Hi Mom!'))

4.6、实时获取(监听)所有串口


const { SerialPort } = require('serialport')

SerialPort.list().then((ports, err) => {
   // 串口列表
   console.log('获取所有串口列表', ports);
})

更多内容


serialport官网教程:serialport.io/docs/next/g…


作者:Yaoqi
来源:juejin.cn/post/7323464381172301860
收起阅读 »

flex布局之美,以后就靠它来布局了

web
写在前面 在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的align 和valign可以实现水平垂直居中等 再后来,由于CSS 不断完善,便演变出了:标准文档流、浮动布局和定位布局 3种布局 来实现水平垂直居中等各种布局...
继续阅读 »

写在前面


在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的alignvalign可以实现水平垂直居中等


再后来,由于CSS 不断完善,便演变出了:标准文档流浮动布局定位布局 3种布局 来实现水平垂直居中等各种布局需求。


下面我们来看看实现如下效果,各种布局是怎么完成的


image-20240114134424060


实现这样的布局方式很多,为了方便演示效果,我们在html代码种添加一个父元素,一个子元素,css样式种添加一个公共样式来设置盒子大小,背景颜色


<div class="parent">
  <div class="child">我是子元素</div>
</div>

/* css公共样式代码 */
.parent{
   background-color: orange;
   width: 300px;
   height: 300px;
}
.child{
   background-color: lightcoral;
   width: 100px;
   height: 100px;
}

①absolute + 负margin 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;;
   top: 50%;
   left: 50%;
   margin-left: -50px;
   margin-top: -50px;
}

②absolute + transform 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
}

③ flex实现


.parent {
   display: flex;
   justify-content: center;
   align-items: center;
}

通过上面三种实现来看,我们应该可以发现flex 布局是最简单了吧。


对于一个后端开发人员来说,flex布局算是最友好的了,因为它操作简单方便


一、flex 布局简介



flex 全称是flexible Box,意为弹性布局 ,用来为盒状模型提供布局,任何容器都可以指定为flex布局。


通过给父盒子添加flex属性即可开启弹性布局,来控制子盒子的位置和排列方式。


父容器可以统一设置子容器的排列方式,子容器也可以单独设置自身的排列方式,如果两者同时设置,以子容器的设置为准



flex布局


二、flex基本概念



flex的核心概念是 容器,容器包括外层的 父容器 和内层的 子容器,轴包括 主轴辅轴



<div class="parent">
   <div class="child">我是子元素</div>
</div>

2.1 轴



  • 在 flex 布局中,是分为主轴和侧轴两个方向,同样的叫法有 : 行和列、x 轴和y 轴,主轴和交叉轴

  • 默认主轴方向就是 x 轴方向,水平向右

  • 默认侧轴方向就是 y 轴方向,水平向下


    主轴和侧轴



注:主轴和侧轴是会变化的,就看 flex-direction 设置谁为主轴,剩下的就是侧轴。而我们的子元素是跟着主轴来排列的


--flex-direction 值--含义
row默认值,表示主轴从左到右
row-reverse表示主轴从右到左
column表示主轴从上到下
column-reverse表示主轴从下到上

2.2 容器



容器的属性可以作用于父容器(container)或者子容器(item)上



①父容器(container)-->属性添加在父容器上



  • flex-direction 设置主轴的方向

  • justify-content 设置主轴上的子元素排列方式

  • flex-wrap 设置是否换行

  • align-items 设置侧轴上的子元素排列方式(单行 )

  • align-content 设置侧轴上的子元素的排列方式(多行)


②子容器(item)-->属性添加在子容器上



  • flex 属性 定义子项目分配剩余空间,用flex来表示占多少份数

  • align-self控制子项自己在侧轴上的排列方式

  • order 属性定义项目的排列顺序


三、主轴侧轴设置


3.1 flex-direction: row



flex-direction: row 为默认属性,主轴沿着水平方向向右,元素从左向右排列。



row


3.2 flex-direction: row-reverse



主轴沿着水平方向向左,子元素从右向左排列



row-reverse


3.3 flex-direction: column



主轴垂直向下,元素从上向下排列



column


3.4 flex-direction: column-reverse



主轴垂直向下,元素从下向上排列



column-reverse


四、父容器常见属性设置


4.1 主轴上子元素排列方式


4.1.1 justify-content


justify-content 属性用于定义主轴上子元素排列方式


justify-content: flex-start|flex-end|center|space-between|space-around



flex-start:起始端对齐


flex-start


flex-end:末尾段对齐


flex-end


center:居中对齐


center


space-around:子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。


space-around


space-between:子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。


space-between


4.2 侧轴上子元素排列方式


4.2.1 align-items 单行子元素排列


这里我们就以默认的x轴作为主轴



align-items:flex-start:起始端对齐


flex-start


align-items:flex-end:末尾段对齐


flex-end


align-items:center:居中对齐


center


align-items:stretch 侧轴拉伸对齐



如果设置子元素大小后不生效



stretch


4.2.2 align-content 多行子元素排列


设置子项在侧轴上的排列方式 并且只能用于子项出现 换行 的情况(多行),在单行下是没有效果的


我们需要在父容器中添加 flex-wrap: wrap;


flex-wrap: wrap; 是啥意思了,具体会在下一小节中细说,就是当所有子容器的宽度超过父元素时,换行显示



align-content: flex-start 起始端对齐


 /* 父容器添加如下代码 */
display: flex;
align-content: flex-start;
flex-wrap: wrap;

align-content: flex-start


align-content: flex-end :末端对齐


/* 父容器添加如下代码 */
display: flex;
align-content: flex-end;
flex-wrap: wrap;

align-content: flex-end


align-content: center: 中间对齐


/* 父容器添加如下代码 */
display: flex;
align-content: center;
flex-wrap: wrap;

align-content: center


align-content: space-around: 子容器沿侧轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半


/* 父容器添加如下代码 */
display: flex;
align-content: space-around;
flex-wrap: wrap;

align-content: space-around


align-content: space-between:子容器沿侧轴均匀分布,位于首尾两端的子容器与父容器相切。


/* 父容器添加如下代码 */
display: flex;
align-content: space-between;
flex-wrap: wrap;

image-20240114171606954


align-content: stretch: 子容器高度平分父容器高度


/* 父容器添加如下代码 */
display: flex;
align-content: stretch;
flex-wrap: wrap;

align-content: stretch


4.3 设置是否换行



默认情况下,项目都排在一条线(又称”轴线”)上。flex-wrap属性定义,flex布局中默认是不换行的。



flex-wrap: nowrap :不换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: nowrap;

flex-wrap: nowrap


flex-wrap: wrap: 换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: wrap;

flex-wrap: wrap


4.4 align-content 和align-items区别



  • align-items 适用于单行情况下, 只有上对齐、下对齐、居中和 拉伸

  • align-content适应于换行(多行)的情况下(单行情况下无效), 可以设置 上对齐、下对齐、居中、拉伸以及平均分配剩余空间等属性值。

  • 总结就是单行找align-items 多行找 align-content


五、子容器常见属性设置



  • flex子项目占的份数

  • align-self控制子项自己在侧轴的排列方式

  • order属性定义子项的排列顺序(前后顺序)


5.1 flex 属性



flex 属性定义子项目分配剩余空间,用flex来表示占多少份数。



① 语法


.item {
   flex: <number>; /* 默认值 0 */
}

②将1号、3号子元素宽度设置成80px,其余空间分给2号子元素


flex:1


5.2 align-self 属性



align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。


默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。



align-self: flex-start 起始端对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-start;

align-self: flex-start


align-self: flex-end 末尾段对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-end;

align-self: flex-end


align-self: center 居中对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素*/
align-self: center;

align-self: center


align-self: stretch 拉伸对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素 未指定高度才生效*/
align-self: stretch;

align-self: stretch


5.3 order 属性



数值越小,排列越靠前,默认为0。



① 语法:


.item {
   order: <number>;
}

② 既然默认是0,那我们将第二个子容器order:-1,那第二个元素就跑到最前面了


/* 父容器添加如下代码 */
display: flex;
/*第二个子元素*/
order: -1;

order


六、小案例


最后我们用flex布局实现下面常见的商品列表布局


商品列表


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>简单商品布局</title>
   <style>
       .goods{
           display: flex;
           justify-content: center;
      }
       p{
           text-align: center;
      }
       span{
           margin: 0;
           color: red;
           font-weight: bold;
      }
       .goods001{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods002{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods003{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods004{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }

   
</style>
</head>
<body>

   <div class="goods">
       <div class="goods001">
           <img src="./imgs/goods001.jpg" >
           <p>松下(Panasonic)洗衣机滚筒</p>
           <span>¥3899.00</span>
       </div>
       <div class="goods002">
           <img src="./imgs/goods002.jpg" >
           <p>官方原装浴霸灯泡</p>
           <span>¥17.00</span>
       </div>
       <div class="goods003">
           <img src="./imgs/goods003.jpg" >
           <p>全自动变频滚筒超薄洗衣机</p>
           <span>¥1099.00</span>
       </div>
       <div class="goods004">
           <img src="./imgs/goods004.jpg" >
           <p>绿联 车载充电器</p>
           <span>¥28.90</span>
       </div>
   </div>
</body>
</html>

以上就是本期内容的全部,希望对你有所帮助。我们下期再见 (●'◡'●)


作者:xiezhr
来源:juejin.cn/post/7323539673346375719
收起阅读 »

面试官:你之前的工作发布过npm包吗?

web
背景🌟 我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~ 01、步骤一注册 打开npm官网,如果没有账号就注册账号...
继续阅读 »

背景🌟


我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~


01、步骤一注册



打开npm官网,如果没有账号就注册账号,如果有就登陆。



02、步骤二创建文件夹



按需求创建一个文件夹,本文以test为例。



03、步骤三初始化package.json文件



进入test文件夹里面,使用cmd打开命令行窗口,在命令行窗口里面输入npm init初始化package.json文件。也可以在Visual Studio Coode的终端里面使用npm init命令初始化。



04、步骤四初始化package.json文件的过程



创建package.json的步骤


01、package name: 设置包名,也就是下载时所使用的的命令,设置需谨慎。


02、version: 设置版本号,如果不设置那就默认版本号。


03、description: 包描述,就是对这个包的概括。


04、entry point: 设置入口文件,如果不设置会默认为index.js文件。


05、test command: 设置测试指令,默认值就是一句不能执行的话,可不设置。


06、git repository: 设置或创建git管理库。


07、keywords: 设置关键字,也可以不设置。


08、author: 设置作者名称,可不设置。


09、license: 备案号,可以不设置。


10、回车即可生成package.json文件,然后还有一行需要输入yes命令就推出窗口。


11、测试package.json文件是否创建成功的命令npm install -g。



05、步骤五创建index.js文件



test文件夹根目录下创建index.js文件,接着就是编写index.js文件了,此处不作详细叙述。



06、步骤六初始化package-lock.json文件



test根目录下使用npm link命令创建package-lock.json文件。



07、步骤七登录npm账号



使用npm login链接npm官网账号,此过程需要输入Username、Password和Email,需要提前准备好。连接成功会输出Logged in as [Username] on registry.npmjs.org/ 这句话,账号不同,输出会有不同。



08、步骤八发布包到npm服务器



执行npm publish命令发布包即可。



09、步骤九下载安装



下载安装使用包,此例的下载命令是npm install mj-calculation --save



10、步骤十更新包



更新包的命npm version patch,更新成功会输出版本号,版本号会自动加一,此更新只针对本地而言。



11、步骤十一发布包到npm服务器



更新包至npm服务器的命令npm publish,成功会输出版本,npm服务器的版本也会更新。



12、步骤十二删除指定版本



删除指定版本npm unpublish mj-calculation@1.0.2,成功会输出删除的版本号,对应服务器也会删除。



13、步骤十三删除包



撤销已发布的包npm unpublish mj-calculation使用的命令。



14、步骤十四强制删除包



强制撤销已发布的包npm unpublish mj-calculation --force使用的命令。



作者:泽南Zn
来源:juejin.cn/post/7287425222365364259
收起阅读 »

面试被问到一个css属性,我却只会向面试官输出js解决方案。。。

web
事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一...
继续阅读 »

事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一些操作按钮,但是按钮在该区域下方,不能显示出来,怎么能够点击到按钮呢?


听到问题我一开始没反应过来,不是还在疯狂问css吗,怎么突然跳跃到canvas了?于是我就回:不好意思,canvas我不是特别熟。。。,面试官就说不关canvas的事,这样吧,你就把canvas想象成一张普通的图片,怎么能点击到图片下方的按钮呢?


思索片刻,难道面试官在考察我的JS基础了?我就很自信的回答:首先把按钮所在标签放到图片所在标签之下,再把图片的层级(z-index)设置比按钮高一点,这样按钮就被图片挡着不会显示出来,再给按钮和图片所在标签都加上点击事件,此时就可以通过事件冒泡处理按钮的事件了。说完我就非常有把握的看向面试官,以为稳了。但是面试官好像不太满意,于是添加条件说,那如果按钮和图片所在标签不是父子节点关系呢,没有这层关系,你就不能使用事件冒泡了,此时怎么处理?答曰:不知道。。。


回来后赶紧查资料,原来一个css属性就搞定了:pointer-events: none; 这才是面试官想要的答案。


pointer-events是一个CSS属性,它定义了在何种情况下元素可以成为鼠标事件(或触摸事件)的目标。这个属性可以控制元素是否可以被点击、是否可以触发鼠标事件,或者是否应该忽略鼠标事件,让事件传递给下面的元素


使用场景


pointer-events属性主要用于以下几种场景:



  • : 元素不会成为鼠标事件的目标。例如,如果想让一个元素透明对用户的点击,可以将其pointer-events设置为none

  • Auto: 默认值。元素正常响应鼠标事件。

  • VisiblePainted: 元素仅在可见部分响应鼠标事件。

  • 其他值: 还有一些其他值用于SVG元素,如visibleFillvisibleStrokepainted, 等。


示例




以下例子和上述试题很像,把mask当做一张图片,为了方便展示,为其设置了透明度,这样能看到具体按钮位置和展示层级关系。正常情况下点击按钮是不会触发click事件的,因为mask的层级更高,完全遮住了按钮,鼠标只会点击到mask,但若此时为其加上pointer-events: none属性,点击事件会“穿透”该元素并可触发下面元素的事件,即按钮点击事件就可以被触发了!!!


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.outer{
position: relative;
width: 200px;
height: 200px;
}
.mask{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, .7);
pointer-events: none; /* 重要 */
}
</style>
<body>
<div class="outer">
<div class="mask"></div>
<button id="btn">click</button>
</div>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', function(e){
console.log('click');
})
</script>
</body>
</html>

image.png


作者:自驱
来源:juejin.cn/post/7320169906221826048
收起阅读 »

不要再滥用可选链运算符(?.)啦!

web
前言 之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。 可选链运算符(?.),大家都很熟悉了,直接看个例子: const result = obj?.a?...
继续阅读 »

前言


之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。


可选链运算符(?.),大家都很熟悉了,直接看个例子:


const result = obj?.a?.b?.c?.d

很简单例子,上面代码?前面的属性如果是空值(null或undefined),则result值是undefined,反之如果都不是空值,则会返回最后一个d属性值。


本文不是讲解这种语法的用法,主要是想分析下日常开发中,这种语法 滥用、乱用 的问题。


滥用、乱用


最近在code review一个公司项目代码,发现代码里用到的可选链运算符,很多滥用,用的很无脑,经常遇到这种代码:


const userName = data?.items?.[0]?.user?.name

↑ 不管对象以及属性有没有可能是空值,无脑加上?.就完了。


// react class component
const name = this.state?.name

// react hooks
const [items, setItems] = useState([])
items?.map(...)
setItems?.([]) // 真有这么写的

↑ React框架下,this.state 值不可能是空值,初始化以及set的值都是数组,都无脑加上?.


const item1 = obj?.item1
console.log(item1.name)

↑ 第一行代码说明obj或item1可能是空值,但第二行也明显说明不可能是空值,否则依然会抛错,第一行的?.也就没意义了。


if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2
const name = obj?.item1?.item2?.name
}

↑ if 里已经判断了非空了,内部就没必要判断非空了。


问题、缺点


如果不考虑 ?. 使用的必要性,无脑滥用其实也没问题,不会影响功能,优点也很多:



  1. 不用考虑是不是非空,每个变量或属性后面加 ?. 就完了。

  2. 由于不用思考,开发效率高。

  3. 不会有空引用错误,不会有页面点点就没反应或弹错问题。


但是问题和缺点也很明显,而且也会很严重。分两点分析下:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


1. 可读性、维护性


可读性和维护性其实是一回事,都是指不是源代码作者的开发维护人员,在捋这块代码逻辑、修改bug等情况时,处理问题的效率,代码写的好处理就快,写的烂就处理慢,很简单道理。


const onClick = () => {
const user = props.data?.items?.[0]?.user
if (user) {
// use user to do something
}
}

已这行代码为例,有个bug现象是点击按钮没反应,维护开发看到这块代码,就会想这一串链式属性里,是不是有可能有空值,所以导致了user是空值,没走进if里导致没反应。然后就继续分析上层组件props传输代码,看data值从哪儿传来的,看是不是哪块代码导致data或items空值了。。。


其实呢?从外部传过来的这一串属性里不会有空值的情况,导致bug问题根本不在这儿。


const user = props.data.items[0].user

那把?.都去掉呢?维护开发追踪问题看到这行代码,data items 这些属性肯定不能是空值,不然console就抛错了,但是bug现象里并没有抛错,所以只需要检查user能不能是空值就行了,很容易就排除了很多情况。


总结就是:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。


2. 隐式过滤了异常


api.get(...).then(result => {
const id = result?.id
// use id to do something
})

比如有个需求,从后台api获取数据时,需要把结果里id属性获取到,然后进行数据处理,从业务流程上看,这个api返回的result以及id必须有值,如果没值的话后续的流程就会走不通。


然后后台逻辑由于写的有问题,导致个别情况返回的 result=null,但是由于前端这里加了?.,导致页面没有任何反应,js不抛错,console也没有log,后续流程出错了,这时候如果想找原因就会很困难,对代码熟悉还行,如果不是自己写的就只能看代码捋逻辑,如果是生产环境压缩混淆了就更难排查了。


api.get(...).then(result => {
const id = result.id
// use id to do something
})

?.去掉呢?如果api返回值有问题,这里会立即抛错,后面的流程也就不能进行下去了,无论开发还是生产环境都能在console里快速定位问题,即使是压缩混淆的也能从error看出一二,或者在一些前端监控程序里也能监听到。


其实这种现象跟 try catch 里不加 throw 类似,把隐式异常错误完全给过滤掉了,比如下面例子:


// 这个try本意是处理api请求异常
try {
const data = getSaveData() // 这段js逻辑也在try里,所以如果这个方法内部抛错了,页面上就没任何反应,很难追踪问题
const result = await api.post(url, data)
// result 逻辑处理
} catch (e) {
// 好点的给弹个框,打个log,甚至有的啥都不处理
}

总结就是:把异常给隐式过滤掉了,导致不能快速定位问题。


3. 编译后代码冗余


如果代码是ts,并且编译目标是ES2016,编译后代码会很长。可以看下 http://www.typescriptlang.org/play 效果。


image.png


Babel在个别stage下,编译效果一样。


image.png


但并不是说一点都不用,意思是尽量减少滥用,这样使用的频率会少很多,这种编译代码沉余也会少不少。


应该怎么用?


说了这么多,.? 应该怎么用呢?意思是不用吗?当然不是不能用,这个特性对于开发肯定好处很多的,但是得合理用,不能滥用。



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。


其实说白了就是:什么时候需要判断一个变量或属性非空,什么时候不需要。首先在使用的时候得想下,问号前面的变量或属性值,有没有可能是空值:



  1. 很明显不可能是空值,比如 React类组件里的 this.state this.props,不要用;

  2. 自己定义的变量或属性,而且没有赋值为空值情况,不要用;

  3. 某些方法或者组件里,参数和属性不允许是空值,那方法和组件里就不需要判断非空。(对于比较common的,推荐写断言,或者判断空值情况throw error)

  4. 后台api请求结果里,要求result或其内部属性必须有值,那这些值就不需要判断非空。

  5. 按正常流程走,某个数据不会有空值情况,如果是空值说明前面的流程出问题了,这种情况就不需要在逻辑里判断非空。


const userName = data?.items?.[0]?.user?.name // 不要滥用,如果某个属性有可能是空值,则需要?.
const userName = data.items[0].user?.name // 比如data.items数组肯定不是空数组

const items2 = items1.filter(item => item.checked)
if (items2?.length) { } // 不需要?.

// react class component
const name = this.state?.name // 不需要?.

// react hooks
const [items, setItems] = useState([])
items?.map(...) // 如果setItems没有赋值空值情况,则不需要?.
setItems?.([]) // 不需要?.

const item1 = obj?.item1 // 不需要?.
console.log(item1.name)

const id = obj?.id // 下面代码已经说明不能是空值了,不需要?.
const name = obj.name

if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2 // 不需要?.
const name = obj?.item1?.item2?.name // 不需要?.
}

const id = obj?.item?.id // 不需要?.
api.get(id).then(...) // 这个api如果id是空值,则api会抛错

当然,写代码时还得多想一下属性是否可能是空值,会一定程度的影响开发效率,也一定有开发会觉得很烦,不理解,无脑写?.多容易啊,但是我从另外两个角度分析下:



  1. 我觉得一个合格的开发应该对自己的代码逻辑很熟悉,应该有责任知道哪些值可能是空值,哪些不可能是空值(并不是说所有,也有大部分了),否则就是对自己的代码了解很少,觉得代码能跑就行,代码质量自然就低。

  2. 想想在这个新特性出来之前大家是怎么写的,会对每个变量和属性都加if非空判断或者用逻辑与(&&)吗?不会吧。


总结


本文以一个 code reviewer 角度,分析了 可选链运算符(?.) 特性的滥用情况,以及“正确使用方式”,只是代表我本人的看法,欢迎大佬参与讨论,无条件接受任何反驳。


滥用的缺点:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


“正确用法”:



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。




后记(09月25日更新)


从评论上看,对于可选链的看法,大多声音是能加就加,多加总比少加好,原因就是不想背锅,不想上线后JS动不动就崩了,无论根本原因是不是前端开发没加判断导致的,第一责任人就会找到你,有的甚至会被上级追责,问题就更严重了,而且很难解释清楚;另一方面就是为了赶工期,可选链的其中一个优点就是简单,提高开发效率。


我再从几个方面浅浅的扩展下我的看法,欢迎参与讨论


总之。。。对对对,你们说的都对!


作者:Mark大熊
来源:juejin.cn/post/7280747572707999799
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


正文


取消功能


我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


 const p = new Promise((resolve, reject) => {
setTimeout(() => {
// handler data, no resolve and reject
}, 1000);
});
console.log(p); // Promise {<pending>} 💡

但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


const getData = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("发送网络请求获取数据"); // ❗
resolve("success get Data");
}, 2500);
});

const timer = () =>
new Promise((_, reject) => {
setTimeout(() => {
reject("timeout");
}, 2000);
});

const p = Promise.race([getData(), timer()])
.then((res) => {
console.log("获取数据:", res);
})
.catch((err) => {
console.log("超时: ", err);
});

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


超时中断.png


而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



取消请求.png


其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="send">Send</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
console.log("delay cancelled");
resolve();
});
});
}
}
const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayedResolve(delay) {
console.log("prepare send request");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
console.log("ajax get data");
resolve();
}, delay);

const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback)
);
cancelToken.promise.then(() => clearTimeout(id));
});
}
sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
</html>

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


介于自己没理解,我就按照自己的思路封装个不一样的🤣:


const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

class CancelPromise {

// delay: 取消功能期限 request:获取数据请求(必须返回 promise)
constructor(delay, request) {
this.req = request;
this.delay = delay;
this.timer = null;
}

delayResolve() {
return new Promise((resolve, reject) => {
console.log("prepare request");
this.timer = setTimeout(() => {
console.log("send request");
this.timer = null;
this.req().then(
(res) => resolve(res),
(err) => reject(err)
);
}, this.delay);
});
}

cancelResolve() {
console.log("cancel promise");
this.timer && clearTimeout(this.timer);
}
}

// 模拟网络请求
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("this is data");
}, 2000);
});
}

const cp = new CancelPromise(1000, getData);

sendButton.addEventListener("click", () =>
cp.delayResolve().then((res) => {
console.log("拿到数据:", res);
})
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

正常发送请求获取数据:


发送请求.gif


中断 promise:


取消请求.gif


没啥大毛病捏~


进度通知功能


进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));


emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



// 模拟数据请求
function getData(timer, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, timer);
});
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
try {
const res1 = await getData1();
notify("已获取到一阶段数据");
const res2 = await getData2();
notify("已获取到二阶段数据");
const res3 = await getData3();
notify("已获取到三阶段数据");
resolve([res1, res2, res3]);
} catch (error) {
notify("出错!");
reject(error);
}
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));


notify获取数据.gif


对味儿了~😀


End


关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


至于说进度通知功能,仁者见仁智者见智吧...


但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


作者:討厭吃香菜
来源:juejin.cn/post/7312349904046735400
收起阅读 »

用脚本来写函数式弹窗,更快更爽

web
前言 在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方...
继续阅读 »

前言


在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方式来向弹窗内更新props和处理弹窗的emit事件。


iShot_2023-08-15_10.13.24.gif


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


传统vue编写弹窗


通过变量来直接控制弹窗的开启和关闭。


<template>
<n-button @click="showModal = true">
来吧
</n-button>

<n-modal v-model:show="showModal" preset="dialog" title="Dialog">
<template #header>
<div>标题</div>
</template>
<div>内容</div>
<template #action>
<div>操作</div>
</template>
</n-modal>

</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup () {
return {
showModal: ref(false)
}
}
})
</script>


痛点



  • 深层次的传props让人有很大的心理负担,污染组件props

  • 要关注弹窗show变量的true,false


函数式弹窗


在主页面用Provider包裹一下


// RootPage.vue
<ModalProvider>
<ChildPage></ChildPage>
</ModalProvider>

<script setup lang="ts">
import ModalProvider from "./ModalProvider.vue"
import ChildPage from "./ChildPage.vue"
</script>


在页面内的某个子组件中,直接通过oepn方法打开弹窗


// ChidPage.vue
<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


优势



  • 对于使用者来说简单,没有控制show的心理负担

  • 弹窗内容和其他业务代码分离,不会污染其他组件props和结构

  • 能够充分复用同一个弹窗,通过函数参数的方式来更新弹窗内容,在不同地方打开同一个弹窗


劣势



  • 对于一些简单需求的弹窗,用这种函数式的弹窗会有些臃肿

  • 在使用函数式弹窗前需要做一些操作(Provider和Inject),会在后面提到以及解决方案。


如何使用这种函数式的弹窗


原理


通过在根页面将我们的Modal组件挂载上去,然后通过使用hook去管理这个modal组件的状态值(props,ref),然后通过provide将props和event和我们的modal组件联系起来,再在我们需要使用弹窗的地方使用inject更新props来启动弹窗。


步骤1(❌):编写Modal


这里我使用的是Naive Ui的Modal组件,按喜好选择就行。按照你的需求编写弹窗的内容。定义好props和emits,写好他们的类型申明。


// TestModal.vue
<template>
<n-modal
v-model:show="isShowModal"
preset="dialog"
@after-leave="handleClose"
>

...你的弹窗内容
</n-modal>

</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"
import { useVModel } from "@vueuse/core"
import { FormRules } from "naive-ui"
interface ModalProps {
show: boolean
testValue: string
}

// 中间区域不要修改
const props = defineProps<ModalProps>()
const formRef = ref()
const loading = ref(false)
const isShowModal = useVModel(props, "show")
// 中间区域不要修改

const rules: FormRules = []

const formData = reactive({
testValue: props.testValue,
})

const callBackData = computed(() => {
return {
formData,
}
})

watch(
() => props.show,
() => {
if (props.show) {
formData.testValue = props.testValue
} else {
formData.testValue = ""
}
}
)

const emits = defineEmits<{
(e: "update:show", value: boolean): void
(e: "close", param: typeof callBackData.value): void
(
e: "confirm",
param: typeof callBackData.value,
close: () => void,
endLoading: () => void
): void
(e: "cancel", param: typeof callBackData.value): void
}>()
function handleCancel() {
// 中间区域不要修改
emits("cancel", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleClose() {
// 中间区域不要修改
emits("close", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleConfirm() {
// 中间区域不要修改
loading.value = true
emits(
"confirm",
callBackData.value,
() => {
loading.value = false
isShowModal.value = false
},
() => {
loading.value = false
}
)
// 中间区域不要修改
}
</script>

步骤2(❌):编写hook来管理弹窗的状态


在这个文件里面,使用hook管理 TestModal 弹窗需要的props和event事件,然后我们通过ts类型体操来获取TestModal的props类型和event类型,我们向外inject一个 open 函数,这个函数可以更新 TestModal 的props和event,同时打开弹窗,他的参数有完整的类型提示,可以让使用者更加明确的使用我们的弹窗。


// use-test-modal.ts
import {
ref,
provide,
InjectionKey,
inject,
VNodeProps,
AllowedComponentProps,
reactive,
} from "vue";
import Modal from "./TestModal.vue";

/**
* 通过引入弹窗组件来获取组件的除show,update:show以外的props和emits来作为open函数的
*/

type ModalInstance = InstanceType<
typeof Modal extends abstract new (...args: any) => any ? typeof Modal : any
>["$props"];
type OpenParam = Omit<
{
readonly [K in keyof Omit<
ModalInstance,
keyof VNodeProps | keyof AllowedComponentProps
>]: ModalInstance[K];
},
"show" | "onUpdate:show"
>;

interface AnyFileChangeModal {
open: (param?: OpenParam) => Promise<void>;
}

/**
* 通过弹窗实例来获取弹窗组件内需要哪些props
*/

type AllProps = Omit<
OpenParam,
"onClose" | "onCancel" | "onConfirm" | "onUpdate:show"
> & { show: boolean };
const anyModalKey: InjectionKey<AnyFileChangeModal> = Symbol("ModalKey");
export function provideTestModal() {
const allProps: AllProps = reactive({
show: false,
} as AllProps);
const closeCallback = ref();
const cancelCallback = ref();
const confirmCallback = ref();
const handleUpdateShow = (value: boolean) => {
allProps.show = value;
};

/**
* @param param 通过函数来更新props
*/

function updateAllProps(param: OpenParam) {
const excludeKey = ["show", "onClose", "onConfirm", "onCancel"];
for (const [key, value] of Object.entries(param)) {
if (!excludeKey.includes(key)) {
allProps[key] = value;
}
}
}
function clearAllProps() {
for (const [key] of Object.entries(allProps)) {
allProps[key] = undefined;
}
}

async function open(param: OpenParam) {
clearAllProps();
updateAllProps(param);
allProps.show = true;
param.onClose && (closeCallback.value = param.onClose);
param.onConfirm && (confirmCallback.value = param.onConfirm);
param.onCancel && (cancelCallback.value = param.onCancel);
}
provide(anyModalKey, { open });
return {
allProps,
closeCallback,
confirmCallback,
cancelCallback,
handleUpdateShow,
};
}

export function injectTestModal() {
return inject(anyModalKey)
}
Ï

步骤3(❌):提供Provider


在这个文件里,我将TestModal放在了根页面,然后将hook返回的props和event绑定给TestModal


// ModalProvider.vue
<template>
<slot />

<TestModal
v-bind="allTestModalProps"
@update:show="handleTestModalUpdateShow"
@close="closeTestModalCallback"
@confirm="confirmTestModalCallback"
@cancel="cancelTestModalCallback"
/>

<!-- 新增Modal -->
</template>

<script setup lang="ts">
import TestModal from "./test-modal/TestModal.vue";
import { provideTestModal } from "./test-modal/use-test-modal";
/** 新增import */

const {
allProps: allTestModalProps,
handleUpdateShow: handleTestModalUpdateShow,
closeCallback: closeTestModalCallback,
confirmCallback: confirmTestModalCallback,
cancelCallback: cancelTestModalCallback,
} = provideTestModal();
/** 新增provide */
</script>


步骤4(❌):通过函数打开弹窗


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


到这里也就结束了。如果这样写一个弹窗,确实可以达到函数式的打开弹窗,但是他实在是太繁琐了,要写这么一堆东西,不如直接修改弹窗的show来的快。如果看过上面的use-test-modal.ts的话,会发现里面有AllProps,这是为了减少方便使用脚本工具来写通用化的代码,可以大大减少人工的重复代码编写,接下来我们使用一个工具来减少这些繁琐的操作。


步骤1(✅):初始化Provider


通过使用工具生成根页面ModalProvder组件。
具体步骤:在你想放置当前业务版本的弹窗文件夹路径下使用终端执行脚本,选择initProvider


iShot_2023-08-15_15.16.14.gif


步骤2(✅):生成弹窗模板


通过继续使用脚本工具,生成弹窗组件以及hook文件。
具体步骤:在你想放该弹窗的文件夹路径下使用终端,使用脚本工具,选择genModal,然后跟着指令操作就行。比如例子中,我们生产名为test的弹窗,然后告诉脚本我们的ModalProvider组件的绝对路径,脚本帮我生产test-modal的文件夹,里面放着testModal.vueuse-test-modal.ts,这里的use-test-modal.ts文件已经是成品了,不需要你去修改,ModalProvider.vue也不需要你去修改,里面的路径关系也帮你处理好了。


iShot_2023-08-15_15.17.33.gif


步骤3(✅):修改弹窗内容


上一步操作中,脚本帮我写好了TestModal.vue组件,我们可以在他的基础上完善我们的业务需求。


步骤4(✅):调用弹窗


我们找到生成的use-test-modal.ts,里面有一个injectXXXModal的方法,我们在哪个地方用到弹窗就引用执行他,返回的对象中有open方法,通过open方法去开启弹窗并且更新弹窗props。


Demo


预览
Demo地址
里面有完整的demo代码


iShot_2023-08-15_18.32.39.gif


脚本工具


仓库地址
这个是我写的脚本工具,用来减少一些重复代码的工作。用法可看readme


总结


本文先通过比较函数式的弹窗和直接传统编写弹窗的优劣,然后引出本文的主旨(如何编写函数式弹窗),再考虑我这一版函数式弹窗的问题,最后通过脚本工具来解决这一版函数式弹窗的劣势,完整的配合脚本工具,可以极大的加快我们的工作效率。
大家不要看这么写要好几个文件夹,实际使用的时候其实就是脚本生成代码后,我们只需要去修改弹窗主题的内容。有什么疑惑或者建议评论区聊。


作者:恐怖屋
来源:juejin.cn/post/7267418473401057321
收起阅读 »

当别人因为React、Vue吵起来时,我们应该做什么

web
大家好,我卡颂。 最近尤大的一个推文引起了不小热议,大概经过是: 有人在推上夸React文档写的好,把可能的坑点都列出来 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户 尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关...
继续阅读 »

大家好,我卡颂。


最近尤大的一个推文引起了不小热议,大概经过是:



  1. 有人在推上夸React文档写的好,把可能的坑点都列出来

  2. 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户



尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关注度自然不低。


再加上国内前端自媒体的一波引导发酵,比如知乎下这个话题相关的问题中的措辞是怒喷,懂得都懂。



在这样氛围与二手信源的影响下,会给人一种大佬都亲手下场撕了的感觉,自然会引来ReactVue各自拥趸的一番激烈讨论。


年年都是一样的套路,毫无新意......


面对这样的争吵,我们应该做什么呢?


首先,回到源头本身,尤大diss的有道理么?有。


React的心智负担重么?确实重。比如useEffec这个API,你能想象文档中一个章节居然有6篇文章是教你如何正确使用useEffec的么?



造成这一现象的原因有很多,比如:



  1. Hooks的实现原理使得必须显式声明依赖

  2. 显式声明依赖无法覆盖useEffect所有场景,为此专门提出一个叫Effect Event的概念,以及一个对应的新hook —— useEffectEvent

  3. useEffect承载了太多功能,比如未来Offscreen的显隐回调(类似Vue中的Keep-Alive)也是通过useEffect实现


当我们继续往前回溯,Hooks必须显式声明依赖React更新机制决定的,而React更新机制又是React实现原理的核心。


本质来说,还是React既往的成功、庞大的社区生态让他积重难返,无法从底层重写。


这是历史必然的进程,如果Vue所有新特性都在Vue2基础上迭代(而不是完全重写的Vue3),我相信也是同样的局面。


所以,当前React的迭代方向是 —— 支持上层框架(比如Next.jsRemix),寄希望于靠这些框架的封装能力弥补React自身心智负担重的缺点。这个策略显然也是成功的。


回到这次争吵本身,尤大不知道React文档为什么要花大篇幅帮开发者避坑(以及背后反映的积重难返)么?他显然是知道的。


他如此回复是因为他所处的位置是框架作者React是他的竞争对手。设想一下,如果你的竞争对手在一些方面确实不如你,但他的用户对此的反应不是“太难用了,我要换个好用的”,而是“一定是我用的姿势不对,你快出个文档好好教教我”


面对这样的用户,换谁都得有一肚子牢骚吧~



让我们再把视角转到React的用户(也就是我们这些普通开发者)上。我们为什么选择React呢?


可能有些人是处于喜好。但大部分开发者之所以用React,完全是因为公司要求用React


React的公司多,招React的岗位多,自然选择React的开发者就多了。


那么为什么用React的公司多呢?这显然是多年前React在先发优势、社区生态两场战役取胜后得到的结果。


总结


所以,我们需要尊重两个事实:



  1. React心智负担重是事实

  2. React的公司多也是事实


两者并不矛盾,他们都是历史进程的产物。


VueReact之间的讨论,即使是从技术层面出发,最后也容易陷入“React心智负担这么重,你们还甘之如饴,你们React党是不是傻”这样的争吵中。


这显然就是忽略了历史的进程。


正确的应对方式是多关心关心自己未来的发展:



  • 如果我的重心在海外,那应该给Next.js更多关注。海外远程团队不是Next就是Nest

  • 如果我的重心在国内,国内流量都被小程序分割了。一个长远的增长点应该是鸿蒙


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

面试官:手写一个“发布-订阅模式”

web
发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。 DOM事件 document.body.addEventListener...
继续阅读 »

发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。


DOM事件


document.body.addEventListener('click',function(){

alert(绑定1);

},false);

document.body.click(); //模拟点击

document.body.addEventListener('click',function(){

alert(绑定2);

},false);

document.body.addEventListener('click',function(){

alert(绑定3);

},false);

document.body.click(); //模拟点击

我们可以增加更多订阅者,不会对发布者的代码造成影响。注意,标准浏览器下用dispatchEvent实现。


自定义事件


① 确定发布者。(例如售票处)


② 添加缓存列表,便于通知订阅者。(预订车票列表)


③ 发布消息。遍历缓存列表。依次触发里面存放的订阅者回调函数(遍历列表,逐个发送短信)。


另外,我们还可以在回调函数填入一些参数,例如车票的价格之类信息。


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (fn) { //增加订阅者
this.clientList.push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
for(var i = 0, fn; fn = this.clientList[i++];){
fn.apply(this, arguments); //arguments 是发布消息时带上的参数
}
}

// 下面进行简单测试:

ticketOffice.on(function(time, path){ //小刚订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});


ticketOffice.on(function(time, path){ //小强订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});

ticketOffice.emit('晚上8:00','深圳-上海');
ticketOffice.emit('晚上8:10','上海-深圳');

至此,我们实现了一个最简单发布-订阅模式。不过这里存在一些问题,我们运行代码可以看到订阅者接收到了所有发布的消息。


// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:10
// 路线:上海-深圳
// 时间:晚上8:10
// 路线:上海-深圳

我们有必要加个key让订阅者只订阅 自己感兴趣的消息。改写后的代码如下:


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// --------- 测试数据 --------------
ticketOffice.on('上海-深圳', function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
});

ticketOffice.on('深圳-上海', function(time){ //小强订阅消息
console.log('小强时间:' + time);
});

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小强时间:晚上8:00
// 小刚时间:晚上8:10

这样子,订阅者就可以只订阅自己感兴趣的事件了。


小强临时行程有变,不想订阅对应的消息了,我们还需要再新增一个移除订阅的方法


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

ticketOffice.remove = function (key, fn) {
let fns = this.clientList[key]
if (!fns) return false

if (!fn) {
fns && (fns.length = 0)
} else {
fns.forEach((cb, i) => {
if (cb === fn) {
fns.splice(i, 1)
}
})
}
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

至此,我们实现了一个相对完善的发布-订阅模式


但是可以看到使用数组进行时间的push和remove可能绑定相同的事件,且事件remove的效率低,我们可以用Set来替换Array


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = new Set();
}
this.clientList[key].add(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// 移除路线的单个订阅
ticketOffice.remove = function (key, fn) {
this.clientList[key]?.delete(fn)
}
// 移除路线的所有订阅
ticketOffice.removeAll = function (key) {
delete this.clientList[key]
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

参考资料


《JavaScript 设计模式与开发实践》


作者:dudulala
来源:juejin.cn/post/7320075000702533671
收起阅读 »

普通的文本输入框无法实现文字高亮?试试这个highlightInput吧!

web
背景 前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到...
继续阅读 »

背景


前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到:没办法,这是老板提的需求,你下去研究研究吧。行吧,老板发话说啥也没用,开干吧!


实现思路


实现标红就需要给文字加上html标签和样式,但是输入框会将html都转为字符串,既然输入框无法实现,那么我们换一种思路,通过div代替输入框来显示输入的文本,那我们是不是就可以实现文本标红了?话不多说,直接上代码(文章结尾会附上demo):


<div class="main">
<div id="shadowInput" class="highlight-shadow-input"></div>
<textarea
id="textarea"
cols="30"
rows="10"
class="highlight-input"
>
</textarea>
</div>

.main {
position: relative;
}
.highlight-shadow-input {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
padding: 8px;
border: 1px;
box-sizing: border-box;
font-size: 12px;
font-family: monospace;
overflow-y: auto;
word-break: break-all;
white-space: pre-wrap;
}
.highlight-input {
position: relative;
width: 100%;
padding: 8px;
box-sizing: border-box;
font-size: 12px;
background: rgba(0, 0, 0, 0);
-webkit-text-fill-color: transparent;
z-index: 999;
word-break: break-all;
}

实现这个功能的精髓就在于将输入框的背景和输入的文字设置为透明,然后将其层级设置在div之上,这样用户既可以在输入框中输入,而输入的文字又不会展示出来,然后将输入的文本处理后渲染到div上。


const textarea = document.getElementById("textarea");
const shadowInput = document.getElementById("shadowInput");
const sensitive = ["敏感词", "禁用词"];
textarea.oninput = (e) => {
let value = e.target.value;
sensitive.forEach((word) => {
value = value.replaceAll(
word,
`<span style="color:#e52e2e">${word}</span>`
).replaceAll("\n", "<br>");;
});
shadowInput.innerHTML = value;
};

监听输入框oninput事件,用replaceAll匹配到敏感词并转为html后渲染到shadowInput上。此外,我们还需要对输入框的滚动进行监听,因为shadowInput是固定高度的,如果用户输入的文本出现滚动条,则需要让shadowInput也滚动到对应的位置


<div><div id="shadowInput" class="highlight-shadow-input"></div></div>

textarea.onscroll = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};
// 此处输入时也需要同步是因为输入触底换行时,div的高度不会自动滚动
textarea.onkeydown = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};

最终实现效果:


至此一个简单的文本输入框实现文字高亮的功能就完成了,上述代码只是简单示例,在实际业务场景中还需要考虑xss注入、特殊字符处理、特殊字符高亮等等复杂问题。


总结


这篇文章主要给遇到有类似业务需求的同学一个参考,以及激发大家的灵感,用这种方法是不是还可以实现一些简单的富文本功能呢?例如文字加删除线、文字斜体加粗等等。有想法或有问题的小伙伴可以在评论区留言一起探讨哦!


demo



作者:宇智波一打七
来源:juejin.cn/post/7295169886177918985
收起阅读 »

多行标签超出展开折叠功能

web
前言  记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。  今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,...
继续阅读 »

前言


 记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。
 今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,类似于多行文本展开折叠功能,如果超过最大行数则显示展开隐藏按钮,如果不超过则不显示按钮。多行文本展开与折叠功能在网上有相当多的文章了,也有许多开源的封装组件,而多行标签展开隐藏的文章却比较少,刚好最近我也遇到了这个功能,所以就单独拿出来与大家分享如何实现。


出处


 【多行标签展开与隐藏】该功能我们平时可能没注意一般在哪里会有,其实最常见的就是各种APP的搜索页面的历史记录这里,下面是我从拼多多(左)和腾讯学堂小程序(右)截下来的功能样式:


多行标签案列图(pdd/txxt)


其它APP一般搜索的历史记录这里都有这个小功能,比如京东、支付宝、淘宝、抖音、快手等,可能稍有点儿不一样,有的是按钮样式,有的是只有展开没有收起功能,可能我们用过了很多年平时都没有注意到这个小功能,有想了解的可以去看一看哈。如果有一天你们需要开发一个搜索页面的话产品就很有可能出这样的一个功能,接下来我们就来看看这种功能我们该如何实现。


功能实现


我们先看实现的效果图,然后再分析如何实现,效果图如下:



【样式一】:标签容器和展开隐藏按钮分开(效果图样式一)


 标签容器和按钮分开的这种样式功能实现起来的话我个人觉得难度稍微简单一些,下面我们看看如何实现这种分开的功能。


第一种方法:通过与第一个标签左偏移值对比实现

原理:遍历每个标签然后通过与第一个标签左偏移值对比,如果有几个相同偏移值则说明有几个换行


具体实现上代码:


<div class="list-con list-con-1">
<div class="label">人工智能div>
<div class="label">人工智能与应用div>
<div class="label">行业分析与市场数据div>
<div class="label">标签标签标签标签标签标签标签标签div>
<div class="label">标签div>
<div class="label">啊啊啊div>
<div class="label">宝宝贝贝div>
<div class="label">微信div>
<div class="label">吧啊啊div>
<div class="label">哦哦哦哦哦哦哦哦div>
div>
<div class="expand expand-1">展开 ∨div>



解析:HTML布局就不用多说了,是个前端都知道该怎么搞,如果不知道趁早送外卖去吧,多说无益,把机会留给其他人。其次CSS应该也是比较简单的,注意的是有个前提需要先规定容器的最大高度,然后使用overflow超出隐藏,这样展开就直接去掉该属性,让标签自己撑开即可。JavaScript部分我这里没有使用啥框架,因为这块实现就是个简单的Demo所以就用纯原生写比较方便,这里我们先获取容器,然后获取容器的孩子节点(这里我们也可以直接通过className查询出所有标签元素),返回的是一个可遍历的变签对象,然后我们记录第一个标签的offsetLeft左偏移值,接下来遍历所有的标签元素,如果有与第一个标签相同的值则累加,最终line表示有几行,如果超过我们最大行数(demo超出2行隐藏)则显示展开隐藏按钮。


第二种方法:通过计算容器高度对比

原理:通过容器底部与标签top比较,如果有top值大于容器底部bottom则表示超出容器隐藏。


具体上代码:




解析:HTMLCSS同方法一同,不同点在于这里是通过getBoundingClientRect()方法来判断,还是遍历所有标签,不同的是如果有标签的top值大于等于了容器的bottom值,则说明了标签已超出容器,则要显示展开隐藏按钮,展开隐藏还是通过容器overflow属性来实现比较简单。


【样式二】:展开隐藏按钮和标签同级(效果图样式二)


 这种样式也是绝大部分APP产品使用的风格,不信你可以打开抖音商城或汽车之家的搜索历史,十个产品九个是这样设计的,不是这样的我倒立洗头。
 这种放在同级的就相对稍微难一点,因为要把展开隐藏按钮塞到标签的最后,如果是隐藏的话就要切割标签展示数量,那下面我就带大家看看我是是如何实现的。


方法一:通过遍历高度判断

原理:同样式一的高度判断一样,通过容器底部bottom与标签top比较,如果有top值大于容器顶部bottom则表示超出容器隐藏,不同的是如何计算标签展示的长度。有个前提是按钮和标签的的宽度要做限制,最好是一行能放一个标签和按钮。


具体实现上代码:


<div id="app3">
<div class="list-con list-con-3" :class="{'list-expand': isExpand}">
<div class="label" v-for="item in labelArr.slice(0, labelLength)">{{ item }}div>
<div class="label expand-btn" v-if="showExpandBtn" @click="changeExpand">{{ !isExpand ? '展开 ▼' : '隐藏 ▲' }}div>
div>
div>


<script>
const { createApp, nextTick } = Vue
createApp({
props: {
maxLine: {
type: Number,
default: 2
}
},
data () {
return {
labelArr: [],
isExpand: false,
showExpandBtn: false,
labelLength: 0,
hideLength: 0
}
},
mounted () {
const labels = ['人工智能', '人工智能与应用', '行业分析与市场数据', '标签标签标签标签标签标签标签', '标签A', '啊啊啊', '宝宝贝贝', '微信', '吧啊啊', '哦哦哦哦哦哦哦哦', '人工智能', '人工智能与应用']

this.labelArr = labels
this.labelLength = labels.length
nextTick(() => {
this.init()
})
},
methods: {
init () {
const listCon = document.querySelector('.list-con-3')
const labels = listCon.querySelectorAll('.label:not(.expand-btn)')
const expandBtn = listCon.querySelector('.expand-btn')

let labelIndex = 0 // 渲染到第几个
const listConBottom = listCon.getBoundingClientRect().bottom // 容器底部距视口顶部距离
for(let i = 0; i < labels.length; i++) {
const _top = labels[i].getBoundingClientRect().top
if (_top >= listConBottom ) { // 如果有标签顶部距离超过容器底部则表示超出容器隐藏
this.showExpandBtn = true
console.log('第几个索引标签停止', i)
labelIndex = i
break
} else {
this.showExpandBtn = false
}
}
if (!this.showExpandBtn) {
return
}
nextTick(() => {
const listConRect = listCon.getBoundingClientRect()
const expandBtn = listCon.querySelector('.expand-btn')
const expandBtnWidth = expandBtn.getBoundingClientRect().width
const labelMaringRight = parseInt(window.getComputedStyle(labels[0]).marginRight)
for (let i = labelIndex -1; i >= 0; i--) {
const labelRight = labels[i].getBoundingClientRect().right - listConRect.left
if (labelRight + labelMaringRight + expandBtnWidth <= listConRect.width) {
this.hideLength = i + 1
this.labelLength = this.hideLength
break
}
}
})
},
changeExpand () {
this.isExpand = !this.isExpand
console.log(this.labelLength)
if (this.isExpand) {
this.labelLength = this.labelArr.length
} else {
this.labelLength = this.hideLength
}
}
}
}).mount('#app3')
script>


解析:同级样式Demo我们使用vue来实现,HTML布局和CSS样式没有啥可说的,还是那就话,不行真就送外卖去比较合适,这里我们主要分析一下Javascript部分,还是先通过getBoundingClientRect()方法来获取容器的bottom和标签的top,通过遍历每个标签来对比是否超出容器,然后我们拿到第一个超出容器的标签序号,就是我们要截断的长度,这里是通过数组的slice()方法来截取标签长度,接下来最关建的如何把按钮拼接上去,因为标签的宽度是不定的,我们要把按钮显示在最后,我们并不确定按钮拼接到最后是不是会导致宽度不够超出,所以我们倒叙遍历标签,如果(最后一个标签的右边到容器的距离right值+标签的margin值+按钮的width)和小于容器宽度,则说明展示隐藏按钮可以直接拼接在后面,否则标签数组长度就要再减一位来判断是否满足。然后展开隐藏功能就通过切换原标签长度和截取的标签长度来完成即可。


方法二:通过与第一个标签左偏移值对比实现

原理:同样式一的方法原理,遍历每个标签然后通过与第一个标签左偏移值对比判断是否超出行数,然后长度截取同方法一一致。


直接上代码:




这里也无需多做解释了,直接看代码即可。


结尾


上面就是【多行标签展开隐藏】功能的基本实现原理,网上相关实现比较少,我也是只用了Javascript来实现,如果可以纯靠CSS实现,有更简单或更好的方法实现可以留言相互交流学。代码没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库。




作者:Liben
来源:juejin.cn/post/7251394142683742269
收起阅读 »

MyBatis实战指南(二):工作原理与基础使用详解

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。一、MyBatis的工作原理1.1 MyBatis的工作原理工作原理图示:1、读取...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。

一、MyBatis的工作原理

1.1 MyBatis的工作原理

工作原理图示:
Description

1、读取MyBatis配置文件

mybatis-config.xml为MyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。

2、加载映射文件(SQL映射文件,一般是XXXMapper.xml)

该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。

XXXMapper.xml可以在mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。

3、构造会话工厂

通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。

4、创建会话对象

由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

5、Executor执行器

MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

6、MappedStatement对象

在 Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

7、输入参数映射

输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

8、输出结果映射

输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

1.2 MyBatis架构

Description

API接口层

提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

MyBatis和数据库的交互有两种方式:使用传统的MyBatis提供的API、使用Mapper接口。

1)使用传统的MyBatis提供的API

这是传统的传递Statement Id和查询参数给SqlSession对象,使用SqlSession对象完成和数据库的交互;
Description
MyBatis提供了非常方便和简单的API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和MyBatis自身配置信息的维护操作。
示例:

SqlSession session = sqlSessionFactory.openSession();
Category c = new Category();
c.setName("新增加的Category");
session.insert("addCategory",c);

上述使用MyBatis的方法,是创建一个和数据库打交道的SqlSession对象,然后根据Statement Id和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。

2)使用Mapper接口

MyBatis将配置文件中的每一个<mapper>节点抽象为一个Mapper接口,而这个接口中声明的方法和跟<mapper>节点中的<select|update|delete|insert>节点项对应,

即<select|update|delete|insert>节点的id值为Mapper接口中的方法名称,parameterType值表示Mapper对应方法的入参类型,而resultMap值则对应了Mapper接口表示的返回值类型或者返回结果集的元素类型。

示例:

SqlSession session = sqlSessionFactory.openSession();
CategoryMapper mapper = session.getMapper(CategoryMapper.class);
List<Category> cs = mapper.list();
for (Category c : cs) {
System.out.println(c.getName());
}

根据MyBatis的配置规范配置后,通过SqlSession.getMapper(XXXMapper.class)方法,MyBatis会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper实例。
Description

使用Mapper接口的某一个方法时,MyBatis会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select(“statementId”,parameterObject)或者SqlSession.update(“statementId”,parameterObject)等等来实现对数据库的操作。

MyBatis引用Mapper接口这种调用方式,纯粹是为了满足面向接口编程的需要。

数据处理层

负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

1)参数映射和动态SQL语句生成

动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis通过传入的参数值,使用OGNL表达式来动态地构造SQL语句,使得MyBatis有很强的灵活性和扩展性。
Description
参数映射指的是对于Java数据类型和JDBC数据类型之间的转换,这里包括两个过程:

  • 查询阶段,我们要将java类型的数据,转换成JDBC类型的数据,通过preparedStatement.setXXX()来设值;

  • 另一个就是对ResultSet查询结果集的JdbcType 数据转换成Java数据类型。

2)SQL语句的执行以及封装查询结果集成List< E>

动态SQL语句生成之后,MyBatis将执行SQL语句并将可能返回的结果集转换成List<E> 。

MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。

基础支撑层

负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

MyBatis层次结构

Description

1.3 Executor执行器

Executor的类别

Mybatis有三种基本的Executor执行器:SimpleExecutor、ReuseExecutor和BatchExecutor。

Description

1、SimpleExecutor

每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

2、ReuseExecutor

执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。

3、BatchExecutor

执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Executor的配置

指定Executor方式有两种:

1、在配置文件中指定

<settings>
<setting name="defaultExecutorType" value="BATCH" />
</settings>

2、在代码中指定

在获取SqlSession时设置,需要注意的时是,如果选择的是批量执行器时,需要手工提交事务(默认不传参就是SimpleExecutor)。

示例:

// 获取指定执行器的sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 获取批量执行器时, 需要手动提交事务
sqlSession.commit();

1.4 Mybatis是否支持延迟加载

延迟加载是什么

MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。

例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。

MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。

假如Clazz 类中有子对象HeadTeacher。两者的关系:

public class Clazz {
private Set<HeadTeacher> headTeacher;
//...
}

是否查出关联对象的示例:

@Test
public void testClazz() {
ClazzDao clazzDao = sqlSession.getMapper(ClazzDao.class);
Clazz clazz = clazzDao.queryClazzById(1);
//只查出主对象
System.out.println(clazz.getClassName());
//需要查出关联对象
System.out.println(clazz.getHeadTeacher().size());
}

延迟加载的设置

在Mybatis中,延迟加载可以分为两种:延迟加载属性和延迟加载集合,association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false

1)延迟加载的全局设置

延迟加载默认是关闭的。如果需要打开,需要在mybatis-config.xml中修改:

<settings>

<setting name="lazyLoadingEnabled" value="true" />

<setting name="aggressiveLazyLoading" value="false"/>
</settings>

比如class班级与student学生之间是一对多关系。在加载时,可以先加载class数据,当需要使用到student数据时,我们再加载 student 的相关数据。

  • 侵入式延迟加载:指的是只要主表的任一属性加载,就会触发延迟加载,比如:class的name被加载,student信息就会被触发加载。

  • 深度延迟加载: 指的是只有关联的从表信息被加载,延迟加载才会被触发。通常,更倾向使用深度延迟加载。

2)延迟加载的局部设置

如果设置了全局加载,但是希望在某一个sql语句查询的时候不适用延时策略,可以配置局部的加载策略。

示例:

 <association
property="dept" select="com.test.dao.DeptDao.getDeptAndEmpsBySimple"
column="deptno" fetchType="eager"/>

etchType值有2种,

  • eager:立即加载;

  • lazy:延迟加载。

由于局部的加载策略的优先级高于全局的加载策略。指定属性后,将在映射中忽略全局配置参数lazyLoadingEnabled,使用属性的值。

延迟加载的原理

MyBatis使用Java动态代理来为查询对象生成一个代理对象。当访问代理对象的属性时,MyBatis会检查该属性是否需要进行延迟加载。

如果需要延迟加载,则MyBatis将再次执行SQL查询,并将查询结果填充到代理对象中。

二、MyBatis基础使用示例

1、添加MyBatis依赖

首先,我们需要在项目中添加MyBatis的依赖。如果你使用的是Maven项目,可以在pom.xml文件中添加以下依赖:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

2、创建实体类

假设我们有一个用户表(user),我们可以创建一个对应的实体类User:

public class User {
private int id;
private String name;
private int age;
// getter和setter方法省略
}

3、创建映射文件UserMapper.xml

在MyBatis的映射文件中,我们需要定义一个与实体类对应的接口。例如,我们可以创建一个名为UserMapper的接口:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserById" parameterType="int" resultType="com.example.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>

4、创建接口UserMapper.java

接下来,我们需要创建一个与映射文件对应的接口。例如,我们可以创建一个名为UserMapper的接口:

package com.example;

import com.example.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") int id);
}

5、使用MyBatis进行数据库操作

最后,我们可以在业务代码中使用MyBatis进行数据库操作。例如,我们可以在一个名为UserService的类中调用UserMapper接口的方法:

public class UserService {
public User getUserById(int id) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(id);
sqlSession.close();
return user;
}
}

总结:

MyBatis是一个非常强大的持久层框架,它可以帮助我们简化数据库操作,提高开发效率。在实际开发中,我们还可以使用MyBatis进行更复杂的数据库操作,如插入、更新、删除等。希望这篇文章能帮助你更好地理解和使用MyBatis。

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


const isEmptyObject = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object;

系列文章



我的更多前端资讯


欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接


作者:shichuan
来源:juejin.cn/post/7230810119122190397
收起阅读 »

检测自己网站是否被嵌套在iframe下并从中跳出

web
iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。 本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。 末尾放了正在使用的完整代码,想直接用的可以拉到最后。 效果 当存...
继续阅读 »

iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。

本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。


末尾放了正在使用的完整代码,想直接用的可以拉到最后。


效果


当存在嵌套时会出现一个蒙版和窗口,提示用户点击。

点击后会在新窗口打开网站页面。


嵌套展示


嵌套检测


设置响应头


响应头中有一个名为X-Frame-Options的键,可以针对嵌套操作做限制。

它有3个可选值:


DENY:拒绝所有


SAMEORIGIN:只允许同源


ALLOW-FROM origin:指定可用的嵌套域名,新浏览器已弃用


后端检测(以PHP为例)


通过获取$_SERVER中的HTTP_REFERERHTTP_SEC_FETCH_DEST值,可以判断是否正在被iframe嵌套


// 如果不是iframe,就为空的字符串
$REFERER_URL = $_SERVER['HTTP_REFERER'];

// 资源类型,如果是iframe引用的,会是iframe
$SEC_FETCH_DEST = $_SERVER['HTTP_SEC_FETCH_DEST'];

// 默认没有被嵌套
$isInIframe = false;

if (isset($_SERVER['HTTP_REFERER'])) {
$refererUrl = parse_url($_SERVER['HTTP_REFERER']);
$refererHost = isset($refererUrl['host']) ? $refererUrl['host'] : '';

if (!empty($refererHost) && $refererHost !== $_SERVER['HTTP_HOST']) {
$isInIframe = true;
}
}

// 这里通过判断$isInIframe是否为真,来处理嵌套和未嵌套执行的动作。
if($isInIframe){
....
}

前端检测(使用JavaScript)


通过比较window.self(当前窗口对象)和window.top(顶层窗口对象)可以判断是否正在被iframe嵌套


if (window.self !== window.top) {
// 检测到嵌套时该干的事
}

从嵌套中跳出


跳出只能是前端处理,如果使用了PHP等后端检测,可以直接返回前端JavaScript代码,或者HTML的A标签设置转跳。


JavaScript直接转跳(不推荐)


不推荐是因为现在大多浏览器为了防止滥用,会阻止自动弹出新窗口。


window.open(window.location.href, '_blank');

A标签点击转跳(较为推荐)


当发生了用户交互事件,浏览器就不会阻止转跳了,所以这是个不错的方法。


href="https://www.9kr.cc" target="_blank">点击进入博客

JavaScript+A标签(最佳方法)


原理是先使用JavaScript检测是否存在嵌套,

如果存在嵌套,再使用JavaScript加载蒙版和A标签,引导用户点击。


这个方法直接查看最后一节。


正在使用的方法


也就是上一节说的JavaScript+A标签。


先给待会要显示的蒙版和A标签窗口设置样式


/* 蒙版样式 */
.overlay1 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景颜色 */
z-index: 9999; /* 确保蒙版位于其他元素之上 */
display: flex;
align-items: center;
justify-content: center;
}

/* 窗口样式 */
.modal1 {
background-color: #fff;
padding: 20px;
border-radius: 5px;
}

然后是检测和加载蒙版+A标签的JavaScript代码


if (window.self !== window.top) {
// 创建蒙版元素
var overlay = document.createElement('div');
overlay.className = 'overlay1';

// 创建窗口元素
var modal = document.createElement('div');
modal.className = 'modal1';

// 创建A标签元素
var link = document.createElement('a');
link.href = 'https://www.9kr.cc';
link.target = '_blank'; // 在新窗口中打开链接
link.innerText = '点击进入博客';
//link.addEventListener('click', function(event) {
// event.preventDefault(); // 阻止默认链接行为
// alert('Test');
//});

// 将A标签添加到窗口元素中
modal.appendChild(link);

// 将窗口元素添加到蒙版元素中
overlay.appendChild(modal);

// 将蒙版元素添加到body中
document.body.appendChild(overlay);
}

博客的话,只需要在主题上设置自定义CSS自定义JavaScript即可


博客后台设置




作者:Edit
来源:juejin.cn/post/7272742720841252901
收起阅读 »

原来小程序分包那么简单!

web
前言 没有理论,只有实操,用最直接的方式来了解和使用小程序分包。 文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。 为什么要有小程序分包? 因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖...
继续阅读 »

前言


没有理论,只有实操,用最直接的方式来了解和使用小程序分包。


文章偏向使用taro来模拟小程序分包配置,在原生小程序中也是几乎差不多的配置方式。


为什么要有小程序分包?


因为上传小程序打包以后的代码包不可以超过2M。但我们在开发小程序的时候需要加载某些依赖或者一下静态图片,代码包难免超过2M。所以需要小程序分包功能将小程序中所有的代码分别打到不同的代码包里去,避免小程序只能上传2M的限制


目前小程序分包大小有以下限制:



  • 整个小程序所有分包大小不超过 20M(开通虚拟支付后的小游戏不超过30M)

  • 单个分包/主包大小不能超过 2M


如何对小程序进行分包?


本质上就是,配置一下app.json(小程序)或app.config.ts(Taro)中的subpackages字段。注意,分包的这个root路径和原本的pages是同级的。


如下图


image.png


这样配置好了,最基本的分包就完成了。


如何配置多个子包?


subpackages是个数组,在下面加上一样的结构就好了。


image.png
image.png


如何判断分包是否已经生效?


打开微信开发者工具,点击右上角详情 => 基本信息 => 本地代码,展开它。出现 主包,/xxxx/就是分包生效了。


如下图


image.png


所有页面都可以打到分包里面吗?


也不是,小程序规定,Tabbar页面不可以,一定需要在主包里。否则他直接报错。


分包中的依赖资源如何分配?


我们先来了解一下小程序分包资源


 引用原则
`packageA` 无法 require `packageB` JS 文件,但可以 require 主包、`packageA` 内的 JS 文件;使用 [分包异步化](https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/async.html) 时不受此条限制
`packageA` 无法 import `packageB` 的 template,但可以 require 主包、`packageA` 内的 template
`packageA` 无法使用 `packageB` 的资源,但可以使用主包、`packageA` 内的资源

原因: 分包是依赖主包运行的,所以主包是必然会被加载的,所以当分包引用主包的时候,主包的相关数据已经存在了,所以可以被引用。而分包不能引用其他分包的数据,也是因为加载顺序的问题。如果分包A引用分包B的数据,但分包B尚未被加载,则会出现引用不到数据的问题。


如果主包和分包同时使用了一个依赖,那么这个依赖会被打到哪里去?


会被打到主包


因为主包不能引用分包的资源,但是子包可以引用主包的资源,所以为了两个包都能引用到资源,只能打到主包中


比如以下情况
image.png


分包和主包同时使用了dayjs,那么这个依赖会被打入到主包中。


如果某一个依赖只在分包中使用呢?


如果某一个资源只在某一个分包中使用,那就会被打入到当前分包。


如果两个子包同时使用同一个资源呢?那资源会被打进哪里。


主包,因为两个子包的资源不能互相引用,所以与其给每一个子包都打入一个独立资源。小程序则会直接把资源打到主包中,这样,两个子包就都可以使用了。


分包需要担心低版本的兼容问题吗


不用


由微信后台编译来处理旧版本客户端的兼容,后台会编译两份代码包,一份是分包后代码,另外一份是整包的兼容代码。 新客户端用分包,老客户端还是用的整包,完整包会把各个 subpackage 里面的路径放到 pages 中。


独立分包


什么是独立分包


顾名思义,独立分包就是可以独立运行的分包。


举个例子,如果你的小程序启动页面是分包(普通分包)中的一个页面,那么小程序需要优先下载主包,然后再加载普通分包,因为普通分包依赖主包运行。但是如果小程序从独立分包进入进入小程序,则不需要下载主包,独立分包自己就可以运行。


普通分包所有的限制对独立分包都有效。


为什么要有独立分包,普通分包不够吗


因为独立分包不需要依赖主包,如果有作为打开小程序的的入口的必要,加载速度会比普通分包快,给客户的体验感更好。毕竟谁也不想打开一个页面等半天。


举个例子,如果小程序启动的时候是打开一个普通分包页面。则加载顺序是:加载主包 => 再加载当前分包


但如果小程序启动的时候是打开一个独立分包页面,则加载顺序是:直接加载独立分包,无需加载主包


独立分包相对于普通分包,就是省去了加载主包的时间和消耗。


独立分包如何配置


配置和普通分包一样,加一个independent属性设为true即可。


image.png


独立分包的缺点


既然独立分包可以不依赖主包,那我把每个分包都打成独立分包可以吗。


最好别那么干


理由有四点


1.独立分包因为不依赖主包,所以他不一定能获取到小程序层面的全局状态,比如getApp().也不是完全获取不到,主包被加载的时候还是可以获取到的。概率性出问题,最好别用。


2.独立分包不支持使用插件


3.小程序的公共文件不适用独立分包。比如Taro的app.less或小程序的app.wxss


上述三个,我觉的都挺麻烦的。所以不是作为入口包这种必要的情况下,确实没有使用独立分包的需求。


PS:一个小程序里可以有多个独立分包


独立分包有版本兼容问题吗


有滴,但你不用这个兼容问题直接让你报错
在低于 6.7.2 版本的微信中运行时,独立分包视为普通分包处理,不具备独立运行的特性。


所以,即使在低版本的微信中,也只是会编译成普通分包而已。


注意!!! 这里有一个可能会遇到的,就是如果你在独立分包中使用了app.wxss或者app.less这些小程序层面的公共css文件,那么在低版本(<6.7.2)进行兼容的时候,你就会发现,独立分包的页面会被这些全局的CSS影响。因为那时候独立分包被编译成了普通分包。而普通分包是适用全局公共文件的。


分包预下载


首先我们需要了解,分包是基本功能是,在下程序打包的时候不去加载分包,然后在进入当前分包页面的时候才开始下载分包。一方面目的是为了加快小程序的响应速度。另一方面的原因是避开微信小程序本身只能上传2M的限制。


这里有一个问题,就是我在首次跳转某个分包的某个页面的时候,出现短暂的白屏怎么办?(下载分包的时间+运行接口的时间+渲染视图的时间)。


后两者没法彻底避免,只能优化代码,第一个下载分包的时间可以使用分包预下载功能解决。


我们可以通过分包预下载在进入分包页面之前就开始下载分包,来减少进入分包页面的时间。


如何配置分包预下载


当前的分包预下载只能在app.config(Taro)或者app.json(原生小程序)通过preloadRule字段去配置。


preloadRule字段是一个对象,key是页面的路径,value是进行预加载的分包name或者key,__APP__代表主包


上案例


image.png


通过preloadRule字段去配置


”packageB/pages/user/index“是key


packages:["packageA"]是value


案例上的意思是当进入packageA分包的时候,开始下载分包packageB


如果要某一个分包在加载主包的就开始下载,那么就设置packages:["APP"]即可。


总结



  1. 分包是为了解决小程序超过2m无法上传的问题

  2. 分包依赖于主包,进入分包页面,主包必然需要优先被加在

  3. 主包和分包同时引用一个依赖或资源,则当前依赖或资源会被打入到主包

  4. 两个分包使用了同一个依赖或资源,则该依赖和资源会被打入到主包

  5. 某资源或依赖只在某一个分包中使用,则该资源和依赖会被打入到该分包中

  6. 独立分包的配置相对于普通分包只是多了一个independent字段,设置为true

  7. 独立分包无需依赖主包,可独立加载。

  8. 独立分包中谨慎使用全局属性,最好别用,可能获取不到

  9. 分包可以被预加载,用于解决进入分包页面时才开始加载分包导致页面可能出现的(取决于加载速度)短暂白屏的问题。


分包官方文档


分包官方分包demo-小程序版


如果您认为对您有用的话,留个赞或收藏一下吧~


image.png


作者:工边页字
来源:juejin.cn/post/7321049399281958922
收起阅读 »

现代 CSS 解决方案:文字颜色自动适配背景色!

web
在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色。简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B。...
继续阅读 »

在 23 年的 CSS 新特性中,有一个非常重要的功能更新 -- 相对颜色

简单而言,相对颜色的功能,让我们在 CSS 中,对颜色有了更为强大的掌控能力。

其核心功能就是,让我们能够基于一个现有颜色 A,通过一定的转换规则,快速生成我们想要的颜色 B

其功能能够涵盖:

完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

当然,今天我们不会一个一个去过这些功能,更多的时候,我们只需要知道我们能够实现这些功能。

本文,我们将从实际实用角度出发,基于实际的案例,看看 CSS 相对颜色,能够如何解决我们的一些实际问题。

快速语法入门

首先,我们通过一张图,一个案例,快速入门 CSS 相对颜色语法:

相对颜色语法的目标是允许从另一种颜色派生颜色。

上图显示了将原始颜色 green 转换为新颜色的颜色空间后,该颜色会转换为以 r、g、b 和 alpha 变量表示的各个数字,这些数字随后会直接用作新的 rgb() 颜色的值。

举个例子:

<p> CSS Relative Color p>
p {
color: rgb(255, 0, 0);
}

实现一个 color 为红色(rgb 值为 rgb(255, 0, 0))的字体:

基于上面的相对颜色语法,我如何通过一个红色生成绿色文字呢?示意如下:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}

效果如下,我们就得到绿色字体:

解释一下:

  1. 原本的红色颜色,我们把它设置为 CSS 变量 --color: rgb(255, 0, 0)
  2. 想通过红色得到绿色,对于红色的 rgb 值 rgb(255, 0, 0) 而言,需要转换成 rgb(0, 255, 0)
  3. 使用 CSS 相对颜色语法,就是 rgb(from var(--color) calc(r - 255) calc(g + 255) b)

通过这个 DEMO,我们把几个核心基础语法点学习一下:

  1. from 关键字

from 关键字,它是相对颜色的核心。它表示会将 from 关键字后的颜色定义转换为相对颜色!在 from 关键字后面,CSS 会期待一种颜色,即能够启发生成另一种颜色

  1. from 关键字 后的颜色表示,支持不同颜色表示或者是 CSS 变量

第二个关键点,from 后面通常会接一个颜色值,这个颜色值可以是任意颜色表示法,或者是一个 CSS 变量,下面的写法都是合法的:

p {
color: rgba(from #ff0000) r g b);
color: rgb(from rgb(255, 0, 0) r g b);
color: rgb(from hsl(0deg, 100%, 50%) r g b);
color: rgb(from var(--hotpink) r g b);
}
  1. 对转换后的变量使用 calc() 或其他 CSS 函数

另外一个非常重要的基础概念就是,我们可以对 (from color r g b) 后的转换变量 r g b 使用 calc() 或其他 CSS 函数。

就是我们上面的例子:

p {
--color: rgb(255, 0, 0);
color: rgb(from var(--color) calc(r - 255) calc(g + 255) b); /* result = rgb(0, 255, 0) */
}
  1. 相对颜色语法支持,各种颜色表示函数:

相对颜色的基础的使用规则就是这样,它不仅支持 rgb 颜色表示法,它支持所有的颜色表示法:

使用 CSS 相对颜色,实现统一按钮点击背景切换

通常页面上的按钮,都会有 hover/active 的颜色变化,以增强与用户的交互。

像是这样:

最常见的写法,就是我们需要在 Normal 状态、Hover 状态、Active 状态下写 3 种颜色:

p {
color: #ffcc00;
transition: .3s all;
}
/* Hover 伪类下为 B 颜色 */
p:hover {
color: #ffd21f;
}
/** Active 伪类下为 C 颜色 **/
p:active {
color: #ab8a05;
}

在之前,我们介绍过一种利用滤镜 filter: contrast() 或者 filter: brightness() 的统一解决方案,无需写多个颜色值,可以根据 Normal 状态下的色值,通过滤镜统一实现更亮、或者更暗的伪类颜色。

在今天,我们也可以利用 CSS 相对颜色来做这个事情:

div {
--bg: #fc0;
background: var(--bg);
transition: .3s all;
}

div:hover {
background: hsl(from var(--bg) h s calc(l * 1.2));
}
div:active {
background: hsl(from var(--bg) h s calc(l * 0.8));
}

我们通过 hsl 色相、饱和度、亮度颜色表示法表示颜色。实现:

  1. 在 :hover 状态下,根据背景色,将背景亮度 l 调整为原背景色的 1.2 倍
  2. 在 :avtive 状态下,根据背景色,将背景亮度 l 调整为原背景色的 0.8 倍

在实际业务中,这是一个非常有用的用法。

完整的 DEMO,你可以戳这里:CodePen Demo -- https://codepen.io/Chokcoco/pen/KKEdOeb

使用 CSS 相对颜色,实现文字颜色自适应背景

相对颜色,还有一个非常有意思的场景 -- 让文字颜色能够自适应背景颜色进行展示。

有这么一种场景,有的时候,无法确定文案的背景颜色的最终表现值(因为背景颜色的值可能是后台配置,通过接口传给前端),但是,我们又需要能够让文字在任何背景颜色下都正常展现(譬如当底色为黑色时文字应该是白色,当背景为白色时,文字应该为黑色)。

像是这样:

在不确定背景颜色的情况下,无论什么情况,文字颜色都能够适配背景的颜色。

在之前,纯 CSS 没有特别好的方案,可以利用 mix-blend-mode: difference 进行一定程度的适配:

div {
// 不确定的背景色
}
p {
color: #fff;
mix-blend-mode: difference;
}

实操过这个方案的同学都会知道,在一定情况下,前景文字颜色还是会有一点瑕疵。并且,混合模式这个方案最大的问题是会影响清晰度

有了 CSS 相对颜色后,我们有了更多的纯 CSS 方案。

利用 CSS 相对颜色,反转颜色

我们可以利用相对颜色的能力,基于背景色颜色进行反转,赋值给 color。

一种方法是将颜色转换为 RGB,然后从 1 中减去每个通道的值。

代码非常简单:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: rgb(from var(--bg) calc(1 - r) calc(1 - g) calc(1 - b)); /** 基于背景反转颜色 **/
}

用 1 去减,而不是用 255 去,是因为此刻,会将 rgb() 表示法中的 0~255 映射到 0~1

效果如下:

配个动图,我们利用背景色的反色当 Color 颜色,适配所有背景情况:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

当然,这个方案还有两个问题:

  1. 如果颜色恰好是在 #808080 灰色附近,它的反色,其实还是它自己!会导致在灰色背景下,前景文字不可见;
  2. 绝大部分情况虽然可以正常展示,但是并不是非常美观好看

为了解决这两个问题,CSS 颜色规范在 CSS Color Module Level 6 又推出了一个新的规范 -- color-contrast()

利用 color-contrast(),选择高对比度颜色

color-contrast() 函数标记接收一个 color 值,并将其与其他的 color 值比较,从列表中选择最高对比度的颜色。

利用这个 CSS 颜色函数,可以完美的解决上述的问题。

我们只需要提供 #fff 白色和 #000 黑色两种可选颜色,将这两种颜色和提供的背景色进行比较,系统会自动选取对比度更高的颜色。

改造一下,上面的代码,它就变成了:

p {
/** 任意背景色 **/
--bg: #ffcc00;
background: var(--bg);

color: color-contrast(var(--bg) vs #fff, #000); /** 基于背景色,自动选择对比度更高的颜色 **/
}

这样,上面的 DEMO 最终效果就变成了:

完整的 DEMO 和代码,你可以戳这里:CodePen Demo -- CSS Relatvie Color Adapt BG

此方案的优势在于:

  1. 可以限定前景 color 颜色为固定的几个色值,以保证 UI 层面的统一及美观
  2. 满足任何情况下的背景色

当然,唯一限制这个方案的最大问题在于,当前,color-contrast 还只是一个实验室功能,未大规模被兼容。

总结一下

到今天,我们可以利用 CSS 提供的各类颜色函数,对颜色有了更为强大的掌控力。

很多交互效果,不借助 JavaScript 的运算,也能计算出我们想要的最终颜色值。本文简单的借助:

  1. 使用 CSS 相对颜色,实现统一按钮点击背景切换
  2. 使用 CSS 相对颜色,实现文字颜色自适应背景

两个案例,介绍了 CSS 相对颜色的功能。但它其实还有更为广阔的应用场景,完整的教程,你可以看这里 -- Chrome for Developers- CSS 相对颜色语法

最后

好了,本文到此结束,希望本文对你有所帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:Chokcoco
来源:juejin.cn/post/7321410822789742618
收起阅读 »

产品经理:“一个简单的复制功能也能写出bug?”

web
问题 刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。 我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在...
继续阅读 »

问题


刚入职时,遇到了一个线上 bug,用户点击复制按钮没办法复制文本,产品经理震怒,“这么简单的一个功能也能出问题?当时是谁验收的?”,因为我刚来还闲着,就把我派去解决这个问题。


我在排查问题时,发现该复制方法写在了一个自定义 hook 中(这里复制方法写在 hook 里没啥意义,但是乙方交付过来的代码好像特别喜欢把工具函数写成个 hook 来用),点进去查看就是简单的一个 navigator.clipboard.writeText()的方法,本地运行我又能复制成功。于是我怀疑是手机浏览器不支持这个 api 便去搜索了一下。


Clipboard


MDN 上的解释:


剪贴板 Clipboard APINavigator 接口添加了只读属性 clipboard,该属性返回一个可以读写剪切板内容的 Clipboard 对象。在 Web 应用中,剪切板 API 可用于实现剪切、复制、粘贴的功能。


只有在用户事先授予网站或应用对剪切板的访问许可之后,才能使用异步剪切板读写方法。许可操作必须通过取得权限 Permissions API (en-US)"clipboard-read" 和/或 "clipboard-write" 项获得。


浏览器兼容性


image.png


使用 document.execCommand() 降级处理


这里我也不清楚用户手机浏览器的版本是多少,那么这个 api 出现之前,是用的什么方法呢?总是可以 polyfill 降级处理的吧!于是我就查到了document.execCommand()这个方法:



  • document.execCommand("copy") : 复制;

  • document.execCommand("cut") : 剪切;

  • document.execCommand("paste") : 粘贴。


对比


Clipboard 的所有方法都是异步的,返回 Promise 对象,复制较大数据时不会造成页面卡顿。但是其支持的浏览器版本较新,且只允许 https 和 localhost 这些安全网络环境可以使用,限制较多。


document.execCommand() 限制较少,使用起来相对麻烦。但是 MDN 上提到该 api 已经废弃:


image.png


image.png


浏览器很可能在某个版本弃用该 api ,不过当前 2023/12/29 ,该复制 api 还是可以正常使用的。


具体代码修改


于是我修改了一下原来的 hook:


import Toast from "~@/components/Toast";

export const useCopy = () => {

const copy = async (text: string, toast?: string) => {

const fallbackCopyTextToClipboard = (text: string, toast?: string) => {
let textArea = document.createElement("textarea");
textArea.value = text;

// Avoid scrolling to bottom
textArea.style.top = "-200";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0"

document.body.appendChild(textArea);
// textArea.focus();
textArea.select();
let msg;
try {
let successful = document.execCommand("copy");
msg = successful ? toast ? toast : "复制成功" : "复制失败";
} catch (err) {
msg = "复制失败";
}
Toast.dispatch({
content: msg,
});
document.body.removeChild(textArea);
};

const copyTextToClipboard = (text: string, toast?: string) => {
if (!navigator.clipboard || !window.isSecureContext) {
fallbackCopyTextToClipboard(text, toast);
return;
}
navigator.clipboard
.writeText(text)
.then(() => {
Toast.dispatch({
content: toast ? toast : "复制成功",
});
})
.catch(() => {
fallbackCopyTextToClipboard(text, toast)
});
};
copyTextToClipboard(text, toast);
};

return copy;
};

上线近一年,这个复制方法没出现异常问题。


作者:HyaCinth
来源:juejin.cn/post/7317577665014448167
收起阅读 »

MyBatis实战指南(一):从概念到特点,助你快速上手,提升开发效率!

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集的过程。

大家好,今天我们要来聊聊一个在Java开发中非常实用的框架——MyBatis。你是否曾经因为数据库操作而感到困扰?是否曾经因为SQL语句的编写而烦恼?那么,MyBatis或许就是你的救星。

接下来,让我们一起来了解一下MyBatis的概念与特点吧!

一、MyBatis基本概念

MyBatis 是一款优秀的半自动的ORM持久层框架,它支持自定义 SQL、存储过程以及高级映射。

MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。

MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

那么,什么是ORM?

要了解ORM,先了解下面概念:

持久化

把数据(如内存中的对象)保存到可永久保存的存储设备中。持久化的主要应用是将内存中的数据存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。

持久层

即专注于实现数据持久化应用领域的某个特定系统的一个逻辑层面,将数据使用者和数据实体相关联。

ORM, 即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射。这样在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。

Description

总结:

  • 它是一种将内存中的对象保存到关系型数据库中的技术;

  • 主要负责实体对象的持久化,封装数据库访问细节;

  • 提供了实现持久化层的另一种模式,采用映射元数据(XML)来描述对象-关系的映射细节,使得ORM中间件能在任何一个Java应用的业务逻辑层和数据库之间充当桥梁。

Java典型的ORM框架:

  • hibernate:全自动的框架,强大、复杂、笨重、学习成本较高;

  • Mybatis:半自动的框架, 必须要自己写sql;

  • JPA:JPA全称Java Persistence API、JPA通过JDK 5.0注解或XML描述对象-表的映射关系,是Java自带的框架。

二、Mybatis的作用

Mybatis是一个Java持久层框架,它主要用于简化与数据库的交互操作。Mybatis的主要作用有以下几点:

  • 将Java对象与数据库表进行映射,通过配置XML文件实现SQL语句的定义和执行,使得开发者可以专注于业务逻辑的实现而无需编写繁琐的JDBC代码。

  • 提供了灵活的SQL映射功能,可以根据需要编写动态SQL,支持复杂的查询条件和更新操作。

  • 支持事务管理,可以确保数据的一致性和完整性。

  • 提供了缓存机制,可以提高数据库查询性能。

  • 可以与Spring、Hibernate等其他框架无缝集成,方便开发者在项目中使用。

Mybatis就是帮助程序员将数据存取到数据库里面。传统的jdbc操作,有很多重复代码块比如: 数据取出时的封装, 数据库的建立连接等等,通过框架可以减少重复代码,提高开发效率 。

MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射。

所有的事情,不用Mybatis依旧可以做到,只是用了它,会更加方便更加简单,开发更快速。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

三、MyBatis特点

1、定制化SQL

同为持久层框架的Hibernate,对操作数据库的支持方式较多,完全面向对象的、原生SQL的和HQL的方式。MyBatis只支持原生的SQL语句,这个“定制化”是相对Hibernate完全面向对象的操作方式的。

2、存储过程

储存过程是实现某个特定功能的一组sql语句集,是经过编译后存储在数据库中。当出现大量的事务回滚或经常出现某条语句时,使用存储过程的效率往往比批量操作要高得多。

MyBatis是支持存储过程的,可以看个例子。假设有一张表student:

create table student
(
id bigint not null,
name varchar(30),
sex char(1),
primary key (id)
);

有一个添加记录的存储过程:

create procedure pro_addStudent (IN id bigint, IN name varchar(30), IN sex char(1))
begin
insert into student values (id, name, sex);
end

此时就可以在mapper.xml文件中调用存储过程:

<!-- 调用存储过程 -->
<!-- 第一种方式,参数使用parameterType -->
<select id="findStudentById" parameterType="java.lang.Long" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(#{id,jdbcType=BIGINT,mode=IN})}
</select>

<parameterMap type="java.util.Map" id="studentMap">
<parameter property="id" mode="IN" jdbcType="BIGINT"/>
</parameterMap>

<!-- 调用存储过程 -->
<!-- 第二种方式,参数使用parameterMap -->
<select id="findStudentById" parameterMap="studentMap" statementType="CALLABLE"
resultType="com.mybatis.entity.Student">
{call pro_getStudent(?)}
</select>

3、高级映射

可以简单理解为支持关联查询。

4、避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

使用Mybatis时,数据库的连接配置信息,是在mybatis-config.xml文件中配置的。同时,获取查询结果的代码,也是尽量做到了简洁。以模糊查询为例,需要做两步工作:

1)首先在配置文件中写上SQL语句,示例:

 <mapper namespace="com.test.pojo">
<select id="listCategoryByName" parameterType="string" resultType="Category">
select * from category_ where name like concat('%',#{0},'%')
</select>
</mapper>

2)在Java代码中调用此语句,示例:

        List<Category> cs = session.selectList("listCategoryByName","cat");
for (Category c : cs) {
System.out.println(c.getName());
}

5、Mybatis中ORM的映射方式也是比较简单的

"resultType"参数的值指定了SQL语句返回对象的类型。示例代码:
<mapper namespace="com.test.pojo">
<select id="listCategory" resultType="Category">
select * from category_
</select>
</mapper>

四、Mybatis的适用场景

MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。MyBatis因其简单易用、灵活高效的特点,广泛应用于各种Java项目中。

以下是一些常见的应用场景:

  • 数据查询:MyBatis可以执行复杂的SQL查询,返回Java对象或者结果集。

  • 数据插入、更新和删除:MyBatis可以执行INSERT、UPDATE和DELETE等SQL语句。

  • 存储过程和函数调用:MyBatis可以调用数据库的存储过程和函数。

  • 高级映射:MyBatis支持一对一、一对多、多对一等复杂关系的映射。

  • 懒加载:MyBatis支持懒加载,只有在真正需要数据时才会去数据库查询。

  • 缓存机制:MyBatis内置了一级缓存和二级缓存,可以提高查询效率。

为什么说Mybatis是半自动ORM映射工具

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。

而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

MyBatis作为半自动ORM映射工具与全自动ORM工具相比,有几个主要的区别点:

1.SQL的灵活性

MyBatis作为半自动ORM映射工具,开发人员可以灵活地编写SQL语句,充分发挥数据库的特性和优势。而全自动ORM工具通常会在一定程度上限制开发人员对SQL的灵活控制。

2.映射关系的可定制性

MyBatis允许开发人员通过配置文件(或注解)自定义对象和数据库表之间的映射关系,可以满足各种复杂的映射需求。而全自动ORM工具通常根据约定和规则自动生成映射关系,对于某些特殊需求无法满足。

3.SQL的可复用性

MyBatis支持SQL的可复用性,可以将常用的SQL语句定义为独立的SQL片段,并在需要的地方进行引用。而全自动ORM工具通常将SQL语句直接与对象的属性绑定在一起,缺乏可复用性。

4.性能调优的灵活性

MyBatis作为半自动ORM映射工具,允许开发人员对SQL语句进行灵活的调优,通过手动编写SQL语句和使用高级特性进行性能优化。而全自动ORM工具通常将性能优化的控制权交给框架,开发人员无法灵活地对SQL进行调优。

MyBatis作为一种半自动ORM映射工具,相对于全自动ORM工具具有更高的灵活性和可定制性。通过灵活的SQL控制、自定义的映射关系、可复用的SQL以及灵活的性能调优,MyBatis可以满足各种复杂的映射需求和性能优化需求。

虽然MyBatis相对于全自动ORM工具需要开发人员编写更多的SQL语句,但正是由于这种半自动的特性,使得MyBatis在某些复杂场景下更加灵活和可控。

因此,我们可以说MyBatis是一种半自动ORM映射工具,与全自动的ORM工具相比,它更适用于那些对SQL灵活性和性能调优需求较高的场景。

五、Mybatis的优缺点

Mybatis有以下优点:

1.基于SQL语句编程,相当灵活

SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。

2. 代码量少

与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接。

3.很好的与各种数据库兼容

4.数据库字段和对象之间可以有映射关系

提供映射标签,支持对象与数据库的ORM字段关系映射。

5.能够与Spring很好的集成

Mybatis有以下缺点:

1.SQL语句的编写工作量较大

尤其当字段多、关联表多时,SQL语句较复杂。

2.数据库移植性差

SQL语句依赖于数据库,不能随意更换数据库(可以通过在mybatis-config.xml配置databaseIdProvider来弥补)。

示例:

 <databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>

然后在xml文件中,就可以针对不同的数据库,写不同的sql语句。

3.字段映射标签和对象关系映射标签仅仅是对映射关系的描述,具体实现仍然依赖于sql。

示例:

public class Student{
String name;
List<Interest> interests;
}

public class Interest{
String studentId;
String name;
String direction;
}

<resultMap id="ResultMap" type="com.test.Student">
<result column="name" property="name" />
<collection property="interests" ofType="com.test.Interest">
<result column="name" property="name" />
<result column="direction" property="direction" />
</collection>
</resultMap>

在该例子中,如果查询sql中,没有关联Interest对应的表,则查询出数据映出的Student对象中,interests属性值就会为空。

4.DAO层过于简单,对象组装的工作量较大

即Mapper层Java代码过少,XxxMapper.xml文件中维护数据库字段和实体类字段的工作量较大。

5.不支持级联更新、级联删除

仍以上面的Student和Interest为例,当要更新/删除某个Student的信息时,需要在两个表进行手动更新/删除。

通过以上的介绍,相信大家对MyBatis已经有了更深入的了解。MyBatis是一个非常强大的持久层框架,它的灵活性、易用性、解耦性、高效性和全面性都使得它在Java开发中得到了广泛的应用。

收起阅读 »

啊?两个vite项目怎么共用一个端口号啊

web
问题: 最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后: ...
继续阅读 »

问题:


最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后:


image.png


该项目的端口号为5173,但是此时我再次通过vite的官方搭建一个react+ts+vite项目:npm create vite@latest react_demos -- --template react-ts,之后通过npm run dev启动项目,发现端口号并没有更新:


image.png


这是什么原因呢?


寻因:


查阅官方文档,我发现:


image.png


那么我主动在vite.config.ts中添加这个配置:


image.png


正常来说,会出现这个报错:


image.png


但是此时结果依然为:


image.png


我百思不得不得其解,于是再次查阅官方文档:


image.png
我寻思这也与文档描述不一致啊,于是我再次尝试,思考是不是vite版本号的问题,两个项目的版本号分别为:


image.png


image.png


我决定创建一个4版本的项目npm create vite@^4.1.4 react_demos3 -- --template react-ts


image.png


结果发现,还是有这个问题,跟版本号没有关系,于是我又耐心继续看官方文档,看到了这个配置:


image.png
我抱着试试的态度,在其中一个vite项目中添加这个配置:


image.png


发现,果然是这个配置的锅,当其中一个项目host配置为0.0.0.0时,vite不会自动尝试更新端口号


难道vite的端口监测机制与host也有关?


结果:


不甘心的我再次进行尝试,将两个项目的host都设置成:


image.png


image.png


vite会自动尝试更新端口号


原来如此,vite的端口号检测机制在对比端口号之前,会先对比host,由于我的微前端项目中设置了host,而新建的项目中没有设置host,新建的项目host默认值为localhost对比不成功,vite不会自动尝试下一个可用端口,而是共用一个端口


总结:


在遇到问题时,要多多去猜,去想各种可能,并且最重要的是去尝试各种可能,还要加上积极去翻阅官方文档,问题一定会得到解决的;哪怕不能解决,那也会在尝试中,学到很多东西


作者:进阶的鱼
来源:juejin.cn/post/7319699173740363802
收起阅读 »

终于搞懂了网盘网页是怎么唤醒本地应用了

web
写在前面 用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。 自定义协议 本身单凭浏览器是没有唤醒本地应用这个能力的,...
继续阅读 »

写在前面


用百度网盘举例,可以通过页面打开本机的百度网盘软件,很多软件的网站页面都有这个功能。这个事情一直令我比较好奇,这次终于有空抽时间来研究研究了,本篇讲的是Windows的,mac的原理与之类似。


自定义协议


本身单凭浏览器是没有唤醒本地应用这个能力的,不然随便一个网页都能打开你的所有应用那不就乱套了吗。但是电脑系统本身又可以支持这个能力,就是通过配置自定义协议。


举个例子,当你用浏览器打开一个本地的PDF的时候,你会发现上面是file://path/xxx.pdf,这就是系统内置的一个协议,浏览器可以调用这个协议进行文件读取。


那么与之类似的,windows本身也支持用户自定义协议来进行一些操作的,而这个协议就在注册表中进行配置。


配置自定义协议


这里我用VS Code来举例子,最终我要实现通过浏览器打开我电脑上的VS Code。


我们先编写一个注册表文件


Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\vscode]
@="URL:VSCode Protocol"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\vscode\shell]

[HKEY_CLASSES_ROOT\vscode\shell\open]

[HKEY_CLASSES_ROOT\vscode\shell\open\command]
@=""D:\VScode\Microsoft VS Code\Code.exe" "%1""

这里我逐行解释



  1. Windows Registry Editor Version 5.00 这行表明该文件是一个 Windows 注册表编辑器文件,这是标准的头部,用于告诉 Windows 如何解析文件。

  2. [HKEY_CLASSES_ROOT\vscode] 这是一个注册表键的开始。在这里,\vscode 表示创建一个名为 vscode 的新键。

  3. @="URL:VSCode Protocol"vscode 键下,这行设置了默认值(表示为 @ ),通过 "URL:VSCode Protocol" 对这个键进行描述。

  4. "URL Protocol"="" 这行是设置一个名为 URL Protocol 的空字符串值。这是代表这个新键是一个 URI 协议。

  5. [HKEY_CLASSES_ROOT\vscode\shell] 创建一个名为 shell 的子键,这是一个固定键,代表GUI界面的处理。

  6. [HKEY_CLASSES_ROOT\vscode\shell\open]shell 下创建一个名为 open 的子键。这耶是一个固定键,open 是一个标准动作,用来执行打开操作。

  7. [HKEY_CLASSES_ROOT\vscode\shell\open\command]open 下创建一个名为 command 的子键。这是一个固定键,指定了当协议被触发时要执行命令。

  8. @=""D:\VScode\Microsoft VS Code\Code.exe" "%1""command 键下,设置默认值为 VSCode 的路径。 "%1" 是一个占位符,用于表示传递给协议的任何参数,这里并无实际用处。


写好了注册表文件后,我们将其保存为 vscode.reg,并双击执行,对话框选择是,相应的注册表信息就被创建出来了。



可以通过注册表中查看。


浏览器打开VS Code


这时,我们打开浏览器,输入 vscode://open



可以看到,就像百度网盘一样,浏览器弹出了询问对话框,然后就可以打开VS Code了。


如果想要在网页上进行打开,也简单


<script>
function openVSCode() {
window.location.href = 'vscode://open/';
}
</script>
<button onclick="openVSCode()">打开 VSCode</button>

写一个简单的JS代码即可。


写在最后


至此,终于是了解了这方面的知识。这就是说,在网盘安装的过程中,就写好了这个注册表文件,自定义了网盘的唤醒协议,才可以被识别。


而我也找到了这个注册表



原来叫baiduyunguanjia协议(不区分大小写),使用 baiduyunguanjia://open 可以打开。


作者:银空飞羽
来源:juejin.cn/post/7320513026188460067
收起阅读 »

前端无感知刷新token & 超时自动退出

web
前端无感知刷新token&超时自动退出 一、token的作用 因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。 以oauth2.0授权码模式为例: 每次请求资...
继续阅读 »

前端无感知刷新token&超时自动退出


一、token的作用


因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。


以oauth2.0授权码模式为例:


oauth2授权码模式.png


每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。


刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。


二、无感知刷新token方案


2.1 刷新方案


当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。


如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。


2.2 原生AJAX请求


2.2.1 http工厂函数


function httpFactory({ method, url, body, headers, readAs, timeout }) {
   const xhr = new XMLHttpRequest()
   xhr.open(method, url)
   xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60

   if(headers){
       forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value))
  }
   
   const HTTPPromise = new Promise((resolve, reject) => {
       xhr.onload = function () {
           let response;

           if (readAs === 'json') {
               try {
                   response = JSONbig.parse(this.responseText || null);
              } catch {
                   response = this.responseText || null;
              }
          } else if (readAs === 'xml') {
               response = this.responseXML
          } else {
               response = this.responseText
          }

           resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
      }

       xhr.onerror = function () {
           reject(xhr)
      }
       xhr.ontimeout = function () {
           reject({ ...xhr, isTimeout: true })
      }

       beforeSend(xhr)

       body ? xhr.send(body) : xhr.send()

       xhr.onreadystatechange = function () {
           if (xhr.status === 502) {
               reject(xhr)
          }
      }
  })

   // 允许HTTP请求中断
   HTTPPromise.abort = () => xhr.abort()

   return HTTPPromise;
}

2.2.2 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

// 存放因token过期而失败的请求
let requests = []

function httpRequest(config) {
   let abort
   let process = new Promise(async (resolve, reject) => {
       const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }})
       abort = request.abort
       
       try {                            
           const { status, response, getResponseHeader } = await request

           if(status === 401) {
               try {
                   if (!isRefreshing) {
                       isRefreshing = true
                       
                       // 刷新token
                       await refreshToken()

                       // 按顺序重新发起所有失败的请求
                       const allRequests = [() => resolve(httpRequest(config)), ...requests]
                       allRequests.forEach((cb) => cb())
                  } else {
                       // 正在刷新token,将请求暂存
                       requests = [
                           ...requests,
                          () => resolve(httpRequest(config)),
                      ]
                  }
              } catch(err) {
                   reject(err)
              } finally {
                   isRefreshing = false
                   requests = []
              }
          }                        
      } catch(ex) {
           reject(ex)
      }
  })
   
   process.abort = abort
   return process
}

// 发起请求
httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })

2.3 Axios 无感知刷新token


// 是否正在刷新token的标记
let isRefreshing = false

let requests: ReadonlyArray<(config: any) => void> = []

// 错误响应拦截
axiosInstance.interceptors.response.use((res) => res, async (err) => {
   if (err.response && err.response.status === 401) {
       try {
           if (!isRefreshing) {
               isRefreshing = true
               // 刷新token
               const { access_token } = await refreshToken()

               if (access_token) {
                   axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;

                   requests.forEach((cb) => cb(access_token))
                   requests = []

                   return axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${access_token}`,
                      },
                  })
              }

               throw err
          }

           return new Promise((resolve) => {
               // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
               requests = [
                   ...requests,
                  (token) => resolve(axiosInstance.request({
                       ...err.config,
                       headers: {
                           ...(err.config.headers || {}),
                           Authorization: `Bearer ${token}`,
                      },
                  })),
              ]
          })
      } catch (e) {
           isRefreshing = false
           throw err
      } finally {
           if (!requests.length) {
               isRefreshing = false
          }
      }
  } else {
       throw err
  }
})

三、长时间无操作超时自动退出


当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。


3.1 操作事件


操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。


特殊事件:某些耗时的功能,比如上传、下载等。


3.2 方案


用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。


在 localStorage 存入两个字段:


名称类型说明说明
lastActiveTimestring最后一次触发操作事件的时间戳
activeEventsstring[ ]特殊事件名称数组

当有操作事件时,将当前时间戳存入 lastActiveTime。


当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。


设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。


3.3 代码实现


const LastTimeKey = 'lastActiveTime'
const activeEventsKey = 'activeEvents'
const debounceWaitTime = 2 * 1000
const IntervalTimeOut = 1 * 60 * 1000

export const updateActivityStatus = debounce(() => {
   localStorage.set(LastTimeKey, new Date().getTime())
}, debounceWaitTime)

/**
* 页面超时未有操作事件退出登录
*/

export function timeout(keepTime = 60) {
   document.addEventListener('mousedown', updateActivityStatus)
   document.addEventListener('mouseover', updateActivityStatus)
   document.addEventListener('wheel', updateActivityStatus)
   document.addEventListener('keydown', updateActivityStatus)

   // 定时器
   let timer;

   const doTimeout = () => {
       timer && clearTimeout(timer)
       localStorage.remove(LastTimeKey)
       document.removeEventListener('mousedown', updateActivityStatus)
       document.removeEventListener('mouseover', updateActivityStatus)
       document.removeEventListener('wheel', updateActivityStatus)
       document.removeEventListener('keydown', updateActivityStatus)

       // 注销token,清空session,回到登录页
       logout()
  }

   /**
    * 重置定时器
    */

   function resetTimer() {
       localStorage.set(LastTimeKey, new Date().getTime())

       if (timer) {
           clearInterval(timer)
      }

       timer = setInterval(() => {
           const isSignin = document.cookie.includes('access_token')
           if (!isSignin) {
               doTimeout()
               return
          }

           const activeEvents = localStorage.get(activeEventsKey)
           if(!isEmpty(activeEvents)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }
           
           const lastTime = Number(localStorage.get(LastTimeKey))

           if (!lastTime || Number.isNaN(lastTime)) {
               localStorage.set(LastTimeKey, new Date().getTime())
               return
          }

           const now = new Date().getTime()
           const time = now - lastTime

           if (time >= keepTime) {
               doTimeout()
          }
      }, IntervalTimeOut)
  }

   resetTimer()
}

// 上传操作
function upload() {
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, [...current, 'upload'])
   ...
   // do upload request
   ...
   const current = JSON.parse(localStorage.get(activeEventsKey))
   localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
}

作者:ww_怒放
来源:juejin.cn/post/7320044522910269478
收起阅读 »

解决扫码枪因输入法中文导致的问题

web
问题 最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题 思考 这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。...
继续阅读 »

问题


最近公司项目上遇到了扫码枪因搜狗/微软/百度/QQ等输入法在中文状态下,使用扫码枪扫码会丢失字符的问题


思考


这种情况是由于扫码枪的硬件设备,在输入的时候,是模拟用户键盘的按键来实现的字符输入的,所以会触发输入法的中文模式,并且也会触发输入法的自动联想。那我们可以针对这个来想解决方案。


方案一


首先想到的第一种方案是,监听keydown的键盘事件,创建一个字符串数组,将每一个输入的字符进行比对,然后拼接字符串,并回填到输入框中,下面是代码:


function onKeydownEvent(e) {
this.code = this.code || ''
const shiftKey = e.shiftKey
const keyCode = e.code
const key = e.key
const arr = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-']
this.nextTime = new Date().getTime()
const timeSpace = this.nextTime - this.lastTime
if (key === 'Process') { // 中文手动输入
if (this.lastTime !== 0 && timeSpace <= 30) {
for (const a of arr) {
if (keyCode === 'Key' + a) {
if (shiftKey) {
this.code += a
} else {
this.code += a.toLowerCase()
}
this.lastTime = this.nextTime
} else if (keyCode === 'Digit' + a) {
this.code += String(a)
this.lastTime = this.nextTime
}
}
if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething....
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
}
}
} else {
if (arr.includes(key.toUpperCase())) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
// 30ms以内来区分是扫码枪输入,正常手动输入时少于30ms的
this.code += key
}
this.lastTime = this.nextTime
} else if (arr.includes(key)) {
if (this.lastTime === 0 && timeSpace === this.nextTime) {
this.code = key
} else if (this.lastTime !== 0 && timeSpace <= 30) {
this.code += String(key)
}
this.lastTime = this.nextTime
} else if (keyCode === 'Enter' && timeSpace <= 30) {
if (String(this.code)) {
// TODO
dosomething()
}
this.code = ''
this.nextTime = 0
this.lastTime = 0
} else {
this.lastTime = this.nextTime
}
}
}


这种方案能解决部分问题,但是在不同的扫码枪设备,以及不同输入法的情况下,还是会出现丢失问题


方案二


使用input[type=password]来兼容不同输入的中文模式,让其只能输入英文,从而解决丢失问题


这种方案网上也有不少的参考

# 解决中文状态下扫描枪扫描错误

# input type=password 取消密码提示框


使用password密码框确实能解决不同输入法的问题,并且Focus到输入框,输入法会被强制切换为英文模式


添加autocomplete="off"autocomplete="new-password"属性


官方文档:
# 如何关闭表单自动填充


但是在Chromium内核的浏览器,不支持autocomplete="off",并且还是会出现这种自动补全提示:


image.png


上面的属性并没有解决浏览器会出现密码补全框,并且在输入字符后,浏览器还会在右上角弹窗提示是否保存:


image.png


先解决密码补全框,这里我想到了一个属性readonly,input原生属性。input[type=password]readonly
时,是不会有密码补全的提示,并且也不会弹窗提示密码保存。


那好,我们就可以在输入前以及输入完成后,将input[type=password]立即设置成readonly


但是需要考虑下面几种情况:



  • 获取焦点/失去焦点时

  • 当前输入框已focus时,再次鼠标点击输入框

  • 扫码枪输出完成最后,输入Enter键时,如果清空输入框,这时候也会显示自动补全

  • 清空输入框时

  • 切换离开页面时


这几种情况都需要处理,将输入框变成readonly


我用vue+element-ui实现了一份,贴上代码:


<template>
<div class="scanner-input">
<input class="input-password" :name="$attrs.name || 'one-time-code'" type="password" autocomplete="off" aria-autocomplete="inline" :value="$attrs.value" readonly @input="onPasswordInput">
<!-- <el-input ref="scannerInput" v-bind="$attrs" v-on="$listeners" @input="onInput"> -->
<el-input ref="scannerInput" :class="{ 'input-text': true, 'input-text-focus': isFocus }" v-bind="$attrs" v-on="$listeners">
<template v-for="(_, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
<!-- <slot slot="suffix" name="suffix"></slot> -->
</el-input>
</div>
</template>

<script>
export default {
name: 'WispathScannerInput',
data() {
return {
isFocus: false
}
},
beforeDestroy() {
this.$el.firstElementChild.setAttribute('readonly', true)
this.$el.firstElementChild.removeEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.removeEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.removeEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.removeEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.removeEventListener('keydown', this.oPasswordKeyDown)
},
mounted() {
this.$el.firstElementChild.addEventListener('focus', this.onPasswordFocus)
this.$el.firstElementChild.addEventListener('blur', this.onPasswordBlur)
this.$el.firstElementChild.addEventListener('click', this.onPasswordClick)
this.$el.firstElementChild.addEventListener('mousedown', this.onPasswordMouseDown)
this.$el.firstElementChild.addEventListener('keydown', this.oPasswordKeyDown)

const entries = Object.entries(this.$refs.scannerInput)
// 解决ref问题
for (const [key, value] of entries) {
if (typeof value === 'function') {
this[key] = value
}
}
this['focus'] = this.$el.firstElementChild.focus.bind(this.$el.firstElementChild)
},
methods: {
onPasswordInput(ev) {
this.$emit('input', ev.target.value)
if (ev.target.value === '') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
onPasswordFocus(ev) {
this.isFocus = true
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
},
onPasswordBlur() {
this.isFocus = false
this.$el.firstElementChild.setAttribute('readonly', true)
},
// 鼠标点击输入框一瞬间,禁用输入框
onPasswordMouseDown() {
this.$el.firstElementChild.setAttribute('readonly', true)
},
oPasswordKeyDown(ev) {
// 判断enter键
if (ev.key === 'Enter') {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
})
}
},
// 点击之后,延迟200ms后放开readonly,让输入框可以输入
onPasswordClick() {
if (this.isFocus) {
this.$el.firstElementChild.setAttribute('readonly', true)
setTimeout(() => {
this.$el.firstElementChild.removeAttribute('readonly')
}, 200)
}
},
onInput(_value) {
this.$emit('input', _value)
},
getList(value) {
this.$emit('input', value)
}
// onChange(_value) {
// this.$emit('change', _value)
// }
}
}
</script>

<style lang="scss" scoped>
.scanner-input {
position: relative;
height: 36px;
width: 100%;
display: inline-block;
.input-password {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0 16px;
font-size: 14px;
letter-spacing: 3px;
background: transparent;
color: transparent;
// caret-color: #484848;
}
.input-text {
font-size: 14px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
background-color: transparent;
::v-deep .el-input__inner {
// background-color: transparent;
padding: 0 16px;
width: 100%;
height: 100%;
}
}

.input-text-focus {
::v-deep .el-input__inner {
outline: none;
border-color: #1c7af4;
}
}
}
</style>


至此,可以保证input[type=password]不会再有密码补全提示,并且也不会再切换页面时,会弹出密码保存弹窗。
但是有一个缺点,就是无法完美显示光标。如果用户手动输入和删除,使用起来会有一定的影响。


我想到过可以使用模拟光标,暂时不知道可行性。有哪位同学知道怎么解决的话,可以私信我,非常感谢


作者:香脆又可口
来源:juejin.cn/post/7265666505102524475
收起阅读 »

年会了,公司想要一个离线PC抽奖应用

web
年会了,公司想要一个离线PC抽奖应用 背景 公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC...
继续阅读 »

年会了,公司想要一个离线PC抽奖应用


封面截图.png


背景


公司年会需要一个能够支撑60000+人的抽奖程序,原本通过找 网页的开源项目 再定制化实现了效果,并成功运行再周年庆上;但是现在又到年会了,领导要求要能够在任何地方、任何人只要有一台电脑就能简单方便的定制自己的PC抽奖应用,所有就有了这么一个主题。


程序需求


以下是领导从其他地方复制粘贴过来的,就是想实现类似的效果而已。



  • 1、支持数字、字母、手机号、姓名部门+姓名、编号、身-份-证号等任意组合的抽奖形式。

  • 2、支持名单粘贴功能,从EXCEL、WORD、TXT等任何可以复制的地方复制名单数据内容,粘贴至抽奖软件中作为名单使用,比导入更方便。

  • 3、支持标题、副标题、奖项提示信息、奖品图片等都可以通过拖拽更改位置。

  • 4、支持内定指定中奖者。

  • 5、支持万人抽奖,非常流畅,中奖机率一致,保证公平性。

  • 6、支持中奖不重复,软件自动排除已中奖人员,每人只有一次中奖机会不会出现重复中奖。

  • 7、支持临时追加奖项、补奖等功能支持自定义公司名称、自定义标题。

  • 8、背景图片,音乐等。

  • 9、支持抽奖过程会自动备份中奖名单(不用担心断电没保存中奖名单)。

  • 10、支持任意添加奖项、标题文字奖项名额,自由设置每次抽奖人数设置不同的字体大小和列数。

  • 11、支持空格或回车键抽奖。

  • 12、支持临时增加摇号/抽奖名单,临时删掉不在场人员名单。


目前未实现的效果


有几个还没实现的



  1. 关于人员信息的任意组合抽奖形式,这边只固定了上传模板的表头,需要组合只能通过改excel的内容。

  2. 对于临时不在场名单,目前只能通过改excel表再上传才能达到效果。


技术选型


由于给的时间不多,能有现成的最好;最终选择下面的开源项目进行集成和修改。


说明:由于之前没看到有electron-vite-vue这个项目,所有自己粗略了用vue3+vite+electron开发了 抽奖程序 , 所以现在就是迁移项目的说明。


github开源项目



根据仓库的说明运行起来


动画.gif


修改web端代码并集成到electron


I 拆分页面


组件说明拼图.png


II 补充组件


​ 本人根据自己想法加了背景图片、奖品展示、操作按钮区、展示全部中奖人员名单这几个组件以及另外9个弹窗设置组件。


III 页面目录结构


目录结构.png

IV 最后就是对开源的网页抽奖项目进行大量的修改了,这里就不详细说了;因为变化太多了,一时半会想不起来改了什么。


迁移项目


I 迁移静态资源


静态资源.png


​ 关于包资源说明,这边因为要做离线的软件,所以我把固定要使用的包保存到本地了;


1. 引入到index.html中

引入资源.png


2. 引入图片静态资源

功能代码调整.png


II 迁移electron代码


说明:由于我之前写的一版代码是用js而不是ts,如果一下子全改为ts需要一些时间;所以嫌麻烦,我直接引用js文件了,后期有时间可以再优化一下。


功能代码调整.png




  1. 这时候先运行一下,看下有没有问题



​ 问题一:


问题一.png


​ 这个是因为 我之前的项目一直是用require 引入的;所以要把里面用到require都改为import引入方式;(在preload.ts里面不能用ESM导入的形式,会报语法错误,要用回require导入)


​ 问题二:


问题二.png


​ __dirname不是ESM默认的变量;改为


import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url))

III 迁移前端代码



  • 目录说明


前端功能代码.png



  • 然后一顿复制粘贴,运行,最后报错;


问题三.png
按提示来改 如下:


修改1.png



  • 问题2:资源报错
    资源报错.png
    修复:


资源变化.png



  • 接下来运行看下是否有问题;
    抽奖运行动画.gif
    ​ 运行成功

  • 下一步试一下功能


功能执行动画.gif
​ 功能报错了



  • 看后台错误打印并修复问题
    保存位置错误.png
    修改:
    路径保存源头.png

  • 再次尝试功能 - 成功
    功能执行动画2.gif


IV 一个流程下来


待使用-删除帧后-运行抽奖一个流程动画-gif.gif


打包安装运行


I 运行“npm run build”之后 报错了


打包-js报错.png
这里再次说明一下;由于本人懒得把原本js文件的代码 改为ts;要快速迁移项目 所以直接使用了js;导致打包报错了,所以需要再 tsconfig.json配置一下才行:


  "compilerOptions": {
"allowJs": true // 把这段加上去
},

II 图标和应用名称错误


default Electron icon is used  reason=application icon is not set
building block map blockMapFile=release\28.0.0\YourAppName-Windows-28.0.0-Setup.exe.blockmap

找到打包的配置文件(electron-builder.json5)进行修改:


1. 更改应用名称
"productName": "抽奖程序",

2. 添加icon图标
"win": {
"icon": "electron/controller/data/img/lottery_icon.ico", // ico保存的位置
},

III 打包后运行;资源路径报错了


打包后资源报错.png
打包后资源路径查询不到.png
由于上面的原因,需要把程序涉及读写的文件目录暴露出来;


1. 在构建配置中加入如下配置,将应用要读写的文件目录暴露出来
"extraResources": [
{
"from": "electron/assets",
"to": "assets"
}
],

剩下的就是要重新调整打包后的代码路径了,保证能够找到读写路径;
路径查找纠正.png


最后打包成功,运行项目


删除帧后-一个完整的流程-gif.gif


总结: 主打的要快速实现,所以这个离线pc抽奖程序还有很多问题,希望大家多多包容;


最后附上github地址:github.com/programbao/…
欢迎大家使用


作者:宝programbao
来源:juejin.cn/post/7319795736153210895
收起阅读 »

前端服务框架调研:Next.js、Nuxt.js、Nest.js、Fastify

web
概述 这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。 最终选取了以下具有代表性的框架: Next.js、Nuxt.js:它们是分别与...
继续阅读 »

概述


这次 Node.js 服务框架的调研将着点于各框架功能、请求流程的组织和介入方式,以对前端 Node.js 服务设计和对智联 Ada 架构改进提供参考,不过多关注具体实现。


最终选取了以下具有代表性的框架:



  • Next.js、Nuxt.js:它们是分别与特定前端技术 React、Vue 绑定的前端应用开发框架,有一定的相似性,可以放在一起进行调研对比。

  • Nest.js:是“Angular 的服务端实现”,基于装饰器。可以使用任何兼容的 http 提供程序,如 Express、Fastify 替换底层内核。可用于 http、rpc、graphql 服务,对提供更多样的服务能力有一定参考价值。

  • Fastify:一个使用插件模式组织代码且支持并基于 schema 做了运行效率提升的比较纯粹的偏底层的 web 框架。


Next.js、Nuxt.js


这两个框架的重心都在 Web 部分,对 UI 呈现部分的代码的组织方式、服务器端渲染功能等提供了完善的支持。



  • Next.js:React Web 应用框架,调研版本为 12.0.x。

  • Nuxt.js:Vue Web 应用框架,调研版本为 2.15.x。


功能


首先是路由部分:



  • 页面路由:

    • 相同的是两者都遵循文件即路由的设计。默认以 pages 文件夹为入口,生成对应的路由结构,文件夹内的所有文件都会被当做路由入口文件,支持多层级,会根据层级生成路由地址。同时如果文件名为 index 则会被省略,即 /pages/users 和 /pages/users/index 文件对应的访问地址都是 users。

    • 不同的是,根据依赖的前端框架的不同,生成的路由配置和实现不同:

      • Next.js:由于 React 没有官方的路由实现,Next.js 做了自己的路由实现。

      • Nuxt.js:基于 vue-router,在编译时会生成 vue-router 结构的路由配置,同时也支持子路由,路由文件同名的文件夹下的文件会变成子路由,如 article.js,article/a.js,article/b.js,a 和 b 就是 article 的子路由,可配合 组件进行子路由渲染。





  • api 路由:

    • Next.js:在 9.x 版本之后添加了此功能的支持,在 pages/api/ 文件夹下(为什么放在pages文件夹下有设计上的历史包袱)的文件会作为 api 生效,不会进入 React 前端路由中。命名规则相同,pages/api/article/[id].js -> /api/article/123。其文件导出模块与页面路由导出不同,但不是重点。

    • Nuxt.js:官方未提供支持,但是有其他实现途径,如使用框架的 serverMiddleware 能力。



  • 动态路由:两者都支持动态路由访问,但是命名规则不同:

    • Next.js:使用中括号命名,/pages/article/[id].js -> /pages/article/123。

    • Nuxt.js:使用下划线命名,/pages/article/_id.js -> /pages/article/123。



  • 路由加载:两者都内建提供了 link 类型组件(LinkNuxtLink),当使用这个组件替代 标签进行路由跳转时,组件会检测链接是否命中路由,如果命中,则组件出现在视口后会触发对对应路由的 js 等资源的加载,并且点击跳转时使用路由跳转,不会重新加载页面,也不需要再等待获取渲染所需 js 等资源文件。

  • 出错兜底:两者都提供了错误码响应的兜底跳转,只要 pages 文件夹下提供了 http 错误码命名的页面路由,当其他路由发生响应错误时,就会跳转到到错误码路由页面。


在根据文件结构生成路由配置之后,我们来看下在代码组织方式上的区别:



  • 路由组件:两者没有区别,都是使用默认导出组件的方式决定路由渲染内容,React 导出 React 组件,Vue 导出 Vue 组件:

    • Next.js:一个普普通通的 React 组件:
      export default function About() {
      return <div>About usdiv>
      }


    • Nuxt.js:一个普普通通的 Vue 组件:







在编译构建方面,两者都是基于 webpack 搭建的编译流程,并在配置文件中通过函数参数的方式暴露了 webpack 配置对象,未做什么限制。其他值得注意的一点是 Next.js 在 v12.x.x 版本中将代码压缩代码和与原本的 babel 转译换为了 swc,这是一个使用 Rust 开发的更快的编译工具,在前端构建方面,还有一些其他非基于 JavaScript 实现的工具,如 ESbuild。


在扩展框架能力方面,Next.js 直接提供了较丰富的服务能力,Nuxt.js 则设计了模块和插件系统来进行扩展。


Nest.js


Nest.js 是“Angular 的服务端实现”,基于装饰器。Nest.js 与其他前端服务框架或库的设计思路完全不同。我们通过查看请求生命周期中的几个节点的用法来体验下 Nest.js 的设计方式。


先来看下 Nest.js 完整的的生命周期:



  1. 收到请求

  2. 中间件

    1. 全局绑定的中间件

    2. 路径中指定的 Module 绑定的中间件



  3. 守卫

    1. 全局守卫

    2. Controller 守卫

    3. Route 守卫



  4. 拦截器(Controller 之前)

    1. 全局

    2. Controller 拦截器

    3. Route 拦截器



  5. 管道

    1. 全局管道

    2. Controller 管道

    3. Route 管道

    4. Route 参数管道



  6. Controller(方法处理器)

  7. 服务

  8. 拦截器(Controller 之后)

    1. Router 拦截器

    2. Controller 拦截器

    3. 全局拦截器



  9. 异常过滤器

    1. 路由

    2. 控制器

    3. 全局



  10. 服务器响应


可以看到根据功能特点拆分的比较细,其中拦截器在 Controller 前后都有,与 Koa 洋葱圈模型类似。


功能设计


首先看下路由部分,即最中心的 Controller:



  • 路径:使用装饰器装饰 @Controller 和 @GET 等装饰 Controller 类,来定义路由解析规则。如:
    import { Controller, Get, Post } from '@nestjs/common'

    @Controller('cats')
    export class CatsController {
    @Post()
    create(): string {
    return 'This action adds a new cat'
    }

    @Get('sub')
    findAll(): string {
    return 'This action returns all cats'
    }
    }

    定义了 /cats post 请求和 /cats/sub get 请求的处理函数。

  • 响应:状态码、响应头等都可以通过装饰器设置。当然也可以直接写。如:
    @HttpCode(204)
    @Header('Cache-Control', 'none')
    create(response: Response) {
    // 或 response.setHeader('Cache-Control', 'none')
    return 'This action adds a new cat'
    }


  • 参数解析:
    @Post()
    async create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat'
    }


  • 请求处理的其他能力方式类似。


再来看看生命周期中其中几种其他的处理能力:



  • 中间件:声明式的注册方法:
    @Module({})
    export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
    consumer
    // 应用 cors、LoggerMiddleware 于 cats 路由 GET 方法
    .apply(LoggerMiddleware)
    .forRoutes({ path: 'cats', method: RequestMethod.GET })
    }
    }


  • 异常过滤器(在特定范围捕获特定异常并处理),可作用于单个路由,整个控制器或全局:
    // 程序需要抛出特定的类型错误
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)

    // 定义
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    const status = exception.getStatus()

    response
    .status(status)
    .json({
    statusCode: status,
    timestamp: new Date().toISOString(),
    path: request.url,
    })
    }
    }
    // 使用,此时 ForbiddenException 错误就会被 HttpExceptionFilter 捕获进入 HttpExceptionFilter 处理流程
    @Post()
    @UseFilters(new HttpExceptionFilter())
    async create() {
    throw new ForbiddenException()
    }


  • 守卫:返回 boolean 值,会根据返回值决定是否继续执行后续声明周期:
    // 声明时需要使用 @Injectable 装饰且实现 CanActivate 并返回 boolean 值
    @Injectable()
    export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
    return validateRequest(context);
    }
    }

    // 使用时装饰 controller、handler 或全局注册
    @UseGuards(new AuthGuard())
    async create() {
    return 'This action adds a new cat'
    }


  • 管道(更侧重对参数的处理,可以理解为 controller 逻辑的一部分,更声明式):

    1. 校验:参数类型校验,在使用 TypeScript 开发的程序中的运行时进行参数类型校验。

    2. 转化:参数类型的转化,或者由原始参数求取二级参数,供 controllers 使用:


    @Get(':id')
    findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
    // 使用 id param 通过 UserByIdPipe 读取到 UserEntity
    return userEntity
    }



我们再来简单的看下 Nest.js 对不同应用类型和不同 http 提供服务是怎样做适配的:



  • 不同应用类型:Nest.js 支持 Http、GraphQL、Websocket 应用,在大部分情况下,在这些类型的应用中生命周期的功能是一致的,所以 Nest.js 提供了上下文类 ArgumentsHostExecutionContext,如使用 host.switchToRpc()host.switchToHttp() 来处理这一差异,保障生命周期函数的入参一致。

  • 不同的 http 提供服务则是使用不同的适配器,Nest.js 的默认内核是 Express,但是官方提供了 FastifyAdapter 适配器用于切换到 Fastify。


Fastify


有这么一个框架依靠数据结构和类型做了不同的事情,就是 Fastify。它的官方说明的特点就是“快”,它提升速度的实现是我们关注的重点。


我们先来看看开发示例:


const routes = require('./routes')
const fastify = require('fastify')({
logger: true
})

fastify.register(tokens)

fastify.register(routes)

fastify.listen(3000, function (err, address) {
if (err) {
fastify.log.error(err)
process.exit(1)
}
fastify.log.info(`server listening on ${address}`)
})

class Tokens {
constructor () {}
get (name) {
return '123'
}
}

function tokens (fastify) {
fastify.decorate('tokens', new Tokens())
}

module.exports = tokens

// routes.js
class Tokens {
constructor() { }
get(name) {
return '123'
}
}

const options = {
schema: {
querystring: {
name: { type: 'string' },
},
response: {
200: {
type: 'object',
properties: {
name: { type: 'string' },
token: { type: 'string' }
}
}
}
}
}

function routes(fastify, opts, done) {
fastify.decorate('tokens', new Tokens())

fastify.get('/', options, async (request, reply) => {
reply.send({
name: request.query.name,
token: fastify.tokens.get(request.query.name)
})
})
done()
}
module.exports = routes

可以注意到的两点是:



  1. 在路由定义时,传入了一个请求的 schema,在官方文档中也说对响应的 schema 定义可以让 Fastify 的吞吐量上升 10%-20%。

  2. Fastify 使用 decorate 的方式对 Fastify 能力进行增强,也可以将 decorate 部分提取到其他文件,使用 register 的方式创建全新的上下文的方式进行封装。


没体现到的是 Fastify 请求介入的支持方式是使用生命周期 Hook,由于这是个对前端(Vue、React、Webpack)来说很常见的做法就不再介绍。


我们重点再来看一下 Fastify 的提速原理。


如何提速


有三个比较关键的包,按照重要性排分别是:



  1. fast-json-stringify

  2. find-my-way

  3. reusify



  • fast-json-stringify:
    const fastJson = require('fast-json-stringify')
    const stringify = fastJson({
    title: 'Example Schema',
    type: 'object',
    properties: {
    firstName: {
    type: 'string'
    },
    lastName: {
    type: 'string'
    }
    }
    })

    const result = stringify({
    firstName: 'Matteo',
    lastName: 'Collina',
    })


    • 与 JSON.stringify 功能相同,在负载较小时,速度更快。

    • 其原理是在执行阶段先根据字段类型定义提前生成取字段值的字符串拼装的函数,如:
      function stringify (obj) {
      return `{"firstName":"${obj.firstName}","lastName":"${obj.lastName}"}`
      }

      相当于省略了对字段值的类型的判断,省略了每次执行时都要进行的一些遍历、类型判断,当然真实的函数内容比这个要复杂的多。那么引申而言,只要能够知道数据的结构和类型,我们都可以将这套优化逻辑复制过去。



  • find-my-way:将注册的路由生成了压缩前缀树的结构,根据基准测试的数据显示是速度最快的路由库中功能最全的。

  • reusify:在 Fastify 官方提供的中间件机制依赖库中,使用了此库,可复用对象和函数,避免创建和回收开销,此库对于使用者有一些基于 v8 引擎优化的使用要求。在 Fastify 中主要用于上下文对象的复用。


总结



  • 在路由结构的设计上,Next.js、Nuxt.js 都采用了文件结构即路由的设计方式。Ada 也是使用文件结构约定式的方式。

  • 在渲染方面 Next.js、Nuxt.js 都没有将根组件之外的结构的渲染直接体现在路由处理的流程上,隐藏了实现细节,但是可以以更偏向配置化的方式由根组件决定组件之外的结构的渲染(head 内容)。同时渲染数据的请求由于和路由组件联系紧密也都没有分离到另外的文件,不论是 Next.js 的路由文件同时导出各种数据获取函数还是 Nuxt.js 的在组件上直接增加 Vue options 之外的配置或函数,都可以看做对组件的一种增强。Ada 的方式有所不同,路由文件夹下并没有直接导出组件,而是需要根据运行环境导出不同的处理函数和模块,如服务器端对应的 index.server.js 文件中需要导出 HTTP 请求方式同名的 GET、POST 函数,开发人员可以在函数内做一些数据预取操作、页面模板渲染等;客户端对应的 index.js 文件则需要导出组件挂载代码。

  • 在渲染性能提升方面,Next.js、Nuxt.js 也都采取了相同的策略:静态生成、提前加载匹配到的路由的资源文件、preload 等,可以参考优化。

  • 在请求介入上(即中间件):

    • Next.js、Nuxt.js 未对中间件做功能划分,采取的都是类似 Express 或 Koa 使用 next() 函数控制流程的方式,而 Nest.js 则将更直接的按照功能特征分成了几种规范化的实现。

    • 不谈应用级别整体配置的用法,Nuxt.js 是由路由来定义需要哪个中间件,Nest.js 也更像 Nuxt.js 由路由来决定的方式使用装饰器配置在路由 handler、Controller 上,而 Next.js 的中间件会对同级及下级路由产生影响,由中间件来决定影响范围,是两种完全相反的控制思路。

    • Ada 架构基于 Koa 内核,但是内部中间件实现也与 Nest.js 类似,将执行流程抽象成了几个生命周期,将中间件做成了不同生命周期内功能类型不同的任务函数。对于开发人员未暴露自定义生命周期的功能,但是基于代码复用层面,也提供了服务器端扩展、Web 模块扩展等能力,由于 Ada 可以对页面路由、API 路由、服务器端扩展、Web 模块等统称为工件的文件进行独立上线,为了稳定性和明确影响范围等方面考虑,也是由路由主动调用的方式决定自己需要启用哪些扩展能力。



  • Nest.js 官方基于装饰器提供了文档化的能力,利用类型声明( 如解析 TypeScript 语法、GraphQL 结构定义 )生成接口文档是比较普遍的做法。不过虽然 Nest.js 对 TypeScript 支持很好,也没有直接解决运行时的类型校验问题,不过可以通过管道、中间件达成。

  • Fastify 则着手于底层细节进行运行效率提升,且可谓做到了极致。同时越是基于底层的实现越能够使用在越多的场景中。其路由匹配和上下文复用的优化方式可以在之后进行进一步的落地调研。

  • 除此之外 swc、ESBuild 等提升开发体验和上线速度的工具也是需要落地调研的一个方向。




作者:智联大前端
来源:juejin.cn/post/7030995965272129567
收起阅读 »

我的天!多个知名组件库都出现了类似的bug!

web
前言 首先声明,没有标题党哈! 以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库: 阿里系:ant design, fusion design, 字节系:arco design 腾讯...
继续阅读 »

前言


首先声明,没有标题党哈!


以下我知道的国内知名react组件库全部都有这个bug,你们现在都能去复现,一个提pr的好机会就让给你们了,哈哈!复现组件库:



本来字节还有一个semi design,结果我发现它没有Affix组件,也就是固钉组件,让他躲过一劫,他有这个组件我也觉得肯定会复现相同的bug。


Affix组件是什么,以及bug复现


Affix组件(固钉组件)能将页面元素钉在可视范围。如下图:


image.png


这个button组件,会在距离顶部80px的时候会固定在屏幕上(position: fixed),如下图:


image.png


如何复现bug


你在这个button元素任意父元素上,加上以下任意style属性



  • will-change: transform;

  • will-change: filter;

  • will-change: perspective;

  • transform 不为none

  • perspective不为none

  • 非safari浏览器,filter属性不为none

  • 非safari浏览器,backdrop-filter属性不为none

  • 等等


都可以让这个固定组件失效,就是原本是距离顶部80px固定。


我的组件库没有这个bug,哈哈


mx-design


目前组件不是很多,还在努力迭代中,不知道凭借没有这个bug的小小优点,能不能从你手里取一个star,哈哈


bug原因


affix组件无非都是用了fixed布局,我是如何发现这个bug的呢,我的组件库动画系统用的framer-motion,我本来是想在react-router切换路由的时候整点动画的,动画效果就是给body元素加入例如transform的变化。


然后我再看我的固钉组件怎么失效了。。。后来仔细一想,才发现想起来fixed布局的一个坑就是,大家都以为fixed布局相对的父元素是window窗口,其实是错误的!


真正的规则如下(以下说的包含块就是fixed布局的定位父元素):



  1. 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:



    1. transform 或 perspective 的值不是 none

    2. will-change 的值是 transform 或 perspective

    3. filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。

    4. contain 的值是 paint(例如:contain: paint;

    5. backdrop-filter 的值不是 none(例如:backdrop-filter: blur(10px);




评论区有很多同学居然觉的这不是bug?


其实这个问题本质是定位错误,在这些组件库里,同样使用到定位的有,例如Tooltip,Select,Popuver等等,明显这些组件跟写Affix组件的不是一个人,其他组件这个bug是没有的,只有Affix组件出现了,所以你说这是不是bug。


还有,如果因为引用了Affix组件,这个固定元素的任一父元素都不能用以上的css属性,我作为使用者,我用了动画库,动画库使用transfrom做Gpu加速,你说不让我用了,因为引起Affix组件bug,我心里想凭啥啊,明明加两行代码就解决了。


最后,只要做过定位组件的同学,其复杂度在前端算是比较高的了,这也是为什么有些组件库直接用第三方定位组件库(floating-ui,@popper-js),而不是自己去实现,因为自己实现很容易出bug,这也是例如以上组件库Tooltip为什么能适应很多边界case而不出bug。


所以你想想,这仅仅是定位组件遇到的一个很小的问题,你这个都解决不了,什么都怪css,你觉得用户会这么想吗,一有css,你所有跟定位相关的组件全部都不能用了,你们还讲理不?


总之一句话,你不能把定位组件的复杂度高怪用户没好好用,建议去看看floating-ui的源码,或者之前我写的@popper-js定位组件的简要逻辑梳理,你就会意识到定位组件不简单。边界case多如牛毛。


解决方案



  • 首先是找出要固定元素的定位元素(定位元素的判断逻辑上面写了),然后如果定位元素是window,那么跟目前所有组件库的逻辑一样,所以没有bug,如果不是window,就要求出相对定位父元素距离可视窗口顶部的top的值

  • 然后在我们原本要定位的值,比如距离顶部80px的时候固定,此时80px再减去上面说的定位父元素距离可视窗口顶部的top的值,就没有bug了


具体代码如下:



  • offsetParent固定元素的定位上下文,也就是相对定位的父元素

  • fixedTop是我们要触发固定的值,比如距离可视窗口顶部80px就固定



affixDom.style.top = `${isHTMLElement(offsetParent) ? (fixedTop as number) - offsetParent.getBoundingClientRect().top : fixedTop}px`;

如何找出offsetParent,也就是定位上下文


export function getContainingBlock(element: Element) {
let currentNode = element.parentElement;
while (currentNode) {
if (isContainingBlock(currentNode)) return currentNode;
currentNode = currentNode.parentElement;
}
return null;
}

工具方法,isContainingBlock如下:


import { isSafari } from './isSafari';

export function isContainingBlock(element: Element): boolean {
const safari = isSafari();
const css = getComputedStyle(element);

// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
return (
css.transform !== 'none' ||
css.perspective !== 'none' ||
(css.containerType ? css.containerType !== 'normal' : false) ||
(!safari && (css.backdropFilter ? css.backdropFilter !== 'none' : false)) ||
(!safari && (css.filter ? css.filter !== 'none' : false)) ||
['transform', 'perspective', 'filter'].some((value) => (css.willChange || '').includes(value)) ||
['paint', 'layout', 'strict', 'content'].some((value) => (css.contain || '').includes(value))
);
}


本文完毕,求关注,求star!!!对于react组件库感兴趣的小伙伴,欢迎加群一起交流哦!


作者:孟祥_成都
来源:juejin.cn/post/7265121637497733155
收起阅读 »

百度考题:反复横跳的个性签名

web
浅聊一下 在各种社交网站上,都会有个性签名一栏,当没有写入时,默认为 “这个人很懒,什么也没有留下” 当我们没有点击它时,默认为一个文本,当我们点击它时,又变成input框了,我们可以在里面添加自己的个性签名...本篇文章将带大家copy一个低配版的的个性签...
继续阅读 »

浅聊一下


在各种社交网站上,都会有个性签名一栏,当没有写入时,默认为 “这个人很懒,什么也没有留下”


image.png


当我们没有点击它时,默认为一个文本,当我们点击它时,又变成input框了,我们可以在里面添加自己的个性签名...本篇文章将带大家copy一个低配版的的个性签名组件

动手


我们要有JS组件思维,个性签名是很多地方都可以用到的,我们把它做成组件,用到的时候直接搬过去就好了。所以我们将使用原生JS将组件封装起来(大家也可以使用VUE)


EditInPlace 类


我们要使用这个JS组件,首先得将其方法和参数封装在一个类里,再通过类的实例化对象来展示。


function EditInPlace(id,parent,value){
this.id = id;
this.parent = parent;
this.value =value || "这个家伙很懒,什么都没有留下";
this.createElements()//动态装配html结点
this.attachEvents();
}


  1. 将传入的idparentvalue参数赋值给新创建的对象的对应属性。

  2. 如果没有提供value参数,则将默认字符串"这个家伙很懒,什么都没有留下"赋值给新对象的value属性。

  3. 调用createElements方法来动态装配HTML节点。

  4. 调用attachEvents方法来附加事件。


EditInPlace.prototype


在 EditInPlace 类中,我们调用了createElements() attachEvents()两个方法,所以我们得在原型上定义这两个方法


createElements


    createElements:function(){
this.containerElement = document.createElement('div');
this.containerElement.id= this.id;
//签名文字部分
this.staicElement = document.createElement('span');
this.staicElement.innerText = this.value
this.containerElement.appendChild(this.staicElement);
//输入框
this.fieldElement = document.createElement('input')
this.fieldElement.type = 'text'
this.fieldElement.value = this.value;
this.containerElement.appendChild(this.fieldElement);
//save 确认
this.saveButton = document.createElement('input');
this.saveButton.type = 'button'
this.saveButton.value = '保存'
this.containerElement.appendChild(this.saveButton)
//取消按钮
this.cancelButton = document.createElement('input')
this.cancelButton.type = 'button'
this.cancelButton.value = '取消'
this.containerElement.appendChild(this.cancelButton)


this.parent.appendChild(this.containerElement)
this.converToText();

}


  1. 创建一个<div>元素,并将其赋值给this.containerElement属性,并设置其id为传入的id

  2. 创建一个<span>元素,并将其赋值给this.staicElement属性,然后设置其文本内容为传入的value

  3. this.staicElement添加到this.containerElement中。

  4. 创建一个<input>元素,并将其赋值给this.fieldElement属性,设置其类型为"text",并将其值设置为传入的value

  5. this.fieldElement添加到this.containerElement中。

  6. 创建一个保存按钮(<input type="button">),并将其赋值给this.saveButton属性,并设置其值为"保存"。

  7. 将保存按钮添加到this.containerElement中。

  8. 创建一个取消按钮(<input type="button">),并将其赋值给this.cancelButton属性,并设置其值为"取消"。

  9. 将取消按钮添加到this.containerElement中。

  10. this.containerElement添加到指定的父元素this.parent中。

  11. 调用converToText方法。


这个方法主要是用于动态生成包含静态文本、输入框和按钮的编辑组件,并将其添加到指定的父元素中。也就是说我们在这里主要就是创建了一个div,div里面有一个text和一个input,还有保存和取消按钮


div和span的显示


我们怎么样实现点击一下,就让text不显示,input框显示呢?
就是在点击一下以后,让text的display为'none',让input框和按钮为 'inline'就ok了,同样的,在保存或取消时采用相反的方法就好


    converToText:function(){
this.staicElement.style.display = 'inline';
this.fieldElement.style.display = 'none'
this.saveButton.style.display = 'none'
this.cancelButton.style.display = 'none'
},
converToEdit:function(){
this.staicElement.style.display = 'none';
this.fieldElement.style.display = 'inline'
this.saveButton.style.display = 'inline'
this.cancelButton.style.display = 'inline'
}

attachEvents


当然,我们需要在text文本和按钮上添加点击事件


    attachEvents:function(){
var that = this
this.staicElement.addEventListener('click',this.converToEdit.bind(this))
this.cancelButton.addEventListener('click',this.converToText.bind(this))
this.saveButton.addEventListener('click',function(){
var value = that.fieldElement.value;
that.staicElement.innerText = value;
that.converToText();
})
}


  1. 通过var that = this将当前对象的引用保存在变量that中,以便在后续的事件监听器中使用。

  2. 使用addEventListener为静态元素(this.staicElement)添加了一个点击事件的监听器,当静态元素被点击时,会调用this.converToEdit.bind(this)方法,这样做可以确保在converToEdit方法中this指向当前对象。

  3. 为取消按钮(this.cancelButton)添加了一个点击事件的监听器,当取消按钮被点击时,会调用this.converToText.bind(this)方法,同样也是为了确保在converToText方法中this指向当前对象。

  4. 为保存按钮(this.saveButton)添加了一个点击事件的监听器,当保存按钮被点击时,会执行一个匿名函数,该函数首先获取输入框的值,然后将该值更新到静态元素的文本内容中,并最后调用converToText方法,同样使用了变量that来确保在匿名函数中this指向当前对象。


通过这些事件监听器的设置,实现了以下功能:



  • 点击静态元素时,将编辑组件转换为编辑状态。

  • 点击取消按钮时,将编辑组件转换为静态状态。

  • 点击保存按钮时,获取输入框的值,更新静态元素的文本内容,并将编辑组件转换为静态状态。


HTML


在html中通过new将组件挂载在root上


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>就地编辑-EditIntPlace</title>
</head>
<body>
<div id="root"></div>
<script src="./editor_in_place.js"></script>
<script>
// 为了收集签名,给个表单太重了 不好看
// 个性签名,就地编辑
// 面向对象 将整体开发流程封装 封装成一个组件
new EditInPlace('editor',document.getElementById('root'))//名字 挂载点
</script>
</body>
</html>

完整效果



结尾


写的不太美观,掘友们可以自己试试加上样式,去掉按钮,再加上一张绝美的背景图片...


作者:滚去睡觉
来源:juejin.cn/post/7315730050576367616
收起阅读 »

前端实习生如何提升自己

web
这几年就业形势很严峻,很多大学生都选择通过实习来提升自己毕业后的就业压力。 同时,国家也在大力鼓励学生们多去实习,一些高校甚至强制学生大三就要出去实习,用来换成学分,而且比重很大,比如从深圳大学分出来的深圳技术大学。最近,我们就从深圳技术大学招聘了多个前端实习...
继续阅读 »

这几年就业形势很严峻,很多大学生都选择通过实习来提升自己毕业后的就业压力。


同时,国家也在大力鼓励学生们多去实习,一些高校甚至强制学生大三就要出去实习,用来换成学分,而且比重很大,比如从深圳大学分出来的深圳技术大学。最近,我们就从深圳技术大学招聘了多个前端实习生。


正常来讲一个资深的研发,最多可以辅导两个实习生,但因为我们是初创阶段,为了降低成本,我们经常会出现需要同时辅导三个以上实习生的情况。


到网上搜了一圈,都没有系统讲解实习生如何提升自己的资料,本文将结合自己过去十年带人的经验,面向前端岗位,谈一谈前端实习生如何提升自己。


作者简介


先后就读于北京交大和某军校,毕业后任职于原北京军区38集团军,带过两次新兵连,最多时管理过120人左右的团队。


退役后,先进入外企GEMALTO做操作系统研发,后进入搜狐、易车和美团等公司做大前端开发和团队管理,最多时管理过30多人的大前端团队,负责过几百个产品的前端开发工作,做过十多个大型复杂前端项目的架构工作。


目前在艺培行业创业,通过AIGC技术提升艺培机构门店运营和招生推广的效率和降低成本。


如何选择实习?


我永远相信,选择比努力奋斗更重要。选择一家合适的实习单位或公司,可以达到事半功倍的效果。


大公司的优势


毫无疑问,很多大学生都会选择大公司作为自己的实习单位,尤其是一些上市公司,甚至为了求得一个实习机会,即使倒贴钱也愿意。


与小企业相比,大公司一般都会有相对完整的企业制度。从签实习协议开始,如薪资结算、工作安排等,完全按照相关的企业制度,以及法律法规进行,这样一定程度有利于保障实习生的劳动权益。


与此相比,一些小企业甚至连实习协议都不签,这对于实习生来说是非常不利的。


大公司一般都会有日历表和日常的作息时刻表。因此,在大公司工作一般都按照正常的作息时间上下班,不太会出现加班的情况。


而相对来说,小企业由于刚刚起步,工作量非常大,人手常常不够,因此加班加点是家常便饭了。


每个大公司都会具有良好的、独特的企业文化,从办公室的装饰、到公司员工午休时的娱乐活动,无不体现出自己的特性。因此,在大公司里实习,你能够亲身感受到大公司内部的企业文化。


而小企业,则不太可能会有自己的企业文化,即使有多数也是非常糟糕的。


小企业的特点


在大公司实习,由于大公司里本身就人员冗余,再加上对“实习生”的不信任,因此他们给实习生安排的工作常属于打杂性质的。可能你在大公司里呆了两个月,可是连它最基本的业务流程都没弄明白。


而在小企业里,由此大多处于起步,没有太多的职位分化,你可能一人需要兼多职,许多事情都得自己解决,能够提高你独立解决问题的能力。


大公司一般有完整的部门体制,上下级关系相对严格分明,因此,官僚作风比较明显,新人难有发言权。


而在小企业,这方面的阻力则小得多,使新人少受束缚,更自由地施展自己的能力、表达自己的想法。


小结


实习为了给将来就业铺路,因此,实习最重要的在于经验。当然,在大公司实习如果能在增加自己进该公司的概率那固然好,但如果只是为了在简历里加上“在某大公司里实习过”的虚名,那实在是没有必要。


结合未来职业选择,要实习的公司和岗位是和你的职业理想直接相关的,这个岗位的实习和这个公司的进入可以给你日后的职业发展加分。


选你能在实习期间出成果的项目。如果你选了个很大的项目,半年还没做完,这就说不清楚是你没做好,还是项目本身的问题了。


实习的时候要更深入的了解企业的实际情况和问题,结合理论和实际,要给企业解决实际问题,有一个具体的成果,让企业真正感觉到你的项目做好了


我个人建议,在正式工作前,大公司和小企业,最好都能争取去实习一段时间,但第一份实习最好去大公司。


通过实习,希望大家能够验证自己的职业抉择,了解目标工作内容,学习工作及企业标准,找到自身职业的差距。


如何做好实习


选择好合适的实习单位,只是走出了万里长征的第一步,要做好实习,大家可以关注以下四个方面:


端正态度


从实习生个人的角度来看,多数人的出发点都是学习提升,但如果我们只是抱着纯学习而不为企业创造价值的心态的话,实习工作就不容易长久。


我们可以从企业的角度来看:



  • 一是实习提供了观察一位潜在的长期员工工作情况的方法,为企业未来发展培养骨干技术力量与领导人;

  • 二手有利于廉价劳动力争夺人才,刚毕业的学员便于管理,这样不仅能降低成本,还能提高企业的知名度,有利于企业长远发展。


实习生只有表现出了企业看重的那些方面,企业才会投入时间和精力去好好培养他,教一些真本领。


不管是在美团,还是我自己创业的公司,我都是基于相同的原则,去评估是否留下某个实习,进行重点培养。


我个人看重以下几点:



  • 不挑活:即使不是自己喜欢或在工作范围的任务,也能积极去完成;

  • 爱思考:接到任务或经过指导后,能及时反馈存在的问题和提升改进方案;

  • 善沟通:遇到困难及时寻求帮助,工作不开心了不封闭自己、不拒绝沟通。


不管当年我去实习还是上班,我的心态都是先为企业创造价值,在此基础上再争取完成自己的个人目标。


注重习惯


有的人毕业10年都还没毕业3年的人混得好,不是因为能力不行,而是因为他没有养成良好的职场习惯,以至于总是吃亏。


开发人员培养好的习惯至关重要。编写好的代码,持续学习,逐步提高代码质量。


主动反馈和同步信息


这个习惯非常重要,可以避免大家做很多无用功,大大提升大家日常工作的效率,而且也特别容易获得大家的好感与信任。


经常冒烟自测


对于自己负责的功能或产品,不管是测试阶段还是正式发布,只要需要其他人使用,都最好冒烟自测一遍,避免低级错误,进而影响大家对你专业度的信任。


良好的时间管理


先每天花几分钟确定好我今天要办的最重要的三到六件事情,如此能帮助提高整体的效率;把今天要做的所有事情一一写下来,按轻重缓急排序,可以按照要事第一的原则;每做完一件事情就把它划掉,在今天工作结束之前检查我是不是把整个list的工作都完成了。


避免重复的错误


不要犯了错误之后就草草了事,花一点时间深度挖掘清楚我犯错的底层原因是什么,再给自己一些小提示,比如做一个便利贴做提醒,在电脑桌面上面放一个警告,避免相同的错误重复发生。


构建知识体系


在信息爆炸的年代,碎片化的知识很多,系统学习的时间越来越少,如果没有自己的知识体系,很容易被淹没在知识的海洋中,而且难以记忆。


100分程序员推荐的做法,通过Wiki或者其他知识管理工具构建一个知识框架,大的分类可以包括软技能、架构、语言、前端、后端等,小的分类可以更细化。


培养大局观


程序员比较容易陷入的困境是专注于自己的一亩三分地,不关心团队的进度和业绩,也不关心软件的整体架构和其他模块。


这种状态长期下去没有好处,特别是在大公司中,逐渐成长为一颗螺丝钉。100分程序员会在工作之余,多看看其他在做什么,看看团队的整体规划,看看软件系统的架构和说明文档。


对自己的工作更理解,而且知道为什么这个产品应该这样设计,为什么领导应该这样做规划,这种大局观非常有利于自己的职业生涯。


技能提升


通过实习阶段,你才会知道专业知识在工作中的具体运用,才能切身体会专业知识的重要性,这样也让自己在以后的学习中更加的努力和勤奋和有所侧重的安排各科目的学习时间。


对于前端实习生的技能学习,除了打牢HTML、CSS和JS这些基础的前端知识以及熟练掌握vue、react这些框架开发技术栈,建议大家还需要选中自己感兴趣的前端技术领域进行持续深入的学习实践,具体请看我之前总结的文章:前端学哪些技能饭碗越铁收入还高


关系处理


大学生在实习过程中最难适应的就是人际关系,同时还要了解企业组织运行的特点、规章制度,了解企业文化对员工的影响等等,知道职场中和领导及同事该如何沟通,企业对员工有着什么样的需求等。


这些也只有在实习时,让自己处于团队之中,才能切身的体会和参与,只有这样大学生才会对社会生活有深刻的理解,对将来就业才有益处。


不论是对同事,还是对客户,还是对合作伙伴,都不要吝啬你的赞美之词。善于夸赞他人的员工,更容易获得他人的好评,有时还能让你成功避免一些不必要的麻烦。


当然,赞美他人,不要过于浮夸,要能说出赞美的理由,以及哪些是你表示认同的地方。比如,我们在称赞他人方案做得好的时候,不要只是说:“这方案不错”。


可以这么说:“这方案在市场推广方面的内容写得很好,逻辑清晰,数据充分,还能有同行的案例借鉴,十分值得我学习。”


要让别人感受到你是真心的在夸奖他,而不是讨好他。


我们常常很难控制激动时的情绪,发怒时更是。往往这个时候表达出的观点都是偏激的,事后再后悔就来不及了。


如果你与同事之间发生争执而发怒,请在这个时间段保持沉默。给自己找个地方安静一会儿,等到情绪稳定以后,再向他人表达你的观点,慢慢地,你就会更好地控制自己的情绪。


在职场中切忌过于情绪化,学会管理好自己的情绪是一个职场人必备的技能之一。坏情绪并不会帮助你解决任何问题,过多的抱怨只会使你成为负能量的传播者。


协作在软件开发中至关重要。一个好的开发人员应该能够与他人很好地合作,并了解如何有效地协作。这意味着对反馈持开放态度,积极寻求团队成员的意见,并愿意作为团队的一部分从事项目。


协作会带来更好的沟通、更高效的工作流程和更好的产品。


总结


社会实践对大学生的就业有着很大的促进作用,是大学生成功就业的前提和基础。


实习的目的因人而异,可以千差万别,而你实习的真正目的也只有自己才最清楚。只有在开始实习之前首先明确自己的目的,后面的路才会变得清晰明了。


作者:文艺码农天文
来源:juejin.cn/post/7319181229520568371
收起阅读 »

前端学哪些技能饭碗越铁收入还高

web
随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。 但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深...
继续阅读 »

随着经济的下行以及移动互联网发展趋于成熟,对软件开发人员的需求大大减少,互联网行业所有的公司都在降本增效,合并通道,降薪裁员的新闻层出不穷。


但相比其他行业,互联网行业的从业者薪资还是比较可观的,但要求也比之前高了很多,需要大家掌握更多的技能和在某些技术领域深耕。


本文,我们就聊聊,掌握了哪些技能,能让前端同学,收入高且稳定。


端智能


首推的是端智能,很多行业大咖都认为,随着ChatGPT的横空出世,开启了第四次工业革命,很多产品都可以用大模型重做一遍。当前,我创业的方向,也和大模型有关。


当前的大模型主要还跑在云端,但云端的成本高,大模型的未来在端智能,这也是小米创始人雷军在今年一次发布会上提出的观点。


在2023年8月14日的雷军年度演讲中,雷军宣布小米已经在手机端跑通13亿参数的大模型,部分场景效果媲美云端。


目前,端上大模型的可行性和前景已经得到了业内的普遍认可,国内外各个科技大厂在大模型的端侧部署领域均开始布局,目前大量工程已在PC端、手机端实现大模型的离线部署,更有部分App登陆应用商店,只需下载即可畅通无阻地对话。


我们相信,在不久的将来,端上大模型推理将会成为智能应用的重要组成部分,为用户带来更加便捷、智能的体验。


我在美团从零研发了web端智能推理引擎,当时立项时,就给老板画饼,美团每天的几百亿推理调用,如果有一半用端智能替代的话,每年能为公司节省上亿元。


要想掌握端智能,需要学习深度学习的基本知识,还要掌握图形学和C++编程,通过webgl或webassembly 技术实现在Web端执行深度学习算法。


图形学


前面提到的端智能,只是涉及到了图形学中的webgl计算,但图形学的未来在元宇宙,通过3D渲染,实现VR、AR、XR等各种R。


计算机图形学是一门快速发展的领域,涵盖了三维建模、渲染、动画、虚拟现实等众多技术和应用。在电影、广告、游戏等领域中,计算机图形学的应用已经非常广泛。


熟练使用threejs开发各种3D应用,只能算是入门。真正的图形学高手,不仅可以架构类似3D家装软件的大型应用,而且能掌握渲染管线的底层原理,熟练掌握各种模型格式和解决各种软件,进行模型转换遇到的各种兼容问题。


随着计算机硬件和算法的不断进步,计算机图形学正迎来新的发展趋势。


首先是实时渲染与逼真度提升



  • 实时渲染技术:随着游戏和虚拟现实的兴起,对实时渲染的需求越来越高。计算机图形学将继续致力于研发更高效的实时渲染算法和硬件加速技术,以实现更逼真、流畅的视觉效果。

  • 光线追踪与全局照明:传统的实时渲染技术在光照模拟方面存在挑战。计算机图形学将借助光线追踪等技术,实现更精确的全局照明效果,提升场景的真实感和细节表现。


其次是虚拟与增强现实的融合



  • 混合现实技术:计算机图形学将与传感器技术、机器视觉等相结合,推动虚拟现实与增强现实的融合发展。通过实时感知和交互,用户可以在真实世界中与虚拟对象进行互动,创造更沉浸式的体验。

  • 空间感知与虚拟对象定位:计算机图形学将致力于解决空间感知和虚拟对象定位的挑战。利用深度学习、摄像头阵列等技术,实现高精度的空间感知和虚实融合,为虚拟与增强现实应用带来更自然、精确的交互方式。


再次是计算机图形学与人工智能的融合



  • 生成对抗网络(GAN)在图形生成中的应用:GAN等人工智能技术为计算机图形学带来了新的创作手段。通过训练模型生成逼真的图像和场景,计算机图形学能够更便捷地创建大量内容,并提供个性化的用户体验。

  • 计算机图形学驱动的虚拟人物与角色生成:结合计算机图形学和人工智能技术,研究人员正在努力开发高度逼真的虚拟人物和角色生成方法。这将应用于游戏、影视等领域,带来更具情感表达和交互性的虚拟角色。


最后是可视化分析与科学研究。



  • 一是大数据可视化:随着大数据时代的到来,计算机图形学在可视化分析方面扮演着关键角色。通过创新的可视化方法和交互技术,研究人员能够更深入地理解和分析庞大而复杂的数据集,揭示潜在的模式和趋势。

  • 二是科学数据可视化:计算机图形学在科学研究中的应用也日益重要。通过将科学数据转化为可视化形式,研究人员能够更直观地理解复杂的数据模式和关系,加快对科学问题的洞察和发现。这种可视化分析有助于领域如天文学、生物学、气象学等的研究进展。


工程提效


其实,过去三年我在美团的工作,至少有一半的精力是做和工程提效相关的事情。当然,也做了降本的事情,从零搭建外包团队。


就像我之前总结的文章:我在美团三年的前端工程化实践回顾 中提到那样,前端工程提效,一般会按照工具化、标准化、平台化和体系化进行演进。


相比前面的端智能和图形学,除了建设低代码平台和WebIDE有点技术难度,其他更多需要的是沟通、整合资源的能力,既要有很强的项目管理能力,又要人产品思维。


前两个方向做好了我们一般称为技术专家,而工程提效则更偏管理者,未来可以成为高管或自己创业。


总结


端智能和图形学,我在美团都尝试过进行深耕,但自己性格外向,很难坐得住。工程提效做得也一般,主要是因为需要换城市而换了部门,没有机缘继续推进,在美团很难往上走,所以只能尝试自己创业。


当前我创业的公司,也需要用到很多端智能的技术,比如我们使用yolov8实现了在web端进行智能抠图(后面计划使用segment anything大模型实现);也需要用到很多图形学的技能,比如开发3D美术馆为艺培机构招生引流,通过3D展示火箭、太阳系、空间站等,提升教学效果。


未来,我们需要招聘很多端智能及图形学方向的技术人才,欢迎大家加入。


作者:三一习惯
来源:juejin.cn/post/7310143510103064585
收起阅读 »