注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

热爱前端,也没能逃过七年之痒

web
大家好,我是杨成功。 从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。 以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工...
继续阅读 »

大家好,我是杨成功。


从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。


以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工程师,但是工作内容已经离前端越来越远了。


以前我觉得做一个骨灰级程序员、掌握各种牛逼的技术是毕生目标;现在我会想人生精彩多样,多尝试一些不一样的事情不也同样有趣?


1-3 年:热爱、探索


我参加工作很早,二十出头。那时候啥也不懂,但是精力旺盛啥也想学,经常写代码到凌晨 2 点也不觉得累。有一部分人选择前端是因为简单,我就是纯粹的喜欢前端。


前端中有很多好玩的东西,比如各种动画、特效,我都非常感兴趣。在工作中常常因为研究出一种“高级”的写法、实现了某个“牛逼”的功能而沾沾自喜。虽然现在看起来很小儿科,但想起来真让人怀念。


我的第一份工作工资很低(<3k),应该比 95% 的前端都低。当时没有经验,心里想着只要能学到东西就成。在那家公司干了一年多,公司用到的技术基本都学了一遍,进步飞快。“又穷又爱”的状态估计以后再也不会有了。


3-5 年:积累、挑战


工作三年多的时候,我换了家公司,带一个前端小团队,每天都扎在项目里。以前总是追求新技术,怎么花哨怎么来。可负责项目后才发现,解决问题和快速产出才是第一位。


当时的前端非常火热,全社会都是跳槽的机会,跳槽等于涨薪。于是面试变得千奇百怪,大家在卷各种原理、源码、八股文,不管面不面试刷题成了必修课。很多开发者们非常讨厌这些东西,但是又不得不去做。


当然也有好处,就是各种新技术层出不穷。虽然很多都是轮子,但确实有不少突破性的技术,帮助传统前端接触到更广的技术面,能做更多的事情。


我没有花大量时间刷面试题,新技术倒是跟了不少,而且很多都用在了项目中。像 JS 原理题、算法题、某些框架的源码之类,我基本没怎么看过;但是像 Node.js、Android、Linux、跨端开发这些,我花了很多的时间研究,因为确实可以解决项目中的问题。


我一直认为我属于“外卷”类型的:Title 一直是前端,但从不认为自己只是一个前端。什么技术都想试试。所以后来我承担过很多攻坚的角色,像服务器、原生 App、音视频等。我发现能让我上头的可能并不是前端,而是搞定一个难题的快感。


得益于这种心态吧,五年内我积累了很多,但我认为收获最大的是习惯了面对挑战。


5-7 年:瓶颈、迷茫


工作五年以上,年龄直逼 30 岁,好像一瞬间就老了,可我总觉得自己还是个孩子。这个时候总会问自己:我的工作有什么意义?我要一直这样下去吗?我想要什么样的生活?


我是在第 6 年的时候感受到了瓶颈。技术方面一直在进步,但对项目的帮助越来越小———项目进入了稳定期。稳定期意味着没有了涨薪的机会,工作重点逐渐从“怎么实现”变成了“怎么汇报”。以前写日报是“汇总成果”,现在变成了“显得有事可做”。


可能任何一家产品成熟的公司都是这样吧,我不习惯,我还在适应阶段。


从今年开始,我最大的迷茫是工作与生活如何平衡。我在北京这几年,大部分精力都扑在了工作上,家人离的很远,每年见个一两次,也没把谈女朋友当回事。想和家人朋友在一块,可工作又不能放弃。成年人说自己不做选择全都要,而我好像只能二选一。


以前一门心思地想靠技术跳槽、进大厂,今年突然觉得没意思。看到很多人被裁员、加班、互卷,我突然想也许现在挺好的呢?双休不加班、领导也 Nice、没有绩效考核、办公室关系也简单。是不是以前自己太浮躁了,没有好好享受当下呢?


所以,要不要继续写代码?还是回老家做别的事?工作上要不要再卷一点?努力攒钱还是趁年轻消费?要不要参加相亲考虑结婚?一连串的问题汹涌而来。


有些问题能想明白,有些问题还是不明白,但更多的是想明白了也做不到。人的成长流失最快的是勇气,可能某天一件意料之外的事情,会让你一下子做出决定。


写了一本书


工作五年之后,我常常会思考一个问题:如果有一天不做程序员了,我还能干什么?


程序员大概都不喜欢社交吧,或者不擅长社交。我特别羡慕大圣老师,他可以把自己的知识通过视频很生动的表达出来。但我就不行,我好像对镜头恐惧,尝试过好多次全身的不自在。


录视频有难度,不过写文章还行。正好积累了很多知识经验,一边总结一边练笔,于是开始写掘金。后来又碰到个机会写书,我就觉得这个更好,可以把这么多年的经验总结浓缩到一本书里。或许可以帮助一些前端朋友快速进阶,或许还能赚点稿费。


这本书名叫 《前端开发实战派》,还在印刷中,估计两个月后就能成书了。


之后怎么走


七年之前觉得我会写代码到 70 岁,直到写不动了为止。七年之后,我最喜欢的工作依然是程序员,但我不再执着于能不能干到 35 岁了。世界还有很多不一样的精彩,我不能把自己困在程序里。


与那些大厂大佬们相比,我赚的不多,心气也不高。没有想过一定要留在大城市,也不觉得以后有了小孩,就一定要奔着“好的教育”和“名校”去卷,太累了。其实只要没有大城市和名校的执念,生活压力也不会那么大。


这样来看,如果有一天我被裁了,其实也没什么可担心的。选择一个离家近的地方,没有大都市的物欲和诱惑,过一些简单轻松的生活,或许并不糟糕。只是身在大城市,面对万千繁华仿佛难以自拔,但你心里好像知道这不是你追求的,却又停不下来。


我有一个预感,可能 30 岁后不再做程序员了,至少不会只埋头钻研技术。做前端这几年让我在各方面成长迅速,不过做久了也有弊端,比如表达能力、社交能力退化,不擅长处理人际关系,不直接接触商业,而这些往往是人生下半场,决定幸福和事业的关键。


但我依然喜欢技术。无论做什么,技术都会是我自己的优势。


我们大老板是技术出身,孩子都上小学了,还经常熬夜帮我们处理技术难题。有次聚会我问他,公司那么多事情要忙,怎么还有精力写代码呢?他说写代码就是我最放松的时候。我不由得一阵佩服,或许这就是技术人的魅力吧。


但在 30 岁之前,我会继续站在技术一线,做一个什么都搞的前端人。


作者:杨成功
来源:juejin.cn/post/7295551745580793919
收起阅读 »

微信内H5页面唤醒App

web
首先,简述一下这个需求的背景,产品希望能够让用户在微信内,打开一个h5页面,然后就能唤醒公司中维护的app,这个是为了能够更好的引流。 唤醒app的三种方案 IOS系统-Universal Link(通用链接) Universal Links可以通过配置指定域...
继续阅读 »

首先,简述一下这个需求的背景,产品希望能够让用户在微信内,打开一个h5页面,然后就能唤醒公司中维护的app,这个是为了能够更好的引流。


唤醒app的三种方案


IOS系统-Universal Link(通用链接)


Universal Links可以通过配置指定域名路径直接唤醒APP,一步到位


具体配置看这篇文章


juejin.cn/post/693761…


遇到的问题:


apple-app-site-association文件放在app域名(假设: my.app.com/)下


{
"applinks": {
"apps": [],
"details": [
{
"appID": "******",
"paths": [ "/abc/*" ]
},
]
}
}

使用Universal Link其实就是跳转到一个页面(中间页),地址:my.app.com/abc/index.h…


根据上面配置,这个地址是已经固定了的,这需要跟app域名保持一致,并且在paths配置里面的目录下,为了能够获取到apple-app-site-association文件


const universalLink = 'https://my.app.com/abc/index.html?redirectUrl=' + window.location.href
location.replace(universalLink);

如果未下载app,则会跳转失败,在中间页中处理,跳转失败后再返回到当前页面。


<script>
function getQueryStringArgs(url, opt) {
const { decode = true, multiple = false } = opt || {};
const args = {};
if (!(typeof url === 'string' && url.includes('?'))) return args;

const arr = url.split('?');
const qs = arr.length === 2 ? arr[1] : '';
if (!(typeof qs === 'string' && qs.length)) return args;

const items = qs.split('&');
for (let i = 0; i < items.length; i++) {
const meta = items[i];
if (!(typeof meta === 'string' && meta.includes('='))) continue;
const item = meta.split('=');
const key = decode ? decodeURIComponent(item[0]) : item[0];
const value = decode ? decodeURIComponent(item[1]) : item[1];
if (Object.prototype.hasOwnProperty.call(args, key) && multiple) {
const temp = args[key];
args[key] = Array.isArray(temp) ? [...temp, value] : [temp, value];
} else {
args[key] = value;
}
}
return args;
}
const { redirectUrl } = getQueryStringArgs(location.href)
if (typeof redirectUrl === 'string' && redirectUrl) {
location.replace(redirectUrl + '?callType=universalLink') // 处理唤醒app失败场景
}
</script>

上面这段逻辑如果直接放在html中,最好先手动转一下ES5语法,然后压缩一下,这样兼容性好,上面这样展示,是为了可读性好。


总结:


ios系统使用Universal Link在微信和浏览器内都能够正常的唤醒App,且兼容性比较好。但是需要注意中间页域名需要跟app域名保持一致;唤醒app的h5链接域名不能跟中间页域名一致。


直接扫二维码进入另一个页面,需要进行点击操作才能跳转,IOS不允许打开页面立刻就跳转。


URL-Schemes


URL scheme是App提供给外部的可以直接操作App的规则。



  • 比如微信提供了打开扫一扫的URL scheme。weixin://dl/scan

  • 比如支付宝提供了转账的URL scheme。alipayqr://platformapi/startapp?saId=20000116

  • 比如知乎提供了打开回答页面的URL scheme。zhihu://answers/{id}


如何找到某个app的URL Scheme呢?可以看下面这篇文章


zhuanlan.zhihu.com/p/53439246


安卓唤醒app呢,就是使用这种方式


比如:安卓开发提供的是


那跳转的链接是什么样的呢?


const schemeURL = 'myapp://www.myapp.apk'
window.href = schemeURL;

如何判断唤醒失败呢?


没有什么好办法来判断,后面只能触发了唤醒操作之后,监听页面几秒之后是否隐藏来判断,目前默认是2秒


export function getSupportedProperty() {
let hidden;
let visibilityChange;

if (typeof document.hidden !== 'undefined') {
// Opera 12.10 and Firefox 18 and later support
hidden = 'hidden';
visibilityChange = 'visibilitychange';
// @ts-ignore
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
// @ts-ignore
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}

return {
hidden,
visibilityChange,
};
}
/**
* 判断页面是否隐藏(进入后台)
*/

export function isPageHidden() {
const ob = getSupportedProperty();
const hidden = ob?.hidden;
if (typeof hidden === 'undefined') return false;
// @ts-ignore
return document[hidden];
}
/**
* 检测是否唤端成功
* 在唤起执行后,当前页面调用此方法根据页面隐藏变化检测是否唤醒成功
* @param {number} timeout 定时时间,默认2秒
* @return {Object} Promise对象
*/

export function checkOpen(timeout = 2000) {
return new Promise((resolve, reject) => {
const ob = getSupportedProperty();
const visibilityChange = ob?.visibilityChange;

const check = () => {
const pageHidden = isPageHidden();
if (pageHidden) {
resolve(); // 页面被隐藏,说明唤醒成功
} else {
reject(new Error('唤醒超时'));
}
};
const timer = setTimeout(() => {
check();
}, timeout);

const fn = () => {
if (typeof visibilityChange !== 'undefined') {
document.removeEventListener(visibilityChange, fn);
} else {
window.removeEventListener('pagehide', fn);
}
check(); // 唤醒执行后,立马触发页面隐藏变化,可检测是否唤醒成功
clearTimeout(timer); // 未到达指定时间,页面隐藏变化,清除定时器
};

if (typeof visibilityChange !== 'undefined') {
document.addEventListener(visibilityChange, fn);
} else {
window.addEventListener('pagehide', fn);
}
});
}

总结:


安卓使用URL Schemes在微信中是不能跳转的,在浏览器中是能够正常拉起。


微信开放标签


由于在微信环境内,所以可以使用微信提供的能力来唤醒app,微信内禁止使用URL Schemes唤醒app,其实就是微信的一种保护机制。


微信文档:


developers.weixin.qq.com/doc/oplatfo…



如上图,使用这个功能,有很多限制,而且需要配置,但是为了安卓用户成功引流,产品还是要求使用这个功能。


微信配置


1.关联App-微信开发平台


微信开发平台配置关联App,关联App需要appId,已经有App的域名


微信开发平台地址: open.weixin.qq.com/



2.H5页面域名配置-微信公众平台


JS安全域名需要配置当前h5页面的域名


微信公众号地址: mp.weixin.qq.com/



3.初始化微信SDK,需要获取签名


微信开发SDK文档


developers.weixin.qq.com/doc/offiacc…


这需要后端开发接口, 去获取签名



使用微信开放标签说明:


developers.weixin.qq.com/doc/offiacc…


async getWxSignatureData() {
const url = window.location.href.split('#')[0];
const res = await getJsapiSignParamers(url);
const { appId, signature, timestamp, nonceStr } = res.data;
wx.config({
debug: false,
appId: appId,
timestamp: timestamp,
nonceStr: nonceStr,
signature: signature,
jsApiList: ['showOptionMenu'], // 必填,故使用一个非实际使用的api用于填充
openTagList: ['wx-open-launch-app'], // 可选,需要使用的开放标签列表
});

wx.ready(() => {
console.info('wx sdk ready');
console.info('调用接口初始化wx sdk 成功');
this.initWxSDKStatus = 'success';
});

wx.error(res => {
console.error('调用接口初始化wx sdk 失败', res);
this.initWxSDKStatus = 'fail';
});
},

接口返回的就是这样的数据结构



只有这样才能正常初始化微信的SDK,只有正常初始化SDK才能够使用微信开放标签的能力。


然后后端开发的时候要注意:签名需要后端配置白名单ip,文档说明如下:


developers.weixin.qq.com/doc/offiacc…



安卓手机,如果出现唤醒app之后,打开了应用,但是并未成功唤起,那是因为Android应用有要求,需要安卓开发兼容一下就行了~



微信环境内场景


接下来就分析一下,在微信中有几种分享的场景:


1.微信好友之间链接分享



这种方式,使用微信标签是不能唤醒App的,除非是在关注公众号里面,这个公众号就是上面绑定了JS安全域名的公众号



这样点击这个链接就能正常用微信标签唤醒


2.微信好友之间卡片分享



这种点击打开是能够正常唤醒App的,而且不需要使用公众号,但是这种分享有限制,需要打开页面点击右上角分享给其他好友会带上卡片形式,如果在浏览器中就只是复制链接了,微信不会自动识别成卡片


而且这个分享其实就是微信的一个功能


developers.weixin.qq.com/minigame/de…


3.长按识别二维码识别H5链接



这种也能正常唤醒App,而且不需要关注公众号,也很方便,不需要将链接分享给其他人,只需要将唤醒App的链接做出二维码就行了。


全部流程图


无标题-2023-11-05-1641.png


作者:0522Skylar
来源:juejin.cn/post/7297526380333400083
收起阅读 »

偷偷给网站写了一个霓虹风格计数器

web
阅读原文,体验更佳 👉 http://www.xiaojun.im/posts/2023-… 有很长一段时间,我都想在博客中集成拟物化的访问计数器用于增加一些趣味性,可是我这网站一开始是纯静态的,没用到任何数据库,所以后边不了了之,但最近我在博客中赋予了一些...
继续阅读 »

2023-10-28-retro-hit-counter.webp


阅读原文,体验更佳 👉 http://www.xiaojun.im/posts/2023-…




有很长一段时间,我都想在博客中集成拟物化的访问计数器用于增加一些趣味性,可是我这网站一开始是纯静态的,没用到任何数据库,所以后边不了了之,但最近我在博客中赋予了一些动态能力,这个想法随之也就又浮现了出来。



这个创意最初来自大佬 Joshua Comeau 开源的 react-retro-hit-counter,但后续我产生了自己的一些想法。




本教程不会涉及任何关于数据库的东西,我假设你已经准备了一个数字,不关心你的数据来源,这里就以 1024 来做演示啦~



认识七段数码管


最初我只想实现一个类似计算器那种数字显示效果,它专业点叫做七段数码管(Seven-segment display),你可以在 wikipedia 上见到具体介绍,它一般长下边这种样子,地球人都见过:


image.png


这种形态还是比较好处理的,让我们先实现这个效果,最终要实现的霓虹灯效果也是以此为基础才行。



以下所有组件皆是用 tailwindcss + react 编写,为了教程简练省略了部分代码,具体请阅读源码



SevenSegmentDisplay 组件开发


开发之前让我们先分析该组件有哪些部分构成,它可以拆分为哪些子组件?



  • 入口组件,也就是父组件,我们将它命名为 SevenSegmentDisplay.jsx

  • 数字单元组件,我们将它命名为 Digit.jsx

  • 数字单元的片段,每个数字有 7 个片段,我们将它命名为 Segment.jsx


SevenSegmentDisplay


作为入口组件,它负责接收所有的 props 配置,并且将传入的 value 分解为单个数字后传给 Digit 组件。


import React, { useMemo } from 'react'
import Digit from './Digit'

const SevenSegmentDisplay = props => {
const {
value, // 要展示的数字
minLength = 4, // 最小长度,不足则前补 0
digitSize = 40, // 数字大小(高度)
digitSpacing = digitSize / 4, // 数字之间的间距
segmentThickness = digitSize / 8, // 片段厚度
segmentSpacing = segmentThickness / 4, // 片段之间的缝隙大小
segmentActiveColor = '#adb0b8', // 片段激活时候的颜色
segmentInactiveColor = '#eff1f5', // 片段未激活时候的颜色
backgroundColor = '#eff1f5', // 背景色
padding = digitSize / 4, // 整个组件的 padding
glow = false, // 微光效果,其实就是阴影效果
} = props

// 将传入的 number 类型数字转为 string 并且根据 minLength 传入的长度进行前补 0
const paddedValue = useMemo(() => value.toString().padStart(minLength, '0'), [value, minLength])
// 将补 0 后的数字转为单个字符
const individualDigits = useMemo(() => paddedValue.split(''), [paddedValue])

return (
<div
className="inline-flex items-center justify-between"
style={{ padding, backgroundColor, gap: digitSpacing }}
>

{individualDigits.map((digit, idx) => (
<Digit
key={idx}
value={Number(digit)}
digitSize={digitSize}
segmentThickness={segmentThickness}
segmentSpacing={segmentSpacing}
segmentActiveColor={segmentActiveColor}
segmentInactiveColor={segmentInactiveColor}
glow={glow}
/>

))}
</div>

)
}

export default SevenSegmentDisplay

Digit


一个 Digit 包含 7 个 Segment,通过控制不同 Segment 的点亮状态,便可以模拟数字显示。


import React from 'react'
import Segment from './Segment'

// Segment 排布规则
//
// A
// F B
// G
// E C
// D
//

const segmentsByValue = {
[0]: ['a', 'b', 'c', 'd', 'e', 'f'],
[1]: ['b', 'c'],
[2]: ['a', 'b', 'g', 'e', 'd'],
[3]: ['a', 'b', 'g', 'c', 'd'],
[4]: ['f', 'g', 'b', 'c'],
[5]: ['a', 'f', 'g', 'c', 'd'],
[6]: ['a', 'f', 'g', 'c', 'd', 'e'],
[7]: ['a', 'b', 'c'],
[8]: ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
[9]: ['a', 'b', 'c', 'd', 'f', 'g'],
}

const isSegmentActive = (segmentId, value) => segmentsByValue[value].includes(segmentId)

const segments = ['a', 'b', 'c', 'd', 'e', 'f', 'g']

const Digit = props => {
const { value, digitSize } = props

return (
<div className="relative w-6 h-8" style={{ width: digitSize * 0.5, height: digitSize }}>
{segments.map(segment => (
<Segment
key={segment}
segmentId={segment}
isActive={isSegmentActive(segment, value)}
segmentThickness={segmentThickness}
segmentSpacing={segmentSpacing}
segmentActiveColor={segmentActiveColor}
segmentInactiveColor={segmentInactiveColor}
glow={glow}
/>

))}
</div>

)
}

export default Digit

Segment


根据 segmentId 以及激活状态用 SVG 渲染出对应的 Segment,这是一个不复杂但是比较繁琐的工作 🤖。


import React, { useMemo } from 'react'
import color from 'color'

const Segment = props => {
const {
segmentId,
isActive,
digitSize,
segmentThickness,
segmentSpacing,
segmentActiveColor,
segmentInactiveColor,
glow,
} = props
const halfThickness = segmentThickness / 2
const width = digitSize * 0.5

const segments = {
a: {
top: 0,
left: 0,
},
b: {
top: 0,
left: width,
transform: 'rotate(90deg)',
transformOrigin: 'top left',
},
c: {
top: width * 2,
left: width,
transform: 'rotate(270deg) scaleY(-1)',
transformOrigin: 'top left',
},
d: {
top: width * 2,
left: width,
transform: 'rotate(180deg)',
transformOrigin: 'top left',
},
e: {
top: width * 2,
left: 0,
transform: 'rotate(270deg)',
transformOrigin: 'top left',
},
f: {
top: 0,
left: 0,
transform: 'rotate(90deg) scaleY(-1)',
transformOrigin: 'top left',
},
g: {
top: width - halfThickness,
left: 0,
},
}

// a, d
const path_ad = `
M ${segmentSpacing} ${0}
L ${width - segmentSpacing} 0
L ${width - segmentThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


// b, c, e, f
const path_bcef = `
M ${segmentSpacing} ${0}
L ${width - halfThickness - segmentSpacing} 0
L ${width - segmentSpacing} ${halfThickness}
L ${width - halfThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


// g
const path_g = `
M ${halfThickness + segmentSpacing} ${halfThickness}
L ${segmentThickness + segmentSpacing} 0
L ${width - segmentThickness - segmentSpacing} 0
L ${width - halfThickness - segmentSpacing} ${halfThickness}
L ${width - segmentThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


const d = useMemo(
() =>
({
a: path_ad,
b: path_bcef,
c: path_bcef,
d: path_ad,
e: path_bcef,
f: path_bcef,
g: path_g,
}[segmentId]),
[path_ad, path_bcef, path_g, segmentId],
)

return (
<svg
className="absolute"
style={{
...segments[segmentId],
// 此处用到了 color 它可以很方便的对颜色进行调整
filter:
isActive && glow
? `
drop-shadow(0 0 ${segmentThickness * 1.5}px ${color(segmentActiveColor).fade(0.25).hexa()})
`
: 'none',
zIndex: isActive ? 1 : 0,
}}
width={width}
height={segmentThickness}
viewBox={`0 0 ${width} ${segmentThickness}`}
xmlns="http://www.w3.org/2000/svg"
>

<path fill={isActive ? segmentActiveColor : segmentInactiveColor} d={d} />
</svg>

)
}

export default Segment

基础效果展示


到此,基础的显示组件已经完成了,让我们测试一下显示效果:


www.xiaojun.im_posts_2023-10-28-retro-hit-counter.png


这是它的配置参数 👇


<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

粗略一看还不错,但这与霓虹效果还相差甚远,因为它看起来有些扁平,边缘过于“锐利”,不够真实,所以接下来的目标是要把它变得更真实拟物一些。



如果你不需要霓虹效果,其实到这一步就足够了 😣,在我的网站中浅色模式也是使用的扁平风格,只有在切换到深色模式才会显示为拟物风格,算是一个小小的彩蛋吧。



霓虹灯效果


先分析一下为什么上边的样式看上去不够真实?



  1. 也许是曝光问题?真实世界中发光物本身相对于它的边缘来说看上去会更亮、更白,并且会稍微模糊一些。

  2. 很多情况下发光源做不到均匀照射到所有地方,所以会产生一片区域亮一片区域稍暗的效果,如果你留意过,很多透字键盘背光灯就是这样。


基于以上两点,接下来就想办法用 CSS 将它模拟的更真实一些。


让我们在 SevenSegmentDisplay 组件的基础上再封装一个 NeonHitCounter 组件。


模拟曝光过度效果


我们可以使用 CSS 中的 backdrop-filter 属性模拟过曝效果。


const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"></div>
</div>

)
}

export default NeonHitCounter

在上边代码中我们新建了一个 div 盖在 SevenSegmentDisplay 上边并使用 badckdrop-filter 使组件变亮变模糊,看上去效果已经好了不少。


image.png


模拟亮度不均匀效果


让我们将组件中间部分变得更亮,用于模拟亮度不均匀的效果。我们可以用 radial-gradient 创建一个白色径向渐变盖在它上边,然后通过 mix-blend-mode 来控制混合模式,这里用 overlay 比较合适。



有关 mix-blend-mode 的更多详细介绍你可以参考这篇文章



const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div
className="absolute inset-0 z-10 mix-blend-overlay pointer-events-none"
style={{
// 通过 luminosity 获取颜色相对亮度如果一个颜色很亮我们则减少亮度增益
background: `radial-gradient(rgba(255, 255, 255, ${
1 - color('#ff5e00').luminosity()
}), transparent 50%)`,
}}
>
</div>
<div className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"></div>
</div>

)
}

export default NeonHitCounter

在上边代码中又创建了一层 div,它利用 radial-gradient + mix-blend-mode: overlay 实现局部颜色增亮,并且根据颜色相对亮度动态判断增益比例,看起来是不是更真实了 👇


image.png



了解相对亮度 👉 developer.mozilla.org/en-US/docs/…



模拟玻璃质感


为了模拟透明玻璃质感,我用 Figma 画了一个 SVG 背景(也可以用 CSS 实现,我偷懒了),另外又用 conic-gradient 实现了 4 颗螺丝效果。


<svg width="76" height="38" viewBox="0 0 76 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.68" clip-path="url(#clip0_467_36)">
<rect width="76" height="38" fill="url(#paint0_radial_467_36)"/>
<rect width="76" height="38" fill="white" fill-opacity="0.01"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-80.0879 0H191.953V272.041H-80.0879V0ZM54.9326 263.211C125.178 263.211 182.124 206.266 182.124 136.021C182.124 65.7744 125.178 8.8291 54.9326 8.8291C-15.3135 8.8291 -72.2588 65.7744 -72.2588 136.021C-72.2588 206.266 -15.3135 263.211 54.9326 263.211Z" fill="url(#paint1_linear_467_36)"/>
</g>
<defs>
<radialGradient id="paint0_radial_467_36" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(38 19) scale(38 19)">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white" stop-opacity="0.05"/>
</radialGradient>
<linearGradient id="paint1_linear_467_36" x1="-8.40528" y1="-21.8896" x2="68.8142" y2="-4.89117e-06" gradientUnits="userSpaceOnUse">
<stop offset="0.199944" stop-color="white" stop-opacity="0.26"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_467_36">
<rect width="76" height="38" fill="white"/>
</clipPath>
</defs>
</svg>

import React from 'react'
import SevenSegmentDisplay from '@/components/SevenSegmentDisplay'
import clsx from 'clsx'
import color from 'color'

const Screw = props => {
const { className } = props

return (
<div
className={clsx(className, 'w-[5px] h-[5px] rounded-full ring-1 ring-zinc-800')}
style={{ background: `conic-gradient(#333, #666, #333, #666, #333)` }}
>
</div>

)
}

const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div
className="absolute inset-0 z-10 mix-blend-overlay pointer-events-none"
style={{
background: `radial-gradient(rgba(255, 255, 255, ${
1 - color('#ff5e00').luminosity()
}), transparent 50%)`,
}}
>
</div>
<div
className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"
style={{
backgroundImage: 'url(/hit-counter-glass-cover.svg)',
backgroundSize: 'cover',
backgroundPosition: 'center',
boxShadow: `
0 0 1px rgba(255, 255, 255, 0.1) inset,
0 1px 1px rgba(255, 255, 255, 0.1) inset
`,
}}
>

<Screw className="absolute left-1 top-1 -rotate-45" />
<Screw className="absolute left-1 bottom-1 rotate-45" />
<Screw className="absolute right-1 top-1 rotate-45" />
<Screw className="absolute right-1 bottom-1 -rotate-45" />
</div>
</div>

)
}

export default NeonHitCounter

大功告成 ✨


image.png


作者:xiaojundebug
来源:juejin.cn/post/7297487761615552564
收起阅读 »

听说前端出大事儿了

web
最近这两天,在前端圈最火的图片莫过于下面这张了。 这是一段 React 代码,就算你完全没用过 React 也没关系,一眼看过去就能看到其中最敏感的一句代码,就是那句 SQL 。 咱们把这端代码简化一下,大概就是下面这个样子。

最近这两天,在前端圈最火的图片莫过于下面这张了。



这是一段 React 代码,就算你完全没用过 React 也没关系,一眼看过去就能看到其中最敏感的一句代码,就是那句 SQL 。
咱们把这端代码简化一下,大概就是下面这个样子。




意思就是在页面上点击一个叫做「提交」的按钮,触发一个 formAction(提交表单)的动作。这有点看到了当年 JSP 和 PHP 的味道了。这还不是最神奇的,最厉害的是提交表单要执行的动作不是一个接口请求,而是直接执行一条 SQL 。使用 use server标签,标示这是一个服务端端执行的方法。



一时间竟分不出这到底是前端还是后端了。


这么发展下去,React 就是妥妥的全栈语言了。此时的 PHP 在旁边笑而不语,还说我不是世界上最好的语言,你们终究还是会活成我的样子。



自从前后端分离以来,前端框架可谓是百花齐放,一片繁荣。最早的是 Angular,然后就是 React 和 Vue,到现在基本都是 Vue 和 React 的天下了。


如果你用过原生的 JavaScript 或者 JQuery,那就能感受到 React 或者 Vue 的出现,完全改变了前端的开发方式。


React 目前的最新版本是 18,支持 ES(ECMAScript) 和TS(TypeScript),除了画界面和写CSS之外,完全可以把它当做一个面向对象的语言工具使用。


这次支持执行执行后端 SQL 的特性是 Next.js 开放的,Next.js 是 在React 框架上再次高度封装的一个框架。有点像 Spring Boot与 Spring 的关系,Spring 好比是 React,Spring Boot 就是 Next.js。


本来好好的前端,为什么要直接支持写 SQL 呢,这也并不是无迹可寻的。前两年,React 就推出了React Server Components 。大致的意思就是说这是一种服务器端组件,为了提高性能,由服务器直接渲染,渲染出来的结果通过元数据的形式发给前端 React,React 拿到元数据后与现有的 UI 树合并,最终由浏览器渲染。


React 官方是大力推荐 Next.js 的,有了官方推荐加上本身已经支持的服务器端组件,Next.js 不知道是出于什么样的目的,竟然直接支持执行服务端方法了。之前要通过 HTTP 请求,现在直接就跳过这一步了。


说实话,站在一个前端框架的视角上,加上我本身是一个后端开发,我是有一点看不懂这个操作了。服务端组件还能理解,毕竟开发效率和性能要兼顾,这无可厚非。


但是直接支持服务端执行,是技术的轮回(照着PHP的方向)还是技术的变革呢,此时的 Next.js 就像是一个站在十字路口的汽车,油都加满了,就看各位开发者驾驶员开着它往哪边走了。


反正依我看来,我是觉得前端框架越简单越好。原因很简单,搞这么复杂,我都快不会用了。



不光是我看不懂,毕竟咱是个后端外行,不是专业的。但是前端同学也是一片调侃,调侃的大致意思就是 React Next.js 啥都能干,既然连后端都能整了,那其他的也能全栈了。


比如有人调侃给 Next.js 赋能 AI,使用 use ai,直接 prompt 编程了。



还有赋能 k8s 的



以及赋能二进制编程的



最厉害的,还有赋能删库跑路的。



调侃归调侃,既然口子已经开了,就会有过来吃螃蟹的人,至于之后会变成什么样子,只能拭目以待了。


作者:古时的风筝
来源:juejin.cn/post/7296384298902929417

SQL中的DDL(数据定义)语言:掌握数据定义语言的关键技巧!

DDL(Data Definition Language),是用于描述数据库中要存储的现实世界实体的语言。前面我们介绍了数据库及SQL语言的相关概念和基础知识,本篇文章我们来重点讲述DDL(数据定义语言的语法格式)的相关内容以及DDL的常用语句。一、DDL介绍...
继续阅读 »

DDL(Data Definition Language),是用于描述数据库中要存储的现实世界实体的语言。

前面我们介绍了数据库及SQL语言的相关概念和基础知识,本篇文章我们来重点讲述DDL(数据定义语言的语法格式)的相关内容以及DDL的常用语句。

一、DDL介绍

这里我们先回顾一下前面讲过的SQL语言的概念:SQL(Structured Query Language),即结构化查询语言,是在关系型数据库(诸如Mysql、SQL Server、Oracle等)里进行相关操作的标准化语言,可以根据sql的作用分为以下几种类型:

下面再来看DDL语言是什么:

DDL,全称为Data Definition Language,即数据定义语言。它是SQL语言的重要组成部分,主要用于定义和管理数据库的结构。

二、DDL语言能做什么?

通过DDL,我们可以创建、修改和删除数据库、表、视图等对象。

创建数据库: 使用CREATE DATABASE语句,我们可以创建一个新的数据库。

删除数据库: 使用DROP DATABASE语句,我们可以删除一个已经存在的数据库。

创建表: 使用CREATE TABLE语句,我们可以在数据库中创建新的表。

** 删除表:**使用DROP TABLE语句,我们可以删除一个已经存在的表。

修改表结构: 使用ALTER TABLE语句,我们可以修改已经存在的表的结构,如添加、删除或修改字段等。

三、什么是数据库对象

数据库对象是数据库的组成部分,常见的有以下几种:

1、表(Table )

数据库中的表与我们日常生活中使用的表格类似,它也是由行(Row) 和列(Column)组成的。

Description

列由同类的信息组成,每列又称为一个字段,每列的标题称为字段名。行包括了若干列信息项。一行数据称为一个或一条记录,它表达有一定意义的信息组合。一个数据库表由一条或多条记录组成,没有记录的表称为空表。每个表中通常都有一个主关键字,用于唯一确定一条记录。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

2、索引(Index)

索引是根据指定的数据库表列建立起来的顺序。它提供了快速访问数据的途径,并且可监督表的数据,使其索引所指向的列中的数据不重复。

Description

3、视图(View)

视图看上去同表似乎一模一样,具有一组命名的字段和数据项,但它其实是一个虚拟的表,在数据库中并不实际存。视图是由查询数据库表产生的,它限制了用户能看到和修改的数据。

Description

4、图表(Diagram)

图表其实就是数据库表之间的关系示意图。利用它可以编辑表与表之间的关系。

Description

5、缺省值(Default)

缺省值是当在表中创建列或插入数据时,对没有指定其具体值的列或列数据项赋予事先设定好的值。

Description

6、规则(Rule)

规则是对数据库表中数据信息的限制,它限定的是表的列。

7、触发器(Trigger)

触发器是一个用户定义的SQL事务命令的集合。当对一个表进行插入、更改、删除时,这组命令就会自动执行。

Description

8、存储过程(Stored Procedure)

存储过程是为完成特定的功能而汇集在一起的一组SQL 程序语句,经编译后存储在数据库中的SQL程序。

Description

9、用户(User)

所谓用户就是有权限访问数据库的人。

四、DDL常用语句

4.1 数据库相关

1)查看所有数据库

格式:show databases;

2)创建数据库

格式:create database 数据库名 charset=utf8;

举例:

#创建一个名为test的数据库
#create database 库名;
create database test;
#创建一个名为test的数据库并指定字符集和编码格式
create database test default charset utf8 collate utf8_general_ci;

3)查看数据库信息

格式:show create database 库名;

**4)删除数据库 **

格式:drop database 数据库名;

举例:

#删除test数据库
drop database test;

5)使用数据库

执行表相关和数据库相关的SQL语句之前必须先使用了某个数据库

格式:use 数据库名;

举例:

use test;

4.2 表相关

1)创建表

格式:create table 表名(字段1名 类型,字段2名 类型,…)

举例:

create table person(name varchar(50),age int);
create table person(name varchar(50),age int);
create table stydent(name varchar(50),chinese int ,math int, english int)charset=utf8;
创建一个员工表emp 保存名字,工资和工作
create table emp(name varchar(50),salary int,job varchar(20));

2)查询所有表

格式:show tables;

3)查询表信息

格式:show create table 表名;

举例:

show create table emp;

4)查询表字段

格式:desc 表名; (description)

5)修改表名

格式:rename table 原名 to 新名;

举例:

rename table stydent to stu;

6)删除表

格式:drop table 表名;

4.3 alter表操作相关

1)添加表字段

格式(最后面添加):alter table 表名 add 字段名 类型;

格式(最前面添加):alter table 表名 add 字段名 类型 first;

在xxx字段后面添加:alter table 表名 add 字段名 类型 after 字段名;

举例:

alter table emp add gender gender varchar(5);
alter table emp add id int first;
alter table emp add dept varchar(20) after name;

2)删除表字段

格式:alter table 表名 drop 字段名;

举例:

alter table emp drop dept;

3)修改表字段

格式:alter table 表名 change 原名 新名 新类型;

举例:

alter table emp change job dept varchar(10);

4)修改列属性

格式:alter table 表名 modify 列名 新列属性

举例(只有MySQL是这样写的):

alter table student modify age int;

关于DDL常用语句就讲这么多了,尽管现在有许多图形化工具可以替代传统的SQL语句进行操作,同时在Java等语言中也可以使用数据库,但对于SQL各类语句的了解仍然非常重要。

收起阅读 »

作为前端,这几个关于console的小知识点,你知道吗

web
在我们实际开发中呢,经常会遇到把一个变量打印到控制台,看一下它的结果的情况 就比如下面这个形式的对象: const obj = { "err_no": 0, "err_msg": "success", "data": { "user_ba...
继续阅读 »

在我们实际开发中呢,经常会遇到把一个变量打印到控制台,看一下它的结果的情况



就比如下面这个形式的对象:


const obj = {
"err_no": 0,
"err_msg": "success",
"data": {
"user_basic": {
"university": {},
"major": {}
},
"user_counter": {},
"user_growth_info": {}
}
}

我们一般会使用 console.log() 看一下它的值: console.log(obj)


image.png


我们点击这个按钮可以一层层的展开这个对象:


image.png


除了 console.log() 外,根据实际情况我们还可以使用下面几种。


console.dir


我们还可以使用 console.dir()。在使用它输出 JS 数据类型数据的时候它和使用 console.log() 的效果差不多:


image.png


我们展开这个对象,可以查看我们想看的数据:


image.png


当我们想打印出个某个 DOM 对象时就不一样了,使用 console.log() 输出的是这个 DOM 元素:


image.png


使用 console.dir() 输出的是这个 DOM 对象:


image.png


JSON.stringify()


我们还可以使用 console.log() 配合 JSON.stringify()


console.log(JSON.stringify(obj, null, 4))

运行效果如下:


image.png


可以看到,这里以字符串的形式将这个对象输出在了控制台。


console.table


我们还可以使用 console.table(),它会以一种表格的形式来输出结果:


image.png


可以看到,这样看着还是很整齐的。


如果我们要打印的是一个数组的话,使用 console.table() 输出数据,看起来会更方便一些:


const arr = ['a', 'b', 'c']
console.table(arr)

image.png


还有,输出多个数据的使用使用 console.table() 也有利于查看数据,如:


const a = 'a', b = 'b', c = 'c'
console.table({a, b, c})

效果如下:


image.png


consle.time 和 console.timeEnd


还有,在我们开发的过程中,有时候需要去看一段代码执行到底消耗了多少时间,我们可以使用 console.time()consle.timeEnd() 包裹想要测试运行时间的代码,比如下面这段代码:


function test() {
for (let i = 0; i < 10000; i++) { }
}

console.time()
test()
console.timeEnd()

运行代码,可以看到控制台输出了这段代码在本机大概的一个运行时间:


image.png



作者:程序员黑豆
来源:juejin.cn/post/7292969465298567187
收起阅读 »

你知道 XHR 和 Fetch 的区别吗?

web
现如今,网站开发普遍采用前后端分离的模式,数据交互成为了不可或缺的关键环节。在这个过程中,XHR 和 Fetch API 是两种最常见的方法,用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的...
继续阅读 »

现如今,网站开发普遍采用前后端分离的模式,数据交互成为了不可或缺的关键环节。在这个过程中,XHRFetch API 是两种最常见的方法,用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的新兴标准。接下来,我们将一同深入学习它们的使用方法和适用场景。


XMLHttpRequest


XMLHttpRequest,通常简称为 XHR。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。XMLHttpRequest 在 AJAX 编程中(比如 jquery)被大量使用。



AJAX :异步 JavaScript 和 XML。许多人容易把它和 jq 的 ajax 混淆。它是一个技术统称,本身不是一种技术。



特点



  1. 异步请求:XHR 允许进行异步请求,它可以在后台执行,而不会阻止页面的其他操作。

  2. 支持跨域请求:通过服务器端设置允许跨域请求,从不同域的服务器获取数据。

  3. 事件驱动:提供了 onloadonerroronprogress 等一系列事件来监听请求的状态变化。

  4. 灵活性:提供了对请求头、响应头以及请求方法的完全控制,使其非常灵活。


工作原理


XHR 的工作原理主要为:



  1. 创建 XHR 对象实例:通过new XMLHttpRequest()创建一个 XHR 对象。

  2. 配置请求:使用open()方法设置请求方法(GET、POST 等)、URL,以及是否要异步执行请求。

  3. 设置回调函数:设置事件处理程序来处理请求完成、成功、失败等不同的状态。

  4. 发起请求:使用send()方法发送请求。

  5. 处理响应:在事件处理程序中处理响应数据,通常使用responseTextresponseXML来访问响应内容。


// 创建一个新的XHR对象
const xhr = new XMLHttpRequest();

// 配置请求
xhr.open("GET", "https://api.baidu.com/test", true);

// 设置响应处理函数
xhr.onload = function() {
if (xhr.status === 200) {
// 请求成功
const responseData = xhr.responseText;
console.log("成功获取数据:", responseData);
} else {
// 请求失败
console.error("请求失败,状态码:" + xhr.status);
}
};

// 发起请求
xhr.send();

XHR 的响应处理通常在onreadystatechange事件处理程序中完成。在上面的例子中,我们等待 XHR 对象的状态变为 4(表示请求完成)并且 HTTP 状态码为 200(表示成功响应)时,解析响应数据。


Fetch API


Fetch 是一种现代的数据网络请求 API,它旨在解决 XHR 的一些问题,提供了更强大、更灵活的方式来处理 HTTP 请求。可以理解为 XMLHttpRequest 的升级版。


特点



  1. Promise 风格:Fetch API 使用 Promise 对象来处理异步请求,使代码更具可读性和可维护性。

  2. 更简单的语法:相较于 XHR,Fetch API 的语法更加简单明了,通常只需要几行代码来完成请求。

  3. 默认不接受跨域请求:为了安全性,Fetch API 默认不接受跨域请求,但可以通过 CORS(跨域资源共享)来进行配置。

  4. 更现代的架构:Fetch API 是建立在 PromiseStream 之上的,支持更灵活的数据处理和流式传输。


工作原理


Fetch 的工作原理主要为:



  1. 使用fetch()函数创建请求:传入要请求的 URL,以及可选的配置参数,例如请求方法、请求头等。

  2. 处理响应:fetch()返回一个 Promise,您可以使用.then()链式调用来处理响应数据,例如使用.json()方法解析 JSON 数据或.text()方法获取文本数据。

  3. 错误处理:您可以使用.catch()方法来捕获任何请求或响应的错误。

  4. 使用async/await:如果需要,您还可以使用async/await来更清晰地处理异步操作。


Fetch API 的特性和简单的语法使它在许多前端项目中成为首选工具。然而,它也有一些限制,例如不支持同步请求,因此需要谨慎使用。


fetch("https://api.baidu.com/test")
.then(response => {
if (!response.ok) {
throw new Error("请求失败,状态码:" + response.status);
}
return response.json();
})
.then(data => {
// 请求成功,处理响应数据
console.log("成功获取数据:", data);
})
.catch(error => {
// 请求失败,处理错误
console.error(error);
});

XHR 和 Fetch 的对比


XHR 和 Fetch 都用于进行 HTTP 请求,但它们之间存在一些关键区别:



  • 语法: Fetch 使用 Promise,更直观和易于理解。

  • 跨域请求: Fetch 在跨域请求方面更灵活,支持 CORS。

  • 流式传输: Fetch 支持可读流,适用于大文件下载。

  • 维护性: Fetch 更容易维护和扩展。


常用库和插件


基于 XHR 封装的库



  • jquery:一个 JavaScript 库,提供了用于处理 DOM 操作、事件处理和 XHR 请求的便捷方法。

  • axios:一个流行的 HTTP 请求库,基于 XHR 开发,支持浏览器和 Node.js。


基于 fetch 封装的库



  • redaxios:它具有与 axios 类似的 API,但更轻量级且适用于现代 Web 开发。

  • umi-request:由 Umi 框架维护的网络请求库,提供了强大的拦截器、中间件和数据转换功能。


总结


XMLHttpRequest (XHR) 和 Fetch API 都是前端开发中用于进行数据请求的有力工具。XHR 在传统项目中仍然有用,而 Fetch API 则在现代 Web 开发中越来越流行。具体选择哪个工具取决于项目的需求和开发团队的偏好,希望本文对你有帮助!


作者:王绝境
来源:juejin.cn/post/7295551704816189467
收起阅读 »

开发一个简单的管理系统,前端选择 Vue 还是 React?

web
在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。 我们在低代码开发领域探索了多年...
继续阅读 »

在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。



我们在低代码开发领域探索了多年,从2014 开始研发低代码前端渲染,到 2018 年开始研发后端低代码数据模型,发布了JNPF快速开发平台。


JNPF是一个Vue2/Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件,无需编写代码即可完成业务需求。


前端采用的是Vue、Element-UI…;后端采用Java(.net)、Springboot…;使用门槛低,支持分布式、k8s集群部署,适用于开发复杂的业务管理系统(ERP、MES等);采用可视化组件模式可以有效地扩展不同的业务功能,并方便实现各种业务需求,且不会导致系统臃肿,若想使用某个组件,按需引入即可,反之亦然。



低代码平台的前端框架采用Vue的优势有哪些?




  •  Vue是组件化开发,减少代码的书写,使代码易于理解。




  •  最突出的优势在于可以对数据进行双向绑定。




  •  相比较传统的用超链接进行页面的切换与跳转,Vue使用的是路由,不用刷新页面。




  •  Vue是单页应用,加载时不用获取所有的数据和dom,提高加载速度,优化了用户体验。




  •  Vue的第三方组件库丰富,低代码平台能够获得更多的支持和资源。




JNPF-Web-Vue3 的技术栈介绍


JNPF 快速开发平台的 Vue3.0 版本是基于 Vue3.x、Vue-router4.x、Vite4.x、Ant-Design-Vue3.x、TypeScript、Pinia、Less 的后台解决方案,采用 Pnpm 包管理工具,旨在为中大型项目做开发,提供开箱即用的解决方案。前端同时适配Vue2/Vue3技术栈。


以下对各项技术做简单的拓展介绍:


(1)Vue3.x

Vue3.x 作为一款领先的 JavaScript 框架,通过响应式数据绑定和组件化架构实现高效的应用开发。相较于 Vue2.x,在大规模应用场景下,Vue3.x 的渲染速度提升了近 3 倍,初始化速度提升了 10 倍以上,这不仅为我们提供了更出色的用户体验,也为企业应用的开发和维护提供了极大的便利。


此外,它所支持Composition API 可以更加灵活地实现代码复用和组件化,让我们的代码更加可读、可维护。总而言之,Vue3 在许多方面都进行了改进,包括更好的性能、更少的代码大小和更好的开发体验。


(2)Vue-router4.x

Vue-router4.x 作为 Vue.js 框架中的路由管理器,具备出色的性能和扩展性,为开发者提供了一种高效而灵活的前端路由解决方案。Vue Router 主要用于构建单页应用程序,允许创建可导航的Web 应用,使您可以轻松地构建复杂的前端应用。


(3)Vite4.x

一个基于 ES Module 的 Web 应用构建工具。作为一种全新的开发模式,Vite 相对于Webpack 更加出色,内置了许多优化手段,包括 HMR、代码分割、CSS 提取、缓存策略等,从而在保证开发速度的前提下,为应用程序的加载速度和性能提供了极致的保障。此外,它还支持快速的冷启动、模块化的打包方式以及自动化的多页面构建等特性,极大的提升了前端开发效率。


(4)Ant-Design-Vue3.x

一款基于 Vue3.x 的企业级 UI 组件库,旨在帮助开发者快速搭建出高质量、美观且易用的界面。不同于其他类似的组件库,Ant-Design-Vue3.x 更注重用户体验和可定制性,提供了一整套视觉、交互和动画设计解决方案,结合灵活的样式配置,可以满足大部分项目的UI 需求,帮助开发者事半功倍。


(5)TypeScript

TypeScript 作为一种静态类型的 JavaScript 超集,不仅完美兼容 JavaScript,还提供了强大的静态类型约束和面向对象编程特性,极大地提升了代码的可读性和重用性。TypeScript拥有强大的类型系统,可以帮助开发者在代码编写阶段发现潜在的错误,减少未知错误发生概率,并提供更好的代码补全和类型检查。这一特性让团队协作更加高效,同时也降低了维护代码的成本。


(6)Pinia

Pinia 是 Vue3.x 的状态管理库,基于 Vue3.x 的 Composition API 特性,为开发者提供了清晰、直观、可扩展和强类型化的状态管理方案,可以更好地管理应用数据和状态。无论是在小型项目还是庞大的企业级应用中,我们都可以依靠这个强大的状态管理库来迅速构建出高质量的应用。


(7)Less

一种 CSS 预处理器,能够以更便捷、灵活的方式书写和管理样式表。通过 Less,开发者可以使用变量、嵌套规则、混合、运算、函数等高级功能,使得样式表的编写更加简单、易于维护。使用 Less 不仅可以提高 CSS 开发效率,还可以生成更快、更小的 CSS 文件,从而减少网站加载时间,提升网站性能。


(8)Pnpm

Pnpm 作为一种快速、稳定、安全的包管理工具,它能够帮助我们管理 JavaScript 包的依赖关系,通过采用更为精简的数据存储结构,极大地减少冗余数据的存储,从而有效地节省磁盘空间。


其他亮点


作为一款基于SpringBoot+Vue3的全栈开发平台,满足微服务、前后端分离架构,基于可视化流程建模、表单建模、报表建模工具,快速构建业务应用,平台即可本地化部署,也支持K8S部署。


引擎式软件快速开发模式,除了上述功能,还配置了图表引擎、接口引擎、门户引擎、组织用户引擎等可视化功能引擎,基本实现页面UI的可视化搭建。内置有百种功能控件及使用模板,使得在拖拉拽的简单操作下,也能大限度满足用户个性化需求。


如果你是一名开发者,可以试试我们研发的JNPF开发平台。基于低代码充分利用传统开发模式下积累的经验,高效开发。


最后,给予一点建议


关于Vue,简单易上手,官方的文档很清晰,易于使用,同时它拥有更好的新能且占据的空间相比其他框架更少,同时vue的学习曲线是很平滑的,所以这是我为什么推荐优先学习vue的原因,对于新手来说易上手,快速帮助新手熟悉一些中小型的项目,但是对于大型的项目,这就要说到Vue响应机制上的问题了,大型项目的state(状态)是特别多的,这时watcher也会很多,进而导致卡顿。


对于React,主要是适应大型项目,由于React灵活的结构和可扩展性,相比Vue对于大型项目的适配性更高,此外其跨浏览器兼容、模块化、单项数据流等都是其优点,但是与Vue相反的就是它的学习曲线是陡峭的,由于复杂的设置过程,属性,功能和结构,它需要深入的知识来构建应用程序,这对于新手来说是不太适合作为一个入门级别的框架。


作者:冲浪中台
来源:juejin.cn/post/7295565904405790761
收起阅读 »

用1100天做一款通用的管理后台框架

web
前言 去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。 很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大...
继续阅读 »

前言


去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。


很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大家对这种非干货的文章也挺感兴趣。于是在这个三年的时间点上(没错,也就是1100天),我打算继续出来和大家唠唠,这一年我又做了些什么事,或者说,如何把一款好的后台框架变得通用?


题外话:如果你对我以前的文章感兴趣,可以点我头像进入主页查看;如果你期待我以后的文章,也可以点个关注。


痛点


因为 Fantastic-admin 是基于 Element Plus 这款 UI 组件库进行开发的,于是今年我陆陆续续被问到一些问题:



  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?

  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?

  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



类似的问题一多,我也在思考一个问题:我的这款框架是不是被 Element Plus 绑架了?如果开发者在做技术选型的时候,因为 UI 组件库不符合预期,而将我的框架筛掉,这是我不希望看到的结果。


基于这个潜在隐患,我开始计划对框架进行转型。


方案


方案一


既然开发者对 UI 组件库有各自的偏好,我又想拉拢这部分开发者,那是不是多出几套不同 UI 组件库版本的就可以了呢?没错,这是我最开始冒出来的念头。


我参考了一些同类产品的做法,尽管它们把不同 UI 组件库版本做得很像,但在使用体验过程中,还是会带来操作上的割裂感。并且因为无法抹平不同 UI 组件库在 API 上的差异,导致在框架功能上,不同版本之间也会有一些差异。



你可以分别对比左右或者上下两张图,包括左侧导航栏的样式、导航收起/展开按钮的位置、右侧项目配置中提供的功能等,都能明显发现它们的差异。


虽然这可能不是什么大问题,但我认为视觉风格上的统一是能帮助产品提高识别度的。就比如上面 4 款基于不同 UI 组件库开发的后台框架,虽然它们属于同一个产品,但如果我不告诉你,你未必能通过图片确定它们师出同门。


其次就是后台框架提供的功能不统一,这里面有一定的原因是因为 UI 组件库导致的。试想一个场景,如果你要从 Element Plus 版本的后台,迁移到 Ant Design Vue 版本的后台,框架的配置文件是否能原封不动的复制过去?如果导航(路由)数据是后端返回的,数据结构能否保持完全一致,后端无需做任何修改?因为不同 UI 组件库对菜单组件的使用方式是完全不同的,比如 Element Plus 是需要手动拼装的,而 Naive UI 则是数据驱动的,只需要传入一个树形结构的数据给组件即可。如果数据结构无法保证一致,就会增加迁移和学习的成本。


最后就是我的一点私心,因为多一个 UI 组件库的版本,势必会占据我更多的业余时间,如果同时维护 4、5 个版本,那我大概下班后的所有时间都要投入到其中,并且如果未来又有新的 UI 组件库成为流行,那就又多一个版本的维护,这并不是一个可持续发展的方案。


方案二


既然上一个方案不符合我的期望,于是我开始思考,框架本身能不能不依赖这些 UI 组件库?如果框架本身不依赖于三方的 UI 组件库,那开发者不就可以根据需要自行引入想要的组件库了么。



就如上图,主/次导航和顶栏是属于框架的部分,而这部分其实并没有用到太多 UI 组件库提供的组件,以 Element Plus 举例,我统计了一下目前 Fantastic-admin 用到的组件:



  • Menu 菜单(主/次导航)

  • Breadcrumb 面包屑(顶栏)

  • Popover 气泡卡片(顶栏右侧的工具栏)

  • Dropdown 下拉菜单(顶栏右侧的工具栏)

  • Drawer 抽屉(应用配置)

    • Message 消息提示

    • Button 按钮

    • Input 输入框

    • Radio 单选框

    • Select 选择器

    • Switch 开关

    • …(等等表单类组件)




可以看到,虽然抽屉组件里用了很多表单类的组件,但这部分组件都是在应用配置里使用的,而应用配置这个模块,主要是方便在线测试框架提供的各种功能,在实际业务开发中,是完全不需要这个模块的。



所以初步算下来,后台框架真正依赖于 Element Plus 实现的组件就只有 4 个:



  • Menu 菜单

  • Breadcrumb 面包屑

  • Popover 气泡卡片

  • Dropdown 下拉菜单


那我为什么不找一些独立的第三方插件替代呢?是的,这是我第二个方案,就是找一些独立的插件替换 UI 组件库中的组件。但问题也立马迎面而来,就是偌大一个 Github ,居然找不到符合我需求和审美的插件。


比如菜单插件,我希望它和 Element Plus 里的菜单组件在功能上没有太大差异,支持水平/垂直模式、支持折叠收起、支持设置默认激活菜单、支持默认展开等。


比如面包屑插件,或许是因为这个插件功能太简单,并且大部分 UI 组件库都有提供,在 Github 能搜到独立的面包屑插件很少,搜到的也基本上是 N 年前的上传的,既没有人维护,风格样式也很丑。


这个方案似乎也行不通……吗?


方案三


虽然方案二在实施的第一步就扑街了,但有一点思路还是正确的,就是让框架本身不依赖于三方 UI 组件库。既然网上搜不到合适的插件,那我为什么不自己写一个呢。


比如面包屑,这是一个很简单的功能,任何前端初学者应该都可以写一个面包屑组件。


而气泡卡片和下拉菜单我没有计划自己写,因为找到了一个还不错的插件 Floating Vue,它由 Vue 团队核心人员开发并维护,并且最重要的是它支持自定义样式,意味着我可以将它魔改成想要的样子,尽可能和我的框架在视觉风格上保持统一。


最后一个比较难啃的骨头就是菜单,因为找不到合适的替代品,自己写的话又比较有挑战,虽然我有一点实现思路,但不多。当然最终还是决定自己写一个,因为觉得三方 UI 组件库这么多,实在写不出来我就去读他们源码,总不能每一个源码我都读不懂吧。


这 4 个组件的替换方案确定后,剩下就是抽屉组件和它里面的一些表单组件了,这些要怎么解决呢?这会我想到了 Headless UI ,它是完全无样式的 UI 组件库,通过与 Tailwind CSS / UnoCSS 集成使用,可以快速构建出属于自己风格的组件。


但是 Headless UI 提供的组件非常有限,并不能覆盖我需要的表单组件。不过它的设计给了我启发。表单组件我并不需要非常复杂的功能,原生的表单控件其实就能满足我的使用需求,只是原生的样式比较丑,和我想要的风格不统一,那我只需要给他们定制一套统一的风格就可以了,也就写一套原子化的 CSS 样式。


于是,方案敲定,开始实操。


实操


我决定从易到难开始处理,因为这样在初期能快速看到进度推进,也避免一上来就被一个菜单功能卡住好几天,甚至十几天都没有进展,打击到自己的信心。


1. 面包屑


和预期一样,并没有什么难度,很轻松就实现了。只不过目前还是保持和 Element Plus 一样的使用方式,就是需要手动拼装,后期计划改成数据驱动的使用方式。



2. 气泡卡片 & 下拉菜单


这部分参考了 nuxt/devtoolsFloating Vue 的自定义样式,以及 nuxt/ui 中下拉菜单的样式风格,最终形成了我自己满意的风格



3. 抽屉


使用了 Headless UI 中的 Dialog 组件,因为它和抽屉组件有相同的交互方式,它们都是在遮罩层上展示内容,只不过 Dialog 更多时候是居中展示,而抽屉则是在左右两侧展示。


其次在使用过程中,发现 Headless UI 中的 Transition 组件是一个惊喜。虽然 Vue 本身就有提供 <transition> 组件用于处理过渡动画,但有一个场景会比较难处理,官方的描述是:



This technique is needed when you want to coordinate different animations for different child elements – for example, fading in a Dialog's backdrop, while at the same time sliding in the contents of the Dialog from one side of the screen.
当您要为不同的子元素协调不同的动画时,就需要使用这种技术,例如,在淡入对话框背景的同时,从屏幕的一侧滑入对话框的内容。



这说的不就是抽屉组件么?于是按照官方的示例,修改了整体风格,最终效果也就出来了。



4. 表单组件


之前的计划是修改原生表单控件的样式,但在开发过程中发现会有一定的局限性。比如 <select> 无法控制弹出选项框的样式,我的解决办法就是用 Floating Vue 封装模拟一个 select 组件。


同时也在开发过程中发现了一些被遗漏组件,于是边做边补,最终大概做了 10 多个组件。虽然看着不少,它们都秉持着最小可用的状态。什么意思呢?就是我不会给它们设计太多的 API ,因为它们的定位和三方 UI 组件库不同,它们只要满足框架本身使用即可,用不到的 API 不会进行开发。并且使用上也不会有太大负担,如果不是对框架进行二次开发,开发者是可以完全不用关注这部分组件。



5. 菜单


菜单组件确实是个难啃的骨头,我差不多用了 3 周的晚上时间去开发。


第一周,按照自己的思路徒手撸,做到一半卡壳,做不下去了;


第二周,开始看 Element Plus 、Naive UI 、Ant Design Vue 里菜单的源码;



Ant Design Vue 的没看懂,放弃;


Naive UI 的看到一半发现核心实现被作者封装到 treemate 这个独立包中了,虽然这个包是开源的,目的也是针对树形结构的一体化解决方案。但我粗略看了一遍文档,感觉有点大材小用,因为它有很多 API 我是用不到的,而我对菜单组件又有一些自己的想法,不确定是否它这个包能否满足我的需求,放弃;


最后选择看 Element Plus 的,通过在本地一点点打印数据,大概理解了实现思路,但组件递归调用,父子组件通过 provide / inject 传递数据和函数的方式,数据状态的变动也是一层层向上级组件通知,直到通知到顶层组件,在我看来有点不太优雅,如果数据能统一在顶层组件里操作就好了。其次我的计划是写一个数据驱动的菜单组件,而不是像 Element Plus 需要手动拼装的,所以虽然我大致看懂了 Element Plus 菜单组件是怎么实现的,但在我自己实现的时候,还是有很大的不同,能参考的代码并不多。


这部分的开发总结,我可能会在以后单独写一篇文章详细说说,因为这部分也是整个方案中唯一的难点。



第三周,因为实现思路大致有了,所以开发上就没有太多的卡壳,最终结果也还不错,基本达到了我的需求。


同时因为组件完全可控,顺带解决了之前使用 Element Plus 菜单组件上无法解决的 bug ,比如当菜单收起时,弹出的悬浮菜单如果数量过多,超出屏幕高度,超出的部分就无法查看了,就像这样:



但是现在则会有滚动条,使用体验上更舒服。



验证


至此,我的后台框架已经摆脱对 Element Plus 的依赖,接下来就需要验证一下是否可以方便的替换成其他 UI 组件库。


我分别用 Ant Design Vue 、Arco Design Vue 、Naive UI 、TDesign 这四款热度比较高的组件库进行了验证:















Ant Design Vue Arco Design Vue Naive UI TDesign

结果还是很满意的,都能够顺利替换,并且替换过程并没有花费很多时间,一个小时内就可以替换成功。



由于登录页这个特殊的存在,替换组件库后是需要对其用到的 Element Plus 组件进行手动修改的,这部分会比较花时间,因为会涉及到表单验证之类的东西,不同组件库的写法差异还是比较大的。



详细的替换步骤可以在 Fantastic-admin 官方文档里找到。


回顾


让我们重新看下一开始的痛点是否都解决了么:




  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?



    虽然不会有,但可以自己动手,根据教程将默认的 Element Plus 替换成你想要的 UI 组件库就可以了





  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?



    不会有冲突,现在可以彻底移除 Element Plus ,安装并使用自己的 UI 组件库





  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



    可以用 Fantastic-admin 源码先进行 UI 组件库的替换,之后再将老项目的业务代码逐部迁移





除了解决这些痛点,甚至还有新收获:




  • 帮助公司/企业打造视觉风格统一的产品,提高产品辨识度



    大公司可能有不止一个项目团队,不同项目团队的技术偏好可能无法完全统一,导致开发的后台长得也千变万化。但即使在这种情况下,使用 Fantastic-admin 依旧可以保持整体视觉风格上的统一。





  • 近乎于 0 的上手成本



    因为后台框架始终都只有一套,开发者不会因为切换 UI 组件库后,要重新了解后台框架的使用





  • 维护成本更低,产品生命周期更长



    这一点是对我自己说的,不管未来会出现多少个新的 UI 组件库,我都不需要去新增一个版本进行单独维护;或者 Element Plus 如果有一天停止维护了,我的产品也不会因此进入了死亡倒计时





总结


文章写到这里,差不多就结束了,虽然阅读一遍可能只花了不到10分钟,但为了做成这件事,我大概从今年 6 月份就开始构思了,也是花了蛮多的精力,所以很感谢你的耐心。


当一款产品做到第 4 个年头,周围大部分同类产品都进入到半停更的状态,这一年里我经常思考如何延长产品的生命周期,如何让更多人来使用,而这篇文章就是对我自己今年的一个总结,也是一份答卷,希望大家能喜欢。


另外,Fantastic-admin V4.0 已经正式发布,感兴趣的朋友可以来看看,或许你的下一个项目,就可以用上了。


作者:Hooray
来源:juejin.cn/post/7295624857432850468
收起阅读 »

记一次使用babel做代码转换的经历

web
前言 前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。 这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访...
继续阅读 »

前言


前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。


这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访问不通,还得拿到本地来。


得,谁让咱们天生就是找事的好手呢,那整吧。


方案对比


既然来活了,那咱们首先得先确定下这个事怎么做?有以下几个方案:


方案一: 发挥中华民族的优良传统,勤劳,即手动将全部的静态资源引用处替换为本地引用,想想手就疼


方案二: 将偷懒运用到极致,将静态资源全部放到public/assets目录下(Vite项目中public目录下的文件会被直接复制到打包目录下),同时修改资源引用的统一前缀为 /assets,即可引用到该静态资源。目测几分钟就能完成


方案三: 写个脚本,自动完成 1 操作,瞬间手就不疼了,但是脑壳开始疼了


对比下这三个方案的优缺点,选出最优解



方案一


优点:简单


缺点:手疼且低效


方案二


优点:省时、省力


缺点:需要考虑打包后的引用路径,同时因为文件都是直接复制到包中的,并没有经过hash处理,浏览器会缓存该文件,后续如果文件修改,不能第一时间反应再客户端。


方案三


优点:高效、一劳永逸、文件会经过Vite处理,生成带有hash值的新文件,没有缓存问题


缺点:这个脚本有点难写,涉及代码转换和项目文件扫描等知识,脑壳疼



最终,本着一劳永逸的想法,我选择了方案三。


过程


整体思路:



  1. 将全部静态资源引用汇总到统一文件中,方便管理及代码分析

  2. 使用代码转换工具将上面的文件内容转换为使用 import 导入的方式


静态资源汇总


所有的静态资源引用散布在项目的各个文件中,这不利于代码分析,也不利于代码转化,所以,第一步就是将散布在项目各个文件中的静态资源引用汇总到一个文件中,方便管理、代码分析、代码转化。


这一步是纯体力活,一次劳动,收益无穷。


最终静态资源汇总文件应该是这样的:


import { ASSETS_PREFIX } from './constants';

const contactUs = `${ASSETS_PREFIX}/login/contact_us.png`;
const userAvatar = `${ASSETS_PREFIX}/login/default_avatar.png`;
const loginBg = `${ASSETS_PREFIX}/login/login_bg.jpg`;

export {
contactUs,
userAvatar,
loginBg,
}


  1. 一个静态资源对应一个变量,一个变量对应一个静态资源路径

  2. 静态资源路径必须使用模版字符串统一前缀,便于后续做替换

  3. 统一导出


代码转换


静态资源全部会送完毕后,接下来就是做代码分析及转换。


我们的目标其实就是将上面的代码转换到下面这种:


import contactUs from '@/assets/login/contact_us.png';
import userAvatar from '@/assets/login/default_avatar.png';
import loginBg from '@/assets/login/login_bg.jpg'

export {
contactUs,
userAvatar,
loginBg,
}

既然涉及代码转换,很自然的就能想到使用babel做转换。


先来简单说下babel做代码转换的过程:



  1. 使用 @babel/parser 将代码解析为抽象语法树(AST: 表示当前代码结构的js对象)

  2. 找到标识为 const 的变量,拿出该变量,并将其后对应的变量内容拿出来,将模版字符串中的变量替换为@/assets,得到新静态资源本地路径(@/assets/login/contact_us.png)

  3. 组合 import 的 AST 对象,并使用该对象替换原来的 const 相关的AST

  4. 使用 @babel/generator 将新的AST转换为代码输出到对应文件中


代码如下:


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import fs from 'fs';

// 静态资源汇总文件
let imgInfoFilePath = 'src/scripts/assets.ts';
// 要替换为的静态资源路径前缀
let replaceToCode = '@/assets';

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历const声明节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

node.declarations.forEach(decl => {
// 存储变量名
const localName = decl.id.name;
// 组装import路径
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;
// 组装import结构
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
const result = generate.default(ast, {}, code);
// 备份原文件
fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);
// 代码输出
fs.writeFileSync(imgInfoFilePath, result.code);
} catch (error: any) {
logError(error);
}
}

这样,代码就转换完成了。


这样转换完后,ts文件中相关的静态资源引用就替换完成了,但是css文件中的静态资源引用还没有被转换。


因为css文件中的静态资源路径都是完整路径,不存在其中掺杂变量的情况,所以我们只需要找到所有的css文件,并将其中的路径前缀统一替换为@/assets 即可。


import { globSync } from 'glob';
import fs from 'fs';

let replaceStr = 'https://xxxxx.xxxx.xxxxx';
let replaceToCode = '@/assets';

function replaceHttpsCode() {
try {
// 扫描文件
const files = globSync('./src/**/*.{scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 写入文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}


  1. 使用 glob 扫描当前目录下的scss、css文件。

  2. 读取文件内容,并使用replace方法替换掉静态资源路径

  3. 写入文件,完成转换


至此,代码全部转换完成。


封装成工具包


因为多个项目都会涉及静态资源转换的问题,所以我将此脚本封装为npm包,并提炼了 transform build 命令,只需执行该命令,即可完成资源转换,以下是源码分享:


cli.ts


import { Command } from 'commander';
import { version } from '../package.json';
import buildAction from './transform';
const program = new Command();

program
.command('build')
.description('transform assets and code')
.option(
'--replaceStr <path>',
'[string] 需要全局替换的字符串,默认值: https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage',
)
.option('--imgInfoFilePath <path>', '[string] 统一的静态资源文件路径 默认值: src/scripts/assets.ts')
.option('--replaceToCode <path>', '[string] 替换为的代码 默认值: @/assets')
.option('--assetsDir <path>', '[string] 静态资源文件目录 默认值: src/assets')
.action(options => {
buildAction(options);
});

program.version(version);

program.parse();

transfrom.ts


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import chalk from 'chalk';
import { globSync } from 'glob';
import fs from 'fs';

interface Options {
replaceStr?: string;
imgInfoFilePath?: string;
replaceToCode?: string;
assetsDir?: string;
}

let replaceStr = 'https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage';
let imgInfoFilePath = 'src/scripts/assets.ts';
let replaceToCode = '@/assets';
let assetsDir = './src/assets';

function checkAssetsDir() {
logInfo('检查 src/assets 目录是否存在');

if (!fs.existsSync(assetsDir)) {
logError('assets 目录不存在,请先联系相关人员下载对应项目的静态资源文件,并放置在 src/assets 目录下');
} else {
logSuccess('assets 目录存在');
}
}

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历VariableDeclarator节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

// @ts-ignore
node.declarations.forEach(decl => {
// @ts-ignore
const localName = decl.id.name;

// @ts-ignore
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;

// @ts-ignore
logInfo(`替换 ${replaceStr}${decl?.init?.quasis?.[1]?.value?.raw}${filePath}`);

// 构建导入规范
// @ts-ignore
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
// @ts-ignore
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
// @ts-ignore
const result = generate.default(ast, {}, code);

logInfo(`备份 ${imgInfoFilePath} 文件为 ${imgInfoFilePath}.bak`);

fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);

fs.writeFileSync(imgInfoFilePath, result.code);

logSuccess(`转换 ${imgInfoFilePath} 成功`);
} catch (error: any) {
logError(error);
}
}

function replaceHttpsCode() {
logInfo('开始转换 其余文件中引用https导入的静态资源');

try {
// 扫描文件
const files = globSync('./src/**/*.{vue,js,ts,scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

if (content.includes(replaceStr)) {
logInfo(`替换 ${file} 中的 ${replaceStr}${replaceToCode}`);
}

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 保存文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}

function logInfo(info: string) {
console.log(chalk.gray(`[INFO] - 🆕 ${info}`));
}

function logSuccess(info: string) {
console.log(chalk.green(`[SUCCESS] - ✅ ${info}`));
}

function logError(info: string) {
console.log(chalk.red(`[ERROR] - ❌ ${info}`));
}

export default function main(options: Options) {
replaceStr = options.replaceStr || replaceStr;
imgInfoFilePath = options.imgInfoFilePath || imgInfoFilePath;
replaceToCode = options.replaceToCode || replaceToCode;
assetsDir = options.assetsDir || assetsDir;

checkAssetsDir();
babelTransformCode();
replaceHttpsCode();
}

作者:程序员小杨v1
来源:juejin.cn/post/7295276751595798580
收起阅读 »

登录页面一些有趣的css效果

web
前言 今天无意看到一个登录页,input框focus时placeholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及...
继续阅读 »

前言


今天无意看到一个登录页,inputfocusplaceholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码


919c40a2a264f683ab5e74e8a649ac5.png


title 的动画实现


首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke, 逐步点亮只需要使用filter即可


text-stroke


text-stroke属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke属性通常与-webkit-text-stroke前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持


text-stroke属性有两个主要值:



  1. 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。

  2. 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。


filter


filter是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。


filter属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:




  1. 模糊(blur) : 通过blur函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。


    .blurred-image {
    filter: blur(5px);
    }



  2. 对比度(contrast) : 通过contrast函数可以调整对比度。值为百分比,1表示原始对比度。


    .high-contrast-text {
    filter: contrast(150%);
    }



  3. 饱和度(saturate) : 通过saturate函数可以调整饱和度。值为百分比,1表示原始饱和度。


    .desaturated-image {
    filter: saturate(50%);
    }



  4. 反色(invert) : 通过invert函数可以实现反色效果。值为百分比,1表示完全反色。


    .inverted-text {
    filter: invert(100%);
    }



  5. 灰度(grayscale) : 通过grayscale函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。


    .gray-text {
    filter: grayscale(70%);
    }



  6. 透明度(opacity) : 通过opacity函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。


    .semi-transparent-box {
    filter: opacity(0.7);
    }



  7. 阴影(drop-shadow) :用于在图像、文本或其他元素周围添加阴影效果。这个属性在 CSS3 中引入,通常用于创建阴影效果,使元素看起来浮在页面上或增加深度感


    drop-shadow(<offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>?)

    各个值的含义如下:



    • <offset-x>: 阴影在 X 轴上的偏移距离。

    • <offset-y>: 阴影在 Y 轴上的偏移距离。

    • <blur-radius> (可选): 阴影的模糊半径。默认值为 0。

    • <spread-radius> (可选): 阴影的扩散半径。默认值为 0。

    • <color> (可选): 阴影的颜色。默认值为当前文本颜色。




filter属性的支持程度因浏览器而异,因此在使用时应谨慎考虑浏览器兼容性。


实现移入标题点亮的效果


想实现移入标题点亮的效果我们首先需要两个通过定位重叠的span元素,一个做镂空用于展示,另一个作为
hover时覆盖掉镂空元素,并通过filter: drop-shadow实现光影效果,需要注意的是这里需要使用inline元素实现效果。


title-animation.gif


input 的动画实现


input的效果比较简单,只需要在focusspan(placeholder)上移变成span(label)同时给inputborder-bottom做一个底色的延伸,效果确定了接着就看看实现思路。


input placeholder 作为 label


使用div作为容器包裹inputspanspan首先绝对定位到框内,伪装为placeholder, 当input状态为focus提高spantop值,即可伪装成label, 这里有两个问题是:




  1. 当用户输入了值的时候,span并不需要恢复为之前的top, 这里我们使用css或者js 去判断都可以, js就是拿到输入框的值,这里不多做赘述,css 有个比较巧妙的做法, 给input required属性值设置为required, 这样可以使用css:valid伪类去判断input是否有值。




  2. 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用pointer-events: none; 来解决。pointer-events 是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。


    pointer-events 具有以下几个可能的值:



    1. auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。

    2. none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。

    3. visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。

    4. visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    5. visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。

    6. painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。

    7. fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    8. stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。




pointer-events 属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。


input border bottom 延伸展开效果


效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span作为底部的边, 初始不可见, focus时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform变形,首先使用transform: scaleX(0);达到不可见的效果, 然后设置变形原点为中间transform-origin: center;,这样效果就可以实现了


input 的动画实现效果


input-animation.gif


按钮的动画实现


关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 background-image(radial-gradient)


background-image(radial-gradient)


background-image 属性用于设置元素的背景图像,而 radial-gradient 是一种 CSS 渐变类型,可用于创建径向渐变背景。这种径向渐变背景通常以一个中心点为基础,然后颜色渐变向外扩展,形成一种放射状的效果。


radial-gradient 的语法如下:


background-image: radial-gradient([shape] [size] at [position], color-stop1, color-stop2, ...);


  • [shape]: 可选,指定渐变的形状。常用的值包括 "ellipse"(椭圆)和 "circle"(圆形)。

  • [size]: 可选,指定渐变的大小。可以是长度值或百分比值。

  • at [position]: 可选,指定渐变的中心点位置。

  • color-stopX: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。


按钮移入动画效果实现


btn-animation.gif


结尾


css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。


作者:刘圣凯
来源:juejin.cn/post/7294908459002331171
收起阅读 »

迷茫的前端们

web
前端已死!这个声音从年初开始,持续了小半年。紧接着,一堆的前端前辈出来说,前端不会死。哈哈哈,我觉得这个话题很有意思。也来蹭个热点。嗯,现在已经不算热点了,这个话题已经凉了。 最近想法很多,总想写点东西,就拿这个话题开始吧。希望能对大家有帮助。 什么情况 首先...
继续阅读 »

前端已死!这个声音从年初开始,持续了小半年。紧接着,一堆的前端前辈出来说,前端不会死。哈哈哈,我觉得这个话题很有意思。也来蹭个热点。嗯,现在已经不算热点了,这个话题已经凉了。


最近想法很多,总想写点东西,就拿这个话题开始吧。希望能对大家有帮助。


什么情况


首先,要看看为什么会有这个观点。原因也很简单,自从去年开始,各大公司都开始裁员,然后HC也开始减少。但是因为前几年互联网工资收入高,每年毕业的应届生那是一年比一年多。前端培训班也火热的不行,每年都向社会输送大面积的前端开发。


所以,从供需角度看,前端开发找工作,尤其是刚毕业和刚培训毕业的前端开发,今年找工作就是地狱模式。所以有人说“前端已死”,自然很快就能获得共鸣


另外的声音


然后,有意思的来了。“前端已死”这个话题火了后,各大前端自媒体,前端前辈都出来发声。观点出奇的一致:前端不会死。一下子就把前端已死的观点给压下去了。这种社区声音180度的大转弯也是少见。


到底死没死


说完现象,我说下我的观点。



  1. “前端已死”的声音能获得共鸣,说明前端已经发展到了一个关键的时间点。

  2. 只要还有页面,还有小程序,前端就不会死。

  3. 不会死,并不是说能活的很好。


行业周期


互联网在行业分类中属于第三产业,也就是服务业。服务业的特点就是周期性。如果所有服务业的周期是往下走的,互联网也好不了。三年疫情,大家都知道,服务业已经被折腾的不行了,即使今年放开了,大家还敢开线下店吗?被互联网服务的行业大部分都过的不好,互联网能好吗。


当然,后面随着经济复苏,互联网也会慢慢恢复景气。但是这里还有另外一个点,由于互联网前几年工资都太高了,年年倒挂,导致选计算机专业的人越来越多。以大学四年计算,后面3年每年毕业的计算机专业的学生,还是会持续增加的。


所以呢,从供需角度,技术互联网恢复景气了,互联网招聘未必会恢复景气。

当然了,我觉得,后面几年,不会像今年这么卷,投10份简历,连个面试机会都没有。(预测这种东西,很容易打脸,大家看看就好)


另外,就前端开发而言,我感觉好多东西都很成熟了。最近看技术社区的文章,感觉很久没有看到很新的内容了(也许是推荐系统搞得信息茧房)。大家都在几个领域里面深挖。如果没有新的方向出来,真的很难容纳这么多的求职者。后面看看AI能不能创新出一波职位吧。


所以呢,从互联网行业和前端行业来看,前端已经发展到了一个关键点,后面如果没有新的方向出来,即使互联网恢复景气,求职也不会像以前那么轻松了。

但是,毕竟还有那么多业务需要页面,需要小程序,这块的需求还是在的,所以还是会有前端开发的。


技术周期


从我毕业,基本上都在做互联网,期间其实经历过好几轮技术迭代。比如最早的Flash, 到PHP, PC端的JQuery。你很难在招聘网站上找到要求这些技术的岗位了。

随着前端的发展,技术栈也一直在变化。但是,大家想想,现在流行的React 和 Vue,已经用了多久了。如果还是只会React,Vue,是不是说明已经很久没有成长了。后面会不会有新的技术栈出来,比如VR出来后,是不是还用React开发?

如果还是继续用React,Vue,那么和行业周期里面说的一样,前端不会死,但是不会像以前那么活的好。


新方向在哪里


其实聊了这么多,本质就是一个供需。供需这个角度真的太强大了,能解释很多问题。如果未来没有一个新的大方向出现,那么在岗位需求变化不大的情况下,每年大量学生毕业,也就是供应增多,求职困难是必然的。

那么会不会有新方向出现呢,目前看,有几个可能的方向:



  1. AI。从去年底开始,AI持续火热,目前虽然有退烧,但是AI的趋势已经明确。后面要看的是,AI是不是需要大量的前端工作。这块大家可以说说自己的判断。

  2. VR元宇宙。Meta搞元宇宙,差点把自己搞死,现在苹果也出VR设备,观察下能不能把这个行业带起来。大家说有没有可能。

  3. Web3。去年年初,Web3还很火了的,现在好像,嗯,一般般。后面能不能再次爆发,也要继续观察。大家也可以说说自己的开发。


扯一句


最后扯一句。我看了好多前端前辈对“前端已死”的看法,都说前端不会死,会死的都是初级前端,前端要持续学习,要让自己不可替代。说实话,我觉得这都是屁话。写代码的谁不是从初级开始的呢,现在是完全不给初级的机会,断档了!写代码的人,已经是最爱学习的那一批人了吧,永远在学习新东西。还有让自己不可替代,公司会想办法让你可替代,后面聊一聊不可替代这个话题,我也有很多想说的。


结束


后面会逐步把掌握的前端知识以及职场知识沉淀下来。 如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:写代码的浩
来源:juejin.cn/post/7253437782333669434
收起阅读 »

如何在网页中展示源代码

web
如何在网页中展示你的源代码 如下图所示: 在做技术说明文档 或者 组件库 的时候,经常需要在网页中引用代码示例,但是直接写又很丑,所以我们来试试如何在网页中展示自己的源代码 第一步: 自定义 vite插件 首先需要一个自定义插件用来转换 vue 的自定义块 ...
继续阅读 »

如何在网页中展示你的源代码


如下图所示:


1698137911654.png


在做技术说明文档 或者 组件库 的时候,经常需要在网页中引用代码示例,但是直接写又很丑,所以我们来试试如何在网页中展示自己的源代码


第一步: 自定义 vite插件


首先需要一个自定义插件用来转换 vue 的自定义块


在vite.config.ts文件里


import fs from 'fs'
import {baseParse} from '@vue/compiler-core'
const vueDemoPlugin = {
name: "vue-block-demo",
transform(code, path) {
if (!/vue&type=demo/.test(path)) {
return;
}
const filePath = path.split("?")[0];
//异步读取文件内容,并转为string类型
const file = fs.readFileSync(filePath).toString();
//将读取到的文件中的自定义快渲染为AST
const parsed = baseParse(file).children.find((n) => n.tag === "demo");
//读取自定义模块中的文本内容
const title = parsed.children[0].content;
//将读取文件中的自定义块切分,并转为字符串类型
const main = file.split(parsed.loc.source).join("").trim();
//以JSON数据类型返回
return `export default Comp => {
Comp.__sourceCode = ${JSON.stringify(main)}
Comp.__sourceCodeTitle = ${JSON.stringify(title)}
}`
;
},
};
export default defineConfig({
plugins: [vue(), vueDemoPlugin],
})

第二步:在要展示的源代码文件里面加上一个自定义块


比如我要展示 SwitchDemo01.vue 文件


<template> 加上 <demo> 自定义块,里面内容写上 title


<demo>常规用法</demo>
<template>
<hx-switch v-model="value1" />
<hx-switch
v-model="value2"
class="ml-2"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
/>
</template>

<script lang="ts" setup>
import { HxSwitch } from 'hx-gulu-ui';
import { ref } from 'vue'

const value1 = ref(true)
const value2 = ref(true)
</script>

第三步: 在页面展示文件里面引用


如: showSwitch.vue 文件:


<template>
<div class="doc-page">
<h2>Switch 组件示例 </h2>
<p class="doc-page-desc">表示两种相互对立的状态间的切换,多用于触发「开/关」。</p>
<div class="demo">
<h3>{{ SwitchDemo01.__sourceCodeTitle }}</h3>
<p class="doc-page-usage">绑定 `v-model` 到一个 `Boolean` 类型的变量。 可以使用 `--el-switch-on-color` 属性与 `--el-switch-off-color` 属性来设置开关的背景色</p>
<div class="demo-component">
<SwitchDemo01></SwitchDemo01>
</div>
<div class="demo-actions">
<Button>查看代码</Button>
</div>
<div class="demo-code">
<pre>{{ SwitchDemo01.__sourceCode }}</pre>
</div>
</div>

</div>
</template>

<script lang="ts" setup>
import SwitchDemo01 from '@/components/switch/SwitchDemo01.vue'

// __sourceCode 这里面是源文件去除了 <demo> 外的所有代码
console.log('SwitchDemo01', SwitchDemo01.__sourceCode)

// __sourceCodeTitle 这里面是 <demo>常规用法</demo> 里面的文字
console.log('SwitchDemo01', SwitchDemo01.__sourceCodeTitle)

</script>

<style lang="scss" scoped>
@import './style.scss';

</style>

此时,已经可以在代码上显示源文件代码了,但是代码没有任何样式,很丑怎么办呢?


第四步: 引入 prismjs




  • prismjs 是代码主题的插件



    • 官网

    • 安装: npm i prismjs




  • 调用


    import 引入 好像有问题,只支持 require('prismjs'),同时在window属性下 添加了 Prismjs属性,大家可以自己试一下


    <script setup lang='ts'>
    import 'prismjs'
    import 'prismjs/themes/prism-okaidia.min.css'
    const Prism = (window as any).Prism

    const code = `var data = 1;`;
    const html = Prism.highlight(code, Prism.languages.javascript, 'javascript');
    </script>

    示例:




<template>
<div class="doc-page">
<h2>Switch 组件示例 </h2>
<p class="doc-page-desc">表示两种相互对立的状态间的切换,多用于触发「开/关」。</p>
<div class="demo">
<h3>{{ SwitchDemo01.__sourceCodeTitle }}</h3>
<p class="doc-page-usage">绑定 `v-model` 到一个 `Boolean` 类型的变量。 可以使用 `--el-switch-on-color` 属性与 `--el-switch-off-color` 属性来设置开关的背景色</p>
<div class="demo-component">
<SwitchDemo01></SwitchDemo01>
</div>
<div class="demo-actions">
<Button>查看代码</Button>
</div>
<div class="demo-code">
<pre class="language-html" v-html="html"></pre>
</div>
</div>

</div>
</template>

<script lang="ts" setup>
import SwitchDemo01 from '@/components/switch/SwitchDemo01.vue'

import 'prismjs'
import 'prismjs/themes/prism-okaidia.min.css'
const Prism = (window as any).Prism

const html = computed(() => {
return Prism.highlight(SwitchDemo01.__sourceCode, Prism.languages.html, 'html')
})

</script>

<style lang="scss" scoped>
@import './style.scss';

</style>

最终效果如下图所示:


1698140953360.png


作者:Blink46
来源:juejin.cn/post/7293348981664399397
收起阅读 »

使用小程序中的 observe 实现数据监听

web
小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。 什么是 observe? observe 是小程序中的一个方法...
继续阅读 »

小程序开发中,数据的监听和响应是非常重要的。为了更方便地监听数据的变化,小程序提供了 observe 方法。本文将详细介绍如何在小程序中使用 observe 实现数据监听,以及一些常见的应用场景。


什么是 observe


observe 是小程序中的一个方法,用于监听数据的变化并触发相应的回调函数。它可以用于监听页面数据、组件数据以及其他数据对象。


如何使用 observe


监听页面数据


在页面的 .js 文件中,可以使用 Page 函数中的 data 中的 observe 字段来监听数据的变化。以下是一个示例:


// pages/index/index.js
Page({
data: {
count: 0,
},

// 监听 count 数据的变化
observe: {
'count': function (newVal, oldVal) {
console.log('count 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},

// 增加 count 值的函数
increaseCount() {
this.setData({
count: this.data.count + 1,
});
},
});

在上述示例中,我们在 observe 字段中定义了一个监听器,当 count 数据发生变化时,会触发相应的回调函数。


监听组件数据


在小程序的组件中,也可以使用 observe 来监听组件数据的变化。以下是一个示例:


// components/my-component/my-component.js
Component({
data: {
message: 'Hello, World!',
},

methods: {
changeMessage() {
this.setData({
message: 'New Message!',
});
},
},

// 监听 message 数据的变化
observe: {
'message': function (newVal, oldVal) {
console.log('message 的值从 ' + oldVal + ' 变为 ' + newVal);
}
},
});

在组件的 observe 字段中同样定义了一个监听器,用于监听 message 数据的变化。


常见应用场景


1. 数据绑定


observe 可以用于在数据变化时自动更新视图,实现数据绑定。这对于构建响应式的页面和组件非常有用。


2. 数据校验


通过监听数据的变化,可以在数据变化时进行校验,确保数据的合法性。例如,监听输入框中的文本变化并验证其格式。


3. 事件通知


当某个数据变化时,可以通过 observe 触发相关的事件,通知其他部分的代码执行相应的操作。


4. 数据持久化


在某些情况下,需要将数据持久化到本地存储或服务器,可以在数据变化时触发数据保存操作。


注意事项和最佳实践


在使用 observe 进行数据监听时,有一些注意事项和最佳实践需要考虑:




  1. 数据引用类型的监听:当监听对象是引用类型(例如对象或数组)时,需要注意对象的引用是否发生变化。observe 监听的是对象的引用,而不是对象内部属性的变化。如果需要监听对象内部属性的变化,可以使用深度监听或手动触发。




  2. 避免过多监听器:不要过度使用 observe,因为过多的监听器可能会导致性能问题。只监听那些真正需要监控的数据。




  3. 监听器的性能开销:监听器的回调函数在数据变化时会被频繁调用,因此要确保回调函数的执行效率较高,以避免影响应用的性能。




  4. 避免循环引用:在监听器回调函数中不要再次修改被监听的数据,以防止循环引用和无限循环触发监听器。




  5. 生命周期管理:在页面或组件销毁时,要记得取消监听以防止内存泄漏。可以在 onUnload 生命周期中取消监听。




onUnload() {
this.setData({
observe: null, // 取消监听
});
}

通过谨慎使用 observe,你可以实现有效的数据监听和响应,提高小程序应用的可维护性和性能。


结语


observe 是小程序中非常有用的功能,它允许你监听数据的变化并执行相应的操作。无论是在页面中还是在组件中,都可以使用 observe 来实现数据的监听和响应。通过合理利用 observe,你可以构建更加动态和交互性的小程序应用。


作者:依旧_99
来源:juejin.cn/post/7295237661618438196
收起阅读 »

用canvas画出一片星空

web
前言 由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理Canvas。Canvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。 Canvas 介绍 首先我们来介绍介...
继续阅读 »

前言


由于最近用了挺多Echarts的,所以突然想学习学习它的底层原理CanvasCanvas对于我们前端来说是一个非常强大的工具,它可以实现各种复杂的图形和动画效果,我们如果能够熟练掌握它,我们就可以做很多炫酷的效果。


Canvas 介绍


首先我们来介绍介绍CanvansCanvasHTML5提供的一个绘图API,它允许通过JavaScriptHTML元素创建和操作图形。Canvas提供的是一个矩形画布,我们可以在上面绘制各种图形、动画与交互效果。


功能


上面了解了Canvas是什么之后, 我们再来和大家展开说说Canvas 一些主要功能:



  1. 画布:Canvas提供了一个矩形的画布区域, 我们可以通过HTML中的<canvas>标签来创建并通过设置画布的宽高来确定绘图区域的大小。

  2. 绘画API:Canvas提供了丰富的绘图API,包括绘制路径、直线、曲线、矩形、圆形、文本等。 我们使用这些API来创建各种图形,并自定义样式、颜色、透明度等属性。

  3. 动画:Canvas可以与JavaScript的动画函数结合使用,实现动态的图形效果。通过在每一帧中更新画布上的内容,能创建平滑的动画效果。

  4. 图像处理:Canvas可以加载和绘制图像。我们可以使用Canvas的API对图像进行裁剪、缩放、旋转等操作,从而实现图像处理的功能。

  5. 事件处理:Canvas支持鼠标和触摸事件的处理,我们可以通过监听这些事件来实现交互效果,例如点击、拖拽、缩放等等。


注意:Canvas 绘制的内容是即时生成的,它不会被浏览器缓存,所以每次页面加载Canvas都需要重新绘制,所以我们在使用时需要考虑性能问题。


星空


在介绍完Canvas之后,我们再来用CanvasJS结合,实现一片星空的效果。


第一步,我们先创建好html 结构,代码如下:


  <div class="landscape">
</div>
<canvas id="canvas"></canvas>
<div class="filter"></div>
<script src="./round_item.js"></script>

现在是没有效果的,我们在给它加上css代码,将其美化,同时我们再在css上加一些动画效果,完整代码如下:


* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}

body {
background: linear-gradient(to bottom, #000 0%, #5788fe 100%);

}

.landscape {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(./star/xkbg.png);
background-repeat: repeat-x;
background-size: 1000px 250px;
background-position: center bottom;
}
.filter{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 2;
background: #fa7575;
animation: colorChange 30s ease-in infinite;
}
/* 渐变动画*/
@keyframes colorChange {
0%,100%{
opacity: 0;
}
50%{
opacity: 0.7;
}

}

这时候,我们看到的效果是这样的:


1698327282484.gif


这效果好像只有黄昏的颜色变化,少了星空。那这最后一步,就该我们Canvas上场了,我们要用Canvas画出来一片星空,并配合css的颜色变化,实现一个夜晚到清晨的感觉。


我们 js 代码这样写:


//创建星星的函数
function RoundItem(index, x, y, ctx) {
this.index = index
this.x = x
this.y = y
this.ctx = ctx
this.r = Math.random() * 2 + 1
this.color = 'rgba(255,255,255,1)'
}
// 绘制
RoundItem.prototype.draw = function () {
this.ctx.fillStyle = this.color //指定颜色
this.ctx.beginPath() // 开始绘制
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false) // 绘制圆形
this.ctx.closePath() // 结束绘制
this.ctx.fill() //填充形状
}
//移动
RoundItem.prototype.move = function () {
this.y -= 0.5
this.draw()
}


let canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
round = [],
initRoundPopulation = 200; //星星的个数

const WIDTH = document.documentElement.clientWidth,
HEIGHT = document.documentElement.clientHeight;

canvas.width = WIDTH
canvas.height = HEIGHT

init()
// setInterval(animate, 1700)
animate()
function init() {
for (var i = 0; i < initRoundPopulation; i++) {
round[i] = new RoundItem(i, Math.random() * WIDTH, Math.random() * HEIGHT, ctx)
round[i].draw()
}
}
function animate() {
ctx.clearRect(0, 0, WIDTH, HEIGHT) //清除画布
for (let star of round) {
star.move()
}
requestAnimationFrame(animate) //通过刷新帧数来调用函数
}


将上述代码添加上之后,我们也就完成星星的绘制并添加到了页面上,最终的效果如下:
1698327645625.gif


到这里,我们就用canvas 画出了一片美丽的星空。同时我们也看到了canvas的强大功能,感兴趣的小伙伴可以深入的了解它更多的用法哦。


作者:潘小七
来源:juejin.cn/post/7294103091019612212
收起阅读 »

2D的雪碧图已经够炫了,那么3D的呢?

web
前言 前2篇文章,分别介绍了dat.gui和纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷! 这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/...
继续阅读 »

output-16_6_11.gif


前言


前2篇文章,分别介绍了dat.gui纹理贴图,老是理论没有实战也是没有什么意思的,今天我们就来着手一个小案例,赶紧实现起来,让你的博客更加炫酷!


这个案例包含了tweenjs动画库的使用,该动画库已在three.js中内置,路径为: examples/jsm/libs/tween.module.js,使用起来也是比较简单。


初始化


老样子,场景、相机、渲染器三要素初始化,并导入需要的插件库,插件库都已在three中内置:


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;

// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);

// 场景
scene = new THREE.Scene();

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
}

// 渲染
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera)
}

CSS3DSprite创建521个水球


// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
})
image.src = './static/img/sprite.png';


上面的代码中,我们创建了img标签,并使用CSS3DSpriteHTML元素转化为threejs的CSS3精灵模型,类似与转换成了three中的网格,并随机分布在-2000,2000的立方体中。


看下效果:


three04-1.jpg


添加控制器


关于控制器,前面也已经介绍过啦,通过控制器,我们就可以改变相机的位置,观察不同角度的物体。


// 定义控制器
let controls;
controls = new TrackballControls( camera, renderer.domElement );

// 渲染
function animate() {
...
controls.update();
...
}

注意哦,controls.update需要防止在animate中,每帧执行。


有了控制器。我们就可以实现交互啦:


output-15_37_12.gif


让小球按规律放大缩小


让小球按照正弦时间,放大缩小,即有一种闪烁的效果:


const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {
const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);
}

output-15_37_59.gif


让小球生成特定图形


生成矩形,对应的每个小球坐标:


const amount = 8;
const separationCube = 150;
const offset = ( ( amount - 1 ) * separationCube ) / 2;

for ( let i = 0; i < particlesTotal; i ++ ) {

const x = ( i % amount ) * separationCube;
const y = Math.floor( ( i / amount ) % amount ) * separationCube;
const z = Math.floor( i / ( amount * amount ) ) * separationCube;

positions.push( x - offset, y - offset, z - offset );

}

tween.js使用


const position = {x: 0,y: 0};
;//创建一段tween动画
const tween = new TWEEN.Tween(position)
//经过2秒,position对象的x和y属性分别从零变化为100、50
tween.to({x: 100,y: 50}, 2000);
//tween动画开始执行
tween.start();
// 动画效果 类似annimation
tween.easing()
// 完成时执行的钩子,里面可以继续执行下一个操作
tween.onComplete()

使杂乱的小球变成矩形


import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;

for (let i = 0; i < particlesTotal; i++) {

const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;

positions.push(x - offset, y - offset, z - offset);

}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);

controls = new TrackballControls(camera, renderer.domElement);

}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

杂乱的小球变成多种形态完整代码


限制文件大小啦,没法完全展示,大家自行运行看看吧!


output-15_49_5.gif


<div id="container"></div>
<script type="module">
import * as THREE from 'three';
// tween动画库
import TWEEN from 'three/addons/libs/tween.module.js';
//通过轨迹球控件TrackballControls 我们可以实现场景的旋转、缩放、平移等功能
import { TrackballControls } from 'three/addons/controls/TrackballControls.js'
// 雪碧图
import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'

// 定义场景、相机、渲染器
let scene, camera, renderer;
// 定义控制器
let controls;

// 定义小球数量
const particlesTotal = 512;
// 定义位置
const positions = [];
// 定义物体
const objects = []
let current = 0;
// 初始化
init()
// 渲染
animate();

function init() {
// 透视相机 远端距离最好设大一点 不然会展示不全
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
// 相机位置
camera.position.set(600, 400, 1500);
// 相机朝向位置
camera.lookAt(0, 0, 0);


scene = new THREE.Scene();

const image = document.createElement('img');
image.addEventListener('load', () => {
for (let i = 0; i < particlesTotal; i++) {
// cloneNode() 方法可创建指定的节点的精确拷贝
const object = new CSS3DSprite(image.cloneNode())
// 随机分布位置 -2000 2000的立方体内
object.position.x = Math.random() * 4000 - 2000,
object.position.y = Math.random() * 4000 - 2000,
object.position.z = Math.random() * 4000 - 2000,
scene.add(object)

objects.push(object);
}
transition()
})
image.src = './static/img/sprite.png';

// Plane
const amountX = 16;
const amountZ = 32;
const separationPlane = 150;
const offsetX = ((amountX - 1) * separationPlane) / 2;
const offsetZ = ((amountZ - 1) * separationPlane) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amountX) * separationPlane;
const z = Math.floor(i / amountX) * separationPlane;
const y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200;
positions.push(x - offsetX, y, z - offsetZ);
}

// Cube
const amount = 8;
const separationCube = 150;
const offset = ((amount - 1) * separationCube) / 2;
for (let i = 0; i < particlesTotal; i++) {
const x = (i % amount) * separationCube;
const y = Math.floor((i / amount) % amount) * separationCube;
const z = Math.floor(i / (amount * amount)) * separationCube;
positions.push(x - offset, y - offset, z - offset);
}

// Random
for (let i = 0; i < particlesTotal; i++) {
positions.push(
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000,
Math.random() * 4000 - 2000
);
}

// Sphere
const radius = 750;
for (let i = 0; i < particlesTotal; i++) {
const phi = Math.acos(- 1 + (2 * i) / particlesTotal);
const theta = Math.sqrt(particlesTotal * Math.PI) * phi;
positions.push(
radius * Math.cos(theta) * Math.sin(phi),
radius * Math.sin(theta) * Math.sin(phi),
radius * Math.cos(phi)
);
}

// 渲染画布
renderer = new CSS3DRenderer();
renderer.setSize(innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
controls = new TrackballControls(camera, renderer.domElement);
}


// 动画
function transition() {
const offset = current * particlesTotal * 3;
const duration = 2000;
for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) {
const object = objects[i];
new TWEEN.Tween(object.position)
.to({
x: positions[j],
y: positions[j + 1],
z: positions[j + 2]
}, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 3)
.onComplete(transition)
.start();
current = (current + 1) % 4;
}

// 渲染
function animate() {
requestAnimationFrame(animate);
controls.update();
TWEEN.update();

// 让小球按照正弦时间,放大缩小
const time = performance.now();

for (let i = 0, l = objects.length; i < l; i++) {

const object = objects[i];
const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1;
object.scale.set(scale, scale, scale);

}
renderer.render(scene, camera)
}

</script>

作者:八月十八
来源:juejin.cn/post/7294301361835147290
收起阅读 »

重复请求优化

web
设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:...
继续阅读 »

设想一种场景,有两个组件,这两个组件在初始化阶段,都需要调用同一个 API 接口去获取数据。为了防止请求冗余,可以把两个组件的请求都挪到父组件中,由父组件统一调用一次请求,然后再将响应的数据结果传给两个子组件。这种方法应该是最常见的,不过它也有一个局限性条件:两个组件必须有一个共同的祖先组件,如果这两个组件是同级的兄弟组件倒也还好,如果非同级,那么数据的传参就会有些麻烦了。那么还有其他办法吗?当然是有的。


我们可以换个思路,每个组件还是保持原有的业务逻辑不变,从请求接口处做文章。既然是同一个接口调用了两次,而且还是返回了相同的请求结果,那么不妨在第一次时调用成功时,就把请求结果缓存起来,等到第二次再调用时,直接返回缓存的数据。按照这个思路可以写出第一版的代码(这里用了 TS 方便查看参数的类型):


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* @param request 请求函数
*/

function cacheRequest<T>(request: (...args: any[]) => Promise<T>) {
const cache = Symbol("cache")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}


  • 首先 cacheRequest 函数,需要接收一个参数 requestrequest 是一个返回结果为 Promise 对象的函数。cacheRequest 执行完后返回一个新的匿名函数。

  • 然后,在匿名函数的内部,先判断 request 的原型对象上是否有 cache(这里的 cache 使用了 Symbol 类型,确保键名唯一)。也即,是否有缓存过的请求结果,如果没有,说明是第一次调用,则将 request 的执行结果存到缓存里。如果有缓存,则直接返回缓存。

  • 可以看到,缓存也是一个 Promise 类型。在同时调用多次请求时,只要在第一次调用执行后,已经把 Promise 存到缓存里了,后续的请求返回的也是缓存里的 Promise,从而保证多个请求都指向同一个 Promise ,也即只会调用一次 API 接口。

  • 这里需要注意一点,由于需要往 request 的原型对象上挂载缓存,所以 request 不能是箭头函数。因为箭头函数没有 this,也就意味着没有原型对象。


小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
return function (...args) {
if (!request.prototype[cache]) {
request.prototype[cache] = request(...args)
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
newRequest()

version1.png


可以看到虽然 newRequest 调用了三次,但是 fetch request 只打印了一次,也就是说 request 只调用了一次,符合预期!但是,最后一次 newRequest 的调用,是在 3 秒后调用的,也是走的缓存,没有重新执行。仔细思考一下,后续无论什么时候调用 newRequest 都会使用缓存里的数据,不会重新调用请求了,这显然是不合理的。我们还需要加个缓存的过期时间,超过这个时间,就重新发起新的请求。第二版如下:


/**
* 缓存请求的响应结果
* 把发起请求的 Promise 对象挂载在原型对象上
* 保证在 cacheTime 时间间隔内的多次请求,只会调用一次
* @param request 请求函数
* @param cacheTime 最大缓存时间(单位毫秒)
*/

export function cacheRequest<T>(request: (...args: any[]) => Promise<T>, cacheTime = 1000) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args: any[]): Promise<T> {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}


  • 首先,cacheRequest 新增一个入参 cacheTime,用于设置过期时间,默认为 1 秒。

  • 其次,在原型对象上新增了一个 lastTime 属性,用来记录最后一次调用的时间。

  • 当缓存为空,或者当前时间距离上一次调用时间超过缓存过期时间时,更新 cachelastTime


再来小测一下:


function cacheRequest(request) {
const cache = Symbol("cache")
const lastTime = Symbol("lastTime")
return function (...args) {
if (!request.prototype[cache] || Date.now() - request.prototype[lastTime] >= cacheTime) {
request.prototype[cache] = request(...args)
request.prototype[lastTime] = Date.now()
}
return request.prototype[cache]
}
}

const request = function () {
return new Promise(resolve => {
console.log("fetch request")
setTimeout(resolve, 2000)
})
}

const newRequest = cacheRequest(request)

newRequest()
newRequest()
setTimeout(newRequest, 3000)

version2.png


这一次,fetch request 打印了两次,符合预期,完美!


作者:showlotus
来源:juejin.cn/post/7294597695478333476
收起阅读 »

【Java集合】了解集合的框架体系结构及常用实现类,从入门到精通!

嗨~ 今天的你过得还好吗?以不同的方式长大谁都没有轻轻松松🌞- 2023.10.27 -通过Java基础的学习,我们掌握了Java语言主要的基本的语法,同时了解学习了Java语言的核心——面向对象编程思想。这篇文章就来带大家深入了解集合的框架体系结构...
继续阅读 »

640 (13).jpg

嗨~ 今天的你过得还好吗?

以不同的方式长大

谁都没有轻轻松松

🌞

- 2023.10.27 -

通过Java基础的学习,我们掌握了Java语言主要的基本的语法,同时了解学习了Java语言的核心——面向对象编程思想。这篇文章就来带大家深入了解集合的框架体系结构

从集合框架开始,也就是进入了java这些基础知识及面向对象思想进入实际应用编码的过程,通过jdk中集合这部分代码的阅读学习,就能发现这一点。


5bbd4f4683e25cfda6c3946e9925c48f.gif


本计划在这篇中把框架体系和一些集合的常用方法一起编写。仔细考虑之后,本着突出重点,结构清晰的思路,所以把框架体系单独拉出来,为让各位看官对java的集合框架有个清晰的认识,最起码记住常用的几种常用实现类!


好的,下面我们进入正题。


集合的框架体系结构

可以在很多JAVAEE进阶知识的学习书籍或者教程中看到,JDK中提供了满足各种需求的API,主要是让我们去学习和了解它提供的各种API。

54f18513580ae2f9b13d1b648e6c2380.jpeg

在使用这些API之前,我们往往需要先了解其继承与接口架构,才能了解何时采用哪个实现类,以及类之间如何彼此合作,从而达到灵活应用。


查看api文档,集合按照存储结构可以分为两大类,

  • 单列集合 java.util.Collection

  • 双列集合 java.util.Map


通过jdk api 来看在 JDK中 提供了丰富的集合类库,为了便于初学者进行系统地学习,我们通过结构图来分别描述集合类的继承体系。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


640 (27).gif


Collection

Description

Collection: 单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口:

  • java.util.List List的特点是 元素有序,元素可重复。

  • java.util.Set Set的特点是 元素无序(不全是),而且不可重复


List 接口主要的实现类有 java.util.ArrayList 和 java.util.LinkedList,Set 接口的主要实现类有 java.util.HashSet 和 java.util.TreeSet。



640 (27).gif

Map

Description

Map: 双列集合,用于存储具有映射关系的对象。常用的实现类有

  • java.util.HashMap

  • java.util.LinkedHashMap

注:图片中 小标中有 I的都是接口类型,而 C 的都是具体的实现类。


好的,框架的介绍就到这里了。本文中主要介绍了框架的两大类,以及我们在开发工作中使用的几种常见的接口和实现类,在后面的文章中,一一介绍吧。



收起阅读 »

俞敏洪:我曾走在崩溃的边缘

web
大家在人生的经历中遇到过很崩溃的事情吗? 我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。 长时间下来,这个习惯让我对名人的苦难经历...
继续阅读 »

大家在人生的经历中遇到过很崩溃的事情吗?


我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。


长时间下来,这个习惯让我对名人的苦难经历或者处理棘手问题的经历有强烈的好奇心。最近,读了俞敏洪的自传《我曾走在崩溃的边缘》,感觉挺有意思。


俞敏洪是新东方的老板,在“双减”政策之后,新东方转型做了直播,也就是大家熟知的东方甄选,可能很多人还买过他们直播间的大米。当然,我没有买过,因为理智促使我很少为情怀买单。


离开北大


俞敏洪曾经是北大的老师,他的梦想是出国留学。但老师的工资低,很难赚够出国的学费。作为南方人的他,天生的商人基因让他找到了赚钱的路子——开英语培训班。这条路子获得的收入比工资高十几倍,利润十分丰厚。


于是,他打着北大的招牌私下招生,这意味着和北大“官方”的托福培训班形成了竞争关系。学校当然不会允许北大老师和北大抢生意,况且学校禁止老师私下办培训班。俞老师无法避免地和校领导发生了冲突,并因此被处分。


图片


处分的通告在学校的高音喇叭上足足播了一个礼拜,这件事情闹得人尽皆知,对俞敏洪名声的伤害极大。后来,学校分房自然没有俞老师的份了。在中国的社会体系下,名声对一个人来说极其重要。这种“德治”社会虽然在人口大国里对秩序起着巨大的作用,但也给一些人带来了巨大伤害。一遭名声败坏,要背一辈子,这对当事人是多大的打击。


那时俞敏洪已经结婚,本可以在大学教书过安稳的生活,但这一纸处分,让他决定从北大离职。最后,他骑着三轮车拉着家当离开了北大,开启了新东方的事业。


图片


死磕办学证


办培训班需要办学证,类似于现在的牌照。如果没有就无法公开招生,这意味着无法扩大规模。俞敏洪没办法,找了当时一个叫东方大学的机构联合办培训班,条件是支付总收入的25%给东方大学。


东方大学不参与招生、培训等所有事情,却要分掉一大笔钱。随着培训班的规模越来越大,俞敏洪意识到这不是长久之计,他决定就算再难,死磕也要把办学证拿到手。


要拿到办学证要符合两个条件:一是必须有大学副教授以上职称,二是要经原单位同意。


俞敏洪在北大只是讲师,没有副教授职称,而且北京大学处分了他,不可能同意他办学。两个条件都不符合,教育局直接拒绝,并叫他不要来了。


不得不说,俞老师的脸皮是够厚的,每隔一两星期就去教育局和办事的人聊天,久了大家就混熟了。


大概耗了半年,教育局放低了办学的要求,只要他能够在人才交流中心拿到允许办学证明就放行。可是人才交流中心的工作人员根本不给他开证明。直到遇见他一个在这里工作的学生,在她的帮助下才拿到证明。


办学证到手后,俞敏洪离开东方大学,开始独立办培训班。原来的“东方大学外语培训部”这块招牌积累了相当的名气,新东方成立后,大量学生还去那边报名。为了顺利切换品牌,新的培训机构起名叫新东方,而且从东方大学买断了“东方大学外语培训部”三年的使用权,每年支付20万。


这一系列的操作,可见俞敏洪有相当不错的商业头脑。


被赶下董事长的位置


中国是一个人情社会,比如亲情、友情、同学情。在这种社会成长起来的人,自然会想到找自己熟悉的人一起做事业。俞敏洪也不例外。新东方的培训班办得风生水起,俞敏洪开始寻找人才。


除了拉亲人朋友入伙,他还出国把大学同学王强、徐小平拉回来一起跟他干事业。这三人被称为“东方三驾马车”,也就是电影《中国合伙人》的原型。


image.png


亲人、同学、朋友之间,天然有信任感,在事业的初创阶段一起工作沟通效率非常高,而且为了共同的目标,凝聚力非常强。


当公司到了一定的规模,这种人情关系构建起来的团队,会使公司的人事关系变得非常复杂。


一是,团队没有组织架构,决策效率低下;二是,老板没有话语权,下面的人不知道该听谁的,却谁都不敢得罪。


后来,在新东方改革期间,创始团队出现各种矛盾,俞敏洪无法短期内处理好这些矛盾,被管理层认为是不合格的董事长。于是,俞敏洪从位置上退了下来。


退位期间,其他几个领导轮流做主,也无法处理好团队的矛盾。俞敏洪开始大量阅读公司管理、股权管理的书籍,积累比其他领导更丰富的管理知识。两三年后,他重新回到董事长的位置上。


他能回到位置上,管理知识是一方面,我斗胆猜测,运气的成分占比很大。毕竟被自己的公司赶走的大有人在。


结尾


除了上面3个故事,俞敏洪还有很多非常精彩的故事,比如“被抢劫险些丧命”、“知识产权侵权风波”、“新东方上市”、“遭遇浑水公司做空”等等。


语言是思想的外衣。他来自农村,《我曾走在崩溃的边缘》这本书语言坦诚,像他本人一样。他的人生非常精彩,展现了他强大的韧性。


他的成功,有时代的机遇,也有个人的努力。我们可能无法准确把握时代的机遇,但可以学习他的努力和韧性,在崩溃之时屹立不倒。


作者:华仔很忙
来源:juejin.cn/post/7218487123212091450
收起阅读 »

JS小白请看!一招让你的面试成功率大大提高——规范代码

web
前言 规范的代码是可以帮你进入优秀的企业的。一个规范的代码,通常能起到事半功倍的作用。并非规范了就代表高水平,实际上是规范的代码更有利于帮助你理解开发语言理解模式理解架构,能够帮助你快速提升开发水平。今天我们就来聊聊,如何规范我们的代码,如何优化我们的代码,如...
继续阅读 »

前言


规范的代码是可以帮你进入优秀的企业的。一个规范的代码,通常能起到事半功倍的作用。并非规范了就代表高水平,实际上是规范的代码更有利于帮助你理解开发语言理解模式理解架构,能够帮助你快速提升开发水平。今天我们就来聊聊,如何规范我们的代码,如何优化我们的代码,如何使我们的代码可读性提高。


正文



  • 这里我们先放出一道面试题


输入一个数组,例如:array [1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
返回一个固定格式的电话号码 例如:(123456-789

function phoneNumber(numbers){

}


  • 注释


当我们拿到这道题时,我们需要先做什么呢?是一上来直接实现这个函数的功能吗?一般人可能就这样上了,但是,如果面试官看到你这样去写代码,这样会显现你的编程素养特别差,面试官对你的好感度肯定也会随之下降。那我们应该怎么做呢?我们需要先写一个良好的注释


/**
* @func 返回固定格式的电话号码 函数功能
* @params array [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
* @return (123) 456-7890
* @author jser
*/

// 函数定义
function phoneNumber(numbers) {

}

良好的注释可以提高代码的可读性。小伙伴们要记住,代码的可读性高于一切!


在大公司中,一份代码可能会经过许多的程序员阅读或修改。如果你写了良好的注释,当你的代码被他人阅读时,其他程序员可以快速读懂这份代码,或者根据自己的需要去修改这份代码,这样大大节省了时间,也提高了团队的效率。



  • 换行


当我们把良好的注释写好之后,就可以写代码去实现功能了。


function phoneNumber(numbers) {
return "(" + numbers[0] + numbers[1] + numbers[2]+ ")" + " " + numbers[3]+numbers[4]+numbers[5] + "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9]+""
}

“什么?这是什么代码,还需要我拖动去查看后面的,你可以走了!”


相信很多小伙伴看到这串代码时,也跟小编一样的头疼,这代码也看的太费劲了吧,怎么全部都挤在一行里去了。运行了一下之后,发现运行结果是对的,但是小编是不建议大家这样写代码的,我们要适当的进行换行操作,这样同样提高了代码的可读性。


function phoneNumber(numbers) {
return "(" + numbers[0] + numbers[1] + numbers[2]
+ ")" + " " + numbers[3] + numbers[4] + numbers[5]
+ "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9] + ""
}

ES6


ES5和ES6都是JavaScript语言的版本。ES5是ECMAScript 5的简称。自ES6(ECMAScript2015)出来后,ES6引入了一些新的语法和关键字,使得代码更加易读、易写,提高了可维护性,例如解构赋值、箭头函数、模板字符串等。



  • 箭头函数


在ES5旧版本中,很多小伙伴会觉得function很繁琐,而且到处都是function。而箭头函数可以算是函数的简版,它的结构变得比函数简单。那我们如何使用箭头函数呢?



  1. 去掉function,在()和{}之间加=>

  2. 如果参数列表只有一个形参,可省略()

  3. 如果函数体只有一句话,可省略{},如果仅有的一句话函数体是return xxx,就必须省略return


在上面那个例子里,我们可以这样写箭头函数


phoneNumber = (numbers) =>"(" + numbers[0] + numbers[1] + numbers[2]
+ ")" + " " + numbers[3] + numbers[4] + numbers[5]
+ "-" + numbers[6] + numbers[7] + numbers[8] + numbers[9] + ""


可以看出,箭头函数的结构比我们使用旧版的函数会简单许多,小伙伴们可以选择使用啊。



  • 模板字符串


模板字符串,也称为模板字面量,是 ECMAScript 6(ES6)引入的一种新的字符串表示法。它允许在字符串中嵌入变量和表达式,使用反引号(``)包围字符串内容。与传统字符串拼接相比,模板字符串具有以下优势:


模板字符串允许在字符串中插入变量值或表达式,使用 ${} 语法。这使得代码更加清晰和可读,不需要繁琐的字符串拼接。


const name = 'junjun'; 
const greeting = `Hello, ${name}!`; // 使用模板字符串插入变量
console.log(greeting);// 输出:Hello, junjun!

那机灵的小伙伴就问了,电话号码那个例子是不是也可以使用模板字符串,我们直接上代码


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]})
${numbers[3]}${numbers[4]}${numbers[5]}
-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`

//输出 (123)
// 456
// -7890

根据结果可以看到,使用模板字符串时,当我们进行换行操作时,模板字符串也会换行。模板字符串虽然方便,但小伙伴们要记住,不是在所有情况下,都可以使用模板字符串的。


总结


代码的可读性高于一切。我们作为小白,在慢慢成长的过程中,一定要尽早的规范自己的代码,提高代码的可读性,学习ES6的语法。当我们以后进入企业工作之后,公司会统一我们的代码风格,并且规定使用哪些语句。


作者:来颗奇趣蛋
来源:juejin.cn/post/7294080876827901993
收起阅读 »

Echarts添加水印

web
如果直接说水印,很难在官方找到一些痕迹,但是换个词【纹理】就能找到了。水印就是一种特殊的纹理背景。 Echarts-backgroundColor backgroundColor 支持使用rgb(255,255,255),rgba(255,255,255,...
继续阅读 »

如果直接说水印,很难在官方找到一些痕迹,但是换个词【纹理】就能找到了。水印就是一种特殊的纹理背景。



Echarts-backgroundColor


backgroundColor



支持使用rgb(255,255,255)rgba(255,255,255,1)#fff等方式设置为纯色,也支持设置为渐变色和纹理填充,具体见option.color



color


支持的颜色格式:




  • 使用 RGB 表示颜色,比如 'rgb(128, 128, 128)',如果想要加上 alpha 通道表示不透明度,可以使用 RGBA,比如 'rgba(128, 128, 128, 0.5)',也可以使用十六进制格式,比如 '#ccc'




  • 渐变色或者纹理填充


    // 线性渐变,前四个参数分别是 x0, y0, x2, y2, 范围从 0 - 1,相当于在图形包围盒中的百分比,如果 globalCoord  `true`,则该四个值是绝对的像素位置
    {
    type: 'linear',
    x: 0,
    y: 0,
    x2: 0,
    y2: 1,
    colorStops: [{
    offset: 0, color: 'red' // 0% 处的颜色
    }, {
    offset: 1, color: 'blue' // 100% 处的颜色
    }],
    global: false // 缺省为 false
    }
    // 径向渐变,前三个参数分别是圆心 x, y 和半径,取值同线性渐变
    {
    type: 'radial',
    x: 0.5,
    y: 0.5,
    r: 0.5,
    colorStops: [{
    offset: 0, color: 'red' // 0% 处的颜色
    }, {
    offset: 1, color: 'blue' // 100% 处的颜色
    }],
    global: false // 缺省为 false
    }
    // 纹理填充
    {
    image: imageDom, // 支持为 HTMLImageElement, HTMLCanvasElement,不支持路径字符串
    repeat: 'repeat' // 是否平铺,可以是 'repeat-x', 'repeat-y', 'no-repeat'
    }



水印


通过一个新的canvas绘制水印,然后在backgroundColor中添加


const waterMarkText = 'YJFicon'; // 水印
const canvas = document.createElement('canvas'); // 绘制水印的canvas
const ctx = canvas.getContext('2d');
canvas.width = canvas.height = 100; // canvas大小 - 控制水印间距
ctx.textAlign = 'center'; // 文字水平对齐
ctx.textBaseline = 'middle'; // 文字对齐方式
ctx.globalAlpha = 0.08; // 透明度
ctx.font = '20px Microsoft Yahei'; // 文字格式 style size family
ctx.translate(50, 50); // 偏移
ctx.rotate(-Math.PI / 4); // 旋转
ctx.fillText(waterMarkText, 0, 0); // 绘制水印

option = {
//...
backgroundColor: {//在背景属性中添加
// type: 'pattern',
image: canvas,
repeat: 'repeat'
}
...
}

image.png


如果只想在 toolbox.saveAsImage 下载的图片才展示水印,toolbox.feature.saveAsImage 支持配置backgroundColor,将其设置为水印【纹理】即可


option = {
//...
toolbox: {
show: true,
feature: {
...
saveAsImage: {
type: 'png',
backgroundColor: {
// type: 'pattern',
image: canvas,
repeat: 'repeat'
}
}
}
}
//...
}

graphic


除了使用纹理背景,还可以 graphic 添加图形元素,其中包括 text,可用于绘制水印。


option = {
//...
graphic: [
{
type: 'group',
rotation: Math.PI / 4,
bounding: 'raw',
top: 100,
left: 100,
z: 100,
children: [
{
type: 'text',
left: 0,
top: 0,
z: 100,
style: {
fill: 'rgba(0,0,0,.2)',
text: 'ECHARTS',
font: 'italic 12px sans-serif'
}
},
{
type: 'text',
left: 40,
top: 40,
z: 100,
style: {
fill: 'rgba(0,0,0,.2)',
text: 'ECHARTS',
font: 'italic 12px sans-serif'
}
}
]
},
],
//...
}

比较繁琐,需要自己设置平铺规律,建议封装一个平铺方法,不能控制区分 saveAsImage。


最初想象


预研


通过源码可以知道 saveAsImage 的实现也是通过内置 API getConnectedDataURL 获取url(base64格式),然后赋值到 a 标签上(带download属性)实现下载。


class SaveAsImage extends ToolboxFeature<ToolboxSaveAsImageFeatureOption> {

onclick(ecModel: GlobalModel, api: ExtensionAPI) {
const model = this.model;
const title = model.get('name') || ecModel.get('title.0.text') || 'echarts';
const isSvg = api.getZr().painter.getType() === 'svg';
const type = isSvg ? 'svg' : model.get('type', true) || 'png';
const url = api.getConnectedDataURL({
type: type,
backgroundColor: model.get('backgroundColor', true)
|| ecModel.get('backgroundColor') || '#fff',
connectedBackgroundColor: model.get('connectedBackgroundColor'),
excludeComponents: model.get('excludeComponents'),
pixelRatio: model.get('pixelRatio')
});
const browser = env.browser;
// Chrome, Firefox, New Edge
if (isFunction(MouseEvent) && (browser.newEdge || (!browser.ie && !browser.edge))) {
const $a = document.createElement('a');
$a.download = title + '.' + type;
$a.target = '_blank';
$a.href = url;
const evt = new MouseEvent('click', {
// some micro front-end framework, window maybe is a Proxy
view: document.defaultView,
bubbles: true,
cancelable: false
});
$a.dispatchEvent(evt);
}
// IE or old Edge
else {
// ...
}
}
// ...
}

思路


可以通过扩展此方法,先获取原始的图片url,基于这个图片重新绘制一个canvas,然后在这个基础上覆盖水印,最后将canvas再次转成url返回。



  • echartsInstance._api.getConnectedDataURL

  • echartsInstance.__proto__.getConnectedDataURL


含有两处实例方法需要处理。


结果



  • ✅可以拦截默认行为获取到添加水印后的图片url

  • ❌通过原api方法获取到url后绘制到新的canvas,涉及到异步处理(img标签需要等待load),由于 saveAsImage 调用 getConnectedDataURL 获取url是同步过程,因此无法正确读取到异步处理完的最终url,导致下载失败;不过,手动调用实例getConnectedDataURL 可以使用,需要配置Promise语法使用。


伪代码:


const originGetConnectedDataURL = echartsInstance._api.getConnectedDataURL
echartsInstance._api.getConnectedDataURL = async function () {
const origin = originGetConnectedDataURL.call(echartsInstance, ...arguments)
const result = await toWaterUrl(origin)
return result
}

function toWaterUrl (url) {
return new Promise(resolve => {
const img = new Image()
img.src = url + '?v=' + Math.random();
img.setAttribute('crossOrigin', 'Anonymous');
img.onload = function() {
// drawCanvas img 转 canvas
// afterWater canvas 绘制水印
resolve(afterWater(drawCanvas(img))
}
})
}

附带drawCanvas/afterWater


function drawCanvas(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
return canvas;
}

function afterWater(canvas, { text = 'WangSu', font = 'italic 10px', fillStyle = 'red', rotate = -30 }, type = 'png') {
return new Promise((resolve, reject) => {
let context = canvas.getContext('2d');
context.font = font;
context.fillStyle = fillStyle;
context.rotate(rotate * Math.PI / 180);
context.textAlign = 'center';
context.textBaseline = 'Middle';
const textWidth = context.measureText(text).width;
for (let i = (canvas.height * 0.5) * -1; i < 800; i += (textWidth + (textWidth / 5))) {
for (let j = 0; j < canvas.height * 1.5; j += 128) {
// 填充文字,i 间距, j 间距
context.fillText(text, i, j);
}
}
resolve(canvas.toDataURL('image/' + type))
})
}

作者:wangsd
来源:juejin.cn/post/7290417906840322106
收起阅读 »

前端开发,微信公众号静默网页授权,本地调试及上线

web
1、前言 基于今天有个朋友问了我相关公众号授权的一些流程问题,今日来记录记录以及讲解一下如果是前端,没怎么了解后端,一些情况下前端自己调试静默授权一些东西,或者想自己试试自己公众号来玩一玩授权。 2、工具 既然在本地调试,那自然少不了微信开发者工具了,对,...
继续阅读 »

1、前言



基于今天有个朋友问了我相关公众号授权的一些流程问题,今日来记录记录以及讲解一下如果是前端,没怎么了解后端,一些情况下前端自己调试静默授权一些东西,或者想自己试试自己公众号来玩一玩授权。



2、工具


既然在本地调试,那自然少不了微信开发者工具了,对,没错,就是那个微信开发者工具。这玩意是微信公众号官方推出来的一款专门给用户制作微信小程序小游戏和公众号的软件。


image.png






概念: 用户在微信客户端访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。



题外话: 每个用户在不同的公众号openid不同。如果需要在多个公众号统一用户的账号的话,就需要UnionID在这里配同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。


3、流程




🔴 微信开发者工具


前期我们需要在本地进行调试,也就是在微信开发者工具里面调试。下载完成之后,打开软件出现二维码:


image.png


微信扫码进去。


手机端出现是否确定登录微信开发者工具,点击确认登录进去。


image.png


进来,点击公众号网页项目进去,


image.png




🔴 网页授权解剖


我们来看一下文档网页授权这块,文档当中提及两种授权,


一种是不弹出授权页面,直接跳转,就能获取用户openid(scope为snsapi_base),


另外一种是弹出授权页面,可通过openid拿到昵称、性别、所在地。并且呢,即使在未关注的情况下,只要用户授权,也能获取这个用户相关信息


1、scope=snsapi_base(静默,无感知):


snsapi_basescope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页面。[用户感知的就是直接进入回调页(往往是业务页面)]


2、snsapi_userinfo(弹框,需要用户手动同意):


snsapi_userinfoscope发起的网页授权,是用来获取用户的基本信息的。[需要用户手动同意,无需关注,用户同意授权给我们去拿他们的相关基本信息]


至于第3种就是需要用户关注了公众号之后,才能得到用户的openid去得到用户的一些基本信息的,这里只讲前面两种。


无论是上面1还是2,都需要条件为已认证服务号,需要引导用户打开地址



注意:



如果在地址栏中输入https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect这个地址,直接提示改链接无法访问的话,解决(1、看参数正确与否,2、看认证没认证,也就是scope参数授权作用域权限有没有)。


🔴 链接格式


1、静默授权snsapi_base


https://open.weixin.qq.com/connect/oauth2/authorize?appid=这个公众号的appid&redirect_uri=后端跳转拿code接口地址&response_type=code&scope=snsapi_base&state=123#wechat_redirect


2、弹窗用户同意授权 snsapi_userinfo:


https://open.weixin.qq.com/connect/oauth2/authorize?appid=这个公众号的appid&redirect_uri=后端跳转拿code接口地址&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect


参数说明
appid公众号的appid
redirect_uri授权后重定向的回调链接地址,使用urlEncode处理
response_type返回类型,就填code就可以了
scope两个,上面👆讲过,一个静默snsapi_base,一个需同意snsapi_userinfo
state重定向会带上state参数,可填a-zA-Z0-9
#wechat_redirect必带


到最后会跳转到redirect_uri/?code=CODE&state=STATE


我们做这些授权的目的就是为了得到code,code这个玩意就是得到access_token的敲门砖,code每次授权都不一样,每次的code只能使用一次,5分钟过期



4、沙盒测试(本地调试)


🔴 配置公众号平台测试账号


image.png


在【设置与开发】-【开发者工具】-【公众平台测试账号】,点进去。


这里呢,微信官方为我们提供了一个测试号,我们本地调试的话,先这个测试号来调网页授权功能。后期部署到线上,再换成我们自己这个公众号的appid和配置线上后端的域名,这是后面本地调试没问题了,再放到线上到这一步。


测试号的appid和appsecret,到后面有用:


image.png


你就看到这里就行了,其他的不用管,看到网页服务-网页账号那里,去授权网页授权获取用户基本信息


image.png


image.png


点击修改,进去网页授权域名填写,就是你希望跳转的地址的域名,这里本地调试可以填ip:port (ip:端口)这样。本地开发不用域名,当然如果你host映射重定向到一个域名(这种就是简单东西复杂化)也不是不行,就是没必要。


做完这些配置,就到代码部分了。


🔴 代码


前端,写一个页面:


代码如下:


auth.html js部分 (直接跳)



<script>
window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=测试号的appid&redirect_uri=" +
"http://192.168.0.57:8001/(注释:本地的调试获取code的地址)" +
"&response_type=code&scope=snsapi_base&state=1#wechat_redirect";
</script>

这里我用vscode的插件live_server跑起来的,地址是http:127.0.0.1:5501/auth.html


接收的话也是这个页面来接收code,得到code之后,就可以为所欲为了(bushi),就可以传给后端就获取token了,前端也可以。


获取token的过程:



获取code后,请求以下链接获取access_token:




请求这个接口 https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code



传参是 appidsecretcodegrant_type写authorization_code。


最后得到的json数据就是下面👇这个:


image.png


具体页面代码是这样的:


image.png


🔴 效果


Kapture 2023-10-25 at 17.55.46.gif


5、上线



此文讲的是本地调试怎么调,上线之后呢,我们就换,appid要换,把测试appid换成线上公众号appid(后台拿),网页授权域名要加(在【设置与开发】-【公众号设置】-【功能设置】-【网页授权域名】那里改成我们线上的域名,一个月只能改5次,可加两个域名)。



6、总结


没有太多花里胡哨的东西,就是通过code拿token,code怎么来,code通过跳转得来,这就完事了。



☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学习。感谢阅读,祝您开发有乐趣。



作者:盏灯
来源:juejin.cn/post/7293804736754106377
收起阅读 »

个人创业中的全栈开发经验

web
前言 个人项目开发创业半年有余,两个项目全部扑街,一无所获。 仔细想来其实也不是什么都没有得到,因为现状就是,我创业开始前能预想到的最坏情况,哪怕一毛钱都挣不到,但是也可以从中积累一些经验,比如微信小程序的开发经验。 我过去7年的工作都是在从事前端开发,从最开...
继续阅读 »

前言


个人项目开发创业半年有余,两个项目全部扑街,一无所获。


仔细想来其实也不是什么都没有得到,因为现状就是,我创业开始前能预想到的最坏情况,哪怕一毛钱都挣不到,但是也可以从中积累一些经验,比如微信小程序的开发经验。


我过去7年的工作都是在从事前端开发,从最开始从事IPTV 开发,用原生JS、JQuery 开发运行在机顶盒上JSP 页面;到18年,创建了项目组的第一个Vue 项目,那时候我才算是开始“现代”前端的工作;21年到上海,在新公司开始全面使用React + TS,也就是时至创业开始,我所有的工作技能,都是前端技术,后端相关的只有自己瞎折腾的项目,没有真正应用到实际项目中的,这次也算是逼着自己进步了一把。


技术选型


前端 - 后台管理系统:React + TS,用了Antd 的组件库提供的模板直接创建项目


前端 - 微信小程序:原生微信小程序开发 + Vant Weapp


服务端:微信云开发


为什么要选用以上技术栈,只有有一个原因,就是成本极低!非常低!并且很稳定,前后端全部用JS 搞定;后台管理系统部署在腾讯云的Web 应用托管上,直接免去运维工作。说个题外话,前几年自己搞个人网站的时候,服务器是薅的阿里云的羊毛,结果就是啥活都得自己干,用Express 框架开发的后端服务,用Nginx 做代理,结果并发超过100个 服务器直接挂掉。。


现在这一套技术栈,几乎没有学习成本,腾讯云的Web 应用托管集成了CI 工具,提交代码到线上分之后,自动部署,用了半年多,网站、小程序都没有挂掉过。(我真不是腾讯的托。。)


后台管理系统


React、TS、Antd 业务开发技术不多赘述,讲讲怎么在Web 端请求微信云开发的接口吧。


微信云开发提供了可访问云服务的Web Sdk,引入sdk 后,只需要进行简单的初始化,即可访问接口。


云开发登录授权配置,打开匿名登录


image.png


示例代码

处理请求


import cloudbase from "@cloudbase/js-sdk";
...
const env = ""; // 环境id
const clientId = ""; 终端id

// 创建实例
const app: any = cloudbase.init({
env,
clientId,
});

const auth = app.auth({
persistence: "local",
});

...
// 请求方法
export const cloudFn = async (
type: string,
params?: any
): Promise<any> => {
// 判断登录态
if (!auth?.hasLoginState()) {
localStorage.clear();
await auth.signInAnonymously();
}

const res = await app.callFunction({
name: "xxxx", // 云函数名称
data: { type, data: options?. }, // 传参
parse: isDev, // 环境
});

// 根据自己的业务方式处理返回数据
...


处理接口


import { cloudFn } from "@/utils";

export const xxx = (params: API.xxx) => {
return cloudFn("name", params);
};

Web Sdk 官方文档:docs.cloudbase.net/api-referen…


部署

提交代码到部署分之后,会自动部署,访问web 应用托管,会提供一个默认访问的域名,可以直接访问,但是不推荐生产使用,只需要再配置一个域名就好了。


微信小程序


如果没有开发过微信小程序,去看一下官方文档,前端基本可以无成本上手,参照官方文档开发就好;为什么组件库选择Vant Weapp,基本补全了官方没有提供的组件,使用方式也很简单,实际使用后体验不错,值得推荐。


微信云开发


我用Java、python、node 都写过后端接口,对于一个前端来说,单论简单、好上手而言,微信云开发,我愿称之为YYDS!就两个字,简单!


官方提供了请求的方法,我对其简单的封装了一下,如果觉得不错,尽管拿去用,如果有不完善的,还请指正


示例代码

云函数入口 index.js

const user = require('./user);

exports.main = async (event, context) => {
switch (event.type) {
case "userGet":
case "userUpdate":
return await user.main(event, context);


default:
return {
code: -1,
msg: '
接口不存在'
}
}
};

user 入口

const get = require("./get");
const update = require("./update");

exports.main = async (event) => {
const apiType = event.type
const data = event.data || {};

if (apiType === 'userGet') {
return await get.main(data);
};

if (apiType === 'userUpdate') {
return await update.main(data);
};
};

user/get.js

const {
dbGet, // 通用get 方法 (见后问)
filterParams, // 清除异常参数,比如空字符串,null 等
} = require("../../utils");
const check = require('./check');

exports.main = async (data) => {
// 校验参数
if (check(data)) return {
code: -1,
msg: check(data),
}

const params = {
offset: data.offset,
limit: data.limit,
name: data.name,
};


// 模糊搜索
if (params.name) {
params.name = {
$regex: ".*" + params.name,
$options: "i",
};
}

return await dbGet("user", filterParams(params));
};

user/update.js

const {
dbUpdate
} = require("../../utils");
const check = require('./check');

exports.main = async (data) => {
if (check(data)) return {
code: -1,
msg: check(data),
}


const params = {
_id: data._id,
name: data.name
};

return await dbUpdate("user", params);
};

utils.js

const cloud = require("wx-server-sdk");

// 初始化云环境
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
});

const db = cloud.database();

async function dbGet(
databaseTable, // 表名
params, // 参数
orderByKey = "", // 排序参数
order = "desc" // 排序方式
) {
const pageInfo = {
offset: params.offset || 1,
limit: params.limit || 10,
};
delete params.offset;
delete params.limit;

try {
// 获取总数
const resCount = await db
.collection(databaseTable)
.where(params)
.count();

const resCountData = formatRes(resCount)
if(resCount?.code !== 0) {
return resCountData
}

// 总数是0,直接返回数据
if(resCount?.data === 0) {
return {
code: 0,
data: { data: [] },
total: 0,
}
}

// 获取数据
const res = await db
.collection(databaseTable)
.where(params)
.skip((pageInfo.offset - 1) * pageInfo.limit) // 分页
.limit(pageInfo.limit) // 最多几条
.orderBy(orderByKey, order) // 排序
.get();

// 处理返回数据
const resData = formatRes(res);
if(resData?.code === 0) {
return {
code: 0,
data: {
data: resData?.data || [],
total: resCountData?.data || 0
},
}
} else {
return resData
}
} catch (error) {
return {
code: -2,
data: null,
msg: "请求失败",
};
}
}

async function dbUpdate(databaseTable, updateData) {
let res;
let isSuccess = false;
try {
const params = updateData;

if (updateData._id) {
// 编辑
delete params._id;
res = await db.collection(databaseTable).doc(updateData._id).update({
data: params,
});

if (res.errMsg === "document.update:ok") {
isSuccess = true;
}
} else {
// 新增
res = await db.collection(databaseTable).add({
data: params,
});

if (res.errMsg === "collection.add:ok") {
isSuccess = true;
}
}
if (isSuccess) {
return {
code: 0,
_id: res._id,
msg: `${updateData._id ? "更新" : "新增"}数据成功`,
};
} else {
return {
code: -1,
data: null,
msg: `${updateData._id ? "更新" : "新增"}数据失败`,
};
}
} catch (error) {
return {
code: -1,
data: null,
msg: `请求服务器失败,${updateData._id ? "新增" : "更新"}数据失败`,
};
}
}

function formatRes(res) {
const cloudFnMsgList = ["document.get:ok", "collection.get:ok", "collection.count:ok"];

if (cloudFnMsgList.includes(res?.errMsg)) {
return {
code: 0,
data: res.data || res.total,
};
} else {
return {
code: -1,
data: null,
msg: "请求服务器失败",
};
}
}

module.exports = {
dbGet,
dbUpdate,
formatRes,
};

差不多到此就结束了,Web 端的后台管理系统,微信小程序的后端接口实现了,并且可以互通,这种方式是我实践过,在保证业务、性能、稳定的前提下,最低成本的全栈开发方案,如果有其他更好的方案,欢迎讨论。


作者:鹿林秋月
来源:juejin.cn/post/7294056563631079424
收起阅读 »

说说js代码写到html里还是单独写到js文件里哪个好?为什么?

web
"将 JavaScript 代码写入 HTML 文件与将其单独写入 JavaScript 文件之间有一些考虑因素。下面是一些关于两种做法的优缺点和适用场景的讨论: 将 JavaScript 代码写入 HTML 文件的优点: 方便快捷:将 JavaScript...
继续阅读 »

"将 JavaScript 代码写入 HTML 文件与将其单独写入 JavaScript 文件之间有一些考虑因素。下面是一些关于两种做法的优缺点和适用场景的讨论:


将 JavaScript 代码写入 HTML 文件的优点:



  • 方便快捷:将 JavaScript 代码直接嵌入到 HTML 文件中可以节省创建和加载额外文件的时间,特别是对于小型项目或仅需少量 JavaScript 代码的情况。

  • 直观可见:通过将 JavaScript 代码嵌入到 HTML 文件中,可以更容易地将其与相关的 HTML 元素和结构联系起来,使代码的逻辑更加清晰明了。


将 JavaScript 代码单独写入 JavaScript 文件的优点:



  • 结构清晰:将 JavaScript 代码与 HTML 分离可以使代码结构更加清晰,提高代码的可读性和可维护性。这样做有助于保持 HTML 文件的简洁和专注于内容。

  • 可重用性:将 JavaScript 代码存储在单独的文件中,可以使其在多个 HTML 文件中重复使用,提高代码的可重用性和一致性。

  • 缓存优化:当 JavaScript 代码被单独提取到外部文件中时,浏览器可以将其缓存起来,从而提高页面加载速度并节省带宽。


综上所述,将 JavaScript 代码写入 HTML 文件适合小型项目或仅需少量 JavaScript 代码的情况,以及需要快速原型设计或简单交互的情况。而将 JavaScript 代码单独写入 JavaScript 文件适合大型项目或需要复杂的逻辑和结构的情况,以及需要提高代码的可读性、可维护性和可重用性的情况。根据项目的需求和规模,我们可以灵活选择适合的方式来组织和管理 JavaScript 代码。"


作者:打野赵怀真
来源:juejin.cn/post/7294171458032336906
收起阅读 »

规范化注释你的代码,成为一名优秀程序员的必经之路!

web
前言 想要成为一名优秀的程序员,首先应该具有的是良好的编程素养,而规范化地写注释则是里面非常重要的一环。 正文 function phoneNumber(numbers) { return"("+numbers[0]+numbers[1]+numb...
继续阅读 »

前言


想要成为一名优秀的程序员,首先应该具有的是良好的编程素养,而规范化地写注释则是里面非常重要的一环。


正文


 function phoneNumber(numbers) {
return"("+numbers[0]+numbers[1]+numbers[2]+")"+' '+numbers[3]+numbers[4]+numbers[5]+'-'+numbers[6]+numbers[7]+numbers[8]+numbers[9]
}

如果我直接丢出这一串代码,你第一眼看过来,心里肯定会想“什么玩意?这一坨代码是干什么用的!居然还需要我拖动横条?!”
但是我如果在它的上方加上这样一段注释,并中途给它换行两次,它就会变成这样


/**
* @func 返回固定格式的电话号码, (123) 456-7890
* @param array [1,2,3,4,5,6,7,8,9,0]
* @return (123) 456-7890
* @author xsir
*/

函数定义
function phoneNumber(numbers) {
return"("+numbers[0]+numbers[1]+numbers[2]+")"
+' '+numbers[3]+numbers[4]+numbers[5]
+'-'+numbers[6]+numbers[7]+numbers[8]+numbers[9]
}

你一眼看过去就能清楚的看到,这个函数的作用是返回固定格式的电话号码,
调用函数需要输入的形参的样式是一个数组,返回值为固定格式的电话号码,函数的作者是xsir。


在大公司做程序开发的时候,一整个大的项目需要多人协作一同完成,所以代码的可读性就显得尤为重要,甚至可以说,代码的可读性高于一切,因为在这个时候你的代码不仅仅是写给你自己看和用的,而是整个开发团队的同志们都需要能快速看懂并且调用的。


如果别人看你写的代码时,仅仅只有代码而没有任何其他注释,那么他就需要整体的阅读你写的所有代码,才能知道你写的函数是干什么用的,这就会浪费很多时间。“Time is money, efficiency is life!”


顺带一提,如果你使用的是'${}'的格式


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]}) ${numbers[3]}${numbers[4]}${numbers[5]}-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`

运行上述代码,输出的结果是这样的:


image.png


为了提高代码可读性,你对它进行了换行


phoneNumber = (numbers) => `(${numbers[0]}${numbers[1]}${numbers[2]})
${numbers[3]}${numbers[4]}${numbers[5]}
-${numbers[6]}${numbers[7]}${numbers[8]}${numbers[9]}`


那么输出结果就会变成这样


image.png


并没有达到预期的效果。所以在实战过程中使用'${}'需谨慎。


总结


在我们开发学习的过程时就要养成良好的编程素养,每次写完一块代码就写好这块代码的注释,做到看“码”知意。同时也要避免单行代码写的过长,尽量使你的代码不需要拖动横条就能看完。


作者:阡陌206
来源:juejin.cn/post/7293789288725889078
收起阅读 »

如何写出让同事崩溃的代码

web
    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。 一、方法或变了名字随便取     首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或...
继续阅读 »

    废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。


一、方法或变了名字随便取


    首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或者方法到底是干嘛用的。哎!对,就是让他去猜,哈哈哈。
来来来,空说没意思,举个栗子图片


    假设现在想要点击某个input框时,显示一个自定义的组件用于选择选择时间。


    正常的写法如下:定义一个 toggleDatePicker 方法
image.png


    这个一看就知道是时间选择器的显示切换方法。


    但是呢,我偏不,定义成下面这样:让同事摸不着头脑,哈哈哈


image.png
当看到很多这样的方法名或变量名时,同事的表情估计时这样的图片图片图片


接下来,第二招图片图片图片


二、方法体尽可能的长,长到不能在长


    这一步至关重要,将所有逻辑全部放在一个方法中写完,坚决不分步骤,不按逻辑一步步拆分方法。让同事为我的超长方法体感到叹为观止,默默流泪。


    老规矩,上栗子


    假设现在有个方法需要处理比较复杂(需要递归,而且每层数据有不同的类型)的json格式的数据回显到页面上(这是用于拼一些条件)。数据格式大概是这样的


[
{
type: "group",
relation: "or",
conditions: [
{
type: "condition",
field: {
name: "员工状态",
id: 12345678
},
logic: "=",
val: 1,
relation: "and"
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2011-07-09",
relation: "and"
}
]
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2001-07-09",
relation: "and"
}
]

    由于上面的 json 数组格式中是一个对象,对象都有 type 属性,一种是 group 类型(表示是一组条件),一种是 condition 类型(表示是真正的条件内容)。


    因此,正常情况下,我们应该是遍历数组,不同的类型分别用不同的方法去处理,并且在处理条件中各个部分的时候分别处理。如下所示:


image.png


image.png


    但是呢?咱主题是啥,让同时崩溃啊,怎么能把方法拆成这样清晰的逻辑呢。图片图片图片


    来给你看看怎么让同事对你的代码叹为观止


image.png


image.png


image.png


image.png


image.png


image.png


    怎么样,牛不牛,一个方法写完所以逻辑,从1825行一口气写到2103行,足足... 2103 - 1825 是多少来着,3减5 不够,向前借位 ,嗯。。。278 行。
****怎么样,有没有被哥的机智震惊到,如此代码。同事看到肯定心生敬佩,连连称绝。此时想到得到同事应该是这样的表情 图片图片图片


    同事还没进到方法体里面,就已经被我的 迷之方法名 和 超长方法体 所折服,接下来就让他在方法体里面快乐的遨游吧
    


    接下来,继续让同时崩溃。


三、坚决不定义统一的变量


    这个怎么说呢,就是因为有点懒,所有很多代码直接复制粘贴,多么的方便快捷。


    正常情况下,如果某个我们需要的对象在是其他对象的属性,并且层次很深,我们先定义一个变量来接收这个对象,再对这个对象操作。


例如:


let a = {
b: {
c: {
d: {
name: "我是最里面的对象"
}
}
}
}

    我们要对d对象进行很多次的操作时,一般先将d赋值给一个变量,然后对变量操作。如下:


var dOfA = a.b.c.d;
dOfA.name = "我现在被赋值给dOfA ";
dOfA.data = 1;
dOfA.other = false;

    但是呢,我就不这么干,就是要写得整整齐齐


a.b.c.d.name = "就要这么干,你打我呀";
a.b.c.d.data = 1;
a.b.c.d.other = false;

    老规矩,没有 实际的 栗子 怎么能说的形象呢,上图


    正常写法:


image.png


    我偏要这么写


image.png


    多么的整齐划一,


    全场动作必须跟我整齐划一


    来左边儿 跟我一起画个龙


    在你右边儿 画一道彩虹


    来左边儿 跟我一起画彩虹...


    咋突然哼起歌来了,不对,咱是要整同事的,怎么能偏题。


    继续,此时同事应该是这个表情图片图片图片


    然后,方法体里面只有这么点东西怎么够玩,继续 come on


四、代码能复制就复制,坚决不提成公用的方法


    代码能 CV ,干嘛费劲封装成方法,而且这样不是显得我代码行数多吗?图片图片图片


    就是玩儿,就是不封装


    来,上栗子


image.png


image.png


image.png


    看到没有,相同的代码。我在 1411行 - 1428行 写了一遍, 后面要用,在1459行-1476行复制一遍


    这还不够?那我在1504-1521行再复制一遍


    这下,爽了吧,哈哈哈


    就是不提方法,就是玩儿,哎! 有意思


    这个时候同事估计是这样的吧图片图片图片


    怎么样,是不是很绝?不不不,这算啥


    虽然以上这些会让看我代码的同事头疼,但是,只要我在公司,他们还会让我改啊。怎么能搞自己呢。


    最后一步


五、离职


    洋洋洒洒的写完代码,尽早离开。够不够绝,哈哈哈


六、申明:


    以上场景纯属个人虚构的,单纯为了给文章增加点乐趣。写这个文章的目的是让各位程序员兄弟尽量避免写这种难以维护的代码。真的太痛苦了!代码质量、代码重构真的是编程过程中很重要的一个步骤。不能抱着能用就行的心态。还是要对自己有一定的要求。只有你看得起自己的代码,别人才有可能看得起你的代码。加油吧!各位


作者:猩兵哥哥
来源:juejin.cn/post/7293888785400856628
收起阅读 »

一文学会请求中断、请求重发、请求排队、请求并发

web
大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处! 以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。 阅读下文需要了解前置知识:promise、class、axios...
继续阅读 »

大家好,今天我们来聊聊前端开发中的网络请求,顺便也来体验一下promise的神奇之处!

以下示例是基于axios@1.5.1进行开发,在一些低版本中的一些用法可能不太一样,建议安装新版进行测试。

阅读下文需要了解前置知识:promise、class、axios


请求中断


1.判定相同请求:请求url、请求方法、请求params参数、请求 body参数,四个值都相同,则认为是一个相同的请求。

2.判断请求中断:在上一个相同请求还没有得到响应前,再次请求,则会自动中断。


image.png


请求重发(无感刷新token)


1.当前请求返回401时,执行刷新token。

2.当同时存在多个请求返回401时,可在类中维护一个静态变量保存请求刷新接口的promise,防止多次调用刷新token。

3.RetryRequest类实例化参数:

   instance:请求实例对象

   success:刷新成功回调函数

   error:刷新失败回调函数

image.png


请求排队



  1. queue:请求等待队列。

  2. isWating:是否正在等待上个请求响应。

  3. add:向队列里加入一个等待请求的promise的resolve方法,执行该方法可立刻发送下一个请求 。

  4. next:执行下一个请求方法,在上一个请求响应后调用。

    image.png


响应处理


image.png


axios请求实例


image.png


测试


1.请求中断测试


快速点击test请求按钮多次


image.png


2.刷新token请求重发测试


(1)当用户没有登录请求接口时


image.png


(2)当用户登录后,accessToken过期,但refreshToken还没过期调用接口时


image.png
在调用刷新token接口成功后,将重发失败的test接口


(3)当refreshToken过期后调用接口时


image.png
这时已经无法刷新token了,只能乖乖跳转到登录页面了。


3.请求排队测试


(1)没有使用请求排队时


场景:当输入框输入关键字实时查找内容时,由于网络原因,可能会出现先请求的后响应的请求,导致请求错乱。

如下,模拟网络请求延迟:
image.png


当输入框依次输入1、2、3、4、5时,期望的返回结果应该是1,12,123,1234,12345。
但确得到了以下的结果:
image.png


(2)使用请求队列时


在网络请求的waterfall列可以清晰看到,当上一请求完成才会执行下一请求,直到等待队列执行完成。
image.png


源码


后端接口


var express = require('express');
var router = express.Router();

const access_token = 'access_token'
const refresh_token = 'refresh_token'
// token有效期(单位毫秒)
const tokenValidTime = 1000*2
// 刷新token有效期
const refreshTokenValidTime = 1000*5
// 登录时间,模拟token过期
let loginTime;
// 模拟判断token是否过期
const IsTokenExpired = () => {
if(new Date().getTime() > loginTime + tokenValidTime) {
return true
}
return false
}
router.post('/login', (req, res) => {
loginTime = new Date().getTime()
res.json({
access_token,
refresh_token
})
})

router.post('/refresh-token', (req, res, next) => {
const refreshToken = req.headers.authorization
console.log('refresh-token', refreshToken)
if(refreshToken !== refresh_token) {
return res.status(401).json({
msg: ' refreshToken不正确!'
})
}
if(new Date().getTime() > loginTime + refreshTokenValidTime) {
return res.status(401).json({
msg: ' refreshToken已过期,请重新登录!'
})
}
loginTime = new Date().getTime()
res.json({
access_token: 'access_token',
refresh_token: 'refresh_token'
})
})

router.get('/test', (req, res, next) => {
const token = req.headers.authorization
if(token !== access_token) {
return res.status(401).json({
msg: '没有访问权限'
})
}
if(IsTokenExpired()) {
return res.status(401).json({
msg: 'token已过期'
})
}
res.json({
name: '哈哈'
})
})

router.get('/random', (req, res) => {
const keyword = req.query.keyword
setTimeout(() => {
res.json({
value: keyword
})
// 5秒内随机返回,测试网络请求延迟效果
}, Math.random()*5000);
})

module.exports = router;

前端


Index.js


import axios from "axios"
import AbortRequest from './hooks/AbortRequest'
import ResponseHanlder from "./hooks/ResponseHanlder"
import { getAccessToken, getRefreshToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'
import { useRequestKey } from "./hooks/useRequestKey"
import RequestQueue from "./hooks/RequestQueue"
import { isAddQueue } from '@/api'

export const baseURL = '/api'
const timeout = 6000

// 创建axios实例
const instance = axios.create({
baseURL,
timeout
});

// 创建中断请求控制器
const abortRequest = new AbortRequest()
// 创建响应处理器
const responseHandler = new ResponseHanlder(instance)
// 创建请求队列排队实例
const requestQueue = new RequestQueue()

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
console.log('在发送请求之前做些什么', config)
// 在发送请求之前做些什么
if(config.url !== '/login') {
const token = config.url === refreshTokenUrl ? getRefreshToken() : getAccessToken()
config.headers.Authorization = token
}
// 刷新token接口不用创建取消请求,已经再RetryRequest类维护静态属性
if(config.url !== refreshTokenUrl) {
abortRequest.create(useRequestKey(config), config)
}
// 加入请求等待队列
if(isAddQueue(config)) {
return requestQueue.add(config.url, config)
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
const config = response.config
console.log('响应成功', response)
abortRequest.remove(useRequestKey(config))
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return responseHandler.success(response)
}, function (error) {
const config = error.config
console.log('响应错误', config)
if(config) {
abortRequest.remove(useRequestKey(config))
}
if(isAddQueue(config)) {
requestQueue.next(config.url, config)
}
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
if(axios.isCancel(error)) {
return Promise.reject('已取消重复请求!')
}
return responseHandler.error(error)
});

export default instance;

AbortRequest.js


// 重复请求中断类
class AbortRequest {
constructor() {
// 请求中断控制器集合
this.list = new Map()
}
// 创建中断请求控制器
create(key, config) {
const controller = new AbortController();
config.signal = controller.signal
// 集合中存在当前一样的请求,直接中断
if(this.list.has(key)) {
controller.abort()
} else {
this.list.set(key, controller)
}
}
// 请求完成后移除集合中的请求
remove(key) {
this.list.delete(key)
}
}
export default AbortRequest

RequestQueue.js


/**
* 相同url请求队列,排队执行维护类
*/

class RequestQueue {
constructor() {
// 请求等待队列
this.queue = {}
// 正在等待上一请求执行中
this.isWating = false
}
add(url, config) {
return new Promise((resolve) => {
const list = this.queue[url] || []
if(this.isWating) {
// 当前请求url存在等待发送的请求,则放入请求队列
list.push({ resolve: () => resolve(config) })
} else {
// 没有等待请求,直接发送
resolve(config)
this.isWating = true
}
this.queue[url] = list
console.log('list', list)
})
}
// 响应处理
next(url) {
this.isWating = false
// 拿出当前请求url的下一个请求对象
if(this.queue[url]?.length > 0) {
const nextRequest = this.queue[url].shift()
// 执行请求
nextRequest.resolve()
}
}
}

export default RequestQueue

RetryRequest.js


/**
* 无感刷新token类
*/

class RetryRequest {
// 解决存在多个并发请求时,重复调用刷新token接口问题
static refreshTokenPromise = null
constructor({
instance, // axios实例
success, // 刷新token成功执行的回调函数
error // 刷新token失败执行的回调函数
}
) {
this.instance = instance
this.success = success
this.error = error
}
/**
* @param config 当前请求对象,等待token刷新完成再重复执行
* @param refreshTokenApi 刷新token方法
*/

useRefreshToken(config, refreshTokenApi) {
if(!config.headers.Authorization) {
this.error()
return Promise.reject('token不存在!')
}
return new Promise((resolve, reject) => {
if(!RetryRequest.refreshTokenPromise) {
// refreshTokenPromise不为null,则当前正在执行刷新token方法,不再重复调用
RetryRequest.refreshTokenPromise = refreshTokenApi()
}
RetryRequest.refreshTokenPromise.then(res => {
// 刷新token成功
this.success(res)
// 重新发送请求
this.instance(config).then(data => {
resolve(data)
}).catch(err => {
// 重发失败
reject(err)
})
}).catch(err => {
// refreshToken失效或刷新token失败
this.error()
reject(err)
}).finally(() => {
// 刷新token调用完成,重置
RetryRequest.refreshTokenPromise = null
})
})
}
}

export default RetryRequest

ResponseHanlder.js


import RetryRequest from './RetryRequest'
import { refreshToken as refreshTokenApi } from '@/api/index'
import { getRefreshToken, setRefreshToken, setAccessToken } from '@/utils'
import { refreshTokenUrl } from '@/api/urls'

/**
* 响应处理类
*/

class ResponseHanlder {
constructor(instance) {
this.retryRequest = new RetryRequest({
instance,
success: (res) => {
const { access_token, refresh_token } = res
setAccessToken(access_token)
setRefreshToken(refresh_token)
},
error: () => {
console.log('刷新token失败!')
// 执行失败逻辑...
}
})
}
// 请求正常响应方法
success(response) {
// 对响应数据做处理
return response.data
}
// 请求错误响应方法
error(error) {
const status = error.response?.status
// 当前返回401,且不是调用刷新token接口响应的(避免后端刷新token失败返回401导致死循环的情况)
if(status === 401 && error.config.url !== refreshTokenUrl) {
return this.retryRequest.useRefreshToken(
error.config,
() => refreshTokenApi(getRefreshToken())
)
} else {
return Promise.reject(error.response)
}
}
}

export default ResponseHanlder

request.js


import instance from './index'

class Request {
constructor() {

}
get(url, params, args) {
return instance.get(url, {
params,
...args
})
}
delete(url, params) {
return instance.get(url, {
params
})
}
post(url, data) {
return instance.post(url, data)
}
put(url, data) {
return instance.put(url, data)
}
}

export default new Request();

结语


还有一个控制请求并发数量还没进行扩展,相信大家了解了请求排队的思路后,实现请求并发控制也不是什么难事了。



无感刷新token参考文章:juejin.cn/post/728974…



作者:云上客人
来源:juejin.cn/post/7293806405650464808
收起阅读 »

判断鼠标从哪个方向进入元素

web
我们需要实现的效果图 理清需求 拿到效果图的第一步,理清下需求~ 首先,元素有左右上下四个方向。这边的问题在我如何在一个元素上划分上下左右四个区域? 然后,鼠标进入元素和离开元素会有触发一个事件,这个简单js就自带了监听事件。 最后,最难的是如何判断鼠标进...
继续阅读 »

我们需要实现的效果图


image.png


理清需求


拿到效果图的第一步,理清下需求~



  • 首先,元素有左右上下四个方向。这边的问题在我如何在一个元素上划分上下左右四个区域?

  • 然后,鼠标进入元素和离开元素会有触发一个事件,这个简单js就自带了监听事件。

  • 最后,最难的是如何判断鼠标进入的时候会落在我们划分好的上下左右四个区域?


思路



  • 首先我们先来划分下四个区域,一般划分的都如下图


image.png




  • 图里面有四个三角形,每个三角形代表的是一个方向,所以问题简化为如何在一个矩形里,根据对角线划分区域。由于元素存在坐标系,也就是X、Y轴,所以问题再次抽象成,如何得到两条对角线的线性函数。(初高中数学问题。)




  • 最后的问题我们就要来搞定判断鼠标落点的问题,首先我们知道我们可以在元素的鼠标事件中通过event得到鼠标的pageX和pageY,再配合元素的offsetLeft和offsetTop就可以得到鼠标在元素中的坐标。综合一下就变成了,我有一个坐标,且我知道对角线的函数表达式,请问我如何知道我这个坐标是在函数的下面还是上面?




  • 当然也许描述的比较抽象,我们可以类比一个例子,我现在有一个坐标(2,1),有一个函数y=x,值域大于0(既y>0),定义域大于0(既x>0),求该坐标在y=x的函数下面还是上面?(是不是感觉到了线性规划得到最优解的味道,对,少年,没有错,就是这样。)这里我们只要把坐标中的x值代入函数,然后判断代入的结果是否大于坐标的y值,如果大于则在函数下面,小于则在函数上面,什么?你问等于怎么办?当然是在函数上面,该坐标即在上面又在下面,所谓薛定谔的坐标是也(当然是在函数上了)。




  • 然后我们是不是可以扩展下,如果存在多个函数,再加上逻辑判断经常用的交集,并集是不是又有新的思维出现了呢?好了,这边就不再扩展了,下面直接上实现代码吧。




实现代码


注意:该demo只是简单的demo,其中有很多可以优化的地方,比如组件化,变量优化,利用发布订阅模式,实现事件联动


<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css" >
.ct{
height: 100px;
width: 100px;
border:1px red solid;
}
</style>
</head>
<body>
<div class="ct" onmouseover="fun1(event);" onmouseout="fun2(event);">

</div>
<script type="text/javascript">
//当然这样绑定事件函数是不对的
var div=document.getElementsByTagName("div")[0];
function fun1(event){
var x=event.pageX-div.offsetLeft;//(得到鼠标在框中的坐标)
var y=event.pageY-div.offsetTop;//(得到鼠标在框中的坐标)
var H=div.clientHeight;
var W=div.clientWidth;
var k=Math.floor(H/W);//为了防止不能整除
//得到2个斜边函数
//设y=ax+c
//(0,0) (width,height)其中一个斜边过这两点
//a=height/width,c=0
//y=(height/width)*x;
//(0,height) (width,0)另外一个斜边过这两点
//a=-height/width,c=height
//y=-(height/width)*x+height
if((k*x)>=y && (H-k*x)>=y){//这是判断从上方进入,这边简化处理不对等于情况做特别处理
console.log("从上方进入");
//todo
}

if((k*x)<y && (H-k*x)<y){
console.log("从下方进入");
//todo
}

if((k*x)<y && (H-k*x)>y){
console.log("从左边进入");
//todo
}

if((k*x)>y && (H-k*x)<y){
console.log("从右边进入");
//todo
}

}
function fun2(event){
var x=event.pageX-div.offsetLeft;//(得到鼠标在框中的坐标)
var y=event.pageY-div.offsetTop;//(得到鼠标在框中的坐标)
var H=div.clientHeight;
var W=div.clientWidth;
var k=Math.floor(H/W);//为了防止不能整除
//得到2个斜边函数
//设y=ax+c
//(0,0) (width,height)其中一个斜边过这两点
//a=height/width,c=0
//y=(height/width)*x;
//(0,height) (width,0)另外一个斜边过这两点
//a=-height/width,c=height
//y=-(height/width)*x+height
if((k*x)>=y && (H-k*x)>=y){//这是判断从上方进入,这边简化处理不对等于情况做特别处理
console.log("从上方离开");
//todo
}

if((k*x)<y && (H-k*x)<y){
console.log("从下方离开");
//todo
}

if((k*x)<y && (H-k*x)>y){
console.log("从左边离开");
//todo
}

if((k*x)>y && (H-k*x)<y){
console.log("从右边离开");
//todo
}

}
</script>
</body>
</html>

作者:洛漓
来源:juejin.cn/post/7293820517374820352
收起阅读 »

彻底搞懂闭包

web
每次面试都问,每次都背;每次都背的不错,每次都不太理解。 定义 闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。 一个简单的例子认识闭包: function init() { var...
继续阅读 »

每次面试都问,每次都背;每次都背的不错,每次都不太理解。


定义


闭包是一个函数和对其周围状态的引用捆绑在一起,这样的组合就是闭包。闭包让我们可以在一个内层函数中访问到其外层函数的作用域。


一个简单的例子认识闭包:


function init() {
var name = 'wendZzzoo';
function getName() {
console.log(name)
}
getName()
}
init()


使用场景


那闭包有什么作用呢?但从上面这个简单的例子中,似乎很难发现这样写,也就是闭包这样的写法的用途。


数据封装和隐藏


通过使用闭包,可以创建一个作用域限定的环境,以保护变量不受外部的访问和修改。这样可以防止变量被意外修改,避免命名冲突和全局污染,提高代码的可维护性和可读性。


举个例子,定义一个计数器函数,用来某些场景下计算次数。


没有使用闭包的示例


let count = 0
function increment() {
count++
console.log(count)
}
increment()
increment()


上述代码是实现了计数器的需求,但是代码存在风险,count变量是全局定义的,在后续开发中或者是其他人维护时可以轻易修改这个变量,导致bug出现。


使用闭包的示例


function createCounter() {
let count = 0;
function increment() {
count++;
console.log(count);
}
return increment;
}

const counter = createCounter();
counter();
counter();


在上述示例中,createCounter函数返回了一个内层函数increment,该函数可以访问并递增count变量。外层函数的作用域被封装在闭包中,外部无法直接访问和修改count变量。


这里可以衍生思考一下,count变量封装在闭包中只能递增,外部无法修改,那该如何重置或者递减count呢?


其实需要新增的逻辑也可以封装到闭包里,以重置count为例:


const counterModule = (function() {
let count = 0;
function increment() {
return ++count;
}
function reset() {
count = 0;
}
return {
increment,
reset
};
})();

console.log(counterModule.increment());
console.log(counterModule.increment());
counterModule.reset();
console.log(counterModule.increment());


在上面的例子中,使用立即调用函数表达式(IIFE)创建了一个闭包,内部定义了count变量和两个操作它的函数incrementreset。通过返回一个包含这些函数的对象,实现了对count变量的封装和控制。


保持数据状态


通过闭包,内层函数可以访问和持有外层函数的变量,即使外层函数执行完毕,这些变量依然存在于内层函数的词法环境中,从而实现了数据状态的保持。


这个使用场景可以算是上一个的延申,在上述示例代码中添加传参,就可以起到了数据状态保持的目的。


函数柯里化


闭包使得函数可以返回另一个函数作为结果,从而形成函数工厂的模式。通过在内层函数中访问外层函数的参数或变量,可以创建具有不同参数或上下文的函数。这种技术称为柯里化,其目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。


举个例子,定义一个求矩形面积的函数。


不使用柯里化的示例


function getArea(width, height) {
return width * height
}
const area1 = getArea(10, 20)
console.log(area1)
const area2 = getArea(10, 30)
console.log(area2)
const area3 = getArea(10, 40)
console.log(area3)


上面代码里,假设我们需要这个计算矩形面积的函数,来计算宽度总是10的多种情况,那就需要多次调用getArea函数传入相同的宽度参数,且在维护的时候,假设需要统一修改宽度为20,就需要重复修改每一次调用时宽度的传参,这样重复的工作在力求优雅的情况下看来是不合适的。


使用闭包柯里化的示例


function getArea(width) {
return height => {
return width * height
}
}

const getTenWidthArea = getArea(10)
const area1 = getTenWidthArea(20)
console.log(area1)
const area2 = getTenWidthArea(30)
console.log(area2)
const area3 = getTenWidthArea(40)
console.log(area3)


如果有需要宽度改变的情况,也可以轻松复用


const getTwentyWidthArea = getArea(20)


再举个例子,定义一个打印日志的函数。


function createLogger(prefix) {
function logger(log) {
console.log(`${prefix}: ${log}`);
}
return logger;
}

const exportWarnning = createLogger('warnning');
exportWarnning('这是一个警告日志');

const exportError = createLogger('error');
exportError('这是一个错误日志');


通过调用createLogger函数并传递不同的参数,可以创建具有不同日志前缀的logger函数。


通过上述两个柯里化的例子,可以发现柯里化是一种技术更多是一种提倡,使用这样的技术可以让你的代码更有维护性。


模拟私有化方法


私有方法是将某些函数或变量限定在一个作用域内,外部无法直接访问。


function makeCounter() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
}

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value());
Counter1.increment();
Counter1.increment();
console.log(Counter1.value());
Counter1.decrement();
console.log(Counter1.value());
console.log(Counter2.value());


上述代码通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式


两个计数器 Counter1 和 Counter2 是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量


注意事项


闭包是一种强大的特性,但滥用闭包可能导致代码可读性和性能方面的问题,因此需要注意的是:



  1. 避免不必要的闭包,只有在确实需要保留状态或隐藏数据时才使用闭包。不要为了使用闭包而创建不必要的函数嵌套,盲目使用闭包并不会让你的代码看起来更高级。

  2. 要注意内存管理,闭包会持有对外部作用域的引用,可能导致内存泄漏。确保在不再需要闭包时,手动解除对外部作用域的引用,以便垃圾回收器能够正确处理。

  3. 特别要小心循环中的闭包,闭包会捕获循环变量的引用,可能导致意外结果。可以使用立即调用函数表达式(IIFE)或函数绑定来解决。

  4. 换一种解决方案,可以使用模块模式,如果需要封装私有方法和变量,考虑使用模块模式或其他模块化工具,如ES6模块。这样可以更清晰地定义私有和公共部分,并提供更好的可维护性和可测试性。


内存泄漏


闭包可以引起内存泄漏的情况,通常是涉及对外部作用域的引用。当函数形成闭包时,它会持有对其包含作用域的引用,这可能导致无法释放被闭包引用的内存。


可能导致内存泄漏的情况:



  1. 未及时释放闭包,如果闭包持有对外部作用域的引用,但不再需要使用闭包时,如果没有显式地解除对外部作用域的引用,闭包将继续存在并持有外部作用域中的变量。

  2. 当闭包和其包含作用域之间存在循环引用时,可能导致内存泄漏。例如,如果闭包中引用了一个对象,而该对象又持有对闭包的引用,这将导致它们互相引用,无法被垃圾回收。

  3. 闭包中引用了全局变量,闭包将一直存在,即使在不再需要闭包时也无法释放。这种情况下,全局变量将一直保持活动状态,无法被垃圾回收。


为避免闭包引起的内存泄漏,建议:



  1. 及时解除引用,当不再需要使用闭包时,确保手动解除对外部作用域的引用。只需要将闭包中引用的变量设置为 null 或重新分配其他值,以便垃圾回收器能够正确处理。

  2. 尽量避免闭包和其包含作用域之间的循环引用。确保在闭包中不引用外部对象,或者在外部对象中不引用闭包,以避免循环引用导致的内存泄漏。

  3. 只在确实需要保留状态或隐藏数据时使用闭包,在不需要闭包的情况下,使用适当的作用域(例如局部变量或模块作用域)来防止不必要的内存占用。


内存泄漏的发生并不一定是由闭包引起的,还可能涉及其他因素,但是,闭包在不正确使用的情况下容易导致内存泄漏问题。


作者:wendZzoo
来源:juejin.cn/post/7293805895918207026
收起阅读 »

📷纯前端也可以实现「用户无感知录屏」?

web
前言 要在 JavaScript 中实现屏幕录制,可以使用 navigator.mediaDevices.getDisplayMedia() 方法来获取屏幕的媒体流。然后,可使用 MediaRecorder 对象将媒体流录制为视频文件。 但该方法会在浏览器弹出...
继续阅读 »

前言


要在 JavaScript 中实现屏幕录制,可以使用 navigator.mediaDevices.getDisplayMedia() 方法来获取屏幕的媒体流。然后,可使用 MediaRecorder 对象将媒体流录制为视频文件。


但该方法会在浏览器弹出一个授权窗口,让用户选择要分享的内容,这不可实现“无感知”。


image.png


如果真正做到无感知,那我们就不能借助浏览器或者系统系统的能力了。我们能做的就只能是通过js去操作了。


要在页面内直接录制视频似乎并不容易,没有现成的开源库可以使用,也没有很好的想法。


那我们换一个思路,视频是由帧组成的,我们是否可以不断的截图,然后组合成一段视频?好像是可以的。


下载.jpeg


效果


mp4.gif


页面


先写一个简单的页面:


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

<head>
<meta charset="UTF-8">
<title>Canvas视频录制</title>
<link rel="stylesheet" href="styles.css">
</head>

<body>
<main>
<div class="buttons">
<button class="start-btn">开始录制</button>
<button class="pause-btn">暂停录制</button>
<button class="resume-btn">继续录制</button>
<button class="stop-btn">结束录制</button>
</div>
<div id="box">
<section class="content">
<h2>TODO LIST</h2>
<div class="background-div">
<button class="background-btn">切换背景颜色</button>
</div>
<div id="todo-form">
<input type="text" class="input-field" placeholder="输入待办事项">
<button type="submit" class="submit-btn">提交</button>
</div>
<div class="list"></div>
</section>
</div>
<img src="" alt="" class="hidden">
</main>

<script src="<https://cdn.bootcss.com/html2canvas/0.5.0-beta4/html2canvas.min.js>" defer></script>
<script src="canvas.js" defer></script>
</body>

</html>

截图


实现网页的截图操作,最常用的库是 html2canvas用,它可以将网页中的 HTML 元素转换为 Canvas 元素,并将其导出为图像文件。在浏览器中捕获整个页面或特定区域的截图,包括 CSS 样式和渲染效果。


const canvasFunction = () => {
html2canvas(box).then(canvas => {
const imgStr = canvas.toDataURL("image/png");
img.src = imgStr;
img.onload = function () {
ctx.drawImage(img, 0, 0, w, h);
}
});
}

合成视频


这里我们要使用到一个 API MediaRecorder ,用于在浏览器中进行音频和视频的录制。它提供了一种简单的方式来捕获来自麦克风、摄像头或屏幕的媒体数据,并将其保存为文件或进行实时流传输。


它有以下几个常用的方法:



  • isTypeSupported() 返回一个 Boolean 值,来表示设置的 MIME type 是否被当前用户的设备支持。

  • start() 开始录制媒体,这个方法调用时可以通过给 timeslice 参数设置一个毫秒值,如果设置这个毫秒值,那么录制的媒体会按照你设置的值进行分割成一个个单独的区块,而不是以默认的方式录制一个非常大的整块内容。

  • pause() 暂停媒体录制。

  • resume() 继续录制之前被暂停的录制动作。

  • stop() 停止录制。同时触发 dataavailable 事件,返回一个存储 Blob 内容的录制数据。之后不再记录。


首先创建一个 canvas 元素,用来保存 html2canvas 的截图,然后通过 captureStream 方法实时截取媒体流。


const w = boxBoundingClientRect.width;
const h = boxBoundingClientRect.height;
const canvas = document.createElement('canvas');
canvas.setAttribute('id', 'canvas');
canvas.setAttribute('width', w);
canvas.setAttribute('height', h);
canvas.style.display = 'none';
box.appendChild(canvas);

const img = document.querySelector('img');
const ctx = canvas.getContext("2d");
const allChunks = [];
const stream = canvas.captureStream(60); // 60 FPS recording 1秒60帧

通过 canvas 的流来创建一个 MediaRecorder 实例,并在 ondataavailable 事件中保存视频信息:


const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});

recorder.ondataavailable = (e) => {
allChunks.push(e.data);
};

最后,在停止录制时将帧信息创建 blob 并插入到页面上:


recorder.stop();
const fullBlob = new Blob(allChunks);
const videoUrl = window.URL.createObjectURL(fullBlob);

const video = document.createElement('video');
video.controls = true;
video.src = videoUrl;
video.muted = true;
video.autoplay = true;
document.body.appendChild(video);

或者可以将视频下载


recorder.stop();
const fullBlob = new Blob(allChunks);
const videoUrl = window.URL.createObjectURL(fullBlob);

let link = document.createElement('a');
link.style.display = 'none';
let fullBlob = new Blob(allChunks);
let downloadUrl = window.URL.createObjectURL(fullBlob);
link.href = downloadUrl;
link.download = 'canvas-video.mp4';
document.body.appendChild(link);
link.click();
link.remove();

这里,为了节省资源,只在点击按钮、输入等事件发生时才调用 html2canvas 截图 DOM。


如果实时记录屏也可以使用 requestAnimationFrame


最后


虽然实现了无感知录制屏幕,但也仅限于网页内,没有办法录制网页以外的部分。


以上的 demo 中只实现了 DOM 的录制,如果想要录制鼠标轨迹,可以增加一个跟随鼠标的元素~


作者:Mengke
来源:juejin.cn/post/7293462197386592283
收起阅读 »

流金岁月

web
本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 小聚 “这里!这里!”我朝着声音望去,便看到小白兴奋的向我招着手,我小步快跑的走...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



小聚


“这里!这里!”我朝着声音望去,便看到小白兴奋的向我招着手,我小步快跑的走了过去,在小白对面落了座。“少爷阔气,今天怎么请我来这里吃饭?”我问出心中疑虑,璇玑地中海自助旋转餐厅,位于广州塔106层,从窗户放眼望去,晚霞与珠江美景浑天然一色,无数高楼一览无遗,万家灯火如星光皆纳入眼前,最要命的是,大众点评人均525/人,还好不是我掏钱。


小白不以为然的笑了笑,“等到核污水传遍全球,你想吃都不敢吃了,人生短短几十年,要懂得及时行乐。而且,咱两的关系也非同一般啊~”。打从有记忆起,我和小白就认识了,年龄跟我差不多,性格跟我差不多,爱好也跟我差不多,好巧不巧,如今他跟我一样也是在广州做IT,所以我们经常联系,关系特别好。“还是你会享受,走吧,拿吃的。”随即我和小白分头寻宝,不一会儿的功夫,桌子上便摆满了芝士波士顿、芝士生蚝、海螺、北极贝、不知名海虾各种海鲜。


我抓起一个芝士生蚝就往里炫,甜中带嫩,入口即化,香味从味蕾刺激我的脑海,当我准备再抓一个,小白轻飘飘的说了句:“我破产了。”我顿时一僵,尴尬地把手收了回去,突然想到了什么,小心翼翼地问,“你不会找我借钱吧?我可没钱哦,这顿AA也行”。


互诉


“你想的倒是挺多,只是这芝士生蚝我才拿了两个,你吃了一个还想再拿,我只能技术性打断你了”,我沉默良久,直至小白把另一只芝士生蚝炫完,露出了他满足的嘴脸


04.png


我才询问道“是你那家自助预约舞蹈室吗?”



破产



小白点了点头,“自从前年疫情过后,收入就一直不太乐观了,且竞争愈来愈激烈,到了暑假最旺期间扣完水电和物业租金,我竟还要倒贴两百元。这样亏本坚持了大概8个月后,合伙人L总她终于决定要解散了。之前成立公司和几位合伙人签订的股份分配书感觉自己犹如走上人生巅峰,随着几家分店加盟,畅想无限美好未来,好日子越来越有,越来越甜。未曾想两年不到,昨日便签了注销公司的文件,现在搞一份副业,付出了大量心血,最后也未必落得一个美好的结局。”


“唉~”我长叹了一声,刚刚吃完这个芝士生蚝,嘴巴有点渴,顺手拿起手边的茶杯抿了一口,这铁观音茶,清淡清香、浓郁扑鼻,喝了一口后自有淡淡回甘。“大局已定,失败乃成功之母,不过这次你收获也不少吧?”之前我也听过小白的舞蹈室副业,记得他刚成立之初前几个月每月都有几万流水,扣除杂七杂八到手还能有个两三千块钱,没想到还是经受不住岁月的考验。


“的确有所收获,L总把舞蹈室的哈曼卡顿音响送我了,这音响听着贼带劲~”。或许是我安慰,小白皱着的双眉似乎舒展几分,“那你呢,你最近在搞什么?”


“我上王者了。”我漫不经心的回了一句。


“王者是谁?”,我白了他一眼,他笑了笑,“没想到你还在玩啊?”。“嗯,现在机制60星就能王者了,有时间便利用下elo机制完成下十年前的梦想,最近练了一手刀妹,真的万军丛中过,片叶不沾身,得心应手。”说罢,我便给他看了看我上王者的截图。


06.jpg


“羡慕你,你还是一如既往的追寻你想要的东西。”小白一边说一边用筷子把半截波士顿龙虾连根拔起,张开血盆大口,吞入嘴中。


“切,就算舞蹈室倒闭也不影响你现在的生活吧,你看你现在一样过的挺滋润啊。”我不屑的说到。


“本来还挺滋润的,但最近压力有点大了,现在每个月发工资后都往家里打几千。”我不解,问道“怎么了,是家里出什么事了吗?”



暴雷



“我家资产暴雷了,你之前也应该知道我家里主要收入吧”。我点了点头,“现在存放到那里的资产因为公司负责人喝酒脑溢血离世了,剩下一大堆债务没法处理,本来每月提供的利息也没有了,本金还被冻结住,现在暂时拿不回来,这些都是我妈跟我说的。”我听完,大吃一惊,不过这种依靠大资金存放赚取高额利息的盈利,本来就是极高风险的,正所谓高风险高回报。但我看着小白失落的神情,也只能安慰说,“有什么要帮忙的跟我说,还有这顿哥请你”。


就等你这句话”。小白乐呵呵的看着我,看着他这副表情,我硬了,拳头硬了。


“现在主要靠负责人的弟弟处理家中事务,他也承诺两年后慢慢还回本金,但是家里主要的收入来源没有了,我也不想家里人看不开,毕竟健康最重要。之前本来就有向家里人打钱,只不过现在翻了一倍,自己花钱也不能像之前大手大脚了,不过请你这顿饭倒是不成问题”。小白见我长舒一口气,好奇问我,“怎么请我吃顿饭好像要你命似的,最近手头很紧吗?”


“你小子,我听你这件事也不是死局,才放心下来,如果承诺两年之内将本金还清并落实到位,也已经算是万幸了。不过我最近手头的确不充裕”。“怎么?之前大礼包十几万全买皮肤去了?”。我摇了摇头,给他打开了我的小鹏APP,“我买了小鹏G9”。


05.jpg

小白看了看我的订单,便看向我,满是不解,“咦,之前你的梦中情车不是宝马5系,连在掘金写小说用的头图都是盗的百度,怎么买了小鹏G9啊,你不是看不上这种牌子的车吗?”


我笑了笑,说“所以这篇小说用的头图是小鹏G9”。“啊?”


“以前我总天真的以为,只要自己不断的存钱,总有一天能够实现自己买下宝马5系的梦想,但梦想终归是梦想,现实毕竟是现实,钱真的很难赚,我不想贷款,不靠父母支持,要自己一边打工一边存钱,凑够这五十万谈何容易。当然,毕竟我也是能力不行,能力配不上自己的野心,转眼之间便差不多到了而立之年,往后的日子还要准备结婚,生娃等等世俗制定好的人生阶梯,之前我老是看懂车帝推文,什么5系,E级,是普通人的天花板,那时候觉得天花板离我触手可及,而现在,我每天睁开眼都觉得天花板离我越来越远。慢慢的我也认清了自己,知道自己是个什么水平,也学会放下,但是,梦想永远会存在我的心中,不会灭去”。


“你爸妈不反对你?”。


“嗐,他们吵上天了,什么电车不安全呀,电车只能买特斯拉啊,小鹏都要倒闭之类的。但是又如何呢,毕竟钱是我的,他们做不了主。我也有试驾过宝马iX3,只是觉得当下,这台车更适合我”。小白听完我的赘述,点了点头,“嗯,我了解你这个人,一旦认定了某些事,别人很难去改变你的想法。”我嘿嘿一笑,“你不也是吗?”


边聊边吃时间总是过得特别快,不一会儿我们两人的桌碟上放满了残骸,堆积如山,刚好服务员经过帮我们更换了新碟子,我们异口同声的说了声谢谢。


进入了短暂的沉默,我率先问小白,“最近工作情况如何?”。


小白听完,擦了擦嘴,表情也正经了起来,“其实今天我主要目的就是来跟你分享一下我最近的经历的,看看你有啥想法。”。


“哦?细说。”



曙光



“前两周我之前的leader让我去他公司面试。”


“你去了?”


“别打岔”。小白没好气的看我一眼,继续说道“一开始我是拒绝的,你也知道,我最近都比较躺平了,拿着一份在广州过的不差的薪资,浑浑噩噩的过着日子。谁知道,那位大佬开口便以35 * 14邀约,我本是性情中人,路见不平便拔刀相助,朋友有困难,我都会鼎力相助,何况这是贵人”。随即,他给我看了看聊天内容。


02.jpg


01.jpg


03.jpg

“听到这个数,我真的是垂死病中惊坐起,手上的switch瞬间丢到一边,立刻屁颠屁颠准备简历了。他说我是人才,其实我知道,贵人的实力,才称得上真正的人才”


“因为base要去深圳,如果薪资涨幅不是太高的话,其实我心里还是不太乐意的,因为现在在公司其实也得到leader青睐了,而且临近年终了,说实话,工作了6年多,之前跳来跳去,为了眼前那点利益,年终奖都没拿过几次,实属心累。但是这次如果薪资到胃,能给到这么高,虽然脉脉都劝别人不要去,但是我还是想尝试一下,毕竟这是更大的平台,为自己以后职业规划着想,现在累点真不算啥。”


我表示理解,“毕竟脉脉都是问就是别去,劝就是不走”。


小白笑着点了点头,“所以我去了,一面二面都过了。”


“牛逼啊,面了什么,分享一下啊~”


“牛逼个啥啊,问的都比较浅,一面印象比较深主要下面几点”。



  • typescript中 interface 和 type 的区别


1. type类型使用范围更广, 接口类型只能用来声明对象
2. 在声明对象时, interface可以多次声明
3. 区别三 interface支持继承的,type不支持
4. interface可以被类实现


  • typescript中 字面量类型


这个我真忘了,回头你自己整理一下


  • redis中如何处理大key


这个我不会,面试官也很nice的讲解了一波


  • 服务器容灾方案


这个我不会,面试官也很nice的讲解了一波


  • service worker作用


这个我不会,面试官也很nice的讲解了一波

“不是,你咋啥都不会啊?”。我超大声的问。


“那你会吗?”小白反问我。


“我也不会”。


“那我就放心了,然后就是问项目细节,我稀里糊涂的答,他稀里糊涂的听,二面的话就是更深入的项目细节了。”


“然后呢然后呢?”


“然后便是昨天的HR终面,HR让我从春秋时期讲起,阐述我的工作经历,我也是乖巧的从百草园讲到三味书屋,其中声情并茂,对细节说到尽情之处,心中也不免感慨之前的辉煌。不过,往事已成云烟。待烟消云散,HR便说重点,问我期望薪资。因为期间我也听HR说现在节奏很紧,加班已是常态,10-10-6稀松平常,我见HR诚心待我,我便以真心待她”。


“你说了多少?”


“98K”


“她同意了?”


“她说你现在的薪资过低,申请不到这个数,如果降低你是否会考虑?”


“那你怎么回?”


“我怎么回,难道你不知道吗?”


“我怎么会知道?”


past lives couldn't ever hold me down, lost love is sweeter when it's finally


"i've got the strangest feelin, this isn't out first time around"


“怎么我的闹钟响了?”突然,周遭的一切突然模糊了起来,旋即进入黑暗。


梦醒


我肌肉记忆般的按停了手机的闹铃,这首歌是我最喜欢的歌,但是自从做了起床闹钟铃声后,我便没有再听过。


我机械般地刷牙洗脸漱口,坐在有点老旧的餐桌椅上,打开昨天晚上下班临时买的方块原味面包,吃了两片,肚子好受一些,看来平日加班还是要按时吃晚饭。


我出门走去,阳光落在我的脸上,小白是我,我亦是我。


作者:很饿的男朋友
来源:juejin.cn/post/7293786784127090715
收起阅读 »

曹贼,莫要动‘我’网站 —— MutationObserver

web
前言 本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。 正文 话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子 这么好看的看的小乔,谁看谁不糊,更何...
继续阅读 »

前言


本篇主要讲述了如何禁止阻止控制台调试以及使用了MutationObserver构造方法禁止修改DOM样式。


正文


话说在三国时期,大都督周瑜为了向世人宣布自己盛世容颜的妻子小乔,并专门创建了一个网站来展示自己的妻子


image.png
这么好看的看的小乔,谁看谁不糊,更何况曹老板。这天,曹操在浏览网页的时候,无意间发现了周瑜的这个网站,看着美若天仙的小乔,曹操的眼泪止不住的从嘴角流了下来。赶紧将网站上的照片保存了下来。

这个消息最后传到了周瑜的耳朵里,他只是想展示小乔,可不是为了让别人下载的。于是在自己的网站上做了一些预防措施。

为了防止他人直接在网站上直接下载图片,周瑜将右键的默认事件给关闭了,并且为了防止有人打开控制台,并对图片保存,采取了以下方法:


禁用右键和F12键


//给整个document添加右击事件,并阻止默认行为
document.addEventListener("contextmenu", function (e) {
e.preventDefault();
return false;
});

//给整个页面禁用f12按键 keyCode即将被禁用 不再推荐使用 但仍可以使用
document.addEventListener("keydown", function (e) {
//当点了f3\f6\f10之后,即使禁用了f12键依旧可以打开控制台,所以一并禁用
if (
[115, 118, 121, 123].includes(e.keyCode) ||
["F3", "F6", "F10", "F12"].includes(e.key) ||
["F3", "F6", "F10", "F12"].includes(e.code) ||
//ctrl+f 效果和f3效果一样 点开搜索之后依旧可以点击f12 打开控制台 所以一并禁用
//缺点是此网站不再能够 **全局搜索**
(e.ctrlKey && (e.key == "f" || e.code == "KeyF" || e.keyCode == 70))||
//禁用专门用于打开控制台的组合键
(e.ctrlKey && e.shiftKey && (e.key == "i" || e.code == "KeyI" || e.keyCode == 73))
) {
e.preventDefault();
return false;
}
});

当曹操再次想保存小乔照片的时候,发现使用网页的另存了已经没用了。这能难倒曹老板吗,破解方法,在浏览器的右上角进行操作就可打开控制台,这个地方是浏览器自带的,没办法禁用


image.png
这番操作之后,曹操可以选择元素保存那个图片了。周瑜的得知了自己的禁用措施被破解后,赶忙连夜加班打补丁,于是又加了一些操作,禁止打开控制台后进行操作


禁用控制台


如何判定控制台被打开了,可以使用窗口大小来判定


function resize() {
var threshold = 100;
//窗口的外部减窗口内超过100就判定窗口被打开了
var widthThreshold = window.outerWidth - window.innerWidth > threshold;
var heightThreshold = window.outerHeight - window.innerHeight > threshold;
if (widthThreshold || heightThreshold) {
console.log("控制台打开了");
}
}
window.addEventListener("resize", resize);

但是也容易被破解,只要让控制台变成弹窗窗口就可以了


也可以使用定时器进行无限debugger,因为只有在控制台打开的时候debugger才会生效。关闭控制台的时候,并不会影响功能。当前网页内存占用比较大的时候,定时器的占用并不明显。在当前网页占用比较小的时候,一直开着定时器才会有较为明显的提升


  setInterval(() => {
(function () {})["constructor"]("debugger")();
}, 500);

破解方法一样有,在debugger的位置右键禁用调试就可以了。这样控制台就可以正常操作了


image.png
既然有方法破解,就还要做一层措施,既然是要保存图片,那就把img转成canvas,这样即使打开控制台也没办法进行对图片的保存


//获取dom
const img = document.querySelector(".img");
const canvas = document.querySelector("#canvas");
//img转成canvas
canvas.width = img.width;
canvas.height = img.height;
ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, img.width, img.height);
document.body.removeChild(img);

经过一夜的努力,该加的措施都加上了。周瑜心想这下就没办法保存我的小乔了吧。

来到曹操这边,再次打开周瑜的小破站,还想故技重施时,发现已经有了各种显示,最后也没难倒曹操,那些阻碍也都被破解了。但是到保存图片的时候傻眼了,竟然已经不是图片格式了,那就没办法下载了呀。但是小乔真的很养神,曹操心有不甘,于是使用了最后一招,既然没办法下载那就截图,虽然有损画质,但是依旧能看。


得知如此情况的大都督周瑜不淡定了,从未见过如此厚颜无耻之人,竟然使用截图。


006APoFYly1g2qcclw1frg308w06ox2t.gif
话说魔高一尺,道高一丈,周瑜再次熬夜加班进行对网站的优化。于是使用了全屏水印+MutationObserver监听水印dom的方法。即使截图也让他看着不舒服。


MutationObserver


MutationObserver是一个构造函数,接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

它接收一个回调函数,每当监听的dom发生改变时,就会调用这个函数,函数传入一个参数,数组包对象的格式,里面记录着dom的变化以及dom的信息。


image.png
返回的实例是一个新的、包含监听 DOM 变化回调函数的 MutationObserver 对象。有三个方法observedisconnecttakeRecords



  • observe接收两个参数,第一个为要监听的dom元素,第二个则是一些配置对象,当调用 observe() 时,childListattributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。配置对象如下:

    • subtree:当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target。默认值为 false

    • childList:当为 true 时,监听 target 节点中发生的节点的新增与删除(同时,如果 subtree 为 true,会针对整个子树生效)。默认值为 false

    • attributes:当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilter 或 attributeOldValue,默认值则为 false

    • attributeFilter:一个用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。

    • attributeOldValue:当为 true 时,记录上一次被监听的节点的属性变化;可查阅监听属性值了解关于观察属性变化和属性值记录的详情。默认值为 false

    • characterDate:当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false

    • characterDateOldValue:当为 true 时,记录前一个被监听的节点中发生的文本变化。默认值为 false



  • disconnect方法用来停止观察(当被观察dom节点被删除后,会自动停止对该dom的观察),不接受任何参数

  • takeRecords:方法返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。


该构造函数监听的dom即使在控制台中被更改属性或值,也会被监听到。


使用MutationObserver对水印dom进行监听,并限制更改。


<style>
//定义水印的样式
#watermark {
width: 100vw;
height: 100vh;
position: absolute;
left: 0;
top: 0;
font-size: 34px;
color: #32323238;
font-weight: 700;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-content: space-evenly;
z-index: 9999999;
}
#watermark span {
transform: rotate(45deg);
}
</style>

<script>
//获取水印dom
const watermark = document.querySelector("#watermark");
//克隆水印dom ,用作后备,永远不要改变
const _watermark = watermark.cloneNode(true);
//获取水印dom的父节点
const d = watermark.parentNode;
//获取水印dom的后一个节点
let referenceNode;
[...d.children].forEach((item, index) => {
if (item == watermark) referenceNode = d.children[index + 1];
});
//定义MutationObserver实例observe方法的配置对象
const prop = {
childList: true,//针对整个子树
attributes: true,//属性变化
characterData: true,//监听节点上字符变化
subtree: true,//监听以target为根节点的整个dom树
};
//定义MutationObserver
const observer = new MutationObserver(function (mutations) {
//在这里每次坚挺的dom发生改变时 都会运行,传入的参数为数组对象格式
mutations.forEach((item) => {
//这里可以只针对监听dom的样式来判断
if (item.attributeName === "style") {
//获取父节点的所有子节点,因为时伪数组,使用扩展运算符转以下
[...d.children].forEach((v) => {
//判断一下,是父节点里的那个节点被改变了,并且删除那个被改变的节点(也就是删除水印节点)
if (item.target.id && v == document.querySelector(`#${item.target.id}`)) {
v.remove();
}
});
//原水印节点被删除了,这里使用克隆的水印节点,再次克隆
const __watermark = _watermark.cloneNode(true);
//这里的this指向是MutationObserver的实例对象,所以同样可以使用observe监听dom
//监听第二次克隆的dom
this.observe(__watermark, prop);
//因为水印dom被删除了,再将克隆的水印dom添加到原来的位置 就是referenceNode节点的前面
d.insertBefore(__watermark, referenceNode);
}
});
});
在初始化的时候监听初始化的水印dom
observer.observe(watermark, prop);
</script>



这样,每当对水印dom进行更改样式的时候,就会删除该节点,并重新添加一个初始的水印dom,即使突破重重困难打开开控制台,用户也是无法对dom 进行操作。


视频转Gif_爱给网_aigei_com.gif


隔天曹操再次打开网页,发现网页上的水印,心里不足为惧,心想区区水印能难倒自己?操作到最后却发现,不论如何对水印dom进行操作,都无法改变样式。虽说只是为了保存图片,但是截图有着这样水印,任谁也不舒服呀。曹操大怒,刚吃了两口的饭啪的一下就盖在了桌子上......


20230508094549_33500.gif
然而曹操不知道的是,在控制台中,获取dom节点右键是可以只下载获取的那个节点的......


image.png


结尾


文章主要是以鬼畜恶搞的方式讲述了,如何禁止用户打开控制台(还有重写toSring,consloe.log等一些方法,但我并没有没有实现,所以这里并没有写上),并且如何使用MutationObserver构造函数来监听页面中的dom元素。其实大多情况下并没有这方面的项目需求,完全可以当扩展知识看了。


写的不好的地方可以提出意见,虚心请教!


作者:iceCode
来源:juejin.cn/post/7290862554657423396
收起阅读 »

在高德地图实现后期效果

web
介绍 最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图...
继续阅读 »

介绍


最近在做可视化图层开发的时候,发现我们自己开发的图层一些优秀的案例比起来,总是有一定的差距。差了后期效果合成环节,就比如个人晒图前忘了用美图秀秀修图。于是花了些时间研究了高德地图JSAPI2.0和GLCustomLayer,探索如何将后期特效接入到3D图层中。
Effect-PointsLayer2.gif


后期特效其实有点类似照片的后期滤镜处理,是对渲染结果的二次处理,可以实现发光、模糊、色调调整、镜头暗角、模拟环境光遮蔽等各种效果,为了方便理解,下面的讲解我将以辉光效果为例,学会了一种其他效果思路类似。


方案调研


Three官方提供了非常简单的方法实现后期特效,貌似仅需要完成以下两个步骤就可以完成我们想要的需求,代码也非常清晰简单:


import * as THREE from 'three'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing//UnrealBloomPass.js'
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'

...

// 1.在图层初始化完成后,创建效果合成器
onLayerInit(){
const { scene, camera, renderer } = this

const renderScene = new RenderPass(scene, camera)

// 后期泛光特效
bloomPass = new UnrealBloomPass(new THREE.Vector2(this.container.clientWidth, this.container.clientHeight), 1, 0, 0)
bloomPass.threshold = params.threshold
bloomPass.strength = params.strength
bloomPass.radius = params.radius

composer = new EffectComposer(renderer)
// 以下代码会遮盖地图
composer.addPass(renderScene)
composer.addPass(bloomPass)
}

// 2.更新合成器
onRender () {
if (composer) {
composer.render()
}
}

本以为这样做就可以开心收工了,燃鹅事情并没有那么简单,把这套方案移入高德的GLCustomLayer中,出现了这样的情况,后期效果直接把地图底图盖住了。
Untitled.png


出现这种情况的原因是实现辉光效果而编写的着色器,它会直接修改整个画面的alpha通道而导致透明效果丢失,因此需要单独修改UnrealBloomPass.js。


然而光是这样还不够,经过各种尝试,仍无法直接在GLCustomLayer上解决地图被遮盖的问题,后来咨询了高德地图开发团队的技术大佬,他给我的建议是后期效果层独立展示,于是就沿着这个思路进行了第二轮尝试。
Untitled 1.png


这里面有几个关键步骤是必须的:



  1. 修改UnrealBloomPass着色器代码

  2. 使用输出通道new OutputPass()置于特效通道的后面

  3. 在customLayer图层中,每次渲染就更新特效合成器EffectComposer


由于我这边是不希望之前开发的可视化图层做太多的修改去迁就这个后期效果的,也有对性能较差的终端机器优雅降级的考虑,索性把后期效果独立为EffectLayer层,以方便灵活地装载或剥离,最终实现了这个效果。


Effect-POI3dLayer1.gif


实现步骤




  1. 修改 UnrealBloomPass.js,由于这个文件在npm包中不能随意修改,我另外写了一个UnrealBloomPass1 继承并覆盖了UnrealBloomPass的方法


    import * as THREE from 'three'
    import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'

    class UnrealBloomPass1 extends UnrealBloomPass {
    constructor (resolution, strength, radius, threshold) {
    super(resolution, strength, radius, threshold)
    }

    getSeperableBlurMaterial (kernelRadius) {
    ...
    fragmentShader:
    `#include <common>
    varying vec2 vUv;
    uniform sampler2D colorTexture;
    uniform vec2 invSize;
    uniform vec2 direction;
    uniform float gaussianCoefficients[KERNEL_RADIUS];

    void main() {
    float weightSum = gaussianCoefficients[0];
    vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
    float alphaSum;
    for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
    float x = float(i);
    float w = gaussianCoefficients[i];
    vec2 uvOffset = direction * invSize * x;
    vec4 sample1 = texture2D( colorTexture, vUv + uvOffset );
    vec4 sample2 = texture2D( colorTexture, vUv - uvOffset );
    diffuseSum += (sample1.rgb + sample2.rgb) * w;
    alphaSum += (sample1.a + sample2.a) * w; //
    weightSum += 2.0 * w;
    }
    // gyrate: overwrite this line for alpha pass
    // gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
    gl_FragColor = vec4(diffuseSum/weightSum, alphaSum/weightSum);
    }`

    })
    }
    }

    export { UnrealBloomPass1 }



  2. 编写EffectLayer


    import * as THREE from 'three'
    import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
    import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
    import { UnrealBloomPass1 } from '../plugins/three/examples/jsm/postprocessing/UnrealBloomPass.js'
    import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js'
    import _ from 'lodash'

    class EffectLayer {

    // 此处省去一些内部变量

    _style = {
    // 光照强度阈值
    threshold: 0.0,
    // 泛光强度
    strength: 1.0,
    // 泛光半径
    radius: 1.5
    }

    /**
    * 创建一个实例
    * @param {Object} config
    * @param {Layer} config.layer 目标图层,要求是Layer的相关子类
    * @param {Number} [config.zIndex=120] 图层的层级
    * @param {EffectStyle} [config.style] 后期特效的配置项
    */

    constructor (config) {
    const conf = _.merge(this._conf, config)
    this._style = _.merge(this._style, conf.style)

    if (!conf.layer.scene || !conf.layer.camera) {
    console.error('缺少场景和相机')
    return
    }
    this.init()
    }

    init () {
    this.createLayer()
    this.addEffect()
    }
    }



  3. 创建自定义图层customLayer


    createLayer () {
    const canvas = document.createElement('canvas')
    this._customLayer = new AMap.CustomLayer(canvas, {
    zooms: [3, 22],
    zIndex: this._conf.zIndex,
    alwaysRender: true
    })

    this._canvas = canvas
    }



  4. 创建特效合成器


    addEffect () {
    const { scene, camera, container, renderer, map } = this._conf.layer
    const { clientWidth, clientHeight } = container

    // 创建渲染器
    const effectRender = new THREE.WebGLRenderer({
    canvas: this._canvas,
    alpha: true,
    antialias: false,
    stencil: false,
    depth: false
    })
    // renderer.setClearColor(0xff0000);
    effectRender.autoClear = false
    effectRender.setSize(clientWidth, clientHeight)

    // 后期效果
    const renderScene = new RenderPass(scene, camera)

    // 后期辉光特效
    const bloomPass = new UnrealBloomPass1(new THREE.Vector2(clientWidth, clientHeight), 1, 0, 0)
    bloomPass.clear = false

    // 输出通道
    const outputPass = new OutputPass()
    outputPass.clear = false

    this.updatePass()

    const composer = new EffectComposer(effectRender)
    composer.addPass(renderScene)
    composer.addPass(bloomPass)
    composer.addPass(outputPass)

    this._composer = composer
    this._bloomPass = bloomPass

    this._customLayer.render = function () {
    if (composer) {
    // 每次渲染就更新特效合成器
    composer.render()
    }
    }

    map.add(this._customLayer)
    }

    updatePass() {
    const {_bloomPass} = this
    if (_bloomPass) {
    _bloomPass.threshold = this._style.threshold
    _bloomPass.strength = this._style.strength
    _bloomPass.radius = this._style.radius
    }
    // 添加其他特效通道...
    }



  5. 使用EffectLayer


    //之前编写的可视化图层
    const layer = new GLlayers.POI3dLayer({
    map: getMap(),
    zooms: [10, 22]
    })

    layer.on('complete', (layer) => {
    let effectLayer = new GLlayers.EffectLayer({
    layer: layer, //把图层传入effectLayer
    style:{
    threshold: 0.0,
    strength: 1.0,
    radius: 0.5,
    }
    })
    })




注意:以上方案three.js版本为0.157, 该版本对three/example/jsm/postprocessing目录中的后期效果通道相关文件做了较多调整,如果是用之前的three.js版本,修改内容可能有所不同。



至此我们就可以在之前的可视化图层基础上,加入几行代码实现辉光效果,以下是挑选一部分图层加上EffectLayer之后的效果,肉眼可见还是有很明显区别的。当然在使用过程中也发现了个别图层原有的问题需要做进一步优化。


Effect-BorderLayer1.gif


Effect-PointsLayer1.gif


Effect-SpriteLayer1.gif


Effect-cakeLayer1.gif


待解决问题


使用独立图层展示后期特效层有个明显缺点,无法关联默认基本图层的场景要素深度信息,最主要的影响是高德的建筑白模图层和自定义可视化图层的远近遮挡关系会丢失,导致可视化图层永远在最前面。比如下面这个城市主要道路的辉光效果,这个是需要后面花时间去解决的,写这篇文章的时候又找到几个方案,有时间再试一把,毕竟上面留给我的时间不多了。


Effect-FlowlineLayer2.gif


相关链接


three.js后期处理


three.js效果合成器文档和示例


实现模型材质局部辉光效果和解决辉光影响场景背景图显示的问题


Three.js带Depth实现分区辉光


作者:gyratesky
来源:juejin.cn/post/7293788726235365426
收起阅读 »

自研框架跻身全球 JS 框架榜单,排名紧随 React、Angular 之后!

web
前言 终于实现了一个重要目标!我独立研发的 JavaScript 框架 Strve,最近发布了重大版本 6.0.2。距离上次大版本发布已经接近两个月,期间进行了大量的优化,使得框架性能和稳定性都得到了大幅度的提升。在上次的大版本更新中,成功实现了对 JSX 语...
继续阅读 »

前言


终于实现了一个重要目标!我独立研发的 JavaScript 框架 Strve,最近发布了重大版本 6.0.2。距离上次大版本发布已经接近两个月,期间进行了大量的优化,使得框架性能和稳定性都得到了大幅度的提升。在上次的大版本更新中,成功实现了对 JSX 语法的全面支持,使得 Strve 在代码智能提示和代码格式化方面更加友好,进一步提高了开发效率。


介绍


相信有些小伙伴没有听说过 Strve 到底是什么,那我这里就大体介绍一下。


Strve 是一个可以将字符串转换为视图(用户界面)的 JavaScript 库。Strve 不仅易于使用,而且可以灵活地拆解不同的代码块。使用模板字符串开发用户界面,主要是利用 JavaScript 的能力,只关注 JavaScript 文件。Strve 又是一个易用性的 JavaScript 框架,它提供了很多实用的功能与生态工具。


我们可以通过一些简单的示例来了解 Strve 的使用方法。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Strve.js</title>
</head>

<body>
<script src="https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full.prod.js"></script>
<script>
const { html, setData, createApp } = Strve;
const state = {
count: 0,
};

function add() {
setData(() => {
state.count++;
});
}

function App() {
return html`<h1 onClick=${add}>${state.count}</h1>`;
}

const app = createApp(App);
app.mount('#app');
</script>
</body>
</html>

在上述代码中,我们通过引入 Strve 库,并使用 createApp 方法创建了一个 App 组件,然后通过 mount 方法挂载到页面上,这里的 App 组件就是通过模板字符串来定义的。这样就可以在 JS 代码中编写用户界面,是不是很方便呢?我们发现,在模板字符串中,我们使用 ${} 来引用数据,并且使用 onClick 方法来绑定事件。这样就可以实现一个计数器的功能。


除了这种简单的示例,Strve 还支持很多复杂的功能,我们可以使用 JSX 语法来编写组件,也可以使用函数式组件来编写组件,还可以使用组件来编写组件,甚至可以编写一些自定义的组件。


如果想了解更多关于 Strve 的信息,稍后可以到文章末尾处查阅官方文档。


性能评估


我们既然发布了 Strve,那么肯定需要对其性能进行评估,我们评估的工具就用js-framework-benchmarkjs-framework-benchmark 是什么?我们这里就简单介绍下 js-framework-benchmark,它是一个用于比较 JavaScript 框架性能的项目。它旨在通过执行一系列基准测试来评估不同框架在各种场景下的性能表现。这些基准测试包括渲染大量数据、更新数据、处理复杂的 UI 组件等。通过运行这些基准测试,可以比较不同框架在各种方面的性能优劣,并帮助开发人员选择最适合其需求的框架。js-framework-benchmark 项目提供了一个包含多个流行 JavaScript 框架的基准测试套件。这些框架包括 Angular、React、Vue 等。每个框架都会在相同的测试场景下运行,然后记录下执行时间和内存使用情况等性能指标。通过比较这些指标,可以得出不同框架的性能差异。这个项目的目标是帮助开发人员了解不同 JavaScript 框架的性能特点,以便在选择框架时能够做出更加明智的决策。同时,它也可以促进框架开发者之间的竞争,推动框架的不断改进和优化


在评估之前,我们必须要了解 js-framework-benchmark 中有两种模式。一种是 keyed,另一种是 non-keyed。在 js-framework-benchmark 中,"keyed" 模式是指通过给数据项分配一个唯一标识符作为 "key" 属性,从而实现数据项与 DOM 节点之间的一对一关系。当数据发生变化时,与之相关联的 DOM 节点也会相应更新。而 non-keyed 模式是指当数据项发生变化时,可能会修改之前与其他数据项关联的 DOM 节点。


因为 Strve 支持keyed模式,所以我们将使用此模式来评估 Strve 的性能。


对以下操作进行了基准测试:



  • 创建行:页面加载后创建 1,000 行的持续时间(无预热)。

  • 替换所有行:替换表中所有 1,000 行的持续时间(5 次预热迭代)。

  • 部分更新:对于具有 10,000 行的表,每 10 行更新一次文本(进行 5 次预热迭代)。

  • 选择行:响应单击该行而突出显示该行的持续时间。 (5 次预热迭代)。

  • 交换行:在包含 1,000 行的表中交换 2 行的时间。 (5 次预热迭代)。

  • 删除行:删除具有 1,000 行的表的行的持续时间。 (5 次预热迭代)。

  • 创建多行:创建 10,000 行的持续时间(无预热)

  • 将行追加到大型表:在包含 10,000 行的表中添加 1,000 行的持续时间(无预热)。

  • 清除行:清除填充有 10,000 行的表的持续时间。 (无热身)

  • 就绪内存:页面加载后的内存使用情况。

  • 运行内存:添加 1,000 行后的内存使用情况。

  • 更新内存:1000 行的表点击 5 次更新后的内存使用情况。

  • 替换内存:点击 5 次创建 1000 行后的内存使用情况。

  • 重复清除内存:创建并清除 1,000 行 5 次后的内存使用情况。

  • 更新内存:1000 行的表点击 5 次更新后的内存使用情况。

  • 启动时间:加载和解析 javascript 代码以及渲染页面的持续时间。

  • 持续交互:灯塔指标 TimeToConstantlyInteractive:悲观 TTI - 当 CPU 和网络都非常空闲时。 (不再有超过 50 毫秒的 CPU 任务)

  • 脚本启动时间:灯塔指标 ScriptBootUpTtime:解析/编译/评估所有页面脚本所需的总毫秒数

  • 主线程工作成本:灯塔指标 MainThreadWorkCost:在主线程上工作所花费的总时间包括样式/布局等。

  • 总字节权重:灯塔指标 TotalByteWeight:加载到页面中的所有资源的网络传输成本(压缩后)。


对于所有基准测试,都会测量持续时间,包括渲染时间。


因为js-framework-benchmark是一个自动化测试的工具,只需要符合标准的代码就可以进行测试。Strve 支持 JSX 语法,所以我们将使用 JSX 语法来编写测试代码。


import { setData, createApp } from 'strve-js';
import { buildData } from './data.js';

let selected;
let rows = [];

function setRows(update = rows.slice()) {
setData(
() => {
rows = update;
},
{
name: TbodyComponent,
}
);
}

function add() {
const data = rows.concat(buildData(1000));
setData(
() => {
rows = data;
},
{
name: TbodyComponent,
}
);
}

function remove(id) {
rows.splice(
rows.findIndex((d) => d.id === id),
1
);
setRows();
}

function select(id) {
setData(
() => {
selected = id;
},
{
name: TbodyComponent,
}
);
}

function run() {
setRows(buildData());
selected = undefined;
}

function update() {
for (let i = 0; i < rows.length; i += 10) {
rows[i].label += ' !!!';
}
setRows();
}

function runLots() {
setRows(buildData(10000));
selected = undefined;
}

function clear() {
setRows([]);
selected = undefined;
}

function swapRows() {
if (rows.length > 998) {
const d1 = rows[1];
const d998 = rows[998];
rows[1] = d998;
rows[998] = d1;
setRows();
}
}

function TbodyComponent() {
return (
<tbody>
{rows.map((item) => (
<tr class={item.id === selected ? 'danger' : ''} data-label={item.label} key={item.id}>
<td class='col-md-1'>{item.id}</td>
<td class='col-md-4'>
<a onClick={() => select(item.id)}>{item.label}</a>
</td>
<td class='col-md-1'>
<a onClick={() => remove(item.id)}>
<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>
</a>
</td>
<td class='col-md-6'></td>
</tr>
))}
</tbody>

);
}

function MainBody() {
return (
<fragment>
<div class='jumbotron'>
<div class='row'>
<div class='col-md-6'>
<h1>Strve-keyed</h1>
</div>
<div class='col-md-6'>
<div class='row'>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='run' onClick={run}>
Create 1,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='runlots'
onClick={runLots}
>

Create 10,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='add' onClick={add}>
Append 1,000 rows
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='update'
onClick={update}
>

Update every 10th row
</button>
</div>
<div class='col-sm-6 smallpad'>
<button type='button' class='btn btn-primary btn-block' id='clear' onClick={clear}>
Clear
</button>
</div>
<div class='col-sm-6 smallpad'>
<button
type='button'
class='btn btn-primary btn-block'
id='swaprows'
onClick={swapRows}
>

Swap Rows
</button>
</div>
</div>
</div>
</div>
</div>
<table class='table table-hover table-striped test-data'>
<component $name={TbodyComponent.name}>{TbodyComponent()}</component>
</table>
<span class='preloadicon glyphicon glyphicon-remove' aria-hidden='true'></span>
</fragment>

);
}

createApp(() => MainBody()).mount('#main');

以下页面就是将进行基准测试的页面:


01.png


我们大体看下测试过程,我们将使用动图来展示页面效果,这样会觉得更加直观。


02.gif


最终,Strve 通过了压力测试!


08.png


基准测试结果


既然我们通过测试,我们就需要提交到js-framework-benchmark官方项目中,进行综合评估,与全球其他框架进行比较。


我们提交的 PR 在 2023 年 9 月 18 号被作者合并了。


03.png


在接下来的时间里,作者进行了一系列的测试。最终,Chrome 118 版本于上周发布,并在 GitHub 上公布了官方的测试结果。


04.png


我们打开下面的网址,看下 Strve 的官方测试结果:


krausest.github.io/js-framewor…


经过查询,全球 JavaScript 框架榜单中共有 142 个框架。


性能测试基准分为三类:



  • 持续时间

  • 启动指标

  • 内存分配


【持续时间】


在此测试基准中,Strve 平均值 1.42,排名第 90 位。


React、Angular 和 Vue,平均值分别为1.401.381.20,分别排名第 85 位、第 83 位和第 51 位。



平均值越小,排名则越靠前。颜色越绿代表越优。



05.png


【启动指标】


在此测试基准中,Strve 平均值 1.07


React、Angular 和 Vue,平均值分别为 1.681.801.30



平均值越小,排名则越靠前。颜色越绿代表越优。



06.png


【内存分配】


在此测试基准中,Strve 平均值 1.33


React、Angular 和 Vue,平均值分别为 2.462.821.86



平均值越小,排名则越靠前。颜色越绿代表越优。



07.png


新特性


我们在上面的测试中,可以看到 Strve 性能表现非常不错。


这次我们发布的大版本号为 6.0.2,我们将这个具有里程碑意义的大版本命名为 Strve6,而 “Strve6,从芯出发!” 这个口号正是 Strve6 的核心理念。这一版本象征着我们从底层技术出发,致力于为用户提供更优质、更高效的开发体验。


此次版本我们在性能与体验之间做了权衡。在源码层面,我们将普通 Diff 算法升级为 双端 Diff 算法,大大提升了性能。另外,我们在用户体验层面也做了很大的改进。


这里,我们提到了双端 Diff 算法,我们在面试中经常提到这个概念,但是很少用到实际项目中去。那么,为了更好地理解双端 Diff 算法如何提高性能,我们来看一个关于 Strve 简单的示例。


我们来遍历一个数组,并且每次点击按钮,往数组头部中添加一个元素。


【普通 Diff 算法】


<script type="module">
import {
html,
setData,
createApp,
} from 'https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full-esm.js';

const state = {
arr: [1, 2],
count: 3,
};

function useUnshift() {
setData(() => {
state.count++;
state.arr.unshift(state.count);
});
}

function App() {
return html`
<fragment>
<button onClick=
${useUnshift}>Unshift</button>
<ul>
${state.arr.map((todo) => html`<li>${todo}</li>`)}
</ul>
</fragment>
`
;
}

const app = createApp(App);
app.mount('#app');
</script>

我们可以看到右侧 DOM 树,每次点击按钮,都会重新渲染整个列表。这样是肯定耗损浏览器性能的。


09.gif


【双端 Diff 算法】


<script type="module">
import {
html,
setData,
createApp,
} from 'https://cdn.jsdelivr.net/npm/strve-js@6.0.2/dist/strve.full-esm.js';

const state = {
arr: [1, 2],
count: 3,
};

function useUnshift() {
setData(() => {
state.count++;
state.arr.unshift(state.count);
});
}

function App() {
return html`
<fragment>
<button onClick=
${useUnshift}>Unshift</button>
<ul>
${state.arr.map((todo) => html`<li key=${todo}>${todo}</li>`)}
</ul>
</fragment>
`
;
}

const app = createApp(App);
app.mount('#app');
</script>

我们可以看到右侧 DOM 树,每次点击按钮,仅添加必要的元素,而不是重新渲染整个列表。这是因为我们在每个列表项中添加了 key 属性,并且这个 key 是唯一的。key 这个特殊的 attribute 主要作为 Strve 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode。只要标签类型与 key 值都相等,就说明当前元素可以被复用。


10.gif


热门话题


文章接近尾声,让我们来回顾一下最近社区的几个热门话题。



  1. 为什么要开发这个框架?初衷是什么?


答:其实,我的动机特别简单,完全受 JSX 语法的影响。刚接触 JSX 语法的时候,就被它那种魔法深深地吸引住了,可以在 JS 中写 HTML。所以,我就想我自己可不可以也搞一个类似 JSX 语法的库或者框架呢!一方面可以锻炼自己的代码能力,另一方面体验开发框架的整个流程,也方便我以后更全面的学习其他框架(Vue.js、React.js 等)。


做自己喜欢的事情是特别有意义的!



  1. 为什么选择 Strve 作为框架的名字?


答:Strve 最初定位是可以将字符串转换为视图(用户界面)的 JavaScript 库,所以是由 StringView 两个单词缩减组成的新单词。



  1. 跟前端热门框架比较,是想超过它们吗?


答:不是,我主要是想学习一下前端热门框架的实现原理,然后自己实现一个框架。有一句话说得好:“只有站在巨人的肩膀上才能望得更远!”。



  1. 记得之前也写过登上框架榜单的文章,这次为什么还要写?


答:之前,Strve 测评的模式是使用"non-keyed"。现在,Strve 新的版本支持"keyed"模式,所以,我重新写了一篇文章,来介绍 Strve 的新特性。



  1. Strve 6.0.2 版本发布,普通 Diff 算法升级为双端 Diff 算法,可以简单讲下双端 Diff 算法的概念吗?


答:双端 diff 算法就是头尾指针向中间移动,分别判断头尾节点是否可以复用,如果没有找到可复用的节点再去遍历查找对应节点的下标,然后移动。全部处理完之后要对剩下的节点进行批量的新增和删除。



  1. Strve 是个 JavaScript 库还是 JavaScript 框架?


答:首先,我们来看下框架与库有什么区别?库更多是一个封装好的特定的集合,提供给开发者使用,而且是特定于某一方面的集合(方法和函数),库没有控制权,控制权在使用者手中,在库中查询需要的功能在自己的应用中使用,我们可以从封装的角度理解库;框架顾名思义就是一套架构,会基于自身的特点向用户提供一套相当于叫完整的解决方案,而且控制权的在框架本身,使用者要找框架所规定的某种规范进行开发。Strve 可以是框架,因为 Strve 提供了路由、插件等生态工具;Strve 也可以是库, 因为 Strve 可以单独作为一个渲染库。



  1. Strve 你还要继续维护下去吗?


答:是的,我还会继续维护下去,因为我也想学习下去,也希望能帮助到更多前端开发者。


关于


Strve 我是从 2021 年下半年开始开发,到现在也快两年了。在这两年中,从一个之前只会 调用 API 的码农,到现在可以独立开发一个框架,让我收获了很多。学习了如何去分析一个框架的实现原理,也学习了如何去设计一个框架。



Strve 源码仓库:github.com/maomincodin…


Strve 中文文档:maomincoding.gitee.io/strve-doc-z…



如果大家觉得 Strve 还不错,麻烦帮我点下 Star 吧,谢谢!


结语


感谢各位读者的阅读,希望本文能对你有所帮助,如果喜欢本文,欢迎点赞,欢迎关注!


最后,分享一段话给大家:



很多时候


不是有希望才去坚持


而是在坚持的过程中慢慢看到希望


我们都是在暗夜里赶路的人


纵使满身疲惫也不肯轻言放弃


愿你所坚持的东西


终有一天反过来拥抱你



作者:前端历劫之路
来源:juejin.cn/post/7293786784127025179
收起阅读 »

【JavaScript】【表达式和运算符】instanceof

web
前言 在JavaScript中,判断变量的类型,常常使用的是typeof运算符 typeof的痛点: 所有的引用类型结果都是 Object; 空值null的结果也是Object; 为此,引入 instanceof 一、instanceof 1.1 作用 ...
继续阅读 »

前言


在JavaScript中,判断变量的类型,常常使用的是typeof运算符


typeof的痛点



  • 所有的引用类型结果都是 Object

  • 空值null的结果也是Object


image.png


为此,引入 instanceof


一、instanceof


1.1 作用



  • 用于判断某个实例是否属于某构造函数

  • 在继承关系中,用来判断一个实例是否属于它的父类型或祖先类型的实例


1.2 使用




  • 语法object instanceof constructor




  • 参数



    • object:某个实例对象

    • constructor:某个构造函数




  • 示例:




// 类
class Maomi {} // 定义类
let fuLai = new Maomi() // fuLai是Maomi类的实例对象
fuLai instanceof Maomi // true

// 时间
new Date() instanceof Date // true

// 构造函数
function SetMaomi() {}
let fulai = new SetMaomi();
fulai instanceof SetMaomi // true

// 函数
function getMaomi() {}
getMaomi instanceof Function // true

1.3 涉及的构造函数



  • 基础类型:String、Number、 Boolean、 Undefind、Null、Symbol

  • 引用类型:Object(Array、RegExp、Date、Function...)


1.3 实现原理


instanceof 的内部实现机制是:通过判断对象的原型链上是否能找到对象的 prototype,来确定 instanceof 返回值。


function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while (true) {
if (L === null)
return false;
if (O === L)
return true;
L = L.__proto__;
}
}

代码释义

① L表示对象实例,R表示构造函数或者父类型实例

② 取R的显式原型,取L的隐式原型

③ 循环遍历,进行判断②中的两个值是否相等,相等返回true,不相等继续查找L的原型链


instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。



  • 示例:


function SetMaomi() {}
let fulai = new SetMaomi();
fulai instanceof SetMaomi

观察fulai.__proto__SetMaomi.prototype的结构:

image.png


image.png



注意点fulai instanceof SetMaomi 返回 true,则并不意味着该表达式会永远返回 trueSetMaomi.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 fulai 的原型链上,这时原表达式的值就会成为 false



二、instanceof产生继承关系


function Cat(name,age,type){
this.name = name;
this.age = age;
this.type = type;
}
function YingDuan(name,age,type,sex){
Cat.call(this,name,age,type);
this.sex = sex;
}
YingDuan.prototype = new Cat(); // 这里改变了原型指向,实现继承
var yd = new YingDuan("有鱼",2,"金渐层","男"); //创建了英短对象yd
console.log(yd instanceof YingDuan); // true
console.log(yd instanceof Cat); // true
console.log(yd instanceof Object); // true

下面为了直观的观察,我就不采用循环的方式,直接一个一个的打印出来:



  • yd instanceof YingDuan:

    image.png

  • yd instanceof Cat:

    image.png

  • yd instanceof Object:

    image.png


三、注意问题



  1. fulai instanceof SetMaomi 返回 true,则并不意味着该表达式会永远返回 true
    SetMaomi.prototype 属性的值有可能会改变,改变之后的值很有可能不存在于 fulai 的原型链上,这时原表达式的值就会成为 false

  2. instanceof 用于判断对象类型,但以下情况的结果都为false,请注意。


console.log(Number instanceof Number)  // false
console.log(String instanceof String) // false
console.log(null instanceof Object) // false,null不具有任何对象的特性,也没有__proto__属性

参考



作者:旺仔小猪
来源:juejin.cn/post/7293348107517001739
收起阅读 »

抓包调试工具的终极答案-whistle

web
前言 抓包工具是一种在计算机网络中进行调试和分析的强大工具,它能够拦截、查看和分析在网络中传输的数据包内容。通过捕获这些数据包,我们可以深入挖掘其中包含的大量有用信息。抓包工具不仅提供了直观和可视化的界面,而且具有强大的过滤和分析功能,使用户能够轻松地掌握网络...
继续阅读 »

前言


抓包工具是一种在计算机网络中进行调试和分析的强大工具,它能够拦截、查看和分析在网络中传输的数据包内容。通过捕获这些数据包,我们可以深入挖掘其中包含的大量有用信息。抓包工具不仅提供了直观和可视化的界面,而且具有强大的过滤和分析功能,使用户能够轻松地掌握网络数据流量的细节。


在计算机通信中,数据包是由发送端(如浏览器或应用程序)构建的,并通过互联网传输到接收端(如服务器或另一台计算机)。通常情况下,这些数据包由发送和接收的应用程序自行处理,用户往往无法直接观察这些数据包的内容。然而,抓包工具可以截取这些数据包,并将其内容以明文或加密的形式展示给用户。如果数据包是以明文形式发送的,或者我们可以推断出其加密方法,那么我们就可以对这些数据包进行深入的分析和解密。这样,我们便可以了解这些数据包的内容、用途和意义。


通过使用抓包工具,开发人员和系统管理员可以调试和分析网络应用程序和通信协议,以便更好地了解它们的性能、安全性和可靠性。此外,普通用户也可以利用抓包工具来了解他们正在使用的应用程序和网络服务的内部工作原理,并保障他们的网络安全和隐私。总之,抓包工具是计算机网络中不可或缺的一部分,它为我们提供了深入洞察和分析网络数据流量的能力。


抓包工具更多的分析网络流量以及数据包内容、检测网络问题、获取数据传输的详细信息、功能调试等。市面上常见的抓包工具有很多,比如说我们网页上常用到的浏览器开发者工具、移动端常用的vConsole以及市面上大家都在用的Charleswhistle,今天就简单给大家分享一下whistle的基本使用。


1. 简介


whistle主要是基于node来实现的一个跨平台web调试代理工具,whistle采用的是类似配置系统hosts的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过Node模块扩展功能


2. 安装启动


wproxy.org/whistle/ins…


1. 环境


首先要使用whistle的话,必须要具备node环境(下载地址),下载成功后可以通过命令行来查看是否已安装


node -v // 查询当前node版本信息

2.安装whistle


后面也会提到桌面端应用LightProxy,也是基于Electron和whistle的桌面端代理软件。


npm install -g whistle // Windows
sudo npm install -g whistle // Mac(非root权限)

安装成功可以使用命令whistle help 或者w2 help查看whistle的相关帮助信息


3.启动whistle


最新版本的whistle支持三种等价的命令whistlew2wproxy


w2 start // 启动whistle
w2 restart // 重启whistle
w2 stop // 停止whistle
w2 run // 调试模式启动whistle

4.配置代理


这里就以Mac配置举例,可以在设置->Wi-Fi->详细信息->代理中选择网页代理(HTTP)填入对应的ip以及端口号;


移动端这里同样以IOS为例,在Wi-Fi->HTTP代理中打开配置代理为手动,同时填入对应ip以及端口号.


我们也可以通过chrome浏览器插件SwitchyOmega来进行网页代理



  1. 点击新建情景模式

  2. 选择选项代理服务器

  3. 配置代理协议、代理ip、代理端口

  4. 点击应用选项保存并切换到代理模式


switchyOmega


5.证书安装


最后我们只需要安装根证书即可。我们打开whistle生成的浏览器生成页,点开HTTPS选项,点击二维码下载证书,这里同样以MAC和IOS为例。



1.Mac我们打开钥匙串访问,这里要注意,当我们添加完成后依旧属于不被信任状态,我们需要双击证书,在信任的里面找到使用此证书时选中始终信任,配置证书完成后我们选中Capture TUNNEL CONNECTS即可代理成功,捕捉传输内容。


证书
2.IOS同样我们通过扫码打开证书,允许描述配置文件下载,在设置已下载描述文件中安装描述文件,安装完成后我们打开通用->关于本机->证书信任设置中选择对下载的whistle证书启用完全信任即可。



如果是windows系统出现证书无法下载的情况,进入系统设置 - 网络 - Windows防火墙 - 高级设置,设置入站规则Node.js开头的都允许连接,保存确定下载;


手机端偶尔可能会遇到无法找到证书的情况,可以连接同一个局域网,访问电脑ip代理对应ip地址,扫码HTTPS进行证书下载




3.使用


whistle官网这里详细介绍了whistle相关的命令行参数,这里我们就不过多赘述。我们只介绍几个常用的功能。


1.HTTPS请求抓取:


所有配置完成后,我们打开whistle页面,浏览器或手机发起HTTPS请求后即可看到.


image.png
那么问题来了,这么多请求同时包含了预检等众多请求,我们怎么快速找到我们需要看到的接口呢?
我们可以在下方的Type filter text来进行简单的搜索功能,默认是按照url来进行搜索的,我们也可以按照固定的分类规则来进行快速查询



  1. 默认搜索url

  2. h: {word}搜索头部

  3. c: {word}搜索内容

  4. p: {word}搜索请求协议

  5. o: {word}搜索ip

  6. m: {word}搜索方法

  7. s: {word}搜索状态码


如果我们依旧觉得不够清晰该怎么办呢,我们就可以用到whistle的Rules功能,Rules支持我们通过扩展系统host配置来进行所有操作。


// whistle将通过pattern匹配来完成对应uri的操作
pattern operatorURI

pattern的匹配规则支持域名、路径、正则、精准匹配和通配符匹配


api.juejin.cn style://color=@fff&fontStyle=italic&bgColor=red

这样我们就可以更清晰的来找到我们想捕捉的内容。


image.png


2.请求响应修改


我们可以通过固定的Rules配置来对请求或者返回来进行修改测试


{pattern} method://get 请求方式修改为get
{pattern} statusCode://500 请求状态码返回500
{pattern} log:// 日志打印
{pattern} resCors:// 跨域

以上提到的是我们部分的简单修改,如果我们需要修改请求的请求体以及相应内容,我们就需要用到whistle提供的Values板块来进行配置


{pattern} reqHeaders://filepath 修改请求头 //filepath: Values里面的key或者本地文件
// reqHeaders example
{pattern} reqHeaders://{testReqHeadersJSON}
// resBody example
{pattern} resBody://{testResBodyJSON}

Values模版中配置testReqHeadersJSON和testResBodyJSON


image.png


image.png


这样就可以添加或者修改请求头内容,修改或添加响应内容同理。


image.png


3.移动端调试


whistle不仅提供强大的web调试功能,对于移动端的调试也是十分友好的。


由于移动端需要适配众多不同的浏览器和设备,包括各种尺寸的屏幕、各种操作系统和不同的设备载体,因此相对于PC端的页面调试,移动端的调试难度更加复杂。在出现问题时,排查的过程也涉及更多因素,需要我们及时发现并修复问题。对于一个合格的开发人员来说,强大的开发能力是基础,同时还需要拥有快速解决问题的能力和精准定位问题的技能,这样才能够在面对不同的问题时应对自如、犹游刃有余。


像我们在测试环境常用到的vConsole一般是不会在生产环境以一般方式展现给用户的,但是whistle提供注入js的方法能够让我们通过js代码以及vConsole等工具来进行页面调试以及问题的快速排查。


1.接口调试

这里我们就简单的以掘金首页为例



  1. 我们在Values中配置好vConsole以及对应生成实例代码;

  2. 在Rules中通过jsPrepend进行js注入
    这样我们就可以成功生成vConsole来进行调试了



vConsole.min.js的源码我们可以去github上自行下载,或者也可以通过插件来解决。


// 集成 vConsole、eruda、mdebug 等调试H5页面工具的插件
sudo npm install -g whistle.inspect

{pattern} whistle.inspect://vConsole
{pattern} whistle.inspect://eruda
{pattern} whistle.inspect://mdebug

2. 元素样式调试

whistle同时内置了Weinre和Chii来帮助我们进行调试


{pattern} weinre://{yourName}

配置Rules后我们在Whistle下拉选项下选中对应name,重新打开页面后即可进行elment调试


image.png{pattern} 同样配置Rules后我们在Whistle选项下的Plugins选中对应Chii,点击打开后选择inspect来进行element调试


{pattern} whistle.chii://

image-20230921140713086.png


4.LightProxy


下载地址


LightProxy是基于whistle使用electron开发的一个桌面端代理软件,从操作以及证书代理配置上更加简单灵活,更好的满足开发者的需求,基本使用规则同whistle一致;同时也帮我们继承了常用的像inspectvase等插件,更加方便快捷。


5.总结


Whistle作为常用的的网络调试工具之一,它不仅具备常规的抓包功能,还在跨域代理等方面展现了多样化的应用。通过巧妙的配置和强大的功能,我们可以进行深度定制和扩展,以满足各种复杂的调试需求。


这个工具的应用场景非常广泛,从简单的HTTP/HTTPS请求拦截,到复杂的爬虫和自动化测试,都可以借助Whistle实现。同时,它还支持JavaScript、TypeScript等多种编程语言,以及各种浏览器和Node.js环境。


使用Whistle进行调试非常简单,只需要简单地设置和配置,就可以轻松地实现对网络请求的拦截和修改。无论是排查问题、测试接口还是调试前端代码,Whistle都能够帮助我们快速定位问题并解决问题。它的易用性和灵活性也使得它成为了前端开发人员的得力助手。


通过使用Whistle,我们可以更好地了解网络请求的细节,掌握API接口的调用和数据传输的规律。这有助于我们优化代码、提高程序的稳定性和性能。因此,无论是初学者还是经验丰富的开发者,都应该尝试使用Whistle来提升自己的调试技能和开发效率。


参考


1.whistle官网: wproxy.org/whistle/


作者:洞窝技术
来源:juejin.cn/post/7293180747400134706
收起阅读 »

手把手教你打造一个“蚊香”式加载

web
前言 这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。 效果预览 从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。 HTML布局 首先我们通过1...
继续阅读 »

前言


这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。


效果预览



从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。


HTML布局


首先我们通过15span子元素来实现金钟罩的每一层,用于创建基本结构。从专业术语上讲,每个span元素都代表加载动画中的一个旋转的小点。通过添加多个span元素,可以创建出一串连续旋转的小点,形成一个加载动画的效果。


<div class="loader">
<span></span>
// 以下省略15span元素
</div>

CSS设计


完成了基本的结构布局,接下来就是为它设计CSS样式了。我们一步一步来分析:


首先是类名为loaderCSS类,相关代码如下。


.loader{
position: relative;
width: 300px;
height: 300px;
transform-style: preserve-3d;
transform: perspective(500px) rotateX(60deg);
}

我们将元素的定位方式设置为相对定位,使其相对于其正常位置进行定位。然后定义好宽度和高度之后,设置元素的变换样式为preserve-3d,这样可以元素的子元素也会受到3D变换的影响。除此之外,还需要transform属性来设置元素的变换效果。这里的perspective(500px)表示以500像素的视角来观察元素,rotateX(60deg)则表示绕X轴顺时针旋转60度。


这样就将一个宽高都定义好的元素进行了透视效果的3D旋转,使其以60度角度绕X轴旋转。


loader类可以理解为父容器,接下来就是loader类中的子元素span


.loader span{
position: absolute;
display: block;
border: 5px solid #fff;
box-shadow: 0 5px 0 #ccc,
inset 0 5px 0 #ccc;
box-sizing: border-box;
border-radius: 50%;
animation: animate 3s ease-in-out infinite;
}

通过以上样式,我们可以创建一个圆形的动画效果,边框有阴影效果,并且以动画的方式不断旋转。关于CSS部分大部分都是一样的,这里主要介绍一下这里定义的动画效果。名称为animate,持续时间为3秒,缓动函数为ease-in-out,并且动画无限循环播放。


@keyframes animate {
0%,100%{
transform: translateZ(-100px);
}
50%{
transform: translateZ(100px);
}
}

这是一个关键帧动画。关键帧是指动画在不同时间点上的状态或样式。首先该动画名为animate,它包含了三个时间点的样式变化:


0%100% 的时间点,元素通过transform: translateZ(-100px)样式将在Z轴上向后移动100像素,这将使元素远离视图。


50% 的时间点,元素通过transform: translateZ(100px)样式将在Z轴上向前移动100像素。这将使元素靠近视图。


通过应用这个动画,span元素将在动画的持续时间内以一定的速率来回移动,从而产生一个视觉上的动态效果。


最后就是单独为每个子元素span赋予样式了。


.loader span:nth-child(1){
top: 0;
left: 0;
bottom: 0;
right: 0;
animation-delay: 1.4s;
}
.loader span:nth-child(2){
top: 10px;
left: 10px;
bottom: 10px;
right: 10px;
animation-delay: 1.3s;
}
......
以下省略到第15span元素

第一个span元素的样式设置了top、left、bottom和right属性为0,这意味着它会填充父元素的整个空间。它还设置了animation-delay属性为1.4秒,表示在加载动画开始之后1.4秒才开始播放动画。


后面14span元素都是按照这个道理,以此类推即可。通过给span元素的动画延迟属性的不同设置,可以实现加载动画的错落感和流畅的过渡效果。


总结


以上就是整个效果的实现过程了,通过设计的动画来实现这个蚊香式加载,整体还是比较简单的。大家可以去码上掘金看看完整代码,然后自己去尝试一下,如果有什么创新的地方或者遇到了什么问题欢迎在评论区告诉我~


作者:一条会coding的Shark
来源:juejin.cn/post/7291951762948259851
收起阅读 »

【Java集合】来了两个“插班生”如何打印花名册,以数组案例带你搞懂Collection集合概念

嗨~ 今天的你过得还好吗?到那时风变得软绵绵的🌞1.1 数组的特点步骤:有三个学生,放在一个长度为3的数组花名册打印学生突然来了两个插班生,请放在数组花名册中无法插入,通过重新定义一个新的数组,组成新的花名册下面我们来实现这个案例:2.输入...
继续阅读 »


嗨~ 今天的你过得还好吗?

每件事情都会好起来的

到那时风变得软绵绵的

阳光也会为你而温暖

🌞


这个系列是我在学习Java集合这部分时候,结合书籍和Java提供的api整理的部分知识,也参考了一些网络上的文章,如果错误,望大家指出。希望本系列文章对大家学习Java有所帮助,也可以回顾下这部分的基础知识,温故而知新。


集合概述

1.1 数组的特点

Java是一种面向对象语言,对一个事物的描述都是以对象的形式存在,为了方便操作这些对象,就需要把这些对象存储起来。为容纳一组对象,我们最适宜的选择就是Array数组;而且容纳一系列的基础数据类型的话,更是必须采用数组。


我们通过一个小案例来回顾一下之前的数组知识。数组不仅可以存放基本数据类型也可以容纳属于同一种类型的对象。


数组案例:一个小班有三个学生,请打印学生的姓名和年龄?

步骤:

  • 有三个学生,放在一个长度为3的数组花名册

  • 打印学生

  • 突然来了两个插班生,请放在数组花名册中

  • 无法插入,通过重新定义一个新的数组,组成新的花名册


下面我们来实现这个案例:

1.首页创建一个 javaee 的项目

Description


2.输入名称 collectPractice,选择对应的 JDK 版本 1.8

Description


3.新增 Class  student

Description

4.文件内容如下:

public class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里可以查看

5.在 Main.java 中,我们将要完成的需求写到注释中,逐行去打印结果

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));
}
}

结果:

Description

6.来了两个新学生,也要加入到花名册中,直接使用数组添加,打印花名册,发现报错

import java.util.Arrays;

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));

//两个插班生
Student student4 = new Student("科比", 40);
Student student5 = new Student("欧文", 25);

students[3] = student4;
students[4] = student5;

System.out.println("学生花名册---"+ Arrays.toString(students));
}
}

打印结果

Description


7.所以在此时,我们需要重新 new 一个 长度为 5 的数组,重新设置新的花名册

public class Main {
public static void main(String[] args) {

/*
我们的业务需求是这样的:
1. 有三个学生,放在一个长度为3的数组花名册
2. 打印学生
3. 突然来了两个插班生,请放在数组花名册中
4. 请获取第三个学生的姓名
* */

//定义一个数组,存储我们的数据
Student[] students = new Student[3];
Student student1 = new Student("麦迪", 29);
Student student2 = new Student("库里", 29);
Student student3 = new Student("杜兰特", 29);


//记录设置到数组中
students[0] = student1;
students[1] = student2;
students[2] = student3;

//打印 花名册 通过数组工具
System.out.println("学生花名册---"+ Arrays.toString(students));

//两个插班生
Student student4 = new Student("科比", 40);
Student student5 = new Student("欧文", 25);

// students[3] = student4;
// students[4] = student5;
//
// System.out.println("学生花名册---"+ Arrays.toString(students));

Student[] studentsNew = new Student[5];
for (int i = 0; i < students.length; i++) {
studentsNew[i] = students[i];
}
studentsNew[3] = student4;
studentsNew[4] = student5;


System.out.println("学生花名册---"+Arrays.toString(students));
System.out.println("学生新的花名册---"+Arrays.toString(studentsNew));
}
}

打印结果:

Description


分析结论

  • 数组长度在初始化后,就确定了,不能更改,不便于存储数量的扩展。比如我们再来了两个插班生,直接往元素组添加元素,会报错误信息。

  • 数组提供的属性和方法少,不便于操作。比如我们在打印花名册时,需要借助工具类的toString方法。

  • 存储的类型可以是基本类型,也可以是对象,但是必须是同一类型。


因为数组存在的这些缺点,Java语言又为我们提供了一种新的存储数据并且存储空间可变的容器,这就是我们Java集合的概念。它和数组一样,都是可以存储数据的容器,一种存储空间可变的存储模型,并且存储的数据容量可以随时发生改变。


1.2 集合的特点

最后我们来总结一下集合的特点:

  • 可以动态保存任意多的对象,使用方便;

  • 集合提供了一系列操作元素的方法,使集合元素的添加和修改等操作变得简单;

  • 集合还可以保存具有映射关系的关联数据;

  • 集合只能保存对象,实际上保存的是对象的引用地址。


文章就写到这里了,觉得不错的话点个赞支持一下吧!


收起阅读 »

为了方便写文章,我开发了一个目录树🌲生成器

web
这个工具主要是为了方便在写文章的时候,展示自己的项目的目录结构,或者在README文件中介绍项目使用的,上传文件夹后可以生成目录结构,支持一键复制。 您可以通过以下链接访问:目录树生成器 - 在线使用 Next.js 是一个React全栈框架,它不仅可以用于...
继续阅读 »

这个工具主要是为了方便在写文章的时候,展示自己的项目的目录结构,或者在README文件中介绍项目使用的,上传文件夹后可以生成目录结构,支持一键复制。


image.png


您可以通过以下链接访问:目录树生成器 - 在线使用


Next.js


是一个React全栈框架,它不仅可以用于构建服务器端渲染(SSR),也支持支持静态渲染。


webkitdirectory



HTMLInputElement.webkitdirectory 是一个反应了 HTML 属性 webkitdirectory 的属性,其指示 <input> 元素应该让用户选择文件目录而非文件。在选择文件目录后,该目录及其整个内容层次结构将包含在所选项目集内。可以使用 webkitEntries (en-US) 属性获取选定的文件系统条目。

———————MDN



简而言之 利用这属性,我们可以在浏览器中上传文件夹,并获取到文件的目录结构。


可以看一个简单的栗子🌰


这个功能,也有一个兼容问题,具体参考这个:


image.png


有一些老版本的浏览器和安卓端火狐浏览器不支持的无法使用该功能。


数据转换


我们要将原数据转换一下


-   Java/main/main.java
- Java/main/main.class
- Java/hello/HelloWorld.class
- Java/hello/HelloWorld.java
- Java/OOP/xx.js
- Java/OOP/Person.class
- Java/OOP/oop.class
- Java/OOP/oop.java

转换为:


{
"name": "Java",
"type": "folder",
"contents": [
{
"name": "main",
"type": "folder",
"contents": [
{
"name": "main.java",
"type": "file"
},
{
"name": "main.class",
"type": "file"
}
]
},
{
"name": "hello",
"type": "folder",
"contents": [
{
"name": "HelloWorld.class",
"type": "file"
},
{
"name": "HelloWorld.java",
"type": "file"
}
]
},
{
"name": "OOP",
"type": "folder",
"contents": [
{
"name": "xx.js",
"type": "file"
},
{
"name": "Person.class",
"type": "file"
},
{
"name": "oop.class",
"type": "file"
},
{
"name": "oop.java",
"type": "file"
}
]
}
]
}

将路径结构转化为对象结构,方便我们的后续逻辑处理,转化方法是:



function convertToDirectoryStructure(fileList) {
const directory = {
name: "App",
type: "folder",
contents: [],
};

for (let i = 0; i < fileList.length; i++) {
const pathSegments = fileList[i].webkitRelativePath.split("/");
let currentDirectory = directory;

for (let j = 0; j < pathSegments.length; j++) {
const segment = pathSegments[j];
const isDirectory = j < pathSegments.length - 1;

let existingEntry = currentDirectory.contents.find((entry) => entry.name === segment);
if (!existingEntry) {
existingEntry = { name: segment };
if (isDirectory) {
existingEntry.type = "folder";
existingEntry.contents = [];
} else {
existingEntry.type = "file";
}
currentDirectory.contents.push(existingEntry);
}

currentDirectory = existingEntry;
}
}

return directory.contents[0];
}

最终效果


最后我们再加上一个一键复制的功能,就完成了。



最后我是将功能优化后部署到了GitHub Pagas上,如何将Next.js部署到GitHub Pages,可以看看我的这篇 如何将Next.js部署到Github Pages


最后希望大家多多使用,给个star,GitHub地址:dir-tree


作者:九旬
来源:juejin.cn/post/7292955000454692875
收起阅读 »

ElectronEgg 快速开发一个桌面应用

web
大家好,我是哆啦好梦。electron-egg 3.8.0 终于发布了。 近3个月的累积更新,让 electron-egg 框架的开发体验更加丝滑。框架也终于到了一个功能完善且非常稳定的版本。 目前,框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、E...
继续阅读 »

大家好,我是哆啦好梦。electron-egg 3.8.0 终于发布了。


近3个月的累积更新,让 electron-egg 框架的开发体验更加丝滑。框架也终于到了一个功能完善且非常稳定的版本。


目前,框架已经广泛应用于记账、政务、企业、医疗、学校、股票交易、ERP、娱乐、视频等领域客户端,请放心使用!


home.png


为什么使用


桌面软件(办公方向、 个人工具),仍然是未来十几年 PC 端需求之一,提高工作效率


electron 技术是流行趋势,QQ、百度翻译、阿里网盘、迅雷、有道云笔记 ......


开源


gitee:gitee.com/dromara/ele… 3900+


github:github.com/dromara/ele… 1200+


本次更新


3.8.0



  1. 【增加】新增 ee-bin exec 命令,支持自定义命令。

  2. 【增加】新增 ee-core jobs 配置,打开/关闭 messageLog。

  3. 【优化】优化 ee-core jsondb 异常处理。

  4. 【优化】优化 ee-core controller/services 异常捕获并写log。

  5. 【优化】优化 ee-bin loading 动画居中。

  6. 【优化】优化 electron-egg logo,优化mac图标,优化Linux系统图标。

  7. 【优化】优化 electron-egg loading 动画居中。

  8. 【升级】升级ee-core v2.6.0,升级ee-bin v1.3.0


下载


# gitee
git clone https://gitee.com/dromara/electron-egg.git

# github
git clone https://github.com/dromara/electron-egg.git

安装


# 设置国内镜像源(加速)
npm config set registry=https://registry.npmmirror.com

#如果下载electron慢,配置如下
npm config set electron_mirror=https://registry.npmmirror.com/-/binary/electron/

# 根目录,安装 electron 依赖
npm i

# 进入【前端目录】安装 frontend 依赖
cd frontend
npm i

运行项目


npm run start

example.png


用户案例


aw-3.png


p1.png


p3.png


更多


访问官网:electron-egg: 一个入门简单、跨平台、企业级桌面软件开发框架


作者:哆啦好梦
来源:juejin.cn/post/7292961931509186595
收起阅读 »

菜鸟前端故事之翅膀硬了

web
2019的故事 最近太忙再加上掘金对此类文章并不推荐,所以写作热情有所消退,停了一段时间没更新,现趁着有点空再继续写一下2019的故事。 话接前文,在之前度过了还算完美的2018,收获了比较满意的工作,也找到了对象,可谓是事业爱情小丰收。 在过年的时候,我人生...
继续阅读 »

2019的故事


最近太忙再加上掘金对此类文章并不推荐,所以写作热情有所消退,停了一段时间没更新,现趁着有点空再继续写一下2019的故事。


话接前文,在之前度过了还算完美的2018,收获了比较满意的工作,也找到了对象,可谓是事业爱情小丰收。


在过年的时候,我人生第一次有了上万存款,发了不少红包给家里老人和晚辈,那种回报家人的感觉真的很好。
现在看来9k的薪资不算什么,但算是很快达到了老爸的预期。若不是计算机专业和互联网的崛起,我这破学校不可能刚毕业就有这种薪资,我庆幸生对了时代,做了对的选择。


加薪


小美妆没有年终奖,年末聚餐的时候发了800块红包,我收到很开心只觉得这是一笔意外的收入。当时也不懂什么13薪之类的,更不知道、也不敢想象在互联网还有四五个月的年终奖,觉得没有很正常。即使有听说腾讯给全体发iphone、游戏工作室数十个月年终,也觉得很遥远,那远不是我能触及的层次,又何苦去比较。


包括小美妆的976作息,因为自己的菜,便也觉得不合理的事也是合理。


可喜的是刚过完年不久,小张总就给我们加了工资,1000块,我开心了很久,不是仅仅是因为多了1000块,另一层含义是我的月薪上万了,传说中的月薪过万我终于达到了,我还记得我当时激动的跟我爸妈还有女友分享,爸妈直呼老板良心让我跟着好好干,在他们眼里能主动加薪还加这么多的太难得了,回想起来还是觉得那一刻很幸福。


躺不平


今年我们有个任务就是要把小美妆的网站做成一个小程序,一度令当时的我头疼,因为我只会jquery连vue都不会更别说小程序了。一开始甚至想直接用小程序包裹webview直接套壳做好的网站了,但是理智和直觉告诉我不能那样,可能会挖大坑。


没办法只有埋头苦学,没想到仅仅两天我就可以上手了,并且把网站最难的特效部分转为用小程序实现了,原来小程序也没那么难嘛,心想我还是有点天赋的。


到后来的其他网站我开始尝试用vue去做,发现跟小程序几乎都是一样,也就顺风顺水了。估计这个顺序跟大多数前端都是反着来的,但也总算是会了一些主流现代的技术。
可惜那时候我还是用的引入CDN的方式引入vue,没有尝试构建工具,这也让我在后面的职业经历中吃了瘪。


在那一年我们做了很多小程序,移动端H5,后台管理系统,内部外部用的都有,对移动端开发、兼容有了一定的技术积累,后面也做了支付宝的小程序。


其实大多都是重复的技术,除了熟练度没有什么提高,到年中的时候已经算是进入平稳开发期,没有什么水花,也没有了深入学习的心思,因为大部分需求已经不需要动脑子
就能实现。用现在流行的一个词来说就是:躺平了。


小张总讲情义,只要我认真完成任务他应该不会赶我走,小作坊朋友式的公司氛围舒服,薪水也不错,不禁开始妄想熬成老员工躺平一辈子多好。


我突然意识到,自己好像成了守哥,可这似乎也没错。


幸运的是接下来发现的一件事让我意识到技术必须不断进步,而不幸的是明白这个道理让我们付出了惨重的代价。


昂贵的教训


在我们来之前公司是有一台旧的云服务器,是某财务软件公司给我们部署财务系统用,使用windowsServer系统,里面存放了相当多的财务信息,以及我们的公众号服务端项目、数据库。


在一天早上我们突然接到用户反馈公众号报错服务故障了,起初我们还以为又是夜间突发流量导致宕机了,心想着重启一下就好。可进入服务器一看却傻了眼,各种文件都被清空,变成了加密文件,连数据库也被删得一干二净只留下大约这么一段话:


recovery data connect email xxx


我们才意识到服务器被攻击入侵了,黑客将我们的数据全都加密,并勒索要求支付比特币才能解锁。


我们的财务数据、公众号用户的充值信息全没了。我清晰的记得,那一刻我的心凉到了冰点,因为我们的数据库已近半年没有备份。即使宽宏大量的小张总知道后也有些生气了,是啊,这件事带来的后果太麻烦了,毫无疑问技术部门负全责。


面对小张总的追问,我异常艰难的开口告诉了实情,在写这段的时候我仍然感觉令人窒息。
报警?支付赎金?找专业人士解密?那段时间我忙得团团转,急得像热锅上的蚂蚁,只想能够挽回损失。比特币赎金的价格高达十几万,而且也非常冒险很可能被再次勒索或者解不开,咨询专业人士都说解不了,甚至还有人冒出来说知道谁干的,可以作为中间人担保帮我们付费解锁。


我才明白原来这种事早已经形成了一条完整的黑色产业链,同时也对黑客这个词不再敬畏,而是深恶痛绝。随意的一次攻击可能毁掉一家公司,让上百无辜的人失业。


还记得有个坊间传闻说阿里面试某低学历高手,高手当面破解阿里系统,被破格录取并重用。有人如法炮制面试鹅厂,刚破解完腾讯系统当场报警被抓。以此来传言阿里格局大,腾讯格局小。虽然这个流言多半是假的,但我的态度在经历被勒索之后也变成了支持腾讯的一方,违法的事就是违法,不因场合而改变性质,永远也不应鼓励这种行为。不然谁都去破解一下,总有出大事的一天。


后来经过深思熟虑小张总还是决定不屈服,而是在现在基础上补救、向用户致歉补偿等措施。而我也开始拼命恶补网络安全方面的知识,这种情况要是再出现一次,即使老板不开除我,我自己也没脸待下去了。后来我们将系统换成了linux,把不该打开的端口也都关闭了,设置了强密码,安排了各种云服务商的安全服务,最重要的是做了db的定时备份。从那以后我们没再出过安全上的问题。


这件事给我好好的上了一课,在解决问题的过程中,面对很多技术一抹黑的境况也让我觉得心里没底,同时给了我要不断学习深入研究的决心。


技术态度的转变


自那以后,我意识到了技术是片汪洋大海,而之前的我不过是在小水坑里扑腾。我开始学习很多工作中暂时用不到的技术,或者在工作中尽量使用新的技术,这也是小团队的好处,没有历史包袱想怎么搞就怎么搞。


我开始学了react,学了php做自己厌恶的服务端,学了linux买了自己的服务器做了自己的网站,也用RN开始写App,第一次了解到了算法题的存在并开始练习,我疯狂的想要提升自己,不允许存在任何技术上的短板。我给自己列了长长的一串学习清单,还因为有其他公司看中我们的小程序想嵌入,去跟上市公司谈合作,签合同。那是除了在小外包入职时期以外的另一段高速汲取知识并成长的时光。



如果有前端初学的朋友在看,我想顺便提一下,个人认为想成为一名优秀的软件工程师,服务端是你无法避开的一环,虽然不需要精通,但是也不要一味的躲避。



2019在经历波折后归于平淡,在平淡的年末却又爆发了疫情,平时总是和女友两个城市来回跑,因为疫情居家被关到了一起,她还担心总被封闭在一起两个人会矛盾显现,结果没想到住一起那么快乐,感情越来越好,給枯燥的广漂生活增加了很甜蜜的一段时光。哈哈抱歉秀了一段,那时候真是觉得糟了,我陷入爱河啦。


离开小美妆


没有不散的宴席。


偶尔会听到小张总吐槽我们部门一年成本几十万好肉疼,不管是不是玩笑话,我也理解这种体量的公司养几个全职的程序员是极为奢侈的。


后来的近一年内我的工资逐渐涨到了12k左右,那时候的我对市面上的行情开始有了些了解,我同时兼任产品、ui、前端、项目管理的工作量,这个水平还是偏低的。


再加上当时的我也开始为未来做打算,我想要的东西很多,我也想给我爱的人更多,因为后来我接过了武汉房子的房贷,当时一个月扣除花费后只能存下三千块,照这个水平我猴年马月能过上我想要的生活啊。而小张总的话也侧面反应了我的薪水已经不会再有什么增长空间。说得更严重一点,技术部门甚至已经成为了公司的负担,后面小张总已经打算接外面的项目来做了。


有了爱人,有了技术,随之而来的便是动力与野心。


虽然我理解守哥,但我却不再想成为守哥。


总结下离开的3大原因:



  • 翅膀硬了觉得没有成长空间了

  • 真心希望给公司减负

  • 不想接外包项目
    基于以上的因素,我决定去到深圳,开始面试,我想要进入更强大的公司。


面试吃瘪之旅


第一次打开了招聘软件参与社招(找实习只用了下实习僧),被hr打招呼的热情搞得受宠若惊,后来才知道全是外包.....


很快就又遇到了某软,没错就是那个实习要录用又不留用我的某软,上来就要我学位证,没有社招经验的我还以为都是这样,啥也没干就老老实实的交了资料。


那时候不觉得外包经历是扣分项,得意洋洋的把小外包的实习经历写在了简历上,充了充工作年限,还写了自己兼任各种岗位(现在看来无疑是扣分项),估计hr心想这人真是个二笔。


hr先问了我现在的薪资,听完说他们这边最多给我13k,他们是按经验算薪资,你再牛不好意思你练习时长都还不够两年半,13k封顶了。我还是试着面了一下非常容易就过了,但是没选择去,只是试了试面试的感觉,我还是不傻的,这个涨幅和公司不值得。后来又面了一些公司比如万科、宝能之类的大型企业,基本有一半的通过率,那个阶段真是处于互联网的黄金年代,岗位多经济好。


当时很喜欢万科,可惜面试官问我微服务我根本答不上来,遗憾挂掉。至于bat之流的公司,还是不敢想。后来还是忍不住试着投了下腾讯,居然拿到了面试机会,我望着印着腾讯logo的面试邀请邮件,充满了渴望。至于面试结果当然不必期待,被一番吊打后挂掉,因为差距太大我也就对互联网大厂死了心。不过当时面试的小鹅拼拼部门据说非常累,而且后面整体被裁撤了。


之后通过了宝能的面试,在当时的我眼里可是大公司,公积金顶格交满,还有双休和食堂,虽然把我薪资压到了13k但我还是兴致勃勃准备去,可是offer一直没审批下来,当时搞得我一度很失落。


后来才知道宝能当时处于内部动荡期,已经锁了hc。


涨幅70%


宝能一直没消息我只能继续面,后来有一家大小周跨境电商公司给我了16x13,还有一家双休金融公司给了18x14,果断选择了后者,这个涨幅令当时的我激动不已,在接到oc后差点没跳起来,也算是正式年薪突破二十万了。


而且还给我了高级前端工程师的title,虽然也是在后来的经历中才知道非名企的职级就是个鸡肋,但当时可以说满足至极。意外的是宝能offer卡了其实是一件大好事,也或许是命运的眷顾。


因为过后不久宝能hr说愿意立即给我发offer,但是当时我已经入职金融公司就拒绝了。再后来就是宝能公积金断缴的消息,hr自己也离职了,再到前两天还看到宝能的姚总拖欠薪资被打的新闻,没进去走了狗屎运。


在拿到offer当天我跟小张总提了离职,他很惊讶,挽留了我并且说愿意加薪,但是我知道不可能在小美妆发展了,离开公司对双方都是好事,小张总的挽留可能只是出于客套或者情义。


其他几个小伙伴也很震惊,不理解这么舒服的地方为什么要离开,去外面肯定受拘束还要加班。可我已执意离开。


因为不好说那些肉麻的话,我在微信给小张总发了长长的感谢的文字。离开那月的工资多了几千奖金,我明白那是小张总对我的肯定,离职后的一天我仍然去了公司,把一个新项目的需求框架谈好才走,免得小伙伴们一时没法适应要自己去处理需求的情况。


至此,我正式离开了小美妆,而跟我一起来广州的小伙伴现在2023年仍然在那里就职。
非常感谢小张总的信任和包容,愿意给一个不认识的菜鸟机会,让他去组建团队,管理项目,而我也回报了一个两年流水过亿的项目。


这个项目也成为了我简历上闪耀的一段背书,或许也是之前大厂愿意给我面试机会的原因。


巅峰跃升


写得有点多了,下次再继续写在金融公司呆了不久又离开的故事,以及我职业经历的的再一次巅峰跃升,欢迎各位小伙伴留言交流~


作者:鹅猴
来源:juejin.cn/post/7264383071318671421
收起阅读 »

如何制作 GitHub 个人主页

web
原文链接:http://www.bengreenberg.dev/posts/2023-… 人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源...
继续阅读 »

原文链接:http://www.bengreenberg.dev/posts/2023-…


人们在网上首先发现你的地方是哪里?也许你的社交媒体是人们搜索你时首先发现的东西,亦也许是你为自己创建的投资组合网站。然而,如果你使用GitHub来分享你的代码并参与开源项目,那么你的GitHub个人主页可能是人们为了了解你而去的第一个地方。


你希望你的GitHub个人主页说些什么?你希望如何以简明易读的方式向访客表达对你的重要性以及你是谁?无论他们是未来的雇主还是开源项目的潜在合作伙伴,你都必须拥有一个引人注目的个人主页。


使用GitHub Actions,你可以把一个静态的markdown文档变成一个动态的、保持对你最新信息更新的良好体验。那么如何做到这一点呢?


我将向你展示一个例子,告诉你如何在不费吹灰之力的情况下迅速做到这一点。在这个例子中,你将学习如何抓取一个网站并使用这些数据来动态更新你的GitHub个人主页。我们将在Ruby中展示这个例子,但你也可以用JavaScript、TypeScript、Python或其他语言来做。


GitHub个人主页如何运作


你的GitHub个人主页可以通过在网页浏览器中访问github.com/[你的用户名]找到。那么该页面的内容来自哪里?


它存在于你账户中一个特殊的仓库中,名称为你的账户用户名。如果你还没有这个仓库,当你访问github.com/[你的用户名]时,你不会看到任何特殊的内容,所以第一步是确保你已经创建了这个仓库,如果你还没有,就去创建它。


探索仓库中的文件


仓库中唯一需要的文件是README.md文件,它是你的个人主页页面的来源。


./
├── README.md

继续在这个文件中添加一些内容并保存,刷新你的用户名主页,你会看到这些内容反映在那里。


为动态内容添加正确的文件夹


在我们创建代码以使我们的个人主页动态化之前,让我们先添加文件夹结构。


在顶层添加一个名为.github的新文件夹,在.github内部添加两个新的子文件夹:scripts/workflows/


你的文件结构现在应该是这样的:


./
├── .github/
│ ├── scripts/
│ └── workflows/
└── README.md

制作一个动态个人主页


对于这个例子,我们需要做三件事:



  • README中定义一个放置动态内容的地方

  • scripts/中添加一个脚本,用来完成爬取工作

  • workflows/中为GitHub Actions添加一个工作流,按计划运行该脚本


现在让我们逐步实现。


更新README


我们需要在README中增加一个部分,可以用正则来抓取脚本进行修改。它可以是你的具体使用情况所需要的任何内容。在这个例子中,我们将在README中添加一个最近博客文章的部分。


在代码编辑器中打开README.md文件,添加以下内容:


### Recent blog posts

现在我们有了一个供脚本查找的区域。


创建脚本


我们正在构建的示例脚本是用Ruby编写的,使用GitHub gem octokit与你的仓库进行交互,使用nokogiri gem爬取网站,并使用httparty gem进行HTTP请求。


在下面这个例子中,要爬取的元素已经被确定了。在你自己的用例中,你需要明确你想爬取的网站上的元素的路径,毫无疑问它将不同于下面显示的在 posts 变量中定义的,以及每个post的每个titlelink


下面是示例代码,将其放在scripts/文件夹中:


require 'httparty'
require 'nokogiri'
require 'octokit'

# Scrape blog posts from the website
url = "<https://www.bengreenberg.dev/blog/>"
response = HTTParty.get(url)
parsed_page = Nokogiri::HTML(response.body)
posts = parsed_page.css('.flex.flex-col.rounded-lg.shadow-lg.overflow-hidden')

# Generate the updated blog posts list (top 5)
posts_list = ["\n### Recent Blog Posts\n\n"]
posts.first(5).each do |post|
title = post.css('p.text-xl.font-semibold.text-gray-900').text.strip
link = "<https://www.bengreenberg.dev#{post.at_css('a')[:href]}>"
posts_list << "* [#{title}](#{link})"
end

# Update the README.md file
client = Octokit::Client.new(access_token: ENV['GITHUB_TOKEN'])
repo = ENV['GITHUB_REPOSITORY']
readme = client.readme(repo)
readme_content = Base64.decode64(readme[:content]).force_encoding('UTF-8')

# Replace the existing blog posts section
posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m
updated_content = readme_content.sub(posts_regex, "#{posts_list.join("\n")}\n")

client.update_contents(repo, 'README.md', 'Update recent blog posts', readme[:sha], updated_content)

正如你所看到的,首先向网站发出一个HTTP请求,然后收集有博客文章的部分,并将数据分配给一个posts变量。然后,脚本在posts变量中遍历博客文章,并收集其中的前5个。你可能想根据自己的需要改变这个数字。每循环一次博文,就有一篇博文被添加到post_list的数组中,其中有该博文的标题和URL。


最后,README文件被更新,首先使用octokit gem找到它,然后在README中找到要更新的地方,并使用一些正则: posts_regex = /### Recent Blog Posts\n\n[\s\S]*?(?=<\/td>)/m


这个脚本将完成工作,但实际上没有任何东西在调用这个脚本。它是如何被运行的呢?这就轮到GitHub Actions出场了!


创建Action工作流


现在我们已经有了脚本,我们需要一种方法来按计划自动运行它。GitHub Actions 提供了一种强大的方式来自动化各种任务,包括运行脚本。在这种情况下,我们将创建一个GitHub Actions工作流,每周在周日午夜运行一次该脚本。


工作流文件应该放在.github/workflows/目录下,可以命名为update_blog_posts.yml之类的。以下是工作流文件的内容:


name: Update Recent Blog Posts

on:
schedule:
- cron: '0 0 * * 0' # Run once a week at 00:00 (midnight) on Sunday
workflow_dispatch:

jobs:
update_posts:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v2

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1

- name: Install dependencies
run: gem install httparty nokogiri octokit

- name: Scrape posts and update README
run: ruby ./.github/scripts/update_posts.rb
env:
GITHUB_TOKEN: $
GITHUB_REPOSITORY: $

这个工作流是根据cron语法定义的时间表触发的,该时间表指定它应该在每个星期天的00:00(午夜)运行。此外,还可以使用workflow_dispatch事件来手动触发该工作流。


update_posts工作由几个步骤组成:



  • 使用 actions/checkout@v2操作来签出仓库。

  • 使用 ruby/setup-ruby@v1 操作来设置 Ruby,指定的 Ruby 版本为 3.1。

  • 使用 gem install 命令安装所需的 Ruby 依赖(httpartynokogirioctokit)。

  • 运行位于.github/scripts/目录下的脚本 update_posts.rbGITHUB_TOKENGITHUB_REPOSITORY环境变量被提供给脚本,使其能够与仓库进行交互。


有了这个工作流程,你的脚本就会每周自动运行,抓取博客文章并更新README文件。GitHub Actions负责所有的调度和执行工作,使整个过程无缝且高效。


将所有的东西放在一起


如今,你的网络形象往往是人们与你联系的第一个接触点--无论他们是潜在的雇主、合作者,还是开源项目的贡献者。尤其是你的GitHub个人主页,是一个展示你的技能、项目和兴趣的宝贵平台。那么,如何确保你的GitHub个人主页是最新的、相关的,并能真正反映出你是谁?


通过利用 GitHub Actions 的力量,我们展示了如何将你的 GitHub 配置文件从一个静态的 Markdown 文档转变为一个动态的、不断变化关于你是谁的例子。通过本指南提供的例子,你已经学会了如何从网站上抓取数据,并利用它来动态更新你的 GitHub个人主页。虽然我们的例子是用Ruby实现的,但同样的原则也可以用JavaScript、TypeScript、Python或你选择的任何其他语言来应用。


回顾一下,我们完成了创建一个Ruby脚本的过程,该脚本可以从网站上抓取博客文章,提取相关信息,并更新你的README.md文件中的"最近博客文章"部分。然后,我们使用GitHub Actions设置了一个工作流,定期运行该脚本,确保你的个人主页中保持最新的内容。


但我们的旅程并没有就此结束。本指南中分享的技术和方法可以作为进一步探索和创造的基础。无论是从其他来源拉取数据,与API集成,还是尝试不同的内容格式,都有无限的可能性。


因此,行动起来让你的 GitHub 个人主页成为你自己的一个充满活力的扩展。让它讲述你的故事,突出你的成就,并邀请你与他人合作。


以上就是本文的全部内容,如果对你有所启发,欢迎点赞、收藏、转发~


作者:chuck
来源:juejin.cn/post/7251884086537650232
收起阅读 »

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpress/2…


作者:Mengke
来源:juejin.cn/post/7251884086536781880
收起阅读 »

前端地位Up!

web
背景 大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C... 然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种...
继续阅读 »

背景


大家好,我是一名前端,我有一天做了一个梦,梦见前端供不应求、梦见大家看到前端就想说:卧槽这个人是前端真牛逼、梦见Javascript突破瓶颈吊打Rust、C...


然后梦醒了,是沉溺于框架、是中台的表格的增删改查、是层出不穷的无效轮子和集成、是无趣的各种小白进阶培训班、是业务形态的致命一击。最终是前端在技术圈子里不如狗的地位。


何至于此?


框架-砒霜or蜜糖?


说说框架


前段跟一个google的算法大佬聊天,他说你们前端很奇怪,现在浏览器已经非常快了,但你们非要搞这样那样的框架,然后去算这个算那个,搞些hooks虚拟dom,完全看不懂在干嘛。


也许我想他说的是对于web的性能层面是对的。


首先虚拟dom也就是中间层如果纯论性能在我看来其实是并不适合现在的时代的,它在这个时代的作用就是作为多端统一以及在真实dom操作前的数据缓冲/计算层...这可能也是这个时代出现了SvelteSolid为代表的此类框架的原因。我倒是希望现在web前端的方向能走向SolidSvelte这种框架的周边社区完善开发。


可是时代的潮流不会随着个人的意志改变,果然时代是分分合合吗,现在ssr为代表的next越来越火(可能有一部分vecel的商业原因),但更重要的一个原因是去改善开发人员的体验(在多端和最佳实践方面),也就是卷颗粒度


以next举例子:我对于next其实是这么理解的,粗颗粒度reactcsr已经进无可进了,改也不好改,那么转化一下方向把,在ssr的领域去降低粒度,就像流式渲染等。


说说人


啊真糟糕,怎么情不自禁就在说框架了,是不是发现我们前端情不自禁在开发or说某个技术的时候就会与框架挂钩。就像是面试我懂某个框架的原理、我学了几个框架诸如此类的,于是我们就从一个框架到另外个框架反反复复的在路上走、打包工具也是一样的(我真的需要这么快的打包工具吗?)。


于是我们就在框架中沉溺了,也许后面会出现一些5年vue工程师10年react工程师,我们整日沉浸在框架之中,日复一日,用着固定的写法(其实我在说Vue,React在这方面会好一些),做着相似的事情,技术的成长变为了我某个框架用得怎么样。


前端工程师or框架工程师


我想啊,前端的潮流很快(娱乐圈),但其实我们要明白一个道理,我用这个东西学这个东西对我有没有收益,对用户体验是不是有很大的提升,对团队开发有没有效率的进步。如果没有的话,不如搞浏览器,当然也可以学学当个PPT天才(纯褒义)或者业务、算法、其他语言等(好堕落啊现在不是PPT就是搞业务),着实没必要把绝大多数时间留着框架上,看多了就会觉得自己很牛逼,然后开摆。


毕竟我们不是框架开发工程师(也就是资源型工具人)、我们是前端开发工程师,我们是面向屏幕开发,我们是人机交互工程师,也就是现在的一个词终端工程师,如果你不能把你的应用在所有屏幕(安卓、Ios、桌面、PC、平板)跑那应该是不合格的。


业务形态


害,说这个之前闲聊一下,我们可以看到一些产品诸如语雀云凤蝶Antd等。蚂蚁体验技术部真的把前端的地位上拉了一截,他们真的很好,可能是未来5年在国内都不会再有这么好的标志性的前端产品,可惜没有一个闭环的商业业务形态,就类似next这种,我在这里不讨论具体的一些原因和后面发生的一些事情。


进入正文,产品和业务形态决定了前端的地位,后端开发通常被认为是应用程序的基础和核心。但其实怎么说那,有的时候其实是因为国内产品思维的局限于和上文提到的沉溺于框架和搜索工具


因为我做过不少国外的产品,有一些很有创造力和创新思维的产品会提出很多天马行空,极具艺术的产品交互效果和体验,在这类产品中其实前端的地位并不算低。


但是在国内就会有这种情况:



  1. 产品不会有这种想法,他的脑子里也是一些国内的那些很普通的竞品,和数据流转逻辑

  2. 前端自己拒绝,一般来说心路历程是这样的,我先看看能不能做 => 去百度掘金搜索 => 搜不到或者框架里没有,好感觉不好做,太复杂了 => 我们换一个普通一点的效果(理由五花八门)。

  3. 大家都是这么做的,那我们这个也这么做吧。


但其实我们自己作为科技触达用户的桥梁,是有能力去推动这个事情的,一个炫酷的配色、合理的交互效果、好看的页面,是可以去给产品去给设计说的,比如我自己有时候会figma或者lottie去自己画一些图和动画效果,去主动纠正设计的颜色和间距。难道产品会拒绝让产品变得更好?设计会拒绝更好看?


说白了,自己不想去做不想去推不会也不想学,觉得很复杂,当然如果实在没有这个土壤果断跑路。


Javascript本身的问题


Javascript吊打RustC估计我是这辈子都看不到了。


Javascript是解释性语言肯定没法跟一些编译语言竞天生就不行,再加上单线程即使有解决方案也就那样。这意味着前端掌握更多语言几乎是一个必要的事情,JavaSwift,Oc这些本来就会用到的不必多说,RustPython选一门掌握也很好。


会得越多你越强,当然我还是建议大家去当PPT天才


前端自信


以后,大伙自信一点,别觉得前端就不如其他技术岗位,地位都是自己争取的,前端优势很大,语言统一、前端立马可见的效果、前端基建相对较小、前端宿主环境统一Docker和容器配置相对统一等。主要是时间,有更多的时间意味着可以做更多的事学更多的东西更多的钱~。


总结


So,改变前端环境从你我做起,你卷一波我卷一波,前端的门槛就提起来了,以后面试的基本要求就是:前端要会Js、Ts、Java、Swift、混合框架、PWA,然后薪资30k起步。


作者:溪饱鱼
来源:juejin.cn/post/7283642910301192244
收起阅读 »

一次性弄清前端上线和生产环境地址

web
💡Tips:不知道小伙伴在前端开发的时候有没有这样的困惑 前端项目打包后,我打包过的静态资源是如何访问后端服务的? 访问后端服务的时候难道不存在跨域的问题吗?如何解决的? 假如我想自己简单修改下部署后的目录该如何去做? 🥲其实不仅仅是你会有这样的疑惑,包...
继续阅读 »

💡Tips:不知道小伙伴在前端开发的时候有没有这样的困惑




  1. 前端项目打包后,我打包过的静态资源是如何访问后端服务的?

  2. 访问后端服务的时候难道不存在跨域的问题吗?如何解决的?

  3. 假如我想自己简单修改下部署后的目录该如何去做?


🥲其实不仅仅是你会有这样的疑惑,包括我在内刚接触前端的时候,由于没有后端的开发经验也会被这些问题所困扰,但是今天我们将一次性弄清楚这个问题,让我们前端的知识体系由点成线,由线成面~


一.明确问题




我们知道,我们平时在开发的时候一般都是使用proxy进行代理的,它的原理是:浏览器会先去访问本地的node服务器,然后node服务器再去代理到你要访问的后端api接口,但是我们可能平时没有node服务器的概念,因为node服务器在webpack中,我们一般是通过下面这种方式来设置



但是我们的项目上线后这种方式就不能用了,(因为Node是我们本地开发的环境,并没有办法在线上使用。)其实,我们一般会通过后端的Nginx代理来解决跨域的问题,但是你知道前端的生产地址配置是什么吗?如何通过Nginx访问后端接口呢?是直接配置的类似于http://www.xxxx.com/api/aaa这样的路径呢?还是直接是一个相对路径/prod?要想搞清楚这些,首先就要了解什么是Nginx


二.什么是Nginx




🐻是一个开源的高性能、轻量级的Web服务器和反向代理服务器,它具有事件驱动,异步非阻塞的架构,被广泛应用于构建高性能的网站,应用程序和服务。


🤡在平时开发中我们经常听到正向代理反向代理这两个名词,那么什么是反向代理,什么是正向代理哪?



  1. 反向代理:服务器的IP是被屏蔽的,也就是说客户端不知道服务器真实的地址是哪个,客户端访问的地址或者域名是访问的Nginx服务器的地址。




  1. 正向代理:和反向代理刚好相反,这个时候服务器不知道真正的客户端是哪个,也就是相当于直接访问服务器的是nginx代理服务器。



三.前端使用Nginx解决跨域




🤗什么是跨域,跨域是指在浏览器的环境下,当一个网页的JavaScript代码通过Ajax Websocket或其他技术发送HTTP请求的目标资源位于不同的域名,端口或者协议下,就会发生跨域。


🐻Nginx如何解决跨域,因为服务器和服务器之间互相请求不发生跨域,所以解决跨域的方法之一就是使用这种方案



  1. 浏览器或者客户端发送请求:http:www.xxx.com:80Nginx服务器对80端口进行监听,Nginx服务器将请求转发到后端真实的服务器地址,这样就实现了代理。




  1. Nginx基本配置项解析


server {
listen 80;
server_name yourdomain.com;

location / { // 无论访问所有路径都返回前端静态资源dist内容
root /path/to/your/frontend;
index index.html;
try_files $uri $uri/ /index.html;
}

location /api/ {
proxy_pass http://backend-server-address/api/; // 真实后端api地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

当在前端代码中使用相对路径/api/users发起请求时候Nginx会将这个请求转发到真实的后端地址,不过你发现了没很多前端项目种生产环境地址都仅仅是一个/api类似这样的相对路径,很少直接是一个绝对路径。


当你请求 yourdomain.com 时,Nginx 会将前端静态资源返回给前端。前端代码中使用的相对路径 /api会基于当前域名 yourdomain.com构建完整的请求 URL。因此,前端代码请求后端地址的完整 URL 将是 yourdomain.com/api/xxx,其中 /xxx表示具体的后端接口路径。


Nginx 的反向代理配置中的 location /api/ 指令将匹配以 /api/ 开头的请求,并将这些请求代理到后端服务器上。因此,当前端代码发起相对路径请求 /api/xxx 时,Nginx 会将请求转发到 yourdomain.com/api/xxx,实现与后端接口的通信。


总结来说,前端代码中的相对路径 /api会根据当前域名构建完整的请求 URL,而 Nginx 的反向代理配置将这些请求转发到后端服务器上的相应路径。这样,前端代码就能够与后端接口进行通信。


四.前端生产环境配置




🥲既然Nginx如何代理的,以及前端打包的路径一般是什么样的我们知道了,那么我们就来唠唠作为一个前端小白该如何快速的完整的构建一个基础项目吧,其实如果基础开发的话,我们往往会使用脚手架,就以Vue开发的话,我们可以使用vuecli来快速构建项目,其实构建完之后你就可以直接npm run build打出的包就可以部署在后端服务器的,这个打出的包的根路径是默认的/,通过上面的Nginx的知识我们应该不难理解。


🤡如果我们要自己想去修改一个路径哪?我们可以在vue.config.js中进行配置,配置如下


module.exports = {
publicPath: process.env.NODE_ENV === 'production' ? '/prod' : '/'
};

👹这样打出的包的静态资源的路径就是下边这样的



🥰如果是直接使用的默认的打包路径就是如下这种



五.总结




🤡最后总结一下,前端上线打包完就是一个静态文件,是一个相对路径,后端会通过Nginx来监听这个资源的请求,当匹配到/就返回静态资源,当匹配到某个/prod就将请求反向代理到后端真实服务器的地址,前端打包的是一个相对路径,Nginx会在前面拼上去具体的域名或者ip,这样就打通了线上前端访问的基本内容。


作者:一溪风月
来源:juejin.cn/post/7291952951048060940
收起阅读 »

同事看到我填写bug原因的表单, 惊呆了, 那么多字段怎么自动填充了?

web
在敏捷开发协作工具中, 公司会要求在关闭bug的时候必须填写一些字段. 其实每次填写的内容都一样的, 要选择bug原因, 是否选择, 填写根因等. 同事在旁边看我关闭了个bug, 看到我的表单是自动填充的, 就问我咋那么方便呢? 实现难度非常简单, 而在日常工...
继续阅读 »

在敏捷开发协作工具中, 公司会要求在关闭bug的时候必须填写一些字段.
其实每次填写的内容都一样的, 要选择bug原因, 是否选择, 填写根因等.


同事在旁边看我关闭了个bug, 看到我的表单是自动填充的, 就问我咋那么方便呢?


实现难度非常简单, 而在日常工作中非常有用, 并且有点小帅. 所以分享给大家.


用什么工具来修改你的网页


我选择的是arc浏览器的boost功能. 在网页上新建一个boost, 点击code, 选到js的tab就可以把编写的js插入到当前host的网页里运行了. 还有辅助功能zag可以帮你抓dom.


对于没有用arc浏览器的大家, 可以写一个chrome extension, 只需要使用content_scripts功能, 可以实现和arc的boost类似的功能: match网址url并且加载一段js. (其实功能是比arc多且灵活的)


修改jira页面


jira是个必须用, 且很多重复操作的网站. 我做了这些修改:


站会看板过滤器顺序调整


每天站会轮到的人的顺序和jira看板上不一致, 导致站会轮下一个人的时候得去找下一个人的位置. 只要获取一下看板过滤器, 调整一下子元素就行了.


const container = document.getElementsByClassName('aui-expander-content ghx-quick-content')[0]
container.children[6].remove()
container.children[10].remove()
container.children[6].after(container.children[2])
container.children[9].after(container.children[1])

看板过滤器多选改单选


jira看板的过滤器是多选的, 所以切换下一个人的时候必须把前一个人取消了, 这样每次都多一次操作.


我们只要给每个过滤器加一个点击事件, 把其他active的过滤器都点击一下就行了.


let child = null
container.onclick = function (e) {
if (child) return
child = e.target
for (let i = 0;i < container.children.length; i++) {
if (container.children[i].children[0] && container.children[i].children[0].classList.contains('ghx-active') && container.children[i].children[0].innerHTML !== child.innerHTML) {
container.children[i].children[0].click()
}
}
child = null
}

关闭bug的时候必须填写原因


公司有个规定, 关闭jira必须填写一些字段. 其实每次填写的内容都一样的, 自动填写可以节省非常多时间.


实现也非常简单, 定时器来寻找指定dom, 然后为这些dom附上指定的值.


const setInputValue = (id, value) => {
if (document.getElementById(id)) {
document.getElementById(id).value = value
}
}

setInterval(() => {
setInputValue('resolution', 10000)
setInputValue('customfield_10903', 12502)
setInputValue('customfield_12301', `故障原因:代码错误
解决方式:修复
影响范围:界面
故障处理人:yo-cwj.com`
)
}, 1000)

获取vue应用的实例来修改界面


老婆画了几套微信表情, 于是我经常登录上去看数据.


但dashboard上信息很少, 需要点到每个表情的详情中才能查看.


通过网络请求, 我看到其实在dashboard的界面, 数据已经请求到了, 于是开始我们的修改.


从dom中寻找vue实例


通过基础的vue知识, 我们知道vue实例是会挂在dom上的.


(vue作者说可以认为他是可用的, 因为vue的devtool也是依赖这个特性的, 那我们一个小脚本是更可用了)


那么哪些dom上有vue实例, 有点像个面试题, 写个简单的脚本就可以找到:


let traverse = (dom) => {
if (dom.__vue__) {
console.log(dom.__vue__._data)
}
for (let i = 0; i < dom.children.length; i++) {
traverse(dom.children[i])
}
}
traverse()

找到目标数据所在的dom, 正式的脚本就这样获取vue实例就可以了.


编写脚本


首先通过vue实例的_data属性获取到数据:


const list = document.querySelector('.page_mod_page_simple.page_home').__vue__.$parent.currentList;

然后把数据贴到对应的dom上:


const emotion_dom = document.querySelector('.table_wrp_emotion_list').querySelector('.table_body');
for (let i = 0; i < emotion_dom.children.length; i++) {
emotion_dom.children[i].children[2].innerHTML += `(${list[i].SendNum} - ${list[i].DataTime})`
}

到这里脚本就写完了, 其他的vue应用其实还可以调用vue实例中的方法获取数据, 或自己获取数据放进vue实例.


解决执行环境的问题


但把这段代码放到boost中会出现拿不到dom的\_\_vue\_\_实例的问题, 因为boost和chrome extension的执行环境并不是浏览器执行环境. 可以通过创建script并执行的方式.


let script = document.createElement('script');
script.textContent = "const list = document.querySelector('.page_mod_page_simple.page_home').__vue__.$parent.currentList;" +
"const emotion_dom = document.querySelector('.table_wrp_emotion_list').querySelector('.table_body');" +
"for (let i = 0; i < emotion_dom.children.length; i++) {" +
" emotion_dom.children[i].children[2].innerHTML += `(${list[i].SendNum} - ${list[i].DataTime})`" +
"}";
setTimeout(() => {
document.documentElement.appendChild(script);
}, 1000)

作者:nujnewnehc
来源:juejin.cn/post/7288628985322307599
收起阅读 »

关于我调部门感觉又重新面试一次这件事,做出知识总结

web
前言 这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识...
继续阅读 »

前言



这篇文章的起因是,当时上周部门调整,要调动到其他部门,最开始我以为就走个流程意思意思,一点准备都没有。没想到,去其他部门还经过了3面,感觉挺正式的,在这期间问的问题有些令我印象深刻,发现了许多不足吧,我是去年毕业的,工作了1年多了,本来以为一些基础知识掌握的差不多了,路还远着,还得学啊!本来那天我还准备一下班就回去玩战地2042,免费周啊!啪的一下兴趣全无,总结一下知识吧,指不定什么时候用上(手动狗头)



节流


节流是指一定时间内只触发一次函数调用,如果在指定时间内多次触发,执行第一次,其他的触发将会被忽略,直到过了设定的时间间隔才触发。


function throttle (fn,delay) {
let timer;
retrun function (...args) {
if(!timer) {
fn(this,args)
timer = settimeout(()=>{
timer=null
},delay)
}
}
}

防抖


防抖是在函数调用后,在指定时间间隔后才触发一次。如果在这个时间间隔内再次触发函数,将重新计时,直到过了设定的时间间隔才会触发最后一次函数调用。


function debounce (fn,delay) {
let timer;
retrun function (...args) {
if(timer) {
clearTimetout(timer)
}
timer = settimeout(()=>{
fn(this,args)
timer=null
},delay)
}
}

数据扁平化


数组


function flatter(arr) {
let result = []
for(let i =0;i<arr.length;i++) {
if(Array.isArray(arr[i]) {
result = result.concat(flatter(arr[i]))
} esle {
result.push(arr[i])
}
}
return result
}

去重


const arr1 = [...new Set(arr)]

const arr1 = arr.map((item,index)=>{
return arr.indexof(item)==index
})

查找字符串中出现最多的字符


当时手写了一半,str.split(item).length应该还要-1才是当前字符出现的次数


  const str = ref<string>('sdfgsgdd');
const fn = (str: string) => {
const arr = Array.from(str);
let maxCount = 0;
let mostFrequentChar = '';
const Nsome = [...new Set(arr)];
Nsome.forEach((item) => {
const count = str.split(item).length - 1;
if (count > maxCount) {
maxCount = count;
mostFrequentChar = item;
}
});
console.log('出现最多的次数,字符', maxCount, mostFrequentChar);
};

闭包及其应用场景


我的回答是:
函数里面嵌套函数,并且内部函数引用了外部函数的变量,就是函数能访问其作用域外的变量


应用场景:
我的回答其中之一是:vueX中状态共享是使用了闭包,节流,防抖
但在 Vuex 中,闭包主要用于封装和共享状态,而不是用于访问和操作外部函数的变量。它使用了闭包的概念,但不是严格意义上的闭包。


1.模块化开发 2.回调函数 3.延迟执行(节流,防抖)


原型&原型链及其应用场景



  1. 原型(Prototype):



  • 每个 JavaScript 对象都有一个原型(prototype),它是一个对象。

  • 对象的原型用于共享属性和方法,当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型可以通过 proto 属性访问,也可以通过 Object.getPrototypeOf() 方法获取。



  1. 原型链(Prototype Chain):



  • 原型链是由对象的原型组成的链式结构,它用于实现对象之间的继承。

  • 当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。

  • 原型链的顶端是 Object.prototype,它是所有对象的原型。


应用场景:



  • 继承:通过原型链,我们可以实现对象之间的继承,一个对象可以继承另一个对象的属性和方法。这样可以避免重复定义和维护相似的代码,提高代码的重用性和可维护性。

  • 共享属性和方法:通过原型链,我们可以将属性和方法定义在原型上,从而实现对象之间的共享。这样可以节省内存空间,避免重复创建相同的属性和方法。

  • 扩展原生对象:通过修改原型链,我们可以扩展 JavaScript 的原生对象,为其添加新的方法和属性。这样可以为原生对象添加自定义的功能,满足特定的需求。


在没有class之前,js是怎么做面向对象的


没答出来,只知道js可以通过class实现面向对象,然后又被问在没有class之前,js是怎么做面向对象的。这也是原型链的应用场景之一,可能是前面原型链的应用场景没说这个,想给我一个提示。


在没有class关键字之前,JavaScript使用原型继承来实现面向对象编程。
javaScript 中的每个对象都有一个原型(prototype),原型是一个对象,它包含了共享的属性和方法。当我们访问一个对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端。


通过原型链,我们可以实现对象之间的继承和共享属性和方法。下面是一个使用原型继承的示例:


// 创建一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在构造函数的原型上定义方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 创建一个 Person 对象
const person1 = new Person('Alice', 25);
// 调用对象的方法
person1.sayHello(); // 输出 "Hello, my name is Alice and I am 25 years old."

node是什么,express是什么,node服务中的中间件是用来干什么的


Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以用于构建高性能的网络应用程序。它允许使用 JavaScript 在服务器端运行代码,而不仅仅局限于在浏览器中运行。


Express 是一个基于 Node.js 的 Web 应用程序框架,它提供了一组简洁而灵活的功能,用于构建 Web 应用程序和 API。Express 提供了路由、中间件、模板引擎等功能,使得构建 Web 应用程序变得更加简单和高效。


中间件的作用是增强和扩展 Node.js 服务的功能,使得处理请求和响应的过程更加灵活和可定制。通过使用中间件,可以将常见的功能模块化,提高代码的可维护性和可重用性。


Express 提供了一些内置的中间件,同时也支持自定义中间件。您可以使用内置的中间件,如 express.json()、express.urlencoded() 来处理请求体的解析,或者编写自己的中间件来满足特定的需求。


你h5怎么处理兼容性


因为是vite+v3项目,vite官方有推荐的插件库,在插件库中有一个关于浏览器兼容支持的插件:@vitejs/plugin-legacy


插件@vitejs/plugin-legacy的作用是为打包后的文件提供传统浏览器兼容性支持



  1. 首先安装插件:npm i @vitejs/plugin-legacy -D

  2. 然后在vite.config.js中配置


import legacyPlugin from '@vitejs/plugin-legacy'
export default defineConfig( {
plugins: [
legacyPlugin({
targets:['chrome 52'], // 需要兼容的目标列表,可以设置多个
additionalLegacyPolyfills:['regenerator-runtime/runtime'] // 面向IE11时需要此插件
})
]
})

rem,px,em这些有什么区别




  1. px(像素):px 是绝对单位,表示屏幕上的一个物理像素点。它是最常用的单位,具有固定的大小,不会根据其他因素而改变。例如,font-size: 16px; 表示字体大小为 16 像素。




  2. rem(根元素字体大小的倍数):rem 是相对单位,相对于根元素(即 元素)的字体大小。如果根元素的字体大小为 16 像素,那么 1rem 就等于 16 像素。如果根元素的字体大小为 20 像素,那么 1rem 就等于 20 像素。通过设置根元素的字体大小,可以方便地调整整个页面的大小。例如,font-size: 1.5rem; 表示字体大小为根元素字体大小的 1.5 倍。




  3. em(相对于父元素字体大小的倍数):em 也是相对单位,相对于父元素的字体大小。如果父元素的字体大小为 16 像素,那么 1em 就等于 16 像素。如果父元素的字体大小为 20 像素,那么 1em 就等于 20 像素。通过设置父元素的字体大小,可以影响其子元素的大小。例如,font-size: 1.2em; 表示字体大小为父元素字体大小的 1.2 倍。




总结来说,px 是绝对单位,不会随其他因素改变;rem 是相对于根元素字体大小的倍数,可以方便地调整整个页面的大小;em 是相对于父元素字体大小的倍数,可以影响子元素的大小。


在实际使用中,可以根据需求选择合适的单位。对于响应式设计,使用 rem 可以方便地调整整个页面的大小;对于局部样式,可以使用 px 或 em 来控制具体的大小。


你工作中遇到了什么坑或者解决什么让自己印象深刻的问题



  • element-plus的el-table表格的二次封装(可以使用tsx)

  • el-table表格的动态合并

  • h5 ios时调起键盘会把整个布局往上推

  • h5调用封装的app JSbrige完成返回

  • 登录的拼图验证

  • h5嵌套在微信小程序中时,由我们h5跳到三方提供的安全验证h5页面,返回时,本地存储的东西没了

  • 利用git hooks+husky+eslint完成前端代码规范和提交规范

  • 银行卡拖拽排序,把排完的顺序返回服务端


上面这些都是我解决了,也不仅仅只有这些,回头想了了下明明自己有很多可以说的,在当时就说了2,3个,然后负责人问我还有吗时,我卡壳了,居然不知道还要说什么。后面我感觉也是根据这个展开来问的


V2混入和V3的hooks,为什么V3要改成hooks的方式


感觉应该是问hooks的好处吧?反正我是答的不太对的,以下是总结:


Vue 3 引入了 Composition API(包括 setup 函数和 hooks),这是一个新的方式来组织和复用代码,与 Vue 2 的混入(mixins)有所不同。

混入在 Vue 2 中被广泛使用,它们允许你在多个组件之间共享行为。然而,混入有一些问题:



  1. 命名冲突:如果混入和组件有相同的方法或数据属性,可能会导致冲突。

  2. 来源不明:当一个组件使用了多个混入时,可能很难确定一个特定的方法或数据属性来自哪个混入。

  3. 复杂性:混入可以包含生命周期钩子、方法、数据等,这可能会增加理解和维护组件的复杂性。

    相比之下,Vue 3 的 Composition API(包括 hooks)提供了一种更灵活、更可控的方式来组织和复用代码:

  4. 更好的逻辑复用和代码组织:你可以将相关的代码(如数据、方法和生命周期钩子)组织在一起,而不是强制按照 Vue 的选项(data、methods、created 等)来组织代码。

  5. 更好的类型推断:对于使用 TypeScript 的项目,Composition API 提供了更好的类型推断。

  6. 更清晰的来源:每个函数和响应式数据的来源都非常明确,因为它们都是从特定的 hook 或 setup 函数返回的。
    因此,虽然 Vue 3 仍然支持混入,但推荐使用 Composition API 来组织和复用代码。


vue3中怎么封装一个自定义指令



  • 通过app.directive()方法注册指令,该方法接受两个参数,第一个参数是指令的名称,第二个参数是一个对象,包含指令的各个生命周期的钩子函数

  • 然后我们就可以在生命周期的钩子函数中定义指令的行为,根据指令的需求,在相应的生命周期钩子函数中编写逻辑代码


什么情况下会使用自定义指令


我的回答是:想要操作dom元素时并且这种类似情况经常出现,如节流和防抖指令,就是给dom加上disabled。按钮权限指令,给当前按钮dom一个显示和隐藏


拖拽排序


拖拽排序的实现原理主要涉及一下几个步骤:



  • 1.监听拖拽事件: 浏览器提供了一系列的拖拽事件,设置draggable="true"



    1. 开始拖拽:当用户开始拖拽一个元素时,会触发 dragstart 事件。在这个事件的处理函数中,我们可以通过 传入的dragstart(e,index) ,中的index来设置当前被拖拽元素的下标。





    1. 拖拽过程:当用户拖拽元素时,会不断触发 dragover 事件。在这个事件的处理函数中,我们需要调用 event.preventDefault 方法来阻止浏览器的默认行为,否则无法触发 拖拽 事件。





    1. 拖拽到另一个元素区域时:当用户拖拽到另一个元素时,会触发 dragenter 事件。在这个事件的处理函数中,我们可以通过 dragente(e,index)方法来获取拖拽到的元素的下标,然后根据获取的两下标来更新列表的排序。




表格动态合并


element-plus表格合并(例如前两列合并) | 耀耀切克闹 (yaoyaoqiekenao.com)


模拟new实例创建的过程



  • 1.创建了新对象并将._proto_指向构造函数.prototype

  • 2.将this指向新创建的对象

  • 3.返回新对象


function newSimulator() {
//1.创建新对象
const obj = new Object()
//2.设置_proto_为构造函数prototype
const constructor = [].shift.call(arguments)
obj._proto_ = constructor.prototype
//3.this指向新对象,也就是改变this的指向
const ret = constructor.apply(obj,arguments)
//4.返回对象或this
return typeof ret = 'object' ? ret : obj
}

冒泡排序


const arr = [1,7,9,2,3,5]
for(let i=0;i<arr.length;i++){
for(let j=0;j<arr.length-i-1;j++){
let a = []
if(arr[j]<arr[j+1]){
a =arr[j]
arr[j]=arr[j+1]
arr[j+1]=a
}
}
}


深拷贝


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


const obj={
arr:[1,2]
}
const clone = JSON.parse(JSON.stringify(obj))

2.使用递归完成深拷贝


这种方式通过递归地遍历原始对象,并对该对象的的属性进行逐一的深拷贝,以创建一个原对象的独立副本。


function deepCloneObject(obj) {
if(obj ===null||typeof object !='object') {
return obj
}
const clone = Array.isArray(obj)?[]:{}
for(let key in obj) {
if(object.prototype.hasOwnProperty.call(obj,key))
clone[key] = deepClone(obj[key])
}
retrun clone
}

函数柯里化


函数柯里化是一种将具有多个参数的函数转换为逐个应用参数的函数序列的技术。通过柯里化,我们可以将一个函数的多个参数转化为一系列嵌套的函数调用。


柯里化的优点是可以创建可复用的函数模板。通过部分应用来生成新的函数。这样可以更灵活地使用函数,并且可以方便的创建更专注于特定功能的函数。
简单的函数柯里化例子:


function add(x) {
return function(y) {
return x + y;
}
}

// 使用柯里化的add函数
var add5 = add(5);
console.log(add5(3)); // 输出 8
console.log(add5(7)); // 输出 12

封装一下


function curry(fn) {
return function curried(...args) {
if(args.length>=fn.length) {
return fn.apply(this,args)
} else {
return function(...moreArgs) {
return curried.apply(this,args.concat(moreArgs))
}
}
}
}

数组API的实现


forEach


Array.portotype.my_forEach = function(callback) {
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)
}
}

map


Array.portotype.my_map = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i],i,this)&&res.push( callback(this[i],i,this))
}
return res
}

filter


Array.portotype.my_filter = function(callback) {
let res= []
for(let i=0;i<this.length;i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}

前端模块化


问:你讲讲前端模块化吧
答:模块化的开发方式可以提高代码复用率,方便进行代码的管理,通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。


问:模块化有哪几种标准?
答:目前流行的js模块化规范有CommonJS、AMD、CMD以及Es6的模块系统


问:ES Modules 和CommonJS的一些区别
答:
1.使用语法层面,CommonJs是通过modules.exports,exports导出,require导入;ES Modules则是export导出,import导入
2.CommonJs是运行时加载模块,EsModules是在静态编译期间就确定模块的依赖
3.EsModulse在编译期间会将所有import提升到顶部,CommonJs不会提升require
4.CommonJs导出是一个值拷贝,会对加载结果进行缓存,一但内部再修改这个值,则不会同步到外部。ESModule是导出的一个引用,内部修改可以同步到外部
5. CommonJs中顶层的this指向这个模块本身,而ESModule中顶层this指向undefined
6. CommonJS加载的是整个模块,将所有的接口全部加载进来,ESModule可以单独加载其中的某个接口


vue的数据双向绑定的原理


vue的响应式原理是采用‘发布-订阅’的设计模式结合object.defineProperty()劫持各个属性的getter和setter,在数据发生变动时通过调用Deo.notity函数发布订阅给观察者watcher,让其更新响应的视图。


虚拟dom


虚拟dom是用来表现真实dom结果的javaScript对象树,是构建在浏览器真实dom上的抽象层,虚拟dom是可以直接在内存中操作的,可以通过diff算法来新旧dom的差异,将最终变化应用到真实dom上


diff算法


diff算法又称虚拟Dom的周界算法,vue的diff算法是通过深度优先、先序遍历的方式进行的,它将前后两个虚拟Dom树进行逐层比较,当找到某一层不一样的节点时,停止下降,然后比较这些节点的子节点,当所有的子节点都完成比较之后,算法会由下至上进行回溯,此过程被称为执行patch操作。在执行patch操作时,Vue对于不同类型的节点的更新方式也不同,对于元素节点可以更新他的属性和子节点;对于文本节点,只能更新它的文本内容;对于每个子节点,如果key值相同,可以进行复用或者重新排序,或者将其他的节点移动到这个位置。


vue中nextTick的理解及作用


使用场景描述:更改一个数据,导致dom元素的width发生了更改,但又要获取这个更新后的dom元素的width,可以用nextTick
vue2 中的nextTick是在下次Dom更新循环之后执行回调函数,并且是作为vue实例的方法调用的


this.$nextTick(() => { // 组件的DOM已经更新完毕,可以进行相应操作 // ... });

Vue 3的nextTick作为一个独立的函数导入,返回一个Promise,并且可以直接传递回调函数作为参数。这些变化使得Vue 3中的nextTick更加灵活和易于使用。


// Vue 3 
import { nextTick } from 'vue';
nextTick(() => { // 在下次DOM更新循环之后执行 });

vue在实例挂载的过程中发生了什么?




  1. 实例化:首先,Vue.js会创建一个新的Vue实例。在这个过程中,Vue.js会设置实例的各种属性和方法,包括数据对象、计算属性、方法、指令等。




  2. 编译模板:Vue.js会将模板编译成渲染函数。模板就是包含Vue特定语法的HTML代码。编译过程中,Vue.js会解析模板中的指令(如v-if、v-for等)和插值表达式(如{{ message }}),并将它们转换为JavaScript代码。




  3. 创建虚拟DOM:渲染函数会被调用,生成一个虚拟DOM树。虚拟DOM是对真实DOM的轻量级表示,它可以更高效地处理DOM的更新。




  4. 挂载:最后,Vue.js会将虚拟DOM渲染为真实DOM,并将其挂载到指定的元素上。这个过程通常在调用vm.$mount()方法或者在实例化Vue时传入el选项后发生。




  5. 更新:当数据变化时,Vue.js会重新执行渲染函数,生成新的虚拟DOM,并与旧的虚拟DOM进行对比(这个过程称为diff)。然后,Vue.js会根据diff结果,以最小的代价更新真实DOM。




这个过程中还会触发一系列的生命周期钩子,如created、mounted等,开发者可以在这些钩子中执行自己的代码。


vue2中data是一个函数而不是对象的原因


data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染


vue2中给对象添加新属性界面页面不刷新


vue2是用Object.defineProperty实现数据响应式,而后面新增的属性,并没有通过Object.defineProperty设置成响应式数据,所以页面没变化,常用解决方式:



  • Vue.set()

  • Object.assign()

  • $forcecUpdated()


Vue SSR的实现原理


vue.js的ssR是一种在服务器上预渲染Vue.js应用程序的技术。



  1. 服务器接收请求:当服务器接收一个请求时,它会创建一个新的Vue实例。

  2. 创建渲染器:使用vue-server-renderer包创建一个渲染器。

  3. 渲染页面:服务器使用渲染器将Vue实例渲染为Html字符串。

  4. 发送响应:服务器将渲染后的Html字符串作为响应发送给客户端。

  5. 客户端接收响应:客户端接收到服务器的响应后,将HTML字符串解析为DOM并显示给用户。

  6. 激活(Hydration): Vue在客户端创建一个新的Vue实例,将其挂载到服务器收到的Dom上


keep-alive的使用


keep-alive的主要作用是缓存路由组件,以提高性能


<router-view v-slot="{ Component }">  
<keep-alive :include="permissionStore.keepAliveName">
<component :is="Component" :key="$route.path" />
</keep-alive>

</router-view>



  1. router-view是 Vue Router 的一个组件,用于渲染当前路由对应的组件。




  2. v-slot="{ Component }" 是一个插槽,用于获取当前路由对应的组件。




  3. keep-alive 是 Vue 的一个内置组件,用于缓存组件,避免重复渲染。




  4. :include="permissionStore.keepAliveName" 是 的一个属性,表示只有名称在 permissionStore.keepAliveName 中的组件会被缓存。




  5. 是一个动态组件,:is="Component" 表示组件的类型由 Component 决定,:key="$route.path" 表示每个路由路径对应一个唯一的组件实例。




Vue项目中有封装axios吗?主要是封装哪方面的?



  • 1.封装前需要和后端协商好一些约定,请求头,状态码,请求时间....

  • 2.设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分

  • 3.移除重复的请求,如果请求在pending中,提示'操作太频繁,请稍后再试'

  • 4.用map结构根据相应状态码处理错误信息

  • 5.请求拦截,若headers中没有token的,移除请求

  • 6.响应拦截器,例如服务端返回的message中有'message',提示'请求超时,请刷新网页重试'

  • 7.请求方法的封装,封装get、post请求方法,使用起来更为方便


css预处理器


css预处理器扩充了css语言,增加了诸如变量、混合(mixin)、函数等功能,让css更易维护、方便。本质上。预处理是css的超集。包含一套自定义的语法及一个解析器,根据这些语法定义自己的样式规则,这些规则最终会通过解析器编译生成对应的css文件。


如何实现上拉加载


image.png
触底公式:


scrollTop + clientHeight >= scrollHeight

简单实现:


    let clientHeight = document.documentElement.clientHeight;//浏览器高度
let scrollHigiht = documnet.body.scrollHeight;//元素内容高度的度量,包括由于溢出导致的视图中不可见内容
let scrollTop = documnet.body.scrollTop; //滚动视窗的高度距离`window`顶部的距离
let distance = 50; //距离视窗还用50的时候,开始触发;

if ((scrollTop + clientHeight) >= (scrollHeight - distance)) {
console.log("开始加载数据");
}

如何实现下拉刷新


关于下拉刷新的原生实现,主要分成三步:



  1. 监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY;

  2. 监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的3. translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值

  3. 监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置。


封装和使用JSBrige




  1. 定义协议:首先,需要定义一种协议,用于约定H5页面与App之间的通信规则。这可以是一组自定义的URL Scheme或JavaScript函数。




  2. 注册事件监听:在H5页面中,通过JavaScript代码注册事件监听器,用于接收来自App的消息或回调。可以使用window.addEventListener或其他类似的方法来监听特定的事件。




  3. 发送消息给App:在H5页面中,通过调用JSBridge提供的方法,将消息发送给App。这可以是通过修改URL Scheme的方式,或者调用App提供的JavaScript接口。




  4. 处理App的消息或回调:在App原生代码中,通过监听URL Scheme或执行JavaScript代码的方式,接收来自H5页面的消息或回调。根据协议约定,处理相应的逻辑或调用相应的功能。




  5. 回调结果给H5页面:在App原生代码中,根据协议约定,将处理结果或回调信息发送回H5页面。可以通过修改URL Scheme的方式,或者调用H5页面中注册的JavaScript回调函数。




个人博客


耀耀切克闹 (yaoyaoqiekenao.com)


gitHub


DarknessZY (zhangyao) (github.com)


作者:耀耀切克闹灬
来源:juejin.cn/post/7291834381315719220
收起阅读 »

分享一个Java小项目:Java实现超级马里奥的冒险之旅

引言超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利...
继续阅读 »

引言

超级马里奥,这个名字对于游戏迷来说一定不陌生。它是一款经典的游戏系列,以一个勇敢的水管工人——马里奥为主角,讲述了他在蘑菇王国中的冒险故事。在这个充满挑战和刺激的游戏中,玩家需要控制马里奥跳跃、躲避障碍物,并与邪恶的蘑菇和食人花敌人战斗,最终抵达城堡的胜利之地。

游戏目标

在这款游戏中,我们的目标是通过控制马里奥完成三个关卡的挑战。每个关卡都有不同的难度和障碍物,玩家需要灵活运用跳跃技巧和反应能力,才能成功通关。同时,消灭普通砖块还可以赚取积分,增加游戏的趣味性和挑战性。

Java实现

为了实现这个经典的游戏,我们将使用Java编程语言进行开发。Java是一种功能强大且广泛使用的编程语言,它具有丰富的图形界面库和游戏开发工具,非常适合用于制作平台跳跃类游戏。

在实现过程中,我们可以使用Java的Swing库来创建游戏的图形界面,包括游戏窗口、角色、背景等元素。同时,我们还需要处理用户的输入操作,例如键盘按键的监听和处理,以便玩家能够控制马里奥的移动和跳跃。

此外,我们还需要考虑游戏的物理引擎和碰撞检测机制,以确保马里奥能够与障碍物和敌人进行正确的交互。这可以通过使用Java的物理引擎库或自己编写相应的算法来实现。

总结

通过使用Java编程语言和相关库,我们可以成功地实现一个经典的超级马里奥小游戏。这将是一个非常有趣和有挑战性的项目,不仅可以锻炼我们的编程技能,还能够让我们体验到游戏开发的乐趣。让我们一起踏上这段冒险之旅吧!

收起阅读 »

Vue 实现 PDF 导出功能

web
笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。 安装依赖 yarn add html2canvas yarn add jspdf 思路 通过网上的一...
继续阅读 »

笨文旨在通过 html2canvas 和 jspdf,先将页面的 html 转成 canvas,再将 canvas 转成 pdf,同时解决了分页截断的问题。


安装依赖


yarn add html2canvas
yarn add jspdf

思路


通过网上的一些教程,初步实现了 html 转 pdf 的功能,将一整个 DOM 元素放进去,虽然可以粗糙实现,但是出现了很多地方被分页截断的情况,这个时候就需要在某一张图片被截断时,将其自动切换到下一页中。


1.拆解父节点


所以第一步:拆解父节点,一行一行细分为很多子节点,循环遍历这些子节点,累加这些子节点的高度,如果超出了 a4 纸(210*297)的高度,则分页。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function oneNodeMultipleChildren(title, node) {
html2Canvas(node, {
scale: 2, // 清晰度
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4"); // 以mm为单位
let position = 0; // 页面偏移
let contentWidth = canvas.width; // 转换成canvas后的宽度
let contentHeight = canvas.height; // 转换成canvas后的高度
let proportion = 210 / node.offsetWidth; // html缩小至a4纸大小时的比例
let currentHeight = 0; // 当前高度
let imgWidth = 210; // canvas缩小至a4纸大小时的宽度
let imgHeight = (210 / contentWidth) * contentHeight; // canvas缩小至a4纸大小时的高度
let pageData = canvas.toDataURL("image/jpeg", 1.0); // 将canvas转成图片

for (let j = 0; j < node.children.length; j++) {
let childHeight = (node.children[j].offsetHeight + 8) * proportion; // 页面中每行的间距 margin-bottom: 8px

if (currentHeight + childHeight > 297) {
// 如果加上这个子节点后内容超出a4纸高度,就从当前位置开始分页
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight; // 这一页放了多少高度的内容,下一页就从这个高度开始偏移
if (position >= -contentHeight) {
PDF.addPage(); // 添加新pdf页
}
currentHeight = childHeight; // 下一页第一个元素的高度
} else {
currentHeight += childHeight;
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight); // 最后一页
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
// PDF.rect参数分别为:起始横坐标、起始纵坐标、绘制宽度、绘制高度、填充色
}

2.合并父节点


经过上述步骤,一个父节点多个子节点,并且每个子节点独占一行的布局可以实现分页,那要是有很多父节点呢?就需要遍历每个父节点,合并所有子节点,进行分页截断。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);

for (let i = 0; i < nodes.length; i++) {
// 根据传入的父节点数量进行循环,遍历父节点,合并所有子节点
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

3.每行多个元素


这个时候新的问题出现了,由于页面布局为 flex 布局,不同缩放下,每行的元素数量会出现变化。所以我们获取第一个子元素与 a4 纸宽度关系,如果为 n 倍,那后面 n-1 个子元素的高度不进行累加。


warning 注意
这里只解决了一行 n 个子元素宽度相等,且近似等于 a4 纸宽度的 1/n 的情况。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, content, nodes) {
html2Canvas(content, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / content.offsetWidth;
let currentHeight = 0;
let imgWidth = 200;
let imgHeight = (200 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < nodes.length; i++) {
for (let j = 0; j < nodes[i].children.length; j++) {
let childHeight = (nodes[i].children[j].offsetHeight + 8) * proportion;
let childWidth = nodes[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth);
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight); // 在当前pdf页添加图片
PDF.setFillColor(255, 255, 255); // 遮挡的颜色
PDF.rect(0, currentHeight, 210, Math.ceil(297 - currentHeight), "F"); // 添加空白遮挡
}

4.添加左右间距和页眉页脚


为了美化 pdf 布局,上下左右留白,就需要添加左右间距和页眉页脚:减少 html 缩小至 a4 纸大小时的比例和 canvas 缩小至 a4 纸大小时宽高,增加偏移量,并对页眉页脚进行空白遮挡。


import html2Canvas from "html2canvas";
import JsPDF from "jspdf";

export function exportAssetPdf(title, id) {
let content = document.querySelector(`#${id}`);
let first = content.firstElementChild.firstElementChild;
let second = content.lastElementChild;
oneNodeMultipleChildren(title, content, [first, second]);
}

export function oneNodeMultipleChildren(title, fNode, sNode) {
html2Canvas(fNode, {
scale: 2,
}).then(function (canvas) {
let PDF = new JsPDF("", "mm", "a4");
let position = 0;
let contentWidth = canvas.width;
let contentHeight = canvas.height;
let proportion = 200 / fNode.offsetWidth; // 减少10mm
let currentHeight = 0;
let imgWidth = 200; // 减少10mm
let imgHeight = (200 / contentWidth) * contentHeight; // 减少10mm
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let sameIndex = 1;
let widthX = 1;

for (let i = 0; i < sNode.length; i++) {
for (let j = 0; j < sNode[i].children.length; j++) {
let childHeight = (sNode[i].children[j].offsetHeight + 8) * proportion;
let childWidth = sNode[i].children[j].offsetWidth * proportion;
if (sameIndex === 1) {
widthX = Math.round(200 / childWidth); // 减少10mm
}
if (sameIndex < widthX) {
childHeight = 0;
sameIndex++;
} else {
sameIndex = 1;
}

if (currentHeight + childHeight > 287) {
// 减小10mm
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
position -= currentHeight;
if (position >= -contentHeight) {
PDF.addPage();
}
currentHeight = childHeight;
} else {
currentHeight += childHeight;
}
}
}
addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight);
PDF.save(title + ".pdf");
});
}

function addImage(PDF, pageData, position, imgWidth, imgHeight, currentHeight) {
PDF.addImage(pageData, "JPEG", 5, position + 5, imgWidth, imgHeight); // 增加偏移量
PDF.setFillColor(255, 255, 255);
PDF.rect(0, 0, 210, 4, "F"); // 添加页眉遮挡
PDF.rect(0, currentHeight + 5, 210, Math.ceil(292 - currentHeight), "F"); // 添加页脚遮挡
}

成果展示


不同缩放下导出 PDF 对比:


每行一个子元素


每行多个子元素


作者:虚惊一场
来源:juejin.cn/post/7291142504123875364
收起阅读 »

如何创建五彩纸屑效果

web
前言 很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。 本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果 简单使用 我们可以通过 npm 安装或...
继续阅读 »

前言


很多网站会在一些按钮上面加上不同的动画效果,这些动感的效果能够更加容易的创建具有视觉吸引力的用户界面。


本文将介绍一个小巧的 JavaScript 库,它能够用非常短的时间以及简短的代码量创建我们想要的五彩纸屑效果


简单使用


我们可以通过 npm 安装或从 cdn 导入两种方式来使用这个库。


这里我采用的是导入的方式。


在你导入完成这个库之后,我们需要一个按钮


<button onclick="myClick()">button</button>

最简单的特效只需要在点击函数当中调用 confetti 函数即可


function myClick () {
confetti()
}

动画4.gif


细节配置参数


通过传入 options 属性,我们可以在特效上自定义很多我们需要的部分,下面是部分配置属性的作用,后面我们会挑出部分属性来展示一下效果:


属性名作用
particleCount飞出的纸屑的数量,默认 50
angle飞出的纸屑的角度 90 是向上,默认 90
spread飞出的纸屑偏离中心的角度,默认 45
startVelocity飞出的纸屑的初始速度,默认 45
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
gravity重力,默认 1
decay飞出的纸屑的减速度,范围 0-1 之间,默认 0.9
flat是否关闭纸屑的翻转,默认 false
ticks纸屑消失速度,默认 200
origin对象,设置发射纸屑的原点,有 x y 两个参数,取值都是 0 - 1 对应上下边缘,默认 0.5 0.5
colors数组:十六进制格式的颜色字符串数组
shapes数组:纸屑的形状
scalar每个五彩纸屑粒子的比例,默认 1
zIndex纸屑的z-index,默认 100
disableForReducedMotion禁用五彩纸屑,默认 false

根据上面的参数,你可以很容易的定义自己想要的效果,下面我们随意定义部分例子:


<body>
  <button onclick="myClick1()">左倾斜</button>
  <button onclick="myClick2()">全是黑色</button>
  <button onclick="myClick3()">数量很多多多</button>
 <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.0/dist/confetti.browser.min.js"></script>
  <script>
    function myClick1 () {
      confetti({
        angle: 135,
      });
    }
    function myClick2 () {
      confetti({
        colors: ['#000000']
      });
    }
    function myClick3 () {
      confetti({
        particleCount: 500,
      });
    }
 
</script>

</body>

动画4 1.gif


详细定义纸屑形状


在上面的配置当中我们已经可以定义纸屑的大部分配置,其中 shapes 可以用于定义纸屑的形状,官方为我们预留了三个形状 'square', 'circle', 'star' 分别对应 方形,圆形,星星,这个字段传入的是一个数组,这个数组中元素的数量,决定了这个形状在所有纸屑中的比例,比如说你要是传入了 ['square', 'circle', 'star'] 那么三种形状就都是占比三分之一。


除了通过官方预留的形状,我们还可以通过两个函数来进行形状自定义,分别是 confetti.shapeFromPathconfetti.shapeFromText


confetti.shapeFromPath


这个函数可以传入一个对象,对象中存在一个 pathkey,这个就决定了创建出来的形状长什么样子


下面的代码可以创建一个三角形的纸屑


var triangle = confetti.shapeFromPath({ path: 'M0 10 L5 0 L10 10z' });

confetti({
shapes: [triangle],
});

confetti.shapeFromText


这个函数同样传入一个对象,对象存在 textscalartext 可以传入任何文字,甚至是一些文字表情也可以使用, scalar 配合 optionsscalar 使用,两个相差过大会导致字体变得很模糊。


下面的代码你就可以创建一个字符串纸屑


var scalar = 2;
var pineapple = confetti.shapeFromText({ text: '🍍🍍🍍', scalar });
confetti({
shapes : [pineapple],
scalar,
});

逻辑事件


纸屑在生成的时候,我们可以会需要一些事件,比如说在我们想要的时候清除掉屏幕上还未消失的纸屑,又或者在一次纸屑彻底消失的时候执行某些逻辑。


消除纸屑


我们可以通过调用 confetti.reset(); 来消除屏幕上的纸屑


监听纸屑消失事件


在我们调用 confetti() 的时候会返回一个 promise 对象,这个对象将会在纸屑完全消失的时候回调。


自定义纸屑产生的位置


在上面的例子当中,我们可能会发现,纸屑都只能在屏幕的正中心产生,这是因为 options 里的 origin 属性的默认值,我们可以通过一些方式来自定义这个产生的位置


动态设置 origin


我们可以通过动态的来设置这个值来做到自定义产生的位置。


在点击事件当中,我们能够拿到当前的点击对象,通过这个 event 对象以及 window 上获取到可视区域的宽高,我们不难算出当前按钮相对于左右的位置。


const windowHeight =
  window.innerHeight ||
  document.documentElement.clientHeight ||
  document.body.clientHeight;
// 获取浏览器高度
const windowWidth =
  window.innerWidth ||
  document.documentElement.clientWidth ||
  document.body.clientWidth;
// 获取浏览器宽度
const origin = {
x: event.pageX/windowWidth,
y: event.pageY/windowHeight,
}
// 获取比例

自定义 canvas


官方支持我们创建自定义画布的  confetti 实例,这样创建的  confetti 不会超出定义的这个画布的范围,这在某些时候可能会起到很重要的作用。


并且官方强调了,一个画布最好只和一个  confetti 实例做关联,所以说当我们创建多个  confetti 实例的时候,就也需要创建多个画布。


因为是自定义添加的画布,所以我们需要在不需要的时候手动去删除这个画布,避免产生多余的元素。


var myCanvas = document.createElement('canvas');
var container = document.querySelector('.container')
container.appendChild(myCanvas)
var myConfetti = confetti.create(myCanvas).then(()=>{
container.removeChild(myCanvas)
});

然后设定位置的操作我这里就不多做了,只需要定义这个 canvas 的生成位置就好了。


示例代码



作者:14332223
来源:juejin.cn/post/7290769553572397056
收起阅读 »