BOE(京东方)位列2024年PCT国际专利申请榜全球第6 创新引擎推动产业高质发展
3月17日(日内瓦当地时间),世界知识产权组织(WIPO)公布了2024年全球PCT国际专利申请排名,中国再次凭借卓越的创新表现领跑全球,PCT国际专利申请量稳居世界第一。其中,BOE(京东方)以1959件PCT专利申请量位列全球第6,连续9年进入全球PCT专利申请TOP10。这一成就不仅凸显了BOE(京东方)在创新显示技术领域的深厚积累和持续创新,更展现了其作为全球科技领军企业的强大竞争力和发展韧性。
技术创新力是实现企业高质量发展的源动力,BOE(京东方)始终秉持对技术的尊重和对创新的坚持,不断加大研发投入,每年将营收的7%左右投入研发,尤其将营收的1.5%用于基础与前沿技术研究,推出的产品全球首发率超过40%。在专利方面,截至2024年,BOE(京东方)累计自主专利申请已超10万件,在年度新增专利申请中,发明专利超90%,海外专利超33%,覆盖美国、欧洲、日本、韩国等多个国家和地区,遍及柔性OLED、传感、人工智能、大数据等多个领域。在标准制定方面,截至2024年,BOE(京东方)已累计主持及参与制、修订国内外技术标准429项,加速从技术创新引领者向行业标准制定者迈进,为柔性OLED、物联网应用、健康显示等领域的标准化发展做出了突出贡献。与此同时,围绕“屏之物联”战略,2021年底BOE(京东方)推出中国半导体显示领域首个技术品牌,以ADS Pro、f-OLED、α-MLED三大技术品牌为抓手,加速推动公司核心显示业务板块持续发展,进一步夯实全球显示领域的领军地位。
作为前沿技术的创造者,BOE(京东方)将技术创新实力全面赋能行业发展,推出众多“全球首发”的创新技术和量产产品,包括全球首款三折屏、搭载Tandem技术的旗舰产品、以及整合了屏下摄像头技术的全面屏手机等市场炙手可热的产品,并持续深耕LTPO、Oxide、OLED、MLED、传感、AIoT、智慧医工、智慧出行等领域,打造新业务增长极,积极构建产业发展的“第N曲线”。2023年,BOE(京东方)正式提出在“屏之物联”战略下打造三大技术策源地,联合高校、产学研专家、顶尖科技企业、生态伙伴共同打造技术高地,搭建开放交流的合作平台,推动半导体显示技术、物联网、人工智能、传感技术、工业互联、智慧能源等领域的创新突破。顺应数字时代浪潮,BOE(京东方)在2024年提出“AI+”战略,聚焦发力“AI+制造”“AI+产品”“AI+运营”三大领域,让人工智能成为BOE(京东方)创新发展的先进生产力。在人工智能算法领域,BOE(京东方)目前已有14项技术获得全球冠军,40余项算法位列全球TOP10。凭借全球领先的研发水平和专利技术,BOE(京东方)不仅在PCT专利申请榜单上取得佳绩,还得到多个全球权威机构的一致认可。全球权威专利服务机构IFI Claims发布的2024年度统计报告显示,BOE(京东方)位列美国专利授权排行榜全球第12位,连续第七年跻身全球TOP20,不仅成为TOP20中仅有的两家中国大陆企业之一,也是半导体显示领域唯一一家中国企业。除此之外,BOE(京东方)还连续四年荣登科睿唯安“全球百强创新机构”,以科技创新赋能产业“向新”。
面向未来,BOE(京东方)将继续坚持“屏之物联”的发展战略,深度挖掘物联网细分市场潜力,探索企业高质量发展的第“N”曲线,并加快专利成果的转化与应用,以科技创新为引擎推动产业价值不断提升,与全球各界生态伙伴开展协同创新,引领全球半导体显示行业迈向新高度。
官宣!联发科天玑开发者大会2025定档4月11日
近日,联发科天玑开发者大会 2025 官宣定档4 月 11 日! 作为 2025 AI 领域的开年盛会,大会将以“AI 随芯 应用无界”为主题,邀请全球开发者、行业大咖和技术专家,共同解读 AI 与芯片技术的融合趋势,推动智能应用进入全新阶段。
据悉,本届大会将包括主题演讲、天玑高峰对话、技术论坛、生态伙伴实践分享等主要环节,为广大开发者和生态伙伴提供深入探索和交流的舞台。目前“天玑开发者中心”官网已开启报名(https://developer.mediatek.com/mddc/2025),感兴趣的朋友可以在官网报名,速速抢位。
从本次官宣的信息来看,智能体AI的话题非常值得关注,相信现场也会有精彩分享。此外,全新一代的旗舰5G智能体芯片新品和生态新品也将正式发布。在AI开发方面,从模型、应用到智能体,联发科今年可以赋予开发者哪些新的能力和实践经验,包括天玑生态的最新进展,也是大会的重要内容。
此外根据去年经验,日常应用、游戏、终端等头部厂商将在现场进行丰富的技术展示,开发者不仅可以第一时间了解最新的AI和游戏技术趋势,更能与更多行业人士面对面交流,共同探索商业机会。
在去年的天玑开发者大会上,天玑9300+旗舰移动芯片全面领跑行业,巩固了移动端AI性能之王的地位。不仅于此,联发科以“天玑AI开发套件”为全场景终端的生成式AI应用开发提供强大的技术支持,已覆盖包括智能手机、智能汽车、物联网、个人电脑等智能终端设备,推动这些领域的AI应用快速创新落地,引发了开发者的热烈响应。同时,去年发布的“天玑AI先锋计划”携手了行业头部生态伙伴,助力开发者在搭载天玑芯片的终端设备上打造创新的用户体验,这一年以来的成果也将是今年大会上的一大看点。
过去一年,AI 在移动生态的应用实现巨大飞跃,而 MDDC 2025 将揭晓新一代技术创新,加速 AI 与游戏、终端的融合,为行业带来新一轮变革。这场 AI 盛会,不容错过!
收起阅读 »BOE(京东方)绵阳“零碳工厂”探访活动圆满落幕 树立显示产业绿色转型新标杆
2025年3月13日,BOE(京东方)“零碳工厂”探访活动在绵阳成功举办,此次活动邀请KOL及媒体代表齐聚京东方绵阳第6代柔性AMOLED生产线,深度探访国内显示行业首个“零碳工厂”。通过实地观摩与技术交流,BOE(京东方)全方位展示了其在绿色制造领域的突破性成果——从100%可再生能源覆盖到全流程碳足迹管理,从技术创新驱动减排到低碳模式行业复制,见证了公司多年来在可持续发展领域的持续投入与引领作用,为全球显示产业绿色转型提供了可借鉴的范本。
BOE(京东方)绵阳第6代柔性AMOLED生产线
定义“零碳工厂”:科技与绿色的深度融合
“零碳工厂”的诞生,源于BOE(京东方)对技术创新与可持续发展融合的极致追求。作为全球半导体显示行业首家实现“碳中和”的工厂,BOE(京东方)绵阳第6代柔性AMOLED生产线通过三大核心路径达成“零碳”目标:
100%绿电覆盖:2023年,工厂通过采购水电、风电及光伏绿电,实现生产用电可再生能源化,年间接减排量达51万吨;
全流程碳管理:从原材料采购到生产环节,通过碳足迹监测与优化分析减碳重点方向,减少产品迭代升级带来的碳排放影响,实现碳排放透明化管控,目前,BOE(京东方)的产品碳足迹已处于行业领先水平;
技术减排降碳:以技术创新驱动节能减排,通过优化生产工艺持续发力,例如延长NF3气体清洗周期、空压机节能改造、洁净室智能照明调控等技术,推动2023年直接碳排放减少3255吨,间接减排3677吨。
京东方绵阳区域总经理曹成熙表示,“科技+绿色”的完美融合代表了显示行业的未来方向,“零碳”不仅是目标,更是企业责任与技术能力的双重体现。BOE(京东方)通过“绿电覆盖+碳足迹管理+技术降碳”的创新模式,率先实现生产环节的“碳中和”。我们希望通过实践,为高精尖制造业探索一条可复制、可推广的绿色路径。
硬核科技:从减碳到赋能的升级
BOE(京东方)绵阳第6代柔性AMOLED生产线不仅通过一系列技术革新显著降低能耗,更以“绿色效能”反哺生产效率,实现环境效益与经济效益双赢,推动全行业协同升级。
屋顶光伏
由京东方能源投建的31.7MW分布式光伏项目,利用了BOE(京东方)绵阳第6代柔性AMOLED生产线23万平方米的屋顶面积,总面积可达32个标准足球场大小。BOE(京东方)通过“自发自用”模式,将屋顶光伏所发绿电在生产线20KV侧全部消纳。数据显示,绵阳京东方屋面光伏项目年均发电量可达2400万kWh,每年可减少二氧化碳排放 1.28 万吨,进一步减少对传统能源的依赖,提升绿色竞争力。
能源管理体系
能源管理体系采用IDC工业数据中心,拥有强大的数据整合和数据分析能力,通过对全公司各个生产环节的能源消耗进行实时追踪,包括生产制造、设备运转等各方面,系统分析并改善节能措施,2024年提出60余项节能措施,累计节电3263万kWh,减碳1.75万吨。
技术普惠赋能行业升级
BOE(京东方)绵阳第6代柔性AMOLED生产线的“光伏自建”、“自由冷却项目”等创新模式正快速复制至BOE(京东方)其他产线。以“自由冷却项目”为例,其年节电量580万kWh、减少二氧化碳排放3112吨,为高耗能洁净生产领域提供了可推广的低成本减碳方案,彰显了技术创新性与行业普惠性。
长期主义:从企业到生态的共振
可持续发展理念是BOE(京东方)多年来融入企业文化血脉中的基因。通过“绿色管理、绿色产品、绿色制造、绿色循环、绿色投资、绿色行动”六大路径,BOE(京东方)构建了覆盖全价值链的低碳体系。截至目前,BOE(京东方)旗下18家工厂获评“国家级绿色工厂”,并拥有显示行业唯一1家国家级“无废工厂”,1家“灯塔工厂”,2家“零碳工厂”及4家“绿色供应链”认证企业。此外,BOE(京东方)还积极响应SBTi及CDP等相关国际倡议,旗下已有7家工厂加入科学碳目标倡议组织(SBTi)。BOE(京东方)更是已连续14年发布可持续发展报告,在社会责任、公司治理等领域也取得了卓越成绩,持续推动产业向高端化、智能化、绿色化进阶。
作为全球领先的物联网创新企业,BOE(京东方)正以“屏之物联”战略为指引,加速推动技术、场景与绿色生态的深度融合。面向2050年碳中和目标,BOE(京东方)将持续践行“科技+绿色”理念,携手行业伙伴构建更真实、更高端、更智能、更绿色的产业生态,共同构建可持续发展的美好未来,为全球绿色发展贡献中国智慧与力量。
关于BOE(京东方):
京东方科技集团股份有限公司(BOE)是全球领先的物联网创新企业,为信息交互和人类健康提供智慧端口产品和专业服务。作为全球半导体显示产业的龙头企业,BOE(京东方)带领中国显示产业破局“少屏”困境,实现了从0到1的跨越。如今,全球每四个智能终端就有一块显示屏来自BOE(京东方)。在“屏之物联”战略引领下,BOE(京东方)凭借“1+4+N+生态链”业务架构,将超高清液晶显示、柔性显示、MLED、微显示等领先技术广泛应用于交通、金融、艺术、零售、教育、办公、医疗等多元场景,赋能千行百业。目前,BOE(京东方)的子公司遍布全球近20个国家和地区,拥有超过5000家全球生态合作伙伴。更多详情可访问BOE(京东方)官网:https://www.boe.com.cn
收起阅读 »微信小程序主包过大终极解决方案
随着小程序项目的不断迭代升级,避免不了体积越来越大。微信限制主包最多2M,然而我们的项目引入了直播插件直接占了1.1M,导致必须采用一些手段去优化。下面介绍一下优化思路和终极解决方案。
1.分包
我相信几乎所有人都能想到的方案,基本上这个方案就能解决问题。具体如何实现可以参照官方文档这里不做过多说明。(基础能力 / 分包加载 / 使用分包 (qq.com)),但是有时候你会发现分包之后好像主包变化不是很大,这是为什么呢?
- 痛点1:通过依赖分析,如果分包中引入了第三方依赖,那么依赖的js仍然会打包在主包中,例如echarts、wxparse、socket.io。这就导致我们即使做了分包处理,但是主包还是很大,因为相关的js都会在主包中的vendor.js
- 痛点2:插件只能在主包中无法分包,例如直播插件直接占据1M
- 痛点3:tabbar页面无法分包,只能在主包内
- 痛点4:公共组件/方法无法分包,只能在主包内
- 痛点5:图片只能在主包内
2.图片优化
图片是最好解决的,除了tabbar用到的图标,其余都放在云上就好了,例如oss和obs。而且放在云上还有个好处就是背景图片无需担心引入不成功。
3.tabbar页面优化
这部分可以采用tabbar页面都在放在一个文件夹下,比如一共有4个tab,那么一个文件夹下就只存放这4个页面。其余tabbar的子页面一律采用分包。
4.独立分包
独立分包是小程序中一种特殊类型的分包,可以独立于主包和其他分包运行。从独立分包中页面进入小程序时,不需要下载主包。当用户进入普通分包或主包内页面时,主包才会被下载。
但是使用的时候需要注意:
- 独立分包中不能依赖主包和其他分包中的内容,包括 js 文件、template、wxss、自定义组件、插件等(使用 分包异步化 时 js 文件、自定义组件、插件不受此条限制)
- 主包中的
app.wxss
对独立分包无效,应避免在独立分包页面中使用app.wxss
中的样式; App
只能在主包内定义,独立分包中不能定义App
,会造成无法预期的行为;- 独立分包中暂时不支持使用插件。
5.终极方案we-script
我们自己写的代码就算再多,其实增加的kb并不大。大部分大文件主要源于第三方依赖,那么有没有办法像webpack中的externals一样,当进入这个页面的时候再去异步加载js文件而不被打包呢(说白了就是CDN)
其实解决方案就是we-script,他允许我们使用CDN方式加载js文件。这样就不会影响打包体积了。
使用步骤
npm install --save we-script
- "packNpmRelationList": [{"packageJsonPath": "./package.json", "miniprogramNpmDistDir":"./dist/"}]
- 点击开发者工具中的菜单栏:工具 --> 构建 npm
"usingComponents": {"we-script": "we-script"}
<we-script src="url1" />
使用中存在的坑
构建后可能会出现依赖报错,解决的方式就是将编译好的文件手动拖入miniprogram_npm文件夹中,主要是三个文件夹:we-script,acorn,eval5
最后成功解决了主包文件过大的问题,只要是第三方依赖,都可以通过这个办法去加载。
感谢阅读,希望来个三连支持下,转载记得标注原文地址~
来源:juejin.cn/post/7355057488351674378
uni-app 接入微信短剧播放器
前言
作为一个 uniapp 初学者,恰巧遇到微信短剧播放器接入的需求,在网上检索许久并没有发现傻瓜式教程。于是总结 uni-app 官网文档及微信开放文档,自行实践后,总结出几个步骤,希望为大家提供些帮助。实践后发现其实确实比较简单,大佬们可能也懒得写文档,那么就由我这个小白大概总结下。本文档仅涉及剧目提审成功后的播放器接入,其余相关问题请参考微信官方文档。
小程序申请插件
参考文档:developers.weixin.qq.com/miniprogram…
首先,需要在小程序后台,申请 appid 为 wx94a6522b1d640c3b 的微信插件,可以在微信小程序管理后台进行添加,路径是 设置 - 第三方设置 - 插件管理 - 添加插件,搜索 wx94a6522b1d640c3b 后进行添加:
uni-app 项目添加微信插件
参考文档:uniapp.dcloud.net.cn/tutorial/mp…
添加插件完成后,在 manifest.json 中,点击 源码视图,找到如下位置并添加红框内的代码,此步骤意在将微信小程序插件引入项目。
/* 添加微短剧播放器插件 */
"plugins" : {
"playlet-plugin" : {
"version" : "latest",
"provider" : "wx94a6522b1d640c3b"
}
}
manifest.json 中完成添加后,需要在 pages.json 中找一个页面(我这边使用的是一个新建的空白页面)挂载组件,挂载方式如下图红框中所示,需注意,这里的组件名称需要与 manifest.json 中定义的一致:
{
"path": "newPage/newPage",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false,
"navigationStyle": "custom",
"app-plus": {
"bounce": "none"
},
"mp-weixin": {
"usingComponents": {
"playlet-plugin": "plugin://playlet-plugin/playlet-plugin"
}
}
}
}
挂载空页面是个笨办法,目前我这边尝试如果不挂载的话,会有些问题,有大神知道别的方法可以在评论区指点一下~
App.vue 配置
参考文档:developers.weixin.qq.com/miniprogram…
首先,找个地方新建一个 playerManager.js,我这边建在了 common 文件夹下。代码如下(代码参考微信官方文档给出的 demo):
var plugin = requirePlugin("playlet-plugin");
// 点击按钮触发此函数跳转到播放器页面
function navigateToPlayer(obj) {
// 下面的${dramaId}变量,需要替换成小程序管理后台的媒资管理上传的剧目的dramaId,变量${srcAppid}是提审方appid,变量${serialNo}是某一集,变量${extParam}是扩展字段,可通过
const { extParam, dramaId, srcAppid, serialNo } = obj
wx.navigateTo({
url: `plugin-private://wx94a6522b1d640c3b/pages/playlet/playlet?dramaId=${dramaId}&srcAppid=${srcAppid}&serialNo=${serialNo}&extParam=${extParam || ''}`
})
}
const proto = {
_onPlayerLoad(info) {
const pm = plugin.PlayletManager.getPageManager(info.playerId)
this.pm = pm
// encryptedData是经过开发者后台加密后(不要在前端加密)的数据,具体实现见下面的加密章节
this.getEncryptData({serialNo: info.serialNo}).then(res => {
// encryptedData是后台加密后的数据,具体实现见下面的加密章节
pm.setCanPlaySerialList({
data: res.encryptedData,
freeList: [{start_serial_no: 1, end_serial_no: 10}], // 1~10集是免费剧集
})
})
pm.onCheckIsCanPlay(this.onCheckIsCanPlay)
// 关于分享的处理
// 开启分享以及withShareTicket
pm.setDramaFlag({
share: true,
withShareTicket: true
})
// 获取分享参数,页面栈只有短剧播放器一个页面的时候可获取到此参数
// 例如从分享卡片进入、从投流广告直接跳转到播放器页面,从二维码直接进入播放器页面等情况
plugin.getShareParams().then(res => {
console.log('getLaunch options query res', res)
// 关于extParam的处理,需要先做decodeURIComponent之后才能得到原值
const extParam = decodeURIComponent(res.extParam)
console.log('getLaunch options extParam', extParam)
// 如果设置了withShareTicket为true,可通过文档的方法获取更多信息
// https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html
const enterOptions = wx.getEnterOptionsSync()
console.log('getLaunch options shareTicket', enterOptions.shareTicket)
}).catch(err => {
console.log('getLaunch options query err', err)
})
// extParam除了可以通过在path传参,还可以通过下面的接口设置
pm.setExtParam('hellotest')
// 分享部分end
},
onCheckIsCanPlay(param) {
// TODO: 碰到不可以解锁的剧集,会触发此事件,这里可以进行扣币解锁逻辑,如果用户无足够的币,可调用下面的this.isCanPlay设置
console.log('onCheckIsCanPlay param', param)
var serialNo = param.serialNo
this.getEncryptData({serialNo: serialNo}).then(res => {
// encryptedData是后台加密后的数据,具体实现见下面的加密章节
this.pm.isCanPlay({
data: res.encryptedData,
serialNo: serialNo,
})
})
},
getEncryptData(obj) {
const { serialNo } = obj
// TODO: 此接口请求后台,返回下面的setCanPlaySerialList接口需要的加密参数
const { srcAppid, dramaId } = this.pm.getInfo()
console.log('getEncryptData start', srcAppid, dramaId, serialNo)
return new Promise((resolve, reject) => {
resolve({
encryptedData: '' // TODO: 此参数需从后台接口获取到
})
})
},
}
function PlayerManager() {
var newProto = Object.assign({}, proto)
for (const k in newProto) {
if (typeof newProto[k] === 'function') {
this[k] = newProto[k].bind(this)
}
}
}
PlayerManager.navigateToPlayer = navigateToPlayer
module.exports = PlayerManager
新建完成后,在 App.vue 中进行组件的配置和引用。
onLaunch: function() {
// playlet-plugin必须和上面的app.json里面声明的插件名称一致
const playletPlugin = requirePlugin('playlet-plugin')
const _onPlayerLoad = (info) => {
var PlayerManager = require('@/common/playerManager.js')
const playerManager = new PlayerManager()
playerManager._onPlayerLoad(info)
}
// 注册播放器页面的onLoad事件
playletPlugin.onPageLoad(_onPlayerLoad.bind(this))
},
_onPlayerLoad(info) {
var PlayerManager = require('@/common/playerManager.js')
const playerManager = new PlayerManager()
playerManager._onPlayerLoad(info)
},
页面使用
参考文档:developers.weixin.qq.com/miniprogram…
以上所有步骤完成后,就可以开心的使用短剧播放器了。 我这边临时写了个图片的 click 事件测试了一下:
clk() {
// 逻辑处理...获取你的各种参数
// 打开组件中封装的播放器页面
PlayerManager.navigateToPlayer({
srcAppid: 'wx1234567890123456', // 剧目提审方 appid
dramaId: '100001', // 小程序管理后台的媒资管理上传的剧目的 dramaId
serialNo: '1', // 剧目中的某一集
extParam: encodeURIComponent('a=b&c=d'), // 扩展字段,需要encode
})
},
写在最后:
总结完了,其实整体下来不是很难,对我这种前端小白来说检索和整合的过程是比较痛苦的,所以希望下一个接入的朋友可以少检索一些文档吧。
另附一个短剧播放器接口的文档: developers.weixin.qq.com/miniprogram…
文档主要介绍了短剧播放器插件提供的几个接口,在js代码里,插件接口实例通过下面的代码获取
// 名字playlet-plugin必须和app.json里面引用的插件名一致
const playletPlugin = requirePlugin('playlet-plugin')
读书越多越发现自己的无知,Keep Fighting!
欢迎友善交流,不喜勿喷~
Hope can help~
来源:juejin.cn/post/7373473695057428506
BOE(京东方)携手微博举办“微博影像年”年度影像大展 创新科技赋能专业影像惊艳呈现
3月8日,微博旗下S级影像赛事IP——微博影像年“光与万物”年度影像大展在北京市鸟巢文化中心盛大启幕。继2024年合作之后,BOE(京东方)与微博再度携手,作为“微博影像年”独家显示技术合作伙伴,以裸眼3D沉浸式体验空间、百变屏、画屏等一系列顶尖科技赋能专业级影像呈现,此外还携手创维将ADS Pro赋能的旗舰电视新品创维A5F Pro创意融于展陈场景,让高端家居电视大屏成为影像艺术的完美载体,共同为观众奉上一场跨越虚拟与现实,融合技术与美学的视觉与感官盛宴。
微博影像年“光与万物”年度影像大展
BOE(京东方)副总裁、首席品牌官司达表示,“微博影像年”作为微博全力打造的S级影像赛事超级IP,吸引汇聚了来自59万余名记录者拍摄的超556万幅摄影作品,已成为一场由摄影艺术家、专业摄影师圈层延展到全民参与的年度影像盛事。BOE(京东方)作为全球半导体显示龙头企业,通过顶尖显示科技为影像作品打造最好的呈现载体,真实还原艺术作品构建的宏大光影世界,为影像艺术带来全新的科技表达。双方再度强强联手,将共同推动前沿显示技术的创新应用,为影像艺术行业的创新发展注入全新动能。
自3月8日至4月13日,涵盖风光、人文、建筑、非遗、明星作品等十八大主题单元的超600幅优秀影像作品将向公众持续性开放展览,观众可以在现场的数字影像展区通过BOE(京东方)的众多顶尖显示科技以全新形式感受影像艺术作品的独特魅力。
长幅“百变屏”,开启影像艺术的“流动式”交互
步入展区,一面由6块55英寸高清大屏精心拼接而成的“百变屏”宛如一幅流动的文化长卷,自由舒展地呈现出“微博影像年”的众多优秀摄影艺术作品。与常规静态展览不同,这面“百变屏”搭载智能化的百变UI系统,让超200幅高清影像艺术作品如同瀑布一般轮转流动,观众轻轻点击即可让影像作品停留眼前,细细观赏作品的每一处细节,详细查阅作品背后的创作故事。这面“百变屏”更搭载先进的多人触控系统,可支持多达12位观众同时触控浏览而互不影响,观众们可在众多佳作中自由切换穿梭,深度感受城市风光、自然奇景、人文风情等众多精彩的艺术瞬间。
BOE(京东方)百变屏
裸眼3D沉浸空间,呈现三维重构的感官奇旅
此次影展最为吸睛的打卡点之一,当属BOE(京东方)打造的影视级裸眼3D沉浸式体验空间。基于顶尖的MLED技术,这座由三面高达3米的MLED大屏创意构建而成的沉浸空间无需任何辅助设备即可让屏幕实现从2D到3D的跨越,高达7680Hz的超高灯点刷新率带来极致丝滑流畅的裸眼3D高速运动视效,低灰阶、广色域的领先技术优势,让每一个画面都能绚丽呈现。“微博影像年”的IP理念在三维空间中自由流转,带来突破感官想象的震撼立体幻境,观众可以在3D沉浸空间中亲身体验一场跨越次元的感官奇幻之旅。
BOE(京东方)裸眼3D沉浸空间
类纸“画屏”,原生还原光影本真世界
展区中央,众多32英寸及55英寸的“画屏”错落林立,如同真实纸张一般自然呈现一幅幅影像艺术作品的原生质感,每一处光影、每一抹色彩都被精准还原,让影像作品背后的光影世界淋漓尽致地呈现在观众眼前。得益于BOE(京东方)独有的无损Gamma显示技术(发明专利,专利号:ZL 2016 1 0214546.6),画屏的电子屏幕可呈现出如同纸张一般极致真实的显示效果;内置领先的智能感光与图像智能匹配算法,配合低蓝光、无频闪、防眩光等领先显示技术,让观众无论何时观展,都能观赏到影像作品的最佳状态。
BOE(京东方)画屏
比OLED更好的Mini LED电视,带来家居美学的艺术新生
在现场特设的科技展区,观众们还能发现一款由BOE(京东方)ADS Pro技术赋能的创维高端旗舰电视新品A5F Pro完美融于艺术化展陈场景之中,媲美OLED的顶级画质让众多优秀影像艺术作品以全新面貌惊艳呈现,让观众们流连沉醉于绚丽多姿的影像世界。这款电视搭载双方联合定制的全新极黑广角类纸屏,通过BOE(京东方)ADS Pro技术和创维自研MiniLED技术的黄金组合,呈现极致深邃、细腻的顶级画质,让优秀影像艺术作品的每一处细节都能惊艳盛放。
BOE(京东方)ADS Pro技术赋能的创维高端旗舰电视新品A5F Pro
近年来,BOE(京东方)通过“科技+艺术”跨界融合的创新模式频频实现技术影响力的成功“破圈”与技术生态的不断延展,通过“你好BOE”这一年度标杆性品牌巡展IP,不仅与微博、上海电影、上影元等众多顶级文化机构及一线合作伙伴联合打造艺术与技术完美融合的光影盛会,更让顶尖显示科技“出海”巴黎,为世界奉上一场中国非遗与当代艺术的视觉盛宴。此次BOE(京东方)与微博共同举办年度影像大展,是领先显示科技与文化艺术IP跨界融合、双向赋能的又一标杆典范。未来,BOE(京东方)还将持续创新,以前沿科技为文化艺术等千行百业的创新发展注入全新活力,为万千大众带来更美好的视觉体验。
关于BOE(京东方):
京东方科技集团股份有限公司(BOE)是全球领先的物联网创新企业,为信息交互和人类健康提供智慧端口产品和专业服务。作为全球半导体显示产业的龙头企业,BOE(京东方)带领中国显示产业破局“少屏”困境,实现了从0到1的跨越。如今,全球每四个智能终端就有一块显示屏来自BOE(京东方)。在“屏之物联”战略引领下,BOE(京东方)凭借“1+4+N+生态链”业务架构,将超高清液晶显示、柔性显示、MLED、微显示等领先技术广泛应用于交通、金融、艺术、零售、教育、办公、医疗等多元场景,赋能千行百业。目前,BOE(京东方)的子公司遍布全球近20个国家和地区,拥有超过5000家全球生态合作伙伴。更多详情可访问BOE(京东方)官网。
收起阅读 »BOE(京东方)珠海晶芯COB量产交付仪式举办 赋能MLED产业新发展
3月10日,BOE(京东方)在珠海京东方晶芯科技有限公司成功举办主题为“屏曜万象 量产启航”的COB量产交付仪式,全球来自美国、欧洲、日韩、东南亚等在内的50余位海外客户齐聚一堂,这一重要时刻标志着BOE(京东方)在MLED显示技术领域取得重要里程碑。通过整合上游芯片、中游封装与下游应用资源,BOE(京东方)构建了行业领先的“芯片-封装-显示”全链条智能制造能力,打造最短产业链生态环境。这一布局不仅加速了MLED产业链的协同创新与全球拓展,更推动了BOE(京东方)向COB产业领先企业快速迈进,为其在全球高端显示市场的布局奠定了坚实基础。
BOE(京东方)执行委员会副主席、首席运营官王锡平在开场致辞中表示,BOE(京东方)始终坚持创新驱动发展,全面开启“屏之物联”企业升维战略。MLED业务是BOE(京东方)“1+4+N+生态链”的高潜航道之一,此次珠海晶芯项目量产交付,是公司打造 MLED 产业集群的重要战略举措。这不仅是全球显示产业迈向未来的重要一步,更是BOE(京东方)推动产业链升级的关键里程碑。我们期待与全球合作伙伴携手同行,共同推动显示行业向更智能、更绿色、更可持续的未来迈进!
BOE(京东方)执行委员会副主席、首席运营官 王锡平
作为中国半导体显示领域首个技术品牌,BOE(京东方)首创、领先行业的高端玻璃基新型 LED 显示技术品牌α-MLED,致力于为全球客户提供领先的新型LED显示系统及解决方案。BOE(京东方)通过室内平台、户外平台、创新应用平台三大产品平台,打造COB、COG、SMD、Micro LED高竞争力产品线,全面满足多元化的市场需求。据悉,珠海京东方晶芯MLED生产线坐落在华发平沙电子电器产业园,从去年6月启动到量产仅用9个月的时间,彰显了BOE(京东方)高效执行力和产业链协同优势,也为MLED技术的产业化进程树立了新的标杆。
BOE(京东方)在MLED领域持续布局,目前已推出P0.3 Micro LED 产品、COG LTPS P0.5 MLED 显示产品、COB P0.9-1.5全系列产品等,同时背光技术在电视、显示器、笔记本电脑、电竞、车载多领域进行布局;此外还将BOE(京东方)独有的ADS Pro显示技术与Mini LED背光相结合,极大提升高速运动画面清晰度,以媲美OLED的画质表现引领行业实现极致视觉体验。此次BOE(京东方)量产的COB产品,以至臻光学、至湛热学、至简结构三大核心技术为着力点,全方位打造MLED产品差异化技术优势和至臻显示效果。在至臻光学方面,通过高亮度(最高2000nit)、高色域(110% NTSC)、超低功耗(同亮度下功耗减半)、以及高对比度、亮态一致性、暗态一致性等优势,为用户提供更清晰、真实、沉浸的视觉体验;在至湛热学方面,关注环境与人的双向友好,打造冷屏卓越体验,提升产品性能的同时实现节能低功耗;在至简结构方面,实现结构标准化、便捷化和创新设计,提高生产效率的同时确保产品稳定性和长久耐用性。此外,BOE(京东方)独创的“无色缝墨色管控工艺”更是打破行业壁垒,无需模组目视筛选即可实现整屏墨色一致性,打造MLED画质最优、一致性更佳的高端显示解决方案。值得一提的是,BOE(京东方)已在埃及打造全球最大COB小间距LED室内显示屏,屏幕面积达600㎡,采用BOE MLED COB P1.2产品,分辨率高达67680×5670,是目前全球分辨率最高的COB大屏,为埃及城市建设注入科技动能。
BOE(京东方)副总裁、MLED业务CEO刘毅表示,珠海京东方晶芯的成功量产,不仅标志着BOE(京东方)在MLED COB领域取得了重要突破,更是我们推动显示技术革新、引领产业升级的关键里程碑。此次项目落地也离不开珠海以及华发集团等各方合作伙伴的全力配合和支持。我们期待与全球合作伙伴携手共进,共同推动MLED技术的创新与应用,为商业显示、高端电视、车载显示等领域提供更优质、更高效的显示解决方案,助力全球客户实现价值提升,深化与全球伙伴构建开放、合作、共赢的产业生态。
BOE(京东方)副总裁、MLED业务CEO 刘毅
值得关注的是,BOE(京东方)MLED业务持续深耕全产业链生态布局,依托珠海京东方晶芯的量产和设备技术升级,首发主推直显新品BYH-COB MLED系列,在同期举办的深圳ISLE 2025展会上,该系列的Pro版和Ultra版产品首次在国内发布,同时BOE(京东方)还携电影屏、球形屏、背光等创新产品重磅亮相,全面展示MLED技术的突破性应用。其中,BYH-COB Pro P0.9采用大板模组设计,单模组尺寸达14.5英寸,有效减少拼缝数量,提升画面整体性,广泛应用于会议室、商超、展厅等场景。该系列中的BOE-BYH ULTRA作为BOE MLED COB定制化高端产品,凭借独创光学膜封装技术与自研高端表面防眩光反射膜材,在环境光10lux条件下实现高达20000:1的对比度,为目前行业领先。
在显示无处不在的时代,BOE(京东方)以“屏”为核心,持续推动显示技术的革新与突破。面向未来,BOE(京东方)将始终秉持“屏之物联”战略,聚焦Mini/Micro LED技术迭代升级,与全球生态伙伴深化创新合作,构建更为完整的产业链生态布局,以更加卓越的显示解决方案,为全球客户创造更大价值!
关于BOE(京东方):
京东方科技集团股份有限公司(BOE)是全球领先的物联网创新企业,为信息交互和人类健康提供智慧端口产品和专业服务。作为全球半导体显示产业的龙头企业,BOE(京东方)带领中国显示产业破局“少屏”困境,实现了从0到1的跨越。如今,全球每四个智能终端就有一块显示屏来自BOE(京东方)。在“屏之物联”战略引领下,BOE(京东方)凭借“1+4+N+生态链”业务架构,将超高清液晶显示、柔性显示、MLED、微显示等领先技术广泛应用于交通、金融、艺术、零售、教育、办公、医疗等多元场景,赋能千行百业。目前,BOE(京东方)的子公司遍布全球近20个国家和地区,拥有超过5000家全球生态合作伙伴。更多详情可访问BOE(京东方)官网。
收起阅读 »后端出身的CTO问:"前端为什么没有数据库?",我直接无语......
😅【现场还原】
"前端为什么没有自己的数据库?把数据存前端不就解决了后端性能问题" ——当CTO抛出这个灵魂拷问时,会议室突然安静得能听见CPU风扇的嗡鸣,在座所有人都无语了。这场因后端性能瓶颈引发的技术博弈,最终以"前端分页查询+本地筛选"的妥协方案告终。
面对现在几乎所有公司的技术leader都是后端出身,有的不懂前端甚至不懂技术,作为前端开发者,我们真的只能被动接受吗?
😣【事情背景】
- 需求:前端展示所有文章的标签列表,用户可以选择标签筛选文章,支持多选,每个文章可能有多个标签,也可能没任何标签。
- 前端观点:针对这种需求,我自然想到用户选中标签后,将标签id传给后端,后端根据id筛选文章列表返回即可。
- 后端观点:后端数据分库分表,根据标签检索数据还要排序分页,有性能瓶颈会很慢,很慢就会导致天天告警。
- 上升决策:由于方案有上述分歧,我们就找来了双方leader决策,双方leader也有分歧,最终叫来了CTO。领导想让我们将数据定时备份到前端,需要筛选的时候前端自己筛选。
CTO语录:
“前端为什么没有数据库?,把数据存前端,前端筛选,数据库不就没有性能压力了”
"现在手机性能比服务器还强,让前端存全量数据怎么了?"
"IndexedDB不是数据库?localStorage不能存JSON?"
"分页?让前端自己遍历数组啊,这不就是你们说的'前端工程化'吗?"
😓【折中方案】
在方案评审会上,我们据理力争:
- 分页请求放大效应:用户等待时间=单次请求延迟×页数
- 内存占用风险:1万条数据在移动端直接OOM
- 数据一致性难题:轮询期间数据更新的同步问题
但现实往往比代码更复杂——当CTO拍板要求"先实现再优化",使用了奇葩的折中方案:
- 前端轮询获取前1000条数据做本地筛选,用户分页获取数据超过1000条后,前端再轮询获取1000条,以此类推。
- 前端每页最多获取50条数据,每次最多并发5个请求(后端要求)
只要技术监控不报错,至于用户体验?慢慢等着吧你......
🖨️【批量并发请求】
既然每页只有50条数据,那我至少得发20个请求来拿到所有数据。显然,逐个请求会让用户等待很长时间,明显不符合前端性能优化的原则。于是我选择了 p-limit 和Promise.all来实现异步并发线程池。通过并发发送多个请求,可以大大减少数据获取的总时间。
import pLimit from 'p-limit';
const limit = pLimit(5); // 限制最多5个并发请求
// 模拟接口请求
const fetchData = (page, pageSize) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`数据页 ${page}:${pageSize}条数据`);
}, 1000);
});
};
// 异步任务池
const runTasks = async () => {
const totalData = 1000; // 总数据量
const pageSize = 50; // 每页容量
const totalPages = Math.ceil(totalData / pageSize); // 计算需要多少页
const tasks = [];
// 根据总页数动态创建请求任务
for (let i = 1; i <= totalPages; i++) {
tasks.push(limit(() => fetchData(i, pageSize))); // 使用pLimit限制并发请求
}
const results = await Promise.all(tasks); // 等待所有请求完成
console.log('已完成所有任务:', results);
};
runTasks();
📑【高效本地筛选数据】
当所有数据都请求回来了,下一步就是进行本地筛选。毕竟后端已经将查询任务分配给了前端,所以我得尽可能让筛选的过程更高效,避免在本地做大量的计算导致性能问题。
1. 使用哈希进行高效查找
如果需要根据某个标签来筛选数据,最直接的做法就是遍历整个数据集,但这显然效率不高。于是我决定使用哈希表(或 Map)来组织数据。这样可以在常数时间内完成筛选操作。
const filterDataByTag = (data, tag) => {
const tagMap = new Map();
data.forEach(item => {
if (!tagMap.has(item.tag)) {
tagMap.set(item.tag, []);
}
tagMap.get(item.tag).push(item);
});
return tagMap.get(tag) || [];
};
const result = filterDataByTag(allData, 'someTag');
console.log(result);
2. 使用 Web Workers 进行数据处理
如果数据量很大,筛选过程可能会比较耗时,导致页面卡顿。为了避免这个问题,可以将数据筛选的过程交给 Web Workers 处理。Web Worker 可以在后台线程运行,避免阻塞主线程,从而让用户体验更加流畅。
const worker = new Worker('worker.js');
worker.postMessage(allData);
worker.onmessage = function(event) {
const filteredData = event.data;
console.log('筛选后的数据:', filteredData);
};
// worker.js
onmessage = function(e) {
const data = e.data;
const filteredData = data.filter(item => item.tag === 'someTag');
postMessage(filteredData);
};
📝【总结】
这场技术博弈给我们带来三点深刻启示:
- 数据民主化趋势:随着WebAssembly、WebGPU等技术的发展,前端正在获得堪比后端的计算能力
- 妥协的艺术:临时方案必须包含演进路径,我们的分页实现预留了切换GraphQL的接口
- 性能新思维:从前端到边缘计算,性能优化正在从"减少请求"转向"智能分发"
站在CTO那句"前端为什么没有数据库"的肩膀上,我们正在构建这样的未来:每个前端应用都内置轻量级数据库内核,通过差异同步策略与后端保持数据一致,利用浏览器计算资源实现真正的端智能。这不是妥协的终点,而是下一代Web应用革命的起点。
后记:三个月后,我们基于SQL.js实现了前端SQL查询引擎,配合WebWorker线程池,使得复杂筛选的耗时从秒级降至毫秒级——但这已经是另一个技术突围的故事了。
来源:juejin.cn/post/7472732247932174388
Vue3 实现最近很火的酷炫功能:卡片悬浮发光
前言
大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~
有趣的动画效果
前几天在网上看到了一个很有趣的动画效果,如下,光会跟随鼠标在卡片上进行移动,并且卡片会有视差的效果
那么在 Vue3 中应该如何去实现这个效果呢?
基本实现思路
其实实现思路很简单,无非就是分几步:
- 首先,卡片是
相对定位
,光是绝对定位
- 监听卡片的鼠标移入事件
mouseenter
,当鼠标进入时显示光 - 监听卡片的鼠标移动事件
mouseover
,鼠标移动时修改光的left、top
,让光跟随鼠标移动 - 监听卡片的鼠标移出事件
mouseleave
,鼠标移出时,隐藏光
我们先在 Index.vue
中准备一个卡片页面,光的CSS效果可以使用filter: blur()
来实现
可以看到现在的效果是这样
实现光源跟随鼠标
在实现之前我们需要注意几点:
- 1、鼠标移入时需要设置卡片
overflow: hidden
,否则光会溢出,而鼠标移出时记得还原 - 2、获取鼠标坐标时需要用
clientX/Y
而不是pageX/Y
,因为前者会把页面滚动距离也算进去,比较严谨
刚刚说到实现思路时我们说到了mouseenter、mousemove、mouseleave
,其实mouseenter、mouseleave
这二者的逻辑比较简单,重点是 mouseover
这个监听函数
而在 mouseover
这个函数中,最重要的逻辑就是:光怎么跟随鼠标移动呢?
或者也可以这么说:怎么计算光相对于卡片盒子的 left 和 top
对此我专门画了一张图,相信大家一看就懂怎么算了
- left = clientX - x - width/2
- height = clientY - y - height/2
知道了怎么计算,那么逻辑的实现也很明了了~封装一个use-light-card.ts
接着在页面中去使用
这样就能实现基本的效果啦~
卡片视差效果
卡片的视差效果需要用到样式中 transform
样式,主要是配置四个东西:
- perspective:定义元素在 3D 变换时的透视效果
- rotateX:X 轴旋转角度
- rotateY:Y 轴旋转角度
- scale3d:X/Y/Z 轴上的缩放比例
现在就有了卡片视差的效果啦~
给所有卡片添加光源
上面只是给一个卡片增加光源,接下来可以给每一个卡片都增加光源啦!!!
让光源变成可配置
上面的代码,总感觉这个 hooks 耦合度太高不太通用,所以我们可以让光源变成可配置化,这样每个卡片就可以展示不同大小、颜色的光源了~像下面一样
既然是配置化,那我们希望是这么去使用 hooks 的,我们并不需要自己在页面中去写光源的dom节点,也不需要自己去写光源的样式,而是通过配置传入 hooks 中
所以 hooks 内部要自己通过操作 DOM 的方式,去添加、删除光源,可以使用createElement、appendChild、removeChild
去做这些事~
完整源码
<!-- Index.vue -->
<template>
<div class="container">
<!-- 方块盒子 -->
<div class="item" ref="cardRef1"></div>
<!-- 方块盒子 -->
<div class="item" ref="cardRef2"></div>
<!-- 方块盒子 -->
<div class="item" ref="cardRef3"></div>
</div>
</template>
<script setup lang="ts">
import { useLightCard } from './use-light-card';
const { cardRef: cardRef1 } = useLightCard();
const { cardRef: cardRef2 } = useLightCard({
light: {
color: '#ffffff',
width: 100,
},
});
const { cardRef: cardRef3 } = useLightCard({
light: {
color: 'yellow',
},
});
</script>
<style scoped lang="less">
.container {
background: black;
width: 100%;
height: 100%;
padding: 200px;
display: flex;
justify-content: space-between;
.item {
position: relative;
width: 125px;
height: 125px;
background: #1c1c1f;
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
</style>
// use-light-card.ts
import { onMounted, onUnmounted, ref } from 'vue';
interface IOptions {
light?: {
width?: number; // 宽
height?: number; // 高
color?: string; // 颜色
blur?: number; // filter: blur()
};
}
export const useLightCard = (option: IOptions = {}) => {
// 获取卡片的dom节点
const cardRef = ref<HTMLDivElement | null>(null);
let cardOverflow = '';
// 光的dom节点
const lightRef = ref<HTMLDivElement>(document.createElement('div'));
// 设置光源的样式
const setLightStyle = () => {
const { width = 60, height = 60, color = '#ff4132', blur = 40 } = option.light ?? {};
const lightDom = lightRef.value;
lightDom.style.position = 'absolute';
lightDom.style.width = `${width}px`;
lightDom.style.height = `${height}px`;
lightDom.style.background = color;
lightDom.style.filter = `blur(${blur}px)`;
};
// 设置卡片的 overflow 为 hidden
const setCardOverflowHidden = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardOverflow = cardDom.style.overflow;
cardDom.style.overflow = 'hidden';
}
};
// 还原卡片的 overflow
const restoreCardOverflow = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.style.overflow = cardOverflow;
}
};
// 往卡片添加光源
const addLight = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.appendChild(lightRef.value);
}
};
// 删除光源
const removeLight = () => {
const cardDom = cardRef.value;
if (cardDom) {
cardDom.removeChild(lightRef.value);
}
};
// 监听卡片的鼠标移入
const onMouseEnter = () => {
// 添加光源
addLight();
setCardOverflowHidden();
};
// use-light-card.ts
// 监听卡片的鼠标移动
const onMouseMove = (e: MouseEvent) => {
// 获取鼠标的坐标
const { clientX, clientY } = e;
// 让光跟随鼠标
const cardDom = cardRef.value;
const lightDom = lightRef.value;
if (cardDom) {
// 获取卡片相对于窗口的x和y坐标
const { x, y } = cardDom.getBoundingClientRect();
// 获取光的宽高
const { width, height } = lightDom.getBoundingClientRect();
lightDom.style.left = `${clientX - x - width / 2}px`;
lightDom.style.top = `${clientY - y - height / 2}px`;
// 设置动画效果
const maxXRotation = 10; // X 轴旋转角度
const maxYRotation = 10; // Y 轴旋转角度
const rangeX = 200 / 2; // X 轴旋转的范围
const rangeY = 200 / 2; // Y 轴旋转的范围
const rotateX = ((clientX - x - rangeY) / rangeY) * maxXRotation; // 根据鼠标在 Y 轴上的位置计算绕 X 轴的旋转角度
const rotateY = -1 * ((clientY - y - rangeX) / rangeX) * maxYRotation; // 根据鼠标在 X 轴上的位置计算绕 Y 轴的旋转角度
cardDom.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; //设置 3D 透视
}
};
// 监听卡片鼠标移出
const onMouseLeave = () => {
// 鼠标离开移出光源
removeLight();
restoreCardOverflow();
};
onMounted(() => {
// 设置光源样式
setLightStyle();
// 绑定事件
cardRef.value?.addEventListener('mouseenter', onMouseEnter);
cardRef.value?.addEventListener('mousemove', onMouseMove);
cardRef.value?.addEventListener('mouseleave', onMouseLeave);
});
onUnmounted(() => {
// 解绑事件
cardRef.value?.removeEventListener('mouseenter', onMouseEnter);
cardRef.value?.removeEventListener('mousemove', onMouseMove);
cardRef.value?.removeEventListener('mouseleave', onMouseLeave);
});
return {
cardRef,
};
};
结语 & 加学习群 & 摸鱼群
我是林三心
- 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;
- 一个偏前端的全干工程师;
- 一个不正经的掘金作者;
- 一个逗比的B站up主;
- 一个不帅的小红书博主;
- 一个喜欢打铁的篮球菜鸟;
- 一个喜欢历史的乏味少年;
- 一个喜欢rap的五音不全弱鸡
如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点
来源:juejin.cn/post/7373867360019742758
都2025年了,还在用Markdown写文档吗?
俗话说得好:“程序员宁愿写 1000 行代码,也不愿意写 10 个字的文档。”
不愿写文档的原因,一方面是咱理科生文采确实不好,另一方面则是文档的更新维护十分麻烦。
每次功能有变更的时候, 时间又急(其实就是懒),很难想得起来同时去更新文档。
特别是文档中代码片段,总是在几天之后(甚至更久),被用户找过来吐槽:“你们也太不专业了,文档里的代码都跑不通。”
作为有素养的程序员,被人说 “不专业”,简直不能忍,一定要想办法解决这个问题。
文档代码化
很多开发者喜欢用语雀,飞书或者钉钉来写文档。
不得不承认,它们的编写和阅读的体验更好,样式也更丰富。
甚至就算是版本管理,语雀,飞书做得也不比 git 差。
不过对于开发者文档,我觉得体验,样式都不是最重要的。毕竟这些都是锦上添花。
更重要的是,文档内容有效性的保证,如果文档上的代码直接复制到本地,都要调试半天才能跑通,那不管它样式再好看开发者都要骂娘了。
所以文档最好就和代码放在同一个仓库中,这样代码功能有更新时,顺便就把文档一起改了。团队前辈 Review 代码时,也能顺便关注下相关的文档是否一同更新。
如果真的一定要搞一个语雀文档,也可以考虑用 Git Action,在分支合并到 master 时触发一次文档到语雀的自动同步。
Markdown 的问题
程序员最常用的代码化文档就是 Markdown 了,估计也是很多开发者的首选,比如我这篇文章就是用 Markdown 写的。
不过 Markdown 文档中的代码示例,也没有经过单元测试的验证,还是会出现文档中代码跑不通的现象。
Python 中有一个叫做 doctest 的工具,能够抽取文档中的所有 python 代码并执行,我们只要在分支合并前,确保被合并分支同时通过了单元测试和 doctest,就能保证文档中的代码示例都是有效的。
在 Java 中我找了半天没有找到类似工具,很多语言(比如 Go, Rust 等等)据我所知也没有类似的工具。
而且对于 Java,文档中给的一般都是不完整的代码片段,无法像 Python 一样直接就能进入命令行执行。
有句俗话 ”单元测试就是最好的文档“。我觉得没必要将单元测试和文档分开,最好的方式就是从单元测试中直接引用部分代码进入文档。
在变更功能时,我们一定也会改单元测试,文档也会同步更新,不需要单独维护。
在合并分支或者发布版本之前,肯定也会有代码门禁执行单元测试,这样就能确保文档中代码示例都是有效的。
目前我发现了一个能解决该问题的方案就是 adoc 文档。
adoc 文档
adoc 的全称是 Asciidoctor, 官网链接。
Github 已经对其实现了原生支持,只要在项目中将 README 文件的后缀改成 README.adoc
,Github 就会按照 adoc 的语法进行解析展示。
adoc 最强悍的能力就是可以对另一个文件的一部分进行引用。以我负责的开源项目 QLExpress 为例。
在单元测试 Express4RunnerTest
中,用 // tag::firstQl[]
和 // end::firstQl[]
圈出一个代码片段:
// import 语句省略...
/**
* Author: DQinYuan
*/
public class Express4RunnerTest {
// 省略...
@Test
public void docQuickStartTest() {
// tag::firstQl[]
Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
Map<String, Object> context = new HashMap<>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS);
assertEquals(7, result);
// end::firstQl[]
}
// 省略...
}
然后在文档 README-source.adoc
中就可以 firstQl
这个 tag 引用代码片段:
=== 第一个 QLExpress 程序
[source,java,indent=0]
----
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl]
----
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl]
用于引用 Express4RunnerTest 文件中被 firstQl tag 包围的代码片段,其他的部分,等价于 Markdown 下面的写法:
### 第一个 QLExpress 程序
```java
```
这个 adoc 文档在渲染后,就会用单测中真实的代码片段替换掉 include
所占的位置,如下:
缺点就是 adoc 的语法和 Markdown 相差还挺大的,对以前用 Markdown 写文档的程序员有一定的熟悉成本。但是现在有 AI 啊,我们可以先用 Markdown 把文档写好,交给 Kimi 把它翻译成 Markdown。我对 adoc 的古怪语法也不是很熟悉,并且项目以前的文档都是 Markdown 写,都是 AI 帮我翻译的。
Github 渲染 adoc 文档的坑
我最开始尝试在 Github 上用 README.adoc
代替 README.md
,发现其中的 include
语法并没有生效:
Github 对于 adoc
include
的渲染逻辑还挺诡异的,既不展示引用文件的内容,也没有原样展示 adoc 代码
查询资料发现 Github 根本不支持 adoc 的 include
语法的渲染(参考)。不过好在参考文档中也给了解决方案:
- 源码中用
README-source.adoc
编写文档 - 使用 Git Action 监听
README-source.adoc
文件的变化。如果有变动,则使用asciidoctor
提供的命令行工具先预处理一下include
语法,将引用的内容都先引用进来。再将预处理的后的内容更新到README.adoc
中,这样README.adoc
就都是 Github 支持的语法了,可以直接在 Github 页面上渲染
Github Action 的参考配置如下(QLExpress中的配置文件):
name: Reduce Adoc
on:
push:
paths:
- README-source.adoc
branches: ['**']
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Install Asciidoctor Reducer
run: sudo gem install asciidoctor-reducer
- name: Reduce README
# to preserve preprocessor conditionals, add the --preserve-conditionals option
run: asciidoctor-reducer --preserve-conditionals -o README.adoc README-source.adoc
- name: Commit and Push README
uses: EndBug/add-and-commit@v9
with:
add: README.adoc
添加这个配置后,你会发现很多额外的 Commit,就是 Git Action 在预处理 README-source.adoc
后,对 README.adoc
发起的提交:
至此,就再也不用担心被人吐槽文档不专业啦。
来源:juejin.cn/post/7464247481227100196
URL地址末尾加不加”/“有什么区别
URL 结尾是否带 /
主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下:
1. 基础概念
- URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。
- 目录 vs. 资源:
- 以
/
结尾的 URL 通常表示目录,例如:
https://example.com/folder/
- 不以
/
结尾的 URL 通常指向具体的资源(如文件),例如:
https://example.com/file
- 以
2. 带 /
和不带 /
的具体区别
(1)目录 vs. 资源
https://example.com/folder/
- 服务器通常会将其解析为 目录,并尝试返回该目录下的默认文件(如
index.html
)。
- 服务器通常会将其解析为 目录,并尝试返回该目录下的默认文件(如
https://example.com/folder
- 服务器可能会将其视为 文件,如果
folder
不是文件,而是目录,服务器可能会返回 301 重定向到folder/
。
- 服务器可能会将其视为 文件,如果
📌 示例:
- 访问
https://example.com/blog/
- 服务器可能返回
https://example.com/blog/index.html
。
- 服务器可能返回
- 访问
https://example.com/blog
(如果blog
是个目录)
- 服务器可能重定向到
https://example.com/blog/
,再返回index.html
。
- 服务器可能重定向到
(2)相对路径解析
URL 末尾是否有 /
会影响相对路径的解析。
假设 HTML 页面包含以下 <img>
标签:
<img src="image.png">
📌 示例:
- 访问
https://example.com/folder/
- 访问
https://example.com/folder
- 图片路径解析为
https://example.com/image.png
- 可能导致 404 错误,因为
image.png
在folder/
里,而浏览器错误地去example.com/
下查找。
- 图片路径解析为
原因:
- 以
/
结尾的 URL,浏览器会认为它是一个目录,相对路径会基于folder/
解析。 - 不带
/
,浏览器可能认为folder
是文件,相对路径解析可能会出现错误。
(3)SEO 影响
搜索引擎对 https://example.com/folder/
和 https://example.com/folder
可能会视为两个不同的页面,导致 重复内容问题,影响 SEO 排名。因此:
- 网站通常会选择 一种形式 并用 301 重定向 规范化 URL。
- 例如:
https://example.com/folder
自动跳转 到https://example.com/folder/
。- 反之亦然。
(4)API 请求
对于 RESTful API,带 /
和不带 /
可能导致不同的行为:
https://api.example.com/users
- 可能返回所有用户数据。
https://api.example.com/users/
- 可能返回 404 或者产生不同的结果(取决于服务器实现)。
一些 API 服务器对 /
非常敏感,因此最好遵循 API 文档的规范。
3. 总结
URL 形式 | 作用 | 影响 |
---|---|---|
https://example.com/folder/ | 目录 | 通常返回 folder/ 下的默认文件,如 index.html ,相对路径解析基于 folder/ |
https://example.com/folder | 资源(或重定向) | 可能被解析为文件,或者服务器重定向到 folder/ ,相对路径解析可能错误 |
https://api.example.com/data/ | API 路径 | 可能与 https://api.example.com/data 表现不同,具体由 API 设计决定 |
如果你在开发网站,建议:
- 统一 URL 规则,例如所有目录都加
/
或者所有请求都不加/
,然后用 301 重定向 确保一致性。 - 测试 API 的行为,确认带
/
和不带/
是否影响请求结果。
来源:juejin.cn/post/7468112128928350242
用node帮老婆做excel工资表
我是天元,立志做1000个有趣的项目的前端。如果你喜欢的话,请点赞,收藏,转发。评论领取
零花钱+100
勋章
背景
我老婆从事HR的工作,公司有很多连锁店,她需要将所有的门店的工资汇总计算,然后再拆分给各门店请确认,最后再提供给财务发工资。
随着门店数量渐渐增多,渐渐的我老婆已经不堪重负,每天加班都做不完,严重影响夫妻感情生活。
最终花费了2天的时间,完成了整个node程序,她只需要传入工资表,相应的各种表格在10s内自动输出。目前已正式交付,得到了每月零花钱提高100元的重大成果
。
整体需求
- 表格的导入和识别
- 表格的计算(计算公式要代入),表格样式正确
- 最终结果按照门店拆分为工资表
需求示例(删减版)
需求为,根据传入的基本工资及补发补扣,生成总工资表,门店工资表,财务工资表发放表。
工资表中字段为门店,姓名,基本工资,补发补扣,最终工资(基本工资+补发补扣)。最后一行为总计
门店工资表按照每个门店,单独一个表格,字段同工资表。最后一行为总计
工资表
基础工资
补发补扣
技术选型
这次的主力库为exceljs
,官方文档介绍如下
读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。
一个 Excel 电子表格文件逆向工程项目
选择exceljs是因为它支持完整的excel的样式及公式。
安装及目录结构
优先安装exceljs
npm init
yarn add exceljs
创建input,out,src三个文件夹,src放入index.js
package.json增加start脚本
"scripts": {
"start": "node src/index.js"
},
代码相关
导入
通过new Excel.Workbook();
创建一个工作簿,通过workbook.xlsx.readFile
来导入文件, 注意这是个promise
const ExcelJS = require("exceljs");
const path = require("path");
const inputPath = path.resolve(__dirname, "../input");
const outputPath = path.resolve(__dirname, "../out");
const loadInput =async () => {
const workbook = new ExcelJS.Workbook();
const inputFile = await workbook.xlsx.readFile(inputPath + "/工资表.xlsx")
};
loadInput()
数据拆分
通过getWorksheet
Api,我们可以获取到对应的工作表的内容
const loadInput =async () => {
...
// 基本工资
const baseSalarySheet = inputFile.getWorksheet("基本工资");
// 补发补扣
const supplementSheet = inputFile.getWorksheet("补发补扣");
}
然后我们需要进一步的来进行拆分,因为第一行为每个工作表的头,这部分在我们实际数据处理中不会使用,所以通过getRows
来获取实际的内容。
const baseSalaryContent = baseSalarySheet.getRows(
2,
baseSalarySheet.rowCount
);
baseSalaryContent.map((row) => {
console.log(row.values);
});
/**
[ <1 empty item>, '2024-02', '海贼王', '路飞', 12000 ]
[ <1 empty item>, '2024-02', '海贼王', '山治', 8000 ]
[ <1 empty item>, '2024-02', '火影忍者', '鸣人', '6000' ]
[ <1 empty item>, '2024-02', '火影忍者', '佐助', 7000 ]
[ <1 empty item>, '2024-02', '火影忍者', '雏田', 5000 ]
[ <1 empty item>, '2024-02', '一拳超人', '琦玉', 4000 ]
[]
[]
**/
可以看到实际的内容已经拿到了,我们要根据这些内容拼装一下最终便于后续的调用。
我们可以通过 row.getCell
Api获取到对应某一列的内容,例如门店是在B
列,那么我们就可以使用row.getCell('B')
来获取。
因为我们需要拆分门店,所以这里的基本工资,我们以门店为单位,把数据进行拆分
const baseSalary = {};
baseSalaryContent.forEach((row) => {
const shopName = row.getCell("B").value;
if (!shopName) return; // 过滤空行
const name = row.getCell("C").value;
const salary = row.getCell("D").value;
if (!baseSalary[shopName]) {
baseSalary[shopName] = [];
}
baseSalary[shopName].push({
name,
salary,
});
});
这样我们得到了一个以门店名称为key的对象,value为该门店的员工信息数组。利用相同方法,获取补发补扣。因为每个人已经确定了门店,所以后续只需要根据姓名来做key,拆分成一个object即可
// 补发补扣
const supplement = {};
supplementSheet.getRows(2, supplementSheet.rowCount).forEach((row) => {
const name = row.getCell("C").value;
const type = row.getCell("H").value;
let count = row.getCell("D").value;
// 如果为补扣,则金额为负数
if (type === "补扣") {
count = -count;
}
if (!supplement[name]) {
supplement[name] = 0;
}
supplement[name] += count;
});
数据组合
门店工资表
因为每个门店需要独立一张表,所以需要遍历baseSalary
Object.keys(baseSalary).forEach((shopName) => {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("工资表");
// 添加表头
worksheet.addRow([
"序号",
"门店",
"姓名",
"基本工资",
"补发补扣",
"最终工资",
]);
baseSalary[shopName].forEach((employee, index) => {
worksheet.addRow([
index + 1,
shopName,
employee.name,
+employee.salary,
supplement[employee.name] || 0,
+employee.salary + (supplement[employee.name] || 0),
]);
});
});
此时你也可以快进到表格输出
来查看输出的结果,以便随时调整
这样我们就把基本工资已经写入工作表了,但是这里存在问题,最终工资使用的是一个数值,而没有公式。所以我们需要改动下
worksheet.addRow([ index + 1, shopName, employee.name, employee.salary, supplement[employee.name] || 0,
{
formula: `D${index + 2}+E${index + 2}`,
result: employee.salary + (supplement[employee.name] || 0),
},
]);
这里的formula
将对应到公式,而result
是显示的值,这个值是必须写入的,如果你写入了错误的值,会在表格中显示该值,但是双击后,公式重新计算,会替换为新的值。所以这里必须计算正确
合计
依照上方的逻辑,继续添加一行作为合计,但是之前计算的时候,需要添加一个临时变量,记录下合计的相关内容。
const count = [0, 0, 0];
baseSalary[shopName].forEach((employee, index) => {
count[0] += +employee.salary;
count[1] += supplement[employee.name] || 0;
count[2] += +employee.salary + (supplement[employee.name] || 0);
worksheet.addRow([
index + 1,
shopName,
employee.name,
+employee.salary,
supplement[employee.name] || 0,
{
formula: `D${index + 2}+E${index + 2}`,
result: +employee.salary + (supplement[employee.name] || 0),
},
]);
});
然后在尾部添加一行
worksheet.addRow([ "合计", "", "", { formula: `SUM(D2:D${baseSalary[shopName].length + 1})`,
result: count[0],
},
{
formula: `SUM(E2:E${baseSalary[shopName].length + 1})`,
result: count[1],
},
{
formula: `SUM(F2:F${baseSalary[shopName].length + 1})`,
result: count[2],
},
]);
美化
表格的合并,可以使用mergeCells
worksheet.mergeCells(
`A${baseSalary[shopName].length + 2}:C${baseSalary[shopName].length + 2}`
);
这样就合并了我们的最后一行的前三列,接下来我们要给表格添加线条。
对于批量的添加,可以直接使用addConditionalFormatting
,它将在一个符合条件的单元格范围内添加规则
worksheet.addConditionalFormatting({
ref: `A1:F${baseSalary[shopName].length + 2}`,
rules: [
{
type: "expression",
formulae: ["true"],
style: {
border: {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
},
alignment: { vertical: "top", horizontal: "left", wrapText: true },
},
},
],
});
表格输出
现在门店工资表已经拆分完成,我们可以直接保存了,使用xlsx.writeFile
Api来保存文件
Object.keys(baseSalary).forEach((shopName) => {
...
workbook.xlsx.writeFile(outputPath + `/${shopName}工资表.xlsx`);
})
最终效果
相关代码地址
来源:juejin.cn/post/7346421986607087635
蓝牙耳机丢了,我花几分钟写了一个小程序,找到了!
你是否曾经经历过蓝牙耳机不知道丢到哪里去的困扰?特别是忙碌的早晨,准备出门时才发现耳机不见了,整个心情都被影响。幸运的是,随着技术的进步,我们可以利用一些简单的小程序和蓝牙技术轻松找到丢失的耳机。今天,我要分享的是我如何通过一个自制的小程序,利用蓝牙发现功能,成功定位自己的耳机。这不仅是一次有趣的技术尝试,更是对日常生活中类似问题的一个智能化解决方案。
1. 蓝牙耳机丢失的困扰
现代生活中,蓝牙耳机几乎是每个人的必备品。然而,耳机的体积小、颜色常常与周围环境融为一体,导致丢失的情况时有发生。传统的寻找方式依赖于我们对耳机放置地点的记忆,但往往不尽人意。这时候,如果耳机还保持在开机状态,我们就可以借助蓝牙技术进行定位。然而,市场上大部分设备并没有自带这类功能,而我们完全可以通过编写小程序实现。
2. 蓝牙发现功能的原理
蓝牙发现功能是通过设备之间的信号传输进行连接和识别的。当一个蓝牙设备处于开机状态时,它会周期性地广播自己的信号,周围的蓝牙设备可以接收到这些信号并进行配对。这个过程的背后其实是信号的强度和距离的关系。当我们在手机或其他设备上扫描时,能够检测到耳机的存在,但并不能直接告诉我们耳机的具体位置。此时,我们可以通过信号强弱来推测耳机的大概位置。
3. 实现步骤:从构想到实践
有了这个想法后,我决定动手实践。首先,我使用微信小程序作为开发平台,利用其内置的蓝牙接口实现设备扫描功能。具体步骤如下:
- • 环境搭建:选择微信小程序作为平台主要因为其开发简便且自带蓝牙接口支持。
- • 蓝牙接口调用:调用
wx.openBluetoothAdapter
初始化蓝牙模块,确保设备的蓝牙功能开启。 - • 设备扫描:通过
wx.startBluetoothDevicesDiscovery
函数启动设备扫描,并使用wx.onBluetoothDeviceFound
监听扫描结果。 - • 信号强度分析:通过读取蓝牙信号强度(RSSI),结合多次扫描的数据变化,推测设备的距离,最终帮助定位耳机。
在代码的实现过程中,信号强度的变化尤为重要。根据RSSI值的波动,我们可以判断耳机是在靠近还是远离,并通过走动测试信号的变化,逐渐缩小搜索范围。
下面是我使用 Taro 实现的全部代码:
import React, { useState, useEffect } from "react";
import Taro, { useReady } from "@tarojs/taro";
import { View, Text } from "@tarojs/components";
import { AtButton, AtIcon, AtProgress, AtList, AtListItem } from "taro-ui";
import "./index.scss";
const BluetoothEarphoneFinder = () => {
const [isSearching, setIsSearching] = useState(false);
const [devices, setDevices] = useState([]);
const [nearestDevice, setNearestDevice] = useState(null);
const [isBluetoothAvailable, setIsBluetoothAvailable] = useState(false);
const [trackedDevice, setTrackedDevice] = useState(null);
useEffect(() => {
if (isSearching) {
startSearch();
} else {
stopSearch();
}
}, [isSearching]);
useEffect(() => {
if (devices.length > 0) {
const nearest = trackedDevice
? devices.find((d) => d.deviceId === trackedDevice.deviceId)
: devices[0];
setNearestDevice(nearest || null);
} else {
setNearestDevice(null);
}
}, [devices, trackedDevice]);
const startSearch = () => {
const startDiscovery = () => {
setIsBluetoothAvailable(true);
Taro.startBluetoothDevicesDiscovery({
success: () => {
Taro.onBluetoothDeviceFound((res) => {
const newDevices = res.devices.map((device) => ({
name: device.name || "未知设备",
deviceId: device.deviceId,
rssi: device.RSSI,
}));
setDevices((prevDevices) => {
const updatedDevices = [...prevDevices];
newDevices.forEach((newDevice) => {
const index = updatedDevices.findIndex(
(d) => d.deviceId === newDevice.deviceId
);
if (index !== -1) {
updatedDevices[index] = newDevice;
} else {
updatedDevices.push(newDevice);
}
});
return updatedDevices.sort((a, b) => b.rssi - a.rssi);
});
});
},
fail: (error) => {
console.error("启动蓝牙设备搜索失败:", error);
Taro.showToast({
title: "搜索失败,请重试",
icon: "none",
});
setIsSearching(false);
},
});
};
Taro.openBluetoothAdapter({
success: startDiscovery,
fail: (error) => {
if (error.errMsg.includes("already opened")) {
startDiscovery();
} else {
console.error("初始化蓝牙适配器失败:", error);
Taro.showToast({
title: "蓝牙初始化失败,请检查蓝牙是否开启",
icon: "none",
});
setIsSearching(false);
setIsBluetoothAvailable(false);
}
},
});
};
const stopSearch = () => {
if (isBluetoothAvailable) {
Taro.stopBluetoothDevicesDiscovery({
complete: () => {
Taro.closeBluetoothAdapter({
complete: () => {
setIsBluetoothAvailable(false);
},
});
},
});
}
};
const getSignalStrength = (rssi) => {
if (rssi >= -50) return 100;
if (rssi <= -100) return 0;
return Math.round(((rssi + 100) / 50) * 100);
};
const getDirectionGuide = (rssi) => {
if (rssi >= -50) return "非常接近!你已经找到了!";
if (rssi >= -70) return "很近了,继续朝这个方向移动!";
if (rssi >= -90) return "正确方向,但还需要继续寻找。";
return "信号较弱,尝试改变方向。";
};
const handleDeviceSelect = (device) => {
setTrackedDevice(device);
Taro.showToast({
title: `正在跟踪: ${device.name}`,
icon: "success",
duration: 2000,
});
};
return (
<View className="bluetooth-finder">
{isSearching && (
<View className="loading-indicator">
<AtIcon value="loading-3" size="30" color="#6190E8" />
<Text className="loading-text">搜索中...Text>
View>
)}
{nearestDevice && (
<View className="nearest-device">
<Text className="device-name">{nearestDevice.name}Text>
<AtProgress
percent={getSignalStrength(nearestDevice.rssi)}
status="progress"
isHidePercent
/>
<Text className="direction-guide">
{getDirectionGuide(nearestDevice.rssi)}
Text>
View>
)}
<View className="device-list">
<AtList>
{devices.map((device) => (
<AtListItem
key={device.deviceId}
title={device.name}
note={`${device.rssi} dBm`}
extraText={
trackedDevice && trackedDevice.deviceId === device.deviceId
? "跟踪中"
: ""
}
arrow="right"
onClick={() => handleDeviceSelect(device)}
/>
))}
AtList>
View>
<View className="action-button">
<AtButton
type="primary"
circle
onClick={() => setIsSearching(!isSearching)}
>
{isSearching ? "停止搜索" : "开始搜索"}
AtButton>
View>
View>
);
};
export default BluetoothEarphoneFinder;
嘿嘿,功夫不负苦心人,我最终通过自己的小程序找到了我的蓝牙耳机。
我将我的小程序发布到了微信小程序上,目前已经通过审核,可以直接使用了。搜索老码宝箱 即可体验。
顺带还加了非常多的小工具,而且里面还有非常多日常可能会用到的工具,有些还非常有意思。
比如
绘制函数图
每日一言
汇率转换(实时)
BMI 计算
简易钢琴
算一卦
这还不是最重要的
最重要的是,这里的工具是会不断增加的,而且,更牛皮的是,你还可以给作者提需求,增加你想要的小工具,作者是非常欢迎一起讨论的。有朝一日,你也希望你的工具也出现在这个小程序上,被千万人使用吧。
4. 实际应用与优化空间
这个小程序的实际效果超出了我的预期。我能够通过它快速找到丢失的耳机,整个过程不到几分钟时间。然而,值得注意的是,由于蓝牙信号会受到环境干扰,例如墙体、金属物等,导致信号强度并不总是精确。在后续的优化中,我计划加入更多的信号处理算法,例如利用三角定位技术,结合多个信号源来提高定位精度。此外,还可以考虑在小程序中加入可视化的信号强度图,帮助用户更直观地了解耳机的大致方位。
一些思考:
蓝牙耳机定位这个小程序的开发,展示了技术在日常生活中的强大应用潜力。虽然这个项目看似简单,但背后的原理和实现过程非常具有教育意义。通过这次尝试,我们可以看到,借助开源技术和简单的编程能力,我们能够解决许多日常生活中的实际问题。
参考资料:
- 微信小程序官方文档:developers.weixin.qq.com
- 蓝牙信号强度(RSSI)与距离关系的研究:http://www.bluetooth.com
- 个人开发者经验分享: 利用蓝牙发现功能定位设备
来源:juejin.cn/post/7423610485180727332
前端可玩性UP项目:大屏布局和封装
前言
autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。
这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。
分析设计稿
分析设计稿之前先吐槽一下大屏这种展现形式,这简直就是自欺欺人、面子工程的最直接的诠释,是吊用没有,只为了好看,如果设计的再不好看啊,这就纯纯是屎。在我的理解中,这就像把PPT放到了web端,仅此而已。
但是王哥告诉我:"你看似没有用的东西,其实都有用,很多想真正做出有用的产品的企业,没钱,就要先把面子工程做好,告诉别人他们要做一件什么事,这样投资人才会看到,后面才有机会发展。"
布局方案
上图展示了一个传统意义上且比较普遍的大屏形态,分为四个部分,分别是
头部
头部经常放标题、功能菜单、时间、天气
左右面板
左右面板承载了各种数字和报表,还有视频、轮播图等等
中间
中间部分一般放地图,这其中又分假地图(一张图片)、图表地图(如echarts)、地图引擎(如:leaflet、mapbox、高德、百度)。或者有的还会放3D场景,一般有专门的同事去做3D场景,然后导入到web端。
大屏的设计通常的分辨率是 1920*1080 的,这也是迄今为止应用最广泛的显示器配置,当然也有基于客户屏幕做的异形分辨率,这就五花八门了。
但是万变不离其宗,分辨率的变化不会影响它的基本结构,根据上面的图,我们可以快速构建结构代码
<div class='Box'>
<div class="header"></div>
<div class="body">
<div class="leftPanel"></div>
<div class="mainMap"></div>
<div class="rightPanel"></div>
</div>
</div>
上面的代码实现了最简单的上下(Box)+左右(body)的布局结构,完全不需要任何定位策略。
要实现上图的效果,只需最简单的CSS即可完成布局。
组件方案
大屏虽然是屎,但是是一种可玩性很强的项目,想的越复杂,做起来就越复杂,想的越简单,做起来就越简单。
可以疯狂封装炫技,因为大屏里面的可玩组件简直太多了,且涵盖的太全了,想怎么玩都可以,包括但不限于 各类图表库的封装(echarts、highCharts、vChart)、轮播图(swiper)、地图引擎、视频库(包括直播流)等等。
如果想简单,甚至可以不用封装,可以看到结构甚至简单到不用CSS几行就可以搭建出基本框架,只把header、leaftPanel、rightPanel、map封装一下就可以了。
这里还有一个误区,就是大家都喜欢把 大型的组件库 拉到大屏里来用,结果做完了发现好像只用了一个 toast 和一个下拉组件,项目打包后却增大了几十倍的体积,其实像这种简单的组件,完全可以手写,或者找小的独立包来用,一方面会减小体积,不至于让项目臃肿,另一方面可以锻炼自己的手写能力,这才是有必要的封装。
适配
目前主流的适配方案,依然是 rem 方案,其原理就是根据根元素的 font-size 自动计算大小,但是此方法需要手动计算 rem 值,或者使用第三方插件如postcss等,但是此方案还有一个弊端,就是无法向下兼容,因为浏览器中最小的文字大小是12px。
vh/vw方案就不再赘述了,原理基本和 rem/em 相似,都涉及到单位的转换。
autofit.js
主要讲一下使用 autofit.js 如何快速实现适配。
不支持的场景
首先 autofit.js 不支持 elementUI(plus)、ant-design等组件库,具体是不支持包含popper.js的组件,popper.js 在计算弹出层位置时,不会考虑 scale 后的元素的视觉大小,所以会造成弹出元素的位置偏移。
其次,不支持 百度地图,百度地图对窗口缩放事件没有任何处理,有同学反馈说,即使使用了resize属性,百度地图在和autofit.js共同使用时,也会有事件热区偏移的问题。而且百度地图使用 bd-09 坐标系,和其他图商不通用,引擎的性能方面也差点意思,个人不推荐在开发中使用百度地图。
然后一些拖拽库,如甘特图插件,可能也不支持,他们在计算鼠标位置时同样没有考虑 scale 后的元素的视觉大小。
用什么单位
不支持的单位:vh、vw、rem、em
让我诧异的是,老有人问我该用什么单位,主要徘徊在 px 和 % 之间,加群的同学多数是因为用了相对单位,导致留白了。不过人各有所长,跟着各位大佬我也学到了很多。
看下图
假如有两个宽度为1000的元素,他们内部都有一个子元素,第一个用百分比设置为 width:50%;left:1%
, 第二个设置为 wdith:500px;left:10px
。此时,只要外部的1000px的容器宽度不变,这两个内部元素在视觉上是一模一样的,且在实际数值上也是一模一样的,他们宽度都为500px,距离左侧10px。
但是如果外部容器变大了,来看一下效果:
在样式不变的情况下,仅改变外部容器大小,差异就出来了,由上图可知,50%的元素依然占父元素的一半,实际宽度变成了 1000px,距离左侧的实际距离变成了 20px。
这当然不难理解,百分比单位是根据 最近的、有确定大小的父级元素计算的。
所以,应该用什么单位其实取决于想做什么,举个例子:在1920*1080
基础上开发,中间的地图写成了宽度为 500px ,这在正常情况下,看起来没有任何问题,它大概占屏幕的 26%,当屏幕分辨率达到4096*2160
时,它在屏幕上只占 12%,看起来就是缩在一角。而当你设置宽度为26%时,无论显示器如何变化,它始终占屏幕26%。
autofit.js 所干的事,就是把1000px 变成了 2000px或者把2000px变成了1000px,并给它设置了一个合适的缩放大小。
图表、图片拉伸
背景或各种图片按需设置 object-fit: cover;
即可
图表如echarts一般推荐使用百分比,且监听窗口变化事件做resize()
结语
再次感慨,大道至简,事情往往没有那么复杂,祝各位前程似锦。
来源:juejin.cn/post/7344625554530779176
我终于从不想上班又不能裸辞的矛盾中挣扎出来了
最近的状态有一种好像一个泄了气的皮球的感觉一样,就是对生活中很多事情都提不起来兴趣。
我希望自己可以多看一点书,但是我不想动;我希望自己可以练习书法,但是我不想动;我希望自己可以学会一门乐器,但是我不想动。
相比上面三点,我更希望的是我可以早上起来不用上班,但是这只是我的希望而已。
这就是我最近的生活状态。
我有一种我的生活仿佛失去了控制的感觉,每一天我的内心好像都有一个小人在不断呐喊,说我不想上班。因为这个声音,我一度非常非常想要裸辞,但是我为什么没有裸辞呢?
还不是因为我买房买车欠了十几万,我到现在才还了两万而已,再加上我每个月还有房贷要还。
然而,当我经常不情愿地做着跟我心里想法相悖的行为的时候,我发现自己常常会做一些小动作来向自己表达抗议和不满。
比如说,我的工作会变得越来越低效,上班的时候会偷偷地摸鱼,还有就是变得越来越容易拖延。
就好像这样的我,可以让那个不想上班的我,取得了一丢丢的小胜利一样。
一旦开始接受自己没有办法辞职,并且还要上个几十年班这样的结果时,就会让人有一种破罐子破摔的想法。
而且随之而来的是一种对未来,对生活的无力感。
这种无力感渐渐地渗透在我生活的方方面面,以至于让我慢慢地对很多东西都提不起兴趣,我生活中的常态就变成了不想动。
但是有趣的事情发生了,有一天我在和我朋友聊天的时候,我的脑子里面突然出现了一个想法,就是我决定两年之后我要实现我不上班的这个目标。
当有了这个想法之后,我就开始认真思考这件事情的可行度。
通过分析我现在收支情况,我把两年之内改成了2026年之前,因为我觉得这样会让我更加信服这个目标的可行性。
同时我把这个想法也拆分成了两个更为具体的目标,其中一个就是我要在2026年之前还完欠的所有钱。
第二个就是我需要给自己存够20万,这20万是不包括投资理财或者基金股票里面的钱,而是我完全可以自由支配的。
毕竟没有人可以在没有工作的情况下,没有收入的情况下。没有存款的情况下,还能保持一个不焦虑的状态。
当我得出了这两个具体的目标之后,我整个人瞬间被一种兴奋的状态填满,我瞬间找到了工作的意义和动力。
也许你会说,我的这个想法对我现在生活根本起不到任何的改变作用。
我依旧还需要每天七点起床,还是要每天重复地去过我两点一线的生活。
但是于我自己而言,当我给我上班的这件事情加了一个两年的期限之后,我突然觉得我的未来,我的生活都充满了希望。
我整个人从不想动的状态,变成了一种被兴奋的状态填满的感觉。
所以,如果你和我一样有一些类似的困扰,有一些你不想做而又不得不做的事情,让你有一种深陷泥潭,无法前进的感觉,那你不妨试一下这个方法。
结合你自己的实际情况,为你不想做这件事情,设计一个期限,这个期限必须要是你认可,你接受,并且你认为你可以在这个截止时间之前完成的。
我想这个决定应该会对你的生活带来一些改变。
来源:juejin.cn/post/7428154034480906278
2025年了,令人唏嘘的Angular,现在怎么样了🚀🚀🚀
迅速崛起和快速退出
时间回到2014年,此时的 Angular 1.x
习得了多种武林秘籍,左手降龙十八掌、右手六脉神剑,哦不,左手双向数据绑定
、右手依赖注入
、上能模块化开发
、下有模板引擎
和 前端路由
, 背后还有Google
这个风头无两的带头大哥做技术背书,可以说集万千功能和宠爱于一身,妥妥的主角光环。
而此时的江湖,B端
开发正尝到了 SPA
的甜头,积极的从传统的 MVC
开发模式转变为更为方便快捷的单页面应用开发模式,
文章同步在公众号:萌萌哒草头将军,欢迎关注!
一拍即合,强大的一站式单页面开发框架Angular
自然而然,就成了公认的武林盟主,江湖一哥。
但是好景不长,2016年9月14日 Angular 2.x
的发布,彻底断送了武林盟主的宝座,
Vue
:大哥,你可是真给机会呀!
2.0
使用ts
彻底重写(最早支持ts
的框架)、放弃了脏检查更新机制,引入了响应式系统、使用现代浏览器标准、加入装饰器语法,和 1.0
完全不兼容。可以从上图看到,此时大家基本上还不太接受ts
!
新手面对陡然升高的学习曲线叫苦连连,已经入坑的开发者因为巨大的迁移工作而怨声载道。
此时,默默耕耘了两年的小弟,Vue
已经拥有完备的本地化文档和丰富的可选生态,而且作为新手你只要会使用html
、css
、javascript
,就可以上手写项目了。
所以,此时的 Vue
振臂一呼:“王侯将相宁有种乎!”,立马新皇加冕!
积极改变,三拜义父的数据驱动
忆往昔峥嵘岁月稠,恰同学少年,风华正茂;书生意气,挥斥方遒。
一转眼,angular 已经发布第19
个大版本了(平均一年两个版本)。
失去武林盟主的Angular
,飘零半生,未逢明主,公若不弃,Angular
愿拜为义父,
从 脏检查机制
到 响应式系统
,再到Signals系统
, Angular
历经沧桑的数据驱动方式可以说是前端发展的缩影。
脏检查机制
脏检查机制
是通过拦截异步操作,http
setTimeout
用户交互事件等,触发变更检测系统,从根组件开始检查组件中数据是否有更新,有更新时,对应的 $scope
变量会被标记为 脏
,然后同步的更新dom
的内容,重新开始变更检查,直到稳定后标记为干净,即通过稳定性检查!
<!DOCTYPE html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title>AngularJS Counter</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
</head>
<body ng-controller="CounterController as ctrl">
<h1>Count: {{ ctrl.count }}</h1>
<h2>Double Count: {{ ctrl.doubleCount() }}</h2>
<button ng-click="ctrl.increment()">+1</button>
<script>
angular.module('myApp', [])
.controller('CounterController', function() {
var vm = this;
vm.count = 0;
vm.increment = function() {
vm.count++;
console.log('Latest count:', vm.count);
};
vm.doubleCount = function() {
return vm.count * 2;
};
});
</script>
</body>
</html>
但是这种检查机制存在缺陷,例如,当数据量十分庞大时,就会触发非常多次的脏检查机制
。
响应式系统
响应式系统
没有出现之前,脏检查机制
是唯一的选择,但是响应式系统
凭借快速轻便的特点,立马在江湖上引起了不小的轰动,Angular
也放弃了笨重的脏检查机制采用了响应式系统
!
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1>Count: {{ count }}</h1>
<h2>Double Count: {{ doubleCount() }}</h2>
<button (click)="increment()">+1</button>
`,
})
export class AppComponent {
count: number = 0;
increment() {
this.count++;
console.log('Latest count:', this.count);
}
doubleCount() {
return this.count * 2;
}
}
和我们熟知的Vue
的响应式不同,Angular
的响应式采用双向数据流的设计,这也使得它在面对复杂项目时,性能和维护上不如Vue
快捷方便。
所以,为了更好的驾驭双向数据流
的响应式系统,Angular
也是自创了很多绝学,例如:局部变更检测。
该绝学主要招式:组件级变更检测策略
、引入zonejs
、OnPush
策略等。
1. 组件级变更检测策略
每个组件都有自己的更新策略,只有组件的属性和文本发生变化时,才会触发变更检测!
2. 引入zonejs
引入zonejs
拦截http
setTimeout
用户交互事件等异步操作
3. OnPush
策略
默认情况下,整个组件树在变更时更新。
但是开发者可以选择 OnPush
策略,使得组件仅在输入属性发生变化、事件触发或手动调用时才进行变更检测。这进一步大大减少了变更检测的频率,适用于数据变化不频繁的场景。
Signals系统
很快啊,当SolidJS
凭借优异的信号系统在江湖上闯出了响亮的名声,这时,大家才意识到,原来还有更优秀的开发方式!signal
系统的开发方式,也被公认为新一代的武林绝技!
于是,Angular 16
它来了,它带着signal
、memo
、effect
三件套走来了!
当使用signal
时,更新仅仅发生在当前组件。
// app.component.ts
import { Component, signal, effect, memo } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1>Count: {{ count() }}</h1>
<h2>Double Count: {{ doubleCount() }}</h2>
<button (click)="increment()">+1</button>
`,
styles: []
})
export class AppComponent {
// 使用 signal 来管理状态
count = signal(0);
// 使用 memo 来计算 doubleCount
doubleCount = memo(() => this.count() * 2);
constructor() {
// 使用 effect 来监听 count 的变化
effect(() => {
console.log('Latest count:', this.count());
});
}
increment() {
// 更新 signal 的值
this.count.set(this.count() + 1);
}
}
总结
Angular
虽然在国内市场一蹶不振,但是在国际市场一直默默耕耘 10
年。它作为一站式解决方案的框架,虽然牺牲了灵活性,但是也为开发者提供了沉浸式开发的选择!
且它不断创新、积极拥抱新技术的精神令人十分钦佩!
今天的内容就这些了,如果你觉得还不错,可以关注我。
如果文章中存在问题,欢迎指正!
来源:juejin.cn/post/7468526097011097654
这个排队系统设计碉堡了
先赞后看,Java进阶一大半
各位好,我是南哥。
我在网上看到某厂最后一道面试题:如何设计一个排队系统?
关于系统设计的问题,大家还是要多多思考,可能这道题考的不是针对架构师的职位,而是关于你的业务设计能力。如果单单只会用开源软件的API,那似乎我们的竞争力还可以再强些。学习设计东西、创作东西,把我们设计的产品给别人用,那竞争力一下子提了上来。
15岁的初中生开源了 AI 一站式 B/C 端解决方案chatnio,该产品在上个月被以几百万的价格收购了。这值得我们思考,程序创造力、设计能力在未来会变得越来越重要。
⭐⭐⭐收录在《Java学习/进阶/面试指南》:https://github/JavaSouth
精彩文章推荐
1.1 数据结构
排队的一个特点是一个元素排在另一个元素的后面,形成条状的队列。List结构、LinkedList链表结构都可以满足排队的业务需求,但如果这是一道算法题,我们要考虑的是性能因素。
排队并不是每个人都老老实实排队,现实会有多种情况发生,例如有人退号,那属于这个人的元素要从队列中删除;特殊情况安排有人插队,那插入位置的后面那批元素都要往后挪一挪。结合这个情况用LinkedList链表结构会更加合适,相比于List,LinkedList的性能优势就是增、删的效率更优。
但我们这里做的是一个业务系统,采用LinkedList这个结构也可以,不过要接受修改、维护起来困难,后面接手程序的人难以理解。大家都知道,在实际开发我们更常用List,而不是LinkedList。
List数据结构我更倾向于把它放在Redis里,有以下好处。
(1)数据存储与应用程序拆分。放在应用程序内存里,如果程序崩溃,那整条队列数据都会丢失。
(2)性能更优。相比于数据库存储,Redis处理数据的性能更加优秀,结合排队队列排完则销毁的特点,甚至可以不存储到数据库。可以补充排队记录到数据库里。
简单用Redis命令模拟下List结构排队的处理。
# 入队列(将用户 ID 添加到队列末尾)
127.0.0.1:6379> RPUSH queue:large user1
127.0.0.1:6379> RPUSH queue:large user2
# 出队列(将队列的第一个元素出队)
127.0.0.1:6379> LPOP queue:large
# 退号(从队列中删除指定用户 ID)
127.0.0.1:6379> LREM queue:large 1 user2
# 插队(将用户 ID 插入到指定位置,假设在 user1 之前插入 user3)
127.0.0.1:6379> LINSERT queue:large BEFORE user1 user3
1.2 业务功能
先给大家看看,南哥用过的费大厨的排队系统,它是在公众号里进行排队。
我们可以看到自己现在的排队进度。
同时每过 10 号,公众号会进行推送通知;如果 10 号以内,每过 1 号会微信公众号通知用户实时排队进度。最后每过 1 号就通知挺人性化,安抚用户排队的焦急情绪。
总结下来,我们梳理下功能点。虽然上面看起来是简简单单的查看、通知,背后可能隐藏许多要实现的功能。
1.3 后台端
(1)排队开始
后台管理员创建排队活动,后端在Redis创建List类型的数据结构,分别创建大桌、中桌、小桌三条队列,同时设置没有过期时间。
// 创建排队接口
@Service
public class QueueManagementServiceImpl {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// queueType为桌型
public void createQueue(String queueType) {
String queueKey = "queue:" + queueType;
redisTemplate.delete(queueKey); // 删除队列,保证队列重新初始化
}
}
(2)排队操作
前面顾客用餐完成后,后台管理员点击下一号,在Redis的表现为把第一个元素从List中踢出,次数排队的总人数也减 1。
// 排队操作
@Service
public class QueueManagementServiceImpl {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 将队列中的第一个用户出队
*/
public void dequeueNextUser(String queueType) {
String queueKey = "queue:" + queueType;
String userId = redisTemplate.opsForList().leftPop(queueKey);
}
}
1.4 用户端
(1)点击排队
用户点击排队,把用户标识添加到Redis队列中。
// 用户排队
@Service
public class QueueServiceImpl {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void enterQueue(String queueType, String userId) {
String queueKey = "queue:" + queueType;
redisTemplate.opsForList().rightPush(queueKey, userId);
log.info("用户 " + userId + " 已加入 " + queueType + " 队列");
}
}
(2)排队进度
用户可以查看三条队列的总人数情况,直接从Redis三条队列中查询队列个数。此页面不需要实时刷新,当然可以用WebSocket实时刷新或者长轮询,但具备了后面的用户通知功能,这个不实现也不影响用户体验。
而用户的个人排队进度,则计算用户所在队列前面的元素个数。
// 查询排队进度
@Service
public class QueueServiceImpl {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public long getUserPositionInQueue(String queueType, String userId) {
String queueKey = "queue:" + queueType;
List<String> queue = redisTemplate.opsForList().range(queueKey, 0, -1);
if (queue != null) {
return queue.indexOf(userId);
}
return -1;
}
}
(3)用户通知
当某一个顾客用餐完成后,后台管理员点击下一号。此时后续的后端逻辑应该包括用户通知。
从三个队列里取出当前用户进度是 10 的倍数的元素,微信公众号通知该用户现在是排到第几桌了。
从三个队列里取出排名前 10 的元素,微信公众号通知该用户现在的进度。
// 用户通知
@Service
public class NotificationServiceImpl {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private void notifyUsers(String queueType) {
String queueKey = "queue:" + queueType;
// 获取当前队列中的所有用户
List<String> queueList = jedis.lrange(queueKey, 0, -1);
// 通知排在10的倍数的用户
for (int i = 0; i < queueList.size(); i++) {
if ((i + 1) % 10 == 0) {
String userId = queueList.get(i);
sendNotification(userId, "您的排队进度是第 " + (i + 1) + " 位,请稍作准备!");
}
}
// 通知前10位用户
int notifyLimit = Math.min(10, queueList.size()); // 避免队列小于10时出错
for (int i = 0; i < notifyLimit; i++) {
String userId = queueList.get(i);
sendNotification(userId, "您已经在前 10 位,准备好就餐!");
}
}
}
这段逻辑应该移动到前面后台端的排队操作。
1.5 存在问题
上面的业务情况,实际上排队人员不会太多,一般会比较稳定。但如果每一条队列人数激增的情况下,可以预见到会有问题了。
对于Redis的List结构,我们需要查询某一个元素的排名情况,最坏情况下需要遍历整条队列,时间复杂度是O(n),而查询用户排名进度这个功能又是经常使用到。
对于上面情况,我们可以选择Redis另一种数据结构:Zset。有序集合类型Zset可以在O(lgn)的时间复杂度判断某元素的排名情况,使用ZRANK命令即可。
# zadd命令添加元素
127.0.0.1:6379> zadd 100run:ranking 13 mike
(integer) 1
127.0.0.1:6379> zadd 100run:ranking 12 jake
(integer) 1
127.0.0.1:6379> zadd 100run:ranking 16 tom
(integer) 1
# zrank命令查看排名
127.0.0.1:6379> zrank 100run:ranking jake
(integer) 0
127.0.0.1:6379> zrank 100run:ranking tom
(integer) 2
# zscore判断元素是否存在
127.0.0.1:6379> zscore 100run:ranking jake
"12"
我是南哥,南就南在Get到你的点赞点赞点赞。
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️
来源:juejin.cn/post/7436658089703145524
⚔️不让我在控制台上调试,哼,休想🛠️
在 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