⚔️不让我在控制台上调试,哼,休想🛠️
在 JavaScript 中,使用 debugger
关键字可以在代码执行到该位置时触发断点调试。这可以帮助开发人员进行代码调试和排错。然而,有些网站开发者可能会故意使用 debugger
关键字来阻止调试,从而增加代码的安全性。但仍然有一些方法可以绕过这种防护措施,进行调试和排错。
禁用浏览器debugger
因为 debugger 其实就是对应的一个断点,它相当于用代码显示地声明了一个断点,要解除它,我们只需要禁用这个断点就好了。
禁用全局断点
全局禁用开关位于 Sources
面板的右上角,如下图所示:
点击它,该按钮会被激活,变成蓝色。
这个时候我们再重新点击一下 Resume script execution(恢复脚本执行)按钮,跳过当前断点,页面就不会再进入到无限 debugger 的状态了。
请注意
,禁用所有断点可能会导致你错过一些潜在的问题或错误
,因为代码将会连续执行而不会在可能的问题点停止。因此,在禁用所有断点之前,请确保你已经理解了代码的行为,并且明白在出现问题时该如何调试。
禁用局部断点
尝试使用另一种方法来跳过这个无限 debugger。在 debugger 语句所在的行的行号上单击鼠标右键,此时会出现一个快捷菜单,操作下图所示:
添加条件断点
在JS代码 debugger 行数位置的最左侧点击右键,添加条件断点(满足条件才会进入断点),将条件设置为false,就是条件永远不成立,永远不会断下来。
添加条件断点还可以监视获取一些变量信息,还是挺好用的。
如果是简单的debugger断点,直接用上边的方式就可以,如果是通过定时器触发的debugger断点,就需要进行Hook处理了。
以上的方案执行完毕之后有时候会跳转空页面,这时候只需要在空页面上打开原先地址即可。
先打开控制台
有时候我们一打开网页,就直接进入空页面,控制台上的js和html文件也随之为空,这时候需要在空白页面,或者F12等键无法打开控制台等,这种可以先打开控制台,然后再在空白页面上打开网站即可。
可以在这个网站上试一下。
替换文件
直接使用浏览器开发者工具替换修改js(Sources面板 --> Overrides),或者通过FD工具替换。
这种方式的核心思路,是替换 JS 文件中的 debugger 关键字,并保存为本地文件,在请求返回的时候、通过正则匹配等方式、拦截并替换返回的 JS 代码,以达到绕过 debugger 的目的。也可以直接删掉相关的debugger代码。
具体实现可参考:2024最新版JavaScript逆向爬虫教程-------基础篇之无限debugger的原理与绕过
快捷方案-使用油猴等插件
使用这种方法,就不需要再打 script
断点。直接安装插件即可。
参考文献
2024最新版JavaScript逆向爬虫教程-------基础篇之无限debugger的原理与绕过
来源:juejin.cn/post/7369505226921738278
🔏别想调试我的前端页面代码🔒
这里我们不介绍禁止右键菜单, 禁止F12快捷键
和代码混淆
方案。
无限debugger
- 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行
- 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的.
基础方案
(() => {
function ban() {
setInterval(() => { debugger; }, 50);
}
try {
ban();
} catch (err) { }
})();
- 将
setInterval
中的代码写在一行,可以禁止用户断点,即使添加logpoint
为false
也无用 - 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的
浏览器宽高
根据浏览器宽高、与打开F12后的宽高进行比对,有差值,说明打开了调试,则替换html内容;
- 通过检测窗口的外部高度和宽度与内部高度和宽度的差值,如果差值大于 200,就将页面内容设置为 "检测到非法调试"。
- 通过使用间隔为 50 毫秒的定时器,在每次间隔内执行一个函数,该函数通过创建一个包含
debugger
语句的函数,并立即调用该函数的方式来试图阻止调试器的正常使用。
(() => {
function block() {
if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
document.body.innerHTML = "检测到非法调试";
}
setInterval(() => {
(function () {
return false;
}
['constructor']('debugger')
['call']());
}, 50);
}
try {
block();
} catch (err) { }
})();
关闭断点,调整空页面
在不打开发者工具的情况下,debugger是不会执行将页面卡住,而恰恰是利用debugger的这一点,如果你打开开发者工具一定会被debugger卡住,那么上下文时间间隔就会增加,在对时间间隔进行判断,就能巧妙的知道绝对开了开发者工具,随后直接跳转到空白页,一气呵成。(文心一言采用方案)
setInterval(function () {
var startTime = performance.now();
// 设置断点
debugger;
var endTime = performance.now();
// 设置一个阈值,例如100毫秒
if (endTime - startTime > 100) {
window.location.href = 'about:blank';
}
}, 100);
第三方插件
disable-devtool
disable-devtool
可以禁用所有一切可以进入开发者工具的方法,防止通过开发者工具进行的代码搬运。
该库有以下特性:
- 支持可配置是否禁用右键菜单
- 禁用 f12 和 ctrl+shift+i 等快捷键
- 支持识别从浏览器菜单栏打开开发者工具并关闭当前页面
- 开发者可以绕过禁用 (url参数使用tk配合md5加密)
- 多种监测模式,支持几乎所有浏览器(IE,360,qq浏览器,FireFox,Chrome,Edge...)
- 高度可配置、使用极简、体积小巧
- 支持npm引用和script标签引用(属性配置)
- 识别真移动端与浏览器开发者工具设置插件伪造的移动端,为移动端节省性能
- 支持识别开发者工具关闭事件
- 支持可配置是否禁用选择、复制、剪切、粘贴功能
- 支持识别 eruda 和 vconsole 调试工具
- 支持挂起和恢复探测器工作
- 支持配置ignore属性,用以自定义控制是否启用探测器
- 支持配置iframe中所有父页面的开发者工具禁用
🦂使用🦂
<script disable-devtool-auto src='https://cdn.jsdelivr.net/npm/disable-devtool'>script>
更多使用方法参见官网:disable-devtool
disable-devtool
console-ban
禁止 F12 / 审查开启控制台,保护站点资源、减少爬虫和攻击的轻量方案,支持重定向、重写、自定义多种策略。
使用
<head>
<script src="https://cdn.jsdelivr.net/npm/console-ban@5.0.0/dist/console-ban.min.js">script>
<script>
// default options
ConsoleBan.init()
// custom options
ConsoleBan.init({
redirect: '/404'
})
script>
head>
在项目中使用:
yarn add console-ban
import { init } from 'console-ban'
init(options)
重定向
ConsoleBan.init({
// 重定向至 /404 相对地址
redirect: '/404',
// 重定向至绝对地址
redirect: 'http://domain.com/path'
})
使用重定向策略可以将用户指引到友好的相关信息地址(如网站介绍),亦或是纯静态 404 页面,高防的边缘计算或验证码等页面。
注:若重定向后的地址可以通过 SPA 路由切换或 pjax 局部加载技术等进行非真正意义上的页面切换,则切换后的控制台监测将不会再次生效,对于 SPA 你可以在路由卫士处重新注册本实例,其他情况请引导至真正的其他页面。
重写
var div = document.createElement('div')
div.innerHTML = '不要偷看啦~'
ConsoleBan.init({
// 重写 body 为字符串
write: ' 不要偷看啦~
',
// 可传入节点对象
write: div
})
重写策略可以完全阻断对网站内容的审查,但较不友好,不推荐使用。
回调函数
ConsoleBan.init({
callback: () => {
// ...
}
})
回调函数支持自定义打开控制台后的策略。
参数
name | required | type | default | description |
---|---|---|---|---|
clear | no | boolean | true | 禁用 console.clear 函数 |
debug | no | boolean | true | 是否开启定时 debugger 反爬虫审查 |
debugTime | no | number | 3000 | 定时 debugger 时间间隔(毫秒) |
redirect | no | string | - | 开启控制台后重定向地址 |
write | no | string 或Element | - | 开启控制台后重写 document.body 内容,支持传入节点或字符串 |
callback | no | Function | - | 开启控制台后的回调函数 |
bfcache | no | boolean | true | 禁用 bfcache 功能 |
注:redirect
、write
、callback
三种策略只能取其一,优先使用回调函数。
参考文章
结语
需要注意的是,这些技术可以增加攻击者分析和调试代码的难度,但无法完全阻止恶意调试。因此,对于一些敏感信息或关键逻辑,最好的方式是在后端进行处理,而不是完全依赖前端来保护。
下篇文章主要介绍如何破解这些禁止调试的方法。
来源:juejin.cn/post/7368313344712179739
DeepSeek引发行业变局,2025 IT人该如何破局抓住机遇
一. 🎯 变局中抓住核心
这个春节被DeepSeek消息狂轰滥炸,很多做IT朋友已经敏锐的意识到 一场变局已经酝酿,整个IT行业都将迎来洗牌重塑。 中小IT企业、个人创业者、普通人该如何面对这场变局,如何不被市场淘汰,如何抓住机遇?
先说结论
2025年,谁能将
🔥技术热点 转换成 🚀业务引擎
谁就能在这场变局中抢得先机
2025年,选择躺平视而不见,以后的路将越来越窄
二. 🧐 AI巨头垄断,小公司别硬刚
头部AI/大模型厂商 (OpenAI、DeepSeek、字节、阿里、百度…)
通过大模型底座控制生态入口
中小IT公司沦为“AI插件开发者”
⬇️
说直白点就是别学大厂烧钱训练大模型
“不要用你搬砖攒下的血汗钱挑战巨头们躺赚的钱袋子”
合理的生存之计是:
- 直接调用低成本接入大厂的大模型能力
- 通过云服务+开源模型聚焦1-2个细分垂直赛道开发领域专属大模型应用
当然你也可以不信邪
学习DeepSeek不走寻常路
十年量化无人问,一朝DS天下知
闷声鼓捣一个大的
三. 🖊️ 产品思维要转变
对于产品现在客户要的不是功能,是智商
产品的设计思路一定是
从功能导向 ➡️ 智能导向
堆功能堆指标是底限,堆智能才是上限
无论是硬件还是软件公司,殊途同归
卖硬件 ➡️ 卖智能,卖软件 ➡️ 卖智能
四. 🔧 定制化服务市场潜力大
虽然AI巨头都推出了N个
行业标准化AI解决方案
以近乎成本价抢占市场
但是,中国客户还是喜欢”定制化“
有数据统计,60%以上的行业需求无法被标准化方案满足
- 中小IT公司:
- 大厂不愿做,我做 📣
- 大厂不屑做,我做 📣
- 大厂不会做,我做 📣
比如,
现在做企业AI应用开发
需要触碰企业长年积累的数据
客户有很强意识👉🏻这是核心资产
所以开发时,就要求定制化+本地化
- 只有定制化,才能构建数据护城河
- 只有定制化,客户对数据隐私才放心
...
也许这不是真理,但却是刚需
总之,客户定制化理由千千万万
这就是IT人的机会
五. 💰 在你懂而别人不懂的领域赚钱
小公司
- 聚焦“AI+垂直场景”做深行业Know-How
- 避免与通用大模型正面竞争
中等公司
- 构建“私有化模型+数据闭环”
- 在特定领域建立技术壁垒
六. 💯 存量市场以稳为主,增量市场探索可能
存量业务
- 用AI改造现有产品和客户场景
- 对于已经稳定的客户和产品应当积极引入 AI 技术进行升级改造
增量市场
- 探索AI原生需求
- 要善于挖掘客户对AI的新需求并及时满足,抢占市场先机
此过程中,有两点需要注意
- 敏捷性 > 规模
- 快速试错、小步快跑的模式比巨额投入更重要
- 场景落地 > 技术炫技
- 能解决具体业务痛点的“60分AI方案”比追求“99分技术指标”更易存活
七. 💥 纯技术团队将面临淘汰
开发团队
- 必须重构开发流程
- 建立“AI+人工”混合开发模式
- 开发流程需和AI工具链深度集成
- 开发不要过重,采用轻量化技术路线
部署和运维团队
- 同样建立“AI+人工”混合运维模式
- 智能运维手段(故障预测、根因分析)将成标配
- 内部要刻意培养AI-Aware工程师
未来技术人员的筛选条件可能不再是年龄、学历、工作经验而是你有没有 AI Awareness
八. 📝 总结
在这场变局中能活好的普通IT公司,AI创业者
不一定是技术最强的
而是最会借力AI
用行业经验+客户积累+AI工具
做巨头看不上的 “小而美”生意 🤩
来源:juejin.cn/post/7468203211725783094
表妹问:前端好玩吗?我说好玩,但表妹接下来的回复看哭了我
表妹问:前端好玩吗?我说好玩,但表妹接下来的回复看哭了我。
是的,回复如下:
这红海血途上,新兵举着 "大前端" 旌旗冲锋,老兵拖着node_modules
残躯撤退。资本织机永不停歇,框架版本更迭如暴君换季,留下满地deprecated
警告如秋后落叶。
其一、夹缝中的苦力
世人都道前端易,不过调接口、改颜色,仿佛稚童搭积木。却不知那屏幕上寸寸像素之间,皆是血泪。产品拍案,需求朝夕三变,昨日之红蓝按钮,今晨便成黑白圆角。UI稿纸翻飞如雪,设计师手持“用户体验”四字大旗,将五更赶工的代码尽数碾碎。后端端坐高台,接口文档空悬如镜花水月,待到交付时辰,方抛来残缺数据。此时节,前端便成了那补天的女娲,于混沌中捏造虚拟对象,用JSON.parse('{"data": undefined}')
这等荒诞戏法,将虚无粉饰成真实。
看这段代码何等悲凉:
// 后端曰:此接口返data字段,必不为空
fetch('api/data').then(res => {
const { data } = res;
render(data[0].children[3].value || '默认值'); // 层层掘墓,方见白骨
});
此乃前端日常——在数据废墟里刨食,用||
与?.
铸成铁锹,掘出三分体面。
其二、技术的枷锁
JavaScript本是脚本小儿,如今却要扛鼎江山。君不见React、Vue、Angular三座大山压顶,每年必有新神像立起。昨日方学得Redux真经,今朝GraphQL又成显学。更有Electron、ReactNative、Flutter诸般法器,教人左手写桌面应用,右手调移动端手势。所谓“大前端”,实乃资本画饼之术,人前跨端写,人后页面仔——既要马儿跑,又言食草易。
且看这跨平台代码何等荒诞:
// 一套代码统治三界(iOS/Android/Web)
<View>
{Platform.OS === 'web' ?
<div onClick={handleWebClick} /> :
<TouchableOpacity onPress={handleNativePress} />
}
</View>
此类缝合怪代码,恰似给长衫打补丁,既失体统,又损性能。待到内存泄漏、渲染卡顿时,众人皆指前端曰:"此子学艺不精!"
何人怜悯前端 node18 react19 逐人老,后端写着 java8 看着 java22 笑。
其三、尊严的消亡
领导提拔,必先问尔可懂SpringBoot、MySQL分库分表?纵使前端用WebGL绘出三维宇宙,用WebAssembly重写操作系统,在会议室里仍是“做界面的”。工资单上数字最是直白——同司后端新人起薪一万五,前端老将苦熬三年方摸得此数。更可笑者,产品经理醉酒时吐真言:"你们不就是改改CSS么?"
再看这可视化代码何等心酸:
// 用Canvas画十万级数据点
ctx.beginPath();
dataPoints.forEach((point, i) => {
if (i % 100 === 0) ctx.stroke(); // 分段渲染防卡死
ctx.lineTo(point.x, point.y);
});
此等精密计算,在他人眼中不过"动画效果",与美工修图无异。待浏览器崩溃,众人皆曰:"定是前端代码劣质!"
技术大会,后端高谈微服务、分布式,高并发,满座掌声如雷,实则系统使用量百十来人也是远矣;前端言及 CSS 栅格、浏览器渲染,众人瞌睡连天。领导抚掌笑曰:“后端者,国之重器;前端者,雕虫小技。” 晋升名单,后端之名列如长蛇,前端者埋没于墙角尘埃。纵使将那界面写出花来,终是 “美工” 二字定终身。
其四、维护者的悲歌
JavaScript本无类型,如野马脱缰。若非经验老道之一,常写出这等代码:
function handleData(data) {
if (data && typeof data === 'object') { // 万能判断
return data.map(item => ({
...item,
newProp: item.id * Math.random() // 魔改数据
}));
}
return []; // 默认返回空阵,埋下百处报错
}
此类代码如瘟疫蔓延,领导却言“这些功能实习生也能写!”,却不顾三月后连作者亦不敢相认,只得下任前端难上加难。
而后端有Type大法,编译检查护体,有Swagger契约,有Docker容器,纵使代码如乱麻,只需扩内存、增实例,便可遮掩性能疮疤。
其五、末路者的自白
诸君且看这招聘启事:"需精通Vue3+TS+Webpack,熟悉React/Node.js,有Electron/小程序经验,掌握Three.js/WebGL者重点考虑。" 薪资却标着"6-8K"。更有机智者发明"全栈"之名,实欲以一人之躯,承三头六臂之劳。
再看这面试题何等荒谬:
// 手写Promise实现A+规范
class MyPromise {
// 三千行后,方知自己仍是蝼蚁
}
此等屠龙之术,入职后唯调API用。恰似逼庖丁解牛,却令其日日杀鸡。
或以使用组件库之经验薪资招之,又以写不好组件库之责裁出。
尾声:铁屋中的叩问
前端者,数字化时代的纺织工也。资本织机日夜轰鸣,框架如梭穿行不息。程序员眼底血丝如网。所谓"全栈工程师",实为包身工雅称;所谓"技术革新",不过剥削新法。
若仍有少年热血未冷,欲投身此业,且听我一言:君有凌云志,何不学Rust/C++,做那操作系统、数据库等真·屠龙技?莫要困在这CSS牢笼中,为圆角像素折腰,为虚无需求焚膏。前端之路,已是红海血途,望后来者三思,三思!
来源:juejin.cn/post/7475351155297402891
Spring 6.0 + Boot 3.0:秒级启动、万级并发的开发新姿势
Spring生态重大升级全景图
一、Spring 6.0核心特性详解
1. Java版本基线升级
- 最低JDK 17:全面拥抱Java模块化特性,优化现代JVM性能
- 虚拟线程(Loom项目):轻量级线程支持高并发场景(需JDK 19+)
// 示例:虚拟线程使用
Thread.ofVirtual().name("my-virtual-thread").start(() -> {
// 业务逻辑
});
- 虚拟线程(Project Loom)
- 应用场景:电商秒杀系统、实时聊天服务等高并发场景
// 传统线程池 vs 虚拟线程
// 旧方案(平台线程)
ExecutorService executor = Executors.newFixedThreadPool(200);
// 新方案(虚拟线程)
ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();
// 处理10000个并发请求
IntStream.range(0, 10000).forEach(i ->
virtualExecutor.submit(() -> {
// 处理订单逻辑
processOrder(i);
})
);
2. HTTP接口声明式客户端
- @HttpExchange注解:类似Feign的声明式REST调用
@HttpExchange(url = "/api/users")
public interface UserClient {
@GetExchange
List<User> listUsers();
}
应用场景:微服务间API调用
@HttpExchange(url = "/products", accept = "application/json")
public interface ProductServiceClient {
@GetExchange("/{id}")
Product getProduct(@PathVariable String id);
@PostExchange
Product createProduct(@RequestBody Product product);
}
// 自动注入使用
@Service
public class OrderService {
@Autowired
private ProductServiceClient productClient;
public void validateProduct(String productId) {
Product product = productClient.getProduct(productId);
// 校验逻辑...
}
}
3. ProblemDetail异常处理
- RFC 7807标准:标准化错误响应格式
{
"type": "https://example.com/errors/insufficient-funds",
"title": "余额不足",
"status": 400,
"detail": "当前账户余额为50元,需支付100元"
}
- 应用场景:统一API错误响应格式
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ProblemDetail handleProductNotFound(ProductNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
problem.setType(URI.create("/errors/product-not-found"));
problem.setTitle("商品不存在");
problem.setDetail("商品ID: " + ex.getProductId());
return problem;
}
}
// 触发异常示例
@GetMapping("/products/{id}")
public Product getProduct(@PathVariable String id) {
return productRepo.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
4. GraalVM原生镜像支持
- AOT编译优化:启动时间缩短至毫秒级,内存占用降低50%+
- 编译命令示例:
native-image -jar myapp.jar
二、Spring Boot 3.0突破性改进
1. 基础架构升级
- Jakarta EE 9+:包名javax→jakarta全量替换
- 自动配置优化:更智能的条件装配策略
- OAuth2授权服务器
应用场景:构建企业级认证中心
- OAuth2授权服务器
# application.yml配置
spring:
security:
oauth2:
authorization-server:
issuer-url: https://auth.yourcompany.com
token:
access-token-time-to-live: 1h
定义权限端点
@Configuration
@EnableWebSecurity
public class AuthServerConfig {
@Bean
public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
}
2. GraalVM原生镜像支持
应用场景:云原生Serverless函数
# 打包命令(需安装GraalVM)
mvn clean package -Pnative
# 运行效果对比
传统JAR启动:启动时间2.3s | 内存占用480MB
原生镜像启动:启动时间0.05s | 内存占用85MB
3. 增强监控(Prometheus集成)
- Micrometer 1.10+:支持OpenTelemetry标准
- 全新/actuator/prometheus端点:原生Prometheus格式指标
- 应用场景:微服务健康监测
// 自定义业务指标
@RestController
public class OrderController {
private final Counter orderCounter = Metrics.counter("orders.total");
@PostMapping("/orders")
public Order createOrder() {
orderCounter.increment();
// 创建订单逻辑...
}
}
# Prometheus监控指标示例
orders_total{application="order-service"} 42
http_server_requests_seconds_count{uri="/orders"} 15
三、升级实施路线图
四、新特性组合实战案例
场景:电商平台升级
// 商品查询服务(组合使用新特性)
@RestController
public class ProductController {
// 声明式调用库存服务
@Autowired
private StockServiceClient stockClient;
// 虚拟线程处理高并发查询
@GetMapping("/products/{id}")
public ProductDetail getProduct(@PathVariable String id) {
return CompletableFuture.supplyAsync(() -> {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
// 并行查询库存
Integer stock = stockClient.getStock(id);
return new ProductDetail(product, stock);
}, Executors.newVirtualThreadPerTaskExecutor()).join();
}
}
四、升级实践建议
- 环境检查:确认JDK版本≥17,IDE支持Jakarta包名
- 渐进式迁移:
- 先升级Spring Boot 3.x → 再启用Spring 6特性
- 使用
spring-boot-properties-migrator
检测配置变更
- 性能测试:对比GraalVM原生镜像与传统JAR包运行指标
通过以上升级方案:
- 使用虚拟线程支撑万级并发查询
- 声明式客户端简化服务间调用
- ProblemDetail统一异常格式
- Prometheus监控接口性能
本次升级标志着Spring生态正式进入云原生时代。重点关注:虚拟线程的资源管理策略、GraalVM的反射配置优化、OAuth2授权服务器的定制扩展等深度实践方向。
来源:juejin.cn/post/7476389305881296934
如何优雅的回复面试官问:“你能接受加班吗?”
面试官问:“你能接受加班吗?”我脑袋嗡的一声,余音绕梁三日不绝于耳。
那一刻,我简直觉得自己像被突然砸中脑袋,脑袋里嗡的一声,余音绕梁三日。作为一个职场小白,这种问题简直颠覆了我对面试的认知。于是,我一时心血来潮,脱口而出一句:“领导抗揍吗?” 结果,大家猜到了,面试是上午结束的,Offer是当天中午凉的。
如何巧妙回答
“我认为加班是工作中不可避免的一部分,尤其是在一些特殊项目或紧急情况下。我非常热爱我的工作,并且对公司的发展充满信心,因此我愿意为了团队的成功付出额外的努力。当然,我也注重工作效率和时间管理,尽量在正常工作时间内完成任务。如果确实需要加班,我也会根据公司合理的安排,积极的响应。”
作为一名资深的面试官,今天面对这个问题,坐下来和大家聊聊应该怎么回答呢?面试官究竟喜欢怎样的回答?让我们深入分析一下。
面试官的心理
在职场中,想要出色地应对面试,需要具备敏锐的观察力和理解力。学会细致入微地观察,善于捕捉每一个细微的线索,这样才能在面试中游刃有余。懂的察言观色,方能尽显英雄本色。
面试官的考量点
- 评估工作稳定性
面试官提出“能否接受加班”的问题,旨在深入了解求职者的职业稳定性和对加班安排的适应性。这一评估有助于预测求职者入职后的表现和长期留任的可能性。工作稳定性是企业考量员工的关键指标之一,通过这一问题,面试官能够洞察求职者的职业发展规划及其对未来工作的期望。
- 筛选合适的候选人
通过询问加班的接受度,面试官筛选出那些愿意为达成工作目标而投入额外时间和精力的候选人。这种筛选方式有助于确保团队的整体运作效率和协作精神。合适的候选人不仅能快速融入团队,还能显著提升工作效率。因此,面试官借此问题寻找最匹配岗位需求的员工。
- 了解求职者的价值观
面试官还利用这个问题来探查求职者的价值观和工作态度,以此判断他们是否与公司的文化和核心价值观相契合。员工的价值观和态度对公司的长远发展起着至关重要的作用。通过这一询问,面试官能够确保求职者的个人目标与公司的发展方向保持一致,从而促进整体的和谐与进步。
考察的问题的意义
要理解问题的本质……为什么面试官会提出这样的问题?难道是因为你的颜值过高,引发了他的嫉妒?
- 工作态度
面试官通过询问加班的接受度,旨在评估求职者是否展现出积极的工作态度和强烈的责任心。在许多行业中,加班已成为常态,面试官借此问题了解求职者是否愿意在工作上投入额外的时间和精力。积极的工作态度和责任心是职场成功的关键因素,通过这一问题,面试官能够初步判断求职者是否适应高强度的工作环境。
- 岗位匹配度
特定岗位因其工作性质可能需要频繁加班。面试官通过提出加班相关的问题,旨在了解求职者是否能适应这类岗位的工作强度。由于不同岗位对工作强度的要求各异,面试官希望通过这一问题确保求职者对即将承担的角色有明确的认识,从而防止入职后出现期望不一致的情况。
- 抗压能力
加班往往伴随压力,面试官通过这一问题考察求职者的抗压能力和情绪管理技巧。抗压能力对于职场成功至关重要,面试官借此了解求职者在高压环境下的表现,以判断其是否符合公司的需求。
- 公司文化
面试官还利用这个问题来评估求职者对公司加班文化的接受程度,以此判断其价值观是否与公司相符。公司文化对员工的工作体验和满意度有着深远的影响,面试官希望通过这一问题确保求职者能够认同并融入公司文化。
回答的艺术
“知己知彼,百战不殆。”在面试中,回答问题的关键在于展现出积极和正向的态度。
- 积极态度
在回答有关加班的问题时,表达你对工作的热爱和对公司的忠诚,强调你愿意为了团队的成功而付出额外的努力。这种积极的态度不仅展示了你的职业素养和对工作的热情,还能显著提升面试官对你的好感。
例如:“我非常热爱我的工作,并且对公司的发展充满信心。我相信为了实现公司的目标和团队的成功,适当的加班是可以接受的。”
- 灵活性和效率
强调你在时间管理和工作效率上的能力,表明你在确保工作质量的同时,会尽可能减少加班的需求。灵活性和效率是职场中极为重要的技能,面试官可以通过这个问题了解你的实际工作表现。
例如:“我在工作中注重效率和时间管理,通常能够在规定的工作时间内完成任务。当然,如果有特殊情况需要加班,我也会全力以赴。”
- 平衡工作与生活
适当地提到你对工作与生活平衡的重视,并希望公司在安排加班时能够充分考虑到员工的个人需求。平衡工作与生活是职场人士普遍关注的问题,面试官通过这个问题可以了解你的个人需求和期望。
例如:“我非常重视工作与生活的平衡,希望在保证工作效率的同时,也能有足够的时间陪伴家人和进行个人活动。如果公司能够合理安排加班时间,我会非常乐意配合。”
- 适度反问
在回答时,可以适当地向面试官询问关于公司加班的具体情况,以便更全面地了解公司的加班文化和预期。这样的反问可以展现你的主动性和对公司的兴趣,有助于获取更多信息,做出更加明智的回答。
例如:“请问公司通常的加班情况是怎样的?是否有相关的加班补偿或调休安排?”
最后
所谓士为知己者死,遇良将则冲锋陷阵,择良人则共谋天下。在职场这场没有硝烟的战争中,我们每个人都是一名战士,寻找着属于自己的知己和良将。当面试官提出挑战性问题时,我们不仅要展示自己的能力和才华,更要表现出对工作的热爱和对公司的忠诚。
面对“你能接受加班吗?”这样的问题,我们应以积极的态度、灵活的思维和对工作与生活平衡的重视来回应。这样的回答不仅能展示我们的职业素养,还能让我们在众多求职者中脱颖而出,赢得面试官的青睐。
正如士为知己者死,我们在职场中也要找到那个能理解我们、支持我们的知己;遇良将则冲锋陷阵,我们要在优秀的领导下发挥自己的潜能,为公司的发展贡献力量;择良人则共谋天下,我们要与志同道合的同事共同努力,实现职业生涯的辉煌。
总之一句话,在面试中展现出积极向上的形象,不仅能为我们的职业生涯加分,更能让我们在职场上找到属于自己的价值和归属感。让我们以这句话为指引,勇敢地迎接职场的挑战,书写属于自己的辉煌篇章。
来源:juejin.cn/post/7457211584709066792
DeepSeek R1 对编程语言未来的预测
欢迎关注我的训练营:云原生AI实战营,一个助力 Go 开发者在 AI 时代建立技术竞争力的实战营。
今天分享下 DeepSeek 对于编程语言未来的预测,这个应该也是很多开发者关注的,因为它可能会影响未来的职业选择。
因我自己最近也在关注这方面,所以今天就问了 DeepSeek。DeepSeek 给出的回答跟我的想法很一致,所以今天把 DeepSeek 的回答分享出来。
问题:Go、Java、Rust、Python 语言未来前景如何
我的问题是Go、Java、Rust、Python 语言未来前景如何。之所以问这 4 门语言,是因为这四门语言在后端开发中最受欢迎,各自在其擅长的领域中发光发热。
目前,我觉得 Go 和 Java 在领域上有一定程度或者很大程度的重合。Rust、Python、(Go、Java)有其擅长的领域,交叉点不多。
我想弄明白的是,每门语言是否会在其领域继续发展状态,或者会有蚕食其他领域的现象。通过这些,也想知道未来编程语言的选型为何。
DeepSeek 思考过程
提示:以下是 DeepSeek 的思考过程
好的,用户问的是Go、Java、Rust、Python这四种语言的未来前景如何。我需要逐一分析每种语言的特点、现状以及发展趋势,同时比较它们的优缺点和应用场景。首先,我应该回忆每种语言的主要用途和社区活跃度,然后看看市场需求和技术趋势。
对于 Go 语言,它由Google开发,以高并发和高效编译著称,适合云原生和微服务。近年来Docker、Kubernetes的流行推动了Go的应用,尤其是在后端和DevOps领域。但Go在泛型和生态系统方面可能不如其他语言,所以需要评估这些因素是否会影响其未来。
Java作为老牌企业级语言,稳定性强,广泛应用于大型系统、Android开发和大数据。虽然有人认为Java在下降,但它的庞大现有代码库和企业依赖意味着它仍会长期存在。不过,Java需要面对Kotlin等语言的竞争,尤其是在Android开发方面,还有云原生趋势下轻量级语言的挑战。
Rust以内存安全和性能为卖点,逐渐在系统编程、嵌入式和高性能计算领域崭露头角。被Linux内核和Windows驱动采用,说明其潜力。但Rust的学习曲线陡峭,可能在普及上需要时间,但在对安全性要求高的领域会持续增长。
Python作为脚本语言之王,在数据科学、AI、Web开发等领域占据主导。随着AI的爆发,Python的需求持续增长,但性能问题可能在需要高性能的场景中成为瓶颈。不过,Python的易用性和丰富的库支持使其在教育和小型项目中保持优势。
接下来要考虑的是行业趋势,比如云计算的持续增长对Go有利,AI发展促进Python,系统级需求推动Rust,而企业维护则维持Java。同时,新语言的出现是否会影响这些老牌语言,例如Rust可能替代部分C/C++的领域,而Go可能继续在云原生领域扩展。
用户可能的深层需求是选择学习方向或技术选型,需要明确各语言的优劣势和适用场景,帮助他们做出决策。因此,回答时要突出各语言的优势领域和未来可能的发展方向,以及潜在的风险点,比如Java可能面临市场份额被蚕食,但不会很快消失。
总结部分需要对比各语言,指出它们在不同领域的地位,并给出建议,比如根据项目需求选择合适语言,而不是盲目跟风。同时,强调多语言掌握的重要性,因为现代开发往往需要多种技术结合。
DeepSeek R1 四种语言前景分析
以下是 DeepSeek R1 关于 Go、Java、Rust、Python 四种语言的前景分析,结合技术趋势、市场需求和生态系统发展分析如下:
1. Go (Golang)
优势领域:
- 云原生与微服务:Go 的并发模型(goroutine)和高效编译特性使其成为 Kubernetes、Docker、Istio 等云原生工具的首选语言。
- 高性能后端:适合 API 网关、分布式系统、实时通信等高并发场景(如 Twitch、Uber 的后端)。
- 开发效率:语法简洁,编译速度快,适合快速迭代的工程团队。
未来前景:
- 持续增长:随着云计算的普及,Go 在云原生领域的地位将进一步巩固。
- 企业采用:越来越多公司(如字节跳动、腾讯)将 Go 作为核心后端语言。
- 挑战:泛型支持的完善程度、生态多样性仍需提升。
推荐方向:云原生开发、基础设施工具链、高性能服务端。
2. Java
优势领域:
- 企业级应用:银行、电信、政府等传统行业的核心系统(如 IBM、Oracle 的解决方案)。
- Android 开发:仍占据主导地位(尽管 Kotlin 在 Android 中逐渐取代)。
- 大数据生态:Hadoop、Spark、Flink 等框架依赖 Java 生态。
未来前景:
- 稳定但缓慢衰退:Java 的存量市场庞大,但新项目可能更倾向于现代语言(如 Kotlin、Go)。
- 云原生转型:Quarkus、Spring Native 等框架帮助 Java 适应云原生环境。
- 挑战:冗长的语法、内存消耗较高,对轻量级场景竞争力不足。
推荐方向:传统企业系统维护、Android 底层开发、大数据工程。
3. Rust
优势领域:
- 系统编程:取代 C/C++ 的潜力(如操作系统、嵌入式、浏览器引擎)。
- 内存安全:所有权模型彻底解决内存泄漏和数据竞争问题,适合对安全性要求极高的场景(如区块链、金融系统)。
- 高性能计算:在游戏引擎、实时渲染等领域逐渐应用(如 Discord 的音频服务)。
未来前景:
- 高速增长:被 Linux 内核、Windows 驱动开发等官方支持,逐步进入主流。
- 生态扩展:WebAssembly、区块链(如 Solana)等领域对 Rust 需求激增。
- 挑战:学习曲线陡峭,中小型项目采用成本较高。
推荐方向:系统级开发、安全敏感型应用、区块链底层。
4. Python
优势领域:
- 数据科学与 AI:TensorFlow、PyTorch、NumPy 等库的绝对统治地位。
- 脚本与自动化:运维、测试、爬虫等场景的首选语言。
- 教育与科研:语法简单,适合快速验证算法和原型开发。
未来前景:
- AI 驱动的霸主:随着生成式 AI(如 ChatGPT)的爆发,Python 需求将持续增长。
- 性能瓶颈:在需要高性能的场景(如高频交易)可能被 Rust/Go 取代。
- 生态护城河:庞大的库和社区资源确保其长期不可替代性。
推荐方向:机器学习工程、数据分析、快速原型开发。
综合对比与趋势总结
维度 | Go | Java | Rust | Python |
---|---|---|---|---|
增长潜力 | 高(云原生) | 低(存量市场) | 极高(系统替代) | 高(AI 驱动) |
学习曲线 | 中等 | 中等偏高 | 高 | 低 |
性能 | 高 | 中等 | 极高 | 低 |
适用领域 | 后端/基础设施 | 企业系统/Android | 系统/安全/区块链 | AI/脚本/科研 |
未来 5 年预测:
- Go 和 Rust 将成为增长最快的语言,分别主导云原生和系统级开发。
- Python 因 AI 热潮保持霸主地位,但需警惕性能相关的替代方案。
- Java 缓慢衰退,但仍长期占据企业市场。
建议:
- 根据职业目标选择:
- 追求高薪和前沿技术:优先 Rust 或 Go。
- 稳重型职业发展:Java 或 Python。
- 多语言组合:例如 Python(AI) + Go(后端) 或 Rust(底层) + Python(脚本)。
个人观点
提示:以下是我个人总结,非 DeepSeek 的输出。
DeepSeek R1 对于四种语言的前景分析、未来 5 年预测及给出的建议,其实跟我的想法是完全一致的。
根据 JetBrains2024 的市场调研,Go 在四种语言中的工资是最高的,背后的原因可能是因为云原生的技术门槛。Go 在云原生领域占据霸主地位,而云原生本身技术门槛要比业务 CURD 高很多,所以薪资自然要比其他语言高。其实,这也说明了,越是门槛高、技术含量高的岗位薪资越有竞争力(这是一句废话)。
Python 依托于其强大的生态及数据处理能力,在 AI 领域成为霸主语言。在 AI 时代,程序员不可避免的要去学习、掌握 Python。
Rust 优点是高性能、高安全,但缺点是学习门槛高、开发效率相较于 Go 低。所以 Rust 很适合系统层软件的开发,反倒不适合应用层软件的开发。在一个业务向的企业中,一般系统层会使用云基础设施,非核心的系统层,也可以用 Go 语言开发。当前很多优秀的系统层软件都是基于 Go 语言开发的。
所以,在一个企业中,系统层软件和应用层软件,往往是二选一的关系,也意味着,在编程语言上,也有了明确的选择:应用型企业选择 Go。如果企业核心产品是系统层软件,那么可以选择 Rust。
所以最终的编程语言选择一般是:Python(AI) + Go(后端) 或 Rust(底层) + Python(AI)。
当然,企业完全可以根据需要选择更多的编程技术栈组合。上述只是介绍一种通用情况下的选择建议。
另外,在编程语言选择时,建议主攻一门核心语言,同时根据职业目标补充其他相关语言,或者在不同阶段调整策略。这样既避免单一风险,又保持专业性。
来源:juejin.cn/post/7475609849939410983
央国企求职“性价比分析”:为什么这几年央国企火了?
浣熊say官方网站:hxsay.com/
浣熊say官方星球:hxsay.com/about/我的星球/…
正文
不知道最近大家有没有发现,越来越多的人在职业选择上都偏向与央国企、体制内等稳定性较高的岗位,而放弃了去私企、互联网等工资高但是强度大的工作。
从我身边的人了解到这一趋势不仅仅存在于工作了多年的职场老油条,希望找个地方躺平,在应届毕业生的群体里面也越来越明显。然而放在10年前,也就是2014年的时候谁毕业要是去国企、体制内可能会被笑话没有理想、躺平。
但是这两年风向仿佛突然变化了,公务员、央国企突然之间变得香了起来,似乎打工人也随着年龄的增长突然明白了一个道理,比起靠着燃烧生命加班挣来的卖命钱以及生活在不知道什么时候就会被干掉的压力之下,不如稳定的央国企来得实在。
35岁被毕业和干到退休的收入差距有多大?
首先叠甲,我这里说的国企是垄断央企的二级以上的公司或者省属国企总部,这些国企一般掌握着国家、省级的核心资源,不违法犯罪的情况下大概率是能干到退休的。当然,如果有人跟我杠说什么某某银行科技子公司,某某央企的孙子公司一样末尾淘汰,一样裁员不稳定,那么我只能说你说得都对!
假设我硕士毕业就去国企,然后月薪8k,2个月年终(央企二级公司,省属国企很容易达到),那么一年的收入是14*0.8 = 11.2w,然后男性目前的法定退休年龄是65岁,从25岁~65岁工作40年,总收入为 448w。
假设你硕士毕业就去互联网大厂,然后月薪3w,4个月年终(这里也是取得平均值来估计的),那么一年的收入为48w,然后35岁一般确实是互联网的大限,25~35岁工作10年,总收入为:480w。
其实,大多数情况下互联网大厂拿到3w的也是凤毛麟角,国企8k一个月的其实还是遍地都是,甚至一些省会的公务员都能达到8k/月甚至更多,两者职业生涯的总收入其实是差不多的。而且这里为了公平都没有考虑随着工龄增长工资的增长情况,其实在互联网大厂拿到100w年薪的难度远远大于你在国企熬年限,涨到1.5w。
所以,其实无论是选择私企打工挣钱,还是垄断国企躺平,你整个职业生涯获得的工资性收入都是差不多的,以2024年的世界观来看,很多私企甚至很难稳定拿到3w/月的工资到35岁。
有时候一个裁员毕业潮下来,你就不得不面临重新找工作的窘境,以前经济好的时候且没有AI时候,从事技术研发的人还可以自信的说我有技术走到哪里都不怕 。 如今,AI取代大多数工作岗位已经是明牌的事情了,那些掌握技术的人可能也不得不担忧自己能否快速找到合适自己的工作。
虽然,最近两会有委员提出取消35岁的年龄限制,我其实个人并不看好这个提案,因为本质说社会上的私企卡35岁主要是因为廉价、能加班的年轻人太多了,企业处于成本考虑肯定愿意招聘这些年轻人,那么上了年龄的中年人不能加班就可以滚蛋了。 这个事情不是一个提案就能解决的,除非整个职场氛围得到了改变,所有公司都将老员工视作一种公司财富而不是消耗品的时候,才是35岁年龄其实真的消失的时候。
普通打工人还真的需要考虑当你年龄上来之后,失去手头这份工作之后怎么办,你辛辛苦苦寒窗苦读这么多年,出入的高级写字楼,做的都是产值上千万的项目。突然让你失业在家,跑滴滴,送外卖这个心里落差你能接受吗?
当35岁你在街头送着外卖,跑着滴滴,你在央国企的同学或许已经是一个小领导,你去当公务员的同学现在是一个科长,他们再不济也是个小职工有着稳定的收入,不太为生计发愁,不知道那个时候的同学聚会你还有心情去参加不?
对于打工人来说稳定到底意味着什么?
20多岁的年轻人总觉得世界是自己的,脑子里面全部是幻想,总觉得爽文小说当中的主角是自己,不说大富大贵至少能够在企业混的风生水起,升职加薪,当上领导。
这些愣头青的想法我也有过,但是对大多数没有抱上大腿的人来说,工作2~3年之后就会让你明白这个世界的真实运转规则,很多事情不是下位者能够决定的,无论是在国企还是私企,本质上事情还是上位者说了算。
简单来说就是,领导说你行你就是行,领导说你不行那么就赶紧想办法跑路吧。
这种情况在私企、国企其实都会遇到,大家刻板印象中老是说国企的官僚主义严重,但是其实私企才是官僚主义更加严重的地方,而且比起来国企就是小打小闹。
本质上来说在真的央国企你的领导实际上是没有人事权的,他就算再讨厌你也只能通过调岗、扣绩效等方式来恶心你,但是真的没办法裁掉你。
但是在私企领导其实就是你们这个地方的土皇帝,你让领导不开心或者领导不喜欢你,那么是真的可以裁掉你,可能就是一句话的事你下午就不用来上班了都是有可能的事情。在这种地方,你除了舔领导,拼命加班,拼命卷之外没有任何办法,因为上位者掌握着你的生死存亡。
在这种极度竞争和内卷的环境下,你的全部精力都会投入到工作当中,但是其实你并不参与蛋糕的分配,也就是你卷出来的成果、剩余价值大部份被老板拿走了。同时,高强度的工作还会剥夺你其它的可能,让你没时间陪家人,没时间发展自己的事业,当你不得不开始发展自己的事业的时候,就是你已经失业的时候。
而在央国企的工作情况就会好很多,首先大多数岗位是比较稳定的,你不用过于担心失业裁员的情况发生。其次,至少在项目不忙的时候,你的休息时间是可以保障的,利用这些时间你其实可以选择发展自己的事业,就像刘慈心一样写科幻小说未来说不定能从副业上面赚到你一辈子赚不到的钱。
所以,比起那些完全占用你时间和心智的工作,我其实觉得轻松但是钱不那么多的工作更加适合一个人的长期发展,从一生的尺度上看财富积累未必会比短短的靠25~35这10年间挣到的钱少。
为什么这几年央国企火了?
其实很多在校的学弟、学妹们沟通,我发现现在的孩子比我们当年看得明白很多,也可能是不同的时代背景造就了不同的人的观点。
我们还是学生的时候听到的故事还都是什么王者荣耀100个月年终,互联网财富自由之类的神话,但是疫情的3年互联网和诸多的财富神话跌落神坛,大多数普通人能够保住手头的这份工作就是件值得庆幸的事情了。 即使是去华为、阿里、腾讯这样的大厂也很难有机会再实现当年的财富神话,技术改变世界的思潮也正在慢慢退潮,现在这些大厂也很难让你挣到财富自由的钱,逐渐变成了一份普通工作而已。
当你在校园中搏杀了20几年好不容易拿到了学士、硕士、博士文凭,这些私企会告诉你现实的残酷,你手中的文凭只能帮你到入职的3个月,之后就各凭本事了。 资本是逐利的,中国的企业更加喜欢揠苗助长,没有任何培养一个员工的文化在里面。所谓的培养更多的是PUA,告诉你这儿也不行,哪儿也不行,然后在绩效考核的时候顺利成章的把锅甩给你,来满足组长必须找一个倒霉蛋背绩效的制度要求。 我不否认能力极强者、能卷的人在这种环境中能够获得快速的升职加薪和财富,但并不是每个人都是大神,也不是每个人在做好本职工作之外还有心情去舔领导。
入职央国企能够在很大成都上帮你避免上述的问题,大型的央国企平台很大有足够的时间和资源来让员工成长,对于刚入职的新员工可能前面半年都不会给你安排真正意义上的工作,多数是各种培训,各种学习。 我以前经常听到在国企的同学抱怨又是什么培训、什么学习、什么党会,让人觉得很烦没有意义,但是在我看来每个人都应该感恩这些公司花着钱让你不干活儿的活动,真的不是所有的公司都这么有耐心。 除此之外,很少有央国企会期待从你身上压榨什么,因为大多数央国企从事的都是垄断行业,拥有足够的利润,并且这些利润也并非属于领导或者某个人的,而是属于整个集团,国家。你和领导本质上都一样,只是这个国企的打工人,没必要压榨你太过分,毕竟赚的钱也一分不会到领导自己包里,对你个人的包容性也会更强一些。
所以当经济增长变缓,私企难以让人挣到足以财富自由的钱,大家就会发现其实没有必要去承担那么大的压力只是比稳定的工作多一点儿钱。这个时候一份稳定、有自己业余时间的央国企工作性价比就变得更高了起来。一边可以用工资保障自己的生活,一边开拓自己的事业在副业这个第二战场挣到更多的钱,确实会比在私企打工35被裁要体面得多。
The End
其实对于职业的选择,有一个核心逻辑就是去门槛更高的地方。
有的人说,大厂门槛很高啊,问那么多技术,刷那么多题,也是万里挑一才能进去一个人。
但是,实际上这些东西不算门槛,真正的门槛是把人堵在外面的不可逾越的鸿沟,比如说:如果你本科不是学的临床专业,那么你一辈子都没办法当上医生,除非重新高考!这才是真正意义上的门槛,而无论是多么高深的技术,只要肯学都能够学会的。
所以,大型垄断央国企其实是个门槛更高的地方,好的岗位除了应届生就没有就会进去,同时一旦进去占住坑了也很难被裁掉,除非你自己离职。大家可能经常会听说哪个国企的用自己的业余时间努力学习然后去了互联网大厂的。但是你可能完全没有听过那个私企的毕业没去 "中国烟草" 靠着自己的不懈努力,社招进入了中国烟草。
如果是应届生,尽量去门槛高、稳定的地方,考虑长期发展而不是贪图短时间的利益,这样一来即使你的能力没有那么强,也可以用马拉松的方式跑到最后。
人生是一段长跑,不到最后一刻不知道谁输谁赢,就算是活得比别人长,那么其实你最后也胜利了。
来源:juejin.cn/post/7343161077061992458
小红书创始人瞿芳,武汉人,北京外国语大学毕业,2013 年从外企离职,目前身价 120 亿
大家好,我是二哥呀。
今天我们来聊聊小红书的创始人瞿芳,1985 年出生于湖北武汉,毕业于武汉外国语学校,硕士就读于北京外国语学校。
毕业后进入贝塔斯曼工作,这是一家老牌外企,1835 年就有了,真的非常老,瞿芳在这里工作了 5 年。
瞿芳的执行力也是拉满,2013 年 5 月底离职,6 月赴美寻找风投,7 月初就和老乡毛文超在上海创立了小红书的母公司行吟信息科技有限公司。
长相上我觉得有一点邓丽君的感觉,大家觉得呢?
- 2015-2016 年,瞿芳连续两年被《创业邦》评为“值得关注的女性创业者”;这些年小红书的成长,瞿芳确实功不可没,值得关注。
- 2017 年,瞿芳荣登腾讯“我是创始人”荣耀榜单;小红书背后有阿里和腾讯两家大佬的投资,原来两家是从来不投一家公司的,瞿芳背后的斡旋算是让两家暂时握了手。
- 2020 年,瞿芳入选“中国最具影响力的 30 位商界女性”榜单;目前来看,小红书还处在上升势头,并且流量拉满,瞿芳的身价肯定也会水涨船高。
- 2024 年,瞿芳以 120 亿元的人民币财富位列《2024-胡润榜》的第 433 位;这还是在小红书没有上市的情况下。
瞿芳曾在采访中强调,用户体验和社区氛围是小红书最看重的。
这也是小红书这个平台和微博、抖音最大的区别,你可能在小红书上百粉不到,但发布的内容却会被推荐到平台首页,成为爆款。
微的推荐机制现在也有这种趋势,就是粉丝数越少,反而被推荐的机会越多。
瞿芳认为,品牌与用户的沟通应该从“教学模式”转向“恋爱模式”。
也就是说,我们创作者不能再以老师的角度切入,把读者作为学生来传达信息,而是奔着双方恋爱的方式切入。
更加的纯粹,双方的地位更加的对等。
宝子们,都看到了吧,我爱你们,😄
2013 年的时候,跨境旅游开始兴起,于是,瞿芳就和毛文超一起去找当地的购物达人,把他们的经验编成了一本厚厚的 PDF,书名就叫“小红书”。
这本 PDF 放到网上以后,引起了巨大的反响,一个月的下载量就突破了 50 万次。
尝到了甜头后,瞿芳和毛文超再接再厉,于 2013 年 12 月上线了小红书 App,相当于提供了一个购物的分享平台,注意不是电商平台,而是社区分享平台,让用户来分享自己的购物心得。
这个定位就非常的巧妙。
如果单纯地做电商平台,那么竞争对手多了去,比如说淘宝、天猫、京东,以及拼多多。
但做社区平台的话,当时还没有什么竞争对手,虽然点评和美图秀秀都曾在自己的业务中加入大量的社区内容,并放出豪言,但最终都没有竞争过小红书。
2014 年,小红书就聚集了几百万用户了,于是瞿芳就上线了一款希腊产的清洗液,结果直接被秒光了。
到 2017 年,小红书的营收就突破了 100 亿。
截止到目前,小红书已经发展成为了一个生活社区,基本上你想要的东西,你想找的地方,你想看的美女,小红书上都有。据说,月活用户已经达到了 3 亿。
其中女性用户占比 70%,日均用户搜索渗透率达到 60%,用户生成内容(UGC)占比高达 90%。
根本不需要 KOL。
2025 年 1 月,由于 TikTok 可能会被美国封禁,所以大量的海外用户开始涌入小红书。
中西文化的融合,在此刻显然格外的自然和松弛。
我现在打开小红书,已经很少看到原住民发的东西了,这波算法也被太平洋彼岸的热情感染了。
瞿芳在一次采访中的一段话我觉得很值得分享给大家,我套用一下:
“就像今天手机屏幕前的你们,可能大学生可能是工作党,但不管大家是怎样的身份,回到家里,可能还是会跟家人吃一顿最简单的饭,跟最爱的人一起去做一些有创造性的事情。”
我们要回到生活中去,而不只是活在虚拟世界里。
三分恶面渣逆袭
我这人一向说到做到,每天给大家汇报一下面渣逆袭的进度,这就来。今天修改到第 36 题。
35.你们线上用的什么垃圾收集器?
我们生产环境中采用了设计比较优秀的 G1 垃圾收集器,因为它不仅能满足低停顿的要求,而且解决了 CMS 的浮动垃圾问题、内存碎片问题。
G1 非常适合大内存、多核处理器的环境。
以上是比较符合面试官预期的回答,但实际上,大多数情况下我们可能还是使用的 JDK 8 默认垃圾收集器。
可以通过以下命令查看当前 JVM 的垃圾收集器:
java -XX:+PrintCommandLineFlags -version
UseParallelGC
= Parallel Scavenge + Parallel Old
,表示新生代用Parallel Scavenge
收集器,老年代使用Parallel Old
收集器。
因此你也可以这样回答:
我们系统的业务相对复杂,但并发量并不是特别高,所以我们选择了适用于多核处理器、能够并行处理垃圾回收任务,且能提供高吞吐量的Parallel GC
。
但这个说法不讨喜,你也可以回答:
我们系统采用的是 CMS 收集器,能够最大限度减少应用暂停时间。
内容来源
三分恶的面渣逆袭:javabetter.cn/sidebar/san…
二哥的 Java 进阶之路(GitHub 已有 13000+star):github.com/itwanger/to…
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。
来源:juejin.cn/post/7461772464738402342
谈谈在大环境低迷下,找工作和入职三个月后的感受
前言
今天是新公司入职后的三个多月了个人也是如愿的转正了,对于我个人而言此时的心情好像没有三个月前刚拿到offer那样的喜悦和兴奋了,更像一件很普通的事情在你身边发生了吧。从2023年底离职在到2024年初开始找工作中间休息了三个月,找工作到入职花了一个多月,在这个过程中也是第一次真真切切感受到了所谓大环境低迷下的“前端已死的言论”,也给大家分享一下自己入职三个月的个人感受吧。
从上一家公司离职时的个人感受
因为上一家公司的工作性质是人力外包驻场开发,年底客户公司(中国移动成都产业研究院)我所在的项目组不需要外包人员了,个人也是被迫拿了赔偿灰溜溜的走人了。
工作感受:对于这段工作经历我个人还是比较认可的,毕竟这里没有任何工作压力,也不加班,工作量少,有很多时间去学习新东西,做做自己的开源,认识了新的朋友等等。
学历的重要性:在这里面随便拎一个人出来可能就是研究生学历的国企单位,自己真实的意识到了学历的重要性(第一学历小专科的我瑟瑟发抖)。
和优秀的人共事:如果在一个长期压抑低沉消极的环境下工作无论你的性格在怎么积极乐观开朗,可能也很容易被影响到。相反如果是和在一群积极,乐观,开朗,充满自信的环境和人一起工作,相信你也会变得积极,乐观,自信这或许也是我这一段工作经历最大的收获吧。
2023年底找工作的市场就业环境
抱着试一试的心态在boss上更新了自己的简历状态,不出所料软件上面安静的奇怪ps:49入国军的感觉,已读未回可能是很失望的感觉吧,但年底找工作令人绝望的是大多数公司都是未读未回,这也就意味着年底基本上是没有正常公司招聘的了。
大概投了两周简历后终于在智联招聘上约到了一个短期三个月岗位的面试,现场两轮面试通过了,不过最终还是没有选择去。
原因有很多:
- 现场的工作环境个人感觉很压抑,从接待我前台和面试官都能感觉满脸写着疲惫
- 说公司最近在996,你也需要和我们一起
- 招聘岗位和工作内容是threejs开发,薪资却说只能给到普通前端开发的水平
- 人力外包公司hr的反复无常令我恶心,二面通过后hr给我打电话最主要就是聊薪资吧,电话内容也很简单hr:成都大部分前端的薪资是在XX-XX,可能给不到你想要的薪资,可能要往下压个1-2K。我:我提的薪资完全是在你们发布招聘岗位薪资的区间,既然你们给不到为什么要这样写了(有感到被侮辱了)。过了几天之后人力外包的hr又给我电话,说可以在原来提的薪资基础上加1.4K,希望能早点去客户公司入职。
总结:年底招聘的公司基本上没啥好鸟,如果你的经济能力还行的话让自己放松休息一段时间也是不错的选择
2024年初找工作:真实的感受到了大环境的低迷下的市场行情
印象最深刻的是在疫情时期的2021年,那会儿出来找工作boos上会有很多HR主动给你打招呼,一周大概能五六个面试,大专学历也有机会去自研公司
解封之后本以为市场行情会变得回缓,结果大概就是今年可是未来十年行情最好的一年
简单总结一下2024年的成都就业环境大概这样的:
- 只有外包公司会招专科学历
- boss上只给hr发一句打招呼的快捷语,99% 都是已读不回
- 大多数要完简历之后就没有后续了
- 待遇好的公司对于学历的要求更严格了(211,985)
- 给你主动打招呼的基本上都是人力外包公司
截至入职新公司前boss上面的投递状况:沟通了1384个岗位,投递了99份简历,一共约到了 8 家公司的面试
今年找工作的个人感受:不怕面试,就怕没有面试机会
首先说一下个人的一些情况吧,因为在创业小公司待过在技术栈方面个人认为算是比较全面的了
项目经验:做过管理系统(CRM,B2C,ERP,saas等管理系统)、商城和门户网站(响应式,自适应)、移动端(H5,小程序,app)、低代码和可视化(工作流,可视化大屏,3d编辑器)、第三方开发(腾讯IM,企业微信侧边栏)、微前端
项目经历:从0-1搭建过整个大项目的基建工作,封装过项目中很多功能性组件和UI组件二次封装(提高开发效率),接手过屎山代码并重构优化,约定项目的开发规范,处理很多比较棘手的疑难bug和提供相关的技术方案,没有需求概念下的敏捷开发,从0-1的技术调研等
代码方面:写过几个开源项目虽然star数量不多(目前最多一个项目是600+),但在代码规范和可读性方面个人认为还是比较OK的(至少不会写出屎山代码吧)
工作经验(4年):2020毕业至今一直从事前端开发工作
学历:自考本科学历(貌似没啥卵用)
学历确实是我很硬伤的一个点但是没办法,人嘛总归要为年轻时的无知买单吧
在这样的背景下开启了24年的找工作,从2月26号开始投递简历到4月1号拿到offer差不多一个多月左右时间,一共约到了8加公司的面试,平均一周两家公司
大概统计了一下这些公司的面试情况:
公司A:
- 数组哪些方法会触发Vue监听,哪些不会触发监听
- position 有哪些属性
- vue watch和computed的区别,computed和method的区别
- vue的watch是否可以取消? 怎么取消?
- position:absolute, position:fixed那些会脱离文档流
- 如何获取到 pomise 多个then 之后的值
- 常见的http状态码
- 谈谈你对display:flex 弹性盒子属性的了解
- 如何判断一个值是否是数组
- typeof 和instanceof的区别
- es6-es10新增了那些东西
- 离职原因,期望薪资,职业规划
公司B
到现场写了一套笔试题,内容记不清楚了
公司C
- vue router 和route 区别
- 说说重绘和重排
- css 权重
- 项目第一次加载太慢优化
- 谈谈你对vue这种框架理解
- sessionstorage cookie localstorage 区别
- 了解过.css 的优化吗?
- 闭包
- 内存泄漏的产生
- 做一个防重复点击你有哪些方案
- 解释一些防抖和节流以及如何实现
- 说一下你对 webScoket的了解,以及有哪些API
- 说一下你对pomise的理解
- vue2,vue3 中 v-for 和v-if的优先级
- 说说你对canvas的理解
公司D
笔试+面试
- vue 首屏加载过慢如何优化
- 说说你在项目中封装的组件,以及如何封装的
- 后台管理系统权限功能菜单和按钮权限如何实现的
- vue 中的一些项目优化
- 期望薪资,离职原因,
- 其他的记不清楚了
公司E
笔试+面试+和老板谈薪资
1.笔试:八股文
2.面试:主要聊的是项目内容比如项目的一些功能点的实现,和项目的技术点
3.老板谈薪资:首先就是非技术面的常规三件套(离职原因,期望薪资,职业规划),然后就是谈薪资(最终因为薪资给的太低了没有选择考虑这家)
公司F
也是最想去的一家公司,一个偏管理的前端岗位(和面试官聊的非常投缘,而且整个一面过程也非常愉快感受到了十分被尊重)
可惜的是复试的时候因为学历原因,以及一些职业规划和加班出差等方面上没有达到公司的预期也是很遗憾的错过了
一面:
- vue 响应式数据原理
- 说说es6 promise async await 以及 promise A+规范的了解
- 谈谈es6 Map 函数
- 如何实现 list 数据结构转 tree结构
- webScoke api 介绍
- webScoke 在vue项目中如何全局挂载
- vuex 和 pinia 区别
- 谈谈你对微任务和宏任务的了解
- call apply bind 区别
- 前端本地数据存储方式有哪些
- 数组方法 reduce 的使用场景
- 说说你对 css3 display:flex 弹性盒模型 的理解
- vue template 中 {{}} 为什么能够被执行
- threejs 加载大模型有没有什么优化方案
- 离职原因,住的地方离公司有多远,期望薪资
- 你有什么想需要了解的,这个岗位平时的工作内容
二面:
1.我看写过一个Express+Mongoose服务端接口的开源项目,说说你在写后端项目时遇到过的难点
2.介绍一下你写的threejs 3d模型可视化编辑器 这个项目
3.以你的观点说一下你对three.js的了解,以及three.js在前端开发中发挥的作用
4.现在的AI工具都很流行,你有没有使用过AI工具来提高你对开发效率
5.说说你认为AI工具对你工作最有帮助的地方是哪些
6.以你的观点谈谈你对AI的看法,以及AI未来发展的趋势
7.你能接受出差时间是多久
8.你是从去年离职的到今天这这几个月时间,你是去了其他公司只是没有写在简历上吗?
9.说说你的职业规划,离职原因,你的优点和缺点,平时的学习方式
公司G
一共两轮面试,也是最终拿到正式offer入职的公司
一面:
- 主要就是聊了一下简历上写的项目
- 项目的技术难点
- 项目从0-1搭建的过程
- 项目组件封装的过程
- vue2 和 vue3 区别
- vue响应式数据原理
- 对于typescript的熟练程度
- 会react吗? 有考虑学习react吗?
- 说一下你这个three.js3d模型可视化编辑器项目的一个实现思路,为什么会想写这样一个项目
二面:
- 说说了解的es6-es10的东西有哪些
- 说说你对微任务和宏任务的了解
- 什么是原型链
- 什么是闭包,闭包产生的方式有哪些
- vue3 生命周期变化
- vue3 响应式数据原理
- ref 和 reactive 你觉得在项目中使用那个更合适
- 前端跨越方式有哪些
- 经常用的搜索工具有哪些?
- 谷歌搜索在国内能使用吗?你一般用的翻墙工具是哪种?
- 用过ChatGPT工具吗? 有付费使用过吗?
- 你是如何看待面试造航母工作拧螺丝螺丝的?
- 谈谈你对加班的看法?
- 你不能接受的加班方式是什么?
- 为什么会选择自考本科?
- 你平时的学习方式是什么?
- 一般翻墙去外网都会干什么?,外网学习对你的帮助大吗?
- 上一家公司的离职原因是什么,期望薪资是多少, 说说你的职业规划
- 手里有几个offer?
hr电话:
- 大概说了一下面试结果通过了
- 然后就是介绍了一下公司的待遇和薪资情况?
- 问了一下上一家公司的离职原因以及上一家公司的规模情况?
- 手里有几个offer?
- 多久能入职?
因为后面没有别的面试了,再加上离职到在到找工作拿到offer已经有四个月时间没有上班了,最终选择了入职这家公司
入职第三天:我想跑路了!
入职后的第一天,先是装了一下本地电脑环境然后拉了一下项目代码熟悉一下,vue3,react,uniapp 项目都有
崩溃的开始:PC端是一个saas 系统由五个前端项目构成,用的是react umi 的微前端项目来搭建的,也是第一次去接触微前端这种技术栈,要命的是这些项目没有一个是写了readme文档的,项目如何启动以及node.js版本这些只能自己去package.json 文件去查看,在经过一番折腾后终于是把这几个项目给成功跑起来了,当天晚上回家也是专门了解了一下微前端
开始上强度: 入职的第二天被安排做了一个小需求,功能很简单就是改个小功能加一下字段,但是涉及的项目很多,pc端两个项目,小程序两个项目。在改完PC端之后,开始启动小程序项目不出所料又是一堆报错,最终在别的前端同事帮助下终于把小程序项目给启动成功了。
人和代码有一个能跑就行:入职的第三天也从别的同事那里了解到了,之前sass项目组被前端大规模裁员过,原因嘛懂得都懂? 能写出这样一堆屎山代码的人,能不被裁吗?
第一次知道 vue 还可以这样写
对于一个有代码强迫症的人来说,在以后的很长一段时间里要求优化和接触完全是一堆屎山一样代码,真的是很难接受的
入职一个月:赚钱嘛不寒掺
在有了想跑路的想法过后,也开始利用上班的空余时间又去投递简历了,不过现实就是在金三银四的招聘季,boss上面依旧是安静的可怕,在退一步想可能其他公司的情况也和这边差不多,于是最终还是选择接受了现实,毕竟赚钱嘛不寒掺
入职两个月:做完一个项目迭代过后,感觉好多了
在入职的前一个月里,基本上每天都要加班,原因也很简单:
1.全是屎山的项目想要做扩展新功能是非常困难的
2.整个项目的逻辑还是很多很复杂的只能边写项目边熟悉
3.因为裁了很多前端,新人还没招到,但是业务量没有减少只能加班消化
功能上线的晚上,加班到凌晨3点
在开发完一个项目迭代过后也对项目有了一些大概的了解,之后的一些开发工作也变得简单了许多
入职三个月:工作氛围还是很重要滴
在入职三个月后,前端组团队的成员也基本上是组建完成了,一共14人,saas项目组有四个前端,虽然业务量依然很多但是好在有更多的人一起分担了,每周的加班时间也渐渐变少了
在一次偶然间了解到我平时也喜欢打篮球后,我和公司后端组,产品组的同事之间也开始变得有话题了,因为大家也喜欢打球,后来还拉了一个篮球群周末有时间大家也会约出来一起打打球
当你有存在价值后一切的人情世故和人际关系都会变得简单起来
在这个世界上大多数时候除了你的父母等直系亲属和另一半,可能会对你无条件的付出
其余任何人对你尊重和示好,可能都会存在等价的利益交换吧
尤其是在技术研发的岗位,只有当你能够完全胜任这份工作时,并且能够体现出足够的价值时才能够有足够的话语权
入职三个月后的感受
- 公司待遇:虽然是一个集团下面的子公司 (200+人)但待遇只能说一般吧,除了工资是我期望的薪资范围,其他的福利待遇都只能是很一般(私企嘛,懂得都懂)
- 工作强度: 听到过很多从大厂来的新同事抱怨说这边的工作量会很大,对我来说其实都还ok,毕竟之前在极端的高压环境下工作过
- 工作氛围:从我的角度来讲的话,还是很不错的,相处起来也很轻松简单,大家也有很多共同话题,没有之前在小公司上班那么累
大环境低迷下,随时做好被裁掉的准备
从2020年毕业工作以来,最长的一段工作经历是1年4个月,有过三家公司的经历
裁员原因也很简单:创业小公司和人力外包,要么就是小公司经营问题公司直接垮掉,或者就是人力外包公司卸磨杀驴
除非你是在国企单位上班,否则需要随时做好被裁掉的准备
什么都不怕,就怕太安逸了
这句话出自《我的团长我的团》电视剧里面龙文章故意对几十个过江的日本人围而不歼时和虞啸卿的对话,龙文章想通过这几十个日本人将禅达搅得鸡犬不宁,来唤醒还在沉睡在自己温柔乡的我们,因为就在我们放松警惕时日本人就已经将枪口和大炮对准了我们。
或许大家会很不认同这句话吧,如果你的父母给你攒下了足够的资本完全可以把我刚才的话当做放屁,毕竟没有哪一个男生毕业之前的梦想是车子和房子,从事自己喜欢的工作不好吗? 但现实却是你喜欢工作的收入很难让你在这座城市里体面的生活
于我而言前端行业的热爱更多是因为能够给我带来不错的收入所以我选择了热爱它吧,所以保持终身学习的状态也是我需要去做的吧
前端已死?
前端彻底死掉肯定是不会的,在前后端分离模式下的软件开发前端肯定是必不可少的一个岗位,只不过就业环境恶劣下的情况里肯定会淘汰掉很多人,不过35岁之后我还是否能够从事前端行业真的是一个未知数
结语
选择卷或者躺平,只是两种不同的生活态度没有对与错,偶尔躺累了起来卷一下也是可以的,偶尔卷累了躺起了休息一下也是不错的。
在这个网络上到处是人均年收入百万以及各种高质量生活的时代,保持独立思考,如何让自己不被负面情绪所影响才是最重要的吧
来源:juejin.cn/post/7391065678546157577
裸辞后,我活得像个废物,但我终于开始活自己
哈喽,大家好!我是Web大鹅只会叫!下去沉淀了那么久,终于有时间回来给大家写点什么了!
你问我人为什么活着?我哪知道啊?我又不是上帝!释迦牟尼为了解这个问题还跑去出家,结果发现人活着就是为了涅槃——也就是死。所以别问我人生的意义,我也没搞明白!不过我能告诉你一个答案,那就是裸辞后,我终于知道了为什么要活着——那就是为了“活得自由”!
裸辞后,那些走过的路,和你说的“脏话”
2024年8月,我做了一个震惊所有人的决定——裸辞!是的,没错,我就是那种毫不犹豫地辞了职、丢下稳定收入和安稳生活,拿着背包去走四方的“疯子”。放下了每天早起20公里开车上班的压力,放下了无聊的加班、枯燥的开会,放下了所谓的“你要努力争取美好生活”的叮嘱。一切都在“离开”这一刻轻轻拂去,带着一种挥之不去的自由感。
带着亲人的责怪、朋友的疑问、同事的眼神、以及自己满满的疑惑,我开始了这段没有目的的旅行。我不知道我想找什么,但我知道,我不想再活得像以前那样。
我走过了无数地方,南京、湖州、宁波、杭州、义乌、金华、嘉兴、镇江、扬州、苏州、无锡、上海……一路走来,路过了每个风景,每个城市,每个人生。我甚至去了中国最北的漠河,站在寒风凛冽的雪地里,终于明白了一个道理:“你活着,才是最值得庆祝的事。
你知道吗,最让人清醒的,不是远方的景色,而是走出去之后,终于能脱离了那一套“你该做什么”的公式。每天不用设闹钟,不用准时吃饭,不用打卡上班,不用开会骂娘,再也不被地铁里的拥挤挤得喘不过气。生活突然变得宽松,我竟然开始意识到:我一直追求的美好生活,原来只是在为别人拼命。
走出内卷圈子的那一刻,我认为我是世界上最快乐的小孩了,我们渴望着幸福却糟践着现在,你所期望的美好未来却始终都是下一个阶段!你认为你是一个努力拼搏美好未来的人。可是现实比理想残酷的多!你没日没夜的拼搏,却让别人没日没夜的享受!你用尽自己半条命,换来的是下半辈子的病!我在裸辞后就告诉我自己:从今以后你想干什么、就干什么!你就是世界的主人! 嗯~ 爽多了!
走过的路,都在暗示我
我在大兴安岭漠河市的市里住了5天,住在一个一天40元的宾馆、干净、暖和!老板是一个退休的铁道工人。脸和双手都布满了冻伤,他的妻子(大姨)很面善。每天都会在我回来的时候和我聊上几句从前,安排一些极寒天气的注意事项。
有一天我去北极村回来,大姨和我聊了一会。大姨对我讲:“趁年轻、别把自己困起来,出去走走。不像我们,60年没出过这片土地,到头还要葬在这片土地上!”。她说这句话的时候没有忧虑、没有悲伤,却是一种满足感。是啊!60多了,还能追求什么?忙了大半辈子,把孩子都送出了这片土地,自己也没有激情出去走走了,很害怕自己的以后也是这样。
我20多岁的年纪,想的不是努力拼搏挣钱、不是搞事业。却总想着无所事事。我觉得自己像一个没有完全“被时间遗弃”的人,我甚至觉得自己不属于这个时代,这个不知道为了什么而拼命的时代。每走一步都好像在掏空自己积压已久的情绪:压力、焦虑、焦灼,让我很享受这种感觉。然后我想起来一本书里的话:“你活着,不是为了活给别人看。“ 是啊,我们都明白这个大道理,可自己从来没打算让自己脱离这个主线。我开始明白,我这次的旅行不是去寻找什么,而是放下什么!
从别人嘴里听到的“脏话”,其实是自己内心的尖刺
这段时间里,我经常回想起来那些让我神经紧绷的日子。尤其是我对“人” 这个物种越来越敏感的那个时期————‘恶毒、自私、无理、肮脏’。朋友的欺骗、同事的推锅、亲人的折磨都是罪恶的!可是到头来,事情还是发生了。地球还是在自转,太阳一样正常升起落下。这些都没有在你认为的这些琐事中消失。我不明白我还在纠结什么?
事实上,这些乱七八糟的事情并不是指向我个人的,它只是我内心脆弱的反射。是的,我一直在内耗自己罢了,把自己放在了一个焦虑的漩涡里。假装没事、假装坚强,结果别人一句话就能作为击垮我的最后一击。直到有一天,我发现我讨厌的只是我自己。所以我决定我不要去在意别人说什么、做什么,我不要逃避问题,我想听听我内心的想法,我不想让自己认为别人在定义我。
过程的意义:也许就是为了“停一停”
好了,我知道我的文采不好,但是也应该有个结尾。
在这一路上,我认识了很多有趣的人,他们不同风格的服装、不同口音,各式各样的生活方式。也有着各式各样的理想和困惑。有的喜欢在山顶等着日出的奇迹,有的则是想在湖边静静地坐着。而我,就是个迷途的羔羊,没有群体头羊的带领,我穿行在这些不同的路途中,慢慢摸索着向所有方向前进着。
偶尔我也会停下来,坐在湖边吹着风、闭上眼睛,听风,感受这一刻的宁静。然后我会微笑,我认为这个时候的我有了轻松的感觉。生活的答案我在这个时候找到了。
我意识到,未来不是重要的,现在才是应该享受的。我不知道我下一步要去哪里,但是我想先停下来看一看,呼吸一下。停下来不是因为我没有了目的,而是我知道,目的地并不重要,重要的是,我和自己在一起,心里不再有那么多焦虑,不再被过去的焦虑所束缚。
所以,我选择了离开,离开这一切,放下所有的焦虑和期待,享受我自己想要的生活。也许,活着的意义不在于追寻一个遥远的目标,而是过好每一个‘现在’。
来源:juejin.cn/post/7454064311079813132
好人难当,坏人不做
好人难当,以后要多注意了,涨点记性。记录三件事情证明下:
1. 免费劳动
之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都一年多过去了,还让我免费帮他改需求,我就说没时间,他说没时间的话可以把源码给他,他自己学着修改,我就直接把源码给他了,这个项目辛苦了一个多月,钱一毛也没赚到,我倒是搭进去一台服务器,一年花了三百多吧。现在源码给他就给他了吧,毕竟同学一场。没想到又过了半年,前段时间又找我来改需求了。这个项目他们家自己拿着赚钱,又不给我一毛钱,我相当于免费给他家做了个软件,还要出服务器钱,还要免费进行维护。我的时间是真不值钱啊,真成义务劳动了。我拒绝了,理由是忙,没时间。
总结一下,这些人总会觉得别人帮自己是理所当然的,各种得寸进尺。
2. 帮到底吧
因为我进行了仲裁,有了经验,然后被一个人加了好友,是一个前同事(就是我仲裁的那家公司),然后这哥们各种问题我都尽心回答,本着能帮别人一点就帮别人一点的想法,但是我免费帮他,他仲裁到手多少钱,又不会给我一毛钱。这哥们一个问题接一个,我都做了回答,后来直接要求用我当做和公司谈判的筹码,我严词拒绝了,真不知道这人咋想的,我帮你并没有获得任何好处,你这个要求有点过分了,很简单,他直接把我搬出来和公司谈判,公司肯定会找我,会给我带来麻烦,这人一点也没想这些事。所以之后他再询问有关任何我的经验,我已经不愿意帮他了。
总结一下,这些人更进一步,甚至想利用下帮自己的人,不考虑会给别人带来哪些困扰。
3. 拿你顶缸
最近做了通过一个亲戚接了一个项目,而这个亲戚的表姐是该项目公司的领导,本来觉得都是有亲戚关系的,项目价格之类开始问了,他们没说,只是说根据每个人的工时进行估价,后面我们每个人提交了个人报价,然后还是一直没给明确答复,本着是亲戚的关系,觉得肯定不会坑我。就一直做下去了,直到快做完了,价格还是没有出来,我就直接问了这个价格的事情,第二天,价格出来了,在我报价基础上直接砍半。我当然不愿意了,后来经过各种谈判,我终于要到了一个勉强可以的价格,期间群里谈判也是我一个人在说话,团队的其他人都不说话。后来前端的那人问我价格,我也把过程都实话说了,这哥们也要加价,然后就各种问我,我也啥都告他了。后来这个前端在那个公司领导(亲戚表姐)主动亮明身份,她知道这个前端和那个亲戚关系好,然后这个前端立马不好意思加价了,并且还把锅甩我头上,说是我没有告诉他她是他姐。还说我不地道,我靠,你自己要加价,关我啥事,你加钱也没说要分我啊,另外我给自己加价的时候你也没帮忙说话啊,我告诉你我加价成功了是我好心,也是想着你能加点就加点吧,这时候你为了面子不加了,然后说成要加价的理由是因为我,真是没良心啊。后面还问我关于合同的事情,我已经不愿意回答他了,让他自己找对面公司问去。
总结一下,这些人你帮他了他当时倒是很感谢你,但是一旦结果有变,会直接怪罪到你头上。
4. 附录文章
这个文章说得挺好的《你的善良,要有锋芒》:
你有没有发现,善良的人,心都很软,他们不好意思拒绝别人,哪怕为难了自己,也要想办法帮助身边的人。善良的人,心都很细,他们总是照顾着别人的情绪,明明受委屈的是自己,却第一时间想着别人会不会难过。
也许是习惯了对别人好,你常常会忽略自己的感受。有时候你知道别人是想占你便宜,你也知道别人不是真心把你当朋友,他们只是觉得你好说话,只是看中了你的善良,但是你没有戳穿,你还是能帮就帮,没有太多怨言。
你说你不想得罪人,你说你害怕被孤立,可是有人在乎过你吗?
这个世界上形形色色的人很多,有人喜欢你,有人讨厌你,你没有办法做到对每一个人好,也没办法要求每一个人都是真心爱你。所以你要有自己的选择,与舒服的人相处,对讨厌的人远离,过你自己觉得开心自在的生活就好,没必要为了便利别人,让自己受尽委屈。
看过一段话:善良是很珍贵的,但善良要是没有长出牙齿来,那就是软弱。
你的善良,要有锋芒,不要把时间浪费在不值得的人身上。对爱你的人,倾心相助,对利用你的人,勇敢说不。
愿你的善良,能被真心的人温柔以待。
来源:juejin.cn/post/7455667125798780980
让闲置 Ubuntu 服务器华丽转身为家庭影院
让闲置 Ubuntu 服务器华丽转身为家庭影院
在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。
在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。
一、实现 Windows 与 Ubuntu 服务器文件互通
要打造家庭影院,首先得让本地 Windows 电脑和 Ubuntu 服务器之间能够方便地传输电影文件。我选择安装 Samba 来实现这一目的。
- 安装 Samba:在 Ubuntu 服务器的终端中输入命令
sudo apt-get install samba samba-common
系统会自动下载并安装 Samba 相关的软件包。
- 备份配置文件:为了以防万一,我先将原来的 Samba 配置文件进行备份,执行命令
mv /etc/samba/smb.conf /etc/samba/smb.conf.bak
- 新建配置文件:使用
vim /etc/samba/smb.conf
命令打开编辑器,写入以下配置内容:
[global]
server min protocol = CORE
workgroup = WORKGR0UP
netbios name = Nas
security = user
map to guest = bad user
guest account = nobody
client min protocol = SMB2
server min protocol = SMB2
server smb encrypt = off
[NAS]
comment = NASserver
path = /home/bddxg/nas
public = Yes
browseable = Yes
writable = Yes
guest ok = Yes
passdb backend = tdbsam
create mask = 0775
directory mask = 0775
这里需要注意的是,我计划的媒体库目录是个人目录下的 nas/
,所以 path
是 /home/bddxg/nas
,如果大家要部署的话记得根据自己的实际情况修改为对应的位置。 
- 连接 Windows 电脑:在 Windows 电脑这边基本不需要什么复杂配置,因为在网络里无法直接看到 Ubuntu,我直接在电脑上添加了网络位置。假设服务器地址是
192.168.10.100
,那么添加网络位置就是 \\192.168.10.100\nas
,这样就可以在 Windows 电脑和 Ubuntu 服务器之间传输文件了。
要打造家庭影院,首先得让本地 Windows 电脑和 Ubuntu 服务器之间能够方便地传输电影文件。我选择安装 Samba 来实现这一目的。
- 安装 Samba:在 Ubuntu 服务器的终端中输入命令
sudo apt-get install samba samba-common
系统会自动下载并安装 Samba 相关的软件包。
- 备份配置文件:为了以防万一,我先将原来的 Samba 配置文件进行备份,执行命令
mv /etc/samba/smb.conf /etc/samba/smb.conf.bak
- 新建配置文件:使用
vim /etc/samba/smb.conf
命令打开编辑器,写入以下配置内容:
[global]
server min protocol = CORE
workgroup = WORKGR0UP
netbios name = Nas
security = user
map to guest = bad user
guest account = nobody
client min protocol = SMB2
server min protocol = SMB2
server smb encrypt = off
[NAS]
comment = NASserver
path = /home/bddxg/nas
public = Yes
browseable = Yes
writable = Yes
guest ok = Yes
passdb backend = tdbsam
create mask = 0775
directory mask = 0775
这里需要注意的是,我计划的媒体库目录是个人目录下的 nas/
,所以 path
是 /home/bddxg/nas
,如果大家要部署的话记得根据自己的实际情况修改为对应的位置。
- 连接 Windows 电脑:在 Windows 电脑这边基本不需要什么复杂配置,因为在网络里无法直接看到 Ubuntu,我直接在电脑上添加了网络位置。假设服务器地址是
192.168.10.100
,那么添加网络位置就是\\192.168.10.100\nas
,这样就可以在 Windows 电脑和 Ubuntu 服务器之间传输文件了。
二、安装 Jellyfin 搭建家庭影院
文件传输的问题解决后,接下来就是安装 Jellyfin 来实现家庭影院的功能了。
- 尝试 Docker 安装失败:一开始我选择使用 Docker 安装,毕竟 Docker 有很多优点,使用起来也比较方便。按照官网指南进行操作,在第三步启动 Docker 并挂载本地目录的时候却一直失败。报错信息为:
docker: Error response from daemon: error while creating mount source path '/srv/jellyfin/cache': mkdir /srv/jellyfin: read-only file system.
即使我给
/srv/jellyfin
赋予了777
权限也没有效果。无奈之下,我决定放弃 Docker 安装方式,直接安装 server 版本的 Jellyfin。
- 安装 server 版本的 Jellyfin:在终端中输入命令
curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash
,安装过程非常顺利。
- 配置 Jellyfin:安装完成后,通过浏览器访问
http://192.168.10.100:8096
进入配置页面。在添加媒体库这里,我遇到了一个麻烦,网页只能选择到/home/bddxg
目录,无法继续往下选择到我的媒体库位置/home/bddxg/nas
。于是我向 deepseek 求助,它告诉我需要执行命令:sudo usermod -aG bddxg jellyfin
# 并且重启 Jellyfin 服务
sudo systemctl restart jellyfin
按照它的建议操作后,我刷新了网页,重新配置了 Jellyfin,终于可以正常添加媒体库了。
- 电视端播放:在电视上安装好 Jellyfin apk 客户端后,现在终于可以正常读取 Ubuntu 服务器上的影视资源了,坐在沙发上,享受着大屏观影的乐趣,这种感觉真的太棒了!
通过这次折腾,我成功地让闲置的 Ubuntu 服务器重新焕发生机,变成了一个功能强大的家庭影院。希望我的经验能够对大家有所帮助,也欢迎大家一起交流更多关于服务器利用和家庭影院搭建的经验。
[!WARNING] 令人遗憾的是,目前 jellyfin 似乎不支持
rmvb
格式的影片, 下载资源的时候注意影片格式,推荐直接下载mp4
格式的资源
本次使用到的软件名称和版本如下:
软件名 | 版本号 | 安装命令 |
---|---|---|
samba | Version 4.19.5-Ubuntu | sudo apt-get install samba samba-common |
jellyfin | Jellyfin.Server 10.10.6.0 | curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash |
ffmpeg(jellyfin 内自带) | ffmpeg version 7.0.2-Jellyfin | null |
来源:juejin.cn/post/7476614823883833382
Mybatis接口方法参数不加@Param,照样流畅取值
在 MyBatis 中,如果 Mapper 接口的方法有多个参数,但没有使用 @Param
注解,默认情况下,MyBatis 会将这些参数放入一个 Map
中,键名为 param1
、param2
等,或者使用索引 0
、1
等来访问。以下是具体的使用方法和注意事项。
一、Mapper 接口方法
假设有一个 Mapper 接口方法,包含多个参数但没有使用 @Param
注解:
public interface UserMapper {
User selectUserByNameAndAge(String name, int age);
}
二、XML 文件中的参数引用
在 XML 文件中,可以通过以下方式引用参数:
1. 使用 param1
、param2
等
MyBatis 会自动为参数生成键名 param1
、param2
等:
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
</select>
2. 使用索引 0
、1
等
也可以通过索引 0
、1
等来引用参数:
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{0} AND age = #{1}
</select>
三、注意事项
- 可读性问题:
- 使用
param1
、param2
或索引0
、1
的方式可读性较差,容易混淆。 - 建议使用
@Param
注解明确参数名称。
- 使用
- 参数顺序问题:
- 如果参数顺序发生变化,XML 文件中的引用也需要同步修改,容易出错。
- 推荐使用
@Param
注解:
- 使用
@Param
注解可以为参数指定名称,提高代码可读性和可维护性。
public interface UserMapper {
User selectUserByNameAndAge(@Param("name") String name, @Param("age") int age);
}
XML 文件:
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{name} AND age = #{age}
</select>
- 使用
四、示例代码
1. Mapper 接口
public interface UserMapper {
User selectUserByNameAndAge(String name, int age);
}
2. XML 文件
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
</select>
或者:
<select id="selectUserByNameAndAge" resultType="User">
SELECT * FROM user WHERE name = #{0} AND age = #{1}
</select>
3. 测试代码
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectUserByNameAndAge("John", 25);
System.out.println(user);
sqlSession.close();
- 如果 Mapper 接口方法有多个参数且没有使用
@Param
注解,可以通过param1
、param2
或索引0
、1
等方式引用参数。 - 这种方式可读性较差,容易出错,推荐使用
@Param
注解明确参数名称。 - 使用
@Param
注解后,XML 文件中的参数引用会更加清晰和易于维护。
来源:juejin.cn/post/7475643579781333029
Java web后端转Java游戏后端
作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程:
一、游戏后端核心职责
- 实时通信管理
- 采用WebSocket/TCP长连接(90%以上MMO游戏选择)
- 使用Netty/Mina框架处理高并发连接(单机支撑5W+连接是基本要求)
- 心跳机制设计(15-30秒间隔,检测断线)
- 游戏逻辑处理
- 战斗计算(需在50ms内完成复杂技能伤害计算)
- 状态同步(通过Delta同步优化带宽,减少60%数据传输量)
- 定时器管理(Quartz/时间轮算法处理活动开启等)
- 数据持久化
- Redis集群缓存热点数据(玩家属性缓存命中率需>95%)
- 分库分表设计(例如按玩家ID取模分128个库)
- 异步落库机制(使用Disruptor队列实现每秒10W+写入)
二、开发全流程实战(以MMORPG为例)
阶段1:预研设计(2-4周)
- 协议设计
// 使用Protobuf定义移动协议
message PlayerMove {
int32 player_id = 1;
Vector3 position = 2; // 三维坐标
float rotation = 3; // 朝向
int64 timestamp = 4; // 客户端时间戳
}
message BattleSkill {
int32 skill_id = 1;
repeated int32 target_ids = 2; // 多目标锁定
Coordinate cast_position = 3; // 技能释放位置
}
- 架构设计
graph TD
A[Gateway] --> B[BattleServer]
A --> C[SocialServer]
B --> D[RedisCluster]
C --> E[MySQLCluster]
F[MatchService] --> B
阶段2:核心系统开发(6-8周)
- 网络层实现
// Netty WebSocket处理器示例
@ChannelHandler.Sharable
public class GameServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) {
ProtocolMsg msg = ProtocolParser.parse(frame.text());
switch (msg.getType()) {
case MOVE:
handleMovement(ctx, (MoveMsg)msg);
break;
case SKILL_CAST:
validateSkillCooldown((SkillMsg)msg);
broadcastToAOI(ctx.channel(), msg);
break;
}
}
}
- AOI(Area of Interest)管理
- 九宫格算法实现视野同步
- 动态调整同步频率(近距离玩家100ms/次,远距离500ms/次)
- 战斗系统
- 采用确定性帧同步(Lockstep)
- 使用FixedPoint替代浮点数运算保证一致性
三、前后端协作关键点
- 协议版本控制
- 强制版本校验:每个消息头包含协议版本号
{
"ver": "1.2.3",
"cmd": 1001,
"data": {...}
}
- 调试工具链建设
- 开发GM指令系统:
/debug latency 200 // 模拟200ms延迟
/simulate 5000 // 生成5000个机器人
- 联调流程
- 使用Wireshark抓包分析时序问题
- Unity引擎侧实现协议回放功能
- 自动化测试覆盖率要求:
- 基础协议:100%
- 战斗用例:>85%
四、性能优化实践
- JVM层面
- G1GC参数优化:
-XX:+UseG1GC -XX:MaxGCPauseMillis=50
-XX:InitiatingHeapOccupancyPercent=35
- 网络优化
- 启用Snappy压缩协议(降低30%流量)
- 合并小包(Nagle算法+50ms合并窗口)
- 数据库优化
- 玩家数据冷热分离:
- 热数据:位置、状态(Redis)
- 冷数据:成就、日志(MySQL)
- 玩家数据冷热分离:
五、上线后运维
- 监控体系
- 关键指标报警阈值设置:
- 单服延迟:>200ms
- 消息队列积压:>1000
- CPU使用率:>70%持续5分钟
- 关键指标报警阈值设置:
- 紧急处理预案
- 自动扩容规则:
if conn_count > 40000:
spin_up_new_instance()
if qps > 5000:
enable_rate_limiter()
- 自动扩容规则:
六、常见问题解决方案
问题场景:战斗不同步
排查步骤:
- 对比客户端帧日志与服务端校验日志
- 检查确定性随机数种子一致性
- 验证物理引擎的FixedUpdate时序
问题场景:登录排队
优化方案:
- 令牌桶限流算法控制进入速度
- 预计等待时间动态计算:
wait_time = current_queue_size * avg_process_time / available_instances
通过以上流程,Java后端开发者可逐步掌握游戏开发特性,重点需要转变的思维模式包括:从请求响应模式到实时状态同步、从CRUD主导到复杂逻辑计算、从分钟级延迟到毫秒级响应的要求。建议从简单的棋牌类游戏入手,逐步过渡到大型实时游戏开发。
来源:juejin.cn/post/7475292103146684479
这个中国亲戚关系计算器让你告别“社死”
大家好,我是 Java陈序员
。
由于为了生活奔波,常年在外,导致很多关系稍疏远的亲戚之间来往并不多。
因此节假日回家时,往往会搞不清楚哪位亲戚应该喊什么称呼,很容易“社死”。
今天给大家介绍一个亲戚关系计算器,让你快速的计算出正确的亲戚称谓!
关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。
项目介绍
relationship
—— 中国亲戚关系计算器,只需简单的输入即可算出称谓。
输入框兼容了不同的叫法,你可以称呼父亲为:“老爸”、“爹地”、“老爷子”等等,方便不同地域的习惯叫法。
快捷输入按键,只需简单的点击即可完成关系输入,算法还支持逆向查找称呼哦~
功能特色:
- 使用别称查询:姥姥的爸爸的老窦 = 外曾外曾祖父
- 使用合称查询:姐夫的双亲 = 姊妹姻父 / 姊妹姻母
- 大小数字混合查询:大哥的二姑妈的七舅姥爷 = 舅曾外祖父
- 不限制祖辈孙辈跨度查询:舅妈的婆婆的外甥的姨妈的侄子 = 舅表舅父
- 根据年龄推导可能性:哥哥的表姐 = 姑表姐 / 舅表姐
- 根据语境确认性别:老婆的女儿的外婆 = 岳母
- 支持古文式表达:吾父之舅父 = 舅爷爷
- 解析某称谓关系链:七舅姥爷 = 妈妈的妈妈的兄弟
- 算两个亲戚间的合称关系:奶奶 + 外婆 = 儿女亲家
项目地址:
https://github.com/mumuy/relationship
在线体验:
https://passer-by.com/relationship/
移动端体验地址:
https://passer-by.com/relationship/vue/
功能体验
1、关系找称呼
2、称呼找关系
3、两者间关系
4、两者的合称
安装使用
1、直接引入安装
<script src="https://passer-by.com/relationship/dist/relationship.min.js">
获取全局方法 relationship
.
2、使用 npm 包管理安装
安装依赖:
npm install relationship.js
包引入:
// CommonJS 引入
const relationship = require("relationship.js");
// ES Module 引入
import relationship from 'relationship.js';
3、使用方法:唯一的计算方法 relationship
.
- 选项模式
relationship(options)
构造函数:
var options = {
text:'', // 目标对象:目标对象的称谓汉字表达,称谓间用‘的’字分隔
target:'', // 相对对象:相对对象的称谓汉字表达,称谓间用‘的’字分隔,空表示自己
sex:-1, // 本人性别:0表示女性,1表示男性
type:'default', // 转换类型:'default'计算称谓,'chain'计算关系链,'pair'计算关系合称
reverse:false, // 称呼方式:true对方称呼我,false我称呼对方
mode:'default', // 模式选择:使用setMode方法定制不同地区模式,在此选择自定义模式
optimal:false, // 最短关系:计算两者之间的最短关系
};
代码示例:
// 如:我应该叫外婆的哥哥什么?
relationship({text:'妈妈的妈妈的哥哥'});
// => ['舅外公']
// 如:七舅姥爷应该叫我什么?
relationship({text:'七舅姥爷',reverse:true,sex:1});
// => ['甥外孙']
// 如:舅公是什么亲戚
relationship({text:'舅公',type:'chain'});
// => ['爸爸的妈妈的兄弟', '妈妈的妈妈的兄弟', '老公的妈妈的兄弟']
// 如:舅妈如何称呼外婆?
relationship({text:'外婆',target:'舅妈',sex:1});
// => ['婆婆']
// 如:外婆和奶奶之间是什么关系?
relationship({text:'外婆',target:'奶奶',type:'pair'});
// => ['儿女亲家']
- 语句模式
relationship(exptession)
参数 exptession 句式可以为:xxx是xxx的什么人、xxx叫xxx什么、xxx如何称呼xxx等。
代码示例:
// 如:舅妈如何称呼外婆?
relationship('舅妈如何称呼外婆?');
// => ['婆婆']
// 如:外婆和奶奶之间是什么关系?
relationship('外婆和奶奶之间是什么关系?');
// => ['儿女亲家']
4、其他 API
// 获取当前数据表
relationship.data
// 获取当前数据量
relationship.dataCount
// 用户自定义模式
relationship.setMode(mode_name,mode_data)
最后
推荐的开源项目已经收录到 GitHub
项目,欢迎 Star
:
https://github.com/chenyl8848/great-open-source-project
或者访问网站,进行在线浏览:
https://chencoding.top:8090/#/
大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!
来源:juejin.cn/post/7344573753538330678
实现抖音 “视频无限滑动“效果
前言
在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅
不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"
这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动
的效果,干货满满
第一篇:200行代码实现类似Swiper.js的轮播组件
第三篇:Vue 路由使用介绍以及添加转场动画
第四篇:Vue 有条件路由缓存,就像传统新闻网站一样
第五篇:Github Actions 部署 Pages、同步到 Gitee、翻译 README 、 打包 docker 镜像
如果您对滑动原理不太熟悉,推荐先看我的这篇文章:200行代码实现类似Swiper.js的轮播组件
最终效果
在线预览:dy.ttentau.top/
Github地址:github.com/zyronon/dou…
实现原理
无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList
里面永远只有 N
个 SlideItem
,就要在滑动时不断的删除和增加 SlideItem
。
滑动时调整 SlideList
的偏移量 translateY
的值,以及列表里那几个 SlideItem
的 top
值,就可以了
为什么要调整 SlideList
的偏移量 translateY
的值同时还要调整 SlideItem
的 top
值呢?
因为 translateY
只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY
值就可以了,上滑了几页就减几页的高度,下滑同理
但是如果整个列表向前移动了一页,同时前面的 SlideItem
也少了一个,,那么最终效果就是移动了两页...因为 塌陷
了一页
这显然不是我们想要的,所以我们还需要同时调整 SlideItem
的 top
值,加上前面少的 SlideItem
的高度,这样才能显示出正常的内容
步骤
定义
virtualTotal
:页面中同时存在多少个 SlideItem
,默认为 5
。
//页面中同时存在多少个SlideItem
virtualTotal: {
type: Number,
default: () => 5
},
设置这个值可以让外部组件使用时传入,毕竟每个人的需求不同,有的要求同时存在 10
条,有的要求同时存在 5
条即可。
不过同时存在的数量越大,使用体验就越好,即使用户快速滑动,我们依然有时间处理。
如果只同时存在 5
条,用户只需要快速滑动两次就到底了(因为屏幕中显示第 3
条,刚开始除外),我们可能来不及添加新的视频到最后
render
:渲染函数,SlideItem
内显示什么由render
返回值决定
render: {
type: Function,
default: () => {
return null
}
},
之所以要设定这个值,是因为抖音首页可不只有视频,还有图集、推荐用户、广告等内容,所以我们不能写死显示视频。
最好是定义一个方法,外部去实现,我们内部去调用,拿到返回值,添加到 SlideList
中
list
:数据列表,外部传入
list: {
type: Array,
default: () => {
return []
}
},
我们从 list
中取出数据,然后调用并传给 render
函数,将其返回值插入到 SlideList中
初始化
watch(
() => props.list,
(newVal, oldVal) => {
//新数据长度比老数据长度小,说明是刷新
if (newVal.length < oldVal.length) {
//从list中取出数据,然后调用并传给render函数,将其返回值插入到SlideList中
insertContent()
} else {
//没数据就直接插入
if (oldVal.length === 0) {
insertContent()
} else {
// 走到这里,说明是通过接口加载了下一页的数据,
// 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验
// 这里额外加载3条数据。所以此刻,html里面有原本的5个加新增的3个,一共8个dom
// 用户往下滑动时只删除前面多余的dom,等滑动到临界值(virtualTotal/2+1)时,再去执行新增逻辑
}
}
}
)
用 watch
监听 list
是因为它一开始不一定有值,通过接口请求之后才有值
同时当我们下滑 加载更多
时,也会触发接口请求新的数据,用 watch
可以在有新数据时,多添加几条到 SlideList
的最后面,这样用户快速滑动也不怕了
如何滑动
这里就不再赘述,参考我的这篇文章:200行代码实现类似Swiper.js的轮播组件
滑动结束
判断滑动的方向
当我们向上滑动时,需要删除最前面的 dom
,然后在最后面添加一个 dom
下滑时反之
slideTouchEnd(e, state, canNext, (isNext) => {
if (props.list.length > props.virtualTotal) {
//手指往上滑(即列表展示下一条视频)
if (isNext) {
//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
} else {
//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
}
}
})
手指往上滑(即列表展示下一条视频)
- 首先判断是否要加载更多,快到列表末尾时就要加载更多数据了
- 再判断是否符合
腾挪
的条件,即当前位置要大于half
,且小于列表长度减half
。 - 在最后面添加一个
dom
- 删除最前面的
dom
- 将所有
dom
设置为最新的top
值(原因前面有讲,因为删除了最前面的dom
,导致塌陷一页,所以要加上删除dom
的高度)
let half = (props.virtualTotal - 1) / 2
//删除最前面的 `dom` ,然后在最后面添加一个 `dom`
if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {
emit('loadMore')
}
//是否符合 `腾挪` 的条件
if (state.localIndex > half && state.localIndex < props.list.length - half) {
//在最后面添加一个 `dom`
let addItemIndex = state.localIndex + half
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)
if (!res) {
slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))
}
//删除最前面的 `dom`
let index = slideListEl.value
.querySelector(`.${itemClassName}:first-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()
slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}
手指往下滑(即列表展示上一条视频)
逻辑和上滑都差不多,不过是反着来而已
- 再判断是否符合
腾挪
的条件,和上面反着 - 在最前面添加一个
dom
- 删除最后面的
dom
- 将所有
dom
设置为最新的top
值
//删除最后面的 `dom` ,然后在最前面添加一个 `dom`
if (state.localIndex >= half && state.localIndex < props.list.length - (half + 1)) {
let addIndex = state.localIndex - half
if (addIndex >= 0) {
let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addIndex}']`)
if (!res) {
slideListEl.value.prepend(getInsEl(props.list[addIndex], addIndex))
}
}
let index = slideListEl.value
.querySelector(`.${itemClassName}:last-child`)
.getAttribute('data-index')
appInsMap.get(Number(index)).unmount()
slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {
_css(item, 'top', (state.localIndex - half) * state.wrapper.height)
})
}
其他问题
为什么不直接用 v-for
直接生成 SlideItem
呢?
如果内容不是视频就可以。要删除或者新增时,直接操作 list
数据源,这样省事多了
如果内容是视频,修改 list
时,Vue
会快速的替换 dom
,正在播放的视频,突然一下从头开始播放了😅😅😅
如何获取 Vue
组件的最终 dom
有两种方式,各有利弊
- 用
Vue
的render
方法
- 优点:只是渲染一个
VNode
而已,理论上讲内存消耗更少。 - 缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅
- 优点:只是渲染一个
- 用
Vue
的createApp
方法再创建一个Vue
的实例
- 和上面相反😅
import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'
/**
* 获取Vue组件渲染之后的dom元素
* @param item
* @param index
* @param play
*/
function getInsEl(item, index, play = false) {
// console.log('index', cloneDeep(item), index, play)
let slideVNode = props.render(item, index, play, props.uniqueId)
const parent = document.createElement('div')
//TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面
if (import.meta.env.PROD) {
parent.classList.add('slide-item')
parent.setAttribute('data-index', index)
//将Vue组件渲染到一个div上
vueRender(slideVNode, parent)
appInsMap.set(index, {
unmount: () => {
vueRender(null, parent)
parent.remove()
}
})
return parent
} else {
//创建一个新的Vue实例,并挂载到一个div上
const app = createApp({
render() {
return <SlideItem data-index={index}>{slideVNode}</SlideItem>
}
})
const ins = app.mount(parent)
appInsMap.set(index, app)
return ins.$el
}
}
总结
原理其实并不难。主要是一开始可能会用 v-for
去弄,折腾半天发现不行。v-for
不行,就只能想想怎么把 Vue
组件搞到 html
里面去,又去研究如何获取 Vue
组件的最终 dom
,又查了半天资料,Vue
官方文档也不写,还得去翻 api
,麻了
结束
以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号 前端张余让,我会更新更多实用的前端知识与技巧,期待与你共同成长~
来源:juejin.cn/post/7361614921519054883
autohue.js:让你的图片和背景融为一体,绝了!
需求
先来看这样一个场景,拿一个网站举例
这里有一个常见的网站 banner 图容器,大小为为1910*560,看起来背景图完美的充满了宽度,但是图片原始大小时,却是:
它的宽度只有 1440,且 background-size 设置的是 contain ,即等比例缩放,那么可以断定它两边的蓝色是依靠背景色填充的。
那么问题来了,这是一个 轮播banner,如果希望添加一张不是蓝色的图片呢?难道要给每张图片提前标注好背景颜色吗?这显然是非常死板的做法。
所以需要从图片中提取到图片的主题色,当然这对于 js 来说,也不是什么难事,市面上已经有众多的开源库供我们使用。
探索
首先在网络上找到了以下几个库:
- color-thief 这是一款基于 JavaScript 和 Canvas 的工具,能够从图像中提取主要颜色或代表性的调色板
- vibrant.js 该插件是 Android 支持库中 Palette 类的 JavaScript 版本,可以从图像中提取突出的颜色
- rgbaster.js 这是一段小型脚本,可以获取图片的主色、次色等信息,方便实现一些精彩的 Web 交互效果
我取最轻量化的 rgbaster.js(此库非常搞笑,用TS编写,npm 包却没有指定 types) 来测试后发现,它给我在一个渐变色图片中,返回了七万多个色值,当然,它准确的提取出了面积最大的色值,但是这个色值不是图片边缘的颜色,导致设置为背景色后,并不能完美的融合。
另外的插件各位可以参考这几篇文章:
- 文章1:blog.csdn.net/weixin_4299…
- 文章2:juejin.cn/post/684490…
- 文章3:http://www.zhangxinxu.com/wordpress/2…
可以发现,这些插件主要功能就是取色,并没有考虑实际的应用场景,对于一个图片颜色分析工具来说,他们做的很到位,但是在大多数场景中,他们往往是不适用的。
在文章 2 中,作者对比了三款插件对于图片容器背景色的应用,看起来还是 rgbaster 效果好一点,但是我们刚刚也拿他试了,它并不能适用于颜色复杂度高的、渐变色的图片。
思考
既然又又又没有人做这件事,正所谓我不入地狱谁入地狱,我手写一个
整理一下需求,我发现我希望得到的是:
- 图片的主题色(面积占比最大)
- 次主题色(面积占比第二大)
- 合适的背景色(即图片边缘颜色,渐变时,需要边缘颜色来设置背景色)
这样一来,就已经可以覆盖大部分需求了,1+2 可以生成相关的 主题 TAG、主题背景,3 可以使留白的图片容器完美融合。
开搞
⚠⚠ 本小节内容非常硬核,如果不想深究原理可以直接跳过,文章末尾有用法和效果图 ⚠⚠
思路
首先需要避免上面提到的插件的缺点,即对渐变图片要做好处理,不能取出成千上万的颜色,体验太差且实用性不强,对于渐变色还有一点,即在渐变路径上,每一点的颜色都是不一样的,所以需要将他们以一个阈值分类,挑选出一众相近色,并计算出一个平均色,这样就不会导致主题色太精准进而没有代表性。
对于背景色,需要按情况分析,如果只是希望做一个协调的页面,那么大可以直接使用主题色做渐变过渡或蒙层,也就是类似于这种效果
但是如果希望背景与图片完美衔接,让人看不出图片边界的感觉,就需要单独对边缘颜色取色了。
最后一个问题,如果图片分辨率过大,在遍历像素点时会非常消耗性能,所以需要降低采样率,虽然会导致一些精度上的丢失,但是调整为一个合适的值后应该基本可用。
剩余的细节问题,我会在下面的代码中解释
使用 JaveScript 编码
接下来我将详细描述 autohue.js 的实现过程,由于本人对色彩科学
不甚了解,如有解释不到位或错误,还请指出。
首先编写一个入口主函数,我目前考虑到的参数应该有:
export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions)
type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }
interface autoColorPickerOptions {
/**
* - 降采样后的最大尺寸(默认 100px)
* - 降采样后的图片尺寸不会超过该值,可根据需求调整
* - 降采样后的图片尺寸越小,处理速度越快,但可能会影响颜色提取的准确性
**/
maxSize?: number
/**
* - Lab 距离阈值(默认 10)
* - 低于此值的颜色归为同一簇,建议 8~12
* - 值越大,颜色越容易被合并,提取的颜色越少
* - 值越小,颜色越容易被区分,提取的颜色越多
**/
threshold?: number | thresholdObj
}
概念解释 Lab ,全称:
CIE L*a*b
,CIE L*a*b*
是CIE XYZ
色彩模式的改进型。它的“L”(明亮度),“a”(绿色到红色)和“b”(蓝色到黄色)代表许多的值。与XYZ比较,CIE L*a*b*
的色彩更适合于人眼感觉的色彩,正所谓感知均匀
然后需要实现一个正常的 loadImg 方法,使用 canvas 异步加载图片
function loadImage(imageSource: HTMLImageElement | string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
let img: HTMLImageElement
if (typeof imageSource === 'string') {
img = new Image()
img.crossOrigin = 'Anonymous'
img.src = imageSource
} else {
img = imageSource
}
if (img.complete) {
resolve(img)
} else {
img.onload = () => resolve(img)
img.onerror = (err) => reject(err)
}
})
}
这样我们就获取到了图片对象。
然后为了图片过大,我们需要进行降采样处理
// 利用 Canvas 对图片进行降采样,返回 ImageData 对象
function getImageDataFromImage(img: HTMLImageElement, maxSize: number = 100): ImageData {
const canvas = document.createElement('canvas')
let width = img.naturalWidth
let height = img.naturalHeight
if (width > maxSize || height > maxSize) {
const scale = Math.min(maxSize / width, maxSize / height)
width = Math.floor(width * scale)
height = Math.floor(height * scale)
}
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('无法获取 Canvas 上下文')
}
ctx.drawImage(img, 0, 0, width, height)
return ctx.getImageData(0, 0, width, height)
}
概念解释,降采样:降采样(Downsampling)是指在图像处理中,通过减少数据的采样率或分辨率来降低数据量的过程。具体来说,就是在保持原始信息大致特征的情况下,减少数据的复杂度和存储需求。这里简单理解为将图片强制压缩为 100*100 以内,也是 canvas 压缩图片的常见做法。
得到图像信息后,就可以对图片进行像素遍历处理了,正如思考中提到的,我们需要对相近色提取并取平均色,并最终获取到主题色、次主题色。
那么问题来了,什么才算相近色,对于这个问题,在 常规的 rgb 中直接计算是不行的,因为它涉及到一个感知均匀的问题
概念解释,感知均匀:XYZ系统和在它的色度图上表示的两种颜色之间的距离与颜色观察者感知的变化不一致,这个问题叫做感知均匀性(perceptual uniformity)问题,也就是颜色之间数字上的差别与视觉感知不一致。由于我们需要在颜色簇中计算出平均色,那么对于人眼来说哪些颜色是相近的?此时,我们需要把 sRGB 转化为 Lab 色彩空间(感知均匀的),再计算其欧氏距离,在某一阈值内的颜色,即可认为是相近色。
所以我们首先需要将 rgb 转化为 Lab 色彩空间
// 将 sRGB 转换为 Lab 色彩空间
function rgbToLab(r: number, g: number, b: number): [number, number, number] {
let R = r / 255,
G = g / 255,
B = b / 255
R = R > 0.04045 ? Math.pow((R + 0.055) / 1.055, 2.4) : R / 12.92
G = G > 0.04045 ? Math.pow((G + 0.055) / 1.055, 2.4) : G / 12.92
B = B > 0.04045 ? Math.pow((B + 0.055) / 1.055, 2.4) : B / 12.92
let X = R * 0.4124 + G * 0.3576 + B * 0.1805
let Y = R * 0.2126 + G * 0.7152 + B * 0.0722
let Z = R * 0.0193 + G * 0.1192 + B * 0.9505
X = X / 0.95047
Y = Y / 1.0
Z = Z / 1.08883
const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)
const fx = f(X)
const fy = f(Y)
const fz = f(Z)
const L = 116 * fy - 16
const a = 500 * (fx - fy)
const bVal = 200 * (fy - fz)
return [L, a, bVal]
}
这个函数使用了看起来很复杂的算法,不必深究,这是它的大概解释:
- 获取到 rgb 参数
- 转化为线性 rgb(移除 gamma矫正),常量 0.04045 是sRGB(标准TGB)颜色空间中的一个阈值,用于区分非线性和线性的sRGB值,具体来说,当sRGB颜色分量大于0.04045时,需要通过 gamma 校正(即采用
((R + 0.055) / 1.055) ^ 2.4
)来得到线性RGB;如果小于等于0.04045,则直接进行线性转换(即R / 12.92
) - 线性RGB到XYZ空间的转换,转换公式如下:
X = R * 0.4124 + G * 0.3576 + B * 0.1805
Y = R * 0.2126 + G * 0.7152 + B * 0.0722
Z = R * 0.0193 + G * 0.1192 + B * 0.9505
- 归一化XYZ值,为了参考白点(D65),标准白点的XYZ值是
(0.95047, 1.0, 1.08883)
。所以需要通过除以这些常数来进行归一化 - XYZ到Lab的转换,公式函数:const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)
- 计算L, a, b 分量
L:亮度分量(表示颜色的明暗程度)
L = 116 * fy - 16
a:绿色到红色的色差分量
a = 500 * (fx - fy)
b:蓝色到黄色的色差分量
b = 200 * (fy - fz)
接下来实现聚类算法
/**
* 对满足条件的像素进行聚类
* @param imageData 图片像素数据
* @param condition 判断像素是否属于指定区域的条件函数(参数 x, y)
* @param threshold Lab 距离阈值,低于此值的颜色归为同一簇,建议 8~12
*/
function clusterPixelsByCondition(imageData: ImageData, condition: (x: number, y: number) => boolean, threshold: number = 10): Cluster[] {
const clusters: Cluster[] = []
const data = imageData.data
const width = imageData.width
const height = imageData.height
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (!condition(x, y)) continue
const index = (y * width + x) * 4
if (data[index + 3] === 0) continue // 忽略透明像素
const r = data[index]
const g = data[index + 1]
const b = data[index + 2]
const lab = rgbToLab(r, g, b)
let added = false
for (const cluster of clusters) {
const d = labDistance(lab, cluster.averageLab)
if (d < threshold) {
cluster.count++
cluster.sumRgb[0] += r
cluster.sumRgb[1] += g
cluster.sumRgb[2] += b
cluster.sumLab[0] += lab[0]
cluster.sumLab[1] += lab[1]
cluster.sumLab[2] += lab[2]
cluster.averageRgb = [cluster.sumRgb[0] / cluster.count, cluster.sumRgb[1] / cluster.count, cluster.sumRgb[2] / cluster.count]
cluster.averageLab = [cluster.sumLab[0] / cluster.count, cluster.sumLab[1] / cluster.count, cluster.sumLab[2] / cluster.count]
added = true
break
}
}
if (!added) {
clusters.push({
count: 1,
sumRgb: [r, g, b],
sumLab: [lab[0], lab[1], lab[2]],
averageRgb: [r, g, b],
averageLab: [lab[0], lab[1], lab[2]]
})
}
}
}
return clusters
}
函数内部有一个 labDistance 的调用,labDistance 是计算 Lab 颜色空间中的欧氏距离的
// 计算 Lab 空间的欧氏距离
function labDistance(lab1: [number, number, number], lab2: [number, number, number]): number {
const dL = lab1[0] - lab2[0]
const da = lab1[1] - lab2[1]
const db = lab1[2] - lab2[2]
return Math.sqrt(dL * dL + da * da + db * db)
}
概念解释,欧氏距离:Euclidean Distance,是一种在多维空间中测量两个点之间“直线”距离的方法。这种距离的计算基于欧几里得几何中两点之间的距离公式,通过计算两点在各个维度上的差的平方和,然后取平方根得到。欧氏距离是指n维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。
总的来说,这个函数采用了类似 K-means 的聚类方式,将小于用户传入阈值的颜色归为一簇,并取平均色(使用 Lab 值)。
概念解释,聚类算法:Clustering Algorithm 是一种无监督学习方法,其目的是将数据集中的元素分成不同的组(簇),使得同一组内的元素相似度较高,而不同组之间的元素相似度较低。这里是将相近色归为一簇。
概念解释,颜色簇:簇是聚类算法中一个常见的概念,可以大致理解为 "一类"
得到了颜色簇集合后,就可以按照count大小来判断哪个是主题色了
// 对全图所有像素进行聚类
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
clusters.sort((a, b) => b.count - a.count)
const primaryCluster = clusters[0]
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
const primaryColor = rgbToHex(primaryCluster.averageRgb)
const secondaryColor = rgbToHex(secondaryCluster.averageRgb)
现在我们已经获取到了主题色、次主题色 🎉🎉🎉
接下来,我们继续计算边缘颜色
按照同样的方法,只是把阈值设小一点,我这里直接设置为 1 (threshold.top 等都是1)
// 分别对上、右、下、左边缘进行聚类
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
topClusters.sort((a, b) => b.count - a.count)
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor
const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
bottomClusters.sort((a, b) => b.count - a.count)
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor
const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
leftClusters.sort((a, b) => b.count - a.count)
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor
const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
rightClusters.sort((a, b) => b.count - a.count)
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor
这样我们就获取到了上下左右四条边的颜色 🎉🎉🎉
这样大致的工作就完成了,最后我们将需要的属性导出给用户,我们的主函数最终长这样:
/**
* 主函数:根据图片自动提取颜色
* @param imageSource 图片 URL 或 HTMLImageElement
* @returns 返回包含主要颜色、次要颜色和背景色对象(上、右、下、左)的结果
*/
export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions): Promise<AutoHueResult> {
const { maxSize, threshold } = __handleAutoHueOptions(options)
const img = await loadImage(imageSource)
// 降采样(最大尺寸 100px,可根据需求调整)
const imageData = getImageDataFromImage(img, maxSize)
// 对全图所有像素进行聚类
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
clusters.sort((a, b) => b.count - a.count)
const primaryCluster = clusters[0]
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
const primaryColor = rgbToHex(primaryCluster.averageRgb)
const secondaryColor = rgbToHex(secondaryCluster.averageRgb)
// 定义边缘宽度(单位像素)
const margin = 10
const width = imageData.width
const height = imageData.height
// 分别对上、右、下、左边缘进行聚类
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
topClusters.sort((a, b) => b.count - a.count)
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor
const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
bottomClusters.sort((a, b) => b.count - a.count)
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor
const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
leftClusters.sort((a, b) => b.count - a.count)
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor
const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
rightClusters.sort((a, b) => b.count - a.count)
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor
return {
primaryColor,
secondaryColor,
backgroundColor: {
top: topColor,
right: rightColor,
bottom: bottomColor,
left: leftColor
}
}
}
还记得本小节一开始提到的参数吗,你可以自定义 maxSize(压缩大小,用于降采样)、threshold(阈值,用于设置簇大小)
为了用户友好,我还编写了 threshold 参数的可选类型:number | thresholdObj
type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }
可以单独设置主阈值、上下左右四边阈值,以适应更个性化的情况。
autohue.js 诞生了
名字的由来:秉承一贯命名习惯,auto 家族成员又多一个,与颜色有关的单词有好多个,我取了最短最好记的一个 hue(色相),也比较契合插件用途。
此插件已在 github 开源:GitHub autohue.js
npm 主页:NPM autohue.js
在线体验:autohue.js 官方首页
安装与使用
pnpm i autohue.js
import autohue from 'autohue.js'
autohue(url, {
threshold: {
primary: 10,
left: 1,
bottom: 12
},
maxSize: 50
})
.then((result) => {
// 使用 console.log 打印出色块元素s
console.log(`%c${result.primaryColor}`, 'color: #fff; background: ' + result.primaryColor, 'main')
console.log(`%c${result.secondaryColor}`, 'color: #fff; background: ' + result.secondaryColor, 'sub')
console.log(`%c${result.backgroundColor.left}`, 'color: #fff; background: ' + result.backgroundColor.left, 'bg-left')
console.log(`%c${result.backgroundColor.right}`, 'color: #fff; background: ' + result.backgroundColor.right, 'bg-right')
console.log(`%clinear-gradient to right`, 'color: #fff; background: linear-gradient(to right, ' + result.backgroundColor.left + ', ' + result.backgroundColor.right + ')', 'bg')
bg.value = `linear-gradient(to right, ${result.backgroundColor.left}, ${result.backgroundColor.right})`
})
.catch((err) => console.error(err))
最终效果
复杂边缘效果
纵向渐变效果(这里使用的是 left 和 right 边的值,可能使用 top 和 bottom 效果更佳)
纯色效果(因为单独对边缘采样,所以无论图片内容多复杂,纯色基本看不出边界)
突变边缘效果(此时用css做渐变蒙层应该效果会更好)
横向渐变效果(使用的是 left 和 right 的色值),基本看不出边界
参考资料
- zhuanlan.zhihu.com/p/370371059
- baike.baidu.com/item/%E5%9B…
- baike.baidu.com/item/%E6%A0…
- zh.wikipedia.org/wiki/%E6%AC…
- blog.csdn.net/weixin_4256…
- zh.wikipedia.org/wiki/K-%E5%…
- blog.csdn.net/weixin_4299…
- juejin.cn/post/684490…
番外
Auto 家族的其他成员
- Auto-Plugin/autofit.js autofit.js 迄今为止最易用的自适应工具
- Auto-Plugin/autolog.js autolog.js 轻量化小弹窗
- Auto-Plugin/autouno autouno 直觉的UnoCSS预设方案
- Auto-Plugin/autohue.js 本品 一个自动提取图片主题色让图片和背景融为一体的工具
来源:juejin.cn/post/7471919714292105270
记一次 CDN 流量被盗刷经历
先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。
600多G流量,100多万次请求。
怎么发现的
先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了。
抱着看热闹的心情点开阅读了。。。心想,看看自己的中招没,结果就真中招了 🍉。
被盗刷资源分析
笔者在 缤纷云
,七牛云
,又拍云
都有存放一些图片资源。本次中招的是 缤纷云
,下面是被刷的资源。
IP来源
查了几个 IP 和文章里描述的大差不差,都是来自山西联通的请求。
大小流量计算
按日志时间算的话,QPS 大概在 20 左右,单文件 632 K,1分钟大概就760MB ,1小时约 45G 左右。
看了几天前的日志,都是 1 小时刷 40G 就停下,从 9 点左右开始,刷到 12 点。
07-09 | 07-08 |
---|---|
![]() | ![]() |
但是 10 号的就变多了,60-70 GB 1次了。也是这天晚上才开始做的反制,不知道是不是加策略的时候影响到它计算流量大小了 😝。
反制手段
Referer 限制
通过观察这些资源的请求头,发现 Referer
和请求资源一致,通常情况下,不应该这样,应该是笔者的博客地址https://sugarat.top
。
于是第一次就限制了 Referer
头不能为空,同时将 cdn.bitiful.sugarat.top
的来源都拉黑。
这个办法还比较好使,后面的请求都给 403 了。
但这个还是临时解决方案,在 V 站上看到讨论,说资源是人为筛选的,意味着 Referer 换个资源还是会发生变化。
IP 限制
有 GitHub 仓库 unclemcz/ban-pcdn-ip 收集了此次恶意刷流量的 IP。
CDN 平台一般支持按 IP 或 IP 段屏蔽请求(虽然后者可能会屏蔽一些正常请求),可以将 IP 段配置到平台上,这样就能限制掉这些 IP 的请求。
缤纷云上这块限制还比较弱,我就直接把缤纷云的 CDN 直接关了,七牛云和又拍云上都加上了 IP 和 地域运营商的限制,等这阵风头过去再恢复。
七牛云 | 又拍云 |
---|---|
![]() | ![]() |
限速
限制单 IP 的QPS和峰值流量。
但是这个只能避免说让它刷得慢一点,还是不治本。
最后
用了CDN的话,日常还是多看看,能加阈值控制的平台优先加上,常规的访问控制防盗链的啥的安排上。
来源:juejin.cn/post/7390678994998526003
新来的总监,把闭包讲得那叫一个透彻
😃文章首发于公众号[精益码农]。
闭包作为前端面试的必考题目,常让1-3年工作经验的Javascripter感到困惑,我的主力语言C#/GO均有闭包。
1. 闭包:关键点在于函数是否捕获了其外部作用域的变量
闭包的形成: 定义函数时, 函数引用了其外部作用域的变量, 之后就形成了闭包。
闭包的结果: 引用的变量和定义的函数都会一同存在(即使已经脱离了函数定义/引用的变量的作用域),一直到闭包被消灭。
public static Action Closure()
{
var x = 1;
Action action= () =>
{
var y = 1;
var result = x + y;
Console.WriteLine(result);
x++;
};
return action;
}
public static void Main() {
var a=Closure();
a();
a();
}
// 调用函数输出
2
3
委托action是一个函数,它使用了“x”这个外部作用域的变量(x变量不是函数内局部变量),变量引用将被捕获形成闭包。
即使action被返回了(即使“x”已经脱离了它被引用时的作用域环境(Closure)),但是两次执行能输出2,3 说明它脱离原引用环境仍然能用。
当你在代码调试器(debugger)里观察“action”时,可以看到一个Target属性,里面封装了捕获的x变量:
实际上,委托,匿名函数和lambda都是继承自Delegate类,
Delegate不允许开发者直接使用,只有编译器才能使用, 也就是说delegate Action都是语法糖。
- Method:MethodInfo反射类型- 方法执行体
- Target:当前委托执行的对象,这些语法糖由编译器生成了继承自Delegate类型的对象,包含了捕获的自由变量。
再给一个反例:
public class Program
{
private static int x = 1; // 静态字段
public static void Main()
{
var action = NoClosure();
action();
action();
}
public static Action NoClosure(){
Action action=()=>{
var y =1;
var sum = x+y;
Console.WriteLine($"sum = { sum }");
x++;
};
return action;
}
}
x 是静态字段,在程序中有独立的存储区域, 不在线程的函数堆栈区,不属于某个特定的作用域。
匿名函数使用了 x,但没有捕获外部作用域的变量,因此不构成闭包, Target
属性对象无捕获的字段。
从编程设计的角度:闭包开创了除全局变量传值, 函数参数传值之外的第三种变量使用方式。
2. 闭包的形成时机和效果
闭包是词法闭包的简称,维基百科上是这样定义的:
“在计算机科学中,闭包是在词法环境中绑定自由变量的一等函数”。
闭包的形成时机:
- 一等函数
- 外部作用域变量
闭包的形态:
会捕获闭包函数内引用的外部作用域变量, 一直持有,直到闭包函数不再使用被销毁。
内部实现是形成了一个对象(包含执行函数和捕获的变量,参考Target对象), 只有形成堆内存,才有后续闭包销毁的行为,当闭包这个对象不再被引用时,闭包被GC清理。
闭包的作用周期:
离不开作用域这个概念,函数理所当然管控了函数内的局部变量作用域,但当它引用了外部有作用域的变量时, 就形成了闭包函数。
当闭包(例如一个委托或 lambda 表达式)不再被任何变量、对象或事件持有引用时,它就变成了“不可达”对象, 闭包被gc清理,其实就是堆内存被清理。
2.1 一等函数
一等函数很容易理解,就是在各语言, 函数被认为是某类数据类型, 定义函数就成了定义变量, 函数也可以像变量一样被传递。
很明显,在C#中我们常使用的匿名函数、lambda表达式都是一等函数。
Func<string,string> myFunc = delegate(string var1)
{
return "some value";
};
Func<string,string> myFunc = var1 => "some value";
string myVar = myFunc("something");
2.2 自由变量
在函数中被引用的外部作用域变量, 注意, 这个变量是外部有作用域的变量,也就说排除全局变量(这些变量在程序的独立区域, 不属于任何作用域)。
public void Test()
{
var myVar = "this is good";
Func<string,string> myFunc = delegate(string var1)
{
return var1 + myVar;
};
}
上面这个示例,myFunc形成了闭包,捕获了myVar这个外部作用域的变量;
即使Test函数返回了委托myFunc(脱离了定义myVar变量的作用域),闭包依然持有myVar的变量引用,
注意,引用变量,并不是使用当时变量的副本值。
我们再回过头来看结合了线程调度的闭包面试题。
3. 闭包函数关联线程调度: 依次打印连续的数字
static void Closure1()
{
for (int i = 0; i < 10; i++)
{
Task.Run(()=> Console.WriteLine(i));
}
}
每次输出数字不固定
并不是预期的 0.1.2.3.4.5.6.7.8.9
首先形成了闭包函数()=> Console.WriteLine(i)
, 捕获了外部有作用域变量i
的引用, 此处捕获的变量i相对于函数是全局变量。
但是Task调度闭包函数的时机不确定, 所以打印的是被调度时引用的变量i值。
数字符合但乱序:为每个闭包函数绑定独立变量
循环内增加局部变量, 解绑全局变量 (或者可以换成foreach,foreach相当于内部给你整了一个局部变量)。
能输出乱序的0,1,2,3,4,5,6,7,8,9
因为每次循环内产生的闭包函数捕获了对应的局部变量j,这样每个任务执行环境均独立维护了一个变量j, 这个j不是全局变量, 但是由于Task启动时机依然不确定,故是乱序。
数字符合且有序
核心是解决 Task调度问题。
思路是:一个共享变量,每个任务打印该变量自增的一个阶段,但是该自增不允许被打断。
public static void Main(string[] args)
{
var s =0;
var lo = new Program();
for (int i = 0; i < 10; i++)
{
Task.Run(()=>
{
lock(lo)
{
Console.WriteLine(s); // 依然形成了闭包函数, 之后闭包函数被线程调度
s++;
}
});
}
Thread.Sleep(2000);
} // 上面是一个明显的锁争用
3.Golang闭包的应用
gin 框架中中间件的默认形态是:
package middleware
func AuthenticationMiddleware(c *gin.Context) {
......
}
// Use方法的参数签名是这样: type HandlerFunc func(*Context), 不支持入参
router.Use(middleware.AuthenticationMiddleware)
实际实践上我们又需要给中间件传参, 闭包提供了这一能力。
func Authentication2Middleware(log *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
... 这里面可以利用log 参数。
}
}
var logger *zap.Logger
api.Use(middleware.Authentication2Middleware(logger))
总结
本文屏蔽语言差异,理清了[闭包]的概念核心: 函数引用了其外部作用域的变量,
核心特征:一等函数、自由变量,核心结果: 即使脱离了原捕获变量的原作用域,闭包函数依然持有该变量引用。
不仅能帮助我们应对多语种有关闭包的面试题, 也帮助我们了解[闭包]在通用语言中的设计初衷。
另外我们通过C# 调试器巩固了Delegate 抽象类,这是lambda表达式,委托,匿名函数的底层抽象数据结构类,包含两个重要属性 Method Target,分别表征了方法执行体、当前委托作用的对象,
可想而知,其他语言也是通过这个机制捕获闭包当中的自由变量。
来源:juejin.cn/post/7474982751365038106
Java利用Deepseek进行项目代码审查
一、为什么需要AI代码审查?
写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。

写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。
二、环境准备(5分钟搞定)
- 安装Deepseek插件(以VSCode为例):
- 插件市场搜索"Deepseek Code Review"
- 点击安装(就像安装手机APP一样简单)

- Java项目配置:
<dependency>
<groupId>com.deepseekgroupId>
<artifactId>code-analyzerartifactId>
<version>1.3.0version>
dependency>
- 安装Deepseek插件(以VSCode为例):
- 插件市场搜索"Deepseek Code Review"
- 点击安装(就像安装手机APP一样简单)
- Java项目配置:
<dependency>
<groupId>com.deepseekgroupId>
<artifactId>code-analyzerartifactId>
<version>1.3.0version>
dependency>
三、真实案例:用户管理系统漏洞检测
原始问题代码:
public class UserService {
// 漏洞1:未处理空指针
public String getUserRole(String userId) {
return UserDB.query(userId).getRole();
}
// 漏洞2:资源未关闭
public void exportUsers() {
FileOutputStream fos = new FileOutputStream("users.csv");
fos.write(getAllUsers().getBytes());
}
// 漏洞3:SQL注入风险
public void deleteUser(String input) {
Statement stmt = conn.createStatement();
stmt.execute("DELETE FROM users WHERE id = " + input);
}
}
public class UserService {
// 漏洞1:未处理空指针
public String getUserRole(String userId) {
return UserDB.query(userId).getRole();
}
// 漏洞2:资源未关闭
public void exportUsers() {
FileOutputStream fos = new FileOutputStream("users.csv");
fos.write(getAllUsers().getBytes());
}
// 漏洞3:SQL注入风险
public void deleteUser(String input) {
Statement stmt = conn.createStatement();
stmt.execute("DELETE FROM users WHERE id = " + input);
}
}
使用Deepseek审查后:
智能修复建议:
- 空指针防护 → 建议添加Optional处理
- 流资源 → 推荐try-with-resources语法
- SQL注入 → 提示改用PreparedStatement
- 空指针防护 → 建议添加Optional处理
- 流资源 → 推荐try-with-resources语法
- SQL注入 → 提示改用PreparedStatement
修正后的代码:
public class UserService {
// 修复1:Optional处理空指针
public String getUserRole(String userId) {
return Optional.ofNullable(UserDB.query(userId))
.map(User::getRole)
.orElse("guest");
}
// 修复2:自动资源管理
public void exportUsers() {
try (FileOutputStream fos = new FileOutputStream("users.csv")) {
fos.write(getAllUsers().getBytes());
}
}
// 修复3:预编译防注入
public void deleteUser(String input) {
PreparedStatement pstmt = conn.prepareStatement(
"DELETE FROM users WHERE id = ?");
pstmt.setString(1, input);
pstmt.executeUpdate();
}
}
public class UserService {
// 修复1:Optional处理空指针
public String getUserRole(String userId) {
return Optional.ofNullable(UserDB.query(userId))
.map(User::getRole)
.orElse("guest");
}
// 修复2:自动资源管理
public void exportUsers() {
try (FileOutputStream fos = new FileOutputStream("users.csv")) {
fos.write(getAllUsers().getBytes());
}
}
// 修复3:预编译防注入
public void deleteUser(String input) {
PreparedStatement pstmt = conn.prepareStatement(
"DELETE FROM users WHERE id = ?");
pstmt.setString(1, input);
pstmt.executeUpdate();
}
}
四、实现原理揭秘
Deepseek的代码审查就像"X光扫描仪",通过以下三步工作:
- 模式识别:比对数千万个代码样本
- 就像老师批改作业时发现常见错误
- 上下文理解:分析代码的"人际关系"
- 数据库连接有没有"成对出现"(打开/关闭)
- 敏感操作有没有"保镖"(权限校验)
- 智能推理:预测代码的"未来"
- 这个变量走到这里会不会变成null?
- 这个循环会不会变成"无限列车"?
五、进阶使用技巧
- 自定义审查规则(配置文件示例):
rules:
security:
sql_injection: error
performance:
loop_complexity: warning
style:
var_naming: info
- 自定义审查规则(配置文件示例):
rules:
security:
sql_injection: error
performance:
loop_complexity: warning
style:
var_naming: info
2. 与CI/CD集成(GitHub Action示例):
- name: Deepseek Code Review
uses: deepseek-ai/code-review-action@v2
with:
severity_level: warning
fail_on: error
六、开发者常见疑问
Q:AI会不会误判我的代码?
A:就像导航偶尔会绕路,Deepseek给出的是"建议"而非"判决",最终决策权在你手中
Q:处理历史遗留项目要多久?
A:10万行代码项目约需3-5分钟,支持增量扫描
七、效果对比数据
指标 | 人工审查 | Deepseek+人工 |
---|---|---|
平均耗时 | 4小时 | 30分钟 |
漏洞发现率 | 78% | 95% |
误报率 | 5% | 12% |
知识库更新速度 | 季度 | 实时 |
来源:juejin.cn/post/7473799336675639308
停止在TS中使用.d.ts文件
看到Matt Pocock 在 X 上的一个帖子提到不使用 .d.ts
文件的说法。
你赞同么?是否也应该把 .d.ts
文件都替换为 .ts
文件呢?
我们一起来看看~
.d.ts
文件的用途
首先,我们要澄清的是,.d.ts
文件并不是毫无用处的。
.d.ts
文件的用途主要用于为 JavaScript 代码提供类型描述。
.d.ts
文件是严格的蓝图,用于表示你的源代码可以使用的类型。最早可以追溯到2012年。其设计受到头文件、接口描述语言(IDL)、JSDoc 等实践的启发,可以被视为 TypeScript 版本的头文件。
.d.ts
文件只能包含声明,所以让我们通过一个代码示例来看看声明和实现之间的区别。假设我们有一个函数,它将两个数字相加:
// 声明 (.d.ts)
export function add(num1: number, num2: number): number;
// 实现 (.ts)
export function add(num1: number, num2: number): number {
return num1 + num2;
}
正如你所见,add
函数的实现实际上展示了加法的执行过程,并返回结果,而声明则没有。
那么 .d.ts
文件在实践中是如何使用的呢?
假设我们有一个 add
函数,分别在两个文件中存储声明和实现:add.d.ts
和 add.js
。
现在我们创建一个新文件 index.js
,它将实际使用 add
函数:
import { add } from "./x";
const result = add(1, 4);
console.log(result); // 输出:5
请注意,在这个 JS 文件中,add
函数具有类型安全性,因为函数在 add.d.ts
中被标注了类型声明。
替换方案 .ts
文件
我们已经了解了 .d.ts
文件的工作原理以及它们的用途。Matt 之所以认为不需要.d.ts
文件,是因为它也可以放在一个 .ts
文件中直接创建带有类型标注的实现。也就是说,拥有一个包含声明和实现的单个 add.ts
文件,等同于分别定义了 add.d.ts
和 add.js
文件。
这意味着你无需担心将声明文件与其对应的实现文件分开组织。
不过,针对类库,将 .d.ts
文件与编译后的 JavaScript 源代码一起使用,比存储 .ts
文件更高效,因为你真正需要的只是类型声明,以便用户在使用你的库时能够获得类型安全性。
这确实没错,需要强调的是,更推荐自动生成。通过更改 package.json
和 tsconfig.json
文件中的几个设置,从 .ts
文件自动生成 .d.ts
文件:
- tsconfig.json:确保添加
declaration: true
,以支持.d.ts
文件的生成。
{
"compilerOptions": {
"declaration": true,
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"strict": true
},
"include": ["src/**/*"]
}
- package.json:确保将
types
属性设置为生成的.d.ts
文件,该文件位于编译后的源代码旁边。
{
"name": "stop using d.ts",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc"
}
}
结论
.d.ts
文件中可以做到的一切,都可以在 .ts
文件中完成。
在 .ts
文件中使用 declare global {}
语法时,花括号内的内容将被视为全局环境声明,这本质上就是 .d.ts
文件的工作方式。
所以即使不使用.d.ts
文件,也可以拥有全局可访问的类型。.ts
文件在功能上既可以包含代码实现,又可以包含类型声明,而且还能实现全局的类型声明,从而在开发过程中可以更加方便地管理和使用类型,避免了在.d.ts
文件和.ts
文件之间进行复杂的协调和组织,提高了开发效率和开发体验。
另外需要注意的是,在大多数项目中,开发者会在 TypeScript 的配置文件(tsconfig.json)中将 skipLibCheck 选项设置为 true。skipLibCheck 的作用是跳过对库文件(包括 .d.ts 文件)的类型检查。当设置为 true 时,TypeScript 编译器不会对这些库文件进行严格的类型检查,从而加快编译速度。但这也会影响项目中自己编写的 .d.ts 文件。这意味着,即使 .d.ts 文件中定义的类型存在错误,TypeScript 编译器也不会报错,从而失去了类型安全性的保障。
而我们直接使用 .ts
文件,就不会有这个问题了,同事手动编写 .d.ts
文件,也会更加安全和高效。
因此,.d.ts
文件确实没有必要编写。在 99% 的情况下,.ts
文件更适合使用,可以改善开发体验,并降低库代码中出现类型错误的可能性。
怎么样??你同意他的看法么?
来源:juejin.cn/post/7463817822474682418
我们都被困在系统里
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
2020年外卖最火热的时候,有一篇文章《外卖骑手,困在系统里》。
作为一个互联网从业人员,我之前从未有机会体会到,当每一个工作都要被时间和算法压榨时,我会是一种怎样的感受。
而最近的一段经历,我感觉也被困在系统里了。
起因
如果你是一个研发人员,免不了要值班、处理线上问题。当然这都很正常,每个系统都有bug或者咨询类的问题。
由于我们面临的客户比较多,加上系统有一些易用性的问题或bug,提出来的问题不少。
公司有一项政策,当客服人员提交工单之后,系统对每一个单子有超时时间,如果超出一定时间你还未提交,罚款50元。
挺奇葩的,谁能保证1个小时就一定能排查出问题呢?
于是就会有一个场景,如果赶上问题多,一下子来5、6个工单,恰巧遇到不容易排查的耽误时间的话,处理后面的工单,都面临着超时的压力。
之前同事们对值班这件事,充满了怨言,大多都会吐槽几个点
- 系统bug太多了,又是刚刚某某需求改出来的问题
- 需求设计不合理,很多奇怪的操作导致了系统问题
- 客服太懒了,明明可以自己搜,非得提个工单问
- 基础设施差,平台不好用
我不太爱吐槽,但当工单一下子来的太多的时候,我不由自主的陷入机械的处理问题中,压缩思考的时间,只求不要超时就好。
明明系统有很多问题需要解决、流程也有很多可以优化,可当系统给到我们的压力越来越多时,我们便不去思考,陷入只有吐槽、怨言和避免罚款的状态。
当陷入了系统的支配,只能被动接受,甚至有了一些怨言的时候,我意识到,这样的状态,是有问题的。
被困住的打工人
外卖员为什么不遵守交通规则呢?
外卖小哥为了多赚钱、避免处罚,我之前也很不理解,为什么为了避免处罚,连自己的生命安全都可以置之不顾。
但转念一想,我们虽然不用在马路上奔波,可受到“系统”的压力,可是一点也不比外卖员少。
大家一定有过类似的经历:你骑车或者开车去上班,距离打卡时间所剩无几,你在迟到的边缘疯狂试探,可能多一个红绿灯,你就赶不上了,这时候你会不会狠踩几脚油门、闯一个黄灯,想要更快一点呢?
但随着裁员、降本增效、各类指标的压力越来越大,我们被迫不停的内卷,不断压榨自己,才能满足职场要求越来越严格的“算法”,比如,每半年一次的绩效考核,月度或者季度的OKR、KPI,还有处理不完的线上问题、事故,充斥在我们的脑海里面。
其实我们何尝不是“外卖员”呢?外卖员是为了不被扣钱,我们是为了年终奖、晋升罢了。
所以回过头来看,其实我们早早的就被困在“系统”中了,为了满足系统的要求,我们不得不埋头苦干,甚至加班透支身体,作出很多非常短线思维的事情。
但为什么,我之前从来没有过被困住的感觉,为什么我现在才回过神来,意识到这个问题呢?
我想,大概是越简单的事情,你作出的反应就越快、越激烈。而越复杂、时间越长的事情,你作出的反应就越缓慢,甚至忽略掉。
比如上班即将迟到的你,你会立刻意识到,迟到可能会受到处罚。但是年终评估你的绩效目标时,你或许只有在最后的几个月才会意识到,某某事情没完成,年终奖或许要少几个月而感到着急。
积极主动
最近正好在读《高效能人士的七个习惯》,其中第一个习惯就是积极主动。
书中说到:人性的本质是主动而非被动的,人类不仅能针对特定环境选择回应方式,更能主动创造有利的环境。
我们面对的问题可以分为三类:
- 可直接控制的(问题与自身的行为有关)
- 可间接控制的(问题与他人的行为有关)
- 无法控制的(我们无能为力的问题,例如我们的过去或现实的环境)
对于这三类问题,积极主动的话,应该如何加以解决呢。
可直接控制的问题
针对可直接控制的问题,可以通过培养正确习惯来解决。
从程序员角度来看,线上bug多,可以在开发前进行技术设计,上线前进行代码CR,自动化测试,帮助自己避免低级的问题。
面对处理工单时咨询量特别多的问题,随手整理个文档出来,放到大家都可以看到的地方。
可间接控制的
对于可间接控制的,我们可以通过改进施加影响的方法来解决。
比如流程机制的不合理,你可以通过向上反馈的方式施加影响,提出自己的建议而不是吐槽。
无法控制的
对于无法控制的,我们要做的就是改变面部曲线,以微笑、真诚与平和来接受现实。
虽然反馈问题的人或许能力参差不齐,导致工单量很多,但我们意识到这一点是无法避免的,不如一笑而过,这样才不至于被问题左右。
说在最后
好了,文章到这里就要结束了。
最近由于值班的原因,陷入了一段时间的无效忙碌中,每一天都很累,几乎抽不出时间来思考,所以更新的频率也降下来了。
但还好,及时的意识到问题,把最近的一点思考分享出来,希望我们每个人都不会被“系统”困住。
欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~
本篇文章是第41篇原创文章,2024目标进度41/100,欢迎有趣的你,关注我。
来源:juejin.cn/post/7385098943942656054
再见Typora,这款大小不到3M的Markdown编辑器,满足你的所有幻想!
Typora 是一款广受欢迎的 Markdown 编辑器,以其所见即所得的编辑模式和优雅的界面而闻名,长期以来是许多 Markdown 用户的首选。然而,从 2021 年起,Typora 不再免费,采用一次性付费授权模式。虽然费用不高,但对于轻量使用者或预算有限的用户可能并不友好。
今天来推荐一款开源替代品,一款更加轻量化、注重隐私且完全免费的 Markdown 编辑器,专为 macOS 用户开发。
项目介绍
MarkEdit 是一款轻量级且高效的 Markdown 编辑器,专为 macOS 用户设计,安装包大小不到 3 MB。它以简洁的设计和流畅的性能,成为技术写作、笔记记录、博客创作以及项目文档编辑的理想工具。无论是编写技术文档、撰写博客文章,还是编辑 README 文件,MarkEdit 都能以快速响应和便捷操作帮助用户专注于内容创作。
根据官方介绍,MarkEdit 免费的原因如下:
MarkEdit 是完全免费和开源的,没有任何广告或其他服务。我们之所以发布它,是因为我们喜欢它,我们不期望从中获得任何收入。
功能特性
MarkEdit 的核心功能围绕 Markdown 写作展开,注重实用与高效,以下是其主要特性:
- 实时语法高亮:清晰呈现 Markdown 的结构,让文档层次分明。
- 多种主题:提供不同的配色方案,总有一种适合你。
- 分屏实时预览:支持所见即所得的写作体验,左侧编辑,右侧实时渲染。
- 文件树视图:适合多文件项目管理,方便在项目间快速切换。
- 文档导出:支持将 Markdown 文件导出为 PDF 或 HTML 格式,方便分享和发布。
- CodeMirror 插件支持:通过插件扩展功能,满足更多 Markdown 使用需求。
- ......
MarkEdit 的特点让它能胜任多种写作场合:
- 技术文档:帮助开发者快速记录项目相关文档。
- 博客创作:支持实时预览,让博客排版更直观。
- 个人笔记:轻量且启动迅速,适合日常记录。
- 项目文档:文件管理功能让多文件项目的编辑更加高效。
效果展示
多种主题风格,总有一种适合你:
实时预览,让博客排版更直观:
设置界面,清晰直观:
安装方法
方法 1:安装包下载
找到 MarkEdit 的最新版本安装包下载使用即可,地址:github.com/MarkEdit-ap… 。
方法 2:通过 Homebrew
在终端中运行相关命令即可完成安装。
brew install markedit
注意:MarkEdit 支持 macOS Sonoma 和 macOS Sequoia, 历史兼容版本包括 macOS 12 和 macOS 13。
总结
MarkEdit 是一款专注于 Markdown 写作的 macOS 原生编辑器,以简洁、高效、隐私友好为核心设计理念。无论是日常写作还是处理复杂文档,它都能提供流畅的体验和强大的功能。对于追求高效写作的 macOS 用户来说,MarkEdit 是一个不可多得的优秀工具。
项目地址:github.com/MarkEdit-ap… 。
来源:juejin.cn/post/7456685819047919651
前端适配:你一般用哪种方案?
前言
最近在公司改bug,突然发现上一个前端留下的毛病不少,页面存在各种适配问题,为此甲方爸爸时常提出宝贵意见!
你的页面是不是时常是这样:
侧边栏未收缩时:
收缩后:
这样(缩小挤成一坨):
又或是这样:
那么废话不多说,今天由我不是程序猿kk为大家讲解一些前端必备知识:适配工作。
流式布局
学会利用相对单位(例如百分比,vh或是vw),而不是只会用px一类固定单位设计布局,前言中提到的收缩后多出一大块空白,就是由于写死了宽度,例如1000px或是89vw,那么当侧边栏进行收缩,右边内容宽度还是只有89个vw,因此我们可以将其更改为100%,这样不论侧边栏是否收缩,内容都会占满屏幕的全部。
.map {
width: 100%;
height: 90vh;
position: relative;
}
rem和第三方插件
什么是rem
rem
与em不同,rem会根据html的根节点字体大小进行变换,例如1rem就是一个字体大小那么大,比如根大小font size
为12px,那么1rem即12px,大家可以在网上寻找单位换算工具进行换算(从设计稿的px到rem)或是下载相关插件例如gulp-px3rem
,这样在不同分辨率,不同缩放比的电脑下都能够轻松应对了。
使用
第三方插件,例如做移动端适配的flexible.js,lib-flexible库
,其核心原理就是rem,我们需要做的就是根据不同屏幕计算出不同的fontsize,而页面中元素都是用rem做单位,据此实现了自适应
源码:
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial-scale=([d.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial-dpr=([d.]+)/);
var maximumDpr = content.match(/maximum-dpr=([d.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}
refreshRem();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}
})(window, window['lib'] || (window['lib'] = {}));
大家如果对相关原理感兴趣,可以阅读:flexible.js如何实现rem自适应-前端开发博客
在实际开发中应用场景不同效果不同,因此不能写死px。
在PC端适配我们可以自动转换rem适配方案(postcss-pxtorem、amfe-flexible),这里以vue3+vite为例子。事实上amfe-flexible是lib-flexible的升级版。
注意: 行内样式px不会转化为rem
npm install postcss postcss-pxtorem --save-dev // 我试过了反正我报错了,版本太高 大家可以指定5.1.1
npm install postcss-pxtorem@^5.1.1
npm i amfe-flexible --save
记得在main.js中引入amfe-flexible
import "amfe-flexible"
相关配置
媒体查询
通过查询不同的宽度来执行不同的css代码,最终以达到界面的配置。
在 CSS 中使用 @media
查询来检测屏幕宽度。当屏幕宽度小于 1024px 时,增加 margin-top
以向下移动表格。
.responsive-table {
transition: margin-top 0.3s; /* 添加过渡效果 */
}
@media (max-width: 1024px) {
.responsive-table {
margin-top: 200px; /* 向下移动的距离 */
}
}
弹性布局
创建一个响应式的卡片布局,当屏幕宽度减小时,卡片会自动换行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flexbox Example</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin: 0;
height: 100vh;
background-color: #f0f0f0;
}
.card-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 90%;
}
.card {
background-color: white;
border: 1px solid #ccc;
border-radius: 5px;
padding: 20px;
margin: 10px;
flex: 1 1 300px; /* 基于300px,允许增长和收缩 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.card:hover {
transform: translateY(-5px);
}
</style>
</head>
<body>
<div class="card-container">
<div class="card">Card 1</div>
<div class="card">Card 2</div>
<div class="card">Card 3</div>
<div class="card">Card 4</div>
<div class="card">Card 5</div>
</div>
</body>
</html>
小结
还是多提一嘴,应该不会有小伙伴把字体大小的单位也用rem吧?
来源:juejin.cn/post/7431999862919446539
独立开发:家人不支持怎么办?
大家好,我是农村程序员,独立开发者,前端之虎陈随易。
这是我的个人网站:chensuiyi.me,欢迎一起交朋友~
有很多人跟我聊到过这个问题:做独立开发,家人不支持怎么办?
。
在我交流沟通下,最终发现,这些人都走进了一个误区:独立开发者
等于 我要辞职全职做独立开发
。
请看我对独立开发者的分类:
- 业余独立开发。特点:
上班 + 下班的业余时间独立开发
。 - 兼职独立开发。特点:
不上班 + 没有充足的时间做独立开发
。 - 全职独立开发。特点:
不上班 + 有充足的时间做独立开发
。 - 混合独立开发。特点:
上班+兼职+没有充足的时间做独立开发
。
现在是不是一目了然了。
你可以根据自己当下的情况,特点,去选择做哪一种 独立开发
。
我们目前所看到的 全职独立开发
,只有极少数人可以做到。
这对于个人的内在要求,包括自律,坚持,执行力,产品力,都有着较高的要求。
同时呢,来自家人的态度和压力,也是 全职独立开发
的重要条件。
不要一开始,啥独立开发的经验都没有,就想做 全职独立开发
。
那么当你可以 理性地选择
适合自己当下情况的的独立开发方式后,你会发现,家人还会不支持吗?至少不会那么反对了。
所以这个问题的答案就是这么简单,只要看了我对独立开发的分类,你就明白了。
独立开发,本就是一个人的战斗,不要妄想这家人会支持你,他们最大的支持就是不反对。
我们遇到这样的问题时,不要觉得家人怎么怎么样,自己受到了多大的委屈和不理解一样。
他们的想法,是完全没有问题的。
人是社会动物,必然要考虑当下的生存问题,这是十分合理且正常的。
那么,如果上面的问题解决后,家人还是不支持,怎么办呢?
也很简单啊,自己偷摸摸继续折腾呗,难道一定要得到家人的支持,才能做独立开发吗?
《明朝那些事》
的作者,当年明月,赚了几千万家人才知道呢。
当然,我不是说你,也不是说我自己,可以赚几千万,我们可以定目标,但不能做梦。
总而言之就是说,做独立开发,要做好一个人长期战斗的准备。
因为你很有可能,很多年都无法比较稳定地每个月赚 5000 块钱,独立开发远没有我们想象的那么轻松。
如果你实在没有时间,没有干劲,没有激情做独立开发,那么不如其他方向,说不定能获得更好的回报。
独立开发是一个美好的梦,不稳定,也容易破碎。
那么我为什么一直在坚持做独立开发呢?因为我想让美梦成真。
来源:juejin.cn/post/7434366864866099234
制作一个页面时,需要兼容PC端和手机端,你是要分别做两个页面还是只做一个页面自适应?为什么?说说你的理由
在制作一个页面时,如何兼容PC端和手机端是一个重要的设计决策。这个决策通常有两个选择:分别制作两个页面,或者只制作一个自适应页面。以下是我选择后者的理由,以及如何实现自适应设计的相关内容。
选择自适应设计的理由
- 提高开发效率
制作一个自适应页面可以显著提高开发效率。开发者只需编写一次代码,就可以在不同设备上展示同样的内容,而不必为每个设备维护多个代码库。这意味着在后续的更新和维护中,相比于维护两个独立的页面,自适应页面的工作量会大幅减少。 - 一致的用户体验
用户在不同设备上访问同一网站时,能够获得一致的用户体验是极其重要的。自适应设计可以确保在不同屏幕尺寸和分辨率下,用户看到的内容和布局尽可能一致,避免用户在不同设备上获取不同信息的困惑。 - SEO优化
使用单一的自适应页面有助于SEO优化。搜索引擎更倾向于索引和排名内容一致的网站,而不是将相同内容分散在多个页面上。使用自适应设计可以集中流量,提高网站在搜索引擎中的权重。 - 成本效益
维护两个独立的页面不仅需要更多的时间和人力成本,还可能导致代码重复,增加出错的可能性。自适应设计能够通过一个代码库降低开发和维护成本。 - 响应式设计的灵活性
现代的 CSS 和 JavaScript 技术(例如 Flexbox 和 CSS Grid)使得创建自适应布局变得更加简单和灵活。媒体查询(Media Queries)可以帮助开发者根据设备的特性(如宽度、高度、分辨率)调整布局和样式,从而提供最佳的用户体验。
如何实现自适应设计
- 使用媒体查询
媒体查询是实现自适应设计的核心。它允许你根据设备的屏幕尺寸和特性应用不同的样式。例如:
/* 默认样式 */
.container {
width: 100%;
padding: 20px;
}
/* 针对手机的样式 */
@media (max-width: 600px) {
.container {
padding: 10px;
}
}
/* 针对平板的样式 */
@media (min-width: 601px) and (max-width: 900px) {
.container {
padding: 15px;
}
}
- 使用流式布局
使用流式布局可以使元素在不同的屏幕上根据可用空间自动调整大小。例如,使用百分比而不是固定像素值来设置宽度:
.box {
width: 50%; /* 宽度为父容器的一半 */
height: auto; /* 高度自动适应内容 */
}
- 灵活的图片和媒体
为了确保图片和视频在不同设备上显示良好,使用max-width: 100%
来确保媒体不会超出其容器的宽度:
img {
max-width: 100%;
height: auto; /* 保持图片的纵横比 */
}
- 测试和优化
在开发完成后,确保在不同设备和浏览器上进行测试。可以使用 Chrome 的开发者工具模拟不同的设备,以检查自适应设计的效果。同时,收集用户反馈并进行必要的优化。
总结
在制作兼容PC端和手机端的页面时,选择制作一个自适应页面是更为高效和经济的方案。它不仅能提高开发效率和维护成本,还能提供一致的用户体验,增强SEO效果。此外,现代的CSS技术使得实现自适应设计变得更加简单。因此,采用自适应设计是现代Web开发的最佳实践之一。
来源:juejin.cn/post/7476010111887949861
别让这6个UI设计雷区毁了你的APP!
一款成功的APP不仅仅取决于其功能性,更取决于用户体验,这其中,UI设计又至关重要。优秀的UI设计能够为用户带来直观、愉悦的交互体验,甚至让用户“一见钟情”,从而大大提高产品吸引力。
然而,有很多设计师在追求创新与美观的同时,往往会不经意间“踩雷”,忽略了一些普通用户更在意的问题和痛点。本文,笔者将谈谈UI设计中的常见误区,同时展示了优秀的UI设计例子,还分享一些行业内推荐的设计工具,希望能助你在设计的道路上更加得心应手~
UI设计常见误区
1、过度设计
设计大师 Dieter Rams 说过一句话:“好的设计是尽可能简单的设计。”
不过,说起来容易做起来难。产品经理和设计师总是不自觉地让产品过于复杂,今天想在产品中再添加一个功能,明天想在页面上再增加一个元素。
尤其是新手设计师,总希望通过增加视觉元素、动画效果和复杂的交互来提升用户体验。然而,这种做法往往适得其反,一个简洁、清晰且直观的界面一定比一个装饰过多、功能复杂的界面更能吸引用户。设计师应该专注于APP的核心功能,避免不必要的元素,确保设计美观又实用。
简约风接单APP界面
http://www.mockplus.cn/example/rp/…
2、忽视用户反馈
有时候,设计师可能过于相信自己的设计,忽略了用户的意见和反馈。这种情况下,即使设计再怎么创新或美观,如果不符合用户的实际需求和使用习惯,也难以获得用户的认可。
毕竟,UI设计不是艺术创作,用户反馈是设计迭代过程中的宝贵资源,它可以帮助设计师了解用户的真实体验,了解设计的不足,从而进行针对性的优化改进。
FARFETCH APP界面
http://www.mockplus.cn/example/rp/…
3、色彩搭配不合适
色彩搭配不合适是UI设计中一个常见的误区。有的设计师会自动倾向于选择那些自己喜欢的颜色,而不是基于用户的偏好和产品特征、背景等来选择颜色。有时,颜色的过度使用或不恰当的搭配会分散用户的注意力,甚至造成视觉疲劳。
另外,为了让用户能看清UI设计的各个方面,需要有足够的对比度。这就是为什么黑色文字配上白色背景效果那么好 —— 文字非常清晰,易于理解。
插画风APP界面
http://www.mockplus.cn/example/rp/…
4、忽略可访问性
对一个APP来说,所有用户,无论是有视觉障碍的人还是老年用户,都应该能够轻松地使用APP。设计如果不考虑这些用户群体的需要,会无意中排除一大部分潜在用户。
为了提高APP的可访问性,设计师应该考虑到字体大小、颜色对比度、语音读出功能等,确保所有用户都能舒适地使用。
社交类APP界面
http://www.mockplus.cn/example/rp/…
5、布局空滤不全面
有的产品界面过多,布局考虑不够全面几乎是常常发生的事。布局不合适有很多种情况,比如元素过于密集、排版杂乱、组件没有对齐、文本大小间距不合适等等。这种情况会导致信息展示不清晰或用户操作不便,非常影响用户的使用体验和效率。
一个美观舒适的布局应该合理利用空间,确保信息的层次清晰,充分考虑元素的大小和间距,让界面既清晰又易于操作,用户可以轻松地找到他们关心的内容。
想要避免这一误区的,在设计初期就需要考虑用户的需求和行为模式,不断迭代优化布局设计,找到最佳的解决方案。
加密货币钱包APP界面
http://www.mockplus.cn/example/rp/…
了解了上述UI设计的常见误区后,接下来是选择合适的设计工具来提升设计效率和质量。目前,市面上有很多优秀的UI设计工具,但以下几款因其强大的功能和良好的用户体验受到设计师的广泛推荐,一起来看看!
UI工具推荐
1、摹客 DT
摹客DT(http://www.mockplus.cn/dt)是一款国内很多UI设计师都爱用的工具,它提供了一整套完整的专业矢量编辑功能,丰富的图层样式选项,可以轻松创建高质量的App设计,并且还能在线实时协同,搞定团队资产管理难题不是问题。
主要功能点和亮点:
1)所有功能完全免费(参与它们的小活动还能永久免费使用),包含所有高级功能、导出能力等;
2)颜色、文本样式、图层样式都可以保存为资源,复用到其他设计元素;
3)支持导出SVG、JPG、PNG、webP、PDF格式文件,适配不同使用场景;
4)具有完善的团队协作和项目管理能力,改变传统设计流程,降低成本,提升效率。
**价格:**完全免费
**学习难度:**简单,新手上手无难度
**使用环境:**Web/客户端/Android/iOS
**推荐理由:**摹客DT,是一款更适合中国设计师的UI/UX设计工具。与一些老牌设计工具(Photoshop、XD等),它的学习门槛更低,上手更简单,社区和实时客户支持更迅速友好;作为Web端工具,摹客DT支持多人实时编辑,大大提高了团队设计效率。
推荐评级:⭐⭐⭐⭐⭐
2、Figma
Figma(http://www.figma.com/)是现在最流行的UI设…
主要功能点及亮点:
1)丰富的功能和工具:提供了全面的工具和功能来设计界面,包括矢量图形工具、网格和布局指导等。
2)设计组件和样式库:支持创建可复用的设计组件和样式库,节省时间和工作量。
3)插件生态系统:有丰富的插件生态系统,可以通过插件扩展功能,增加额外的设计工具和集成,以满足特定需求。
**价格:**提供免费版和付费版(12美元/月起)
**学习难度:**对新手相对友好,操作简单。
**使用环境:**Figma是基于Web的平台,通过浏览器即可使用。
推荐理由:
Figma设计功能强大,提供了丰富的插件和集成,还有有一个庞大的社区交流学习,是一款非常适合团队协作的工具。遗憾的是,Figma作为一款国外设计工具,在国内使用还是存在一定的劣势,比如网络访问限制、数据隐私和安全、本地化支持等,所以只能给到四星。
推荐评级:⭐⭐⭐⭐
3、Sketch
Sketch(http://www.sketch.com/)是一款专业的UI/U…
主要功能及亮点:
- 1)矢量编辑:直观的矢量编辑工具和可编辑的布尔运算,支持形状绘制、路径编辑、填充和描边等常见的矢量操作,使用户能够轻松创建和编辑各种设计元素。
- 2)设计规范库:设计师可以在sketch中把常用的颜色、图层样式存为规范,也可以将已经设计好的矢量图标存到规范库,在整个设计中复用它们,提高工作效率。
3)Sketch支持众多插件和第三方资源,使得其生态系统非常丰富,能够更好地满足设计师的需求。
**价格:**标准订阅 12/月/人(按月付费)
**使用环境:**macOS操作系统
推荐理由:
Sketch被广泛应用于移动应用、网页设计和用户界面设计等领域。它友好的界面、丰富的设计资源使设计师们能够快速创建出符合良好用户体验和视觉效果要求的设计。可惜的是,它只支持macOS系统,限制了部分用户使用。
**推荐评级:**⭐⭐⭐⭐
4、Adobe XD
Adobe XD(helpx.adobe.com/support/xd.…
主要功能及亮点:
1)丰富的设计和布局功能:设计师可以借助这些功能,轻松创建响应式设计,预览在不同设备上的效果,确保设计的一致性和灵活性。
2)丰富的交互动画和过渡效果:可以创建十分流畅的交互体验,用来更好地展示APP或网站的交互逻辑。
3)共享和协作:支持团队协作,用户可以轻松地共享设计文件,协同编辑,以及通过实时协作功能进行设计评审。
**价格:**提供免费试用,提供付费订阅 $9.99/月
**学习难度:**中
**使用环境:**Windows、macOS
**推荐理由:**Adobe XD与Adobe Creative Cloud紧密集成,十分方便同时使用Adobe其他软件的用户。不过有个问题是,Adobe xd已经官宣停止更新,只向老用户提供服务了,如果是之前没有使用Adobe XD的用户,最好选择其他产品。
**推荐评级:**⭐️⭐️⭐️
五、Principle
Principle是一款专门用于UI/UX设计的软件,它的设计理念是想要让设计师能够轻松创建出高质量、富有吸引力的产品界面效果。
主要功能及亮点:
1)高级动画控制:软件支持创建复杂的动画效果,包括过渡、转换和多状态动画。这对于展示复杂的交互过程和动态效果非常有用。
2)实时预览:Principle允许设计师实时预览他们的工作成果,这意味着可以即时查看和测试动画效果,确保设计的交互体验符合预期。
3)组件和重复使用:设计师可以创建可重复使用的组件,这大大提高了工作效率,特别是在处理具有相似元素或布局的多个界面时。
价格:$129
**学习难度:**中
**使用环境:**MacOS
推荐理由:
设计师通过Principle可以快速地制作出具有丰富交互的产品界面设计,并且它能和Sketch进行无缝链接,可以快速导入你在Sketch里已经做好的图,
推荐评级:⭐️⭐️⭐️⭐️
好的UI设计不仅仅是视觉上的享受,更是能让用户能够轻松、愉快地使用APP。避免上述UI设计误区,选择合适的设计工具,可以帮助我们设计出既美观又实用的产品。
希望本文章能为UI设计师/准UI设计师们有所帮助,创作出更多优秀的设计作品~
看到这里的读者朋友有福啦!本人吐血搜集整理,全网最全产品设计学习资料!
只要花1分钟填写**问卷**就能免费领取以下超值礼包:
1、产品经理必读的100本书 包含:产品思维、大厂案例、技能提升、数据分析、项目管理等各类经典书籍,从产品入门到精通一网打尽! 2、UI/UX设计师必读的115本书 包含:UI/UX设计、平面设计、版式设计、设计心理学、设计思维等各类经典书籍,看完你就是设计大神! 3、30G互联网人知识礼包 包含:
- 10GPM礼包,产品案例、大咖分享、行业报告等应有尽有
- 10GUI/UE资源,优秀设计案例、资料包、源文件免费领
- 5G运营资料包,超全产品、电商、新媒体、活动等运营技能
- 5G职场/营销资料包,包含产品设计求职面试、营销增长等
4、50G热门流行的AI学习大礼包
包含:AI绘画、AIGC精选课程、AI职场实用教程等
5、30G职场必备技能包
包含:精选PPT模板、免费可商用字体包、各岗位能力模型、Excel学习资料、求职面试、升职加薪、职场写作和沟通等实用资源。
礼包资源持续更新,互联网行业知识一网打尽!礼包领取地址:
来源:juejin.cn/post/7356535808931627046
后端:没空,先自己 mock 去
前言
后端开发忙,不给你接口?
后端抱怨你在测试过程中,频繁的给脏数据?
后端修个接口很慢没法测试?

有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!
后端开发忙,不给你接口?
后端抱怨你在测试过程中,频繁的给脏数据?
后端修个接口很慢没法测试?
有了 mockjs ,这些问题将迎刃而解。不要 998,pnpm i 带回家!
真这么丝滑?
请看我的使用方式:
当后端接口无法满足要求,且不能及时更改时。例如后端返回
{
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
}
}
],
}
但我此时希望增加一个 user_type
来确定页面的展示。
那我就直接起一个文件:user.js
,把刚才的响应 copy 过来,并追加改动
myMock('/api/v1/user', 'post', () => {
return {
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
"user_type": "admin",
}
}
],
}
});
如此一来,这个请求就被无缝替换为了我们的 mock,可以随便测试了。
请看我的使用方式:
当后端接口无法满足要求,且不能及时更改时。例如后端返回
{
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
}
}
],
}
但我此时希望增加一个 user_type
来确定页面的展示。
那我就直接起一个文件:user.js
,把刚才的响应 copy 过来,并追加改动
myMock('/api/v1/user', 'post', () => {
return {
"err_no": 0,
"err_msg": "success",
"data": [
{
"comment_id": "7337487924836287242",
"user_info": {
"user_name": "陈陈陈_",
"user_type": "admin",
}
}
],
}
});
如此一来,这个请求就被无缝替换为了我们的 mock,可以随便测试了。
如何接入 mockjs
有的同学就要问了,主播主播,你的 mockjs 确实很方便,怎么接入比较好呀。别急,我们一步一步来
- 安装 mockjs
pnpm i mockjs
如果是使用 ts 的同学,可能需要额外安装 type 类型包:@types/mockjs
- 新建一个 mock 文件夹,在 mock/index.ts 放入基本路径
// 各种 mock 的文件,视条件而定,我这里有俩文件就引入了俩
import './login/user.js';
import './model/model.js';
有的同学就要问了,主播主播,你的 mockjs 确实很方便,怎么接入比较好呀。别急,我们一步一步来
- 安装 mockjs
pnpm i mockjs
如果是使用 ts 的同学,可能需要额外安装 type 类型包:@types/mockjs
- 新建一个 mock 文件夹,在 mock/index.ts 放入基本路径
// 各种 mock 的文件,视条件而定,我这里有俩文件就引入了俩
import './login/user.js';
import './model/model.js';
并且在你的项目入口 ts 中引入 mock/index.ts
import './mock/index'; // 引入 mock 配置
- 导出一个 myMock 方法,并追加一个 baseUrl 方便直接联动你的 axios
import { ENV_TEST } from '@/api/config/interceptor';
import Mock from 'mockjs';
export const myMock = (
path: string,
method: 'get' | 'post',
callback: (options: any) => any
) => {
Mock.mock(`${ENV_TEST}${path}`, method, callback);
};
如此一来,你就可以在 mock 文件夹下去搞了,比如:
我想新增一个服务模块的各类接口的 mock,那么我就新增一个 service 文件夹,在其下增加一个 index.ts,并对对应路径进行 mock
myMock('/api/v1/service', 'get', () => {
return {
code: 0,
msg: 'hello service',
data: null,
};
});
另外,别忘了在 mock/index.ts 引入文件
不显示在 network 中?
需要说明的是,这样走 mock 是不会触发真正的请求的,相当于 xhr 直接被 mock 拦截了下来并给了你返回值。所以你无法在 network 中看到你的请求。
这是个痛点,目前比较好的解决方案还是起一个单独的服务来 mock。但这样也就意味着,需要另起一个项目来单独做 mock,太不优雅了。
有没有什么办法,既可以走上述简单的mock,又可以在需要的时候起一个服务来查看 network,并且不需要额外维护两套配置呢?
有的兄弟,有的。
import express from 'express';
import bodyParser from 'body-parser';
import Mock from 'mockjs';
import './login/user.js';
import './model/model.js';
import { ENV_TEST } from './utils/index.js';
const app = express();
const port = 3010;
// 使用中间件处理请求体和CORS
app.use(bodyParser.json());
// 设置CORS头部
app.use(( _ , res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
// 设置Mock路由的函数
const setupMockRoutes = () => {
const mockApis = Mock._mocked || {};
// 遍历每个Mock API,并生成对应的路由
Object.keys(mockApis).forEach((key) => {
const { rurl, rtype, template } = mockApis[key];
const route = rurl.replace(ENV_TEST, ''); // 去掉环境前缀
// 根据请求类型(GET, POST, 等)设置路由
app[rtype.toLowerCase()](route, (req, res) => {
const data =
typeof template === 'function' ? template(req.body || {}) : template;
res.json(Mock.mock(data)); // 返回模拟数据
});
});
};
// 设置Mock API路由
setupMockRoutes();
// 启动服务器
app.listen(port, () => {
process.env.NODE_ENV = 'mock'; // 设置环境变量
console.log(`Mock 服务已启动,访问地址: http://localhost:${port}`);
});
直接在 mock 文件夹下追加这个启动文件,当你需要看 network 的时候,将环境切换为 mock 环境即可。本质是利用了 Mock._mocked
可以拿到所有注册项,并用 express 起了一个后端服务响应这些注册项来实现的。
在拥有了这个能力的基础上,我们就可以调整我们的命令
"scripts": {
"dev": "cross-env NODE_ENV=test vite",
"mock": "cross-env NODE_ENV=mock vite & node ./src/mock/app.js"
},
顺便贴一下我的 env 配置:
export const ENV_TEST = 'https://api-ai.com/fuxi';
export const ENV_MOCK = 'http://localhost:3010/';
let baseURL: string = ENV_TEST;
console.log('目前环境为:' + process.env.NODE_ENV);
switch (process.env.NODE_ENV) {
case 'mock':
baseURL = ENV_MOCK;
break;
case 'test':
baseURL = ENV_TEST;
break;
case 'production':
break;
default:
baseURL = ENV_TEST;
break;
}
export { baseURL };
这样一来,如果你需要看 network ,就 pnpm mock,如果不需要,就直接 pnpm dev,完全不需要其他心智负担。
三个字:
参数相关
具体的 api 可查阅:github.com/nuysoft/Moc… 相关的文章也非常多,就不展开说明了。
如果这篇文章对你有帮助,不妨点个赞吧~
来源:juejin.cn/post/7460091261762125865
前端哪有什么设计模式
前言
- 常网IT源码上线啦!
- 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
- 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
- 接下来想分享一些自己在项目中遇到的技术选型以及问题场景。
你生命的前半辈子或许属于别人,活在别人的认为里。那把后半辈子还给你自己,去追随你内在的声音。
一、前言
之前在讨论设计模式、算法的时候,一个后端组长冷嘲热讽的说:前端哪有什么设计模式、算法,就好像只有后端语言有一样,至今还记得那不屑的眼神。
今天想起来,就随便列几个,给这位眼里前端无设计模式的人,睁眼看世界。
二、观察者模式 (Observer Pattern)
观察者模式的核心是当数据发生变化时,自动通知并更新相关的视图。在 Vue 中,这通过其响应式系统实现。
Vue 2.x:Object.defineProperty
在 Vue 2.x 中,响应式系统是通过 Object.defineProperty
实现的。每当访问某个对象的属性时,getter
会被触发;当设置属性时,setter
会触发,从而实现数据更新时视图的重新渲染。
源码(简化版):
function defineReactive(obj, key, val) {
// 创建一个 dep 实例,用于收集依赖
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 当访问属性时,触发 getter,并把当前 watcher 依赖收集到 dep 中
if (Dep.target) {
dep.addDep(Dep.target);
}
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify(); // 数据更新时,通知所有依赖重新渲染
}
}
});
}
Dep
类:它管理依赖,addDep
用于添加依赖,notify
用于通知所有依赖更新。
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach(dep => dep.update());
}
}
- 依赖收集:当 Vue 组件渲染时,会创建一个
watcher
对象,表示一个视图的更新需求。当视图渲染过程中访问数据时,getter
会触发,并将watcher
添加到dep
的依赖列表中。
Vue 3.x:Proxy
Vue 3.x 使用了 Proxy
来替代 Object.defineProperty
,从而实现了更高效的响应式机制,支持深度代理。
源码(简化版):
function reactive(target) {
const handler = {
get(target, key) {
// 依赖收集:当访问某个属性时,触发 getter,收集依赖
track(target, key);
return target[key];
},
set(target, key, value) {
// 数据更新时,通知相关的视图更新
target[key] = value;
trigger(target, key);
return true;
}
};
return new Proxy(target, handler);
}
track
:收集依赖,确保只有相关组件更新。trigger
:当数据发生变化时,通知所有依赖重新渲染。
三、发布/订阅模式 (Publish/Subscribe Pattern)
发布/订阅模式通过中央事件总线(Event Bus)实现不同组件间的解耦,Vue 2.x 中,组件间的通信就是基于这种模式实现的。
Vue 2.x:事件总线(Event Bus)
事件总线就是一个中央的事件处理器,Vue 实例可以充当事件总线,用来处理不同组件之间的消息传递。
// 创建一个 Vue 实例作为事件总线
const EventBus = new Vue();
// 组件 A 发布事件
EventBus.$emit('message', 'Hello from A');
// 组件 B 订阅事件
EventBus.$on('message', (msg) => {
console.log(msg); // 输出 'Hello from A'
});
$emit
:用于发布事件。$on
:用于订阅事件。$off
:用于取消订阅事件。
四、工厂模式 (Factory Pattern)
工厂模式通过一个函数生成对象或实例,Vue 的组件化机制和动态组件加载就是通过工厂模式来实现的。
Vue 的 render
函数和 functional
组件支持动态生成组件实例。例如,functional
组件本质上是一个工厂函数,通过给定的 props
返回一个 VNode。
Vue.component('dynamic-component', {
functional: true,
render(h, context) {
// 工厂模式:根据传入的 props 创建不同的 VNode
return h(context.props.type);
}
});
functional
组件:它没有实例,所有的逻辑都是在render
函数中处理,返回的 VNode 就是组件的“产物”。
五、单例模式 (Singleton Pattern)
单例模式确保某个类只有一个实例,Vue 实例就是全局唯一的。
在 Vue 中,全局的 Vue
构造函数本身就是一个单例对象,通常只会创建一个 Vue 实例,用于管理应用的生命周期和全局配置。
const app = new Vue({
data: {
message: 'Hello, Vue!'
}
});
- 单例保证:整个应用只有一个 Vue 实例,所有全局的配置(如
Vue.config
)都是共享的。
六、模板方法模式 (Template Method Pattern)
模板方法模式定义了一个操作中的算法框架,而将一些步骤延迟到子类中。Vue 的生命周期钩子就是一个模板方法模式的实现。
Vue 定义了一系列生命周期钩子(如 created
、mounted
、updated
等),它们实现了组件从创建到销毁的完整过程。开发者可以在这些钩子中插入自定义逻辑。
Vue.component('my-component', {
data() {
return {
message: 'Hello, 泽!'
};
},
created() {
console.log('Component created');
},
mounted() {
console.log('Component mounted');
},
template: '<div>{{ message }}</div>'
});
Vue 组件的生命周期钩子实现了模板方法模式的核心思想,开发者可以根据需要重写生命周期钩子,而 Vue 保证生命周期的流程和框架。
七、策略模式 (Strategy Pattern)
策略模式通过定义一系列算法,将它们封装起来,使它们可以相互替换。Vue 的 计算属性(computed) 和 方法(methods) 可以看作是策略模式的应用。
计算属性允许我们定义动态的属性,其值是基于其他属性的计算结果。Vue 会根据依赖关系缓存计算结果,只有在依赖的属性发生变化时,计算属性才会重新计算。
new Vue({
data() {
return {
num1: 10,
num2: 20
};
},
computed: {
sum() {
return this.num1 + this.num2;
}
}
});
八、装饰器模式 (Decorator Pattern)
装饰器模式允许动态地给对象添加功能,而无需改变其结构。在 Vue 中,指令就是一种装饰器模式的应用,它通过指令来动态地改变元素的行为。
<div v-bind:class="className"></div>
<div v-if="isVisible">谁的疯太谍</div>
这些指令动态地修改 DOM 元素的行为,类似于装饰器在不修改对象结构的情况下,动态地增强其功能。
九、代理模式 (Proxy Pattern)
代理模式通过创建一个代理对象来控制对目标对象的访问。在 Vue 3.x 中,响应式系统就是通过 Proxy
来代理对象的访问。
vue3
const state = reactive({
count: 0
});
state.count++; // 会触发依赖更新
reactive:使用 Proxy 对对象进行代理,当对象的属性被访问或修改时,都会触发代理器的 get 和 set 操作。
function reactive(target) {
const handler = {
get(target, key) {
// 依赖收集:当访问某个属性时,触发 getter,收集依赖
track(target, key);
return target[key];
},
set(target, key, value) {
// 数据更新时,触发依赖更新
target[key] = value;
trigger(target, key);
return true;
}
};
return new Proxy(target, handler);
}
track
:当读取目标对象的属性时,收集依赖,这通常涉及到将当前的watcher
加入到依赖列表中。trigger
:当对象的属性发生改变时,通知所有相关的依赖(如组件)更新。
这个 Proxy
机制使得 Vue 可以动态地观察和更新对象的变化,比 Object.defineProperty
更具灵活性。
十、适配器模式 (Adapter Pattern)
适配器模式用于将一个类的接口转换成客户端期望的另一个接口,使得原本不兼容的接口可以一起工作。Vue 的插槽(Slots)和组件的跨平台支持某种程度上借用了适配器模式的思想。
Vue 插槽机制
Vue 的插槽机制是通过提供一个适配层,将父组件传入的内容插入到子组件的指定位置。开发者可以使用具名插槽、作用域插槽等方式,实现灵活的插槽传递。
<template>
<child-component>
<template #header>
<h1>This is the header</h1>
</template>
<p>This is the default content</p>
</child-component>
</template>
父组件通过 #header
插槽插入了一个标题内容,而 child-component
会将其插入到适当的位置。这里,插槽充当了一个适配器,允许父组件插入的内容与子组件的内容结构灵活匹配。
十全十美
至此撒花~
后记
我相信技术不分界,不深入了解,就不要轻易断言。
一个圆,有了一个缺口,不知道的东西就更多了。
但是没有缺口,不知道的东西就少了。
这也就是为什么,知道得越多,不知道的就越多。
谢谢!
最后,祝君能拿下满意的offer。
我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车
👍 如果对您有帮助,您的点赞是我前进的润滑剂。
以往推荐
原文链接
来源:juejin.cn/post/7444215159289102347
再见 XShell!一款万能通用的终端工具,用完爱不释手!
作为一名后端开发,我们经常需要使用终端工具来管理Linux服务器。最近发现一款比Xshell更好用终端工具XPipe,能支持SSH、Docker、K8S等多种环境,还具有强大的文件管理工具,分享给大家!
XPipe简介
XPipe是一款全新的终端管理工具,具有强大的文件管理功能,目前在Github上已有4.8k+Star
。它可以基于你本地安装的命令行工具(例如PowerShell)来执行远程命令,反应速度非常快。如果你有使用 ssh、docker、kubectl 等命令行工具来管理服务器的需求,使用它就可以了。
XPipe具有如下特性:
- 连接中心:能轻松实现所有类型的远程连接,支持SSH、Docker、Podman、Kubernetes、Powershell等环境。
- 强大的文件管理功能:具有对远程系统专门优化的文件管理功能。
- 多种命令行环境支持:包括bash、zsh、cmd、PowerShell等。
- 多功能脚本系统:可以方便地管理可重用脚本。
- 密码保险箱:所有远程连接账户均完全存储于您本地系统中的一个加密安全的存储库中。
下面是XPipe使用过程中的截图,界面还是挺炫酷的!
这或许是一个对你有用的开源项目,mall项目是一套基于
SpringBoot3
+ Vue 的电商系统(Github标星60K),后端支持多模块和2024最新微服务架构
,采用Docker和K8S部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!
- Boot项目:github.com/macrozheng/…
- Cloud项目:github.com/macrozheng/…
- 教程网站:http://www.macrozheng.com
项目演示:
使用
- 首先去XPipe的Release页面下载它的安装包,我这里下载的是
Portable
版本,解压即可使用,地址:github.com/xpipe-io/xp…
- 下载完成后进行解压,解压后双击
xpiped.exe
即可使用;
- 这里我们先进行一些设置,将语言设置成
中文
,然后设置下主题,个人比较喜欢黑色主题;
- 接下来新建一个SSH连接,输入服务器地址后,选择
添加预定义身份
;
- 这个预定义身份相当于一个可重用的Linux访问账户;
- 然后输入连接名称,点击完成即可创建连接;
- 我们可以发现XPipe能自动发现服务器器上的Docker环境并创建连接选项,如果你安装了K8S环境的话,也是可以发现到的;
- 然后我们单击下
Linux-local
这个连接,就可以通过本地命令行工具来管理Linux服务器了;
- 如果你想连接到某个Docker容器的话,直接点击对应容器即可连接,这里以mysql为例;
- 选中左侧远程服务器,点击右侧的
文件浏览器
按钮可以直接管理远程服务器上的文件,非常方便;
- 在
所有脚本
功能中,可以存储我们的可重用脚本;
- 在
所有身份
中存储着我们的账号密码,之前创建的Linux root账户在这里可以进行修改。
总结
今天给大家分享了一款好用的终端工具XPipe,界面炫酷功能强大,它的文件管理功能确实惊艳到我了。而且它可以用本地命令行工具来执行SSH命令,对比一些套壳的跨平台终端工具,反应速度还是非常快的!
项目地址
来源:juejin.cn/post/7475662844789637160
关于生娃的思考
生娃是人生很重要的事,值得花时间思考。我断断续续想了几年,没想明白,最近看 B 站半佛老师对哪吒 2 的影评,理解了什么情况下可以生娃,把感受分享给大家。
什么时候可以给孩子无条件的、不抱任何期待的爱,什么时候就可以生娃了。
半佛老师在影评中表示非常羡慕哪吒有那样的父母,因为哪吒的父母给哪吒无条件的、不抱任何期待的爱,而半佛老师的童年比较悲惨,让他有点羡慕嫉妒恨。为此半佛特意查了导演饺子的家庭,希望饺子是因为缺爱才把哪吒父母写的那么好,结果发现饺子就是哪吒本吒,他彻底破防了。
过年回家免不了和父母共处几天,我也几度破防,好几次想直接回出租屋。也被催生,丈母娘也催。
之前对生娃的看法,我觉得生娃太费钱了,而我没钱,就算我有钱,我也不想把钱花在娃上面,我想花在自己身上,多体验人生。
其次我自己这辈子都没活明白,也挺痛苦,何必生个娃来这悲惨世间走一遭?所以我不生娃。
但这次我有新的想法,是否生娃,应取决于我是否做好了为人父母的准备,即是否可以给孩子无条件的、不抱任何期待的爱。这决定孩子一辈子是否幸福。
两个关键词,无条件、不抱期待。
无条件
考试考得好,父母就爱,考得不好就不爱;听话就爱,不听话就不爱;不知道你们怎样,我小时候是这样的。
这让我没有安全感,下意识会做些讨好父母的行为,来获得父母的 “ 爱 ”。
当我成年的时候,我妈发现这招不管用了,我不需要他们的爱,我需要钱,因为钱能给我安全感。
所以我大学去食堂打工,每天中午和晚上早点下课过去,结果一个月才 300 块钱包两顿饭。
不抱期待
期待这个东西,在教育中尤为突出,否则不会也有那么多鸡娃的父母。
父母对孩子的期待,让孩子有非常强烈的愧疚。我辛辛苦苦把你从农村带到城市,你就考这么几分?我辛辛苦苦把你拉扯大,你就这么对我?
在这种环境下,我成为了一个逆子,六亲不认,自动屏蔽亲情。我不接受他们的爱,我也不给他们爱。
大学打工的 300 块不能养活我,每年还要交学费,我一想到学费是父母出的,他们又会以此要挟,我辛辛苦苦赚钱供你上大学,结果你就这样?
所以只读了半年大学,我退学了,自己出去找工作,我必须在经济上独立,必须逃离这个家。
别人说家是港湾,外面受伤了家是永远的依靠,对我来说家就是伤害。
其实我这样还算好的,至少活下来了,还有更多孩子,承受不了这种愧疚,选择了自杀,他们要把愧疚,加倍偿还给父母。
我身边就有这样的案例,跳楼了,他父母不知道为什么说了两句,孩子就直接从阳台上跳下去了。
我可太懂了,我也想过自杀,一来是当时家里装了防盗窗,我有点胖钻不过去;然后换成了撞墙,头上撞了几个包有点疼,基因让我停下了;我还尝试过离家出走,也没走成。
但我也在脑海中无数次幻想,要是我死了,我爸妈有多愧疚,这就是我自杀的目的。
我写的和想的有点黑暗,有人说什么爸妈把你辛辛苦苦拉扯大,不容易什么的。
但是我不需要,我没求着来这个世上。
所以我对自己生娃,非常谨慎,我不希望他的童年会想自杀,我不希望他成年后和父母几乎断绝关系,我希望他要来就过的幸福点,这取决我能否会给他无条件的、没有期待的爱。
愿各位都是好父母,愿世上孩子都幸福,以上是我的思考,共勉。
来源:juejin.cn/post/7467353503088246784
官方回应无虚拟DOM版Vue为什么叫Vapor
相信很多人和我一样,好奇无虚拟DOM
版的Vue
为什么叫Vue Vapor
。之前看过一个很新颖的观点:Vue1
时代就没有虚拟DOM
,而那个时代恰好处于前端界的第一次工业革命,也就是以声明式代替命令式的写法。所以无虚拟DOM
版Vue让人感觉梦回Vue1
,于是就采取了Vapor
这个名字。
Vapor是蒸汽的意思,第一次工业革命开创了以机器代替手工劳动的时代,该革命以蒸汽机作为动力机被广泛使用为标志。
不过这个说法并非来自官方,虽然乍一听还挺有道理的,但总感觉官方并不一定是这么想的。事实也的确如此,在官方的Vue Conf
中,Vue Vapor
的作者出面说明了Vapor
这个名字的含义:
由于无虚拟DOM
的特性,纯Vapor
模式下可以去掉很多代码,比如VDom Diff
。所以Vue Vapor
的包体积可以做的更加的轻量化,像水蒸气一样轻。
(前面那段话是官方说的,这段话是我说的)当然不是说
Vapor
模式就不需要diff
算法了,我看过同为无虚拟DOM
框架的Svelte
和Solid
源码,无虚拟DOM
只是不需要vDom
间的Diff
算法了,列表之间还是需要diff
的。毕竟再怎么编译,你从后端获取到的数组,编译器也不可能预测得到。
那具体能轻量多少呢?官方给出的数据是比虚拟DOM
版Vue
小33.6%
:
Vapor
的名字除了想表示轻量化之外还有一个另外一个原因,那就是Solid
。可能有人会说这关Solid
什么事啊?实际上Vapor
的灵感正是来自于Solid
(尤雨溪亲口承认)而Solid
代表固体:
为了跟Solid
有个趣味联动,那无虚拟DOM
就是气体好了:
以上就是Vue Vapor
作者告诉大家为什么叫Vapor
的两大原因。
性能
之前都说虚拟DOM
是为了性能,怎么现在又反向宣传了?无虚拟DOM
怎么又逆流而上成为了性能的标杆了呢?这个话题我们留到下一篇文章去讲,本篇文章我们只看数据:
从左到右依次为:
- 原生
JS
:1.01 Solid
:1.09Svelte
:1.11- 无虚拟
DOM
版Vue
:1.24 - 虚拟
DOM
版Vue
:1.32 React
:1.55
数字越小代表性能越高,但无论再怎么高都不可能高的过手动优化过的原生JS
,毕竟无论什么框架最终打包编译出来的还是JS
。不过框架的性能其实已经挺接近原生的了,尤其是以无虚拟DOM
著称的Solid
和Svelte
。但无虚拟DOM
版Vue
和虚拟DOM
版Vue
之间并没有拉开什么很大的差距,1.24
和1.32
这两个数字证明了其实二者的性能差距并不明显,这又是怎么一回事呢?
一个原因是Vue3
本来就做了许多编译优化,包括但不限于静态提升、大段静态片段将采用innerHTML
渲染、编译时打标记以帮助虚拟DOM
走捷径等… 由于本来的性能就已经不错了,所以可提升的空间自然也没多少。
看完上述原因肯定有人会说:可提升的空间没多少可以理解,那为什么还比同为无虚拟DOM
的Solid
和Svelte
差那么多?如果Vapor
和Solid
性能差不多的话,那可提升空间小倒是说得过去。但要是像现在这样差的还挺多的话,那这个理由是不是就有点站不住脚了?之所以会出现这样的情况是因为Vue Vapor
的作者在性能优化和快速实现之间选择了后者,毕竟尤雨溪第一次公布要开始做无虚拟DOM
版Vue
的时间是在2020
年:
而如今已经是2025
年了。也就是说如果一个大一新生第一次满心欢喜的听到无虚拟DOM
版Vue
的消息后,那么他现在大概率已经开始毕业工作了都没能等到无虚拟DOM
版Vue
的发布。这个时间拖的有点太长了,甚至从Vue2
到Vue3
都没用这么久。
所以现在的首要目标是先实现出来一版能用的,后面再慢慢进行优化。我们工作不也是这样么?先优先把功能做出来,优化的事以后再说。那么具体该怎么优化呢:
现在的很多渲染逻辑继承自原版Vue3
,但无虚拟DOM
版Vue
可以采用更优的渲染逻辑。而且现在的Vapor
是跟着原版Vue
的测试集来做的,这也是为了实现和原版Vue
一样的行为。但这可能并不是最优解,之后会慢慢去掉一些不必要的功能,尤其是对性能产生较大影响的功能。
往期精彩文章:
来源:juejin.cn/post/7477104460452872202
Netflix 删除了 React ?
Netflix 删除了 React
"Netflix 删除了 React,网站加载时间减少了 50%!"
这条爆炸性新闻在推特炸开了锅。这不禁让人想起半年前"微软应该禁止所有新项目使用 React" 的新闻 - 两大科技巨头相继 "抛弃" React,难道这预示着什么?
"React 心智负担重、性能差、bundle 体积大...". 一时间,各种唱衰 React 的声音此起彼伏。有人甚至信誓旦旦地表示:"只要像 Netflix 一样干掉 React,你的网站性能立马提升 50%!"
这条发布于 2024 年 10 月 27 日的推文有着惊人的 150 万浏览量。但是,这大概率是 AI
生成的假新闻。
事实上,我们去 Netflix
的官网打开 react-devtools
,发现他们依然在使用 React
构建他们的网站。
Netflix 的真实案例
这篇 AI
生成的假新闻灵感来自 2017 年 Netflix
工程师在 hack news
上发布的一篇文章 - Netflix: Removing client-side React.js improved performance by 50%
他直接移除了这篇文章最重要的部分 - client-side React.js
, 也就是客户端的 React.js
代码。
实际的情况是,Netflix
团队在 2017 年的时候在使用 React
构建他们的 landing page
。
为什么在一个简单的 landing page
上要使用 React
呢?因为在 landing page
上
Netflix
需要处理大量的AB 测试
- 支持近 200 个国家的本地化
- 根据用户设备、地理位置等因素动态调整内容
- 需要服用现有的
React
组件
基于上述需求的考虑,Netflix
团队选择了使用 React
来构建他们的 landing page
。
为了优化页面加载性能,他们采用了服务端渲染的方案。同时,为了保证后续交互的流畅性,系统会预先加载(pre-fetch
)后续流程所需的 React/Redux
相关代码。
从架构上看,这个 landing page
本质上仍然是一个单页面应用(SPA
),保持了 SPA
快速响应的优势。不同之处在于首次访问时,页面内容是由服务端直接生成的,这样可以显著提升首屏加载速度。
这样做的缺点
显然,Netflix
在 2017 年这么做是有原因的,他们当时的确也没有更好的方案。在 2025 年的今天,
再来回顾这个方案,显然有以下缺点:
数据重复获取
在首屏渲染时,服务端需要获取数据来生成 HTML,而在客户端激活(hydration)后,为了保持交互性,往往又需要重新获取一遍相同的数据。
这种重复的数据获取不仅浪费资源,还可能带来不必要的性能开销。
客户端代码体积膨胀
因为本质上,Netflix
的 landing page
是一个还是一个 SPA
,那么不可避免的,所有可能的 UI
状态都需要打包,
即使用户只需要其中的一部分代码。例如,在一个很常见的 tabs
页面
<Tabs
defaultActiveKey="1"
items={[
{
label: 'Tab 1',
key: '1',
children: 'Tab 1',
},
{
label: 'Tab 2',
key: '2',
children: 'Tab 2',
disabled: true,
},
]}
/>
即使用户只点击了 Tab 1
, 即使 Tab 2
没有被渲染,但是 Tab 2
的代码也会被打包。
如何解决这些问题
React Server Components (RSC)
为上述问题提供了优雅的解决方案:
避免数据重复获取
使用 RSC
,组件可以直接在服务器端获取数据并渲染,客户端只接收最终的 HTML
结果。不再需要在客户端重新获取数据。
智能代码分割
RSC
允许我们选择性地决定哪些组件在服务器端运行,哪些在客户端运行。例如:
function TabContent({ tab }: { tab: string }) {
// 这部分代码只在服务器端运行,不会打包到客户端
return <div>{tab} 内容</div>
}
// 客户端组件
'use client'
function TabWrapper({ children }) {
const [activeTab, setActiveTab] = useState('1')
return (
<div>
{/* Tab 切换逻辑 */}
{children}
</div>
)
}
在这个例子中:
TabContent
的所有可能状态都在服务器端预渲染- 只有实际需要交互的
TabWrapper
会发送到客户端 - 用户获得了更小的
bundle
体积和更快的加载速度
这不就是 PHP?
经常会看到有人说:"Server Components 不就是重新发明了 PHP 吗?都是在服务端生成 HTML。"
显然,PHP 与现在的 Server Components
在开发体验上有本质的区别。
1. 细粒度的服务端-客户端混合
与 PHP 不同,RSC 允许我们在组件级别决定渲染位置,用一个购物车的例子来说明:
// 服务端组件
function ProductDetails({ id }: { id: string }) {
// 在服务器端获取数据和渲染
const product = await db.products.get(id);
return <div>{product.name}</div>;
}
// 客户端组件
'use client'
function AddToCart({ productId }: { productId: string }) {
// 在客户端处理交互
return <button onClick={() => addToCart(productId)}>加入购物车</button>;
}
// 混合使用
function ProductCard({ id }: { id: string }) {
return (
<div>
<ProductDetails id={id} />
<AddToCart productId={id} />
</div>
);
}
这种设计充分利用了服务端和客户端各自的优势 - 在服务端可以直接访问数据库获取 ProductDetails
所需的数据,而在客户端则能更好地处理 AddToCart
这样的用户交互。这不仅提升了性能,也让代码结构更加清晰合理。
2. 保持组件的可复用性
RSC
最强大的特性之一是组件的可复用性不受渲染位置的影响:
// 这个组件可以在服务端渲染
function UserProfile({ id }: { id: string }) {
return <ProfileCard id={id} />;
}
// 同样的组件也可以在客户端动态加载
'use client'
function UserList() {
const [selectedId, setSelectedId] = useState(null);
return selectedId ? <ProfileCard id={selectedId} /> : null;
}
因为都是 React
组件,区别仅仅是渲染位置的不同,同一个组件可以:
- 在服务端预渲染时使用
- 在客户端动态加载时使用
- 在流式渲染中使用
这种统一的组件模型是 PHP 等传统服务端渲染所不具备的。
3. 智能的序列化
RSC
还提供了智能的序列化机制,可以自动将组件的 props
和 state
序列化,从而在服务端和客户端之间传递。
避免了重复获取数据的问题。
// 服务端组件
async function Comments({ postId }: { postId: string }) {
// 1. 获取评论数据
const comments = await db.comments.list(postId);
// 2. 传递给客户端组件
return <CommentList initialComments={comments} />;
}
// 客户端组件
'use client'
function CommentList({ initialComments }) {
// 3. 直接使用服务端数据,无需重新请求
const [comments, setComments] = useState(initialComments);
return (
// 渲染评论列表
);
}
4. 渐进式增强
RSC
还提供了渐进式增强的能力,可以在服务端和客户端之间无缝过渡。
- 首次访问时返回完整的 HTML
- 按需加载客户端交互代码
- 保持应用的可访问性
这让我们能够构建既有良好首屏体验,又能提供丰富交互的现代应用,完美解决了在 2017 年 Netflix
所提出的问题。
总结
通过对上面这些案例的分析,我们可以看出
1. 不要轻信网络传言
网络上充斥着各种技术传言。虽然像上面这种完全虚构的假新闻容易识破,但有些传言会巧妙地利用真实数据和案例,通过夸张的描述来误导技术选型和决策,这类信息需要我们格外谨慎分辨。
例如:
svelte 放弃 TypeScript 改用 JSdoc 进行类型检查
这个确实是一个真的新闻,但是并不代表着 Typescript
的没落,实际上
- Svelte 团队选择 JSDoc 是为了减少编译时间
- 这是针对框架源码的优化,而不是面向使用者的建议
- Svelte 依然完整支持 TypeScript,用户代码可以继续使用 .ts/.ts
tauri 打包后的体积比 electron 小多了,我们应该放弃 electron 使用 tauri
技术选型不能仅仅看单一指标。虽然 tauri
的打包体积确实小于 electron
,但在开发体验、性能、稳定性、生态和社区支持等关键维度上都存在明显短板。
如果你尝试用 tauri
开发复杂应用,很可能会因为生态不完善、社区支持不足而陷入困境。当你遇到问题去 GitHub
寻找解决方案时,看到许多库已经一年未更新,就会明白为什么大多数团队仍在选择 electron
。
2. 历史的选择
2017 年的 Netflix 面临着复杂的业务需求,他们选择了当时最佳的解决方案 - 服务端渲染 + 客户端激活。这个方案虽然解决了问题,但也带来了一些困扰:
- 数据需要在服务端和客户端重复获取
- JavaScript bundle 体积过大
3. RSC 带来的改变
React Server Components
为这些历史遗留问题带来了全新的解决思路:
- 服务端渲染与客户端渲染完美融合
- 智能的代码分割,最小化客户端 bundle 体积
- 数据获取更高效,避免重复请求
- 渐进式增强,提供流畅的用户体验
4. 技术演进的启示
从 Netflix
2017 年的实践到今天的 RSC
,我们可以看到:
- 技术方案在不断进化,过去的最佳实践可能已不再适用
- RSC 不是简单的"回归服务端",而是开创了全新的开发模式
- 性能与开发体验不再是非此即彼的选择
RSC
代表了现代前端开发的新趋势 - 既保持了 React
强大的组件化能力,又通过创新的架构设计解决了历史难题。这让我们终于可以在性能和开发体验之间找到完美平衡。
来源:juejin.cn/post/7459029441039794211
一次失败的UI规范制定
前言
在公司中,前端使用了统一的组件element-ui,但是有一些页面大家并没有达成共识,造成了不同的团队开发出来的页面不同,公司决定在24年8月的迭代进行统一调整。在这个中间,我遇到了很多意料之外的问题,希望未来的你遇到了类似的问题,可以尽量避免
为什么会产生这个问题
这个问题我也思考过,大概有以下原因
- 我们有4个产品经理,每个人负责不同的模块,都有各自的风格,造成页面不统一
- 没有一个严格的UI规范,前端开发和测试并没有一个可以参考标准页面
- 22年至今,项目都在疯狂的迭代功能,并没有人停下来看看之前做的功能有哪些问题,我们该怎么去优化
项目背景
参与人:UI设计师、前端开发、产品(主要负责审核,并没参与讨论)、测试
牵头人:UI设计师
职责:找出问题点,整理为在线文档
解决者:前端
职责:整理问题点、改公告组件、输出文档
主要问题如下
- 弹窗(Dialog)组件大小不一。宽度有400px、700px、800px、940px、1250px、50%、60%等,用户在操作的时候,会感觉到页面时大时小,不统一
- 表格样式很多(Table)。搜索的列未对齐、分页码数未统一、表格后的操作栏千奇百怪、表头的标题有的进行了换行等
- 颜色的乱用。颜色有很多,有各种颜色的红色
- 弹窗表单的按钮位置不同。有的取消在左,有的取消在右面。
- 等等一些小问题就不一一列举了
弹窗组件大小不一
弹窗大小不统一部分截图
800px
600px
1180px
解决方案
我们在私服中clone了一份element-ui,直接修改了源码
默认制定了三个尺寸的size,small(600px), medium(900px),large(1200px),满足基本的需求。当需要支持特殊组件的时候,也可以直接设置Width满足需求
表格不统一
部分截图
上方的截图有几个问题
- 搜索条件(查找人员)没有和新增按钮对齐
- 离职和删除的按钮是比较敏感的操作,但是夹在了一堆按钮中间
- 操作按钮有的有icon,有的没icon,看着些许的混乱
进行修改后效果如下,页面看着更加的工整
解决方案如下
- 搜索条件的第一个在组件内直接进行计算宽度,防止业务端进行二次修改,如果是自定义的搜索条件,并且设置了宽度,需要手动进行修改
- 一些危险的操作:比如离职、删除统一放在最后,并且每个操作按钮都要有icon
表格按钮的调整
调整前
调整后
解决方案如下
表格后的按钮去除所有的icon,直接在组件内进行拦截,默认展示三个按钮,多余的放在dropDown内,当然也支持业务端设置显示几个按钮
核心部分代码如下
分页数据不统一
调整前
调整后
解决方案
分页条数统一改为(20,50,100)
考虑的因素为:之前的页面有15一页的。当页面过高的时候,会撑不满一个页面,造成table的页面空白,不太美观
弹窗中,下方的操作栏的按钮位置不统一
调整前
调整后
解决方案
所有的按钮都是取消在左,保存在右面,同时,confirm组件,在element-ui的代码中直接修改调整位置,减少业务端工作量
颜色的乱用
部分截图
解决方案
在主应用的根节点 使用css变量,在每个子应用的页面直接使用根节点的样式,避免子应用的颜色出现各种奇奇怪怪的颜色。
使用的地方
等等
当然还有一些基础颜色的修改、按钮最小宽度的修改、一些表格间距的调整等。这些和设计师、产品达成共识后,进行修改就行了,也就不一一列举了
交付给测试
- 我这边开发的差不多了,输出了一个文档,别的前端开发按照着文档进行开发。
- 测试按照文档进行编写测试用例
不好搞了
测试这边疯狂提bug。
还有一个小小的背景
测试这边其实是有一个绩效考核:bug提的越多,绩效越高
但是,我们这边项目上线的时候,其实还有一个要求,bug不能超过9个
这个UI规范制定,到这个功能的提测,只有10天就项目上线了。
有的功能并没有做,但是测试觉得不合理,就提了这个bug(比如表单的label也没有对齐,但是这个迭代并没有去做修复),造成我们前端开发这边bug超级多
同时,开发这边也会有绩效考核,bug越多绩效就会越低,会扣除工资,大家心态崩溃了,改的速度根本赶不上提的速度。
当然,也有部分功能是我这边测试不充分,造成业务端不好去实现
找领导协助
这个过程是个很折磨人的过程, 开发同事在抱怨测试乱提、测试也在提本次优化范围外的bug,内心也很着急
- 重新定义了测试的范围,超过范围的问题可以先记着,不再提bug,方便后续进行修改
- 前端开发的bug都先指派给我,我看看能否从底层解决,我解决后再转给研发经理(研发经理没有此项的考核),当底层解决不了,业务端解决后,也指派给我,这样前端的开发就没有这么暴躁了
如果再来一次UI规范的升级我会怎么做
- 先在每个团队中找一个人配合我,先升级页面后,进行灰度测试
- 在决定我们这个页面要成什么样子的时候,一定要拉上产品。设计师在做设计稿的时候,虽然每次都说了找产品的领导确认过了,但是领导还是比较忙, 并没有很多精力来做这件事情,可以找他们组内的一个资历比较深的产品一起讨论下。开发、UI设计师站的角度不同,一定要需要产品来参与讨论,做的页面才会更加的易用
- 限制测试的范围。 测试测着测试着会发散思维,导致改不完的bug,最终会导致开发没有了心态
- UI标准的功能,越早出来越好,越大后期需要投入的人力越多
来源:juejin.cn/post/7456685819047608355
uni-app 实现好看易用的抽屉效果
往期文章推荐:
一. 前言
我之前使用 uni-app
和 uniCloud
开发了一款软考刷题应用,在这个应用中,我使用了抽屉组件来实现一些功能,切换题库,如下图所示:
在移动应用开发中,抽屉(Drawer
)是一种常见的界面设计模式,这个组件可以在需要侧边导航或者额外信息展示的地方使用。它允许用户通过侧滑的效果打开一个菜单或额外的内容区域。
这种设计不仅能够节省屏幕空间,还能提供一种直观的交互方式。
例如,在电商应用中,可以将购物车或分类列表放在抽屉里;在新闻阅读器中,可以放置频道选择等;而在有题记刷题软件中,我主要用于题库的选择功能。
本文将介绍如何使用 uni-app 框架来实现一个简单的抽屉组件:DrawerWindow
。文末提供完整的代码示例,让你能够轻松地在 uni-app 中实现抽屉效果。
二. 实现分析
Vue 组件的结构通常由三个主要部分组成:模板(<template>
)、脚本(<script>
)和样式(<style>
),标准的的单文件组件(SFC
)结构。
uni-app 也是如此,在这个组件中,我们也将使用 Vue 的单文件组件(SFC
)结构,这意味着我们将在一个 .vue
文件中同时包含模板、脚本和样式。
接下来我们按照这个格式来简单实现一下。
1. 模板页面 (<template>
)
首先,模版页面是很简单的部分,我们需要创建一个基础的 Vue 组件,该组件包含了主页面、抽屉内容和关闭按钮三个部分。以下是组件的模板代码:
<template>
<view class="drawer-window-wrap">
<scroll-view scroll-y class="DrawerPage" :class="{ show: modalName === 'viewModal' }">
<!-- 主页面 -->
<slot></slot>
</scroll-view>
<!-- 关闭抽屉 -->
<view class="DrawerClose" :class="{ show: modalName === 'viewModal' }" @tap="hide">
<u-icon name="backspace"></u-icon>
</view>
<!-- 抽屉页面 -->
<scroll-view scroll-y class="DrawerWindow" :class="{ show: modalName === 'viewModal' }">
<slot name="drawer"></slot>
</scroll-view>
</view>
</template>
在模板部分,我们主要定义了三个主要元素:主页面、关闭按钮和抽屉页面。每个元素都有一个class
绑定,这个绑定会根据 modalName
的状态来决定是否添加 .show
类。
- 主页面 (
<scroll-view class="DrawerPage">
):
- 这个滚动视图代表应用的主要内容区域。
- 当抽屉打开时,它会被缩小并移向屏幕右侧。
- 提供默认插槽
<slot></slot>
,允许父组件传递自定义内容到这个位置。
- 关闭按钮 (
<view class="DrawerClose">
):
- 位于屏幕右侧的一个透明背景层,当点击时触发
hide()
方法来关闭抽屉。 - 包含了一个图标
<u-icon name="backspace"></u-icon>
,这里使用的是 uView UI 库中的图标组件。你可以选用其他组件库里的图标或者图片。
- 位于屏幕右侧的一个透明背景层,当点击时触发
- 抽屉页面 (
<scroll-view class="DrawerWindow">
):
- 这是抽屉本身的内容区域,通常包含菜单或其他附加信息。
- 同样地,定义特有的插槽名称,
<slot name="drawer"></slot>
允许从外部插入特定的内容。 - 抽屉默认是隐藏的,并且当显示时会有动画效果。
在这里,我们主要使用了 <slot>
元素来定义可以插入自定义内容的位置。modalName
属性用来控制抽屉的状态。
2. 逻辑处理 (<script>
)
接下来,逻辑处理其实也很简单,主要会定义打开和关闭抽屉的方法:
<script>
export default {
data() {
return {
modalName: null
}
},
methods: {
// 打开抽屉
show() {
this.modalName = 'viewModal';
},
// 关闭抽屉
hide() {
this.modalName = null;
}
}
}
</script>
- 数据 (
data
):
modalName
: 用于控制抽屉状态的数据属性。当它的值为'viewModal'
时,表示抽屉处于打开状态;否则,抽屉是关闭的。
- 方法 (
methods
):
show()
: 将modalName
设置为'viewModal'
,从而通过 CSS 样式控制抽屉显示。hide()
: 将modalName
重置为null
,控制抽屉隐藏。
当调用 show()
方法时,modalName
被设置为 'viewModal'
,这会触发 CSS 中的 .show
类,从而显示抽屉;反之,调用 hide()
方法则会隐藏抽屉。
3. 样式设计 (<style>
)
在这个组件中,其实要做好的在样式部分,主要是显示抽屉的动画部分。在主页面,我们主要定义了三个主要的样式类:主页面、关闭按钮和抽屉页面。
- 主页面样式 (
DrawerPage
):
- 初始状态下占据整个屏幕宽度和高度。
- 当抽屉打开时(即有
.show
类),页面会缩小并移动到屏幕右侧 85%的位置,同时增加阴影效果以模拟深度。
- 关闭按钮样式 (
DrawerClose
):
- 默认情况下是不可见且不响应用户交互的。
- 当抽屉打开时,按钮变为可见并可点击,提供了一种关闭抽屉的方式。
- 抽屉页面样式 (
DrawerWindow
):
- 初始状态下位于屏幕左侧外侧,不显示也不响应交互。
- 当抽屉打开时,抽屉平滑滑入屏幕内,变得完全可见且可以与用户互动。
- 动画与过渡
- 所有的
.show
类都带有transition: all 0.4s;
,这使得任何属性的变化都会有一个 0.4 秒的平滑过渡效果。 - 抽屉和主页面的
transform
属性被用来控制它们的位置和大小变化。 opacity
和pointer-events
属性确保在不需要时抽屉不会影响用户的操作。
- 所有的
如下代码所示,我们主要添加一些 CSS 样式来实现平滑的过渡效果以及视觉上的美观:
<style lang="scss">
// 省略其他样式...
.DrawerPage.show,
.DrawerWindow.show,
.DrawerClose.show {
transition: all 0.4s;
}
.DrawerPage.show {
transform: scale(0.9, 0.9) translateX(85vw);
box-shadow: 0 0 60rpx rgba(0, 0, 0, 0.2);
}
.DrawerWindow.show {
transform: scale(1, 1) translateX(0%);
opacity: 1;
pointer-events: all;
}
.DrawerClose.show {
width: 15vw;
color: #fff;
opacity: 1;
pointer-events: all;
}
</style>
以上的这些样式确保了当抽屉显示或隐藏时有流畅的动画效果,并且在不需要的时候不会影响用户的交互。
三. 完整代码
1. 完整抽屉组件,复制可使用
<template>
<view class="drawer-window-wrap">
<scroll-view scroll-y class="DrawerPage" :class="modalName == 'viewModal' ? 'show' : ''">
<!-- 主页面 -->
<slot></slot>
</scroll-view>
<!-- 关闭抽屉 -->
<view class="DrawerClose" :class="modalName == 'viewModal' ? 'show' : ''" @tap="hide()">
<u-icon name="backspace"></u-icon>
</view>
<!-- 抽屉页面 -->
<scroll-view scroll-y class="DrawerWindow" :class="modalName == 'viewModal' ? 'show' : ''">
<slot name="drawer"></slot>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
modalName: null
}
},
methods: {
// 打开抽屉
show() {
this.modalName = 'viewModal'
},
// 关闭抽屉
hide() {
this.modalName = null
}
}
}
</script>
<style lang="scss">
page {
width: 100vw;
overflow: hidden !important;
}
.DrawerPage {
position: fixed;
width: 100vw;
height: 100vh;
left: 0vw;
background-color: #f1f1f1;
transition: all 0.4s;
}
.DrawerPage.show {
transform: scale(0.9, 0.9);
left: 85vw;
box-shadow: 0 0 60rpx rgba(0, 0, 0, 0.2);
transform-origin: 0;
}
.DrawerWindow {
position: absolute;
width: 85vw;
height: 100vh;
left: 0;
top: 0;
transform: scale(0.9, 0.9) translateX(-100%);
opacity: 0;
pointer-events: none;
transition: all 0.4s;
background-image: linear-gradient(45deg, #1cbbb4, #2979ff) !important;
}
.DrawerWindow.show {
transform: scale(1, 1) translateX(0%);
opacity: 1;
pointer-events: all;
}
.DrawerClose {
position: absolute;
width: 40vw;
height: 100vh;
right: 0;
top: 0;
color: transparent;
padding-bottom: 50rpx;
display: flex;
align-items: flex-end;
justify-content: center;
background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 0.6));
letter-spacing: 5px;
font-size: 50rpx;
opacity: 0;
pointer-events: none;
transition: all 0.4s;
}
.DrawerClose.show {
opacity: 1;
pointer-events: all;
width: 15vw;
color: #fff;
}
</style>
2. 在父组件中使用抽屉组件
在父组件中,可以通过以下简单的代码使用它,你可以继续进行丰富:
<template>
<drawer-window ref="drawerWindow">
<view class="main-container" @click="$refs.drawerWindow.show()">
主页面,点击打开抽屉
</view>
<view slot="drawer" class="drawer-container"> 抽屉页面 </view>
</drawer-window>
</template>
<script>
export default {}
</script>
<style lang="scss" scoped>
.main-container,
.drawer-container {
font-weight: 700;
font-size: 20px;
text-align: center;
color: #333;
padding-top: 100px;
}
</style>
以上代码的实现效果如下图所示:
四. 小程序体验
以上的组件,来源于我独立开发的软考刷题小程序中的效果,想要体验或软考刷题的掘友可以参考以下文章,文末获取:
五. 结语
通过以上步骤,我们已经构建了一个基本的抽屉组件。当然,你也可以根据具体的应用场景对这个组件进行进一步的定制和优化。
来源:juejin.cn/post/7417374536670707727
个人或个体户,如何免费使用微信小程序授权登录
需求
个人或个体户,如何免费使用微信小程序授权,快速登录进系统内部?
微信授权登录好处:
- 不用自己开发一个登录模块,节省开发和维护成本
- 安全性得到了保障,安全验证完全交由腾讯验证,超级可靠哇
可能有的人会问,为何不用微信公众号授权登录?原因很简单,因为一年要300元,小公司得省钱啊!
实现步骤说明
所有的步骤里包含四个对象,分别是本地后台
、本地微信小程序
、本地网页
、以及第三方微信后台
本地后台
调用微信后台
的https://api.weixin.qq.com/cgi-bin/token
接口,get
请求,拿到返回的access_token
;本地后台
根据拿到的access_token
,调用微信后台
的https://api.weixin.qq.com/wxa/getwxacodeunlimit
接口,得到二维码图片文件,将其输出传递给本地网页
显示本地微信小程序
扫本地网页
的二维码图片,跳转至小程序登录页面,通过wx.login
方法,在success
回调函数内得到code
值,并将该值传递给本地后台
本地后台
拿到code
值后,调用微信后台
的https://api.weixin.qq.com/sns/jscode2session
接口,get
请求,得到用户登录的openid
即可。
注意点:
- 上面三个微信接口
/cgi-bin/token
、/getwxacodeunlimit
、/jscode2session
必须由本地后台
调用,微信小程序那边做了前端限制;
本地网页
如何得知本地微信小程序
已扫码呢?
本地微信小程序
将code
,通过A接口
,将值传给后台,后台拿到openid
后,再将成功结果返回给本地微信小程序
;同时,本地网页
不断地轮询A接口
,等待后台拿到openid
后,便显示登录成功页面。
微信小程序核心代码
Page({
data: {
theme: wx.getSystemInfoSync().theme,
scene: "",
jsCode: "",
isLogin: false,
loginSuccess: false,
isChecked: false,
},
onLoad(options) {
const that = this;
wx.onThemeChange((result) => {
that.setData({
theme: result.theme,
});
});
if (options !== undefined) {
if (options.scene) {
wx.login({
success(res) {
if (res.code) {
that.setData({
scene: decodeURIComponent(options.scene),
jsCode: res.code,
});
}
},
});
}
}
},
handleChange(e) {
this.setData({
isChecked: Boolean(e.detail.value[0]),
});
},
formitForm() {
const that = this;
if (!this.data.jsCode) {
wx.showToast({
icon: "none",
title: "尚未微信登录",
});
return;
}
if (!this.data.isChecked) {
wx.showToast({
icon: "none",
title: "请先勾选同意用户协议",
});
return;
}
wx.showLoading({
title: "正在加载",
});
let currentTimestamp = Date.now();
let nonce = randomString();
wx.request({
url: `A接口?scene=${that.data.scene}&js_code=${that.data.jsCode}`,
header: {},
method: "POST",
success(res) {
wx.hideLoading();
that.setData({
isLogin: true,
});
if (res.statusCode == 200) {
that.setData({
loginSuccess: true,
});
} else {
if (res.statusCode == 400) {
wx.showToast({
icon: "none",
title: "无效请求",
});
} else if (res.statusCode == 500) {
wx.showToast({
icon: "none",
title: "服务内部错误",
});
}
that.setData({
loginSuccess: false,
});
}
},
fail: function (e) {
wx.hideLoading();
wx.showToast({
icon: "none",
title: e,
});
},
});
},
});
scene
为随机生成的8位数字
本地网页核心代码
let isInit = true
function loginWx() {
isInit = false
refreshQrcode()
}
function refreshQrcode() {
showQrLoading = true
showInfo = false
api.get('/qrcode').then(qRes => {
if (qRes.status == 200) {
imgSrc = `${BASE_URL}${qRes.data}`
pollingCount = 0
startPolling()
} else {
showToast = true
toastMsg = '二维码获取失败,请点击刷新重试'
showInfo = true
}
}).finally(() => {
showQrLoading = false
})
}
// 开始轮询
// 1000毫秒轮询一次
function startPolling() {
pollingInterval = setInterval(function () {
pollDatabase()
}, 1000)
}
function pollDatabase() {
if (pollingCount >= maxPollingCount) {
clearInterval(pollingInterval)
showToast = true
toastMsg = '二维码已失效,请刷新'
showInfo = true
return
}
pollingCount++
api.get('/result').then(res => {
if (res.status == 200) {
clearInterval(pollingInterval)
navigate('/os', { replace: true })
} else if (res.status == 408) {
clearInterval(pollingInterval)
showToast = true
toastMsg = '二维码已失效,请刷新'
showInfo = true
}
})
}
html的部分代码如下所示
<button class="btn" on:click={loginWx}>微信登录</button>
<div id="qrcode" class="relative mt-10">
{#if imgSrc}
<img src={imgSrc} alt="二维码图片"/>
{/if}
{#if showQrLoading}
<div class="mask absolute top-0 left-0 w-full h-full z-10">
<Loading height="12" width="12"/>
</div>
{/if}
</div>
尾声
若需要完整代码,或想知道如何申请微信小程序
,欢迎大家关注或私信我哦~~
附上网页微信授权登录动画、以及小程序登录成功后的截图
来源:juejin.cn/post/7351649413401493556
基于uniapp带你实现了一个好看的轮播图组件
背景
最近,朋友说在做uniapp
微信小程序项目时接到一个需求,需要实现一个类似上图的轮播图效果,问我有没有什么好的实现方案,他也和我说了他的方案,比如让产品直接把图片切成两部分再分别进行上传,但是我觉得这个方案不够灵活,当每次修改banner
图片时,都得让产品把图切好再分别上传。下文将探究一个更好的实现思路。
需求分析
由文章顶部的gif
动图,我们可以看出每次执行轮播动画时,只会裁剪图片的中间部分进行滚动,图片的其余部分保持不变,等待轮播动画执行完成后,再淡化背景图片切换到下一张轮播图。
从中可得出两点关键信息:
1.两种相同图片堆叠在一起,一张背景图(大图),一张轮播图(小图);
2.需要对图片中间部分进行裁剪,并且定位到刚好能够和背景图重合得区域;
根据以上得出的信息,我们还需解决两个疑问:
1.如何对图片进行裁剪?
2.图片裁剪后如何定位和背景图重合的区域?
前端裁剪图片可以使用canvans
,但是兼容性不好,太麻烦!还有没有好一点的方法呢?当然有,参考css
中的雪碧图进行图片裁剪显示!!但还是有些麻烦,还有没有简单的方式呢?有,那就是使用css
属性overflow: hidden;
进行图片裁剪,下文也主要是讲这个方案。
开始实现
vi-swiper.vue
<template>
<view class="v-banner" :style="[boxStyle]">
<swiper class="v-swiper" autoplay indicator-dots circular
@animationfinish="onAnimationfinish"
>
<swiper-item class="v-swiperi-tem" v-for="(url,index) in list" :key="index">
<image class="v-img" :src="url" mode="scaleToFill"></image>
</swiper-item>
</swiper>
</view>
</template>
<script>
export default {
props: {
// 当前索引
value: {
type: Number,
default: 0
},
// 轮播图列表
list: {
type: Array,
default: () => []
}
},
computed: {
boxStyle() {
return {
backgroundImage: `url(${this.list[this.value]})`,
// 开启background-image转场动画
transition: '1s background-image'
}
}
},
methods: {
// 轮播图动画结束后更新底部更新图索引
onAnimationfinish(e) {
this.$emit('input', e.detail.current)
}
}
}
</script>
<style lang="scss">
/*sass变量,用于动态计算*/
$swiperWidth: 650rpx;
$swiperHeight: 350rpx;
$verticalPadding: 60rpx;
$horizontalPadding: 50rpx;
$imgWidth: $swiperWidth + $horizontalPadding * 2;
$imgHeight: $swiperHeight + $horizontalPadding * 2;
.v-banner {
/* 因为需要根据内边距动态调节背景图宽高,所以设为行内块 */
display: inline-block;
// 背景图铺满容器
background-size: 100% 100%;
padding: $verticalPadding $horizontalPadding;
.v-swiper {
height: $swiperHeight;
width: $swiperWidth;
// 裁剪图片
overflow: hidden;
.v-swiperi-tem {
.v-img {
width: $imgWidth;
height: $imgHeight;
}
}
}
}
</style>
以上代码主要实现思路是让底部背景图大小和轮播图大小相同使两种重合,尺寸相等才能重合。swiper
轮播图容器组件固定宽高,使用overflow: hidden;
来裁剪内部图片, 然后给底部背景图容器使用padding
内边距来撑开容器,达到两种图片堆叠的效果;图片转场通过transition
设置动画。
以上组件页面显示效果如下:
发现两张图片还没有重合在一起,原因是两张图片虽然大小一致了,但是位置不对,如下图所示:
那么由上图描绘的信息可知,想要两张图重合,那么需要把轮播图分别向上和向左移动对应的内边距距离即可,我们可以通过给轮播图设置负的外边距实现
,样式如下:
.v-img {
...
// 使两张图片重合
margin-top: -$verticalPadding;
margin-left: -$horizontalPadding;
}
效果如下图所示:
到这我们实现了图片的裁剪和重合,已经实现了最终效果。完整的代码会在文章结尾附上。
另外,我已经把这个组件发布到了uniapp插件市场
,并且做了相应封装,可灵活定制轮播图大小及相关样式,感兴趣的可以点击这里:
vi-swiper轮播图,跳转到文档查阅源码或使用。
总结
这个堆叠轮播图效果实现起来不难,主要是要找对思路,一句话概括就是两张大小相等的图片进行重合,轮播图容器对图片进行裁剪。
完整代码
vi-swiper.vue
<template>
<view class="v-banner" :style="[boxStyle]">
<swiper class="v-swiper" autoplay indicator-dots circular
@animationfinish="onAnimationfinish"
>
<swiper-item class="v-swiperi-tem" v-for="(url,index) in list" :key="index">
<image class="v-img" :src="url" mode="scaleToFill"></image>
</swiper-item>
</swiper>
</view>
</template>
<script>
export default {
props: {
// 当前索引
value: {
type: Number,
default: 0
},
// 轮播图列表
list: {
type: Array,
default: () => []
}
},
computed: {
boxStyle() {
return {
backgroundImage: `url(${this.list[this.value]})`,
// 开启background-image转场动画
transition: '1s background-image'
}
}
},
methods: {
// 轮播图动画结束后更新底部更新图索引
onAnimationfinish(e) {
this.$emit('input', e.detail.current)
}
}
}
</script>
<style lang="scss">
/*sass变量,用于动态计算*/
$swiperWidth: 650rpx;
$swiperHeight: 350rpx;
$verticalPadding: 60rpx;
$horizontalPadding: 50rpx;
$imgWidth: $swiperWidth + $horizontalPadding * 2;
$imgHeight: $swiperHeight + $horizontalPadding * 2;
.v-banner {
/* 因为需要根据内边距动态调节背景图宽高,所以设为行内块 */
display: inline-block;
// 背景图铺满容器
background-size: 100% 100%;
padding: $verticalPadding $horizontalPadding;
.v-swiper {
height: $swiperHeight;
width: $swiperWidth;
// 裁剪图片
overflow: hidden;
.v-swiperi-tem {
.v-img {
width: $imgWidth;
height: $imgHeight;
margin-top: -$verticalPadding;
margin-left: -$horizontalPadding;
}
}
}
}
</style>
来源:juejin.cn/post/7377245069474021412
Java 泛型中的通配符 T,E,K,V,?有去搞清楚吗?
前言
不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。
不久前,被人问到Java 泛型中的通配符 T,E,K,V,? 是什么?有什么用?这不经让我有些回忆起该开始学习Java那段日子,那是对泛型什么的其实有些迷迷糊糊的,学的不这么样,是在做项目的过程中,渐渐有又看到别人的代码、在看源码的时候老是遇见,之后就专门去了解学习,才对这几个通配符 T,E,K,V,?有所了解。
泛型有什么用?
在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。
Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。
- 类型安全:编译是检查类型是否匹配,避免了ClassCastexception的发生。
// 非泛型写法(存在类型转换风险)
List list1 = new ArrayList();
list1.add("a");
Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException
// 泛型写法(编译时检查类型)
List list2 = new ArrayList<>();
// list.add(1); // 编译报错
list2.add("a");
String str = list2.get(0); // 无需强制转换
- 消除代码强制类型转换:减少了一些类型转换操作。
// 非泛型写法
Map map1 = new HashMap();
map1.put("user", new User());
User user1 = (User) map1.get("user");
// 泛型写法
Map map2 = new HashMap<>();
map2.put("user", new User());
// 自动转换
User user2 = map2.get("user");
在介绍这几个通配符之前,我们先介绍介绍泛型,看看泛型带给我们的好处。
Java泛型是JDK5中引入的一个新特性,泛型提供了编译是类型安全检测机制,这个机制允许开发者在编译是检测非法类型。泛型的本质就是参数化类型,就是在编译时对输入的参数指定一个数据类型。
- 类型安全:编译是检查类型是否匹配,避免了ClassCastexception的发生。
// 非泛型写法(存在类型转换风险)
List list1 = new ArrayList();
list1.add("a");
Integer num = (Long) list1.get(0); // 运行时抛出 ClassCastException
// 泛型写法(编译时检查类型)
Listlist2 = new ArrayList<>();
// list.add(1); // 编译报错
list2.add("a");
String str = list2.get(0); // 无需强制转换
- 消除代码强制类型转换:减少了一些类型转换操作。
// 非泛型写法
Map map1 = new HashMap();
map1.put("user", new User());
User user1 = (User) map1.get("user");
// 泛型写法
Map map2 = new HashMap<>();
map2.put("user", new User());
// 自动转换
User user2 = map2.get("user");
3.代码复用:可以支持多种数据类型,不要重复编写代码,例如:我们常用的统一响应结果类。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
/**
* 响应状态码
*/
private int code;
/**
* 响应信息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 时间戳
*/
private long timestamp;
其他代码省略...
- 增强可读性:通过类型参数就直接能看出要填入什么类型。
List list = new ArrayList<>();
泛型里的通配符
我们在使用泛型的时候,经常会使用或者看见多种不同的通配符,常见的 T,E,K,V,?这几种,相信大家一定不陌生,但是真的问你他们有什么作用?有什么区别时,很多人应该是不能很好的介绍它们的,接下来我就来给大家介绍介绍。
T,E,K,V
- T(Type) T表示任意类型参数,我们举个例子
pubile class A{
prvate T t;
//其他省略...
}
//创建一个不带泛型参数的A
A a = new A();
a.set(new B());
B b = (B) a.get();//需要进行强制类型转换
//创建一个带泛型参数的A
A a = new A();
a.set(new B());
B b = a.get();
- E(Element) E表示集合中的元素类型
List list = new ArrayList<>();
- K(Key) K表示映射的键的数据类型
Map map = new HashMap<>();
- V(Value) V表示映射的值的数据类型
Map map = new HashMap<>();
- T(Type) T表示任意类型参数,我们举个例子
pubile class A{
prvate T t;
//其他省略...
}
//创建一个不带泛型参数的A
A a = new A();
a.set(new B());
B b = (B) a.get();//需要进行强制类型转换
//创建一个带泛型参数的A
A a = new A();
a.set(new B());
B b = a.get();
List list = new ArrayList<>();
Map map = new HashMap<>();
Map map = new HashMap<>();
通配符 ?
- 无界通配符 表示未知类型,接收任意类型
// 使用无界通配符处理任意类型的查询结果
public void logQueryResult(List resultList) {
resultList.forEach(obj -> log.info("Result: {}", obj));
}
- 上界通配符 表示类型是T或者是子类
// 使用上界通配符读取缓存
public extends Serializable> T getCache(String key, Class clazz) {
Object value = redisTemplate.opsForValue().get(key);
return clazz.cast(value);
}
- 下界通配符 表示类型是T或者是父类
// 使用下界通配符写入缓存
public void setCache(String key, super Serializable> value) {
redisTemplate.opsForValue().set(key, value);
}
综合示例:
import java.util.ArrayList;
import java.util.List;
public class Demo {
//实体类
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog is eating");
}
}
class Husky extends Dog {
@Override
void eat() {
System.out.println("Husky is eating");
}
}
/**
* 无界通配符
*/
// 只能读取元素,不能写入(除null外)
public static void printAllElements(List list) {
for (Object obj : list) {
System.out.println(obj);
}
// list.add("test"); // 编译错误!无法写入具体类型
list.add(null); // 唯一允许的写入操作
}
/**
* 上界通配符
*/
// 安全读取为Animal,但不能写入(生产者场景)
public static void processAnimals(List animals) {
for (Animal animal : animals) {
animal.eat();
}
// animals.add(new Dog()); // 编译错误!无法确定具体子类型
}
/**
* 下界通配符
*/
// 安全写入Dog,读取需要强制转换(消费者场景)
public static void addDogs(Listsuper Dog> dogList) {
dogList.add(new Dog());
dogList.add(new Husky()); // Husky是Dog子类
// dogList.add(new Animal()); // 编译错误!Animal不是Dog的超类
Object obj = dogList.get(0); // 读取只能为Object
if (obj instanceof Dog) {
Dog dog = (Dog) obj; // 需要显式类型转换
}
}
public static void main(String[] args) {
// 测试无界通配符
List strings = List.of("A", "B", "C");
printAllElements(strings);
List integers = List.of(1, 2, 3);
printAllElements(integers);
// 测试上界通配符
List dogs = new ArrayList<>();
dogs.add(new Dog());
processAnimals(dogs);
List huskies = new ArrayList<>();
huskies.add(new Husky());
processAnimals(huskies);
// 测试下界通配符
List animals = new ArrayList<>();
addDogs(animals);
System.out.println(animals);
List
- 无界通配符 表示未知类型,接收任意类型
// 使用无界通配符处理任意类型的查询结果
public void logQueryResult(List resultList) {
resultList.forEach(obj -> log.info("Result: {}", obj));
}
// 使用上界通配符读取缓存
public extends Serializable> T getCache(String key, Class clazz) {
Object value = redisTemplate.opsForValue().get(key);
return clazz.cast(value);
}
// 使用下界通配符写入缓存
public void setCache(String key, super Serializable> value) {
redisTemplate.opsForValue().set(key, value);
}
综合示例:
import java.util.ArrayList;
import java.util.List;
public class Demo {
//实体类
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog is eating");
}
}
class Husky extends Dog {
@Override
void eat() {
System.out.println("Husky is eating");
}
}
/**
* 无界通配符
*/
// 只能读取元素,不能写入(除null外)
public static void printAllElements(List list) {
for (Object obj : list) {
System.out.println(obj);
}
// list.add("test"); // 编译错误!无法写入具体类型
list.add(null); // 唯一允许的写入操作
}
/**
* 上界通配符
*/
// 安全读取为Animal,但不能写入(生产者场景)
public static void processAnimals(List animals) {
for (Animal animal : animals) {
animal.eat();
}
// animals.add(new Dog()); // 编译错误!无法确定具体子类型
}
/**
* 下界通配符
*/
// 安全写入Dog,读取需要强制转换(消费者场景)
public static void addDogs(Listsuper Dog> dogList) {
dogList.add(new Dog());
dogList.add(new Husky()); // Husky是Dog子类
// dogList.add(new Animal()); // 编译错误!Animal不是Dog的超类
Object obj = dogList.get(0); // 读取只能为Object
if (obj instanceof Dog) {
Dog dog = (Dog) obj; // 需要显式类型转换
}
}
public static void main(String[] args) {
// 测试无界通配符
List strings = List.of("A", "B", "C");
printAllElements(strings);
List integers = List.of(1, 2, 3);
printAllElements(integers);
// 测试上界通配符
List dogs = new ArrayList<>();
dogs.add(new Dog());
processAnimals(dogs);
List huskies = new ArrayList<>();
huskies.add(new Husky());
processAnimals(huskies);
// 测试下界通配符
List animals = new ArrayList<>();
addDogs(animals);
System.out.println(animals);
List
总结
在村里上班的程序员
今天是在山上办公的第二天,现在时间是晚上的10点36分,刚刚写完今日份工作日报又刷10分钟手机的我决定写上一篇日记简单记下最近两天我的日常。
我的上班,是跟着国家法定节假日安排走的,前天初八,是我25年年后上班的第一天。
阿妮的假期还未结束,她决定回一趟娘家,由此,我在初八一大早——7点半到8点50分——先送阿妮进城,送到她的外公外婆家。接着,我在外公外婆家办公一天。
初八那天的午饭
大嘎嘎(外公)小嘎嘎(外婆)总是很欢迎我和阿妮的到来,午饭晚饭,我连连摇头不停摆手依然吃得很撑。
我干活的节奏,和年前是一样的:上午9点到12点多些,下午2点到6点7点,晚上8点9点到10点更多些,都是我的干活——单纯指做工作上的事情——时间。
阿妮晚上约了她的初中同学一起聚餐,我带上电脑跟着大家一起。烧烤桌上,对着电脑试用VS Code新插件Cline的我,是没有感受到心理压力的;烧烤桌上的干活时间,我是专注的。
--
初九一大早,我从县城回家回到山上,带着两个帽子和几个包子。帽子是初八晚上一起逛楼下街道时阿妮买给我父亲母亲的,包子,是我自己想吃再给父亲母亲带着的。
初九是个很好日子,返程路上,我碰见的接亲队伍有4个。
初九的天气,很有些特色,上午飘雪,下午大太阳,晚上很有些冷。我在山上的上班,上午烤电火,下午太阳下干活,晚上的干活,有一部分是洗漱完毕躺床上完成的。
我的办公桌椅
昨天的在家办公体验不错,但我需要对自己诚实些,当时我的内心,有两个小小担忧存在。
第一个关于当前的环境,下午父母外出吃酒只留我一个人在家,我家近处只有邻居一户。到了夜晚,整个世界很安静,安静到我能听见自己的呼吸,我的一个小小动作都能发出很大声响,我承认自己理智相信科学,但同时我也看过许多志怪故事。对的,晚上一个人在家,我很有些害怕。
第二个是工作内容,我总觉得自己近来的效率并没有很高。其实不太对,其实效率应该是ok的,不ok的是我关于AI技术的知识储备太少,而我并没有给自己一个好的计划去每天进步一点点。emmm~其实还是有些不太对,这个概念有些大,更准确的是,我感觉自己在有些工作内容上,还可以做的好一些。
晚上11点又过10分钟,背完单词的我睡下,脑子中那轻微恐惧并不散去,直到深睡。
很神奇的是,今天早上6点多醒来,那对于安静的害怕,一点都不剩下。
--
整个过年前后,我都感觉自己很忙,忙到已经坚持周更四年多的公众号都不能在周日及时更新。今天即便晚睡也想要写上一篇日记的情绪,来源于下午6点发生的一件,一件让我感觉有些窘迫的事情。
伯娘进门问我:”你们作料放啊哪儿的呢?“
”你老汉喊我来给你煮一碗面条。我给他说,叫你到我们那里去吃,你老汉儿说你走不开哈,那我就来给你煮面条了啊。“
当时我真的很窘迫,感觉很有些尴尬,而同时内心,又会有些开心情绪蔓延。
我是很想给自己找一个借口的:我以为父母吃酒,会在下午三点左右回家。我的早餐,是昨天剩下的两个肉包;我的午餐,是一碗加了母亲自制麻糖的泡阴米子,再加一个橙子与一个苹果。我认为肉包、橙子与苹果,是可以撑到母亲回家的。
工作日,我真的不想做饭。
即便并不很饿,但我确实有让阿雪对母亲说:“你再不回家,你的大娃娃和你的喂的猪,都要饿趴了哦。”
很是粘人的狗子
对的,今天干活间隙,我有喂猪——猪食当然是母亲提前煮好的,遛狗——我只需要前面走着,狗自然在后面跟着。
伯娘烧火又煮面,花半小时。
我为面条,拍了一张好看照片。
好吃的清汤面与好看的照片
时间已经是晚上的11点20分,且快快睡觉。
今晚,我一点都不害怕。
来源:juejin.cn/post/7468970428918906906
入职第一天,看了公司代码,牛马沉默了
入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。
打开代码发现问题不断
- 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置
一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为
prop_c.setProperty(key, value);
value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
- 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
role.haveRole("ADMIN_USE")
- 日志打印居然sout和log混合双打
先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;
4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;
5.随意更改生产数据库,出不出问题全靠开发的职业素养;
6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上
<type>pom
来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教
以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;
那有什么优点呢:
- 不用太怎么写文档
- 束缚很小
- 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)
解决之道
怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar &
来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,
其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;
我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!
来源:juejin.cn/post/7371986999164928010
React:我做出了一个违背祖训的决定!
React 的 useEffect
,大家都熟吧?这玩意儿就像个万金油,啥副作用都能往里塞:取数据、搞订阅、手动操作 DOM……反正渲染完了,它帮你擦屁股。它帮你擦屁股。
但是!React 团队最近搞了个大新闻,他们居然要对 useEffect
动刀子了!而且,这次的改动,用他们的话说,简直是——“违背祖训”!“违背祖训”!
useEffect
要变身?实验性 CRUD 支持来了!
新的 useEffect
签名,整合了以前一个实验性的 Hook useResourceEffect
的功能,现在长这样:
function useEffect(
create: (() => (() => void) | void) | (() => {...} | void | null),
createDeps: Array<mixed> | void | null,
update?: ((resource: {...} | void | null) => void) | void,
updateDeps?: Array<mixed> | void | null,
destroy?: ((resource: {...} | void | null) => void) | void,
): void
是不是看得一脸懵逼?别慌,我来给你翻译翻译。
以前的 useEffect
,创建和清理都挤在一个函数里,跟两口子似的,难舍难分。举个栗子:
useEffect(() => {
// 创建阶段:发起请求
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => setData(data));
// 清理阶段:取消请求
return () => {
controller.abort();
};
}, [someDependency]);
看到了吧?创建(发起请求)和清理(取消请求)都得写在一个函数里。
现在好了,React 团队直接把它们拆散了!新签名里,创建、更新、销毁,各司其职,清清楚楚:
create
: 专门用来造东西(比如,发个请求,整个订阅)。createDeps
:create
的跟屁虫,它们一变,create
就得重新执行。update
(可选): 想更新?找它!它会拿着create
造出来的东西,给你更新。updateDeps
(可选):update
的小弟,它们一变,update
就得带着老东西,重新来过。destroy
: 可选的销毁时候的回调。
“祖宗之法不可变”?React:我就变!
自从 Hook 在 2016 年推出,到现在已经九年了!九年啊!“组合优于继承”、“函数式编程”,这些 React 的“祖训”,各路大神、大V,哪个没给你讲过几百遍?哪个没给你讲过几百遍?
useEffect
把创建和清理揉在一起,也算是“组合”的一种体现,深入人心。可现在呢?React 团队居然亲手把它拆了!这……这简直是自己打自己的脸啊!
不过,话说回来,这种拆分,对于那些复杂的副作用,确实更清晰、更好管理。以前,你可能得在一个 useEffect
里写一堆 if...else
,现在,你可以把它们放到不同的阶段,代码更清爽,逻辑更分明。
注意!前方高能预警!
这个 CRUD 功能,现在还是个“试验品”,React 团队还没打算把它放出来。你要是头铁,非要试试,记得先去把 feature flag 打开。不然,你会看到这个:
useEffect CRUD overload is not enabled in this build of React.
重要的事情说三遍:这都是猜的!猜的!猜的!猜的!猜的!猜的!
现在,关于这个新特性,React 团队还没放出任何官方文档或者 RFC。所以,这篇文章,你看看就好,别太当真。它就是基于代码瞎猜的。等官方消息出来了,咱们再好好研究!
来源:juejin.cn/post/7470819965014474771
为什么程序员痴迷于错误信息上报?
前言
上一篇已经聊过日志上报的调度原理,讲述如何处理日志上报堆积、上报失败以及上报优化等方案。
从上家公司开始,监控就由我们组身强体壮的同事来负责,而我只能开发Admin
和H5
;经过一系列焦虑的面试后,咸鱼翻身,这辈子我也做上监控了。千万不要以为我是因为监控的重要性才这么执着,人往往得不到的东西才是最有吸引力的。
在写这篇文章时,我也在思考,为什么走到哪里都会有一群程序员喜欢封装监控呢?即使换个公司、换个组,依然可能需要有人来迭代监控。嗯,话不多说,先点关注,正文开始。
错误监控的核心价值
如果让你封装一个前端监控,你会怎么设计监控的上报优先级?
对于一个网页来说,能否带给用户好的体验,核心指标就是 白屏时长 和 FMP时长,这两项指标直接影响用户的 留存率 和 体验。
下面通过数据加强理解:
- 白屏时间 > 3秒 导致用户流失率上升47%
- 接口错误率 > 0.5% 造成订单转化率下降23%
- JS错误数 > 1/千次访问 预示着系统稳定性风险
设想一下,当你访问页面时白屏等待了3秒,并且页面没有骨架屏或者Loading态时,你会不会觉得这个页面挂了?这时候如果我们的监控优先关注的是性能,可能用户已经退出了,我们的上报还没调用到。
在这个白屏等待的过程中,JS Error可能已经打印在控制台了,接口可能已经返回了错误信息,但是程序员却毫无感知。
优先上报错误信息,本质是为了提升生产环境的错误响应速度、减少生产环境的损失、提高上线流程的规范。以下是错误响应的黄金时间轴:
时间窗口 | 响应动作 | 业务影响 |
---|---|---|
< 1分钟 | 自动熔断异常接口 | 避免错误扩散 |
1-5分钟 | 触发告警通知值班人员 | 降低MTTR(平均修复时间) |
>5分钟 | 生成故障诊断报告 | 优化事后复盘流程 |
重要章节
一:错误类型,你需要关注的五大场景
技术本质:任何错误收集系统都需要先明确错误边界。前端错误主要分为两类: 显性错误(直接阻断执行)和 隐性错误(资源加载、异步异常等)。
// 显性错误(同步执行阶段)
function criticalFunction() {
undefinedVariable.access(); // ReferenceError
}
// 隐性错误(异步场景)
fetchData().then(() => {
invalidJSON.parse(); // 异步代码中的错误
});
关键分类: 通过错误本质将前端常见错误分为5种类型,图示如下。
- 语法层错误(SyntaxError)
ESLint 可拦截,但运行时需注意动态语法(如eval
,这个用法不推荐)。 - 运行时异常
错误的时机场景大部分是在页面渲染完成后,用户对页面发生交互行为,触发JS执行异常。以下是模拟报错的一个例子,用于学习。 // 典型场景 element.addEventListener('click', () => { throw new Error('Event handler crash'); }); - 资源加载失败
常见的资源比如图片、JS脚本、字体文件、外链引入的三方依赖等。我们可以通过全局监听处理,比如使用document.addEventListener('error', handler, true)
来捕获资源加载失败的情况。但需要注意以下几点:
、
uni-app初体验,如何实现一个外呼APP
起因
2024年3月31日,我被公司裁员了。
2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。
2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回上海开始找工作。结果环境比预想的还要差啊,以前简历放开就有人找,现在每天投个几十封都是石沉大海。。。
2024年4月15日,有个好朋友找我,想让我给他们公司开发一个“拨号APP”(主要原因其实是这个好哥们想让我多个赚钱门路😌),主要的功能就是在他们的系统上点击一个“拨号”按钮,然后员工的工作手机上就自动拨打这个号码。
可行性分析
涉及到的修改:
- 系统前后端
- 拨号功能的APP
拿到这个需求之后,我并没有直接拒绝或者同意,而是先让他把他公司那边的的源代码发我了一份,大致看了一下使用的框架,然后找一些后端的朋友看有没人有人一起接这个单子;而我自己则是要先看下能否实现APP的功能(因为我以前从来没有做过APP!!!)。
我们各自看过自己的东西,然后又沟通了一番简单的实现过程后达成了一致,搞!
因为我这边之前的技术栈一直是VUE,所以决定使用uni-app实现,主要还是因为它的上手难度会低很多。
第一版
需求分析
虽说主体的功能是拨号,但其实是隐含很多辅助性需求的,比如拨号日志、通时通次统计、通话录音、录音上传、后台运行,另外除了这些外还有额外的例如权限校验、权限引导、获取手机号、获取拨号状态等功能需要实现。
但是第一次预算给的并不高,要把这些全部实现显然不可能。因此只能简化实现功能实现。
- 拨号APP
- 权限校验
- 实现部分(拨号、录音、文件读写)
- ❌权限引导
- 查询当前手机号
- 直接使用input表单,由用户输入
- 查询当前手机号的拨号任务
- 因为后端没有socket,使用setTimeout模拟轮询实现。
- 拨号、录音、监测拨号状态
- 根据官网API和一些安卓原生实现
- 更新任务状态
- 告诉后端拨号完成
- ❌通话录音上传
- ❌通话日志上传
- ❌本地通时通次统计
- 程序运行日志
- 其他
- 增加开始工作、开启录音的状态切换
- 兼容性,只兼容安卓手机即可
- 权限校验
基础设计
一个input框来输入用户手机号,一个开始工作的switch,一个开启录音的切换。用户输入手机号,点击开始工作后开启轮询,轮询到拨号任务后就拨号同时录音,同时监听拨号状态,当挂断后结束录音、更新任务状态,并开启新一轮的轮询。
开干
虽然本人从未开发过APP,但本着撸起袖子就是干的原则,直接打开了uni-app的官网就准备开怼。
1、下载 HbuilderX。
2、新建项目,直接选择了默认模板。
3、清空 Hello页面,修改文件名,配置路由。
4、在vue文件里写主要的功能实现,并增加 Http.js
、Record.js
、PhoneCall.js
、Power.js
来实现对应的模块功能。
⚠️关于测试和打包
运行测试
在 HbuilderX 中点击“运行-运行到手机或模拟器-运行到Android APP基座”会打开一个界面,让你选择运行到那个设备。这是你有两种选择:
- 把你手机通过USB与电脑连接,然后刷新列表就可以直接运行了。
- 很遗憾,可能是苹果电脑与安卓手机的原因,插上后检测不出设备😭。。。
- 安装Android Studio,然后通过运行内置的模拟器来供代码运行测试。
- 这种很麻烦,要下载很久,且感觉测试效果并不好,最好还是用windows电脑连接手机的方法测试。
关于自定义基座和标准基座的差别,如果你没有买插件的话,直接使用基准插座就好。如果你要使用自定义基座,就首先要点击上图中的制作自定义基座,然后再切换到自定义基座执行。
但是不知道为什么,我这里一直显示安装自定义基座失败。。。
打包测试
除了以上运行测试的方法外,你还有一种更粗暴的测试方法,那就是打包成APP直接在手机上安装测试。
点击“发行-原生APP 云打包”,会生成一个APK文件,然后就可以发送到手机上安装测试。不过每天打包的次数有限,超过次数需要购买额外的打包服务或者等第二天打包。
我最终就是这样搞得,真的我哭死,我可能就是盲调的命,好多项目都是盲调的。
另外,在打包之前我们首先要配置manifest.json
,里面包含了APP的很多信息。比较重要的一个是AppId,一个是App权限配置。参考uni-app 权限配置、Android官方权限常量文档。以下是拨号所需的一些权限:
// 录制音频
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 修改音频设置
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
// 照相机
<uses-permission android:name="android.permission.CAMERA" />
// 写入外部存储
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// 读取外部存储
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
// 读取电话号码
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
// 拨打电话
<uses-permission android:name="android.permission.CALL_PHONE" />
// 呼叫特权
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
// 通话状态
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
// 读取拨号日志
<uses-permission android:name="android.permission.READ_CALL_LOG" />
// 写入拨号日志
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
// 读取联系人
<uses-permission android:name="android.permission.READ_CONTACTS" />
// 写入联系人
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
// 读取SMS?
<uses-permission android:name="android.permission.READ_SMS" />
// 写入设置
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
// 唤醒锁定?
<uses-permission android:name="android.permission.WAKE_LOCK" />
// 系统告警窗口?
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// 接受完整的引导?
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
⚠️权限配置这个搞了很长时间,即便现在有些权限还是不太清楚,也不知道是不是有哪些权限没有配置上。。。
⚠️权限校验
1、安卓 1
好像除了这样的写法还可以写"scope.record"
或者permission.CALL_PHONE
。
permision.requestAndroidPermission("android.permission.CALL_PHONE").then(res => {
// 1 获得权限 2 本次拒绝 -1 永久拒绝
});
2、安卓 2
plus.android.requestPermissions(["android.permission.CALL_PHONE"], e => {
// e.granted 获得权限
// e.deniedPresent 本次拒绝
// e.deniedAlways 永久拒绝
});
3、uni-app
这个我没测试,AI给的,我没有用这种方法。有一说一,百度AI做的不咋地。
// 检查权限
uni.hasPermission({
permission: 'makePhoneCall',
success() {
console.log('已经获得拨号权限');
},
fail() {
// 示例:请求权限
uni.authorize({
scope: 'scope.makePhoneCall',
success() {
console.log('已经获得授权');
},
fail() {
console.log('用户拒绝授权');
// 引导用户到设置中开启权限
uni.showModal({
title: '提示',
content: '请在系统设置中打开拨号权限',
success: function(res) {
if (res.confirm) {
// 引导用户到设置页
uni.openSetting();
}
}
});
}
});
}
});
✅拨号
三种方法都可以实现拨号功能,只要有权限,之所以找了三种是为了实现APP在后台的情况下拨号的目的,做了N多测试,甚至到后面搞了一份原生插件的代码不过插件加载当时没搞懂就放弃了,不过到后面才发现原来后台拨号出现问题的原因不在这里,,具体原因看后面。
另外获取当前设备平台可以使用let platform = uni.getSystemInfoSync().platform;
,我这里只需要兼容固定机型。
1、uni-app API
uni.makePhoneCall({
phoneNumber: phone,
success: () => {
log(`成功拨打电话${phone}`);
},
fail: (err) => {
log(`拨打电话失败! ${err}`);
}
});
2、Android
plus.device.dial(phone, false);
3、Android 原生
写这个的时候有个小插曲,当时已经凌晨了,再加上我没有复制,是一个个单词敲的,结果竟然敲错了一个单词,测了好几遍都没有成功。。。还在想到底哪里错了,后来核对一遍才发现😭,control cv才是王道啊。
// Android
function PhoneCallAndroid(phone) {
if (!plus || !plus.android) return;
// 导入Activity、Intent类
var Intent = plus.android.importClass("android.content.Intent");
var Uri = plus.android.importClass("android.net.Uri");
// 获取主Activity对象的实例
var main = plus.android.runtimeMainActivity();
// 创建Intent
var uri = Uri.parse("tel:" + phone); // 这里可修改电话号码
var call = new Intent("android.intent.action.CALL", uri);
// 调用startActivity方法拨打电话
main.startActivity(call);
}
✅拨号状态查询
第一版用的就是这个获取状态的代码,有三种状态。第二版的时候又换了一种,因为要增加呼入、呼出、挂断、未接等状态的判断。
export function getCallStatus(callback) {
if (!plus || !plus.android) return;
let maintest = plus.android.runtimeMainActivity();
let Contexttest = plus.android.importClass("android.content.Context");
let telephonyManager = plus.android.importClass("android.telephony.TelephonyManager");
let telManager = plus.android.runtimeMainActivity().getSystemService(Contexttest.TELEPHONY_SERVICE);
let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
onReceive: (Contexttest, intent) => {
plus.android.importClass(intent);
let phoneStatus = telManager.getCallState();
callback && callback(phoneStatus);
//电话状态 0->空闲状态 1->振铃状态 2->通话存在
}
});
let IntentFilter = plus.android.importClass("android.content.IntentFilter");
let filter = new IntentFilter();
filter.addAction(telephonyManager.ACTION_PHONE_STATE_CHANGED);
maintest.registerReceiver(receiver, filter);
}
⚠️录音
录音功能这个其实没啥,都是官网的API,无非是简单的处理一些东西。但是这里有一个大坑!
一坑
就是像通话录音这种涉及到的隐私权限很高,正常的这种录音是在通话过程中是不被允许的。
二坑
后来一次偶然的想法,在接通之后再开启录音,发现就可以录音了。
但随之而来的是第二个坑,那就是虽然录音了,但是当播放的时候发现没有任何的声音,还是因为保护隐私的原因,我当时还脱离代码专门试了试手机自带的录音器来在通话时录音,发现也不行。由此也发现uni的录音本身也是用的手机录音器的功能。
三坑
虽然没有声音,但是我还是试了下保存,然后就发现了第三个坑,那就是虽然获取了文件权限,但是现在手机给的读写权限都是在限定内的,录音所在的文件夹是无权访问的。。。
另辟蹊径
其实除了自己手动录音外还可以通过手机自带的通话录音来实现,然后只要手机去读取录音文件并找到对应的那个就可以了。思路是没啥问题,不过因为设置通话录音指引、获取录音文件都有问题,这一版本就没实现。
// 录音
var log = console.log,
recorder = null,
// innerAudioContext = null,
isRecording = false;
export function startRecording(logFun = console.log) {
if (!uni.getRecorderManager || !uni.getRecorderManager()) return logFun('不支持录音!');
log = logFun;
recorder = uni.getRecorderManager();
// innerAudioContext = uni.createInnerAudioContext();
// innerAudioContext.autoplay = true;
recorder.onStart(() => {
isRecording = true;
log(`录音已开始 ${new Date()}`);
});
recorder.onError((err) => {
log(`录音出错:${err}`);
console.log("录音出错:", err);
});
recorder.onInterruptionBegin(() => {
log(`检测到录音被来电中断...`);
});
recorder.onPause(() => {
log(`检测到录音被来电中断后尝试启动录音..`);
recorder.start({
duration: 10 * 60 * 1000,
});
});
recorder.start({
duration: 10 * 60 * 1000,
});
}
export function stopRecording() {
if (!recorder) return
recorder.onStop((res) => {
isRecording = false;
log(`录音已停止! ${new Date()}`); // :${res.tempFilePath}
// 处理录制的音频文件(例如,保存或上传)
// powerCheckSaveRecord(res.tempFilePath);
saveRecording(res.tempFilePath);
});
recorder.stop();
}
export function saveRecording(filePath) {
// 使用uni.saveFile API保存录音文件
log('开始保存录音文件');
uni.saveFile({
tempFilePath: filePath,
success(res) {
// 保存成功后,res.savedFilePath 为保存后的文件路径
log(`录音保存成功:${res.savedFilePath}`);
// 可以将res.savedFilePath保存到你的数据中,或者执行其他保存相关的操作
},
fail(err) {
log(`录音保存失败! ${err}`);
console.error("录音保存失败:", err);
},
});
}
运行日志
为了更好的测试,也为了能实时的看到执行的过程,需要一个日志,我这里就直接渲染了一个倒序的数组,数组中的每一项就是各个函数push的字符串输出。简单处理。。。。嘛。
联调、测试、交工
搞到最后,大概就交了个这么玩意,不过也没有办法,一是自己确实不熟悉APP开发,二是满共就给了两天的工时,中间做了大量的测试代码的工作,时间确实有点紧了。所幸最起码的功能没啥问题,也算是交付了。
第二版
2024年05月7日,老哥又找上我了,想让我们把他们的这套东西再给友商部署一套,顺便把这个APP再改一改,增加上通时通次的统计功能。同时也是谈合作,如果后面有其他的友商想用这套系统,他来谈,我们来实施,达成一个长期合作关系。
我仔细想了想,觉得这是个机会,这块东西的市场需求也一直有,且自己现在失业在家也有时间,就想着把这个简单的功能打磨成一个像样的产品。也算是做一次尝试。
需求分析
- ✅拨号APP
- 登录
- uni-id实现
- 权限校验
- 拨号权限、文件权限、自带通话录音配置
- 权限引导
- 文件权限引导
- 通话录音配置引导
- 获取手机号权限配置引导
- 后台运行权限配置引导
- 当前兼容机型说明
- 拨号
- 获取手机号
- 是否双卡校验
- 直接读取手机卡槽中的手机号码
- 如果用户不会设置权限兼容直接input框输入
- 拨号
- 全局拨号状态监控注册、取消
- 支持呼入、呼出、通话中、来电未接或挂断、去电未接或挂断
- 获取手机号
- 录音
- 读取录音文件列表
- 支持全部或按时间查询
- 播放录音
- ❌上传录音文件到云端
- 读取录音文件列表
- 通时通次统计
- 云端数据根据上面状态监控获取并上传
- 云端另写一套页面
- 本地数据读取本机的通话日志并整理统计
- 支持按时间查询
- 支持呼入、呼出、总计的通话次数、通话时间、接通率、有效率等
- 云端数据根据上面状态监控获取并上传
- 其他
- 优化日志显示形式
- 封装了一个类似聊天框的组件,支持字符串、Html、插槽三种显示模式
- 在上个组件的基础上实现权限校验和权限引导
- 在上两个组件的基础上实现主页面逻辑功能
- 增加了拨号测试、远端连接测试
- 修改了APP名称和图标
- 打包时增加了自有证书
- 优化日志显示形式
- 登录
中间遇到并解决的一些问题
关于框架模板
这次重构我使用了uni中uni-starter + uni-admin 项目模板。整体倒还没啥,这俩配合还挺好的,就只是刚开始不知道还要配置东西一直没有启动起来。
建立完项目之后还要进uniCloud/cloudfunctions/common/uni-config-center/uni-id
配置一个JSON文件来约定用户系统的一些配置。
打包的时候也要在manifest.json
将部分APP模块配置进去。
还搞了挺久的,半天才查出来。。
类聊天组件实现
- 设计
- 每个对话为一个无状态组件
- 一个图标、一个名称、一个白底的展示区域、一个白色三角
- 内容区域通过类型判断如何渲染
- 根据前后两条数据时间差判断是否显示灰色时间
- 参数
- ID、名称、图标、时间、内容、内容类型等
- 样式
- 根据左边右边区分发送接收方,给与不同的类名
- flex布局实现
样式实现这里,我才知道原来APP和H5的展示效果是完全不同的,个别地方需要写两套样式。
关于后台运行
这个是除了录音最让我头疼的问题了,我想了很多实现方案,也查询了很多相关的知识,但依旧没效果。总体来说有以下几种思路。
- 通过寻找某个权限和引导(试图寻找到底是哪个权限控制的)
- 通过不停的访问位置信息
- 通过查找相应的插件、询问GPT、百度查询
- 通过程序切入后台之后,在屏幕上留个悬浮框(参考游戏脚本的做法)
- 通过切入后台后,发送消息实现(没测试)
测试了不知道多少遍,最终在一次无意中,终于发现了如何实现后台拨号,并且在之后看到后台情况下拨号状态异常,然后又查询了应用权限申请记录,也终于知道,归根到底能否后台运行还是权限的问题。
关于通话状态、通话记录中的类型
这个倒还好,就是测试的问题,知道了上面为啥异常的情况下,多做几次测试,就能知道对应的都是什么状态了。
通话状态:呼入振铃、通话中(呼入呼出)、通话挂断(呼入呼出)、来电未接或拒绝、去电未接或拒接。
通话日志:呼入、呼出、未接、语音邮件、拒接
交付
总体上来说还过得去,相比于上次简陋的东西,最起码有了一点APP的样子,基本上该有的功能也基本都已经实现了,美中不足的一点是下面的图标没有找到合适的替换,然后录音上传的功能暂未实现,不过这个也好实现了。
后面的计划
- 把图标改好
- 把录音文件是否已上传、录音上传功能做好
- 把APP的关于页面加上,对接方法、使用方法和视频、问题咨询等等
- 原本通话任务、通时通次这些是放在一个PHP后端的,对接较麻烦。要用云函数再实现一遍,然后对外暴露几个接口,这样任何一个系统都可以对接这个APP,而我也可以通过控制云空间的跨域配置来开放权限
- 把数据留在这边之后,就可以再把uni-admin上加几个页面,并且绑定到阿里云的云函数前端网页托管上去
- 如果有可能的话,上架应用商店,增加上一些广告或者换量联盟之类的东西
- 后台运行时,屏幕上加个悬浮图标,来电时能显示个振铃啥的
- 增加拨号前的校验,对接平台,对于经常拉黑电销的客户号码进行过滤
大致的想法就这些了,如果这个产品能继续卖下去,我就会不断的完善它。
最后
现在的行情真的是不好啊,不知道有没有大哥给个内推的机会,本人大专计算专业、6.5年Vue经验(专精后台管理、监控大屏方向,其他新方向愿意尝试),多个0-1-2项目经验,跨多个领域如人员管理、项目管理、产品设计、软件测试、数据爬虫、NodeJS、流程规范等等方面均有了解,工作稳定不经常跳,求路过的大哥给个内推机会把!
😂被举报标题党了,换个名字。
来源:juejin.cn/post/7368421971384860684
无构建和打包,浏览器直接吃上Vue全家桶?
Vue 是易学易用,性能出色,适用场景丰富的 Web 前端渐进式框架;常用来开发单页面应用。
主流开发方式-编译打包
用脚手架工具 create-vue 可以快速通过 npm create vue@latest
命令 来定制化新建一个 Vite 驱动的 Vue 单页面应用项目。
这是常规的使用 Vue 的方式。当然也可以从 Vite 那边入手。
我们新建一个项目 vue-demo
来试试,选上 Vue-Router 和 Pinia, 其余的不选:
访问 http://localhost:5173/
, 正常打开:
初始化的模板,用上了 Vue-Router,有两个路由, '/'
, '/about'
;那 Pinia 呢?可以看到依赖已经安装了引入了,给了一个 demo 了
我们来用一下 Pinia, 就在about路由组件里面用下吧:
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
const { increment } = store
</script>
<template>
<div class="about">
<h1>{{ count }}</h1>
<h1>{{ doubleCount }}</h1>
<button @click="increment">+1</button>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
这就是 Vue + Vue-Router + Pinia 全家桶在 打包构建工具 Vite 驱动下的开发方式。
Vite 开发阶段不打包,但会预构建项目的依赖,需要哪个资源会在请求的时候编译,而项目上线则需要打包。
完美对吧!但你有没有注意到,官网除了介绍这种方式,还介绍了 “Using Vue from CDN”:
也就是说,可以 HTML 文件里面直接用上 Vue 的对吧?那我还想要 Vue-Router、 Pinia、Axios、 Element-Plus 呢?怎么全部直接用,而不是通过npm install xxx 在需要构建打包的项目里面用?
如何直接吃上 Vue 全家桶
我们将会从一个 HTML 文件开始,用浏览器原生的 JavaScript modules 来引入 Vue 、引入 Vue-Router,、引入 Pinia、引入 Axios, 并且构建一个类似工程化的目录结构,但不需要打包,JS 是 ES modules 语法;而项目的运行,只需要用npx serve -s
在当前项目目录起一个静态文件服务器,然后浏览器打开即可。
HTML 文件引入 Vue
找个空文件夹,我们新建一个 index.html
:
把 Vue 文档代码复制过来:
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from 'vue'
createApp({
setup() {
const message = ref('Hello Vue!')
return {
message
}
}
}).mount('#app')
</script>
当前目录下执行下npx serve -s
打开看看
没问题。
但是经常写页面的朋友都知道,肯定得拆分组件,不然全写一个页面不好维护,这点官网也给了例子:
照猫画虎,我们拆分一下:
新建 src/app.js
文件,如下内容:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `<div @click="count++">Count is: {{ count }}</div>`
}
然后在 index.html
引入:
<script type="module">
import { createApp, ref } from 'vue'
import App from './src/app.js'
createApp(App).mount('#app')
</script>
刷新下页面看看:
Vue 成功引入并使用了。但还有遗憾,就是app.js
"组件"的 template 部分是字符串,没有高亮,不利于区分:
关于这点,官网也说了,如果你使用 VS Code, 那你可以安装插件 es6-string-html
,用 /*html*/实现高亮:
我们来试试看:
至此,我们可以相对舒服地使用 Vue 进行组件开发了。
HTML 文件引入、Vue 集成 Vue-Router
项目如果有不同的页面,就需要 Vue-Router 了, Vue-Router官网同样有网页直接引入的介绍:
我们来试一下,先在 Import Maps 添加 vue-router
的引入:
然后写个使用 Vue-Router 的demo: 新建两个路由组件:src/view/home.js
, src/view/about.js
, 在 HTML 文件中引入:
src/app.js
作为根组件,放个 RouterLink、RouterView 组件:
然后我们刷新下页面,看看是否正常生效:
很遗憾,没有生效,控制台报错了:
意思是声明的 vue-router 模块,没有导出我们引用到的方法 createRouter
;这说明,Vue-Router 打包的默认文件,并不是默认的 ES Modules 方式,我们得找找对应的构建产物文件才行;
这对比 Vue 的引入,Vue 引入的是构建产物中的 “esm-browser” 后缀的文件:
那么斗胆猜测下,Vue-Router 同样也有 esm 的构建产物,我们引入下该文件,应该就可以了。
但是怎么知道 Vue-Router 的构建产物有哪些?难道去翻官方的构建配置吗?不用,我们找个 npm 项目,然后npm install vue-router
,在 node_mudules/xxx
翻看就知道了。
我们上面正好有个 vue-demo, 使用了 Vue-Router。我们看看:
我们改下 Import Maps 里面 vue-router
的映射:
刷新下页面看看:
还是有报错:
@vue/devtools-api
我们并没有引入,报了这个错,斗胆猜测是 vue-router 中使用的,该模块应该是属于外部模块,我们看看网络里面响应的文件验证下:
确实如此,那么 Import Maps 也补充下引入这个模块,我们先翻看该模块的 npm 包看看,确定下路径:
Import Maps 里面引入:
再刷新下页面试试:
至此,我们成功地在 HTML 文件中引入,在 Vue 中集成了 Vue-Router。
下面我们来看 Pinia 的
但在这之前,我们来整理下现在的目录划分吧。
新建 src/router/index.js
文件,将路由相关的逻辑放到这里:
在index.html
引入 router:
然后type=module
的 script 里面的内容也可以抽离出来到单独的文件里面:
新建 main.js
文件,将内容搬过去并引入:
页面刷新下,正常运行。
HTML 文件引入、Vue 集成 Pinia
有了上面引入 Vue-Router 的经验,我们就知道了,引入其他的库也是相同的套路。我们去之前的脚手架工具生成的项目 vue-demo 的依赖里面翻看一下,Pinia 包的构建产物是如何的,然后在现在的 esm 项目里面引入吧:
我们在项目里面使用一下 Pinia, 在main.js
里面引入 Pinia:
import { createApp, ref } from 'vue'
import App from './src/app.js'
import router from './src/router/index.js'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
app.use(router)
.mount('#app')
新建 src/stores/useCounterStore.js
文件,填入如下内容:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export default defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
return { count, doubleCount, increment }
})
即如下:
之后我们在 src/view/home.js
组件里面使用一下这个 store:
import useCounterStore from "../stores/useCounterStore.js"
import { storeToRefs } from 'pinia'
export default {
setup() {
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
const { increment } = store
return { count, doubleCount, increment }
},
template: /*html*/`<div>
<h1>Home</h1>
<p>{{ count }}</p>
<p>{{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>`
}
我们刷新页面看看,报错了, 缺了一个模块 vue-demi
我们确认一下,在响应的 Pinia 库中确实有对这模块的引入
那么我们也引入一下吧,我们翻看需要的库的文件路径,注意这里的 esm 模块是 .mjs 后缀文件
再刷新看看:
至此,我们就在 HTML 文件中直接引入 Vue, 集成了 Vue-Router、Pinia。
HTML 文件引入 Axios
接下来,我们来看看网络请求库 Axios。
网络请求, 原生的 fetch API 可以胜任,但是对于项目的网络请求,最好有统一的拦截器处理,而 Axios 已经有了一套可行的方案,所以我项目开发一般会用 Axios。本节不讲Axios封装,只介绍在原生 HTML 文件中直接引入和使用 Axios。
要以 ESM 方式引入 Axios,我们得知道 Axios esm 模块的路径。我们在上述的工程化项目 vue-demo 中安装和查看路径
我们在 Import Maps 添加引入
我们添加 src/mock/test.json
文件,里面存放JSON 数据,然后用 axios 请求试试看:
我们在 src/view/about.js
组件里面使用一下 Axios 来获取 mock 数据,并且显示到页面上,代码如下:
import axios from 'axios'
import { ref } from 'vue'
export default {
setup() {
const mockData = ref(null)
axios.get('/src/mock/test.json').then(res => {
mockData.value = res.data
})
return { mockData }
},
template: /*html*/`<div>
<h1>About</h1>
<pre>
{{ mockData }}
</pre>
</div>`
}
刷新看看:
没有问题,可以正常使用,至于 Axios 如何封装得适合项目,这里就不展开了。
CSS 样式解决方案
但目前为止,我们几乎没有写样式,但这种纯 ESM 项目,我们应该怎么写样式呢?
用打包构建工具的项目,一般都有 CSS 的预构建处理工具,比如 Less, Scss等;但实际开发中,大部分就使用一下嵌套而已;
现在最新的浏览器已经支持 CSS 嵌套了:
还有 CSS 模块化的兼容性也完全没问题:
那么此 ESM 项目我这里给一个建议的方案,读者欢迎评论区留言提供其他方案。
新建 src/style/index.css
文件,键入如下样式:
body {
background-color: aqua;
}
在 index.html
文件中引入该样式:
刷新看看是否生效
项目中该怎么进行组件的 CSS 样式隔离呢?这里就建议 采用 ESM 的类名样式方案咯,这里不展开讲,只给一个样式目录参考。建议如下:
将样式放在 src/style
下面,按照组件的目录进行放置,然后在src/style/index.css
引入:
效果如下:
样式中,我使用了CSS模块化语法和嵌套语法,都生效了。
HTML 文件引入、Vue 集成 Element-Plus
最后,我们再引入组件库吧。我这里使用 Element-Plus
官网可以看到也是支持直接引入的,要注意的是得引入其样式
我们在上面工程化项目 vue-demo 里面安装下 Element-Plus 的 npm 包看看 esm 文件的位置(.mjs后缀文件一般就是esm模块):
在 index.html
文件里面引入样式,在 Import Maps 里面引入 element-plus:
然后在 main.js
里把所有 element-plus 组件注册为全局组件并在 src/view/home.js
使用下 Button 组件:
效果如下:
至此,我们在项目中集成了 Element-Plus 组件库了。
其他优化
以上所有的库,都可以在网络的响应里面,复制到本地,作为本地文件引入,这样加载速度更快,没有网络延迟问题。
总结
我们先按照 Vue 官方文档使用了常规的项目开发方式创建了一个项目。
然后我们提出了一个想法:能否直接在 HTML
文件中使用 Vue 及其全家桶?
答案是可行的,因为几乎所有的库都提供了 ESM 的构建文件,而现今的浏览器也都支持 ESM 模块化了。
我们也探讨和实践了 CSS 模块化 和 CSS 嵌套,用在了 demo 中作为 esm 项目的样式方案。
最后我们在项目中集成了 Element-Plus 组件库。
至此,我们可以点题了:无打包构建,浏览器确实能吃上 Vue 全家桶了。但这并不是说,可以在真实项目中这样使用,兼容性就不说了,还有项目的优化,一般得打包构建中做:比如 Tree Shaking、代码压缩等。但如果是一些小玩具项目,可以试试这么玩。无构建和打包,浏览器跑的代码就是你写的源码了。
本文示例代码地址:gitee.com/GumplinGo/1…
来源:juejin.cn/post/7399094428343959552
蔚来面试题:计算白屏时间
深入理解白屏时间及其优化策略
在前端性能优化中,白屏时间(First Paint Time)是一个非常重要的指标。它指的是从用户输入网址并按下回车键,到浏览器开始渲染页面内容的时间段。在这段时间内,用户看到的只是一个空白页面,因此白屏时间的长短直接影响了用户的体验。本文将详细探讨白屏时间的定义、影响因素、测量方法以及优化策略,并结合代码示例进行说明。
什么是白屏时间?
白屏时间是指从用户发起页面请求到浏览器首次开始渲染页面内容的时间。具体来说,白屏时间包括以下几个阶段:
- DNS解析:浏览器将域名解析为IP地址。
- 建立TCP连接:浏览器与服务器建立TCP连接(三次握手)。
- 发起HTTP请求:浏览器向服务器发送HTTP请求。
- 服务器响应:服务器处理请求并返回响应数据。
- 浏览器解析HTML:浏览器解析HTML文档并构建DOM树。
- 浏览器渲染页面:浏览器根据DOM树和CSSOM树生成渲染树,并开始渲染页面。
- 页面展示第一个标签:浏览器首次将页面内容渲染到屏幕上。
白屏时间的长短直接影响了用户对网站的第一印象。如果白屏时间过长,用户可能会感到不耐烦,甚至直接关闭页面。因此,优化白屏时间是前端性能优化的重要目标之一。
白屏时间的影响因素
白屏时间的长短受到多种因素的影响,主要包括以下几个方面:
- 网络性能:网络延迟、带宽、DNS解析时间等都会影响白屏时间。如果网络状况不佳,DNS解析和TCP连接建立的时间会变长,从而导致白屏时间增加。
- 服务器性能:服务器的响应速度、处理能力等也会影响白屏时间。如果服务器响应缓慢,浏览器需要等待更长的时间才能接收到HTML文档。
- 前端页面结构:HTML文档的大小、复杂度、外部资源的加载顺序等都会影响白屏时间。如果HTML文档过大或包含大量外部资源,浏览器需要更长的时间来解析和渲染页面。
- 浏览器性能:浏览器的渲染引擎性能、缓存机制等也会影响白屏时间。不同浏览器的渲染性能可能存在差异,导致白屏时间不同。
如何测量白屏时间?
测量白屏时间的方法有多种,下面介绍两种常用的方法:基于时间戳的方法和基于Performance API的方法。
方法一:基于时间戳的方法
在HTML文档的<head>
标签中插入JavaScript代码,记录页面开始加载的时间戳。然后在<head>
标签解析完成后,记录另一个时间戳。两者的差值即为白屏时间。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>白屏时间计算</title>
<script>
// 记录页面开始加载的时间
window.pageStartTime = Date.now();
</script>
<link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/ionicons/2.0.1/css/ionicons.min.css~tplv-t2oaga2asx-image.image">
<link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/asset/fw-icon/1.0.9/iconfont.css~tplv-t2oaga2asx-image.image">
<script>
// head 解析完成后,记录时间
window.firstPaint = Date.now();
console.log(`白屏时间:${firstPaint - pageStartTime}ms`);
</script>
</head>
<body>
<div class="container"></div>
</body>
</html>
方法二:基于Performance API的方法
使用Performance API可以更精确地测量白屏时间。Performance API提供了PerformanceObserver
接口,可以监听页面的首次绘制(first-paint
)事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/ionicons/2.0.1/css/ionicons.min.css~tplv-t2oaga2asx-image.image">
<link rel="stylesheet" href="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-assets/asset/fw-icon/1.0.9/iconfont.css~tplv-t2oaga2asx-image.image">
<!-- 只是为了让白屏时间更长一点 -->
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.13/vue.global.js"></script>
</head>
<body>
<h1>Hello, World!</h1>
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_61ae954a0c4c41dba37b189a20423722@000000_oswg66502oswg900oswg600_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_9e1df42e783841e79ff021cda5fc6ed4@000000_oswg41322oswg1026oswg435_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_0376475b9a6a4dcab3f7b06a1b339cfc@5888275_oswg287301oswg729oswg545_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_e3213623ab5c46da8a6f9c339e1bd781@5888275_oswg1251766oswg1080oswg810_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_919d4445116f4efda326f651619b4c69@5888275_oswg169476oswg598oswg622_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<img
src="https://img.36krcdn.com/hsossms/20250217/v2_0457ccbedb984e2897c6d94815954aae@5888275_oswg383406oswg544oswg648_img_000?x-oss-process=image/format,jpg/interlace,1"
alt="">
<script>
// 性能 观察器 观察者模式
const observer = new PerformanceObserver((list) => {
// 获取所有的 性能 指标
const entries = list.getEntries();
for(const entry of entries) {
// body 里的第一个 标签的渲染
// 'first-paint' 表示页面首次开始绘制的时间点,也就是白屏结束的时间点
if(entry.name === 'first-paint') {
const whiteScreenTime = entry.startTime;
console.log(`白屏时间:${whiteScreenTime}ms`);
}
}
})
// 首次绘制 first-paint
// 首次内容绘制 first-contentful-paint 事件
// observe 监听性能指标
// buffered 属性设置为 true,表示包含性能时间线缓冲区中已经记录的相关事件
// 这样即使在创建 PerformanceObserver 之前事件已经发生,也能被捕获到
observer.observe({ type: 'paint', buffered: true });
</script>
</body>
</html>
总结
白屏时间是前端性能优化中的一个重要指标,直接影响用户的体验。通过理解白屏时间的定义、影响因素以及测量方法,开发者可以有针对性地进行优化。
来源:juejin.cn/post/7475652009103032358
不得不安利的富文本编辑器,太赞了!
hello,大家好,我是徐小夕。之前和大家分享了很多可视化,零代码和前端工程化的最佳实践,最近也在迭代可视化文档知识引擎Nocode/WEP
。在研究文档编辑器的时候也发现了很多优秀的开源项目,从中吸取了很多先进的设计思想。
接下来就和大家分享一款由facebook开源的强大的富文本编辑器——Lexical。目前在github
上已有 17.7k star
。
github地址: https://github.com/facebook/lexical
往期精彩
Lexical 基本介绍
Lexical 是一个可扩展的 JavaScript 文本编辑器框架,聚焦于可靠性、可访问性和性能。旨在提供一流的开发人员体验,因此我们可以轻松地进行文档设计和构建功能。
结合高度可扩展的架构,Lexical 允许开发人员创建独特的文本编辑体验,功能可以二次扩展,比如支持多人协作,定制文本插件等。
demo演示
我们可以使用它实现类似 Nocode/WEP
文档引擎的编辑体验。
我们可以轻松的选中文本来设置文本样式:
同时还能对文本内容进行评论:
当然插入表格和代码等区块也是支持的:
接下来就和大家一起分享以下它的设计思路。
设计思想
Lexical 的核心是一个无依赖的文本编辑器框架,允许开发人员构建强大、简单和复杂的编辑器表面。Lexical 有几个值得探索的概念:
- 编辑器实例:编辑器实例是将所有内容连接在一起的核心。我们可以将一个 contentEditable DOM 元素附加到编辑器实例,并注册侦听器和命令。最重要的是,编辑器允许更新其 EditorState。我们可以使用 createEditor() API 创建编辑器实例,但是在使用框架绑定(如@lexical/react)时,通常不必担心,因为这会为我们自动处理。
- 编辑器状态:编辑器状态是表示要在 DOM 上显示的内容的底层数据模型。编辑器状态包含两部分:
- Lexical 节点树
- Lexical 选择对象
- 编辑器状态一旦创建就是不可变的,为了更新它,我们必须通过 editor.update(() => {...}) 来完成。但是,也可以使用节点变换或命令处理程序“挂钩”到现有更新中 - 这些处理程序作为现有更新工作流程的一部分被调用,以防止更新的级联/瀑布。我们还可以使用 editor.getEditorState() 检索当前编辑器状态。
- 编辑器状态也完全可序列化为 JSON,并可以使用 editor.parseEditorState() 轻松地将其序列化为编辑器。
- 读取和更新编辑器状态:当想要读取和/或更新 Lexical 节点树时,我们必须通过 editor.update(() => {...}) 来完成。也可以通过 editor.getEditorState().read(() => {...}) 对编辑器状态进行只读操作。
Lexical的设计模型如下:
这里为了大家更直观的了解它的使用,我分享一个相对完整的代码案例:
import {$getRoot, $getSelection, $createParagraphNode, $createTextNode, createEditor} from 'lexical';
// 第一步,创建编辑器实例
const config = {
namespace: 'MyEditor',
theme: {
...
},
onError: console.error
};
const editor = createEditor(config);
// 第二步,更新编辑器内容
editor.update(() => {
const root = $getRoot();
const selection = $getSelection();
// 创建段落节点
const paragraphNode = $createParagraphNode();
// 创建文本节点
const textNode = $createTextNode('Hello world');
// 添加文本节点到段落
paragraphNode.append(textNode);
// 插入元素
root.append(paragraphNode);
});
通过以上两步,我们就实现了文本编辑器的创建和更新,是不是非常简单?
如果大家对这款编辑器感兴趣,也欢迎在github上学习使用,也欢迎在留言区和我交流反馈。
github地址: https://github.com/facebook/lexical
最后
后续我还会持续迭代 Nocode/WEP
项目, 让它成为最好用的可视化 + AI知识库,同时也会持续迭代和分享H5-Dooring零代码搭建平台, 如果你也感兴趣,欢迎随时交流反馈。
往期精彩
来源:juejin.cn/post/7377662459921006629
为什么面试官在面试中都爱问 HTTPS ❓❓❓
尽管 HTTP
在我们的项目中应用已经很广泛了,然而 HTTP
并非只有好的一面,事物皆具有两面性,它也是有不足之处的。
HTTP 的不足之处主要有以下几个方面:
- 数据传输不加密:HTTP 传输的数据是明文的,任何人都可以在网络中监听并读取传输的数据。这意味着,如果通过 HTTP 传输的是敏感信息(如用户名、密码、银彳亍卡号等),就会容易被窃取。这就会导致数据泄露,影响用户隐私和安全。
- 数据容易被篡改:HTTP 不提供数据完整性保护,数据在传输过程中可以被中途篡改。恶意攻击者可以通过中间人攻击(Man-in-the-Middle, MITM)修改数据,导致用户接收到被篡改的内容,如篡改的文件、消息等。
- 缺乏身份验证:HTTP 协议本身无法验证客户端访问的是合法的服务器,可能会遭遇伪造网站或钓鱼网站。攻击者可以通过创建假网站诱导用户输入个人信息或执行恶意操作,造成信息泄露或财产损失。
- 容易遭受中间人攻击(MITM):由于 HTTP 协议的数据是明文传输的,攻击者能够通过中间人攻击拦截、读取、修改传输数据。攻击者可以截获会话内容,窃取敏感信息,甚至伪造响应返回给客户端,造成严重的安全隐患。如下图所示:
- 缺乏数据完整性保护:HTTP 协议本身没有内建的校验机制来验证数据是否在传输过程中被篡改。恶意攻击者可以修改数据,客户端无法判断是否收到被篡改的内容。
- 浏览器安全警告:许多现代浏览器已经将 HTTP 网站标记为“不安全”,并警告用户。HTTP 网站会影响用户信任,特别是在涉及电子商务、登录、支付等敏感操作时,用户会更加倾向于避免访问 HTTP 网站。
- 不支持 HTTP/2 特性:HTTP 协议(特别是 HTTP/1.x 版本)效率较低,无法充分利用现代网络的性能优势。比如,它存在队头阻塞(Head-of-Line Blocking)问题,多个请求必须按顺序处理。在大流量的网站或复杂的请求/响应场景下,HTTP 的性能较差,响应速度较慢。
- 搜索引擎优化(SEO)劣势:搜索引擎(如 Google)更倾向于优先排名 HTTPS 网站,HTTP 网站的排名可能会受到影响。如果一个网站仅使用 HTTP 协议,其搜索引擎排名可能会比使用 HTTPS 的网站低,从而减少网站的访问量。
什么是 HTTPS
为了解决上述存在的问题,就用到了 HTTPS
,实际上它也并发是应用层的一种新协议,只是 HTTP
通信接口部分用 SSL
和 TLS
协议代替而已。
在正常情况下,HTTP
直接和 TCP
通信,当使用 SSL
时,则演变成先和 SSL
通信,再由 SSL
和 TCP
通信了,换句话说,所谓的 HTTPS
实际上就是身披 SSL
协议这层外壳的 HTTP
。
在采用 SSL
后,HTTP
就拥有了 HTTPS
的加密、证书和完整性保护这些功能。
相互交换秘钥的公开密钥加密技术
在对 SSL 进行讲解之前,我们先来了解一下加密方法。SSL 采用一种叫做公开密钥加密的加密处理方式。
在近代的加密方法中,加密算法是公开的,而密钥是保密的,通过这种方式得以保持加密方法的安全性。加密和揭秘都会用到密钥,没有密钥就无法对密码解密,反过来说,任何人只要持有密钥就能解密了。
对称密钥加密(共享密钥加密)
加密和揭秘同用一个密钥的方式称为共享密钥加密,也被叫做对称密钥加密:
以共享密钥方式加密时必须将密钥也发给对方,这是一个挑战,因为在传输密钥本身也需要保证其安全性。如果密钥在传输过程中被截获或篡改,通信的机密性将会被威胁。
在使用共享密钥的通信中,通信双方必须共享同一个密钥,并且双方都必须信任这个密钥的安全性。如果这个密钥在任何一方处被泄露或公开,通信的机密性将无法得到保证。因此,确保双方对共享密钥的安全性保持信任是至关重要的。
我们先来看一个对称加密的例子,假设用户 A 想给用户 B 发送一条加密信息:
- 用户 A 和用户 B 事先共享一个密钥
K
。 - 用户 A 使用密钥
K
对消息M
进行加密,生成密文C
:C = E(M, K)
,其中E
是加密算法。 - 用户 A 将密文
C
发送给用户 B。 - 用户 B 收到密文后,使用相同的密钥
K
解密,恢复原始消息M
:M = D(C, K)
,其中D
是解密算法。
对称密钥加密的缺点非常明显
- 双方需要事先共享密钥,密钥传输过程容易被截获。如果密钥泄露,通信安全将受到严重威胁。
- 不适合大规模使用:在多方通信中,每对通信方都需要一个独立的密钥。密钥数量增长迅速,难以管理。例如,若有 1000 个用户,每两人之间需要一个密钥,总共需要约 50 万个密钥。
- 无法实现身份验证:对称加密本身无法验证通信方的身份,容易受到中间人攻击。对称加密本身无法验证通信方的身份,容易受到中间人攻击。
非对称密钥加密(公开密钥加密)
公开密钥加密方式很好地解决了共享密钥加密的困难。它使用一对非对称的密钥,一把叫作私有密钥,另外一把叫作公开密钥。私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得。
使用方式: 发送密文的一方使用 对方的公钥
对信息进行加密,对方接收到被加密的信息后再使用自己的私钥进行解密。
特点: 信息传输一对多,服务器只需要维持好一个私钥就能和多个客户端进行加密通信。可以实现安全的身份验证、数字签名和密钥交换等功能。
优点:
- 安全性高: 私钥不会被公开传输,只有私钥的持有者才能解密加密的信息;
- 方便的密钥交换: 发送方和接收方只需交换公钥,而无需交换密钥;
- 可以实现数字签名: 私钥持有者可以使用时要对消息进行签名,接收方可以使用公钥验证签名的有效性;
缺点:
- 计算复杂度高: 与对称密钥加密相比,非对称密钥加密的计算速度慢,处理大量数据时可能会更耗时;
- 密钥管理复杂: 由于涉及到公钥和私钥的生成、发布和保护,密钥管理可能会更复杂;
- 通信效率较低:由于加密和解密操作需要使用较长的密钥,导致加密数据的大小增加,从而降低了通信效率;
虽然说安全性高,但也不是没有被盗的可能,因为公钥是公开的,谁都可以获取,如果发送的加密信息是通过私钥加密的话,有公钥的黑客就可以用这个公钥来解密拿到里面的信息。
下面有一个例子,假设用户 A 想发送一条安全消息给用户 B:
- 用户 A 获取用户 B 的
公钥
。 - 用户 A 使用 B 的公钥对消息 其中, 是用户 B 的公钥。 进行加密,生成密文 :
- 用户 A 将密文 发送给用户 B。
- 用户 B 收到密文后,使用自己的
私钥
解密,恢复原始消息 :其中, 是用户 B 的私钥。
非对称加密是一种安全性极高的加密技术,适用于身份验证、密钥交换和数字签名等场景。尽管速度较慢、不适合大数据加密,但它通过与对称加密结合,可以在现代网络通信中高效地提供安全保障。
为什么非对称加密效率低一点
非对称加密的效率较低主要是由于其算法的复杂性和计算成本较高的特点。以下是一些导致非对称加密效率低的主要原因:
- 密钥长度较长: 非对称加密需要使用一对密钥,包括公钥和私钥。通常情况下,这些密钥的长度要比对称加密中使用的密钥长得多。较长的密钥长度会导致加密和解密的操作都需要更多的计算时间。
- 计算复杂性: 非对称加密算法(如 RSA 和 Elliptic Curve Cryptography)涉及到大整数运算、模幂运算等复杂的数学运算。这些运算需要更多的计算资源和时间,因此非对称加密的处理速度较慢。
- 加密速度较慢: 由于非对称加密的加密和解密操作都使用不同的密钥,因此加密和解密速度都较慢。这使得非对称加密不适合处理大量数据,特别是实时通信和大规模数据传输方面。
- 密钥管理复杂性: 非对称加密需要管理和保护两个密钥:公钥和私钥。这增加了密钥管理的复杂性,包括生成、存储和分发密钥等方面的挑战。
- 安全性优先: 非对称加密的设计目标之一是提供更高的安全性,因此牺牲了一些性能。密钥的长长度和复杂的数学运算增加了攻击者破解加密的难度,但同时也降低了效率。
非对称加密效率较低主要源于其复杂的数学运算、较长的密钥长度和双密钥管理需求。这些特性决定了非对称加密在性能上无法与对称加密相比,但它通过提供更高的安全性和灵活性,成为密钥交换、身份验证和数字签名等场景的关键技术。通过混合加密和硬件优化,非对称加密的性能瓶颈可以得到有效缓解,从而实现安全与效率的平衡。
混合加密机制
HTTPS
采用共享密钥加密和公开密钥加密两者并用的混合加密机制。它采用了对称密钥加密算法的高效性和非对称密钥加密算法的安全性,可以保证安全性的同时提高加密和揭秘的效率。
混合加密机制的操作步骤主要一下几个方面:
- 密钥交换: 接收方生成一对非对称密钥 (
公钥
和私钥
),并将公钥发送给发送方; - 对称密钥生成: 发送方生成一个随机的对称密钥,用于对消息进行加密;
- 对称密钥加密: 发送方使用接收方的公钥将对称密钥加密,并将加密后的对称密钥发送给接收方;
- 消息处理: 发送方使用对称密钥对要发送的消息进行加密,并将加密后的消息发送给接收方;
- 密文传输: 接收方收到加密后的对称密钥和消息;
- 对称密钥加密: 接收方使用自己的私钥解密接收到的对称密钥;
- 消息解密: 接收方使用解密后的对称密钥对接收到的消息进行解密,获得原文明文消息;
在 HTTPS 中,非对称密钥用于安全地交换对称密钥,确保通信双方能在不暴露私密信息的情况下共享加密密钥。之后,对称密钥用于加密和解密实际的数据传输,因为对称加密处理数据速度更快。两者结合确保了数据传输的安全性和效率。
使用文字的方式来表达难免会有些难以理解,接下来我们使用一个流程图来看看混合加密机制的步骤是怎样实现的:
虽然混合加密机制结合了对称加密和非对称加密两者的优势,能够实现双方之间安全的传输。但也不是没有缺点,它的缺点主要有以下几个方面:
- 数据不完整性: 混合加密主要是为了解决
HTTP
中内容可能被窃听的问题。但是它并不能保证数据的完整性,也就是说在传输的时候数据是有可能被第三方篡改的,比如完全替换掉,所以说它并不能校验数据的完整性; - 复杂性: 混合加密涉及多种加密算法和密钥管理过程,因此实现和管理起来相对复杂;
- 密钥交换: 混合加密需要在通信双方之间进行密钥交换,以便建立安全的通信信道,如果密钥交换过程不正确或者被攻击者窃取,那么整个加密系统的安全性将会受到威胁;
- 性能开销: 混合加密需要同时使用非对称加密和对称加密算法,非对称加密算啊的加密和解密速度较慢,而对称加密算法的加密和解密速度较快。因此,在大规模数据传输时,可能会引入性能开销;
- 中间人攻击: 混合加密并不能防止中间人攻击,如果攻击者能够劫持或篡改通信信道,并替换公钥或插入恶意代码,那么它们仍然可以窃听、修改或伪装通信内容;
假设用户 A 需要向用户 B 发送加密消息,以下是混合加密的详细过程:
- 用户 A 生成会话密钥:用户 A 生成一个随机的会话密钥 。例如, 是一个 256 位的对称加密密钥。
- 用户 A 加密数据**:使用对称加密(如 AES),用户 A 使用
- 用户 A 加密会话密钥:使用非对称加密(如 RSA),用户 A 用用户 B 的公钥
- 用户 A 发送数据:用户 A 将加密的会话密钥 和加密的数据 一起发送给用户 B。
- 用户 B 解密会话密钥:用户 B 使用自己的私钥
- 用户 B 解密数据:用户 B 使用会话密钥
假设用户 B 收到用户 A 通过混合加密机制发送的密文,用户 B 如何通过解密获取明文?以下是完整的解密过程:
- 解密会话密钥
用户 B 收到加密的会话密钥
和加密的数据密文 。用户 B 使用自己的私钥 对加密的会话密钥 进行解密,恢复出会话密钥 :
解密后,
是对称加密所需的密钥。- 解密数据密文
用户 B 使用解密得到的会话密钥
对数据密文 进行对称解密:解密后,
是用户 A 发送的原始明文数据。混合加密机制结合了对称加密和非对称加密的优点,既保证了数据传输的安全性,又提高了加密处理的效率。这种机制在现代网络通信和数据加密中广泛使用,特别是在 HTTPS 协议、云存储、电子邮件加密和区块链等场景中,成为实现高效安全通信的关键技术。
保证公开密钥正确性的数字证书
目前来看,混合加密机制已经很安全了,但也不是完全没有问题。那就是无法证明公开密钥本身就是货真价实的公开密钥。它有可能在公开密钥传输途中,真正的公开密钥已经被攻击者替换掉了。
为了解决这个问题,通过数字证书认证机构和其他相关机关颁发的公开密钥证书。其中数字证书的基本组成部分主要有以下几个主体:
- 公钥:证书中包含了公钥,即需要验证的公开密钥;
- 签名:证书颁发机构使用自己的私钥对证书的内容进行数字签名,以验证证书的完整性和真实性;
- 有效期:证书包含了开始和结束的有效期,指定了证书的有效期限;
- 颁发机构信息:证书中包含了颁发机构的身份信息,用于验证颁发机构的可信性;
证书的主体部分包含了公钥持有者的身份信息,如名称、电子邮件地址等。
服务器会将这份由数字证书认证机构办法的公钥证书发送给客户端,以进行公开密钥加密方式通信。接到证书的客户端可使用数字证书认证机构的公开密钥,对那张证书上的数字签字进行验证,一旦验证通过,客户端便可以明确两件事:
- 认证服务器的公开密钥的真实有效的数字证书认证机构;
- 服务器的公开密钥是值得信赖的;
数字签名是什么呢,它是一种用于验证数据完整性和身份认证的技术,它的产生过程主要有以下几个步骤:
- 生成密钥对: 数字签名使用非对称密钥加密算法,首先需要生成密钥对。密钥对包括一个私钥和一个公钥。私钥用于生成签名,而公钥用于验证签名;
- 签名生成: 使用私钥对数据进行签名,签名生成的过程通常是先对数据进行哈希运算,然后使用私钥对哈希值进行加密,生成签名;
- 签名附加:将生成的签名与原始数据一起发送或存储;
- 验证签名:接收方或验证者收到签名和原始数据后,可以执行以下步骤验证签名的有效性
- 提取公钥: 从签名的来源获取签名者的公钥;
- 解密签名: 使用签名者的公钥对签名进行解密,得到解密后的哈希值;
- 哈希计算:对原始数据进行哈希运算,得到哈希值;
- 比较哈希值:将解密后的哈希值与计算得到的哈希值进行比较。如果两者匹配,说明签名是有效的。如果不匹配,说明签名无效;
通过这个过程,验证者可以确保数据在传输过程中没有被篡改,并且可以确定签名的来源。
数字证书的颁发流程
有了数字签名校验数据的完整性,但是数字签名校验的前提是能拿到发送方的公钥,并且保证这个公钥是可信赖的,所以就需要数字证书。
数字证书的颁发流程通常涉及以下步骤:
- 密钥生成:
- 实体(
个人
、组织
或服务器
)生成一个密钥对,包括一个公钥和一个私钥; - 私钥用于加密和签名,公钥用于解密和验证;
- 实体(
- 证书请求:
- 实体向证书办法机构(
Certificate Authority
,CA
)提交证书请求; - 证书请求中包含实体的公钥以及一些身份信息,例如名称、电子邮件地址等;
- 实体向证书办法机构(
- 身份验证:
CA
对实体的身份进行验证,验证的方式包括人工审核、文件验证、域名验证等;CA
确保证书请求的提交者拥有对应的私钥,并具备合法身份;
- 证书生成:
- 经过身份验证后,
CA
使用自己的私钥对证书进行签名,生成数字证书; - 数字证书中包含实体的公钥,身份信息以及
CA
的签名;
- 经过身份验证后,
- 证书颁发:
CA
将生成的数字证书颁发给实体,通常以电子文件的形式提供;- 实体接收到数字证书后,可以将其用于加密通信、数字签名等安全操作;
- 证书验证:
- 其他参与者在与实体进行通信时,可以获取实体的数字证书;
- 参与者使用证书颁发机构的公钥验证证书的签名,确保证书的完整性和真实性;
为什么说数字证书就能对通信方的身份进行验证呢?
数字证书能够对通信方身份进行验证,是因为数字证书采用了公钥加密和数字签名的技术,结合了非对称密钥加密算法的特性。
在数字证书中,证书颁发机构使用自己的私钥对证书进行签名,这个数字签名可以被其他参与这使用 CA
的公钥进行验证,通过验证数字签名,可以确保证书的完整性和真实性。
以下几个步骤是数字证书验证通信方身份的过程:
- 获取证书: 通信方在通信开始之前,从对方获取数字证书;;
- 提取公钥: 通信方从数字证书中提取对方的公钥;
- 验证签名: 通信方使用证书颁发机构的公钥对证书中签名进行解密,得到签名的哈希值;
- 哈希计算: 通信方对原始证书内容进行哈希计算,生成一个哈希值;
- 比较哈希值: 通信方将解密得到的哈希值与自己计算的哈希值进行比较,如果两者相同,则证书的签名是有效的,证明证书没有被篡改;
通过以上验证步骤,通信方可以确保证书的完整性,并且确定证书的来源是可信的。这样通信方可以信任证书中关联的公钥,并使用公钥进行加密、身份认证或数字签名的验证。
总的来说,数字证书通过使用证书颁发机构的私钥对证书进行签名,提供了一种可信任的方式来验证证书的完整性和真实性。通过验证证书,通信方可以建立对对方身份的信任,并使用其公钥进行安全的通信操作。
SSL/TLS 是如何工作的
HTTPS 是 HTTP 协议的一种安全形式。它围绕 HTTP、传输层安全性 (TLS) 包装了一个加密层。
HTTP 只是一种协议,但当与 TLS 配对时,它会被加密。
TLS 和 SSL 是面向 Socket 的协议,因此加密发送方和接收方之间的套接字或传输通道,但不加密数据。这是使这两个协议独立于应用层的主要原因。
接下来我们来看看 TLS 是如何工作的。先上图:
我们将对图中的每一个步骤做详细的解释:
- 握手启动 (Initiation of TLS Handshake):浏览器(客户端)发起 TLS 握手请求,与服务器建立安全通信。
- 客户端问候 (Client Hello):客户端发送 ClientHello 消息,包含以下内容:
- 支持的 TLS 协议版本(如 TLS 1.2、TLS 1.3)。
- 支持的加密算法(如 RSA、ECDHE、AES)。
- 随机数(用于密钥协商)。
- 会话 ID(如果是恢复连接时用)。
- 服务器问候 (Server Hello):服务器响应 ServerHello 消息,内容包括:
- 确认使用的 TLS 协议版本。
- 选择的加密算法。
- 服务器生成的随机数。
- 会话 ID。
- 服务器证书(Server Certificate):服务器发送其 SSL/TLS 证书(由 CA 签发),包含:
- 服务器的公开密钥。
- 服务器的身份信息(如域名)。
- 证书的有效期。
- 服务器密钥交换 (Server Key Exchange,可选):在某些情况下(如使用 Diffie-Hellman 密钥交换算法),服务器会发送密钥交换参数。这一步是可选的,具体取决于协商的加密算法。
- 服务器握手结束通知 (Server Handshake Finished):服务器发送 ServerHelloDone,表示服务器端的握手阶段完成。
- 客户端密钥交换 (Client Key Exchange):客户端生成一个 预主密钥(Pre-Master Secret),并使用服务器的公钥加密后发送给服务器。服务器用私钥解密,得到预主密钥。
- 生成主密钥(Pre-Master to Master Secret):客户端和服务器各自使用预主密钥、客户端随机数、服务器随机数,以及协商的加密算法,生成主密钥。
- 通知切换到加密模式 (Change Cipher Spec):客户端和服务器分别发送 ChangeCipherSpec 消息,表明后续通信将使用加密模式。
- 握手完成确认 (Handshake Finished):客户端和服务器分别发送握手完成确认消息,确认握手过程完成。
- 加密通信 (Encrypted Communication):握手完成后,客户端和服务器使用主密钥进行加密通信。
在上面的步骤中,主要有三个核心流程:
- 身份验证:通过服务器的 SSL/TLS 证书验证其身份。
- 密钥协商:利用非对称加密生成共享的会话密钥。
- 加密通信:使用对称加密(如 AES)提高传输效率。
HTTPS 是通过在 HTTP 上加入 TLS(传输层安全协议)实现安全通信的,它提供加密、身份验证和数据完整性保护。TLS 握手是 HTTPS 的核心流程,客户端与服务器通过握手协商加密算法、验证服务器身份,并生成共享的会话密钥。完成握手后,双方使用对称加密对数据进行高效传输,确保通信内容的机密性和完整性。
总结
尽管 HTTPS 提供了显著的安全优势,但由于 性能开销、证书管理成本、特定场景需求 和 历史遗留问题,一些场景下仍然使用 HTTP。不过,随着免费证书的普及、TLS 1.3 的性能提升以及对安全性的重视,使用 HTTPS 已成为现代互联网的趋势,并被搜索引擎(如 Google)优先推荐。
HTTPS
的本质就是在 HTTP
的基础上添加了安全层,主要是通过他来加密和验证机制来保护通信数据的安全性和隐私性。它提供了保密性、完整性和身份验证的重要机制,使得数据在传输过程中得到了有效的保护,防止数据被窃听、篡改和伪装。
来源:juejin.cn/post/7459561147580235795
10年开发后,我后悔坚持的8个技术信仰,不知你踩中几个
今天,我在生产环境排查一个莫名其妙的崩溃。日志里布满了层层抽象的调用栈,像一张无边的蜘蛛网。
代码里的每一行都符合“最佳实践”,架构精雕细琢,可Bug还是来了。那个瞬间,我突然想起四年前的自己——曾无比自豪地告诉新人:“优雅架构就是一切。”
可现在,我只想对那时候的自己说:“别装了,写能跑的代码吧。”
十年开发生涯让我推翻了许多曾深信不疑的技术理念。今天,我把这些踩坑经历整理出来,希望能帮你少走些弯路。
01 | 技术理念的崩塌
1. “简单”从来不是免费的,它是最昂贵的选择
四年前,我坚信“简单至上”。后来我才发现,让代码保持简单,需要持续的投入。业务需求膨胀时,每个“简单”的架构决策,都要付出昂贵的代价去维护。
真正的“简单”,不是一开始就写出完美代码,而是有能力在复杂性爆炸前,把代码逐步“修剪”回合理状态。
2. “优雅”是幻觉,能跑才是道理
曾经,我会在代码里反复推敲命名,调整缩进,优化模式,追求某种“美感”。但经历了几次生产事故后,我意识到:“优雅”从来不是一个真实的技术指标。
系统能否稳定运行、团队能否快速交接,远比代码的“形式美”重要。优雅的代码,不如无Bug的代码。
3. ORM是恶魔,SQL才是答案
我曾经推崇ORM,认为它能屏蔽数据库差异,提升开发效率。后来被它坑惨了:复杂查询写不出来,性能优化受限,Debug像拆炸弹。
最终,我回归了最原始的方式——直接写SQL。数据库优化的最佳方式,就是尊重SQL,而不是绕开它。
4. “类型安全”是团队的保护网
过去,我对强类型语言嗤之以鼻,觉得动态语言写起来灵活高效。直到一次团队交接,动态代码的“魔法”变成了无尽的痛苦。
Typed语言像护栏,帮团队里经验不同的人保持代码质量。个人写代码可以随性,团队协作必须稳健。
5. 前端开发已经卷成了噩梦
十年前,前端是HTML+CSS+JS,简单直白。现在,前端是一套复杂的工程体系,动不动就要学框架、学编译、学服务端渲染。
我越来越觉得,前端的“工程化”并没有带来应有的幸福感,而是让开发变得越来越焦虑。
6. Serverless 未来是好东西,但现在还是坑
Serverless的愿景很美好,可落地时,我无数次因为它的冷启动、调试难、监控难而崩溃。
如果你要做长期稳定的业务,老老实实选传统架构,Serverless 还没成熟到能承载大部分业务的程度。
7. “软件工程”大多时候只是沟通问题
这几年,我越来越发现,软件开发不是“写代码”这么简单,而是沟通、协作、妥协的过程。技术难点从来不是代码,而是“如何让所有人理解代码”。
会编码是一回事,能让别人看懂你的代码,才是真本事。
8. “管理”比技术重要,但真正好的管理极为稀缺
我花了很长时间才意识到,一个烂的管理,会让优秀的工程师一身狼狈;而一个好的管理,能让普通人也做出优秀的产品。
好管理者太少了,大多数的管理者,只是在消耗开发者的创造力。
02 |如何避免踩坑?
十年后的我,给刚入行的开发者3个建议:
① “代码洁癖”适可而止,写业务代码时别钻牛角尖
不要为了“优雅”而牺牲实用性,别陷入“最佳实践”的执念。实用 > 形式美。
② 直接写SQL,别太信任ORM
ORM适合简单查询,但复杂业务逻辑,SQL才是终极答案。与其踩坑,不如早点学会手写SQL。
③ 沟通能力比技术能力更值钱
代码能跑很重要,但能解释给别人听,能让团队顺畅协作,才是更核心的能力。
03 | 技术思维的升级
- “代码简单”不是靠写出来的,而是靠不断重构出来的
- “优雅”不是工程目标,稳定和可维护才是
- “工程师文化”最重要的是沟通,不是编码
04 | 你踩过这些坑吗?
如果你也在开发生涯中经历了类似的转变,欢迎留言分享你的故事。
你现在的信仰,四年后还会坚守吗?点击“在看”,收藏这篇文章,未来某一天再回来看,你的想法是否还一样。
来源:juejin.cn/post/7468100737994784787
最近看到太多 cursor 带来的焦虑,有些话想说
大家好,我卡颂,专注于AI助力程序员转型(阅读我的更多思考)
最近,有很多用cursor短时间开发应用的例子,其中不乏没有编程能力的非程序员。
这就给程序员群体带来一种焦虑 —— 我赖以谋生的技能会快速贬值么?
之所以会有这种焦虑,是因为看待AI与看待自身职业的角度不同:
- 从发展角度看待AI:默认AI能力会越来越强
- 从静态角度看待本职工作:默认程序员工作一成不变
如果我们能从发展角度看待本职工作,就能看到不一样的东西。
前端会经历的三个阶段
以我熟悉的前端行业举例(其实程序员工种也适用)。
从发展角度看待前端行业,当前行业正处在大规模的开发范式迁移中。
什么是开发范式迁移?举2个例子:
- 从原生JS过渡到
jQuery
的链式写法 - 从
jQuery
的命令式过渡到前端框架的声明式写法
当前,前端行业正处在由AI主导的新一轮开发范式迁移中,这是阶段一。
当开发范式迁移完成后,会形成事实上的新的前端技术栈,这是阶段二。
当新的前端技术栈形成后,会产生新的前端发展路径。
完整三个阶段演进过程如下:
接下来我详细解释下每个阶段。
阶段1:开发范式迁移
大规模开发范式迁移的显著特征,是不断出现新的开发工具,不断有开发工具被抛弃。
比如,在AI辅助编码领域,先行者是Github Copilot
,他开创了AI驱动的Tab补全代码这一AI辅助模式。
再往后,有了Continue
、Cursor
、Windsurf
,在Copilot
基础上创造了更多辅助模式,比如:
- Chat模式
- Normal/Agent Composer
其中,AI驱动的Tab补全代码已经逐渐成为程序员开发标配。
这就是开发范式迁移的一个例子。
接下来,我再举一个例子。
AI驱动的前端脚手架工具
当提到前端脚手架工具,大部分人第一反应是Vite
、CRA
这样的工具。
他们都属于上个前端开发范式时代的脚手架工具。
在当下,已经涌现很多AI驱动的前端脚手架工具,比如v0、bolt.new。
v0
是Vercel
开发的,可以理解为他是基于Vercel
旗下开发工具(Next.js
、shadcn
)的前端脚手架工具。
bolt.new
是Stackblitz
开发的,没有绑定具体前端技术,是一个比较通用的全栈项目(基于Node.js
)脚手架工具。
总结下,在范式迁移过程中,这些新技术不断涌现,又不断消失。
最终的胜者会成为未来前端技术栈中的固定嘉宾。
一件有趣的事:一般来说,技术、工具的普及是由于“程序员大规模认可”。但未来,他们的普及可能是因为“AI大规模认可”
阶段2:新的前端技术栈
为什么AI辅助编码已经是开发标配,但大部分公司的招聘要求中却没有提及?
因为AI辅助编码还在高速发展中,没有形成最佳实践。
只有到形成业界公认的最佳实践,成为新的前端技术栈,才会出现在主流的招聘要求中。
对Cursor
的焦虑本质来说,就是没意识到随着AI的发展,前端技术栈也会更新。
如果Cursor
(或者Cursor
同赛道的最终胜者)是程序员技术栈中要求需要熟练掌握的工具(就如同当前前端技术栈中的前端框架)。
那你还会因为“工具提高了开发效率”而焦虑么?
阶段3:新的前端发展路径
当前端(或其他任何程序员工种)完成开发范式迁移,形成事实上的新技术栈。
会造成两个结果:
- 职业门槛大幅度上升
- 开发效率大幅度提高
这势必会让行业洗牌,出清掉大量从业者。
同时也会形成新的前端发展路径。
当我们最终达到阶段3(产生新的前端发展路径),我认为他会是下面这样:
未来,基础的前端岗位是使用AI工具的前端专才,他包括两项能力:
- 熟练使用AI编码辅助工具
- 熟练的前端理论知识(类比当前的资深前端)
前端专才有两个进阶方向:
- 制作AI工具的前端
- 使用AI工具的前端多才
我解释下这两者。
其中,制作AI工具的前端类似当前的基建岗前端。
但不同的是,当下的基建岗前端很难回答一个问题:我花费大量时间做的工具相比同类开源产品有啥决定性优势?
与前者不同,制作AI工具的前端的产出是业务定制化的AI提效工具,这是与业务强相关的。
就像曾几何时,任何前端团队都需要一个webpack配置工程师
一样。
前端多才
则是指以前端技能为核心能力,同时掌握与前端相邻、平行工种的工作技能
- 相邻工种:UI、测试、后端、产品
- 平行工种:其他端的前端
这里举一个大前端开发工程师
的例子。
当前国内开发现状是 —— 端碎片化。
小程序、移动端、hybrid、web等,一个需求可能有多端开发需要。
对于多端开发需求,常见的解决方案有两种:
- 花钱方案:组建大前端团队
- 省钱方案:使用跨端方案,UniApp、Taro、React Native、Flutter...
由于各端逻辑类似,如果你同时掌握多端能力,只要实现一端后,就能借助AI将代码转为其他端。
只要“AI转代码的成本”比“调试跨端框架”低,这就是可行的。
随着AI的发展,当前者的收益显著高于后者后,就会出现大前端开发工程师
这一前端多才职业。
总结
事物是发展的,不仅AI如此,程序员行业亦是如此。
如果用静态的眼光看待程序员行业,满满都是被AI取代的焦虑。
但用发展的眼光看待时,这一行仍处于这波AI浪潮的早期 —— 大规模开发范式迁移阶段。
后面还有很长的路要走。
来源:juejin.cn/post/7452736507826683931