注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

微信小程序:uniapp解决上传小程序体积过大的问题

web
概述 在昨天的工作中遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,首先介绍一下,技术栈是使用uniapp框架+HBuilderX的开发环境。 错误提示 真机调试,提示包提交过大,不能正常生成二维码,后续上传代码更是不可能了,减少包中的...
继续阅读 »

概述


在昨天的工作中遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,首先介绍一下,技术栈是使用uniapp框架+HBuilderX的开发环境。


错误提示


图4.png


真机调试,提示包提交过大,不能正常生成二维码,后续上传代码更是不可能了,减少包中的体积顺着这条思路去解决问题。


1.静态图片资源改变成网络请求的方式


问题5.png


我们使用的初衷是,把图片加载在static本地,缓存在本地,以便提升更快的响应速度,第一步剥离大的图片更换成网络请求,顺着编辑器提示去处理。


2.对小程序进行分包


小程序主包最大可以加载到1.5M,加载所有的依赖和插件不能大于2M,小程序中有个解决办法是对小程序进行分包处理,使每个包保持在2M的大小,主包和分包之间直接进行跳转,分包和分包不能跳转。


"optimization" : {
"subPackages" : true
},

进行了拆包还是没有解决问题,分包的作用主要运行的是代码,也就是说代码要尽量的小,多了需要进行分解。


3.压缩vendor.js


昨天真正的定位问题是vendor.js 1.88M ,小程序开发代码工具-详情-代码依赖分析中查看,解决vendor.js才是根本的解决之道。


使用HBuilderX打包上传来解决问题,HBuilderX -> 发行 -> 小程序(微信),操作的过程失败了一次,是因为需要注意的是需要绑定开发者后台的地方,开发管理->开发设置->小程序代码上传下载小程序代码上传密钥和绑定IP白名单,这个需要管理员同意。


问题6.png


最后包的体积从12.88M压缩到了4.16M,问题得以解决。


作者:stark张宇
来源:juejin.cn/post/7282363816020508733
收起阅读 »

uni-app app端 人脸识别

web
在听到人脸识别,连忙去看看,去阿里 腾讯 看他们的人脸识别方法,官方sdk什么的。 到后来,需求确定了,拍照(照片)上传,后台去识别是不是本人,这一瞬间从天堂到地狱,放着官方那么好的方法。 用照片,还的自己去写,去实现。 下面为大家提供一个 uni-app 自...
继续阅读 »

在听到人脸识别,连忙去看看,去阿里 腾讯 看他们的人脸识别方法,官方sdk什么的。


到后来,需求确定了,拍照(照片)上传,后台去识别是不是本人,这一瞬间从天堂到地狱,放着官方那么好的方法。


用照片,还的自己去写,去实现。


下面为大家提供一个 uni-app 自动拍照 上传照片 后端做匹配处理。


参考插件市场的 ext.dcloud.net.cn/plugin?id=4…


在使用前 先去manifest.json 选择APP模块配置, 勾选直播推流



直接采用nvue开发,直接使用live-pusher组件进行直播推流,如果是vue开发,则需要使用h5+的plus.video.LivePusher对象来获取


nuve js注意事项


注意nuve 页面 main.js 的封装函数 。无法直接调用(小程序其他的端没有测试)


在APP端 this.api报错,显示是undefined,难道nvue页面,要重新引入api文件


在APP端,main.js中挂载Vuex在nvue页面无法使用this.$store.state.xxx


简单粗暴点直接用uni.getStorageSync 重新获取一遍


//获取用户数据 userInfo在Data里定义


this``.userInfo = uni.getStorageSync(``'userInfo'``)


nuve css注意事项


单位只支持px


其他的em,rem,pt,%,upx 都不支持


需要重新引入外部css


不支持使用 import 的方式引入外部 css


<``style src="@/common/test.css"></``style``>


 默认flex布


display: flex; //不需要写
//直接用下面的标签
flex-direction: column;
align-items: center;
justify-content: space-between;

页面样式


<view class="live-camera" :style="{ width: windowWidth, height: windowHeight }">
<view class="title">
{{second}}秒之后开始识别
</view>
<view class="preview" :style="{ width: windowWidth, height: windowHeight-80 }">
<live-pusher id="livePusher" ref="livePusher" class="livePusher" mode="FHD" beauty="1" whiteness="0"
aspect="2:3" min-bitrate="1000" audio-quality="16KHz" :auto-focus="true" :muted="true"
:enable-camera="true" :enable-mic="false" :zoom="false" @statechange="statechange"
:style="{ width: cameraWidth, height: cameraHeight }">
</live-pusher>

<!--提示语-->
<cover-view class="remind">
<text class="remind-text" style="">{{ message }}</text>
</cover-view>

<!--辅助线-->
<cover-view class="outline-box" :style="{ width: windowWidth, height: windowHeight-80 }">
<cover-image class="outline-img" src="../../static/idphotoskin.png"></cover-image>
</cover-view>
</view>
</view>

JS部分


<script>
import operate from '../../common/operate.js'
import api from '../../common/api.js'
export default {
data() {
return {
//提示
message: '',
//相机画面宽度
cameraWidth: '',
//相机画面宽度
cameraHeight: '',
//屏幕可用宽度
windowWidth: '',
//屏幕可用高度
windowHeight: '',
//流视频对象
livePusher: null,
//照片
snapshotsrc: null,
//倒计时
second: 0,
ifPhoto: false,
// 用户信息
userInfo: []
};
},
onLoad() {
//获取屏幕高度
this.initCamera();
//获取用户数据
this.userInfo = uni.getStorageSync('userInfo')
setTimeout(() => {
//倒计时
this.getCount()
}, 500)
},
onReady() {
// console.log('初始化 直播组件');
this.livePusher = uni.createLivePusherContext('livePusher', this);
},
onShow() {
//开启预览并设置摄像头
/*
* 2023年12月28日
* 在最新的APP上面这个周期 比onReady 直播初始要早执行
* 故而第二次进入页面 相机启动失败
* 把该方法 移步到 onReady 即可
*/


this.startPreview();

},
methods: {
//获取屏幕高度
initCamera() {
let that = this
uni.getSystemInfo({
success: function(res) {
that.windowWidth = res.windowWidth;
that.windowHeight = res.windowHeight;
that.cameraWidth = res.windowWidth;
that.cameraHeight = res.windowWidth * 1.5;
}
});
},
//启动相机
startPreview() {
this.livePusher.startPreview({
success(res) {
console.log('启动相机', res)
}
});
},
//停止相机
stopPreview() {
let that = this
this.livePusher.stopPreview({
success(res) {
console.log('停止相机', res)
}
});
},
//摄像头 状态
statechange(e) {
console.log('摄像头', e);
if (this.ifPhoto == true) {
//拍照
this.snapshot()
}
},
//抓拍
snapshot() {
let that = this
this.livePusher.snapshot({
success(res) {
that.snapshotsrc = res.message.tempImagePath;
that.uploadingImg(res.message.tempImagePath)
}
});
},
// 倒计时
getCount() {
this.second = 5
let timer = setInterval(() => {
this.second--;
if (this.second < 1) {
clearInterval(timer);
this.second = 0
this.ifPhoto = true
this.statechange()
}
}, 1000)
},
// 图片上传
uploadingImg(e) {
let url = e
// console.log(url);
let that = this
uni.uploadFile({
url: operate.api + 'api/common/upload',
filePath: url,
name: 'file',
formData: {
token: that.userInfo.token
},
success(res) {
// console.log(res);
let list = JSON.parse(res.data)
// console.log(list);
that.request(list.data.fullurl)
}
})
},
//验证请求
request(url) {
let data = {
token: this.userInfo.token,
photo: url
}
api.renzheng(data).then((res) => {
// console.log(res);
operate.toast({
title: res.data.msg
})
if (res.data.code == 1) {
setTimeout(() => {
operate.redirectTo('/pages/details/details')
}, 500)
}
if (res.data.code == 0) {
setTimeout(() => {
this.anew(res.data.msg)
}, 500)
}
})
},
// 认证失败,重新认证
anew(msg) {
let that = this
uni.showModal({
content: msg,
confirmText: '重新审核',
success(res) {
if (res.confirm) {
// console.log('用户点击确定');
that.getCount()
} else if (res.cancel) {
// console.log('用户点击取消');
uni.navigateBack({
delta: 1
})
}
}
})
},
}
};
</script>

css 样式


<style lang="scss">
// 标题
.title {
font-size: 35rpx;
align-items: center;
justify-content: center;
}

.live-camera {
.preview {
justify-content: center;
align-items: center;

.outline-box {
position: absolute;
top: 0;
left: 0;
bottom: 0;
z-index: 99;
align-items: center;
justify-content: center;

.outline-img {
width: 750rpx;
height: 1125rpx;
}
}

.remind {
position: absolute;
top: 880rpx;
width: 750rpx;
z-index: 100;
align-items: center;
justify-content: center;

.remind-text {
color: #dddddd;
font-weight: bold;
}
}
}
}
</style>


作者:虚乄
来源:juejin.cn/post/7273126566459719741
收起阅读 »

35岁,是终点?还是拐点?

35岁,是终点还是拐点,取决于我们对生活和事业的态度、目标以及行动。这个年龄可以看作是一个重要的转折点,具有多重意义和可能性。 很多人在35岁时,已经在自己的职业生涯中建立了一定的基础,可能达到了管理层或专家级别。如果你还是一个基层员工,那你要反思一下,你的...
继续阅读 »

35岁,是终点还是拐点,取决于我们对生活和事业的态度、目标以及行动。这个年龄可以看作是一个重要的转折点,具有多重意义和可能性。



很多人在35岁时,已经在自己的职业生涯中建立了一定的基础,可能达到了管理层或专家级别。如果你还是一个基层员工,那你要反思一下,你的职业生涯规划可能出了问题,工作能力与人情世故为什么都没有突破?是否在某个领域深耕多年?



有些人可能会选择在这个年龄段重新评估自己的职业,考虑转型或创业,寻找新的挑战和机遇。这是个不错的想法,基于过往积累的经验和能力,现在自媒体发达,个人的创业成本低到0也可以创业,就是你能坚持多久,给自己多长时间的规划,又想达成怎样的目标。



35岁通常是家庭责任较重的时期,可能要照顾孩子和父母,家庭生活会影响个人的时间和精力分配。这是35岁中年油腻大叔最难的地方,上有老,下有小,自己还是很渺小。这是最痛苦的,家里顶梁柱,连倒下的资格都没有。



在自我认知层面,35岁的人通常对自己的优缺点、兴趣爱好有更清晰的认识,知道自己想要什么,不想要什么。所以这个年龄段的人可能会更加关注健康和自我提升,进行一些以前没有时间或精力去做的事情,如学习新技能、锻炼身体等。


举个例子,现在的 AI 和鸿蒙,热得烫手,是程序员学习的新方向,不同于区块链、AR/VR这些事件,AI 落地各行各业产品,未来是 XXX+AI 的时代,不管你正在做 JAVA 后端开发,还是即将学习 JAVA 开发,你都逃脱不了 AI。



鸿蒙就不用说了,国产操作系统,多少年了,国人终于可以用上自己的操作系统,这不是事件,不是概念,是必然趋势,一个新的技术领域将要开启,我是很坚信的,拒绝反驳。



经过多年的生活和工作经历,相信大多数人已经积累了较为丰富的经验和智慧,对待问题更加冷静和理性。


35岁既不是终点,也不是绝对的拐点,而是人生旅途中一个重要的里程碑。它为未来的发展提供了丰富的经验和更明确的方向。关键在于如何利用过去的积累,进行自我调整和规划,为未来的生活和事业开辟新的道路。以上是 V哥的浅见,突出想到这些问题,就记录了下来,你有什么见解,咱们评论区讨论,拍砖请下手轻点,欢迎关注威哥爱编程,程序员路上愿与你成为一起前行的基友。


作者:威哥爱编程
来源:juejin.cn/post/7383342927509471283
收起阅读 »

Jquery4.0发布!下载量依旧是 Vue 的两倍!

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 其实在去年,Jquery 就宣布了要发布 4 版本 可以看到,Jquery 在五天前发布了 4 版本 Jquery4.0 更新了啥? 接下来说一下到...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


其实在去年,Jquery 就宣布了要发布 4 版本



可以看到,Jquery 在五天前发布了 4 版本




Jquery4.0 更新了啥?


接下来说一下到底更新了啥?


弃用了 1x 和 2x 版本,废弃一些方法


这意味着不再去兼容低版本了,未来 Jquery 将着力于发展新的版本,弃用了一些方法



  • jQuery.cssNumber

  • jQuery.cssProps

  • jQuery.isArray

  • jQuery.parseJSON

  • jQuery.nodeName

  • jQuery.isFunction

  • jQuery.isWindow

  • jQuery.camelCase

  • jQuery.type

  • jQuery.now

  • jQuery.isNumeric

  • jQuery.trim

  • jQuery.fx.interval


Typescript 重构


看过 Jquery 源码的都知道,以前 Jquery 是用 JavaScript 写的,现在新版本是采用 Typescript 重构的,提高整体代码的可维护性


对新特性的支持


jQuery 4.0 将添加对新的 JavaScript 特性的支持,包括:



  • async/await

  • Promise

  • Optional Chaining

  • Nullish Coalescing


优化性能



  • 优化 DOM 操作

  • 改进事件处理

  • 优化 Ajax 请求

  • 增强兼容性


增强兼容性



  • 支持 Internet Explorer 11 和更高版本

  • 支持 Edge 浏览器

  • 支持 Safari 浏览器


FormData 支持


jQuery.ajax 添加了对二进制数据的支持,包括 FormData。


此外,jQuery 4.0 还删除了自动 JSONP 升级、将 jQuery source 迁移至 ES 模块;以及添加了对 Trusted Types 的支持,确保以 TrustedHTML 封装的 HTML 能以不违反 require-trusted-types-for 内容安全策略指令的方式用作 jQuery 操作方法的输入。


由于删除了 Deferreds 和 Callbacks(现在压缩后不到 20k 字节),jQuery 4.0.0 的 slim build 变得更加小巧。


还有人用 Jquery 吗?


随着现在前端发展的迅速,越来越多人投入了 React、Vue 的怀抱,这意味着越来越少人用 Jquery 了,而且用 Jquery 的基本都是老项目,老项目都是求稳的,所以也不会去升级 Jquery


所以我不太看好 Jquery 后续的发展趋势,虽然曾经它真的帮助了我们很多


虽然如此,现阶段 NPM 上,Jquery 的下载量依旧是 Vue 的两倍



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

适配最新微信小程序隐私协议开发指南,兼容uniapp版本

web
前一阵微信小程序官方发布了一个用户隐私保护指引填写说明,说是为了规范开发者的用户个人信息处理行为,保障用户合法权益,小程序、插件中涉及处理用户个人信息的开发者,均需补充相应用户隐私保护指引。 估计是有啥政策强制要求,微信自己也还没想好要咋实现就匆匆发布了第一版...
继续阅读 »

前一阵微信小程序官方发布了一个用户隐私保护指引填写说明,说是为了规范开发者的用户个人信息处理行为,保障用户合法权益,小程序、插件中涉及处理用户个人信息的开发者,均需补充相应用户隐私保护指引。


估计是有啥政策强制要求,微信自己也还没想好要咋实现就匆匆发布了第一版,然后不出意外各种问题,终于在2023年8月22发布了可以正常接入调试的版本。


逛开发者社区很多人在吐槽这个东西,按照现在的实现方式微信完全可以自己在它的框架层实现,非得让开发者多此一举搞个弹窗再去调它的接口通知它。


吐槽归吐槽,代码还是要改的不是,毕竟不改9月15号之后相关功能就直接挂了!时间紧任务重下面直说干货。


准备工作



  • 小程序后台设置用户隐私保护指引,需要等待审核通过:设置-基本设置-服务内容声明-用户隐私保护指引

  • 小程序的基础库版本从 2.32.3 开始支持,所以要选这之后的版本

  • 在 app.json 中加上这个设置 " usePrivacyCheck" : true,在2023年9月15号之前需要自己手动加这个设置,15号之后平台就强制了


具体步骤可以参考官方给的开发文档,里面也有官方提供的 demo 文件。



原生小程序适配代码


直接参考的官方给的 demo3 和 demo4 综合修改出的版本,通过组件的方式引用,所有相关处理逻辑全部放到了 privacy 组件内部,其他涉及到隐私接口的页面只需在 wxml 里引用一下就行了,其他任何操作都不需要,组件内部已经全部处理了。


网上有其他人分享的,要在页面 onLoad、onShow 里获取是否有授权这些,用下面的代码这些都不需要,只要页面里需要隐私授权,引入 privacy 组件后,用户触发了隐私接口时会自动弹出此隐私授权弹窗。



长话短说这一步你总共只需做2个步骤:



  1. 新建一个 privacy 组件:privacy.wxml、privacy.wxss、privacy.js、privacy.json,完整代码在下方

  2. 在涉及隐私接口的页面引入 privacy 组件,如果使用的页面比较多,可以直接在 app.json 文件里通过 usingComponents 全局引入


privacy.wxml


<view wx:if="{{innerShow}}" class="privacy">
<view class="privacy-mask" />
<view class="privacy-dialog-wrap">
<view class="privacy-dialog">
<view class="privacy-dialog-header">用户隐私保护提示</view>
<view class="privacy-dialog-content">感谢您使用本小程序,在使用前您应当阅读井同意<text class="privacy-link" bindtap="openPrivacyContract">《用户隐私保护指引》</text>,当点击同意并继续时,即表示您已理解并同意该条款内容,该条款将对您产生法律约束力;如您不同意,将无法继续使用小程序相关功能。</view>
<view class="privacy-dialog-footer">
<button
id="btn-disagree"
type="default"
class="btn btn-disagree"
bindtap="handleDisagree"
>不同意</button>
<button
id="agree-btn"
type="default"
open-type="agreePrivacyAuthorization"
class="btn btn-agree"
bindagreeprivacyauthorization="handleAgree"
>同意并继续</button>
</view>
</view>
</view>
</view>

privacy.wxss


.privacy-mask {
position: fixed;
z-index: 5000;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.2);
}

.privacy-dialog-wrap {
position: fixed;
z-index: 5000;
top: 16px;
bottom: 16px;
left: 80rpx;
right: 80rpx;
display: flex;
align-items: center;
justify-content: center;
}

.privacy-dialog {
background-color: #fff;
border-radius: 32rpx;
}

.privacy-dialog-header {
padding: 60rpx 40rpx 30rpx;
font-weight: 700;
font-size: 36rpx;
text-align: center;
}

.privacy-dialog-content {
font-size: 30rpx;
color: #555;
line-height: 2;
text-align: left;
padding: 0 40rpx;
}

.privacy-dialog-content .privacy-link {
color: #2f80ed;
}

.privacy-dialog-footer {
display: flex;
padding: 20rpx 40rpx 60rpx;
}

.privacy-dialog-footer .btn {
color: #FFF;
font-size: 30rpx;
font-weight: 500;
line-height: 100rpx;
text-align: center;
height: 100rpx;
border-radius: 20rpx;
border: none;
background: #07c160;
flex: 1;
margin-left: 30rpx;
justify-content: center;
}

.privacy-dialog-footer .btn::after {
border: none;
}

.privacy-dialog-footer .btn-disagree {
color: #07c160;
background: #f2f2f2;
margin-left: 0;
}

privacy.js


let privacyHandler
let privacyResolves = new Set()
let closeOtherPagePopUpHooks = new Set()

if (wx.onNeedPrivacyAuthorization) {
wx.onNeedPrivacyAuthorization(resolve => {
if (typeof privacyHandler === 'function') {
privacyHandler(resolve)
}
})
}

const closeOtherPagePopUp = (closePopUp) => {
closeOtherPagePopUpHooks.forEach(hook => {
if (closePopUp !== hook) {
hook()
}
})
}

Component({
data: {
innerShow: false,
},
lifetimes: {
attached: function() {
const closePopUp = () => {
this.disPopUp()
}
privacyHandler = resolve => {
privacyResolves.add(resolve)
this.popUp()
// 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗
closeOtherPagePopUp(closePopUp)
}

closeOtherPagePopUpHooks.add(closePopUp)

this.closePopUp = closePopUp
},
detached: function() {
closeOtherPagePopUpHooks.delete(this.closePopUp)
}
},
pageLifetimes: {
show: function() {
this.curPageShow()
}
},
methods: {
handleAgree(e) {
this.disPopUp()
privacyResolves.forEach(resolve => {
resolve({
event: 'agree',
buttonId: 'agree-btn'
})
})
privacyResolves.clear()
},
handleDisagree(e) {
this.disPopUp()
privacyResolves.forEach(resolve => {
resolve({
event: 'disagree',
})
})
privacyResolves.clear()
},
popUp() {
if (this.data.innerShow === false) {
this.setData({
innerShow: true
})
}
},
disPopUp() {
if (this.data.innerShow === true) {
this.setData({
innerShow: false
})
}
},
openPrivacyContract() {
wx.openPrivacyContract({
success: res => {
console.log('openPrivacyContract success')
},
fail: res => {
console.error('openPrivacyContract fail', res)
}
})
},
curPageShow() {
if (this.closePopUp) {
privacyHandler = resolve => {
privacyResolves.add(resolve)
this.popUp()
// 额外逻辑:当前页面的隐私弹窗弹起的时候,关掉其他页面的隐私弹窗
closeOtherPagePopUp(this.closePopUp)
}
}
}
}
})

privacy.json


{
"component": true,
"usingComponents": {}
}

uniapp版本


uniapp 版本也可以直接用上面的代码,新建的 privacy 组件放到微信小程序对应的 wxcompnents 目录下,这个目录下是可以直接放微信小程序原生的组件代码的,因为目前只有微信小程序有这个东西,后期还可能随时会更改,所以没必要再额外去封装成 vue 组件了。


页面引用组件的时候直接用条件编译去引用:


{
// #ifdef MP-WEIXIN
"usingComponents": {
"privacy": "/wxcomponents/privacy/privacy"
}
// #endif
}

在 vue 页面里使用组件也要用条件编译:


<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<privacy />
<!-- #endif -->
</view>
</template>

注意uniapp官方目前还没有来适配微信这,目前开发调试 usePrivacyCheck 这个设置放到 page.json 文件里无效的,要放到 manifest.json 文件的 mp-weixin 下面:


{
"name" : "uni-plus",
"appid" : "__UNI__3C6F1BF",
"mp-weixin" : {
"appid" : "wx123456789",
"__usePrivacyCheck__" : true
}
}

作者:cafehaus
来源:juejin.cn/post/7272276908381175819
收起阅读 »

扒一扒uniapp是如何做ios app应用安装的

为何要扒 因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来...
继续阅读 »

为何要扒


因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。




开干


官方模板




先打开uniapp云打包一下项目看看


image-20230824112232275.png




复制地址到移动端浏览器打开看看


image-20230824112410817.png


这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。




开扒




F12打开choromdevtools,ctrl+s保存网页html。


image.png


保存成功,接下来看看html代码(样式代码删除了)


    <!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
   <head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
 </head>

<body>
<br><br>
   <center>
       <a class="button" href="itms-services://?action=download-manifest&amp;url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
   </center>
   <br><br>
   <center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>



解析




从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")


先看看itms-services是什么意思,下面是代码开发助手给的解释


image-20230824113418246.png


大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。




什么又是plist呢,这里再请我们的代码开发助手解释一下


image-20230824113748570.png


对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。




打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。


image-20230824114108792.png


访问后会出现


image-20230824115354028.png




别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求


image-20230824115609551.png


直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
      <dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>

直接抓重点,这里存你存放ipa包的地址


image-20230824120013828.png


这里改你应用的昵称


image-20230824120453368.png


这里改图标


image-20230824120509797.png


因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。




为我所用


分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:


image-20230824155040313.png


将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:


image-20230824155306228.png


可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至此,本次扒拉过程结束,需求落幕!


作者:廿一c
来源:juejin.cn/post/7270799565963149324
收起阅读 »

在微信小程序里使用rpx,被坑了😕 | 不同设备表现不同

web
小小需求 实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。 放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。 开发 组件结构 按照上面说的过程,放好按钮,添加好点击事...
继续阅读 »

小小需求


实现一个 Tooltip,就是那种很简单有一个按钮,按一下就会出现或消失气泡的组件,就是下面这个效果。


gif


放个按钮、加个点击事件、做好气泡的定位好像就搞定了。然鹅,事情没有发展的这么顺利。


开发


组件结构


按照上面说的过程,放好按钮,添加好点击事件。比较复杂的地方就是处理气泡的定位,气泡需要进行绝对定位,让它脱离文档流,不能在隐藏或偏移的时候还占个坑(需要实现那种浮动的效果)。还有气泡有个小三角,这个三角也是需要额外处理定位的。于是设计了组件的结构如下:
old.png
Tooltip 包着 Button 显示在界面上,设置定位属性,让它可以成为子元素定位的基准元素。然后创建 Prompt,它会相对于 Tooltip 进行定位,Prompt 中的小三角形则相对于 Prompt 进行定位。定位的具体数值则根据元素的尺寸和想放置的位置进行定位。在这个例子中就是实现气泡在按钮下方居中显示, Prompt 偏移数值计算如下:

水平偏移需要考虑 Tooltip 和 Prompt 的宽度,偏移的距离就是两者宽度之差的一半。


move


left=width(Tooltip)/2width(Prompt)/2left = width(Tooltip) / 2 - width(Prompt) / 2

垂直偏移则要考虑 Tooltip、Prompt 和 小三角的高度,计算方法类似。(偷懒不做动图了)

top=height(Tooltip)+hegiht(::before_border)+height(gap)top = height(Tooltip) + hegiht(::before\_border) + height(gap)

小三角相对于气泡的偏移也是类似的计算方法,总之能够根据元素的尺寸让偏移刚好能够居中。


代码


代码如下:(wxml 和 wxss 没有高亮,用 html 和 css 格式代替了)


<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>

.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;

position:absolute;
padding: 20rpx;
top: 80rpx;
left: 55rpx;
}
.prompt-container::before{
position:absolute;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
top: -28rpx;
left: 103rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}

踩坑


美滋滋呀,这就做完了,在 iPhone 12 mini 模拟器上看起来丝毫没有问题,整个气泡内容看起来是那么完美~


12mini


换个最豪(ang)华(gui)的机型(iPhone 14 Pro Max)看看,大事不妙,怎么小三角和气泡之间出现了一条缝,再看看代码按按计算器,算的尺寸没有任何问题呀!但是展示就是变成了这样:


14pm


爬坑


打开调试器一顿猛调试,发现了一些不对劲,下面慢慢说。


关于 rpx


微信小程序提供了个特殊的单位 rpx,代码中也都是使用这个单位进行开发,据说是能够方便开发者在不同尺寸设备上实现自适应布局。
放一张官方文档截图:
wxwd.png
它的意思就是,它把所有的屏幕宽度都设置为 750rpx,不管这个设备真实的宽度有几个设备独立像素(就是宽度有多少 px)。开发者只需要使用 rpx 为单位,小程序会帮你把 rpx 转成 px,听起来是不是很方便很友好~(但并不是🌚)


试试这个公式是不是真的


根据图中提供的转换方式 1rpx=(screenWidth/750)×px1rpx = (screenWidth / 750) \times px
用上面那个例子中的 Tooltip 组件来进行验证,手动算一下在设备上得到的 px 值是不是真的能用上面的公式计算出来。


设备屏幕尺寸 / 750组件理论尺寸真实尺寸
iPhone 12 mini375 / 750 = 0.5Tooltip175 × 32175 × 32
iPhone 14 Pro Max428 / 750 = 0.57Tooltip199.5 × 36.48199× 36

看起来真实的计算会直接省略小数点后的值,直接进行取整。这可能是导致我们的预期和真实展示有偏差的原因。再看看其他组件的尺寸计算:


设备屏幕尺寸 / 750组件理论尺寸真实尺寸
iPhone 12 mini375 / 750 = 0.5Prompt120120
::before14 × 1414 × 14
iPhone 14 Pro Max428 / 750 = 0.57Prompt136.8136
::before15.96 × 15.9614 × 14

因为高度是根据文字自适应的,所以这里没有计算 Prompt 的高度。但依然可以从表中看出,rpx 到 px 的转换并不是简单的直接取整,不然 iPhone 14 Pro Max 中的 ::before 应该尺寸为 15 × 15。至于到底是所有 rpx 到 px 转换都有隐藏的规则,还是伪元素的尺寸转换和其他元素不统一,还是 border 尺寸计算比较特殊,我们也无从得知,官方也没有相关说明。


小三角相对于气泡的偏移


其实在这个例子中,我们最关心的就是这个小三角相对于气泡的偏移是不是符合预期,整体气泡居中与否那么小的差别我们几乎看不出来,但是这个小三角偏离气泡这段距离,搁谁都无法接受。
那着重看下这个小三角的偏移我们是怎么做的,小三角的尺寸完全是由边构成的,完整的矩形尺寸是 28rpx×28rpx28rpx × 28rpx,我们向上的偏移需要设置成整个矩形的尺寸,也就是 top:28rpxtop: -28rpx,这样才能让下半部分的小三角完全展示出来。理论上来说,尺寸和偏移都设置 rpx 为单位,如果使用统一的转换规则,那肯定也是没问题的,既然出现了问题肯定是两者的计算不是那么的统一。我们看到实际的结果,尺寸计算是不符合我们的预期的,那么就猜测偏移可能是按照公式计算的。可以来验证一下,计算得到的值 1515 和真实值 1414 相差 1px1px,我打算放个高度为 1px1px 的长条在这个缝隙里,看看是不是刚好塞进去。
test.png

竟然真的刚好塞进去了,这说明我的猜测应该没有错,偏移的计算在我们预期中,小三角向上移动了 15px。但不能进行更多的验证了,再猜我就要把这个规律猜出来了(手动狗头🤪)。
总之就是,盒子尺寸的计算和偏移距离的计算用的不是一个规律,这就是坑之所在。


解决方案


针对当前需求


我们可以避开这个坑,让小三角相对于气泡不要产生偏移,而是能死死的贴在气泡上。想要达到这样的效果,我们需要修改一下布局结构,改成下面这个样子:


new.png
让气泡整体和小三角形成兄弟关系,那他俩就不会分离了,然后整体的偏移让他们的父节点 Prompt 来决定。


代码如下:


<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle"></view>
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>

.tooltip{
display: flex;
align-items: center;
position:relative;
width:350rpx;
height: 64rpx;
}
.prompt-container{
position:absolute;
top: 80rpx;
left: 55rpx;
}
.prompt-triangle{
position:relative;
content:'';
width: 0rpx;
height: 0rpx;
border: 14rpx solid transparent;
border-bottom-color:rgba(0,0,0,0.7);
left: 103rpx;
}
.prompt-text-container{
background-color: rgba(0,0,0,0.7);
border-radius: 16rpx;
color: #fff;
width: 200rpx;
padding: 20rpx;
}
.prompt-title{
font-size: 26rpx;
font-weight: bold;
}
.prompt-content{
font-size: 24rpx;
}


通用方法


除了上面避坑的方法,还有一个方法就是进行一个填坑。自定义实现一个 rpx2px 方法,动态的根据设备来进行 px 值的计算,再通过内联样式传递给元素。


function rpx2px(rpx){
return ( wx.getSystemInfoSync().windowWidth / 750) * rpx
}
Page({
data: {
top: rpx2px(100), // 类似这样定义一个状态,通过内联样式传入
},
})

<view class="tooltip" >
<button size="mini" bindtap="handleTooltipShow">TOOLTIP</button>
<view wx:if="{{showTooltip}}" class="prompt-container">
<view class="prompt-triangle" style="top:{{top}}px"></view>
<!-- 注意上面👆这里添加了 style 内联样式 -->
<view class="prompt-text-container">
<view class="prompt-title">这是弹窗标题</view>
<view class="prompt-content">这是弹窗内容,啊吧啊吧吧</view>
</view>
</view>
</view>

这样子不管是什么尺寸什么偏移都按照统一的规则进行换算,妈妈再也不同担心我被坑啦~~~


总结


虽然解法看起来如此简单,但是爬坑的过程真是无比艰难,各种猜测和假设,虽然一些得到了验证,但最终也是无法猜透小程序的心~~ 只能自己避坑和填坑,按需选择吧。


作者:用户9787521254131
来源:juejin.cn/post/7257516901843763257
收起阅读 »

Moka Ascend 2024|势在·人为,技术创新,激发企业管理内在效能

2024年6月19日,Moka 在深圳举办的 Moka Ascend 2024 产品发布会圆满落幕。此次大会以「势在·人为」为主题,汇聚了来自不同企业的管理者、人力资源专家和技术创新者,共同探讨和分享他们在人力资源管理、组织效能提升以及全球化招聘等方面的最新实...
继续阅读 »

2024年6月19日,Moka 在深圳举办的 Moka Ascend 2024 产品发布会圆满落幕。此次大会以「势在·人为」为主题,汇聚了来自不同企业的管理者、人力资源专家和技术创新者,共同探讨和分享他们在人力资源管理、组织效能提升以及全球化招聘等方面的最新实践和深刻见解。

Moka 联合创始人兼 CEO 李国兴在题为「势在必行:激发管理效率的内在势能」的主题演讲中,正式发布「Moka 人效管理解决方案」,以回应当前企业对经营和管理的诉求。要看清楚「人效」,管理者不应该只是得到事后汇报和对补救过程做出决策,而是真的需要一个内嵌于HR业务中,涵盖企业人力资源管理全流程,具备人力资源全面效能的工具。「Moka 人效管理解决方案」从成本到人效,从规划到落地,让决策更有依据,让管理更有保障,为企业健康发展[ 控本增效 ],帮助企业更清晰而全面地看到人力资源效能的运作情况,也让过程中的管理价值不断重复循环起来,「看得清」才能更好的「管得住」。

今年1月初,Moka 携手钉钉,推出「钉钉人事旗舰版」,致力于解决中大型企业在 HR 软件的使用过程中面临的系统割裂、数据孤岛等问题。此产品一经推出,对于整个行业来说,都是让人眼前一亮的「HR SaaS 新物种」。

钉钉生态业务部总经理陈霆旭与李国兴一起就合作后的进展和成绩进行了分享。在阐述钉钉开放生态战略合作的构想时,陈霆旭提到,在人事场景方面,Moka 是理想的合作伙伴,我们双方通过深度融合的方式,创新性地满足了客户需求。「钉钉人事旗舰版」之所以能被市场快速验证和接纳,本质还为了更好满足客户价值,大家在价值链上找到了更精细的分工和更清晰的边界,干自己更擅长的事,钉钉有非常丰富的IM、OA、文档、音视频等协同办公能力,也让企业的组织数字化实现零门槛,Moka在人事领域也有非常深的积淀,有了这种强强融合共建的产品能给员工和HR带来前所未有的创新体验,为客户带来「1+1>2」的价值和效果。

除了「看得清」,「看得远」成为企业不断进步与更新的动力。新一代大模型时代到来,AI 技术越来越多的可能性呈现在大众面前。Moka 合伙人兼 CTO 刘洪泽在大会上分享了 Moka 在 AI 领域的技术创新和应用实践。以 AI 原生为理念,深入业务场景,Moka Eva 的能力不断进化,不仅完成了从智能面试到智能招聘解决方案的升级,同时也赋能于全新功能「SmartPractice」,集成多国家和地区招聘最佳实践,智能辅助HR全球招聘,全流程提升招聘效率与招聘质量。

同时,AI 亦全面赋能 Moka 技术平台,构建新一代底层平台能力。伴随着新加坡数据中心的投入使用,以及一直以来与合作伙伴的生态共融,Moka 将与更多客户实现合作共赢,不断向打造世界级HR产品的愿景靠近。

企业成长是个超长周期的动态过程,过程中企业会面临市场内卷、业务难管等挑战,针对企业发展难题,各行业有成功实践经验的管理者提出新解法。

本次大会亦邀请了多位企业管理者分享了他们在人效管理、本地化人才战略和全球化招聘方面的深刻见解和实践经验。来自卓尔数科的CHO屠俊强调了在不确定性环境下,"1+3"模型发挥着重要作用,在应用SOTE模型的同时,构建组织力、业务力和影响力,实现业务与组织管理的深度融合。KLOOK 客路旅行的中国区人力资源总监戴良辰则分享了如何通过业务共创和团队成长两大维度,落地本地招聘,推动业务快速发展。

此外,本次大会还举办了题为”无界之帆:全球化招聘的挑战和实践“的圆桌论坛,由李国兴主持,邀请了携程集团招聘运营负责人梁昊耘、AfterShip Global HRD Flora Zeng和海辰储能招聘总监金一凡,共同探讨海外团队组建的挑战与经验,如何在全球范围内构建雇主品牌,以及如何利用系统和工具优化招聘流程。这些分享不仅展现了中国企业在全球化浪潮中的积极作为,也为其他企业提供了宝贵的参考和启示。

势在人为,破局而出。Moka 未来仍会继续坚持「全员体验更好」的产品理念,响应客户需求,持续优化产品与服务,助力企业实现更高效、更智能的人事管理。同时,Moka 也将积极与各方共同探索人事管理新机遇,在动态的变化中寻求可期待的攀升,保持始终向上的能量。

收起阅读 »

我是如何解决职场内卷、不稳定、没前景的

大家好,我卡颂。 我的读者大部分是职场人,在经济下行期,大家普遍反映混职场艰难。 再深究下,发现造成职场艰难的原因主要有三个: 内卷:狼多肉少 不稳定:裁员总是不期而遇 没前景:明知过几年会被优化,但无法改变 本文根据我的个人经历以及大量案例走访,得出一套...
继续阅读 »

大家好,我卡颂。


我的读者大部分是职场人,在经济下行期,大家普遍反映混职场艰难。


再深究下,发现造成职场艰难的原因主要有三个:



  1. 内卷:狼多肉少

  2. 不稳定:裁员总是不期而遇

  3. 没前景:明知过几年会被优化,但无法改变


本文根据我的个人经历以及大量案例走访,得出一套切实可行的不内卷、高稳定、有前景的职业发展路径。


推荐职场发展遇到卡点的同学阅读。


造成三个问题的原因


要知道问题的解法,首先得了解问题成因。


造成内卷的原因


传统职业发展路径中的目标通常需要竞争,比如:



  • 升职加薪的名额有限

  • 跳槽时目标公司的 HC 有限

  • 在职学历提升的录取人数有限


更有甚者,在行业下行期,一些负面的名额也需要竞争(让自己不被选中),比如:



  • 绩效打C

  • 裁员的名额


问题的本质并不在个人身上,而在依附于公司的职业发展路径上。


我们从小接受的普鲁士教育的目的,是通过考试不断筛选守规则且有专业技能的公民


职场可以理解为这种筛选模式的延续,所以面对职场内卷时,大部分人不会认为这是模式的问题,而是从自身找原因(我是不是还不够努力?)


所以,内卷的本质原因是 —— 传统职业发展路径在有限的资源下充满竞争,有竞争就有加剧内卷。


造成“不稳定”的原因


试问,什么样的工作才是稳定的工作?很多同学会脱口而出:国企、考编。


这些传统意义的铁饭碗稳定的原因是 —— 他们依附于一个稳定的平台。


而造成打工人职业发展不稳定的原因正是如此 —— 他们依附于某个公司。


有人会说:如果公司不行了我可以跳槽啊。


问题是,你跳槽的目的地不也是一家公司嘛。再发散点讲,你职业生涯的每一步都依附于公司。



所以,不稳定的本质原因是 —— 传统职业发展路径都依附于公司存在。在经济下行期,公司这个载体并不稳定。


造成“没前景”的原因


前段时间,我和一个金融业的前辈聊天,他在5年前就做到行长的level,政治斗争失败后离职,现在作为普通职员,在一家银行做执行层面的工作。


见多了这样的例子,不禁让我思考 —— 什么样的工作才是有前景的工作?


我的结论是 —— 在不内卷且稳定的基础上,有复利的工作。


可以做一辈子,且做的越久越吃香的工作老中医就是这样的一份工作:



  • 不内卷:患者是冲着他来的,即使市面上有很多其他中医,也不会对他造成影响

  • 稳定:不会失业

  • 有复利:随着年龄增长,会更有竞争力


抛开现象看本质,老中医这类工作的核心模式是 —— 以专家影响力为基础形成的圈层生意。


剖析“老中医模式”


老中医模式为什么能做到不内卷、高稳定、有前景?让我们一条条分析。


如何做到“不内卷”?


上文提到,内卷源于有限资源下的竞争。所以,不内卷的核心是创造新价值,而不是占有已有资源


新价值可以在“没有人被剥夺价值”的情况下被创造,就像老中医在治好一个病人时,不会有另一个病人为此蒙受损失。


如何做到“高稳定”?


传统职业生涯的不稳定性来源于依附于公司,而老中医并不依附于医院(即使在医院坐诊,也是医院沾了老中医的光)。


那么,老中医依附于什么呢?答案是 —— 中医行业。


中国人几千年来对中医形成的认可,使得依附于行业的中医会获得极大的稳定性,你体会下两者的区别:



  • 依附于公司:我是xx医院的中医

  • 依附于行业:我是xx领域名中医


背后的本质原因是 —— 依附于公司,你的收益完全来源于公司(xx医院的中医,患者是冲着医院来的)。


而依附于行业,你的收益则完全来自行业受众(xx领域名中医,患者是冲着他的领域专业度来的)。


把鸡蛋从公司这个篮子拿出来,放到千千万万从业者的篮子里,这就是稳定性的由来。


如何做到“有前景”?


在不内卷、高稳定性的基础上,随着时间、阅历增长,一名中医的含金量会指数增长。


一位40岁的中医和一位80岁的中医,在老百姓的认知中,后者显然比前者可信赖度高了不止一倍。因为老百姓潜意识里会认为 —— 年龄越大,知识、经验越丰富。


这就是知识的复利


WIN事业工作法


根据老中医模式的特点,我们可以总结一条全新的职业发展路径:



  1. 智慧(Wisdom):以“领域专家”为目标,构建领域智慧

  2. 影响力(Influence):围绕领域智慧打造行业影响力

  3. 圈层(Network):运用影响力吸引关注者,形成圈层


这就是W.I.N事业工作法,一种不内卷、高稳定、有前景的职业发展路径。


在这条新职业路径中:



  1. 没有占有已有资源,而是通过构建领域智慧创造新的价值

  2. 没有依附于公司,而是依附于行业,成为领域专家

  3. 没有将鸡蛋放在公司的篮子里,而是将鸡蛋放在圈层受众手中


总结


传统职业发展路径三大问题的主要成因是:



  1. 内卷:在有限的资源下充满竞争,有竞争就有加剧内卷

  2. 不稳定:依附于公司,公司在行业下行期不稳定

  3. 没前景:内卷且不稳定,还没有复利效应


为了克服这三点,一种可行的方式是“借鉴老中医的职业发展路径”。


这种新职业发展路径被称为W.I.N事业工作法,包括三个步骤:



  1. 智慧(Wisdom):以“领域专家”为目标,构建领域智慧

  2. 影响力(Influence):围绕领域智慧打造行业影响力

  3. 圈层(Network):运用影响力吸引关注者,形成圈层


关于工作法更详细的内容已经开源为免费的电子书《W.I.N事业工作法》,推荐对职业发展有卡点的同学阅读。


作者:魔术师卡颂
来源:juejin.cn/post/7376586649981780005
收起阅读 »

写html页面没意思,来挑战chrome插件开发

web
谷歌浏览器插件开发是指开发可以在谷歌浏览器中运行的扩展程序,可以为用户提供额外的功能和定制化的体验。谷歌浏览器插件通常由HTML、CSS和JavaScript组成,非常利于前端开发者。 开发者可以利用这些技术在浏览器中添加新的功能、修改现有功能或者与网页进行交...
继续阅读 »

谷歌浏览器插件开发是指开发可以在谷歌浏览器中运行的扩展程序,可以为用户提供额外的功能和定制化的体验。谷歌浏览器插件通常由HTML、CSS和JavaScript组成,非常利于前端开发者。
开发者可以利用这些技术在浏览器中添加新的功能、修改现有功能或者与网页进行交互。


要开发谷歌浏览器插件,开发者通常需要创建一个包含*清单文件(manifest.json)、背景脚本(background script)、内容脚本(content script)*等文件的项目结构。清单文件是插件的配置文件,包含插件的名称、版本、描述、权限以及其他相关信息。背景脚本用于处理插件的后台逻辑,而内容脚本则用于在网页中执行JavaScript代码。


谷歌浏览器插件可以实现各种功能,例如添加新的工具栏按钮、修改网页内容、捕获用户输入、与后台服务器进行通信等。开发者可以通过谷歌浏览器插件API来访问浏览器的各种功能和数据,实现各种定制化的需求。
插件开发涉及的要点:


image.png


基础配置


开发谷歌浏览器插件,最重要的文件 manifest.json


{
"name": "Getting Started Example", // 插件名称
"description": "Build an Extension!", // 插件描述
"version": "1.0", // 版本
"manifest_version": 3, // 指定插件版本,这个很重要,指定什么版本就用什么样的api,不能用错了
"background": {
"service_worker": "background.js" // 指定background脚本的路径
},
"action": {
"default_popup": "popup.html", // 指定popup的路径
"default_icon": { // 指定popup的图标,不同尺寸
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
}
},
"icons": { // 指定插件的图标,不同尺寸
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
},
"permissions": [],// 指定应该在脚本中注入那些变量方法,后文再详细说
"options_page": "options.html",
"content_scripts": [ // 指定content脚本配置
{
"js": [ "content.js"], // content脚本路径
"css":[ "content.css" ],// content的css
"matches": ["<all_urls>"] // 对匹配到的tab起作用。all_urls就是全部都起作用
}
]
}


  • name: 插件名称


manifest_version:对应chrome API插件版本,浏览器插件采用的版本,目前共2种版本,是2和最新版3



  • version: 本插件的版本,和发布相关

  • action:点击图标时,设置一些交互

    • default_icon:展示图标

      • 16、32、48、128



    • default_popup:popup.html,一个弹窗页面

    • default_title:显示的标题



  • permissions:拥有的权限

    • tabs:监听浏览器tab切换事件



  • options_ui

  • background:

    • service_worker:设置打开独立页面




官方实例


官方教程


打开pop弹窗页面


设置action的default_popup属性


{
"name": "Hello world",
"description": "show 'hello world'!",
"version": "1.0",
"manifest_version": 3,
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "/images/icon16.png",
"32": "/images/icon32.png",
"48": "/images/icon48.png",
"128": "/images/icon128.png"
}
},
"permissions":["tabs", "storage", "activeTab", "idle"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"js": [ "content.js"],
"css":[ "content.css" ],
"matches": ["<all_urls>"]
}
]
}

创建popup.html


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

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>显示出hello world</title>
<link rel="stylesheet" type="text/css" href="popup.css">
</head>

<body>
<h1>显示出hello world</h1>
<button id="clickBtn">点击按钮</button>
<script src="popup.js"></script>
</body>
</html>

文件可以通过链接引入css、js。


body {
width: 600px;
height: 300px;
}
h1 {
background-color: antiquewhite;
font-weight: 100;
}


console.log(document.getElementById('clickBtn'));
document.getElementById('clickBtn').addEventListener('click', function () {
console.log('clicked');
});

点击插件图标


点击图标可以看到如下的popup的页面。


image.png


调试popup.js的方法



  • 通过弹窗,在弹窗内部点击右键,选择审查内容
    image.png

  • 通过插件图标,进行点击鼠标右键,选择审查弹出内容
    image.png


通过background打开独立页面


基于backgroundservice_workerAPI可以打开一个独立后台运行脚本。此脚本会随着插件安装,初始化执行一次,然后一直在后台运行。可以用来存储浏览器的全局状态数据。
background脚本是长时间运行在后台,随着浏览器打开就运行,直到浏览器关闭而结束运行。通常把需要一直运行的、启动就运行的、全局公用的数据放到background脚本。


chrome.action.onClicked.addListener(function () {
chrome.tabs.create({
url: chrome.runtime.getURL('newPage.html')
});
});


为了打开独立页面,需要修改manifest.json


{
"name": "newPage",
"description": "Demonstrates the chrome.tabs API and the chrome.windows API by providing a user interface to manage tabs and windows.",
"version": "0.1",
"permissions": ["tabs"],
"background": {
"service_worker": "service-worker.js"
},
"action": {
"default_title": "Show tab inspector"
},
"manifest_version": 3
}

为了实现打开独立页面,在manifest.json中就不能在配置 action:default_popup
newPage.js文件中可以使用*chrome.tabs*和chrome.windowsAPI;
可以使用 chrome.runtime.getUrl 跳转一个页面。


chrome.runtime.onInstalled.addListener(async () => {
chrome.tabs.create(
{
url: chrome.runtime.getURL('newPage.html'),
}
);
});

content内容脚本


content-scripts(内容脚本)是在网页上下文中运行的文件。通过使用标准的文档对象模型(DOM),它能够读取浏览器访问的网页的详细信息,可以对打开的页面进行更改,还可以将DOM信息传递给其父级插件。内容脚本相对于background还是有一些访问API上的限制,它可以直接访问以下chrome的API



  • i18n

  • storage

  • runtime:

    • connect

    • getManifest

    • getURL

    • id

    • onConnect

    • onMessage

    • sendMessage




content.js运行于一个独立、隔离的环境,它不会和主页面的脚本或者其他插件的内容脚本发生冲突
有2种方式添加content脚本


在配置中设置


"content_scripts": [
{
"js": [ "content.js"],
"css":[ "content.css" ],
"matches": ["<all_urls>"]
}
]

content_scripts属性除了配置js,还可以设置css样式,来实现修改页面的样式。
matches表示需要匹配的页面;
除了这3个属性,还有



  • run_at: 脚本运行时刻,有以下3个选项

    • document_idle,默认;浏览器会选择一个合适的时间注入,并是在dom完成加载

    • document_start;css加载完成,dom和脚本加载之前注入。

    • document_end:dom加载完成之后



  • exclude_matches:排除匹配到的url地址。作用和matches相反。


动态配置注入


在特定时刻才进行注入,比如点击了某个按钮,或者指定的时刻
需要在popup.jsbackground.js中执行注入的代码。


chrome.tabs.executeScript(tabs[0].id, {
code: 'document.body.style.backgroundColor = "red";',
});

也可以将整个content.js进行注入


chrome.tabs.executeScript(tabs[0].id, {
file: "content.js",
});

利用content制作一个弹窗工具


某天不小心让你的女神生气了,为了能够道歉争取到原谅,你是否可以写一个道歉信贴到每一个页面上,当女神打开网站,看到每个页面都会有道歉内容。


image.png


道歉信内容自己写哈,这个具体看你的诚意。
下面设置2个按钮,原谅和不原谅。 点击原谅,就可以关闭弹窗。 点击不原谅,这个弹窗调整css布局位置继续显示。(有点像恶意贴片广告了)


下面设置content.js的内容


let newDiv = document.createElement('div');
newDiv.innerHTML = `<div id="wrapper">
<h3>小仙女~消消气</h3>
<div><button id="cancel">已消气</button>
<button id="reject">不原谅</button></div>
</div>`
;
newDiv.id = 'newDiv';
document.body.appendChild(newDiv);
const cancelBtn = document.querySelector('#cancel');
const rejectBtn = document.querySelector('#reject');
cancelBtn.onclick = function() {
document.body.removeChild(newDiv);
chrome.storage.sync.set({ state: 'cancel' }, (data) => {
});
}
rejectBtn.onclick = function() {
newDiv.style.bottom = Math.random() * 200 + 10 + "px";
newDiv.style.right = Math.random() * 800 + 10 + "px";
}
// chrome.storage.sync.get({ state: '' }, (data) => {
// if (data.state === 'cancel') {
// document.body.removeChild(newDiv);
// }
// });

content.css布局样式


#newDiv {
font-size: 36px;
color: burlywood;
position: fixed;
bottom: 20px;
right: 0;
width: 300px;
height: 200px;
background-color: rgb(237, 229, 216);
text-align: center;
z-index: 9999;
}

打开option页面


options页,就是插件的设置页面,有2个入口



  • 1:点击插件详情,找到扩展程序选项入口


image.png



  • 2插件图标,点击右键,选择 ‘选项’ 菜单


image.png


可以看到设置的option.html页面


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>插件的option配置</title>
</head>
<body>
<h3>插件的option配置</h3>
</body>
</html>

此页面也可以进行js、css的引入。


替换浏览器默认页面


override功能,是可以替换掉浏览器默认功能的页面,可以替换newtab、history、bookmark三个功能,将新开页面、历史记录页面、书签页面设置为自定义的内容。
修改manifest.json配置


{
"chrome_url_overrides": {
"newtab": "newtab.html",
"history": "history.html",
"bookmarks": "bookmarks.html"
}
}

创建一个newtab的html页面


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>new tab</h1>
</body>
</html>

插件更新后,点开新的tab,就会出现我们自定义的页面。第一次的情况会让用户进行选择,是进行更换还是保留原来的配置。


image.png
很多插件都是使用newtab进行自定义打开的tab页,比如掘金的浏览器插件,打开新页面就是掘金网站插件


页面之间进行数据通信


image.png
如需将单条消息发送到扩展程序的其他部分并选择性地接收响应,请调用 runtime.sendMessage()tabs.sendMessage()。通过这些方法,您可以从内容脚本向扩展程序发送一次性 JSON 可序列化消息,或者从扩展程序向内容脚本发送。如需处理响应,请使用返回的 promise。
来源地址:developer.chrome.com/docs/extens…


content中脚本发送消息


chrome.runtime.sendMessage只能放在content的脚本中。


(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

其他页面发送消息


其他页面需向内容脚本发送请求,请指定请求应用于哪个标签页,如下所示。此示例适用于 Service Worker、弹出式窗口和作为标签页打开的 chrome-extension:// 页面


(async () => {
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();

接收消息使用onMessage


在扩展程序和内容脚本中使用相同的代码


chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting === "hello")
sendResponse({farewell: "goodbye"});
}
);

添加右键菜单


创建菜单


首先在manifest.json的权限中添加配置


{
"permissions": ["contextMenus"]
}


background.js中添加创建菜单的代码


let menu1 = chrome.contextMenus.create({
type: 'radio', // 可以是 【normal、checkbox、radio】,默认是normal
title: 'click me',
id: "myMenu1Id",
contexts:['image'] // 只有是图片时,菜显示
}, function(){

})

let menu2 = chrome.contextMenus.create({
type: 'normal', // 可以是 【normal、checkbox、radio】,默认是normal
title: 'click me222',
id: "myMenu222Id",
contexts:['all'] //所有类型都显示
}, function(){

})

let menu3 = chrome.contextMenus.create({
id: 'baidusearch1',
title: '使用百度搜索:%s',
contexts: ['selection'], //选择页面上的文字
});

// 删除一个菜单
chrome.contextMenus.remove('myMenu222Id'); // 被删除菜单的id menuItemId
// 删除所有菜单
chrome.contextMenus.removeAll();

// 绑定菜单点击事件
chrome.contextMenus.onClicked.addListener(function(info, tab){
if(info.menuItemId == 'myMenu222Id'){
console.log('xxx')
}
})

以下是其他可以使用的api


// 删除某一个菜单项
chrome.contextMenus.remove(menuItemId);
// 删除所有自定义右键菜单
chrome.contextMenus.removeAll();
// 更新某一个菜单项
chrome.contextMenus.update(menuItemId, updateProperties);
// 监听菜单项点击事件, 这里使用的是 onClicked
chrome.contextMenus.onClicked.addListener(function(info, tab)) {
//...
});

绑定点击事件,发送接口请求


首先需要在manifest.jsonhosts_permissions中添加配置


{
"host_permissions": ["http://*/*", "https://*/*"]
}

创建node服务器,返回json数据


// server.mjs
const { createServer } = require('node:http');
const url = require('url');

const server = createServer((req, res) => {
var pathname = url.parse(req.url).pathname;

if (pathname.includes('api')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(
JSON.stringify({
name: 'John Doe',
age: 30,
})
);
res.end();
} else {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World!\n' + pathname);
}
});

server.listen(8080, '127.0.0.1', () => {
console.log('Listening on 127.0.0.1:8080');
});

编辑background.js文件


// 插件右键快捷键
// 点击右键进行选择
chrome.contextMenus.onClicked.addListener(function (info, tab) {
if (info.menuItemId === 'group1') {
console.log('分组文字1', info);
}
if (info.menuItemId === 'group2') {
console.log('分组文字2');
}
// 点击获取到数据
if (info.menuItemId === 'fetch') {
console.log('fetch 获取数据');
const res = fetch('http://localhost:8080/api', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => {
console.log(res, '获取到http://localhost:8080/api接口数据');
chrome.storage.sync.set({ color: 'red' }, function (err, data) {
console.log('store success!');
});
});
}
// 创建百度搜索,并跳转到搜索结果页
if (info.menuItemId === 'baidusearch1') {
// console.log(info, tab, "baidusearch1")
// 创建一个新的tab页面
chrome.tabs.create({
url:
'https://www.baidu.com/s?ie=utf-8&wd=' + encodeURI(info.selectionText),
});
}
});

// 创建右键快捷键
chrome.runtime.onInstalled.addListener(function () {
// Create one test item for each context type.
let contexts = [
'page',
'selection',
'link',
'editable',
'image',
'video',
'audio',
];
// for (let i = 0; i < contexts.length; i++) {
// let context = contexts[i];
// let title = "Test '" + context + "' menu item";
// chrome.contextMenus.create({
// title: title,
// contexts: [context],
// id: context,
// });
// }

// Create a parent item and two children.
let parent = chrome.contextMenus.create({
title: '操作数据分组',
id: 'parent',
});
chrome.contextMenus.create({
title: '分组1',
parentId: parent,
id: 'group1',
});
chrome.contextMenus.create({
title: '分组2',
parentId: parent,
id: 'group2',
});
chrome.contextMenus.create({
title: '获取远程数据',
parentId: parent,
id: 'fetch',
});

// Create a radio item.
chrome.contextMenus.create({
title: '创建单选按钮1',
type: 'radio',
id: 'radio1',
});
chrome.contextMenus.create({
title: '创建单选按钮2',
type: 'radio',
id: 'radio2',
});

// Create a checkbox item.
chrome.contextMenus.create({
title: '可以多选的复选框1',
type: 'checkbox',
id: 'checkbox',
});
chrome.contextMenus.create({
title: '可以多选的复选框2',
type: 'checkbox',
id: 'checkbox2',
});

// 在title属性中有一个%s的标识符,当contexts为selection,使用%s来表示选中的文字
chrome.contextMenus.create({
id: 'baidusearch1',
title: '使用百度搜索:%s',
contexts: ['selection'],
});

// Intentionally create an invalid item, to show off error checking in the
// create callback.
chrome.contextMenus.create(
{ title: 'Oops', parentId: 999, id: 'errorItem' },
function () {
if (chrome.runtime.lastError) {
console.log('Got expected error: ' + chrome.runtime.lastError.message);
}
}
);
});

点击鼠标右键,效果如下


image.png


image.png


如果在页面选择几个文字,那么就显示出百度搜索快捷键,


image.png


缓存,数据存储


首先在manifest.json的权限中添加storage配置


{
"permissions": ["storage"]
}

chrome.storage.sync.set({color: 'red'}, function(){
console.log('background js storage set data ok!')
})

然后就可以在content.js或popup.js中获取到数据


// 这里的参数是,获取不到数据时的默认参数
chrome.storage.sync.get({color: 'yellow'}, function(){
console.log('background js storage set data ok!')
})

tabs创建页签


首先在manifest.json的权限中添加tabs配置


{
"permissions": ["tabs"]
}

添加tabs的相关操作


chrome.tabs.query({}, function(tabs){
console.log(tabs)
})
function getCurrentTab(){
let [tab] = chrome.tabs.query({active: true, lastFocusedWindow: true});
return tab;
}

notifications消息通知


Chrome提供chrome.notifications的API来推送桌面通知;首先在manifest.json中配置权限


{
"permissions": [
"notifications"
],
}

然后在background.js脚本中进行创建


// background.js
chrome.notifications.create(null, {
type: "basic",
iconUrl: "drink.png",
title: "喝水小助手",
message: "看到此消息的人可以和我一起来喝一杯水",
});


devtools开发扩展工具


在manifest中配置一个devtools.html


{
"devtools_page": "devtools.html",
}

devtools.html中只引用了devtools.js,如果写了其他内容也不会展示


<!DOCTYPE html>
<html lang="en">
<head> </head>
<body>
<script type="text/javascript" src="./devtools.js"></script>
</body>
</html>

创建devtools.js文件


// devtools.js
// 创建扩展面板
chrome.devtools.panels.create(
// 扩展面板显示名称
"DevPanel",
// 扩展面板icon,并不展示
"panel.png",
// 扩展面板页面
"Panel.html",
function (panel) {
console.log("自定义面板创建成功!");
}
);

// 创建自定义侧边栏
chrome.devtools.panels.elements.createSidebarPane(
"Sidebar",
function (sidebar) {
sidebar.setPage("sidebar.html");
}
);

然后在创建自定的Panel.html和sidebar.html页面。


相关代码下载


作者:北鸟南游
来源:juejin.cn/post/7350571075548397618
收起阅读 »

程序员提高效率的 10 个方法

1. 早上不要开会 📅 每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说? 因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比...
继续阅读 »

1. 早上不要开会 📅


每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说?


因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比如编程、学习外语等,如果把时间浪费在开会、刷手机等低专注度的事情上,那么就会白白浪费早上的价值。


2. 不要使用番茄钟 🍅


有时候在专心编程的时候,会产生“心流”,心流是一种高度专注的状态,当我们专注的状态被打破的时候,需要 15 分钟的时候才能重新进入状态。


有很多人推荐番茄钟工作法,设定 25 分钟倒计时,强制休息 5 分钟,之后再进入下一个番茄钟。本人在使用实际使用这种方法的时候,经常遇到的问题就是刚刚进入“心流”的专注状态,但番茄钟却响了,打破了专注,再次进入这种专注状态需要花费 15 分钟的时间。


好的替换方法是使用秒表,它跟番茄钟一样,把时间可视化,但却是正向计时,不会打破我们的“心流”,当我们编程专注度下降的时候中去查看秒表,确定自己的休息时间。


3. 休息时间不要玩手机 📱


大脑处理视觉信息需要动用 90% 的机能,并且闪烁的屏幕也会让大脑兴奋,这就是为什么明明休息了,但是重新回到工作的时候却还是感觉很疲惫的原因。


那么对于休息时间内,我们应该阻断视觉信息的输入,推荐:



  • 闭目养神 😪

  • 听音乐 🎶

  • 在办公室走动走动 🏃‍♂️

  • 和同事聊会天 💑

  • 扭扭脖子活动活动 💁‍♂️

  • 冥想 or 正念 🧘


4. 不要在工位上吃午饭 🥣


大脑经过一早上的编程劳累运转之后,此时的专注度已经下降 40%~50%,这个时候我们需要去重启我们的专注度,一个好的方法是外出就餐,外出就餐的好处有:



  • 促进血清素分泌:我们体内有一种叫做血清素的神经递质,它控制着我们的睡眠和清醒,外出就餐可以恢复我们的血清素,让我们整个人神经气爽:

    • 日光浴:外出的时候晒太阳可以促进血清素的分泌

    • 有节奏的运动:走路是一种有节奏的运动,同样可以促进血清素分泌



  • 激发场所神经元活性:场所神经元是掌控场所、空间的神经细胞,它存在于海马体中,外出就餐时场所的变化可以激发场所神经元的活性,进而促进海马体活跃,提高我们的记忆力

  • 激活乙酰胆碱:如果外出就餐去到新的餐馆、街道,尝试新的事物的话,可以激活我们体内的乙酰胆碱,它对于我们的“创作”和“灵感”起到非常大的作用。


5. 睡午觉 😴


现在科学已经研究表现,睡午觉是非常重要的一件事情,它可以:



  • 恢复我们的身体状态:26 分钟的午睡,可以让下午的工作效率提升 34%,专注力提升 54%。

  • 延长寿命:中午不睡午觉的人比中午睡午觉的人更容易扑街

  • 预防疾病:降低老年痴呆、癌症、心血管疾病、肥胖症、糖尿病、抑郁症等


睡午觉好处多多,但也要适当,15 分钟到 30 分钟的睡眠最佳,超过的话反而有害。


6. 下午上班前运动一下 🚴


下午 2 点到 4 点是人清醒度最低的时候,10 分钟的运动可以让我们的身体重新清醒,提高专注度,程序员的工作岗位和场所如果有限,推荐:



  • 1️⃣ 深蹲

  • 2️⃣ 俯卧撑

  • 3️⃣ 胯下击掌

  • 4️⃣ 爬楼梯(不要下楼梯,下楼梯比较伤膝盖,可以向上爬到顶楼,再坐电梯下来)


7. 2 分钟解决和 30 秒决断 🖖


⚒️ 2 分钟解决是指遇到在 2 分钟内可以完成的事情,我们趁热打铁把它完成。这是一个解决拖延的小技巧,作为一个程序员,经常会遇到各种各样的突发问题,对于一些问题,我们没办法很好的决策要不要立即完成, 2 分钟解决就是一个很好的辅助决策的办法。


💣 30 秒决断是指对于日常的事情,我们只需要用 30 秒去做决策就好了,这源于一个“快棋理论”,研究人员让一个著名棋手去观察一盘棋局,然后分别给他 30 秒和 1 小时去决定下一步,最后发现 30 秒和 1 小时做出的决定中,有 90% 都是一致的。


8. 不要加班,充足睡眠 💤


作为程序员,我们可能经常加班到 9 点,到了宿舍就 10 点半,洗漱上床就 12 点了,再玩会儿手机就可以到凌晨 2、3 点。


压缩睡眠时间,大脑就得不到有效的休息,第二天的专注度就会降低,工作效率也会降低,这就是一个恶性循环。


想想我们在白天工作的时候,其实有很多时间都是被无效浪费的,如果我们给自己强制设定下班时间,创新、改变工作方式,高效率、高质量、高密度的完成工作,那是否就可以减少加班,让我们有更多的自由时间去学习新的知识技术,进而又提高我们的工作效率,形成一个正向循环。


9. 睡前 2 小时 🛌



  1. 睡前两小时不能做的事情:

    • 🍲 吃东西:空腹的时候会促进生长激素,生长激素可以提高血糖,消除疲劳,但如果吃东西把血糖提高了,这时候生长激素就停止分泌了

    • 🥃 喝酒

    • ⛹️ 剧烈运动

    • 💦 洗澡水温过高

    • 🎮 视觉娱乐(打游戏,看电影等)

    • 📺 闪亮的东西(看手机,看电脑,看电视)

    • 💡 在灯光过于明亮的地方



  2. 适合做的事情

    • 📖 读书

    • 🎶 听音乐

    • 🎨 非视觉娱乐

    • 🧘‍♂️ 使身体放松的轻微运动




10. 周末不用刻意补觉 🚫


很多人以周为单位进行休息,周一到周五压缩睡眠,周末再补觉,周六日一觉睡到下午 12 点,但这与工作日的睡眠节奏相冲突,造成的后果就是星期一的早上起床感的特别的厌倦、焦躁。


其实周末并不需要补觉,人体有一个以天为单位的生物钟,打破当前的生物钟周期,就会影响到下一个生物钟周期,要调节回来也需要花费一定时间。


我们应该要以天为单位进行休息,早睡早起,保持每天的专注度。


参考


以上大部分来源于书籍 《为什么精英都是时间控》,作者桦泽紫苑,是一个脑神经专家。


作者:吴楷鹏
来源:juejin.cn/post/7253605936144285757
收起阅读 »

我转产品了-前端转产品是一种什么样的体验

程序之路 入门前端的 3 年,前端技术从 pug/handlebars/jquery 制作各种企业官网,再到 gulp/vue/react/webpack 的工程化开发后台管理、 webapp 。然后是 node/express/koa ,开始涉及全栈。 代码...
继续阅读 »

程序之路


入门前端的 3 年,前端技术从 pug/handlebars/jquery 制作各种企业官网,再到 gulp/vue/react/webpack 的工程化开发后台管理、
webapp 。然后是 node/express/koa ,开始涉及全栈。
代码管理工具也从 svn 到 git ,然后制定提交规范,分支管理规范,结合 gitflow/githook 以及各种 lint 保证团队开发风格及可维护
性。
产品发布的方式从 ftp 上传,到 npm/nodejs/shell 脚本,然后再到 jenkins/docker/git 多分支多环境部署。


从第 3 年之后就感觉技术没什么提升了 ,后面都是在各个小作坊担任前端组长角色(其实感觉就是救火队长),哪里项目急去哪里,哪里有难题去哪里。实际比 UI、比测试、比实习产品的地位还低,基本没有话语权。



为什么转产品


严格来说,并不是专门的喜欢产品这个职位,而是希望了解产品经理所做的事。因为在软件开发的工作里,工作的内容和返工程序大大取决与产品对用户需求的理解能力,业务熟悉能力。而作为前端,经常只集中精力在处理页面还原、交互实现、数据对接、浏览器兼容等工作上面。对整个系统的业务逻辑是比较片面的。


如果对用户需求和产品业务有所了解,那可能在开发之前就能发现需求上的不必要性,发现设计上的错误,而减少程序开发的返工率。


总的说来,是期望:



  • 拒绝无效编程

  • 深入理解业务

  • 培养跨部门沟通能力

  • 培养产品设计能力


是否适合转产品


根据上面所说的几点理解,我自身而言并不拒绝,这是在心理方面。


在能力方面,我认为我是可以去学习和培养得到这份能力的。因为自己做的一个程序库 demo,得到了第一份前端工作。前端工作 2 年后,老板尝试让我做产品,并在过程中得到老板的一些建议。1、做产品就不要去考虑程序实现;2、如果自己是对的,就要去坚持,争执得面红耳赤也没有关系。对于这两点建议现在我是如何理解的,后面我会讲。


在习惯方面,我经常会吐槽 xx 产品应如何实现,经常觉得 xx 产品很难用,也经常自己开发 xx 小工具。当然这里我想说:人人都是产品经理,我是认可这句话的。因为产品的受众就是大众,而大众的感受就是产品。至于我自己的 xx 小工具,当然也会被吐槽,不过我觉得这并不影响“喜欢做产品”这个习惯,而做出好产品,是在做产品的过程中去获得的能力。


如何得到这份工作


严格意义上的这份工作,大家都知道一般而言薪资是比开发要低一些的。我说下我能给到的:



  • 接受作为入门岗产品的薪资,不考虑自己的开发经验的工资

  • 能陪开发一起加班,一起赶项目

  • 能在与客户的需求讨论阶段,通过自己的开发经验给出符合客户所需和较低开发成本的解决方案

  • 能处理好产品核心的工作,例如需求文档、原型设计等(仅限于我当前对产品职务的了解)

  • 必要时可为前端团队提供技术方案


真正意义的产品岗的入门工作


我入职这家公司,公司管理层有征求我的意见。问公司现在缺产品,把我拉来填这个位置,问我的想法之类。我接受之后,在这个公司的职位就正式为产品了。


前期的工作,是与另一个兼职的产品去客户现场去了解需求,我做会议纪要,每场会我都在。


领导的意思是,因为兼职的那个产品可能会照顾不到。所以期望我今后能全权接受他手上的项目和往后的产品项目。


另一个公司的项目是两个团队开发的,公司一个团队,公司外部一个团队。这个项目有二期,计划我来接手二期。因为一期临近上线,把我接去做测试,说是我也刚好可以熟悉一下这个项目。虽然在之前我的理解中,产品就是产品,测试就是测试,心里多少有一点抵触。但想到确实在测试过程中多少可以增加对系统的了解,也坦然接受了。


然后在临近上线时,客户认为当前的产品流程不符合需要。需要修改流程,还要增加一个额外的流程。本来项目时间有所有延后,又加上客户添加需求,所以双方决定延后半月上线,但要添加新的流程以及再加一个二期功能。


这个二期功能中有一个拓客功能就是我将来要设计的模块。现在相当于我要提前介入。


不过好在这个系统的客户都还比较好相处,在客户现场做测试、改 BUG、讨论需求的这几天里,经常各种好吃好喝的东西都拿过来。饭点也问大家的口味情况,不重样的给大家点餐。系统有不少的问题,客户也没发脾气(这个我至今没理解)。


一个拓客模块原型


在我的构思中,是打算把整个拓客功能高度抽象化,尽量减少与原系统的耦合度。希望将来其他系统能便于复用这个模块,因为拓客功能是面向 C 端程序常见需求,并且流程也容易标准化。


所以构思了很多东西。


当与客户讲了这些东西之后,客户表示很多东西都有考虑到位。当然也有客户的自己侧重点的东西和必要上的东西的考量,这些东西在前期可能作为产品是比较难感知到的。



与客户讨论需求的部分心得


心得来源于分歧。


虽然这次需求沟通总的来说达到了自己的预期。但这边负责人后面批评了我,所为什么我要给客户讲这么多东西?为什么要答应他们?我们做不完!


我说我没有答应他们什么,我只是尽可能的去了解客户想做的,和让客户知道我想做的。后面我有意识到,由于这个模块是在这半月之类要临时加上去的,负责人害怕客户会认为我给他讲的那些功能就是这半月之类要上的功能。


所以,在这种情况下,在与客户表达功能的同时,要避免客户对功能产生错误的预期。


所以我后面单独找客户聊了,由于时间紧迫,之前给他讲的那些功能并不能完全实现。然后给他展示我这边能给到的一个满足他拓客条件的简化版本。客户表示理解,欣然接受,这个简化版本也与团队进行了同步,没什么问题。


另外,对于一个功能的实现,有很多做法和分支。我们不用一开始做得很细,当与客户沟通,得到客户想做的方向之后(当然客户想做的方向不一定正确,而如何能提前知道客户的方向不正确,这可能是更上一层的能力,比客户更了解客户所面临的问题)。


一个需求文档


拓客所处的项目第一期进行了近一年左右,神奇的是居然没有还没有需求文档。现在项目要上线了,负责人要去找客户结账了才想到要这文档。然后这文档让我来写,对于半路介入这个项目并且刚试岗这个职位的我来说简直头皮发麻。因为据我了解需求文档这东西巨细无遗,需要深入到系统的每个流程和细节。


谁让我现在是这个角色,我不入地狱谁入地狱?随后我反手就找公司把公司的需求文档模板发我一下。模板发了,但我一眼看过去,只知道需要填些什么内容,像是一个骷髅,却想不有内容的样子应会是怎样的,不知道一个有血肉甚至是有灵魂的样子是怎样的。


然后又让公司把以前的其他项目发我一份。然后公司随手发我一份,我打开一看,好家伙,161 页,部分内容如下:



以我之前的了解,需求文档这东西主要是用于验收的(实际开发中需求文档根本来不及跟上需求的变化)。而验收时为了表达工作量,需求文档通常都是内容越多越好。


所以这真也是个体力活。


为了让需求文本能与现有的实现相符合,我打开了现在的系统,现在的系统有些流程还跑不通,然后又根据我的之前的测试结果和现有原型的理解,进行梳理,先把页面和功能拉出来,大概如下:


# 后台管理系统
- 登录
- 用户名
- 密码
- 验证码
- 记住密码
- 系统管理
- 区域架构
- 展开和折叠
- 上级区域
- 名称
- 排序
- 状态是启用还是停用
- 区域层级
- 搜索 -- 名称、层级、状态
......
......
......

# 小程序
- 推广中心
- 统计面板
- 奖励总金额 -- 考虑隐私问题暂不展示应邀人员的细目
- 注册人数 -- 考虑隐私问题暂不展示应邀人员的细目
- 去提现 -- 跳转到体现页面
- 去提现
- 展示总的可提现金额
- 输入想提现的金额发起提现申请
- 展示提现申请记录

- 登录
- 有手机号时授权登录
- 无需要号时通过验证码登录,并进行实名认证
- 设置安全密码
......
......
......


然后根据页面和功能点去展开描述。具了解,需求文档需要包含以下内容:


- 产品概述
- 功能概述
- 用户需求
- 功能分析
- 非功能性需求
- 界面设计
- 数据需求
- 约束和假设

而在功能需求中,有几点是常见的:


- 功能概述
- 功能分析
- 界面设计
- 数据需求

看起来就是功能概括是怎样的?功能具体是怎样的?界面怎样的?数据库设计是怎样的?


很明显,数据库设计这个我暂时细致不了,而且我看现有的需求文档中也不是每个功能都把数据库设计放上去的。总之我认为,能基本把功能描述清楚,看起来够分量就行啦。


那么基于上面我列出的功能结构,例如:


- 登录
- 用户名
- 密码
- 验证码
- 记住密码

是很容易能推导出来:


- 功能概述
- 功能分析
- 界面设计

这东西的:


### 功能概述
本功能旨在提供用户登录系统的功能,包括输入用户名、密码和验证码,并提供记住密码的选项。

### 功能分析
用户登录功能主要涉及以下几个要素:

1. 用户名:用户需要输入其注册时使用的用户名。
2. 密码:用户需要输入与用户名对应的密码。密码应该以安全的方式进行存储和传输,例如使用哈希算法进行加密。
3. 验证码:为了增加登录的安全性,可以添加验证码功能,要求用户输入验证码。验证码通常是由字母和数字组成的随机字符串,用于验证用户的真实性。
4. 记住密码:提供一个选项,让用户选择是否记住密码。如果用户选择记住密码,下次登录时系统会自动填充用户名和密码。

### 界面设计
用户登录界面应包含以下元素:

- 用户名输入框:用于输入用户名。
- 密码输入框:用于输入密码。密码应以隐藏或替代字符的形式显示。
- 验证码输入框:用于输入显示的验证码。
- 验证码图片:用于显示验证码的图像,以便用户看到并输入。
- 记住密码复选框:用于让用户选择是否记住密码。
- 登录按钮:用户点击此按钮以提交登录表单并尝试登录系统。


然后我就以这种方式完成了 98 页的需求文档,这样应该能先交差一版了。


image.png


作者:四叶草会开花
来源:juejin.cn/post/7283766477802864675
收起阅读 »

到底怎样配色才能降低图表的可读性?

web
点赞 + 关注 + 收藏 = 学会了 本文简介 在数据可视化的世界里,图表是我们最常用的语言。但你是否曾被一张图表的配色误导? 配色方案的选择往往被看作是一种艺术,但其实它更是一门科学。 文章将带你一探究竟,哪些配色选择实际上会削弱图表的表达力,甚至误导读者。...
继续阅读 »

点赞 + 关注 + 收藏 = 学会了


本文简介


在数据可视化的世界里,图表是我们最常用的语言。但你是否曾被一张图表的配色误导?


配色方案的选择往往被看作是一种艺术,但其实它更是一门科学。


文章将带你一探究竟,哪些配色选择实际上会削弱图表的表达力,甚至误导读者。


过于丰富的颜色


我管理着10家酒店。以下是这10家酒店在2023年里的收入数据。


1月2月3月4月5月6月7月8月9月10月11月12月
酒店A134501360013200135001370013350136501340013800132001360013700
酒店B680071007300690072007400700073007500760071007200
酒店C152001490015100148001500014700152001490015100148001500014700
酒店D830085008100840086008200850083008600840085008600
酒店E118001200012200121001190012100122001200011800121001190012000
酒店F790081007700830078008400800082008100830084008200
酒店G146501440014700145001480014400146001470014500144001460014800
酒店H550057005800560059005750580059505600590057005800
酒店I143001400014200141001430014200140001410014300142001410014300
酒店J960094009800950097009600990094009800950097009600

我想按月对比酒店G酒店I的收入,并且能直观的知道这两家酒店在所有酒店中的收入属于什么水平。


如果按下图这样展示,对吗?


01.png


粗略一看,这图的数据还挺丰富的,色彩也挺吸引眼球。但你花了多久才找到酒店G酒店I


我们使用 Echarts 等图表库时,通常都会在页面中展示图例。如果想看酒店G酒店I的数据,那我们把其他酒店的数据隐藏掉就行了。


02.png


这样确实能很直观的看到酒店G酒店I的收入趋势和对比。


但把其他酒店的数据隐藏了,又观察不到这两家酒店在所有酒店中的收入水平。


更好的做法是将其他酒店的颜色设置为灰色。


03.png


灰色是一个不起眼的颜色,非常适合用来展示“背景信息”,它不像其他颜色那样吸引眼球。


在上面这个例子中,灰色的主要作用是描述“大环境”,用来凸显想要强调的信息。


但在实际项目中,如果页面的背景色不是白色,又想做到上面这个例子的效果,那可以在页面背景色的基础上往“白色”或者“黑色”方向调色。


04.png


比如,圆点是页面的背景色,红框部分就是可以选择的“背景信息”的颜色。


现在回过头来看看为什么会出现色彩丰富的图表。


我猜有两种可能。


一是项目需求,比如做To G的大屏项目,通常需要炫酷的特效和丰富的色彩去吸引甲方眼球。


二是设计工具或者前端的图表库默认提供了丰富的颜色,开发者只管把数据丢给图表库使用默认的配色去渲染。


配色始终不如一


同一个数据,在不同页中使用了不同的配色方案。用户会觉得你的产品很不专业,也很难培养用户习惯和对品牌的认知。


举个例子,在下方这个图中,顶部的柱状图和下方3个折线图的配色完全不一样。


05.png


反传统的配色


我们的产品支持微信和支付宝这两个支付方式,我们都知道支付宝的主色是蓝色,微信的主色是绿色。


在统计支付来源的数据时,如果出现反传统的配色就会影响用户对数据的理解。


06.png


再错得离谱点的话,可能会将支付宝和微信的主色对掉。


07.png




IMG_0393.GIF


点赞 + 关注 + 收藏 = 学会了


作者:德育处主任
来源:juejin.cn/post/7383268946819792911
收起阅读 »

浅谈放弃出国读研回忆录

那件事回想起来依然内心波澜起伏,那是2017年的冬天,那天风很大,信息工程学院2017级研究生群突然发了一条公告,关于《瑞典林奈大学双学位硕士项目推荐的通知会》,辅导员说感兴趣的同学可以去参加一下宣讲会。看完消息后,我久久不能平复。感谢qq群一直保存着群文档,...
继续阅读 »

那件事回想起来依然内心波澜起伏,那是2017年的冬天,那天风很大,信息工程学院2017级研究生群突然发了一条公告,关于《瑞典林奈大学双学位硕士项目推荐的通知会》,辅导员说感兴趣的同学可以去参加一下宣讲会。看完消息后,我久久不能平复。感谢qq群一直保存着群文档,现在我还能找到它。


mmexport1718958470859.jpg


真要从头说起,那是从我17年的春天成功收到上海一所高校的研究生录取通知书开始的。外地朋友不太理解从河南考到上海有多难,谈高考只有河南人自己懂。家人朋友为我感到骄傲,说我前途无量,以后在上海混出人头地了,别忘记曾经的河南四线城市的这帮老朋友。那年我真的以为考上了一线城市的研究生了,我就真的前途似锦了。经历了没有任何想法无所事事的暑假后(总结起来就干了一件事-35天拿了驾-照),2017年9月10号正式开学,然后命运的齿轮随着我乘坐的从河南开往上海的那趟火车开始转动了。我拎着大包小包的行李箱,随着校友的指引来到了研究生公寓,三人一间,有空调有独卫有阳台,比起我在河南的大学宿舍条件,档次直接提升了一个level,那会我别提有多新鲜了,新的面孔,新的环境,还是在一线魔都,这让我一个出身在河南落后小县城的普通家庭出身的女孩子对一切都感到新奇和欣喜。没有晚自习我就办了一张健身卡每天晚上去锻炼,周末没事就去市中心逛街,魔都的繁华,让我像刘姥姥进大观园一样。就连偶尔去泡的学校图书馆都比本科学校的长的像马桶的图书馆看起来更气派,甚至还有自己的独立自习室,每天跟同门师兄弟几人有说有笑的共同学习,那段时光大概是我到上海以后最无忧无虑的日子了。


面对当时研究生群里发的出国留学项目,我激动又向往,开始认真了解了起来,首先,学校的位置在典瑞,学校qs排名百度上说一百多位,还算可以,最后也是最重要的:学费和生活费


截屏2024-06-21 16.56.15.png
截屏2024-06-21 16.56.35.png

(107000学年 + 40320 * 2学期 + 机票)* 2年 约等于 40w


给大家科普一下19年的40w+什么概念,那会所有城市房子价格疯涨,相当于二线城市一套房子首付,普通家庭的全部家底,一个计算机专业的研究生省吃俭用996加班至少2-3年攒下的全部积蓄。


看完学费后犹豫了,父亲在电话里说支持我,就算是砸锅卖铁也愿意。我知道父亲就算是找亲戚借钱也真的能做到。但是我考虑了很久,花光家里的钱出国读书后,我知道眼界+经历是非常珍贵的,但是代价要牺牲父母的所有积蓄,更何况我还不是独生女,家里还有一个同胞在读书,怎么办,怎么选择?我做不到那么果断和自私。


可是机会真的很难得,读了国内也保留学籍,回国可以拿国内国外双硕士学位,3年拿双硕士性价比非常高,过了这村就没了这店了,这让我一个出身在18线小县城的穷学生非常心动又纠结。努力改变,但是面对经济状况,望而却步。辗转反侧了几周后,最后我默默的选择了放弃。安慰自己说父母不容易,操劳了一辈子,不让家里人负担太重。别的同学留学,可能是家里伸伸手就能够的着,我留学,是父母削尖脑袋往上挤才能做得到。这就是差距,我的自尊心,敏感又脆弱,面对现实,卑微又无奈。


后来在魔都打拼了多年,买了房,买了车,定居。今天上班开车路过家附近一所国际学校时勾起了自己陈年往事,如今回想起来,依然双眼湿润,内心久久不能平复。依然不禁遐想,假如当初咬咬牙,选择出国读书了会怎么样?是不是工资比现在高?圈子和眼界比现在精彩,人生体验会不会更圆满,等等一切遗憾的疑问句.......


但过去了终究是过去了,现在看这区区40万,并不是大数字,但多对于当时一个穷学生来说,简直就是一块沉重的大石头压在父母的肩膀上,压弯了腰,压不断精神的脊柱但能压断身体的脊椎。至少现在家里的同胞也成功毕业结了婚,对于父母来说,没有任何负担了。如果回到过去,我依然坚持自己的选择:放弃出国读研。


我对自己暗暗发誓说:我一定不会让自己的孩子经历我曾经经历的这些,存够至少100-200万再生孩子,让下一代有做选择的资本,而不是被现实所禁锢住飞翔的翅膀。不让自己的出身局限了刻在骨子里的认知,至少不要像我这样自卑无助,能给下一代提供自由发挥的环境,引导式教育,希望孩子勇敢自信独立,大胆的体验人生。


我想每一位当父母的都想给自己的孩子最好的,都希望下一代更有出息。在育儿的这条路上理解父母,成为父母。


作者:为了WLB努力
来源:juejin.cn/post/7382879981527400467
收起阅读 »

勇敢的人先拿到结果

上周许久未见的大学学长叫我出去喝酒,他这次来贵阳是为开分店的事情而来的,他比我高一个年级,在我毕业的时候,他就自己开始做生意了,短短两三年,到现在他已经开了七八个分店了,还在不断发展,并且加盟的人也不少,平均下来,现在每个月的收入也是很可观的。 对于我们这种末...
继续阅读 »

上周许久未见的大学学长叫我出去喝酒,他这次来贵阳是为开分店的事情而来的,他比我高一个年级,在我毕业的时候,他就自己开始做生意了,短短两三年,到现在他已经开了七八个分店了,还在不断发展,并且加盟的人也不少,平均下来,现在每个月的收入也是很可观的。


对于我们这种末流二本院校毕业的学生,特别还是在贵州这个经济相对比较落后的地区,拿到这个成绩还是挺厉害的,并且这个收入并不是固定的,还是不断增长。


学长是学市场营销的,这也算是个天坑专业,所以那会他就知道自己将来肯定是从事不了这个行业的,所以自己就在宿舍开了一个小卖部,每天下课后就骑着电瓶车去送货,虽然每个月赚不了多少钱,但是对于做生意这一块,他的思维肯定是得到了锻炼。


因为我们是在广西读书,所以螺蛳粉就比较多,在毕业后,他就去柳州考察做螺蛳粉,联系好各种渠道后,回到贵州就直接开干。


因为那会贵州的各个市里面卖螺蛳粉的还很少,并且没有特色和品牌效应,所以自己就先设计名称,logo,最后先开了一个店铺,自己亲自下厨,因为比较有特色,一个月直接干到了全市螺蛳粉餐饮销量的第二名。


随后又开了第二家,第三家......别人在看到他赚了钱后,其它市区的人也纷纷向他学习,他自己就收加盟费用,现在他要做的事情就是玩,还有考察门店,然后扩展。


从他的事迹中,我说两个点。


勇于放弃


对于很多人而言,读书的目的就是为了找一份稳定的工作,最好是体制内。


如果你读完大学后出去做销售,做生意,那么对于你身边的很多人而言,他们会觉得你这个大学白读了,因为在他们眼中,只有坐在办公室里面才是最体面了。


你和他说做生意,创业这些东西,他会给你说:这些不稳,以后没有退休工资。


但是如果你真听他们的,那么后面后悔的一定是你。


就像学长,如果他也和别人一样毕业后回到自己那地方加入考编大军,那么他现在肯定和别人一样,也在背书,焦虑,但是他选择了其它的路。


这时候有些人就会抬杠:考上了就能吃一辈子,而你做生意如果运气不好那么就直接亏光,到时候你就知道编制的香了。


这也是很多人的通病。


我觉得如果一件事情你看不到希望,就别过于去迷恋它,舍不得它,不然会被它束缚,比如学历,经验等等。


敢想敢干


可能你会觉得他家里应该有底子的,不然毕业后怎么就能开店。


但是我们问一下自己,就算你家里有底子,毕业后就给你十万块让你开店,你觉得你行吗?恐怕大部分人都不知道自己该做什么吧。


首先躬身入局本身就是一件很难的事情,我们多数人能够拼命上班,但是如果让你脱离平台去自己干一件事就比登天还难。


因为你在公司有别人给你安排好,你去做就行了,换句话来说,你就是个干苦力的,真让你去谈判,去闯市场,大多数人是没这个能力的。


这也是一种损失厌恶心态,因为你怕自己花时间去做,到后面不仅亏了钱,还把自己弄得很累,而安安稳稳打工不一样,它是“稳赚不赔”的。


但是这个世界上很难有稳赚不赔的东西,就说安安稳稳打工拿工资,但是工资不高,那一定是在亏着走的,除非你觉得自己的时间毫无价值,那么就是赚的。


作者:苏格拉的底牌
来源:juejin.cn/post/7340898858556178432
收起阅读 »

解决小程序web-view两个恶心问题

web
1.web-view覆盖层问题 问题由来 web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。 所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。 解决办法 web-view内部使用cover-...
继续阅读 »

1.web-view覆盖层问题


问题由来


web-view 是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。



所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。



解决办法


web-view内部使用cover-view,调整cover-view的样式即可覆盖在web-view上。


cover-view


覆盖在原生组件上的文本视图。


app-vue和小程序框架,渲染引擎是webview的。但为了优化体验,部分组件如map、video、textarea、canvas通过原生控件实现,原生组件层级高于前端组件(类似flash层级高于div)。为了能正常覆盖原生组件,设计了cover-view。


支持的平台:


AppH5微信小程序支付宝小程序百度小程序

具体实现


<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
</view>

</template>

.close-view{
position: fixed;
z-index: 99999;
top: 30rpx;
left: 45vw;

.close-icon{
width: 100rpx;
height: 80rpx;
}
}

代码说明:这里的案例是一个关闭按钮图标悬浮在webview上,点击图标可以关闭当前预览的webview。


注意


仅仅真机上才生效,开发者工具上是看不到效果的,如果要调整覆盖层的样式,可以先把web-view标签注释了,写完样式没问题再释放web-view标签。


2.web-view导航栏返回


问题由来



  • 小程序端 web-view 组件一定有原生导航栏,下面一定是全屏的 web-view 组件,navigationStyle: custom 对 web-view 组件无效。


场景


用户在嵌套的webview里填写表单,不小心按到导航栏的返回了,就全没了。


解决办法


使用page-container容器,点击到返回的时候,给个提示。


page-container


页面容器。


小程序如果在页面内进行复杂的界面设计(如在页面内弹出半屏的弹窗、在页面内加载一个全屏的子页面等),用户进行返回操作会直接离开当前页面,不符合用户预期,预期应为关闭当前弹出的组件。 为此提供“假页”容器组件,效果类似于 popup 弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用 navigateBack 接口。


具体实现


<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
<!--这里这里,就这一句-->
<page-container :show="isShow" :overlay="false" @beforeleave="beforeleave"></page-container>
</view>

</template>

export default {
data() {
return {
isShow: true
}
},
methods: {
beforeleave(){
this.isShow = false
uni.showToast({
title: '别点这里',
icon: 'none',
duration: 3000
})
}
}
}


结语


算是小完美的解决了吧,这里记录一下,看看就行,勿喷。


作者:世界哪有真情
来源:juejin.cn/post/7379960023407198220
收起阅读 »

React Native新架构:恐怖的性能提升

web
自2018年以来,React Native团队一直在重构其核心架构,以便开发者能够创建更高质量更好性能的体验。最近在 React Native 的官网看到他们在安利他们的新的架构,本文将我所了解到的一些皮毛带给大家。以浅薄的见解来揭示其所带来的显著的性能改进,...
继续阅读 »

新架构


自2018年以来,React Native团队一直在重构其核心架构,以便开发者能够创建更高质量更好性能的体验。最近在 React Native 的官网看到他们在安利他们的新的架构,本文将我所了解到的一些皮毛带给大家。以浅薄的见解来揭示其所带来的显著的性能改进,并探讨为何以及如何过渡到这一新架构。


为什么需要新的架构?


多年来,使用React Native构建应用遇到了一些不可避免的限制。比如:React Native的布局和动画效果可能不如原生应用流畅,JavaScript和原生代码之间的通信效率低下,序列化和反序列化开销大,以及无法利用新的React特性等。这些限制在现有架构下无法解决,因此新的架构应运而生。新的架构提升了React Native在数个方面的能力,使得一些之前无法实现的特性和优化成为可能。


同步布局和效果


对比下老的架构(左边)和新的架构(右边)的效果:


React


构建自适应的UI体验通常需要测量视图的大小和位置并进行调整。在现有架构中,使用onLayout事件获取布局信息可能导致用户看到中间状态或视觉跳跃。而在新架构下,useLayoutEffect可以同步获取布局信息并更新,让这些中间状态彻底消失。可以明显看到不会存在跟不上的情况。


function ViewWithTooltip() {
const targetRef = React.useRef(null);
const [targetRect, setTargetRect] = React.useState(null);

useLayoutEffect(() => {
targetRef.current?.measureInWindow((x, y, width, height) => {
setTargetRect({ x, y, width, height });
});
}, [setTargetRect]);

return (
<>
<View ref={targetRef}>
<Text>一些内容,显示一个悬浮提示Text>

View>
<Tooltip targetRect={targetRect} />

);
}

支持并发渲染和新特性


可以看到新架构支持了并发渲染的效果对比,左边是老架构,右边是新架构:


并发渲染特性


新架构支持React 18及之后版本的并发渲染和新特性,例如Suspense数据获取和Transitions。这使得web和原生React开发之间的代码库和概念更加一致。同时,自动批处理减少了重绘的次数,提升了UI的流畅性。


function TileSlider({ value, onValueChange }) {
const [isPending, startTransition] = useTransition();

return (
<>
<View>
<Text>渲染 {value} 瓷砖Text>

<ActivityIndicator animating={isPending} />
View>
<Slider
value={value}
minimumValue={1}
maximumValue={1000}
step={1}
onValueChange={newValue =>
{
startTransition(() => {
onValueChange(newValue);
});
}}
/>

);
}

快速的JavaScript/Native接口


新架构移除了JavaScript和原生代码之间的异步桥接,取而代之的是JavaScript接口(JSI)。JSI允许JavaScript直接持有C++对象的引用,从而大大提高了调用效率。这使得像VisionCamera这样处理实时帧的库能够高效运行,消除大量序列化的开销。


JSI


VisionCamera 的地址是:github.com/mrousavy/re…


目前多达6K+的star,这个在 React Native 上的份量还是响当当的,可以看到它明显是用上了 JSI 了,向先驱们致敬。
VisionCamera


启用新架构的期望


尽管新架构提供了显著的改进,启用新架构并不一定会立即提升应用的性能。你的代码可能需要重构以利用新的功能,如同步布局效果或并发特性。或许,我认为,React Native 可能会同步出一些工具来帮助我们更好的迁移。比如配套的 eslint 插件,提示更优的建议写法等等。


现在是否应该使用新架构?


目前新架构仍被视为实验性,在2024年末发布的React Native版本中将成为默认设置。对于大多数生产环境应用,建议等待正式发布。库维护者则可以尝试启用并确认其用例被覆盖。另外看到react-native-vision-camera 这个库的 issue 下面反馈,JSI 目前还是存在一些坑需要爬的,所以要尝鲜的话,还是要有心理准备。
还是有坑在


通过详细介绍新架构的一系列优势和实际应用,我们可以看到React Native的未来发展前景。尽早了解和适应这些变化,一旦新架构正式发布,我们就能更好地利用React Native的潜力,为用户提供更好的体验。更好的产品体验,意味着产品的竞争力也会更强。


作者:brzhang
来源:juejin.cn/post/7377277576651898899
收起阅读 »

Jenkins 自动化部署微信小程序

web
近期一直参与微信小程序的开发工作,这段时间让我受益匪浅。在整个过程中,学到了很多关于小程序开发的知识和技能,比如如何优化小程序的性能、如何设计更好的用户界面、如何提高小程序的安全性,以及在小程序展示统计图表,层级渲染问题等等。同时,我也深刻认识到了小程序开发中...
继续阅读 »

近期一直参与微信小程序的开发工作,这段时间让我受益匪浅。在整个过程中,学到了很多关于小程序开发的知识和技能,比如如何优化小程序的性能、如何设计更好的用户界面、如何提高小程序的安全性,以及在小程序展示统计图表,层级渲染问题等等。同时,我也深刻认识到了小程序开发中的一些痛点,比如提测和修改bug需要被测试催着在 测试、uat、生产 环境中频繁发版,很是难受,于是想把这些繁琐的步骤交给机器处理,最终确定技术方案,利用Jenkinsuniapp() 还有 官方打包部署预览脚手架(miniprogram-ci) 配置了一套自动化部署的流程


准备


安装jenkins


服务器(本文 服务器系统是Ubuntu 22.04) 安装好jenkins,具体的步骤可以参考这篇文章


jenkins 自动化部署前端项目


配置项目


开发项目git仓库,项目搭建 具体请查看这篇


用Vue打造微信小程序,让你的开发效率翻倍!


打包部署预览原理和脚本编写请移步这篇文章


命令行秒传:一键上传微信小程序和生成二维码预览


上传脚本沿用了这篇文章的中脚本:命令行秒传:一键上传微信小程序和生成二维码预览,只需要略微改动, 改动支持了 设置版本号和备注,且先生成预览二维码和上传到微信小程序后台平台体验版


下面的代码中 appid 和 私钥(小程序后台的私钥 具体配置获取方法请参考上面文章链接)的路径 请自行更改


// 小程序发版
const ci = require('miniprogram-ci');
const path = require('path');
const argv = require('yargs').argv;

const appid = '*******'
let versions = '1.0.1'
let descs = '备注'
let projectPath = './dist/build/mp-weixin'
return (async () => {
if (argv.version) {
versions = argv.version
}
if (argv.descs) {
versions = argv.version
}
// 注意: new ci.Project 调用时,请确保项目代码已经是完整的,避免编译过程出现找不到文件的报错。
const project = new ci.Project({
appid: appid,
type: 'miniProgram',
projectPath: path.join(__dirname, projectPath), // 项目路径
privateKeyPath: path.join(__dirname, './private.*******.key'), // 私钥的路径
ignores: ['node_modules/**/*'],
})
// 生成二维码
const previewResult = await ci.preview({
project,
desc: 'hello', // 此备注将显示在“小程序助手”开发版列表中
setting: {
es6: true,
},
qrcodeFormat: 'image',
qrcodeOutputDest: path.join(__dirname, '/qrcode/destination.jpg'),
onProgressUpdate: console.log,
pagePath: 'pages/index/index', // 预览页面
searchQuery: 'a=1&b=2', // 预览参数 [注意!]这里的`&`字符在命令行中应写成转义字符`\&`
})
console.log('previewResult', previewResult)
console.log('等待5秒后 开始上传')
// 开始上传
let s = 5
let timer = setInterval(async () => {
--s
console.log(`${s}秒`)
if (s == 0) {
clearInterval(timer);
timer = undefined
const uploadResult = await ci.upload({
project,
version: versions,
desc: descs,
setting: {
es6: false, // es6 转 es5
disableUseStrict: true,
autoPrefixWXSS: true, // 上传时样式自动补全
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
}
})
console.log('uploadResult', uploadResult)
}
}, 1000);
})()

服务器配置 ssh配置


root 用户登录服务器 执行以下命令 切换为jenkins用户


sudo su jenkins

执行生成sshkey命令


 ssh-keygen -t rsa -C "你的邮箱"
// 然后一路回车

image.png


输出ssh私钥 和 公钥 保存备用


 cat ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub

image.png


jenkins配置


全局工具配置


配置Git installations


image.png
配置NodeJS版本


image.png


安装jenkins插件


Git Parameter: git分支参数插件


description setter 根据构建日志文件的正则表达式设置每个构建的描述


Version Number 修改版本号


在 Manage Jenkins->插件管理中 搜索 Git Parameter 并且安装重启生效


image.png


image.png


3.jpg


image.png


image.png


配置 ssh


jenkins 全局安全配置


系统管理->全局安全配置->Git Host Key Verification Configuration,选则Manually provided keys


Approved Host Keys中填写上方 服务器的jenkins用户生成的私钥内容


image.png


image.png


git仓库配置 ssh


如果你的项目是私人隐藏的,则需要在项目 配置 SSH 公钥(从上文服务器jenkins用户生成公钥获取内容)


image.png


修改标记格式器


这一步是为了 在构建记录中输出二维码和备注准备


在全局安全配置中 找到标记格式器,改为Safe HTML 保存


image.png


创建任务


首页新建任务


构建一个多配置项目
1.jpg
填写描述、选择github项目写入地址


设置参数


勾选参数化构建过程,添加git参数,输入名称、描述、默认分支
参数类型选择 分支


image.png
继续新增 字符参数 version和remark(这里名字可以自定义,随便起,与shell 脚本中变量名称相匹配就好 )


image.png


配置源码管理


源码管理选择Git,填写 Repository URL,Branches to build 指定分支 ${branch}


image.png


构建环境


勾选 Create a formatted version number


依次填写


Environment Variable Name:BUILD_VERSION


Version Number Format String: ${branch}


Project Start Date: 2023-06-30(项目开始日期)


image.png


Provide Node & npm bin/ folder to PATH 选择 18.16.1


image.png


Build Steps shell脚本


点击 增加构建步骤,选择 执行 shell 输入以下命令(可根据自己的实际情况进行改写)


下方的图片(destination.jpg)存放目录,记得配置文件访问服务,或者自行 编写上传图片逻辑,保证能访问到图片即可;



cd /var/lib/jenkins/workspace/wechart;
npm config set registry https://registry.npmmirror.com/;
npm install -g yarn;
yarn config set registry https://registry.npmmirror.com/;
yarn;
npm run build:mp-weixin;
node upload.js --version=$version --remark=$remark;
mv qrcode/destination.jpg /var/www/html;
chmod -R 777 /var/www/html/destination.jpg;
echo DESC_INFO:http://服务器域名/destination.jpg,$remark;
exit 0;

继续新增构建步骤 选择 Set build description


Regular expression 填写 DESC_INFO:(.*),(.*)


Description 填写 <img src="\1" height:"200" width="200" /><div style="color:blue;">\2</div>


image.png


构建后操作


选择Git Publisher


勾选Push Only If Build Succeeds


点击 Add Tag


Tag to push: wechart-$BUILD_NUMBER


勾选 Create new tag
image.png


点击保存


打包运行构建


选择 Build with Parameters,设置分支(这里会默认显示 git仓库的所有分支)


image.png


打包构建完成后,选择 wechart 点击 配置default


image.png


image.png


image.png


作者:iwhao
来源:juejin.cn/post/7250374485567750203
收起阅读 »

深入理解前端缓存

web
前端缓存是所有前端程序员在成长历程中必须要面临的问题,它会让我们的项目得到非常大的优化提升,同样也会带来一些其它方面的困扰。大部分前端程序员也了解一些缓存相关的知识,比如:强缓存、协商缓存、cookie等,但是我相信大部分的前端程序员不了解它们的缓存机制。接下...
继续阅读 »

前端缓存是所有前端程序员在成长历程中必须要面临的问题,它会让我们的项目得到非常大的优化提升,同样也会带来一些其它方面的困扰。大部分前端程序员也了解一些缓存相关的知识,比如:强缓存协商缓存cookie等,但是我相信大部分的前端程序员不了解它们的缓存机制。接下来我将带你们深入理解缓存的机制以及缓存时间的判断公式,如何合理的使用缓存机制来更好的提升优化。我将会把前端缓存分成HTTP缓存和浏览器缓存两个部分来和大家一起聊聊。


HTTP 缓存


HTTP是一种超文本传输协议,它通常运行在TCP之上,从浏览器Network中可以看到,它分为Respnse Headers(响应头)Request Headers(请求头)两部分组成。


image.png


接下来介绍一下与缓存相关的头部字段:


image.png


expires


我们先来看一下MDN对于expires的介绍



响应标头包含响应应被视为过期的日期/时间。


备注:  如果响应中有指令为 max-age 或 s-maxage 的 Cache-Control 标头,则 Expires 标头会被忽略。



Expires: Wed, 24 Apr 2024 14:27:26 GMT

Cache-Control


Cache-ControlHTTP/1.1中定义的缓存字段,它可以由多种组合使用,分开列如:max-age、s-maxage、public/private、no-cache/no-store等


Cache-Control: max-age=3600, s-maxage=3600, public

max-age是相对当前时间,单位是秒,当设置max-age时则expires就会失效,max-age的优先级更高。


而 s-maxage 与 max-age 不同之处在于,其只适用于公共缓存服务器,比如资源从源服务器发出后又被中间的代理服务器接收并缓存。


public是指该资源可以被任何节点缓存,而private只能提供给客户端缓存。当设置了private之后,s-maxage则会无效。


使用no-store表示不进行资源缓存。使用no-cache表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求,而当在响应首部中被返回时,表示客户端可以缓存资源,但每次使用缓存资源前都必须先向服务器确认其有效性,这对每次访问都需要确认身份的应用来说很有用。


当然,我们也可以在代码里加入 meta 标签的方式来修改资源的请求首部:


<meta http-equiv="Cache-Control" content="no-cache" />

示例


这里我起了一个nestjs的服务,该getdata接口缓存10s的时间,Ï代码如下:


  @Get('/getdata')
getData(@Response() res: Res) {
return res.set({ 'Expires': new Date(Date.now() + 10).toUTCString() }).json({
list: new Array(1000000).fill(1).map((item, index) => ({ index, item: 'index' + index }))
});Ï
}

第一次请求,花费了334ms的时间。


image.png


第二次请求花费了163ms的时间,走的是磁盘缓存,快了近50%的速度


image.png


接下来我们来验证使用Cache-Control是否可以覆盖Exprie,我们将getdata接口修改如下,Cache-Control设置了1s。Ï我们刷新页面可以看到getdata接口并没有缓存,每次都会想服务器发送请求。


  @Get('/getdata')
getData(@Response() res: Res) {
return res.set({ 'Expires': new Date(Date.now() + 10).toUTCString(), 'Cache-Control': 1 }).json({
list: new Array(1000000).fill(1).map((item, index) => ({ index, item: 'index' + index }))
});
}

仔细的同学应该会发现一个问题,清除缓存后的第一次请求和第二次请求Size的大小不一样,这是为什么呢?


打开f12右键刷新按钮,点击清空缓存并硬性重新加载。


image.png


我们开启Big request rows更方便查看Size的大小,开启时Size显示两行,第一行就是请求内容的大小,第二行则是实际的大小。


image.png


刷新一下,可以看到Size变成了283B大小了。


image.png


带着这个问题我们来深入研究一下浏览器的压缩。HTTP2和HTTP3的压缩算法是大致相同,我们就拿HTTP2的压缩算法(HPACK)来了解一下。


HTTP2 HPACK压缩算法


HPACK压缩算法大致分为:静态Huffman(哈夫曼)压缩和动态Huffman哈夫曼压缩,所谓静态压缩是指根据HTTP提供的静态字典表来查找对应的请求头字段从而存储对应的index值,可以极大的减少内催空间。


动态压缩它是在同一个会话级的,第一个请求的响应里包含了一个比如 {list: [1, 2, 3]},那么就会把它存进表里面,后续的其它请求的响应,就可以只返回这个 header 在动态表里的索引,实现压缩的目的


需要详细了解哈夫曼算法原理的可以去这个博客看一看。


Last-Modified 与 If-Modified-Since


Last-Modified代表资源的最后修改时间,其属于响应首部字段。当浏览器第一次接收到服务器返回资源的 Last-Modified 值后,其会把这个值存储起来,并下次访问该资源时通过携带If-Modified-Since请求首部发送给服务器验证该资源是否过期。


yaml
复制代码
Last-Modified: Fri , 14 May 2021 17:23:13 GMT
If-Modified-Since: Fri , 14 May 2021 17:23:13 GMT

如果在If-Modified-Since字段指定的时间之后资源都没有发生更新,那么服务器会返回状态码 304 Not Modified 的响应。


Etag 与 If--Match


Etag代码该资源的唯一标识,它会根据资源的变化而变化着,同样浏览器第一次收到服务器返回的Etag值后,会把它存储起来,并下次访问该资源通过携带If--Match请求首部发送给服务器验证资源是否过期


Etag: "29322-09SpAhH3nXWd8KIVqB10hSSz66" 
If--Match: "29322-09SpAhH3nXWd8KIVqB10hSSz66"

如果两者不相同则代表服务器资源已经更新,服务器会返回该资源最新的Etag值。


强缓存


强缓存的具体流程如下:


image.png


上面我们介绍了expires设置的是绝对的时间,它会根据客户端的时间来判断,所以会造成expires不准确,如果我有一个资源缓存到到期时间是2024年4月31日我将客户端时间修改成过期的时间,则在一次访问该资产会重新请求服务器获取最新的数据。


max-age则是相对的时间,它的值是以秒为单位的时间,但是max-age也会不准确。


那么到底浏览器是怎么判断该资源的缓存是否有效的呢?这里就来介绍一下资源新鲜度的公式。


我们来用生活中的食品新鲜度来举例:


食品是否新鲜 = (生产日期 + 保质期) > 当前日期

那么缓存是否新鲜也可以借助这个公式来判断


缓存是否新鲜 = (创建时间 + expire || max-age) > 缓存使用期

这里的创建时间可以理解为服务器返回资源的时间,它和expires一样是一个绝对时间。


缓存使用期 = 响应使用期 + 传输延迟时间 + 停留缓存时间

响应使用期


响应使用期有两种获取方式:



  • max(0, responseTime - dateTime)

  • age


responseTime: 是指客户端收到响应的时间

dateTime: 是指服务器创建资源的时间

age:是响应头部的字段,通常是秒为单位


传输延迟时间


传输延迟的时间 = 客户端收到响应的时间 - 请求时间

停留时间


停留时间 = 当前客户端时间 - 客户端收到响应的时间

所以max-age也会失效的问题就是它也使用到了客户端的时间


协商缓存


协商缓存的具体流程如下:


image.png


从上文可以知道,协商缓存就是通过EtagLast-Modified这两个字段来判断。那么这个Etag的标识是如何生成的呢?


我们可以看node中etag第三方库。


该库会通过isState方法来判断文件的类型,如果是文件形式的话就会使用第一种方法:通过文件内容和修改时间来生成Etag


image.png


第二种方法:通过文件内容和hash值和内容长度来生成Etag


image.png


浏览器缓存


我们访问掘金的网站,查看Network可以看到有Size列有些没有大小的,而是disk cachememory cache这样的标识。


image.png


memory cache翻译就是内存缓存,顾名思义,它是存储在内存中的,优点就是速度非常快,可以看到Time列是0ms,缺点就是当网页关闭则缓存也就清空了,而且内存大小是非常有限的,如果要存储大量的资源的话还是使用磁盘缓存。


disk cache翻译就是磁盘缓存,它是存储在计算机磁盘中的一种缓存,它的优缺点和memory cache相反,它的读取是需要时间的,可以看到上方的图片Time列用了1ms的时间。


缓存获取顺序



  1. 浏览器会先查找内存缓存,如果存在则直接获取内存缓存中的资源

  2. 内存缓存没有,就回去磁盘缓存中查找,如果存在就返回磁盘缓存中的资源

  3. 磁盘缓存没有,那么就会进行网络请求,获取最新的资源然后存入到内存缓存或磁盘缓存


缓存存储优先级


浏览器是如何判断该资源要存储在内存缓存还是磁盘缓存的呢?


打开掘金网站可以看到,发现除了base64图片会从内存中获取,其它大部分资源会从磁盘中获取。


image.png


js文件是一个需要注意的地方,可以看到下面的有些js文件会被磁盘缓存有些则会被内存缓存,这是为什么呢?


image.png


Initiator列表示资源加载的位置,我们点击从内存获取资源的该列发现资源在HTML渲染阶段就被加载了,列入一下代码


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>DocumentÏ</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body>
<div id="root">这样加载的js资源大概率会存储到内存中</div>
</body>
</html>

而被内存抛弃的可以发现就是异步资源,这些资源不会被缓存到内存中。


上图我们可以看到有一个Initiator列的值是(index):50但是它还是被内存缓存了,我们可以点击进去看到他的代码如下:


image.png


这个js文件还是通过动态创建script标签来动态引入的。


Preload 与 Prefetch


PreloadPrefetch也会影响浏览器缓存的资源加载。


Preload称为预加载,用在link标签中,是指哪些资源需要页面加载完成后立刻需要的,浏览器会在渲染机制介入前加载这些资源。


<link rel="preload" href="//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/0358ea0.js" as="script">

当使用preload预加载资源时,这些资源一直会从磁盘缓存中读取。


prefetch表示预提取,告诉浏览器下一个页面可能会用到该资源,浏览器会利用空闲时间进行下载并存储到缓存中。


<link rel="prefretch" href="//lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/0358ea0.js"Ï>

使用 prefetch 加载的资源,刷新页面时大概率会从磁盘缓存中读取,如果跳转到使用它的页面,则直接会从磁盘中加载该资源。


作者:sorryhc
来源:juejin.cn/post/7382891974942179354
收起阅读 »

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

前言 兄弟们可能有的感受 感受一:入职 初创公司 或者成立 新的团队 而只有你一个前端,不知道从何做起。 感受二:天天写业务代码和内部工具,感觉没技术沉淀,成长太慢,然后发现架构那边挺有趣的。 感受三:对一些框架的原理、源码、工具 研究较少,无法突破评级,...
继续阅读 »

前言




兄弟们可能有的感受



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

  • 感受二:天天写业务代码和内部工具,感觉没技术沉淀,成长太慢,然后发现架构那边挺有趣的。

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


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




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




1.什么是基建?



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

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

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




2.基建的意义


主要是为了以下几点:



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

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

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

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




基建搞什么




1.核心:


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



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

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


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




2.方向


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


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


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




3.搞什么


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



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

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

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

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

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

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

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


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




4.大小公司基建重点


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



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

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


大小公司基建重点:



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

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




基建怎么搞




下面,会针对一些大家都感兴趣的方向,结合自身过去部分的建设产出和一些文章的记录的整合(部分地方可能比较久远的,有些不记得出处。如有冒犯,深感抱歉,可与我联系!),为大家列举一些前端基建类的沉淀,以供参考。


1. 规范&文档


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


文档:



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

  • 技术文档、

  • 业务文档、

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

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

  • 技术分享交流会文档


规范:



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



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




2. 脚手架


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


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




3. 组件


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



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

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

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




4. 工具 / 函数库


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


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




5. 模板


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



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

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

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

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

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




6. API管理 / BFF


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


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




7. CI/CD 构建部署


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




8. 数据埋点与分析


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



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

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

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

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




9.微前端


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


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




基建之外思考




1. 从当下业务场景出发


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




2.基建讲究循序渐进


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




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


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


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


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




4.个人不足


总结了这么多,结果发现自己对于一些知识点还是了解的太浅显了,自身在那些方面能分享的还是不多,平时也看了一些文章记录了一些笔记,却只能描出个大概,实在是有点不好意思。但回头想想,这何尝也不算个勉励自己的方法,能够鞭策自己。后续,在我学习深入一些基建方面的知识后,会再出一些文章分享给大家,希望能够帮助到大家,共勉!!!☺(这篇文章来自于自身学习和平常看文章记录的笔记的整合,如一些地方有冒犯到,深感抱歉,请与我联系修改!)


落尾




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


计划是试试每一两月(最近有点🕊了,太忙了)复盘一次,总结出至少一个知识点,目的是尽快给自己的反馈,将自己产品一样快速迭代上升,希望可以坚持✊。


输出的文章也是个人的一些浅显理解,网上看的一些好文章记录的一些笔记,望大家包容!!!


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


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


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


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


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

Strapi让纯前端成为全栈不再是口号!🚀🚀🚀

web
序言很早以前就知道strap的存在,一直没有机会使用到。很早以前就想找一个类似strapi的框架,来帮我快速搭建后台服务。如果你只懂前端、那么它将非常适合你用来快速构建自己的api服务,以此实现全栈项目开发。strapi是什么?Strapi在国内鲜为人知,但它...
继续阅读 »

序言

很早以前就知道strap的存在,一直没有机会使用到。

很早以前就想找一个类似strapi的框架,来帮我快速搭建后台服务。

如果你只懂前端、那么它将非常适合你用来快速构建自己的api服务,以此实现全栈项目开发。

image.png

strapi是什么?

Strapi在国内鲜为人知,但它在国外的使用情况真的很Nice!

image.png 其仓库也是一直在维护、更新的。

Strapi 是一个开源的 Headless CMS(无头内容管理系统)。它允许开发者通过自定义的方式快速构建、管理和分发内容。Strapi 提供了一个强大的后端 API,支持 RESTful  GraphQL 两种方式,使得开发者可以方便地将内容分发到任何设备或服务,无论是网站、移动应用还是 IoT 设备。

Strapi 的主要特点包括:

  • 灵活性和可扩展性:通过自定义模型、API、插件等,Strapi 提供了极高的灵活性,可以满足各种业务需求。
  • 易于使用的 API:Strapi 提供了一个简洁、直观的 API,使得开发者可以轻松地与数据库进行交互。
  • 内容管理界面:Strapi 提供了一个易于使用的管理界面,使得用户可以轻松地创建、编辑和发布内容。
  • 多语言支持:Strapi 支持多种语言,包括中文、英语、法语、德语等。
  • 可扩展性:Strapi 具有高度的可扩展性,可以通过插件和自定义模块、插件来扩展其功能。
  • 社区支持:Strapi 拥有一个活跃的社区,提供了大量的文档、示例和插件,使得开发人员可以轻松地解决问题和扩展功能。

主要适用场景:

  • 多平台内容分发( 将内容分发到不同web、h5等不同平台 
  • 定制化 CMS 需求( 通过插件等扩展性高度定制 
  • 快速开发api(API管理界面能够大大加快开发速度,尤其是MVP(最小可行产品)阶段 

strapi实战

光看官网界面原官网地址),还是相当漂亮的🙈:

安装Strapi

超级简单,执行下面的命令后、坐等服务启动

(安装完后,自动执行了strapi start,其mysql、语言切换、权限配置等都内置到了@strapi包中)

yarn create strapi-app my-strapi --quickstart

浏览器访问:http://localhost:1337/admin/

第一步就完成了、是不是so easy😍,不过默认是英文的,虽然英语还凑合,但使用起来还是多有不便。strapi原本就支持国际化,我们来切换成中文再继续操作。

语言切换

  1. 设置国际化

  1. 个人设置中配置语言即可:

如果看不到"中文(简体)"选项,就在项目根目录下执行build并重启:npm run build && npm start,再刷新页面应该就能看到了。注意npm start默认是生产环境的启动(只能使用表,无法创建表)、开发环境启动用"npm run develop"

strapi的基础使用

在第一步完成的时候,其实数据库就已经搭建好了,我们只管建表、增加curd的接口即可

1. 建表

设置字段、可以选择需要的类型:

在保存左边的按钮可以继续添加字段

blog字段、建模完成后,进入内容管理器给表插入数据

2. curd

上面只是可视化的查看、插入数据,怎样才能变成api来进行curd了。

  • 设置API令牌,跟进提示操作

  • 权限说明

find GET请求 /api/blogs 查找所有数据

findone GET请求 /api/blogs/:id 查找单条数据

create POST请求 /api/blogs 创建数据

update PUT请求 /api/blogs/:id 更新数据

delete DELETE请求 /api/blogs/:id 删除数据

  • postman调试

先给blog公共权限,以便调试:

  1. 查找所有数据(find)

  1. 查找单条数据(findone)

  1. 更新修改数据(update)

  1. 删除数据(delete),返回被删除的数据

再次查看:

好了,恭喜你。 到这一步,你已经掌握了strapi curd的使用方法。


strapi数据可视化、Navicat辅助数据处理

Strapi 支持多种数据库,包括 MySQL、PostgreSQL、MongoDB 和 SQLite,并且具有高度的可扩展性和自定义性,可以满足不同项目的需求。(默认使用的是SQLite数据库)

我们也可以借助Navicat等第三个工具来实现可视化数据操作:

其用户名、密码默认都是strapi

strapi数据迁移

SQLite数据库

如果你只是需要将SQLite数据库从一个环境迁移到另一个环境(比如从一个服务器迁移到另一个服务器),操作相对简单:

  1. 备份SQLite数据库文件:找到你的SQLite数据库文件(默认位置是项目根目录下的 .tmp/data.db)并将其复制到安全的位置。
  2. 迁移文件:将备份的数据库文件移动到新环境的相同位置。
  3. 更新配置(如有必要) :如果新环境中数据库文件的位置有变化,确保更新Strapi的数据库配置文件(./config/database.js)以反映新的文件路径。

SQLite到其他数据库系统

如果你需要将SQLite数据库迁移到其他类型的数据库系统,比如PostgreSQL或MySQL,流程会更复杂一些:

  1. 导出SQLite数据:首先,你需要导出SQLite数据库中的数据。这可以通过多种工具完成,例如使用sqlite3命令行工具或一个图形界面工具(如DB Browser for SQLite)来导出数据为SQL文件。
  2. 准备目标数据库:在目标数据库系统中创建一个新的数据库,为Strapi项目准备使用。
  3. 修改Strapi的数据库配置:根据目标数据库类型,修改Strapi的数据库配置文件(./config/database.js)。你需要根据目标数据库系统的要求配置连接参数。
  4. 导入数据到目标数据库:使用目标数据库系统的工具导入之前导出的数据。不同数据库系统的导入工具和命令会有所不同。例如,对于PostgreSQL,你可能会使用psql工具,对于MySQL,则可能使用mysql命令行工具。
  5. 处理数据类型和结构差异:不同的数据库系统在数据类型和结构上可能会有所差异。在导入过程中,你可能需要手动调整SQL文件或在导入后调整数据库结构,尤其是对于关系和外键约束。
  6. 测试:迁移完成后,彻底测试你的Strapi项目,确保数据正确无误,所有功能正常工作。

注意事项

  • 数据兼容性:在不同数据库系统之间迁移时,可能会遇到数据类型不兼容的问题,需要仔细处理。
  • 性能调优:迁移到新的数据库系统后,可能需要根据新的数据库特性进行调优以确保性能。
  • 备份:在进行任何迁移操作之前,总是确保已经备份了所有数据和配置。

具体步骤可能会因你的具体需求和所使用的数据库系统而异。根据你的目标数据库系统,可能有特定的迁移工具和服务可以帮助简化迁移过程。

小结

好了,梳理一下。现在我要建一套博客系统的API该怎么做了?

  1. 安装启动(已安装可忽略)yarn create strapi-app my-strapi --quickstart
  2. 在后台建表建模、设置字段
  3. 设置表的API调用权限
  4. 在需要用到的地方使用即可
    是不是超级简单了!

总结

上面我们了解了strapi的后台使用、curd操作、数据迁移等。相信大家都能快速掌握使用。我们无需基于ORM框架去搭建数据模型,也无需使用python、nestjs等后台框架去创建后台服务了。 这势必能大大提升我们的开发效率。

后续会再继续讲解strapi富文本插件的使用,有了富文本的加持、我们都能省去搭建管理后台了,如果用来做博客、高度定制化的文档系统将是非常不错的选择。


作者:tager
来源:juejin.cn/post/7340152660224819212

收起阅读 »

打脸了,rust确实很快

正文: 原标题:不要盲目迷信rust,rust或许没有你想象中的那么快 之前的文章内容是关于rust速度不快的问题。但是我遗留的问题是,计算出来的哈希值不一样。经过老哥的指点,原来是我js代码的实现有点问题,让大家见笑了。 于是我修改代码,重新测试了一遍,这...
继续阅读 »

正文:


原标题:不要盲目迷信rust,rust或许没有你想象中的那么快


之前的文章内容是关于rust速度不快的问题。但是我遗留的问题是,计算出来的哈希值不一样。经过老哥的指点,原来是我js代码的实现有点问题,让大家见笑了。


image.png


于是我修改代码,重新测试了一遍,这时哈希值就一样了,并且计算的结果确实是rust要快。刚好拿来佐证rust快的事实。


image.png


以后一定经过仔细验证,再发表文章。原文不删,留作警醒。


以下是原文:


原文:


先说结论:抛开应用场景,单说语言速度都是耍流氓。因为js调度rust,会有时间损耗。所以rust在一定应用场景下,比js还慢。


# 使用 wasm 提高前端20倍的 md5 计算速度


前两天看了一篇文件,是使用rust和wasm来加快md5的计算时间。跑了他的demo,发现只有rust的demo,而没有js的对比,于是我fork项目后,补充了一个js的对比。


image.png


测试下来,发现rust并没有比js快多少,由于浏览器限制,我只能用2GB文件来测,不知道是不是这个原因。还是我使用的js的原因。至少在2GB的边界时,js比rust要快。


他rust部分我没有动,只是添加了一个js的对比,如果大家觉得我的js写得有问题,欢迎pr重新比较。


在线对比地址:minori-ty.github.io/digest-wasm…


项目地址: github.com/Minori-ty/d…


作者:天平
来源:juejin.cn/post/7359757993732734991
收起阅读 »

手把手带你开发一套用户权限系统,精确到按钮级

在实际的软件项目开发过程中,用户权限控制可以说是所有运营系统中必不可少的一个重点功能,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单这三个部分展开。 如何设计一套可以精确到按钮级别的用户权限功能呢? 今天通过这篇...
继续阅读 »

在实际的软件项目开发过程中,用户权限控制可以说是所有运营系统中必不可少的一个重点功能,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单这三个部分展开


如何设计一套可以精确到按钮级别的用户权限功能呢?


今天通过这篇文章一起来了解一下相关的实现逻辑,不多说了,直接上案例代码!


01、数据库设计


在进入项目开发之前,首先我们需要进行相关的数据库设计,以便能存储相关的业务数据。


对于【用户权限控制】功能,通常5张表基本就可以搞定,分别是:用户表、角色表、用户角色表、菜单表、角色菜单表,相关表结构示例如下。



其中,用户和角色是多对多的关系角色与菜单也是多对多的关系用户通过角色来关联到菜单,当然也有的用户权限控制模型中,直接通过用户关联到菜单,实现用户对某个菜单权限独有控制,这都不是问题,可以自由灵活扩展。


用户、角色表的结构设计,比较简单。下面,我们重点来解读一下菜单表的设计,如下:



可以看到,整个菜单表就是一个父子表结构,关键字段如下



  • name:菜单名称

  • menu_code:菜单编码,用于后端权限控制

  • parent_id:菜单父节点ID,方便递归遍历菜单

  • node_type:菜单节点类型,可以是文件夹、页面或者按钮类型

  • link_url:菜单对应的地址,如果是文件夹或者按钮类型,可以为空

  • level:菜单树的层次,以便于查询指定层级的菜单

  • path:树id的路径,主要用于存放从根节点到当前树的父节点的路径,想要找父节点时会特别快


为了方便项目后续开发,在此我们创建一个名为menu_auth_db的数据库,SQL 初始脚本如下:


CREATE DATABASE IF NOT EXISTS `menu_auth_db` default charset utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE TABLE `menu_auth_db`.`tb_user` (
`id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '用户手机号',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '用户姓名',
`password` varchar(128) NOT NULL DEFAULT '' COMMENT '用户密码',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='用户表';

CREATE TABLE `menu_auth_db`.`tb_user_role` (
`id` bigint(20) unsigned NOT NULL COMMENT '主键',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='用户角色表';

CREATE TABLE `menu_auth_db`.`tb_role` (
`id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
`name` varchar(100) NOT NULL DEFAULT '' COMMENT '角色名称',
`code` varchar(100) NOT NULL DEFAULT '' COMMENT '角色编码',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='角色表';


CREATE TABLE `menu_auth_db`.`tb_role_menu` (
`id` bigint(20) unsigned NOT NULL COMMENT '主键',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`),
) ENGINE=InnoDB COMMENT='角色菜单表';


CREATE TABLE `menu_auth_db`.`tb_menu` (
`id` bigint(20) NOT NULL COMMENT '菜单ID',
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单名称',
`menu_code` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单编码',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父节点',
`node_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '节点类型,1文件夹,2页面,3按钮',
`icon_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单图标地址',
`sort` int(11) NOT NULL DEFAULT '1' COMMENT '排序号',
`link_url` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜单对应的地址',
`level` int(11) NOT NULL DEFAULT '0' COMMENT '菜单层次',
`path` varchar(2500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '树id的路径,主要用于存放从根节点到当前树的父节点的路径',
`is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (`id`) USING BTREE,
KEY idx_parent_id (`parent_id`) USING BTREE
) ENGINE=InnoDB COMMENT='菜单表';

02、项目构建


菜单权限模块的数据库设计搞定之后,就可以正式进入系统开发阶段了。


2.1、创建项目


为了快速构建项目,这里采用的是springboot+mybatisPlus框架来快速开发,借助mybatisPlus提供的生成代码器,可以一键生成所需的daoserviceweb层的服务代码,以便帮助我们剩去 CRUD 中重复编程的工作量,内容如下:



CRUD 代码生成完成之后,此时我们就可以编写业务逻辑代码了,相关示例如下!


2.2、菜单功能开发


2.2.1、菜单新增逻辑示例

@Override
public void addMenu(Menu menu) {
//如果插入的当前节点为根节点,parentId指定为0
if(menu.getParentId().longValue() == 0){
menu.setLevel(1);//默认根节点层级为1
menu.setPath(null);//默认根节点路径为空
}else{
Menu parentMenu = baseMapper.selectById(menu.getParentId());
if(parentMenu == null){
throw new CommonException("未查询到对应的父菜单节点");
}
menu.setLevel(parentMenu.getLevel().intValue() + 1);
// 重新设置菜单节点路径,多个用【,】隔开
if(StringUtils.isNotEmpty(parentMenu.getPath())){
menu.setPath(parentMenu.getPath() + "," + parentMenu.getId());
}else{
menu.setPath(parentMenu.getId().toString());
}
}
// 设置菜单ID,可以用发号器来生成
menu.setId(System.currentTimeMillis());
// 将菜单信息插入到数据库
super.save(menu);
}

2.2.2、菜单查询逻辑示例

首先,编写一个视图对象,用于数据展示。


public class MenuVo {

/**
* 主键
*/

private Long id;

/**
* 名称
*/

private String name;

/**
* 菜单编码
*/

private String menuCode;

/**
* 父节点
*/

private Long parentId;

/**
* 节点类型,1文件夹,2页面,3按钮
*/

private Integer nodeType;

/**
* 图标地址
*/

private String iconUrl;

/**
* 排序号
*/

private Integer sort;

/**
* 页面对应的地址
*/

private String linkUrl;

/**
* 层次
*/

private Integer level;

/**
* 树id的路径 整个层次上的路径id,逗号分隔,想要找父节点特别快
*/

private String path;

/**
* 子菜单集合
*/

List childMenu;

// set、get方法等...
}

接着编写菜单查询逻辑,这里需要用到递归算法来封装菜单视图。


@Override
public List queryMenuTree() {
Wrapper queryObj = new QueryWrapper<>().orderByAsc("level","sort");
List
allMenu = super.list(queryObj);
// 0L:表示根节点的父ID
List resultList = transferMenuVo(allMenu, 0L);
return resultList;
}

递归算法,方法实现逻辑如下!


/**
* 封装菜单视图
*
@param allMenu
*
@param parentId
*
@return
*/

private List transferMenuVo(List
allMenu, Long parentId){
List resultList = new ArrayList<>();
if(!CollectionUtils.isEmpty(allMenu)){
for (Menu source : allMenu) {
if(parentId.longValue() == source.getParentId().longValue()){
MenuVo menuVo = new MenuVo();
BeanUtils.copyProperties(source, menuVo);
//递归查询子菜单,并封装信息
List childList = transferMenuVo(allMenu, source.getId());
if(!CollectionUtils.isEmpty(childList)){
menuVo.setChildMenu(childList);
}
resultList.add(menuVo);
}
}
}
return resultList;
}

最后编写一个菜单查询接口,将其响应给客户端。


@RestController
@RequestMapping("/menu")
public class MenuController {

@Autowired
private MenuService menuService;

@PostMapping(value = "/queryMenuTree")
public List queryTreeMenu(){
return menuService.queryMenuTree();
}
}

为了便于演示,这里我们先在数据库中初始化几条数据,最后三条数据指的是按钮类型的菜单,用户真正请求的时候,实际上请求的是这三个功能,内容如下:



queryMenuTree接口发起请求,返回的数据结果如下图:



将返回的数据,通过页面进行渲染之后,结果类似如下图:



2.3、用户权限开发


在上文,我们提到了用户通过角色来关联菜单,因此,很容易想到,用户控制菜单的流程如下:



  • 第一步:用户登陆系统之后,查询当前用户拥有哪些角色;

  • 第二步:再通过角色查询关联的菜单权限点;

  • 第三步:最后将用户拥有的角色名下所有的菜单权限点,封装起来返回给用户;


带着这个思路,我们一起来看看具体的实现过程。


2.3.1、用户权限点查询逻辑示例

首先,编写一个通过用户ID查询菜单的服务,代码示例如下!


@Override
public List queryMenus(Long userId) {
// 第一步:先查询当前用户对应的角色
Wrapper queryUserRoleObj = new QueryWrapper<>().eq("user_id", userId);
List userRoles = userRoleService.list(queryUserRoleObj);
if(!CollectionUtils.isEmpty(userRoles)){
// 第二步:通过角色查询菜单(默认取第一个角色)
Wrapper queryRoleMenuObj = new QueryWrapper<>().eq("role_id", userRoles.get(0).getRoleId());
List roleMenus = roleMenuService.list(queryRoleMenuObj);
if(!CollectionUtils.isEmpty(roleMenus)){
Set menuIds = new HashSet<>();
for (RoleMenu roleMenu : roleMenus) {
menuIds.add(roleMenu.getMenuId());
}
//查询对应的菜单
Wrapper queryMenuObj = new QueryWrapper<>().in("id", new ArrayList<>(menuIds));
List
menus = super.list(queryMenuObj);
if(!CollectionUtils.isEmpty(menus)){
//将菜单下对应的父节点也一并全部查询出来
Set allMenuIds = new HashSet<>();
for (Menu menu : menus) {
allMenuIds.add(menu.getId());
if(StringUtils.isNotEmpty(menu.getPath())){
String[] pathIds = StringUtils.split(",", menu.getPath());
for (String pathId : pathIds) {
allMenuIds.add(Long.valueOf(pathId));
}
}
}
// 第三步:查询对应的所有菜单,并进行封装展示
List
allMenus = super.list(new QueryWrapper().in("id", new ArrayList<>(allMenuIds)));
List resultList = transferMenuVo(allMenus, 0L);
return resultList;
}
}

}
return null;
}

然后,编写一个通过用户ID查询菜单的接口,将数据结果返回给用户,代码示例如下!


@PostMapping(value = "/queryMenus")
public List queryMenus(Long userId){
//查询当前用户下的菜单权限
return menuService.queryMenus(userId);
}

2.4、用户鉴权开发


完成以上的逻辑开发之后,可以实现哪些用户拥有哪些菜单权限点的操作,比如用户【张三】,拥有【用户管理】菜单,那么他只能看到【用户管理】的界面;用户【李四】,用于【角色管理】菜单,同样的,他只能看到【角色管理】的界面,无法看到其他的界面。


但是某些技术人员发生漏洞之后,可能会绕过页面展示逻辑,直接对接口服务发起请求,依然能正常操作,例如利用用户【张三】的账户,操作【角色管理】的数据,这个时候就会发生数据安全隐患的问题。


为此,我们还需要一套用户鉴权的功能,对接口请求进行验证,只有满足要求的才能获取数据。


其中上文提到的菜单编码menuCode就是一个前、后端联系的桥梁。其实所有后端的接口,与前端对应的都是按钮操作,因此我们可以以按钮为基准,实现前后端双向权限控制


以【角色管理-查询】这个为例,前端可以通过菜单编码实现是否展示这个查询按钮,后端可以通过菜单编码来鉴权当前用户是否具备请求接口的权限,实现过程如下!


2.4.1、权限控制逻辑示例

在此,我们采用权限注解+代理拦截器的方式,来实现接口权限的安全验证。


首先,编写一个权限注解CheckPermissions


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermissions {

String value() default "";
}

然后,编写一个代理拦截器,拦截所有被@CheckPermissions注解标注的方法


@Aspect
@Component
public class CheckPermissionsAspect {

@Autowired
private MenuMapper menuMapper;

@Pointcut("@annotation(com.company.project.core.annotation.CheckPermissions)")
public void checkPermissions() {}

@Before("checkPermissions()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
Long userId = null;
// 获取请求参数
Object[] args = joinPoint.getArgs();
Object requestParam = args[0];
// 用户请求参数实体类中的用户ID
if(!Objects.isNull(requestParam)){
// 获取请求对象中属性为【userId】的值
Field field = requestParam.getClass().getDeclaredField("userId");
field.setAccessible(true);
userId = (Long) field.get(parobj);
}
if(!Objects.isNull(userId)){
// 获取方法上有CheckPermissions注解的参数
Class clazz = joinPoint.getTarget().getClass();
String methodName = joinPoint.getSignature().getName();
Class[] parameterTypes = ((MethodSignature)joinPoint.getSignature()).getMethod().getParameterTypes();
// 寻找目标方法
Method method = clazz.getMethod(methodName, parameterTypes);
if(method.getAnnotation(CheckPermissions.class) != null){
// 获取注解上的参数值
CheckPermissions annotation = method.getAnnotation(CheckPermissions.class);
String menuCode = annotation.value();
if (StringUtils.isNotBlank(menuCode)) {
// 通过用户ID、菜单编码查询是否有关联
int count = menuMapper.selectAuthByUserIdAndMenuCode(userId, menuCode);
if(count == 0){
throw new CommonException("接口无访问权限");
}
}
}
}
}
}

2.4.2、鉴权逻辑验证

我们以上文说到的【角色管理-查询】为例,编写一个服务接口来验证一下逻辑的正确性。


首先,编写一个请求实体类RoleDTO,添加userId属性


public class RoleDTO extends Role {

//添加用户ID
private Long userId;

// set、get方法等...
}

其次,编写一个角色查询接口,并在方法上添加@CheckPermissions注解,表示此方法需要鉴权,满足条件的用户才能请求通过。


@RestController
@RequestMapping("/role")
public class RoleController {

private RoleService roleService;

@CheckPermissions(value="roleMgr:list")
@PostMapping(value = "/queryRole")
public List queryRole(RoleDTO roleDTO){
return roleService.list();
}
}

最后,在数据库中初始化相关的数据。例如给用户【张三】分配一个【访客人员】角色,同时这个角色只有【系统配置】、【用户管理】菜单权限。






启动项目,在postman中传入用户【张三】的ID,查询用户具备的菜单权限,只有两个,结果如下:



同时,利用用户【张三】发起【角色管理-查询】操作,提示:接口无访问权限,结果如下:



与预期结果一致!因为没有配置角色查询接口,所以无权访问!


03、小结


最后总结一下,【用户权限控制】功能在实际的软件系统中非常常见,希望本篇的知识能帮助到大家。




作者:潘志的研发笔记
来源:juejin.cn/post/7380283378153914383
收起阅读 »

Android 复杂项目崩溃率收敛至0.01%实践

一、崩溃收敛机制 1、创建修BUG分支 在我们的项目中,每个版本发布之后,我们会创建一个opt分支,用于修复线上崩溃以及业务逻辑BUG。 开发过程中,一个APP可能同时并行开发多个需求,每个需求上线的预期时间可能会有不同。但是这个opt分支我们会保证在下个版本...
继续阅读 »

一、崩溃收敛机制


1、创建修BUG分支


在我们的项目中,每个版本发布之后,我们会创建一个opt分支,用于修复线上崩溃以及业务逻辑BUG。


开发过程中,一个APP可能同时并行开发多个需求,每个需求上线的预期时间可能会有不同。但是这个opt分支我们会保证在下个版本一定上线,QA同学也会在每个版本发布前预留测试opt分支的时间。


2、每天早晨查看Dump后台


每天上班第一件事就是查看DUMP后台,收集昨天线上发生的DUMP崩溃,具体的堆栈分配给对应的业务负责人。


业务负责人收到崩溃之后,会优先跟进排查。排查下来如果相对好修复,会第一时间直接修复掉,并提交到opt分支。如果排查下来发现,较难定位或者耗时较久,则需要给出修复预期。也可以将Bug转为技术优化,作为专项推进。因为确实有一些Bug需要通盘考虑,所有业务配合。


二、崩溃容灾机制


1、背景


我们为什么要开发一套崩溃容灾逻辑?


在对线上崩溃进行收敛时,我们发现线上有几类崩溃是我们在应用无法修复的。


例如:案例一


java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.content.ClipDescription.hasMimeType(java.lang.String)' on a null object reference
at android.widget.TextView.canPasteAsPlainText(TextView.java:15065)
at android.widget.Editor$TextActionModeCallback.populateMenuWithItems(Editor.java:4692)
at android.widget.Editor$TextActionModeCallback.onCreateActionMode(Editor.java:4627)

案例二:小米手机上出现


java.lang.NullPointerException:Attempt to invoke virtual method 'int android.text.Layout.getLineForOffset(int)' on a null object reference

案例三:集成华为推送SDK后,偶现


ava.lang.RuntimeException:Unable to start activity ComponentInfo{com.netease.popo/com.huawei.hms.activity.BridgeActivity}:
android.util.AndroidRuntimeException: requestFeature() must be called before adding content"

案例四:BadTokenException


android.view.WindowManager$BadTokenException:Unable to add window -- token android.os.BinderProxy

我们大致将以上问题划分为四类:



  • 我们认为是系统异常,应用层仅能在使用的位置try cache,有些崩溃甚至无处try cache;

  • 排查下来发现仅在某个厂商的手机上出现;

  • 集成的一些第三方SDK所引入,依赖对方修复,时间上不好掌控;

  • 由于Android系统的一些机制引发的崩溃,如弹出弹框时,恰好依赖的Actity正在销毁。业务层希望弹框可以不弹出但不要崩溃,可是系统最终是抛出来一个BadTokenException。我们可以在使用DiaLog时做判断,但是总会用有同学忘记。


基于以上我们思考是否可以开发一个框架,将这些崩溃统计进行拦截,使其不影响用户的使用。


2、技术方案


作为Android开发应该都比较清楚Handler机制。我们的崩溃容灾主要是利用了Handler机制。
具体的逻辑图如下:


popo_2022-05-15  16-16-01.jpg



  • 应用启动后,初始化崩溃白名单(应用内置,也支持服务端动态下发)

  • 通过Handler#post()方法,向主线程中发送一条消息。

  • 在Runnable#run()方法中,执行一个死循环逻辑

  • 死循环中逻辑中使用try cache将Looper.loop()防护

  • 这样只要应用的进程不结束,相当于任务一直执行在我们前面post的消息中

  • 只是我们在这个消息中,再次执行了Looper.loop()方法,执行后续消息队列中所有的消息

  • 一旦后续所有消息遇到崩溃,会先被try cache捕获。

  • 然后判断崩溃信息是否在我们的白名单中,一旦在白名单中直接捕获掉,不向外抛异常,逻辑会回到外部的死循环中,继续执行Looper.loop()方法获取后续的消息。这样就保证了逻辑的连贯,后续的事件可以继续处理。

  • 不再我们的白名单中则继续将这个异常throw出去。


3、现状


崩溃拦截框架上线至今几年的时间,积累的崩溃种类目前已经达到81种。


比较典型的除了上面介绍的几类崩溃以外,还有如我们在适配Android 12的SplasScree时,遇到的TransferSplashScreenViewStateItem 相关的错误


java.lang.IllegalArgumentException: Activity client record must not be null to execute transaction item: android.app.servertransaction.TransferSplashScreenViewStateItem@de845fa
at android.app.servertransaction.ActivityTransactionItem.getActivityClientRecord(ActivityTransactionItem.java:85)
at android.app.servertransaction.ActivityTransactionItem.getActivityClientRecord(ActivityTransactionItem.java:58)
at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:43)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:149)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:103)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2708)
at android.os.Handler.dispatchMessage(Handler.java:114)
at android.os.Looper.loopOnce(Looper.java:206)
at android.os.Looper.loop(Looper.java:296)

按照过往的手段,我们仅能等待Google官方来修复这个错误,或者先下线掉这个错误。本框架可以直接将其进行拦截,同时用户无感知。


三、其他崩溃收敛


我们针对自身业务特点,大致梳理以下几种业务中非常常见的崩溃场景,提醒每一位同学注意。同时内部维护了一个研发质量表,每个需求提测时我们会过一遍研发质量表格,提醒同学注意相关代码质量与性能。


1、空指针问题


NPE应该是最常见的问题了。针对NPE的问题,我们的解决方式有:



  • 推荐组内所有同学习惯使用注解@NonNull和@Nullable

  • 推荐大家使用Kotlin;并且在Java调用kotlin方法时,一定要注意Kotlin方法是否要求入参不为空

  • 从List、Map中取到的对象,使用前必须判空

  • 业务代码需要将Context传给第三方工具类,传入之前必须判空

  • 对象建议声明为final或者val,防止后续其他位置置空引起NPE

  • 外接传入的对象使用前必须判空

  • 基于AS插件进行检测


2、IndexOutBoundsException


角标越界异常在平时开发中也特别常见,在我们的业务中常见于集合以及Span操作



  • 集合传入index时需要判断是否在[0, size]内

  • 操作Spannable接口setSpan方法时,需要start与end的数值不会超过长度,同时不能够为负数


3、ConcurrentModificationException 并发修改异常


并发修改异常在复杂的业务中,是非常容易遇到的。通常有两个场景容易触发,分别是foreach循环中直接调用remove方法移除元素,以及线程不安全环境下使用线程不安全集合。


针对并发修改异常:



  • 我们推荐在遍历集合时,可以new一个新的List集合,将要遍历的List集合作为参数传入,然后遍历新的集合。这样原集合在遍历时改变也不会抛异常

  • 使用线程安全的集合,如CopyOnWriteArraylist、ConcurrentHashMap等


4、系统服务(FrameWork API)



  • 调用系统服务通常需要跨进程通信,其内部很可能会抛异常,所有调用系统服务的地方都必须使用try cache。cache异常必须写入日志文件,根据业务重要性判断是否需要上报埋点数据;

  • 系统服务频繁调用时可能会引发ANR,这点也需要特别注意;


5、数据库类问题


由于我们的业务重度依赖数据库,所以数据库相关的问题占比也比较高。
主要有以下几类问题:


CursorWindowAllocationException 2048问题:


com.tencent.wcdb.CursorWindowAllocationException: 
Cursor window allocation of 2048 kb failed. total:8159,active:49
at com.tencent.wcdb.CursorWindow.<init>(SourceFile:127)

针对CursorWindowAllocationException,在我们的工程中主要是短时间内大量的内存申请。 解决方案是基于SQL监控,统计工程中SQL执行的数量,基于SQL语句针对性的优化相关逻辑,将SQL语句执行数量降低了90%以上,这个问题线上不再复现。


存储空间不足


Caused by: 
com.tencent.wcdb.database.SQLiteFullException:database or disk is full (code 13,errno 0):
at com.tencent.wcdb.database.SQLiteConnection.nativeExecute(Native Method)
at com.tencent.wcdb.database.SQLiteConnection.execute(SourceFile:728)
at com.tencent.wcdb.database.SQLiteSession.endTransactionUnchecked(SourceFile:436)
at com.tencent.wcdb.database.SQLiteSession.endTransaction(SourceFile:400)
at com.tencent.wcdb.database.SQLiteDatabase.endTransaction(SourceFile:533)
at com.tencent.wcdb.room.db.WCDBDatabase.endTransaction(SourceFile:100)

针对存储空间不足,在我们的APP中主要是添加手机存储空间检测,当空间不足时引导用户清理。


数据库损坏


com tencent wcdb database.SQLiteDatabaseCorruptException: database disk image is malformed (code 11, errno 0): 
at com.tencent.wcdb.database.SQLiteConnection.nativePrepareStatement(Native Method)
at com.tencent.wcdb.database.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:1004)
at com,tencent.wcdb.database.SQLiteConnection.executeForString(SQLiteConnection.java:807)
at com.tencent.wcdb.database.SQLiteConnection.setJournalMode(SQLiteConnection.java:424)
at com.tencent.wcdb.database.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:414)
at com.tencent.wcdb.database.SQLiteConnection.open(SQLiteConnection.java:289)
at com.tencent.wcdb.database.SQLiteConnection.open(SQLiteConnection.java:254)
at com,tencent.wcdb.database.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:603)
at com.tencent.wcdb.database.SQLiteConnectionPool.open(SQLiteConnectionPool.java:225)
at com.tencent.wcdb.database.SQLiteConnectionPool.open(SQLiteConnectionPool.java:217)
at com.tencent.wcdb.database.SQLiteDatabase.openInner(SQLiteDatabase.java:1002)

数据库损坏我们是引入了修复工具进行修复,同时对数据库损坏崩溃进行拦截。修复完成后退出到登录页面引导用户重新登录。


数据库的崩溃问题一度在我们的工程中占比超过50%,所以我们有启动数据库优化专项投入大量时间。针对数据库优化的具体介绍可以查看:Android 数据库系列三:复杂项目SQL治理与数据库的优化总结
,内部有更加详细的介绍。


四、OOM问题收敛


1、OOM介绍


OOM的问题在Android中也是非常常见了,所以这里单独拎出来说说。
OOM产生的条件:待申请的内存大于系统分配给应用的剩余内存。
OOM原因大致可以归为以下几类



  • 堆内存分配失败

    • 堆内存溢出

    • 没有足够的连续内存空间



  • 创建线程失败(pthread_create (1040KB stack) failed: Try again)

  • FD数量超出限制

  • Native虚拟内存OOM


2、内存泄露监控


线上内存泄露的监控我们是使用的快手的KOOM。
KOOM原理这里笔者就不详解了,社区内也有专门分析的文章,大家可以找找看,不过还是建议去读读源码,写的挺不错的。



指路地址:github.com/KwaiAppTeam…



将KOOM分析的报告上报到我们的后台中,有专门的同学每周会排时间跟进。


3、全局浮窗实时显示APP当前总体内存


除了线上的监控,我们也有一个自研的开发者工具。工具有一个浮窗功能,我们会在浮窗上实时显示当前应用的内存信息(每秒采集一次)。数据主要是通过获取Debug.MemoryInfo#getTotalPss()。与Android Studio Profile中Memory数据基本一致。同时在UI层面我们还会设置一个阈值,超过阈值就会将浮窗中内存的数值颜色改为红色,旨在提醒开发同学关注内存变化。


通过实时显示内存我们在开发过程中,就可以发现一些问题。如我们再进入某一个业务时,发现内存会固定涨50M+,基于此开发同学去排查,发现了很多优化点。线下发现这个问题的意义就是可以质量左移,避免到线上影响到用户。


4、线程数量监控与收敛


获取线程数量,我们可以读取文件/proc/[pid]/status 中的线程数量,代码大致如下:


public static String readThreadStatus(String pid){
RandomAccessFile reader2= null;
try {
reader2 = new RandomAccessFile("/proc/" + pid + "/status", "r");
String str;
while ((str = reader2.readLine()) != null) {
if (str.contains("Threads")) {
return str;
}
}
reader2.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader2 != null) {
reader2.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return "";
}


在前面提到的开发者工具的浮窗中,我们也有一行用来实时显示线程的数量。不过在我们的工具中,我们没有使用上面的方法,而是使用:Thread.getAllStackTraces();


使用Thread.getAllStackTraces();获取的数量比 /proc/[pid]/status 获取的少,但是在我们的工程中,我们主要关注Java线程而且通过Java线程的数量的波动也能观察到App当中线程的变化。而且Thread.getAllStackTraces()会返回Thread对象,以及堆栈数据这个对我们更加有用。


在开发者工具我们有一个单独的页面可以实时查看线程的ID、名称以及对应堆栈。在线上我们会间隔一段收集一次线程数据上报到我们的后台中。


在笔者过往的开发经历中,遇到过一次由于线程数量较多直接导致应用崩溃的情况,即某个独立业务使用OkHttp没有创建OkHttpClient单例对象,而是每次接口回调都创建一个新的client...


定位过程比较简单,在特定的场景下,可以看到浮窗中的线程数量基本处于线性增长,通过开发者工具查看线程列表可以直接看到非常多的OkHttp相关的线程。


5、FD 数量监控


获取FD数量大致可以通过以下代码


public static int getCurrentFdSize() {
int size = 0;
File dir = new File("/proc/self/fd");
try {
File[] fds = dir.listFiles();
if (fds != null) {
size = fds.length;
for (File fd : fds) {
if (Build.VERSION.SDK_INT >= 21) {
MLog.d("message", Os.readlink(fd.getAbsolutePath()));
}
}
}

} catch (Exception e) {
e.printStackTrace();
}
return size;
}


同时在KOOM中每次分析的结果中会携带所有FD句柄信息,所以我们没有单独做额外的监控了,直接查看KOOM的解析数据。


笔者仅遇到过一次由于FD句柄超限导致的异常。异常信息如下


java.lang.RuntimeException: Could not read input channel file descriptors from parcel.
at android.view.InputChannel.nativeReadFromParcel(Native Method)
at android.view.InputChannel.readFromParcel(InputChannel.java:148)
at android.view.InputChannel$1.createFromParcel(InputChannel.java:39)at android.view.InputChannel$1.createFromParcel(InputChannel.java:37)
at com.android.internal.view.InputBindResult.<init>(InputBindResult.java:68)
at com.android.internal.view.InputBindResult$1.createFromParcel(InputBindResult.java:112)at com.android.internal.view.InputBindResult$1.createFromParcel(InputBindResult.java:110)at com.android.internal.view.IInputMethodManager$Stub$Proxy.startInputOrWindowGainedFocus(IInp
at android.view.inputmethod.InputMethodManager.startInputInner(InputMethodManager.java:1361)
at android.view.inputmethod.InputMethodManager.onPostWindowFocus (InputMethodManager.java:1631)
at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:4259)
at android.os.Handler.dispatchMessage(Handler.java:109)
at android.os.Looper.loop(Looper.java:166)

后面经过排查进入内置浏览器查看某网页,FD句柄的数量会瞬间飙升。通过遍历 /proc/pid/fd 文件发现大多是都是Socket。后面排查下来是应用内某个前端页面存在Bug,疯狂new Socket...


五、总结


以上简单介绍了一下我们在工程中如何针对各类崩溃信息进行收敛,值得欣喜的是经过几年的努力基本可以将崩溃控制在万一。回过头来看很多问题还是遇到问题-解决问题的思路,这就依赖我们的开发同学本身所写的代码质量要高,否则就会陷入到写Bug-改Bug这样的循环中。


所以我们也在积极探索如何通过前期的review,工具扫描的方式尽量降低线上问题的发生概率。尽可能将问题提前暴露。不过这方面目前还没有建设的特别好,人工review实验了一段时间发现在一个业务较多的团队的实施起来很难,没有时间不说盯着代码review也不见得就能发现一些逻辑上的异常。好在公司内其他团队再研究基于AI的代码扫描,后续计划接入到当项目中看看。


作者:半山居士
来源:juejin.cn/post/7377200392059617295
收起阅读 »

未登录也能知道你是谁?浏览器指纹了解一下!

web
引言 大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢? 本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。 浏览器指纹 浏览器指纹是指通过浏览器的特征来唯一标识用户身份的...
继续阅读 »

引言


大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢?


本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。


浏览器指纹


浏览器指纹是指通过浏览器的特征来唯一标识用户身份的一种技术。


它通过记录用户浏览器的一些基本信息,包括操作系统、浏览器类型、浏览器版本、屏幕分辨率、字体、颜色深度、插件、时间戳等,通过这些信息,可以唯一标识用户身份。


应用场景


其实浏览器指纹这类的技术已经被运用的很广泛了,通常都是用在一些网站用途上,比如:



  • 资讯等网站:精准推送一些你感兴趣的资讯给你看

  • 购物网站: 精确推送一些你近期浏览量比较多的商品展示给你看

  • 广告投放: 有一些网站是会有根据你的喜好,去投放不同的广告给你看的,大家在一些网站上经常会看到广告投放吧?

  • 网站防刷: 有了浏览器指纹,就可以防止一些恶意用户的恶意刷浏览量,因为后端可以通过浏览器指纹认得这些恶意用户,所以可以防止这些用户的恶意行为

  • 网站统计: 通过浏览器指纹,网站可以统计用户的访问信息,比如用户的地理位置、访问时间、访问频率等,从而更好的为用户提供服务


如何获取浏览器指纹


指纹算法有很多,这里介绍一个网站 https://browserleaks.com/ 上面介绍了很多种指纹,可以根据自己的需要选择。



这里我们看一看canvas,可以看到光靠一个canvas的信息区分,就可以做到15万用户只有7个是重复的,如果结合其他信息,那么就可以做到更精准的识别。


canvas指纹


canvas指纹的原理就是通过 canvas 生成一张图片,然后将图片的像素点信息记录下来,作为指纹信息。


不同的浏览器、操作系统、cpu、显卡等等,画出来的 canvas 是不一样的,甚至可能是唯一的。


具体步骤如下:



  1. 用canvas 绘制一个图像,在画布上渲染图像的方式可能因web浏览器、操作系统、图形卡和其他因素而异,从而生成可用于创建指纹的唯一图像。在画布上呈现文本的方式也可能因不同web浏览器和操作系统使用的字体渲染设置和抗锯齿算法而异。




  1. 要从画布生成签名,我们需要通过调用toDataURL() 函数从应用程序的内存中提取像素。此函数返回表示二进制图像文件的 base64 编码字符串。然后,我们可以计算该字符串的MD5哈希来获得画布指纹。或者,我们可以从IDAT块中提取 CRC校验和IDAT块 位于每个 PNG 文件末尾的16到12个字节处,并将其用作画布指纹。


我们来看看结果,可以知道,无论是否在无痕模式下,都可以生成相同的 canvas 指纹。
在这里插入图片描述


换台设备试试



其他浏览器指纹


除了canvas,还有很多其他的浏览器指纹,比如:


WebGL 指纹


WebGL(Web图形库)是一个 JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D2D 图形,而无需使用插件。


WebGL 通过引入一个与 OpenGL ES 2.0 非常一致的 API 来做到这一点,该 API 可以在 HTML5 元素中使用。


这种一致性使 API 可以利用用户设备提供的硬件图形加速。


网站可以利用 WebGL 来识别设备指纹,一般可以用两种方式来做到指纹生产:


WebGL 报告——完整的 WebGL 浏览器报告表是可获取、可被检测的。在一些情况下,它会被转换成为哈希值以便更快地进行分析。


WebGL 图像 ——渲染和转换为哈希值的隐藏 3D 图像。由于最终结果取决于进行计算的硬件设备,因此此方法会为设备及其驱动程序的不同组合生成唯一值。这种方式为不同的设备组合和驱动程序生成了唯一值。


可以通过 Browserleaks test 检测网站来查看网站可以通过该 API 获取哪些信息。


产生 WebGL 指纹原理是首先需要用着色器(shaders)绘制一个梯度对象,并将这个图片转换为Base64 字符串。


然后枚举 WebGL 所有的拓展和功能,并将他们添加到 Base64 字符串上,从而产生一个巨大的字符串,这个字符串在每台设备上可能是非常独特的。


例如 fingerprint2js 库的 WebGL 指纹生产方式:


HTTP标头


每当浏览器向服务器发送请求时,它会附带一个HTTP标头,其中包含了诸如浏览器类型、操作系统、语言偏好等信息。


这些信息可以帮助网站优化用户体验,但同时也能用来识别和追踪用户。


屏幕分辨率


屏幕分辨率指的是浏览器窗口的大小和设备屏幕的能力,这个参数因用户设备的不同而有所差异,为浏览器指纹提供了又一个独特的数据点。


时区


用户设备的本地时间和日期设置可以透露其地理位置信息,这对于需要提供地区特定内容的服务来说是很有价值的。


浏览器插件


用户安装的插件列表是非常独特的,可以帮助形成识别个体的浏览器指纹。


音频和视频指纹


通过分析浏览器处理音频和视频的方式,网站可以获取关于用户设备音频和视频硬件的信息,这也可以用来构建用户的浏览器指纹。


webgl指纹案例


那么如何防止浏览器指纹呢?


先讲结论,成本比较高,一般人不会使用。


现在开始实践,根据上述的原理,我们知道了如何生成一个浏览器指纹,我们只需要它在获取toDataURL时,修改其中的内容,那么结果就回产生差异,从而无法通过浏览器指纹进行识别。


那么,我们如何修改toDataURL的内容呢?


我们不知道它会在哪里调用,所以我们只能通过修改它的原型链来修改。


又或者使用专门的指纹浏览,该浏览器可以随意切换js版本等信息来造成无序的随机值。


修改 toDataURL


第三方指纹库


FingerprintJS



FingerprintJS是一个源代码可用的客户端浏览器指纹库,用于查询浏览器属性并从中计算散列访问者标识符。


cookie和本地存储不同,指纹在匿名/私人模式下保持不变,即使浏览器数据被清除。


ClientJS Library



ClientJS 是另一个常用的JavaScript库,它通过检测浏览器的多个属性来生成指纹。


该库提供了易于使用的接口,适用于多种浏览器指纹应用场景。




作者:我码玄黄
来源:juejin.cn/post/7382344353069088803
收起阅读 »

h5 如何跳转微信小程序(uni-app)?

web
前几天组员遇到h5跳转微信小程序的功能,由于时间关系,我这边接手实现这个功能。 遇到问题点 微信内部跳转怎么实现? 外部浏览器或者app打开链接如何跳转? 微信内部跳转怎么实现 使用 wx-open-launch-weapp 这边标签进行跳转,使用这个标签...
继续阅读 »

前几天组员遇到h5跳转微信小程序的功能,由于时间关系,我这边接手实现这个功能。


遇到问题点



  • 微信内部跳转怎么实现?

  • 外部浏览器或者app打开链接如何跳转?


微信内部跳转怎么实现

使用 wx-open-launch-weapp 这边标签进行跳转,使用这个标签跳转小程序,需要满足3个条件



  • 首先 wx.config 授权,公众号设置 > 功能设置 > js安全域名设置

  • 域名设置,静态资源托管

  • 小程序需要关联这个公众号 设置 > 基本设置 > 相关公众号


h5上代码源码



<html>
<head>
<title>打开小程序</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<script>
window.onerror = e => {
console.error(e)
alert('发生错误' + e)
}
</script>
<!-- 引入jQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<!-- weui 样式 -->
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.4.1/weui.min.css"></link>
<!-- 调试用的移动端 console -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
<!-- 公众号 JSSDK -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!-- 云开发 Web SDK -->
<script src="https://res.wx.qq.com/open/js/cloudbase/1.1.0/cloud.js"></script>
<script>
const baseUrl ="/pages/biddingCenter/index"
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}

function docReady(fn) {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
fn()
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
var version = 'trial'
var fetchData = new Promise((resolve, reject) => {
// 获取签名,timestamp、nonceStr、signature
$.ajax({
url: "xxxxxxx",
dataType: "json",
method: 'get',
data: { xxx },

success: function (res) {
console.log("WeChatConfig", res);
if (res.data) {
var data = res.data; // 根据实际情况返还的数据进行赋值
console.log(999, data)
resolve(data);
}
},
error: function (error) {
console.error('error-->', error)
return reject(error)
}
})
});



docReady(async function() {
var ua = navigator.userAgent.toLowerCase()
var isWXWork = ua.match(/wxwork/i) == 'wxwork'
var isWeixin = !isWXWork && ua.match(/MicroMessenger/i) == 'micromessenger'
var isMobile = false
var isDesktop = false
if (navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|IEMobile)/i)) {
isMobile = true
} else {
isDesktop = true
}
console.warn('ua', ua)
console.warn(ua.match(/MicroMessenger/i) == 'micromessenger')
var m = ua.match(/MicroMessenger/i)
console.warn(m && m[0] === 'micromessenger')

if (isWeixin) {
var containerEl = document.getElementById('wechat-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'wechat-web-container')

var launchBtn = document.getElementById('launch-btn')
launchBtn.addEventListener('ready', function (e) {

launchBtn.setAttribute('env-version',version)
launchBtn.setAttribute('path', baseUrl)

console.log('开放标签 ready')
})
launchBtn.addEventListener('launch', function (e) {
console.log('开放标签 success')
})
launchBtn.addEventListener('error', function (e) {
console.log('开放标签 fail', e.detail)
})

await fetchData.then(res=> {
wx.config({
// debug: true, // 调试时可开启
appId: res.appId,
timestamp: res.timestamp, // 必填,填任意数字即可
nonceStr: res.nonceStr, // 必填,填任意非空字符串即可
signature: res.signature, // 必填,填任意非空字符串即可
jsApiList: ['chooseImage'], // 安卓上必填一个,随机即可
openTagList:['wx-open-launch-weapp'], // 填入打开小程序的开放标签名
})
})

} else if (isDesktop) {
// 在 pc 上则给提示引导到手机端打开
var containerEl = document.getElementById('desktop-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'desktop-web-container')
} else {

var containerEl = document.getElementById('public-web-container')
containerEl.classList.remove('hidden')
containerEl.classList.add('full', 'public-web-container')
var c = new cloud.Cloud({
// 必填,表示是未登录模式
identityless: true,
// 资源方 AppID
resourceAppid: 'wxaabe7e5d652fc1d8',
// 资源方环境 ID
resourceEnv: 'cloud1-7gnlxfema33074ec',
})
await c.init()
console.log(c)
window.c = c

var buttonEl = document.getElementById('public-web-jump-button')
var buttonLoadingEl = document.getElementById('public-web-jump-button-loading')
try {
await openWeapp(() => {
console.log('dui')
buttonEl.classList.remove('weui-btn_loading')
buttonLoadingEl.classList.add('hidden')
})
} catch (e) {
console.log(e,'cuo')
buttonEl.classList.remove('weui-btn_loading')
buttonLoadingEl.classList.add('hidden')
throw e
}
}
})

async function openWeapp(onBeforeJump) {
var c = window.c
const res = await c.callFunction({
name: 'public',
data: {
action: 'getUrlScheme',

},
})

if (onBeforeJump) {
onBeforeJump()
}
location.href = res.result.openlink
}
</script>
<style>
.hidden {
display: none;
}

.full {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}

.public-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.public-web-container p {
position: absolute;
top: 40%;
}

.public-web-container a {
position: absolute;
bottom: 40%;
}

.wechat-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.wechat-web-container p {
position: absolute;
top: 40%;
}

.wechat-web-container wx-open-launch-weapp {
position: absolute;
bottom: 40%;
left: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
}

.desktop-web-container {
display: flex;
flex-direction: column;
align-items: center;
}

.desktop-web-container p {
position: absolute;
top: 40%;
}
</style>
</head>
<body>
<div class="page full">
<div id="public-web-container" class="hidden">
<p class="">正在打开 “ 小程序”...</p>
<a id="public-web-jump-button" href="javascript:" class="weui-btn weui-btn_primary weui-btn_loading" onclick="openWeapp()">
<span id="public-web-jump-button-loading" class="weui-primary-loading weui-primary-loading_transparent"><i class="weui-primary-loading__dot"></i></span>
小程序
</a>
</div>
<div id="wechat-web-container" class="hidden">
<p class="">点击以下按钮打开 “小程序”</p>
<!-- 跳转小程序的开放标签。文档 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html -->
<wx-open-launch-weapp id="launch-btn" username="gh_36a8dca79910" path="/pages/biddingCenter/index" env-version="trial">
<template>
<button style="width: 200px; height: 45px; text-align: center; font-size: 17px; display: block; margin: 0 auto; padding: 8px 24px; border: none; border-radius: 4px; background-color: #07c160; color:#fff;">打开小程序</button>
</template>
</wx-open-launch-weapp>
</div>
<div id="desktop-web-container" class="hidden">
<p class="">请在手机打开网页链接</p>
</div>
</div>
</body>
</html>

以上第二点后面会讲到
以上功能满足基本可以在微信内部打开小程序了,但是实际上,我们url可能是其他app,短信,或者浏览器里面打开的


外部app或者短信跳转怎么实现

首先我这边是uni-app实现的



  • 第一步在项目根目录项目新建functions这个文件夹,然后到


image.png
点击下载放入刚刚的functions下面,然后再 manifest.json 下面


 "mp-weixin" : {
"appid" : "xxxxx",
"cloudfunctionRoot": "./functions/", // 这一行就是标记云函数目录的字段
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"permission" : {}
}

这个时候编译项目在编译之后的小程序项目是没有functions 这个文件夹的,我们这个时候需要在vue.config.js


const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.join(__dirname, 'functions'),
to: path.join(__dirname, 'unpackage/dist', process.env.NODE_ENV === 'production' ? 'build' : 'dev', process.env.UNI_PLATFORM, 'functions')
}
]
})
]
}
}

在终端上面npm install 才行,需要node环境
设置以上代码重新启动编译,在微信工具里面就会出现


image.png


image.png


右击public上传并部署(云端安装依赖),成功之后打开微信开发工具云开发


image.png


image.png


会出现刚刚的云函数文件
然后再次点击云函数权限


image.png
点击所有用户访问,保存


image.png


然后再点击右上角设置>权限设置> 未登录用户访问云资源权限设置打开


image.png


以上搞定也只是函数环境搞定,然后还要配置刚刚的函数方法api


image.png


只有配置这个才能在index方法里面调用


// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 云函数入口函数
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()

switch (event.action) {
case 'getUrlScheme': {
return getUrlScheme(event)
}
}

return 'action not found'
}

async function getUrlScheme(event) {

return cloud.openapi.urlscheme.generate({
jumpWxa: {
path:'/pages/index/index', // <!-- replace -->
env_version:'trial',
},
// 如果想不过期则置为 false,并可以存到数据库
isExpire: false,
// 一分钟有效期
expireTime: parseInt(Date.now() / 1000 + 60),
})
}


代码讲到这边会发现之前h5里面调用的 c.callFunction 方法其实调用的就是exports.main 这个导出得方法
然后还需要把h5代码放入


image.png


image.png
以上就是整个h5跳转微信小程序的过程,需要注意的点事在h5里面 方法返回 res.result.openlink 这个参数需要跳转的url在生产上线过,要不然报错 openapi.urlscheme.generate pagepath 获取不到


作者:成书平
来源:juejin.cn/post/7264772939773526075
收起阅读 »

25k大专前端外包从深圳回武汉能拿多少?

2023 年 08 月我正式从深圳公司离职,从 7 月初开始投武汉的公司,截止 8 月底,2 个月时间有 5 个面试 3 个 offer:一个自研的 22k、两个外包一个 17k,一个 18k。最终选了一家离我比较近的 18k offer,但遗憾的是刚去第一天...
继续阅读 »

2023 年 08 月我正式从深圳公司离职,从 7 月初开始投武汉的公司,截止 8 月底,2 个月时间有 5 个面试 3 个 offer:一个自研的 22k、两个外包一个 17k,一个 18k。最终选了一家离我比较近的 18k offer,但遗憾的是刚去第一天就发现坑太深,还是决定放弃这家公司,目前失业在家,这里和大家聊一聊最近从深圳回武汉找工作的经历。

基本情况

先大致介绍下我的基本情况:大专学历,30+,从 17 年开始做全职前端开发,到现在有 6-7 年了,属于一年工作经验用 5 年的那种,Vue 一把梭,技术一般。进不了中、大厂,只能在外包混混日子。

时间回到两年前,2021 年 8 月前端行情好的时候,我凭运气找到了一家深圳二线互联网公司的前端外包岗位,offer 是 25k * 12,到今年 8 月正好呆了两年左右,为了说明真实性,下面附上我 2022 年度个税 App 收入纳税截图。

2-2022.png

声明

本人在网上冲浪时从未公开过这家我呆了两年的公司,出于薪资保密原则,如果有人认出我,知道我所在的这家公司,还请不要透露公司名字,万分感谢!

另外,为了避免纠纷,后面面试的公司,我都会进行匿名处理,如果有人猜到公司名称,评论还请使用化名,希望大家能理解。

深圳回武汉

从 21 年入职这家公司开始,这两年前端行情越来越差,目前我这个学历、技术水平比较难找到 25k 以上的工作。我有考虑过要不要先苟在这家公司,毕竟这个工作工资还可以,leader、同事、工作氛围都不错。但由于各方面原因,最终还是决定回武汉。

  1. 工作方面:我做的大多是技术需求,做的比较无聊,成就感较低;另外,新需求越来越少,蛋糕就这么大,僧多粥少,发挥空间较小。
  2. 生活方面:我时常在反思,我是不是一个精致的利己主义者?这些年基本就过年回家,回家也呆不了几天,在照顾父母、关心家人这方面我是做的比较差的,如果我只想着自己能不能拿高工资,自己过的是否惬意,我觉得这是很自私的,回武汉离家近可以很好的解决这个问题。

在业务需求少后,部门也有了裁员的消息,我正好在这边快两年了,也想回武汉换个环境。

为了变被动为主动,就在 7 月初开始投武汉的公司了,计划拿到 offer 就离职回去。因为在行业下行周期,越想往上挣扎越累,还不一定有好的结果,不如顺势躺平,好好享受生活。

简历投递面试数据

23 年 6 月 20 号左右,将简历开放,状态修改为在职-看机会。过了一段时间,发现没 hr 联系我,行情确实差了很多,之前简历一开放,一堆 hr 主动找你,这个时候还没主动投。

一直到 23 年 7 月 3 号,我终于修改好了简历,开始投简历。但如下图,简历比较难投出去,需要双方回复才能投。

3-boss-huifu.png

于是我又下载了拉钩、猎聘。拉钩猎聘大部分都可以直接投,但拉钩 20k+ 武汉的岗位很少,猎聘投了很多也没回复,整体还是 BOSS 上面试机会最多,下面是具体数据

App 类型沟通投递面试机会面试通过/Offer
BOSS13186142
猎聘-29411
拉钩-62(投递反馈)00

卡学历问题

我基本把武汉的 20k+ 前端岗位都投了一遍,但基本没有中、大厂都能通过简历筛选。分三种情况

  1. 没有任何回复(最多)
  2. 回复看了简历不合适(个别)
  3. 直接指出学历不符合(个别)

4-xueli.png

虽然我有自考本科+学士学位也没啥用,一般还是至少要统招本科及以上。当然也有可能会是年龄、技术菜、要的工资高等其他因素。

4-2-xueli.png

面试记录

某电商小公司 - 自研 22k(过)

来源:猎聘 App,岗位:中高级前端开发工程师(自研)(14-22k)

2023 年 7 月 10 号,在投了一个星期后,终于有了第一个面试,晚上 19:00 腾讯会议远程面,大概面了一个小时,问的问题不难,比如

  • 先自我介绍
  • 垂直居中有几种方式?
  • flex: 2 有用过吗?多列布局怎么实现?
  • 怎么判断对象为空?
  • 寻找字符串中出现最多的字符怎么实现?
  • 知不知道最新的 url 参数获取的 API?
  • 实现深拷贝
  • 实现 Promise
  • 新版本发布后,怎么用技术手段通知用户刷新页面?
  • 性能优化数据怎么上报、分析?
  • Vue 组件通信方式有哪些,各有什么特点?
  • Vue 项目怎么提高项目性能?举一些例子
  • element ui table 吸顶怎么做,滚动怎么处理等
  • 你有什么想问我的?

然后还问了一些项目问题,能不能加班,因为虽然双休,但周一到周五会有 3 天加班等。基本没有问啥原理性的问题,就是看基础怎么样,能不能干活。

面试第二天,没有消息,我以为挂了,但隔了一天,7 月 12 号,HR 电话二面,我问了我的一些基本情况后,表示可以直接发 offer,确定薪资为 22k,但其中 2.2k 要当做季度绩效发放,说的是一般不犯啥错误都可以拿到。下面是 offer 截图

5-offer-1.png

沟通入职时间定的 8 月 1 号,比较坑的是甲方都同意 7 月底可以走,外包公司这边不同意,要到 8 月中才放我走,合同确实是这样写的,我也不好说啥。

这家公司比较着急,觉得等的时间有点长了,1个月+,风险有点高。我也不能说让别人一直等,只能说,让他们可以先考虑其他候选人,这家公司过了段时间招到人了,这个 offer 就黄了。

(后面回想起来,我可能有点傻,规定是死的,人是活的,应该直接按甲方允许的 7 月底时间来,这样 offer 就没问题了。如果我们公司不让我走,我可以直接走人,就当旷工,直接被开除就行,只是没有离职证明,但工资流水是有的)

武汉某小公司 - 自研 (12-20k)x

来源:BOSS,岗位:前端开发工程师 - 自研(12-20k)14薪

在上面的 22k 这个 offer 时间有冲突的时候,我就意识到这个 offer 有风险,就开始继续投了。

到 23 年 8 月 2 号终于又有了面试机会,一面是笔试,如下图

6-hema.png

有 4 题,最后一题最简单,第 1、2 题忘记了,1、2、3 我都是用递归实现的,3、4 题如下

  1. _.flatten() 实现一个数组打平方法,支持第二个参数(可指定打平层级)
const array = [[0, 1], [2, [3, 4]], [5, 6]];
const result = _.flatten(array);
  1. 菜单数组转换为嵌套树形结构,但示例只有两级
[
{ id: 1, menu: '水果', level: 1 },
{ id: 2, menu: '橘子', level: 2, parentId: 1 }
// ...
]
// 转换为
[
{
id: 1, menu: '水果', level: 1, children: [{ id: 2, menu: '橘子', level: 2, parentId: 1 }]
},
// ...
]

笔试难度一般,主要靠思维,难度比 leetcode 算法题低,算是过了。

二面是 8 月 7 号电话面,19:00 - 20:00 一个小时左右,大部分问题都忘记了,模糊记得部分问题

  • 先自我介绍
  • 把之前的笔试题一题一题拿出来讲实现思路。
  • 对象的继承有哪几种?
  • TS 用的多吗?
  • 工作中解决的最有成就感的事?
  • vue3 在某些场景比 vue2 性能更低,为什么会这样?
  • 在团队协作时,有遇到过什么问题吗,如果有冲突你会怎么做
  • 你有什么想问我的?

另外面试小哥对我之前有两家半年左右的工作经历比较在意,问了很多之前公司的细节,因为他说之前有面试过的最后背调没通过,所以要问清楚。我的简历写的很真实,基本没有水分,是什么就是什么。

他最后透露,可能就算他可以过,但 HR 那边可能过不了,不知道是我跳槽太频繁还是啥,总之后面基本没消息了,这个算是挂了。

某上海武汉分公司 - 自研(18-23k)x

来源:BOSS,岗位:前端开发 自研(18-23k)

上次面试的挂了之后,继续投,但没面试机会,后面又忙搬家、邮寄东西,回武汉,找房子等,中间大概用了一个多星期。

在 8 月 18 号终于又有了一个自研的面试, 15:40 腾讯会议线上一面 - 技术面,上海那边的开发负责面试,问了一些问题,比较普通,我现在毫无印象。

一面过了,在 8 月 22 日,13:00 二面(现场面),公司办公地点在武昌火车站地铁口,刚开始觉得还不错,但一进去,一个开发都没有,就 1 个人,直接无语...... 武汉算是分部,那个人还不懂技术,和我吹了一下公司怎么怎么厉害,先是做了一份笔试题(比较基础)比如

  • 3 种方式实现顶部导航+左侧菜单+右侧主内容区域布局
  • jwt 鉴权逻辑
  • vue 数组下标改值,响应式丢失、为什么

7-hangshu.png

然后那个人拍了我写的笔试题,让上海那边的人看,说是做的不错。再视频连线进行面试,大致问了一些基础问题,然后坑的地方来了。我之前待过的公司,一个一个问我离职原因。。。。。。

然后就是副总面,问我有没有做过异形屏的适配,有没有写过绘制、渲染逻辑,我。。。。。。然后又问了我好几个假大空的问题,我一脸懵逼,比如一个公司呆 8 年和 8 年每年换一家公司你觉得哪种好。

后面就是回去等消息了,然后就没有然后了。。。。。

某金融公司 - 外包 17k(过)

来源:BOSS,岗位:前端开发 - 外包 17k

和上面那个公司同一时间段,在 8 月 18 号也进行了这家公司的腾讯会议一面

一面比较简单,大致为了下工作经历,重点问了下低代码、怎么动态加载渲染一个组件,底层怎么实现?面试时间比较短,有点仓促

8 月 21 号二面,大致问了一些问题后,还是追问低代码方面的问题,组件级别、可以内嵌到其他指定页面的这种低代码 sdk 封装怎么做?他们是想招个会低代码,有过 sdk 封装经验的。我之前工作中有做过组件库,封装过百万用户级别的小程序 sdk、也做过功能引导、错误上报等 sdk,还自己实现过多个 npm 包轮子,算是勉强符合他们的要求。

二面过了后,开始谈薪资,17k,基本不加班,8 月 23 号三面笔试(类似走过场),有题库,刷一下就没问题,通过就发邮件 offer 了。

8-zhengquan.png

这家公司过了,但我没有接轻易接 offer,而是让 HR 等第二天中午我的反馈,我不想接了别人 offer 又不去。这家公司的 HR 比较好、很热心积极。

主要有以下几个原因

  1. 后面还有一个 18k 的也是同一天二面,且面试体验好,大概率过了,只等确定 offer。
  2. 这家比较远,在花山,而后面一家离我比较近
  3. 这家试用期打折,下面一家不打折。

最终拒了这家 offer,因为下面要讲的这家 offer 下来了,前方高能预警,后面这家公司巨坑、后悔拒了这家。。。。

某互联网公司 - 外包 18k(过)

来源:BOSS,岗位:前端开发(外包)18k

和上面那家几乎同一时间,这家公司也进行了两轮面试

一面,腾讯会议,从 3-4 个 UI 中,选一个题来实现,30 分钟,就是平常干活画 UI,难度不大,面试官是个声音好听的妹子。

二面,腾讯会议,结对编程,面试官出题,我描述实现,面试官写代码,包括

  1. 一个简单的需要使用 Promise 应用题
  2. 运行一个 vue 项目,vue2 写法改 vue3 写法,封装一个计时器组件,组件加 props,组件加插槽等

面试体验真的很好,18k offer 下来后,果断选择了这家离我近的公司。

9-offer-3.png

但没想到的是,入职第一天发现这家公司管理问题很大

  1. 开发环境差,只能用网页版的 vscode,除了要配置 host 外,还有配置端口映射,配置稍微有问题就运行不起来,体验较差。
  2. 沟通太依赖线上,武汉这边基本是xx一线城市那边的产品、UI、开发分配任务给这边开发,沟通成本非常高。
  3. 加班问题,说的是早 9 晚 6,但他们自研一般下班这个点可能会去吃个饭, 然后回来加班,git log 看了下提交记录,不少是 20:00 之后的,还有 21 点、22 点之后的.... 如果真融入这个团队,不加班我是不信的。

从面试体验、沟通来看,这里的开发人员是优秀的,但实际入职却发现环境、氛围差的情况,我只能把这种问题归纳到管理上了。

第一天基本没干活就是配置环境,但这个氛围,我真的接受不了,后面就果断放弃这家公司了。

武汉找工作经验总结

上面我大致描述了从 7 月初到 8 月底的简历投递、面试经历。主要是面试少,实际面试通过率为 60%。下面是一些总结

  • 投递简历时段最好是周一到周三上午 8-9 点,回复、面试机会较多,周五到周天基本没反应。
  • 武汉原理性问的不多,主要还是能干活,比较需要多面手,就是什么都会的,比如 WebGL, Three.js,uni-app 等
  • 一定要问清楚、开发环境、加班问题,不要不好意思,能找自研就尽量找自研。
  • 不要听 HR 或者面试官怎么说,而是自己通过行业、所做的业务去判断是否有坑。

完结撒花,如果觉得内容对您有帮助,那就点个免费的赞吧~~

另外最近有和我一样在找工作的小伙伴吗?你们有遇到过什么坑吗?欢迎在评论区讨论~~~


作者:dev_zuo
来源:juejin.cn/post/7275225948453568552
收起阅读 »

【小程序分包】小程序包大于2M,来这教你分包啊

web
前言🍊缘由该大的不大,小程序包超出2M,无法上传发布前段时间项目迭代时,因版本大升级,导致uniapp打包后小程序后,包体积大于2M。虽然将图片等静态资源压缩,体积大的资源放置cdn,在不懈的努力下,治标不治本,包体积还是不听话的长到2M以上。憋的实在没办法,...
继续阅读 »

前言

🍊缘由

该大的不大,小程序包超出2M,无法上传发布

前段时间项目迭代时,因版本大升级,导致uniapp打包后小程序后,包体积大于2M。虽然将图片等静态资源压缩,体积大的资源放置cdn,在不懈的努力下,治标不治本,包体积还是不听话的长到2M以上。憋的实在没办法,遂将小程序分包,彻底解除封印,特来跟大家分享下如何将小程序分包,减小主包大小


🎯主要目标

实现2大重点

  1. 如何进行小程序分包
  2. 如个根据分包调整配置文件

🍈猜你想问

如何与狗哥联系进行探讨

关注公众号【JavaDog程序狗】

公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹。

此群优势:

  1. 技术交流随时沟通
  2. 任何私活资源免费分享
  3. 实时科技动态抢先知晓
  4. CSDN资源免费下载
  5. 本人一切源码均群内开源,可免费使用

2.踩踩狗哥博客

javadog.net

大家可以在里面留言,随意发挥,有问必答


🍯猜你喜欢

文章推荐

正文

🍵三个问题

  1. 为什么小程序会有2M的限制?
  1. 用户体验:小程序要求在用户进入小程序前能够快速加载,以提供良好的用户体验。限制小程序的体积可以确保小程序能够在较短的时间内下载和启动,避免用户长时间的等待。
  2. 网络条件:考虑到不同地区和网络条件的差异,限制小程序的体积可以确保在低速网络环境下也能够较快地加载和打开小程序,提供更广泛的用户覆盖。
  3. 设备存储:一些用户使用的设备可能存储空间有限,限制小程序的体积可以确保小程序可以在这些设备上正常安装和运行。
  1. 如何解决包过大问题?
  1. 优化代码,删除掉不用的代码
  2. 图片压缩或者上传服务器
  3. 分包加载
  1. 什么是分包加载?

小程序一般都是由某几个功能组成,通常这几个功能之间是独立的,但会依赖一些公共的逻辑,且这些功能一般会对应某几个独立的页面。那么小程序代码的打包,可以按照功能的划分,拆分成几个分包,当需要用到某个功能时,才加载这个功能对应的分包。


🧀实操分包步骤

1.查看项目结构

通过上方三个问题,我们开始具体分包流程,首先看一下分包前项目结构pages.json配置文件

pages.json
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/card/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/device/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/order/index",
"style": {
"navigationBarTitleText": "uni-app"
}
},
{
"path": "pages/product/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

2.分析主包大小

微信开发者工具中,查看【详情】进行分析,此处本地代码只有一个主包大小399.8KB

3.参考文档

本文以uniapp为实操介绍案例

小程序官方文档:

developers.weixin.qq.com/miniprogram…

uniapp 分包文档:

uniapp.dcloud.net.cn/collocation…

4. 结构调整

将咱们项目结构按照如下图所示进行拆分

新建subPages_A 和 subPages_B,将pages下不同页面移入进新增的两个包,此处subPages_A的名字只做示例,实际要按照标准命名!

比较下之前项目结构,此处项目会报错,不用担心,稍后修改pages.json


5. 修改pages.json

根据上一步拆分的包路径,进行配置文件的调整,此处注意"subPackages" 要和 "pages" 同级

{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"subPackages": [{
"root": "pages/subPages_A",
"pages": [{
"path": "card/index",
"style": {
"navigationBarTitleText": "card"
}
},
{
"path": "device/index",
"style": {
"navigationBarTitleText": "device"
}
}
]
}, {
"root": "pages/subPages_B",
"pages": [{
"path": "order/index",
"style": {
"navigationBarTitleText": "order"
}
},
{
"path": "product/index",
"style": {
"navigationBarTitleText": "product"
}
}
]
}],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

这里的意思是将主包拆成subPages_A和subPages_B两个子包,对比下之前的配置


6. 启动测试

启动后查看微信开发者工具,查看【详情】可看到主包大小降为326.0kb,并且下方还有subPages_A和subPages_B两个子包

比较之前包大小,分包成功!


7. 特别注意

🎯 如果设计代码中路径问题,需要调成最新包结构路径。例如

拆包前跳转到对应设备页面

uni.navigateTo({
url:'/pages/device/index'
})

拆包后跳转到对应设备页面

uni.navigateTo({
url:'/pages/subPages_A/device/index'
})

切记如果拆包后所有路径问题需要统一修改,否则则会报错!!!


总结

本文通过实际demo进行uniapp小程序拆包,通过分析项目主包大小,查看官方文档,按照功能划分进行子包拆分,如果还有博友存在疑问或者不理解可以在上方与本狗联系,或者查看本狗发布在上方的代码,希望可以帮到大家。



作者:JavaDog程序狗
来源:juejin.cn/post/7270445774324367394

收起阅读 »

产品经理:优惠金额只入不舍,支付金额只舍不入...

web
前言 当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。 优惠券分为:折扣券(n折)、抵扣券(减x元) 需求 优惠金额、支付金额都需要保留两位小数。 优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些 支付金...
继续阅读 »

前言


当前做的项目是一个售卖会员的平台。其中涉及到优惠券、支付金额等。

优惠券分为:折扣券(n折)、抵扣券(减x元)


需求


优惠金额、支付金额都需要保留两位小数。

优惠金额只入不舍,比如18.811元,显示为:18.82元。这样看起来,优惠的相对多一些

支付金额只舍不入,比如18.888元,显示为:18.88元。

从产品角度来讲,这个设计相当人性化。


实现


/**
* 金额计算
* @param {number} a sku原始价格
* @param {number} b 优惠券金额/折扣
* @param {string} mathFunc 四舍五入:round/ceil/floor
* @param {string} type 计算方式默认减法
* @param {digits} type 精度,默认两位小数
* */

export function numbleCalc(a, b,mathFunc='round', type = '-',digits=2) {

var getDecimalLen = num =>{
return num.toString().split('.')[1] ? num.toString().split('.')[1].length : 0;
}
//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}


var c;
//获取2个数字中,最长的小数位数
var aLen = getDecimalLen(a);
var bLen = getDecimalLen(b);
var decimalLen = aLen>bLen?aLen:bLen;
var mul = decimalLen>0?(10 ** decimalLen):1;

//转换成整数
var aInteger = floatToInt(a,aLen,decimalLen)
var bInteger = floatToInt(b,bLen,decimalLen)


if(type=='-'){
c = (aInteger - bInteger)/mul;
}else if(type=='*'){
c = aInteger * bInteger/mul/mul;
}

c = digits==0?c : Math.round(c * (10 ** digits));

if(mathFunc=='floor'){
c= Math.floor(c);
}else if(mathFunc=='ceil'){
c= Math.ceil(c);
}else {
c= Math.round(c);
}
return digits==0?c : c/(10**digits);
}



整体思路:获取两个数字之间最大的小数位,先取整再计算。
不直接进行计算,是因为存在0.1+0.2!=0.3的情况,具体原因可以看下文章下方的参考链接,写的很详细。




  • Math.ceil()  总是向上舍入,并返回大于等于给定数字的最小整数。

  • Math.floor()  函数总是返回小于等于一个给定数字的最大整数。

  • Math.round() 四舍五入


【重点】小数位取整:我之前的写法原来是错误的


image.png
我一直以来也是这种形式,预想的是直接乘100变成整数,但是出现了以下情况


19.9 * 100 = 1989.9999999999998
5.02 * 100 = 501.99999999999994

可以看到,出现了意料之外的结果!!

最后采用的方案是:将小数转成字符串,再将小数点替换成空格


//将小数按照一定的倍数转换成整数数
var floatToInt = (num,numlen,maxlen)=>{
var numInt = num.toString().replace('.', '');
if(numlen==maxlen)return +numInt;
return numInt * (10**(maxlen-numlen))
}

总结


省流:将小数点替换成空格,变成整数,再进行相应计算。

封装的这个函数,只考虑了当前业务场景,未兼容一些边界值情况。



  • 大金额计算问题

  • 计算方式:加法、除法未做处理


参考


# 前端金额运算精度丢失问题及解决方案


作者:前端大明
来源:juejin.cn/post/7341210909069770792
收起阅读 »

Alpha系统联结大数据、GPT两大功能,助力律所管理降本增效

如何通过AI工具实现法律服务的提质增效,是每一位法律人都积极关注和学习的课题。但从AI技术火爆一下,法律人一直缺乏系统、实用的学习资料,来掌握在法律场景下AI的使用技巧。今年5月,iCourt携手贵阳律协大数据与人工智能专业委员会,联合举办了《人工智能助力律师...
继续阅读 »

如何通过AI工具实现法律服务的提质增效,是每一位法律人都积极关注和学习的课题。但从AI技术火爆一下,法律人一直缺乏系统、实用的学习资料,来掌握在法律场景下AI的使用技巧。

alphagpt

今年5月,iCourt携手贵阳律协大数据与人工智能专业委员会,联合举办了《人工智能助力律师行业高质量发展巡回讲座》,超过100家律所的律师参与活动。

讲座上, iCourt AIGC 研究员、AlphaGPT产品研发负责人兰洋,为贵州律协的律师们讲解了AI技术在法律领域的应用原理、AI技术赋能法律服务发展九大典型场景和业务实操、AI赋能法律服务的发展趋势,并强调了AI时代下,律师需要掌握的关键技能,以期帮助律师们更好地抓住技术变革带来的发展机遇。

iCourt很早就开始了法律发数据与法律AI工具的研发应用,旗下智能法律操作系统Alpha、法律人专属AI助手AlphaGPT,在法律科技日益繁荣的市场上始终走在前列。

alpha

Alpha系统的大数据功能整合了超过1.6亿的案例和超过410万的法规,通过与司法观点库、类案同判库、实务文章库等多数据库协同穿透检索,为律师办案提供精确实用的法律信息。大数据功能不仅支持最高26维度的检索,还支持标签选取、模糊检索、AI检索等多种检索方式,解决律师在任意工作场景下的检索需求。 

alphagpt

值得一提的是,Alpha系统还是仅有的一个集法律大数据、人工智能工具、数智化办公工具、律所管理与团队协同工具于一体的法律智能操作系统。Alpha系统不仅可以帮助律师实现工作效率、工作效果的双重提升,还可以助力律所推动一体化改革,真正地实现降本增效。Alpha系统中涵盖的利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理等功能模块,涵盖律所管理与发展的方方面面,是律所发展、转型、升级的的必备工具。

而法律人专属AI助手AlphaGPT,可以帮助律师在案情分析、合同审查、文书写作、法律咨询、文件阅读、法律检索六大场景实现提质增效。例如,依托Alpha系统的法律大数据,AlphaGPT可以在短时间内实现案情智能分析,像律师一样思考,输出客观的法律服务意见书,还可以分钟级时间内实现合同风险与主体风险的一建审查。此外,AlphaGPT可以在短时间内实现文本的主题概括和核心提炼,还可以提供文本校对、润色、翻译等处理操作,大大节省了时间成本。

AI时代下,法律人应当抓住科技发展带来的机遇风口,拥抱AI等新兴技术,积极应用到法律服务领域当中,以实现自身的提质增效和推动行业的快速发展。

AlphaGPT官网入口:https://icourt.cc/product/alphaGPT

收起阅读 »

律所管理OA系统推荐,Alpha法律智能操作系统荣登榜首

随着“一体化”日渐成为律所改革的重要抓手,一个好的线上一体化管理工具(律所管理OA系统)就成为了几乎所有律所改革的必需品。Alpha法律智能操作系统集律所利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理于一体,涵盖律所管理的方方面面,是律所管理O...
继续阅读 »

随着“一体化”日渐成为律所改革的重要抓手,一个好的线上一体化管理工具(律所管理OA系统)就成为了几乎所有律所改革的必需品。Alpha法律智能操作系统集律所利冲管理、知识管理、客户管理、财务管理、审批管理和人事行政管理于一体,涵盖律所管理的方方面面,是律所管理OA系统中的代表性产品。目前,Alpha系统已帮助到1.5万家律所提高办案能力与质效。

经过调研与精确设计,Alpha系统以科学的工作流程为基准,以解决律所管理痛点问题为目标,进一步完善功能设置。其中,案件管理、知识管理与行政事务管理广受好评,这是律所一体化建设中不可缺少的三大环节。

alpha

据调研,律所管理的痛点之一是办案过程难管理,从而导致人员调配、案件跟进等徒增风险。对此,Alpha系统的办案件进程管理功能将律师办案过程线上化,通过项目模板将一个案件的立项、签约、办案、开庭、结案等全流程进行统追踪记录,极大地帮助管理人更好地对案件进行监察跟进。Alpha系统的律所OA打通了企业微信、钉钉、飞书等办公软件,可以自动创建工作群,助力开展团队协作,实时提醒律师待办任务,降低工作出错率。

此外,律所一体化的过程中,知识管理能否做好直接关系到律所能否长期高效地运行,决定着管理模式的成败。好的知识管理方案可以促进律所内部的优秀经验高效复制、知识成果快速共享,进而整体提升律师的业务能力。Alpha系统为律所打造了电子化、协同式的知识管理功能,提供超1.5亿案例数据,提供全面、精细的数据检索库;同时,基于多项数据库的组合应用,系统还支持5秒内自动导出客户尽调报告与法律风险分析报告,帮助律师快速了解企业客户基本信息与可能存在的法律风险,高效置顶办案策略。

alpha

针对行政事务管理,Alpha系统集成了律所OA、财务统计分析、绩效管理、审批管理等方面,促进行政统一管理,有力帮助律所降本增效。Alpha系统的自定义审批引擎支持内务外勤分条件高效审批,同时提供全自动利冲检索,做好案涉合同、发票、收款等信息的记录与审批,保障项目信息高效同步流转,实现业务数据和财务数据的统一管理,真正做到了省时省力。

Alpha是目前唯一一个将大数据,律所OA,人工智能相结合的律所管理软件,在律所一体化改革与建设上发挥巨大作用,助力律所实现整体创收提升。

Alpha系统官网入口:https://www.icourt.cc/product/alpha

收起阅读 »

如何使用Electron集成环信UIKIT

写在前面环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库,本篇文章介绍如何在Electron中如何集成UIKit,采用框架Electron-vite-react--- 准备工作1.已经在环信即时...
继续阅读 »

写在前面
环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库,本篇文章介绍如何在Electron中如何集成UIKit,采用框架Electron-vite-react


---
 准备工作
1.已经在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了App Key

2.了解并可以创建Electron-vite-react项目

3.了解UIkit各功能以及api调用


---

开始集成
第一步:创建一个Electron项目进度10%
Electron-vite官网有详细的教程,此处不做过多赘述,仅以当前示例项目为参考集成,更多详情指路官网

yarn create @quick-start/electron electronReact --template react

第二步:安装依赖进度15%

yarn install

第三步:启动项目进度20%

yarn run dev

到这一步我们可以得到下图


你的目录结构如下图


第四步:安装UIKit进度50%

下载easemob-chat-uikit
使用 npm 安装 easemob-chat-uikit 包

npm install easemob-chat-uikit --save

使用 yarn 安装 easemob-chat-uikit 包

yarn add easemob-chat-uikit

第五步:引入UIKit组件进度80%

1、删除App.tsx自带的内容,在App.tsx中引入UIKit组件

import {
Provider as UIKitProvider,
Chat,
ConversationList,
useClient
} from 'easemob-chat-uikit'
import 'easemob-chat-uikit/style.css'
import { useEffect } from 'react'
import './App.css'
const ChatApp = () => {
const client = useClient()
useEffect(() => {
client &&
client
.open({
user: 'userId',
pwd: 'pwd'
})
.then((res) => {
console.log('get token success', res)
})
}, [client])
return (
<div className="app_container">
<div className="conversation_container">
<ConversationList />
</div>
<div className="chat_container">
<Chat />
</div>
</div>
)
}
function App(): JSX.Element {
return (
<UIKitProvider
initConfig={{
appKey: 'your app key'
}}
>
<ChatApp />
</UIKitProvider>
)
}

export default App


2、将src/renderer/src/assets/main.css中的css样式全部替换如下
body {
display: flex;
flex-direction: column;
font-family:
Roboto,
-apple-system,
BlinkMacSystemFont,
'Helvetica Neue',
'Segoe UI',
'Oxygen',
'Ubuntu',
'Cantarell',
'Open Sans',
sans-serif;
color: #86a5b1;
background-color: #2f3241;
}

* {
padding: 0;
margin: 0;
}

ul {
list-style: none;
}

code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
background-color: #26282e;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 85%;
}

a {
color: #9feaf9;
font-weight: 600;
cursor: pointer;
text-decoration: none;
outline: none;
}

a:hover {
border-bottom: 1px solid;
}

.container {
flex: 1;
display: flex;
flex-direction: column;
max-width: 840px;
margin: 0 auto;
padding: 15px 30px 0 30px;
}

.versions {
margin: 0 auto;
float: none;
clear: both;
overflow: hidden;
font-family: 'Menlo', 'Lucida Console', monospace;
color: #c2f5ff;
line-height: 1;
transition: all 0.3s;
}

.versions li {
display: block;
float: left;
border-right: 1px solid rgba(194, 245, 255, 0.4);
padding: 0 20px;
font-size: 13px;
opacity: 0.8;
}

.versions li:last-child {
border: none;
}

.hero-logo {
margin-top: -0.4rem;
transition: all 0.3s;
}

@media (max-width: 840px) {
.versions {
display: none;
}

.hero-logo {
margin-top: -1.5rem;
}
}

.hero-text {
font-weight: 400;
color: #c2f5ff;
text-align: center;
margin-top: -0.5rem;
margin-bottom: 10px;
}

@media (max-width: 660px) {
.hero-logo {
display: none;
}

.hero-text {
margin-top: 20px;
}
}

.hero-tagline {
text-align: center;
margin-bottom: 14px;
}

.links {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
font-size: 18px;
font-weight: 500;
}

.links a {
font-weight: 500;
}

.links .link-item {
padding: 0 4px;
}

.features {
display: flex;
flex-wrap: wrap;
margin: -6px;
}

.features .feature-item {
width: 33.33%;
box-sizing: border-box;
padding: 6px;
}

.features article {
background-color: rgba(194, 245, 255, 0.1);
border-radius: 8px;
box-sizing: border-box;
padding: 12px;
height: 100%;
}

.features span {
color: #d4e8ef;
word-break: break-all;
}

.features .title {
font-size: 17px;
font-weight: 500;
color: #c2f5ff;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.features .detail {
font-size: 14px;
font-weight: 500;
line-height: 22px;
margin-top: 6px;
}

@media (max-width: 660px) {
.features .feature-item {
width: 50%;
}
}

@media (max-width: 480px) {
.links {
flex-direction: column;
line-height: 32px;
}

.links .link-dot {
display: none;
}

.features .feature-item {
width: 100%;
}
}


3、在src/renderer/src目录下添加App.css
.app_container {
width: calc(100%);
height: 100vh;
display: flex;
}
.conversation_container {
width: 30%;
}
.chat_container {
width: 70%;
}


到这一步你可以得到如下图



第六步:解决问题`进度99%`

在第五步执行完毕之后发现调试器有如下图报错


经查阅资料,发现是Electron内容安全策略在搞鬼,并提供了解决方案


接下来我们就需要在src/renderer/index.html中更改meta标签
同样out/renderer/index.html也需要更改meta标签

<meta
http-equiv="Content-Security-Policy"
content="font-src 'self' data:; img-src 'self' data:; default-src 'self'; connect-src * ws://* wss://*; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
/>


接下来保存代码并运行你将得到下图


第七步:发送消息进度100%
点击好友并发送一条消息,如下图

恭喜你集成完毕~
总结:
通过以上步骤,你已经成功在Electron中集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧

收起阅读 »

Go 再次讨论 catch error 模型,官方回应现状

大家好,我是煎鱼。 最近社区的同学和 Go 官方又因为错误处理的提案屡屡被否,发生了一些小的摩擦。也非常难得的看到核心团队成员首次表达了目前的态度和情况。 基于此,我们今天进行该内容分享。紧跟 Go 官方最新进展。 快速背景 Go 的错误处理机制,主要是依赖于...
继续阅读 »

大家好,我是煎鱼。


最近社区的同学和 Go 官方又因为错误处理的提案屡屡被否,发生了一些小的摩擦。也非常难得的看到核心团队成员首次表达了目前的态度和情况。


基于此,我们今天进行该内容分享。紧跟 Go 官方最新进展。


快速背景


Go 的错误处理机制,主要是依赖于 if err != nil 的方式。因此在对函数做一定的封装后。


代码会呈现出以下样子:



jy1, err := Foo1()
if err != nil {
return err
}
jy2, err := Foo2()
if err != nil {
return err
}
err := Foo3()
if err != nil {
return err
}
...

有部分开发者会认为这比较的丑陋、混乱且难以阅读。因此 Go 错误处理的优化,也是社区里一直反复提及和提出的领域。饱受各类争议。


新提案:追求类似 try-catch


最近一位国内的同学 @xiaokentrl 提了个类似 try catch error 的新提案,试图用魔法打败魔法。



原作者给出的提案内容是:


1、新增环境变量做开关:


ERROR_SINGLE = TRUE   //error_single = true

2、使用特定标识符来做 try-catch:


Demo1 单行错误处理:


//Single-line error handling
file, err := os.Create("abc.txt") @ return nil , err
defer file.Close()

Demo2 多行错误处理:


func main() {
//Multiline error handling
:@

file, err:= os.Open("abc.txt")
defer file.Close()

buf := make([]byte, 1024)
_, err2 := file.Read(buf)

@ err | err2 return ...
}

主要的变化内容是:利用标签 @ 添加一个类似 try-catch 的代码区块,并添加运算符和相关错误处理的联动规则。


这个提案本身,其实就是以往讲到的 goto error 和 check/with 这种类似 try-catch 的模式。


当然非常快的就遭到了 Go 核心团队的反对:



@Ian Lance Taylor 表示:由于很难处理声明和应用,如果一个标签的作用域中还有其他变量,就不能使用 goto。


新的争端:官方你行你上


社区中有同学看到这一次次被否的错误处理和关联提案们,深感无奈和无语。他发出了以下的质疑:


“为什么不让 Ian Lance Taylor 和/或 Go 核心团队的其他成员提出改进的错误处理框架的初始原型,然后让 Go 社区参与进来,为其最终形式做出贡献呢?Go 中的泛型正是这样发展到现在的。


如果我们等待 Go 社区提出最初的原型,我认为我们将永远不会有改进的 Go 错误处理框架,至少在未来几年内不会。”


但其实很可惜,因为人家真干过。


Go 核心团队是有主动提出过错误处理的优化提案的,提案名为《Proposal: A built-in Go error check function, try》,快速讲一下。


以前的代码:


f, err := os.Open(filename)
if err != nil {
return …, err // zero values for other results, if any
}

应用该提案后的新代码:


f := try(os.Open(filename))

try 只能在本身返回错误结果的函数中使用,且该结果必须是外层函数的最后一个结果参数。


不过很遗憾,该官方提案,喜提有史以来被否决最多的提案 TOP1:



最终该提案也由于形形色色的意见,最终凉了。感觉也给 Go 核心团队泼了一盆凉水,因为这是继 check/handle 后的 try,到目前也没有新的官方提案了。


Go 官方回应


本次提及的新提案下,大家的交流愈演愈烈,有种认为就是 Go 核心团队故意不让错误处理得到更好的改善。


此时 Go 核心团队的元老之一 @Ian Lance Taylor 站出来发声,诠释了目前 Go 团队对此的态度。这也是首次。


具体内容如下:



“我们愿意考虑一个有良好社区支持的好的错误处理提案。


不幸的是,我很遗憾地说,基本上所有新的错误处理提案都不好,也没有社区支持。例如,这个提案有 10 个反对票,没有赞成票。我当然会鼓励人们在广泛使用这门语言之前,避免提交错误处理提案。


我还鼓励人们审查早期的提案。它们在这里:github.com/golang/go/i… 。目前已有 183 个并在不断增加。


我自己阅读了每一个。重要的是,请记住,对已被否决提案的微调的新提案也几乎肯定也会被否决。


并且请记住,我们只会接受一个与现有语言契合良好的提案。例如:这个提案中使用了一个神奇的 @ 符号,这完全不像现有语言中的任何其他东西。


Go 团队可能会在适当的时候提出一个新的错误处理提案。然而,正如其他人所说,我们最好的想法被社区认为是不可接受的。而且有大量的 Go 程序员对现状表示满意。”


总结


目前 Go 错误处理的情况和困境是比较明确的,很多社区同学会基于以往已经被否决的旧提案上进行不断的微改,再不断提交。


现阶段都是被全面否定的,因为即使做了微调,也无法改变提案本身的核心思想。


而 Go 官方自己提出的 check/handle 和 try 提案,在社区中也被广大的网友否决了。还获得了史上最多人否决的提案的位置。


现阶段来看,未来 1-3 年内在错误处理的优化上仍然会继续僵持。



作者:煎鱼eddycjy
来源:juejin.cn/post/7381741857708752905
收起阅读 »

写了一个字典hook,瞬间让组员开发效率提高20%!!!

web
1、引言 在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react ...
继续阅读 »

1、引言


在项目的开发中,发现有很多地方(如:选择器、数据回显等)都需要用到字典数据,而且一个页面通常需要请求多个字典接口,如果每次打开同一个页面都需要重新去请求相同的数据,不但浪费网络资源、也给开发人员造成一定的工作负担。最近在用 taro + react 开发一个小程序,所以就写一个字典 hook 方便大家开发。


2、实现过程


首先,字典接口返回的数据类型如下图所示:
image.png


其次,在没有实现字典 hook 之前,是这样使用 选择器 组件的:


  const [unitOptions, setUnitOptions] = useState([])

useEffect(() => {
dictAppGetOptionsList(['DEV_TYPE']).then((res: any) => {
let _data = res.rows.map(item => {
return {
label: item.fullName,
value: item.id
}
})
setUnitOptions(_data)
})
}, [])

const popup = (
<PickSelect
defaultValue=""
open={unitOpen}
options={unitOptions}
onCancel={() =>
setUnitOpen(false)}
onClose={() => setUnitOpen(false)}
/>

)

每次都需要在页面组件中请求到字典数据提供给 PickSelect 组件的 options 属性,如果有多个 PickSelect 组件,那就需要请求多次接口,非常麻烦!!!!!


既然字典接口返回的数据格式是一样的,那能不能写一个 hook 接收不同属性,返回不同字典数据呢,而且还能 缓存 请求过的字典数据?


当然是可以的!!!


预想一下如何使用这个字典 hook?


const { list } = useDictionary('DEV_TYPE')

const { label } = useDictionary('DEV_TYPE', 1)

const { label } = useDictionary('DEV_TYPE', 1, '、')

从上面代码中可以看到,第一个参数接收字典名称,第二个参数接收字典对应的值,第三个参数接收分隔符,而且后两个参数是可选的,因此根据上面的用法来写我们的字典 hook 代码。


interface dictOptionsProps {
label: string | number;
value: string | number | boolean | object;
disabled?: boolean;
}

interface DictResponse {
value: string;
list: dictOptionsProps[];
getDictValue: (value: string) => string
}

let timer = null;
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {}; // 字典缓存

// 因为接收不同参数,很适合用函数重载
function useDictionary(type: string): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]); // 字典数组
const [dictValue, setDictValue] = useState(""); // 字典对应值

const init = () => {
if (!dict[type] || !dict[type].length) {
dict[type] = [];

types.push(type);

// 当多次调用hook时,获取所有参数,合成数组,再去请求,这样可以避免多次调用接口。
timer && clearTimeout(timer);
timer = setTimeout(() => {
dictAppGetOptionsList(types.slice()).then((res) => {
for (const key in dictRes.data) {
const dictList = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
dict[type] = dictList
setOptions(dictList) // 注意这里会有bug,后面有说明的
}
});
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
};

// 获取字典对应值的中文名称
const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];

const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
)

useEffect(() => init(), [])
useEffect(() => setDictValue(getLabel(value)), [options, value])

return { value: dictValue, list: options, getDictValue: getLabel };
}

初步的字典hook已经开发完成,在 Input 组件中添加 dict 属性,去试试看效果如何。


export interface IProps extends taroInputProps {
value?: any;
dict?: string; // 字典名称
}

const CnInput = ({
dict,
value,
...props
}: IProps
) => {
const { value: _value } = dict ? useDictionary(dict, value) : { value };

return <Input value={_value} {...props} />
}

添加完成,然后去调用 Input 组件


<CnInput
readonly
dict="DEV_ACCES_TYPE"
value={formData?.accesType}
/>

<CnInput
readonly
dict="DEV_SOURCE"
value={formData?.devSource}
/>


没想到,翻车了


会发现,在一个页面组件中,多次调用 Input 组件,只有最后一个 Input 组件才会回显数据


image.png


这个bug是怎么出现的呢?原来是 setTimeout 搞的鬼,在 useDictionary hook 中,当多次调用 useDictionary hook 的时候,为了能拿到全部的 type 值,请求一次接口拿到所有字典的数据,就把字典接口放在 setTimeout 里,弄成异步的逻辑。但是每次调用都会清除上一次的 setTimeout,只保存了最后一次调用 useDictionary 的 setTimeout ,所以就会出现上面的bug了。


既然知道问题所在,那就知道怎么去解决了。


解决方案: 因为只有调用 setOptions 才会引起页面刷新,为了不让 setTimeout 清除掉 setOptions,就把 setOptions 添加到一个更新队列中,等字典接口数据回来再去执行更新队列就可以了。



let timer = null;
const queue = []; // 更新队列
const types: string[] = [];
const dict: Record<string, dictOptionsProps[]> = {};

function useDictionary2(type: string): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value: number | string | Array<number | string>,
separator?: string
): DictResponse;
function useDictionary2(
type: string | dictOptionsProps[],
value?: number | string | Array<string | number>,
separator = ","
): DictResponse {
const [options, setOptions] = useState<dictOptionsProps[]>([]);
const [dictValue, setDictValue] = useState("");

const getLabel = useCallback(
(value) => {
if (value === undefined || value === null || !options.length) return "";
const values = Array.isArray(value) ? value : [value];

const items = values.map((v) => {
if (typeof v === "number") v = v.toString();
return options.find((item) => item.value === v) || { label: value };
});
return items.map((v) => v.label).join(separator);
},
[options]
);

const init = () => {
if (typeof type === "string") {
if (!dict[type] || !dict[type].length) {
dict[type] = [];

const item = {
key: type,
exeFunc: () => {
if (typeof type === "string") {
setOptions(dict[type]);
} else {
setOptions(type);
}
},
};
queue.push(item); // 把 setOptions 推到 更新队列(queue)中

types.push(type);

timer && clearTimeout(timer);
timer = setTimeout(async () => {
const params = types.slice();

types.length = 0;

try {
let dictRes = await dictAppGetOptionsList(params);
for (const key in dictRes.data) {
dict[key] = dictRes.data[key].map((v) => ({
label: v.description,
value: v.subtype,
}));
}

queue.forEach((item) => item.exeFunc()); // 接口回来了再执行更新队列
queue.length = 0; // 清空更新队列
} catch (error) {
queue.length = 0;
}
}, 10);
} else {
typeof type === "string" ? setOptions(dict[type]) : setOptions(type);
}
}
};

useEffect(() => init(), []);

useEffect(() => setDictValue(getLabel(value)), [options, value]);

return { value: dictValue, list: options, getDictValue: getLabel };
}

export default useDictionary;

修复完成,再去试试看~


image.png


不错不错,已经修复,嘿嘿~


这样就可以愉快的使用 字典 hook 啦,去改造一下 PickSelect 组件


export interface IProps extends PickerProps {
open: boolean;
dict?: string;
options?: dictOptionsProps[];
onClose: () => void;
}
const Base = ({
dict,
open = false,
options = [],
onClose = () => { },
...props
}: Partial<IProps>
) => {
// 如果不传 dict ,就拿 options
const { list: _options } = dict ? useDictionary(dict) : { list: options };

return <Picker.Column>
{_options.map((item) => {
return (
<Picker.Option
value={item.value}
key={item.value as string | number}
>

{item.label}
</Picker.Option>
);
})}
</Picker.Column>


在页面组件调用 PickSelect 组件


image.png


效果:


image.png


这样就只需要传入 dict 值,就可以轻轻松松获取到字典数据啦。不用再手动去调用字典接口啦,省下来的时间又可以愉快的摸鱼咯,哈哈哈


最近也在写 vue3 的项目,用 vue3 也实现一个吧。


// 定时器
let timer = 0
const timeout = 10
// 字典类型缓存
const types: string[] = []
// 响应式的字典对象
const dict: Record<string, Ref<CnPage.OptionProps[]>> = {}

// 请求字典选项
function useDictionary(type: string): Ref<CnPage.OptionProps[]>
// 解析字典选项,可以传入已有字典解析
function useDictionary(
type: string | CnPage.OptionProps[],
value: number | string | Array<number | string>,
separator?: string
): ComputedRef<string>
function useDictionary(
type: string | CnPage.OptionProps[],
value?: number | string | Array<number | string>,
separator = ','
): Ref<CnPage.OptionProps[]> | ComputedRef<string> {
// 请求接口,获取字典
if (typeof type === 'string') {
if (!dict[type]) {
dict[type] = ref<CnPage.OptionProps[]>([])

if (type === 'UNIT_LIST') {
// 单位列表调单独接口获取
getUnitListDict()
} else if (type === 'UNIT_TYPE') {
// 单位类型调单独接口获取
getUnitTypeDict()
} else {
types.push(type)
}
}

// 等一下,人齐了才发车
timer && clearTimeout(timer)
timer = setTimeout(() => {
if (types.length === 0) return
const newTypes = types.slice()
types.length = 0
getDictionary(newTypes).then((res) => {
for (const key in res.data) {
dict[key].value = res.data[key].map((v) => ({
label: v.description,
value: v.subtype
}))
}
})
}, timeout)
}

const options = typeof type === 'string' ? dict[type] : ref(type)
const label = computed(() => {
if (value === undefined || value === null) return ''
const values = Array.isArray(value) ? value : [value]
const items = values.map(
(value) => {
if (typeof value === 'number') value = value.toString()
return options.value.find((v) => v.value === value) || { label: value }
}
)
return items.map((v) => v.label).join(separator)
})

return value === undefined ? options : label
}

export default useDictionary

感觉 vue3 更简单啊!


到此结束!如果有错误,欢迎大佬指正~


作者:用户2885248830266
来源:juejin.cn/post/7377559533785022527
收起阅读 »

运维打工人,周末兼职送外卖的一天

运维打工人,周末兼职送外卖的一天 在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。 早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。...
继续阅读 »

运维打工人,周末兼职送外卖的一天


在那个不经意的周末,我决定尝试一份新的工作——为美团外卖做兼职配送员。这份工作对于一向规律生活的我来说,既是突破也是挑战。


早晨,城市的喧嚣还未完全苏醒,空气中带着几分凉意和宁静。准备好出发时,线上生产环境出现问题,协助处理。


收拾好后,戴上头盔,骑上踏板车,开始了自己的第一次外卖配送之旅。


刚开始,我的心情既紧张又兴奋。手机里的订单提示声是今日的任务号角。第一份订单来自一公里外的一家外卖便利店。我快速地在地图上规划路线,开启高德导航,发动踏板车,朝着目的地出发。


123.jpg


由于便利店在园区里面,转了两圈没找到,这是就慌张了,这找不到店咋办了,没办法赶紧问下旁边的老手骑手,也就顺利找到了,便利店,进门问老板,美团104号好了嘛?老板手一指,在架子上自己看。核对没问题,点击已达到店,然后在点击已取货。


然后在导航去收获目的地,找到C栋,找到107门牌号,紧接敲门,说您好,美团外卖到了,并顺利的送达,然后点击已送达,第一单顺利完成,4.8元顺利到手。


其中的小插曲,送给一个顾客时,手机导航提示目的地,结果一看,周围都拆了。没办法给顾客打电话,加微信确认位置具体在哪里,送达时,还差三分钟,这单就要超时了。


1.jpg


配送过程中,我遇到了第一个难题:找不到店家在哪里,我的内心不禁生出些许焦虑。但很快,我调整心态,不懂不知道的地方,需要多多问人。


紧接着,第二份、第三份订单接踵而至。每一次出发和到达,每一条街道和巷弄,我开始逐渐熟悉。


7.jpg


6.jpg


日落时分,我结束了一天的工作。虽然身体有些疲惫,但内心充满了前所未有的充实感。这份工作让我体验到了不一样的人生角色,感受到了城市节奏背后的种种辛劳与甘甜


周末的兼职跑美团外卖,对我来说不仅是一份简单的工作,更是一段特别的人生经历。它教会了我坚持与责任,让我在忙碌中找到了属于自己的节奏,在逆风中学会了更加珍惜每一次到达。


最后实际周六跑了4个小时,周天跑了7个小时,一共跑了71公里,合计收获了137.80,已提现到账。


5.jpg


2.png


作者:平凡的运维之路
来源:juejin.cn/post/7341669201010425893
收起阅读 »

为了解决小程序tabbar闪烁的问题,我将小程序重构成了 SPA

web
(日落西山,每次看到此景,我总是会想到明朝(明朝那些事儿第六部的标题,日落西山))  前言 几个月前,因工作需求,我开发了一个小程序,当时遇到了一个需求,是关于tabbar权限的问题。小程序的用户分两种,普通用户和vip用户,普通用户tabbar有两个,vip...
继续阅读 »

1.jpg


(日落西山,每次看到此景,我总是会想到明朝(明朝那些事儿第六部的标题,日落西山))


 前言


几个月前,因工作需求,我开发了一个小程序,当时遇到了一个需求,是关于tabbar权限的问题。小程序的用户分两种,普通用户和vip用户,普通用户tabbar有两个,vip用户小程序下面的tabbar有五个。  

因为涉及自定义tabbar的问题,所以官方自带的tabbar肯定就不能用了,我们需要自定义tabbar。官方也提供了自定义tabbar的功能。


官网自定义tabbar


官网地址:基础能力 / 自定义 tabBar (qq.com)


{
"tabBar": {
"custom": true,
"list": []
}
}

就是需要在 app.json 中的 tabBar 项指定 custom 字段,需要注意的是 list 字段也需要存在。


然后,在代码根目录下添加入口文件:


custom-tab-bar/index.js
custom-tab-bar/index.json
custom-tab-bar/index.wxml
custom-tab-bar/index.wxss

具体代码,大家可以参考官网案例。


需要注意的是每个tabbar页面 / 组件都需要在onshow / show 函数中执行以下函数,否则就会出现tabbar按钮切换两次,才会变成选中色的问题。


      if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
selected: 0 // 第n个tabbar页面就填 n-1
})
}

接下来就是我的思路


2.png


我在 custom-tab-bar/index.js 中定义了一个函数,这个函数去判断当前登录人是否为vip,如果是就替换掉tabbar 的数据。


那么之前每个页面的代码就要写成这样


      if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().change_tabbar_list()
this.getTabBar().setData({
selected: 0 // 第n个tabbar页面就填 n-1
})
}

ok,我们来看一下效果。注意看视频下方的tabbar,每个页面,第一次点击的时候,有明显的闪烁bug。(大家也可以参考一下市面上的小程序,小部分的小程序有这个闪烁问题,大部分的小程序没有这个闪烁的问题(如:携程小程序))



bug产生原因


那么我们就要去思考了,为什么人家的小程序没有这个bug呢?


想这个问题前,要先去想这个bug是怎么产生的,我猜测是每个tabbar页面都有个初始化的过程,第一次渲染页面的时候要去重新渲染tabbar,每个页面的tabbar都是从0开始渲染,然后会缓存到每个页面上,所以第二次点击就没有这个bug了。


解决tabbar闪烁问题


为了解决这个问题,我想到了SPA ,也就是只留一个页面,其他的tabbar页面都弄成组件。


效果展示



已经解决,tabbar闪烁的问题。


代码思路,通过wx:if 控制组件的显示隐藏。


3.png


4.png


源码地址:gitlab.com/wechat-mini…

https克隆地址:gitlab.com/wechat-mini…


写在最后


1、我也是在网上见过别人的一些评论,说如果将小程序重构成这种单页面,会有卡顿问题,我目前没有发现这个问题,可能是我做的小程序功能比较少。


2、至于生命周期,将页面切换成组件后,页面的那些生命周期也肯定都不能使用了,只能用组件的生命周期,我之前开发使用组件的生命周期实现业务逻辑也没什么问题。 触底加载这些也只能换成组件去实现了。


3、小程序最上面的标题,也可以使用以下代码来实现。就是在每个组件初始化的时候要去执行下列代码。


            wx.setNavigationBarTitle({
title: '',
});

作者:楚留香Ex
来源:juejin.cn/post/7317281367111827475
收起阅读 »

程序员真的很死板很无聊吗?

你的生活快乐吗?如果把工作和生活10等分,你会怎么分配你的时间? 故事背景 前几天随手分享了一个关于我每天早上吃的补充剂的照片,没想到有很多小伙伴留言反馈,着实挺出乎我意料 不禁让我想起一句自黑的老梗:想结婚找程序员,钱多话少死的早,虽然是调侃,但是也反映...
继续阅读 »

你的生活快乐吗?如果把工作和生活10等分,你会怎么分配你的时间?



故事背景


前几天随手分享了一个关于我每天早上吃的补充剂的照片,没想到有很多小伙伴留言反馈,着实挺出乎我意料


截屏2024-05-28 22.00.19.png


不禁让我想起一句自黑的老梗:想结婚找程序员,钱多话少死的早,虽然是调侃,但是也反映了大众对程序员团体的刻板映象确实很糟糕。


加上我身边很多同行业的同事这个毛病那个毛病的,30出头就脂肪肝...这也让我产生了一些思考:



  • 我们这个行业的从业者,在到了25岁、30岁、35岁、40岁的生活状况,健康状况分别是怎么样的

  • 会运动吗?吃的健康吗?身体还好吗?

  • 是不是真的像网上说的一样,程序员很无聊,很宅,死板?


所以我真的很好奇,大多数开发的生活状态是怎么样的,今天我分享一下我作为一个开发的日常,想看看我的生活是不是具有普遍性。(多图预警)


自我介绍


先来自我介绍一下,我叫dev,今年32岁(中年男人了),是个从业快7年的前端,主攻 React 技术栈,目前在一家ToG的公司做架构师。现在一边写基建一边在带人,基本脱离了业务代码的编写(偶尔)。


工作之余会写写文章,发发视频,阐述一些我认知的价值观和能提升生活幸福度的方法。下面是我平时的生活状态,兴趣比价广泛,基本上喜欢什么干什么(人生苦短及时行乐):



  1. 喜欢旅游,尤其那些山山水水,它会让我的内心平静
    露营
    潜水

  2. 喜欢画画,从初中开始,课本就是我的绘画本
    画画

  3. 会弹吉他,大学玩过乐队,也受邀在很多地方表演过
    吉他

  4. 目前喜欢上了popping,因为它很酷【在学】

  5. 因为很爱健康方面的知识,2024年准备考健康管理师


养生日常


7点起床,7.30做饭


早上7点起床,洗漱完之后,喝一杯温水,7:30 准备今天的伙食。(因为公司提供早餐,周末会自己做)


午餐


早餐



我不太喜欢吃外卖,因为我感觉很多外卖的食材不新鲜,而且大多为了可以放的时间久一点,盐分含量过高了!这是我每天早上早起做饭的动力之一。



9点到公司,开始工作吃早饭


早餐一般我会搭配文章开头的补剂一起吃。上班的时间我就快速跳过了,应该大部分时间和大家一样:


9点30-12点:工作时间
12点-1点:中饭时间
1点-1点30:午休半小时
1点30-5点30:工作时间

5点30下班


一般我会在公司待到6点半,因为我习惯6点准时吃饭,吃完之后再回来打个卡下班。常规操作。


下班之后我的安排一般是:周一8点撸铁,周三周五都有 popping 课,周二周四一般就在家写点东西,或者拍点东西,看看书啥的学习一下。


周末


周六外出


周五一到,令人兴奋的周末就要来了。一般周四周五就会大概想好周六的行程。有时候三五好友,有时候一个人,找个偏僻的乡村,搭个帐篷,架个躺椅,平静的过一天~(手机关机,啥都别来烦我)


截屏2024-05-28 23.17.18.png


周日创作


一般很少会连续两天出去玩儿,所以周日会安排自己学习一些新的东西,比如最近就在备考健康管理师,周日基本上午都在复习,周日下午就灵活安排,可能刷刷剧,撸撸猫,兴致来了晚上也会去上课,因为周日晚上也有一节课。
灵感来了也会写写文章,不管技术型的或者是生活向的,想到啥写啥,我很喜欢这种自由创作的感觉。让我有对生活的掌控感。


总结


以上就是我的日常生活,最近很喜欢一个成语:向死而生。我认为人到了一定年纪之后就会从学生变为布道者。写这篇文章的目的也很简单,希望能通过自己的生活态度告诉外界,我们也懂生活,我们也有自己对于生活的态度。


当我们光着屁股来到这个世界上,死的时候除了我们这一辈子的经历也是光着屁股走。所以我希望死的那一天,回顾我的一生,能很有底气的告诉自己:这一生无憾了!


这些年不断的犯错,不断的纠正。从以前天上地下唯我独尊的摇滚少年,到现在成熟低调的中年男人。总结下来,我发现想要的生活其实很简单:



  1. 有爱我和我爱的人

  2. 有我喜欢做的事情

  3. 身体健康(能比同龄人看起来年轻10岁我就心满意足了👀)


希望这篇文章能帮到你重新认识生活,评论区也可以留下你对自己生活的态度,和你认为对自己生活的掌控感有多少?


作者:dev
来源:juejin.cn/post/7373955162127876123
收起阅读 »

“请杀死自己的学生思维”

Hello,大家好,我是 Sunday。 昨天在B站上看到了一个视频叫做 请“杀死”自己的学生思维 这不禁让我想起之前写过的一篇文章 我们应避免感动自己的无效学习! 在现在 “内卷” 日益严重的大行情之下,很多同学都会充满的焦虑,生怕自己一个不小心就会被淘...
继续阅读 »

Hello,大家好,我是 Sunday。


昨天在B站上看到了一个视频叫做 请“杀死”自己的学生思维


作者:黄一刀有毒


这不禁让我想起之前写过的一篇文章 我们应避免感动自己的无效学习!



在现在 “内卷” 日益严重的大行情之下,很多同学都会充满的焦虑,生怕自己一个不小心就会被淘汰掉。从而开始学习很多很多的内容,期望可以通过这种方式来 “安慰自己”,告诉我已经很努力了,我不会被淘汰。


可是很多时候,这种无目标,无结果的努力,其实是 毫无价值 的!



什么是学生思维


“什么是学生思维” 并没有一个明确的定义。我个人认为,所谓的学生思维指的是:期望通过 “按部就班” 的努力,来达到 “超过大多数人” 的结果


回顾下我们的学生生涯:每天的课程是固定的、学习的知识是固定的、什么时候考试是固定的、甚至 考试的内容是什么也是固定的。几乎所有的一切都是固定的


所以,我们只需要 “按部就班” 的走,学习固定的内容,掌握固定的知识,就可以了。


因为,所有的人学习的内容都一样。所以,如果想要超过大多数人怎么办呢?


那就只剩下一个办法了, 卷!!!


卷学习时长、卷补习班、卷做题,最重要的是 父母、老师也告诉你这样是对的,因为 TA们在工作中也是这么做的


从而让我们认为 卷是常态,想要超过大多数人,获得排名(注意是排名而不是成绩)的靠前,那么就 必须要比别人卷!


最可怕的是 在学生时代,这样做是对的! 只要你足够卷,那么你就可以获得更好的排名,把其他的人卷下去。


就像这张图片一样


但是,这样的方式当你进入到社会中,你会发现 它好像失效了!


我们曾经亲眼看到过很多特别努力的同事依然免不了失业的结局,就好像前几天我写的这篇文章一样 跟一位 40+ 岁的同学沟通之后,差点泪崩


所以,当你进入职场 “请杀死自己的学生思维”


建立全新的职场思维体系


“杀死自己的学生思维” 是一件非常难的事情。因为 过去几十年的教育和经历都告诉我们:好好学习、好好上班、多听老板的话、吃亏是福!


几十年的固化思维是很难在一朝一夕之间改变的。但是,只要你开始做了,那么就比不做好。


1:工作只是利益的结合,天下没有不散的宴席


最近这两年,公司裁员已经不是一个新鲜的话题了。


上班的时候,老板说:“你要把公司当家”。裁员的时候,老板说:“不努力的就不是我兄弟”


所谓的工作,本质上就是利益的结合。你需要一份工作赚钱,老板需要人帮他赚更多的钱,仅此而已。


当一份关系通过利益进行捆绑时,那么通常就不是那么稳定了!


当一方的利益无法得到保证时,离开就是一个必然的结果。跳槽如是,裁员亦如是。


所以,把工作当成一场“宴席”。虽然“天下没有不散的宴席”,但是当一场宴席结束后,你才会迎来新的机会。


2:努力适应变化,生活不是考试,不需要排名


这个社会中唯一不变的就是一直在变化。这是与学生时代的最大不同。


诚如前面所说,学生时代几乎所有的一切都是固定的。但是,在生活中一切都是 瞬息万变 的。


进入社会,我们才发现生活远不止 上学、考试 这些固定的轨道。工作环境、市场需求、个人生活状况,甚至全球的经济和政治局势,都在不断变化。我们面对的已不是一张张试卷,而是一个个现实的挑战和机遇。我们需要学会适应这些变化,在不确定性中寻找方向和机会。


同时,生活也不是考试,没有所谓的得分,也不存在所谓的正确答案。


跟大家说一个某同学的故事,这位同学我们叫他 小A:



小A 之前是某英语培训机构的老师。K12 被禁止后失业。


因为英语很好,同时身边有做开发的朋友,所以进到 私教训练营 里学习,想要做前端开发的岗位。


但是,因为之前没有编程经验,所以学习速度并不快,预计需要 5 个月的学习周期。


一次无意中,小A 了解到小区中很多小孩子都有英语学习的需求。就过来问我:“Sunday 老师,你觉得我能不能帮小朋友学习英语,这样也可以获取一些收入。”


我帮他出了一整套的方案,那一个月的时间,我们针对 小A 的培训需求进行了很多次的沟通。最后大家猜怎么着?


小A 成功的做起来了一个小范围的英语学习班,每个月的收入甚至远超之前工作的收入。



我们每个人都有很多的选择。就如 小A 一样 “做开发”,“做老师”,“做培训”。


以上的选择都可以是正确的,也可能都是错误的。生活没有标准答案,每个人都有自己的独特路径和目标。


3:不要妄图规划未来,先把当下的事情做好


很多同学喜欢规划未来,这个并没有太大的意义。


比如:很多同学还没有找到工作的时候,就在担心 35 岁之后,如果失业了怎么办?这无疑是一种杞人忧天的想法。


未来的事情不可预测。很多鸡汤书籍都在告诉大家需要思考未来的情况,从而安排未来的计划:3 年计划、5 年计划 甚至是 10 年计划。


乍一听,好像很有道理的样子。但是 现实却告诉我们,哪怕是 3 天之后的情况,我们都预测不了。


过分的思考未来,会让我们忘记当下。就像之前的躺平文中所说:“人总会死,那么为什么还要努力呢?(注意:这是不对的)”。这甚至算是一个 “百年计划” 了。


所以,先解决当下的问题 吧,这才是最重要的!当下都解决不了,谈什么未来?


总结


无论你现在处于生活中的哪一个阶段,是痛苦着,还是快乐着。我都希望你可以摆脱原有的学生思维,尝试适应变化,找到自己独特的路径和目标 从而获得自己想要的幸福。


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

程序员都在用哪些神器?

工作中,我们有时候往往需要合理利用工具为我们提高一定的工作效率。利用包罗万象的浏览器搜索想要的资源已经是司空见惯了。 既然浏览器已经成为每一位电脑工作者的首要工具,那必然会有老六的出现,在我们的浏览器上增加广告,限制资源访问的手段,逼迫我们不得不妥协或者充值满...
继续阅读 »

工作中,我们有时候往往需要合理利用工具为我们提高一定的工作效率。利用包罗万象的浏览器搜索想要的资源已经是司空见惯了。


既然浏览器已经成为每一位电脑工作者的首要工具,那必然会有老六的出现,在我们的浏览器上增加广告,限制资源访问的手段,逼迫我们不得不妥协或者充值满足他们。


一名电脑从业者,想要一台干净的办公电脑环境可以说是一份奢求。我也观察其他部门同事们的电脑,电脑时不时弹出小电影片段,一刀 9999 的弹窗,可以说是皮到家了,不解释一下都以为平时就干这些事。


所以,我这里分享几款程序员常用的神器,即可应付老六行为,也可以方便我们摸鱼以及日常的工作使用。


AdGuard 拦截器


我们工作时候难免搜索一些紧急的关键信息,搜索过程中却时不时弹出亮眼的广告,甚至出现更加过分的花屏广告。


一怒之下想点击关闭窗口,却发现关闭窗口异常的难点,甚至阴差阳错点进广告内容,选择加入他们,小紧张的和旁边观看的同事解释我并没有干这种事情。


图片


AdGuard 广告拦截器,一款无与伦比的广告拦截扩展,用以对抗各式广告与弹窗。可以拦截所有网站的广告。



  1. 拦截所有广告,包括:视频广告,各种媒体广告,例如视频广告,插播广告和浮动广告 ,令人讨厌的弹窗 横幅广告和文字广告。

  2. 加速页面载入,节省带宽,屏蔽广告和弹窗 。

  3. 拦截各种软件,广告软件和拨号安装程序(可选) 。


图片


一旦安装了这款神器,我是万万没想到多流氓的网站都能够乖巧的显示它本应该显示的内容,不夹杂花里胡哨。眼不见为净,气不过难不成咱还躲不起,还给自己一个优质的工作环境。


Global Speed


作为当代视频冲浪儿,我们在闲暇时刻总是难免观看一些视频内容来打发时间,比如哔哩哔哩,腾讯视频,百度网盘, 爱奇艺等。


图片


有些视频内容往往是冗长且枯燥的,以及万恶的资本早已经捕捉到群体的口味,通常会增加一些不知是否趣味的广告。这时我们往往万般无奈等待着播放完,或者跳过观看。


如果我们使用 Global Speed 工具,我们可以躲避那些没有充值会员时候给我们增加冗长的广告,使得视频播放速度提高至 16 倍进行躲闪。它几乎支持所有的视频平台。


也就意味着 100 秒的广告,我们 8 秒就过完了。


Imagus


在使用浏览器逛一些网站和商城时,需要查看一些视频和图片,我们往往会选择点击进去查看大图。且有时候查看大图时,需要选择登录或者 vip 才能查看大图的权限。


图片


Imagus 插件,很好的解决了多点一下的问题,他可以停留在页面上不去点击进去直接鼠标悬停之后可查看大图效果。


他的作用是鼠标指针悬停在链接或缩略图上时直接在当前页面的弹出视图上显示这些图片、HTML5 视频/音频和内容专辑。


图片


图片


Picture-in-Picture Extension


这款画中画,我只能说是摸鱼神器了。在工作中,往往会趁着老板不注意或者躲避监控范围偷偷地摸鱼。


图片


时不时看一些好玩的视频,或者看看电竞直播,但又不敢夸张全屏观看,得给足老板的面子。所以我们会选择画中画观看,蜷缩在屏幕的一个小筐里进行观看,又不引人注意,也不影响手头的工作。


图片


使用 Picture-in-Picture 工具,我们既可以一边工作一边画中画观看自己想看的小视频,还能随意所动位置以及设置画面大小,别提多惬意。


购物党自动比价工具


我所认识的程序员群体里,有 10 个程序员里有 7-8 个都是会过日子的。不管是淘淘自己喜爱的电子产品还是生活必需品里,他们往往追求极致的性价比。在购买自己喜爱的电子产品时,往往会搜罗多个电商平台进行比价,以及寻求降价机会。


图片


而且,在面对自己的女神或者妹妹赠送礼物时,会徘徊在既要给出大方又要价格合理的礼品,毕竟不能不失对方的面子也不能让自己陷入窘迫的险境。如今的环境里寸金都不是大风刮来的。


购物党自动比价工具,往往是一些程序员必备的工具。浏览商品页面时,自动查询 180 天历史价格、比较同款商品的全网最低价、提示促销和隐藏优惠券、一旦降价还能通过微信提醒你,海淘、二手房游戏平台也能比价。


图片


当我们搜索一款商品时,左上角会自动查找各个平台的价格,价格的走势,以及优惠券的信息。是能够极大得缩减我们淘尚品的时间。


SuperCopy 超级复制


我们得益于互联网包罗万象,总是能够通过检索得到我们所需要的信息,使得我们尽管是一名程序员,也容易陷入 CV(复制粘贴)工程师的身份。


图片


总是有老六的出现,好不容易搜索到关键信息,结果一复制,温馨的弹框告诉我们需要登录或者充值 VIP。


气的人直跺脚,我们还拿他没办法,老板微信里三番五次催出我们好了没有,但我们又徘徊在付费工作的僵局。


SuperCopy 超级复制,这款插件绝对能够治理那些老六。


一键破解禁止右键、破解禁止选择、破解禁止复制、破解禁止粘贴,启用复制,启用右键,启用选择,启用粘贴。


超级复制 SuperCopy ,在禁止复制、禁止右键、禁止选择的站点,一键复制,一键粘贴,一键选择,启用右键,启用复制,启用选择,启用粘贴。


主要功能:解除禁止复制、解除禁止右键、解除禁止选择、解除禁止粘贴。


图片


如何安装


方式一: 如果您有科学上网,可以直接进入谷歌商城,依次搜索插件名称即可安装。


谷歌商城地址:chromewebstore.google.com/


如您没有科学上网,您可在公众号后台会回复 【科学上网】 即可获取工具。如您有使用问题,可在后台加作者联系方式,可以进行指导和咨询。


方式二: 您可以进入插件小屋,搜索插件名称进行下载。


插件小屋:http://www.chajianxw.com/


图片


下载之后点击谷歌浏览器设置,进入扩展程序界面。


图片


您可以将刚刚下载的插件压缩包拖拽到扩展程序界面,即可使用。


图片


作者:程序员小榆
来源:juejin.cn/post/7346119032524357642
收起阅读 »

如何将微信小程序从WebView迁移到Skyline

web
什么是 Skyline 微信小程序新的渲染引擎,使用更加高效的渲染管线,提供更好的性能和全新的交互动画体验。 具体可以查阅官网介绍 将开发者工具切换成 Sykline 模式 调试基础库切到 2.30.4 或以上版本 确保右上角 > 详情 > 本地...
继续阅读 »

什么是 Skyline


微信小程序新的渲染引擎,使用更加高效的渲染管线,提供更好的性能和全新的交互动画体验。


具体可以查阅官网介绍


将开发者工具切换成 Sykline 模式



  1. 调试基础库切到 2.30.4 或以上版本

  2. 确保右上角 > 详情 > 本地设置里的 开启 Skyline 渲染调试、启用独立域进行调试 选项被勾选上

  3. 确保右上角 > 详情 > 本地设置里的 将 JS 编译成 ES5 选项被勾选上


使用 skylint 工具迁移



npx skylint


image.png


image.png


使用过程中可能会出现文件未找到错误,例如


image.png


原因就是使用绝对路径 <import src="/components/chooserList/index.wxml" />导入模块,而 skylint 无法找到该文件,需要修改为相对路径 <import src="../../components/chooserList/index.wxml" />导入模块


有几种提示不是很准确,可以评估下:



  1. @position-fixed 不支持 position: fixed:如果你根据不同 renderer 兼容,则会导致该提示一直存在

  2. @no-pseudo-element 不支持伪元素: 目前对已经支持的 ::before 和 ::after 也会进行提示


手动迁移


在 app.json 配置



{
"lazyCodeLoading": "requiredComponents", // 开启按需注入
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true // skyline 下节点默认为 flex 布局,可以在此切换为默认 block 布局
}
}
}


在 page.json 配置



{
"renderer": "skyline", // 声明为 skyline 渲染,对于已有的项目,建议渐进式迁移,对于新项目,直接全局打开,在 app.json 里进行配置
"componentFramework": "glass-easel", // 声明使用新版 glass-easel 组件框架
"disableScroll": true, // skyline 不支持页面全局滚动,为了使之与WebView保持兼容,在此禁止滚动
"navigationStyle": "custom" // skyline 不支持原生导航栏,为了使之与WebView保持兼容,并且自行实现自定义导航栏
}


skyline 不支持页面全局滚动,如果需要页面滚动,在需要滚动的区域使用 scroll-view 实现



<scroll-view type="list" scroll-y style="flex: 1; width: 100%; overflow: hidden;"></scroll-view>



page {
display: flex;
flex-direction: column;
height: 100vh;
}


skyline 渲染模式下 flex-direction 默认值是 column,为了使之与WebView保持兼容,需要在 flex 布局里将 flex-direction 默认值改为 row


在真机上调试 skyline 渲染模式


小程序菜单 > 开发调试 > Switch Render,会出现三个选项,说明如下:


Auto :跟随 AB 实验,即对齐小程序正式用户的表现


WebView :强制切为 WebView 渲染


Skyline :若当前页面已迁移到 Skyline,则强制切为 Skyline 渲染


image.png


常见问题


position: fixed 不支持


需要将



<view class="background"></view>



.background {

position: fixed;

}


修改为



<root-portal>

<view class="background"></view>

</root-portal>



.background {

position: absolute;

}


如果无法做到适配,则可以根据不同 renderer 兼容



<view class="position {{renderer}}"></view>



.position {
position: fixed;
}

.position.skyline {
position: absolute;
}



Page({
data: {
renderer: 'webview'
},

onLoad() {
this.setData({
renderer: this.renderer,
})
},
})


不支持 Canvas 旧版接口


Skyline 渲染模式下,旧版 Canvas 绘制图案无效(使用 wx.createCanvasContext 创建的上下文)


在真机中图片的 referer 丢失


测试结果如下:


使用 WebView 渲染 Image,请求的 header 是:



{
host: 'xxx',
connection: 'keep-alive',
accept: 'image/webp,image/avif,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5',
'user-agent': 'xxx',
'accept-language': 'zh-CN,zh-Hans;q=0.9',
referer: 'https://servicewechat.com/',
'accept-encoding': 'gzip, deflate'
}


使用 Skyline 渲染 Image,请求的 header 是:



{

'user-agent': 'Dart/2.16 (dart:io)',
'accept-encoding': 'gzip',
host: 'xxx'
}


官方 Demo


可以参考官方Demo学习使用 Skyline 的增强特性,比如 Worklet 动画、手势系统等,但在首次下载编译时,会遇到【交互动画】页面为空的问题,主要原因是该页面是由 TypeScript 写的,编译成 JavaScript 需要开启工具内置的 TypeScript编译插件,需要在project.config.json project.config.json 配置:



setting.useCompilerPlugins: ["typescript"]


参考



作者:不二先生不二
来源:juejin.cn/post/7262656196854644792
收起阅读 »

小程序实现无感登录+权限分配

web
djngo开发:小程序实现无感登录+权限分配 由来 最近开发一个预约系统,需要区分普通用户和工作人员。由于账号密码/短信验证过于繁琐,因而选择记录openid实现无感登录。(基本小程序都这样操作) 同时事先在数据库中录入客户手机号,即可在用户登录时根据有无手机...
继续阅读 »

djngo开发:小程序实现无感登录+权限分配


由来


最近开发一个预约系统,需要区分普通用户和工作人员。由于账号密码/短信验证过于繁琐,因而选择记录openid实现无感登录。(基本小程序都这样操作)


同时事先在数据库中录入客户手机号,即可在用户登录时根据有无手机号来区分普通用户和工作人员。这样在项目交付时,工作人员和普通用户一样可以直接登录无感登录小程序


1. 开发思路


微信的openid是一种唯一标识用户身份的字符串

用户登录小程序,通过手机号快速验证组件获取动态令牌code,后端向微信服务器发送get请求并带上code获取每个用户唯一的openid,然后记录到mysql中,并签发token。该openid就是登录小程序的唯一凭证。


2.简单实现



  • 获取openid,如果通过openid查不用户,就自动新建用户,并返回token。


#####LoginView###########

code = request.data.get("code")
appid = appid # 微信小程序的appid
appsecret = "xxxxxxxx" # 微信小程序的密钥,登录微信公众平台即可获取
# 获取openid和session_token
querystring = {"appid":appid,"js_code":code,"secret":appsecret,"grant_type":"authorization_code"}
jscode2session = requests.get('https://api.weixin.qq.com/sns/jscode2session',params=querystring)
if not jscode2session.json().get("errcode"):
data = jscode2session.json()
########拿到openid#########
openid=data.get("openid")
#######去数据库比对,如果通过openid查到用户并且未被禁用,就新建##########
try:
user = models.UserInfo.objects.get(openid)
if user.is_deleted: # 检查用户是否被禁用
return ErrorResponse(msg='用户已被禁用,无法登录',data=data,code=302)
except models.UserInfo.DoesNotExist:
models.UserInfo.objects.create()

3.更进一步:通过手机号来区别普通用户和工作人员


openid虽然做到的唯一性验证,但是当用户数量庞大时,该如何区分用户角色:



  • 一:手动在后台根据已有用户分配权限

  • 二:登录时根据某一标识区分角色


    方法一显然不靠谱,因为用户至少会超过1000人,方法二需要额外标识,显然手机号最合适。



3.1 前端获取手机号的动态令牌


小程序提供了手机号快速验证组件,方便我们获取手机号


bindgetphonenumber 事件回调中的动态令牌code传到开发者后台


  <view class="title">欢迎来到广盈预约</view>
<view class="card">
<view class="button">快捷登录</view>
<button
style="opacity: 0"
class="bottom-button"
open-type="getPhoneNumber|agreePrivacyAuthorization"
bindgetphonenumber="getrealtimephonenumber"
bindagreeprivacyauthorization="handleAgreePrivacyAuthorization"
>

同意隐私协议并授权手机号注册
</button>
</view>
</view>

Page({
getPhoneNumber (e) { console.log(e.detail.code) // 动态令牌 }
})

image.png



注意:如果你想获取用户手机号就必须添加用户授权《隐私保护协议》bindagreeprivacyauthorization,否则小程序无法上线



3.2 后端带着动态令牌去微信服务器获取手机号


简单来说就是用户登录时数据库中没有手机号对应的用户,后端就会自动建立一个账号,并分配权限为普通用户,然后直接登录。


这样的话,只需要第一次登录时获取手机号,以后登录就可以直接进入系统。


class RegisterView(APIView):  
authentication_classes = []
permission_classes = []
def getmobile(self,appid,code):
"""获取用户的手机号"""
try:
appsecret = "cxxxxxxxxx"
querystring = {"appid":appid,"secret":appsecret,"grant_type":"client_credential"}
response = requests.get('https://api.weixin.qq.com/cgi-bin/token',params=querystring)
access_token = response.json().get("access_token")
querystring = {"access_token":access_token}
headers = {"content-type": "application/json"}
payload = {"code":code}
mobile =requests.post(f"https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={access_token}",json=payload,headers=headers)
return mobile.json().get('phone_info').get('phoneNumber')
except Exception as e:
return


def post(self, request):
unionid =request.data.get("unionid")
nickname =request.data.get("nickname")
openid = request.data.get("openid")
code = request.data.get("code")
appid = request.data.get("appid")

mobile = self.getmobile(appid=appid,code=code)
if not mobile:
return ErrorResponse(msg="手机号获取失败")

defaults = {
"openid":openid,
"unionid":unionid,
"mobile":mobile,
"nickname":nickname,
}

"""这条语句将查找一个符合mobile=mobile条件的记录,如果找到就更新 defaults中的字段 ,否则就创建
注意: 查询的条件必须是唯一的,否则会造成多条数据返回而报错,这个逻辑同 get() 函数。
注意: 使用的字段,没有唯一的约束,并发的调用这个方法可能会导致多条相同的值插入。
"""

models.UserInfo.objects.update_or_create(mobile=mobile,defaults=defaults)
models.User_GZH.objects.get_or_create(unionid=unionid, defaults={'unionid':unionid})
return DetailResponse()

作者:大海前端
来源:juejin.cn/post/7293177336488804391
收起阅读 »

进程还在,JSF接口不干活了,这你敢信?

1、问题背景: 应用在配合R2m升级redis版本的过程中,上游反馈调用接口报错,RpcException:[Biz thread pool of provider has been exhausted],通过监控系统和日志系统定位到现象只出现在一两个节点,并...
继续阅读 »

1、问题背景:


应用在配合R2m升级redis版本的过程中,上游反馈调用接口报错,RpcException:[Biz thread pool of provider has been exhausted],通过监控系统和日志系统定位到现象只出现在一两个节点,并持续出现。第一时间通过JSF将有问题的节点下线,保留现场,业务恢复。


报错日志如下:


24-03-13 02:21:20.188 [JSF-SEV-WORKER-57-T-5] ERROR BaseServerHandler - handlerRequest error msg:[JSF-23003]Biz thread pool of provider has been exhausted, the server port is 22003
24-03-13 02:21:20.658 [JSF-SEV-WORKER-57-T-5] WARN BusinessPool - [JSF-23002]Task:com.alibaba.ttl.TtlRunnable - com.jd.jsf.gd.server.JSFTask@0 has been reject for ThreadPool exhausted! pool:80, active:80, queue:300, taskcnt: 1067777

2、排查步骤:


从现象开始推测原因,系统启动时,会给JSF线程池分配固定的大小,当线程都在工作的时,外部流量又打进来,那么会没有线程去处理请求,此时会有上述的异常。那么JSF线程在干什么呢?


1)借助SGM打印栈信息


2)分析栈信息


可以用在线分析工具:spotify.github.io/threaddump-…


2.1)分析线程状态


通过工具可以定位到JSF线程大部分卡在JedisClusterInfoCache#getSlaveOfSlotFromDc方法,如图:






























2.2)分析线程夯住的方法


getSlaveOfSlotFromDc在方法入口就需要获取读锁,同时在全局变量声明了读锁和写锁:
















此时对问题有一个大体的了解,大概推测:getSlaveOfSlotFromDc是获取redis连接池,该方法入口处需要获取读锁,由于读锁之间不会互斥,所以猜测有业务获取到写锁后没有释放。同时读锁没有设置超时时间,所以导致杰夫线程处理业务时卡在获取读锁处,无法释放。


2.3)从业务的角度分析持有写锁的逻辑


向中间件研发寻求帮助,经过排查,定位到有个更新拓扑的定时任务,执行时会先获取写锁,根据该消息,定位到任务的栈信息:









代码截图:









图1









图2









图3


从日志验证:日志只打印更新拓扑的日志,没有打印更新成功的日志,且02:20分以后r2m-topo-updater就不在打印日志









2.4)深入挖掘原因


虽然现象已经可以推测出来,但是对问题的原因还是百思不得其解,难道parallelStream().forEach存在bug?难道有远程请求,没有设置超时时间?...


经过查找资料确认,如果没有指定,那么parallelStream().forEach会使用ForkJoinPool.commonPool这个默认的线程池去处理任务,该线程池默认设置(容器核心数-1)个活跃线程。同时caffeine数据过期后会异步刷新数据,如果没有指定线程池,它默认也会使用ForkJoinPool.commonPool()来执行异步线程。那么就有概率出现获取到写锁的线程无法获取执行权,获取执行权的线程无法获取到读锁。









2.5)验证


3个ForkJoinPool.commonPool-worker的确都夯在获取redis连接处,线程池的活跃线程都在等待读锁。









本地caffeine缓存没有设置自定义线程池









topo-updater夯在foreach业务处理逻辑中









3.复盘


1)此问题在特定的使用场景下才会小概率出现,非常感谢中间件团队一起协助定位问题,后续也将异步更新拓扑改为同步处理。


2)Java提供了很多异步处理的能力,但是异常处理也代表需要开启线程或者使用共用的线程池,也需要注意。


3)做好监控,能第一时间发现问题并处理问题。


作者:京东科技 田蒙


来源:京东云开发者社区


作者:京东云开发者
来源:juejin.cn/post/7379831020496715813
收起阅读 »

通过代码实现 pdf 文件自动盖章

序言在数字化时代,电子文档的安全性和真实性越来越受到重视。电子印章作为一种数字化的身份验证工具,已经成为确保文档合法性和不可篡改性的重要手段。然而,传统的电子印章往往需要人工操作,不仅效率低下,而且在处理大量文件时容易出错。为了解决这一问题,自动化地给PDF文...
继续阅读 »

序言

在数字化时代,电子文档的安全性和真实性越来越受到重视。电子印章作为一种数字化的身份验证工具,已经成为确保文档合法性和不可篡改性的重要手段。然而,传统的电子印章往往需要人工操作,不仅效率低下,而且在处理大量文件时容易出错。为了解决这一问题,自动化地给PDF文件盖电子章成为了一个迫切的需求。本文将详细介绍,如何通过 .net 程序实现这一功能,废话不多说,步入正题

Nuget 包

本文的核心包为:

  • iTextSharp,用它来操作 pdf 文件非常方便,具体的用法这里不多赘述,请参考官网
  • DynamicExpresso,一个非常好用的动态表达式解析工具包

Include="DynamicExpresso.Core" Version="2.16.1" />
Include="iTextSharp" Version="5.5.13.3" />
Include="Newtonsoft.Json" Version="13.0.3" />

素材准备

本案例用到的素材包括:用于测试的 pdf 文件一个,模拟电子章图片一张,以及盖章配置文件,文件内容如下:

[
{
"SignType" : "image",//素材类型,image表示图片素材,text 表示文本素材
"LastPage" : true,//是否仅最后一页盖章
"ImageUrl" : "https://xxxxxxxx",//图片素材的下载链接
"FileName" : "sign.png",//图片素材文件名称
"ScalePercent" : 20,//图片缩放百分比,100 表示不缩放
"Opacity" : 0.6,//图片透明度,1 表示不透明
"LocationX" : "(input.Width/10)*6",//图片素材的绝对位置表达式,(0,0) 表示左下角
"LocationY" : "input.Height/23 +20",//input.With 和 input.Height 代表 pdf 文件的宽度及高度
"Rotation" : 0//素材的旋转角度
},
{
"SignType" : "text",
"LastPage" : true,
"LocationX" : "(input.Width/10)*6+85",
"LocationY" : "input.Height/23 ",
"Rotation" : 0,
"FontSize" : 20,
"Opacity" : 0.6,
"FontColor" : {//文本素材的字体颜色值
"R" : 255,
"G" : 0,
"B" : 0
},
"Text" : "input.Date"//文本素材的表达式,也可以直接写固定文本
}
]

说明:

  1. 这里之所以设计为一个数组,是因为可能有些场景下,不仅需要盖电子章,还需要自动签上日期,比如本案例。
  2. 签署位置可以自定义,坐标(0,0)代表的是左下角,x 变大即表示横向右移,y 变大表示纵向上移。
  3. 配置文件存储,我这里是把配置文件放在了本地,当然你可以存储在任何地方,比如 MongoDB等。

代码展示

本案例采用的是 .net7.0,当然 .net6及以后都是可以的。

  1. 配置文件类,与上一步的 json 配置文件对应
namespace PdfSign;

public class SignOpt
{
public string SignType { get; set; }
public bool LastPage { get; set; }
public string ImageUrl { get; set; }
public string FileName { get; set; }
public int ScalePercent { get; set; } = 50;
public string LocationX { get; set; }
public string LocationY { get; set; }
public float LocationYf { get; set; }
public float Rotation { get; set; } = 0;
public int FontSize { get; set; }
public float Opacity { get; set; }
public RBGColor FontColor { get; set; }
public string? Text { get; set; }

public record RBGColor(int R, int G, int B);
}
  1. pdf 签署方法
using System.Dynamic;
using DynamicExpresso;
using iTextSharp.text;
using iTextSharp.text.pdf;
using Newtonsoft.Json.Linq;

namespace PdfSign;

public class SignService
{
public static string PdfSign(List signOpts, string pdfName)
{
var beforeFileName = pdfName; //签名之前文件名
var afterFileName = pdfName + "_sign"; //签名之后文件名
var idx = 0;
foreach (var opt in signOpts)
{
//创建盖章后生成pdf
var outputPdfStream =
new FileStream(afterFileName + ".pdf", FileMode.Create, FileAccess.Write, FileShare.);
//读取原有pdf
var pdfReader = new PdfReader(beforeFileName + ".pdf");
var pdfStamper = new PdfStamper(pdfReader, outputPdfStream);
//读取页数
var pdfPageSize = pdfReader.NumberOfPages;
//读取pdf文件第一页尺寸,得到 With 和 Height
var size = pdfReader.GetPageSize(1);
//通过表达式计算出签署的绝对坐标
var locationX = Eval(opt.LocationX, new { size.Width, size.Height });
var locationY = Eval(opt.LocationY, new { size.Width, size.Height });

if (opt.LastPage)
{
//盖章在最后一页
var pdfContentByte = pdfStamper.GetOverContent(pdfPageSize);
var gs = new PdfGState
{
FillOpacity = opt.Opacity
};
pdfContentByte.SetGState(gs);
switch (opt.SignType.ToLower())
{
case "image":
//获取图片
var image = Image.GetInstance(opt.FileName);
//设置图片比例
image.ScalePercent(opt.ScalePercent);
//设置图片的绝对位置,位置偏移方向为:左到右,下到上
image.SetAbsolutePosition(locationX, locationY);
//图片添加到文档
pdfContentByte.AddImage(image);
break;
case "text":
if (string.IsNullOrWhiteSpace(opt.Text))
continue;
var font = BaseFont.CreateFont();
var text = Eval(opt.Text, new { Date = DateTime.Now.ToString("yyyy-MM-dd") });
//开始写入文本
pdfContentByte.BeginText();
pdfContentByte.SetColorFill(
new BaseColor(opt.FontColor.R, opt.FontColor.G, opt.FontColor.B));
pdfContentByte.SetFontAndSize(font, opt.FontSize);
pdfContentByte.SetTextMatrix(0, 0);
pdfContentByte.ShowTextAligned(Element.ALIGN_CENTER, text,
locationX, locationY, opt.Rotation);

pdfContentByte.EndText();
break;
}
}

pdfStamper.Close();
pdfReader.Close();
idx++;
if (idx >= signOpts.Count) continue;
//文件名重新赋值
beforeFileName = afterFileName;
afterFileName += "_sign";
}

return afterFileName + ".pdf";
}

//计算动态表达式的值
public static T? Eval(string expr, object context)
{
if (string.IsNullOrWhiteSpace(expr))
return default;

var target = new Interpreter();
var input = JObject.FromObject(context);

target.SetVariable("input", input.ToObject());
return target.Eval(expr);
}
}
  1. 测试调用
using Newtonsoft.Json;
using PdfSign;

//读取签名所需配置文件
var signOpts = await GetSignOpt();

if (signOpts != null && signOpts.Any())
{
//执行 pdf 文件盖章
var signFileName= SignService.PdfSign(signOpts, "test");
}

//读取配置文件
static async Task<List<SignOpt>?> GetSignOpt()
{
var strSign = await File.ReadAllTextAsync("cfg.json");
return JsonConvert.DeserializeObject<List<SignOpt>>(strSign);
}
  1. 效果展示
    原 pdf 文件如下图: image.png 最终效果如下图: image.png

结束语

随着本文的深入探讨,我们共同经历了一个完整的旅程,从理解电子印章的重要性到实现一个自动化的.NET程序,用于在PDF文件上高效、准确地加盖电子章。我们不仅学习了.NET环境下处理PDF文件的技术细节,还掌握了如何将电子印章整合到我们的应用程序中,以实现自动化的文档认证过程。


作者:架构师小任
来源:juejin.cn/post/7377643248187080715

收起阅读 »

【已解决】uniapp小程序体积过大、隐私协议的问题

web
概述 在前几天的工作中又遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,说明我们需要进一步的优化它,技术栈是使用uniapp框架+HBuilderX的开发环境,微信小程序更新了隐私协议,Http返回信息{errMsg: "getUserP...
继续阅读 »

概述


在前几天的工作中又遇到了一个微信小程序上传代码过大的情况,在这里总结一下具体的解决步骤,说明我们需要进一步的优化它,技术栈是使用uniapp框架+HBuilderX的开发环境,微信小程序更新了隐私协议,Http返回信息{errMsg: "getUserProfile:fail can only be invoked by user TAP gesture."}


定位原因


程序出现问题,首先需要把原因定位归结在第一点,这是解决问题的关键,检查了一下Git仓库的修改情况,发现引入了一个7kb大小的防抖插件,其实7kb的插件不是根本问题,问题是之前的代码写的太不规范了。


压缩资源


尽量把static下面的图片都压缩一下,这里推荐一个好用的压缩网站,图片进行压缩:tinypng.com/


我没有压缩过Js文件,但会有一种方法压缩js文件,使js文件尽量的缩小来减少js文件建立的文件体积。


uniapp官方压缩建议:


小程序工具提示vendor.js过大,已经跳过es6向es5转换。这个转换问题本身不用理会,因为vendor.js已经是es5的了。


关于体积控制,参考如下:



  • 使用运行时代码压缩
    HBuilderX创建的项目勾选运行-->运行到小程序模拟器-->运行时是否压缩代码

  • cli创建的项目可以在package.json中添加参数--minimize,示例:"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --minimize"


小程序分包处理



  • 在对应平台的配置下添加 "optimization":{"subPackages":true}开启分包优化

  • 目前只支持 mp-weixin、mp-qq、mp-baidu、mp-toutiao、mp-kuaishou的分包优化


分包优化具体逻辑:



  • 静态文件:分包下支持 static 等静态资源拷贝,即分包目录内放置的静态资源不会被打包到主包中,也不可在主包中使用

  • js文件:当某个 js 仅被一个分包引用时,该 js 会被打包到该分包内,否则仍打到主包(即被主包引用,或被超过 1 个分包引用)

  • 自定义组件:若某个自定义组件仅被一个分包引用时,且未放入到分包内,编译时会输出提示信息


分包内静态文件示例


"subPackages": [{
"root": "pages/sub",
"pages": [{
"path": "index/index"
}]
}]

网络请求


还有一个解决小程序体积过大的问题,把非必要的组件都使用Http Api接口的形式去进行交互,尽量去减少本地包中的体积,再根目录下/utils里有一个232kb的获取地址交互,可以替换成Http Api的形式来解决。


隐私协议


在开发微信小程序过程中遇到了{errMsg: "getUserProfile:fail can only be invoked by user TAP gesture."},出现这个信息的原因是微信平台更新了隐私协议,需要再后台备案更新一下,搜索了很多,都不准确,这个隐私协议没有什么特殊情况,2个小时就可以通过了。


设置路径1: 公众号平台->设置->服务内容声明,设置通过后显示的状态是已更新,状态之前的是审核中


111.png


设置路径2: 首页->管理->版本管理->提交审核 ,再这里面提审,隐私协议审核过了,就可以继续开发了。


作者:stark张宇
来源:juejin.cn/post/7296025902911897627
收起阅读 »

uniapp微信小程序授权后得到“微信用户”

web
背景 近日在开发微信小程序的时候,发现数据库多了很多用户名称是"微信用户"的账号信息。接口的响应信息如下。 (nickName=微信用户, avatarUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4...
继续阅读 »

背景


近日在开发微信小程序的时候,发现数据库多了很多用户名称是"微信用户"的账号信息。接口的响应信息如下。


(nickName=微信用户, avatarUrl=https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132, gender=0, country=, province=, city=, language=), code=0e1abNFa1dBwRG0lnoJa18qT0i2abNFk)

经过排查,发现官方是对微信授权的接口做出了调整。小程序用户头像昵称获取规则调整公告


WX20240206-112518@2x.png

根据上面标红的字体说明,官方的意图就是只提供openid和unionid, 不暴露用户头像昵称数据。

基于此才会在新版的接口中返回"微信用户"的信息。



  • 针对这个问题,官方提供的解决方案如下。


WX20240206-112912@2x.png
以上解决方案,表达的意思是新版用户授权的接口中, 官方只会给你提供unionid和openid.

至于用户的昵称和头像,开发者可以提供功能,以用户的意志去完成修改和更新。

tips: 建议授权接口生成用户名和昵称,采用系统默认的方式。


微信授权流程


152f3cb28a734e768381f986cec1dd26.png


uniapp代码实现


uni.login接口文档


WX20240207-221556@2x.png


后端代码


WX20240207-221641@2x.png


以上是uniapp和springboot部分代码截图展示,关注微信公众号:JeecgFlow,或微信扫描下面二维码.

回复"微信用户"可以获取完整代码。


异常分析


//如果你的接口出现如下信信息,该如何处理呢?
# {errMsg: “getUserProfile:fail api scope is not
declared in the privacy agreement“, errno: 112}

出现问题的原因: api 范围未在隐私协议中声明,建议大家更具公告,更新对应的隐私协议。

【设置-服务内容声明-用户隐私保护指引】,更新隐私协议,在第一条:开发者处理的信息中,点击【增加信息类型】,选择需要授权的信息,头像昵称我已经勾选了,所以列表中不显示了,根据需求选择和填写其他内容,最后确定并生成协议。等待隐私协议审核通过。


68b3f3f0c4ee419d9ca5dec8aa5b0a4c.png
建议按需添加,以防审核不通过。


为了分辨用户,开发者将在获取你的明示同意后,收集你的微信昵称、头像。
为了显示距离,开发者将在获取你的明示同意后,收集你的位置信息。
开发者收集你的地址,用于获取位置信息。
开发者收集你的发票信息,用于维护消费功能。
为了用户互动,开发者将在获取你的明示同意后,收集你的微信运动步数。
为了通过语音与其他用户交流互动,开发者将在获取你的明示同意后,访问你的麦克风。
开发者收集你选中的照片或视频信息,用于提前上传减少上传时间。
为了上传图片或者视频,开发者将在获取你的明示同意后,访问你的摄像头。
为了登录或者注册,开发者将在获取你的明示同意后,收集你的手机号。
开发者使用你的通讯录(仅写入)权限,用于方便用户联系信息。
开发者收集你的设备信息,用于保障你正常使用网络服务。
开发者收集你的身-份-证号码,用于实名认证后才能继续使用的相关网络服务。
开发者收集你的订单信息,用于方便获取订单信息。
开发者收集你的发布内容,用于用户互动。
开发者收集你的所关注账号,用于用户互动。
开发者收集你的操作日志,用于运营维护。
为了保存图片或者上传图片,开发者将在获取你的明示同意后,使用你的相册(仅写入)权限。
为了用户互动,开发者将在获取你的明示同意后,收集你的车牌号。
开发者访问你的蓝牙,用于设备连接。
开发者使用你的日历(仅写入)权限,用于用户日历日程提醒。
开发者收集你的邮箱,用于在必要时和用户联系。
开发者收集你选中的文件,用于提前上传减少上传时间。


当你选择所需的接口后,需要您填写使用说明。 可以参考上面的内容进行填写。

给大家看一下我申请的接口。折腾半天终于把授权登录给整好了。


WX20240208-100953@2x.png


做完上述隐私设置后,需要你重新发布自己的小程序。 并且设置成采集用户隐私。

审核通过后就可以啦。如下图, 请一定注意!!!


WX20240208-101216@2x.png


参考文档


头像昵称填写-微信官方文档

uniapp头像昵称填写

getUserProfile:fail api scope is not declared in the privacy agreement


作者:代码次位面
来源:juejin.cn/post/7332113324651610150
收起阅读 »

UniApp TabBar的巅峰之作:个性化导航的魅力

web
前言在当今数字化时代,用户界面(UI)设计扮演着至关重要的角色,它不仅仅是产品的外表,更是用户与产品互动的第一印象。在一个社交群里,我有幸结识了一位创业的大佬,陈总,他自研的产品UI设计堪称一流,尤其是引人注目的菜单栏设计,深深吸引了我的注意,我就想着将从零玩...
继续阅读 »

前言

在当今数字化时代,用户界面(UI)设计扮演着至关重要的角色,它不仅仅是产品的外表,更是用户与产品互动的第一印象。在一个社交群里,我有幸结识了一位创业的大佬,陈总,他自研的产品UI设计堪称一流,尤其是引人注目的菜单栏设计,深深吸引了我的注意,我就想着将从零玩转系列之微信支付也优化一下

⚠️注意 本次不是从零玩转系列需要有一定的编程能力的同学

二、介绍

UniApp的TabBar

如果应用是一个多 tab 应用,可以通过 tabBar 配置项指定一级导航栏,以及 tab 切换时显示的对应页。

在 pages.json 中提供 tabBar 配置,不仅仅是为了方便快速开发导航,更重要的是在App和小程序端提升性能。在这两个平台,底层原生引擎在启动时无需等待js引擎初始化,即可直接读取 pages.json 中配置的 tabBar 信息,渲染原生tab。

Tips

  • 当设置 position 为 top 时,将不会显示 icon
  • tabBar 中的 list 是一个数组,只能配置最少2个、最多5个 tab,tab 按数组的顺序排序。
  • tabbar 切换第一次加载时可能渲染不及时,可以在每个tabbar页面的onLoad生命周期里先弹出一个等待雪花(hello uni-app使用了此方式)
  • tabbar 的页面展现过一次后就保留在内存中,再次切换 tabbar 页面,只会触发每个页面的onShow,不会再触发onLoad。
  • 顶部的 tabbar 目前仅微信小程序上支持。需要用到顶部选项卡的话,建议不使用 tabbar 的顶部设置,而是自己做顶部选项卡

三、设计

原本的ui样式,真滴丑不好看......

我改造后的,我滴妈真漂亮pink 猛男粉

设计图如下,懂前端的大佬肯定觉得没什么,虽然但是.....我是后端

可以分析他一个大的div包裹并且设置了边框圆形,里面有多个item元素菜单也设置了边框样式,每个菜单上面点击的时候会有背景颜色,我滴妈很简单啊,这我们在 从零玩转系列之微信支付当中讲过呀 给一个 `class样式 如果当前是谁就给谁 通过 vue 的 动态样式 so easy to happy !

四、实现思路

  • 删除TabBar配置的菜单栏:首先,需要从原始TabBar配置中移除默认的菜单栏,这将为自定义TabBar腾出空间。
  • 自定义底部菜单栏:接下来,自定义创建一个底部菜单栏,他是一个组件页面每个页面都需要引入
  • 自定义样式:使用CSS或相关样式设置,将自定义菜单栏精确地定位到底部,确保它与屏幕底部对齐,以实现预期的效果。

五、删除TabBar配置

好的我们尝试来删除 TabBar 配置 重新编译

可以看到报错了,这个错误就是我们使用的是switchTab进行菜单跳转使用别的肯定可以.但是为什么要用switchTab呢?

需求: 和原先的菜单栏功能一样不能销毁其他的菜单页面

那么我们将配置重新填上,他就不会报错了

⚠️注意: 这里有个问题,我们做的是菜单栏在uniapp当中菜单栏跳转是不会销毁其他页面的他其实是根据 switchTab 来进行路由的跳转不回销毁其他TabBar页面

菜单栏跳转的我们是不能销毁的那么这个配置就必须存在了呀,存在就存在无所谓!

遇事不要慌打开文档看看

这个时候我看到了什么?  hide 隐藏啊给我猜到了.绝壁有!!!!

uni.hideTabBar(OBJECT)

好我们知道有这个懂就行,后面我们进行创建我们的 自定义菜单栏组件 tabbar.vue

六、自定义TabBar

创建组件 tabbar.vue 这里我们使用vue3组合式Api搭建页面

<template>

<view class="tab-bar">

<view v-for="(item,index) in tabBarList" :key="index"
:class="{'tab-bar-item': true,currentTar: selected == item.id}"
@click="switchTab(item, index)">

<view class="tab_text" :style="{color: selected == index ? selectedColor : color}">
<image class="tab_img" :src="selected == index ? item.selectedIconPath : item.iconPath">image>
<view>{{ item.text }}view>
view>
view>
view>
template>

代码详细介绍

  1. : 这是一个外部的 view 元素,它用来包裹整个选项卡栏。
  1. : 这是一个 Vue.js 的循环指令 v-for,它用来遍历一个名为 tabBarList 的数据数组,并为数组中的每个元素执行一次循环。在循环过程中,item 是数组中的当前元素,index 是当前元素的索引。v-for 指令还使用 :key="index" 来确保每个循环元素都有一个唯一的标识符。
  1. :tab-bar-item': true,currentTar: selected == item.id}": 这是一个动态的 class 绑定,它根据条件为当前循环的选项卡元素添加不同的 CSS 类。如果 selected 的值等于当前循环元素的 item.id,则添加 currentTar 类,否则添加 tab-bar-item 类。
  1. @click="switchTab(item, index)": 这是一个点击事件绑定,当用户点击选项卡时,会触发名为 switchTab 的方法,并将当前选项卡的 item 对象和索引 index 作为参数传递给该方法。
  1. : 这是一个包含文本内容的 view 元素,它用来显示选项卡的文本。它还具有一个动态的样式绑定,根据条件选择文本的颜色。如果 selected 的值等于当前循环元素的 index,则使用 selectedColor,否则使用 color
  1. : 这是一个 image 元素,它用来显示选项卡的图标。它的 src 属性也是根据条件动态绑定,根据 selected 的值来选择显示不同的图标路径。
  1. {{ item.text }}: 这是一个用来显示选项卡文本内容的 view 元素,它显示了当前选项卡的文本,文本内容来自于 item.text

编写函数

代码当中的 tabBarList 函数要和 pages.json -> tabbar 配置一样哦

<script setup>
import { defineProps, ref } from 'vue'

// 子组件传递参数
const props = defineProps({
selected: {
type: Number,
default: 0
}
})

// 为选中颜色
let color = ref('#000')
// 选中的颜色
let selectedColor = ref('#ffb2b2')
// 菜单栏集合 - 与 pages.json -> tabbar 配置一样
let tabBarList = ref([
{
"id": 0,
"pagePath": "/pages/index/index",
"iconPath": "../../static/icon/icon_2.png",
"selectedIconPath": "../../static/icon/icon_2.png",
"text": "购买课程"
},
{
"id": 1,
"pagePath": "/pages/order/order",
"iconPath": "../../static/icon/gm_1.png",
"selectedIconPath": "../../static/icon/gm_1.png",
"text": "我的订单"
},
{
"id": 2,
"pagePath": "/pages/about/about",
"iconPath": "../../static/icon/about_3.png",
"selectedIconPath": "../../static/icon/about_3.png",
"text": "关于"
}
])

// 跳转tabBar菜单栏
const switchTab = (item) => {
let url = item.pagePath;
uni.switchTab({
url
})
}

script>

自定义TabBar样式


<style lang="less" scoped>
// 外部装修
.tab-bar {
position: fixed;
bottom: 25rpx;
left: 15rpx;
right: 15rpx;
height: 100rpx;
background: white;
padding: 20rpx;
border-radius: 30rpx;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 15px rgba(165, 168, 171, 0.83) !important;

// 当前点击的
.currentTar {
border-radius: 15rpx;
box-shadow: 0 0 15px #D7D7D7FF !important;
transition: all 0.5s ease-in-out;
}

// 给每个 item 设置样式
.tab-bar-item {
//flex: 0.5;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 150rpx;
padding: 15rpx;
background-color: transparent;
transition: all 0.5s ease-in-out;
margin: auto;

// 限制每个icon的大小
.tab_img {
width: 37rpx;
height: 41rpx;
}

// 限制文字大小
.tab_text {
font-size: 20rpx;
margin-top: 9rpx;
flex: 1;
}
}
}
style>

测试

我们自定义的效果出来了但是下面是什么鬼.....

可以看到我们下面也有一个菜单栏是 tabbar 配置产生出来的,我们前面不是说了隐藏吗?

修改函数新增隐藏tabbar代码

// 隐藏原生TabBar
uni.hideTabBar();

最后

本期结束咱们下次再见👋~

🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗


作者:杨不易呀
来源:juejin.cn/post/7330295657167290403

收起阅读 »

优雅解决uniapp微信小程序右上角胶囊菜单覆盖问题

前言 大家好,今天聊一下在做uniapp多端适配项目,需要用到自定义导航时,如何解决状态栏塌陷及导航栏安全区域多端适配问题,下文只针对H5、APP、微信小程序三端进行适配,通过封装一个通用高阶组件包裹自定义导航栏内容,主要是通过设置padding来使内容始终保...
继续阅读 »

前言


大家好,今天聊一下在做uniapp多端适配项目,需要用到自定义导航时,如何解决状态栏塌陷及导航栏安全区域多端适配问题,下文只针对H5、APP、微信小程序三端进行适配,通过封装一个通用高阶组件包裹自定义导航栏内容,主要是通过设置padding来使内容始终保持在安全区域,达到低耦合,可复用性强的效果。


一、创建NavbarWrapper.vue组件


大致结构如下:




<script>
export default {
name: 'NavbarWrapper',
data() {
return {
// 像素单位
pxUnit: 'px',
// 默认状态栏高度
statusBarHeight: 'var(--status-bar-height)',
// 微信小程序右上角的胶囊菜单宽度
rightSafeArea: 0
}
}
}
script>


<style scoped>
.navbar-wrapper {
/**
* 元素的宽度和高度包括了内边距(padding)和边框(border),
* 而不会被它们所占据的空间所影响
* 子元素继承宽度时,只会继承内容区域的宽度
*/

box-sizing: border-box;
}
style>


目的


主要是动态计算statusBarHeight和rightSafeArea的值。


解决方案


APP端只需一行css代码即可


.navbar-wrapper {
padding-top: var(--status-bar-height);
}

下面是关于--status-bar-height变量的介绍:


image.png


从上图可以知道--status-bar-height只在APP端是手机实际状态栏高度,在微信小程序是固定的25px,并不是手机实际状态栏高度;


微信小程序时,除了状态栏高度还需要获取右上角的胶囊菜单所占宽度,保持导航栏在安全区域。


以下使用uni.getWindowInfo()uni.getMenuButtonBoundingClientRect()来分别获取状态栏高度和胶囊相关信息,api介绍如下图所示:


image.png


image.png


主要逻辑代码


在NavbarWrapper组件创建时,做相关计算


created() {
const px = this.pxUnit
// #ifndef H5
// 获取窗口信息
const windowInfo = uni.getWindowInfo()
this.statusBarHeight = windowInfo.statusBarHeight + px
// #endif

// #ifdef MP-WEIXIN
// 获取胶囊左边界坐标
const { left } = uni.getMenuButtonBoundingClientRect()
// 计算胶囊(包括右边距)占据屏幕的总宽度:屏幕宽度-胶囊左边界坐标
this.rightSafeArea = windowInfo.windowWidth - left + px
// #endif
}

用法


<NavbarWrapper>
<view class="header">headerview>
NavbarWrapper>

二、多端效果展示


微信小程序


b15a0866000c13e58259645f2459440.jpg


APP端


45ee33b12dcf082e5ac76dc12fc41de.jpg


H5端


22b1984f8b21a4cb79f30286a1e4161.jpg


三、源码


NavbarWrapper.vue




<script>
export default {
name: 'NavbarWrapper',
data() {
return {
// 像素单位
pxUnit: 'px',
// 默认状态栏高度
statusBarHeight: 'var(--status-bar-height)',
// 微信小程序右上角的胶囊菜单宽度
rightSafeArea: 0
}
},
created() {
const px = this.pxUnit
// #ifndef H5
// 获取窗口信息
const windowInfo = uni.getWindowInfo()
this.statusBarHeight = windowInfo.statusBarHeight + px
// #endif

// #ifdef MP-WEIXIN
// 获取胶囊左边界坐标
const { left } = uni.getMenuButtonBoundingClientRect()
// 计算胶囊(包括右边距)占据屏幕的总宽度:屏幕宽度-胶囊左边界坐标
this.rightSafeArea = windowInfo.windowWidth - left + px
// #endif
}
}
script>


<style scoped>
.navbar-wrapper {
/**
* 元素的宽度和高度包括了内边距(padding)和边框(border),
* 而不会被它们所占据的空间所影响
* 子元素继承宽度时,只会继承内容区域的宽度
*/

box-sizing: border-box;
background-color: deeppink;
}
style>




作者:vilan_微澜
来源:juejin.cn/post/7309361597556719679
收起阅读 »

MySQL的 where 1=1会不会影响性能?看完官方文档就悟了!

在日常业务开发中,会通过使用where 1=1来简化动态 SQL语句的拼接,有人说where 1=1会影响性能,也有人说不会,到底会不会影响性能?本文将从 MySQL的官方资料来进行分析。 动态拼接 SQL的方法 在 Mybatis中,动态拼接 SQL最常用的...
继续阅读 »

在日常业务开发中,会通过使用where 1=1来简化动态 SQL语句的拼接,有人说where 1=1会影响性能,也有人说不会,到底会不会影响性能?本文将从 MySQL的官方资料来进行分析。


动态拼接 SQL的方法


在 Mybatis中,动态拼接 SQL最常用的两种方式:使用 where 1=1 和 使用标签。


使用where 1=1


使用过 iBATIS的小伙伴应该都知道:在 iBATIS中没有标签,动态 SQL的处理相对较为原始和复杂,因此使用where 1=1这种写法的用户很大一部分是还在使用 iBATIS 或者是从 iBATIS过度到 Mybatis。


如下示例,通过where 1=1来动态拼接有效的 if语句:


<select id="" parameterType = "">
SELECT * FROM user
WHERE 1=1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null ">
AND age = #{age }
</if>
</select>

使用标签


Mybatis提供了标签,标签只有在至少一个 if条件有值的情况下才去生成 where子句,若 AND或 OR前没有有效语句,where元素会将它们去除,也就是说,如果 Mybatis通过标签动态生成的语句为where AND name = '111',最终会被优化为where name = '111'


标签使用示例如下:


<select id="" parameterType = "">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>

标签是在 MyBatis中引入的,所以,很多一开始就使用 MyBatis的用户对这个标签使用的比较多。


性能影响


where 1=1到底会不会影响性能?我们可以先看一个具体的例子:



说明:示例基于 MySQL 8.0.30



可以使用如下指令查看 MySQL版本:


SELECT VERSION();

image.png


场景:基于一张拥有 100多万条数据的user表,根据name进行查询,


查看表结构和表的总数据,如下图:


image.png


image.png


下面,通过执行两条 SQL查询语句(一条带有 1=1):


select * from user where name = 'name-96d1b3ce-1a24-4d47-b686-6f9c6940f5f6';
select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';

image.png


对比两条 SQL执行的结果,可以发现它们消耗的时间几乎相同,因此,看起来where 1=1对整体的性能似乎并不影响。


为了排除一次查询不具有代表性,我们分别对两条 SQL语句查询 100遍,然后计算平均值:


SET PROFILING = 1;
DO SLEEP(0.001); -- 确保每次查询之间有足够时间间隔

SET @count = 0;
WHILE @count < 100 DO
select * from user where name = 'name-96d1b3ce-1a24-4d47-b686-6f9c6940f5f6';
-- or
select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';
SET @count = @count + 1;
END WHILE;

SHOW PROFILES;

两条 SQL分别执行 100次后,最终也发现它们的平均值几乎相同,因此,上述示例似乎证明了 where 1=1 对整体的性能并没有不影响。


为什么没有影响?是不是 MySQL对 1=1进行了优化?


为了证明猜想,我们借助show warnings命令来查看信息,在 MySQL中,show warnings命令用于显示最近执行的 SQL语句产生的警告、错误或通知信息。它可以帮助我们了解语句执行过程中的问题。如下示例:


explain select * from user where 1=1 and name = 'name-f692472e-40de-4053-9498-54b9800e9fb1';
show warnings;

image.png


将上述示例的 warnings信息摘出来如下:


/* select#1 */ select `yuanjava`.`user`.`id` AS `id`,
`yuanjava`.`user`.`name` AS `name`,
`yuanjava`.`user`.`age` AS `age`,
`yuanjava`.`user`.`sex` AS `sex`,
`yuanjava`.`user`.`created_at` AS `created_at`
from `yuanjava`.`user`
where (`yuanjava`.`user`.`name` = 'name-f692472e-40de-4053-9498-54b9800e9fb1')

从 warnings信息可以看出:1=1已经被查询优化器优化掉,因此,对整体的性能影响并不大。


那么,有没有 MySQL的官方资料可以佐证 where 1=1确实被优化了?


答案:有!MySQL有一种 Constant-Folding Optimization(常量折叠优化)的功能。


Constant-Folding Optimization


MySQL的优化器具有一项称为 Constant-Folding Optimization(常量折叠优化)的功能,可以从查询中消除重言式表达式。Constant-Folding Optimization 是一种编译器的优化技术,用于优化编译时计算表达式的常量部分,从而减少运行时的计算量,换句话说:Constant-Folding Optimization 是发生在编译期,而不是引擎执行期间。


对于上述表达的"重言式表达式"又是什么呢?


重言式


重言式(Tautology )又称为永真式,它的汉语拼音为:[Chóng yán shì],是逻辑学的名词。命题公式中有一类重言式,如果一个公式,对于它的任一解释下其真值都为真,就称为重言式(永真式)。


其实,重言式在计算机领域也具有重要应用,比如"重言式表达式"(Tautological expression),它指的是那些总是为真的表达式或逻辑条件。


在 SQL查询中,重言式表达式是指无论在什么情况下,结果永远为真,它们通常会被优化器识别并优化掉,以提高查询效率。例如,如果 where中包含 1=1 或 A=A 这种重言式表达式,它们就会被优化器移除,因为对查询结果没有实际影响。如下两个示例:


SELECT * from user where 1=1 and name = 'xxx';
-- 被优化成
SELECT * from user where name = 'xxx'

SELECT id, name, salary * (1 + 0.05 * 2) AS real_salary FROM employees;
-- 优化成(1 + 0.05 * 2 被优化成 1.1)
SELECT id, name, salary * 1.1 AS real_salary FROM employees;

另外,通过下面 MySQL架构示意图可以看出:优化器是属于 MySQL的 Server层,因此,Constant-Folding Optimization功能支持受 MySQL Server的版本影响。


image.png


查阅了 MySQL的官方资料,Constant-Folding Optimization 从 MySQL5.7版本开始引入,至于 MySQL5.7以前的版本是否具备这个功能,还有待考证。


如何选择?


where 1=1 标签 两种方案,该如何选择?



  • 如果 MySQL Server版本大于等于 5.7,两个随便选,或者根据团队的要求来选;

  • 如果 MySQL Server版本小于 5.7,假如使用的是 MyBatis,建议使用 标签,如果使用的还是比较老的 iBATIS,只能使用where 1=1

  • 如果 MySQL Server版本小于 5.7,建议升升级



信息补充:2009年5月,iBATIS从 2.0版本开始更名为 MyBatis, 标签最早出现在MyBatis 3.2.0版本中



总结


where 1=1 标签到底会不会影响性能,这个问题在网上已经出现了很多次,今天还是想从官方文档来进行说明。本文通过 MySQL的官方资料,加上百万数据的表进行真实测试,得出下面的结论:



  • 如果 MySQL Server版本大于等于 5.7,两个随便选,或者根据团队的要求来选;

  • 如果 MySQL Server版本小于 5.7,假如使用的是 MyBatis,建议使用 标签,如果使用的还是比较老的 iBATIS,只能使用where 1=1


最后,遇到问题,建议首先查找官方的一手资料,这样才能帮助自己在一条正确的技术道路上成长!


参考资料


MySQL8.0 Constant-Folding Optimization


MySQL5.7 WHERE Clause Optimization


What’s New in MySQL 5.7




作者:猿java
来源:juejin.cn/post/7374238289107648551
收起阅读 »

代码很少,却很优秀!RocketMQ的NameServer是如何做到的?

今天我们来一起深入分析 RocketMQ的注册中心 NameServer。 本文基于 RocketMQ release-5.2.0 首先,我们回顾下 RocketMQ的内核原理鸟瞰图: 从上面的鸟瞰图,我们可以看出:Nameserver即和 Broker...
继续阅读 »

今天我们来一起深入分析 RocketMQ的注册中心 NameServer。



本文基于 RocketMQ release-5.2.0



首先,我们回顾下 RocketMQ的内核原理鸟瞰图:


image.png


从上面的鸟瞰图,我们可以看出:Nameserver即和 Broker交互,也和 Producer和 Consumer交互,因此,在 RocketMQ中,Nameserver起到了一个纽带性的作用。


接着,我们再看看 NameServer的工程结构,如下图:


image.png


整个工程只有 11个类(老版本好像只有不到 10个类),为什么 RocketMQ可以用如此少的代码,设计出如此高性能且轻量的注册中心?


我觉得最核心的有 3点是:



  1. AP设计思想

  2. 简单的数据结构

  3. 心跳机制


AP设计思想


像 ZooKeeper,采用了 Zab (Zookeeper Atomic Broadcast) 这种比较重的协议,必须大多数节点(过半数)可用,才能确保了数据的一致性和高可用,大大增加了网络开销和复杂度。


而 NameServer遵守了 CAP理论中 AP,在一个 NameServer集群中,NameServer节点之间是P2P(Peer to Peer)的对等关系,并且 NameServer之间并没有通信,减少很多不必要的网络开销,即便只剩一个 NameServer节点也能继续工作,足以保证高可用。


数据结构


NameServer维护了一套比较简单的数据结构,内部维护了一个路由表,该路由表包含以下几个核心元数据,对应的源码类RouteInfoManager如下:


public class RouteInfoManager {
private final static long DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2; // broker失效时间 120s
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Map/* topic */, Map> topicQueueTable;
private final Map/* brokerName */, BrokerData> brokerAddrTable;
private final Map/* clusterName */, Set/* brokerName */>> clusterAddrTable;
private final Map/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final Map/* brokerAddr */, List/* Filter Server */> filterServerTable;
}


  • topicQueueTable: Topic消息队列路由信息,消息发送时根据路由表进行负载均衡

  • brokerAddrTable: Broker基础信息,包括brokerName、所属集群名称、主备Broker地址

  • clusterAddrTable: Broker集群信息,存储集群中所有Broker名称

  • brokerLiveTable: Broker状态信息,NameServer每次收到心跳包是会替换该信息

  • filterServerTable: Broker上的FilterServer列表,用于过滤标签(Tag)或 SQL表达式,以减轻 Consumer的负担,提高消息消费的效率。


TopicRouteData


TopicRouteData是 NameServer中最重要的数据结构之一,它包括了 Topic对应的所有 Broker信息以及每个 Broker上的队列信息,filter服务器列表,其源码如下:


public class TopicRouteData {
private List queueDatas;
private List brokerDatas;
private HashMap> filterServerTable;
//It could be null or empty
private Map/*brokerName*/, TopicQueueMappingInfo> topicQueueMappingByBroker;
}

BrokerData


BrokerData包含了 Broker的基本属性,状态,所在集群以及 Broker服务器的 IP地址,其源码如下:


public class BrokerData {
private String cluster;//所在的集群
private String brokerName;//所在的brokerName
private HashMap brokerAddrs;//该broker对应的机器IP列表
private String zoneName; // 区域名称
}

QueueData


QueueData包含了 BrokerName,readQueue的数量,writeQueue的数量等信息,对应的源码类是QueueData,其源码如下:


public class QueueData {
private String brokerName;//所在的brokerName
private int readQueueNums;// 读队列数量
private int writeQueueNums;// 写队列数量
private int perm; // 读写权限,参考PermName 类
private int topicSysFlag; // topic同步标记,参考TopicSysFlag 类
}

元数据举例


为了更好地理解元数据,这里对每一种元数据都给出一个数据实例:


topicQueueTable:{
"topicA":[
{
"brokeName":"broker-a",
"readQueueNums":4,
"writeQueueNums":4,
"perm":6,
"topicSyncFlag":0
},
{
"brokeName":"broker-b",
"readQueueNums":4,
"writeQueueNums":4,
"perm":6,
"topicSyncFlag":0
}
],
"topicB":[]
}

brokeAddrTable:{
"broker-a":{
"cluster":"cluster-1",
"brokerName":"broker-a",
"brokerAddrs":{
0:"192.168.0.1:8000",
1:"192.168.0.2:8000"
}
},
"broker-b":{
"cluster":"cluster-1",
"brokerName":"broker-b",
"brokerAddrs":{
0:"192.168.0.3:8000",
1:"192.168.0.4:8000"
}
}
}

brokerLiveTable:{
"192.168.0.1:8000":{
"lastUpdateTimestamp":1533434434344,//long 的时间戳
"dataVersion":dataVersionObj, //参考DataVersion类
"channel":channelObj,// 参考io.netty.channel.Channel
"haServerAddr":"192.168.0.2:8000"
},
"192.168.0.2:8000":{
"lastUpdateTimestamp":1533434434344,//long 的时间戳
"dataVersion":dataVersionObj, //参考DataVersion类
"channel":channelObj,// 参考io.netty.channel.Channel
"haServerAddr":"192.168.0.1:8000"
},
"192.168.0.3:8000":{ },
"192.168.0.4:8000":{ },
}

clusterAddrTable:{
"cluster-1":[{"broker-a"},{"broker-b"}],
"cluster-2":[],
}

filterServerTable:{
"192.168.0.1:8000":[{"192.168.0.1:7000"}{"192.168.0.1:9000"}],
"192.168.0.2:8000":[{"192.168.0.2:7000"}{"192.168.0.2:9000"}],
}

心跳机制


心跳机制是 NameServer维护 Broker的路由信息最重要的一个抓手,主要分为接收心跳、处理心跳、心跳超时 3部分:


接收心跳


Broker每 30s会向所有的 NameServer发送心跳包,告诉它们自己还存活着,从而更新自己在 NameServer的状态,整体交互如下图:


image.png


处理心跳


NameServer收到心跳包时会更新 brokerLiveTable缓存中 BrokerLiveInfo的 lastUpdateTimeStamp信息,整体交互如下图:


image.png


处理逻辑可以参考源码:
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest#brokerHeartbeat:


public RemotingCommand brokerHeartbeat(ChannelHandlerContext ctx,
RemotingCommand request)
throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final BrokerHeartbeatRequestHeader requestHeader =
(BrokerHeartbeatRequestHeader) request.decodeCommandCustomHeader(BrokerHeartbeatRequestHeader.class);

this.namesrvController.getRouteInfoManager().updateBrokerInfoUpdateTimestamp(requestHeader.getClusterName(), requestHeader.getBrokerAddr());

response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}

心跳超时


NameServer每隔 10s(每隔5s + 5s延迟)扫描 brokerLiveTable检查 Broker的状态,如果在 120s内未收到 Broker心跳,则认为 Broker异常,会从路由表将该 Broker摘除并关闭 Socket连接,同时还会更新路由表的其他信息,整体交互如下图:


image.png


private void startScheduleService() {
this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker,
5, this.namesrvConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS);
}

源码参考:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unRegisterBroker(),核心流程:



  1. 遍历brokerAddrTable

  2. 遍历broker地址

  3. 根据 broker地址移除 brokerAddr

  4. 如果当前 Topic只包含待移除的 Broker,则移除该 Topic


其他核心源码解读


NameServer启动


NameServer的启动类为:org.apache.rocketmq.namesrv.NamesrvStartup,整个流程如下图:


image.png
图片来自:Mark_Zoe


NameServer启动最核心的 3个事情是:



  1. 加载配置:NameServerConfig、NettyServerConfig主要是映射配置文件,并创建 NamesrvController。

  2. 启动 Netty通信服务:NettyRemotingServer是 NameServer和Broker,Producer,Consumer通信的底层通道 Netty服务器。

  3. 启动定时器和钩子程序:NameServerController实例一方面处理 Netty接收到消息后,一方面内部有多个定时器和钩子程序,它是 NameServer的核心控制器。


总结


NameServer并没有采用复杂的分布式协议来保持数据的一致性,而是采用 CAP理论中的 AP,各个节点之间是Peer to Peer的对等关系,数据的一致性通过心跳机制,定时器,延时感知来完成。


NameServer最核心的 3点设计是:



  1. AP的设计思想

  2. 简单的数据结构

  3. 心跳机制




作者:猿java
来源:juejin.cn/post/7379431978814275596
收起阅读 »

uni-app新建透明页面实现全局弹窗

web
需求背景 实现一个能遮住原生 tabbar 和 navbar 的全局操作框 原理 使用一个透明的页面来模拟弹窗,这个页面可以遮住原生 tabbar 和 navbar 页面配置 { "path" : "pages/shootAtWill/shootAtW...
继续阅读 »

需求背景


实现一个能遮住原生 tabbarnavbar 的全局操作框


原理


使用一个透明的页面来模拟弹窗,这个页面可以遮住原生 tabbarnavbar


页面配置


{
"path" : "pages/shootAtWill/shootAtWill",
"style" :
{
"navigationBarTitleText" : "随手拍",
"navigationStyle": "custom",
"backgroundColor": "transparent",
"app-plus": {
"animationType": "slide-in-bottom", // 我这边需求是从底部弹出
"background": "transparent",
"popGesture": "none",
"bounce": "none",
"titleNView": false,
"animationDuration": 150
}
}
}

页面样式


<style>
page {
/* 必须的样式,这是页面背景色为透明色 */
background: transparent;
}
</style>
<style lang="scss" scoped>
// 写你页面的其他样式
</style>

这样的话就新建成功了一个透明的页面,那么这个页面上的东西都可以遮挡住原生 tabbarnavbar


我还加了遮罩:


<template>
<view>
<view class="modal" style="z-index: -1"></view>

</view>
</template>

<style lang="scss" scoped>
.modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}
</style>


效果演示


在这里插入图片描述


作者:鹏北海
来源:juejin.cn/post/7317325043541639178
收起阅读 »