注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

React框架部署实战:打造高效现代化的Web应用

web
React框架部署实战 在前端开发领域,React框架因其高效的组件化架构和出色的性能而备受开发者喜爱。然而,要让React应用真正展现其魅力,一个关键的环节就是部署。 本文将带领你深入了解React框架的部署过程,为你的Web应用提供一个高效、稳定的运行...
继续阅读 »

React框架部署实战



在前端开发领域,React框架因其高效的组件化架构和出色的性能而备受开发者喜爱。然而,要让React应用真正展现其魅力,一个关键的环节就是部署。



本文将带领你深入了解React框架的部署过程,为你的Web应用提供一个高效、稳定的运行环境。


📍 准备工作:配置环境


在开始部署React应用之前,确保你的开发环境已经配置完善。首先,安装Node.js和npm,这是React应用的基础依赖。随后,使用以下命令安装Create React App,这是一个官方推荐的React应用脚手架,简化了项目的初始化和配置过程。


npx create-react-app my-react-app

📍 生产环境构建:优化性能


React应用在开发过程中使用的是开发环境配置,而在部署到生产环境时,我们需要进行一些优化以提升性能。使用以下命令进行生产环境构建:


npm run build

这将生成一个build文件夹,包含了优化后的、用于生产环境的代码。这一步骤将帮助你减小应用的体积,提高加载速度,使其更适合在生产环境中运行。


📍 选择合适的服务器:保障稳定性


选择一个合适的服务器对于React应用的部署至关重要。你可以选择使用传统的Web服务器,比如Nginx或Apache,也可以考虑使用专门为React应用设计的服务器,如Express或Firebase Hosting。确保服务器能够正确配置,以支持React路由和处理单页面应用的特殊需求。


📍 域名与SSL:提升安全性


为你的React应用配置域名,并考虑启用SSL证书以提高安全性。在大多数情况下,你可以通过云服务提供商或第三方SSL证书颁发机构获取免费的SSL证书。使用HTTPS协议不仅有助于提升安全性,还有可能对搜索引擎排名产生积极影响。


📍 自动化部署:提高效率


自动化部署是一个高效的实践,可以减少人为错误并提高开发团队的工作效率。你可以考虑使用持续集成/持续部署(CI/CD)工具,如Jenkins、Travis CI或GitHub Actions,将代码的自动构建和部署流程整合到你的开发工作流中。


📍 监控与日志:保障可维护性


部署完成后,监控和日志记录是必不可少的环节。使用工具如Sentry、New Relic等,实时监测应用的性能和错误,及时发现并解决潜在的问题。同时,记录应用的日志可以帮助你追踪和分析用户行为,为后续的优化提供有力支持。


📍 版本管理:确保灵活性


在生产环境中,灵活地管理React应用的版本是至关重要的。使用工具如Docker,可以打包你的应用及其依赖,确保在不同环境中的一致性。结合版本控制工具如Git,能够轻松地进行回滚和发布新版本。


📍 总结


通过本文的步骤,你可以更好地了解如何部署React应用,确保其在生产环境中高效、稳定地运行。部署不仅仅是一个技术问题,更是一个关乎用户体验和团队效率的重要环节。通过合理的部署流程,你的React应用将能够展现出其设计之美和高效性能,为用户提供卓越的使用体验。


作者:知识浅谈
来源:juejin.cn/post/7301976895913689125
收起阅读 »

历时一个月,6年前端降薪上岸了

web
6年前端,11.15号,拿到了这一个月以来的第一份offer,降薪,教育行业,正如之前在文章中所说的,我就是头铁。 这篇文章就水一下我这差不多一个月的面试旅程吧。 我是在7.14号左右从一家培训公司毕业,然后拿了N+1,我就回家了,一直待到国庆之后才回的北京。...
继续阅读 »

6年前端,11.15号,拿到了这一个月以来的第一份offer,降薪,教育行业,正如之前在文章中所说的,我就是头铁。
这篇文章就水一下我这差不多一个月的面试旅程吧。
我是在7.14号左右从一家培训公司毕业,然后拿了N+1,我就回家了,一直待到国庆之后才回的北京。


第一次面试


第一个面试是我的朋友给我内推的公司,智联招聘,时间大概是在10.08,当时我啥也没有准备,裸面,最后还是被pass了。面试官当时问了我这么几个问题:




  1. 说说你自己想做的技术方向(主要是针对招聘岗位看看你是否合适,他们想找一个做监控平台和业务的)

  2. 你认为什么是好的代码

  3. 对程序设计原则有了解吗

  4. 对于质量保障,前端职能该做什么(前端质检工作)

  5. 小程序从聊天记录选择文件,上传失败,你觉得应该如何去解决和排查这个问题

  6. node是如何处理高并发的



从整个面试来看,涉及到前端技术相关的问题其实也没几个,第1个问题,面试官就已经给我挖坑了,我当时巴拉巴拉说了一堆关于前端基建和自己想做前端底层相关的事情,很明显,他需要的不是这样的人,因为他们现在做的还是以业务为主,还有监控相关的事情正在推进,需要有人着手去做。然后就是第4个问题,这个问题就可以考察出我对于前端整个开发生命周期的质量保证了解多少,从而判断出我是不是适合这个岗位,很明显的,我的回答并不满意。第2个和第3个问题,我确实以前没有考虑过,后来我查了一下,如果对理论知识比较关注的话,这些东西应该都需要了解的。由此考察出我对这方面知识的欠缺。第5个问题,是之前他们在开发遇到的一个问题,然后问我如何解决这个问题,我当时脑子短路了,一直在寻思这个问题到底出在哪里,而他想知道的是,当遇到问题的时候如何排查解决的一个思路,很明显,我走错路了。整个面试过程也很清晰,考察三个点,第一点是不是匹配他们的招聘岗位,第二点是不是理论基础比较扎实,第三点解决问题的能力。


如何复习


经过这次面试,我把上面几个问题总结了,在后面的面试中2、3、4经常被问到,我也是对答如流。由此开始了我的面试旅程。那么接下来就讲讲我是如何来复习和做总结的。


首先,复习基础知识(八股文),一般就是css、js、ts、vue、react、webpack浏览器相关面试题,如何去找面试题呢?我最常用的方式是掘金去看优质的文章,这是最浪费时间也是最能能掌握的一种方式,可能很多面试题手册写的就是针对面试题的那么几句话,并没有讲的很清楚,知其然,不知其所以然,问的详细一点就会懵了。如果需要面试题手册的可以加我WX:xiumubai01。然后就是就是总结,我选择的是xmind写个思维导图,罗列大纲。这样看起来就非常清晰,哪些知识点我复习过了,对应的每个知识点有哪些细节,我都会做出标记,这样,在我的脑海中就形成了一套面试的话术,我面试的时候也会根据这样一个结构去讲,一来我心中有思路,二来面试官听的不迷糊。
大概差不多像下面的这样的:
image.png


以某个知识点为例:
image.png
我把整个浏览器相关的知识点都总结到了一起,这样既可以方便复习,也能让我系统的掌握相关浏览器的知识点。
想要获取思维导图的道友们可以可以加我WX:xiumubai01


关于项目问题,我也提以下。非常重要。一般面试官会根据你写的简历上面的项目问你,让你讲讲做的最拿手的一个项目,做了哪些事?遇到过什么问题?怎么解决的?当你讲项目的时候,面试官就能直观的感受到你平时工作中到底几斤几两。我的项目中写了一个业务(剧本直播),微前端平台,低代码平台。比如我微前端平台面试官会问到的问题:你如何选型?为什么选这个框架(qiankun)?那qiankun当中你使用的时候遇到过什么问题?你觉得这个框架有哪些不足的地方?它是如何实现js沙箱隔离的?(变态一点的直接让你实现一个)。所以项目你一定要吃透。当你讲的过程中,人家会提问各种场景,问你如何解决的,如果你提前没有想到,那只能当场退役。


除了自己复习相关的知识点以外,面试总结特别重要。我养成的习惯是每次面试完了立马总结问到的问题,然后进行复盘,这次面试哪里回答的不好,面试官想要考察的能力是什么?这次回答不好的问题下次我能不能应对?按照我的经验,每次总结完之后大概率后面的面试都会问到同样的问题。以下是我总结的:
image.png


如果你面试完了记不住面试官问的问题,我的做法是掏出纸和笔,在面试官提问的时候,记下问题关键词,这样方便面试完了以后回忆。


如何coding


加下来就是关于coding题,很多人都会恐惧,我也是,没有思路,前一天刚刷的代码第二天就忘了。没有他法,脑子笨,只能靠理解加强记忆,那最好的办法就是你要把这道题目吃透,研究明白,思路清晰,其实不管任何代码,你先得知道这道题怎么解啊,然后才能用代码实现。我这里总结了我面试以来手写的一些coding题,放在github了。大家可以打开链接自取。



github: github.com/xiumubai/co…


gitee: gitee.com/xiumubai/co…



如何消除焦虑


在面试的过程中,难免会有学不进去,面试遭受打击,长时间没有offer心中气垒等等情况的发生。尤其是当你面试3-4轮以后,眼见要拿到offer了,最后杳无音信,或者人事直接通知你pass了,这时候是最打击人的。整个人就像被掏空了一样。


面对以上这种情况,我后来想了一种排解的方式,拉了个微信群,把最近找工作的道友们一起拉进去,大家互相鼓励,讨论遇到的面试题,最近的市场行情,这样可以分解一下压力,转移注意力,有可能有的人比你更惨。有想进群的朋友可以关注一下我的公众号「白哥学前端」,进群,群里有我最新的一些面试题和xmind文件分享。


当然保持积极的心情也很重要,不要做一些沉迷动作(比如打游戏,晚上不睡,早上不起),把自己的时间调整成上班的时间,这个时间点你就做跟学习有关的事情。


这个过程是很痛苦和煎熬的,相信大家都能坚持住,最后,祝大家都能顺利找到满意的工作。


作者:白哥学前端
来源:juejin.cn/post/7301909438540267531
收起阅读 »

前端版本过低引导弹窗方案分享

web
作者:费昀锋 背景 作为 TOB 的业务方,我们偶尔会收到一些如下图所示的反馈。 作为 PC 页面为主的业务方,大多数用户在一天的工作中,可能都不太会刷新或者重新打开我们的页面,导致我们在下午或者白天发布的前端版本,往往需要到几个小时甚至第二天,才能覆盖到...
继续阅读 »

作者:费昀锋



背景


作为 TOB 的业务方,我们偶尔会收到一些如下图所示的反馈。



作为 PC 页面为主的业务方,大多数用户在一天的工作中,可能都不太会刷新或者重新打开我们的页面,导致我们在下午或者白天发布的前端版本,往往需要到几个小时甚至第二天,才能覆盖到 98% 以上的用户。



我们统计了 bscm 平台 5 次下午 2-3 点左右发布的版本,在发布后每个时间段内老版本用户的占比情况。选择这个时间点发布的原因是这个时间点基本是平台用户的上班时间,是最有可能出现用户已经打开了页面同时我们在发布新代码的场景的,比较具有代表性。按平台用户六七点下班来看,我们可以看到还有将近 6% 的用户在当天是会一直访问老版本的前端代码的,按照 bscm 平台 1w+的 uv 来看,约有 600 多人会可能遇到前端版本过低导致的使用问题。


方案


弹窗内容



弹窗的触发条件


首先介绍两个概念,本地版本号和云端版本号。本地版本号是用户请求到的前端页面的代码版本号,是用户访问页面时决定;云端版本号可以理解为最新前端版本号,它是每次开发者发布前端代码时决定的。



判断触发条件的时机


有了弹窗的触发条件,我们还需要去决定什么时候判断弹窗是否满足触发的条件,上面也提到了,出现这类问题的场景多见于用户在使用过程中,开发者进行了前端代码发布,那我们主要可以有两个类型的时机去进行触发条件的判断。




  1. 前端代码去感知什么时候有新版本的代码发布了,去进行条件判断(消息推送)




  2. 前端在一定的条件下主动去判断触发条件(轮询,请求后端接口时,一些中频前端事件的监听)




我们对这些时机在更新是否及时,判断次数多少、实现成本高低等维度进行一个对比。



⭐️ 越多表示这个维度得分越高




根据表格可以看到 websocket 消息推送和前端事件监听这两种方案综合来看是更合适一些的,但是前端事件监听其实它的劣势在实际运用场景中会被弱化(一天的上线数量有限,请求次数一天不会多太多次),但是实现成本远低于 websocket,所以无疑是实际落地场景中比较理想的选择。



根据 can i use 的结果我们也可以发现 visibilitychange 事件也基本符合我们目前 B 端页面对于 PC 浏览器的要求。


版本号的生成


本地版本号


本地版本号是用户访问时决定的,那无疑页面的 html 文件就是这个版本号存在的最佳载体,我们可以在打包时通过 plugin 给 html 文件注入一个版本号。


云端版本号


云端版本号的选择则有很多方式了,数据库、cdn 等等都可以满足需求。不过考虑到实现成本和泳道的情况,想了一下两个思路一个是打包的同时生成一个 version.json 文件,配一个路由去访问;另一个是直接访问对应的 html 代码,解析出注入的版本号,二者各自有适合的场景。


微前端的适配


我们现在的大多数项目都包含了主应用和子应用,那其实不管是子应用的更新还是主应用的更新都应该有相关的提示,而且相互独立,但同时又需要保证弹窗不同时出现。


想要沿用之前的方案其实只需要解决三个问题。



  1. 主子应用的本地版本号标识需要有区分,因为 html 文件只有一个,需要能在 html 文件中区分出哪个应用的版本是什么,这个我们只需在 plugin 中注入标识即可解决。

  2. 云端版本号请求时也要请求对应的云端版本号,这个目前采用的方案是主应用去请求唯一的 version.json 文件,因为主应用路由是唯一的,子应用则去请求最新的 html 资源文件,解析出云端版本号。

  3. 不重复弹窗我们只需要在展示弹窗前,多加一个是否已经有弹窗展示的判断即可了。


具体实现


版本号的写入和读取



监听时机和频控逻辑


正如前文提到的,本身版本发布不是一个高频事件,但是监听事件的频次有时候可能过高了,不希望频繁的去进行触发条件判断。同时如果出现一天内多次发布的场景,也不希望这个弹窗对于用户有过多的打扰,所以需要去添加一个频控逻辑。



具体代码


plugin


/* eslint-disable */
import { CoraWebpackPlugin, WebpackCompiler } from '@ies/eden-web-build';
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');

interface IVersion {
name?: string; // 编译完的文件夹名称
subName?: string; // 子应用的名称,主应用可以不传
}

export class VersionPlugin implements CoraWebpackPlugin {
readonly name = 'versionPlugin'; // 插件必须要有一个名字,这个名字不能和已有插件冲突
private _version: number;
private _name: string;
private _subName: string;
constructor(params: IVersion) {
this._version = new Date().getTime();
this._name = params?.name || 'build';
this._subName = params?.subName || ''
}
apply(compiler: WebpackCompiler): void {
compiler.hooks.afterCompile.tap('versionPlugin', () => {
try {
const filePath = path.resolve(`./${this._name}/template/version.json`);
fs.writeFile(filePath, JSON.stringify({ version: this._version }), (err: any) => {
if (err) {
console.log('@@@err', err);
}
});
const htmlPath = path.resolve(`./${this._name}/template/index.html`);
const data = fs.readFileSync(htmlPath);
const $ = cheerio.load(data);
$('body').append(`
${this._subName}versionTag" style="display: none">${this._version}
`
);
fs.writeFile(htmlPath, $.html(), (err: any) => {
if (err) {
console.log('@@@htmlerr', err);
}
});
} catch (err) {
console.log(err);
}
});
}
}

弹窗组件


import React, { useEffect } from 'react';

import { Modal } from '@ecom/auxo';
import axios from 'axios';
import moment from 'moment';

export interface IProps {
isSub?: boolean; // 是否为子应用
subName?: string; // 子应用名称
resourceUrl?: string; // 子应用的资源url
}

export type IType = 'visibilitychange' | 'popstate' | 'init';

export default React.memo<IProps>(props => {
const { isSub = false, subName = '', resourceUrl = '' } = props || {};

const cb = (latestVersion: number | undefined, currentVersion: number | undefined, type: IType) => {
try {
// 版本落后,提示可以刷新页面
if (latestVersion && currentVersion && latestVersion > currentVersion) {
// 提醒过了就设置一个更新提示过期时间,一天内不需要再提示了,弹窗过期时间暂时全局只需要一个!!
localStorage.setItem(`versionUpdateExpireTime`, moment().endOf('day').format('x'));
if (!document.getElementById('versionModalTitle')) {
Modal.confirm({
title: <div id="versionModalTitle">版本更新提示div>,
content:
'您已经长时间未使用此页面,在此期间平台有过更新,如您此时在页面中没有填写相关信息等操作,请点击刷新页面使用最新版本!',
okText: <div data-text={`前端版本升级引导-立即更新 ${type}`}>刷新页面div>,
cancelText: <div data-text={`前端版本升级引导-我知道了 ${type}`}>我知道了div>,
onCancel: () => {
console.log('fe-version-watcher INFO: 未更新~');
},
onOk: () => {
location.reload();
},
});
}
}
// 不管版本是否落后,半小时内都不需要去重新请求判断
localStorage.setItem(`versionInfoExpireTime`, String(new Date().getTime() + 1000 * 60 * 30));
} catch {}
};

const formatVersion = (text?: string) => (text ? Number(text) : undefined);

useEffect(() => {
try {
const fn = function (type: IType) {
if (document.visibilityState === 'visible') {
/**
*
@desc 为了防止打扰,版本更新每个应用一天只提示一次 所以过期时间设为当天23:59:59,没过期则直接return
*/

if (Number(localStorage.getItem(`versionUpdateExpireTime`) || 0) >= new Date().getTime()) {
return;
}
/**
*
@desc 不需要每次切换页面都去判断资源,每次从服务器获取到的版本信息,给半个小时的缓存时间,需要区分子应用
*/

if (Number(localStorage.getItem(`versionInfoExpireTime`) || 0) > new Date().getTime()) {
return;
}

if (!isSub) {
/**
*
@desc 主应用使用version.json文件来获取最新的版本号
*/

const dom = document.getElementById('versionTag');
const currentVersion = formatVersion(dom?.innerText);
axios.get(`/version?timestamp=${new Date().getTime()}`).then(res => {
const latestVersion = res?.data?.version;
cb(latestVersion, currentVersion, type);
});
} else {
/**
*
@desc 子应用使用最新html中的innerText来获取最新版本号
*/

if (resourceUrl) {
const dom = document.getElementById(`${subName}versionTag`);
const currentVersion = dom?.innerText ? Number(dom?.innerText) : undefined;
axios.get(resourceUrl).then(res => {
/** ignore_security_alert */
try {
const html = res.data;
const doc = new DOMParser().parseFromString(html, 'text/html');
const latestVersion = formatVersion(doc.getElementById(`${subName}versionTag`)?.innerText);
cb(latestVersion, currentVersion, type);
} catch {}
});
}
}
}
};
const visibleFn = () => {
fn('visibilitychange');
};
const routerFn = () => {
fn('popstate');
};
if (isSub) {
// 子应用可能会有缓存,初始化的时候先判断一次
fn('init');
}
document.addEventListener('visibilitychange', visibleFn);
window.addEventListener('popstate', routerFn);
return () => {
document.removeEventListener('visibilitychange', visibleFn);
window.removeEventListener('popstate', routerFn);
};
} catch {}
}, []);

return <div />;
});

如何接入


主应用版本



  1. 安装依赖


npm i @ecom/fe-version-watcher-plugin # 安装plugin 
npm i @ecom/logistics-supply-chain-fe-version-watcher # 安装引导弹窗


  1. 引入 versionPlugin,自动生成 version.json + html 文件中自动注入


import { VersionPlugin } from '@ecom/fe-version-watcher-plugin';

// 有些项目打包后template文件夹下的名字不是build而是build_cn
// 可以根据自己项目的实际情况传入{name: build_cn}

{
...,
plugins: [
...,
[VersionPlugin, {}],
]
}


  1. 引入版本引导弹窗


import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';

<FeVersionWatcher />


  1. goofy 新增路由配置,/version 指向 version.json 文件 (或者其它方式可以使得/version 的路由指向该 version.json 文件)



预告


采用 version.json 的方案,引入 FersionWatcher 组件就不再需要任何参数,目前主应用只支持这种模式。未来也将参考子应用,主应用支持读取 html 中版本标识的能力,将配置路由的工作改成组件 props 传入资源 url,开发者可以根据实际情况自行选择。


子应用版本



  1. 安装依赖


npm i @ecom/fe-version-watcher-plugin # 安装plugin
npm i @ecom/logistics-supply-chain-fe-version-watcher # 安装引导弹窗


  1. 引入 versionPlugin, html 文件中自动注入版本号,需要子应用标识参数(必填)


import { VersionPlugin } from '@ecom/fe-version-watcher-plugin';

// 有些项目打包后template文件夹下的名字不是build而是build_cn
// 可以根据自己项目的实际情况传入{name: build_cn}

{
...,
plugins: [
...,
[VersionPlugin, {subName: 'general-supplier', name: 'build_cn'}],
]
}


  1. 引入版本引导弹窗(subName 和 plugin 中保持一致,resourceUrl 为配置的子应用路由)


import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';

// subName需要和plugin的参数保持一致,resourceUrl为子应用资源的路径(子引用goofy上配置的路由)
<FeVersionWatcher isSub subName="general-supplier" resourceUrl="/webApp/general-supplier" />

resourceUrl一般就是goofy上配置的路由设置,,如果不同平台有区分,可以动态传入。



如何调试/效果展示


发布成功后,可以根据如下步骤测试:




  1. 删除 localstorage 中相关的 value





  2. 修改 html 中的 version,改成一个比较小的数值即可





  3. 切换路由,或者隐藏/打开页面,出现弹窗




收益统计



同样我们截取了 4 次该平台 2-3 点发布的版本情况,可以看到老版本用户的 uv 占比有着明显的下降。



上线至今共计提示 10 万+用户,帮助约 5 万人次及时更新了前端代码。


作者:字节前端
来源:juejin.cn/post/7301530293377843235
收起阅读 »

防抖是回城,节流是攻击

web
前言 防抖和节流是前端开发中常用的函数优化手段,它们可以限制函数的执行频率,提升性能和用户体验。在我们的日常开发中,经常会遇到一些需要对函数进行优化的场景,比如防止表单的重复提交。 一 防抖与节流的区别 我们简单描述下它们的作用 防抖:它限制函数在一段连续的时...
继续阅读 »

前言


防抖节流是前端开发中常用的函数优化手段,它们可以限制函数的执行频率,提升性能和用户体验。在我们的日常开发中,经常会遇到一些需要对函数进行优化的场景,比如防止表单的重复提交。


一 防抖与节流的区别


我们简单描述下它们的作用


防抖:它限制函数在一段连续的时间内只执行一次。当连续触发某个事件时,只有在事件停止触发一段时间后,才会执行函数。


节流:它按照固定的时间间隔执行函数。当连续触发某个事件时,每隔一段时间执行一次函数。


简而言之,防抖是在事件停止触发后延迟执行函数,而节流是按照固定的时间间隔执行函数。


因为防抖节流的作用和应用场景基本相同,也就导致它们容易被人混淆,不好记忆。


之前在网上看到了一个例子非常的有趣形象,和大家分享下。


王者荣耀大家都玩过吧,里面的英雄都有一个攻击间隔,当我们连续的点击普通攻击的时候,英雄的攻速并不会随着我们点击的越快而更快的攻击。这个其实就是节流,英雄会按照自身攻速的系数执行攻击,我们点的再快也没用。


而防抖在王者荣耀中就是回城,在游戏中经常会遇到连续回城嘲讽对手的玩家,它们每点击一次回城,后一次的回城都会打断前一次的回城,只有最后一次点击的回城会被触发,从而保证回城只执行一次,这就是防抖的概念。


自从我看到这个例子后,节流和防抖就再也没记混过了。作为一个8年王者老玩家。


下面是防抖和节流的实现


防抖的实现与使用


防抖的应用场景:



  1. 输入框搜索:当用户在搜索框中输入关键字时,使用防抖可以避免频繁发送搜索请求,而是在用户停止输入一段时间后才发送请求,减轻服务器压力。

  2. 窗口调整:当窗口大小调整时,使用防抖可以避免频繁地触发重排和重绘操作,提高页面性能。

  3. 按钮点击:当用户点击按钮时,使用防抖可以避免用户多次点击造成的多次提交或重复操作。


immediate参数用于控制防抖函数是否立即触发,true立即触发,false过delay时间后触发。


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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按钮</button>
<script>
function debounce(func, delay, immediate) {
let timer;
return function () {
let context = this;
let args = arguments;

if (timer) {
clearTimeout(timer)
}

if (immediate && !timer) {
func.apply(context, args)
}

timer = setTimeout(() => {
timer = null
if (!immediate) {
func.apply(context, args)
}
}, delay);
}
}

// 创建一个被防抖的函数
const debouncedFunction = debounce(() => {
console.log("Debounced function executed.");
}, 1000, false);

document.getElementById('btn').addEventListener('click', debouncedFunction)

</script>
</body>

</html>

节流的实现与使用


节流的应用场景:



  1. 页面滚动:当页面滚动时,使用节流可以限制滚动事件的触发频率,减少事件处理的次数,提高页面的响应性能。

  2. 鼠标移动:当鼠标在某个元素上移动时,使用节流可以减少事件处理的次数,避免过于频繁的操作。


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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<button id="btn">按钮</button>
<script>

function throttle(func, delay, immediate) {
let timer;
return function () {
const context = this
const args = arguments

if (timer) {
return
}

if (!timer && immediate) {
func.apply(context, args)
}

timer = setTimeout(() => {
timer = null

if (!immediate) {
func.apply(context, args)
}
}, delay);
}
}

// 创建一个被节流的函数
const throttledFunction = throttle(() => {
console.log("throttled function executed.");
}, 1000, false);

document.getElementById('btn').addEventListener('click',throttledFunction)
</script>
</body>

</html>

结尾


看完本文章后,希望能够加深大家对防抖和节流的印象,分清二者的区别。


作者:欲买炸鸡同载可乐
来源:juejin.cn/post/7301244391153467431
收起阅读 »

一文带你如何优雅地封装小程序蓝牙模块

web
一. 前言。 蓝牙功能在我们日常软件中的使用率还是蛮高的----譬如各类共享单车/电单车。正因此,我们开发中接触蓝牙功能也是日渐增长。对于很多从未开发过蓝牙功能的童鞋来说,当PM小姐姐扔过来一个蓝牙协议要你接入时,简直一头雾水(我是谁?我在哪?)。只能一翻度娘...
继续阅读 »

一. 前言。


蓝牙功能在我们日常软件中的使用率还是蛮高的----譬如各类共享单车/电单车。正因此,我们开发中接触蓝牙功能也是日渐增长。对于很多从未开发过蓝牙功能的童鞋来说,当PM小姐姐扔过来一个蓝牙协议要你接入时,简直一头雾水(我是谁?我在哪?)。只能一翻度娘和AI,可是网上文章大多水准参差不齐,技术五花八门,没法真正地让你从无到有掌握蓝牙功能/协议对接。


二. 说明。


本文就基于uni-app框架结合微信和支付宝小程序为例,来讲述蓝牙功能在各类型小程序中的整体开发流程和如何“优雅”高效的封装蓝牙功能模块。本文使用到的主要技术栈和环境有:



  • uni-app

  • JavaScript

  • AES加解密

  • 微信小程序

  • 支付宝小程序


三. 蓝牙流程图。


正所谓“知己知彼,百战不殆”,所以在讲述蓝牙模块如何在小程序中开发和封装之前,我们先要了解蓝牙功能模块是如何在小程序中“走向”的,各API是如何交互通讯的。为了让大家看得清楚,学的明白----这里简明扼要地梳理了一份蓝牙核心API流程图(去除了非必要的逻辑走向,只展示了实际开发中最重要的步骤和交互)。



  • uni-app: 蓝牙API

  • 微信小程序:蓝牙API

  • 支付宝小程序:蓝牙API

  • 核心API流程图(注:每家厂商的小程序API大同小异,uni-app的基本通用,具体明细详见各厂商开发文档):


小程序蓝牙流程.png


四. 蓝牙协议。


了解完开发所需的API后,就需要根据实际开发场景中所对接的硬件和其厂家提供的蓝牙对接协议来结合上述的API来编写代码了。每家厂商的蓝牙协议是不一样的,不过“万变不离其宗”。只要知道其中的规则,真正看懂一家,那换其他家的也是可以看懂的。本文以下述协议(蓝牙寻车+蓝牙开锁)为例解释下。


1. 寻车:



  • 协议内容:


image.png



  • 解读:


根据上述图文的描述,我们可以知道想要开启蓝牙锁,那么必须先通过寻车蓝牙指令(7B5B01610060 或 7B5B01610160)写入,然后根据蓝牙响应的信息功能体和错误码判断响应是否正确,如正确,那么就拿到此时的随机数,后根据协议规定对该随机数做相应的处理,最后将处理后得到的结果用于组装开锁的蓝牙写入指令。



  • 案例代码:


image.png
image.png


2. 开锁:



  • 协议内容:


image.png



  • 解读:


根据上述图文的描述,我们可以知道开锁的写入指令是需要自己组装的,组装规则为:7B5B(数据头) 1B(信息体长度) 62(信息功能) 00(秘钥索引)018106053735(补1位0的电话号码)4B大端的时间戳 寻车拿到的随机码补8位0后经AES加密组合得到的16B数据 00(校验码);所以开锁写入的数据就是这种(案例:7B5B1B6200018106053735XXXXXXXXXXXXXXXXXXXX)。响应的话,也是根据信息功能体和错误码来判断开锁失败(9201)还是成功(9200)。



  • 案例代码:


image.png


五.代码编写。


这里为了提高蓝牙模块的代码耦合度,我们会把业务层和蓝牙模块层分离出来----也就是会把蓝牙整体流程交互封装成一个蓝牙模块js,然后根据业务形态,在各个业务层面上通过传参的形式来区分每个组件的蓝牙功能。


1. 业务层:



  • 核心代码:


//引入封装好的蓝牙功能JS模块核心方法函数
import { operateBluetoothYws } from '@/utils/bluetoothYws.js';

//调用蓝牙功能
blueTooth() {
//初始化蓝牙模块,所有的蓝牙API都需要在此步成功后才能调用
uni.openBluetoothAdapter({
success(res) {
console.log('初始化蓝牙成功res', res);
let mac = 'FF8956DEDA29';
let key = 'oYQMt8LFavXZR6sB';
operateBluetoothYws('open', mac, key, flag => {
if (flag) {
console.log('flag存在回调函数--蓝牙成功,可以执行后续步骤了', flag);
} else {
console.log('flag不存在回调函数--蓝牙成功,可以执行后续步骤了', flag);
}
})
},
fail(err) {
console.log('初始化蓝牙失败err', err);
}
})
},


  • 解读:


这里是我们具体业务层需要的写法,一开始就是引入我们封装好的蓝牙JS模块核心方法函数(operateBluetoothYws),然后启用uni.openBluetoothAdapter这个蓝牙功能启动前提,成功后在其success内执行operateBluetoothYws方法,此时的参数根据实际开发业务和相对应的蓝牙协议而定(这里以指令参数、设备编号和AES加密秘钥为例),实际中每个mac和key是数据库一一匹配的,我们按后端童鞋提供的接口获取即可(这里为了直观直接写死)。


2. 蓝牙模块层:



  • 核心代码:


let CryptoJS = require('./crypto-js.min.js'); //引入AES加密
let callBack = null; //回调函数,用于与业务层交互
let curOrder; //指令(开锁还是关锁后取锁的状态)
let curMac; //当前扫码的车辆编码对应的设备mac
let curKey; //当前扫码的车辆编码对应的秘钥secret(用于AES加密)
let curDeviceId; //当前扫码的车辆编码对应的设备的 id
let curServiceId; //蓝牙服务 uuid,需要使用 getBLEDeviceServices 获取
let curCharacteristicRead; //当前设备读的uuid值
let curCharacteristicWrite; //当前设备写的uuid值


//蓝牙调用核心方法(order: 指令;mac:车辆编码;key:秘钥secret;cb:回调)
function operateBluetoothYws(order,mac, key, cb) {
curOrder = order;
curMac = mac;
curKey = key;
callBack = cb
searchBluetooth();
}

//第一步(uni.startBluetoothDevicesDiscovery(OBJECT),开始搜寻附近的蓝牙外围设备。)
function searchBluetooth() {
uni.startBluetoothDevicesDiscovery({
services: ['00000001-0000-1000-8000-00805F9B34FB', '00000002-0000-1000-8000-00805F9B34FB'],
success(res) {
console.log('第一步蓝牙startBluetoothDevicesDiscovery搜索成功res', res)
watchBluetoothFound();
},
fail(err) {
console.log('第一步蓝牙startBluetoothDevicesDiscovery搜索失败err', err)
callBack && callBack(false)
}
})
}

//第二步(uni.onBluetoothDeviceFound(CALLBACK),监听寻找到新设备的事件。)
function watchBluetoothFound() {
uni.onBluetoothDeviceFound(function(res) {
curDeviceId = res.devices.filter(i => i.localName.includes(curMac))[0].deviceId;
stopSearchBluetooth()
connectBluetooth()
})
}

//第三步(uni.createBLEConnection(OBJECT),连接低功耗蓝牙设备。)
function connectBluetooth() {
if (curDeviceId.length > 0) {
// #ifdef MP-WEIXIN
uni.createBLEConnection({
deviceId: curDeviceId,
timeout: 5000,
success: (res) => {
console.log('第三步通过deviceId连接蓝牙设备成功res', res);
getBluetoothServers()
},
fail: (err) => {
console.log('第三步通过deviceId连接蓝牙设备失败err', err);
callBack && callBack(false)
}
});
// #endif
// #ifdef MP-ALIPAY
my.connectBLEDevice({
deviceId: curDeviceId,
timeout: 5000,
success: (res) => {
console.log('第三步通过deviceId连接蓝牙设备成功res', res);
getBluetoothServers()
},
fail: (err) => {
console.log('第三步通过deviceId连接蓝牙设备失败err', err);
callBack && callBack(false)
}
});
// #endif
}
}

//第四步(uni.stopBluetoothDevicesDiscovery(OBJECT),停止搜寻附近的蓝牙外围设备。)
function stopSearchBluetooth() {
uni.stopBluetoothDevicesDiscovery({
success: (res) => {
console.log('第四步停止搜寻附近的蓝牙外围设备成功res', res);
},
fail: (err) => {
console.log('第四步停止搜寻附近的蓝牙外围设备失败err', err);
}
})
}

//第五步(uni.getBLEDeviceServices(OBJECT),获取蓝牙设备所有服务(service)。)
function getBluetoothServers() {
uni.getBLEDeviceServices({
deviceId: curDeviceId,
success(res) {
console.log('第五步获取蓝牙设备所有服务成功res', res);
//这里取res.services中的哪个,这是硬件产商配置好的,不同产商不同,具体看对接协议
if (res.services && res.services.length > 1) {
curServiceId = res.services[1].uuid
getBluetoothCharacteristics()
}
},
fail(err) {
console.log('第五步获取蓝牙设备所有服务失败err', err);
callBack && callBack(false)
}
})
}

//第六步(uni.getBLEDeviceCharacteristics(OBJECT),获取蓝牙设备某个服务中所有特征值(characteristic)。)
function getBluetoothCharacteristics() {
// #ifdef MP-WEIXIN
uni.getBLEDeviceCharacteristics({
deviceId: curDeviceId,
serviceId: curServiceId,
success: (res) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值成功res', res);
curCharacteristicWrite = res.characteristics.filter(item => item && item.uuid.includes('0002'))[
0].uuid
curCharacteristicRead = res.characteristics.filter(item => item && item.uuid.includes('0003'))[
0].uuid
notifyBluetoothCharacteristicValueChange()
},
fail: (err) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值失败err', err);
callBack && callBack(false)
}
});
// #endif
// #ifdef MP-ALIPAY
my.getBLEDeviceCharacteristics({
deviceId: curDeviceId,
serviceId: curServiceId,
success: (res) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值成功res', res);
curCharacteristicWrite = res.characteristics.filter(item => item && item.characteristicId.includes('0002'))[
0].characteristicId
curCharacteristicRead = res.characteristics.filter(item => item && item.characteristicId.includes('0003'))[
0].characteristicId
notifyBluetoothCharacteristicValueChange()
},
fail: (err) => {
console.log('第六步获取蓝牙设备某个服务中所有特征值失败err', err);
callBack && callBack(false)
}
});
// #endif
}

//第七步(uni.notifyBLECharacteristicValueChange(OBJECT),启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值。)
function notifyBluetoothCharacteristicValueChange() {
uni.notifyBLECharacteristicValueChange({
deviceId: curDeviceId,
serviceId: curServiceId,
characteristicId: curCharacteristicRead,
state: true,
success(res) {
console.log('第七步启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值成功res', res);
if(curOrder == 'open'){
//寻车指令
getRandomCode();
}else if(curOrder == 'close'){
//查看锁状态指令
getLockStatus();
}else{

}
//第八步(监听)(uni.onBLECharacteristicValueChange(CALLBACK),监听低功耗蓝牙设备的特征值变化事件。),含下发指令后的上行回应接受
//这里会一直监听设备上行,所以日志等需清除
uni.onBLECharacteristicValueChange((characteristic) => {
// #ifdef MP-WEIXIN
//完整的蓝牙回应数据
let ciphertext = ab2hex(characteristic.value);
//蓝牙回应数据的信息功能体和错误码
let curFeature = ab2hex(characteristic.value).slice(6, 10);
//蓝牙回应数据的错误码
let errCode = ab2hex(characteristic.value).slice(8, 10);
// #endif

// #ifdef MP-ALIPAY
//完整的蓝牙回应数据
let ciphertext = characteristic.value;
//蓝牙回应数据的信息功能体和错误码
let curFeature = characteristic.value.slice(6, 10);
//蓝牙回应数据的错误码
let errCode = characteristic.value.slice(8, 10);
// #endif
if (curFeature.startsWith('91')) { //寻车响应,拿到随机码
//用于给开锁的随机码
getUnlockData(ciphertext)
} else if (curFeature.startsWith('9200')) { //开锁响应(成功)
callBack && callBack(true)
} else if (curFeature.startsWith('98')) { //关锁后APP主动读取后的响应,查看是否已关锁
if (curFeature == '9801') { //关锁成功
callBack && callBack(true)
} else { //关锁失败
callBack && callBack(false)
}
} else {

}
})
},
fail(err) {
console.log('第七步启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值失败err', err);
callBack && callBack(false)
}
})
}

// ArrayBuffer转16进度字符串示例
function ab2hex(buffer) {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function (bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('')
}

//寻车指令,用于拿到开锁所需的随机码
function getRandomCode() {
let str = '7B5B01610060';
writeBLE(str)
}

//开锁指令,获取到开锁所需的数据
function getUnlockData(ciphertext) {
if (ciphertext.length > 16) { //确保寻车后蓝牙响应内容有用于开锁的随机码
//开锁头(固定值)
let headData = '7B5B1B6200';
//用户手机号
let userPhone = '018106053735';
//4B大端秒级时间戳
let timestamp = convertLettersToUpperCase(decimalToHex(getSecondsTimestamp()));
//随机码 + 8个‘0’
let randomVal = convertToLower(ciphertext.slice(16, 24)) + '00000000';
//AES加密后的前32位密文
let aesResult = aesEncrypt(randomVal,curKey).slice(0,32)
//校验码
let checkCode = '00';
//最后用于发指令的内容
let result = headData + userPhone + timestamp + aesResult + checkCode;
writeBLE(result)
} else {
getRandomCode();
}
}

//查看锁状态指令,用于验证用户手工关锁后查询是否真的已关锁
function getLockStatus() {
let str = '7B5B006868';
writeBLE(str)
}

//AES的ECB方式加密,以hex格式(转大写)输出;参数一:明文数据,参数二:秘钥
function aesEncrypt(encryptString, key) {
let aeskey = CryptoJS.enc.Utf8.parse(key);
let aesData = CryptoJS.enc.Utf8.parse(encryptString);
let encrypted = CryptoJS.AES.encrypt(aesData, aeskey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
//将base64格式转为hex格式并转换成大写
let password = encrypted.ciphertext.toString().toUpperCase()
return password;
}

//处理写入数据
function writeBLE(str) {
//如果大于20个字节则分包发送
if (str.length > 20) {
let curArr = splitString(str,20);
// #ifdef MP-WEIXIN
curArr.map(i => writeBLECharacter(hexStringToArrayBuffer(i)))
// #endif

// #ifdef MP-ALIPAY
curArr.map(i => writeBLECharacter(i))
// #endif
} else {
// #ifdef MP-WEIXIN
writeBLECharacter(hexStringToArrayBuffer(str));
// #endif

// #ifdef MP-ALIPAY
writeBLECharacter(str);
// #endif
}
}

//第八步(写入)(uni.writeBLECharacteristicValue(OBJECT),向低功耗蓝牙设备特征值中写入二进制数据。)
function writeBLECharacter(bufferValue){
uni.writeBLECharacteristicValue({
deviceId: curDeviceId,
serviceId: curServiceId,
characteristicId: curCharacteristicWrite,
value: bufferValue,
success(res) {
console.log('第八步(写入)向低功耗蓝牙设备特征值中写入二进制数据成功res', res);
},
fail(err) {
console.log('第八步(写入)向低功耗蓝牙设备特征值中写入二进制数据失败err', err);
callBack && callBack(false)
}
})
}

//将字符串以每length位分割为数组
function splitString(str, length) {
var result = [];
var index = 0;
while (index < str.length) {
result.push(str.substring(index, index + length));
index += length;
}
return result;
}

//字符转ArrayBuffer
function hexStringToArrayBuffer(str) {
// 将16进制转化为ArrayBuffer
return new Uint8Array(str.match(/[\da-f]{2}/gi).map(function(h) {
return parseInt(h, 16)
})).buffer
}

//对字符串中的英文大写转小写
function convertToLower(str) {
var result = '';
for (var i = 0; i < str.length; i++) {
if (/[a-zA-Z]/.test(str[i])) {
result += str[i].toLowerCase();
} else {
result += str[i];
}
}
return result;
}

//对字符串中的英文小写转大写
function convertLettersToUpperCase(str) {
var result = str.toUpperCase(); // 将字符串中的字母转换为大写
return result;
}

//获取秒级时间戳(十进制)
function getSecondsTimestamp() {
var timestamp = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位为秒)
return timestamp;
}

//将十进制时间戳转成十六进制
function decimalToHex(timestamp) {
var hex = timestamp.toString(16); // 将十进制时间戳转换为十六进制字符串
return hex;
}


//抛出蓝牙核心方法
module.exports = {
operateBluetoothYws
};


  • 解读:


这里的步骤和上面流程图中的步骤走向是一样的,不过里面的详情,笔者还是想每一步都拆开来对着实际案例讲述为好,详见下文(这里主要是为了照顾小白,大佬勿怪)。


六. 蓝牙模块层各步骤详解。



  1. 蓝牙功能调用核心方法的定义和导出(operateBluetoothYws)


operateBluetoothYws 这里没啥好特别的,就是将业务层传进来的参数做个中转处理,为后续步骤的api所调用,详见上文代码及其注释。



  1. 第一步(uni.startBluetoothDevicesDiscovery(OBJECT))


uni.startBluetoothDevicesDiscovery 这里主要注意的是services这个参数,这个参数会由硬件厂家提供,一般在其提供的蓝牙协议文档中会标注,作用是要搜索的蓝牙设备主 service 的 uuid 列表。某些蓝牙设备会广播自己的主 service 的 uuid。如果设置此参数,则只搜索广播包有对应 uuid 的主服务的蓝牙设备。建议主要通过该参数过滤掉周边不需要处理的其他蓝牙设备。


image.png



  1. 第二步(uni.onBluetoothDeviceFound(CALLBACK))


uni.onBluetoothDeviceFound 这一步用来确定目标设备id,即后续步骤所需的参数deviceId。 这里主要注意的是其回调函数的devices结果,我们要根据厂家或其提供的蓝牙对接协议规定和我们业务层传进来的mac来匹配筛选目标设备(因为这里会监听到第一步同样的uuid的每一台设备)(这里我就一台设备测试,所以回调函数的devices结果数组中内容就一个;然后之所以用localName.includes(curMac) 来匹配目标设备,这是根据厂商协议文档来做的,每家厂商和每种设备不一样,这里要按实际情况处理,不过万变不离其宗)。


image.png



  1. 第三步(uni.createBLEConnection(OBJECT))


uni.createBLEConnection 这里没啥特别的,主要就是用到第二步中得到的deviceId去连接低功耗蓝牙目标设备。需要注意的是这里支付宝小程序的API不一致,为my.connectBLEDevice


image.png



  1. 第四步(uni.stopBluetoothDevicesDiscovery(OBJECT))


uni.stopBluetoothDevicesDiscovery 这一步主要是为了节省电量和资源,在第三步连接目标设备成功后给停止搜寻附近的蓝牙外围设备。


image.png



  1. 第五步(uni.getBLEDeviceServices(OBJECT))


uni.getBLEDeviceServices 这里通过第二步中得到的deviceId用来获取蓝牙目标设备的所有服务并确定后续步骤所需用的蓝牙服务uuid(serviceId)。这里取res.services中的哪个,这是硬件厂商定好的,不同厂商不同,具体看对接协议(案例中的是固定放在第2个,所以是通过curServiceId = res.services[1].uuid得到)。


image.png



  1. 第六步(uni.getBLEDeviceCharacteristics(OBJECT))


uni.getBLEDeviceCharacteristics 这里通过第二步获取的目标设备IddeviceId和第五步获取的蓝牙服务IdserviceId来得到目标设备的写的uuid读的uuid。这里取characteristics的哪一个也是要根据厂商和其提供的蓝牙协议文档来决定的(案例以笔者这的协议文档为主,所以是这样获取的:curCharacteristicWrite = res.characteristics.filter(item => item && item.uuid.includes('0002'))[0].uuid 和 curCharacteristicRead = res.characteristics.filter(item => item && item.uuid.includes('0003'))[0].uuid)。需要注意的是这里支付宝小程序的API不一致,为my.getBLEDeviceCharacteristics,其res返回值也不一样,curCharacteristicWrite = res.characteristics.filter(item => item && item.characteristicId.includes('0002'))[0].characteristicId 和 curCharacteristicRead = res.characteristics.filter(item => item && item.characteristicId.includes('0003'))[0].characteristicId。


image.png



  1. 第七步(uni.notifyBLECharacteristicValueChange(OBJECT))


uni.notifyBLECharacteristicValueChange 这里就是开启低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值。可以在其的success内执行一些写入操作执行uni.onBLECharacteristicValueChange(CALLBACK)来监听低功耗蓝牙设备的特征值变化事件了。


image.png



  1. 第八步(写入)(uni.writeBLECharacteristicValue(OBJECT))


uni.writeBLECharacteristicValue 这里特别要注意的是参数value必须为二进制值(这里需用注意的是支付宝小程序的参数value可以不为二进制值,可直接传入,详见支付宝小程序开发文档);并且单次写入不得超过20字节,超过了需分段写入


image.png


image.png



  1. 第八步(监听)(uni.onBLECharacteristicValueChange(CALLBACK))


uni.onBLECharacteristicValueChange 这里需根据实际开发的业务场景对CALLBACK 返回参数转16进度字符串后自行处理(支付宝小程序如果写入时未转换,那么这里读取时也不需要转换)(本文以寻车--开锁--检测锁状态为例)。


image.png


七. 总结。


以上就是本文的所有内容,主要分为2部分----业务层蓝牙模块层(封装)。业务层只需要关注目标设备和其对应的密钥(不同厂家和设备不同);蓝牙模块层主要是按蓝牙各API拿到以下四要素并按流程图一步步执行即可。



  1. 蓝牙设备Id:deviceId

  2. 蓝牙服务uuid:serviceId

  3. 蓝牙写操作的uuid

  4. 蓝牙读操作的uuid


至此,如何在小程序中优雅地封装蓝牙模块并高效使用就已经完结了,当然本文只是以最简而易学的案例来讲述蓝牙模块开发,大多只处理了success的后续,至于fail后续可以根据大家实际业务处理。相信看到这,你已经对小程序开发蓝牙功能,对接各种蓝牙协议已经有一定的认识了,再也不虚PM小姐姐的蓝牙需求了。完结撒花~ 码文不易,还请各位大佬三连鼓励(如发现错别之处,还请联系笔者修正)。


作者:三月暖阳
来源:juejin.cn/post/7300929241948422179
收起阅读 »

浏览器的秘密

web
 浏览器架构 1 浏览器的历史 单进程与多进程浏览器 在2007年之前,市面上浏览器都是单进程的,所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。 最新的Chrome浏览器包括:1个浏览器(B...
继续阅读 »

 

浏览器架构


1 浏览器的历史


单进程与多进程浏览器



在2007年之前,市面上浏览器都是单进程的,所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。20210415092040.png




最新的Chrome浏览器包括:1个浏览器(Browser)主进程1个 GPU 进程1个网络(NetWork)进程多个渲染进程多个插件进程
20210415092356.png




  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU进程。其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。

  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响


2 JavaScript的单线程模型



  • 因为JS是与用户互动,以及操作DOM。如果JavaScript是多线程的,会带来很多复杂的问题,假如 JavaScript有A和B两个线程,A线程在DOM节点上添加了内容,B线程删除了这个节点,应该是哪个为准呢? 所以,为了避免复杂性,所以设计成了单线程。

  • H5提供了多线程的方案:Web Worker, 他允许主线程创建worker线程,分配任务给worker进程处理,但是worker线程完全受到主线程控制,也不能操作DOM,没有改变JS的单线程本质。


3 Chrome 打开一个页面需要启动多少进程?分别有哪些进程?


打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。



  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。


4 渲染机制


1. 浏览器如何渲染网页


概述:浏览器渲染一共有五步



  1. 处理 HTML 并构建 DOM 树。

  2. 处理 CSS构建 CSSOM 树。

  3. 将 DOM 与 CSSOM 合并成一个渲染树。

  4. 根据渲染树来布局,计算每个节点的位置。

  5. 调用 GPU 绘制,合成图层,显示在屏幕上



第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染



具体如下图过程如下图所示


img


img


渲染



  • 网页生成的时候,至少会渲染一次

  • 在用户访问的过程中,还会不断重新渲染



重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)




  • 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢

  • 当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且CSS也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM


知识点1



  • 由于<script>标签是阻塞解析的,将脚本放在网页尾部会加速代码渲染。

  • deferasync属性也能有助于加载外部脚本。

  • defer使得脚本会在dom完整构建之后执行;

  • async标签使得脚本只有在完全available才执行,并且是以非阻塞的方式进行的


知识点2: 重绘(Repaint)和回流(Reflow)



重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大




  • 重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘

  • 回流是布局或者几何属性需要改变就称为回流。



回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流



以下几个动作可能会导致性能问题



  • 改变 window 大小

  • 改变字体

  • 添加或删除样式

  • 文字改变

  • 定位或者浮动

  • 盒模型


很多人不知道的是,重绘和回流其实和 Event loop 有关



  • 当 Event loop 执行完Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。

  • 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms才会触发一次,并且自带节流功能。

  • 判断是否触发了 media query

  • 更新动画并且发送事件

  • 判断是否有全屏操作事件

  • 执行 requestAnimationFrame 回调

  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好

  • 更新界面

  • 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调


常见的引起重绘的属性



  • color

  • border-style

  • visibility

  • background

  • text-decoration

  • background-image

  • background-position

  • background-repeat

  • outline-color

  • outline

  • outline-style

  • border-radius

  • outline-width

  • box-shadow

  • background-size


3.4 常见引起回流属性和方法



任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子




  • 添加或者删除可见的DOM元素;

  • 元素尺寸改变——边距、填充、边框、宽度和高度

  • 内容变化,比如用户在input框中输入文字

  • 浏览器窗口尺寸改变——resize事件发生时

  • 计算 offsetWidth 和 offsetHeight 属性

  • 设置 style 属性的值


回流影响的范围



由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种




  • 全局范围:从根节点html开始对整个渲染树进行重新布局。

  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局


全局范围回流


<body>
<div class="hello">
<h4>hello</h4>
<p><strong>Name:</strong>BDing</p>
<h5>male</h5>
<ol>
<li>coding</li>
<li>loving</li>
</ol>
</div>
</body>


p节点上发生reflow时,hellobody也会重新渲染,甚至h5ol都会收到影响



局部范围回流



用局部布局来解释这种现象:把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界



3.5 减少重绘和回流



使用 translate 替代 top



<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector('.test').style.top = '100px'
}, 1000)
</script>


@程序员poetry: 代码已经复制到剪贴板



  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

  • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改100次,然后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量


for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}


@程序员poetry: 代码已经复制到剪贴板



  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

  • CSS选择符从右往左匹配查找,避免 DOM深度过深

  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层。


img


img


作者:vast11
来源:juejin.cn/post/7298893187065659430
收起阅读 »

前端基建有哪些?大小公司的偏重啥?🤨

web
前言 兄弟们可能有的感受 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。 感受一:天天写业务代码,感觉没啥技术含量,没沉淀,成长太慢,架构那边挺有趣的。 感受二:天天写内部工具,觉得没啥提升,感觉要废。 感受三:对一些框架的...
继续阅读 »

前言




兄弟们可能有的感受



  • 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。

  • 感受一:天天写业务代码,感觉没啥技术含量,没沉淀,成长太慢,架构那边挺有趣的。

  • 感受二:天天写内部工具,觉得没啥提升,感觉要废。

  • 感受三:对一些框架的原理、源码、工具 研究较少,无法突破评级,成为leader。


上面的感受都是一些兄弟们的典型感受(也包括我自己)。这时候不妨可以考虑一下,了解了解前端的基础建设,进而 搭建起一个坚实的底座和让自己得到一个提升




正文开始——关于“基建”




1.什么是基建?



  • “技术基建”,就是研发团队的技术基础设施建设,一个团队通用技术能力的沉淀。

  • 小到文档规范,脚手架工具,大到工程化、各个领域工具链,凡是能促进业务效率、沟通成本都可以称作基建。

  • 网上看到的一句话,说的很好, “业务支撑是活在当下,技术基建是活好未来”




2.基建的意义


主要是为了以下几点:



  • 业务复用,提高效率: 基建可以提高单个人的工作产出和工作效率,可以从代码层面解决一些普遍性和常用性的业务问题

  • 规范、优化流程制度: 优异的流程制度必将带来正面的、积极的、有实效的业务支撑。

  • 更好面对未来业务发展: ,像建房子一样,好的地基可以建出万丈高楼。

  • 影响力建设、开源建设:建设结果对于业务的促进,更容易获得内部合作方的认可;沉淀下来的好的经验,可以对外输出分享,也是对影响力的有力帮助。




基建搞什么




1.核心:


下手之前首先得记住总结出的核心概念:



  • 三个落地要素: 公司的团队规模、公司的业务、团队水平。

  • 四大基础特性: 技术的健全性、基建的稳定性、研发的效率性、业务的体验性


根据结合落地和基础特性,来搭建不同"重量"和"复杂度"的基建系统。(毕竟每个公司的情况都不同)




2.方向


基建开始之前,首先得确定建设的策略及步骤,主要是从 拆解研发流程 入手的:


一个基本的研发流程闭环一般是:需求导入 => 需求拆解 => 技术方案制定 => 本地编码 => 联调 => 自测优化 => 提测修复 Bug => 打包 => 部署 => 数据收集&分析复盘 => 迭代优化 。


在研发流程闭环中每一个环节的阻塞点越少,研发效率就越高。基建,就是从这些耽误研发时间的阻塞点入手,按照普遍性 + 高频的优先级标准,挨个突破。




3.搞什么


通用的公式是: 标准化 + 规范化 + 工具化 + 自动化 ,能力完备后可以进一步提升到平台化 + 产品化。在方向上,主要是从下面的 8 个主要方向进行归类和建设,供大家参考:



  • 开发规范:这一部分沉淀的是团队的标准化共识,标准化是团队有效协作的必备前提。

  • 研发流程: 标准化流程直接影响上下游的协作分工和效率,优秀的流程能带来更专业的协作。

  • 工程管理: 面向应用全生命周期的低成本管控,从应用的创建到本地环境配置到低代码搭建到打包部署。

  • 性能体验: 自动化工具化的方式发现页面性能瓶颈,提供优化建议。

  • 安全防控: 三方包依赖安全、代码合规性检查、安全风险检测等防控机制。

  • 统计监控: 埋点方案、数据采集、数据分析、线上异常监控等。

  • 质量保障: 自测 CheckList、单测、UI 自动化测试、链路自动化测试等。


如上是一般性前端基建的主要方向和分区,不论是 PC 端还是移动端,这些都是基础的建设点。业务阶段、团队能力的差异,体现在基建上,在于产出的完整性颗粒度深入度自动化的覆盖范围。




4.大小公司基建重点


小团队的现实问题:考虑到现实,毕竟大多数前端团队不像大厂那样有丰富的团队人员配置,大多数还是很小的团队,小团队在实施基建时就不可避免的遇到很现实的阻力:



  • 最大的阻力应该就是 受限于团队规模小 ,无法投入较多精力处理作用于直接业务以外的事情

  • 其次应该是团队内部 对于基建的必要性和积极性认识不够 (够用就行的思想)


大小公司基建重点:




  • 小公司: 针对一些小团队或者说偏初创期的团队,其建设,往往越偏向于基础的技术收益,如脚手架组件库打包部署工具等;优先级应该排好,推荐初创公司和小团队成立优先搭建好:规范文档、统一开发环境技术栈/方法/工具、项目模板、CI/CD流程 ,把基础的闭环优先搭建起来。




  • 大公司: 越是成熟的业务和成熟沉淀的团队,其建设会越偏向于获取更多的业务收益,如直接服务于业务的系统,技术提效的同时更能直接带来业务收益。搭建起一套坚实的项目底座,能够更好的支持上层建筑的发展,同时也能够提升团队的成长,打开在业界的知名度,获取更好的信任支持。大公司在基础建设上,会更加考虑数据一些监控以及数据的埋点分析和统计,更加的偏重于数据的安全防范,做到质量保证。对于这点,很多前端需要写许多的测试case,有些人感觉很折磨,哈哈哈哈哈哈。






基建怎么搞




下面,会针对一些大家都感兴趣的方向,结合我们团队过去部分的建设产出,为大家列举一些前端基建类的沉淀,以供参考。


1. 规范&文档


规范和文档是最应该先行的,规范意味着标准,是团队的共识,是沟通协作的基础。


文档:



  • 新人文档(公司、业务、团队、流程等)

  • 技术文档、

  • 业务文档、

  • 项目文档(旧的、新的)

  • 计划文档(月度、季度、年度)

  • 技术分享交流会文档


规范:



  • 项目目录规范:比如api,组件,页面,路由,hooks,store等



  • 代码书写规范:组件结构、接口(定义好参数类型和响应数据类型)、事件、工具约束代码规范、代码规范、git提交规范




2. 脚手架


开发和维护一个通用的脚手架工具,可以帮助团队快速初始化项目结构、配置构建工具、集成常用的开发依赖等。


省事的可能直接拥抱框架选型对应的全家桶,如 Vue 全家桶,或者用 Webpack 撸一个脚手架。能力多一些的会再为脚手架提供一些插件服务,如 Lint 或者 Mock。从简单的一个本地脚手架,到复杂的一个工程化套件系统。




3. 组件


公司项目多了会有很多公共的组件,可以抽离出来,方便自身和其他项目复用,一般可以分为以下几种组件:



  • UI组件:antd、element、vant、uview...

  • 业务组件:表单、表格、搜索...

  • 功能组件:上拉刷新,滚动到底部加载更多,虚拟滚动,拖拽排序,图片懒加载..




4. 工具 / 函数库


前端工具库,如 axios、loadsh、Day.js、moment.js、big.js 等等(太多太多,记不得了)


常见的 方法 / API封装:query参数解析、device设备解析、环境区分、localStorage封装、Day日期格式封装、Thousands千分位格式化、防抖、节流、数组去重、数组扁平化、排序、判断类型等常用的方法hooks抽离出来组成函数库,方便在各个项目中使用




5. 模板


可以提前根据公司的业务需求,封装出各个端对应通用开发模版,封装好项目目录结构,接口请求,状态管理,代码规范,git规范钩子,页面适配,权限,本地存储管理等等,来减少开发新项目时前期准备工作时间,也能更好的统一公司整体的代码规范。



  1. 通用后台管理系统基础模版封装

  2. 通用小程序基础模版封装

  3. 通用h5端基础模版封装

  4. 通用node端基础模版封装

  5. 其他类型的项目默认模版封装,减少重复工作。




6. API管理 / BFF


推荐直接使用axios封装或fetch,项目中基于次做二次封装,只关注和项目有关的逻辑,不关注请求的实现逻辑。在请求异常的时候不返回 Promise.reject() ,而是返回一个对象,只是code改为异常状态的 code,这样在页面中使用时,不用用 try/catch 包裹,只用 if 判断 code 是否正确就可以。再在规定的目录结构、固定的格式导出和导入。


BFF(Backends For Frontends)主要将后端复杂的微服务,聚合成对各种不同用户端(无线/Web/H5/第三方等)友好和统一的API;




7. CI/CD 构建部署


前端具备自己的构建部署系统,便于专业化方面更好的流程控制。很多公司目前,都实现了云打包、云检测和自动化部署,每次 git commit 代码后,都会自动的为你部署项目至 测试环境、预生产环境、生产环境,不用你每次手动的去打包后 cv 到多个服务器和环境。开发新的独立系统之初,也会希望能实现一种 Flow 的流式机制,以便实现代码的合规性静态检测能力。如果可以的话,可以去实现了一套插件化机制,可以按需配置不同的检测项,如某检测项检测不通过,最终会阻塞发布流程。




8. 数据埋点与分析


前端团队可以做的是 Web 数据埋点收集和数据分析、可视化相关的全系统建设。可实现埋点规范、埋点 SDK、数据收集及分析、PV/UV、链路分析、转化分析、用户画像、可视化热图、坑位粒度数据透出等数据化能力,下面给大家细分一些这些数据:



  • 行为数据:时间、地点、人物、交互、交互的内容;

  • 质量数据:浏览器加载情况、错误异常等;

  • 环境数据:浏览器相关的元数据以及地理、运营商等;

  • 运营数据:PV、UV、转化率、留存率(很直观的数据);




9.微前端


将您的大型前端应用拆分为多个小型前端应用,这样每个小型前端应用都有自己的仓库,可以专注于单一的某个功能;也可再聚合成有各个应用组成的一个平台,而各个应用使用的技术栈可以不同,也就是可以将不同技术栈的项目给整合到一块。这点就很不错,在如今电子办公化如此细致的时代,可能许多公司工作中都不止一个平台,平台之间的切换十分的繁琐,这时候平台之间聚合的趋势想来是必然的。(个人浅显的理解)


目前成熟一点的框架有蛮多的,使用的底层思想也各有不同,目前我也在学习qiankun等框架中,期待后面能够给大家分享一篇文章,加油💪




基建之外思考




1. 从当下业务场景出发开始


很多时候我们的建设推不下去,往往不是因为人力的问题,而是 没想清楚/没有方向 。对于研发同学,我们更应该着重于当下,从方案出发找实际场景的问题,也就是从我们项目和团队目前的业务问题、人员问题,一步步出发。还有就是,我们得开这个头。没有一个作家是看小说看成的,同理技术专家也不会是通过看技术书籍养成的。在实践中也就是实际场景中学习,从来都是最快的方式。许多有价值的事从来都是从业务本身的问题出发。到头来你会发现:问题就是机会,问题就是长萝卜的坑




2.基建讲究循序渐进


业界大部分的研发团队,都不是阿里、腾讯、头条这样基础完备沉淀丰富的情况,起步期和快速爬坡期居多,建设滞后。体现在基建上,可能往往只有一个基于 Webpack 搞搞的脚手架,和一个第三方开源的 UI 组件库上封装下自己的业务组件库,除此之外无他。如果兄弟们现在恰好是我说的这种情况,不用焦虑,很多前端也是一样的情况。只要我们一步步建设,慢慢落地基础设施,就一定会取得好的反馈




3. 技术的价值,在于解决业务问题,并且匹配


技术的价值,在于解决业务问题;人的身价,在于解决问题的能力


基建的内容我认为首先是 和业务阶段相匹配 的。不同团队服务的业务阶段不同,基建的内容和广深度也会不同。高下之分不在于多寡,而在于对业务的理解和支持程度。


“业务支撑” 和 “基础建设” 都是同一件事的两个面,这个 “同一件事”,就是帮助业务解决问题。任何脱离解决实际场景而发起的基建,都需要重新审视甚至不应被鼓励。如果时间成本没有那么多的话,建议先搭建好基本的建设底座,想要更好的闭环的想法还是先搁置一下。




4.个人不足


总结了这么多,结果发现自己对于一些知识点还是了解的太浅显了,自身在那些方面能分享的还是不多,也看了一些文章,只能描出个大概,实在是有点不好意思。但回头想想,这何尝也不算个勉励自己的方法,能够鞭策自己。后续,在我学习深入一些基建方面的知识后,会再出一些文章分享给大家,希望能够帮助到大家,共勉!!!☺(发现问题会及时补充)




落尾




大家好,我是 KAIHUA ,一个来自阿卡林省目前在深圳前端区Frank + ikun


从这周开始,我想试试每一两周复盘一次,总结出至少一个知识点,目的是尽快给自己的反馈,将自己产品一样快速迭代上升,希望可以坚持✊。


如果有什么相关错误,望大家指正,感谢感谢!!!(还在学习中,嘿嘿🤭)


下一篇文章应该会是关于 前端思考 方面的,希望早一点归纳出,和大家沟通交流...


各位 彦祖 / 祖贤,fan yin (欢迎) 关注点赞收藏,将泼天的富贵带点给我😭


一起加油!!! giao~~~🐵🙈🙉


作者:KAIHUA
来源:juejin.cn/post/7301150860825133110
收起阅读 »

用剥蒜的方式去学习闭包,原来这么简单!!!

web
对于很多学习js的码友们来说,闭包无疑是最难啃的模块之一,今天我们就用剥蒜的方式一层一层的剥开它。 我们先用一个案例来引入它 大多数肯定会觉得它输出的结果是0到9,那么大多数人都错了,它输出的结果是十个10 那么要怎么让它输出0到9呢?这里我们要先引入一个新...
继续阅读 »

对于很多学习js的码友们来说,闭包无疑是最难啃的模块之一,今天我们就用剥蒜的方式一层一层的剥开它。


我们先用一个案例来引入它



大多数肯定会觉得它输出的结果是0到9,那么大多数人都错了,它输出的结果是十个10
那么要怎么让它输出0到9呢?这里我们要先引入一个新的东西,叫调用栈


调用栈


调用栈是用来管理函数调用关系的一种数据结构


在代码预编译的时候就会产生一个生成一个调用栈,它会先去找到代码中的全局变量、函数声明,形成一个变量环境,这个变量环境和词法环境(这里我们不去探讨)一起放在全局执行上下文中。然后再从上到下去执行代码,每调用一个函数,就会生成一个该函数的执行上下文,然后入栈,函数执行的时候先去它的词法环境去找对应的变量名,找不到再去变量环境中找,再找不到就去它的下一级去寻找直到找到为止,然后函数执行完成后再将其执行上下文销毁,避免栈溢出,我们用一个代码来举例:


iwEcAqNwbmcDAQTRB4AF0QQ4BrAXVDtt0dImQAU9UGjZw-8AB9IZAt15CAAJomltCgAL0gAC84A.png_720x720q90.jpg


执行函数add时,先去add的执行上下文中寻找b,b在add的变量环境中,但是并没有a,于是再去全局执行上下文中按照词法环境和变量环境的顺序去找,找到了a,最终返回a+b=12。


作用域链


调用栈在生成执行上下文时会默认在变量环境中产生一个outer,它指向该函数的外层作用域,函数声明在哪里,哪里就是函数的外层作用域,然后形成一个作用域链。


我们再来看下一个案例



调用foo的时候生成了foo的执行上下文,foo的函数体中有bar的调用,所以又生成了一个bar的执行上下文,bar声明在最外面,所以它的outer指向全局执行上下文,因此当bar在寻找myName这个变量的时候直接跳过foo去了全局执行上下文,所以最终输出的结果是万总


iwEcAqNwbmcDAQTRB4AF0QQ4BrDMeEmBKzv5FAU9V3BEkJoAB9IZAt15CAAJomltCgAL0gADbJY.png_720x720q90.jpg


闭包


了解完调用栈和作用域链之后,就可以进入我们今天的主题闭包了,还是用一个案例来说明



函数a的函数体中声明了一个函数b,并且函数a的结果是返回了函数b


var c = a() 先调用a,并且把a的返回值赋给c,因此c就是一个函数,然后再调用c,这就是整个的执行过程。在调用完a后,a的函数体已经全部执行完毕,应该被销毁,但是在调用c的时候(c就是函数b),需要用到a中的变量,因此在销毁掉a的执行上下文的同时会分出一个区域用来存储b中所需要用到a的变量,这个存储了count的地方就叫做闭包。


iwEcAqNwbmcDAQTRB4AF0QQ4BrCKgRsdcT9JIgU9Y1plkyAAB9IZAt15CAAJomltCgAL0gACzhQ.png_720x720q90.jpg


因此闭包的概念就是:


即使外部函数已经执行完毕,但是内部函数引用了外部函数中的变量依然会保存在内存中,我们把这些变量的集合,叫做闭包


现在我们再回到第一个问题,如何让它输出0到9,很显然,就是在for的内部形成一个闭包,让i每次可以叠加存在内存中,因此代码如下:



这样一层一层把从外到内的去了解闭包,是不是就更容易了呢,你学会了吗?


作者:欣之所向
来源:juejin.cn/post/7300063572074201125
收起阅读 »

前端小练:kiss小动画分享

web
最近的学习中,哈士奇学习了简单的动画的平移和旋转的用法,同时对于z-index有了一部分了解,那么这次就通过学习写了个kiss动画 人狠话不多,哈士奇给大家献上代码先 <!DOCTYPE html> <html lang="en"> ...
继续阅读 »

最近的学习中,哈士奇学习了简单的动画的平移和旋转的用法,同时对于z-index有了一部分了解,那么这次就通过学习写了个kiss动画


image.png


人狠话不多,哈士奇给大家献上代码先


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="container">
<div class="ball" id="l-ball">
<div class="face face-l">
<div class="eye-l eye-ll"></div>
<div class="eye-l eye-lr"></div>
<div class="mouth"></div>
</div>
</div>
<div class="ball" id="r-ball">
<div class="face face-r">
<div class="eye-r eye-rl"></div>
<div class="eye-r eye-rr"></div>
<div class="mouth-r"></div>
<div class="kiss-r">
<div class="kiss"></div>
<div class="kiss"></div>
</div>
</div>
</div>
</div>
</body>
</html>

body{
background-color:#78e08f;
margin: 0;
padding: 0px;
}
.container{
width: 232px;
height: 200px;

position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.ball{
width: 100px;
height: 100px;
border: 6px solid #000;
border-radius: 50%;
background-color: white;
position: relative;
display: inline-block;/*令块元素变为具有行内块元素的特点表现为可以使得div处于同一行*/
}
.face-l{
width: 70px;
height: 30px;
position: absolute;
right: 0px;
top: 30px;
}
.face-r{
width: 70px;
height: 30px;
position: absolute;
top: 30px;
}
.eye-l{
width: 15px;
height: 14px;
border-bottom:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-r{
width: 15px;
height: 14px;
border-top:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-ll{
left: 10px;
}
.eye-lr{
right: 5px;
}
.eye-rl{
left: 10px;
}
.eye-rr{
right: 5px;
}
.mouth{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
}
.face-l::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
right: -8px;
top: 20px;
}
.face-l::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
left:-5px;
top: 20px;

}
.face-r::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius:100%;
background-color: red;
position: absolute;
right: 3px;
top: 20px;
}
.face-r::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
background-color: red;
position: absolute;
top: 20px;
}
#l-ball{
z-index: 2;/*设置元素层级,使得左边的小人可以覆盖在右边的小人脸上*/
animation: close-l 4s ease infinite;/*平移4s*/
}
@keyframes close-l{
0%{
transform: translate(0);
}
20%{
transform: translate(20px);
}
35%{
transform: translate(20px);
}
55%{
transform: translate(0);
}
100%{
transform: translate(0);
}

}
.face-l{
animation:face-l 4s ease infinite;
}
@keyframes face-l{
0%{
transform: translate(0) rotate(0);/*translate 平移 rotate旋转*/
}
10%{
transform: translate(0) rotate(0);
}
20%{
transform: translate(5px) rotate(2deg);
}
28%{
transform: translate(0) rotate(0);
}
35%{
transform: translate(5px) rotate(2deg);/*需要写清楚像素,否则没有动画*/
}
50%{
transform: translate(0) rotate(0);
}
100%{
transform: translate(0) rotate(0);
}
}
.kiss-r{
margin: 0 auto;
position: absolute;
left: 35px;
right: 0;
bottom: -5px;
opacity: 0;
animation: kiss-r 4s ease infinite;
}
.kiss{
width: 13px;
height: 10px;
border-radius: 50%;
border-left: 5px solid #000;
display: block;
}
.mouth-r{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
animation: mouth-r 4s ease infinite;
}
#r-ball{
animation: close-r 4s ease infinite;
}
@keyframes close-r{
0%{
transform: translate(0);
}
50%{
transform: translate(0);
}
70%{
transform: translate(-45px)rotate(15deg);
}
85%{
transform: translate(-45px)rotate(-10deg);
}
100%{
transform: translate(0);
}

}
@keyframes mouth-r{
0%{
opacity: 1;
}
50%{
opacity: 1;
}
50.5%{
opacity: 0;
}
/*70%{
opacity: 1;
}*/

84.9%{
opacity: 0;
}
85%{
opacity: 1;
}
100%{
opacity: 1;
}
}
@keyframes kiss-r{
0%{
opacity: 0;
}
50%{
opacity: 0;
}
50.5%{
opacity: 1;
}
84.9%{
opacity: 1;
}
85%{
opacity: 0;
}
100%{
opacity: 0;
}
}

接下来哈士奇为大家依次聊聊这段代码
首先是html的部分:


html主要是使用div对整个页面做出一个布局,哈士奇此次的小人主要是小人的两张脸和五官,因此我们在html的代码创建的过程中需要留出脸 眼睛 嘴巴的部分进行后面的css代码的操作。


在这里有些同学可能会问到,这里的kiss和mouth怎么回事,稍后我们就知道了!


那么再给大家讲讲css的部分:


首先我们通过整个页面的设置,将整个页面背景设置,也就是body部分,去除之前的默认值。


body{
background-color:#78e08f;
margin: 0;
padding: 0px;
}

接下来就是容器container的设置,我们的设置一个大容器用于放下两个小人,通过position中的absolute对于父容器(此处的是body)进行定位,使用translate函数将容器移到页面的正中心的位置


.container{
width: 232px;
height: 200px;

position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}

将大致的位置确定好了以后,我们就可以开始对于两个kiss小人进行操作了


首先确定两个小球的设置


.ball{
width: 100px;
height: 100px;
border: 6px solid #000;
border-radius: 50%;
background-color: white;
position: relative;
display: inline-block;/*令块元素变为具有行内块元素的特点表现为可以使得div处于同一行*/
}

通过border将外形线条确定,这样一来就可以制造出小人外面一圈的线,通过border-radius确定弧度,最后通过relative的相对定位,针对元素的原本位置进行定位。那么为什么要使用display呢?我们都知道,inlie-block可以使得块级元素div转化为具有行内块的特点的元素,因此div中的两个ball小球就能处于同一行了


确定两个小球的位置以后我们开始确定小球的脸


.face-l{
width: 70px;
height: 30px;
position: absolute;
right: 0px;
top: 30px;
}
.face-r{
width: 70px;
height: 30px;
position: absolute;
top: 30px;
}

通过左右两脸的设置确定他们相对于他们父容器l-ball 和r-ball的位置


接下来设置眼睛的相同元素的设置


.eye-l{
width: 15px;
height: 14px;
border-bottom:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-r{
width: 15px;
height: 14px;
border-top:5px solid #000;
border-radius: 50%;
position: absolute;
}

对于我们来说,这里的两个眼睛其实就是两个弧线,所以我们只需要确定两根线,然后使用boder-radius进行弯曲,就能把眼睛制造出来了,再通过absolute对于自己的父容器进行定位


.eye-ll{
left: 10px;
}
.eye-lr{
right: 5px;
}
.eye-rl{
left: 10px;
}
.eye-rr{
right: 5px;
}

微调设置眼睛的具体位置


再进行嘴巴的设计


.mouth{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
}

也是进行一个弯曲的曲线的设计


接下来难度要升级了,两个脸颊红红的部分应该如何实现呢?
这里我们使用到了伪元素进行创建


.face-l::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
right: -8px;
top: 20px;
}
.face-l::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
left:-5px;
top: 20px;

}
.face-r::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius:100%;
background-color: red;
position: absolute;
right: 3px;
top: 20px;
}
.face-r::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
background-color: red;
position: absolute;
top: 20px;
}

给大家复习一下伪元素的用法,在哈士奇的这个代码中,before和after分别是对于父容器的第一个子元素进行操作,也就是face里面的左眼睛进行操作,针对左眼定位脸颊的位置(记住哦,如果没有给出伪元素的定位,也就是父容器的话,是无法显示伪元素的


这样一来,我们的脸颊也做好了
最后就是属于亲脸颊时的嘴巴部分


.kiss-r{
margin: 0 auto;
position: absolute;
left: 35px;
right: 0;
bottom: -5px;
opacity: 0;
animation: kiss-r 4s ease infinite;
}
.kiss{
width: 13px;
height: 10px;
border-radius: 50%;
border-left: 5px solid #000;
display: block;
}
.mouth-r{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
animation: mouth-r 4s ease infinite;
}

哈士奇在设计中,想要右边的小球能够在最后亲到左边小球,那么光移动嘴巴是不行的,还需要让嘴巴变形成为嘟嘴的样子,因此哈士奇在mouth-r中设置了嘴巴的样式,接下来又在kiss中设置了嘴巴转变后的样式。这就是为什么右边的小球要设置kiss和mouth的原因了!


在kiss-r中大家看到opacity,如果opacity:0; 那么代表着这个块是隐藏状态,如果opacity:1; 那么就是显示的状态


最后就是动画设置的部分了,有些小伙伴已经看出来了哈士奇已经写过的
animation: mouth-r 4s ease infinite;


那么在这给大家讲讲这是个啥意思


animation(声明动画): mouth-r(动画的名字) 4s(时间) ease(规定慢速开始,然后变快,然后慢速结束的过渡效果) infinite(永久执行:动画会循环播放)


先聊聊左脸的动画,哈士奇希望它平移过去,然后做出小鸟依人的蹭一蹭的动作,于是就有了


#l-ball{
z-index: 2;/*设置元素层级,使得左边的小人可以覆盖在右边的小人脸上*/
animation: close-l 4s ease infinite;/*平移4s*/
}
@keyframes close-l{
0%{
transform: translate(0);
}
20%{
transform: translate(20px);
}
35%{
transform: translate(20px);
}
55%{
transform: translate(0);
}
100%{
transform: translate(0);
}

}
.face-l{
animation:face-l 4s ease infinite;
}
@keyframes face-l{
0%{
transform: translate(0) rotate(0);/*translate 平移 rotate旋转*/
}
10%{
transform: translate(0) rotate(0);
}
20%{
transform: translate(5px) rotate(2deg);
}
28%{
transform: translate(0) rotate(0);
}
35%{
transform: translate(5px) rotate(2deg);/*需要写清楚像素,否则没有动画*/
}
50%{
transform: translate(0) rotate(0);
}
100%{
transform: translate(0) rotate(0);
}
}

首先就是在元素中声明需要准备动画,然后在下方使用@keyframes 动画名 写出一个动画的具体内容(我们需要写出什么时候动画要做什么)


比如4s中0%的时候我希望动画开始平移,就写transform:translate()写出平移的位置是多少像素,那么在下一个%出现前,浏览器就会执行你的操作,表示在%~%之间执行动画的操作


rotate()则是进行旋转,使用以后动画将会根据一定的比例进行旋转


最后就是右脸小球的亲亲操作了


#r-ball{
animation: close-r 4s ease infinite;
}
@keyframes close-r{
0%{
transform: translate(0);
}
50%{
transform: translate(0);
}
70%{
transform: translate(-45px)rotate(15deg);
}
85%{
transform: translate(-45px)rotate(-10deg);
}
100%{
transform: translate(0);
}

}
@keyframes mouth-r{
0%{
opacity: 1;
}
50%{
opacity: 1;
}
50.5%{
opacity: 0;
}
/*70%{
opacity: 1;
}*/

84.9%{
opacity: 0;
}
85%{
opacity: 1;
}
100%{
opacity: 1;
}
}
@keyframes kiss-r{
0%{
opacity: 0;
}
50%{
opacity: 0;
}
50.5%{
opacity: 1;
}
84.9%{
opacity: 1;
}
85%{
opacity: 0;
}
100%{
opacity: 0;
}
}

前面还是进行平移操作,到了后面,小球需要进行亲亲,那么哈士奇通过opacity实时操作嘴巴的出现时间,最后在亲的前面的时间把嘟嘴展现出来。


你学会了吗?快去给你的女友写个亲亲动画吧!!


总结与联想


总结


今天哈士奇给大家分享了一个前端小动画的展现,并且逐步为大家解释了一个前端小动画应该如何写出来,在这其中涉及到了transform opacity animation z-index的使用,大家可以简单上手做做哦


联想


那么动画是否还有其他的关键词呢?ease就能解决所有的平移问题吗?我们是否可以通过其他方式展示不同效果呢?


作者:疯犬丨哈士奇
来源:juejin.cn/post/7300460850010734646
收起阅读 »

周爱民:告前端同学书

web
一年前,InfoQ的编辑约请我对前端技术做了些回顾总结,说了三个方面的话题:其一,前端过去的15年大致可以怎样划分;其二,前端的现状以及面临的挑战;其三,前端会有怎样的未来。后来刊发成综述,是《技术15年》。缘于文体变动,访谈的味道十不存一,所以这里再次整理成...
继续阅读 »

一年前,InfoQ的编辑约请我对前端技术做了些回顾总结,说了三个方面的话题:其一,前端过去的15年大致可以怎样划分;其二,前端的现状以及面临的挑战;其三,前端会有怎样的未来。后来刊发成综述,是《技术15年》。缘于文体变动,访谈的味道十不存一,所以这里再次整理成文,是为《告前端同学书》。



作者:周爱民 / aimingoo


各位前端同学,就我的所知来看,每⼀个具体的技术,在其⽅向上都有着不同的标志事件,也因此有着不同的阶段划分。但是我想,如果从我们这个领域对“前端”的认识来观察这件事,⼤概会对不同的前端阶段能有更清晰的认识。


早期前端的从业⼈员⼤多来⾃后端开发者、应⽤软件开发者,或者⽹⻚设计师,⽽并没有专职的前端开发。例如说阿⾥巴巴在 2010 年之前都把前端归在产品部⻔,可⻅前端⼯程师的来源和定位⼀直都很模糊。这个时代,ECMAScript 还陷在 Ed4 的泥坑中没有⾛出来,IE 浏览器带来的标准分裂还没有得到全⾯的修补,源于对这个领域的漠视,⼤⼚优势也没有体现出来,前端开发者们基本上各⾃为战,框架和中间件层出不穷⽽⼜良莠难分,开发⼯具和环境却荒草凄凄以⾄于乏善可陈。但是也正是在这个时代,ES6、CSS3、HTML5 等等都在筑基、渗透与蓄势。


随着专⽤⼯具链和开发流程的成熟,前后端分离的运动从项⽬内部开始蔓延到整个领域,出现了专⻔的前端开发⼯程师、团队,以及随之⽽来的⻆⾊细分,很多独⽴的技术社区就是在这个时代出现的。前后端分离不仅仅是⼀种技术表现,更是⼀种⾏业协作的模式与规范,并且反过来推动了⼯具和框架的⼤发展。信⼼满满的前端不拘于⼀城⼀地,⼀⽅⾯向前、向专业领域推进,从⽽影响到交互与接触层。因此更丰富的界⾯表现,以及从移动设备到⼈机交互界⾯等领域都成了前端的研究⽅向,是所谓“⼤前端”。⽽另⼀⽅⾯则向后、向系统领域渗透,有了所谓⼯程师“全栈化”运动。这个时候的“全栈”,在⼯程上正好符合敏捷团队的需求,在实践上正好⼜叠加上DevOPS、云端开发和⼩应⽤的⼏阵助⼒,前端因此⼀⽚繁华景象。


所以 2008 年左右开始的前后端分离是整个前端第⼆阶段的起点,这场运动改变了软件开发的体系与格局,为随后⼗年的前端成熟期拓开了局⾯。那⼀年的 SD2C 我谈了《VCL 已死、RAD 已死》,⽽⼗年后阿⾥的圆⼼在GMTC 上讲了《前端路上的思考》,可算作对这个时代的预⾔和反思。


相对于之前所说的第⼀、第⼆阶段,我认为如今我们正⾏进在⼀个全新阶段中。这个阶段初起的主要表现是:前端分离为独⽴领域,并向前、后两个⽅向并进之举已然势微。其关键在于,前端这个领域中的内容已经逐渐复杂,⽽其应⽤的体量也将愈加庞⼤,因此再向任何⽅向发展都难尽全⼒、难得全功。


摊⼦铺得⼤了,就需要再分家。所以下⼀个阶段中,将再次发⽣横向的领域分层,⼀些弥合层间差异的技术、⽅法与⼯具也将出现,类似于 Babel 这样的“嵌缝膏”产品将会再次成为⼀时热⻔。但⻓期来说,领域分层带来的是更专精的职业与技能,跨域协作是规约性的、流程化的,以及⼯具适配的。从 ECMAScript 的实践来看,规范的快速更新和迭代已经成为现实,因此围绕规范与接⼝的新的开发技术与⼯程模型,将会在这个阶段中成为主要⼒量,并成为维持系统稳定性的主要⼿段。


这是在⼀个新阶段的前夜。故此,有很多信息并不那么明朗,⽐如说像前后端分离这样的标志性事件并没有出现,亦或者出现了也还没有形成典型影响。我倾向于认为引领新时代的,或者说开启下⼀个阶段的运动将会发⽣在交互领域,也就是说新的交互⽅式决定了前端的未来。之前⾏业⾥在讲的 VR 和 AR(虚拟现实和增强实境)是在这个⽅向上的典型技术,但不唯于此。⼏乎所有在交互⽅式上的变⾰,都会成为⼈们认识与改变这个世界的全新动⼒,像语⾳识别、视觉捕捉、脑机接⼝等等,这些半成熟的或者实验性的技术都在影响着我们对“交互”的理解,从⽽也重新定义了前端。


⾏业⽣态也会重构,如同今天的前端⼤会已经从“XX技术⼤会”中分离出来⼀样,不久之后“交互”也会从前端分化出来,设计、组件化、框架与平台等等也会成体系地分化出来。前端会变得⽐后端更复杂、更多元,以及更加的⽣机勃勃。这样的⽣态起来了,⼀个新的时代也就来临了。简单地说,1、要注重领域与规范,2、要跟进交互与体验,3、要在⽣态中看到机会。


然而,前端的同学们,我们也不要忘记在这背景中回望自身,正视我们前端自己的问题。


其⼀,底⼦还是薄,前端在技术团队与社区的积累上仍然不够。看起来摊⼦是铺开了,但是每每只在“如何应⽤”上下功夫,真正在⽹络、系统、语⾔、编译、机器学习等等⽅⾯有深⼊研究的并不多。⼀直以来,真正有创建性或预⻅性的思想、⽅法与理论鲜⻅于前端,根底薄是⾸要原因。


其⼆,思维转换慢,有些技术与思想抛弃得不够快,不够彻底。不能总是把核⼼放在“三⼤件(JS+CSS+HTML)”上⾯,核⼼要是不变,前端的⾰命也就不会真正开始。要把“Web 前端”前⾯的“Web”去掉,就现实来说,很多⼈连“观望”都没有开始。


其三,还没有找到跟“交互”结合起来的有效⽅法与机制。前端过去⼗年,在 IoT、机器学习、云平台等等每⼀次潮流都卡上了点⼉,但是如果前端的下⼀次转型起于“交互”,那么我们⽬前还没有能⼒适应这样的变化。当然,契机也可能不在于“交互”,但如果这样,我们的准备就更不充分了。


其四,向更多的应⽤领域渗透的动机与动⼒不明确。⻓期以来,前端在各个领域上都只是陪跑,缺乏真正推动这些领域的动机与动⼒。往将来看,这些因素在前端也将持续缺乏。寻求让前端持续发展,甚⾄领跑某些领域的内驱⼒量,任重⽽道远。


同学们,我想我们必须有一种共同的、清醒的认识与认知:浏览器是未来。去操作系统和云化是两个⼤的⽅向,当它们达成⽬标时,浏览器将成为与⽤户接触的唯⼀渠道。研究浏览器,其本质就是研究交互和表现,是前端的“终极私活”。但不要局限于“Web 浏览器”,它必将成为历史,如同操作系统的“⽂件浏览器”⼀样。


要极其关注 JavaScript 的类型化,弱类型是这⻔语⾔在先天条件上的劣势,是它在⼤型化和系统化应⽤中的明显短板。这个问题⼀旦改善,JavaScript 将有⼒量从其它各种语⾔中汲取营养,并得以⾯向更多的开发领域,这是 JavaScript 的未来。


AI 和 WASM 在前端可以成为⻬头并进的技术,⼀个算法,⼀个实现。对于前端来说,性能问题⼀直是核⼼问题,⽽交互与表现必将“⼤型与复杂化”,例如虚拟现实交互,以及模拟反馈等等,⽽ WASM 是应对这些问题的有效⼿段。


所谓交互与表现,本质上都是“空间问题”。亦即是说,前端表现中的所谓布局、块、位置、流等等传统模式与技术,与将来的交互技术在问题上是同源的。就像“盒模型”确定了 CSS 在前端的核⼼地位⼀样,新的空间定位技术,以及与之匹配的表现与交互⽅法是值得关注和跟进的。


前端要有更强的组织⼒,才能应付更⼤规模的系统。这⾥的组织⼒主要是针对⼯程化⽽⾔,所有⼯程化⼯具,其最终的落脚点都在快速、可靠,并以体系化的⽅式来组织⼯程项⽬。这包括⼈、资源、信息、时间、能⼒与关系等等⼯程因素,每个⽅⾯都有问题,都值得投⼊技术⼒量。


相较于新入行的前端的同学们,我能从没有前端走到如今前端的⼤发展,何其幸也。以我⼀路之所⻅,前端真正让我钦佩的是持久的活⼒。前端开发者⼏乎总是⼀个团队中“新鲜⾎液”的代名词,因此前端在业界的每个阶段都⾛在时代的前列。如今看 C 语⾔的⽼迈,操作系统的封闭,后台的保守,以及业务应⽤、产品市场等等各个领域都在筑城⾃守,再看前端种种,便总觉得开放与探索的信念犹在。


曾经与我⼀道的那些早期的前端开发者们,如今有做了主管的,有搞了标准的,有带了团队的,有转了后端的,有做架构做产品做运维等等⼀肩担之,也有开了公司做了顾问从商⼊政的,但也仍然还有在前端⼀线上做着努⼒,仍看好于这⼀个⽅向并在具体事务上勉⼒前⾏的。我曾经说,“任何事情做个⼗年,总会有所成绩的”,如今看来,这个时间还是说少了,得说是:⼏个⼗年地做下去,前端总能做到第⼀。


惟只提醒⼤家,领域分层的潮流之下,层间技术的核⼼不是功能(functional),⽽是能⼒(capabilities)。向应⽤者交付能⼒,需要有体系性的思维,要看向系统的全貌。我们专精于细节没错,专注于⼀城⼀地也没错,然而眼光⾼远⽽脚踏实地,是前端朋友们当有之势。


亦是这个时代予我们的当为之事!


周爱民/aimingoo


初稿于2022.06


此稿于2023.10


作者:裕波
来源:juejin.cn/post/7290751135903236137
收起阅读 »

【微信小程序】 token 无感刷新

web
⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖 一、【实现思路】🚩 小程序端登录时,除了返回用户信息,还需返回两个 token 信息 accessToken:用于验证用户身份 refreshToken:用于刷新 a...
继续阅读 »

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖


一、【实现思路】🚩



  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息

    • accessToken:用于验证用户身份

    • refreshToken:用于刷新 accessToken



  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求

  3. 刷新成功会拿到新的 accessToken 和 refreshToken

  4. 然后使用新的 accessToken 将失败的请求重新发送


二、【流程图】🚩


图中为大致流程,为了看起来简洁直观,略去了与本文内容不相关的步骤


image.png


三、【后端代码】🚩



1. ⌈签发和验证 token⌋🍥




  • 签发 accessToken 时,设置的过期时间是4个小时

  • 签发 refreshToken 时,设置的过期时间是7天

  • 自己在测试的时候,可以把时间改短一点,例如30s

  • 正常实现的效果是:登录时间超过4个小时之后,再次发送需要身份验证的请求,会使用 refreshToken 去请求刷新 accessToken;如果距离上次登录已经超过了7天,则会提示重新登录

  • 这样的话,实现了一定的安全性(因为 accessToken 每隔4个小时就会更新),同时又没有让用户频繁地重新登录



2. ⌈登录⌋🍥




  • 拿到请求参数中的登录凭证(code),以及保存的 appId 和 appSecret

  • 基于上述三个参数发送请求到微信官方指定的服务器地址,获取 openid

  • openid 是小程序登录用户的唯一标识,每次登录时的登录凭证(code)会变,但是获取到的 openid 是不变的

  • 根据 openid 在数据库中查找用户,如果没有查找到,说明本次登录是当前用户的首次登录,需要创建一个新用户,存入数据库中

  • 然后根据用户 id 以及设置的签发密钥进行 accessToken 和 refreshToken 的签发

  • 签发密钥可以是自己随意设置的一段字符串,两个 token 要设置各自对应的签发密钥

  • 这个签发密钥,在进行 token 验证的时候会使用到


四、【前端代码】🚩



1. ⌈封装的登录方法⌋🍥




  • 在创建微信小程序项目时,默认是在根目录下 app.js 的 onLaunch 生命周期函数中进行了登录

  • 也就是说每次在小程序初始化的时候都会进行登录操作

  • 作者这里是把登录操作单独提取出来了,这样可以在个人主页界面专门设置一个登录按钮

  • 当本地存储的用户信息被清除,或者上面提到的 refereshToken 也过期的情况下,我们点击登录按钮进行登录操作


import { loginApi } from '@/api/v1/index'

const login = async () => {
try {
// 登录获取 code
const {code} = await wx.login()
// 调用后端接口,获取用户信息
const {user, accessToken, refreshToken} = await loginApi(code)
wx.setStorageSync('profile', user)
wx.setStorageSync('accessToken', `Bearer ${accessToken}`)
wx.setStorageSync('refreshToken', refreshToken)
} catch (error) {
wx.showToast({
title: '登录失败,请稍后重试',
icon: 'error',
duration: 2000
})
}
}

export default login



2. ⌈封装的请求方法⌋🍥






  • 接收5个参数:url、method、params、needToken、header(点击展开)

    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接

    • method:请求方法,默认值为 'GET'

    • params:请求参数,数据格式为 object,例如: {name: 'test'}

    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false

    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null






  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。在上面的验证 token 代码中,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息




  • 调用 wx.request 发送请求,在 success 回调函数中判断请求返回信息中的状态码,根据状态码的不同做对应的操作,这里只讨论401 token 过期的情况





  • 当 token 过期时,从本地存储中获取到 refreshToken,然后调用对应后端接口刷新 token(点击展开)

    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)

    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求

    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true






  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来




  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送





  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)

    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中





【源码】🚩



【说明】🚩



  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢


作者:Libra0809
来源:juejin.cn/post/7299357353538486291
收起阅读 »

【Taro】【微信小程序】token 无感刷新

web
⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖 一、【实现思路】🚩 小程序端登录时,除了返回用户信息,还需返回两个 token 信息 accessToken:用于验证用户身份 refreshToken:用于刷新 a...
继续阅读 »

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖


一、【实现思路】🚩



  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息

    • accessToken:用于验证用户身份

    • refreshToken:用于刷新 accessToken



  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求

  3. 刷新成功会拿到新的 accessToken 和 refreshToken

  4. 然后使用新的 accessToken 将失败的请求重新发送



👉具体实现可以翻阅作者的上一篇文章:【微信小程序】token 无感刷新



👉实现思路中的后三步,微信小程序中是在请求的 success 回调函数中做的处理,Taro 中则是设置了响应拦截器,在拦截器中做的对应处理,本文仅讨论有区别的这部分



二、【前端代码】🚩



1. ⌈封装的请求方法⌋🍥






  • 接收5个参数:url、method、params、needToken、header(点击展开)

    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接

    • method:请求方法,默认值为 'GET'

    • params:请求参数,数据格式为 object,例如: {name: 'test'}

    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false

    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null






  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。后端接口在验证 token 时,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息




  • 先通过 Taro.addInterceptor 设置拦截器,然后调用 Taro.request 发送请求。这样的话,当请求真正发送之前以及获取到响应信息时,都会先进入到拦截器中,我们就是在这里进行的 token 刷新操作





  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { baseUrl } from '../config'
    import responseInterceptor from '../http/interceptors'

    // 添加拦截器
    Taro.addInterceptor(responseInterceptor)

    // 封装的请求方法
    const request = (url, method = 'GET', params = {}, needToken = false, header = null) => {
    const {contentType = 'application/json'} = header || {}
    if (url.indexOf(baseUrl) === -1) url = baseUrl + url

    const option = {
    url,
    method,
    data: method === 'GET' ? {} : params,
    header: {'Content-Type': contentType}
    }

    // 处理 token
    if (needToken) {
    const token = Taro.getStorageSync('accessToken')

    if (token) {
    option['header']['Authorization'] = token
    } else {
    Taro.setStorageSync('profile', null)
    Taro.showToast({
    title: '请登录',
    icon: 'error',
    duration: 2000
    })

    return
    }
    }

    // 发起请求
    return Taro.request(option)
    }

    export default request





2. ⌈拦截器⌋🍥



拦截器是一个函数,接受 chain 对象作为参数。chain 对象中含有 requestParams 属性,代表请求参数。拦截器最后需要调用 chain.proceed(requestParams) 以调用下一个拦截器或者发起请求。Taro 中的这个拦截器没有请求拦截器和响应拦截器之分,具体看你是在调用 chain.proceed(requestParams) 之前还是之后做的操作。具体说明可查阅官方文档




  • 拦截器中先调用 chain.proceed(requestParams) 发送请求,其返回的是一个 promise 对象,所以可以在 .then 中做响应处理




  • .then 中先判断响应状态码,这里我们只讨论 401 token 过期的情况





  • 当 token 过期时,获取本地存储的 refreshToken,然后调用对应后端接口刷新 token(点击展开)

    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)

    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求

    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true






  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来




  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送





  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)

    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中







  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { statusCode } from '../config'
    import request from './request'

    // 标识 token 刷新状态
    let isTokenRefreshing = false

    // 存储因为等待 token 刷新而挂起的请求
    let failedRequests = []

    // 设置响应拦截器
    const responseInterceptor = chain => {
    // 先获取到本次请求的参数,后面会使用到
    let {requestParams} = chain

    // 发起请求,然后进行响应处理
    return chain.proceed(requestParams)
    .then(res => {
    switch (res.statusCode) {
    // 404
    case statusCode.NOT_FOUND:
    return Promise.reject({message: '请求资源不存在'})
    // 502
    case statusCode.BAD_GATEWAY:
    return Promise.reject({message: '服务端出现了问题'})
    // 403
    case statusCode.FORBIDDEN:
    return Promise.reject({message: '没有权限访问'})
    // 401
    case statusCode.AUTHENTICATE:
    // 获取 refreshToken 发送请求刷新 token
    // 刷新请求发送前,先判断是否有已发送的请求,如果有就挂起,如果没有就发送请求
    if (isTokenRefreshing) {
    const {url: u, method, params, header} = requestParams
    return failedRequests.push(() => request(u, method, params, true, header))
    }

    isTokenRefreshing = true
    const url = '/auth/refresh-token'
    const refreshToken = Taro.getStorageSync('refreshToken')
    return request(url, 'POST', {refreshToken}, false)
    .then(response => {
    // 刷新成功,将新的 accesToken 和 refreshToken 存储到本地
    Taro.setStorageSync('accessToken', `Bearer ${response.accessToken}`)
    Taro.setStorageSync('refreshToken', response.refreshToken)

    // 将 failedRequests 中的请求使用刷新后的 accessToken 重新发送
    failedRequests.forEach(callback => callback())
    failedRequests = []

    // 再将之前报 401 错误的请求重新发送
    const {url: u, method, params, header} = requestParams
    return request(u, method, params, true, header)
    })
    .catch(err => Promise.reject(err))
    .finally(() => {
    // 无论刷新是否成功,都需要将 isTokenRefreshing 重置为 false
    isTokenRefreshing = false
    })
    // 500
    case statusCode.SERVER_ERROR:
    // 刷新 token 失败
    if (res.data.message === 'Failed to refresh token') {
    Taro.setStorageSync('profile', null)
    Taro.showToast({
    title: '请登录',
    icon: 'error',
    duration: 2000
    })
    return Promise.reject({message: '请登录'})
    }

    // 其他问题导致失败
    return Promise.reject({message: '服务器错误'})
    // 200
    case statusCode.SUCCESS:
    return res.data
    // default
    default:
    return Promise.reject({message: ''})
    }
    })
    .catch(error => {
    console.log('网络请求异常', error, requestParams)
    return Promise.reject(error)
    })
    }

    export default responseInterceptor




【源码】🚩



【说明】🚩



  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢


作者:Libra0809
来源:juejin.cn/post/7300592516759306291
收起阅读 »

语雀又崩了?今天咱们玩点花的,手把手教你写出令人窒息的“烂代码”

web
Hello,大家好,我是Sunday。 10月23日 2023年10月23日,语雀崩溃 10 个小时,作为一款知名度极高的产品,这样的一次崩溃可以说对语雀的口碑影响极大。 不过,好在语雀的公关团队处理的还不错,没有甩锅而是及时发布公告,明确是自己的问题。同时在...
继续阅读 »

Hello,大家好,我是Sunday。


10月23日


2023年10月23日,语雀崩溃 10 个小时,作为一款知名度极高的产品,这样的一次崩溃可以说对语雀的口碑影响极大。


不过,好在语雀的公关团队处理的还不错,没有甩锅而是及时发布公告,明确是自己的问题。同时在问题解决之后,给大家 六个月的会员补偿 也可以说是诚意满满(以下为10月24日语雀团队公告)。




毕竟大家都是程序员嘛,这种事也不是不能接受。毕竟:谁还没搞崩过系统呢?😂



本以为这件事就这么过去了,哪知道昨天的一个故障,再次让语雀登上了“风口浪尖”......


11月12日


昨天下午,我在正常使用语雀记录同学学习情况的时候,突然出现了无法保存的情况。心想:“这不会是又崩了吧~~”


看了眼语雀群的微信,果然......




说实话,当时我的第一反应是:“又有瓜可以吃了~~~~~,开心😂”



反正也写不成了,坐等吃瓜就行了。正好恰逢双十一,看看买的硬盘到哪了。


结果打开淘宝才发现,这次不对劲啊,淘宝也崩了!!!




最终我们了解了事情的全貌:



本次事故是由于阿里云 OSS 的故障导致的。钉钉、咸鱼、淘宝、语雀都崩了....



从语雀的公告也体现出了这点:



公告内容如下:



尊敬的客户:您好!北京时间2023年11月12日 17:44起,阿里云监控云产品控制台访问及API调用出现出现使用异常,阿里云工程师正在紧急介入排查。非常抱歉给您的使用带来不便,若有任何问题,请随时联系我们。



可以说,语雀这次有点躺枪了(谁让你刚崩过呢~~~)。


玩点花的!教你写出令人窒息的“烂代码”


好啦,瓜吃完啦。



关于语雀崩溃的反思,网上有很多文章,我就不凑这个热闹了,想要看的同学可以自行搜索~~



“回归正题”,接下来咱们就来看看咱们的文章正题:“如何写出烂代码”。



以下共有十三条烂代码书写准则,可能并没有面面俱到,如果大家发现有一些难以忍受的烂代码习惯,也可以留言发表意见~~



第一条:打字越少越好


  // Good 👍🏻
const a = 18

// Bad 👎
const age = 18

第二条:变量/函数混合命名风格


  // Good 👍🏻
const my_name = 'Suday'
const mName = 'Sunday'
const MnAme = 'Sunday'

// Bad 👎
const myName = 'Sunday'

第三条:不要写注释


  // Good 👍🏻
const cbdr = 666

// Bad 👎
// 666ms 是根据 UX A/B 测试结果进行经验计算的。
// 具体可查看 xxxxxx
const callbackDebounceRate = 666

第四条:使用母语写注释


  // Good 👍🏻
// 666 мс было эмпірычна вылічана на аснове вынікаў UX A/B.
const callbackDebounceRate = 666

// Bad 👎
// 666ms 是根据 UX A/B 测试结果进行经验计算的。
// 具体可查看 xxxxxx
const callbackDebounceRate = 666

第五条:尽可能混合不同的格式


  // Good 👍🏻
const n = 'Sunday';
const a = "18"
const g = "MAN"

// Bad 👎
const name = 'sunday'
const age = '18'
const gender = 'man'

第六条:尽可能把代码写成一行


  // Good 👍🏻
document.location.search.replace(/(^\?)/, '').split('&').reduce(function (o, n) { n = n.split('=') })

// Bad 👎
document.location.search
.replace(/(^\?)/, '')
.split('&')
.reduce((searchParams, keyValuePair) => {
keyValuePair = keyValuePair.split('=')
searchParams[keyValuePair[0]] = keyValuePair[1]
return searchParams
})

第七条:发现错误要保持静默


   // Good 👍🏻
try {
...
} catch () {🤐}

// Bad 👎
try {
...
} catch (error) {
setErrorMessage(error.message)
logError(error)
}

第八条:广泛使用全局变量


  // Good 👍🏻
let count = 1
function addCount() {
count += 1
}

// Bad 👎
function addCount() {
let count = 1
count += 1
}

第九条:构建备用变量


  // Good 👍🏻
let count = 1
function addCount() {
count += 1
}

// Bad 👎
function addCount() {
let count = 1
count += 1
}

第十条:Type 使用需谨慎


  // Good 👍🏻
function sum(a, b) {
return a + b
}

// Bad 👎
function sum(a: number, b: number) {
return a + b
}

第十一条:准备「Plan B」


  // Good 👍🏻
function square(num) {
if (typeof num === 'undefined') {
return undefined
} else {
return num ** 2
}
return null
}

// Bad 👎
function square(num) {
if (typeof num === 'undefined') {
return undefined
}
return num ** 2
}

第十二条:嵌套的三角法则


    // Good 👍🏻
function somFun(num) {
if (condition1) {
if (condition2) {
asyncFunction(param, (result) => {
if (result) {
for (; ;) {
if (condition3) {

}
}
}
})
}
}
}

// Bad 👎
async function somFun(num) {
if (!condition1 || !condition2) {
return;
}
const result = await asyncFunction(params);
if (!result) {
return;
}
for (; ;) {
if (condition3) {

}
}
}

第十三条:混合缩进


      // Good 👍🏻
const f = ['zs'
, 'lisi', 'wangwu']
const d = {
name: 'zs',
age: '18'
}

// Bad 👎
const f = ['zs'
, 'lisi', 'wangwu']
const d = {
name: 'zs',
age: '18'
}

总结


所谓的“烂代码”,是大家一定 不要 照着写的哈。


“教你写出令人窒息的“烂代码”“ 是一个反义,这个大家肯定是可以明白的哈~~~~。



”烂代码“内容参考自:zhuanlan.zhihu.com/p/516564022



作者:程序员Sunday
来源:juejin.cn/post/7300440002999435316
收起阅读 »

Uniapp Record:获取手机号

web
前言:渠道 -> 产品:"我需要收集用户信息"。产品 -> 开发:"那就新增一个功能来获取用户的手机号地址相关信息"。最近项目中增加了获取用户信息相关需求,这个功能怎么说呢,对于我甚至是大部分人来说都是比较抵触的吧,毕竟无缘无故获取个人信息就是感觉...
继续阅读 »

前言:渠道 -> 产品:"我需要收集用户信息"。产品 -> 开发:"那就新增一个功能来获取用户的手机号地址相关信息"。最近项目中增加了获取用户信息相关需求,这个功能怎么说呢,对于我甚至是大部分人来说都是比较抵触的吧,毕竟无缘无故获取个人信息就是感觉不爽,哈哈!但是没也没法,身为打工人的无奈,照做呗。



由于目前项目技术栈是 uniapp,所以先去官方文档查阅相关资料,了解到目前有是三种方式涉及手机号相关的,自然也是能够获取到手机号。


1. uni一键登录


uni一键登录是DCloud公司联合个推公司推出的,整合了三大运营商网关认证能力的服务。实现流程如下:



  1. App 界面弹出请求授权,询问用户是否同意该App获取手机号。这个授权界面是运营商SDK弹出的,可以有限定制;

  2. 用户同意授权后,SDK底层访问运营商网关鉴权,获得当前设备access_token等信息;

  3. 在服务器侧通过 uniCloud 将access_token等信息 置换为当前设备的真实手机号码。然后服务器直接入库,避免手机号传递到前端发生的不可信情况。


对该方法大致了解了下,其中流程相对比较简单,但是结合当前项目来说:



  1. 每次验证需要收费,虽然很便宜(2分)

  2. 需要开通uni一键登录服务,uniCloud 服务


因为项目不涉及云开发,而且不考虑产品使用时产生的额外费用,所以暂时pass掉。


2. OAuth 登录鉴权


App端OAuth(登录鉴权)模块封装了市场上主流的三方登录SDK,提供JS API统一调用登录鉴权功能。也看下实现流程:



  1. 向三方登录平台申请开通,有些平台(如微信登录)申请成功后会获取appid;

  2. 在HBuilder中配置申请的参数(如appid等),提交云端打包生成自定义基座;

  3. 在App项目中用API进行登录,成功后获取到授权标识提交到业务服务器完成登录操作。


该方式需要在项目 mainifest.json 中去开启 OAuth 鉴权模块:


uni02.png


可以看到里面除了前面提到的 一键登录,还包含 苹果登录、微信登录、QQ登录等三方登录平台,因为要涉及开通相关服务,并且当前登录业务鉴权逻辑比较简单(手机号、密码验证),并且app也为上架应用市场,所以这种相对繁琐的方式也就不考虑了。


3. 微信小程序登录


前面两种方式都pass掉了,意味着要获取手机号相关信息在APP中是行不通了的,但是不慌,不是还有微信小程序版嘛,正好产品也包含小程序平台,前段时间做公众号网页开发时也是包含登录授权,所以小程序的授权登录应该也差不多,而且小程序对比APP来说相对便捷(缺点是某些涉及原生插件相关的功能暂时无法使用)。


同样,先去微信官方文档查阅,看到有两种方式可以获取:


uni03.png


下面具体介绍下实现方案:


3-1. 纯前端实现

<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" plain="true">获取手机号</button>

这个 button 里面的一些属性及事件的具体用法说明可以去看文档说明:uniapp button 用法,文档解释的很清楚,写法也是固定的。


这里还需要用到一个加解密插件:WXBizDataCrypt,下载链接如下,


https://res.wx.qq.com/wxdoc/dist/assets/media/aes-sample.eae1f364.zip

可以去下载选择对应的版本,目前有 Java、C++、Node、Python四个版本,我们这里选择Node版本,将 WXBizDataCrypt.js 添加到项目中


完整代码如下:


<!-- testPhone.vue -->
<template>
<view class="wrap">
<view class="box-container">
<input v-model="phone" />
<view class="action-btn">
<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" plain="true">获取手机号</button>
</view>
</view>
</view>
</template>

<script>
import WXBizDataCrypt from '@/utils/WXBizDataCrypt.js'

export default {
data() {
return {
phone: "",
phone_iv: "",
js_code: "",
session_key: "",
phone_encryptedData: null,
}
},
onShow() {
this.initLogin()
},
methods: {
initLogin() {
uni.login({
provider: 'weixin',
success: res => {
this.js_code = res.code
uni.request({
url: 'https://api.weixin.qq.com/sns/jscode2session', // 请求微信服务器
method: 'GET',
data: {
appid: 'xxxxxxxx', // 微信appid
secret: 'xxxxxxxxxxxxx', // 微信秘钥
js_code: this.js_code,
grant_type: 'authorization_code'
},
success: (res) => {
console.log('获取信息', res.data);
this.session_key = res.data.session_key
}
});
}
});
},
getPhoneNumber(res) {
console.log(res, '---手机号回调信息')
this.phone_encryptedData = res.detail.encryptedData;
this.phone_iv = res.detail.iv;
let pc = new WXBizDataCrypt('填写微信appid', this.session_key);
try {
let data = pc.decryptData(this.phone_encryptedData, this.phone_iv);
if (data.phoneNumber !== '') {
this.phone = data.phoneNumber;
}
} catch (error) {
console.error('获取失败:', error);
}
}
}
}
</script>

<style lang="less">
.wrap {
width: 100vw;
height: 100vh;
background-color: #F1F2F6;
display: flex;
align-items: center;
justify-content: center;

.box-container {
width: 70vw;
height: 30vh;

input {
border: 2rpx solid black;
}

.action-btn {
width: 50%;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 40rpx auto;
}
}
}
</style>

大致流程是:


先通过 uni.login 拿到一个 code,用这 code 作为js_code、appid(微信小程序后台设置中获取)、secret(微信公众号后台获取的密钥)、grant_type(固定值:authorization_code) 去请求 https://api.weixin.qq.com/sns/jscode2session 这个地址,返回结果如下:


{"session_key":"zkJJOfHPYHc\/cVK2kydibg==","openid":"oHXOj5NJMH78yWdVcf6loGOL4cno"}

然后点击按钮调起微信手机号授权页:


999.png


@getphonenumber 事件的回调中获取的信息打印结果如下:


888.png


框选的信息就是我们需要的,是一个加密后的数据。


最后使用 WXBizDataCrypt 对信息进行解密,解密后就是我们需要的手机号信息了。


3-2. 前后端实现


前端代码逻辑改了下:


<script>
export default {
data() {
return {
phone: "",
}
},
methods: {
getPhoneNumber(res) {
console.log(res, '---手机号回调信息')
// 注:这里的code和前面登录返回的code是不同的
const { code } = res.detail
// 根据code去请求后端提供的接口,即可从响应数据中拿到手机号
}
}
}
</script>

后端做了哪些事情呢?


首先会去 获取接口调用凭证 ,官方文档描述如下:


777.png


// 参数说明

{
grant_type: client_credential, // 固定值
appid: '', // 填写微信小程序的appid
secret: '', // 填写微信小程序的密钥
}

返回参数为:access_token(接口凭证)expire_in(过期时间,默认为2小时)


然后再去调获取手机号接口(getPhoneNumber),


666.png


参数携带前面返回的 access_token,再加上前端传过来的 code,即可获取到手机号信息。


下面是我用 Postman 对三个接口做了测试验证:


weixin08.png


weixin07.png


weixin06.png


对比两种方式,个人建议还是采用第二种好一点,让相关的业务都在后端去处理,除此之外还有一个原因就是涉及一个安全性相关问题,前面代码中可以看到我们在请求小程序登录接口是将 appid、screct等信息放在请求参数中的,这种极易通过源码拿到,所以存在相关信息泄露问题,事实证明这种方式也是不建议使用的:


555.png


踩坑点




  1. 注意区分登陆时返回的 code 和 button 按钮获取手机号回调返回的 code 是不相同的




  2. @getphonenumber 回调函数的返回信息如果信息为:api scope is not declared in the privacy agreement ,这种是小程序的【隐私保护策略】限制的,排查下你的小程序中用户隐私保护指引设置送是否添加了相关的用户隐私类型(手机号、通讯录、位置信息等)




444.png


以上就是结合项目需求场景对获取手机号的实现做的一个记录!


作者:瓶子丶
来源:juejin.cn/post/7300036605099343926
收起阅读 »

token 和 cookie 还在傻傻分不清?

web
token 概念和作用 Token是一种用于身份验证和授权的令牌。在Web应用程序中,当用户进行登录或授权时,服务器会生成一个Token并将其发送给客户端。客户端在后续的请求中将Token作为身份凭证携带,以证明自己的身份。 Token可以是一个字符串,通常是...
继续阅读 »

token 概念和作用


Token是一种用于身份验证和授权的令牌。在Web应用程序中,当用户进行登录或授权时,服务器会生成一个Token并将其发送给客户端。客户端在后续的请求中将Token作为身份凭证携带,以证明自己的身份。


Token可以是一个字符串,通常是经过加密和签名的,以确保其安全性和完整性。服务器收到Token后,会对其进行解析和验证,以验证用户的身份并授权对特定资源的访问权限。


Token的使用具有以下特点:



  • 无状态:服务器不需要在数据库中存储会话信息,所有必要的信息都包含在Token中。

  • 可扩展性:Token可以存储更多的用户信息,甚至可以包含自定义的数据。

  • 安全性:Token可以使用加密算法进行签名,以确保数据的完整性和安全性。

  • 跨域支持:Token可以在跨域请求中通过在请求头中添加Authorization字段进行传递。


Token在前后端分离的架构中广泛应用,特别是在RESTful API的身份验证中常见。它比传统的基于Cookie的会话管理更灵活,并且适用于各种不同的客户端,例如Web、移动应用和第三方接入等。


cookie 和 token 的关系


Cookie和Token是两种不同的概念,但它们在身份验证和授权方面可以有关联。


Cookie是服务器在HTTP响应中通过Set-Cookie标头发送给客户端的一小段数据。客户端浏览器将Cookie保存在本地,然后在每次对该服务器的后续请求中将Cookie作为HTTP请求的一部分发送回服务器。Cookie通常用于在客户端和服务器之间维护会话状态,以及存储用户相关的信息。


Token是一种用于身份验证和授权的令牌。它是一个包含用户身份信息的字符串,通常是服务器生成并返回给客户端。客户端在后续的请求中将Token作为身份凭证发送给服务器,服务器通过验证Token的有效性来确认用户的身份和权限。


Cookie和Token可以结合使用来实现身份验证和授权机制。服务器可以将Token存储在Cookie中,然后发送给客户端保存。客户端在后续的请求中将Token作为Cookie发送给服务器。服务器通过验证Token的有效性来判断用户的身份和权限。这种方式称为基于Cookie的身份验证。另外,也可以将Token直接存储在请求的标头中,而不是在Cookie中进行传输,这种方式称为基于Token的身份验证。


需要注意的是,Token相对于Cookie来说更加灵活和安全,可以实现跨域身份验证,以及客户端和服务器的完全分离。而Cookie则受到一些限制,如跨域访问限制,以及容易受到XSS和CSRF攻击等。因此,在实现身份验证和授权机制时,可以选择使用Token替代或辅助Cookie。


token 一般在客户端存在哪儿


Token一般在客户端存在以下几个地方:



  • Cookie:Token可以存储在客户端的Cookie中。服务器在响应请求时,可以将Token作为一个Cookie发送给客户端,客户端在后续的请求中会自动将Token包含在请求的Cookie中发送给服务器。

  • Local Storage/Session Storage:Token也可以存储在客户端的Local Storage或Session Storage中。这些是HTML5提供的客户端存储机制,可以在浏览器中长期保存数据。

  • Web Storage API:除了Local Storage和Session Storage,Token也可以使用Web Storage API中的其他存储机制,比如IndexedDB、WebSQL等。

  • 请求头:Token也可以包含在客户端发送的请求头中,一般是在Authorization头中携带Token。


需要注意的是,无论将Token存储在哪个地方,都需要采取相应的安全措施,如HTTPS传输、加密存储等,以保护Token的安全性。


存放在 cookie 就安全了吗?


存放在Cookie中相对来说是比较常见的做法,但是并不是最安全的方式。存放在Cookie中的Token可能存在以下安全风险:



  • 跨站脚本攻击(XSS) :如果网站存在XSS漏洞,攻击者可以通过注入恶意脚本来获取用户的Cookie信息,包括Token。攻击者可以利用Token冒充用户进行恶意操作。

  • 跨站请求伪造(CSRF) :攻击者可以利用CSRF漏洞,诱使用户在已经登录的情况下访问恶意网站,该网站可能利用用户的Token发起伪造的请求,从而执行未经授权的操作。

  • 不可控的访问权限:将Token存放在Cookie中,意味着浏览器在每次请求中都会自动携带该Token。如果用户在使用公共计算机或共享设备时忘记退出登录,那么其他人可以通过使用同一个浏览器来访问用户的账户。


为了增加Token的安全性,可以采取以下措施:



  • 使用HttpOnly标识:将Cookie设置为HttpOnly,可以防止XSS攻击者通过脚本访问Cookie。

  • 使用Secure标识:将Cookie设置为Secure,只能在通过HTTPS协议传输时发送给服务器,避免明文传输。

  • 设置Token的过期时间:可以设置Token的过期时间,使得Token在一定时间后失效,减少被滥用的风险。

  • 使用其他存储方式:考虑将Token存储在其他地方,如Local Storage或Session Storage,并采取加密等额外的安全措施保护Token的安全性。


token 身份验证代码实现


服务端使用 JWT 进行 token 签名和下发


可以参考使用这个库 node-jsonwebtoken


后端代码示例 (Node.js / Express),代码简单实现如下:


const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

const secretKey = 'mysecretkey';

app.use(express.json());

app.post('/api/login', (req, res) => {
// 从请求中获取用户名和密码
const { username, password } = req.body;

// 验证用户名和密码
if (username === 'admin' && password === 'password') {
// 用户名和密码验证成功,生成Token并返回给前端
const token = jwt.sign({ username }, secretKey, { expiresIn: '1h' });
res.json({ token });
} else {
// 用户名和密码验证失败,返回错误信息给前端
res.status(401).json({ error: 'Authentication failed' });
}
});

app.get('/api/protected', verifyToken, (req, res) => {
// Token验证成功,可以访问受保护的路由
res.json({ message: 'Protected API endpoint' });
});

function verifyToken(req, res, next) {
const token = req.headers.authorization;

if (!token) {
return res.status(401).json({ error: 'Missing token' });
}

// 验证Token
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token' });
}

// Token验证通过,将解码后的数据存储在请求中,以便后续使用
req.user = decoded;
next();
});
}

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

在上述后端代码中,我们使用了jsonwebtoken库来生成和验证Token。在登录路由/api/login中,验证用户名和密码成功后,生成一个Token并返回给前端。在受保护的路由/api/protected中,我们使用verifyToken中间件来验证请求中的Token,只有通过验证的请求才能访问该路由。


当然实际开发中, 可以使用中间件来进行 jwt 的验证, 下发方式也因人而异, 可以放在 cookie 中, 也可以作为 response 返回均可, 上述代码仅作参考;


前端代码实现示范如下


前端获取到了Token后将其存储在Cookie中,并在后续请求中自动发送给后端,可以通过以下方式实现前端代码:


import React, { useState, useEffect } from 'react';

function App() {
const [token, setToken] = useState('');

useEffect(() => {
// 检查本地是否有保存的Token
const savedToken = localStorage.getItem('token');
if (savedToken) {
setToken(savedToken);
}
}, []);

const handleLogin = async () => {
// 发送请求到后端进行登录验证
const response = await fetch('http://example.com/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: 'admin', password: 'password' }),
});

if (response.ok) {
// 登录成功,获取Token并保存到前端
const data = await response.json();
setToken(data.token);
// 保存Token到本地
localStorage.setItem('token', data.token);
}
};

const handleLogout = () => {
// 清除保存的Token
setToken('');
// 清除本地保存的Token
localStorage.removeItem('token');
};

return (
<div>
{token ? (
<div>
<p>Token: {token}</p>
<button onClick={handleLogout}>Logout</button>
</div>
) : (
<button onClick={handleLogin}>Login</button>
)}
</div>

);
}

export default App;

作者:晴小篆
来源:juejin.cn/post/7299731897626443785
收起阅读 »

看完还学不会正则,快来锤我!

web
前言 各位同仁在表单验证规则,或者在验证数据的时候,是不是经常都是xxx.xxxx,然后对数据处理来处理去,最后进行后续操作……,不仅费时费力,而且消耗精神,于是乎: ps(我):要不要用用正则,但是想不起来怎么写啊,百度搜一下吧,chatGpt搜一下,贴上去...
继续阅读 »

前言


各位同仁在表单验证规则,或者在验证数据的时候,是不是经常都是xxx.xxxx,然后对数据处理来处理去,最后进行后续操作……,不仅费时费力,而且消耗精神,于是乎:


ps(我):要不要用用正则,但是想不起来怎么写啊,百度搜一下吧,chatGpt搜一下,贴上去


我:欧哟?刚刚好啊,效果还怪好的嘞,哈哈哈,天助我也!


这还是好的情况,刚刚好符合,要是不好……


ps(我):服了,这正则不得行,chatGpt搜的错的,自己写吧又不会,找吧又没有……
我:乖乖写if else吧


又是啃哧啃哧,耗时耗力……可能大家伙都是差不多的哈,谁也别说谁(大佬除外)。本着多一事不如少一事的原则,直接学习一波!


js正则表达式


正则(正则表达式)是一种用于描述文本模式的工具,它通过使用特定的符号和语法规则来定义一个字符串的模式。正则表达式通常由各种字符和特殊元字符组成,用于进行字符串匹配、查找、替换和验证等操作。


使用正则表达式,可以执行以下操作:



  1. 模式匹配:正则表达式可以用于查找和匹配具有特定模式的字符串。通过定义一个模式,可以搜索和识别符合该模式的字符串。

  2. 字符串查找与替换:正则表达式可以用于在文本中进行字符串查找和替换。通过指定要查找或替换的模式,可以对目标字符串进行修改和处理。

  3. 数据验证:正则表达式可以用于验证用户输入或其他数据的格式和有效性。例如,可以使用正则表达式验证电子邮件地址、电话号码、日期等的格式是否符合预期。

  4. 文本提取:在文本处理中,可以使用正则表达式从大量文本数据中提取出所需的信息。例如,可以使用正则表达式从日志文件中提取特定的时间戳或关键字。

  5. 数据清洗与转换:使用正则表达式,可以进行文本数据的清洗和转换。可以根据模式匹配和替换规则,删除非法字符、规范化日期格式、提取关键信息等。


正则表达式提供了一种强大和灵活的文本处理工具,它被广泛应用于编程语言、文本编辑器、数据处理工具等各种软件中。虽然正则表达式的语法可能会显得复杂,但掌握它可以极大地提高对文本模式处理的能力。


应用


正则表达式在计算机科学和文本处理中具有广泛的应用。以下是一些常见的正则表达式应用:



  • 模式匹配:正则表达式可用于检测字符串是否与特定模式匹配。例如,可以使用正则表达式来验证电子邮件地址、检查电话号码的格式、识别日期等。

  • 字符串搜索与替换:正则表达式可以用于在文本中搜索特定的模式,并进行替换或提取。这对于在大量文本中进行批量操作非常有用,如查找和替换文本文件中的特定单词或短语。

  • 表单验证:在前端开发中,可以使用正则表达式验证用户输入的表单数据。例如,验证用户名是否只包含字母和数字、检查密码是否符合指定的复杂度要求等。

  • URL路由:许多Web框架使用正则表达式来解析URL路由和处理动态路由。它们通过正则表达式匹配URL字符串并将其映射到相应的处理程序或控制器。

  • 日志分析:使用正则表达式可以解析和提取日志文件中的有用信息。例如,可以使用正则表达式从服务器日志中提取IP地址、日期时间戳、错误消息等。

  • 数据清洗与转换:正则表达式可用于清洗和转换数据,如从多种格式的文本数据中提取特定字段、规范化日期格式、去除特殊字符等。

  • 编程工具与编辑器:许多编程工具和文本编辑器支持正则表达式搜索和替换功能。这使得开发人员能够更灵活地进行代码重构和批量修改操作。


创建正则


js 中内置了正则表达式对象 RegExp,我们要创建一个正则表达式,可以:



  • 第一种创建方式,接收patternmodifiers两个参数


// 构造函数RegExp
var regex = new RegExp(pattern, modifiers);
// 构造函数创建
var regex = new RegExp('xyz', 'i');
// 等价于 字面量创建
var regex = /xyz/i;

RegExp 构造函数接收两个参数,pattern 描述了表达式的模式,为字符串,modifiers 是正则表达式的修饰符,用于执行区分大小写和全局匹配。



  • 第二种创建方式,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。


var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;

在构造函数创建时 pattern 是正则字符串,字面量创建时,pattern 是一个类似 /正则规则/ 表达式,是放在双斜杠里的。
modifiers有三个值,分别为


描述
i不区分大小写。在匹配时忽略大小写
g全局匹配。查找所有可能的匹配,而不仅仅是第一个匹配。
s单行匹配。启用单行模式,使点号(.)可以匹配包括换行符在内的任意字符。
u启用完整的 Unicode 匹配支持,会正确处理四个字节的 UTF-16 编码。
m多行匹配。启用多行模式,^ 和 $ 可以匹配行的开始和结束。

例如,使用 ig 修饰符创建一个忽略大小写和全局匹配的正则表达式:


javascript
var pattern = new RegExp("pattern", "ig");

或者使用字面量表示法也可以传递修饰符:


var pattern = /pattern/ig;

正则对象的方法


正则表达式对象通常提供一组方法,用于在字符串上执行不同的操作。以下是一些常见的正则表达式对象的方法:




  1. test(string): 检测指定字符串是否与正则表达式匹配。返回一个布尔值,表示是否找到匹配项。一般开发当中,我们使用这种方法较多。


    const regex = /apple/;
    console.log(regex.test("I love apples")); // true
    console.log(regex.test("I prefer oranges")); // false



  2. exec(string): 在给定字符串中搜索匹配项,并返回一个数组或 null。数组包含有关匹配项的详细信息,如匹配的子字符串、捕获组等。


    const regex = /\d+/;
    console.log(regex.exec("I have 100 apples")); // ["100"]
    console.log(regex.exec("There are no numbers here")); // null



  3. match(regexp): 在字符串中查找与正则表达式匹配的内容,并返回一个数组或 null。类似于 exec() 方法,但是 match() 是在字符串上调用,而不是在正则表达式上调用。


    const string = "I have 100 apples";
    const regex = /\d+/;
    console.log(string.match(regex)); // ["100"]



  4. search(string): 在字符串中搜索与正则表达式匹配的内容,并返回匹配项的索引。如果没有找到匹配项,则返回 -1。


    const string = "I prefer oranges";
    const regex = /oranges/;
    console.log(string.search(regex)); // 8



  5. replace(regexp, replacement): 替换字符串中与正则表达式匹配的部分。可以将匹配项替换为指定的字符串或使用函数进行替换。


    const string = "I like cats and dogs";
    const regex = /cats/;
    const replacement = "birds";
    const newString = string.replace(regex, replacement);
    console.log(newString); // "I like birds and dogs"



  6. split(regexp): 将字符串分割为由正则表达式匹配的子字符串组成的数组。正则表达式定义了分隔符。


    const string = "apple,banana,orange";
    const regex = /,/;
    const parts = string.split(regex);
    console.log(parts); // ["apple", "banana", "orange"]



正则规则


分为基本字符匹配;元字符匹配,如\w;锚点匹配指定匹配发生的位置, 如^ 表示匹配行的开头;量词和限定符, 如*; 分组和捕获();零宽断言:正向肯定断言 (?=...):匹配满足断言条件的位置,但不会消耗字符;


接下来一一进行介绍。


基本字符匹配


匹配字面量字符/ /


如果想在javaScript当中直接匹配java,可以直接在我们的字面量当中写入想要匹配的值,即java直接进行匹配。


正则: /java/


可以匹配的不能匹配的
javascriptJavascript
javajaava

字符组[ ]


如果不仅仅想要匹配java还想要匹配Java,那光光/java/是不够的。这时候还需要用到我们的字符组。


正则:/[Jj]ava/


可以匹配的不能匹配的
javascriptjaava
Javascriptjvav

[]匹配规则当中,目标字符可以匹配中括号里面的任意一个字符即可,转为javaScript语言就是 ||的意思。观察两个目标字符串,java与Java的区别也仅仅是首字母不同,那么只需要兼容开头的大小字母即可。


拓展

若是想匹配java Java JAva,正则需要如何编写?通过观察各个字符当中的差别,即前两个字母的可能性都可能为大小写,便得出前两个位置的匹配使用字符组即可。


正则:/[J][Aa]va/


字符组区间 -


如果说只想匹配前缀为123,后面是二十六个字母当中任何一个的字符怎么办?


这简单,刚刚学完字符组,我直接一手/123[a,b,c,d....]/把二十六个字母全部列一遍,话虽如此,但大可不必!


此处若是可选匹配字母过多的话,可直接使用字符组区间连接


正则: /123[a-zA-Z]/


可以匹配的不能匹配的
123a123
123B12345

同时还可以匹配多个数字,比如我只想匹配[3-9]的数字,那么也可以使用连接符


正则123[3-9][a-zA-Z]


可以匹配的不能匹配的
1233a123a9
1236B123B

字符组取反:[^]


有的时候你可能也不想匹配某些字符,比如只晕小写字母,那么这个时候你可以对你所要匹配的字符组进行取反,那就匹配不到了。


正则:/[^a-z]/


可以匹配的不能匹配的
1233ABCDEabcde
12345678adasd
123adasdadasd


注意: 此处需要全部为小写字母test匹配结果才是false,若字符包含其他的字符,test的匹配结果仍然为true。



const pattern = /[^a-z]/ // 表示的意思为所有字符都不是小写
const string = '123adasd' // 此处还有数字
pattern.test(string) // true

元字符匹配


日常开发当中,元字符单独使用的情况并不多,更多的是跟随后续的量词一块使用,最终形成限定字符格式的正则。


单点 .


. 是一个特殊的元字符,可以用于匹配除了换行符 \n(或其他行终止符,如 \r\n)之外的任意单个字符。


正则:/./


可以匹配的不能匹配的
1\n(换行)
a\r(回车)

数字 \d


\d 可以匹配任意一个数字字符,包括 0 到 9 的数字。


字符 \w


用于匹配字母字符、数字和下划线。


具体来说,\w 匹配以下字符:



  • 小写字母(a-z)

  • 大写字母(A-Z)

  • 数字(0-9)

  • 下划线(_)


空白符 \s


用于匹配空白字符



  • 空格符(Space)

  • 制表符(\t)

  • 换行符(\n)

  • 回车符(\r)

  • 垂直制表符(\v)

  • 换页符(\f)



注意:如果说想要匹配正则当中的匹配规则符号,例如只想匹配单点字符.,则需要使用反斜杠进行转义,即/\./ 任何匹配正则当中具有意义的字符都需要进行转义。



量词


量词用于指定模式重复出现的次数。允许你匹配一定数量的字符或子模式,是正则当中见怪不怪的玩意。与上述字符相互搭配,能获得意想不到的结果。


量词 {}


用于匹配前面的字符或子表达式指定的精确的重复次数。


比如需要匹配重复多个字符,如需要匹配出现两次a的字符串。


正则:/a{2}/


可以匹配的不能匹配的
aaabab
aabbabb

但是我只知道会出现a字符,可能是两到三个呢?这个时候就可以使用区间来表示,囊括出现的次数。


正则:/a{2,3}/


可以匹配的不能匹配的
aaabbbb
aabbabb
aaababab

如果只知道出现一次,但是不清楚具体有几次,便直接可以不写右区间,表示至少出现n次,比如下面的正则就表示至少出现3次a


正则:/a{3,}/


可以匹配的不能匹配的
aaabbbb
baaaaaabb

量词 +


用于匹配前面的字符或子表达式至少一次或多次出现。
实际上,+的表现形式,还可以用{1,}来表示


正则: /a+/ 等价于 /a{1,}/


可以匹配的不能匹配的
abbbb
aabbb

量词 *


用于匹配前面的字符或子表达式出现0次或多次出现。实际上,*的表现形式,也可以用{0,}来表示


正则: /a*/ 等价于 /a{0,}/


可以匹配的不能匹配的
abbbb
aabbb

量词 ?


用于匹配前面的字符或子表达式零次或一次。实际上,*的表现形式,也可以用{0,1}来表示


正则: /a?b/ 等价于 /a{0,1}b/


可以匹配的不能匹配的
babcde
bad


正则表达式的贪婪匹配和非贪婪匹配是用来描述匹配模式时的两种不同行为。
贪婪匹配是指正则表达式尽可能地匹配更长的文本片段。它会尽量多地消耗输入字符串,并尝试匹配满足整个正则表达式模式的最长可能结果,是默认的行为,
反之,非贪婪匹配(也称为懒惰匹配或最小匹配) 则是指正则表达式尽可能地匹配更短的文本片段。它会尽量少地消耗输入字符串,并尝试匹配满足整个正则表达式模式的最短可能结果。
通常非贪婪匹配通过在正则字符串后面加?号来表示。



示例
正则表达式 /a+/,它表示匹配一个或多个连续出现的字符 "a"。


对于字符串 "aaa",贪婪匹配将尽量匹配更长的连续的 "a" 字符串,在这种情况下会匹配整个字符串 "aaa"。


使用非贪婪匹配需要在量词后面添加 ?。正则表达式 /a+?/ 表示非贪婪匹配,将匹配一个或多个连续出现的字符 "a",但只尽量匹配最短的结果。非贪婪匹配将尽量匹配最短的连续的 "a" 字符串。在这个例子中,非贪婪匹配会匹配第一个 "a" 字符,因为它是最短的满足正则表达式模式的子串。


锚点匹配


锚点是正则表达式中的特殊字符,用于匹配字符串的位置而不是具体的字符,可用于指定匹配发生的位置,常用的锚点有^$\b


^ 起始位置


表示匹配行的开头。下面正则表示匹配以a为开头的字符


正则:^a


可以匹配的不能匹配的
ada
abbc

$ 结束位置


表示匹配行的结尾。下面正则表示匹配以a为结尾的字符


正则:a$


可以匹配的不能匹配的
aab
dabc

\b 边界


表示匹配单词边界。下面正则表示匹配独立的单词


正则:/\bapple\b/


可以匹配的不能匹配的
I love applepineapple
applepinapple

\b还有很多其他的应用,比如



  • \b\w+\b:匹配一个或多个连续的单词字符,可以用来分割句子为单词数组。

  • \b\d{4}\b:匹配仅包含4位数字的字符串


在转义\b的时候需要使用\\b


分组和捕获:


分组 ()


括号 ( ):用于将一组模式作为单个单元进行匹配,并将其视为一个分组。


比如,我要匹配以jstsjava后缀的文件
正则:/.*\.(js|ts|java)/


可以匹配的不能匹配的
index.js1.png
1.ts2.jpg
calss.java3.mp3

再比如 正则:/(ab){1,}/,可以匹配一个或出现多个连续的ab,利用分组实现的


可以匹配的不能匹配的
abaa
ababba

捕获组


通过圆括号捕获分组内的内容,可以在后续操作中进行引用。


可能这比较难理解,我们举例说明,比如,我们有1-82-2这种类型的数据,我们可以使用正则的分组将两边的数据包裹,并使用exec进行捕获。分组符号的数据就是把这些想要捕获的数据标记出来。


如果我们想要 () 的分组能力,但是又不想捕获数据,可以使用 (?:) 表达式。可以提高正则表达式的性能和简洁性。


image.png


零宽断言



  1. 正向肯定预查(?=...):表示在当前位置后面,如果满足括号内的表达式,则继续匹配成功。

  2. 正向否定预查(?!...):表示在当前位置后面,如果不满足括号内的表达式,则继续匹配成功。

  3. 反向肯定预查(?<=...):表示在当前位置前面,如果满足括号内的表达式,则继续匹配成功。

  4. 反向否定预查(?<!...):表示在当前位置前面,如果不满足括号内的表达式,则继续匹配成功。



  • /(?=\d)\w+/ 匹配由数字紧随其后的单词字符。
    | 可以匹配的 | 不能匹配的 |
    | --- | --- |
    | 1 | w |
    | 1w | ww |


为什么这里能匹配1呢?1首先同样属于字符,其次还是数字,在断言的时候,不消耗字符,符合数字随其后的规则(本身)



  • /(?<!\d)\w+/ 匹配没有数字紧随在前面的单词字符。(js不支持)



js并不支持反向预查,只支持正向预查。这是因为正向预查在匹配时,可以当前位置后面的内容进行断言判断,如果不符合预期,则无法继续匹配成功。这种类型的预查可以通过回溯来实现。


然而,反向否定预查需要从当前位置回溯到前面的位置进行条件判断,这就使得正则引擎需要逆序地扫描前面的内容,增加了匹配的复杂度。因此,实现反向否定预查的算法相对更为复杂,并且可能导致性能下降。


反向否定预查在某些特定情况下可以被其他模式替代,比如使用捕获组结合后续的处理代码来达到类似的效果。



正则表达式大全



  1. 邮箱验证


/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(.\w{2,3})+$/

^\w+匹配以字符开头,([.-]?\w+)* 部分出现两次,品牌包含一个或多个由-或点.连接的部分,(.\w{2,3})+匹配域名




  1. URL 验证:包括 HTTP 和 HTTPS 协议。


/^(https?://)?[\w-]+(.[\w-]+)+[/#?]?.*$/


  1. 身-份-证号码验证:验证中国大陆身-份-证号码的有效性。


低配:
/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/

高配:
身-份-证号匹配
/^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|3[0-1])\d{3}[0-9Xx]$/


  • ^[1-9]\d{5}:匹配 6 位行政区划代码、

  • (19|20)\d{2}:年份,匹配以 19 或 20 开头的四位数字、

  • (0[1-9]|1[0-2]):月份,取值范围为 01 到 12、

  • (0[1-9]|[1-2]\d|3[0-1]):日期,取值范围为 01 到 31、

  • \d{3}:顺序码,任意三位数字、

  • [0-9Xx]:校验码,可以是数字或字母 X 或 x、



  1. 数字验证:用于验证一个字符串是否只由数字组成。


`/^\d+$/`


  1. 字母验证:用于验证一个字符串是否只由字母组成。


`/^[a-zA-Z]+$/`


  1. 小数验证:匹配的数字可包含小数点,此处转义了小数点,


/^\d+(\.\d+)?$/


  1. 整数验证(包括负数):用于验证一个字符串是否为整数,可以包含正负号。


`/^[-+]?\d+$/`


  1. IP 地址验证: 用于验证 IPv4 地址的有效性。


/^((25[0-5]|2[0-4]\d|[01]?\d\d?).){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/


  1. 手机号码验证:


低配版本,仅表示11位数字


```
^\d{11}$ 低配版本,11位数字
```

高配版本


```
/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/

如果不想这么复杂,可以写为
/^1[3-9]d{9}$/


还能匹配*特殊符号的,但是会失去匹配11位数功能
/^1[3-9]\d{1}(?:\*{1,})*\d+$/
```

如果确定符号个数,可改为/^1[3-9]\d{1}((?:\*{4})|\d{4})\d{4}$/,就能匹配固定11位数的号码



  • 可以匹配152702365242

  • 可以匹配152****65242


10.密码复杂度要求




  • 8位任意密码


    /^.{8,}$/



  • 包括至少8个字符,包含大写字母、小写字母和数字


    /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/



?=为正向断言,判断条件是否符合.*\d,即任意字符但是需要出现一个数字,其余类似


这个正则表达式用于强制密码应至少包含一个数字(?=.*\d)、一个小写字母(?=.*[a-z])和一个大写字母(?=.*[A-Z]),并且长度至少为8个字符.{8,}



  • 包括至少8个字符,包含大写字母、小写字母和数字,包括特殊字符
    /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*?]).{8,}$/



11.以8结尾,且位数在6位以内的数字


/^\d{0,5}8$/


  1. 时间匹配,匹配时分,年月日的匹配建议还是按照Date的API,正则在匹配闰年的二月份时候无法匹配


/^(?:[01]\d|2[0-3]):(?:[0-5]\d)$/


  • 可以匹配09:10 12:12 23:01 23:59


/^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$/



  • (?!0000) 表示后面不能跟着四个0,即年份不能为0000。

  • [0-9]{4} 表示匹配四个数字,即年份的格式为四位数字。

  • - 表示匹配“-”字符。

  • (?:…) 表示非捕获型分组,用于提高正则表达式的效率。

  • (?:0[1-9]|1[0-2]) 表示匹配01-12月份,其中0[1-9]表示01-09月份,1[0-2]表示10-12月份。

  • (?:0[1-9]|1[0-9]|2[0-8]) 表示匹配01-28日,其中0[1-9]表示01-09日,1[0-9]表示10-19日,2[0-8]表示20-28日。

  • (?:0[13-9]|1[0-2])-(?:29|30) 表示匹配01、03、05、07、08、10、12月份的29或30日。

  • (?:0[13578]|1[02])-31 表示匹配01、03、05、07、08、10、12月份的31日。

  • (?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29 表示匹配闰年的2月29日,其中[0-9]{2}表示匹配两位数字的年份,(?:0[48]|[2468][048]|[13579][26])表示匹配闰年的年份,即能被4整除但不能被100整除,或者能被400整除。

  • $ 表示匹配字符串的结束位置。



  1. 用户名:4-10位的用户名,包含下划线、连接符


/^[a-zA-Z0-9_-]{4,10}$/

总结


以上就是目前能想到的常用的正则,大家如果也有或者说常用的正则,也可以在评论区反馈,谢谢各位!


作者:原野风殇
来源:juejin.cn/post/7299376141451411490
收起阅读 »

耗时七天,我写完了自己的第一个小程序

web
一入红尘深似海。 自2016年参加工作时申请了第一张信用卡,至今已有七年矣。在这七年之中,自己信用卡的数量由1张逐渐增加到12张,后又慢慢减少到如今的5张。七年的卡海浮沉让我从初入社会的一无所有,到如今的负债累累。 当然,这篇文章并不是来记录自己七年的负债之旅...
继续阅读 »

一入红尘深似海。


自2016年参加工作时申请了第一张信用卡,至今已有七年矣。在这七年之中,自己信用卡的数量由1张逐渐增加到12张,后又慢慢减少到如今的5张。七年的卡海浮沉让我从初入社会的一无所有,到如今的负债累累。


当然,这篇文章并不是来记录自己七年的负债之旅,而是在经历了多年“钱都去哪儿了?”的内心呼唤后的心灵觉醒:还是要有个账本记账啊!


我的需求并不复杂:



1、可以快速的知道如今自己卡的总额度是多少,还需要还的欠款是多少(清楚负债情况)


2、可以快速知道每张卡的可用额度是多少,账单日是哪天(便于刷卡时明确知道该刷哪张卡,不至于出现今天刚刷了卡,明天就出了账单要还的现象)


3、可以知道每个月刷卡的总手续费是多少(清楚损益,明白每个月的损耗)


4、记录收支(了解每一分钱都去了哪里)



基于以上四个简单的需求,在尝试现在市面上十几款记账软件后,我惊奇的发现:竟然没有一款合适的软件可以满足我的需求!


于是,我做了一个【XXXX】的决定:自己来写一个工具吧!


然后,就诞生了我发布的第一个小程序:了账


小程序简介


了(liao)账是一款简洁的记账小程序。了账中的了字,是明了之意,清楚明白自己的账目,亦是了结之意,祝愿各位卡友早日上岸。


了账的页面相对简洁,有【未登录】【已登录】两种状态展示,如下图所示:


【未登录】


截屏2023-10-23 08.19.09.png


【已登录】


截屏2023-10-23 08.27.30.png


了账只有账户账单两个tab页,分别用来展示当前账户信息和查看收支记录。


账户页面展示了用户(目前只有作者本人😄)较为关心的几个数据:【当前额度】、【可用额度】、【现金额度】、【信用卡总览】、【当前账户】。


账单页面除了查看每一笔收支记录外,在顶部也展示了当月总出账、总入账信息。


截屏2023-10-29 14.53.05.png


新增账户


用户可通过【新增】按键创建账户,在账户页面,顶部账户信息会随之动态改变。如下图所示:


截屏2023-10-29 15.05.44.png


在新增页面,用户可点击账户类型修改新增账户的类型,目前【了账】共包含【信用卡】、【储蓄卡】、【支付宝】、【微信】、【其他】共五类账户。除信用卡外,其余四类账户额度统一归类为【现金额度】。


信用卡除了【固定额度】之外有时会给一部分【临时额度】,因此,在新增账户页面,除了【固定额度】之外,添加了【当前额度】字段。【当前额度】是包含【固定额度】和【临时额度】的账户总额度。


新增收支


当用户创建过账户后,就可以点击【账户】页面右下角【记一笔】浮块创建收支记录,并在【账单】页面查看。相应的,账户页面所展示的【账户信息】也会随之动态改变。如下图所示:


截屏2023-10-29 15.24.00.png


在记录收支时,不同的账户类型可选的账单类型也不相同。如:信用卡账户下可选择的账单类型为【日常支出】、【个人刷卡】、【账单还款】,储蓄卡账户下可选择的账单类型为【日常支出】、【日常收入】、【转账支出】、【转账收入】,支付宝账户微信账户其他账户则多出【提现】类型可供选择。如下图所示:


截屏2023-10-29 15.41.54.png


当账单类型为【日常支出】时,则须选择支出类型。目前共有【食】、【行】、【衣】、【住】、【娱乐】、【其他支出】六类支出可供选择。如下图所示:


截屏2023-10-29 15.35.20.png


信用卡账户账单类型为【个人刷卡】,以及支付宝账户微信账户其他账户账单类型为【提现】时,则需填写【收款金额】。收款账户为除【信用卡账户】外的其他账户,收款金额为除去手续费之外的实际到账金额。如下图所示:


截屏2023-10-29 15.53.45.png


账单的编辑、删除和账户的编辑、删除


用户可通过左滑对当前账户及当前收支进行编辑、删除。当收支被删除后,账户信息将会回退该笔收支。当账户被删除后,该账户下的所有收支将不可被编辑、删除。如下图所示:


截屏2023-11-11 12.27.15.png


账户详情和账单详情


点击每个账户和账单,可进入详情页,查看详情信息。如下图所示:


截屏2023-11-11 14.25.39.png


写在最后


账本只是工具,最主要的还是要诸位卡友调整好心态,量入为出。祝愿各位早日上岸!!!


写代码用了7天,备案发布将近一个月!!!最后上线认证居然还收了30块巨款!!!至今仍未明白:经历了实名注册小程序号,实名IPC备案后,最后上线认证的意义在哪里?难道只为承袭小马哥一贯的氪金传统?


作者:凡铁
来源:juejin.cn/post/7299733832413069347
收起阅读 »

看明白两个案例,秒懂事件循环

web
事件循环的任务队列包括宏任务和微任务 执行顺序就是:同步代码 -> 第一轮微任务 -> 第一轮宏任务 -> 第二轮微任务 ->... 宏任务有:setTimeout, setInterval, setImmediate, I/O, UI...
继续阅读 »

事件循环的任务队列包括宏任务微任务


执行顺序就是:同步代码 -> 第一轮微任务 -> 第一轮宏任务 -> 第二轮微任务 ->...


宏任务有:setTimeout, setInterval, setImmediate, I/O, UI rendering。


微任务有:process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)


两大原则:



  1. setTimeout和setInterval同源,且均优先于setImmediate执行

  2. nextTick队列会比Promie.then方法里面的代码先执行


简单案例


setTimeout(function() {
console.log('timeout1'); // 5-第一轮宏任务
})

new Promise(function(resolve) {
console.log('promise1'); // 1-同步代码
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2'); // 2-同步代码
}).then(function() {
console.log('then1'); // 4-第一轮微任务
})

console.log('global1'); // 3-同步代码


/*
promise1
promise2
global1
then1
timeout1
*/


综合案例


console.log('golb1'); // 1-同步代码

setTimeout(function() {
console.log('timeout1'); // 3.1-第一轮宏任务
process.nextTick(function() {
console.log('timeout1_nextTick'); // 3.3-第二轮微任务
})
new Promise(function(resolve) {
console.log('timeout1_promise'); // 3.2-第一轮宏任务
resolve();
}).then(function() {
console.log('timeout1_then') // 3.4-第二轮微任务
})
})

setImmediate(function() {
console.log('immediate1'); // 3.1-第一轮宏任务
process.nextTick(function() {
console.log('immediate1_nextTick'); // 3.3-第二轮微任务
})
new Promise(function(resolve) {
console.log('immediate1_promise'); // 3.2-第一轮宏任务
resolve();
}).then(function() {
console.log('immediate1_then') // 3.4-第二轮微任务
})
})

process.nextTick(function() {
console.log('glob1_nextTick');// 2.1-第一轮微任务
})
new Promise(function(resolve) {
console.log('glob1_promise');// 1-同步代码
resolve();
}).then(function() {
console.log('glob1_then') // 2.2-第一轮微任务
})

setTimeout(function() {
console.log('timeout2'); // 3.1-第一轮宏任务
process.nextTick(function() {
console.log('timeout2_nextTick'); // 3.3-第二轮微任务
})
new Promise(function(resolve) {
console.log('timeout2_promise'); // 3.2-第一轮宏任务
resolve();
}).then(function() {
console.log('timeout2_then') // 3.4-第二轮微任务
})
})

process.nextTick(function() {
console.log('glob2_nextTick');// 2.1-第一轮微任务
})
new Promise(function(resolve) {
console.log('glob2_promise');// 1-同步代码
resolve();
}).then(function() {
console.log('glob2_then')// 2.2-第一轮微任务
})

setImmediate(function() {
console.log('immediate2'); // 3.1-第一轮宏任务
process.nextTick(function() {
console.log('immediate2_nextTick'); // 3.3-第二轮微任务
})
new Promise(function(resolve) {
console.log('immediate2_promise'); // 3.2-第一轮宏任务
resolve();
}).then(function() {
console.log('immediate2_then') // 3.4-第二轮微任务
})
})

/*
(1-同步代码)
golb1
glob1_promise
glob2_promise
(2-第一轮微任务)
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
(3-第一轮宏任务)
(setTimeout)
timeout1
timeout1_promise
timeout1_nextTick
timeout1_then
timeout2
timeout2_promise
timeout2_nextTick
timeout2_then
(setImmediate)
immediate1
immediate1_promise
immediate1_nextTick
immediate1_then
immediate2
immediate2_promise
immediate2_nextTick
immediate2_then
*/


注:在Node 11前,Node的事件循环会与浏览器存在差异,以上面案例中的两个setTimeout为例:


//在Node 11前
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
// 在Node11后和浏览器
timeout1
timeout1_promise
timeout1_nextTick
timeout1_then
timeout2
timeout2_promise
timeout2_nextTick
timeout2_then

即在同一类任务分发器(如:多个setTimeout),在Node 11前,会先执行所有的nextTick,再到Promise.then;而在Node11后和浏览器,都是依次执行每个setTimeout,在同一个setTimeout里面先执行所有nextTick,再到Promise.then。


Refs:


mp.weixin.qq.com/s/m3a6vjp8-…


作者:星辰_Stars
来源:juejin.cn/post/7298325881731219496
收起阅读 »

面试题:小男孩毕业之初次面试

web
前言 看到身边的同学渐渐的都有了一些面试之后,我逐渐感到了焦虑,甚至都有些对自己感到不自信,之后在上周三的上午,终于时来运转,先是梭翱打电话来,之后就是美云和琻瑢那边的简历初筛通过,通知我面试,之后按照自己的回忆写下了一些感悟与题目,希望对你们有所帮助。 浙江...
继续阅读 »

前言


看到身边的同学渐渐的都有了一些面试之后,我逐渐感到了焦虑,甚至都有些对自己感到不自信,之后在上周三的上午,终于时来运转,先是梭翱打电话来,之后就是美云和琻瑢那边的简历初筛通过,通知我面试,之后按照自己的回忆写下了一些感悟与题目,希望对你们有所帮助。


浙江杭州(实习 130-160/天)



这是我的第一场面试,面试官问的都是vue的问题。这场面试全程懵逼下来的,因为我前面基本都在准备js和css方面,vue方面也就瞄了几眼,结果就是和面试官疯狂的扯。面试完之后反思,在自我介绍中一定要讲清楚自己使用了是vue2还是vue3,不熟悉或者面试前没准备好的知识点一定不要讲出来,全程懵下来血的教训。然后也是电话面试,所以在听面试官老师的问题方面可能有点费力。在看面试题的时候,不要死记硬背,可以根据自己熟悉的语句自己表达出来就行。



1. 说一下vue2和vue3生命周期的实现和它们的不同点?


每个Vue实例在创建时都会经过一系列的初始化过程,vue的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件


Vue2的生命周期函数




  • create阶段:vue实例被创建


    beforeCreate: 创建前,此时data和methods中的数据都还没有初始化


    created: 创建完毕,data中有值,未挂载




  • mount阶段: vue实例被挂载到真实DOM节点


    beforeMount:可以发起服务端请求,取数据


    mounted: 此时可以操作DOM




  • update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染


    beforeUpdate:更新前


    updated:更新后




  • destroy阶段:vue实例被销毁


    beforeDestroy:实例被销毁前,此时可以手动销毁一些方法


    destroyed:销毁后




上述生命周期钩子函数中,beforeCreate和created钩子函数在组件创建时只会执行一次,而beforeMount、mounted、beforeUpdate和updated钩子函数则会在组件的数据发生变化时多次执行。在组件销毁时,beforeDestroy和destroyed钩子函数也只会执行一次。


Vue3的生命周期函数




  • setup() : 开始创建组件之前,在 beforeCreate 和 created 之前执行,创建的是 data 和 method




  • mount阶段: vue实例被挂载到真实DOM节点


    onBeforeMount() : 组件挂载到节点上之前执行的函数;


    onMounted() : 组件挂载完成后执行的函数;




  • update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染


    onBeforeUpdate(): 组件更新之前执行的函数;


    onUpdated(): 组件更新完成之后执行的函数;




  • unmount阶段:vue实例被销毁


    onBeforeUnmount(): 组件卸载之前执行的函数;


    onUnmounted(): 组件卸载完成后执行的函数;




在Vue3中,beforeDestroy钩子函数被废弃,取而代之的是onUnmounted钩子函数。与Vue2不同,onUnmounted钩子函数在组件卸载之后调用,而不是在组件销毁之前调用。此外,Vue3还新增了一个onErrorCaptured钩子函数,用于处理子孙组件抛出的错误。


不同


1. vue3和vue2的生命周期函数名称


在Vue2中,我们熟悉的生命周期函数有:beforeCreate、created、beforeMountmounted、beforeUpdate、updated、 beforeDestroy、destroyed。而在Vue3中,这些函数名称被进行了重命名,变成了:beforeCreate->setup,created->setup,beforeMount->onBeforeMount,mounted->onMounted,beforeUpdate->onBeforeUpdate,updated->onUpdated,beforeUnmount ->onBeforeUnmount,unmounted ->onUnmounted。


重命名的原因是为了更好地反映生命周期的不同阶段,方便开发者进行理解和使用。


常用生命周期对比如下表所示。


vue2vue3
beforeCreate使用 setup()
created使用 setup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted

2. 新增和废弃生命周期函数


Vue3为我们提供了一些新的生命周期函数,这些函数可以帮助我们更好地管理组件,Vue3废弃了beforeDestroy钩子函数,并且新增了生命周期函数。这些新的生命周期函数分别是:


onRenderTracked:当渲染跟踪或依赖项跟踪时被调用。


onRenderTriggered:当渲染时触发其他渲染时,或者当在当前渲染中延迟调度的作业时被调用。


onErrorCaptured:当子组件抛出未处理的错误时被调用。这些新的生命周期函数可以帮助我们更好地调试、优化组件,提升应用的性能。


3. 使用hook函数代替生命周期函数


Vue3引入了新的API——Composition API,通过这个API可以使用hook函数来代替生命周期函数。 Composition API可以让我们更好地管理代码逻辑,将不同的功能划分为不同的小函数,便于维护和复用。hook函数在组件中的调用顺序与生命周期函数类似,但是更加灵活,可以根据需要进行组合和抽离。


4.v-if和v-for的优先级不同


vue2生命周期执行过程


生命周期.png


vue3生命周期执行过程


image.png


2. Vue2和Vue3数据更新时有什么不一样?


Proxy 替代 Object.defineProperty:在Vue2中,使用Object.defineProperty来拦截数据的变化,但是该方法存在一些缺陷,比如不能监听新增的属性和数组变化等。Vue3中使用了ES6中的Proxy来拦截数据的变化,能够完全监听数据变化,并且能够监听新增的属性。


批量更新:Vue2中,在数据变化时,会立即触发虚拟DOM的重渲染,如果在一个事件循环中连续修改多个数据,可能会造成性能问题。而Vue3中,使用了更高效的批量更新策略,会在下一个事件循环中统一处理数据变化,提高了性能。


更快的响应式系统:Vue3中使用了更快的响应式系统,能够更快地追踪依赖关系,并在数据变化时更快地更新视图。此外,Vue3还对Reactivity API进行了优化,使得开发者能够更灵活地使用响应式数据。


Composition API:Vue3中引入了Composition API,可以更好地组织代码逻辑,也可以更好地处理数据更新。通过使用setup函数和ref、reactive等函数,能够更方便地对数据进行监听和修改。


3. 为什么vue中更改对象和数组时,有时候页面没有进行更新




  1. 对象或数组未在初始时声明为响应式:在Vue中,只有在初始时声明为响应式的对象和数组才能进行监听和更新。如果在初始时没有声明为响应式,那么更改对象或数组时,Vue无法检测到变化,从而无法进行更新。




  2. 直接更改对象或数组的属性或元素:在Vue中,如果直接更改对象或数组的属性或元素,Vue无法检测到变化。因此,应该使用Vue提供的响应式方法来更改对象或数组的属性或元素,例如Vue.setVue.$set方法。




  3. 变异方法不会触发更新:Vue会对一些常用的数组变异方法进行封装,使其成为响应式的,例如pushpopshiftunshiftsplicesortreverse方法。但是,如果使用不在这个列表中的变异方法来更改数组,Vue就无法检测到变化。因此,应该尽可能使用Vue封装过的变异方法。




  4. 异步更新:在Vue中,更新是异步的。当数据发生变化时,Vue会将更新推迟到下一个事件循环中。因此,如果在一个事件循环中进行多次数据更改,Vue只会进行一次更新。如果需要在一次事件循环中进行多次数据更改,请使用Vue.nextTick方法。




总之,为了确保Vue可以正确地监听和更新对象和数组,应该在初始时将它们声明为响应式,避免直接更改对象或数组的属性或元素,尽可能使用Vue提供的响应式方法,避免使用不在Vue封装列表中的变异方法,以及注意异步更新的特性。


4. 你在项目里面是怎么使用vuex/pinia?


在我的项目中我使用的是pinia


首先,先通过npm安装pinia


npm install pinia

其次,在根组件app.vue中创建Pinia实例并将其注册为应用程序的插件


import { createPinia } from 'pinia'
const pinia = createPinia()
createApp(App).use(pinia).mount('#app')

接着,在src目录下创建一个store文件夹中的index.js,而在使用Pinia时,通过引入Pinia中的defineStore来定义一个store实例,类似于Vuex的store。然后我定义了不同的子仓库并暴露(export),来存储对应不同的页面所需的数据与操作,之后再返回(return)数据和操作。而在组件中使用Pinia时,需要通过引入,useStore辅助函数获取store实例,并将状态、操作和获取器映射到组件中,以便使用。


import { defineStore } from "pinia";
import { reactive } from "vue";

export const useUserStore = defineStore('user', () => {
const state = reactive({gridList:[]})
const loadUser = async () => {}
return {
state,
loadUser
}
})

import { useUserStore } from "@/store/user";

const userStore = useUserStore();
const gridList = computed(() => userStore.state.gridList);

上海(实习 100-150/天)



该面试是通过视频面试,面试的时候题目相对比较简单,都是一些基础的问题,这也就给了我极大的自信



1. JS的Event Loop你能给我介绍下吗?


因为JS是单线程的语言,为了防止一个函数执行时间过长阻塞后面的代码,所以就需要Event Loop这个事件环的运行机制。


当执行一段有同步又有异步的代码时,会先将同步任务压入执行栈中,然后把异步任务放入异步队列中等待执行,微任务放到微任务队列,宏任务放到宏任务队列,依次执行。执行完同步任务之后,Event Loop会先把微任务队列执行清空,微任务队列清空后,进入宏任务队列,取宏任务队列的第一个项任务进行执行,执行完之后,查看微任务队列是否有任务,有的话,清空微任务队列。然后再执行宏任务队列,反复微任务宏任务队列,直到所有队列任务执行完毕。


PS: 答完了基本的答案之后,最好可以往下继续延申,不要让面试成为一问一答,这样你的面试就会变的比较丰满,让面试官不至于太枯燥,直到面试官让你停为止。



异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列先执行。


微任务队列的代表就是,Promise.thenMutationObserver,宏任务的话就是setImmediate setTimeout setInterval



2. 渲染页面的重绘回流你能给我讲一下吗?




  • 重排/回流(Reflow):当DOM元素发生了规格大小,位置,增删改的操作时,浏览器需要重新计算元素的几何属性,重新生成布局,重新排列元素。




  • 重绘(Repaint): 当一个DOM元素的外观样式发生改变,但没有改变布局,重新把DOM元素的样式渲染到页面的过程。





重排和重绘它们会破坏用户体验,并且让UI展示非常迟缓,而在两者无法避免的情况下,重排的性能影响更大,所以一般选择代价更小的重绘。


『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。



上海(实习 200-210/天)



这场面试很正常,自我感觉含金量也比较高,通过视频面试能够知道,面试官老师人也长得挺帅的,说话和蔼,讲真,人真的挺好的。不过自己还会犯傻,走进思维误区,没有理解清面试官老师的问题,所以在面试中如果没听清楚问题,千万一定要再问一下面试官。



1. 响应式开发你了解吗?响应式是如何实现的呢?


响应式开发是一种设计和开发网站或应用程序的方法,使其能够在不同设备上以适应性和灵活性的方式呈现。它可以确保网站或应用程序在各种屏幕尺寸、浏览器和设备上都能提供良好的用户体验。


响应式开发的实现基于使用CSS媒体查询、弹性布局和流体网格等技术。以下是一些主要的实现方法:




  1. CSS媒体查询:使用CSS媒体查询可以检测设备的屏幕尺寸、分辨率和方向等特性,并根据这些特性应用不同的样式规则。通过定义不同的CSS样式,可以使网页在不同的设备上以不同的方式呈现。




  2. 弹性布局:即(display:flex),使用弹性布局(flexbox)可以创建灵活的布局结构,使内容能够根据屏幕尺寸进行自动调整。弹性布局使得元素的大小、位置和间距能够根据可用空间进行自适应。




  3. 网格布局:即(display:grid),使用流体网格(fluid grid)可以创建基于相对单位(如百分比)的网格系统,使页面的布局能够根据屏幕大小进行缩放和调整。这样可以确保内容在不同屏幕尺寸上均匀分布和对齐。




2. 媒体查询这个你了解吗?


我在使用less预编译样式中使用过媒体查询(这里提一嘴自己使用过less或者其他的预编译),媒体查询使用@media规则来定义,其语法如下:


@media mediatype and|not|only (media feature) {
/* CSS样式规则 */
}

其中,mediatype指定了媒体类型,如screen表示屏幕媒体、print表示打印媒体等。andnotonly是逻辑运算符,用于组合多个条件。media feature表示设备的特性,如width表示屏幕宽度、orientation表示屏幕方向等。


下面是一些常用的媒体特性:



  • width:屏幕宽度。

  • height:屏幕高度。

  • device-width:设备屏幕宽度。

  • device-height:设备屏幕高度。

  • orientation:屏幕方向(横向或纵向)。

  • aspect-ratio:屏幕宽高比。

  • color:设备的颜色位深。

  • resolution:屏幕分辨率。


通过结合不同的媒体特性和条件,可以根据设备的不同特性来应用不同的CSS样式。例如,可以使用媒体查询来定义在屏幕宽度小于某个阈值时应用的样式,或者根据屏幕方向调整布局等。


以下是一个示例,演示如何使用媒体查询在屏幕宽度小于600px时应用特定的样式:


@media screen and (max-width: 600px) {
/* 在屏幕宽度小于600px时应用的样式 */
body {
font-size: 14px;
}
/* 其他样式规则 */
}

这样,当浏览器窗口宽度小于600px时,body元素的字体大小将被设置为14px。


3. CSS的伪元素你知道是什么东西吗?


伪元素是CSS中的一种特殊选择器,用于向选中的元素的特定部分添加样式,而不需要在HTML结构中添加额外的元素。伪元素使用双冒号::作为标识符,用于区分伪类(pseudo-class)和伪元素。(在旧版本的CSS中,单冒号:也被用作伪元素的标识符,但在CSS3规范中,建议使用双冒号以区分伪类和伪元素。)


以下是一些常用的CSS伪元素:



  1. ::before:在选中元素的内容之前插入一个生成的内容。

  2. ::after:在选中元素的内容之后插入一个生成的内容。


这些伪元素可以与CSS的属性和样式一起使用,例如contentcolorbackground等,以为选中的元素的特定部分添加样式。


以下是一个示例,演示如何使用伪元素为元素的内容之前插入一个生成的内容并应用样式:


p::before {
content: "前缀:";
font-weight: bold;
color: blue;
}

在上述示例中,::before伪元素被应用于<p>元素,它在该段落的内容之前插入了一个生成的文本"前缀:",并为该生成的文本应用了加粗字体和蓝色的颜色。


4. 介绍一下HTML5的特有的标签?



  1. 语义化标签



  • <article>:用于表示独立的、完整的文章内容。

  • <section>:用于表示页面或应用程序中的一个区域,可以包含一个标题。

  • <header>:用于表示页面或应用程序的标题,通常包含logo和导航。

  • <footer>:用于表示页面或应用程序的页脚部分,通常包含版权信息、联系方式等。

  • <nav>:用于表示导航链接的集合,通常包含一组指向其他页面的链接。

  • <aside>:用于表示页面或应用程序的旁边栏,通常包含相关的信息、广告、链接等。



  1. <video>:用于嵌入视频文件,可以使用<source>标签指定多个视频文件,以便在不同的浏览器和设备上播放。

  2. <audio>:用于嵌入音频文件,可以使用<source>标签指定多个音频文件,以便在不同的浏览器和设备上播放。

  3. <canvas>:用于创建绘图区域,可以使用JavaScript在上面绘制图形、动画等。

  4. <progress>:用于显示进度条,表示任务完成的进度。


5. 你如果要做一个搜索引擎比较友好的页面,应该是要做到些什么东西呢?




  1. 使用语义化的HTML标记:使用适当的HTML标签来正确表示页面的结构,如使用<header><nav><article>等。




  2. 使用有意义的标题:使用恰当的标题标签(<h1><h2>等)来突出页面的主题和内容。




  3. 提供关键词和描述:在HTML文档中,可以通过<meta>标签来定义各种属性,比如页面的描述和关键字。


    keywords:向搜索引擎说明你的网页的关键词


     `<meta name="keyword" content="前端,面试,小厂">`

    description:告诉搜索引擎你的站点的主要内容


    <meta name="description" content="页面描述,包含关键字和吸引人的内容">



  4. 使用合适的图像标签:为图片使用适当的alt属性,描述图片内容,方便搜索引擎理解图像。




  5. 使用服务端渲染(SSR)的框架,比如vue中的Nuxtreact中的Next,即在服务端生成完整的 HTML 页面,并将其发送给浏览器。这使得搜索引擎可以更好地理解和索引页面的内容,因为它们可以直接看到渲染后的页面。




6. 介绍一下flex的布局吧?


## 阮一峰老师有一个博客,专门讲解一个flex布局,你可以讲一下flex布局吗?


7. 后端和前端的一些交互,你了解是什么东西?


后端和前端之间的交互通常通过前后端分离的架构来实现,其中前端负责展示界面和用户交互,后端负责处理数据和逻辑操作。


以下是一些常见的后端和前端交互的方式和技术:




  1. RESTful API:使用基于HTTP的RESTful API,前端可以向后端发送请求并获取数据。后端提供API接口,通过GETPOSTPUTDELETE等HTTP方法来处理前端请求,并返回相应的数据。前端可以使用Ajax、Fetch API或axios等工具来发送请求和处理响应。




  2. 数据传输格式前后端交互时需要使用一致的数据传输格式。常见的数据格式包括JSON(JavaScript Object Notation)和XML(eXtensible Markup Language)。前端可以发送数据请求给后端,后端将数据以指定的格式进行封装和返回给前端。




  3. 然后我还使用过nodejs中的koa洋葱模型简单搭建过一个MVC结构的服务器。




8. 那你有遇到过跨域问题吗?实际解决方法?


我分别说了




  • JSONP:在DOM文档中,使用<script>标签,但却缺点只能发 GET 请求并且容易受到XSS跨站脚本攻击




  • CORS:通过在服务器配置响应头,Access-Control-Allow-xxx字段来设置访问的白名单、可允许访问的方式等




  • postMessage




  • html原生的websocket




  • 代理 白嫖即食:构建工具的proxy代理配置区别(解决跨域)




讲了这些东西之后,面试官就让我说一下实际解决方法,像jsonp,postMeassage都不是正常的


然后我就把整个CORS跨域的过程给讲了一遍,包含了浏览器的跨域拦截



首先,浏览器进行了一个跨域请求,向服务器发送了一个预检(options)请求,服务器会在响应头部中设置Access-Control-Allow-Origin和Access-Control-Allow-Methods等配置,告知浏览器是否允许跨域请求。如果该页面满足服务器设置的白名单和可允许访问的方式,那么服务器就允许跨域访问,浏览器就会接受响应,进行真实的跨域请求,否则就会报错。



面试基本必问问题


1. 你有什么想问我的吗?(问到这里一场面试结束了)




  1. 公司团队使用的技术栈有哪些?




  2. 如果我面试通过后,公司是否有人带,主要做些什么




  3. 公司团队提交代码的工具有什么要求吗?




  4. 把之前没答上来的问题可以再问一遍(让面试官感到你很好学)




2. 你写项目的时候碰到过印象里比较深刻的一些bug或困难,你怎么解决的?


其实这部分可以从侧面分析这个问题,问你遇到的bug可能一时半会儿不知道怎么回答,但如果问你是如何实现项目中的某个功能,这时候就好回答了,只需要转换回答成没有这个功能代码会出现什么问题。所以面试官不是问你有什么bug,而是你在项目中有哪些亮点。



前端中常见的一些bug



  1. JavaScript 错误:在应用程序中使用的 JavaScript 代码可能包含语法错误或逻辑错误,这些错误会导致应用程序在执行时出现问题,从而导致性能问题。

  2. DOM 操作错误:通过 JavaScript 操作文档对象模型 (DOM) 可以更新应用程序中的 HTML 元素。但是,如果 DOM 操作不正确或在操作过程中执行了太多的操作,可能会导致性能问题。

  3. 页面重绘:当用户与页面交互时,浏览器会执行重新绘制和重排操作。如果页面包含太多的重绘操作或页面重排操作,则可能导致性能问题。

  4. 图像和资源加载:在加载图像和其他资源时,如果没有正确管理缓存或使用适当的图像格式,则可能导致性能问题。

  5. 前端框架错误:使用前端框架时,可能会出现错误或不良的编码实践,这些问题可能会导致性能问题。



axios响应拦截


遇到bug:我是使用mockjs来模拟后端的接口,当时我在设置端口的返回值时,我返回数据有一个状态码以及把json数据中export出来的detail数据添加到data这个需要返回的数据(代码如下),这导致我在获取接口里的数据时需要多.data(引用一层data),当时我没意识,结果一直获取不到数据。


解决办法:通过使用axios进行请求和响应,并在响应的时候设置一个拦截,对响应进行一番处理之后就可以直接拿到接口返回的值,而不会导致接口返回的值不会有太多的嵌套了。


Mock.mock(/\/detail/, 'get', () => {
return {
code: 0, // 返回状态码
data: detail // 返回数据
}
})

import axios from "axios";
// 响应拦截器
axios.interceptors.response.use((res) => {
return res.data
})

图片和组件的懒加载


遇到的bug:我做的项目使用了很多的组件页面和大量的图片,导致在加载页面时耗时比较久,以及在页面的切换时很多暂时不需要的页面组件一次性全部加载了,导致整个项目的性能非常差。


解决办法


图片懒加载:在App.vue中引入VueLazy并且使用app.use启用它,然后把图片中的src改成v-lazy


<img :src="xxx.png">

改成


<img v-lazy="xxx.png">

页面组件懒加载:在router配置中的component,把直接在代码一开始引入组件页面,改成箭头函数式引入。


    import Home from '@/views/Home/Home.vue' 
{
path: '/',
component: Home
},

改成


    {
path: '/',
component: () => import('@/views/Home/Home.vue')
},

搜索界面节流


遇到的bug:在搜索界面的时候,当我一直点击搜索时,它会频繁的进行请求,造成了不必要的性能损耗。


解决办法:使用loadash库中的节流API,进行对触发搜索事件进行节流,防止用户进行频繁的搜索请求导致性能损耗。



import _ from 'lodash'

const value = ref(null)

const ajax1 = () => {
console.log('开始搜索,搜索内容为' + value.value)
}

let debounceAjax1 = _.debounce(ajax1, 1000)

const onSearch = () => {
if (!value.value) {
showToast('搜索内容为空,请输入内容')
return
}
debounceAjax1()
}

404页面


遇到的bug:当输入url中没有在路由配置中配置过的路径时,页面它会出现空白,并且浏览器发出警告,如果我这个项目上线的话,可能会造成用户的体验不友好和搜索引擎不友好。


解决办法:在路由配置中再配置一个404页面的路径,这样就能使用户不管怎么输入不合规的url后,都会提示用户输错了网址。


    {
path: '/404',
name: 'NotFound',
component: () => import('@/views/NotFound/Index.vue')
},
// 所有未定义路由,全部重定向到404页
{
path: '/:pathMatch(.*)',
redirect: '/404'
}

结语


面试,说到底,迈开第一步其实是最重要的,别想那么多,要抱着反正有那么多家公司,我没必要非要去你这一家的心态去面试,把面试官当作一个久久未联系过的老朋友,突然有一天碰到了聊起天。面试完之后一定及时的整理复盘,不断地让自己变得更加牢固。


作者:吃腻的奶油
来源:juejin.cn/post/7233307834456375353
收起阅读 »

我给项目加了性能守卫插件,同事叫我晚上别睡的太死

web
引言 给组内的项目都在CICD流程上更新上了性能守卫插件,效果也还不错,同事还疯狂夸奖我 接下里进入我们的此次的主题吧 由于我组主要是负责的是H5移动端项目,老板比较关注性能方面的指标,比如首页打开速度,所以一般会关注FP,FCP等等指标,所以一般项目写完...
继续阅读 »



引言


给组内的项目都在CICD流程上更新上了性能守卫插件,效果也还不错,同事还疯狂夸奖我


WX20230708-170807@2x.png


接下里进入我们的此次的主题吧



由于我组主要是负责的是H5移动端项目,老板比较关注性能方面的指标,比如首页打开速度,所以一般会关注FP,FCP等等指标,所以一般项目写完以后都会用lighthouse查看,或者接入性能监控系统采集指标.



WX20230708-141706@2x.png


但是会出现两个问题,如果采用第一种方式,使用lighthouse查看性能指标,这个得依赖开发自身的积极性,他要是开发完就Merge上线,你也不知道具体指标怎么样。如果采用第二种方式,那么同样是发布到线上才能查看。最好的方式就是能强制要求开发在还没发布的时候使用lighthouse查看一下,那么在什么阶段做这个策略呢。聪明的同学可能想到,能不能在CICD构建阶段加上策略。其实是可以的,谷歌也想到了这个场景,提供性能守卫这个lighthouse ci插件


性能守卫



性能守卫是一种系统或工具,用于监控和管理应用程序或系统的性能。它旨在确保应用程序在各种负载和使用情况下能够提供稳定和良好的性能。



Lighthouse是一个开源的自动化工具,提供了四种使用方式:




  • Chrome DevTools




  • Chrome插件




  • Node CLI




  • Node模块




image.png


其架构实现图是这样的,有兴趣的同学可以深入了解一下


这里我们我们借助Lighthouse Node模块继承到CICD流程中,这样我们就能在构建阶段知道我们的页面具体性能,如果指标不合格,那么就不给合并MR


剖析lighthouse-ci实现


lighthouse-ci实现机制很简单,核心实现步骤如上图,差异就是lighthouse-ci实现了自己的server端,保持导出的性能指标数据,由于公司一般对这类数据敏感,所以我们一般只需要导出对应的数据指标JSON,上传到我们自己的平台就行了。


image.png


接下里,我们就来看看lighthouse-ci实现步骤:





    1. 启动浏览器实例:CLI通过Puppeteer启动一个Chrome实例。


    const browser = await puppeteer.launch();




    1. 创建新的浏览器标签页:接着,CLI创建一个新的标签页(或称为"页面")。


    const page = await browser.newPage();




    1. 导航到目标URL:CLI命令浏览器加载指定的URL。


    await page.goto('https://example.com');




    1. 收集数据:在加载页面的同时,CLI使用各种Chrome提供的API收集数据,包括网络请求数据、JavaScript执行时间、页面渲染时间等。





    1. 运行审计:数据收集完成后,CLI将这些数据传递给Lighthouse核心,该核心运行一系列预定义的审计。





    1. 生成和返回报告:最后,审计结果被用来生成一个JSON或HTML格式的报告。


    const report = await lighthouse(url, opts, config).then(results => {
    return results.report;
    });




    1. 关闭浏览器实例:报告生成后,CLI关闭Chrome实例。


    await browser.close();



// 伪代码
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const {URL} = require('url');

async function run() {
// 使用 puppeteer 连接到 Chrome 浏览器
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

// 新建一个页面
const page = await browser.newPage();

// 在这里,你可以执行任何Puppeteer代码,例如:
// await page.goto('https://example.com');
// await page.click('button');

const url = 'https://example.com';

// 使用 Lighthouse 进行审查
const {lhr} = await lighthouse(url, {
port: new URL(browser.wsEndpoint()).port,
output: 'json',
logLevel: 'info',
});

console.log(`Lighthouse score: ${lhr.categories.performance.score * 100}`);

await browser.close();
}

run();

导出的HTML文件


image.png


导出的JSON数据


image.png


实现一个性能守卫插件


在实现一个性能守卫插件,我们需要考虑以下因数:





    1. 易用性和灵活性:插件应该易于配置和使用,以便它可以适应各种不同的CI/CD环境和应用场景。它也应该能够适应各种不同的性能指标和阈值。





    1. 稳定性和可靠性:插件需要可靠和稳定,因为它将影响整个构建流程。任何失败或错误都可能导致构建失败,所以需要有强大的错误处理和恢复能力。





    1. 性能:插件本身的性能也很重要,因为它将直接影响构建的速度和效率。它应该尽可能地快速和高效。





    1. 可维护性和扩展性:插件应该设计得易于维护和扩展,以便随着应用和需求的变化进行适当的修改和更新。





    1. 报告和通知:插件应该能够提供清晰和有用的报告,以便开发人员可以快速理解和处理任何性能问题。它也应该有一个通知系统,当性能指标低于预定阈值时,能够通知相关人员。





    1. 集成:插件应该能够轻松集成到现有的CI/CD流程中,同时还应该支持各种流行的CI/CD工具和平台。





    1. 安全性:如果插件需要访问或处理敏感数据,如用户凭证,那么必须考虑安全性。应使用最佳的安全实践来保护数据,如使用环境变量来存储敏感数据。




image.png


// 伪代码
//perfci插件
const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const { port } = new URL(browser.wsEndpoint());

async function runAudit(url) {
const browser = await puppeteer.launch();
const { lhr } = await lighthouse(url, {
port,
output: 'json',
logLevel: 'info',
});
await browser.close();

// 在这里定义你的性能预期
const performanceScore = lhr.categories.performance.score;
if (performanceScore < 0.9) { // 如果性能得分低于0.9,脚本将抛出错误
throw new Error(`Performance score of ${performanceScore} is below the threshold of 0.9`);
}
}

runAudit('https://example.com').catch(console.error);


使用


name: CI
on: [push]
jobs:
lighthouseci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install && npm install -g @lhci/cli@0.11.x
- run: npm run build
- run: perfci autorun


性能审计


const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const nodemailer = require('nodemailer');

// 配置邮件发送器
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com',
pass: 'your-password',
},
});

// 定义一个函数用于执行Lighthouse审计并处理结果
async function runAudit(url) {
// 通过Puppeteer启动Chrome
const browser = await puppeteer.launch({ headless: true });
const { port } = new URL(browser.wsEndpoint());

// 使用Lighthouse进行性能审计
const { lhr } = await lighthouse(url, { port });

// 检查性能得分是否低于阈值
if (lhr.categories.performance.score < 0.9) {
// 如果性能低于阈值,发送警告邮件
let mailOptions = {
from: 'your-email@gmail.com',
to: 'admin@example.com',
subject: '网站性能低于阈值',
text: `Lighthouse得分:${lhr.categories.performance.score}`,
};

transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}

await browser.close();
}

// 使用函数
runAudit('https://example.com');


接下来,我们分步骤大概介绍下几个核心实现


数据告警


// 伪代码
const lighthouse = require('lighthouse');
const puppeteer = require('puppeteer');
const nodemailer = require('nodemailer');

// 配置邮件发送器
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your-email@gmail.com',
pass: 'your-password',
},
});

// 定义一个函数用于执行Lighthouse审计并处理结果
async function runAudit(url) {
// 通过Puppeteer启动Chrome
const browser = await puppeteer.launch({ headless: true });
const { port } = new URL(browser.wsEndpoint());

// 使用Lighthouse进行性能审计
const { lhr } = await lighthouse(url, { port });

// 检查性能得分是否低于阈值
if (lhr.categories.performance.score < 0.9) {
// 如果性能低于阈值,发送警告邮件
let mailOptions = {
from: 'your-email@gmail.com',
to: 'admin@example.com',
subject: '网站性能低于阈值',
text: `Lighthouse得分:${lhr.categories.performance.score}`,
};

transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
}

await browser.close();
}

// 使用函数
runAudit('https://example.com');

处理设备、网络等不稳定情况


// 伪代码

// 网络抖动
const { lhr } = await lighthouse(url, {
port,
emulatedFormFactor: 'desktop',
throttling: {
rttMs: 150,
throughputKbps: 1638.4,
cpuSlowdownMultiplier: 4,
requestLatencyMs: 0,
downloadThroughputKbps: 0,
uploadThroughputKbps: 0,
},
});


// 设备
const { lhr } = await lighthouse(url, {
port,
emulatedFormFactor: 'desktop', // 这里可以设定为 'mobile' 'desktop'
});


用户登录态问题



也可以让后端同学专门提供一条内网访问的登录态接口环境,仅用于测试环境



const puppeteer = require('puppeteer');
const lighthouse = require('lighthouse');
const fs = require('fs');
const axios = require('axios');
const { promisify } = require('util');
const { port } = new URL(browser.wsEndpoint());

// promisify fs.writeFile for easier use
const writeFile = promisify(fs.writeFile);

async function runAudit(url, options = { port }) {
// 使用Puppeteer启动Chrome
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 访问登录页面
await page.goto('https://example.com/login');

// 输入用户名和密码
await page.type('#username', 'example_username');
await page.type('#password', 'example_password');

// 提交登录表单
await Promise.all([
page.waitForNavigation(), // 等待页面跳转
page.click('#login-button'), // 点击登录按钮
]);

// 运行Lighthouse
const { lhr } = await lighthouse(url, options);

// 保存审计结果到JSON文件
const resultJson = JSON.stringify(lhr);
await writeFile('lighthouse.json', resultJson);

// 上传JSON文件到服务器
const formData = new FormData();
formData.append('file', fs.createReadStream('lighthouse.json'));

// 上传文件到你的服务器
const res = await axios.post('https://your-server.com/upload', formData, {
headers: formData.getHeaders()
});

console.log('File uploaded successfully');

await browser.close();
}

// 运行函数
runAudit('https://example.com');

总结


性能插件插件还有很多需要考虑的情况,所以,不懂还是来私信问我吧,我同事要请我吃饭去了,不写了。


作者:linwu
来源:juejin.cn/post/7253331974051823675
收起阅读 »

聊聊深色模式(Dark Mode)

web
什么是深色模式 深色模式(Dark Mode),或者叫暗色模式,黑夜模式,是和日常使用的浅色(亮色)模式(Light Mode)相对应的一种UI主题。 深色模式最早来源于人机交互领域的研究和实践,从2018年左右开始,Apple推出了iOS 13,其中包含了系...
继续阅读 »

什么是深色模式


深色模式(Dark Mode),或者叫暗色模式,黑夜模式,是和日常使用的浅色(亮色)模式(Light Mode)相对应的一种UI主题。


深色模式最早来源于人机交互领域的研究和实践,从2018年左右开始,Apple推出了iOS 13,其中包含了系统级别的深色模式,可以将整个系统的界面切换为暗色调。


Google也在Android 10中推出了类似的深色模式功能,使深色模式得到了更广泛的应用和推广。


iOS官网的深色模式示例


iOS官网的深色模式示例


它不是简单的把背景变为黑色,文字变为白色,而是一整套的配色主题,这种模式相比浅色模式更加柔和,可以减少亮度对用户眼睛造成的刺激和疲劳。


随着越来越多的应用开始支持深色模式,作为开发也理应多了解下深色模式。


首先,怎么打开深色模式


在说怎么实现之前,先来说说我们要怎么打开深色模式,一般来说只需要在系统调节亮度的地方就可以调节深色模式,具体我们可以看各个系统的官方网站即可:
如何打开深色模式



但是在开发调试调试时,不断切换深色模式可能比较麻烦,这时浏览器就提供了一种模拟系统深色模式的方法,可以让当前的Web页面临时变为深色模式,以Chrome为例:
浏览器模拟深色/浅色模式



  1. 打开Chrome DevTools

  2. Command+Shift+P

  3. 输入dark或者light

  4. 打开深色或者浅色模式打开深色模式打开浅色模式


不过要注意的是,浏览器DevTools里开启深色模式,在关闭开发者工具后就会失效。


自动适配 - 声明页面支持深色模式


其实,在支持深色模式的浏览器中,有一套默认的深色模式,只需要我们在应用中声明,即可自动适配深色模式,声明有两种方式:


1. 添加color-schememeta标签


在HTML的head标签中增加color-schememeta标签,如下所示:


<!--
The page supports both dark and light color schemes,
and the page author prefers light.
-->

<meta name="color-scheme" content="light dark">

通过上述声明,告诉浏览器这个页面支持深色模式和浅色模式,并且页面更倾向于浅色模式。在声明了这个之后,当系统切换到深色模式时,浏览器将会把我们的页面自动切换到默认的深色模式配色,如下所示:
左边浅色,右边是浏览器自动适配的深色


左边浅色,右边是浏览器自动适配的深色


2. 在CSS里添加color-scheme属性


/*
The page supports both dark and light color schemes,
and the page author prefers light.
*/

:root {
color-scheme: light dark;
}

通过上面在:root元素上添加color-scheme属性,值为light dark,可以实现和meta标签一样的效果,同时这个属性不只可用于:root级别,也可用于单个元素级别,比meta标签更灵活。


但是提供color-schemeCSS属性需要首先下载CSS(如果通过<link rel="stylesheet">引用)并进行解析,使用meta可以更快地使用所需配色方案呈现页面背景。两者各有优劣吧。


自定义适配


1. 自动适配的问题


在上面说了我们可以通过一些标签或者CSS属性声明,来自动适配深色模式,但是从自动适配的结果来看,适配的并不理想:
左边浅色,右边是浏览器自动适配的深色


左边浅色,右边是浏览器自动适配的深色




  • 首先是默认的黑色字体,到深色模式下变成了纯白色#FFFFFF,和黑色背景(虽然说不是纯黑)对比起来很扎眼,在一些设计相关的文章[1][2]里提到,深色模式下避免使用纯黑和纯白,否则更容易使人眼睛👁疲劳,同时容易在页面滚动时出现拖影:


    滚动时出现拖影,图片来源「即刻」




滚动时出现拖影,图片来源「即刻」




  • 自动适配只能适配没有指定颜色和背景色的内容,比如上面的1、2、3级文字还有背景,没有显式设置colorbackground-color


    对于设置了颜色和背景色(这种现象在开发中很常见吧)的内容,就无法自动适配,比如上面的7个色块的背景色,写死了颜色,但是色块上的文字没有设置颜色。最终在深色渲染下渲染出的效果就是,色块背景色没变,但是色块上的文字变成了白色,导致一些文字很难看清。




所以,最好还是自定义适配逻辑,除了解决上面的问题,还可以加一下其他的东西,比如加一些深浅色模式变化时的过渡动画等。


2. 如何自定义适配


自定义适配有两种方式,CSS媒体查询和通过JS监听主题模式


1). CSS媒体查询


prefers-color-scheme - CSS:层叠样式表 | MDN
我们可以通过在CSS中设置媒体查询@media (prefers-color-scheme: dark),来设置深色模式下的自定义颜色。比如:


.textLevel1 {
color: #404040;
margin-bottom: 0;
}
.textLevel2 {
color: #808080;
margin-bottom: 0;
}
.textLevel3 {
color: #bfbfbf;
margin-bottom: 0;
}

@media (prefers-color-scheme: dark) {
.textLevel1 {
color: #FFFFFF;
opacity: 0.9;
}
.textLevel2 {
color: #FFFFFF;
opacity: 0.6;
}
.textLevel3 {
color: #FFFFFF;
opacity: 0.3;
}
}

通过媒体查询设置元素在深色模式下的1、2、3级文字的颜色,在浅色模式下设置不同的颜色,在深色模式下,增加不透明度:


截屏2023-03-12 下午6.04.18.png左边的是自动适配的浅色深色,右边是自定义适配的浅色深色


左边的是自动适配的浅色深色,右边是自定义适配的浅色深色


对于prefers-color-scheme的兼容性也不必担心,主流浏览器基本都支持了:


截屏2023-03-12 下午6.10.07.png


2). JS监听主题颜色


Window.matchMedia() - Web API 接口参考 | MDN


通过CSS媒体查询适配深色模式已经很方便了,完全不需要修改JS代码逻辑,那为什么还需要JS监听主题颜色呢?


因为通过CSS媒体查询虽然方便,但是只能跟随系统的主题颜色变化,假设用户想要类似于下面的自定义主题颜色,不跟随系统变化,或者业务上想做一些什么高级的操作,那么CSS媒体查询就无能为力了。


7b368843-dd42-4b4b-bc73-6d445de78923.gif


在JS中,可以通过window.matchMedia('(prefers-color-scheme: dark)').match来判断当前的颜色主题:


const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

if (darkModeMediaQuery.matches) {
// dark mode
} else {
// light mode
}

darkModeMediaQuery.addEventListener('change', (event: MediaQueryListEvent) => {
if (event.matches) {
// dark mode
} else {
// light mode
}
});

如果使用的是React,还可以写一个自定义hooks,来监听当前的颜色主题:


export enum EColorScheme {
LIGHT = 'light',
DARK = 'dark',
}

export const useColorScheme = (): EColorScheme => {
const [scheme, setScheme] = useState(
darkModeMediaQuery.matches ? EColorScheme.DARK : EColorScheme.LIGHT
);

useEffect(() => {
const listener = (event: MediaQueryListEvent) => {
setScheme(event.matches ? EColorScheme.DARK : EColorScheme.LIGHT);
};
darkModeMediaQuery.addEventListener('change', listener);
return () => {
darkModeMediaQuery.removeEventListener('change', listener);
};
}, []);

return scheme;
};

一些题外话


我写完这个hooks之后,有一次偶然用Chat GPT让它写一段监听深色模式的代码,写成hooks,最后生成的代码和这个几乎一模一样,Chat GPT还是有点东西的:


import { useState, useEffect } from 'react';

type ThemeType = 'light' | 'dark';

function useTheme(): ThemeType {
const [theme, setTheme] = useState<ThemeType>(
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
);

useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

const handleChange = () => setTheme(mediaQuery.matches ? 'dark' : 'light');

mediaQuery.addEventListener('change', handleChange);

return () => mediaQuery.removeEventListener('change', handleChange);
}, []);

return theme;
}

export default useTheme;

window.matchMedia的兼容性也挺好的:


截屏2023-03-12 下午7.03.48.png
通过JS监听颜色主题变化之后,那可玩性就很多了,我们可以通过下面这些方式来适配深色模式:




  • 动态添加类名覆盖样式


    通过判断深色模式来添加一个深色模式的类名,覆盖浅色模式样式:


    <div
    className={classnames(
    style.wrapper,
    scheme === EColorScheme.DARK && style.darkModeWrapper
    )}
    >
    {/* some code here */}
    </div>



  • 对于深色模式直接引用不同的CSS资源文件




  • 用一些第三方的库,比如postcss-darkmode




回到上面话题,通过JS可以监听到系统的颜色主题,那怎么实现用户主动选择颜色主题,不随系统的改变呢?其实也很简单,可以在本地store中设置一个颜色主题的值,用户设置了就优先选用store里的,没有设置就跟随系统,以上面的hooks为例:


export const useColorScheme = (): EColorScheme => {
// 从 store 中取出用户手动设置的主题
const manualScheme = useSelector(selectManualColorScheme);
const [scheme, setScheme] = useState(
darkModeMediaQuery.matches ? EColorScheme.DARK : EColorScheme.LIGHT
);

useEffect(() => {
const listener = (event: MediaQueryListEvent) => {
setScheme(event.matches ? EColorScheme.DARK : EColorScheme.LIGHT);
};
darkModeMediaQuery.addEventListener('change', listener);
return () => {
darkModeMediaQuery.removeEventListener('change', listener);
};
}, []);

// 优先取用户手动设置的主题
return manualScheme || scheme;
};

React Native中的适配


上面说的都是在浏览器里对深色模式的适配,那在React Native里面要怎么适配深色模式呢?


1. 大于等于0.62的版本


Appearance · React Native


在React Native 0.62版本中,引入了Appearance模块,通过这个模块:


type ColorSchemeName = 'light' | 'dark' | null | undefined;

export namespace Appearance {
type AppearancePreferences = {
colorScheme: ColorSchemeName;
};

type AppearanceListener = (preferences: AppearancePreferences) => void;

/**
* Note: Although color scheme is available immediately, it may change at any
* time. Any rendering logic or styles that depend on this should try to call
* this function on every render, rather than caching the value (for example,
* using inline styles rather than setting a value in a `StyleSheet`).
*
* Example: `const colorScheme = Appearance.getColorScheme();`
*/

export function getColorScheme(): ColorSchemeName;

/**
* Add an event handler that is fired when appearance preferences change.
*/

export function addChangeListener(listener: AppearanceListener): EventSubscription;

/**
* Remove an event handler.
*/

export function removeChangeListener(listener: AppearanceListener): EventSubscription;
}

/**
* A new useColorScheme hook is provided as the preferred way of accessing
* the user's preferred color scheme (aka Dark Mode).
*/

export function useColorScheme(): ColorSchemeName;

通过Appearance模块,可以获得当前的系统颜色主题:


const colorScheme = Appearance.getColorScheme();
if (colorScheme === 'dark') {
// dark mode
} else {
// light mode
}

Appearance.addChangeListener((prefer: Appearance.AppearancePreferences) => {
if (prefer.colorScheme === 'dark') {
// dark mode
} else {
// light mode
}
});

同时也提供了一个上面我们自己实现的hooks,useColorScheme


const colorScheme = useColorScheme();

一些坑




  1. Appearance这个接口在Chrome调试模式下,会不生效,永远返回light


    Appearance.getColorScheme() always returns ‘light’




  2. Appearance想要生效,还需要Native做一些配置


    React Native 0.62.2 Appearance return wrong color scheme



    Also make sure you do not have UIUserInterfaceStyle set in your Info.plist. I had it set to 'light' so Appearance.getColorScheme() was always returning 'light'.





2. 小于0.62的版本


对于0.62之前的版本,由于RN没有提供官方接口,需要通过第三方的库react-native-dark-mode来实现:
GitHub - codemotionapps/react-native-dark-mode: Detect dark mode in React Native


它的实现原理感兴趣的可以看下:



react-native-dark-mode 实现原理(这段实现原理其实也是问Chat GPT得到的答案😂)


react-native-dark-mode库的实现原理比较简单,它主要是利用了原生平台的接口来检测当前系统是否处于深色模式。在iOS平台上,它使用了UIUserInterfaceStyle接口来获取当前系统的界面风格,然后判断是否为暗黑模式。在Android平台上,它使用了UiModeManager接口来获取当前系统的 UI 模式,然后判断是否为夜间模式。


具体来说,react-native-dark-mode在React Native项目中提供了一个名为useDarkMode的 React Hooks,用于获取当前系统是否处于深色模式。当使用这个Hooks时,它会首先检测当前平台是否支持暗黑模式,如果支持,就直接调用原生平台的接口获取当前系统的界面风格或UI模式,并将结果返回给调用方。如果不支持,就返回一个默认值(比如浅色模式)。


需要注意的是,由于react-native-dark-mode是一个纯JS库,它无法直接调用原生平台的接口。所以它在Native端编写了一个名为DarkMode的模块,在JS层通过NativeModules.DarkMode来调用。



  • 在iOS上,DarkMode模块会通过RCT_EXPORT_MODULE()宏将自己暴露给RN的JS层。同时,它还会使用RCT_EXPORT_METHOD()宏将检测系统界面风格的方法暴露给JS层,使得在JS中可以直接调用该方法。

  • 在Android上,DarkMode模块同样会通过@ReactModule注解将自己暴露给JS层。然后,它会创建一个名为DarkModeModule的Java类,并在该类中实现检测系统UI模式的方法。最后,它会使用@ReactMethod注解将该方法暴露给JS层,使得在JS中可以直接调用该方法。



参考链接



作者:酥风
来源:juejin.cn/post/7298997940019085366
收起阅读 »

00年菜鸡前端的面试经历分享

web
去年8月份入职的某大厂(外包)今年6月份被通知甲方即将转移去广州。我们外包人员产品和测试被外包公司安排了赔偿,但也赔的很少。前端和后端就是被安排其他甲方的面试,我碰巧很想去旅游,就直接自离了。 出去玩了一个月以后兜里的元子也基本见底了,虽说目前还没有车贷房贷但...
继续阅读 »

去年8月份入职的某大厂(外包)今年6月份被通知甲方即将转移去广州。我们外包人员产品和测试被外包公司安排了赔偿,但也赔的很少。前端和后端就是被安排其他甲方的面试,我碰巧很想去旅游,就直接自离了。


出去玩了一个月以后兜里的元子也基本见底了,虽说目前还没有车贷房贷但也要交房租也要吃饭,就又开始了找工作。不过令人没想到的是今年的行情能这么这么的差,以前每年都说今年环境差但每次我离职基本都能在两周内拿到满意的ofr,但今年算是找了将近两个月才找到个稍微稍微差不多点的(短期,三个月,而且薪资比上家低了3K,好在离家近,办公环境还算敞亮


(图片是面试路上拍的与文章内容没啥关系)


微信图片_20230817085740.jpg

简单记录,问的问题以及我的回答有的记不太清我就从简了。


第一家是一个研究所,面试我的不知道是个大哥还是大姐反正有点中性那种感觉(不过听声音应该是大哥),问了react中的useEffect,我说是用于修改以及监听数据变化,相当于react18之前的componentDidMount、componentDidUpdate和componentWillUnmount,传递的参数分别是要处理的逻辑函数以及数组。大文件上传,我说大文件上传主要的解决方案就是切片处理,和后端定义好key关键值,然后分割file分批通过接口上传文件以及参数后端拿到后再进行合并。第三个问了我性能优化,我说了几个大概方向:图片优化(大图片压缩、雪碧图)、代码优化(组件化减少复用、外部链接)、懒加载预加载、节流防抖。


然后问了我以前的工作亮点,这个问题 其实很多次被问到我也只是挑我觉得业务逻辑稍微难一点的东西说,实在没有个说出来让面试官眼前一亮的答案。


然后回家后hr联系我说给过,但只给到了12,我说我最低接受13,其实不是拉扯她我这次找工作本来给自己定的目标就是13-14,我是觉得这家离家比较近,但办公环境有些压抑,屋里人多 有点阴暗 我说能不能争取到13,hr说尝试一下,过了一会说最高12.5了我说那我再看看吧,其实也是因为心态问题,这是第一家我也只是试水的状态,他真的给到了13我可能也不是说一定就会去。


微信图片_20230801013245.jpg

第二家也是个自研,这家离家距离中规中矩,45分钟地铁。问的都是些基础面试题早就背的滚瓜烂熟那种,什么水平垂直居中 我说了三种 一种弹性盒、一种topleft50%然后margin各负一半、还有一种绝对定位相对定位。什么组件通信、路由传参,但这家吃亏在我没做过GIS和地图,所以结果是也没给过。第三家是个外包,其实我从不介意外包,因为我学历就不太顶,而且现在行情不好有的干就不错了。这家公司位置还挺好,在新街口附近,应该很有钱,问了vue中父子组件生命周期的执行顺序,我说父create-子create-子mount-父mount。然后他又追问我哪个先beforeCreate我说子先


然后问我 v-if和v-for的优先级以及vue2 和 vue3中他们的区别,其实应该是2中for大于if3中if大于for但我回答的时候说反了她还问我确定吗我说确定
然后和我说他们公司主要用的技术栈是react(我纳闷那你问我vue干啥玩楞)而且他们主要是用react native我寻思也行 做一些我没做过的东西也算开拓新领域了,但很遗憾也没给过


微信图片_20230817090227.jpg

第四家 就有意思了,贼拉远。怎么事儿呢? 上午十点半我刚自然醒迷瞪的我就看boss一看有个面试邀请乐呵的就接受了,然后一看是今天的我寻思那起来洗漱换衣服出发吧,结果一出门看路线才看到他娘的两个小时的路程,地铁转三趟,还要做十站公交,还要徒步1.5公里。我寻思这就算面试通过了以后也不好上下班呀,一天四个小时都在路上,我就打算取消了吧,但boss上即将面试的面试还不能取消,我跟hr说 hr说没事我们好多员工也在你那附近,过来吧。其实大概也能察觉到估计是让我过去填她人事kpi的,但我想着在家闲着也是闲着就当打发时间了,就去了。


确实是麻烦,这路程真的就算给我18k我都不想去,然后接我进去的是个花臂小哥,他花臂还挺帅的。我从家出发是十点,到那十二点都午休了,他们让我等到一点半我说我下午有事就联系了人事让面试官这会儿面一下子


问了我关于深浅拷贝 我说就是引用指针的区别,深拷贝就是重新注册一块空间声明变量,常用的方法有递归和json.parse再strfy但后者只能处理基本数据类型。问了我事件执行机制,我就大概往红任务微任务那方向回答的,然后让我手写了个递推和冒泡。就回了,吗的这面试就面了半个小时,来回路程四个半小时


出门十点,回家下午四点了(面完出来在地铁口吃了口饭)


面试官意思说我还可以,但回家以后我也没问hr后续,因为过了也不打算去,而且hr也没主动联系我


然后就搬了个家。。。


微信图片_20230817090228.jpg

最后一家面试(也就是现在入职这家)问了我跨域,我说跨域是出于浏览器的同源策略,当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。然后解决办法 第一个我说的jsonp解决,用script标签括住跨域的部分,第二个是本地代理。他又说线上环境你怎么办呢,我说线上的话那就cors解决,什么w3c标准啊跨源ajax啥的就都忽悠上了,其实正儿八经工作中我基本没用过cors和jsonp,基本全是本地代理


然后问了我数组的一些方法我就可增删改查合并分割这些的说了一些


微信图片_20230817115038.jpg

然后让我手写了一个promise和节流函数还有一个去重,讲实话就去重写出来的比较完整,promise和节流就写出来个大概思路


就让我进了


但就三个月,我想的仨月就仨月吧,干完也就十一月中旬了,再躺一个月过完元旦回家过年了


其实要不是因为刚和小伙伴签了一年的房子合同真有点打算去别的城市了


作者:牛油果好不好吃
来源:juejin.cn/post/7268011328940539939
收起阅读 »

你的代码不堪一击!太烂了!

web
前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
继续阅读 »

前言


小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


一、变量解构一解就报错


优化前


const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



所以当 dataundefinednull 时候,上述代码就会报错。


优化后


const App = (props) => {
const { data } = props;
const { name, age } = data || {};
}

二、不靠谱的默认值


估计有些同学,看到上小节的代码,感觉还可以再优化一下。


再优化一下


const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



所以当 props.datanull,那么 const { name, age } = null 就会报错!


三、数组的方法只能用真数组调用


优化前:


const App = (props) => {
const { data } = props;
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


优化后:


const App = (props) => {
const { data } = props;
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象


优化前:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


优化后:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


二次优化后:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用


优化前:


const App = (props) => {
const { data } = props;
const nameList = Object.keys(data);
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


优化后:


const App = (props) => {
const { data } = props;
const nameList = Object.keys(data || {});
}

二次优化后:


const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props;
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获


优化前:


import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。


优化后:


import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


二次优化后:


import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse


优化前:


const App = (props) => {
const { data } = props;
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


优化后:


const App = (props) => {
const { data } = props;
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据


优化前:


const App = (props) => {
const { data } = props;
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


优化后:


import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props;
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作


优化前:


const App = (props) => {
const { data } = props;
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


优化后:


const App = async (props) => {
const { data } = props;
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御


优化前:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


优化后:


const App = (props) => {
const { data } = props;
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续


以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


作者:红尘炼心
来源:juejin.cn/post/7259007674520158268
收起阅读 »

坏了,CSS真被他们玩出花来了

web
前言 事情是这样子的,本人由于摸鱼过多被临时抽调去支援公司的一个官网开发,其中有个任务是改造侧边栏导航,我心想着很简单嘛,两下搞完继续我的摸鱼大业😁.然后ui就丢给了一个网站让我照着这个做(龟龟现成的都有,这也太好了),网站在这里,让我们来看一下简单的交互. ...
继续阅读 »

前言


事情是这样子的,本人由于摸鱼过多被临时抽调去支援公司的一个官网开发,其中有个任务是改造侧边栏导航,我心想着很简单嘛,两下搞完继续我的摸鱼大业😁.然后ui就丢给了一个网站让我照着这个做(龟龟现成的都有,这也太好了),网站在这里,让我们来看一下简单的交互.


iShot_2023-10-30_11.07.56.gif


看了一下大致就是滚动到特定位置后把导航栏固定,hover的时候显示导航列表,这不是小菜一碟.


滚动到特定位置固定导航


主要逻辑就是下面这段,简单来说就是监听滚动条位置,当滚动到特定高度时改变导航按钮的定位方式,
css部分是用的@emotion/styled,文档可以看一下这里.


const [fixed, setFixed] = useState(false);
useEffect(() => {
const root = document.querySelector('#root');
if (!root) return;
const fn = () => {
if (root.scrollTop > 451) {
setFixed(true);
} else {
setFixed(false);
}
};
root.addEventListener('scroll', fn);
return () => {
root.removeEventListener('scroll', fn);
};
}, []);

const StyledFixed = styled.div<{ fixed?: boolean }>`
width: 0;
position: ${(props) => (props.fixed ? 'fixed' : 'absolute')};
top: ${(props) => (props.fixed ? '80px' : '531px')};
left: 80px;
z-index: 9;
@media (max-width: 1800px) {
left: 12px;
}
`;

效果如下


iShot_2023-10-30_11.24.58.gif


hover显示导航列表


然后就是鼠标移入的时候显示导航列表了,我心想这还不简单,几行css就搞定了(简单描述下就是把导航列表放在按钮里面,给按钮加上hover效果),但是当我研究了一下腾讯网站的代码,我发现事情好像没那么简单.


image.png


导航按钮和列表是平级的,这样的话鼠标移上去列表显示,但列表显示的同时hover效果也没有了,列表就又隐藏了,就会导致闪烁的效果,gif图展示不够明显.


iShot_2023-10-30_11.49.33.gif

现在问题来了,如果是平级元素,那如何控制hover显示呢,答案就在他们的父元素身上,从下面2张图上可以看出,导航按钮以及他的父元素都加上了hover样式


image.png


image.png


默认情况下父元素宽度为0,防止误触发列表展示,hover状态下设置width:auto
image.png


实现效果


到这里,我已经完全清楚了实现原理,完整的代码在下面


const LeftNav = observer(() => {
const { selectedKeys, openKeys } = menuStore;
const [fixed, setFixed] = useState(false);
useEffect(() => {
const root = document.querySelector('#root');
if (!root) return;
const fn = () => {
if (root.scrollTop > 451) {
setFixed(true);
} else {
setFixed(false);
}
};
root.addEventListener('scroll', fn);
return () => {
root.removeEventListener('scroll', fn);
};
}, []);
const onMenuClick = ({ key }: { key: string }) => {
menuStore.selectedKeys = [key];
if (key) {
const dom = document.querySelector(`#${key}`);
menuStore.scrollingKey = key;
dom?.scrollIntoView({
behavior: 'smooth'
});
}
};

const onOpenChange = (keys: string[]) => {
menuStore.openKeys = keys.filter((i) => i !== openKeys[0]);
};

return (
<StyledFixed fixed={fixed}>
<div className='left-nav-btn'>
<RightOutlinedIcon />
</div>
<div className='left-nav-list'>
<StyledMenuWrap>
<Menu selectedKeys={selectedKeys} openKeys={openKeys} mode='inline' items={MENU} onClick={onMenuClick} onOpenChange={onOpenChange} />
</StyledMenuWrap>
</div>
</StyledFixed>

);
});

const StyledFixed = styled.div<{ fixed?: boolean }>`
width: 0;
position: ${(props) => (props.fixed ? 'fixed' : 'absolute')};
top: ${(props) => (props.fixed ? '80px' : '531px')};
left: 80px;
z-index: 9;
&:hover {
width: auto;
.left-nav-btn {
display: none;
}
.left-nav-list {
transform: none;
visibility: visible;
}
}
@media (max-width: 1800px) {
left: 12px;
}
.left-nav-btn {
position: absolute;
top: 80px;
width: 40px;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
padding: 12px;
border-radius: 100px;
border: 1px solid #fff;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0px 4px 30px 0px rgba(12, 25, 68, 0.05);

cursor: pointer;

&::before {
content: '页面导航';
background: linear-gradient(139deg, #c468ef 5.3%, #2670ff 90.91%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}

&:hover {
display: none;
& + .left-nav-list {
transform: none;
visibility: visible;
}
}
@media (min-width: 1799px) {
display: none;
}
}
.left-nav-list {
position: relative;
width: 172px;
z-index: 1;
transition: all 0.3s ease-in;
@media (max-width: 1800px) {
transform: translateX(-200px);
visibility: hidden;
}
}
`
;

最终的实现效果如下
iShot_2023-10-30_14.39.20.gif


至于腾讯网站中的滚动到对应模块高亮菜单的实现可以看看 IntersectionObserver 这个api,好了本次的分享就到此为止了,感谢各位大佬的阅读与点赞😁,你可以说我菜,因为我是真的菜.


作者:hahayq
来源:juejin.cn/post/7295343805020487690
收起阅读 »

JSON慢地要命: 看看有啥比它快!

web
是的, 你没听错! 网络开发中无处不在的数据交换格式JSON, 可能会拖慢你的应用程序. 在这个速度和响应速度至上的世界里, 检查 JSON 的性能影响至关重要, 而我们对此却常常忽略. 在本博客中, 我们将深入探讨 JSON 成为应用程序瓶颈的原因, 并探索...
继续阅读 »


是的, 你没听错! 网络开发中无处不在的数据交换格式JSON, 可能会拖慢你的应用程序. 在这个速度和响应速度至上的世界里, 检查 JSON 的性能影响至关重要, 而我们对此却常常忽略. 在本博客中, 我们将深入探讨 JSON 成为应用程序瓶颈的原因, 并探索更快的替代方案和优化技术, 以确保你的应用程序以最佳状态运行.


JSON 是什么? 为何我要关注这个问题?



JSON 教程 | w3resource


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


JSON 是应用中数据的粘合剂. 它是服务器和客户端之间进行数据通信的语言, 也是数据库和配置文件中存储数据的格式.


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


JSON 在Web开发领域的受欢迎程度怎么强调都不为过. 它已成为数据交换的事实标准, 这其中有几个令人信服的原因:


它易于使用!



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

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

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

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

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

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


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


速度需求


🚀🚀🚀


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


在当今快节奏的数字环境中, 应用的速度和响应能力是不可或缺的. 用户希望在Web和移动应用中即时获取信息, 快速交互和无缝体验. 对速度的这种要求是由以下几个因素驱动的:



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

  2. 竞争优势: 速度可以成为重要的竞争优势. 反应迅速的应用往往比反应迟缓的应用更能吸引和留住用户.

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

  4. 转化率: 电子商务网站尤其清楚速度对转化率的影响. 网站速度越快, 转换率越高, 从而增加收入.

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


JSON 会拖慢我们的应用吗?


现在, 让我们来讨论核心问题: JSON 是否会拖慢我们的应用?


如前所述, JSON 是一种非常流行的数据交换格式. 它灵活, 易用, 并得到广泛支持. 然而, 这种广泛的应用并不意味着它不会面临性能挑战.


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


在接下来的内容中, 我们将探讨 JSON 成为应用瓶颈的具体原因, 更重要的是, 探讨如何缓解这些问题. 在深入探讨的过程中, 请记住我们的目标不是诋毁 JSON, 而是了解其局限性并发现优化其性能的策略, 以追求更快, 反应更灵敏的应用.



LinkedIn将Protocal Buffers与Rest.li集成以提高微服务性能| LinkedIn工程


JSON 为什么会变慢


尽管 JSON 被广泛使用, 但它也难逃性能挑战. 让我们来探究 JSON 可能会变慢的原因, 并理解为什么 JSON 并不总是数据交换的最佳选择.


1. 解析带来的开销


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


2. 序列化和反序列化


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


微服务架构的世界里, JSON 通常用于在服务之间传递消息. 但是, 很关键的是, 我们必须认识到, JSON 消息需要序列化和反序列化, 这两个过程会带来巨大的开销.



在有大量微服务不断通信的场景中, 这种开销可能会增加, 并有可能使应用变慢, 以至于影响用户体验.




我们面临的第二个挑战是, 由于 JSON 的文本性质, 序列化和反序列化的延迟和吞吐量都不理想.
— LinkedIn



1_74sQfiW0SjeFfcTcgNKupw.webp
序列化和反序列化


3. 字符串操作


JSON 基于文本, 在连接和解析等操作中严重依赖字符串操作. 与处理二进制数据相比, 处理字符串的速度会慢一些.


4. 缺乏数据类型


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



5. 冗余


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



第一个挑战是 JSON 是一种文本格式, 往往比较冗余. 这导致网络带宽使用量增加, 更高的延迟, 效果并不理想.
— LinkedIn



6. 不支持二进制


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


7. 深度嵌套


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


JSON的替代方案


虽然 JSON 是一种通用的数据交换格式, 但由于其在某些情况下的性能限制, 人们开始探索更快的替代格式. 让我们深入探讨其中的一些替代方案, 了解何时以及为何选择它们:


1. Protocol Buffers(protobuf)


Protocal Buffers通常被称为protobuf, 是由谷歌开发的一种二进制序列化格式. 它的设计宗旨是高效, 紧凑和快速. Protobuf 的二进制性质使其在序列化和反序列化方面的速度明显快于 JSON.



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


GitHub - vaishnav-mk/protobuf-example


2. MessagePack


MessagePack 是另一种二进制序列化格式, 以速度快, 结构紧凑而著称. 它比 JSON 更有效率, 同时与各种编程语言保持兼容.



  • 何时选择: 当你需要在速度和跨语言兼容性之间取得平衡时, MessagePack 是一个不错的选择. 它适用于实时应用和对减少数据大小至关重要的情况.


3. BSON (二进制 JSON)


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



  • 何时选择: 如果你正在使用 MongoDB, 或者需要一种格式来弥补 JSON 和二进制效率之间的差距, 那么 BSON 是一个很有价值的选择.


4. Apache Avro


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



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


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



JSON, Protobufs, MessagePack, BSON 和 Avro 之间的差异


每个字节都很重要: 优化数据格式


在效率和速度至上的数据交换世界中, 数据格式的选择会产生天壤之别. 本节将探讨从简单的 JSON 数据表示到更高效的二进制格式(如 Protocol Buffers, MessagePack, BSON 和 Avro)的过程. 我们将深入探讨每种格式的细微差别, 并展示为什么每个字节都很重要.


开始: JSON 数据


我们从简单明了的 JSON 数据结构开始. 下面是我们的 JSON 数据示例片段:


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

JSON 总大小: ~ 139 字节


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


效率挑战: 使用二进制格式减小尺寸



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


Protocol Buffers (protobuf):


syntax = "proto3";

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

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

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

Protocol Buffers 总大小: ~ 38 bytes


MessagePack:


(注意:MessagePack 是一种二进制格式, 此处的表示法非人工可读.)


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


a36a6964000000000a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d042100000005011241313302bdcccc3f0530112434353625029a99993f


MessagePack 总大小: ~34 字节


BSON (二进制 JSON):


(注意:BSON 是一种二进制格式, 此处的表示法非人工可读.)


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


3e0000001069640031000a4a6f686e20446f6502656d61696c006a6f686e646f65406578616d706c652e636f6d1000000022616765001f04370e4940

BSON 总大小: ~ 43 字节


Avro:


(注: Avro使用schema, 因此数据与schema信息一起编码.)


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


0e120a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d049a999940040a020b4108312e3525312e323538323539

Avro 总大小: ~ 32 字节



(这些替代方案的实际字节数可能会有所不同, 提供这些数字只是为了让大家有个大致的了解.)


现在你可能会感到奇怪, 为什么我们的程序会有这么多的字节数?


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


1. Avro:



  • Avro 使用schema对数据进行编码, 二进制表示法中通常包含该schema.

  • Avro 基于schema的编码可提前指定数据结构, 从而实现高效的数据序列化和反序列化.

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


2. MessagePack:



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

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

  • MessagePack 不包含schema信息, 因此更适用于schema已预先知道并在发送方和接收方之间共享的情况.


3. BSON:



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

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

  • 与 MessagePack 一样, BSON 不包含schema信息.


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



  • Avro 包含模式信息并具有自描述性, 这导致二进制大小稍大, 但提供了schema兼容性.

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

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


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


优化 JSON 性能


JSON 虽然用途广泛, 在Web开发中被广泛采用, 但在速度方面也存在挑战. 这种格式的人类可读性会导致数据负载较大, 处理时间较慢. 因此, 问题出现了: 我们能够怎样优化JSON以使得它更快更高效? 在本文中, 我们将探讨可用于提高 JSON 性能的实用策略和优化方法, 以确保 JSON 在提供应用所需的速度和效率的同时, 仍然是现代 Web 开发中的重要工具.


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


1. 最小化数据大小:



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


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

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


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


// Inefficient
{
"transaction_type": "purchase"
}

// Efficient
{
"txnType": "purchase"
}

2. 明智地使用数组:



  • 最小化嵌套: 避免深度嵌套数组, 因为它们会增加解析和遍历 JSON 的复杂性.


// Inefficient
{
"order": {
"items": {
"item1": "Product A",
"item2": "Product B"
}
}
}

// Efficient
{
"orderItems": ["Product A", "Product B"]
}

3. 优化数字表示:



  • 尽可能使用整数:  如果数值可以用整数表示, 请使用整数而不是浮点数.


// Inefficient
{
"quantity": 1.0
}

// Efficient
{
"quantity": 1
}

4. 消除冗余:



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


// Inefficient
{
"product1": {
"name": "Product A",
"price": 10
},
"product2": {
"name": "Product A",
"price": 10
}
}

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

5. 使用压缩:



  • 使用压缩算法:  如何可行的话, 使用压缩算法, 比如Gzip 或者Brotli, 以在传输过程中减少JSON负载大小.


// Node.js example using zlib for Gzip compression
const zlib = require('zlib');

const jsonData = {
// Your JSON data here
};

zlib.gzip(JSON.stringify(jsonData), (err, compressedData) => {
if (!err) {
// Send compressedData over the network
}
});

6. 采用服务器端缓存:



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


7. 剖析与优化:



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



请记住, 你实施的具体优化措施应符合应用的要求和限制.



真实世界的优化: 在实践中加速


在这一部分, 我们将深入探讨现实世界中遇到 JSON 性能瓶颈并成功解决的应用和项目. 我们将探讨企业如何解决 JSON 的局限性, 以及这些优化为其应用带来的切实好处. 从 LinkedIn 和 Auth0 这样的知名平台到 Uber 这样的颠覆性科技巨头*, 这些示例为我们提供了宝贵的见解, 让我们了解在尽可能利用 JSON 的多功能性的同时提高速度和响应能力的策略.


1. LinkedIn集成Protocol Buffers:


挑战: LinkedIn 面临的挑战是 JSON 的冗长以及由此导致的网络带宽使用量增加, 从而导致延迟增加.
解决方案: 他们在微服务通信中采用了二进制序列化格式 Protocol Buffers 来取代 JSON.
影响: 这一优化将延迟降低了60%, 提高了 LinkedIn 服务的速度和响应能力.


2. Uber的H3地理索引:



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

  • 解决方法 他们引入了H3地理索引, 这是一种用于地理空间数据的高效六边形网格系统, 可减少 JSON 解析开销.

  • 影响: 这一优化大大加快了地理空间操作, 增强了 Uber 的叫车和地图服务.


3. Slack的消息格式优化:



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

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

  • 影响: 这一优化提高了消息渲染速度, 改善了 Slack 用户的整体聊天性能.


4. Auth0的Protocal Buffers实现:



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

  • 解决方案: 他们采用Protocal Buffers来替代 JSON, 以编解码与身份验证相关的数据.

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


这些真实案例表明, 通过优化策略解决 JSON 的性能难题, 可对应用的速度, 响应和用户体验产生重大积极影响. 它们强调了在各种应用场景中考虑使用替代数据格式和高效数据结构来克服 JSON 相关缓慢的问题的重要性.


总结一下


在开发领域, JSON 是数据交换不可或缺的通用工具. 其人类可读格式和跨语言兼容性使其成为现代应用的基石. 然而, 正如我们在本文中所探讨的, JSON 的广泛应用并不能使其免于性能挑战.


我们在优化 JSON 性能的过程中获得的主要启示是显而易见的:



  • 性能至关重要: 在当今的数字环境中, 速度和响应速度至关重要. 用户希望应用能够快如闪电, 即使是微小的延迟也会导致不满和机会的丧失.

  • 尺寸至关重要: 数据有效载荷的大小会直接影响网络带宽的使用和响应时间. 减少数据大小通常是优化 JSON 性能的第一步.

  • 替代格式: 当效率和速度至关重要时, 探索其他数据序列化格式, 如Protocal Buffers, MessagePack, BSON 或 Avro.

  • 真实世界案例: 从企业成功解决 JSON 速度变慢问题的实际案例中学习. 这些案例表明, 优化工作可以大幅提高应用的性能.


在继续构建和增强Web应用时, 请记住要考虑 JSON 对性能的影响. 仔细设计数据结构, 选择有意义的键名, 并在必要时探索其他序列化格式. 这样, 你就能确保你的应用在速度和效率方面不仅能满足用户的期望, 而且还能超越用户的期望.


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


作者:bytebeats
来源:juejin.cn/post/7299353265099423753
收起阅读 »

仿写el-upload组件,彻底搞懂文件上传

web
用了那么久的Upload组件,你知道是怎么实现的么,今天就来仿写一个饿了么el-upload vue组件,彻底搞懂前端的文件上传相关知识! 要实现的props 参数说明action必选参数,上传的地址headers设置上传的请求头部multiple是否支持多选...
继续阅读 »

用了那么久的Upload组件,你知道是怎么实现的么,今天就来仿写一个饿了么el-upload vue组件,彻底搞懂前端的文件上传相关知识!


要实现的props


参数说明
action必选参数,上传的地址
headers设置上传的请求头部
multiple是否支持多选文件
data上传时附带的额外参数
name上传的文件字段名
with-credentials支持发送 cookie 凭证信息
show-file-list是否显示已上传文件列表
drag是否启用拖拽上传
accept接受上传的文件类型
on-preview点击文件列表中已上传的文件时的钩子
on-remove文件列表移除文件时的钩子
on-success文件上传成功时的钩子
on-error文件上传失败时的钩子
on-progress文件上传时的钩子
on-change添加文件时被调用
before-upload上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。
before-remove删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 Promise 且被 reject,则停止删除。
list-type文件列表的类型
auto-upload是否在选取文件后立即进行上传
file-list上传的文件列表, 例如: [{name: 'food.jpg', url: 'xxx.cdn.com/xxx.jpg'}]
limit最大允许上传个数
on-exceed文件超出个数限制时的钩子

参考:element.eleme.cn/#/zh-CN/com…


这里面有几个重要的点:



  1. input file 的美化

  2. 多选

  3. 拖拽


一个个实现


创建upload组件文件


src/components/upload/index.vue


<template></template>
<script setup>
  // 属性太多,把props单独放一个文件引入进来
  import property from './props'
  const props = defineProps(property)
</script>
<style></style>

./props.js


export default {
  action: {
    typeString
  },
  headers: {
    typeObject,
    default: {}
  },
  multiple: {
    typeBoolean,
    defaultfalse
  },
  data: {
    typeObject,
    default: {}
  },
  name: {
    typeString,
    default'file'
  },
  'with-credentials': {
    typeBoolean,
    defaultfalse
  },
  'show-file-list': {
    typeBoolean,
    defaulttrue,
  },
  drag: {
    typeBoolean,
    defaultfalse
  },
  accept: {
    typeString,
    default''
  },
  'list-type': {
    typeString,
    default'text' // text、picture、picture-card
  },
  'auto-upload': {
    typeBoolean,
    defaulttrue
  },
  'file-list': {
    typeArray,
    default: []
  },
  disabled: {
    typeBoolean,
    defaultfalse
  },
  limit: {
    typeNumber,
    defaultInfinity
  },
  'before-upload': {
    typeFunction,
    default() => {
      return true
    }
  },
  'before-remove': {
    typeFunction,
    default() => {
      return true
    }
  }

具体的编写upload组件代码


1. 文件上传按钮的样式


我们都知道,<input type="file">的默认样式是这样的: 很丑,并且无法改变其样式。


解决办法:可以把input隐藏,重新写个按钮点击来触发input的文件选择。


<template>
  <input 
     type="file" 
     id="file" 
     @change="handleChange"
  >

  <button 
     class="upload-btn" 
     @click="choose"
  >

    点击上传
  </button>
</template>
<script setup>
  // 触发选择文件
  const choose = () => {
    document.querySelector('#file').click()
  }
  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
  }
</script>
<style scoped>
  #file {
    display: none;
  }
  .upload-btn {
    border: none;
    background-color#07c160;
    color#fff;
    padding6px 10px;
    cursor: pointer;
  }
</style>

效果:



这样也是可以调起文件选择框,并触发input的onchange事件。



2. 多选


直接在input上加一个Booelan属性multiple,根据props中的值动态设置


顺便把accept属性也加上


<template>
  <input 
     type="file" 
     id="file" 
     :multiple="multiple"
     :accept="accept"
     @change="handleChange"
  >
</template>

3. 拖拽


准备一个接收拖拽文件的区域,props传drag=true就用拖拽,否则就使用input上传。


<template>
  <input 
    type="file" 
    id="file" 
    :multiple="multiple"
    :accept="accept"
    @change="handleChange"
  >
  <button 
     class="upload-btn" 
     v-if="!drag" 
     @click="choose"
  >
    点击上传
  </button>
  <div 
    v-else 
    class="drag-box" 
    @dragover="handleDragOver"
    @dragleave="handleDragLeave"
    @drop="handleDrop"
    @click="choose"
    :class="{'dragging': isDragging}"
  >
    将文件拖到此处,或<span>点击上传</span>
  </div>
</template>

dragging用来拖拽鼠标进入时改变样式


<script setup>
  const isDragging = ref(false)
  // 拖放进入目标区域
  const handleDragOver = (event) => {
    event.preventDefault()
    isDragging.value = true
  }
  const handleDragLeave = (event) => {
    isDragging.value = false
  }
  let files = []
  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log(files);
  }
</script>

.drag-box {
    width240px;
    height150px;
    line-height150px;
    text-align: center;
    border1px dashed #ddd;
    cursor: pointer;
    border-radius8px;
  }
  .drag-box:hover {
    border-color: cornflowerblue;
  }
  .drag-box.dragging {
    background-colorrgb(131161216, .2);
    border-color: cornflowerblue;
  }
  .drag-box span {
    color: cornflowerblue;
  }

跟使用input上传效果一样


4. 上传到服务器


并实现on-xxx钩子函数


  const emit = defineEmits()
  const fileList = ref([])
  let files = []

  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  const handleBeforeUpload = (files) => {
    if (files.length > props.limit - fileList.value.length) {
      console.error(`当前限制选择 ${props.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.value.length} 个文件`)
      emit('on-exceed', files, toRaw(fileList.value))
      return
    }
    // 可以把锁哥文件放到一个formData中一起上传,
    // 遍历文件一个个上传,这里一个个上传是为了实现钩子函数回调时返回对应的file对象。
    files.forEach(async file => {
      emit('on-change', file, files)
      if (!props.beforeUpload()) {
        return
      }
      if (props.autoUpload) {
        uploadRequest(file, files)
      }
    })
  }

  // 手动上传已选择的文件
  const submit = () => {
    files.forEach(async file => {
      uploadRequest(file, files)
    })
  }
  
  // 保存xhr对象,用于后面取消上传
  let xhrs = []
  const uploadRequest = async (file, files) => {
    let xhr = new XMLHttpRequest();
    // 调用open函数,指定请求类型与url地址。请求类型必须为POST
    xhr.open('POST', props.action);
    // 设置自定义请求头
    Object.keys(props.headers).forEach(k => {
      xhr.setRequestHeader(k, props.headers[k])
    })
    // 额外参数
    const formData = new FormData()
    formData.append('file', file);
    Object.keys(props.data).forEach(k => {
      formData.append(k, props.data[k]);
    })
    // 携带cookie
    xhr.withCredentials = props.withCredentials
    xhr.upload.onprogress = (e) => {
      emit('on-progress', e, file, files)
    }
    // 监听状态
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        const res = JSON.parse(xhr.response)
        const fileObj = {
          name: file.name,
          percentage: 100,
          raw: file,
          response: res,
          status: 'success',
          size: file.size,
          uid: file.uid,
        }
        fileList.value.push(fileObj)
        if (xhr.status === 200 || xhr.status === 201) {
          emit('on-success', res, fileObj, toRaw(fileList.value))
        } else {
          emit('on-error', res, fileObj, toRaw(fileList.value))
        }
      }
    }
    // 发起请求
    xhr.send(formData);
    xhrs.push({
      xhr,
      file
    })
  }

  const preview = (file) => {
    emit('on-preview', file)
  }

  const remove = (file, index) => {
    if (!props.beforeRemove()) {
      return
    }
    fileList.value.splice(index, 1)
    emit('on-remove', file, fileList.value)
  }

  // 取消上传
  const abort = (file) => {
    // 通过file对象找到对应的xhr对象,然后调用abort
    // xhr.abort()
  }

  defineExpose({
    abort,
    submit
  })

全部代码


<template>
  <input 
    type="file" 
    id="file" 
    :multiple="multiple"
    :accept="accept"
    @change="handleChange"
  >

  <button class="upload-btn" v-if="!drag" @click="choose">
    点击上传
  </button>
  <div 
    v-else 
    class="drag-box" 
    @dragover="handleDragOver"
    @dragleave="handleDragLeave"
    @drop="handleDrop"
    @click="choose"
    :class="{'dragging': isDragging}"
  >

    将文件拖到此处,或<span>点击上传</span>
  </div>
  <template v-if="showFileList">
    <template v-if="listType === 'text'">
      <p class="file-item" v-for="(file, index) in fileList" :key="index" @click="preview(file)">
        <span>{{file.name}}</span>
        <span class="remove" @click.stop="remove(file, index)">×</span>
      </p>
    </template>
  </template>
</template>

<script setup>
  import { ref, toRaw, onMounted } from 'vue'
  import property from './props'
  const props = defineProps(property)
  const emit = defineEmits()

  const fileList = ref([])
  const isDragging = ref(false)

  // 触发选择文件
  const choose = () => {
    document.querySelector('#file').click()
  }

  // 拖放进入目标区域
  const handleDragOver = (event) => {
    event.preventDefault()
    isDragging.value = true
  }

  const handleDragLeave = (event) => {
    isDragging.value = false
  }

  let files = []

  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  const handleBeforeUpload = (files) => {
    if (files.length > props.limit - fileList.value.length) {
      console.error(`当前限制选择 ${props.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.value.length} 个文件`)
      emit('on-exceed', files, toRaw(fileList.value))
      return
    }
    files.forEach(async file => {
      emit('on-change', file, files)
      if (!props.beforeUpload()) {
        return
      }
      if (props.autoUpload) {
        uploadRequest(file, files)
      }
    })
  }

  // 手动上传已选择的文件
  const submit = () => {
    files.forEach(async file => {
      uploadRequest(file, files)
    })
  }

  let xhrs = []
  const uploadRequest = async (file, files) => {
    let xhr = new XMLHttpRequest();
    // 调用open函数,指定请求类型与url地址。请求类型必须为POST
    xhr.open('POST', props.action);
    // 设置自定义请求头
    Object.keys(props.headers).forEach(k => {
      xhr.setRequestHeader(k, props.headers[k])
    })
    // 额外参数
    const formData = new FormData()
    formData.append('file', file);
    Object.keys(props.data).forEach(k => {
      formData.append(k, props.data[k]);
    })
    // 携带cookie
    xhr.withCredentials = props.withCredentials
    xhr.upload.onprogress = (e) => {
      emit('on-progress', e, file, files)
    }
    // 监听状态
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        const res = JSON.parse(xhr.response)
        const fileObj = {
          name: file.name,
          percentage100,
          raw: file,
          response: res,
          status'success',
          size: file.size,
          uid: file.uid,
        }
        fileList.value.push(fileObj)
        if (xhr.status === 200 || xhr.status === 201) {
          emit('on-success', res, fileObj, toRaw(fileList.value))
        } else {
          emit('on-error', res, fileObj, toRaw(fileList.value))
        }
      }
    }
    // 发起请求
    xhr.send(formData);
    xhrs.push({
      xhr,
      file
    })
  }

  const preview = (file) => {
    emit('on-preview', file)
  }

  const remove = (file, index) => {
    if (!props.beforeRemove()) {
      return
    }
    fileList.value.splice(index, 1)
    emit('on-remove', file, fileList.value)
  }

  // 取消上传
  const abort = (file) => {
    // 通过file对象找到对应的xhr对象,然后调用abort
    // xhr.abort()
  }

  defineExpose({
    abort,
    submit
  })
</script>

<style scoped>
  #file {
    display: none;
  }
  .upload-btn {
    border: none;
    background-color#07c160;
    color#fff;
    padding6px 10px;
    cursor: pointer;
  }
  .drag-box {
    width240px;
    height150px;
    line-height150px;
    text-align: center;
    border1px dashed #ddd;
    cursor: pointer;
    border-radius8px;
  }
  .drag-box:hover {
    border-color: cornflowerblue;
  }
  .drag-box.dragging {
    background-colorrgb(131161216, .2);
    border-color: cornflowerblue;
  }
  .drag-box span {
    color: cornflowerblue;
  }
  .file-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top12px;
    padding0 8px;
    border-radius4px;
    cursor: pointer;
  }
  .file-item:hover {
    background-color#f5f5f5;
    color: cornflowerblue;
  }
  .file-item .remove {
    font-size20px;
  }
</style>

如何使用


<template>
<upload
ref="uploadRef"
action="http://localhost:3000/upload"
multiple
show-file-list
drag
auto-upload
upload-folder
:headers="headers"
:data="data"
:limit="3"
:before-upload="beforeUpload"
:before-remove="beforeRemove"
@on-change="handleChange"
@on-success="handleSuccess"
@on-error="handleError"
@on-preview="handlePreview"
@on-remove="handleRemove"
@on-exceed="handleExceed"
@on-progress="handleProgress"
>

</upload>
</template>

<script setup>
import { ref } from 'vue'
import upload from '@/components/upload/index.vue'

const uploadRef = ref(null)

const data = {
name: '张三',
age: 20
}

const headers = {
a: 111
}

const handleChange = (file, fileList) => {
// console.log('onChange', file, fileList)
}
const handleSuccess = (res, file, fileList) => {
console.log(res, file, fileList)
}
const handleError = (err, file, fileList) => {
console.log(err, file, fileList)
}
const handlePreview = (file) => {
console.log('handlePreview', file)
}
const handleRemove = (file, fileList) => {
console.log('handleRemove', file, fileList)
}
const handleExceed = (files, fileList) => {
console.log('文件个数超限', files, fileList)
}
const handleProgress = (e, file, fileList) => {
console.log('上传进度');
if (e.lengthComputable) {
const percentComplete = Math.ceil((e.loaded / e.total) * 100)
console.log('[ percentComplete ] >', percentComplete)
}
}
const beforeUpload = (file) => {
return true
}
const beforeRemove = (file, fileList) => {
return true
}
</script>

<style>

</style>


作者:xintianyou
来源:juejin.cn/post/7292302859964727346
收起阅读 »

前端如何直接上传文件夹

web
前面写了一篇仿写el-upload组件,彻底搞懂文件上传,实现了选择/拖拽文件上传,我们经常看到一些网站支持直接选择整个文件夹上传,例如:宝塔面板、cloudflare托管、对象存储网站等等需要模拟文件路径存储文件的场景。那是怎么实现的呢? 依然从两方面来说:...
继续阅读 »

前面写了一篇仿写el-upload组件,彻底搞懂文件上传,实现了选择/拖拽文件上传,我们经常看到一些网站支持直接选择整个文件夹上传,例如:宝塔面板、cloudflare托管、对象存储网站等等需要模拟文件路径存储文件的场景。那是怎么实现的呢?


依然从两方面来说:



  1. input选择文件夹

  2. 拖拽文件夹


input选择文件夹


在props.js中加一个属性,upload-folder是否支持上传文件夹


export default {
// 前面的省略了...
// 是否支持选择文件夹
'upload-folder': {
type: Boolean,
default: false
}
}

改一下input标签,依然是根据props的值动态判断是否支持上传文件夹。主要是webkitdirectory这个属性,由于不是一个标准属性,需要加浏览器前缀。


<input 
type="file"
id="file"
:multiple="multiple"
:accept="accept"
:webkitdirectory="uploadFolder"
:mozdirectory="uploadFolder"
:odirectory="uploadFolder"
@change="handleChange"
>


注意:支持选择文件夹时就只能选择文件夹,无法选择文件。


那么如何获取选择的文件夹呢?其实我们最终要上传的依然是文件,也就是file对象,文件夹也是一个特殊的文件。


依然是通过inputonchange事件回调拿到上传的event


或者直接获取input这个dom对象,然后拿到files属性,结果是一样的。


// input选择文件回调
const handleChange = (event) => {
console.log('[ files ] >', event.target.files)
const inputDom = document.querySelector('#file')
console.log('[ files ] >', inputDom.files)
}


可以看到,比选择单个文件时,多了一个webkitRelativePath属性,并且它是递归选择的文件夹,拿到这个文件夹及其子文件夹下所有的文件,我们可以通过这个属性拿到上传时文件所在的文件夹名称路径


拖拽文件夹


上篇文章讲过拖拽如何拿到文件,首先要准备一个用于拖拽放置的区域。
调用upload组件时,传入drag=true


<div 
class="drag-box"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>

将文件拖到此处,或<span>点击上传span>
div>

// 拖放进入目标区域
const handleDragOver = (event) => {
event.preventDefault()
}
// 拖拽放置
const handleDrop = (event) => {
event.preventDefault()
console.log('[ event ] >', event)
}

注意:和input上传不同,拖拽时,是可以同时拖拽文件和文件夹的。


因为可以同时拖拽文件和文件夹,我们就不能直接使用event.dataTransfer.files,如果刚好拖拽进来的是一个文件,那可以这么获取,如果是个文件夹呢?那就不行了。


同时拖拽一个文件和一个文件夹


这时候就要用到event.dataTransfer.items


// 拖拽放置
const handleDrop = (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
}

打印一下看看:

得到一个List类型的数据,里面是两个DataTransferItem,控制台无法直接查看它到底是个什么玩意儿。

看MDN,也看不出它具体是个啥。既然是List,遍历一下看看:


const handleDrop = (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
for (const item of event.dataTransfer.items) {
console.log('[ item ] >', item)
}
}


可以看到不管是文件还是文件夹,都被识别成了file,只不过图片是直接能识别出type为image/png


查看MDN,developer.mozilla.org/zh-CN/docs/…


点击查看itemPrototype,发现里面有个webkitGetAsEntry方法,执行它就能拿到item的具体信息。


看方法名,带了个webkit,但是这个方法除了Android Firefox浏览器以外都可以用。


for (const item of event.dataTransfer.items) {
const entry = item.webkitGetAsEntry()
console.log(entry)
}

依然拖动上面那个图片文件和一个文件夹:


可以看出,文件夹里面还有文件和文件夹,但是只显示了一个文件和一个文件夹,看来拖拽和input上传不一样,它不会自动的把里面所有的文件递归列出来。


通过isDirectory属性,就能区分是文件还是文件夹。除了这些基础属性以外,继续查看Prototype,可以看到还有一系列方法:


先看怎么拿到文件


entry是一个文件时,它有两个方法:createWriter()file(),查看MDN,developer.mozilla.org/en-US/docs/…

createWriter()已经废弃了,而且也不是我们今天要用的。

file()才是我们要找的。


这不就是我们熟悉的file对象吗,跟input上传拿到的一毛一样。


再看怎么拿到文件夹


查看MDN的Drop API webkitGetAsEntry()方法,developer.mozilla.org/zh-CN/docs/… 可得,如果是文件夹,可以通过createReader方法创建一个文件目录阅读器,然后通过readEntries方法,重新拿到每个item,这就是event.dataTransfer.items里面的每个item

我们写一下试试

依然是之前那个图片和文件夹

只打印出了跟目录下一级的一个文件和一个文件夹,那下面还有一个文件怎么办呢?


递归呀!


写一个递归读文件的方法。


const readFiles = async (item) => {
if (item.isDirectory) {
// 是一个文件夹
console.log('=======文件夹=======');
const directoryReader = item.createReader();
// readEntries是一个异步方法
const entries = await new Promise((resolve, reject) => {
directoryReader.readEntries(resolve, reject);
});

let files = [];
for (const entry of entries) {
const resultFiles = await readFiles(entry);
files = files.concat(resultFiles);
}
return files;
} else {
// 是一个文件
console.log('=======文件=======');
// file也是一个异步方法
const file = await new Promise((resolve, reject) => {
item.file(resolve, reject);
});
console.log('[ file ] >', file);
return [file];
}
}

handleDrop方法也要改一下


// 拖拽放置
const handleDrop = async (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)

const files = [];
const promises = [];
for (const item of event.dataTransfer.items) {
const entry = item.webkitGetAsEntry();
console.log('[ entry ] >', entry);
promises.push(readFiles(entry));
}

const resultFilesArrays = await Promise.all(promises);
const allFiles = resultFilesArrays.flat();

console.log('[ All files ] >', allFiles);
}

再次拖拽上传看看

三个文件我们都拿到了。


总结


上传文件夹,还是直接使用input比较简单,使用它能直接拿到文件夹下所有的文件,以及每个文件在本地的路径,代码量也少很多。


拖拽的好处是文件和文件夹能一起上传。


作者:xintianyou
来源:juejin.cn/post/7292323606875553843
收起阅读 »

vue3 轮播图的实现

web
最近开发过程中,有一些轮播图的需求,虽然公司的组件库已经有swiper的组件,但是功能不全,很多效果实现不了,于是经过翻找swiper的官网,发现你想要的样式都有,下面来说一下swiper的简单使用。 想实现的效果 点击prev和next可实现图片的切换 安...
继续阅读 »

最近开发过程中,有一些轮播图的需求,虽然公司的组件库已经有swiper的组件,但是功能不全,很多效果实现不了,于是经过翻找swiper的官网,发现你想要的样式都有,下面来说一下swiper的简单使用。


想实现的效果


点击prev和next可实现图片的切换


image.png


安装


swiper的安装是比较简单的。

中文官网:http://www.swiper.com.cn/index.html

英文官网: swiperjs.com/


npm i swiper

使用


接下来就是swiper的使用了,swiper的使用非常简单。可查看官网例子

codesandbox.io/p/sandbox/2…


例子有归有,使用简单归简单,但是实现的样式和自己想要的差距还是很大,查了一波资料,现将代码放出,哈哈。

html


<swiper
:navigation="{
nextEl: '.swiper-button-next1',
prevEl: '.swiper-button-prev1'
}"

:modules="modules"
class="mySwiper"
:slides-per-view="3"
>

<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<div class="swiper-button-prev-one" slot="button-prev">
<Button>
</Button>
</div>
<div class="swiper-button-next-one" slot="button-next">
<Button>
</Button>
</div>
</swiper>

通过navigation来控制轮播图的上一页,下一页,通过slides-per-view来控制每页显示几张图片。
js


import { Swiper, SwiperSlide, navigationPrev } from 'swiper/vue'
import 'swiper/css'
import 'swiper/css/navigation'
import { Navigation } from 'swiper/modules'
const modules = [Navigation]

js部分也是毕竟简单的,把该引入的文件引入即可。这样难道就实现效果了吗,当然不是,还需要改css样式


css(css部分部分采用tailwindcss编写)


.mySwiper {
@apply pb-2xl;
.swiper-button-prev-one {
@apply text-[#333] absolute text-[.875rem] left-0 bottom-0 cursor-pointer;
:deep(.m-button) {
@apply w-6 h-6;

span {
@apply text-sm #{!important};
}
}
}
.swiper-button-next-one {
@apply text-[#333] absolute text-[.875rem] left-[2.5rem] bottom-0 cursor-pointer;
:deep(.m-button) {
@apply w-6 h-6 bg-[#000] text-[#fff];
span {
@apply text-sm #{!important};
}
}
}
}

至此轮播图的效果就实现了,在做轮播图的需求时,需要仔细认真地查看文档,我是比较喜欢看英文文档,我觉得讲述比较全,大家学习的时候自行选择即可。


作者:zhouzhouya
来源:juejin.cn/post/7298907435061100595
收起阅读 »

容易忽视前端点击劫持

web
有一句话叫做:你看到的,不一定是真的,但可能是想让你看到的。在学习前端之前,有幸学习过一阵子PPT和Flash设计,其中有一个知识点就是,视觉效果,最常用的套路就是使用遮罩层来制作效果,今天就聊聊基于遮罩导致的前端攻击:点击劫持。 前端点击劫持 前端点击劫持实...
继续阅读 »

有一句话叫做:你看到的,不一定是真的,但可能是想让你看到的。在学习前端之前,有幸学习过一阵子PPT和Flash设计,其中有一个知识点就是,视觉效果,最常用的套路就是使用遮罩层来制作效果,今天就聊聊基于遮罩导致的前端攻击:点击劫持。


前端点击劫持


前端点击劫持实际上就是通过层叠样式,在原有的页面样式上叠加自己的内容,然后通过色彩或者透明消除用户的警惕,当用户点击看到的功能的时候,实际上点击的是隐藏的功能,这样满足攻击者的需求。比如:


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>前端劫持</title>
   <style>
       .myidea{
           width: 100%;
           height: 100%;
           position: absolute;
           top: 0;
           left: 0;
           opacity: 0.0;
      }
   
</style>
</head>
<body>
   <div>
      视觉效果展示内容
       <img src="F:\dazejiuzhang\图片资料\yqkfdx1.jpg">
   </div>
   <div class="myidea">
       <a href="http://www.jiece.com">跳转链接</a>
   </div>
</body>

</html>

视觉效果
image.png


但实际上


image.png


当然,这个案例当中只是采用了一个透明的div,在实际的劫持场景当中,更多的是采用复杂的iframe来覆盖。


前端点击劫持防御


知道原理之后,防御也就需要琢磨了:


1、在自己不使用iframe的前提下,对iframe进行限制,这个可以通过HTTP头进行设置X-Frame-Options属性可以实现,X-Frame-Options有三种模式:


属性描述
DENY浏览器拒绝解析当前页面任何frame;
SAMEORIGIN浏览器只解析当前页面同源的frame;
ALLOW-FROM origin浏览器只解析当前页面当中设置的origin的的frame;

配置在nginx当中设置:


add_header X-Frame-Options DENY;

2、检测所有提交的内容,防止通过前端XSS攻击携带前端点击劫持内容,这个和防御XSS攻击类似。


3、通过JS限制顶级的嵌套样视:


<head>
 <style id="click_box">
   html {
     display: none !important;
  }
 
</style>
</head>
<body>
 <script>
   if (self == top) {
     var style = document.getElementById('click_box')
     document.body.removeChild(style)
  } else {
     top.location = self.location
  }
 
</script>
</body>

思考


前端点击劫持本身攻击成本比XSS要高一点,并且容易被防御,但是,更容易出现在大家不注意,不经意的地方,这里忽然想到之前一个大佬聊的一句话:搞安全的最后,最难的就是要搞懂人心。“关于前端点击劫持就先聊这么多,还请各位大佬多多指点。


作者:老边
来源:juejin.cn/post/7228932662814769207
收起阅读 »

简单清理掉项目中没用的180+文件

web
遇到的痛点 这篇文章或许有另一个不太优雅的名字--“屎山治理”。 在繁重的业务开发当中,我们会面临一些问题。伴随着项目的不断发展,项目出现代码冗余,存在大片没用代码的情况。 举个栗子,重构优化后,某位同学没有删除掉冗余代码,项目残留着废弃的没用文件,导致跨文件...
继续阅读 »

遇到的痛点


这篇文章或许有另一个不太优雅的名字--“屎山治理”。


在繁重的业务开发当中,我们会面临一些问题。伴随着项目的不断发展,项目出现代码冗余,存在大片没用代码的情况。


举个栗子,重构优化后,某位同学没有删除掉冗余代码,项目残留着废弃的没用文件,导致跨文件引用混乱。还有,业务变更所导致逻辑代码的废弃,项目中重复的定义代码,这些情况在一个长期的项目发展的阶段里面会造成逻辑混乱,重复定义,二义性等等。


其实,程序员都是写代码的,但是很少人敢删代码,久而久之,也就没人敢动废弃代码了。


虽然在项目构建工具的加持下,tree-shaking能够控制项目的包产物体积,但是从开发体验(DX)的角度出发,这往往都是一些心智负担。结合我自己的一些优化经验,简单分享一下:


优化手段


手段一:eslint的unused检查


首先我们应该考虑的是,通过 eslint 的规则有效的去规避一些项目当中已有的没用的变量和方法,这样保证单文件代码的可用性,我们可以很容易的发现哪个import或者variable没有被使用。import的冗余控制也能够有效控制打包的范围,减少包体积。


eslint最常用的就是官方的no-unused-vars这一条规则。


当然还有一些,第三方的unused-exports规则,例如eslint-plugin-canonical的no-unused-exports或者eslint-plugin-unused-imports,这种大家可以适度采用,毕竟eslint是把“双刃剑”。


手段二:静态代码工具扫描


通过一些静态分析工具可以有效地分析代码语法,根据语法树信息来判断内容是有用还是没用。


ts-unused-exports是一个很成熟的分析工具,它可以通过 ts-compiler 对 typescript代码语法进行分析,(tsconfig可以配置allowjs,分析javascript语法),通过TS语法树有效地找到语法中没用的 export。


该工具能够把所有的没用的 export 找到。这时候我们会很自然地想到一个问题,能否找到完全没有使用的废弃文件。这里分两种情况,情况一,该文件所有的 export 都已经被废弃了,这种情况出现在代码重构的情况,另外一种情况是部分的export没有被使用,那这种需要case by case的判断,到底这个代码有没有存在意义?


暂时这个工具只能找到所有的 export 函数,并没有文件粒度,并不能满足我们的“诉求”。我们希望能把完全没用的文件直接删除掉,所以我提了一个issue。


找出所有 export 的文件


我查看了源码,parse过后,会通过getExportMap获取每个文件,且它的所有exports内容。我写了一个PR,在和作者沟通交流下,尽量以最小的 api 改动情况来处理。利用一个参数findCompletelyUnusedFiles来控制是否找出完全没有被使用的文件,参考PR#254


PR 细节


改动涉及最核心内容,如下。将该文件的真实所有 export 和 unused export 作对比,以此判断它是完全没用的文件。


const realExportNames = Object.keys(exports);

if (
extraOptions?.findCompletelyUnusedFiles &&
isEqual(realExportNames, unusedExports)
) {
unusedFiles.push(path);
}
});

当我们得到了这个结果后,我们可以通过自己编写的脚本“大胆”的删除文件了。


在删除脚本内,我们要想清楚几个事情:



  1. 有范围的扫描(避免错删,所有改动在可控的范围内)

  2. 后缀名白名单(多市场的代码可能会存在“多态”,例如,id代表印尼,index.id.ts它不应该被清除掉)


const result = analyzeTsConfig('./tsconfig.json', ['--findCompletelyUnusedFiles']);

const outputPattern = ['/pages/partner/', '/pages/store/', '/pages/staff/', '/services/'];
const excludePattern = ['.id.', '.my.', '.ph.', '.sg.', '.vn.', '.th.', '.br.'];

function filterOutput(name: string) {
for (let index = 0; index < outputPattern.length; index++) {
if (name.includes(outputPattern[index]) ) {
return true;
}
}
return false;
}

function filterExclude(name: string) {
for (let index = 0; index < excludePattern.length; index++) {
if (name.includes(excludePattern[index]) ) {
return false;
}
}
return true;
}

const { unusedFiles, ...rest } = result;

Object.keys(rest)
.filter(r => filterOutput(r))
.filter(r => filterExclude(r))
.map((key) => {
const exportNames = rest[key].map(r=> r.exportName).join(',')
console.log(chalk.green(key) + ' ' + exportNames);
})

if(result.unusedFiles) {
console.log('no used files: ');
result.unusedFiles
.filter(r => filterOutput(r))
.filter(r => filterExclude(r))
.forEach((r) => {
fs.unlinkSync(r);
})
}

手段三:人工调整已有代码的合理性


在删除完代码后,项目中 ts-unused-export 还会扫描出一些部分 export 废弃的文件,我们只能按照自身的情况做出调整。每个团队的代码分层情况有所不同。这些文件可能不需要改动,也可能是需要调整该纯函数位置。我们应该把它们放在合理的位置。


代码优化


总结


首先“清除废弃代码”是一个低频操作。可能我们一年或者几年,清理一次即可,保证代码的“清爽”。所以放在 webpack 等构建工具执行反而不太合适,脚本偶尔扫描,把一些废弃代码清干净,你的DX(developing experience)又回来了。


当然你忍受能力很强也可以“不做”。这篇文章适合具有轻度“代码强迫症”的同学食用。


PS:加餐,也可以参考knip,功能更强大噢。


作者:brandonxiang
来源:juejin.cn/post/7298918307746267174
收起阅读 »

关于我很不情愿地帮一个破电脑优化了首屏时间75%这件事

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 最近我们公司有一个页面,这个页面其实结构很简单,就是三个列表同时出现在某个项目的首页里,大概页面是这样的,差不多每一个列表都有100-1000条数据吧~数...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


最近我们公司有一个页面,这个页面其实结构很简单,就是三个列表同时出现在某个项目的首页里,大概页面是这样的,差不多每一个列表都有100-1000条数据吧~数据量也不算多,三个列表顶天就3000条数据,并且数据也不复杂,所以也没做什么处理



开发阶段没发现什么问题,但是上了生产后,问题来了,很多“用户”反馈说这个首屏加载超级慢,这个项目是公司内部使用的,“用户”都是公司内部某个部门的员工们,由于他们的公司电脑配置比较差,所以这点数据量就足够他们电脑吃不消了,所以导致了首屏加载会非常慢~~有多慢呢?最慢的居然达到 8s。。。




  • Scripting: JavaScript逻辑处理时间

  • Rendering: UI渲染时间


有人会问,不应该是数据量多导致渲染慢吗?那为啥主要耗时都在 Scripting 呢?


那是因为 Vue3 在渲染前会进行一系列的 JavaScript 逻辑处理,包括:



  • 1、数据创建

  • 2、模板解析

  • 3、虚拟DOM创建

  • 4、虚拟DOM转真实DOM


不过最耗时的肯定就是后两步了,所以需要针对这个问题,做一个解决方案


页面代码先贴一下



懒加载分页?虚拟滚动?不行!


很多人第一想象到的解决方案就是做分页,或者做懒加载,其实分页和懒加载都是一个道理,就是按需去加载数据,但是不好意思,后端接口是老接口并且没有做分页,团队不想要耗费后端精力去重构这个接口,所以这条路就别想啦!!!


又有人说虚拟滚动,我这么说吧,虚拟滚动用在性能好的电脑上效果是很不错的,如果用在性能差的电脑上,那么效果会非常糟糕,毫无疑问虚拟滚动确实会减少首屏加载的时间,但是性能差的电脑滚动快了,会有白屏现象,而且很严重,熟悉虚拟滚动的人都知道,虚拟滚动是根据滚动时间去重新计算渲染区域的,这个计算时需要时间的,但是用户滚动是很快的,性能差的电脑根本计算不过来所以导致会有短暂白屏现象。。


又有人说虚拟滚动不是可以做预加载吗?可以解决短暂白屏现象。还是那句话,在性能好的电脑上确实可以,但是性能差的电脑上,你预渲染再多也没用,该白屏还是得白屏



分片渲染


不能做分页,不能做懒加载,不能做虚拟滚动,那么咋办呢?我还是选择了分片渲染来进行渲染,也就是在浏览器渲染的每一帧中去不断渲染列表数据,一直到渲染出整个列表数据为止。


这样做就能保证首屏时不会一股脑把整个列表都渲染出来了,而是先进首页后,再慢慢把所有列表都渲染完成



实施


要怎么才能保证在每一个帧中去控制列表渲染呢?可以使用requestAnimationFrame,我们先封装一个useDefer



  • frame: 记录当前的帧数

  • checkIsRender: 拿列表每一项的索引去跟当前帧数比较,到达了指定索引帧数才渲染这一项



页面里直接使用这个 hooks 即可




这样就能保证了达到一定帧数时,才去渲染一定的列表数据,我们来看看效果,可以看到首屏快了很多,从8s -> 2s,因为首屏并不会一股脑加载所有数据,而是逐步加载,这一点看滚动条的变化就知道了~



滚动条一直在变,因为数据在不断逐步渲染



已经尽力了,实在不行这边劝你还是换电脑吧~



优化点


我们在完成一个功能后,可以看出这个功能有什么



  • 列表渲染完毕后,可以停止当前帧的计算

  • 现在是一帧渲染一条数据,能否控制一帧渲染的多条数据?




作者:Sunshine_Lin
来源:juejin.cn/post/7298646156426526754
收起阅读 »

echart 桑基图的点击高亮

web
先上效果图 引入echarts-for-react import ReactEcharts from 'echarts-for-react'; 增加点击事件, 这里需要注意的是当用的是setState时,在onChartReady里获取的state的值一直...
继续阅读 »

先上效果图


image.png
引入echarts-for-react


import ReactEcharts from 'echarts-for-react';

增加点击事件, 这里需要注意的是当用的是setState时,在onChartReady里获取的state的值一直是空值,所以增加useRef来临时存放curHighLight的值;


const [curHighLight, setCurHighLight] = useState(null);
const curHighLightRef = useRef(null);

<ReactEcharts
notMerge={true}
option={chartOption}
onChartReady={(EChartsInstance) =>
{
ChartsInstance.current = EChartsInstance;
// 双击高亮
ChartsInstance.current.on('click', (params) => {
console.log('点击高亮', params);

if (isHighlighted(params, curHighLightRef.current)) {
setCurHighLight(null);
curHighLightRef.current = null;
} else {
const cur = {
dataType: params?.dataType,
name: params?.data?.name,
source: params?.data?.source,
target: params?.data?.target
}
setCurHighLight(cur);
curHighLightRef.current = cur;
}

return false;
});
}}
/>


判断是否已经被点击过


const isHighlighted = (params, curHighLight) => {
if (params.dataType === 'node') {
return params?.data?.name === curHighLight?.name;
}

if (params.dataType === 'edge') {
return params?.data?.source === curHighLight?.source && params?.data?.target === curHighLight?.target;
}

return false;
}

点击事件增加后,把当前点击节点或连接线存起来后,再通过useEffect更新option



  1. 调整lineStyle和itemStyle里 opacity 值


const temp = cloneDeep(chartOption);
temp.series[0].lineStyle.opacity = curHighLight === null ? lineOpacity / 100 : 0.1;
temp.series[0].itemStyle.opacity = curHighLight === null ? 1 : 0.1;
temp.series[0].emphasis.disabled = curHighLight !== null;


  1. 调整高亮节点的


// 获取高亮详情
const getHighLightInfo = ({ curHighLight, links, nodes }) => {
// 当取消高亮时,文字颜色恢复正常
if (curHighLight === null) {
const isHighLight = false;
links?.forEach(item => {
item.isHighLight = isHighLight;
item.lineStyle.opacity = null;
});

nodes.forEach(item => {
item.isHighLight = isHighLight;
item.itemStyle.opacity = null;
item.label = {
color: null
}
});
}

// 节点
if (curHighLight?.dataType === 'node') {
const selectList = [];
links.forEach(item => {
const isHighLight = item.source === curHighLight.name || item.target === curHighLight.name;
item.isHighLight = isHighLight;
item.lineStyle.opacity = isHighLight ? opacityHL_link : 0.1;

if (isHighLight) {
selectList.push(item);
}
});

nodes.forEach(item => {
const isIn = selectList.find(v => v.source === item.name || v.target === item.name);
const isHighLight = !!isIn;

item.isHighLight = isHighLight;
item.itemStyle.opacity = isHighLight ? opacityHL_node : 0.1;
item.label = {
color: !isHighLight ? 'rgba(0, 0, 0, 0.35)' : null
}
});
}

// 连线
if (curHighLight?.dataType === 'edge') {
links?.forEach(item => {
const isHighLight = item.source === curHighLight?.source && item.target === curHighLight?.target;
item.isHighLight = isHighLight;
item.lineStyle.opacity = isHighLight ? opacityHL_link : 0.1;
});

nodes.forEach(item => {
const isHighLight = item.name === curHighLight.source || item.name === curHighLight.target;
item.isHighLight = isHighLight;
item.itemStyle.opacity = isHighLight ? opacityHL_node : 0.1;
item.label = {
color: !isHighLight ? 'rgba(0, 0, 0, 0.35)' : null
}
});
}
}

作者:一切随意
来源:juejin.cn/post/7293788137662677026
收起阅读 »

算法(TS):只出现一次的数字

web
给你一个非空整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。 上题要求时间复杂度为 O(n),空间复杂度为O(1) 解法一...
继续阅读 »

给你一个非空整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。


你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。



上题要求时间复杂度为 O(n),空间复杂度为O(1)



解法一:维护一个 Set 对象


创建一个空的 Set 对象,遍历数组 nums,每遍历出一个 num,便在 Set 对象中查找是否存在它,不存在则加入,存在则删除,等数组遍历结束,Set 对象中剩下的就是只出现一次的数字。


function singleNumber(nums: number[]): number {
const uniqueSet = new Set<number>()
for (const num of nums) {
if (uniqueSet.has(num)) {
uniqueSet.delete(num)
} else {
uniqueSet.add(num)
}
}

return [...uniqueSet][0]
}

存在一次遍历数组,因此时间复杂度为 O(n),uniqueSet.size的最大值为 n/2,最小为 1, 空间复杂度为O(n)


解法二:位运算符(异或)


异或运算符有下面 3 个特性



  1. a ^ 0 = a,即,任何数与数字 0 异或,得到的结果都等于它本身

  2. a ^ a = 0,即,任何数与自身异或,得到的结果都等于 0

  3. a ^ b ^ c = a ^ c ^ b,异或运算符,满足交换率


遍历 nums,让数组中的数字两两异或,最终得到的结果便是数组中只出现一次的数字


function singleNumber(nums: number[]): number {
let uniqueNumber = 0

for (const num of nums) {
uniqueNumber ^= num
}

return uniqueNumber
}

存在一次遍历数组,因此时间复杂度为 O(n),没有额外的中间量空间复杂度为O(1)


作者:何遇er
来源:juejin.cn/post/7298674250965155877
收起阅读 »

利用腾讯地图实现地图选点功能

web
基于腾讯地图组件实现地图选点功能 使用到了腾讯地图官提供的组件,实现了地图选点 <template> <iframe id="mapPage" width="100%" height="100%" frameborder="0" src...
继续阅读 »

基于腾讯地图组件实现地图选点功能


使用到了腾讯地图官提供的组件,实现了地图选点


image.png


<template>
<iframe id="mapPage" width="100%" height="100%" frameborder="0" src="https://apis.map.qq.com/tools/locpicker?search=1&type=1&key=你自己申请的KEY&referer=myapp"></iframe>
</template>

<script setup>
import { ref } from 'vue'

const key = '自己申请到的Key'

window.addEventListener(
'message',
function (event) {
// 接收位置信息,用户选择确认位置点后选点组件会触发该事件,回传用户的位置信息
var loc = event.data
if (loc && loc.module == 'locationPicker') {
//防止其他应用也会向该页面post信息,需判断module是否为'locationPicker'
// loc 这里面存放详细的位置信息
emit('addressInfo', loc)
}
},
true
)
</script>

<style lang="scss">
// 样式自己去修改,可以使用到样式渗透
:deep(.map-wrap) {
height: 60%;
}
</style>

我是将这个代码封装成了组件,在使用的地方直接调用就可以.


其中:


window.addEventListener(
'message',
function (event) {
// 接收位置信息,用户选择确认位置点后选点组件会触发该事件,回传用户的位置信息
var loc = event.data
if (loc && loc.module == 'locationPicker') {
//防止其他应用也会向该页面post信息,需判断module是否为'locationPicker'
// loc 这里面存放详细的位置信息
emit('addressInfo', loc)
}
},
true
)

当这段代码被执行时,它会添加一个事件监听器,用于监听浏览器窗口的message事件。
第一个参数是要监听的事件类型,这里是message,表示监听来自窗口的消息事件。


第二个参数是一个回调函数,当message事件被触发时,回调函数会被执行。


在回调函数中,它首先通过event.data获取传递过来的数据,并将其保存在一个变量loc中。


接下来,通过判断loc对象中的module属性是否为locationPicker来确定这个消息是否来自选点组件。这样做的目的是为了避免处理来自其他应用程序的消息。


如果条件满足,即该消息确实来自选点组件,则会触发一个自定义的事件addressInfo,并将loc对象作为参数传递给该事件。这可以通过一个emit函数来实现,该函数的作用是触发指定名称的事件,并传递相关的数据。这样其他部分的代码就可以订阅并处理addressInfo事件,从而获取位置信息并执行相应的逻辑。


当你在地图选点后点击下面的信息就能看到对应的数据了。


image.png


基于腾讯地图实现地图选点功能(手写)


这是最终实现的效果:


image.png
有时候的腾讯地图组件的选点功能会稳定,或者失效,显示列表更新失败这就导致可能用户使用感受较差,有时候就必须手写一份,下面的代码是手写的代码以及对应的代码说明:👇👇👇👇


1.首先你需要在项目的html文件引入腾讯地图(vue3的项目)


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script charset="utf-8" src="https://map.qq.com/api/js?v=2.exp&key=这里是你申请的key"></script>
</body>
</html>


  1. 将地图选点功能封装成组件,实现代码的高复用。


<template>
<div>
<div class="container" id="container">
<img class="coordinate" src="@/assets/坐标.png" alt="图片加载失败" />
</div>
</div>
</template>

<script setup>
import { onMounted, reactive } from 'vue'
import axios from 'axios'

//地图
const dataMap = reactive({
map: '',
markerLayer: '',
latitude: '', //纬度
lngitude: '', //经度
})

onMounted(() => {
getlocation() //获取经纬度
initMap() // 初始化地图
})

//初始化地图
const initMap = () => {
dataMap.map = new qq.maps.Map(document.getElementById('container'), {
center: new qq.maps.LatLng(45.190524, 124.797766), //设置地图中心点坐标
zoom: 20, //设置地图缩放级别
})
qq.maps.event.addListener(dataMap.map, 'center_changed', center_changed)
}
// 监听地图拖动获取经纬度
const center_changed = () => {
dataMap.latitude = dataMap.map.getCenter().lat
dataMap.lngitude = dataMap.map.getCenter().lng
console.log('选点后的经纬度:', dataMap.latitude, dataMap.lngitude)
}

//获取经纬度
const getlocation = async () => {
const res = await axios.get('/api', {
params: {
key: '自己申请的key',
},
})
dataMap.map.setCenter(new qq.maps.LatLng(res.data.result.location.lat, res.data.result.location.lng))
}
</script>

<style lang="scss" scoped>
.container {
box-sizing: border-box;
margin: 50px;
width: 800px;
height: 400px;
border: 1px solid #999;

.coordinate {
z-index: 9999;
position: relative;
top: 50%;
left: 50%;
}
}
</style>


  1. 其中中心点的图片是自己设置上的,下面给提供了这个图片的地址,大家可以下载使用


坐标.png
图片地址:img1.imgtp.com/2023/11/08/…


中心点的位置是根据定位设置的,如果大家使用的容器的宽度和高度和我的不一样,需要手动的设置。


作者:LuHang
来源:juejin.cn/post/7298361908443463734
收起阅读 »

近三个月的排错,原来的憧憬消失喽

web
作为应届生,带着无限憧憬来到这里,但是经过这三个月的经历,发现只有无限的消耗,并没有任何想要留下的感觉,每天携着自己早已不属于自己的脑袋来到早已不想来的地方... 动效逻辑实现 将元素布局设置好,调整元素的一个动态css属性让其置于可视视图以外,使用动效类库,...
继续阅读 »

作为应届生,带着无限憧憬来到这里,但是经过这三个月的经历,发现只有无限的消耗,并没有任何想要留下的感觉,每天携着自己早已不属于自己的脑袋来到早已不想来的地方...


动效逻辑实现


将元素布局设置好,调整元素的一个动态css属性让其置于可视视图以外,使用动效类库,去改变他的css属性让其还原回正确的位置。


动效类库




  • ScrollTrigger




  • ScrollMagic



    • ScrollMagic 是一个 JavaScript 库,用于在滚动事件上创建视差滚动和其他滚动效果。

    • ScrollMagic 允许您在页面滚动时触发动画,例如根据滚动位置触发动画、控制元素的可见性、触发事件等。

    • 您可以使用 ScrollMagic 来创建交互式滚动体验,例如滚动到特定部分时触发动画效果。

    • ScrollMagic 可以与 TweenMax 或其他动画库一起使用,以创建复杂的滚动动画。




  • TweenMax



    • TweenMax 是 GreenSock Animation Platform (GSAP) 库的一部分。GSAP 是一个用于创建高性能动画的 JavaScript 库,提供了丰富的动画功能。

    • TweenMax 是 GSAP 的核心库之一,它用于创建各种动画,包括基本的属性动画,缓动动画,时间轴动画等。

    • TweenMax 提供了灵活且易于使用的 API,允许您创建复杂的动画效果,如淡入淡出、缩放、旋转、移动等。

    • 您可以单独使用 TweenMax 来创建动画,或与其他库和插件一起使用,以实现更高级的效果。




  • animation.gsap.js



    • animation.gsap.js 是 ScrollMagic 的插件,它允许您在 ScrollMagic 中使用 GSAP(包括 TweenMax)来控制动画。

    • 此插件通过将 GSAP 和 ScrollMagic 集成,使您能够在滚动事件中触发和控制 TweenMax 动画。

    • 使用 animation.gsap.js,您可以创建更具交互性的滚动动画,将滚动事件与强大的 TweenMax 动画引擎结合使用,实现更丰富的效果。




综上所述,TweenMax 是 GSAP 库的一部分,用于创建各种动画。ScrollMagic 是一个独立的库,用于处理滚动事件和创建滚动效果。animation.gsap.js 是 ScrollMagic 的插件,它使 ScrollMagic 能够与 GSAP(包括 TweenMax)一起使用,以在滚动事件中创建动画效果。这些库和插件可以协同工作,以创建引人入胜的交互式网页效果。


ScrollMagic很久没有维护了。
image.png


浏览器跨页面通信


前几天有这样一个需求,当我们在当前页面点击编辑时,我们跳转到编辑页面,编辑完成后,我们需要刷新当前页面并关闭编辑页面。这就需要用到跨页面通信功能了。


image.png


image.png


下面总结一下前端中实现在一个页面上进行操作,然后刷新其他页面功能的实现方法:



前提条件是两个页面同源


在页面 A:


 // 判断是否是对比项目页面跳转过来的
if (route.query?.type === 'diff') {
localStorage.setItem('diffProjectChanged', 'true');
setTimeout(() => {
window.close();
}, 500);
} else {
router.back();
}

在页面 B:


// 进入页面将localStorage中的 diffProjectChanged 置为false
localStorage.setItem('diffProjectChanged', 'false');
// 监听编辑
onMounted(() => {
window.addEventListener('storage', (event) => {
if (event.key === 'diffProjectChanged' && event.newValue === 'true') {
location.reload();
}
});
});


在页面 A 中触发一个自定义事件,将相关数据传递给其他页面。


// 触发自定义事件
const event = new CustomEvent('dataChanged', { detail: { newData: 'someData' } });
window.dispatchEvent(event);

在页面 B 中监听该自定义事件,并在事件触发时执行刷新操作。


// 监听自定义事件
window.addEventListener('dataChanged', (event) => {
// 获取数据并执行刷新操作
const newData = event.detail.newData;
location.reload();
});


  • 使用 WebSocket


在页面 A 中通过 WebSocket 发送消息,通知其他页面。
在页面 B 中监听 WebSocket 消息,接收通知并执行刷新操作。


这种方法需要在服务器上设置 WebSocket 服务。


当前项目避免使用其他包管理工具


使用一些约束,让当前项目只能通过指定的包管理工具安装,防止项目配置乱七八糟的。


在当前根目录下


    // scripts/preinstall.js
if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.log('只能使用pnpm');
console.warn(
`\u001b[33mThis repository requires using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`,
);
process.exit(1);
}

在packages.json中scripts配置。


"preinstall": "node ./scripts/preinstall.js"

或者直接配置


"preinstall": "npx only-allow pnpm"

文本省略


这种方式需要设置具体宽高。如果是使用了 flex: 1 / 百分比数据 这种不会生效。动态的宽度是不会出现省略号的。


text-overflow: ellipsis;
width: 100%;
overflow: hidden;
white-space: nowrap;

所以我们可以使用多文本的方式代替。


word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

不同大小文字底部对齐方式


在CSS中,要让不同字体大小的内容底部对齐,你可以使用verti**cal-align属性。(设置在对齐元素上,只需要设置在一个元素上即可) 这属性用于控制内联元素(如文本)在其父元素内的垂直对齐方式。你可以将其设置为bottom来实现底部对齐。但需要注意的是,vertical-align属性通常用于内联元素,而不是块级元素。 如果是块级元素,我们可以使用flex布局的align-items: flex-end;来实现父容器内的文本底部对齐。要想实现底部对齐,父容器必须具有足够的高度来容纳最大的字体大小。


<style>
.container {
height: 100px;
border: 1px solid #ccc;
display: flex;
align-items: flex-end; /* 底部对齐的关键 */
}

.text {
font-size: 20px;
}

.small-text {
font-size: 14px;
}
</style>

<div class="container">
<p class="text">这是一些文本</p>
<p class="small-text">这是较小的文本</p>
</div>

了解的一些git操作


git提交错误分支,希望将中间的commit删除掉


先通过git rebase -i commitid切换到删除commit的前一个commitid。-i 表示要进行交互式 rebase,<commit-hash>^ 表示要删除指定提交及之后的提交。这将打开一个文本编辑器,列出了要进行 rebase 的提交。
image.png


image.png
这样他只是删除了本地的记录,但是并没有更新远程仓库。


image.png
所以我们需要强制当前记录提交


git push origin <branch> --force

image.png


如果直接执行git push他会告诉你需要拉取最新代码。如果执行了git pull前面做的工作就没用了。所以我们需要使用--force强制提交。


注意:最后git push一定要强制提交,不然按照他的提示拉取了远程代码,那么前面做的内容都没用了。


回退解决冲突之前的状态


git merge --about

查看当前分支基于那个分支创建的


git reflog show

修改分支名称


// 在当前分支
git branch -m feature/v1.4.1.0

// 不在当前分支
git branch -m old-branch new-branch

将已提交的记录提交到别的分支


ruanyifeng.com/blog/2020/0…


// 切换到需要提交的分支
// 找到需要提交的代码commit
git cherry-pick commitId

props的双向绑定便捷性


我们在使用表格编辑功能时,直接在dataSource中绑定props对应的值,当编辑单元格时,就直接更新props,很方便。由于一些其他的因素,这个模块并没有采用这个,导致以下bug出现。


这个问题是测试发现方改变一些字段时,字段为发送给后端,排查发现我在修改时,并没有通过emits将值更新到props中。所以造成bug。,导致最近需求一直变更代码bug很多,已经没有在去维护的力气了。😑
image.png


watch监听可能出现的问题


这个bug对于当时我写代码来说排查很困难。排查了很久,最后也是找我导师解决的,不得不说我导师排查bug的能力好nb。👍


最开始我是通过监听用户切换不同内容,监听diffProjectId,然后拿到formFirstValues和formSecondValues,去完成逻辑。这样看似没啥问题。但是完成逻辑的时候,拿到的formSecondValues总是上一次的值。这就很懵逼了。


出现这种情况的原因是我们监听的diffProjectId是同步的,而formFirstValues,formSecondValues这两个值是异步获取的。所以就会出现问题。最后通过下图方式实现功能。
image.png


删除后端不必要字段,造成的问题


由于后端定义的查询详情和请求传参字段不统一collectionPlanResp,collectionPlanReq,导致我回写数据不好处理,直接通过collectionPlanResp对象进行处理,在提交时在赋值。当时想着把多余字段删除collectionPlanResp,这样就会出现一个问题,我提交表单后,当后端服务抛出异常提示(比如字数限制),我们修改后,在当前页面再次提交,就会导致collectionPlanReq传递为空值。造成数据未传递给后端的bug。


分析了一下,字段的一些必填项长度限制,前端还是不要偷懒,做一下处理。


image.png


image.png


对于多字段UI处理


我们可以使用Collapse组件去处理,让UI看起来更简洁。


image.png


善于使用margin和定位来解决间距问题


调整间距时,如果margin不好调整,我们可以使用相对定位来配合调整。 这种方式是当时接了一个对比字段差异的需求,为了以后可以直接在当前对比页面编辑,所以采用了两个form去实现的。设置一个form的label,另一个不设置。这样就可以完美的在一行突出标题对比两个不同内容的字段了。为了做到响应式,就有了这样的做法。


image.png


这样看下来工作三个月基本都是在马代码,每天写不完的需求,发不完的版,上午写代码,下午改需求,真的挺无语的,下个月辞职回家种地喽。😅


往期文章



专栏文章



最近也在学习nestjs,有一起小伙伴的@我哦。


作者:Spirited_Away
来源:juejin.cn/post/7292036126269063178
收起阅读 »

这款支持安全多人协作的在线终端,真的吓到我了❗️❗️

web
这款支持安全多人协作的在线终端 ☀️ 前言 事情是这样的: 周末一个同事的项目报错了,但是无法精准定位到问题😠。 他希望我帮忙看一下他的报错并协助解决,于是扔了一个链接给我🤔。 我心想你给我链接干啥,你倒是截图报错啊😡。 打开链接后我直呼 wassu...
继续阅读 »

这款支持安全多人协作的在线终端



☀️ 前言



  • 事情是这样的:

    • 周末一个同事的项目报错了,但是无法精准定位到问题😠。

    • 他希望我帮忙看一下他的报错并协助解决,于是扔了一个链接给我🤔。

    • 我心想你给我链接干啥,你倒是截图报错啊😡。



  • 打开链接后我直呼 wassup🔥,我居然可以在一个网页中操作他的终端,并且还是实时协同的!有鼠标动来动去那种!

  • 询问得知原来是用的 sshx ,那么我们本文就来了解一下这个神奇的产品。


🔥 sshx



  • sshx 这是一款基于网络的安全的团队协作终端,它允许您在多人无限画布上通过链接与任何人共享您的终端。

  • 只需要共享的人员执行一下“sshx”再将链接分享给你同事,则它能马上加入到你的终端进行操作。

  • 它具有实时协作远程光标聊天功能。它还具有快速且端到端加密的特点,并具有用 Rust 编写的轻量级服务器。

  • 实时协同代表着什么,这将使远程团队调试终端问题变得更加容易。


🤔 怎么使用


安装命令行界面



  • 通过在终端中运行此命令curl -sSf https://sshx.io/get | sh来获取 sshx CLI。它很小,只需几秒钟即可下载(3 MB)


分享您的终端



  • 直接在你需要分享的终端内执行 sshx,此时这个终端不要关闭,他会生成一个分享链接。




  • 将这个终端用浏览器打开即可,进入到这个网址,会让你输入一个名称方便团队协时展现光标的用户。





  • 在上方的操作栏新建一个虚拟终端即可操作真正的终端了。




  • 为了方便演示我这里打开两个浏览器来模拟别人协同操作我的终端,来我们跑个苹果来看看。




  • 实时对话也是很流畅。




  • 我们可以看到,在页面会出现另一个用户的移动光标,并且可以与他对话,他的延迟是非常低的,这真的可以帮助我们实时协作。




  • (协不协作我不知道,但是可以看到光标是真的帅啊!)




❓ 用来干啥



  • 那么这么一款产品,有的同学就会问了:他的作用是什么呢?看起来很鸡肋啊?

  • 有了这么一款产品,我们可以:

    • 在帮助客户部署相关公司产品的时候不需要远程操控别人的电脑,只需要客户安装这款 cli 并且联网,我们既可以远程帮忙操作。

    • 更好的公司运维,在同事操作的时候,可以随时介入进行操作。

    • 很多群友在前端群中问问题时习惯抛出一个截图,但是又没有说明白上下文,这时候就可以将你终端分享给大佬们定位问题。

    • (手摸手教女同学命令行操作🐶)



  • 那么肯定又会有同学问了:那我不是可以随便删除别人的文件?我直接rm -f * 敢问阁下如何应对?

  • 是的,看了下确实可以执行这些操作,所以还是尽量分享给你信得过的人,我觉得其实作者可以出一个只读模式only-read,这样你就可以让别人在你的终端上阅读和滚动,减少一些权限。


👋 写在最后



作者:快跑啊小卢_
来源:juejin.cn/post/7298642242117238834
收起阅读 »

Node.js如何处理多个请求?

web
前言 在计算机科学领域,关于并发和并行的概念经常被提及。然而,这两个术语常常被混为一谈,导致很多人对它们的理解存在着很多混淆。本文小编将通过对并发和并行的深入解析,帮助读者更好地理解它们之间的不同特点和应用场景。同时,文章还将介绍Node.js如何高效地处理多...
继续阅读 »

前言


在计算机科学领域,关于并发和并行的概念经常被提及。然而,这两个术语常常被混为一谈,导致很多人对它们的理解存在着很多混淆。本文小编将通过对并发和并行的深入解析,帮助读者更好地理解它们之间的不同特点和应用场景。同时,文章还将介绍Node.js如何高效地处理多个请求的技巧和方法。


什么是并发


并发是指两个或多个任务可以在重叠的时间段内开始、运行和完成。这并不一定意味着它们将同时运行,但它们可以交错执行,以便在任何给定的时间,总有一个任务在运行。


下面小编以一个简单的例子给读者详细的解释并发的特点:


假设在一个餐厅里面,有一个服务员从1号桌的客人那里接受了一份点单,然后这个服务员在厨房一直等待1号桌客人的饭做好,做好之后将饭端到1号桌。


这个服务员完成第一桌客人的点单后,再前往下一桌的2号客人处,接受订单,并前往厨房等待准备完成,等饭做好后再将点餐的餐点交给客人。


看到这里,各位读者可能会觉得这个服务员的做法一点都不高效,他完全可以在等第一单饭的时候去第二桌点单,按照这位服务员现在的做法,他在每一单的饭做好之前的这个时间段内什么事情都干不了,这样就浪费了大量的时间。


我们现在修改一下这位服务员的做法,修改后如下:


服务员将前往1号桌接受订单并将其交给厨房,然后返回2号桌接受订单并将其同样交给厨房。在这种情况下,服务员不会等待订单准备完成,而是会继续前往下一个桌子接受订单,直到食物准备好。当食物准备好后,服务员会为所有桌子上的客人上菜。像上述的这种情况,没有增加线程(服务员)的数量,但通过缩短空闲时间来加快处理过程。同时处理多个任务,这个就是并发。


例如:你正在做饭的同时,接到一通电话,于是你接听了电话,当听到炉子发出警报时,你回去关掉炉子,然后再继续接电话。


这个例子很好地展示了并发的概念。做饭的过程中,能够同时处理来自电话和炉子的不同事件。你在不中断一个任务的情况下,暂时切换到另一个任务,然后再回到原来的任务。这种并发的方式能够提高效率并更好地应对多个任务的情况。(同时做两件事,但是一次只做一件事)


什么是并行


并行是指两个或多个任务可以真正同时运行。为了实现这一点,这些任务必须能够在独立的CPU或核心上运行。同样的,小编依然以做饭的例子给大家解释一下什么是并行:


例如:你正在做饭的同时,接到一通电话,你的家人接听了电话,你继续做饭,你和你的家人谁也不会干扰谁,两个不同的事情发生在两个人身上,这个就是并行。


什么是单线程进程?


单线程进程是按照单一顺序执行编程指令的过程。话虽如此,如果一个应用程序具有以下一组指令:


指令A


指令B


指令C


如果这组指令在单线程进程中执行,执行过程将如下所示:


多线程进程是什么?


多线程进程是在多个序列中执行编程指令。因此,除非多个指令被分组在不同的序列中,否则指令不需要等待执行。


为什么Node.js是单线程的?


Node.js是一个单线程的平台。这意味着它一次只能处理一个请求。


例如:服务员从1号桌子上接订单并将其传给厨房,然后去2号桌子接订单。当从2号桌子接订单时,1号桌子的食物已经准备好了,但是服务员不能立即过去将食物送到1号桌子,服务员必须先完成1号桌子的订单,然后将其交给厨房,然后再将准备好的餐点送到1号桌子。


Node.js Web服务器维护一个有限的线程池,为客户端请求提供服务。多个客户端向Node.js服务器发出多个请求。Node.js接收这些请求并将它们放入事件队列中。Node.js服务器有一个内部组件,称为事件循环(Event Loop),它是一个无限循环,接收并处理请求。这个事件循环是单线程的,也就是说,事件循环是事件队列的监听器。


Node.js如何处理多个请求?


Node.js可以通过事件驱动模型轻松处理多个并发请求。


当客户端发送请求时,单个线程会将该请求发送给其他人。当前线程不会忙于处理该请求。服务器有工作人员为其工作。服务器将请求发送给工作人员,工作人员进一步将其发送给其他服务器并等待响应。同时,如果有另一个请求,线程将其发送给另一个工作人员,并等待来自另一个服务器的响应。


这样,单个线程将始终可用于接收客户端的请求。它不会阻塞请求。


Node.js实现多个请求的代码:


const http = require('http');

// 创建一个 HTTP 服务器对象
const server = http.createServer((req, res) => {
// 处理请求
if (req.url === '/') {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('Hello, World!');
} else if (req.url === '/about') {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('About Us');
} else {
// 设置响应头
res.writeHead(404, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('Page Not Found');
}
});

// 监听 3000 端口
server.listen(3000, () => {
console.log('Server listening on port 3000');
});

总结


总的来说,Node.js在处理多个请求方面具有优势。它利用事件驱动和非阻塞式I/O的特性,能够高效地处理并发请求,提供快速响应和良好的可扩展性。同时,通过采用适当的工具和技术,可以进一步优化性能,控制并发量,并提高系统的可靠性和稳定性。


扩展链接:


从表单驱动到模型驱动,解读低代码开发平台的发展趋势


低代码开发平台是什么?


基于分支的版本管理,帮助低代码从项目交付走向定制化产品开发


Redis从入门到实践


一节课带你搞懂数据库事务!


Chrome开发者工具使用教程


作者:葡萄城技术团队
来源:juejin.cn/post/7298646156437438464
收起阅读 »

支持远程调试的 "vConsole"

web
背景 前阵子一直在做业务需求,是嵌在公司 APP 里的 H5。而且是跨地区协作,我在 A 城市,测试和产品都在 B 城市。 由于是 H5 项目,开发的时候一般都会实例化个 vConsole,方便查看项目的上下文信息。同时我想着当程序出现问题之后,测试小姐姐可以...
继续阅读 »

背景


前阵子一直在做业务需求,是嵌在公司 APP 里的 H5。而且是跨地区协作,我在 A 城市,测试和产品都在 B 城市。


由于是 H5 项目,开发的时候一般都会实例化个 vConsole,方便查看项目的上下文信息。同时我想着当程序出现问题之后,测试小姐姐可以直接截个 vConsole 的图给我,可以减少沟通时间。


痛点


后来发现一切都是想象之中,我们两个在沟通问题上依旧没少花时间!如果把程序出现的问题分级,那么会有:



  • 😄 简单问题:测试小姐姐 描述问题 发生的过程后,基本可以定位、解决;

  • 😅 中等问题:测试流程走不下去或者程序报错,这时候得查看调试信息去分析。此时需要测试小姐姐 截图 vConsole 上面显示的内容发我,但由于截的图并不一定是关键信息或者信息数据不够,导致这中间会产生几轮沟通;

  • 😥 复杂问题:遇到一些依赖外部信息或者奇奇怪怪的问题的时候,可能会 远程视频 操作测试机给我看,同时我会告诉她什么时候打开 vConsole 查看什么面板的信息。


可以看到只要问题牵扯到了项目的运行信息,前前后后就会导致很多沟通上的时间成本


不禁让人思考是什么原因导致的这个问题……


问题的本质


结合前面的描述我们得知,由于物理空间、跨地域的限制,程序的错误信息都是由测试人员转达给技术人员,不得不说这对测试人员有点勉为其难了,而另一方面造成问题的关键就在于此:技术人员无法和 Bug 直接来个正面交锋!


那么该如何解决这个「中间人」的问题呢?


这个问题的答案其实很简单,我们只要将浏览器的原生 API 进行一层包装将运行时调用的参数收集起来,然后再整一套类似控制台的 UI,最后整合成 SDK 处理参数 -> 中间层网络通信 -> UI 控制台展示的样子,开发同学直接和控制台上的 BUG 切磋,就能完美的解决这个问题!


虽然说起来简单,但是这一整套下来开发的工作量可不容小觑:



  • 包装原生 API 的 SDK

  • 负责通信的服务

  • 控制台 UI……


不用慌!开箱即用的 PageSpy 值得你拥有 😄!


PageSpy


Page Spy 是由货拉拉大前端开源的一款用于调试 H5 、或者远程 Web 项目的工具。基于对原生 API 的封装,它将调用原生方法时的参数进行过滤、转化,整理成格式规范的消息供调试端消费;调试端收到消息数据,提供类控制台可交互式的功能界面将数据呈现出来。


PageSpy是一个强大的开源前端远程调试平台,它可以显著提高我们在面对前端问题时的效率。以下是PageSpy的一些主要特点:



  • 一眼查看客户端信息 PageSpy 会对客户端的运行环境进行识别,其中系统识别支持 Mac / iOS / Window / Linux / Android,浏览器识别支持谷歌、火狐、Safari、Edge、微信、UC、百度、QQ;

  • 实时查看输出: PageSpy 可以实时捕获并显示程序输出,包括 Console、Network、Storage 和 Element。这使开发人员能够直观地了解页面的外观和行为,无需依赖用户的描述或截图。

  • 网络请求监控: PageSpy 还可以捕获和显示页面的网络请求,有助于开发人员更好的查看与后端的交互。

  • 远程控制台: PageSpy 支持远程调试JavaScript代码,允许开发人员执行 JavaScript 代码在用户的浏览器上运行。这对于排查特定问题或测试代码修复非常有帮助。

  • 跨浏览器兼容性: SDK 可以在各种主流浏览器中运行,确保你可以检查和调试不同浏览器上的问题。

  • 用户体验提升: 通过快速识别和解决前端问题,PageSpy可以显著提升用户体验,减少用户因前端问题而受到的不便。


使用 PageSpy 进行远程调试


使用PageSpy进行远程调试是相对简单的。以下是一些基本步骤:



  • 部署PageSpy: 首先,PageSpy 提供了 Docker、Node 和 Release 的一键部署方案,点击查看

  • 实例化 SDK: PageSpy 成功部署后,你可以在项目中引入对应的 SDK 文件并进行实例化,它提供了多场景类型的参数,以便于用户对它的行为进行定制。

  • 实时监控页面: 之后,你可以实时查看页面的各种数据,这有助于你直观地理解页面的问题。

  • 监控网络请求: 你还可以查看所有的网络请求,包括请求的URL、响应代码和响应时间。这可以帮助你识别与后端通信相关的问题。

  • 解决问题: 借助PageSpy提供的信息和工具,你可以更快速地定位和解决前端问题,从而提高用户体验。


相关截图


门户首页


image.png


待调试列表


image.png


调试界面


image.png


image.png


结语


前端远程调试对于快速而准确地解决前端问题至关重要。Page Spy 作为一个强大的开源工具,支持开箱即用,为开发人员提供了一个高效的方式来查看用户端的页面输出、网络请求和执行远程调试。它有助于加速问题的定位和解决,减少了对用户反馈和日志的依赖,提高了整体开发效率。除了解决跨地区协同的场景之外,还覆盖了本地开发时的调试 H5 的场景。


希望本文能够帮到大家对 PageSpy 有个初步的认识,感谢阅读。


作者:Blucas
来源:juejin.cn/post/7298161887882592310
收起阅读 »

把jsp重构成vue

web
记录一次重构的经历与感想!望自己将来开发之路越走越顺利。 话说,我在入职之前,公司一直使用的jsp技术,并结合jQuery来处理前端页面逻辑。 但在我入职之后不久,我们领导就要求把它重构成vue。 这时,我对jsp根本不熟,业务也没摸清楚。且在这个关键时刻,另...
继续阅读 »

记录一次重构的经历与感想!望自己将来开发之路越走越顺利。


话说,我在入职之前,公司一直使用的jsp技术,并结合jQuery来处理前端页面逻辑。


但在我入职之后不久,我们领导就要求把它重构成vue。


这时,我对jsp根本不熟,业务也没摸清楚。且在这个关键时刻,另一个前端因为离家较远直接离职了!


这个担子竟然压到我一个人身上,心里一万匹草泥马奔过。。。


但也没办法,只能赶鸭子上架,怀着极其忐忑的心情进入了开发阶段,当然也有点兴奋,于我而言这也是一个难得的实践机会!


我把这次重构的经历大致分成四个阶段:(1)摸清楚jsp项目的代码(2)用Vue CLI把项目工程搭建起来(3)整理业务逻辑(4)写vue代码


1.摸清楚jsp项目的代码


老项目中的jsp文件是长这样子:


jsp页面.png


JSP(JavaServer Pages)技术是一种基于Java的Web应用程序开发技术,它允许开发人员将Java代码嵌入到HTML中,以动态生成Web页面。


虽然这是一个比较古老的技术,我也是一脸懵圈,但秉承着前端框架无非是对html、css、js的结合的原则,我硬着头皮读代码。


经过一段时间的浸泡,并且在分析后,我发现,只需要稍微懂一点jsp技术,其实就完全可以读懂jsp了,jsp页面最大的特点就是可以通过java注入参数,除此,它和所有前端框架一样是由三部分组成:



  • html部分,就把jsp文件看成html文件,虽然里面注入了一些参数,但这些参数可以让后端通过接口返回,再拿去渲染就行了

  • css部分,老项目和vue基本可以共用

  • js部分,新老项目的功能是一样的,老项目中用jquery实现的,再拿vue去实现一遍即可


基于这些,此时我对把jsp重构成vue已经有了一些把握,总体原则大概是:对html、css、js这三部分,可以重用的部分就重用,不能重用的部分就重写。


2.用Vue CLI把项目工程搭建起来


使用脚手架,很快就搭建好了项目,都是傻瓜式的操作,这个没什么好说的,给大家看一下目录结构:


vue目录结构.png


3.整理业务逻辑


我后知后觉才发现,重构最难的不是编码,而是业务逻辑


我对业务逻辑的信息来源有两个:一是看老项目的源代码,二是问其他老员工


但是前者效率极低,后者又困难重重


为何这么说,参考以下两点:



  1. 程序员最痛苦的莫过于阅读别人写的代码

  2. 作为新人,得遵守一些职场潜规则


但又没办法,只得蛮力通关了,忍受着巨大的痛苦,一方面得加班阅读代码,一方面要虚心求教老员工(我司的环境大概是,对业务越熟的人,脾气就越大,问题问多了,他们会很不耐烦,对此,我做了很多心理建设)。


后来,我也是整理出了一份前端业务逻辑资料,然后被放到了公司公共文件夹里,被后来的员工永久查看学习🐶!


前端业务逻辑整理.png


4.写vue代码


最后阶段就是编码了,我把它分为前期和后期。


前期攻坚难点,重点关照那些难实现的功能,后期画页面,要保持效率,基本要能够一天画2个页面。


后来翻看了一下代码库的提交记录,从第一行代码的提交,到进入测试,历时3个月。


编码阶段是枯燥乏味的,前面靠"蛮劲"可以挺过去,但是现在每天得靠"有恒"二字给自己打气🐪!


然后,终于把项目重构完成了,我也长吁了一口气!


但是事情并没有我想象的那么简单,更恶心的事情来了,bug颇多!


短短几天,测试就提了几百个问题单!


问题单数.png


泪奔啊!蛮劲用完了,恒心也消磨的差不多了!但是问题还是不依不饶的出现。。。


可能我这人就属于那种打不死的小强,想着好不容易坚持到这一步,无论如何我都要拿下它!


于是又向bug们发起了"猛攻"!


又渡过了一段漫长且艰难的解bug时期。。。


终于把bug也解完了,我和测试都长吁一口气!


什么?代码要想顺利上线,还要处理CI?


最后,我精疲力竭的处理完了一千多个CI问题,也终于体会到了,有时候,不逼自己一把,你永远不知道自己可以做到什么程度!


CI数.png


至此,项目终于上线了!


这次重构经历,我思考了这么几个问题:




  1. 公司为什么要重构这个项目?


    答:这个项目本来用户量大,将来还有大量的新需求要接,但是技术架构上已经落后了,如果不重构,将来搞不定新需求,老代码也不好维护,毕竟新来的员工会jsp的没多少。




  2. 重构的重点在哪?


    答:在于业务。业务是大于技术的,特别是新员工,别急着钻研项目中用到了哪些技术,还是多花点时间了解业务吧!




  3. 重构的难点在哪?


    答:技术上的困难总有办法,但是沟通上的困难却似不可逾越的鸿沟,因为工作的日常除了编码,更多的是:和产品互怼、与测试撕逼、向领导交差,所以,程序员们,提升情商吧!




我觉得,最重要的,是进行心态建设,遇到难关不要怕,永远相信自己可以挺过去,毕竟知识是死的,人是活的,只要我们"有恒",就算再难的东西,用"蛮劲"去"猛攻",终将拿下!


作者:玄玄子
来源:juejin.cn/post/7298167437526269952
收起阅读 »

破涕为笑,一个node中间层bug我让同事的名字出现在全球用户的页面上

web
前言 近期遇到了一个线上故障,排查花了很多时间精力,在bug复现过程中,我还不小心让同事的名字出现在了全球用户的页面上,从“一把辛酸泪”到“破涕为笑”,感觉特别有意思。本文中的代码、描述都过滤了敏感信息,以简单demo的形式复现该故障。 如下图,不管用户搜索啥...
继续阅读 »

前言


近期遇到了一个线上故障,排查花了很多时间精力,在bug复现过程中,我还不小心让同事的名字出现在了全球用户的页面上,从“一把辛酸泪”到“破涕为笑”,感觉特别有意思。本文中的代码、描述都过滤了敏感信息,以简单demo的形式复现该故障。


如下图,不管用户搜索啥词,每个用户页面回显的都是sivan
image.png


业务功能描述


站点的搜索功能,搜索功能会根据业务场景继续细分为a搜索,b搜索...,每种搜索在node中间层走的可能是不同的链路。


如下图展示,a搜索回显了default


image.png


b搜索回显了sang和for


image.png


image.png


故障描述



  1. 偶现,但是触发频率很高

  2. 现象为在x国家站点上,不管用户搜索什么内容,页面回显的大概率是一串固定的字符串

  3. 只有在x国家站点会出现该故障,其他国家站点没有出现

  4. 测试环境无法复现,只有线上环境会出现该故障


image.png


image.png


排查


炒面代码分析


从线上的故障现象来看,像是搜索词被替换掉了,分析看客户端页面下发的参数是没有任何问题的,找搜索服务的后端协助,后端说他们接收到的搜索词就已经是有问题的搜索词了。


初步猜测是被类似xssFilter之类的转换函数替换掉了原来的搜索词,或者node中间层有某一条链路的代码把搜索词改掉了。于是把node中间层的搜索链路的相关代码都研究了好几遍,通过关键字搜项目全局,把每个可疑的地方都看了,感觉代码逻辑写的都没毛病(node中间层的代码链路写得跟炒面一样,看得头都大了)。


没办法,代码分析不出来问题所在,测试环境又无法复现,只能在代码分析的基础上,把每个有可能改到搜索词的可疑地方打上日志,在搜索链路的一些比较关键的执行地方也打上日志,重新发版,来辅助排查。


// 线上打日志的时候需要注意加条件限制,不然每个用户请求都打日志,一下子就打爆了
if (req.query.sdebug === 's') {
logger.warn({ /** data */ })
}

抓住日志这根救命稻草


之前也有猜测,可能是网关啊、waf啊把请求拦截下来更改了搜索词,所以我们在请求入口那里也打了日志。从日志上来看,从中间层入口进来时,此刻的搜索词还是正常的,说明不是网关、waf搞的鬼。第一次的日志帮助我们缩小了排查范围,但是还不能分析出来,还需要再补充一些日志,意味着还要再发版,没办法,就是这么麻烦。


考验你js能力的时候到了


日志只是一种辅助手段,帮你记录异常数据,缩小排查范围。是否能从一堆代码中找出那一行有问题的代码就要看你自己了,我把有问题的代码写成一个demo了,展示在下面,你能分析出来问题所在吗?


const express = require('express')
const app = express()

const aConfig = Object.freeze({
info: { word: 'default' },
getWord ({ word }) {
return word
},
})
const bConfig = Object.freeze({
info: {},
getWord ({ req, word }) {
// 日志记录到这里word是'sivan',正常word应该是undefined,取的是req.params[0]才对
return word || req.params[0]
},
})

const setRequestData = ({ info: { word }, getWord }) => {
return (req, res, next) => {
word = word || req.query.word
res.end(getWord({ req, word })) // 回显搜索词
}
}

const getHandler = (config) => {
return setRequestData(config)
}
const aSearch = getHandler(aConfig)
const bSearch = getHandler(bConfig)

app.get('/a-search', aSearch)
app.get(/^\/b-search\/([^\/]+)\/?$/, bSearch)

app.listen(2333, () => {
console.log('run')
})

开始揭开谜底


const setRequestData = ({ info: { word }, getWord }) => {
return (req, res, next) => {
word = word || req.query.word // 罪魁祸首
res.end(getWord({ req, word }))
}
}

在a搜索链路中,word是有值的,为'default'。
在b搜索链路中,word和req.query.word都应该是undefined,所以b搜索链路传给getWord的word应该是undefined才对。


观察setRequestData这个函数的实现,它对config解构出了word变量,然后返回了一个中间件函数,word变量的作用域是在setRequestData的函数作用域里的,setRequestData函数只会执行一次,而中间件函数在每一次请求中都会执行。


中间件函数使用了word变量,这就创建了一个闭包,闭包使得word变量可以长期存储和被访问。


复现步骤如下:



  1. 我们第一次输入http://localhost:2333/b-search/sang?word=sivan(拼接上?word=sivan),回显sivan

  2. 之后我们每次输入http://localhost:2333/b-search/xxx,xxx为任意字符串,都会回显sivan


setRequestData函数只会执行一次,中间件函数每一次请求都会执行,所以当我们第一次输入http://localhost:2333/b-search/sang?word=sivan时,word变量被赋值为req.query.word并因为闭包被存储起来,等下一次输入时,由于word = word || xx,会先取存储的word,这就导致了每一次输入都会回显sivan。


改动思路如下截图:虽然闭包还存在,但是这样修改就不会让闭包的变量值被意外篡改,导致意料之外的结果了。


image.png


归因


这个问题其实挺严重的,搜索功能直接没用,用户都搜不了内容了,打工人打工不容易,哭泣。也挺有意思的,我只要在链接后面拼接?word=sivan就可以让全球的用户看到同事的名字,扬名立万(不止万了,起码千万了),破涕为笑。


为什么是偶现的呢?因为是集群,有很多服务器节点,每一次请求都可能打到不同的节点上,你输入b-search/xx?word=sivan时,请求只会打到其中一个节点上,只会污染那一个节点上的那个长期存储的word变量。所以被污染的集群节点有问题,没被污染到的集群节点就没问题。


为什么只有x国家站点出现该故障?测试环境没出现过该故障?因为这个故障的触发条件比较苛刻,必须输入b-search/xx?word=sivan才会触发,而正常情况下b搜索链路是不会拼接word=sivan这个query参数的。猜测最开始之时,就是有人在x国家站点因为一些原因输入了http://localhost:2333/b-search/sang?word=sivan引发问题,其他国家站点和测试环境没有输入就没有问题。


触发条件这么苛刻,是谁触发的呢?



  • 有可能是用户,毕竟几千万用户在用搜索,什么情况在用户那都会发生

  • 有可能是测试人员,测试在线上环境偶然拼接了这个参数

  • 有可能是黑客或者友商(概率很低,因为只影响了部分站点)

  • 前端开发人员,实现了这么一段如此隐晦的bad代码,等哪一天加班太多,心里不爽了,回家敲几个字拼接url访问,网站功能立马下线。


删库跑路的梗大家都耳熟能详,我们前端不止是会在svg里面、console里面吐槽公司,我们还可以在node中间层里写bad bad的代码哦,而且还很难测出来,事后归因到前端身上。


作者:前端爆冲
来源:juejin.cn/post/7294852698460471308
收起阅读 »

记录一次接口加密的实现方案

web
隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。 背景介绍 由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日...
继续阅读 »

0002.jpg
隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。


背景介绍


由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日程,部门的大佬们也讨论了各种加密方式,考虑各种情况,最终敲定了方案。说到我们常用的数据加密方法,方式是各种各样的,根据我们实际的业务需求,我们可以选择其中的一种或者几种方式方法进行数据加密处理。




  • 加密方法:常用的AES,RSA,MD5,BASE64,SSL等等;

  • 加密方式:单向加密,对称加密,非对称加密,加密盐,数字签名等等;



首先我们来简单分析一下上面说到的这几种加密有什么区别吧:




  • AES加密:对称加密的方法,加解密使用相同的加密规则,密钥最小能够支持128,192,256位(一个字节8位,后面我使用的是16位字符);

  • RSA加密:非对称加密的方法,加解密使用一对公钥私钥进行匹配,客户端使用公钥加密,服务端使用私钥进行解密;

  • MD5加密:单向加密,加密后不可解密,只能通过相同的数据进行相同的加密再与库中数据进行对比;

  • BASE64:一种数据编码方式,伪加密,把数据转化为BASE64的编码形式,通过A-Z,a-z,0-9,+,/ 共64个字符对明文数据进行转化;

  • SSL加密:https协议使用的加密方式,使用多种加密方式进行加密(具体使用哪些,我也不了解,感兴趣的同学可以去搜一下告诉我哈);



想要详细了解各类加密方式方法的同学,可以自行百度一下哈,这里就不进行赘述了,之后就来详细讲一下本次使用的加密方式。本次为了更加全面加密,使用了AES,RSA,以及加密盐,时间戳,BASE64与BASE16转化等方式进行加密处理。


请求体AES加密


请求体使用AES的对称加密方式,每次接口请求会随机生成一个16位的秘钥,使用秘钥对数据进行加密处理,返回的数据也会使用此秘钥进行解密处理。


import CryptoJs from 'crypto-js'// AES加密库
import { weAtob } from './weapp-jwt' // atob方法

// 请求体加密方法
export const encryptBodyEvent = (data, aeskey, isEncryption) => {
// 请求体内容
const wirteData = {
data: data, // 接口数据
token: Taro.getStorageSync("authToken"), // token 校验
nonce: randomNumberEvent(32), // 32位随机数,接口唯一随机数,可查询服务日志
timestamp: new Date().getTime, // 时间戳,用于设置接口调用过期时间
}
const encryptBodyJson = CryptoJs.AES.encrypt(JSON.stringify(wirteData), CryptoJs.enc.Utf8.parse(aeskey), {
mode: CryptoJs.mode.ECB,
padding: CryptoJs.pad.Pkcs7
}).toString()
// 判断接口是否需要加密
// 服务接收BASE16数据,Base64toHex方法为BASE64转化为BASE16方法
return isEncryption ? Base64toHex(encryptBodyJson) : wirteData
}

// BASE64转化BASE16方法
function Base64toHex (base64) {
let raw = weAtob(base64)
let HEX = ""
for (let i=0; i < raw.length; i++) {
let _HEX = raw.charCodeAt(i).toString(16)
HEX = (_HEX.length == 2 ? _HEX : "0" + _HEX)
}
return HEX
}

// 生成n位随机数,默认生成16位
function randomNumberEvent (length = 16) {
let str = ""
let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
// 随机产生
for(let i=0; i < length; i++){
let pos = Math.round(Math.random() * (arr.length-1));
str += arr[pos];
}
return str
}


  • mode是分组加密模式:有五种模式(ECB、CBC、CFB、OFB、CTR),这里我们使用最简单的ECB模式,明文分组加密之后的结果将直接成为密文分组,对其他几种模式感兴趣的可以去搜索一下几种模式的区别;

  • padding是填充模式:正常的加密后的字节长度不可能刚刚好满足固定字节的对齐(块大小),所以需要进行一定的填充,常用的有三种模式(PKCS7、PKCS5、Zero,还有其他模式),这里我们使用的是PKCS7模式。假设数据长度需要填充n个字节才对齐,那么填充n个字节,每个字节都是n;假设数据本身就已经对齐了,则填充一个长度为块大小的数据,每个字节都是块大小;

  • weAtob即小程序使用的atob方法:atob是JS的一个全局函数,用于将BASE64编码转化为原始字符串,在正常的H5项目中atob可以直接使用,但是在小程序中此方法不可用,因此使用一个手动实现的方式(文件就不上传了,电脑是加密的,上传也是乱码,网上也是能找到类似的方法);

  • timestamp是用于防止过期调用:这里的时间是为了展示方便直接使用客户端时间,实际是会调用一个服务端的接口获取服务器时间进行时间校准,防止客户端手动修改时间,服务端设置过期时间,会根据传入的时间判断是否过期;


请求头RSA加密


看完上面的请求体加密,我们会想到一个问题,就是我们的aesKey是客户端随机生成的,但是服务端也需要这个aesKey进行数据的加解密,那么我们通过什么形式传给服务端呢?因此我们在请求头中设置一个secret-key字段,使用RSA中的公钥对aesKey进行加密,服务端使用对应私钥进行解密;


// import JSEncrypt from 'jsencrypt' // RSA加密库,小程序不支持
import WxmpRsa from 'wxmp-rsa' // RSA加密库,小程序支持

let public_key = 'xxxxxxxxxxxxxxxx' // 公钥
// 请求头加密方法
export const randomKeyEvent = (aesKey) => {
// JSEncrypt方法小程序不可用
// const RSAUtils = new JSEncrypt() // 新建JSEncrypt对象
// RSAUtils.setPublicKey(public_key) // 设置公钥
// return RSAUtils.encrypt(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')

const RSAUtils = new WxmpRsa() // 新建WxmpRsa对象
RSAUtils.setPublicKey(public_key) // 设置公钥
// 进行RSA加密后,生成字符串中的部分特殊字符在服务端会被自动转化为空格,导致解密失败,所以先进行转换处理
return RSAUtils.encryptLong(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
}


  • JSEncrypt在小程序不可用是由于库里面存在window对象以及navigator对象,但是小程序没有对应的方法,所以使用了一个优化后的wxmp-rsa库;

  • replaceAll处理字符是因为RSA加密后,生成字符串中的部分特殊字符传给服务端会被自动转化为空格,导致解密失败,所以需要进行转换处理,为了兼容低版本replaceAll方法不支持可使用replace加正则进行替换;


返回体AES解密


服务端返回的数据内容使用了相同的AES加密方法,因此也需要使用AES进行数据解密处理,并且返回的数据是BASE16,因此还需要进行一次编码转换处理;


// 返回体解密方法
export const decryptBodyEvent = (data, aeskey) => {
// HexToBase64为BASE16转化为BASE64方法
const responseData = CryptoJs.AES.decrypt(HexToBase64(data), CryptoJs.enc.Utf8.parse(aeskey), {
mode: CryptoJs.mode.ECB,
padding: CryptoJs.pad.Pkcs7
}).toString(CryptoJs.enc.Uth8)
return JSON.parse(responseData)
}

// base16转base64 网上找个一个方法,应该有其他简单的实现方式
function HexToBase64 (sha1) {
var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var base64_rep = ""
var ascv
var bit_arr = 0
var bit_num = 0

for (var n = 0; n < sha1.length; ++n) {
if (sha1[n] >= 'A' && sha1[n] <= 'Z') {
ascv = sha1.charCodeAt(n) - 55
} else if (sha1[n] >= 'a' && sha1[n] <= 'z') {
ascv = sha1.charCodeAt(n) - 87
} else {
ascv = sha1.charCodeAt(n) - 48
}

bit_arr = (bit_arr << 4) | ascv
bit_num += 4
if (bit_num >= 6) {
bit_num -= 6
base64_rep += digits[bit_arr >>> bit_num]
bit_arr &= ~ (-1 << bit_num)
}
}

if (bit_num > 0) {
bit_arr <<= 6 - bit_num
base64_rep += digits[bit_arr]
}
var padding = base64_rep.length % 4
if (padding > 0) {
for (var n = 0; n < 4 - padding; ++n) {
base64_rep += "="
}
}
return base64_rep
}

封装接口


因为是小程序项目,使用的是Taro框架进行封装的,Vue中使用axios封装其实也是类似的,还封装了一套ajax的方法,除了接口这里封装有区别,加密都是类似的。


const baseUrl = 'https://xxx.xxx.com' // 接口请求头

// Taro封装接口方法
export const requestEncHttp ({url, data, isEncryption = true}) => {
// 每次调用都会随机生成一个动态的aesKey,防止接口被复用
const aesKey = randomNumberEvent()
return new Promise((resolve, reject) => {
Taro.request({
method: "POST",
header: {
"content-type": "application/json",
"secret-key": isEncryption ? randomKeyEvent(aesKey): ''
},
dataType: 'text',
data: encryptBodyEvent(data, aesKey, isEncryption),
url: baseUrl + url,
success: (result) => {
if(result.status === 200) {
resolve(isEncryption ? decryptBodyEvent(result.data, aesKey) : JSON.parse(result.data))
} else {
reject(result)
}
}, fail: (err) => {
reject(err)
}
})
})
}


  • dataType使用text:ajax没有此问题,Taro框架会出现接口有返回数据,但是在success中接收不到数据,因为数据是BASE16形式,Taro封装的数据返回格式默认应该是JSON的,所以要单独设置一下。


总结


加密的方式有很多,这篇文章也只是浅尝即止,想更加详细了解的同学可以再搜一些大佬们的总结文章,我这里也只是结合业务做了一点总结,标注一下踩坑点;



  • 刚开始本来是准备一个月最少写一篇的,但是由于八月份刚换工作,不太有时间去写,所以也是一直拖着;而且是用公司电脑写的这篇文章,所以代码都没有直接粘贴过来,可能会存在疏漏,请多多包涵哈;


作者:追风筝的呆子
来源:juejin.cn/post/7298160530291490828
收起阅读 »

拖拽API的简单应用

web
我们在实际开发中经常能遇见拖拽的运用场景,比如说拖拽排序、拖拽删除等,本文将以实现一个简单的课程表来进行拖拽API的简单应用,帮助大家复习一下一些基础知识。 相关拖拽事件 实现一个元素拖拽,我们只需要在HTML标签设置draggable为true <...
继续阅读 »

我们在实际开发中经常能遇见拖拽的运用场景,比如说拖拽排序、拖拽删除等,本文将以实现一个简单的课程表来进行拖拽API的简单应用,帮助大家复习一下一些基础知识。


tutieshi_640x390_9s.gif


相关拖拽事件


实现一个元素拖拽,我们只需要在HTML标签设置draggabletrue


 <div class="left">
<div draggable="true" class="color1 item">语文</div>
<div draggable="true" class="color2 item">数学</div>
<div draggable="true" class="color3 item">英语</div>
<div draggable="true" class="color4 item">音乐</div>
<div draggable="true" class="color5 item">政治</div>
<div draggable="true" class="color6 item">历史</div>
<div draggable="true" class="color7 item">体育</div>
</div>

我们设置了拖拽属性,在拖动的过程中我们会触发很多事件


// 拖动开始
container.ondragstart = (e) => {
console.log('start', e.target)
}

// 拖动覆盖
container.ondragover = (e) => {
console.log('over', e.target)
}

// 拖动进入
container.ondragenter = (e) => {
console.log('enter', e.target)
}

// 拖动结束
container.ondrop = (e) => {
// 一般div、td是不允许有元素置于他们上面,在ondragover设置阻止冒泡
console.log('drop', e.target)
}

如上,我们在这个应用主要用到了这几个拖拽事件,其中要特别注意的是ondrop事件,因为很多的HTML标签是不允许有其他元素覆盖在他们上面的,我们在案例中最外层用了div标签,所以必须要设置阻止冒泡才能让该事件生效


设置拖拽鼠标样式


如效果图所演示,我们在新增课程的时候,鼠标呈现的是一个加号的状态,在移除时又是一个简单的鼠标样式。这里我们是通过datasetondragstart设置相关属性来进行动态实现的


    <div class="left">
<div data-effect="copy" draggable="true" class="color1 item">语文</div>
<div data-effect="copy" draggable="true" class="color2 item">数学</div>
<div data-effect="copy" draggable="true" class="color3 item">英语</div>
<div data-effect="copy" draggable="true" class="color4 item">音乐</div>
<div data-effect="copy" draggable="true" class="color5 item">政治</div>
<div data-effect="copy" draggable="true" class="color6 item">历史</div>
<div data-effect="copy" draggable="true" class="color7 item">体育</div>
</div>

container.ondragstart = (e) => {
// 设置拖拽鼠标样式 默认值为move
e.dataTransfer.effectAllowed = e.target.dataset.effect
}

设置拖拽背景色


依旧根据设置的datakey,并检索父级,通过ondragenter事件动态插入class,实现背景色的显示


  <div class="left" data-drop="move">
<div data-effect="copy" draggable="true" class="color1 item">语文</div>
<div data-effect="copy" draggable="true" class="color2 item">数学</div>
<div data-effect="copy" draggable="true" class="color3 item">英语</div>
<div data-effect="copy" draggable="true" class="color4 item">音乐</div>
<div data-effect="copy" draggable="true" class="color5 item">政治</div>
<div data-effect="copy" draggable="true" class="color6 item">历史</div>
<div data-effect="copy" draggable="true" class="color7 item">体育</div>
</div>


<tr>
<th rowspan="4" class="span">上午</th>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>


function getDropNode(node){
while(node){
if(node?.dataset?.drop){
return node
}
node = node.parentNode
}
}

function clearDropStyle(){
const dropNodes = document.querySelectorAll('.drop-over')
dropNodes.forEach((node) => {
node.classList.remove('drop-over')
})
}

container.ondragenter = (e) => {
clearDropStyle()
const dropNode = getDropNode(e.target)
if(!dropNode){
return
}
if( e.dataTransfer.effectAllowed === dropNode?.dataset?.drop){
dropNode.classList.add('drop-over')
}
}

实现新增删除


根据一开始的设想,我们是新增了dataset进行同类别的有效拖拽,依旧进行比较,根据情况新增、删除节点


let source;

container.ondragstart = (e) => {
// 设置拖拽鼠标样式
e.dataTransfer.effectAllowed = e.target.dataset.effect
source = e.target
}


container.ondrop = (e) => {
// 一般div、td是不允许有元素置于他们上面,在ondragover设置组织冒泡
console.log('drop', e.target)

clearDropStyle()
const dropNode = getDropNode(e.target)
if(!dropNode){
return
}
if(e.dataTransfer.effectAllowed !== dropNode.dataset.drop){
return
}
if(dropNode.dataset.drop === 'copy'){
dropNode.innerHTML = ''
const cloned = source.cloneNode(true)
cloned.dataset.effect = 'move'
dropNode.appendChild(cloned)
}else{
source.remove()
}
}

我们在ondrop是不能拿到拖拽的节点的,设置一个全局变量,在ondragstart中保存节点,同时在复制完节点后要将其dataset-effect改成move


作者:_初七
来源:juejin.cn/post/7297908859176681484
收起阅读 »

了不起的Base64

web
不要乱说话。话说出去之前我们还是话的主人,话说出去之后我们就成了话的奴隶。 大家好,我是柒八九。 前言 在我们项目开发中,Base64想必大家都不会很陌生,Base64是将二进制数据转换为文本的一种优雅方式,使存储和传输变得容易。但是,作为一个合格的程序员,...
继续阅读 »

不要乱说话。话说出去之前我们还是话的主人,话说出去之后我们就成了话的奴隶。



大家好,我是柒八九


前言


在我们项目开发中,Base64想必大家都不会很陌生,Base64是将二进制数据转换为文本的一种优雅方式,使存储和传输变得容易。但是,作为一个合格的程序员,我们应该有一种打破砂锅问到底的求助欲望。


所以,今天我们来讲讲在各种语言中出镜率都高的离谱的Base64算法。今天,我们就用我们在初高中语文老师教我们的描述一个事物的三大步骤:1. 是什么,2. 如何工作,3. 为什么它很重要。来讲讲Base64算法。


好了,天不早了,干点正事哇。



我们能所学到的知识点




  1. 前置知识点

  2. 为什么会出现 Base64 编码

  3. 什么是 Base64 编码?

  4. Base64 使用案例

  5. Base64 编码算法

  6. 如何进行 Base64 编码和解码





1. 前置知识点



前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用



RFC



RFC,全称为Request for Comments,是一种用于定义互联网标准和协议的文件系列。



RFC最早由互联网工程任务组(IETF)创建,用于记录和传播互联网协议、方法和最佳实践的提案、规范和讨论。


每个 RFC 都有一个唯一的编号,通常以RFC开头,后面跟着一个数字,例如RFC 791RFC 2616等。RFC文档通常包含了协议规范、技术说明、最佳实践、标准化提案等,以促进互联网技术的发展和互操作性。


我们可以在IETF-datatracker中输入指定的编号或者查找的关键字进行搜寻。



以下是一些常见的RFC文档,大家可以翻阅自己想了解的技术点:




  1. RFC 791 - Internet Protocol (IP): 定义了 IPv4,是互联网上最基本的协议之一。




  2. RFC 793 - Transmission Control Protocol (TCP): 定义了 TCP,一种重要的传输协议,用于可靠的数据传输。




  3. RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1: 定义了 HTTP 协议,用于在 Web 上传输超文本的基础通信协议。




  4. RFC 2326 - Real Time Streaming Protocol (RTSP): RTSP 用于流媒体传输,如音频和视频流的控制。




  5. RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2: 定义了 TLS 1.2,用于安全地传输数据,如 HTTPS 协议中使用的加密通信。




  6. RFC 4648 - 这是咱们今天的主角,Base64的相关内容









Latin-1 字符集


Latin-1,也称为ISO-8859-1,是一种由国际标准化组织(ISO)认可的8 位字符集,代表了西欧语言的字母表。正如其名称所示,它是ISO-8859的一个子集,该标准还包括用于写作系统如西里尔文、希伯来文和阿拉伯文的其他相关字符集。它被大多数Unix系统以及Windows系统使用。



Latin-1有时被不太准确地称为扩展 ASCII



这是因为其字符集的前 128 个字符与美国 ASCII 标准相同。其余字符集包含了带重音的字符和符号。


关于更详细的Latin-1的表格,可以参考Latin-1-table




btoa


btoaJavaScript 中的一个内置函数,用于将二进制数据(通常是 8 位字节)编码为 Base64 字符串。它的名称是 binary to ASCII 的缩写,用于将二进制数据转换为文本字符串,以便在文本协议中传输或存储。


用法:


btoa 函数接受一个字符串参数,该字符串包含二进制数据。它将该二进制数据转换为 Base64 编码的字符串。


const binaryData = "front789";
const base64String = btoa(binaryData);
console.log(base64String);

这段代码将 front789 这个字符串转换为 Base64 编码的字符串并将结果打印到控制台。


限制:


尽管 btoa 是一个有用的函数,但它有一些限制:




  1. 只能编码字符串: btoa 函数只接受字符串作为参数,而不接受其他类型的数据(如二进制数组)。如果需要编码二进制数据,需要先将其转换为字符串。




  2. 字符集限制: btoa 函数仅支持 Latin-1 字符集,这意味着它只能编码包含在 Latin-1 字符集内的字符。如果字符串包含超出 Latin-1 字符集的字符,那么会导致编码失败。




  3. 不适合加密:Base64 编码不是加密,它只是一种编码方式,不提供安全性。如果需要加密数据,应该使用专门的加密算法而不是仅仅进行 Base64 编码。




  4. 数据大小增加: Base64 编码会增加数据大小。通常情况下,Base64 编码后的数据会比原始二进制数据更大,这可能会对数据传输和存储造成额外开销。




Data URL


Data URL 是一种统一资源标识符(URI)方案,用于将数据嵌入到文档中,而不是从外部文件加载数据。Data URL 允许我们将数据(如文本、图像、音频等)直接包含在网页或文档中,而不需要额外的 HTTP 请求。这种方式对于小型资源或需要避免外部请求的情况非常有用。


Data URL 的基本结构如下:


data:[<mediatype>][;base64],<data>

其中:



  • <mediatype> 是可选的媒体类型(例如,text/plainimage/png),用于描述数据的类型。如果被省略,则默认值为 text/plain;charset=US-ASCII

  • ;base64 是可选的,表示数据以 Base64 编码方式包含。如果省略了 ;base64,则数据将以纯文本方式包含。

  • <data> 包含实际的数据,可以是文本或二进制数据。


以下是 Data URL 的一些常见用途和示例:




  1. 嵌入图像: Data URL 可用于将图像直接嵌入 HTMLCSS 中,而不需要外部图像文件。例如,将一张 PNG 图像嵌入 HTML 中:


    <img
    src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg=="
    alt="Embedded Image"
    />




  2. 内联 CSS: Data URL 可用于内联 CSS 样式表,以减少外部 CSS 文件的请求。例如,将 CSS 样式表嵌入 HTML 中:


    <style>
    body {
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg==);
    }
    </style>



  3. 嵌入字体: Data URL 可用于嵌入自定义字体,以确保字体在不同设备上显示一致。例如,嵌入一个字体文件:


    @font-face {
    font-family: "CustomFont";
    src: url(data:application/font-woff;base64,d09GRgABAAAA...) format("woff");
    }



  4. 内联脚本: Data URL 可用于内联小型 JavaScript 脚本,以减少外部脚本文件的请求。例如,内联一个简单的 JavaScript 函数:


    <script>
    let greeting = "前端柒八九";
    alert(greeting);
    </script>





2. 为什么会出现 Base64 编码


要理解为什么需要 Base64 编码,我们需要了解一些计算机历史。


计算机以二进制(01)进行通信,但人们通常希望使用更丰富的数据形式进行通信,如文本图像为了在计算机之间传输数据,首先必须将其编码为 0 和 1,然后再解码。以文本为例,有许多不同的编码方式。如果我们都能就一个单一的编码方式达成一致,那将会简单得多,但很遗憾,这并不是事实。针对这块的内容,可以参考了不起的 Unicode


最初创建了许多不同的编码方式(例如 Baudot 编码),每种方式使用不同数量的比特来表示一个字符,直到最终 ASCII 成为一个标准,每个字符使用 7 位。然而,大多数计算机将二进制数据存储为每个字节由 8 位组成的数据,因此 ASCII 不适合传输这种类型的数据。一些系统甚至会删除最高位。


为解决这些问题,引入了 Base64 编码。这允许我们将任意字节编码为已知不会损坏的字节(ASCII 字母数字字符和一些符号)。缺点是使用 Base64 对消息进行编码会增加其长度 - 每 3 个字节的数据编码为 4 个 ASCII 字符


要可靠地发送文本,我们可以首先使用自己选择的文本编码(例如 UTF-8)将其编码为字节,然后将结果的二进制数据使用 Base64 编码为可安全传输的 ASCII 文本字符串。接收者反转此过程以恢复原始消息。当然,这需要接收者知道使用了哪种编码,通常需要单独发送这些信息。


我们来看一个示例:


我希望发送一个带有两行的文本消息:


Hello
world!

如果我将其发送为 ASCII(或 UTF-8),它将如下所示:


72 101 108 108 111 10 119 111 114 108 100 33

某些系统会破坏字节 10,所以我们可以将这些字节作为 Base64 字符串进行 Base64 编码:


SGVsbG8Kd29ybGQh

这里的所有字节都是已知的安全字节,所以很少有机会使任何系统损坏此消息。我可以发送这个消息而不是我的原始消息,然后让接收者反转此过程以恢复原始消息。




2. 什么是 Base64 编码?


Base64编码将二进制数据转换为文本,具体来说是ASCII文本。生成的文本仅包含A-Za-z0-9以及符号+/这些字符。


而在之前我们在了不起的 Unicode中介绍过ASCII的。


由于字母表中有 26 个字母,我们有26 + 26 + 10 + 2(64)个字符。因此,这种编码被命名为Base64。这 64 个字符被认为是安全的,也就是说,与字符<>\n等不同,它们不会被旧计算机和程序误解


下面是经过 Base64 编码的文本front789的样子:ZnJvbnQ3ODk=


还有一点需要注意,如果在使用JS对某一个文本进行准换时,如果该文本包含非Latin1字符的字符串,会报错,所以我们需要对其进行准换处理。


// 原始文本字符串,包含非Latin1字符
const text = "前端柒八九";

// 创建一个 TextEncoder 对象,用于将文本编码为字节数组
const encoder = new TextEncoder();

// 使用 TextEncoder 对象将文本编码为字节数组
const data = encoder.encode(text);

// 使用 String.fromCharCode 和展开运算符 (...) 将字节数组转换为字符串
// 然后使用 btoa 函数将字符串转换为 Base64 编码
const base64 = btoa(String.fromCharCode(...data));

// 打印 Base64 编码后的结果
console.log(base64); //5YmN56uv5p+S5YWr5Lmd

我们在这里并没有加密文本。给定Base64编码的数据,非常容易将其转换回(解码)原始文本。我们只是改变了数据的表示,即编码



在本质上,Base64编码使用一组特定的、减少的字符来编码二进制数据,以防止数据损坏。



Base64字母表


由于只有64个字符可用于编码,我们可以仅使用6位来表示它们,因为2^6 = 64。每个Base64数字表示6位数据。一个字节中有8位,而 86最小公倍数24。因此,24 位,或 3 个字节,可以用四个 6 位的 Base64 数字表示


4. Base64 使用案例


我们可能在HTML文档中使用了<img src="789.jpeg">标签来包含图像。其实,我们可以直接将图像数据嵌入到 HTML 中,而不必使用外链!数据URL可以做到这一点,它们使用Base64编码的文本来内联嵌入文件。


<img src="data:image/gif;base64,xxxxbase64encodedtextxxxx" />

data:[<mime type
>
][;charset=<charset>][;base64],<encoded data></encoded></charset
></mime>

另一个常见的用例是当我们需要在网络上传输或存储一些二进制数据,而网络只能处理文本或ASCII数据时。这确保了数据在传输过程中保持不变。还有就是在 URL 中传递数据时,当数据包含不适合 URL 的字符时,此时Base64就有了用武之地。


Base编码还在许多应用程序中使用,因为它使得可以使用文本编辑器来操作对象。


我们还可以使用 Base64 编码将文件作为文本传输



  • 首先,获取文件的字节并将它们编码为 Base64

  • 然后传输 Base64 编码的字符串,然后在接收端解码为原始文件内容




5. Base64 编码算法


以下是将一些文本转换为 Base64 的简单算法。



  1. 将文本转换为其二进制表示

  2. 比特位分组为每组6位

  3. 将每个组转换为0到63的十进制数。它不能大于 64,因为每组只有 6 位。

    • 如果转换为十进制数的数字大于 64,我们可以将其取模64 例如:151 % 64 = 23



  4. 使用Base64字母表将此十进制数转换为等效的Base64字符


通过上述操作我们会得到一个Base64编码的字符串。如果最后一组中的比特位不足,可以使用===作为填充。


让我们以front7作为范例,来模拟上述操作。




  1. 通过首先将每个字符转换为其对应的 ASCII 数字,然后将该十进制数转换为二进制,(使用ASCII 转二进制工具)将文本front7转换为二进制:


    01100110 01110010 01101111 01101110 01110100 00110111

    f r o n t 7



  2. 将比特位分组为每组6位


    011001 100111 001001 101111 011011 100111 010000 110111



  3. 将每个组转换为 0 到 63 之间的十进制数:


    011001 100111 001001 101111 011011 100111 010000 110111

    25 23 9 47 27 23 16 27


    • 这步中如果数据超过 64,需要对其 64 取模




  4. 现在使用 Base64 字母表将每个十进制数转换为其 Base64 表示:


    25  23   9   47  27  23  16  27

    Z n J v b n Q 3



然后我们完成了。名字front7在 Base64 中表示为ZnJvbnQ3


乍一看,Base64 编码的好处并不是很明显。


想象一下,如果我们有一张图片或一个敏感文件(PDF、文本、视频等),而不是简单的字符串,我们想将它存储为文本。我们可以首先将其转换为二进制,然后进行 Base64 编码,以获得相应的 ASCII 文本。


现在我们可以将该文本发送或存储在任何地方,以任何我们喜欢的方式,而不必担心一些旧设备、协议或软件会错误解释原始二进制数据以损坏我们的文件。


6. 如何进行 Base64 编码和解码


所有编程语言都支持将数据编码为 Base64 格式以及从 Base64 格式解码数据。


JS 中处理


// 简单字符串
const text1 = "front789";
bota(text1); // ZnJvbnQ3ODk=

// 超出`Latin-1`字符的字符串
const text2 = "前端柒八九";
const encoder = new TextEncoder();
const data = encoder.encode(text);
const base64 = btoa(String.fromCharCode(...data));
console.log(base64); //5YmN56uv5p+S5YWr5Lmd

Rust 中处理


Rust的话,我们可以直接用 base64 crate。


Cargo.toml 文件中添加以下内容:


[dependencies]
base64 = "0.21.5"

use base64::{Engine as _, engine::general_purpose};

let orig = b"data";
let encoded: String = general_purpose::STANDARD_NO_PAD.encode(orig);
assert_eq!("ZGF0YQ", encoded);
assert_eq!(orig.as_slice(), &general_purpose::STANDARD_NO_PAD.decode(encoded).unwrap());

// or, URL-safe
let encoded_url = general_purpose::URL_SAFE_NO_PAD.encode(orig);

想了解更多关于Rust如何处理Base64,可以查看Rust base64


此外,终端也内置支持 Base64 编码。在终端中尝试以下命令:


echo "前端柒八九" | base64
5YmN56uv5p+S5YWr5LmdCg==

$ echo "5YmN56uv5p+S5YWr5LmdCg==" | base64 -d
前端柒八九



后记


分享是一种态度


全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。



作者:前端小魔女
来源:juejin.cn/post/7298190770401001512
收起阅读 »

qiankun?这次我选了wujie!

web
写在最前: 本文面向对无界和微前端有一定了解的人群,不再对微前端的概念和无界的基础使用方式做解释说明 前言 掘金上搜wujie,那么大眼一看,好像全都是介绍的,并没有几个落地方案的分享。正好我上个月把部门内三个业务系统用wujie整合了一下,记录成文章和大...
继续阅读 »

写在最前:



本文面向对无界和微前端有一定了解的人群,不再对微前端的概念和无界的基础使用方式做解释说明



前言


掘金上搜wujie,那么大眼一看,好像全都是介绍的,并没有几个落地方案的分享。正好我上个月把部门内三个业务系统用wujie整合了一下,记录成文章和大家分享一下。(为什么不用qiankun?qiankun之前做了好多次了,这次想尝个鲜~)


背景说明


笔者部门内有三个管理系统,技术栈分别是:


A: Vue2 + Webpack4 + ant-design-vue@1.7.8:该项目是部门内“司龄”最长的,从部门成立之初起,所有的业务都堆在里边。


B: Vue3 + Webpack5 + ant-desgin-vue@3.2.20:由于业务目标不清晰以及前端开发各自为战,部分需求被拆出来了一个单独的项目进行开发,但实际上然并卵。


C: Vue3 + Vite2 + ant-design-vue@3.2.20:为了响应领导“统一前端UI规范”和“低代码降本增效”的号召,这个项目应运而生,使用JSON Scheme渲染列表页 + 手写Form表单的形式开发需求。


没错,就是3个纯业务向的管理系统。对接我们部门的大部分业务人员,日常都至少需要操作3个系统,甚至有些人还会用到别的部门的系统,甚至有的人习惯打开多个浏览器tab页来回切换对比同个页面的数据。。。poor guy。。。浏览器密密麻麻的全是tab页。。。


契机


某天,发生了如下对话:



  • 领导:业务部门老大说,系统间来回切换太麻烦了,有没有办法解决这个问题?

  • 我:有,微前端。

  • 领导:之前XXX不是用qiankun做过吗,问题很多,不了了之了。

  • 我:我看过他的代码,没有什么大问题,都是一些细节方面的小bug,而且还有别的微前端方案可以选择。

  • 领导:行,你安排一下,尽快上线

  • 我:好的。( 打工人被安排任务就是这么朴实,无华,且枯燥。。。)


为什么选择无界?


(此处省略万字长文对比分析qiankun、micro app、single-app...)


直接摆出站在个人角度以及团队技术、业务背景下选择无界的原因:



  1. 喜欢吃螃蟹:之前有过多次qiankun的落地经验,直接上qiankun,一点都不酷。(第一次了解到无界是22年的10月份左右,彼时的无界还在beta版,想尝尝鲜。况且就算使用无界出了岔子,也有信心能cover住)

  2. 子应用改造,侵入程度低:就像文档中宣传的那样,我用公司的项目跑demo,除去登录态的因素外,基本可以说是0改动接入,当时脑海中只有2个字----牛X!(当然,仅仅这样接入,离上生产的标准还相距甚远;而且最后我还是选择了类似qiankun根据宿主应用动态选择layout的布局方案,改造成本也可以说是不算低了,这个暂且按下不表)

  3. 方便独立开发、部署:与第2点相似但又不同:现有的项目有独立的域名、部署方案、且在生产环境已经稳定运行,在保留这些基础的前提下,无界的iframe方案算是最理想的出路(另外也有一点私心,如果生产环境的无界挂了,业务人员可以直接使用老的域名访问独立的子应用进行业务操作,毕竟出了生产事故是要通报批评的)


综上所述,确实没经过太多深思熟虑,想用就用,干就完了image.png


干货区


下面,就是在我接入文章开头提到的3个系统后,总结出来的大致接入步骤:



  1. 准备主应用,在接入第一个系统之前,不出意外的要先准备宿主应用。

  2. 子系统登录态管理

  3. 根据宿主环境,选择layout方案

  4. 安装wujieEventBus(基于无界去中心化的通信系统做的二次封装)

  5. 子应用afterMount生命周期

  6. 子系统网络请求管理

  7. UI组件定位修复

  8. 公共状态提升


1.准备主应用


一个比较常规、纯净的管理系统,没有过多的封装,因为宿主应用本身,也不需要什么内容。技术栈为Vue3 + Vite2 + ant-design-vue@3.2.20(没错,和系统C的技术栈一致,主打的就是一个偷懒),放张目录结构大家就明白了,没什么特殊的,有些细节后边会提到。


image.png

2.子系统登录态管理


简单来说,对于一个子应用,无论你是基于JWT还是Cookie的用户鉴权方案,在他单独运行时发生登陆态失效的情况,是要被redirect到自己的Login页面去;而当集成到了无界中运行的时候,登录态失效则应该被redirect到主应用的Login页面。


一般情况下,有两个地方需要做处理:



  1. http响应拦截,以axios为例:


if (response.status === 401) {
if (window.__POWERED_BY_WUJIE__) {
wujieEventBus.$emit("LOGIN_EXPIRED", APP_NAME_IN_WUJIE);
} else {
message.error("登录失效,请重新登录");
router.replace("/login");
}
}

window.__POWERED_BY_WUJIE__是无界注入到子应用window当中的一个全局变量。


wujieEventBus是我对无界自带的去中心化通信方式eventBus的封装,具体内容放在第四点展开讲,这里只需要知道,是通知主应用“我”登录失效了,并且附上“我”在主应用中的身份标识(对应组件方式使用无界的<WujieVue />所需的name属性)



  1. 路由守卫:可根据你的需要更改路由钩子,这里以beForeEach为例:


router.beforeEach((to, from, next) => {
if(validToken()) {
// some your logic ...
next();
}else {
wujieEventBus.$emit("LOGIN_EXPIRED", APP_NAME_IN_WUJIE);
}
}

当然,通过路由守卫拦截下登录态失效的情况可能很少很少,但操作和上面是一样的:通知主应用“我”登录失效了,并且附上“我”在主应用中的身份标识


3.根据宿主环境,子应用动态选择layout方案


如果你的主应用布局是打算这样:


8f1fb5771d3992651707926b38a8e5d.png


子应用甚至不用切换layout方案,在下方content区域中保留子应用所有的模块;上方的Menu区作为一个应用级的切换菜单。


但如果你的主应用是打算像这样常规布局:


b144f129bd3ba973060ee43d6273b1c.png
想实现应用级的切换,大体上有三种思路:



  1. 主应用不设任何layout模块:即Header、Menu、Content全都是子应用的模块。那么就需要所有子应用都是这种布局,且每个子应用的Menu菜单都必须是所有应用菜单的集合,当切换到非自身的路由时,与宿主通信进行应用切换。

  2. 与1相同,Header、Menu、Content全都是子应用的模块,但Menu仍是自己的菜单。你问我怎么切换应用?加个position: fixed的悬浮球呗(或类似的可折叠菜单)。


bb92fd682e09d744792a370c513d41c.png


通过hover悬浮球,展开/折叠菜单,点击进行应用切换。


说实话,这方案我自己都不相信有人会用。950fcc41577cd669da1b68e54714ad8.png



  1. 而第三个,也就是我选择的方案:主应用设有Header和Menu,剔除所有子应用的Header和Menu,只保留子应用的Content模块接入进来。熟悉吗?就是接qiankun那套。


大概长这样:


<template v-if="!isInWujieContainer">
<Menu />
<Layout>
<Header />
<Layout>
<keep-alive>
<router-view />
</keep-alive>
</Layout>
</Layout>
</template>
<template v-else>
<keep-alive>
<router-view />
</keep-alive>
</template>

// const isInWujieContainer = window.__POWERED_BY_WUJIE__

为什么选择方案3,在我看来:Menu维护在主应用中,相比于对每个子应用的Menu进行侵入式改造,开发成本和维护成本都更小。Header维护在主应用中,可以方便的管理路由栈(面包屑、tab页签,这里多提一下,我的子应用接入方式是保活+sync路由同步)


既然Menu维护在了主应用中,那么问题来了:点击了Menu中的某个菜单,怎么通知子应用跳转到对应的路由?


我们都知道,当无界开启了url sync同步的时候,主应用、子应用的url变化规则是:子应用url发生变化时,子应用的iframe会与主应用进行通信,主应用同步更新url;当页面刷新时,子应用iframe会从主应用的url中读取路由信息,保证子应用路由状态不丢失。但是并没有一种规则是主应用主动发起改变url、并且子应用能同步更新路由的方案。


我的做法其实也很简单,点击主应用Menu中的菜单时,通过wujieEventBus进行广播,对应的子应用收到消息时,切换路由:


// 主应用中点击Menu菜单
export const openChildRoute = (
_router: RouterObj,
app: AppCollection,
) => {
// 通知子应用路由已改变,registerMountedQueue可以理解为给子应用注册一个mounted后需要立即执行的事件,防止出现跳转到一个还未初始化的子应用时,$emit miss的问题。
EventBus.$registerMountedQueue(
app,
"CHANGE_ROUTE",
{ path: _router.path, app }
);

// 更新主应用自己的url和tab页签
router.push(fullPath);
store.commit("tabs/setList", {
fullPath,
name: _router?.name || "",
title: _router?.name,
});
setActiveKey(fullPath);
};

// 子应用收到消息
wujieEventBus.$on("CHANGE_ROUTE", function ({ path, query, app }) {
if (app !== APP_NAME_IN_WUJIE) return;
router.push({ path, query });
});

并且CHANGE_ROUTE这个事件可以是双向的:可以由主应用主动发起,通知子应用改变路由;也可以由子应用主动发起,通知主应用改变url和tab页签的显示状态。


企业微信截图_16991865379344.png


之所以这样设计,是因为我们的系统中存在一种特殊的路由页面,他不存在于Menu菜单中,是必须通过点击页面中的指定按钮才能进入。所以对于这类页面,必须是由子应用主动发起的。


4.安装wujieEventBus


无界提供了一套去中心化的通信方案,去中心化的优点显而易见:



  • 不关心发送方和接收方是谁,可以是不同应用之间通信,可以是一个应用内不同路由通信,可以是一个应用内不同组件通信

  • 可以很方便的一对多通信


但同时也有一个致命的缺点:通信成功的前提是建立在通信双方都online的情况下


假设这样一个场景:用户从站外的某个带参链接进入系统,参数的目的是告诉系统要重定向到指定子应用的指定路由,甚至具体要打开某个弹框。


bb72a5d9b7de765bdf88bd8d089d942.png


正常情况下,主应用判断url参数做跳转的逻辑不管放在哪里,都存在子应用未加载完成的可能性。


(如果你说每个子应用component的afterMount事件里都写一遍,fine,你赢了)


这个时候,只需要对无界的eventBus稍作改动,即可满足需求:


import WujieVue from "wujie-vue3";
import { AppCollection } from "@/constant";
import store from '@/store';
const { bus } = WujieVue;
type EventList = "LOGIN_EXPIRED" | "EVENT_NAME1" | "EVENT_NAME2"; // 一些事件类型涉及到公司业务,这里省去了

type EventBusInstance = {
$emit: (e: EventList, params: Record<string, any>) => void;
$on: (e: EventList, fn: (...args: any[]) => void) => void;
$registerMountedQueue: (
app: AppCollection,
e: EventList,
params: Record<string, any>
) =>
void; // 将事件注册到子应用mount成功的的事件队列中
$cleanMountedQueue: (app: AppCollection) => void; // 清空子应用mount事件队列
};

type Queue = {
[app in AppCollection]?: any[];
};

let instance: EventBusInstance | undefined = undefined;

export default () => {
const queue: Queue = {};
if (!instance) {
instance = {
$emit: (event, params) => bus.$emit(event, params),
$on: (event, fn) => bus.$on(event, fn),
$registerMountedQueue: (app, event, params) => {
const isMounted = store.state.globalState.appMounted[app]; // store中存储了子应用是否mount完成的状态
const fn = () => bus.$emit(event, params);

// 子应用已挂载完成可以直接通信
if (isMounted) return fn();

if (queue[app] && queue[app]!.length) {
queue[app]!.push(fn);
} else {
queue[app] = [fn];
}
},
$cleanMountedQueue: (app) => {
while (queue[app] && queue[app]!.length) {
const fn = queue[app]!.shift();
fn();
}
},
};
}

return instance;
};

为每个子应用都维护一个事件队列,主应用通过$registerMountedQueue注册事件时,若对应子应用已经mount完成,则直接emit进行通信;若子应用没有mount完成,则将注册的事件推入队列中。


子应用afterMount钩子中调用$cleanMountedQueue,清空属于自己的事件队列。


目前根据业务需要,只做了这一点封装,后续有可能会继续补充。


当然前边提到的这个场景,肯定还有许多不同的解决方案,根据自己的项目因地制宜才是最重要的。


5.子应用afterMount生命周期


上边第4点已经提到过,子应用afterMount钩子中要做两件事情:



  1. store中保存自己mount完成的状态。

  2. 调用$cleanMountedQueue清空自己的事件队列。


6.子系统网络请求管理


网络请求管理,主要解决的是跨域问题,分两种:




  • 调用后端服务跨域
    如果你的用户鉴权是基于cookie的,那最方便的就是使用无界推荐的方法:将主应用的fetch自定义改写后传给子应用。如果你的用户鉴权是基于JWT或者你使用了其他的http请求库,赶快买上两杯咖啡贿赂一下运维大佬,给子应用对应的服务配置下Response Header,支持主应用域名的跨域资源共享。但是要切记,生产环境不要使用Access-Control-Allow-Origin: *




  • 请求子应用静态资源跨域




刚才为啥要让买两杯咖啡,因为一杯是改后端服务支持跨域,还有一杯是改前端静态资源服务器(比如Nginx)支持跨域。48d109abb6ffcad175c35c4c8ecf90c.png


至此,你(wo)的无界微前端方案已经落地大半了,不出意外的话,除了个别地方的样式比较古怪,业务流程已经没啥大问题了,下面的工作就是各个页面点一点,修一修奇怪的样式问题。


7.UI组件定位修复


无界官方针对element-plus冒泡系列组件弹出位置不正确的解决方案是给子应用的body添加position: relative,但我这边使用ant-design-vue@1.7.8的项目并不是弹出位置不正确,而是弹出方向不对,只能暂时通过调整组件位置+修改placement的方式见一个改一个。48d109abb6ffcad175c35c4c8ecf90c.png


我这边还有一些使用左弹出的drawer组件也会有问题,起始位置并不是屏幕最左边,而是content区域的最左边。


企业微信截图_16991870384309.png


不知是否是无界的bug,drawer有个fixed定位的包裹容器,按理来说,创建这个包裹容器的时候会使用webcomponent代理的appendChild方法,可以突破iframe的区域限制,但通过审查元素发现,这个position: fixed; left: 0的元素,开始位置还是iframe的左侧。。。导致drawerposition: absolute的主体开始位置也只能是iframe的左侧。但又不是所有的左弹出drawer都有这个问题,很神奇。。。没办法,只好把这些有问题的暂且改为右弹出。。。有解决方案的朋友也可以交流一下。。。


8.公共状态提升


其实从这里开始,就属于优化的范畴了,目前只做了这一趴,后续有其他优化会持续补充。


做公共状态提升的原因,简单来讲就是:除了登录用户的信息以外,我们不同系统中也有着很多相同的枚举数据,这些数据本身也是从同样的接口中读的,存在vuex/pinia中。所以当一个系统独立运行时,他数据获取的逻辑不变;当作为子应用接入了微前端体系中时,只需要从主应用中等待数据同步,不需要自己再调接口去取。


// 主应用
export default () => {
const duties = [
// some http request callbacks
];
duties.forEach(async (d) => {
const { action, type, commition } = d;
const data = await action();
store.commit(commition, data);
bus.$registerMountedQueue(
'APP_NAME', // 业务系统name标识
"SYNC_STATE",
{
type,
data: toRaw(data),
}
);
});
};

// 子应用
const state = {
// a vuex state
}

const mutations = {
// a vuex mutation
}

const actions = {
// a vuex action
}

if(window.__POWERED_BY_WUJIE__){
wujieEventBus.$on("SYNC_STATE", ({ type, data }) => {
const [updateFn, stateKey, ...restPath] = type;
let config = state[stateKey];
if (restPath && restPath.length) {
set(config, restPath, data); // lodash set
} else {
config = data;
}
mutations[updateFn](state, config);
});
}else {
// old logic, init all states by actions
}

结语


这篇文章从开篇到写下结语,中间经历了一整个星期。后半部分整体写的比较仓促,可能有些地方和起笔之初的设想有所出入;并且许多的细节之处涉及到公司业务也没有做过多的说明。有不明白的地方、或者有想交流的同学也可以留言,我会尽可能的做答复。


另外做个说明,其实最开始的时候文章标题叫【无界(wujie-micro)微前端落地方案分享】,后来才改成现在这个名字,原因有二:



  • 这并不是一套完整的落地方案,只是我对我落地整个过程中,值得记录、分享的一些点的总结

  • 原先的名字有种让人一看就不想点进来的感觉


48d109abb6ffcad175c35c4c8ecf90c.png

行吧,第一版先到这里,欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


作者:Elecat
来源:juejin.cn/post/7297592806569164810
收起阅读 »

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

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
收起阅读 »