注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Taro开发小程序记录-海报生成

Taro开发小程序记录-海报生成在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。 自定义海报 说到自定义海报...
继续阅读 »

Taro开发小程序记录-海报生成


在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。



自定义海报


说到自定义海报可以说是很多小程序中都会进行开发的内容,比如需要进行二维码的保存,然后再对二维码进行一点文字的修饰,涉及到这方面的时候我们就需要使用canvas了。


在实际开发的过程中,遇到了一些很坑的问题,当我们需要使用离屏canvas来进行绘制时,我们可能就会遇到问题(我自己就遇到了)。


对于安卓端,我们可以正常的使用OffscreenCanvas来创建离屏canvas,然后绘制相关内容,最后在使用Taro.canvasToTempFilePath方法保存到临时文件下,Taro.canvasToTempFilePath方法会返回文件路径,我们就可以通过获取到的文件路径来进行下载。


下面是安卓端的一个🌰,大家有需要也可以直接拿去使用


  • 需要使用到的方法
 /**
  * @description 获取二维码图像
  */
 export const qrCodeImage = async (qrCodeValue: string, size: number = 128) => {
     /* NOTE: 通过创建离屏canvas承载code */
     const context = createOffscreenCanvas('2d', size, size);
     QRCode.toCanvas(context, qrCodeValue, { width: size, height: size, margin: 1 });
     return (context as unknown as HTMLCanvasElement).toDataURL();
 };
 /**
  * @description 创建离屏canvas对象,width与height单位为px
  */
 export const createOffscreenCanvas = (type: '2d' | 'webgl', width: number = 100, height: number = 100) => {
     return Taro.createOffscreenCanvas({ type, width, height });
 };
 /**
  * @description 将传入的图片url转换成一个ImageElement对象
  */
 export const loadImageByUrlToCanvasImageData = async (url: string, width: number = 100, height: number = 100) => {
     const context = createOffscreenCanvas('2d', width, height);
     const imageElement = context.createImage();
     await new Promise(resolve => {
         imageElement.onload = resolve;
         imageElement.src = url;
    });
     return imageElement;
 };
 /**
  * @description 将canvas转成图片文件并保存在临时路径下
  */
 export const changeCanvasToImageFileAndSaveToTempFilePath = async (options: Taro.canvasToTempFilePath.Option) => {
     const successCallback = await Taro.canvasToTempFilePath(options);
     return successCallback.tempFilePath;
 };
 interface SettingOptions {
     title: string;
     titleInfo: {
         dx: number;
         dy: number;
         color?: string;
         font?: string;
    };
     imageUrl: string;
     imagePos: {
         dx: number;
         dy: number;
    };
     width: number;
     height: number;
 }
 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
    } = option;
     const context = await createOffscreenCanvas('2d', width, height);
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };
 /**
  * @description 保存图片
  */
 export const saveImage = async (urls: string[], isLocal: boolean = true) => {
     let filePath = urls;
     if (!isLocal) {
         filePath = await netImageToLocal(urls);
    }
     await Promise.all(filePath.map(path => {
         return Taro.saveImageToPhotosAlbum({ filePath: path });
    }));
 
     return true;
 };
 /**
  * @description 加载在线图片,并返回临时图片文件地址
  */
 export const netImageToLocal = async (urls: string[]) => {
     const res = await Promise.all(urls.map((url:string) => {
         return Taro.downloadFile({ url });
    }));
 
     const result = res.map(data => {
         if (data.statusCode === 200) {
             return data.tempFilePath;
        }
         throw new Error(data.errMsg);
    });
 
     return result;
 };
 /**
  * @description 判断用户是否授权保存图片
  */
 export const checkHasAuthorizedSaveImagePermissions = async () => {
     const setting = await Taro.getSetting();
     const { authSetting } = setting;
     return authSetting['scope.writePhotosAlbum'];
 };
 /**
  * @description 下载图片,需要区分是本地图片还是在线图片
  */
 export const downloadImage = async (urls: string[], isLocal: boolean = true) => {
     const hasSaveImagePermissions = await checkHasAuthorizedSaveImagePermissions();
     if (hasSaveImagePermissions === undefined) {
         // NOTE: 用户未授权情况下,进行用户授权,允许保存图片
         await Taro.authorize({ scope: 'scope.writePhotosAlbum' });
         return await saveImage(urls, isLocal);
    } else if (typeof hasSaveImagePermissions === 'boolean' && !hasSaveImagePermissions) {
         return new Promise((resolve, reject) => {
             Taro.showModal({
                 title: '是否授权保存到相册',
                 content: '需要获取您的保存图片权限,请确认授权,否则图片将无法保存到相册',
                 success: (result) => {
                     if (result.confirm) {
                         Taro.openSetting({
                             success: async (data) => {
                                 if (data.authSetting['scope.writePhotosAlbum']) {
                                     showLoadingModal('正在保存...');
                                     resolve(await saveImage(urls, isLocal));
                                }
                            },
                        });
                    } else {
                         reject(new Error('未授予保存权限'));
                    }
                },
            });
        });
    }
     await saveImage(urls, isLocal);
     return true;
 };

  • 生成海报(二维码+标题头)

 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
    } = option;
     const context = await createOffscreenCanvas('2d', width, height);
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };

  • 具体使用

 export const saveQrCodeImageWithTitle = async () => {
     const url = await qrCodeImage(enterAiyongShopUrl(), 160);
     const imgUrl: string = await generateQrCodeWithTitle({
         title: 'adsionli菜鸡前端',
         titleInfo: {
             dx: 95,
             dy: 20,
             font: '600 14px PingFang SC',
             color: 'black',
        },
         imageUrl: url,
         imagePos: {
             dx: 15,
             dy: 34,
        },
         width: 190,
         height: 204,
    });
     await downloadImage([imgUrl]);
 }


上面三块内容就可以组成我们的海报生成了,这里面的主要步骤不是很难,包括了几个方面:


  1. 用户授权鉴定,主要是是否允许保存,这里做了一点处理,就是可以在用户第一次授权不允许时,进行二次授权调起,这个可以看一下上面的downloadImage这个函数,以及用于判断用户是否授权的checkHasAuthorizedSaveImagePermissions这个函数
  2. 创建OffscreenCanvas并进行绘制,这里其实没有太多的难点,主要就是需要知道,如果我们使用image的内容的话,或者是一个图片的url时,我们需要先将其绘制到一个canvas上(这里可以获取imageElement对象,也可以直接使用canvas),这样方便我们后面进行drawImage时进行使用
  3. 图片保存,这里也有一个需要注意的点,如果图片(或二维码)是网络图片的话,我们需要处理以下,先将其转成本地图片,也就是通过netImageToLocal这个方法,然后再还给对应的将图片画在canvas上的方法。最后的保存很简单,我们可以直接使用Taro.canvasToTempFilePath这个方法转到临时地址,再通过downloadImage就可以搞定了。

感觉好像很麻烦,其实就四步:图片加载转化—>canvas绘制—>用户鉴权—>图片保存。


安卓端实现起来还是很简单的,但是这些方法对于ios端就出现了问题,如果按照上面的路线进行海报绘制保存的话,在ios端就会报一个错误(在本地开发的时候并不会抛出): canvasToTempFilePath:fail invalid viewId


这一步错误就是发生在Taro.canvasToTempFilePath这里,保存到临时文件时会触发,然后这一切的原因就是使用了OffscreenCanvas离屏canvas造成的。


所以为了能够兼容ios端的这个问题,有了以下的修改:


首先需要在我们要下载海报的pages中,添加一个Canvas,帮助我们可以获取CanvasElement

 <Canvas
     type='2d'
     id='qrCodeOut'
     className='aiyong-shop__qrCode'
 />

这里需要注意一下,我们需要添加一个type='2d'的属性,这是为了能够使用官方提供的获取Canvas2dContext的属性,这样就可以不使用createCanvasContext这个方法来获取了(毕竟已经被官方停止维护了)。


然后我们就可以获取一下CanvasElement对象了

 /**
  * @description 获取canvas标签对象
  */
 export const getCanvasElement = (canvasId: string): Promise<Taro.NodesRef> => {
     return new Promise(resolve => {
         const canvasSelect: Taro.NodesRef = selectQuery().select(`#${canvasId}`);
         canvasSelect.node().exec((res: Taro.NodesRef) => {
             resolve(res);
        });
    });
 };

注:这里又有一个小坑,我们在获取CanvasElement之后,如果直接进行绘制的话,这里存在一个问题,就是这个CanvasElementwidth:300、height:150被限制死了,所以我们需要自己在拿到CanvasElement之后,在设置一下width、height

 const canvasNodeRef = await getCanvasElement(canvas);
 let context;
 if (canvasNodeRef && canvasNodeRef[0].node !== null) {
     context = canvasNodeRef[0].node;
    (context as Taro.Canvas).width = width;
    (context as Taro.Canvas).height = height;
 }

好了,改造完成,这样就可以兼容ios端的内容了,实际我们只需要修改generateQrCodeWithTitle这个方法和page新增Canvas用于获取CanvasElement就可以了,其他可以不要动。修改后的generateQrCodeWithTitle方法如下:

 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
         qrCodeSize,
         canvas,
    } = option;
     const canvasNodeRef = await getCanvasElement(canvas);
     let context;
     if (canvasNodeRef && canvasNodeRef[0].node !== null) {
         context = canvasNodeRef[0].node;
        (context as Taro.Canvas).width = width;
        (context as Taro.Canvas).height = height;
    }
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: Taro.Image = await loadImageByUrlToCanvasImageData(imageUrl, qrCodeSize.width, qrCodeSize.height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage((imgElement as HTMLImageElement), imagePos.dx, imagePos.dy, imgElement.width, qrCodeSize.height);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };


如果大家不想让海报被人看到,那可以设置一下css

 .qrCode {
     position: fixed;
     left: 100%;
 }

这样就可以啦



突然发现内容可能有点多了,所以打算分成两篇进行Taro使用过程中的总结,开发完之后进行总结,总是可以让自己回顾在开发过程中遇到的问题的进一步进行思考,这是一个很好的进步过程,加油加油!!!


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

iOS项目运行时XCode内存暴涨、速度慢、卡的解决过程

iOS
XCode老罢工 从今年开始,项目中一个组件的主工程在开发过程中,运行编译时间耗时长,XCode是不是都会转菊花,平均每次编译的时间大概在5min左右,非常影响开发效率,今日刚好提测完,抽空仔细看看为何如此卡顿。环境 在卡顿的时候打开活动监视器,发现XCode...
继续阅读 »

XCode老罢工


从今年开始,项目中一个组件的主工程在开发过程中,运行编译时间耗时长,XCode是不是都会转菊花,平均每次编译的时间大概在5min左右,非常影响开发效率,今日刚好提测完,抽空仔细看看为何如此卡顿。

环境



在卡顿的时候打开活动监视器,发现XCode占用内存非常高,平均在20GB左右,峰值达到60GB




在Command + k 删除DerivedData 里面的缓存之后,还是没有明显的加速结果。


寻找原因


查看编译日志




发现组件内的所有文件在编译的时候都会有几个相似的警告。


这些警告来自同一个文件,通过pch文件引用。


有警告的文件是该组件的网络请求文件,是很早以前建立的,文件里面没有自动生成NS_ASSUME_NONNULL_BEGIN文件内大概有几百个警告。在编译文件的时候,这些警告都会去做缓存、分析。导致运行起来非常卡顿。


解决


消除警告,重新编译,发现项目跑起来非常的舒畅!


如果是有其他第三方库或者组件的警告,可以在podFile中增加 :inhibit_warnings => true 来避免编译的时候检查警告。这种方式也会加快编译速度。

pod 'XXNetEngineModule', :inhibit_warnings => true

可以看到解决完XCode的内存大小基本就在1GB左右。编译速度也基本上能达到秒启(10s内)。




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

chrome 搞事,下个月全面删除 Event.path

背景 前两天下午,测试同学反馈生产环境有个功能出现异常,然后给我发了张报错截图。  随后在测试同学的电脑复现并排查问题,通过异常断点,捕捉到异常信息如下:  可以看到异常的原因是 n.path 的值为 undefined,因此 n.path...
继续阅读 »

背景


前两天下午,测试同学反馈生产环境有个功能出现异常,然后给我发了张报错截图。 



随后在测试同学的电脑复现并排查问题,通过异常断点,捕捉到异常信息如下: 



可以看到异常的原因是 n.path 的值为 undefined,因此 n.path.find 等价于 undefined.find,因此程序报错。其中 n 是一个 Event 实例;path 是事件冒泡经过的节点组成的数组。n.path 有值的情况如下: 


Event.path 不是标准属性,常见于 chrome 浏览器,取不到值出现异常不足为奇,只要做个兼容就搞定了,当 Event.path 取不到值时就取 Event.composedPath()。那这是兼容性问题吗,事情好像没有这么简单。仔细对比上述两张截图可以发现,异常时 Event 实例甚至不存在 path 属性,这跟属性存在但值为空是两码事。


进一步排查


好好的 path 属性怎么就不翼而飞了,这是个神奇的问题。当我用自己电脑尝试复现问题时,发现功能正常且无法复现,事情变得更加神奇。两边浏览器都升到了最新版 chrome 108,区别是系统不同,一个是 windows 一个是 macOS。也就是 说同样的代码在同样的软件上跑出了不同结果,这说明可能不是代码或兼容性问题。


为找出真正的原因,我做了几组对照实验,以排除代码、硬件、操作系统和浏览器的影响。情况如下 :


分析这些结果,出现了更有意思的事:只有一种情况会出现异常,使用测试同学的电脑且浏览器是 chrome 108;当改变电脑、系统、浏览器、浏览器版本等因素时结果都是正常。 也就是说导致异常的因素居然不是单一的,而是多个因素组合(测试同学电脑+chrome+108 版本)产生的结果。



chromium issue 的助攻


从上面的结果看好像没办法再继续排查下去,不过从经验判断,多半是 chrome 又在搞事,这时候可以去 chromium issue 里找找蛛丝马迹,经过一番搜索找到了这条 issue: Issue 1277431: Remove event.path。 



issue 标题很直白,Event.path 将被删除。 从 issue 内容可以看到,这次搞事是从 2021 年 12 月 7 日开始,起因是 chromium 开发团队认为 Event.path 属于非标准 API,会导致 Firefox 等其他浏览器的兼容性问题,于是他们决定将其删除。目前这个变更在 chrome 108 属于灰度阶段,在即将发布的 chrome 109 上会全面应用,webview 则是从 109 版本开始逐步禁用。


变更详情和计划


另外 issue 中提到这个变更会在 console 中进行告警。 



console 中确实有这个告警,不过藏在 console 面板的右上角,不太容易发现,而且需要调用 Event.path 后才会显示。点进去之后会跳转到 Issues 面板并显示详细信息。
 



从图中可以看到这个变更属于 Breaking Change,即破坏性变更。另外可以看到变更详情链接版本计划链接。打开变更详情链接可以看到详细的说明、目的、状态、开发阶段等信息。 



打开版本计划链接可以看到,chrome 108 已经在 2022-11-29 正式发布(Stable Release Tue, Nov 29, 2022),chrome 109 将在 2023-01-10 正式发布(Stable Release Tue, Jan 10, 2023)。 



验证


由于英文水平有限,为了避免个人理解存在歧义,使用 chrome 的前瞻版本进行测试,以验证 chrome 108 之后的版本是否真的会应用这个变更。


  • 测试使用的系统为 macOS,浏览器版本包括:chrome-stable(108.0.5359.124)、chrome-beta(109.0.5414.36)、chrome-dev(110.0.5464.2)、chrome-canary(110.0.5477.0)。



  • 测试代码如下
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<script>
function test() {
console.log("event.path is:", window.event.path);
}
</script>
<h1 onclick="test()">click me</h1>
</body>
</html>

  • 测试结果如下

chrome-stable(108.0.5359.124)在 macOS 下 Event.path 有值,结合上文的对照实验中 windows10 下一个有值一个为空。说明 chrome 108 中该变更属于灰度阶段。
image.png


chrome-beta(109.0.5414.36)、chrome-dev(110.0.5464.2)、chrome-canary(110.0.5477.0)在 macOS 下 Event.path 都为空,说明 chrome 109 之后全面删除了 Event.path 属性。
image.png


解决方案


先看影响范围,从项目维度来看,所有前端项目都可能受到影响;从代码维度来看,项目源码和第三方依赖都可能受影响。在 github 中搜索发现 swipperopenlayers 等第三方库中都有相关 issue。因此解决方案需要全面考虑:最好对所有项目都进行排查修复,另外不仅要排查源码,还要考虑第三方库。


根据官方建议及综合考虑,推荐在前端项目中统一添加如下 polyfill 代码:

  Object.defineProperty(Event.prototype, "path", {
get() {
return this.composedPath();
},
});

最后


chrome 109 预计在 2023-01-10 正式发布,届时会全面禁用 Event.path,所有源码中使用该属性或第三方库使用该属性的前端项目都可能会出现异常,还有 20 几天时间,建议尽快排查修复。


一些经验

  • 关注 devtools 中的 console、issue 等各种告警信息,有助于调试和排查问题、以及发现潜在的问题
    • 关注 chorme 迭代计划,有条件可以做前瞻性测试,预防未来可能发生的异常



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

    领导让我面试别人,我准备了那些面试题

    关于领导让我面试别人,我准备了那些面试题 前端领导一天对我说你准备下面试题,然后我会筛选些简历给到你,你到时候负责一面。然后大致跟领导聊了下需要那样得,有没有什么具体要求。 大致就是要求能干活,不管是表格、echarts等能立刻上手,别是刚培训班出来得就行...
    继续阅读 »

    关于领导让我面试别人,我准备了那些面试题



    前端领导一天对我说你准备下面试题,然后我会筛选些简历给到你,你到时候负责一面。然后大致跟领导聊了下需要那样得,有没有什么具体要求。



    大致就是要求能干活,不管是表格、echarts等能立刻上手,别是刚培训班出来得就行。


    可能也是上个同事来了一个礼拜也没写出来什么太多东西。然后领导最后跟我们说他每天我看到再微信聊天, 让写个表格都能墨迹半天写不出来。本来再一周得时候都能看出大致水平了,但是领导觉得都招进来了好好学,每天积极学习也是愿意培养得。最后领导跟我们说他既然不愿意学,每天就是按时下班等情况吧就让人走了总共算是待了两个礼拜,工资也正常发放了。


    我也是去看了好多面试得一些经验,希望能招到合适得同事,一荣俱荣,我也是想我招聘进来得同事能符合岗位要求,不然就是面试官得问题了。记得有一篇文章中说,你跟面试者身份是同等得,不过是你单位需要招聘。刚好我求职而已,不要趾高气扬得去问一些你不知道在哪里看到得新名词去为难人。


    以下就是我准备得一些面试题,我觉得能答得七七八八基本满足干活要求了,然后可能回答得好的我也会基于原问题扩展下。回答不上来也没关系,能回答上来更好了。


    Css



    1. 常用的伪类有哪些?



    可能有些人会一时没想起来,哪有问这么简单得哈哈。我也会稍微提示下:not:hover...我可能更希望面试者能说点其他得比如first-child、last-child、nth-child、before、after等等。




    1. css 说下弹性盒子;



    这个也是工作中必须会的,我可能希望你能说的多点,当然不常用的那些对子盒子的一些属性除外。例如:




    • flex-direction 是更改方向的可以。

    • flex-wrap 是可以设置是否允许换行。

    • justify-content x轴的对齐方式。

    • align-items yz轴的对齐方式。



    1. 说下localstoage/ sessionStroge区别;



    可以从大小啊、特性分别说下。




    1. 清除浮动的方法有哪些 分别说下



    可以简单说下比如overflow: hidden;等。




    1. 说下重排和重绘 什么场景下会发生重排和重绘



    希望面试者能了解dom是怎么样的刷新渲染机制,什么样下就发生重绘(color、background等)、什么样下会发生重排(更改宽高等)。




    1. 实现垂直居中的方法有哪些



    希望面试者能说出两种就很好了,这两种也是经常会用到的,像还有个display:table;...这个我确实也没咋用到。只要能说出:




    1. display: flex; justify-content: center; align-items: center;;

    2. 子元素position: relative;left: 50%;top: 50%; transform: translate(-50%, -50%);


    Js



    由于我们可能是重前端多一点所以对数据转换会要求多点。所以我准备了一些数据处理的面试题如下:



    1、请使用forEach、map、filter、some、every等相关api对1,2,3...,10这些数据进行处理:



    1. 请用相关api计算出1-10累加。(期望得到一个Number: [1,2,3...])

    2. 请用相关api过滤出1-10大于5的值。(期望得到一个Array: [1,2,3...])

    3. 请用相关api得到一个都是乘以*2的新数组(期望得到一个Array: [2,4,6...])

    4. 请用相关api查找出1-10是否有数值11(期望得到一个Boolean: true、false)。

    5. 请用相关api判断1-10中是否都大于5。(期望得到一个Boolean: true、false)

    6. 请用相关api判断1-10中是否存在大于9数值的(期望得到一个Boolean: true、false)。



    如果回答的很好,我还会多问问面试者假如不使用这些数组api函数的话,你能用for循环一样得到相关结果嘛。如果你能说的很好,我会再让面试者自己封装一个forEach、map、filter、some、every那可就太棒了。



    2、说下防抖、节流的场景以及怎么实现这些。



    能说出防抖和节流的用处和场景,假如你能说对应用场景,实现都是使用lodash等或者找的方法使用的也能接收。但是还是希望能写出来一个自己如何实现防抖和节流的。



    3、说下let/count区别



    可以说下两者区别:常量不允许修改等。




    1. let a = 1; let b = a; b = 2;(这个时候a、b分别是什么值?)(加分项:说下原因)

    2. let a = {name: 1}; let b = a; b.name = 3; (这个时候a、b分别是什么值?)(说下原因)


    其实是希望面试者对堆、栈有一定的了解。


    4、事件执行机制
    setTimeout(() => { console.log(2)}, 0); const fn = new Promise(res => { console.log(3); res('success')}); console.log(1); fn.then(res => console.log(4)) 以上代码会输出什么 执行顺序是啥样的(加分项: 说出理由)


    其实是希望面试者对宏任务、微任务有一定的了解,了解代码的执行机制。


    5、说下箭头函数和普通函数的区别:


    说完会继续问下bind\apply\call 用法以及能力 (加分项:怎么实现一个bind、apply、call)


    这里其实是希望面试者能对this能有一定的理解。


    6、说下对浅克隆/深克隆的理解 以及实现一个深克隆函数

    这个问题可能会根据问题3回答情况问答。


    7、说下怎么合并一个数组、对象。



    {...obj1, ...obj2}Object.assign。 数组可以使用concat,如果回答的好我会再问下如何使用for满足。



    8、说下字符串常用的属性api


    说完后会再追问下substr、substring的区别


    9、说下数组的splice 和 slice的区别


    10、const obj = {a: {b: {c: 123 } } }; 希望你写一个get函数如lodash.get(obj, 'a.b.c', '-')一样的函数,就是可以通过传入a.b.c获取到值得一个函数。


    11、const originArray = [{name: '小明', a: 1}, {name: '小明', b: 1}, {name: '小红', a: 1}]希望你对上面数据groupByname,得到{小明: [{name: '小明', a: 1}, {name: '小明', b: 1}], 小红:[{name: '小红', a: 1}]}


    const originArray = [{name: '小明', a: 1}, {name: '小明', b: 1}, {name: '小红', a: 1}]

    /** 调用groupBy(originArray, 'name')得到:
    * {
    * 小明: [{name: '小明', a: 1}, {name: '小明', b: 1}],
    * 小红:[{name: '小红', a: 1}]
    * }
    /


    如果JS回答的很好,基本已经能满足我们的招聘要求了。我后面也会对vue简单问问,放心哈哈 对源码没啥要求,我们要求是能干活就可以。



    VUE



    由于我们单位使用的是vue,我也会象征性的问一些用法相关的,如果你的js回答很好没有使用过vue,一直再使用react等框架,这个我就不会再问了,当然不影响我们的面试结果的,相信你js基础那么好,也就看下文档就能上手了。




    1. vue 组件间传值实现通信的方式有哪些?

    2. 有使用过vueBus嘛(加分项: 说下发布订阅者模式)

    3. 有用过watch嘛? watchdeepimmediate都是干嘛的 有啥作用 (加分项怎么实现一个watch)

    4. v-show 与 v-if 有什么区别?

    5. 说下vue的生命周期?

    6. 说下vuex的个人理解

    7. 说下nextTick使用场景



    其实这里更希望你能说出获取dom等基本就符合问题了。但是如果能答出来nextTick是怎么设计的那可太棒了




    1. 说下路由hash、history的区别



    这里面其实不只是想听到带不带#号,还希望你能说出两者会不会携带路由去请求html,然后你们是怎么处理history下404界面的。



    加分项



    一般达到这里已经满足我们得招聘要求了,这个时候我可能会多问一些完事把整体情况汇报给领导,由领导下决定选择谁入职




    1. 使用过canvas嘛(都用canvas再场景下实现了什么功能)

    2. 使用echarts都画过那些图表(解决过什么困难)

    3. 都用webpack做过什么?

    4. vue中假如请求api不以api/v1统一规范开头了 怎么实现接口转发?


    结语



    面试前还是要多准备准备,以上就是我面试新同事准备得一些面试题,其实好多同学都卡再了js,js基础不够扎实。 相比与学习框架api,语法,我更会花更多时间再js上面。 然后再去了解框架好的设计以及API得实现。



    作者:三原
    来源:juejin.cn/post/7262349502920540217
    收起阅读 »

    谈谈用行政手段解决行业问题

    很多企业常常用行政的手段,去解决行业的问题。 这个问题是一名叫“小赵”的读者反馈给我的。 这里的“行政”一词,是指一种通用的管理与协调方式,而“行业”指的是某个特定的专业领域。 小赵在一家软件企业的研发部做开发。请记住这是一个研发部,他是一个技术工种。但是,...
    继续阅读 »

    很多企业常常用行政的手段,去解决行业的问题。



    这个问题是一名叫“小赵”的读者反馈给我的。


    这里的“行政”一词,是指一种通用的管理与协调方式,而“行业”指的是某个特定的专业领域。


    小赵在一家软件企业的研发部做开发。请记住这是一个研发部,他是一个技术工种。但是,他的会议非常多,基本上一坐到工位就得再站起来去开会。而会议的内容多是关于开发流程的:原型评审会、技术分析会、工期评估会……


    后来,他们开发出的产品,全是bug,无法运行,常常强制交付,遭到客户频繁投诉。


    领导层很震怒,要求解决问题。


    于是,大家分析原因,各抒己见,最终汇总整理,原因如下:



    • 产品没有设计好就交给技术

    • 技术没有思考就开始写代码

    • 功能没做完就交给测试验证


    最终,领导层觉得需要加强监管。于是,他们决定在原型评审之前,加个宣贯会作为预热。后来宣贯会没有达到效果,又加上了个预宣贯会作为宣贯会的预热……


    结果,这个团队的产出质量依然没有改善。随后,领导层很震怒,要求分析原因,解决问题。


    于是,大家不吐不快,纷纷吐槽会议太多,光开会了,没时间干活。领导层一听,决定开会研究一下,如何减少会议、提高效率。


    通过对基层进行访谈,然后以会议的形式汇报给中层,中层汇报给高层。最终形成一份总结,再以全员会议的形式同全体员工宣讲。


    结果,依然没有改善。于是,领导层再次要求解决问题。于是大家便开会讨论,会议的主题是:为什么那套减少开会的方案没有起到少开会的作用?


    后来,还是没有改善。通过一次次争吵,各抒己见,相互吐槽,最终领导发现,原来各部门之间相互不满意,测试觉得技术不行,技术觉得产品不行,产品觉得运营不行。领导点点头,似乎已经成功了,当即决定,引入相互评价机制。将各部门人员之间的互评作为重要考核,与工资挂钩。出于利益考虑,各方势必会让各方都满意。这样一来,问题就解决了。


    实际上,后续更精彩,研发的重点就转移到人情世故了……先到这吧,只是想让大家感受下“行政”模式。


    上面说的情况,在我看来,绝不是一个“不断完善”的过程。这种完善没有尽头。大家都在想办法解决问题,没有人思考问题产生的原因。这无异于办公室漏水了,大家合力去清扫,没有人关心漏点在哪里。


    小赵反馈,他们公司更重视每个会议是否被召开,大家似乎并不关心工作内容本身。


    比如,测试用例评审。虽然测试要拿着用例一条条去验证开发的成果。但是直到会上,开发人员从来都没有提前看过,整场会议从头到尾,大家一个问题也没有。有流程吗?有!流程是提前1天发出来让大家准备问题。


    再比如,开发返讲时,一半以上的人都在看手机。快结束时,就连主讲者都长出一口气:“哎呀,终于讲完了!”。他并不关心其他人听没听懂,重要的是自己讲完了。别人就是不听,我又能怎么样?


    在我看来,小赵公司的开发流程,从第一个版开始就是规范和专业的。已经包含了软件开发的各个关键节点。但是,他们每一步都没有落实到位


    就拿测试用例评审会来说。开会之前,有没有人确认过里面的内容大家都看过了?看了的话,问几个问题;没看,等你看完后再开。但是一旦给你时间看了,你依然无视它,一问三不知,不好意思,得给你记上一笔。
    开会过程中,人家讲的条例,有没有和你理解不一致的地方。如有,请提出来。如果后面你做完了,对于有明确描述的操作,你再说当时我不知道,那么不好意思,给你记上一笔……


    在具有大家共同认可标准的前提下,每一项会议都要落地砸坑,步步有效。有了落实,甚至不开会都行。


    这并非是不人性化,这不就是你正常的工作吗?最符合职场人的人性化是摸鱼。领导多辛苦一点,担负起把控和监管的责任,员工就无法摸鱼。从领导开始就摸鱼,员工就直接下网了。


    除了落实到位,还有一个专业性的问题。对于落实流程,“行政”手段再勤劳一点也是可以做到的。但是,到专业性这儿,就得由专业的人来干了。


    每一项流程怎样的标准才算是做到位?什么样的员工才是好员工?通过民主的形式,没有答案。这段代码写得好不好、那种技术方案能不能支撑起五万人同时访问,大家来投个票吧!保洁说,我觉得行!产品说,我觉得不行!采购幸好问了下,发现居然还需要买服务器,当即反对。他们各有各的意图。给我添麻烦了,我就不同意。跟我没关系,我就送个人情,举个手。专业的事情,就该专业的人一言堂,给我权力的同时,也让我承担责任。


    实际上,并非所有老板都愿意孤注一掷,完全相信某个员工的建议。他们常常通过自己的方式去做流程干预。尤其遇到需要决断的问题时,因为不够专业,往往采取通用的行政手段去解决。比如引入相互监视来保证不偷懒,再设立秘密小组来监督大家确实在相互监视。如此下去,无限不循环……


    以前我感觉老板们这种做法不明智,我也想不通他们为什么这么想。直到那天我看到一段历史,忽然明白这是人类的通病。


    话说拿破仑时期,两方打仗,都是各自码好了人,面对面。然后相互往对方扔炮弹。这种硬杠的方式,似乎有点弱智。难道就没有攻城掠地的形式吗?其实之前是有的,而且国王很重视城池的建设,认为那是安全的保障。但是建设城池时,负责建设的工匠总是糊弄国王,而国王又看不懂,最后导致腐败严重,还没打仗国家就虚了。最后,国王们就干脆不用城池做保护了,用省下做城池的钱,直接养兵更实惠。


    如果国王是做工程出身,那肯定不会被骗。但是又有多少国王干过瓦匠呢?因此,不论哪个组织的老大,采用自己熟悉的行政手段去干预流程,起码是保底的。


    当然,遇到专业的管理更好。两者很好区分。专业者一般是帮助你,行政者一般是质疑你。帮助你的人,他首先会了解你在做一件什么事,然后给出指导。而质疑你的人,则会想方设法拿出证据来,比如某件事物存在,先去证明是你不行,然后借机让你继续想办法。


    其实,拿破仑之后,城池的建设又兴起了。因为后来各种建设规范,甚至财务审计等制度也健全起来,即便是很狡猾的工匠,也难以再搞出腐败。反而导致工匠们认为建设城池是一份荣耀,自己是为了国家的荣誉而战。


    作者:TF男孩
    来源:juejin.cn/post/7270427148192071735
    收起阅读 »

    30岁程序媛求职路复盘:文转码+失业半年+PHP如何涨薪5K的!?

    这篇文章来自一位群友的分享: 这篇文章写于下班路上,刚刚入职不久,我想再冲刺一下大厂,阳哥建议我坚持总结打卡,可以尝试写写博客。 那我就从这篇开始吧,希望开个好头! 上班的感觉真好 今天是入职的第二周,还在熟悉业务和代码,晚上下班和周末还在补业务知识和技术栈...
    继续阅读 »

    这篇文章来自一位群友的分享:


    这篇文章写于下班路上,刚刚入职不久,我想再冲刺一下大厂,阳哥建议我坚持总结打卡,可以尝试写写博客。


    那我就从这篇开始吧,希望开个好头!



    上班的感觉真好


    今天是入职的第二周,还在熟悉业务和代码,晚上下班和周末还在补业务知识和技术栈。


    我就趁着在地铁上的时间来复盘一下2023年的上半年的求职之路好了。


    遥想一月


    一月十八号公司宣布深圳研发部解散,给了n+1赔偿,我当时很开心,因为我本来也打算换工作。


    现在看来真是盲目乐观了!真是没想到,紧接着迎来了长达七个月的失业时光😭。


    自我怀疑


    那段时间太痛苦了:简历投出去,已读不回,面试也没有。


    这期间我恶补了好多八股文,甚至还学了些计算机网络,操作系统的知识。但是感觉学的很不系统,东一榔头,西一杠子,有点熊瞎子掰玉米的感觉。


    然后偶然机会去朋友公司,写了一个月Go,种种原因,一个月后我又离职了,这时候差不多五月份了,我想干脆转Go吧,两个都投简历,机会应该能多一点,就这样我今天学点PHP看看源码,明天学下Go的基础。反复摇摆,反复横跳,啥都没学好,面试也约不到,每天就是蒙头学,心里一直很难过...


    上有老下有小的,只有我老公一个人上班,我在家跟我爸妈我娃一起,心里真的万分沮丧,觉得自己像个垃圾,对自己非常失望,然后又会自己给自己打气,说加油吧,努力学,起码能在机会来了时候努力抓住!就一直在这样自我鼓励和自我厌弃中来回反复。


    迎来转机


    时间到了六月底,迎来了转机,我在微信群里看到阳哥在帮群友做简历优化和就业辅导。我就和阳哥联系了一下,说明了我的情况。


    阳哥建议我不要再摇摆不定了,踏踏实实去学Go肯定比继续做PHP有前途,也帮我规划了从学Go到求职找工作的学习路线。


    然后我就度过了特别充实且痛苦的一个半月,按照制定好的学习路线去学,学完一个阶段就和阳哥约模拟面试,期间也碰到了不少自己没搞懂的问题,有的问ChatGPT解决了,有的问阳哥解决了。


    和之前相比,目标明确了,就不会像之前那么焦虑。



    就这样我把Go基础、并发编程、数据库、缓存、消息队列、计网、项目、简历的问题等等都和阳哥过了一遍,靠谱的很!我心里终于有底了,也有重新约面试的底气了!


    阳哥靠谱


    这里我夸阳哥几句:我第一个面试的前一天,很紧张,跟阳哥吐槽了几句。当时正好赶上周日休息,阳哥马上就说下午或者晚上回到家就给我做个模拟面试,针对这个公司的岗位需求来做个突击辅导,当时我很意外也很感动。我想着是周日,再说也是第一家面试只是刷经验包,也没有跟阳哥提先说给我面面,但是他还主动找我来做辅导😂,反正当时心里挺感谢的,然后阳哥就腾讯会议和我聊了一个多小时:从自我介绍到专业技能、再到项目经验怎么挖掘亮点等等,结合这家公司的要求又过了一遍,这样聊下来之后,我心里就有谱多了,也不慌了。


    面试复盘


    我一共面了三家就找到工作了:真的是没想到在我踏踏实实学Go一个半月之后,才面试了三家就顺利入职了。


    后来反思了一下,这肯定和我运气好有关系,但是更重要的,和我加入训练营之后这一个多月的突击学习关系更大!这一个多月遭的罪是值得的!


    第一家


    第一家技术点问的很少,问的都是管理经验啥的,问给你个团队怎么管理,但是就很搞笑,薪资范围15K-20K招带团队的😂,你是在逗我吗!?


    反正没什么有用信息感觉,就不细聊啦~


    第二家


    第二家就是我现在入职的公司,针对简历项目问了很多,这个我还是很有底的,毕竟我的简历优化迭代了好几遍,阳哥也针对我的简历做了多次模拟面试了。 另外这家公司的一面面试官也特别好,能感觉到面试官很有水平,能挖掘我的闪光点,一面的整体过程都比较舒服。


    二面就一言难尽,二面的面试官完全换了一个风格,我感觉自己一直在被打压,我的情绪就有点崩,有点上头。还好加入训练营之后和阳哥做了好几次模拟面试,硬着头皮把能回答的都回答了。另外阳哥和我说,有不懂的就和面试官主动问他们的解决方案是什么,我也问了下,也从面试中学到了东西,虽然这场面试情绪有点崩,但是该说的还是说到了!


    二面整体不如一面试理想,二面到最后一面的面试官进来缓和了一下气氛,还算画了一个不错的句号吧。


    然后就让我回去等通知了...


    万万没想到,第二天就说通过了(说实话我是有些意料之外的,就像我和阳哥说的,我是不是在做梦,竟然有种范进中举的感觉,哈哈)


    因为我感觉不够真实,再加上第二家只是口头承诺,没有发任何实质性的东西。所以我又和阳哥约了一场模拟面试,再为后面的面试继续做准备(万一这家公司放我鸽子呢...)


    第三家


    第三家,有不少问题模拟面试的时候阳哥都问过我,但是我记得不牢,当时记住了,没多久又忘了,导致我回答时候都比较模棱两可,不够深入。因为我只记得框架和整体思路,阳哥和我讲的太细节、偏底层的东西我就有点记不清了。


    第三家的面试官就说我技术掌握的不够深,我心里想:你再给我时间准备一个星期试试,我可以深到你难以想象!😂 反正当时出了门还是有些挫败感,冷静一会之后安慰自己加油干:暴露的问题越多越能去补救,下一个面试就可以回答很好了。


    怎么说呢,第三家对我还是很有帮助的:让我非常珍惜目前的工作机会!



    第二家的转机


    就这样还在自我安慰和鼓励的同时,准备和阳哥复盘一些不确定的问题,第二家就不只是口头承诺了,而是正式发Offer走流程了!开心!!! 然后我就开始有针对性的准备新公司需要的技术栈了。


    薪资涨幅也很满意,这只是我做Go的一个起点,但绝不是终点!等稳定下来我打算继续向阳哥请教,我要冲刺大厂!!!


    小建议:



    1. 失业期间负能量多的群建议屏蔽不看,很扰乱心情,我当时看了满屏的失业找不到工作,很焦虑。我又去找阳哥,我说怎么办,我觉得我永远也找不到工作了,阳哥说不会,那是他们自己的问题,能找到工作的人不会在群里抱怨环境不好,有问题针对性解决就好了,你按照计划去做事,不要受别人的影响。

    2. 另外还要发几句牢骚,我开始投简历前跟阳哥说,我决定拼了,深圳投完了投广州,杭州,北京,还不能有一个开发工作给我? 然后我就上班了😀,爱拼的女生运气不会太差!

    3. 真的不要摇摆,不要像我刚开始一样既想做PHP,又想做Go,这样大概率啥都做不好!瞄准一个方向,然后踏踏实实的去做事情!

    4. 做思维导图、坚持总结真的是个好习惯,再次感谢阳哥,这招真的太好用了,而且一旦坚持下来,养成习惯,真的很香,能明显提高学习效率,梳理清楚知识体系。

    5. 接下来的日子,还要像之前一样:坚持总结打卡,挑战写博客,把技术再深挖一下,冲刺大厂! 也算给自己立个Flag。



    好了,我到站了,还在找工作的伙计们加油啦!💪


    阳哥读后感


    首先很感谢对我的肯定,这位群友的分享让我很感动!也给了我继续帮大家做简历优化和就业的信心!


    最近帮不少朋友都拿到了满意的Offer,可以说是我最有成就感的事情了。


    作者:王中阳Go
    来源:juejin.cn/post/7275550543287697468
    收起阅读 »

    Android 沉浸式状态栏,透明状态栏 采用系统api,超简单近乎完美的实现

    前言 沉浸式的适配有多麻烦,相信大家既然来搜索这个,就说明都在为此苦恼,那么看看这篇文章吧,也许对你有所帮助(最下面有源码链接) 有写的不对的地方,欢迎指出 从adnroid 6.0开始,官方逐渐完善了这方面的api,直到android 11... ... 让...
    继续阅读 »

    前言


    沉浸式的适配有多麻烦,相信大家既然来搜索这个,就说明都在为此苦恼,那么看看这篇文章吧,也许对你有所帮助(最下面有源码链接)


    有写的不对的地方,欢迎指出


    从adnroid 6.0开始,官方逐渐完善了这方面的api,直到android 11...


    ... 让我们直接开始吧


    导入核心包


    老项目非androidx的请自行研究下,这里使用的是androidx,并且用的kotlin语言
    本次实现方式跟windowInsets息息相关,这可真是个好东西
    首先是需要导入核心包
    androidx.core:core

    kotlin可选择导入这个:
    androidx.core:core-ktx
    我用的版本是
    androidx.core:core-ktx:1.12.0

    开启 “沉浸式” 支持


    沉浸式原本的意思似乎是指全屏吧。。。算了,不管那么多,喊习惯了 沉浸式状态栏,就这么称呼吧。

    在activity 的oncreate里调用
    //将decorView的fitSystemWindows属性设置为false
    WindowCompat.setDecorFitsSystemWindows(window, false)
    //设置状态栏颜色为透明
    window.statusBarColor = Color.TRANSPARENT
    //是否需要改变状态栏上的 图标、字体 的颜色
    //获取InsetsController
    val insetsController = WindowCompat.getInsetsController(window, window.decorView)
    //mask:遮罩 默认是false
    //mask = true 状态栏字体颜色为黑色,一般在状态栏下面的背景色为浅色时使用
    //mask = false 状态栏字体颜色为白色,一般在状态栏下面的背景色为深色时使用
    var mask = true
    insetsController.isAppearanceLightStatusBars = mask
    //底部导航栏是否需要修改
    //android Q+ 去掉虚拟导航键 的灰色半透明遮罩
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    window.isNavigationBarContrastEnforced = false
    }
    //设置虚拟导航键的 背景色为透明
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    //8.0+ 虚拟导航键图标颜色可以修改,所以背景可以用透明
    window.navigationBarColor = Color.TRANSPARENT
    } else {
    //低版本因为导航键图标颜色无法修改,建议用黑色,不要透明
    window.navigationBarColor = Color.BLACK
    }
    //是否需要修改导航键的颜色,mask 同上面状态栏的一样
    insetsController.isAppearanceLightNavigationBars = mask

    修改 状态栏、虚拟导航键 的图标颜色,可以在任意需要的时候设置,防止图标和字体颜色和背景色一致导致看不清

    补充一下:
    状态栏和虚拟导航栏的背景色要注意以下问题:
    1.在低于6.0的手机上,状态栏上的图标、字体颜色是白色且不支持修改的,MIUI,Flyme这些除外,因为它们有自己的api能实现修改颜色
    2.在低于8.0的手机上,虚拟导航栏的图标、字体颜色是白色且不支持修改的,MIUI,Flyme这些除外,因为他们有自己的api能实现修改颜色
    解决方案:
    低于指定版本的系统上,对应的颜色就不要用透明,除非你的APP页面是深色背景,否则,建议采用半透明的灰色

    在带有刘海或者挖孔屏上,横屏时刘海或者挖孔的那条边会有黑边,解决方法是:
    给APP的主题v27加上
    <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
    参考图:

    image.png


    监听OnApplyWindowInsetsListener


    //准备一个boolean变量 作为是否在跑动画的标记
    var flagProgress = false

    //这里可以使用decorView或者是任意view
    val view = window.decorView

    //监听windowInsets变化
    ViewCompat.setOnApplyWindowInsetsListener(view) { view: View, insetsCompat: WindowInsetsCompat ->
    //如果要配合下面的setWindowInsetsAnimationCallback一起用的话,一定要记得,onProgress的时候,这里做个拦截,直接返回 insets
    if (flagProgress) return@setOnApplyWindowInsetsListener insetsCompat
    //在这里开始给需要的控件分发windowInsets

    //最后,选择不消费这个insets,也可以选择消费掉,不在往子控件分发
    insetsCompat
    }
    //带平滑过渡的windowInsets变化,ViewCompat中的这个,官方提供了 api 21-api 29的支持,本来这个只支持 api 30+的,相当不错!
    //启用setWindowInsetsAnimationCallback的同时,也必须要启用上面的setOnApplyWindowInsetsListener,否则在某些情况下,windowInsets改变了,但是因为不会触发setWindowInsetsAnimationCallback导致padding没有更新到UI上
    //DISPATCH_MODE_CONTINUE_ON_SUBTREE这个代表动画事件继续分发下去给子View
    ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
    override fun onProgress(insetsCompat: WindowInsetsCompat, runningAnimations: List<WindowInsetsAnimationCompat>): WindowInsetsCompat {
    //每一帧的windowInsets
    //可以在这里分发给需要的View。例如给一个聊天窗口包含editText的布局设置这个padding,可以实现键盘弹起时,在底部的editText跟着键盘一起滑上去,参考微信聊天界面,这个比微信还丝滑(android 11+最完美)。
    //最后,直接原样return,不消费
    return insetsCompat
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
    super.onEnd(animation)
    //动画结束,将标记置否
    flagProgress = false
    }

    override fun onPrepare(animation: WindowInsetsAnimationCompat) {
    super.onPrepare(animation)
    //动画准备开始,在这里可以记录一些UI状态信息,这里将标记设置为true
    flagProgress = true
    }
    })

    读取高度值


    通过上面的监听,我们能拿到WindowInsetsCompat对象,现在,我们从这里面取到我们需要的高度值


    先定义几个变量,我们需要拿的包含:
    1. 刘海,挖空区域所占据的宽度或者是高度
    2. 被系统栏遮挡的区域
    3. 被输入法遮挡的区域

    //cutoutPadding 刘海,挖孔区域的padding
    var cutoutPaddingLeft = 0
    var cutoutPaddingTop = 0
    var cutoutPaddingRight = 0
    var cutoutPaddingBottom = 0

    //获取刘海,挖孔的高度,因为这个不是所有手机都有,所以,需要判空
    insetsCompat.displayCutout?.let { displayCutout ->
    cutoutPaddingTop = displayCutout.safeInsetTop
    cutoutPaddingLeft = displayCutout.safeInsetLeft
    cutoutPaddingRight = displayCutout.safeInsetRight
    cutoutPaddingBottom = displayCutout.safeInsetBottom
    }


    //systemBarPadding 系统栏区域的padding
    var systemBarPaddingLeft = 0
    var systemBarPaddingTop = 0
    var systemBarPaddingRight = 0
    var systemBarPaddingBottom = 0

    //获取系统栏区域的padding
    //系统栏 + 输入法
    val systemBars = insetsCompat.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars())
    //左右两侧的padding通常直接赋值即可,如果横屏状态下,虚拟导航栏在侧边,那么systemBars.left或者systemBars.right的值就是它的宽度,竖屏情况下,一般都是0
    systemWindowInsetLeft = systemBars.left
    systemWindowInsetRight = systemBars.right
    //这里判断下输入法 和 虚拟导航栏是否存在,如果存在才设置paddingBottom
    if (insetsCompat.isVisible(WindowInsetsCompat.Type.ime()) || insetsCompat.isVisible(WindowInsetsCompat.Type.navigationBars())) {
    systemWindowInsetBottom = systemBars.bottom
    }
    //同样判断下状态栏
    if (insetsCompat.isVisible(WindowInsetsCompat.Type.statusBars())) {
    systemWindowInsetTop = systemBars.top
    }

    到这里,我们需要的信息已经全部获取到了,接下来就是根据需求,设置padding属性了

    补充一下:
    我发现在低于android 11的手机上,insets.isVisible(Type)返回始终为true
    并且,即使系统栏被隐藏,systemBars.top, systemBars.bottom也始终会有高度
    所以这里


    保留原本的Padding属性


    上述获取的值,直接去设置padding的话,会导致原本的padding属性失效,所以我们需要在首次设置监听,先保存一份原本的padding属性,在最后设置padding的时候,把这份原本的padding值加上即可,就不贴代码了。


    第一次写文章,写的粗糙了点

    可能我写的不太好,没看懂也没关系,直接去看完整代码吧


    我专门写了个小工具,可以去看看:
    沉浸式系统栏 小工具


    如果有更好的优化方案,欢迎在github上提出,我们一起互相学习!


    作者:Matchasxiaobin
    来源:juejin.cn/post/7275943802938130472
    收起阅读 »

    如何在上班时间利用终端控制台摸鱼🧐🧐🧐

    web
    作为一个资深的摸鱼小能手,班我们每天要上,终端也是我们也要每天要用到的,那么有什么办法可以在控制台终端中去摸鱼呢,那么在接下来的文章中我们就来看看它可以做到怎么样摸鱼。 简介 在我们开发的项目中,几乎有很多项目要都是使用 webpack 作为构建工具来进行开发...
    继续阅读 »

    作为一个资深的摸鱼小能手,班我们每天要上,终端也是我们也要每天要用到的,那么有什么办法可以在控制台终端中去摸鱼呢,那么在接下来的文章中我们就来看看它可以做到怎么样摸鱼。


    简介


    在我们开发的项目中,几乎有很多项目要都是使用 webpack 作为构建工具来进行开发的,在它进行构建的时候,会有一些信息会输出在控制台上面,如下图所示:


    20230910150719


    爱瞎折腾的朋友们可能就会想了,为什么 create-react-pp 也是用的 webpack 作为构建工具,为什么我的输出和它的输出是不一样的呢?


    20230910150945


    compiler


    通过查阅文档,我发现了问题所在,原来在 webpack 中它提供了一个 compiler 钩子,它用来监控文件系统的 监听(watching) 机制,并且在文件修改时重新编译。 当处于监听模式(watch mode)时, compiler 会触发诸如 watchRun, watchClose 和 invalid 等额外的事件。


    done 钩子就是当我们的代码被编译完成的时候被调用的。


    如何调用 done 钩子


    要想调用我们的 done 钩子,首先我们要引入 webpack 包,并把 webpack 配置传递给 webpack 函数,如下图所示:


    20230910151624


    接下来我们看看终端输出:


    20230910151749


    这些就是我们的一些 webpack 配置,在这个 compiler 对象上,它存在一个 hooks 对象,如下代码所示:


    compiler.hooks.done.tap("done", async (stats) => {
    console.log(11111111111111);
    });

    它会在代码编译完成阶段调用该回调函数:


    20230910152621


    咦,你会发现了,代码编译执行完成,我的终端上的输出会这么干净,是因为在输出控制台之前, 已经被我调用了一个函数清空了。


    通过这个函数,你可以情况控制台上的一些输出信息,如下代码所示:


    function clearConsole() {
    process.stdout.write(
    process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H"
    );
    }

    再调用以下,你会发现控制台上面很干净的,图下图所示:


    20230910153357


    要想这一些个性化的输出,我们直接在这个回调函数中打印输出就可以了,如果你要你输出的信息和项目中的信息有关,你可以利用 stats 这个参数:


    20230910160905


    大概就这样子,如果你想更好玩的话,你可以使用一些网络请求库,去获取一些网络资源:


    20230910161247


    去获取这些资源都是可以的呀。


    总结


    如果你的项目是使用的 webpack,并且要想在项目的开发中自定义,你可以通过 compiler.hooks 的方式去监听不同的钩子,然后通过不同的方式来实现不同的信息输出。


    源代码地址


    作者:Moment
    来源:juejin.cn/post/7277065056575848448
    收起阅读 »

    花亿点时间,写个Android抓包库

    0x1、引言 上周五版本刚提测,这周边改BUG边摸鱼,百无聊赖,想起前不久没业务需求时,随手写的Android抓包库。 就公司的APP集成了 抓包功能,目的是:方便非Android开发的同事在 接口联调和测试阶段 能够看到APP的请求日志,进行一些简单的问题定...
    继续阅读 »

    0x1、引言


    上周五版本刚提测,这周边改BUG边摸鱼,百无聊赖,想起前不久没业务需求时,随手写的Android抓包库。


    就公司的APP集成了 抓包功能,目的是:方便非Android开发的同事在 接口联调和测试阶段 能够看到APP的请求日志,进行一些简单的问题定位(如接口字段错误返回,导致APP UI显示异常),不用动不动就来找Android崽~


    手机摇一摇,就能查看 APP发起的请求列表具体的请求信息



    能用,但存在一些问题,先是 代码层面



    • 耦合:抓包代码直接硬编码在项目中,线上包不需要抓包功能,也会把这部分代码打包到APK里

    • 复用性差:其它APP想添加抓包功能,需要CV大量代码...

    • 安全性:是否启用抓包功能,通过 BuildConfig.DEBUG 来判断,二次打包修改AndroidManifest.xml文件添加 android:debuggable="true" 或者 root手机后修改ro.debuggable为1 设置手机为可调试模式,生产环境的接口请求一览无余。


    当然,上面的安全性有点 夸大 了,编译时,编译器会进一步优化代码,可能会删除未使用的变量或代码块。比如这样的代码:


    if (BuildConfig.DEBUG) {
    xxx.setBaseUrl(Config.DEBUG_BASE_URL);
    } else {
    xxx.setBaseUrl(Config.RELEASE_BASE_URL);
    }

    Release打包,BuildConfig.DEBUG永远为false,编译器会优化下代码,编译后的代码可能就剩这句:


    xxx.setBaseUrl(Config.RELEASE_BASE_URL);

    不信的读者可以反编译自己的APP试试康~


    尽管编译后的Release包不包含 启用抓包的代码,但是把抓包代码打包到APK里,始终是不妥的。


    毕竟,反编译apk,smail加个启用抓包的代码,并不是什么难事,最好的处理方式还是不要把抓包代码打包到Release APK中!


    接着说说 实用性层面



    • 请求相关信息太少:只有URL、请求参数和响应参数这三个数据,状态吗码都没有,有时需要看下请求头或响应头参数。

    • 只能看不能复制:有时需要把请求参数发给后端。

    • 字段查找全靠肉眼扫:请求/响应Json很长的时候,看到眼花😵‍💫。

    • 不支持URL过滤: 执行一个操作,唰唰唰一堆请求,然后就是滑滑滑,肉👀筛URL。

    • 请求记录不会动态更新,要看新请求得关闭页面再打开。

    • 等等...


    综上,还是有必要完善下这个库的,毕竟也是能 提高团队研发效率的一小环~


    说得天花龙凤,其实没啥技术难点,库的本质就是:自定义一个okhttp拦截器获取请求相关信息然后进行一系列封装 而已。


    库不支持HttpUrlConnection、Flutter、其它协议包的抓取!!!此抓包库的定位是:方便非Android崽,查看公司APP的请求日志


    如果是 Android崽或者愿意折腾,想抓手机所有APP包 的朋友,可以参考下面两篇文章:



    接着简单记录下库的开发流程~


    0x2、库


    ① 拦截器 和 请求实体类


    这一步就是了解API,把能抠的参数都抠出来,请求/响应头,请求体响应体,没啥太的难度,直接参考 lygttpod/AndroidMonitor 拦截器部分的代码:


    class CaptureInterceptor : Interceptor {
    private var maxContentLength = 5L * 1024 * 1024

    override fun intercept(chain: Interceptor.Chain): Response {
    val request = chain.request()
    val networkLog = NetworkLog().apply {
    method = request.method() // 请求方法
    request.url().toString().takeIf(String::isNotEmpty)?.let(URI::create)?.let { uri ->
    url = "$uri" // 请求地址
    host = uri.host
    path = uri.path + if (uri.query != null) "?${uri.query}" else ""
    scheme = uri.scheme
    requestTime = System.currentTimeMillis()
    }
    requestHeaders = request.headers().toJsonString() // 请求头
    request.body()?.let { body -> body.contentType()?.let { requestContentType = "$it" } }
    }
    val startTime = System.nanoTime() // 记录请求发起时间(微秒级别)
    val requestBody = request.body()
    requestBody?.contentType()?.let { networkLog.requestContentType = "$it" }
    when {
    // 请求头为空、未知编码类、双工(可读可写)、请求体只能用一次
    requestBody == null || bodyHasUnknownEncoding(request.headers()) || requestBody.isDuplex || requestBody.isOneShot -> {}
    // 上传文件
    requestBody is MultipartBody -> {
    networkLog.requestBody = StringBuilder().apply {
    requestBody.parts().forEach {
    val key = it.headers()?.value(0)
    append(
    if (it.body().contentType()?.toString()?.contains("octet-stream") == true)
    "${key}; value=文件流\n" else "${key}; value=${it.body().readString()}\n"
    )
    }
    }.toString()
    }
    else -> {
    val buffer = Buffer()
    requestBody.writeTo(buffer)
    val charset = requestBody.contentType()?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
    if (buffer.isProbablyUtf8()) networkLog.requestBody =
    formatBody(buffer.readString(charset), networkLog.requestContentType)
    }
    }

    val response: Response
    try {
    response = chain.proceed(request)
    networkLog.apply {
    responseHeaders = response.headers().toJsonString() // 响应头
    responseTime = System.currentTimeMillis()
    duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) // 当前时间减去请求发起时间得出响应时间
    protocol = response.protocol().toString()
    responseCode = response.code()
    responseMessage = response.message()
    }
    val responseBody = response.body()
    responseBody?.contentType()?.let { networkLog.responseContentType = "$it" }
    val bodyHasUnknownEncoding = bodyHasUnknownEncoding(response.headers())
    // 响应体不为空、支持获取响应体、知道编码类型
    if (responseBody != null && response.promisesBody() && !bodyHasUnknownEncoding) {
    val source = responseBody.source()
    source.request(Long.MAX_VALUE) // 将响应体的内容都读取到缓冲区中
    var buffer = source.buffer // 获取响应体源数据流
    // 如果响应体经过Gzip压缩,先解压缩
    if (bodyGzipped(response.headers())) {
    GzipSource(buffer.clone()).use { gzippedResponseBody ->
    buffer = Buffer()
    buffer.writeAll(gzippedResponseBody)
    }
    }
    // 获取不到字符集的话默认使用UTF-8 字符集
    val charset = responseBody.contentType()?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
    if (responseBody.contentLength() != 0L && buffer.isProbablyUtf8()) {
    val body = readFromBuffer(buffer.clone(), charset)
    networkLog.responseBody = formatBody(body, networkLog.responseContentType)
    }
    networkLog.responseContentLength = buffer.size()
    }
    NetworkCapture.insertNetworkLog(networkLog)
    Log.d("NetworkInterceptor", networkLog.toString())
    return response
    } catch (e: Exception) {
    networkLog.errorMsg = "$e"
    Log.e("NetworkInterceptor", networkLog.toString())
    NetworkCapture.insertNetworkLog(networkLog)
    throw e
    }
    }

    // 检查头中的内容编码是否为除了 "identity" 和 "gzip" 外的其他未知编码类型
    private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
    val contentEncoding = headers["Content-Encoding"] ?: return false
    return !contentEncoding.equals("identity", ignoreCase = true) &&
    !contentEncoding.equals("gzip", ignoreCase = true)
    }

    // 判断头是否包含Gzip压缩
    private fun bodyGzipped(headers: Headers): Boolean {
    return "gzip".equals(headers["Content-Encoding"], ignoreCase = true)
    }

    // 从缓冲区读取字符串数据
    private fun readFromBuffer(buffer: Buffer, charset: Charset?): String {
    val bufferSize = buffer.size()
    val maxBytes = min(bufferSize, maxContentLength)
    return StringBuilder().apply {
    try {
    append(buffer.readString(maxBytes, charset!!))
    } catch (e: EOFException) {
    append("\n\n--- Unexpected end of content ---")
    }
    if (bufferSize > maxContentLength) append("\n\n--- Content truncated ---")
    }.toString()
    }

    }

    请求实体:


    data class NetworkLog(
    var id: Long? = null,
    var method: String? = null,
    var url: String? = null,
    var scheme: String? = null,
    var protocol: String? = null,
    var host: String? = null,
    var path: String? = null,
    var duration: Long? = null,
    var requestTime: Long? = null,
    var requestHeaders: String? = null,
    var requestBody: String? = null,
    var requestContentType: String? = null,
    var responseCode: Int? = null,
    var responseTime: Long? = null,
    var responseHeaders: String? = null,
    var responseBody: String? = null,
    var responseMessage: String? = null,
    var responseContentType: String? = null,
    var responseContentLength: Long? = null,
    var errorMsg: String? = null,
    var source: String? = null
    ) : Serializable {
    fun getRequestTimeStr(): String =
    if (requestTime == null) "无" else TIME_LONG.format(Date(requestTime!!))

    fun getResponseTimeStr(): String =
    if (requestTime == null) "无" else TIME_LONG.format(Date(responseTime!!))
    }

    ② 数据库 和 Dao


    直接用原生SQLite实现,就一张表和一些简单操作,就不另外引个第三方库了,自定义SQLiteOpenHelper:


    class NetworkLogDB(context: Context) :
    SQLiteOpenHelper(context, "cp_network_capture.db", null, DB_VERSION) {
    companion object {
    private const val DB_VERSION = 1
    }

    override fun onCreate(db: SQLiteDatabase?) {
    db?.execSQL(NetworkLogDao.createTableSql())
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
    }

    接着在Dao里编写建表,增删查表的方法:


    class NetworkLogDao(private val db: NetworkLogDB) {
    companion object {
    const val TABLE_NAME = "network_log"

    /**
    * 建表SQL语句
    * */

    fun createTableSql() = StringBuilder("CREATE TABLE $TABLE_NAME(").apply {
    append("id INTEGER PRIMARY KEY AUTOINCREMENT,")
    append("method TEXT,")
    append("url TEXT,")
    append("scheme TEXT,")
    append("protocol TEXT,")
    append("host TEXT,")
    append("path TEXT,")
    append("duration INTEGER,")
    append("requestTime INTEGER,")
    append("requestHeaders TEXT,")
    append("requestBody TEXT,")
    append("requestContentType TEXT,")
    append("responseCode INTEGER,")
    append("responseTime INTEGER,")
    append("responseHeaders TEXT,")
    append("responseBody TEXT,")
    append("responseMessage TEXT,")
    append("responseContentType TEXT,")
    append("responseContentLength INTEGER,")
    append("errorMsg STRING,")
    append("source STRING")
    append(")")
    }.toString()
    }


    /**
    * 插入数据
    * */

    fun insert(data: NetworkLog) {
    db.writableDatabase.insert(TABLE_NAME, null, ContentValues().apply {
    put("method", data.method)
    put("url", data.url)
    put("scheme", data.scheme)
    put("protocol", data.protocol)
    put("host", data.host)
    put("path", data.path)
    put("duration", data.duration)
    put("requestTime", data.requestTime)
    put("requestHeaders", data.requestHeaders)
    put("requestBody", data.requestBody)
    put("requestBody", data.requestBody)
    put("requestContentType", data.requestContentType)
    put("responseCode", data.responseCode)
    put("responseTime", data.responseTime)
    put("responseHeaders", data.responseHeaders)
    put("responseBody", data.responseBody)
    put("responseMessage", data.responseMessage)
    put("responseContentType", data.responseContentType)
    put("responseContentLength", data.responseContentLength)
    put("errorMsg", data.errorMsg)
    put("source", data.source)
    })
    NetworkCapture.context?.contentResolver?.notifyChange(NetworkCapture.networkLogTableUri, null)
    }

    /**
    * 查询数据
    * @param offset 第几页,从0开始
    * @param limit 分页条数
    * */

    fun query(
    offset: Int = 0,
    limit: Int = 20,
    selection: String? = null,
    selectionArgs: Array<String>? = null
    )
    : ArrayList<NetworkLog> {
    val logList = arrayListOf<NetworkLog>()
    val cursor = db.readableDatabase.query(
    TABLE_NAME, null, selection, selectionArgs, null, null, "id DESC", "${offset * limit},${limit}"
    )
    if (cursor.moveToFirst()) {
    do {
    logList.add(NetworkLog().apply {
    id = cursor.getLong(0)
    method = cursor.getString(1)
    url = cursor.getString(2)
    scheme = cursor.getString(3)
    protocol = cursor.getString(4)
    host = cursor.getString(5)
    path = cursor.getString(6)
    duration = cursor.getLong(7)
    requestTime = cursor.getLong(8)
    requestHeaders = cursor.getString(9)
    requestBody = cursor.getString(10)
    requestContentType = cursor.getString(11)
    responseCode = cursor.getInt(12)
    responseTime = cursor.getLong(13)
    responseHeaders = cursor.getString(14)
    responseBody = cursor.getString(15)
    responseMessage = cursor.getString(16)
    responseContentType = cursor.getString(17)
    responseContentLength = cursor.getLong(18)
    errorMsg = cursor.getString(19)
    source = cursor.getString(20)

    })
    } while (cursor.moveToNext())
    }
    cursor.close()
    return logList
    }

    /**
    * 根据id删除数据
    * @param id 记录id
    * */

    fun deleteById(id: Long) {
    db.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf("$id"))
    }

    /**
    * 清空数据
    * */

    fun clear() {
    db.writableDatabase.delete(TABLE_NAME, null, null)
    }
    }

    ③ UI 与 交互


    没带安卓机回家...待补充图片...


    ④ 集成方式


    参考leakcanary的集成方式,利用 activity-alias 标签单独创建一个桌面图标,作为抓包页面入口:


    <activity-alias
    android:name=".NetworkCaptureActivity"
    android:exported="true"
    android:icon="@mipmap/cp_network_capture_logo"
    android:label="抓包"
    android:targetActivity="cn.coderpig.cp_network_capture.ui.activity.NetworkCaptureActivity"
    android:taskAffinity="cn.coderpig.cp_dev_helper.${applicationId}">

    <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity-alias>

    接着是Context传递的,自定义一个ContentProvider,在onCreate()处获得,顺带加上监听数据库变化:


    class CpNetworkCaptureProvider : ContentProvider() {
    override fun onCreate(): Boolean {
    val context = context
    if (context == null) {
    Log.e(TAG, "CpNetworkCapture库初始化Context失败")
    } else {
    Log.e(TAG, context.packageName)
    NetworkCapture.init(context)
    }
    return true
    }

    override fun query(
    uri: Uri,
    projection: Array<out String>?,
    selection: String?,
    selectionArgs: Array<out String>?,
    sortOrder: String?
    )
    : Cursor? = null

    override fun getType(uri: Uri): String? = null
    override fun insert(uri: Uri, values: ContentValues?): Uri? = null
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
    }

    接着使用 debugImplementation 方式导入依赖,打debug包才会打包这部分代码,接着使用使用反射的方式添加抓包拦截器即可~


    作者:coder_pig
    来源:juejin.cn/post/7276750877250699320
    收起阅读 »

    发送验证码后的节流倒计时丨刷新 & 重新进入页面,还原倒计时状态

    web
    前言   最近在做一个 H5 工具,需要手机号 + 验证码登录,很自然地,点击发送验证码后需要等待一段时间才能重新发送,用于请求节流,避免用户疯狂点击:     不过这里其实有个隐藏需求——如果仍然在冷却时间内,那么用户无论是刷新或是关闭页面,再次打开登...
    继续阅读 »

    前言


      最近在做一个 H5 工具,需要手机号 + 验证码登录,很自然地,点击发送验证码后需要等待一段时间才能重新发送,用于请求节流,避免用户疯狂点击:





     

      不过这里其实有个隐藏需求——如果仍然在冷却时间内,那么用户无论是刷新或是关闭页面,再次打开登录弹窗,需要直接展示正确的倒计时状态


    解决方案



    使用经典的 localStorage




    1. 发送验证码时,将发送时间 (lastSendingTime) 存入 localStorage,并开启 60 秒倒计时。

    2. 倒计时结束后,清除 localStorage 中的 lastSendingTime

    3. 重新进入页面时,若 localStorage 中存有 lastSendingTime,则说明仍处于冷却时间内,那么计算出剩余的倒计时 N,并开启 N 秒倒计时。


    Talk is cheap, show me the code!


      const [countdown, setCountdown] = useState(60) // 倒计时
    const [canSendCode, setCanSendCode] = useState(true) // 控制按钮文案的状态
    const [timer, setTimer] = useState() // 定时器 ID

    async function sendVerificationCode() {
    try {
    // network request...
    Toast.show({ content: '验证码发送成功' })
    startCountdown()
    setCanSendCode(false)
    } catch (error) {
    setCountdown(0)
    setCanSendCode(true)
    }
    }

    function startCountdown() {
    const nowTime = new Date().getTime()
    const lastSendingTime = localStorage.getItem('lastSendingTime')
    if (lastSendingTime) {
    // 若 localStorage 中存有 lastSendingTime,则说明仍处于冷却时间内,计算出剩余的 countdown
    const restCountdown = 60 - parseInt(((nowTime - lastSendingTime) / 1000), 10)
    setCountdown(restCountdown <= 0 ? 0 : restCountdown)
    } else {
    // 否则说明冷却时间已结束,则 countdown 为 60s,并将发送时间存入 localStorage
    setCountdown(60)
    localStorage.setItem('lastSendingTime', nowTime)
    }

    setTimer(
    setInterval(() => {
    setCountdown(old => old - 1)
    }, 1000),
    )
    }

    // 重新进入页面时,若 localStorage 中存有上次的发送时间,则说明还处于冷却时间内,则调用函数计算剩余倒计时;
    // 否则什么也不做
    useEffect(() => {
    const lastSendingTime = localStorage.getItem('lastSendingTime')
    if (lastSendingTime) {
    setCanSendCode(false)
    startCountdown()
    }

    return () => {
    clearInterval(timer)
    }
    }, [])


    // 监听倒计时,倒计时结束时:
    // * 清空 localStorage 中存储的上次发送时间
    // * 清除定时器
    // * 重置倒计时
    useEffect(() => {
    if (countdown <= 0) {
    setCanSendCode(true)
    localStorage.removeItem('lastSendingTime')
    clearInterval(timer)
    setCountdown(60)
    }
    }, [countdown])

    return (
    {canSendCode ? (
    <span onClick={sendVerificationCode}>
    获取验证码
    </span>

    ) : (
    <span>
    获取验证码({`${countdown}`})
    </span>

    )}
    )

    最终效果





    总结


      一开始感觉这是个很简单的小需求,可能 20min 就写完了,但实际花了两个多小时才把逻辑全部 cover 到,还是不能太自信啊~


    作者:Victor_Ye
    来源:juejin.cn/post/7277187894872014848
    收起阅读 »

    别再用 display: contents 了

    web
    文章讨论了在网站上使用"display: contents"属性可能导致的潜在问题。作者强调了这种做法可能破坏网页的语义结构,并可能对可访问性产生不利影响。文章还提到了一些潜在的解决方案,以帮助开发人员避免这些问题。 下面是正文~~ display: cont...
    继续阅读 »

    文章讨论了在网站上使用"display: contents"属性可能导致的潜在问题。作者强调了这种做法可能破坏网页的语义结构,并可能对可访问性产生不利影响。文章还提到了一些潜在的解决方案,以帮助开发人员避免这些问题。


    下面是正文~~


    display: contents 介绍


    CSS(层叠样式表)中的 display: contents 是一个相对较新的属性值,它对元素的布局和可视化有特殊的影响。当你对一个元素应用 display: contents,这个元素本身就像从DOM(文档对象模型)中消失了一样,而它的所有子元素则会升级到DOM结构中的下一个层级。换句话说,该元素的盒模型将被忽略,它的子元素会取而代之,就像直接插入到父元素中一样。


    假设我们有这样一个HTML结构:


    id="parent">
    id="child1">Child 1
    id="child2">Child 2

    正常情况下,#parent#child1#child2 的父元素,它们在DOM和布局中有一个明确的层级关系。


    现在,如果我们对 #parent 应用 display: contents


    #parent {
    display: contents;
    }

    在这种情况下,#parent 在页面布局中就像是“消失了”一样。它的所有子元素(这里是 #child1#child2)会直接升级到#parent所在的DOM层级。也就是说,在布局和渲染过程中,#child1#child2 将不再被视为 #parent 的子元素,而是像直接插入到 #parent 的父元素中一样。


    这样做的结果是,任何应用于 #parent 的布局和样式都不会影响到页面的渲染,但 #child1#child2 会像正常元素一样被渲染。


    主要用途:



    1. 语义改进:能够改进HTML结构,使其更符合语义,但不影响布局和样式。

    2. 布局优化:在某些复杂的布局场景中,它可以简化DOM结构,提高渲染性能。


    display: contents 和可访问性的长期问题


    从字面上看,这个CSS声明改变了其应用到的元素的显示属性。它使元素“消失”,将其子元素提升到DOM中的下一层级。


    这种声明在很多方面都可能是有用的。讽刺的是,其中一个用例就是改善你工作的底层语义。然而,这个声明一开始的效果有点过头了。


    CSS和可访问性


    不是每个人都意识到这一点,但某些CSS会影响辅助技术的工作方式。就像烧毁你的房子确实会成功地除去其中可能存在的蜘蛛一样,使用 display: contents 可能会完全消除某些元素被辅助技术识别的关键属性。


    简而言之,这会导致按钮不被声明为按钮,表格不被声明和导航为表格,列表也是如此,等等。


    换句话说:当人们说“HTML默认是可访问的”时,display: contents 彻底破坏了这个“默认”。这不好。


    可访问性从业者注意到了这个问题,并提出了完全合理的修复要求。特别值得一提的是Adrian Roselli的勤勉、有条理和实事求是的文档和报告工作。


    修复已经完成,浏览器也已经更新,我们得到了一个快乐的结局。对吗?并不是那么简单。


    回归问题


    在软件开发中,回归可能意味着几件事情。这个词通常用于负面语境,表达更新后的行为不小心恢复到以前,不太理想的工作方式。


    对于 display: contents,这意味着每个人的自动或近乎自动更新的浏览器抛弃了非常必要的错误修复,而没有任何警告或通知,就回到了破坏语义HTML与辅助技术交流的基础属性。


    这种类型的回归不是一个令人讨厌的 bug,而是破坏了 Web 可访问性的基础方面。


    Adrian注意到了这一点。如果你继续阅读我给你链接的部分,他继续注意到这一点。总之,我统计了关于 display: contents 的行为以不可访问的方式回归了16次的更新。


    看问题的角度


    制作浏览器是一件困难的事情。需要考虑很多、很多不同的事情,那还没考虑到软件的复杂性。


    可访问性并不是每个人的首要任务。我可以在这里稍微宽容一些,因为我主要是尝试用我拥有的东西工作,而不是我希望能有的东西。我习惯了应对由于这种优先级而产生的所有小问题、陷阱和杂项。


    然而,能够使用Web界面绝非小事。display: contents 的问题对使用它的界面的人们的生活质量有非常真实、非常可量化的影响。


    我还想让你考虑一下这种打地鼠游戏是如何影响可访问性从业者的。告诉某人他们不能使用一个闪亮的新玩具永远不会受到欢迎。然后告诉他们你可以,但后来又不能了,这会削弱信任和能力的认知。


    别用 display: contents


    现在,我不认为我们这个行业可以自信地使用 display: contents。过去的行为是未来行为的良好指标,而走向地狱的道路是由好意铺成的。


    我现在认为这个声明是不可预测的。常见的“只需用辅助技术测试其支持情况”的回应在这里也不适用——当前浏览器版本中该声明的期望行为并不能保证在该浏览器的未来版本中持续。


    这是一件罕见且令人不安的事情——整个现代Web都是建立在这样的假设之上,即这样的事情不会以这种方式停止工作。这不是互操作性问题,而是由于疏忽造成的伤害。


    display: contents 的回归给我们提供了一个小小的窗口,让我们看到浏览器制作的某些方面是如何(或不是如何)被优先考虑和测试的。


    人们可以发誓说像可访问性和包容性这样的事情是重要的,但当涉及到这个特定的CSS声明时,很明显大多数浏览器制造商是不可信的。


    这个声明在实践中的不稳定性代表了一种非常真实、非常严重的风险,即在你无法控制的情况下,可能会在你的网站或Web应用中引入关键的可访问性问题。


    作者:王大冶
    来源:juejin.cn/post/7275973778915573772
    收起阅读 »

    程序员创业:从技术到商业的转变

    作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。 为什么程...
    继续阅读 »

    作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。


    为什么程序员要创业?


    创业其实并非只适用于商学院的毕业生或者有创新理念的企业家。程序员在业内有着相当高的技术储备和市场先知,因此更容易从技术角度前瞻和切入新兴市场,更好地利用技术储备来实现创业梦想。


    此外,创业可以释放我们的潜力,同时也可以让我们找到自己的定位和方向。在创业的过程中,我们可能会遇到各种挑战和困难,但这些挑战也将锻炼我们的意志力和决策能力,让我们更好地发挥自己的潜力。


    创业需要具备的技能


    作为一名技术人员,创业需要具备更多的技能。首先是商业和运营的技能:包括市场分析、用户研究、产品策划、项目管理等。其次是团队管理和沟通能力,在创业的过程中,人才的招聘和管理是核心问题。


    另外,还需要具备跨界合作的能力,通过开放性的合作与交流,借助不同团队的技术和资源,完成创业项目。所以我们应该将跨界合作看作是创业过程中的重要选择,选择和加强自己的跨界交流和合作能力,也能为我们的企业注入活力和创新精神。


    如何创业?


    从技术到商业的转变,从最初想法的诞生到成熟的企业的创立,都需要一个创业的路线图。以下是一些需要注意的事项:

    1. 研究市场:了解市场趋势,分析需求,制定产品策略。可以去参加行业论坛,争取到专业意见和帮助。

    2. 制定商业计划:包括产品方案、市场营销、项目管理、团队建设等。制定一个系统的商业计划是投资者和团队成员对创业企业的认可。

    3. 招募团队:由于我们一般不是经验丰富的企业家,团队的选择尤为重要。要找的不仅要是技能和经验匹配的团队,更要找能一起携手完成创业项目的合作者。

    4. 行动计划:从实现规划步入到实战行动是创业项目的关键。按部就班地完成阶段性任务,控制实施进度和途中变化,在完成一个阶段后可以重新评估计划。

    5. 完成任务并分析:最后,团队成员需要根据企业进展,完整阶段性的目标,做自己的工作。及时完成考核任务并一起分享数据分析、事件解决和项目总结等信息,为项目下一阶段做出准确预测。


    结语


    创业是一条充满挑战性和机遇的路线,也是在我们的技术和业务的进一步升级中一条非常良好的通道。越来越多的技术人员意识到了自己的潜力,开始考虑自己创业的可能性。只要学会逐步掌握创业所需的技能和知识,并制订出详细的创业路线图,大可放手去尝试,才能最终实现自己心中的创业梦想。


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

    南洋才女,德艺双馨,孙燕姿本尊回应AI孙燕姿(基于Sadtalker/Python3.10)

    孙燕姿果然不愧是孙燕姿,不愧为南洋理工大学的高材生,近日她在个人官方媒体博客上写了一篇英文版的长文,正式回应现在满城风雨的“AI孙燕姿”现象,流行天后展示了超人一等的智识水平,行文优美,绵恒隽永,对AIGC艺术表现得极其克制,又相当宽容,充满了语言上的古典之美...
    继续阅读 »

    孙燕姿果然不愧是孙燕姿,不愧为南洋理工大学的高材生,近日她在个人官方媒体博客上写了一篇英文版的长文,正式回应现在满城风雨的“AI孙燕姿”现象,流行天后展示了超人一等的智识水平,行文优美,绵恒隽永,对AIGC艺术表现得极其克制,又相当宽容,充满了语言上的古典之美,表现出了“任彼如泰山压顶,我只当清风拂面”的博大胸怀。


    本次我们利用edge-tts和Sadtalker库让AI孙燕姿朗诵本尊的博文,让流行天后念给你听。


    Sadtalker配置


    之前我们曾经使用百度开源的PaddleGAN视觉效果模型中一个子模块Wav2lip实现了人物口型与输入的歌词语音同步,但Wav2lip的问题是虚拟人物的动态效果只能局限在嘴唇附近,事实上,音频和不同面部动作之间的连接是不同的,也就是说,虽然嘴唇运动与音频的联系最强,但可以通过不同的头部姿势和眨眼来反作用于音频。


    和Wav2lip相比,SadTaker是一种通过隐式3D系数调制的风格化音频驱动Talking头部视频生成的库,一方面,它从音频中生成逼真的运动系数(例如,头部姿势、嘴唇运动和眨眼),并单独学习每个运动以减少不确定性。对于表达,通过从的仅嘴唇运动系数和重建的渲染三维人脸上的感知损失(唇读损失,面部landmark loss)中提取系数,设计了一种新的音频到表达系数网络。


    对于程序化的头部姿势,通过学习给定姿势的残差,使用条件VAE来对多样性和逼真的头部运动进行建模。在生成逼真的3DMM系数后,通过一种新颖的3D感知人脸渲染来驱动源图像。并且通过源和驱动的无监督3D关键点生成扭曲场,并扭曲参考图像以生成最终视频。


    Sadtalker可以单独配置,也可以作为Stable-Diffusion-Webui的插件而存在,这里推荐使用Stable-Diffusion插件的形式,因为这样Stable-Diffusion和Sadtalker可以共用一套WebUI的界面,更方便将Stable-Diffusion生成的图片做成动态效果。


    进入到Stable-Diffusion的项目目录:

    cd stable-diffusion-webui

    启动服务:

    python3.10 webui.py

    程序返回:

    Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]  
    Version: v1.3.0
    Commit hash: 20ae71faa8ef035c31aa3a410b707d792c8203a3
    Installing requirements
    Launching Web UI with arguments: --xformers --opt-sdp-attention --api --lowvram
    Loading weights [b4d453442a] from D:\work\stable-diffusion-webui\models\Stable-diffusion\protogenV22Anime_protogenV22.safetensors
    load Sadtalker Checkpoints from D:\work\stable-diffusion-webui\extensions\SadTalker\checkpoints
    Creating model from config: D:\work\stable-diffusion-webui\configs\v1-inference.yaml
    LatentDiffusion: Running in eps-prediction mode
    DiffusionWrapper has 859.52 M params.
    Running on local URL: http://127.0.0.1:7860

    代表启动成功,随后http://localhost:7860


    选择插件(Extensions)选项卡


    点击从url安装,输入插件地址:github.com/Winfredy/SadTalker


    安装成功后,重启WebUI界面。


    接着需要手动下载相关的模型文件:

    https://pan.baidu.com/s/1nXuVNd0exUl37ISwWqbFGA?pwd=sadt

    随后将模型文件放入项目的stable-diffusion-webui/extensions/SadTalker/checkpoints/目录即可。


    接着配置一下模型目录的环境变量:

    set SADTALKER_CHECKPOINTS=D:/stable-diffusion-webui/extensions/SadTalker/checkpoints/

    至此,SadTalker就配置好了。


    edge-tts音频转录


    之前的歌曲复刻是通过So-vits库对原歌曲的音色进行替换和预测,也就是说需要原版的歌曲作为基础数据。但目前的场景显然有别于歌曲替换,我们首先需要将文本转换为语音,才能替换音色。


    这里使用edge-tts库进行文本转语音操作:

    import asyncio  

    import edge_tts

    TEXT = '''

    As my AI voice takes on a life of its own while I despair over my overhanging stomach and my children's every damn thing, I can't help but want to write something about it.

    My fans have officially switched sides and accepted that I am indeed 冷门歌手 while my AI persona is the current hot property. I mean really, how do you fight with someone who is putting out new albums in the time span of minutes.

    Whether it is ChatGPT or AI or whatever name you want to call it, this "thing" is now capable of mimicking and/or conjuring, unique and complicated content by processing a gazillion chunks of information while piecing and putting together in a most coherent manner the task being asked at hand. Wait a minute, isn't that what humans do? The very task that we have always convinced ourselves; that the formation of thought or opinion is not replicable by robots, the very idea that this is beyond their league, is now the looming thing that will threaten thousands of human conjured jobs. Legal, medical, accountancy, and currently, singing a song.

    You will protest, well I can tell the difference, there is no emotion or variance in tone/breath or whatever technical jargon you can come up with. Sorry to say, I suspect that this would be a very short term response.

    Ironically, in no time at all, no human will be able to rise above that. No human will be able to have access to this amount of information AND make the right calls OR make the right mistakes (ok mayyyybe I'm jumping ahead). This new technology will be able to churn out what exactly EVERYTHING EVERYONE needs. As indie or as warped or as psychotic as you can get, there's probably a unique content that could be created just for you. You are not special you are already predictable and also unfortunately malleable.

    At this point, I feel like a popcorn eater with the best seat in the theatre. (Sidenote: Quite possibly in this case no tech is able to predict what it's like to be me, except when this is published then ok it's free for all). It's like watching that movie that changed alot of our lives Everything Everywhere All At Once, except in this case, I don't think it will be the idea of love that will save the day.

    In this boundless sea of existence, where anything is possible, where nothing matters, I think it will be purity of thought, that being exactly who you are will be enough.

    With this I fare thee well.

    '''

    VOICE = "en-HK-YanNeural"
    OUTPUT_FILE = "./test_en1.mp3"


    async def _main() -> None:
    communicate = edge_tts.Communicate(TEXT, VOICE)
    await communicate.save(OUTPUT_FILE)


    if __name__ == "__main__":
    asyncio.run(_main())

    音频使用英文版本的女声:en-HK-YanNeural,关于edge-tts,请移步:口播神器,基于Edge,微软TTS(text-to-speech)文字转语音免费开源库edge-tts语音合成实践(Python3.10),这里不再赘述。


    随后再将音频文件的音色替换为AI孙燕姿的音色即可:AI天后,在线飙歌,人工智能AI孙燕姿模型应用实践,复刻《遥远的歌》,原唱晴子(Python3.10)


    本地推理和爆显存问题


    准备好生成的图片以及音频文件后,就可以在本地进行推理操作了,访问 localhost:7860



    这里输入参数选择full,如此会保留整个图片区域,否则只保留头部部分。


    生成效果:



    SadTalker会根据音频文件生成对应的口型和表情。


    这里需要注意的是,音频文件只支持MP3或者wav。


    除此以外,推理过程中Pytorch库可能会报这个错误:

    torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 20.00 MiB (GPU 0; 6.00 GiB total capacity; 5.38 GiB already allocated; 0 bytes free; 5.38 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation. See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF

    这就是所谓的"爆显存问题"。


    一般情况下,是因为当前GPU的显存不够了所导致的,可以考虑缩小torch分片文件的体积:

    set PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:60

    如果音频文件实在过大,也可以通过ffmpeg对音频文件切片操作,分多次进行推理:

    ffmpeg -ss 00:00:00 -i test_en.wav -to 00:30:00 -c copy test_en_01.wav

    藉此,就解决了推理过程中的爆显存问题。


    结语


    和Wav2Lip相比,SadTalker(Stylized Audio-Driven Talking-head)提供了更加细微的面部运动细节(如眼睛眨动)等等,可谓是细致入微,巨细靡遗,当然随之而来的是模型数量和推理成本以及推理时间的增加,但显然,这些都是值得的。


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

    开发一个基于环信IM的Vue3聊天室插件,从而快速实现直播间聊天室功能

    前言由于看到有部分的需求为在页面层,快速的引入一个包,并且以简单的配置,就可以快速实现一个聊天窗口,因此尝试以 Vue3 插件的形式开发一个轻量的聊天窗口。这次简单分享一下此插件的实现思路,以及实现过程,并描述一下本次插件发布 npm 的过程。技术栈Vue3p...
    继续阅读 »

    前言

    由于看到有部分的需求为在页面层,快速的引入一个包,并且以简单的配置,就可以快速实现一个聊天窗口,因此尝试以 Vue3 插件的形式开发一个轻量的聊天窗口。

    这次简单分享一下此插件的实现思路,以及实现过程,并描述一下本次插件发布 npm 的过程。

    技术栈

    • Vue3
    • pnpm
    • Typescript
    • Vite

    插件核心目录设计

    📦 emchat-chatroom-widget
    ┣ 📂 build // 插件打包输出的目录
    ┣ 📂 demo // 验证插件demo相关目录
    ┣ 📂 scripts // 打包脚本目录
    ┣ 📂 src // 插件源代码
    ┃ ┣ 📂 components // 组件目录
    ┃ ┣ 📂 container // 容器组件目录
    ┃ ┣ 📂 EaseIM // 环信IM相关目录
    ┃ ┣ 📂 utils // 工具相关目录
    ┃ ┣ 📜 index.ts // 插件入口文件
    ┃ ┗ 📜 install.ts // 插件初始化文件
    ┣ 📜 package.json // 项目配置文件
    ┣ 📜 vite.config.ts // vite配置文件
    ┗ 📜 README.md // 项目说明文档
    ...

    实现过程

    确认功能范围

    首先确认本次插件实现的功能范围,从而围绕要实现的功能着手进行开发准备。

    1. Vue3 框架使用
    2. 轻量配置、仅配置少量参数即可立即使用聊天功能
    3. 页面大小自适应,给定容器宽高,插件内部宽高自适应。
    4. 仅聊天室类型消息支持基础文本,表情,图片。
      暂时第一期仅支持这些功能范围。

    着手开发

    1、创建空白项目

    pnpm create vite emchat-chatroom-widget --template vue-ts

    2、配置eslint pretter 等代码校验、以及代码风格工具。

    pnpm i eslint eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
    pnpm i prettier eslint-config-prettier eslint-plugin-prettier -D

    同时也不要忘了创建对应的 .eslintrc.cjs.prettierrc.cjs

    这里遇到了一个问题:

    这几个文件以 cjs 结尾是因为 package.json 创建时设置了"type": "module" 后你的所有 js 文件默认使用 ESM 模块规范,不支持 commonjs 规范,所以必须显式的声明成 xxx.cjs 才能标识这个是用 commonjs 规范的,把你的配置都改成.cjs 后缀。

    3、配置 scripts 打包脚本

    目录下新建一个文件夹命名为scripts,新加一个 build.js 或者为.ts 文件。

    在该文件中引入vite进行打包时的配置。由于本次插件编写时使用了jsx语法进行编写,因此 vite 打包时也需要引入 jsx 打包插件。
    安装@vitejs/plugin-vue-jsx插件。

    const BASE_VITE_CONFIG = defineConfig({
    publicDir: false, //暂不需要打包静态资源到public文件夹
    plugins: [
    vue(),
    vueJSX(),
    // visualizer({
    // emitFile: true,
    // filename: "stats.html"
    // }),
    dts({
    outputDir: './build/types',
    insertTypesEntry: true, // 插入TS 入口
    copyDtsFiles: true, // 是否将源码里的 .d.ts 文件复制到 outputDir
    }),
    ],
    });

    package.json中增加 build 脚本执行命令,

      "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint src --fix",
    "build:widget": "node ./scripts/build.js"
    },

    整体 build.js 代码由于篇幅关系,可以后面查看文末的源码地址。

    4、 编写 Vue3 插件入口函数

    import type { App } from 'vue';
    import EasemobChatroom from './container';
    import { initEMClient } from './EaseIM';
    export interface IEeasemobOptions {
    appKey: string;
    }

    export default {
    install: (app: App, options: IEeasemobOptions) => {
    // 在这里编写插件代码
    console.log(app);
    console.log('options', options);
    if (options && options?.appKey) {
    initEMClient(options.appKey);
    } else {
    throw console.error('appKey不能为空');
    }
    app.component(EasemobChatroom.name, EasemobChatroom);
    },
    };

    5、聊天插件入口代码

    聊天插件入口组件主要用来接收插件使用者所传递进来的一些必要参数,比如登录用户 id、密码、token、聊天室 id,以及针对初始化插件的初始状态。

    import { defineComponent, onMounted } from "vue"
    import { EMClient } from "../EaseIM"
    import { useManageChatroom } from "../EaseIM/mangeChatroom"
    import { manageEasemobApis } from "../EaseIM/imApis"
    import "./style/index.css"
    /* components */
    import MessageContainer from "./message"
    import InputBarContainer from "./inputbar"
    console.log("EMClient", EMClient)
    export default defineComponent({
    name: "EasemobChatroom",
    props: {
    username: {
    type: String,
    default: "",
    required: true
    },
    password: {
    type: String,
    default: ""
    },
    accessToken: {
    type: String,
    default: ""
    },
    chatroomId: {
    type: String,
    default: "",
    required: true
    }
    },
    setup(props) {
    const { setCurrentChatroomId } = useManageChatroom()
    const { loginIMWithPassword, loginIMWithAccessToken } = manageEasemobApis()
    const loginIM = async (): Promise<void> => {
    if (!EMClient) return
    try {
    if (props.accessToken) {
    await loginIMWithAccessToken(props.username, props.accessToken)
    } else {
    await loginIMWithPassword(props.username, props.password)
    }
    } catch (error: any) {
    throw `${error.data.message}`
    }
    }
    const closeIM = async (): Promise<void> => {
    console.log(">>>>>断开连接")
    // EMClient.close()
    }
    onMounted(() => {
    loginIM()
    if (props.chatroomId) {
    setCurrentChatroomId(props.chatroomId)
    }
    })
    return {
    loginIM,
    closeIM
    }
    },
    render() {
    return (
    <>
    <div class={"easemob_chatroom_container"}>
    <MessageContainer />
    <InputBarContainer />
    </div>
    </>
    )
    }
    })

    6、输入框组件代码

    主要处理插件输入框功能,实现消息文本内容,图片内容的发送。

    import { defineComponent, ref } from "vue"
    import { EasemobChat } from "easemob-websdk"
    import { EMClient } from "../EaseIM"
    import { useManageChatroom } from "../EaseIM/mangeChatroom"
    /* compoents */
    import InputEmojiComponent from "../components/InputEmojiComponent"
    import UploadImageComponent from "../components/UploadImageComponent"
    import "./style/inputbar.css"
    export enum PLACE_HOLDER_TEXT {
    TEXT = "Enter 发送输入的内容..."
    }
    export default defineComponent({
    name: "InputBarContainer",
    setup() {
    //基础文本发送
    const inputContent = ref("")
    const setInputContent = (event: Event) => {
    inputContent.value = (event.target as HTMLInputElement).value
    }
    const { currentChatroomId, loginUserInfo, sendDisplayMessage } =
    useManageChatroom()
    const sendMessage = async (event: KeyboardEvent) => {
    if (inputContent.value.match(/^\s*$/)) return
    if (event.code === "Enter" && !event.shiftKey) {
    event.preventDefault()
    console.log(">>>>>>调用发送方法")
    const param: EasemobChat.CreateTextMsgParameters = {
    chatType: "chatRoom",
    type: "txt",
    to: currentChatroomId.value,
    msg: inputContent.value,
    from: EMClient.user,
    ext: {
    nickname: loginUserInfo.nickname
    }
    }
    try {
    await sendDisplayMessage(param)
    inputContent.value = ""
    } catch (error) {
    console.log(">>>>>消息发送失败", error)
    }
    }
    }
    const appendEmojitoInput = (emoji: string) => {
    inputContent.value = inputContent.value + emoji
    }
    return () => (
    <>
    <div class={"input_bar_container"}>
    <div class={"control_strip_container"}>
    <InputEmojiComponent onAppendEmojitoInput={appendEmojitoInput} />
    <UploadImageComponent />
    </div>

    <div class={"message_content_input_box"}>
    <input
    class={"message_content_input"}
    type
    ="text"
    value
    ={inputContent.value}
    onInput
    ={setInputContent}
    placeholder
    ={PLACE_HOLDER_TEXT.TEXT}
    onKeyup
    ={sendMessage}
    />
    </div>
    </div>
    </>
    )
    }
    })

    7、消息列表组件代码

    渲染聊天室内收发的消息代码,以及列表滚动。

    import { defineComponent, nextTick, watch } from 'vue';
    import { useManageChatroom } from '../EaseIM/mangeChatroom';
    import { scrollBottom } from '../utils';
    import './style/message.css';
    import { EasemobChat } from 'easemob-websdk';
    const { messageCollect } = useManageChatroom();

    const MessageList = () => {
    const downloadSourceImage = (message: EasemobChat.MessageBody) => {
    if (message.type === 'img') {
    window.open(message.url);
    }
    };
    return (
    <>
    {messageCollect.length > 0 &&
    messageCollect
    .map((msgItem) => {
    return (
    <div class={'message_item_box'} key={msgItem.id}>
    <div class={'message_item_nickname'}>
    {msgItem?.ext?.nickname || msgItem.from}
    </div>
    {msgItem.type === 'txt' && (
    <p class={'message_item_textmsg'}>{msgItem.msg}</p>
    )}
    {msgItem.type === 'img' && (
    <img
    style
    ={'cursor: pointer;'}
    onClick
    ={() => {
    downloadSourceImage(msgItem);
    }}
    src
    ={msgItem.thumb}
    />
    )}
    </div>
    );
    })}
    </>
    );
    };
    export default defineComponent({
    name: 'MessageContainer',
    setup() {
    watch(messageCollect, () => {
    console.log('>>>>>>监听到消息列表改变');
    nextTick(() => {
    const messageContainer = document.querySelector('.message_container');
    setTimeout(() => {
    messageContainer && scrollBottom(messageContainer);
    }, 300);
    });
    });

    return () => {
    return (
    <>
    <div class='message_container'>
    <MessageList />
    </div>
    </>
    );
    };
    },
    });

    8、聊天室内核心方法

    聊天室内部分状态管理

    import { EasemobChat } from "easemob-websdk"
    import { reactive, ref } from "vue"
    import { DisplayMessageType, ILoginUserInfo } from "../types/index"
    import { manageEasemobApis } from "../imApis/"
    const messageCollect = reactive<DisplayMessageType[]>([])
    const loginUserInfo: ILoginUserInfo = {
    loginUserId: "",
    nickname: ""
    }
    const currentChatroomId = ref("")
    export const useManageChatroom = () => {
    const setCurrentChatroomId = (roomId: string) => {
    currentChatroomId.value = roomId
    }
    const setLoginUserInfo = async (loginUserId: string) => {
    const { fetchLoginUserNickname } = manageEasemobApis()
    loginUserInfo.loginUserId = loginUserId
    try {
    const res = await fetchLoginUserNickname(loginUserId)
    loginUserInfo.nickname = res[loginUserId].nickname
    console.log(">>>>>>获取到用户属性", loginUserInfo.nickname)
    } catch (error) {
    console.log(">>>>>>获取失败")
    }
    }
    const pushMessageToList = (message: DisplayMessageType) => {
    messageCollect.push(message)
    }
    const sendDisplayMessage = async (payload: EasemobChat.CreateMsgType) => {
    const { sendTextMessage, sendImageMessage } = manageEasemobApis()
    return new Promise((resolve, reject) => {
    if (payload.type === "txt") {
    sendTextMessage(payload)
    .then(res => {
    messageCollect.push(res as unknown as EasemobChat.TextMsgBody)
    resolve(res)
    })
    .catch(err => {
    reject(err)
    })
    }
    if (payload.type === "img") {
    sendImageMessage(payload)
    .then(res => {
    messageCollect.push(res as unknown as EasemobChat.ImgMsgBody)
    resolve(res)
    })
    .catch(err => {
    reject(err)
    })
    }
    })
    }

    return {
    messageCollect,
    currentChatroomId,
    loginUserInfo,
    setCurrentChatroomId,
    sendDisplayMessage,
    pushMessageToList,
    setLoginUserInfo
    }
    }

    实例化 im sdk

    import EaseSDK, { EasemobChat } from "easemob-websdk"
    import { mountEaseIMListener } from "./listener"
    export let EMClient = {} as EasemobChat.Connection
    export const EMCreateMessage = EaseSDK.message.create
    export const initEMClient = (appKey: string) => {
    EMClient = new EaseSDK.connection({
    appKey: appKey
    })
    mountEaseIMListener(EMClient)
    return EMClient
    }

    挂载聊天室相关监听监听

    import { EasemobChat } from 'easemob-websdk';
    import { useManageChatroom } from '../mangeChatroom';
    import { manageEasemobApis } from '../imApis';
    export const mountEaseIMListener = (EMClient: EasemobChat.Connection) => {
    const { pushMessageToList, setLoginUserInfo, currentChatroomId } =
    useManageChatroom();
    const { joinChatroom } = manageEasemobApis();
    console.log('>>>mountEaseIMListener');
    EMClient.addEventHandler('connection', {
    onConnected: () => {
    console.log('>>>>>onConnected');
    joinChatroom();
    setLoginUserInfo(EMClient.user);
    },
    onDisconnected: () => {
    console.log('>>>>>Disconnected');
    },
    onError: (error: any) => {
    console.log('>>>>>>Error', error);
    },
    });
    EMClient.addEventHandler('message', {
    onTextMessage(msg) {
    if (msg.chatType === 'chatRoom' && msg.to === currentChatroomId.value) {
    pushMessageToList(msg);
    }
    },
    onImageMessage(msg) {
    if (msg.chatType === 'chatRoom' && msg.to === currentChatroomId.value) {
    pushMessageToList(msg);
    }
    },
    });
    EMClient.addEventHandler('chatroomEvent', {
    onChatroomEvent(eventData) {
    console.log('>>>>chatroomEvent', eventData);
    },
    });
    };

    使用方式

    npm install emchat-chatroom-widget
    import EMChatroom from "emchat-chatroom-widget/emchat-chatroom-widget.esm.js"
    //引入插件内部样式
    import "emchat-chatroom-widget/style.css"
    //appKey 需从环信申请
    createApp(App)
    .use(EMChatroom, {
    appKey: "easemob#XXX"
    })
    .mount("#app")

    //模版组件内使用
    /**
    * @param {username} string
    * @param {password} string
    * @param {accessToken} string
    * @param {chatroomId} string
    */

    <EasemobChatroom
    :username="'hfp'"
    :password="'1'"
    :chatroomId="'208712152186885'"
    >
    </EasemobChatroom>

    最终效果

    image.png

    相关代码

    参考资料


    收起阅读 »

    sip中继

    sip
    sip中继是什么?sip是一个基于文本的应用层控制协议,用于创建、修改和释放一个或多个参与者的会话,同时也是一种源于互联网的IP语音会话控制协议。使用SIP,服务提供商可以随意选择标准组件,快速驾驭新技术。不论媒体内容和参与方数量,用户都可以查找和联系对方。s...
    继续阅读 »

    sip中继是什么?

    sip是一个基于文本的应用层控制协议,用于创建、修改和释放一个或多个参与者的会话,同时也是一种源于互联网的IP语音会话控制协议。使用SIP,服务提供商可以随意选择标准组件,快速驾驭新技术。不论媒体内容和参与方数量,用户都可以查找和联系对方。

    sip中继的功能用途

    SIP中继是基于网络连接不同的电话系统和视频会议系统,使它们能够相互通信。这种连接方式可以让用户在不同的地方进行无障碍的沟通交流,提高工作效率。

    sip中继的接入类型

    1、通过语音网关将PSTN转换成SIP:这一种的应用场景是运营商会拉一条电话线到用户的办公网地点。因为这条电话线是铜线的形式,它是无法被IP电话系统直接使用的。
    2、通过数字中继网关将数字中继转换成SIP:这种事其实交付的方式和上面的方法是一样的。唯一不同的是网关的接口不太一样,这个网关是要通过E1的线路接入,然把物理线路转换成sip线路。
    3、运营商SIP IMS中继-网线直连IP PBX:这应该是未来的主流形式。其形式就是运营商拉一条光猫到用户的办公场所。然后光猫上有一条网线可以接在用户的sip系统服务器的网络接口。
    4、三方SIP线路-互联网接入号码:即由基础运营商之外的第三方提供的SIP线路,从用户体验来说,这其实应该是中国最理想的sip中继。这是中国的某些二类电信运营商,或者不是运营商的一些企业,把SIP号码通过互联网的形式直接交付给用户。通过代理商的形式申请号码的这种类型会涉及到号码实名的问题。

    国内SIP中继的分类

    国内的运营商目前提供2种SIP中继连接方式:本地SIP中继和云端SIP中继

    本地sip中继由于是私有网络连接,适合于对安全要求比较高的企业

    云端sip中继通过互联网接入,简单易用


    云SIP中继的优势

    1、云部署,弹性按需扩展
    2、纯软,虚拟化先进架构
    3、云服务器:按需订阅,扩容方便
    4、无需布线:现有网线或者WIFI
    5、无需准备机房机柜
    6、无需专门的电话模块和电话线跳线
    7、无需自己维护:供应商提供整个系统的维护

    结语

    SIP中继是一种用于连接不同SIP网络的设备,它可以将SIP信号从一个网络传输到另一个网络。

    收起阅读 »

    GPT-4耗尽全宇宙数据!OpenAI接连吃官司,竟因数据太缺了,UC伯克利教授发出警告

    穷尽「全网」,生成式AI很快无数据可用。 近日,著名UC伯克利计算机科学家Stuart Russell称,ChatGPT和其他AI工具的训练可能很快耗尽「全宇宙的文本」。 换句话说,训练像ChatGPT这样的AI,将因数据量不够而受阻。 这可能会影响生成式...
    继续阅读 »

    穷尽「全网」,生成式AI很快无数据可用。




    近日,著名UC伯克利计算机科学家Stuart Russell称,ChatGPT和其他AI工具的训练可能很快耗尽「全宇宙的文本」。


    换句话说,训练像ChatGPT这样的AI,将因数据量不够而受阻。




    这可能会影响生成式AI开发人员,在未来几年收集数据,以及训练人工智能的方式。


    同时,Russell认为人工智能将在「语言输入,语言输出」的工作中取代人类。


    数据不够,拿什么凑?



    Russell近来的预测引起了大家重点关注。


    OpenAI和其他生成式AI开发人员,为训练大型语言模型,开始进行数据收集。


    然而,ChatGPT和其他聊天机器人不可或缺的数据收集实践,正面临着越来越多的审查。




    其中就包括,未经个人同意情况下创意被使用,以及平台数据被自由使用感到不满的一些高管。


    但Russell的洞察力指向了另一个潜在的弱点:训练这些数据集的文本短缺。


    去年11月,MIT等研究人员进行的一项研究估计,机器学习数据集可能会在2026年之前耗尽所有「高质量语言数据」。




    论文地址:arxiv.org/pdf/2211.04…


    根据这项研究,「高质量」集中的语言数据来自:书籍、新闻文章、科学论文、维基百科和过滤后的网络内容等。


    而加持当红炸子鸡ChatGPT背后的模型GPT-4同样接受了大量优质文本的训练。


    这些数据来自公共在线的资源(包括数字新闻来源和社交媒体网站)


    从社交媒体网站「数据抓取」,才导致马斯克出手限制用户每天可以查看的推文数量。




    Russell表示,尽管许多报道未经证实,但都详细说明了OpenAI从私人来源购买了文本数据集。虽然这种购买行为可能存在解释,但自然而然的推断是,没有足够的高质量公共数据了。


    一直以来,OpenAI尚未公开GPT-4背后训练的数据。


    而现在,OpenAI需要用「私人数据」来补充其公共语言数据,以创建该公司迄今最强大、最先进的人工智能模型 GPT-4。


    足见,高质量数据确实不够用。


    OpenAI在发布前没有立即回复置评请求。


    OpenAI深陷数据风波



    近来,OpenAI遇上了大麻烦,原因都和数据有关。


    先是16人匿名起诉OpenAI及微软,并提交了长达157页的诉讼,声称他们使用了私人谈话和医疗记录等敏感数据。




    他们的索赔金额高达30亿美元,诉讼中指出,



    尽管制定了购买和使用个人信息的协议,但是OpenAI和微软系统性地从互联网中窃取了3000亿个单词,包括数百万未经同意获取的个人信息。



    这其中包含账户信息、姓名、联系方式、电子邮件、支付信息、交易记录、浏览器数据、社交媒体、聊天数据、cookie等等。


    这些信息被嵌入到ChatGPT中,但这些恰恰反映出个人爱好、观点、工作履历甚至家庭照片等。


    而负责这次起诉的律师事务所Clarkson,此前曾负责过数据泄露和虚假广告等问题的大规模集体诉讼。




    紧接着,这周又有几位全职作者提出,OpenAI未经允许使用了自己的小说训练ChatGPT,构成侵权。


    那么是如何确定使用自己小说训练的呢?


    证据就是,ChatGPT能够针对他们的书生成准确的摘要,这就足以说明这些书被当作数据来训练ChatGPT。


    作者Paul Tremblay和Mona Awad表示,「ChatGPT未经许可就从数千本书中拷贝数据,这侵犯了作者们的版权」。




    起诉书中预估,OpenAI的训练数据中至少包含30万本书,其中很多来自侵权网站。


    比如,GPT-3训练数据情况披露时,其中就包含2个互联网图书语料库,大概占比为15%。


    2位起诉的作者认为,这些数据就是来自一些免费的网址,比如Z-Library、Sci-Hub等。


    另外2018年,OpenAI曾透露训练GPT-1中的数据就包括了7000+本小说。起诉的人认为这些书没有获得作者认可就直接使用。


    另谋他法?



    不得不说,OpenAI使用数据来源一事确实存在诸多争议。


    今年2月,《华尔街日报》记者Francesco Marconi曾表示,新闻媒体的数据也被用来训练ChatGPT。


    Marconi让ChatGPT列了一个清单,竟有20家媒体。




    早在今年5月,Altman在接受采访时曾表示,OpenAI已经有一段时间没有使用付费客户数据来训练大语言模型了。



    客户显然不希望我们训练他们的数据,所以我们改变了计划,不再这么做。





    其实,OpenAI在3月初,曾悄然更新了服务条款。


    Altman提到,现在公司正在开发的新技术,可以使用更少的数据来训练模型。


    或许从OpenAI身上受到了启发,谷歌选择先行堵上这一漏洞。


    7月1日,谷歌更新了其隐私政策,现在的政策中明确谷歌有权收集任何公开可用的数据,并将其用于其人工智能模型的训练。




    谷歌向所有用户表明,只要是自己能够行公开渠道获得的内容,都可以拿来训练Bard以及未来的AI。


    参考资料:


    http://www.businessinsider.com/ai-could-ru…


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

    很佩服的一个Google大佬,离职了。。

    这两天,科技圈又有一个突发的爆款新闻相信不少同学都已经看到了。 那就是75岁的计算机科学家Geoffrey Hinton从谷歌离职了,从而引起了科技界的广泛关注和讨论。 而Hinton自己也证实了这一消息。 提到Geoffrey Hinton这个名字,对于...
    继续阅读 »

    这两天,科技圈又有一个突发的爆款新闻相信不少同学都已经看到了。


    那就是75岁的计算机科学家Geoffrey Hinton从谷歌离职了,从而引起了科技界的广泛关注和讨论。



    而Hinton自己也证实了这一消息。




    提到Geoffrey Hinton这个名字,对于一些了解过AI人工智能和机器学习等领域的同学来说,应该挺熟悉的。


    Hinton是一位享誉全球的人工智能专家,被誉为“神经网络之父”、“深度学习的鼻祖”、“人工智能教父”等等,在这个领域一直以来都是最受尊崇的泰斗之一。



    作为人工智能领域的先驱,他的工作和成就也对该领域的后续发展产生了深远的影响。




    其实算一下时间,距离Hinton 2013年加入谷歌,已经也有十个年头了。


    据报道,Hinton在4月份其实就提出了离职,并于后来直接与谷歌CEO劈柴哥(Sundar Pichai)进行了交谈。


    Hinton在接受媒体访谈时表示,他非常关注人工智能的风险,并表示对自己多年的工作和研究存在遗憾。


    正当大家都在好奇Hinton离职原因的时候,Hinton自己却表示,这样一来可以更加自由地讨论人工智能的风险。





    1947年,Geoffrey Hinton出生于英国温布尔登的一个知识分子家庭。



    他的父亲Howard Everest Hinton是一个研究甲壳虫的英国昆虫学家,而母亲Margaret Clark则是一名教师。


    除此之外,他的高曾祖父George Boole还是著名的逻辑学家,也是现代计算科学的基础布尔代数的发明人,而他的叔叔Colin Clark则是一个著名的经济学家。


    如此看来,Hinton家庭里的很多成员都在学术和研究方面都颇有造诣。




    Hinton主要从事神经网络和机器学习的研究,在AI领域做出过许多重要贡献,其中最著名的当属他在神经网络领域所做的研究工作。



    他在20世纪80年代就已经开启了反向传播算法(Back Propagation, BP算法)的研究,并将其应用于神经网络模型的训练中。这一算法被广泛应用于语音识别、图像识别和自然语言处理等领域。



    除此之外,Hinton还在卷积神经网络(Convolutional Neural Networks,CNN)、深度置信网络(Deep Belief Networks,DBN)、递归神经网络(Recursive Neural Networks,RNN)、胶囊网络(Capsule Network)等领域做出了重要贡献。




    2013年,Hinton加入Google,同时把机器学习相关的很多技术带进了谷歌,同时融合到谷歌的多项业务之中。



    2019年3月,ACM公布了2018年度的图灵奖得主。


    图灵奖大家都知道,是计算机领域的国际最高奖项,也被誉为“计算机界的诺贝尔奖”。


    而Hinton则与蒙特利尔大学计算机科学教授Yoshua Bengio和Meta首席AI科学家Yann LeCun一起因为研究神经网络而获得了该年度的图灵奖,以表彰他们在对应领域所做的杰出贡献。



    除此之外,Hinton在他的学术生涯中发表了数百篇论文,这些论文中提出了许多重要的理论和方法,涵盖了人工智能、机器学习、神经网络、计算机视觉等多个领域。


    而且他的论文被引用的次数也是惊人,这对于这些领域的研究和发展都产生了重要的影响。





    除了自身在机器学习方面的造诣很高,Hinton同时也是一个优秀的老师。


    Hinton带过很多大牛学生,其中不少都被像苹果、Google等这类硅谷科技巨头所挖走,在对应的公司里领导着人工智能相关领域的研究。


    这其中最典型的就是Ilya Sutskever,他是Hinton的学生,同时他也是最近大名鼎鼎的OpenAI公司的联合创始人和首席科学家。



    聊到这里,不得不感叹大佬们的创造力以及对这个领域所作出的贡献。


    既然离开了谷歌,那也就意味着将开启一段新的旅程,也期待着大佬后续给大家带来更多精彩的故事。


    好了,以上就是今天的文章内容,感谢大家的收看,我们下期见。


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

    软件开发者的自身修养

    关键词:工作任务、测试开发、孰能生巧、代码优化、团队开发 一、工作任务 ① 会议主题: 一般在日常的工作会议中,要总结和反思:我这周干了什么、我下周打算干什么、我遇到了什么问题 ② 编程是需要持续投入精力和注意力的智力活动。注意力是稀缺资源,类似于魔力点数。如...
    继续阅读 »

    关键词:工作任务、测试开发、孰能生巧、代码优化、团队开发


    一、工作任务


    会议主题:
    一般在日常的工作会议中,要总结和反思:我这周干了什么、我下周打算干什么、我遇到了什么问题


    编程是需要持续投入精力和注意力的智力活动。注意力是稀缺资源,类似于魔力点数。如果用光了自己的注意力点数,必须花一个小时或者更多的时间做不需要注意力的事情来补充它


    时间拆分:对于每天的工作时间可以参考番茄工作法策略进行时间拆分


    ④ 专业开发人员评估每个任务的优先级,排除个人的喜好和需要,按照真实紧急程度来执行任务


    小步快跑, 以防步履蹒跚


    ⑥ 专业开发人员会用心管理自己的时间和注意力


    需求预估是软件开发人员面对的最简单、也是最可怕的活动之一了


    ⑧ 业务方觉得预估就是承诺,开发方认为预估就是猜测。两者相差迥异


    ⑨ 需求承诺是必须做到的,是关于确定性的


    ⑩ 专业开发人员能够清楚区分预估和承诺。只有在确切知道可以完成的前提下,他们才会给出承诺


    ① 预估任务:达成共识,把大任务分成许多小任务,分开预估再加总,结果会比单独评估大任务要准确很多?这样做之所以能够提高准确度,是因为小任务的预估错误几乎可以忽略,不会对总得结果产生明显影响


    ② 对需要妥善对待的预估结果,专业开发人员会与团队的其他人协商,以取得共识


    二、测试开发


    ① 在工作中,有一种现象叫观察者效应,或者不确定原则。每次你向业务方展示一项功能,他们就获得了比之前更多的信息,这些新信息反过来又会影响他们对整个系统的看法


    ② 专业开发人员,也包括业务方必须确认,需求中没有任何不确定因素


    ③ 开发人员有责任把验收测试与系统联系起来,然后让这些测试通过


    ④ 请记住,身为专业开发人员,你的职责是协助团队开发出最棒的软件。也就是说,每个人都需要关心错误和疏忽,并协力改正


    单元测试是深入系统内部进行,调用特定类的方法;验收测试则是在系统外部,通常是在API或者UI级别进行


    QC:检验产品的质量,保证产品符合客户的需求,是产品质量检查者;QA:审计过程的质量,保证过程被正确执行,是过程质量审计者


    ⑦ 测试策略:单元测试、组件测试、集成测试、系统测试、探索式测试


    ⑧ 8小时其实非常短暂,只有480分钟,28800秒。身为专业的开发人员,你肯定希望能在这短暂的时间里尽可能高效的工作,取得尽可能多的成果


    ⑨ 再说一次,仔细管理自己的时间是你的责任


    三、孰能生巧


    调试时间和编码时间是一样昂贵的


    ② 管理延迟的诀窍,便是早期监测和保持透明。要根据目标定期衡量进度


    ③ 如果可怜的开发人员在压力之下最终屈服,同意尽力赶上截止日期,结局会十分悲惨。那些开发人员会开始抄近路,会额外加班加点工作,抱着创造奇迹的渺茫希望


    ④ 即使你的技能格外高超,也肯定能从另外一名程序员的思考与想法中获益


    测试代码之匹配于产品代码,就如抗体之匹配于抗原一样


    ⑥ 整洁的代码更易于理解,更易于修改,也更易于扩展。代码更简洁了,缺陷也更少了。整个代码库也会随之稳步改善,杜绝业界常见的放任代码劣化而视若不见的状况


    ⑦ 任何事情,只要想做得快,都离不开练习!无论是搏斗还是编程,速度都来源于练习!从练习中学到很多东西,深入了解解决问题的过程,进而掌握更多的方法,提升专业技能


    关于练习的职业道德职业程序员用自己的时间来练习。老板的职责不包括避免你的技术落伍,也不包括为你打造一份好看的履历


    ⑨ 东西画在纸上与真正做出来,是不一样的


    四、代码优化


    ① 好代码应该可扩展、易于维护、易于修改、读起来应该有散文的韵味……


    ② 在经济全球化时代,企业唯利是图,为提升股价而采用裁员、员工过劳和外包等方式,我遇到的这种缩减开发成本的手段,已经消解了高质量程序的存在价值和适宜了。只要一不小心,我们这些开发人员就可能会被要求、被指示或是被欺骗去花一半的时间写出两倍数量的代码


    ③ 客户所要的任何一项功能,一旦写起来,总是远比它开始时所说的要复杂许多


    ④ 很少有人会认真对待自己说的话,并且说到做到


    言必信,行必果


    ⑥ 如果感到疲劳或者心烦意乱,千万不要编码


    ⑦ 专业开发人员善于合理分配个人时间,以确保工作时间段中尽可能富有成效


    ⑧ 流态区:程序员在编写代码时会进入的一种意识高度专注但思维视野却会收拢到狭窄的状态


    创造性输出依赖于创造性输入


    五、团队开发


    ① 我认为自己是团队的一员,而非凌驾于团队之上


    ② 要勇于承担作为一名手艺人工程师所肩负的重大责任


    ③ 代码中难免会出现bug,但并不意味着你不用对它们负责;没人能写出完美的软件,但这并不表示你不用对不完美负责


    ④ 什么样的代码是有缺陷的呢?那些你没把握的代码都是


    ⑤ 我不是在建议,是在要求!你写的每一行代码都要测试,完毕!


    ⑥ 作为开发人员,你需要有个相对迅捷可靠的机制,以此判断所写的代码可否正常工作,并且不会干扰系统的其他部分


    编程是一种创造性活动,写代码是无中生有的创造过程,我们大胆地从混沌之中创建秩序


    ⑧ 他们各表异议相互说“不”,然后找到了双方都能接受的解决方案。他们的表现是专业的


    ⑨ 许诺“尝试”,意味着只要你再加把劲还是可以达成目标的


    作者:纯之风
    来源:juejin.cn/post/7273051203562143763
    收起阅读 »

    做项目和做产品的区别

    TL;DR: 产品最终目标是客观的,项目的最终目标是主观的,产品有长期价值,项目没有。 在这几年中,做过产品也做过项目,在做产品的团队做过项目,也在做项目的团队做过产品。做产品和做项目,看起来没有什么区别,毕竟做产品的团队也是按照项目来开发一个个功能,做项目...
    继续阅读 »

    TL;DR: 产品最终目标是客观的,项目的最终目标是主观的,产品有长期价值,项目没有。



    在这几年中,做过产品也做过项目,在做产品的团队做过项目,也在做项目的团队做过产品。做产品和做项目,看起来没有什么区别,毕竟做产品的团队也是按照项目来开发一个个功能,做项目的团队也是做一个产品交付出去,都是开发。但实际上,做产品和做项目的目标是完全不同的,开发者关注的重点也皆然不同,本文就聊一聊我以为的两者的区别。本文中的产品和项目背景都是IT行业,其他行业我不了解,以下依旧简称为产品和项目。


    产品


    在移动互联网如此发达的今天,每人每天都会接触到无数的产品,涵盖生活和工作的方方面面。这些产品都有一个统一的特征——发布后保持品牌,然后持续迭代。10年前我们用的是微信,今天我们用的还是微信,但是功能已经今非昔比,增长了数倍,但10年前的大多数功能依旧存在。微信已经是中国互联网里最克制的产品之一了,依旧增加了这么多功能,阿里系的APP一直被人诟病臃肿,本质上也是在不停的增加功能。很多2B的产品也是一样,只不过由于对用户有更强的控制力,2B的产品可以更干脆的舍弃一些老旧功能。


    可见,在保持现有功能的基础上不停迭代是产品的基本特征。一个产品自诞生之初开始,就开始了这个过程,直到这个产品即将死去,该过程才会停止。对于开发来说,很难有人预料到一个产品完整的生命周期,尤其是成功的产品。那么一个扩展性强、可维护性好、稳定性高的架构就会成为一个产品的核心价值,这样的架构往往需要经验丰富的开发人员才能设计出来。除了对业务的熟悉程度外,这种架构经验也是开发者在做产品时积累的核心价值。经过历练的开发者可以用之前的经验对类似的业务场景进行快速复制,少走很多弯路,所以之前程序员跳槽的涨薪普遍比较可观。


    产品通常会经历市场和直接使用者的检验,所以评价的标准也是相对客观的,比如产品的营收、盈利能力,用户反馈,增长率,等等。尽管2B的产品某些人会有明显权重更大的评价,比如老板之于钉钉,但一个产品的好坏还是很难由个别人说了算,这也是互联网公司大多靠数据驱动的原因。


    项目


    项目就不一样了,尽管项目做出来的也是一个产品,供客户使用,但是和真正的产品相比有很大的区别。常见的项目形态是甲乙方项目,对于广大的非互联网公司来说,这是更常见的开发形态,这个行业也囊括了众多的开发者。下面聊的项目就是这种甲乙方的项目。需要特别说明的是,这种项目并不一定就很low,也可以非常的高大上,甲乙方都是世界500强也多的是。


    项目一般有明确的周期,在项目周期结束后,项目会整体交给客户,所有权在客户。这样造成了很少有项目开发团队会对项目中做的产品做持续性维护,这在很大程度上削弱了好架构的意义。大部分项目开始时会对要做的事情有明确的预期,技术架构通常只需要满足这个预期即可。有一些项目客户可能会指定技术栈或者一些高屋建瓴的方案,但很少有客户会关心具体的实现方式。由于有明确的维护周期和相关利益者不重视,在项目中没什么人会关注扩展性、维护性这些东西,经验的价值更多体现在项目开始的技术选型上。这种环境对于想要变的更好的开发者是很不利的。


    除了生命周期和持续迭代的差异外,项目和产品另一个最大的区别在于评价体系。由于甲乙方关系的存在,甲方不可能让所有会使用项目产品的人都来评价好坏,这样一定会导致项目的失败,所以甲方就会选出几名利益相关方来负责项目的验收。由于人数有限,再加上负责验收的人可能并不使用项目开发的产品,对于乙方来说,与其做好项目,不如想办法直接搞定负责项目验收的人。毕竟,能不能合格验收实际上是这个几个人的主观评价,项目产品真正的使用者此时还不知道产品的存在。所以在项目中,开发的话语权就变的很小了,所谓的项目经理变成了最重要的人,对于产品来说本应该最重要的业务价值和技术实现,也变成了和甲方几位关键角色的关系。在这种情况下,开发和其他干活的很容易被压榨,因为他们不重要。


    另外,不要觉得上面说的这些对技术不屑一顾的事情只发生在不重视技术的中小型公司。之前说了,甲乙方可能都非常的高大上,技术水平也不低,但这些事情全部会照常发生,这是由利益关系决定的。


    谈谈外包


    最后,谈谈外包。虽然都叫外包,但人员外包和项目外包是完全不同的两种状态。人员外包多见于互联网大厂,这些人虽然关系在外包公司,但做的事情其实和大厂里面的基层员工差异不大,通常做一些难度不高,但重复性强的工作。虽然听起来无趣,但是这些人一般可以看到所在团队的完整代码库的,也可以和大厂员工一样,了解产品的整体设计,跟随产品迭代发展,只要自己愿意,可以很快的积累。前些年人才供不应求时,也有很多外包人员就地转正。虽然理论上这些人可以随时被换掉,但实际上只要不是这个人太差,加上负责的工作难度也不高,大部分团队都希望能有稳定的人员,所以这些人相对稳定,是相对不错的机会。


    另一种就是项目外包了,被外包公司派遣到各个项目上,按照人天收取甲方费用。这种外包工作对于开发者来说就很差了。首先,中国的甲方把乙方当人的少,工作环境普遍堪忧,中国的甲方又基本都希望在现场办公,方便监督,通常来说能有个像样的会议室就不错了。其次,由于真正明白的项目管理的项目经理很少,导致大部分项目实际上都是失控状态,到了中后期开始疲于奔命,最终熬夜加班,勉强交付。这对于开发者的心理和压力都不好,长时间在这种环境中的开发者,很难有精力和意愿把事情做好,更多是糊弄了事,然后开启恶性循环。第三,由于上面谈到的项目的特征,开发者很难在项目中得到积累,更缺少和项目一起成长的机会,大部分开发者都是重复最初1至2年的经验,然后不停的重复。即便有学习意愿的开发者,也缺乏学习的机会和环境,不知道方向在哪,最终丧失成长的机会。


    所以,除了脏简历这种功利的原因,在项目外包的开发者,最好能明确知道自己所处环境的问题,如果有继续前进的意愿,要知道做什么是对自己有用的,等待机会,改变处境。


    总结


    本文结合我之前的经历和看到的现象,对做产品和做项目的差异做了一些个人观点的总结。陈述了一些事实,抽象了一些具体的事例,较为深入的分析了产生差异的原因。这些东西很难改变,个人也没有必要去改变,没有好与坏,只是不同的实际情况。对个人来说,人的主观能动性还是很重要的,即便在做产品的团队,很多人也按照做项目的态度在干活,导致了很多产品扩展困难,难以为继。有的人虽然处在恶劣的环境和巨大的项目压力中,但依然可以每天自我成长,最终找到理想的工作机会。如果这篇文章能帮你看清一点自己的处境,那么我的这点经验就有了价值。


    作者:J_Wang
    来源:juejin.cn/post/7275945995622662183
    收起阅读 »

    产品:请给我实现一个在web端截屏的功能!

    web
    一、故事的开始 最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。 作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行! 我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的...
    继续阅读 »

    一、故事的开始


    最近产品又开始整活了,本来是毫无压力的一周,可以小摸一下鱼的,但是突然有一天跟我说要做一个在网页端截屏的功能。


    作为一个工作多年的前端,早已学会了尽可能避开麻烦的需求,只做增删改查就行!


    我立马开始了我的反驳,我的理由是市面上截屏的工具有很多的,微信截图、Snipaste都可以做到的,自己实现的话,一是比较麻烦,而是性能也不会很好,没有必要,把更多的时间放在核心业务更合理!


    结果产品跟我说因为公司内部有个可以用来解析图片,生成文本OCR的算法模型,web端需要支持截取网页中部分然后交给模型去训练,微信以及其他的截图工具虽然可以截图,但需要先保存到本地,再上传给模型才行。


    网页端支持截图后可以在在截屏的同时直接上传给模型,减少中间过程,提升业务效率。


    我一听这产品小嘴巴巴的说的还挺有道理,没有办法,只能接了这个需求,从此命运的齿轮开始转动,开始了我漫长而又曲折的思考。


    二、我的思考


    在实现任何需求的时候,我都会在自己的脑子中大概思考一下,评估一下它的难度如何。我发现web端常见的需求是在一张图片上截图,这个还是比较容易的,只需要准备一个canvas,然后利用canvas的方法 drawImage就可以截取这个图片的某个部分了。


    示例如下:


    <!DOCTYPE html>
    <html>
    <head>
    <title>截取图片部分示例</title>
    </head>
    <body>
    <canvas id="myCanvas" width="400" height="400"></canvas>
    <br>
    <button onclick="cropImage()">截取图片部分</button>
    <br>
    <img id="croppedImage" alt="截取的图片部分">
    <br>

    <script>
    function cropImage() {
    var canvas = document.getElementById('myCanvas');
    var ctx = canvas.getContext('2d');
    var image = new Image();

    image.onload = function () {
    // 在canvas上绘制整张图片
    ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

    // 截取图片的一部分,这里示例截取左上角的100x100像素区域
    var startX = 0;
    var startY = 0;
    var width = 100;
    var height = 100;
    var croppedData = ctx.getImageData(startX, startY, width, height);

    // 创建一个新的canvas用于显示截取的部分
    var croppedCanvas = document.createElement('canvas');
    croppedCanvas.width = width;
    croppedCanvas.height = height;
    var croppedCtx = croppedCanvas.getContext('2d');
    croppedCtx.putImageData(croppedData, 0, 0);

    // 将截取的部分显示在页面上
    var croppedImage = document.getElementById('croppedImage');
    croppedImage.src = croppedCanvas.toDataURL();
    };

    // 设置要加载的图片
    image.src = 'your_image.jpg'; // 替换成你要截取的图片的路径
    }
    </script>
    </body>
    </html>

    一、获取像素的思路


    但是目前的这个需求远不止这样简单,因为它的对象是整个document,需要在整个document上截取一部分,我思考了一下,其实假设如果浏览器为我们提供了一个api,能够获取到某个位置的像素信息就好了,这样我将选定的某个区域的每个像素信息获取到,然后在一个像素一个像素绘制到canvas上就好了。


    我本以为我发现了一个很好的方法,可遗憾的是经过调研浏览器并没有为我们提供类似获取某个位置像素信息的API。


    唯一为我们提供获取像素信息的是canvas的这个API。


    <!DOCTYPE html>
    <html>
    <head>
    <title>获取特定像素信息示例</title>
    </head>
    <body>
    <canvas id="myCanvas" width="400" height="400"></canvas>
    <br>
    <button onclick="getPixelInfo()">获取特定像素信息</button>
    <br>
    <div id="pixelInfo"></div>

    <script>
    function getPixelInfo() {
    var canvas = document.getElementById('myCanvas');
    var ctx = canvas.getContext('2d');

    // 绘制一些内容到canvas
    ctx.fillStyle = 'red';
    ctx.fillRect(50, 50, 100, 100);

    // 获取特定位置的像素信息
    var x = 75; // 替换为你想要获取的像素的x坐标
    var y = 75; // 替换为你想要获取的像素的y坐标
    var pixelData = ctx.getImageData(x, y, 1, 1).data;

    // 提取像素的颜色信息
    var red = pixelData[0];
    var green = pixelData[1];
    var blue = pixelData[2];
    var alpha = pixelData[3];

    // 将信息显示在页面上
    var pixelInfo = document.getElementById('pixelInfo');
    pixelInfo.innerHTML = '在位置 (' + x + ', ' + y + ') 的像素信息:<br>';
    pixelInfo.innerHTML += '红色 (R): ' + red + '<br>';
    pixelInfo.innerHTML += '绿色 (G): ' + green + '<br>';
    pixelInfo.innerHTML += '蓝色 (B): ' + blue + '<br>';
    pixelInfo.innerHTML += 'Alpha (透明度): ' + alpha + '<br>';
    }
    </script>
    </body>
    </html>


    浏览器之所以没有为我们提供相应的API获取像素信息,停下来想想也是有道理的,甚至是必要的,因为假设浏览器为我们提供了这个API,那么恶意程序就可以通过这个API,不断的获取你的浏览器页面像素信息,然后全部绘制出来。一旦你的浏览器运行这个段恶意程序,那么你在浏览器干的什么,它会一览无余,相当于在网络的世界里裸奔,毫无隐私可言。


    二、把DOM图片化


    既然不能走捷径直接拿取像素信息,那就得老老实实的把document转换为图片,然后调用canvas的drawImage这个方法来截取图片了。


    在前端领域其实99%的业务场景早已被之前的大佬们都实现过了,相应的轮子也很多。我问了一下chatGPT,它立马给我推荐了大名鼎鼎的html2canvas,这个库能够很好的将任意的dom转化为canvas。这个是它的官网。


    我会心一笑,因为这不就直接能够实现需求了,很容易就可以写出下面的代码了:


    html2canvas(document.body).then(function(canvas) {
    // 将 Canvas 转换为图片数据URL
    var src = canvas.toDataURL("image/png");
    var image = new Image();
    image.src = src;
    image.onload = ()=>{
    const canvas = document.createElement("canvas")
    const ctx = canvas.getContext("2d");
    const width = 100;
    const height = 100;
    canvas.width = width;
    canvas.height = height;
    // 截取以(10,10)为顶点,长为100,宽为100的区域
    ctx.drawImage(image, 10, 10, width, height , 0 , 0 ,width , height);
    }
    });


    上面这段代码就可以实现截取document的特定的某个区域,需求已经实现了,但是我看了一下这个html2canvas库的资源发现并没有那么简单,有两个点并不满足我希望实现的点:


    1.大小


    当我们将html2canvas引入我们的项目的时候,即便压缩过后,它的资源也有近200kb:


    Screen Shot 2023-09-09 at 3.15.10 PM.png


    要知道整个react和react-dom的包压缩过后也才不到150kb,因此在项目只为了一个单一的功能引入一个复杂的资源可能并不划算,引入一个复杂度高的包一个是它会增加构建的时间,另一方面也会增加打包之后的体积。


    如果是普通的web工程可能情有可原,但是因为我会将这需求做到插件当中,插件和普通的web不一样的一点,就是web工程如果更新之后,客户端是自动更新的。但是插件如果更新了,需要客户端手动的下载插件包,然后再在浏览器安装,因此包的大小尽可能小才好,如果一个插件好几十MB的话,那客户端肯定烦死了。


    2.性能


    作为业内知名的html2canvas库,性能方面表现如何呢?


    我们可以看看它的原理,一个dom结构是如何变成一个canvas的呢!


    它的源码在这里:核心的实现是canvas-renderer.ts这个文件。


    当html2canvas拿到dom结构之后,首先为了避免副作用给原dom造成了影响,它会克隆一份全新的dom,然后遍历DOM的每一个节点,将其扁平化,这个过程中会收集每个节点的样式信息,尤其是在界面上的布局的几何信息,存入一个栈中。


    然后再遍历栈中的每一个节点进行绘制,根据之前收集的样式信息进行绘制,就这样一点点的绘制到提前准备的和传入dom同样大小的canvas当中,由于针对很多特殊的元素,都需要处理它的绘制逻辑,比如iframe、input、img、svg等等。所以整个代码就比较多,自然大小就比较大了。


    整个过程其实需要至少3次对整个dom树的遍历才可以绘制出来一个canvas的实例。


    这个就是这个绘制类的主要实现方法:


    Screen Shot 2023-09-09 at 4.08.30 PM.png


    可以看到,它需要考虑的因素确实特别多,类似写这个浏览器的绘制引擎一样,特别复杂。


    要想解决以上的大小的瓶颈。


    第一个方案就是可以将这个资源动态加载,但是一旦动态加载就不能够在离线的环境下使用,在产品层面是不能接受的,因为大家可以想一想如果微信截图的功能在没有网络的时候就使用不了,这个肯定不正常,一般具备工具属性的功能应该尽可能可以做到离线使用,这样才好。


    因此相关的代码资源不能够动态加载。


    二、dom-to-image


    正当我不知道如何解决的时候,我发现另外了一个库dom-to-image,我发现它打包后的大小只有10kb左右,这其实已经一个很可以接受的体积了。这个是它的github主页。好奇的我想知道它是怎么做到只有这么小的体积就能够实现和html2canvas几乎同样的功能的呢?于是我就研究了一下它的实现。


    dom-to-image的实现利用了一个非常灵活的特性--image可以渲染svg


    我们可以复习一下img标签的src可以接受什么样的类型:这里是mdn的说明文档


    可以接受的格式要求是:



    如果我们使用svg格式来渲染图片就可以是这样的方式:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG</title>
    </head>
    <body>
    <h1>SVG示例</h1>
    <img src="example.svg" alt="SVG示例">
    </body>
    </html>


    但是也可以是这样的方式:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img id="svg-image" src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /></svg>" alt="SVG图像">
    </div>
    </body>
    </html>


    把svg的标签序列化之后直接放在src属性上,image也是可以成功解析的,只不过我们需要添加一个头部:data:image/svg+xml,


    令人兴奋的是,svg并不是只支持svg语法,也支持将其他的xml类型的语法比如html嵌入在其中。antv的x6组件中有非常多这样的应用例子,我给大家截图看一下:


    Screen Shot 2023-09-09 at 4.49.40 PM.png


    在svg中可以通过foreignObject这个标签来嵌套一些其他的xml语法,比如html等,有了这一特性,我们就可以把上面的例子改造一下:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img
    id="svg-image"
    src="data:image/svg+xml, <svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='black' stroke-width='2' fill='red' /><foreignObject>{ 中间可以放 dom序列化后的结果呀 }</foreignObject></svg>"
    alt="SVG图像"
    >

    </div>
    </body>
    </html>


    所以我们可以将dom序列化后的结构插到svg中,这不就天然的形成了一种dom->image的效果么?下面是演示的效果:


    <!DOCTYPE html>
    <html>
    <head>
    <title>渲染SVG字符串</title>
    </head>
    <body>
    <div id="render" style="width: 100px; height: 100px; background: red"></div>
    <br />
    <div id="svg-container">
    <!-- 这里是将SVG内容渲染到<img>标签中 -->
    <img id="svg-image" alt="SVG图像" />
    </div>

    <script>
    const perfix =
    "data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'>";
    const surfix = "</foreignObject></svg>";

    const render = document.getElementById("render");

    render.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");

    const string = new XMLSerializer()
    .serializeToString(render)
    .replace(/#/g, "%23")
    .replace(/\n/g, "%0A");

    const image = document.getElementById("svg-image");

    const src = perfix + string + surfix;

    console.log(src);

    image.src = src;
    </script>
    </body>
    </html>


    Screen Shot 2023-09-09 at 5.18.12 PM.png


    如果你将这个字符串直接通过浏览器打开,也是可以的,说明浏览器可以直接识别这种形式的媒体资源正确解析对应的资源:


    data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><foreignObject x='0' y='0' width='100%' height='100%'><div id="render" style="width: 100px; height: 100px; background: red" xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>

    实不相瞒这个就是dom-to-image的核心原理,性能肯定是不错的,因为它是调用浏览器底层的渲染器。


    通过这个dom-to-image我们可以很好的解决资源大小性能这两个瓶颈的点。


    三、优化


    这个库打包后的产物是umd规范的,并且是统一暴露出来的全局变量,因此不支持treeshaking。


    Screen Shot 2023-09-09 at 9.13.08 PM.png


    但是很多方法比如toJpeg、toBlob、等方法我们其实都用不到,所以打包了很多我们不需要的产物,于是其实我们可以把核心的实现自己写一遍,使用1-2kb的空间就可以做到这一点。


    经过以上的思考我们就可以基本上确定方案了:


    基于dom-to-image的原理,实现一个简易的my-dom-to-image,大约只需要100行代码左右就可以做到。


    然后将document.body转化为image,再从这个image中截取特定的部分。


    Screen Shot 2023-09-09 at 9.23.06 PM.png


    好了,以上就是我关于这个需求的一些思考,如果掘友也有一些其他非常有意思的需求,欢迎评论区讨论我们一起头脑风暴啊!!!


    四、最后的话


    以下是我的其他文章,欢迎掘友们阅读哦!


    保姆级讲解JS精度丢失问题(图文结合)


    shell、bash、zsh、powershell、gitbash、cmd这些到底都是啥?


    从0到1开发一个浏览器插件(通俗易懂)


    用零碎时间个人建站(200+赞)


    另外我有一个自己的网站,欢迎来看看 new-story.cn


    创作不易,如果您觉得文章有任何帮助到您的地方,或者触碰到了自己的知识盲区,请帮我点赞收藏一下,或者关注我,我会产出更多高质量文章,最后感谢您的阅读,祝愿大家越来越好。


    作者:Story
    来源:juejin.cn/post/7276694924137463842
    收起阅读 »

    一个简单的TODO,原来这么好用

    平常我们再开发的时候,遇到一些想要之后去编写的部分,或者说再开发某个模块的时候,突然被事情打断,暂时无法实现的代码,以后才会去修复的bug的时候,要如何精准快速的去定位到那个位置呢? 下面来介绍一个很多人会忽律的标记TODO TODO是一个特殊的标记,用于标识...
    继续阅读 »

    平常我们再开发的时候,遇到一些想要之后去编写的部分,或者说再开发某个模块的时候,突然被事情打断,暂时无法实现的代码,以后才会去修复的bug的时候,要如何精准快速的去定位到那个位置呢?


    下面来介绍一个很多人会忽律的标记TODO


    TODO是一个特殊的标记,用于标识需要实现但目前还未实现的功能。这是一个Javadoc的标签,因此它只能应用于类、接口和方法。


    它可以帮助我们跟踪和管理开发中的待办事项。


    使用方法


    首先看一个最基本的使用方法


    @RestController
    public class TestController {

    @GetMapping("/hello")
    public String hello(){
    //TODO do something
    return "Hello World";
    }
    }

    这里我们加上TODO。之后再需要去进行修改的时候。


    直接去搜索就可以了


    image-20230906195743692


    除了这个方法,还有很多隐藏的方法


    进入设置


    image-20230906195949934


    这里就可以自定义todo了


    如果是团队协作的话,每个人可以自定义其他的todo类型。


    也可以用自己喜欢的更加醒目的颜色


    image-20230906200230765


    同时也可以在idea中进行全局的todo查看


    image-20230906200444351


    除了这个之外,还有过滤器,可以进行自定义的todo类型


    image-20230906200527489


    阿里巴巴Java开发手册中对TODO的规范标注主要有以下两点:



    1. TODO:表示需要实现,但目前还未实现的功能。这个标记通常用于类、接口和方法中。

    2. FIXME:标记某代码是错误的,而且不能工作,需要及时纠正的情况。


    最佳实践


    编写一个代码模板


    image-20230906201219291


    image-20230906201810835


    这样,就是一个最佳的实战了。


    作者:小u
    来源:juejin.cn/post/7276696131113959458
    收起阅读 »

    谈谈干前端四年的几点感受

    19年毕业的我,最开始怀揣着无限憧憬进入这个行业 不知不觉,已经工作4年了,如果算上大四实习的时光,也接近5年了。 4年间换了两家公司。 对于工作,我也有过很多的困扰和迷茫,现在依旧在走一步看一步的状态。 或许一觉起来工作没了,都是概率事件。 为什么会有这篇文...
    继续阅读 »

    19年毕业的我,最开始怀揣着无限憧憬进入这个行业


    不知不觉,已经工作4年了,如果算上大四实习的时光,也接近5年了。


    4年间换了两家公司。


    对于工作,我也有过很多的困扰和迷茫,现在依旧在走一步看一步的状态。


    或许一觉起来工作没了,都是概率事件。


    为什么会有这篇文章?


    一是与行业内大佬山月交流了一次,解惑答疑,有所感悟,想记录下心中所想;


    二是呼应一年前的文章《谈谈干前端三年的几点感受》,对比看看自己想法变化。




    前端在一个公司定位


    一年前我说,前端在公司的定位是必要不重要,现在的想法依旧不变,只是对象变了。


    整个技术开发人员,在一个公司的定位都是处于必要但不重要的角色,可替代性非常高。


    可替代的属性越高,价值属性便越低。


    也许局部或者短期看,技术开发的薪资是高的,但这对于公司来说,是成本。


    如果公司要降本增效,最先压榨的也是这部分人员。


    有这样一种说法,“技术傍身,编程改变世界”等等,其实是有些误导人的。


    重要的从来都是想法,是渠道,不是技术。


    只要能想到,大概率都能实现,实现不了就加班想办法实现。


    能够提出想法的人,才处于一个公司重要的地位。


    就正如我看到的,一个公司,核心业务人员离职,公司上下极力挽留,一个开发离职,领导回复祝好。




    我们要学些什么


    学有价值的东西。


    何谓价值,价值就是经过时间考验,依旧不变的东西。


    我个人极其反对花太多精力深入研究各个技术的源码。


    技术说到底是工具,工具最重要的使用,不是本身,而且只要是工具,便都有替代品,


    有新的技术,又会有新的源码,学是学不完的。


    而计算机行业,不变的什么,有价值的是什么?


    是计算机网络,是计算机组成原理,是数据结构与网络,是操作系统,是信息安全,是项目管理,是软件测试。


    以上都是大学中计算机类的专业课程,这些年没有变过。


    具体一点,与前端不变的什么,有价值的是什么?


    是网络请求,是nginx,是性能优化,是前端工程化,是脚手架,是对UI的基本审美。


    当然了,如果做可视化,音视频,跨端方向等等也有属于自己的专业壁垒。


    以上我提到的都属于目前自己看到,前端通用知识。




    如何评价自己的薪资和技术水平


    其实,我们学到的99%的知识是无用的,或者学完不用就忘记了。


    我学习的目的很单纯,就是为了跳槽涨薪。


    让自己的实力能够匹配和市场对我的要求和我自己期望的薪资。


    那么,如何得知自己的薪资水平,是否符合年限,技术水平,是否符合市场要求?


    需要比较。


    我们常说,人要和自己比,不要和别人比,人别人,气死人。但其实这是句自我安慰的鸡汤,不是生活的真相。


    人得自知,不比较,如何自知呢?


    当然,这种事情很难和同事交流,也不建议问同行。


    问同事,同事水平参差不齐,给不了你准确的答案。


    问同行,同行也许自己也发展不顺,多半是同病相怜,或是能给你方向,但给不了方案。


    我的建议是做付费咨询,向行业内大佬求助。


    同行业过来人的经验,更靠谱一些,做不到感同身受,但能明白心中所想。


    我想看到这篇文章的各位,都关注过几位技术大佬,那就去主动搭讪,说明来意,付费咨询。


    “我的薪资目前和我的工作年限匹配吗?我的技术还应该补充哪部分?我应该如何学习某些知识,有没有推荐的学习路线和文章,等等之类的问题”


    而付费是获取能够心安理得的咨询,不要计较那一顿火锅的钱。


    但其实只要搭讪成功,大佬一般都不会收费。




    2023年了,还要不要往大厂努力?


    当然要啊,这个想法和一年前比,没有动摇。


    但是大厂今年都在降本增效,门槛更高了,面试更难了,工作更卷了。


    但这并不能成为放弃的借口。无论结果与否,人总得有个工作上的目标啊。


    正如我的前同事今年初送给我的一句话,


    “备考公务员或者向大厂努力,总得找个目标,找件事情去做吧。如果觉得大厂太卷,那就干一年就走,但这个经历会成为你永久的财富。”




    前端已死?


    今年上半年受chatGPT冲击,这个言论甚嚣尘上。


    我这里不讨论死不死的事情,只觉得这个问题很荒谬,多思考思考就会明白这句话,在创造概念,制造焦虑。


    仔细想想,这波言论最大的受益方还是 做职业教育的那帮人。




    拿多少钱干多少活还是干多少活拿多少钱?


    第一家公司一切都很好。


    我还是义无反顾的离开了,离开后公司发展得更好了。


    离职的直接原因就是,当时要前端使用uni-app做跨端应用,去替换客户端的工作。


    这项工作的直接影响就是,整个公司只前端部门加班,我疲惫不堪的同时,uni-app踩不完的坑,也身心俱疲。


    每当加班到很晚时,委屈总是涌上心头。


    受不了之时,就只剩一个走字。


    这时候,小兵心态就出现了,拿多少钱多少活,我就拿这点工资,整这么多活,我无法承担,只能摆烂了。


    当然,也有领导心态,你得先努力干,干出成果,我拿着成果才去争取涨薪。


    这中间就有一个认知偏差,双方因为角度不同,无法理解对方的心态和想法。


    领导觉得小兵不懂他的良苦用心,小兵觉得领导天天画饼。


    哪种做法是对的呢?


    得就事论事。


    如果这件事情,对你有成长,有帮助,比如做一些工程化,脚手架,性能优化的工作,肯定得先干出成果。


    如果这件事情,对自己是一种消耗,那还是持小兵心态吧。


    如何区分这件事情是对你的帮助还是对自己的消耗呢?


    其实自己最清楚。


    如果干这项工作时,总是充满期待,充满激情,加班也无怨无悔,那就是帮助。


    如果干这项工作时,总是身心俱疲,牢骚满腹,加班会委屈抱怨,那就是消耗。




    人都是不愿意被管理的


    这句话出自山月,我听后豁然开朗。


    今年听闻行业内很多公司严抓考勤,多了很多制度和会议,吐槽随处可见。


    新的领导,势必会带来新的管理制度,新的实施方案。


    人都是不愿意被管理的,所以会引起各种不适应,但是一般一个月后都会销声匿迹,因为已经适应了。


    无法评价这些变化的好与坏。


    身处其中的我们只有慢慢适应,打工到哪里都一样,只要被管理着,都需要面临不同的问题。




    最后


    其实,回顾毕业这些年,19年谣传资本寒冬,然后是防疫三年,到后来前端已死,到现在无法言状的行业颓势。


    正应了那句话,“今年是过去十年最差的一年,却可能是未来十年最好的一年。”


    然后呢,这句话想表达什么?仅仅是传播了一个情绪,放在近些年都受用。


    我想说,


    “大环境的整体劣势,不影响个人的局部优势,我们要想办法建立这种个人优势;”


    “种一棵树最好的时间是十年前,其次是现在。”


    作者:虎妞先生
    来源:juejin.cn/post/7258509816691834917
    收起阅读 »

    突然发现,前端好像没几个做到 CTO 的……

    web
    大家好,我 ssh,这几天,在推上看到了一个节奏,swyx 小哥发了一篇关于前端天花板的讨论,吸引了 150w 左右的阅读。主要是在讲前端天花板,前端人员被集中捆绑在低级别工程师这个行列中,通往 VP(技术副总裁) 或者 CTO 的大门是朝他们关上的。 而他...
    继续阅读 »

    大家好,我 ssh,这几天,在推上看到了一个节奏,swyx 小哥发了一篇关于前端天花板的讨论,吸引了 150w 左右的阅读。主要是在讲前端天花板,前端人员被集中捆绑在低级别工程师这个行列中,通往 VP(技术副总裁) 或者 CTO 的大门是朝他们关上的。


    微信图片_20230726161930.jpg


    而他发出这篇推文的起因,正是 swyx 正文里配的这篇文章截图:



    经过我查询,这是 honeycomb.io 的一篇博客 成为工程 VP 里的一段话。他们没有刻意贬低前端工程师,只是客观的描述了统计情况而已,这反而是更加令人悲观的。


    其实这个问题我也不止一次想过,尤其是有一些校招的同学特别喜欢思考这个问题,之前一次校招的宣讲会后答疑环节,也有不止一个同学过来问我这个问题。


    确实,仔细想想,国内的前端界比较出名的前端出身做到很高职位的,玉伯算是一个代表,后期他基本上已经成为一个产品设计方面的负责人了,脱离了单纯前端的范畴。主导设计了云凤蝶、语雀这些非常 nb 的产品。


    image.png


    image.png


    但是除了玉伯之外,让我们仔细想想,是不是大概率情况下,前端升到更高级别负责人的概率比后端要低很多呢?第一印象是如此,而且我以前在阿里没有隐藏职位的时候,在钉钉上直接搜索 title 来确认过这个问题。


    在阿里,资深前端专家则对应前端的 p9,资深技术专家对应后端的 p9,这两个职位的人数在我印象里是相差很悬殊的,很多倍的关系…… 而且我记得 p9 的前端非常稀少。这其实也侧面反应出大家的主观感受是确有其事的。


    写到这里,我深感焦虑,赶紧去问问万能的 AI:


    ai.sb


    卧槽,被辱骂了一通。拿出我大哥 Dan 也没用!


    回到正题,swyx 又提到,有人说只要成为全栈就好了。



    直接看看这张图:



    全栈并不是口头说说那么简单,有一个小型公司的 CTO 也现身说出了自己的看法:


    image.png


    后端普遍认为前端简单,在国外也一样



    前端成为产品总负责人,比成为技术 vp 的路径要概率更大一些,这也符合玉伯的发展路径:


    image.png


    关于这件事儿,Hacker News 也有一些讨论,不过质量比较差,走偏了:


    开始讨论后端的烂代码了


    讨论男女平等


    我的看法


    看完了几乎全部的讨论以后,我感觉国外的开发者对于前端天花板的看法和国内差不多,确实是认为有后端工程背景的人升为 VP/CTO 级别的概率比较高,而前端更倾向于在框架中日复一日的迷失。


    以我自己的职业经历来说,假设我在使用 React 技术栈,今天在用 redux,明天出了一个 redux-toolkit 来解决 redux 太烂的问题,你迁移过去了,学到了很多范式很充实。再过几个月,又来了个 recoil,又来个 jotai。好像在很忙碌的学习,但其实都没有脱离状态管理的范畴,就像是被困在小学里反复的读五年级,而后端的人可能去研究更广阔的东西了。比如:




    1. 稳定性:各种灾备方案,限流等操作。




    2. 高并发:延迟,tps。




    3. 一致性:数据正确性。




    而前端比较好的处境,就是在一家前端主导产品的公司(比如最近比较火的 AffiNE)参与核心功能的研发,那么可以接触到前端比较深入的一些技术,而且有一帮大牛同事可以陪你玩最新的技术栈。又或者是参与到大型公司的基础架构建设,我了解到的比如性能监控、低代码搭建、serveless 建设、自研 JS 引擎、自研 Rust 编译库,也可以获得比较深入的技术提升。


    不过,大部分人的整个职业生涯可能都在做一些 Vue 或者 React 的应用开发,后台管理系统、活动页等等。。。是不是就完了?人生没希望了?


    再问问 AI:



    我丢,这 AI 吃枪药了吧。


    不过他骂的也不无道理,安心做个平庸的前端又怎么样呢?比起很多职业来说,坐在电脑前敲敲你喜欢的代码,当个快乐的小前端,拿个 10-20k 的薪资,不够过日子的嘛?想想土木老哥在烈日下的样子?



    我对于平庸人生的看法,把注意力转移到自己的生活中,有一个可以坚持热爱的爱好(比如我自己就喜欢踢足球和健身)。做一个自信阳光的小骚年,不是也很不错吗?


    不要高杠杆买房,不要负债太多,保持一定的积蓄习惯,注意资产的合理配置。你肯定能比一般职业的人过得更好,欲望才是万恶之源。


    当然,这只是比较悲观的想法,如果你有一颗上进的心,拼到个资深工程师,有点管理能力的话,再争取个前端小 leader 当当,过上小资点的生活也没问题。


    我的意思是,人生短短几十年,职业不是生活的全部。假设你全心全意拼在工作上,到了 40 岁挣了一堆钱,落了一身的病。你觉得你真的快乐吗?如果钱是快乐的全部的话,李玟也不会得抑郁症,张朝阳也不会因为抑郁放弃公司管理跑去修行了。


    总结


    前端确实天花板比较低,不过那又咋样呢?最终能成为 VP 的人也没几个,如果你从小就就是天之骄子,目标是星辰大海,那你考上 985 的计算机系应该没什么问题,在校招的时候就果断选后端吧,确实有几率爬的更高点,但是付出相应的代价也是必要的(后端头发平均值明显低于前端)。


    屏幕截图 2023-07-29 052610.jpg


    否则,你就做个快乐的小前端,也比其他大多数职业过得舒服。


    作者:ssh_晨曦时梦见兮
    来源:juejin.cn/post/7261807670746513463
    收起阅读 »

    手机网站支付(在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
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »