注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

基于英雄联盟人物的加载动画,奇怪的需求又增加了!

web
1、背景 前两天老板找到我说有一个需求,要求使用英雄联盟的人物动画制作一个加载中的组件,类似于下面这样: 我定眼一看:这个可以实现,但是需要UI妹子给切图。 老板:UI? 咱们啥时候招的UI ! 我:老板,那不中呀,不切图弄不成呀。 老板:下个月绩效给你A。...
继续阅读 »

1、背景


前两天老板找到我说有一个需求,要求使用英雄联盟的人物动画制作一个加载中的组件,类似于下面这样:


iShot_2024-06-06_18.09.55.gif


我定眼一看:这个可以实现,但是需要UI妹子给切图。


老板:UI? 咱们啥时候招的UI !


我:老板,那不中呀,不切图弄不成呀。


老板:下个月绩效给你A。


我:那中,管管管。


2、调研


发动我聪明的秃头,实现这个需求有以下几种方案:



  • 切动画帧,没有UI不中❎。

  • 去lol客户端看看能不能搞到什么美术素材,3D模型啥的,可能行❓

  • 问下 gpt4o,有没有哪个老表收集的有lol英雄的美术素材,如果有那就更得劲了✅。


经过我一番搜索,发现了这个网站:model-viewer,收集了很多英雄联盟的人物模型,模型里面还有各种动画,还给下载。老表,这个需求稳了50%了!


image-20240606182312802.png


接下来有几种选择:



  • 将模型动画转成动画帧,搞成雪碧图,较为麻烦,且动画不支持切换。

  • 直接加载模型,将模型放在进度条上,较为简单,支持切换不同动画,而且可以自由过渡。就是模型文件有点大,初始化加载可能耗时较长。但是后续缓存一下就好了。


聪明的我肯定先选第二个方案呀,你糊弄我啊,我糊弄你。


3、实现


web中加载模型可以使用谷歌基于threejs封装的 model-viewer, 使用现代的 web component 技术。简单易用。


先初始化一个vue工程


 npm create vue@latest

然后将里面的初始化的组件和app.vue里面的内容都删除。


安装model-viewer依赖:


npm i three // 前置依赖
npm i @google/model-viewer

修改vite.config.js,将model-viewer视为自定义元素,不进行编译


import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
 plugins: [
   vue({
     template: {
       // 添加以下内容
       compilerOptions: {
         isCustomElement: (tag) => ['model-viewer'].includes(tag)
      }
    }
  })
],
 resolve: {
   alias: {
     '@': fileURLToPath(new URL('./src', import.meta.url))
  }
},
 assetsInclude: ['./src/assets/heros/*.glb']
})


新建 src/components/LolProgress.vue


<template>
 <div class="progress-container">
   <model-viewer
     :src="hero.src"
     disable-zoom
     shadow-intensity="1"
     :camera-orbit="hero.cameraOrbit"
     class="model-viewer"
     :style="heroPosition"
     :animation-name="animationName"
     :camera-target="hero.cameraTarget"
     autoplay
     ref="modelViewer"
   >
</model-viewer>
   <div
     class="progress-bar"
     :style="{ height: strokeWidth + 'px', borderRadius: strokeWidth / 2 + 'px' }"
   >

     <div class="progress-percent" :style="currentPercentStyle"></div>
   </div>
 </div>

</template>

<script setup lang="ts">
import { computed, onMounted, ref, watch, type PropType } from 'vue'
/** 类型 */
interface Hero {
 src: string
 cameraOrbit: string
 progressAnimation: string
 finishAnimation: string
 finishAnimationIn: string
 cameraTarget: string
 finishDelay: number
}
type HeroName = 'yasuo' | 'yi'

type Heros = {
[key in HeroName]: Hero
}
const props = defineProps({
 hero: {
   type: String as PropType<HeroName>,
   default: 'yasuo'
},
 percentage: {
   type: Number,
   default: 100
},
 strokeWidth: {
   type: Number,
   default: 10
},
 heroSize: {
   type: Number,
   default: 150
}
})

const modelViewer = ref(null)

const heros: Heros = {
 yasuo: {
   src: '/src/components/yasuo.glb',
   cameraOrbit: '-90deg 90deg',
   progressAnimation: 'Run2',
   finishAnimationIn: 'yasuo_skin02_dance_in',
   finishAnimation: 'yasuo_skin02_dance_loop',
   cameraTarget: 'auto auto 0m',
   finishDelay: 2000
},
 yi: {
   src: '/src/components/yi.glb',
   cameraOrbit: '-90deg 90deg',
   progressAnimation: 'Run',
   finishAnimationIn: 'Dance',
   finishAnimation: 'Dance',
   cameraTarget: 'auto auto 0m',
   finishDelay: 500
}
}

const heroPosition = computed(() => {
 const percentage = props.percentage > 100 ? 100 : props.percentage
 return {
   left: `calc(${percentage + '%'} - ${props.heroSize / 2}px)`,
   bottom: -props.heroSize / 10 + 'px',
   height: props.heroSize + 'px',
   width: props.heroSize + 'px'
}
})

const currentPercentStyle = computed(() => {
 const percentage = props.percentage > 100 ? 100 : props.percentage
 return { borderRadius: `calc(${props.strokeWidth / 2}px - 1px)`, width: percentage + '%' }
})

const hero = computed(() => {
 return heros[props.hero]
})

const animationName = ref('')

watch(
() => props.percentage,
(percentage) => {
   if (percentage < 100) {
     animationName.value = hero.value.progressAnimation
  } else if (percentage === 100) {
     animationName.value = hero.value.finishAnimationIn
     setTimeout(() => {
       animationName.value = hero.value.finishAnimation
    }, hero.value.finishDelay)
  }
}
)
onMounted(() => {
 setTimeout(() => {
   console.log(modelViewer.value.availableAnimations)
}, 2000)
})
</script>
<style scoped>
.progress-container {
 position: relative;
 width: 100%;
}
.model-viewer {
 position: relative;
 background: transparent;
}
.progress-bar {
 border: 1px solid #fff;
 background-color: #666;
 width: 100%;
}
.progress-percent {
 background-color: aqua;
 height: 100%;
 transition: width 100ms ease;
}
</style>


组件非常简单,核心逻辑如下:



  • 根据传入的英雄名称加载模型

  • 指定每个英雄的加载中的动画,

  • 加载100%,切换完成动作进入动画和完成动画即可。

  • 额外的细节处理。


    最后修改 app.vue:


    <script setup lang="ts">
    import { ref } from 'vue'
    import LolProgress from './components/LolProgress.vue'
    const percentage = ref(0)
    setInterval(() => {
     percentage.value = percentage.value + 1
    }, 100)
    </script>

    <template>
     <main>
       <LolProgress
         :style="{ width: '200px' }"
         :percentage="percentage"
         :heroSize="200"
         hero="yasuo"
       />

     </main>
    </template>

    <style scoped></style>




这不就完成了吗,先拿给老板看看。


老板:换个女枪的看看。


我:好嘞。


iShot_2024-06-06_19.08.49.gif


老板:弄类不赖啊小伙,换个俄洛伊的看看。


4、总结


通过本次需求,了解到了 model-viewer组件。


老板招个UI妹子吧。


在线体验:github-pages


作者:盖伦大王
来源:juejin.cn/post/7377217883305279526
收起阅读 »

我在团队内部提倡禁用单元测试

先说结论: 在绝大部分的业务前端开发中,写单元测试的收益非常小,但是带来的维护成本却非常大,请不要再写单元测试感动自己,为难同事了。 现在很多单元测试的教程都是那种很简单的比如,测个 1+1=2,这需要测吗?下面这段代码已经出现过很多次了,纯纯的误导人。 co...
继续阅读 »

先说结论:
在绝大部分的业务前端开发中,写单元测试的收益非常小,但是带来的维护成本却非常大,请不要再写单元测试感动自己,为难同事了。


现在很多单元测试的教程都是那种很简单的比如,测个 1+1=2,这需要测吗?下面这段代码已经出现过很多次了,纯纯的误导人。


const sum = (a,b) => a+b
it('1+1=2',()=> {
expect(sum(1,1)).toBe(2)
})

稍微上点复杂度,来写个组件的单测,比如一个用户信息展示组件,叫 UserInfoBlock,支持设置头像的大小,点击名字支持跳转到个人主页,组件代码大致长这样


interface UserInfoBlockProps {
name: string
size: 16 | 24
id: string
icon: string
}
export const UserInfoBlock:FC<UserInfoBlockProps> = (props) => {
const navigate = useNavigate()
return <div
class='xxxxxx'
style={{width: props.size}}
onClick={() =>
{navigate(`/user/${props.id}`)}}>
<img src={props.icon}/>
{props.name}
</div>

}

然后开始给组件写单测,先测头像大小的功能


import { UserInfoBlock, UserInfoBlockProps } from './user-info-block'
import { fireEvent, render, screen } from '@testing-library/react'
describe('UserInfoBlock 组件单测', () => {
const userInfo:UserInfoBlockProps = {
name: '张三',
icon:'https://xxx.png',
id:'abcd1234',
size: 16
}
const userInfoLarge:UserInfoBlockProps = {
name: '张三',
icon:'https://xxx.png',
id:'abcd1234',
size: 24
}
describe('展示默认头像大小', () => {
const component = render(<UserInfoBlock {...userInfo}/>)
it('img 标签的宽度为 16px', () => {
expect(screen.getByTag('img').style.width).toBe(16)
})
})
describe('展示large头像大小', () => {
const component = render(<UserInfoBlock {...userInfoLarge}/>)
it('img 标签的宽度为 24px', () => {
expect(screen.getByTag('img').style.width).toBe(24)
})
})
})


接下来测一下跳转,因为用了 react-router,所以在渲染组件的时候必须包裹一下 RouterProvider


...
describe('点击可以跳转', () => {
const component = render(<MemoryRouter>
<UserInfoBlock {...userInfoLarge}/>
</MemoryRouter>

)
fireEvent.click(component.div)
it('url 会变成 user/abcd1234', () => {
expect(location.pathname).toBe('user/abcd1234')
})
})
...

这个组件的测试就写完了,看起来挺有用,但实际没啥用。


首先这个测试的收益不高,因为这是一个很简单的组件,五分钟写完,但是写这个测试需要翻倍的时间,因为需要构造数据,之前没有经验不知道要加 MemoryRouterjestlocation 对象不方便,还要 mock 一下。等把这一套搞完了才能跑通,这就好像你疯狂锻炼,练出麒麟臂,就是为了举自拍杆举的稳一点。如果组件内要发请求,要做的准备工作就更加多了。


其次,user/abcd1234是什么,断言这个没用,因为别人改了链接,你的测试也一样会过,应该断言成功的打开了用户主页,比如断言一个必定会存在的文字expect(screen.getByText('用户详情')).toBeInDocument()这才能证明真的打开了用户主页。


1+1 什么时候不等于 2。头像设置 16px,什么时候不会是 16 px。什么时候点击不跳转到用户主页。肯定是有人修改了这里的业务逻辑才会这样,只有在做产品需求,代码优化的时候才会去改以前的代码。那么这时,这段测试对接手这段代码的人就是负担。


假设我接到需求,用户主页url 变了,改成 user?id=xxx。我心想这个简单,一秒钟解决,提了 pr 发现测试挂了,然后我就得把测试也改一下。


如果一段测试代码在我每次修改需求的时候都要改,甚至有的时候还会为了便于测试在业务代码中留一些后门,那这个测试就是纯纯的副作用。


大家肯定深有体会,一般一个模块自己不会出问题,但是多个模块组合起来就很容易出问题了,所以测试的重点不应该是一个组件内部各种细节,而应该测这个组件在被使用的地方是否正确。


举个例子,比如在一个列表里面,使用头像为 24px 的用户组件,然后有一天出 bug 了,说这里变成 16 了。那这会是组件的 bug 吗,这肯定是使用的地方没有用对。所以应该要测试的是这里。如果有个测试断言了在列表中用户头像必须为 24px,即使没有组件的单测,这个组件也不会被改坏,因为上层已经覆盖到了。


什么是好的测试?



  1. 我认为好的测试是稳定的测试,不仅不需要经常修改,甚至还能扛住代码重构。比如你把项目从 react 完全迁移到了 vue,测试都能跑过。

  2. 好的测试是可以当作项目文档的,组内来新人了,不需要给他介绍过多,有问题让他自己看测试用例就能理解业务。


那么怎么写出这样的测试呢?答案就是写集成测试,集成测试说白了就是一个超级大的单测,之前测试 render 的是一个组件,现在直接 render(<App/>),将路由跳转,请求 mock,fake数据库等等都统一处理。可以将上面写的测试简单的改为


import { creatAppContext, AppContext } from './app-test-context'
describe('S: 测试头像组件', () => {
let _:AppContext
beforeEach(() => {
- = creatAppContext()
})
describe('W: 去用户列表也', () => {
beforeEach(() => {
_.click(screen.getByText('查看用户列表'))
})
it('T: 列表页有十个用户,用户头像是 24px', ()=>{
expect(screen.getAllByTestid('user-item').length).toBe(10)
expect(screen.getAllByTestid('user-item')[0].img.style.width).toBe(24)
})
describe('W: 点击第一个用户', () => {
beforeEach(() => {
_.click(screen.getAllByTestid('user-item')[0])
})
it('T: 打开了用户主页', () => {
expect(screen.getByText('用户详情')).toBeInDocument()
})
})
})
})

关于怎么写好测试可以展开的点实在太多了,就不过多阐述,想要了解的朋友可以查看我的主页,之前专门出过一些列文章详细的讲这些事情。


作者:阿古达木
来源:juejin.cn/post/7478515057510154255
收起阅读 »

组长说:公司的国际化就交给你了,下个星期给我

web
从“跑路程序员”到“摸鱼仙人”,我用这插件把国际化的屎山代码盘活了! tips:使用有道翻译,朋友们,要去有道官网注册一下,有免费额度,github demo的key已经被用完了。 一、命运的齿轮开始转动 “小王啊,海外业务要上线了,国际化你搞一下,下个月...
继续阅读 »

从“跑路程序员”到“摸鱼仙人”,我用这插件把国际化的屎山代码盘活了!



tips:使用有道翻译,朋友们,要去有道官网注册一下,有免费额度,github demo的key已经被用完了。



一、命运的齿轮开始转动


“小王啊,海外业务要上线了,国际化你搞一下,下个月验收。”组长轻描淡写的一句话,让我盯着祖传代码陷入沉思——


翻译代码注释.png
(脑补画面:满屏中文硬编码,夹杂着"确定""取消""加载中..."


正当我准备打开BOSS直聘时,GitHub Trending上一个项目突然闪现——

auto-i18n-translation-plugins

项目简介赫然写着:“不改代码,三天交付国际化需求,摸鱼率提升300%”




二、极限操作:48小时从0到8国语言


🔧 第1步:安装插件(耗时5分钟)


祖训“工欲善其事,必先装依赖”


# 如果你是Vite玩家(比如Vue3项目)
npm install vite-auto-i18n-plugin --save-dev

# 如果你是Webpack钉子户(比如React老项目)
npm install webpack-auto-i18n-plugin --save-dev

🔧 第2步:配置插件(关键の10分钟)


Vue3 + Vite の 摸鱼配置


// vite.config.js
import { defineConfig } from 'vite';
import vitePluginAutoI18n from 'vite-auto-i18n-plugin';

export default defineConfig({
plugins: [
vue(),
vitePluginAutoI18n({
targetLangList: ['en', 'ja', 'ko'], // 要卷就卷8国语言!
translator: new YoudaoTranslator({ // 用有道!不用翻墙!
appId: '你的白嫖ID', // 去官网申请,10秒搞定
appKey: '你的密钥' // 别用示例里的,会炸!
})
})
]
});

🔧 第3步:注入灵魂——配置文件(生死攸关の5分钟)


在项目入口文件(如main.js)的第一行插入


// 这是插件的生命线!必须放在最前面!
import '../lang/index.js'; // 运行插件之后会自动生成引入即可



三、见证奇迹的时刻


🚀 第一次运行(心脏骤停の瞬间)


输入npm run dev,控制台开始疯狂输出:


[插件日志] 检测到中文文本:"登录" → 生成哈希键:a1b2c3  
[插件日志] 调用有道翻译:"登录" → 英文:Login,日文:ログイン...
[插件日志] 生成文件:lang/index.json(翻译の圣杯)

突然!页面白屏了!

别慌!这是插件在首次翻译时需要生成文件,解决方法:



  1. 立即执行一次 npm run build (让插件提前生成所有翻译)

  2. 再次npm run dev → 页面加载如德芙般丝滑




四、效果爆炸:我成了全组の神


1. 不可置信の48小时


当我打开浏览器那一刻——\


Untitled.gif


(瞳孔地震):“卧槽…真成了?!”

组长(凑近屏幕):“这…这是你一个人做的?!”(眼神逐渐迷茫)

产品经理(掏出手机拍照):“快!发朋友圈!《我司技术力碾压硅谷!》”


2. 插件の超能力



  • 构建阶段:自动扫描所有中文 → 生成哈希键 → 调用API翻译

  • 运行时:根据用户语言动态加载对应翻译

  • 维护期:改个JSON文件就能更新所有语言版本


副作用



  • 测试妹子开始怀疑人生:“为什么一个bug都找不到?”

  • 后端同事偷偷打听:“你这插件…能翻译Java注释吗?”




五、职场生存指南:如何优雅甩锅


🔨 场景1:测试妹子提着40米大刀来了!


问题:俄语翻译把“注册”译成“Регистрация”(原意是“登记处”)

传统应对



  • 熬夜改代码 → 重新打包 → 提交测试 → 被骂效率低

    插件玩家



  1. 打开lang/index.json

  2. Регистрация改成Зарегистрироваться(深藏功与名)

  3. 轻描淡写:“这是有道翻译的锅,我手动修正了。”


🔨 场景2:产品经理临时加语言


需求:“老板说下周要加印地语!”

传统灾难



  • 重新配框架 → 人肉翻译 → 测试 → 加班到秃头

    插件玩家



  1. 配置加一行代码:targetLangList: ['hi']

  2. 运行npm run build → 自动生成印地语翻译

  3. 告诉产品经理:“这是上次预留的技术方案。”(其实只改了1行)


🔨 场景3:组长怀疑你摸鱼


质问:“小王啊,你这效率…是不是有什么黑科技?”

标准话术

“组长,这都是因为:



  1. 您制定的开发规范清晰

  2. 公司技术栈先进(Vue3真香)

  3. 我参考了国际前沿方案(打开GitHub页面)”




六、高级摸鱼の奥义


🎯 秘籍1:把翻译文件变成团队武器



  1. lang/index.json扔给产品经理:“这是国际化核心资产!”

  2. 对方用Excel修改后,你直接git pull → 无需动代码

  3. 出问题直接甩锅:“翻译是市场部给的,我只负责技术!”




(脑补画面:产品经理在Excel里疯狂改翻译,程序员在刷剧)


🎯 秘籍2:动态加载の神操作


痛点:所有语言打包进主文件 → 体积爆炸!

解决方案


// 在index.js里搞点骚操作
const loadLanguage = async (lang) => {
const data = await import(`../../lang/${lang}.json`); // 动态加载翻译文件
window.$t.locale(data, 'lang');
};

// 切换语言时调用
loadLanguage('ja'); // 瞬间切换日语,深藏功与名

🎯 秘籍3:伪装成AI大神



  1. 周会汇报:“我基于AST实现了自动化国际翻译中台”

  2. 实际:只是配了个插件

  3. 老板评价:“小王这技术深度,值得加薪!”(真相只有你知道)




七、终局:摸鱼の神,降临!


当组长在庆功会上宣布“国际化项目提前两周完成”时,我正用手机刷着《庆余年2》。


测试妹子:“你怎么一点都不激动?”

(收起手机):“常规操作,要习惯。”(心想:插件干活,我躺平,这才叫真正的敏捷开发!)




立即行动(打工人自救指南)



  1. GitHub搜auto-i18n-translation-plugins(点星解锁摸鱼人生)

  2. 复制我的配置 → 运行 → 见证魔法

  3. 加开发者社群:遇到问题发红包喊“大哥救命!”


终极警告

⚠️ 过度使用此插件可能导致——



  • 你的摸鱼时间超过工作时间,引发HR关注

  • 产品经理产生“国际化需求可以随便加”的幻觉

  • 老板误以为你是隐藏的技术大佬(谨慎处理!)




文末暴击

“自从用了这插件,我司翻译团队的工作量从3周变成了3分钟——现在他们主要工作是帮我选中午吃啥。” —— 匿名用户の真实反馈




常见问题汇总


常见问题汇总


作者:wenps
来源:juejin.cn/post/7480267450286800911
收起阅读 »

8年程序员本以为到头了,没想到柳暗花明...

讲一个这两年发生在我身上的故事,一个从程序员到AI产品负责人,一个选择大于努力的故事: 回到2022年,作为一个8年的前端程序员的我,在一家行业头部的企业中勤勤恳恳,兢兢业业。每天与代码为伴,沉浸在React的世界里,整日优化、升级、写框架、写工具,解决点疑难...
继续阅读 »

讲一个这两年发生在我身上的故事,一个从程序员到AI产品负责人,一个选择大于努力的故事:


回到2022年,作为一个8年的前端程序员的我,在一家行业头部的企业中勤勤恳恳,兢兢业业。每天与代码为伴,沉浸在React的世界里,整日优化、升级、写框架、写工具,解决点疑难杂症,改改BUG,后来甚至成为了React的contributor。


我的生活和工作都很稳定,技术能力也得到了领导和团队的认可。


但内心深处却隐隐感到一种不安——前端技术的发展似乎进入了瓶颈期,我的职业生涯也仿佛停滞不前。


随着时间的推移,我开始面临两个无法回避的问题:



  1. 个人能力无法进一步提升:前端技术的更新速度放缓,框架和工具趋于成熟,我能做的事情越来越少了。

  2. 薪资增长停滞:薪资似乎已经到达了行业的天花板,如果我不能做更多的事情,也再难有突破。


自己是否已经走到了职业生涯的尽头?


面对这个问题,我一度陷入迷茫。跟其他程序员一样,我也尝试学习新的技术栈来缓解心中的这份焦虑,更底层的语言比如Rust、更宽的技术栈比如three.js、学学区块链、学学算法。


shu3.jpg
但是随之而来的就是,这些东西我学了之后除了做做demo、写写小工具之外,我好像做不了什么大事,我没有动力向着更深的内容走下去,这反而更加加深了我的焦虑。


那时候我32岁,每天都在担心中年危机。担心三年之后我凭什么和体力更好的、钱更少的、拥有同样技术栈的年轻人竞争?【虽然三年后的今天,并没有这样的年轻人出现】


幸运的是,2022年年底,chatGPT爆火全球。一夜之间,所有人都在讨论GPT,我也不例外,按照惯例,我再次把学习AI加入了我的日程。


当时体验了chatGPT之后,我两眼放光,我觉得,只要我努力学习这个技术,我一定可以像当初选择前端开发一样,再次进入一个程序员的风口,于是报课、买书、找行业大佬交流。


就这样过来两三个月......


你以为怎么了?我柳暗花明了? 这是一个前端程序员逆袭转战AI年薪百万的故事? 很不幸,这并不是。


事实是三个月之后,我再次陷入了困境:AI的底层技术,一我根本学不会,二我学了好像也没有用武之地。总不能跟老板说:给我几千万美金我给你撸个大模型?


dw.jpg


世界再次灰暗下来......


可是,天无绝人之路,就在我已经准备放弃的时候,我在某个产品群里看到了这样一句话:


ltjl.jpg


“???,这居然没人知道?”


过去三个月的高强度学习,在我看来在业务中找到嵌入AI的场景,甚至为了AI单独新增功能都是非常容易的事情。


AI时代已经来了,GUI到LUI是必然发生的事情,所有企业的产品都将会经历加入AI的功能或者逻辑。如果必须有一人主导这件事情的话,那为什么不能是我?


“汽车干掉马车之后,所有的司机都是之前的马车司机么?”


“计算器干掉算盘之后,所有的会计都是之前的师爷么?”


“安卓干掉塞班之后,所有的安卓工程师都是之前的塞班工程师么?”


既然都不是,那么AI带来全新的交互体验,必须是传统产品去做么?显然不是,这玩意,谁先干算谁的啊。


gds.jpg


我决定接受这个挑战,开始把视野放到应用层。开始研究产品的逻辑,尝试找到AI技术与业务需求的结合点。


不会怎么办? 学呗,这流程咱熟,报班、买书、找大佬交流。


shu1.jpg


shu2.jpg


一边学,我就一边结识了一些志同道合的伙伴,我们一起讨论、尝试、失败、再尝试。


经历过那一年的朋友应该知道,国内大模型的能力远远没有广告上说的那些效果,底层能力的限制一度成为我们思路中最大的卡点。


不过,我们也总结整理了一大批不同的产品中,不训练自己的大模型的情况下,能够结合AI的逻辑,这里简单放几个例子。



  • TOB的管理系统,新用户没办法很好的知道产品中有什么功能,可以直接通过AI助手来唤起、使用对应功能。【我很喜欢这个,典型的GUI升级LUI的场景】

  • 产品售卖,通过AI + 埋点的技术,实时分析用户的行为,在恰当的时机向用户推送VIP折扣。

  • 电商详情页浏览助手。【页面上的AI助手,也是这两年很多产品已经落地了的选择】

  • TOC的评论页面,AI拥有很好的控评能力

  • 智能问答产品,【我们认为是受到冲击最大的产品,跟AI比起来,现在市场上所有的智能问答产品,都不够智能。都应该被重做升级!】

  • 多模态能力会带来的全新产品。【情感陪伴是目前最为广泛且收益较快的赛道。】


不扯远了,说回我自己。


我的转机出现在我们自己的领导也开始说要结合AI,升级产品能力。


ddn.jpg


我以最快的速度,做好agent流程的思维导图、MVP(最小可行性产品)。 开会,给领导布道,说服领导们相信我的这套设计。


因为之前就进行过AI相关的内容分享,所以这一步比较容易。一顿操作猛如虎之后,终于获得了领导认可,接到了协助产品负责AI升级的任务,升级过程其实遇到了非常多的困难。


再次吐槽一下那一年的国产大模型,流程稍微多一点,响应时间就巨慢,流程少了,大模型的理解能力又不足。


有时候一个提示词一调就是半天,我甚至一度都要放弃了。不过好在最终我们选择了曲线救国,采用大模型+小模型,配合多专家 + RAG + 变种function call组合的架构,我们在23年底也做出了不错的东西。


上线之后,客户表示了满意。领导表示了满意。团队表示了满意。


水到渠成,后来一切就按部就班了,24年我又虚线负责了两个AI产品的升级和功能迭代。从确定需求是否适用AI实现,到方案选型,到设计agent流程,到功能上的交互设计,组织团队最终交付。


今年开始实线负责多个产品,有了自己的团队负责大模型方向的新产品设计和落地,也有需要横向拉通的部门,负责协助规划旧产品AI方向的升级和产品中AI需求的挖掘。


柳暗花明,未来可期。


结语


虽然我是前端转型到AI产品负责人的,但是如果你问我AI一定会代替掉程序员么?


我的回答是:不会,但是AI一定会让这个岗位所需要的人数减少,无论你是初级岗还是高级岗。


我们要做的就是抓住新的机会、占据有利位置、执行下去,然后等待时代的洪流将我们高高托起。


你看deepseek这不就来了? 我前面两年所做的事情,因为deepseek的出现,再次被抬高了一个维度,这就是时代,选择大于努力。



如果大家对“程序员如何转型AI产品负责人”感兴趣,可以点个赞。如果大家喜欢,后面我再多多分享转型细节。



文章更新列表


# 序员转型,带领团队落地AI产品的全流程


# 30行代码,随时进行云端DeepSeek能力评估!


# 5000字长文,AI时代下程序员的巨大优势!


作者:华洛
来源:juejin.cn/post/7469263585287651340
收起阅读 »

实现基于uni-app的项目自动检查APP更新

web
我们平时工作中开发APP时,及时为用户提供应用更新是提升用户体验和保证功能完整性的重要一环。本文将通过一段实际的代码示例,详细介绍如何在基于uni-app框架的项目中实现自动检查应用更新的功能,并引导用户完成升级过程。该功能主要涉及与服务器端交互获取最新版本信...
继续阅读 »

我们平时工作中开发APP时,及时为用户提供应用更新是提升用户体验和保证功能完整性的重要一环。本文将通过一段实际的代码示例,详细介绍如何在基于uni-app框架的项目中实现自动检查应用更新的功能,并引导用户完成升级过程。该功能主要涉及与服务器端交互获取最新版本信息、比较版本号、提示用户升级以及处理下载安装流程。



创建一个checkappupdate.js文件


这个文件是写升级逻辑处理的文件,可以不创建,直接在App.vue中写,但是为了便于维护,还是单独放出来比较好,可以放在common或者util目录中(App.vue能引入到就行,随意放,根目录也行),App.vue中引入该文件,调用升级函数如下图所示:


image.png


js完整代码


为了防止一点点代码写,容易让人云里雾里,先放完整代码,稍后再详细解释,其实看注释也就够了。


//这是服务端请求url配置文件,如果你直接卸载下面的请求中,可以不引入
import configService from '@/common/service/config.service.js'

export default function checkappupdate(param = {}) {
// 合并默认参数
param = Object.assign({
title: "A new version has been detected!",
content: "Please upgrade the app to the latest version!",
canceltext: "No upgrade",
oktext: "Upgrade now"
}, param)

plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
let platform = plus.os.name.toLocaleLowerCase() //Android
let os_version = plus.os.version //13 安卓版本
let vendor = plus.device.vendor //Xiaomi
let url = configService.apiUrl
uni.request({
url: url + '/checkAppUpdate',
method: 'GET',
data: {
platform: platform,
os_version: os_version,
vendor: vendor,
cur_version: widgetInfo.version
},
success(result) {
console.log(result)
let versionCode = parseInt(widgetInfo.versionCode)
let data = result.data ? result.data : null;
// console.log(data);
let downAppUrl = data.url
//判断版本是否需要升级
if (versionCode >= data.versionCode) {
return;
}
//升级提示
uni.showModal({
title: param.title,
content: data.log ? data.log : param.content,
showCancel: data.force ? false : true,
confirmText: param.oktext,
cancelText: param.canceltext,
success: res => {
if (!res.confirm) {
console.log('Cancel the upgrade');
// plus.runtime.quit();
return
}
// if (data.shichang === 1) {
// //去应用市场更新
// plus.runtime.openURL(data.shichangurl);
// plus.runtime.restart();
// } else {
// 开始下载
// 创建下载任务
var dtask = plus.downloader.createDownload(downAppUrl, {
filename: "_downloads/"
},
function (d, status) {
// 下载完成
if (status == 200) {
plus.runtime.install(d.filename, {
force: true
}, function () {
//进行重新启动;
plus.runtime.restart();
}, (e) => {
uni.showToast({
title: 'install fail:' + JSON
.stringify(e),
icon: 'none'
})
console.log(JSON.stringify(e))
});
} else {
this.tui.toast("download fail,error code: " +
status);
}
});
let view = new plus.nativeObj.View("maskView", {
backgroundColor: "rgba(0,0,0,.6)",
left: ((plus.screen.resolutionWidth / 2) - 45) +
"px",
bottom: "80px",
width: "90px",
height: "30px"
})

view.drawText('start download...', {}, {
size: '12px',
color: '#FFFFFF'
});
view.show()
// console.log(dtask);
dtask.addEventListener("statechanged", (e) => {
if (e && e.downloadedSize > 0) {
let jindu = ((e.downloadedSize / e.totalSize) *
100).toFixed(2)
view.reset();
view.drawText('Progress:' + jindu + '%', {}, {
size: '12px',
color: '#FFFFFF'
});
}
}, false);
dtask.start();
// }
},
fail(e) {
console.log(e);
uni.showToast({
title: 'Request error'
})
}
})
}
})

});
}


函数定义:checkappupdate


定义核心函数checkappupdate,它接受一个可选参数param,用于自定义提示框的文案等信息。函数内部首先通过Object.assign合并默认参数与传入参数,以确保即使未传入特定参数时也能有良好的用户体验。


获取应用信息与环境变量


利用plus.runtime.getProperty获取当前应用的详细信息,包括但不限于应用ID、版本号(version)和版本号代码(versionCode),以及设备的操作系统名称、版本和厂商信息。这些数据对于后续向服务器请求更新信息至关重要。


请求服务器检查更新


构建包含平台信息、操作系统版本、设备厂商和当前应用版本号的请求参数,发送GET请求至配置好的API地址/checkAppUpdate,查询是否有新版本可用。后端返回参数参考下面:


   /**
* 检测APP升级
*/

public function checkAppUpdate()
{
$data['versionCode'] = 101;//更新的版本号
$data['url'] = 'http://xxx/app/xxx.apk';//下载地址
$data['force'] = true;//是否强制更新
return json_encode($data);//返回json格式数据到前端
}

比较版本与用户提示


一旦收到服务器响应,解析数据并比较当前应用的版本号与服务器提供的最新版本号。如果存在新版本,使用uni.showModal弹窗提示用户,展示新版本日志(如果有)及升级选项。此步骤充分考虑了是否强制更新的需求,允许开发者灵活配置确认与取消按钮的文案。


下载与安装新版本


用户同意升级后,代码将执行下载逻辑。通过plus.downloader.createDownload创建下载任务,并监听下载进度,实时更新进度提示。下载完成后,利用plus.runtime.install安装新APK文件,并在安装成功后调用plus.runtime.restart重启应用,确保新版本生效。


用户界面反馈


在下载过程中,通过创建原生覆盖层plus.nativeObj.View展示一个半透明遮罩和下载进度信息,给予用户直观的视觉反馈,增强了交互体验,进度展示稍微有点丑,可以提自己改改哈。


image.png


总结



通过上述步骤,我们实现了一个完整的应用自动检查更新流程,不仅能够有效通知用户新版本的存在,还提供了平滑的升级体验。此功能的实现,不仅提升了用户体验,也为产品迭代和功能优化提供了有力支持。开发者可以根据具体需求调整提示文案、下载逻辑、进度样式等细节,以更好地适配自身应用的特点和用户群体。



作者:掘金归海一刀
来源:juejin.cn/post/7367555191337828361
收起阅读 »

10年深漂,放弃高薪,回长沙一年有感

大明哥是 2014 年一个人拖着一个行李箱,单身杀入深圳,然后在深圳一干就是 10 年。 10 年深漂,经历过 4 家公司,有 20+ 人的小公司,也有上万人的大厂。 体验过所有苦逼深漂都体验过的难。坐过能把人挤怀孕的 4 号线,住过一天见不到几个小时太阳的城...
继续阅读 »

大明哥是 2014 年一个人拖着一个行李箱,单身杀入深圳,然后在深圳一干就是 10 年。


10 年深漂,经历过 4 家公司,有 20+ 人的小公司,也有上万人的大厂。


体验过所有苦逼深漂都体验过的难。坐过能把人挤怀孕的 4 号线,住过一天见不到几个小时太阳的城中村,见过可以飞的蟑螂。欣赏过晚上 6 点的晚霞,但更多的是坐晚上 10 点的地铁看一群低头玩手机的同行。


10 年虽然苦、虽然累,但收获还是蛮颇丰的。从 14年的 5.5K 到离职时候的 xxK。但是因为种种原因,于 2023年 9 月份主动离职离开深圳。


回长沙一年,给我的感觉就是:除了钱少和天气外,样样都比深圳好


生活


在回来之前,我首先跟我老婆明确说明了我要休息半年,这半年不允许跟我提任何有关工作的事情,因为在深圳工作了 10 年真的太累,从来没有连续休息超过半个月的假期。哪怕是离职后我也是无缝对接,这家公司周五走,下家公司周一入职。


回来后做的第一件事情就是登出微信、删除所有闹钟、手机设置全天候的免打扰,全心全意,一心一意地陪女儿和玩,在这期间我不想任何事情,也不参与任何社交,就认真玩,不过顺便考了个驾-照。


首先说消费。


有很多人说长沙是钱比深圳少,但消费不比深圳低。其实不然,我来长沙一年了,消费真的比深圳低不少。工作日我一天的消费基本上可以控制在 40 左右,但是在深圳我一天几乎都要 80 左右。对比


长沙深圳
5+5+
15 ~ 2525 ~ 35
10 ~ 15,不加班就回家吃25 ~ 35,几乎天天加班

同时,最近几个月我开始带饭了,周一到超时买个百来块的菜,我一个人可以吃两个星期。


总体上,一个月消费长沙比深圳低 1000 左右(带饭后更低了)。


再就是日常的消费。如果你选择去长沙的商城里面吃,那与深圳其实差不多了多少,当然,奶茶方面会便宜一些。但是,如果你选择去吃长沙的本土菜,那就会便宜不少,我跟我朋友吃饭,人均 50 左右,不会超过 70,选择美团套餐会更加便宜,很多餐馆在支持美团的情况下,选择美团套餐,两个人可以控制在 30 ~ 40 之间。而深圳动不动就人均 100+。


当然,在消费这块,其实节约的钱与少的工资,那就是云泥之别,可忽略不计。


再说生活这方面。


在长沙这边我感觉整体上的幸福感比深圳要强蛮多,用一句话说就是:深圳都在忙着赚钱,而长沙都在忙着吃喝玩乐加洗脚。我说说跟我同龄的一些高中和大学同学,他们一毕业就来长沙或者来长沙比较早,所以买房就比较早,尤其是 16 年以前买的,他们的房贷普遍在 3000 左右,而他们夫妻两的工资税后可以到 20000,所以他们这群人周末经常约着一起耍。举两个例子来看看他们的松弛感:



  • 晚上 10 点多喊我去吃烧烤,我以为就是去某个夜市撸串,谁知道是开车 40+公里,到某座山的山顶撸串 + 喝酒。这是周三,他们上班不上班我不知道,反正我是不上班。

  • 凌晨 3 点多拉我出来撸串


跟他们这群人我算是发现了,大部分的聚会都是临时起意,很少提前约好,主打就是一个随心随意。包括我和同事一样,我们几乎每个月都会出来几次喝酒(我不喜欢喝酒,偶尔喝点啤酒)、撸串,而且每次都是快下班了,某个人提议今晚喝点?完后,各回各家。


上面是好的方面,再说不好的。


长沙最让我受不了的两点就是天气 + 交通。


天气我就不说了,冬天冻死你,夏天热死你。去年完整体验了长沙的整个冬天,是真他妈的冷,虽然我也是湖南人,但确实是把我冻怕了。御寒?不可能的,全靠硬抗。当然,也有神器:火桶子,那是真舒服,我可以在里面躺一整天。


交通,一塌糊涂,尤其是我每天必经的西二环,简直惨不忍睹,尤其是汽车西站那里,一天 24 小时都在堵,尤其是周一和周五,高德地图上面是红的发黑。所以,除非特殊情况,我周一、周五是不开车的,情愿骑 5 公里小电驴去坐地铁。


然后是一大堆违停,硬生生把三车道变成两车道,什么变道不打灯,实线变道,双黄线调头见怪不怪了,还有一大群的小电驴来回穿梭,对我这个新手简直就是恐怖如斯(所以,我开车两个月喜提一血,4S点维修报价 9800+)。


美食我就不说了,简直就是吃货的天堂。


至于玩,我个人觉得长沙市内没有什么好玩的,我反而喜欢去长沙的乡里或者周边玩。所以,我实在是想不通,为什么五一、国庆黄金周长沙是这么火爆,到底火爆在哪里???


还有一点就是,在深圳我时不时就犯个鼻炎,回了长沙一年了我一次都没有,不知道什么原因。


工作


工资,长沙这边的钱是真的少,我现在的年收入连我深圳的三分之一都没有,都快到四分之一了。


当然,我既然选择回来了,就会接受这个低薪,而且在回来之前我就已经做好了心理建设,再加上我没有房贷和车贷,整体上来说,每个月略有结余。


所以,相比以前在深圳赚那么多钱但是无法和自己家人在一起,我更加愿意选择少赚点钱,当然,每个人的选择不同。我也见过很多,受不了长沙的低工资,然后继续回深圳搬砖的。


公司,长沙这边的互联网公司非常少,说是互联网荒漠都不为过。大部分都是传统性的公司,靠国企、外包而活着,就算有些公司说自己是互联网公司,但也不过是披着互联网的羊皮而已。而且在这里绝大多数公司都是野路子的干法,基建差,工作环境也不咋地,福利待遇与深圳的大厂更是没法比,比如社保公积金全按最低档交。年假,换一家公司就清零,我进入公司的时候,我一度以为我有 15 天,一问人事,试用期没有,转正后第一年按 5 天折算,看得我一脸懵逼。


加班,整体上来说,我感觉比深圳加班得要少,当然,大小周,单休的公司也不少,甚至有些公司连五险一金都不配齐,劳动法法外之地名副其实。


同时,这边非常看重学历,一些好的公司,非 985 、211 不要,直接把你门焊死,而这些公司整体上来说工资都很不错,40+ 起码是有的(比如某银行,我的履历完美契合,但就是学历问题而被拒),在长沙能拿这工资,简直就是一种享受,一年就是一套房的首付。


最后,你问我长沙工资这么低,你为什么还选择长沙呢?在工作和生活之间,我只不过选择了生活,仅此而已!!


作者:大明哥09
来源:juejin.cn/post/7457175937736163378
收起阅读 »

我写了个App,上架 Google Play 一年,下载不到 10 次,于是决定把它开源了

缘起 起初接触某某标签笔记,我被其以标签为核心的卡片笔记模式深深吸引。然而,99 元一年的会员费感觉有点贵了,再加上数据存储在它的服务器,总感觉缺乏一份安全感。 身为 Android 开发者,一个念头在我脑海中闪现:何不亲手写一款属于自己的类似应用? 开发历程...
继续阅读 »

pEK8AXT.jpg


缘起


起初接触某某标签笔记,我被其以标签为核心的卡片笔记模式深深吸引。然而,99 元一年的会员费感觉有点贵了,再加上数据存储在它的服务器,总感觉缺乏一份安全感。


身为 Android 开发者,一个念头在我脑海中闪现:何不亲手写一款属于自己的类似应用?


开发历程


说干就干,最初采用 xml 方式进行开发,慢悠悠的写了几个月写完后。但随着谷歌大力推广 Compose,我决定试试新的技术,发现真香,于是对项目进行 Compose 重构。在 UI 风格上,也经历多次迭代,从最初的随意设计,到遵循谷歌 MD 风格,再到引入 Slat UI,改了无数次,程序员设计 ui 是真的难🥲。


写完后,原本计划在酷安或国内应用市场上架,于是觉得申请软著,反反复复打回,半年后终于申请下来了。结果新政出来了,要公安局备案,还要买服务器。


无奈之下,我选择放弃国内上架计划,转而开通谷歌开发者账号,当时谷歌开发者账号容易弄,交钱绑卡,很快就将应用上架至 Google Play。


现在一年左右了,没做宣传, 10+左右的下载量😓。


pEKJCQ0.webp


现在换了一个朝九晚九的的工作,再加上要带娃,决定将这个项目开源,有兴趣的朋友可以一起维护与开发。


理想与现实的碰撞


从事 Android 开发多年,一直在写公司业务代码,初入行时便怀揣着打造一款个人 App,上架应用市场,甚至有可能获得一点睡后收入的梦想。


开发完成后,我才发现,开发只是整个过程中最简单的环节。


在国内上架应用,软著开公司备案买服务器各种卡流程,很佩服那些上班又自己能这么折腾的开发者。感觉现在国内个人 Android 开发者这条路基本是断了。


Google Play 以前很简单,交钱就行了,现在好像也需要拉人头内测等流程。


App如果不氪金去推广,现在移动互联网已经卷成一片死海,应用市场 App 一大堆,特别是程序员三件套,再去做的话基本凉凉。


如果软件收费,售后问题同样棘手。我在酷安付费购买过多款优秀 App,并加入开发者群,潜水好几年。一旦软件收费,便意味着要对用户负责,面对用户五花八门的需求、各机型适配问题、还有跨平台需求,还有的人无脑喷,管理和维护成本其实非常高。


折腾了这么久,对于大多数人而言,个人开发者之路并不好走,尤其是身处 996 工作环境时,更是难上加难。过去工作稍有空闲,还能抽时间写代码,现在到家都快 10 点了,看到电脑就想吐(真实的心里反应)。


浅谈一下 35 岁程序员焦虑和副业问题,程序员软件三件套是行不通的,因为这些产品非常多,并且大多都是没什么亮点的,大部分估计赚的钱还不够服务器和各种流程的成本。我觉得如果是客户端,2025 年 个人开发者这条路都不建议去走了,前段时间看到很多套壳的 AI 客户端,套完壳容易,推广如果不砸钱,基本没人问津。好不容易推广,你干的过免费的 deepseek 和 kimi 之类的吗🤔


对于 35 岁焦虑, 个人觉得最好的路是找个制造业或者二三线城市的国企银行这种不怎么裁人的企业,躺平式继续 coding。
尾声


开源后,会不定时更新,后续打算试试 KMP 跨平台技术,平常时间大部分带娃,代码写的比较乱,大佬们将就的看。
欢迎提 PR,一起维护。


Github


github.com/ldlywt/Idea…


如果能给一个 star,不胜感激🙏


2025.2.26更新


对于有些人说是AI做的,我贴几张图。最开始在Github,然后迁到Gitee,后面又迁到Github,开源后为了防止敏感信息,把以前的提交记录给清了。


play store发版记录


image.png


Gitee 记录


image.png


作者:方之长
来源:juejin.cn/post/7471630643534512164
收起阅读 »

Uniapp小程序地图轨迹绘画

web
轨迹绘画 简介 轨迹绘画常用于展示车辆历史轨迹,运动历史记录等,本次案例采用的是汽车案例同时利用腾讯地图API,来实现地图轨迹绘画功能,具体情况根据实际项目变更。 本例是汽车轨迹绘画功能 1.在页面的onReady生命周期中创建map对象 onReady...
继续阅读 »

轨迹绘画


简介



  • 轨迹绘画常用于展示车辆历史轨迹,运动历史记录等,本次案例采用的是汽车案例同时利用腾讯地图API,来实现地图轨迹绘画功能,具体情况根据实际项目变更。


本例是汽车轨迹绘画功能


t7mcx-hlyjp.gif

1.在页面的onReady生命周期中创建map对象


onReady() {
// 创建map对象
this.map = uni.createMapContext('map');
// 获取屏幕高度(此处获取屏幕高度是因为本示例中使用了colorui的cu-custom自定义头部,需根据系统高度来自适应)
uni.getSystemInfo({
success: res => {
this.windowHeight = res.windowHeight;
}
});
},

2.设置轨迹动画事件


页面代码:


<view class="container">
<map id='map' :latitude="latitude" :longitude="longitude" :markers="covers"
:style="{ width: '100%', height: mapHeight + 'px' }" :scale="13" :polyline="polyline">

</map>


<view class="btnBox">
<button :disabled="isDisabled" @click="start" class="cu-btn bg-blue round shadow lg">开始回放</button>
<button @click="pause" class="cu-btn bg-red round shadow lg">暂停</button>
</view>

</view>


逻辑代码:



  • 1.轨迹动画的开始事件


start() {
if (this.movementInterval) {
clearInterval(this.movementInterval);
}
this.isStart = true;
this.moveMarker();
},


  • 2.轨迹动画的暂停事件


pause() {
this.isStart = false;
this.isDisabled = false;
if (this.movementInterval) {
clearInterval(this.movementInterval);
this.movementInterval = null;
}
},


  • 3.轨迹动画移动事件


moveMarker() {
if (!this.isStart) return;

if (this.playIndex >= this.coordinate.length) {
this.playIndex = 0;
uni.showToast({
title: "播放完成",
duration: 1400,
icon: "none",
});
this.isStart = false;
this.isDisabled = false;
return;
}

let datai = this.coordinate[this.playIndex];
this.map.translateMarker({
markerId: 1,
autoRotate: true,
destination: {
longitude: datai.longitude,
latitude: datai.latitude,
},
duration: 700,
complete: () => {
this.playIndex++;
this.moveMarker();
},
});
},


完整代码如下


<!-- 地图轨迹组件 -->
<template>
<view>
<cu-custom class="navBox" bgColor="bg-gradual-blue" :isBack="true">
<block slot="backText">返回</block>
<block slot="content">地图轨迹</block>
</cu-custom>

<view class="container">
<map id='map' :latitude="latitude" :longitude="longitude" :markers="covers"
:style="{ width: '100%', height: mapHeight + 'px' }" :scale="13" :polyline="polyline">

</map>

<view class="btnBox">
<button :disabled="isDisabled" @click="start" class="cu-btn bg-blue round shadow lg">开始回放</button>
<button @click="pause" class="cu-btn bg-red round shadow lg">暂停</button>
</view>
</view>
</view>
</template>


<script>
export default {
data() {
return {
map: null,
movementInterval: null, // 用于存储定时器的引用
windowHeight: 0,
mapHeight: 0,
timer: null,

isDisabled: false,
isStart: false,
playIndex: 1,

id: 0, // 使用 marker点击事件 需要填写id
title: 'map',
latitude: 34.263734,
longitude: 108.934843,
// 标记点
covers: [{
id: 1,
width: 42,
height: 47,
rotate: 270,
latitude: 34.259428,
longitude: 108.947040,
iconPath: 'http://zgonline.top/car.png',
callout: {
content: "鄂A·88888", // <img src="车牌信息" alt="" width="50%" />
display: "ALWAYS",
fontWeight: "bold",
color: "#5A7BEE", //文本颜色
fontSize: "12px",
bgColor: "#ffffff", //背景色
padding: 5, //文本边缘留白
textAlign: "center",
},
anchor: {
x: 0.5,
y: 0.5,
},
}],

// 线
polyline: [],

// 坐标数据
coordinate: [{
latitude: 34.259428,
longitude: 108.947040,
problem: false,
},
{
latitude: 34.252918,
longitude: 108.946963,
problem: false,
},
{
latitude: 34.252408,
longitude: 108.946240,
problem: false,
},
{
latitude: 34.249286,
longitude: 108.946184,
problem: false,
},
{
latitude: 34.248670,
longitude: 108.946640,
problem: false,
},
{
latitude: 34.248129,
longitude: 108.946826,
problem: false,
},
{
latitude: 34.243537,
longitude: 108.946816,
problem: true,
},
{
latitude: 34.243478,
longitude: 108.939003,
problem: true,
},
{
latitude: 34.241218,
longitude: 108.939027,
problem: true,
},
{
latitude: 34.241192,
longitude: 108.934802,
problem: true,
},
{
latitude: 34.241182,
longitude: 108.932235,
problem: true,
},
{
latitude: 34.247227,
longitude: 108.932311,
problem: true,
},
{
latitude: 34.250833,
longitude: 108.932352,
problem: true,
},
{
latitude: 34.250877,
longitude: 108.931756,
problem: true,
},
{
latitude: 34.250944,
longitude: 108.931576,
problem: true,
},
{
latitude: 34.250834,
longitude: 108.929662,
problem: true,
},
{
latitude: 34.250924,
longitude: 108.926015,
problem: true,
},
{
latitude: 34.250802,
longitude: 108.910121,
problem: true,
},
{
latitude: 34.269718,
longitude: 108.909921,
problem: true,
},
{
latitude: 34.269221,
longitude: 108.922366,
problem: false,
},
{
latitude: 34.274531,
longitude: 108.922388,
problem: false,
},
{
latitude: 34.276201,
longitude: 108.923433,
problem: false,
},
{
latitude: 34.276559,
longitude: 108.924004,
problem: false,
},
{
latitude: 34.276785,
longitude: 108.945855,
problem: false,
}
],
posi: {
id: 1,
width: 32,
height: 32,
latitude: 0,
longitude: 0,
iconPath: "http://cdn.zhoukaiwen.com/car.png",
callout: {
content: "鄂A·888888", // 车牌信息
display: "BYCLICK",
fontWeight: "bold",
color: "#5A7BEE", //文本颜色
fontSize: "12px",
bgColor: "#ffffff", //背景色
padding: 5, //文本边缘留白
textAlign: "center",
},
anchor: {
x: 0.5,
y: 0.5,
},
}
}
},
watch: {},
// 分享小程序
onShareAppMessage(res) {
return {
title: '看看这个小程序多好玩~',
};
},
onReady() {
// 创建map对象
this.map = uni.createMapContext('map');
// 获取屏幕高度
uni.getSystemInfo({
success: res => {
this.windowHeight = res.windowHeight;
}
});
},
mounted() {
this.setNavTop('.navBox')

this.polyline = [{
points: this.coordinate,
color: '#025ADD',
width: 4,
dottedLine: false,
}];
},
methods: {
setNavTop(style) {
let view = uni.createSelectorQuery().select(style);
view
.boundingClientRect((data) => {
console.log("tabInList基本信息 = " + data.height);
this.mapHeight = this.windowHeight - data.height;
console.log(this.mapHeight);
})
.exec();
},
start() {
if (this.movementInterval) {
clearInterval(this.movementInterval);
}
this.isStart = true;
this.moveMarker();
},
moveMarker() {
if (!this.isStart) return;

if (this.playIndex >= this.coordinate.length) {
this.playIndex = 0;
uni.showToast({
title: "播放完成",
duration: 1400,
icon: "none",
});
this.isStart = false;
this.isDisabled = false;
return;
}

let datai = this.coordinate[this.playIndex];
this.map.translateMarker({
markerId: 1,
autoRotate: true,
destination: {
longitude: datai.longitude,
latitude: datai.latitude,
},
duration: 700,
complete: () => {
this.playIndex++;
this.moveMarker();
},
});
},
pause() {
this.isStart = false;
this.isDisabled = false;
if (this.movementInterval) {
clearInterval(this.movementInterval);
this.movementInterval = null;
}
},
}
}
</script>


<style lang="scss" scoped>
.container {
position: relative;
}

.btnBox {
width: 750rpx;
position: absolute;
bottom: 60rpx;
z-index: 99;
display: flex;
justify-content: space-around;
}
</style>


作者:Coriander
来源:juejin.cn/post/7406173972738867227
收起阅读 »

使用 canvas 实现电子签名

web
一、引言 电子签名作为数字化身份认证的核心技术之一,已广泛应用于合同签署、审批流程等场景。之前做公司项目时遇到这个需求,于是研究了下,目前前端主要有两种方式实现电子签名:原生Canvason 和 使用signature_pad 依赖库。 本文将基于Vue3 +...
继续阅读 »

一、引言


电子签名作为数字化身份认证的核心技术之一,已广泛应用于合同签署、审批流程等场景。之前做公司项目时遇到这个需求,于是研究了下,目前前端主要有两种方式实现电子签名:原生Canvason 和 使用signature_pad 依赖库。


本文将基于Vue3 + TypeScript技术栈,深入讲解原生Canvas功能实现方案,并提供完整的可落地代码。


二、原生Canvas实现方案


完整代码:GitHub - seapack-hub/seapack-template: seapack-template框架


实现的逻辑并不复杂,就是使用canvas提供一个画板,让用户通过鼠标或者移动端触屏的方式在画板上作画,最后将画板上的图案生成图片保存下来。


(一) 组件核心结构


需要同时处理 鼠标事件(PC端)触摸事件(移动端),实现兼容的效果。


// PC端 鼠标事件
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDrawing);

// 移动端 触摸事件
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', endDrawing);

具体流程:通过状态变量控制绘制阶段:


阶段触发事件行为
开始绘制mousedown记录起始坐标,标记isDrawing=true
绘制中mousemove连续绘制路径(lineTo + stroke)
结束绘制mouseup重置isDrawing=false`

代码实现:


<div class="signature-container">
<canvas
ref="canvasRef"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="endDrawing"
@mouseleave="endDrawing"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="endDrawing"
>
</canvas>
<div class="controls">
<button @click="clearCanvas">清除</button>
<button @click="saveSignature">保存签名</button>
</div>
</div>

(二) 类型和变量



//类型定义
type RGBColor = `#${string}` | `rgb(${number},${number},${number})`
type Point = { x: number; y: number }
type CanvasContext = CanvasRenderingContext2D | null

// 配置
const exportBgColor: RGBColor = '#ffffff' // 设置为需要的背景色

//元素引用
const canvasRef = ref<HTMLCanvasElement | null>(null)
const ctx = ref<CanvasContext>()

//绘制状态
const isDrawing = ref(false)
const lastPosition = ref<Point>({ x: 0, y: 0 })

(三) 绘制逻辑实现


初始化画布


//初始化画布
onMounted(() => {
if (!canvasRef.value) return
//设置画布大小
canvasRef.value.width = 800
canvasRef.value.height = 400

//获取2d上下文
ctx.value = canvasRef.value.getContext('2d')
if (!ctx.value) return

//初始化 画笔样式
ctx.value.lineWidth = 2
ctx.value.lineCap = 'round'
ctx.value.strokeStyle = '#000' //线条颜色
// 初始填充背景
fillBackground(exportBgColor)
})

//填充背景方法
const fillBackground = (color: RGBColor) => {
if (!ctx.value || !canvasRef.value) return
ctx.value.save()
ctx.value.fillStyle = color
ctx.value.fillRect(0, 0, canvasRef.value.width, canvasRef.value.height)
ctx.value.restore()
}

获取坐标


将事件坐标转换为 Canvas 内部坐标,兼容滚动偏移


//获取坐标点,将事件坐标转换为 Canvas 内部坐标,兼容滚动偏移
const getCanvasPosition = (clientX: number, clientY: number): Point => {
if (!canvasRef.value) return { x: 0, y: 0 }

//获取元素在视口(viewport)中位置
const rect = canvasRef.value.getBoundingClientRect()
return {
x: clientX - rect.left,
y: clientY - rect.top,
}
}

// 获取事件坐标
const getEventPosition = (e: MouseEvent | TouchEvent): Point => {
//TouchEvent 是在支持触摸操作的设备(如智能手机、平板电脑)上,用于处理触摸相关交互的事件对象
if ('touches' in e) {
return getCanvasPosition(e.touches[0].clientX, e.touches[0].clientY)
}
return getCanvasPosition(e.clientX, e.clientY)
}

开始绘制


将 isDrawing 变量值设置为true,表示开始绘制,并获取当前鼠标点击或手指触摸的坐标。


//开始绘制
const startDrawing = (e: MouseEvent | TouchEvent) => {
isDrawing.value = true
const { x, y } = getEventPosition(e)
lastPosition.value = { x, y }
}

绘制中


每次移动时创建新路径,连接上一个点与当前点。


//绘制中
const draw = (e: MouseEvent | TouchEvent) => {
if (!isDrawing.value || !ctx.value) return
//获取当前所在位置
const { x, y } = getEventPosition(e)
//开始新路径
ctx.value.beginPath()
//移动画笔到上一个点
ctx.value.moveTo(lastPosition.value.x, lastPosition.value.y)
//绘制线条到当前点
ctx.value.lineTo(x, y)
//描边路径
ctx.value.stroke()
//更新最后的位置
lastPosition.value = { x, y }
}

结束绘制


将 isDrawing 变量设为false,结束绘制


//结束绘制
const endDrawing = () => {
isDrawing.value = false
}

添加清除和保存方法


//清除签名
const clearCanvas = () => {
if (!ctx.value || !canvasRef.value) return
ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
}

//保存签名
const saveSignature = () => {
if (!canvasRef.value) return
const dataURL = canvasRef.value.toDataURL('image/png')
const link = document.createElement('a')
link.download = 'signature.png'
link.href = dataURL
link.click()
}

移动端适配


// 触摸事件处理
const handleTouchStart = (e: TouchEvent) => {
e.preventDefault();
startDrawing(e.touches[0]);
};

const handleTouchMove = (e: TouchEvent) => {
e.preventDefault();
draw(e.touches[0]);
};



(四) 最终效果


image.png


作者:烈风逍遥
来源:juejin.cn/post/7484987385665011762
收起阅读 »

努力工作,你已经是很了不起的成年人了

1、像潮水一样的焦虑 "我二十三岁,靠自己的努力,全款小米Su7 Ultra。" "二十七岁,工作之余,副业一个月赚两万。" "用 DeepSeek 做副业,三天赚了九千" 对的,这些是目前互联网上短视频、问答平台、公众号里再常见不过的文案,衬托之...
继续阅读 »

1、像潮水一样的焦虑



"我二十三岁,靠自己的努力,全款小米Su7 Ultra。"




"二十七岁,工作之余,副业一个月赚两万。"




"用 DeepSeek 做副业,三天赚了九千"



对的,这些是目前互联网上短视频、问答平台、公众号里再常见不过的文案,衬托之下:



仿佛早上挤地铁四号线、一边走路一边吃热干面、坐在格子间里连喝一口水都要见缝插针、晚上加班结束要迈着飞快的脚步去赶最后一班地铁的你,是一个恬不知耻正在迈向中年的你,像是一个混吃等死毫无作为的废物。



凌晨你回到出租屋,打开朋友圈,看到的却是这样那样的焦虑:



三十天写作训练营、素人短视频日入八百、宝妈靠手作实现财务自由。



这些标题像便利店门口的霓虹灯,在深夜里闪烁得格外刺眼,仿佛在质问每个失眠的成年人:你的人生怎么还没开张?


你躺在床上,闭上眼睛的时候还在脑海里反复复盘着:“我这领个死工资也不是个事儿啊,什么时候能搞个副业,也实现一下财务自由。赚够两百万存款的时候,我要把辞职信拍到领导的脸上。”


在焦虑、自责、懊恼之余,你是否忘了什么事情?


你是否忘了,你其实是一个:


经过完整的高等教育,每天算上通勤,为工作付出超过12小时时间,没有沾染赌毒行为,勤勤恳恳态度认真的年轻人。工作之余,你唯一的爱好可能就是下班后躲在出租屋里打两把游戏。


从任何角度上看,你都是一个努力的,踏实的,积极向上的年轻人。


那么,究竟是谁在引导你否定自己的努力,对自己的状态进行 PUA 呢?


副业如果真那么神奇的话,为什么它还是 “副业” 呢?


了解那些焦虑的本质,你会发现:“副业可能是版本陷阱”。


2,副业可能是版本陷阱


我们 Dota2 玩家有个经常被挂在嘴边的词语,叫 “版本陷阱”,指的是那些版本更新之后,被大家口口相传多么厉害,但是真在职业赛场上被选出来却胜率惨不忍睹的英雄。


很抱歉,我可能要说一句丧气话:


“副业就是这个版本的【版本陷阱】。”


为什么呢?



  • 先聊副业本质


    副业的本质是跳出主页赛道,恰好进入一条机会更多、收入更高的蓝海赛道;这个机会可能来自于你自身(你恰好擅长某些主业之外的能力),更多的时候来自于时代红利(早几年间你非常擅长制作短视频)。


    因此,少量时间+副业可以带来高收入的本质,是个人长项在蓝海领域博取一个创收机会,但是你先问问自己,有没有主业之外领域的优势技能。


  • 再说版本环境


    在上一条里面我们提到了“蓝海行业”,这一点非常重要,只有某个赛道出现明显的技术缺口或人员缺口时,副业者才有腾挪的空间。但如果一个行业或赛道已经人满为患,你准备拿每天2个小时和那些每天12个小时的团队作战吗?


  • 最后分析下“口口相传”的那些人


    你以为天天分享副业赚钱的人群:“人生导师、大公无私、先富的来带动后富的。”


    实际发那些文案的人群:那些教你"睡后收入"的博主,自己往往在通宵直播卖课;晒着海岛定位说走就走的旅行博主,回程机票可能都是网贷分期。这个魔幻的时代,连贩卖成功学的都在996加班。


    而他们兜售焦虑的最后一环,还是希望焦虑者咬上他们那锈迹斑斑的铁钩,成为下一个为“财富自由”付费的“上进者”。



请容许我再说一句丧气话:



绝大多数普通人的搞钱手段就是上班,只有上班。



哪有那么多暴富的机会在前方等着你?哪有那么多“睡后”的收入在梦中等着你?哪有那么一劳永逸又无比轻松的战役,在彼岸等着你。


你之所以相信彼岸的美好,有没有可能是因为你看不穿横在河面上的浓雾?


3,努力上班,已经是很棒的成年人了


每天我总能见到大家在各种空间和途径输出焦虑:



女孩责怪男孩不上进,下班只知道打游戏。




男孩抱怨不该生孩子,没有时间和机会创业搞财富自由。




总有人和自己身边的成功同龄人做比对,反思得出“自己是个废物”的神奇结论。



可是啊同学,我们也只是一群普通人而已。


在步入三十五岁之后,变成互联网行业里的“下岗预备役”之后,我逐渐开始认清了自己的属性和位置,不再苛求自己与众不同。


经过完整的高等教育,每天算上通勤,为工作付出超过12小时时间,没有沾染赌毒行为,勤勤恳恳态度认真的年轻人。工作之余,你唯一的爱好可能就是下班后躲在出租屋里打两把游戏。


每周可以有时间陪家人一起做一些不花钱的娱乐。


每周可以有时间陪孩子梳理一下近期的学习、分享一下思考。


每周有那么几个小时可以做一些让自己从内在感到快乐、感到满足、感到宁静的事情。


你没有去赌博欠下一屁股债,没有去加杠杆投资卖掉父母的房产,没有闷着头贷款创业亏光爹妈的棺材本。


同学,你可能意识不到,但我今天非常想郑重地告诉你:



在成年人这个群体之中,你已经是非常杰出、非常棒的那一批了。



副业焦虑?让它见鬼去吧。


作者:摸鱼的春哥
来源:juejin.cn/post/7481187555866804224
收起阅读 »

微信小程序主包过大终极解决方案

web
随着小程序项目的不断迭代升级,避免不了体积越来越大。微信限制主包最多2M,然而我们的项目引入了直播插件直接占了1.1M,导致必须采用一些手段去优化。下面介绍一下优化思路和终极解决方案。 1.分包 我相信几乎所有人都能想到的方案,基本上这个方案就能解决问题。具...
继续阅读 »

随着小程序项目的不断迭代升级,避免不了体积越来越大。微信限制主包最多2M,然而我们的项目引入了直播插件直接占了1.1M,导致必须采用一些手段去优化。下面介绍一下优化思路和终极解决方案。



1.分包


我相信几乎所有人都能想到的方案,基本上这个方案就能解决问题。具体如何实现可以参照官方文档这里不做过多说明。(基础能力 / 分包加载 / 使用分包 (qq.com)),但是有时候你会发现分包之后好像主包变化不是很大,这是为什么呢?



  • 痛点1:通过依赖分析,如果分包中引入了第三方依赖,那么依赖的js仍然会打包在主包中,例如echarts、wxparse、socket.io。这就导致我们即使做了分包处理,但是主包还是很大,因为相关的js都会在主包中的vendor.js

  • 痛点2:插件只能在主包中无法分包,例如直播插件直接占据1M
    image.png

  • 痛点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文件。这样就不会影响打包体积了。


使用步骤



  1. npm install --save we-script

  2. "packNpmRelationList": [{"packageJsonPath": "./package.json", "miniprogramNpmDistDir":"./dist/"}]

  3. 点击开发者工具中的菜单栏:工具 --> 构建 npm

  4. "usingComponents": {"we-script": "we-script"}

  5. <we-script src="url1" />


使用中存在的坑


构建后可能会出现依赖报错,解决的方式就是将编译好的文件手动拖入miniprogram_npm文件夹中,主要是三个文件夹:we-script,acorn,eval5


最后成功解决了主包文件过大的问题,只要是第三方依赖,都可以通过这个办法去加载。


感谢阅读,希望来个三连支持下,转载记得标注原文地址~


作者:前端小鱼26
来源:juejin.cn/post/7355057488351674378
收起阅读 »

uni-app 接入微信短剧播放器

web
前言 作为一个 uniapp 初学者,恰巧遇到微信短剧播放器接入的需求,在网上检索许久并没有发现傻瓜式教程。于是总结 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 中定义的一致:
pages.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
收起阅读 »

后端出身的CTO问:"前端为什么没有数据库?",我直接无语......

web
😅【现场还原】 "前端为什么没有自己的数据库?把数据存前端不就解决了后端性能问题" ——当CTO抛出这个灵魂拷问时,会议室突然安静得能听见CPU风扇的嗡鸣,在座所有人都无语了。这场因后端性能瓶颈引发的技术博弈,最终以"前端分页查询+本地筛选"的妥协方案告终。 ...
继续阅读 »

😅【现场还原】


"前端为什么没有自己的数据库?把数据存前端不就解决了后端性能问题" ——当CTO抛出这个灵魂拷问时,会议室突然安静得能听见CPU风扇的嗡鸣,在座所有人都无语了。这场因后端性能瓶颈引发的技术博弈,最终以"前端分页查询+本地筛选"的妥协方案告终。


面对现在几乎所有公司的技术leader都是后端出身,有的不懂前端甚至不懂技术,作为前端开发者,我们真的只能被动接受吗?


😣【事情背景】



  • 需求:前端展示所有文章的标签列表,用户可以选择标签筛选文章,支持多选,每个文章可能有多个标签,也可能没任何标签。

  • 前端观点:针对这种需求,我自然想到用户选中标签后,将标签id传给后端,后端根据id筛选文章列表返回即可。

  • 后端观点:后端数据分库分表,根据标签检索数据还要排序分页,有性能瓶颈会很慢,很慢就会导致天天告警。

  • 上升决策:由于方案有上述分歧,我们就找来了双方leader决策,双方leader也有分歧,最终叫来了CTO。领导想让我们将数据定时备份到前端,需要筛选的时候前端自己筛选。



    CTO语录


    “前端为什么没有数据库?,把数据存前端,前端筛选,数据库不就没有性能压力了”


    "现在手机性能比服务器还强,让前端存全量数据怎么了?"


    "IndexedDB不是数据库?localStorage不能存JSON?"


    "分页?让前端自己遍历数组啊,这不就是你们说的'前端工程化'吗?"





😓【折中方案】


在方案评审会上,我们据理力争:



  1. 分页请求放大效应:用户等待时间=单次请求延迟×页数

  2. 内存占用风险:1万条数据在移动端直接OOM

  3. 数据一致性难题:轮询期间数据更新的同步问题


但现实往往比代码更复杂——当CTO拍板要求"先实现再优化",使用了奇葩的折中方案:



  • 前端轮询获取前1000条数据做本地筛选,用户分页获取数据超过1000条后,前端再轮询获取1000条,以此类推。

  • 前端每页最多获取50条数据,每次最多并发5个请求(后端要求)


只要技术监控不报错,至于用户体验?慢慢等着吧你......


🖨️【批量并发请求】


既然每页只有50条数据,那我至少得发20个请求来拿到所有数据。显然,逐个请求会让用户等待很长时间,明显不符合前端性能优化的原则。于是我选择了 p-limitPromise.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);
};


📝【总结】


这场技术博弈给我们带来三点深刻启示:



  1. 数据民主化趋势:随着WebAssembly、WebGPU等技术的发展,前端正在获得堪比后端的计算能力

  2. 妥协的艺术:临时方案必须包含演进路径,我们的分页实现预留了切换GraphQL的接口

  3. 性能新思维:从前端到边缘计算,性能优化正在从"减少请求"转向"智能分发"


站在CTO那句"前端为什么没有数据库"的肩膀上,我们正在构建这样的未来:每个前端应用都内置轻量级数据库内核,通过差异同步策略与后端保持数据一致,利用浏览器计算资源实现真正的端智能。这不是妥协的终点,而是下一代Web应用革命的起点。


后记:三个月后,我们基于SQL.js实现了前端SQL查询引擎,配合WebWorker线程池,使得复杂筛选的耗时从秒级降至毫秒级——但这已经是另一个技术突围的故事了。


作者:VeryCool
来源:juejin.cn/post/7472732247932174388
收起阅读 »

Vue3 实现最近很火的酷炫功能:卡片悬浮发光

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 有趣的动画效果 前几天在网上看到了一个很有趣的动画效果,如下,光会跟随鼠标在卡片上进行移动,并且卡片会有视差的效果 那么在 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多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


作者:Sunshine_Lin
来源:juejin.cn/post/7373867360019742758
收起阅读 »

都2025年了,还在用Markdown写文档吗?

俗话说得好:“程序员宁愿写 1000 行代码,也不愿意写 10 个字的文档。” 不愿写文档的原因,一方面是咱理科生文采确实不好,另一方面则是文档的更新维护十分麻烦。 每次功能有变更的时候, 时间又急(其实就是懒),很难想得起来同时去更新文档。 特别是文档中代码...
继续阅读 »

俗话说得好:“程序员宁愿写 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渲染示例


缺点就是 adoc 的语法和 Markdown 相差还挺大的,对以前用 Markdown 写文档的程序员有一定的熟悉成本。但是现在有 AI 啊,我们可以先用 Markdown 把文档写好,交给 Kimi 把它翻译成 Markdown。我对 adoc 的古怪语法也不是很熟悉,并且项目以前的文档都是 Markdown 写,都是 AI 帮我翻译的。


Github 渲染 adoc 文档的坑


我最开始尝试在 Github 上用 README.adoc 代替 README.md,发现其中的 include 语法并没有生效:



Github 对于 adoc include的渲染逻辑还挺诡异的,既不展示引用文件的内容,也没有原样展示 adoc 代码



Github对于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 发起的提交:


image.png


至此,就再也不用担心被人吐槽文档不专业啦。


作者:代码不洗头
来源:juejin.cn/post/7464247481227100196
收起阅读 »

URL地址末尾加不加”/“有什么区别

web
URL 结尾是否带 / 主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下: 1. 基础概念 URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。 目录 vs. 资源: 以 / 结尾的 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/




📌 示例





(2)相对路径解析


URL 末尾是否有 / 会影响相对路径的解析


假设 HTML 页面包含以下 <img> 标签:


<img src="image.png">

📌 示例:



原因:



  • / 结尾的 URL,浏览器会认为它是一个目录,相对路径会基于 folder/ 解析。

  • 不带 /,浏览器可能认为 folder文件,相对路径解析可能会出现错误。




(3)SEO 影响


搜索引擎对 https://example.com/folder/https://example.com/folder 可能会视为两个不同的页面,导致 重复内容问题,影响 SEO 排名。因此:





(4)API 请求


对于 RESTful API,带 / 和不带 / 可能导致不同的行为:



一些 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 设计决定

如果你在开发网站,建议:



  1. 统一 URL 规则,例如所有目录都加 / 或者所有请求都不加 /,然后用 301 重定向 确保一致性。

  2. 测试 API 的行为,确认带 / 和不带 / 是否影响请求结果。


作者:Chiyamin
来源:juejin.cn/post/7468112128928350242
收起阅读 »

用node帮老婆做excel工资表

web
我是天元,立志做1000个有趣的项目的前端。如果你喜欢的话,请点赞,收藏,转发。评论领取零花钱+100勋章 背景 我老婆从事HR的工作,公司有很多连锁店,她需要将所有的门店的工资汇总计算,然后再拆分给各门店请确认,最后再提供给财务发工资。 随着门店数量渐渐增...
继续阅读 »

我是天元,立志做1000个有趣的项目的前端。如果你喜欢的话,请点赞,收藏,转发。评论领取零花钱+100勋章



背景


我老婆从事HR的工作,公司有很多连锁店,她需要将所有的门店的工资汇总计算,然后再拆分给各门店请确认,最后再提供给财务发工资。


随着门店数量渐渐增多,渐渐的我老婆已经不堪重负,每天加班都做不完,严重影响夫妻感情生活。


最终花费了2天的时间,完成了整个node程序,她只需要传入工资表,相应的各种表格在10s内自动输出。目前已正式交付,得到了每月零花钱提高100元的重大成果


整体需求



  • 表格的导入和识别

  • 表格的计算(计算公式要代入),表格样式正确

  • 最终结果按照门店拆分为工资表


需求示例(删减版)


image.png

需求为,根据传入的基本工资及补发补扣,生成总工资表,门店工资表,财务工资表发放表。


工资表中字段为门店,姓名,基本工资,补发补扣,最终工资(基本工资+补发补扣)。最后一行为总计

门店工资表按照每个门店,单独一个表格,字段同工资表。最后一行为总计


工资表


image.png


基础工资


image.png


补发补扣


image.png


技术选型


这次的主力库为exceljs,官方文档介绍如下



读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。


一个 Excel 电子表格文件逆向工程项目



选择exceljs是因为它支持完整的excel的样式及公式。


安装及目录结构


优先安装exceljs


npm init
yarn add exceljs

创建input,out,src三个文件夹,src放入index.js

image.png


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()

数据拆分


通过getWorksheetApi,我们可以获取到对应的工作表的内容


  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.getCellApi获取到对应某一列的内容,例如门店是在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.writeFileApi来保存文件


 Object.keys(baseSalary).forEach((shopName) => {

...

workbook.xlsx.writeFile(outputPath + `/${shopName}工资表.xlsx`);
})

最终效果


image.png


image.png


相关代码地址


github.com/tinlee/1000…


作者:天元reborn
来源:juejin.cn/post/7346421986607087635
收起阅读 »

蓝牙耳机丢了,我花几分钟写了一个小程序,找到了!​

web
你是否曾经经历过蓝牙耳机不知道丢到哪里去的困扰?特别是忙碌的早晨,准备出门时才发现耳机不见了,整个心情都被影响。幸运的是,随着技术的进步,我们可以利用一些简单的小程序和蓝牙技术轻松找到丢失的耳机。今天,我要分享的是我如何通过一个自制的小程序,利用蓝牙发现功能,...
继续阅读 »

你是否曾经经历过蓝牙耳机不知道丢到哪里去的困扰?特别是忙碌的早晨,准备出门时才发现耳机不见了,整个心情都被影响。幸运的是,随着技术的进步,我们可以利用一些简单的小程序和蓝牙技术轻松找到丢失的耳机。今天,我要分享的是我如何通过一个自制的小程序,利用蓝牙发现功能,成功定位自己的耳机。这不仅是一次有趣的技术尝试,更是对日常生活中类似问题的一个智能化解决方案。

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 { ViewText } from "@tarojs/components";
import { AtButtonAtIconAtProgressAtListAtListItem } 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 >= -50return 100;
    if (rssi <= -100return 0;
    return Math.round(((rssi + 100) / 50) * 100);
  };

  const getDirectionGuide = (rssi) => {
    if (rssi >= -50return "非常接近!你已经找到了!";
    if (rssi >= -70return "很近了,继续朝这个方向移动!";
    if (rssi >= -90return "正确方向,但还需要继续寻找。";
    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.rssidBm`}
              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. 实际应用与优化空间

这个小程序的实际效果超出了我的预期。我能够通过它快速找到丢失的耳机,整个过程不到几分钟时间。然而,值得注意的是,由于蓝牙信号会受到环境干扰,例如墙体、金属物等,导致信号强度并不总是精确。在后续的优化中,我计划加入更多的信号处理算法,例如利用三角定位技术,结合多个信号源来提高定位精度。此外,还可以考虑在小程序中加入可视化的信号强度图,帮助用户更直观地了解耳机的大致方位。

一些思考:

蓝牙耳机定位这个小程序的开发,展示了技术在日常生活中的强大应用潜力。虽然这个项目看似简单,但背后的原理和实现过程非常具有教育意义。通过这次尝试,我们可以看到,借助开源技术和简单的编程能力,我们能够解决许多日常生活中的实际问题。

参考资料:

    1. 微信小程序官方文档:developers.weixin.qq.com
    1. 蓝牙信号强度(RSSI)与距离关系的研究:http://www.bluetooth.com
    1. 个人开发者经验分享: 利用蓝牙发现功能定位设备

  • 作者:老码小张
    来源:juejin.cn/post/7423610485180727332
    收起阅读 »

    前端可玩性UP项目:大屏布局和封装

    web
    前言 autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。 这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。 分析设计稿 分...
    继续阅读 »

    前言


    autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。


    这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。


    分析设计稿


    分析设计稿之前先吐槽一下大屏这种展现形式,这简直就是自欺欺人、面子工程的最直接的诠释,是吊用没有,只为了好看,如果设计的再不好看啊,这就纯纯是屎。在我的理解中,这就像把PPT放到了web端,仅此而已。



    但是王哥告诉我:"你看似没有用的东西,其实都有用,很多想真正做出有用的产品的企业,没钱,就要先把面子工程做好,告诉别人他们要做一件什么事,这样投资人才会看到,后面才有机会发展。"



    布局方案


    image.png
    上图展示了一个传统意义上且比较普遍的大屏形态,分为四个部分,分别是


    头部


    头部经常放标题、功能菜单、时间、天气


    左右面板


    左右面板承载了各种数字和报表,还有视频、轮播图等等


    中间


    中间部分一般放地图,这其中又分假地图(一张图片)、图表地图(如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 和 % 之间,加群的同学多数是因为用了相对单位,导致留白了。不过人各有所长,跟着各位大佬我也学到了很多。


    看下图


    image.png
    假如有两个宽度为1000的元素,他们内部都有一个子元素,第一个用百分比设置为 width:50%;left:1% , 第二个设置为 wdith:500px;left:10px 。此时,只要外部的1000px的容器宽度不变,这两个内部元素在视觉上是一模一样的,且在实际数值上也是一模一样的,他们宽度都为500px,距离左侧10px。


    但是如果外部容器变大了,来看一下效果:


    image.png
    在样式不变的情况下,仅改变外部容器大小,差异就出来了,由上图可知,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万是不包括投资理财或者基金股票里面的钱,而是我完全可以自由支配的。


    毕竟没有人可以在没有工作的情况下,没有收入的情况下。没有存款的情况下,还能保持一个不焦虑的状态。


    当我得出了这两个具体的目标之后,我整个人瞬间被一种兴奋的状态填满,我瞬间找到了工作的意义和动力。


    也许你会说,我的这个想法对我现在生活根本起不到任何的改变作用。


    我依旧还需要每天七点起床,还是要每天重复地去过我两点一线的生活。


    但是于我自己而言,当我给我上班的这件事情加了一个两年的期限之后,我突然觉得我的未来,我的生活都充满了希望。


    我整个人从不想动的状态,变成了一种被兴奋的状态填满的感觉。


    所以,如果你和我一样有一些类似的困扰,有一些你不想做而又不得不做的事情,让你有一种深陷泥潭,无法前进的感觉,那你不妨试一下这个方法。


    结合你自己的实际情况,为你不想做这件事情,设计一个期限,这个期限必须要是你认可,你接受,并且你认为你可以在这个截止时间之前完成的。


    我想这个决定应该会对你的生活带来一些改变。


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

    2025年了,令人唏嘘的Angular,现在怎么样了🚀🚀🚀

    web
    迅速崛起和快速退出 时间回到2014年,此时的 Angular 1.x 习得了多种武林秘籍,左手降龙十八掌、右手六脉神剑,哦不,左手双向数据绑定、右手依赖注入、上能模块化开发、下有模板引擎 和 前端路由, 背后还有Google这个风头无两的带头大哥做技术背书,...
    继续阅读 »

    迅速崛起和快速退出


    时间回到2014年,此时的 Angular 1.x 习得了多种武林秘籍,左手降龙十八掌、右手六脉神剑,哦不,左手双向数据绑定、右手依赖注入、上能模块化开发、下有模板引擎前端路由, 背后还有Google这个风头无两的带头大哥做技术背书,可以说集万千功能和宠爱于一身,妥妥的主角光环。


    而此时的江湖,B端开发正尝到了 SPA 的甜头,积极的从传统的 MVC 开发模式转变为更为方便快捷的单页面应用开发模式,



    文章同步在公众号:萌萌哒草头将军,欢迎关注!



    一拍即合,强大的一站式单页面开发框架Angular自然而然,就成了公认的武林盟主,江湖一哥。


    angular下载量


    但是好景不长,2016年9月14日 Angular 2.x 的发布,彻底断送了武林盟主的宝座,



    Vue:大哥,你可是真给机会呀!



    ts下载量


    2.0 使用ts彻底重写(最早支持ts的框架)、放弃了脏检查更新机制,引入了响应式系统、使用现代浏览器标准、加入装饰器语法,和 1.0 完全不兼容。可以从上图看到,此时大家基本上还不太接受ts!


    新手面对陡然升高的学习曲线叫苦连连,已经入坑的开发者因为巨大的迁移工作而怨声载道。


    此时,默默耕耘了两年的小弟,Vue已经拥有完备的本地化文档和丰富的可选生态,而且作为新手你只要会使用htmlcssjavascript,就可以上手写项目了。


    所以,此时的 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也是自创了很多绝学,例如:局部变更检测。


    该绝学主要招式:组件级变更检测策略、引入zonejsOnPush 策略等。


    1. 组件级变更检测策略

    每个组件都有自己的更新策略,只有组件的属性和文本发生变化时,才会触发变更检测!


    2. 引入zonejs

    引入zonejs拦截http setTimeout 用户交互事件等异步操作


    3. OnPush 策略

    默认情况下,整个组件树在变更时更新。


    默认策略


    但是开发者可以选择 OnPush 策略,使得组件仅在输入属性发生变化、事件触发或手动调用时才进行变更检测。这进一步大大减少了变更检测的频率,适用于数据变化不频繁的场景。


    OnPush策略


    Signals系统


    很快啊,当SolidJS凭借优异的信号系统在江湖上闯出了响亮的名声,这时,大家才意识到,原来还有更优秀的开发方式!signal系统的开发方式,也被公认为新一代的武林绝技!


    于是,Angular 16它来了,它带着signalmemoeffect三件套走来了!


    当使用signal时,更新仅仅发生在当前组件。


    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,那似乎我们的竞争力...
    继续阅读 »

    先赞后看,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到你的点赞点赞点赞。


    在这里插入图片描述



    创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️



    作者:JavaSouth南哥
    来源:juejin.cn/post/7436658089703145524
    收起阅读 »

    ⚔️不让我在控制台上调试,哼,休想🛠️

    web
    在 JavaScript 中,使用 debugger 关键字可以在代码执行到该位置时触发断点调试。这可以帮助开发人员进行代码调试和排错。然而,有些网站开发者可能会故意使用 debugger 关键字来阻止调试,从而增加代码的安全性。但仍然有一些方法可以绕过这种防...
    继续阅读 »

    在 JavaScript 中,使用 debugger 关键字可以在代码执行到该位置时触发断点调试。这可以帮助开发人员进行代码调试和排错。然而,有些网站开发者可能会故意使用 debugger 关键字来阻止调试,从而增加代码的安全性。但仍然有一些方法可以绕过这种防护措施,进行调试和排错。


    禁用浏览器debugger


    因为 debugger 其实就是对应的一个断点,它相当于用代码显示地声明了一个断点,要解除它,我们只需要禁用这个断点就好了。


    禁用全局断点


    全局禁用开关位于 Sources 面板的右上角,如下图所示:


    image-20240516204937081.png


    点击它,该按钮会被激活,变成蓝色。


    这个时候我们再重新点击一下 Resume script execution(恢复脚本执行)按钮,跳过当前断点,页面就不会再进入到无限 debugger 的状态了。


    image-20240516205310992.png


    注意,禁用所有断点可能会导致你错过一些潜在的问题或错误,因为代码将会连续执行而不会在可能的问题点停止。因此,在禁用所有断点之前,请确保你已经理解了代码的行为,并且明白在出现问题时该如何调试。


    禁用局部断点


    尝试使用另一种方法来跳过这个无限 debugger。在 debugger 语句所在的行的行号上单击鼠标右键,此时会出现一个快捷菜单,操作下图所示:


    动画.gif


    添加条件断点


    在JS代码 debugger 行数位置的最左侧点击右键,添加条件断点(满足条件才会进入断点),将条件设置为false,就是条件永远不成立,永远不会断下来。


    动画.gif


    添加条件断点还可以监视获取一些变量信息,还是挺好用的。


    如果是简单的debugger断点,直接用上边的方式就可以,如果是通过定时器触发的debugger断点,就需要进行Hook处理了。


    以上的方案执行完毕之后有时候会跳转空页面,这时候只需要在空页面上打开原先地址即可。


    先打开控制台


    有时候我们一打开网页,就直接进入空页面,控制台上的js和html文件也随之为空,这时候需要在空白页面,或者F12等键无法打开控制台等,这种可以先打开控制台,然后再在空白页面上打开网站即可。


    可以在这个网站上试一下。


    替换文件


    直接使用浏览器开发者工具替换修改js(Sources面板 --> Overrides),或者通过FD工具替换。


    这种方式的核心思路,是替换 JS 文件中的 debugger 关键字,并保存为本地文件,在请求返回的时候、通过正则匹配等方式、拦截并替换返回的 JS 代码,以达到绕过 debugger 的目的。也可以直接删掉相关的debugger代码。


    具体实现可参考:2024最新版JavaScript逆向爬虫教程-------基础篇之无限debugger的原理与绕过


    快捷方案-使用油猴等插件


    使用这种方法,就不需要再打 script 断点。直接安装插件即可。


    image-20240516203434774.png


    参考文献


    2024最新版JavaScript逆向爬虫教程-------基础篇之无限debugger的原理与绕过


    解决浏览器调试无限debugger


    作者:Aplee
    来源:juejin.cn/post/7369505226921738278
    收起阅读 »

    🔏别想调试我的前端页面代码🔒

    web
    这里我们不介绍禁止右键菜单, 禁止F12快捷键和代码混淆方案。 无限debugger 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行 由于程序被 debugger 阻止,所以无法进...
    继续阅读 »

    71e52c67f5094e44b92ccaed93db15c5.jpg


    这里我们不介绍禁止右键菜单, 禁止F12快捷键代码混淆方案。


    无限debugger



    • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行

    • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的.


    基础方案


    (() => {
     function ban() {
       setInterval(() => { debugger; }, 50);
    }
     try {
       ban();
    } catch (err) { }
    })();


    • setInterval 中的代码写在一行,可以禁止用户断点,即使添加 logpointfalse 也无用

    • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的


    浏览器宽高


    根据浏览器宽高、与打开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可以禁用所有一切可以进入开发者工具的方法,防止通过开发者工具进行的代码搬运。



    该库有以下特性:



    1. 支持可配置是否禁用右键菜单

    2. 禁用 f12 和 ctrl+shift+i 等快捷键

    3. 支持识别从浏览器菜单栏打开开发者工具并关闭当前页面

    4. 开发者可以绕过禁用 (url参数使用tk配合md5加密)

    5. 多种监测模式,支持几乎所有浏览器(IE,360,qq浏览器,FireFox,Chrome,Edge...)

    6. 高度可配置、使用极简、体积小巧

    7. 支持npm引用和script标签引用(属性配置)

    8. 识别真移动端与浏览器开发者工具设置插件伪造的移动端,为移动端节省性能

    9. 支持识别开发者工具关闭事件

    10. 支持可配置是否禁用选择、复制、剪切、粘贴功能

    11. 支持识别 eruda 和 vconsole 调试工具

    12. 支持挂起和恢复探测器工作

    13. 支持配置ignore属性,用以自定义控制是否启用探测器

    14. 支持配置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: () => {
      // ...
    }
    })

    回调函数支持自定义打开控制台后的策略。


    参数


    namerequiredtypedefaultdescription
    clearnobooleantrue禁用 console.clear 函数
    debugnobooleantrue是否开启定时 debugger 反爬虫审查
    debugTimenonumber3000定时 debugger 时间间隔(毫秒)
    redirectnostring-开启控制台后重定向地址
    writenostring 或Element-开启控制台后重写 document.body 内容,支持传入节点或字符串
    callbacknoFunction-开启控制台后的回调函数
    bfcachenobooleantrue禁用 bfcache 功能

    注:redirectwritecallback 三种策略只能取其一,优先使用回调函数。


    参考文章


    禁止别人调试自己的前端页面代码


    前端防止恶意调试


    禁止调试,阻止浏览器F12开发者工具


    前端防止调试技术


    结语


    需要注意的是,这些技术可以增加攻击者分析和调试代码的难度,但无法完全阻止恶意调试。因此,对于一些敏感信息或关键逻辑,最好的方式是在后端进行处理,而不是完全依赖前端来保护。


    下篇文章主要介绍如何破解这些禁止调试的方法。


    矛与盾:⚔️不让我在控制台上调试,哼,休想🛠️


    作者:Aplee
    来源:juejin.cn/post/7368313344712179739
    收起阅读 »

    DeepSeek引发行业变局,2025 IT人该如何破局抓住机遇

    一. 🎯 变局中抓住核心 这个春节被DeepSeek消息狂轰滥炸,很多做IT朋友已经敏锐的意识到 一场变局已经酝酿,整个IT行业都将迎来洗牌重塑。 中小IT企业、个人创业者、普通人该如何面对这场变局,如何不被市场淘汰,如何抓住机遇? 先说结论 2025年,谁能...
    继续阅读 »

    image.png


    一. 🎯 变局中抓住核心


    这个春节被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工具


    做巨头看不上的 “小而美”生意 🤩


    作者:赛博东哥CyberFD
    来源:juejin.cn/post/7468203211725783094
    收起阅读 »

    表妹问:前端好玩吗?我说好玩,但表妹接下来的回复看哭了我

    web
    表妹问:前端好玩吗?我说好玩,但表妹接下来的回复看哭了我。 是的,回复如下: 这红海血途上,新兵举着 "大前端" 旌旗冲锋,老兵拖着node_modules残躯撤退。资本织机永不停歇,框架版本更迭如暴君换季,留下满地deprecated警告如秋后落叶。 其一...
    继续阅读 »

    表妹问:前端好玩吗?我说好玩,但表妹接下来的回复看哭了我。


    微信聊天图片 - 20250226100527.png


    是的,回复如下:


    这红海血途上,新兵举着 "大前端" 旌旗冲锋,老兵拖着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+) // 示例:虚拟...
    继续阅读 »

    Spring生态重大升级全景图


    Spring 6.0 + Boot 3.0 技术体系.png




    一、Spring 6.0核心特性详解


    1. Java版本基线升级



    • 最低JDK 17:全面拥抱Java模块化特性,优化现代JVM性能

    • 虚拟线程(Loom项目):轻量级线程支持高并发场景(需JDK 19+)


    // 示例:虚拟线程使用
    Thread.ofVirtual().name("my-virtual-thread").start(() -> {
    // 业务逻辑
    });




      1. 虚拟线程(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全量替换

    • 自动配置优化:更智能的条件装配策略



      1. 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



    三、升级实施路线图


    升级准备阶段.png


    四、新特性组合实战案例


    场景:电商平台升级


    // 商品查询服务(组合使用新特性)
    @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();
    }
    }



    四、升级实践建议



    1. 环境检查:确认JDK版本≥17,IDE支持Jakarta包名

    2. 渐进式迁移

      • 先升级Spring Boot 3.x → 再启用Spring 6特性

      • 使用spring-boot-properties-migrator检测配置变更



    3. 性能测试:对比GraalVM原生镜像与传统JAR包运行指标


    通过以上升级方案:



    1. 使用虚拟线程支撑万级并发查询

    2. 声明式客户端简化服务间调用

    3. ProblemDetail统一异常格式

    4. Prometheus监控接口性能




    本次升级标志着Spring生态正式进入云原生时代。重点关注:虚拟线程的资源管理策略、GraalVM的反射配置优化、OAuth2授权服务器的定制扩展等深度实践方向。


    作者:后端出路在何方
    来源:juejin.cn/post/7476389305881296934
    收起阅读 »

    如何优雅的回复面试官问:“你能接受加班吗?”

    面试官问:“你能接受加班吗?”我脑袋嗡的一声,余音绕梁三日不绝于耳。 那一刻,我简直觉得自己像被突然砸中脑袋,脑袋里嗡的一声,余音绕梁三日。作为一个职场小白,这种问题简直颠覆了我对面试的认知。于是,我一时心血来潮,脱口而出一句:“领导抗揍吗?” 结果,大家猜到...
    继续阅读 »

    面试官问:“你能接受加班吗?”我脑袋嗡的一声,余音绕梁三日不绝于耳。


    那一刻,我简直觉得自己像被突然砸中脑袋,脑袋里嗡的一声,余音绕梁三日。作为一个职场小白,这种问题简直颠覆了我对面试的认知。于是,我一时心血来潮,脱口而出一句:“领导抗揍吗?” 结果,大家猜到了,面试是上午结束的,Offer是当天中午凉的。


    是牛是马


    如何巧妙回答


    “我认为加班是工作中不可避免的一部分,尤其是在一些特殊项目或紧急情况下。我非常热爱我的工作,并且对公司的发展充满信心,因此我愿意为了团队的成功付出额外的努力。当然,我也注重工作效率和时间管理,尽量在正常工作时间内完成任务。如果确实需要加班,我也会根据公司合理的安排,积极的响应。”


    作为一名资深的面试官,今天面对这个问题,坐下来和大家聊聊应该怎么回答呢?面试官究竟喜欢怎样的回答?让我们深入分析一下。


    面试官的心理


    在职场中,想要出色地应对面试,需要具备敏锐的观察力和理解力。学会细致入微地观察,善于捕捉每一个细微的线索,这样才能在面试中游刃有余。懂的察言观色,方能尽显英雄本色。


    请在此添加图片描述


    面试官的考量点



    • 评估工作稳定性


    面试官提出“能否接受加班”的问题,旨在深入了解求职者的职业稳定性和对加班安排的适应性。这一评估有助于预测求职者入职后的表现和长期留任的可能性。工作稳定性是企业考量员工的关键指标之一,通过这一问题,面试官能够洞察求职者的职业发展规划及其对未来工作的期望。



    • 筛选合适的候选人


    通过询问加班的接受度,面试官筛选出那些愿意为达成工作目标而投入额外时间和精力的候选人。这种筛选方式有助于确保团队的整体运作效率和协作精神。合适的候选人不仅能快速融入团队,还能显著提升工作效率。因此,面试官借此问题寻找最匹配岗位需求的员工。



    • 了解求职者的价值观


    面试官还利用这个问题来探查求职者的价值观和工作态度,以此判断他们是否与公司的文化和核心价值观相契合。员工的价值观和态度对公司的长远发展起着至关重要的作用。通过这一询问,面试官能够确保求职者的个人目标与公司的发展方向保持一致,从而促进整体的和谐与进步。


    考察的问题的意义


    要理解问题的本质……为什么面试官会提出这样的问题?难道是因为你的颜值过高,引发了他的嫉妒?


    请在此添加图片描述



    • 工作态度


    面试官通过询问加班的接受度,旨在评估求职者是否展现出积极的工作态度和强烈的责任心。在许多行业中,加班已成为常态,面试官借此问题了解求职者是否愿意在工作上投入额外的时间和精力。积极的工作态度和责任心是职场成功的关键因素,通过这一问题,面试官能够初步判断求职者是否适应高强度的工作环境。



    • 岗位匹配度


    特定岗位因其工作性质可能需要频繁加班。面试官通过提出加班相关的问题,旨在了解求职者是否能适应这类岗位的工作强度。由于不同岗位对工作强度的要求各异,面试官希望通过这一问题确保求职者对即将承担的角色有明确的认识,从而防止入职后出现期望不一致的情况。



    • 抗压能力


    加班往往伴随压力,面试官通过这一问题考察求职者的抗压能力和情绪管理技巧。抗压能力对于职场成功至关重要,面试官借此了解求职者在高压环境下的表现,以判断其是否符合公司的需求。



    • 公司文化


    面试官还利用这个问题来评估求职者对公司加班文化的接受程度,以此判断其价值观是否与公司相符。公司文化对员工的工作体验和满意度有着深远的影响,面试官希望通过这一问题确保求职者能够认同并融入公司文化。


    回答的艺术


    “知己知彼,百战不殆。”在面试中,回答问题的关键在于展现出积极和正向的态度。


    请在此添加图片描述



    • 积极态度


    在回答有关加班的问题时,表达你对工作的热爱和对公司的忠诚,强调你愿意为了团队的成功而付出额外的努力。这种积极的态度不仅展示了你的职业素养和对工作的热情,还能显著提升面试官对你的好感。


    例如:“我非常热爱我的工作,并且对公司的发展充满信心。我相信为了实现公司的目标和团队的成功,适当的加班是可以接受的。”



    • 灵活性和效率


    强调你在时间管理和工作效率上的能力,表明你在确保工作质量的同时,会尽可能减少加班的需求。灵活性和效率是职场中极为重要的技能,面试官可以通过这个问题了解你的实际工作表现。


    例如:“我在工作中注重效率和时间管理,通常能够在规定的工作时间内完成任务。当然,如果有特殊情况需要加班,我也会全力以赴。”



    • 平衡工作与生活


    适当地提到你对工作与生活平衡的重视,并希望公司在安排加班时能够充分考虑到员工的个人需求。平衡工作与生活是职场人士普遍关注的问题,面试官通过这个问题可以了解你的个人需求和期望。


    例如:“我非常重视工作与生活的平衡,希望在保证工作效率的同时,也能有足够的时间陪伴家人和进行个人活动。如果公司能够合理安排加班时间,我会非常乐意配合。”



    • 适度反问


    在回答时,可以适当地向面试官询问关于公司加班的具体情况,以便更全面地了解公司的加班文化和预期。这样的反问可以展现你的主动性和对公司的兴趣,有助于获取更多信息,做出更加明智的回答。


    例如:“请问公司通常的加班情况是怎样的?是否有相关的加班补偿或调休安排?”


    最后


    所谓士为知己者死,遇良将则冲锋陷阵,择良人则共谋天下。在职场这场没有硝烟的战争中,我们每个人都是一名战士,寻找着属于自己的知己和良将。当面试官提出挑战性问题时,我们不仅要展示自己的能力和才华,更要表现出对工作的热爱和对公司的忠诚。


    面对“你能接受加班吗?”这样的问题,我们应以积极的态度、灵活的思维和对工作与生活平衡的重视来回应。这样的回答不仅能展示我们的职业素养,还能让我们在众多求职者中脱颖而出,赢得面试官的青睐。


    正如士为知己者死,我们在职场中也要找到那个能理解我们、支持我们的知己;遇良将则冲锋陷阵,我们要在优秀的领导下发挥自己的潜能,为公司的发展贡献力量;择良人则共谋天下,我们要与志同道合的同事共同努力,实现职业生涯的辉煌。


    总之一句话,在面试中展现出积极向上的形象,不仅能为我们的职业生涯加分,更能让我们在职场上找到属于自己的价值和归属感。让我们以这句话为指引,勇敢地迎接职场的挑战,书写属于自己的辉煌篇章。


    作者:不惑_
    来源:juejin.cn/post/7457211584709066792
    收起阅读 »

    DeepSeek R1 对编程语言未来的预测

    欢迎关注我的训练营:云原生AI实战营,一个助力 Go 开发者在 AI 时代建立技术竞争力的实战营。 今天分享下 DeepSeek 对于编程语言未来的预测,这个应该也是很多开发者关注的,因为它可能会影响未来的职业选择。 因我自己最近也在关注这方面,所以今天就问...
    继续阅读 »

    欢迎关注我的训练营:云原生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 取代。

    • 生态护城河:庞大的库和社区资源确保其长期不可替代性。


    推荐方向:机器学习工程、数据分析、快速原型开发。


    综合对比与趋势总结


    维度GoJavaRustPython
    增长潜力高(云原生)低(存量市场)极高(系统替代)高(AI 驱动)
    学习曲线中等中等偏高
    性能中等极高
    适用领域后端/基础设施企业系统/Android系统/安全/区块链AI/脚本/科研


    未来 5 年预测



    1. GoRust 将成为增长最快的语言,分别主导云原生和系统级开发。

    2. Python 因 AI 热潮保持霸主地位,但需警惕性能相关的替代方案。

    3. 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/我的星球/… 正文 不知道最近大家有没有发现,越来越多的人在职业选择上都偏向与央国企、体制内等稳定性较高的岗位,而放弃了去私企、互联网等工资高但是强度大的工作。 从...
    继续阅读 »


    浣熊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


    其实对于职业的选择,有一个核心逻辑就是去门槛更高的地方。


    有的人说,大厂门槛很高啊,问那么多技术,刷那么多题,也是万里挑一才能进去一个人。


    但是,实际上这些东西不算门槛,真正的门槛是把人堵在外面的不可逾越的鸿沟,比如说:如果你本科不是学的临床专业,那么你一辈子都没办法当上医生,除非重新高考!这才是真正意义上的门槛,而无论是多么高深的技术,只要肯学都能够学会的。


    所以,大型垄断央国企其实是个门槛更高的地方,好的岗位除了应届生就没有就会进去,同时一旦进去占住坑了也很难被裁掉,除非你自己离职。大家可能经常会听说哪个国企的用自己的业余时间努力学习然后去了互联网大厂的。但是你可能完全没有听过那个私企的毕业没去 "中国烟草" 靠着自己的不懈努力,社招进入了中国烟草。


    如果是应届生,尽量去门槛高、稳定的地方,考虑长期发展而不是贪图短时间的利益,这样一来即使你的能力没有那么强,也可以用马拉松的方式跑到最后。


    人生是一段长跑,不到最后一刻不知道谁输谁赢,就算是活得比别人长,那么其实你最后也胜利了。


    作者:浣熊say
    来源:juejin.cn/post/7343161077061992458
    收起阅读 »

    小红书创始人瞿芳,武汉人,北京外国语大学毕业,2013 年从外企离职,目前身价 120 亿

    大家好,我是二哥呀。 今天我们来聊聊小红书的创始人瞿芳,1985 年出生于湖北武汉,毕业于武汉外国语学校,硕士就读于北京外国语学校。 毕业后进入贝塔斯曼工作,这是一家老牌外企,1835 年就有了,真的非常老,瞿芳在这里工作了 5 年。 瞿芳的执行力也是拉满,2...
    继续阅读 »

    大家好,我是二哥呀。


    今天我们来聊聊小红书的创始人瞿芳,1985 年出生于湖北武汉,毕业于武汉外国语学校,硕士就读于北京外国语学校。


    毕业后进入贝塔斯曼工作,这是一家老牌外企,1835 年就有了,真的非常老,瞿芳在这里工作了 5 年。


    瞿芳的执行力也是拉满,2013 年 5 月底离职,6 月赴美寻找风投,7 月初就和老乡毛文超在上海创立了小红书的母公司行吟信息科技有限公司。


    长相上我觉得有一点邓丽君的感觉,大家觉得呢?



    • 2015-2016 年,瞿芳连续两年被《创业邦》评为“值得关注的女性创业者”;这些年小红书的成长,瞿芳确实功不可没,值得关注。

    • 2017 年,瞿芳荣登腾讯“我是创始人”荣耀榜单;小红书背后有阿里和腾讯两家大佬的投资,原来两家是从来不投一家公司的,瞿芳背后的斡旋算是让两家暂时握了手。

    • 2020 年,瞿芳入选“中国最具影响力的 30 位商界女性”榜单;目前来看,小红书还处在上升势头,并且流量拉满,瞿芳的身价肯定也会水涨船高。

    • 2024 年,瞿芳以 120 亿元的人民币财富位列《2024-胡润榜》的第 433 位;这还是在小红书没有上市的情况下。


    瞿芳曾在采访中强调,用户体验和社区氛围是小红书最看重的


    这也是小红书这个平台和微博、抖音最大的区别,你可能在小红书上百粉不到,但发布的内容却会被推荐到平台首页,成为爆款。


    微的推荐机制现在也有这种趋势,就是粉丝数越少,反而被推荐的机会越多。


    沉默王二 2024 年就有 3000 万次阅读


    瞿芳认为,品牌与用户的沟通应该从“教学模式”转向“恋爱模式”。


    也就是说,我们创作者不能再以老师的角度切入,把读者作为学生来传达信息,而是奔着双方恋爱的方式切入。


    更加的纯粹,双方的地位更加的对等。


    宝子们,都看到了吧,我爱你们,😄


    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

    二哥的 Java 进阶之路:JDK 默认垃圾收集器


    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年初开始找工作中间休息了三个月,找工作到入职花了一个多月,...
    继续阅读 »

    前言


    今天是新公司入职后的三个多月了个人也是如愿的转正了,对于我个人而言此时的心情好像没有三个月前刚拿到offer那样的喜悦和兴奋了,更像一件很普通的事情在你身边发生了吧。从2023年底离职在到2024年初开始找工作中间休息了三个月,找工作到入职花了一个多月,在这个过程中也是第一次真真切切感受到了所谓大环境低迷下的“前端已死的言论”,也给大家分享一下自己入职三个月的个人感受吧。


    从上一家公司离职时的个人感受


    因为上一家公司的工作性质是人力外包驻场开发,年底客户公司(中国移动成都产业研究院)我所在的项目组不需要外包人员了,个人也是被迫拿了赔偿灰溜溜的走人了。


    工作感受:对于这段工作经历我个人还是比较认可的,毕竟这里没有任何工作压力,也不加班,工作量少,有很多时间去学习新东西,做做自己的开源,认识了新的朋友等等。


    学历的重要性:在这里面随便拎一个人出来可能就是研究生学历的国企单位,自己真实的意识到了学历的重要性(第一学历小专科的我瑟瑟发抖)。


    和优秀的人共事:如果在一个长期压抑低沉消极的环境下工作无论你的性格在怎么积极乐观开朗,可能也很容易被影响到。相反如果是和在一群积极乐观开朗充满自信的环境和人一起工作,相信你也会变得积极,乐观,自信这或许也是我这一段工作经历最大的收获吧。


    2023年底找工作的市场就业环境


    抱着试一试的心态在boss上更新了自己的简历状态,不出所料软件上面安静的奇怪ps:49入国军的感觉已读未回可能是很失望的感觉吧,但年底找工作令人绝望的是大多数公司都是未读未回,这也就意味着年底基本上是没有正常公司招聘的了。


    大概投了两周简历后终于在智联招聘上约到了一个短期三个月岗位的面试,现场两轮面试通过了,不过最终还是没有选择去。


    原因有很多:



    1. 现场的工作环境个人感觉很压抑,从接待我前台和面试官都能感觉满脸写着疲惫

    2. 说公司最近在996,你也需要和我们一起

    3. 招聘岗位和工作内容是threejs开发,薪资却说只能给到普通前端开发的水平

    4. 人力外包公司hr的反复无常令我恶心,二面通过后hr给我打电话最主要就是聊薪资吧,电话内容也很简单hr:成都大部分前端的薪资是在XX-XX,可能给不到你想要的薪资,可能要往下压个1-2K。:我提的薪资完全是在你们发布招聘岗位薪资的区间,既然你们给不到为什么要这样写了(有感到被侮辱了)。过了几天之后人力外包的hr又给我电话,说可以在原来提的薪资基础上加1.4K,希望能早点去客户公司入职。


    总结:年底招聘的公司基本上没啥好鸟,如果你的经济能力还行的话让自己放松休息一段时间也是不错的选择


    2024年初找工作:真实的感受到了大环境的低迷下的市场行情


    印象最深刻的是在疫情时期的2021年,那会儿出来找工作boos上会有很多HR主动给你打招呼,一周大概能五六个面试,大专学历也有机会去自研公司


    解封之后本以为市场行情会变得回缓,结果大概就是今年可是未来十年行情最好的一年


    简单总结一下2024年的成都就业环境大概这样的:



    1. 只有外包公司会招专科学历

    2. boss上只给hr发一句打招呼的快捷语,99% 都是已读不回

    3. 大多数要完简历之后就没有后续了

    4. 待遇好的公司对于学历的要求更严格了(211,985)

    5. 给你主动打招呼的基本上都是人力外包公司


    截至入职新公司前boss上面的投递状况:沟通了1384个岗位,投递了99份简历,一共约到了 8 家公司的面试


    image.png


    今年找工作的个人感受:不怕面试,就怕没有面试机会


    首先说一下个人的一些情况吧,因为在创业小公司待过在技术栈方面个人认为算是比较全面的了


    项目经验:做过管理系统(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:



    1. 数组哪些方法会触发Vue监听,哪些不会触发监听

    2. position 有哪些属性

    3. vue watch和computed的区别,computed和method的区别

    4. vue的watch是否可以取消? 怎么取消?

    5. position:absolute, position:fixed那些会脱离文档流

    6. 如何获取到 pomise 多个then 之后的值

    7. 常见的http状态码

    8. 谈谈你对display:flex 弹性盒子属性的了解

    9. 如何判断一个值是否是数组

    10. typeof 和instanceof的区别

    11. es6-es10新增了那些东西

    12. 离职原因,期望薪资,职业规划


    公司B


    到现场写了一套笔试题,内容记不清楚了


    公司C



    1. vue router 和route 区别

    2. 说说重绘和重排

    3. css 权重

    4. 项目第一次加载太慢优化

    5. 谈谈你对vue这种框架理解

    6. sessionstorage cookie localstorage 区别

    7. 了解过.css 的优化吗?

    8. 闭包

    9. 内存泄漏的产生

    10. 做一个防重复点击你有哪些方案

    11. 解释一些防抖和节流以及如何实现

    12. 说一下你对 webScoket的了解,以及有哪些API

    13. 说一下你对pomise的理解

    14. vue2,vue3 中 v-for 和v-if的优先级

    15. 说说你对canvas的理解


    公司D


    笔试+面试



    1. vue 首屏加载过慢如何优化

    2. 说说你在项目中封装的组件,以及如何封装的

    3. 后台管理系统权限功能菜单和按钮权限如何实现的

    4. vue 中的一些项目优化

    5. 期望薪资,离职原因,

    6. 其他的记不清楚了


    公司E


    笔试+面试+和老板谈薪资


    1.笔试:八股文


    2.面试:主要聊的是项目内容比如项目的一些功能点的实现,和项目的技术点


    3.老板谈薪资:首先就是非技术面的常规三件套(离职原因,期望薪资,职业规划),然后就是谈薪资(最终因为薪资给的太低了没有选择考虑这家)


    公司F


    也是最想去的一家公司,一个偏管理的前端岗位(和面试官聊的非常投缘,而且整个一面过程也非常愉快感受到了十分被尊重)


    可惜的是复试的时候因为学历原因,以及一些职业规划和加班出差等方面上没有达到公司的预期也是很遗憾的错过了


    一面:



    1. vue 响应式数据原理

    2. 说说es6 promise async await 以及 promise A+规范的了解

    3. 谈谈es6 Map 函数

    4. 如何实现 list 数据结构转 tree结构

    5. webScoke api 介绍

    6. webScoke 在vue项目中如何全局挂载

    7. vuex 和 pinia 区别

    8. 谈谈你对微任务和宏任务的了解

    9. call apply bind 区别

    10. 前端本地数据存储方式有哪些

    11. 数组方法 reduce 的使用场景

    12. 说说你对 css3 display:flex 弹性盒模型 的理解

    13. vue template 中 {{}} 为什么能够被执行

    14. threejs 加载大模型有没有什么优化方案

    15. 离职原因,住的地方离公司有多远,期望薪资

    16. 你有什么想需要了解的,这个岗位平时的工作内容


    二面:


    1.我看写过一个Express+Mongoose服务端接口的开源项目,说说你在写后端项目时遇到过的难点


    2.介绍一下你写的threejs 3d模型可视化编辑器 这个项目


    3.以你的观点说一下你对three.js的了解,以及three.js在前端开发中发挥的作用


    4.现在的AI工具都很流行,你有没有使用过AI工具来提高你对开发效率


    5.说说你认为AI工具对你工作最有帮助的地方是哪些


    6.以你的观点谈谈你对AI的看法,以及AI未来发展的趋势


    7.你能接受出差时间是多久


    8.你是从去年离职的到今天这这几个月时间,你是去了其他公司只是没有写在简历上吗?


    9.说说你的职业规划,离职原因,你的优点和缺点,平时的学习方式


    公司G


    一共两轮面试,也是最终拿到正式offer入职的公司


    一面:



    1. 主要就是聊了一下简历上写的项目

    2. 项目的技术难点

    3. 项目从0-1搭建的过程

    4. 项目组件封装的过程

    5. vue2 和 vue3 区别

    6. vue响应式数据原理

    7. 对于typescript的熟练程度

    8. 会react吗? 有考虑学习react吗?

    9. 说一下你这个three.js3d模型可视化编辑器项目的一个实现思路,为什么会想写这样一个项目


    二面:



    1. 说说了解的es6-es10的东西有哪些

    2. 说说你对微任务和宏任务的了解

    3. 什么是原型链

    4. 什么是闭包,闭包产生的方式有哪些

    5. vue3 生命周期变化

    6. vue3 响应式数据原理

    7. ref 和 reactive 你觉得在项目中使用那个更合适

    8. 前端跨越方式有哪些

    9. 经常用的搜索工具有哪些?

    10. 谷歌搜索在国内能使用吗?你一般用的翻墙工具是哪种?

    11. 用过ChatGPT工具吗? 有付费使用过吗?

    12. 你是如何看待面试造航母工作拧螺丝螺丝的?

    13. 谈谈你对加班的看法?

    14. 你不能接受的加班方式是什么?

    15. 为什么会选择自考本科?

    16. 你平时的学习方式是什么?

    17. 一般翻墙去外网都会干什么?,外网学习对你的帮助大吗?

    18. 上一家公司的离职原因是什么,期望薪资是多少, 说说你的职业规划

    19. 手里有几个offer?


    hr电话:



    1. 大概说了一下面试结果通过了

    2. 然后就是介绍了一下公司的待遇和薪资情况?

    3. 问了一下上一家公司的离职原因以及上一家公司的规模情况?

    4. 手里有几个offer?

    5. 多久能入职?


    因为后面没有别的面试了,再加上离职到在到找工作拿到offer已经有四个月时间没有上班了,最终选择了入职这家公司


    image.png


    入职第三天:我想跑路了!


    入职后的第一天,先是装了一下本地电脑环境然后拉了一下项目代码熟悉一下,vue3,react,uniapp 项目都有


    崩溃的开始:PC端是一个saas 系统由五个前端项目构成,用的是react umi 的微前端项目来搭建的,也是第一次去接触微前端这种技术栈,要命的是这些项目没有一个是写了readme文档的,项目如何启动以及node.js版本这些只能自己去package.json 文件去查看,在经过一番折腾后终于是把这几个项目给成功跑起来了,当天晚上回家也是专门了解了一下微前端


    开始上强度: 入职的第二天被安排做了一个小需求,功能很简单就是改个小功能加一下字段,但是涉及的项目很多,pc端两个项目,小程序两个项目。在改完PC端之后,开始启动小程序项目不出所料又是一堆报错,最终在别的前端同事帮助下终于把小程序项目给启动成功了。


    人和代码有一个能跑就行:入职的第三天也从别的同事那里了解到了,之前sass项目组被前端大规模裁员过,原因嘛懂得都懂? 能写出这样一堆屎山代码的人,能不被裁吗?


    第一次知道 vue 还可以这样写


    image.png


    对于一个有代码强迫症的人来说,在以后的很长一段时间里要求优化和接触完全是一堆屎山一样代码,真的是很难接受的


    入职一个月:赚钱嘛不寒掺


    在有了想跑路的想法过后,也开始利用上班的空余时间又去投递简历了,不过现实就是在金三银四的招聘季,boss上面依旧是安静的可怕,在退一步想可能其他公司的情况也和这边差不多,于是最终还是选择接受了现实,毕竟赚钱嘛不寒掺


    入职两个月:做完一个项目迭代过后,感觉好多了


    在入职的前一个月里,基本上每天都要加班,原因也很简单:


    1.全是屎山的项目想要做扩展新功能是非常困难的


    2.整个项目的逻辑还是很多很复杂的只能边写项目边熟悉


    3.因为裁了很多前端,新人还没招到,但是业务量没有减少只能加班消化


    功能上线的晚上,加班到凌晨3点


    image.png


    在开发完一个项目迭代过后也对项目有了一些大概的了解,之后的一些开发工作也变得简单了许多


    入职三个月:工作氛围还是很重要滴


    在入职三个月后,前端组团队的成员也基本上是组建完成了,一共14人,saas项目组有四个前端,虽然业务量依然很多但是好在有更多的人一起分担了,每周的加班时间也渐渐变少了


    在一次偶然间了解到我平时也喜欢打篮球后,我和公司后端组,产品组的同事之间也开始变得有话题了,因为大家也喜欢打球,后来还拉了一个篮球群周末有时间大家也会约出来一起打打球


    image.png


    image.png


    当你有存在价值后一切的人情世故和人际关系都会变得简单起来


    在这个世界上大多数时候除了你的父母等直系亲属和另一半,可能会对你无条件的付出


    其余任何人对你尊重和示好,可能都会存在等价的利益交换吧


    尤其是在技术研发的岗位,只有当你能够完全胜任这份工作时,并且能够体现出足够的价值时才能够有足够的话语权


    入职三个月后的感受



    1. 公司待遇:虽然是一个集团下面的子公司 (200+人)但待遇只能说一般吧,除了工资是我期望的薪资范围,其他的福利待遇都只能是很一般(私企嘛,懂得都懂)

    2. 工作强度: 听到过很多从大厂来的新同事抱怨说这边的工作量会很大,对我来说其实都还ok,毕竟之前在极端的高压环境下工作过

    3. 工作氛围:从我的角度来讲的话,还是很不错的,相处起来也很轻松简单,大家也有很多共同话题,没有之前在小公司上班那么累


    大环境低迷下,随时做好被裁掉的准备


    从2020年毕业工作以来,最长的一段工作经历是1年4个月,有过三家公司的经历


    裁员原因也很简单:创业小公司和人力外包,要么就是小公司经营问题公司直接垮掉,或者就是人力外包公司卸磨杀驴


    除非你是在国企单位上班,否则需要随时做好被裁掉的准备


    什么都不怕,就怕太安逸了


    这句话出自《我的团长我的团》电视剧里面龙文章故意对几十个过江的日本人围而不歼时和虞啸卿的对话,龙文章想通过这几十个日本人将禅达搅得鸡犬不宁,来唤醒还在沉睡在自己温柔乡的我们,因为就在我们放松警惕时日本人就已经将枪口和大炮对准了我们。


    或许大家会很不认同这句话吧,如果你的父母给你攒下了足够的资本完全可以把我刚才的话当做放屁,毕竟没有哪一个男生毕业之前的梦想是车子和房子,从事自己喜欢的工作不好吗? 但现实却是你喜欢工作的收入很难让你在这座城市里体面的生活


    于我而言前端行业的热爱更多是因为能够给我带来不错的收入所以我选择了热爱它吧,所以保持终身学习的状态也是我需要去做的吧


    前端已死?


    前端彻底死掉肯定是不会的,在前后端分离模式下的软件开发前端肯定是必不可少的一个岗位,只不过就业环境恶劣下的情况里肯定会淘汰掉很多人,不过35岁之后我还是否能够从事前端行业真的是一个未知数


    结语


    选择或者躺平,只是两种不同的生活态度没有对与错,偶尔躺累了起来卷一下也是可以的,偶尔卷累了躺起了休息一下也是不错的。


    在这个网络上到处是人均年收入百万以及各种高质量生活的时代,保持独立思考,如何让自己不被负面情绪所影响才是最重要的吧


    作者:答案answer
    来源:juejin.cn/post/7391065678546157577
    收起阅读 »

    裸辞后,我活得像个废物,但我终于开始活自己

    哈喽,大家好!我是Web大鹅只会叫!下去沉淀了那么久,终于有时间回来给大家写点什么了! 你问我人为什么活着?我哪知道啊?我又不是上帝!释迦牟尼为了解这个问题还跑去出家,结果发现人活着就是为了涅槃——也就是死。所以别问我人生的意义,我也没搞明白!不过我能告诉你...
    继续阅读 »

    哈喽,大家好!我是Web大鹅只会叫!下去沉淀了那么久,终于有时间回来给大家写点什么了!


    image.png


    你问我人为什么活着?我哪知道啊?我又不是上帝!释迦牟尼为了解这个问题还跑去出家,结果发现人活着就是为了涅槃——也就是死。所以别问我人生的意义,我也没搞明白!不过我能告诉你一个答案,那就是裸辞后,我终于知道了为什么要活着——那就是为了“活得自由”!


    裸辞后,那些走过的路,和你说的“脏话”


    2024年8月,我做了一个震惊所有人的决定——裸辞!是的,没错,我就是那种毫不犹豫地辞了职、丢下稳定收入和安稳生活,拿着背包去走四方的“疯子”。放下了每天早起20公里开车上班的压力,放下了无聊的加班、枯燥的开会,放下了所谓的“你要努力争取美好生活”的叮嘱。一切都在“离开”这一刻轻轻拂去,带着一种挥之不去的自由感。


    带着亲人的责怪、朋友的疑问、同事的眼神、以及自己满满的疑惑,我开始了这段没有目的的旅行。我不知道我想找什么,但我知道,我不想再活得像以前那样。


    我走过了无数地方,南京、湖州、宁波、杭州、义乌、金华、嘉兴、镇江、扬州、苏州、无锡、上海……一路走来,路过了每个风景,每个城市,每个人生。我甚至去了中国最北的漠河,站在寒风凛冽的雪地里,终于明白了一个道理:“你活着,才是最值得庆祝的事。


    你知道吗,最让人清醒的,不是远方的景色,而是走出去之后,终于能脱离了那一套“你该做什么”的公式。每天不用设闹钟,不用准时吃饭,不用打卡上班,不用开会骂娘,再也不被地铁里的拥挤挤得喘不过气。生活突然变得宽松,我竟然开始意识到:我一直追求的美好生活,原来只是在为别人拼命。


    走出内卷圈子的那一刻,我认为我是世界上最快乐的小孩了,我们渴望着幸福却糟践着现在,你所期望的美好未来却始终都是下一个阶段!你认为你是一个努力拼搏美好未来的人。可是现实比理想残酷的多!你没日没夜的拼搏,却让别人没日没夜的享受!你用尽自己半条命,换来的是下半辈子的病!我在裸辞后就告诉我自己:从今以后你想干什么、就干什么!你就是世界的主人! 嗯~ 爽多了!


    走过的路,都在暗示我


    我在大兴安岭漠河市的市里住了5天,住在一个一天40元的宾馆、干净、暖和!老板是一个退休的铁道工人。脸和双手都布满了冻伤,他的妻子(大姨)很面善。每天都会在我回来的时候和我聊上几句从前,安排一些极寒天气的注意事项。


    有一天我去北极村回来,大姨和我聊了一会。大姨对我讲:“趁年轻、别把自己困起来,出去走走。不像我们,60年没出过这片土地,到头还要葬在这片土地上!”。她说这句话的时候没有忧虑、没有悲伤,却是一种满足感。是啊!60多了,还能追求什么?忙了大半辈子,把孩子都送出了这片土地,自己也没有激情出去走走了,很害怕自己的以后也是这样。


    我20多岁的年纪,想的不是努力拼搏挣钱、不是搞事业。却总想着无所事事。我觉得自己像一个没有完全“被时间遗弃”的人,我甚至觉得自己不属于这个时代,这个不知道为了什么而拼命的时代。每走一步都好像在掏空自己积压已久的情绪:压力、焦虑、焦灼,让我很享受这种感觉。然后我想起来一本书里的话:“你活着,不是为了活给别人看。“ 是啊,我们都明白这个大道理,可自己从来没打算让自己脱离这个主线。我开始明白,我这次的旅行不是去寻找什么,而是放下什么!


    从别人嘴里听到的“脏话”,其实是自己内心的尖刺


    这段时间里,我经常回想起来那些让我神经紧绷的日子。尤其是我对“人” 这个物种越来越敏感的那个时期————‘恶毒、自私、无理、肮脏’。朋友的欺骗、同事的推锅、亲人的折磨都是罪恶的!可是到头来,事情还是发生了。地球还是在自转,太阳一样正常升起落下。这些都没有在你认为的这些琐事中消失。我不明白我还在纠结什么?


    事实上,这些乱七八糟的事情并不是指向我个人的,它只是我内心脆弱的反射。是的,我一直在内耗自己罢了,把自己放在了一个焦虑的漩涡里。假装没事、假装坚强,结果别人一句话就能作为击垮我的最后一击。直到有一天,我发现我讨厌的只是我自己。所以我决定我不要去在意别人说什么、做什么,我不要逃避问题,我想听听我内心的想法,我不想让自己认为别人在定义我。


    过程的意义:也许就是为了“停一停”


    好了,我知道我的文采不好,但是也应该有个结尾。


    在这一路上,我认识了很多有趣的人,他们不同风格的服装、不同口音,各式各样的生活方式。也有着各式各样的理想和困惑。有的喜欢在山顶等着日出的奇迹,有的则是想在湖边静静地坐着。而我,就是个迷途的羔羊,没有群体头羊的带领,我穿行在这些不同的路途中,慢慢摸索着向所有方向前进着。


    偶尔我也会停下来,坐在湖边吹着风、闭上眼睛,听风,感受这一刻的宁静。然后我会微笑,我认为这个时候的我有了轻松的感觉。生活的答案我在这个时候找到了。


    我意识到,未来不是重要的,现在才是应该享受的。我不知道我下一步要去哪里,但是我想先停下来看一看,呼吸一下。停下来不是因为我没有了目的,而是我知道,目的地并不重要,重要的是,我和自己在一起,心里不再有那么多焦虑,不再被过去的焦虑所束缚。


    所以,我选择了离开,离开这一切,放下所有的焦虑和期待,享受我自己想要的生活。也许,活着的意义不在于追寻一个遥远的目标,而是过好每一个‘现在’。


    作者:Web大鹅只会叫
    来源:juejin.cn/post/7454064311079813132
    收起阅读 »

    好人难当,坏人不做

    好人难当,以后要多注意了,涨点记性。记录三件事情证明下: 1. 免费劳动 之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都...
    继续阅读 »

    好人难当,以后要多注意了,涨点记性。记录三件事情证明下:


    1. 免费劳动


    之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都一年多过去了,还让我免费帮他改需求,我就说没时间,他说没时间的话可以把源码给他,他自己学着修改,我就直接把源码给他了,这个项目辛苦了一个多月,钱一毛也没赚到,我倒是搭进去一台服务器,一年花了三百多吧。现在源码给他就给他了吧,毕竟同学一场。没想到又过了半年,前段时间又找我来改需求了。这个项目他们家自己拿着赚钱,又不给我一毛钱,我相当于免费给他家做了个软件,还要出服务器钱,还要免费进行维护。我的时间是真不值钱啊,真成义务劳动了。我拒绝了,理由是忙,没时间。


    总结一下,这些人总会觉得别人帮自己是理所当然的,各种得寸进尺。


    2. 帮到底吧


    因为我进行了仲裁,有了经验,然后被一个人加了好友,是一个前同事(就是我仲裁的那家公司),然后这哥们各种问题我都尽心回答,本着能帮别人一点就帮别人一点的想法,但是我免费帮他,他仲裁到手多少钱,又不会给我一毛钱。这哥们一个问题接一个,我都做了回答,后来直接要求用我当做和公司谈判的筹码,我严词拒绝了,真不知道这人咋想的,我帮你并没有获得任何好处,你这个要求有点过分了,很简单,他直接把我搬出来和公司谈判,公司肯定会找我,会给我带来麻烦,这人一点也没想这些事。所以之后他再询问有关任何我的经验,我已经不愿意帮他了。


    总结一下,这些人更进一步,甚至想利用下帮自己的人,不考虑会给别人带来哪些困扰。


    3. 拿你顶缸


    最近做了通过一个亲戚接了一个项目,而这个亲戚的表姐是该项目公司的领导,本来觉得都是有亲戚关系的,项目价格之类开始问了,他们没说,只是说根据每个人的工时进行估价,后面我们每个人提交了个人报价,然后还是一直没给明确答复,本着是亲戚的关系,觉得肯定不会坑我。就一直做下去了,直到快做完了,价格还是没有出来,我就直接问了这个价格的事情,第二天,价格出来了,在我报价基础上直接砍半。我当然不愿意了,后来经过各种谈判,我终于要到了一个勉强可以的价格,期间群里谈判也是我一个人在说话,团队的其他人都不说话。后来前端的那人问我价格,我也把过程都实话说了,这哥们也要加价,然后就各种问我,我也啥都告他了。后来这个前端在那个公司领导(亲戚表姐)主动亮明身份,她知道这个前端和那个亲戚关系好,然后这个前端立马不好意思加价了,并且还把锅甩我头上,说是我没有告诉他她是他姐。还说我不地道,我靠,你自己要加价,关我啥事,你加钱也没说要分我啊,另外我给自己加价的时候你也没帮忙说话啊,我告诉你我加价成功了是我好心,也是想着你能加点就加点吧,这时候你为了面子不加了,然后说成要加价的理由是因为我,真是没良心啊。后面还问我关于合同的事情,我已经不愿意回答他了,让他自己找对面公司问去。


    总结一下,这些人你帮他了他当时倒是很感谢你,但是一旦结果有变,会直接怪罪到你头上。


    4. 附录文章


    这个文章说得挺好的《你的善良,要有锋芒》


    你有没有发现,善良的人,心都很软,他们不好意思拒绝别人,哪怕为难了自己,也要想办法帮助身边的人。善良的人,心都很细,他们总是照顾着别人的情绪,明明受委屈的是自己,却第一时间想着别人会不会难过。


    也许是习惯了对别人好,你常常会忽略自己的感受。有时候你知道别人是想占你便宜,你也知道别人不是真心把你当朋友,他们只是觉得你好说话,只是看中了你的善良,但是你没有戳穿,你还是能帮就帮,没有太多怨言。


    你说你不想得罪人,你说你害怕被孤立,可是有人在乎过你吗?


    这个世界上形形色色的人很多,有人喜欢你,有人讨厌你,你没有办法做到对每一个人好,也没办法要求每一个人都是真心爱你。所以你要有自己的选择,与舒服的人相处,对讨厌的人远离,过你自己觉得开心自在的生活就好,没必要为了便利别人,让自己受尽委屈。


    看过一段话:善良是很珍贵的,但善良要是没有长出牙齿来,那就是软弱。


    你的善良,要有锋芒,不要把时间浪费在不值得的人身上。对爱你的人,倾心相助,对利用你的人,勇敢说不。


    愿你的善良,能被真心的人温柔以待。


    作者:一线大码
    来源:juejin.cn/post/7455667125798780980
    收起阅读 »

    让闲置 Ubuntu 服务器华丽转身为家庭影院

    让闲置 Ubuntu 服务器华丽转身为家庭影院在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。...
    继续阅读 »

    让闲置 Ubuntu 服务器华丽转身为家庭影院

    在数字化的时代,家里的设备更新换代频繁,很容易就会有闲置的服务器吃灰。我家里就有一台闲置的 Ubuntu 24.04 服务器,一直放在角落,总觉得有些浪费。于是,我决定让它重新发挥作用,打造一个属于自己的家庭影院。

    一、实现 Windows 与 Ubuntu 服务器文件互通

    要打造家庭影院,首先得让本地 Windows 电脑和 Ubuntu 服务器之间能够方便地传输电影文件。我选择安装 Samba 来实现这一目的。

    1. 安装 Samba:在 Ubuntu 服务器的终端中输入命令

      sudo apt-get install samba samba-common

      系统会自动下载并安装 Samba 相关的软件包。

    2. 备份配置文件:为了以防万一,我先将原来的 Samba 配置文件进行备份,执行命令

      mv /etc/samba/smb.conf /etc/samba/smb.conf.bak
    3. 新建配置文件:使用 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 ,如果大家要部署的话记得根据自己的实际情况修改为对应的位置。 

    1. 连接 Windows 电脑:在 Windows 电脑这边基本不需要什么复杂配置,因为在网络里无法直接看到 Ubuntu,我直接在电脑上添加了网络位置。假设服务器地址是 192.168.10.100,那么添加网络位置就是 \\192.168.10.100\nas,这样就可以在 Windows 电脑和 Ubuntu 服务器之间传输文件了。

    二、安装 Jellyfin 搭建家庭影院

    文件传输的问题解决后,接下来就是安装 Jellyfin 来实现家庭影院的功能了。

    1. 尝试 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。

    1. 安装 server 版本的 Jellyfin:在终端中输入命令 curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash,安装过程非常顺利。

    1. 配置 Jellyfin:安装完成后,通过浏览器访问 http://192.168.10.100:8096 进入配置页面。在添加媒体库这里,我遇到了一个麻烦,网页只能选择到 /home/bddxg 目录,无法继续往下选择到我的媒体库位置 /home/bddxg/nas。于是我向 deepseek 求助,它告诉我需要执行命令:

      sudo usermod -aG bddxg jellyfin
      # 并且重启 Jellyfin 服务
      sudo systemctl restart jellyfin

      按照它的建议操作后,我刷新了网页,重新配置了 Jellyfin,终于可以正常添加媒体库了。

    2. 电视端播放:在电视上安装好 Jellyfin apk 客户端后,现在终于可以正常读取 Ubuntu 服务器上的影视资源了,坐在沙发上,享受着大屏观影的乐趣,这种感觉真的太棒了!

     通过这次折腾,我成功地让闲置的 Ubuntu 服务器重新焕发生机,变成了一个功能强大的家庭影院。希望我的经验能够对大家有所帮助,也欢迎大家一起交流更多关于服务器利用和家庭影院搭建的经验。

    [!WARNING] 令人遗憾的是,目前 jellyfin 似乎不支持rmvb 格式的影片, 下载资源的时候注意影片格式,推荐直接下载 mp4 格式的资源


    本次使用到的软件名称和版本如下:

    软件名版本号安装命令
    sambaVersion 4.19.5-Ubuntusudo apt-get install samba samba-common
    jellyfinJellyfin.Server 10.10.6.0curl https://repo.jellyfin.org/install-debuntu.sh | sudo bash
    ffmpeg(jellyfin 内自带)ffmpeg version 7.0.2-Jellyfinnull

    作者:冰冻大西瓜
    来源:juejin.cn/post/7476614823883833382

    收起阅读 »

    Mybatis接口方法参数不加@Param,照样流畅取值

    在 MyBatis 中,如果 Mapper 接口的方法有多个参数,但没有使用 @Param 注解,默认情况下,MyBatis 会将这些参数放入一个 Map 中,键名为 param1、param2 等,或者使用索引 0、1 等来访问。以下是具体的使用方法和注意事...
    继续阅读 »

    在 MyBatis 中,如果 Mapper 接口的方法有多个参数,但没有使用 @Param 注解,默认情况下,MyBatis 会将这些参数放入一个 Map 中,键名为 param1param2 等,或者使用索引 01 等来访问。以下是具体的使用方法和注意事项。




    一、Mapper 接口方法


    假设有一个 Mapper 接口方法,包含多个参数但没有使用 @Param 注解:


    public interface UserMapper {
    User selectUserByNameAndAge(String name, int age);
    }



    二、XML 文件中的参数引用


    在 XML 文件中,可以通过以下方式引用参数:


    1. 使用 param1param2 等


    MyBatis 会自动为参数生成键名 param1param2 等:


    <select id="selectUserByNameAndAge" resultType="User">
    SELECT * FROM user WHERE name = #{param1} AND age = #{param2}
    </select>

    2. 使用索引 01 等


    也可以通过索引 01 等来引用参数:


    <select id="selectUserByNameAndAge" resultType="User">
    SELECT * FROM user WHERE name = #{0} AND age = #{1}
    </select>



    三、注意事项



    1. 可读性问题



      • 使用 param1param2 或索引 01 的方式可读性较差,容易混淆。

      • 建议使用 @Param 注解明确参数名称。



    2. 参数顺序问题



      • 如果参数顺序发生变化,XML 文件中的引用也需要同步修改,容易出错。



    3. 推荐使用 @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 注解,可以通过 param1param2 或索引 01 等方式引用参数。

    • 这种方式可读性较差,容易出错,推荐使用 @Param 注解明确参数名称。

    • 使用 @Param 注解后,XML 文件中的参数引用会更加清晰和易于维护。


    作者:码农liuxin
    来源:juejin.cn/post/7475643579781333029
    收起阅读 »

    Java web后端转Java游戏后端

    作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程: 一、游戏后端核心职责 实时通信管理 采用WebSocket/...
    继续阅读 »

    作为Java后端开发者转向游戏后端开发,虽然核心编程能力相通,但游戏开发在架构设计、协议选择、实时性处理等方面有显著差异。以下从实际工作流程角度详细说明游戏后端开发的核心要点及前后端协作流程:




    一、游戏后端核心职责



    1. 实时通信管理



      • 采用WebSocket/TCP长连接(90%以上MMO游戏选择)

      • 使用Netty/Mina框架处理高并发连接(单机支撑5W+连接是基本要求)

      • 心跳机制设计(15-30秒间隔,检测断线)



    2. 游戏逻辑处理



      • 战斗计算(需在50ms内完成复杂技能伤害计算)

      • 状态同步(通过Delta同步优化带宽,减少60%数据传输量)

      • 定时器管理(Quartz/时间轮算法处理活动开启等)



    3. 数据持久化



      • 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周)



    1. 网络层实现


      // 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;
      }
      }
      }


    2. AOI(Area of Interest)管理



      • 九宫格算法实现视野同步

      • 动态调整同步频率(近距离玩家100ms/次,远距离500ms/次)



    3. 战斗系统



      • 采用确定性帧同步(Lockstep)

      • 使用FixedPoint替代浮点数运算保证一致性






    三、前后端协作关键点



    1. 协议版本控制



      • 强制版本校验:每个消息头包含协议版本号


      {
      "ver": "1.2.3",
      "cmd": 1001,
      "data": {...}
      }


    2. 调试工具链建设



      • 开发GM指令系统:


      /debug latency 200  // 模拟200ms延迟
      /simulate 5000 // 生成5000个机器人


    3. 联调流程



      • 使用Wireshark抓包分析时序问题

      • Unity引擎侧实现协议回放功能

      • 自动化测试覆盖率要求:

        • 基础协议:100%

        • 战斗用例:>85%








    四、性能优化实践



    1. JVM层面



      • G1GC参数优化:


      -XX:+UseG1GC -XX:MaxGCPauseMillis=50 
      -XX:InitiatingHeapOccupancyPercent=35


    2. 网络优化



      • 启用Snappy压缩协议(降低30%流量)

      • 合并小包(Nagle算法+50ms合并窗口)



    3. 数据库优化



      • 玩家数据冷热分离:

        • 热数据:位置、状态(Redis)

        • 冷数据:成就、日志(MySQL)








    五、上线后运维



    1. 监控体系



      • 关键指标报警阈值设置:

        • 单服延迟:>200ms

        • 消息队列积压:>1000

        • CPU使用率:>70%持续5分钟





    2. 紧急处理预案



      • 自动扩容规则:
        if conn_count > 40000:
        spin_up_new_instance()
        if qps > 5000:
        enable_rate_limiter()







    六、常见问题解决方案


    问题场景:战斗不同步

    排查步骤



    1. 对比客户端帧日志与服务端校验日志

    2. 检查确定性随机数种子一致性

    3. 验证物理引擎的FixedUpdate时序


    问题场景:登录排队

    优化方案



    1. 令牌桶限流算法控制进入速度

    2. 预计等待时间动态计算:
      wait_time = current_queue_size * avg_process_time / available_instances



    通过以上流程,Java后端开发者可逐步掌握游戏开发特性,重点需要转变的思维模式包括:从请求响应模式到实时状态同步、从CRUD主导到复杂逻辑计算、从分钟级延迟到毫秒级响应的要求。建议从简单的棋牌类游戏入手,逐步过渡到大型实时游戏开发。


    作者:加瓦点灯
    来源:juejin.cn/post/7475292103146684479
    收起阅读 »

    这个中国亲戚关系计算器让你告别“社死”

    web
    大家好,我是 Java陈序员。 由于为了生活奔波,常年在外,导致很多关系稍疏远的亲戚之间来往并不多。 因此节假日回家时,往往会搞不清楚哪位亲戚应该喊什么称呼,很容易“社死”。 今天给大家介绍一个亲戚关系计算器,让你快速的计算出正确的亲戚称谓! 关注微信公众号...
    继续阅读 »

    大家好,我是 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/#/


    大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



    作者:Java陈序员
    来源:juejin.cn/post/7344573753538330678
    收起阅读 »

    实现抖音 “视频无限滑动“效果

    web
    前言 在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅 不禁感叹道 "垃圾抖音,费我时间,毁我青春😅" 这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满 第一篇:200行代码...
    继续阅读 »

    前言


    在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅

    不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"




    这是我的 模仿抖音 系列文章的第二篇,本文将一步步实现抖音首页 视频无限滑动 的效果,干货满满



    第一篇:200行代码实现类似Swiper.js的轮播组件

    第三篇:Vue 路由使用介绍以及添加转场动画

    第四篇:Vue 有条件路由缓存,就像传统新闻网站一样

    第五篇:Github Actions 部署 Pages、同步到 Gitee、翻译 README 、 打包 docker 镜像



    如果您对滑动原理不太熟悉,推荐先看我的这篇文章:200行代码实现类似Swiper.js的轮播组件


    最终效果


    在线预览:dy.ttentau.top/


    Github地址:github.com/zyronon/dou…


    源码:SlideVerticalInfinite.vue


    实现原理


    无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList 里面永远只有 NSlideItem,就要在滑动时不断的删除和增加 SlideItem

    滑动时调整 SlideList 的偏移量 translateY 的值,以及列表里那几个 SlideItemtop 值,就可以了


    为什么要调整 SlideList 的偏移量 translateY 的值同时还要调整 SlideItemtop 值呢?

    因为 translateY 只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY 值就可以了,上滑了几页就减几页的高度,下滑同理


    但是如果整个列表向前移动了一页,同时前面的 SlideItem 也少了一个,,那么最终效果就是移动了两页...因为 塌陷 了一页

    这显然不是我们想要的,所以我们还需要同时调整 SlideItemtop 值,加上前面少的 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


    有两种方式,各有利弊



    • Vuerender 方法

      • 优点:只是渲染一个 VNode 而已,理论上讲内存消耗更少。

      • 缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅



    • VuecreateApp 方法再创建一个 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:让你的图片和背景融为一体,绝了!

    web
    需求 先来看这样一个场景,拿一个网站举例 这里有一个常见的网站 banner 图容器,大小为为1910*560,看起来背景图完美的充满了宽度,但是图片原始大小时,却是: 它的宽度只有 1440,且 background-size 设置的是 contain ...
    继续阅读 »

    需求


    先来看这样一个场景,拿一个网站举例


    image.png


    这里有一个常见的网站 banner 图容器,大小为为1910*560,看起来背景图完美的充满了宽度,但是图片原始大小时,却是:


    image.png


    它的宽度只有 1440,且 background-size 设置的是 contain ,即等比例缩放,那么可以断定它两边的蓝色是依靠背景色填充的。


    那么问题来了,这是一个 轮播banner,如果希望添加一张不是蓝色的图片呢?难道要给每张图片提前标注好背景颜色吗?这显然是非常死板的做法。


    所以需要从图片中提取到图片的主题色,当然这对于 js 来说,也不是什么难事,市面上已经有众多的开源库供我们使用。


    探索


    首先在网络上找到了以下几个库:



    • color-thief 这是一款基于 JavaScript 和 Canvas 的工具,能够从图像中提取主要颜色或代表性的调色板

    • vibrant.js 该插件是 Android 支持库中 Palette 类的 JavaScript 版本,可以从图像中提取突出的颜色

    • rgbaster.js 这是一段小型脚本,可以获取图片的主色、次色等信息,方便实现一些精彩的 Web 交互效果


    我取最轻量化的 rgbaster.js(此库非常搞笑,用TS编写,npm 包却没有指定 types) 来测试后发现,它给我在一个渐变色图片中,返回了七万多个色值,当然,它准确的提取出了面积最大的色值,但是这个色值不是图片边缘的颜色,导致设置为背景色后,并不能完美的融合。


    另外的插件各位可以参考这几篇文章:



    可以发现,这些插件主要功能就是取色,并没有考虑实际的应用场景,对于一个图片颜色分析工具来说,他们做的很到位,但是在大多数场景中,他们往往是不适用的。


    在文章 2 中,作者对比了三款插件对于图片容器背景色的应用,看起来还是 rgbaster 效果好一点,但是我们刚刚也拿他试了,它并不能适用于颜色复杂度高的、渐变色的图片。


    思考


    既然又又又没有人做这件事,正所谓我不入地狱谁入地狱,我手写一个


    整理一下需求,我发现我希望得到的是:



    1. 图片的主题色(面积占比最大)

    2. 次主题色(面积占比第二大)

    3. 合适的背景色(即图片边缘颜色,渐变时,需要边缘颜色来设置背景色)


    这样一来,就已经可以覆盖大部分需求了,1+2 可以生成相关的 主题 TAG、主题背景,3 可以使留白的图片容器完美融合。


    开搞


    ⚠⚠ 本小节内容非常硬核,如果不想深究原理可以直接跳过,文章末尾有用法和效果图 ⚠⚠


    思路


    首先需要避免上面提到的插件的缺点,即对渐变图片要做好处理,不能取出成千上万的颜色,体验太差且实用性不强,对于渐变色还有一点,即在渐变路径上,每一点的颜色都是不一样的,所以需要将他们以一个阈值分类,挑选出一众相近色,并计算出一个平均色,这样就不会导致主题色太精准进而没有代表性。


    对于背景色,需要按情况分析,如果只是希望做一个协调的页面,那么大可以直接使用主题色做渐变过渡或蒙层,也就是类似于这种效果


    image.png


    但是如果希望背景与图片完美衔接,让人看不出图片边界的感觉,就需要单独对边缘颜色取色了。


    最后一个问题,如果图片分辨率过大,在遍历像素点时会非常消耗性能,所以需要降低采样率,虽然会导致一些精度上的丢失,但是调整为一个合适的值后应该基本可用。


    剩余的细节问题,我会在下面的代码中解释


    使用 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*bCIE 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]
    }

    这个函数使用了看起来很复杂的算法,不必深究,这是它的大概解释:



    1. 获取到 rgb 参数

    2. 转化为线性 rgb(移除 gamma矫正),常量 0.04045 是sRGB(标准TGB)颜色空间中的一个阈值,用于区分非线性和线性的sRGB值,具体来说,当sRGB颜色分量大于0.04045时,需要通过 gamma 校正(即采用 ((R + 0.055) / 1.055) ^ 2.4)来得到线性RGB;如果小于等于0.04045,则直接进行线性转换(即 R / 12.92

    3. 线性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



    4. 归一化XYZ值,为了参考白点(D65),标准白点的XYZ值是 (0.95047, 1.0, 1.08883)。所以需要通过除以这些常数来进行归一化

    5. XYZ到Lab的转换,公式函数:const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)

    6. 计算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))


    最终效果


    image.png


    复杂边缘效果


    image.png


    纵向渐变效果(这里使用的是 left 和 right 边的值,可能使用 top 和 bottom 效果更佳)


    image.png


    纯色效果(因为单独对边缘采样,所以无论图片内容多复杂,纯色基本看不出边界)


    image.png


    突变边缘效果(此时用css做渐变蒙层应该效果会更好)


    image.png


    横向渐变效果(使用的是 left 和 right 的色值),基本看不出边界


    参考资料



    番外


    Auto 家族的其他成员



    作者:德莱厄斯
    来源:juejin.cn/post/7471919714292105270
    收起阅读 »

    记一次 CDN 流量被盗刷经历

    先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。 600多G流量,100多万次请求。 怎么发现的 先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了。 抱着看热闹的心情点开阅读了。。。心想,看看自己的中...
    继续阅读 »

    先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。



    600多G流量,100多万次请求。


    怎么发现的


    先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了


    抱着看热闹的心情点开阅读了。。。心想,看看自己的中招没,结果就真中招了 🍉。


    被盗刷资源分析


    笔者在 缤纷云七牛云又拍云 都有存放一些图片资源。本次中招的是 缤纷云,下面是被刷的资源。



    IP来源


    查了几个 IP 和文章里描述的大差不差,都是来自山西联通的请求。



    大小流量计算


    按日志时间算的话,QPS 大概在 20 左右,单文件 632 K,1分钟大概就760MB ,1小时约 45G 左右。


    看了几天前的日志,都是 1 小时刷 40G 就停下,从 9 点左右开始,刷到 12 点。


    07-0907-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. 闭包:关键点在于函数是否捕获了其外部作用域的变量 闭包的形成: 定义函数时, 函数引用了其外部作用域的...
    继续阅读 »

    😃文章首发于公众号[精益码农]。


    闭包作为前端面试的必考题目,常让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小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。二、环境准备(5分钟搞定...
    继续阅读 »

    一、为什么需要AI代码审查?

    写代码就像做饭,即使是最有经验的厨师(程序员),也难免会忘记关火(资源未释放)、放错调料(逻辑错误)或者切到手(空指针异常)。Deepseek就像一位24小时待命的厨房监理,能帮我们实时发现这些"安全隐患"。

    二、环境准备(5分钟搞定)

    1. 安装Deepseek插件(以VSCode为例):
      • 插件市场搜索"Deepseek Code Review"
      • 点击安装(就像安装手机APP一样简单)

    1. 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);
    }
    }

    使用Deepseek审查后:

    智能修复建议:

    1. 空指针防护 → 建议添加Optional处理
    2. 流资源 → 推荐try-with-resources语法
    3. 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();
    }
    }

    四、实现原理揭秘

    Deepseek的代码审查就像"X光扫描仪",通过以下三步工作:

    1. 模式识别:比对数千万个代码样本
      • 就像老师批改作业时发现常见错误
    1. 上下文理解:分析代码的"人际关系"
      • 数据库连接有没有"成对出现"(打开/关闭)
      • 敏感操作有没有"保镖"(权限校验)
    1. 智能推理:预测代码的"未来"
      • 这个变量走到这里会不会变成null?
      • 这个循环会不会变成"无限列车"?

    五、进阶使用技巧

    1. 自定义审查规则(配置文件示例):
    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%
    知识库更新速度季度实时

    作者:Java技术小馆
    来源:juejin.cn/post/7473799336675639308

    收起阅读 »

    停止在TS中使用.d.ts文件

    web
    看到Matt Pocock 在 X 上的一个帖子提到不使用 .d.ts 文件的说法。 你赞同么?是否也应该把 .d.ts 文件都替换为 .ts 文件呢? 我们一起来看看~ .d.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.tsadd.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.tsadd.js 文件。


    这意味着你无需担心将声明文件与其对应的实现文件分开组织。




    不过,针对类库,将 .d.ts 文件与编译后的 JavaScript 源代码一起使用,比存储 .ts 文件更高效,因为你真正需要的只是类型声明,以便用户在使用你的库时能够获得类型安全性。


    这确实没错,需要强调的是,更推荐自动生成。通过更改 package.jsontsconfig.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年外卖最火热的时候,有一篇文章《外卖骑手,困在系统里》。 作为一个互联网从业人员,我之前从未有机会体会到,当每一个工作都要被时间和算法压榨时,我会是一种怎样的感受。 而最近的一段经历...
    继续阅读 »

    前言


    Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。


    2020年外卖最火热的时候,有一篇文章《外卖骑手,困在系统里》。



    作为一个互联网从业人员,我之前从未有机会体会到,当每一个工作都要被时间和算法压榨时,我会是一种怎样的感受。


    而最近的一段经历,我感觉也被困在系统里了。


    起因


    如果你是一个研发人员,免不了要值班、处理线上问题。当然这都很正常,每个系统都有bug或者咨询类的问题。


    由于我们面临的客户比较多,加上系统有一些易用性的问题或bug,提出来的问题不少。


    公司有一项政策,当客服人员提交工单之后,系统对每一个单子有超时时间,如果超出一定时间你还未提交,罚款50元。


    挺奇葩的,谁能保证1个小时就一定能排查出问题呢?


    于是就会有一个场景,如果赶上问题多,一下子来5、6个工单,恰巧遇到不容易排查的耽误时间的话,处理后面的工单,都面临着超时的压力。



    之前同事们对值班这件事,充满了怨言,大多都会吐槽几个点



    1. 系统bug太多了,又是刚刚某某需求改出来的问题

    2. 需求设计不合理,很多奇怪的操作导致了系统问题

    3. 客服太懒了,明明可以自己搜,非得提个工单问

    4. 基础设施差,平台不好用


    我不太爱吐槽,但当工单一下子来的太多的时候,我不由自主的陷入机械的处理问题中,压缩思考的时间,只求不要超时就好。


    明明系统有很多问题需要解决、流程也有很多可以优化,可当系统给到我们的压力越来越多时,我们便不去思考,陷入只有吐槽、怨言和避免罚款的状态。


    当陷入了系统的支配,只能被动接受,甚至有了一些怨言的时候,我意识到,这样的状态,是有问题的。


    被困住的打工人


    外卖员为什么不遵守交通规则呢?


    外卖小哥为了多赚钱、避免处罚,我之前也很不理解,为什么为了避免处罚,连自己的生命安全都可以置之不顾。



    但转念一想,我们虽然不用在马路上奔波,可受到“系统”的压力,可是一点也不比外卖员少。


    大家一定有过类似的经历:你骑车或者开车去上班,距离打卡时间所剩无几,你在迟到的边缘疯狂试探,可能多一个红绿灯,你就赶不上了,这时候你会不会狠踩几脚油门、闯一个黄灯,想要更快一点呢?


    但随着裁员、降本增效、各类指标的压力越来越大,我们被迫不停的内卷,不断压榨自己,才能满足职场要求越来越严格的“算法”,比如,每半年一次的绩效考核,月度或者季度的OKR、KPI,还有处理不完的线上问题、事故,充斥在我们的脑海里面。


    其实我们何尝不是“外卖员”呢?外卖员是为了不被扣钱,我们是为了年终奖、晋升罢了。


    所以回过头来看,其实我们早早的就被困在“系统”中了,为了满足系统的要求,我们不得不埋头苦干,甚至加班透支身体,作出很多非常短线思维的事情。


    但为什么,我之前从来没有过被困住的感觉,为什么我现在才回过神来,意识到这个问题呢?


    我想,大概是越简单的事情,你作出的反应就越快、越激烈。而越复杂、时间越长的事情,你作出的反应就越缓慢,甚至忽略掉。


    比如上班即将迟到的你,你会立刻意识到,迟到可能会受到处罚。但是年终评估你的绩效目标时,你或许只有在最后的几个月才会意识到,某某事情没完成,年终奖或许要少几个月而感到着急。


    积极主动


    最近正好在读《高效能人士的七个习惯》,其中第一个习惯就是积极主动


    书中说到:人性的本质是主动而非被动的,人类不仅能针对特定环境选择回应方式,更能主动创造有利的环境。


    我们面对的问题可以分为三类:



    • 可直接控制的(问题与自身的行为有关)

    • 可间接控制的(问题与他人的行为有关)

    • 无法控制的(我们无能为力的问题,例如我们的过去或现实的环境)


    对于这三类问题,积极主动的话,应该如何加以解决呢。


    可直接控制的问题


    针对可直接控制的问题,可以通过培养正确习惯来解决。


    从程序员角度来看,线上bug多,可以在开发前进行技术设计,上线前进行代码CR,自动化测试,帮助自己避免低级的问题。


    面对处理工单时咨询量特别多的问题,随手整理个文档出来,放到大家都可以看到的地方。


    可间接控制的


    对于可间接控制的,我们可以通过改进施加影响的方法来解决。


    比如流程机制的不合理,你可以通过向上反馈的方式施加影响,提出自己的建议而不是吐槽。


    无法控制的


    对于无法控制的,我们要做的就是改变面部曲线,以微笑、真诚与平和来接受现实。


    虽然反馈问题的人或许能力参差不齐,导致工单量很多,但我们意识到这一点是无法避免的,不如一笑而过,这样才不至于被问题左右。


    说在最后


    好了,文章到这里就要结束了。


    最近由于值班的原因,陷入了一段时间的无效忙碌中,每一天都很累,几乎抽不出时间来思考,所以更新的频率也降下来了。


    但还好,及时的意识到问题,把最近的一点思考分享出来,希望我们每个人都不会被“系统”困住。


    欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~


    本篇文章是第41篇原创文章,2024目标进度41/100,欢迎有趣的你,关注我。


    作者:东东拿铁
    来源:juejin.cn/post/7385098943942656054
    收起阅读 »

    再见Typora,这款大小不到3M的Markdown编辑器,满足你的所有幻想!

    Typora 是一款广受欢迎的 Markdown 编辑器,以其所见即所得的编辑模式和优雅的界面而闻名,长期以来是许多 Markdown 用户的首选。然而,从 2021 年起,Typora 不再免费,采用一次性付费授权模式。虽然费用不高,但对于轻量使用者或预算有...
    继续阅读 »

    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…


    作者:Github掘金计划
    来源:juejin.cn/post/7456685819047919651
    收起阅读 »

    前端适配:你一般用哪种方案?

    web
    前言 最近在公司改bug,突然发现上一个前端留下的毛病不少,页面存在各种适配问题,为此甲方爸爸时常提出宝贵意见! 你的页面是不是时常是这样: 侧边栏未收缩时: 收缩后: 这样(缩小挤成一坨): 又或是这样: 那么废话不多说,今天由我不是程序猿kk为大家...
    继续阅读 »

    前言


    最近在公司改bug,突然发现上一个前端留下的毛病不少,页面存在各种适配问题,为此甲方爸爸时常提出宝贵意见!


    你的页面是不是时常是这样:


    侧边栏未收缩时:
    image.png


    收缩后:


    image.png


    这样(缩小挤成一坨):


    image.png


    又或是这样:


    image.png


    那么废话不多说,今天由我不是程序猿kk为大家讲解一些前端必备知识:适配工作。


    流式布局


    学会利用相对单位(例如百分比,vh或是vw),而不是只会用px一类固定单位设计布局,前言中提到的收缩后多出一大块空白,就是由于写死了宽度,例如1000px或是89vw,那么当侧边栏进行收缩,右边内容宽度还是只有89个vw,因此我们可以将其更改为100%,这样不论侧边栏是否收缩,内容都会占满屏幕的全部。


    .map {
    width: 100%;
    height: 90vh;
    position: relative;
    }

    image.png


    image.png


    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"

    相关配置


    image.png


    媒体查询


    通过查询不同的宽度来执行不同的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吧?


    作者:zykk
    来源:juejin.cn/post/7431999862919446539
    收起阅读 »

    独立开发:家人不支持怎么办?

    大家好,我是农村程序员,独立开发者,前端之虎陈随易。 这是我的个人网站:chensuiyi.me,欢迎一起交朋友~ 有很多人跟我聊到过这个问题:做独立开发,家人不支持怎么办?。 在我交流沟通下,最终发现,这些人都走进了一个误区:独立开发者 等于 我要辞职全...
    继续阅读 »

    大家好,我是农村程序员,独立开发者,前端之虎陈随易。


    个人网站


    这是我的个人网站:chensuiyi.me,欢迎一起交朋友~




    有很多人跟我聊到过这个问题:做独立开发,家人不支持怎么办?


    在我交流沟通下,最终发现,这些人都走进了一个误区:独立开发者 等于 我要辞职全职做独立开发


    请看我对独立开发者的分类:



    1. 业余独立开发。特点:上班 + 下班的业余时间独立开发

    2. 兼职独立开发。特点:不上班 + 没有充足的时间做独立开发

    3. 全职独立开发。特点:不上班 + 有充足的时间做独立开发

    4. 混合独立开发。特点:上班+兼职+没有充足的时间做独立开发


    现在是不是一目了然了。


    你可以根据自己当下的情况,特点,去选择做哪一种 独立开发


    我们目前所看到的 全职独立开发,只有极少数人可以做到。


    这对于个人的内在要求,包括自律,坚持,执行力,产品力,都有着较高的要求。


    同时呢,来自家人的态度和压力,也是 全职独立开发 的重要条件。


    不要一开始,啥独立开发的经验都没有,就想做 全职独立开发


    那么当你可以 理性地选择 适合自己当下情况的的独立开发方式后,你会发现,家人还会不支持吗?至少不会那么反对了。


    所以这个问题的答案就是这么简单,只要看了我对独立开发的分类,你就明白了。


    独立开发,本就是一个人的战斗,不要妄想这家人会支持你,他们最大的支持就是不反对。


    我们遇到这样的问题时,不要觉得家人怎么怎么样,自己受到了多大的委屈和不理解一样。


    他们的想法,是完全没有问题的。


    人是社会动物,必然要考虑当下的生存问题,这是十分合理且正常的。


    那么,如果上面的问题解决后,家人还是不支持,怎么办呢?


    也很简单啊,自己偷摸摸继续折腾呗,难道一定要得到家人的支持,才能做独立开发吗?


    《明朝那些事》 的作者,当年明月,赚了几千万家人才知道呢。


    当然,我不是说你,也不是说我自己,可以赚几千万,我们可以定目标,但不能做梦。


    总而言之就是说,做独立开发,要做好一个人长期战斗的准备。


    因为你很有可能,很多年都无法比较稳定地每个月赚 5000 块钱,独立开发远没有我们想象的那么轻松。


    如果你实在没有时间,没有干劲,没有激情做独立开发,那么不如其他方向,说不定能获得更好的回报。


    独立开发是一个美好的梦,不稳定,也容易破碎。


    那么我为什么一直在坚持做独立开发呢?因为我想让美梦成真。


    作者:农村程序员陈随易
    来源:juejin.cn/post/7434366864866099234
    收起阅读 »

    制作一个页面时,需要兼容PC端和手机端,你是要分别做两个页面还是只做一个页面自适应?为什么?说说你的理由

    web
    在制作一个页面时,如何兼容PC端和手机端是一个重要的设计决策。这个决策通常有两个选择:分别制作两个页面,或者只制作一个自适应页面。以下是我选择后者的理由,以及如何实现自适应设计的相关内容。 选择自适应设计的理由 提高开发效率 制作一个自适应页面可以显著提高开...
    继续阅读 »

    在制作一个页面时,如何兼容PC端和手机端是一个重要的设计决策。这个决策通常有两个选择:分别制作两个页面,或者只制作一个自适应页面。以下是我选择后者的理由,以及如何实现自适应设计的相关内容。


    选择自适应设计的理由



    1. 提高开发效率

      制作一个自适应页面可以显著提高开发效率。开发者只需编写一次代码,就可以在不同设备上展示同样的内容,而不必为每个设备维护多个代码库。这意味着在后续的更新和维护中,相比于维护两个独立的页面,自适应页面的工作量会大幅减少。

    2. 一致的用户体验

      用户在不同设备上访问同一网站时,能够获得一致的用户体验是极其重要的。自适应设计可以确保在不同屏幕尺寸和分辨率下,用户看到的内容和布局尽可能一致,避免用户在不同设备上获取不同信息的困惑。

    3. SEO优化

      使用单一的自适应页面有助于SEO优化。搜索引擎更倾向于索引和排名内容一致的网站,而不是将相同内容分散在多个页面上。使用自适应设计可以集中流量,提高网站在搜索引擎中的权重。

    4. 成本效益

      维护两个独立的页面不仅需要更多的时间和人力成本,还可能导致代码重复,增加出错的可能性。自适应设计能够通过一个代码库降低开发和维护成本。

    5. 响应式设计的灵活性

      现代的 CSS 和 JavaScript 技术(例如 Flexbox 和 CSS Grid)使得创建自适应布局变得更加简单和灵活。媒体查询(Media Queries)可以帮助开发者根据设备的特性(如宽度、高度、分辨率)调整布局和样式,从而提供最佳的用户体验。


    如何实现自适应设计



    1. 使用媒体查询

      媒体查询是实现自适应设计的核心。它允许你根据设备的屏幕尺寸和特性应用不同的样式。例如:


      /* 默认样式 */
      .container {
      width: 100%;
      padding: 20px;
      }

      /* 针对手机的样式 */
      @media (max-width: 600px) {
      .container {
      padding: 10px;
      }
      }

      /* 针对平板的样式 */
      @media (min-width: 601px) and (max-width: 900px) {
      .container {
      padding: 15px;
      }
      }


    2. 使用流式布局

      使用流式布局可以使元素在不同的屏幕上根据可用空间自动调整大小。例如,使用百分比而不是固定像素值来设置宽度:


      .box {
      width: 50%; /* 宽度为父容器的一半 */
      height: auto; /* 高度自动适应内容 */
      }


    3. 灵活的图片和媒体

      为了确保图片和视频在不同设备上显示良好,使用 max-width: 100% 来确保媒体不会超出其容器的宽度:


      img {
      max-width: 100%;
      height: auto; /* 保持图片的纵横比 */
      }


    4. 测试和优化

      在开发完成后,确保在不同设备和浏览器上进行测试。可以使用 Chrome 的开发者工具模拟不同的设备,以检查自适应设计的效果。同时,收集用户反馈并进行必要的优化。


    总结


    在制作兼容PC端和手机端的页面时,选择制作一个自适应页面是更为高效和经济的方案。它不仅能提高开发效率和维护成本,还能提供一致的用户体验,增强SEO效果。此外,现代的CSS技术使得实现自适应设计变得更加简单。因此,采用自适应设计是现代Web开发的最佳实践之一。


    作者:Riesenzahn
    来源:juejin.cn/post/7476010111887949861
    收起阅读 »

    别让这6个UI设计雷区毁了你的APP!

    web
    一款成功的APP不仅仅取决于其功能性,更取决于用户体验,这其中,UI设计又至关重要。优秀的UI设计能够为用户带来直观、愉悦的交互体验,甚至让用户“一见钟情”,从而大大提高产品吸引力。 然而,有很多设计师在追求创新与美观的同时,往往会不经意间“踩雷”,忽略了一些...
    继续阅读 »

    一款成功的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. 1)矢量编辑:直观的矢量编辑工具和可编辑的布尔运算,支持形状绘制、路径编辑、填充和描边等常见的矢量操作,使用户能够轻松创建和编辑各种设计元素。

    2. 2)设计规范库:设计师可以在sketch中把常用的颜色、图层样式存为规范,也可以将已经设计好的矢量图标存到规范库,在整个设计中复用它们,提高工作效率。


    3)Sketch支持众多插件和第三方资源,使得其生态系统非常丰富,能够更好地满足设计师的需求。


    **价格:**标准订阅 10//人(按年付费),10/月/人(按年付费),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学习资料、求职面试、升职加薪、职场写作和沟通等实用资源。


    礼包资源持续更新,互联网行业知识一网打尽!礼包领取地址:


    docs.qq.com/form/page/D…



    作者:摹客
    来源:juejin.cn/post/7356535808931627046
    收起阅读 »