注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一夜之间,3.0万 Star,全部清零!

这是开源圈不久前发生的一件事情。 不知道大家有没有听说过NeteaseCloudMusicApi这个项目,从项目名字大家也能猜出,这是一个和网易云音乐API有关的开源项目。 该项目之前由开发者Binaryify所打造,是网易云音乐API的一个非官方封装库。 该...
继续阅读 »

这是开源圈不久前发生的一件事情。


不知道大家有没有听说过NeteaseCloudMusicApi这个项目,从项目名字大家也能猜出,这是一个和网易云音乐API有关的开源项目。


该项目之前由开发者Binaryify所打造,是网易云音乐API的一个非官方封装库。


该项目采用Node.js编写,可以实现非官方的网易云音乐API调用,从而可以让其他开发者可以基于该项目来获取网易云音乐平台上的一些歌曲、歌单、歌词、专辑、电台等信息,方便开发者构建基于网易云音乐服务的应用程序。



这个项目曾经在GitHub上非常火热,也因此一度获得了超过3.0w的Star标星,以及1.5w+的Fork。



不过就在最近,该项目的作者彻底清零了这个项目,包括其所有的代码、文档,以及commit提交信息,并在项目的主页README中更新了动态:



保护版权,此仓库不再维护




原因很简单,原来是作者收到了来自网易云音乐官方发送的法务通知函。


网易云音乐声明该项目侵犯了其公司的相关版权,并且要求开发者立即下线该项目中盗链网易云音乐的方法及内容。


具体的通知如下:



网易云音乐由杭州网易云音乐有限公司独立开发运营,网易云音乐作为国内知名的在线音乐平台,致力于为用户提供优质的音乐内容服务,我司以高额的成本采购了海量音乐作品的内容,在未我司合法授权的任何第三方均没有权利播放由我司享有版权的音乐作品,我司有权以自己的名义或授权第三方进行维权。




我司收到用户的举报,您开发的 NeteaseCloudMusicApi 或存在涉嫌通过非法破解网易云音乐内容接口的方式获取网易云音乐享有版权的歌曲内容。




贵司未经我司授权,通过技术手段破解绕开限制直接提供网易云音乐享有版权的音乐作品内容,该行为不仅侵犯了我司享有的音乐作品版权,亦非法攫取了网易云音乐的用户流量构成不正当竞争,损害了我司作为权利人的合法权益。




同时贵方通过非法技术手段破解网站获取大量歌曲内容的行为,涉嫌构成侵犯著作权罪,破坏 / 非法入侵计算机信息系统罪及 / 或提供破坏 / 非法入侵计算机信息系统工具罪。




针对上述侵权行为,根据中华人民共和国《著作权法》、《刑法》等相关法律法规规定,我司现郑重致函:




1、立即下线 NeteaseCloudMusicApi 上盗链网易云音乐的方法及内容;




2、在未获得我方授权的前提下,停止一切侵犯我司合法权益的行为。




请贵方充分认识到该行为的违法性和严重性,按照本函要求立即处理侵权行为,并将处理结果及时告知我方。若贵方怠于履行该项义务,为维护自身合法权益,我司将采取包括诉讼、投诉、举报等在内的一切必要的法律措施确保合法权利得到有效保护。



从这个项目的维护历史可以看出,曾经作者对于该项目还是花了不少心血的,包括维护的活跃度以及项目文档这块,都做了不少工作。



而且也有不少开发者来基于该项目进行二开,从而实现开发者自己的不同想法和需求。


然而面对这样一封告知函,项目作者也不得不做出清空仓库并将其进行永久归档的处理,毕竟这类项目确实存在版权方面的问题。


而尊重版权和合规开源也确实是每一位开源作者的基本职责。


其实像这类项目在GitHub上还是有不少的,在之前官方没有明确追责的情况下,其实大家对于一些非盈利性的小众开源项目基本也会睁一只眼闭一只眼,但是官方一旦追责起来,这类项目生存的概率就微乎其微了。


所以这件事也提醒我们,后续大家如果要经营和维护自己的开源项目,也是要多关注和留意一下版权方面的问题,从而避免后续可能会出现的一些不必要的麻烦。


作者:CodeSheep
来源:juejin.cn/post/7343137522069995529
收起阅读 »

用上了Jenkins,个人部署项目真方便!

作者:小傅哥 博客:bugstack.cn 项目:gaga.plus 沉淀、分享、成长,让自己和他人都能有所收获!😄 本文的宗旨在于通过简单干净实践的方式教会读者,如何在 Docker 中部署 Jenkins,并通过 Jenkins 完成对项目的打包构建并...
继续阅读 »

作者:小傅哥
博客:bugstack.cn
项目:gaga.plus



沉淀、分享、成长,让自己和他人都能有所收获!😄



本文的宗旨在于通过简单干净实践的方式教会读者,如何在 Docker 中部署 Jenkins,并通过 Jenkins 完成对项目的打包构建并在 Docker 容器中部署。


Jenkins 的主要作用是帮助你,把需要在本地机器完成的 Maven 构建、Docker 镜像发布、云服务器部署等系列动作全部集成在一个服务下。简化你的构建部署操作过程,因为 Jenkins 也被称为 CI&CD(持续集成&持续部署) 工具。提供超过 1000 个插件(Maven、Git、NodeJs)来支持构建、部署、自动化, 满足任何项目的需要。


官网:



本文涉及的工程:



一、操作说明


本节小傅哥会带着大家完成 Jenkins 环境的安装,以及以最简单的方式配置使用 Jenkins 完成对 xfg-dev-tech-jenkins 案例项目的部署。部署后可以访问 xfg-dev-tech-jenkins 项目提供的接口进行功能验证。整个部署操作流程如下;






  • 左侧竖列为核心配置部署流程,右侧是需要在配置过程中处理的细节。

  • 通过把本地对项目打包部署的过程拆解为一个个模块,配置到 Jenkins 环境中。这就是 Jenkins 的作用。


二、环境配置



  1. 确保你已经在(云)服务器上配置了 Docker 环境,以及安装了 docker-compose。同时最好已经安装了 Portainer 管理界面这样更加方便操作。

  2. 在配置和后续的验证过程中,会需要访问(云)服务的地址加端口。如果你在云服务配置的,记得开放端口;9000 - portainer9090 - jenkins8091 - xfg-dev-tech-app 服务


1. Jenkins 部署


1.1 上传文件






  • 如图;以上配置内容已经放到 xfg-dev-tech-jenkins 工程中,如果你是云服务器部署则需要将 dev-ops 部分全部上传到服务器的根目录下。

  • compose-down.sh 是 docker-compose 下载文件,只有你安装了 docker-compose 才能执行 docker-compose -f docker-compose-v1.0.yml up -d

  • jdk-down.sh 是 jdk1.8 下载路径,以及解压脚本。如果你在云服务器下载较慢,也可以本地搜索 jdk1.8 下载,并上传到云服务器上解压。注意:本步骤是可选的,如果你的项目不强依赖于 jdk1.8 也可以使用 Jenkins 默认自带的 JDK17。可以通过在安装后的 Jenkins 控制台执行 which java 找到 JDK 路径。

  • maven 下的 settings.xml 配置,默认配置了阿里云镜像文件,方便在 Jenkins 构建项目时,可以快速地拉取下载下来包。


1.2 脚本说明


version: '3.8'
# 执行脚本;docker-compose -f docker-compose-v1.0.yml up -d
services:
jenkins:
image: jenkins/jenkins:2.439
container_name: jenkins
privileged: true
user: root
ports:
- "9090:8080"
- "50001:50000"
volumes:
- ./jenkins_home:/var/jenkins_home # 如果不配置到云服务器路径下,则可以配置 jenkins_home 会创建一个数据卷使用
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/local/bin/docker
- ./maven/conf/settings.xml:/usr/local/maven/conf/settings.xml # 这里只提供了 maven 的 settings.xml 主要用于修改 maven 的镜像地址
- ./jdk/jdk1.8.0_202:/usr/local/jdk1.8.0_202 # 提供了 jdk1.8,如果你需要其他版本也可以配置使用。
environment:
- JAVA_OPTS=-Djenkins.install.runSetupWizard=false # 禁止安装向导「如果需要密码则不要配置」docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
restart: unless-stopped

volumes:
jenkins_home:

Jenkins Docker 执行安装脚本。



  • ./jenkins_home:/var/jenkins_home 是在云服务器端挂一个映射路径,方便可以重新安装后 Jenkins 依然存在。你也可以配置为 jenkins_home:/var/jenkins_home 这样是自动挂在 volumes jenkins_home 数据卷下。

  • docker 两个 docker 的配置是为了可以在 Jenkins 中使用 Docker 命令,这样才能在 Docker 安装的 Jenkins 容器内,使用 Docker 服务。

  • ./maven/conf/settings.xml:/usr/local/maven/conf/settings.xml 为了在 Jenkins 中使用映射的 Maven 配置。

  • ./jdk/jdk1.8.0_202:/usr/local/jdk1.8.0_202 用于在 Jenkins 中使用 jdk1.8

  • JAVA_OPTS=-Djenkins.install.runSetupWizard=false 这个是一个禁止安装向导,配置为 false 后,则 Jenkins 不会让你设置密码,也不会一开始就安装一堆插件。如果你需要安装向导可以注释掉这个配置。并且当提示你获取密码时,你可以执行;docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword 获取到登录密码。


1.3 执行安装





[root@lavm-aqhgp9nber dev-ops]# docker-compose -f docker-compose-v1.0.yml up -d
[+] Building 0.0s (0/0)
[+] Running 1/0
✔ Container jenkins Running

执行脚本 docker-compose -f docker-compose-v1.0.yml up -d 后,这样执行完毕后,则表明已经安装成功了💐。


2. 插件安装


地址:http://localhost:9090/ - 登录Jenkins









  • 1~2步,设置镜像源,设置后重启一下 Jenkins。

  • 3~4步,下载插件,先下载安装 chinese 汉化插件,方便不太熟悉 Jenkins 的伙伴更好的知道页面都是啥内容。

  • 5步,所有的插件安装完成后,都需要重启才会生效。安装完 chinese 插件,重启在进入到 Jenkins 就是汉化的页面了

  • 除了以上步骤,你还需要同样的方式安装 maven、git、docker 插件。

  • 注意,因为网络问题你可以再做过程中,提示失败。没关系,你可以再搜这个插件,再重新下载。它会把失败的继续下载。


3. 全局工具配置


地址:http://localhost:9090/manage/configureTools/





用于构建部署的 SpringBoot 应用的环境,都需要在全局工具中配置好。包括;Maven、JDK、Git、Docker。注意这里的环境路径配置,如果配置了是会提示你没有对应的路径文件夹。


4. 添加凭证


地址:http://localhost:9090/manage/credentials/store/system/domain/_/






  • 配置了Git仓库的连接凭证,才能从Git仓库拉取代码。

  • 如果你还需要操作如 ssh 也需要配置凭证。


三、新建任务


一个任务就是一条构建发布部署项目的操作。


1. 配置任务





xfg-dev-tech-jenkins

2. 配置Git





# 你可以 fork 这个项目,到自己的仓库进行使用
https://gitcode.net/KnowledgePlanet/ddd-scene-solution/xfg-dev-tech-content-moderation.git

3. 配置Maven






  • 在高级中设置 Maven 配置的路径 /usr/local/maven/conf/settings.xml。这样才能走自己配置的阿里云镜像仓库。


clean install -Dmaven.test.skip=true

3. 配置Shell


# 先删除之前的容器和镜像文件
if [ "$(docker ps -a | grep xfg-dev-tech-app)" ]; then
docker stop xfg-dev-tech-app
docker rm xfg-dev-tech-app
fi
if [ "$(docker images -q xfg-dev-tech-app)" ]; then
docker rmi xfg-dev-tech-app
fi

#
重新生成
cd /var/jenkins_home/workspace/xfg-dev-tech-jenkins/xfg-dev-tech-app
docker build -t xiaofuge/xfg-dev-tech-app .
docker run -itd -p 8091:8091 --name xfg-dev-tech-app xiaofuge/xfg-dev-tech-app





  • 当你熟悉后还可以活学活用,比如这里只是做build 但不做run执行操作。具体的部署可以通过 docker compose 执行部署脚本。

  • 另外如果你有发布镜像的诉求,也可以在这里操作。


四、测试验证


1. 工程准备


工程https://gitcode.net/KnowledgePlanet/road-map/xfg-dev-tech-jenkins 你可以fork到自己的仓库进行使用,你的账号密码就是 CSDN 的账号密码。


@SpringBootApplication
@RestController()
@RequestMapping("/api/")
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class);
}

/**
* http://localhost:8091/api/test
*/

@RequestMapping(value = "/test", method = RequestMethod.GET)
public ResponseBodyEmitter test(HttpServletResponse response) {
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");

ResponseBodyEmitter emitter = new ResponseBodyEmitter();

String[] words = new String[]{"嗨,臭宝。\r\n", "恭喜💐 ", "你的", " Jenkins ", " 部", "署", "测", "试", "成", "功", "了啦🌶!"};
new Thread(() -> {
for (String word : words) {
try {
emitter.send(word);
Thread.sleep(250);
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();

return emitter;
}

}


2. CI&CD - 构建发布


地址http://localhost:9090/job/xfg-dev-tech-jenkins/






  • 点击构建项目,最终会完成构建和部署成功。运行到这代表你全部操作完成了。


3. 验证结果


地址http://localhost:9000/#!/2/docker/containers





访问http://localhost:8091/api/test






  • 运行到这代表着你已经完整的走完了 Jenkins CI&CD 流程。


作者:小傅哥
来源:juejin.cn/post/7329573732597710874
收起阅读 »

https 协议是安全传输,为啥还要再加密?

背景这几天,我们家娃吃奶粉的量嗷嗷的涨起来了。我这颗小鹿乱撞的心,忍不住去面了两家互联网金融公司。因为没有准备,结果你懂的~这两家共同都有一个共同点,特别关系安全问题,尤其是看到你用过 okhttp3,那不得给你撸脱毛了就不算完事儿。协议HTTP vs HTT...
继续阅读 »

背景

这几天,我们家娃吃奶粉的量嗷嗷的涨起来了。我这颗小鹿乱撞的心,忍不住去面了两家互联网金融公司。
因为没有准备,结果你懂的~
这两家共同都有一个共同点,特别关系安全问题,尤其是看到你用过 okhttp3,那不得给你撸脱毛了就不算完事儿。

协议

HTTP vs HTTPS

我们都知道,https 相比于之前的 http 多了一层, 如下:

image.png
HTTP是一个基于TCP/IP通信协议来传递数据的协议,TCP/IP通信协议只有四层,从上往下依次为:应用层、传输层、网络层、数据链路层这四层,大学课本上的计算机网络知识是不是来了。但是,HTTP 协议在在网上进行传输用的是明文,就像某鸟给你发的快递,你的手机号、姓名都是写的清清楚楚,用户看着都瑟瑟发抖。
后来京东和顺丰意识到了这一点,就对手机号中间四位做了加密处理,姓名中间那几个个字也看不到了,甚至快递小哥给你打电话都是虚拟号码,你自己的电话只有自己心里清楚。
HTTPS 也是这个路子,为了解决 HTTP 明文传输存在的安全问题,在应用层和传输层之间加了一层安全层:SSL/TLS。
SSL: Secure Socket Layer, 安全套接层
TLS: Transport Layer Security,传输层安全协议
关于 HTTP 和 HTTPS 的对比文章,知乎上有一大坨,这里贴几篇,喜欢的可以多撸几遍:

HTTPS优缺点

  1. 使用 HTTP 明文传输存在被窃听风险,HTTPS 通过内容加密解决,注意这一步用的是对称加密哦
  2. 使用 HTTP 明文传输存在通信内容被篡改风险,HTTPS 通过校验机制解决
  3. 使用 HTTP 明文传输存在冒充服务风险,HTTPS 通过身-份-证书认证机制解决

通过以上三点,我们知道HTTPS 相对于 HTTP 有很强的安全感,但是对于一些app 这种安全级别还不够,如果出现以下意外咋整?

我干脆去问了讯飞星火,他是这么回答我的:

讯飞星火

我又去问了 ChatGPT,他的回答是这样的

ChatGPT

这俩货说的差不多,HTTPS 只能保证传输过程的安全性,不能保证全链路的安全性,更不能满足特定安全标准的规范诉求,所以我们需要根据安全的级别要求,采取不同加密策略。

既然如此,我们使用的三方网络库这里特指 okhttp 是怎么全链路传输安全的?我们接着往下唠:

OKhttp 库

Okhttp 库是基于HTTP和HTTPS协议封装的网络请求库,用于 Android 和 Java 应用中的网络通信。库在这儿:github.com/square/okht… ,

我们可以先了解一下okhttp 库是如何保证传输安全的~

问:OKhttp是如何支持https协议,以保证传输安全的,尤其是如何进行防止侦听、篡改、拦截信息?

AI 回答:
OkHttp是一个流行的开源HTTP客户端库,由Square公司开发。它提供了对HTTP和HTTPS的支持,并通过内置的TLS(Transport Layer Security)来确保HTTPS连接的安全性。以下是OkHttp如何支持HTTPS协议以及保障传输安全的一些关键方面:

  1. TLS/SSL支持:

    • OkHttp使用Java的javax.net.ssl包来支持TLS/SSL协议。TLS是HTTP的安全版本,用于加密通信内容,防止中间人攻击、窃听和篡改。
    • OkHttp通过SSLSocketFactoryHostnameVerifier等类来处理TLS/SSL握手和验证服务器证书的过程。
  2. 证书验证:

    • OkHttp会验证服务器提供的SSL证书,确保通信双方的身份合法。如果服务器的证书无效或无法验证,OkHttp会拒绝连接,从而防止中间人攻击。
  3. 证书锁定(Certificate Pinning):

    • OkHttp支持证书锁定机制,即在应用中预先指定服务端的证书信息。这样可以防止使用伪造证书的攻击,即使系统受到信任的根证书被破坏,应用仍然可以保持安全。
  4. 连接池和持久连接:

    • OkHttp通过连接池(Connection Pool)来重用已建立的连接,从而减少握手次数,提高性能。持久连接也有助于减少建立连接时的潜在风险。
  5. 安全配置:

    • OkHttp允许开发者配置TLS版本、密码套件(Cipher Suites)等参数,以满足特定的安全要求。
  6. 拦截器(Interceptors):

    • OkHttp提供了拦截器机制,可以在HTTP请求和响应的过程中插入自定义的逻辑。这使得开发者可以在请求或响应阶段添加额外的安全检查或处理逻辑,从而增强安全性。

总的来说,OkHttp通过支持TLS/SSL、证书验证、证书锁定、连接池等安全机制,以及提供灵活的配置和拦截器功能,确保在HTTPS连接中防止侦听、篡改和拦截信息的风险。开发者在使用OkHttp时,应该合理配置相关参数,以满足应用的安全需求。

好家伙,okhttp 是支持合理配置安全参数的,而且其拦截器机制可以很方便的进行安全检查,那还等什么,走一波~

策略一:对称加密(加密内容)

这里主打一个,客户端不想让别人看到自己的传输内容,想当然对自己的请求内容进行加密处理。基于这种思路我们封装一个 EncryptedOkHttpClient,代码如下:

public static OkHttpClient createEncryptedOkHttpClient() {
// 创建一个OkHttpClient.Builder
OkHttpClient.Builder builder = new OkHttpClient.Builder();

// 添加自定义拦截器,用于加密请求内容
builder.addInterceptor(new EncryptionInterceptor());

// 创建OkHttpClient实例
return builder.build();
}

注释里已经写了,通过EncryptionInterceptor拦截器对请求进行加密处理,这里选择加密请求体 RequestBody image.png 在encryptRequestBody方法中,RequestBody 依赖 okio 的 Buffer 类转换为ByteArray用于加密,加密算法选择对称加密算法 AES 加密字节数据,实现如下:

private RequestBody encryptRequestBody(RequestBody originalRequestBody) throws IOException {
// 从原始RequestBody中读取字节数据
// Read the byte data from the original RequestBody using Okio
Buffer buffer = new Buffer();
originalRequestBody.writeTo(buffer);
byte[] bytes = buffer.readByteArray();

// 使用对称加密算法(AES)加密字节数据
byte[] encryptedBytes = encryptWithAES(bytes, SECRET_KEY);

// 创建新的RequestBody
return RequestBody.create(originalRequestBody.contentType(), encryptedBytes);
}

可以看到,AES 使用了encryptWithAES方法加密字节数据,同时传了SECRET_KEY这个密钥,那我们看看 AES 是怎么加密的:

private byte[] encryptWithAES(byte[] input, String key) {
try {
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(input);
} catch (Exception e) {
e.printStackTrace();
return new byte[0];
}
}

四行代码搞定,首先通过SecretKeySpec类将SECRET_KEY字符串加密成 SecretKey 对象,然后Cipher以加密模式 对密钥进行初始化然后加密 input 也就是转换为字节数组的请求体。 加密完成了,服务器当然要进行解密,解密方法如下:

public static String decrypt(String encryptedText) {
try {
byte[] encryptedData = Base64.decode(encryptedText,Base64.DEFAULT);

SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);

byte[] decryptedBytes = cipher.doFinal(encryptedData);

return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

可以看到,解密过程使用了相同AES算法和密钥SECRET_KEY,这就是对称加密使用一把钥匙上锁和开锁。但是这种加密算法有很大的问题:

首先,这把钥匙如果想通过网络传输让服务端知道,传输过程中被劫持了密钥就会暴露。

另外,SECRET_KEY是硬编码在代码中的,这也不安全,这可咋整啊?

千里之堤,溃于hacker

为了防止这种中间人攻击的问题,非对称加密开始表演了~

策略二:非对称加密

非对称加密是一把锁两把钥匙:公钥和私钥。前者是给大家伙用的,谁都能够来配一把公钥进行数据加密,但是要对加密数据进行解密,只能使用私钥。

假设我们用公钥加密一份数据,就不怕拦截了。因为只有拿着私钥的服务端才能解密数据,我们拿着服务器提供的公钥把策略一中的对称密钥给加密了,那不就解决了网络传输密钥的问题了。对的,HTTPS 也是这么做的,按照这个思路我们再添加一个 MixtureEncryptionInterceptor 拦截器。

// 添加自定义拦截器,用服务器非对称加密的公钥加密对称加密的密钥,然后用对称加密密钥加密请求内容
builder.addInterceptor(new MixtureEncryptionInterceptor());

MixtureEncryptionInterceptor 拦截器同样实现 Interceptor 接口如下:

image.png

其 intercept 方法跟 EncryptionInterceptor 一模一样,具体的变化在 encryptRequestBody() 方法中。具体实现如下:

private RequestBody encryptRequestBody(RequestBody originalRequestBody) throws IOException {
// 生成对称加密的密钥
byte[] secretKeyBytes = generateSecretKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "AES");
// 使用服务器的公钥加密对称加密的密钥
byte[] encryptedSecretKey = encryptWithPublicKey(secretKeyBytes, SERVER_PUBLIC_KEY);
// 从原始 RequestBody 中读取字节数据
Buffer buffer = new Buffer();
originalRequestBody.writeTo(buffer);
byte[] bytes = buffer.readByteArray();

// 使用对称加密算法(AES)加密请求体
byte[] encryptedRequestBodyBytes = encryptWithAES(bytes, secretKeySpec);

// 创建新的 RequestBody,将加密后的密钥和请求体一并传输
return RequestBody.create(null, concatenateArrays(encryptedSecretKey, encryptedRequestBodyBytes));
}

如代码中注释,整个混合加密共 4 个步骤,依次是:

  1. 生成对称加密的密钥,用来加密传输内容。代码如下:
/**
* try block 里使用的是加密算法和随机数生成器,生成的较为复杂的密钥
* catch block 里使用的是示范性的非安全密钥
* @return
*/

private byte[] generateSecretKey() {
// 生成对称加密的密钥
try {
// 创建KeyGenerator对象,指定使用AES算法
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");

// 初始化KeyGenerator对象,设置密钥长度为128位
keyGenerator.init(128, new SecureRandom());

// 生成密钥
SecretKey secretKey = keyGenerator.generateKey();

// 获取密钥的字节数组表示形式
byte[] keyBytes = secretKey.getEncoded();

// 打印密钥的字节数组表示形式
for (byte b : keyBytes) {
Log.d(TAG,b + " ");
}
return keyBytes;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
// 这里简单地示范了生成密钥的过程,实际上可以使用更复杂的方法来生成密钥
return "YourSecretKey".getBytes(StandardCharsets.UTF_8);
}

}

如注释所言,上面try block 里使用的是加密算法和随机数生成器,生成的较为复杂的密钥,catch block 里使用的是示范性的非安全密钥。这里主要是想说明生成对称密钥的方式有很多,但是硬编码生成密钥那是不推荐的,因为太不安全了,很容易被恶意用户获取到。

  1. 使用服务器的公钥加密对称加密的密钥,防止被破解
private byte[] encryptWithPublicKey(byte[] input, String publicKeyString) {
try {
// 封装 PublicKey
byte[] keyBytes = Base64.decode(publicKeyString, Base64.DEFAULT);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);

Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);

return cipher.doFinal(input);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException |
InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
e.printStackTrace();
return new byte[0];
}
}

将服务端提供的公钥字符串转化成字节数组,然后通过 RSA 非对称算法加密 input,也就是我们的对称密钥。

注意:Cipher.getInstance("RSA/ECB/PKCS1Padding") 表示获取一个Cipher对象,该对象使用RSA算法、ECB模式和PKCS1填充方式。

  1. 使用对称加密算法(AES)加密请求体,请求体仍然要用对称加密密钥加密,只是对称加密密钥用公钥保护起来
private byte[] encryptWithAES(byte[] input, SecretKeySpec secretKeySpec) {
try {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
return cipher.doFinal(input);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
BadPaddingException | IllegalBlockSizeException e) {
e.printStackTrace();
return new byte[0];
}

}

非对称加密加密内容,策略一已经实现了。

  1. 创建新的 RequestBody,将加密后的密钥和请求体一并传输,这样就算 hacker 拦截了请求解析出请求体的数据,也无法直接获取到原始对称密钥。 加密完成后,通过 concatenateArrays 方法将加密后的密钥和请求体,实现如下:
private byte[] concatenateArrays(byte[] a, byte[] b) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
outputStream.write(a);
outputStream.write(b);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return outputStream.toByteArray();
}

非对称加密解决了密钥网络传输的问题,但是 hacker 还是可以伪装成目标服务端,骗取客户端的密钥。在伪装成客户端,用服务端的公钥加密自己篡改的内容,目标服务端对此无法辨别真伪。这就需要证书校验。

策略三:证书校验(单向认证)

okhttp3 提供了CertificatePinner这个类用于证书校验,CertificatePinner 可以验证服务器返回的证书是否是预期的证书。在创建createEncryptedOkHttpClient()方法中,添加证书代码如下:

image.png

okhttp 会利用其内置的证书固定机制来校验服务器返回证书的有效性。如果证书匹配,请求会继续进行;如果不匹配,OkHttp会抛出一个异常,通常是一个SSLPeerUnverifiedException,表明证书验证失败。验证过程在CertificatePinner 类的check()方法中,如下:

/**
* Confirms that at least one of the certificates pinned for {@code hostname} is in {@code
* peerCertificates}. Does nothing if there are no certificates pinned for {@code hostname}.
* OkHttp calls this after a successful TLS handshake, but before the connection is used.
*
* @throws SSLPeerUnverifiedException if {@code peerCertificates} don't match the certificates
* pinned for {@code hostname}.
*/

public void check(String hostname, List peerCertificates)
throws SSLPeerUnverifiedException {
List pins = findMatchingPins(hostname);
if (pins.isEmpty()) return;

if (certificateChainCleaner != null) {
peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
}

for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);

// Lazily compute the hashes for each certificate.
ByteString sha1 = null;
ByteString sha256 = null;

for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
if (pin.hashAlgorithm.equals("sha256/")) {
if (sha256 == null) sha256 = sha256(x509Certificate);
if (pin.hash.equals(sha256)) return; // Success!
} else if (pin.hashAlgorithm.equals("sha1/")) {
if (sha1 == null) sha1 = sha1(x509Certificate);
if (pin.hash.equals(sha1)) return; // Success!
} else {
throw new AssertionError();
}
}
}

// If we couldn't find a matching pin, format a nice exception.
StringBuilder message = new StringBuilder()
.append("Certificate pinning failure!")
.append("\n Peer certificate chain:");
for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
message.append("\n ").append(pin(x509Certificate))
.append(": ").append(x509Certificate.getSubjectDN().getName());
}
message.append("\n Pinned certificates for ").append(hostname).append(":");
for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
message.append("\n ").append(pin);
}
throw new SSLPeerUnverifiedException(message.toString());
}

从校验方法中得知,

  1. 可以没有固定证书
  2. 证书加密使用sha256/sha1
  3. 证书校验失败会抛出AssertionError错误
  4. 获取不到匹配的固定证书,会抛异常SSLPeerUnverifiedException

可以看到,使用相当方便。但是它有一个问题:请求之前需要预先知道服务端证书的 hash 值。就是说如果证书到期需要更换,老版本的应用就无法获取到更新的证书 hash 值了,老用户要统一升级。这~~~

策略四:创建SSLContext认证(客户端、服务端双向认证)

除了固定证书校验,还有一种基于 SSLContext 的校验方式。在建立HTTPS连接时,在客户端它依赖 SSLContext 和 TrustManager 来验证服务端证书。这里我们通过一createTwoWayAuthClient()方法实现如下:

private static OkHttpClient createTwoWayAuthClient() throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
// 服务器证书
InputStream serverCertStream = TwoWayAuthHttpClient.class.getResourceAsStream("/server_certificate.crt");
X509Certificate serverCertificate = readCertificate(serverCertStream);
if (serverCertStream != null) {
serverCertStream.close();
}

// 客户端证书和私钥
InputStream clientCertStream = TwoWayAuthHttpClient.class.getResourceAsStream("/client_centificate.p12");
KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
clientKeyStore.load(clientCertStream, "client_password".toCharArray());
if (clientCertStream != null) {
clientCertStream.close();
}

// 创建 KeyManagerFactory 和 TrustManagerFactory
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, "client_password".toCharArray());

// 创建信任管理器,信任服务器证书
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
trustStore.setCertificateEntry("server", serverCertificate);
trustManagerFactory.init(trustStore);

// 初始化 SSL 上下文
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());

// 创建 OkHttpClient
return new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagerFactory.getTrustManagers()[0])
.build();
}

private static X509Certificate readCertificate(InputStream inputStream) throws CertificateException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
return (X509Certificate) certificateFactory.generateCertificate(inputStream);
}
  1. 加载服务器证书

    • 使用getResourceAsStream从类路径中加载服务器证书文件(.crt格式)。
    • 通过readCertificate方法读取证书内容,并生成一个X509Certificate对象。
    • 关闭输入流以释放资源。

注意:/server_certificate.crt可以动态加载服务器自签名证书的办法避免更新旧版本应用

  1. 加载客户端证书和私钥

    • 同样使用getResourceAsStream从类路径中加载客户端证书和私钥文件(.p12格式,通常是PKCS#12格式的密钥库)。
    • 创建一个KeyStore实例,并使用PKCS12算法加载客户端证书和私钥。密码为"client_password"
    • 关闭输入流。
  2. 创建KeyManagerFactory和TrustManagerFactory

    • KeyManagerFactory用于管理客户端的私钥和证书,以便在建立SSL/TLS连接时使用。
    • TrustManagerFactory用于管理信任的证书,以便在建立SSL/TLS连接时验证服务器的证书。
    • 使用默认算法初始化这两个工厂,并分别加载客户端的密钥库和信任的服务器证书。
  3. 初始化SSLContext

    • 创建一个SSLContext实例,指定使用TLS协议。
    • 使用之前创建的KeyManagerFactoryTrustManagerFactory初始化SSLContext。这会将客户端的私钥和证书,以及信任的服务器证书整合到SSL/TLS握手过程中。
  4. 创建OkHttpClient

    • 使用OkHttpClient.Builder创建一个新的OkHttpClient实例。
    • 配置SSL套接字工厂和信任管理器,以确保在建立连接时使用两向认证。
    • 构建并返回配置好的OkHttpClient实例。

这样客户端发起请求时,会将客户端证书发送给服务端,同时会校验服务端握手时返回的证书。校验逻辑如下:

image.png

这样整个双向校验工作就完成了。

封装

腾讯云有个同学封装了库,主要给服务端使用的,看的挺有味道,可以参考 cloud.tencent.com/developer/a…

总结

okhttp 作为一个支持 HTTPS 协议的网络库,同时支持对称加密非对称加密客户端证书校验客户端、服务端双向证书校验等安全加密方式,足见其强大的功能。

此外,为了兼顾性能:它使用证书校验保证通信双方的合法性,使用对称加密加密传输内容保证性能,使用非对称加密加密对称密钥防止hacker 拦截,整体提高了网络通信的安全性。

FAQ

文章被郭霖老师转发后,同学们也提出了一些疑问:
Q: HTTPS为啥不能保证全链路安全?

  1. 端点安全性: 如果你的手机、电脑、服务器中毒了,不管输入啥私密信息,都会被病毒软件截胡,https 管不了这事儿。需要杀毒软件大显身手了,给腾讯手机管家做个广告~
  2. 中间人攻击: hacker 通过非法方式获得 CA 证书,满足了 https 的安全策略,可以与客户端通信。okhttp 可以通过证书锁定(Certificate Pinning)的方式,只跟特定的服务器通讯,自签名证书不通过,就算 hacker 黑了 CA 机构你也不怕
  3. 协议漏洞:okhttp 团队也会定期更新修复漏洞,所以版本该升级升级

Q: SSLContext如何动态更新证书

其实这个问题的关键还是不理解 your_certificate.crt 下载过程中被攻击了咋办。首先,第一版应用的证书秘密存储。其次,后期更新的过程中,下载链路是安全的,自动替换最新的证书并通过安全校验就 OL

Q:PKCS1 有安全问题,建议使用 OAEP

import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

public class RSAUtil {
public static byte[] encrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}

public static byte[] decrypt(byte[] encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedData);
}

public static PublicKey getPublicKey(byte[] publicKeyBytes) throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}

public static PrivateKey getPrivateKey(byte[] privateKeyBytes) throws Exception {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
}

Q:证书固定问题

Certificate Pinning 涉及到涉及到三层证书:根证书(Root Certificate)、中间证书(Intermediate Certificate)和服务器证书(Server Certificate)。每个证书都有自己的公钥,因此在证书固定中需要验证这三个证书的公钥。 具体做法是,将服务器证书和根证书的 hash 值添加到证书固定中,这样,在建立连接时,除了验证服务器证书的公钥外,还会验证中间证书和根证书的公钥,确保整个证书链的完整性和真实。

这里以 example.com为例:

import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class CertificatePinningExample {

public static void main(String[] args) throws Exception {
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // 服务器证书的哈希值
.add("example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 根证书的哈希值
.build())
.build();

Request request = new Request.Builder()
.url("https://example.com")
.build();

try (Response response = client.newCall(request).execute()) {
System.out.println(response.body().string());
}
}
}

AI 是个好东西~

参考文章


作者:hongyi0609
来源:juejin.cn/post/7333162360360796171
收起阅读 »

移动端APP版本治理

1、背景 在许多公司,APP版本都是不受重视的,产品忙着借鉴,开发埋头编码,测试想着不粘锅。 只有在用户反馈app不能用的时候,你回复客服说,让用户升级最新版本,是不是很真实。 而且业界也很少有听说版本治理的,但其实需求上线并不是终点,在用户数据回传之前,这中...
继续阅读 »

1、背景


在许多公司,APP版本都是不受重视的,产品忙着借鉴,开发埋头编码,测试想着不粘锅。


只有在用户反馈app不能用的时候,你回复客服说,让用户升级最新版本,是不是很真实。


而且业界也很少有听说版本治理的,但其实需求上线并不是终点,在用户数据回传之前,这中间还有一个更新升级的空档期,多数公司在这里都是一个“三不管”地带,而这个空档期,我称之为版本交付的最后一公里



2、价值



2.1、业务侧


总有人会挑战“有什么业务价值?”对吧,那就先从业务价值来看。


尽管有些app的有些业务是动态发布的,但也一定会有些功能是依赖跟版的,也就是说,你没办法让所有用户都用上你的新功能,那对于产运团队来说,业务指标就还有提升的空间。


举两个例子:



  1. 饿了么免单活动需要8.+版本以上的app用户才能参与,现在参与用户占比80%,治理一波后,免单用户参与占比提升到90%,对业务来说,免单数没变,但是订单量却是有实实在在的提升的。

  2. 再来一个,酷狗音乐8.+的app用户才可以使用扫码登录,app低版本治理之后,扫码登录的用户占比势必也会提升,那相应的,登录成功率也可以提升,登录流程耗时也会缩短,这都是实实在在的指标提升。



虚拟数据,不具备真实参考性。



2.2、技术侧


说完业务看技术,在技术侧也可以分为三个维度来看:



  1. 稳定性,老版本的crash、anr之类的问题在新版本大概率是修复了的,疑难杂症可能久一点;

  2. 性能优化,比如启动、包大小、内存,可以预见是比老版本表现更好的,个别指标一两个版本可能会有微量劣化,但是一直开倒车的公司早晚会倒闭;

  3. 安全合规,不管是老的app版本还是老的服务接口,都可能会存在安全问题,那么黑产就可能抓住这个漏洞从而对我们服务的稳定性造成隐患,甚至产生资损;


2.3、其他方面


除了上面提到的业务指标和用户体验之外,还有没有?


有没有想过,老版本的用户升上来之后,那些兼容老版本的接口、系统服务等,是不是可以下线了,除了减少人力维护成本之外,还能减少机器成本啊,这也都是实打实的经费支出。


对于项目本身来说,也可以去掉一些无用代码,减少项目复杂度,提升健壮性、可维护性。


3、方案



3.1、升级交互


采用新的设计语言和新的交互方式。


3.1.1、弹窗样式


样式上要符合app整体的风格,信息展示明确,主次分明。


bad casegood case

3.1.2、操作表达


按钮的样式要凸显出来,并放在常规易操作的位置上。


bad casegood case

3.1.3、提醒链路


从一级菜单到二级页面的更新提醒链路,并保持统一。



3.1.4、进度感知


下载进度一定要可查看并准确,如果点了按钮之后什么提示都没有,用户会进入一个很迷茫的状态,体验很差。



3.2、提醒策略


我们需要针对不同的用户下发不同的提醒策略,这种更细致的划分,不光是为了稳定性和目标的达成,也是为了更好的用户体验,毕竟反复提醒容易引起用户的反感。


3.2.1、提醒时机


提醒时机其实是有讲究的,原则上是不能阻塞用户的行为。


特别是有强制行为的情况,比如强更,肯定不能在app启动就无脑拉起弹窗。



bad case:双十一那天,用户正争分夺秒准备下单呢,结果你让人升级,这不扯淡的吗。



时机的考虑上有两个维度:



  1. 平台:峰时谷时;

  2. 用户:闲时忙时;


3.2.2、逻辑引擎


为什么需要逻辑引擎呢?逻辑引擎的好处是跨端,双端逻辑可以保持一致,也可以动态下发。




可以使用接口平替,约定好协议即可。



3.2.3、软强更


强制更新虽然有可以预见的好效果,但是也伴随着投诉风险,要做好风险管控。


而在2023-02-27日,工信部更是发布了《关于进一步提升移动互联网应用服务能力》的通知,其中第四条更是明确表示“窗口关闭用户可选”,所以强更弹窗并不是我们的最佳选择。



虽然强更不可取,但是我们还可以提高用户操作的费力度,在取消按钮上增加倒计时,再根据低版本用户的分层,来配置不同的倒计时梯度,比如5s、10s、20s这样。


用户一样可以选择取消,但是却要等待一会,所以称之为软强更



3.2.4、策略字段



  • 标题

  • 内容

  • 最新版本号

  • 取消倒计时时长

  • 是否提醒

  • 是否强更

  • 双端最低支持的系统版本

  • 最大提醒次数

  • 未更新最大版本间隔

  • 等等


3.3、提示文案


升级文案属于是ROI很高的了,只需要总结一下新版本带来了哪些新功能、有哪些提升,然后配置一下就好了,但是对用户来说,却是实打实的信息冲击,他们可以明确的感知到新版本中的更新,对升级意愿会有非常大的提升。



虽然说roi很高,但是也要花点心思,特别是大的团队,需要多方配合。


首先是需求要有精简的价值点,然后运营同学整合所有的需求价值点,根据优先级,出一套面向用户的提醒话术,也就是提示的升级文案了。



3.4、更新渠道


iOS用户一般在App Store更新应用,但是对于Android来说,厂商比较多,对应的渠道也多,还有一些三方的,这些碎片化的渠道自然就把用户人群给分流了,为了让每一个用户都有渠道可以更新到最新版本,那就需要在渠道的运营上下点功夫了,尽可能的多覆盖。



除了拓宽更新渠道之外,在应用市场的介绍也要及时更新。


还有一点细节是,优化应用市场的搜索关键词。


最后,别忘了自有渠道-官网。


3.5、触达投放


如果我们做了上面这么多,还是有老版本的用户不愿意升级怎么办?我们还有哪些方式可以告诉他需要升级?


触达投放是一个很好的方式,就像游戏里的公告、全局喇叭一样,但是像发短信这种需要预算的方式一般都是放在最后使用的,尽可能的控制成本。




避免过度打扰,做好人群细分,控制好成本。



3.6、其他方案


类型介绍效果
更新引导更新升级操作的新手引导😀
操作手册描述整个升级流程,可以放在帮助页面,也可以放在联系客服的智能推荐🙂
营销策略升级给会员体验、优惠券之类的😀
内卷策略您获得了珍贵的内测机会,您使用的版本将领先同行98%😆
选择策略「88%的用户选择升级」,替用户做选择😆
心理策略「预计下载需要15秒」,给用户心理预期😀
接口拦截在使用某个功能或者登录的时候拦截,引导下载最新版本😆
自动下载设置里面加个开关,wifi环境下自动下载安装包😀
版本故事类似于微信,每个版本都有一个功能介绍的东西,点击跳转加上新手引导🙂

好的例子:


淘宝拼多多

4、长效治理


制定流程SOP,形成一个完整链路的组合拳打法🐶。


白话讲,就是每当一个新版本发布的时候,明确在每个时间段要做什么事,达成什么样的目标。


让更多需要人工干预的环节,都变成流程化,自动化。



5、最后


一张图总结APP版本治理依次递进的动作和策略。



作者:yechaoa
来源:juejin.cn/post/7299799127625170955
收起阅读 »

如何将用户输入的名称转成艺术字体-fontmin.js

web
写在开头 日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下: <template> <div class="font">橙某人</div> </template...
继续阅读 »

写在开头


日常我们在页面中使用特殊字体,一般操作都是直接由前端来全量引入设计师提供的整个字体包即可,具体操作如下:


<template>
<div class="font">橙某人</div>
</template>

<style scoped>
@font-face {
font-family: "orange";
src: url("./orange.ttf");
}
.font {
font-family: "orange";
}
</style>


很简单吧🤡,但有时应用场景不同,可能需要我们考虑一下性能问题。



一般来说,我们常见的字体包整个是非常大的,小的有几M到十几M,大的可能去到上百M都有,特别是中文类的字体包会相对英文类的要更大一些。



如本文案例,我们仅需在用户输入完后加载对应的字体包即可,这样能避免性能的损耗。


为此,我们需要把整个字体包拆分、细致化、子集化,让它能达到按需引入的效果。


那么这要如何来做这个事情呢?这个方案单单前端可做不了,我们需要配合后端一起,下面就来看看具体的实现过程吧。😗


前端


前端小编用 Vue 来编写,具体如下:


<template>
<div>
<input v-model="name" />
<button @click="handleClick">生成</button>
<div v-if="showName" class="font">{{ showName }}</div>
</div>

</template>

<script>
export default {
data() {
return {
name: "",
showName: "",
};
},
methods: {
handleClick() {
// 创建link标签
const linkElement = document.createElement("link");
linkElement.setAttribute("rel", "stylesheet");
linkElement.setAttribute("type", "text/css");
linkElement.href = `http://localhost:3000?name=${encodeURIComponent(this.name)}`;
document.body.appendChild(linkElement);
// 优化显示效果
setTimeout(() => {
this.showName = this.name;
}, 300);
},
},
};
</script>


<style>
.font {
font-family: orange;
font-size: 50px;
}
</style>


应该都能看懂吧,主要就是生成了一个 <link /> 标签并插入到文档中,标签的请求地址指向我们服务端,至于服务端会返回什么你可以先猜一猜。👻


服务端


服务端小编选择用 Koa2 来编写,你也可以选择 Express 或者 Egg ,甚至 Node 也是可以的,差异不大,具体逻辑如下:


const koa = require("koa2");
const fs = require("fs");
const FontMin = require("fontmin");

const app = new koa();

/** @name 优化,缓存已经加载过的字体包进内存 **/
const fontFamilyMap = {};

/** @name 加载字体包 **/
function loadFontLibrary(fontPath, fontFamily) {
if (fontFamilyMap[fontFamily]) return fontFamilyMap[fontFamily];
return new Promise((resolve, reject) => {
fs.readFile(fontPath, (error, file) => {
if (error) {
reject(new Error(error.message));
} else {
fontFamilyMap[fontFamily] = file;
resolve(file);
}
});
});
}

app.use(async (ctx) => {
const { name } = ctx.query;
// 设置返回文件类型
ctx.set("Content-Type", "text/css");

const fontPath = "./font/orange.ttf";
const fontFamily = "orange";
if (!fs.existsSync(fontPath)) return (ctx.body = "字体包读取失败");

const fontMin = new FontMin();
const fontFile = await loadFontLibrary(fontPath, fontFamily);
fontMin.src(fontFile);

const getFontCSS = () => {
return new Promise((resolve) => {
fontMin
.use(FontMin.glyph({ text: name }))
.use(FontMin.css({ base64: true, fontFamily }))
.run((error, files) => {
if (error) {
console.log("error", error.message);
} else {
const fontContent = files?.[1]?.contents;
resolve(fontContent);
}
});
});
};

const fontCSS = await getFontCSS();

ctx.body = fontCSS;
});

app.listen(3000);

console.log("服务器开启: http://localhost:3000/");

我们主要是采用了 Fontmin 库来完成整个字体包的按需加载功能,这个库是第一个纯 JavaScript 字体子集化方案。



可能有后端是 Java 或者其他技术栈的小伙伴,你们也不用担心,据小编和公司后端同事了解,不同技术栈也是有对应的库可以解决的,需要的可以自行查查看。










至此,本篇文章就写完啦,撒花撒花。


image.png


希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。


作者:橙某人
来源:juejin.cn/post/7293151700869038099
收起阅读 »

用位运算维护状态码,同事直呼牛X!

位运算是一种非常高效的运算方式。在算法考察中比较常见,它使用位级别的操作来表示和控制状态,这在管理多个布尔标志或状态时尤其有用。那么业务代码中我们如何使用位运算呢? 位运算基础 我们先来回顾一下位运算的基础: 与(AND)运算:只有当两个位都是1时,结果才是...
继续阅读 »

位运算是一种非常高效的运算方式。在算法考察中比较常见,它使用位级别的操作来表示和控制状态,这在管理多个布尔标志或状态时尤其有用。那么业务代码中我们如何使用位运算呢?


位运算基础


我们先来回顾一下位运算的基础:



  • 与(AND)运算:只有当两个位都是1时,结果才是1(a & b)。

  • 或(OR)运算:如果两个位中至少有一个为1,那么结果就是1(a | b)。

  • 异或(XOR)运算:如果两个位不同,则结果为1(a ^ b)。

  • 非(NOT)运算:反转位的值(~a)。

  • 左移:将位向左移动,右侧填充0(a << b)。

  • 右移:将位向右移动,左侧填充0(a >> b)。


业务状态码应用


如何通过位运算维护业务状态码呢?我们可以在一个整数中存储多个布尔值,每个位代表一个不同的状态或标志。


让我们将上述课程状态的例子修改为管理订单状态的示例。假设一个订单有以下几种状态:已创建(Created)、已支付(Paid)、已发货(Shipped)、已完成(Completed)。


定义状态常量


我们首先定义这些状态作为常量,并为每个状态分配一个位:



  • 已创建(Created): 0001 (1)

  • 已支付(Paid): 0010 (2)

  • 已发货(Shipped): 0100 (4)

  • 已完成(Completed): 1000 (8)


Java 实现


接下来,我们在Java中实现一个OrderStatus类来管理这些状态:


public class OrderStatus {

    private static final int CREATED = 1;   // 0001
    private static final int PAID = 2;      // 0010
    private static final int SHIPPED = 4;   // 0100
    private static final int COMPLETED = 8// 1000

    private int status;

    public OrderStatus() {
        this.status = CREATED; // 默认状态为已创建
    }

    // 添加状态
    public void addStatus(int status) {
        this.status |= status;
    }

    // 移除状态
    public void removeStatus(int status) {
        this.status &= ~status;
    }

    // 检查是否有特定状态
    public boolean hasStatus(int status) {
        return (this.status & status) == status;
    }

    // 示例输出
    public static void main(String[] args) {
        OrderStatus orderStatus = new OrderStatus();

        System.out.println("-------订单已支付-----------");
        // 假设订单已支付
        orderStatus.addStatus(PAID);
        System.out.println("创建订单是否创建 " + orderStatus.hasStatus(CREATED));
        System.out.println("创建订单是否支付 " + orderStatus.hasStatus(PAID));

        // 假设订单已发货
        System.out.println("-------订单已发货-----------");
        orderStatus.addStatus(SHIPPED);
        System.out.println("创建订单是否发货 " + orderStatus.hasStatus(SHIPPED));

        // 假设订单已完成
        System.out.println("-------假设订单已完成-----------");
        orderStatus.addStatus(COMPLETED);
        System.out.println("创建订单是否完成 " + orderStatus.hasStatus(COMPLETED));
    }
}

运行结果:


截屏2024-03-06 12.09.07.png


在这个例子中,我们通过OrderStatus类使用位运算来管理订单的不同状态。这种方式允许订单在其生命周期中拥有多个状态,而且能够高效地检查、添加或删除这些状态。当订单状态变化时,我们只需要简单地调用相应的方法来更新状态。这样实现后相信同事肯定对你刮目的!


作者:半亩方塘立身
来源:juejin.cn/post/7343138804482408448
收起阅读 »

谁还没个靠bug才能正常运行的程序😌

web
最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^ 这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。 下面是演示代码和动图 <!DO...
继续阅读 »

最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^


这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。


下面是演示代码和动图


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
width: 300px;
max-height: 300px;
background-color: black;
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%);
overflow-y: auto;
}
.child {
width: 260px;
height: 600px;
margin: 0px 20px;
background-color: pink;
position: relative;
}
.flag {
position: absolute;
width: 100%;
height: 25px;
background-color: blueviolet;
color: aliceblue;
text-align: center;
line-height: 25px;
font-size: 14px;
left: 0;
right: 0;
}
.top {
top: 0;
}
.bottom {
bottom: 0px;
}
</style>
</head>

<body>
<div class="container">
<div class="child">
<div class="flag top">top</div>
<div class="flag bottom">bottom</div>
</div>
</div>
</body>
</html>


20230927105849_rec_.gif




开始计算啦,公式:滚动比例 = 滚动距离 / 可滚动距离


滚动距离: $0.scrollTop


可滚动距离: $0.scrollHeight - $0.offsetHeight


即:scrollRatio = scrollTop / (scrollHeight - offsetHeight)


滚动到底部,计算结果是 300 / (600 - 300) = 1


image.png


我们需要拿scrollRatio某界定值(比如0.1)作大小的比较,计算是true还是false(用isShow = scrollRatio < 某界定值来保存)。


这里一切正常。




不正常的情况出现了


就是没有出现滚动条的情况,即.child的高度没有超过.container的高度时,把.child的高度设成.containermax-height,就没有滚动条了(下面讲的情景也都是没有滚动条的情况)。


image.png


这个时候再去计算,得到了NaN,以至于 NaN < 0.1 = false


image.png


因为isShow的预期就是false,所以一直都没有发现这个bug。




那么它是如何暴露的呢?


后来新的需求给.container加了border。演示一下加border,然后再去计算:


image.png


发现没,这时候$0.offsetHeight的高度把border的高度也算进去了,结果就成了true,这不是想要的结果 ❌。




然后就是一番查验


offsetHeight是一个元素的总高度,包括可见内容的高度、内边距(padding)、滚动条的高度(如果存在)以及边框(border)的高度。


而我们这里只需要可见的高度,就可以用到另一个属性了clientHeight


clientHeight是指元素的可见内容区域的高度,不包括滚动条的高度和边框的高度。它仅包括元素的内部空间,即内容加上内边距。


image.png


当然这也只是继续使除数为0,然后得到结果为NaN,不过bug已经暴露出来了,后面就是一些其他的优化啦~




总结 + 复习(盒模型 box-sizing)


发现没有,offsetHeightclientHeight的区别,就像盒模型中的标准盒模型怪异盒模型的区别:


box-sizing: content-box(默认,标准盒模型):宽度和高度的计算值都 不包含 内容的边框(border)和内边距(padding)。添加padding和border时, 使整个div的宽高变大。


box-sizing: border-box(怪异盒模型):宽度和高度的计算值都 包含 内容的边框(border)和内边距(padding)。添加padding和border时, 不会 使整个div的宽高变大。


这样讲是不是加深一下对这两种属性的印象


^ - ^


作者:aomyh
来源:juejin.cn/post/7283087306603823116
收起阅读 »

职场上的人情世故-初入新公司的第一天

写在前面 想必很多同学在步入新公司的时候,不仅仅带有激动的心情(毕竟大概率涨了点薪资)的同时,也会有点点紧张的心情,也没熟悉的人,也可能不太清楚要做的事儿。4年前刚步入目前所在公司时,也是面临着不少挑战,下面结合我的亲身经历,给大家分享下我是如何快速融入新公司...
继续阅读 »

写在前面


想必很多同学在步入新公司的时候,不仅仅带有激动的心情(毕竟大概率涨了点薪资)的同时,也会有点点紧张的心情,也没熟悉的人,也可能不太清楚要做的事儿。4年前刚步入目前所在公司时,也是面临着不少挑战,下面结合我的亲身经历,给大家分享下我是如何快速融入新公司,如何面对各种挑战。


入职第一天


办理好入职手续后,hr将我带到TL面前,安排好座位后,我尴尬地说了句:”大家好大家好“,更尴尬的是没一个人看向我,好在TL给我解了围:”会有时间给你做自我介绍的,我先发你个文档,搭建好开发环境“。


解锁了第一个任务:【搭建开发环境】


一小时后到了午餐的时间


这里画下重点了,不是每一个公司的同事都会主动拉上新同事吃午饭。这时候又被TL看出我的尴尬:”一会儿还有一批同事去吃饭,你跟他们一起去吧“。


入职新公司后,午餐要跟着同团队的同事一起,主动点跟团
平时有带饭习惯的也先别带了
午餐时间是最好地了解团队信息贺快速融入团队的好机会

下午搭建环境时,发现一处命令始终报错(时间太久实在想不起详情来了),查到正确的命令后找TL报告了下情况,更新了文档。


千万别一来就直接问
先自己思考一段时间
实在解决不了,也要带着自己查询资料后的结论去问下(不要管自己查的对不对,至少有解决问题的态度)

不知不觉时间来到了晚上6:45,下班时间已到,甚至还多加了15分钟班~~
环顾四周,没一个人有走的意思
好吧,接下来要说的我估计会被喷.... 但老夫该说还得说


如果是初创公司建议多留一会儿,毕竟刚来公司,尽快熟悉下公司情况
虽然有点那啥...,但摆一下态度还是能留下一些好印象的
毕竟在试用期,公司与自身是否合拍,还是早点看清楚好些,多花时间调研下
最不济,实在没事儿做,看看群里的同事名字,多记几个总是好的吧

晚上7点过群里发起了投票,问过后才得知加班到20:30可以点加班餐,群里直接投票就行。那还走啥,吃个饭再说。有一说一,这顿晚饭还是挺值的。可不是说省下多少多少晚餐费,而是跟其他同事边吃边聊,更深入的了解了下当前团队的人员配置、当前版本进度以及后续版本安排。对公司产品和研发配置有了更好的理解,这也侧面坚定了我想留下来的信念。


很多时候,吃饭和来根小快乐(不会的别学)的时候,最能拉进同事关系
闲聊的时候多问多了解,老同事在感到被你需要的时候,也很容易拉进同事关系

结束新公司的第一天


总的来说,来公司第一天的表现还是差点意思。事事不够主动,虽说上班的主要目的是挣银子,但对我来说舒适的上班环境和良好的同事氛围还是挺重要的。第一天比较顺利地度过了,公司规模也不小,后面要做什么版本功能也不得而知,还是有点忐忑,毕竟第一天早上TL就直接告知:”我们的试用期不会是常规的三个月,一共三个阶段,会给你安排导师带你,如果某个阶段不合格也会直接劝退“。


我想说,无论做什么版本功能,我相信始终能做出来,只是质量的区别
所以新环境中不用过于紧张,总有水到桥头自然直的时候

后续


试用期的几个月也相当不顺利,做功能的时候也相当痛苦,也有过好几次返工的时候。既然说是后续了,那就后面再细聊。


作者:snowlover
来源:juejin.cn/post/7342750487873257491
收起阅读 »

一年前端经验的我,开始带队开发了!

关于刚开始接触带队开发 时间:2023年3月5日 这篇文章不是工具型文章,单纯聊一聊我工作生活中的心得体会。经历是非常非常勇敢的行为和非常非常有意义的。很多书其实也是一个一个的经历和经历总结。经历总是迷人的。 前言 先简单介绍一下我自己。我是一名前端程序员...
继续阅读 »

关于刚开始接触带队开发


时间:2023年3月5日



这篇文章不是工具型文章,单纯聊一聊我工作生活中的心得体会。经历是非常非常勇敢的行为和非常非常有意义的。很多书其实也是一个一个的经历和经历总结。经历总是迷人的。



前言


先简单介绍一下我自己。我是一名前端程序员,2022年毕业的,但是当时我并没有立即进入程序员的行列。我首先参与了考研考公大军。不过正如我大学时的一位任课老师所说,我并不像个考研的人。我想补充一下,我或许也不是一个考公的人。



哈哈哈,不好意思,我这么久了还记得您这句话。只是印象深刻,因为我当时听到这句话的时候好像我的内心并不排斥,还隐隐觉得是对的🤣。不过老师应该看不到,她是教我们供应链管理的老师,她应该关注不到这儿。



考研我没过初试,考公我没过面试。可以说那段时日我是凄凄惨惨戚戚,严重精神内耗。在生活和内心的重压之下,我准备先到省会找一份工作,先自己养活自己。


22年11月15日,我深刻的记得这天,这是我回到IT的第一天,这天起,我开始了前端开发。我其实入职的是一家小公司,不算大,研发部一开始也就20来号人,所以总有人员不足的时候。特别是我手上的项目,是公司自研项目,但是公司投入的人不多,就我一个前端开发,外加部门经理直接领导我。于是乎我是前端,但不只是前端。


时至今日,我经历了不止前端开发。还有 electron 的桌面端开发、网络安全漏洞复现、漏洞收集分析、视频制作。这些是我在23年夏天开始经常出去做售前的基础。后来我一方面做售前技术支持,一方面收集需求,在经理的指导下做系统优化。后面还做过对产品部分功能模块的原型设计。这也是后来我第一次懵懵懂懂的开始带人一起开发的第一个功能。当时有两个人和我一起开发,我负责前端部分需求实现和 nodejs 后端功能实现,并做总开发进度监督汇报。


这一年的时间中,不乏各位同事对我热情的帮助、经理对我的容忍和耐心指导、还有老板对我的肯定。



1、遥想当初,“package.json”我都会说错🤣,不过那都是过去式。


2、有关的经历我会慢慢通过文字或者视频的形式输出,欢迎持续关注。



又是一次带队协作开发


1 和“队友”打配合的体会


今天我已经不是第一次带队开发了。但是我还是觉得我要把这次真正当成初次。为什么这么说?


这次的开发从调研到设计到整合人马,以及后续我都参与,并且我主要负责。相对而言,这次是更为完整的带队经验。而上一次原型是我画的,针对需求的技术方案调研我也是做过,但是当时我并没有以我是带头人的角度去思考问题,就是简单的,领导要我做啥我做啥,他提出需求,告诉我可以考虑用哪些去做,我就想办法去实现就好了。而这次不一样。我没有完全参与调研,因为这个时候我手上还有很多任务要做,不能完全把时间堆在上面。所以更多的我是让我的“队友”(其实也就只有一个,从公司结构来说她是我的下属)去做主要调研工作,我告诉她我需要她调研什么,结果以什么格式呈现给我。完成后我就她给我的信息做开发计划。



小体会:


要安排下属做一件事情的时候,告诉她“主体是什么”、“我需要的是什么”、“呈现形式是什么”,这将能带来更好的收益。


因为下属也是实习生刚转正,她其实还有很多在汇报或者其他工作上考虑不完善的地方。那这个时候就需要有这样的模式,能够让她快速知道自己要做什么,怎么做。她就不会胡乱给你信息,增大你信息检索的压力。我也明白了,为什么说领导更多时候就是在乎结果。因为直接使用结果对领导是更高效的。



2 开发计划书


开发计划我之前从未写过。我也好久没有一个 word 文档是自己从头到尾一个字一个字码出来的。我也更深刻的感受到了之前大学时期参与 “互联网+” 创新创业比赛中写项目计划书给我带来的思路优势了。我的开发计划书也更有了一点逻辑性。


从市场环境出发,到产品涉及的目标客户的需求共性,来作为开发计划的大背景,也是梳理需求的重要依据。再到需求点分析,需求特征分析。通过需求分析找到实现需求的重要做法。有句话说的好,问题的答案往往要从问题本身去寻找。(不用查了,这是我编的🤪)然后依据需求点要实现什么样的功能。最后是做前端的页面划分、功能描述、后端的数据库设计等。



小体会:


软考除了能帮我们减税、落户加分,还可以帮助我们有系统意识以及设计思想。


23年下半年我参与过一次软考中级考试,备考的经历不长,只有一个月,但是针对“软件设计师”的学习真的让我更好的理解了工作中的一些事情还有项目、电脑上的一些思想。有些计算机的思想甚至能帮助我在生活中拥有更为清晰的思路。



3 动工!小组会议


开会前我可担心了。因为产品所面对的是能懂开发的开发者,产品相对而言更有一些开发的专业性。本身我就是开发者,很容易就会去用我熟悉的词汇去讲,然而这未必是 UI 或者 测试 他们能接受的信息。


有之前和其他部门同事的交流以及和客户交流的经验,我非常明白这点的重要性,所以我本来可以在上午直接开完的会议我硬生生拖到了下午来开。为什么?我做了我能想到的各种情形的回答备案,以及整个会议的把控大纲。和UI我要说明什么,和测试我要强调什么。


但是我还是考虑不周😂。



  1. 对UI,我没有做到更为简单易懂的沟通。我的开发计划书中有对各个页面的功能描述,但是那只是文字。根据文字大家能想想到的情形是不一定相同的。后来我用纸笔给她边说边画,毫无疑问这是低效的,但在当时这是没办法的办法。会议结束前,UI 也告知我,如果没有原型或者草图的情况下,可以考虑给一些竞品的页面和她说,然后说自己需求,这样会更容易理解我想做什么事情。

  2. 我考虑了UI、测试,但是我没有考虑开发。因为我和我的“队友”是负责前后端开发的主要人员,我给她的定位就是前端开发,我习惯了,所以我没有想过会议上要和她说明她要做什么。以至于她会议结束的时候立马问我她要做什么。



当场我也立即意识到了自己疏忽了😂。考虑的还是不够周全。希望我今后不会再发生这样的事情。




小体会:


组织者真是个不好当的角色,他应该尽可能考虑周全,不管是在事务说明上要让所有成员都明白、还要在工作安排上有明确完整的分工,即使对方知道了也应该在会议上明确说明。


幸好我们都是一群年轻人,互相都很好理解。



4 向上汇报——我是怎么避免被压工时


通过小组会议,我不仅要对各个成员的工作做安排,同时也要对任务的工期要有把控。而这个工期的情况,也是需要向上汇报的,这是告诉他什么时候来验收。


这个时候也有一些学问。相信有很多同学被压过工时。我也被压过。我还有过因为自己过少的考虑到工作情况的复杂性,没有给自己留够时间,周末还免费加班过一次。(当时年轻,也是不懂事,不好意思去提加班)。


而这次要开发的内容我之前也没有开发过,而且其中存在很多变数。这个时候是非常难和领导谈判的。因为在领导看来,这些任务就是非常简单的。就比如这个场景:“这个需求,我分分钟就写完了,哪里还需要这么久?你这个时间我不接受,今天下班前一定要出来!”


大学的时候我也学过IT项目管理,我也知道要有工期预计,这也是对成本的估计。但是我这个时候真觉得是扯淡。因为没办法预估啊,太难了,我都不知道会有什么样奇奇怪怪的情况等着我。所以我这次学乖了,我要多估计一些时间,要给领导说清楚我需要这么多时间的原因(就是表明可能存在一些未知的困难,或者手头上的事情还有很多,强压我赶不过来等等)。


我也开始觉得我需要学一学怎么“讨价还价”了。



小体会:


要量力而行,稳妥行事,切忌冲动吹牛夸海口,学会“讨价还价”。





目前能总结出的就是这么多,我犯的错误希望大家可以引以为戒。另外,我也想更多的学习有关领导力的知识还有技巧,欢迎交流。




去经历,不去后悔。保持热爱,奔赴山海!


持续更新中 ... ...


作者:LisEcho
来源:juejin.cn/post/7342793007937552394
收起阅读 »

项目经理要求不能回退到项目以外的路由 , 简单解决 !

web
不知道小伙伴们有遇到过项目经理哪些奇奇怪怪的需求呢 ? 曾经在项目中遇到过这样一个需求 : 点击按钮回退时不能回退到项目以外的路由 ; 例如我们的用户正在访问我们的页面时 , 突然有访问某个有趣的网站的冲动 , 在浏览器输入了一串祖传网址 , 一番欣...
继续阅读 »

不知道小伙伴们有遇到过项目经理哪些奇奇怪怪的需求呢 ?


640 (2).png




  • 曾经在项目中遇到过这样一个需求 : 点击按钮回退时不能回退到项目以外的路由 ;

  • 例如我们的用户正在访问我们的页面时 , 突然有访问某个有趣的网站的冲动 , 在浏览器输入了一串祖传网址 , 一番欣赏后通过浏览器搜索栏回到我们的应用中 , 又点击了我们应用中的回退按钮 , 要求不能回退到用户刚才访问的项目外地址 。



router编程式导航


首先先回顾一下router的两个回退方法(Vue2用法) :



  • this.$router.back() --回退

  • this.$router.go(-1) --前进或后退 , 值为-1时后退


// Vue3用法
// 1. 引入 useRouter 方法
import { useRouter , useRoute } from 'vue-router'
// 2. 实例化router
const router = useRouter()
// 3. 使用方法进行回退
router.back()

history全局对象


我们怎样知道刚才访问的页面是否为项目中配置的路由呢 ?


history对象 !!



  • history对象是浏览器提供的一个全局对象,它包含了浏览器的浏览历史记录

  • history.state : history提供了state属性 , 返回当前历史状态对象


我们在点击返回按钮时可以在控制台查看一下history.state 属性


当我们使用项目外的网站跳转至项目路由再进行回退 :


null.png


我们可以看到state中有一个back属性 , 当外部网站跳转回来时history.state.back值为null


那么项目内部相互跳转再进行回退是什么效果呢 ?


login.png


我们可以看到state中的back值为/login , 那么我们就可以用小back来做判断了


// 回退按钮
<button @click="onClickBack">返回</button>
<templete>

</templete>
// 点击返回按钮事件函数
const onClickBack = () => {
//1. console.log(history) 可以试打印一下history对象
if ( history.state?.back ) {
//2. 如果history.state?.back不为null , 返回上一个页面
router.back()
} else {
//3. 否则返回主页面
router.push('/')
}
}


拓展: 可选链



  • 上面代码中我们用到了history.state?.back, 上文我们有提到history.state?.back的值有可能为null , 所以会发生找不到back属性的情况 ;

  • 我们可以使用ES2021可选链, 当然也可以使用条件判断或三元运算符等方法 , 相较而言可选链更加便捷一些 ;

  • ES2021(也称为ES12)是JavaScript的最新版本,于2021年6月发布。



640 (11).jpg


以上是我解决此问题的方案 , 小伙伴们有什么更好的方案可以一起探讨一下下~


作者:Kikoyuan
来源:juejin.cn/post/7263025923967516733
收起阅读 »

抛弃legacy,拥抱Babel

web
背景 公司项目使用Vite + Vue3技术栈,为了兼容低版本浏览器,使用@vitejs/plugin-legacy做代码转换,关于@vitejs/plugin-legacy是如何做代码转换的,参考我的这篇文章。 不过@vitejs/plugin-legacy...
继续阅读 »

背景


公司项目使用Vite + Vue3技术栈,为了兼容低版本浏览器,使用@vitejs/plugin-legacy做代码转换,关于@vitejs/plugin-legacy是如何做代码转换的,参考我的这篇文章


不过@vitejs/plugin-legacy存在以下几个问题:



  • 速度太慢,生成两套代码真的很耗时间

  • 动态加载兼容性代码在使用wujie等微前端框架时存在问题,无法正确加载兼容代码


基于此,笔者决定试试直接使用Babel转化代码,看看效果怎么样。


拥抱Babel


Babel是什么


如果你不知道Babel是什么,请参考这里


Babel 和 @vitejs/plugin-legacy对比


@vitejs/plugin-legacy 内部使用Babel做代码转化从而兼容低版本浏览器


@vitejs/plugin-legacy 会向html文件中插入按需加载兼容代码的逻辑,只有在低版本浏览器中才加载兼容代码


如果使用Babel做转化,则没有按需加载兼容代码的能力,每次都是加载兼容代码,在高版本的浏览器中毫无疑问的需要加载更多代码


使用Babel做转换,不会动态加载兼容代码,在微前端框架中稳定性会更好


实操


安装babel插件


首先安装@rollup/plugin-babel插件,此插件是一个Rollup插件,允许在Rollup中使用babel,因为Vite在打包时使用的就是Roolup,Vite官方也对部分主流Rollup插件做了兼容,所以此插件在Vite中可以放心使用。


pnpm add @rollup/plugin-babel -D

同时需要安装一些babel依赖:


pnpm add @babel/preset-env core-js@3 regenerator-runtime

注意 core-js需要使用最新的3版本,regenerator-runtime则用来做async、await语法转化


配置方法


首先需要在项目入口文件处加上如下两句:即引入polyfill


import 'core-js/stable';
import 'regenerator-runtime/runtime';

然后,在vite.config.ts文件中删除@vitejs/plugin-legacy插件,并在打包阶段加入@rollup/plugin-babel插件


import { defineConfig } from 'vite';
import PostCssPresetEnv from 'postcss-preset-env';
import { babel } from '@rollup/plugin-babel';

export default defineConfig(() => {
return {
build: {
cssTarget: 'chrome70', // 注意添加css的低版本兼容,当然也可以配置PostCssPresetEnv
target: 'es2015', // 使用esbuild将代码转换为ES5
rollupOptions: {
plugins: [
// https://www.npmjs.com/package/@rollup/plugin-babel
babel({
babelHelpers: 'bundled',
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'entry', // 注意这里只能使用 entry
corejs: '3',
targets: 'last 2 versions and not dead, > 0.2%, Firefox ESR',
},
],
],
plugins: [],
compact: false,
}),
],
},
},
css: {
preprocessorOptions: {
css: { charset: false },
},
postcss: {
// 注意这里需要对css也做下低版本兼容,否则部分样式无法应用
plugins: [PostCssPresetEnv()],
},
},
};
});

使用以上配置,表示当前我们在构建阶段要使用Babel,其中有如下几点注意事项:



  • 入口处必须导入polyfill相关文件

  • babel的配置中useBuiltIns选项必须设置为entry,不可使用usage,使用后者会导致生成的兼容代码出问题,具体原因未知,有兴趣的小伙伴可以研究下。

  • corejs版本写自己的安装版本,一般为3即可

  • build.target需要配置为esbuild最低可转化版本es2015,能低就低原则

  • 注意配置css的兼容方案,可以使用postcss-preset-env做降级,这是比较推荐的方式,当然也可以使用build.cssTarget属性配置,具体配置方法参考这里


目前按照这一套下来是可以跑通,实现使用babel兼容低版本浏览器。


总结


本文介绍了一种在Vite中使用babel做低版本浏览器兼容的方法,亲测可行,但是在整个过程中遇到了很多阻力,比如:



  • 不能使用babel中的useBuiltIns: 'usage'

  • css 也需要做兼容

  • 入口处需要引入兼容库

  • ...


不过最后好在完成了低版本浏览器兼容。


在这个过程中,笔者越来越觉着Vite在带来优秀的开发体验的同时,也同样引入了打包的高复杂度,高度的默认优化使得用户很难自己随心所欲的配置打包方案,开发和打包的差异性也让人很是担忧,不知道打包后的代码是否能正常运行,种种这些问题让我很是怀念webpack的打包时代。


每个新型事物的出现都会伴随着利弊,Vite还很新,它大幅优化了前端的开发体验,但也间接提高了打包复杂度。


市面上的打包器很多Vite、Webpack、Esbuild、Turbopack、Rspack ...,如何抉择还得看屏幕前的你了。


最后,加油吧,前端工程师们!期待有一天一个真正完美的打包器的问世,那将是美妙的一天。


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

改造mixins,我释放了20倍终端性能

web
前言 彦祖们,今天分享一个笔者遇到的真实项目场景, 做了一个项目肿瘤切除术,直接把性能提升 20 倍 认真看完,帮你简历上亮点。阅读本文前,默认彦祖们已经了解 vue.mixins 眼见为实,彦祖们先看下优化前后的性能对比 优化前 优化后 项目背景 开...
继续阅读 »

前言


彦祖们,今天分享一个笔者遇到的真实项目场景, 做了一个项目肿瘤切除术,直接把性能提升 20 倍


认真看完,帮你简历上亮点。阅读本文前,默认彦祖们已经了解 vue.mixins


眼见为实,彦祖们先看下优化前后的性能对比



  • 优化前
    WechatIMG142.jpg

  • 优化后
    WechatIMG143.jpg


项目背景


开始之前,让我们来简述一下项目背景


笔者的项目业务是工业互联网,简而言之就是帮助工厂实现数字化


其中的终端叫做工控机(性能较我们 PC 会相差几十倍),理解一下 就是工业操控机器,说白了就是供工人操作业务的一个终端


类似于我们去医院自助挂号/打印报告的那种终端


技术栈



  • vue2


问题定位


在笔者接手项目(历时三年的老项目,实在是非常痛苦)的时候,发现其中一个页面过一段时间就奔溃无响应,导致现场屡次投诉


这种依附于终端的界面属实不好调试


经过各种手段摸排,我们定位到了问题所在


其实就是 vue mixins 内容部添加了重复的 websocket 事件监听器


导致页面重复渲染,接口重复调用


在线 Demo


老规矩先上 demo


stackblitz.com/edit/vue-74…


现场场景复现


下面笔者简单模拟一下线上的真实代码场景


代码结构


因为线上的组件结构非常复杂,子组件数量达到了 20 个甚至 30 个以上


笔者就抽象了主要问题,模拟了一下 5 个子组件的情况


image.png


总结一下图中的两个关键信息


1.child 子组件可能 会被多个父组件引用


2.child 子组件的层级是不固定


代码目录结构大致如下



  • Parent.vue // 主页面

  • mixins

    • index.js // 核心的 mixin 文件



  • component

    • child1.vue // 子组件

      • grandchild1.vue // 孙子组件



    • child2.vue

    • child3.vue

    • child4.vue

    • child5.vue




代码说明


接下来让我们简单来看下项目中各个代码文件的主要作用



  • mixins.js


剥离业务逻辑后,核心就是增加了一个onmessage事件监听器


最后通过各自子组件自定义的onWsMessage去处理对应的业务逻辑


export const wsMixin = {
created() {
window.addEventListener('onmessage', this.onmessage)
},
beforeDestory() {
window.addEventListener('onmessage', this.onmessage)
},
methods: {
// ... 省略其他业务方法
async onmessage(e) {
// 开始处理业务逻辑,这里用 fetch 接口代替,当然实际业务比这复杂太多
fetch(`https://api.example.com/${Date.now()}`)
// ...

// 开始处理对应的业务逻辑
this.onWsMessage(e.detail)
}
}
}



  • Parent.vue


引入子组件,并且模拟了 websocket 推送消息行为


<template>
<div id="app">
<Child1 />
<Child2 />
<Child3 />
<Child4 />
<Child5 />
</div>

</template>
<script>
import Child1 from './components/Child1.vue'
import Child2 from './components/Child2.vue'
import Child3 from './components/Child3.vue'
import Child4 from './components/Child4.vue'
import Child5 from './components/Child5.vue'
import { wsMixin } from './mixins'
// 模拟 websocket 1s 推送一次消息
setInterval(() => {
const event = new CustomEvent('onmessage', {
detail: { currentTime: new Date() }
})
window.dispatchEvent(event)
}, 1000)

export default {
name: 'Parent',
components: { Child1, Child2, Child3, Child4, Child5 },
mixins: [wsMixin],
methods: {
onWsMessage(data) {
console.log('parent onWsMessage', data)
}
}
}
</script>



  • child.vue


child.vue 核心逻辑都非常相似,此处以 child1.vue 举例,其他不再赘述


<template>
<div>
child1
</div>

</template>
<script>
import { wsMixin } from '../mixins'
export default {
mixins: [wsMixin],
methods: {
onWsMessage(data) {
console.log('child1 onWsMessage', data)
// 处理业务逻辑
}
}
}
</script>


现场预览


彦祖们,让我们来看一下模拟的现场


我们期望的效果应该是 onmessage 收到消息后,会发送一次请求


但是目前来看显然是发送了 6 次请求


实际线上更为复杂可能高达 20 倍,30 倍...这是非常可怕的事


2023-11-26 11.13.35.gif


开始动刀


接下来让我们一步步来切除这个监听器肿瘤,让终端变得更轻松


定位重复的监听器


现象已经比较明显了


彦祖们大致能猜想到是因为绑定了过多的 onmessage 监听器导致过多的重复逻辑.


我们可以借助 getEventListeners API 来看下指定对象的绑定事件



这个 API 只能在浏览器中调试,无法在代码中使用



chrome devTools 执行一下 getEventListeners(window)


很明显有 6 个重复的监听器(1个 Parent.vue + 5个 Child.vue)


image.png


getEventListeners 介绍


彦祖们这个 API 对于事件监听类的代码优化还是蛮有效的


我们还可以右键 listener 定位到具体的赋值函数
2023-11-26 11.25.18.gif


切除重复的监听器


目标已经很明确了,我们只需要一个 onmessage 监听器就足够了


那么把 child.vuemixins的监听器移除不就好了吗?


彦祖们可能会想到最简单的方案,就是把 mixins 改成函数形式,通过传参判断是否需要添加监听器


但是因为实际业务的复杂性,上文中也提到了 mixins 同时也被其他多个文件所引用,最终这个方案被 pass 了


那么我们可以反向思考一下,只给 Parent.vue 添加监听器


需要一个辅助函数来判断是否为 Parent.vue,直接看代码吧


const isSelfByComponentName = (vm, componentName) => {
// 这里借助了 element 的思路,新增了 componentName 属性,不影响 name 属性
return vm.$options.componentName === componentName
}

让我们来测试一下,很完美,为什么第一个 true 就能确定是父组件呢?


如果不了解的彦祖,建议你看下父子组件的加载渲染顺序


image.png


此时的



  • mixins.js


const isSelfComponentName = (vm, componentName) => {
return vm.$options.componentName === componentName
}

export const wsMixin = {
created() {
console.log('__SY__🍦 ~ created ~ isSelfComponentName', isSelfComponentName(this, 'Parent'))
if (isSelfComponentName(this, 'Parent')) window.addEventListener('onmessage', this.onmessage)
},
beforeDestory() {
window.removeEventListener('onmessage', this.onmessage)
},
methods: {
async onmessage(e) {
// 开始处理业务逻辑,这里用 fetch 接口代替
fetch(`https://api.example.com/${Date.now()}`)
console.log('__SY__🍦 ~ onmessage ~ e:', e)
// 省略处理统一逻辑....

// 开始处理对应的业务逻辑
this.onWsMessage(e.detail)
}
}
}

如何进行子组件的消息分发?


前面我们已经把多余的监听器给切除了,网络请求的确变成了 1s一次, 但是新问题随即出现了


2023-11-26 12.18.50.gif


我们会发现此时只有Parent.vue触发了onWsMessage


child.vue的对应的 onWsMessage 并没有触发


那么此时的核心问题就是 如何从父组件的监听事件中分发消息给多个子组件?


利用观察者模式思想解决消息分发


我们可以借助观察者模式思想来实现这个功能


解决这个问题还有个前提,我们得知道哪些组件是 Parent.vue的子组件


同样我们需要借助一个辅助函数,直接安排


const isChildOf = (vm, componentName) => {
let parent = vm.$parent
// 这里为什么要向上遍历呢?因为前面提到了,子组件的层级是不固定的
while (parent) {
if (parent.$options.componentName === componentName) return true
parent = parent.$parent
}
return false
}

测试一下,不用看 就是自信


image.png


核心代码


彦祖们核心代码来了!


我们在 mixins.js 初始化一个 observerList=[], 用来存储子组件的 onWsMessage方法


created() {
if (isSelfComponentName(this, 'Parent')) {
observerList.push(this.onWsMessage) // 统一由 observerList 管理
window.addEventListener('onmessage', this.onmessage)
} else if(isChildOf(this,'Parent') {
observerList.push(this.onWsMessage)
}
}

收到消息后进行分发


methods: {
async onmessage(e) {
// 开始处理业务逻辑,这里用 fetch 接口代替
fetch(`https://api.example.com/${Date.now()}`)

// 省略业务逻辑....

// 这里我们就要遍历 observerList
observerList.forEach(observer=>observer(e.detail))
}
}

看下优化后的效果
接口 1s一次,各组件也完整的接受到了信息


2023-11-26 12.16.25.gif


当然,除此之外,笔者还做了很多的性能优化手段
比如


1.把大量的 O(n^2) 的算法降维到了 O(n)


2.把非实时性数据做了节流保护


3.大量的template表达式语法迁移到了 computed


4.针对重复的赋值更新逻辑进行了拦截


5.利用 requestIdleCallback 在空闲帧执行 echarts 的渲染


写在最后


之前有彦祖问过笔者,什么才算是面试简历中的亮点


如果笔者是面试官,我觉得 能用最细碎的知识点 解决最复杂的业务问题 绝对算的上是项目亮点


文中的各个知识点,彦祖们应该都非常熟悉


能把你的八股文知识,转换成真正解决业务问题的能力,这是非常难得的


个人能力有限


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


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

前端接口防止重复请求实现方案

web
前言 前段时间老板心血来潮,要我们前端组对整个的项目都做一下接口防止重复请求的处理(似乎是有用户通过一些快速点击薅到了一些优惠券啥的)。。。听到这个需求,第一反应就是,防止薅羊毛最保险的方案不还是在服务端加限制吗?前端加限制能够拦截的毕竟有限。可老板就是执意要...
继续阅读 »

前言


前段时间老板心血来潮,要我们前端组对整个的项目都做一下接口防止重复请求的处理(似乎是有用户通过一些快速点击薅到了一些优惠券啥的)。。。听到这个需求,第一反应就是,防止薅羊毛最保险的方案不还是在服务端加限制吗?前端加限制能够拦截的毕竟有限。可老板就是执意要前端搞一下子,行吧,搞就搞吧,you happy jiu ok


虽然大部分的接口处理我们都是加了loading的,但又不能确保真的是每个接口都加了的,可是如果要一个接口一个接口的排查,那这维护了四五年的系统,成百上千的接口肯定要耗费非常多的精力,根本就是不现实的,所以就只能去做全局处理。下面就来总结一下这次的防重复请求的实现方案:


方案一


这个方案是最容易想到也是最朴实无华的一个方案:通过使用axios拦截器,在请求拦截器中开启全屏Loading,然后在响应拦截器中将Loading关闭。


image.png

这个方案固然已经可以满足我们目前的需求,但不管三七二十一,直接搞个全屏Loading还是不太美观,何况在目前项目的接口处理逻辑中还有一些局部Loading,就有可能会出现Loading套Loading的情况,两个圈一起转,头皮发麻。


方案二


加Loading的方案不太友好,而对于同一个接口,如果传参都是一样的,一般来说都没有必要连续请求多次吧。那我们可不可以通过代码逻辑直接把完全相同的请求给拦截掉,不让它到达服务端呢?这个思路不错,我们说干就干。


首先,我们要判断什么样的请求属于是相同请求


一个请求包含的内容不外乎就是请求方法地址参数以及请求发出的页面hash。那我们是不是就可以根据这几个数据把这个请求生成一个key来作为这个请求的标识呢?


// 根据请求生成对应的key
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

有了请求的key,我们就可以在请求拦截器中把每次发起的请求给收集起来,后续如果有相同请求进来,那都去这个集合中去比对,如果已经存在了,说明就是一个重复的请求,我们就给拦截掉。当请求完成响应后,再将这个请求从集合中移除。合理,nice!


具体实现如下:


image.png

是不是觉得这种方案还不错,万事大吉?


no,no,no! 这个方案虽然理论上是解决了接口防重复请求这个问题,但是它会引发更多的问题。


比如,我有这样一个接口处理:


image.png


那么,当我们触发多次请求时:


image.png

这里我连续点击了4次按钮,可以看到,的确是只有一个请求发送出去,可是因为在代码逻辑中,我们对错误进行了一些处理,所以就将报错消息提示了3次,这样是很不友好的,而且,如果在错误捕获中有做更多的逻辑处理,那么很有可能会导致整个程序的异常。


而且,这种方案还会有另外一个比较严重的问题


我们在上面在生成请求key的时候把hash考虑进去了(如果是history路由,可以将pathname加入生成key),这是因为项目中会有一些数据字典型的接口,这些接口可能有不同页面都需要去调用,如果第一个页面请求的字典接口比较慢,第二个页面的接口就被拦截了,最后就会导致第二个页面逻辑错误。那么这么一看,我们生成key的时候加入了hash,讲道理就没问题了呀。


可是倘若我这两个请求是来自同一个页面呢?


比如,一个页面同时加载两个组件,而这两个组件都需要调用某个接口时:


image.png

那么此时,后调接口的组件就无法拿到正确数据了。啊这,真是难顶!


方案三


方案二的路子,我们发现确实问题重重,那么接下来我们来看第三种方案,也是我们最终采用的方案。


延续我们方案二的前面思路,仍然是拦截相同请求,但这次我们可不可以不直接把请求挂掉,而是对于相同的请求我们先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求


image.png

思路我们已经明确了,但这里有几个需要注意的点:



  • 我们在拿到响应结果后,返回给之前我们挂起的请求时,我们要用到发布订阅模式(日常在面试题中看到,这次终于让我给用上了(^▽^))

  • 对于挂起的请求,我们需要将它拦截,不能让它执行正常的请求逻辑,所以一定要在请求拦截器中通过return Promise.reject()来直接中断请求,并做一些特殊的标记,以便于在响应拦截器中进行特殊处理


最后,直接附上完整代码:


import axios from "axios"

let instance = axios.create({
baseURL: "/api/"
})

// 发布订阅
class EventEmitter {
constructor() {
this.event = {}
}
on(type, cbres, cbrej) {
if (!this.event[type]) {
this.event[type] = [[cbres, cbrej]]
} else {
this.event[type].push([cbres, cbrej])
}
}

emit(type, res, ansType) {
if (!this.event[type]) return
else {
this.event[type].forEach(cbArr => {
if(ansType === 'resolve') {
cbArr[0](res)
}else{
cbArr[1](res)
}
});
}
}
}


// 根据请求生成对应的key
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()

// 添加请求拦截器
instance.interceptors.request.use(async (config) => {
let hash = location.hash
// 生成请求Key
let reqKey = generateReqKey(config, hash)

if(pendingRequest.has(reqKey)) {
// 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
// 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
let res = null
try {
// 接口成功响应
res = await new Promise((resolve, reject) => {
ev.on(reqKey, resolve, reject)
})
return Promise.reject({
type: 'limiteResSuccess',
val: res
})
}catch(limitFunErr) {
// 接口报错
return Promise.reject({
type: 'limiteResError',
val: limitFunErr
})
}
}else{
// 将请求的key保存在config
config.pendKey = reqKey
pendingRequest.add(reqKey)
}

return config;
}, function (error) {
return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 将拿到的结果发布给其他相同的接口
handleSuccessResponse_limit(response)
return response;
}, function (error) {
return handleErrorResponse_limit(error)
});

// 接口响应成功
function handleSuccessResponse_limit(response) {
const reqKey = response.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(response))
}catch(e) {
x = response
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'resolve')
delete ev.reqKey
}
}

// 接口走失败响应
function handleErrorResponse_limit(error) {
if(error.type && error.type === 'limiteResSuccess') {
return Promise.resolve(error.val)
}else if(error.type && error.type === 'limiteResError') {
return Promise.reject(error.val);
}else{
const reqKey = error.config.pendKey
if(pendingRequest.has(reqKey)) {
let x = null
try {
x = JSON.parse(JSON.stringify(error))
}catch(e) {
x = error
}
pendingRequest.delete(reqKey)
ev.emit(reqKey, x, 'reject')
delete ev.reqKey
}
}
return Promise.reject(error);
}

export default instance;

补充


到这里,这么一通操作下来上面的代码讲道理是万无一失了,但不得不说,线上的情况仍然是复杂多样的。而其中一个比较特殊的情况就是文件上传


image.png

可以看到,我在这里是上传了两个不同的文件的,但只调用了一次上传接口。按理说是两个不同的请求,可为什么会被我们前面写的逻辑给拦截掉一个呢?


我们打印一下请求的config:


image.png

可以看到,请求体data中的数据是FormData类型,而我们在生成请求key的时候,是通过JSON.stringify方法进行操作的,而对于FormData类型的数据执行该函数得到的只有{}。所以,对于文件上传,尽管我们上传了不同的文件,但它们所发出的请求生成的key都是一样的,这么一来就触发了我们前面的拦截机制。


那么我们接下来我们只需要在我们原来的拦截逻辑中判断一下请求体的数据类型即可,如果含有FormData类型的数据,我们就直接放行不再关注这个请求就是了。


function isFileUploadApi(config) {
return Object.prototype.toString.call(config.data) === "[object FormData]"
}

最后


到这里,整个的需求总算是完结啦!不用一个个接口的改代码,又可以愉快的打代码了,nice!


Demo地址


作者:沽汣
来源:juejin.cn/post/7341840038964363283
收起阅读 »

我的发!被后端五万条数据爆破我是怎么处理的

web
前言 今天面试的时候面试官直接问了一句后端一次性返回10万条数据给你,你如何处理?,我脑中浮现的第一句话就是拿着物理学圣剑找后端进行 “友好的协商”,谁打赢了听谁的。不过虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将带着大家一些处理方案。...
继续阅读 »

前言


今天面试的时候面试官直接问了一句后端一次性返回10万条数据给你,你如何处理?,我脑中浮现的第一句话就是拿着物理学圣剑找后端进行 “友好的协商”,谁打赢了听谁的。不过虽然这种情况很少,不过我在实际开发中还真遇到了类似的情况,接下来我将带着大家一些处理方案。


正文


方案一 直接渲染


如果请求到10万条数据直接渲染,页面会卡死的,很显然,这种方式是不可取的。 pass!


 async getData() {
this.loading = true;
const res = await axios.get("/api/getData");
this.arr = res.data.data;
this.loading = false;
}

方案二 setTimeout分页渲染


这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了。


const renderData = async () => {
const Data = await getData()
const total = Data.length

const page = 0
//每页数量
const limit = 200
//总页数
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = Data[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `${item.src}" />${item.text}`
container.appendChild(div)
}
render(page + 1)
}, 0)
}

render(page)
}

方案三 requestAnimationFrame


使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame


const renderData = async () => {
const Data = await getData()
const total = Data.length

const page = 0
//每页数量
const limit = 200
//总页数
const totalPage = Math.ceil(total / limit)


const render = (page) => {
if (page >= totalPage) return
// 使用requestAnimationFrame代替setTimeout
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = Data[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `${item.src}" />${item.text}`
container.appendChild(div)
}
render(page + 1)
})
}

render(page)
}

方案四 表格滚动触底加载


原理很简单,就是在列表尾部放一个空节点,然后先渲染第1页数据,向上滚动,等到空节点出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。


至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性。也可以用 js 的IntersectionObserver API 来实现




<template>
<div id="container" @scroll="handleScroll" ref="container">
<div class="sunshine" v-for="(item) in showList" :key="item.tid">
<img :src="item.src" />
<span>{{ item.text }}span>

div>
<div ref="blank">div>
div>
template>

方案五 虚拟列表


什么是虚拟列表?


所谓的虚拟列表实际上是前端障眼法的一种表现形式。


看到的好像所有的数据都渲染了,实际上只渲染可视区域的部分罢了。如果10万条数据都渲染,那得需要多少dom节点元素呢?所以我们只给用户看,他当下能看到的如果用户要下拉滚动条或者上拉滚动条再把对应的内容呈现在可视区域内。这样就实现了看着像是所有的dom元素每一条数据都有渲染的障眼法效果了


实现


<template>

<div
class="virtualListWrap"
ref="virtualListWrap"
@scroll="handleScroll"
:style="{ height: itemHeight * count + 'px' }"
>


<div
class="placeholderDom"
:style="{ height: allListData.length * itemHeight + 'px' }"
>
div>

<div class="contentList" :style="{ top: topVal }">

<div
v-for="(item, index) in showListData"
:key="index"
class="itemClass"
:style="{ height: itemHeight + 'px' }"
>

{{ item.name }}
div>
div>

<div class="loadingBox" v-show="loading">
<i class="el-icon-loading">i>
  <span>loading...span>
div>
div>
template>

作者:笨鸟更要先飞
来源:juejin.cn/post/7338636024212504613
收起阅读 »

普通二本一路打怪升级入职腾讯

2016 年初开始北漂工作,距今刚好 7 年了,做个简单的总结 考研败北,不再二战 聊工作不得不先说下考研失败的经历 大三和大四一直备战考研,没有为找工作做一点准备工作,可以说就没给自己留任何的退路,起早贪黑的备考,皇天不负有心人---落榜 当年真没少刷张宇、...
继续阅读 »

2016 年初开始北漂工作,距今刚好 7 年了,做个简单的总结


考研败北,不再二战


聊工作不得不先说下考研失败的经历 大三和大四一直备战考研,没有为找工作做一点准备工作,可以说就没给自己留任何的退路,起早贪黑的备考,皇天不负有心人---落榜


当年真没少刷张宇、李永乐、肖秀荣的题,但翻看当年的一条动态,这个结果在意料之中,得知结果后,当时纠结是否要二战,二战真的需要极大的勇气和毅力,我是经历过高考复读的,11年高考过了线,但志愿没报好【报志愿是非常重要的事儿,我这是错误示范】,脱靶了,这个没有别的选择了,再战,复读那一年压力,可能只有经历过的人能懂,所以这次考研我没有选择二战,再战一年考上的概率多大呢?自己心里是没底的,可能尽早步入社会于我而言是个不错的选择


图片


考研倒计时


第一次步入社会的实习


因为大学最后的时光备战考研,没有准备工作的面试,过年放假在家里那段时间也是蛮焦虑的,当时拿到了一个网优工程师的机会,但说白了是探测信号的,感觉与自己期望的不一样,虽说当时自己真的不知道要啥,但还是放弃了这个机会


图片


就业焦虑


高中同学在卓望公司实习,人力资源方向,找人帮我推荐了下,1 月来北京面试,面试那天和老七,李雪琪一起吃的饭,与他们见面确实挺开心的,那晚在老七那儿住的,第一次吃羊蝎子【没吃过之前以为羊蝎子是蝎子】,现在回首,依然感慨有同学真好,要珍惜呀


图片


同学情


面试完很快就出了结果-通过了,自己确实技能远远不够,大家都很忙,带我的师傅没空带我,我就自学点东西,实习工资 2100,带薪学习,也还行吧,但我更渴望的是接触真实的工作,但直到我最后离开,也没体验到,那段时间蛮焦虑的,明耀哥和我说可以转正,但可能在这里并非特别适合我,我也认同,当时还找组内大龄程序猿郭哥聊,最后我选择了离开


图片


实习迷茫


3 个月的实习时间,赶上了公司的一次团建和一次聚餐,蛮开心的


反思



没有人有义务带你即使是你的师傅,一切要靠自己,切记!



短暂的实习知道了自己的不足,开始集中学习,毕业后又开始了社招面试之旅,没有工作经验的社招的难度,经历过的都懂,下面开始了崎岖之旅


社招曲折之旅


第一次给了外包,且只做了一天


第一个 offer 是一个外包,外派到中国移动,本来面试时是做 Java 开发,入职第一天,记得非常清楚,那天下雨了,仿佛暗示着什么


刚到公司,要先做移动的安全检测题,耗费了我洪荒之力,终于通过了,然后左拐右拐终于到了我的办公区,一间狭小的会议室,几个人在会议室里,一看就不正规,然后负责人也是我当时的面试官说,先看看安卓测试的知识吧,然后巴拉巴拉一堆,最后得到的信息是让我先干测试,我内心万马奔腾,感觉受到了欺骗


这里做的事和我的短期规划不符,我当时依然没有任何退路,但因为刚开始嘛,试错成本很低 所以我坚定了选择了辞职,第二天跑到海淀西北旺那边的联想总部园区办离职,期间路过百度大厦,仰望着发了会儿呆,继续朝目的地走去,就这样结束了一天的外包生涯


下面这个截图大概率是入职当天下班后发的动态,现在看我也有点懵,瞎BB这么多字,到底想表达啥?人生不是选择就是循环,也可能嵌套,我用了 do-while 先做再校验的循环,接受这个机会,发现并不适合自己,那就果断跳出循环体了,没有遗憾了


图片


第一次外包第一天下班感想


反思



面试一定要聊清楚进去做什么,是否和自己的规划匹配,如果不,最好果断做决定。如果确定不了,不妨再试错成本低的前提下尝试一下,然后再做选择



第二次选择了第三方支付


外包离职后做了简单的调整,又开始了忙碌的找工作,投了不少简历,大多石沉大海,庆幸被几家公司捞起来邀请了面试,拿了三个 offer,有一个忘记公司名字了,面试通过了但让我先去工作几天,然后再签合同,大概是想先检查下我是否能干活儿,这种要求没听说过,果断拒绝


另两个 offer 是一家做互联网医疗的公司和第三方支付,互联网医疗那个面试流程走的很快,且催的比较急,我先接了 offer,也定了入职时间。同时一直在催第三方支付的走流程,还好在医疗那家公司入职日期前走完了流程。然后告诉 hr 说不去了,这是第一次拒绝 offer,当时非常的愧疚,所以从那以后一直到现在,在不确定的接 offer 的情况下,从没有答应过,不想给别人找麻烦


在这两个 offer 之间做选择时几乎没怎么犹豫,因为在 16 年时,支付业务还是可以的,而且这家公司还有支付牌照,一个支付牌照很贵的,而且不增发了,所以很多大公司通过收购有支付牌照的小公司来开展自己的支付业务 这家支付公司也是一样,被亿利资源集团收购了,估计也是看重了支付牌照


这家公司挺有意思的,地点在大望路,北京电视台大裤衩旁边,这个位置价格可不便宜,可以看出亿利资源这家集团还是很强的,国企范儿,下面我必须要聊聊在这家公司的一些事儿


你听说过程序猿要穿西装,白衬衫,打领带,穿皮鞋吗?这个集团就这个要求,不管啥职位,不管男女,都这要求,中午我们几个出去溜达,不知有多少人投来异样的眼光,估计我们要么是卖保险的,要么是中介。而且更离谱的是每天都有检查的,要是被发现不符合规范,可是真的要扣钱的,我一共被扣了两次,每次 500 元。一次是说我皮鞋颜色不符合规范,一次是没有戴工卡,当天检查当天就通报呀,会立马在下次发工资中扣除掉,办事特迅速


这真的是我职业生涯中挺有意思的一个插曲


我们这个支付公司比较小,主要做的业务有批量代付、批量代收、网银支付、快捷支付等,一般客户都是集团内部的,使用的比较少 说说技术,基础服务用的 Dubbo + Zookeeper,数据库用的 Oracle,MQ  用的 RabbitMQ,这些还行,尤其是 Dubbo,这可是我第一次接触,还是有点兴奋的 Web 服务相对有点 low 了,还用了 JSP,我是写过 JSP 的人,我敢打赌,现在看这篇文章的人,一定有没写过 JSP 的人,如果写过 JSP 的请留言告诉我


上线采用人肉上线法,替换 class,对,你没看错,上线前开发要写好上线文档,替换哪些 JSP 文件以及 class 文件,然后由运维手动替换,再重启服务,现在都记忆犹新


但是,但是了,这家公司有 2 点特别好吧 1.管早饭和午饭,是伙食非常好的那种,真的可以用非常好来形容,总之从那家公司出来后,我是没见过哪家公司的食堂这么给力 2.不加班,但严格卡打卡时间,早 8 点到晚 6 点,迟到要扣 500 块的,迟到 1 s 也要扣,但大家真的是 6 点准时下班呀,是真的不加班,当然上线除外


图片


西装皮鞋程序猿上线


在这家公司待的挺开心的,即使有穿西装皮鞋的规则,不过最后选择离开还是考虑自身成长,活儿很少,用户也少,成长相对慢一些,而且加上 18 年央妈出手要让各个支付公司对接银联,支付公司也不好过,所以在 18 年我选择了离开,后面得知我离开后没多久,公司就开始了裁员了,稳定后人就很少了,然后大家也都陆续选择了离开


画外音



17 年,18 年是我疯狂关注技术公众号的时间,可以说看到一个技术公众号就关注,也是那个时候有幸加了很多号主的微信,很多已经是大佬了。那个时候也喜欢参加线下技术沙龙,也有幸亲眼见到过很多技术大佬。虽然没有变成我的人脉,毕竟彼此差距大,不奢求,但也会有一丝丝联系



反思



准时上下班,但有一些奇怪的规定的公司,要根据自己当前看重什么来做选择,如果不想加班还可以承受一些奇怪的规定,也挺好的,做到 wlb



第三次是我最不愿提起的经历


这次跳槽面试的公司很少,因为还在上一家公司上班时面的,当年都是现场面,一般几轮面试顺利的话,当天就知道结果了。所以这次几乎不涉及到选择,我是通过海峰总的朋友圈投的饿了么。饿了么北京研发中心是海峰总带,很快收到了面试邀请,面试的是新零售的无人货架业务 当时无人货架的业务还挺多的,很多公司在搞,属于抢市场的阶段吧,我没有选择继续做支付业务,原因是我在支付业务的知识储备还是比较少的,还是前面提到的,试错成本低,所以没有继续找支付业务


下午去的,面试还算顺利,3 轮技术面 + 1 轮 HR  面通过了,大约第二天就发了 offer,我也很爽快的接了 offer 当时最后一轮技术面试是海峰总面的,印象深的是问我的职业规划,我说当架构师,说实话,那个时候我连架构师是啥都不知道,不过海峰总应该是和我说了些啥,但我已经记不清了,对不起海峰总


说不愿提起这段经历的原因是,我待的很不开心,从第一天进去到最后离开,我感觉一直都没有融入进去 先说结果,饿了么试用期我没有通过,但具体原因我真的到现在都不知道,也许只有他们几个做决定的人最清楚了


那里工作节奏还是很快的,刚去半个月吧,我就上了一次线,那上线流程确实丝滑,点点就上线了,和上家公司替换 class 文件对比还是很明显的 后面做第二个需求时有点吃力,业务逻辑还没整明白,然后给了排期后,确实遇到了一些问题,但我没有主动找师傅请教,这是我的问题,我不好意思打扰他,也可能和没融入进去有关,说不清楚了,总之结果是我没有主动找他,他也没主动问我是否有问题,到要求的提测时间,代码虽然提交了一版,但 CR 时被嫌弃了,我也从其他地方听到了一些议论我的声音,大概就是说我写代码不行吧 然后小组长把我代码重构了,说实话,我也看他写的了,真没觉得比我写的好多少,但这个行为确实伤害到了我


最后师傅找我聊,大概就是告诉我,大概试用期过不了了,他也被领导批了,从和师傅的谈话中,学到了遇到问题要及时沟通的,别一个人憋着死扛


然后就是组长找我谈话嘛,说过不了试用期啥的,我当时竟然还求给我次机会,现在想想,真的挺后悔的


领导和 hr 找我谈话时已经是最后了,他和我说我不适合这个团队,离开可能是当时最好的选择,这点我是承认的 然后我就开始新一轮的找工作了


图片


饿厂


这段经历最不愿提起就是试用期没过嘛,感觉是自己职业的污点,但真正不过的原因,到现在我也不清楚,当时代码写的不好是一个原因,但应该不止这个。后面离开后,从其他离职的同事那听到我是被人搞走的,至于真实情况是啥,我也不在乎了,算是职场上了一课


反思



1.有卡点一定要及时找师傅或组长沟通求助,不要一个人憋着死扛,等到最后再开口


2.不适合不要强求,要体面的分手,有尊严的离开


3.打铁还需自身硬



第四次选择了程序化广告


再次出来求职,内心是有一点自卑的,毕竟结果是这样的,但路还得继续走 这次也面了好几家公司,再拿到两家的 offer 后,其他未走完流程的面试也都主动取消了,主要就是做选择了 一个 offer 是首汽约车的派单系统开发,工资比 58 的月 base 高 5k,领导比较欣赏我,很想要我 一个是 58 同城的程序化广告开发,高并发 低延时系统,工资是饿了么平移过来的 这俩 offer 都是当天面试完,当天出结果


当时纠结的点是首汽约车的领导很想要我,给我打了两次电话吧,而且工资比 58 的也高 5k,当时工资本来就没多少,高 5k,真的很多了,而且派单系统很核心,应该也不错 58同城的那个工作机会更吸引我,做联盟 DSP 开发,真的是高并发,低延时系统,我是从未接触过的,听到后真的就差流哈喇子了,而且公司相对比首汽约车大一些,但工资低,而且低的挺多,所以我陷入了纠结


我后面从个人成长的角度做了选择,选择了 58同城,因为做的事儿我更喜欢,工资低点就低点吧,做选择工资确实是一个很重要的因素,但不是全部,在钱和成长之间,我选择了成长。当然这里并不是说首汽约车的派单没成长,这个判断只属于我个人的判断。我内心的天平更倾向 58


现在回看在 58 同城 3 年的时光,确实成长很多,这个选择我是很满意的。而且团队氛围很好,大家年龄都相仿,很能聊的来,那里有太多回忆了


从 58 选择离开的原因还挺多 1.当时据说可能要组织架构调整,我们领导可能要走了,当时我们部门很多人都在看机会,算是动荡吧 2.没啥新增业务了,主要维护系统正常运转 3.工资低


再继续待下去,成长和成事都不太满足了,而且工资还低,也挺难受,所以选择了离开


图片


初到58


图片


离开58


反思



1.从成长和成事的维度做选择,早期钱的权重可以低一些


2.选择一个好的平台和业务很重要,你学的那些技术点要有使用的地方很重要



第五次选择了大厂


21 年出来看机会,感觉是 58 的经历给自己做了一些背书,这次拿了挺多 offer,也挺纠结的


offer



  1. 腾讯广告,偏内部系统

  2. 小红书广告检索,直属领导挺欣赏我,做的方向是我想做的

  3. 美团外卖广告检索,做的方向是我想做的

  4. 猿辅导用户增长,现金+期权,总包最高,当时还没出教育政策,发展向好

  5. 爱奇艺广告投放平台

  6. 滴滴电商

  7. 去哪儿网客服平台


这几个 offer,我当时主要的纠结点是前 4 个 腾讯是大厂,我没在大厂干过,很想去体验一下,面试时我做了预期,可能做的业务不是我想做的,但大厂光环实在诱人


小红书做的业务是我想做的,而且小红书发展势头正猛呢,直属领导也挺欣赏我,还帮我争取工资,我和他沟通是最多的,电话了几次,而且我还去了小红书公司一趟,他还请我吃饭了,但我最后没去,感觉很对不起他,这也是我一直没敢再联系他的原因。但不好的一点是大小周


美团做的方向是我想做的,而且我对一面面试官的印象特别好,加上美团的 Java 也很好。不足是这个 offer 算是我后面争取的,因为面美团是比较早的,3 面和 HR 面完后,一直没接到进一步的通知,这种大概率是作为备选中或 pass 了。后面我拿到了腾讯的 offer,又找当时的二面也就是直属领导聊了下,他又帮忙推进了流程,那个时候 58 很多做广告的去面美团这个岗位,所以卡薪资卡的挺死的,说白了,候选人很多嘛,选择权在手,硬气一些。而且美团福利相对差一些,就没福利


猿辅导当时上市谣言四起,给的薪资是最高的,那月 base 高不说,还给了期权,而且猿辅导不卷


当时真的很纠结 从成长角度看,美团和小红书应该是更好的选择 从成事角度看,小红书和猿辅导应该是更好的选择 但腾讯有大厂光环,没待过大厂,机会摆在眼前,不心动很难 小红书的直属领导都请我吃饭了,而且做的事儿也是自己想做的,且符合成长和成事,但架不住大小周呀,有娃了,想分出一些时间陪娃 美团做的事儿也是自己想做的,加上这个机会还是自己主动争取的,但美团没啥福利呀,薪资卡的还那么死 猿辅导可是前景一片看好呀,上市概率这么大,一旦上市,不敢想了,但做的事儿相比其他,可能不是那么有吸引力


怎么办?这次选择没有从成长和成事的角度选,而是遵从了内心,我内心是想弥补大厂的空缺,所以最后选了腾讯


这个选择的 2 年后,也就是现在,回看下当时的选择 猿辅导,后面双减政策一出,猿辅导开始裁员,上市更是无望,没去对了 滴滴电商,滴滴 app 下架,滴滴橙心优选应该是黄了吧,滴滴的电商应该是没做起来,没去对了 爱奇艺,后面也经历了裁员,没去就对了 去哪儿网,这个在我选择的最后 小红书,小红书广告业务还算不错,但没有其他的信息 美团,据我观察,那个团队发展的应该也不错,在美团技术公众号上看到过那个团队的文章 腾讯,面试时说做 cs,但因为组织架构调整以及人员分配,我做了其他的系统,现在看这个系统是我能团队最重要的项目了,虽说是内部系统,但现在看是符合成事的。不足的是,最近两年腾讯广告业绩不太好,年终奖折损很多。然后就是感觉个人成长较少


总结下来,还算满意,毕竟现在工作是相对稳定的,就是成长少了些,成长嘛,可以自己主动学习,从别的地方补补。但小红书和美团那个也算是自己的小遗憾,没能做自己想做的方向。哪有那么十全十美,既要又要,总得舍弃些什么


图片


鹅厂新员工培训


反思



优先考虑成长和成事,但如果有些事儿不确定,以及内心有个强烈的遗憾需要弥补,稍微遵从下内心吧,不然错过就不在了



虽说社招与 5 家公司产生过关系,但真正我想承认的经历只有 3 家,一天外包不算很正常,饿了么 2 个月简短的时间也可以忽略不计


这就是我从毕业到现在,做的和工作相关的选择,出身不好的我,靠着点滴的进步,一步步走到现在,过程确实曲折些,不过还算满意吧


作者:每天晒白牙
来源:juejin.cn/post/7220793382020743223
收起阅读 »

为什么前端开发变得越来越复杂了?这可能是我们的错

Hello,大家好,我是 Sunday。 最近有很多同学来问我:“Sunday 老师,前端学起来好多的东西啊。各种框架、各种库、各种语法、各种标准,弄得我完全懵逼了。Sunday 老师,你之前是怎么学的,好厉害啊!” 这不禁让我开始反思:“我厉害吗?我可不这么...
继续阅读 »

Hello,大家好,我是 Sunday。


最近有很多同学来问我:“Sunday 老师,前端学起来好多的东西啊。各种框架、各种库、各种语法、各种标准,弄得我完全懵逼了。Sunday 老师,你之前是怎么学的,好厉害啊!”


这不禁让我开始反思:“我厉害吗?我可不这么觉得!” 那我之前是怎么学习这些东西的呢?


我只是占了时间的便利而已~~


背景


如今可用的大量框架和工具给人的印象是 Web 开发可能已经变得过于复杂。如果是一个新人,有这么多要考虑的东西确实会令人感到“恐惧”。好像每一个框架都需要学习,如果不学习给人的感觉就是 “就是没有办法工作” 的样子。更何况还有各种新的热门框架教程和课程增加我们的焦虑感。


但是,大家有没有想过一点,或许前端开发远没有那么负责,这一切也只是一种所谓的 “夸张” 而已。




我记得最初在我学习前端的时候,Angular 还是 1 的版本。Vue 也才刚刚出现。那个时候前端开发好像很简单。标准网站主要由 HTML 和 CSS 组成的静态页面组成,并添加少量 JavaScript 和 jQuery。


快进到今天,看起来就像一个平行宇宙正在发生,有大量的选择。新项目应该使用哪个框架?也许是更成熟的产品,如 React、Angular、Vue、Svelte,或者可能是上个月推出的热门新产品?每个框架都有其独特的生态系统。你还需要决定是否使用 TypeScript 而不是 “普通的” JavaScript,并选择如何使用Next、Nuxt 或 Gatsby 等框架进行服务器端渲染(或静态站点生成) 。如果你想要一个没有错误的应用程序,还不能忘记单元和端到端测试。不过哪怕做到了这些,我们也仅仅知识触及了前端生态系统的表面!


但建立网站真的变得更加复杂了吗?我们今天使用的许多框架和工具最初都是为大型项目而设计的。作为一个新人,真的有这么多要考虑的东西吗?一些新的课程(可能是上周刚刚发布的新框架)没有了它们真的会影响你的工作(找工作)吗?显然不会的!


最初的前端开发状态


作为 2013 年开始工作,2015 后进入 Web 开发的人,那个时候的前端开发真的是非常简单,至少对我来说是这样的。


我记得我可以只使用静态 HTML 页面、最少的 CSS 样式以及少量的 JavaScript(或许还有一点 jQuery)来添加交互功能(这是应用在一个 最高有百万日活 的项目中的技术栈)。你看,哪怕是这样的一个大型项目,它的开发方案依然非常简单。



不过,当时也确实存在一些大型的框架,比如:angular 1。有部分团队也在尝试使用这种全新的技术方案,不过随着 angular 2 的推出,这埋葬了一大批的项目(经历过那个年代的同学应该有对应的感受)。



在当时,我们更关注与 SEO 和页面的优化,但是也仅限于一些标记和关键字之类的东西。与现在的标准相比,可访问性、用户体验和响应式布局 等其他因素几乎在当时都是被忽略的。


不过,随着原生(android || IOS)开发褪去,web 逐渐占据主流。越来越多的功能添加,以及越来越多的人开始依赖于 web 项目。我们开始逐渐在原有的基础之上创建出:新的解决方案、新工具、新工作流程、新功能以及满足更大网络和更大需求所需的任何其他新功能。


JavaScript 框架


越来越多的 JavaScript 框架开始出现,比如大家所熟知的:Vue、React、Angular,通过它们又延伸出了各种生态,如:Element、AntD、Pinia、Redux 等等。


现在,哪怕是一个很小的项目(比如我前几天所做的 运气模拟器依赖了 vue)也会依赖这些框架完成。我好像已经失去了 不依赖框架,完成项目独立开发的能力


但是我们好像忘记了:JavaScript 框架最初应该是为了支持大型项目而不是小型项目而创建的。 学习框架的成本很高,虽然它使用起来确实很简单。但是这是一个增加了我们学习复杂度的来源。


不是所有的项目都必须要依赖框架


JavaScript 框架最适合用于较大的应用程序。如果你正在处理较小的项目,基于组件的框架只会使事情变得复杂,将网站拆分为组件层次结构,这对于小型项目来说很多时候是多余的。


就像 vuex 中犹大所说的一样:



请记住,框架的目的是:简化你的开发并节省你的时间。 不要被框架所绑架。


大家的期望正在变得越来越大


项目的规模随着时间的推移而增长,这是合乎情理的。但是反过来 先构建一个巨型的架构,来应对小型的功能 显得就 不合情理 了。



Web 开发变得过于复杂的想法源于:我们相信我们都拥有与大型企业相同的需求和资源。



这是一个很可怕的事情!


这意味着每个新项目可能需要独特的架构来满足其要求。大公司的所有项目都需要庞大的架构,并且他们的结构经过 “精心设计(至少相对来说是这样)”,以确保可扩展性和维护。他们还拥有庞大的客户群,因此,通过更多的收入、更多的工程师和更清晰的问题了解,维护大型代码库将变得更加容易。


但是对于小公司来说。为了最大程度地减少浪费,小公司的项目应该最小化,不仅要满足其需求的规模,还要满足团队中开发人员的能力。试图模仿大公司的巨型代码架构是毫无意义的


有些人可能会说:“这是我们为了未来的可扩展性和维护而必须做出的牺牲!” 相信我,如果你这么做的话,那么你们肯定撑不到那个时候。


模仿大公司的巨型架构只有一个价值,那就是 增加你简历中的亮点。 如果你从这一点进行考虑,那么你这样做是 “正确的”。


每个解决方案都会引入一个新的问题


每个新框架或库都会出现一个新问题。缺少时间的开发人员花费几个月时间开发出来一个新的工具来解决该问题。如果没有问题呢?那么请放心——我们会创建一个问题,就像多年前的 中、前 台分离一样。


不过世界上任何的行业好像都是想通的,这个事情并不仅仅存在于开发行业。UG 克里希那穆提(印度哲学家)说过:显而易见的事实是,如果你没有问题,你就会制造问题。如果你没有问题,你就不会觉得自己还活着。



我们可以随意以 React 为例(因为我更喜欢 Vue ,所以我不想以 Vue 为例)。它最初是由Facebook创建的,旨在为Facebook开发更多动态功能,同时改善 Facebook 的开发者体验。


自从 React 在 2013 年开源,已经创建了数百个依赖库来解决各种特定于 React 的问题。



  • 如何启动 React 项目?有 Create React App 和 Vite

  • 如何进行状态管理?有 Redux,还有 Mobx

  • 需要创建表单吗?有一个 React Hook 表单

  • 需要服务器端渲染吗?有 Next、Remix 或 Gatsby 等


每个解决方案都有自己的注意事项,开发人员将为它们创建自己的解决方案。


React 可能会认为自己很委屈,因为它认为自己是一个库,而不是一个框架。但是它不可避免地会被社区扩展。同时,Angular 和 Vue 都是拥有自己社区生态系统的框架。并且这只是冰山一角,因为存在许多 JavaScript 框架,每个框架都有自己独特的意识形态和依赖关系。


如何简化我们的代码


在刚才我们已经讨论了很多代码变得复杂的原因。那么接下来我们就要思考 如何才能简化我们的代码


想象一下,你正在开发一个项目、评论和评价其所在地区的餐厅(类似于大众点评)。该项目有关每家餐厅的信息、以及评论的功能。但是在很多情况下,项目最终都会被 延迟发布,因为我们总会添加很多不必要的功能。比如:SSR、通知、离线模式和动画等等。我们在做这些的时候 甚至还没有任何一个用户


我们要知道:完成比完美要重要的多


思考的越多会让我们真正陷入到 过渡设计 的怪圈之中:



所以 简化你的项目吧。去掉那些暂时不需要的功能,去掉那些暂时不需要的框架。 为目标选择合适的工具,而不要为工具选择合适的目标!


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

HTML简介:想成为前端开发者?先从掌握HTML开始!

在这个数字化的时代,我们每天都在与网页打交道。你是否曾经好奇过,这些充满魔力的网页是如何诞生的呢?今天,我们就来揭开构成这些网页的神秘面纱——HTML(超文本标记语言)。一、什么是HTML网页的基本组成网页是构成网站的基本元素,通常由图片、链接、文字、声音、视...
继续阅读 »

在这个数字化的时代,我们每天都在与网页打交道。你是否曾经好奇过,这些充满魔力的网页是如何诞生的呢?今天,我们就来揭开构成这些网页的神秘面纱——HTML(超文本标记语言)。

一、什么是HTML

网页的基本组成

网页是构成网站的基本元素,通常由图片、链接、文字、声音、视频等元素组成,通常我们看见的网页都是.htm和.html后缀结尾的文件,因为都称为HTML文件。

什么是HTML

HTML 英文全称是 Hyper Text Markup Language,中文译为“超文本标记语言”,专门用来设计和编辑网页。

Description

使用 HTML 编写的文件称为“HTML 文档”,一般后缀为.html(也可以使用.htm,不过比较少见)。HTML 文档是一种纯文本文件,您可以使用 Windows 记事本、Linux Vim、Notepad++、Sublime Text、VS Code 等文本编辑来打开或者创建。

每个网页都是一个 HTML 文档,使用浏览器访问一个链接(URL),实际上就是下载、解析和显示 HTML 文档的过程。将众多 HTML 文档放在一个文件夹中,然后提供对外访问权限,就构成了一个网站。

二、HTML的历史

HTML的故事始于1989年,当时蒂姆·伯纳斯-李在欧洲核子研究中心(CERN)提出了一个名为“万维网”的概念。

为了实现这一概念,他发明了HTML,并随后与罗伯特·卡里奥一起发明了HTTP协议。从那时起,HTML就成为了互联网不可或缺的一部分。
Description
上图简单罗列了HTML的发展历史,大家可以简单了解一下。

三、HTML相关概念

什么是标签

HTML 标记通常被称为 HTML 标签 (HTML tag)。HTML 标签是由尖括号包围的关键词,比如<html/>。

  • 封闭类型标记(也叫双标记),必须成对出现,如<p></p> 

  • 标签对中的第一个标签是开始标签,第二个标签是结束标签,开始和结束标签也被称为开放标签和闭合标签 。

  • 非封闭类型标记,也叫作空标记,或者单标记,如<br/>

<标签>内容<标签/>

什么是元素

“HTML 标签” 和 “HTML 元素” 通常都是描述同样的意思。但是严格来讲,一个HTML 元素包含了开始标签与结束标签,如下实例。

HTML 元素:

<p>这是一个段落</p>

web浏览器

Web 浏览器(如谷歌浏览器,Internet Explorer,Firefox,Safari)是用于读取 HTML 文件,并将其作为网页显示。浏览器并不是直接显示的 HTML 标签,但可以使用标签来决定如何展现 HTML页面的内容给用户:

Description

HTML 属性

属性是用来修饰元素的,属性必须位于开始标签里,一个元素的属性可能不止一个,多个属性之间用空格隔开,多个属性之间不区分先后顺序。

Description

每个属性都有值,属性和属性的值之间用等号链接,属性的值包含在引号当中,属性总是以名称/值对的形式出现。

四、HTML的基本结构

一个典型的HTML文档由以下几个基本元素构成:

  • <!DOCTYPE html>

这是文档类型声明,告诉浏览器这个文档使用的是HTML5标准。

  • <html>

这是整个HTML文档的根元素,其他所有元素都包含在这个标签内。

  • <head>

这个部分包含了所有关于网页的元信息,如标题、字符集声明、引入的CSS样式表和JavaScript文件等。

  • <title>

这个标签定义了网页的标题,它显示在浏览器的标题栏或标签页上。

  • <body>

这个部分包含了网页的所有内容,如文本、图片、链接、表格、列表等。

HTML的结构示例

让我们通过一个简单的例子来具体了解HTML的结构:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的第一个HTML页面</title>
</head>
<body>
<h1>欢迎来到我的网页!</h1>
<p>这是一个简单的段落。</p>
<a href="https://www.example.com">点击这里访问示例网站</a>
</body>
</html>

在这个例子中,我们可以看到一个完整的HTML文档结构,从<!DOCTYPE html>开始,到最后一个</html>结束。

想象一下,如果HTML是一棵树,那么<html>就是树干,<head>和<body>就像是树的两个主要分支。<head>中的标签好比是树叶,它们虽然不起眼,但却至关重要,为树木提供营养。而<body>中的标签则像是树枝和果实,它们构成了树的主体,吸引人们的目光。

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

点这里前往学习哦!

五、HTML的特点

HTML的特点主要包括简易性、可扩展性、平台无关性和通用性等。具体如下:

1.简易性:
HTML是一种相对容易学习和使用的语言,它的版本升级通常采用超集方式,使得新版本能够兼容旧版本的标签和功能,这样既保持了向后兼容性,又能够灵活方便地引入新的功能。

2.可扩展性:
随着互联网的发展,HTML也在不断增加新的元素和属性来满足新的需求,如支持多媒体内容的嵌入、更丰富的表单控件等。这种设计使得HTML能够适应不断变化的网络环境。

3.平台无关性:
HTML编写的网页可以在不同的操作系统和浏览器上显示,这是因为HTML是一种与平台无关的语言。这意味着无论用户使用什么设备或浏览器,都能够访问和浏览HTML页面。

4.通用性:
HTML是网络的通用语言,它是一种简单的标记语言,用于创建和结构化网页内容。由于其广泛的支持和普及,几乎所有的设备和浏览器都能够解析和显示HTML内容。

5.支持多种媒体格式:
HTML不仅支持文本内容,还能够嵌入图片、音频、视频等多种媒体格式,这使得网页可以提供丰富的用户体验。

6.标准化:
HTML遵循万维网联盟(W3C)制定的国际标准,这意味着网页开发者可以根据这些标准来创建网页,确保网页的互操作性和可访问性。

7.标签丰富:
HTML提供了一系列的标签,如标题、列表、链接、表格等,这些标签使得开发者能够创建出结构清晰、功能丰富的网页。

综上所述,HTML作为一种基础的网页开发语言,因其易学易用、跨平台、多功能和高度标准化的特点,成为了构建现代网络内容的核心工具。

HTML作为连接世界的纽带,其重要性不言而喻。它是数字世界的基石,也是每个想要进入互联网领域的人必须掌握的技能。无论你是梦想成为前端开发者,还是仅仅想要更好地理解这个由代码构成的世界,学习HTML都是一个不错的开始。

收起阅读 »

面试官:能否三行代码实现JS的New关键字

web
谁能不相思,独在机中织。 探索 凡实践,需理论先行,在开始之前,我们要先具体了解一下new创建对象的具体过程。 new的this指向 或者说构造函数的this指向,先来看一个小的示例,思考一下,log打印出来的是什么? function Person(nam...
继续阅读 »

2021_08_26_08_27_IMG_0042.JPG



谁能不相思,独在机中织。



探索


凡实践,需理论先行,在开始之前,我们要先具体了解一下new创建对象的具体过程。


new的this指向


或者说构造函数的this指向,先来看一个小的示例,思考一下,log打印出来的是什么?


function Person(name, age) {
this.name = name;
this.age = age;
}

let person = new Person("后俊生", 18);
console.log(person.name); //后俊生
console.log(person.age); //18

很显然,大家都知道打印出来的分别是"后俊生", 18,那么,你有没有思考过这样的简单问题,为什么打印出来的是这些数据?


->我明明把参数传递给了构造函数Person,而不是实例person?参数为什么会附加到实例上边去了?


OK,带着这些思考,我们将代码稍稍改动,思考一下,打印出来的会是什么?


function Person(name, age) {
this.name = name;
this.age = age;
}

let person = new Person("后俊生");
console.log(person.name); //后俊生
console.log(person.age); //undefined

结果是"后俊生", undefined,我们把函数中this赋值语句注释,实例中的属性就没了,好像这两句话是给实例赋值的?是不是有了一些眉目了?


既然this.name = name是给实例person复制的,那么是不是this.name就是person.name,是不是this = person


bingo~,恭喜你,答对了,


构造函数中的this,指向的是实例本身!!!


构造函数的原型


我们将代码继续改造,向他的原型链上添加数据


function Person(name, age) {
this.name = name;
this.age = age;

function logIfo() {
console.log(age, 1);
return 1;
}
}

Person.prototype.habit = "Games";
Person.prototype.sayHi = function() {
console.log("Hi " + this.name);
};

let person = new Person("后俊生", 18);
console.log(person.name); //后俊生
console.log(person.age); //18
console.log(person.habit); //Games
person.sayHi(); //Hi 后俊生

由上面的代码,不难发现,当函数被使用new创建的时候,构造函数的原型链上的数据也会被添加到实例上。


返回值


以上都是没有返回值的情况,那么,如果函数有返回值呢?


那么我们将代码再次改造一下:


function Person(name, age) {
this.name = name;
this.age = age;

return {
hair: 'black',
gender: 'man'
}
}

let person = new Person("后俊生", 18);
console.log(person.name); //undefined
console.log(person.age); //undefined
console.log(person.hair); //black
console.log(person.gender); //man

我们发现,实例person上不存在name、age属性了,只包含返回对象的属性,好像我们构造的是返回对象的实例,那么,真的是这样吗?


再来看看这个代码


function Person(name, age) {
this.name = name;
this.age = age;

return 1;
}

let person = new Person("后俊生", 18);
console.log(person.name); //后俊生
console.log(person.age); //18

咦?什么情况,为什么这次又存在name、age属性了?


事实上: 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。


new方法思路


我们来总结一下new方法做的事情:



  1. 改边this,指向实例

  2. 将构造函数的原型复制到实例上

  3. 根据返回值类型决定实例的属性


这就是我们的new方法需要实现的功能,


最终实现:


之前写过一下实现方式,功能一样,但是不够优雅,这是我见过最优雅的解决方案,三行代码解决问题


function _new(fn, ...arg) {
//以一个现有对象作为原型,创建一个新对象,继承fn原型链上的属性
const obj = Object.create(fn.prototype);
// 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
const ret = fn.apply(obj, arg);
// 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
return ret instanceof Object ? ret : obj;
}

测试


我们来做一下测试:


function Person(name, age) {
this.name = name;
this.age = age;

return {
hair: 'black',
gender: 'man'
}
}

let person = _new(Person,"后俊生", 18);
console.log(person.name); //undefined
console.log(person.age); //undefined
console.log(person.hair); //black
console.log(person.gender); //man

function Person(name, age) {
this.name = name;
this.age = age;

return 1;
}
Person.prototype.habit = "Games";

let person = _new(Person,"后俊生", 18);
console.log(person.name); //后俊生
console.log(person.age); //18
console.log(person.habit); //Games

发现,和我们使用new方法的结果一模一样,至此,new方法实现完成。


注意


这里的_new方法只能传入函数,不能传入class,因为class在使用apply时会报错。


const ret = fn.apply(obj, arg);
^

TypeError: Class constructor Person cannot be invoked without 'new'

引用


面试官问:能否模拟实现JS的new操作符 - 掘金


Object.create() - JavaScript | MDN


github.com/mqyqingfeng…


作者:十里八乡有名的后俊生
来源:juejin.cn/post/7280436307914309672
收起阅读 »

H5 下拉刷新如何实现

web
H5 下拉刷新如何实现 最近我需要做一个下拉刷新的功能,实现功能后我发现,它需要处理的情况还蛮多,于是我整理了这篇文章。 下图是我实现的效果,分为三步:开始下拉时,屏幕顶部会出现加载动画;加载过程中,屏幕顶部高度保持不变;加载完成后,加载动画隐藏。 首先我会...
继续阅读 »

H5 下拉刷新如何实现


最近我需要做一个下拉刷新的功能,实现功能后我发现,它需要处理的情况还蛮多,于是我整理了这篇文章。


下图是我实现的效果,分为三步:开始下拉时,屏幕顶部会出现加载动画;加载过程中,屏幕顶部高度保持不变;加载完成后,加载动画隐藏。


pull-down.gif


首先我会讲解下拉的原理、根据原理写出初始代码;然后我会说明代码存在的缺陷、解决缺陷并做些额外优化;最后我会给出完整代码,并做一个总结。


下拉的原理


prinple.png


如图所示,蓝色框代表视口,绿色框代表容器,橙色框代表加载动画。最开始时,加载动画处于视口外;开始下拉之后,容器向下移动,加载动画从上方进入视口;结束下拉后,容器又开始向上移动,加载动画也从上方退出视口。


下拉基础代码


知道原理,我们现在开始写实现代码,首先是布局的代码:


布局代码


我们把 box 元素当作容器,把 loader-box,loader-box + loading 元素当作动画,至于 h1 元素不需要关注,我们只把它当作操作提示。


<div id="box">
<div class="loader-box">
<div id="loading"></div>
</div>
<h1>下拉刷新 ↓</h1>
</div>

loader-box 的高度是 80px,按上一节原理中的分析,初始时我们需要让 loader-box 位于视口上方,因此 CSS 代码中我们需要把它的位置向上移动 80px。


.loader-box {
position: relative;
top: -80px;
height: 80px;
}

loader-box 中的 loader 是纯 CSS 的加载动画。我们利用 border 画出的一个圆形边框,左、上、右边框是浅灰色,下边框是深灰色:


loader.png


#loader {
width: 25px;
height: 25px;
border: 3px solid #ddd;
border-radius: 50%;
border-bottom: 3px solid #717171;
transform: rotate(0deg);
}

开始刷新时,我们给 loader 元素增加一个动画,让它从 0 度到 360 度无限旋转,就实现了加载动画:


loading.gif


#loader.loading {
animation: loading 1s linear infinite;
}

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

逻辑代码


看完布局代码,我们再看逻辑代码。逻辑代码中,我们要监听用户的手指滑动、实现下拉手势。我们需要用到三个事件:



touchstarttouchmove 事件中我们可以获取手指的坐标,比如 event.touches[0].clientX 是手指相对视口左边缘的 X 坐标,event.touches[0].clientY 是手指相对视口上边缘的 Y 坐标;从 touchend 事件中我们则无法获得 clientXclientY


我们可以先记录用户手指 touchstart 的 clientY 作为开始坐标,记录用户最后一次触发 touchmove 的 clientY 作为结束坐标,二者相减就得到手指移动的距离 distanceY。


设置手指移动多少距离,容器就移动多少距离,就得到了我们的逻辑代码:


const box = document.getElementById('box')
const loader = document.getElementById('loader')
let startY = 0, endY = 0, distanceY = 0

function start(e) {
startY = e.touches[0].clientY
}

function move(e) {
endY = e.touches[0].clientY
distanceY = endY - startY
box.style = `
transform: translateY(${distanceY}px);
transition: all 0.3s linear;
`

}

function end() {
setTimeout(() => {
box.style = `
transform: translateY(0);
transition: all 0.3s linear;
`

loader.className = 'loading'
}, 1000)
}

box.addEventListener('touchstart', start)
box.addEventListener('touchmove', move)
box.addEventListener('touchend', end)

逻辑代码实现一个简陋的下拉效果,当然现在还有很多缺陷。


pull-down-basic.gif


简陋下拉效果的 6 个缺陷


之前我们实现了简陋的下拉效果,它还需要解决 6 个缺陷,才能算一个完善的功能。


没有最小、最大距离限制


第一个缺陷是,下拉没有做最小、最大距离的限制。


通常来说,我们下拉屏幕时,距离太小应该不能触发刷新,距离太大也不行,下滑到一定距离后,就应该无法继续下滑。


因此我们可以给下拉设置最小距离限制 DISTANCE_Y_MIN_LIMIT、最大距离限制 DISTANCE_Y_MAX_LIMIT。如果 touchend 中发现下拉距离小于最小距离,直接不触发加载;如果 touchmove 中下拉距离超过最大距离,页面只向下移动最大距离。


解决缺陷关键代码如下:


const DISTANCE_Y_MAX_LIMIT = 150
DISTANCE_Y_MIN_LIMIT = 80

function move(e) {
endY = e.touches[0].clientY
distanceY = endY - startY
if (distanceY > DISTANCE_Y_LIMIT) {
distanceY = DISTANCE_Y_LIMIT
}
box.style = `
transform: translateY(${distanceY}px);
transition: all 0.3s linear;
`

}

function end() {
if (distanceY < DISTANCE_Y_MIN_LIMIT) {
box.style = `
transform: translateY(0px);
transition: all 0.3s linear;
`

return
}
...
}

加载动画没有停留在视口顶部


第二个缺陷是,下拉没有让加载动画停留在视口顶部。


我们可以把 end 函数加以改造,在数据还没有加载完成时(用 setTimeout 模拟的),让加载动画 style 的 translateY 一直是 80px,translateY(80px) 可以和 初始 CSS 的 top: -80px; 相互抵消,让动画在未刷新完成前停留在视口顶部。


function end() {
...
box.style = `
transform: translateY(80px);
transition: all 0.3s linear;
`

loader.className = 'loading'
setTimeout(() => {
box.style = `
transform: translateY(0px);
transition: all 0.3s linear;
`

loader.className = ''
}, 1000)
}

重复触发


第三个缺陷是,下拉可以重复触发。


正常来说,如果我们已经下拉过,数据正在加载中时,我们不能继续下拉。


我们可以增加一个加载锁 loadLock。当加载锁开启时,start,move 和 end 事件都不会触发。


let loadLock = false

function start(e) {
if (loadLock) { return }
...
}

function move(e) {
if (loadLock) { return }
...
}

function end(e) {
if (loadLock) { return }
...
setTimeout(() => {
...
loadLock = true
...
}, 1000)
}

没有限制方向


第四个缺陷是,没有限制方向。


目前我们的代码,用户上拉也能触发。我们可以增加判断,当 endY - startY 小于 0 时,阻止 touchmovetouchend 的逻辑。


function move(e) {
...
if (endY - startY < 0) { return }
...
}

function end() {
if (endY - startY < 0) { return }
...
}

你可能会疑惑,为什么我宁愿写多个判断拦截,也不取消监听事件。这是因为一旦取消监听事件,我们需要考虑在一个合适的时间重新监听,这会把问题变得更复杂。


没有阻止原生滚动


第五个缺陷时,我们在加载数据时没有阻止原生滚动。


虽然我们已经阻止了重复下拉,touchmove 和 touchend 事件被拦截了,但是 H5 原生滚动还能用。


我们可以在刷新时给 body 设置一个 overflow: hidden; 属性,刷新结束后清除 overflow: hidden,这样就可以阻止原生滚动。


body.overflowHidden {
overflow: hidden;
}

const body = document.body
function end() {
...
box.style = `
transform: translateY(80px);
transition: all 0.3s linear;
`

loader.className = 'loading'
body.className = 'overflowHidden'
setTimeout(() => {
...
box.style = `
transform: translateY(0px);
transition: all 0.3s linear;
`

loader.className = ''
body.className = ''
}, 1000)
}

没有阻止 iOS 橡皮筋效果


第 6 个缺陷是,没有阻止 iOS 的橡皮筋效果。


iOS 浏览器默认滑动时有一个橡皮筋效果,我们需要阻止它,避免影响我们的下拉手势。阻止方式就是给监听器设置 passive: false


function addTouchEvent() {
box.addEventListener('touchstart', start, { passive: false })
box.addEventListener('touchmove', move, { passive: false })
box.addEventListener('touchend', end, { passive: false })
}

addTouchEvent()

解决完 6 个缺陷后,我们已经得到无缺陷的下拉刷新功能,但离丝滑的下拉刷新还有一段距离。我们还可以做一些优化,让下拉刷新更完善。


优化


我们可以做两个优化,第一个优化是添加阻尼效果:


增加阻尼效果


所谓阻尼效果,就是下拉过程我们可以感受到一股阻力的存在,虽然我们下拉力度是一样的,但距离的增加速度变慢了。用物理术语表示的话,就是加速度变小了。


体现到代码上,我们可以设置一个百分比,百分比会随着下拉距离增加而减少,把百分比乘以距离当作最后的距离。


代码中百分比 percent 设为 (100 - distanceY * 0.5) / 100,当 distanceY 越来越大时,百分比 percent 越来越小,最后再把 distanceY * percent 赋值给 distanceY


function move(e) {
...
distanceY = endY - startY
let percent = (100 - distanceY * 0.5) / 100
percent = Math.max(0.5, percent)
distanceY = distanceY * percent
if (distanceY > DISTANCE_Y_MAX_LIMIT) {
distanceY = DISTANCE_Y_MAX_LIMIT
}
...
}

利用角度判断用户下拉意图


第二个优化是利用角度判断用户下拉意图。


下图展示了两种用户下拉的情况,β 角度比 α 角度小,角度越小用户下拉意图越明显、误触的可能性更小。


intension.png


我们可以利用反三角函数求出角度来判断下拉意图。


JavaScript 中,反正切函数是 Math.atan(),需要注意的是,反正切函数算出的是弧度,我们还需要将它乘以 180 / π 才能获取角度。


下面的代码中,我们做了一个限制,只有角度小于 40 时,我们才认为用户的真实意图是想要下拉刷新。


const DEG_LIMIT = 40
function move(e) {
...
distanceY = endY - startY
distanceX = endX - startX
const deg = Math.atan(Math.abs(distanceX) / distanceY)
* (180 / Math.PI)
if (deg > DEG_LIMIT) {
[startY, startX] = [endY, endX]
return
}
...
}

代码示例


你可以在 codepen 中查看效果,web 端需要按 F12 用手机浏览器打开。


codepen.gif


总结


本文讲解了下拉的原理、并根据原理写出初始代码。在初始代码的基础上,我解决了 6 个缺陷、做了 2 个优化,实现了一个完善的下拉刷新效果。


作者:小霖家的混江龙
来源:juejin.cn/post/7340836136208859174
收起阅读 »

你真的熟悉HTML标签吗?--“看不见”却有用的标签

web
HTML标签,前端工程师在开发页面时经常使用。但往往关注更多的是页面渲染效果及交互逻辑,比如表单、菜单栏、列表、图文。还有一些非常重要却容易被忽视的标签,这些标签大多数用在页面头部head标签内,在某些场景下,比如交互实现、性能优化、搜索优化。合理利用它们可以...
继续阅读 »

HTML标签,前端工程师在开发页面时经常使用。但往往关注更多的是页面渲染效果及交互逻辑,比如表单、菜单栏、列表、图文。还有一些非常重要却容易被忽视的标签,这些标签大多数用在页面头部head标签内,在某些场景下,比如交互实现、性能优化、搜索优化。合理利用它们可以达到事半功倍的效果。


交互实现


提倡一个编码原则:Less code,less bug,提倡编码简约


meta标签



自动刷新/跳转



在使用它的时候,刷新和跳转操作是不可取消的,对刷新时间间隔或者需要手动取消的,推荐使用JavaScript定时器来实现



如果只是想实现页面的定时刷新或跳转。(比如某些页面缺乏访问权限,在X秒后跳回首页这样的场景),建议实践下meta标签的用法



PPT自动播放


要实现PPT自动播放的功能,只需要在每个页面的meta标签内设置好下一个页面的地址即可


<!-->五秒后自动跳转到page2.html页面<-->
<meta http-equiv="Refresh" content="5;URL=page2.html">

刷新大屏



比如:每隔一分钟就需要刷新页面的大屏幕监控,也可以通过meta标签来实现,只需去掉后面的URL即可



<meta http-equiv="Refresh" content="60">

title标签与Hack手段



B/S架构的优点:版本更新方便、跨平台、跨终端。但在处理某些场景,比如即时通信场景时,会变得比较麻烦。因为前后端通信深度依赖HTTP协议,而HTTP协议采用“请求-响应”模式。一种低效的解决方案是客户端通过轮询机制获取最新消息 (HTML5下可使用WebSocket协议)



消息提醒



消息提醒功能实现比较困难。HTML5标准发布之前,浏览器没有开放图标闪烁、弹出系统消息之类的接口,只能借助一些Hack的手段,比如修改title标签来达到类似的效果,(HTML5下可使用Web Notifications API弹出系统消息)



// 通过定时修改title内容 模拟了消息提醒闪烁
let msgNum = 1 //消息条数
let cnt = 0 //计数器
const inerval = setInterval(() => {
 cnt = (cnt + 1) % 2
 if (msgNum === 0) {
   document.title += `聊天页面` //通过DOM修改title
   clearlnterval(inerval)
   return
}
 const prefix = cnt % 2 ? `新消息(${msgNum}` ''
 document.title = `${prefix}聊天页面`
}, 1000)

image_0.2333475722562257.gif
定时修改title标签内容,可以制作其他动画效果,比如文字滚动,但需要注意浏览器会对title标签文本进行去空格操作。动态修改title标签可以将一些关键信息显示到标签上(比如下载时的进度、当前操作步骤)


性能优化



性能问题的两方面原因:渲染速度慢、请求时间长。合理地使用标签,可以在一定程度上提升渲染速度以及减少请求时间



script标签



调整加载顺序提升渲染速度。


浏览器的底层渲染机制中:当渲染引擎在解析HTML时,若遇到script标签引用文件,则会暂停解析过程,同时通知网络线程加载文件,文件加载后会切换至JavaScript引擎来执行对应代码,代码执行完成之后切换至渲染引擎继续渲染页面。


可以看出页面渲染过程中包含了,请求文件以及执行文件的时间。但页面的首次渲染可能并不依赖这些文件,所以请求文件和执行文件的动作反而延长了页面渲染的时间。为了减少这些损耗,可以借助script的属性来实现



asyc属性



立即请求文件,但不阻塞渲染引擎,文件加载完毕后阻塞渲染引擎并立即执行文件内容



defer属性



立即请求文件,但不阻塞渲染引擎,等到解析完HTML之后再执行文件内容



HTML5标准type属性--对应值为“module”



让浏览器按照ECMA Script6标准将文件当作模块进行解析,默认阻塞效果同defer,也可以配合async在请求完成后立即执行



image.png


从图中得知:采用三种属性都能减少请求文件引起的阻塞时间,只有defer、type=“module”属性能保证渲染引擎优先执行,从而减少执行文件内容消耗的时间。


当渲染引擎解析HTML遇到script标签引入文件时,会立即进行一次渲染



这也是为什么script放在底部的原因:构建工具会把编译好的引用JavaScript代码的script标签放入到body标签底部 当渲染引擎执行到body底部时会先将已解析的内容渲染出来,然后再去请求相应的JavaScript文件,如果是内联脚本(即不通过src属性引用外部脚本文件直接在HTML编写JavaScript代码的形式),渲染引擎则不会渲染



link标签



通过预处理提升渲染速度。在对大型单页应用进行性能优化时,会用到按需、懒加载的方式来加载对应的模块。但如果能使用link标签的预加载,就能进一步的提升加载速度。



rel = “dns-prefetch”



当link标签的rel属性值为“dns-prefetch”时,浏览器会对某个域名预先进行DNS解析并缓存。如此当浏览器在请求同域名资源时,能省去从域名查询IP的过程从而减少时间消耗



<!-->淘宝网的DNS解析<-->
<link rel="dns-prefetch" href="//g.alicdn.com">
<link rel="dns-prefetch" href-"L/img.alicdn.com">
<link rels"dns-prefetch" href="_/tce.alicdn.com">
<link rel="dns-prefetch" href="L/gm.mmstat.com">
<link ref="dns-prefetch" href="//tce.taobao.com">
<link "dns-prefetch" href="//log.mmstat.com">
<link rel="dns-prefetch" href="L/tui.taobao.com">
<link rel="dns-prefetch" href="//ald.taobao.com">
<link rel="dns-prefetch" href="L/gw.alicdn.com">
<link rel="dns-prefetch" href="L/atanx.alicdn.com">
<link "dns-prefetch" hrefs"_/dfhs.tanx.com">
<link rel="dns-prefetch" href="L/ecpm.tanx.com">
<link rel="dns-prefetch" href="//res.mmstat.com">

preconnect



让浏览器在一个HTTP请求正式发给服务器前预先执行一些操作 包括DNS解析、TLS协商、TCP握手,通过消除往返延迟来为用户节省时间



prefetch/preload



两个值都是让浏览器预先下载并缓存某个资源,但不同的是,prefetch可能会在浏览器忙时被忽略,而preload则是一定会被预先下载



prerender



浏览器不仅会加载资源,还会解析执行页面,进行预渲染



<link rel="preconnect" href="L/atanx.alicdn.com">
<link rel-"prefetch" hrefs"_/dfhs.tanx.com">
<link rel="preload" href="L/ecpm.tanx.com">
<link rel="prerender" href="//res.mmstat.com">

搜索优化


meta标签



提取关键信息。


这些描述信息是通过meta标签专门为搜索引擎设置的。目的是方便用户预览搜索到的结果



<meta content="拉勾,拉勾网,拉勾招聘,拉钩,拉钩网,互联网招聘,拉勾互联网招聘,移动互联网招聘,垂直互联网招聘,微信招聘,微博招聘,拉勾官网,拉勾百科,跳槽,高薪职位,互联网圈子,T招聘,职场招聘,猎头招聘,O2O招聘,LBS招聘,社交招聘,校园招聘,校招,社会招聘,社招"name="keywords">

在实际工作中推荐使用一些关键字工具来挑选,比如Google Trends、站长工具


link标



减少重复


对于同一个页面会有多个网址,又或者存在某些重定向页面,比如:`xx.com/a.htmlxx.com/detail?id="abcd"



合并网址的方式:比如使用站点地图,或者在HTTP请求响应头部添加rel="canonical"


<link href="https://xx.com/a.html"rel="canonical">

知识支撑


浏览器获取资源过程



浏览器获取资源过程解析



image.png


OGP(Open Graph Protocal,开放图表协议)



OGP是Facebook公司在2010年提出的,目的是通过增加文档信息来提升社交网页在被分享时的预览效果,只需要在一些分享页面中添加一些meta标签及属性,支持OGP协议的社交网站就会在解析页面时生成丰富的预览信息,比如站点名称、网页作者、预览图片


官方网站



微信文章支持OPG协议代码



通过mate标签属性值,声明了网址、预览图片、描述信息、站点名称、网页类型、作者等一系列信息



image.png

最后一句: 说一说你还知道哪些“看不见”的标签及用法?

学习心得!若有不正,还望斧正。


作者:沉曦
来源:juejin.cn/post/7246280283556380709
收起阅读 »

独立开发的 APP 卖不好恐怕不是价格的问题

分享一下我对独立 App 定价的一些个人经验。 新人独立开发最大的误区是产品购买率差是价格定的高了。有可能是价格高了,但是大概率不是这个问题。我看到很多人降价、促销、限免,这种没有后手的非策略型降价其实帮助不大。 从付费角度我粗暴的把用户分为三类:一种是有付费...
继续阅读 »

分享一下我对独立 App 定价的一些个人经验。


新人独立开发最大的误区是产品购买率差是价格定的高了。有可能是价格高了,但是大概率不是这个问题。我看到很多人降价、促销、限免,这种没有后手的非策略型降价其实帮助不大。


从付费角度我粗暴的把用户分为三类:一种是有付费习惯的用户,这类用户你产品满足他的点,他就付钱。一种是无论如何都不愿意为这种 app 付钱的,他们宁愿看 30 秒广告,宁愿连续签到 30 天,不收费不给用马上就去下其他免费 app 了。只有一小部分是观望用户,有可能觉得贵了,有可能现在没付费意愿,但是有点想买。


对于优质付费用户,app 的价格浮动 20% 没感知的。关键在于你给他的价格锚点是多少。这就很看你的产品定位和产品的质感传达。一瓶水在便利店和在五星级酒店价格不一样,关键在于价格的感知。所以如果你的产品的商业模式主要是高质量的付费用户,重点不是便宜,重点是让他们觉得值。


对于铁了心的不花钱的用户,如果你没有广告,那么这些用户只有传播价值。所以给到最低使用门槛后,找到一个产品机制让他们产生传播可能。否则这些用户应该果断无视。


对于摇摆观望类,如果能做到定向优惠是很好的手段。但是我没搞这种精细化运营,我一般的做法是画饼和吃定心丸。我会说未来半年我还要做这么多功能,我保证未来价格不打折,我保证明年这个时候涨价。一般观望里真的有购买动机的,这么一说之后 50% 的就会说那我现在买吧。


降价促销其实是一个双刃剑,很多人看到了他的好处,却没有看到他的伤害。伤害的是原价买的老用户,伤害的是本来就会买的人的收入减损,伤害的是产品的价格锚。只要一次促销,用户知道了以后,你的原价就再也没说服力了。因此降价促销只在你能确保这个带来的销量增长远大于这些折损的商誉价格。如果做不到,那最好就是定向优惠。你的促销价就不能公开给已经买了的人和已经是用户的人知道。


作者:独立开花卓富贵
来源:juejin.cn/post/7343132617967976500
收起阅读 »

为了解决一个bug我读了iview源码

web
前言 “小蚂蚁,你这个页面不对啊,输入框有值但还是提示没有填写数据”,测试妹子对我说,我一点也不耐烦地说:“应该不可能出现这种问题,再说了你这没有截图啊,没图没真相啊”,妹子马上发来截图:看这就是你要的截图。 我低下头说:“我看看吧”,这一看不要紧从早晨看到...
继续阅读 »

前言


“小蚂蚁,你这个页面不对啊,输入框有值但还是提示没有填写数据”,测试妹子对我说,我一点也不耐烦地说:“应该不可能出现这种问题,再说了你这没有截图啊,没图没真相啊”,妹子马上发来截图:看这就是你要的截图。


截屏2023-08-09 22.41.58.png


我低下头说:“我看看吧”,这一看不要紧从早晨看到下午,从下午看到下班,还是没看出来哪里有问题啊!既然看不出来问题,那么就去看看源码,接下来一顿操作猛如虎;


深入iview源码


有时候迫不得已必须去看源码,源码有助于分析问题的本质;我这个表单页面用的vue2+iview,那么这就用到了Form和FormItem组件,问题肯定出在他们身上;但是怎么从本地代码断点调试到iview源码呢?


这就需要用到软链,我们在本地clone一份iview源码,然后修改packagejson中的main字段,改成src/index.js,这样就可以调试源码了,然后执行npm link;呀,发现报错了,原来iview还在使用webpack3,gulp的版本比较低,所以node也需要降低到11.15.0这个版本才行,降了版本再执行就OK了;


再到项目文件夹下执行npm link iview,成功链接到iview源码上去了,发现有个别源码的错误,凭借感觉先修复一下,比如下面这个改成esm导出:


截屏2023-08-09 22.52.04.png


找到执行校验的函数:一般都是调用$refs.form.validate方法,打断点


截屏2023-08-09 22.53.45.png


点击提交,成功进入断点,接下来就是秀操作的时候了,F11进入函数:


2023-08-11-20.12.09.webp


可以看到最关键的几行代码:


const validator = new AsyncValidator(descriptor);
let model = {};

model[this.prop] = this.fieldValue;

validator.validate(model, { firstFields: true }, errors => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';

callback(this.validateMessage);
});

iview使用的校验工具是async-validator,相信很多同学没有使用过这个库,那么我们就来看一看:


async-validator


创建一个mjs,直接使用node跑一下这个脚本:


import Schema from "async-validator";

const schema = new Schema.default({
name: {
required: true,
message: "姓名不能为空",
},
age: {
required: true,
message: "年龄不能为空",
},
});

schema.validate(
{
name: "张三",
age: 18,
},
undefined,
(errors) => {
console.log(errors);
}
);


发现没有报错,配置里面支持配置数据的类型,就是相当于在js这个弱类型语言里面加上强类型校验,这在几大框架里面都有做这件事情,js开发者永远都向往着强类型,给age加上一个string类型那么这个时候就会报错[ { message: '年龄不能为空', fieldValue: 18, field: 'age' } ]


    age: {
+ type: "string",
required: true,
message: "年龄不能为空",
},

再回到之前的问题上,有没有可能就是类型判断的错误导致即使填充了数据也报错,再打印errors看一看,发现果然如此,FormItem组件中validator中的type默认为string,而赋予的默认值为number类型,导致类型校验不通过而报错;


这也引发我们深思,后端都是强类型语言,比方说有些场景我们需要输入一些数字,这个时候input框内获取到的必然都是字符串,而后端返回给我们的必然又是number类型,这就导致输入输出类型不一致了,本来对于js这个弱类型语言,这一点完全没有必要纠结,反正字符串和数字他们之间隐式转换可以随便转;所以就我而言,js没有必要把字符串和数字类型限制得这么死,就像async-validator一样,弱类型不需要强校验


同样的async-validator为什么iview就有问题?稍有经验的同学应该一眼就能看出来,那必然是版本的问题,我们安装iview使用的版本:1.12.2,然后执行同样的脚本报错:[ { message: '年龄不能为空', field: 'age' } ],我们再深入到源码里面去看就知道它是默认设置为string类型了,这样的话如果不指定类型,就会当做string类型来校验,是不是感觉好像找到了一个bug,我本来以为可以提一个PR,但是后来想一想找个问题在于async-validator,给iview提PR好像也没什么用啊,而且async-validator在高版本已经解决了这个问题;


后记


在这个解决问题的过程中,我翻看了iview源码,学会了通过软链来调试源码的技巧,同时还掌握了一个异步校验工具:async-validator,可谓一箭双雕


另外我们还发现一个规律就是,在js的世界里面其实string和number这两种类型傻傻分不清,因为他们大部分情况下都可以隐式转换,所以在开发的时候需要格外注意,如果有用到强类型校验的话,我们就需要保证我们变量的类型一致性,比如说一个变量它是string类型那么就不能赋值number类型的变量


最后我顺利地解决了这个问题,虽然这个时候已经“夕阳西下”,我还是硬着头皮去找测试说:下班之前给它解决了,该你上了!然后就是测试测完了,我们顺利地上线了。


作者:蚂小蚁
来源:juejin.cn/post/7266340931359424551
收起阅读 »

前端代码重复度检测

web
在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd。 jscpd简介 jscpd是一款开源的JavaScr...
继续阅读 »

在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd


jscpd简介


jscpd是一款开源的JavaScript的工具库,用于检测代码重复的情况,针对复制粘贴的代码检测很有效果。它可以通过扫描源代码文件,分析其中的代码片段,并比较它们之间的相似性来检测代码的重复度。jscpd支持各种前端框架和语言,包括HTML、CSS和JavaScript等150种的源码文件格式。无论是原生的JavaScript、CSS、HTML代码,还是使用typescriptscssvuereact等代码,都能很好的检测出项目中的重复代码。


开源仓库地址:github.com/kucherenko/jscpd/tree/master


如何使用


使用jscpd进行代码重复度检测非常简单。我们需要安装jscpd。可以通过npmyarn来安装jscpd


npm install -g jscpd

yarn global add jscpd

安装完成后,我们可以在终端运行jscpd命令,指定要检测的代码目录或文件。例如,我们可以输入以下命令来检测当前目录下的所有JavaScript文件:


jscpd .

指定目录检测:


jscpd /path/to/code

在命令行执行成功后的效果如下图所示:



简要说明一下对应图中的字段内容:



  • Clone found (javascript):
    显示找到的重复代码块,这里是javascript文件。并且会显示重复代码在文件中具体的行数,便于查找。

  • Format:文件格式,这里是 javascript,还可以是 scss、markup 等。

  • Files analyzed:已分析的文件数量,统计被检测中的文件数量。

  • Total lines:所有文件的总行数。

  • Total tokens:所有的token数量,一行代码一般包含几个到几十个不等的token数量。

  • Clones found:找到的重复块数量。

  • Duplicated lines:重复的代码行数和占比。

  • Duplicated tokens:重复的token数量和占比。

  • Detection time:检测耗时。


工程配置


以上示例是比较简单直接检测单个文件或文件夹。当下主流的前端项目大多都是基于脚手架生成或包含相关前端工程化的文件,由于很多文件是辅助工具如依赖包、构建脚本、文档、配置文件等,这类文件都不需要检测,需要排除。这种情况下的工程一般使用配置文件的方式,通过选项配置规范 jscpd 的使用。


jscpd 的配置选项可以通过以下两种方式创建,增加的内容都一致无需区分对应的前端框架。


在项目根目录下创建配置文件 .jscpd.json,然后在该文件中增加具体的配置选项:


    {
"threshold": 0,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true
}

也可直接在 package.json 文件中添加jscpd


    {
...
"jscpd": {
"threshold": 0.1,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true,
"gitignore": true
}
...
}

简要介绍一下上述配置字段含义:



  • threshold:表示重复度的阈值,超过这个值,就会输出错误报警。如阈值设为 10,当重复度为18.1%时,会提示以下错误❌,但代码的检测会正常完成。


ERROR: jscpd found too many duplicates (18.1%) over threshold (10%)


  • reporters:表示生成结果检测报告的方式,一般有以下几种:

    • console:控制台打印输出

    • consoleFull:控制台完整打印重复代码块

    • json:输出 json 格式的报告

    • xml:输出 xml 格式的报告

    • csv:输出 csv 格式的报告

    • markdown:输出带有 markdown 格式的报告

    • html:生成html报告到html文件夹

    • verbose:输出大量调试信息到控制台



  • ignore:检测忽略的文件或文件目录,过滤一些非业务代码,如依赖包、文档或静态文件等

  • format:需要进行重复度检测的源代码格式,目前支持150多种,我们常用的如 javascript、typescript、css 等

  • absolute:在检测报告中使用绝对路径


除此之外还有很多其他的配置,有兴趣的可以看源码文档中有详细的介绍。


检测报告


完成以上jscpd配置后执行以下命令即可输出对应的重复检测报告。运行完毕后,jscpd会生成一个报告,展示每个重复代码片段的信息。报告中包含了重复代码的位置、相似性百分比和代码行数等详细信息。通过这些信息,我们可以有针对性的进行代码重构。


jscpd ./src -o 'report'

项目中的业务代码通常会选择放在 ./src 目录下,所以可以直接检测该目录下的文件,如果是放在其他目录下根据实际情况调整即可。
通过命令行参数-o 'report'输出检测报告到项目根目录下的 report 文件夹中,这里的report也可以自定义其他目录名称,输出的目录结构如下所示:



生成的报告页面如下所示:


项目概览数据:



具体重复代码的位置和行数:



默认检测重复代码的行数(5行)和tokens(50)比较小,所以产生的重复代码块可能比较多,在实际使用中可以针对检测范围进行设置,如下设置参数供参考:



  • 最小tokens:--min-tokens,简写 -k

  • 最小行数:--min-lines,简写 -l

  • 最大行数:--max-lines,简写 -x


jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'

为了更便捷的使用此命令,可将这段命令集成到 package.json 中的 scripts 中,后续只需执行 npm run jscpd 即可执行检测。如下所示:


"scripts": {
...
"jscpd": "jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'",
...
}

忽略代码块


上面所提到的ignore可以忽略某个文件或文件夹,还有一种忽略方式是忽略文件中的某一块代码。由于一些重复代码在实际情况中是必要的,可以使用代码注释标识的方式忽略检测,在代码的首尾位置添加注释,jscpd:ignore-startjscpd:ignore-end 包裹代码即可。


在js代码中使用方式:


/* jscpd:ignore-start */
import lodash from 'lodash';
import React from 'react';
import {User} from './models';
import {UserService} from './services';
/* jscpd:ignore-end */

在CSS和各种预处理中与js中的用法一致:


/* jscpd:ignore-start */
.style {
padding: 40px 0;
font-size: 26px;
font-weight: 400;
color: #464646;
line-height: 26px;
}
/* jscpd:ignore-end */

在html代码中使用方式:


<!--
// jscpd:ignore-start
-->

<meta data-react-helmet="true" name="theme-color" content="#cb3837"/>
<link data-react-helmet="true" rel="stylesheet" href="https://static.npmjs.com/103af5b8a2b3c971cba419755f3a67bc.css"/>
<link data-react-helmet="true" rel="apple-touch-icon" sizes="120x120" href="https://static.npmjs.com/58a19602036db1daee0d7863c94673a4.png"/>
<link data-react-helmet="true" rel="icon" type="image/png" href="https://static.npmjs.com/b0f1a8318363185cc2ea6a40ac23eeb2.png" sizes="32x32"/>
<!--
// jscpd:ignore-end
-->


总结


jscpd是一款强大的前端本地代码重复度检测工具。它可以帮助开发者快速发现代码重复问题,简单的配置即可输出直观的代码重复数据,通过解决重复的代码提高代码的质量和可维护性。


使用jscpd我们可以有效地优化前端开发过程,提高代码的效率和性能。希望本文能够对你了解基于jscpd的前端本地代码重复度检测有所帮助。




看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


作者:南城FE
来源:juejin.cn/post/7288699185981095988
收起阅读 »

前端操作:用户首次登录强制更改密码

web
用户首次登录强制更改密码 这个系统每个新用户的初始密码都是后端分配的123456,要求用户初次登录此系统,就强制要求用户将密码修改为字母+数字,再用新密码才可登录; 封装了个组件,个人觉得校验规则写得比较全面,所以记录下分享给有需要的朋友;组件名为:First...
继续阅读 »

用户首次登录强制更改密码


这个系统每个新用户的初始密码都是后端分配的123456,要求用户初次登录此系统,就强制要求用户将密码修改为字母+数字,再用新密码才可登录;


封装了个组件,个人觉得校验规则写得比较全面,所以记录下分享给有需要的朋友;组件名为:FirstLoginDialog


先看效果图:


bc2f3f4f8ef8f13b6aa2a10ab5e2c6d.png


bbac9b40423d4e654fd6aa5c2099e90.png


da546cfcebb9819940c5086a5717b1a.png


实现思路:


1、在登陆跳转前判断密码是否为初始密码,如果是的话就弹框修改,否则直接登录首页


// 判断密码是否为初始密码 123456
if(this.loginForm.password == '123456'){
this.$message.error('您是首次登录系统,请修改初始密码!')
setTimeout(() => {
this.dialogTableVisible = true;
}, 1500);
} else {
this.$router.push({ path: this.redirect || "/" }).catch(() => {});
}

2、在修改密码的input校验规则中,设定必须要为字母+数字,不可再为123456;


login父组件:


<FirstLoginDialog :dialogTableVisible="dialogTableVisible" @handleClose="handleClose" :username="this.loginForm.username"></FirstLoginDialog>

FirstLoginDialog子组件:


<!-- 首次登录系统,修改密码弹框  -->
<template>
<el-dialog ref="dailog" width="40%" title="初始密码修改" show-close :visible="dialogTableVisible"
:before-close="close" :close-on-click-modal="false">

<!-- 进度条 -->
<el-steps :active="active" align-center>
<el-step title="初次登录" />
<el-step title="修改初始密码" />
<el-step title="完成" />
</el-steps>
<!-- 第1步展示 -->
<div v-if="active == 1" style="color: red;text-align: center;line-height: 30px;font-weight: 700;margin: 10px 0;">
您好,为了您的账号安全,请点击下一步修改初始密码
</div>
<!-- 第2、3步展示 -->
<div v-if="active != 3" style="width: 60%; margin: 0 auto;text-align:center">
<el-form ref="form" :model="user" status-icon :rules="rules">
<!-- 第1步展示 -->
<el-form-item v-show="active == 1" label="登陆账号">
<el-input v-model="username" :disabled="true" size="medium" />
</el-form-item>
<!-- 第2步展示 -->
<template v-if="active == 2">
<el-form-item label="旧密码" prop="oldPassword" class="password-item">
<el-input type="password" v-model="user.oldPassword" autocomplete="off" :show-password="true">
</el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPassword" class="password-item">
<el-input type="password" v-model="user.newPassword" autocomplete="off" :show-password="true"></el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPassword" class="password-item">
<el-input type="password" v-model="user.confirmPassword" :show-password="true" autocomplete="off"></el-input>
</el-form-item>
</template>
</el-form>
<!-- 第2、3步展示 -->
<div v-if="active != 1" slot="footer" class="dialog-footer">
<el-button @click="resetForm('form')">上一步</el-button>
<el-button type="primary" @click="submit('form')">下一步</el-button>
</div>
<!-- 第1步展示 -->
<div v-else slot="footer" class="dialog-footer">
<el-button type="primary" style="width:75%" @click="nextTip">下一步</el-button>
</div>
</div>
<!-- 第3步展示 -->
<div v-if="active === 3" class="ImgTip" style="text-align: center;margin: 0 auto;">
<div style="margin:20px 0">
<img v-if="isSuccess === true" src="@/assets/images/password_2.png" alt="">
<img v-else src="@/assets/images/password_1.png">
</div>
<p v-if="isSuccess === true" style="margin: 20px 0;">修改密码成功</p>
<p v-else style="margin: 20px 0;">网络开小差了,密码修改失败,请重新修改</p>
<el-button v-if="isSuccess === true" type="primary" @click="close">重新登录</el-button>
<el-button v-else type="primary" @click="again">重新修改</el-button>
</div>
<!-- 第2步展示 -->
<div v-if="active == 2" class="tip" style="color: red;margin-top: 20px;">
<h4>温馨提示</h4>
<p style="margin: 5px">1、密码长度不能低于6个字符</p>
<p style="margin: 5px">2、密码必须由数字、英文字符组成</p>
</div>
</el-dialog>
</template>

<script>
import { updateUserPwd } from "@/api/system/user";

export default {
props: {
dialogTableVisible: {
type: Boolean,
default: false
},
username: {
type: String,
default: ''
},
},
data () {
// 验证规则
// 是否包含一位数字
const regNumber = /(?=.*[\d])/;
// 是否包含一位字母
const regLetter = /(?=.*[a-zA-Z])/;
// 是否包含一位特殊字符
// const regCharacter = /(?=.*[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、])/
// 校验新密码
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('新密码不能为空!请重新输入'))
} else {
if (value.length > 16) {
callback(new Error('密码长度不超过16个字符。'))
} else if (value.length < 6) {
callback(new Error('密码长度不低于6个字符。'))
} else {
if (!/^[a-zA-Z\d]{1}/.test(value)) {
callback(new Error('密码必须以英文字母或数字开头!'))
} else {
if (!regNumber.test(value)) {
callback(new Error('密码必须由数字,英文字母组成!'))
} else if (!regLetter.test(value)) {
callback(new Error('密码必须由数字,英文字母组成!'))
} else {
callback()
}
}
}
}
}
var validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== this.user.newPassword) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
return {
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
},
isSuccess: false,
active: 1,
// 表单校验
rules: {
newPassword: [
{ required: true, validator: validatePass, trigger: "blur" }
],
confirmPassword: [
{ required: true, validator: validatePass2, trigger: "blur" }
],
oldPassword: [
{ required: true, message: "旧密码不能为空", trigger: "blur" }
],
},
};
},
methods: {
nextTip () {
this.active += 1
},
resetForm () {
this.active -= 1
},
again () {
this.active = 1
},
submit () {
this.$refs["form"].validate(valid => {
if (valid) {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(response => {
if (response.code == 200) {
this.isSuccess = true
} else {
this.isSuccess = false
}
this.active = 3
});
}
});
},
close () {
this.$emit('handleClose');
},
}
};
</script>
<style lang="scss" scoped></style>

作者:呼啦啦呼_
来源:juejin.cn/post/7236988255072223287
收起阅读 »

HTML开发工具和环境介绍,内附超详细的VS code安装教程!

工欲善其事必先利其器,一款好的开发工具可以让我们事半功倍。前面我们对HTML的相关概念和基本结构已经有了基本的了解,下面我们就来安装在前端开发中的需要使用的开发工具及环境。在众多HTML编辑器中,选择一个适合自己的工具至关重要。今天我们就来认识一下前端开发工作...
继续阅读 »

工欲善其事必先利其器,一款好的开发工具可以让我们事半功倍。前面我们对HTML的相关概念和基本结构已经有了基本的了解,下面我们就来安装在前端开发中的需要使用的开发工具及环境。

在众多HTML编辑器中,选择一个适合自己的工具至关重要。今天我们就来认识一下前端开发工作中使用的最广泛的工具 “VS Code” , 并在本地搭建好开发环境。

一、前端开发工具简介

首先,在介绍 “VS Code” 之前,我们先来了解一下什么是 “IDE”。

Description

什么是 “IDE”

IDE 是集成开发环境的英文缩写 (Integrated Development Environment),集成开发环境就是将在开发过程中所需要的工具或功能集成到了一起,比如:代码编写、分析、编译、调试等功能,从而最大化地提高开发者的工作效率。

IDE 通用特点:

  • 提供图形用户界面,在 IDE 中可以完成开发过程中所有工作;

  • 支持代码补全与检查,并提供快速修复选项;

  • 内置解释器与编译器;

  • 功能强大的调试器,支持设置断点与单步执行等功能。

前端开发IDE

而在前端开发中我们需要安装一个“趁手”的IDE,帮助我们更快更高效的开发,一个好的IDE是每个程序员的必备武器。前端开发IDE有很多种,例如 Visual Studio Code、HBuilder、WebStorm、Atom 或 Sublime Text 等。

我们可以任选一种使用。这几种IDE的对比如下:

Description

这么多IDE该怎么选呢?对于我们初学者来说,选择Visual Studio Code,(简称VS Code)就可以了。VS code具备内置功能非常丰富、插件很全且安装简单、轻量、对电脑的配置要求不算很高,且有MAC版本,应用广泛等优点,很适合新手。

下面就和我一起下载并安装VS code吧!

二、VS code下载与安装

1、进入VScode官网
官网地址:https://code.visualstudio.com/

点击【Download】进入下载,不要点击【Download for Windows Stable Build】,否则它会自动帮你下载User Installer用户版本。

Description

  • 【Stable】:稳定版本,比较稳定。

  • 【Insiders】:测试版本,添加了一些新东西,还在测试中,可能会存在一些Bug,不怎么稳定。

2、然后你会看见Windows,Linux,苹果三个版本,我们选择Windows版本,选择System Installer 点击【x64】进行下载,不要点击【↓ Windows windows8,10,11】,否则它也会自动默认下载User Installer用户版本。

Description

  • 【User Installer】:用户安装程序,VScode安装在你电脑当前账户的目录上,如果你换了一个其他账户登录你的电脑,那么你就用不了之前那个账户下载的VScode了。

  • 【System Installer】:系统安装程序,VScode不会安装在你电脑的当前账户上,而是直接安装在系统上,所有账户都可以使用。

其实选哪个版本都无伤大雅,就算你下载了【User Installer】版本也没事,因为没人会没事把自己电脑上的账户换成其他人的账户登录,就算换了也可以换回来,只是有时候特殊情况换了个账户登录不能使用就有一点麻烦,所以还是推荐尽量下载【System Installer】版本。

【x86】:32位操作系统。【x64】:64位操作系统,如果想知道自己是什么系统,可以敲击Win键找到“设置”→“系统”→“关于”→“系统类型”。

Description

3、正在下载

Description

这个下载会比较慢,如果不想等可直接去找个别人下好的安装包哦!也可找小编领取。

4、下载完后打开文件,会弹出许可协议弹窗,勾选我同意此协议,单击【下一步】。

Description

5、先去D盘里创建一个新文件夹取名叫“VScode”,点击【浏览】按钮修改安装路径,把路径改到刚刚在D盘里创建的VScode文件夹里。如果觉得麻烦也可以直接默认安装在C盘,然后单击【下一步】,但还是建议安装在D盘里。

Description

6、修改完路径后,单击【下一步】。(安装路径是这个样子D:\VScode\Microsoft VS Code)

Description

7、选择开始菜单文件夹,默认"Visual Studio Code",单击【下一步】。

Description

8、根据自己的需求进行勾选,勾选完单击【下一步】。

Description
【创建桌面快捷方式】:在桌面创建VScode快捷方式。

【将“通过Code打开”操作添加到Windows资源管理器文件上下文菜单】:选中一个文件鼠标右键可以通过VScode打开文件。

【将“通过Code打开”操作添加到Windows资源管理器目录上下文菜单】:选中一个文件夹鼠标右键可以通过VScode打开文件夹。

【将Code注册为受支持的文件类型的编辑器】:对于受支持的文件类型的文件,鼠标右键选择“打开方式”,可以通过“Vscode”打开。

Description

【添加到PATH】:添加VScode文件夹里的bin目录到PATH环境变量里,添加完以后可通过系统命令输入code直接启动VScode。

Description

9、单击【安装】进行安装。
Description

10、安装完成后单击【完成】启动。
Description

三、VS code配置

插件下载完之后,大家可以根据自己的需求下载插件,这里推荐我用的比较顺手的几个。

1、下载汉化包

点击扩展,在搜索栏搜索Chinese,选择Chinese中文简体点击【Install】进行安装。(建议少用,多看英文,这是一位优秀的程序员走向成功的标志性成长。)

Description

安装完后单击【Change Language and Restart】重启VScode软件,刷新一下就变成中文简体了。
Description

2、下载【会了吧】

插件在搜索栏里搜索【会了吧】,这个是在你敲代码时会自动识别你敲的单词进行翻译,如果你有一个单词不认识,可以点进“会了吧”看看翻译,对英语基础差的人很友好。

Description

3、下载【Open in browser】插件

这个是用来运行代码,并且在浏览器打开,查看运行效果的,这个插件必须下,否则当你写完HTML网页时你无法运行,无法预览页面,不信你可以先试试能不能运行再回来下载。

Description

4、下载【Live Server】插件

这个是用于实时预览运行效果,当你使用open in browser运行代码时,只要你的代码有改变,你就需要手动刷新重新预览页面运行结果,而Live Server是自动刷新运行结果,非常方便,非常滴银性!

Description

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

点这里前往学习哦!

四、用VScode编写HTML代码

1、先去D盘里创建一个新文件夹取名叫“Workspace”(名字随便取名)。

Description

2、进入VScode找到左上角的文件选择点击打开文件夹。

Description

3、找到刚刚创建的“workspace”文件夹,单击【选择文件夹】。

Description

4、找到WORKSPACRE,点击新建文件,名字输入“01.html”,然后点击回车键创建。

Description

5、在刚刚创建的01.html文件下输入以下代码。

<!DOCTYPE html>
<html>
   <head>
       <meta charest="utf-8">
       <title>HTML</title>
   </head>
   <body>
       <h1>这是我的第一个网页</h1>
   </body></html>

Description

6、鼠标右击空白处单击【Open In Default Browser】查看运行结果。

Description

7、运行结果如下。

Description

以上就是常用的前端开发工具VS code的下载和安装教程了,你的第一个HTML网页运行成功了吗?

一个高效的HTML开发工具和环境是每个前端开发者的得力助手。通过合理选择工具、配置环境、使用框架和库、以及不断的调试和测试,你可以将创意转化为现实,构建出令人惊叹的网页。记住,技术永远在变,唯有不断学习和实践,才能让你在这个数字世界中游刃有余。

收起阅读 »

网页空白区域消除点击的方法

web
一、前言 现在开发前端,总会遇到一些奇奇怪怪的形状,比如要求这个图标是个三角形(或者其他不规整的情况)。 大多数情况就是ui直接给了图,我们当背景引入,但是呢,我们的元素是一个矩形区域,这个时候如果绑定了点击事件之类的,透明的部分也会被绑定到。 一般来说是不会...
继续阅读 »

一、前言


现在开发前端,总会遇到一些奇奇怪怪的形状,比如要求这个图标是个三角形(或者其他不规整的情况)。


大多数情况就是ui直接给了图,我们当背景引入,但是呢,我们的元素是一个矩形区域,这个时候如果绑定了点击事件之类的,透明的部分也会被绑定到。


一般来说是不会有什么大问题,但耐不住测试跟你较真啊,这个空白区域怎么也能点击?bugbug,都是bug,回炉重造。


所以今天就来盘点盘点下,空白区域消除点击的方法。


二、方案


1、cilp-path属性


看过我之前的文章# clip-path属性深入使用的朋友们都知道,cilp-path是一个灵活度非常高的属性,让我们打破了盒子模型,可以自由撰写多边形。


这里我们简单用下里面的circle属性


clip-path: circle(40px at 50% 50%);


直接在网页上修改样式,可以看到百度的按钮变成了椭圆形,而原本周围可点击的部分,也无法点击了。(border-radius也可以做到类似效果)


QQ图片20230430164354.png


这里只是简单检验一下效果,一般而言用得到的clip-path是polygon多边形属性。


2、html5原生标签map和area


这里我们还是沿用这个w3school的例子,老经典了。


引入了一张图,或者其他元素吧,设置usemap为#workmap,然后后面跟map标签,name要对得上。


里面用area划分区域,shape确定形状,然后coords界定具体范围,然后就可以绑定各自的事件了。


<img src="workplace.jpg" alt="Workplace" usemap="#workmap" width="400" height="379"> 
<map name="workmap">
<area shape="rect" coords="34,44,270,350" alt="Computer" href="computer.htm">
<area shape="rect" coords="290,172,333,250" alt="Phone" href="phone.htm">
<area shape="circle" coords="337,300,44" alt="Cup of coffee" href="coffee.htm">
</map>

QQ图片20230430142008.png


画面中有电脑手机咖啡等等物品,我们创建了map容器,指向了这张图,同时创建了3个area区域,分别划分范围,实现不同的事件触发。


换个思路就是,我们画出非空白部分,给其绑定事件,空白区域就不绑定事件就可以了。


3、利用伪元素或其他元素


思路就是,利用伪元素或者其他元素,写出一块和空白区域一样大小的dom,叠在空白区域上面,也可以实现效果。


不过就是有点费时费力,如果遇到的是比较复杂的图形的话。


4、图片透明部分不可点击,实体部分可点击


思路:用canvas画一个同等大小、同一位置的图片,叠上去。用canvas固有方法判断点击位置是否透明。


QQ图片20230430222322.png


其中的ctx.getImageData就是用来取色的,通过判断透明度来决定触不触发事件。


var ctx = c.getContext("2d");


var imgdata = ctx.getImageData(x, y, 1, 1);

console.log("点击位置的全部颜色数据[r,g,b,a]", imgdata.data);

console.log("点击位置的透明度颜色数据(0~255,0代表完全透明,255代表完全不透明) value:", imgdata.data[3]);

三、个人推荐


复杂区域推荐用方案一、方案二


简单区域可以用方案三


方案四的话,操作起来比较麻烦,如果空白区域太复杂的话,也可以试一试。


ps: 我是地霊殿__三無, 51小水一波。


Snipaste_2022-07-19_15-30-26.jpg


作者:地霊殿__三無
来源:juejin.cn/post/7228692613036081189
收起阅读 »

产品经理:优惠金额只入不舍,支付金额只舍不入...

web
前言 当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。 优惠券分为:折扣券(n折)、抵扣券(减x元) 需求 优惠金额、支付金额都需要保留两位小数。 优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些 支付金...
继续阅读 »

前言


当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。

优惠券分为:折扣券(n折)、抵扣券(减x元)


需求


优惠金额、支付金额都需要保留两位小数。

优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些

支付金额只舍不入,比如18.888元,显示为:18.88元。

从产品角度来讲,这个设计相当人性化。


实现


/**
* 金额计算
* @param {number} a sku原始价格
* @param {number} b 优惠券金额/折扣
* @param {string} mathFunc 四舍五入:round/ceil/floor
* @param {string} type 计算方式默认减法
* @param {digits} type 精度,默认两位小数
* */

export function numbleCalc(a, b,mathFunc='round', type = '-',digits=2) {

var getDecimalLen = num =>{
return num.toString().split('.')[1] ? num.toString().split('.')[1].length : 0;
}
//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}


var c;
//获取2个数字中,最长的小数位数
var aLen = getDecimalLen(a);
var bLen = getDecimalLen(b);
var decimalLen = aLen>bLen?aLen:bLen;
var mul = decimalLen>0?(10 ** decimalLen):1;

//转换成整数
var aInteger = floatToInt(a,aLen,decimalLen)
var bInteger = floatToInt(b,bLen,decimalLen)


if(type=='-'){
c = (aInteger - bInteger)/mul;
}else if(type=='*'){
c = aInteger * bInteger/mul/mul;
}

c = digits==0?c : c * (10 ** digits);

if(mathFunc=='floor'){
c= Math.floor(c);
}else if(mathFunc=='ceil'){
c= Math.ceil(c);
}else {
c= Math.round(c);
}
return digits==0?c : c/(10**digits);
}



整体思路:获取两个数字之间最大的小数位,先取整再计算。
不直接进行计算,是因为存在0.1+0.2!=0.3的情况,具体原因可以看下文章下方的参考链接,写的很详细。




  • Math.ceil()  总是向上舍入,并返回大于等于给定数字的最小整数。

  • Math.floor()  函数总是返回小于等于一个给定数字的最大整数。

  • Math.round() 四舍五入


【重点】小数位取整:我之前的写法原来是错误的


image.png
我一直以来也是这种形式,预想的是直接乘100变成整数,但是出现了以下情况


19.9 * 100 = 1989.9999999999998
5.02 * 100 = 501.99999999999994

可以看到,出现了意料之外的结果!!

最后采用的方案是:将小数转成字符串,再将小数点替换成空格


//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}

总结


省流:将小数点替换成空格,变成整数,再进行相应计算。

封装的这个函数,只考虑了当前业务场景,未兼容一些边界值情况。



  • 大金额计算问题

  • 计算方式:加法、除法未做处理


参考


# 前端金额运算精度丢失问题及解决方案


作者:前端大明
来源:juejin.cn/post/7341210909069770792
收起阅读 »

想进国企,跳槽真的不要太频繁

想进国企,跳槽真的不要太频繁 去年内推的一个学弟,面试兜兜转转一个月,最终北京的领导还是把他挂了,因为觉得他跳槽是在太频繁了,担心稳定性不行。 其实这个学弟本身是非常优秀的,本科和研究生都毕业于985大学,学校期间实习的地方也都是腾讯、阿里、字节这样的大公司...
继续阅读 »

想进国企,跳槽真的不要太频繁


image.png


去年内推的一个学弟,面试兜兜转转一个月,最终北京的领导还是把他挂了,因为觉得他跳槽是在太频繁了,担心稳定性不行。


其实这个学弟本身是非常优秀的,本科和研究生都毕业于985大学,学校期间实习的地方也都是腾讯、阿里、字节这样的大公司,毕业后的第一份工作也是大家耳熟能详的某国有大银行的软开岗。


简历和履历都非常不错,但是不知道什么原因他却选择了毕业刚工作半年就裸辞,当我刚认识他的时候他以及在成都到处投简历找工作快一个月了。


虽然这种履历也不至于颗粒无收,但是比起他上一份工作的档次还是差了很多,并且也可能是在银行的科技岗被卷怕了,所以总跟我说想找一个稳定的地方呆上一段时间。


然而,遗憾的是我们这边北京的领导觉得他半年就离职十分不稳定,也对他关上了大门。


想要了解更多成都央国企招聘、内推、简历辅导、面试辅导等信息,欢迎关注知识星球【成都央国企指南】,帮助应届生、社会人士等了解成都央国企信息,可进行一对一简历辅导、职业规面试辅导等。


为什么国企这么在乎稳定性?


我记得当时面试来到这家公司的时候,招聘我的主任心里还很虚,因为我也是呆了一年就跳槽的,所以担心我过来心里落差大,呆不了多久。


好在当时我没有离职,所以心态很平和然后才跟这边把offer谈下来了,所以我想想如果当时我已经离职了的话,作为社招我可能就拿不到这个offer了吧。


那么国企为什么这么在乎稳定性呢,国企其实和私企不同(这里我说的是真国企,像什么科技子公司,有末尾淘汰还真的搞的不在我的讨论范围之内),不需要你作为个人有太大的能力和产出,更不需要把你压榨到极致。


本质上国企的项目是通过领导层的关系运作来的,不需要去市场是厮杀来抢一个项目,有些垄断行业这项目就得你们公司做,哪怕是拿到项目后外包出去,都得你们集团来牵头。


并且国企的领导和你一样,都是给国家打工,而且国企的升迁也不是唯业绩论成败,所以事情即使你干得没那么好对领导的影响也没那么大,不会搞你搞得太难看,除非你这个领导有精神疾病。


所以,一般垄断国企的稳定性还行,很多员工都是呆上好几年甚至数十年的,一方面是工作环境确实不错,没人难为你,另一方面就是在国企呆久了就确实失去了去外面厮杀的能力,也不敢跳槽。


工作的节奏也会比较慢,在国企你呆了半年可能项目方面才上手,在这种环境下,由于待遇也没有外面私企那么高,领导招一个人实际上是非常慎重的。


像这个学弟的半年一跳的情况,在国企领导眼里看起来可能觉得你都还没开始上手,怎么就跳槽了,那我招你进来会不会是瞎折腾,所以就选择敬而远之了。


如何在招聘的时候展示你的稳定性?


上面说了那么多,在国企的招聘当中,如果你能够恰到好处的展示你的稳定性的话,那么国企的领导可能会更加青睐你。主要可以从以下几个方面来展示你的稳定性:


第一、在当前城市已经定居或者有定居打算,如果你买了房子有了生存压力,并且打算扎根在这里的话,肯定就是一个加分项了,因为这样的人一般是不敢轻易跳槽的,所以领导很放心。


第二、能力其实不需要太强,在互联网、私企混了很久的人其实有一个误区,能力越强越好。其实国企的领导心里有数的,第一这里不需要什么高精尖技术,第二,能力太强的呆不了多久就跑了。所以,包括我在内面试的时候也不需要能力太强,或者那种觉得自己能力强就沾沾自喜的人。这些人有更好的去处,我们这小庙就不供这座大佛了,所以如果是真国企的岗位其实像什么阿里P8的还真不愿意要,除非是集团领导直接招进来,普通岗位招进来真怕你卷我。


第三、有女朋友在这边,我同时就是典型的为了女朋友从杭州跳槽到成都来的,所以有这个理由也会很放心,觉得你过来就跑不掉了。


第三、年龄还不大或者年龄稍大从一线城市回来,这样的人一般都会选择在当前的城市定居的,面试的时候如果表现出定居的意图那么也会有一定优势。


想进国企,真的不要随便跳槽


其实现在很多私企都很在乎稳定性,什么五年三跳、三年两跳的都会成为简历上的劣势而被无情拒绝。


虽然我觉得这不是一个好的事情,尤其是私企本身人员变动就频繁你还要求人家有稳定性,这不闹着玩吗?


但是如果你未来想着跳槽进入一个不卷的国企平稳降落,那么一定跳槽就不要太平凡,国企这方面的逻辑和私企完全不同,私企是无理取闹,但是国企是真的希望人员稳定一点儿。


无论什么情况都坚持一些,保证自己的简历不要花掉,不然像这位学弟一样被拒绝就是一件很难受的事情了。


The End


其实这个学弟还是拿到了四川九洲的offer,但是他拿不准到底要不要去,怕又是一个很卷的军工国企。


我是这样劝他的,其实哪里工作都差不多,重点是你要能扛住工作的压力不要因为一些莫名其妙的小问题,玻璃心就离职。


哪怕你等着公司裁你还能拿一波n+1赔偿呢,所以有地方去就先呆着,先找到工作结束失业状态再说,先上岸再考虑其他的。


最后,也衷心希望他可以找到合适的工作。




作者:浣熊say
来源:juejin.cn/post/7338721296866656296
收起阅读 »

纯前端就可以实现的两个需求

web
一:多文件下载并压缩成zip形式   我们一般看到这个需求,大多前端都会觉得这不就是一个接口的事嘛,咱们直接window.open()就搞定了,emm...,我之前也是这样想的,可惜现在是在三线城市的小互联网公司,人家后端说服务器不行,带宽不够,出不来接口0....
继续阅读 »

一:多文件下载并压缩成zip形式


  我们一般看到这个需求,大多前端都会觉得这不就是一个接口的事嘛,咱们直接window.open()就搞定了,emm...,我之前也是这样想的,可惜现在是在三线城市的小互联网公司,人家后端说服务器不行,带宽不够,出不来接口0.0,所以我就尝试着寻找……最终找到了解决办法——


  前端可以直接从cos资源服务器中下载多个文件,并放进压缩包中哦,这样就省去了后端在中间中转的那个环节,实现方式如下:


1.安装依赖


  我们需要用到两个第三方依赖哦,分别执行以下安装


npm i jszip
npm i file-saver

2.引入 


  在需要使用的时候,我们引入


import JSZip from "jszip";
import FileSaver from "file-saver";

3.实现过程


  我这里是在vue框架中,所以方法是在methods中哦,先在data里声明一个文件demo数据


data () {
return {
fileList: [ //这里的数据 实际中 应该是从后端接口中get { name: "test1.doc", url: "https://collaboration-pat-1253575580.cos.ap-shenzhen-fsi.myqcloud.com/baseFile/202303/bdf4303f-4e8d-4d95-977c-4fd5ef28d354/new.doc" }, { name: "test2.doc", url:"https://collaboration-pat-1253575580.cos.ap-shenzhen-fsi.myqcloud.com/baseFile/202303/bdf4303f-4e8d-4d95-977c-4fd5ef28d354/new.doc" } ],
}
}

methods中的方法:


handleClickDownAll () { //实现多文件压缩包下载
let _this = this
let zip = new JSZip()
let cache = {}
let promises = []
for(let item of this.fileList) {
const promise = _this.getFileArrBuffer(item.url).then(res => {
//下载文件, 并存成ArrayBuffer对象(blob)
zip.file(item.name,res,{binary:true}) //逐个添加文件
cache[item.name] = data
})
promises.push(promise) }
Promese.all(promises).then(() => {
zip.generateAsync({type:'blob'}).then(content => {
FileSaver.saveAs(content,"压缩包名字") //利用file-saver保存文件 自定义文件名
})
})
},
getFileArrBuffer(url) {
return new Promise((resolve) => {
let xmlhttp = new XMLHttpRequest()
xmlhttp.open('GET',url,true)
xmlhttp.responseType = 'blob'
xml.onload = function () {
resolve(this.response)
}
xmlhttp.send()
})
}

二:electron-vue中,生成二维码,并支持复制二维码图片


要实现的功能如下,就是点击这个“复制二维码”,可以直接把二维码图片复制下来



1.安装依赖


npm i qrcodejs2

2.引入


import QRCode from 'qrcodejs2';
import { clipboard, nativeImage} = require('electron')

3.实现


  要先在template中写一个这样的元素,用来显示二维码图片框


<div id="qrcodeImg" class="qrcode" style="height: 120px"></div>

然后再写一个画二维码的方法,如下:


drawQrcode() {
new QRCode("qrcodeImg",{
width:120,
height:120,
text:"http://www.baidu.com",
colorDark:"#000",
colorLight:"#fff"
})
}

复制二维码的方法如下:


copyCode() {
let src = document.getElementById("qrcodeImg").getElementsByTagName("img")[0].src
const image = nativeImage.createFromDataURL(src)
clipboard.writeImage(image)
this.$Message.success('复制成功')
}

4.使用


要先确保dom元素已经有了,所以在mounted中调用drawQrcode()这个方法,然后点击“复制二维码”时,调用 copyCode()这个方法就可以实现啦




作者:wenLi
来源:juejin.cn/post/7213983712732348474
收起阅读 »

从裁员到加入腾讯架构组,愿大家都能走出低谷

不知不觉已经加入腾讯某事业线的架构部门工作快半年了,今天来分享一下自己去年的裁员经历,希望能对大家有所启发和激励。 加入直播部门 我是2022年年初加入了公司的直播事业部,从事直播这块的后台开发内容。当时主要是想多些接触到直播这块的业务,运用自己学过的一些技术...
继续阅读 »

不知不觉已经加入腾讯某事业线的架构部门工作快半年了,今天来分享一下自己去年的裁员经历,希望能对大家有所启发和激励。


加入直播部门


我是2022年年初加入了公司的直播事业部,从事直播这块的后台开发内容。当时主要是想多些接触到直播这块的业务,运用自己学过的一些技术手段去解决高并发场景的一些问题。可以说在这个组里头学习到了很多有用的实战技巧,微服务拆分,分库分表,服务限流,降级,推荐系统,自研im系统,还有各种自研中间件,花了大概大半年的时间,把这块都自己吭哧吭哧地造了遍轮子,实践了一番,收获非常之大。


部门裁员


去年的2月底,通过一些小道消息得知,我所在的直播部门开始了裁员计划,并且自己因为只是刚加入这个项目组,于是便被列入了裁员的名单中了。不过其实离开也算是一种解脱,因为直播组的工作压力是非常大的,当时我们有10个产品经理,而后台开发只有5个人手。每个产品为了冲各自的kpi,每周都会出2-3个需求,拍下来一个人一周大概要并行做3-4个需求,做不完的就只好往后排期。小需求还好,有时候是一些大需求,流程复杂,还容易出错。


裁员通知


大概在2022年3月10号左右,当时我被喊到了“小黑屋”,人事和leader正式告知了我裁员的事情,然后也按照规定签署了N+1。


被裁员那天下午,当时在公司的窗台边上看着窗外的风景,感觉有一种久久不能释怀的解脱感和不舍感。自己从毕业就进到这家公司,如今正好满了4年,不知后边的路该如何是好。


8141708351261_.pic.jpg


同事离别


最后离别搬东西的那天,我和公司所有认识的同事都打了声招呼,尤其是之前带过我的两任领导,由衷感谢他们之前对我的照顾和提拔,能让我这几年学习到了许多。


当时和当初一起进公司的应届生伙伴们一起喝了一杯奈雪,聊起了当初一起加入公司,最后居然还能一起离开,真是有缘份,哈哈哈。(大家都在名单当中)也和几个关系很好的开发同事吃了一顿椰子鸡,聊了聊以前工作的趣事和一些技术话题(程序员的爱好,哈哈哈哈)。


几乎所有熟人都告别了一轮后,晚上便离开了公司大楼。


离职后生活调整


当时公司离职的同事有不少,所以大家也就一起组建了一个离职同事交流群。有些人在离职后去了旅游,有些人选择了回老家陪家人。当时我给自己放了一周的假,放空了思绪。经常晚上一个人到海边公园去跑步,多锻炼下身体,先把精神状态调整好。当时还买了乐刻健身一个月的会员,时不时到健身房撸撸铁。后边我把离职的事情告诉关系不错的几个同学,大伙时不时约几顿烧烤,爬山,跑步,各种活动安排了起来。


新的规划


刚离职的前几周,那会确实很舒服,每天都过得无忧无虑的。但是刚到四月初的时候,开始感觉不能再这么躺下去了,于是便开始新的规划。


早在2021年的时候,就已经开始尝试做各种副业赚取一些其他收入,到2023年的时候已经基本成型了,于是当时是尝试去继续做自己的副业工作。副业之一就是技术讲师,当时和好几个平台有签约合作。于是当时便萌生了一个idea,如果花几个月时间投入到副业的话,会有多大的收益。


于是当时的4,5,6月份几乎都把精力投入到了几门课程的研发当中,然后还报名了一个做运营的课程,学习了一些不同自媒体平台的运营手段。(现在回看,感觉当时的尝试是非常有意义的)
下边是当时那几个月在飞书的日程表记录记录图,因为怕自己忘记每天要做什么事情,所以每天早上都会提前规划好:


image.png


image.png


image.png


每天大概会花4-5个小时在课程录制上,然后剩余时间可能会看看电影,上上b站刷刷八股文面试题或者出门运动下啥的。


记得当时5,6月份的时候,在boss上简单投了一下,约到了几家小公司的面试。当时只是抱着一个试一试的心态去练练手。


开始海投求职


真正的海投应该是在7月份开始,当时动用了各种渠道,内推,boss直聘,前后投递了大概500份简历。前期先投小公司开始,一边投简历,一边优化简历的内容以及对技术知识的查漏补缺。前边面了大概3-4家小公司,基本都能面到2-3轮,大概就熟悉了面试的感觉后,后期便开始面一些中大型公司了。



  • 顺丰科技(压薪,谈薪挂)

  • 华润电商(二面没去)

  • 华润物业(后台开发,offer)

  • 跨越速运(后台开发,offer)

  • 迅雷网络(风控中台,二面挂)

  • tcl(基础架构组,二面没去)

  • 360(基础架构组,二面没去)

  • 大疆(简历挂)

  • 平安(业务开发,二面没去)

  • 乐信(简历挂)

  • 某证券交易所 (业务开发,二面没去)

  • 货拉拉(简历挂)

  • 某跨界电商独角兽公司(后台开发,offer)


前前后后面试了十几家公司,这么多家公司面下来发现,大部分问的内容都差不多,人渐渐会有一种面试的疲倦感。


当时拿到的三个offer,其中华润那边的福利和待遇是比较好的,但是也存在问题就是项目组是比较传统的,担心自己进去学不到太多东西。当时大概是八月初,有点纠结。


面试准备


光是一个7月,就面了有十余家,最高的频率有试过一天面五家公司。是的,你没看错,我也没编造,那段时间运气好,确实能约到很多面试,然后基本所有面试都是约了线上视频或者语音沟通。



  • 八股文


当时是在5,6月的时候,花了一些时间,专门重温了一份八股文笔记(b站上一些培训机构发的,大概10w字,确实有些帮助,能帮你很好地把不同技术点给系统化起来)。



  • 项目


然后简历这块主要是写了一个直播的项目,加了一些“三高”字眼的词藻去修饰了下。也在简历上写了自己的博客,开源项目,公众号这些东西。(在简历筛选中应该会有些帮助)



  • 算法


leecode也时不时刷一刷,保持下手感,我只刷了100道左右,一般面试都是从easy或者normal中挑选,hard就看运气了。



  • 心态


多运动,与朋友交流,心态健康,面试的时候发挥就会正常很多。


腾讯面试


记得那会是刚拿到三个offer还在纠结的第二天,当时把三个offer和几个认识的大佬聊了下 要如何选择,大佬们大多是建议去华润,毕竟大环境下求稳会更重要。原本是要下决定的了,然而在一天晚上,自己正在做饭的时候,忽然打开了一个电话☎️。


我接听了,对面是腾讯的hr,说有人推了我的简历,想约一轮面试看看有没有时间。我当时是有点懵的,因为自己前两天刚好在boss上投了腾讯的岗位,于是边爽快的答应了。


腾讯的面试jd上写的其实是比较含糊的,当我面完了一面的时候,问了下面试官这个岗位的职责才知道是腾讯的基础架构组。(内心有点意外和开心)


一面面试官不怎么问八股文 上来就是聊项目,然后问了很多直播里面的高并发处理方案(后来进去后才知道,这个面试官之前是腾讯那边的直播业务负责人,难怪直播这块的技术方案那么了解)。一面也问了些架构设计的问题,例如四百亿的聊天信息要如何设计存储架构,最后给了一道算法题要求电脑上写出来,好在以前有刷leecode的习惯,那天正常地写了出来。


二面大概是一面后的第三天,二面的面试官问了很多中间件的原理,以及自己以前做基础组件的开发经验,重点问了下im架构是如何实现的。因为之前有过十几家公司的面试经验,所以大部分回答起来都比较顺利。


三面是部门的副总经理面,这一轮的面试压力会比较大,对方会不断的否定你,然后让你现场优化设计方案。不过一切都好在有所准备,流程还算正常。最后还和我介绍了一下基础架构组的工作职责,以及大概定级。


四面是hr面了,大概问了下个人职业规划 ,学习方法,还有一些其他内容记不太清了,大概唠了半个小时吧。


前前后后腾讯这边的面试持续了两周左右时间,一共四轮,最后也谈了定级和薪资,并且顺利发了offer。


offer比对


可能会有朋友好奇,八月初拿了其他几家的offer,可以持有到腾讯面试结束吗?


不行的,当时其他几家公司的offer最久的也得在一周内回复,所以当时是放弃了已有的几个offer, 打算搏一搏,好在最后搏上了。


其实当时腾讯在面试过程中的时候,还有tcl,360,某证券公司,平安等在准备二面,也给自己留了几家其他公司面试机会的后路。


其他大厂有试过不


类似阿里,京东,美团,快手,字节,oppo,vivo这些大公司,之前也有尝试投过,但是简历基本都挂了,不过也可能和自己学历有关,三本软件工程。


从离职到后边重新步入职场工作,中间gap了大概五个月(正好也和之前的赔偿金顶上了),三月中旬离职,九月初入职tx。中间四五六月在忙副业,赚了一些些,然后七八月开始不停约面试,算是比较幸运地过渡下来了。


干副业会影响面试准备吗


这里我做的副业的内容是和主业相关的,当时是讲了一门直播相关的技术课程,这门课程的内容正好就是我在简历上所写的内容,所以其实所谓的给人讲课,就是变相的面试复习,同时还能锻炼自己表达能力。(视频授课)


没有工作后会焦虑吗


其实是会的,尤其是面试屡屡挫败的时候,一开始会打击人自信心,不过后边多试几次就感觉还好。


另外也好在副业帮我打发了大部分的时间,所以4,5,6月份的生活,生活节奏和上班差不多,所以整体感觉还好。


新环境工作感受



  • 卷:经常要加班到晚上九点后,好在是双休,所以一般周末会选择去运动,保证身体健康。

  • 技术收获很大:小组是专门做技术方向的,如监控 ,数仓,中台,自研组件,还有一些运维的活也要干,对自己的技术成长这块有着很巨大的帮助。

  • 程序员不能只懂技术,打交道的能力也是很重要的,尤其是跨部门协作,技术宣讲这些东西,都需要靠口才和个人魅力去达成。


作者:DannyIdea
来源:juejin.cn/post/7337489263877111842
收起阅读 »

下一代 Node.js?运行速度狂飙 10 倍!!!

web
【💥 重磅新闻 💥】JavaScript 服务端运行时环境又出新秀,LLRT 来势汹汹!🌟 嘿,各位 JavaScript 开发者们,你们知道吗?最近有个叫 LLRT 的新运行时环境火了起来,据说速度比 Node.js 快了 10 倍!🏎️ 前有 Deno,后...
继续阅读 »

【💥 重磅新闻 💥】JavaScript 服务端运行时环境又出新秀,LLRT 来势汹汹!🌟


嘿,各位 JavaScript 开发者们,你们知道吗?最近有个叫 LLRT 的新运行时环境火了起来,据说速度比 Node.js 快了 10 倍!🏎️


前有 Deno,后有 Bun,现在又来了个 LLRT,Node.js 这些年的日子可真是“不是被超越就是在被超越的路上”啊!😅


那么问题来了,你觉得 LLRT 的出现会对 Node.js 造成威胁吗?


什么是 LLRT


LLRT(低延迟运行时)是亚马逊推出的一种轻量级 JavaScript 运行时。旨在满足对快速高效的无服务器应用程序不断增长的需求。


LLRT 优势:



  • 🔥 不使用 V8 引擎,而是采用 Rust 构建,确保高效内存使用。就像给应用程序装了涡轮增压器,速度瞬间飙升!💨

  • 🔥 使用 QuickJS 作为 JavaScript 引擎,快速启动不是梦!就像闪电一样快,让你的应用程序瞬间响应!⚡


如下所示 LLRT 和 Node.js 20 运行速度对比,可以看出


LLRT - DynamoDB Put, ARM, 128MB:
llrt-ddb-put


Node.js 20 - DynamoDB Put, ARM, 128MB:
node20-ddb-put


LLRT 兼容性



LLRT 仅支持一小部分 Node.js API。它不是 Node.js 的替代品,也永远不会是。下面是部分支持的 API 和模块的高级概述。有关更多详细信息,请参阅 API 文档:github.com/awslabs/llr…



Node.jsLLRT ⚠️
buffer✔︎✔︎️
streams✔︎✔︎*
child_process✔︎✔︎⏱
net:sockets✔︎✔︎⏱
net:server✔︎✔︎
tls✔︎✘⏱
fetch✔︎✔︎
http✔︎✘⏱**
https✔︎✘⏱**
fs/promises✔︎✔︎
fs✔︎✘⏱
path✔︎✔︎
timers✔︎✔︎
uuid✔︎✔︎
crypto✔︎✔︎
process✔︎✔︎
encoding✔︎✔︎
console✔︎✔︎
events✔︎✔︎
ESM✔︎✔︎
CJS✔︎✔︎
async/await✔︎✔︎
Other modules✔︎

LLRT 能否替代 Node.js


LLRT 的出现确实对 Node.js 构成了一定程度的挑战,但这并不意味着 Node.js 会立即被淘汰。


事实上,Node.js 在许多方面仍然具有优势,例如生态系统的庞大规模、广泛的社区支持和大量的现有项目。此外,Node.js 作为一个成熟的运行时环境,已经积累了大量的功能和模块,这些在 LLRT 中可能尚未实现。


然而,LLRT 的出现确实为 JavaScript 服务端运行时环境带来了新的选择。对于那些对性能有较高要求、需要快速启动低内存占用的项目,LLRT 可能是一个更好的选择。


总之,LLRT 的出现对 Node.js 构成了一定程度的挑战,但这并不意味着 Node.js 会立即被淘汰。开发者可以根据项目需求和场景来选择最适合的运行时环境。


参考连接:






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

2023年给一位团队成员绩效“打c”的经历

2023年作为疫情开放的第一个年份,国家整体的经济形势还处于低迷阶段。IT行业同样如此,各公司都保持着降本增效的节奏,企业招聘人才的需求明显放缓,有人力缺口的岗位都优先考虑内部人才的转岗。在这样的大环境下,IT行业的从业者们想要离职会更加谨慎,不会再如以前那样...
继续阅读 »

2023年作为疫情开放的第一个年份,国家整体的经济形势还处于低迷阶段。IT行业同样如此,各公司都保持着降本增效的节奏,企业招聘人才的需求明显放缓,有人力缺口的岗位都优先考虑内部人才的转岗。在这样的大环境下,IT行业的从业者们想要离职会更加谨慎,不会再如以前那样“老子干的不爽,就离职换个公司”。


即便如此,每个公司依然还会有一些人,总过得浑浑噩噩,需要别人踢一脚就走一步,不去主动承担和思考事情,只能做一些确定性很强和设计好的工作。2023年,我们团队就有这样的一名成员,最终不好的绩效,也只能落到他头上。


然而,最终和他进行绩效约谈的时候,他却完全不认可,表现出非常激烈的逆反情绪,认为他的职级不需要去承担过多的事情。而我作为他的直接管理者,这次约谈的过程显然不够成功,现在整体再复盘一下这个过程。


前提背景


员工背景: 进入公司已经超过三年,近一年由于部门变动转入我的团队。本身的职级较低,由于之前工作平平没有太多起色,所以一直也没有得到过晋升,而年龄却已经越来越大。


进入我的团队后,团队内年龄与其不相上下的成员,职级已比他高出较多。从而,也引起了他心态的失衡,总觉得公司亏欠他的,他的能力不应该得不到晋升。所以在工作时,只愿做自己职级内的事情,也不愿意承担更多。


过程管控


我作为他的直接管理者,发现问题后私下跟他聊过。跟他说过几次,他做事情太被动,工作时对外沟通经常带着个人情绪,需要更加积极正面的去承担事情。为了打消他的顾虑,也跟他说明了,只要你的工作能力有所提升,对团队有所帮助,我会尽量帮助你晋升。


然而,一个人心态的问题,是一个历史长期积累的过程。他并没有因为和我的几次沟通,就打破了自己的认知,对外依旧较封闭,对内能力又显得不足。而他自己却认识不到,总认为他在当前的职级上,已经足够了,除非公司让其晋升,不然他也不会付出更多。


为了打破这种僵硬的局面,作为管理者我安排了一项稍有困难的任务给他,这既是机会也是挑战。第一、让其认识到自身的不足;第二、如果他能够较好的完成任务,也就为后面的晋升提供了保障。


也就是这么一次任务,不但目的没有达到,最后还惹得双方都陷入了僵局。



这项任务还未开始3个月前,我就跟他说:要开始熟悉相关的业务和代码了,后面会有大的项目变更,需要提前做好准备。


前期我并没有明确说明,要交付什么产物。更多的是,给他自己空间,让他在一个相对宽松的时间内,把整体的业务和细节都了解清楚,能够在组内进行一次分享。


任务我已经给出去了,在项目开始前将近3个月的时间, 他并没有给到我任何反馈,也没有交付任何相关的文档。


随着时间的推移,项目开始启动了,基于这样的工作态度和结果,我本不打算让他再负责这个项目。但是上级管理者,也希望能够给予他一次机会,做好了能够为后面的晋升,提供较好的铺垫。


就是这样的安排,由于他前期没有较好的准备,后面在落地方案评审时漏洞百出,导致项目出现了延期的风险。所以最终不得不由我直接来接管项目,重新分配和协调各个研发人员,最终确保了项目的质量和进度。



约谈结果


对于这样的团队成员,既不能给团队带来正向的帮助,也不能让其自身得到成长。这是一个双输的局面,管理者要让团队保持正向的发展,就必须要勇于去解决这样的问题。


所以年度的绩效考核,就必须亮明你的态度,即使公司没有淘汰的指标,你也要去做那个坏人,把不合适的人从团队清除掉。只是,我没有想到这个过程如此艰难。


下面从他的视角,来反驳这个结果的几个观点:



  • 他的职级,只需要配合做好相应的开发任务就可以,不需要去主导事情。

  • 那个有挑战的项目,不管过程怎样,结果是好的,项目按时按质的上线了。

  • 给他不好的绩效,需要参照公司的标准,给出明确的原因。


然后,带着强烈的情绪说要去投诉,甚至要上升到CTO、CEO 那边。 投诉没有问题,我也表明了态度, 你可以向上申请表达自己的诉求, 但是我也会持有自己的观点和建议。


最终,当然也不会因为他的申诉就改变结果。只是,这个现状本应该在管理的过程中,就应该让其感知到,不要等到最后的环节,才让双方都陷入难堪的局面。


反思总结


作为一个管理者,要面对各式各样的研发人员。有的人优秀,上来就能够跟你站在一个视角看问题;有的人有潜力,需要你给出机会和试错空间,让其成长;有的人就该辞退,针对这些人,你尤其要做好备战。作为管理者,既要有开放和怀柔的心态去留住人才,也要有铁血的手腕去清退团队的毒瘤。


清退毒瘤,是一项艰难但必要的任务,如何去做呢?



  1. 评估情况: 评估对团队的影响,是否对团队的合作和效率产生负面影响,是否违反了团队的价值观和行为准则。要确保有足够的证据来支持你的决定。

  2. 沟通和反馈: 与他进行一对一沟通,明确表达你对他们行为的关注,并提供具体的例子。给予他们改进的机会,并讨论如何改变。

  3. 制定行动计划: 如果没有改善他们的行为,你需要制定一个行动计划。包括培训和指导。

  4. 寻求支持: 寻求其他团队成员和上级的支持。


作者:云游者
来源:juejin.cn/post/7341368001203699747
收起阅读 »

pnpm才是前端工程化项目的未来

web
前言 相信小伙伴们都接触过npm/yarn,这两种包管理工具想必是大家工作中用的最多的包管理工具,npm作为node官方的包管理工具,它是随着node的诞生一起出现在大家的视野中,而yarn的出现则是为了解决npm带来的诸多问题,虽然yarn提高了依赖包的安装...
继续阅读 »

前言


相信小伙伴们都接触过npm/yarn,这两种包管理工具想必是大家工作中用的最多的包管理工具,npm作为node官方的包管理工具,它是随着node的诞生一起出现在大家的视野中,而yarn的出现则是为了解决npm带来的诸多问题,虽然yarn提高了依赖包的安装速度与使用体验,但它依旧没有解决npm的依赖重复安装等致命问题。pnpm的出现完美解决了依赖包重复安装的问题,并且实现了yarn带来的所有优秀体验,所以说pnpm才是前端工程化项目的未来


如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~


npm 与 yarn 存在的问题


早期的npm


在npm@3之前,node_modules结构可以说是整洁可预测的,因为当时的依赖结构是这样的:


node_modules 
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
└─ package.json
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
└─ package.json

每个依赖下面都维护着自己的node_modules,这样看起来确实非常整洁,但同时也带来一些较为严重的问题:



  • 依赖包重复安装

  • 依赖层级过多

  • 模块实例无法共享


依赖包重复安装


从上面的依赖结构我们可以看出,依赖A与依赖C同时引用了依赖B,此时的依赖B会被下载两次。此刻我们想想要是某一个依赖被引用了n次,那么它就需要被下载n次。(此时心里是不是在想,怎么会有如此坑的设计)


01203040_0.jpeg


依赖层级过多


我们再来看另外一种依赖结构:


node_modules 
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖D
├─ index.js
└─ package.json

这种依赖层级少还能接受,要是依赖层级多了,这样一层一层嵌套下去,就像一个依赖地狱,不利于维护。


npm@3与yarn


为了解决上述问题,npm3yarn都选择了扁平化结构,也就是说现在我们看到的node_modules里面的结构不再有依赖嵌套了,都是如下依赖结构:


node_modules 
└─ 依赖A
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖C
├─ index.js
├─ package.json
└─ node_modules
└─ 依赖B
├─ index.js
├─ package.json
└─ node_modules

node_modules下所有的依赖都会平铺到同一层级。由于require寻找包的机制,如果A和C都依赖了B,那么A和C在自己的node_modules中未找到依赖C的时候会向上寻找,并最终在与他们同级的node_modules中找到依赖包C。 这样就不会出现重复下载的情况。而且依赖层级嵌套也不会太深。因为没有重复的下载,所有的A和C都会寻找并依赖于同一个B包。自然也就解决了实例无法共享数据的问题


由于这个扁平化结构的特点,想必大家都遇到了这样的体验,自己明明就只安装了一个依赖包,打开node_modules文件夹一看,里面却有一大堆。


nz2.jpeg


这种扁平化结构虽然是解决了之前的嵌套问题,但同时也带来了另外一些问题:



  • 依赖结构的不确定性

  • 扁平化算法的复杂度增加

  • 项目中仍然可以非法访问没有声明过的依赖包(幽灵依赖)


依赖结构的不确定性


这个怎么理解,为什么会产生这种问题呢?我们来仔细想想,加入有如下一种依赖结构:


依赖1.png


A包与B包同时依赖了C包的不同版本,由于同一目录下不能出现两个同名文件,所以这种情况下同一层级只能存在一个版本的包,另外一个版本还是要被嵌套依赖。


那么问题又来了,既然是要一个扁平化一个嵌套,那么这时候是如何确定哪一个扁平化哪一个嵌套的呢?


依赖2.png


这两种结构都有可能,准确点说哪个版本的包被提升,取决于包的安装顺序!


这就是为什么会产生依赖结构的不确定问题,也是 lock 文件诞生的原因,无论是package-lock.json(npm 5.x 才出现)还是yarn.lock,都是为了保证 install 之后都产生确定的node_modules结构。


尽管如此,npm/yarn 本身还是存在扁平化算法复杂package 非法访问的问题,影响性能和安全。


pnpm


前面说了那么多的npmyarn的缺点,现在再来看看pnpm是如何解决这些尴尬问题的。


什么是pnpm



快速的,节省磁盘空间的包管理工具



就这么简单,说白了它跟npmyarn没有区别,都是包管理工具。但它的独特之处在于:



  • 包安装速度极快

  • 磁盘空间利用非常高效


特性


安装包速度快


p1.png


从上图可以看出,pnpm的包安装速度明显快于其它包管理工具。那么它为什么会比其它包管理工具快呢?


我们来可以来看一下各自的安装流程



  • npm/yarn


npm&yarn.png



  1. resolving:首先他们会解析依赖树,决定要fetch哪些安装包。

  2. fetching:安装去fetch依赖的tar包。这个阶段可以同时下载多个,来增加速度。

  3. wrting:然后解压包,根据文件构建出真正的依赖树,这个阶段需要大量文件IO操作。



  • pnpm


pnpm.png


上图是pnpm的安装流程,可以看到针对每个包的三个流程都是平行的,所以速度会快很多。当然pnpm会多一个阶段,就是通过链接组织起真正的依赖树目录结构。


磁盘空间利用非常高效


pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:



  • 不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink

  • 即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。举个例子,比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的 hardlink,仅仅写入那一个新增的文件


支持monorepo


pnpm 与 npm/yarn 另外一个很大的不同就是支持了 monorepo,pnpm内置了对monorepo的支持,只需在工作空间的根目录创建pnpm-workspace.yaml和.npmrc配置文件,同时还支持多种配置,相比较lerna和yarn workspace,pnpm解决monorepo的同时,也解决了传统方案引入的问题。



monorepo 的宗旨就是用一个 git 仓库来管理多个子项目,所有的子项目都存放在根目录的packages目录下,那么一个子项目就代表一个package



依赖管理


pnpm使用的是npm version 2.x类似的嵌套结构,同时使用.pnpm 以平铺的形式储存着所有的包。然后使用Store + Links和文件资源进行关联。简单说pnpm把会包下载到一个公共目录,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。通过Store + hard link的方式,使得项目中不存在NPM依赖地狱问题,从而完美解决了npm3+和yarn中的包重复问题。


store.jpeg


我们分别用npmpnpm来安装vite对比看一下


npmpnpm
npm-demo.pngpnpm-demo.png
所有依赖包平铺在node_modules目录,包括直接依赖包以及其他次级依赖包node_modules目录下只有.pnpm和直接依赖包,没有其他次级依赖包
没有符号链接(软链接)直接依赖包的后面有符号链接(软链接)的标识

pnpm安装的vite 所有的依赖都软链至了 node_modules/.pnpm/ 中的对应目录。 把 vite 的依赖放置在同一级别避免了循环的软链。


软链接 和 硬链接 机制


pnpm 是通过 hardlink 在全局里面搞个 store 目录来存储 node_modules 依赖里面的 hard link 地址,然后在引用依赖的时候则是通过 symlink 去找到对应虚拟磁盘目录下(.pnpm 目录)的依赖地址。


这两者结合在一起工作之后,假如有一个项目依赖了 A@1.0.0B@1.0.0 ,那么最后的 node_modules 结构呈现出来的依赖结构可能会是这样的:


node_modules
└── A // symlink to .pnpm/A@1.0.0/node_modules/A
└── B // symlink to .pnpm/B@1.0.0/node_modules/B
└── .pnpm
├── A@1.0.0
│ └── node_modules
│ └── A -> /A
│ ├── index.js
│ └── package.json
└── B@1.0.0
└── node_modules
└── B -> /B
├── index.js
└── package.json

node_modules 中的 A 和 B 两个目录会软连接到 .pnpm 这个目录下的真实依赖中,而这些真实依赖则是通过 hard link 存储到全局的 store 目录中。


store


pnpm下载的依赖全部都存储到store中去了,storepnpm在硬盘上的公共存储空间。


pnpmstore在Mac/linux中默认会设置到{home dir}>/.pnpm-store/v3;windows下会设置到当前盘符的根目录下。使用名为 .pnpm-store的文件夹名称。


项目中所有.pnpm/依赖名@版本号/node_modules/下的软连接都会连接到pnpmstore中去。



作者:前端南玖
来源:juejin.cn/post/7239875883254300729
收起阅读 »

运维打工人,周末兼职送外卖的一天

运维打工人,周末兼职送外卖的一天 在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。 早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。...
继续阅读 »

运维打工人,周末兼职送外卖的一天


在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。


早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。


收拾好后,戴上头盔,骑上踏板车,开始了自己的第一次外卖配送之旅。


刚开始,我的心情既紧张又兴奋。手机里的订单提示声是今日的任务号角。第一份订单来自一公里外的一家外卖便利店。我快速地在地图上规划路线,开启高德导航,发动踏板车,朝着目的地出发。


123.jpg


由于便利店在园区里面,转了两圈没找到,这是就慌张了,这找不到店咋办了,没办法赶紧问下旁边的老手骑手,也就顺利找到了,便利店,进门问老板,美团104号好了嘛?老板手一指,在架子上自己看。核对没问题,点击已达到店,然后在点击已取货。


然后在导航去收获目的地,找到C栋,找到107门牌号,紧接敲门,说您好,美团外卖到了,并顺利的送达,然后点击已送达,第一单顺利完成,4.8元顺利到手。


其中的小插曲,送给一个顾客时,手机导航提示目的地,结果一看,周围都拆了。没办法给顾客打电话,加微信确认位置具体在哪里,送达时,还差三分钟,这单就要超时了。


1.jpg


配送过程中,我遇到了第一个难题:找不到店家在哪里,我的内心不禁生出些许焦虑。但很快,我调整心态,不懂不知道的地方,需要多多问人。


紧接着,第二份、第三份订单接踵而至。每一次出发和到达,每一条街道和巷弄,我开始逐渐熟悉。


7.jpg


6.jpg


日落时分,我结束了一天的工作。虽然身体有些疲惫,但内心充满了前所未有的充实感。这份工作让我体验到了不一样的人生角色,感受到了城市节奏背后的种种辛劳与甘甜


周末的兼职跑美团外卖,对我来说不仅是一份简单的工作,更是一段特别的人生经历。它教会了我坚持与责任,让我在忙碌中找到了属于自己的节奏,在逆风中学会了更加珍惜每一次到达。


最后实际周六跑了4个小时,周天跑了7个小时,一共跑了71公里,合计收获了137.80,已提现到账。


5.jpg


2.png


作者:平凡的运维之路
来源:juejin.cn/post/7341669201010425893
收起阅读 »

【Java实战项目】SpringBoot + Vue3打造你的在线电子书平台!

今天给大家分享一个基础的Java实战项目,用SpringBoot和Vue3开发一个电子书平台,大家可以尝试做一下这个项目,以此来检验这段时间的学习成果!废话不多说,下面正式进入项目:一、项目介绍1. 项目简介在线电子书微实战项目是一个实践性的基础项目,主要目的...
继续阅读 »

今天给大家分享一个基础的Java实战项目,用SpringBoot和Vue3开发一个电子书平台,大家可以尝试做一下这个项目,以此来检验这段时间的学习成果!废话不多说,下面正式进入项目:

一、项目介绍

1. 项目简介

在线电子书微实战项目是一个实践性的基础项目,主要目的是通过开发一个在线电子书网站来帮助入门学习和实践相关的技术。

预览链接:在线电子书平台项目实战




该项目涵盖了以下主要功能:

  • 电子书管理:主要包括电子书的基本信息、电子书的章节管理、章节信息和章节内容等。

  • 电子书阅读:用户可以浏览在线电子书,并享受连续翻页、目录导航等阅读体验。


通过该项目,可以深入了解电子书网站的设计和开发过程,学习相关的前端和后端技术,并提升实际项目经验。这个项目不仅有助于理论知识的实践运用,还能够培养问题解决的能力。


2. 项目重点

  • 电子书管理:

实现电子书的结构设计,包括电子书信息、章节列表、章节排序和章节编辑等功能,并支持Md格式的内容编写及预览。



  • 电子书阅读界面设计:

重点关注电子书阅读界面的流畅性和易用性,实现灵活的阅读布局和翻页体验,让用户能够快速定位所需内容。


3. 项目目标

旨在通过实践操作,了解和掌握在线电子书平台的基本功能和技术要点。通过完成这些功能,将获得相关的Web前后端技术、数据库管理技术以及用户界面设计等方面的实践经验,并能够将所学知识应用到实际项目中。


4. 项目技术实现

前端技术实现:

主要基于Vite4 + Vue3作为前端框架来进行开发,利用Vue Router进行路由管理,Axios库进行HTTP请求和响应处理等技术和工具。同时还使用Element UI统一页面风格。


为了实现内容的编辑和预览功能,项目还引入了v-md-editor编辑器组件。通过该组件,用户可以方便地编辑和排版电子书的内容,并实时预览效果。这为用户提供了一个直观、便捷的内容管理方式,使其能够快速编辑、修改和发布电子书的内容。


后端技术实现:

项目采用Spring Boot作为后端框架,通过Spring MVC进行请求处理和路由管理,使用MyBatis作为持久层框架进行数据库操作。后端主要实现了API的对接、电子书管理的逻辑处理以及与前端的数据交互。


5. 实现流程


01

规划和设计:

确定项目需求和功能,并进行整体设计和规划,包括前端界面设计、后端API设计以及数据库结构设计等。

02

搭建前端项目框架:

使用Vue3+vite4等前端框架创建项目,并配置相关开发环境和插件,如Vue Router、Axios等。

03

 开发前端页面和功能:

根据设计,开发前端页面组件和功能模块,包括电子书编辑、预览、章节内容管理等功能。

04

设计和创建数据库:

根据需求设计数据库结构,选择MySQL数据库管理系统,创建数据库和相应的表/集合。

05

开发后端API:

使用SpringBoot后端技术,搭建后端服务器,编写API接口,提供与前端交互的数据处理和业务逻辑。

06

处理前后端数据交互:

前端通过Axios等工具发送HTTP请求,后端接收请求,进行数据处理和验证,返回相应的数据结果。

07

数据库操作和持久化:

后端根据接收到的请求,通过数据库操作进行数据的读取、写入、更新和删除等操作,实现数据的持久化存储。

6. 业务流程及页面效果展示

6.1 电子书管理


6.2 电子书的详情(预览)


6.3 电子书基本信息编辑


6.4 电子书章节管理

6.5 电子书章节内容编辑

  • 导入功能:直接导入 Markdown 文件,并将其作为章节内容展示。



  • 导出功能:将章节的内容以 Markdown 文档的形式进行导出


  • 全屏编辑与实时预览:在全屏模式下使用 Markdown 编辑器对章节内容进行编辑,同时能够实时预览编辑后的效果。




7. 总结

在线电子书功能微实战是一项学习实践型的项目,它涉及到多个技术领域,包括前端、后端、数据库和数据交互等。在学习和实践中,我们需要了解项目的需求和功能,并进行规划和设计。


通过选择合适的技术和工具,开发前端页面和后端API实现完整功能,包括电子书编辑、预览、章节内容管理等。这个项目不仅可以帮助我们了解实际的开发流程和技术应用,还可以提升我们的编程能力和实践经验。


二、部署教程


后端部署文档


1、 后端技术栈

  • Java:项目使用Java编程语言进行开发。

  • Spring Boot:项目基于Spring Boot框架搭建。

  • Maven:项目使用Maven作为构建工具和依赖管理工具。

  • MySQL:项目使用MySQL作为关系型数据库。


2、开发环境准备

项目环境所需:

  • 开发工具 IDEA 版本是IntelliJ IDEA 2023.2 (Ultimate Edition)

  • 使用的 Java 环境是 jdk8

  • Maven 使用的 3.8.1

  • Spring Boot 使用的 2.7.10

  • 接口文档 使用 swagger3

  • MySQL 使用的 8.0.31

  • 工具使用 Navicat


3、项目源码下载


源码下载地址:在线电子书平台项目实战源码


4、 构建项目

解压缩下载包得到:electronic_book.sql 数据库SQL文件 和 electronic-book 项目文件夹


4.1. 导入数据库

新建数据库 electronic_book




导入 electronic_book.sql 数据库SQL文件



点击 开始 导入即可

4.2. 编辑器内 导入项目源码

在IDE编辑器内,打开 electronic-book 项目文件夹;


配置项目MAVEN设置,编辑器内 打开 File - Settings,找到MAVEN设置位置;


设置完成后等待MAVEN加载相关依赖,或后续手动在右侧刷新加载。

4.3. 修改数据库配置

在 resources 下 application.yml 文件内,修改合适配置;


4.4. 接口文档查看测试

  • 启动项目


  • 如果端口占用,在 resources 下 application.yml 文件内修改端口再启动


  • 打开 swagger 接口文档查看是否正常启动



5. 源码解析

5.1. 目录结构

电子书业务相关

  • pojo : 电子书相关pojo 实体类及其他

  • mapper : 电子书相关mapper

  • controller - ElectronicBookController: 电子书相关接口

  • controller - ElectronicBookItemController: 电子书内容相关接口

  • service : 电子书相关接口实现


公共类

  • baseClass : 处理统一业务,如参数校验等

  • config - CrosConfig: 处理测试时可能存在的跨域问题

  • config - SwaggerConfig: swagger文档配置

  • enumeration : 业务枚举等

  • exception : 通用异常返回处理

  • result : 统一结果返回

  • utils : 常用工具类


5.2. README

具体源码解析以及电子书管理介绍和流程等,查看源码文件 read.md以及相关文件内注释详情。



前端部署文档

1. 前端技术栈

vite4 + vue3 + scss(1.69.5) + Typescript(5.2.0)+ @kangc/v-md-editor(2.3.18)

  • Vite是一款快速构建现代web应用程序的构建工具,特别适合于Vue.js应用程序。

  • Vue3是一款流行的JavaScript框架。

  • SCSS是一种CSS预处理器,它允许开发者使用类似编程语言的方式来编写CSS,提高代码的可维护性和重用性。

  • TypeScript是一种由微软开发的静态类型检查的JavaScript超集。

  • @kangc/v-md-editor是基于 Vue 开发的 markdown 编辑器组件。


2. 开发环境

  • Node.js 和 npm 的安装
  • 代码编辑器的选择

    推荐:VSCode + Volar(并禁用Vetur) + TypeScript Vue插件(Volar)


3. 准备项目

3.1. 解压项目源码

将解压缩下载包得到的前端项目文件夹(electronic-book-view),放到到指定的文件夹(例如:E:\workspace_dev\project-practice\electronic-book-view)。


3.2. 更改配置文件
  • 代码编辑器中打开项目源码文件夹
  • api地址配置

点击\src\config.ts

// 配置api地址(以实际项目服务器地址为准)
const baseURL ="http://loacalhost:8087/";


4. 启动项目

4.1. 打开命令行界面
4.2. 安装依赖

在命令窗口或者终端窗口输入命令 npm install


4.3. 构建编译

在命令窗口或者终端窗口输入命令 npm run dev


注意:若端口号3000被占用,会自动重新分配一个空闲的端口号,具体以实际为准。

若启动失败,有报错,请具体问题具体分析,如有不懂也可在云端源想【技术咨询】咨询老师进行答疑解惑(地址:https://www.ydcode.cn/)也可点击文末的阅读原文直接跳转。

4.4. 访问测试

上述启动命令脚本,已经配置默认浏览器打开访问模式,若无正常打开先自行尝试解决,若有问题继续咨询老师解答。


默认打开访问地址为:http://localhost:3000/



4.5. 终止项目

若需要终止项目,首先聚焦在命令窗口或者终端窗口,然后按键CTRL+c即可终止项目。


5. 前端项目说明

请参阅项目的 README.md 文档,其中包含了项目的介绍、安装说明和使用方法等关键信息。以下是其中的重点信息展示:


5.1 引入 markdown 编辑器 介绍

本项目中采用 v-md-editor 进阶版编辑器,进阶版编辑器左侧编辑区域使用 CodeMirror (opens new window)实现。


进阶版编辑器可以根据 CodeMirror 提供的 Api 来自定义扩展编辑区域功能,提高编辑体验。但是文件体积远大于轻量版。使用者可根据所在项目的情况进行选择。

5.2 项目文件结构介绍
/
├── public
│ ├── favicon.ico # vue官方自带标识
│ └── logo.svg # 浏览器标签页logo
├── src # 项目源代码
│ ├── api # 用于存放与后端 API 相关的接口定义。
│ ├── assets # 用于存放项目所需的静态资源文件,例如图片、字体等。
│ ├── components # 用于存放可复用的组件。
│ ├── router # 路由的定义和配置
│ ├── styles # 样式文件
│ │ └── main.scss # 全局的 SCSS 变量等
│ ├── views # 页面组件
│ │ ├──components # 页面所需组件
│ │ └──xxx.vue # 页面
│ ├── app.vue # 应用程序根组件
│ ├── config.ts # 应用程序根组件
│ └── main.ts # 应用程序入口
├── .gitinore # git忽略配置文件
├── env.d.ts # 用于声明环境变量的类型
├── index.html # 整个应用的入口HTML文件
├── package-lock.json # 用于锁定安装时的依赖版本
├── package.json # 应用的配置文件,其中包含了项目的依赖、脚本命令等信息。
├── README.md # 项目的说明文档,通常包含了项目的介绍、安装和使用方法等信息。
├── tsconfig.app.json # 用于前端应用程序的TypeScript编译配置
├── tsconfig.json # TypeScript 项目的配置文件
├── tsconfig.node.json # 用于后端(服务器端)应用程序的TypeScript编译配置
└── vite.config.js # Vite 的配置文件

6. 常见问题处理

  • node和npm版本问题

建议:Vite 4 需要 Node.js 版本 ≥ 16.0.0,npm 版本 ≥ 8.0.0。但是,某些模板需要更高的 Node.js 版本才能工作,如果您的包管理器发出警告,请升级。

  • 依赖安装失败

报错:ERESOLVE unable to resolve dependency tree


三、源码下载

到这里这个运用SpringBoot + Vue3打造在线电子书平台项目介绍及部署就讲完了,小伙伴可以自己尝试写一下这个项目来巩固练习你所学的Java知识哦!

完整的源码下载地址:在线电子书平台源码

收起阅读 »

30岁之前透支,30岁之后还债。

前言 看到不少私信问我为啥没有更新了,我没有一一回复,实在是身体抱恙,心情沉重,加上应付于工作,周旋于家庭,自然挤压了自我空间。 今天思来想去,重新执键,决定久违地又一次写点分享,奉劝大家珍惜身体,愉悦生活。 愉悦二字说来容易,但各位都一样,奔波于现实,劳累于...
继续阅读 »

前言


看到不少私信问我为啥没有更新了,我没有一一回复,实在是身体抱恙,心情沉重,加上应付于工作,周旋于家庭,自然挤压了自我空间。


今天思来想去,重新执键,决定久违地又一次写点分享,奉劝大家珍惜身体,愉悦生活。


愉悦二字说来容易,但各位都一样,奔波于现实,劳累于生活,岂是三言两语就能改变的。


病来如山倒


我又病了,有些意外和突然的,令我措手不及。


一天早上我起来,脖子有些酸,就伸手揉揉捏捏,忽然发现脖颈左侧有一个肿块,仔细拿捏,发现竟然是在里面,而且硬邦邦的,伴有轻微的疼痛感。


当时早上对着镜子拍下来的肿块,我还保留了照片。


1.jpg


立马便一身冷汗冒出,我从未经历过这样的事情,去年身体毕竟出过问题,两相叠加之下,内心更是难以描述。


因为是周一,怀着忐忑的心情去上班了,接下来一直都有些神经兮兮,觉得自己身体出了大问题。


之前我有文章讲过自己去年其实已经检查出血脂的问题,停更半年之久,调养了一番,才真正感觉到身体有所恢复,根据我发文的日期可见一二。


恢复更新的这段时间,报复式地写作和分享,一度不知不觉地排到榜单第二,今天登录看了一下,居然还在月榜前三没下来,也是意外。


话说回来,人一旦身体冒出点病痛,整个心情都显得低沉萎靡,很快就能在方方面面反应出来。


我是硬着头皮上班的,抽空网上查了下好让自己有个心理准备。



百度一搜便是绝症,这是很多年前就知道的,但病急乱投医果然是人之本性,我毅然决然还是搜了。


然后,各种甲状腺之类的就来了,再搜,淋巴瘤也来了,再搜,好家伙,直接恶性肿瘤十有八九了。



面对未知而产生的接近绝望的心情,想必不少人有类似经验。


比如我,下意识先想到的竟然不是我是不是要完蛋了,而是想到自己是家中独子,父母年迈身体有恙,妻子操劳,孩子尚小,家中主要经济来源也是我。


我一旦倒下,实在不敢想,往深了一想各种负面因子都蜂拥而来。


我不知道有多少人和我的性格相似,就是身体出了这种未知的问题,一面觉得应该去医院看看,一面又怕折腾来去最后拿到最不可接受的结果,可能不知道反而能活久一点,大概就是这种心情了。


是的,我大体是个胆子还算大的人,也猛然间抗拒去医院了。


不去医院的结果,就是你每天都在意这个肿块,每天都要摸摸它是不是变小了,是不是消失了,每天都小心呵护着它,甚至还想对它说说话倾诉一下,像是自己偷养的小情人一样。


只盼着某天睡觉醒来,用手一摸,哈哈没有了这样。


我就是差不多一个月都这样惶惶不可终日地度过,直到这周六才被妻子赶去医院做了检查。


透支和还债



30岁之前透支,30岁之后还债。



说来好笑,摸到肿块的第二天吧,还有朋友私信找我合作,换做平时,我肯定欣然接受,并开始设计文稿。


但身体有问题,一切都索然无味了,再次真切地体会到这种被现实打碎一切欲望的撕裂感。


2.png


为什么我30岁之后身体慢慢开始出现各种问题,这两年我有静下心来思考过。


到底还是30岁之前透支太多了,30岁之后你依然养成30岁之前的生活习惯,无异于自杀行为。



我把身体比作一根橡皮筋,它大概只能扯那么长,我长期将它扯那么那么长,我以为它没事,直到有一次我将它扯那么那么那么长,砰的一声它就断了。


我们都无法知道自己的这根橡皮筋到底能扯多长,只要它没断,我们都觉得它还能扯很长,代价就是,只需断一次,你再也无法重来了。



30岁之前,我努力学习各种知识,熬夜那是家常便饭,睡一觉便生龙活虎。


我就像以前上学的三好学生一样,在学校我扎扎实实,放学了我还进补习班,补习班回来了我还上网学知识。


回头想想,真特么离谱啊,我上学都没这样,走上社会了竟然付出了之前在学校几倍的努力。


早知如此,我好好上学读书最后进入一个更优质的圈子,不就少走很多弯路了吗,但是谁又会听当年的老师和父母一番肺腑之言呢。


埋怨过去没有什么意义,只能偶尔借着都市小说幻想一下带着记忆重生回校园的自己。


细数下来,我30岁之前熬过的夜比我加的班还多,我不是天天加班,但好像真的天天熬夜。


可我身体一点问题都没有,我觉得自己不是那种被命运抛弃的人,内心一直这么侥幸,你是不是也和我一样呢。


30岁之后,该来的还是来了,32岁那年,我有一次咳嗽入院,反复高烧,退了又发烧,医生一度以为是新冠,或结核,或白血病什么的,后来全部检查了都不是,发现就是普通的肺部感染。


每天两瓶抗病毒的点滴,大概半个月才逐渐恢复,人都瘦脱相了,这是我人生头一次住院,躺在病床上像废人一样。


等到33岁也就是去年,偶然头晕了一次,那种眩晕,天旋地转,犯恶心,怎么站怎么坐怎么躺都不行,真正要死的感觉。


后面我一度以为是年纪轻轻得了高血压,结果查了下是血脂的问题,还不算严重,但继续下去很可能会变成一些心脑血管疾病。


我难以置信,这可都是老年病啊,我一个30几岁的程序员说来就来了?


调养半年多,肉眼可见身体有好转,我又开始没忍住熬夜了,想做自己的课题,想分享更多的东西,这些都要花时间,而且包括一些其他领域的内容,想得太多,自然花的时间就多。


一不小心就连续熬了一个多月,平均每晚都是2点左右躺下,有时中午还不午休,刷手机找素材。


终于,脖子上起了肿块,让我整个人都蒙圈了,觉得一切努力都是在玩弄自己,忽然间什么都没意思了。


我尽量把这种感受描述出来,希望你们能看明白,真切体会一二。


为什么30岁之后我一熬夜就有问题出现,说白了,30岁之前透支了已经,一来是身体负荷达到临界,二来养成了多年的坏习惯,一时想改还改不过来。



30岁之前真别玩弄自己的身体了xdm,橡皮筋断了就真断了,接不上了,接上了也没以前的弹性了。



健康取决于自律和心情



对于程序员来说,健康取决于两点:自律和心情。



30岁之前,请学会自律,学习时间自律,生活作息自律,一日三餐自律,养成这样的习惯,30岁之后的你会受益匪浅。


自律真的很难,我就是一个很难做到的人,我有倔强地适应过,却又悲哀地失败了。


就像你是一个歇斯底里的人,忽然让你温文尔雅,你又能坚持多久呢。


我用很多鸡汤说服过自己,对于已经30几岁的我来说,也只能维持一段时间。


想看的多,想玩的多,想学的也多,时间是真不够啊,真想向天再借五百年。


我应该算是幸运的那一类,至少我这般透支身体,我还活着,也没用余生去直面绝望。



我用这两年的身体故障给自己上了重要的一课,人死如灯灭。



如果能重来,我一定会学习时间规划,我一定会把每天的时间安排的好好的。


我一定会保证一日三餐不落下,少吃外卖,多吃水果蔬菜。


我一定会保证每晚充足的睡眠,早睡早起,绝不熬夜。


我一定会每天下班和放假抽出一些时间运动和锻炼。


我不是说给自己听的,因为我已经透支了。


我是说给在看文章的你们听的,还年轻点的,还没透支的,请用我的现在当做你可能更坏的未来,早点醒悟,为时不晚。


自律很难,但不自律可能等死,这个选择一点也不难。



工作压力大,作为程序员是避免不了的,所以我以前有劝过大家,薪水的重要性只占一半,你应该追寻一份薪水尚可,但压力一定在承受范围内的工作,这是我认为在国内对于程序员来说相对友好的途径。



我进入IT行业目前为止的整个生涯中,学习阶段听到过传智播客张孝祥老师的猝死,工作阶段听说过附近的4396游戏公司里面30多岁程序员猝死,今年又听到了左耳朵耗子先生的离世。


我想着,那一天,离我和你还有多远。


心情真的很重要,至少能快速反应在身体上。


当我这周六被妻子劝说去检查的时候,我内心一直是紧张的,妻子没去,就在家陪着孩子,跟我说你自己去吧,如果有坏消息就别回复了,等回来再说,如果没什么事那就发个微信。


我想我理解她的意思了,点了点头就骑车去了医院。


医院真不是什么好地方,我就是给医院干活的,我全身上下都讨厌这里。


最煎熬的时间是做彩超前的一个多小时,因为人太多,我得排队,盯着大屏上的号序,我脑子里想了很多事情,甚至连最坏的打算都想好了。


人就很奇怪,越是接近黑暗,越是能回忆起非常多的往事,连高中打篮球挥洒汗水的模样和搞笑的投篮姿势都能想起来。


喊到我的时候,我心跳了一下,然后麻木地进去了,躺下的时候,医生拿着仪器对着我的脖子扫描,此时的我是近一个月以来第一次内心平静,当真好奇怪的感觉。


随着医生一句:没什么事,就一个淋巴结。


犹如审判一般,我感觉一下无罪释放了。


当时听到这句话简直犹如天籁,这会儿想起来还感觉毛孔都在欢快地愉悦。


我问她不是什么肿瘤或甲状腺吧,她说不是,就一个正常的淋巴结,可能是炎症导致了增生,这种一般3个多月至半年才会完全消掉。


这是当时拍的结果


3.jpg


拿给主任医师看了之后,对方也说一点事没有,只是告诫我别再熬夜了。


我不知道人生还会给我几次机会,但我从20几岁到30几岁,都没有重视过这个问题,也没有认真思考过。


直到最近,我才发现,活着真好。


当晚是睡得最踏实的一晚,一点梦都没做,中途也没醒,一觉到天亮。


更离谱的是,早上我摸了一下脖子,竟然真的小了点,这才短短一天,说了都没人信。


我头一次相信,心情真的会影响身体,你心情好了,身体的器官和血液仿佛都欢腾了起来。


如何保持一个好心情,原来这般重要,我拿自己的身体给大家做实验了,有用!



希望大家每天在自律的基础上保持好心情,不负年华,不负自己。



总结


xdm,好好活着,快乐活着。


作者:程序员济癫
来源:juejin.cn/post/7300564263344128051
收起阅读 »

纯前端也可以访问文件系统!

web
前言 周末逛github的时候,发现我们只需要在github域名上加上1s他就能够打开一个vscode窗口来阅读代码,比起在github仓库中查看更加方便 然后我就想网页端vscode能不能打开我本地的项目呢,带着这个疑惑我打开了网页版vscode,它居然真...
继续阅读 »

前言


周末逛github的时候,发现我们只需要在github域名上加上1s他就能够打开一个vscode窗口来阅读代码,比起在github仓库中查看更加方便


vs0.png


然后我就想网页端vscode能不能打开我本地的项目呢,带着这个疑惑我打开了网页版vscode,它居然真的可以打开我本地的项目代码!


vs1.png


难道又出了新的API让前端的能力更进一步了?打开MDN查了一下相关文档,发现了几个新的API


showOpenFilePicker



用来选择文件



vs-f1.png


语法


showOpenFilePicker()

参数



  • options:(可选)包含以下属性

    • multiple:布尔值,默认为false。为true表示允许用户选择多个文件

    • excludeAcceptAllOption:布尔值,默认为false。默认情况下,文件选择器带有一个允许用户选择所有类型文件的过滤选项(展开于文件类型选项中)。设置此选项为 true 以使该过滤选项不可用。

    • types:表示允许选择的文件类型的数组




返回值


返回一个promise对象,会兑现一个包含 FileSystemFileHandle 对象的 Array 数组。


体验


<template>
<div class="open_file" @click="openFile">打开文件</div>
</template>

<script setup lang="ts">
const openFile = async () => {
const res = await window.showOpenFilePicker();
console.log(res);
};
</script>

默认只能打开一个文件,可以传入multiple:true打开多个文件


vs3.png


showDirectoryPicker



用来选择目录



vs2.png


语法


属于浏览器全局方法,直接调用即可


showDirectoryPicker()

参数



  • options:(可选)包含以下属性

    • multiple:布尔值,默认为false。为true表示允许用户选择多个文件

    • excludeAcceptAllOption:布尔值,默认为false。默认情况下,文件选择器带有一个允许用户选择所有类型文件的过滤选项(展开于文件类型选项中)。设置此选项为 true 以使该过滤选项不可用。

    • types:表示允许选择的文件类型的数组




返回值


返回一个promise对象,会兑现一个包含 FileSystemFileHandle 对象的 Array 数组。


体验


<template>
<div class="open_file" @click="openFile">打开文件</div>
<div class="open_file" @click="openDir">打开文件夹</div>
</template>

<script setup lang="ts">
const openFile = async () => {
const res = await window.showOpenFilePicker({
// multiple: true,
});
console.log(res.length);
};

const openDir = async () => {
const res = await window.showDirectoryPicker();
console.log(res);
};
</script>

vs4.png


扩展


FileSystemFileHandle


FileSystemFileHandle提供了一些方法可以用来获取和操作文件



  • getFile:返回一个Promise对象,用于获取文件;

  • createSyncAccessHandle:返回一个FileSystemSyncAccessHandle对象,用于同步访问文件;

  • createWritable:返回一个Promise对象,用于创建一个可写流,用于写入文件;


FileSystemDirectoryHandle


FileSystemDirectoryHandle对象是一个代表文件系统中的目录的对象,它同样提供了方法来获取和操作目录



  • entries:返回一个AsyncIterable对象,用于获取目录中的所有文件和目录;

  • keys:返回一个AsyncIterable对象,用于获取目录中的所有文件和目录的名称;

  • values:返回一个AsyncIterable对象,用于获取目录中的所有文件和目录的FileSystemHandle对象;

  • getFileHandle:返回一个Promise对象,用于获取目录中的文件;

  • getDirectoryHandle:返回一个Promise对象,用于获取目录中的目录;

  • removeEntry:返回一个Promise对象,用于删除目录中的文件或目录;

  • resolve:返回一个Promise对象,用于获取目录中的文件或目录;


entrieskeysvalues这三个方法都是用来获取目录中的所有文件和目录的,它们返回的都是一个AsyncIterable对象,我们可以通过for await...of语法来遍历它。


开发编辑器


了解完这些知识点,我们就可以来开发一个简陋网页版编辑器了,初期只包含打开文件、打开文件夹、查看文件、切换文件


编辑器大概长这样:


vs5.png


打开文件夹


const openDir = async () => {
const res = await window.showDirectoryPicker({});
const detalAction = async (obj: any) => {
if (obj.entries) {
const dirs = obj.entries();
for await (const entry of dirs) {
if (entry[1].entries) {
// 文件夹,递归处理
detalAction(entry[1]);
} else {
// 文件
fileList.value.push({
name: entry[0],
path: obj.name,
fileHandle: entry[1],
});
}
}
}
};
await detalAction(res);
showCode(fileList.value[0], 0);
console.log("--fileList--", fileList);
};

这里主要是递归处理文件夹,返回一个文件列表


读取文件内容


const showCode = async (item: any, index: number) => {
const file = await item.fileHandle.getFile();
const text = await file.text();
codeText.value = text;
currentIndex.value = index;
};

展示文件内容


使用highlight.js来高亮展示代码


<div class="show_code">
<pre v-highlight>
<code class="lang-dart">
{{ codeText }}
</code>
</pre>
</div>

最终效果如下:


vs6.gif


想不到吧,这种功能现在纯前端就能够实现了,当然还可以做的更复杂一点,包括修改保存等功能,保存可以使用showSaveFilePickerAPI,它可以写入文件,同样是返回一个promise。感兴趣的可以试着完善编辑器的功能。


作者:前端南玖
来源:juejin.cn/post/7277045020423045176
收起阅读 »

读《代码整洁之道》有感

最近读了一本书,名字大家都看到了:《代码整洁之道》,之前一直只是听说过这本书的大名,却一直没有进行拜读,最近想起来了就想着看一看,不看不要紧,看了之后就像吃了炫迈,根本停不下来。。。虽然这本书已经出版了十几年的时间,但里面的理论到现在为止也不过时。 有人也许...
继续阅读 »

image.png
最近读了一本书,名字大家都看到了:《代码整洁之道》,之前一直只是听说过这本书的大名,却一直没有进行拜读,最近想起来了就想着看一看,不看不要紧,看了之后就像吃了炫迈,根本停不下来。。。虽然这本书已经出版了十几年的时间,但里面的理论到现在为止也不过时。



有人也许会以为,关于代码的书有点儿落后于时代--代码不再是问题;我们应当关注模型和需求。确实,有人说过我们正在临近代码的终结点。很快,代码就会自动产生出来,不需要再人工编写。程序员完全没用了,因为商务人士可以从规约直接生成程序。扯淡!我们永远抛不掉代码,因为代码呈现了需求的细节。在某些层面上,这些细节无法被忽略或抽象,必须明确之。将需求明确到机器可以执行的细节程度,就是编程要做的事。而这种规约正是代码。



看这本书的那种感觉很奇妙,有时感觉作者说地真对!有时感觉作者骂地真对!有时感觉作者讽刺地真对!还有时看到作者列出的真实代码中的错误示例,再看到作者写出的优化后的代码,内心不禁在想:太妙了,代码本应这样啊!


没错,代码本应该是整洁的,也本应该是好理解、易扩展的!我们常说的设计模式也并不是一种炫技,而是几十年来的老前辈们总结出来的经验,是为了让你的代码更好维护的,是一种理所应当。


在工作中遇到烂代码的可能性是 100%,即使是很厉害的大佬写的代码,在不知情的情况下让你去看,看了一会后都会得出以下结论:“写的啥玩意啊,看都看不懂,乱七八糟的语法糖,考虑过后面工作的人么?什么设计模式,什么各种模块,直接写一块不好么?” 假设的可能有点夸张,但也都是人之常情。有时工作中遇到的烂代码是假的,可能是由于当前自己的技术水平不够,不理解;当然还有一部分可能真的是烂,但是这种情况下还是要做出一些改变!


现在让大家看几个月之前自己写的代码可能都会觉得写的一团糟,用当前的眼光来看可能会有更好的方式或方法来实现,如果你有这种想法的话,请付诸实践!不要等,哪怕是一个单词的拼写错误、一段本不应该写两遍的逻辑、一段没有进行格式化的代码。。。。亦或者是比较大规模的代码改动,改完之后可扩展性会更强,维护起来会更加容易。千万不要等,不要忍受当前的烂代码,代码本就是一直在重构的一个过程,没有哪段代码从出来就不改。下面这段话是书里的内容:



我们都曾经瞟一眼自己亲手造成的混乱,决定弃之而不顾,走向新一天。我们都曾经看到自己的烂程序居然能运行,然后断言能运行的烂程序总比什么都没有强。我们都曾经说过有朝一日再回头清理。当然,在那些日子里,我们都没听过勒布朗(LeBlanc)法则:稍后等于永不(Later equals never).



当然很多人会说:“项目中的屎山代码,我能在上面雕花已经很厉害了,还要干什么,即使我知道那块写的不好,但我也不会去动,因为现在它处于一个稳定的状态,如果我去修改了之后,出了问题全是自己背,吃力不讨好!”这也确实是很多人的现状,考虑的也不无道理,但在这里咱们单纯从代码的角度来看,从写代码的初心来看,早早的就背道而驰了。



代码格式不可忽略,必须严肃对待。代码格式关平沟通而沟通是专业开发者的头等大事。 或许你认为“让代码能工作”才是专业开发者的头等大事。然而,我希望本书能让你抛掉那种想法。你今天编写的功能,极有可能在下一版本中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。原始代码修改之后很久,其代码风格和可读性仍会影响到可维护性和扩展性。即便代码已不复存在,你的风格和律条仍存活下来。



我有代码洁癖,看不了没有格式的代码,看着有的项目中一个函数几百行甚至更多,里面各种重复逻辑,if/else不知道嵌套了多少层,表面看是逻辑复杂,再转念一想,为什么不用工厂、或者写一些别的类来简化下逻辑。这时肯定有人会站起来反对:“明明很简单的逻辑,非得使用什么设计模式,搞得一团乱还看不懂。。” 如果只是一个if/else,或者逻辑比较简单肯定没必要,但是逻辑复杂的情况下光if/else也足够将人搞晕,且当需求改变时代码变得难以维护。


我之前一直觉得写完代码格式化是正常的,是基本操作,但是工作中发现好像不是一个基本操作,格式化并不涉及到专业能力,而是态度,连格式化都懒得做,你说你写出的代码经过了严格的测试。。。。想起之前上学时老师经常说的一句话:作业会不会是能力问题,而做不做就是态度问题了。


说这些并没有什么恶意,仅是这本书的读后感,读完后就好像和作者已经是相识多年的老友,相视一笑。


作者:Zhujiang
来源:juejin.cn/post/7341019035186298915
收起阅读 »

如果让你实现实时消息推送你会用什么技术?轮询、websocket还是sse

web
前言 在日常的开发过程中,我们经常能碰见到需要主动推送消息给客户端数据的业务场景,比如数据大屏幕实时数据,聊天消息推送等等。 本文介绍sse: 服务端向客户端推送数据的方式有哪几种呢? WebSocket SSE 长轮询 轮询简介 长轮询是一种模拟实时通...
继续阅读 »

前言


在日常的开发过程中,我们经常能碰见到需要主动推送消息给客户端数据的业务场景,比如数据大屏幕实时数据,聊天消息推送等等。
本文介绍sse:


image.png
服务端向客户端推送数据的方式有哪几种呢?



  • WebSocket

  • SSE

  • 长轮询


轮询简介


长轮询是一种模拟实时通信的技术。在传统的Http请求中,客户端向服务端发送请求,并且在完成请求后立即响应,然后连接关闭。这意味着客户端需要不停的发送请求来更新数据。


相比之下,长轮询的思想是客户端发送一个Http到服务端,服务端不立即返回响应。相反,服务端会保持该请求打开,直到有新的数据可用或超时。如果有新的数据可用,服务端会立即返回响应,并关闭连接。此时,客户端会重新发起一个新的请求,继续等待新的数据。


使用长轮询的优势在于,它在大部分的浏览器中有更好的兼容性,因为它使用的是Http协议。缺点就是较高的延迟性、较大的资源消耗、以及大量并发操作可能导致服务端资源的瓶颈和一些浏览器对并发请求数目进行了限制比如chorme最大并发数目为6,这个限制前提是针对同一个域名下,超过这一限制后续请求就会堵塞。


websocket简介


websocket是一个双向通信的协议,它支持客户端和服务端彼此之间进行通信。功能强大。
缺点就是是一个新的协议,ws/wss,也就是说支持http协议的不一定支持ws协议。相比较websocket结构复杂并且比较重。


SSE简介


sse是一个单向通讯的协议也是一个长链接,它只能支持服务端主动向客户端推送数据,但是无法让客户端向服务端推送消息。


SSE的优点是,它是一个轻量级的协议,相对于websockte来说,他的复杂度就没有那么高,相对于客户端的消耗也比较少。而且_SSE使用的是http协议_(websocket使用的是ws协议),也就是现有的服务端都支持SSE,无需像websocket一样需要服务端提供额外的支持。


websocket和SSE有什么区别?


轮询


对于当前计算机的发展来说,几乎很少出现同时不支持websocket和sse的情况,所以轮询是在极端情况下浏览器实在是不支持websocket和see的下策。


Websocket和SSE


我们一般的服务端和客户端的通讯基本上使用这两个方案。首先声明:这两个方案没有绝对的好坏,只有在不同的业务场景下更好的选择。


SSE的官方对于SSE和Websocket的评价是



  1. WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向浏览器端发送。

  2. WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在HTTP协议之上的,现有的服务器软件都支持。

  3. SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂。

  4. SSE默认支持断线重连,WebSocket则需要额外部署。

  5. SSE支持自定义发送的数据类型。


Websocket和SSE分别适用于什么业务场景?


对于SSE来说,它的优点就是轻,而且对于服务端的支持度要更好。换言之,可以使用SSE完成的功能需求,没有必要使用更重更复杂的websocket。


比如:数据大屏的实时数据,消息中心的消息推送等一系列只需要服务端单方面推送而不需要客户端同时进行反馈的需求,SSE就是不二之选。


对于Websocket来说,他的优点就是可以同时支持客户端和服务端的双向通讯_。所适用的业务场景:最典型的就是聊天功能。这种服务端需要主动向客户端推送信息,并且客户端也有向服务端推送消息的需求时,Websocket就是更好的选择。


SSE有哪些主要的API?


建立一个SSE链接 :var source = new EventSource(url);


SSE连接状态


source.readyState



  • 0,相当于常量EventSource.CONNECTING,表示连接还未建立,或者连接断线。

  • 1,相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。

  • 2,相当于常量EventSource.CLOSED,表示连接已断,且不会重连。


SSE相关事件



  • open事件(连接一旦建立,就会触发open事件,可以定义相应的回调函数)

  • message事件(收到数据就会触发message事件)

  • error事件(如果发生通信错误(比如连接中断),就会触发error事件)


数据格式


Content-Type: text/event-stream //文本返回格式  
Cache-Control: no-cache  //不要缓存
Connection: keep-alive //长链接标识

如何实操一个SSE链接?Demo↓


这里Demo前端使用的就是最基本的html静态页面连接,没有使用任何框架。后端选用语言是node,框架是Express。


理论上,把这两段端代码复制过去跑起来就直接可以用了。



  1. 第一步,建立一个index.html文件,然后复制前端代码Demo到index.html文件中,打开文件

  2. 第二步,进入一个新的文件夹,建立一个index.js文件,然后将后端Demo代码复制进去,然后在该文件夹下执行


npm init          //初始化npm         
npm i express     //下载node express框架
node index        //启动服务

上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam
每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。


[field]: value\n

上面的field可以取四个值。


-   data
- event
- id
- retry

此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。


: This is a comment

data 字段


数据内容用data字段表示


data:  message\n\n

如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。


data: begin message\n
data: continue message\n\n

下面是一个发送 JSON 数据的例子。


data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

id 字段


数据标识符用id字段表示,相当于每一条数据的编号。


id: msg1\n
data: message\n\n

浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。


event 字段


event字段表示自定义的事件类型,默认是message事件。浏览器可以用addEventListener()监听该事件。




event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n


retry 字段


服务器可以用retry字段,指定浏览器重新发起连接的时间间隔。




retry: 10000\n


两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。


上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。


image.png
前端代码


<!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>Document</title>
</head>
<body>
<ul id="ul"></ul>
</body>
<script>
//生成li元素
function createLi(data) {
let li = document.createElement("li");
li.innerHTML = String(data.message);
return li;
}

//判断当前浏览器是否支持SSE
let source = "";
if (!!window.EventSource) {
source = new EventSource("http://localhost:8088/sse/");
} else {
throw new Error("当前浏览器不支持SSE");
}

//对于建立链接的监听
source.onopen = function (event) {
console.log(source.readyState);
console.log("长连接打开");
};

//对服务端消息的监听
source.onmessage = function (event) {
console.log(JSON.parse(event.data));
console.log("收到长连接信息");
let li = createLi(JSON.parse(event.data));
document.getElementById("ul").appendChild(li);
};

//对断开链接的监听
source.onerror = function (event) {
console.log(source.readyState);
console.log("长连接中断");
};
</script>
</html>



后端代码


const express = require("express"); //引用框架
const app = express(); //创建服务
const port = 8088; //项目启动端口

//设置跨域访问
app.all("*", function (req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
//允许的header类型
res.header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With"
);
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
// 可以带cookies
res.header("Access-Control-Allow-Credentials", true);
if (req.method == "OPTIONS") {
res.sendStatus(200);
} else {
next();
}
});

app.get("/sse", (req, res) => {
res.set({
"Content-Type": "text/event-stream", //设定数据类型
"Cache-Control": "no-cache", // 长链接拒绝缓存
Connection: "keep-alive", //设置长链接
});

console.log("进入到长连接了");
//持续返回数据
setInterval(() => {
console.log("正在持续返回数据中ing");
const data = {
message: `Current time is ${new Date().toLocaleTimeString()}`,
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
});

//创建项目
app.listen(port, () => {
console.log(`项目启动成功-http://localhost:${port}`);
});


20240229103040.gif


参考文章:http://www.ruanyifeng.com/blog/2017/0…


作者:917号先生
来源:juejin.cn/post/7340621143009067027
收起阅读 »

还在直接用localStorage么?全网最细:本地存储二次封装(含加密、解密、过期处理)

web
背景 很多人在用 localStorage 或 sessionStorage 的时候喜欢直接用,明文存储,直接将信息暴露在;浏览器中,虽然一般场景下都能应付得了且简单粗暴,但特殊需求情况下,比如设置定时功能,就不能实现。就需要对其进行二次封装,为了在使用上增加...
继续阅读 »



背景


很多人在用 localStoragesessionStorage 的时候喜欢直接用,明文存储,直接将信息暴露在;浏览器中,虽然一般场景下都能应付得了且简单粗暴,但特殊需求情况下,比如设置定时功能,就不能实现。就需要对其进行二次封装,为了在使用上增加些安全感,那加密也必然是少不了的了。为方便项目使用,特对常规操作进行封装。


结构设计


在封装一系列操作本地存储的API之前,先准备了一个全局对象,对具体的操作进行判断,如下:


interface globalConfig {
type: 'localStorage' | 'sessionStorage';
prefix: string;
expire: number;
isEncrypt: boolean;
}

const config: globalConfig = {
type: 'localStorage', //存储类型,localStorage | sessionStorage
prefix: 'react-view-ui_0.0.1', //版本号
expire: 24 * 60, //过期时间,默认为一天,单位为分钟
isEncrypt: true, //支持加密、解密数据处理
};


  1. type 表示存储类型,为 localStoragesessionStorage

  2. prefix 表示视图唯一标识,如果配置可在浏览器视图中放在前缀显示;

  3. expire 表示过期时间,默认为一天,单位为分钟;

  4. isEncrypt 表示支持加密、解密数据处理;


加密准备工作


这里是用到了 crypto-js 来处理加密和解密,可先下载包并导入。


npm i --save-dev crypto-js

import CryptoJS from 'crypto-js';

crypto-js 设置密钥和密钥偏移量,可以采用将一个私钥经 MD5 加密生成16位密钥获得。


const SECRET_KEY = CryptoJS.enc.Utf8.parse('3333e6e143439161'); //十六位十六进制数作为密钥
const SECRET_IV = CryptoJS.enc.Utf8.parse('e3bbe7e3ba84431a'); //十六位十六进制数作为密钥偏移量

加密


const encrypt = (data: object | string): string => {
//加密
if (typeof data === 'object') {
try {
data = JSON.stringify(data);
} catch (e) {
throw new Error('encrypt error' + e);
}
}
const dataHex = CryptoJS.enc.Utf8.parse(data);
const encrypted = CryptoJS.AES.encrypt(dataHex, SECRET_KEY, {
iv: SECRET_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.ciphertext.toString();
};

解密


const decrypt = (data: string) => {
//解密
const encryptedHexStr = CryptoJS.enc.Hex.parse(data);
const str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
const decrypt = CryptoJS.AES.decrypt(str, SECRET_KEY, {
iv: SECRET_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
};

这两个API都是将获取到的本地存储的value作为参数进行传递,这样就实现了加密和解密。


在传入数据进行处理、改变的时候需要进行解密;
在数据需要传出时需要进行加密。


核心API实现


setStorage 设置值


Storage 本身是不支持过期时间设置的,要支持设置过期时间,可以效仿 Cookie 的做法,setStorage(key, value, expire) 方法,接收三个参数,第三个参数就是设置过期时间的,用相对时间,单位分钟,要对所传参数进行类型检查。可以设置统一的过期时间,也可以对单个值得过期时间进行单独配置。


const setStorage = (key: string, value: any, expire: number = 24 * 60): boolean => {
//设定值
if (value === '' || value === null || value === undefined) {
//空值重置
value = null;
}
if (isNaN(expire) || expire < 0) {
//过期时间值合理性判断
throw new Error('Expire must be a number');
}
const data = {
value, //存储值
time: Date.now(), //存储日期
expire: Date.now() + 1000 * 60 * expire, //过期时间
};
//是否需要加密,判断装载加密数据或原数据
window[config.type].setItem(
autoAddPreFix(key),
config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data),
);
return true;
};

getStorageFromKey 根据key获取value


首先要对 key 是否存在进行判断,防止获取不存在的值而报错。对获取方法进一步扩展,只要在有效期内就可以获取 Storage 值,如果过期则直接删除该值,并返回 null。


const getStorageFromKey = (key: string) => {
//获取指定值
if (config.prefix) {
key = autoAddPreFix(key);
}
if (!window[config.type].getItem(key)) {
//不存在判断
return null;
}
const storageVal = config.isEncrypt
? JSON.parse(decrypt(window[config.type].getItem(key) as string))
: JSON.parse(window[config.type].getItem(key) as string);
const now = Date.now();
if (now >= storageVal.expire) {
//过期销毁
removeStorageFromKey(key);
return null;
//不过期回值
} else {
return storageVal.value;
}
};

getAllStorage 获取所有存储值


const getAllStorage = () => {
//获取所有值
const storageList: any = {};
const keys = Object.keys(window[config.type]);
keys.forEach((key) => {
const value = getStorageFromKey(key);
if (value !== null) {
//如果值没有过期,加入到列表中
storageList[key] = value;
}
});
return storageList;
};

getStorageLength 获取存储值数量


const getStorageLength = () => {
//获取值列表长度
return window[config.type].length;
};

removeStorageFromKey 根据key删除存储值


const removeStorageFromKey = (key: string) => {
//删除值
if (config.prefix) {
key = autoAddPreFix(key);
}
window[config.type].removeItem(key);
};

clearStorage 清空存储列表


const clearStorage = () => {
window[config.type].clear();
};

autoAddPreFix 基于全局配置的prefix参数添加前缀


const autoAddPreFix = (key: string) => {
//添加前缀,保持浏览器Application视图唯一性
const prefix = config.prefix || '';
return `${prefix}_${key}`;
};

这是一个不导出的函数,作为整体封装的内部工具函数,在setStorage、getStorageFromKey、removeStorageFromKey会使用到。


导出函数列表


提供了6个函数的处理能力,足够应对实际业务的大部分操作。


export {
setStorage,
getStorageFromKey,
getAllStorage,
getStorageLength,
removeStorageFromKey,
clearStorage,
};

使用


在实际业务中使用,则将函数导入即可,这里先看下笔者的文件目录吧:
在这里插入图片描述
实际使用:


import {
setStorage,
getStorageFromKey,
getAllStorage,
getStorageLength,
removeStorageFromKey,
clearStorage
} from '../../_util/storage/config'

setStorage('name', 'fx', 1)
setStorage('age', { now: 18 }, 100000)
setStorage('history', [1, 2, 3], 100000)
console.log(getStorageFromKey('name'))
removeStorageFromKey('name')
console.log(getStorageFromKey('name'))
console.log(getStorageLength());
console.log(getAllStorage());
clearStorage();

接下来看一下浏览器视图:


在这里插入图片描述


可以看到,key经过处理加入了config.prefix的前缀,有了唯一性。
value经过了加密处理。


再看一下通过get方式获取到的控制台值输出:


在这里插入图片描述


很完美,实际业务会把前缀清除返回进行处理,视图中有前缀绑定以及加密处理,保证了本地存储的安全性。


完整代码


config.ts:


import { encrypt, decrypt } from './encry';
import { globalConfig } from './interface';

const config: globalConfig = {
type: 'localStorage', //存储类型,localStorage | sessionStorage
prefix: 'react-view-ui_0.0.1', //版本号
expire: 24 * 60, //过期时间,默认为一天,单位为分钟
isEncrypt: true, //支持加密、解密数据处理
};

const setStorage = (key: string, value: any, expire: number = 24 * 60): boolean => {
//设定值
if (value === '' || value === null || value === undefined) {
//空值重置
value = null;
}
if (isNaN(expire) || expire < 0) {
//过期时间值合理性判断
throw new Error('Expire must be a number');
}
const data = {
value, //存储值
time: Date.now(), //存储日期
expire: Date.now() + 1000 * 60 * expire, //过期时间
};
//是否需要加密,判断装载加密数据或原数据
window[config.type].setItem(
autoAddPreFix(key),
config.isEncrypt ? encrypt(JSON.stringify(data)) : JSON.stringify(data),
);
return true;
};

const getStorageFromKey = (key: string) => {
//获取指定值
if (config.prefix) {
key = autoAddPreFix(key);
}
if (!window[config.type].getItem(key)) {
//不存在判断
return null;
}

const storageVal = config.isEncrypt
? JSON.parse(decrypt(window[config.type].getItem(key) as string))
: JSON.parse(window[config.type].getItem(key) as string);
const now = Date.now();
if (now >= storageVal.expire) {
//过期销毁
removeStorageFromKey(key);
return null;
//不过期回值
} else {
return storageVal.value;
}
};
const getAllStorage = () => {
//获取所有值
const storageList: any = {};
const keys = Object.keys(window[config.type]);
keys.forEach((key) => {
const value = getStorageFromKey(autoRemovePreFix(key));
if (value !== null) {
//如果值没有过期,加入到列表中
storageList[autoRemovePreFix(key)] = value;
}
});
return storageList;
};
const getStorageLength = () => {
//获取值列表长度
return window[config.type].length;
};
const removeStorageFromKey = (key: string) => {
//删除值
if (config.prefix) {
key = autoAddPreFix(key);
}
window[config.type].removeItem(key);
};
const clearStorage = () => {
window[config.type].clear();
};
const autoAddPreFix = (key: string) => {
//添加前缀,保持唯一性
const prefix = config.prefix || '';
return `${prefix}_${key}`;
};
const autoRemovePreFix = (key: string) => {
//删除前缀,进行增删改查
const lineIndex = config.prefix.length + 1;
return key.substr(lineIndex);
};

export {
setStorage,
getStorageFromKey,
getAllStorage,
getStorageLength,
removeStorageFromKey,
clearStorage,
};


encry.ts:


import CryptoJS from 'crypto-js';

const SECRET_KEY = CryptoJS.enc.Utf8.parse('3333e6e143439161'); //十六位十六进制数作为密钥
const SECRET_IV = CryptoJS.enc.Utf8.parse('e3bbe7e3ba84431a'); //十六位十六进制数作为密钥偏移量

const encrypt = (data: object | string): string => {
//加密
if (typeof data === 'object') {
try {
data = JSON.stringify(data);
} catch (e) {
throw new Error('encrypt error' + e);
}
}
const dataHex = CryptoJS.enc.Utf8.parse(data);
const encrypted = CryptoJS.AES.encrypt(dataHex, SECRET_KEY, {
iv: SECRET_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.ciphertext.toString();
};

const decrypt = (data: string) => {
//解密
const encryptedHexStr = CryptoJS.enc.Hex.parse(data);
const str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
const decrypt = CryptoJS.AES.decrypt(str, SECRET_KEY, {
iv: SECRET_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
};

export { encrypt, decrypt };


interface.ts:


interface globalConfig {
type: 'localStorage' | 'sessionStorage';
prefix: string;
expire: number;
isEncrypt: boolean;
}

export type { globalConfig };


总结


前端开发中直接使用明文存储在本地是比较常见的一件事情同时也是不安全的一件事,对本地存储进行二次封装可以提高安全性,并且有了API的支持,可以在本地存储操作时更加简单。


作者:sorryhc
来源:juejin.cn/post/7237441562664681529
收起阅读 »

如何成为领导眼中不可或缺的人

这篇文章主要还是阅读所感,总结出来共勉,不喜勿喷,也欢迎有不同意见的指教。 首先,主要是以问题起步,如何成为一个优秀的架构师? 这个问题主要分成两种情况,其一就是所谓的面霸架构师,其二则是领导眼中不可或缺的人。 第一种,只要经历过项目的架构,并且理解项目背后...
继续阅读 »

这篇文章主要还是阅读所感,总结出来共勉,不喜勿喷,也欢迎有不同意见的指教。



首先,主要是以问题起步,如何成为一个优秀的架构师?


这个问题主要分成两种情况,其一就是所谓的面霸架构师,其二则是领导眼中不可或缺的人。


第一种,只要经历过项目的架构,并且理解项目背后所要解决的场景问题,以及把里面所用到的技术背后的原理搞清楚,那么你就是一个面霸架构师,可是面霸架构师并不一定就是领导眼中不可或缺的人。那么问题就来到了,如何成为一个领导眼中不可或缺的人。


以下以一个大佬的真实经历来解答这个问题。



ps: 下文中的我指 xx 大佬。



无关职责,帮助领导解决技术难题


第一个经历,我工作第三年的时候,认识了一个老板。某个周六,他打电话给我,说他们的系统碰到了一个问题,做了某一个操作之后,整个页面就会冻结,怎么点都没有用,他们的技术人员没有头绪,客户也一直在催,让我赶紧帮忙看看。


我打开看了一下,的确是做了操作之后,整个界面都无法点击或者输入信息了,然后我重现了很多次问题,发现整个界面冻结的时候,好像颜色有点不一样,会不会是一个透明的浮层置顶了?最终确认,确实是 bug 造成浮层没有退出。


那么为什么他们的技术人员都没有头绪?因为他们主要是后端开发,js 工作经验比较少,而我的经历也属于偏向后端,可是我能跟领导说,这个问题不属于我的专业范围,因为我是做后端的吗?


领导并不会在乎你的职责是什么,领导喜欢的是可以帮他解决技术难题的人。



总结: 要有能够帮助领导解决技术难题的能力(无关职责)。



理解领导的非技术难题


第二个经历,这是我在外企当中碰到的一个问题。


有一天,公司的领导过来问我,"你有没有觉得我们的开发速度很慢?是不是我们的技术不行?"


"您能跟我详细说说,是哪些地方慢?"


"产品部的人跟我说,他们现在提一个需求,经常要好几个月才能上线,有时候一个简单改文字的需求都是这样。"


这个问题确实不好解释清楚,因为这次沟通一开始就不在一个维度上。开发人员认为,说开发速度慢,应该是指开始开发到最终上线的时间久。可是领导认为,一个需求从提出到最终上线的时间久,就是开发速度慢。


那么开发速度慢算是一个技术问题吗?可以算,也可以不算。那么最终如何解决?开发团队最终一起讨论列出了所有影响开发效率的问题,然后能用技术解决就用技术解决。


所以有时候领导跟你谈的问题并不是单纯的技术问题,你需要把领导的问题转化成技术可以解决的问题。



总结: 理解领导的非技术难题,并最终转化成技术可以解决的问题。



搞清楚领导对你的期望值


可能有人会想,开发效率低这种事情不应该找架构师,这是管理的问题。下面接着说第三个经历。xx 公司系统用了 4 年,架构相对比较老旧,然后有一个新的项目,需求比较多,一位架构师提议,趁着这次的需求把架构更新一下,


然后就跟领导仔细讨论了架构更新的代价和好处,最终达成了一致的意见。


可是任何一个项目都会有各种各样的变数,比如业务方的临时需求变更,再比如有些系统可以不用迁移,只需要对接一下,也会有有一部分人不熟悉新架构,就需要多花一点时间去学习,最终项目延期了。


某一次会议,领导说架构师不行,这次系统上线以后如果不稳定,就把架构师开了,主要还是因为项目延期了。开发也解释了一下不全是架构师的问题,还有一些需求变更,可是领导认为需求改动不大,不至于延期一两个月。然后开发团队私下商量着保住这个架构师,不能让他一个人担责。


后来一次聚餐,领导解释他的压力也很大,本来跟老板说好可以按时完成,结果拖了这么久。


事后团队回顾了一下,这件事情之所以是架构师担责,其实最重要的一个原因就是老板对架构师的期望值是什么,是开发效率,系统稳定性保障,还是复杂问题的突破?


所以整件事由架构师承担的原因就是,大家对架构师的期望值是不一样的。所以,作为架构师最重要的一点就是要明白公司对你的期望值。



总结: 明白公司或者领导对你的期望值。



最后


当然,以上经历并不能代表所有公司的评判标准,并且架构师的优秀也有很多维度可以讲,但是这三个故事可以给我们一些启发。



  1. 要有能够帮助领导解决技术难题的能力(无关职责)。

  2. 理解领导的非技术难题,并最终转化成技术可以解决的问题。

  3. 明白公司或者领导对你的期望值。


以上的三个要点,个人也比较赞同的。


作者:夕水
来源:juejin.cn/post/7340834858275389492
收起阅读 »

为了不和测试扯皮, 我抄了这个vite插件

web
前言 这算是我的第二个vite插件了,第一个是腾讯云OSS上传,思路以及部份代码借鉴了浏览器API调用工程师的项目,在此基础上完善和增加配置项。 如果文章对你有帮助的话,记得一键三连哟。有问题和疑惑的话也可以在评论区留言。我会第一时间回复大家,如果觉得我的文章...
继续阅读 »

前言


这算是我的第二个vite插件了,第一个是腾讯云OSS上传,思路以及部份代码借鉴了浏览器API调用工程师的项目,在此基础上完善和增加配置项。


如果文章对你有帮助的话,记得一键三连哟。有问题和疑惑的话也可以在评论区留言。我会第一时间回复大家,如果觉得我的文章哪里有知识点错误的话,也恳请能够告知,把错的东西理解成对的,无论在什么行业,都是致命的。


背景


项目环境比较多,经常漏发错发版本,导致测试和开发间的扯皮比较多。于是,这个插件便截天地气运而生🤪🤪🤪。


功能特性


目的: 在控制台显示当前运行代码的构建人、构建时间、分支、最新的COMMIT信息等, 方便确认是否漏发错发版本。

注意: 只在GitLab流水线有用


效果预览


可以在控制台查看代码的部署信息



安装


pnpm i -D vite-plugin-gitlab-flow

或者


yarn add -D vite-plugin-gitlab-flow

或者


npm i -D vite-plugin-gitlab-flow

基本使用


vite.config.js/ts中配置


import vitePluginGitLabFlow from "vite-plugin-gitlab-flow";

plugins: [
vitePluginGitLabFlow({
projectName: '榕树工具',
debug: true,
extra: [
{
keys: 'VITE_APP_TITLE',
label: '项目title'
}
],
styles:{
color: 'red'
}
}),
]

配置项


optionsdescriptiontypedefault
projectName?项目名称,默认取package.json里的name字段。stringpackage.name
debug?debug模式booleanfalse
extra?额外需要显示的字段,需要是env里面有的字段,可开启debug模式查看string[]
styles?自定义样式Style{}

实现原理


import type { Plugin, HtmlTagDescriptor } from 'vite';
import dayjs from 'dayjs';
import fs from 'fs';
import {Properties,PropertiesHyphen} from 'csstype';
interface Style extends Properties, PropertiesHyphen {}
export const defaultStyle:Style = {
color: 'white',
background: 'green',
'font-size': '16px',
border: '1px solid #fff',
'text-shadow': '1px 1px black',
padding: '2px 5px',
}

interface GitLabFlowOptions {
projectName?: string,
debug?:boolean,
extra?:{
label:string
keys:string,
}[],
styles?:Style
}
export default function gitLabFlow(options: GitLabFlowOptions={}): Plugin {
let {debug=false,extra=[],styles=defaultStyle}=options
let styleOption=''
for (const styleOptionKey in styles) {
styleOption+=`${styleOptionKey}:${styles[styleOptionKey]};`
}
const env = process.env;

const pkg:any = JSON.parse(fs.readFileSync(process.cwd() + '/package.json', 'utf-8'));

let packageInfo: any = JSON.parse(fs.readFileSync(process.cwd() + '/node_modules/vite-plugin-gitlab-flow/package.json', 'utf-8'))

const appInfo = {
projectName: options.projectName || pkg.name,
name:pkg.name,
version:pkg.version,
lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
};

let extStr=`
console.log("%c插件名称:
${packageInfo.name} 当前版本: V${packageInfo.version}","${styleOption}" );
console.log("%c插件作者:
${packageInfo.author} 仓库地址: ${packageInfo.homepage}","${styleOption}");
console.log("%c项目名称:
${appInfo.projectName}", "${styleOption}");
console.log("%c打包时间:
${appInfo.lastBuildTime}","${styleOption}");
console.log("%c流水线执行人:
${env.GITLAB_USER_NAME || '-'}", "${styleOption}");
console.log("%c标签:
${env.CI_COMMIT_REF_NAME || '-'}", "${styleOption}");
console.log("%cCOMMIT信息:
${env.CI_COMMIT_TITLE || '-'} ${env.CI_COMMIT_SHA || '-'}", "${styleOption}");
`


// 新增自定义字段
extra.forEach(({label,keys})=>{
extStr+=`console.log("%c${label}: ${env?.[keys] || '-'}","${styleOption}");`
})

// debugger模式
if (debug){
extStr+=`console.log('appInfo', ${JSON.stringify(appInfo)});`
extStr+=`console.log('packageInfo', ${JSON.stringify(packageInfo)});`
extStr+=`console.log('env', ${JSON.stringify(env)});`
}

return {
name: 'vite-plugin-gitlab-flow',
apply: 'build',
transformIndexHtml(html): HtmlTagDescriptor[] {
return [
{
tag: 'script',
attrs: { defer: true },
children: extStr,
injectTo: 'body'
},
]
}
};
}

引用



作者:小猪努力学前端
来源:juejin.cn/post/7211428447921422396
收起阅读 »

勇敢的人先拿到结果

上周许久未见的大学学长叫我出去喝酒,他这次来贵阳是为开分店的事情而来的,他比我高一个年级,在我毕业的时候,他就自己开始做生意了,短短两三年,到现在他已经开了七八个分店了,还在不断发展,并且加盟的人也不少,平均下来,现在每个月的收入也是很可观的。 对于我们这种末...
继续阅读 »

上周许久未见的大学学长叫我出去喝酒,他这次来贵阳是为开分店的事情而来的,他比我高一个年级,在我毕业的时候,他就自己开始做生意了,短短两三年,到现在他已经开了七八个分店了,还在不断发展,并且加盟的人也不少,平均下来,现在每个月的收入也是很可观的。


对于我们这种末流二本院校毕业的学生,特别还是在贵州这个经济相对比较落后的地区,拿到这个成绩还是挺厉害的,并且这个收入并不是固定的,还是不断增长。


学长是学市场营销的,这也算是个天坑专业,所以那会他就知道自己将来肯定是从事不了这个行业的,所以自己就在宿舍开了一个小卖部,每天下课后就骑着电瓶车去送货,虽然每个月赚不了多少钱,但是对于做生意这一块,他的思维肯定是得到了锻炼。


因为我们是在广西读书,所以螺蛳粉就比较多,在毕业后,他就去柳州考察做螺蛳粉,联系好各种渠道后,回到贵州就直接开干。


因为那会贵州的各个市里面卖螺蛳粉的还很少,并且没有特色和品牌效应,所以自己就先设计名称,logo,最后先开了一个店铺,自己亲自下厨,因为比较有特色,一个月直接干到了全市螺蛳粉餐饮销量的第二名。


随后又开了第二家,第三家......别人在看到他赚了钱后,其它市区的人也纷纷向他学习,他自己就收加盟费用,现在他要做的事情就是玩,还有考察门店,然后扩展。


从他的事迹中,我说两个点。


勇于放弃


对于很多人而言,读书的目的就是为了找一份稳定的工作,最好是体制内。


如果你读完大学后出去做销售,做生意,那么对于你身边的很多人而言,他们会觉得你这个大学白读了,因为在他们眼中,只有坐在办公室里面才是最体面了。


你和他说做生意,创业这些东西,他会给你说:这些不稳,以后没有退休工资。


但是如果你真听他们的,那么后面后悔的一定是你。


就像学长,如果他也和别人一样毕业后回到自己那地方加入考编大军,那么他现在肯定和别人一样,也在背书,焦虑,但是他选择了其它的路。


这时候有些人就会抬杠:考上了就能吃一辈子,而你做生意如果运气不好那么就直接亏光,到时候你就知道编制的香了。


这也是很多人的通病。


我觉得如果一件事情你看不到希望,就别过于去迷恋它,舍不得它,不然会被它束缚,比如学历,经验等等。


敢想敢干


可能你会觉得他家里应该有底子的,不然毕业后怎么就能开店。


但是我们问一下自己,就算你家里有底子,毕业后就给你十万块让你开店,你觉得你行吗?恐怕大部分人都不知道自己该做什么吧。


首先躬身入局本身就是一件很难的事情,我们多数人能够拼命上班,但是如果让你脱离平台去自己干一件事就比登天还难。


因为你在公司有别人给你安排好,你去做就行了,换句话来说,你就是个干苦力的,真让你去谈判,去闯市场,大多数人是没这个能力的。


这也是一种损失厌恶心态,因为你怕自己花时间去做,到后面不仅亏了钱,还把自己弄得很累,而安安稳稳打工不一样,它是“稳赚不赔”的。


但是这个世界上很难有稳赚不赔的东西,就说安安稳稳打工拿工资,但是工资不高,那一定是在亏着走的,除非你觉得自己的时间毫无价值,那么就是赚的。


作者:苏格拉的底牌
来源:juejin.cn/post/7340898858556178432
收起阅读 »

实战(简单):20分钟页面不操作,页面失效

web
如果没有时间想直接解决问题,看最下面的最终代码即可 场景需求 总结: 20分钟内如果不操作,页面就是提示失效并且回到列表页面,如果操作了,计时就会清零。 如果 A 在编辑,B 点击编辑会提示正在编辑。A 在编辑期间,每分钟会向后端发送续租(即正在编辑...
继续阅读 »

如果没有时间想直接解决问题,看最下面的最终代码即可



场景需求


image.png

总结:




  1. 20分钟内如果不操作,页面就是提示失效并且回到列表页面,如果操作了,计时就会清零。

  2. 如果 A 在编辑,B 点击编辑会提示正在编辑。A 在编辑期间,每分钟会向后端发送续租(即正在编辑)的请求,后端收到请求后,会在服务端帮你保留这一分钟的编辑状态,别人就无法在编辑了。并且别人编辑时,后端会返回相应的信息。



前言


乐了,产品提出了需求,然后我去找导师问问团队中有没有现成的解决方案。。。没有,然后导师提出了 web worker 的思路,让我自己思考解决方案。好吧,那就开始吧。


一开始,我想着能否能用 setInterval 来进行定时的,结果后端发来消息


image.png

emm......后端大佬,惹不起~


如图上所说,如果切换了页面,setInterval 会停止计时的(咱就说不信的可以试试),也就是说这个线程被停止了。


那么就需要新建一个线程,也就是 web worker 了,用它单纯来进行计时,不用管其他逻辑,切换页面也不会终止。


正文思路


基本demo


首先,百度了下 web worker 的基本实现案例,一文彻底学会使用web worker


需要该需求的页面


// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
</template>

<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名

// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('Greeting from Main.js');

myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息
console.log(e.data); // Greeting from Worker.js,worker线程发送的消息
});

</script>


放入 public 文件下 worker.js


// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
self.postMessage('Greeting from Worker.js'); // 向主线程发送消息
});

其中,worker.js 的存放路径和 new Worker()里的值有关,比如此时我是在本地资源的根路径创建的 /worker.js ,那么就是放在public下的。


而如果是 ./worker.js,或者 ../worker.js,这是无法找到的,因为此时的 worker.js 已经被打包编译成了 app.js。



注意,public 文件的变动需要重启项目,和 vue.config.js一样



image.png
worker.js 和 主线程通信走通后,开始分析需求了。


1. 每分钟续租一次 =》 1秒钟续租一次


什么叫续租,每分钟你向服务端发送一个续租请求,后端就会帮你保持正在编辑的状态(假设为 edit: true),而且后端其实也在计时一分钟。在这一分钟内,由于 edit 为 true,如果别人想要编辑,就会拒绝别人的编辑。如果你一分钟后没发送这个续租请求,后端会把 true 改成 false,这时别人想要编辑,后端就会接受别人的编辑了。


因此,前端就需要每隔一分钟发送一次续租请求,来维持此时的编辑状态。


当然,由于产品要求的更复杂,你发送续租请求的时候请求头往往会携带用户信息,来反馈谁在进行编辑以提高用户体验感。


下述代码为了更好的测试,把每分钟续租变为了每秒续租一次


2. 20分钟期间不操作就会提示页面失效 =》 10秒钟一到就会触发提示事件


当然,就算 setInterval 不能作为解决方案,但还是需要用它来做定时器的,这还是挺香的。


// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息
setInterval(() => {
self.postMessage('Greeting from Worker.js'); // 向主线程发送消息
}, 1 * 1000)
});

如上代码,Greeting from Worker.js 这条消息每隔 1 秒钟就会向 editEmail.vue 页面发送,这时就算你切换浏览器标签页也仍然会发送。


好,简单的定时器做完了,那就开始进行计时了。


// worker.js(worker线程)
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息

let sum = 0;
let msg;

setInterval(() => {
sum += 1;
msg = {
text: 'editing',
sum
}
self.postMessage(msg); // 向主线程发送消息 msg 对象
}, 1 * 1000)
});

每过一秒,worker.js 都会发送一次信息,用来持续触发续租事件,而 sum 则是用来进行计时过了多少秒。


image.png


// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
</template>

<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名

// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('start');

myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息,每隔一秒都会接收到
console.log(e.data); // {text: 'editing', sum: sum},worker线程发送的消息
campaignListLock(); // 发起续租请求
if (e.data.sum >= 10) {
message.error("页面失效");
PageGoBack(); // 返回上一页,看产品要求
}
});

</script>


image.png

OK,这样,基本的需求就完成了,10 秒一到就会提示页面失效,并且在这 10 秒内谁都无法进入编辑页面(在进入编辑页面前得先向后端请求看看是否有人在编辑)。


但是,10 秒后呢,这个计时器仍然在进行中,所以我需要在 10 秒过后清除这个计时器了。也就是在 e.data.sum >= 10 这个条件内对 worker 进程进行通信,触发清除事件。


// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
<input @change="onChange" />
</template>

<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名

// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('start');

myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息,每隔一秒都会接收到
console.log(e.data); // {text: 'editing', sum: sum},worker线程发送的消息
campaignListLock(); // 发起续租请求
if (e.data.sum >= 10) {
message.error("页面失效");
PageGoBack(); // 返回上一页,看产品要求
myWorker.postMessage('end');
}
});

</script>


在这里我们分别向 worker 进程发送了 startend 两个信息,worker 进程拿到信息后进行判断,如果是 start,那么就开始每秒续租,如果为 end,那么就清除定时器来终止续租(即停止每秒向主线程进行通信来触发续租请求)。


// worker.js(worker线程)
let timer;
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息

let sum = 0;
let msg;

if (e.data === "start") {
timer = setInterval(() => {
sum += 1;
msg = {
text: '编辑中',
sum
}
self.postMessage(msg); // 向主线程发送消息 msg 对象
}, 1 * 1000)
} else {
clearInterval(timer);
}
});

如上代码,定义一个全局变量 timer 用来存储定时器,以便能够随时清除。


image.png

定时重置



Stop,别冲太猛,这里我们需要总结一下了



开启定时


myWorker.postMessage('start');

就会重新 worker.js 中的 self.addEventListener('message',()=>{}) 函数,sum 重置为 0,计时重新开始计算。


停止定时


myWorker.postMessage('end');

就会触发 worker 中的 clearInterval(timer) 来清除定时器


重置定时


myWorker.postMessage('end');
myWorker.postMessage('start');

先清除定时器停止定时,然后再重新开启定时


最后


// 开启定时
const onTimeStart = () => {
myWorker.postMessage('start');
}
// 停止定时
const onTimeEnd = () => {
myWorker.postMessage('end');
}
// 重置定时
const onTime = () => {
onTimeEnd();
onTimeStart();
}

3. 10 秒内如果进行了表单操作则重置计时


const onChange = () => {
onTime();
}

优化代码


// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
<input @change="onChange" />
</template>

<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名

// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('start');

// 开启定时
const onTimeStart = () => {
myWorker.postMessage('start');
}
// 停止定时
const onTimeEnd = () => {
myWorker.postMessage('end');
}
// 重置定时
const onTime = () => {
onTimeEnd();
onTimeStart();
}

myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息,每隔一秒都会接收到
console.log(e.data); // {text: 'editing', sum: sum},worker线程发送的消息
campaignListLock(); // 发起续租请求
if (e.data.sum >= 10) {
message.error("页面失效");
PageGoBack(); // 返回上一页,看产品要求
onTimeEnd(); // 停止计时,终止续租
}
});

const onChange = () => {
onTime();
}

</script>


// worker.js(worker线程)
let timer;
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息

let sum = 0;
let msg;

if (e.data === "start") {
timer = setInterval(() => {
sum += 1;
msg = {
text: 'editing',
sum
}
self.postMessage(msg); // 向主线程发送消息 msg 对象
}, 1 * 1000)
} else {
clearInterval(timer);
}
});

而到这里,只是实现了单纯的停留在页面,但切换浏览器标签页时,没有做相应的监听事件。虽然有着另一个 worker 线程在运行着,但当你切换页面后过 10s 再返回原页面,提示虽然会有,但是一闪即逝,基本看不到提示信息。


4. 切换浏览器标签页


而监听浏览器标签页的切换事件是 visibilitychangedocument.visibilityStat 属性


document.addEventListener("visibilitychange", function () {
if (document.visibilityState == "visible") {
message.error("页面已失效");
} else if (document.visibilityState == "hidden") {
message.error("页面已隐藏");
}
});

其中的隐藏我们并不需要用到,而且过了 10s 后如果反复的切换标签,“页面已失效”的提示会反复的弹出,因为我们并没有进行控制。


此时我们也需要区分过了 10s 后用户是停留在当前页面还是离开了页面又返回了。


如果是停留,那么页面属性为 visible。如果是返回,那么就需要监听 visibilitychange 事件并且页面属性为 visible


let timeCount = 0; // 全局中定义变量,用以控制切换标签页后的提示次数。

myWorker.addEventListener("message", (e) => {
if (e.data.notime >= 10) {
onTimingEnd();
if (document.visibilityState === "visible") {
message.error("页面已失效");
pageGoBack();
timeCount = 1; // 触发了提示就禁止它后续再触发
}
document.addEventListener("visibilitychange", function () {
if (document.visibilityState == "visible" && timeCount == 0) {
message.error("页面已失效");
pageGoBack();
timeCount = 1; // 触发了提示就禁止它后续再触发
}
});
}
})

最终代码


替换成20分钟了


// editEmail.vue(主线程)
<template>
<div>编辑页面哦</div>
<input @change="onChange" />
</template>

<script>
const myWorker = new Worker('/worker.js'); // 创建worker,函数里的值为 webWorker 文件名

// 向 worker.js 线程发送消息,对应 worker.js 线程中的 e.data
myWorker.postMessage('start');

// 开启定时
const onTimeStart = () => {
myWorker.postMessage('start');
}
// 停止定时
const onTimeEnd = () => {
myWorker.postMessage('end');
}
// 重置定时
const onTime = () => {
onTimeEnd();
onTimeStart();
}

myWorker.addEventListener('message', e => { // 从 worker.js 那接收消息,每隔一秒都会接收到
console.log(e.data); // {text: 'editing', sum: sum},worker线程发送的消息
campaignListLock(); // 发起续租请求
if (e.data.sum >= 20) { // 超过 20 分钟,终止续租并提示页面失效
onTimingEnd();
if (document.visibilityState === "visible") {
message.error("页面已失效");
pageGoBack();
timeCount = 1; // 触发了提示就禁止它后续再触发
}
document.addEventListener("visibilitychange", function () {
if (document.visibilityState == "visible" && timeCount == 0) {
message.error("页面已失效");
pageGoBack();
timeCount = 1; // 触发了提示就禁止它后续再触发
}
});
}
});

const onChange = () => {
onTime();
}

</script>


// worker.js(worker线程)
let timer;
self.addEventListener('message', e => { // 接收到消息
console.log(e.data); // Greeting from Main.js,主线程发送的消息

let sum = 0;
let msg;

if (e.data === "start") {
timer = setInterval(() => {
sum += 1;
msg = {
text: 'editing',
sum
}
self.postMessage(msg); // 向主线程发送消息 msg 对象
}, 60 * 1000) // 每分钟 sum 加 1 标识积累了 1 分钟
} else {
clearInterval(timer);
}
});

作者:吃腻的奶油
来源:juejin.cn/post/7340636105765535796
收起阅读 »

通过ip查询归属地 要小心了

背景 最近公司做了一些营销活动,投入资金进行了流量推广,pv、UV都做了统计。老板说,我要看下用户的区域分布的数据。 以前的文章我讲过,pv、UV如何统计?我们是基于ip进行统计的。用的ip能获取到,那通过ip查询归属地就ok了。 思维扩展下,ip 查询归属地...
继续阅读 »

背景


最近公司做了一些营销活动,投入资金进行了流量推广,pv、UV都做了统计。老板说,我要看下用户的区域分布的数据。


以前的文章我讲过,pv、UV如何统计?我们是基于ip进行统计的。用的ip能获取到,那通过ip查询归属地就ok了。


思维扩展下,ip 查询归属地的的场景还蛮多的,我列举一些:


场景



  1. 网络安全调查:当发生网络攻击或恶意行为时,通过查询IP地址的归属地可以帮助调查人员追踪攻击者的位置和身份,进而采取相应的应对措施。

  2. 电商网站反欺诈:电商平台可以通过查询IP的归属地来检测是否有异常行为,如异地登录或使用虚假身份信息下单,从而防止欺诈行为发生。

  3. 广告定向投放:在在线广告市场中,根据用户所在地区进行IP归属地查询可以帮助广告主精准定位目标受众,提高广告投放效果和ROI。

  4. 地理位置服务:地图应用、天气预报和周边生活服务等可以利用IP归属地查询来确定用户的大概地理位置,提供个性化的地理服务和信息。

  5. 网站流量分析:网站管理员可以利用IP归属地查询来分析网站访问的地域分布情况,评估市场覆盖范围,制定针对性的营销策略和内容优化计划。


这些具体的使用场景说明了IP归属地查询在网络安全、营销推广、个性化服务等方面的重要作用,能够帮助用户更好地理解用户行为和优化业务流程。


谷歌搜索了下,第三方提供的ip查询归属地服务,挺多的,但是收费、收费、收费!!!免费也有些,但是怕不稳定。


无意间找到了ip2region这个项目,一直持续维护更新,试用后,效果杠杆的。那我们怎么用的,继续往下看


ip2region


Ip2region 是什么


ip2region - 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现。


Ip2region 特性


1、IP 数据管理框架


xdb 支持亿级别的 IP 数据段行数,默认的 region 信息都固定了格式:国家|区域|省份|城市|ISP,缺省的地域信息默认是0。 region 信息支持完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


2、数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


3、极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


Ip2region 支持那些语言


Ip2region大部分主流语言都支持,支持的语言如下:



Ip2region怎么用


在这里,我以golang语言作为演示,其他语言,可以看下官方文档


例子:我需要查询ip为:218.63.140.248 的归属地


下载ip2region.xdb包


访问ip2region 项目,ip的库文件在data目录下,点击下载即可



package 获取


go get github.com/lionsoul2014/ip2region/binding/golang

完全基于文件的查询


package main

import (
"fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"time"
)

func main() {
//dbPath写入你下载的ip2region.xdb文件的路径,我这里放在了当前目录下
var dbPath = "ip2region.xdb"
searcher, err := xdb.NewWithFileOnly(dbPath)
if err != nil {
fmt.Printf("failed to create searcher: %s\n", err.Error())
return
}
defer searcher.Close()
// 查询218.63.140.248对应的地址
var ip = "218.63.140.248"
var tStart = time.Now()
region, err := searcher.SearchByStr(ip)
if err != nil {
fmt.Printf("failed to SearchIP(%s): %s\n", ip, err)
return
}
fmt.Printf("{region: %s, took: %s}\n", region, time.Since(tStart))
// 备注:并发使用,每个 goroutine 需要创建一个独立的 searcher 对象。
}

查询结果


此ip的归属地为: 中国云南省昆明市电信



缓存整个 xdb 数据


可以预先加载整个 ip2region.xdb 到内存,完全基于内存查询,类似于之前的 memory search 查询。


package main

import (
"fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"time"
)

func main() {
//dbPath写入你下载的ip2region.xdb文件的路径,我这里放在了当前目录下
var dbPath = "ip2region.xdb"
// 1、从 dbPath 加载整个 xdb 到内存
cBuff, err := xdb.LoadContentFromFile(dbPath)
if err != nil {
fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err)
return
}

// 2、用全局的 cBuff 创建完全基于内存的查询对象。
searcher, err := xdb.NewWithBuffer(cBuff)
if err != nil {
fmt.Printf("failed to create searcher with vector index: %s\n", err)
return
}
defer searcher.Close()
// 查询218.63.140.248对应的地址
var ip = "218.63.140.248"
var tStart = time.Now()
region, err := searcher.SearchByStr(ip)
if err != nil {
fmt.Printf("failed to SearchIP(%s): %s\n", ip, err)
return
}
fmt.Printf("{region: %s, took: %s}\n", region, time.Since(tStart))
// 备注:并发使用,每个 goroutine 需要创建一个独立的 searcher 对象。
}

查询结果:



方案比对



  • 基于文件的查询,响应时间:38us

  • 基于缓存的查询,响应时间:10.29µs


生成环境使用建议使用方式为:基于缓存的查询


生产如何使用


以上的演示,只是个demo,如果要放在线上如何使用呢?



  1. 以sdk的形式嵌入到项目,使用基于缓存的查询方式。

  2. ip查询的场景很多,可以单独构建一个ip查询的公共服务,提高给各个业务线使用


sdk接入的方式,用到的业务线都需要对接一次,ip2region.xdb如果有更新,所有用到的项目都要自己去更新升级db文件,维护成本太高。如果你的项目比较单一,sdk接入也是不错的


我们的方案:因为我业务线相对太多,如果各个业务线自己接,维护的成本太高。我们决定构建IP查询归属地公共服务,往外提供查询的能力。后续服务的升级、维护等,统一在公共服务里面来做。


作者:柯柏技术笔记
来源:juejin.cn/post/7340950101534982179
收起阅读 »

一个小公司的技术开发心酸事

背景 长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。 自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给...
继续阅读 »

背景


长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。


自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给发。


当时老板的要求就是尽力降低人力成本,尽快的开发出来App(Android+IOS),老板需要尽快的运营起来。


初期的技术选型


当时就自己加上一个刚毕业的纯前端开发以及一个前面招聘的ui,连个人事、测试都没有。


结合公司的需求与自己的技术经验(主要是前端和nodejs的经验),选择使用如下的方案:



  1. 使用uni-app进行App的开发,兼容多端,也可以为以后开发小程序什么的做方案预留,主要考虑到的点是比较快,先要解决有和无的问题;

  2. 使用egg.js + MySQL来开发后端,开发速度会快一点,行业比较小众,不太可能会遇到一些较大的性能问题,暂时看也是够用了的,后期过渡到midway.js也方便;

  3. 使用antd-vue开发运营后台,主要考虑到与uni-app技术栈的统一,节省转换成本;


也就是初期选择使用egg.js + MySQL + uni-app + antd-vue,来开发两个App和一个运营后台,快速解决0到1的问题。


关于App开发技术方案的选择


App的开发方案有很多,比如纯原生、flutter、uniapp、react-native/taro等,这里就当是的情况做一下选择。



  1. IOS与Android纯原生开发方案,需要新招人,两端同时开发,两端分别测试,这个资金及时间成本老板是不能接受的;

  2. flutter,这个要么自己从头开始学习,要么招人,相对于纯原生的方案好一点,但是也不是最好的选择;

  3. react-native/taro与uni-app是比较类似的选择,不过考虑到熟练程度、难易程度以及开发效率,最终还是选择了uni-app。


为什么选择egg.js做后端


很多时候方案的选择并不能只从技术方面考虑,当是只能选择成本最低的,当时的情况是egg.js完全能满足。



  1. 使用一些成熟的后端开发方案,如Java、、php、go之类的应该是比较好的技术方案,但对于老板来说不是好的经济方案;

  2. egg.js开发比较简单、快捷,个人也比较熟悉,对于新成员的学习成本也很低,对于JS有一定水平的也能很快掌握egg.js后端的开发


中间的各种折腾


前期开发还算顺利,在规定的时间内,完成了开发、测试、上线。但是,老板并没有如前面说的,很快运营,很快就盈利,运营的开展非常缓慢。中间还经历了各种折腾的事情。



  1. 老板运营遇到困难,就到处找一些专家(基本跟我们这事情没半毛钱关系的专家),不断的提一些业务和ui上的意见,不断的修改;

  2. 期间新来的产品还要全部推翻原有设计,重新开发;

  3. 还有个兼职的领导非要说要招聘原生开发和Java开发重新进行开发,问为什么,也说不出什么所以然,也是道听途说。


反正就是不断提出要修改产品、设计、和代码。中间经过不断的讨论,摆出自己的意见,好在最终技术方案没修改,前期的工作成果还在。后边加了一些新的需求:系统升级1.1、ui升级2.0、开发小程序版本、开发新的配套系统(小程序版本)以及开发相关的后台、添加即时通信服务、以及各种小的功能开发与升级;


中间老板要加快进度了就让招人,然后又无缘无故的要开人,就让人很无奈。最大的运营问题,始终没什么进展,明显的问题并不在产品这块,但是在这里不断的折腾这群开发,也真是难受。


明明你已经很努力的协调各种事情、站在公司的角度考虑、努力写代码,却仍然无济于事。


后期技术方案的调整



  1. 后期调整了App的打包方案;

  2. 在新的配套系统中,使用midway.js来开发新的业务,这都是基于前面的egg.js的团队掌握程度,为了后续的开发规范,做此升级;

  3. 内网管理公用npm包,开发业务组件库;

  4. 规范代码、规范开发流程;


人员招聘,团队的管理


人员招聘


如下是对于当时的人员招聘的一些感受:



  1. 小公司的人员招聘是相对比较难的,特别是还给不了多少钱的;

  2. 好在我们选择的技术方案,只要对于JS掌握的比较好就可以了,前后端都要开发一点,也方便人员工作调整,避免开发资源的浪费。


团队管理


对于小团队的管理的一些个人理解:



  1. 小公司刚起步,就应该实事求是,以业务为导向;

  2. 小公司最好采取全栈的开发方式,避免任务的不协调,造成开发资源的浪费;

  3. 设置推荐的代码规范,参照大家日常的代码习惯来制定,目标就是让大家的代码相对规范;

  4. 要求按照规范的流程设计与开发、避免一些流程的问题造成管理的混乱和公司的损失;

    1. 如按照常规的业务开发流程,产品评估 => 任务分配 => 技术评估 => 开发 => 测试 => cr => 上线 => 线上问题跟踪处理;



  5. 行之有效可量化的考核规范,如开发任务的截止日期完成、核心流程开发文档的书写、是否有线上bug、严谨手动修改数据库等;

  6. 鼓励分享,相互学习,一段工作经历总要有所提升,有所收获才是有意义的;

  7. 及时沟通反馈、团队成员的个人想法、掌握开发进度、工作难点等;


最后总结及选择创业公司避坑建议!important



  1. 选择创业公司,一定要确认老板是一个靠谱的人,别是一个总是画饼的油腻老司机,或者一个优柔寡断,没有主见的人,这样的情况下,大概率事情是干不成的;

    1. 老板靠谱,即使当前的项目搞不成,也可能未来在别的地方做出一番事情;



  2. 初了上边这个,最核心的就是,怎么样赚钱,现在这种融资环境,如果自己不能赚钱,大概率是活不下去的@自己;

  3. 抓住核心矛盾,解决主要问题,业务永远是最重要的。至于说选择的开发技术、代码规范等等这些都可以往后放;

  4. 对上要及时反馈自己的工作进度,保持好沟通,老板总是站在更高一层考虑问题,肯定会有一些不一样的想法,别总自以为什么什么的;

  5. 每段经历最好都能有所收获,人生的每一步都有意义。

作者:qiuwww
来源:juejin.cn/post/7257085326471512119
收起阅读 »

丈母娘,你这是来真的啊?

今年春节在家里呆的时间比往年都要久一些,就算离家不远也会这样感觉,随着年龄的增加,越发思念故土以及父母。 以往回家都比较晚,待着待着就想着回工作的城市。因为回家总会伴随着和二老的争论,而且我那时也很戾气,合不到一块就气,或者进房间睡觉。 要说啥事能值得争论,我...
继续阅读 »

今年春节在家里呆的时间比往年都要久一些,就算离家不远也会这样感觉,随着年龄的增加,越发思念故土以及父母。


以往回家都比较晚,待着待着就想着回工作的城市。因为回家总会伴随着和二老的争论,而且我那时也很戾气,合不到一块就气,或者进房间睡觉。


要说啥事能值得争论,我的确忘了,大概是思想观念以及家里生意的问题产生的。以至于身处异乡也要离开家里不愿意喋喋不休。


随着时间流逝,心态已经发生变化。我内心深处逐渐向着故土,和家里二老的关系也潜移默化,彼此心照不宣的拉近了很多。


二老也不过多问责一些,也不再以长者的心态和我对质。


事实也觉得普通家的孩子成年后就该放权干他自己的事,父母少些干预,有些做法和观念已经帮不上忙。父母与孩子之间往往都会产生很大分歧,尤其是思想观念的矛盾。


每一代都有可能推翻上一代的思想意识。父母与子女和谐,一般父母的思想观念足够前卫能够传承给子女,或者子女的思想观念没有很大差异,认可父母的思想体系。


我们会一起坐下来交谈家里的事情,想好对策处理外事,规划老家祠堂以及屋子的安排;参与布置家里,代替老爹出席各项习俗礼节活动。


从读大学开始到现在就没再给它们制造任何的麻烦和搞砸事情。只见他坐在店里抽着烟,沉默着望向我,迟迟不再发话,只在最后说了一句婚姻的问题。


这个话题别说他了,老家众多的亲戚都会抛出这个尖锐的问题,我说我回家只想休息,逢年过节和老朋友叙旧,去亲戚家串串门,一起乐呵乐呵聊聊家常,聊聊八卦。


95‘ 后教师彭老师


小学同学彭老师,现在是一名县城重点中学的高中老师。她是一名 90 后老师,我在她身上看到了很多老师的缩影。她毕业后第一份工作就是任高一班主任兼任课老师。


图片


▲图/ 学生:???


彭老师很是焦虑很迷茫,害怕自己不能胜任;害怕自己教不好;害怕自己耽误学生的前途。


每一个担心都很沉重,责任肩负在身上,没日没夜的备课,改作业,演练怎么让学生听懂。


图片


▲图/ 在最无能为力的学科遇到了最不想辜负的老师


小城市义务教育向来重视成绩不太注重心理建设,导致问题学生很多。彭老师经常 solo 学生和家长,试图说服他们把重心放在当前阶段要做的事。


尽管生病,喉咙发炎也坚持做着这些事情。


图片


▲图/ 除夕夜,彭老师回学校,顺道提零食带红包给她


图片


▲图/ 她赠了一罐饮料,握着只觉比当时天气还冷


图片
▲图/ 彭老师收到开工红包请吃饭,还给了红包,呜呜呜感动


有时煎熬的对着同事和朋友说,看着自己的希望学生开始堕落真的很难受,说也说不通;面对自己的学生读着读着因家里或者各种的问题而辍学时更无语。


图片


▲图/ 00 后学生在 95 后老师教案涂鸦


忍不住会直接联系家长 solo,开导家长有时候如同开山凿石一样,只有坚持不懈才使得那位学生继续读下去,尽管读大专,也改变了不少走向。


有一天她苦笑的跟我们说,她自己都没怎么谈过恋爱却还要开导失恋的学生,甚至被失恋学生反问:“老师,你谈过恋爱吗?”


???


彭老师所带的班级在 23 年第一批结业。上本率超额完成上面给的任务,这一路上面临学生堕落、早恋、辍学、逃课、网瘾...


彭老师有时候很想放弃摆烂,像那些老道的老师一样,风清云淡,看开一点,上课就上课,喝茶就喝茶,晒太阳就晒太阳。


后来她骑着小电瓶,向我重申了她那坚定的信念,绝不可能摆烂,我要对我的学生负责,要为他们的前途着想,就这么决定了。


我看着她感觉头上出现了一顶为人师表的光环。


图片


实话说,老师的行为无论是怎样的,都会被学生刻在记忆深处,尽管有时不会联系,也会在某一刻回想起,念其良莠。


图片


▲图/ 念大学的学生探望彭老师


她性格一直都很逗比很乐观,走路也喜欢蹦蹦跳跳,还被她的老师和学生嘲笑她走路蹦蹦跳跳,和她玩王者鲁班的走姿如出一辙。


在她学生毕业之后,彭老师回归万年鲁班,但技术依然停留在四五年前大学生时期,经常遭截杀一路“啊”,开疾跑徒走回水晶,也经常被我们和她的学生给护着。


现在见她时还是很逗比,嘴硬心软。脸上刻印出一副班主任的形象,坚定严肃而又亲和。


她说她带完一届之后不当班主任了,太辛苦太累了。后面她又被安排带复读班班主任。


阿姨,你来真的啊?


老家的天气很不错,逢年过节我们经常互串亲戚的门,晒晒太阳,欺负欺负小朋友,欺负过头了说送给他们一份《三年高考五年模拟卷》礼物,他们哭的更厉害。


图片


▲图/帮人带孩子真好玩


春节假期充电的时间正在倒计时,最后赶着串堂弟的门,也就是大叔家,那时他们家里很忙,家族里的会做饭打点的都来帮忙了。


原来是堂弟未来丈母娘查家环节,家里上上下下忙活。


我等闲杂人在巷子里晒着太阳,准备迎接他们。堂弟我和同岁,月份比我小,在温州工作,这几年他老爹操够了心,费了不少钱。龙年又长了一岁,他父母更是着急。


图片


▲图/ 回家路途中天色很震撼,大家都不想说话


闲聊之际,只见一列车齐刷刷地开到门口,不知道的还以为是迎亲来了。


二叔见状连忙赶上去打招呼并指挥停车泊位。只见那丈母娘下车后整理着装,望着周边的房屋装饰,一脸严肃对着二叔说位置有些偏远,绕来绕去的,二叔连忙解释可能走的道不一样,走国道会很顺。


亲友团齐刷刷招呼张罗着进去喝茶,握手,递烟,倒茶。摆了三桌才能坐下,我们自己人在旁边站着观望或帮忙。两方互相寒暄之后,不到十分钟对方便开始切入主题,商量儿女婚姻问题。


丈母娘吐露堂弟家位置偏僻,路道不好走,绕了很多弯才抵达,彩礼需要增加 3w 到 5w,作为她女儿的嫁衣钱。


阿姨,你这是来真的啊?


大叔一边忙着圆场有不同的道路可以走,镇与镇之间来往有很多路可走也方便,一边递烟倒茶使其思量再三。


丈母娘依然坚定不移,重申了一遍,对方亲戚应声附和。


大叔脸色像是喝酒上了头一样,随后陷入沉思,见其态度坚硬,且事已至此,作出退步可以增加 1w - 2w,给到对方女儿身上。


前些天,大叔带着堂弟相亲到女方家,据说对方开价 35.8w 彩礼,回礼是购买房之后支援 10w+,不知其是否商量后的价格。


只见那天大叔来我家喝茶水时带着儿子和未来儿媳去了县城买了“五金”, 4w 左右。


见此事既成,并不买二叔的帐,坚持需要增加 3w,并声称给女儿做嫁衣。


大叔又陷入了思考,心里计算着账面,上个月女儿刚嫁出去,彩礼还没捂热,就要付之东流,是为不甘且又无奈。


场面陷入了安静,对方只管握着茶杯吃着果子等待结果,势必做好了撤离的打算。


姑舅们遂即递烟倒茶聊家常。


堂弟陪同坐着喝茶望着对象低下了头,儿媳妇安静得陪在身边,挽着堂弟的手。


掂量之后大叔同意了对方的要求,双方态度方能缓和很多,继续喝茶,商量事宜,聊家常,聊孩子幸福。


饭后,互相道了别,每人随了红包礼。只觉得对方结婚习俗没有讲明白有点遗憾,但愿不会阻碍他两组建一个幸福的小家庭。


我和表弟坐在沙滩上,对着河扔石子打水漂,谁都不想再提,心里比谁都清楚。却和群里的伙伴嘲笑着自己家的那位是否也要几十个 w。


作者:程序员小榆
来源:juejin.cn/post/7336822951273824282
收起阅读 »

架构: 自由表单设计界面布局

web
简介 设计表单设置界面是为了给用户提供一个直观、吸引人的操作界面,方便用户对表单进行个性化的设置。就像我们日常生活中玩游戏一样,游戏中的设置界面可以让我们调整游戏音量、画面效果等,以获得更好的游戏体验。 设计表单设置界面可以让用户自定义表单的外观和功能。就像换...
继续阅读 »

简介


设计表单设置界面是为了给用户提供一个直观、吸引人的操作界面,方便用户对表单进行个性化的设置。就像我们日常生活中玩游戏一样,游戏中的设置界面可以让我们调整游戏音量、画面效果等,以获得更好的游戏体验。


设计表单设置界面可以让用户自定义表单的外观和功能。就像换装一样,通过设置界面,用户可以选择不同的颜色、字体、图标等,将表单变得更漂亮、更符合自己的喜好。不仅如此,用户还可以设置表单的输入验证规则、选项布局等,使表单更加智能、便捷,确保数据的准确性和一致性。


此外,设计吸引人的表单设置界面还可以提升用户的参与感和满足感。当用户看到一个漂亮、有趣的设置界面时,不仅能够激发他们的兴趣和好奇心,还能给他们一种参与到表单设计的快乐感。通过提供丰富的个性化设置选项和交互效果,用户可以将自己的想法和创意融入到表单中,让表单更有个性和趣味性。


设计表单设置界面还可以提供简洁明了的操作流程和指导。一个好的设置界面应该具备清晰的布局和明确的操作步骤,用户在使用时能够快速找到需要的设置选项,并且清楚地知道如何进行设置。同时,设置界面还应该提供一些提示和建议,帮助用户更好地理解设置选项的作用和影响,减少用户的困惑和错误操作。


如何设计表单操作界面?


模块划分


为了更好地讲解该部分内容,我们可以直接上手设计一个通用的表单操作界面。如下图所示,可以看到它差不多涵盖了我们常见的大部分组件……XXXX



从图中我们可以看到,主要包括了如下三个大的模块:



  • 表单设计器模块:这是自由表单设计的核心模块,用户通过该模块可以创建、编辑和配置表单的各个元素,如输入字段、选项、验证规则等。表单设计器通常提供简单直观的界面,让用户可以轻松地拖拽和调整表单元素的位置、大小和样式。

  • 表单属性设置模块:这个模块用于设置表单的基本属性,如表单名称、描述、提交按钮文本等。用户可以在该模块中对表单的基本信息进行编辑和修改。

  • 字段属性设置模块:用户可以选择一个字段或元素,然后在该模块中设置该字段的属性,如标签文本、默认值、是否必填、验证规则等。通过字段属性设置模块,用户可以灵活地对表单中的各个字段进行个性化的配置和定制。


当然,如果加上表单预览实际效果,那就是锦上添花。因此,我们还可以加上个“表单预览模块”用于显示用户设计的表单的实时预览效果,使用户可以随时查看表单的外观和布局。在该模块中,用户可以模拟填写表单,测试表单的交互和功能。具体显示效果如下:




当然,除了以上四个模块,还有其他的模块点,具体可以按照我们个人或者公司的需求,进行合理扩展。


步骤


设置低代码自由表单的操作界面,可以按照以下步骤进行。



  1. 确定操作界面的目标和功能:明确你希望操作界面能够实现什么功能,满足哪些需求。

  2. 设计主要操作区域:确定主要的操作区域,通常会包括表单编辑、保存、提交等功能按钮。

  3. 定义表单字段的布局和样式:根据表单的字段分组情况,确定各字段在操作界面的布局方式,如垂直排列、水平排列等。为字段选择合适的输入控件和标签样式,并考虑必要的验证信息。

  4. 设计其他操作元素:除了表单字段,还可以考虑添加其他操作元素,如工具栏、菜单、侧边栏等,用于辅助用户进行操作和导航。

  5. 考虑交互设计:确定用户与操作界面进行交互的方式,包括按钮点击、字段编辑、菜单选项等。确保用户能够直观地理解和使用操作界面。

  6. 设计响应式布局:如果需要在不同设备上使用操作界面,确保界面能够根据不同屏幕尺寸进行自适应布局,保证在各种设备上都能显示良好的用户体验。

  7. 进行布局调整和优化:根据实际效果进行布局调整和优化,确保界面的整体美观和易用性。


实战规划


经过上面大致的设计介绍,我想你应该有了初步的认识,也大概有了个设计思路。那么,接下来我们开始规划和设计实战项目中的表单设计页面的布局了。


布局规划


先来看下这张规划图:



共分四个模块:



  1. 表单元素;

  2. 元素布局;

  3. 属性设置(包括字段属性、布局属性及其他,而这里只针对字段属性来实践);

  4. 预览。



其中预览模块可以另起一个页面,更为直观。用户可以随时查看表单的外观和布局。还可以提供一些交互功能,例如表单提交的模拟按钮,让用户可以体验表单实际的交互效果。


设计的界面,并不需要你有多花里胡哨,相反,界面的简洁性和易用性,直观的操作方式和清晰的视觉指引,才可帮助用户快速熟悉并使用该低代码自由表单工具。同时,界面设计也应该考虑不同设备和屏幕尺寸的适配(这里针对 PC 和 h5 做一个示范),以确保在不同的平台上都能够良好地展示和操作。


流程模版开发规划-扩展


当我们封装了表单设计这一块功能后,就可以进一步与流程结合


如下例子:


假设我们封装了一个低代码自由表单设计的功能,我们可以将表单设计与流程管理结合使用,实现一个请假流程的应用。



  1. 表单设计:用户使用表单设计功能创建一个请假表单,包括请假类型、请假时间、请假事由等字段。可以设置字段的属性,例如是否必填、格式校验等。

  2. 流程设计:用户使用流程管理功能创建一个请假流程,包括审批节点、流程图设计、流程条件等。可以设置不同节点的审批人、流程条件,以及流程的流转路径。

  3. 表单与流程关联:在表单设计界面中,用户可以将请假表单与请假流程进行关联。可以选择使用该表单作为流程的申请表单,并将表单的字段与流程的变量进行映射。

  4. 表单数据与流程集成:当用户填写请假表单并提交后,表单的数据将被保存,并触发请假流程的启动。流程将根据流程设计中的条件和审批人设定,自动进行流转和审批。

  5. 审批和处理:在流程进行中,审批人可以通过流程管理界面进行审批操作。审批人可以查看已提交的请假表单数据,根据情况进行批准或拒绝,并可以填写审批意见等备注信息。

  6. 流程状态和统计:用户可以通过流程管理界面查看每个请假流程的状态、进度和统计信息。可以了解每个流程当前所处的节点,各个节点的审批状态,以及整个流程的总体情况。


通过将表单设计和流程管理功能结合,我们可以实现一个完整的请假申请流程,简化了传统的请假流程管理流程,同时减少了开发的工作量,提高了工作效率


作者:糖墨夕
来源:juejin.cn/post/7302965547087527977
收起阅读 »

在开源项目中看到一个改良版的雪花算法,现在它是你的了。

你好呀,我是歪歪。 在 Seata 的官网上看到一篇叫做“关于新版雪花算法的答疑”的文章。 seata.io/zh-cn/blog/… 看明白之后,我觉得还是有点意思的,结合自己的理解和代码,加上画几张图,给你拆解一下 Seata 里面的“改良版雪花算法...
继续阅读 »

你好呀,我是歪歪。


在 Seata 的官网上看到一篇叫做“关于新版雪花算法的答疑”的文章。



seata.io/zh-cn/blog/…




看明白之后,我觉得还是有点意思的,结合自己的理解和代码,加上画几张图,给你拆解一下 Seata 里面的“改良版雪花算法”。


虽然是在 Seata 里面看到的,但是本篇文章的内容和 Seata 框架没有太多关系,反而和数据库的基础知识有关。


所以,即使你不了解 Seata 框架,也不影响你阅读。


当你理解了这个类的工作原理之后,你完全可以把这个只有 100 多行的类搬运到你的项目里面,然后就变成你的了。


你懂我意思吧。



先说问题


如果你的项目中涉及到需要一个全局唯一的流水号,比如订单号、流水号之类的,又或者在分库分表的情况下,需要一个全局唯一的主键 ID 的时候,就需要一个算法能生成出这样“全局唯一”的数据。


一般来说,我们除了“全局唯一”这个基本属性之外,还会要求生成出来的 ID 具有“递增趋势”,这样的好处是能减少 MySQL 数据页分裂的情况,从而减少数据库的 IO 压力,提升服务的性能。


此外,在当前的市场环境下,不管你是啥服务,张口就是高并发,我们也会要求这个算法必须得是“高性能”的。


雪花算法,就是一个能生产全局唯一的、递增趋势的、高性能的分布式 ID 生成算法。


关于雪花算法的解析,网上相关的文章比雪花还多,我这里就不展开了,这个玩意,应该是“面试八股文”中重点考察模块,分布式领域中的高频考题之一,如果是你的盲区的话,赶紧去了解一下。


比如一个经典的面试题就是:雪花算法最大的缺点是什么?



背过题的小伙伴应该能立马答出来:时钟敏感。


因为在雪花算法中,由于要生成单调递增的 ID,因此它利用了时间的单调递增性,所以是强依赖于系统时间的。


如果系统时间出现了回拨,那么生成的 ID 就可能会重复。


而“时间回拨”这个现象,是有可能出现的,不管是人为的还是非人为的。


当你回答出这个问题之后,面试官一般会问一句:那如果真的出现了这种情况,应该怎么办呢?


很简单,正常来说只要不是不是有人手贱或者出于泄愤的目的进行干扰,系统的时间漂移是一个在毫秒级别的极短的时间。


所以可以在获取 ID 的时候,记录一下当前的时间戳。然后在下一次过来获取的时候,对比一下当前时间戳和上次记录的时间戳,如果发现当前时间戳小于上次记录的时间戳,所以出现了时钟回拨现象,对外抛出异常,本次 ID 获取失败。


理论上当前时间戳会很快的追赶上上次记录的时间戳。


但是,你可能也注意到了,“对外抛出异常,本次 ID 获取失败”,意味着这段时间内你的服务对外是不可使用的。


比如,你的订单号中的某个部分是由这个 ID 组成的,此时由于 ID 生成不了,你的订单号就生成不了,从而导致下单失败。


再比如,在 Seata 里面,如果是使用数据库作为 TC 集群的存储工具,那么这段时间内该 TC 就是处于不可用状态。


你可以简单的理解为:基础组件的错误导致服务不可用。


再看代码


基于前面说的问题,Seata 才提出了“改良版雪花算法”。



seata.io/zh-cn/blog/…




在介绍改良版之前,我们先把 Seata 的源码拉下来,瞅一眼。


在源码中,有一个叫做 IdWorker 的类:



io.seata.common.util.IdWorker



我带你看一下它的提交记录:



2020 年 5 月 4 日第一次提交,从提交时的信息可以看出来,这是把分布式 ID 的生成策略修改为 snowflake,即雪花算法。


同时我们也能在代码中找到前面提到的“对外抛出异常,本次 ID 获取失败”相关代码,即 nextId 方法,它的比较方式就是用当前时间戳和上次获取到的时间戳做对比:



io.seata.common.util.IdWorker#nextId




这个类的最后一次提交是 2020 年 12 月 15 日:



这一次提交对于 IdWorker 这个类进行了大刀阔斧的改进,可以看到变化的部分非常的多:



我们重点关注刚刚提到的 nextId 方法:



整个方法从代码行数上来看都可以直观的看到变化,至少没有看到抛出异常了。


这段代码到底是怎么起作用的呢?


首先,我们得理解 Seata 的改良思路,搞明白思路了,再说代码就好理解一点。


在前面提到的文章中 Seata 也说明了它的核心思路,我带着你一起过一下:



原版的雪花算法 64 位 ID 是分配这样的:



可以看到,时间戳是在最前面的,因为雪花算法利用了时间的单调递增的特性。


所以,如果前面的时间戳一旦出现“回退”的情况,即打破了“时间的单调递增”这个前提条件,也就打破了它的底层设计。


它能怎么办?


它只能给你抛出异常,开始摆烂了。


然后我主要给你解释一下里面的节点 ID 这个玩意。


节点 ID 可以理解为分布式应用中的一个服务,一个服务的节点 ID 是固定的。


可以看到节点 ID 长度为二进制的 10 位,也就是说最多可以服务于 1024 台机器,所以你看 Seata 最开始提交的版本里面,有一个在 1024 里面随机的动作。


因为算法规定了,节点 ID 最多就是 2 的 10 次方,所以这里的 1024 这个值就是这样来的:



包括后面有大佬觉得用这个随机算法一点都不优雅,就把这部分改成了基于 IP 去获取:



看起来有点复杂,但是我们仔细去分析最后一行:



return ((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF);



变量 & 0B11 运算之后的最大值就是 0B11 即 3。


Byte.SIZE = 8。


所以,3 << 8,对应二进制 1100000000,对应十进制 768。


变量 & 0xFF 运算之后的最大值就是 0xFF 即 255。


768+255=1023,取值范围都还是在 [0,1023] 之间。


然后你再看现在最新的算法里面,官方的老哥们认为获取 IP 的方式不够好:



所以又把这个地方从使用 IP 地址改成了获取 Mac 地址。



最后一行是这样的:



return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);



还是我刚刚说的 0B11 << 8 和 0xFF。


那么理论上的最大值就是 768 | 255 ,算出来还是 1023。


所以不管你怎么玩出花儿来,这个地方搞出来的数的取值范围就只能是 [0,1023] 之间。


别问,问就是规定里面说了,算法里面只给节点 ID 留了 10 位长度。


最后,就是这个 12 位长度的序列号了:



这个玩意没啥说的,就是一个单纯的、递增的序列号而已。


既然 Seata 号称是改良版,那么具体体现在什么地方呢?


简单到你无法想象:



是的,仅仅是把时间戳和节点 ID 换个位置就搞定了。


然后每个节点的时间戳是在 IdWorker 初始化的时候就设置完成了,具体体现到代码上是这样的:



io.seata.common.util.IdWorker#initTimestampAndSequence




主要看第一行:



long timestamp = getNewestTimestamp();



可以看到在 getNewestTimestamp 方法里面获取了一次当前时间,然后减去了一个 twepoch 变量。


twepoch 是什么玩意?



是 2020-05-03 的时间戳。


至于为什么是这个时间,我想作者应该是在 2020 年 5 月 3 日写下的关于 IdWorker 的第一行代码,所以这个日期是 IdWorker 的生日。


作者原本完全可以按照一般程序员的习惯,写 2020 年 1 月 1 日的,但是说真的,这个日期到底是 2020-01-01 还是 2020-05-03 对于框架来说完全不重要,所以还不如给它赋予一个特殊的日期。


他真的,我哭死...


那么为什么要用当前时间戳减去 twepoch 时间戳呢?


你想,如果仅仅用 41 位来表示时间戳,那么时间戳的最大值就是 2 的 41 次方,转化为十进制是这么多 ms:



然后再转化为时间:



也就是说,在雪花算法里面,41 位时间戳最大可以表示的时间是 2039-09-07 23:47:35。


算起来也没几年了。


但是,当我们减去 2020-05-03 的时间戳之后,计算的起点不一样了,这一下,咔咔的,就能多用好多年。


twepoch 就是这么个用途。


然后,我们回到这一行代码:



前一行,我们把 41 位的时间戳算好了,按照 Seata 的设计,时间戳之后就是 12 位的序列号了呀:



所以这里就是把时间戳左移 12 位,好把序列号的位置给腾出来。


最后,算出来的值,就是当前这个节点的初始值,即 timestampAndSequence。


所以,你看这个 AtomicLong 类型的变量的名字取的,叫做 timestampAndSequence。


timestamp 和 Sequence,一个字段代表了两个含义,多贴切。


Long 类型转化为二进制一共 64 位,前 11 位不使用,中间的 41 位代表时间戳,最后的 12 位代表序列号,一个字段,两个含义。


程序里面使用的时候也是在一起使用,用 Long 来存储,在内存里面也是放在一块的:



优雅,实在优雅。


上一次看到这么优雅的代码,还是线程池里面的 ctl 变量:



现在 timestampWithSequence 已经就位了,那么获取下一 ID 的时候是怎么搞的呢?


看一下 nextId 方法:




io.seata.common.util.IdWorker#nextId





标号为 ① 的地方是基于 timestampWithSequence 进行递增,即 +1 操作。


标号为 ② 的地方是截取低 53 位,也就是 41 位的时间戳和 12 位的序列号。


标号为 ③ 的地方就是把高 11 位替换为前面说过的值在 [0,1023] 之间的 workerId。


好,现在你再仔细的想想,在前面描述的获取 ID 的过程中,是不是只有在初始化的时候获取过一次系统时间,之后和它就再也没有关系了?


所以,Seata 的分布式 ID 生成器,不再依赖于时间。


然后,你再想想另外一个问题:


由于序列号只有 12 位,它的取值范围就是 [0,4095]。


如果我们序列号就是生成到了 4096 导致溢出了,怎么办呢?


很简单,序列号重新归 0,溢出的这一位加到时间戳上,让时间戳 +1。


那你再进一步想想,如果让时间戳 +1 了,那么岂不是会导致一种“超前消费”的情况出现,导致时间戳和系统时间不一致了?


朋友,慌啥啊,不一致就不一致呗,反正我们现在也不依赖于系统时间了。


然后,你想想,如果出现“超前消费”,意味着什么?


意味着在当前这个毫秒下,4096 个序列号不够用了。


4096/ms,约 400w/s。


你啥场景啊,怎么牛偪?


(哦,原来是面试场景啊,那懂了~)


另外,官网还抛出了另外一个问题:这样持续不断的"超前消费"会不会使得生成器内的时间戳大大超前于系统的时间戳,从而在重启时造成 ID 重复?


你想想,理论上确实是有可能的。假设我时间戳都“超前消费”到一个月以后了。


那么在这期间,你服务发生重启时我会重新获取一次系统时间戳,导致出现“时间回溯”的情况。


理论上确实有可能。


但是实际上...


看看官方的回复:



别问,问就是不可能,就算出现了,最先崩的也不是我这个地方。


好,到这里,我终于算是铺垫完成了,前面的东西就算从你脑中穿脑而过了,你啥都记不住的话,你就抓住这个图,就完事了:



现在,你再仔细的看这个图,我问你一个问题:



改良版的算法是单调递增的吗?



在单节点里面,它肯定是单调递增的,但是如果是多个节点呢?


在多个节点的情况下,单独看某个节点的 ID 是单调递增的,但是多个节点下并不是全局单调递增。


因为节点 ID 在时间戳之前,所以节点 ID 大的,生成的 ID 一定大于节点 ID 小的,不管时间上谁先谁后。


这一点我们也可以通过代码验证一下,代码的意思是三个节点,每个节点各自生成 5 个 ID:



从输出来看,一眼望去,生成的 ID 似乎是乱序的,至少在全局的角度下,肯定不是单调递增的:


但是我们把输出按照节点 ID 进行排序,就变成了这样,单节点内严格按照单调递增,没毛病:



而在原版的雪花算法中,时间戳在高位,并且始终以系统时钟为准,每次生成的时候都会严格和系统时间进行对比,确保没有发生时间回溯,这样可以保证早生成的 ID 一定小于晚生成的 ID ,只有当 2 个节点恰好在同一时间戳生成 ID 时,2 个 ID 的大小才由节点 ID 决定。


这样看来,Seata 的改进算法是不是错的?


好,我再说一次,前面的所有的内容都是铺垫,就是为了引出这个问题,现在问题抛出来了,你得读懂并理解这个问题,然后再继续往下看。



分析一波


分析之前,先抛出官方的回答:



我先来一个八股文热身:请问为什么不建议使用 UUID 作为数据库的主键 ID ?


就是为了避免触发 MySQL 的页分裂从而影响服务性能嘛。


比如当前主键索引的情况是这样的:



如果来了一个 433,那么直接追加在当前最后一个记录 432 之后即可。



但是如果我们要插入一个 20 怎么办呢?


那么数据页 10 里面已经放满了数据,所以会触发页分裂,变成这样:



进而导致上层数据页的分裂,最终变成这样的一个东西:



上面的我们可以看出页分裂伴随着数据移动,所以我们应该尽量避免。


理想的情况下,应该是把一页数据塞满之后,再新建另外一个数据页,这样 B+ tree 的最底层的双向链表永远是尾部增长,不会出现上面画图的那种情况:在中间的某个节点发生分裂。


那么 Seata 的改良版的雪花算法在不具备“全局的单调递增性”的情况下,是怎么达到减少数据库的页分裂的目的的呢?


我们还是假设有三个节点,我用 A,B,C 代替,在数值上 A < B < C,采用的是改良版的雪花算法,在初始化的情况下是这样的。



假设此时,A 节点申请了一个流水号,那么基于前面的分析,它一定是排在 A-seq1 之后,B-seq1 之前的。


但是这个时候数据页里面的数据满了,怎么办?


分裂呗:



又来了 A-seq3 怎么办?


问题不大,还放的下:



好,这个时候数据页 7 满了,但是又来了 A-seq4,怎么办?


只有继续分裂了:



看到这个分裂的时候,你有没有嗦出一丝味道,是不是有点意思了?


因为在节点 A 上生成的任何 ID 都一定小于在节点 B 上生成的任何 ID,节点 B 和节点 C 同理。


在这个范围内,所有的 ID 都是单调递增的:



而这样的范围最多有多少个?


是不是有多少个节点,就有多少个?


那么最多有多少个节点?



2 的 10 次方,1024 个节点。


所以官方的文章中有这样的一句话:



新版算法从全局角度来看,ID 是无序的,但对于每一个 workerId,它生成的 ID 都是严格单调递增的,又因为 workerId 是有限的,所以最多可划分出 1024 个子序列,每个子序列都是单调递增的。



经过前面的分析,每个子序列总是单调递增的,所以每个子序列在有限次的分裂之后,最终都会达到稳态。


或者用一个数学上的说法:该算法是收敛的。


再或者,我给你画个图:



我画的时候尽力了,至于你看懂看不懂的,就看天意了。


如果看不懂的话,自信一点,不要怀疑自己,就是我画的不好。大胆的说出来:什么玩意?这画的都是些啥,看求不懂。呸,垃圾作者。



页分裂


前面写的所有内容,你都能在官网上我前面提到的两个文章中找到对应的部分。


但是关于页分裂部分,官方并没有进行详细说明。本来也是这样的,人家只是给你说自己的算法,没有必要延伸的太远。


既然都说到页分裂了,那我来补充一个我在学习的时候看到的一个有意思的地方。


也就是这个链接,这一节的内容就是来源于这个链接中:



mysql.taobao.org/monthly/202…



还是先搞个图:



问,在上面的这个 B+ tree 中,如果我要插入 9,应该怎么办?


因为数据页中已经没有位置了,所以肯定要触发页分裂。


会变成这样:



这种页分裂方式叫做插入点(insert point)分裂。


其实在 InnoDB 中最常用的是另外一种分裂方式,中间点(mid point)分裂。


如果采用中间点(mid point)分裂,上面的图就会变成这样:



即把将原数据页面中的 50% 数据移动到新页面,这种才是普遍的分裂方法。


这种分裂方法使两个数据页的空闲率都是 50%,如果之后的数据在这两个数据页上的插入是随机的话,那么就可以很好地利用空闲空间。


但是,如果后续数据插入不是随机,而是递增的呢?


比如我插入 10 和 11。


插入 10 之后是这样的:



插入 11 的时候又要分页了,采用中间点(mid point)分裂就变成了这样:



你看,如果是在递增的情况下,采用中间点(mid point)分裂,数据页 8 和 20 的空间利用率只有 50%。


因为数据的填充和分裂的永远是右侧页面,左侧页面的利用率只有 50%。


所以,插入点(insert point)分裂是为了优化中间点(mid point)分裂的问题而产生的。


InnoDB 在每个数据页上专门有一个叫做 PAGE_LAST_INSERT 的字段,记录了上次插入位置,用来判断当前插入是是否是递增或者是递减的。


如果是递减的,数据页则会向左分裂,然后移动上一页的部分数据过去。


如果判定为递增插入,就在当前点进行插入点分裂。


比如还是这个图:



上次插入的是记录 8,本次插入 9,判断为递增插入,所以采用插入点分裂,所以才有了上面这个图片。


好,那么问题就来了,请听题:


假设出现了这种情况,阁下又该如何应对?



在上面这个图的情况下,我要插入 10 和 9:


当插入 10 的时候,按 InnoDB 遍历 B+ tree 的方法会定位到记录 8,此时这个页面的 PAGE_LAST_INSERT 还是 8。所以会被判断为递增插入,在插入点分裂:



同理插入 9 也是这样的:



最终导致 9、10、11 都独自占据一个 page,空间利用率极低。


问题的根本原因在于每次都定位到记录 8(end of page),并且都判定为递增模式。


哦豁,你说这怎么办?


答案就藏在这一节开始的时候我提到的链接中:



前面我画的所有的图都是在没有并发的情况下展开的。


但是在这个部分里面,牵扯到了更为复杂的并发操作,同时也侧面解释了为什么 InnoDB 在同一时刻只能有有一个结构调整(SMO)进行。


这里面学问就大了去了,有兴趣的可以去了解一下 InnoDB 在 B+ tree 并发控制上的限制,然后再看看 Polar index 的破局之道。


反正我是学不动了。


哦,对了。前面说了这么多,还只是聊了页分裂的情况。


有分裂,就肯定有合并。


那么什么时候会触发页合并呢?


页合并会对我们前面探讨的 Seata 的改良版雪花算法带来什么影响呢?


别问了,别问了,学不动了,学不动了。



自己看一下吧:



最后,如果本文对你有一点点帮助的话,点个免费的赞,求个关注,不过分吧?



作者:why技术
来源:juejin.cn/post/7264387737276203065
收起阅读 »

web端屏幕截屏,生成自定义海报!

web
在一些社群网站,经常会碰到问题、活动、商品的信息分享,这种分享通常是以海报的形式发送给好友或保存到本地。在这种场景下,海报肯定是动态变化的,所以我们要动态的渲染内容并生成图片,海报其实就是图片。 官网:html2canvas 海报示例: 介绍 了解 htm...
继续阅读 »

在一些社群网站,经常会碰到问题、活动、商品的信息分享,这种分享通常是以海报的形式发送给好友或保存到本地。在这种场景下,海报肯定是动态变化的,所以我们要动态的渲染内容并生成图片,海报其实就是图片。

官网:html2canvas

海报示例:

在这里插入图片描述




介绍


了解 html2canvas,它是如何工作的以及它的一些局限性。

在你开始使用这个脚本以前,这里有些帮助你更好的了解脚本的好处及其的一些局限性。


关于


html2canvas 是一个 HTML 渲染器。该脚本允许你直接在用户浏览器截取页面或部分网页的“屏幕截屏”,屏幕截图是基于 DOM,因此生成的图片并不一定 100% 一致,因为它没有制作实际的屏幕截图,而是根据页面上可用的信息构建屏幕截图。


它是如何工作的


该脚本通过读取 DOM 以及应用于元素的不同样式,将当前页面呈现为 canvas 图像。

它不需要来自服务器的任何渲染,因为整个图像是在客户端上创建的。但是,由于它太依赖于浏览器,因此该库不适合在 nodejs 中使用。它也不会神奇地规避任何浏览器内容策略限制,因此呈现跨域内容将需要代理来将内容提供给相同的源。




开始


准备工作


安装依赖


npm install html2canvas

在需要的页面引入依赖


import html2canvas from 'html2canvas'

然后就可以使用html2canvas相关API了。


定义海报结构


在使用之前我们要先定义好页面,我们先在页面上写好海报的html


class="html2canvas">
<view class="poster_title">
海报标题
view>

<view class="img_box">
<img class="img_case" src="http://image.gwmph.com/weican/2024/02/27/695aa1d4c2394be48925a6858dd68e9d.jpg" alt="" />
view>

<view class="poster_title" @click="getPoster()">
确定分享
view>



	.html2canvas{
padding: 20rpx;
.poster_title{
text-align: center;
}
.img_box{
display: flex;
justify-content: space-around;
margin: 10rpx 0;
.img_case{
width: 300rpx;
height: 300rpx;
}
}
}

image.png


script部分


在这里我们要区分两种script类型,一种正常的,一种是renderjs

在一个页面中script可以有多个,它也可以写在任意位置,如果我们做正常的逻辑操作,可以在普通的script中编码;如果我们要对页面进行交互,请使用renderjs



renderjs是一个运行在视图层的js。它比WXS更加强大。它只支持app-vueweb
renderjs的主要作用有2个:



  1. 大幅降低逻辑层和视图层的通讯损耗,提供高性能视图交互能力

  2. 在视图层操作dom,运行 for web 的 js库





点击确定分享,我们则会调用getPoter来生成图片,canvas.toDataURL生成的图片为base64格式,下面是生成后的内容:

在这里插入图片描述


然后我们通过a标签图片进行下载,下面是生成海报并下载的完整逻辑。




下面就是下载下来的图片

在这里插入图片描述

在这里插入图片描述




注意事项


1、多个script




<script module="html2canvas" lang="renderjs">
import html2canvas from 'html2canvas';
export default {
methods: {
}
}
script>


uniapp中,我们如果想要提供逻辑层和视图层的通讯效率,可能会使用renderjs,你可能会在页面中看到多个script,这是正常的,我们可能会将生成海报的功能封装成组件,通过组件传参的方式在多个页面复用,这种结构页面就可能有两个script,一个是正常的vuescrpit,用于处理正常逻辑以及接收传参和事件等,一个是用于视图层通讯的renderjs


2、html2canvas不要用image标签


我们在生成图片的时候,可能会调整清晰度和分辨率,让画面更高清,html2canvas应该使用img标签,而不是image标签,image标签不会对html2canvasscaledpi生效。


3、html2canvas对于现在的css高级属性的支持


html2canvas可能不会支持css高级属性,例如:

● background-blend-mode

● border-image

● box-decoration-break

● box-shadow

● filter

● font-variant-ligatures

● mix-blend-mode

● object-fit

● repeating-linear-gradient()

● writing-mode

● zoom

● ......

对于渐变文字裁切之类的高阶属性可能不支持,如果海报生成的时候没有生效,那就是不支持,需要思考替代方案。




最后


1、html2canvas是基于html的渲染器,只要定义好海报结构即可生成,可以看成html2canvas就是将页面结构转换成图片。

2、不要使用image标签,应该使用img标签。

3、不支持部分css高阶属性。




作者:DCodes
来源:juejin.cn/post/7340208335982903322
收起阅读 »