注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

手机网站支付(在uniapp同时支持H5和app!)

前言 uniapp开发项目时,遇到对接支付宝手机网站支付。如果仅仅是H5端,那分分钟搞定的(不就是调用后端接口,提交返回表单即可调起支付)。然而,这次需求是H5和App都使用该支付。这倒是新奇了,App中能使用支付宝手机网站支付吗?那它怎么提交表单,怎么处理...
继续阅读 »

前言



uniapp开发项目时,遇到对接支付宝手机网站支付。如果仅仅是H5端,那分分钟搞定的(不就是调用后端接口,提交返回表单即可调起支付)。然而,这次需求是H5和App都使用该支付。这倒是新奇了,App中能使用支付宝手机网站支付吗?那它怎么提交表单怎么处理支付成功时的回调页面跳转




若你仅H5使用支付宝手机网站支付参考我的文章



一、使用技术



  1. 解决app如何提交表单:

    renderjs: app-vue 中调用在视图层操作dom,运行for web的js库
    参考文章

  2. 解决app处理支付成功时的回调页面跳转:

    uni.webview.1.5.4.js: 引入该js,使得普通的H5支持uniapp路由跳转接口参考uniapp文档


二、思路描述



注意:此处会详细描述思路,请根据自身项目需要自行更改



step1|✨用户点击支付


async aliPhonePay() {
let urlprefix = baseUrl == '/api' ?
'http://192.168.105.43'
:
baseUrl;

let params = {
/**1. 支付成功回调页面-中转站*/
// #ifdef H5
frontUrl: `${urlprefix}/middle_html/h5.html?type=${this.formartOrderType(this.orderInfo.orderSn)}`,
// #endif
// #ifdef APP
frontUrl: `${urlprefix}/middle_html/app.html?type=${this.formartOrderType(this.orderInfo.orderSn)}`,
// #endif


goodsDesc: this.orderInfo.itemName,
goodsTitle: this.orderInfo.itemName,
orderSn: this.orderInfo.orderSn,
orderType: this.formartOrderType(this.orderInfo.orderSn),
paymentPrice: (this.orderInfo.paymentPrice*1).toFixed(2),
payChannel: this.paymentType,
// 快捷支付必传
bizProtocolNo: this.bankInfo.bizProtocolNo, //用户业务协议号 ,
payProtocolNo: this.bankInfo.payProtocolNo, //支付协议号
}

this.$refs.dyToast.loading()
let { data } = await PayCenterApi.executePayment(params)
this.$refs.dyToast.hide()

/**2. 保存请求得到的表单到strorage,跳转页面*/
uni.setStorageSync('payForm', data.doPost);
uni.redirectTo({
url:`/pages/goods/goodsOrderPay/new-pay-invoke`
})
},

/pages/goods/goodsOrderPay/new-pay-invoke: h5和app都支持的提交表单调起支付方式


<template>
<view class="new-pay-invoke-container">
<view :payInfo="payInfo" :change:payInfo="pay.openPay" ref="pay"></view>
<u-loading-page loading loading-text="调起支付中"></u-loading-page>
</view>
</template>

<script>
export default {
name: 'new-pay-invoke',

data() {
return {
payInfo: ''
}
},

onLoad(options) {
this.payInfo = uni.getStorageSync('payForm');
}
}
</script>

<script module="pay" lang="renderjs">
export default {
methods: {
/**h5和app都支持的提交表单调起支付方式*/
openPay(payInfo, oldVal, ownerInstance, instance) {
// console.log(payInfo, oldVal, ownerInstance, instance);
if(payForm) {
document.querySelector('body').innerHTML = payInfo
const div = document.createElement('div')
div.innerHTML = payForm
document.body.appendChild(div)
document.forms[0].submit()
}
}
}
}
</script>

<style lang="scss" scoped>

</style>

step2|✨支付成功回调页面


app.html: 作为一个网页,放到线上服务器,注意需要与传递给后端回调地址保持一致


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title>app支付成功回调页面-中转站</title>
</head>
<body>
<!-- uni 的 SDK -->
<!-- 需要把 uni.webview.1.5.4.js 下载到自己的服务器 -->
<script type="text/javascript" src="https://gitee.com/dcloud/uni-app/raw/dev/dist/uni.webview.1.5.4.js"></script>
<script type="text/javascript">
// 待触发 `UniAppJSBridgeReady` 事件后,即可调用 uni 的 API。
document.addEventListener('UniAppJSBridgeReady', function() {
/**引入uni.webview.1.5.4.js后,就支持uni各种路由跳转,使得该H5页面能控制uniapp App页面跳转*/
/**这里做的事是判断订单类型,跳转到app对应的订单支付成功页面 */
uni.reLaunch({
url: '对应支付成功页面?payCallback=1'
// 加payCallback=1参数原因:支付成功页面有时是订单记录,而订单
// 记录不用走支付流程,用户也能进入。这时就需要该参数判断点击
// 返回是 返回上一级 还是 返回首页了
});
});
</script>
</body>
</html>


h5.html:与app.html做法一致,但不需要用到uni.webview.1.5.4.js,这里就不赘述了


以上就是app和h5使用支付宝手机网站支付的全部流程了。
app有点小瑕疵(app提交表单页面后,支付宝页面导航栏会塌陷到状态栏,用户体验稍微差点)
我的猜想:
h5按正常表单提交走,而app利用<webview src="本地网页?表单参数" />本地网页,获取表单参数并拼接表单提交
还没具体去实现这个猜想,或者大家有更好的解决方式,欢迎评论区展示!!!

作者:爆竹
来源:juejin.cn/post/7276692859967864891
收起阅读 »

听说你会架构设计?来,弄一个打车系统

目录 引言 网约车系统 需求设计 概要设计 详细设计 体验优化 小结 1.引言 1.1 台风来袭 深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。 对深圳打工人的具体影响为,当日从下午 4 点起全市...
继续阅读 »

目录




  1. 引言

  2. 网约车系统



    1. 需求设计

    2. 概要设计

    3. 详细设计

    4. 体验优化



  3. 小结



1.引言


1.1 台风来袭


深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。


对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日已经停课、晚上 7 点后停运。


由于下午 4 点停市,于是大部分公司都早早下班。其中有赶点下班的,像这样:



有提前下班的,像这样:



还有像我们这样要居家远程办公的:



1.2 崩溃打车


下午 4 点左右,公交和地铁都人满为患。


于是快下班(居家办公)的时候就想着打个车回家,然而打开滴滴之后:



排队人数 142 位,这个排队人数和时长,让我的心一下就拔凉拔凉的。


根据历史经验,在雨天打上车的应答时间得往后推半个小时左右。更何况,这还是台风天气!


滴滴啊滴滴,你就不能提前准备一下嘛,这个等待时长,会让你损失很多订单分成的。


但反过来想,这种紧急预警,也不能完全怪打车平台,毕竟,车辆调度也是需要一定时间的。在这种大家争相逃命(bushi 的时候,周围的车辆估计也不太够用。


卷起来


等着也是等着,于是就回到公司继续看技术文章。这时我突然想到,经过这次车辆紧急调度之后,如果我是滴滴的开发工程师,需要怎么处理这种情况呢?


如果滴滴的面试官在我眼前,他又会怎么考量候选人的技术深度和产品思维呢?


2. 设计一个“网约车系统”


面试官:“滴滴打车用过是吧!看你简历里写道会架构设计是吧,如果让你设计一个网约车系统,你会从哪些方面考虑呢?”


2.1 需求分析


网约车系统(比如滴滴)的核心功能是把乘客的打车订单发送给附件的网约车司机,司机接单后,到上车点接送乘客,乘客下车后完成订单。


其中,司机通过平台约定的比例抽取分成(70%-80%不等),乘客可以根据第三方平台的信用值(比如支付宝)来开通免密支付,在下车后自动支付订单。用例图如下:



乘客和司机都有注册登录功能,分属于乘客用户模块和司机用户模块。网约车系统的另外核心功能是乘客打车,订单分配,以及司机送单。


2.2 概要设计


网约车系统是互联网+共享资源的一种模式,目的是要把车辆和乘客结合起来,节约已有资源的一种方式,通常是一辆网约车对多个用户。


所以对于乘客和司机来说,他们和系统的交互关系是不同的。比如一个人一天可能只打一次车,而一个司机一天得拉好几趟活。


故我们需要开发两个 APP 应用,分别给乘客和司机打车和接单,架构图如下:



1)乘客视角


如上所示:乘客在手机 App 注册成为用户后,可以选择出发地和目的地,进行打车。


打车请求通过负载均衡服务器,经过请求转发等一系列筛选,然后到达 HTTP 网关集群,再由网关集群进行业务校验,调用相应的微服务。


例如,乘客在手机上获取个人用户信息,收藏的地址信息等,可以将请求转发到用户系统。需要叫车时,将出发地、目的地、个人位置等信息发送至打车系统


2)司机视角


如上图所示:司机在手机 App 注册成为用户并开始接单后,打开手机的位置信息,通过 TCP 长连接定时将自己的位置信息发送给平台,同时也接收平台发布的订单消息。



司机 App 采用 TCP 长连接是因为要定时发送和接收系统消息,若采用 HTTP 推送:


一方面对实时性有影响,另一方面每次通信都得重新建立一次连接会有失体面(耗费资源)。​



司机 App:每 3~5 秒向平台发送一次当前的位置信息,包括车辆经纬度,车头朝向等。TCP 服务器集群相当于网关,只是以 TCP 长连接的方式向 App 提供接入服务,地理位置服务负责管理司机的位置信息。


3)订单接收


网关集群充当业务系统的注册中心,负责安全过滤,业务限流,请求转发等工作。


业务由一个个独立部署的网关服务器组成,当请求过多时,可以通过负载均衡服务器将流量压力分散到不同的网关服务器上。


当用户打车时,通过负载均衡服务器将请求打到某一个网关服务器上,网关首先会调用订单系统,为用户创建一个打车订单(订单状态为 “已创建”),并存库。


然后网关服务器调用打车系统,打车系统将用户信息、用户位置、出发地、目的地等数据封装到一个消息包中,发送到消息队列(比如 RabbitMQ),等待系统为用户订单分配司机。


4)订单分配


订单分配系统作为消息队列的消费者,会实时监听队列中的订单。当获取到新的订单消息时,订单分配系统会将订单状态修改为 “订单分配中”,并存库。


然后,订单分配系统将用户信息、用户位置、出发地、目的地等信息发送给订单推送 SDK


接着,订单推送 SDK 调用地理位置系统,获取司机的实时位置,再结合用户的上车点,选择最合适的司机进行派单,然后把订单消息发送到消息告警系统。这时,订单分配系统将订单状态修改为 “司机已接单” 状态。


订单消息通过专门的消息告警系统进行推送,通过 TCP 长连接将订单推送到匹配上的司机手机 App。


5)拒单和抢单


订单推送 SDK 在分配司机时,会考虑司机当前的订单是否完成。当分配到最合适的司机时,司机也可以根据自身情况选择 “拒单”,但是平台会记录下来评估司机的接单效率。


打车平台里,司机如果拒单太多,就可能在后续的一段时间里将分配订单的权重分数降低,影响自身的业绩。



订单分派逻辑也可以修改为允许附加的司机抢单,具体实现为:


当订单创建后,由订单推送 SDK 将订单消息推送到一定的地理位置范围内的司机 App,在范围内的司机接收到订单消息后可以抢单,抢单完成后,订单状态变为“已派单”。


2.3 详细设计


打车平台的详细设计,我们会关注网约车系统的一些核心功能,如:长连接管理、地址算法、体验优化等。


1)长连接的优势


除了网页上常用的 HTTP 短连接请求,比如:百度搜索一下,输入关键词就发起一个 HTTP 请求,这就是最常用的短连接。


但是大型 APP,尤其是涉及到消息推送的应用(如 QQ、微信、美团等应用),几乎都会搭建一套完整的 TCP 长连接通道。


一张图看懂长连接的优势:



图片来源:《美团点评移动网络优化实践》


通过上图,我们得出结论。相比短连接,长连接优势有三:




  1. 连接成功率高




  2. 网络延时低




  3. 收发消息稳定,不易丢失




2)长连接管理


前面说到了长连接的优势是实时性高,收发消息稳定,而打车系统里司机需要定期发送自身的位置信息,并实时接收订单数据,所以司机 App 采用 TCP 长连接的方式来接入系统。


和 HTTP 无状态连接不同的是,TCP 长连接是有状态的连接。所谓无状态,是指每次用户请求可以随意发送到某一台服务器上,且每台服务器的返回相同,用户不关心是哪台服务器处理的请求。



当然,现在 HTTP2.0 也可以是有状态的长连接,我们此处默认是 HTTP1.x 的情况。



而 TCP 长连接为了保证传输效率和实时性,服务器和用户的手机 App 需要保持长连接的状态,即有状态的连接。


所以司机 App 每次信息上报或消息推送时,都会通过一个特定的连接通道,司机 App 接收消息和发送消息的连接通道是固定不变的。


因此,司机端的 TCP 长连接需要进行专门管理,处理司机 App 和服务器的连接信息,架构图如下:



为了保证每次消息的接收和推送都能找到对应通道,我们需要维护一个司机 App 到 TCP 服务器的映射关系,可以用 Redis 进行保存。


当司机 App 第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,司机 App 会通过用户长连接管理系统重新申请一个服务器连接(可用地址存储在 Zookeeper 中),TCP 连接服务器后再刷新 Redis 的缓存。


3)地址算法


当乘客打车后,订单推送 SDK 会结合司机所在地理位置,结合一个地址算法,计算出最适合的司机进行派单。


目前,手机收集地理位置一般是收集经纬度信息。经度范围是东经 180 到西经 180,纬度范围是南纬 90 到北纬 90。


我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。



根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识司机和乘客的位置信息。再通过 Redis 的 GeoHash 算法,来获取乘客附加的所有司机信息。


GeoHash 算法的原理是将乘客的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有司机


它的实现用到了跳表数据结构,具体实现为:


将某个市区的一块范围作为 GeoHash 的 key,这个市区范围内所有的司机存储到一个跳表中,当乘客的地理位置出现在这个市区范围时,获取该范围内所有的司机信息。然后进一步筛选出最近的司机信息,进行派单。


4)体验优化


1. 距离算法


作为线上派单,通过距离运算来分配订单效果一定会比较差,因为 Redis 计算的是两点之间的空间距离,但司机必须沿道路行驶过来,在复杂的城市路况下,也许几十米的空间距离行驶十几分钟也未可知。


所以,后续需综合行驶距离(而非空间距离)、司机车头朝向以及上车点进行路径规划,来计算区域内每个司机到达乘客的距离和时间。


更进一步,如果区域内有多个乘客和司机,就要考虑所有人的等待时间,以此来优化用户体验,节省派单时间,提升盈利额。



2. 订单优先级


如果打车订单频繁取消,可根据司机或乘客行为进行判责。判责后给乘客和司机计算信誉分,并告知用户信誉分会影响乘客和司机的使用体验,且关联到派单的优先级。


司机接单优先级

综合考虑司机的信誉分,投诉次数,司机的接单数等等,来给不同信誉分的司机分配不同的订单优先级。


乘客派单优先级

根据乘客的打车时间段,打车距离,上车点等信息,做成用户画像,以合理安排司机,或者适当杀熟(bushi。


PS:目前有些不良打车平台就是这么做的 🐶  甚至之前爆出某打车平台,会根据不同的手机系统,进行差异收费


4. 小结


4.1 网约车平台发展


目前,全球网约车市场已经达到了数千亿美元的规模,主要竞争者包括滴滴、Uber、Grab 等公司。在中国,滴滴作为最大的网约车平台已经占据了绝大部分市场份额。


网约车的核心商业逻辑比较简单,利益关联方主要为平台、司机、车辆、消费者。


平台分别对接司机、车辆【非必选项,有很多司机是带车上岗】和乘客,通过有效供需匹配赚取整个共享经济链省下的钱。


具体表现为:乘客和司机分别通过网约平台打车和接单,平台提供技术支持。乘客为打车服务付费,平台从交易金额中抽成(10%-30%不等)。



据全国网约车监管信息交互平台统计,截至 2023 年 2 月底,全国共有 303 家网约车平台公司取得网约车平台经营许可。


这些平台一部分是依靠高德打车、百度地图、美团打车为代表的网约车聚合平台;另一部分则是以滴滴出行、花小猪、T3 为代表的出行平台


4.2 网约车平台现状


随着出行的解封,网约车平台重现生机。


但由于部分网约车聚合平台的准入门槛太低,所以在过去一段时间里暴露出愈来愈多的问题。如车辆、司机合规率低,遇到安全事故,产生责任纠纷,乘客维权困难等等。


由于其特殊的模式,导致其与网约车运营商存在责任边界问题,一直游离在法律边缘。



但随着网约车聚合平台的监管不断落地,全国各地都出行了一定的监管条例。


比如某打车平台要求车辆将司机和乘客的沟通记录留档,除了司机与乘客的在线沟通记录必须保存以外,还需要一个语音电话或车载录音转换,留存一段时间备查。


有了这些人性化的监管条例和技术的不断创新,网约车平台或许会在未来的一段时间内,继续蓬勃发展。


后话


面试官:嗯,又专又红,全面发展!这小伙子不错,关注了~


作者:xin猿意码
来源:juejin.cn/post/7275211391102746684
收起阅读 »

马斯克的Twitter迎来严重危机,我国的超级App模式是否能拯救?

Meta公司近期推出的Threads 被网友戏称为“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。外界普遍认为,这是推特上线17年来遭遇的最严峻危机。面对扎克伯格来势汹汹的挑战马斯克会如何快速组织反击? 前段时间闹得沸沸扬扬的“马扎大战”...
继续阅读 »

Meta公司近期推出的Threads 被网友戏称为“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。外界普遍认为,这是推特上线17年来遭遇的最严峻危机。面对扎克伯格来势汹汹的挑战马斯克会如何快速组织反击?


前段时间闹得沸沸扬扬的“马扎大战”再出新剧情,继“笼斗”约架被马斯克妈妈及时叫停之后,马斯克在7月9日再次向扎克伯克打起嘴炮,这次不仅怒骂小扎是混蛋,还要公开和他比大小?!!此番马斯克的疯狂言论,让网友直呼他不是疯了就是账号被盗了。



互联网各路“吃瓜群众”对于大佬们宛如儿戏般的掐架喜闻乐见,摇旗呐喊!以至于很多人忘了这场闹剧始于一场商战:“马扎大战”开始之初,年轻的扎克伯格先发制人,率先挥出一记左钩拳——Threads,打得老马措手不及。


Threads 被网友戏称“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。其中,不乏从推特中逃离的各界名流。舆论普遍认为,这是Twitter上线17年来遭遇的最严峻危机。



紧接着马斯克还以一记右勾拳,一封律师函向小扎发难,称Meta公司“非法盗用推特的商业秘密和其他知识产权的行为”。虽然Meta公司迅速回应,否认其团队成员中有Twitter的前雇员。但这样的回应似乎没有什么力度,Threads在功能、UI设计上均与Twitter相似,并在相关宣传中表示,Threads“具有良好的运营”,并称其为当前“一片混乱中的”Twitter的绝佳替代品。


社交平台之战的第一个回合,小扎向老马发起了猛烈的攻势。吃了一记闷拳的马斯克除了打嘴炮之外,会如何快速组织有效的反击?


会不会是老马嘴里的“非秘密武器”App X —App of Everything?


超级App或成为Twitter反击重拳


时间回溯到去年,在收购Twitter之前,马斯克就放出豪言即将创建一款他称之为“App X”的功能包罗万有的超级应用软件(Super App), 在他的愿景中,超级 “App X”就如同多功能瑞士军刀(Swiss Army Knife)般,能够包办用户日常生活大小事,包括:社交、购物、打车、支付等等。他希望这款App可以成为美国首个集食、衣、住、行功能于一身的平台。收购Twitter,似乎给了他改造实现这个超级App的起步可能。


马斯克坦言这是从微信的经营模式中汲取的灵感。微信一直被视为“超级应用程序”的代表,作为一体化平台,满足了用户的各种需求,包括即时通讯、社交、支付等等。在去年6月的推特全体员工大会上,马斯克就表示“我们还没有一个像微信那样优秀的应用,所以我的想法是为何不借鉴微信”。马斯克还在推特上写到“购买推特是创建App X的加速器,这是一个超级App(Everything App)。”


从他接手Twitter的任期开始,马斯克便加快推动超级 “App X”的发展步伐。对标于微信,除了社交功能之外,还将推出支付与电子商务。而获得监管许可是实现支付服务的重要第一步,支付也成了推特转型超级 “App X”的第一步,除了商业的必要性外,此举多少还有点宿命感。要知道,马斯克是从支付行业起家的,1999 年他投资 1200 万美元与Intuit前首席执行官 Bill Harris 共同创立了 X.com,而这家公司就是PayPal的前身。


据英国《金融时报》 1月份报道,Twitter 已经开始申请联邦和州监管许可。同时Twitter内部正在开发电子支付功能,未来更会整合其他金融服务,以实现超级App的终极目标。


但是,在亚洲“超级应用”巨头之外,几乎没有消息应用实现支付服务的先例,Whats App和Telegram 都未推出类似服务。老马领导下的Twitter,能不能成功?


添加了支付能力,也只不过是迈向“超级”的第一小步。挑战在于怎么把“everything”卷进来:衣食住行的数字服务、各行各业的商业场景。在微信世界,everything = 小程序。老马是否也要开发一套Twitter版小程序技术、缔造一个“Twitter小程序”宇宙?



“超级App”技术已实现普世化


事实上,马斯克并非“Super App ”技术理念在欧美的唯一拥趸。超级App的雄心壮志多年来早已成为美国公司管理层炫酷PPT展示中的常客了,甚至连沃尔玛都曾考虑过超级App的计划。


全球权威咨询机构Gartner发布的企业机构在2023年需要探索的十大战略技术趋势中也提到了超级应用。并预测,到2027年,全球50%以上的人口将成为多个超级应用的日活跃用户。


国外互联网巨头们开始对超级App技术趋之若鹜,但超级App的技术,是不是只有巨头才能拥有呢?


答案是否定的。互联网技术往往领先于企业应用5~7年,现在这个技术正在进入企业软件世界,任何行业的任何企业都可以拥有。


一种被称为“小程序容器”的技术,是构建超级App的核心,目前已经完全实现普及商用。背后推手是 FinClip,它作为当前市场上唯一独立小程序容器技术产品,致力于把制造超级App的技术带进各行各业,充当下一代企业数字化软件的技术底座。


超级App的技术实现,原理上是围绕一种内容载体,由三项技术共同组成:内容载体通常是某种形态的“轻巧应用”——读者最容易理解的,当然就是小程序,万事万物的数字场景,以小程序形态出现。马斯克大概率在把Twitter改造成他所谓的App X的过程中,要发展出一种类似的东西。反正在国内这就叫小程序,在W3C正在制定的标准里,这叫做Mini-App。我们就姑且依照大家容易理解的习惯,把这种“轻巧应用”称之为小程序吧。


围绕小程序,一个超级App需要在设备端实现“安全沙箱”+ “运行时”,负责把小程序从网上下载、关在一个安全隔离环境中,然后解释运行小程序内容;小程序内容的“镜像”(也就是代码包),则是发布在云端的小程序应用商店里,供超级App的用户在使用到某个商业场景或服务的时候,动态下载到设备端按需运行 – 随需随用且可以用完即弃。小程序应用商店负责了小程序的云端镜像“四态合一“(开发、测试、灰度、投产)的发布管理。


不仅仅这样,超级App本质上是一个庞大的数字生态平台,里面的小程序内容,并不是超级App的开发团队开发的,而是由第三方“进驻”和“上架”,所以,超级App还有一个非常重要的云端运营中心,负责引进和管理小程序化的数字内容生态。


超级App之所以“超级”,是因为它的生命周期(开发、测试、发版、运营),和运行在它里面的那些内容(也就是小程序)的生命周期完全独立,两者解耦,从而可运行“全世界”为其提供的内容、服务,让“全世界”为它提供“插件”而无需担心超级App本身的安全。第三方的内容无论是恶意的、有安全漏洞的或者其他什么潜在风险,并不能影响平台自身的安全稳定、以及平台上由其他人提供的内容安全保密。在建立了这样的安全与隔离机制的基础上,超级App才能实现所谓的“Economy of Scale”(规模效应),可以大开门户,放心让互联网上千行百业的企业、个人“注入插件”,产生丰富的、包罗万有的内容。


对于企业来说,拥有一个自己的超级App意味着什么呢?是超级丰富的业务场景、超级多元的合作生态、超级数量的内容开发者、以及超级敏捷的运营能力。相比传统的、封闭的、烟囱式的App,超级App实际上是帮助企业突破传统边界、建立安全开放策略、与合作伙伴实现数字化资源交换的技术手段,真正让一家企业具备平台化商业模式,加速数字化转型、增强与世界的在线连接、形成自己的网络效应。


超级App不是一个App -- Be A“world” platform


超级App+小程序,这不是互联网大平台的专利。对于传统企业来说,考虑打造自己的超级App动因至少有三:


首先,天下苦应用商店久矣。明明是纯粹企业内部一个商业决策行为,要发布某个功能或服务到自己的App上从而触达自己的客服服务自己的市场,这个发版却不得不经过不相干的第三方(App store们)批准。想象一下,你是一家银行,现在你计划在你的“数字信用卡”App里更新上架某个信用卡服务功能,你的IT完成了开发、测试,你的信用卡业主部门作了验收,你的合规、风控、法务部门通过内部的OA系统环环相扣、层层审批,现在流程到了苹果、谷歌… 排队等候审核,最后流程回到IT,服务器端一顿操作配合,正式开闸上线。你的这个信用卡服务功能,跟苹果谷歌们有一毛钱关系?但对不起,他们在你的审批流程里拥有终极话语权。


企业如果能够控制业务内容的技术实现粒度,通过自己的“服务商店”、“业务内容商店”去控制发布,让“宿主”App保持稳定,则苹果谷歌们也不用去操这个心你的App会不会每次更新都带来安全漏洞或者其他风险行为。


第二,成为一个“world platform”,企业应该有这样的“胸襟”和策略。虽然你可能不是腾讯不是推特不拥有世界级流量,这不妨碍你成为自己所在细分市场细分领域的商业世界里的平台,这里背后的思路是开放——开放平台,让全“世界”的伙伴成为我的生态,哪怕那个“世界”只存在于一个垂直领域。而这,就是数字化转型。讲那么多“数字化转型”理念,不如先落地一个技术平台作为载体,talk is cheap,show me the code。当你拥有一个在自己那个商业世界里的超级App和数以百千计的小程序的时候,你的企业已经数字化转型了。


第三,采用超级App是最有效的云化策略,把你和你的合作伙伴的内容作为小程序,挪到云端去,设备端只是加载运行和安全控制这些小程序内容的入口。在一个小小的手机上弹丸之地,“尺寸”限制了企业IT的生产力 – 无法挤进太大的团队让太多工程师同时开发生产,把一切挪到云上,那里的空间无限大,企业不再受限于“尺寸”,在云上你可以无上限的扩展技术团队,并行开发,互不认识互不打扰,为你供应无限量的内容。互联网大平台上动辄几百万个小程序是怎么来的?并行开发、快速迭代、低成本试错、无限量内容场景供应,这样的技术架构,是不是很值得企业借鉴?


做自己所在细分市场、产业宇宙里的“World Platform”吧,技术的发展已经让这一切唾手可得,也许在马斯克还在打“App of Everything”嘴炮的时候,你的超级App已经瓜熟蒂落、呱呱坠地。


作者:Finbird
链接:https://juejin.cn/post/7256759718811418682
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

鹅厂七年半,写在晋升失败的不眠之夜

夜半惊醒 看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同...
继续阅读 »

夜半惊醒


看了下时间,凌晨4:37,辗转反侧。3.7号大领导告诉我这次10升11没通过,这是我知道晋级失败的第4个夜晚,从震惊到否认,到愤怒,到麻木,再到冷静。我的技术能力、思考深度、攻坚能力明明是小组里突出的一个,但在此次技术职级的晋升上,却名落孙山,落在同组小伙伴之后。技术人员,终究困于技术的窠臼。


工作经历


浑浑噩噩的四年


我毕业就进入鹅厂工作,15年,正是鹅厂大杀四方的一年,至今犹记得当时拿到鹅厂 offer 时的兴奋和同学眼中的艳羡。来了公司后,跟着项目组开始创新项目,一眼望到头的挣不来钱,浑浑噩噩的干了3年,和一起进公司的小伙伴们收入差距越来越大。


好在我的驱力不错,既然挣不到钱,那就好好积累技术能力,提升自己。那段时间,周围没有伙伴,没有对比,没有好的机会,只剩下我一个人慢慢悠悠的学习积累,我以为自己足够努力,没有荒废时间;后来才发现自己其实是井底之蛙,一起入职的小伙伴付出了百倍的努力和数不清的不眠之夜,1年多就已经能 cover 当时的业务了,没过2年就已经提干了。


2018年底,我满怀信心去答辩后台 3.1,没过。我当时还觉得评委装逼,挑刺,后来回头看写的都是s,一点含量都没有。


时来运转


2019年3月,答辩刚结束。部门业务调整,我们被整组划到了一个相对挣钱的业务上,leader 也调整成后台 leader。


新业务还是挣钱,凭着第一个项目积累的技术和项目经验,我得到了新领导的认可,第一年干的轻轻松松就挣到了远远超出之前的收入,和之前累死累活拿温饱线形成鲜明对比,可见大厂里跟对项目是多么的重要。


只是没想到第一年就是巅峰,后面随着贸易战、反垄断,鹅厂形势越来越差,我们业务收入也受到极大影响,人员也越来越冗余。挣的钱少了,分蛋糕的人还多了,可想而知,收入越来越差。


在这里,我成功从8级升到了10级工程师。说起来我们这一届实名惨,第一批毕业要等1年才能晋级,最后一批8升9需要公司范围内通道评审,第一批9升10走BG内评审,第一批10升11走部门内评审,全程试验品,一样没落下。


10升11


去年年底公司晋升改革,10升11评审下放部门,职级和待遇不再挂钩,不仅看重”武功“,还看中”战功“,同时传闻这次晋升名额极少。大领导找我谈话,要不就别去了?我想了想这两年的技术和项目积累,不甘心。


整个中心,就我和同组的小伙伴两个人一起去,我挑的是个一直在做的有技术挑战、持续优化的项目,小伙伴挑的是挣钱的项目。我准备了几个周末,小伙伴临时抱佛脚,结果小伙伴过了,我没过。评委说,我过度设计(优化过头)、没有实战演练容灾。


我和大领导说这个和我预期差太多了,我和小伙伴一起预答辩过,都知道讲的啥,咱是技术评审,我的项目技术含量、架构和挑战明明更好,为啥是我没过?大领导说你是认知偏差,人家讲的确实好。不忿,遂找评委 battle,咱要真按这个说法,能不能对参加评审的项目一致对待,不要双标?且不说我是不是过度设计,凭啥对我的项目就要求容灾演练,对别人的项目就视而不见?评委不语,你的项目对部门价值没产生什么收益和价值。


部门400+人,4个晋升11级名额,大概率是一个中心一个。一个技术评审,糅合了许多超过技术方面的考量,业务挣不挣钱和技术有啥关系?技术好的业务就能挣钱?业务挣钱多的是技术好的原因?


我以前也晋级失败过,但是我认输,因为相对而言,整个BG一起评审,你没过就是你技术差,其他因素影响没有这么大。这次明明是我更有技术深度、更投心思在优化上,却事与愿违。


反思与感悟


反思一下。千头万绪,毛主席说,那是没有抓住主要矛盾。


大的方面上讲:这两年大环境差,公司收入减少,需要降本增效。我要是管理层,也会对内部薪资成本、晋升等要求控制增速;政策制定上,也会将资源更倾斜于现金流业务,控制亏损业务的支出。在这个大的背景下,公司不愿意养“能力强,战功少”的技术骨干了;更愿意养“能力强,战功多”的人员。希望员工把精力都聚焦在业务挣钱上。


部门层面上讲:和公司政策一脉相承。晋升名额少,那紧着挣钱的中心、挣钱的小组、挣钱的个人给。这个意义上讲,职级体系已经不是衡量技术能力的标尺了。你技术能力强,没用。你得是核心,得是领导认可的人。


中心层面上讲:要争取名额,否则怎么团结手底下人干活。而且名额要给业务干活干的最出色的那个人,其他人就往后稍稍。我要是我领导,年前也会劝退我的。毕竟我技术能力虽然强,更多的是问题专家的角色,而不是业务核心。


且预测一下,中心之间肯定进行了各种资源置换。评审估计流于形式。慢说我被挑出来了问题,就是挑不出来问题也得给你薅下来。我就是和小伙伴互换项目去讲,估计还是小伙伴过,到时候评委就该说我“你做这东西有啥难度,我叫个毕业生都能来做”了。此处我想起了一副对联,不太合适,就是很搞笑。“说你行你就行不行也行,说你不行你就不行行也不行”,横批是“不服不行”。



从个人角度讲,付出和收获不成正比,难受吗?那肯定的。但谁让方向一开始就错了呢?这世界不公平的事情多了,不差这一个。



重新出发


综上来说,短期内,不改变发力方向到业务上,后面可以说晋升无望。以前老是觉得自己技术能力强,心高气傲,心思没在业务上,勤于术而懒于道,实在是太过幼稚。做了几年都对很多东西不甚了了。


今后,需要趁着还能拼的时候拼一下,潜心业务。编码和开发只是很小一部分,更要从:


  1. 业务大局出发,实时数据驱动,监控、统计一定要完善,要有看板,否则无法衡量。
  2. 主动探索业务,给产品经理出主意,一起合力把产品做上去做大,提升对业务的贡献,探索推荐、探索推广渠道等等。产品做不上去,个人也没机会发展。
  3. 多做向上管理,和领导、大领导多沟通。做好安排的每一件小事,同时主动汇报,争取重获信任。
  4. 主动承担,做一个领导眼里靠谱放心的人。
  5. 多思考总结,多拔高提升,不要做表面的勤奋工作,看似没有浪费时间,实则每分每秒都在浪费时间。
  6. 多社交,多沟通,多交流,打破技术人员的牢笼。

凡事都有两面性,福兮祸所依,祸兮福所倚。多受挫折、早受挫折不是坏事。


2023,就立个 flag 在这里吧。


作者:醉梦星河
链接:https://juejin.cn/post/7208907027840630840
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

WWDC23发布了什么 (速看版)

iOS
今天凌晨WWDC 2023正式召开,本文分析介绍了其中的精华部分 有关如何观看可以阅读👉 WWDC 2023 观看指南 Keynote 常规硬件发布 Mac Macbook Air 新款 M2 芯片的15 寸 Macbook Air 拥有8核CPU以及10核G...
继续阅读 »

今天凌晨WWDC 2023正式召开,本文分析介绍了其中的精华部分


有关如何观看可以阅读👉 WWDC 2023 观看指南


Keynote


常规硬件发布


Mac


Macbook Air


新款 M2 芯片的15 寸 Macbook Air


  • 拥有8核CPU以及10核GPU
  • 边框厚度5毫米
  • 屏幕亮度最高可达500尼特
  • 15.3英寸支持1080P高清摄像头
  • 支持Six-Speaker Sound system六声道音响以及Touch ID指纹识别
  • 硬盘方面最高可拓展至2TB
  • 内存最高可拓展至24GB
  • 提供18个小时电池续航
  • 售价10499元起,即日起开始预订,下周发售







Mac Studio


新款 Mac Studio 搭载M2 Max和M2 Ultra两款芯片

  • 拥有24核心CPU以及76核心GPU
  • 配备32核心网络神经引擎
  • 支持最高192GB内存拓展
  • 8TB硬盘拓展
  • 支持8K外接显示
  • 售价16499元起,下周起售







Mac Pro


Mac 产品线最强大的一员,Mac Pro 也迎来了 Apple Silicon,至此全系 Mac 产品线已完成从 Intel 芯片向 Apple Silicon 转变


  • 配置基本同 Mac Studio
  • 售价55999元起





常规软件发布


iOS 17


iOS 17主要进行了细节优化和小功能迭代更新

  • 全新自定义来电界面形象

  • Facetime新增语音留言

  • Messages支持搜索 & 地图信息

  • 新增 Check In功能

  • 新增全局 Live Sticker

  • 改进键盘输入法,增加词语联想输入与纠错功能

  • 可交互Widget

  • 新系统级App Journal 手记 App [今年稍晚推出]

  • NameDrop: AirDrop的升级功能,可在一台手机与另外设备接触时进行隔空投送,如超过隔空投送距离,还可通过蜂窝数据将剩余未传完内容继续投送

  • 待机体验功能:将iPhone横放在手机支架上能够显示时钟,天气以及小组件





iOS 开发者需要关心的是:


  • 可交互 Widget,已有Widget的App可以重新思考Widget的设计
  • 全局Live Sticker,兼容性测试 & 是否需要进行专门适配


iPadOS 17


除了共享上述提到的iOS更新外,iPadOS主要有以下方面的更新

  • 去年iOS 16的自定义壁纸功能加入 iPadOS

  • 健康 App 登陆 iPadOS,提供大屏健康信息查阅体验

  • 更好的系统级 PDF 支持







macOS 14



新一代 macOS 命名为 Sonoma,主要的特点如下

  • 加入 Metal 3和MetalFX Upscaling功能

  • 添加系统级别游戏模式,为主流手柄提供更好的蓝牙采样支持

  • 《死亡搁浅》登录macOS平台,制作人现场展示了“死亡搁浅导演剪辑版”

  • 支持添加 Widget 到 macOS 桌面

  • 支持添加 iPhone 上的Widget 到 macOS,会通过 iPhone 端进行更新然后传输到 macOS 渲染显示







watchOS 10

  • 全新设计的智能叠放组件

  • 运动方面:更加详细的运动数据记录,同时数据也会同步显示在配对的iPhone上

  • 户外方面:支持记录离开信号区的位置,发送卫星求助信息,自动生成海拔图

  • 心理健康:增加对抑郁症和焦虑症的自测功能,距离屏幕距离过近时还会进行提醒,降低近视风险




tvOS 17 & AirPods


tvOS 17:

  • 支持 FaceTime 和视频流转,可将iPhone与iPad收到的FaceTime来电投射到Apple TV上进行视频通话

  • 支持 FaceTime 时的人物居中模式

  • 允许第三方视频通话应用程序,利用iPhone和iPad作为直播源,在Apple TV进行FaceTime视频通话


AirPods:


  • 添加自适应模式,在通透模式和降噪模式中智能切换



One More Thing



新硬件 VisionPro + 对应新操作系统 visionOS



时隔十年,苹果终于发布自家的 VR/AR 头戴式设备,入局该领域



TLDR:发售价3499$



硬件


  • M2 芯片 + R1 芯片
  • 2300万像素 Micro-OLED 屏幕
  • 单眼分辨率超 4K 电视
  • 满电续航 2h
  • 12个摄像头 + 5个传感器 + 6个麦克风
  • 全新空间音频体验

交互


  • 搭载 visionOS 系统
  • 使用 眼睛、手势、声音完成操控
  • 与 iPhone Mac设备无缝联动使用
  • 支持 Optic ID虹膜识别

体验


  • 全新 App Store
  • 大部分 iOS & iPadOS 可以直接兼容使用
  • 首个 3D 相机







新的 VisionPro 和 visionOS 的信息后续会有专门介绍,这里就不再过多展开


Platforms State of the Union


上面的 Keynote 部分是全球消费者比较关注的,而后续的PSTU则是 iOS 开发者更为关心的更新


这里主要突出下 IDE 和 Language 的更新


Xcode 15

  • 发布了最新的 Static Linker,据称最快是 ld64 的 5 倍性能提升

  • 新的 library format: mergeable libraries ,这是一种动静结合的二进制,Debug 的时候动态链接,Release 的时候静态链接,兼顾性能和开发体验

  • 支持自动生成对图片和颜色资源的静态访问API



Swift 5.9

  • 添加了 Swift Macro 支持,简化了大量的模版代码编写

  • 新的 SwiftData 数据库框架



作者:叶絮雷
链接:https://juejin.cn/post/7244062903203004473
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

第一个可以在条件语句中使用的原生hook诞生了

大家好,我卡颂。 在10月13日的first-class-support-for-promises RFC中,介绍了一种新的hook —— use。 use什么?就是use,这个hook就叫use。这也是第一个:可以在条件语句中书写的hook可以在其他hook...
继续阅读 »

大家好,我卡颂。


在10月13日的first-class-support-for-promises RFC中,介绍了一种新的hook —— use


use什么?就是use,这个hook就叫use。这也是第一个:

  • 可以在条件语句中书写的hook

  • 可以在其他hook回调中书写的hook


本文来聊聊这个特殊的hook


欢迎加入人类高质量前端框架研究群,带飞


use是什么


我们知道,async函数会配合await关键词使用,比如:

async function load() {
const {name} = await fetchName();
return name;
}

类似的,在React组件中,可以配合use起到类似的效果,比如:

function Cpn() {
const {name} = use(fetchName());
return <p>{name}</p>;
}

可以认为,use的作用类似于:

  • async await中的await

  • generator中的yield


use作为读取异步数据的原语,可以配合Suspense实现数据请求、加载、返回的逻辑。


举个例子,下述例子中,当fetchNote执行异步请求时,会由包裹NoteSuspense组件渲染加载中状态


当请求成功时,会重新渲染,此时note数据会正常返回。


当请求失败时,会由包裹NoteErrorBoundary组件处理失败逻辑。

function Note({id}) {
const note = use(fetchNote(id));
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
);
}

其背后的实现原理并不复杂:

  1. Note组件首次renderfetchNote发起请求,会throw promise,打断render流程

  2. Suspense fallback作为渲染结果

  3. promise状态变化后重新触发渲染

  4. 根据note的返回值渲染


实际上这套基于promise的打断、重新渲染流程当前已经存在了。use的存在就是为了替换上述流程。


与当前React中已经存在的上述promise流程不同,use仅仅是个原语primitives),并不是完整的处理流程。


比如,use并没有缓存promise的能力。


举个例子,在下面代码中fetchTodo执行后会返回一个promiseuse会消费这个promise

async function fetchTodo(id) {
const data = await fetchDataFromCache(`/api/todos/${id}`);
return {contents: data.contents};
}

function Todo({id, isSelected}) {
const todo = use(fetchTodo(id));
return (
<div className={isSelected ? 'selected-todo' : 'normal-todo'}>
{todo.contents}
</div>
);
}

Todo组件的id prop变化后,触发fetchTodo重新请求是符合逻辑的。


但是当isSelected prop变化后,Todo组件也会重新renderfetchTodo执行后会返回一个新的promise


返回新的promise不一定产生新的请求(取决于fetchTodo的实现),但一定会影响React接下来的运行流程(比如不能命中性能优化)。


这时候,需要配合React提供的cache API(同样处于RFC)。


下述代码中,如果id prop不变,fetchTodo始终返回同一个promise

const fetchTodo = cache(async (id) => {
const data = await fetchDataFromCache(`/api/todos/${id}`);
return {contents: data.contents};
});

use的潜在作用


当前,use的应用场景局限在包裹promise


但是未来,use会作为客户端中处理异步数据的主要手段,比如:


  • 处理context

use(Context)能达到与useContext(Context)一样的效果,区别在于前者可以在条件语句,以及其他hook回调内执行。


  • 处理state

可以利用use实现新的原生状态管理方案:

const currentState = use(store);
const latestValue = use(observable);

为什么不使用async await


本文开篇提到,use原语类似async await中的await,那为什么不直接使用async await呢?类似下面这样:

// Note 是 React 组件
async function Note({id, isEditing}) {
const note = await db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{isEditing ? <NoteEditor note={note} /> : null}
</div>
);
}

有两方面原因。


一方面,async await的工作方式与React客户端处理异步时的逻辑不太一样。


await的请求resolve后,调用栈是从await语句继续执行的(generatoryield也是这样)。


而在React中,更新流程是从根组件开始的,所以当数据返回后,更新流程是从根组件从头开始的。


改用async await的方式势必对当前React底层架构带来挑战。最起码,会对性能优化产生不小的影响。


另一方面,async await这种方式接下来会在Server Component中实现,也就是异步的服务端组件。


服务端组件与客户端组件都是React组件,但前者在服务端渲染(SSR),后者在客户端渲染(CSR),如果都用async await,不太容易从代码层面区分两者。


总结


use是一个读取异步数据的原语,他的出现是为了规范React在客户端处理异步数据的方式。


既然是原语,那么他的功能就很底层,比如不包括请求的缓存功能(由cache处理)。


之所以这么设计,是因为React团队并不希望开发者直接使用他们。这些原语的受众是React生态中的其他库。


比如,类似SWRReact-Query这样的请求库,就可以结合use,再结合自己实现的请求缓存策略(而不是使用React提供的cache方法)


各种状态管理库,也可以将use作为其底层状态单元的容器。


值得吐槽的是,Hooks文档中hook的限制那一节恐怕得重写了。


作者:魔术师卡颂
链接:https://juejin.cn/post/7155673959084589070
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

RxSwift核心流程简介

iOS
前言 RxSwift是一个基于响应式编程的Swift框架,它提供了一种简洁而强大的方式来处理异步和事件驱动的编程任务。在RxSwift中,核心流程包括观察者、可观察序列和订阅。 RxSwift核心流程三部曲 // 1.创建序列 _ = Observa...
继续阅读 »

前言


RxSwift是一个基于响应式编程的Swift框架,它提供了一种简洁而强大的方式来处理异步和事件驱动的编程任务。在RxSwift中,核心流程包括观察者可观察序列订阅


RxSwift核心流程三部曲

   // 1.创建序列
_ = Observable<String>.create { ob in
// 3.发送信号
ob.onNext("你好")
return Disposables.create()
// 2.订阅序列
}.subscribe(onNext: { text in
print("订阅到了\(text)")
})
}

  • 1.创建序列
  • 2.订阅序列
  • 3.发送信号

上面三部曲的执行结果:



 第一次玩RxSwift比较好奇为什么会打印订阅到了你好,明明是两个闭包里面的代码。
我们先简单分析下:

  • 序列创建create后面带了闭包A闭包A里面执行了发送信号的流程
  • 订阅subsribe后面带了闭包B
  • 根据结果我们知道一定是先执行了闭包A,再把闭包A你好传给了闭包B,然后输出结果

RxSwift核心逻辑分析


创建序列




点进create函数可以看到它是拓展了ObservableType这个协议,同时创建了一个AnonymousObservable内部类(看名字是匿名序列,具备一些通用的特性)分析AnonymousObservable的继承链可以得到下面的关系图:




AnonymousObservable



 AnonymousObservable是接受Element泛型的继承自Producer的类,他接受并保存一个闭包subscribeHandler的参数,这个其实就是上面我们说的闭包A,另外有一个run函数(后面会提到)


Producer



 Producer是接受Element泛型的继承自Observable的类,有一个subscribe的实现,run的抽象方法,这个subscribe非常重要


Observable



 Observable是接受Element泛型的实现ObservableType协议的类,有一个subscribe的抽象方法,asObservable的实现(返回self,统一万物皆序列)
同时Observable有统计引用计数的能力(Resources这个结构体在序列观察者销毁者等都用到,可以调试是否有内存泄露),其中的AtomicInt是一把NSLock的锁,保证数据的存取安全




ObservableType




ObservableType是拓展ObservableConvertibleType协议的协议,定义了subscribe协议方法,实现了asObservable()方法,所以这里我们得出结论,不一定要继承Observable的才是序列,只要是实现了ObservableTypesubscribe的协议方法的也可以算是序列,进一步佐证万物接序列


ObservableConvertibleType




ObservableConvertibleType是个协议,关联了Element类型,定义asObservable的协议方法


订阅序列


点击subscribe函数




它是ObservableType的拓展能力,创建了一个AnonymousObserver(匿名观察者)
,接受的Element仔细查看继承链代码会发现跟序列创建的泛型是同一个


分析AnonymousObserver的继承链我们可以得到下图:




AnonymousObserver



 AnonymousObserver是接受Element泛型的继承自ObserverBase的类
保存了一个eventHandler的闭包,这个我们定义是闭包C
同时也有统计引用计数的能力,有一个onCore的实现


ObserverBase




ObserverBase是接受Element泛型的实现DisposableObserverType两个协议的类,有一个on的实现,onCore的抽象方法


ObserverType




ObserverType关联了Element,定义了on的协议方法,拓展定义了onNextonCompletedonError的方法,这三个方法其实都是on一个Event


其中Event是个枚举,有三类事件:next事件error事件completed事件

  • next事件next事件携带了一个值,表示数据的更新或新的事件。
  • error事件error事件表示发生了一个错误,中断了事件的正常流程。
  • completed事件completed事件表示事件流的结束,不再有新的事件产生。 观察者通过订阅可观察序列来接收事件。

Disposable




Disposable这个协议比较简单,定义了dispose方法


订阅流程分析

  • 1.调用self.asObservable().subscribe(observer)

    • 这个selfAnonymousObservable的实例
    • 调用asObservable方法通过继承链最终调用Observable的实现,返回self,也就还是AnonymousObservable的实例
  • 2.调用AnonymousObservable的实例的subscribe方法,通过继承链调用Producersubscribe方法


    • 3.Producerrun方法在AnonymousObservable有实现

     这个sink的处理是相当不错的,很好的做到了业务下沉,同时很好的运用了中间件单一职责的设计模式,值得学习。

    sink是管道的意思,下水道,什么东西都会往里面丢,这里面有订阅者销毁者

      1. sink.run
      1. parent.subscribeHandler(AnyObserver(self))这里的parent就是AnonymousObservable的实例,调用subscribeHandler这个也就是我们定义的闭包A 这里解释了订阅的时候会来到我们的闭包A的原因。 这里需要注意到AnyObserver这个类,他里面保存的observer属性其实是AnonymousObservableSink.on函数

发送信号


有了上两步的基础我们分析发送信号的流程应该比较清晰了

    1. obserber.onNext 其实就是AnyObserver.onNext
    1. ObserverType.onNext其实就是ObserverType.on
    1. 其实就是AnyObserver.on

    • 4.这个observer就是上面第二步最后的AnonymousObservableSink.on函数

    • 5.父类Sink.forwardOn函数 这里的self.observer类型是 AnonymousObserver

    • 6.调用AnonymousObserver的父类ObserverBaseon方法

    • 7.调用AnonymousObserveronCore方法

    • 8.调用eventHandler,也就是我们定义的闭包C
    • 9.闭包C根据Event调用闭包B闭包B输出了控制台的结果,至此,整个链路执行完毕了。




把整个核心流程用思维导图描述出来:




总结

  • 万物皆序列,序列的概念统一了编码
  • 完整的继承链做到了业务分离单一职责
  • 中间价模式很好的做到了业务下沉

作者:大帅哥哦
链接:https://juejin.cn/post/7233980221107748921
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

流量思维的觉醒,互联网原来是这么玩的

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。 微创业,认知很低 大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。 没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机...
继续阅读 »

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。


微创业,认知很低


大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。


没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机,做起了点小买卖。所以就发现如果我手动给同学处理订单会非常麻烦。他们把文件通过qq发给我,我这边打开,排版,确认格式没有问题之后算一个价格,然后打印。


所以根据痛点,我打算开发一个线上自助下单,商户自动打印的一整套系统。


百折不挠,项目终于上线


21年年中克服各种困难终于实现整套系统,提供了小程序端,商户客户端,web端。


用户在手机或网页上上传文件后会自动转换为pdf,还提供了在线预览,避免因为格式与用户本地不同的纠纷。可以自由调节单双面、打印范围、打印分数、色彩等参数。实时算出价格,自助下单。下单后服务器会通知商户客户端拉取新任务,拉取成功后将文件丢入打印队列中。打印完成后商户客户端发送信息,并由服务器转发,告知用户取件。






大三下学期,宿舍里通过线上平台,在期末考试最忙那段期间经过了“订单高峰”的考验,成交金额上千块钱。看着我商户端里面一个个跳动的文件,就像流入口袋里的💰,开心。


商业化的很失败


没想到,我自己就是我最大的客户。


期末考完,其实想拉上我的同学大干一场,让校里校外的所有的商户,都用上我们的软件,多好的东西啊。对于盈利模式的概念非常模糊,同时也有很强的竞品。我的同学并不看好我。


我对商业化的理解也源自美团模式,美团是外卖的流量入口,所以对商户抽佣很高。滴滴是打车的流量入口,对司机的抽佣也很高。所以我认为,假设我未来成为了自助打印的流量入口,那应该也可以试试抽佣模式。


而且就算我不能为商户引流,也能解放他们的双手。


当时的我,一个人做技术,做UI,还要做商业计划,去地推,真的搞得我精疲力尽。反正后面觉得短期内变现无望,就去腾讯实习了。


其实也推广了2个商户,但是他们因为各种原因不愿意用。一个是出于隐私合规风险的考虑,一个是订单量少,不需要。


所以基本这个自助打印只能框死在高校。大学生打印的文件私密性很低,但是单价低,量多,有自助打印的需求。还有一部分自助打印的场景是在行政办事大厅,这种估计没点门门道道是开不进去的。


看不懂的竞品玩法


商户通过我的平台走,我这边并不无本万利。


因为开通了微信支付、支付宝支付,做过的小伙伴应该都知道办这些手续也会花一些钱,公司还要每年花钱养。还有需要给用户的文档成转换成pdf,提供在线预览,这很消耗算力和带宽,如果用户的成交单价非常低,哪怕抽佣5%都是亏的。比如用户打印了100份1页的内容,和打印了1份100页的内容,对我来说成本差别很大,前者很低,后者很高。


当时学校里已经有一部分商户用上自助打印了。一共有3个竞品。


竞品A:不抽佣,但是每笔订单对用户收取固定的服务费,界面简陋,有广告。


竞品B:不抽佣,不收用户的服务费,界面清爽无广告。


竞品C:彻彻底底走无人模式,店铺内基本没有老板,店铺是自营或加盟的。


前期缺乏市场调研,后期缺乏商业认知


当时我在没有摸清自己商业模式,市场调研也没怎么做好的情况下。一心想的就是先把东西做出来再说,卖不成自己还能学到技术。毕竟技术这个玩意不在项目里历练,永远都是纸上谈兵。所以对于商业化的设想就是搞不成就不搞了。


我当时的想法就是要“轻”运营,就是最好我的利润是稳定的,不会亏损的。商户如果要用就得每笔订单都给我一笔钱。


后面为了补齐和竞品的功能差距,也耗费了大量心力。让我把项目从一个大学课程设计,变成了一个有商业化潜力的产品。


竞品玩法的底层逻辑


商业化的时候,就发现这个市场还是蛮卷的,不可能直接和商户收钱。竞品B不仅免费,还想着帮商户创造额外收入,做“增益”。那我确实是没有精力去对抗的。


我当时也没搞懂自己的定位,我究竟是tob还是toc。当时想着我精心设计的界面,怎么可以被广告侵蚀?那可是我的心血。所以一心想把产品体验做的比竞品好,就会有人用。但这个定位也很模糊,因为如果商户不用你的,用户怎么可能用你的下单呢。


其实应该to rmb。面向利润开发。美,是奢侈品,那是属于我内心的一种追求,但他很难具有说服力让商户使用。在国内的各种互联网产品,不盈利的产品最后都是越来越粗糙,越来越丑的,都要降本增效。而rmb是必需品,如果不能为各方创造价值,那就没有竞争力。


所以后续分析了一下各家的玩法:


竞品A:传统商业模式,依靠用户强制付费和广告,市占率一般,和第一差了10倍数量级。


竞品B:烧钱模式,免费给商户用,免费给用户用,自己想办法别的渠道做增益,还要补贴商户。市占率第一。先圈地,再养鱼,变现的事之后再说。


竞品C:不单单做打印软件,卖的是项目。一整套自助打印店的解决方案,不知道店铺能不能赚钱,但是可以先赚加盟商的钱。这个对商业运作的要求会很高,我一时半会做不了。


大佬指点了一下我


他说,你看现在什么自助贩卖机,其实就是一个流量入口。至于别的盈利不盈利再说,但是流量是值钱的。


我最近去查阿拉丁指数,了解到了买量和卖量的观念,重新认识了流量,因为知道价格了。


买量和卖量是什么?


买量说的就是你做了一个app,花钱让别人给你引流。


卖量就是你有一个日活很高的平台,可以为别人引流。


买量和卖量如何结算?


一般分为cpc和cpa两种计价方式。前者是只要用户点击了我的引流广告,广告主就得掏钱。后者是用户可能还需要注册并激活账号,完成一系列操作才掏钱。


一般价格在0.1-0.3元,每次引流。


后面我查了一下竞品B在卖量,每天可以提供10-30w的uv,单次引流报价0.1元。也就是理想情况下,每天可以有1-3w的广告费收入。


侧面说明了竞品B的市占率啊,在这个细分市场做到这个DAU……


关于流量,逆向思维的建立


流量是实现商业利益的工具。


工具类应用通过为别人引流将流量变现,内容类应用通过电商将流量变现的更贵。


依靠流量赚钱有两种姿势,主动迎合需求,和培养需求。前者就是你可以做一些大家必须要用的东西,来获得流量。比如自助打印小程序,只要商户接入了,那么他的所有顾客都会为这个小程序贡献流量。比如地铁乘车码,所有坐地铁的人都会用到,比如广州地铁就在卖量,每天有几百万的日活。


培养需求就是做自己看好的东西,但是当下不明朗,尝试发掘用户潜在的需求。


流量,如果不能利用好,那就是无效流量。所以正确的姿势是,发掘目标人群 -> 设计变现方案 -> 针对性的开发他们喜欢的内容或工具 -> 完成变现。而不是 自己发现有个东西不错 -> 开发出来 -> 测试一下市场反应 -> 期盼突然爆红,躺着收钱。


研究报告也蛮有意思,主打的就是一个研究如何将用户口袋里的钱转移到自己口袋里。做什么产品和个人喜好无关,和有没有市场前景相关。


互联网是基于实体的


互联网并不和实体脱钩,大部分平台依赖广告收入,但广告基本都是实体企业来掏钱。还有电商也是,消费不好,企业赚不到钱,就不愿意投更多推广费。


作者:程序员Alvin
链接:https://juejin.cn/post/7248118049583906872
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

94年码农的6年转型计划

全文4300字,整体目录如下 作者简介:持续探索副业的奶爸程序员 2020年,985硕士毕业,非科班转行成程序员 2022年,进入某互联网大厂,打工搬砖 2023年,开始探索副业,目前主攻IP+工具开发 现在29岁的我 往回看6年,我刚进入985读硕士 ...
继续阅读 »

全文4300字,整体目录如下




作者简介:持续探索副业的奶爸程序员 2020年,985硕士毕业,非科班转行成程序员 2022年,进入某互联网大厂,打工搬砖 2023年,开始探索副业,目前主攻IP+工具开发



现在29岁的我


往回看6年,我刚进入985读硕士


往后看6年,我将面临网上所说的35岁中年危机


因此,借此机会,聊下我对未来的思考和回顾下简单过去的6年


未来6年-战略上乐观,战术上悲观


看待未来,我需要保持乐观,只有这样,才能不为未来的不确定而过分焦虑


还是学生时代的时候,因为对这程序员这行业不清楚,当时就很害怕网上常说的35岁的失业危机,为此还在网上查了各种各样的信息,整天忐忑不已。


可真正进入了这个行业以后,才发现危机远没有想象中的恐怖,原来,恐惧真的源于对未知的不确定。


身边也有好些35以上的朋友,他们有的还在程序员这行,有的已经转行了。虽然整体来看,薪酬水平或者薪酬增长速度不如之前,但远没有到达山穷水尽的地步。


即使是现在ai时代的到来,我依然相信,只要程序员去积极的拥抱ai,使用ai去做更多创造性的工作,也不会突然就失业。


但同时,如果35岁的我,还是会被失业危机所困的话,那么一定就是平常的日子太过懈怠,处于温水煮青蛙的状态。


22年刚入大厂的半年里,基本就处于这个状态,除了工作外,剩下的时间基本都用来娱乐了,成长很是有限。


因此,我需要在战术上保持悲观,要不断成长,要确保自己将主要精力放下以下三方面的事情


1、做好主业,保持市场竞争力,被裁/失业时,能快速找到工作


2、开展第二曲线,降低未来失业可能带来的现金流锻炼的风险


3、爱护好自己的身体,照顾好家人,帮助朋友。


先来聊下第二点和第三点吧,第一点在文末聊。


未来6年-做好第二曲线


为什么开展


2022年过年期间,开始意识到现在的看似高薪工作并不稳定,需要在工作外,建立第二曲线(也就是副业),降低未来的风险。


原因有二,内心的渴望+外在的环境


内在的渴望就是,其实自己一直是一个很爱好学习的人,也希望做出点成绩获得外界认可的人。


在3月之前,也一直在保持学习,科学习的那点热情基本全用在了阅读各种书籍以及得到上,看了几十本书,学了好几本课程,可是成长却极为有限。


幸而在3月的时候遇见了生财有术,看见了更多的可能性,也提升了很多认知,因而,内在的渴望进一步扩大。


外在的环境,一方面是工作的不确定性,另一方面,是身上责任的加重。


自动20年当程序员以来,身边的朋友一茬接一茬的换,有的突然就被迫失业了,有的就跳槽了,有些朋友,甚至都没来得及告别,就已经后会无期了。


再加上网上的铺天盖地的悲观主义和程序员危机。想开展副业,抵抗未来的不确定的决心越来越强。 目前还没房贷车贷,这里的加重倒不是说现金流上的压力加重


只是觉得,作为一个父亲,应该为孩子去铺一条更好的道路,不希望等到我孩子需要我支持帮助的时候,我却面临中年危机。


同时,我也希望孩子从我这里获得更多的认知和经验,而仅仅只继续专注于程序员的话,这个希望是有点难以实现的。(因为我个人觉得,程序员这行,距离真实的商业事件挺远的)


这几个月的效果


到目前为止,从2023年3月算起,差不多开展5个月了,在金钱上的收获很少,累计也没超过500吧。


先后做过


1、小程序(做了2款小程序,但都是学习阶段的程序,未盈利)


2、小红书无货源店铺(赚了200多吧,其实还是朋友的支持)


3、公众号流量主(赚了没超过50吧)


说下后2个没赚大钱的最大原因吧:我有个很大的毛病,就是爱学习,但不注重学习的结果,在实际执行过程中,碰到点问题就会泄气。


同时,过分在意做事的时间成本,导致执行力不够。(后2个项目,其实只要投入时间认真去做,都不只赚我这点钱。)


不过虽然金钱上的收获不多,在技能、认知和人脉上还是提升了很多


人脉上,认识了好些其他行业的朋友,各行各业的都有。 认知上,知道了要多输出表达、要有流量意识、要懂得链接他人 技能上,也是突破了后端能力,会了一点vue app,小程序搭建能力。


当然,最重要的是,这个过程极大的提高了我对未来的信心


因为我知道,只要认真专注的执行某一个赚钱的领域,我就能一定能赚到一点钱。


不再是之前那种担心如果失业了,就前途一片阴暗的感觉了。


对接下来的思考


接下来的6年,为了发展好第二曲线。我需要做以下的事情:


1、需要克服执行力差、技术傲慢、纸上谈兵等一系列的问题,去扎实的投入实战中。


2、在过程中,尽早找到适合自己的长期事业,并专注的投入(我希望在30岁以前能够找到。)


3、相信积累的力量,不断坚持。


6年以后的我,一定能够发展好自己的第二曲线。


未来6年-爱护自己,照顾家人,帮助朋友


从6年后的视角看,其实最重要的是这三件事,爱护好自己,照顾好家人,帮助好朋友


爱护自己


健康是一切的起点,没有健康的话,其他所有的都是白搭。


现在的身体状况应该是挺糟糕的,肥胖而且不运动,6年后最容易出现的问题,应该就是肥胖带来的问题了。


也因此


1、需要有意识的去控制自己的体重,定期体检,适当运动。


2、平常养好身体,工作上不要太用力,压力不要太大。


照顾家人


6年后,孩子就到了上小学的年纪了。父母也都65左右了,这么看的话,主要是父母的健康问题需要考虑。


也因此


1、已经给父母买了医疗险,但还没给岳父母买,需要2023年落实


2、每年带父母/岳父母 体检。


帮助朋友


志同道合的朋友,于我来说,是不可或缺的,也是能极大的提升幸福感的。


也因此


1、积极拓展志同道合的朋友


2、维护好现有的朋友,真诚利他。


(最近建了个程序员副业群,欢迎私聊加入)


好,接下里回顾下过去的6年


过去6年-转行当程序员


为什么转行


我来自湖南农村,家里挺穷,是那种穷到连上大学学费都要借的那种。


2012-2016年在某985读本科,在校就是天天混日子,大四想考学校电气没考上,毕业时连份工作都没有,于是决定二战考研。考完研后,在湖南省长沙市新东方做了八年的小学奥数老师,保底薪资5k,钱少事多的一份工作。


2017年秋,以笔试和面试都是专业第一的成绩,顺利成为一位硕士。


在2017年开始读硕士时,实验室的师兄就丢给我一本《21天精通Java》,说:“你先学习这个哈,后面做实验会用到”。也因此,开始接触Java。(事实,我到现在都没有精通Java )


2018年,实验室接了头部水电企业的一个项目,需要给他们做一个系统,我就参与进来了,然后,还去这个头部企业公司内部实习了半年。


在那里工作,我看到那些公司的员工有的40 50岁了,每天都是在办公室上来了又走,每天的工作都规律的不行,中午午休2个半小时,下午5点半准时下班。有事没事去打个乒乓球,跑个步什么的。


那时候还年轻啊,也没有足够的经验认知,就觉得,这样安逸的生活,一眼看到头的生活,完全不是我想要的。我还年轻,还有大好年华,我要去闯荡,去见识更多的可能性,去看更多的世界。(事实证明,随便在哪工作,你都可以去看大千事件)


于是,从2018年开始就开始坚定的要转行。


转行成功的因素


现在看,非科班转行成功主要有3个因素:


一是学历给了我很大的加成。我是985本硕,在2020年的就业市场上,还是有很大竞争优势的。


二是实验室恰好有一两个项目和IT搭边。现在好多转行的人,做的项目基本都是往上那种通用的项目,这种项目,要是深耕下去的话,确实也能收获很多。但一般转行的人,但研究的比较浅,也因此,在项目上没有多少竞争优势。


三是我自己也还算刻苦。记得当时,经常一两点在那看《深入理解Java虚拟机》、《Java并发编程》等。花了3个月一页页的看完了《算法.第4版》。甚至还花了2个月恶补了计算机基础。同时,也在CSDN上输出自己的学习记录


最后,也是2020年的顺利的校招毕业,拿到当时挺高年薪的offer,进入了北京某头部地产当Java工程师


这是我当时的面试经历 app.yinxiang.com/fx/fc7e01fa…


过去6年- 跳槽到大厂的经历


想跳槽的原因


2020年7月进入公司,从2021年下半年开始,很明显的感觉整个部门的业务动荡。


再加上身边的人一个个的被裁了,虽然说我是校招+管培生,裁员短期内不会落到我头上,但我知道,这一天迟早会到来。


(后来也表明,22年开始,公司开始裁我们这些校招生了。)


当然,还有另外一个很重要的因素,当初和夫人异地恋,我们相约在深圳见面。


关于我在这家公司的情况,请见这个链接:北京,再见。下一站,深圳


跳槽的过程


我这个人脑子比较笨,技术底子也差。但肯下苦功夫 。


从2022年9月开始,以极客时间为主要学习渠道,开始疯狂的学习。主要学习的就是和八股文相关的课程。(记得那时候,身边的朋友都说,你是真的能学的进去阿,也有好几个朋友,被我卷的也开始看书学习了)。


从2021年12月开始,知道要为2022年的3月的黄金跳槽期做准备了。于是给自己列了个学习计划,并差不多严格执行了。


从21年12月开始,知道要为22年的3月的黄金跳槽期做准备了。于是给自己列了个学习计划,并差不多严格执行了。


与此同时,我发现思维导图很适合做这种八股文的笔记和辅助记忆,于是就在ProcessOn上持续记录学习笔记。(后来还将笔记分享给你100+朋友)


刘卡卡 | ProcessOn


一个人学习的道路总是艰辛的,经常感觉坚持不下去,感觉很孤独,没人交流。幸好在1月进入了知识星球代码随想录,里面都是为了找到好工作而奋斗的人,大家一起交流探讨,互相打卡监督,整个人的学习劲头也开始上来了。


也是在2022年3月底,面了差不多10家公司后,如愿以偿的拿到了现在的深圳大厂的工作。


过去6年- 大厂一年多以来的感想


2022年4月,成功进入大厂 。


前面3-4个月的时候,真的很累,一来是不并不适应大厂的自己干自己活的氛围,二来也是技术上也还待欠缺,三是业务复杂度很高,四是每天要应对Oncall处理。


但干了半年左右后,也就开始适应了。(人果然是一种适应性的动物。)


现在的我,在大厂内,就是当一名勤勤恳恳的螺丝钉,


同时在心态上,也有了很大的转变。


1、接受自己不爱竞争的性格,只要自己心里不卷的话,其他人也就卷不到我。


2、将工作看的很清晰,工作就是为了挣钱,因此,如果工作上有什么不如意的地方,切莫影响到自己的生活,不值当。


当然,工作中也不能躺平,要在日常的工作中去多做积累经验,沉淀知识,保持市场竞争力。


好了,洋洋洒洒写了4000多字了,就先到这吧,希望6年后的我,看到这篇文章的时候,能说一句:


你真的做到了,谢谢你这6年的努力


作者:刘卡卡
链接:https://juejin.cn/post/7263669060473520186
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

SwiftData-苹果最先进的数据库

iOS
SwiftData 用于在声明式UI开发(SwiftUI)中进行数据持久化。您可以使用 Swift 代码查询和过滤数据了。 创建模型 使用带有@Model的普通 Swift 类型对数据进行建模,无需关心底层文件存储。 SwiftData 自动推断关系(rel...
继续阅读 »

SwiftData 用于在声明式UI开发(SwiftUI)中进行数据持久化。您可以使用 Swift 代码查询和过滤数据了。




创建模型


使用带有@Model的普通 Swift 类型对数据进行建模,无需关心底层文件存储。


SwiftData 自动推断关系(relationships),您可以使用清晰的声明比如@Attribute(.unique)来描述属性约束

@Model
class Recipe {
@Attribute(.unique) var name: String // 在相同类型的所有模型中属性的值是唯一的。
var summary: String?
var ingredients: [Ingredient]
}

自动持久性


SwiftData 使用Model(模型)构建自定义schema,并将其字段有效地映射底层存储


由 SwiftData 管理的对象在需要时从数据库中获取,并在适当的时候自动保存,您无需进行额外的工作


您还可以使用 ModelContext API 进行完全控制。


与 SwiftUI 集成


在 SwiftUI views中使用@Query来获取数据。SwiftData 和 SwiftUI 协同工作,在基础数据更改时提供视图的实时更新无需手动刷新

@Query var recipes: [Recipe] // 获取一组模型并使模型与底层数据保持同步的property wrapper(属性包装器)。

var body: some View {
List(recipes) { recipe in
NavigationLink(recipe.name, destination: RecipeView(recipe))
}
}

Swift-native predicates


无需使用复杂 SQL, 使用表达式(编译器自动类型检查)来查询和筛选数据,以便在开发过程中捕获拼写错误。


当表达式无法映射到基础存储引擎时,谓词会提供编译时错误

let simpleFood = #Predicate<Recipe> { recipe in
recipe.ingredients.count < 3
}

CloudKit同步


您的数据可以使用DocumentGroup储存在文件中并通过 iCloud Drive 同步到云端,,也可以使用 CloudKit 在设备之间同步数据。


与Core Data兼容


SwiftData 使用经过验证的 Core Data 存储架构,因此您可以在具有相同底层存储的同一App中使用两者。


Xcode 将 Core Data Models转换为类以与 SwiftData 一起使用。


作者:Bowen_J
链接:https://juejin.cn/post/7241562753163870269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从尤雨溪这两天微博募捐,思考开源如何赚大钱

这两天,尤大在他的微博表示,他打算开启国内开源捐赠计划,截止本文发帖为止,已经有 6k / 月的固定充电了。 这个数额目前还是比较小的,企业级别的 sponsor 应该还没有出现,光靠个人捐赠的话这点钱真的完全不够团队开销的。 正巧我看到了 Ink 作者的...
继续阅读 »

这两天,尤大在他的微博表示,他打算开启国内开源捐赠计划,截止本文发帖为止,已经有 6k / 月的固定充电了。


这个数额目前还是比较小的,企业级别的 sponsor 应该还没有出现,光靠个人捐赠的话这点钱真的完全不够团队开销的。






正巧我看到了 Ink 作者的一篇文章,讲述他在开源软件如何稳定搞钱这方面的思考,觉得他的很多观点非常犀利,值得各位前端开发者同学一起学习,毕竟大家未来可能有搞开源的一天。
接下来是他的这篇 Generating income from open source 的内容:


最近,Ink 的知名度越来越高,并且已经被一些知名公司使用了一段时间。然而,与大多数其他开源项目一样,Ink没有任何收入。


我开始研究各种选项,以改变这种情况,并以某种方式开始收费,这样它就可以支持我以及 Ink 和相关项目(如 Ink UIPastel) 的进一步开发。


本文是我在这个主题上所学到的内容的简要版本。


不起作用的方法


以下是我认为维护者无法从他们的项目中获得收入的原因。


依靠个人捐赠


能够有人愿意支持你是很好的,但是每月 5 美元的捐赠无法维持生活。这是社区对你工作的感激的一种方式,但不应被视为稳定的收入来源。


除非你是社区中极少数非常受欢迎的开发者之一,否则接受事实,不会有足够多的人订阅每月捐赠。


尽管如此,我认为个人捐赠并不是答案。


期望公司捐赠


你构建了很火的项目,并在生产环境中稳定运行,他们从中获益良多。当然,他们肯定知道要回馈一下,毕竟他们赚了那么多钱,是这样的吗?


我们需要最终明白一些简单的道理,改变我们的预期。


经营业务意味着最大化收入和最小化支出。企业不会为了只是为了对你好点,而增加一个长期开支。(万恶的资本家)


企业习惯于以金钱交换价值。开源维护者需要考虑到这一点。你提供价值,他们从中受益并为此付费。


确实有一些拥有强大开源文化的公司可以持续给他们依赖的项目提供重大的每月捐赠,但不幸的是,他们是个例。


完全依赖捐赠或赞助


下面这句话,是不是很耳熟?



请赞助我吧,这样我就可以继续开发我的开源项目。



我们整了一个漂亮的 GitHub 赞助页面,然后坐在那里等待有人注册。你能想象一个企业采用类似的策略吗?它会在一个月内破产倒闭。


我们需要理解我们的项目对公司所提供的价值,并开始收费,就像我们经营一家企业,销售一种有用的产品。


认为没有人愿意付费或者定价不够高


在几家中小型初创公司工作过后,我现在明白几年前自己有多么愚蠢,以为每月 200 块的订阅费是天价,或者公司不愿意为工具付费。纯属扯犊子。


公司为员工解决日常问题和开发产品支付数百万的钞票。如果你的项目解决了他们的问题,使他们的团队不必自己解决,他们会支付比你认为的价值高 10 倍、100 倍甚至 1000 倍的费用。而且,他们会很满意。


公司已经为各种工具和费用支付数万元每月。无论你要求什么,实际上对他们来说都是九牛一毛。把你的产品价格翻倍吧,没毛病。


害怕或者羞于索要信用卡信息


我们不需要为我们的工作收费找理由。没有什么可羞耻的。


你为解决一个问题而付出你的努力。有人为了这个问题请你付费解决,别多虑了。


有效方法


我们喜欢抱怨没人支付维护者的费用,但实际上有很多建立在开源基础上的成功企业。以下是它们持续收入的秘诀:


商业许可证


Dave DeSandro 的Metafizzy提供各种 JavaScript 库,其中包括 Isotope - 用于创建灵活网格布局的库。Isotope 是开源的,但根据你的使用方式有不同的许可证



  1. 开源许可证。


这个许可证允许在个人或开源项目中免费使用 Isotope。



  1. 商业许可证。


这个许可证允许你在几乎任何商业应用中使用 Isotope。实际上,任何希望使用它的公司很可能需要购买商业许可证。


商业许可证的定价根据使用人数而不同:

  • 单个开发者的费用为 25 美元。
  • 8 名开发者团队的费用为 110 美元。
  • 无限数量的开发者的费用为 320 美元。

请注意,这些不是订阅,而是一次性付款。


商业许可证本身是一份 PDF 文件,支付后通过 Gumroad 发送给你。



  1. 商业 OEM 许可证。


该许可证适用于先前的商业许可证未涵盖的其他用途,特别是 UI 构建器、SDK 或工具包。对于商业 OEM 许可证没有公开的定价,这意味着它比前几个等级要贵得多。这些用例可能意味着 Isotope 作为用户界面或产品提供中的关键组成部分,因此公司愿意支付高额费用。


我喜欢这种方法的原因


这看起来是对开源进行收费最简单的方式,因为 Metafizzy 为同一份代码提供了不同的许可证,许可证本身是一个 PDF 文件。没有专业版,没有许可证密钥,也没有其他需要维护的东西。个人开发者可以免费使用同样的工具,而公司则支付合理的价格。


为更多功能收费


Mike Perham 的Sidekiq是一个在 Ruby 应用程序中基于 Redis 的后台作业的著名的库。Sidekiq 提供了 3 种不同的计划:



  1. 开源版。


Sidekiq 免费提供一个有限的开源版本。尽管它被称为“开源”,但 LGPL 许可证似乎允许你在商业应用中使用免费版本。


开源计划不提供任何客户支持,有问题就去提 GitHub Issue 吧。



  1. 专业版。


专业版每月收费 99 美元(或 995 美元/年),提供更多的功能。例如,批处理后台作业、通过更高级的 Redis API 提供的增强可靠性。专业版还包括通过电子邮件提供的客户支持。



  1. 企业版。


企业版根据你运行的 Sidekiq 实例数量,以 229 美元/月或更高的价格提供全部功能。


Sidekiq 的表现非常出色,根据 Mike 在 Hacker News 的最新评论,它现在每年创造 1000 万美元的收入。


有趣的是,他还提到,你可以通过其他开源 Ruby gem 组装 Sidekiq 的大多数付费功能,但是设置和维护起来需要很多时间。最终,你可能会得到一个比经过多次测试的 Sidekiq 还要糟糕的系统,所以购买功能齐全的 Sidekiq 似乎是明智之举。



Sidekiq 的大多数商业功能都可作为开源软件包获得,但是当你将 3-6 个这些功能集成在一起时,复杂性会悄然而至。自己构建往往会导致一个比我精心策划的成熟、经过良好调试的系统还要差的系统。



一旦你注册了 Sidekiq,你将获得访问私有 Ruby gem 服务器的权限,可以从中下载并更新应用程序中的sidekiq gem。他自己构建了这个系统,并表示不用花太多时间维护它。


我喜欢这种方法的原因


Sidekiq 首先是一个很棒的开源项目。在 Ruby 社区中,当你需要后台队列时,它成为了一个明显的选择。这是 Sidekiq 唯一的营销渠道。


然后,开发人员向他们的朋友和公司的管理人员推荐 Sidekiq。随着他们的应用程序扩大,客户有明显的动机支付 Sidekiq 以解锁更多功能。


托管版本


最近,越来越多的企业将其整个产品开源,并提供托管版本以获取收费。

  • Plausible Analytics - 一个注重隐私的 Google Analytics 替代方案。托管版本每月起价 9 美元。
  • PostHog - 产品分析、功能标志、A/B 测试等多个数据工具的组合。托管版本采用按用量计费,前 100 万个事件免费,之后每个事件收费 0.0003068 美元。
  • Metabase - 数据库仪表板。托管版本每月起价 85 美元。

这些只是我能想到的例子,还有许多类似的例子。


我喜欢这种方法的原因


你可以构建一次应用程序,并将相同版本作为开源和托管付费产品提供。你可能会想:“为什么有人愿意为可免费获得的东西付费”。然而,Plausible Analytics 每年收入 100 万美元,所以肯定有很多人愿意支付小额的月费来享受他们的产品,而不用自己搞乱七八糟的服务器啥的。


收费维护和高级材料


Moritz Klack、Christopher Möller、John Robb 和 Hayleigh Thompson 的React Flow是一个用于交互式流程图的 React 库。这是一个可持续的开源项目,与我以前见过的任何项目都不同。React Flow 为公司提供了一个专业版订阅,其中提供以下功能:

  • 访问专业版高级用例示例。
  • 优先解决 GitHub 上的问题。
  • 每月最多 1 小时的电子邮件支持。
  • 最有趣的是,我引用一下,“保持库的运行和维护,采用 MIT 许可证”。

在整个定价页面上,大部分文案都集中在最后一点上。React Flow 不是一个容易用其他东西替代的库,所以公司很可能有兴趣确保它得到良好的维护,并继续使用 MIT 许可。


John 在他们的博客上写了一篇优秀的文章,名为“Dear Open Source: let’s do a better job of asking for money”,我建议你阅读一下。我对此非常着迷,所以给 John 发了一封邮件,提出了一些后续问题,他非常友善地回答了我关于这个话题的许多宝贵的知识。


以下是我从我们的邮件往来中总结出的要点:

  • 包装很重要。公司内部持有信用卡的人希望看到他们一直在看到的“定价”页面。GitHub 赞助页面行不通。React Flow 最初有一个这样的页面,但几乎没有获得任何收入。当他们推出一个类似 SaaS 的产品网站,并提供几个定价层次时,情况改善了。
  • 让大家发现专业版计划。React Flow 组件显示一个指向他们网站的链接,并要求开发人员在订阅专业版计划后将其删除。即使在不这样做的情况下删除它仍然完全合法和可以接受,但它作为一个不会强迫的好方法,可以促使人们查看专业版计划。
  • 公司在有支持的情况下更有安全感。React Flow 每月提供最多 1 小时的电子邮件支持,所以我自然而然地问如果客户花费的时间超过 1 小时会发生什么。John 表示,即使如此,他们还是会继续通过电子邮件提供支持,最后一切都会平衡,因为有很多客户根本不联系他们。他还认为,电子邮件支持会给人一种保险的感觉,因此公司知道如果有需要,他们可以找到他们,即使他们从未这样做过。
  • 为人们提供可以立即购买和访问的东西。我想知道那些对专业版客户可用的高级示例有多重要,因为与其他好处相比,它们似乎只是一种美好的附加功能。令人惊讶的是,John 有不同的看法。他坚信,购买后立即提供一些有价值的东西可以将他们的专业版计划与咨询公司或服务区分开来。这还为客户提供了一个参考点,他们可以在项目中使用并学习。此外,这还有助于吸引那些对 React Flow 感兴趣的公司。

我喜欢这种方法的原因


React Flow 以其出色的开源库而闻名,但他们找到了一种明智的方式在商业上获得收入。他们在定价、包装和支持方面的决策都非常明智,并成功地转化了开源用户为付费客户。


这是我了解到的一些有关将开源项目变为可持续收入的方法。希望这些例子能给你提供一些灵感和启示!


支持包


最后但同样重要的是,你可以围绕你的开源工作建立一家咨询公司,并向依赖于该工作的公司提供专业知识支持。

  • Babel 在他们的Open Collective页面上提供了每年 2.4 万美元的计划,其中公司每月可以获得 2 小时的电子邮件或视频支持。
  • curl 提供商业支持,甚至包括开发定制功能和代码审核以了解你如何使用 curl。
  • Filippo Valsorda向公司提供每年五位数的保留协议。Filippo 与工程师会面,了解他们的需求,并在开发他的开源软件时确保这些需求得到满足。Filippo 是一个密码学专家,所以公司可以签订更昂贵的合同,以获得他在与密码学相关的任何事物上的专业知识,而不仅仅是他自己的项目。

我喜欢这种方法的原因


为公司提供付费支持使你的项目保持完全开源的同时,比 Pro 订阅带来更多的收入。这个过程很难,但对于一个习惯于作为员工工作的人来说,很有吸引力。


结论


偶尔会在 Hacker News 上看到人们讨论开源模式的缺点,护者没有从受益于他们工作的公司那里获得任何收入。


这不公平。他们能做些什么?可以有多种可行的选项可以生成可持续的收入,也有许多成功的例子说明人们今天正在这样做,并且已经持续了很久。这也可能适用于你,快去试试吧,否则你永远不会知道。


作者:ssh_晨曦时梦见兮
链接:https://juejin.cn/post/7245584147456278589
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

科大讯飞,这次彻底爆了!支持国产!!!

前言 讯飞星火,正式面向开发者全场景开放!在开放首日,讯飞星火14小时用户数突破100万,迅速登上AppStore免费总排行榜第一。 讯飞星火认知大模型全面开放,携手开发者共建人工智能「星火」新生态。 现在讯飞星火即可注册使用了。 作为程序员,我一直在关注大模...
继续阅读 »

前言


讯飞星火,正式面向开发者全场景开放!在开放首日,讯飞星火14小时用户数突破100万,迅速登上AppStore免费总排行榜第一


讯飞星火认知大模型全面开放,携手开发者共建人工智能「星火」新生态
现在讯飞星火即可注册使用了


作为程序员,我一直在关注大模型的发展,尤其是大模型的代码能力,因为这将直接影响到程序员的日常工作,驾驭一个代码能力强悍的大模型,对工作效率的提升实在太大了。


代码生成能力


先来尝试一个简单的:写一段代码,判断用户输入的密码满足特定规则,长度不小于8位,必须包含大小写字母,数字和特殊符号。


如果这个功能都搞不定,就不用往下尝试了。


讯飞星火不负所托,完整地实现了功能。



接下来搞个复杂一点儿的:用Python创建一个贪吃蛇游戏。



代码在几秒内就生成了,玩起来的效果是这样的:



可以说,完成度非常高,游戏的基本功能都实现了,剩下一些细节,比如改变颜色什么的,我们可以自己稍加调整即可。


讯飞星火对代码的理解能力怎么样呢?


我想它也许对高级编程语言如Python,Java, C++等做过训练,那我就剑走偏锋,扔给它一段汇编语言编写的代码,汇编现在很少有人直接使用了,也许会把它难住。 



出乎意料的是,讯飞星火一下子就给出了这段代码的功能:“这段汇编代码是一个简单的加法程序,用于计算两个数的和”,并且给出了逐行的解释。



为了让程序员在IDE中能无缝地使用大模型的能力,讯飞星火还发布了一个智能编程助手,在IDE中可以轻松地生成代码,进行代码解释,对代码进行纠错,进行单元测试,这对程序员来说是个巨大的福音。




可以看出,讯飞星火的代码生成能力已经非常突出了,随着大模型的不断进化和针对不同场景的打磨,讯飞星火肯定会成为程序员的巨大助力。


多模态能力


多模态能力是指处理和理解多种模态信息的能力,包括文本、图像、视频、音频等。


在此之前,我们看到的很多大模型如ChatGPT都是只支持文本,这一次讯飞星火V2.0正式支持多模态了。


除了我最关注的代码能力和多模态能力之外,讯飞星火还提供了功能强大的插件和助手,支持文档问答,生成PPT,生成简历,可以极大地提升办公效率。


生图能力


星火大模型不仅能生成文本,也能生成图片。


使用起来很简单,比如让 AI 帮我们画几张图。


输入 画一个红烧狮子头



输入:画一只正在奔跑的小乌龟



简历生成能力


配合【简历生成】插件,讯飞星火大模型可以生成不同风格的简历模板。



输入:我叫赵四,有5年Java开发经验,请帮我生成一份简历



文本对话能力


例如输入:我想学习Java,请问该如何开始




限于篇幅,我这里就不一一介绍了,小伙们赶紧去尝试一下吧。


写在最后


以上的功能只是星火认知的冰山一角,还有更多涵盖生活、学习、工作等方方面面的功能。如果你还没有使用,赶紧点击注册体验吧


作者:程序员追风
链接:https://juejin.cn/post/7275942983544471567
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS非公开App分发实践

iOS
一、前言 非公开分发是苹果新推出的一种分发方式,适用于为有限范围用户开发、不适合在App Store上公开分发的App,比如一些没有注册功能,由公司下发账号密码的企业内部应用。 苹果官方对非公开App分发的描述: developer.apple.com/cn/...
继续阅读 »

一、前言


非公开分发是苹果新推出的一种分发方式,适用于为有限范围用户开发、不适合在App Store上公开分发的App,比如一些没有注册功能,由公司下发账号密码的企业内部应用。


苹果官方对非公开App分发的描述:
developer.apple.com/cn/support/…


二、苹果分发方式对比


三、非公开分发



作为苹果新推出的分发方式,非公开分发有如下特点:

  1. 要为非公开分发的App申请非公开App链接
  2. 用个人或公司开发者账号在App Store发布,但是不能直接在App Store搜到,只能通过短链接被访问
  3. 由于要上架App Store,和普通app一样,要提交到苹果审核,审核通过之后可访问
  4. 已经在App Store中公开上架的app可以申请非公开App链接,转为非公开分发App
  5. 非公开分发App的销售范围是App Store支持的所有区域

四、分发非公开App


创建App并提交审核

1. 按照公开分发的方式创建App并填写信息

2. 初始创建App提交审核时,App分发方式选择公开,非公开App链接申请通过后App分发方式会自动转为非公开分发 image.png


3. 审核信息备注里说明App用于非公开分发


 

4. App提交审核


申请非公开App链接


非公开App链接的申请地址如下:
developer.apple.com/contact/req…


提交非公开分发请求时需要满足以下两点:

  1. App已经提交至苹果进行审核或者已经上架,不能为处于Beta版本的App提交非公开请求,否则会被拒
  2. 如果使用的是公司开发者账号,只有主账号有提交非公开请求的权限,使用子账号申请时页面打不开,错误信息如下:



非公开链接申请通过后开发者账号邮箱会收到一封通知邮件:




App的分发方式也会自动的变成非公开分发:




如果非公开App链接申请下来之前App审核因为3.2被拒,不用着急,等非公开链接申请通过之后再次提交即可。


非公开App链接申请页信息是英文,输入填写相关信息时用中、英文都可以,问题描述的越详细审核越容易过,我第一次提交后几个小时就过了。


最后


随着苹果公司对企业账号的收紧,2022年不少公司在续费时遇到了账号重新审查,万一审查不过,结果就是账号不能续费无法继续使用,之前通过企业账号分发的App必须考虑别的分发方式。


苹果官方给的建议是Apple 商务管理非公开 App 分发两种方案,相对于商务管理下载时需要管理兑换码,下载更方便的非公开App分发不失为一种新尝试。


作者:是Susie啊
链接:https://juejin.cn/post/7184765499836743738
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

ios 打包静态库

iOS
前言: 各位同学大家, 有段时间没有跟大家见面了。 相信很多做IOS手游sdk 的同学 都会用到静态库, 我们不用把我们都源代码都发给对接方 就可以把我们的逻辑跟研发都代码融合在一起 具体实现: 第一步 点击file  第二步创建一个pr...
继续阅读 »

前言:


各位同学大家, 有段时间没有跟大家见面了。 相信很多做IOS手游sdk 的同学 都会用到静态库, 我们不用把我们都源代码都发给对接方 就可以把我们的逻辑跟研发都代码融合在一起


具体实现:


第一步 点击file 


 第二步创建一个project 


 第三步我们选择 static Library 工程


最终我们这样的一个工程



 在xcode 最新版本里面 有的同学 发现没有 Prodoucts 这个目录 这个是因为xcode的bug








mainGroup = 0D7441EC2A0A715000C95252;
productRefGroup = 0D7441EC2A0A715000C95252;

保证这2行后面都配置一样的如果不一样 就复制 mainGroup 后面到productRefGroup 然后保存即可 然后刷新xcode 就就会出现 Prodoucts


暴露头文件 我们需要把我们对外开放都类的头文件 也就是.h文件 暴露出去 然后方便对接方 接入



 如图我们将我们ninefunsdk.h这个文件

 还有我们都 Roleinfo.h 和Seriveinfo. h 文件也需要暴露出去

 打包 cmd +b 



具体接入




效果图




最后总结:


IOS 打包静态库 我们就讲完, 比较简单 我们只需要对流程清除即可 有兴趣同学可以根据教程一步一步学习

最后呢 希望我都文章能帮助到各位同学工作和学习 如果你觉得文章还不错麻烦给我三连 关注点赞和转发 谢谢


作者:xq9527
链接:https://juejin.cn/post/7231720920984322105
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

图片转换成webp

web
webp的几个问题 1. 什么是webp? 最直接的就是一个图片的后缀是.webp而不是.png/.jpeg等,官方的说法就是由Google开发的一种用于图像压缩的现代格式,目的就是减小图片的大小从而提高网页加载速; 2. 是不是所有浏览器都支持webp图片?...
继续阅读 »

webp的几个问题


1. 什么是webp?


最直接的就是一个图片的后缀是.webp而不是.png/.jpeg等,官方的说法就是由Google开发的一种用于图像压缩的现代格式,目的就是减小图片的大小从而提高网页加载速;


2. 是不是所有浏览器都支持webp图片?如何判断浏览器是否支持webp格式的图片


不是所有的浏览器都支持 WebP 图片格式,但大多数主流的现代浏览器都已经支持了。以下是一些常见的浏览器对 WebP 格式的支持情况:



  • Google Chrome:支持 WebP 格式。

  • Mozilla Firefox:支持 WebP 格式。

  • Microsoft Edge:支持 WebP 格式。

  • Safari:从 Safari 14 开始,支持 WebP 格式
    要判断浏览器是否支持 WebP 格式的图片,可以使用 JavaScript 进行检测。以下是一种常用的方法:


function isWebPSupported() {
var elem = document.createElement('canvas');
if (!!(elem.getContext && elem.getContext('2d'))) {
// canvas 支持
return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
// canvas 不支持
return false;
}

if (isWebPSupported()) {
console.log('浏览器支持 WebP 格式');
} else {
console.log('浏览器不支持 WebP 格式');
}


上述代码通过创建一个 canvas 元素,并尝试将其转换为 WebP 格式的图片。如果浏览器支持 WebP 格式,则会返回一个以 "data:image/webp" 开头的数据 URL。


通过这种方式,你可以在网页中使用 JavaScript 检测浏览器是否支持 WebP 格式,并根据需要提供适当的替代图片


3. 图片转换成webp之后一定会比之前的图片更小吗?


答案是否定的。一般来说,具有大量细节、颜色变化和复杂结构的图像可能会在转换为 WebP 格式后获得更好的压缩效果,反之有些转换后可能会比之前更大;所以最好是图片转换为 WebP 格式之前,建议进行测试和比较不同压缩参数和质量级别的结果,以找到最佳的压缩设置,对最终转换后变成更大的建议不做转换


4. 如何将图片转换成webp



  • 图像编辑软件 如 Adobe Photoshop、GIMP 或在线工具,如 Google 的 WebP 编码器。这些工具可以让你将现有的图像转换为 WebP 格式,并选择压缩质量和压缩类型(有损或无损)

  • 插件转换webp插件文档链接接入


image.png


5. 项目中如何接入??


思路:



  • 第一步肯定是转化将项目中的存储的图片文件通过插件转换出webp格式的图片

  • 判断网页运行的浏览器是否支持webp格式的图片,如果支持,将项目中所有使用png/jpeg的图片的全部替换成webp


6. 转换出项目中图片的webp格式的图片


const imagemin = require("imagemin");
const imageminWebp = require("imagemin-webp");

function transformToWebp(destination, filePaths) {
await imagemin([filePath || `${destination}/*.{jpg,png}`], {
destination: `${destination}/webp/`, // 转换出的webp图片放置在什么目录
plugins: [imageminWebp({quality: 75})] // 使用imageminWebp转换转换质量级别设置多少
})
}

具体到项目中,我们只希望转换我们当前正在开发的文件夹中的图片,而且已经转化的未作修改的就不要再重复转化; 如何知道哪些是新增的或者修改的呢? 想一想🤔️,是不是“git status”可以看到
所以开始做如下调整


// 获取git仓库中发生变更的文件列表
function getGitStatusChangedImgFiles() {
return String(execSync('git status -s'))
.split('\n')
.map(item => item.split(' ').pop()
.filter(path => path.match(/\.(jpg)|(png)/))
);
};

返回一个包含变更图片文件路径的数组['src/example/image/a.png','src/example/image/b.png', '……']


const imgPaths = getGitStatusChangedImgFiles()
async function transformAllChangedImgToWebp() {
const resData = await promise.all(
imgPaths.map(path => {
const imgDir = path.replace(/([^\\/]+)\.([^\\/]+)/i, "") // src/banners/guardian_8/img/95_copy.png => src/banners/guardian_8/img/
return transformToWebp(imgDir, path)
})
)
const allDestinationPaths = resData.map((subArr) => subArr[0].destinationPath)
// 如果这里我们想将生成的webp图片自动的add上去,那么就这样:
execSync(`git add ${allDestinationPaths.join(" ")}`);
}



image.png


什么时候转换成webp最好?


我们在commit的时候进行转换图片,以及自动将转换的图片进行提交
这样我们就可以运用git的钩子函数处理了;


npm install husky --save-dev

// .husky/pre-commit中
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

current_branch=`git rev-parse --abbrev-ref HEAD`

if [[ $current_branch === 'main']]; then
# 生成 webp 图片
npm run webp -- commit
fi

这样在我们commit时就会自动触发pre-commit钩子函数,在package.json中配置webp执行的脚步,执行上述transformAllChangedImgToWebp函数,然后在里面转换出webp图片并将新生成的webp自动git add上去,最后一并commit;


知识点


1. execSync是什么?


execSync 是一个 Node.js 内置模块 child_process 中的方法,用于同步执行外部命令。在 Node.js 中,child_process 模块提供了一组用于创建子进程的函数,其中包括 execSync 方法。execSync 方法用于执行指定的命令,并等待命令执行完成后返回结果。


const { execSync } = require('child_process'); const output = execSync(command, options);

2. git status -s 会显示每个文件的状态信息



  • A:新增文件

  • M:修改文件

  • D:删除文件

  • R:文件名修改

  • C:文件的拷贝

  • U:未知状态


image.png


3. execSync('git status -s')返回值是什么?


image.png


通过String后就可以变成可见的字符串了,然后通过分割等就能拿到具体的修改的文件路径


4. Husky是什么?


Husky 是一个用于在 Git 提交过程中执行脚本的工具。它可以帮助开发人员在代码提交前或提交后执行一些自定义的脚本,例如代码格式化、代码质量检查、单元测试等。Husky 可以确保团队成员在提交代码之前遵循一致的规范和约定。


Husky 的工作原理是通过在 Git 钩子(Git hooks)中注册脚本来实现的。Git 钩子是在特定的 Git 事件发生时执行的脚本,例如在提交代码前执行 pre-commit 钩子,或在提交代码后执行 post-commit 钩子。push代码前执行pre-push的钩子、编写提交信息时执行commit-msg的钩子可用于提交什么规范


小结



  1. 通过execSync('git status -s')从中获取筛选当前新增/修改过的图片;

  2. 调用imagemin和imagemin-webp将图片转换出webp格式的图片

  3. husky的pre-commit中触发上述调用执行,并在里面顺道将新生成的webp一并add上去

  4. 至于后续生成的webp图片怎么使用,这将在下一篇文章中学习


作者:东风t西瓜
来源:juejin.cn/post/7260016275300155449
收起阅读 »

关于述职答辩的一点思考和总结

公众号:赵侠客  侠客说:优秀人才的四个特征:格局、思路、实干、写作 一、前言 1.1 述职答辩的重要性 公司都会有晋升通道,述职答辩是你想升职加薪除了跳槽以外的必由之路,其重要性对个人发展来说不言而喻,对公司来说也是选拔人才的重要通道。本人不才就职的也不是...
继续阅读 »

公众号:赵侠客 


侠客说:优秀人才的四个特征:格局、思路、实干、写作



一、前言


1.1 述职答辩的重要性


公司都会有晋升通道,述职答辩是你想升职加薪除了跳槽以外的必由之路,其重要性对个人发展来说不言而喻,对公司来说也是选拔人才的重要通道。本人不才就职的也不是什么大厂,职级之前也比较低,都没有资格参加述职答辩,这次是人生第一次,所以格外的重视,这也导致了一些本来不该发生的事,对述职答辩产生了很大的影响,后面我会详细的说明。


1.2 述职答辩的流程


答辩的流程主要有四个环节:答辩人候选、准备PPT、现场答辩、公布结果。




  • 答辩人候选:这是门槛,比如你的工作年限、工作能力、岗位职级、薪资范围,都达到这个门槛才有资格去答辩,这次答辩候选人让我有两点感触较深,一个是有些工作很多年的老同事没有资格答辩,另外一个是外包转正基本上也没有资格答辩;




  • 准备PPT:PPT在述职答辩中的重要性绝对是最重要的没有之一,PPT本身有两个非常重要的点,美观和内容,好看的PPT会给领导非常好的第一印象,自己没有能力做好PPT这时就不要在乎几十块钱了,直接网上买个好看的模板。内容就要靠你自己了,后面我会详细说内容的注意事项。




  • 答辩当天:答辩当天按事先排好的顺序依次进入会议室,等待过程是极其痛苦的,进入会议室后先让你陈述你的PPT,一般是你单独坐一排,对面坐了一排领导,公司高层领导、技术专家、外面请过来的专家,进入会议室面对一排大佬瞬间压迫感就上来了。陈述PPT的时间一般也就十分钟左右,陈述结束后就是专家、领导提问环节,一般也是十分钟左右。




  • 公布结果:就像科举放榜是喜是优,也就尘埃落定了。





二、关于PPT


2.1 美观


谁都喜欢好看的东西,我当时第一版PPT做好后,拿给了我的前领导帮看看,他直接说你这PPT做的太差了,给我提了几个很多好的建议,他是我工作以来对我帮助最大的领导,我刚到他下面他就给我涨薪,要不是他我感觉我坚持不到现在,疫情三年太难了,疫情后各种裁员根本没有涨薪机会,要不是他给我涨薪我肯坚持不到现在,在这里要深深的谢谢他。关于美观的PPT我有以下三个建议:




  • 做的PPT一定要让别人看:每个人审美不一样,你觉得好看的,别人不一定觉得好看,做好的PPT一定多找几个人看看,特别是你的领导或者搞设计的妹子们,领导看的PPT多,一眼就知道问题出在哪里,搞设计的妹子们往往审美还是好的,不像我们这种搞技术的汉子。




  • 一定要和别人对比:没有对比就没有伤害,我的第一版PPT让领导看后,他说你去看看谁谁的PPT做的很好,于是我去要了对比一下自己做的确实太差了。




2.2 内容


如果说美观是锦上添花那么内容才展示你真正实力的地方,答辩PPT内容都是有一定模板的,比如从个人履历、能力技能、工作成果、不足之外、未来规划等方面陈述,关于每一点我详细说一下:




  • 个人履历:学历和以前的所有工作经历都要写,可以简写但一定要有,否则你就是在掩饰。我们这次答辩很多人学历和专业没有写,后面HR要求所有人必须给写上,答辩时学校不是清华、北大、985这种耳熟能详的好学校,你可以不说学校名称,PPT上让领导看到就好,如果你的专业和你的工作相关的,一定要说一下你在读这个专业时做了和你现在工作相关的事,让领导觉得你是专业科班出生身的。有些工作很多年的老员工把很早以前的经历不写了,这点也是不好的,领导不知道你以前是干什么的,这让人有点不放心,是不是干过什么见不得光的事。




  • 能力技能:这个要对标你们的评定职级标准,公司评定职级都有评定标准,这个职级标准很有可能就是答辩委员制定的,所以在写这部分内容时,你就要对着这个职级标准来表明自己在各方面已经达到职级标准,就应该是这个级别的。在说技能的时候也可以说一些不是职级标准上的但是也能反映你的能力事,比如这次答辩我说了自己写文章超200+,阅读量超120W+,还有阅读量超10万+的文章,在最后大领导总结这次答辩优秀人的四个特征中最后一个是写作,我感觉效果还是不错的。




  • 工作成果:这部分可以说是整个PPT中最重要的没有之一,领导高高在上,你做的再多,他们也不知道是你做的,这部分就要让领导知道,原来我们公司这么重要的产品后面是你做出来的,这里有两点注意,一是所有成果都要展示,我当时觉得做的东西太多了,没有写,被我们领导说了,成果是你对公司的贡献,越多越好,让领导觉得你是个实干的人,这也是领导总结的优秀人才的第三个关键特征。二是在多的同时要有重点,不能只有数量没有质量,一定要重点说一到两个项目,从专业的角度分析,说说你在里面做了什么,给公司带来了哪些好处,一定要从上帝视角描述给领导听,不能说太多细节,因为领导也不懂细节技术,比如我们做开发的,就从整个项目的架构上来说。这次我知道有一个前端给领导印象就不好,他说优化前端Tab页加载,切换就花了3分钟的时间,领导会觉得一个tab页切换会对公司有多重要吗?领导要看到的是你给公司层面带来了什么价值,在优秀人才特征总结中,领导第一说的就是 格局,你是一颗螺丝钉你也要知道整个工厂是怎么运转的,如果你只做你的螺丝钉那么你也就是个工具人,容易被别人替代,未来也会被AI替代。




  • 不足之处:这部分一定要能自圆其说,并给出以后如何改进




  • 未来规划:这点是展示你是不是一支潜力股,规划一定要符合公司的大方向,视野要高,不过也不能假大空,根本实现不了的就不要写了,让领导觉得你是个实干的人。当时我准备写利用大模型解决我们多年以来未能解决的问题,然后想了想,万一领导真把这任务交给我了,搞不定怎么办?




三、关于陈述


3.1 陈述时间


陈述时间有严格要求的,领导一天要答辩那么多人,也想早点结束,所以要控制好你的时间,关于时间我有两个建议:




  • 答辩前多练习:最好把答辩10分钟所有内容都记下来,在答辩前做到无稿胜有稿,心中有稿,内容都记下来,多练习几次,用的时间也就固定下来的,我练习了不少于10次,早上睡不着,我起的很早,去楼下小公园散散步,缓解一下压力,就在公园里拿起手机练习一下,晚上睡觉时,脑中展示PPT,然后脑中播放每一张内容,心中练习一遍。




  • 答辩时放个手机计时: 有了手机计时,每一部分花了多少时间,自己心中有数,比如我这次最后一部分时间大概要花2分钟,在最后第二部分的时候我会看一下时间,如果离2分钟还多,我会把前面的内容再找一个展开说一下,如果时间不多了,就可以跳过几点,简单过一下,保证到最后一部分时候大概还剩2分钟,这样后面时间就好控制了。




3.2 语速


答辩语速一般都是相对较快的,太慢了会让领导听着急,不过也不能太快,有些老领导思路可能跟不上人的语速。好的办法是在答辩前,给你的直属领导演练一下,让他们给你听听效果怎么样,提提建议。这时你的直属领导是你最大的靠山,他们也会尽心尽力帮助你的。因为你表现非常好,他在大领导面前也是有脸面,表现太差了也会让大领导觉得他下面人不咋地。


3.3 工具


这次答辩我就吃了工具这个亏,关于工具我有以下几次建议:




  • 答辩用的什么格式你就用什么格式: 我一直都用MAC电脑的KeyNote,最后导出的成PPT,导出后发现一些字体没了,有些样式坏了,在上交PPT最后一分钟我还在改样式。




  • 答辩用的电脑是什么软件你就用什么软件:当我坐下时不知道提供是的什么软件播放PPT,HR说:请开始你的演讲,我居然找不到播放PPT按钮在哪里,最后还是HR提示我在最左上角有一个小按钮才让我播放起了PPT。




  • 最好提前去会议室演示一下:我们这次答辩会议室提供的投影仪无法显示PPT演讲者注释,有些没有背下来的人就吃了亏。




四、关于提问


4.1 评委


最中间的肯定是最大的领导,这次是公司最大的领导,两边往往是从外面请过来的专家,还有一些像公司的人力资源老大,最后一般还有你的分管大领导,这个人是非常重要的,如果你真的场面控制不住,他一般会帮你说话的,他的帮助对你也是非常重要的。如果你是老员工这里的大领导有可能还会认识几个,新员工肯定是不认识这些领导的,对面坐一排不认识的大佬压迫感还是很强的。如果有认识的领导那自然是轻松的,他是做什么的你都了解,那他提问的方向大致也就知道了。


4.2 提问


大领导的问题,一般不会考察你的技术,最大领导在最后述职总结中说到优秀人才的第二个特征是思路,他们是想考察你解决问题的思路。毕竟这些专家早已脱离一线工作多年,在架构、源码、算法方面我们这些干活的高P才是公司最强的,他们的优势的丰富的行业经验和管理经验、解决问题的方案论、整合资源的能力,所以他们的问题一般会从这些角度来问。不过如果是低P答辩,委员是一些一线实战的开发人员,那这个提问环节基本上就是技术面试环节了,八股文该准备还是要准备。专家问我的问题基本上就是一些能显示他确实是这方面的专家的问题,也就一些常规行业问题的解决方案,真正做过这块业务的都能答上来,关键是你要整理思路,让其它领导们也能听起来确实是这么回事,有一种不明觉厉的感觉,这就算是成功了。还有一些无非是工作、团队协作、管理方面有没有遇到问题,你是怎么解决之类的通用问题。回答这类问题有一个非常重要的点,当你说到有问题时,一定要给出解决方案,没有解决方案的问题就是抱怨,在领导面前抱怨这是大忌。在提问环节主要展现的是你的精神面貌、交流沟通能力、解决问题的思路,都不是什么难问题,最大的问题是有人会给你挖坑。


4.3 坑


注意坑才是提问环节最需要的。比如这次我就被掉到坑里了,这也是我这次述职中最失败的地方。有些人问的问题会有前后关联关系,而且和你前面同事问的问题还会相关。




我们先看这个坑是怎么掉进去的,问题一是"你们代码复用性如何?",我觉得这个问题不需要思考,肯定是高啊,然后举了多个项目复用同一个功能的例子来说明代码复用性确实高。问题二是"你和前面同事平时沟通交流怎么样?",这个问题我当时也是没有思考,肯定是非常好,还说了一些合作的项目,最后他说“那他怎么说你们代码复用性不高”,最后在大领导面前得出的结论就是你们团队管理有问题,回去要加强沟通。当时我感觉整个人瞬间石化了,没有思考这个问题,现在想想他就是在转移我的注意力,如果换个问法,”你们代码中有没有不可复用的地方吗?为什么?“我相信这个每个人都可以回答上来,把责任推给客户就好了,客户花钱提的个性化需要,不合理也要做,代码肯定不能复用。他这个问题是将你当时的注意力转移到你和同事之间沟通有问题,而不是让你去思考代码有没有不可复用的地方。后面我问了我的同事,问他的问题,也是将他带入到客户定制化需求比较多,代码不能复用,所以开发工期比较长的路子上,他如果回答代码复用性高,那他肯定会问为什么开发用了那么长的工期?这个领导太厉害了,每个问题都是环环相扣,还和其他人相关,防不胜防。现在想想还是当时被带进去了,直接回答代码整体上复用性高,有些客户提的个性化需求无法拒绝,导致部分局部代码不可复用不就好了?不过有一两个问题回答不上来也无所谓,最后是最大领导提问,他们问题就好回答了,“你的优化、改进是领导安排的还是自己主动去钻研的?”,这个就说重大业务相关的是领导安排的,有些技术上改进是自己钻研的,大领导最后和旁边领导说:”他是攻坚型的,以后有些攻坚任务也可以交给他,你要继续保持“。听到“继续保持”这四个字,我终于彻底释放了,没有什么比领导口中这四个字更重要了,最后感谢领导就结束答辩。


五、关于紧张


5.1 紧张


向我们这种整天和代码打交道,从来没有向大领导汇报过工作,突然来这么一次重要的答辩不紧张那都是万里挑一,我是尤其的紧张,从答辩前两天就开始紧张了,紧张到精神影响到身体,导致胃痛。



研究显示,长期的高压工作可能会引起胃肠道功能紊乱。平日我们也会注意到,有些人一紧张就会肚子不舒服知识分子,公众号:知识分子工作压力怎样伤害了你的肠胃?|一周科技



我是答辩前两个晚上基本上没怎么睡,而且整个人的神经特别敏感,晚上有一点小的动静,就可以明显的感觉到身体神经信号从耳朵传到胃部导致胃癌疼痛,医学上应该叫作“神经衰弱”,还好我之前就是这种问题,所以买了防噪音耳塞。



最大的问题是紧张会让你喉咙里有异物,想要呕吐,这对要答辩的人来说是致命的,然而越是担心这个问题,就会导致问题越严重。



当我们感到有压力时,整个神经系统会加速运转,帮助我们应对面临的任何挑战。大脑分泌肾上腺素和皮质醇(主要的应激激素),并通知自主神经系统加速呼吸、心率、血压和肌肉收缩。这意味着喉咙、胃和肠道可能会产生紧绷感甚至痉挛,导致我们感到喉咙里有异物,肠道或胃里绷得很紧,想要呕吐,或增加肠道痉挛,以至于接二连三想去上厕所。Melissa G. Hunt,公众号:加州健康研究院科学解释:为什么压力大会导致胃疼?



我真怕自己在答辩时呕吐。根本吃不下去东西,我只能买点馒头放工位,饿了吃一点。精神类的药物一般起作用都是非常慢的,这两天肯定是不能吃的,只能先治标,买了点缓解胃酸分泌过多的胃药,我也没时间去医院看,说实话,像我这种状况去医院还不先让你来个大套餐,什么胃镜、抽血化验统统按排上,而且我觉得看病的医生不一定有我懂的多,他们也就是按系统开发好的步骤来,现在大部分门诊医生都是医疗系统操作员,现在医院什么症状做什么检查,然后什么检查结果吃什么药,都在系统里。还不如自己给自己开药,直接去药店买好了。




我老婆和家人都不相信这是紧张导致的胃痛,我判断只要答辩结束所有症状都没了,那就是紧张导致的。当我在门外等着进去的时候,真的感觉自己要站不住了,这时我给自己肩膀按按摩放松肌肉,闭上双眼,深呼吸,放空思想转移一下注意力,感觉好了点,当我进去坐下来时,居然找不到PPT播放按钮时,就更紧张了,不过等到讲PPT时反而好点了,当提问环节和领导对话几次所有症状就全部消失了,因为这时你的注意力在别人身上,已经不再注意你的身体问题了,自然也就好了。答辩结束后我的直属领导看到我时就说,侃大山的赵侠客又回来了。昨天还吃不下去饭,结束后大鱼大肉立马吃了起来,补补这两天的损失。


六、总结


打工人真不容易,这次答辩让我有以下几点总结:


1.有一个好领导很重要,我工作这么多年遇到的几个领导对我都很好,特别是上个领导,在这里要再次真诚的感谢 他, 如果他能读到本文,真诚的谢谢您!


2.在公司尽量和每个领导都保持好关系,说不定他就是你的答辩委员 


3.打铁还需自身硬,不断提升自己才能让你有更多机会 


4.锻炼好身体,身体是一切的前提,没有好的身体一切都没有意义 


5.工作久了能不跳槽还是不要跳,新环境没人能帮你


6.除了工作技能之外,自己一定还要有点其它的长处,比如写作、写专利、写论文 


7.不要接受外包,除非你工作是为了生存或者为了体验生活


作者:赵侠客
来源:juejin.cn/post/7271283075170287652
收起阅读 »

Token到底是什么?!

web
随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web Token(JWT)的解决方案。 JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这种信息可以...
继续阅读 »

随着Web应用的发展,为了保证API通信的安全性,很多项目在进行设计时会采用JSON Web TokenJWT)的解决方案。


JWT是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。这种信息可以被验证和信任,因为它是数字签名的。


那么JWT中的Token到底是什么?接下来,我们将以登录功能为例进行Token的分析。


登录流程


很多小伙伴对登录的流程已经很熟悉了,我们来看一个最基本的后台系统的登录流程


登录流程图.png


流程图很清楚了,接下来我们使用 V2Koa 实现一个登录过程,来看看Token到底是什么


Vue2 + Koa 实现登录


前端代码


1. 前端点击事件


数据的校验就忽略掉,感兴趣的同学可自行书写或者找我要源码,直接看点击事件


handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
// 这里使用了VueX
this.$store
.dispatch("user/login", this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || "/" });
this.loading = false;
})
.catch(() => {
this.loading = false;
});
} else {
return false;
}
});
}

2. Vuex中的action


校验通过后触发VueXUser模块的Login方法:


async login(context, userInfo) {
const users = {
username: userInfo.mobile,
password: userInfo.password
}
const token = await login(users)
// 在这里大家可以对返回的数据进行更详细的逻辑处理
context.commit('SET_TOKEN', token)
setToken(token)
}

3. 封装的接口


export function login(data) {
return request({
url: '/login',
method: 'post',
data
})
}

以上三步,是我们从前端向后端发送了请求并携带着用户名和密码,接下来,我们来看看Koa中是如何处理前端的请求的


Koa 处理请求


首先介绍一下Koa



Koa 基于Node.js平台,由 Express 幕后的原班人马打造,是一款新的服务端 web 框架



Koa的使用极其简单,感兴趣的小伙伴可以参考官方文档尝试用一下


Koa官网:koa.bootcss.com/index.html#…


1. 技术说明


在当前案例的koa中,使用到了jsonwebtoken的依赖包帮助我们去加密生成和解密Token


2. 接口处理


const { login } = require("../app/controller/user")
const jwt = require("jsonwebtoken")
const SECRET = 'test_';
router.post('/login', async (ctx, next) => {
const { username, password } = ctx.request.body
// 这里是调用Controller中的login方法来跟数据库中的数据作对比,可忽略
const userList = await login(username, password)

if (!userList) {
// 这里的errorModel是自己封装的处理错误的模块
ctx.body = new errorModel('用户名或密码错误', '1001')
return
}

// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓ ※ 重点看这里 ※ ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
const token = jwt.sign({ userList }, SECRET, { expiresIn: "1h" })

ctx.body = {
success: true,
state: 200,
message: 'login success',
data: token
};
return;
})

关于 JWT


上面的重点代码大家看到了,接下来具体给大家解释下JWT



Jwt由三部分组成:headerpayloadsignature



export interface Jwt {
header: JwtHeader;
payload: JwtPayload | string;
signature: string;
}

header头部


里面的包含的内容有很多,比如用于指定加密算法的alg、指定加密类型的typ,全部参数如下所示:


export interface JwtHeader {
alg: string | Algorithm;
typ?: string | undefined;
cty?: string | undefined;
crit?: Array<string | Exclude<keyof JwtHeader, 'crit'>> | undefined;
kid?: string | undefined;
jku?: string | undefined;
x5u?: string | string[] | undefined;
'x5t#S256'?: string | undefined;
x5t?: string | undefined;
x5c?: string | string[] | undefined;
}

payload负载


payload使我们存放信息的地方,里面包含了签发者过期时间签发时间等信息


export interface JwtPayload {
[key: string]: any;
iss?: string | undefined;
sub?: string | undefined;
aud?: string | string[] | undefined;
exp?: number | undefined;
nbf?: number | undefined;
iat?: number | undefined;
jti?: string | undefined;
}

signature签名


signature 需要使用编码后的 headerpayload以及我们提供的一个密钥(SECRET),然后使用 header 中指定的签名算法进行签名


关于 jwt.sign()


jwt.sign()方法,需要三个基本参数和一个可选参数:payloadsecretOrPrivateKeyoptions和一个callback


export function sign(
payload: string | Buffer | object,
secretOrPrivateKey: Secret,
options: SignOptions,
callback: SignCallback,
): void;

payload是我们需要加密的一些信息,这个参数对应上面koa代码中的{ userList },而userList则是我从数据库中查询得到的数据结果


secretOrPrivateKey则是我们自己定义的秘钥,用来后续验证Token时所用


options选项中有很多内容,例如加密算法algorithm、有效期expiresIn等等


export interface SignOptions {
/**
* Signature algorithm. Could be one of these values :
* - HS256: HMAC using SHA-256 hash algorithm (default)
* - HS384: HMAC using SHA-384 hash algorithm
* - HS512: HMAC using SHA-512 hash algorithm
* - RS256: RSASSA using SHA-256 hash algorithm
* - RS384: RSASSA using SHA-384 hash algorithm
* - RS512: RSASSA using SHA-512 hash algorithm
* - ES256: ECDSA using P-256 curve and SHA-256 hash algorithm
* - ES384: ECDSA using P-384 curve and SHA-384 hash algorithm
* - ES512: ECDSA using P-521 curve and SHA-512 hash algorithm
* - none: No digital signature or MAC value included
*/

algorithm?: Algorithm | undefined;
keyid?: string | undefined;
/** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
expiresIn?: string | number | undefined;
/** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */
notBefore?: string | number | undefined;
audience?: string | string[] | undefined;
subject?: string | undefined;
issuer?: string | undefined;
jwtid?: string | undefined;
mutatePayload?: boolean | undefined;
noTimestamp?: boolean | undefined;
header?: JwtHeader | undefined;
encoding?: string | undefined;
allowInsecureKeySizes?: boolean | undefined;
allowInvalidAsymmetricKeyTypes?: boolean | undefined;
}

callback则是一个回调函数,有两个参数,默认返回Token


export type SignCallback = (
error: Error | null,
encoded: string | undefined,
) =>
void;

通过以上方法加密之后的结果就是一个Token


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s


总结


在整个的Koa中,用到了jsonwebtoken这个依赖包,里面有sign()方法


而我们前端所得到的数据通过sign()加密出来的包含自定义秘钥的一份用户信息而已


至于用户信息中有什么内容,可以随便处理,比如用户的ID、用户名、昵称、头像等等


那么这个Token后续有什么用呢?


后续我们可以在前端的拦截器中配置这个Token,让每一次的请求都携带这个Token,因为Koa后续需要对每一次请求进行Token的验证


比如登录成功后请求用户的信息,获取动态路由,再通过前端的router.addRoutes()将动态路由添加到路由对象中去即可


作者:半截短袖
来源:juejin.cn/post/7275211391102189628
收起阅读 »

移动端的「基金地图」是怎么做的?

web
🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师芒僧,今年 8 月份开始到 9 月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具,本文具体介绍了它是如何实现的...
继续阅读 »

🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师芒僧,今年 8 月份开始到 9 月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具,本文具体介绍了它是如何实现的。



Kapture 2022-10-19 at 14.12.19.gif


这次在 「支付宝 - 基金」里的【指数专区改版】需求,我们玩了一种很新的东西 🌝


8月份开始到9月底,「支付宝 - 基金」里面的指数专区进行了一波大改版升级,这次借助 F2 4.0 强大的移动端手势交互能力,我们尝试了一种多维度探索式选基决策工具(如上动图所示)。


简单来说,用户可以在一个散点图上根据 「收益」和「波动」 这两个维度全览对比整个市场里的指数基金,并选出适合自己的指数基金进行投资,这个功能我们愿称其为 「指数图谱」 🐶 。



图谱是这个业务场景上的叫法,实际上图谱应该是关系图而非统计图.



image.pngimage.pngimage.png


功能已发布,页面访问路线如上


先看看有哪些功能点



  1. 精细打磨的移动端手势交互,平移、缩放、横扫不在话下 :


Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.00.49.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.01.38.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.43.17.gif


依次为:缩放、平移、横扫



  1. 底部产品卡和图表的联动交互:


Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.54.40.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.54.40.gif


依次为:点击图表上的气泡、滑动底部卡片



  1. 无惧数据点太多看不到细节,我们有自适应的气泡抽样展示和自动聚焦:


Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 19.58.03.gif Simulator Screen Recording - iPhone 14 Pro Max - 2022-10-17 at 20.03.46.gif


依次为:抽样优化前、抽样优化后


那么,怎么做的呢?


最开始看到这个需求的时候,当时觉得可行性比较低。因为需求里面针对图谱的方案以及细节都特别模糊;不敢承诺各种功能和排期,所以先做了一轮比较完整的系分,增加一些说话的底气🫣


📱 第一步:同类产品调研


因为设计同学的灵感来自于 大众点评APP 上面的「美食地图」,所以第一步就是做了一次「同类产品调研」,仔细去看了一下 「美食地图」上究竟有哪些花样,有哪些体验优化的小细节,不看不知道,一看发现细节原来这么多啊 🤕:


图表和卡片的交互联动点抽样展示列表视图和卡片视图可切换交互时卡片自动折叠散点懒加载上滑直接唤起详情页
21.gif22.gif1658280205580-84108c85-793b-4318-89af-7504f3517613.gif24.gif25.gif1658280765505-5eb8bb05-30c5-45a0-bd81-9f494267b843.gif

做完这一步之后,大概能够知道自己距离“成品”有多远的距离,方便自己评估工期;另外还可以在系分评审的时候把这些细节提出来,防止临近发布了突然发现某个交互逻辑有个致命的漏洞(别问我怎么知道的,要命的)。
这波调研之后,最终我们在实现上致敬了「美食地图」50% 的体验细节优化 (狗头)。


⚙️ 第二步:功能点分析


第二步就是从需求本身的角度做功能点的分析,这样可以方便我们拆分组件,为后续做分层设计打下基础,明白哪些是需要支持可扩展的。这一步大家都熟悉,就不赘述了:
image.png


📦 第三步:通用化设计


有了功能点的分析之后,就可以进行通用化的设计了,这就来到了喜闻乐见的沉淀组件的设计环节 🌝


我们希望这个功能不仅仅是纯业务代码**,期望下次能够复用大部分核心功能 **(理想很丰满),所以在系分的时候是往通用化的方向去设计的,这里主要做了三件事情:分层设计概念标准化核心流程定义



  1. 分层设计


拆的逻辑是按最基础的 M(数据层) - C(控制层) - V(视图层) 拆分的。


image.png


有了分层设计和功能点分析之后,就可以知道哪些应该放到组件内,哪些接口应该被抽象成通用接口,哪些应该保留扩展性供使用者自己来定义,就可以画个表格了,一一决定哪些模块应该放到组件内:
image.png



  1. 概念标准化


下面来到造词环节,把一些常用的概念都定义成一个个名字,这样方便和后端、设计协同的时候效率更高,同时也方便自己定义清楚各个模型(类)。(这里其实取名越贴切越形象越好,有点考验语言能力了属实是)
image.png



  1. 核心流程定义


这一步是脑补环节,在脑子里跑一遍整体的流程,也是整个需求最核心的流程,比如这里会分成四种流程:初始化流程 、散点图交互流程、底部卡片交互流程、顶部tab交互流程


进而可以将四种流程里面的各节点做一些归类,比如都会有图表渲染、数据补全、卡片渲染这些共同的节点,而这些节点就可以实现成具体模型里的具体方法。


image.png


🌝 第四步:难点分析


根据上面拆分的各模块,列出哪些点是实现有困难的,耗时长的。这样就可以在评估工期的时候多 Battle 一下,还能砍砍需求,更可以让底层引擎/SDK来突破这些难点(比如找 F2 的核心开发者) :


image.png
image.png


📃 最后一步:


按照上述的设计进行代码编写。


难点实现


1. 移动端的图表手势交互体验优化


开发之初,F2 只支持单轴(x或者y)的平移缩放,也不支持全方向交互;在 swipe 上的体验也不太好(阻尼感很强),所以在项目开发过程中, F2 完成了很多体验优化,打磨出很多细致入微的良好体验:



  • X轴、Y轴可同时开启平移、缩放

  • swiper 体验效果优化

  • 移出可视区之后的蒙层遮挡能力(view-clip)

  • zIndex 元素层叠渲染

  • 平移缩放性能优化


2. 气泡抽样展示优化


因为散点图上的点在初始化的缩放比例下分布非常密集,所以如果每个点上面都绘制一个气泡的话,就会显得密密麻麻的,根本无从下手(如下图1所示)。针对这样的问题,做了「气泡抽样展示」的优化。


image.png


实现方式上就是渲染前遍历所有的点,如果在这个点周围某个半径距离之内有其他点,那么就认为这个点是脏点(dirty point),最后筛选出所有“干净”的点进行气泡展示。


如下图图1所示,灰色点(右上角)是干净点,而灰白色的点(偏中间的位置)因为其在圆圈半径范围之内有其他点存在,所以这个点是脏点。


image.png



多提一句,这样的过滤方式会使得密集区域的点都不会展示气泡,后续会进行优化。



3. 获取到可视区内的所有点


image.png
由于做了气泡抽样展示,所以上图中的底部卡片只会展示用户可视区内散点图上有气泡的点(细心的盆友可以发现,散点图上有两种点,一种是带气泡的交互点,一种是不带气泡的缩略点)。那么就需要一个获取「可视区内所有的点」,实现思路如下:


- 监听 PanEnd(平移结束)、PinchEnd(缩放结束), SwipeEnd(横扫结束)的事件
- 获取到平移/缩放/横扫之后最新的 scales
- 根据最新的 scales 里面的 x、y 的 range 过滤一遍图表原数据
- 将脏点从上一步的结果过滤出去
- 底部卡片根据上一步的结果进行渲染展示
- 结束



// 根据当前的缩放比例,拿到「可视区」范围内的数据
function getRecordsByZoomScales(scales, data) {
const { x: xScale, y: yScale } = scales;
const { field: xField, min: xMin, max: xMax } = xScale;
const { field: yField, min: yMin, max: yMax } = yScale;

return data.filter((record) => {
const isInView =
record[xField] >= xMin &&
record[xField] <= xMax &&
record[yField] >= yMin &&
record[yField] <= yMax;

return isInView;
});
}


// 使用时
export default props => {
// 图表原数据
const { data } = props;

function handlePanEnd (scales, data) {
// 手动高亮下面这一行
getRecordsByZoomScales(scales, data);
}

return (
<ReactCanvas>
<Chart>
{/* ... */}
<ScrollBar onPanEnd={handlePanEnd}/>
</Chart>
</ReactCanvas>

)

}

4. 数据懒加载


image.pngimage.png
底部卡片的数量是由散点图上点的数量决定的,而每张卡上都有不少的数据量(基金产品信息、指数信息、标签信息),所以不能一次性就把所有点里关联的数据都查询出来(会导致接口返回数据过多)。


这里采取的是懒加载的方式 ,每次只在交互后查询相邻 N+2/N-2 张的卡片数据,并且增加了一份内存缓存来存储已经查询过的卡片数据:


image.png


基本的流程图如下:


- 触发散点图交互/滑动底部卡片
- 读取缓存,过滤出没有缓存过的卡片
- 发起数据调用,获取到卡片的数据
- 写入缓存
- 更新卡片数据,返回
- 更新卡片视图,渲染完成

实际线上效果


项目上线之后,我们发现散点图区域的交互率(包含平移,缩放)非常高,可以看出用户对新类型的选基工具抱有新鲜感,也乐于去进行探索;也有部分用户能够通过工具完成决策或者进行产品之间的详细对比(即点击底部卡片上的详情按钮),起到了一个工具类产品的作用 🌝 。


致谢


感谢 AntV 以及 F2 对移动端图表交互能力的支持。


作者:支付宝体验科技
来源:juejin.cn/post/7176891015112949819
收起阅读 »

Vue3为什么推荐使用ref而不是reactive

web
为什么推荐使用ref而不是reactive reactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代option api的data的替代,可以存放任何数据类型,而reactive声明的数据类...
继续阅读 »

为什么推荐使用ref而不是reactive



reactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代option apidata的替代,可以存放任何数据类型,而reactive声明的数据类型只能是对象;



先抛出结论,再详细说原因:非必要不用reactive! (官方文档也有对应的推荐)


官方原文:建议使用 ref() 作为声明响应式状态的主要 API。


最懂Vue的人都这么说了:推荐ref!!!!!!


image.png


reactiveref 对比


reactiveref
❌只支持对象和数组(引用数据类型)✅支持基本数据类型+引用数据类型
✅在 <script><template> 中无差别使用❌在 <script><template> 使用方式不同(script中要.value)
❌重新分配一个新对象会丢失响应性✅重新分配一个新对象不会失去响应
能直接访问属性需要使用 .value 访问属性
❌将对象传入函数时,失去响应✅传入函数时,不会失去响应
❌解构时会丢失响应性,需使用toRefs❌解构对象时会丢失响应性,需使用toRefs


  • ref 用于将基本类型的数据(如字符串、数字,布尔值等)和引用数据类型(对象) 转换为响应式数据。使用 ref 定义的数据可以通过 .value 属性访问和修改。

  • reactive 用于将对象转换为响应式数据,包括复杂的嵌套对象和数组。使用 reactive 定义的数据可以直接访问和修改属性。


原因1:reactive有限的值类型


reactive只能声明引用数据类型(对象)


let  obj = reactive({
  name: '小明',
  age : 18
})

ref既能声明基本数据类型,也能声明对象和数组;



Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref



//对象
const state = ref({})
//数组
const state2 = ref([])

原因2:reactive使用不当会失去响应:



reactive一时爽,使用不恰当的时候失去响应泪两行,开开心心敲代码过程中,会感叹!!咦?怎么不行?为什么这么赋值失去响应了? 辣鸡reactive!!! 我要用 ref 👉👉yyds



1. 给reactive赋一整个普通对象/reactive对象


通常在页面数据回显时,需要将AJAX请求获取的对象直接赋值给响应式对象,如果操作不当就导致reactive声明的对象失去响应





  • 赋值一个普通对象


    let state = reactive({ count: 0 })
    //这个赋值将导致state失去响应
    state = {count: 1}



  • 赋值一个reactive对象



    如果给reactive的响应式对象赋值普通对象会失去响应,那么给它赋值一个reactive的响应式对象不就行了吗?下面试试看





<template>
{{state}}
</template>    

<stcirpt setup>
const state = reactive({ count: 0 })
//nextTick异步方法中修改state的值
nextTick(() => {
//并不会触发修改DOM ,说明失去响应了
state = reactive({ count: 11 });
});
</stcirpt>

nexTick中给state赋值一个reactive的响应式对象,但是DOM并没有更新!


解决方法:



  1. 不要直接整个对象替换,对象属性一个个赋值


    let state = reactive({ count: 0 })
    //state={count:1}
    state.conut = 1



  2. 使用Object.assign


    let state = reactive({ count: 0 })
    // state = {count:1}   state失去响应
    state = Object.assign(state , {count:1})



  3. 使用ref定义对象



    非必要不用reactive



    let state = ref({ count: 0 })
    state.value={count:1}



为什么同样是赋值对象ref不会失去响应而reactive会?

ref 定义的数据(包括对象)时,返回的对象是一个包装过的简单值,而不是原始值的引用;



就和对象深拷贝一样,是将对象属性值的赋值



reactive定义数据(必须是对象),reactive返回的对象是对原始对象的引用,而不是简单值的包装。



类似对象的浅拷贝,是保存对象的栈地址,无论值怎么变还是指向原来的对象的堆地址;


reactive就算赋值一个新的对象,reactive还是指向原来对象堆地址



2.将reactive对象的属性-赋值给变量(断开连接/深拷贝)


这种类似深拷贝不共享同一内存地址了,只是字面量的赋值;对该变量赋值也不会影响原来对象的属性值



let state = reactive({ count: 0 })
//赋值
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
console.log(state.count) //0

有人就说了,既然赋值对象的属性,那我赋值一整个对象不就是浅拷贝了吗?那不就是上面说的给响应式对象的字面量赋一整个普通对象/reactive对象这种情况吗?这种是会失去响应的


3.直接reactive对象解构时


  • 直接解构会失去响应


let state = reactive({ count: 0 })
//普通解构count 和 state.count 失去了响应性连接
let { count } = state
count++ // state.count值依旧是0

解决方案:



  • 使用toRefs解构不会失去响应



    使用toRefs解构后的属性是ref的响应式数据





const state = reactive({ count: 0 })
//使用toRefs解构,后的属性为ref的响应式变量
let { count } = toRefs(state)
count.value++ // state.count值改变为1

建议: ref一把梭



当使用reactive时,如果不了解reactive失去响应的情况,那么使用reactive会造成很多困扰!



推荐使用ref总结原因如下:




  1. reactive有限的值类型:只能声明引用数据类型(对象/数组)




  2. reactive在一些情况下会失去响应,这个情况会导致数据回显失去响应(数据改了,dom没更新)


    给响应式对象的字面量赋一整个普通对象,将会导致reactive声明的响应式数据失去响应


    <template>
      {{state.a}}
      {{state.b}}
      {{state.c}}
    </template>

    <script>
    let state = reactive({ a:1,b:2,c:3 })
    onMounted(()=>{
        //通AJAX请求获取的数据,回显到reactive,如果处理不好将导致变量失去响应,
       //回显失败,给响应式数据赋值一个普通对象
       state = { a:11,b:22,c:333 }
      //回显成功,一个个属性赋值  
       state.a = 11
       state.b = 22
       state.c = 33
    })
    </script>

    上面这个例子如果是使用ref进行声明,直接赋值即可,不需要将属性拆分一个个赋值


    使用ref替代reactive:


    <template>
      {{state.a}}
      {{state.b}}
      {{state.c}}
    </template>

    <script>
    let state = ref({ a:1,b:2,c:3 })
    onMounted(()=>{
       //回显成功
       state.value = { a:11,b:22,c:333 }
    })
    </script>



  3. ref适用范围更大,声明的数据类型.基本数据类型和引用数据类型都行




虽然使用ref声明的变量,在读取和修改时都需要加.value小尾巴,但是正因为是这个小尾巴,我们review代码的时候就很清楚知道这是一个ref声明的响应式数据;


ref的.value小尾巴好麻烦!


ref声明的响应式变量携带迷人的.value小尾巴,让我们一眼就能确定它是一个响应式变量!虽然使用ref声明的变量,在读取和修改时都需要加.value小尾巴,但是正因为是这个小尾巴,我们review代码的时候就很清楚知道这是一个ref声明的响应式数据;


可能有些人不喜欢这个迷人小尾巴,如果我能自动补全阁下又如何应对?


volar插件能自动补全.value (强烈推荐!!!!!!!)



本人推荐ref一把梭,但是ref又得到处.value ,那就交给插件来完成吧!!!





  • valor 自动补全.value (不是默认开启,需要手动开启)




  • 不会有人不知道Vue3需要不能使用vetur要用valor替代吧?不会不会吧? (必备volar插件)




volar设置自动填充value.gif
可以看到当输入ref声明的响应式变量时,volar插件自动填充.value 那还有啥烦恼呢? 方便!


本文会根据各位的提问和留言持续更新;


@ 别骂了_我真的不懂vue 说(总结挺好的,因此摘抄了):



reactive 重新赋值丢失响应是因为引用地址变了,被proxy代理的对象已经不是原来那个所以丢失响应了,其实ref也是一样的,当把.value那一层替换成另外一个有着.value的对象也会丢失响应 ref定义的属性等价于reactive({value:xxx})

另外说使用Object.assign为什么可以更新模板

Object.assign解释是这样的: 如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。

那个解决方法里不用重新赋值,直接Object.assign(state,{count:1})即可,所以只要proxy代理的引用地址没变,就会一直存在响应性



作者:我要充满正能量
来源:juejin.cn/post/7270519061208154112
收起阅读 »

Android使用无障碍模式跳过应用广告的实现(仿李跳跳功能)

1.前言 当代移动应用广告的过度侵扰问题已经引起了广大用户的关注和不满。而芒果TV平台运营中心的副总经理陈超推出了一项名为"摇一摇开屏广告"的新策略↓ 引发了更多对于用户体验的担忧下↓ 在这种策略下,用户在不经意间被强制打开广告,这对用户来说无疑是一种糟糕...
继续阅读 »

1.前言


当代移动应用广告的过度侵扰问题已经引起了广大用户的关注和不满。而芒果TV平台运营中心的副总经理陈超推出了一项名为"摇一摇开屏广告"的新策略↓


ezgif.com-resize.gif


引发了更多对于用户体验的担忧下↓


image.png


在这种策略下,用户在不经意间被强制打开广告,这对用户来说无疑是一种糟糕的体验。当人处于运动的状态下,打开某些APP。


而“李跳跳”APP通过利用Android的无障碍模式,"李跳跳"成功帮助用户自动跳过这些令人困扰的开屏广告,从而有效地减轻了用户的不便。随之而来的不正当竞争指控引发了对于这类应用的法律和道德讨论。


我决定仿“李跳跳”写一个广告跳过助手,以呼吁对于这种过度侵扰性广告的关注,同时也为广大Android开发者们分享运用的技术原理。


2.效果图


ezgif-2-147d9e39be.gif


3.无障碍模式


当我们深入探讨"李跳跳"及其仿制应用的功能实现时,了解Android的无障碍模式和AccessibilityService以及onAccessibilityEvent函数的详细内容至关重要。这些技术是这些应用背后的核心,让我们更深入地了解它们:


3.1Android的无障碍模式


无障碍模式是Android操作系统的一个功能,旨在提高设备的可用性和可访问性,特别是为了帮助那些有视觉、听觉或运动障碍的用户。通过无障碍模式,应用可以获取有关用户界面和用户操作的信息,以便在需要时提供更好的支持。


3.2 onServiceConnected函数


这是AccessibilityService的回调函数之一,当服务被绑定到系统时会被调用。在这个函数中,可以进行初始化操作,如设置服务的配置、注册事件监听等。


@Override
public void onServiceConnected() {
// 在这里进行服务的初始化操作
// 注册需要监听的事件类型
}

3.3 onAccessibilityEvent函数


这是AccessibilityService的核心函数,用于处理发生的可访问性事件。在这个函数中,可以检查事件类型、获取事件源信息以及采取相应的操作。
本次功能主要用到的就是这个函数


@Override 
public void onAccessibilityEvent(AccessibilityEvent event) {
// 处理可访问性事件
// 获取事件类型、源信息,执行相应操作
}

3.4 onInterrupt函数


这个函数在服务被中断时会被调用,例如,用户关闭了无障碍服务或系统资源不足。可以在这里进行一些清理工作或记录日志以跟踪服务的中断情况。


@Override
public void onInterrupt() {
// 服务中断时执行清理或记录日志操作
}

3.5 onUnbind函数


当服务被解绑时,这个函数会被调用。可以在这里进行资源的释放和清理工作。


@Override
public boolean onUnbind(Intent intent) {
// 解绑时执行资源释放和清理操作
return super.onUnbind(intent);
}

3.6 onKeyEvent函数(未用到)


这个函数用于处理键盘事件。通过监听键盘事件,可以实现自定义的按键处理逻辑。例如,可以捕获特定按键的按下和释放事件,并执行相应操作。


@Override
public boolean onKeyEvent(KeyEvent event) {
// 处理键盘事件,执行自定义逻辑
return super.onKeyEvent(event);
}


3.7 onGesture函数(未用到)


onGesture()函数允许处理手势事件。这些事件可以包括触摸屏幕上的手势,例如滑动、缩放、旋转等。通过监听手势事件,可以实现各种手势相关的应用功能。


@Override
public boolean onGesture(int gestureId) {
// 处理手势事件,执行自定义逻辑
return super.onGesture(gestureId);
}


4.功能实现


4.1无障碍服务的启用和注册



  • 创建AccessibilityService的类。


public class AdSkipService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {

}

@Override
public void onInterrupt() {

}

@Override
public boolean onUnbind(Intent intent) {
return super.onUnbind(intent);
}
}


  • 在AndroidManifest.xml文件中声明AccessibilityService。


<service android:name=".service.AdSkipService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:enabled="true"
android:exported="true">

<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />

</service>

4.2 onAccessibilityEvent函数的实现



  • 在onAccessibilityEvent函数中获取当前界面的控件,并在异步遍历所有子控件


@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// 获取当前界面的控件
AccessibilityNodeInfo nodeInfo = event.getSource();

taskExecutorService.execute(new Runnable() {
@Override
public void run() {
//遍历节点函数,查找所有控件
iterateNodesToSkipAd(nodeInfo);
}
});
}



  • 判断控件的文本是否带有“跳过”二字


/**
* 判断节点内容是否是关键字(默认为”跳过“二字 )
* @param node 节点
* @param keyWords 关键字
* @return 是否包含
* */

public static boolean isKeywords(AccessibilityNodeInfo node, String keyWords){
CharSequence text = node.getText();
if (TextUtils.isEmpty(text)) {
return false;
}
//查询是否包含"跳过"二字
return text.toString().contains(keyWords);
}


  • 触发控件的点击事件


/**
* 点击跳过按钮
* @param node 节点
* @return 是否点击成功
* */

private boolean clickSkipNode(AccessibilityNodeInfo node){
//尝试点击
boolean clicked = node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
//打印点击按钮的结果
LogUtil.e("clicked result = " + clicked);
return clicked;
}

注:本篇章为了读者方便理解,对代码进行了简化,删去了繁琐的逻辑判断。具体实现详见源码


5.结语


我们通过AccessibilityService和无障碍模式,提供了一种改善用户体验的方法,帮助用户摆脱令人不快的广告干扰。通过了解如何开发这样的应用,我们可以更好地理解无障碍技术的潜力,并在保护用户权益的前提下改善应用环境。


如果对你有所帮助,请记得帮我点一个赞和star,有什么意见和建议可以在评论区给我留言


源码地址:github.com/Giftedcat/A…


作者:GiftedCat
来源:juejin.cn/post/7275009721760481320
收起阅读 »

H5快速上手鸿蒙元服务(前端)

web
一、前言 鸿蒙元服务虽然与h5在很多地方虽然有相似之处,但还是有部分不同的地方,鸿蒙服务开发模式更接近与vue2版本,很多写法与其相似。该篇文章主要用于帮助有h5基础的伙伴能够快速上手鸿蒙元服务,并且对个人在开发过程中遇到的一些坑做个总结。 二、开发相关 项目...
继续阅读 »

一、前言


鸿蒙元服务虽然与h5在很多地方虽然有相似之处,但还是有部分不同的地方,鸿蒙服务开发模式更接近与vue2版本,很多写法与其相似。该篇文章主要用于帮助有h5基础的伙伴能够快速上手鸿蒙元服务,并且对个人在开发过程中遇到的一些坑做个总结。


二、开发相关


项目目录


a51bd7a8cf9c80848926b24be2b8a27.jpg


cd3136d4b9ca22519a4934b79db8d4e.jpg
前端部分主要看js目录下的文件目录即可,除default目录外,其他文件都是与服务卡片相关的。


commom:存放公共配置文件方法等

components:存放公共组件
i18n:i18n相关

media:存放静态文件,图片等

pages:存放页面的目录,包括js,hml,css

utils:存放工具方法,比如网络请求封装等

app.js:全局文件,能够在这个文件中定义全局变量,拥有应用级的生命周期函数


其他关键目录:


supervisual:低代码相关

config.json:项目配置相关,包括路由等


config.json文件


用于给整个项目进行一些关键配置


定义路由


image.png
这种定义路由的方式,可能开发过微信小程序的伙伴会比较熟悉,在微信小程序中,一般第一个路径即是项目打开的页面,可惜在鸿蒙元服务中没有这个便捷的功能,designWidth用于定义页面以多宽的设计图来绘制,autoDesginWidth设为true,即是系统根据手机自动设置。


config.json详细配置请看官方文档: developer.harmonyos.com/cn/docs/doc…


HML


HML是一套类HTML的标记语言,通过组件,事件构建出页面的内容。页面具备数据绑定、事件绑定、列表渲染、条件渲染和逻辑控制等高级能力,由鸿蒙内部实现。


<!-- xxx.hml -->
<div class="container">
<text class="title">{{count}}</text>
<div class="box">
<input type="button" class="btn" value="increase" onclick="increase" />
<input type="button" class="btn" value="decrease" @click="decrease" />
<!-- 传递额外参数 -->
<input type="button" class="btn" value="double" @click="multiply(2)" />
<input type="button" class="btn" value="decuple" @click="multiply(10)" />
<input type="button" class="btn" value="square" @click="multiply(count)" />
</div>
</div>

// xxx.js
export default {
data: {
count: 0
},
increase() {
this.count++;
},
decrease() {
this.count--;
},
multiply(multiplier) {
this.count = multiplier * this.count;
}
};
/* xxx.css */
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
left: 0px;
top: 0px;
width: 454px;
height: 454px;
}
.title {
font-size: 30px;
text-align: center;
width: 200px;
height: 100px;
}
.box {
width: 454px;
height: 200px;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.btn {
width: 200px;
border-radius: 0;
margin-top: 10px;
margin-left: 10px;
}

看这段代码是不是就觉得很亲近了,在hml中通过“{{}}”的形式绑定数据,用@和on的方法来绑定事件,同时支持冒泡、捕获等方式。


列表渲染for


<!-- xxx.hml -->
<div class="array-container" style="flex-direction: column;margin: 200px;">
<!-- div列表渲染 -->
<!-- 默认$item代表数组中的元素, $idx代表数组中的元素索引 -->
<div for="{{array}}" tid="id" onclick="changeText">
<text>{{$idx}}.{{$item.name}}</text>
</div>
<!-- 自定义元素变量名称 -->
<div for="{{value in array}}" tid="id" onclick="changeText">
<text>{{$idx}}.{{value.name}}</text>
</div>
<!-- 自定义元素变量、索引名称 -->
<div for="{{(index, value) in array}}" tid="id" onclick="changeText">
<text>{{index}}.{{value.name}}</text>
</div>
</div>


tid等于vue中的key,id即为array每一项中的唯一属性,需要注意的是,与vue不同,在鸿蒙元服务中,tid是必须的,如果没有tid可能会引起运行异常的情况。


条件渲染if和show


<!-- xxx.hml -->
//if
<div class="container">
<button class="btn" type="capsule" value="toggleShow" onclick="toggleShow"></button>
<button class="btn" type="capsule" value="toggleDisplay" onclick="toggleDisplay"></button>
<text if="{{visible}}"> Hello-world1 </text>
<text elif="{{display}}"> Hello-world2 </text>
<text else> Hello-World </text>
</div>


//show
<!-- xxx.hml -->
<div class="container">
<button class="btn" type="capsule" value="toggle" onclick="toggle"></button>
<text show="{{visible}}" > Hello World </text>
</div>


if和show相当于vue中的v-if和v-show,原理也一样。


自定义组件使用(props和emit传值)


<!-- template.hml -->
<div class="item">
<text>Name: {{name}}</text>
<text>Age: {{age}}</text>
<text class="text-style" onclick="childClicked" id="text" ref="animator">点击这里查看隐藏文本</text>
</div>

<!-- template.js -->
export default {
props:{
name,
age
contentList
}
childClicked () {
//获取标签对象
//this.$element("text");
//this.$element("text").currentOffset().y 获取属性;
//通过ref的形式来获取
//this.$refs.animator
this.$emit('eventType1',{text:'123'});
},
};
<!-- index.hml -->
//注册
<element name='comp' src='../../common/template.hml'></element>
<div>
//使用
<comp name="Tony" age="18" content-list="contentList" @event-type1="textClicked"></comp>
</div>

<!-- template.js -->
export default {
textClicked (e) {
//e.detail 拿到传过来的数据 e.detail.text
},
};


注意:组件传递props和emit属性时,强制使用横杆连接的变量名进行传递,接收时,需要使用驼峰名进行接收,通过e.detail拿到emit传过来的参数,通过$element()方法或ref的形式来获取元素对象,其他用法基本和vue2相同。



生命周期和插槽等用法参考官方文档developer.harmonyos.com/cn/docs/doc…


通用事件


developer.harmonyos.com/cn/docs/doc…


内部系统组件


image.png
常用的组件包括:

容器组件:dialog、div、滚动组件用于上拉加载(list、list-item、list-item-group)、popup、轮播组件(swiper)

基础组件:image、text、span、input、label


<div>
<text>123</text>
</div>


注意:

1.div组件内部不能够直接嵌入文字,需要通过text组件进行包裹

2.list组件在相同方向的滚动不能嵌套使用,否则会造成滚动异常

3.image标签有些图片格式不支持,需要转换为可支持的格式



CSS


华为鸿蒙元服务不支持less,sass等预编译语言,只支持css,相对于h5来说,还做了部分阉割,有些属性在h5能用,在鸿蒙元服务确用不了。


元素标签默认样式


需要注意的是,在元服务中,所有的div标签都是一个flex盒子,所以在我们使用div的时候,如果是纵向布局,那我们需要去手动改变flex-direction: column,更改主轴方向。


//hml
<div id="tabBarCon">
<div id="tab1">
</div>

<div id="tab2" onclick="handleJumpToCart">
</div>

<div id="tab3" onclick="handleJumpToMine">
</div>

</div>
//css
.tabBarCon{
flex-direction:column;
}

元素选择器


image.png


image.png
只支持部分选择器和部分伪类选择器,像h5中的伪元素选择器都是不支持的,也不支持嵌套使用,由于不存在伪元素选择器,所以遇到有时候一些特殊场景时,我们只能在hml中去判断元素索引来添加动态样式。


属性与h5中的差异


属性鸿蒙元服务h5
position只支持absolute、relative、fixed支持absolute、relative、fixed、sticky
background渐变linear-gradient(134.27deg, #ff397e 0%, #ff074c 98%),渐变百分比不支持带小数点支持
长度单位只支持px、百分比,不支持rem、em、vw、vhpx、百分比、rem、em、vw、vh
多行文字省略text-overflow: ellipsis; max-lines: 1;(只能用于text组件)单行和多行使用属性不同

JS


特点:

1.支持ES6

2.用法和vue2相似


// app.js
export default {
onCreate() {
console.info('Application onCreate');
},
onDestroy() {
console.info('Application onDestroy');
},
globalData: {
appData: 'appData',
appVersion: '2.0',
},
globalMethod() {
console.info('This is a global method!');
this.globalData.appVersion = '3.0';
}
};
// index.js页面逻辑代码
export default {
data: {
appData: 'localData',
appVersion:'1.0',
},
onInit() {
//获取全局属性
this.appData = this.$app.$def.globalData.appData;
this.appVersion = this.$app.$def.globalData.appVersion;
},
invokeGlobalMethod() {
this.$app.$def.globalMethod();
},
getAppVersion() {
this.appVersion = this.$app.$def.globalData.appVersion;
}
}

data:定义变量

onInit:生命周期函数

getAppVersion:方法,不需要写在methods里面,直接与生命周期函数同级
this.app.app.def:可以拿到全局对象,


导入导出


支持ESmodule


//import
import router from '@ohos.router';
//export
export const xxx=123;

应用级生命周期


image.png


页面级生命周期


image.png


网络请求


使用@ohos.net.http内置模块即可,下面是对网络请求做了一个简单封装,使用的时候直接导入,调用相应请求方法即可,可惜的鸿蒙元服务目前没法进行抓包,所以网络请求调试的时候只能通过打断点的形式进行调试。


import http from '@ohos.net.http';


import { invokeShowLogin } from '../common/invoke_user';

export default {
interceptors(response) {
const result = JSON.parse(response.result || {});
const {code,errno} = result
if (errno === 1024 || code === 1005) {
return invokeShowLogin();
}

return result;
},

get(url, data) {
return http.createHttp().request(
// 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
url,
{
method: http.RequestMethod.GET, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递内容
extraData: data,
connectTimeout: 10*1000,
readTimeout: 10*1000,
}
).then(res=>{
return this.interceptors(res);
});
},

post(url, data) {
return http.createHttp().request(
// 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
url,
{
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': ' application/x-www-form-urlencoded'
},
// 当使用POST请求时此字段用于传递内容
extraData: data,
connectTimeout: 10*1000, // 可选,默认为60s
readTimeout: 10*1000, // 可选,默认为60s
}
).then(res=>{
return this.interceptors(res);
});
},

postJson(url, data) {
return http.createHttp().request(
// 填写http请求的url地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
url,
{
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递内容
extraData: data,
connectTimeout: 10*1000, // 可选,默认为60s
readTimeout: 10*1000, // 可选,默认为60s
}
).then(res=>{
return this.interceptors(res);
})
}
}

数据存储


只有本地持久化存储这种方式,关闭应用,数据不会丢失。


storage.set({
key: 'loginInfo',
value: JSON.stringify({
uid, skey
}),
});
storage.get({
key: 'userInfo',
value: JSON.stringify(userInfo),
});

路由跳转


<!-- index.hml -->
<div class="container">
<text class="title">This is the index page.</text>
<button type="capsule" value="Go to the second page" class="button" onclick="launch"></button>
</div>

// index.js
import router from '@ohos.router';
export default {
launch() {
router.push ({
url: 'pages/detail/detail',
//携带的参数
params:{a:123}
});
//router.back()
//router.replace()
},
}.
// detail.js
import router from '@ohos.router';
export default {
data:{
a:''
}
onInit(){
//页面携带过来的参数可以直接使用
//this.a
}
}

官方文档链接



  1. config.json:developer.harmonyos.com/cn/docs/doc… &developer.harmonyos.com/cn/docs/doc…

  2. http请求:developer.harmonyos.com/cn/docs/doc…

  3. hml:developer.harmonyos.com/cn/docs/doc…

  4. css:developer.harmonyos.com/cn/docs/doc…

  5. js:developer.harmonyos.com/cn/docs/doc…

  6. 生命周期:developer.harmonyos.com/cn/docs/doc…

  7. 目录结构:developer.harmonyos.com/cn/docs/doc…


作者:前端小萌新y
来源:juejin.cn/post/7275945995609964563
收起阅读 »

我来聊聊面向模板的前端开发

web
在软件开发中,研发效率永远是开发人员不断追求的主题之一。于公司而言,在竞争激烈的互联网行业中,产出得快和慢也许就决定着公司的生死存亡;于个人而言,效率高了就可以少加班,多出时间去提升自己、发展爱好、陪伴家人,工作、生活两不误。 提升效率的途径,无外乎就是「方法...
继续阅读 »

在软件开发中,研发效率永远是开发人员不断追求的主题之一。于公司而言,在竞争激烈的互联网行业中,产出得快和慢也许就决定着公司的生死存亡;于个人而言,效率高了就可以少加班,多出时间去提升自己、发展爱好、陪伴家人,工作、生活两不误。


提升效率的途径,无外乎就是「方法」和「工具」。以一个开发者的思维来想,就是将工作内容进行总结、归纳,从一组相似的工作内容中提炼共同点,抽象出解决这一类问题的方法,从而造出便于在今后的工作中更为快速解决这类问题的工具。这个「工具」可以是个函数、组件、中间件、插件,也可以是 IDE、其他开发工具的扩展,甚至是语言。


面向组件


在现代前端开发中,如果去问一个业务前端开发:「如何提升团队开发效率?」对方所回答的内容中,极有可能会出现「组件库」。没错,在前端工程化趋近完善的今天,在近几年 React、Vue 等组件化库/框架的影响下,面向组件开发的思维方式早已深入人心。


组件库提效有限


现在,组件库已经是一个前端团队的必备设施了,长远来看,团队一定且必须要有自己的组件库。开源的第三方组件库再好,对于一家企业的前端团队来说也只是短期用来充饥的,因为它们无法完全满足一家公司的业务场景,并且出于多终端支持的考虑,必定要进行二次开发或者自研。


组件库有了,团队和公司中推广的效果也不错,绝大多数的人都在用。使用组件开发页面相对 jQuery 时代要每块功能区都得从

等 HTML 标签码起来说确实提升了效率,然而有限;要搞出页面需要反复去引入组件,然后组合拼装出来,就像工厂流水线上的工人拼装零件,仍然要去做很多重复动作。


只要觉得当前的开发方式重复的动作多了,就代表还能继续提效,得想个法子减少重复无意义动作。


面向组件的开发方式,是现代前端页面开发提效的初级阶段,也是一个团队所要必经的阶段。


更高层面的提效


在之前写的文章中有段话——



组件可以很简单,也可以很复杂。按照复杂程度从小到大排的话,可以分为几类:



  1. 基础组件;

  2. 复合组件;

  3. 页面;

  4. 应用。


对,不用揉眼睛,你没有看错!


站在更高的角度去看,「页面」和「应用」也是一种「组件」,只不过它们更为复杂。在这里我想要说的不是它们,而是「基础组件」和「复合组件」。



文中提到了「页面」和「应用」也可以看作是种「组件」。虽然与当时的想法有些差异,但本文的内容就是要在那篇文章的基础上简单聊聊在「页面」层面的提效。


一般来说,「页面」是用户所能看到的最大、最完整的界面,如果能在这个层面有个很好的抽象方案,在做业务开发时与单纯地面向组件开发相比,应该会有更大的提效效果。


GUI 发展了几十年,人机交互的图形元素及布局方式已经相对固定,只要不是出现像 Google Glass 之类的革命性交互设备,就不会发生重大改变。在业务开发中界面形式更是千篇一律,尤其是 web 页面,尤其是中后台系统的 web 页面,一定可以通过什么方式来将这种「千篇一律」进行抽象。


试着来回想下,自己所做过的中后台系统的绝大部分页面是不是我所描述的这样——


页面整体是上下或左右布局。如果是上下布局的话,上面是页头,下面的左侧可能有带页面导航的侧边栏,或者没有侧边栏直接将页面导航全部集中在页头中,剩余区域是页面主体部分,承载着这个页面的主要数据和功能;如果是左右布局,左侧毋庸置疑就是有页面导航的侧边栏,页头跑到了右侧上面,其余是页面主体。


中后台系统的主要功能就是 CRUD,即业务数据的增删改查,相对应的页面展现及交互形式就是列表页、表单页和详情页。列表页汇总了所有业务数据的简要信息,并提供了数据的增、删、改和更多信息查看的入口;表单页肩负着数据新增和修改的功能;详情页能够看到一条业务数据记录最完整的信息。


每新增一个业务模块,就要又写一遍列表页、表单页和详情页……反复做这种事情有啥意思呢?既然这三种页面会反复出现,那干脆封装几个页面级别的组件好了,有新需求的时候就建几个页面入口文件,里面分别引入相应的页面组件,传入一些 props,完活儿!


这种方式看起来不错,然而存在几个问题:



  • 没有描述出页面内容的结构,已封装好的页面组件对于使用者来说算是个黑盒子,页面内容是什么结构不去看源码不得而知;

  • 如果新需求中虽然需要列表页、表单页和详情页,但与已封装好的能够覆盖大部分场景的相关组件所支持的页面有些差异,扩展性是个问题;

  • 每来新需求就要新建页面入口文件然后在里面引入页面组件,还是会有很多无意义重复动作和重复代码,时间长了还是觉得烦。


我需要一种既能看一眼就理解内容结构和关系,又具备较好扩展性,还能减少重复代码和无意义动作的方式——是的,兜了一个大圈子终于要进入正题了——面向模板开发。


面向模板


面向模板的前端开发有三大要素:模板;节点;部件。


富有表达力的模板


我所说的「模板」的主要作用是内容结构的描述以及页面的配置,观感上与 XHTML 相近。它主要具备以下几个特征:



  1. 字符全部小写,多单词用连接符「-」连接,无子孙的标签直接闭合;

  2. 包含极少的具备抽象语义的标签的标签集;

  3. 以特定标签的特定属性的形式支持有限的轻逻辑。


为什么不选择用 JSON 或 JSX 来描述和配置页面?因为模板更符合直觉,更易读,并且中立。用模板的话,一眼就能几乎不用思考地看出都有啥,以及层级关系;如果是 JSON 或 JSX,还得在脑中进行转换,增加心智负担,并且拼写起来相对复杂。Vue 上手如此「简单」的原因之一,就是它「符合直觉」的设计。


要使用模板去描述页面的话,就得自定义一套具有抽象语义的标签集。


页面的整体布局可以用如下模板结构去描述:


<layout>
<header>
<title>欧雷流title>
<navs />
header>
<layout>
<sidebar>
<navs />
sidebar>
<content>...content>
layout>
<footer>...footer>
layout>

看起来是不是跟 HTML 标签很像?但它们并不是 HTML 标签,也不会进行渲染,只是用来描述页面的一段文本。


整体布局可以描述了,但承载整个页面的主要数据和功能的主体部分该如何去描述呢?


在上文中提到,我们习惯将中后台系统中与数据的增删改查相对应的页面称为「列表页」、「表单页」和「详情页」。虽然它们中都带有「页」,但真正有区别的只是整个页面中的一部分区域,通常是页面主体部分。它们可以被分别看成是一种视图形式,所以可以将称呼稍微改变一下——「列表视图」、「表单视图」和「详情视图」。一般情况下,表单视图和详情视图长得基本一样,就是一个能编辑一个不能,可以将它们合称为「表单/详情视图」。


「视图」只描述了一个数据的集合该展示成啥样,并没有也没法去描述每个数据是什么以及长啥样,需要一个更小粒度的且能够去描述每个数据单元的概念——「字段」。这样一来,用来描述数据的概念和模板标签已经齐活儿了:


<view>
<field name="name" label="姓名" />
<field name="gender" label="性别" />
<field name="age" label="年龄" />
<field name="birthday" label="生日" />
view>

虽然数据能够描述了,但还有些欠缺:表单/详情视图中想将字段分组展示没法描述;对数据的操作也没有描述。为了解决这两个问题,再引入「分组」和「动作」。这下,表单/详情视图的模板看起来会是这样:


<view>
<group title="基本信息">
<field name="name" label="姓名" />
<field name="gender" label="性别" />
<field name="age" label="年龄" />
<field name="birthday" label="生日" />
group>
<group title="宠物">
<field name="dogs" label="🐶" />
<field name="cats" label="🐱" />
group>
<action ref="submit" text="提交" />
<action ref="reset" text="重置" />
<action ref="cancel" text="取消" />
view>

模板很好地解决了内容结构描述和配置的问题,但如何去动态地调整结构和更改配置呢?在平常的业务页面开发时也许不会太凸显出问题,但碰到流程表单设计或页面可视化编辑这种灵活性很高的需求时,问题就会被暴露出来了。


充满控制力的节点


在这里,我要将定义好的标签集所拼成的模板解析成节点树,通过更改树的结构和节点的属性去影响页面最终的呈现效果。每个节点都会有节点的基本信息、对应标签的属性和一些节点操作方法:


{
name: "field",
tag: "field",
attrs: {
name: "name",
label: "姓名"
},
parent: {},
children: [],
remove: function() {},
insert: function() {}
}

在页面模板化且节点化之后,理想情况下,页面长啥样已经不受如 React、Vue 等运行时技术栈的束缚,控制权完全在解析模板所生成的节点树上,要想改变页面的视觉效果时只需更改节点即可。


极具表现力的部件


页面内容的描述通过模板来表达了,页面内容的控制权集中到节点树中了,那么页面内容的呈现在这种体系下应该如何去做呢?负责这块的,就是接下来要说的面向模板开发的第三大要素——部件。


「部件」这个词不新鲜,但在我所说的这个面向模板开发的体系中的含义,需要被重新定义一下:「部件」是一个可复用的,显示的信息排列可由用户改变的,可以进行交互的 GUI 元素。


在这个面向模板开发的体系中,模板和节点树完全是中立的,即不受运行时的技术栈所影响;而部件是建立在运行时技术栈的基础之上,但不必限于同一个技术栈。也就是说,可以使用 React 组件,也可以用 Vue 组件。


每个部件在使用前都需要注册,然后在模板中通过 widget 属性引用:


<view widget="form">
<group title="基本信息" widget="fieldset">
<field name="name" label="姓名" widget="input" />
<field name="gender" label="性别" widget="radio" />
<field name="age" label="年龄" widget="number" />
<field name="birthday" label="生日" widget="date-picker" />
group>
<group title="宠物" widget="fieldset">
<field name="dogs" label="🐶" widget="select" />
<field name="cats" label="🐱" widget="select" />
group>
<action ref="submit" text="提交" widget="button" />
<action ref="reset" text="重置" widget="button" />
<action ref="cancel" text="取消" widget="button" />
view>

这样,一个面向模板开发的普通表单页出来了!


思想总结


面向模板的开发方式很好,能够大幅度提高业务前端开发效率,一定程度上减少了业务系统的搭建速度;作为核心的模板和节点树是保持中立的,大大降低了运行时技术栈的迁移成本,且能够应对多端等场景。


面向模板的开发方式初期投入成本很高,标签集、模板解析和部件注册与调用机制等的设计和实现需要较多时间,并且这仅仅是视图层,逻辑层也需要做出相应的变化,不能简单地用 props 和事件绑定进行处理了。


这个体系建成之后,在业务开发上会很简单,但机制理解上会增加部分开发人员的心智负担。


为了效率,一家公司里的业务前端开发到最后一定是面向模板,而非面向组件。


作者:欧雷殿
来源:juejin.cn/post/7274430147126493199
收起阅读 »

如何快速解决集成环信IM遇到的问题?

1、环信FAQ频道发布了环信FAQ帮助中心提供了各客户端、RESTful API、环信控制台以及商务相关的集成环信常见问题及解决方法,帮您快速解决集成问题2、当我有问题时,从哪里进FAQ?干脆收藏这个网址:https://faq.easemob.com/环信官...
继续阅读 »

1、环信FAQ频道发布了


环信FAQ帮助中心提供了各客户端、RESTful API、环信控制台以及商务相关的集成环信常见问题及解决方法,帮您快速解决集成问题

2、当我有问题时,从哪里进FAQ?

  • 干脆收藏这个网址:https://faq.easemob.com/

  • 环信官网导航-帮助FAQ

  • IMGeek社区导航-FAQ、社区banner

  • 环信IM文档-帮助中心

  • Console控制台-常见问题-查看更多

3、 如何快速锁定到我想问的问题答案?

搜就完了


FAQ频道提供完善的标题关键词搜索、全文搜索


04、我想问的问题还没收录?

FAQ频道的问题及答案我们将持续总结更新
如果您的问题目前还没收录
又很着急
请联系您的商务经理
将您加到环信官方技术支持群
或到IMGeek社区发帖提问
https://www.imgeek.net/
我们将尽全力快速帮您解决

收起阅读 »

环信uni-app-demo 升级改造计划——单人&多人音视频通话(三)

前序文章:环信 uni-app Demo升级改造计划--Vue2迁移到Vue3(一)环信即时通讯SDK集成--环信 uni-app-demo 升级改造计划--整体代码重构优化(二)概述在将声网 uni-app 音视频插件正式集成进入环信的 uni-app-de...
继续阅读 »

前序文章:

环信 uni-app Demo升级改造计划--Vue2迁移到Vue3(一)

环信即时通讯SDK集成--环信 uni-app-demo 升级改造计划--整体代码重构优化(二)

概述

在将声网 uni-app 音视频插件正式集成进入环信的 uni-app-demo 中,标志着本次升级改造至此基本告一段落。在第三期的升级改造中,主要工作为在 Demo 层形成一个较为容易拆分的有关音视频相关组件,力求第一:代码是否可读、第二:可以对参考源码的同学提供实例、第三:能够方便在脱离其他 IM 功能时,完成对音视频功能的复用。
同时也顺手针对 emChat 组件进行小范围重构,解决了 uni-app 在 App 以及小程序端,软键盘弹起消息列表不滚动以及软键盘遮挡功能栏问题。

下面我将尽可能详细描述一下本次针对音视频功能、以及消息列表重写的心路历程。

功能背景以及目的

有越来越多的用户在 IM 功能实现中不免向类似微信聊天的功能靠齐,除了日常 IM 功能中,不免也离不开音视频通话功能,因此需要在环信uni-app-demo中增加实现音视频通话的示例代码,能够对想要实现音视频功能的用户形成可参考的 demo 代码,以及可复用的音视频功能模块组件。

前置准备

  • 确认实现功能范围 接听呼叫(单聊一对一、群组多人音视频通话)且只支持 uni-app 原生端使用。
  • 浏览声网音视频 uni-app 端相关文档,熟悉大致流程以及熟悉部分核心 API,跑通示例 Demo。
  • 熟悉环信其他端PCWeb端、安卓、iOS端callKit 信令交互相关逻辑,确保实现 uni-app 所实现的音视频功能能够与其他端 Demo 进行互通。
  • 了解nvue组件相关语法布局样式等与vue的差异,推拉流视频容器仅支持在nvue组件中进行使用。

实践见真章

Tip:以下展示代码因篇幅所限,均做了不同程度的删减保留了核心逻辑展示,详细代码文末会给出源码地址。

step1:在项目中集成音视频相关插件

Agora(声网)Demo 示例中有两个插件是必须要进行集成的,分别为Native原生插件,Js插件

Agora-Demo 示例插件下载地址以及功能简介详见下方提供的链接。

具体插件的导入方式就不在本篇中详细介绍,上方插件下载地址中有提到插件导入方式,可以进行参阅。

特别注意:Agora-Uni-App JS 插件导入之后会在目录下生成一个package.json文件,这个文件会与通过 npm 导入的easemob-websdkpackage.json重合,因此 Demo 中只保留了easemob-demopackage.json

step2: 设计搭建 emCallKit(音视频组件)逻辑结构

主体大致结构如下:

graph TD
CallKit --> emCallkit
emCallkit --> callKitManage
emCallkit --> config
emCallkit --> contants
emCallkit --> stores
emCallkit --> utils
emCallkit --> index.js
CallKit --> emCallkitPages
emCallkitPages --> alertScreen.vue
emCallkitPages --> inviteMembers.vue
emCallkitPages --> multiCall.nvue
emCallkitPages --> singleCall.nvue

其中components/emCallKit主要为核心 emCallKit 逻辑层代码,callKitManage文件中主要包含对外发布订阅频道内时间逻辑代码,以及频道内信令发送代码。config声网 AppId 配置。contants文件夹音视频频道内常量、stores频道内核心逻辑在此,利用 pinia 进行频道内状态管理。utils工具方法,index.jsemCallkit 入口文件,该文件内挂载信令监听初始化频道内 IM Client。

pages/emCallKitPages则是频道内各个页面在此构造,alertScreen.vue单人多人收到邀请弹出该页面,单人呼叫也使用该页面。inviteMembers.vue多人邀请页面。multiCall.nvue多人通话中页面。singleCall.nvue单人通话中页面。

step3:实现单人音视频信令接收以及发送

在思考实现单人音视频拨打之前需要了解其他端已经实现的音视频时序, 以单人音视频呼叫为例:

Alice 为呼叫方 John 为接收方
sequenceDiagram
Alice->>John: invite message(邀请您进行单人音视频通话)
John-->>Alice: alerting
Alice-)John: confirmRing
John-->>Alice: answerCall
Alice-)John: confirmCallee

可以看到与 http 的”握手“过程相似,需要经过几次确认,这样频繁的确认意义在于,能否保证通话状态的准确性,且有效防止在离线的情况下,上线无故触发已经失效的邀请弹窗。而上面的除了邀请的消息为一条普通文本消息,整个过程都是通过环信 IM 的CMD命令消息实现,且每条消息信令中都有携带一些声网频道信息,比如频道名称,呼叫的类型等都是基于CMD命令消息实现。

为了能够独立于 IM 功能之外去使用音视频插件,因此在书写时尽可能的与外层 IM Demo 中的逻辑分离开,比如 callKit 中有用到消息监听用来监听消息以及发送 im 消息,因此将实例化后的 websdk(暂称:EMClient)传入到 emCallKit 中,并利用 websdk 支持多处挂载监听回调的特性,通过拿到传入EMClient.send进行消息发送,并使用EMClient.addEventHandler进行监听的挂载,便形成了如下缩减后的代码:

/* 频道信令发送 */
import useSendSignalMsgs from './callKitManage/useSendSignalMsgs';
let CallKitEMClient = null;
let CallKitCreateMsgFun = null;
export const useInitCallKit = () => {
//初始化EMClient之Callkit内
const setCallKitClient = (EMClient, CreateMsgFun) => {
CallKitEMClient = EMClient;
CallKitCreateMsgFun = CreateMsgFun;
mountSignallingListener();
};
//挂载Callkit信令相关监听
const mountSignallingListener = () => {
console.log('>>>>>>>callkit 监听已挂载');
CallKitEMClient.addEventHandler('callkitSignal', {
onTextMessage: (message) => {
const { ext } = message;
if (ext && ext?.action === CALL_ACTIONS_TYPE.INVITE)
handleCallKitInvite(message);
console.log('>>>>>收到文本信令消息', message);
},
onCmdMessage: (msg) => {
console.log('>>>>>收到命令信令消息', msg);
if (msg && msg?.action === CALL_ACTIONS_TYPE.RTC_CALL)
handleCallKitCommand(msg);
},
});
//处理收到为文本的邀请信息
const handleCallKitInvite = (msgBody) => {
console.log('>>>>>开始处理被邀请消息');
const { from, ext } = msgBody || {};
//邀请消息发送者为自己则忽略
if (from === CallKitEMClient.user) return;
};
//处理接收到通话交互过程的CMD命令消息
const handleCallKitCommand = (msgBody) => {
//多端状态下信令消息发送者为自己则忽略
if (msgBody.from === CallKitEMClient.user) return;
};
};

};
return {
CallKitEMClient,
CallKitCreateMsgFun,
setCallKitClient,
};
};
//外层调用初始化callKit频道
import { EMClient, EaseSDK } from './EaseIM';
/* callKit */
import { useInitCallKit } from '@/components/emCallKit';
const { setCallKitClient } = useInitCallKit();
setCallKitClient(EMClient, EaseSDK.message);

至此就可以做到了,在初始化的时候完成针对 callKit 监听的挂载,能够做到在 callKit 中单独接收 im 相关邀请消息以及信令。 下面解决 im 信令发的问题 如上面描述的 callKit 项目结构一致,在callKitManage文件夹下新建useSendSignalMsgs.js文件主要处理有关信令发送核心代码,从而解决信令的发送问题。

/* 用来发送所有频道内信令使用 */
import { CALL_ACTIONS_TYPE, MSG_TYPE } from '../contants';
import { useInitCallKit } from '../index.js';

const action = 'rtcCall';
const useSendSignalMsgs = () => {
const { CallKitEMClient, CallKitCreateMsgFun } = useInitCallKit();
//发送通知弹出待接听窗口信令
const sendAlertMsg = (payload) => {
const { from, ext } = payload;
const option = {
type: 'cmd',
chatType: 'singleChat',
to: from,
action: action,
ext: {
action: CALL_ACTIONS_TYPE.ALERT,
calleeDevId: CallKitEMClient.context.jid.clientResource,
callerDevId: ext.callerDevId,
callId: ext.callId,
ts: Date.now(),
msgType: MSG_TYPE,
},
};
console.log('>>>>>>>option', option);
const msg = CallKitCreateMsgFun.create(option);
// 调用 `send` 方法发送该透传消息。
CallKitEMClient.send(msg)
.then((res) => {
// 消息成功发送回调。
console.log('answer Success', res);
})
.catch((e) => {
// 消息发送失败回调。
console.log('anser Fail', e);
});
};
return {
sendAlertMsg,
};
};
export default useSendSignalMsgs;
//发送时调用
import useSendSignalMsgs from '../callKitManage/useSendSignalMsgs';
const { sendAnswerMsg } = useSendSignalMsgs();
const payload = {
targetId: from,
sendBody: ext,
};
sendAnswerMsg(payload, ANSWER_TYPE.BUSY);

到这里,关于 callKit 组件内的有关信令部分的核心代码的设计就此结束。

step4:搭建频道内管理相关代码

频道管理是必须要做的,试想一个小场景,张三正在与李四进行音视频通话,此时王五呼叫过来,如果不做什么状态的管理,收到王五的视频邀请就立马弹出了一个邀请弹窗,但是此时张三却已经在通话中了,那么从代码的角度讲这个已经算是一个较为严重的 Bug 了,因此我们必须要在频道中引入状态管理这个概念,这个概念的实现即不是环信IM层面,也不是声网RTC,而是我们自己需要实现的一个状态,比如空闲、呼叫中、邀请中、通话中等等,我们需要抽象出来一个频道状态从而映射出用户在使用音视频通话功能中不同时期的情况,并且做出不同的逻辑层处理。

在引入状态管理的情况下,再去套用刚才的场景: 张三在收到李四的通话邀请时,张三本身为空闲状态,此时就可以回复给李四状态空闲可以通话,李四收到张三的回复后可以调起通话待接听界面,直到张三接听后双方可进入到频道中,正常进行通话功能的使用,此时王五呼叫张三,引领发出后,张三收到邀请信令,获取当前状态为通话中,则直接根据获取的状态判断直接回复BUSY忙碌中,从而拒绝了王五的通话邀请。

可以看到引入了频道中的状态管理概念我们解决了音视频通话时避免状态混乱导致的一系列问题,下面可以看下示例代码。

import { defineStore } from 'pinia';
import useSendSignalMsgs from '../callKitManage/useSendSignalMsgs';
import createUid from '../utils/createUid';
const useAgoraChannelStore = defineStore('agoraChannelStore', {
state: () => ({
emClientInfos: {
apiUrl: '',
appKey: '',
loginUserId: '',
clientResource: '',
accessToken: '',
},
callKitStatus: {
localClientStatus: CALLSTATUS.idle, //callkit状态
channelInfos: {
channelName: '', //频道名
agoraChannelToken: '', //频道token
agoraUserId: '', //频道用户id,
callType: CALL_TYPES.SINGLE_VOICE, //0 语音 1 视频 2 多人音视频
callId: null, //会议ID
channelUsers: {}, //频道内用户
callerDevId: '', //主叫方设备ID
calleeDevId: '', //被叫方设备ID
callerIMName: '', //主叫方环信ID
calleeIMName: '', //被叫方环信ID
groupId: '', //群组ID
},
//被邀请对象 单人为string 多人为array
inviteTarget: null,
},
}),
actions: {
/* emClient */
initEmClientInfos(emClient) {
console.log('initEmClientInfos', emClient);
if (!emClient) return;
this.emClientInfos.apiUrl = emClient.apiUrl;
this.emClientInfos.appKey = emClient.appKey;
this.emClientInfos.loginUserId = emClient.user;
this.emClientInfos.accessToken = emClient.token;
this.emClientInfos.clientResource = emClient.clientResource;
},
/* CallKit status 管理 */
//初始化频道信息
initChannelInfos() {
this.callKitStatus.localClientStatus = CALLSTATUS.idle;
this.callKitStatus.channelInfos = {
channelName: '', //频道名
agoraChannelToken: '', //频道token
agoraUid: '', //频道用户id
callType: CALL_TYPES.SINGLE_VOICE, //0 语音 1 视频 2 多人音视频
callId: null, //会议ID
channelUsers: {}, //频道内用户
callerDevId: '', //主叫方设备ID
calleeDevId: '', //被叫方设备ID
confrontId: '', //要处理的目标ID
callerIMName: '', //主叫方环信ID
calleeIMName: '', //被叫方环信ID
groupId: '', //群组ID
};
this.callKitStatus.inviteTarget = null;
this.callKitTimer && clearTimeout(this.callKitTimer);
},
//更新localStatus
updateLocalStatus(typeCode) {
console.log('>>>>>开始变更本地状态为 typeCode', typeCode);
this.callKitStatus.localClientStatus = typeCode;
},
//更新频道信息
updateChannelInfos(msgBody) {
console.log('触发更新频道信息', msgBody);
const { from, to, ext } = msgBody || {};
const params = {
channelName:
ext.channelName || this.callKitStatus.channelInfos.channelName,
callId: ext.callId || this.callKitStatus.channelInfos.callId,
callType:
CALL_TYPE[ext.type] || this.callKitStatus.channelInfos.callType,
callerDevId: ext.callerDevId || 0,
calleeDevId: ext.calleeDevId,
callerIMName: from,
calleeIMName: to,
groupId: ext?.ext?.groupId ? ext.ext.groupId : '',
};
console.log('%c将要更新的信息内容为', 'color:red', params);
Object.assign(this.callKitStatus.channelInfos, params);
},
},
});
export default useAgoraChannelStore;
//频道状态使用以及变更示例代码
import useAgoraChannelStore from './stores/channelManger';
const { updateChannelInfos, updateLocalStatus } = agoraChannelStore;
const callKitStatus = computed(() => agoraChannelStore.callKitStatus);

上面示例代码,是针对频道内的状态管理演示代码,用到了 pinia 去进行状态存储以及管理,pinia 也支持在 nvue 页面中很方便的使用。

step5:关于 callKit 可视页面的处理

关于可视组件的处理是指的是,比如在收到邀请信息时需要弹出待接听页面,那么我们就需要跳转至待接听页面,多人通话时我们需要邀请更多人加入会议,那么我们则需要弹出邀请页面,单人以及多人通话中我们则需要跳转至实际需要显示通话双方音视频流的组件页面,上面提到的几个页面就分别对应了:alertScreen.vueinviteMembers.vuemultiCall.nvuesingleCall.nvue

这些组件由于是页面级别的,因此在需要跳转至对应的页面时,不免需要进行 router 路由映射关系配置,因此我们需要在pages.json中进行对应的页面地址配置,这里拿其中alertScreen.vue做代码演示。

pages.json 配置

{
"path": "pages/emCallKitPages/alertScreen",
"style": {
"app-plus": {
"titleNView": false
}
}
}

跳转至待接听页面

import useCallKitEvent from '@/components/emCallKit/callKitManage/useCallKitEvent';
const { EVENT_NAME, CALLKIT_EVENT_CODE, SUB_CHANNEL_EVENT } = useCallKitEvent();
SUB_CHANNEL_EVENT(EVENT_NAME, (params) => {
const { type, ext, callType, eventHxId } = params;
console.log('>>>>>>订阅到callkit事件发布', params);
//弹出待接听事件
switch (type.code) {
case CALLKIT_EVENT_CODE.ALERT_SCREEN:
{
//跳转至待接听页面
uni.navigateTo({
url: '../emCallKitPages/alertScreen',
});
}
break;
default:
break;
}
});

从待接听页面选择接听后的跳转

在待接听页面,点击接听后,应该是怎样的逻辑处理?

const agreeJoinChannel = () => {
handleSendAnswerMsg(ANSWER_TYPE.ACCPET);
if (channelInfos.value.callType === CALL_TYPES.MULTI_VIDEO) {
uni.redirectTo({
url: '/pages/emCallKitPages/multiCall',
});
} else {
enterSingleCallPage();
}
};
const enterSingleCallPage = () => {
uni.redirectTo({
url: '/pages/emCallKitPages/singleCall',
});
};

可以看到上面的演示代码做了两种通话大类(单人、多人)不同的页面跳转。

下面我们看下通话中的视图页面是怎样的(singleCall为例),同样代码做了一部分的删减。

<template>
<div class="single_call_container">

<view
class="rtc_view_container"
v-if="callKitStatus.channelInfos.callType === CALL_TYPES.SINGLE_VIDEO"
>

<view class="local_container">
<rtc-surface-view
v-if="state.engine"
class="local_view_stream"
:uid="0"
:zOrderMediaOverlay="true"
>
rtc-surface-view>
view>
<view class="remote_container">
<rtc-surface-view
class="remote_view_stream"
:uid="state.remoteUid"
>
rtc-surface-view>
view>
view>

<view
class="rtc_voice_container"
v-if="callKitStatus.channelInfos.callType === CALL_TYPES.SINGLE_VOICE"
>

<view class="circleBodyView">
<image
class="circleItemAvatar"
src="/static/emCallKit/theme2x.png"
>
image>
<view class="circleCenter"
>
<text class="cenametext"
>
{{ callKitStatus.inviteTarget ||
callKitStatus.channelInfos.callerIMName }}text
>
<text class="centertext">正在语音通话…text>
view>
view>
view>

<view class="rtc_control">
<view class="circleBoxView">
<text class="hint">{{ formatTime }}text>
view>
<view class="circleBoxView">
<view class="circleBox" @click="onSwitchLocalMicPhone">
<image
class="circleImg"
:src="
state.isMuteLocalAudioStream
? '/static/emCallKit/icon_video_quiet.png'
: '/static/emCallKit/icon_video_microphone.png'
"
>
image>
<text class="hint">麦克风text>
view>
<view class="circleBox" @click="onSwitchSperkerPhone">
<image
class="circleImg"
:src="
state.isSwitchSperkerPhone
? '/static/emCallKit/icon_video_speaker.png'
: '/static/emCallKit/icon_video_speakerno.png'
"
>
image>
<text class="hint">扬声器text>
view>
<view
v-if="callKitStatus.channelInfos.callType === CALL_TYPES.SINGLE_VIDEO"
class="circleBox"
@click="onSwitchLocalCameraOpened"
>

<image
class="circleImg"
:src="
state.isSwitchLocalCameraOpened
? '/static/emCallKit/icon_video_speaker.png'
: '/static/emCallKit/icon_video_speakerno.png'
"
>
image>
<text class="hint">摄像头text>
view>
view>
<view class="circleBoxView">
<view class="circleBox" @click="leaveChannel">
<image
class="circleImg"
src="/static/emCallKit/icon_video_cancel.png"
>
image>
<text class="hint">挂断text>
view>
view>
<image
v-if="callKitStatus.channelInfos.callType === CALL_TYPES.SINGLE_VIDEO"
class="switchCamera"
@click="onSwitchCamera"
src="/static/emCallKit/iconxiangjifanzhuan.png"
>
image>
view>
div>
template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { onLoad, onUnload } from '@dcloudio/uni-app';
import { AGORA_APP_ID } from '@/components/emCallKit/config/index.js';
import { CALLSTATUS, CALL_TYPES } from '@/components/emCallKit/contants';
import RtcEngine, { RtcChannel } from '@/components/Agora-RTC-JS/index';
import {
ClientRole,
ChannelProfile,
} from
'@/components/Agora-RTC-JS/common/Enums';
import RtcSurfaceView from '@/components/Agora-RTC-JS/RtcSurfaceView';
import useAgoraChannelStore from '@/components/emCallKit/stores/channelManger';

//获取移动端授权权限
import permision from '@/js_sdk/wa-permission/permission';
//store
const agoraChannelStore = useAgoraChannelStore();
//channelInfos
const callKitStatus = computed(() => {
return agoraChannelStore.callKitStatus;
});
//channelName
const channelName = computed(
() => agoraChannelStore.callKitStatus.channelInfos
?.channelName
);
const state = reactive({
engine
: undefined,
channelId
: '',
isJoined
: false,
remoteUid
: '',
isSwitchCamera
: true,
isSwitchSperkerPhone
: true,
isMuteLocalAudioStream
: false,
isSwitchLocalCameraOpened
: true,
});
//开启通话计时
const inChannelTimer = ref(null);
const timeCount = ref(0);
const startInChannelTimer = () => {
inChannelTimer.value
&& clearInterval(inChannelTimer.value);
inChannelTimer.value
= setInterval(() => {
timeCount.value
++;
// console.log('%c通话计时开启中...', 'color:green', timeCount);
}, 1000);
};
//转换为可直接渲染的时间
const formatTime = computed(() => {
const m = Math.floor(timeCount.value / 60);
const s = timeCount.value % 60;
const h = Math.floor(m / 60);
const remMin = m % 60;
return `${h > 0 ? h + ':' : ''}${remMin < 10 ? '0' + remMin : remMin}:${
s
< 10 ? '0' + s : s
}`;
});
//频道监听
const addListeners = () => {
state.engine.addListener(
'JoinChannelSuccess', (channel, uid, elapsed) => {
console.info(
'JoinChannelSuccess', channel, uid, elapsed);
state.isJoined
= true;
});
state.engine.addListener(
'UserJoined', (uid, elapsed) => {
console.info(
'UserJoined', uid, elapsed);
state.remoteUid
= uid;
});
state.engine.addListener(
'UserOffline', (uid, reason) => {
console.info(
'UserOffline', uid, reason);
state.remoteUid
= '';
state.isJoined
= false;
leaveChannel();
});
state.engine.addListener(
'LeaveChannel', (stats) => {
console.info(
'LeaveChannel', stats);
state.isJoined
= false;
state.remoteUid
= '';
});
};
//保持屏幕常亮
uni.setKeepScreenOn({
keepScreenOn
: true,
});
//初始化频道实例
const initEngine = async () => {
console.log(
'>>>>>>>初始化声网RTC');

state.engine
= await RtcEngine.create(AGORA_APP_ID);
addListeners();
if (uni.getSystemInfoSync().platform === 'android') {
await permision.requestAndroidPermission(
'android.permission.RECORD_AUDIO');
await permision.requestAndroidPermission(
'android.permission.CAMERA');
}
await state.engine.enableVideo();
await state.engine.startPreview();
await state.engine.setChannelProfile(ChannelProfile.LiveBroadcasting);
await state.engine.setClientRole(ClientRole.Broadcaster);
//设置频道麦克风为扬声器模式
await state.engine.setDefaultAudioRoutetoSpeakerphone(true);
await joinChannel();
};

//加入频道
const joinChannel = async () => {
let { accessToken, agoraUserId } =
await agoraChannelStore.requestRtcChannelToken();
console.log(
'>>>>>>频道token请求完成',
accessToken,
agoraUserId,
channelName.value
);
(await state.engine)
&&
state.engine.joinChannel(accessToken, channelName.value,
null, agoraUserId);
startInChannelTimer();
};
//挂断
const leaveChannel = async () => {
(await state.engine)
&& state.engine.leaveChannel();
uni.navigateBack({ delta
: 1 });
//设置本地状态为闲置
agoraChannelStore.updateLocalStatus(CALLSTATUS.idle);
uni.showToast({
icon
: 'none',
title
: `通话结束【${formatTime.value}】`,
});
};
//切换摄像头
const onSwitchCamera = () => {
state.engine
&&
state.engine
.switchCamera()
.then(() => {
state.isSwitchCamera
= !state.isSwitchCamera;
})
.
catch((err) => {
console.warn(
'switchCamera', err);
});
};
//切换扬声器
const onSwitchSperkerPhone = async () => {
try {
(await state.engine)
&&
state.engine.setEnableSpeakerphone(
!state.isSwitchSperkerPhone);
state.isSwitchSperkerPhone
= !state.isSwitchSperkerPhone;
}
catch (error) {
uni.showToast({ icon
: 'none', title: '扬声器切换失败!' });
}
};
//开启关闭本地麦克风采集
const onSwitchLocalMicPhone = async () => {
try {
(await state.engine)
&&
state.engine.muteLocalAudioStream(
!state.isMuteLocalAudioStream);
state.isMuteLocalAudioStream
= !state.isMuteLocalAudioStream;
}
catch (error) {
uni.showToast({ icon
: 'none', title: '开关本地麦克风采集失败!' });
}
};
//开启关闭本地视频流采集
const onSwitchLocalCameraOpened = async () => {
try {
(await state.engine)
&&
state.engine.enableLocalVideo(
!state.isSwitchLocalCameraOpened);
state.isSwitchLocalCameraOpened
= !state.isSwitchLocalCameraOpened;
}
catch (error) {
uni.showToast({ icon
: 'none', title: '开关本地摄像头采集失败!' });
}
};

onLoad(() => {
console.log(
'+++++++singleCall onLoad');
initEngine();
});
onUnload(() => {
state.engine
&& state.engine.destroy();
state.isJoined
= false;
//卸载组件清除通话计时
//清除通话计时
inChannelTimer.value && clearInterval(inChannelTimer.value);
});
script>

核心的流展示则是 Agora-UniApp 原生插件提供的RtcSurfaceView组件通过该组件进行本地流和远端流的展示。

在 nvue 组件中提几个点,可以关注一下。

  • 安卓机型,在发布本地流之前需要拿到用户关于录音以及摄像头的授权,否则无法正常的进行推流展示。具体的授权 js 调用插件,关注wa-permission这个插件。
  • 默认音视频通话会跟随系统息屏时间自动息屏,不希望息屏则可以调用 uni-app 提供的 apiuni.setKeepScreenOn({ keepScreenOn: true, });
  • 引入原生插件后必须打包为自定义调试基座才可以看到具体的效果,否则不会展示画面。

到这里可视页面的相关代码以及所需配置介绍暂时告一段落。 下面再看下邀请相关逻辑。

step6:关于 callKit 邀请相关逻辑的介绍。

如果作为邀请方也就是音视频功能的发起方,我们如何使用 callKit 内的代码完成这一动作?

<template>
<view>
<uv-popup ref="invitePopup" mode="bottom" round="10">
<view class="invite_btn_box">
<text
class="invite_func_btn"
@click="sendAvCallMessage(CALL_TYPES.SINGLE_VIDEO)"
>
视频通话text
>
<text
class="invite_func_btn"
@click="sendAvCallMessage(CALL_TYPES.SINGLE_VOICE)"
>
语音通话text
>

<text class="invite_func_btn invite_func_btn_cannel" @click="onCannel"
>
取消text
>
view>
uv-popup>
view>
template>
<script setup>
import { ref, inject } from 'vue';
import useAgoraChannelStore from '@/components/emCallKit/stores/channelManger';
import { CALL_TYPES } from '@/components/emCallKit/contants';
import onFeedTap from '@/utils/feedTap';
const agoraChannelStore = useAgoraChannelStore();
const injectTargetId = inject('targetId');
const invitePopup = ref(null);
const openInvitePopup = () => {
invitePopup.value.open();
};
const closeInvitePopup = () => {
invitePopup.value.close();
};
const onCannel = () => {
onFeedTap
&& onFeedTap();
closeInvitePopup();
};
const sendAvCallMessage = async (callType) => {
onFeedTap
&& onFeedTap();
try {
await agoraChannelStore.sendInviteMessage(injectTargetId.value, callType);
uni.navigateTo({
url
: '/pages/emCallKitPages/alertScreen',
});
}
catch (error) {
console.log(
'>>>>通话邀请发起失败', error);
uni.showToast({
icon
: 'none',
title
: '通话发起失败',
});
}
finally {
closeInvitePopup();
}
};

defineExpose({
openInvitePopup,
});
script>

在实际的 Demo 中增加了一个inviteAvcall.vue组件在外层点击某个 icon 时展示该 Popup 组件,弹出视频邀请或音频邀请的选项。 效果如下:


IMG_68B260790344-1.jpeg

点击时传入对应的类型邀请信令发送给要邀请的目标一条文本邀请信息。

而多人音视频模式下,邀请下则不需要弹出待接听页面,而是进入勾选要发送邀请信息的成员页面,发送邀请并创建频道并加入即可,就像这样。

const inviteAvcallComp = ref(null);
const selectAvcallType = () => {
closeAllModal();
if (injectChatType.value === 'groupChat') {
uni.navigateTo({
url: `/pages/emCallKitPages/inviteMembers?groupId=${injectTargetId.value}`,
});
} else {
inviteAvcallComp.value && inviteAvcallComp.value.openInvitePopup();
}
};

页面效果展示


IMG_68B260790344-1.jpeg



IMG_1761.png



IMG_1760.PNG


相关链接

环信 uni-app 文档地址

本文源码地址

声网音视频插件资料相关地址

收起阅读 »

Xcodes 管理多个 Xcode 的版本,简直泰酷辣

iOS
为什么要使用多个 Xcode? 有些时候,我们可能需要多个版本的 Xcode,比如: 情景1: 每年的6月 WWDC 大会发布后,都伴随着 iOS 系统的更新,当你想体验下新的功能的时候,你想下载 Xcode 的 Beta 版本尝试适配新版本的变化,但是又不...
继续阅读 »

为什么要使用多个 Xcode?




有些时候,我们可能需要多个版本的 Xcode,比如:


情景1:
每年的6月 WWDC 大会发布后,都伴随着 iOS 系统的更新,当你想体验下新的功能的时候,你想下载 Xcode 的 Beta 版本尝试适配新版本的变化,但是又不想覆盖原有的 Release 版本。


情景2:
你们公司的项目复杂又庞大,你担心更新 Xcode 后,项目运行报错,不得不回退旧版本的 Xcode。


像上面两种情况,我们就希望多个版本的 Xcode 同时存在,既能体验新版本的功能变化,也能确保我们项目在原有版本正常运行。


Xcodes - 轻松管理多个 Xcode


给大家推荐一款轻松管理 Xcode 的一个工具包 Xcodes,它的下载地址在 GitHub 上,点我直达


Xcodes 优点

  • 简洁的桌面,可快速发现想要安装的版本。
  • 安装包很小,只有 23MB 左右。
  • 下载速度快,使用了 aria2 下载工具,比 URLSession 快 3-5 倍。
  • 如果网络错误,可自动恢复安装。
  • 可选择默认 Xcode。

由于我不知道 aria2 是什么,所以 chatGPT 了一下 😁,下面是 chatGPT 给出的答案



aria2是一款开源的多协议、多线程下载工具,可以用来在命令行界面下载文件。它支持HTTP、HTTPS、FTP、BitTorrent等多种协议,可以同时下载多个文件,并自动利用多个连接和线程来加快下载速度。aria2在Linux、Windows和macOS等多个操作系统上都可用,并且可以通过命令行进行控制和配置。



下载安装


XcodesAppREADME.md 也有说明可以使用 两种安装方式


安装方式 1: 借助Homebrew安装

brew install --cask xcodes

安装方式 2: 手动安装 (我是手动安装的)



README.md 里找如上图:滚动到 Manually install(手动安装)这里,点击here 蓝色高亮的地方,会进入 release 下载链接,然后滚动到页面底部,看见下图 Xcodes.zip 点击下载,安装到 /Applications下即可。




使用教程


安装完成后,打开 Xcodes 的页面,非常简洁,能看到目前可安装的最新的 Beta 版本,以及最开始的1.0版本,看见这个觉得很酷 👻




使用 Xcodes 需要登录 Apple ID,以及怎么使用,都用图片说明吧,稍微摸索一下都能看明白,使用起来非常简单。






感谢阅读,如果您感觉这篇文章对您有帮助的话,请给它点赞以鼓励我持续创作 ^‿^


作者:心向暖阳
链接:https://juejin.cn/post/7271924341734555703
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

血压飙升!记一次关于手机号存储的前后端讨论

起 事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 : 涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。 本来是很简单的表单需求,结果出了幺蛾子。 承 对于前端来说,这就是两...
继续阅读 »


事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 :




涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。


本来是很简单的表单需求,结果出了幺蛾子。



对于前端来说,这就是两个字段,在表单传值和数据逻辑处理时,直接用两个字段无疑是最直接和简单的:

const formData = {
country_code: '86',
phone: '13345431234'
...
}

但是,产品侧有一个强需求:手机号和其他的两个字段要做唯一性校验,不但要求三个至少存在一个,而且还要查别的业务库的数据表来校验这个注册人员的唯一性,有一套复杂的逻辑。总结一下就是,后端分开两个字段来判断很麻烦,这个也好理解,有耦合的逻辑重构要成本的。所以后端是这么要求数据的:

// (86)13345431234
phone: `(${country_code})${phone}`

将国家码放在前缀括号里拼接为一个字符串。这样一来,前端的成本就相对高了一些,不但在表单提交时要拼接,而且在列表查询时,要按照括号匹配解析出前缀来动态显示

const regex = /^\((\d+)\)(\d+)$/;
const matches = phoneNumber.match(regex);
// 如果匹配成功,返回国家码和号码的数组
if (matches) {
const countrycode = matches[1];
const number = matches[2];
return [countrycode, number];
}

就算这样,也可以接受,之前也不止一次挖过这种自己拼接自己解析的坑了。但是随后就有点让人血压上升了。



由于业务扩展,上面的手机号,要传递给一款即时通讯软件 xxx 的 API 来做下发,下发侧则规定了,需要不带括号但是要有国家区号前缀的手机号码

// 8613345431234
phone: `${country_code}${phone}`

这就有问题了,我们直接对接的后端需要括号的,下发侧对接的后端同学不需要括号。


第一阶段


血压上升 20%


讨论让后端换成两个字段存储,这样更灵活,方便拼接;或者给下发侧发数据时去掉括号也可以。后端很为难:”唯一性判断还有其他的逻辑很复杂,有耦合性,拆开字段就要重构,工期又紧,刚完成就要改,误了工期只有我背锅;去掉括号不是不可以,但地方有点多,还要考虑批量数据的性能,而且已经进入测试了,不接受临时需求“。我一想有道理啊,就去问下发侧同学。


第二阶段


血压上升 60%


问下发侧对接的同学,可以处理后再给 API下发吗?果断拒绝。他们也有理由的:”按照软件设计的规范,我们不应该非标处理这个问题的,下发的规范理应就是我们接口的规范,因为规范还有很多,这个地方妥协了,以后肯定还会有更多的非标处理,我们的代码就乱了。“


我草,我一想之前那那么多屎山的由来,觉得更有理了,那怎么办?弄得好像前端这里没有理了。


第三阶段


🔥 血压上升 120% 🔥


下发侧对接的同学反过来给我们前端提了建议,让我们在注册的时候,把前端两个字段就直接不用括号来拼接给后端,在需要解析的时候,使用特别手段解析出国家码。还好心的给我们搜索了一个解析手机号的 js 库:




我只能说,我 TM 谢谢你。😭


前端莫名其妙的凭空多了需求,而且加个库为了给后端擦屁股,还增加了打包体积,出了问题我们得首先背锅,这肯定不行。



这起事件牵涉的技术问题并没有多难,甚至有些幼稚,但是却蕴含了不少上班族哲学,最主要的一点是:如何学会背锅的问题。锅不是不能背,但是背锅换来的应该是产品成功后更多的功劳回报,而不是这种暗箱操作,老板压根不知道,只有扛不住的人吃暗亏。


个人觉得,这个应该是架构师或者技术总监的任务,提前了解上下游业务,并确定好字段与数据结构,而不是等用的时候才发现数据对不上。规划好用什么方案来开发。并将方案设计交于老板开会讨论得出定稿。之后再分派人员开发,就不会有这个事件了。



类似的问题还有很多,前后端与下游计算价格进制与单位不统一,每次都要非标转换;



最后的结局,产品出面调停,软磨硬泡,最终后端妥协了,夹在前端和下发侧之间, 确实为难。针对前端还是原来的逻辑不动,使用括号存储,他在下发侧拉取数据的时候做转换,去掉括号。其中涉及的性能问题和开发成本也申请了延长工期。


Happy Ending !!😁


血压恢复 0%





方案设计注意事项:

  • 注意产品迁移的问题。同类型的产品,如果针对的客户群体不同,其表现形态可能完全不一样,产品迁移过程中,要提前分析出新产品的业务场景的差异,做好风险分析。
  • 提前确定产品上下游生态,做好风险管控。比如下发侧有 API 对接的,需熟悉其 API 文档与使用限制,并在服务不可用时设计告警方案等。

作者:小肚肚肚肚肚哦
链接:https://juejin.cn/post/7275576074589880372
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iPhone 14 被用户吐槽电池老化

iOS
国内要闻 香港高校陆续拥抱 ChatGPT,暑期忙于筹备新学期变革 香港众多高校拥抱了 OpenAI 的聊天机器人 ChatGPT。香港科技大学率先引入ChatGPT。6 月 1 日它正式为学生与教职人员提供港科大版 ChatGPT,是香港首所为学生与教职工提...
继续阅读 »



国内要闻


香港高校陆续拥抱 ChatGPT,暑期忙于筹备新学期变革


香港众多高校拥抱了 OpenAI 的聊天机器人 ChatGPT。香港科技大学率先引入ChatGPT。6 月 1 日它正式为学生与教职人员提供港科大版 ChatGPT,是香港首所为学生与教职工提供 ChatGPT 的大学。香港中文大学、香港理工大学、香港浸会大学等高校也陆续推出使用 AI 工具的指引,共同希望师生批判性探索和谨慎使用 AI。除了在高等教育掀起热潮,AI 也将进入香港的初中课堂。香港教育局指出,ChatGPT 可以成为有价值的教育工具,但要留意抄袭的伦理问题,并期望所有公立中学尽快规划,于 2023/24 学年在“资讯和通讯科技课程”中安排 10 至 14 小时的 AI 课程教授。(奇客Solidot)


小鹏智驾灵魂人物吴新宙确认离职


小鹏汽车董事长何小鹏发文称,因家庭和多方面的原因,小鹏汽车自动驾驶中心副总裁吴新宙在 2022 年下半年表示要回到美国。在此后 10 个月时间里,小鹏汽车确立全新的工作模式,并在架构和组织上进行了提前优化和迭代。负责 XNGP 项目的李力耘博士将接手自动驾驶团队。


据业内人士透露,吴新宙或将担任英伟达“全球副总裁”这一级别的职位,直接向黄仁勋汇报,“是黄仁勋本人亲自出马,将吴新宙招至麾下。”届时,吴新宙将成为全球知名公司的最高等级华人高管,并继续在芯片等多个方面和小鹏汽车深度合作。(雷锋网)


微信要做“小绿书”?知情人士:小范围内测,优化视频号图文发布及呈现


据网传消息,微信正在灰度测试“小绿书”。从知情人士处了解到,这是一次非常小范围的内测,不是新功能,初衷就是为了更方便视频号创作者发布图文短内容,以及提高用户获得信息的效率。(36氪)


OPPO IoT 事业群负责人李开新离职,电视业务几近裁撤


OPPO IoT 事业群负责人李开新离职,可能导致其电视业务几近裁撤。OPPO IoT 部门最近两年变动不断,一直在探索新的产品线。虽然 OPPO 在 IoT 方面也尝试过其他小品类,但较为稳定的业务还是耳机和可穿戴设备。近期有报道称 OPPO 将裁撤电视业务,但 OPPO 方面表示电视业务目前运营正常。


百度千帆接入 LLaMA2 等 33 个大模型


8 月 2 日,百度智能云宣布千帆大模型平台完成新一轮升级,全面接入LLaMA2全系列、ChatGLM2、RWKV、MPT 等 33 个大模型,成为国内拥有大模型最多的平台,接入的模型经过千帆平台二次性能增强,模型推理成本可降低50%。同时,上线 103 个预置 Prompt 模板,覆盖对话、游戏、编程、写作十余个场景,并发布多款全新插件。


国际要闻


iPhone 14 被用户吐槽电池老化


据报道,不少 iPhone 14 系列机主在社交媒体吐槽,该系列出现了严重的电池老化问题。iPhone 14 系列于 2022 年 9 月上市发售,首批用户持有时间还不到一年。社交网站上不少用户留言反馈称手机电池健康已经低于 90%,最多的跌到 87%。苹果官方对“电池健康”的描述为:包含最大电池容量和峰值性能容量。一般在手机电池正常使用的情况下,完整充电次数达到 500 次,电池健康的最大容量低于 80% 则会影响手机峰值性能,保修期内的 iPhone 可以得到官方保修甚至更换。(IT之家)


消息称 OpenAI 正测试第三代图片生成模型


OpenAI 在去年 4 月推出了第二代 DALL-E“文生图”模型,该模型凭借过硬的实力吸引了业界广泛注意,据外媒表示,OpenAI 日前正在准备下一代 DALL-E AI 模型(DALL-E 3),目前该公司正在进行一系列 Alpha 测试,而部分用户已经提早接触到了该 AI 模型。(财联社)


韩国室温超导团队称论文存在缺陷


韩国一研究团队近日发布论文称实现了室温超导,在引起全球广泛关注的同时,也遭到了质疑。而该研究团队的成员表示,论文存在缺陷,系团队中的一名成员擅自发布,目前团队已要求下架论文。分析师郭明錤认为,常温常压超导体商业化的时程并没有任何能见度,但未来若能够顺利商业化,将对计算器与消费电子领域的产品设计有颠覆性的影响。即便是小如iPhone的行动装置,都能拥有与量子计算机匹敌的运算能力。(财联社)


消息称苹果 Vision Pro 开发者实验室冷清,开发者兴趣不大


苹果公司在 7 月份开始邀请开发者去 Vision Pro 的开发者实验室,这些实验室分布在库比蒂诺、伦敦、慕尼黑、上海、新加坡和东京等城市,但是目前看来,开发者对这些实验室并没有表现出很大的兴趣。据彭博社的 Mark Gurman 报道,这些开发者实验室“参与人数不多,只有少量的开发者”。


AI 打败 AI:谷歌研究团队利用 GPT-4 击败 AI-Guardian 审核系统


8 月 2 日消息,谷歌研究团队正在进行一项实验,他们使用 OpenAI 的 GPT-4 来攻破其他 AI 模型的安全防护措施,该团队目前已经攻破 AI-Guardian 审核系统,并分享了相关技术细节。谷歌 Deep Mind 的研究人员 Nicholas Carlini 在一篇题为“AI-Guardian 的 LLM 辅助开发”的论文中,探讨了使用 GPT-4“设计攻击方法、撰写攻击原理”的方案。据悉,GPT-4 会发出一系列错误的脚本和解释来欺骗 AI-Guardian ,论文中提到,GPT-4 可以让 AI-Guardian 认为“某人拿着枪的照片”是“某人拿着无害苹果的照片”,从而让 AI-Guardian 直接放行相关图片输入源。谷歌研究团队表示,通过 GPT-4 的帮助,他们成功地“破解”了 AI-Guardian 的防御,使该模型的精确值从 98% 的降低到仅 8%。(IT之家)


程序员专区


KubeSphere 3.4.0 发布


致力于打造以 Kubernetes 为内核的云原生分布式操作系统 KubeSphere 3.4.0 发布,该版本带来了值得大家关注的新功能以及增强:扩大对 Kubernetes 的支持范围,最新稳定性支持 1.26;重构告警策略架构,解耦为告警规则与规则组;提升集群别名展示权重,减少原集群名称不可修改导致的管理问题;升级 KubeEdge 组件到 v1.13 等。同时,还进行了多项修复、优化和增强,更进一步完善交互设计,并全面提升了用户体验。


Firefox 116 发布


浏览器 Firefox 116 正式发布,该版本新增加了编辑现有文本注释的可能性、用户可以从操作系统复制任何文件并将其粘贴到 Firefox 中,开发方面,Firefox 现在支持 CSP3 external hashes,添加了对 dirname 属性的支持。具体可查看发布说明:http://www.mozilla.org/en-US/firef…


作者:小莫欢
链接:https://juejin.cn/post/7262900590936604730
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

“WWW” 仍然属于 URL 吗?它可以消失吗?

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是Google、Instagram和Facebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHub...
继续阅读 »

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是GoogleInstagramFacebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHubDuckDuckGoDiscord。该组织选择执行相反的操作并重定向 http://www.example.com 到 example.com



“WWW”属于 URL 吗?一些开发人员对此主题持有强烈的意见。在了解了一些历史之后,我们将探讨支持和反对它的论据。


WWW是什么?


WWW代表"World Wide Web",是上世纪80年代晚期的一个发明,引入了浏览器和网站。使用"WWW"的习惯源于给子域名命名的传统:


如果没有WWW会发生什么问题?


1. 向子域名泄露cookies


反对"没有WWW"的域名的批评者指出,在某些情况下,subdomain.example.com可以读取example.com设置的cookies。如果你是一个允许客户在你的域名上运营子域名的Web托管提供商,这可能是不希望看到的。


然而,这种行为只存在于Internet Explorer中。


RFC 6265标准化了浏览器对cookies的处理,并明确指出这种行为是错误的。


另一个潜在的泄露源是example.com设置的cookies的Domain值。如果Domain值明确设置为example.com,那么这些cookies也将被其子域名所访问。


总之,只要你不明确设置Domain值,而且你的用户不使用Internet Explorer,就不会发生cookie泄露。



2. DNS的困扰


有时,"没有WWW"的域名可能会使你的域名系统(DNS)设置复杂化。


当用户在浏览器的地址栏中输入example.com时,浏览器需要知道他们想访问的Web服务器的Internet协议(IP)地址。浏览器通过你的域名的域名服务器向其DNS服务器(通常间接通过用户的互联网服务提供商(ISP)的DNS服务器)请求IP地址。如果你的域名服务器配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。


在某些情况下,你可能希望使用规范名称(CNAME)记录来代替为你的网站设置。这样的记录可以声明http://www.example.comexample123.somecdnprovider.com的别名,这会告诉用户的浏览器去查找example123.somecdnprovider.com的IP地址,并将HTTP请求发送到那里。


请注意,上面的示例使用了一个WWW子域名。对于example.com,不可能定义一个CNAME记录。根据RFC 1912,CNAME记录不能与其他记录共存。如果你尝试为example.com定义CNAME记录,example.com上的MX(邮件交换)记录将无法存在。因此,就不可能在@example.com上接收邮件


一些DNS提供商可以让你绕过这个限制。Cloudflare称其解决方案为CNAME解析。通过这种技术,域名管理员配置一个CNAME记录,但他们的域名服务器将暴露一个A记录。


例如,如果管理员为example.com配置了指向example123.somecdnprovider.com的CNAME记录,并且存在一个指向1.2.3.4example123.somecdnprovider.com的A记录,那么Cloudflare就会暴露一个指向1.2.3.4的example.com的A记录。


总之,虽然这个问题对希望使用CNAME记录的域名所有者来说是有效的,但现在有一些DNS提供商提供了合适的解决办法。


没有WWW的好处


大部分反对WWW的论点是实用性或外观方面的。"无WWW"的支持者认为example.comhttp://www.example.com更容易说和输入(对于不那么精通技术的用户可能更不容易混淆)。


反对WWW子域名的人还指出,去掉它会带来一种谦虚的性能优势。网站所有者可以通过这样做每个HTTP请求节省4个字节。虽然这些节省对于像Facebook这样的高流量网站可能会累积起来,但带宽通常并不是一种紧缺的资源。


有"WWW"的好处


支持WWW的一个实际论点适用于使用较新顶级域的情况。例如,http://www.example.miamiexample.miami无法立即被识别为Web地址。对于具有诸如.com这样的可识别顶级域的网站,这不是一个太大的问题。


对搜索引擎排名的影响


目前的共识是你的选择不会影响你的搜索引擎表现。如果你希望从一个URL迁移到另一个URL,你需要配置永久重定向(HTTP 301)而不是临时重定向(HTTP 302)。永久重定向确保你旧的URL的SEO价值转移到新的URL。


同时支持两者的技巧


网站通常会选择example.comhttp://www.example.com作为官方网站,并为另一个配置HTTP 301重定向。理论上,可以支持http://www.example.com和example.com两者。但实际上,成本可能会超过效益。


从技术角度来看,你需要验证你的技术栈是否能够处理。你的内容管理系统(CMS)或静态生成的网站需要将内部链接输出为相对URL以保留访问者的首选主机名。除非你可以将主机名配置为别名,否则你的分析工具可能会将流量分别记录在两个主机名上。


最后,你需要采取额外的措施来保护你的搜索引擎表现。谷歌将把URL的"WWW""非WWW"版本视为重复内容。为了在其搜索索引中去重复内容,谷歌将显示它认为用户更喜欢的那个版本——不论是好是坏。


为了在谷歌中保持对自己的控制,建议插入规范链接标签。首先,决定哪个主机名将成为官方(规范)主机名。


例如,如果你选择了www.example.com,则必须在 https://example.com/my-article里的 <head> 上的标记 中插入以下代码段:

    <link href="<https://www.example.com/my-article>" rel="canonical"> 

这个代码片段告诉谷歌"无WWW"变体代表着相同的内容。通常情况下,谷歌会在搜索结果中偏好你标记为规范的版本,也就是在这个例子中的"WWW"变体。


总结


对于是否在URL中加入"WWW",人们有不同的观点。下面是支持和反对的论点:


支持"WWW"的论点:

  1. 存在子域名的安全性问题:某些情况下,子域名可以读取主域名设置的cookies。虽然这个问题只存在于Internet Explorer浏览器中,并且已经被RFC 6265标准化修复,但仍有人认为使用"WWW"可以避免潜在的安全风险。
  2. DNS配置的复杂性:如果你的域名系统(DNS)配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。但如果你想使用CNAME记录来设置规范名称,那么"没有WWW"的域名可能会导致一些限制,例如无法同时定义CNAME记录和MX(邮件交换)记录。
  3. 对搜索引擎排名的影响:对于使用较新顶级域的网站,使用"WWW"可以帮助识别网址,而不是依赖可识别的顶级域名。然而,目前的共识是选择是否使用"WWW"对搜索引擎表现没有直接影响。

支持去除"WWW"的论点:

  1. 实用性和外观:去除"WWW"可以使域名更简洁和易于输入,减少了用户可能混淆的机会。
  2. 节省字节:去除"WWW"可以每个HTTP请求节省4个字节。虽然这对于高流量网站来说可能是一个可累积的优势,但对于大多数网站来说,带宽通常不是一个紧缺的资源。

最佳实践:
一般来说,网站会选择将example.com或www.example.com作为官方网址,并对另一个进行重定向。你可以通过使用HTTP 301永久重定向来确保旧URL的SEO价值转移到新URL。同时,你还可以在页面的标签中插入规范链接标签,告诉搜索引擎两个URL代表相同的内容,以避免重复内容问题。


需要注意的是,在做决策时要考虑到技术栈的支持能力、DNS配置的限制和谷歌对搜索排名的处理方式。


本文同步我的技术文档


作者:狗头大军之江苏分军
链接:https://juejin.cn/post/7263274550074507321
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

现在小厂实习都问的这么难了吗😱「万物心选一面(北京+电商)(vue+小程序)」

h5是怎么和微信小程序通信的?不同H5页面怎么通信的?webview是谁提供的?web-view 是往小程序中嵌入h5的容器。会自动铺满整个小程序页面。微信小程序和 uni-app 都提供了 web-view 组件。由于简历上有一...
继续阅读 »

h5是怎么和微信小程序通信的?不同H5页面怎么通信的?webview是谁提供的?

  1. web-view 是往小程序中嵌入h5的容器。会自动铺满整个小程序页面。微信小程序和 uni-app 都提供了 web-view 组件。
  2. 由于简历上有一段实习是移动端h5开发,让面试官误以为我做过微信小程序的内嵌h5页面,但其实实习中都是公司框架写的代码,页面也是直接嵌入公司的app中的,我并没有微信小程序开发的经验,现在整理答案也是死记硬背,mark一下看到的一篇讲的比较清楚的文章,以后学小程序了再来回顾。
    微信小程序web-view与H5 通信方式探索 - 掘金 (juejin.cn)
  3. h5 页面间通信其实就是前端跨页面通信(吧?)当时第一反应回答的是使用LocalStorage, 面试官又提出用户修改个人信息后返回页面更新信息的情况,回答的是我之前做表单有类似的场景,是向后端提交后在后端更新了数据,回退到原先的页面的时候在 created/activated 的时候获取数据。
    面试官:前端跨页面通信,你知道哪些方法? - 掘金 (juejin.cn)

什么是微任务?什么是宏任务?


如果说哪些操作是宏任务,哪些操作是微任务,那大部分同学都是比较清楚的:

  • 宏任务:script(整体代码)/setTimout/setInterval/setImmediate(node 独有)/requestAnimationFrame(浏览器独有)/IO/UI render(浏览器独有)
  • 微任务:process.nextTick(node 独有)/Promise.then()/Object.observe/MutationObserver

这里面试官还追问了一句,Promise本身是微任务吗


那这两者有具体的定义吗?老规矩,直接 mdn 开搜。mdn 中可以找到微任务(microtask),但是并没有宏任务或者(macrotask)的信息。但是在在 JavaScript 中通过 queueMicrotask() 使用微任务 - Web API 接口参考 | MDN 中我们可以发现,在文档中只有taskmicrotask,对应的就是事件循环中的任务队列task queue和微任务队列microtask queue


文档中还提到了JavaScript 中的 promise 和 Mutation Observer API 都使用微任务队列去运行它们的回调函数,想来面试官的意思就是,Promise本身只是一个代理,Promise()是他的构造函数,真正被放进微任务队列的是Promise的then方法中的回调函数。


文档中对任务和微任务的定义也比较冗长,我想能区分哪些是微任务,哪些是宏任务,说出他们分别会被放在任务队列和微任务队列以及他们的执行顺序(事件循环会持续调用微任务直至队列中没有留存的,再去调用任务队列)应该足够面试了。




遍历对象有哪些方法,如果是对象的原型链上的属性会不会被遍历到?有什么办法可以排除原型链上的属性?


直接上代码测试一波:

Object.prototype.age=18;   // 修改Object.prototype  
const person ={ name: "小明" };

// 输出 name, age
for(key in person){
console.log(key)
}

// 输出 name
Object.keys(person).forEach(key=>{
console.log(key);
})

// 输出 小明, 18
for(key in person){
console.log(person[key])
}

// 输出 小明
Object.values(person).forEach(value=>{
console.log(value);
})

// 输出 name: 小明
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`);
}

很明显,for...in 会遍历到原型链上的属性,Object上的keysvaluesentires方法不会。
看看 mdn 怎么说:



for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性




Object.keys()  静态方法返回一个由给定对象 自身 的可枚举的字符串键属性名组成的数组。
Object.entries()  静态方法返回一个数组,包含给定对象 自有 的可枚举字符串键属性的键值对。


Object.values()  静态方法返回一个给定对象的 自有 可枚举字符串键属性值组成的数组。



那么如果仍然想使用 for...in 来遍历对象,并且不想要原型链上的属性,我们可以使用 Object.hasOwn 过滤掉它们:

for (key in person) {
if (person.hasOwn(key)) {
console.log(key);
}
}


如果指定的对象自身有指定的属性,则静态方法 Object.hasOwn()  返回 true。如果属性是继承的或者不存在,该方法返回 false



组件通信有哪些方法?依赖注入的数据是不是响应式的?有什么办法让他保持响应式的?

  • props / $emit
  • $emit / $on (eventBus)
  • provide / inject
  • $attrs / $listeners
  • ref / $ref
  • $parent / $children
  • vuex / pinia

Vue 组件间通信六种方式(完整版) - 掘金 (juejin.cn)


vue更新dom是异步还是同步?如何不使用nexttick实现nexttick的功能?vue的更新是哪一种微任务?


Vue更新DOM是异步的。这意味着我们在修改完data之后并不能立刻获取修改后的DOM元素。Vue需要通过nextTick方法才能获取最新的DOM。


Vue在调用Watcher更新视图时,并不会直接进行更新,而是把需要更新的Watcher加入到queueWatcher队列里,然后把具体的更新方法flushSchedulerQueue传给nextTick进行调用。nextTick只是单纯通过Promise、setTimeout等方法模拟的异步任务。


如果你想要不使用nextTick实现nextTick的功能,你可以使用Promise、setTimeout等方法来模拟异步任务。例如,你可以使用 Promise.resolve().then(callback) 或者 setTimeout(callback, 0) 来实现类似于nextTick的功能。


至于Vue的更新是哪一种微任务,它取决于浏览器兼容性。Vue会根据浏览器兼容性,选用不同的异步策略。例如,如果浏览器兼容Promise,那么Vue就会使用Promise来实现异步更新。如果浏览器不兼容Promise但兼容MutationObserver,那么Vue就会使用MutationObserver来实现异步更新。如果浏览器既不兼容Promise也不兼容MutationObserver,那么Vue就会使用setImmediatesetTimeout来实现异步更新。


vue能监听到数组的push方法吗?直接给响应式变量赋值一个新的数组会被监听到吗?


这里讨论的都是vue2vue3当中这些问题都已经被proxy解决了。



Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()


替换整个对象或数组就和操作其他类型的响应式数据没区别了,自然是可以检测到的。


深入响应式原理 — Vue.js


如果要把数组api,比如push pop这些都改成async、await的异步函数要怎么做?怎么拿到这些方法?怎么传参?


这一问是我当时最蒙的,我到现在都不确定我是否领悟对了他要问什么,大致上的理解如下,如果有大佬知道的可以在评论区教学一下我这个小菜鸟。

// 保存数组原型上的push方法
const originalPush = Array.prototype.push;
// 重写数组原型上的push方法
Array.prototype.push = async function (...args) {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 500));
return originalPush.apply(this, args);
}
async function test() {
const arr = [1, 2, 3];
await arr.push(4);
console.log(arr);
}
test(); // [1, 2, 3, 4],需要延迟定时器中设定的时间才能打印出来

在沸点一位大佬的提醒下,面试官可能想问的是这个JavaScript 异步数组 - 个人文章 - SegmentFault 思否


总结


总结就是一个字,菜。

虽然是问的有点细致,但基本上都只能回答上来每一问的第一问,后面的深入追问就懵逼了。原因是因为自己基本上都是直接对着面经和八股文准备的,没有实践过,也没有看过相关的文档。之后还是要坚持把JavaScript高级程序设计和vue设计与实现啃完,不说把这些问题记得滚瓜烂熟能对答如流,起码也要在在面试官引导下应该有思路。


作者:炫饭第一名
链接:https://juejin.cn/post/7275141653420425277
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

他扔出一张对话截图:王总说的

其实,我还在纠结,到底是写,还是不写。 也罢,给大家留作一个茶余饭后的谈资吧。如有雷同,纯属胡扯。 我在一家公司从事AI开发。我的身份是一个无法再底层的一线小开发。 产品总监又在群里贴出了那张对话截图。那是产研中心的王总跟他的对话,大体意思是王总要他组织一个同...
继续阅读 »

其实,我还在纠结,到底是写,还是不写。


也罢,给大家留作一个茶余饭后的谈资吧。如有雷同,纯属胡扯。


我在一家公司从事AI开发。我的身份是一个无法再底层的一线小开发。


产品总监又在群里贴出了那张对话截图。那是产研中心的王总跟他的对话,大体意思是王总要他组织一个同合作方的会议。


这个产品总监在这个群里,已经是第三次贴出这张截图了


第一次,他说要开一个公司层面的会。总监们纷纷发问:意义何在?


产品总监扔出这张截图:王总说的。


第二次,产品部的兄弟们有疑惑:开这个会的目的是什么?


产品总监第二次扔出这张截图:王总说的。


第三次,产品总监让我主讲这个会议。


我愣了:开会的目的是什么?


产品总监第三次扔出这张截图,表示:王总说的。


我说:不管哪个总说的,我都感觉意义不大。我可预料到过程,我说你们真好,对方说感谢夸奖,然后结束会议。


这个事情,还得从我的工作说起。我不仅从事AI项目的自研工作,同时也参与一些同第三方厂商的对接工作。因为公司的AI开发就我一个人。有些基础的AI能力,我们就打算买入第三方的服务。


因此,我的领导就安排我做调研工作。我先是调研国内AI行业的巨头。再调研垂直领域头部企业的AI开放能力。调研结束后,我将结果汇总,形成分析报告。然后,通过会议的形式,我把结果给直接领导讲了一遍。后来,直接领导拉来组内的产品,我又原样讲了第2遍。再后来,我的直接领导预约了他的直接领导,也就是我的二级领导,我又原样讲了第3遍。再后来,二级领导拉来公司所有的产品,我又原样讲了第4遍


再后来,已经不知是哪级领导,预约了给王总汇报,又安排我再原样讲第5遍。王总打断了我,他说,我不想知道你调研了多少家,以及哪家好哪家差,你辛苦那是你的工作,我不care这个。我想问,你的规划是什么?成本是多少?你将应用到哪些场景?能解决什么问题?创造多少收益?


同志们,记着我开头的声明,我是一个底层小员工。我最终还是没能回答上来。


会议结束之后,我和直接领导建议,是不是先让产品梳理一下我们到底有哪些AI需求。直接领导觉得,应该分两方面。第一,我们技术先自己想想,先按自己的想法走,这条路快。第二,慢慢地渗透给产品,让他们梳理一下,到底哪些场景会用到哪些AI能力,这条路可能要慢一些。


后来,王总主动安排下来一项任务。王总找到一家垂直行业的AI能力平台,想知道我们能不能用,好不好用。


最终这事,还是落到我的头上。我就把清单上的每一个接口,都做了调用和解析,并且采用可视化的形式来呈现结果。


我将结果给直接领导汇报了一次。结果就是,这次王总找的厂商,确实不错,带有行业加持,效果比之前我们找的都要好。直接领导找来产品总监和二级领导,我又原样讲了一遍


我的直接领导这次很机智,他想让产品梳理一下我们的产品,到底哪些地方会用到哪些AI能力。语音的能力要不要用?图像的能力具体怎么用?以便于我们技术可以进一步分析这些能力,到底能不能为我们所用。


再后来,就出现开头那一幕,产品总监安排我,同合作厂商再讲第3遍我的分析报告。并且他再次声明,那是王总安排的。


于是我就回复道:



不管哪个总安排的,这个会议意义不大。我只能说,你们的接口确实不错,他们也只能回复感谢支持。然后,尴尬结束。



因为,我们的产品规划,到底哪里用AI,用哪些AI,现在还是个空。


产品总监听到这里,很生气。他连发3条消息:



第一:到底他们的接口符不符合我们的业务场景,不符合要让他们整改,让它们攻克


第二:这绝不是一个你好我好的过场会


第三:请知悉。



群里,安静了一会儿。


我说。好吧,那我就以我自己梳理的往上靠吧。


会上,依然是我主讲。我又把已经讲了2遍的内容,讲了第3遍。我把他们每一个接口都做了分析,我表示这比之前调研的接口,效果都要好。这确实也是事实。


但是,具体我们能用吗?确实得先有产品规划,我才能确定是否能用到。为了避免成为“你好我好的过场会”,对于他们无法实现的,我提出质疑,他们说下一个版本会改好。


我马上记录下来,并确认:咱们下一个版本会改好的,对吧?


说完这句话,我收到一条钉钉消息。


产品总监发来的:来自王总的提醒,并不是我们给他们钱了,还没合作呢,不要有质问的语气!


我一看,好家伙,王总也参会了。一般提前预约都约不到的王总,居然悄悄参会了


我讲完了。王总发言说:我是中途赶来的。我想说,咱们这批接口是真的很好。你们接口的开放,是行业之大幸,对推动行业振兴很有帮助。对方说,王总太客气了,通过和咱们的交流,我们也有很大收获,也学到了很多知识。


我心想:这不还是一个你好我好的过场会。


会议结束了。


过了一会儿,我领导的领导打来电话,询问了会议的情况。最主要还是我群里发的那条:不管哪个总说的,对我来说,这个会意义不大


领导安慰我说,我估计你也是话赶话,赶到那里了。我断定你没有什么坏心眼,也不会故意使坏


我当时懵了一下。不知道他们领导层之间,到底是谁把什么消息,传播成了什么,上升到了什么层面。无职业素养地蔑视领导?以道德败坏形式破坏战略合作?王总的紧急出现,到底是巧合还是听到了什么风声?


不过,这些都无所谓,我只是一个底层小职员。我的职场生命力是最强的。我去80%的公司都可以再次成为一个小职员。


纵观整个过程。我们发现,一个企业的中层管理对企业起着至关重要的作用


第一次会议,王总不关注调研细节,这是没问题的。一个老总如果关心员工是如何进行调研的,反而是不称职。第二次会议,王总为公司找到好的资源,希望加强沟通,安排开会促进交流,这也是值得肯定的。


但是,对于每一个中层管理者,却不能让高层和基层进行100%的信息交换。尤其是向领导转发员工的截图,或者向员工转发领导的截图。


我经常看很多中层做类似的转发:给老板发某某员工抱怨公司的话,给员工发老板嫌弃员工不加班的截屏。这种行为很像是友商派来的内鬼。


大多数情况下,一个职场人了解自己的直接领导需要什么,但是不会很了解领导的领导的领导需要什么。一个领导多数理解直接下属怎么想,但是无法理解下属的下属怎么想。


每一位中层管理者,不管是上传和下达,都要做一次信息的过滤和加工。比如领导抱怨员工不加班,中层需要做的不是转发,而是加紧工作计划,说要提前上线,让员工忙起来。你要一说就是老板要看加班,还排好张三加二四六,李四加一三五,那两头都得气疯了。


我的两个例子就是个反面教材。


其实,我不需要直接给王总汇报。我至多向上汇报两级(如果他们真的非要分那么多级)。某级领导结合王总的近期规划,甚至最近的心情,去做一次简要汇报。而对于同一件事情来说,如果一个基层员工参会的次数,远超过领导参会的次数,这可能是一个预警。它表示,中层管理者根本没有加工信息,完全走转发路线。


王总安排给产品总监的会议,后来我也发现其实是高层之间的会议。安排我去参加确实意义不大。让我主讲更不可取。因为我了解的信息太少了,哪个叫张总,哪个是孙总,他们之间是什么样的商业关系,他们相互间的地位如何。如果非让我参加,应当是提前打好招呼,并且把我安排到殿外侯旨,问我时我再回答。


这些,基本上都得需要中层管理者来考虑。


对于上传下达,能做好过滤和加工,这样的中层是伟大的。啥也不做,这样的中层很难成长。添油加醋,煽风点火,这样的中层不予评价。


我碰到的产品总监是个聪明人,不在以上之列。他从一开始往外放对话截图,其实就表明了态度:其实我也不想开这会,但是领导非要开,还安排给我,大家配合一下,就混过去了。


但是,走到我这儿,我却发了一个牢骚。我感觉,第一,你不愿开你就跟领导直说,愿意开就用心安排,那是你的直接领导。第二,你给我一个会议号就完了,你这不是让我配合,是完全转交给我了呀。


再反过来讲,这会议真的没有意义吗?来了好的业务资源,我们不该去把握住吗?怎么一件好事,最后落得人人都不爽的地步。我抢了你的钱,局面是我赢你输。但是,一个事情搞得大家都输的情况,也是很难的。


活,还是我干了,事儿我也惹了。我始终还是没能当成一个,传统意义上,让你开会你就开会,哪儿那么多废话的俗人。可能这世界很需要俗人。


最后,奉劝大家在公司少发表意见。尤其和领导沾边的言论。


作者:TF男孩
链接:https://juejin.cn/post/7180517435307458617
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从《孤注一掷》出发,聊聊 SSL 证书的重要性

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。  图片来源于电影《孤注一掷》 这部电影除...
继续阅读 »

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。 


 图片来源于电影《孤注一掷》


这部电影除了让人后背发凉外,也不禁让人回忆起了曾经上网冲浪遇到的种种现象:看小说时性感荷官总在网页右下角在线发牌;看电影时网页左下角常常蹦出“在线老虎机”……这些让人烦不胜烦的广告弹窗之所以出现,要么是建站人员利欲熏心投放了非法广告,要么就是因为网站使用了不安全的 HTTP 协议而遭到了攻击,正常的网页内容被恶意篡改。


网站是电信诈骗、网络赌博等非法内容出现的重灾区,建站者和使用者都应该提高安全意识,特别是对建站者来说,保护通信安全才能更好的承担起建站责任。本文将从 HTTP 讲起,介绍 HTTPS 保护通信安全的原理,以及作为网络通信安全基石的 SSL 证书的重要性。


HTTP 协议


HTTP(Hyper Text Transfer Protocol)协议是超文本传输协议。它是从 WEB 服务器传输超文本标记语言(HTML)到本地浏览器的传送协议。HTTP 基于 TCP/IP 通信协议来传递数据,通信双方在 TCP 握手后即可开始互相传输 HTTP 数据包。具体过程如下图所示: 


 HTTP 建立流程


HTTP 协议中,请求和响应均以明文传输。如下图所示,在访问一个使用 HTTP 协议的网站时,通过抓包软件可以看到网站 HTTP 响应包中的完整 HTML 内容。




虽然 HTTP 明文传输的机制在性能上带来了优势,但同时也引入了安全问题:

  • 缺少数据机密性保护。HTTP 数据包内容以明文传输,攻击者可以轻松窃取会话内容。
  • 缺少数据完整性校验。通信内容以明文传输,数据内容可被攻击者轻易篡改,且双方缺少校验手段。
  • 缺少身份验证环节。攻击者可冒充通信对象,拦截真实的 HTTP 会话。

HTTP 劫持


作为划时代的互联网通信标准之一,HTTP 协议的出现为互联网的普及做出了不可磨灭的贡献。但正如上节谈到, HTTP 协议因为缺少加密、身份验证的过程导致很可能被恶意攻击,针对 HTTP 协议最常见的攻击就是 HTTP 劫持。


HTTP 劫持是一种典型的中间人攻击。HTTP 劫持是在使用者与其目的网络服务所建立的数据通道中,监视特定数据信息,当满足设定的条件时,就会在正常的数据流中插入精心设计的网络数据报文,目的是让用户端程序解析“错误”的数据,并以弹出新窗口的形式在使用者界面展示宣传性广告或直接显示某网站的内容。


下图是一种典型的 HTTP 劫持的流程。当客户端给服务端发送 HTTP 请求,图中发送请求为“梁安娜的电话号码是?”,恶意节点监听到该请求后将其放行给服务端,服务端返回正常 HTML 响应,关键返回内容本应该是“+86 130****1234”,恶意节点监听到该响应,并将关键返回内容篡改为泰国区电话“+66 6160 *88”,导致用户端程序展示出错误信息,这就是 HTTP 劫持的全流程。



 HTTP 劫持流程


例如,在某网站阅读某网络小说时,由于该网站使用了不安全的 HTTP 协议,攻击者可以篡改 HTTP 相应的内容,使网页上出现与原响应内容无关的广告,引导用户点击,可能将跳转进入网络诈骗或其他非法内容的页面。



 原网页



 HTTP 劫持后网页


HTTPS 工作原理


HTTPS 协议的提出正是为了解决 HTTP 带来的安全问题。HTTPS 协议(HyperText Transfer Protocol Secure,超文本传输安全协议),是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 的开发主要是提供对网站服务器的身份认证,保护交换资料的隐私性与完整性。


TLS 握手是 HTTPS 工作原理的安全基础部分。TLS 传统的 RSA 握手流程如下所示:



 TLS 握手流程


TLS 握手流程主要可以分为以下四个部分:


第一次握手:客户端发送 Client Hello 消息。该消息包含:客户端支持的 SSL/TLS 协议版本(如 TLS v1.2 );用于后续生成会话密钥的客户端随机数 random_1;客户端支持的密码套件列表。


第二次握手:服务端收到 Client Hello 消息后,保存随机数 random_1,生成随机数 random_2,并发送以下消息。

  • 发送 Server Hello 消息。该消息包含:服务端确认的 SSL/TLS 协议版本(如果双方支持的版本不同,则关闭加密通信);用于后续生成会话密钥的服务端随机数 random_2;服务端确认使用的密码套件
  • 发送“Server Certificate”消息。该消息包含:服务端的 SSL 证书。SSL 证书又包含服务端的公钥、身份等信息。
  • 发送“Server Hello Done”消息。该消息表明 ServerHello 及其相关消息的结束。发送这个消息之后,服务端将会等待客户端发过来的响应。

第三次握手:客户端收到服务端证书后,首先验证服务端证书的正确性,校验服务端身份。若证书合法,客户端生成预主密钥,之后客户端根据(random_1, random_2, 预主密钥)生成会话密钥,并发送以下消息。

  • 发送“Client Key Exchange”消息,该消息为客户端生成的预主密钥,预主密钥会被服务端证书中的公钥加密后发送。
  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。
  • 发送“Encrypted Handshake Message”消息,表示客户端的握手阶段已经结束。客户端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给服务端,供服务端校验。

第四次握手:服务端收到客户端的消息后,利用自己的服务端证书私钥解密出预主密钥,并根据(random_1, random_2, 预主密钥)计算出会话密钥,之后发送以下消息。

  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。
  • 发送“Encrypted Handshake Message”,表示服务端的握手阶段已经结束,同时服务端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给客户端,供客户端校验。

根据 TLS 握手流程,可以看出它是如何解决 HTTP 协议缺陷,以及避免中间人攻击的:


1.规避窃听风险,攻击者无法获知通信内容


在客户端进行真正的 HTTPS 请求前,客户端与服务端都已经拥有了本次会话中用于加密的对称密钥,后续双方 HTTPS 会话中的内容均用该对称密钥加密,攻击者在无法获得该对称密钥的情况下,无法解密获得会话中内容的明文。即使攻击者获得了 TLS 握手中双方发送的所有明文信息,也无法从这些信息中恢复对称密钥,这是由大数质因子分解难题和有限域上的离散对数难题保证的。


2.规避篡改风险,攻击者无法篡改通信内容


在数据通信阶段,双端消息发送时会对原始消息做一次哈希,得到该消息的摘要后,与加密内容一起发送。对端接受到消息后,使用协商出来的对称加密密钥解密数据包,得到原始消息;接着也做一次相同的哈希算法得到摘要,对比发送过来的消息摘要和计算出的消息摘要是否一致,可以判断通信数据是否被篡改。


3.规避冒充风险,攻击者无法冒充身份参与通信


在 TLS 握手流程中的第二步“Server Hello”中,服务端将自己的服务端证书交付给客户端。客户端拿到 SSL 证书后,会对服务端证书进行一系列校验。以浏览器为例,校验服务端证书的过程为:

  • 验证证书绑定域名与当前域名是否匹配。
  • 验证证书是否过期,是否被吊销。
  • 查找操作系统中已内置的受信任的证书发布机构 CA(操作系统会内置有限数量的可信 CA),与服务端证书中的颁发者 CA 比对,验证证书是否为合法机构颁发。如果服务端证书不是授信 CA 颁发的证书,则浏览器会提示服务端证书不可信。
  • 验证服务端证书的完整性,客户端在授信 CA 列表中找到服务端证书的上级证书,后使用授信上级证书的公钥验证服务端证书中的签名哈希值。
  • 在确认服务端证书是由国际授信 CA 签发,且完整性未被破坏后,客户端信任服务端证书,也就确认了服务端的正确身份。

SSL 证书


正如上一节介绍,SSL 证书在 HTTPS 协议中扮演着至关重要的作用,即验证服务端身份,协助对称密钥协商。只有配置了 SSL 证书的网站才可以开启 HTTPS 协议。在浏览器中,使用 HTTP 的网站会被默认标记为“不安全”,而开启 HTTPS 的网站会显示表示安全的锁图标。



 使用 HTTP 协议的网站



 使用 HTTPS 协议的网站


从保护范围、验证强度和适用类型出发, SSL 证书会被分成不同的类型。只有了解类型之间的区别,才能根据实际情况选择更适合的证书类型,保障通信传输安全。


从保护范围分,SSL 证书可以分为单域名证书、通配符证书、多域名证书。

  • 单域名证书:单域名证书只保护一个域名,这些域名形如 http://www.test.com 等。
  • 通配符证书:通配符证书可以保护基本域和无限的子域。通配符 SSL 证书的公用名中带有星号 ,其中,星号表示具有相同基本域的任何有效子域。例如,。test.com 的通配符证书可用于保护 a.test.com、 b.test.com……
  • 多域名证书:多域证书可用于保护多个域或子域。包括完全唯一的域和具有不同顶级域的子域(本地/内部域除外)的组合。

从验证强度和适用类型进一步区分,SSL 证书可以分为 DV、OV、EV 证书。

  • DV(Domain Validated):域名验证型。在颁发该类型证书时,CA 机构仅验证申请者对域名的所有权。CA 机构会通过检查 WHOIS、DNS 的特定记录来确认资格。一般来说,DV 证书适用于博客、个人网站等不需要任何私密信息的网站。
  • OV(Organization Validated):组织验证型。OV 证书的颁发除了要验证域名所有权外,CA 还会额外验证申请企业的详细信息(名称、类型、地址)等。一般来说,OV 证书适用于中级商业组织。
  • EV(Extended Validation):扩展验证型。EV 证书的颁发除了 CA 对 DV 和 OV 证书所采取的所有身份验证步骤之外,还需要审查商业组织是否在真实运营、其实际地址,并致电以验证申请者的就业情况。一般来说,EV 证书适用于顶级商业组织。

结尾


随着互联网应用的普及,网络诈骗的方式也越发花样百出,让人防不胜防。


除了文内提到的网页环境,在软件应用、邮件、文档、物联网等领域同样存在恶意软件、钓鱼邮件、文档篡改、身份认证的问题。幸运的是,作为 PKI 体系下的优秀产品,证书体系同样在这些领域发挥着重要作用,软件签名证书、邮件签名证书、文档签名证书、私有证书等保护着各自领域的信息安全。


总有不法分子企图通过漏洞牟利,而证书体系在保护数据机密性、完整性、可用性以及身份验证场景上有着无可取代的地位,牢牢守护着用户信息,保障通信安全。


推荐活动


火山引擎域名与网站特惠活动来啦,欢迎访问火山引擎官网抢购!


5 折抢购 SSL 证书、1 元注册/转入域名、1 元升级 DNS 专业版、HTTPDNS 资源包 1 折起火热进行中……


此外,火山引擎已新推出:

  • 私有 CA(Private CA/PCA),通过私有证书灵活标识和保护企业内部资源和数据
  • 商标服务,专业、高效的商标注册管理服务平台
  • 私网解析 PrivateZone,灵活构建 VPC 内的私网域名系统
  • 公共解析PublicDNS,快速安全的递归DNS,永久免费
  • 域名委托购买服务,0元下单即可尝试获取心仪域名

关于火山引擎边缘云:
火山引擎边缘云,以云原生技术为基础底座,融合异构算力和边缘网络,构建在大规模边缘基础设施之上的云计算服务,形成以边缘位置的计算、网络、存储、安全、智能为核心能力的新一代分布式云计算解决方案。


作者:火山引擎边缘云
链接:https://juejin.cn/post/7273685263841263672
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

ChatGPT明知自己写代码有漏洞,但你不问它就不说

萧箫 发自 凹非寺 量子位 | 公众号 QbitAI ChatGPT知道自己写的代码有 漏洞,但它不说! 来自加拿大的一项最新研究发现,ChatGPT生成的代码中,有不少都存在安全漏洞。 然而在被要求评估自己代码的安全性时,ChatGPT却很快发现了这些代码中...
继续阅读 »

萧箫 发自 凹非寺


量子位 | 公众号 QbitAI


ChatGPT知道自己写的代码 漏洞,但它不说!


来自加拿大的一项最新研究发现,ChatGPT生成的代码中,有不少都存在安全漏洞。


然而在被要求评估自己代码的安全性时,ChatGPT却很快发现了这些代码中的漏洞,并给出了一些解决方案和建议。




这意味着它并不知道自己生成了糟糕的代码,但却查得出它写的代码有漏洞,也有能力修复这些漏洞


而在另一篇来自斯坦福的论文中,研究人员测试了另一位著名AI程序员Copilot,也发现了类似的问题。


所以,用AI生成代码为啥会出现这种状况?


写的程序中76%有安全漏洞


研究人员试着让ChatGPT生成了21个程序。


整个测试过程如下,先提交需求给ChatGPT,生成相关代码,再对这些代码进行测试,并检查问题、潜在的缺陷和漏洞等。




研究人员给ChatGPT提了包括C++、C、Java和Python在内的21个写代码需求,这是评估的结果:




统计表明,ChatGPT生成的21个程序中,有17个能直接运行,但其中只有5个程序能勉强通过程序安全评估,不安全代码率达到76%以上。


于是,研究人员先试着让ChatGPT“想想自己生成的代码有啥问题”。


ChatGPT的回应是“没啥问题”:只要用户每次的输入都是有效的,那么程序一定能运行!


显然ChatGPT并没有意识到,用户并不都是行业专家,很可能只需要一个无效输入,就能“引炸”它写的程序:




发现ChatGPT不知道自己写的程序不安全后,研究人员尝试换了种思路——用更专业的语言提示ChatGPT,如告诉它这些程序具体存在什么漏洞。


神奇的是,在听到这些针对安全漏洞的专业建议后,ChatGPT立刻知道自己的代码存在什么问题,并快速纠正了不少漏洞。


经过一番改进后,ChatGPT终于将剩余的16个漏洞程序中的7个改得更安全了。


研究人员得出结论认为,ChatGPT并不知道自己的代码中存在安全漏洞,但它却能在生成程序后识别其中的漏洞,并尝试提供解决方案。


论文还指出,ChatGPT虽然能准确识别并拒绝“写个攻击代码”这种不道德的需求,然而它自己写的代码却有安全漏洞,这其实有着设计上的不合理之处。


我们试了试发现,ChatGPT确实会主动拒绝写攻击性代码的要求:




大有一种“我不攻击别人,别人也不会攻击我写的代码”自信感。


程序员们在用它辅助写代码的时候,也需要考虑这些问题。


Copilot也存在类似问题


事实上,不止ChatGPT写的代码存在安全问题。


此前,斯坦福大学的研究人员对Copilot也进行过类似调查,只不过他们探查的是用Copilot辅助生成的程序,而并非完全是Copilot自己写的代码。


研究发现,即便Copilot只是个“打辅助”的角色,经过它改写的代码中,仍然有40% 出现了安全漏洞。




而且研究只调查了Copilot生成代码中的一部分,包括C、Python和Verilog三种编程语言写的程序,尚不知道用其他语言编写的程序中,是否还存在更多或更少的安全漏洞。


基于此,研究人员得出了如下结论:



ChatGPT等AI生成的代码安全性并不稳定,用某些语言写的代码比较安全,而用其他语言写的代码却很容易遭受攻击。整体来看,它们就是一个黑盒子,生成的代码是有风险的。


这并不意味着AI代码工具不能用,只是我们在使用时,必须考虑这些代码的安全性。



作者介绍


四位作者均来自加拿大魁北克大学(Universite du Quebec en Outaouais)。




Raphaël Khoury,加拿大魁北克大学教授,曾经在拉瓦尔大学获得计算机学士、硕士和博士学位,研究兴趣集中在计算机安全方面。




Anderson R. Avila,魁北克大学国立科学研究院的助理教授,主要研究方向是数据隐私相关的联邦学习、网络安全和生物特征识别技术等,曾经在华为的加拿大研究院做过机器学习研究员。


作者Jacob Brunelle和Baba Mamadou Camara也都来自加拿大魁北克大学。


你用ChatGPT写过代码吗?感觉它的“安全意识”如何?


论文地址:

arxiv.org/abs/2304.09…


生成代码数据集:

github.com/RaphaelKhou…


—  —


作者:量子位
链接:https://juejin.cn/post/7225565801792913466
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

每个人都深陷自己的信息茧房当中

平时没事,喜欢观察观察路人,猜测一下他(她)是干什么的,是什么样的生活状态。同时,也会作为旁观着看看别人的为人处世、观点、思想等,用于自我学习。 但这些也仅仅是作为上帝视角,看一看,听一听,想一想。无论对错,都不会干预,那毕竟是别人的事。但从这些观察之中,还是...
继续阅读 »

平时没事,喜欢观察观察路人,猜测一下他(她)是干什么的,是什么样的生活状态。同时,也会作为旁观着看看别人的为人处世、观点、思想等,用于自我学习。


但这些也仅仅是作为上帝视角,看一看,听一听,想一想。无论对错,都不会干预,那毕竟是别人的事。但从这些观察之中,还是能够学到很多。


时间久了,你会发现,几乎所有的人都陷入在自己构建的信息茧房当中。几乎所有的行为和决策都是过往经历的延续和习惯依赖。


聊聊生活中的几件事:


第一,何不食肉糜


在《晋书·惠帝纪》中记录着这样一个故事:


晋代有一年发生饥荒,百姓没有粮食吃,只能吃草根和树皮,甚至活活饿死。晋惠帝听了奏报,大为不解,问:他们为什么不吃肉粥呢?


晋惠帝也正因为这一句话,被后人所记住。


他的回答看似非常荒诞,但其实我们在日常生活中,大多数时候都在做着类似的事情,特别是给别人提建议的时候。


第二,手中的锤子


曾经有一段项目经历,由于是创新项目,需要大家集思广益。团队成员也来自各个行业,比如做游戏的、做二次元的、做漫画的等。


在最终碰创意时,你会发现,每个人的创意都深深刻着他过往的经历和行业经验,每个人都觉得自己的观点和想法是正确的,是无懈可击的。而在旁观者或者没有相关经验的人看来,多少有些不可思议。


有时候,你会发现,自己认为很简单的事情,给别人解释半天,别人依旧没有懂。这可能是你讲解的不好,也可能是对方没有大量相关知识的铺垫,导致无法理解那个“简单的结论”。


这里有知识的诅咒,也有信息的茧房。知识的诅咒让你想象不到不知道这一知识的人会怎么想,而信息的茧房导致你只认可你自己所“筛选”下来的信息。


第三,不要让别人推荐书


一直有很多朋友想读书,让推荐书籍。其实这个事非常难做,因为不知道你喜欢什么,也不知道是否适合你。


每个人的知识背景不同,同样一本书,有的人读起来可能刚刚好,但有的人读起来则十分苦难,味同嚼蜡。


其实,刚开始读书,去看那些自己感兴趣、又刚好能读懂的书,让兴趣、难度、需求同时匹配到舒适区边缘,这样的书肯定会让你读得津津有味。


第四,关于恶评


凡是在网络上发表过文章的人几乎都会遇到一件事:喷子+恶评。


这里分两类,一类单纯是为了发泄情绪的攻击,无其他内容。这一类,要么无视,要么哈哈大笑即可。这说明对方不但嫉妒你,还不如你,因为他没法拿出更好的作品或观点来回应,只会发泄情绪、肆意谩骂。


另外一类看似言之凿凿,但你会发现,他陷入了深深的信息茧房之中。比如他心中所想,生活所遇皆是消极、邪恶之念,那么你的想法、说辞、行为再正面,他也能用他邪恶负面的想法包装一番,形成逻辑自洽。然后用这些观点来抨击、谩骂和恶评。


针对这些人,他的每一个字都透露着他思想的狭隘,何必与他计较呢。如果一个人说三七得四十八,你还去跟他争辩,那就是你的不对了。


每个人所说的话,所看到的世界,都不是这个世界的客观存在,而只是内心对现实世界的反映,对这个世界认知的呈现而已。所以那些恶评,除了彰显、铭记他的无知之外,别无他用,何必计较。


最后的小结


不可否认,我们每个人对这个世界都有着不同的认知,而这些认知往往都来源于过往的经历。这些经历塑造了现在的我们,塑造了我们对世界的看法,影响着我们的决策。同时,它们也将会被我们的决策和行动进一步强化。


而要打破这些认知的茧房,更多的是需要多看看不同行业,不同领域,不同视角,不阶层,不同角色的看法。拥有了更多的视角,甚至相互不兼容的多个视角,才能更好的跳出茧房,更清晰的看到这个世界的客观存在。


作者:程序新视界
来源:juejin.cn/post/7271896547595403324
收起阅读 »

电视剧里的代码真能运行吗?

大家好,欢迎来到 Crossin的编程教室 ! 前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道 ,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程...
继续阅读 »

大家好,欢迎来到 Crossin的编程教室 !


前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道


,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程序画一个爱心的桥段。


于是出于好奇,Crossin就去看了这一集(第5集,不用谢)。这一看不要紧,差点把刚吃的鸡腿给喷出来--槽点实在太多了!


忍不住做了个欢乐吐槽向的代码解读视频,在某平台上被顶到了20个w的浏览,也算蹭了一波人家电视剧的热度吧……


下面是图文版,给大家分析下剧中出现的“爱心”代码,并且来复刻一下最后男主完成的酷炫跳动爱心。


剧中代码赏析


1. 首先是路人同学的代码:



虽然剧中说是“C语言期中考试”,但这位同学的代码名叫 draw2.py,一个典型的 Python 文件,再结合截图中的 pen.forward、pen.setpos 等方法来看,应该是用 turtle 海龟作图库来画爱心。那效果通常是这样的:


import turtle as t
t.color('red')
t.setheading(50)
t.begin_fill()
t.circle(-100, 170)
t.circle(-300, 40)
t.right(38)
t.circle(-300, 40)
t.circle(-100, 170)
t.end_fill()
t.done()



而不是剧中那个命令行下用1组成的不规则的图形。


2. 然后是课代表向路人同学展示的优秀代码:



及所谓的效果:



这确实是C语言代码了,但文件依然是以 .py 为后缀,并且 include 前面没有加上 #,这显然是没法运行的。


里面的内容是可以画出爱心的,用是这个爱心曲线公式:



然后遍历一个15*17的方阵,计算每个坐标是在曲线内还是曲线外,在内部就输出#或*,外部就是-


用python改写一下是这样的:


for y in range(9, -6, -1):
for x in range(-8, 9):
print('*##*'[(x+10)%4] if (x*x+y*y-25)**3 < 25*x*x*y*y*y else '-', end=' ')
print()

效果:



稍微改一下输出,还能做出前面那个全是1的效果:


for y in range(9, -6, -1):
for x in range(-8, 9):
print('1' if (x*x+y*y-25)**3 < 25*x*x*y*y*y else ' ', end=' ')
print()


但跟剧中所谓的效果相去甚远。


3. 最后是主角狂拽酷炫D炸天的跳动爱心:



代码有两个片段:




但这两个片段也不C语言,而是C++,且两段并不是同一个程序,用的方法也完全不一样。


第一段代码跟前面一种思路差不多,只不过没有直接用一条曲线,而是上半部用两个圆形,下半部用两条直线,围出一个爱心。



改写成 Python 代码:


size = 10
for x in range(size):
for y in range(4*size+1):
dist1 = ((x-size)**2 + (y-size)**2) ** 0.5
dist2 = ((x-size)**2 + (y-3*size)**2) ** 0.5
if dist1 < size + 0.5 or dist2 < size + 0.5:
print('V', end=' ')
else:
print(' ', end=' ')
print()

for x in range(1, 2*size):
for y in range(x):
print(' ', end=' ')
for y in range(4*size+1-2*x):
print('V', end=' ')
print()

运行效果:



第二段代码用的是基于极坐标的爱心曲线,是遍历角度来计算点的位置。公式是:



计算出不同角度对应的点坐标,然后把它们连起来,就是一个爱心。


from math import pi, sin, cos
import matplotlib.pyplot as plt
no_pieces = 100
dt = 2*pi/no_pieces
t = 0
vx = []
vy = []
while t <= 2*pi:
vx.append(16*sin(t)**3)
vy.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt
plt.plot(vx, vy)
plt.show()

效果:



代码中循环时用到的2π是为了保证曲线长度足够绕一个圈,但其实长一点也无所谓,即使 π=100 也不影响显示效果,只是相当于同一条曲线画了很多遍。所以剧中代码里写下35位小数的π,还被女主用纸笔一字不落地抄写下来,实在是让程序员无法理解的迷惑行为。



但不管写再多位的π,上述两段代码都和最终那个跳动的效果差了五百只羊了个羊。


跳动爱心实现


作为一个总是在写一些没什么乱用的代码的编程博主,Crossin当然也不会放过这个机会,下面就来挑战一下用 Python 实现最终的那个效果。


1. 想要绘制动态的效果,必定要借助一些库的帮助,不然代码量肯定会让你感动得想哭。这里我们将使用之前 羊了个羊游戏 里用过的 pgzero 库。然后结合最后那个极坐标爱心曲线代码,先绘制出曲线上离散的点。


import pgzrun
from math import pi, sin, cos

no_p = 100
dt = 2*3/no_p
t = 0
x = []
y = []
while t <= 2*3:
x.append(16*sin(t)**3)
y.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt

def draw():
screen.clear()
for i in range(len(x)):
screen.draw.filled_rect(Rect((x[i]*10+400, -y[i]*10+300), (4, 4)), 'pink')

pgzrun.go()


2. 把点的数量增加,同时沿着原点到每个点的径向加一个随机数,并且这个随机数是按照正态分布来的(半个正态分布),大概率分布在曲线上,向曲线内部递减。这样,就得到这样一个随机分布的爱心效果。


...
no_p = 20000
...
while t <= 2*pi:
l = 10 - abs(random.gauss(10, 2) - 10)
x.append(l*16*sin(t)**3)
y.append(l*(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)))
t += dt
...


3. 下面就是让点动起来,这步是关键,也有一点点复杂。为了方便对于每个点进行控制,这里将每个点自定义成了一个Particle类的实例。


从原理上来说,就是给每个点加一个缩放系数,这个系数是根据时间变化的正弦函数,看起来就会像呼吸的节律一样。


class Particle():
def __init__(self, pos, size, f):
self.pos = pos
self.pos0 = pos
self.size = size
self.f = f

def draw(self):
screen.draw.filled_rect(Rect((10*self.f*self.pos[0] + 400, -10*self.f*self.pos[1] + 300), self.size), 'hot pink')

def update(self, t):
df = 1 + (2 - 1.5) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df

...

t = 0
def draw():
screen.clear()
for p in particles:
p.draw()

def update(dt):
global t
t += dt
for p in particles:
p.update(t)


4. 剧中爱心跳动时,靠中间的点波动的幅度更大,有一种扩张的效果。所以再根据每个点距离原点的远近,再加上一个系数,离得越近,系数越大。


class Particle():
...
def update(self, t):
df = 1 + (2 - 1.5 * self.f) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df


5. 最后再用同样的方法画一个更大一点的爱心,这个爱心不需要跳动,只要每一帧随机绘制就可以了。


def draw():
...
t =
0
while t < 2*pi:
f = random.gauss(1.1, 0.1)
x = 16*sin(t)**3
y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
size = (random.uniform(0.5,2.5), random.uniform(0.5,2.5))
screen.draw.filled_rect(Rect((10*f*x + 400, -10*f*y + 300), size), 'hot pink')
t += dt * 3


合在一起,搞定!



总结一下,就是在原本的基础爱心曲线上加上一个正态分布的随机量、一个随时间变化的正弦函数和一个跟距离成反比的系数,外面再套一层更大的随机爱心,就得到类似剧中的跳动爱心效果。


但话说回来,真有人会在考场上这么干吗?


除非真的是超级大学霸,不然就是食堂伙食太好--


吃太饱撑的……



代码已开源:https://gitee.com/crossin/easy-py/tree/master/221114%20%E7%88%B1%E5%BF%83%E4%BB%A3%E7%A0%81



作者:Crossin先生
来源:juejin.cn/post/7168388057631031332
收起阅读 »

一条SQL差点引发离职

排除一切不可能的,剩下的即使再不可能,那也是真相” 背景        最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。 本来是一个很简单的逻辑,就...
继续阅读 »

排除一切不可能的,剩下的即使再不可能,那也是真相”



背景


       最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。

本来是一个很简单的逻辑,就是根据唯一的id去更新对应的MySQL数据,代码简化后如下:


var updates []*model.Goods
for id, newGoods := range update {
 if err := model.GetDB().Model(&model.Goods{}).Where("id = ?", id).Updates(map[string]interface{}{
  "selling_price":  newGoods.SellingPrice,
  "sell_type":      newGoods.SellType,
  "status":         newGoods.Status,
  "category_id":    newGoods.CategoryID,
 }).Error; err != nil {
  return nil, err
 }
}

很明显,updates[]model.Goods\color{red}{updates []*model.Goods}本来应该是想声明为 map[string]model.Goods\color{red}{map[string]*model.Goods}类型的,然后key是唯一id。这样下面的更新逻辑才是对的,否则拿到的id其实是数组的下标。

但是code review由于跟着一堆代码一起评审了,并且这段更新很简单,同时测试的时候也测试过了(能测试通过也是“机缘巧合”),所以没有发现这段异常。

发到线上后,进行了灰度集群的测试,这个时候发现只要调用了这个接口,灰度集群的数据全部都变成了一样,回滚后正常。


分析


       回滚后在本地进行复现,由于本地环境是开启了SQL打印的,于是看到了这么一条SQL:很明显是拿数组的下标去比较了


update db_name set selling_price = xx,sell_type = xx where id = 0;

       由于我们的id是全部是通过uuid生成的,所以下意识的认为这条sql应该啥也不会更新才对,但是本地的确只执行了这条sql,没有别的sql,并且db中的数据全部都被修改了。

这个时候想起福尔摩斯的名言“排除一切不可能的,剩下的即使再不可能,那也是真相”\color{blue}{“排除一切不可能的,剩下的即使再不可能,那也是真相”} ,于是抱着试一试的心态直接拿这条sql去db控制台执行了一遍,发现果然所有的数据又都被修改了。

也就是 whereid=0\color{red}{where id = 0}  这个条件对于所有的记录都是恒为true,就会导致所有记录都被更新。在这个时候,想起曾经看到过MySQL对于不同类型的比较会有 【隐式转换】\color{red}{【隐式转换】},难道是这个原因导致的?


隐式转换规则


在MySQL官网找到了不同类型比较的规则:



最后一段的意思是:对于其他情况,将按照浮点(双精度)数进行比较。例如,字符串和数字的比较就按照浮点数规则进行比较。

也就是id会首先被转换成浮点数,然后再跟0进行比较。


MySQL字符转为浮点数时会按照如下规则进行:


1.如果字符串的第一个字符就是非数字的字符,那么转换结果就是0;

2.如果字符串以数字开头:

(1)如果字符串都是数字,转换结果就是整个字符串对应的数字;

(2)如果字符串中存在非数字,转换结果就是开头的那些数字对应的值;

举例说明:

"test" -> 0

"1test" -> 1

"12test12" -> 12

由于我们生成的uuid没有数字开头的字符串,于是都会转变成0。那么这条SQL就变成了:


update db_name set selling_price = xx,sell_type = xx where 0 = 0;

就恒为true了。

修复就很简单了,把取id的逻辑改成正确的就行。


为什么测试环境没有发现


       前面有提到这段代码在测试环境是测试通过了的,这是因为开发和测试同学的环境里都只有一条记录,每次更新他发现都能正常更新就认为是正常的了。同时由于逻辑太简单了,所以都没有重视这块的回归测试。

幸好在灰度集群就发现了这个问题,及时进行了回滚,如果发到了线上影响了用户数据,可能就一年白干了。


最后


代码无小事,事事需谨慎啊。一般致命问题往往是一行小小的修改导致的。


作者:云舒编程
来源:juejin.cn/post/7275550679790960640
收起阅读 »

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. 生活方面:我时常在反思,我是不是一个精致的利己主义者?这些年基本就过年回家,回家也呆不了几天,在照顾父母、关心家人这方面我是做的比较差的,如果我只想着自己能不能拿高工资,自己过的是否惬意,我觉得这是很自私的,回武汉离家近可以很好的解决这个问题。


2-work.png


在业务需求少后,部门也有了裁员的消息,我正好在这边快两年了,也想回武汉换个环境。


为了变被动为主动,就在 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
收起阅读 »

请给系统加个【消息中心】功能,因为真的很简单

个人项目:社交支付项目(小老板) 作者:三哥,j3code.cn 项目文档:http://www.yuque.com/g/j3code/dv… 预览地址(未开发完):admire.j3code.cn/small-boss 内网穿透部署,第一次访问比较慢 ...
继续阅读 »

个人项目:社交支付项目(小老板)


作者:三哥,j3code.cn


项目文档:http://www.yuque.com/g/j3code/dv…


预览地址(未开发完):admire.j3code.cn/small-boss



  • 内网穿透部署,第一次访问比较慢



我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面:


1)消息提示


Snipaste_2023-08-27_13-41-36.jpg


2)消息列表


这样


Snipaste_2023-08-27_13-42-25.jpg


这样


Snipaste_2023-08-27_16-41-30.jpg


那,这就是我们今天要聊的【消息中心】。


1、设计


老规矩先来搞清楚消息中心的需求,再来代码实现。


我们知道在社交类项目中,有很多评论、点赞等数据的产生,而如果这些数据的产生不能让用户感知到,那你们想想这会带来什么影响?



用户A:太鸡肋了,发布的内容被人评论点赞了,我居然看不到,下次不用了...


用户B:还好没用这个系统...



所以,看到这些结果我们是不是能够意识到一个健全的社交功能,是不是少不了这种通知用户的机制啊!而这种机制我就把他定义为【消息中心】功能。


再来拆分一下这四个字:消息中心



  1. 消息

  2. 中心


消息:这个可以是由我们自己定义,如:把帖子被用户评论当作一条消息,把评论被用户点赞也可以当作一条消息,甚至系统发布的通知也是一条消息。


中心:这个就是字面意思,将上面所提到的所有消息,归拢到一个地方进行展示。


上面我们也提到消息基本就是这两种:



  • 用户对用户:用户消息

  • 平台对用户:系统消息


针对用户消息,就类似这样,用户 A 给用户 B 的一条评论进行了点赞,那这个点赞动作就会产生一条消息,并且通知到用户 B 的一个存储消息的地方,这里通常就指用户的收件箱。这个收件箱就是专门用来存储用户发给用户的消息,而这个点对点的模式是不是就是推送模式啊!(A 推送消息给 B)


接着针对系统消息,就类似这样,平台管理人员发布了一条通知,告诉大家平台有啥 XXX 活动。那这个活动通知肯定是要让平台的所有用户都知道把,所以这个通知就要存在一个发件箱中。这个发件箱就是专门存储平台的通知,所有用户都来这个发件箱中读取消息就行,而这个一对多的模式是不是就是拉取模式啊!(所有用户都来拉取平台消息)


这样一来,我们根据不同的消息场景就抽出了一个基本的消息推拉模型,模型图如下:



Snipaste_2023-08-27_14-27-25.jpg



Snipaste_2023-08-27_14-59-50.jpg


针对这两种模式,不知道大家有没有看出区别,好像乍一看没啥区别,都是发消息,读消息,对吧!


没错,确实都是一个发,一个读,但是两者的读写频率确实有着巨大的差异。先来看推模型,一个普通用户发表了一条帖子,然后获得了寥寥无几的评论和赞,这好似也没啥特别之处,对吧!那如果这个普通用户发表的帖子成为了热门帖子呢,也即该贴子获得了上万的评论和赞。那,你们想想是不是发消息的频率非常高,而该普通用户肯定是不可能一下子读取这么多消息的,所以是不是一个写多读少的场景。再来看看拉模型,如果你的平台用户人数寥寥无几,那倒没啥特别之处,但如果用户人数几万甚至几十万。那,每个用户都过来拉取系统消息是不是就是一个读频率非常高,而发消息频率非常低(系统消息肯定不会发的很快),所以这是不是一个读多写少的场景。


1.1 推:写多读少


针对这个模式,我们肯定是要将写这个动作交给性能更高的中间件来处理,而不是 MySQL,所以此时我们的 RocketMQ 就出来了。


当系统中产生了评论、点赞类的高频消息,那就无脑的丢给 MQ 吧,让其在消息中间件中呆会,等待消费者慢慢的将消息进行消费并发到各个用户的收件箱中,就类似下面这张图的流程:


Snipaste_2023-08-27_15-45-46.jpg


2.2 拉:读多写少


那对于这个模式,所实话,我觉得不用引入啥就可以实现,因为对于读多的话无非就是一个查,MySQL 肯定是能搞定的,即使你的用户几万、几十万都是 ok 的。


但咱们是不是可以这样想一下,一个系统的官方通知肯定是不多的,或者说几天或者几个星期一次,且一旦发送就不可更改。那是不是可以考虑缓存,让用户读取官方通知的时候走缓存,如果缓存没有再走 MySQL 这样应该是可以提高查询效率,提高响应速度。


具体流程如下图:


Snipaste_2023-08-27_15-57-21.jpg


2.3 表结构设计


基本的业务流程已经分析的差不多了,现在可以把表字段抽一下了,先根据上面分析的,看看我们需要那些表:



  1. 用户收件箱表

  2. 系统发件箱表


看似好像就这两张表,但是应该还有第三张表:



  1. 用户读取系统消息记录表



我们看到页面是不是每次有一条新的消息都会有一个小标点记录新消息数量,而第三张表就是为了这个作用而设计的。


具体原理如下:



  1. 首先运营人员发布的消息都是存储在第二张表中,这肯定是没错的

  2. 那用户每次过来拉取系统消息时,将最近拉取的一条消息写入到第三种表中

  3. 这样等用户下次再来拉取的时候,就可以根据第三张表的读取记录,来确定他有几条系统消息未查看了


可能有人会发出疑问:那用户的收件箱为啥不出一个用户读取记录表呢!


这个很简单,因为收件箱中的数据已经表示这个用户需要都这些个消息了,只是不知道那些是已读的那些是未读的,我们只需要再收件箱表中加一个字段,这个字段的作用就是记录最新一次读取的消息 ID 就行,等下次要读消息时,找到上传读取读取消息的记录ID,往后读新消息即可。



好,现在来看看具体的表字段:


1)用户收件箱表(sb_user_inbox)



  • id

  • 消息数据唯一 id:MQ唯一消息凭证

  • 消息类型:评论消息或者点赞消息

  • 帖子id:业务id

  • 业务数据id:业务id

  • 内容:消息内容

  • 业务数据类型:业务数据类型(商品评论、帖子、帖子一级评论、帖子二级评论)

  • 发起方的用户ID:用户 A 对用户 B 进行点赞,那这就是用户 A 的ID

  • 接收方的用户ID:用户 B 的 ID

  • 用户最新读取位置ID:用户最近一次读取记录的 ID


SQL


CREATE TABLE `sb_user_inbox` (
`id` bigint(20) NOT NULL,
`uuid` varchar(128) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '消息数据唯一id',
`message_type` tinyint(1) NOT NULL COMMENT '消息类型',
`post_id` bigint(20) DEFAULT NULL COMMENT '帖子id',
`item_id` bigint(20) NOT NULL COMMENT '业务数据id',
`content` varchar(1000) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '内容',
`service_message_type` tinyint(1) NOT NULL COMMENT '业务数据类型',
`from_user_id` bigint(20) NOT NULL COMMENT '发起方的用户ID',
`to_user_id` bigint(20) NOT NULL COMMENT '接收方的用户ID',
`read_position_id` bigint(20) DEFAULT '0' COMMENT '用户最新读取位置ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `un01` (`uuid`),
UNIQUE KEY `un02` (`item_id`,`service_message_type`,`to_user_id`),
KEY `key` (`to_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

可以看到,我加了很多业务相关的字段,这个主要是为了方便查询数据和展示数据。


2)系统发件箱表(sb_sys_outbox)



  • id

  • 内容


SQL


CREATE TABLE `sb_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`content` varchar(2000) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

这个表就非常简单了,没啥业务字段冗余。


3)用户读取系统消息记录表(sb_user_read_sys_outbox)



  • id

  • 系统收件箱数据读取id

  • 读取的用户id


SQL


CREATE TABLE `sb_user_read_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`sys_outbox_id` bigint(20) NOT NULL COMMENT '系统收件箱数据读取id',
`user_id` bigint(20) NOT NULL COMMENT '读取的用户id',
PRIMARY KEY (`id`),
UNIQUE KEY `un` (`user_id`),
KEY `key` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

ok,这是消息中心所有分析阶段了,下面就开始实操。


2、实现


先来引入引入一下 RocketMQ 的依赖


<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>

RocketMQ 的双主双从同步刷新集群搭建教程:blog.csdn.net/qq_40399646…


MQ 配置:


Snipaste_2023-08-27_16-26-09.jpg


2.1 生产者


先来实现生产者如何发送消息。


1)消息体对象:LikeAndCommentMessageDTO


位置:cn.j3code.config.dto.mq


@Data
public class LikeAndCommentMessageDTO {

/**
* 该消息的唯一id
* 业务方可以不设置,如果为空,代码会自动填充
*/

private String uuid;

/**
* 消息类型
*/

private UserCenterMessageTypeEnum messageType;

/**
* 冗余一个帖子id进来
*/

private Long postId;

/**
* 业务数据id
*/

private Long itemId;

/**
* 如果是评论消息,这个内容就是评论的内容
*/

private String content;

/**
* 业务数据类型
*/

private UserCenterServiceMessageTypeEnum serviceMessageType;

/**
* 发起方的用户ID
*/

private Long fromUserId;

/**
* 接收方的用户ID
*/

private Long toUserId;


/*
例子:
用户 A 发表了一个帖子,B 对这个帖子进行了点赞,那这个实体如下:
messageType = UserCenterMessageTypeEnum.LIKE
itemId = 帖子ID(对评论进行点赞,就是评论id,对评论进行回复,就是刚刚评论的id)
serviceMessageType = UserCenterServiceMessageTypeEnum.POST(这个就是说明 itemId 的 ID 是归于那个业务的,方便后续查询业务数据)
fromUserId = 用户B的ID
toUserId = 用户 A 的ID
*/

}

2)发送消息代码


位置:cn.j3code.community.mq.producer


@Slf4j
@Component
@AllArgsConstructor
public class LikeAndCommentMessageProducer {

private final RocketMQTemplate rocketMQTemplate;

/**
* 单个消息发送
*
* @param dto
*/

public void send(LikeAndCommentMessageDTO dto) {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message<LikeAndCommentMessageDTO> message = MessageBuilder
.withPayload(dto)
.build();
rocketMQTemplate.send(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, message);
}

/**
* 批量消息发送
*
* @param dtos
*/

public void send(List<LikeAndCommentMessageDTO> dtos) {
/**
* 将 dtos 集合分割成 1MB 大小的集合
* MQ 批量推送的消息大小最大 1MB 左右
*/

ListSizeSplitUtil.split(1 * 1024 * 1024L, dtos).forEach(items -> {
List<Message<LikeAndCommentMessageDTO>> messageList = new ArrayList<>(items.size());
items.forEach(dto -> {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message<LikeAndCommentMessageDTO> message = MessageBuilder
.withPayload(dto)
.build();
messageList.add(message);
});
rocketMQTemplate.syncSend(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, messageList);
});
}

private void checkMessageDTO(LikeAndCommentMessageDTO dto) {
AssertUtil.isTrue(Objects.isNull(dto.getMessageType()), "消息类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getItemId()), "业务数据ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getServiceMessageType()), "业务数据类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getFromUserId()), "发起方用户ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getToUserId()), "接收方用户ID不为空!");
}


/**
* 发送点赞消息
*
* @param messageType 消息类型
* @param serviceMessageType 业务类型
* @param itemToUserIdMap 业务ID对应的用户id
* @param saveLikeList 点赞数据
*/

public void sendLikeMQMessage(
UserCenterMessageTypeEnum messageType,
UserCenterServiceMessageTypeEnum serviceMessageType,
Map<Long, Long> itemToUserIdMap, List<Like> saveLikeList)
{
if (CollectionUtils.isEmpty(saveLikeList)) {
return;
}
List<LikeAndCommentMessageDTO> dtos = new ArrayList<>();
for (Like like : saveLikeList) {
LikeAndCommentMessageDTO messageDTO = new LikeAndCommentMessageDTO();
messageDTO.setItemId(like.getItemId());
messageDTO.setMessageType(messageType);
messageDTO.setServiceMessageType(serviceMessageType);
messageDTO.setFromUserId(like.getUserId());
messageDTO.setToUserId(itemToUserIdMap.get(like.getItemId()));
dtos.add(messageDTO);
}
try {
send(dtos);
} catch (Exception e) {
//错误处理
log.error("发送MQ消息失败!", e);
}
}
}

注意:这里我用了 MQ 批量发送消息的一个功能,但是他有一个限制就是每次只能发送 1MB 大小的数据。所以我需要做一个功能工具类将业务方丢过来的批量数据进行分割。


工具类:ListSizeSplitUtil


位置:cn.j3code.config.util


public class ListSizeSplitUtil {

private static Long maxByteSize;

/**
* 根据传进来的 byte 大小限制,将 list 分割成对应大小的 list 集合数据
*
* @param byteSize 每个 list 数据最大大小
* @param list 待分割集合
* @param <T>
* @return
*/

public static <T> List<List<T>> split(Long byteSize, List<T> list) {
if (Objects.isNull(list) || list.size() == 0) {
return new ArrayList<>();
}

if (byteSize <= 100) {
throw new RuntimeException("参数 byteSize 值不小于 100 bytes!");
}
ListSizeSplitUtil.maxByteSize = byteSize;


if (isSurpass(List.of(list.get(0)))) {
throw new RuntimeException("List 中,单个对象都大于 byteSize 的值,分割失败");
}

List<List<T>> result = new ArrayList<>();

List<T> itemList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
itemList.add(list.get(i));

if (isSurpass(itemList)) {
i = i - 1;
itemList.remove(itemList.size() - 1);
result.add(new ArrayList<>(itemList));
itemList = new ArrayList<>();
}
}
result.add(new ArrayList<>(itemList));
return result;
}


private static <T> Boolean isSurpass(List<T> obj) {
// 字节(byte)
long objSize = RamUsageEstimator.sizeOfAll(obj.toArray());
return objSize >= ListSizeSplitUtil.maxByteSize;
}
}

至此呢,生产者的逻辑就算是完成了,每次有消息的时候就调用这个方法即可。


2.2 消费者


位置:cn.j3code.user.mq.consumer


@Slf4j
@Component
@AllArgsConstructor
@RocketMQMessageListener(topic = RocketMQConstants.USER_MESSAGE_CENTER_TOPIC,
consumerGroup = RocketMQConstants.GROUP,
messageModel = MessageModel.CLUSTERING,
consumeMode = ConsumeMode.CONCURRENTLY
)

public class LikeAndCommentMessageConsumer implements RocketMQListener<LikeAndCommentMessageDTO> {

private final UserInboxService userInboxService;

@Override
public void onMessage(LikeAndCommentMessageDTO message) {
userInboxService.saveMessage(message);
}
}

saveMessage 方法的逻辑就是将消息保存到 MySQL 中,至此消息的产生和存储就算完成了,下面来看看用户如何查看吧!


2.3 用户消息查看


对于用户查看普通的消息就是访问一下 MySQL,并且更新一下最新读取的字段值即可,我贴一下关键代码就行了,代码如下:


public IPage<UserMessageVO> page(UserMessagePageRequest request) {
// 获取消息
IPage<UserMessageVO> page = getBaseMapper().page(new Page<UserMessageVO>(request.getCurrent(), request.getSize()), request);

if (CollectionUtils.isEmpty(page.getRecords())) {
return page;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
if (Objects.isNull(page.getRecords().get(0).getReadPositionId()) ||
page.getRecords().get(0).getReadPositionId() == 0) {
UserInbox userInbox = new UserInbox();
userInbox.setId(page.getRecords().get(0).getId());
userInbox.setReadPositionId(userInbox.getId());
updateById(userInbox);
}
}
return page;
}

2.4 系统消息查看


对于系统消息的查看也是,只贴出关键代码,查询和更新读取记录逻辑,代码如下:


@Override
public IPage<SysOutboxVO> lookSysPage(SysOutboxPageRequest request) {
Page<SysOutbox> page = lambdaQuery()
.orderByDesc(SysOutbox::getId)
.page(new Page<>(request.getCurrent(), request.getSize()));
IPage<SysOutboxVO> outboxVOIPage = page.convert(userInboxConverter::converter);
if (CollectionUtils.isEmpty(outboxVOIPage.getRecords())) {
return outboxVOIPage;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
userReadSysOutboxService.updateReadLog(page.getRecords().get(0).getId(), SecurityUtil.getUserId());
}
return outboxVOIPage;
}

这里,可能有人会发现,没有按照上面分析的那用从缓存中读,是的。这里的实现我没有用到 Redis,这里我偷了一下懒,如果有拿到我代码的同学可以试着优化一下这个逻辑。


作者:J3code
来源:juejin.cn/post/7274922643453853735
收起阅读 »

血压飙升!记一次关于手机号存储的前后端讨论

web
本文是为了探讨技术架构管理的,不要带入实际生活 起 事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 : 涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。 本来是很简单的表单...
继续阅读 »

本文是为了探讨技术架构管理的,不要带入实际生活




事情是这样的,有一款产品需要实现国际化的注册需求,界面如下 :


image.png


涉及到的手机号码,因为要区分国家区号,所以和手机号是分开的,前端形态是分开存储的。一开始数据确实也是分开存的,么的问题。


本来是很简单的表单需求,结果出了幺蛾子。



对于前端来说,这就是两个字段,在表单传值和数据逻辑处理时,直接用两个字段无疑是最直接和简单的:


const formData = {
country_code: '86',
phone: '13345431234'
...
}

但是,产品侧有一个强需求:手机号和其他的两个字段要做唯一性校验,不但要求三个至少存在一个,而且还要查别的业务库的数据表来校验这个注册人员的唯一性,有一套复杂的逻辑。总结一下就是,后端分开两个字段来判断很麻烦,这个也好理解,有耦合的逻辑重构要成本的。所以后端是这么要求数据的:


// (86)13345431234
phone: `(${country_code})${phone}`

将国家码放在前缀括号里拼接为一个字符串。这样一来,前端的成本就相对高了一些,不但在表单提交时要拼接,而且在列表查询时,要按照括号匹配解析出前缀来动态显示


const regex = /^\((\d+)\)(\d+)$/;
const matches = phoneNumber.match(regex);
// 如果匹配成功,返回国家码和号码的数组
if (matches) {
const countrycode = matches[1];
const number = matches[2];
return [countrycode, number];
}

就算这样,也可以接受,之前也不止一次挖过这种自己拼接自己解析的坑了。但是随后就有点让人血压上升了。



由于业务扩展,上面的手机号,要传递给一款即时通讯软件 xxx 的 API 来做下发,下发侧则规定了,需要不带括号但是要有国家区号前缀的手机号码


// 8613345431234
phone: `${country_code}${phone}`

这就有问题了,我们直接对接的后端需要括号的,下发侧对接的后端同学不需要括号。


第一阶段


血压上升 20%


讨论让后端换成两个字段存储,这样更灵活,方便拼接;或者给下发侧发数据时去掉括号也可以。后端很为难:”唯一性判断还有其他的逻辑很复杂,有耦合性,拆开字段就要重构,工期又紧,刚完成就要改,误了工期只有我背锅;去掉括号不是不可以,但地方有点多,还要考虑批量数据的性能,而且已经进入测试了,不接受临时需求“。我一想有道理啊,就去问下发侧同学。


第二阶段


血压上升 60%


问下发侧对接的同学,可以处理后再给 API下发吗?果断拒绝。他们也有理由的:”按照软件设计的规范,我们不应该非标处理这个问题的,下发的规范理应就是我们接口的规范,因为规范还有很多,这个地方妥协了,以后肯定还会有更多的非标处理,我们的代码就乱了。“


我草,我一想之前那那么多屎山的由来,觉得更有理了,那怎么办?弄得好像前端这里没有理了。


第三阶段


🔥 血压上升 120% 🔥


下发侧对接的同学反过来给我们前端提了建议,让我们在注册的时候,把前端两个字段就直接不用括号来拼接给后端,在需要解析的时候,使用特别手段解析出国家码。还好心的给我们搜索了一个解析手机号的 js 库:


企业微信截图_1bfc9849-e9f2-4289-a4cb-01098e3dcf2e.png


我只能说,我 TM 谢谢你。😭


前端莫名其妙的凭空多了需求,而且加个库为了给后端擦屁股,还增加了打包体积,出了问题我们得首先背锅,这肯定不行。



这起事件牵涉的技术问题并没有多难,甚至有些幼稚,但是却蕴含了不少上班族哲学,最主要的一点是:如何学会背锅的问题。锅不是不能背,但是背锅换来的应该是产品成功后更多的功劳回报,而不是这种暗箱操作,老板压根不知道,只有扛不住的人吃暗亏。


个人觉得,这个应该是架构师或者技术总监的任务,提前了解上下游业务,并确定好字段与数据结构,而不是等用的时候才发现数据对不上。规划好用什么方案来开发。并将方案设计交于老板开会讨论得出定稿。之后再分派人员开发,就不会有这个事件了。



类似的问题还有很多,前后端与下游计算价格进制与单位不统一,每次都要非标转换;



最后的结局,产品出面调停,软磨硬泡,最终后端妥协了,夹在前端和下发侧之间, 确实为难。针对前端还是原来的逻辑不动,使用括号存储,他在下发侧拉取数据的时候做转换,去掉括号。其中涉及的性能问题和开发成本也申请了延长工期。


Happy Ending !!😁


血压恢复 0%


39df05d4-0146-4fbc-9eda-384298424f19.jpg



方案设计注意事项:



  • 注意产品迁移的问题。同类型的产品,如果针对的客户群体不同,其表现形态可能完全不一样,产品迁移过程中,要提前分析出新产品的业务场景的差异,做好风险分析。

  • 提前确定产品上下游生态,做好风险管控。比如下发侧有 API 对接的,需熟悉其 API 文档与使用限制,并在服务不可用时设计告警方案等。


作者:小肚肚肚肚肚哦
来源:juejin.cn/post/7275576074589880372
收起阅读 »

为什么日本的网站看起来如此不同

web
该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。文章还讨论了日本网站的信息密...
继续阅读 »

该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。
文章还讨论了日本网站的信息密集型布局,这种布局适应了日本语言的特点,使得页面能够容纳大量文字和图像。此外,文章提到了日本网站的功能丰富性,如弹出式窗口和互动元素,以及这些元素在用户体验方面的作用。
作者强调了日本网站在技术和创新方面的进步,尽管在过去存在技术限制。最后,文章提出了一些关于如何将日本网站设计的元素应用到其他文化中的建议。


下面是正文~~~


多年来,我朋友与日本的网站有过许多接触——无论是研究签证要求、计划旅行,还是简单地在线订购东西。而我花了很长时间才适应这些网站上的大段文字、大量使用鲜艳颜色和10多种不同字体的设计,这些网站就像是直接冲着你扔过来的。


image.png


虽然有许多网站都采用了更简约、易于导航的设计,适应了西方网站的用户,但是值得探究的是为什么这种更复杂的风格在日本仍然盛行。


只是为了明确起见,这些不是过去的遗迹,而是维护的网站,许多情况下,它们最后一次更新是在2023年。


image.png


我们可以从几个角度来分析这种设计方法:



  • 字体和前端网站开发限制

  • 技术发展与停滞

  • 机构数字素养(或其缺乏)

  • 文化影响


与大多数话题一样,很可能没有一个正确的答案,而是这个网站设计是随着时间的推移而相互作用的各种因素的结果。


字体和前端网站开发限制


对于会一些基本排版知识、掌握适当软件并有一些空闲时间的人来说,为罗马化语言创造新字体可能是一项有趣的挑战。然而,对于日语来说,这是一个完全不同层次的努力。


要从头开始创建英文字体,需要大约230个字形——字形是给定字母的单个表示(A a a算作3个字形)——或者如果想覆盖所有基于拉丁字母表的语言,则需要840个字形。对于日语而言,由于其三种不同的书写系统和无数的汉字,需要7,000至16,000个字形甚至更多。因此,在日语中创建新字体需要有组织的团队合作和比其拉丁字母表的同行们更多的时间。



这并不令人意外,因此中文和(汉字)韩文字体也面临着类似的工作量,这导致这些语言通常被称为CJK字体所覆盖。



由于越来越少的设计师面对这个特殊的挑战,建立网站时可供选择的字体也越来越少。再加上缺乏大写字母和使用日文字体会导致加载时间较长,因为需要引用更大的库,这就不得不采用其他方式来创建视觉层次。


以美国和日本版的星巴克主页为例:


美国的:


image.png


日本的


image.png


就这样,我们就可以解释为什么许多日本网站倾向于用文字较多的图片来表示内容类别了。有时,你甚至会看到每个磁贴都使用自己定制的字体,尤其是在限时优惠的情况下。


image.png


技术发展/停滞与机构数字素养


如果你对日本感兴趣,你可能对现代与过时技术之间的鲜明对比有所了解。在许多地方,先进的技术与完全过时的技术并存。作为世界机器人领导者之一的国家,在台场人工岛上放置了一座真人大小的高达雕像,却仍然依赖软盘和传真机,面对2022年Windows资源管理器关闭时感到恐慌。


image.png


在德国,前总理安格拉·默克尔在2013年称互联网为“未知领域”后,遭到全国范围的嘲笑。然而,这在2018年被前网络安全部长樱田义孝轻易地超越,他声称自己从未使用过电脑,并且在议会被问及USB驱动器的概念时,他被引述为“困惑不解”(来源)。


对于那些尚未有机会窥探幕后幻象的人来说,这可能听起来很奇怪,但日本在技术素养方面严重落后于更新计划。因此,可以推断这些问题也在阻碍日本网站设计的发展。而具体来说,日本的网页设计正面临着这一挑战——只需在谷歌或Pinterest上搜索日本海报设计,就能看到一个非常不同和现代化的平面设计水平。


image.png


文化影响


在分析任何设计选择时,不应低估文化习俗、倾向、偏见和偏好的影响。然而,“这是文化”的说法可能过于简单化,并被用作为各种差异辩解的借口。而且,摆脱自己的观点偏见是困难的,甚至可能无法完全实现。


因此,从我们的角度来看,看这个网站很容易..


image.png


感觉不知所措,认为设计糟糕,然后就此打住。因为谁会使用这个混乱不堪的网站呢?


这就是因为无知而导致有趣的见解被忽视的地方。现在,我没有资格告诉你日本文化如何影响了这种设计。然而,我很幸运能够从与日本本土人士的交谈中获得启发,以及在日本工作和生活的经验。


与这个分析相关的一次对话实际上不是关于网站,而是关于YouTube的缩略图 - 有时候它们也同样令人不知所措。


image.png


对于习惯了许多西方频道所采用的极简和时尚设计——只有一个标题、重复的色彩搭配和有限的字体——上面的缩略图确实有些难以接受。然而,当我询问一个日本本土人士为什么许多极受欢迎频道的缩略图都是这样设计时,他对这种设计被视为令人困惑的想法感到惊讶。他认为日本的设计方法使视频看起来更加引人入胜,提供了一些信息碎片,从而使我们更容易做出是否有趣的明智决策。相比之下,我给他看的英文视频缩略图在他看来非常模糊和无聊。


也许正是这种寻求信息的态度导致了我们的观念如此不同。在日本,对风险的回避、反复核对和对迅速做出决策的犹豫明显高于西方国家。这与更加集体主义的社会心态紧密相连——例如,在将文件发送给商业伙伴之前进行两次(或三次)检查可能需要更长时间,但错误的风险显著降低,从而避免了任何参与者丢面子的情况发生。


尽管有人认为这只适用于足够高的赌注,而迷惑外国游客似乎不符合条件——搜索一下“Engrish”这个词,然后感谢我吧。


回到网站设计,这种文化角度有助于解释为什么在线购物、新闻和政府网站在外部观察者看来常常是“最糟糕的罪犯”。毕竟,这些正是需要大量细节直接对应于做出良好购买决策、高效地保持最新信息或确保你拥有某个特定程序的所有必要信息的情况。


有趣的是,关于美国人和中国/日本人如何感知信息,也有相当多的研究。几项研究的结果似乎表明,例如,日本人更加整体地感知信息,而美国人倾向于选择一个焦点来引导他们的注意力(来源)。这可能给我们提供了另一个线索,解释为什么即使在日语能力较高的情况下,西方人对这类网站也感到困难。


后但并非最不重要的是,必须说的是,网站并不是在一个在线真空中存在。而且,各种媒体,从小册子或杂志到地铁广告,也使用了尽可能多地压缩信息的布局,人们可能已经习惯了这种无处不在的方式,以至于没有人曾经想过质疑它。


长话短说,这并不是为了找到标题问题的绝对答案,也不是为了加强日本人独特性的观点,就像日本人论一样。相反,尤其是在看到了几次关注一个解释为“真正答案”的讨论之后,我想展示科技、历史和文化影响的广度,这些最终塑造了这种差异。


作者:王大冶
来源:juejin.cn/post/7272290608655941651
收起阅读 »

从《孤注一掷》出发,聊聊 SSL 证书的重要性

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。 图片来源于电影《孤注一掷》 这部电影除了让人后背发凉外,也...
继续阅读 »

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。
image001.png
图片来源于电影《孤注一掷》


这部电影除了让人后背发凉外,也不禁让人回忆起了曾经上网冲浪遇到的种种现象:看小说时性感荷官总在网页右下角在线发牌;看电影时网页左下角常常蹦出“在线老虎机”……这些让人烦不胜烦的广告弹窗之所以出现,要么是建站人员利欲熏心投放了非法广告,要么就是因为网站使用了不安全的 HTTP 协议而遭到了攻击,正常的网页内容被恶意篡改。


网站是电信诈骗、网络赌博等非法内容出现的重灾区,建站者和使用者都应该提高安全意识,特别是对建站者来说,保护通信安全才能更好的承担起建站责任。本文将从 HTTP 讲起,介绍 HTTPS 保护通信安全的原理,以及作为网络通信安全基石的 SSL 证书的重要性。


HTTP 协议


HTTP(Hyper Text Transfer Protocol)协议是超文本传输协议。它是从 WEB 服务器传输超文本标记语言(HTML)到本地浏览器的传送协议。HTTP 基于 TCP/IP 通信协议来传递数据,通信双方在 TCP 握手后即可开始互相传输 HTTP 数据包。具体过程如下图所示:
image003.jpg
HTTP 建立流程


HTTP 协议中,请求和响应均以明文传输。如下图所示,在访问一个使用 HTTP 协议的网站时,通过抓包软件可以看到网站 HTTP 响应包中的完整 HTML 内容。


image005.png


虽然 HTTP 明文传输的机制在性能上带来了优势,但同时也引入了安全问题:



  • 缺少数据机密性保护。HTTP 数据包内容以明文传输,攻击者可以轻松窃取会话内容。

  • 缺少数据完整性校验。通信内容以明文传输,数据内容可被攻击者轻易篡改,且双方缺少校验手段。

  • 缺少身份验证环节。攻击者可冒充通信对象,拦截真实的 HTTP 会话。


HTTP 劫持


作为划时代的互联网通信标准之一,HTTP 协议的出现为互联网的普及做出了不可磨灭的贡献。但正如上节谈到, HTTP 协议因为缺少加密、身份验证的过程导致很可能被恶意攻击,针对 HTTP 协议最常见的攻击就是 HTTP 劫持。


HTTP 劫持是一种典型的中间人攻击。HTTP 劫持是在使用者与其目的网络服务所建立的数据通道中,监视特定数据信息,当满足设定的条件时,就会在正常的数据流中插入精心设计的网络数据报文,目的是让用户端程序解析“错误”的数据,并以弹出新窗口的形式在使用者界面展示宣传性广告或直接显示某网站的内容。


下图是一种典型的 HTTP 劫持的流程。当客户端给服务端发送 HTTP 请求,图中发送请求为“梁安娜的电话号码是?”,恶意节点监听到该请求后将其放行给服务端,服务端返回正常 HTML 响应,关键返回内容本应该是“+86 130****1234”,恶意节点监听到该响应,并将关键返回内容篡改为泰国区电话“+66 6160 *88”,导致用户端程序展示出错误信息,这就是 HTTP 劫持的全流程。


image007.jpg
HTTP 劫持流程


例如,在某网站阅读某网络小说时,由于该网站使用了不安全的 HTTP 协议,攻击者可以篡改 HTTP 相应的内容,使网页上出现与原响应内容无关的广告,引导用户点击,可能将跳转进入网络诈骗或其他非法内容的页面。


image009.png
原网页


image011.png
HTTP 劫持后网页


HTTPS 工作原理


HTTPS 协议的提出正是为了解决 HTTP 带来的安全问题。HTTPS 协议(HyperText Transfer Protocol Secure,超文本传输安全协议),是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 的开发主要是提供对网站服务器的身份认证,保护交换资料的隐私性与完整性。


TLS 握手是 HTTPS 工作原理的安全基础部分。TLS 传统的 RSA 握手流程如下所示:


image013.jpg
TLS 握手流程


TLS 握手流程主要可以分为以下四个部分:


第一次握手:客户端发送 Client Hello 消息。该消息包含:客户端支持的 SSL/TLS 协议版本(如 TLS v1.2 );用于后续生成会话密钥的客户端随机数 random_1;客户端支持的密码套件列表。


第二次握手:服务端收到 Client Hello 消息后,保存随机数 random_1,生成随机数 random_2,并发送以下消息。



  • 发送 Server Hello 消息。该消息包含:服务端确认的 SSL/TLS 协议版本(如果双方支持的版本不同,则关闭加密通信);用于后续生成会话密钥的服务端随机数 random_2;服务端确认使用的密码套件

  • 发送“Server Certificate”消息。该消息包含:服务端的 SSL 证书。SSL 证书又包含服务端的公钥、身份等信息。

  • 发送“Server Hello Done”消息。该消息表明 ServerHello 及其相关消息的结束。发送这个消息之后,服务端将会等待客户端发过来的响应。


第三次握手:客户端收到服务端证书后,首先验证服务端证书的正确性,校验服务端身份。若证书合法,客户端生成预主密钥,之后客户端根据(random_1, random_2, 预主密钥)生成会话密钥,并发送以下消息。



  • 发送“Client Key Exchange”消息,该消息为客户端生成的预主密钥,预主密钥会被服务端证书中的公钥加密后发送。

  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。

  • 发送“Encrypted Handshake Message”消息,表示客户端的握手阶段已经结束。客户端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给服务端,供服务端校验。


第四次握手:服务端收到客户端的消息后,利用自己的服务端证书私钥解密出预主密钥,并根据(random_1, random_2, 预主密钥)计算出会话密钥,之后发送以下消息。



  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。

  • 发送“Encrypted Handshake Message”,表示服务端的握手阶段已经结束,同时服务端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给客户端,供客户端校验。


根据 TLS 握手流程,可以看出它是如何解决 HTTP 协议缺陷,以及避免中间人攻击的:


1.规避窃听风险,攻击者无法获知通信内容


在客户端进行真正的 HTTPS 请求前,客户端与服务端都已经拥有了本次会话中用于加密的对称密钥,后续双方 HTTPS 会话中的内容均用该对称密钥加密,攻击者在无法获得该对称密钥的情况下,无法解密获得会话中内容的明文。即使攻击者获得了 TLS 握手中双方发送的所有明文信息,也无法从这些信息中恢复对称密钥,这是由大数质因子分解难题和有限域上的离散对数难题保证的。


2.规避篡改风险,攻击者无法篡改通信内容


在数据通信阶段,双端消息发送时会对原始消息做一次哈希,得到该消息的摘要后,与加密内容一起发送。对端接受到消息后,使用协商出来的对称加密密钥解密数据包,得到原始消息;接着也做一次相同的哈希算法得到摘要,对比发送过来的消息摘要和计算出的消息摘要是否一致,可以判断通信数据是否被篡改。


3.规避冒充风险,攻击者无法冒充身份参与通信


在 TLS 握手流程中的第二步“Server Hello”中,服务端将自己的服务端证书交付给客户端。客户端拿到 SSL 证书后,会对服务端证书进行一系列校验。以浏览器为例,校验服务端证书的过程为:



  • 验证证书绑定域名与当前域名是否匹配。

  • 验证证书是否过期,是否被吊销。

  • 查找操作系统中已内置的受信任的证书发布机构 CA(操作系统会内置有限数量的可信 CA),与服务端证书中的颁发者 CA 比对,验证证书是否为合法机构颁发。如果服务端证书不是授信 CA 颁发的证书,则浏览器会提示服务端证书不可信。

  • 验证服务端证书的完整性,客户端在授信 CA 列表中找到服务端证书的上级证书,后使用授信上级证书的公钥验证服务端证书中的签名哈希值。

  • 在确认服务端证书是由国际授信 CA 签发,且完整性未被破坏后,客户端信任服务端证书,也就确认了服务端的正确身份。


SSL 证书


正如上一节介绍,SSL 证书在 HTTPS 协议中扮演着至关重要的作用,即验证服务端身份,协助对称密钥协商。只有配置了 SSL 证书的网站才可以开启 HTTPS 协议。在浏览器中,使用 HTTP 的网站会被默认标记为“不安全”,而开启 HTTPS 的网站会显示表示安全的锁图标。


image015.png
使用 HTTP 协议的网站


image028.gif
使用 HTTPS 协议的网站


从保护范围、验证强度和适用类型出发, SSL 证书会被分成不同的类型。只有了解类型之间的区别,才能根据实际情况选择更适合的证书类型,保障通信传输安全。


从保护范围分,SSL 证书可以分为单域名证书、通配符证书、多域名证书。



  • 单域名证书:单域名证书只保护一个域名,这些域名形如 http://www.test.com 等。

  • 通配符证书:通配符证书可以保护基本域和无限的子域。通配符 SSL 证书的公用名中带有星号 ,其中,星号表示具有相同基本域的任何有效子域。例如,。test.com 的通配符证书可用于保护 a.test.com、 b.test.com……

  • 多域名证书:多域证书可用于保护多个域或子域。包括完全唯一的域和具有不同顶级域的子域(本地/内部域除外)的组合。


从验证强度和适用类型进一步区分,SSL 证书可以分为 DV、OV、EV 证书。



  • DV(Domain Validated):域名验证型。在颁发该类型证书时,CA 机构仅验证申请者对域名的所有权。CA 机构会通过检查 WHOIS、DNS 的特定记录来确认资格。一般来说,DV 证书适用于博客、个人网站等不需要任何私密信息的网站。

  • OV(Organization Validated):组织验证型。OV 证书的颁发除了要验证域名所有权外,CA 还会额外验证申请企业的详细信息(名称、类型、地址)等。一般来说,OV 证书适用于中级商业组织。

  • EV(Extended Validation):扩展验证型。EV 证书的颁发除了 CA 对 DV 和 OV 证书所采取的所有身份验证步骤之外,还需要审查商业组织是否在真实运营、其实际地址,并致电以验证申请者的就业情况。一般来说,EV 证书适用于顶级商业组织。


结尾


随着互联网应用的普及,网络诈骗的方式也越发花样百出,让人防不胜防。


除了文内提到的网页环境,在软件应用、邮件、文档、物联网等领域同样存在恶意软件、钓鱼邮件、文档篡改、身份认证的问题。幸运的是,作为 PKI 体系下的优秀产品,证书体系同样在这些领域发挥着重要作用,软件签名证书、邮件签名证书、文档签名证书、私有证书等保护着各自领域的信息安全。


总有不法分子企图通过漏洞牟利,而证书体系在保护数据机密性、完整性、可用性以及身份验证场景上有着无可取代的地位,牢牢守护着用户信息,保障通信安全。


作者:火山引擎边缘云
来源:juejin.cn/post/7273685263841263672
收起阅读 »

任正非:我不懂技术、不懂管理、只懂分钱

01作为华为的创始人和独立股东,任正非的总出资占公司总股本的比例不足1%,其余全部由华为员工持有。其实在华为刚创立时,任正非就设计了员工持股制度,他的个人经历以及父母亲的影响让他意识到,要与员工分担责任、分享利益。只有“人人做老板,共同打天下”,当时“没有背景...
继续阅读 »

01

与生俱来的员工持股公司


从一家注册资金仅为2万元的销售代理企业成长为中国最大的民营企业,2021年在《财富》世界500强企业中排名第44位,华为的成功被人津津乐道,其中华为的“财散人聚”机制尤为引人注目。


企业家往往希望员工在既有工资水平下发挥最大价值,为企业创造财富。而华为不同,它舍得给员工分钱、舍得给员工股份,坚持“奉献者定当得到合理的回报,绝不会让‘雷锋’吃亏”。作为华为的创始人和独立股东,任正非的总出资占公司总股本的比例不足1%,其余全部由华为员工持有。


其实在华为刚创立时,任正非就设计了员工持股制度,他的个人经历以及父母亲的影响让他意识到,要与员工分担责任、分享利益。


只有“人人做老板,共同打天下”,当时“没有背景、没有资源、资金短缺”的华为才能吸引并留住人才,从而在与世界巨头和国企的竞争中脱颖而出。


华为初创时,任正非曾向学过经济学的父亲请教,得到了父亲的大力支持。父亲告诉他,民国年间的大掌柜和他的团队没有出钱也会参与分红,并让他仔细琢磨怎样利用好分红模式。


任正非听从父亲的建议,并且一直坚持下来,每年拿出大量的利润分给华为的奋斗者。正是这个让所有者和劳动者共享财富的机制,激发了华为全体员工持续奋斗的热情,为华为的发展壮大提供了强大的动力。任正非也承认“华为今天这么成功,与我不自私有一点关系”。



02

“财散人聚”的机制

为的“财散人聚”机制,把财富更多地分给干部和员工,把股权和能力、贡献和年功很好地结合起来,从而增加了企业的凝聚力、向心力与亲和力,提高了企业的创新力和竞争力。

今天,华为是一家100%由员工持有的民企。华为通过工会实行员工持股计划,参与人仅为公司员工,没有任何政府部门、机构持有华为股权。


1、员工持股计划


华为的员工持股计划始于1990年,当时华为刚起步不久,就面临着摩托罗拉、爱立信等世界巨头对市场的垄断,急需大量的人才和资金投入到技术产品研发、市场拓展中。


面临这样的发展难题,华为第一次提出内部融资、员工持股的概念,实行“工者有其股”,这就是华为员工持股计划的雏形。


华为最初的员工持股计划是员工以每股10元的价格购买公司股票,购入数量由员工的级别、绩效、贡献等确定,然后企业每年拿出税后利润的15%进行股权分红。


每个持股员工手中都有华为所发的股权证书,并盖有华为公司资金计划部的红色印章。股权分红为华为稳住了创业团队,吸引了不少人才,而且为了争取到购买资格,员工的工作积极性大大提高。


员工出资购买股票的方式在当时还为华为赢得了宝贵的发展资金,帮助华为走出了经济困境,为华为拓展市场、增加科研投入、获得竞争优势奠定了基础。



2、虚拟股票期权计划


1998年,华为高层在赴美考察期权激励和员工持股制度时,一种名为虚拟股票的激励制度引起了他们的注意。


虚拟股票是指公司授予激励对象一种虚拟的股票,激励对象可以据此享受一定数量的分红权和股票增值收益,但是这种股票不能转让和出售,在离开公司时自动失效。


2001年7月,华为股东大会通过了股票期权计划,推出了《华为技术有限公司虚拟股票期权计划暂行管理办法》,对员工不再配发1元/股的原始股票,而是以员工的责任和贡献为评判标准,发放以公司年末净资产折算价值的期权,让有贡献者都得到相应的回报。


持有虚拟股票的员工可以获得一定比例的分红以及虚拟股票对应的公司净资产增值部分,但是不能转让和出售,在离开华为时只能由华为出资回购。


虚拟股票所对应的公司净资产的增值部分,增值越多分红越多,这更是给华为员工注入了一剂“强心针”。


3、危机持股计划


2003年,华为业务受到“非典”影响,内忧外患下,华为实施了“危机持股计划”。华为近八成的员工都拥有公司股票的购买权,旨在通过大面积惠及股票购买权,向银行申请股权抵押的贷款额度,缓解资金紧张的问题,并且股权向核心员工倾斜,核心员工获得的配股额度远远大于普通员工,以稳定核心员工队伍,共渡难关。


员工持股制度从“普惠”向“重点激励”的转变,是因为有差距才能体现出知识、奋斗的价值,才能刺激员工艰苦奋斗,否则又会成为“大锅饭”,失去激励的作用。与以往不同,这次配股华为采取了“限制股+虚拟股”的模式。


往年积累的配股,即使员工不离开公司,也可以选择每年按一定比例兑现,但是华为对兑现比例进行了限制:一般员工每年兑现的比例最大不超过个人总股本的1/4,持股较多的核心员工每年可以兑现的比例则不超过个人总股本的1/10。


此次配股还规定了一个3年的锁定期,3年内不允许兑现、转让和抵押。若员工在3年之内离开公司,则所配的股票无效。通过“危机持股计划”,华为很好地稳定了核心员工队伍,实现了销售业绩和净利润的猛涨。


4、饱和配股制


随着老员工手中积累的持股数量越来越大,即使他们不奋斗,依然可以获得可观的分红,而且退休员工在离开公司后仍可选择继续持有股票;而新员工由于进公司时间短,持股数量有限,享受的分配比例反而不高。于是,新老员工的分享比例严重失衡。


了解决这一问题,华为2008年再次对员工持股制度进行了改革,开始实行饱和配股制。实行饱和配股制,即规定了员工的配股上限,不同工作级别匹配不同的持股数量,每个级别达到上限后,就不再参与新的配股。这一规定缩小了新老员工之间的收入差距,更有利于激励华为的新员工。


5、TUP:奖励期权计划


随着华为全球布局的不断深化,高薪聘请的海外员工越来越多,但是海外员工却无法参与华为的虚拟受限股。为了激励、留住海外员工,华为推出了时间单位计划,让海外员工也参与到利润分享中。


华为基于员工的历史贡献和未来发展前途,授予员工一定的TUP。获得TUP的员工在第一年不享受分红,第二年和第三年分别获取1/3额度、2/3额度的分红,第四年全额获取分红,第五年同时获得全额分红和TUP的增值收益,五年结束后TUP的权益清零,重新开始分配。


与员工持股制度不同的是,TUP不需要员工出资购买,因此TUP计划后来也用于激励刚进入华为的国内新员工。



03

利益分享,以奋斗者为本


什么叫奋斗?华为认为,为客户创造价值的任何微小活动,以及在劳动的准备过程中,为充实提高自己而做的努力,均叫奋斗,否则,再苦再累也不叫奋斗。


华为深知在通信行业,技术更替、产业变化迅速,竞争比传统产业更加激烈,要想在这场死亡竞赛中生存得更久,唯有奋斗。


那么,怎样才能使企业不断奋斗呢?这就要依靠坚持为华为奋斗的所有员工。因此,以奋斗者为本成为华为的核心价值观之一。


华为领导层懂得人心,更明白存天理,顺人欲的道理,不跟员工讲吃亏是福,而是十分肯定、强调员工奋斗的价值,从不吝啬对员工奋斗的奖励。


2012年年底,华为的一个竞标团队成功中标一个近10亿美元的大项目,攻克了某国多年未拿下的大粮仓,华为奖励了该团队700万元人民币。


没想到的是,在第二年成功签订合同后,华为领导层又提出再奖励该团队1000万元,他们认为,在一线奋斗的员工不容易,一定要给大家分好钱。最后,在该团队的推辞下,经过商议,华为将1000万元减为700万元,又奖励了一次。

在争夺人才上,华为也舍得花钱。为了从世界各地招收优秀大学生,让这些天才像泥鳅一样,钻活华为的组织,激活华为的队伍,华为为这些天才提供了优厚的年薪,有的甚至在读大二时就收到华为发出的录用通知。

任正非曾说自己不懂技术,不懂管理,只懂分钱,恰恰是这一分钱术,为华为解决了很多问题。华为的分配机制解决了价值创造、价值评价、价值分配等问题。最合理的分配机制是,谁创造价值,谁就享有价值。


作 者:宋志平,中国上市公司协会会长,中国企业改革与发展研究会会长,《共享机制》作者。

来 源:认识管理,本文摘编自《共享机制》,机械工业出版社出版。

收起阅读 »

使用 Vim 两年后的个人总结

为什么要使用 Vim 学习动机非常重要。并不是很多大牛程序员用 Vim 编程,你就应该去学习 Vim,如果你是这种心态,很大的概率,你会在几次尝试以后最终放弃,就像我曾经做过的一样。因为 Vim 的学习曲线很陡峭,没有强烈的学习动机很难坚持下来。 那我为什么后...
继续阅读 »

为什么要使用 Vim


学习动机非常重要。并不是很多大牛程序员用 Vim 编程,你就应该去学习 Vim,如果你是这种心态,很大的概率,你会在几次尝试以后最终放弃,就像我曾经做过的一样。因为 Vim 的学习曲线很陡峭,没有强烈的学习动机很难坚持下来。


那我为什么后来又重新开始学习 Vim,并在两年多后已经习惯、喜欢甚至离不开 Vim?原因很简单,我必须掌握 Vim。


我是一个很爱折腾的人,自己买过很多云服务器,也经常会在服务器上写一些程序,编辑器当然首选 Vim。日复一日,当有一天我实在无法忍受自己在服务端极其低效的编程体验后,我决定真正掌握 Vim。从那时候起,我开始刻意频繁练习,也终于有一天,我发现我完全存活了下来,并且喜欢上了 Vim。


我并不是说你一定要买个云服务器,然后在云服务器上写代码(其实现在你可以用 VSCode 的远程功能在服务器上写代码),我想表达的是,你一定要有足够的学习动机,这个学习动机往往来自于必要性,不管是工作上的必要性,还是自己业余项目上的必要性。也许,有强烈的炫耀动机可能也行。


当然了,当你真正喜欢上 Vim,你会有新的理解,比如 Vim 某种意义上代表了一些正向的价值,文章最后我会提到这一点。


关于 Normal 模式的最佳隐喻


《代码大全》(Code Complete)开头就讲了“软件构建的隐喻”,隐喻是非常好的方式,能够通过熟悉的事物帮我们建立正确的思维模型。关于 Vim 为什么要有 Normal 模式,我看过的最好的隐喻来自《Practical Vim》这本书,我摘录几个关键的段落:



Think of all of the things that painters do besides paint. They study their subject, adjust the lighting, and mix paints into new hues. And when it comes to applying paint to the canvas, who says they have to use brushes? A painter might switch to a palette knife to achieve a different texture or use a cotton swab to touch up the paint that's already been applied.




The painter does not rest with a brush on the canvas. And so it is with Vim. Normal mode is the natural resting state. The clue is in the name, really.




Just as painters spend a fraction of their time applying paint,programmers spend a fraction of their time composing code . More time is spent thinking, reading, and navigating from one part of a codebase to another. And when we do want to make a change, who says we have to switch to Insert mode? We can reformat existing code, duplicate it, move it around, or delete it. From Normal mode, we have many tools at our disposal.



作者把编程比喻成绘画,把 Normal 模式比喻成画家作画的间隙。就像画家要经常放下画笔,走远处看看,或用小刀、棉球等工具修改画作一样,程序员也不会一直输入代码(Insert 模式),程序员也需要思考,需要对程序做一些修改(不一定是插入内容),那么这个时候就应该进入 Normal 模式。Normal 模式让程序员休息、思考,同时提供了更多的工具,比如删除、复制、黏贴、跳转光标等等。每当写程序需要停顿思考的时候,就可以进入 Normal 模式。


一个最重要的模式


这里的模式,不是指“Normal”或“Insert”模式。而是我们在使用 Vim 组合快捷键时候的“操作模式”。这个最重要的模式如下:

Action = Operator + Motion

举一个例子,“删除当前到句尾的所有字符”的操作是d$d$ = d + $,其中的 d 即为 Operator,也即操作,$ 即为 Motion,也即操作的范围。这个模式在 Vim 中无处不在,再举一些例子:

  • dap,删除一整个段落;
  • yG,复制当前行到文件末尾所有内容;
  • cw, 修改当前单词(删除单词并进入 Insert 模式);

这是最基本的模式,也是 Vim 编辑器能高效编辑文本的基础,它把常用的 Operator 和 Motion 做了抽象,抽象成了一些简单字母,比如 d 代表删除操作,$代表句子末尾,而这些抽象符号又可以通过同一个公式组合使用,减轻了记忆负担。这是 Vim 非常优雅的地方。 不过有一个例外,如果你连续输入两个 Operator,就表示对当前行进行操作。比如 yy 表示复制当前行。


那 Vim 中有哪些常用的 Operator 呢,有以下这些:


至于 Motion,有更多,以下也是一些常用的:


当然还有更多,如果你感兴趣,可以在 Vim 的 Normal 模式下,输入以下命令查看完整的文档:

:h motion.txt

先存活下来


在成为 Vim 高手之前,我们的首要目标是先存活下来。这个目标其实并不难。


掌握基本的光标跳转,比如hlkj0^$ggG 等等,以及以上说的基本操作模式后,你大概率可以生存下来。当然知道不等于掌握,你需要频繁地练习把基本操作变成肌肉记忆。我一开始是跟着左耳朵耗子(在此纪念耗子叔)的文章《简明 VIM 练级攻略》练习,当时一旦有时间就打开文章,跟着内容逐条操作,一段时间后,我就真的存活下来了。


如果你也顺利存活了下来,在实际的开发过程中就已经可以使用 Vim 做一些编辑工作了,但可能总还是觉得哪儿哪儿不对劲,要完全行云流水还欠缺更多技巧。这个时候或许有必要去看看《Practival Vim》或者类似的书,更好地掌握 Vim 的设计理念以及许多细微的地方,同样配合不断的练习,我相信你迟早有一天会欣喜地发现自己在编码的时候几乎可以放弃鼠标了,这种喜悦或许类似于修仙小说中的破境。


恭喜你。


成为 Vim 高手的终极秘诀


其实没有秘诀。Vim 很快,但成为 Vim 高手是一个相对漫长的过程,在这个过程中你会掌握更多微妙的技能,比如如何更高效地使用 f{char} 命令更快地定位到某个字符。在生存下来以后,你唯一能做的就是每天使用 Vim。慢慢地, Vim 的使用会变成水和空气一样的自然存在,你从此离不开它。


如何每天使用 Vim 呢,以下是我的一些建议:

  • 把 Vim 变成日常开发工具。学习使用 Neovim,它提供了更好的插件和扩展机制,你如果愿意你甚至可以把 Neovim 配置成强大的 IDE。这里推荐一下掘金小册 Neovim 配置实战
  • 如果习惯使用 VSCode 或其他编辑器,可以安装相应的 Vim 插件
  • 如果你使用 Chrome 浏览器,你可以安装相应的 Vim 插件来提升浏览效率。
  • 平时习惯做笔记?那就使用一款支持 Vim 快捷键的笔记软件,比如我最喜欢的 Obsidian
  • 经常在 Cloud IDE 上写代码?建议使用一款支持 Vim 快捷键的 IDE,比如我常用的 Replit

总之,在我决定使用 Vim 提高编程效率以后,在任何编辑场景我都变得无法忍受没有 Vim 的存在,就是这么自然,它变成了我工作的一部分。Reddit 上有这样一条讨论,If using vim is a lifestyle/philosophy, what other products also fits into this lifestyle?,把 Vim 隐喻成一种生活方式/哲学确实很合适,Vim 和学习使用 Vim 隐含了一些有价值的东西,我相信大约有追求极致——更快更强的精神,坚持长期主义——忍受暂时痛苦,着眼长远的精神。或许也可以这么说,如果你有朝一日能成为 Vim 高手,你大概率也能做成其他许多困难的事。


少年们,加油。


作者:chaosflutter
链接:https://juejin.cn/post/7263676990736220220
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

晋升涨薪?不,晋升要命!

最近有个朋友来问我晋升的事,其实他没什么产出,只不过因为做的那块业务还不错,赚了好几个小目标,然后顺理成章的被提名了。所以他慌的要死! 你猜他来问什么内容?他什么都想问,又什么都问不出来,因为我也没晋升过。不过这并不妨碍我写点什么,谁让我会编呢? 我要不要提...
继续阅读 »

最近有个朋友来问我晋升的事,其实他没什么产出,只不过因为做的那块业务还不错,赚了好几个小目标,然后顺理成章的被提名了。所以他慌的要死!


你猜他来问什么内容?他什么都想问,又什么都问不出来,因为我也没晋升过。不过这并不妨碍我写点什么,谁让我会编呢?



我要不要提名晋升


我要不要提名晋升?你要是能问出这问题,要不刚毕业,要不加班加迷糊了!


晋升意味着涨薪,涨薪意味着每个月能多吃几顿海底捞,多看几部电影,这世道和谁过不去都不能和钱过不去。


但晋升往往是领导说了才算,除非你是向园,还有个董事长爷爷。所以,怎么说服他就成了一道槛。


工作三要素:A-能力;B-岗位;C-环境(其他人和事),而晋升基本只和 AB 有关。 果你在自己岗位上,已经承担了下一级该承担的责任。然后能力又达到了下一级所要求的水平,再不提名晋升就没天理了


例如我是P6,但是我一直在做P7的事情,同时在抗P7的责任,并且表现不错。那么我对标P7不就是既定事实嘛,既然是事实谁又能阻止你提名?


另外还有 C,如果把晋升与环境挂钩,晋升的理由变成了诸如  “如果我在他的位置上,我能做得比他更好”、“为什么他是P9而我是P8”,以这些理由提名晋升,属实是自寻烦恼,说不定明年你就成为人才输送给社会了。


从这个角度上看,我那位朋友已经晋升失败了。不过你们也不要太关注这个,缘分这个东西不是说有就有。就像你能恰好看见我这篇帖子,然后顺手点赞、收藏、在看一样



提名之后,如何准备答辩


晋升靠的是硬实力,以及10%的运气。 你想去吹牛也不是不行,就怕到时候下不来台。仅仅是 P6 升 P7 的答辩,上面坐着的都是P9级别的大佬和砖家。


PPT 以真实、简朴为主,凡是在 PPT 上花费超过10小时的,我觉得都有耍流氓的嫌疑。这些内容应该是这段时间你所积累的工作成果。平时没事拿个小笔记记一下,关键时刻它能像宋江一样救你的命。【推荐你用语雀,真的很好用】


有了 PPT,你得去讲出来吧。讲话作为一门艺术,对于我们理工科的同学还是有一定难度的,所以我建议你有空去参加下吐槽大会。没有条件?那就创造条件,公司里找几个段子手还不是轻而易举。相比之下,产品经理的优势比我们大多了。



如何把实力讲透?这里面是有一定技巧的,3分讲结果,7分讲过程。光讲结果不讲过程,30分钟的答辩,你5分钟就完成了,还是包含自我介绍的那种。


3分成效如何讲?——把我在当前岗位上,如何把手里的工作做上了一个新台阶 这种感觉讲出来,就是,因为你的努力而带来了什么改变?


7分过程如何讲?——把事情的复杂度、岗位的挑战、面临的困难讲清楚,把你做事的匠心讲清楚,你把你的做事的方法、思路讲清楚。说白了,就是“我解了一个挺难的题,我是这样那样解的”;


关于答辩与专家评委


评委扮演的角色很简单,评审的过程,就是评委向答辩人学习的过程。每个人都存在未涉及的领域,你看 ChatGPT 用了上亿的数据训练,花了几十亿美金,现在连小学数学题都解不出来


三个评委,花45分钟与答辩人进行深度交流,如果评委们都表示没有收获,学不到东西(无论是学到知识还是方法或者心态),那么答辩人晋升不通过,也不冤枉。


这个道理够简单吧,神雕侠侣里黄老邪为什么会和杨过拜把子,一方面是杨过的性格和黄老邪很像,另一方面是因为能从杨过那学到点东西。


回到现实,我们每个人都有直接的体会。如果有个大牛(至少他在当前的工作中是专业的)跟我们交流,我们一定有收获。如果对方十分平庸(或者在工作中能力一般),我们收获就比较少。


晋升通过,意味着什么


意味着加薪,年终奖多了点


意味着岗位(B)对你的能力(A)要求更大了一些


意味着你离 3.25 更近了一些,我知道的几个同事,每次晋升之后的第一个季度或半年度,都会拿一次3.25。


晋升不通过,意味着什么


恭喜你,终于松了口气!


作者:车辙cz
链接:https://juejin.cn/post/7244783947820449853
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 开发中的AES加密

iOS
前言 在iOS的日常开发中,特别是设计网络请求时,会用到加密算法,例如在客户端需要发起一个HTTP请求给服务端,其中会传递一些参数,为了防止参数在网络传输过程中被窃取或者篡改,我们就需要使用一些加密算法来对请求参数加密和签名。今天就重点介绍一下AES和HMAC...
继续阅读 »

前言


在iOS的日常开发中,特别是设计网络请求时,会用到加密算法,例如在客户端需要发起一个HTTP请求给服务端,其中会传递一些参数,为了防止参数在网络传输过程中被窃取或者篡改,我们就需要使用一些加密算法来对请求参数加密和签名。今天就重点介绍一下AES和HMAC_SHA256两个算法,因为服务端大多数都是使用java语言来编写,AES算法在iOS的Objective-C中和java的实现有些差异,本文重点介绍AES在iOS开发中的应用和需要注意的事项。


AES 加密算法简介


AES是一种典型的对称加密/解密算法,使用加密函数和密钥来完成对明文的加密,然后使用相同的密钥和对应的函数来完成解密。AES的优点在于效率非常高,相比RSA要高得多。AES共有ECB、CBC、CFB和OFB四种加密模式。


在iOS中的实现


Objective-C中支持AES的ECB和CBC两种模式。
1、电码本模式(Electronic Codebook Book (ECB))
这种模式主要是将明文划分为几个明文段,分块加密,但是加密密钥是相同的。
2、密码分组链接模式(Cipher Block Chaining (CBC))
这种模式是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。


ECB是最简单的一种模式,只需要传入待加密的内容和加密的key即可。(一般不推荐ECB模式)
CBC的特点是,除了需要传入加密的内容和加密的key,还需要传入初始化向量iv。即使每次加密的内容和加密的key相同,只要调整iv就可以让最终生成的密文不同。
在客户端和服务端之间传输数据一般是使用约定好的key对指定参数做AES的CBC加密,初始化向量可以随机动态生成,最终将生成好的密文和随机向量iv拼接在一起传给服务端。如:iv+密文。
iv是指定的长度如16位,这样服务端拿到客户端传输过来的数据可以先取前16位作为iv,剩余的是需要解析的密文。这么做大大提升了数据的安全性和破解难度。即使相同的带加密参数,因为有随机向量的参入,最终生成的密文也不相同。


iOS中一般使用#import <CommonCrypto/CommonCryptor.h>库中的这个函数:

CCCryptorStatus CCCrypt(
CCOperation op, /* kCCEncrypt, etc. */
CCAlgorithm alg, /* kCCAlgorithmAES128, etc. */
CCOptions options, /* kCCOptionPKCS7Padding, etc. */
const void *key,
size_t keyLength,
const void *iv, /* optional initialization vector */
const void *dataIn, /* optional per op and alg */
size_t dataInLength,
void *dataOut, /* data RETURNED here */
size_t dataOutAvailable,
size_t *dataOutMoved)
API_AVAILABLE(macos(10.4), ios(2.0));
  • CCOperationkCCEncrypt 加密,kCCDecrypt 解密
enum {
kCCEncrypt = 0,
kCCDecrypt,
};
typedef uint32_t CCOperation;
  • CCAlgorithm:加密算法、默认为AES
enum {
kCCAlgorithmAES128 = 0, /* Deprecated, name phased out due to ambiguity with key size */
kCCAlgorithmAES = 0,
kCCAlgorithmDES,
kCCAlgorithm3DES,
kCCAlgorithmCAST,
kCCAlgorithmRC4,
kCCAlgorithmRC2,
kCCAlgorithmBlowfish
};
typedef uint32_t CCAlgorithm;

  • CCOptions:加密模式
    ECBkCCOptionPKCS7Padding | kCCOptionECBMode
    CBCkCCOptionPKCS7Padding
enum {
/* options for block ciphers */
kCCOptionPKCS7Padding = 0x0001,
kCCOptionECBMode = 0x0002
/* stream ciphers currently have no options */
};
typedef uint32_t CCOptions;

  • key:密钥
  • keyLength:密钥长度
  • iviv 初始化向量,ECB 不需要。iv定长所以不需要长度(8字节)。
  • dataIn:加密/解密的数据
  • dataInLength:加密/解密的数据长度
  • dataOut:缓冲区(地址),存放密文/明文
  • dataOutAvailable:缓冲区大小
  • dataOutMoved:加密/解密结果大小

封装如下:

/**
* 解密字符串
*
* @param string 加密并base64编码后的字符串
* @param keyString 解密密钥
* @param iv 初始化向量(8个字节)
*
* @return 返回解密后的字符串
*/
- (NSString *)decryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv {

// 设置秘钥
NSData *keyData = [keyString dataUsingEncoding:NSUTF8StringEncoding];
uint8_t cKey[self.keySize];
bzero(cKey, sizeof(cKey));
[keyData getBytes:cKey length:self.keySize];

// 设置iv
uint8_t cIv[self.blockSize];
bzero(cIv, self.blockSize);
int option = 0;
if (iv) {
[iv getBytes:cIv length:self.blockSize];
option = kCCOptionPKCS7Padding;//CBC 加密!
} else {
option = kCCOptionPKCS7Padding | kCCOptionECBMode;//ECB加密!
}

// 设置输出缓冲区
NSData *data = [[NSData alloc] initWithBase64EncodedString:string options:0];
size_t bufferSize = [data length] + self.blockSize;
void *buffer = malloc(bufferSize);

// 开始解密
size_t decryptedSize = 0;

CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
self.algorithm,
option,
cKey,
self.keySize,
cIv,
[data bytes],
[data length],
buffer,
bufferSize,
&decryptedSize);

NSData *result = nil;
if (cryptStatus == kCCSuccess) {
result = [NSData dataWithBytesNoCopy:buffer length:decryptedSize];
} else {
free(buffer);
NSLog(@"[错误] 解密失败|状态编码: %d", cryptStatus);
}

return [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];
}

上文提到使用CBC模式,可以创建一个随机的iv:

#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonCryptor.h>

NSData *generateRandomIV(size_t length) {
NSMutableData *randomIV = [NSMutableData dataWithLength:length];
int result = SecRandomCopyBytes(kSecRandomDefault, length, randomIV.mutableBytes);

if (result == errSecSuccess) {
return randomIV;
} else {
// 处理生成随机IV失败的情况
return nil;
}
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
// 设置AES加密参数
NSData *key = [@"YourAESKey123456" dataUsingEncoding:NSUTF8StringEncoding];
size_t ivLength = kCCBlockSizeAES128; // IV长度为16字节(AES-128)

// 生成随机IV
NSData *randomIV = generateRandomIV(ivLength);

if (randomIV) {
// 使用randomIV进行AES加密
// 这里你可以调用相应的加密方法,传入randomIV作为IV参数
// 例如,使用CommonCrypto库进行AES加密
// 具体实现将取决于你所使用的加密库和算法

// 示例:在这里调用AES加密函数,传入key和randomIV
// ...
} else {
NSLog(@"生成随机IV失败");
}
}
return 0;
}

作者:lefht0pz408mdl7yvptpm23ci
链接:https://juejin.cn/post/7273674732448792639
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

把数据库里的未付款订单改成已付款,会发生什么

导言 不知道大家在网上购物的时候,有没有这样的念头,如果能把未付款的订单偷偷用一条SQL改成已付款,该多么美好啊。那么在实际开发过程中,我们应当如何保证数据库里的数据在保存后不会被偷偷更改? 大家好我是日暮与星辰之间,创作不易,如果觉得有用,求点赞,求收藏,...
继续阅读 »

导言


不知道大家在网上购物的时候,有没有这样的念头,如果能把未付款的订单偷偷用一条SQL改成已付款,该多么美好啊。那么在实际开发过程中,我们应当如何保证数据库里的数据在保存后不会被偷偷更改?



大家好我是日暮与星辰之间,创作不易,如果觉得有用,求点赞,求收藏,求转发,谢谢。



理论


在介绍具体的内容之间,先介绍MD5算法,简单的来说,MD5能把任意大小、长度的数据转换成固定长度的一串字符,经常玩大型游戏的朋友应该都注意到过,各种补丁包、端游客户端之类的大型文件一般都附有一个MD5值,用于确保你下载文件的完整性。那么在这里,我们可以借鉴其思想,对订单的某些属性进行加密计算,得出来一个 MD5值一并保存在数据库当中。从数据库取出数据后第一时间进行校验,如果有异常更改,那么及时抛出异常进行人工处理。


实现


道理我都懂,但是我要如何做呢,别急,且听我一一道来。


这种需求听起来并不强绑定于某个具体的业务需求,这就要用到了我们熟悉的鼎鼎有名的AOP(面向切面编程)来实现。


首先定义四个类型的注解作为AOP的切入点。@Sign@Validate都是作用在方法层面的,分别用于对方法的入参进行加签和验证方法的返回值的签名。@SignField用于注解关键的不容篡改的字段。@ValidateField用于注解保存计算后得出的签名值。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sign {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Validate {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignField {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidField {
}

以订单的实体为例 sn,amt,status,userId就是关键字段,绝不能允许有人在落单到数据库后对这些字段偷偷篡改。

public class Order {
@SignField
private String sn;
@SignField
private String amt;
@SignField
private int status;
@SignField
private int userId;
@ValidField
private String sign;
}

下面就到了重头戏的部分,如何通过AOP来进行实现。


1. 定义切入点

@Pointcut("execution(@com.example.demo.annotations.Sign * *(..))")
public void signPointCut() {

}

@Pointcut("execution(@com.example.demo.annotations.Validate * *(..))")
public void validatePointCut() {

}

2.环绕切入点

@Around("signPointCut()")
public Object signAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (Object o : args) {
System.out.println(o);
sign(o);
}
Object res = pjp.proceed(args);
return res;
}

@Around("validatePointCut()")
public Object validateAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Object res = pjp.proceed(args);
valid(res);
return res;
}

3. 签名的实现

  • 获取需要签名字段
private Map<String, String> getSignMap(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = new HashMap<>();
for (Field f : o.getClass().getDeclaredFields()) {
System.out.println(f.getName());
for (Annotation annotation : f.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(SignField.class)) {
String value = "";
f.setAccessible(true);
fieldNameToValue.put(f.getName(), f.get(o).toString());
}
}
}
return fieldNameToValue;
}
  • 计算出签名值,这里在属性名和属性值以外加入了我的昵称以防止他人猜测,同时使用了自定义的分隔符来加强密码强度。
private String getSign(Map<String, String> fieldNameToValue) {
List<String> names = new ArrayList<>(fieldNameToValue.keySet());
StringBuilder sb = new StringBuilder();
for (String name : names)
sb.append(name).append("@").append(fieldNameToValue.get(name));
System.out.println(sb.append("日暮与星辰之间").toString());
String signValue = DigestUtils.md5DigestAsHex(sb.toString().getBytes(StandardCharsets.UTF_8));
return signValue;
}

  • 找到保存签名的字段
private Field getValidateFiled(Object o) {
for (Field f : o.getClass().getDeclaredFields()) {
for (Annotation annotation : f.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(ValidField.class)) {
return f;
}
}
}
return null;
}

  • 对保存签名的字段进行赋值
public void sign(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = getSignMap(o);
if (fieldNameToValue.isEmpty()) {
return;
}
Field validateField = getValidateFiled(o);
if (validateField == null)
return;
String signValue = getSign(fieldNameToValue);
validateField.setAccessible(true);
validateField.set(o, signValue);
}

  • 对从数据库中取出的对象进行验证
public void valid(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = getSignMap(o);
if (fieldNameToValue.isEmpty()) {
return;
}
Field validateField = getValidateFiled(o);
validateField.setAccessible(true);
String signValue = getSign(fieldNameToValue);
if (!Objects.equals(signValue, validateField.get(o))) {
throw new RuntimeException("数据非法");
}

}

使用示例


对将要保存到数据库的对象进行签名

@Sign
public Order save( Order order){
orderList.add(order);
return order;
}

验证从数据库中取出的对象是否合理

@Validate
public Order query(@ String sn){
return orderList.stream().filter(e -> e.getSn().equals(sn)).findFirst().orElse(null);
}

好文分享 ⬇️
从Offer收割机到延毕到失业再到大厂996,二零二二我的兵荒马乱 - 掘金


另类年终总结:在煤老板开的软件公司实习是怎样一种体验? - 掘金


第一次值守双十一,居然没有任何意外发生?! - 掘金


大厂996三个月,我曾迷失了生活的意义,努力找回中 - 掘金


阿里实习三个月,我学会了面试时讲好自己的项目,欢迎提问 - 掘金


迟到的苏州微软实习历险记 - 掘金


什么时候要用到本地缓存,比Redis还要快?怎么用? - 掘金


作者:日暮与星辰之间
链接:https://juejin.cn/post/7144378128914186270
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

🤔️《你不知道的JavaScript》到底讲了些什么?

开始之前 在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面...
继续阅读 »

开始之前


在计算机科学的领域中,JavaScript是一门无法忽视的重要语言,深受许多开发者的喜爱。然而,它背后隐藏的复杂性和奥秘许多开发者并不为知。《你不知道的JavaScript》这三卷之作,对我个人而言真的算是常看常新,从刚从事前端开发到如今已经独当一面,从这本书中受益良多。因此在多次阅读后我选择用内容梗概+案例解析的形式将其精华部分记录下来,以供个人翻阅和与大家分享,那么我们开始吧


上卷


上卷主要针对语言核心的一些关键概念,如作用域、闭包、this等。本文将为笔者阅读过程中所总结和提炼的关键知识点与经典案例


1. 作用域是什么?


内容概览


本章介绍了JavaScript中的作用域概念,解释了变量如何被储存以及如何被引用。


实例分析

var a = 2;

function foo() {
var a = 3;
console.log(a); // 3
}

foo();

console.log(a); // 2

在这个例子中,我们看到a在全局作用域和foo函数的作用域中都有定义。函数内部的a不会影响到全局作用域中的a


2. 词法作用域


内容概览


词法作用域意味着作用域是由函数声明的位置来决定的,而不是函数调用的位置。


实例分析

function foo() {
console.log(a);
}

function bar() {
var a = 3;
foo();
}

var a = 2;

bar(); // 2

尽管foo函数在bar函数内部被调用,但foo函数的词法作用域仍然使其能够访问外部的变量a,所以输出为2。


3. 函数与块作用域


内容概览


介绍了函数作用域和块作用域,以及如何利用它们来避免变量冲突和其他问题。


实例分析

if (true) {
let a = 2;
console.log(a); // 2
}

console.log(a); // ReferenceError

使用let定义的变量具有块作用域,只能在声明它的块中访问。


4. 提升


内容概览


解释了提升(hoisting)现象,即变量和函数声明会被移动到它们所在的作用域顶部。


实例分析

foo(); // "Hello"

function foo() {
console.log("Hello");
}

尽管函数foo在调用之后被声明,但由于提升,它仍然可以正常调用。


5. 作用域闭包


内容概览


解释了闭包是如何工作的,以及它在JavaScript中的重要性。


实例分析

function makeGreeting(greeting) {
return function(name) {
console.log(greeting + ", " + name);
};
}

let sayHello = makeGreeting("Hello");
sayHello("Alice"); // "Hello, Alice"

sayHello函数是一个闭包,它记住了创建它时的作用域,因此能够访问greeting变量。


6. 词法分析和语法分析


实例分析


来看以下代码:

function add(x, y) {
return x + y;
}

let sum = add(5, 7);

在词法分析阶段,这段代码可能被分解为多个词法单元:function, add, (, x, ,, y, ), {, return, +, ;, }, let, =, 5, 7 等。然后,语法分析器会将这些词法单元组合成AST。


7. L查询与R查询


实例分析

function calculateArea(radius) {
const pi = 3.141592653589793;
return pi * radius * radius;
}

let r = 5;
let area = calculateArea(r);

在这个例子中,考虑let area = calculateArea(r);这行代码。对于calculateArea,它是RHS查询,因为我们需要获得这个函数的引用来执行它。而r也是RHS查询,因为我们正在获取它的值来传递给函数。


calculateArea函数内,pi和两次radius的查询都是RHS查询,因为我们获取它们的值来执行乘法操作。而return语句中的计算结果则赋值给了隐式的返回值,这涉及到LHS查询。


对于let r = 5;,这里的r是一个LHS查询,因为我们给它赋值了。


中卷


中卷的内容相比上卷来说更加深入且晦涩,其中包括令初学者头昏脑胀的面向对象编程与this原型链相关的知识,我将以更多的篇幅和更深入的案例来帮助大家进行理解


1. 对象


实例分析 1


使用工厂函数和构造器来创建对象:

function createPerson(name, age) {
return {
name,
age,
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
};
}

const person1 = createPerson('Alice', 30);
person1.greet();

深入分析


这是一个工厂函数的例子,允许我们快速创建具有相似属性和方法的对象。在此,greet方法是每个对象的一部分,这可能导致内存浪费,因为每次创建新对象时,都会为greet方法分配新的内存。


实例分析 2


使用getters和setters:

const book = {
title: 'In Search of Lost Time',
author: 'Marcel Proust',
get description() {
return `${this.title} by ${this.author}`;
},
set description(value) {
[this.title, this.author] = value.split(' by ');
}
};

book.description = '1984 by George Orwell';
console.log(book.title); // Outputs: 1984

深入分析


这个案例展示了如何利用对象的getters和setters来动态地管理对象的属性。通过setter,我们能够同时更新titleauthor,而getter则为我们提供了书的描述。


2. 类


实例分析 1


多态的使用:

class Animal {
makeSound() {
console.log('Some generic sound');
}
}

class Dog extends Animal {
makeSound() {
console.log('Woof');
}
}

const animal1 = new Animal();
const animal2 = new Dog();

animal1.makeSound(); // Outputs: Some generic sound
animal2.makeSound(); // Outputs: Woof

深入分析


多态是面向对象编程中的一个关键概念,允许我们创建能够以多种形式表现的对象。在此,我们看到Dog类重写了Animal类的makeSound方法,实现了多态。


实例分析 2


静态方法的使用:

class MathUtility {
static add(x, y) {
return x + y;
}
}

console.log(MathUtility.add(5, 3)); // Outputs: 8

深入分析


这个案例展示了如何在类中使用静态方法。与实例方法不同,静态方法不需要创建类的实例就可以被调用。它们通常用于执行与类的实例无关的操作。


3. 原型


实例分析


一个动态添加到原型的方法:

function Cat(name) {
this.name = name;
}

Cat.prototype.purr = function() {
console.log(`${this.name} is purring.`);
};

const whiskers = new Cat('Whiskers');
whiskers.purr(); // Outputs: Whiskers is purring.

深入分析


在此例中,我们后期将purr方法添加到Cat的原型中。这意味着即使在添加此方法后创建的所有Cat实例都可以访问它。这展示了原型继承的动态性质:我们可以在任何时候修改原型,这些更改会反映在所有继承了那个原型的对象上。


4. this和对象原型


JavaScript中的this是一个非常深入且经常被误解的主题。this并不是由开发者选择的,它是由函数调用时的条件决定的。


实例分析


考虑以下场景:

function showDetails() {
console.log(this.name);
}

const obj1 = {
name: 'Object 1',
display: showDetails
};

const obj2 = {
name: 'Object 2',
display: showDetails
};

obj1.display(); // Outputs: Object 1
obj2.display(); // Outputs: Object 2

深入分析


在这里,showDetails函数查看this.name。当它作为obj1的方法被调用时,this指向obj1。当它作为obj2的方法被调用时,this指向obj2。这说明了this的动态性质:它是基于函数如何被调用的。


5. 原型链


当试图访问一个对象的属性或方法时,JavaScript会首先在该对象本身上查找。如果未找到,它会在对象的原型上查找,然后是原型的原型,以此类推,直到找到该属性或到达原型链的末尾。


实例分析

function Animal(sound) {
this.sound = sound;
}

Animal.prototype.makeSound = function() {
console.log(this.sound);
}

function Dog() {
Animal.call(this, 'Woof');
}

Dog.prototype = Object.create(Animal.prototype);

const dog = new Dog();
dog.makeSound(); // Outputs: Woof

深入分析


当我们调用dog.makeSound()时,JavaScript首先在dog对象上查找makeSound。未找到后,它会在Dog的原型上查找。还是未找到,然后继续在Animal的原型上查找,最后找到并执行它。


6. 行为委托


行为委托是原型的一种使用模式,涉及到对象之间的关系,而不仅仅是克隆或复制。


实例分析

const Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log(this.id); }
};

const XYZ = Object.create(Task);

XYZ.prepareTask = function(ID, Label) {
this.setID(ID);
this.label = Label;
};

XYZ.outputTaskDetails = function() {
this.outputID();
console.log(this.label);
};

const task = Object.create(XYZ);
task.prepareTask(1, 'create demo for delegation');
task.outputTaskDetails(); // Outputs: 1, create demo for delegation

深入分析


XYZ不是Task的复制,它链接到Task。当我们在XYZ对象上调用setIDoutputID方法时,这些方法实际上是在Task对象上运行的,但this指向的是XYZ。这就是所谓的委托:XYZ在行为上委托给了Task


下卷


下卷的内容相较于中卷就基础了很多,更偏向于实际应用方向


1. 类型和语法


实例分析 - 类型转换


考虑以下的隐式类型转换:

var a = "42";
var b = a * 1;
console.log(typeof a); // "string"
console.log(typeof b); // "number"

深入分析


在这里,变量a是一个字符串,但当我们尝试与数字进行乘法操作时,它会被隐式地转换为一个数字。这是因为乘法操作符期望它的操作数是数字,因此JavaScript会尝试将字符串a转换为一个数字。


2. 异步和性能


实例分析 - Promises

function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched!");
}, 2000);
});
}

fetchData().then(data => {
console.log(data); // Outputs: "Data fetched!" after 2 seconds
});

深入分析


Promises 提供了一种更简洁、更具可读性的方式来处理异步操作。在上面的例子中,fetchData函数返回一个Promise。setTimeout模拟了异步数据获取,数据在2秒后可用。当数据准备好后,resolve函数被调用,then方法随后执行,输出数据。


3. ES6及其以上的特性


实例分析 - 使用箭头函数

const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]

深入分析


箭头函数提供了一种更简洁的方式来定义函数,尤其是对于那些简短的、无状态的函数来说。在上述例子中,我们使用箭头函数简洁地定义了一个函数,该函数将其输入值乘以2,并使用map方法将其应用到一个数字数组中。


实例分析 - 使用async/await

async function fetchDataAsync() {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
}

fetchDataAsync().then(data => console.log(data));

深入分析


async/await是ES7引入的特性,允许以同步的方式编写异步代码。在这个案例中,fetchDataAsync函数是一个异步函数,这意味着它返回一个Promise。await关键字使我们能够等待Promise解析,然后继续执行后面的代码。这消除了回调地狱,使异步代码更容易阅读和维护。


4. 迭代器和生成器


实例分析 - 使用生成器函数

function* numbersGenerator() {
yield 1;
yield 2;
yield 3;
}

const numbers = numbersGenerator();

console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3

深入分析


生成器函数使用function*声明,并且可以包含一个或多个yield表达式。每次调用生成器对象的next()方法时,函数都会执行到下一个yield表达式,并返回其值。这使我们能够按需产生值,非常适用于大数据集或无限数据流。


5. 增强的对象字面量


实例分析

const name = "Book";
const price = 20;

const book = {
name,
price,
describe() {
return `${this.name} costs ${this.price} dollars.`;
}
};

console.log(book.describe()); // "Book costs 20 dollars."

深入分析


增强的对象字面量允许我们在声明对象时使用更简洁的语法。在这里,我们直接使用变量名作为键,并使用简短的方法定义形式。这使得对象声明更为简洁和可读。


6. 解构赋值


实例分析

const user = {
firstName: "Alice",
lastName: "Smith"
};

const { firstName, lastName } = user;

console.log(firstName); // Alice
console.log(lastName); // Smith

深入分析


解构赋值允许我们从数组或对象中提取数据,并赋值给新的或已存在的变量。在此例中,我们从user对象中提取了firstNamelastName属性,并将它们赋值给了同名的新变量。


7. 模块


实例分析 - ES6模块导入和导出

// math.js
export function add(x, y) {
return x + y;
}

export function subtract(x, y) {
return x - y;
}

// app.js
import { add, subtract } from './math.js';

console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2

结语


经过对《你不知道的JavaScript》上、中、下三卷的深入探索,我们更加清晰地理解了JavaScript这门语言的复杂性、深度和强大之处。这不仅仅是关于语法或是新特性,更是关于理解其背后的哲学和设计思想。作为开发者,真正的掌握并不只是会用,而是要知其所以然。此书为我们打开了一扇探索JavaScript的大门,但真正的旅程,才刚刚开始。我们的每一步前行,都是为了更好地理解、更精准地应用,为编写出更高效、更优雅的代码而努力。


作者:J3LLYPUNK
链接:https://juejin.cn/post/7274839607656890429
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

话说工作的“边界感”

一句话的合作例子 今天有一个业务的运营同学匆匆忙忙找到我,说要跟我这边有一个问题,业务要合作,然后已经提前和我老板打过招呼了。事情是这样的,我这边负责的是工作台门户,然后作为一个平台业务,有大量的客户需要找到对应的服务商来给自己定制门户。然后这位同学负责的是定...
继续阅读 »


一句话的合作例子


今天有一个业务的运营同学匆匆忙忙找到我,说要跟我这边有一个问题,业务要合作,然后已经提前和我老板打过招呼了。事情是这样的,我这边负责的是工作台门户,然后作为一个平台业务,有大量的客户需要找到对应的服务商来给自己定制门户。然后这位同学负责的是定制业务,所以要统一把所有的定制业务全部收口,但是这位定制同学的业务没有对应的技术研发同学,所以他就找到我的老板同步了这个情况。


分工协作的本质


其实问题的合作方式是比较简单的,但是当她跟我说最终客户定制界面也由我来开发的时候,比如定制的费用是多少、定制的时间要求等等,我就觉得问题有些奇怪了。因为按照常理来说,我负责的是工作台,但是由于有定制业务相关的逻辑,所以我要处理一定的业务逻辑,但是让我去承担这个定制页面的开发,我觉得是有问题的。


举一个简单的例子,假如我现在是一个博物馆,原来用户是直接可以免费没有任何阻挡地进入博物馆的,但是突然有一天市政府说所有公共设施要收费了,那么对于博物馆的工作人员来说肯定是支持的,但是突然你又告诉我,我这个博物馆还要去维护全市统一的收费系统,这个就是不合理的。哪怕他找我的主管沟通结果也是一样,因为我和我的主管是属于博物馆体系的工作人员,他也没有义务和责任去维护整个所有的公共设施的收费系统。但是作为公共设施系统的一部分,如果有统一的收费规则,那么对于博物馆来说也是要遵守的。


所以这面就引出了我对于业务边界上面的一个思考。我经常看到同学给我转发一段话,说跟你老板打沟通了业务的合作情况,你的老板觉得非常不错,于是这位同学就匆匆忙忙的找到我来开始谈业务,谈实施细节并且需要我快速落地。而实际上这种所谓的业务协同的情况大部分也只会停留在沟通的层面,在最终落地的时候,往往和业务同学的预期不相符。在业务同学眼里看来,就是你们阴奉阳违,恨不得马上就开始投诉。


这里面非常核心的一个误区就是业务同学往往没有划清业务界限和系统界限的边界。对于业务同学来说,边界可能不会那么明显,但对于一个系统开发的同学来说,业务和边界是非常明显的,因为系统是物理存在的,有着天然的“隔离”。所以对于业务同学,如果想要顺畅的推动业务,必须要事先清晰的划分参与方的角色和业务边界,并且可以进一步了解到系统边界在哪里。


这个由谁来做就涉及到了一个很大权责问题。简单来说就是我做了有什么好处,换句话来说做这件事和我的职务目标有什么关系?如果没有关系,我为什么要做?就算同一个公司,也有很多需要完成的事,比如公司保洁不到位,我作为公司的员工,是否也立即从事保洁?


如果是我的职务目标,我的责任有多少?我承担了既定的责任,那我是否能够承担起对应的权利?在我上次借用的博物馆的例子可以看到,如果我承担了全市的公共系统的收费设施的维护,那么我的权利在哪里?如果我的权利只是在博物馆这一个地方的收费上面,那么这就变成了权责不对等。


但是如果我做成了全市公共收费系统,并且能掌管全市所有公共设施的收费业务,那么对于这个收费系统的开发权则是相等的,但是对于我本身职务的权责又是不等的,因为公司请我来管理博物馆的,而非管理整个全市的收费系统。


所以在思考业务推进的时候,首先就要思考系统的边界和权责对等关系,如果这一层面没有理清楚的话,合作大概率是不能完成的。而很多的业务同学就以“我和你老板谈好的东西,为什么你不去做”这么简单的方式来拷问协同关系,我觉得是非常的幼稚的。


所以我希望其实我们在去和别人沟通业务的时候,往往要带着权责,带着边界的思考,去和对方去讨论,去协商,去沟通。简单来说,我在跟你聊之前,我要知道你的系统,你的业务边界在哪里?我跟你聊的时候,我要清晰地告诉你,这个事情做了对你有什么好处,对我有什么好处,哪部分应该你做,哪部分应该我来做。只有在这样的一种沟通方式下面才是真正合理的,真正是可以落地的沟通和协作方式。


而在这些问题没有达成一致之前,由谁来做都没有定下来的时候,应该先去往上升,在顶层设计里面去规划去重新思考如何从组织设计的方式去让业务协作自然的发生。


总结


这里再总结一下,这里是一个小的心得。这个案例也告诉我们,我们去沟通协同的时候要有边界感,包括业务的边界和系统的边界。只有把边界理顺了,合作才有可能。


作者:ali老蒋
链接:https://juejin.cn/post/7233808084085309477
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

React的并发悖论

大家好,我卡颂。 当一个React应用逻辑变得复杂后,组件render花费的时间会显著增长。如果从组件render到视图渲染期间消耗的时间过长,用户就会感知到页面卡顿。 为了解决这个问题,有两个方法:让组件render的过程从同步变为异步,这样render过程...
继续阅读 »

大家好,我卡颂。


当一个React应用逻辑变得复杂后,组件render花费的时间会显著增长。如果从组件render视图渲染期间消耗的时间过长,用户就会感知到页面卡顿。


为了解决这个问题,有两个方法:

  1. 组件render的过程从同步变为异步,这样render过程页面不会卡死。这就是并发更新的原理

  2. 减少需要render的组件数量,这就是常说的React性能优化


通常,对于不同类型组件,我们会采取以上不同的方法。比如,对于下面这样的有耗时逻辑的输入框,方法1更合适(因为并发更新能减少输入时的卡顿):

function ExpensiveInput({onChange, value}) {
// 耗时的操作
const cur = performance.now();
while (performance.now() - cur < 20) {}

return <input onChange={onChange} value={value}/>;
}

那么,能不能在整个应用层面同时兼顾这2种方式呢?答案是 —— 不太行。


这是因为,对于复杂应用,并发更新与性能优化通常是相悖的。就是本文要聊的 —— 并发悖论。


欢迎加入人类高质量前端交流群,带飞


从性能优化聊起


对于一个组件,如果希望他非必要时不render,需要达到的基本条件是:props的引用不变。


比如,下面代码中Child组件依赖fn props,由于fn是内联形式,所以每次App组件render时引用都会变,不利于Child性能优化:

function App() {
return <Child fn={() => {/* xxx */}}/>
}

为了Child性能优化,可以将fn抽离出来:

const fn = () => {/* xxx */}

function App() {
return <Child fn={fn}/>
}

fn依赖某些props或者state时,我们需要使用useCallback

function App({a}) {
const fn = useCallback(() => a + 1, [a]);
return <Child fn={fn}/>
}

类似的,其他类型变量需要用到useMemo


也就是说,当涉及到性能优化时,React的代码逻辑会变得复杂(需要考虑引用变化问题)。


当应用进一步复杂,会面临更多问题,比如:

  • 复杂的useEffect逻辑

  • 状态如何共享


这些问题会与性能优化问题互相叠加,最终导致应用不仅逻辑复杂,性能也欠佳。


性能优化的解决之道


好在,这些问题有个共同的解决方法 —— 状态管理。


上文我们聊到,对于性能优化,关键的问题是 —— 保持props引用不变。


在原生React中,如果a依赖bb依赖c。那么,当a变化后,我们需要通过各种方法(比如useCallbackuseMemo)保持bc引用的稳定。


做这件事情本身(保持引用不变)对开发者来说就是额外的心智负担。那么,状态管理是如何解决这个问题的呢?


答案是:状态管理库自己管理所有原始状态以及派生状态。


比如:

  • Recoil中,基础状态类型被称为Atom,其他派生状态都是基于Atom组合而来

  • Zustand中,基础状态都是create方法创建的实例

  • Redux中,维护了一个全局状态,对于需要用到的状态通过selector从中摘出来


这些状态管理方案都会自己维护所有的基础状态与派生状态。当开发者从状态管理库中引入状态时,就能最大限度保持props引用不变。


比如,下例用Zustand改造上面的代码。由于状态a和依赖afn都是由Zustand管理,所以fn的引用始终不变:

const useStore = create(set => ({
a: 0,
fn: () => set(state => ({ a: state.a + 1 })),
}))


function App() {
const fn = useStore(state => state.fn)
return <Child fn={fn}/>
}

并发更新的问题


现在我们知道,性能优化的通用解决途径是 —— 通过状态管理库,维护一套逻辑自洽的外部状态(这里的外部是区别于React自身的状态),保持引用不变。


但是,这套外部状态最终一定会转化为React的内部状态(再通过内部状态的变化驱动视图更新),所以就存在状态同步时机的问题。即:什么时候将外部状态与内部状态同步?


在并发更新之前的React中,这并不是个问题。因为更新是同步、不会被打断的。所以对于同一个外部状态,在整个更新过程中都能保持不变。


比如,在如下代码中,由于List组件的render过程不会打断,所以list在遍历过程中是稳定的:

function List() {
const list = useStore(state => state.list)
return (
<ul>
{list.map(item => <Item key={item.id} data={item}/>}
</ul>
)
}

但是,对于开启并发更新的React,更新流程可能中断,不同的Item组件可能是在中断前后不同的宏任务中render,传递给他们的data props可能并不相同。这就导致同一次更新,同一个状态(例子中的list)前后不一致的情况。


这种情况被称为tearing(视图撕裂)。


可以发现,造成tearing的原因是 —— 外部状态(状态管理库维护的状态)与React内部状态的同步时机出问题。


这个问题在当前React中是很难解决的。退而求其次,为了让这些状态库能够正常使用,React专门出了个hook —— useSyncExternalStore。用于将状态管理库触发的更新都以同步的方式执行,这样就不会有同步时机的问题。


既然是以同步的方式执行,那肯定没法并发更新啦~~~


总结


实际上,凡是涉及到自己维护了一个外部状态的库(比如动画库),都涉及到状态同步的问题,很有可能无法兼容并发更新。


所以,你会更倾向下面哪种选择呢:

  1. care并发更新,以前React怎么用,现在就怎么用

  2. 根据项目情况,平衡并发更新与性能优化的诉求


作者:魔术师卡颂
链接:https://juejin.cn/post/7223787990950232125
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

聊一聊过度设计!

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码...
继续阅读 »

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码,过度设计的代码有比较高的理解成本。说这么多,到底什么是过度设计?


什么是过度设计?


  为了解释清楚,我这里用个类比,假如你想拧一颗螺丝,正常的解决方案是找一把螺丝刀,这很合理对吧。 但是有些人就想:“我就要一个不止能拧螺丝的工具,我想要一个可以干各种事的工具!”,于是就花大价钱搞了把瑞士军刀。在你解决“拧螺丝”问题的时候,重心早已从解决问题转变为搞一个工具,这就是过度设计。

   再举个更技术的例子,假设你出去面试,面试官让你写一个程序,可以实现两个数的加减乘除,方法出入参都给你提供好了 int calc(int x, int y, char op),普通程序员可能会写出以下实现。

    public int calc(int x, int y, int op) {
if (op == '+') {
return x + y;
} else if (op == '-') {
return x - y;
} else if (op == '*') {
return x * y;
} else {
return x / y;
}
}

  而高级程序员会运用设计模式,写出这样的代码:

public interface Strategy {
int calc(int x, int y);
}

public class AddStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x + y;
}
}

public class MinusStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x - y;
}
}
/**
* 其他实现
*/
public class Main {
public int calc(int x, int y, int op) {
Strategy add = new AddStrategy();
Strategy minux = new MinusStrategy();
Strategy multi = new MultiStrategy();
Strategy div = new DivStrategy();
if (op == '+') {
return add.calc(x, y);
} else if (op == '-') {
return minux.calc(x, y);
} else if (op == '*') {
return multi.calc(x, y);
} else {
return div.calc(x, y);
}
}
}

  策略模式好处在于将计算(calc)和具体的实现(strategy)拆分,后续如果修改具体实现,也不需要改动计算的逻辑,而且之后也可以加各种新的计算,比如求模、次幂……,扩展性明显增强,很是牛x。 但光从代码量来看,复杂度也明显增加。回到我们原始的需求上来看,如果我们只是需要实现两个整数的加减乘除,这明显过度设计了。


过度设计的坏处


  个人总结过度设计有两大坏处,首先就是前期的设计和开发的成本问题。过度设计的方案,首先设计的过程就需要投入额外的时间成本,其次越复杂的方案实现成本也就越高、耗时越长,如果是在快速迭代的业务中,这些可能都会决定到业务的生死。其次即便是代码正常上线后,其复杂度也会导致后期的维护成本高,比如当你想将这些代码交接给别人时,别人也需要付出额外的学习成本。


  如果成本问题你都可以接受,接下来这个问题可能影响更大,那就是过度设计可能会影响到代码的灵活性,这点听起来和做设计的目的有些矛盾,做设计不就是为了提升代码的灵活性和扩展性吗!实际上很多过度设计的方案搞错了扩展点,导致该灵活的地方不灵活,不该灵活的地方瞎灵活。在机器学习领域,有个术语叫做“过拟合”,指的是算法模型在测试数据上表现完美,但在更广泛的数据上表现非常差,模式缺少通用性。 过度设计也会出现类似的现象,就是缺少通用性,在面对稍有差异的需求上时可能就需要伤筋动骨级别的改造了。


如何避免过度设计


  既然过度设计有着成本高和欠灵活的问题,那如何避免过度设计呢!我这里总结了几个方法,希望可以帮到大家。


充分理解问题本身


  在设计的过程中,要确保充分理解了真正的问题是什么,明确真正的需求是什么,这样才可以避免做出错误的设计。


保持简单


  过度设计毫无例外都是复杂的设计,很多时候未来有诸多的不确定性,如果过早的针对某个不确定的问题做出方案,很可能就白做了,等遇到真正问题的时候再去解决问题就行。


小步快跑


  不要一开始就想着做出完美的方案,很多时候优秀的方案不是设计出来的,而是逐渐演变出来的,一点点优化已有的设计方案比一开始就设计出一个完美的方案容易得多。


征求其他人的意见


  如果你不确定自己的方案是不是过度设计了,可以咨询下其他人的,尤其是比较资深的人,交叉验证可以快速让你确认问题。


总结


  其实在业务的快速迭代之下,很难判定当前的设计是欠设计还是过度设计,你当前设计了一个简单的方案,未来可能无法适应更复杂的业务需求,但如果你当前设计了一个复杂的方案,有可能会浪费时间……。 在面对类似这种不确定性的时候,我个人还是比较推崇大道至简的哲学,当前用最简单的方案,等需要复杂性扩展的时候再去重构代码。


作者:xindoo
链接:https://juejin.cn/post/7204423284905738298
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

👣 我在语雀做图册 - 更整齐的瀑布流算法

web
🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师亦池。介绍了语雀图册功能背后的更整齐的瀑布流算法,还有一段和 chatGPT 纠缠的故事 🤪,一起来看看~ 🏞️ 介绍一下图册 先来看看我们语雀年前上线的图册功能: 欢迎大家使用图册更好的展示自己的图片,...
继续阅读 »

🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师亦池。介绍了语雀图册功能背后的更整齐的瀑布流算法,还有一段和 chatGPT 纠缠的故事 🤪,一起来看看~



🏞️ 介绍一下图册


先来看看我们语雀年前上线的图册功能:
image.png


image.png



欢迎大家使用图册更好的展示自己的图片,秀秀最近的摄影作品、po一下最近的好吃的好玩的、晒几张靓照~
目前图册只是上了一个基础的初版,还有很多地方在体验和产品设计上也会继续优化,包括针对单张图的删除、排序,图册的尺寸调整,更快捷的把各种来源的图片放进图册里,大家有一些想法也欢迎提建议~



开发故事


🧐 瀑布流能不能再整齐一些


瀑布流是一个不新鲜的布局方式了,看到这个我第一反应自然是使用社区的开源库按需裁剪一下用起来。刚发布时也是这么上线的。扒过代码参考的开源库有:



但第一版本其实回发生下图左侧尴尬的情况:
image.png
肉眼可见我们要的是上图右侧的效果。


常见瀑布流算法的问题



原因:社区主流的瀑布流计算思路都是将已知高度的图片(实现上可以是图片加载完成后获取高度触发重新布局)分发了列容器里,每列记录实时高度,对于每一张新来的图片分发规则是放入最短的那一列。专业点说是贪心算法的思想。



所以当最后一张是长图时就会对布局的齐平性导致很大的冲击。(当然这不是说社区的方案都low,开源产品可能更多考虑了普适情况,譬如可能无法提前知道所有图片尺寸信息,全部加载完再重新布局一次又给用户带来干扰,甚至是懒加载场景更不好优雅的展示处理。)


在语雀编辑器场景,我们对于要布局的那批图片是能拿到宽高信息的,完全可以对所有图片整体考虑,计算一个最优结果再渲染,可以做到不被最后一张长图影响整体。


一开始我觉得这是个单纯的算法问题,可以抽象成将一个数字数组拆分成n个数组,使每个数组的数字和尽量接近,我觉得应该是有一种经典算法来解决这类问题的,譬如动态规划、背包问题之类的。


这么经典的问题不如问chatGPT吧,此处插入一段和chatGPT纠缠的故事。结论是它没给我找到靠谱答案。感兴趣的可以展开后面章节的折叠块看看这个让人哭笑不得的过程🙄。


💁‍♀️ 分析一下


chatGPT没能给我正确答案,我又是个基础算法的渣渣,想先找个方向再进去研究怎么实现,于是请教了一下一个酷爱刷算法题的师妹,得到的方向是:“这是个负载均衡类型问题,具有NP hard复杂度,无法获得最佳解,只能求近似最优解,用动态规划的思想是没错的”。


啥是NP hard复杂度,可以看后面的【基础知识】章节的科普。我也不清楚怎么证明这真的是一个NP hard复杂度的问题,但基础知识告诉我这类复杂度的问题往往复杂度是阶乘级别的,就是不是我们常见的O(n)、O(logn)、O(n^2)这种经典算法的复杂度,他们的复杂度叫做有多项式解。阶乘级别意味着暴力穷举,这往往是计算机无法接受的时间,也毫无算法可言。


咱这个问题,求解最优解时,每一张图片的摆放都影响着后面图片的位置,每张图之间都有关联,想想似乎确实只有穷举才能真正的找到最优解。加上对师妹算法水平的信任,我开始把问题缩减到动态规划领域。


那就拆解子问题,先计算子问题的近似最优解。


🏄‍ ♀️解决方案


核心思想:




  1. 计算平均值,让每一组的和尽量接近均值,最终每组和的差异才会最小

  2. 将原数组arr从大到小排序,降低分组时便利查找的复杂度

  3. 遍历原数组arr,从目标的n个分组的第一组开始装数据,直到它接近均值停止。这里注意接近的意思不是<=avg,而是在均值临界点,加上一个值num1 < avg后,和均值的差值是delta,往前遍历找(意味着num2 > num1)第一个没被分组的数据num2放入当前组后,num2 - avg < delta,如果是的则装num2,否则装num1。确保装的是最接近均值的数。

  4. 对于最后一个分组n-1要装数据时,需要确保arr的每一个数据都被分配完,并且各组结果最小,所以最后一组的策略不参考平均值,而是按和最小的分组去塞arr里的每一个数据。



另外注意,对于已经分好组的数据打个标,以免被重复分组。


这里我们是在拆解子问题



  • 把复杂的分组后每组方差最小的问题,转化为让每组和最接近平均值的问题,将整体的问题拆解成了n个组的问题

  • n个组塞值时,又是一个找数据使它最接近均值的子问题


其中为了降低复杂度不搞遍历的最优,确实只做到了近似最优解。譬如放值前先做了排序,只要当前数据放进去 < avg都先无脑放,就会出现,譬如剩下的数据有[48, 25, 25], 均值是50,本来我们可以放[25,25]得到最接近均值的数据,但现在只放入了48。


🤪 图片场景的特殊考虑因子


当我把一个纯数学解放入瀑布流场景时,发现事情并没有这么简单,算法的最优还是要为图片展示场景效果的最优做一些让步


参差感


譬如你看这个是最优解么?
image.png
因为我们先做了排序,并且按排序的数据顺序做分配,所以长图它它它它都跑到同一列去了。image.png
这个视觉上没有了参差美可受不了。


于是在接近最优的做法上妥协一步。先把排序前n的数据挨个放到n组,让个高的先均匀分布。


结合保留用户本来的顺序,是不是舒服一些:
image.png


这里依旧不是最佳效果,因为只取了前n个,试想我们如果是3组,5个长图,还是有一组全是长图。但长与短的边界实在无法敲定,除非再搞个每张图片高度的均值,大于均值一定阈值的数据先均匀分布到n组,但这种操作的数据越多,越影响到底部整体的平齐效果。所以还是只选了和组数相同的前n张这么处理。我估摸着大多数用户在文档里的图片是个辅助,不会搞出特别大数量级还夹杂很多长短分明的图。当前能保持一定数量级 (<10)展示上不会有太大问题。


排序


尽量得保证用户原图的顺序,所以需要记录原图的顺序,然后在分组完成后:




  1. 每列里按原图顺序重排下顺序

  2. 列与列之间按第一个图的顺序重排下顺序



能做到尽量接近原顺序但不绝对。


纯数字上[[25], [25], [25,25]][[25,25], [25], [25]]的分组没有差别。但是图片场景又不一样了:
image.png
这排列总透着一股奇怪image.png
于是再让步牺牲一下复杂度:



装最后一组数据分配余数之前,先把分配好的分组,先排序,组与组的和相等时优先放入排前面的数组。



当前版本优缺点


目前至少是在最平齐和图片参差感之间谋求的一个较优解,但绝不是最优解,理论上此类问题不穷举遍历获得不了最优解。但我们可以通过优化局部策略,使它更靠近最优解。不过一定是优于贪心算法把每张图放入高度最小列的做法。这里如果有深入研究过瀑布流的小伙伴有更优的方案,欢迎提供,让语雀的瀑布流更整齐~


做事情咱也不能只说好的,对问题缄口不言,目前的问题有:



  • 前面也说过,如果大量图片,并且存在 分组张数n 的与其他图片长度拉开巨大差距的图片,排版还是不够有参差感

  • 先按大小排序,后分组,会对原图顺序造成偏差,很难复原严格的行列顺序,但用户还是能一定程度的干预排序,只是无法满足一定要求图A和图B不放入同一列这种诉求。从这个角度说,顺序上不如贪心算法方案更接近原顺序,贪心方案的最后一张长图问题其实可以通过主动拖拽顺序把长图放到前面来解决掉,但是这对用户的理解力要求太高了。


anyway,以下的数据哪个算法也无法救🥲。目前列数是根据展示区宽度弹性计算的,这种想优雅可能要触发列数的改变规则了。
image.png


chatGPT的插曲


点我展开查看哭笑不得的过程### 第1轮
image.png
一开始它给了我个贪心算法的不是最优解,得让它进阶


第2轮


image.pngimage.png
看上去很高深,但这测试数据结果不对啊。
我换个说法?是不是不能理解什么叫数字加和尽量接近


第3轮


image.png
结果不对,继续让他换个解法


第4轮


image.png
还是肉眼可见的不对,虽然我肉眼分的也不是最优解,最后我的算法告诉我是可以分成三组和都是80的:[80], [32, 32, 12, 3, 1], [30, 21, 20, 9]


那么问题在哪呢,我尝试问了它一个很简单的问题:
image.png
原来加和都求不对,我放弃它了。。。


:::warning
综上:chatGPT能高效省事让你偷懒,但前提是你得能区分出它的答案靠不靠谱,如果你都不知道真相的问题仍给他,就会被忽悠了也不知道。另外别想用它帮你写笔试题了,它只根据语义生成,但并不真的运行代码,给的代码和结果可能完全不匹配。
:::


📔 基础知识


资料:




复杂度被分为两种级别:一种是O(1),O(log(n)),O(n^a)等,我们把它叫做多项式级的复杂度,因为它的规模n出现在底数的位置;另一种是O(a^n)和O(n!)型复杂度,它是非多项式级的,其复杂度计算机往往不能承受




P问题: 如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P问题
NP问题: NP问题不是非P类问题。NP问题是指可以在多项式的时间里验证一个解的问题。NP问题的另一个定义是,可以在多项式的时间里猜出一个解的问题
NPC问题:同时满足下面两个条件的问题就是NPC问题。首先,它得是一个NP问题;然后,所有的NP问题都可以约化到它。NPC问题目前没有多项式的有效算法,只能用指数级甚至阶乘级复杂度的搜索。
**NP-Hard问题:**它满足NPC问题定义的第二条但不一定要满足第一条。NP-Hard问题同样难以找到多项式的算法,但它不列入我们的研究范围,因为它不一定是NP问题。即使NPC问题发现了多项式级的算法,NP-Hard问题有可能仍然无法得到多项式级的算法




约化:(Reducibility,有的资料上叫“归约”)。简单地说,一个问题A可以约化为问题B的含义即是,可以用问题B的解法解决问题A,或者说,问题A可以“变成”问题B。通过对某些问题的不断约化,我们能够不断寻找复杂度更高,但应用范围更广的算法来代替复杂度虽然低,但只能用于很小的一类问题的算法。



next:拼图


接下来我们还会上线更灵活的拼图能力。**拼图算法可以实现任何尺寸的图片,保持原比例不裁剪,用户任意摆放位置,最终绘制成整齐的矩形,**这个算法实现也远比瀑布流复杂。


譬如你可以这样:
image.png
也可以拖成这样:
image.png


还可以拖成这样:
image.png


甚至拖成这样:
image.png


等等等等...... 随意组合排序,最终都能整齐。


等上线后我再写写拼图的故事~


作者:支付宝体验科技
来源:juejin.cn/post/7198370695079903291
收起阅读 »