谷歌浏览器禁用第三方Cookie:独裁者的新工具?
2024年,Chrome将要正式禁用第三方Cookie了,这个变化对Web开发者来说是非常重要的,因为它将改变开发者如何设计网站以及如何搜集和使用用户数据。这是怎么一回事,到底有什么具体影响?
什么是Cookie?
随着互联网技术的发展,我们的生活变得越来越数字化,网上购物、社交、阅读新闻成为日常。而在这个数字化的世界中,Cookie扮演了一个不可或缺的角色。
Cookie是一种由浏览器保存在用户电脑上的小块数据,用来帮助网站记住用户的信息和设置。网站可以在前端直接操作Cookie,也可以根据服务器返回的指令设置Cookie,当浏览器请求同一服务器时相应的Cookie会被回传。Cookie让网站能够记住用户的登录状态、购物车中的商品、以及个性化设置等,极大地提升了用户体验。
第三方Cookie是咋回事?
然而,Cookie并非只有一种。其中,第三方Cookie与网站直接设置的第一方Cookie不同,它们通常由第三方广告商或网站分析服务设置。网站一般通过在前端页面引入第三方的Javascript程序文件来实现这种能力。
用于网站分析时,Cookie可以收集和存储有关用户访问网站的数据,如用户的浏览历史、停留时间、点击行为等。这些数据对于网站运营者来说非常有价值,可以帮助他们了解用户的行为和兴趣,优化网站的设计和功能,提升用户的体验。
用于广告时,Cookie可以用来追踪用户在不同网站上的行为,以便提供个性化广告和内容。这种广泛的追踪能力让广告商能够了解用户的喜好和习惯,从而推送更加精准的广告。
为什么要禁用第三方Cookie?
但第三方Cookie也引发了隐私方面的担忧。许多用户和隐私倡导者认为,广告商利用Cookie追踪用户的行为侵犯了个人隐私。
很多同学应该有这样的体验:你在某个网站搜索了某个东西,然后访问其它网站或服务时,网页会向你展示之前搜索过的类似东西。基于某些原因,有时候我们并不想这样被跟踪。
这种担忧导致了对第三方Cookie的禁用呼声。同时隐私担忧不仅仅来自于用户和隐私倡导者,也来自于法规如欧盟的通用数据保护条例(GDPR)和加州消费者隐私法案(CCPA)这样的法律要求。
为了应对这一问题,苹果公司在其Safari浏览器中率先禁用了第三方Cookie,微软和Mozilla也采取了类似措施。
谷歌的行动计划是什么样的?
但是,作为浏览器市场占有率第一的谷歌,却迟迟没有明显的动作,遭到了不少人的非议和批评。
为了解决这些问题,谷歌提出了“隐私沙盒”(Privacy Sandbox)计划,旨在开发一系列新的技术,既能保护用户隐私,又能支持广告商进行有效的广告投放。
想象一下,如果有一个中立的场所,既能让你安心地存放你的个人物品,又能让有需要的人在不侵犯你隐私的情况下了解你的需求,这就是隐私沙盒的理念。
例如,谷歌提出的FLoC(Federated Learning of Cohorts)技术,就是将用户分群,而非单独追踪个人行为,从而在保护个人隐私的同时提供群体级的广告定位。
作为市场份额最大的浏览器,谷歌计划在2024年1月开始逐步禁用第三方Cookie,在2024年第三季度完成第三方Cookie的全面禁用。
禁用导致的问题有哪些?
禁用第三方 Cookie 对网站主、广告商和用户都会产生一系列影响。针对不同群体的影响及解决方案如下:
网站主
第三方 Cookie 的禁用可能会让网站主失去对用户行为的深入分析能力,因为他们将不能再依靠第三方 Cookie 来追踪用户在多个网站上的活动。这可能会影响到网站的个性化服务和广告定位的准确性,进而影响网站收入。
解决方案:网站主可以更多地依赖第一方 Cookie,即直接由网站域设置和读取的 Cookie。这些 Cookie 可以帮助网站主跟踪用户在自己网站上的行为,而不越过隐私边界。此外,网站主可以通过提供更多的价值服务来鼓励用户主动分享信息。
对于拥有多个域名的网站主来说,可以使用同一个根域名来设置Cookie,这样在根域名下的所有子域名都可以访问这些Cookie,这种方法仍然在用户隐私保护的框架内。如果网站主拥有多个相关联的服务,可以实施更安全的单点登录解决方案,比如使用会话令牌和OAuth等协议。
广告商
广告商将难以像过去那样追踪用户在整个互联网的行为,从而无法进行精准的广告定向,这可能导致广告效果下降和收入减少。
解决方案:谷歌提出的隐私沙盒计划中,Event Conversion Measurement API 是一种技术解决方案,它允许广告商在不侵犯用户隐私的情况下测量广告转化率。此外,广告商还可以利用机器学习等技术,通过分析大量的匿名化和聚合数据来预测用户兴趣。不过广告商也有机会使用一些更隐蔽的技术追踪手段来保持原有的业务模式,具体一些技术下文会提到。
用户
用户的隐私得到更好的保护,但可能会失去一些基于个性化广告的便利性,例如推荐系统的准确性可能会下降。
解决方案:用户可以获得更多的隐私控制权,例如通过浏览器设置来决定哪些数据可以被网站使用。同时,随着技术的发展,用户可能会遇到更多基于隐私保护的个性化体验,例如使用本地算法来进行内容推荐,而不需要将个人数据传输到服务器。
谷歌真的做好了吗?
尽管谷歌提出了Privacy Sandbox这样的计划,希望在不依赖个人用户信息的情况下,仍然能够支持广告生态系统,但实际上这个计划也遭到了一些批评。批评者认为,这些新提出的技术可能仍然允许用户被追踪,只不过追踪的方式更加隐蔽了。
例如,FLoC的提出本意是为了代替传统的个人定向广告,它通过对用户进行群组化来推送广告,这样不会直接暴露个人的行为数据。但问题在于,群组化的数据仍然可能被用来间接识别和追踪个人,特别是当某个群组里的用户数量不多时。这就导致了一种新形式的隐私问题,即“群组隐私”的泄露。
还有广告服务商仍可能通过一些技术手段突破隐私限制,比如通过网站转发、Canvas指纹技术、网络信标、用户代理字符串、本地存储和ETag跟踪等。
此外,一些隐私倡导者还担心,谷歌作为一个广告公司,其提出的隐私解决方案可能偏向于维护其在在线广告市场中的主导地位。他们认为,谷歌有动机设计一套系统,搜集用户在搜索、YouTube和其他谷歌服务上的行为数据,使得自家的广告网络相比其他竞争对手拥有优势。而这可能会影响到广告市场的公平竞争,甚至可能对开放网络生态系统构成威胁。
总的来说,谷歌在隐私问题上的努力是一个积极的开始,但隐私保护的路还很长。而对于技术人员而言,理解这些变化和挑战,以及如何在保护用户隐私的同时提供优质服务,将是未来发展的一个重要课题。
关注萤火架构,提升技术不迷路!
来源:juejin.cn/post/7313414896783769609
关于菜狗做了一个chrome小插件

前言
很多时候,老是质疑反问自己,在空闲时间都在干嘛呢?是沉迷于看动漫、美女视频,打游戏?还是在追求更有意义的事呢?自我回答,没错我是属于前者了(dog),然后前后左右思考,还是决定找一些事做,不能一直这么荒废了,起码做一些是一些,积少成多!但是能做什么呢?这又引起我这不聪明的大脑深深的思考,是从自己从事的职位来,还是从自己的兴趣来呢?然后在这段迷茫的时间一直在寻找中,突然有一天看到别人的文章或视频感触深刻,最后还是敲定做项目!然后就着手开始准备做起来,就这么愉快决定行动起来了,奥利给!
于是利用上班摸鱼时间和下班空闲的时间开始行动起来~
起源
在空闲的摸鱼时间里,我经常喜欢看别人写的文章,读完一篇又一篇,大多数时候都会感叹并羡慕。然鹅,特别是对于某些事情,我会有特别深的感触。因为有时候的情绪只是在特定时刻被触发的,所以我想记录下当时的触发感受和情绪。然后我就去寻找相关的插件,结果找到了一个相当不错的插件。试用了一番后,发现效果还不错,于是我决定将关注点留在这里,尝试着制作这类插件,看看自己做的的效果如何。有时候我在想,为什么要费力自己创建一个插件呢?毕竟市面上已经有现成的插件了,为什么不用呢?然而,答案很明了:一是想要找点事情做,二是想要提升一下自己的技能水平。于是,这样的动力激励着我行动起来。
需求
产品需求其实很明确,因为从我的角度来看已经有现成的产品可供参考,然后只需根据个人需求进行定制。因此,产品的主要目的是为了方便我们的生活。
因此,一个产品的设计应当能够满足不同用户的需求,以提供更好的使用体验。这里着重考虑我自己个人使用,因为每个人的习惯和偏好都不尽相同。
收集的功能需求:
- 对内容进行划线
- 记录想法/感想
- 统计列表数据
- 预览原文(主要是针对文章)
- 拷贝(划线、原文、Markdown)内容
- 下载(划线TXT、原文TXT、原文Markdown)内容
目前第一版只涵盖了这些功能需求(感谢ChatGPT,它帮助解决了我作为菜狗的许多问题),后续将根据需要情况进行调整和完善~
下面是完成的功能截图:
突然发现我开发这个插件还有一个小用处哈哈哈,针对类似我这种不会写文章格式的小白来说,有时候真的不知道该如何开始写。但有一个现成的文章作为参考,真是太棒了!它不仅能够给我提供灵感,还能够帮助我了解文章的结构和写作风格,让我更加自信地面对写作挑战!
拷贝的文章markdown格式:
最后打算发版到chrome,目前正在审核中~
总结
可能我是一个老老实实上班族的一个菜狗,但是勇于尝试也挺好的,毕竟在尝试中,我能够不断学习、成长,迎接新的挑战,拓展自己的能力和视野。
历经三个月的努力,终于完成了第一版。其实本来应该在一个多月内完成的,但中途我可能有些懒惰哈哈。虽然我觉得自己做的东西还不尽如人意,但从某种角度来说,至少我迈出了那一步。哪怕是简单的东西,也是通过自己制造出来的,多多少少都有一些成就感。而且,这个过程也丰富了我的技术栈。因此,我在开始思考如何完成一个要好的项目,是否可以继续打造一款完整的产品,让人们使用,如果有人使用我的产品,我也会感到非常满足!
最后思考
1、有时候,当你不知道该做什么时,一定不要让自己闲着。我深以为然,这句话给了我很大启发,我记得是在阅读一篇文章时被深深触动的。
2、有时候,想得再多也不如行动来得有效。即使方向错误,但这也是我从中获得的宝贵经验。总结经验才能不断进步。
3、时间是可以挤出来的,再忙再累也会有时间的。看个人是否愿意付诸行动,但也要适当放松,保持身心健康。
第一次写文章还是有点乱七八糟的,但这也是成长的一部分。还得继续努力,不断学习,能够更好地传达我的想法和观点!
来源:juejin.cn/post/7341642966790144035
耗时两周,我终于自己搭了一个流媒体服务器
RTSP流媒体播放器
前言:因公司业务需求,研究了下在web端播放rtsp流视频的方案,过程很曲折,但也算是颇有收获。
播放要求
- web网页播放或者手机小程序播放
- 延迟小于500ms
- 支持多路播放
- 免费
舍弃的方案
- 【hls】延时非常高,有时候能达到几十秒,实时性场景直接pass
- 【转rtmp】需要借助flash插件
- 【转图片帧】需要后端借助工具将rtsp视频流每一帧转成图片,再通过websocket协议实时传输到前端,前端用canvas绘制,这种方法对后端转流要求较高,每张图片如果太大会掉帧,延时也不稳定
思路尝试
1、 flvjs + ffmpeg + websocket + node
这套方案的核心为 BiLiBiLi 开源的
flvjs
,原理是在后端利用 转流工具 FFmpeg 将rtsp流
转成flv流
,然后通过websocket
传输flv流
,在利用flvjs
解析成可以在浏览器播放的视频。
flv不支持ios 请自行取舍
2、WebRTC
Webrtc是前端的技术,后端使用有点困难,原理是将
rtsp流
转成webrtc流
,直接在video中播放(需要浏览器支持webrtc)
如何将rtsp转成webrtc 基于两个工具实现
参考链接1 :webrtcstreamer.js 前端实现
参考链接2 : mediamtx转流
3、jsmpeg.js + ffmpeg + websocket + node
这种方案是我测试过免费方案中效果最好的,原理是在后端利用 转流工具 FFmpeg 将
rtsp流
转成图片流
,然后通过websocket
传输图片
,在利用jsmpeg.js
绘制到canvas上显示
优点:
- 可以通时兼容多路视频,且对浏览器内存占用不会太高
- 延迟还可以 测试在300-1000ms左右
缺点:
- 多路无法使用主码流 会把浏览器卡死
- 清晰度不够,画面大概在720p左右
前后端代码放jsbin了 地址 :jsbin.com/hazacak/edi…
注意
使用时请下载ffmpeg 并把ffmpeg添加值环境变量
4 、终极方案:ZLMediaKit +h265webjs
该方案应该是此类问题的终极解决方案(个人认为,有好的方案请共享)
原理:
可以看到ZlMediaKit支持把各种流进行转换输出,我们可以使用输出的流进行播放
为了便捷 推荐你使用ZLM文档提供的Docker镜像,同时ZLM提供各种的restful AP供你使用,可以转流,推流等等,具体查看文档中的 restful API部分内容
其中需要注意的是 API中的secret 在镜像文件 /opt/mdeia/conf 文件夹下 请手动复制出来 每次构建镜像 改值会变化
另外 推荐一个ZLM 的管理界面 github.com/1002victor/…
只需要把代码全部复制到 www目录下即可放心食用
前端部分:
因为前端部分相关的视频库都存在部分协议不支持,没办法完全进行测试
故选择了ZLM官方推荐的 h265webjs这个播放库,经过测试,便捷容易,可安全食用
地址:
来源:juejin.cn/post/7399564369229496358
如何让 localStorage 存储变为响应式
背景
项目上有个更改时区的全局组件,同时还有一个可以更改时区的局部组件,想让更改时区的时候能联动起来,实时响应起来。
其实每次设置完时区的数据之后是存在了前端的 localStorage 里边,时间组件里边也是从 localStorage 拿去默认值来回显。如果当前页面不刷新,那么时间组件就不能更新到最新的 localStorage 数据。
怎么才能让 localStorage 存储的数也变成响应式呢?
实现
- 应该写个公共的方法,不仅仅时区数据能用,万一后边其他数据也能用。
- 项目是 React 项目,那就写个 hook
- 怎么才能让 localStorage 数据变成响应式呢?监听?
失败的案例 1
首先想到的是按照下边这种方式做,
useEffect(()=>{
console.log(11111, localStorage.getItem('timezone'))
},[localStorage.getItem('timezone')])
得到的测试结果肯定是失败的,但是为啥失败?我们也应该知道一下。查了资料说,使用 localStorage.getItem('timezone')
作为依赖项会导致每次渲染都重新计算依赖项,这不是正确的做法。
具体看一下官方文档:useEffect(setup, dependencies?)
在此说一下第二个参数 dependencies:
可选 dependencies
:setup
代码中引用的所有响应式值的列表。响应式值包括 props、state 以及所有直接在组件内部声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将验证是否每个响应式值都被正确地指定为一个依赖项。依赖项列表的元素数量必须是固定的,并且必须像 [dep1, dep2, dep3]
这样内联编写。React 将使用 Object.is
来比较每个依赖项和它先前的值。如果省略此参数,则在每次重新渲染组件之后,将重新运行 Effect 函数。
- 如果你的一些依赖项是组件内部定义的对象或函数,则存在这样的风险,即它们将 导致 Effect 过多地重新运行。要解决这个问题,请删除不必要的 对象 和 函数 依赖项。你还可以 抽离状态更新 和 非响应式的逻辑 到 Effect 之外。
如果你的 Effect 依赖于在渲染期间创建的对象或函数,则它可能会频繁运行。例如,此 Effect 在每次渲染后重新连接,因为 createOptions
函数 在每次渲染时都不同:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 此函数在每次重新渲染都从头开始创建
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // 它在 Effect 中被使用
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 因此,此依赖项在每次重新渲染都是不同的
// ...
}
失败的案例 2
一开始能想到的是监听,那就用 window 上监听事件。
在 React 应用中监听 localStorage
的变化,可以使用 window
对象的 storage
事件。这个事件在同一域名的不同文档之间共享,当某个文档修改 localStorage
时,其他文档会收到通知。
写代码...
// useRefreshLocalStorage.js
import { useState, useEffect } from 'react';
const useRefreshLocalStorage = (key) => {
const [storageValue, setStorageValue] = useState(
localStorage.getItem(key)
);
useEffect(() => {
const handleStorageChange = (event) => {
if (event.key === key) {
setStorageValue(event.newValue)
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, [key]);
return [storageValue];
};
export default useRefreshLocalStorage;
使用方式:
// useTimezone.js
import { useState, useEffect } from "react";
import { getTimezone, timezoneKey } from "@/utils/utils";
import useRefreshLocalStorage from "./useRefreshLocalStorage";
function useTimezone() {
const [TimeZone, setTimeZone] = useState(() => getTimezone());
const [storageValue] = useRefreshLocalStorage(timezoneKey);
useEffect(() => {
setTimeZone(() => getTimezone());
}, [storageValue]);
return [TimeZone];
}
export default useTimezone;
经过测试,失败了,没有效果!!!那到底怎么回事呢?哪里出现问题了?查阅��料经过思考,可能出现的问题的原因有:只能监听同源的两个页面之间的 storage 变更,没法监听同一个页面的变更。
成功的案例
import { useState, useEffect } from "react";
// 自定义 Hook,用于监听 localStorage 中指定键的变化
function useRefreshLocalStorage(localStorage_key) {
// 检查 localStorage_key 是否有效
if (!localStorage_key || typeof localStorage_key !== "string") {
return [null];
}
// 创建一个状态变量来保存 localStorage 中的值
const [storageValue, setStorageValue] = useState(
localStorage.getItem(localStorage_key)
);
useEffect(() => {
// 保存原始的 localStorage.setItem 方法
const originalSetItem = localStorage.setItem;
// 重写 localStorage.setItem 方法,添加事件触发逻辑
localStorage.setItem = function(key, newValue) {
// 创建一个自定义事件,用于通知 localStorage 的变化
const setItemEvent = new CustomEvent("setItemEvent", {
detail: { key, newValue },
});
// 触发自定义事件
window.dispatchEvent(setItemEvent);
// 调用原始的 localStorage.setItem 方法
originalSetItem.apply(this, [key, newValue]);
};
// 事件处理函数,用于处理自定义事件
const handleSetItemEvent = (event) => {
const customEvent = event;
// 检查事件的键是否是我们关心的 localStorage_key
if (event.detail.key === localStorage_key) {
// 更新状态变量 storageValue
const updatedValue = customEvent.detail.newValue;
setStorageValue(updatedValue);
}
};
// 添加自定义事件的监听器
window.addEventListener("setItemEvent", handleSetItemEvent);
// 清除事件监听器和还原原始方法
return () => {
// 移除自定义事件监听器
window.removeEventListener("setItemEvent", handleSetItemEvent);
// 还原原始的 localStorage.setItem 方法
localStorage.setItem = originalSetItem;
};
// 依赖数组,只在 localStorage_key 变化时重新运行 useEffect
}, [localStorage_key]);
// 返回当前的 storageValue
// 为啥没有返回 setStorageValue ?
// 因为想让用户直接操作自己真实的 “setValue” 方法,这里只做一个只读。
return [storageValue];
}
export default useRefreshLocalStorage;
具体的实现步骤如上,每一步也加上了注释。
接下来就是测试了,
useTimezone 针对 timezone 数据统一封装,
// useTimezone.js
import { useState, useEffect } from "react";
import { getTimezone, timezoneKey } from "@/utils/utils";
import useRefreshLocalStorage from "./useRefreshLocalStorage";
function useTimezone() {
const [TimeZone, setTimeZone] = useState(() => getTimezone());
const [storageValue] = useRefreshLocalStorage(timezoneKey);
useEffect(() => {
setTimeZone(() => getTimezone());
}, [storageValue]);
return [TimeZone];
}
export default useTimezone;
具体的业务页面组件中使用,
// 页面中
// ...
import useTimezone from "@/hooks/useTimezone";
export default (props) => {
// ...
const [TimeZone] = useTimezone();
useEffect(()=>{
console.log(11111, TimeZone)
},[TimeZone)
}
测试结果必须是成功的啊!!!
小结
其实想要做到该效果,用全局 store 状态管理也能做到,条条大路通罗马嘛!不过本次需求由于历史原因一直使用的是 localStorage ,索性就想着 如何让 localStorage 存储变为响应式 ?
不知道大家还有什么更好的方法吗?
来源:juejin.cn/post/7399461786348044325
接口一异常你的前端页面就直接崩溃了?
前言
在 JavaScript 开发中,细节处理不当往往会导致意想不到的运行时错误,甚至让应用崩溃。可能你昨天上完线还没问题,第二天突然一大堆人艾特你,你就说你慌不慌。
来吧,咱们来捋一下怎么做才能让你的代码更健壮,即使后端数据出问题了咱前端也能稳得一批。
解构失败报错
不做任何处理直接将后端接口数据进行解构
const handleData = (data)=> {
const { user } = data;
const { id, name } = user;
}
handleData({})
VM244:3 Uncaught TypeError: Cannot destructure property 'id' of 'user' as it is undefined.
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象(装箱)。由于 undefined 、null 无法转为对象,所以对它们进行解构赋值时就会报错。
所以当 data 为 undefined 、null 时候,上述代码就会报错。
第二种情况,虽然给了默认值,但是依然会报错
const handleData = (data)=> {
const { user = {} } = data;
const { id, name } = user;
}
handleData({user: null})
ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。
所以当 props.data
为 null
,那么 const { name, age } = null
就会报错!
good:
const handleData = (data)=> {
const { user } = data;
const { id, name } = user || {};
}
handleData({user: null})
数组方法调用报错
从接口拿回来的数据直接用当成数组来用
const handleData = (data)=> {
const { userList } = data;
const newList = userList.map((item)=> item.name)
}
handleData({userList: null})
handleData({userList: 123})
VM394:3 Uncaught TypeError: userList.map is not a function
那么问题来了,如果 userList 不符合预期,不是数组时必然就报错了,所以最好判断一下
good:
const handleData = (data)=> {
const { userList } = data;
if(Array.isArray(userList)){
const newList = userList.map((item)=> item.name)
}
}
handleData({userList: 123})
遍历对象数组报错
遍历对象数组时也要注意 null
或 undefined
的情况
const handleData = (data)=> {
const { userList } = data;
const newList = userList.map((item)=> item?.name)
}
handleData({userList: [ null, undefined ]})
VM547:3 Uncaught TypeError: Cannot read properties of null (reading 'name')
一旦数组中某项值是 undefined 或 null,那么 item.name 必定报错,可能又白屏了。
good:
const handleData = (data)=> {
const { userList } = data;
const newList = userList.map((item)=> item?.name)
}
handleData({userList: [null]})
但是如果是这种情况就不good了
const handleData = (data)=> {
const { userList } = data;
const newList = userList.map((item)=> `用户id是${item?.id},用户名字是${item?.name},用户年龄是${item?.age}岁了`);
}
handleData({userList: [null]})
? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name
,滥用会导致编译后的代码size增大。
good:
const handleData = (data)=> {
const { userList } = data;
const newList = userList.map((item)=> {
const { id, name, age } = item || {};
return `用户id是${id},用户名字是${name},用户年龄是${age}岁了`
});
}
handleData({userList: [null]})
当可选链操作符较多的情况时无论是性能还是可读性都明显上面这种方式更好。
复习一下装箱
大家可以思考一下,以下代码会不会报错
const handleData = (data)=> {
const { userList } = data;
const newList = userList.map((item)=> item.name)
}
handleData({userList: ['', 123]})
是不会报错的,因为在 JavaScript 中,当你在一些基本类型上直接访问属性时这些类型会被自动临时转换成它们对应的对象类型。这种转换称为“装箱”(boxing)。例如:
('').name
空字符串被临时转换成一个字符串对象。由于没有名为 name 的属性,所以它返回 undefined,但不会报错。
let str = "hello";
console.log(str.length); // 5
在这里,str.length 实际上是在字符串对象上调用的,而不是直接在基本类型字符串上。JavaScript 引擎在幕后将字符串 "hello" 装箱为 String 对象,因此可以访问 length 属性。
(123).name
数字 123 被临时转换成一个数字对象。由于没有名为 name 的属性,所以它返回 undefined,但不会报错。
let num = 123;
console.log(num.toFixed(2)); // "123.00"
num.toFixed(2) 调用了数字对象的 toFixed 方法。JavaScript 将数字 123 装箱为 Number 对象。
(null).name
null 是一个特殊的基本类型,当尝试访问其属性时会报错,因为 null 不能被装箱为对象。
try {
const name = (null).name; // TypeError: Cannot read property 'name' of null
} catch (error) {
console.error(error);
}
(undefined).name
undefined 也不能被装箱为对象。
try {
const name = (undefined).name; // TypeError: Cannot read property 'name' of undefined
} catch (error) {
console.error(error);
}
JavaScript 中的基本类型包括:
string
number
boolean
symbol
bigint
null
undefined
对应的对象类型是:
String
Number
Boolean
Symbol
BigInt
装箱的工作原理:
当你访问基本类型的属性或方法时,JavaScript 会自动将基本类型装箱为其对应的对象类型。这个临时的对象允许你访问属性和方法,但它是短暂的,一旦属性或方法访问完成,这个对象就会被销毁。
需要注意的是,null 和 undefined 没有对应的对象类型,不能被装箱。所以访问它们的属性或方法会直接报错!所以时刻警惕 null
和 undefined
这俩坑。
使用对象方法时报错
同理,只要变量能被转成对象,就可以使用对象的方法,但是 undefined 和 null 无法转换成对象。对其使用对象方法时就会报错。
const handleData = (data)=> {
const { user } = data;
const newList = Object.entries(user);
}
handleData({user: null});
VM601:3 Uncaught TypeError: Cannot convert undefined or null to object
下面这两种优化方式都可
good:
const handleData = (data)=> {
const { user } = data;
const newList = Object.entries(user || {})
}
handleData({user: null})
good:
/**
* 判断给定值的类型或获取给定值的类型名称。
*
* @param {*} val - 要判断类型的值。
* @param {string} [type] - 可选,指定的类型名称,用于检查 val 是否属于该类型。
* @returns {string|boolean} - 如果提供了 type 参数,返回一个布尔值表示 val 是* 否属于该类型;如果没有提供 type 参数,返回 val 的类型名称(小写)。
*
* @example
* // 获取类型名称
* console.log(judgeDataType(123)); // 输出 'number'
* console.log(judgeDataType([])); // 输出 'array'
*
* @example
* // 判断是否为指定类型
* console.log(judgeDataType(123, 'number')); // 输出 true
* console.log(judgeDataType([], 'array')); // 输出 true
*/
function judgeDataType(val, type) {
const dataType = Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
return type ? dataType === type : dataType;
}
const handleData = (data)=> {
const { user } = data;
// 判断是否为对象
if(judgeDataType({}, "object")){
const newList = Object.entries(user || {})
}
}
handleData({user: null})
async/await 报错未捕获
这个也是比较容易犯且低级的错误
import React, { useState } from 'react';
const List = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await fetchListData();
setLoading(false);
}
}
如果 fetchListData() 执行报错,页面就会一直在加载中,所以一定要捕获一下。
good:
const List = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}
当然如果觉得这种方式不优雅,用 await-to-js
库或者其他方式都可以,记得捕获就行。
JSON.parse报错
如果传入的不是一个有效的可被解析的 JSON 字符串就会报错啦。
const handleData = (data)=> {
const { userStr } = data;
const user = JSON.parse(userStr);
}
handleData({userStr: 'fdfsfsdd'})
16:06:57.521 VM857:1 Uncaught SyntaxError: Unexpected token 'd', "fdfsfsdd" is not valid JSON
这里没必要去判断一个字符串是否为有效的 JSON 字符串,只要利用 trycatch 来捕获错误即可。
good:
const handleData = (data)=> {
const { userStr } = data;
try {
const user = JSON.parse(userStr);
} catch (error) {
console.error('不是一个有效的JSON字符串')
}
}
handleData({userStr: 'fdfsfsdd'})
动态导入模块失败报错
动态导入某些模块时,也要注意可能会报错
const loadModule = async () => {
const module = await import('./dynamicModule.js');
module.doSomething();
}
如果导入的模块存在语法错误、网络或者跨域问题、文件不存在、循环依赖、甚至文件非常大导致内存不足、模块内的运行时错误等都有可能阻塞后续代码执行。
good:
const loadModule = async () => {
try {
const module = await import('./dynamicModule.js');
module.doSomething();
} catch (error) {
console.error('Failed to load module:', error);
}
}
API 兼容性问题报错
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
低版本 Node 不支持 fetch
,需要更高兼容性的场景使用 axios
等更好。
其他包括小程序开发,web开发等也同理,如果使用了某些不支持的 es 新特性或者较新版本的平台才支持的api也会导致直接报错,使用时做好判断或直接用兼容性更好的写法。
框架在编译时已经帮我们解决了大部分的兼容性问题,但是有些场景还需额外注意。
内存溢出崩溃
滥用内存缓存可能会导致内存溢出
const cache = {};
function addToCache(key, value) {
cache[key] = value;
// 没有清理机制,缓存会无限增长
}
避免闭包持有大对象的引用
function createClosure() {
const largeData = new Array(1000000).fill('x');
return function() {
console.log(largeData.length);
};
}
const closure = createClosure();
// largeData 现在被闭包引用会一直存活在内存中,即使不再直接使用
closure = null; // 手动解除引用
记得清除定时器和事件监听器
// React
useEffect(() => {
const timeoutId = setTimeout(() => {
// 一些操作
}, 1000);
return () => clearTimeout(timeoutId);
}, []);
function setupHandler() {
const largeData = new Array(1000000).fill('x');
const handler = function() {
console.log(largeData.length);
};
document.getElementById('myButton').addEventListener('click', handler);
return function cleanup() {
document.getElementById('myButton').removeEventListener('click', handler);
};
}
const cleanup = setupHandler();
// 在适当的时候调用
// cleanup();
还有深度递归,JSON.parse()
解析超大数据等都可能会对内存造成压力。
总结
以上列举了js在运行时可能会发生错误而导致的应用崩溃的一些边界情况,这些都是在开发时不那么容易察觉,eslint等静态检查工具也无能为力的场景,当然如果用typescript的话还是可以帮助我们避免大部分坑的,如果不用 ts 的话就不可避免的需要考虑这些情况才能写出健壮的代码。
边界场景的容错一定要做,原则上不信任任何外部输入数据的存在性和类型,历史经验告诉我们,不做容错出错只是早晚的事。
帮别人review代码的时候也可以参考以上清单,如果大家还有补充欢迎讨论,最后祝各位大佬没有bug。
来源:juejin.cn/post/7388022210856222732
为了检测360浏览器,我可是煞费苦心……
众所周知,360浏览器一向是特立独行的存在,为了防止用户识别它,隐藏了自己的用户代理(User-Agent)。只有在自己域名下访问,用户代理(User-Agent)才暴露出自己的特征。显然,这样对于开发者想要识别它,造成了不少麻烦。
非360官方网站访问
360官方网站访问
为了识别出360,只能通过Javascript检测360的特殊属性,找和其他浏览器的区别。最常见的方式就是找navigator.mimeTypes
或者navigator.plugins
有哪些不一样的值。为此,我在各个版本的360浏览器(360安全浏览器、360极速浏览器)也找到了各种可以利用的特征。
比如,无论 360安全浏览器 还是 360极速浏览器 的navigator.mimeTypes
都可能存在
application/360softmgrplugin
, application/mozilla-npqihooquicklogin
, application/npjlgplayer3-chrome-jlp
,application/vnd.chromium.remoting-viewer
这几种类型,我们可以通过判断这几个值是否存在识别 360浏览器。不仅如此,在早期的360浏览器中,明明是chrome内核,还保留着IE时代有了showModalDialog
方法,这些都可以用来做识别的依据。
import getMime from '../method/getMime.js';
import _globalThis from '../runtime/globalThis.js';
export default {
name: '360',
match(ua) {
let isMatch = false;
if (_globalThis?.chrome) {
let chrome_version = ua.replace(/^.*Chrome\/([\d]+).*$/, '$1');
if (getMime("type", "application/360softmgrplugin") || getMime("type", "application/mozilla-npqihooquicklogin") || getMime("type", "application/npjlgplayer3-chrome-jlp")) {
isMatch = true;
} else if (chrome_version > 36 && _globalThis?.showModalDialog) {
isMatch = true;
} else if (chrome_version > 45) {
isMatch = getMime("type", "application/vnd.chromium.remoting-viewer");
if (!isMatch && chrome_version >= 69) {
isMatch = getMime("type", "application/asx");
}
}
}
return ua.includes('QihooBrowser')
||ua.includes('QHBrowser')
||ua.includes(' 360 ')
||isMatch;
},
version(ua) {
return ua.match(/QihooBrowser(HD)?\/([\d.]+)/)?.[1]
||ua.match(/Browser \(v([\d.]+)/)?.[1]
||'';
}
};
然而,360并不是只有1种浏览器,还包含了 360安全浏览器, 360极速浏览器,360 AI浏览器等。我们又怎么区分呢,这时候还要寻找它们之间的差别。360AI浏览器较为简单,用户代理(User-Agent)中直接暴露相关信息。
我发现有一个值是360安全浏览器独有的,那就是application/gameplugin
,应该是浏览器内置的游戏插件。可是在后续的版本中也消失了,我又发现navigator.userAgentData.brands
里的值也有细微区别。于是就可以如下处理:
import getMime from '../method/getMime.js';
import _Chrome from './Chrome.js';
import _360 from './360.js';
import _globalThis from '../runtime/globalThis.js';
export default {
name:'360SE',
match(ua,isAsync=false){
let isMatch = false;
if(_360.match(ua)){
if(getMime("type", "application/gameplugin")){
isMatch = true;
}else if(_globalThis?.navigator?.userAgentData?.brands.filter(item=>item.brand=='Not.A/Brand').length){
isMatch = true;
}
}
return ua.includes('360SE')||isMatch;
},
version(ua){
let hash = {
'122':'15.3',
'114':'15.0',
'108':'14.0',
'86':'13.0',
'78':'12.0',
'69':'11.0',
'63':'10.0',
'55':'9.1',
'45':'8.1',
'42':'8.0',
'31':'7.0',
'21':'6.3'
};
let chrome_version = parseInt(_Chrome.version(ua));
return hash[chrome_version]||'';
}
};
而 360极速浏览器的识别依据就相对较多了!各种身份验证的插件都能在里面找到。
import getMime from '../method/getMime.js';
import _Chrome from './Chrome.js';
import _360 from './360.js';
import _globalThis from '../runtime/globalThis.js';
export default {
name:'360EE',
match(ua){
let isMatch = false;
if(getMime('type','application/cenroll.cenroll.version.1')||getMime('type','application/hwepass2001.installepass2001')){
isMatch = true;
}else if(_360.match(ua)){
if(_globalThis?.navigator?.userAgentData?.brands.find(item=>item.brand=='Not A(Brand'||item.brand=='Not?A_Brand')){
isMatch = true;
}
}
return ua.includes('360EE')||isMatch;
},
version(ua){
let hash = {
'122':'22.3', // 360极速X
'119':'22.0', // 360极速X
'108':'14.0', // 360极速
'95':'21.0', // 360极速X
'86':'13.0',
'78':'12.0',
'69':'11.0',
'63':'9.5',
'55':'9.0',
'50':'8.7',
'30':'7.5'
};
let chrome_version = parseInt(_Chrome.version(ua));
return ua.match(/Browser \(v([\d.]+)/)?.[1]
||hash[chrome_version]
||'';
}
};
可惜的是在Mac系统中的情况复杂点,这些插件的方法都不存在,这下又失去了判断的依据了。还有,在一次无意打开网络连接一次的时候,发现了360浏览器在请求一个奇怪的地址,居然返回了浏览器版本信息。
import _globalThis from '../runtime/globalThis.js';
const GetDeviceInfo = () => {
return new Promise((resolve) => {
const randomCv = `cv_${new Date().getTime() % 100000}${Math.floor(Math.random()) * 100}`
const params = { key: 'GetDeviceInfo', data: {}, callback: randomCv }
const Data = JSON.stringify(params)
if(_globalThis?.webkit?.messageHandlers){
_globalThis.webkit.messageHandlers['excuteCmd'].postMessage(Data)
_globalThis[randomCv] = function (response) {
delete _globalThis[randomCv];
resolve(JSON.parse(response||'{}'));
}
}else{
return resolve({});
}
})
};
export default {
name: '360EE',
match(ua) {
return GetDeviceInfo().then(function(response){
return response?.pid=='360csexm'||false;
});
},
version(ua) {
return GetDeviceInfo().then(function(response){
return response?.module_version||'';
});
}
};
原本觉得一切应该就这么顺利了,然后当我从Windows10迁移到windows11的时候,发现原来的浏览器中的插件特征识别已经失效了,我找不到360安全浏览器的识别特征。
于是我疯了……我开始一个个属性对比差异,就是找不到有什么特征是可以区分开的。就在我一筹莫展的时候,我无意间发现,我自己的网站在360安全浏览器中,莫默其妙多了一个奇怪的节点,看样子是一个AI组件。我敢确定,这个节点并不是我写的,于是我断言是360做了什么特殊处理。
经过分析,我发现这是360安全浏览器内置的一个“扩展程序 - 360智脑” ,是默认安装的而且无法卸载。我脑子一下子亮起来了,心想着我可以根据这个节点判断啊,只要检测它是否加载就能判断出来。于是,我写了以下代码:
// 根据检测文档中是否被插入“360智脑”组件,判断是否为360安全浏览器
if(!document?.querySelector('#ai-assist-root')){
return new Promise(function(resolve){
let hander = setTimeout(function(){
resolve(false);
},1500);
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(function($item){
if($item.id=='ai-assist-root'){
hander&&clearTimeout(hander);
resolve(true);
}
});
}
});
});
observer.observe(document,{childList: true, subtree: true});
});
}
但确实也有不足之处,我只能判断出什么时候加载了节点,但是无法对非360浏览器不加载进行判断。只能通过定时器去做超时处理,这就意味着非360浏览器判断需要一定耗时,这样太不友好了。
就在这时候,我发现这个“扩展程序”是需要加载资源的,而这个资源在浏览器本地。也就意味着,我可以直接直接对资源进行判断,这样并不需要等插件加载超时判断,非360安全浏览器可以较快地判断出“非他”的条件。
// 根据判断扩展程序CSS是否加载成功判断是否为360安全浏览器
return new Promise(function(resolve){
fetch('chrome-extension://fjbbmgamncjadhlpmffehlmmkdnkiadk/css/content.css').then(function(){
resolve(true);
}).catch(function(){
resolve(false);
});
});
于是我终于可以判断出这烦人的“小妖精”了!而这也是浏览器嗅探的其中一项工作,为此我还做了诸如:操作系统、屏幕、处理器架构、GPU、CPU、IP地址、时区、语言、网络等浏览器信息的判断和识别。
浏览器在线检测: passer-by.com/browser/
开源项目仓库地址:github.com/mumuy/brows…
如果你对此感兴趣或者有什么内容要补充,欢迎关注此项目~
来源:juejin.cn/post/7390588322768748580
因为不知道Object.keys,被嫌弃了
关联精彩文章:# 改进tabs组件切换效果,丝滑的动画获得一致好评~
背景
今天,同事看到了我一段遍历读取对象key的代码后,居然嘲笑我技术菜(虽然我确实菜)
const person = {
name: '员工1',
age: 30,
profession: 'Engineer'
// ....
};
// 使用 for 循环读取对象的键
const keys = Object.keys(person);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = person[key];
console.log(key + ': ' + value);
}
我说,用for循环读取对象的key不对吗,他二话不说直接给我改代码
const person = {
name: '员工1',
age: 30,
profession: 'Engineer'
};
Object.keys(person).forEach(key => {
console.log(key + ': ' + person[key]);
});
然后很装的走了......钢铁直男啊!我很生气,我这个人比较较劲,我一定要把Object.keys吃透!
Object.keys的基础用法
语法
Object.keys
是 JavaScript 中的一个方法,用于获取对象自身的可枚举属性名称,并以数组形式返回。
Object.keys(obj)
- 参数:要返回其枚举自身属性的对象
- 返回值:一个表示给定对象的所有可枚举属性的字符串数组
不同入参的返回值
- 处理对象,返回可枚举的属性数组
let person = {name:"张三",age:25,address:"深圳",getName:function(){}}
Object.keys(person)
// ["name", "age", "address","getName"]
- 处理数组,返回索引值数组
let arr = [1,2,3,4,5,6]
Object.keys(arr)
// ["0", "1", "2", "3", "4", "5"]
- 处理字符串,返回索引值数组
let str = "saasd字符串"
Object.keys(str)
// ["0", "1", "2", "3", "4", "5", "6", "7"]
Object.keys的常用技巧
Object.keys在处理对象属性、遍历对象和动态生成内容时非常有用。
遍历对象属性
当你需要遍历一个对象的属性时,Object.keys
可以将对象的所有属性名以数组形式返回,然后你可以使用 forEach
或 for...of
来遍历这些属性名。
示例:
const person = {
name: '员工1',
age: 30,
profession: 'Engineer'
};
Object.keys(person).forEach(key => {
console.log(key + ': ' + person[key]);
});
输出:
name: 员工1
age: 30
profession: Engineer
获取对象属性的数量
可以使用 Object.keys
获取对象的属性名数组,然后通过数组的 length
属性来确定对象中属性的数量。
示例:
const person = {
name: '员工2',
age: 30,
profession: 'Engineer'
};
const numberOfProperties = Object.keys(person).length;
console.log(numberOfProperties); // 输出: 3
过滤对象属性
可以使用 Object.keys
来获取对象的属性名数组,然后使用数组的 filter
方法来筛选属性名,从而创建一个新的对象。
示例:
const person = {
name: '员工3',
age: 30,
profession: '钢铁直男'
};
const filteredKeys = Object.keys(person).filter(key => key !== 'age');
const filteredPerson = {};
filteredKeys.forEach(key => {
filteredPerson[key] = person[key];
});
console.log(filteredPerson); // 输出: { name: '员工3', profession: '钢铁直男' }
检查对象是否为空
可以通过检查 Object.keys
返回的数组长度来确定对象是否为空。
示例:
const emptyObject = {};
const nonEmptyObject = { name: '讨厌的人' };
function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
console.log(isEmpty(emptyObject)); // 输出: true
console.log(isEmpty(nonEmptyObject)); // 输出: false
深拷贝对象
虽然 Object.keys
本身并不能进行深拷贝,但它可以与其他方法结合使用来创建对象的深拷贝,特别是当对象的属性是另一层对象时。
示例:
const person = {
name: '快乐就是哈哈哈',
age: 18,
profession: 'coder',
address: {
city: 'Wonderland',
postalCode: '12345'
}
};
function deepCopy(obj) {
const copy = {};
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
copy[key] = deepCopy(obj[key]);
} else {
copy[key] = obj[key];
}
});
return copy;
}
const copiedPerson = deepCopy(person);
console.log(copiedPerson);
是小姐姐,不是小哥哥~
来源:juejin.cn/post/7392115478549069861
JavaScript实现访问本地文件夹
这个功能放在之前是不可能实现的,因为考虑到用户的隐私,但是最近有一个新的api可以做到这一点。下面来进行一个简单的功能实现。
如何选择文件夹
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button>打开文件夹</button>
<script>
var btn = document.querySelector('button');
btn.onclick=function() {
showDirectoryPicker()
}
</script>
</body>
</html>
我们调用showDirectoryPicker这个函数就可以实现一个选择文件夹的功能。
showDirectoryPicker()
options
可选
选项对象,包含以下属性:
id
通过指定 ID,浏览器能够记住不同 ID 所对应的目录。当使用相同的 ID 打开另一个目录选择器时,选择器会打开相同的目录。mode
字符串,默认为"read"
,可对目录进行只读访问。设为"readwrite"
可对目录进行读写访问。startIn
一个FileSystemHandle
对象或者代表某个众所周知的目录的字符串(如:"desktop"
、"documents"
、"downloads"
、"music"
、"pictures"
、"videos"
)。用于指定选择器的起始目录。
返回值
一个 Promise
对象,会兑现一个 FileSystemDirectoryHandle
(en-US) 对象。
异常
AbortError
当用户直接关闭了目录选择器或选择的目录是敏感目录时将会抛出 AbortError。
如何得到文件夹中的文件/子文件夹
首先对于上面所写的东西,我们进行try catch的优化
try {
// 获得文件夹的句柄
const handle = await showDirectoryPicker();
}
catch {
//用户拒绝查看文件
alert('访问失败')
}
之后我们来看一下这个headler打印出来是什么
句柄的简单解释
对于“句柄”,在下一直停留在一知半解的认识层面,近日在下学习Windows编程,决定趁此机会将句柄彻底搞清楚。查阅了一些网络上的资料,发现网络上的讲解大概可以分为两类:一种是以比喻、类比的方式说明,这种方法虽然形象易懂,但并没有从原理上、本质上加以揭示,让人仍然想问“为什么?”、“怎么实现?”。另一种是给出源代码,无可厚非,这当然是最本质的说明了,但这样一来,又显得不够直观,初学者理解起来有一定的难度。鉴于此,在下尽微末之能,结合自己的愚见,在两者之间折中,用图解的方式来将原理呈现出来,做到一目了然。
这里需要说明:
1.这里将句柄所能标识的所有东西(如窗口、文件、画笔等)统称为“对象”。
2.图中一个小横框表示一定大小的内存区域,并不代表一个字节,如标有0X00000AC6的横框表示4个字节。
3.图解的目的是为了直观易懂,所以不一定与源码完全对应,会有一定的简化。
让我们先看图,再解释。
其中,图1是程序运行到某时刻时的内存快照,图2是程序往后运行到另一时刻时的内存快照。红色部分标出了两次的变化。
简单解释:
Windows是一个以虚拟内存为基础的操作系统,很多时候,进程的代码和数据并不全部装入内存,进程的某一段装入内存后,还可能被换出到外存,当再次需要时,再装入内存。两次装入的地址绝大多数情况下是不一样的。也就是说,同一对象在内存中的地址会变化。(对于虚拟内存不是很了解的读者,可以参考有关操作系统方面的书籍)那么,程序怎么才能准确地访问到对象呢?为了解决这个问题,Windows引入了句柄。
系统为每个进程在内存中分配一定的区域,用来存放各个句柄,即一个个32位无符号整型值(32位操作系统中)。每个32位无符号整型值相当于一个指针,指向内存中的另一个区域(我们不妨称之为区域A)。而区域A中存放的正是对象在内存中的地址。当对象在内存中的位置发生变化时,区域A的值被更新,变为当前时刻对象在内存中的地址,而在这个过程中,区域A的位置以及对应句柄的值是不发生变化的。这种机制,用一种形象的说法可以表述为:有一个固定的地址(句柄),指向一个固定的位置(区域A),而区域A中的值可以动态地变化,它时刻记录着当前时刻对象在内存中的地址。这样,无论对象的位置在内存中如何变化,只要我们掌握了句柄的值,就可以找到区域A,进而找到该对象。而句柄的值在程序本次运行期间是绝对不变的,我们(即系统)当然可以掌握它。这就是以不变应万变,按图索骥,顺藤摸瓜。
**所以,我们可以这样理解Windows **句柄:
数值上,是一个32位无符号整型值(32位系统下);逻辑上,相当于指针的指针;形象理解上,是Windows中各个对象的一个唯一的、固定不变的ID;作用上,Windows使用句柄来标识诸如窗口、位图、画笔等对象,并通过句柄找到这些对象。
下面,关于句柄,再交代一些关键性细节:
1.所谓“唯一”、“不变”是指在程序的一次运行中。如果本次运行完,关闭程序,再次启动程序运行,那么这次运行中,同一对象的句柄的值和上次运行时比较,一般是不一样的。
其实这理解起来也很自然,所谓“一把归一把,这把是这把,那把是那把,两者不相干”(“把”是形象的说法,就像打牌一样,这里指程序的一次运行)。
2.句柄是对象生成时系统指定的,属性是只读的,程序员不能修改句柄。
3.不同的系统中,句柄的大小(字节数)是不同的,可以使用sizeof()来计算句柄的大小。
4.通过句柄,程序员只能调用系统提供的服务(即API调用),不能像使用指针那样,做其它的事。
再回归正题。
处理句柄函数
async function processHandler(handle) {
if (handle.kind==='file'){
return handle
}
handle.children=[]
const iter = await handle.entries();//获得文件夹中的所有内容
//iter:异步迭代器
for await (const info of iter){
var subHandle = await processHandler(info[1]);
handle.children.push(subHandle)
}
return handle
}
如何得到文件内容
const root = await processHandler(handle);
// 获得文件内容
const file = await root.children[1].getFile();
const reader = new FileReader();
reader.onload=e=>{
// 读取结果
console.log(e.target.result)
}
reader.readAsText(file,'utf-8')
这里用到的就是一个很简单的文件读了。
下面是完整的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button>打开文件夹</button>
<script>
var btn = document.querySelector('button');
btn.onclick=async function() {
try {
// 获得文件夹的句柄
const handle = await showDirectoryPicker();
const root = await processHandler(handle);
// 获得文件内容
const file = await root.children[1].getFile();
const reader = new FileReader();
reader.onload=e=>{
// 读取结果
console.log(e.target.result)
}
reader.readAsText(file,'utf-8')
}
catch {
//用户拒绝查看文件
alert('访问失败')
}
}
async function processHandler(handle) {
if (handle.kind==='file'){
return handle
}
handle.children=[]
const iter = await handle.entries();//获得文件夹中的所有内容
//iter:异步迭代器
for await (const info of iter){
var subHandle = await processHandler(info[1]);
handle.children.push(subHandle)
}
return handle
}
</script>
</body>
</html>
来源:juejin.cn/post/7268011328940769315
三大微前端框架,谁是你的理想型?
1. 分享目标:
2. 什么是微前端?
故事开始于三年前…
小明为公司重构了一版新的管理后台,采用了市面上最流行的SPA渲染模式,具体技术栈使用的是 react + react-router。

项目第一版很快就顺利上线了,但在后续的迭代中,遇到一个棘手的问题:产品经理希望快速复用之前项目的某些页面。这让小明犯了难,因为老项目是用“上古神器” jQuery 写的,完全重构成 react,成本非常高。这时后端老哥丢过来一句:“你们前端用 iframe 嵌进来就可以了吧? ” 小明心里很清楚 iframe 有许多小毛病,但在当时,也确实没有比它更好的选择了。
上线后,随着时间的推移,用户产生了困惑:
- 为什么这个页面的弹框不居中了?
- 为什么这个页面的跳转记录无法保存? ...
小明心里其实非常清楚,这一切都是 iframe 带来的弊端。
时间来到三年后的今天,小明听说微前端能够解决 iframe 的各种疑难杂症,于是展开了调研。
市面上对微前端的定义让人眼花缭乱,比如微前端是:

这里给出我对微前端最接地气的定义:

故事开始于三年前…
小明为公司重构了一版新的管理后台,采用了市面上最流行的SPA渲染模式,具体技术栈使用的是 react + react-router。
项目第一版很快就顺利上线了,但在后续的迭代中,遇到一个棘手的问题:产品经理希望快速复用之前项目的某些页面。这让小明犯了难,因为老项目是用“上古神器” jQuery 写的,完全重构成 react,成本非常高。这时后端老哥丢过来一句:“你们前端用 iframe 嵌进来就可以了吧? ” 小明心里很清楚 iframe 有许多小毛病,但在当时,也确实没有比它更好的选择了。
上线后,随着时间的推移,用户产生了困惑:
- 为什么这个页面的弹框不居中了?
- 为什么这个页面的跳转记录无法保存? ...
小明心里其实非常清楚,这一切都是 iframe 带来的弊端。
时间来到三年后的今天,小明听说微前端能够解决 iframe 的各种疑难杂症,于是展开了调研。
市面上对微前端的定义让人眼花缭乱,比如微前端是:
这里给出我对微前端最接地气的定义:
“类似于iframe的效果,但没有它带来的各种问题”——小明。
3. 主流技术方向分类
首先,“微前端”作为近几年国内前端界最火的技术之一,目前存在多个技术流派。我按照它们对 iframe 看法的不同,将主流微前端方案分为了三大派系:革新派、改良派、中间派。

首先,“微前端”作为近几年国内前端界最火的技术之一,目前存在多个技术流派。我按照它们对 iframe 看法的不同,将主流微前端方案分为了三大派系:革新派、改良派、中间派。
3.1. 革新派 qiankun
以 qiankun 为主的革新派认为: iframe 问题很多,应避免使用它。 完全可以利用现有的前端技术自建一套应用隔离渲染方案。
以 qiankun 为主的革新派认为: iframe 问题很多,应避免使用它。 完全可以利用现有的前端技术自建一套应用隔离渲染方案。
3.1.1. 原理:
3.1.1.1. 基于 single-spa
将路由切换与子应用加载、卸载等生命周期结合起来是微前端的一项核心能力。这一步 qiankun 是基于 single-spa 实现的,不同的是它支持以 html 作为加载子应用的入口,不必像 single-spa 那样需要手动梳理资源链接,内部插件 import-html-entry 会自动分析 html 以获取 js 和 css。

将路由切换与子应用加载、卸载等生命周期结合起来是微前端的一项核心能力。这一步 qiankun 是基于 single-spa 实现的,不同的是它支持以 html 作为加载子应用的入口,不必像 single-spa 那样需要手动梳理资源链接,内部插件 import-html-entry 会自动分析 html 以获取 js 和 css。
3.1.1.2. 样式隔离
为了确保子应用之间样式互不影响,qiankun 内置了三种样式隔离模式:
- 默认模式。
原理是加载下一个子应用时,将上一个子应用的 、
等样式相关标签通通删除与替换,来实现样式隔离。缺点是仅支持单例模式(同一时间只能渲染单个子应用),且没法做到主子应用及多个子应用之间的样式隔离。
- 严格模式。
为了确保子应用之间样式互不影响,qiankun 内置了三种样式隔离模式:
- 默认模式。
原理是加载下一个子应用时,将上一个子应用的 、
等样式相关标签通通删除与替换,来实现样式隔离。缺点是仅支持单例模式(同一时间只能渲染单个子应用),且没法做到主子应用及多个子应用之间的样式隔离。
- 严格模式。
可通过 strictStyleIsolation:true
开启。原理是利用 webComponent 的 shadowDOM 实现。但它的问题在于隔离效果太好了,在目前的前端生态中有点水土不服,这里举两个例子。
- 可能会影响 React 事件。比如这个issue 当 Shadow Dom 遇上 React 事件 ,大致原因是在 React 中事件是“合成事件”,在React 17 版本之前,所有用户事件都需要冒泡到 document 上,由 React 做统一分发与处理,如果冒泡的过程中碰到 shadowRoot 节点,就会将事件拦截在 shadowRoot 范围内,此时
event.target
强制指向 shadowRoot,导致在 react 中事件无响应。React 17 之后事件监听位置由 document 改为了挂载 App 组件的 root 节点,就不存在此问题了。
- 弹框样式丢失。 原因是主流UI框架比如 antd 为了避免上层元素的样式影响,通常会把弹框相关的 DOM 通过
document.body.appendChild
插入到顶层 body 的下边。此时子应用中 antd 的样式规则,由于开启了 shadowDom ,只对其下层的元素产生影响,自然就对全局 body 下的弹框不起作用了,造成了样式丢失的问题。
解决方案:调整 antd 入参,让其在当前位置渲染。
- 实验模式。
可通过 experimentalStyleIsolation:true
开启。 原理类似于 vue 的 scope-css,给子应用的所有样式规则增加一个特殊的属性选择器,限定其影响范围,达到样式隔离的目的。但由于需要在运行时替换子应用中所有的样式规则,所以目前性能较差,处于实验阶段。
3.1.1.3. JS 沙箱
确保子应用之间的“全局变量”不会产生冲突。
- 快照沙箱( snapshotSandbox )
- 激活子应用时,对着当前
window
对象照一张相(所有属性 copy 到一个新对象windowSnapshot
中保存起来)。 - 离开子应用时,再对着
window
照一张相,对比离开时的window
与激活时的 window (也就是windowSnapshot
)之间的差异。- 记录变更。Diff 出在这期间更改了哪些属性,记录在
modifyPropsMap
对象中。 - 恢复环境。依靠
windowSnapshot
恢复之前的window
环境。
- 记录变更。Diff 出在这期间更改了哪些属性,记录在
- 下次激活子应用时,从
modifyPropsMap
对象中恢复上一次的变更。
- 单例的代理沙箱 ( LegacySanbox )
与快照沙箱思路很相似,但它不用通过 Diff 前后 window 的方式去记录变更,而是通过 ES6的 Proxy 代理 window 属性的 set 操作来记录变更。由于不用反复遍历 window,所以性能要比快照沙箱好。
- 支持多例的代理沙箱( ProxySandbox )
以上两种沙箱机制,都只支持单例模式(同一页面只支持渲染单个子应用)。
原因是:它们都直接操作的是全局唯一的 window。此时机智的你肯定想到了,假如为每个子应用都分配一个独立的“虚拟window”,当子应用操作 window 时,其实是在各自的“虚拟 window”上操作,不就可以实现多实例共存了?事实上,qiankun 确实也是这样做的。
既然是“代理”沙箱,那“代理”在这的作用是什么呢?
主要是为了实现对全局对象属性 get、set 的两级查找,优先使用fakeWindow,特殊情况(set命中白名单或者get到原生属性)才会改变全局真实window。
如此,qiankun 就对子应用中全局变量的 get 、 set 都实现了管控与隔离。
3.1.2. 优势:
3.1.2.1. 具有先发优势
2019年开源,是国内最早流行起来的微前端框架,在蚂蚁内外都有丰富的应用,后期维护性是可预测的。
3.1.2.2. 开箱即用
虽然是基于国外的 single-spa 二次封装,但提供了更加开箱即用的 API,比如支持直接以 HTML 地址作为加载子应用的入口。
3.1.2.3. 对 umi 用户更加友好
有现成的插件 @umijs/plugin-qiankun 帮助降低子应用接入成本。
3.1.3. 劣势:
3.1.3.1. vite 支持性差
由上可知,代理沙箱实现的关键是需要将子应用的 window “替换”为 fakeWindow,在这一步 qiankun 是通过函数 window 同名参数 + with 作用域绑定的方式,更改子应用 window 指向为 fakeWindow,最终使用 eval(...) 解析运行子应用的代码。
const jsCode = `
(function(window, self, globalThis){
with(this){
// your code
window.a = 1;
b = 2
...
}
}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);
`
eval(jsCode)
问题就出在这个 eval 上, vite 的构建产物如果不做特殊降级,默认打包出的就是 ESModule 语法的代码,使用 eval 解析运行会报下图这个错误。
报错的大意是, import 语法的代码必须放在 中执行。
官方目前推荐的解决方法是关闭沙箱... 但其实还有另一种比较取巧的方案:vite 生态里有一款专门兼容此问题的vite-plugin-qiankun 插件,它的原理是: eval 虽然没办法执行静态 import 语法,但它可以执行动态 import(...) 语法。
所以这款插件的解决方案就是替换子应用代码中的静态 import 为动态 import(),以绕过上述限制。
3.1.3.2. 子应用接入成本较高,详细步骤参考子应用接入文档
umi 用户可忽略这点,尤其是 @umi/max 用户,相比 webpack 接入成本要低很多。
3.1.3.3. JS 沙箱存在性能问题,且并不完善。
大致原因是 with + proxy 带来的性能损耗,详见 JS沙箱的困境 。当然 qiankun 官方也在针对性的进行优化,进展在这篇《改了 3 个字符,10倍的沙箱性能提升?!!》文章中可见一斑 。
3.2. 改良派 wujie
3.2.1. 原理:
wujie 是腾讯出品的一款微前端框架。作为改良派的代表,它认为: iframe 虽然问题很多,但仅把它作为一个 js 沙箱去用,表现还是很稳定的,毕竟是浏览器原生实现的,比自己实现 js 沙箱靠谱多了。至于 iframe 的弊端,可以针对性的去优化:
- DOM 渲染无法突破 iframe 边界?(弹框不居中问题)
那 DOM
就不放 iframe
里渲染了,而是单独提取到一个 webComponent
里渲染,顺便用 shadowDOM
解决样式隔离的问题。
简单说,无界的方案就是:JS 放 iframe 里运行,DOM 放 webComponent 渲染。
那么问题来了: 用 JS 操作 DOM 时,两者如何联系起来呢?毕竟 JS 默认操作的总是全局的 DOM。无界在此处用了一种比较 hack 的方式:代理子应用中所有的 DOM 操作,比如将 document
下的 getElementById、querySelector、querySelectorAll、head、body
等查询类 api 全部代理到 webComponent
。
下图是子应用真实运行时的例子:
至于多实例模式,就更容易理解了。给每个子应用都分配一套 iframe
+ webComponent
的组合,就可以实现相互之间的隔离了!
- 刷新页面会导致子应用路由状态丢失?
通过重写 iframe
实例的history.pushState
和 history.replaceState
,将子应用的 path
记录到主应用地址栏的 query
参数上,当刷新浏览器初始化 iframe
时,从地址栏读到子应用的 path
并使用 iframe
的 history.replaceState
进行同步。
简单理解就是:将子应用路径记录在地址栏参数中。
3.2.2. 优势:
3.2.2.1. 相比 qiankun 接入成本更低。
- 父应用:
- 与 iframe 的接入方式很类似,只需引入一个 React 组件渲染子应用即可。
- 与 iframe 的接入方式很类似,只需引入一个 React 组件渲染子应用即可。
- 子应用理论上不需要做任何改造
3.2.2.2. vite 兼容性好
直接将完整的 ESM 标签块 插入 iframe 中,避免了 qiankun 使用 eval 执行 ESM 代码导致的报错问题。
3.2.2.3. iframe 沙箱隔离性好
3.2.3. 劣势:
3.2.3.1. 坑比较多
- 明坑: 用于 JS 沙箱的 iframe 的 src 必须指向一个同域地址导致的问题。
具体问题描述见下图:
具体问题描述见下图:
此 [issue]() 至今无法在框架层面得到解决,属于 iframe 的原生限制。
手动的解决方案:
- 主应用提供一个路径比如说 https://host/empty ,这个路径不需要返回任何内容,子应用设置 attr 为 {src:'https://host/empty'},这样 iframe 的 src 就是 https://host/empty。
- 在主应用 template 的 head 插入
这样的代码可以避免主应用代码污染。
- 暗坑: 复杂的 iframe 到 webComponent 的代理机制,导致市面上大部分富文本编辑器都无法在无界中完好运行。所以有富文本的项目,尽量别用无界,除非你对富文本库的源码了如指掌。issues 在这里。
3.2.3.2. 长期维护性一般。
3.2.3.3. 内存开销较大
用于 js 沙箱的 iframe 是隐藏在主应用的 body 下面的,相当于是常驻内存,这可能会带来额外的内存开销。
3.3. 中间派 micro-app
3.3.1. 原理:
京东的大前端团队出品。
样式隔离方案与 qiankun 的实验方案类似,也是在运行时给子应用中所有的样式规则增加一个特殊标识来限定 css 作用范围。
子应用路由同步方案与 wujie 类似,也是通过劫持路由跳转方法,同步记录到 url 的 query 中,刷新时读取并恢复。
组件化的使用方式与 wujie 方案类似,这也是 micro-app 主打的宣传点。
最有意思的是它的沙箱方案,居然内置了两种沙箱:
- 类 qiankun 的 with 代理沙箱,据说相比 qiankun 性能高点,但目前微前端框架界并没有一个权威的基准性能测试依据,所以并无有效依据支撑。
- 类 wujie 的 iframe 沙箱,用于兼容 vite 场景。
开发者可以根据自身的实际情况自由选择。
整体感觉 micro-app 是一种偏“现实主义”的框架,它的特点就是取各家所长,最终成为了功能最丰富的微前端框架。
3.3.2. 优势:
3.3.2.1. 支持的功能最丰富。
3.3.2.2. 接入成本低。
3.3.2.3. 文档完善。
micro-zoe.github.io/micro-app/d…
3.3.3. 劣势:
3.3.3.1. 功能丰富导致配置项与 api 太多。
3.3.3.2. 静态资源补全问题。
静态资源补全是基于父应用的,而非子应用这需要开发者自己手动解决。
4. 选型建议
统计时间2023.12.3 | npm周下载量 | star数 | issue数 | 最近更新时间 | 接入成本 | 沙箱支持vite |
---|---|---|---|---|---|---|
qiankun | 22k | 15k | 362/1551 | 12天前 | 高 | ❌ |
wujie | 1.3k | 3.4k | 280/271 | 24天前 | 低 | ✅ |
micro-app | 1.1k | 4.9k | 57/748 | 1个月前 | 中 | ✅ |
- 刚性建议。
- vite 项目且对 js 沙箱有刚需,选 wujie 或者 micro-app。
- 项目存在复杂的交互场景,比如有用到富文本编辑器库,选 wujie 前请做好充分的测试。
- 如果你的团队对主、子应用的开发完全受控,即使有隔离性问题也可以通过治理来解决,那么可以试试更轻量的 single-SPA 方案。
- 如果特别重视稳定性,那无疑是 iframe 最佳... 因为 iframe 存在的问题都是摆在明面的,市面上现有的微前端框架多多少少都有一些隐性问题。
- 综合推荐。
主要从接入成本、功能稳定性、长期维护性三方面来衡量:
- 接入成本: wujie > microApp > qiankun (由低到高)
- 功能稳定性:qiankun > microApp > wujie
- 长期维护性:qiankun > microApp > wujie
看你的团队最看重哪一点,针对性去选择就好了,没有十全十美微前端框架,只有适合自己的。
最后
以上内容,确实会有我强烈的个人理解与观点,这也是我写文章一贯的风格。我并不喜欢那种客观且枯燥无味的文章,读完之后感觉像流水账,给不了读者任何的指导。我认为文章就是要有观点输出,技术文章也不例外,如果非常看重准确无误的表达,可以直接去看说明文档or源码,那应该是最权威的知识。如有错误或者误解,可以评论区或者私信指出,我积极改正。
来源:juejin.cn/post/7309477710523269174
从组件库中学习颜色主题配置
前言
对于一般前端来说,在颜色选择配置上可能没有设计师那么专业,特别在某些项目中的一些场景颜色配置上可能都是用相近的颜色或者透明度来匹配,没有一个专门的颜色对比输出。
所以本文想给大家讲一下主题色的配置应用,其中antd组件库
给我们提供了十二种自然主题色板,在美感和视觉上感觉非常的舒适自然,可以参考来使用。
我们以antd组件库
的色彩体系中的火山主题为例来讲解下面的内容。(本文会涉及到 sass
的语法)
其中提供的主题色每一种都有从浅至深有 10
个颜色,一般以第 6
种为主题的主色,其中的一些场景也给出了我们对应的颜色级别。

以图为例,告诉我们常用的场景对应的颜色深浅级别
- selected 选中:颜色值为
1
- hover 悬浮:颜色值为
5
- click 点击:颜色值为
7
主题色场景
以上述场景为例我们来实践一下,先列出10种颜色,然后写入在 css
变量中
$color-valcano: (
'valcano-1': #fff2e8,
'valcano-2': #ffd8bf,
'valcano-3': #ffbb96,
'valcano-4': #ff9c6e,
'valcano-5': #ff7a45,
'valcano-6': #fa541c,
'valcano-7': #d4380d,
'valcano-8': #ad2102,
'valcano-9': #871400,
'valcano-10': #610b00
);
:root{
@each $attribute, $value in $color-valcano {
#{'--color-#{$attribute}'}: $value
}
}
对于以上的场景,我们只需要应用对应的 css
变量即可
最终的变量如下
:root {
--color-valcano-1: #fff2e8;
--color-valcano-2: #ffd8bf;
--color-valcano-3: #ffbb96;
--color-valcano-4: #ff9c6e;
--color-valcano-5: #ff7a45;
--color-valcano-6: #fa541c;
--color-valcano-7: #d4380d;
--color-valcano-8: #ad2102;
--color-valcano-9: #871400;
--color-valcano-10: #610b00;
}
示例如下
通过变量后缀带数字这种非常难记住对应的场景值,不利于开发,我们可以再优化一下,把对应的场景细化出来,存储对应的颜色级别。
$scence-color-level: (
'primary': 6,
'selected': 1,
'hover': 5,
'border': 5,
'click': 7
);
:root{
@each $attribute, $value in $scence-color-level {
#{'--color-#{$attribute}'}: map-get($color-valcano, #{'valcano-#{$value}'})
}
}
我们来看看转换之后的变量
:root {
--color-primary: #fa541c;
--color-selected: #fff2e8;
--color-hover: #ff7a45;
--color-border: #ff7a45;
--color-click: #d4380d;
}
这样遇到对应的变量我们就可以不用关心颜色的深浅级别,只需要找对应场景,例如 selected
场景只需要使用变量 var(--color-selected)
就可以了。
当我们想切换其他主题的时候,难道要全部重写一遍,手动变更吗?
我们再来优化一下,将主题的变量变成动态的
$theme: 'valcano';
$theme-color: (
'valcano': $color-valcano,
'lime': $color-lime,
'cyan': $color-cyan,
'purple': $color-purple
);
:root{
@each $attribute, $value in $scence-color-level {
#{'--color-#{$attribute}'}: map-get(map-get($theme-color,$theme), #{'#{$theme}-#{$value}'})
}
}
以代码为例 引入了四种主题valcano
lime
cyan
purple
,若要切换主题,只需要更改变量$theme
即可
可以在代码片段中的 style
中手动更改$theme
变量值,然后运行查看效果
element组件库主题切换
了解完原理并实践之后,我们来看看 element组件库 的切换主题的原理是怎样的?
这是element的主题变量
$colors: () !default;
$colors: map.deep-merge(
(
'white': #ffffff,
'black': #000000,
'primary': (
'base': #409eff,
),
'success': (
'base': #67c23a,
),
'warning': (
'base': #e6a23c,
),
'danger': (
'base': #f56c6c,
),
'error': (
'base': #f56c6c,
),
'info': (
'base': #909399,
),
),
$colors
);
官网提供的覆盖方法
// styles/element/index.scss /* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: ( 'primary': ( 'base': green, ), ),
);
// 如果只是按需导入,则可以忽略以下内容。
// 如果你想导入所有样式:
// @use "element-plus/theme-chalk/src/index.scss" as *;
官网定义的主题变量是通过 map.deep-merge
来实现主题映射合并。
map.deep-merge
的作用是:用于深度合并两个或多个映射(maps)。它可以在不丢失嵌套映射的情况下合并映射,这对于处理复杂的配置数据结构非常有用。
所以其实就是通过新的配置去合并覆盖它,有点类似 Object.assign()
这种变量对象覆盖的感觉。
其中element组件库也是通过sass函数自动生成需要用到的 css 变量来重构整一个样式系统。
为什么最后都转变成css变量?
- 兼容性:因为CSS 变量是一个非常有用的功能,几乎所有浏览器都支持。
- 动态性:每个组件都是有对应的css变量,想要改变颜色,只需要动态地改变组件内的个别变量即可。
- 多样性:也可以通过js来控制css变量
来源:juejin.cn/post/7398340132161994793
内网开发提效指南
❝
工欲善其事必先利其器,使用过内网开发的小伙伴都知道,CV大法在内网基本就废了,查资料也是非常的不便。对于一名程序员来说,如果把搜索引擎和CV键给他ban了,遇到问题后那基本是寸步难行。今天给大家介绍几种帮助内网开发提效的方法,希望能够帮助到大家。
一、文档站点内网部署
可以把项目中所用技术和框架的文档部署到公司内网中。
以elementPlus为例:
1、访问gh-pages分支https://github.com/element-plus/element-plus/tree/gh-pages
,下载文档站源码。
2、将文档站部署到内网服务器(以nginx为例)。
server {
listen 9800;
server_name localhost;
location / {
root html/element-plus-gh-pages;
index index.html index.htm;
try_files $uri $uri/ /element-plus-gh-pages/index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
部署后的访问速度也是非常快的
使用这种方式,随着部署的站点增多,后续框架、文档更新的时候,维护起来相对是比较麻烦的。且只能查看文档,遇到问题需要求助度娘还是不太方便。
下面介绍两种物理外挂,可以直接访问外网。
二、USB跨屏穿越器数据线
个人感觉此方案的体验是最好的,缺点是需要两台电脑,并且需要花钱买一根线,价格在80-200之间。
购买
某宝、某鱼都有销售,我是在某宝85块买的。
使用
连接两台电脑的USB端口即可,会自动安装驱动,那根线实际上就相当于是一个文件中转器,可以实现剪切板、文件的互传。使用体验就跟一台电脑连接了两台显示器一样。如下图所示:
三、手机投屏
本文重点介绍此方案,因为可以白嫖且不需要第二台电脑。一部安卓手机+数据线即可,缺点是文件传输不太方便。它就是一个开源投屏项目scrcpy
。可以看到,此项目在github上拥有高达102k的star数量。
✨亮点
- 亮度 (原生,仅显示设备屏幕)
- 表演 (30~60fps)
- 质量 (1920×1080或以上)
- 低延迟 (70~100ms)
- 启动时间短 (显示第一张图像约1秒)
- 非侵入性 (设备上没有安装任何东西)
- 不需要 ROOT
- 有线无线都可连接
- 可以随便调整界面和码率
- 画面随意裁剪,自带录屏(手游直播利器)
- 支持多设备同时投屏
- 利用电脑的键盘和鼠标可以控制手机
- 把 APK 文件拖拽到电脑窗口即可安装应用到手机,把普通文件拖拽到窗口即可复制到手机
- 手机电脑共享剪贴板
- 自动检测USB连接的设备
- 可直接添加设备的局域网IP,达到无线控制的效果
- 将自动保存连接过的IP地址,下次输入时,自动提醒
- 支持设备别名
- 支持中英两种语言
- Tray menu
- 等等等...
安装
根据不同系统直接去release页面下载对应版本即可:github.com/Genymobile/…
使用
下载解压完,进入软件目录,点击下图按钮打开命令行界面,输入启动命令即可。
命令行输入scrcpy,按回车, 猿神,起洞!
启动之后,即可使用鼠标操作手机,非常的丝滑
1、手机复制文本到电脑
2、电脑复制文本到手机
可以看到,使用投屏的方式,也可以实现CV大法。并且可以使用手机端的外网搜索资料、解决问题等。以下是该项目的快捷键,熟练使用,即可达到人机合一的地步。
快捷键
操作 | 快捷键 | 快捷键 (macOS) | ||
---|---|---|---|---|
切换全屏模式 | Ctrl +f | Cmd +f | ||
将窗口调整为 1:1 | Ctrl +g | Cmd +g | ||
调整窗口大小以删除黑色边框 | Ctrl +x | 双击黑色背景 | Cmd +x | 双击黑色背景 |
设备HOME 键 | Ctrl +h | 鼠标中键 | Ctrl +h | 鼠标中键 |
设备BACK 键 | Ctrl +b | 鼠标右键 | Cmd +b | 鼠标右键 |
设备任务管理 键 | Ctrl +s | Cmd +s | ||
设备菜单 键 | Ctrl +m | Ctrl +m | ||
设备音量+ 键 | Ctrl +↑ | Cmd +↑ | ||
设备音量- 键 | Ctrl +↓ | Cmd +↓ | ||
设备电源 键 | Ctrl +p | Cmd +p | ||
点亮手机屏幕 | 鼠标右键 | 鼠标右键 | ||
关闭设备屏幕(保持镜像) | Ctrl +o | Cmd +o | ||
展开通知面板 | Ctrl +n | Cmd +n | ||
折叠通知面板 | Ctrl +Shift +n | Cmd +Shift +n | ||
将设备剪贴板中的内容复制到计算机 | Ctrl +c | Cmd +c | ||
将计算机剪贴板中的内容粘贴到设备 | Ctrl +v | Cmd +v | ||
将计算机剪贴板中的内容复制到设备 | Ctrl +Shift +v | Cmd +Shift +v | ||
安装APK | 将APK 文件拖入投屏 | 将APK 文件拖入投屏 | ||
传输文件到设备 | 将文件拖入投屏 | 将文件拖入投屏 | ||
启用/禁用FPS计数器(stdout) | Ctrl +i | Cmd +i |
使用小技巧
经过笔者几天的使用,总结出几个小技巧。
1、电脑键盘控制手机进行中文输入,必须使用正确的输入法组合。
手机端:讯飞输入法(搜狗输入法不支持)
电脑端:ENG(使用英文键盘)
2、手机熄屏状态下投屏。 在scrcpy命令后加上熄屏参数即可:scrcpy --turn-screen-off
这样就可以在手机熄屏的状态下,仍可以被电脑操作,达到节省电量和减轻发热的目的。
诸如此类的命令参数还有很多,执行scrcpy --help
就可查看详细的帮助文档。
衍生项目
因为开源的特性,scrcpy也衍生了一些相关项目,列举其中一些:
- QtScrcpy 使用qt重新实现的桌面端,并加强了对游戏的支持。
- scrcpy-gui 为scrcpy的命令行提供了gui界面。
- guiscrcpy 另一个scrcpy的gui界面。
- scrcpy-docker docker版本的scrcpy。
- scrcpy-go go语言版本的scrcpy,增强对游戏的支持。
总结
第二第三种方法虽然建立了内网开发电脑和外网设备的联系,但是是不会被公司的安全系统检测到一机双网的,因为其本质就类似于设计模式中的发布订阅模式,用数据线充当了中间人,两台设备之间方便传输数据了而已,不会涉及到联网。
内网开发的痛点,无非就是复制粘贴、文件传输不便,只要打通这个链路,就能解决此问题。以上三个方法,笔者在实际工作中都用到了,确实极大的提高了工作效率。如果你也在饱受内网开发的折磨,不妨试试这几个方法。
来源:juejin.cn/post/7362464700879716403
UI: 为啥你这个页面边框1px看起来这么粗?
背景
最近在开发H5,ui稿给的border:1px solid,因为ui稿上的宽度是750px,我们运行的页面宽度是375px,所以我们需要把所以尺寸/2。所以我们可能会想写border:0.5px solid。但是实际上,我们看页面渲染,仍然是渲染1px而不是0.5
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
}
.flex {
display: flex;
}
.item {
margin-right: 10px;
padding: 10px;
font-size: 13px;
line-height: 1;
background-color: rgba(242, 243, 245,1);
}
.active {
color: rgba(250, 100, 0, 1);
font-size: 14px;
border: 0.5px solid ;
}
</style>
</head>
<body>
<div class="flex">
<!-- <div class="item active">
active
</div> -->
<div class="item">
item1
</div>
<div class="item">
item2
</div>
<div class="item">
item3
</div>
</div>
</body>
</html>
在没active的情况下
他们的内容都是占13px
在有active的情况下
active占了14px这个是没问题的,因为它font-size是14px嘛,但是我们是设置了border的宽度是0.5px,但展现的却是1px。
再来看看item
它内容占了16px,它受到相邻元素影响是14px+2px的上下边框
为啥border是1px呢
在 CSS 中,边框可以设置为 0.5px,但在某些情况下,尤其是低分辨率的屏幕上,浏览器可能会将其渲染为 1px 或根本不显示。这是因为某些浏览器和显示设备不支持小于 1px 的边框宽度或不能准确渲染出这样的细小边框。
浏览器渲染机制
- 不同浏览器对于小数像素的处理方式不同。一些浏览器可能会将
0.5px
边框四舍五入为1px
,以确保在所有设备上的一致性。
设备像素比
- 在高 DPI(如 Retina 显示器)设备上,
0.5px
边框可能看起来更清晰,因为这些设备可以渲染更细的边框。 - 在低 DPI 设备上,
0.5px
边框可能会被放大或者根本不会被显示。
解决办法
方法一:使用伪类和定位
.active {
color: rgba(250, 100, 0, 1);
font-size: 14px;
position: relative;
}
.active::after {
content: "";
pointer-events: none;
display: block;
position: absolute;
left: 0;
top: 0;
transform-origin: 0 0;
border: 1px #ff892e solid;
box-sizing: border-box;
width: 100%;
height: 100%;
}
另外的item的内容高度也是14px了符合要求
方法二:使用阴影,使用F12看的时候感觉还是有些问题
.active2 {
margin-left: 10px;
color: rgba(250, 100, 0, 1);
font-size: 14px;
position: relative;
box-shadow: 0 0 0 0.5px #ff892e;
}
方法三:使用svg,但这种自己设置了宽度。
<div class="active">
<svg width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
<rect x="0" y="0" width="100" height="100" fill="none" stroke="#ff892e" stroke-width="0.5"></rect>
</svg>
active
</div>
方案四:使用svg加定位,也比较麻烦,而且有其他的问题
<div class="active">
<svg viewBox="0 0 100 100" preserveAspectRatio="none">
<rect x="0" y="0" width="100" height="100" fill="none" stroke="#ff892e" stroke-width="0.5"></rect>
</svg>
<div class="content">active</div>
</div>
.active {
color: rgba(250, 100, 0, 1);
font-size: 14px;
position: relative;
display: inline-block;
}
.active svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
box-sizing: border-box;
}
.active .content {
position: relative;
z-index: 1;
}
方法五:使用一个父元素 比较麻烦
<div class="border-container">
<div class="active">active</div>
</div>
.border-container {
display: inline-block;
padding: 0.5px;
background-color: #ff892e;
}
.active {
color: rgba(250, 100, 0, 1);
font-size: 14px;
background-color: white;
}
最后
在公司里,我们使用的都是方案一,这样active和item它们的内容高度都是14px了。然后我们再给他们的父盒子加上 align-items: center。这样active的高度是14px,其他都是13px了。但是active的高度会比其他item的盒子高1px,具体看个人需求是否添加吧。
来源:juejin.cn/post/7393656776539963407
基于英雄联盟人物的加载动画,奇怪的需求又增加了!
1、背景
前两天老板找到我说有一个需求,要求使用英雄联盟的人物动画制作一个加载中的组件,类似于下面这样:
我定眼一看:这个可以实现,但是需要UI妹子给切图。
老板:UI? 咱们啥时候招的UI !
我:老板,那不中呀,不切图弄不成呀。
老板:下个月绩效给你A。
我:那中,管管管。
2、调研
发动我聪明的秃头,实现这个需求有以下几种方案:
- 切动画帧,没有UI不中❎。
- 去lol客户端看看能不能搞到什么美术素材,3D模型啥的,可能行❓
- 问下 gpt4o,有没有哪个老表收集的有lol英雄的美术素材,如果有那就更得劲了✅。
经过我一番搜索,发现了这个网站:model-viewer,收集了很多英雄联盟的人物模型,模型里面还有各种动画,还给下载。老表,这个需求稳了50%了!
接下来有几种选择:
- 将模型动画转成动画帧,搞成雪碧图,较为麻烦,且动画不支持切换。
- 直接加载模型,将模型放在进度条上,较为简单,支持切换不同动画,而且可以自由过渡。就是模型文件有点大,初始化加载可能耗时较长。但是后续缓存一下就好了。
聪明的我肯定先选第二个方案呀,你糊弄我啊,我糊弄你。
3、实现
web中加载模型可以使用谷歌基于threejs
封装的 model-viewer
, 使用现代的 web component 技术。简单易用。
先初始化一个vue工程
npm create vue@latest
然后将里面的初始化的组件和app.vue里面的内容都删除。
安装model-viewer
依赖:
npm i three // 前置依赖
npm i @google/model-viewer
修改vite.config.js
,将model-viewer
视为自定义元素,不进行编译
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: {
// 添加以下内容
compilerOptions: {
isCustomElement: (tag) => ['model-viewer'].includes(tag)
}
}
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
assetsInclude: ['./src/assets/heros/*.glb']
})
新建 src/components/LolProgress.vue
<template>
<div class="progress-container">
<model-viewer
:src="hero.src"
disable-zoom
shadow-intensity="1"
:camera-orbit="hero.cameraOrbit"
class="model-viewer"
:style="heroPosition"
:animation-name="animationName"
:camera-target="hero.cameraTarget"
autoplay
ref="modelViewer"
></model-viewer>
<div
class="progress-bar"
:style="{ height: strokeWidth + 'px', borderRadius: strokeWidth / 2 + 'px' }"
>
<div class="progress-percent" :style="currentPercentStyle"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch, type PropType } from 'vue'
/** 类型 */
interface Hero {
src: string
cameraOrbit: string
progressAnimation: string
finishAnimation: string
finishAnimationIn: string
cameraTarget: string
finishDelay: number
}
type HeroName = 'yasuo' | 'yi'
type Heros = {
[key in HeroName]: Hero
}
const props = defineProps({
hero: {
type: String as PropType<HeroName>,
default: 'yasuo'
},
percentage: {
type: Number,
default: 100
},
strokeWidth: {
type: Number,
default: 10
},
heroSize: {
type: Number,
default: 150
}
})
const modelViewer = ref(null)
const heros: Heros = {
yasuo: {
src: '/src/components/yasuo.glb',
cameraOrbit: '-90deg 90deg',
progressAnimation: 'Run2',
finishAnimationIn: 'yasuo_skin02_dance_in',
finishAnimation: 'yasuo_skin02_dance_loop',
cameraTarget: 'auto auto 0m',
finishDelay: 2000
},
yi: {
src: '/src/components/yi.glb',
cameraOrbit: '-90deg 90deg',
progressAnimation: 'Run',
finishAnimationIn: 'Dance',
finishAnimation: 'Dance',
cameraTarget: 'auto auto 0m',
finishDelay: 500
}
}
const heroPosition = computed(() => {
const percentage = props.percentage > 100 ? 100 : props.percentage
return {
left: `calc(${percentage + '%'} - ${props.heroSize / 2}px)`,
bottom: -props.heroSize / 10 + 'px',
height: props.heroSize + 'px',
width: props.heroSize + 'px'
}
})
const currentPercentStyle = computed(() => {
const percentage = props.percentage > 100 ? 100 : props.percentage
return { borderRadius: `calc(${props.strokeWidth / 2}px - 1px)`, width: percentage + '%' }
})
const hero = computed(() => {
return heros[props.hero]
})
const animationName = ref('')
watch(
() => props.percentage,
(percentage) => {
if (percentage < 100) {
animationName.value = hero.value.progressAnimation
} else if (percentage === 100) {
animationName.value = hero.value.finishAnimationIn
setTimeout(() => {
animationName.value = hero.value.finishAnimation
}, hero.value.finishDelay)
}
}
)
onMounted(() => {
setTimeout(() => {
console.log(modelViewer.value.availableAnimations)
}, 2000)
})
</script>
<style scoped>
.progress-container {
position: relative;
width: 100%;
}
.model-viewer {
position: relative;
background: transparent;
}
.progress-bar {
border: 1px solid #fff;
background-color: #666;
width: 100%;
}
.progress-percent {
background-color: aqua;
height: 100%;
transition: width 100ms ease;
}
</style>
组件非常简单,核心逻辑如下:
- 根据传入的英雄名称加载模型
- 指定每个英雄的加载中的动画,
- 加载100%,切换完成动作进入动画和完成动画即可。
- 额外的细节处理。
最后修改
app.vue
:
<script setup lang="ts">
import { ref } from 'vue'
import LolProgress from './components/LolProgress.vue'
const percentage = ref(0)
setInterval(() => {
percentage.value = percentage.value + 1
}, 100)
</script>
<template>
<main>
<LolProgress
:style="{ width: '200px' }"
:percentage="percentage"
:heroSize="200"
hero="yasuo"
/>
</main>
</template>
<style scoped></style>
这不就完成了吗,先拿给老板看看。
老板:换个女枪的看看。
我:好嘞。
老板:弄类不赖啊小伙,换个俄洛伊的看看。
4、总结
通过本次需求,了解到了 model-viewer
组件。
老板招个UI妹子吧。
在线体验:github-pages
来源:juejin.cn/post/7377217883305279526
做了这么久前端还不会手写瀑布流?(H5 & 小程序)
前言
做了7年前端我一直不知道瀑布流是什么(怪设计师不争气啊,哈哈哈),我一直以为就是个普通列表,几行css解决的那种。
当然瀑布流确实有css解决方案,但是这个方案对于分页列表来说完全不能用,第二页内容一出来位置都变了。
我看了一下掘金的一些文章,好长啊,觉得还是自己想一下怎么写吧。就自己实现了一遍。希望思路给大家一点帮助。
分析瀑布流

以小红书的瀑布流为例,相同宽度不同高度的卡片堆叠在一起形成瀑布流。
这里有两个难点:
- 卡片高度如何确定?
- 堆叠布局如何实现?
卡片的高度 = padding + imageHeight + textHeight....
不固定的内容包括:图片高度、标题行数
也就是说当我们解决了图片和标题的高度问题,那么瀑布流的第一个问题就解决了。(感觉已经写好代码了一样)
堆叠问题——因为css没有这样的布局方式,所以肯定得用js实现。最简单的解决方案就是对每一个盒子进行绝对定位。
这个问题就转换成计算现有盒子的定位问题。
从问题到代码
第一个问题——图片高度
无论是企业业务场景还是个人开发,通过后端返回图片的width、height都是合理且轻松的。
前端去获取图片信息,无疑让最重要的用户体验变得糟糕。前端获取图片信息并不困难,但是完全没有必要。
所以我直接考虑后端返回图片信息的情况。
const realImageHeight = imageWidth / imageHeight * cardContentWidth;
图片高度轻松解决,无平台差异
第二个问题——文字高度
从小红书可以看出,标题有些两行有些一行,也有些三行。
如果你固定一行,这个问题完全可以跳过。
- 方案一:我们可以用字数和宽度来计算可能得行数
优势:速度快,多平台复用
劣势:不准确(标题包括英文中文) - 方案二:我们可以先渲染出来再获取行数
优势:准确
劣势:相对而言慢,不同平台方法不同
准确是最重要的!选择方案二
其实方案二也有两种方案,一种是用canvas模拟,这样可以最大限度摆脱平台(h5、小程序)的限制,
然而我试验后,canvas还没找到准确的计算的方法(待后续更新)
第二种就是用div渲染一遍,获取行数或者高度。
创建一个带有指定样式的 div 元素
function createDiv(style: string): HTMLDivElement {
const div = document.createElement('div');
div.style.cssText = style;
document.body.appendChild(div);
return div;
}
计算文本数组在指定字体大小和容器宽度下的行数
/**
* 计算文本数组在指定字体大小和容器宽度下的行数
* @param texts - 要渲染的文本数组
* @param fontSize - 字体大小(以像素为单位)
* @param lineHeight - 字体高度(以像素为单位)
* @param containerWidth - 容器宽度(以像素为单位)
* @param maxLine - 最大行数(以像素为单位)
* @returns 每个文本实际渲染时的行数数组
*/
export function calculateTextLines(
texts: string[],
fontSize: number,
lineHeight: number,
containerWidth: number,
maxLine?: number
): number[] {
// 创建一个带有指定样式的 div 元素
const div = createDiv(`font-size: ${fontSize}px; line-height: ${lineHeight}px; width: ${containerWidth}px; white-space: pre-wrap;`);
const results: number[] = [];
texts.forEach((text) => {
div.textContent = text;
// 获取 div 的高度,并根据字体大小计算行数
const divHeight = div.offsetHeight;
const lines = Math.ceil(divHeight / lineHeight);
maxLine && lines > maxLine ? results.push(maxLine) : results.push(lines);
});
// 清理 div
removeElement(div);
return results;
}
这个问题小程序如何解决放在文末
第三个问题——每个卡片的定位问题
解决了上面的问题,就解决了盒子高度的问题,这个问题完全就是相同宽度不同高度盒子的堆放问题了
问题的完整描述是这样的:
写一个ts函数实现将一堆小盒子,按一定规则顺序推入大盒子里
函数输入:小盒子高度列表
小盒子:不同小盒子高度不一致,宽度为stackWidth,彼此间隔gap
大盒子:高度无限制,宽度为width
堆放规则:优先放置高度低的位置,高度相同时优先放在左侧
返回结果:不同盒子的高度和位置信息
如果你有了这么清晰的描述,接下去的工作你只需要交给gpt来写你的函数
// 返回的盒子信息
export interface Box {
x: number;
y: number;
height: number;
}
// 盒子堆叠的方法类
export class BoxPacker {
// 返回的小盒子信息列表
private boxes: Box[] = [];
// 大盒子宽度
private width: number;
// 小盒子宽度
private stackWidth: number;
// 小盒子间隔
private gap: number;
constructor(width: number, stackWidth: number, gap: number) {
this.width = width;
this.stackWidth = stackWidth;
this.gap = gap;
this.boxes = [];
}
// 添加单个盒子
public addBox(height: number): Box[] {
return this.addBoxes([height]);
}
// 添加多个盒子(一般用这个方法)
public addBoxes(heights: number[], isReset?: boolean): Box[] {
isReset && (this.boxes = [])
console.log('this.boxes—————— ', JSON.stringify(this.boxes) )
for (const height of heights) {
const position = this.findBestPosition();
const newBox: Box = { x: position.x, y: position.y, height };
this.boxes.push(newBox);
}
return this.boxes;
}
// 查找定位函数
private findBestPosition(): { x: number; y: number } {
let bestX = 0;
let bestY = Number.MAX_VALUE;
for (let x = 0; x <= this.width - this.stackWidth; x += this.stackWidth + this.gap) {
const currentY = this.getMaxHeightInColumn(x, this.stackWidth);
if (currentY < bestY || (currentY === bestY && x < bestX)) {
bestX = x;
bestY = currentY;
}
}
return { x: bestX, y: bestY };
}
private getMaxHeightInColumn(startX: number, width: number): number {
return this.boxes
.filter(box => box.x >= startX && box.x < startX + width)
.reduce((maxHeight, box) => Math.max(maxHeight, box.y + box.height + this.gap), 0);
}
}
这样我们就实现了根据高度获取定位的功能了
来实现一波
核心的代码就是获取每个盒子的定位、宽高信息
// 实例
const boxPacker = useMemo(() => {
return new BoxPacker(width, stackWidth, gap)
}, []);
const getCurrentPosition = (currentData: DataItem[], reset?: boolean) => {
// 获取标题文本行数列表
const textLines = calculateTextLines(currentData.map(item => item.title),card.fontSize,card.lineHeight, cardContentWidth)
// 获取图片高度列表
const imageHeight = currentData.map(item => (item.imageHeight / item.imageWidth * cardContentWidth))
// 获取小盒子高度列表
const cardHeights = imageHeight.map((h, index) => (
h + textLines[index] * card.lineHeight + card.padding * 2 + (card?.otherHeight || 0)
)
);
// 获取盒子定位信息
const boxes = boxPacker.addBoxes(
cardHeights,
reset
)
// 返回盒子列表信息
return boxes.map((box, index) => ({
...box,
title: currentData[index]?.title,
url: currentData[index]?.url,
imageHeight: imageHeight[index],
}))
}
set获取到的盒子信息
const [boxPositions, setBoxPositions] = useState<(Box & Pick<DataItem, 'url' | 'title' | 'imageHeight'>)[]>([]);
useEffect(() => {
// 首次和刷新
if (page === 1) {
setBoxPositions(getCurrentPosition(data, true))
} else {
// 加载更多
setBoxPositions(getCurrentPosition(data.slice((page - 1) * pageSize, page * pageSize)))
}
}, [])
效果如下

小程序获取文本高度
从上面的分析可以看出来只有文本高度实现是不同的,如果canvas方案实验成功,说不定还能做到大一统。
目前没成功大家就先看看我的目前方案:先实际渲染文字然后读取信息,然后获取实际高度
import React, {useEffect, useMemo, useState} from 'react'
import { View } from '@tarojs/components'
import Taro from "@tarojs/taro";
import './index.less'
import {BoxPacker} from "./flow";
const data = [
'vwyi这是一个标题,这是一个标题,这是一个标题,这是一个标题',
'这是一个标题',
'这是一个标题,这是一个标题,这是一个标题,这是一个标题',
'这是一个标题',
'这是一个标题,这是一个标题,这是一个标题,一个标题',
'这是一个标题,这是一个标题,这是一个标题,这题',
'这是一个标题,这是一个标题,这是一',
'这是一个标题,这是一个标题,这是一',
];
function Index() {
const boxPacker = useMemo(() => new BoxPacker(320, 100, 5), []);
const [boxPositions, setBoxPositions] = useState<any[]>([])
function getTextHeights() {
return new Promise((resolve, reject) => {
Taro.createSelectorQuery()
.selectAll('#textContainer .text-item')
.boundingClientRect()
.exec(res => {
if (res && res[0]) {
const heights = res[0].map(item => item.height);
resolve(heights);
} else {
reject('No buttons found');
}
});
});
}
useEffect(() => {
getTextHeights().then(h => {
setBoxPositions(boxPacker.addBoxes(h))
})
}, [])
return (
<View className="flow-container">
<View id="textContainer">
{
data.map((item, index) => (<View key={index} className="text-item">{item}</View>))
}
</View>
<View className="text-box-container">
{boxPositions.map((position, index) => (
<View
key={index}
className="text-box"
style={{
left: `${position.x}px`,
top: `${position.y}px`,
height: `${position.height}px`,
width: '100px', // 假设盒子的宽度固定为100px
}}
>
{`${data[index]}`}
</View>
))}
</View>
</View>
)
}
export default Index
来源:juejin.cn/post/7397278180644372521
面试官:假如有几十个请求,如何去控制并发?
面试官:看你简历上做过图片或文件批量下载,那么假如我一次性下载几十个,如何去控制并发请求的?
让我想想,额~, 选中ID,循环请求?,八嘎!肯定不是那么沙雕的做法,这样做服务器直接崩溃啦!突然灵光一现,请求池!!!
我:利用Promise模拟任务队列,从而实现请求池效果。
面试官:大佬!
废话不多说,正文开始:
众所周知,浏览器发起的请求最大并发数量一般都是6~8
个,这是因为浏览器会限制同一域名下的并发请求数量,以避免对服务器造成过大的压力。
首先让我们来模拟大量请求的场景
const ids = new Array(100).fill('')
console.time()
for (let i = 0; i < ids.length; i++) {
console.log(i)
}
console.timeEnd()
一次性并发上百个请求,要是配置低一点,又或者带宽不够的服务器,直接宕机都有可能,所以我们前端这边是需要控制的并发数量去为服务器排忧解难。
什么是队列?
先进先出就是队列,push
一个的同时就会有一个被shift
。我们看下面的动图可能就会更加的理解:
我们接下来的操作就是要模拟上图的队列行为。
定义请求池主函数函数
export const handQueue = (
reqs // 请求数量
) => {}
接受一个参数reqs
,它是一个数组,包含需要发送的请求。函数的主要目的是对这些请求进行队列管理,确保并发请求的数量不会超过设定的上限。
定义dequeue函数
const dequeue = () => {
while (current < concurrency && queue.length) {
current++;
const requestPromiseFactory = queue.shift() // 出列
requestPromiseFactory()
.then(() => { // 成功的请求逻辑
})
.catch(error => { // 失败
console.log(error)
})
.finally(() => {
current--
dequeue()
});
}
}
这个函数用于从请求池中取出请求并发送。它在一个循环中运行,直到当前并发请求数current
达到最大并发数concurrency
或请求池queue
为空。对于每个出队的请求,它首先增加current
的值,然后调用请求函数requestPromiseFactory
来发送请求。当请求完成(无论成功还是失败)后,它会减少current
的值并再次调用dequeue
,以便处理下一个请求。
定义返回请求入队函数
return (requestPromiseFactory) => {
queue.push(requestPromiseFactory) // 入队
dequeue()
}
函数返回一个函数,这个函数接受一个参数requestPromiseFactory
,表示一个返回Promise的请求工厂函数。这个返回的函数将请求工厂函数加入请求池queue
,并调用dequeue
来尝试发送新的请求,当然也可以自定义axios,利用Promise.all
统一处理返回后的结果。
实验
const enqueue = requestQueue(6) // 设置最大并发数
for (let i = 0; i < reqs.length; i++) { // 请求
enqueue(() => axios.get('/api/test' + i))
}
我们可以看到如上图所示,请求数确实被控制了,只有有请求响应成功的同时才会有新的请求进来,极大的降低里服务器的压力,后端的同学都只能喊6。
整合代码
import axios from 'axios'
export const handQueue = (
reqs // 请求总数
) => {
reqs = reqs || []
const requestQueue = (concurrency) => {
concurrency = concurrency || 6 // 最大并发数
const queue = [] // 请求池
let current = 0
const dequeue = () => {
while (current < concurrency && queue.length) {
current++;
const requestPromiseFactory = queue.shift() // 出列
requestPromiseFactory()
.then(() => { // 成功的请求逻辑
})
.catch(error => { // 失败
console.log(error)
})
.finally(() => {
current--
dequeue()
});
}
}
return (requestPromiseFactory) => {
queue.push(requestPromiseFactory) // 入队
dequeue()
}
}
const enqueue = requestQueue(6)
for (let i = 0; i < reqs.length; i++) {
enqueue(() => axios.get('/api/test' + i))
}
}
实战文章
之前写过一篇关于web-worker
大文件切片的案例文章,就是利用了此特性感兴趣的小伙伴可以看看web-worker的基本用法并进行大文件切片上传(附带简易node后端)
来源:juejin.cn/post/7356534347509645375
文件上传你会吧?那帮我做个文件下载功能
大家好,又是我,大聪明,立志做个早起吃草的马儿。话说上回解决完部署的问题(部署完了,样式不生效差点让我这个前端仔背锅),我又感觉回到了眼神清澈的大聪明状态,直到今天产品跟我说:“听说你是文件上传高手?做过大文件上传?切片?断点续传?”,听完我一脸戒备和紧张,“难道我面试吹的牛皮被他发现了,现在要捅破了?”我正在犹豫要不要跟他摊牌说,我面试掺了水的时候,他又来了一句,“那帮我做个 文件下载 的功能吧”,我突然放松下来了,原来是要加需求呀,害我白担心一场。作为CVT工程师,这点事根本难不倒我。
好了下面开始我的CV大法。
首先找到后端协调,他让我返回一个file_id
,该file_id是我在文件上传到服务器存储的时候,后端返回给我的,通过此file_id,来找到对应的文件,很好很简单。
接着,看后端提供的文件下载接口,咱就是说,经历的少,不知道对不对,后端直接就是返回文件的字节流(bytes),除此之外没有任何信息,没有文件名,没有文件类型,去问了一下说就是这样的,咱也不敢多问
天无绝人之路,还好我在前端获取文件的时候能找到总的文件列表,通过遍历出来也能拿到文件的信息
下面是判断文件类型方法
getFileTypeMime (key) {
let mimeType = ''
switch (key) {
case 'png':
mimeType = 'image/png'
break
case 'jpeg':
mimeType = 'image/jpeg'
break
case 'pdf':
mimeType = 'application/pdf'
break
case 'xlsx':
mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
break
case 'docx':
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
break
default:
mimeType = 'text/plain'
break
}
return mimeType
},
下面是文件下载的方法 (错误的)
downLoad (value, key) {
const type = key.split('.')[1] // 文件类型
const name = key.split('.')[0] // 文件名
axiosBase({
url: `/ass/download?alert_id=${this.alert_id}&file_id=${value}`,
method: 'get',
}).then(res => {
const blob = new Blob([res], { type: this.getFileTypeMime(type) }) // 文件类型
const link = document.createElement('a')
const href = window.URL.createObjectURL(blob) // 创建下载的链接
link.href = href
link.download = name // 下载后文件名
document.body.appendChild(link)
link.click() // 点击下载
document.body.removeChild(link) // 下载完成移除元素
window.URL.revokeObjectURL(href) // 释放掉blob对象
}).catch((err) => { console.log(err) })
}
为什么是错误的呢?点了效果也确实是实现了 文件的下载,但是打开,然后格式错误了??
又是百思不得其解的问题,直接打开度娘,搜索又找到了这篇(神文)解决
downLoad (value, key) {
const type = key.split('.')[1] // 文件类型
const name = key.split('.')[0] // 文件名
axiosBase({
url: `/download?alert_id=${this.alert_id}&file_id=${value}`,
method: 'get',
responseType: 'blob'
}).then(res => {
const blob = new Blob([res], { type: this.getFileTypeMime(type) }) // 文件类型
const link = document.createElement('a')
const href = window.URL.createObjectURL(blob) // 创建下载的链接
link.href = href
link.download = name // 下载后文件名
document.body.appendChild(link)
link.click() // 点击下载
document.body.removeChild(link) // 下载完成移除元素
window.URL.revokeObjectURL(href) // 释放掉blob对象
}).catch((err) => { console.log(err) })
}
又是有惊无险的一天
来源:juejin.cn/post/7389913027654434857
部署完了,样式不生效差点让我这个前端仔背锅
大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。
叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动物的好心情都没了。
部署?因为该公司人数比较少,也没有自动化部署操作,只能让前端build一下dist包然后丢给后端,让后端部署,然后该项目也比较急,我刚来,也不好意思@后端,恰巧跟我对接的后端也是刚来不熟悉,所以我就跟负责人继续扯皮拖延时间了。(真是不好意思,假的)↓↓↓
终于经过不断努力(后端),部署好了,但是,我的样式乱了,为什么呢?看了一下网络和源代码,样式文件都请求到了,问了后端,一致说是我前端的问题,我懵了,然后快马加鞭查找问题
解决
ok,那我先看打包的dist文件有样式有问题吗,使用npm安装个 http-server 运行打包后的index.html,运行打开运行后的地址,欧克样式没乱,文件引用也没问题,那到底是什么鬼影响的呢?
在我百思不得其解的时候,我打开了神器--csdn,搜索打包dist部署后样式文件不生效,就遇到了这个神文t.csdnimg.cn/WbQyq
欧克,按照博主说的Nginx没有配置这两个东西而导致的,我有点不确定是不是,有点犹犹豫豫的找到后端说帮我在Nginx配置文件的http加下这两个配置,然后样式就好了,完美!后端由于疏忽,没加上就把问题推给我,结果被我扳回一城。
include mime.types;
default_type application/octet-stream;
- include mime.types;
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
mime.types
文件,Nginx可以识别不同类型的文件并正确地处理它们。 - 示例:假设
mime.types
文件中定义了.html
文件为text/html
类型,Nginx在处理请求时会根据这个定义设置正确的HTTP响应头。
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
- default_type application/octet-stream;
- 这个指令设置了默认的MIME类型。如果Nginx无法根据文件扩展名或其他方式确定响应的MIME类型时,就会使用这个默认类型。
application/octet-stream
是一个通用的MIME类型,表示未知的二进制数据。当服务器无法识别文件类型时,会默认使用这个类型。例如,如果请求的文件没有合适的MIME类型或没有被mime.types
文件中列出,Nginx就会返回application/octet-stream
类型。- 这种设置对于确保未知文件类型的安全传输很有用,因为浏览器通常会下载这些文件而不是尝试在浏览器中打开它们。
总之,添加 include mime.types;
和 default_type application/octet-stream;
配置后,Nginx能够正确地识别和处理CSS文件的MIME类型,从而确保浏览器能够正确加载和应用CSS样式。
所以,前端仔不能只当前端仔,还是要好好学点服务端的知识,不然锅给你,你还认为这是你该背的锅。
以上是开玩笑的描述,只是为了吸引增加阅读量
来源:juejin.cn/post/7388696625689051170
前端更新部署后通知用户刷新
前言
周五晚上组里说前端有bug,正在吃宵夜的我眉头一紧,立即打开了钉钉(手贱...),看了一下这不是前几天刚解决的吗,果然,使用刷新大法就解决,原因不过是用户一直停留在页面上,新的版本发布后,没有刷新拿不到新的资源。
现在大部分的前端系统都是SPA,用户在使用中对系统更新无感知,切换菜单等并不能获取最新资源,如果前端是覆盖性部署,切换菜单请求旧资源,这个旧资源已经被覆盖(hash打包的文件),还会出现一直无响应的情况。
那么,当前端部署更新后,提示一直停留在系统中的用户刷新系统很有必要。
解决方案
- 在public文件夹下加入manifest.json文件,记录版本信息
- 前端打包的时候向manifest.json写入当前时间戳信息
- 在入口JS引入检查更新的逻辑,有更新则提示更新
- 路由守卫router.beforeResolve(Vue-Router为例),检查更新,对比manifest.json文件的响应头Etag判断是否有更新
- 通过Worker轮询,检查更新,对比manifest.json文件的响应头Etag判断是否有更新。当然你如果不在乎这点点开销,可不使用Worker另开一个线程
Public下的加入manifest.json文件
{
"timestamp":1706518420707,
"msg":"更新内容如下:\n--1.添加系统更新提示机制"
}
这里如果是不向用户提示更新内容,可不填,前段开发者也无需维护manifest.json的msg内容,这里主要考虑到如果用户在填长表单的时候,填了一大半,你这时候给用户弹个更新提示,用户无法判断是否影响当前表单填写提交,如果将更新信息展示出来,用户感知更新内容,可判断是否需要立即刷新,还是提交完表单再刷新。
webpack向manifest.json写入当前时间戳信息
// 版本号文件
const filePath = path.resolve(`./public`, 'manifest.json')
// 读取文件内容
readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('读取文件时出错:', err)
return
}
// 将文件内容转换JSON
const dataObj = JSON.parse(data)
dataObj.timestamp = new Date().getTime()
// 将修改后的内容写回文件
writeFile(filePath, JSON.stringify(dataObj), 'utf8', err => {
if (err) {
console.error('写入文件时出错:', err)
return
}
})
})
如果你无需维护更新内容的话,可直接写入timestamp
// 生成版本号文件
const filePath = path.resolve(`./public`, 'manifest.json')
writeFileSync(filePath, `${JSON.stringify({ timestamp: new Date().getTime() })}`)
检查更新的逻辑
入口文件main.js处引入
我这里检查更新的文件是放在utils/checkUpdate
// 检查版本更新
import '@/utils/checkUpdate'
checkUpdate文件内容如下
import router from '@/router'
import { Modal } from 'ant-design-vue'
if (process.env.NODE_ENV === 'production') {
let lastEtag = ''
let hasUpdate = false
let worker = null
async function checkUpdate() {
try {
// 检测前端资源是否有更新
let response = await fetch(`/manifest.json?v=${Date.now()}`, {
method: 'head'
})
// 获取最新的etag
let etag = response.headers.get('etag')
hasUpdate = lastEtag && etag !== lastEtag
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}
async function confirmReload(msg = '', lastEtag) {
worker &&
worker.postMessage({
type: 'pause'
})
try {
Modal.confirm({
title: '温馨提示',
content: '系统后台有更新,请点击“立即刷新”刷新页面\n' + msg,
okText: '立即刷新',
cancelText: '5分钟后提示我',
onOk() {
worker.postMessage({
type: 'destroy'
})
location.reload()
},
onCancel() {
worker &&
worker.postMessage({
type: 'recheck',
lastEtag: lastEtag
})
}
})
} catch (e) {}
}
// 路由拦截
router.beforeEach(async (to, from, next) => {
next()
try {
await checkUpdate()
if (hasUpdate) {
worker.postMessage({
type: 'destroy'
})
location.reload()
}
} catch (e) {}
})
// 利用worker轮询
worker = new Worker(
/* webpackChunkName: "checkUpdate.worker" */ new URL('../worker/checkUpdate.worker.js', import.meta.url)
)
worker.postMessage({
type: 'check'
})
worker.onmessage = ({ data }) => {
if (data.type === 'hasUpdate') {
hasUpdate = true
confirmReload(data.msg, data.lastEtag)
}
}
}
这里因为缺换路由本来就要刷新页面,用户可无需感知系统更新信息,直接通过请求头的Etag即可,这里的Fetch方法就用head获取相应头就好了。
checkUpdate.worker.js文件如下
let lastEtag
let hasUpdate = false
let intervalId = ''
async function checkUpdate() {
try {
// 检测前端资源是否有更新
let response = await fetch(`/manifest.json?v=${Date.now()}`, {
method: 'get'
})
// 获取最新的etag和data
let etag = response.headers.get('etag')
let data = await response.json()
hasUpdate = lastEtag !== undefined && etag !== lastEtag
if (hasUpdate) {
postMessage({
type: 'hasUpdate',
msg: data.msg,
lastEtag: lastEtag,
etag: etag
})
}
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}
// 监听主线程发送过来的数据
addEventListener('message', ({ data }) => {
if (data.type === 'check') {
// 每5分钟执行一次
// 立即执行一次,获取最新的etag,避免在setInterval等待中系统更新,第一次获取的etag是新的,但是lastEtag还是undefined,不满足条件,错失刷新时机
checkUpdate()
intervalId = setInterval(checkUpdate,5 * 60 * 1000)
}
if (data.type === 'recheck') {
// 每5分钟执行一次
hasUpdate = false
lastEtag = data.lastEtag
intervalId = setInterval(checkUpdate, 5 * 60 * 1000)
}
if (data.type === 'pause') {
clearInterval(intervalId)
}
if (data.type === 'destroy') {
clearInterval(intervalId)
close()
}
})
如果不使用worker直接讲轮询逻辑放在checkUpdate即可
Worker引入
从 webpack 5 开始,你可以使用 Web Workers 代替 worker-loader
。
new Worker(new URL('./worker.js', import.meta.url));
以下版本的就只能用worker-loader
咯
也可以逻辑写成字符串,然后通过ToURL给new Worker,如下:
function createWorker(f) {
const blob = new Blob(['(' + f.toString() +')()'], {type: "application/javascript"});
const blobUrl = window.URL.createObjectURL(blob);
const worker = new Worker(blobUrl);
return worker;
}
createWorker(function () {
self.addEventListener('message', function (event) {
// 消费信息
self.postMessage('send message')
}, false);
})
worker数据通信
// 主线程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);
// Worker 线程
self.onmessage = function (e) {
var uInt8Array = e.data;
postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};
但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。
如果要直接转移数据的控制权,就要使用下面的写法。
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
Web Worker 使用教程 - 阮一峰的网络日志 (ruanyifeng.com)
然而,并不是所有的对象都可以被转移。只有那些被设计为可转移的对象(用[ Transferable ] IDL 扩展属性修饰),比如ArrayBuffer、MessagePort,ImageBitmap,OffscreenCanvas,才能通过这种方式来传递。转移操作是不可逆的,一旦对象被转移,原始上下文中的引用将不再有效。转移对象可以显著减少复制数据所需的时间和内存。
---------------------------------更新-------------------------------------
如果只考虑是否更新,无需告知内容,无需创建manifest.json,直接通过index.html的Etag判断即可,因为打包工具会自动在filename添加hash,如果有内容修改,入口文件的js引入资源文件hash会改变,通过Etag判断即可。
async function checkUpdate() {
try {
// 检测前端资源是否有更新
let response = await fetch(`/?v=${Date.now()}`, {
method: 'head',
cache: 'no-cache'
})
// 获取最新的etag
let etag = response.headers.get('etag')
hasUpdate = lastEtag && etag !== lastEtag
lastEtag = etag
} catch (e) {
return Promise.reject(e)
}
}
来源:juejin.cn/post/7329280514628534313
🎲选择困难症的福音-基于threejs+cannonjs的扔骰子小游戏
在一个美好的周末,闲来无事,约上朋友一起在家打麻将,奈何尘封已久的麻将包里翻来翻去也没找到骰子的踪影,于是想在网上找一个骰子模拟器来代替,找了半天都没有发现一款合适好用的软件,于是心血来潮,打算自己做一个🎲模拟器。
1.制定需求设计以及技术方案
1.骰子模型
- 用户可以选择多种骰子模型,如六面 15面 20面等不同风格的供选择
- 并且可以选择要投掷骰子的数量 暂定1-10
2.分数计算规则
- 每次投掷完后会自动计算总点数并显示在屏幕上
- 可以保存历史摇骰子记录 最多支持历史100条记录
- 可以自定义用户参与摇骰子,投掷完成后会显示:luke摇出了xx点。。并且保存在历史记录中
- 支持多人参与摇骰子比赛:比如先加入4名玩家,开始后会依次提示轮到哪位玩家来开始投掷,并且可以选择每局大家需要投掷的次数,最后会统计总点数以及排名。
3.动画特效
- 每次投掷时对骰子随机一个角度以及投掷方向,如果同时投掷多个骰子则随机每一个骰子的角度,给骰子施加一个向赌盘中心的力,让骰子随机落在赌盘中部,在赌盘周围增加一道空气墙来阻止骰子移动到牌桌外。
- 模拟不同骰子的落地音效
4.技术实现
- 利用threejs来实现webgl相关渲染。如骰子模型、场景渲染、相机、棋盘等。
- 利用cannosjs来模拟物理引擎。如骰子碰撞检测、抛出坠落动画、重力加速等物理效果。
2.效果展示
3.开始实现
准备3d骰子模型和棋盘素材
首先是找到合适的gltf模型,这里我们在sketchfab上找到了一款质感很真实的骰子模型。下载下来的模型可以通过gltf-viewer来查看模型效果。
棋盘的话其实是一张图片平铺起来的,这里我用了一张木地板图片。
引入资源以及初始化webgl场景
import * as CANNON from "cannon-es";
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
//创建物理世界对象
const world = new CANNON.World();
// 设置物理世界重力加速度
world.gravity.set(0, -100, 0); //重力加速度: 单位:m/s²
创建物理模型、地面以及网格模型地面
这里简单叙述一下物理世界和webgl世界的联系以及如何在webgl场景里模拟出真实的物理效果。
- 首先在three创建的webgl场景是无法直接创建并感知到物理世界的,threejs只负责实时渲染物理的状态并展示在画布上。而cannos恰好相反,它不负责渲染,只负责创建一个物理世界以及具备物理引擎的物体,并根据物体状态实时计算物体的位置、角度等。并把这些信息实时同步给webgl场景中的模型,把模型渲染到页面上实现物理世界的可视化。
- 所以我们创建骰子、地面等模型都需要创建两份。一份在webgl中创建,一份在物理世界中创建,并且保持同样的尺寸。
//创建骰子网格模型(gltf模型)
const loader = new GLTFLoader();
const gltf = await loader.loadAsync('./assets/model/dice_model.glb');
const meshModel = gltf.scene;//获取箱子网格模型
meshModel.position.set(50,30,50);
meshModel.scale.set(5,5,5);
scene.add(meshModel);
//包围盒计算
const box3 = new THREE.Box3();
box3.expandByObject(meshModel);//计算模型包围盒
const size = new THREE.Vector3();
box3.getSize(size);//包围盒计算箱子的尺寸
// 创建骰子物理模型
const sphereMaterial = new CANNON.Material()//碰撞体材质
// 物理正方体
const bodyModel = new CANNON.Body({
mass: 0.3, // 碰撞体质量0.3kg
position: new CANNON.Vec3(50,30,50), // 位置
shape: new CANNON.Box(new CANNON.Vec3(size.x/2, size.y/2, size.z/2)),
material: sphereMaterial
});
// 物理地面
const groundMaterial = new CANNON.Material()
const groundBody = new CANNON.Body({
mass: 0, // 质量为0,始终保持静止,不会受到力碰撞或加速度影响
shape: new CANNON.Plane(),
material: groundMaterial,
});
// 改变平面默认的方向,法线默认沿着z轴,旋转到平面向上朝着y方向
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);//旋转规律类似threejs 平面
world.addBody(groundBody)
//设置物理世界参数
const contactMaterial = new CANNON.ContactMaterial(groundMaterial, sphereMaterial, {
restitution: 0.5, //反弹恢复系数
})
// 把关联的材质添加到物理世界中
world.addContactMaterial(contactMaterial)
// 网格地面
const planeGeometry = new THREE.PlaneGeometry(1000, 1000);
const texture = new THREE.TextureLoader().load('./assets/textures/hardwood2_diffuse.jpg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10);
const planeMaterial = new THREE.MeshLambertMaterial({
// color:0x777777,
map: texture,
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.rotateX(-Math.PI / 2);
scene.add(planeMesh);
创建好模型后,我们在创建光源,相机等,这里就不再赘述。接下来我们开始设计物理世界的骰子抛出后坠落效果,并将物理世界和webgl渲染同步。
//点击屏幕后,设置物理骰子的角度和速度,物理会向上抛出并随着重力下落,触碰到地面后则会发生碰撞反弹
renderer.domElement.addEventListener('click', function (event) {
start_throw = true;
clearPoints();
const randomEuler = Math.random()*3;
bodyModel.quaternion.setFromEuler(Math.PI / randomEuler, Math.PI / randomEuler, Math.PI / randomEuler);
bodyModel.position.set(0,50,0);//点击按钮,body回到下落的初始位置
// 为物体设置初始速度(用以产生抛物线效果)
bodyModel.velocity.set(option.x,option.y,option.z); // x, y, z 方向上的速度
// 选中模型的第一个模型,开始下落
world.addBody(bodyModel);
})
function render() {
world.step(1/60);//更新物理计算
meshModel.position.copy(bodyModel.position); //渲染循环中,同步物理球body与网格球mesh的位置
meshModel.quaternion.copy(bodyModel.quaternion); //同步姿态角度
locateView(); //相机跟随物体移动
requestAnimationFrame(render);
renderer.render(scene, camera);
if (isBodyStopped(bodyModel)&&start_throw) {
showPoints(); //停止运动后,显示点数
start_throw = false;
}
}
render();//根据帧数渲染
接下来,我们再添加骰子点数计算相关逻辑。
//获取朝上面的点数
function getUpperFace(mesh) {
// 定义每个面的局部法线向量(手动定义)
const localNormals = [
new THREE.Vector3(0, 1, 0), // 面1
new THREE.Vector3(0, 0, -1), // 面2
new THREE.Vector3(-1, 0, 0), // 面3
new THREE.Vector3(1, 0, 0), // 面4
new THREE.Vector3(0, 0, 1), // 面5
new THREE.Vector3(0, -1, 0), // 面6
];
let maxDot = -Infinity;
let faceValue = 0;
for (let i = 0; i < localNormals.length; i++) {
// 将局部法线向量转化为世界空间
const worldNormal = localNormals[i].clone().applyQuaternion(mesh.quaternion);
// 与全局上方向的点积
const dot = worldNormal.dot(new THREE.Vector3(0, 1, 0));
// 检查点积,找到最大值
if (dot > maxDot) {
maxDot = dot;
faceValue = i + 1; // 面的点数即为索引加1
}
}
return faceValue;
}
//判断物体是否停止运动
function isBodyStopped(body, linearVelocityThreshold = 0.1, angularVelocityThreshold = 0.1) {
// 获取物体的线性速度和角速度
const linearVelocityMagnitude = body.velocity.length();
const angularVelocityMagnitude = body.angularVelocity.length();
// 判断速度是否低于设定的阈值
return linearVelocityMagnitude < linearVelocityThreshold && angularVelocityMagnitude < angularVelocityThreshold;
}
//展示点数
function showPoints() {
let res = getUpperFace(meshModel);
let point = document.getElementById("points");
point.innerHTML = `点数:${res}`
}
//清空点数
function clearPoints() {
let point = document.getElementById("points");
point.innerHTML = ``
}
实现这些逻辑后,我们已经可以模拟出骰子抛出坠落并触碰地面后反弹,在停止运动后计算点数的效果。已经实现基础功能,但是我们发现如果随机速度过大的时候会移动很远才停下,于是我们增加一个空气墙来限制骰子在固定范围内。并且增加一个碰撞检测来触发撞地声音的效果。
//添加空气墙
const wallShape = new CANNON.Box(new CANNON.Vec3(100, 100, 0.1));
const wall1 = new CANNON.Body({ mass: 0 });
wall1.addShape(wallShape);
wall1.position.set(0, 100, -100); // Back wall
world.addBody(wall1);
const wall2 = new CANNON.Body({ mass: 0 });
wall2.addShape(wallShape);
wall2.position.set(0, 100, 100); // Front wall
world.addBody(wall2);
const wall3 = new CANNON.Body({ mass: 0 });
wall3.addShape(wallShape);
wall3.quaternion.setFromEuler(0, Math.PI / 2, 0); // Rotate for side walls
wall3.position.set(100, 100, 0); // Right wall
world.addBody(wall3);
const wall4 = new CANNON.Body({ mass: 0 });
wall4.addShape(wallShape);
wall4.quaternion.setFromEuler(0, Math.PI / 2, 0); // Rotate for side walls
wall4.position.set(-100, 100, 0); // Left wall
world.addBody(wall4);
//监听物体碰撞回调
const audio = new Audio('./assets/audio/peng.mp3');
bodyModel.addEventListener('collide', (event) => {
const contact = event.contact;
//获得沿法线的冲击速度
const ImpactV = contact.getImpactVelocityAlongNormal();
// 碰撞越狠,声音越大
if(ImpactV/35>1) {
audio.volume = 1;
} else {
audio.volume = ImpactV/35>0?ImpactV/35:0;
}
audio.currentTime = 0;
audio.play();
})
这样我们就初步完成了抛掷一个骰子并获取点数的功能,看似简单的一个场景实际上设计起来并不容易,要考虑很多因素。后续我会继续增加多个骰子同时抛掷的场景,以及比赛模式。源码也会贡献出来供大家一起学习参考,如果有更好的idea也可以在评论区留言或私信,大家一起在webgl中感受物理世界的魅力!
附完整代码:
import * as CANNON from "cannon-es";
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 实例化一个gui对象
// const gui = new GUI();
// //改变交互界面style属性
// gui.domElement.style.right = '0px';
// gui.domElement.style.width = '300px';
const option = {
z: -24,
x: -36,
y: -17,
z1: 1,
x1: 1,
y1: 1,
}
// //gui控制参数
// const folder_position = gui.addFolder('速度方向');
// folder_position.add(option, 'z', -100, 100);
// folder_position.add(option, 'x', -100, 100);
// folder_position.add(option, 'y', -100, 100);
// const folder_rotation = gui.addFolder('角度');
// folder_rotation.add(option, 'z1', -10, 10).step(0.1);
// folder_rotation.add(option, 'x1', -10, 10).step(0.1);
// folder_rotation.add(option, 'y1', -10, 10).step(0.1);
// CANNON.World创建物理世界对象
const world = new CANNON.World();
// 设置物理世界重力加速度
// world.gravity.set(0, -1000, 0); //重力加速度: 单位:m/s²
world.gravity.set(0, -100, 0);
//网格球体(gltf模型)
const loader = new GLTFLoader();
const gltf = await loader.loadAsync('./assets/model/dice_model.glb');
const meshModel = gltf.scene;//获取箱子网格模型
meshModel.position.set(50,30,50);
meshModel.scale.set(5,5,5);
scene.add(meshModel);
//包围盒计算
const box3 = new THREE.Box3();
box3.expandByObject(meshModel);//计算模型包围盒
const size = new THREE.Vector3();
box3.getSize(size);//包围盒计算箱子的尺寸
// 物理球体
const sphereMaterial = new CANNON.Material()//碰撞体材质
// 物理箱子
const bodyModel = new CANNON.Body({
mass: 0.3, // 碰撞体质量0.3kg
position: new CANNON.Vec3(50,30,50), // 位置
shape: new CANNON.Box(new CANNON.Vec3(size.x/2, size.y/2, size.z/2)),
material: sphereMaterial
});
// 骨骼辅助显示
const skeletonHelper = new THREE.SkeletonHelper(meshModel);
scene.add(skeletonHelper);
// world.addBody(bodyModel);
//添加空气墙
// Create air walls
const wallShape = new CANNON.Box(new CANNON.Vec3(100, 100, 0.1));
const wall1 = new CANNON.Body({ mass: 0 });
wall1.addShape(wallShape);
wall1.position.set(0, 100, -100); // Back wall
world.addBody(wall1);
const wall2 = new CANNON.Body({ mass: 0 });
wall2.addShape(wallShape);
wall2.position.set(0, 100, 100); // Front wall
world.addBody(wall2);
const wall3 = new CANNON.Body({ mass: 0 });
wall3.addShape(wallShape);
wall3.quaternion.setFromEuler(0, Math.PI / 2, 0); // Rotate for side walls
wall3.position.set(100, 100, 0); // Right wall
world.addBody(wall3);
const wall4 = new CANNON.Body({ mass: 0 });
wall4.addShape(wallShape);
wall4.quaternion.setFromEuler(0, Math.PI / 2, 0); // Rotate for side walls
wall4.position.set(-100, 100, 0); // Left wall
world.addBody(wall4);
camera.position.set(42,85,21)
camera.lookAt(0,10,0);
// 物理地面
const groundMaterial = new CANNON.Material()
const groundBody = new CANNON.Body({
mass: 0, // 质量为0,始终保持静止,不会受到力碰撞或加速度影响
shape: new CANNON.Plane(),
material: groundMaterial,
});
// 改变平面默认的方向,法线默认沿着z轴,旋转到平面向上朝着y方向
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);//旋转规律类似threejs 平面
world.addBody(groundBody)
//设置物理世界参数
const contactMaterial = new CANNON.ContactMaterial(groundMaterial, sphereMaterial, {
restitution: 0.5, //反弹恢复系数
})
// 把关联的材质添加到物理世界中
world.addContactMaterial(contactMaterial)
//光源设置
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
directionalLight.position.set(20, 100, 10);
scene.add(directionalLight);
// 网格地面
const planeGeometry = new THREE.PlaneGeometry(1000, 1000);
const texture = new THREE.TextureLoader().load('./assets/textures/hardwood2_diffuse.jpg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10);
const planeMaterial = new THREE.MeshLambertMaterial({
// color:0x777777,
map: texture,
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.rotateX(-Math.PI / 2);
scene.add(planeMesh);
// 添加一个辅助网格地面
// const gridHelper = new THREE.GridHelper(50, 50, 0x004444, 0x004444);
// scene.add(gridHelper);
var controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 允许阻尼效果
controls.dampingFactor = 0.25; // 阻尼系数
let start_throw = false;
renderer.domElement.addEventListener('click', function (event) {
start_throw = true;
clearPoints();
const randomEuler = Math.random()*3;
bodyModel.quaternion.setFromEuler(Math.PI / randomEuler, Math.PI / randomEuler, Math.PI / randomEuler);
bodyModel.position.set(0,50,0);//点击按钮,body回到下落的初始位置
// 为物体设置初始速度(用以产生抛物线效果)
bodyModel.velocity.set(option.x,option.y,option.z); // x, y, z 方向上的速度
// 选中模型的第一个模型,开始下落
world.addBody(bodyModel);
})
const audio = new Audio('./assets/audio/peng.mp3');
bodyModel.addEventListener('collide', (event) => {
const contact = event.contact;
//获得沿法线的冲击速度
const ImpactV = contact.getImpactVelocityAlongNormal();
// 碰撞越狠,声音越大
if(ImpactV/35>1) {
audio.volume = 1;
} else {
audio.volume = ImpactV/35>0?ImpactV/35:0;
}
audio.currentTime = 0;
audio.play();
})
//判断物体是否停止运动
function isBodyStopped(body, linearVelocityThreshold = 0.1, angularVelocityThreshold = 0.1) {
// 获取物体的线性速度和角速度
const linearVelocityMagnitude = body.velocity.length();
const angularVelocityMagnitude = body.angularVelocity.length();
// 判断速度是否低于设定的阈值
return linearVelocityMagnitude < linearVelocityThreshold && angularVelocityMagnitude < angularVelocityThreshold;
}
function showPoints() {
let res = getUpperFace(meshModel);
let point = document.getElementById("points");
point.innerHTML = `点数:${res}`
}
//相机跟随物体移动
function locateView() {
camera.position.x = meshModel.position.x;
camera.position.y = meshModel.position.y + 30;
camera.position.z = meshModel.position.z + 20;
camera.lookAt(meshModel.position)
}
function clearPoints() {
let point = document.getElementById("points");
point.innerHTML = ``
}
//获取朝上面的点数
function getUpperFace(mesh) {
// 定义每个面的局部法线向量(手动定义)
const localNormals = [
new THREE.Vector3(0, 1, 0), // 面1
new THREE.Vector3(0, 0, -1), // 面2
new THREE.Vector3(-1, 0, 0), // 面3
new THREE.Vector3(1, 0, 0), // 面4
new THREE.Vector3(0, 0, 1), // 面5
new THREE.Vector3(0, -1, 0), // 面6
];
let maxDot = -Infinity;
let faceValue = 0;
for (let i = 0; i < localNormals.length; i++) {
// 将局部法线向量转化为世界空间
const worldNormal = localNormals[i].clone().applyQuaternion(mesh.quaternion);
// 与全局上方向的点积
const dot = worldNormal.dot(new THREE.Vector3(0, 1, 0));
// 检查点积,找到最大值
if (dot > maxDot) {
maxDot = dot;
faceValue = i + 1; // 面的点数即为索引加1
}
}
return faceValue;
}
function render() {
world.step(1/60);//更新物理计算
meshModel.position.copy(bodyModel.position); //渲染循环中,同步物理球body与网格球mesh的位置
meshModel.quaternion.copy(bodyModel.quaternion); //同步姿态角度
locateView(); //相机跟随物体移动
requestAnimationFrame(render);
renderer.render(scene, camera);
if (isBodyStopped(bodyModel)&&start_throw) {
showPoints(); //停止运动后,显示点数
start_throw = false;
}
}
render();
来源:juejin.cn/post/7394993393125064704
谈谈国内前端的三大怪啖
因为工作的原因,我和一些外国前端开发有些交流。他们对于国内环境不了解,有时候会问出一些有趣的问题,大概是这些问题的启发,让我反复在思考一些更为深入的问题。
今天聊三个事情:
- 小程序
- 微前端
- 模块加载
小程序
每个行业都有一把银座,当坐上那把银座时,做什么便都是对的。
“我们为什么需要小程序?”
第一次被问到这个问题,是因为一个法国的同事。他被派去做一个移动端业务,刚好那个业务是采用小程序在做。于是一个法国小哥就在被痛苦的中文文档和黑盒逻辑中来回折磨着 🤦。
于是,当我们在有一次交流中,他问出了我这个问题:我们为什么需要小程序?
说实话,我试图解释了 19 年国内的现状,以及微信小程序推出时候所带来的便利和体验等等。总之,在我看来并不够深刻的见解。
即便到现在为止,每次当我使用小程序的时候,依旧会复现这个问题。在 ChatGPT 11 月份出来的时候,我也问了它这个很有国内特色的问题:
看起来它回答的还算不错,至少我想如果它来糊弄那些老外,应该会比我做的更好些。
但如果扪心自问,单从技术上来讲。以上这些事情,一定是有其他方案能解决的。
所以从某种程度上来看,这更像是一场截胡的商业案例:
应用市场
全世界的互联网人都知道应用市场是非常有价值的事情,可以说操作系统最值钱的部分就在于他们构建了自己的应用市场。
只要应用在这里注册发行,雁过拔毛,这家公司就是互联网世界里的统治阶级,规则的制定者。
反之则需要受制于人,APP 做的再大,也只是应用市场里的一个应用,做的好与坏还得让应用商店的评判。
另外,不得不承认的是,一个庞大如苹果谷歌这样的公司,他们的应用商店对于普通国内开发者来说,确实是有门槛的。
在国内海量的 APP 需求来临之前,能否提供一个更低成本的解决方案,来消化这些公司的投资?
毕竟不是所有的小企业都需要 APP,其实他们大部分需求 Web 就可以解决,但是 Web 没牌面啊,做 Web 还得砸搜索的钱才有流量。(某度搜索又做成那样...)
那做操作系统?太不容易,那么多人都溺死在水里了,这水太深。
那有没有一种办法可以既能构建生态,又有 APP 的心智,还能给入驻企业提供流量?
于是,在 19 年夏天,滨海大厦下的软件展业基地里,每天都在轮番播放着,做 XX小程序,拥抱下一个风口...
全新体验心智
小程序用起来挺方便的。
你有没有想过,这些美妙感觉的具体都来自哪些?以及这些真的是 Web 技术本身无法提供的吗?
- 靠谱感,每个小程序都有约束和规范,于是你可以将他们整整齐齐的陈放在你的列表里,仿佛你已经拥有了这一个个精心雕琢的作品,相对于一条条记不住的网页地址和鱼龙混杂的网页内容来说,这让你觉得小程序更加的有分量和靠谱。
- 安全感,沉浸式的头部,没有一闪而过的加载条,这一切无打扰的设计,都让你觉得这是一个在你本地的 APP,而不是随时可能丢失网页。你不会因为网速白屏而感到焦虑,尽管网络差的时候,你的 KFC 依旧下不了单 😂
- 沉浸感,我不知道是否打开网页时顶部黑黑的状态栏是故意留下的,还是不小心的... 这些限制都让用户非常强烈的意识到这是一个网页而不是 APP,而小程序中虽然上面也存在一个空间的空白,但是却可以被更加沉浸的主题色和氛围图替代。网页这个需求做不了?我不信。
H5 | 小程序 |
---|---|
![]() | ![]() |
- 顺滑感,得益于 Native 的容器实现,小程序在所有的视图切换时,都可以表现出于原生应用一样的顺滑感。其实这个问题才是在很多 Hybrid 应用中,主要想借助客户端来处理和解决的问题。类似容器预开、容器切换等技术是可以解决相关问题的,只是还没有一个标准。
我这里没有提性能,说实话我不认为性能在小程序中是有优势的(Native 调用除外,如地图等,不是一个讨论范畴)。作为普通用户,我们感受到的只是离线加载下带来的顺滑而已。
而上述提到的许多优势,这对于一个高品质的 Web 应用来说是可以做得到的,但注意这里是高品质的 Web 应用。而这种“高品质”在小程序这里,只是入驻门槛而已。
心智,这个词,听起来很黑话,但却很恰当。当小程序通过长期这样的筛选,所沉淀出来一批这样品质的应用时。就会让每个用户即便在还没打开一个新的小程序之前,也有不错体验的心理预期。这就是心智,一种感觉跟 Web 不一样,跟 APP 有点像的心智。
打造心智,这件事情好像就是国内互联网企业最擅长做的事情,总是能从一些细微的差别中开辟一条独立的领域,然后不断强化灌输本来就不大的差异,等流量起来再去捞钱变现。
我总是感觉现在的互利网充斥着如何赚钱的想法,好像永远赚不够。“赚钱”这个事情,在这些公司眼里就是圈人圈地抢资源,看看谁先占得先机,别人有的我也得有,这好像是最重要的事情。
很少有企业在思考如何创造些没有的市场,创造些真正对技术发展有贡献,对社会发展有推动作用的事情。所以在国内互联网圈也充斥着一种奇怪的价值观,有技术的不一定比赚过钱的受待见。
管你是 PHP 还是 GO,管你是在做游戏还是直播带货,只要赚到钱就是高人。并且端的是理所应当、理直气壮,有些老板甚至把拍摄满屋子的程序员为自己打工作为一种乐趣。什么情怀、什么优雅、什么愿景,人生就俩字:搞钱。
不是故意高雅,赚钱这件事情本身不寒碜,只是在已经赚到盆满钵满、一家独大的时候还在只是想着赚更多的钱,好像赚钱的目的就是为了赚钱一样,这就有点不合适。企业到一定程度是要有社会责任的,龙头企业每一个决定和举措,都有会影响接下来的几年里这个行业的价值观走向。
当然也不是完全没有好格局的企业,我非常珍惜每一个值得尊重的中国企业,来自一个蔚来车主。
小程序在商业上固然是成功的,但吃的红利可以说还是来自 网页 到 应用 的心智变革。将本来流向 APP 的红利,截在了小程序生态里。
但对于技术生态的发展却是带来了无数条新的岔路,小程序的玩法就决定了它必须生长在某个巨型应用里面,不论是用户数据获取、还是 API 的调用,其实都是取决于应用容器的标准规范。
不同公司和应用之间则必然会产生差异,并且这种差异是墒增式的差异,只会随着时间的推移越变越大。如果每个企业都只关注到自己业务的增长,无外部约束的话,企业必然会根据自己的业务发展和政策需要,选择成本较低的调整 API,甚至会故意制造一些壁垒来增加这种差异。
小程序,应该是 浏览器 与 操作系统 的融合,这本应该是推动这两项技术操刀解决的事情。
微前端
qiankun、wujie、single-spa 是近两年火遍前端的技术方案,同样一个问题:我们为什么需要微前端?
我不确定是否每个在使用这项技术的前端都想清楚了这个问题,但至少在我面试过的候选人中,我很少遇到对自己项目中已经在使用的微前端,有很深的思考和理解的人。
先说下我的看法:
- 微前端,重在解决项目管理而不在用户体验。
- 微前端,解决不了该优化和需要规范的问题。
- 微前端,在挽救没想清楚 MPA 的 SPA 项目。
没有万能银弹
银色子弹(英文:Silver Bullet),或者称“银弹”“银质子弹”,指由纯银质或镀银的子弹。在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银色子弹往往被描绘成具有驱魔功效的武器,是针对狼人、吸血鬼等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。
所有技术的发展都是建立在前一项技术的基础之上,但技术依赖的选择过程中一定需要保留第一性原理的意识。
当 React、Vue 兴起,当 打包技术(Webpack) 兴起,当 网页应用(SPA) 兴起,这些杰出的技术突破都在不同场景和领域中给行业提供了新的思路、新的方案。
不知从何时开始,前端除了 div 竟说不出其他的标签(还有说 View 的),项目中再也不会考虑给一个通用的 class 解决通用样式问题。
不知从何时开始,有没有权限需要等到 API 请求过后才知道,没有权限的话再把页面跳转过去申请。
不知从何时开始,大家的页面都放在了一个项目里,两个这样的巨石应用融合竟然变成了一件困难的事。
上面这些不合理的现状,都是在不同的场景下,不思考适不适合,单一信奉 “一招吃遍天” 下演化出的问题。
B 端应用,是否应该使用 SPA? 这其实是一个需要思考的问题。
微前端从某种程度上来讲,是认准 SPA 了必须是互联网下一代应用标准的产物,好像有了 SPA 以后,MPA 就变得一文不值。甭管页面是移动端的还是PC的;甭管页面是面对 C 端的还是 B 端的;甭管一个系统是有 20 个页面还是 200 个页面,一律行这套逻辑。
SPA 不是万能银弹,React 不是万能银弹,Tailwind 不是万能银弹。在新技术出现的时候,保持热情也要保持克制。
ps. 我也十分痛恨 React 带来的这种垄断式的生态,一个 React 组件将 HTML 和 Style 都吃进去,让你即使在做一个移动端的纯展示页面时,也需要背上这些称重的负担。
质疑 “墨守成规”,打开视野,深度把玩,理性消费。
分而治之
分治法,一个很基本的工程思维。
在我看来在一个正常商业迭代项目中的主要维护者,最好不要超过 3 个人,注意是主要维护者(Maintainer) 。
你应该让每个项目都有清晰的责任人,而不是某行代码,某个模块。责任人的理解是有归属感,有边界感的那种,不是口头意义上的责任人。(一些公司喜欢搞这种虚头巴脑的事情,什么连坐…)
我想大部分想引入微前端的需求都是类似 如何更好的划分项目边界,同时保留更好的团队协同。
比如 导航菜单 应该是独立收口独立管理的,并且当其更新时,应该同时应用于所有页面中。类似的还有 环境变量、时区、主题、监控及埋点。微前端将这些归纳在主应用中。
而具体的页面内容,则由对应的业务进行开发子应用,最后想办法将路由关系注册进主应用即可。
当然这样纯前端的应用切换,还会出现不同应用之间的全局变量差异、样式污染等问题,需要提供完善的沙箱容器、隔离环境、应用之间通信等一系列问题,这里不展开。
当微前端做到这一部分的时候,我不禁在想,这好像是在用 JavaScript 实现一个浏览器的运行容器。这种本应该浏览器本身该做的事情,难道 JS 可以做的更好?
只是做到更好的项目拆分,组织协同的话,引入后端服务,由后端管控路由表和页面规则,将页面直接做成 MPA,这个方案或许并不比引入微前端成本高多少。
体验差异
从 SPA 再回 MPA,说了半天不又回去了么。
所以不防想想:在 B端 业务中使用 SPA 的优势在哪里?
流畅的用户体验:
这个话题其实涵盖范围很广,对于 SPA 能带来的 “流畅体验”,对于大多数情况下是指:导航菜单不变,内容变化 发生变化,页面切换中间不出现白屏。
但要做到这个点,其实对于 MPA 其实并没有那么困难,你只需要保证你的 FCP 在 500ms 以内就行。
以上的页面切换全部都是 MPA 的多页面切换,我们只是简单做了导航菜单的 拆分 和 SWR,并没有什么特殊的 preload、prefetch 处理,就得到了这样的效果。
因为浏览器本身在页面切换时会在 unload 之前先 hold 当前页面的视图不变,发起一下一个 document 的请求,当页面的视图渲染做到足够快和界面结构稳定就可以得到这样的效果。
这项浏览器的优化手段我找了很久,想找一篇关于它的博文介绍,但确实没找到相关内容,所以 500ms 也是我的一个大致测算,如果你知道相关的内容,可以在评论区补充,不胜感激。
所以从这个角度来看,浏览器本身就在尽最大的努力做这些优化,并且他们的优化会更底层、更有效的。
离线访问 (PWA)
SPA 确实会有更好的 PWA 组织能力,一个完整的 SPA 应用甚至可以只针对编译层做改动就可以支持 PWA 能力。
但如果看微前端下的 SPA 应用,需要支持 PWA 那就同样需要分析各子应用之间的元数据,定制 Service Worker。这种组织关系和定制 SW,对于元数据对于数据是来自前端还是后端,并不在意。
也就是说微前端模式下的 PWA,同样的投入成本,把页面都管理在后端服务中的 MPA 应用也是可以做到相同效果的。
项目协同、代码复用
有人说 SPA 项目下,项目中的组件、代码片段是可以相互之间复用的,在 MPA 下就相对麻烦。
这其实涉及到项目划分的领域,还是要看具体的需求也业务复杂度来定。如果说整个系统就是二三十个页面,这做成 SPA 使用前端接管路由高效简单,无可厚非。
但如果你本身在面对的是一个服务于复杂业务的 B 端系统,比如类似 阿里云、教务系统、ERP 系统或者一些大型内部系统,这种往往需要多团队合作开发。这种时候就需要跟完善的项目划分、组织协同和系统整合的方案。
这个时候 SPA 所体现出的优势在这样的诉求下就会相对较弱,在同等投入的情况下 MPA 的方案反而会有更少的执行成本。
也不是所有项目一开始就会想的那么清楚,或许一开始的时候就是个简单的 SPA 项目,但是随着项目的不断迭代,才变成了一个个复杂的巨石应用,现在如果再拆出来也会有许多迁移成本。引入微前端,则可以...
这大概是许多微前端项目启动的背景介绍,我想说的是:对于屎山,我从来不信奉“四两拨千斤”。
如果没有想好当下的核心问题,就引入新的“银弹”解决问题,只会是屎山雕花。
项目协同,抽象和复用这些本身不是微前端该解决的问题,这是综合因素影响下的历史背景问题。也是需要一个个资深工程师来把控和解决的核心问题,就是需要面对不同的场景给出不同的治理方案。
这个道理跟防沙治沙一样,哪有那么多一蹴而就、立竿见影的好事。
模块加载
模块加载这件事情,从玉伯大佬的成名作 sea.js 开始就是一个非常值得探讨的问题。在当时 jQuery 的时代里,这是一个绝对超前的项目,我也在实际业务中体会过在无编译的环境下 sea.js 的便捷。
实际上,不论是微前端、低代码、会场搭建等热门话题离不开这项技术基础。
import * from *
我们每天都在用,但最终的产物往往是一个自运行的 JS Bundle,这来自于 Webpack、Vite 等编译技术的发展。让我们可以更好的组织项目结构,以构建更复杂的前端应用。
模块的概念用久了,就会自然而然的在遇到浏览器环境中,遇到动态模块加载的需求时,想到这种类似模块加载的能力。
比如在遇到会场千奇百怪的个性化营销需求时,能否将模块的 Props 开放出来,给到非技术人员,以更加灵活的方式让他们去做自由组合。
比如在低代码平台中,让开发者自定义扩展组件,动态的将开发好的组件注册进低代码平台中,以支持更加个性的需求。
在万物皆组件的思想影响下,把一整个完整页面都看做一个组件也不是不可以。于是在一些团队中,甚至提倡所有页面都可以搭建、搭建不了的就做一个大的页面组件。这样及可以减少维护运行时的成本,又可以统一管控和优化,岂不美哉。
当这样大一统的“天才方案”逐渐发展成为标准后,也一定会出现一些特殊场景无法使用,但没关系,这些天才设计者肯定会提供一种更加天才的扩展方案出来。比如插件,比如扩展,比如 IF ELSE。再后来,就会有性能优化了,而优化的 追赶对象 就是用原来那种简单直出的方案。
有没有发现,这好像是在轮回式的做着自己出的数学题,一道接一道,仿佛将 1 + 1的结论重新演化了一遍。
题外话,我曾经自己实现过一套通过 JSON Schema 描述 React 结构的 “库” ,用于一个低代码核心层的渲染。在我的实现过程中,我越发觉得我在做一件把 JSX 翻译成 JS 的事情,但 JSX 或者 HTML 本身不就是一种 DSL 么。为什么一定要把它翻译成 JSON 来传输呢?或者说这样的封装其本身有意义么?这不就是在做 PHP、.Net 直接返回带有数据的 HTML Ajax 一样的事情么。
传统的浏览器运行环境下要实现一个模块加载器,无非是在全局挂载一个注册器,通过 Script 插入一段新的 JS,该 JS 通过特殊的头尾协议,将运行时的代码声明成一个函数,注册进事先挂载好的注册器。
但实际的实现场景往往要比这复杂的多,也有一些问题是这种非原生方式无法攻克的问题。比如全局注册器的命名冲突;同模块不同版本的加载冲突;并发加载下的时序问题;多次模块加载的缓存问题 等等等等等...
到最后发现,这些事情好像又是在用 JS 做浏览器该做的事情。然而浏览器果然就做了,,Vite 就主要采用这种模式实现了它 1 年之内让各大知名项目切换到 Vite 的壮举。
“但我们用不了,有兼容性问题。”
哇哦,当我看着大家随意写出的 display: grid
样式定义,不禁再次感叹人们对未知的恐惧。
import.meta
的兼容性是另外一个版本,是需要 iOS12 以上,详情参考:caniuse.com/?search=imp…
试想一下,现在的低代码、会场搭建等等各类场景的模块加载部分,如果都直接采用 ESM 的形式处理,这对于整个前端生态和开发体验来说会有多么大的提升。
模块加载,时至今日,本来就已经不再需要 loader。 正如 seajs 中写的:前端模块化开发那点历史
历史不是过去,历史正在上演。随着 W3C 等规范、以及浏览器的飞速发展,前端的模块化开发会逐步成为基础设施。一切终究都会成为历史,未来会更好。
结语
文章的结尾,我想感叹另外一件事,国人为什么一定要有自己的操作系统?为什么一定需要参与到一些规范的制定中?
因为我们的智慧需要有开花的土壤,国内这千千万开发者的抱负需要有地方释放。
如果没有自己掌握核心技术,就是只能在问题出现的时候用另类的方式来解决。最后在一番折腾后,发现更底层的技术只要稍稍一改就可以实现的更好。这就像三体中提到的 “智子” 一样,不断在影响着我们前进的动力和方向。
不论是小程序、微前端还是模块加载。试想一下,如果我们有自己的互联网底蕴,能决定或者影响操作系统和浏览器的底层能力。这些 “怪啖” 要么不会出现,要么就是人类的科技创新。
希望未来技术人不用再追逐 Write Once, Run Everywhere 的事情...
来源:juejin.cn/post/7267091810366488632
为什么删除node_modules文件夹那么慢
Windows系统 为什么删除node_modules文件夹那么慢?
在Windows系统中删除node_modules
文件夹可能会比较慢的原因有以下几点:
- 文件数量过多:
node_modules
文件夹通常包含大量的文件和文件夹,如果其中文件数量过多,系统需要逐一扫描并删除每个文件,这会导致删除过程变得缓慢。 - 文件路径过长:在Windows系统中,文件路径的长度有限制,如果
node_modules
文件夹中存在过长的文件路径,系统在删除这些文件时可能会变得缓慢。 - 文件占用:
node_modules
文件夹中可能包含一些被其他程序占用的文件,这会导致系统无法立即删除这些文件,从而延长删除时间。 - 磁盘速度:如果
node_modules
文件夹位于机械硬盘上而非固态硬盘,机械硬盘的读写速度相对较慢,也会影响删除操作的速度。 - 杀软扫描:有些杀毒软件在删除文件时会对文件进行扫描,以确保文件不包含恶意代码。这个额外的扫描过程也会增加删除文件的时间。
为什么在苹果系统上删除node_modules文件夹就很快?
- 文件系统差异:Windows采用的是NTFS文件系统,而macOS使用的是APFS文件系统,APFS 在快速复制、文件元数据管理、空间分配等方面具有优势,支持快速文件复制、快速目录大小计算、快速空间释放等功能,而 NTFS 和 exFAT 在某些方面可能不如 APFS 那么快速和高效。
- 文件路径处理:Windows对文件路径长度有限制,而macOS对文件路径长度的限制相对较宽松。如果
node_modules
文件夹中存在过长的文件路径,Windows系统在处理这些文件时可能会变得缓慢。 - 文件锁定:Windows系统在处理被其他程序占用的文件时,可能会出现文件锁定的情况,导致删除操作变得缓慢。而macOS系统在这方面可能更加灵活。
- 文件系统碎片:Windows系统在长时间使用后可能会产生文件系统碎片,这会影响文件的读写和删除速度。而macOS对文件系统碎片的处理可能更加高效。
Windows中删除慢解决方案
为了加快在Windows系统中删除文件夹的速度,可以尝试使用命令行删除、关闭占用文件的程序、使用专门的删除工具等方法,以提高删除效率。
- 在删除前关闭占用文件的程序:确保
node_modules
文件夹中的文件没有被其他程序占用,可以提前关闭相关程序再进行删除操作。 - 使用固态硬盘:如果可能的话,将
node_modules
文件夹放在固态硬盘上,可以显著提高文件的读写速度。 - 使用命令行删除:在命令行中使用
rd /s /q node_modules
命令可以快速删除node_modules
文件夹,避免Windows资源管理器中的删除操作。 - 使用专门的删除工具:例如 npm 全局安装 rimraf,以后直接使用删除命令即可。
npm install rimraf -g
~
rimraf node_modules/
来源:juejin.cn/post/7350107540325875721
Fuse.js一个轻量高效的模糊搜索库
最近逛github的时候发现了一个非常好用的轻量工具库,Fuse.js,支持模糊搜索。感觉还是非常好用的,所以有了此篇博客,这篇文章主要是介绍Fuse的使用,同样,我对这个开源项目的实现也非常感兴趣。后续会出一篇Fuse源码解析的文章来分析其实现原理。
Fuse.js是什么?
强大、轻量级的模糊搜索库,没有任何依赖关系。
什么是模糊搜索?
一般来说,模糊搜索(更正式的名称是近似字符串匹配)是查找与给定模式近似相等(而不是完全相等)的字符串的技术。
通常我们项目中的的模糊搜索大多数情况下有几种方案可用:
- 前端工程通过正则表达式或者字符串匹配来实现
- 调用后端接口去匹配搜索
- 使用搜索引擎如:ElasticSearch或Algolia等
但是这些方案都有各自的缺陷,比如正则表达式和字符串匹配的效率较低,且无法处理复杂的搜索需求,而调用后端接口和搜索引擎虽然效率高,但是需要额外的服务器资源,且需要维护一套搜索引擎。
所以,Fuse.js的出现就是为了解决这些问题,它是一个轻量级的模糊搜索库,没有依赖关系,支持复杂的搜索需求,且效率高,当然Fuse并不适用于所有场景。
Fuse.js的使用场景
它可能不适用于所有情况,但根据您的搜索要求,它可能是最理想的。例如:
- 当您想要对小型到中等大型数据集进行客户端模糊搜索时
- 当您无法证明设置专用后端只是为了处理搜索时
- ElasticSearch 或 Algolia 虽然都是很棒的服务,但对于您的特定用例来说可能有些过度
Fuse.js的使用
安装
Fuse支持多种安装方式
NPM
npm install fuse.js
Yarn
yarn add fuse.js
CDN 引入
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0">script>
引入
ES6 模块语法
import Fuse from 'fuse.js'
CommonJS 语法
const Fuse = require('fuse.js')
Tips: 使用npm或者yarn引入,支持两种模块语法引入,如果是使用cdn引入,那么Fuse将被注册为全局变量。直接使用即可
使用
以下是官网一个最简单的例子,只要简单的构造new Fuse对象,就能模糊搜索匹配到你想要的结果
// 1. List of items to search in
const books = [
{
title: "Old Man's War",
author: {
firstName: 'John',
lastName: 'Scalzi'
}
},
{
title: 'The Lock Artist',
author: {
firstName: 'Steve',
lastName: 'Hamilton'
}
}
]
// 2. Set up the Fuse instance
const fuse = new Fuse(books, {
keys: ['title', 'author.firstName']
})
// 3. Now search!
fuse.search('jon')
// Output:
// [
// {
// item: {
// title: "Old Man's War",
// author: {
// firstName: 'John',
// lastName: 'Scalzi'
// }
// },
// refIndex: 0
// }
// ]
从上述代码中可以看到我们要通过Fuse 对books的这个数组进行模糊搜索,构建的Fuse对象中,模糊搜索的key定义为['title', 'author.firstName'],支持对title及author.firstName这两个字段进行搜索。然后执行fuse的search API就能过滤出我们的期望结果。整体代码还是非常简单的。
高级配置
Demo示例只是提供了一个基础版本的模糊搜索。如果用户想获得更灵活的搜索能力,比如搜索结果排序、权重控制、搜索结果高亮等,那么就需要对Fuse进行一些高级配置。
Fuse的所有配置都是通过new Fuse时传入的参数来配置的,下面列举一些常用的配置项:
const options = {
keys: ['title', 'author'], // 指定搜索key值,可多选
isCaseSensitive: false, //是否区分大小写 默认为false
includeScore: false, //结果集中是否展示匹配项的分数字段, 分数越大代表匹配程度越低,区间值为0-1,注意:当此项为true时,会返回完整的结果集,只不过每一项中携带了score分数字段
includeMatches: false, //匹配项是否应包含在结果中。当时true,结果的每条记录都包含匹配项的索引。这个通常我们用来对搜索内容做高亮处理
threshold: 0.6, // 阈值控制匹配的敏感度,默认值为0.6,如果要完全匹配这里要设置为0
shouldSort: true, // 是否对结果进行排序
location: 0, // 匹配的位置,0 表示开头匹配
distance: 100, // 搜索的最大距离
minMatchCharLength: 2, // 最小匹配字符长度
};
出了上述常用的一些配置项之外,Fuse还支持更高阶模糊搜索,如权重搜索,嵌套搜索,运算符拓展搜索,具体高阶用法可以参考官方文档。
Fuse的主要实现原理是通过改写Bitap 算法(近似字符串匹配)算法的内部实现来支撑其模糊搜索的算法依据,后续会出一篇文章看一下作者源码的算法实现。
总结
Fuse的文章到此就结束了,你没看错就这么一点介绍就基本能支撑我们在项目中的应用,谢谢阅读,如果哪里有不对的地方请评论博主,会及时进行改正。
来源:juejin.cn/post/7393172686115569705
微信小程序 折叠屏适配
最近维护了将近的一年的微信小程序(某知名企业),突然提出要兼容折叠屏,这款小程序主要功能一些图表汇总展示,也就是专门给一些领导用的,也不知道为啥领导们为啥突然喜欢用折叠屏手机了,一句话需求,苦的还是咱们程序员,但没办法,谁让甲方是爸爸呢,硬着头皮改吧,好在最后解决了,因为是甲方内部使用的小程序,这里不便贴图,但有官方案例图片,以供参考
启用大屏模式
从小程序基础库版本 2.21.3 开始,在 Windows、Mac、车机、安卓 WMPF 等大屏设备上运行的小程序可以支持大屏模式。可参考小程序大屏适配指南。方法是:在 app.json 中添加 "resizable": true
看到这里我心里窃喜,就加个配置完事了?这也太简单了,但后面证明我想简单了,
主要有两大问题:
- 1 尺寸不同的情况下内容展示效果兼容问题
- 2 预览版和体验版 大屏模式冷启动会生效,但热启动 和 菜单中点击重新进入小程、授权操作,会失效变成窄屏
解决尺寸问题
因为css的长度单位大部分用的 rpx,窄屏和宽屏展示差异出入较大,别说客户不认,自己这关就过不了,简直都不忍直视,整个乱成一片,尤其登录页,用了定位,更是乱上加乱。
随后参考了官方的文档 小程序大屏适配指南和自适应布局,方案对于微信小程序原生开发是可行的,但这个项目用的 uni-app开发的,虽然uni-app 也有对应的响应式布局组件,再加上我是个比较爱偷懒的人(甲方给的工期事件也有限制),不可能花大量时间把所有也页面重新写一遍布局,这是不现实的。
于是又转战到uni-app官网寻找解决方案 uni-app宽屏适配指南
内容缩放拉伸的处理 这一段中提出了两个策略
- 1.局部拉伸:页面内容划分为固定区域和长宽动态适配区域,固定区域使用固定的px单位约定宽高,长宽适配区域则使用flex自动适配。当屏幕大小变化时,固定区域不变,而长宽适配区域跟着变化
- 2.等比缩放:根据页面屏幕宽度缩放。rpx其实属于这种类型。在宽屏上,rpx变大,窄屏上rpx变小。
随后看到这句话特别符合我的需求,哈哈 省事 省事 省事
策略2省事,设计师按750px屏宽出图,程序员直接按rpx写代码即可。但策略2的实际效果不如策略1好。程序员使用策略1,分析下界面,设定好局部拉伸区域,这样可以有更好的用户体验
具体实现
1.配置 pages.json 的 globeStyle
{
"globalStyle": {
"rpxCalcMaxDeviceWidth": 1200, // rpx 计算所支持的最大设备宽度,单位 px,默认值为 960
"rpxCalcBaseDeviceWidth": 375, // rpx 计算使用的基准设备宽度,设备实际宽度超出 rpx 计算所支持的最大设备宽度时将按基准宽度计算,单位 px,默认值为 375
"rpxCalcIncludeWidth": 750 // rpx 计算特殊处理的值,始终按实际的设备宽度计算,单位 rpx,默认值为 750
},
}
2.单位兼容
还有一点官方也提出来了很重要,那就很多时候 会把宽度750rpx 当成100% 使用,这在宽屏的设备上就会有问题, uniapp给了两种解决方案
- 750rpx 改为100%
- 另一种是配置rpxCalcIncludeWidth,设置某个特定数值不受rpxCalcMaxDeviceWidth约束
想要用局部拉伸:页面内容划分为固定区域和长宽动态适配区域”的策略,单位必须用px
添加脚本
项目根目录新增文件 postcss.config.js 内容如下。则在编译时,编译器会自动转换rpx单位为px。
// postcss.config.js
const path = require('path')
module.exports = {
parser: 'postcss-comment',
plugins: {
'postcss-import': {
resolve(id, basedir, importOptions) {
if (id.startsWith('~@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
} else if (id.startsWith('@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
} else if (id.startsWith('/') && !id.startsWith('//')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
}
return id
}
},
'autoprefixer': {
overrideBrowserslist: ["Android >= 4", "ios >= 8"],
remove: process.env.UNI_PLATFORM !== 'h5'
},
// 借助postcss-px-to-viewport插件,实现rpx转px,文档:https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
// 以下配置,可以将rpx转换为1/2的px,如20rpx=10px,如果要调整比例,可以调整 viewportWidth 来实现
'postcss-px-to-viewport': {
unitToConvert: 'rpx',
viewportWidth: 200,
unitPrecision: 5,
propList: ['*'],
viewportUnit: 'px',
fontViewportUnit: 'px',
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
replace: true,
exclude: undefined,
include: undefined,
landscape: false
},
'@dcloudio/vue-cli-plugin-uni/packages/postcss': {}
}
}
大屏模式失效问题
下面重头戏来了,这期间经历 蜿蜒曲折 ,到头来发现都是无用功,我自己都有被wx蠢到发笑,唉,
样式问题解决后 开始着重钻研 大屏失效的问题,但看了官方的多端适配示例demo,人家的就是好的,那就应该有解决办法,于是转战github地址 下项目,谁知这项目暗藏机关,各种报错,让你跑不起来。。。。,让我一度怀疑腾讯也这么拉跨
还好issues 区一位大神有解决办法 感兴趣的老铁可以去瞅瞅
另外 微信小程序开发工具需要取消这两项,最后当项目跑起来后我还挺开心,模拟器上没有问题,但用真机预览的时候我啥眼了,还是窄屏,偶尔可以大屏,后面发现 冷启动是大屏,热启动和点击右上角菜单中的重新进入小程序按钮都会自己变成窄屏幕
![]() | ![]() |
这是官方的项目啊,为啥人家的可以,我本地跑起来却不可以,让我一度怀疑这里有内幕,经过几轮测试还是不行,于是乎,我开始了各种询问查资料,社区、私聊、评论、github issues,最后甚至 统计出来了 多端适配示例demo 开发者的邮箱 挨个发了邮件,但都结果无一例外,全部石沉大海
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
结果就是,没有办法了,想看看是不是只有预览和体验版有问题,后面发布到正式版后,再看居然没问题了,就是这么神奇,也是无语!!!! 原来做了这么多无用功。。。。
来源:juejin.cn/post/7273764921456492581
前端项目公共组件封装思想(Vue)
1. 通用组件(表单搜索+表格展示+分页器)
在项目当中我们总会遇到这样的页面:页面顶部是一个表单筛选项,下面是一个表格展示数据。表格下方是一个分页器,这样的页面在我们的后台管理系统中经常所遇到,有时候可能不止一个页面,好几个页面的结构都是这种。如图:
本人记得,在react中的高级组件库中有这么一个组件,就实现了这么一个效果。就拿这个页面来说我们实现一下组件封装的思想:1.首先把每个页面的公共部分抽出来,比如标题等,用props或者插槽的形式传入到组件中进行展示 2. 可以里面数据的双向绑定实现跟新的效果 3. 设置自定义函数传递给父组件要做上面事情
1.将公共的部分抽离出来
TableContainer组件
<template>
<div class="container">
<slot name="navbar"></slot>
<div class="box-detail">
<div class="detail-box">
<div class="box-left">
<div class="left-bottom">
<div class="title-bottom">{{ title }}</div>
<div class="note">
<div class="note-detail">
<slot name="table"></slot>
</div>
</div>
</div>
</div>
</div>
</div>
<el-backtop style="width: 3.75rem; height: 3.75rem" :bottom="10" :right="5">
<div
style="
{
width: 5.75rem;
flex-shrink: 0;
border-radius: 2.38rem;
background: #fff;
box-shadow: 0 0.19rem 1rem 0 #2b4aff14;
}
"
>
<i class="el-icon-arrow-up" style="color: #6e6f74"></i>
</div>
</el-backtop>
</div>
</template>
这里的话利用了具名插槽插入了navbar、table组件,title通过props的属性传入到子组件当中。进行展示,
父组件
<TableContainer title="资源审核">
<template v-slot:navbar>
<my-affix :offset="0">
<Navbar/>
</my-affix>
</template>
<template v-slot:table>
<SourceAuditTable/>
</template>
</TableContainer>
当然这是一个非常非常简单的组件封装案例
接下来我们看一个高级一点的组件封装
父组件
<template>
<div>
<hr>
<HelloWorld :page.sync="page" :limit.sync="limit" />
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue';
export default {
data() {
return {
page: 1,
limit: 5
}
},
components: {
HelloWorld
},
}
</script>
父组件传递给子组件各种必要的属性:total(总共多少条数据)、page(当前多少页)、limit(每页多少条数据)、pageSizes(选择每页大小数组)
子组件
<template>
<el-pagination :current-page.sync="currentPage" :page-size.sync="pageSize" :total="20" />
</template>
<script>
export default {
name: 'HelloWorld',
props: {
page: {
default: 1
},
limit: {
default: 5
},
},
computed: {
currentPage: {
get() {
return this.page
},
set(val) {
//currentPage 这里对currentPage做出来改变就会走这里
//这边更新数据走这里
console.log('currentPage', this.currentPage)
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
这里的page.sync、limit.sync目的就是为了实现数据的双向绑定,computed中监听page和limit的变化,子组件接收的数据通过computed生成的currentPage通过sync绑定到了 el-pagination中, 点击分页器的时候会改变currentPage 此时会调用set函数设置新的值,通过代码 this.$emit(update:page,value) 更新父组件中的值,实现双向的数据绑定
本文是作者在闲暇的时间随便记录一下, 若有错误请指正,多多包涵。感谢支持!
来源:juejin.cn/post/7312353213347708940
小程序和h5有什么差别
差别
微信小程序和H5应用在实现原理上的差异主要体现在架构、渲染方式、数据通信、运行环境和API接口等方面。以下是详细的对比:
1. 架构和运行环境
微信小程序:
架构:微信小程序主要分为逻辑层(JavaScript)和视图层(WXML、WXSS)。逻辑层运行在小程序的JSCore中,而视图层运行在WebView(它是基于浏览器内核重构的内置解析器,它并不是一个完整的浏览器,官方文档中重点强调了脚本内无法使用浏览器中常用的
window
对象和document
对象,就是没有DOM
和BOM
的相关的API
,这一条就干掉了JQ
和一些依赖于BOM
和DOM
的NPM包)中,两者通过平台提供的桥接机制进行通信。运行环境:逻辑层在微信提供的JS引擎中运行,视图层在微信内置的WebView中渲染。
H5 应用:
架构:H5应用是一个整体。HTML、CSS和JavaScript共同构成了一个Web页面。
运行环境:H5应用在浏览器中运行,所有代码都在浏览器的环境中解析和执行。
2. 渲染方式
微信小程序:
微信小程序采用双线程模型,将逻辑层和视图层分离,分别运行在不同的线程中(两者通过平台提供的桥接机制进行通信):
逻辑层:运行在小程序的JSCore环境中,负责处理业务逻辑、数据计算和API调用。
视图层:运行在WebView中,负责渲染用户界面和处理用户交互。( 性能提升:由于小程序的渲染过程并不依赖于JS,因此即使JS线程发生阻塞,页面的渲染也不会受到影响。这种机制有利于提高渲染效率,减少卡顿,提升用户体验。)
通信桥接机制
逻辑层和视图层之间不能直接访问和操作对方的数据和界面,因此需要通过微信小程序框架提供的桥接机制来进行通信。这种通信机制通常包括以下几个方面:
1. 数据绑定和响应式更新(逻辑层--->视图层)
逻辑层通过数据绑定的方式将数据传递给视图层,视图层根据数据变化自动更新界面。数据绑定的过程如下:
设置数据:逻辑层通过
Page
或Component
实例的setData
方法,将数据传递给视图层。更新视图:视图层接收到数据变化的消息后,根据新的数据重新渲染界面。
2. 事件处理(视图层--->逻辑层)
视图层中的用户交互(如点击、输入等)会触发事件,这些事件通过桥接机制传递给逻辑层进行处理。事件处理的过程如下:
事件绑定:在视图层(WXML)中定义事件处理函数。
事件触发:用户在界面上进行交互时,触发相应的事件。
事件传递:视图层将事件信息通过桥接机制传递给逻辑层。
事件处理:逻辑层的事件处理函数接收到事件信息,执行相应的业务逻辑。
3. 消息传递
逻辑层和视图层之间的通信实际是通过消息传递的方式实现的。微信小程序框架负责在两个层之间传递消息,包括:
逻辑层到视图层的消息:如数据更新、视图更新等。
视图层到逻辑层的消息:如用户交互事件、视图状态变化等
通信桥接机制具体实现
依赖于微信小程序框架内部的设计和优化,开发者无需直接接触底层的通信细节。以下是桥接机制的一些关键点:
消息队列:逻辑层和视图层之间维护一个消息队列,用于存储待传递的消息。
消息格式:消息以JSON格式进行编码,包含消息类型、数据内容等信息。
消息处理:逻辑层和视图层各自维护一个消息处理器,负责接收、解析和处理消息。
异步通信:消息传递通常是异步进行的,以确保界面和逻辑的流畅性和响应性
H5 应用:
H5应用的逻辑层和视图层通常是在同一线程(主线程)中运行,直接通过JavaScript代码操作DOM来更新界面。主要的通信方式包括:
直接DOM操作:通过JavaScript直接操作DOM元素,更新界面。
事件监听和处理:通过JavaScript监听DOM事件(如点击、输入等)并处理。
数据绑定:使用现代前端框架(如Vue.js、React.js)的数据绑定和响应式机制,实现视图的自动更新。
3. 数据通信
微信小程序:
通信机制:逻辑层和视图层之间的通信通过小程序框架提供的机制来实现,通常是通过事件和数据绑定。
后台通信:可以通过小程序提供的API与服务器通信,例如wx.request等。
H5 应用:
通信机制:页面内的通信可以通过DOM事件、JavaScript函数调用等方式实现。
后台通信:可以使用标准的AJAX请求、Fetch API、WebSocket等方式与服务器通信。
4. 运行机制
微信小程序
启动
如果用户已经打开过某小程序,在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,整个过程就是所谓的
热启动
如果用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动,就是
冷启动
销毁
当小程序进入后台一定时间,或系统资源占用过高,或者是你手动销毁,才算真正的销毁
h5:解析HTML CSS形成DOM树和CSSOM树,两者结合形成renderTree,js运行,当然中间存在一系列的阻塞问题,还有同源策略等等
5. 系统权限方面(特定功能)
微信小程序依托于微信平台,能够利用微信提供的特有功能和API,实现许多H5应用无法直接实现或不易实现的功能,如微信支付、微信登录、硬件接口(如摄像头、麦克风、蓝牙、NFC等)、微信特有功能等。
6.更新机制
h5更新后访问地址即可
微信小程序需要审核
开发者在发布新版本之后,无法立刻影响到所有现网用户,要在发布之后 24 小时之内才下发新版本信息到用户
小程序每次
冷启动
时,都会检查有无更新版本,如果发现有新版本,会异步下载新版本代码包,并同时用客户端本地包进行启动,所以新版本的小程序需要等下一次冷启动
才会应用上,当然微信也有wx.getUpdateManager
可以做检查更新
7. 开发工具和调试
微信小程序:
开发工具:微信提供了专门的开发者工具,集成了调试、预览、上传等功能,方便开发者进行开发和测试。
调试:可以使用微信开发者工具进行实时调试,并提供丰富的日志和调试信息。
H5 应用:
开发工具:可以使用任何Web开发工具和IDE(如VS Code、WebStorm等),以及浏览器的开发者工具进行调试。
调试:依赖浏览器的开发者工具(如Chrome DevTools),可以进行断点调试、查看网络请求、分析性能等。
总结来说,微信小程序和H5应用在实现原理上的差异主要是由于它们的架构设计、运行环境和生态系统的不同。小程序依托于微信平台,提供了许多平台专属的优化和功能,而H5应用则更加开放和灵活,依赖于浏览器的标准和特性。
小程序为什么使用双层架构
微信小程序采用双线程架构的原因主要是为了优化性能和用户体验。双线程架构将逻辑层和视图层分离,使得业务逻辑处理和视图渲染在不同的线程中进行,从而提高了小程序的运行效率和响应速度。以下是采用双线程架构的具体原因和优势:
提高性能:
将逻辑处理和页面渲染分离到不同的线程中,可以避免互相干扰,提高整体性能。例如,在复杂的业务逻辑计算过程中,视图层仍然可以保持流畅的界面更新和响应。
逻辑层和视图层通过消息机制进行异步通信,可以避免阻塞和卡顿。这样即使逻辑层的操作较为耗时,也不会影响界面的即时响应。
安全性: 视图层无法直接操作逻辑层的数据和代码,这样可以避免一些潜在的安全风险和漏洞。
XSS
由于逻辑层和视图层分离,视图层不能直接执行逻辑层的JavaScript代码。这种隔离使得即使视图层(WXML)中存在注入的恶意代码,也不能直接影响逻辑层的数据和操作。
逻辑层和视图层之间的通信通过统一的API进行,传递的数据会经过平台的安全检查和过滤,进一步减少了XSS攻击的风险。
CSRF
小程序通过平台的统一API进行请求,这些请求包含了平台自动添加的安全令牌(如
session_key
等),确保请求的合法性。由于逻辑层和视图层的分离,用户在视图层进行操作时,逻辑层的业务逻辑和数据处理经过平台的校验,减少了CSRF攻击的风险。
DOM篡改:视图层的DOM结构由WXML和WXSS定义,不能直接通过逻辑层的JavaScript代码进行操作,这种隔离减少了DOM篡改的可能性。
安全权限管理:小程序的API权限由平台统一管理和控制,开发者需要申请和用户授权后才能使用特定的API。
用户体验: 微信小程序在启动时可以并行加载逻辑层和视图层资源,减少初始加载时间,提升启动速度。同时,微信平台会对小程序进行预加载和缓存优化,进一步提升加载性能。
rpx
微信的自适应单位,可以根据屏幕宽度进行自适应。
在微信小程序中,1 rpx 表示屏幕宽度的 1/750,因此 rpx
和 px
的换算关系是动态的,基于设备的实际屏幕宽度。
作者:let_code
来源:juejin.cn/post/7389168680747614245
展开收起的箭头动画应该怎么做?
背景
我们在日常开发中,一定经常遇到折叠面板的开发,为了美观,我们经常会添加展开收起按钮,并且带有箭头旋转动画。
比如下面的几种情况
- 文字点击变化,且有箭头旋转动画
- 只有箭头动画
这几种情况的核心其实就是:点击箭头开始旋转,再点击箭头恢复初始位置。
如何实现
思路分析
要实现展开和收起箭头的旋转动画,我们可以使用 CSS 和 JavaScript。我们在点击按钮时,通过添加和移除 CSS 类,实现箭头的旋转动画。并且添加transition属性实现过渡效果。
代码实现
我们以第一种动画效果为例,先写基础代码
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span>▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
现在我们点击按钮,只有文字会变化,箭头不会旋转
我们给按钮加一个动态类
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span :class="{ rotate: open }">▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
<style scoped>
.rotate {
transform: rotate(180deg);
transition: transform 0.3s linear;
}
</style>
可以看到,展开的时候有动画,但是收起的时候是没有过渡效果的。
我们只需要加一个transition属性即可
<template>
<div @click="open = !open">
<span>{{ open ? '收起' : '展开' }}</span>
<span :class="{ rotate: open }" class="arrow">▼</span>
</div>
</template>
<script>
const open = ref(false)
</script>
<style scoped>
.arrow {
transition: transform 0.3s linear;
}
.rotate {
transform: rotate(180deg);
transition: transform 0.3s linear;
}
</style>
现在样式就ok了
html版本
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Arrow Rotation</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<button id="toggleButton">
<span id="arrow" class="arrow">▼</span>
</button>
</div>
<script src="script.js"></script>
</body>
</html>
css
/* styles.css */
.container {
text-align: center;
margin-top: 50px;
}
#toggleButton {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
outline: none;
}
.arrow {
display: inline-block;
transition: transform 0.3s ease;
}
.arrow.rotate {
transform: rotate(180deg);
}
js
// script.js
document.getElementById('toggleButton').addEventListener('click', function() {
const arrow = document.getElementById('arrow');
arrow.classList.toggle('rotate');
});
这种方式可以实现箭头在点击时的旋转动画效果。在实际项目中使用,我们也可以根据具体需求调整样式和逻辑。
来源:juejin.cn/post/7385132403025149989
如果iconfont停止服务了,我们怎么办
前言
个人一直都比较喜欢阿里大佬提供的一些组件服务什么的,这里就只说图标管理吧,最开始的时候我是使用的icomoon.io 后面发现http://www.iconfont.cn/ 在以前开发是时候我们为了图片的请求少点,提升网站的性能会把这些小图标做到一张图上面专业叫法是雪碧图,后来随着前端发展有了字体图标,不啰嗦了吧,回到正题,由于上次icofont官网挂了之后,导致我的项目无法进行正常迭代,我就决定要自己弄个简单的图标管理。
需求
一般使用需要的是把设计做好的图标或者其他地方下载的svg图标上传到服务器然后可以查看,并且打包成一个js文件加载到项目中。
准备
都说天下文章一大抄,我也是去抄iconfont,我把iconfont的使用demo打开研究了一下。
我这里只说Symbol,通过分析看到就是我们需要在项目中引入./iconfont.js
iconfont把我们上传的图标进行了删减优化,最外层就一个svg标签里面使用了一个symbol标签来包裹之前的图标内容,每一个symbol的id都对应之前的svg图标名称。
iconfont也没有开源,具体怎么做的咱也不知道,只能知道目前这些信息了,然后就按照自己理解开始开发实现了
前端开发
通过上面我们可以知道,现在我们前端需要的是把设计稿的svg图变成symbol里面的内容,打开svg图 可以简单测试下,直接把内容复制然后粘贴带了iconfont.js中,然后运行demo图标正常显示就说明是ok的,不然就是这样做是不行的。
通过图片我们可以看到,现在需要的就是把svg里面的path改成用symblo来包裹,在开发中svg图我们是通过文档流上传上来的,这个时候我们就需要对文档流进行操作,先把文档流变成字符串,然后js操作字符串拼接达到目的。
- 使用到FileReader和readAsText获取到字符串
const fileParse = (file) => {
return new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsText(file, "UTF-8");
fileReader.onload = (e) => {
resolve({ content: e.target?.result, name: file.name })
}
})
}
- 字符操作拼接我使用的是cheerio
const handleUploadSvg = ($, result) => {
let index = result.indexOf('
通过上面的操作我们可以得到一个新的svg源码,图标显示还是flow
然后我把这个字符串传送给后端就行了
后端开发
后端要做的就是把我上传上来的svg字符串拼接到一起,然后以iconfont.js一样通过一个get接口返回给我就可以了, 通过浏览器可以访问到就说明ok了
其他两种需要用到些第三方库当时也有顺便研究看到了,如果有需要我再写一篇。
文章中会有些不怎么清晰或者有问题的地方还望各位大佬见谅,刚刚开始决定写一些技术相关文章,还很生疏
来源:juejin.cn/post/7340197367515578378
谁说forEach不支持异步代码,只是你拿不到异步结果而已
在前面探讨 forEach 中异步请求后端接口时,很多人都知道 forEach 中 async/await 实际是无效的,很多文章也说:forEach 不支持异步,forEach 只能同步运行代码,forEach 会忽略 await 直接进行下一次循环...
当时我的理解也是这样的,后面一细想好像不对,直接上我前面一篇文章用到的示例代码:
async function getData() {
const list = await $getListData()
// 遍历请求
list.forEach(async (item) => {
const res = await $getExtraInfo({
id: item.id
})
item.extraInfo = res.extraInfo
})
// 打印下最终处理过的额外数据
console.log(list)
}
上面 $getListData、$getExtraInfo 都是 promise 异步方法,按照上面说的 forEach 会直接忽略掉 await,那么循环体内部拿到的 res 就应该是 undefined,后面的 res.extraInfo 应该报错才对,但是实际上代码并没有报错,说明 await 是有效的,内部的异步代码也是可以正常运行的,所以 forEach 肯定是支持异步代码的。
手写版 forEach
先从自己实现的简版 forEach 看起:
Array.prototype.customForEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}
里面会为数组的每个元素执行一下回调函数,实际拿几组数组测试和正宗的 forEach 方法效果也一样。可能很多人还是会有疑问你自己实现这到底靠不靠谱,不瞒你说我也有这样的疑问。
MDN 上关于 forEach 的说明
先去 MDN 上搜一下 forEach,里面的大部分内容只是使用层面的文档,不过里面有提到:“forEach() 期望的是一个同步函数,它不会等待 Promise 兑现。在使用 Promise(或异步函数)作为 forEach 回调时,请确保你意识到这一点可能带来的影响”。
ECMAScript 中 forEach 规范
继续去往 javascript 底层探究,我们都知道执行 js 代码是需要依靠 js 引擎,去将我们写的代码解释翻译成计算机能理解的机器码才能执行的,所有 js 引擎都需要参照 ECMAScript 规范来具体实现,所以这里我们先去看下 ECMAScript 上关于 forEach 的标准规范:
添加图片注释,不超过 140 字(可选)
谷歌 V8 的 forEach 实现
常见的 js 引擎有:谷歌的 V8、火狐 FireFox 的 SpiderMonkey、苹果 Safari 的 JavaScriptCore、微软 Edge 的 ChakraCore...后台都很硬,这里我们就选其中最厉害的谷歌浏览器和 nodejs 依赖的 V8 引擎,V8 中对于 forEach 实现的主要源码:
transitioning macro FastArrayForEach(implicit context: Context)(
o: JSReceiver, len: Number, callbackfn: Callable, thisArg: JSAny): JSAny
labels Bailout(Smi) {
let k: Smi = 0;
const smiLen = Cast<Smi>(len) otherwise goto Bailout(k);
const fastO = Cast<FastJSArray>(o) otherwise goto Bailout(k);
let fastOW = NewFastJSArrayWitness(fastO);
// Build a fast loop over the smi array.
for (; k < smiLen; k++) {
fastOW.Recheck() otherwise goto Bailout(k);
// Ensure that we haven't walked beyond a possibly updated length.
if (k >= fastOW.Get().length) goto Bailout(k);
const value: JSAny = fastOW.LoadElementNoHole(k)
otherwise continue;
Call(context, callbackfn, thisArg, value, k, fastOW.Get());
}
return Undefined;
}
源码是 .tq 文件,这是 V8 团队开发的一个叫 Torque 的语言,语法类似 TypeScript,所以对于前端程序员上面的代码大概也能看懂,想要了解详细的 Torque 语法,可以直接去 V8 的官网上查看。
从上面的源码可以看到 forEach 实际还是依赖的 for 循环,没有返回值所以最后 return 的一个 Undefined。看完源码是不是发现咱上面的手写版也大差不差,只不过 V8 里实现了更多细节的处理。
结论:forEach 支持异步代码
最后的结论就是:forEach 其实是支持异步的,循环时并不是会直接忽略掉 await,但是因为 forEach 没有返回值,所以我们在外部没有办法拿到每次回调执行过后的异步 promise,也就没有办法在后续的代码中去处理或者获取异步结果了,改造一下最初的示例代码:
async function getData() {
const list = await $getListData()
// 遍历请求
list.forEach(async (item) => {
const res = await $getExtraInfo({
id: item.id
})
item.extraInfo = res.extraInfo
})
// 打印下最终处理过的额外数据
console.log(list)
setTimeout(() => {
console.log(list)
}, 1000 * 10)
}
你会发现 10 秒后定时器中是可以按照预期打印出我们想要的结果的,所以异步代码是生效了的,只不过在同步代码中我们没有办法获取到循环体内部的异步状态。
如果还是不能理解,我们对比下 map 方法,map 和 forEach 很类似,但是 map 是有返回值的,每次遍历结束之后我们是可以直接 return 一个值,后续我们就可以接收到这个返回值。这也是为什么很多文章中改写 forEach 异步操作时,使用 map 然后借助 Promise.all 来等待所有异步操作完成后,再进行下面的逻辑来实现同步的效果。
参考文档
- MDN forEach 文档:developer.mozilla.org/zh-CN/docs/…
- ECMAScript 中 forEach 规范:tc39.es/ecma262/#se…
- 谷歌 V8 中 forEach 源码:chromium.googlesource.com/v8/v8.git/+…
- 谷歌 V8 中 map 源码:chromium.googlesource.com/v8/v8.git/+…
- 谷歌 V8 官网:v8.dev
- 谷歌 V8 源码:github.com/v8/v8
来源:juejin.cn/post/7389912354749087755
js如何实现当文本内容过长时,中间显示省略号...,两端正常展示
前一阵做需求时,有个小功能实现起来废了点脑细胞,觉得可以记录一下。
产品的具体诉求是:用户点击按钮进入详情页面,详情页内的卡片标题内容过长时,标题的前后两端正常展示,中间用省略号...表示,并且鼠标悬浮后,展示全部内容。
关于鼠标悬浮展示全部内容的代码就不放在这里了,本文主要写关于实现中间省略号...的代码。
实现思路
- 获取标题盒子的真实宽度, 我这里用的是clientWidth;
- 获取文本内容所占的实际宽度;
- 根据文字的大小计算出每个文字所占的宽度;
- 判断文本内容的实际宽度是否超出了标题盒子的宽度;
- 通过文字所占的宽度累加之和与标题盒子的宽度做对比,计算出要截取位置的索引;
- 同理,文本尾部的内容需要翻转一下,然后计算索引,截取完之后再翻转回来;
代码
html代码
<div class="title" id="test">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</div>
css代码: 设置文本不换行,同时设置overflow:hidden
让文本溢出盒子隐藏
.title {
width: 640px;
height: 40px;
line-height: 40px;
font-size: 14px;
color: #00b388;
border: 1px solid #ddd;
overflow: hidden;
/* text-overflow: ellipsis; */
white-space: nowrap;
/* box-sizing: border-box; */
padding: 0 10px;
}
javascript代码:
获取标题盒子的宽度时要注意,如果在css样式代码中设置了padding, 就需要获取标题盒子的左右padding值。 通过getComputedStyle
属性获取到所有的css样式属性对应的值, 由于获取的padding值都是带具体像素单位的,比如: px
,可以用parseInt特殊处理一下。
获取盒子的宽度的代码,我当时开发时是用canvas计算的,但计算的效果不太理想,后来逛社区,发现了嘉琪coder
大佬分享的文章,我这里就直接把代码搬过来用吧, 想了解的掘友可以直接滑到文章末尾查看。
判断文本内容是否超出标题盒子
// 标题盒子dom
const dom = document.getElementById('test');
// 获取dom元素的padding值
function getPadding(el) {
const domCss = window.getComputedStyle(el, null);
const pl = Number.parseInt(domCss.paddingLeft, 10) || 0;
const pr = Number.parseInt(domCss.paddingRight, 10) || 0;
console.log('padding-left:', pl, 'padding-right:', pr);
return {
left: pl,
right: pr
}
}
// 检测dom元素的宽度,
function checkLength(dom) {
// 创建一个 Range 对象
const range = document.createRange();
// 设置选中文本的起始和结束位置
range.setStart(dom, 0),
range.setEnd(dom, dom.childNodes.length);
// 获取元素在文档中的位置和大小信息,这里直接获取的元素的宽度
let rangeWidth = range.getBoundingClientRect().width;
// 获取的宽度一般都会有多位小数点,判断如果小于0.001的就直接舍掉
const offsetWidth = rangeWidth - Math.floor(rangeWidth);
if (offsetWidth < 0.001) {
rangeWidth = Math.floor(rangeWidth);
}
// 获取元素padding值
const { left, right } = getPadding(dom);
const paddingWidth = left + right;
// status:文本内容是否超出标题盒子;
// width: 标题盒子真实能够容纳文本内容的宽度
return {
status: paddingWidth + rangeWidth > dom.clientWidth,
width: dom.clientWidth - paddingWidth
};
}
通过charCodeAt返回指定位置的字符的Unicode
编码, 返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。
截取和计算文本长度
// 计算文本长度,当长度之和大于等于dom元素的宽度后,返回当前文字所在的索引,截取时会用到。
function calcTextLength(text, width) {
let realLength = 0;
let index = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2 * 14; // 14是字体大小
}
// 判断长度,为true时终止循环,记录索引并返回
if (realLength >= width) {
index = i;
break;
}
}
return index;
}
// 设置文本内容
function setTextContent(text) {
const { status, width } = checkLength(dom);
let str = '';
if (status) {
// 翻转文本
let reverseStr = text.split('').reverse().join('');
// 计算左右两边文本要截取的字符索引
const leftTextIndex = calcTextLength(text, width);
const rightTextIndex = calcTextLength(reverseStr, width);
// 将右侧字符先截取,后翻转
reverseStr = reverseStr.substring(0, rightTextIndex);
reverseStr = reverseStr.split('').reverse().join('');
// 字符拼接
str = `${text.substring(0, leftTextIndex)}...${reverseStr}`;
} else {
str = text;
}
dom.innerHTML = str;
}
最终实现的效果如下:
上面就是此功能的所有代码了,如果想要在本地试验的话,可以在本地新建一个html文件,复制上面代码就可以了。
下面记录下从社区内学到的相关知识:
- js判断文字被溢出隐藏的几种方法;
- JS获取字符串长度的几种常用方法,汉字算两个字节;
1、 js判断文字被溢出隐藏的几种方法
1. Element-plus这个UI框架中的表格组件实现的方案。
通过document.createRange
和document.getBoundingClientRect()
这两个方法实现的。也就是我上面代码中实现的checkLength
方法。
2. 创建一个隐藏的div模拟实际宽度
通过创建一个不会在页面显示出来的dom元素,然后把文本内容设置进去,真实的文本长度与标题盒子比较宽度,判断是否被溢出隐藏了。
function getDomDivWidth(dom) {
const elementWidth = dom.clientWidth;
const tempElement = document.createElement('div');
const style = window.getComputedStyle(dom, null)
const { left, right } = getPadding(dom); // 这里我写的有点重复了,可以优化
tempElement.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
white-space: nowrap;
padding-left:${style.paddingLeft};
padding-right:${style.paddingRight};
font-size: ${style.fontSize};
font-family: ${style.fontFamily};
font-weight: ${style.fontWeight};
letter-spacing: ${style.letterSpacing};
`;
tempElement.textContent = dom.textContent;
document.body.appendChild(tempElement);
const obj = {
status: tempElement.clientWidth + right + left > elementWidth,
width: elementWidth - left - right
}
document.body.removeChild(tempElement);
return obj;
}
3. 创建一个block元素来包裹inline元素
这种方法是在UI框架acro design vue
中实现的。外层套一个块级(block)元素,内部是一个行内(inline)元素。给外层元素设置溢出隐藏的样式属性,不对内层元素做处理,这样内层元素的宽度是不变的。因此,通过获取内层元素的宽度和外层元素的宽度作比较,就可以判断出文本是否被溢出隐藏了。
// html代码
<div class="title" id="test">
<span class="content">近日,银行纷纷下调大额存单利率,但银行定期存款仍被疯抢。银行理财经理表示:有意向购买定期存款要尽快,不确定利率是否会再降。</span>
</div>
// 创建一个block元素来包裹inline元素
const content = document.querySelector('.content');
function getBlockDomWidth(dom) {
const { left, right } = getPadding(dom);
console.log(dom.clientWidth, content.clientWidth)
const obj = {
status: dom.clientWidth < content.clientWidth + left + right,
width: dom.clientWidth - left - right
}
return obj;
}
4. 使用canvas中的measureText方法和TextMetrics对象来获取元素的宽度
通过Canvas 2D渲染上下文(context)可以调用measureText方法,此方法会返回TextMetrics对象,该对象的width
属性值就是字符占据的宽度,由此也能获取到文本的真实宽度,此方法有弊端,比如说兼容性,精确度等等。
// 获取文本长度
function getTextWidth(text, font = 14) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")
context.font = font
const metrics = context.measureText(text);
return metrics.width
}
5. 使用css实现
这种方式来自评论区的掘友@S_mosar
提供的思路。
先来看下效果:
代码如下:
css部分
.con {
font-size: 14px;
color: #666;
width: 600px;
margin: 50px auto;
border-radius: 8px;
padding: 15px;
overflow: hidden;
resize: horizontal;
box-shadow: 20px 20px 60px #bebebe, -20px -20px 60px #ffffff;
}
.wrap {
position: relative;
line-height: 2;
height: 2em;
padding: 0 10px;
overflow: hidden;
background: #fff;
margin: 5px 0;
}
.wrap:nth-child(odd) {
background: #f5f5f5;
}
.title {
display: block;
position: relative;
background: inherit;
text-align: justify;
height: 2em;
overflow: hidden;
top: -4em;
}
.txt {
display: block;
max-height: 4em;
}
.title::before{
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
html部分
<ul class="con">
<li class="wrap">
<span class="txt">CSS 实现优惠券的技巧 - 2021-03-26</span>
<span class="title" title="CSS 实现优惠券的技巧 - 2021-03-26">CSS 实现优惠券的技巧 - 2021-03-26</span>
</li>
<li class="wrap">
<span class="txt">CSS 测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26</span>
<span class="title" title="CSS 测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26">CSS
测试标题,这是一个稍微有点长的标题,超出一行以后才会有title提示,标题是 实现优惠券的技巧 - 2021-03-26</span>
</li>
<li class="wrap">
<span class="txt">CSS 拖拽?</span>
<span class="title" title="CSS 拖拽?">CSS 拖拽?</span>
</li>
<li class="wrap">
<span class="txt">CSS 文本超出自动显示title</span>
<span class="title" title="CSS 文本超出自动显示title">CSS 文本超出自动显示title</span>
</li>
</ul>
思路解析:
- 文字内容的父级标签li设置
line-height: 2;
、overflow: hidden;
、height: 2em;
,因此 li 标签的高度是当前元素字体大小的2倍,行高也是当前字体大小的2倍,同时内容若溢出则隐藏。 - li 标签内部有两个 span 标签,二者的作用分别是:类名为
.txt
的标签用来展示不需要省略号时的文本,类名为.title
用来展示需要省略号时的文本,具体是如何实现的请看第五步。 - 给
.title
设置伪类before
,将伪类宽度设置为50%,搭配浮动float: right;
,使得伪类文本内容靠右,这样设置后,.title
和伪类就会各占父级宽度的一半了。 .title
标签设置text-align: justify;
,用来将文本内容和伪类的内容两端对齐。- 给伪类
before
设置文字对齐方式direction: rtl;
,将伪类内的文本从右向左流动,即right to left
,再设置溢出省略的css样式就可以了。 .title
标签设置了top: -4em
,.txt
标签设置max-height: 4em;
这样保证.title
永远都在.txt
上面,当内容足够长,.txt
文本内容会换行,导致高度从默认2em变为4em,而.title
位置是-4em
,此时正好将.txt
覆盖掉,此时显示的就是.title
标签的内容了。
知识点:text-align: justify;
- 文本的两端(左边和右边)都会与容器的边缘对齐。
- 为了实现这种对齐,浏览器会在单词之间添加额外的空间。这通常意味着某些单词之间的间距会比其他单词之间的间距稍大一些。
- 如果最后一行只有一个单词或少数几个单词,那么这些单词通常不会展开以填充整行,而是保持左对齐。
需要注意的是,
text-align: justify;
主要用于多行文本。对于单行文本,这个值的效果与text-align: left;
相同,因为单行文本无法两端对齐。
2、JS获取字符串长度的几种常用方法
1. 通过charCodeAt判断字符编码
通过charCodeAt获取指定位置字符的Unicode
编码,返回的值对应ASCII码表对应的值,0-127包含了常用的英文、数字、符号等,这些都是占一个字节长度的字符,而大于127的为占两个字节长度的字符。
function calcTextLength(text) {
let realLength = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
realLength += 1;
} else {
realLength += 2;
}
}
return realLength;
}
2. 采取将双字节字符替换成"aa"的做法,取长度
function getTextWidth(text) {
return text.replace(/[^\x00-\xff]/g,"aa").length;
};
参考文章
4. canvas绘制字体偏上不居中问题、文字垂直居中后偏上问题、measureText方法和TextMetrics对象
来源:juejin.cn/post/7329967013923962895
只会Vue的我,一入职就让用React,用了这个工具库,我依然高效
由于公司最近项目周期紧张,还有一个项目因为人手不够排不开,时间非常紧张,所以决定招一个人来。这不,经过一段时间紧张的招聘,终于招到了一个前端妹子。妹子也坦白过,自己干了3年,都是使用的Vue开发,自己挺高效的。但如果入职想用React的话,会稍微费点劲儿。我说,没事,来就是了,我们都可以教你的。
但入职后发现,这个妹子人家一点也不拖拉,干活很高效。单独分给她的项目,她比我们几个干的还快,每天下班准时就走了,任务按时完成。终于到了分享会了,组长让妹子准备准备,分享一下高效开发的秘诀。
1 初始化React项目
没想到妹子做事还挺认真,分享并没有准备个PPT什么的,而是直接拿着电脑,要给我们手动演示她的高效秘诀。而且是从初始化React项目开发的,这让我们很欣慰。
首先是初始化React项目的命令,这个相信大家都很熟悉了:
第一步:启动终端
第二步:npm install -g create-react-app
第三步:create-react-app js-tool-big-box-website
(注意:js-tool-big-box-website是我们要创建的那个项目名称)
第四步:cd js-tool-big-box-website
(注意:将目录切换到js-tool-big-box-website项目下)
第五步:npm start
然后启动成功后,可以看到这样的界面:
2 开始分享秘诀
妹子说,自己不管使用Vue,还是React,高效开发的秘诀就是 js-tool-big-box 这个前端JS库
首先需要安装一下: npm install js-tool-big-box
2.1 注册 - 邮箱和手机号验证
注册的时候,需要验证邮箱或者手机号,妹子问我们,大家平时怎么验证?我们说:不是有公共的正则验证呢,就是验证一下手机号和邮箱的格式呗,你应该在utils里加了公共方法了吧?或者是加到了表单验证里?
妹子摇摇头,说,用了js-tool-big-box工具库后,会省事很多,可以这样:
import logo from './logo.svg';
import './App.css';
import { matchBox } from 'js-tool-big-box';
function App() {
const email1 = '232322@qq.com';
const email2 = '232322qq.ff';
const emailResult1 = matchBox.email(email1);
const emailResult2 = matchBox.email(email2);
console.log('emailResult1验证结果:', emailResult1); // true
console.log('emailResult2验证结果:', emailResult2); // false
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
js-tool-big-box,使React开发更加高效
</header>
</div>
);
}
export default App;
2.2 验证密码强度值
验证密码强度值的时候呢,妹子问我们,大家平时怎么验证?我们说:不就是写个公共方法,判断必须大于几位,里面是否包含数字,字母,大写字母,特殊符号这样子吗?
妹子摇摇头,说,不是,我们可以这样来验证:
const pwd1 = '12345';
const pwd1Strength = matchBox.checkPasswordStrength(pwd1);
console.log('12345的密码强度值为:', pwd1Strength); // 0
const pwd2 = '123456';
const pwd2Strength = matchBox.checkPasswordStrength(pwd2);
console.log('123456的密码强度值为:', pwd2Strength); // 1
const pwd3 = '123456qwe';
const pwd3Strength = matchBox.checkPasswordStrength(pwd3);
console.log('123456qwe的密码强度值为:', pwd3Strength); // 2
const pwd4 = '123456qweABC';
const pwd4Strength = matchBox.checkPasswordStrength(pwd4);
console.log('123456qweABC的密码强度值为:', pwd4Strength); // 3
const pwd5 = '123@456qwe=ABC';
const pwd5Strength = matchBox.checkPasswordStrength(pwd5);
console.log('123@456qwe=ABC的密码强度值为:', pwd5Strength); // 4
2.3 登录后存localStorage
登录后,需要将一些用户名存到localStorage里,妹子问,我们平时怎么存?我们说:就是直接拿到服务端数据后,存呗。妹子问:你们加过期时间不?我们说:有时候需要加。写个公共方法,传入key值,传入value值,传个过期时间,大家不都是这样?
妹子摇摇头,说,不是,我们可以这样来存:
import { storeBox } from 'js-tool-big-box';
storeBox.setLocalstorage('today', '星期一', 1000*6);
2.4 需要判断是否手机端浏览器
我们市场需要判断浏览器是否是手机端H5浏览器的时候,大家都怎么做?我们说:就是用一些内核判断一下呗,写好方法,然后在展示之处判断一下,展示哪些组件?不是这样子吗?
妹子又问:我这个需求,老板比较重视微信内置的浏览器,这样大家写的方法是不是就比较多了?我们说,那再写方法,针对微信内置浏览器的内核做一下判断呗。
妹子摇摇头,说,那样得写多少方法啊,可以用这个方法,很全面的:
如果你单纯的只是想判断一下是否是手机端浏览器,可以这样:
import { browserBox } from 'js-tool-big-box';
const checkBrowser = browserBox.isMobileBrowser();
console.log('当前是手机端浏览器吗?', checkBrowser);
如果你需要更详细的,根据内核做一些判断,可以这样:
const info = browserBox.getBrowserInfo();
console.log('=-=-=', info);
这个getBrowserInfo方法,可以获取更详细的ua,浏览器名字,以及浏览器版本号
2.5 日期转换
妹子问,大家日常日期转换怎么做?如果服务端给的是一个时间戳的话?我们说:不就是引入一个js库,然后就开始使用呗?
妹子问:这次产品的要求是,年月日中间不是横岗,也不是冒号,竟然要求我显示这个符号 “~” ,也不是咋想的?然后我们问:你是不是获取了年月日,然后把年月日中间拼接上了这个符号呢?
妹子摇摇头,说,你可以这样:
import { timeBox } from 'js-tool-big-box';
const dateTime2 = timeBox.getFullDateTime(1719220131000, 'YYYY-MM-DD', '~');
console.log(dateTime2); // 2024~06~24
2.6 获取数据的详细类型
妹子问,大家日常获取数据的类型怎么获取?我们说,typeof呀,instanceof呀,或者是
Object.prototype.toString.call 一下呗,
妹子摇摇头,说,你可以这样:
import { dataBox } from 'js-tool-big-box';
const numValue = 42;
console.log('42的具体数据类型:', dataBox.getDataType(numValue)); // [object Number]
const strValue = 'hello';
console.log('hello的具体数据类型:', dataBox.getDataType(strValue)); // [object String]
const booleanValue = true;
console.log('true的具体数据类型:', dataBox.getDataType(booleanValue)); // [object Boolean]
const undefinedValue = undefined;
console.log('undefined的具体数据类型:', dataBox.getDataType(undefinedValue)); // [object Undefined]
const nullValue = null;
console.log('null的具体数据类型:', dataBox.getDataType(nullValue)); // [object Null]
const objValue = {};
console.log('{}的具体数据类型:', dataBox.getDataType(objValue)); // [object Object]
const arrayValue = [];
console.log('[]的具体数据类型:', dataBox.getDataType(arrayValue)); // [object Array]
const functionValue = function(){};
console.log('function的具体数据类型:', dataBox.getDataType(functionValue)); // [object Function]
const dateValue = new Date();
console.log('date的具体数据类型:', dataBox.getDataType(dateValue)); // [object Date]
const regExpValue = /regex/;
console.log('regex的具体数据类型:', dataBox.getDataType(regExpValue)); // [object RegExp]
2.8 更多
估计妹子也是摇头摇的有点累了,后来演示的就快起来了,我后来也没听得太仔细,大概有,
比如我们做懒加载的时候,判断某个元素是否在可视范围内;
比如判断浏览器向上滚动还是向下滚动,距离底部和顶部的距离;
比如某个页面,需要根据列表下载一个excel文件啦;
比如生成一个UUID啦;
比如后面还有将小写数字转为大写中文啦,等等等等
3 最后
分享完了第二天,妹子就没来,我们还准备请教她具体js-tool-big-box的使用心得呢。据说是第一天分享的时候,摇头摇得把脖子扭到了,希望妹子能早日康复,早点来上班。
最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)
js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)
来源:juejin.cn/post/7383650248265465867
绑定大量的的v-model,导致页面卡顿的解决方案
绑定大量的的v-model,导致页面卡顿的解决方案
设计图如下:
页面布局看着很简单使用element组件,那就完蛋了,因为是大量的数据双向绑定,所以使用组件,延迟非常高,高到什么程度,请求100条数据到渲染到页面上,要10-12s,特别是下拉选择的时候,延迟都在2-3s,人麻了老铁!!!
卡顿的原因很长一段时间都是在绑定v-model,为什么绑定v-model会很卡呢,请求到的每一条数据有14个数据需要绑定v-model,每次一请求就是100个打底,那就是1400个数据需要绑定v-model;而且组件本身也有延迟,所以这个方案不能采用,那怎么做呢?
我尝试采用原生去写,写着写着,哎解决了!!!惊呆了
做完后100条数据页面渲染不超过2s,毕竟还是需要绑定v-model,能在2s内,我还是能接受的吧;选择和输入延迟基本没有
下面就来展示一下我的代码,写的不好看着玩儿就好了:
请求到的数据:
methods这两个事件做的什么事儿呢,就是手动将数据绑定到数据上去也就是row上如图:
当然还有很多解决方案
来源:juejin.cn/post/7392248233222881316
无框架,跨框架!时隔两年,哈啰Quark Design迎来重大特性升级!
引言
历经1年多迭代,Quarkd 2.0 版本正式发布,这是自 Quarkd 开源以来第二个重大版本。本次升级主要实现了组件外部可以穿透影子Dom,修改组件内部元素的任何样式。
- (迁移后)最新官网:quark-ecosystem.github.io/quarkd-docs
- Github 地址:github.com/hellof2e/qu…
Quark Design 介绍
Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架/无框架中。
前端各类框架技术发展多年,很多公司存量前端项目中必定存在各类技术栈。为了解决各类不同技术栈下UI交互统一,我们开发了这套UI组件库。
之前技术瓶颈
熟悉 quarkd 的开发者都知道其底层基因是 Web Components,从而实现了跨技术栈使用。但Web Components 中的 shadow dom 特性决定了其“孤岛”的特性,组件内部是个独立于外部的小世界,外部无法修改组件内部样式,若要修改内部样式,我们在 quarkd 1.x 版本中采用了 CSS 变量的方式来支援这种做法。
但这种做法依旧局限性非常大,你只能修改预设css变量的指定样式,比如你要修改 Dialog 内容中的字体大小/颜色:

// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
// 内部css源码
:host .quark-dialog-content {
font-size: var(--dialog-content-font-size, 14px);
color: var(--dialog-content-color, "#5A6066");
// ... 其它样式
}
这时候,你需要在组件外部书写:
.dialog {
--dialog-content-font-size: 36px;
--dialog-content-color: red;
}
这种做法会带来一些问题,比如当源码中没有指定的css变量,就意味着你无法通过css变量从外面渗透进入组件内部去修改,比如 dialog conent 内的 font-style
。
升级后
得益于 ::part
CSS 伪元素的特性, 我们将 Quarkd 主要 dom 节点进行改造,升级后,你可以通过如下方式来自定义任何组件样式。
custom-element::part(foo) {
/* 样式作用于 `foo` 部分 */
}
::part
可以用来表示在阴影树中任何匹配 part
属性的元素。
该特性已兼容主流浏览器,详情见:mozilla.org # ::part()
用法示例:
// 使用组件
<quark-dialog class=“dialog” content="生命远不止连轴转和忙到极限,人类的体验远比这辽阔、丰富得多。"></quark-dialog>
.dialog::part(body) {
font-size: 24px;
color: #666;
}
.dialog::part(footer) {
font-size: 14px;
color: #333;
}
其它DEMO地址:stackblitz.com/edit/quarkd…
关于升级
Quarkd 2.x 向下兼容所有 1.x 功能及特性,之前的css变量也被保留,所以使用者可以从1.x直接升级到2.x!
One more thing
假如你也想利用 quarkd 底层能力构建属于自己的跨技术栈组件,欢迎使用:
github.com/hellof2e/qu…
最后
感谢在Quarkd迭代期间作出贡献的朋友们,感谢所有使用quarkd的开发者!
来源:juejin.cn/post/7391753478123864091
zero-privacy——uniapp小程序隐私协议弹窗组件
一. 引言
为规范开发者的用户个人信息处理行为,保障用户的合法权益,自2023年9月15日起,对于涉及处理用户个人信息的小程序开发者,微信要求,仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则后,方可调用微信提供的隐私接口。
公告地址:关于小程序隐私保护指引设置的公告
developers.weixin.qq.com/miniprogram…
接下来我们将打造一个保姆级的隐私协议弹窗组件
二. 开发调试基础
划重点,看文档,别说为什么没有效果,没有弹窗
1. 更新用户隐私保护指引
小程序管理员或开发者可以根据具体小程序涉及到的隐私相关接口来更新微信小程序后台的用户隐私保护指引,更新并审核通过后就可以进行相关的开发调试工作。仅有在指引中声明所处理的用户信息,才可以调用平台提供的对应接口或组件。若未声明,对应接口或组件将直接禁用。
- ���知道怎么填写隐私协议,看看文档:用户隐私保护指引设置developers.weixin.qq.com/miniprogram…
- 哪些api需要用户点击同意隐私协议才可以使用的看这里:小程序用户隐私保护指引内容介绍developers.weixin.qq.com/miniprogram…
审核时间有人说十几分钟,我自己的给大家参考一下。
审核通过!审核通过!审核通过后才可以开发调试。
2.配置调试字段 "__usePrivacyCheck__": true
- 在 2023 年 9 月 15 号之前,在 app.json 中配置
"__usePrivacyCheck__": true
后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。 - 在 2023 年 9 月 15 号之后,不论 app.json 中是否有配置 usePrivacyCheck,隐私相关功能都会启用。
- 所以在基于uni-app开发时,我们在 2023 年 9 月 15 号之前进行相关开发调试则需要在manifest.json文件mp-weixin中添加
"__usePrivacyCheck__": true
- manifest.json文件源码视图
"mp-weixin" : {
"__usePrivacyCheck__": true
},
3. 配置微信开发工具基础库
将调试基础库改为3.0.0以上。具体路径为:
微信开发者工具->详情->本地设置->调试基础库
以上配置完成后,即可看看效果,我在小程序后台设置了剪切板的隐私接口,果然,已经提示没有隐私授权不能使用了。
三. zero-privacy组件介绍
组件下载地址:ext.dcloud.net.cn/plugin?name…
组件的功能和特点
- 支持 居中弹出,底部弹出
- 不依赖第三方弹窗组件,内置轻量动画效果
- 支持自定义触发条件
- 支持自定义主题色
- 组件中最重要的4个api(只需用到前3个):
- wx.getPrivacySetting 查询隐私授权情况 官方链接
- wx.onNeedPrivacyAuthorization 监听隐私接口需要用户授权事件。 官方链接
- wx.openPrivacyContract 跳转至隐私协议页面 官方链接
- wx.requirePrivacyAuthorize 模拟隐私接口调用,并触发隐私弹窗逻辑 官方链接
四. zero-privacy组件使用方法
在uniapp插件市场直接下载导入 uni_modules
后使用即可
- 最直接看到弹窗效果的测试方法
<template>
<view class="container">
<zero-privacy :onNeed='false'></zero-privacy>
</view>
</template>
注意以上是测试方案,不建议实际开发中按上面的方法使用,推荐以下两种方法
- 在小程序首页等tabbar页面直接处理隐私弹窗逻辑
<template>
<view class="container">
<zero-privacy :onNeed='false' :hideTabBar='true'></zero-privacy>
</view>
</template>
- 在页面点击某些需要用到隐私协议后处理隐私弹窗逻辑
<template>
<view class="container">
<view class="btn" @click="handleCopy">
复制
</view>
<zero-privacy></zero-privacy>
</view>
</template>
- 自定义内容使用
<template>
<view class="container">
<zero-privacy title="测试自定义标题" predesc="协议前内容" privacy-contract-name-custom="<自定义名称及括号>" subdesc="协议后内容协议后内容协议后内容. 主动换行"></zero-privacy>
</view>
</template>
五. zero-privacy组件参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
position | String | center | 可选 bottom ,从底部弹出 |
color | String | #0396FF | 主颜色: 协议名和同意按钮的背景色 |
bgcolor | String | #ffffff | 弹窗背景色 |
onNeed | Boolean | true | 使用到隐私相关api时触发弹窗,设置为false时初始化弹窗将判断是否需要隐私授权,需要则直接弹出 |
hideTabBar | Boolean | false | 是否需要隐藏tabbar,在首页等tabbar页面使用改弹窗时建议改为true |
title | String | #ffffff | 用户隐私保护提示 |
predesc | String | 使用前请仔细阅读 Ï | 协议名称前的内容 |
subdesc | String | 当您点击同意后,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用该服务。 | 协议名称后的内容 |
privacyContractNameCustom | String | '' | 自定义协议名称,不传则由小程序自动获取 |
predesc
和 subdesc
的自定义内容,需要主动换行时在内容中添加实体字符
即可
六. zero-privacy组件运行效果
来源:juejin.cn/post/7273803674790150183
领导让前端实习生在网页上添加一个长时间不操作锁定电脑的功能
前情提要
大约一个月前,公司的医疗管理系统终于完工上线。后面一个周一,领导叫大家开会,说后面没有项目进来了,用不了这么多开发人员,原地宣布裁员。再后一周后,花 2000 招了个实习生,工作内容为系统维护。
工作内容
领导:由于我们工作内容很简单,事情轻松,基本就在页面上加加按钮就行,所以工资相对较少一些,是否接受?
实习生小李:能开实习证明吗?
领导:能的。
实习生小李:好的,谢谢老板。
领导:什么时候能入职?
实习生小李:现在。
工作来源
医疗系统是一个比较数据敏感的系统,现在医院那边需要添加一个十分钟时间没有在系统进行操作,则锁定电脑的功能,使用者再次使用时,必须输入密码。客户那边在系统对接群里发出需求时,并没有人回复(PS:人都裁完了),然后老板回复到:好的。
工作安排
领导:小李,我们有个医疗系统,需要添加锁屏功能,你处理一下,两天时间应该没问题吧?
实习生小李:(思索片刻)好的,有代码吗
(4小时之后)
领导:有的,我找下
(第二天10点)
实习生小李:王总,代码找到了没有
(第二天12点)
领导:没代码改不了吗?
实习生小李:(瑟瑟发抖)我试试
(第二天14点)
实习生小李:王总,是那种长时间不操作就锁定系统的功能吗
领导:是的
实习生小李:多久不操作才锁
领导:十分钟,锁了需要输入密码才能使用
实习生小李:但是我们医疗系统没有密码功能
领导:客户电脑有密码啊
实习生小李:是锁电脑系统吗
领导:对
实习生小李:(若有所思)我试试
实现过程
实习生小李:魔镜魔镜,我们有个医疗系统,需要做一个十分钟不操作电脑,就锁定用户电脑系统的功能,在没有源代码的情况下如何实现?
魔镜:好的,在没有源代码的情况下为医疗系统添加十分钟不操作电脑就锁定用户电脑系统的功能,可以使用 sys-shim 实现。
第一步,创建一个目录例如 medical-system
,目录里有以下两个文件:
package.json
文件用来配置 sys-shimpreload.js
用来向医疗系统添加功能
第二步
在 package.json 中编写内容如下
{
"browserArguments": "--disable-web-security --allow-running-insecure-content ",
"form": {
"right": "1300",
"bottom": "800"
},
"page": "https://www.baidu.com/"
}
- browserArguments 用来指定浏览器参数,这里配置为允许跨域以方便注入代码
- form 用来控制窗口如何显示,这里表示窗口大小
- page 表示医疗系统的页面
在 preload.js 中编写内容如下
new Promise(async function () {
window.main = await new window.Sys({ log: true })
// 设置倒计时时间,为了测试方便,这里改为 30 秒
const TIMEOUT = 0.5 * 60 * 1000;
// 声明一个变量来存储 setTimeout 的引用
let timeoutId = null;
// 定义一个函数来重置倒计时并在2分钟后打印日志
function startInactivityCheck() {
// 清除之前的倒计时(如果有的话)
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// 设置一个新的倒计时
timeoutId = setTimeout(function() {
// 锁定系统
window.main.native.sys.lock()
}, TIMEOUT);
}
// 为 body 元素添加点击事件监听器
document.body.addEventListener('click', function() {
console.log("检测到点击事件,重新开始计时。");
// 重置倒计时
startInactivityCheck();
});
// 初始化倒计时
startInactivityCheck();
})
sys.lock()
方法用于锁定操作系统。
第三步,生成应用程序
npx sys-shim pack --input medical-system
运行该命令后,会在当前目录生成一个名为 medical-system.exe 的可执行文件。它封装了医疗系统这个 web 程序,并在里面添加了锁屏功能。
pack
指定表示打包应用--input
参数表示要打包的目录
--input 参数也可以是线上的网页,比如:
npx sys-shim pack --input https://www.baidu.com/
即可获取一个可以调用操作系统 api 的 web 应用。
交付反馈
用户:以前我们还需要进入浏览器输入网址才能进入系统,现在直接在桌面上就能进入,并且还有安全锁屏功能,非常好!
领导:小李干得不错,但没有在规定的时间内完成,但由于客户反馈不错,就不扣你的考核分了。
实习生小李:(不得其解)谢谢老板。
后记
不知不觉,又到了周五,这是公司技术分享会的时候。当前公司技术人员只有实习生小李,由小李负责技术分享。
宽旷的会议室里,秘书、领导、小李三人面面相觑,小李强忍住尴尬,开始了自己的第一次技术分享:
实习生小李:感谢领导给我的工作机会,在这份工作里,我发现了 sys-shim 这个工具,它可以方便的在已有的 web 页面中添加系统 api,获取调用操作系统层面功能的能力,比如关机、锁屏。
领导:(好奇)那他可以读取电脑上的文件吗?
实习生小李:可以的,它可以直接读取电脑上的文件,例如电脑里面的文档、照片、视频等。
突然领导脸色一黑,看了一眼秘书,并关闭了正在访问的医疗系统,然后在技术分享考核表上写下潦潦草草的几个字:考核分-5
。
续集:托领导大福!前端实习生用 vue 随手写了个系统修复工具,日赚 300
提示
大家可以直接运行这个命令生成 app 体验:
npx sys-shim pack --input https://www.baidu.com/
生成后的 app 可以右键解压,看到内部结构。如果遇到问题,可以在这里提交,方便追溯,我会及时解答的。
参考
来源:juejin.cn/post/7373831659470880806
领导被我的花式console.log吸引了!直接写入公司公共库!
文章的效果,大家可以直接只用云vscode实验一下:juejin.cn/post/738875…
背景简介
这几天代码评审,领导无意中看到了我本地代码的控制台,被我花里胡哨的console打印
内容吸引了!
老板看见后,说我这东西有意思,花里胡哨的,他喜欢!
但是随即又问我,这么花里胡哨的东西,上生产会影响性能吧?我自信的说:不会,代码内有判断的,只有开发环境会打印
!
老板很满意,于是让我给其他前端同事分享一下,讲解下实现思路!最终,这个方法还被写入公司的公用utils库里,供大家使用!
console简介
console 是一个用于调试和记录信息的内置对象, 提供了多种方法,可以帮助开发者输出各种信息,进行调试和分析。
console.log()
用于输出一般信息,大家应该在熟悉不过了。
console.info() :
输出信息,与 console.log 类似,但在某些浏览器中可能有不同的样式。
console.warn() :
输出警告信息,通常会以黄色背景或带有警告图标的样式显示。
console.error() :
输出错误信息,通常会以红色背景或带有错误图标的样式显示。
console.table() :
以表格形式输出数据,适用于数组和对象。
例如:
const users = [
{ name: '石小石', age: 18 },
{ name: '刘亦菲', age: 18 }
];
console.table(users);
通过上述介绍,我们可以看出,原生的文本信息、警告信息、错误信息、数组信息打印出来的效果都很普通,辨识度不高!现在我们通过console.log来实现一些花里花哨的样式!
技术方案
console.log()
console.log() 可以接受任何类型的参数,包括字符串、数字、布尔值、对象、数组、函数等。最厉害的是,它支持占位符!
常用的占位符:
- %s - 字符串
- %d or %i - 整数
- %f - 浮点数
- %o - 对象
- %c - CSS 样式
格式化字符串
console.log() 支持类似于 C 语言 printf 函数的格式化字符串。我们可以使用占位符来插入变量值。
const name = 'Alice';
const age = 30;
console.log('Name: %s, Age: %d', name, age); // Name: Alice, Age: 30
添加样式
可以使用 %c 占位符添加 CSS 样式,使输出内容更加美观。
console.log('%c This is a styled message', 'color: red; font-size: 20px;');
自定义样式的实现,其实主要是靠%c 占位符添加 CSS 样式实现的!
实现美化的信息打印
基础信息打印
我们创建一个prettyLog方法,用于逻辑编写
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
// 基础信息打印
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
return {
info
};
};
上述代码定义了一个 prettyLog 函数,用于美化打印信息到控制台。通过自定义样式,输出信息以更易读和美观的格式呈现。
我们使用一下看看效果
// 创建打印对象
const log = prettyLog();
// 不带标题
log.info('这是基础信息!');
//带标题
log.info('注意看', '这是个男人叫小帅!');
info 方法用于输出信息级别的日志。它接受两个参数:textOrTitle 和 content。如果只提供一个参数,则视为内容并设置默认标题为 Info;如果提供两个参数,则第一个参数为标题,第二个参数为内容。最后调用 prettyPrint 方法进行输出。
错误信息打印
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
// ...
};
const info = (textOrTitle: string, content = '') => {
// ...
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
// retu;
return {
info,
error,
};
};
// 创建打印对象
const log = prettyLog();
log.error('奥德彪', '出来的时候穷 生活总是让我穷 所以现在还是穷。');
log.error('前方的路看似很危险,实际一点也不安全。');
成功信息与警告信息打印
// 美化打印实现方法
const prettyLog = () => {
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
// retu;
return {
info,
error,
warning,
success
};
};
// 创建打印对象
const log = prettyLog();
log.warning('奥德彪', '我并非无路可走 我还有死路一条! ');
log.success('奥德彪', '钱没了可以再赚,良心没了便可以赚的更多。 ');
实现图片打印
// 美化打印实现方法
const prettyLog = () => {
// ....
const picture = (url: string, scale = 1) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
return {
info,
error,
warning,
success,
picture
};
}
// 创建打印对象
const log = prettyLog();
log.picture('https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2024%2F0514%2Fd0ea93ebj00sdgx56001xd200u000gtg00hz00a2.jpg&thumbnail=660x2147483647&quality=80&type=jpg');
上述代码参考了其他文章:Just a moment...
url可以传支持 base64,如果是url链接,图片链接则必须开启了跨域访问才能打印
实现美化的数组打印
打印对象或者数组,其实用原生的console.table比较好
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.table(data);
当然,我们也可以伪实现
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
但是,我们无法控制表格的宽度,因此,这个方法不太好用,不如原生。
仅在开发环境使用
// 美化打印实现方法
const prettyLog = () => {
//判断是否生产环境
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
// ...
};
// ...
const picture = (url: string, scale = 1) => {
if (isProduction) return;
// ...
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
我们可以通过import.meta.env.MODE 判断当前环境是否为生产环境,在生产环境,我们可以禁用信息打印!
完整代码
// 美化打印实现方法
const prettyLog = () => {
const isProduction = import.meta.env.MODE === 'production';
const isEmpty = (value: any) => {
return value == null || value === undefined || value === '';
};
const prettyPrint = (title: string, text: string, color: string) => {
if (isProduction) return;
console.log(
`%c ${title} %c ${text} %c`,
`background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`,
`border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`,
'background:transparent'
);
};
const info = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Info' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#909399');
};
const error = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Error' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#F56C6C');
};
const warning = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Warning' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#E6A23C');
};
const success = (textOrTitle: string, content = '') => {
const title = isEmpty(content) ? 'Success ' : textOrTitle;
const text = isEmpty(content) ? textOrTitle : content;
prettyPrint(title, text, '#67C23A');
};
const table = () => {
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 }
];
console.log(
'%c id%c name%c age',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;',
'color: white; background-color: black; padding: 2px 10px;'
);
data.forEach((row: any) => {
console.log(
`%c ${row.id} %c ${row.name} %c ${row.age} `,
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;',
'color: black; background-color: lightgray; padding: 2px 10px;'
);
});
};
const picture = (url: string, scale = 1) => {
if (isProduction) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
const ctx = c.getContext('2d');
if (ctx) {
c.width = img.width;
c.height = img.height;
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0);
const dataUri = c.toDataURL('image/png');
console.log(
`%c sup?`,
`font-size: 1px;
padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px;
background-image: url(${dataUri});
background-repeat: no-repeat;
background-size: ${img.width * scale}px ${img.height * scale}px;
color: transparent;
`
);
}
};
img.src = url;
};
// retu;
return {
info,
error,
warning,
success,
picture,
table
};
};
// 创建打印对象
const log = prettyLog();
来源:juejin.cn/post/7371716384847364147
fabric.js实战
一、业务需求
- 给定一个图片作为参考
- 可配置画笔颜色、粗细
- 可通过手势生成实际轨迹
- 可通过手势来圈选轨迹
- 可撤销、删除轨迹
- 可操作轨迹
- 可缩放、拖动
- 背景网格,且背景网格可缩放和拖动
- 参考图片可缩放、偏移
- 手势处理系统,单指绘制、双指缩放、三指拖动
- 需要一个禁止绘制区域,方便用户在平板上有手掌的支撑区域
二、技术选型
因为涉及到轨迹
、操作轨迹
两点,
svg无法满足大量且复杂的轨迹,canvas没法操作轨迹,所以从 fabricjs/konva 中选型,
因 fabricjs 使用人数更多,所以采取了其作为技术选型。
三、fabric 原理
- 通过其内置的几何对象来创建图形
- 维护一个对象树
- 将对象树通过 canvas-api 绘制在实际的 canvas 上
因此,fabric 能做非常多的优化手段
- 已渲染的节点可通过
子canvas
做缓存 - 对比新旧对象树能做差值更新
- 虚拟画布,类似于虚拟滚动,只渲染可视化的区域
四、模块拆分
- header:与业务相关
- toolBar:工具栏
- sidebar:与业务相关
- canvas:绘制画板
五、架构设计
六、问题收集
6.1 性能问题
- 圈选是实时的,即判断一个多边形是否相交于或包含于一条复杂轨迹,因为使用了射线法,当遇到大量轨迹的时候,可能会卡顿。目前做了多重优化手段,比如函数节流、先稀疏复杂轨迹的点、然后判断图形的占位区域是否相交、然后判断图形的线段之间是否相交、然后判断是否包含关系。
- 轨迹的实时生成,在一长串touchmove事件中,使用一个初始化的polyline,后续更改其点集,这样只需要实例化一个对象,性能高。
- touchmove回调里执行复杂的逻辑,这会阻塞touchmove的触发频率,我们将touchmove里的回调通过settimeout放到异步队列中,这样就剥离了touchmove
事件层
和 回调函数处理层
,这样touchmove的触发频率就不会被影响
6.2 禁止绘制区域
该需求无法实现,因为当我们手掌放在平板上时,会触发系统级别的误触识别算法,阻止所有的触摸事件,所以我们没办法在页面上实现该功能。
touch事件
一个屏幕上可以有多个touch触摸点,这些触摸点绑定的target是可以多个的
touchEvent对象有如下重要属性
- targetTouches:只在当前target(比如某div)上触发的touch触摸点
- touches:在屏幕上触发的所有touch触目点
特别注意点:
- 没有类似鼠标的mouseout事件,所以你一开始是点在div上的,然后移出div的范围后,依旧触发touchmove事件
平板调试手段-chrome浏览器
- 平板安装chrome移动版
- 电脑安装chrome + chrome插件:inspect devices
- 平板开启开发者选项,然后允许usb调试
- usb链接平板和chrome
- 平板和电脑都打开chrome
- 电脑启动插件,然后就能控制平板的chrome,并且对该chrome访问的网页进行调试
- 很好用哇
来源:juejin.cn/post/7278931998650744869
实现小红书响应式瀑布流
前言
瀑布流布局,不管是在pc端还是手机端都很常见,但是我们通常都是列固定。今天来实现一下小红书的响应式瀑布流。后面有仓库地址。
正文
还是先来看看效果
原理:
对每一个item都使用绝对定位,left和top都是0,最后根据容器大小、item的height通过计算来确定item的transform值
接下来从易到难来解析一下实现
初始化数据
列表怎么可以没有数据,先来初始化一下数据
确定列数及列大小
由于是响应式,我们要去监听列表容器的大小变化并记录容器宽度,这样才能做出相应的处理
根据监听得到的容器大小信息,我们可以确定每行个数
和每一个item的宽度
确定列表中item位置
确定item的位置,那么我们只需要确定transform
值就可以了,这也是整个实现的核心。我们还需要解决几个问题
- 对还不知道item的高度,怎么确定
- 我们希望把新的item放置在最低高度的旧item下方,这样全部渲染完每一列的高度才不会相差很多。
item放置的原理图,放置在当前最低高度的下面
更新item高度
当我们第一次运行的时候,每一个item的高度一定都是随机生成的,现在我们要确定item的实际高度。在这里我们还可优化一下,使用懒加载和底部加载,提升性能
。这两个在这里就不讲了,不懂的可以去搜一下。
下面代码一共两个作用
- 记录容器滚动值,传递给每一个
item
,用于判断是否加载图片。 - 判断是否请求添加数据
根据滚动值判断是否加载图片,加载图片后触发父亲更新高度函数
父亲接受到新的高度并更新高度,然后去重新计算transform
值和item高度
完整代码
结语
感兴趣的可以去试试
来源:juejin.cn/post/7270160291411886132
明明 3 行代码即可轻松实现,Promise 为何非要加塞新方法?
给前端以福利,给编程以复利。大家好,我是大家的林语冰。
00. 观前须知
地球人都知道,JS 中的异步编程是 单线程 的,和其他多线程语言的三观一龙一猪。因此,虽然其他语言的异步模式彼此互通有无,但对 JS 并不友好,比如 Actor 模型等。
这并不是说 JS 被异步社区孤立了,只是因为 JS 天生和多线程八字不合。你知道的,要求 JS 使用多线程,就像要求香菜恐惧症患者吃香菜一样离谱。本质上而言,这是刻在 JS 单线程 DNA 里的先天基因,直接决定了 JS 的“异步性状”。有趣的是,如今 JS 也变异出若干多线程的使用场景,只是比较非主流。
ES6 之后,JS 的异步编程主要基于 Promise
设计,比如人气爆棚的 fetch
API 等。因此,最新的 ES2024 功能里,又双叒叕往 Promise
加塞了新型静态方法 Promise.withResolvers()
,也就见怪不怪了。
问题在于,我发现这个新方法居然只要 3 行代码就能实现!奥卡姆剃刀原则告诉我们, 若无必要,勿增实体。那么这个鸡肋的新方法是否违背了奥卡姆剃刀原则呢?我决定先质疑、再质疑。
当然,作为应试教育的漏网之鱼,我很擅长批判性思考,不会被第一印象 PUA。经过三天三夜的刻意练习,机智如我发现新方法果然深藏不露。所以,本期我们就一起来深度学习 Promise
新方法的技术细节。
01. 静态工厂方法
Promise.withResolvers()
源自 tc39/proposal-promise-with-resolvers
提案,是 Promise
类新增的一个 静态工厂方法。
静态的意思是,该方法通过 Promise
类调用,而不是通过实例对象调用。工厂的意思是,我们可以使用该方法生成一个 Promise
实例,而无须求助于传统的构造函数 + new
实例化。
可以看到,这类似于 Promise.resolve()
等语法糖。区别在于,传统构造函数实例化的对象状态可能不太直观,而这里的 promise
显然处于待定状态,此外还“买一送二”,额外附赠一对用于改变 promise
状态的“变态函数” —— resolve()
和 reject()
。
ES2024 之后,该方法可以作为一道简单的异步笔试题 —— 请你在一杯泡面的时间里,实现一下 Promise.withResolvers()
。
如果你是我的粉丝,根本不慌,因为新方法的基本原理并不复杂,参考我下面的实现,简单给面试官表演一下就欧了。
可以看到,这个静态工厂方法的实现难点在于,如何巧妙地将变态函数暴露到外部作用域,其实核心逻辑压缩后有且仅有 3 行代码。
这就引发了本文开头的质疑:新方法是否多此一举?难道负责 JS 标准化的 tc39 委员会也有绩效考核,还是确实存在某些不为人知的极端情况?
02. 技术细节
通过对新方法进行苏格拉底式的“灵魂拷问”和三天三夜的深度学习,我可以很有把握地说,没人比我更懂它。
首先,与传统的构造函数实例化不同,新方法支持无参构造,我们不需要在调用时传递任何参数。
可以看到,构造函数实例化要求传递一个执行器回调,偷懒不传则直接报错,无法顺利实例化。
其次,变态函数的设计更加自由。
可以看到,传统的构造函数中,变态函数能且仅能作为局部变量使用,无法在构造函数外部调用。而新方法同时返回实例及其变态函数,这意味着实例和变态函数处于同一级别的作用域。
那么,这个设计上的小细节有何黑科技呢?
假设我们想要一个 Promise
实例,但尚未知晓异步任务的所有细节,我们期望先将变态函数抽离出来,再根据业务逻辑灵活调用,请问阁下如何应对?
ES2024 之前,我们可以通过 作用域提升 来“曲线救国”,举个栗子:
可以看到,这种方案的优势在于,诉诸作用域提升,我们不必把所有猫猫放在一个薛定谔的容器里,在构造函数中封装一大坨“代码屎山”;其次,变态函数不被限制在构造函数内部,随时随地任你调用。
该方案的缺陷则在于,某些社区规范鼓励“const
优先”的代码风格,即 const
声明优先,再按需修改为 let
声明。
这里的变态函数被迫使用 let
声明,这意味着存在被愣头青意外重写的隐患,但为了缓存赋值,我们一开始就不能使用 const
声明。从防御式编程的角度,这可能不太鲁棒。
因此,Promise.withResolvers()
应运而生,该静态工厂方法允许我们:
- 无参构造
const
优先- 自由变态
03. 设计动机
在某些需要封装 Promise
风格的场景中,新方法还能减少回调函数的嵌套,我把这种代码风格上的优化称为“去回调化”。
举个栗子,我们可以把 Node 中回调风格的 API 转换为 Promise
风格,以 fs
模块为例:
可以看到,由于使用了传统的构造函数实例化,在封装 readFile()
的时候,我们被迫将其嵌套在构造函数内部。
现在,我们可以使用新方法来“去回调化”。
可以看到,传统构造函数嵌套的一层回调函数就无了,整体实现更加扁平,减肥成功!
粉丝请注意,很多 Node API 现在也内置了 Promise
版本,现实开发中不需要我们手动封装,开箱即用就欧了。但是这种封装技巧是通用的。
举个栗子,瞄一眼 MDN 电子书搬运过来的一个更复杂的用例,将 Node 可读流转换为异步可迭代对象。
可以看到,井然有序的代码中透露着一丝无法形容的优雅。我脑补了一下如何使用传统构造函数来实现上述功能,现在还没缓过来......
04. 高潮总结
从历史来看,Promise.withResolvers()
并非首创,bluebird 的 Promise.defer()
或 jQuery 的 $.defer()
等库就提供了同款功能,ES2024 只是换了个名字“新瓶装旧酒”,将其标准化为内置功能。
但是,Promise.withResolvers()
的标准化势在必行,比如 Vite 源码中就自己手动封装了同款功能。
无独有偶,Axios、Vue、TS、React 等也都在源码内部“反复造轮子”,像这种回头率超高的代码片段我们称之为 boilerplate code(样板代码)。
重复乃编程之大忌,既然大家都要写,不如大家都别写,让 JS 自己写,牺牲小我,成全大家。编程里的 DRY 原则就是让我们不要重复,因为很多 bug 就是重复导致的,而且不好统一管理和维护,《ES6 标准入门教程》科普的 魔术字符串 就是其中一种反模式。
兼容性方面,我也做过临床测试了,主流浏览器广泛支持。
总之,Promise.withResolvers()
通过将样板代码标准化,达到了消除重复的目的,原生实现除了性能更好,是一个性价比较高的静态工厂方法。
参考文献
- GitHub:github.com/tc39/propos…
- MDN:developer.mozilla.org/en-US/docs/…
- bluebird:bluebirdjs.com/docs/deprec…
粉丝互动
本期话题是:你觉得新方法好评几颗星,为什么?你可以在本文下方自由言论,文明科普。
欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。
坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~
来源:juejin.cn/post/7391745629876469760
终于找到一个比较好用的前端国际化方案了
在开发Vue/React应用,一直对现有的多语言方案不是很满足,现在终于出了一个比较满意好用的了。
本节以标准的Nodejs
应用程序为例,简要介绍VoerkaI18n
国际化框架的基本使用。
vue
或react
应用的使用流程也基本相同,可以参考Vue集成和React集成。
myapp
|--package.json
|--index.js
在本项目的所有支持的源码文件中均可以使用t
函数对要翻译的文本进行包装,简单而粗暴。
// index.js
console.log(t("中华人民共和国万岁"))
console.log(t("中华人民共和国成立于{}",1949))
t
翻译函数是从myapp/languages/index.js
文件导出的翻译函数,但是现在myapp/languages
还不存在,后续会使用工具自动生成。voerkai18n
后续会使用正则表达式对提取要翻译的文本。
第一步:安装命令行工具
安装@voerkai18n/cli
到全局。
> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
> pnpm add -g @voerkai18/cli
第二步:初始化工程
在工程目录中运行voerkai18n init
命令进行初始化。
> voerkai18n init
上述命令会在当前工程目录下创建languages/settings.json
文件。如果您的源代码在src
子文件夹中,则会创建在src/languages/settings.json
settings.json
内容如下:
{
"languages": [
{
"name": "zh",
"title": "zh"
},
{
"name": "en",
"title": "en"
}
],
"defaultLanguage": "zh",
"activeLanguage": "zh",
"namespaces": {}
}
上述命令代表了:
- 本项目拟支持
中文
和英文
两种语言。 - 默认语言是
中文
(即在源代码中直接使用中文) - 激活语言是
中文
(代表当前生效的语言)
注意:
- 可以修改该文件来配置支持的语言、默认语言、激活语言等。可支持的语言可参阅语言代码列表。
voerkai18n init
是可选的,voerkai18n extract
也可以实现相同的功能。- 一般情况下,您可以手工修改
settings.json
,如定义名称空间。 voerkai18n init
仅仅是创建languages
文件,并且生成settings.json
,因此您也可以自己手工创建。- 针对
js/typescript
或react/vue
等不同的应用,voerkai18n init
可以通过不同的参数来配置生成ts
文件或js
文件。 - 更多的
voerkai18n init
命令的使用请查阅这里
第三步:标识翻译内容
接下来在源码文件中,将所有需要翻译的内容使用t
翻译函数进行包装,例如下:
import { t } from "./languages"
// 不含插值变量
t("中华人民共和国")
// 位置插值变量
t("中华人民共和国{}","万岁")
t("中华人民共和国成立于{}年,首都{}",1949,"北京")
t
翻译函数只是一个普通函数,您需要为之提供执行环境,关于t
翻译函数的更多用法见这里
第四步:提取文本
接下来我们使用voerkai18n extract
命令来自动扫描工程源码文件中的需要的翻译的文本信息。 voerkai18n extract
命令会使用正则表达式来提取t("提取文本")
包装的文本。
myapp>voerkai18n extract
执行voerkai18n extract
命令后,就会在myapp/languages
通过生成translates/default.json
、settings.json
等相关文件。
- translates/default.json : 该文件就是从当前工程扫描提取出来的需要进行翻译的文本信息。所有需要翻译的文本内容均会收集到该文件中。
- settings.json: 语言环境的基本配置信息,包含支持的语言、默认语言、激活语言等信息。
最后文件结构如下:
myapp
|-- languages
|-- settings.json // 语言配置文件
|-- translates // 此文件夹是所有需要翻译的内容
|-- default.json // 默认名称空间内容
|-- package.json
|-- index.js
如果略过第一步中的voerkai18n init
,也可以使用以下命令来为创建和更新settings.json
myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
以上命令代表:
- 扫描当前文件夹下所有源码文件,默认是
js
、jsx
、html
、vue
文件类型。 - 支持
zh
、en
、de
、jp
四种语言 - 默认语言是中文。(指在源码文件中我们直接使用中文即可)
- 激活语言是中文(即默认切换到中文)
-D
代表显示扫描调试信息,可以显示从哪些文件提供哪些文本
第五步:人工翻译
接下来就可以分别对language/translates
文件夹下的所有JSON
文件进行翻译了。每个JSON
文件大概如下:
{
"中华人民共和国万岁":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"] // 记录了该信息是从哪几个文件中提取的
},
"中华人民共和国成立于{}":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"]
}
}
我们只需要修改该文件翻译对应的语言即可。
重点:如果翻译期间对源文件进行了修改,则只需要重新执行一下voerkai18n extract
命令,该命令会进行以下操作:
- 如果文本内容在源代码中已经删除了,则会自动从翻译清单中删除。
- 如果文本内容在源代码中已修改了,则会视为新增加的内容。
- 如果文本内容已经翻译了一部份了,则会保留已翻译的内容。
总之,反复执行voerkai18n extract
命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
第六步:自动翻译
voerkai18n
支持通过voerkai18n translate
命令来实现调用在线翻译服务进行自动翻译。
>voerkai18n translate --appkey <在百度翻译上申请的密钥> --appid <在百度翻译上申请的appid>
在项目文件夹下执行上面的语句,将会自动调用百度的在线翻译API
进行翻译,以现在的翻译水平而言,您只需要进行少量的微调即可。关于voerkai18n translate
命令的使用请查阅后续介绍。
第七步:编译语言包
当我们完成myapp/languages/translates
下的所有JSON语言文件
的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续名称空间
介绍),接下来需要对翻译后的文件进行编译。
myapp> voerkai18n compile
compile
命令根据myapp/languages/translates/*.json
和myapp/languages/settings.json
文件编译生成以下文件:
|-- languages
|-- settings.json // 语言配置文件
|-- idMap.js // 文本信息id映射表
|-- index.js // 包含该应用作用域下的翻译函数等
|-- storage.js
|-- zh.js // 语言包
|-- en.js
|-- jp.js
|-- de.js
|-- formatters // 自定义扩展格式化器
|-- zh.js
|-- en.js
|-- jp.js
|-- de.js
|-- translates // 此文件夹包含了所有需要翻译的内容
|-- default.json
|-- package.json
|-- index.js
第八步:导入翻译函数
第一步中我们在源文件中直接使用了t
翻译函数包装要翻译的文本信息,该t
翻译函数就是在编译环节自动生成并声明在myapp/languages/index.js
中的。
import { t } from "./languages"
因此,我们需要在需要进行翻译时导入该函数即可。
但是如果源码文件很多,重次重复导入t
函数也是比较麻烦的,所以我们也提供了一个babel/vite
等插件来自动导入t
函数,可以根据使用场景进行选择。
第九步:切换语言
当需要切换语言时,可以通过调用change
方法来切换语言。
import { i18nScope } from "./languages"
// 切换到英文
await i18nScope.change("en")
// 或者VoerkaI18n是一个全局单例,可以直接访问
await VoerkaI18n.change("en")
i18nScope.change
与VoerkaI18n.change
两者是等价的。
一般可能也需要在语言切换后进行界面更新渲染,可以订阅事件来响应语言切换。
import { i18nScope } from "./languages"
// 切换到英文
i18nScope.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})
//
VoerkaI18n.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})
@voerkai18n/vue和@voerkai18n/react提供了相对应的插件和库来简化重新界面更新渲染。
第十步:语言包补丁
一般情况下,多语言的工程化过程就结束了,voerkai18n
在多语言实践考虑得更加人性化。有没有经常发现这样的情况,当项目上线后,才发现:
- 翻译有误
- 客户对某些用语有个人喜好,要求你更改。
- 临时要增加支持一种语言
一般碰到这种情况,只好重新打包构建工程,重新发布,整个过程繁琐而麻烦。 现在voerkai18n
针对此问题提供了完美的解决方案,可以通过服务器来为应用打语言包补丁
和动态增加语言
支持,而不需要重新打包应用和修改应用。
方法如下:
- 注册一个默认的语言包加载器函数,用来从服务器加载语言包文件。
import { i18nScope } from "./languages"
i18nScope.registerDefaultLoader(async (language,scope)=>{
return await (await fetch(`/languages/${scope.id}/${language}.json`)).json()
})
- 将语言包补丁文件保存在Web服务器上指定的位置
/languages/<应用名称>/<语言名称>.json
即可。 - 当应用启动后会自动从服务器上加载语言补丁包合并,从而实现动为语言包打补丁的功能。
- 利用该特性也可以实现动态增加临时支持一种语言的功能
来源:juejin.cn/post/7275944565885485116
使用Tauri快速搭建桌面项目
什么是 Tauri
Tauri 是一个跨平台的 GUI
框架,与 Electron
的理念相似。Tauri 的前端部分同样基于 Web 技术,但它的后端则采用了 Rust
语言。Tauri 可以创建体积更小、运行更快且更加安全的跨平台桌面应用。
与 Electron
不同,Tauri 并没有内置 Chromium
,因此打包后的应用体积要比 Electron
小很多,启动速度更快,内存和 CPU 占用率也更低。
然而,由于没有内置 Chromium
,Tauri 使用系统原生的 WebView 来渲染网页,这可能导致不同系统之间的页面表现存在差异。同时,Tauri 的后端需要使用 Rust
进行开发,这对前端开发人员来说可能会有一定的上手成本。
好在 Tauri 已经为我们封装了大部分 API,即使不懂 Rust
,也可以开发出一款简单的应用。
预先准备
我们以 macOS 为例:
1. 首先安装 Xcode 命令行工具
在终端中执行以下命令:
xcode-select --install
如果已经安装过 Xcode 命令行工具,则可以直接进行下一步。
2. 安装 Rust
在 macOS 上安装 Rust,请打开终端并输入以下命令:
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
安装成功后,终端将显示以下内容:
Rust is installed now. Great!
请确保重新启动终端以使更改生效。
快速开始
创建项目
Tauri 官方提供了多种项目模板,可以快速搭建项目:
# pnpm
pnpm create tauri-app
# npm
npm create tauri-app
# yarn
yarn create tauri-app
按照提示选择自己喜欢的模板。
这里我们选择 react
开发前端页面。
一路回车后,打开项目文件夹。执行安装依赖命令:pnpm i
。依赖安装完成后,执行 pnpm tauri dev
命令启动项目。这时便会启动一个应用,如下图所示:
开发
Tauri 的开发非常容易上手,我们先来看下项目文件结构:
是不是和 Vite 的目录结构一样?
没错,这就是一个常规的 Vite 目录结构,唯一的区别是增加了一个 src-tauri
文件夹,这里面是 Rust
部分的代码,也就是后端代码。
打包
首先,我们需要修改默认的包标识符,位置在 src-tauri > tauri.conf.json > tauri > bundle > identifier
。
这里我们随便填写一个标识符 com.example.app
,保存,然后执行命令:pnpm tauri build
就可以正常打包了。
tauri.conf.json
文件是我们的应用配置文件,包含了应用的基本信息。
打包完成后,就可以在 tauri-app/src-tauri/target/release/bundle
目录下找到我们的应用。
现在我们只构建了 macOS 下的应用。
打开之后就可以看到我们的应用了。
参考文档
来源:juejin.cn/post/7388842078798823433
学会Grid之后,我觉得再也没有我搞不定的布局了
说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局
、双飞翼布局
等非常耳熟的名词;
为了实现这些布局我们有很多种实现方案,例如:table布局
、float布局
、定位布局
等,当然现在比较流行的肯定是flex布局
;
flex布局
属于弹性布局,所谓弹性也可以理解为响应式布局
,而同为响应式布局的还有Grid布局
;
Grid布局
是一种二维布局,可以理解为flex布局
的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;
本篇不会过多介绍
grid
的基础内容,更多的是一些布局的实现方案和一些小技巧;
常见布局
所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局
、双飞翼布局
这种名词我个人觉得不用太过于去在意;
因为这类布局最后的解释都会变成几行几列
,内容在哪一行哪一列,而这些就非常直观的对标了grid
的特性;
接下来我们来一起看看一些非常常见的布局,并且用grid
来实现;
1. 顶部 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr;
height: 100vh;
}
.header {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.header,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
</body>
</html>
2. 顶部 + 内容 + 底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr 60px;
height: 100vh;
}
.header {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.footer {
background-color: #039BE5;
}
.header,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>
这里示例和上面的示例唯一的区别就是多了一个
footer
,但是我们可以看到代码并没有多少变化,这就是grid
的强大之处;
可以看
码上掘金
的效果,这里的内容区域是单独滚动的,从而实现了header
和footer
固定,内容区域滚动的效果;
实现这个效果也非常简单,只需要在
content
上加上overflow: auto
即可;
3. 左侧 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.left {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>
这个示例效果其实和第一个是类似的,只不过是把
grid-template-rows
换成了grid-template-columns
,这里就不提供码上掘金
的示例了;
4. 顶部 + 左侧 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.header {
grid-column: 1 / 3;
background-color: #039BE5;
}
.left {
background-color: #4FC3F7;
}
.content {
background-color: #99CCFF;
}
.header,
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>
这个示例不同点在于
header
占据了两列,这里我们可以使用grid-column
来实现,grid-column
的值是start / end
,例如:1 / 3
表示从第一列到第三列;
如果确定这一列是占满整行的,那么我们可以使用
1 / -1
来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧
的布局,那么header
就不需要修改了;
5. 顶部 + 左侧 + 内容 + 底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header header"
"left content"
"left footer";
grid-template-rows: 60px 1fr 60px;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.left {
grid-area: left;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.footer {
grid-area: footer;
background-color: #6699CC;
}
.header,
.left,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>
这个示例的小技巧是使用了
grid-template-areas
,使用这个属性可以让我们通过代码来直观的看到布局的样式;
这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:
"header header"
表示第一行的两列都是header
,这里的header
是我们自己定义的,可以是任意值;
定义好了之后就可以在对应的元素上使用
grid-area
来指定对应的区域,这里的值就是我们在grid-template-areas
中定义的值;
在
码上掘金
中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto
即可;
响应式布局
响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;
这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;
1. 基础布局实现
移动端布局
以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是
header
、navigation
、content
;
注:这里不是要
100%
还原掘金的页面,只是为了演示grid
布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header"
"navigation"
"content";
grid-template-rows: 60px 48px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
</body>
</html>
iPad布局
这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下
grid-template-rows
和grid-template-columns
的值即可;
由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的
css
代码,只保留需要修改的代码;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.right {
display: none;
background-color: #6699CC;
}
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header"
"navigation navigation"
"content right";
grid-template-columns: 1fr 260px;
}
.right {
grid-area: right;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
PC端布局
和上面处理方式相同,由于
Navigation
移动到了左侧,所以还要额外的修改一下grid-template-areas
的值;
这里就可以体现
grid
的强大之处了,我们可以简单的修改grid-template-areas
就可以实现一个完全不同的布局,而且代码量非常少;
为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用
.
来实现,这里的.
表示一个空白区域;
由于内容的宽度基本上是固定的,所以留白区域简单的使用
1fr
进行占位即可,这样就可以平均的分配剩余的空间;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 1fr;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
完善一些细节
最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用
column-gap
和一个空的区域进行占位来实现的;
这里的
column-gap
表示列与列之间的间距,值可以是px
、em
、rem
等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr
;
空区域进行占位留间距其实我并不推荐,这里只是演示
grid
布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin
来实现;
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header header header"
"navigation navigation navigation"
". . ."
". content .";
grid-template-columns: 1fr minmax(0, 720px) 1fr;
grid-template-rows: 60px 48px 10px 1fr;
column-gap: 10px;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.right {
display: none;
background-color: #6699CC;
}
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header header header"
"navigation navigation navigation navigation"
". . . ."
". content right .";
grid-template-columns: 1fr minmax(0, 720px) 260px 1fr;
}
.right {
grid-area: right;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". . . . ."
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 10px 1fr;
}
}
.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
简单复刻版
以
码上掘金
上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;
异型布局
异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid
是如何实现的;
1. 照片墙
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
background: #f2f3f5;
overflow: auto;
}
body {
display: grid;
grid-template-columns: repeat(12, 100px);
grid-auto-rows: 100px;
place-content: center;
gap: 6px;
height: 100vh;
}
.photo-item {
width: 200px;
height: 200px;
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
</style>
</head>
<body>
</body>
<script>
function randomColor() {
return '#' + Math.random().toString(16).substr(-6);
}
let row = 1;
let col = 1;
for (let i = 0; i < 28; i++) {
const div = document.createElement('div');
div.className = 'photo-item';
div.style.backgroundColor = randomColor();
div.style.gridRow = `${row} / ${row + 2}`;
div.style.gridColumn = `${col} / ${col + 2}`;
document.body.appendChild(div);
col += 2;
if (col > 11) {
row += 1;
col = row % 2 === 0 ? 2 : 1;
}
}
</script>
</html>
这是一个非常简单的照片墙效果,如果不使用
grid
的话,我们大概率是会使用定位去实现这个效果,但是换成grid
的话就非常简单了;
而且代码量是非常少的,这里就不提供
码上掘金
的 demo 了,感兴趣的同学可以将代码复制下来自行查看效果;
2. 漫画效果
在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用
grid
的话就非常简单了;
可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用
z-index
来实现,这里的z-index
值越大,元素就越靠前;
而且气泡文字效果也是通过
grid
来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;
3. 画报效果
在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;
在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用
grid
的话就会简单很多;
我这里将页面划分为
12 * 12
区域的网格,然后依次对不同的元素进行单独排列和样式的设置;
流式布局
流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;
但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;
通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))
这种;
直接看效果:
这里有两个关键字,一个是
auto-fit
,还有一个是auto-fill
,在行为上它们是相同的,不同的是它们在网格创建的不同,
就像上面图中看到的一样,使用
auto-fit
会将空的网格进行折叠,可以看到他们的结束colum
的数字都是6
;
像我们上面的实例中不会出现这个问题,因为我们使用了响应式单位
fr
,只有使用固定单位才会出现这个现象;
感兴趣的同学可以将
minmax(200px, 1fr)
换成200px
尝试;
对比 Flex 布局
在我上面介绍了这么多的布局场景和案例,其实可以很明显的发现一件事,那就是我使用grid
进行的布局基本上都是大框架;
当然上面也有一些布局使用flex
也是可以实现的,但是我们再换个思路,除了flex
可以做到上面的一些布局,float
布局、table
布局、定位布局其实也都能实现;
不同的是float
布局、table
布局、定位布局基本上都是一些hack
的方案,就拿table
布局来说,table
本身就是一个html
标签,作用就是用来绘制表格,被拿来当做布局的一种方案也是迫不得已;
而web布局
发展到现在的我们有了正儿八经可以布局的方案flex
,为什么又要出一个grid
呢?
grid
的出现绝对不是用来替代flex
的,在我上面的实现的一些布局案例中,也可以看到我还会使用flex
;
我个人理解的是使用grid
进行主体的大框架的搭建,flex
作为一些小组件的布局控制,两者搭配使用;
flex
能实现一些grid
不好实现的布局,同样grid
也可以实现flex
实现困难的布局;
本身它们的定位就不痛,flex
作为一维布局的首选,grid
定位就是比flex
高一个维度,它的定位是二维布局,所以他们之间没有必要进行对比,合理使用就好;
总结
上面介绍的这么多基于grid
布局实现的布局方案,足以看出grid
布局的强大;
grid
布局的体系非常庞大,本文只是梳理出一些常见的布局场景,通过grid
布局去实现这些布局,来体会grid
带来的便利;
可能需要完全理解我上面的全部示例需要对grid
有一定的了解才可以,但是都看到这里了,不妨去深挖一下;
grid
布局作为一项强大的布局技术,有望在未来继续发展,除了我上面说到的布局,grid
还有很多小技巧来实现非常多的布局场景;
碍于我的见识和文笔的限制,我这次介绍grid
肯定是有很多不足的,但是还是希望这篇文章能为你对于布局相关能有新的认识;
来源:juejin.cn/post/7310423470546354239
我为什么选择Next.js+Supabase做全栈开发
作为一名前端工程师,选择合适的技术栈对项目的成功至关重要,我最近一个星期尝试了下这两个技术栈的组合,大概在一个星期就写了一个小 SAAS,总共 10 多个页面。在本文中,我将分享为什么我选择Next.js 14和Supabase作为全栈开发的首选组合,并通过最新的代码示例和比较数据,直观地展示这个选择带来的诸多优势。
Next.js 14: 现代React应用的革新框架
默认服务器组件的优势
Next.js 14默认使用服务器组件,这对于提升性能和开发体验至关重要。
例如,一个简单的服务器组件如下:
// app/page.js
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Home() {
const data = await getData()
return <div>Welcome to {data.name}div>
}
在这个例子中,Home
组件是一个异步的服务器组件,它可以直接进行数据获取,而无需使用useEffect或getServerSideProps。
App Router: 更强大的路由系统
Next.js 14采用了新的App Router,提供了更灵活和直观的路由方式:
app/
page.js // 对应路由 /
about/
page.js // 对应路由 /about
posts/
[id]/
page.js // 对应路由 /posts/1, /posts/2, 等
Server Actions: 无需API路由的表单处理
Next.js 14引入了Server Actions,允许我们直接在服务器上处理表单提交,无需单独的API路由:
// app/form.js
export default function Form() {
async function handleSubmit(formData) {
'use server'
// 在服务器上处理表单数据
const name = formData.get('name')
// ...处理逻辑
}
return (
<form action={handleSubmit}>
<input type="text" name="name" />
<button type="submit">Submitbutton>
form>
)
}
这个能力好用到哭,不用再写API路由了,直接在页面上处理表单提交。代码简单了不止一点点。
Supabase: 开源Firebase替代品的崛起
数据库即服务的便利性
Supabase提供了PostgreSQL数据库即服务,使用起来非常简单:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
// 插入数据
const { data, error } = await supabase
.from('users')
.insert({ name: 'John', email: 'john@example.com' })
实时功能的强大支持
Supabase的实时订阅功能让实现实时更新变得轻而易举:
import { useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
function RealtimeData() {
useEffect(() => {
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'users' }, payload => {
console.log('New user:', payload.new)
})
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [])
return <div>Listening for new users...div>
}
身份认证和授权的简化
Supabase内置的身份认证系统大大简化了用户管理:
const { data, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
})
Next.js 14 + Supabase: 完美的全栈开发组合
开发效率的显著提升
结合Next.js 14和Supabase,我们可以快速构建全功能的Web应用。以下是一个简单的例子,展示了如何在Next.js 14的服务器组件中使用Supabase:
// app/posts/page.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
export default async function Posts() {
const { data: posts } = await supabase.from('posts').select('*')
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}div>
))}
div>
)
}
这个例子展示了Next.js 14服务器组件如何与Supabase无缝集成,直接在服务器端获取数据并渲染。
与其他技术栈的对比
为了更直观地展示Next.js 14+Supabase的优势,我们来看一个更新后的比较表格:
特性 | Next.js 14+Supabase | MERN Stack | Firebase | Django |
---|---|---|---|---|
默认服务器组件 | ✅ | ❌ | ❌ | N/A |
App Router | ✅ | ❌ | ❌ | ❌ |
Server Actions | ✅ | ❌ | ❌ | ✅ |
实时数据库 | ✅ | 需配置 | ✅ | 需配置 |
SQL支持 | ✅ (PostgreSQL) | ❌ (默认NoSQL) | ❌ (NoSQL) | ✅ |
身份认证 | ✅ | 需配置 | ✅ | ✅ |
学习曲线 | 中 | 中 | 低 | 高 |
全栈JavaScript | ✅ | ✅ | ✅ | ❌ |
开源 | ✅ | ✅ | ❌ | ✅ |
选型优势的直观感受
- 开发速度:使用Next.js 14+Supabase,你可以在几小时内搭建起一个包含用户认证、数据库操作和实时更新的全栈应用。
- 代码量减少:得益于Next.js 14的服务器组件和Supabase的简洁API,代码量可以减少40%-60%。
- 性能提升:通过Next.js 14的默认服务器组件和自动代码分割,页面加载速度可以提升40%-70%。
- 学习成本:虽然新概念(如服务器组件)需要一定学习时间,但整体学习曲线比传统全栈开发更平缓,2-3周即可上手。
- 维护简化:单一语言(TypeScript)贯穿全栈,加上Next.js的文件约定和Supabase的声明式API,大大减少了维护的复杂度。
- 可扩展性:Supabase基于PostgreSQL,为未来的扩展提供了更多可能性,而Next.js的渐进式框架特性也允许逐步采用高级功能。
一些想法
Next.js 14和Supabase是现代全栈开发的最佳选择,它们的结合提供了前所未有的开发体验和性能优势。如果你正在寻找一个全栈开发的新方向,不妨试试Next.js 14和Supabase,相信你会爱上这个组合。而且 supabase 学了也很划算,即便你想做 react native,Flutter,他都可以作为你坚实的后端。
来源:juejin.cn/post/7389925676520226825
大厂都在”偷偷“用语义化标签,你却还在div?
引言
在我们日常浏览网页的时候,通常会看到各种各样的内容,如文字、图片、视频等。这些内容背后都有一个共同的语言,那就是HTML(超文本标记语言)。HTML是构建网页的基础,它就像建筑物的框架,决定了网页的基本结构和布局。
然而,仅仅有结构是不够的。如果网页只是简单地用一些基础标签堆砌而成,那么浏览器、搜索引擎甚至我们自己在后期维护时,都会感到非常吃力。
这时候,HTML的语义化标签就显得尤为重要。语义化标签不仅能使网页结构更加清晰,还能帮助搜索引擎更好地理解和索引网页内容。
什么是HTML语义化标签?
HTML语义化标签,就是那些带有特定含义的标签,它们告诉浏览器和搜索引擎,每一部分内容是什么。这就好比是给每个内容部分都贴上了一个清晰的标签,让所有人都能明白这个部分是用来做什么的。
举个例子,假设你在看一本书,书的封面、目录、章节标题等都是明确标示出来的,这样你就能快速找到自己想看的部分。同样,HTML语义化标签也是为了让网页的内容更加明晰易懂。比如:
<header>
标签用来定义网页的头部内容,通常包含导航栏、Logo等信息;<nav>
标签专门用于定义导航链接,这样搜索引擎就能更好地理解网站的结构;<article>
标签用于定义独立的内容,比如一篇新闻文章或者博客帖子。- ……
通过使用这些语义化标签,不仅提高了网页的可读性和维护性,还能帮助搜索引擎更准确地抓取和排名内容,从而提升网站的SEO效果。
为什么使用语义化标签?
使用HTML语义化标签有很多好处,它们不仅能让代码更清晰,还能带来实际的效果和便利。
- 提高网页的可读性和结构化
- 语义化标签让HTML代码更加直观,其他开发者在阅读和维护代码时,可以快速理解每个部分的作用。这有助于团队合作和项目的长期维护。
- 有助于搜索引擎优化(SEO)
- 搜索引擎通过爬虫程序抓取网页内容,并根据网页的结构和内容进行索引。使用语义化标签可以帮助搜索引擎更好地理解网页的层次和重点内容,从而提升网站在搜索结果中的排名。
- 无障碍支持
- 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用
<nav>
标签可以让屏幕阅读器快速跳转到导航部分。 - 还记得浏览器内置的“沉浸阅读器”吗?它们也大多基于语义化标签提供服务。例如掘金的文章都是用
<article>
标签包裹的,所以你可以在掘金文章页面启用沉浸阅读器,而且精准的获取了文章的主体内容。
- 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用
另外不得不说,目前苹果对语义化标签的使用是最炉火纯青的。怪不得都说苹果优雅,现在算是在前端上见识到了这个细节怪……
其实还有很多大厂都在使用,但都是偷偷地使用。它们没有全局使用语义化标签,而是在特定的关键位置使用语义化标签来 “谄媚” 一下搜索引擎或浏览器提供的无障碍功能。
所以我相信很多人还是非常支持div一把梭的,只要老板不限制,想怎么做就怎么做。不过如果你也能学习大厂,在漫天div下加一点语义化标签的小巧思,骗过搜索引擎和浏览器,这不是很香吗?
所以,本文着重介绍那些搜索引擎和浏览器有特别支持的语义化标签,搞定他们就搞定了一大半!
常用的语义化标签
搜索引擎钟爱的语义化标签
搜索引擎(如Google、Bing等)特别关注某些HTML语义化标签,因为这些标签能够帮助它们更好地理解网页的结构和内容,从而改进搜索结果的质量。
以下是一些被搜索引擎特别关注的语义化标签:
<header>
- 搜索引擎会识别
<header>
标签中的内容,通常包括页面的标题、导航链接等,有助于理解网页的整体结构和主要部分。
- 搜索引擎会识别
<nav>
<nav>
标签标示出导航链接区域,帮助搜索引擎理解网站的链接结构和页面之间的关系,有助于内部链接的优化。
<article>
<article>
标签表示独立的内容块,如新闻文章、博客帖子等。搜索引擎会特别关注这些标签,认为其包含主要的内容。
<footer>
<footer>
标签包含页脚内容,通常包括版权信息、联系信息等,搜索引擎会利用这些信息来补充网页的相关性数据。
<main>
<main>
标签标示出页面的主要内容区域,帮助搜索引擎更快地定位和抓取主要内容,而忽略导航栏、页脚等次要部分。
浏览器的无障碍功能
现代浏览器具备许多无障碍功能(accessibility features),这些功能可以帮助有特殊需求的用户更好地浏览网页。
以下是一些关键的无障碍功能:
- 屏幕阅读器支持
- 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,
<nav>
标签可以让用户快速跳转到导航部分,而<article>
标签则可以帮助用户找到主要的文章内容。
- 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,
- 键盘导航
- 无障碍浏览器允许用户通过键盘进行导航,语义化标签如
<header>
、<nav>
、<main>
、<footer>
等,可以帮助键盘用户快速跳转到页面的不同部分,提高浏览效率。
<header>
<h1>网站标题</h1>
</header>
<nav>
<!-- 导航内容 -->
</nav>
<main>
<h2>主要内容标题</h2>
<p>这是主要内容区域。</p>
</main>
<footer>
<p>版权所有 © 2024 公司名称</p>
</footer>
- 无障碍浏览器允许用户通过键盘进行导航,语义化标签如
- 高对比度模式
- 一些浏览器提供高对比度模式,帮助视觉有障碍的用户更容易阅读内容。使用正确的语义化标签和良好的结构,可以确保在高对比度模式下内容的可读性和可访问性。
<section>
<h2>章节标题</h2>
<article>
<h3>文章标题</h3>
<p>文章内容...</p>
</article>
<aside>
<h3>附加内容</h3>
<p>例如广告或链接...</p>
</aside>
</section>
- ARIA(可访问性富互联网应用)标签
- 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,
aria-label
、aria-labelledby
等属性可以为非文本元素提供文本描述,帮助辅助技术更好地解释内容。
<button aria-label="关闭">X</button>
<div role="dialog" aria-labelledby="dialogTitle" aria-describedby="dialogDescription">
<h2 id="dialogTitle">对话框标题</h2>
<p id="dialogDescription">对话框内容描述。</p>
</div>
- 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,
语义化标签的实际应用
为了更好地理解语义化标签的使用方法,让我们通过一个具体的案例来展示它们的实际应用。
假设我们要创建一个简单的博客页面,包含标题、导航栏、文章内容、侧边栏和页脚。下面是一个示例代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的博客</title>
<style>
body { font-family: Arial, sans-serif; }
header, nav, article, aside, footer { margin: 20px; padding: 10px; border: 1px solid #ccc; }
nav ul { list-style-type: none; padding: 0; }
nav ul li { display: inline; margin-right: 10px; }
aside { float: right; width: 30%; }
article { float: left; width: 65%; }
</style>
</head>
<body>
<header>
<h1>我的博客</h1>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#about">关于我</a></li>
<li><a href="#contact">联系我</a></li>
</ul>
</nav>
</header>
<section>
<article>
<h2>文章标题</h2>
<p>这里是文章的正文内容。</p>
</article>
<aside>
<h2>侧边栏</h2>
<p>这里是一些附加内容,比如广告或链接。</p>
</aside>
</section>
<footer>
<p>版权所有 Dikkoo; 2024 我的博客</p>
</footer>
</body>
</html>
回顾一下
在这个案例中,我们使用了多个语义化标签来组织页面内容:
<header>
包含网站的标题和导航栏。<nav>
用于定义导航链接区域。<section>
用于分隔主要内容区域,包含文章和侧边栏。<article>
定义了独立的文章内容。<aside>
包含附加内容,如侧边栏。<footer>
包含页面的底部信息。
怎样合理运用语义化标签?
为了充分发挥HTML语义化标签的优势,以下是一些最佳实践建议:
- 规划页面结构,提前设计
- 在编写HTML之前,先绘制页面的结构图,明确各部分的功能和内容。根据设计选择合适的语义化标签,这样可以避免在编写过程中频繁修改结构。
- 保持代码简洁
- 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签和使用多余的标签。使用语义化标签替代大量的
<div>
和<span>
,使代码更加简洁和易读。
- 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签和使用多余的标签。使用语义化标签替代大量的
- 合理嵌套标签
- 语义化标签应按照其语义进行嵌套。例如,将
<nav>
放在<header>
内,表示导航是头部的一部分;将<section>
和<article>
合理地嵌套在一起,表示内容的层次结构。
- 语义化标签应按照其语义进行嵌套。例如,将
- 遵循HTML规范
- 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将
<header>
标签用在每个段落中,而应仅用于页面或章节的头部。
- 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将
来源:juejin.cn/post/7388056946121113637
怎样实现每次页面打开时都清除本页缓存?
"```markdown
每次页面加载时清除本页缓存可以通过多种方式实现,具体方法取决于需要的粒度和数据类型。以下是一些常见的技术:
使用meta标签(HTML):
<meta http-equiv=\"cache-control\" content=\"no-cache, no-store, must-revalidate\">
<meta http-equiv=\"pragma\" content=\"no-cache\">
<meta http-equiv=\"expires\" content=\"0\">
使用JavaScript:
// 清除整个页面缓存
window.location.reload(true);
// 清除特定资源的缓存
const url = 'https://example.com/style.css';
fetch(url, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
}).then(response => {
// 处理响应
});
// 清除localStorage
localStorage.clear();
// 清除sessionStorage
sessionStorage.clear();
使用HTTP头信息(服务端设置):
// Express.js 示例
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
使用框架或库功能:
例如,React中可以通过key属性强制重新渲染组件来清除缓存:
function App() {
const [key, setKey] = useState(0);
const resetPage = () => {
setKey(prevKey => prevKey + 1);
};
return (
<div key={key}>
{/* 页面内容 */}
<button onClick={resetPage}>重置页面</button>
</div>
);
}
清除浏览器缓存:
用户可以手动清除浏览器缓存来达到相同的效果。这通常通过浏览器设置或开发者工具的Network面板来实现。
综上所述,实现每次页面加载时清除本页缓存可以根据具体情况选择合适的方法。无论是通过HTML标签、JavaScript代码、服务器端设置还是框架功能,都可以有效地控制和管理页面的缓存行为,确保用户获得最新和最准确的内容。
来源:juejin.cn/post/7389643363160965130
百亿补贴为什么用 H5?H5 未来会如何发展?
23 年 11 月末,拼多多市值超过了阿里。我想写一篇文章《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。
眼看着灵感就要烂在手里,我决定把两篇文章合为一篇,与大家分享。当然,这些分析预测只是个人观点,如果你有不同的意见,欢迎在评论区讨论交流。
拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。
百亿补贴为什么用 H5
我们先看两张图,在 Android 手机开发者模式下,开启显示布局边界,可以看到「百亿补贴」是一个完整大框,说明「百亿补贴」在 App 内是 H5;拷贝分享链接,在浏览器打开,可以看到资源中有 react 名字的 js 文件,说明「百亿补贴」技术栈大概率是 React。
不只是拼多多,我特地确认了,京东、淘宝的的「百亿补贴」技术栈也是 H5。
那么,为什么电商巨头会在「百亿补贴」这种重要活动上选择 H5 呢?用 H5 有什么好处呢?
H5 技术已经成熟
第一个原因,也是最基础的原因,就是 H5 技术已经成熟,能够完整地实现功能。具体来说:
浏览器兼容性不断提高
自 2008 年 HTML5 草案发布以来,截止 2024 年,HTML5 已有 16 年历史。16 年间,主流浏览器对 HTML5、CSS3 和 JavaScript 的标准语法兼容性一直持续改进,22 年微软更是亲手盖上了 IE 棺材板。虽然 Safari(iOS 浏览器)的兼容性仍然备受诟病,但总体来说兼容成本已经变得可以接受。
主流框架已经成熟
前端最主流的两大框架 Vue 和 React 已经成熟。它们的成熟体现在多个方面:
- 从时间的角度看,截止 2024 年,React 已经发布了 11 年,而 Vue 已经发布了 10 年。经过多年的发展,前端开发者已经非常熟悉 React 和 Vue,能熟练地应用它们进行开发。
- 从语法的角度看,自 React16.8 发布 Hooks,以及 Vue3 发布 Composition API 以来,两大框架语法基本稳定,不再有大的变化。前端开发者可以更加专注于业务逻辑,无需过多担心框架语法的变动。
- 从未来发展方向看,React 目前致力于推广 React Server Component 1;Vue 则在尝试着无 VDom 的 Vapor 方向,并计划利用 Rust 重写 Vite 2。这表明旧领域不再有大的颠覆,两大框架已经正寻求新的发展领域。
混合开发已经成熟
混合开发是指将原生开发(Android 和 iOS)和 Web 开发结合起来的一种技术。简而言之,它将 H5 嵌入到移动应用内部运行。近些年,业界对混合开发的优势和缺陷已经有清晰的认识,并针对其缺陷进行了相应的优化。具体来说:
- 混合开发的优势包括开发速度快、一套代码适配 Android 和 iOS,以及实现代码的热更新。这意味着程序员能更快地编写跨平台应用,及时更新应用、修复缺陷;
- 混合开发的缺陷则是性能较差、加载受限于网络。针对这个缺陷,各大 App、以及云服务商如阿里云 3 和腾讯云 4 都推出了自己的离线包方案。离线包方案可以将网页的静态资源(如 HTML、CSS、JS、图片等)缓存到本地,用户访问 H5 页面时,可以直接读取本地的离线资源,从而提升页面的加载速度。可以说,接入离线包后,H5 不再有致命缺陷。
前端基建工具已经成熟
近些年来,业界最火的技术话题之一,就是用 Rust 替代前端基建,包括:用 Rust 替代 Webpack 的 Rspack;用 Rust 替代 Babel 的 SWC;用 Rust 替代 Eslint 的 OxcLint 等等。
前端开发者对基建工具抱怨,已经从「这工具能不能用」,转变为「这工具好不好用」。这种「甜蜜的烦恼」,只有基建工具成熟后才会出现。
综上所述,浏览器的兼容性提升、主流框架的成熟、混合开发的发展和前端基建工具的完善,使 H5 完全有能力承载「百亿补贴」业务。
H5 开发成本低
前文我们已经了解到,成熟的技术让 H5 可以实现「百亿补贴」的功能。现在我们介绍另一个原因——H5 开发成本低。
「百亿补贴」需要多个 H5
「百亿补贴」的方式,是一个常住的 H5,搭配上多个流动的 H5。(「常住」和「流动」是我借鉴「常住人口」和「流动人口」造的词)
- 常住 H5 链接保持不变。站外投放的链接基本都是常住 H5 的,站内首页入口链接也是常住 H5 的,这样方便用户二次访问。
- 流动 H5 链接位于常住 H5 的不同位置,比如头图、侧边栏等。时间不同、用户不同、算法不同,流动 H5 的链接都会不同,流动 H5 可以区分用户,方便分发流量。
具体来看,拼多多至少有三个流量的分发点,第一个是可点击的头图,第二个是列表上方的活动模块,第三个是右侧浮动的侧边栏,三者可以投放不同的链接。最近就分别投放 3.8 女神节链接、新人链接和品牌链接:
「百亿补贴」需要及时更新
不难想到,每到一个节日、每换一个品牌,「百亿补贴」就需要更新一次。
有时还需要为一些品牌定制化 H5 代码。如果使用其他技术栈,排期跟进通常会比较困难,但是使用 H5 就能够快速迭代并上线。
H5 投放成本低
我们已经「百亿补贴」使用 H5 技术栈的两个原因,现在来看第三个原因——H5 适合投放。
拼多多的崛起过程中,投放到其他 App 的链接功不可没。早期它通过微信等社交平台「砍一刀」的模式,低成本地吸引了大量用户。如今,它通过投放「百亿补贴」策略留住用户。
H5 的独特之处,在于它能够灵活地在多个平台上进行投放,其他技术栈很难有这样的灵活性。即使是今天,抖音、Bilibili 和小红书等其他 App 中,「百亿补贴」的 H5 链接也随处可见。
拼多多更是将 H5 这种灵活性发挥到极致,只要你有「百亿补贴」的链接,你甚至可以在微信、飞书、支付宝等地方直接查看「百亿补贴」 H5 页面。
综上所述,能开发、能快速开发、且开发完成后能大量投放,是「百亿补贴」青睐 H5 的原因。
H5 未来会如何发展
了解「百亿补贴」选择 H5 的原因后,我们来看看电商巨头对 H5 未来发展的影响。我认为有三个影响:
H5 数量膨胀,定制化要求苛刻
C 端用户黏性相对较低,换一个 App 的成本微不足道。近年 C 端市场增长缓慢,企业重点从获取更多的新客变成留住更多的老客,很难容忍用户丢失。因此其他企业投放活动 H5 时,企业必须也投放活动 H5,电商活动 H5 就变得越来越多。
这个膨胀的趋势不仅仅存在于互联网巨头的 App 中,中小型应用也不例外,甚至像 12306、中国移动、招商银行这种工具性极强的应用也无法幸免。
随着市场的竞争加剧,定制化要求也变得越来越苛刻,目的是让消费者区分各种活动。用互联网黑话来说,就是「建立用户心智」。在可预见的未来,尽管电商活动 H5 结构基本相同,但是它们的外观将变得千差万别、极具个性。
SSR 比例增加,CSR 占据主流
在各家 H5 数量膨胀、竞争激烈的情况下,一定会有企业为提升 H5 的秒开率接入 SSR,因此 SSR 的比例会增加。
但我认为 CSR 依然会是主流,主要是因为两个原因:
- SSR 需要额外的服务器费用,包括服务器的维护、扩容等。这对于中小型公司来说是一个负担。
- SSR 对程序员技术水平要求比 CSR 更高。SSR 需要程序员考虑更多的问题,例如内存泄露。使用 CSR 在用户设备上发生内存泄露,影响有限;但是如果在服务器上发生内存泄露,则是会占用公司的服务器内存,增加额外的成本和风险。
因此,收益丰厚、技术雄厚的公司才愿意使用 SSR。
Monorepo 比例会上升,类 Shadcn UI 组件库也许会兴起
如前所述,H5 的数量膨胀,代码复用就会被着重关注。我猜测更多企业会选择 Monorepo 管理方式。所谓 Monorepo,简单来说,就是将原本应该放到多个仓库的代码放入一个仓库,让它们共享相同的版本控制。这样可以降低代码复用成本。
定制化要求苛刻,我猜测社区中类似 Shadcn UI 的 H5 组件库或许会兴起。现有的 H5 组件库样式太单一,即使是 Shadcn UI,也很难满足国内 H5 的定制化需求。然而,Shadcn UI 的基本思路——「把源码下载到项目」,是解决定制化组件难复用的问题的好思路。因此,我认为类似 Shadcn 的 H5 组件库可能会逐渐兴起。
总结
本文介绍了我认为「百亿补贴」会选用 H5 的三大原因:
- H5 技术已经成熟
- H5 开发成本低
- H5 投放成本低
以及电商巨头对 H5 产生的三个影响:
- 数量膨胀,定制化要求苛刻
- SSR 比例增加,CSR 占据主流
- Monorepo 比例增加,类 Shadcn UI 组件库也许会兴起
总而言之,H5 开发会越来越专业,对程序员要求会越来越高。至于这种情况是好是坏,仁者见仁智者见智,欢迎大家在评论区沟通交流。
拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。
Footnotes
来源:juejin.cn/post/7344325496983732250
前端开发中过度封装的现象与思考
前言
作为公司内的一名高级前端码喽,大大小小也封装过了不少组件和功能,我逐渐意识到封装并非全是优点,也会存在一些不可忽视的潜在劣势。
在项目中,我们急切地对各种功能和 UI 进行封装,却在不经意间忽略了封装可能带来的额外成本与潜在问题。比如,在之前的一个项目中,为了实现一个看似简单的列表展示功能,我将数据获取、渲染逻辑以及交互处理都塞进了一个繁杂的组件中。后续当需要对列表的某一特定功能进行细微调整时,由于封装的过度复杂,修改工作变得极为棘手,耗费了大量时间去梳理内部的逻辑关系。
还有一次我在对一个表单验证功能的封装时,为追求过高的通用性,添加了过多的配置选项和繁杂的验证规则。这不但增加了代码量,还使得新加入团队的成员在使用时感到困惑,理解和运用这个封装的成本大幅提高。如果让我在写标准代码和学习过度封装的组件之间做选择,我绝对毫不犹豫的选择写标准代码。
一、前端功能封装的优势
- 可以提高代码复用性
在众多项目中,常碰到类似的数据请求、表单验证等功能需求。将这些功能封装成独立的函数或模块,能极大提升代码的复用程度。例如,我们成功封装了一个通用的数据获取函数,在不同页面中仅需传入各异的参数,就能顺利获取所需数据,无需反复编写请求逻辑。 - 有效增强代码的可维护性
封装后的功能代码相对独立,当需要对功能进行修改或优化时,只需在封装的模块内操作,不会对其他使用该功能的部分产生任何影响。如此一来,代码的维护工作变得更加清晰、易于掌控。 - 大幅提升代码的可读性
通过为封装的功能赋予清晰、有意义的函数名和详尽的参数说明,其他开发者能够迅速理解其功能和使用方式,这样也极大提高团队协作的效率。写到这里我突然想起曾经在一个屎山项目中看到过的aaaa、Areyouok、jiashizheng等变量和函数名,我花了好久的时间才把它们修改正常...
二、前端功能封装的劣势
- 事极必反
有时为追求极致的封装效果,可能会对一些简单且复用频率不高的功能进行封装,这反倒会增加代码的复杂程度和理解成本。例如,一个仅仅用于计算两个数之和的简单功能,若过度封装,可能会令后续的开发者感到迷茫。
代码示例:
function add(a, b) {
return a + b;
}
// 过度封装
function complexAdd(a, b) {
if (typeof a!== 'number' || typeof b!== 'number') {
throw new Error('输入必须为数字');
}
const result = a + b;
// 一些额外的复杂逻辑
return result;
}
在一个小型项目中,仅仅为了计算两个数字的和,使用了复杂的封装函数
complexAdd
,导致新同事在理解和使用时花费了过多时间,而原本简单的add
函数就能满足需求。 - 可能隐藏底层实现细节
过度封装或许会让使用功能的开发者对其内部实现一无所知。当问题出现时,可能需要耗费更多时间去理解封装内部的逻辑,进而影响问题的排查和解决效率。
三、UI 二次封装的优势
- 成功统一风格和交互
在大型项目中,保障 UI 的一致性至关重要。通过对基础 UI 组件进行二次封装,能够明确统一的样式、交互行为和响应式规则。例如,对按钮组件进行二次封装,设定不同状态下的颜色、尺寸和点击效果。
- 显著提高开发效率
开发人员能够直接运用封装好的 UI 组件,迅速搭建页面,无需在样式和交互的调整上耗费大量时间。
- 方便后期维护和更新
当需要对 UI 进行整体风格的调整或优化时,只需修改封装的组件,所有使用该组件的页面都会自动更新,大幅减少了维护的工作量。
四、UI 二次封装的劣势
- 过度封装的危害
- 增加不必要的代码量和复杂度,致使应用的加载性能降低。例如,一个简单的输入框组件,如果过度封装了很多复杂的逻辑和样式,可能会使代码体积过大。
- 可能引入过多的抽象层次,让代码变得难以理解和调试。复杂的封装结构可能让开发者在排查 UI 问题时感到无从下手。
- 过度复杂的封装在频繁的渲染和更新操作中,可能会导致性能瓶颈,影响用户体验。
代码示例:
// 过度封装的输入框组件
class OverlyComplexInput extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
handleChange = (e) => {
// 复杂的处理逻辑
this.setState({ value: e.target.value });
// 更多的额外操作
}
render() {
return (
<input
value={this.state.value}
onChange={this.handleChange}
// 过多的样式和属性设置
/>
);
}
}
在一个性能要求较高的页面中,使用了过度封装的输入框组件,导致页面加载缓慢,用户输入时出现明显的卡顿。
- 灵活性受限
过于严格的封装可能限制了开发者在特定场景下对 UI 进行个性化定制的能力。有时候,某些页面可能需要独特的样式或交互效果,而过度封装的组件无法满足这些特殊需求。 - 版本兼容性问题
当对封装的 UI 组件进行更新时,可能会与之前使用该组件的页面产生兼容性问题。新的版本可能改变了组件的行为、样式或接口,导致使用旧版本组件的页面出现显示异常或功能失效。
所以在实际的开发过程中,我们需要权衡封装带来的好处和潜在的问题。封装应该是有针对性的,基于实际的复用需求和项目的规模。同时,要保持封装的适度性,避免过度封装带来的负面影响。只有这样,才能真正提高前端开发的效率和质量。
来源:juejin.cn/post/7387731346733121551
部署完了,样式不生效差点让我这个前端仔背锅
大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。
叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动物的好心情都没了。
部署?因为该公司人数比较少,也没有自动化部署操作,只能让前端build一下dist包然后丢给后端,让后端部署,然后该项目也比较急,我刚来,也不好意思@后端,恰巧跟我对接的后端也是刚来不熟悉,所以我就跟负责人继续扯皮拖延时间了。(真是不好意思,假的)↓↓↓
终于经过不断努力(后端),部署好了,但是,我的样式乱了,为什么呢?看了一下网络和源代码,样式文件都请求到了,问了后端,一致说是我前端的问题,我懵了,然后快马加鞭查找问题
解决
ok,那我先看打包的dist文件有样式有问题吗,使用npm安装个 http-server 运行打包后的index.html,运行打开运行后的地址,欧克样式没乱,文件引用也没问题,那到底是什么鬼影响的呢?
在我百思不得其解的时候,我打开了神器--csdn,搜索打包dist部署后样式文件不生效,就遇到了这个神文t.csdnimg.cn/WbQyq
欧克,按照博主说的Nginx没有配置这两个东西而导致的,我有点不确定是不是,有点犹犹豫豫的找到后端说帮我在Nginx配置文件的http加下这两个配置,然后样式就好了,完美!后端由于疏忽,没加上就把问题推给我,结果被我扳回一城。
include mime.types;
default_type application/octet-stream;
- include mime.types;
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
mime.types
文件,Nginx可以识别不同类型的文件并正确地处理它们。 - 示例:假设
mime.types
文件中定义了.html
文件为text/html
类型,Nginx在处理请求时会根据这个定义设置正确的HTTP响应头。
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
- default_type application/octet-stream;
- 这个指令设置了默认的MIME类型。如果Nginx无法根据文件扩展名或其他方式确定响应的MIME类型时,就会使用这个默认类型。
application/octet-stream
是一个通用的MIME类型,表示未知的二进制数据。当服务器无法识别文件类型时,会默认使用这个类型。例如,如果请求的文件没有合适的MIME类型或没有被mime.types
文件中列出,Nginx就会返回application/octet-stream
类型。- 这种设置对于确保未知文件类型的安全传输很有用,因为浏览器通常会下载这些文件而不是尝试在浏览器中打开它们。
总之,添加 include mime.types;
和 default_type application/octet-stream;
配置后,Nginx能够正确地识别和处理CSS文件的MIME类型,从而确保浏览器能够正确加载和应用CSS样式。
所以,前端仔不能只当前端仔,还是要好好学点服务端的知识,不然锅给你,你还认为这是你该背的锅。
以上是开玩笑的描述,只是为了吸引增加阅读量
来源:juejin.cn/post/7388696625689051170
axios中的那些天才代码!看完我实力大涨!
axios的两种调用方式
经常调接口的同学一定非常熟悉aixos下面的两种使用方式:
- axios(config)
// 配置式请求
axios({
method: 'post',
url: '/user/12345',
});
- axios.post(url, config)
// 简洁的写法
axios.post('/user/12345')
不知道各位大佬有没有思考过这样的问题:
axios到底是个什么东西?我们为什么可以使用这两种方式请求接口呢?axios是怎么设计的?
axios原理简析
为了搞明白上面的问题,我们先按照传统思路仿照axios源码实现一个简单的axios。
手写一个简单的axios
创建一个构造函数
function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}
上面的代码中,我们实现了一个基本的Axios类,但它还不具备任何功能。我们现在给它添加功能。
原型上添加方法
Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}
上面的代码中,我们在request属性上创建了一个通用的接口请求方法,get和post实际都调用了request,但内部传递了不同的参数,这和axios(config)、axios.post()有异曲同工之妙。
参考aixos的用法, 现在,我们需要创建实例对象
let aixos = new Axios(config)
创建后的axios包含defaults
和interceptors
属性,其对象原型__proto__
上(指向Axios的prototype)包含request、get及post方法,因此,我们现在可以使用aixos.post()
的方法模拟调用接口了。
但注意,此时aixos只是一个实例对象,不是一个函数!我们似乎也没办法做到改造代码使用aixos(config)
的形式调用接口!
aixos是如何实现的呢?
aixos中的天才想法
为了即能使用axios(config)又能使用axios.get(),axios的核心伪逻辑如下
function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}
Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}
function createInstance(config) {
//注意instance是函数
const instance = Axios.prototype.request;
instance.get = Axios.prototype.get
instance.post = Axios.prototype.post
return instance;
}
let axios = createInstance();
通过上述的伪代码,我们可以知道axios是createInstance()函数的返回值instance。
- instance 是一个函数,因此,axios也是一个函数,可以使用axios(config);
- instance也是一个对象(js万物皆对象),其原型上有get方法和post方法,因此,我们可以使用axios.post()。
我们看看aixos的源码
aixos的源码实现
function createInstance(config) {
//实例化一个对象
var context = new Axios(config); //但是不能直接当函数使用
var instance = Axios.prototype.request.bind(context);
//instance 是一个函数,并且可以 instance({}),
//将Axios.prototype 对象中的方法添加到instance函数中,让instance拥有get、post、request等方法属性
Object.keys(Axios.prototype).forEach(key => {
// console.log(key); //修改this指向context
instance[key] = Axios.prototype[key].bind(context);
})
//总结一下,到此instance自身即相当于Axios原型的request方法,
//然后又给instance的属性添加了上了Axios原型的request、get、post方法属性
//然后调用instance自身或instance的方法属性时,修改了this指向context这个Axios实例对象
//为instance函数对象添加属性 default 与 intercetors
Object.keys(context).forEach(key => {
instance[key] = context[key];
})
return instance;
}
可以说,上面的代码真的写的精妙绝伦啊!
注意这里,为什么要修改this的指向
var instance = Axios.prototype.request.bind(context);
首先,requset 是Axios原型对象上的方法,其方法内部的this指向的是其实例化对象context!
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
因此,如果我们直接使用Axios.prototype.request()
就会出现问题,因为这事reques方法内部的this会指向错误,导致函数不能运行,因此,我们必须将this重新指向其实例化对象。
来源:juejin.cn/post/7387029190620184611
后端同事下班早,前端排序我来搞
写掘金博客有一小段时间了,我发现了一个秘密。文章阅读量小了吧,很心烦,阅读量大了吧,更心烦。很多特别懂特别会的大哥们就会来“指点”我。感谢这些大哥们的“指点”,使我进步。后来我和群里的小伙伴们讨论了一下,为什么掘金文章会有那么多大哥们来“开心指点”呢?大概是这么几种情况:
1 他们爱学习,爱分享,但是即将要被裁员了,所以心情不是太好,怎么办呢?去掘金上指点他们去,让他们知道自己很会很懂;
2 他们在团队中就是翘楚,指点完了团队的人,然后不放心,怕世界不足够完美,反正自己也要被裁员了,有时间,然后补偿没拿够,心情不太好,去给他们指点指点去;
3 他们一直觉得自己不太懂,但是看见文章又想指点指点,所以呢,在家找了3个月工作这段时间,一边学习,然后看大家的文章,学习过程中呢,看哪里觉得不太合适,指点指点,希望趁着阅读量高的文章,好希望有人能发现他们,直接邀约他们入职;
这种人呢,我们总结了一下,他们可以叫“黑哥会”,意思就是黑哥们,比较会,啥都会那种,嗯,希望黑哥会的成员们早日找到心仪的工作,在家闲着不好的。
好啦,在文章正式开始之前呢,告诉大家个好消息,本文点赞,友善评论,友善建议的大哥大姐们,2024年的后半年,一定能够心想事成,工作顺利,家庭和睦,一直到永久。
321... 文本正式开始。
1 未排序的数据
今天早上来了公司,我赶紧喊老张,问:新来的前端妹子这么快就被你搞定啦?听说昨晚你俩10点一起出的公司?是不是,快说。 老张,猛地抬头,问:你咋知道的?我保密工作做这么好。 我说:门口的李大爷说的。你快说说什么情况啊。
老张说:别瞎说,昨天后端下班早,把接口就给妹子了,妹子本来以为调一调接口,传几个参数完事,结果发现后端给的数据没有排序,但看了产品文档,发现,又要根据学生姓名按字母排序,又要根据分数排序,又要根据年龄排序,又要根据日期排序,直接把妹子气的快哭了,所以我就帮他弄了弄。然后就弄到10点了呗,一起出的公司而已,别瞎想。
但是妹子为了感谢我,告诉了我一个好消息,过会儿和你说。我说:你快点说。老张说:你先听我把功能说完,我再告诉你。
你看,后端就一个接口,给的数据大概是这样子:
const users = [
{"name": "小张伟", "age": 19, "score": 55, "dateTime": '2021-03-03 15:33:10'},
{"name": "张三", "age": 22, "score": 65, "dateTime": '2023-03-03 10:10:10'},
{"name": "李四", "age": 30, "score": 87, "dateTime": '2024-04-03 10:10:10'},
{"name": "阿斌", "age": 50, "score": 90, "dateTime": '2021-03-03 10:10:10'},
{"name": "曹小操", "age": 1300, "score": 23, "dateTime": '1021-05-08 10:10:10'},
{"name": "小张灰", "age": 31, "score": 15, "dateTime": '1994-03-04 08:33:10'},
];
2 根据属性排序
这是一个杂乱的json型数组,但是要根据属性进行排序。我们目前做了3种类型的实现
2.1 引入工具库
这里说一个高效便捷功能丰富的前端JS库,首先引入js-tool-big-box工具库。
执行安装命令:
npm install js-tool-big-box
引入dataBox对象,排序的这些公共方法被放到了这个对象下面:
import { dataBox } from 'js-tool-big-box';
2.2 数值型排序
数值型排序呢,就是,你看,age 和 score 都是数值型的,我们把这些归结为一类进行排序。
2.2.1 根据age从小到大的排序
代码如下:
const ageResult1 = dataBox.sortByNumber(users, 'age');
console.log('age是数值型,从小到大,排序后的值为:', ageResult1);
结果如下:
2.2.2 根据age从大到小的排序
代码如下:
const ageResult2 = dataBox.sortByNumber(users, 'age', 1);
console.log('age是数值型,从大到小,排序后的值为:', ageResult2);
结果如下:
2.2.3 根据score从低到高的排序
代码如下:
const ageResult3 = dataBox.sortByNumber(users, 'score');
console.log('score是数值型,从低到高,排序后的值为:', ageResult3);
结果如下:
2.2.4 根据score从高到低的排序
代码如下:
const ageResult4 = dataBox.sortByNumber(users, 'score', 1);
console.log('score是数值型,从大到小,排序后的值为:', ageResult4);
结果如下:
2.3 中文按字母排序
比如我们的姓名,很多时候需要按字母从A到Z来展示,这个时候就可以用下面这个方法来快速实现:
2.3.1 按字母从A到Z排序
代码如下:
const nameResult1 = dataBox.sortByletter(users, 'name');
console.log('比如name,我们按照字母顺序排序后为:', nameResult1);
结果如下:
2.3.2 按字母从Z到A排序
代码如下:
const nameResult2 = dataBox.sortByletter(users, 'name', 1);
console.log('比如name,我们按照字母顺序倒序排序后为:', nameResult2);
结果如下:
2.3.3 注意
需要注意的是,我们这里只是传入了name的属性,如果这个json中有其他中文属性,也是可以使用这个方法进行按字母排序的,很灵活。
2.4 按日期时间排序
比如我们例子中的时间,按时间排序也是非常实用且常见的需求,
2.4.1 按时间从早到晚排序
代码如下:
const timeResult1 = dataBox.sortByTime(users, 'dateTime');
console.log('以时间从早到晚排序后的值为:', timeResult1);
结果如下:
2.4.2 按时间从晚到早排序
代码如下:
const timeResult2 = dataBox.sortByTime(users, 'dateTime', 1);
console.log('以时间从晚到早排序后的值为:', timeResult2);
结果如下:
2.4.3 注意
需要注意的是,我们例子中只是传入了dateTime属性,如果json对象中有其他的是时间格式的属性值,也可以把属性传入,就可以进行字段的属性排序啦,很便捷。
3 最后
把效果展示完了,我赶紧催促老张说:你刚才跟我说的好消息呢?老张悄声说:妹子和门口老大爷,还有咱们公司老板都姓李,你品去吧。妹子跟我说了,她跟她爸爸说:这个季度的优秀就是我。我一听也跟着高兴起来,希望看到这篇文章的大哥大姐们,也都能像老张一样,升职加薪,变得越来越优秀。
最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)
js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)
来源:juejin.cn/post/7384419675073789991