注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

在我硬盘安监控了?纯 JS 监听本地文件的一举一动

web
💰 点进来就是赚到知识点!本文带你用 JS 代码监控本地文件,点赞、收藏、评论更能促进消化吸收! 🚀 想解锁更多 Web 文件系统的技能吗?快来订阅专栏「Web 玩转文件操作」! 📣 我是 Jax,在畅游 Web 技术海洋的又一年,我仍然是坚定不移的 Java...
继续阅读 »

💰 点进来就是赚到知识点!本文带你用 JS 代码监控本地文件点赞收藏评论更能促进消化吸收!


🚀 想解锁更多 Web 文件系统的技能吗?快来订阅专栏「Web 玩转文件操作」!


📣 我是 Jax,在畅游 Web 技术海洋的又一年,我仍然是坚定不移的 JavaScript 迷弟,Web 技术带给我太多乐趣。如果你也和我一样,欢迎关注私聊



开门见 demo


先来玩玩这个 demo —— 在 Chrome 中监控本地文件夹


20241005-220737.gif


在上面的 demo 中,点击按钮选择一个本地文件夹后,无论是在该文件夹中新增、修改还是删除内容,网页端都能够感知到每一步操作的细节,包括操作时间、操作对象是文件还是文件夹、具体执行了什么操作等等。



如果你的感觉是:”哟?有点儿意思!“ 那么这篇文章就是专门为你而写的,读下去吧。



本专栏的前几篇文章中,我们已经知道,Web 应用能对本地文件进行各种花式操作,例如选择文件/文件夹、增/删/改/查文件等等。网页好像能伸出长长的手臂,穿过浏览器触摸到了用户的本地文件。但你可能还不知道,网页也能长出千里眼、顺风耳,本地文件有什么风吹草动,都能被网页端监控到。如此灵通的耳目,它的名字就是 File System Observer API(文件系统观察者)。


API 简介


现在想象我们要开发一个 Web 端相册应用,展示用户本地文件夹中的图片。我们希望这个相册能实时响应用户的操作,例如增加/删掉几张图片后,无需用户手动在 Web 端刷新,就能自动更新到最新状态。


如果请你来实现自动刷新,阁下又该如何应对?


经典思路可能会是以短时间间隔轮询文件夹状态,读取并缓存每个文件的 lastModified 时间戳,如果前后两次轮询的时间戳发生了变化,再把前后差异更新到 Web 视图中。这种实现方式能达到效果,但还是有一些缺点,比如不能真正做到即时响应,且会有很大的性能问题等。


其实咱们都知道,最优雅高效的做法是仅在文件被操作时触发更新。原生操作系统如 WIndows 和 MacOS 都有这样的文件监听机制,但显然目前 Web 端还无法享受其便利性。除了在用户端,Node.js 应用也面临这样的问题。开发者苦此久矣。


直到 2023 年 6 月,来自谷歌的贡献者们开始推进一项 W3C 提案 —— File System Observer(为方便叙述,下文将简称其为 FSO),旨在从浏览器层面向 Web 应用提供跨平台的文件监听支持。如果这项提案能够顺利进入 ECMAScript 标准,那么 Web 文件系统的又一块重要功能版图将得以补全,Web 生态将会变得更友好、更强大。


解锁尝鲜:加入 Origin Trial


FSO 还是一套崭新的 API,有多新呢?MDN 和 CanIUse 中还没有建立关于它的词条。但这并不意味着我们完全无法用于生产环境 —— 正如你在本文开头的 demo 中体验到的,我已经用到线上功能中了。只要做一点配置工作,你和你的用户就能成为全球第一批享受到 FSO 的人 😎。


Chrome 已经对 FSO 开启了试用,版本范围是 129 到 134,你可以为你的 Web App 域名注册一个试用 token,你可以跟着我一步一步操作:


首先我们访问 developer.chrome.com/origintrial… 并登录账号。


20241006-163407.jpeg


点击界面下方的「REGISTER」按钮,进入表单页:


20241006-163919.jpeg


按照上图的标注填写信息。每一个域名都需要单独注册一次。例如我本地开发调试时用的是localhost:3000,而线上域名是 rejax.fun,那么就需要给这两个域名分别走一遍 REGISTER 流程。


填写信息后提交表单,你会得到一串字符串 token:


20241006-164840.jpeg


将 token 复制出来,写到 Web App 的 html 文件中,像这样:


<meta http-equiv="origin-trial" content="把 token 粘贴到这里" />

或者用 JavaScript 动态插入:


const meta = document.createElement('meta')
meta.httpEquiv = 'origin-trial'
meta.content = token
document.head.appendChild(meta)

最后,在 Chrome 中打开你注册的域名所在的页面,在 Console 中输入 FileSystemObserver 并回车:


20241006-165520.jpeg


如果打印出了「native code」而不是「undefined」,那么恭喜,你已经成功解锁了 FSO 试用!


监听一个文件


有了试用资格,我们来监听一个文件,边调试代码边研究 FSO 的设计和实现。


实例化


上一小节的最后,我们用来测试是否解锁成功的 FileSystemObserver 就是 FSO 的构造函数,它接收一个回调函数作为参数。我们可以像这样实例化一个观察者:


function callback (params) {
console.log(params)
}
const observer = new FileSystemObserver(callback)

callback 函数会在被监听的文件发生变动时被执行,所以我们可以把响应变动的业务处理逻辑放在其中。


绑定目标文件


实例 observer 有一个 observe 方法,它接收两个参数。第二个参数暂且按下不表,我们先专心看第一个参数。


这个参数是一个 FileSystemHandle 格式的对象,代表着本地文件在 JavaScript 运行时中的入口。我们可以通过 showOpenFilePicker 来选择一个文件(假如我们选择了文件 a.js),并获取到对应的 FileSystemHandle


const [fileHandle] = await window.showOpenFilePicker()
observer.observe(fileHandle)


如果你想看 FileSystemHandleshowOpenFilePicker 的详解,可以移步至本专栏的上一篇文章谁也别拦我们,网页里直接增删改查本地文件!



调用 observe 方法后,这个文件就算是进入了我们的监控区域 📸 了,直到我们主动解除监听或者网页被关闭/刷新。


监听文件操作


当我们编辑文件 a.js 的内容时,给 observe() 传入的回调函数被调用,并且会接收到两个参数,第一个是本次的变动记录 records,第二个是实例 observer 本身。我们打印 records 可以看到如下结构:


20241006-201248.jpeg


records 是一个数组,其元素是 FileSystemChangeRecord 类型的对象,我们重点关注以下几个属性:



  • changedHandle:可以理解为这就是我们绑定的文件。

  • type:变动类型,可取值及对应含义如下:


    type 值含义
    appeared新建文件,或者移入被监听的根目录
    disappeared文件被删除,或者移出被监听的根目录
    modified文件内容被编辑
    moved文件被移动
    unknown未知类型
    errored出现报错



一般情况下,如果我们监听的是单个文件而不是一个目录,那么无论是把文件移走、重命名、删除, record 中的 type 值都会是 disappeared。


监听一个文件夹


监听文件夹的方式和监听文件类似,我们先用 showDirectoryPicker 选择一个文件夹(以文件夹 foo 为例),再把 DirectoryHandle 传入 observe 方法。



为方便描述,我们假设文件夹 foo 的结构如下:


/foo


├── 文件夹 dir1


├── 文件夹 **dir2**


└── 文件 a.js



const dirHandle = await window.showDirectoryPicker()
observer.observe(dirHandle)

与文件有所不同的是,文件夹会有子文件夹和子文件,这是一个树形结构。如果我们只想监听 foo 下面的一级子内容,那么使用像上方代码块那样的调用方式就可以了。但如果我们想密切掌控每一子级的变动,就需要额外的配置参数,也就是前文提到的第二个参数:


observer.observe(dirHandle, {
recursive: true
})

此时你可以在 foo 文件夹里面任意增、删、改子文件或文件夹,一切操作都能在回调函数里以 record 的形式被捕获到。子文件和子文件夹所支持的操作类型,record 值也具有相同结构,因此接下来我们从监听子文件的视角来观察 FSO。


监听子文件


创建和移入、删除和移出 a.js 的情况,record.type 的值分布如下:


文件移入 foo在 foo 中创建文件文件从 foo 中移出删除文件
appearedappeareddisappeareddisappeared

其中移出和删除的表现,与监听单文件的情况是相同的。


我们来试试把 a.js 移到与它同级的文件夹 dir1 中,看看会得到怎样的 record


20241006-211746.jpeg


有几个点值得我们注意:



  • type 的值是 moved,说明只要 a.js 还在 foo 内,不管处于第几层,都不会触发 type: appeared/disappeared

  • relativePathMovedFrom 是一个单元素数组,它代表移动前 a.js 的文件路径

  • relativePathComponents 有两个数组元素,代表被移动文件的新路径是 dir1/a.js


但重命名子文件和监听单文件时不同。例如我们将 a.js 更名为 b.js,会监听到如下 record


20241006-210550.jpeg


我们本以为 type 的值是 renamed,但其实是 moved,确实有点反直觉。从 record 上来看,与真正的移动操作相比,重命名的不同之处在于:



  • changedHandle 指向了重命名后的新文件 b.js

  • relativePathMovedFromrelativePathComponents 分别包含的是旧名和新名


FSO 在状态设计上并没有直接定义一个重命名状态,但我们可以自己来区分。重命名的响应数据有这样的特征:



  • relativePathMovedFromrelativePathComponents 这两个数组的 length 一定相等

  • 除了最后一个元素,两个数组的其他元素一定是一一对应相等的


因此我们可以这样判断重命名操作:


const { oldList: relativePathMovedFrom, newList: relativePathComponents } = recors[0]
let operation = '是常规的移动操作'
// 重命名前后,文件的目录路径没变,只是文件名变了
if (oldList.length === newList.length) {
const len = newList.length
for (let i = 0; i < len; i++) {
// 相同序号的新旧路径是否一样
const isEqual = newList[i] === oldList[i]
if (i < len - 1) {
if (!isEqual) break
} else if (!isEqual) {
operation = '是重命名操作,不是移动操作'
}
}
}

至此,我们已经摸清了如何监听子文件上的不同操作,除了监听单文件部分已经覆盖到的内容,增量知识点仅有移动和重命名这两块。


监听子文件夹


对子文件夹的操作,也不外乎新建、删除、移动、重命名,和子文件在逻辑上基本一致,我们可以直接复用子文件的监听逻辑,再加上用 record.changedHandle.kind === ‘directory’ 来判断是否是文件夹即可。


解除监听


当我们想主动解除对文件或文件夹的监听时,只需要调用对应 observerdisconnect 即可:


observer.disconnect()

结语


恭喜你读完了本文,你真棒!


这一次,我们勇敢地品尝了一只新鲜生猛的螃蟹,对 File System Observer API 进行了较为深入的理解和实践。如果你之前一直苦于 JS 无法监听文件,无法带给用户完备的功能和极致的体验,那么从现在开始,你可以开始着手准备升级你的 Web App 了!


这套船新版本的 API 有力地补齐了 Web 文件系统 API 的短板,增强了 Web App 的实现能力,提升了开发者和用户的体验。它还在不断修改完善中,非常需要我们开发者积极参与到标准的制定中来,让 Web 技术栈变得更高效、更易用!


作者:JaxNext
来源:juejin.cn/post/7422275840069615652
收起阅读 »

如何实现一个稳如老狗的 websocket?

web
前言 彦祖们,前端开发中对于 setTimeout setInterval 一定用得烂熟于心了吧? 但你知道你的定时器并没那么靠谱吗? 本文涉及技术栈(非必要) vue2 场景复现 今天笔者在开发业务的时候就遇到了这样一个场景 前后端有一个 ws 通道,我...
继续阅读 »

前言


彦祖们,前端开发中对于 setTimeout setInterval 一定用得烂熟于心了吧?


但你知道你的定时器并没那么靠谱吗?


本文涉及技术栈(非必要)



  • vue2


场景复现


今天笔者在开发业务的时候就遇到了这样一个场景


前后端有一个 ws 通道,我们暂且命名为 channel


前后端约定如下:



  1. 前端每隔 5000ms 发送一个 ping 消息

  2. 后端收到 ping 后回复一个 pong 消息

  3. 后端如果 15000ms 未收到 ping,则视为 channel 失活,直接 kill

  4. kill 后前端会主动发起重连


文章还没写两分钟,一只暴躁的测试老哥说道:"你们的 ws 也太不稳定了,几十秒就断开一次?废物?"


骂骂咧咧的甩过来一张截图


image.png


笔者心想:"为什么我的界面稳如老狗?浏览器问题,绝对是浏览器问题..."


起身查看,遂发现毫无问题,和笔者一模一样的 chrome 版本...


静心而坐,对着浏览器屏幕茶颜悦色(哦,察言观色)...


10 分钟过去了,半小时过去了...还是稳如老狗,根本不断


image.png


问题分析


那么问题到底出在哪里呢?


笔者坐在测试妹纸身边仔细观察了她的操作后!


发现她不断得切屏,此时已初步心虚,不禁问道 GPT



当浏览器标签页变为非活动状态时,setIntervalsetTimeout 的执行频率通常会被降级。大多数现代浏览器将其执行频率限制在 1 秒(1000 毫秒)或更高,以减少 CPU 和电池的消耗。



问题原因大致是这样了


问题复现


此时笔者在本地写了个 demo


let prev = performance.now()

setInterval(()=>{
const offset = performance.now() - prev
prev = performance.now()
console.log('__SY__🎄 ~ setInterval ~ offset:', offset)

},1000)

理想的情况,我们这个 offset 应该是一直维持在 1000ms 左右


那么后续我们就要看页面激活 | 失活时候的情况了


页面激活时


我们先看下页面激活时的打印数据


image.png


没什么问题,符合我们的期望值


页面失活时


接下来我们,切换到其他浏览器标签,保持几分钟,几分钟后我们看下打印数据


image.png


明显发现有些数据不符合我们的期望值


甚至有些夸张到长达 41003ms,将近 40 倍,不靠谱!


寻找方案


用 setTimeout 模拟 setInterval


其实网上最多的方案就是说用 setTimeout 模拟 setInterval


但是很可惜,笔者亲自模拟下来,也是同样的结果,我们看截图


image.png


而且发现更加不靠谱了...错误的概率明显更高了...


其实可想而知,setIntervalsetTimeout 在事件循环中都属于 Task


事件循环的优先级是一样的,同样都属于主线程任务(标记起来,后面考重点)


Web Worker


其实网上还有类似于 requestAnimationFrame 的方案


但是测试下来更离谱,就不浪费彦祖们的时间了


进入正题吧


其实上文说了,主线程任务的优先级会被降低,那么我们思考一下子线程任务呢?


子线程任务在前端领域,我们不就能想到 Web Worker 吗?


当然除了 Web Worker,还有 SharedWorker Service Worker


非本文重点,不做赘述


什么是 Web Worker


首先我们来认识下什么是 Web Worker



Web Worker 是一种运行在浏览器后台的独立 JavaScript 线程,允许我们在不阻塞主线程(即不影响页面 UI 和用户交互的情况下)执行一些耗时的任务,比如数据处理、文件操作、复杂计算等。



不阻塞主线程这恰恰是我们的所需要的!


使用 Web Worker


其实 Web Worker常规使用vue 中还是有一定的区别的


常规使用


常规使用其实非常简单,我们还是以上文中的 demo 为例


改造一下



  • index.html


const worker = new Worker('./worker.js');

我们还需要一个 worker.js文件



  • worker.js


let prev = performance.now()

setInterval(() => {
const offset = performance.now() - prev
prev = performance.now()
console.log('__SY__🎄 ~ setInterval ~ offset:', offset)
}, 1000)

切换 tab 几分钟后让我们来看看打印结果


image.png


非常完美,几乎都保持在 1000ms 左右


在 vue 中使用 Web Worker


在 vue 中使用就和常规使用有所不同了


这也是笔者今天踩坑比较多的地方


网上很多文中配置了 webpackworker-loader,然后改造 vue.config.js


但是笔者多次尝试,还是各种报错(如果有大佬踩过坑,请在评论区留言)


最后笔者翻到了之前的笔记,其实早在多年之前就记录了在 vue 中使用 Web Worker 的文章


使用方式非常简单


我们只需要把 worker.js 放置于 public 目录即可!


看下我们此时的代码



  • App.vue


// 此处注意要访问 根路径 /
const myWorker = new Worker('/worker.js')


  • public

    • worker.js




let prev = performance.now()

setInterval(() => {
const offset = performance.now() - prev
prev = performance.now()
console.log('__SY__🎄 ~ setInterval ~ offset:', offset)
}, 1000)

测试一下


image.png


非常完美!


解决业务问题


彦祖们此时可能要问道,你只是证明了 Web Worker 不会阻塞主进程


和你的业务有什么关系吗?


其实这还得依赖于Web Worker的通信机制


我们继续改造



  • App.vue


const myWorker = new Worker('/worker.js')

myWorker.postMessage('createPingInterval') //向 worker 发送开启定时器的指令
// 接收 Web Worker 的执行指令,执行对应业务
myWorker.onmessage = function (event) {
console.log('__SY__🎄 ~ event:', event)
}


  • worker.js


// 接收到主进程 `开启定时器的指令` 处理定时器逻辑
self.onmessage = function(event) {
const interval = setInterval(() => {
self.postMessage('executor') // 定时向主进程发送定时器执行指令
}, 1000)
}

封装一个 setWorkerInterval


其实有了以上的代码模型,我们就能封装一个不受主进程阻塞的定时器了


我们暂且命名它为 setWorkerInterval


函数设计


首先设计一下我们的函数


为了减少开发者心智负担


我们需要把函数设计成和 setInterval 一样的用法


我们在使用 setInterval 的时候,日常最常用的参数就是 callbackdelay


它的返回值是一个 intervalID


由此可见我们的函数签名如下


function setWorkerInterval(callback,delay){
const intervalID = xxx(callback,delay) // 定时执行
return intervalID
}

动手实现


有了上面的函数设计,我们就开始来实现


目前我们遇到一个问题,那就是上文中的 xxx 具体是个啥?


这其实就是 Web Worker 中的 setInterval


我们只需要把 Web Worker 中的 setInterval的功能暴露给主线程不就完事了吗?


来看 代码



  • setWorkerInterval.js


export default function(callback, delay) {
//创建一个 worker
const worker = new Worker('/worker.js')
worker.postMessage('') // 开启定时器
// 接收 Web Worker 的消息
worker.onmessage = function(event) {
// 收到 worker 的 setInterval 触发,触发对应业务逻辑
}
}


  • worker.js



// 处理定时器逻辑
self.onmessage = function(event) {
const interval = setInterval(() => {
self.postMessage({}) // 定时通知主线程,即上文中的 xxx
}, 1000)
}

这样我们就初步完成了以上 xxx 的逻辑


但随之而来又有两个问题


1.如何触发对应业务逻辑?


2.如何清除定时器?


触发对应业务逻辑

其实第一个问题非常容易解决,我们不是传递了一个 callback 吗?


这不就是我们的业务逻辑吗


改造一下



  • setWorkerInteraval.js


export default function(callback, delay) {
const worker = new Worker('/worker.js')

worker.postMessage('') // 开启定时器
// 接收 Web Worker 的消息
worker.onmessage = function(event) {
callback() // 定时执行业务 callback
}
}

清除定时器

这个问题还是踩了坑的,刚开始以为 intervalID 的来源不就在 worker.js吗?


那我们只需要把它通知给主线程即可,后来发现不可行,主线程的 clearInteravl 对于 workerintervalID 并不生效...


那我们换个思路,在主线程发送一个 clear 指令不就行了吗? 说干就干,思路有了,直接看代码



  • worker.js


// 处理定时器逻辑
self.onmessage = function(event) {

const intervalID = setInterval(() => {
self.postMessage({}) // 定时通知主线程,即上文中的 xxx
}, 1000)

/// 收到clear消息后, 清除定时器
if (event.data === 'clear') {
clearInterval(intervalID)
}

}


  • setWorkerInteraval


export default function(callback, delay) {
const worker = new Worker('/worker.js');
// 因为 onmessage 是异步的, 所以我们要抛出一个 promise
return new Promise((resolve) => {
worker.postMessage('') // 开启定时器
// 接收 Web Worker 的消息
worker.onmessage = function(event) {
callback() // 执行业务逻辑
}
})
const clear = () => {worker.postMessage('clear')}
return clear // 返回一个函数, 用于关闭定时器
}

让我们看下使用方式


let prev = performance.now()
const clear = setWorkerInteraval(function(){
const offset = performance.now() - prev
console.log('__SY__🎄 ~ setWorkerInteraval ~ offset:', offset)
prev = performance.now()
},1000)

setTimeout(clear,5000) // 5000ms 后清除

以上代码看似没问题,但是使用下来并不生效,也就是定时器并未被清除


问题出在哪里呢?


其实我们在发送 clear 指令的时候,也会进入 self.onmessage 函数


那么此时又会新建一个 interval,而我们清空的只是当前 interval 而已


那么我们必须想个方法,使得 interval 在当前实例是唯一的


其实非常简单,借助于 JS 万物皆对象 的思想,我们的 self 不也是一个对象吗?


那我们在它上面挂载一个 interval 有何不可呢?说干就干



  • worker.js


// 处理定时器逻辑
self.onmessage = function (event) {
// 返回一个非零值 所以我们可以大胆使用 ||=
self.intervalID ||= setInterval(() => {
self.postMessage({}) // 定时通知主线程,即上文中的 xxx
}, 1000)

/// 收到clear消息后, 清除定时器
if (event.data === 'clear') {
clearInterval(self.intervalID)
}
}

测试后,非常完美,至此,一个靠谱的定时器我们就完成了!


当然我们还可以把上文中的 1000ms 改成 delay 传参,直接看完成代码吧


完整代码



  • worker.js (vue项目 需要放在 public 中)


// 处理定时器逻辑
self.onmessage = function (event) {
/// 收到clear消息后, 清除定时器
if (event.data === 'clear') {
clearInterval(self.intervalID)
} else {
const delay = event.data
self.intervalID ||= setInterval(() => {
self.postMessage({}) // 定时通知主线程,即上文中的 xxx
}, delay)
}
}


  • setWorkerInteraval


export default function(callback, delay) {
const worker = new Worker('/worker.js');

worker.postMessage(delay) // 传递 delay 延时参数
// 接收 Web Worker 的消息
worker.onmessage = function(event) {
callback() // 执行业务逻辑
}
const clear = () => {worker.postMessage('clear')}
return clear
}


写在最后


技术服务于业务,但最怕局限于业务


希望彦祖们在开发业务中,能获取更多更深层次的思考和能力!共勉✨


感谢彦祖们的阅读


个人能力有限


如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟


彩蛋


宁波团队还有一个资深前端hc, 带你实现海鲜自由。 欢迎彦祖们私信😚


作者:前端手术刀
来源:juejin.cn/post/7418391732163182607
收起阅读 »

css3+vue做一个带流光交互效果的功能入口卡片布局

web
前言 该案例主要用到了css的新特性 @property来实现交互流光效果 @property是CSS Houdini API的一部分,通过它,开发者可以在样式表中直接注册自定义属性,而无需运行任何JavaScript代码。 Demo在线预览 @prope...
继续阅读 »

前言


该案例主要用到了css的新特性 @property来实现交互流光效果

@property是CSS Houdini API的一部分,通过它,开发者可以在样式表中直接注册自定义属性,而无需运行任何JavaScript代码。


Demo在线预览



@propert语法说明


@property --自定义属性名 {  
syntax: '语法结构';
initial-value: '初始值';
inherits: '是否允许该属性被继承';
}

自定义属性名:需要以--开头,这是CSS自定义属性的标准命名方式。

syntax:描述该属性所允许的语法结构,是必需的。它定义了自定义属性可以接受的值的类型,如颜色、长度、百分比等。

initial-value:用于指定自定义属性的默认值。它必须能够按照syntax描述符的定义正确解析。在syntax为通用语法定义时,initial-value是可选的,否则它是必需的。

inherits:用于指定该自定义属性是否可以被其他元素所继承,通过布尔值truefalse赋值。


Html代码部分


  <div class="flex-x-box">
<!-- 内容 start -->
<div class="card" v-for="(i,index) in list" :key="index">
<img :src="i.url"/>
<div class="info-box">
<h1>{{ i.label +'管理' }}</h1>
<p>{{ i.desc }}</p>
</div>
<div class="details-box">
<span>
<h2>{{ i.total }}</h2>
<p>{{i.label +'总数'}}</p>
</span>
<span>
<h2>{{ i.add }}</h2>
<p>今日新增</p>
</span>
</div>
</div>
<!-- 内容 end -->
</div>

数据结构部分


list:[
{
label:'报表',
desc:'深度数据挖掘,多维度报表生成,实时监控业务动态,为企业决策提供有力数据支撑',
url:'图标地址',
total:'108',
add:'12'
},{
label:'产品',
desc:'全生命周期管理,从研发到销售无缝衔接,优化产品组合,提升市场竞争力',
url:'图标地址',
total:'267',
add:'25'
},{
label:'文档',
desc:'高效知识存储,智能检索体系,确保信息准确传递,助力团队协作与知识共享',
url:'图标地址',
total:'37',
add:'2'
}
]

css样式控制部分


.flex-x-box{
display:flex;
}

/* 卡片宽度 */
$card-height:280px;
$card-width:220px;


div,span,p{
box-size:border-box;
}
.card {
position: relative;
width: $card-width;
height: $card-height;
border-radius: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
color: rgba(88,199,250,0%);
cursor: pointer;
box-size:border-box;
background: linear-gradient(210deg,#203656,#426889);
margin:0px 15px;

img{
width:90%;
margin-top:-80px;
transition:200ms linear;
}
.info-box{
width:100%;
}
h1{
font-size:14px;
color:#f3fbff;
}
p{
font-size:12px;
color:#a1c4de;
padding: 0px 28px;
line-height: 1.6;
}
.details-box{
position: absolute;
display:flex;
align-items:center;
width: 100%;
height:120px;
box-sizing: border-box;
bottom: 0px;
padding-top: 45px;
z-index: 10;
background: linear-gradient(0deg, #152d4a 70%, transparent);
border-radius: 0px 0px 12px 12px;
transition:200ms linear;
opacity:0;
span{
width:0px;
flex-grow:1;
}
h2{
margin:0px;
font-size:14px;
line-height:1.8;
color:#fff;
}
p{
margin:0px;
}
}
}

.card:hover{
&::before{
background-image: linear-gradient(var(--rotate) , #5ddcff, #3c67e3 43%, #ffddc5);
animation: spin 3s linear infinite;
}
&::after{
background-image: linear-gradient(var(--rotate), #5ddcff, #3c67e3 43%, #ff8055);
animation: spin 3s linear infinite;
}
.details-box{
opacity:1;
}
img{
width:96%;
margin-top:-60px;
}
}

.card::before {
content: "";
width:$card-width +2px;
height:$card-height +2px;
border-radius: 13px;
background-image: linear-gradient(var(--rotate) , #435b7c, #5f8cad 50%, #729bba);
position: absolute;
z-index: -1;
top: -1px;
left: -1px;
}
.card::after {
position: absolute;
content: "";
top: calc($card-height / 6);
left: 0;
right: 0;
z-index: -1;
height: 100%;
width: 100%;
margin: 0 auto;
transform: scale(0.9);
filter: blur(20px);
opacity: 1;
transition: opacity .5s;
}

/* CSS 自定义属性 */
@property --rotate {
syntax: "<angle>";
initial-value: 90deg;
inherits: false;
}

@keyframes spin {
0% {
--rotate: 0deg;
}
100% {
--rotate: 360deg;
}
}

作者:Easy_Y
来源:juejin.cn/post/7423708823428055081
收起阅读 »

入职N天的我,终于接到了我的第一个需求

web
我是9月份中下旬入职的,入职了之后,每天的工作就是熟悉代码和框架,就这样过了几天之后,迎来了我在新公司的第一个节日:国庆节。每年国庆节都是农民最开心的日子,因为要秋收了,秋收就意味着赚钱了,但是今年农作物的行情就跟程序员的行情一样,超级不好,各种粮食的价格都很...
继续阅读 »

我是9月份中下旬入职的,入职了之后,每天的工作就是熟悉代码和框架,就这样过了几天之后,迎来了我在新公司的第一个节日:国庆节。

每年国庆节都是农民最开心的日子,因为要秋收了,秋收就意味着赚钱了,但是今年农作物的行情就跟程序员的行情一样,超级不好,各种粮食的价格都很便宜,辛辛苦苦一年只能算个收支平衡(来自一个农二代的吐槽)。

节后第一天,由于领导请假了,所以我还是在自己看代码(是的,还在看)。等到了第二天,领导来上班了,我迫不及待的问领导有没有什么需求给我,领导说快有了,然后我就只能默默地等待,好在中午快吃饭的时候,领导跟我说有个功能需要修改一下,让我来做(激动地心,颤抖的手,终于有需求可做了)

项目排序功能

需求:根据汉字拼音进行排序。

有以下几种实现方案

方式一:通过String.prototype.localCompare()

const arr = ['我十分', '喜欢', '写', '代码'];

function sortArr(data) {
    return data.sort((a, b) => a.localeCompare(b, 'zh', { sensitivity'base' }))
}
sortArr(arr)

如果大家有对localeCompare不熟悉的可以继续往下看,如果有比较熟悉的,那么可以直接跳过。

localeCompare

返回一个数字,用于表示引用字符串(调用者referenceString)在比较字符串(第一个参数compareString)的前面、后面、或者相等

参数
  • compareString: 要与referenceString进行比较的字符串,所有值都会被强制转换为字符串。如果不传或者传入undefined会出现你不想要的情况
  • locals:表示语言类型,比如zh就是中文
  • options: 一堆属性
  • localeMather:区域匹配算法
  • usage:比较是用于排序还是用于搜索匹配的字符串,支持sort和search,默认sort
  • sensitivity:字符串哪些差异导致非0
    • base:字母不同的字符串比较不相等,a≠b,a=A
    • accent:字母不同 || 重音符号和其他变音符号的不同字符串比较不相等, a ≠ b、a ≠ á、a = A。
    • case:字母不同 或者 大小写的字母表示不同 a ≠ b,a ≠ A
    • variant:字符串的字母、重音和其他变音符号,或者不同大小写比较不相等,a ≠ b、a ≠ á、a ≠ A

    注意:当useage的字为sort时,sensitivity的字默认为variantignore

  • Punctuation:是否忽略标点符号,默认为falsen
  • umeric:是否使用数字对照,使得“1”<“2”<“10”,默认是false
  • caseFirst:是否个根据大小写排序,可能的值是upper、lower、false(默认设置)
  • collation:区域的变体
    • pinyin:汉语
    • compat:阿拉伯语

locals中的配置会和options内的配置会发生重叠,如果有重叠,options的优先级更高

注意:某些浏览器中可能不支持locals或者options,那么此时会忽略这些属性

返回值

返回一个数字

  • 如果引用字符串在比较字符串前面,返回负数
  • 如果引用字符串在比较字符串后面,返回正数
  • 如果两者相同,那么返回0

warning:不要依赖具体的返回值去做一些事情,因为在不同的浏览器、或者相同浏览器的不同版本中,返回的具体数值可能是不一样的,W3C只要求返回值是正数或者负数,而不规定具体的值。

方式2:通过pinyin库

  1. 需要安装pinyin库,在命令行中执行
npm install pinyin
  1. 实现排序逻辑

const { pinyin } = require('pinyin')

const arr = [ '我十分', '喜欢', '写', '代码' ]

const sortedWords = arr.sort((a, b) => {
const pinyinA = pinyin(a, { style: pinyin.STYLE_NORMAL }).flat().join('')
const pinyinB = pinyin(b, { style: pinyin.STYLE_NORMAL }).flat().join('')
return pinyinA.localeCompare(pinyinB)
})

console.log(sortedWords)

方式3:自己实现一套映射

因为我们的文案不是确定的,且可以随意修改,所以这种方案不提倡,但是如果只有几个固定的文案,这样可以自己实现一套映射

const pinyinMap = {
我十分: 'woshifen',
喜欢: 'xihuan',
写: 'xie',
代码: 'daima',
}
const arr = [ '我十分', '喜欢', '写', '代码' ]

const sortedWords = arr.sort((a, b) => pinyinMap[a].localeCompare(pinyinMap[b]))
console.log(sortedWords)

上面三种方式,可以看的出来,第一种还是存在一定的误差,但是我还是选择了第一种实现方式,有以下几个原因

  1. 不需要额外的引入库
  2. 我们的文案是随时可以修改的
  3. 我们对于排序的要求没有那么强烈,只要排一个大致的顺序即可

以上就是我对根据汉字拼音排序实现方案的理解,欢迎大家补充,希望大家一起进步!


作者:落魄的开发
来源:juejin.cn/post/7423573726400299027
收起阅读 »

在线人数统计功能怎么实现?

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方...
继续阅读 »

一、前言


大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。


在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方式:使用Redis的有序集合(zset)实现。
核心方法是这四个:zaddzrangeByScorezremrangeByScorezrem


二、实现步骤


1. 如何认定用户是否在线?


认定用户在线的条件一般跟网站有关,如果网站需要登录才能进入,那么这种网站就是根据用户的token令牌有效性判断是否在线;
如果网站是公开的,是那种不需要登录就可以浏览的,那么这种网站一般就需要自定一个规则来识别用户,也有很多方式实现如IPdeviceId浏览器指纹,推荐使用浏览器指纹的方式实现。


浏览器指纹可能包括以下信息的组合:用户代理字符串 (User-Agent string)、HTTP请求头信息、屏幕分辨率和颜色深度、时区和语言设置、浏览器插件详情等。现成的JavaScript库,像 FingerprintJSClientJS,可以帮助简化这个过程,因为它们已经实现了收集上述信息并生成唯一标识的算法。


使用起来也很简单,如下:


// 安装:npm install @fingerprintjs/fingerprintjs

// 使用示例:
import FingerprintJS from '@fingerprintjs/fingerprintjs';

// 初始化指纹JS Library
FingerprintJS.load().then(fp => {
// 获取访客ID
fp.get().then(result => {
const visitorId = result.visitorId;
console.log(visitorId);
});
});


这样就可以获取一个访问公开网站的用户的唯一ID了,当用户访问网站的时候,将这个ID放到访问链接的Cookie或者header中传到后台,后端服务根据这个ID标示用户。


2. zadd命令添加在线用户


(1)zadd命令介绍
zadd命令有三个参数



key:有序集合的名称。
score1、score2 等:分数值,可以是整数值或双精度浮点数。
member1、member2 等:要添加到有序集合的成员。
例子:向名为 myzset 的有序集合中添加一个成员:ZADD myzset 1 "one"



(2)添加在线用户标识到有序集合中


// expireTime给用户令牌设置了一个过期时间
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeout);
String expireTimeStr = DateUtil.formatFullTime(expireTime);
// 添加用户token到有序集合中
redisService.zadd("user.active", Double.parseDouble(expireTimeStr), userToken);


由于一个用户可能户会重复登录,这就导致userToken也会重复,但为了不重复计算这个用户的访问次数,zadd命令的第二个参数很好的解决了这个问题。
我这里的逻辑是:每次添加一个在线用户时,利用当前时间加上过期时间计算出一个分数,可以有效保证当前用户只会存在一个最新的登录态。



3. zrangeByScore命令查询在线人数


(1)zrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:查询分数在 1 到 3之间的所有成员:ZRANGEBYSCORE myzset 1 3



(2)查询当前所有的在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 查询当前日期到"+inf"之间所有的用户
Set userOnlineStringSet = redisService.zrangeByScore("user.active", now, "+inf");


利用zrangeByScore方法可以查询这个有序集合指定范围内的用户,这个userOnlineStringSet也就是在线用户集,它的size就是在线人数了。



4. zremrangeByScore命令定时清除在线用户


(1)zremrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:删除分数在 1 到 3之间的所有成员:ZREMRANGEBYSCORE myzset 1 3



(2)定时清除在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 清除当前日期到"-inf"之间所有的用户
redisService.zremrangeByScore(""user.active"","-inf", now);


由于有序集合不会自动清理下线的用户,所以这里我们需要写一个定时任务去定时删除下线的用户。



5. zrem命令用户退出登录时删除成员


(1)zrem命令介绍



key:指定的有序集合的名字。
members:需要删除的成员
例子:删除名为xxx的成员:ZREM myzset "xxx"



(2)定时清除在线用户


// 删除名为xxx的成员
redisService.zrem("user.active", "xxx");


删除 zset中的记录,确保主动退出的用户下线。



三、小结一下


这种方案的核心逻辑就是,创建一个在线用户身份集合为key,利用用户身份为member,利用过期时间为score,然后对这个集合进行增删改查,实现起来还是比较巧妙和简单的,大家有兴趣可以试试看。


作者:summo
来源:juejin.cn/post/7356065093060427816
收起阅读 »

nginx(前端必会-项目部署-精简通用篇)

前言 最近在公司部署项目时遇上了一点关于nginx的问题,于是就想着写一篇关于nginx的文章... 主要给小白朋友分享,nginx是什么,nginx有什么用,最后到nginx的实际应用,项目部署。 nginx 公司项目刚刚上线,用户量少访问量不大,并发量低,...
继续阅读 »

前言


最近在公司部署项目时遇上了一点关于nginx的问题,于是就想着写一篇关于nginx的文章...


主要给小白朋友分享,nginx是什么,nginx有什么用,最后到nginx的实际应用,项目部署。


nginx


公司项目刚刚上线,用户量少访问量不大,并发量低,一个jar包启动应用就能够解决问题了。但是随着项目的不断扩大,用户体量大增加,一台服务器可能就无法满足我们的需求了(当初是一个人用一个服务器,现在是多人用一个服务器,时间长了服务器都要红温)。


于是乎,我们就会想着增加服务器,那么我们多个项目就启动在多个服务器上面,用户需要访问,就需要做一个代理服务器,通过代理请求服务器来做前后端之间的转发和请求包括跨域等等问题。


那么到这就不得不说一下nginx的反向代理了,正向代理指的其实就是比如我们通过VPN去请求xxx,这里就是因为用到了其他地方的代理服务器,这是一个从客户端到服务端的过程,然而反向代理其实就是,因为我们有多个服务器,最后都映射到了代理服务器身上,客户端最终访问的都是例如:baidu.com,但是事实上他底下是有多台服务器的。


既然他有多台服务器,每台服务器的性能,各种条件都是不同的,这里就要说到nginx的另一个能力---负载均衡,可以给不同的服务器增加不同的权重,能力更强的服务器可以增大他的负荷,减轻其他服务器的负荷


这就是大家常说的nginx:Nginx 是一个高性能的 HTTP反向代理服务器,它还支持 IMAP/POP3/SMTP 代理服务器。


nginx的特点:



  1. 高性能



    • 高并发连接处理能力:Nginx 使用异步事件驱动模型(如 epoll, kqueue 等),能够高效地处理大量并发连接。

    • 低资源消耗:与 Apache 相比,Nginx 在相同硬件环境下通常消耗更少的内存和其他系统资源。



  2. 稳定性



    • 运行稳定:在高负载情况下依然保持稳定运行,崩溃或错误的发生率较低。

    • 平滑升级:可以在不停止服务的情况下进行升级或配置更改。



  3. 丰富的功能集



    • 反向代理:可以作为反向代理服务器,将请求转发到后端服务器。

    • URL 重写:通过简单的配置即可实现复杂的 URL 重写规则。

    • 动态内容与静态内容分离:可以配置为只处理静态文件请求,而动态请求则交给后端应用服务器处理。



  4. 高度可配置性



    • 灵活的配置选项:可以根据需要定制各种配置选项,以适应不同的应用场景。

    • 容易管理:配置文件结构清晰,易于理解和修改。



  5. 负载均衡



    • 支持多种负载均衡算法,例如轮询、加权轮询、最少连接数等,可以帮助分散到多个后端服务器的流量。



  6. 缓存功能



    • 可用作HTTP缓存服务器,减少对后端数据库或应用服务器的压力。



  7. 安全性



    • 提供 SSL/TLS 加密支持,保障数据传输安全。

    • 可以设置访问控制、防火墙规则等来增强安全性。



  8. 模块化架构



    • 支持第三方模块扩展功能,比如 Nginx+Lua 使得开发者可以在 Nginx 中直接使用 Lua 脚本语言编写插件或处理逻辑。



  9. 日志与监控



    • 详细的访问和错误日志记录,便于故障排查和性能分析。

    • 支持实时监控和统计,方便管理员了解当前系统的状态。




nginx下载


nginx.org/ 大家自行下载,我下载的是一个稳定版本,以防万一。下载完毕之后大家自行解压即可(默认大家是windows系统),解压完毕之后,可以看到nginx.exe就是我们的启动文件,conf就是配置文件,nginx.config中可以看到server的listen监听端口为80,这意味着当我们访问了80端口就会被nginx拦截,首先启动nginx,可以直接双击nginx.exe也可以通过cmd命令行直接输入nginx.exe运行(推荐,因为这样不会关闭窗口,双击的话就是一闪而过了)


image.png


接下来我们浏览器访问localhost:80


image.png


启动成功。


nginx常用命令


停止:nginx.exe -s stop
安全退出:nginx.exe -s quit
重新加载配置文件:nginx.exe -s reload(常用)例如我们更改了端口
查看nginx进程:ps aux|grep nginx


实际应用


下载完毕后打开可以看到:


image.png


image.png


于是我们建立aa、bb两个文件夹,我们将index.html 分别放入aa和bb,这两个页面都打上自己的标记aa/bb


image.png


然后我们对nginx进行配置 nginx.conf


server {
listen 8001;
server_name localhost;

location / {
root html/aa;
index index.html index.htm;
}
}

server {
listen 8002;
server_name localhost;

location / {
root html/bb;
index index.html index.htm;
}
}

如果没结束,记得reload


nginx.exe -s reload

image.png


image.png


接下来我们放一个项目进去,打包后放入html中


image.png


修改配置文件,然后reload


 server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html/dist;
index index.html index.htm;
}


然后我们访问localhost,端口默认是80所以不用写,如果失败,可能是reload失败,再次reload就可


image.png


其他配置问题


Nginx的主配置文件(conf/nginx.conf)按以下结构组织:



  • 全局块 与Nginx运行相关的全局设置

  • events 与网络连接有关的设置

  • http 代理、缓存、日志、虚拟主机等的配置

  • server 虚拟主机的参数设置(一个http块可包含多个server块)

  • location 定义请求路由及页面处理方式


前端开发中经常会遇到跨域问题,nginx可以做代理轻松解决,事实上原理和cors一样,设置请求头


server {
   location /api {
       proxy_pass http://backend_server;
       add_header Access-Control-Allow-Origin *;
       add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
       add_header Access-Control-Allow-Headers 'Origin, Content-Type, Accept';
  }
}


缓存问题:


proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;

server {
   location / {
       proxy_cache my_cache;
       proxy_pass http://backend;
       add_header X-Cache-Status $upstream_cache_status;
  }
}


https提升安全性


server {
   listen 443 ssl;
   server_name example.com;

   ssl_certificate /etc/nginx/ssl/example.com.crt;
   ssl_certificate_key /etc/nginx/ssl/example.com.key;

   location / {
       proxy_pass http://backend_server;
  }
}


一个比较全面的配置


# 全局段配置
# ------------------------------

# 指定运行nginx的用户或用户组,默认为nobody。
#user administrator administrators;

# 设置工作进程数,通常设置为等于CPU核心数。
#worker_processes 2;

# 指定nginx进程的PID文件存放位置。
#pid /nginx/pid/nginx.pid;

# 指定错误日志的存放路径和日志级别。
error_log log/error.log debug;

# events段配置信息
# ------------------------------
events {
# 设置网络连接序列化,用于防止多个进程同时接受到新连接的情况,这种情况称为"惊群"
accept_mutex on;

# 设置一个进程是否可以同时接受多个新连接。
multi_accept on;

# 设置工作进程的最大连接数。
worker_connections 1024;
}

# http配置段,用于配置HTTP服务器的参数。
# ------------------------------
http {
# 包含文件扩展名与MIME类型的映射。
include mime.types;

# 设置默认的MIME类型。
default_type application/octet-stream;

# 定义日志格式。
log_format myFormat '$remote_addr$remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for';

# 指定访问日志的存放路径和使用的格式。
access_log log/access.log myFormat;

# 允许使用sendfile方式传输文件。
sendfile on;

# 限制每次调用sendfile传输的数据量。
sendfile_max_chunk 100k;

# 设置连接的保持时间。
keepalive_timeout 65;

# 定义一个上游服务器组。
upstream mysvr {
server 127.0.0.1:7878;
server 192.168.10.121:3333 backup; #此服务器为备份服务器。
}

# 定义错误页面的重定向地址。
error_page 404 https://www.baidu.com;

# 定义一个虚拟主机。
server {
# 设置单个连接上的最大请求次数。
keepalive_requests 120;

# 设置监听的端口和地址。
listen 4545;
server_name 127.0.0.1;

# 定义location块,用于匹配特定的请求URI
location ~*^.+$ {
# 设置请求的根目录。
#root path;

# 设置默认页面。
#index vv.txt;

# 将请求转发到上游服务器组。
proxy_pass http://mysvr;

# 定义访问控制规则。
deny 127.0.0.1;
allow 172.18.5.54;
}
}
}


如果有不明白的地方,遇到问题可以通过ai去迅速了解,在ai时代,我们的学习成本也大大下降了。


小结


本次主要带小白朋友学习nginx是什么、用于做什么,nginx的常用命令,nginx如何进行配置,最后实际操作一次简单的nginx。


作者:zykk
来源:juejin.cn/post/7424168473423020066
收起阅读 »

几种神秘鲜为人知但却有趣的前端技术

web
测定网速 navigator.connection.downlink 是一个用于表示当前连接下行速度的属性,它是 JavaScript 的 Network Information API 的一部分。此属性返回一个数值,单位为 Mbps(Megabits per...
继续阅读 »

测定网速


navigator.connection.downlink 是一个用于表示当前连接下行速度的属性,它是 JavaScript 的 Network Information API 的一部分。此属性返回一个数值,单位为 Mbps(Megabits per second),表示网络的下行带宽。


例如,你可以通过以下方式使用它:


if (navigator.connection) {
const downlink = navigator.connection.downlink;
console.log(`当前下行速度为: ${downlink} Mbps`);
} else {
console.log("当前浏览器不支持Network Information API");
}

需要注意的是,Network Information API 并不是所有浏览器都支持,因此在使用时最好进行兼容性检查。


在智能手机上启用振动


window.navigator.vibrate 是一个用于触发设备震动的 Web API,通常用于移动设备上。这个方法允许开发者控制设备的震动模式,它接受一个数字或一个数组作为参数。



  • 如果传入一个数字,这个数字表示震动的时长(以毫秒为单位)。

  • 如果传入一个数组,可以定义震动和静止的模式,例如 [200, 100, 200] 表示震动200毫秒,静止100毫秒,再震动200毫秒。


以下是一个简单的示例:


// 使设备震动 500 毫秒
if (navigator.vibrate) {
navigator.vibrate(500);
} else {
console.log("当前浏览器不支持震动 API");
}

// 使用数组来创建震动模式
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200, 100, 200]);
}

请注意,并不是所有的设备和浏览器都支持震动 API,因此在使用时最好确认设备的兼容性。


禁止插入文字


你可能不希望用户在输入字段中粘贴从其他地方复制的文本(请仔细考虑清楚是否真的要这样做)。通过跟踪事件paste并调用其方法preventDefault()就很容易完成了。


<input type="text"></input>
<script>
  const input = document.querySelector('input');

  input.addEventListener("paste"function(e){
    e.preventDefault()
  })

</script>

好了,现在你无法复制粘贴,必须得手动编写和输入所有的内容了


快速隐藏dom


要隐藏DOM元素,不是非得用到JavaScript。原生的HTML属性完全可以实现hidden。效果类似于添加样式display: none;。元素就从页面上消失了。


<p hidden>我在页面看不到了</p>

注意,这个技巧不适用于伪元素


快速使用定位


你知道CSS的inset属性吗?这是我们所熟悉的topleftrightbottom的缩写版本。通过类比短语法margin或属性padding,只要一行代码就可以设置元素的所有偏移量。


/* 普通写法 */ 
div {
  position: absolute;
  top0;
  left0;
  bottom0;
  right0;
}

/* inset写法 */ 
div {
  position: absolute;
  inset0;
}

使用简短的语法可以大大减小CSS文件的体积,这样代码看起来更干净。但是,可别忘了inset是一个布尔属性,它考虑了内容排版方向。换句话说,如果站点使用的是具有rtl方向(从右到左)的语言,那么left要变成right,反之亦然。


你不知道的Console的用法


通常我们用的最多的console.log(xxx),其实在 JavaScript 中,console 对象提供了一些很有用的方法用于调试和查看信息。以下是一些可能不太常见的 console 用法:



  1. console.table() : 可以用来以表格的格式输出数组或对象,非常适合查看数据结构。


    const data = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 }
    ];
    console.table(data);


  2. console.group() 和 console.groupEnd() : 可以将相关日志信息分组,方便查看和组织输出。


    console.group('Gr0up Label');
    console.log('这是一条 log');
    console.log('这是一条 log 2');
    console.groupEnd();


  3. console.time() 和 console.timeEnd() : 用于测量代码块的执行时间。


    console.time('myTimer');
    // 执行一些操作
    console.timeEnd('myTimer'); // 输出所用的时间


  4. console.error() 和 console.warn() : 用于输出错误和警告信息,通常会以不同的颜色高亮显示。


    console.error('这是一个错误信息');
    console.warn('这是一个警告信息');


  5. console.assert() : 用于在条件为 false 时输出错误信息。


    const condition = false;
    console.assert(condition, '条件为 false,输出这条信息');


  6. console.clear() : 清空控制台的输出。


    console.clear();


  7. console.dir() : 用于打印对象的可枚举属性,方便查看对象的详细结构。


    const obj = { name: 'Alice', age: 25 };
    console.dir(obj);



禁止下拉刷新


下拉刷新是当前流行的移动开发模式。如果你不喜欢这样做,只需将overscroll-behavior-y属性的值设为contains即可达到此效果。


body {
 overscroll-behavior-y: contain;
}

这个属性对于组织模态窗口内的滚动也非常有用——它可以防止主页在到达边框时拦截滚动


使整个网页的 <body> 内容可编辑


document.body.contentEditable='true'; 是一段 JavaScript 代码,用于使整个网页的 <body> 内容可编辑。这意味着用户可以直接在浏览器中点击并编辑文本,就像在文本编辑器中一样。


以下是一些相关的要点:



  1. 启用编辑模式:将 contentEditable 属性设置为 'true',浏览器会允许用户更改页面的内容。


    document.body.contentEditable = 'true';


  2. 禁用编辑模式:如果希望用户无法编辑页面,您可以将该属性设置为 'false'


    document.body.contentEditable = 'false';


  3. 注意事项



    • 这种做法在很多场景中很方便,比如在展示一些信息并希望用户能快速修改的时候。例如,创建自定义的富文本编辑器。

    • 但是,使用 contentEditable 也可能会带来一些不便,比如用户修改了页面的结构,甚至可能影响脚本的运行。因此在使用时要谨慎,并确保有合适的方法来处理用户的输入。

    • 启用 contentEditable 后,如果网页中有表单元素,用户的输入可能与表单的默认行为产生冲突。



  4. 样式和功能:在启用编辑模式后,你可能还想添加一些 CSS 来改变光标样式,或者结合 JavaScript 进一步增强编辑体验,比如自动保存用户的修改。


示例代码:


// 启用编辑功能
document.body.contentEditable = 'true';

// 禁用编辑功能
// document.body.contentEditable = 'false';

这种功能可以非常方便地用于快速原型设计或需要快速内容编辑的应用,但在生产环境中要慎重使用。


带有Id属性的元素,会创建全局变量


在一张HTML页面中,所有设置了ID属性的元素会在JavaScript的执行环境中创建对应的全局变量,这意味着document.getElementById像人的智齿一样显得多余了。但实际项目中最好还是老老实实该怎么写就怎么写,毕竟常规代码出乱子的机会要小得多。


<div id="test"></div>
<script>
console.log(test)
</script>

网站平滑滚动


<html> 元素中添加 scroll-behavior: smooth,以实现整个页面的平滑滚动。


html{    
scroll-behavior: smooth;
}

:empty 表示空元素


此选择器定位空的 <p> 元素并隐藏它们


p:empty{   
display: none;
}

作者:鱼樱前端
来源:juejin.cn/post/7423314983884292134
收起阅读 »

Kafka 为什么要抛弃 Zookeeper?

嗨,你好,我是猿java 在很长一段时间里,ZooKeeper都是 Kafka的标配,现如今,Kafka官方已经在慢慢去除ZooKeeper,Kafka 为什么要抛弃 Zookeeper?这篇文章我们来聊聊其中的缘由。 Kafka 和 ZooKeeper 的关...
继续阅读 »

嗨,你好,我是猿java


在很长一段时间里,ZooKeeper都是 Kafka的标配,现如今,Kafka官方已经在慢慢去除ZooKeeper,Kafka 为什么要抛弃 Zookeeper?这篇文章我们来聊聊其中的缘由。


Kafka 和 ZooKeeper 的关系


ZooKeeper 是一个分布式协调服务,常用于管理配置、命名和同步服务。长期以来,Kafka 使用 ZooKeeper 负责管理集群元数据、控制器选举和消费者组协调等任务理,包括主题、分区信息、ACL(访问控制列表)等。


ZooKeeper 为 Kafka 提供了选主(leader election)、集群成员管理等核心功能,为 Kafka提供了一个可靠的分布式协调服务,使得 Kafka能够在多个节点之间进行有效的通信和管理。然而,随着 Kafka的发展,其对 ZooKeeper的依赖逐渐显露出一些问题,这些问题也是下面 Kafka去除 Zookeeper的原因。


抛弃ZooKeeper的原因


1. 复杂性增加


ZooKeeper 是独立于 Kafka 的外部组件,需要单独部署和维护,因此,使用 ZooKeeper 使得 Kafka的运维复杂度大幅提升。运维团队必须同时管理两个分布式系统(Kafka和 ZooKeeper),这不仅增加了管理成本,也要求运维人员具备更高的技术能力。


2. 性能瓶颈


作为一个协调服务,ZooKeeper 并非专门为高负载场景设计, 因此,随着集群规模扩大,ZooKeeper在处理元数据时的性能问题日益突出。例如,当分区数量增加时,ZooKeeper需要存储更多的信息,这导致了监听延迟增加,从而影响Kafka的整体性能34。在高负载情况下,ZooKeeper可能成为系统的瓶颈,限制了Kafka的扩展能力。


3. 一致性问题


Kafka 内部的分布式一致性模型与 ZooKeeper 的一致性模型有所不同。由于 ZooKeeper和 Kafka控制器之间的数据同步机制不够高效,可能导致状态不一致,特别是在处理集群扩展或不可用情景时,这种不一致性会影响消息传递的可靠性和系统稳定性。


4. 发展自己的生态


Kafka 抛弃 ZooKeeper,我个人觉得最核心的原因是:Kafka生态强大了,需要自立门户,这样就不会被别人卡脖子。纵观国内外,有很多这样鲜活的例子,当自己弱小时,会先选择使用别家的产品,当自己羽翼丰满时,再选择自建完善自己的生态圈。


引入KRaft


为了剥离和去除 ZooKeeper,Kafka 引入了自己的亲儿子 KRaft(Kafka Raft Metadata Mode)。KRaft 是一个新的元数据管理架构,基于 Raft 一致性算法实现的一种内置元数据管理方式,旨在替代 ZooKeeper 的元数据管理功能。其优势在于:



  1. 完全内置,自包含:KRaft 将所有协调服务嵌入 Kafka 自身,不再依赖外部系统,这样大大简化了部署和管理,因为管理员只需关注 Kafka 集群。

  2. 高效的一致性协议:Raft 是一种简洁且易于理解的一致性算法,易于调试和实现。KRaft 利用 Raft 协议实现了强一致性的元数据管理,优化了复制机制。

  3. 提高元数据操作的扩展性:新的架构允许更多的并发操作,并减少了因为扩展性问题导致的瓶颈,特别是在高负载场景中。

  4. 降低延迟:在消除 ZooKeeper 作为中间层之后,Kafka 的延迟性能有望得到改善,特别是在涉及选主和元数据更新的场景中。

  5. 完全自主:因为是自家产品,所以产品的架构设计,代码开发都可以自己说了算,未来架构走向完全控制在自己手上。


KRaft的设计细节



  1. 控制器(Controller)节点的去中心化:KRaft 模式中,控制器节点由一组 Kafka 服务进程代替,而不是一个独立的 ZooKeeper 集群。这些节点共同负责管理集群的元数据,通过 Raft 实现数据的一致性。

  2. 日志复制和恢复机制:利用 Raft 的日志复制和状态机应用机制,KRaft 实现了对元数据变更的强一致性支持,这意味着所有控制器节点都能够就集群状态达成共识。

  3. 动态集群管理:KRaft 允许动态地向集群中添加或移除节点,而无需手动去 ZooKeeper 中更新配置,这使得集群管理更为便捷。


下面给出一张 Zookeeper 和 KRaft的对比图:


why-kafka-deprecates-zookeeper-1.jpg


总结


本文,我们分析了为什么 Kafka 要移除 ZooKeeper,主要原因有两个:ZooKeeper不能满足 Kafka的发展以及 Kafka想创建自己的生态。在面临越来越复杂的数据流处理需求时,KRaft 模式为 Kafka 提供了一种更高效、简洁的架构方案。不论结局如何,Kafka 和 ZooKeeper曾经也度过了一段美好的蜜月期,祝福 Kafka 在 KRaft模式越来越强大,为使用者带来更好的体验。


学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7425812523129634857
收起阅读 »

面试官:为什么你们项目中还在用多表关联!

我们来看这样一个面试场景。面试官:“在你们的项目中,用到多表关联查询了吗?”候选人:“嗯,每个项目都用到了。”面试官听了似乎有些愤怒,说:“多表关联查询这么慢,为什么你们还要用它,那你们项目的性能如何保障呢?”面对这突如其来地质问,候选人明显有些慌了,解释道:...
继续阅读 »

我们来看这样一个面试场景。

面试官:“在你们的项目中,用到多表关联查询了吗?”

候选人:“嗯,每个项目都用到了。”

面试官听了似乎有些愤怒,说:“多表关联查询这么慢,为什么你们还要用它,那你们项目的性能如何保障呢?”

面对这突如其来地质问,候选人明显有些慌了,解释道:“主要是项目周期太紧张了,这样写在开发效率上能高一些,后期我们会慢慢进行优化的。”

面试官听了,带着三分理解、三分无奈、四分恨铁不成钢地摇了摇头。

面试之后,这个同学问我说:“学长,是不是在项目中使用多表关联太low了,可是从业这五年多,我换过三家公司,哪个公司的项目都是这么用的。”

下面我来从实现原理的角度,回答一下这个问题。

“是否应该使用多表关联”,这绝对是个王炸级别的话题了,其争议程度,甚至堪比“中医废存之争”了。

一部分人坚定地认为,应该禁止使用SQL语句进行多表关联,正确的方式是把各表的数据读到应用程序中来,再由程序进行数据merge操作。

而另一部分人则认为,在数据库中进行多表关联是没有问题的,但需要多关注SQL语句的执行计划,只要别产生过大的资源消耗和过长的执行时间即可。

嗯,我完全支持后者的观点,况且MySQL在其底层算法的优化上,一直在努力完善。

MySQL表连接算法

我们以最常用的MySQL数据库为例,其8.0版本支持的表连接算法为Nested-Loops Join(嵌套循环连接)和Hash Join(哈希连接)。

如下图所示:

嵌套循环连接算法,顾名思义,其实现方式是通过内外层循环的方式进行表连接,SQL语句中有几个表进行连接就实现为几层循环。

接下来,我们看看嵌套循环连接算法的四种细分实现方式。

1、简单嵌套循环连接(Simple Nested-Loops Join)

这是嵌套循环连接中最简单粗暴的一种实现方式,直接用驱动表A中符合条件的数据,一条一条地带入到被驱动表B中,进行全表扫描匹配。

其伪代码实现为:

for each row in table A matching range {
for each row in table B matching join column{
if row satisfies join conditions,send to client
}
}

我们以下面的SQL语句举例:

SELECT * FROM product p INNER JOIN order o ON p.id = o.product_id;

其对应的详细执行步骤为:

(1)外循环从product表中每次读取一条记录。

(2)以内循环的方式,将该条记录与order表中的记录进行关联键的逐一比较,并找到匹配记录。

(3)将product表中的记录和order表中的匹配记录进行关联,放到结果集中。

(4)重复执行步骤1、2、3,直到内外两层循环结束。

也就是说,如果驱动表A中符合条件的数据有一万条,那么就需要带入到被驱动表B中进行一万次全表扫描,这种查询效率会非常慢。因此,除非特殊场景,否则查询优化器不太会选择这种连接算法。

2、索引嵌套循环连接(Index Nested-Loops Join)

接上文,如果在被驱动表B的关联列上创建了索引,那MySQL查询优化器极大概率会选择这种这种实现方式,因为其非常高效。

我们依然以下面的SQL语句举例:

SELECT * FROM product p INNER JOIN order o ON p.id = o.product_id;

其对应的详细执行步骤为:

(1)外循环从product表中每次读取一条记录。

(2)以内循环的方式,将该条记录与order表中关联键的索引进行匹配,直接找到匹配记录。

(3)将product表中的记录和order表中的匹配记录进行关联,放到结果集中。

(4)重复执行步骤1、2、3,直到内外两层循环结束。

这里需要说明一下,若order表的关联列是主键索引,则可以直接在表中查到记录;若order表的关联列是二级索引,则通过索引扫描的方式查到记录的主键,然后再回表查到具体记录。

3、缓存块嵌套循环连接(Block Nested-Loops Join)

我们在上文中说过,在简单嵌套循环连接中,如果驱动表A中符合条件的数据有一万条,那么就需要带入到被驱动表B中进行一万次全表扫描,这种查询效率会非常慢。

而缓存块嵌套循环连接,则正是针对于该场景进行的优化。

当驱动表A进行循环匹配的时候,数据并不会直接带入到被驱动表B,而是使用Join Buffer(连接缓存)先缓存起来,等到Join Buffer满了再去一次性关联被驱动表B,这样可以减少被驱动表B被全表扫描的次数,提升查询性能。

其伪代码实现为:

for each row in table A matching range {
store join column in join buffer
ifjoin buffer is full){
for each row in table B matching join column{
if row satisfies join conditions,send to client
}
}
}

我们依然以下面的SQL语句举例:

SELECT * FROM product p INNER JOIN order o ON p.id = o.product_id;

其对应的详细执行步骤为:

(1)外循环从product表中每次读取一定数量的记录,将Join Buffer装满。

(2)以内循环的方式,将Join Buffer中记录与order表中的记录进行关联键的逐一比较,并找到匹配记录。

(3)将product表中的记录和order表中的匹配记录进行关联,放到结果集中。

(4)重复执行步骤1、2、3,直到内外两层循环结束。

需要注意的是,从MySQL 8.0.20开始,MySQL就不再使用缓存块嵌套循环连接了,将以前使用缓存块嵌套循环连接的场景全部改为哈希连接。

所以,MySQL的研发者一直在努力优化这款产品,其产品本身也没有大家所想的那么弱鸡。

4、批量键访问连接(Batched Key Access Joins)

说到这里,不得不先提一下MySQL5.6版本的新特性,多范围读(Multi-Range Read)。

我们继续拿product表的场景进行举例。

SELECT * FROM product WHERE price > 5 and price < 20;

没有使用MRR的情况下:

由上图可见,在MySQL InnoDB中使用二级索引的范围扫描时,所获取到的主键ID是无序的,因此在数据量很大的情况下进行回表操作时,会产生大量的磁盘随机IO,从而影响数据库性能。

使用MRR的情况下:

显而易见的是,在二级索引和数据表之间增加了一层buffer,在buffer中进行主键ID的排序操作,这样回表操作就从磁盘随机I/O转化为顺序I/O,并可减少磁盘的I/O次数(1个page里面可能包含多个命中的record),提高查询效率。

而从MySql 5.6开始支持Batched Key Access Joins,则正是结合了MRR和Join Buffer,以此来提高多表关联的执行效率,其发生的条件为被驱动表B有索引,并且该索引为非主键。

其伪代码实现和详细步骤为:

for each row in table A matching range {
store join column in join buffer
ifjoin buffer is full){
for each row in table B matching join column{
send to MRR interface,and order by its primary key
if row satisfies join conditions,send to client
}
}
}

另外,如果查询优化器选择了批量键访问连接的实现方式,我们可以在执行计划中的Extra列中看到如下信息:

Using where; Using join buffer (Batched Key Access)

结语

在本文中,我们介绍了嵌套循环连接的四种实现方式,下文会继续讲解哈希连接算法,以及跟在程序中进行多表数据merge的实现方式进行对比。


作者:托尼学长
来源:juejin.cn/post/7387626171158675466
收起阅读 »

大公司如何做 APP:背后的开发流程和技术

我记得五六年前,当我在 Android 开发领域尚处初出茅庐阶段之时,我曾有一个执念——想看下大公司在研发一款产品的流程和技术上跟小公司有什么区别。公司之大,对开发来说,不在于员工规模,而在于产品的用户量级。只有用户量级够大,研发过程中的小问题才会被放大。当用...
继续阅读 »

我记得五六年前,当我在 Android 开发领域尚处初出茅庐阶段之时,我曾有一个执念——想看下大公司在研发一款产品的流程和技术上跟小公司有什么区别。公司之大,对开发来说,不在于员工规模,而在于产品的用户量级。只有用户量级够大,研发过程中的小问题才会被放大。当用户量级够大,公司才愿意在技术上投入更多的人力资源。因此,在大公司里做技术,对个人的眼界、技术细节和深度的提升都有帮助。


我记得之前我曾跟同事调侃说,有一天我离职了,我可以说我毕业了,因为我这几年学到了很多。现在我想借这个机会总结下这些年在公司里经历的让我印象深刻的技术。


1、研发流程


首先在产品的研发流程上,我把过去公司的研发模式分成两种。


第一种是按需求排期的。在评审阶段一次性评审很多需求,和开发沟通后可能删掉优先级较低的需求,剩下的需求先开发,再测试,最后上线。上线的时间根据开发和测试最终完成的时间确定。


第二种是双周迭代模式,属于敏捷开发的一种。这种开发机制里,两周一个版本,时间是固定的。开发、测试和产品不断往时间周期里插入需求。如下图,第一周和第三周的时间是存在重叠的。具体每个阶段留多少时间,可以根据自身的情况决定。如果需求比较大,则可以跨迭代,但发布的时间窗口基本是固定的。


截屏2023-12-30 13.00.33.png


有意思的是,第二种开发机制一直是我之前的一家公司里负责人羡慕的“跑火车”模式。深度参与过两种开发模式之后,我说下我的看法。


首先,第一种开发模式适合排期时间比较长的需求。但是这种方式时间利用率相对较低。比如,在测试阶段,开发一般是没什么事情做的(有的会在这个时间阶段布置支线需求)。这种开发流程也有其好处,即沟通和协调成本相对较低。


注意!在这里,我们比较时间利用率的时候是默认两种模式的每日工作时间是相等的且在法律允许范围内。毕竟,不论哪一种研发流程,强制加班之后,时间利用率都“高”(至少老板这么觉得)。


第二种开发方式的好处:



  1. 响应速度快。可以快速发现问题并修复,适合快速试错。

  2. 时间利用率高。相比于按需求排期的方式,不存在开发和测试的间隙期。


但这种开发方式也有缺点:



  1. 员工压力大,容易造成人员流失。开发和测试时间穿插,开发需要保证开发的质量,否则容易影响整个迭代内开发的进度。

  2. 沟通成本高。排期阶段出现人力冲突需要协调。开发过程中出现问题也需要及时、有效的沟通。因此,在这种开发模式里还有一个角色叫项目经理,负责在中间协调,而第一种开发模式里项目经理的存在感很低。

  3. 这种开发模式中,产品要不断想需求,很容易导致开发的需求本身价值并不大。


做了这么多年开发,让人很难拒绝一个事实是,绝大多数互联网公司的壁垒既不是技术,也不是产品,而是“快速迭代,快速试错”。从这个角度讲,双周迭代开发机制更适应互联网公司的要求。就像我们调侃公司是给电脑配个人,这种开发模式里就是给“研发流水线”配个人,从产品、到开发、到测试,所有人都像是流水线上的一员。


2、一个需求的闭环


以上是需求的研发流程。如果把一个需求从产品提出、到上线、到线上数据回收……整个生命周期列出来,将如下图所示,


需求闭环.drawio.png


这里我整合了几个公司的研发过程。我用颜色分成了几个大的流程。相信每个公司的研发流程里或多或少都会包含其中的几个。在这个闭环里,我说一下我印象比较深刻的几个。


2.1 产品流程


大公司做产品一个显著的特点是数据驱动,一切都拿数据说话。一个需求的提出只是一个假设,开发上线之后效果评估依赖于数据。数据来源主要有埋点上报和舆情监控。


1. 数据埋点


埋点数据不仅用于产品需求的验证,也用于推荐算法的训练。因此,大公司对数据埋点的重视可以说是深入骨髓的。埋点数据也经常被纳入到绩效考核里。


开发埋点大致要经过如下流程,



  • 1). 产品提出需要埋的点。埋点的类型主要包括曝光和点击等,此外还附带一些上报的参数,统计的维度包括用户 uv 和次数 pv.

  • 2). 数据设计埋点。数据拿到产品要埋的点之后,设计埋点,并在埋点平台录入。

  • 3). 端上开发埋点。端上包括移动客户端和 Web,当然埋点框架也要支持 RN 和 H5.

  • 4). 端上验证埋点。端上埋点完成之后需要测试,上报埋点,然后再在平台做埋点校验。

  • 5). 产品提取埋点数据。

  • 6). 异常埋点数据修复。


由此可见,埋点及其校验对开发来说也是需要花费精力的一环。它不仅需要多个角色参与,还需要一个大数据平台,一个录入、校验和数据提取平台,以及端上的上报框架,可以说成本并不低。


2. 舆情监控


老实说,初次接触舆情监控的时候,它还是给了我一点小震撼的。没想到大公司已经把舆情监控做到了软件身上。


舆情监控就是对网络上关于该 APP 的舆情的监控,数据来源不仅包括应用内、外用户提交的反馈,还包括主流社交平台上关于该软件的消息。所有数据在整合到舆情平台之后会经过大数据分析和分类,然后进行监控。舆情监控工具可以做到对产品的负面信息预警,帮助产品经理优化产品,是产品研发流程中重要的一环。


3. AB 实验


很多同学可能对 AB 实验都不陌生。AB 实验就相当于同时提出多套方案,然后左右手博弈,从中择优录用。AB 实验的一个槽点是,它使得你代码中同时存在多份作用相同的代码,像狗皮膏药一样,也不能删除,非常别扭,最后导致的结果是代码堆积如山。


4. 路由体系建设


路由即组件化开发中的页面路由。但是在有些应用里,会通过动态下发路由协议支持运营场景。这在偏运营的应用里比较常见,比如页面的推荐流。一个推荐流里下发的模块可能打开不同的页面,此时,只需要为每个页面配置一个路由路径,然后推荐流里根据需要下发即可。所以,路由体系也需要 Android 和 iOS 双端统一,同时还要兼容 H5 和 RN.


mdn-url-all.png


在路由协议的定义上,我们可以参考 URL 的格式,定义自己的协议、域名、路径以及参数。以 Android 端为例,可以在一个方法里根据路由的协议、域名对原生、RN 和 H5 等进行统一分发。


2.2 开发流程


在开发侧的流程里,我印象深的有以下几个。


1. 重视技术方案和文档


我记得之前在一家公司里只文档平台就换了几个,足见对文档的重视。产品侧当然更重文档,而对研发侧,文档主要有如下几类:1). 周会文档;2).流程和规范;3).技术方案;4).复盘资料等。


对技术方案,现在即便我自己做技术也保留了写大需求技术方案先行的习惯。提前写技术方案有几个好处:



  • 1). 便于事后回忆:当我们对代码模糊的时候,可以通过技术方案快速回忆。

  • 2). 便于风险预知:技术方案也有助于提前预知开发过程中的风险点。前面我们说敏捷开发提前发现风险很重要,而做技术方案就可以做到这点。

  • 3). 便于全面思考:技术方案能帮助我们更全面地思考技术问题。一上来就写代码很容易陷入“只见树木,不见森林”的困境。


2. Mock 开发


Mock 开发也就是基于 Mock 的数据进行开发和测试。在这里它不局限于个人层面(很多人可能有自己 Mock 数据开发的习惯),而是在公司层面将其作为一种开发模式,以实现前后端分离。典型的场景是客户端先上线预埋,而后端开发可能滞后一段时间。为了支持 Mock 开发模式,公司需要专门的平台,提供以接口为维度的 Mock 工具。当客户端切换到 Mock 模式之后,上传到网络请求在后端的网关直接走 Mock 服务器,拉取 Mock 数据而不是真实数据。


这种开发模式显然也是为了适应敏捷开发模式而提出的。它可以避免前后端依赖,减轻人力资源协调的压力。这种开发方式也有其缺点:



  • 1). 数据结构定义之后无法修改。客户端上线之后后端就无法再修改数据结构。因此,即便后端不开发,也需要先投入人力进行方案设计,定义数据结构,并拉客户端进行评审。

  • 2). 缺少真实数据的验证。在传统的开发模式中,测试要经过测试和 UAT 两个环境,而 UAT 本身已经比较接近线上环境,而使用 Mock 开发就完全做不到这么严谨。当我们使用 Mock 数据测试时,如果我们自己的 Mock 的数据本身失真比较严重,那么在意识上你也不会在意数据的合理性,因此容易忽视一些潜在的问题。


3. 灰度和热修复


灰度的机制是,在用户群体中选择部分用户进行应用更新提示的推送。这要求应用本身支持自动更新,同时需要对推送的达到率、用户的更新率进行统计。需要前后端一套机制配合。灰度有助于提前发现应用中存在的问题,这对超大型应用非常有帮助,毕竟,现在上架之后发现问题再修复的成本非常高。


但如果上架之后确实出现了问题就需要走热修复流程。热修复的难点在于热修复包的下发,同时还需要审核流程,因此需要搭建一个平台。这里涉及的细节比较多,后面有时间再梳理吧。


4. 配置下发


配置下发就是通过平台录入配置,推送,然后在客户端读取配置信息。这也是应用非常灵活的一个功能,可以用来下发比如固定的图片、文案等。我之前做个人开发的时候也在服务器上做了配置下发的功能,主要用来绕过某些应用商店的审核,但是在数据结构的抽象上做得比较随意。这里梳理下配置下发的细节。



  • 首先,下发的配置是区分平台特征的。这包括,应用的目标版本(一个范围)、目标平台(Android、iOS、Web、H5 或者 RN)。

  • 其次,为了适应组件化开发,也为了更好地分组管理,下发的配置命名时采用 模块#配置名称 的形式。

  • 最后,下发的数据结构支持,整型、布尔类型、浮点数、字符串和 Json.


我自己在做配置下发的时候还遇到一个比较棘手的问题——多语言适配。国内公司的产品一般只支持中文,这方面就省事得多。


5. 复盘文化


对于敏捷开发,复盘是不可或缺的一环。有助于及时发现问题,纠正和解决问题。复盘的时间可以是定期的,在一个大需求上线之后,或者出现线上问题之后。


3、技术特点


3.1 组件化开发的痛点


在大型应用开发过程中,组件化开发的意义不仅局限于代码结构层面。组件化的作用体现在以下几个层面:



  • 1). 团队配合的利器。想想几十个人往同一份代码仓库里提交代码的场景。组件化可以避免无意义的代码冲突。

  • 2). 提高编译效率。对于大型应用,全源码编译一次的时间可能要几十分钟。将组件打包成 aar 之后可以减少需要编译的代码的数量,提升编译效率。

  • 3). 适应组织架构。将代码细分为各个组件,每个小团队只维护自己的组件,更方便代码权限划分。


那么,在实际开发过程中组件化开发会存在哪些问题呢?


1. 组件拆分不合理


这在从单体开发过渡到组件化开发的应用比较常见,即组件化拆分之后仍然存在某些模块彼此共用,导致提交代码的时候仍然会出现冲突问题。冲突包含两个层面的含义,一是代码文件的 Git 冲突,二是在打包合入过程中发布的 aar 版本冲突。比较常见的是,a 同学合入了代码到主干之后,b 同学没有合并主干到自己的分支就打包,导致发布的 aar 没有包含最新的代码。这涉及打包的问题,是另一个痛点问题,后面再总结。


单就拆分问题来看,避免上述冲突的一个解决办法是在拆分组件过程中尽可能解耦。根据我之前的观察,存在冲突的组件主要是数据结构和 SPI 接口。这是我之前公司没做好的地方——数据结构仓库和 SPI 接口是共用的。对于它们的组件化拆分,我待过的另一家公司做得更好。他们是如下拆分的,这里以 A 和 B 来命名两个业务模块。那么,在拆分的时候做如下处理,


模块:A-api
模块:A
模块:B-api
模块:B

即每个业务模块拆分成 api 和实现两部分。api 模块里包含需要共享的数据结构和 SPI 接口,实现模块里是接口的具体实现。当模块 A 需要和模块 B 进行交互的时候,只需要依赖 B 的 api 模块。可以参考开源项目:arch-android.


2. 打包合入的痛点


上面我们提到了一种冲突的情况。在我之前的公司里,每个组件有明确的负责人,在每个迭代开发的时候,组件负责人负责拉最新 release 分支。其他同学在该分支的开发需要经过负责人同意再合入到该分支。那么在最终打包的过程中,只需要保证这个分支的 aar 包含了全部最新的代码即可。也就是说,这种打包方式只关心每个 aar 的版本,而不关心实际的代码。因为它最终打包是基于 aar 而不是全源码编译。


这种打包方式存在最新的分支代码没有被打包的风险。一种可行的规避方法是,在平台通过 Git tag 和 commit 判断该分支是否已经包含最新代码。此外,还可能存在某个模块修改了 SPI 接口,而另一个模块没有更新,导致运行时异常的风险。


另一个公司是基于全源码编译的。不过,全源码编译只在最终打包阶段或者某个固定的时间点进行,而不是每次合入都全源码编译(一次耗时太久)。同时,虽然每个模块有明确的负责人,但是打包的 aar 不是基于当前 release 分支,而是自己的开发分支。这是为了保障当前 release 分支始终是可用的。合并代码到 release 分支的同时需要更新 aar 的版本。但它也存在问题,如果合并到 release 而没有打包 aar,那么可能导致 release 分支无法使用。如果打包了 aar 但是此时其他同学也打包了 aar,则可能导致本次打包的 aar 落后,需要重新打包。因此,这种合入方式也是苦不堪言。


有一种方法可以避免上述问题,即将打包和合入事件设计成一个消息队列。每次合入之前自动化执行上述操作,那么自然就可以保证每次操作的原子性(因为本身就是单线程的)。


对比两种打包和合入流程,显然第二种方式更靠谱。不过,它需要设计一个流程。这需要花费一点功夫。


3. 自动化切源码


我在之前的一家公司开发时,在开发过程中需要引用另一个模块的修改时,需要对另一个模块打 SNAPSHOT 包。这可行,但有些麻烦。之前我也尝试过手动修改 settings.gradle 文件进行源码依赖开发。不过,太麻烦了。


后来在另一个公司里看到一个方案,即动态切换到源码开发。可以将某个依赖替换为源码而只需要修改脚本即可。这个实践很棒,我已经把它应用到独立开发中。之前已经梳理过《组件化开发必备:Gradle 依赖切换源码的实践》.


3.2 大前端化开发


1. React Native


如今的就业环境,哪个 Android 开发不是同时会五六门手艺。跨平台开发几乎是不可避免的。


之前的公司为什么选择 React Native 而不是 Flutter 等新锐跨平台技术呢?我当时还刻意问了这个问题。主要原因:



  • 1). 首先是 React Native 相对更加成熟,毕竟我看了下 Github 第一个版本发布已经是 9 年前的事情了,并且至今依旧非常活跃。

  • 2). React Native 最近更新了 JavaScript 引擎,页面启动时间、包大小和内存占用性能都有显著提升。参考这篇文章《干货 | 加载速度提升15%,携程对RN新一代JS引擎Hermes的调研》.

  • 3). 从团队人才配置上,对 React Native 熟悉的更多。


React Native 开发是另一个领域的东西,不在本文讨论范围内。每个公司选择 React Native 可能有它的目的。比如,我之前的一家公司存粹是为了提效,即一次开发双端运行。而另一家公司,则是为了兼顾提效和动态化。如果只为提效,那么本地编译和打包 js bundle 就可以满足需求。若要追求动态化,就需要搭建一个 RN 包下发平台。实际上,在这个公司开发 RN 的整个流程,除了编码环节,从代码 clone 到最终发布都是在平台上执行的。平台搭建涉及的细节比较多,以后用到再总结。对于端侧,RN 的动态化依赖本地路由以及 RN 容器。


2. BFF + DSL


DSL 是一种 UI 动态下发的方案。相比于 React Native,DSL 下发的维度更细,是控件级别的(而 RN 是页面级别的)。简单的理解是,客户端和后端约定 UI 格式,然后按照预定的格式下发的数据。客户端获取到数据之后渲染。DSL 不适合需要复杂动画的场景。若确实要复杂动画,则需要自定义控件。


工作流程如下图中左侧部分所示,右侧部分是每个角色的责任。


DSL workflow.drawio.png


客户端将当前页面和位置信息传给 DSL 服务器。服务器根据上传的信息和位置信息找到业务接口,调用业务接口拉取数据。获取到数据后根据开发过程中配置的脚本对数据进行处理。数据处理完成之后再交给 DSL 服务器渲染。渲染完成之后将数据下发给客户端。客户端再根据下发的 UI 信息进行渲染。其中接口数据的处理是通过 BFF 实现的,由客户端通过编写 Groovy 脚本实现数据结构的转换。


这种工作流程中,大部分逻辑在客户端这边,需要预埋点位信息。预埋之后可以根据需求进行下发。这种开发的一个痛点在于调试成本高。因为 DSL 服务器是一个黑盒调用。中间需要配置的信息过多,搭建 UI 和编写脚本的平台分散,出现问题不易排查。


总结


所谓他山之石,可以攻玉。在这篇文章中,我只是选取了几个自己印象深刻的技术点,零零碎碎地写了很多,比较散。对于有这方面需求的人,会有借鉴意义。


作者:开发者如是说
来源:juejin.cn/post/7326268908984434697
收起阅读 »

threejs渲染高级感可视化风力发电车模型

web
本文使用threejs开发一款风力发电机物联可视化系统,包含着色器效果、动画、补间动画和开发过程中使用模型材质遇到的问题,内含大量gif效果图, 视频讲解及源码见文末 技术栈 three.js 0.165.0 vite 4.3.2 nodej...
继续阅读 »

本文使用threejs开发一款风力发电机物联可视化系统,包含着色器效果、动画、补间动画和开发过程中使用模型材质遇到的问题,内含大量gif效果图,



视频讲解及源码见文末



技术栈



  • three.js 0.165.0

  • vite 4.3.2

  • nodejs v18.19.0


效果图


一镜到底动画


一镜到底 (1).gif


切割动画


切割动画.gif


线稿动画


线稿动画.gif


外壳透明度动画


外壳透明度动画.gif


展开齿轮动画


展开齿轮动画.gif


发光线条动画


发光线条.gif


代码及功能介绍


着色器


文中用到一个着色器,就是给模型增加光感的动态光影


创建顶点着色器 vertexShader:


varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

创建片元着色器 vertexShader:


varying vec2 vUv;
uniform vec2 u_center; // 添加这一行

void main() {
// 泡泡颜色
vec3 bubbleColor = vec3(0.9, 0.9, 0.9); // 乳白色
// 泡泡中心位置
vec2 center = u_center;
// 计算当前像素到泡泡中心的距离
float distanceToCenter = distance(vUv, center);
// 计算透明度,可以根据实际需要调整
float alpha = smoothstep(0.1, 0.0, distanceToCenter);

gl_FragColor = vec4(bubbleColor, alpha);

创建着色器材质 bubbleMaterial


export const bubbleMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true, // 开启透明
depthTest: true, // 开启深度测试
depthWrite: false, // 不写入深度缓冲
uniforms: {
u_center: { value: new THREE.Vector2(0.3, 0.3) } // 添加这一行
},
});


从代码中可以看到 uniform声明了一个变量u_center,目的是为了在render方法中动态修改中心位置,从而实现动态光效的效果,


具体引用 render 方法中


 // 更新中心位置(例如,每一帧都改变)  
let t = performance.now() * 0.001;
bubbleMaterial.uniforms.u_center.value.x = Math.sin(t) * 0.5 + 0.5; // x 位置基于时间变化
bubbleMaterial.uniforms.u_center.value.y = Math.cos(t) * 0.5 + 0.5; // y 位置基于时间变化

官网案例 # Uniform,详细介绍了uniform的使用方法,支持通过变量对着色器材质中的属性进行改变


光影着色器.gif


从模型上可能看不出什么,下面的图是在一个圆球上加的这个效果


光影着色器-球体.gif


着色器中有几个参数可以自定义也可以自己修改, float alpha = smoothstep(0.6, 0.0, distanceToCenter);中的smoothstep 是一个常用的函数,用于在两个值之间进行平滑插值。具体来说,smoothstep(edge0, edge1, x) 函数会计算 x 在 edge0 和 edge1 之间的平滑过渡值。当 x 小于 edge0 时,返回值为 0;当 x 大于 edge1 时,返回值为 1;而当 x 在 edge0 和 edge1 之间时,它返回一个在 0 和 1 之间的平滑过渡值。


切割动画


切割动画使用的是数学库平面THREE.Plane和属性 constant,通过修改constant值即可实现动画,从normal法向量起至constant的距离为可展示内容。



从原点到平面的有符号距离。 默认值为 0.



constant取模型的box3包围盒的min值,至max值做补间动画,以下是代码示意


const wind = windGltf.scene
const boxInfo = wind.userData.box3Info;

const max = boxInfo.worldPosition.z + boxInfo.max.z
const min = boxInfo.worldPosition.z + boxInfo.min.z

let tween = new TWEEN.Tween({ d: min - 0.2 })
.to({ d: max + 0.1 }, 1000 * 2)
.start()
.onUpdate(({ d }) => {
clippingPlane.constant = d
})

详看切割效果图


切割动画.gif


图中添加了切割线的辅助线,可以通过右侧的操作面板显示或隐藏。


模型材质需要注意的问题


由于齿轮在风车的内容部,并且风车模型开启了transparent=true,那么计算透明度深度就会出现问题,首先要设置 depthWrite = true,开启深度缓存区,renderOrder = -1



这个值将使得scene graph(场景图)中默认的的渲染顺序被覆盖, 即使不透明对象和透明对象保持独立顺序。 渲染顺序是由低到高来排序的,默认值为0



threejs的透明材质渲染和不透明材质渲染的时候,会互相影响,而调整renderOrder顺序则可以让透明对象和不透明对象相对独立的渲染。


depthWrite对比


depthwrite对比.jpeg


renderOrder 对比


renderOrder 对比.jpeg


自定义动画贝塞尔曲线


众所周知,贝塞尔曲线通常用于调整关键帧动画,创建平滑的、曲线的运动路径。本文中使用的tweenjs就内置了众多的运动曲线easing(easingFunction?: EasingFunction): this;类型,虽然有很多内置,但是毕竟需求是无限的,接下来介绍的方法就是可以自己设置动画的贝塞尔曲线,来控制动画的执行曲线。


具体使用


// 使用示例
const controlPoints = [ { x: 0 }, { x: 0.5 }, { x: 2 }, { x: 1 }];
const cubicBezier = new CubicBezier(controlPoints[0], controlPoints[1], controlPoints[2], controlPoints[3]);

let tween = new TWEEN.Tween(edgeLineGr0up.scale)
.to(windGltf.scene.scale.clone().set(1, 1, 1), 1000 * 2)
.easing((t) => {
return cubicBezier.get(t).x
})
.start()
.onComplete(() => {
lineOpacityAction(0.3)
res({ tween })
})

在tween的easing的回调中添加一个方法,方法中调用了cubicBezier,下面就介绍一下这个方法


源码


[p0] – 起点  
[p1] – 第一个控制点
[p2] – 第二个控制点
[p3] – 终点

export class CubicBezier {
private p0: { x: number; };
private p1: { x: number; };
private p2: { x: number; };
private p3: { x: number; };

constructor(p0: { x: number; }, p1: { x: number; }, p2: { x: number; }, p3: { x: number; }) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
}

get(t: number): { x: number; } {
const p0 = this.p0;
const p1 = this.p1;
const p2 = this.p2;
const p3 = this.p3;

const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
const t2 = t * t;
const t3 = t2 * t;

const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;

return { x };
}
}


CubicBezier支持get方法,通过四个关键点位信息,绘制三次贝塞尔曲线,参数t在0到1之间变化,当t从0变化到1时,曲线上的点从p0平滑地过渡到p3


mt = 1 - t;:这是t的补数(1减去t)。
mt2 = mt * mt; 和 mt3 = mt2 * mt;:计算mt的平方和立方。
t2 = t * t; 和 t3 = t2 * t;:计算t的平方和立方。


这是通过取四个点的x坐标的加权和来完成的,其中权重是基于t的幂的。具体来说,p0的权重是(1-t)^3p1的权重是3 * (1-t)^2 * tp2的权重是3 * (1-t) * t^2,而p3的权重是t^3


{ x: 0 },{ x: 0.5 },{ x: 2 },{ x: 1 } 这组数据形成的曲线效果是由start参数到end的两倍参数再到end参数


具体效果如下


贝塞尔曲线.gif


齿轮


齿轮动画


模型中自带动画


齿轮动画数据.jpeg


源码中有一整套的动画播放类方法,HandleAnimation,其中功能包含播放训话动画,切换动画,播放一次动画,绘制骨骼,镜头跟随等功能。


具体使用方法:


   // 齿轮动画
/**
*
* @param model 动画模型
* @param animations 动画合集
*/

motorAnimation = new HandleAnimation(motorGltf.scene, motorGltf.animations)
// 播放动画 take 001 是默认动画名称
motorAnimation.play('Take 001')

在render中调用


motorAnimation && motorAnimation.upDate()

齿轮展开(补间动画)


补间动画在齿轮展开时调用,使用的tweenjs,这里讲一下定位运动后的模型位置,使用# 变换控制器(TransformControls),代码中有封装好的完整的使用方法,在TransformControls.ts中,包含同时存在轨道控制器时与变换控制器对场景操作冲突时的处理。


使用方法:


/**
* @param mesh 受控模型
* @param draggingChangedCallback 操控回调
*/

TransformControls(mesh, ()=>{
console.log(mesh.position)
})

齿轮展开定位.jpeg


齿轮发光


发光效果方法封装在utls/index.ts中的unreal方法,使用的是threejs提供的虚幻发光通道RenderPass,UnrealBloomPass,以及合成器EffectComposer,方法接受参数如下



// params 默认参数
const createParams = {
threshold: 0,
strength: 0.972, // 强度
radius: 0.21,// 半径
exposure: 1.55 // 扩散
};

/**
*
* @param scene 渲染场景
* @param camera 镜头
* @param renderer 渲染器
* @param width 需要发光位置的宽度
* @param height 发光位置的高度
* @param params 发光参数
* @returns
*/


调用方法如下:



const { finalComposer: F,
bloomComposer: B,
renderScene: R, bloomPass: BP } = unreal(scene, camera, renderer, width, height, params)
finalComposer = F
bloomComposer = B
renderScene = R
bloomPass = BP
bloomPass.threshold = 0


除了调用方法还有一些需要调整的地方,比如发光时模型什么材质,又或者不发光时又是什么材质,这里需要单独定义,并在render渲染函数中调用


 if (guiParams.isLight) {
if (bloomComposer) {
scene.traverse(darkenNonBloomed.bind(this));
bloomComposer.render();
}
if (finalComposer) {
scene.traverse(restoreMaterial.bind(this));
finalComposer.render();
}
}

scene.traverse的回调中,检验模型是否为发光体,再进行材质的更换,这里用的标识是 object.userData.isLighttrue时,判定该物体为发光物体。其他物体则不发光


回调方法


function darkenNonBloomed(obj: THREE.Mesh) {
if (bloomLayer) {
if (!obj.userData.isLight && bloomLayer.test(obj.layers) === false) {
materials[obj.uuid] = obj.material;
obj.material = darkMaterial;
}
}

}

function restoreMaterial(obj: THREE.Mesh) {
if (materials[obj.uuid]) {
obj.material = materials[obj.uuid];
// 用于删除没必要的渲染
delete materials[obj.uuid];
}
}


再场景的右上角我们新增了几个参数,用来调整线条的发光效果,下面通过动图看一下,图片有点大,请耐心等待加载


调试发光效果.gif


好啦,本篇文章到此,如看源码有不明白的地方,可私信~


最近正在筹备工具库,以上可视化常用的方法都将涵盖在里面


历史文章


three.js——商场楼宇室内导航系统 内附源码


three.js——可视化高级涡轮效果+警报效果 内附源码


高德地图巡航功能 内附源码


three.js——3d塔防游戏 内附源码


three.js+物理引擎——跨越障碍的汽车 可操作 可演示


百度地图——如何计算地球任意两点之间距离 内附源码


threejs——可视化地球可操作可定位


three.js 专栏


源码及讲解



源码 http://www.aspiringcode.com/content?id=…


体验地址:display.aspiringcode.com:8888/html/171422…


作者:孙_华鹏
来源:juejin.cn/post/7379906492038889512
收起阅读 »

都说PHP性能差,但PHP性能真的差吗?

今天本能是想测试一个PDO持久化,会不会带来会话混乱的问题 先贴一下PHP代码, 代码丑了点,但是坚持能run就行,反正就是做个测试。 <?php $dsn = 'mysql:host=localhost;dbname=test;charset=utf8...
继续阅读 »

今天本能是想测试一个PDO持久化,会不会带来会话混乱的问题
先贴一下PHP代码, 代码丑了点,但是坚持能run就行,反正就是做个测试。


<?php
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$user = 'root';
$password = 'root';

// 设置 PDO 选项,启用持久化连接
$options = [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
];

try {
// 创建持久化连接
$pdo = new PDO($dsn, $user, $password, $options);

$stmt = $pdo->prepare("INSERT INTO test_last_insert_id (uni) VALUES (:uni);");
$uni = uniqid('', true);
$stmt->bindValue(':uni', $uni);
$aff = $stmt->execute(); //
if ($aff === false) {
throw new Exception("insert fail:");
}
$id = $pdo->lastInsertId();


function getExecutedSql($stmt, $params)
{
$sql = $stmt->queryString;
$keys = array();
$values = array();

// 替换命名占位符 :key with ?
$sql = preg_replace('/\:(\w+)/', '?', $sql);

// 绑定的参数可能包括命名占位符,我们需要将它们转换为匿名占位符
foreach ($params as $key => $value) {
$keys[] = '/\?/';
$values[] = is_string($value) ? "'$value'" : $value;
}

// 替换占位符为实际参数
$sql = preg_replace($keys, $values, $sql, 1, $count);

return $sql;
}


$stmt = $pdo->query("SELECT id FROM test_last_insert_id WHERE uni = '{$uni}'", PDO::FETCH_NUM);
$row = $stmt->fetch();
$value = $row[0];
if ($value != $id) {
throw new Exception("id is diff");
}

echo "success" . PHP_EOL;

} catch (PDOException $e) {
header('HTTP/1.1 500 Internal Server Error');
file_put_contents('pdo_perisistent.log', $e->getMessage() . PHP_EOL);
die('Database connection failed: ' . $e->getMessage());
} catch (Exception $e) {
header('HTTP/1.1 500 Internal Server Error');
file_put_contents('pdo_perisistent.log', $e->getMessage() . PHP_EOL);
die('Exception: ' . $e->getMessage());
}

用wrk压测,一开始uniqid因为少了混淆参数还报了500,加了一下参数,用来保证uni值


% ./wrk -c100 -t2 -d3s --latency  "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.17ms 7.48ms 103.38ms 80.57%
Req/Sec 0.96k 133.22 1.25k 75.81%
Latency Distribution
50% 51.06ms
75% 54.17ms
90% 59.45ms
99% 80.54ms
5904 requests in 3.10s, 1.20MB read
Requests/sec: 1901.92
Transfer/sec: 397.47KB

1900 ~ 2600 之间的QPS,其实这个数值还是相当满意的,测试会话会不会混乱的问题也算完结了。
但是好奇心突起,之前一直没做过go和php执行sql下的对比,正好做一次对比压测


package main

import (
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"time"

_ "github.com/go-sql-driver/mysql"
"log"
)

var id int64 = time.Now().Unix() * 1000000

func generateUniqueID() int64 {
return atomic.AddInt64(&id, 1)
}

func main() {
dsn := "root:root@tcp(localhost:3306)/test?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer func() { _ = db.Close() }()

//// 设置连接池参数
//db.SetMaxOpenConns(100) // 最大打开连接数
//db.SetMaxIdleConns(10) // 最大空闲连接数
//db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var err error
uni := generateUniqueID()

// Insert unique ID int0 the database
insertQuery := `INSERT INTO test_last_insert_id (uni) VALUES (?)`
result, err := db.Exec(insertQuery, uni)
if err != nil {
log.Fatalf("Error inserting data: %v", err)
}

lastInsertID, err := result.LastInsertId()
if err != nil {
log.Fatalf("Error getting last insert ID: %v", err)
}

// Verify the last insert ID
selectQuery := `SELECT id FROM test_last_insert_id WHERE uni = ?`
var id int64
err = db.QueryRow(selectQuery, uni).Scan(&id)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}

if id != lastInsertID {
log.Fatalf("ID mismatch: %d != %d", id, lastInsertID)
}

fmt.Println("success")
})

_ = http.ListenAndServe(":8080", nil)

}

truncate表压测结果,这低于预期了吧


% ./wrk -c100 -t2 -d3s --latency  "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.05ms 36.86ms 308.57ms 80.77%
Req/Sec 0.98k 243.01 1.38k 63.33%
Latency Distribution
50% 43.70ms
75% 65.42ms
90% 99.63ms
99% 190.18ms
5873 requests in 3.01s, 430.15KB read
Requests/sec: 1954.08
Transfer/sec: 143.12KB

开个连接池,清表再测,结果半斤八两


% ./wrk -c100 -t2 -d3s --latency  "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.07ms 35.87ms 281.38ms 79.84%
Req/Sec 0.97k 223.41 1.40k 60.00%
Latency Distribution
50% 44.91ms
75% 66.19ms
90% 99.65ms
99% 184.51ms
5818 requests in 3.01s, 426.12KB read
Requests/sec: 1934.39
Transfer/sec: 141.68KB

然后开启不清表的情况下,php和go的交叉压测


% ./wrk -c100 -t2 -d3s --latency  "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.51ms 43.28ms 436.00ms 86.91%
Req/Sec 1.08k 284.67 1.65k 65.00%
Latency Distribution
50% 40.22ms
75% 62.10ms
90% 102.52ms
99% 233.98ms
6439 requests in 3.01s, 471.61KB read
Requests/sec: 2141.12
Transfer/sec: 156.82KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 41.41ms 10.44ms 77.04ms 78.07%
Req/Sec 1.21k 300.99 2.41k 73.77%
Latency Distribution
50% 38.91ms
75% 47.62ms
90% 57.38ms
99% 69.84ms
7332 requests in 3.10s, 1.50MB read
Requests/sec: 2363.74
Transfer/sec: 493.98KB

// 这里骤降是我很不理解的不明白是因为什么
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 156.72ms 75.48ms 443.98ms 66.10%
Req/Sec 317.93 84.45 480.00 71.67%
Latency Distribution
50% 155.21ms
75% 206.36ms
90% 254.32ms
99% 336.07ms
1902 requests in 3.01s, 139.31KB read
Requests/sec: 631.86
Transfer/sec: 46.28KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 43.47ms 10.04ms 111.41ms 90.21%
Req/Sec 1.15k 210.61 1.47k 72.58%
Latency Distribution
50% 41.17ms
75% 46.89ms
90% 51.27ms
99% 95.07ms
7122 requests in 3.10s, 1.45MB read
Requests/sec: 2296.19
Transfer/sec: 479.87KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 269.08ms 112.17ms 685.29ms 73.69%
Req/Sec 168.22 125.46 520.00 79.59%
Latency Distribution
50% 286.58ms
75% 335.40ms
90% 372.61ms
99% 555.80ms
1099 requests in 3.02s, 80.49KB read
Requests/sec: 363.74
Transfer/sec: 26.64KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 41.74ms 9.67ms 105.86ms 91.72%
Req/Sec 1.20k 260.04 2.24k 80.33%
Latency Distribution
50% 38.86ms
75% 46.77ms
90% 49.02ms
99% 83.01ms
7283 requests in 3.10s, 1.49MB read
Requests/sec: 2348.07
Transfer/sec: 490.71KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 464.85ms 164.66ms 1.06s 71.97%
Req/Sec 104.18 60.01 237.00 63.16%
Latency Distribution
50% 467.00ms
75% 560.54ms
90% 660.70ms
99% 889.86ms
605 requests in 3.01s, 44.31KB read
Requests/sec: 200.73
Transfer/sec: 14.70KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 50.62ms 9.16ms 85.08ms 75.74%
Req/Sec 0.98k 170.66 1.30k 69.35%
Latency Distribution
50% 47.93ms
75% 57.20ms
90% 61.76ms
99% 79.90ms
6075 requests in 3.10s, 1.24MB read
Requests/sec: 1957.70
Transfer/sec: 409.13KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 568.84ms 160.91ms 1.04s 66.38%
Req/Sec 81.89 57.59 262.00 67.27%
Latency Distribution
50% 578.70ms
75% 685.85ms
90% 766.72ms
99% 889.39ms
458 requests in 3.01s, 33.54KB read
Requests/sec: 151.91
Transfer/sec: 11.13KB

go 的代码随着不断的测试,很明显处理速度在不断的下降,这说实话有点超出我的认知了。
PHP那边却是基本稳定的,go其实一开始我还用gin测试过,发现测试结果有点超出预料,还改了用http库来测试,这结果属实差强人意了。


突然明白之前经常看到别人在争论性能问题的时候,为什么总有人强调PHP性能并不差。
或许PHP因为fpm的关系导致每次加载大量文件导致的响应相对较慢,比如框架laravel 那个QPS只有一两百的家伙,但其实这个问题要解决也是可以解决的,也用常驻内存的方式就好了。再不行还有phalcon


我一直很好奇一直说PHP性能问题的到底是哪些人, 不会是从PHP转到其他语言的吧。


% php -v
PHP 8.3.12 (cli) (built: Sep 24 2024 18:08:04) (NTS)
Copyright (c) The PHP Gr0up
Zend Engine v4.3.12, Copyright (c) Zend Technologies
with Xdebug v3.3.2, Copyright (c) 2002-2024, by Derick Rethans
with Zend OPcache v8.3.12, Copyright (c), by Zend Technologies

% go version
go version go1.23.1 darwin/amd64

image.png


这结果,其实不太能接受,甚至都不知道原因出在哪了,有大佬可以指出问题一下吗


加一下时间打印再看看哪里的问题


package main

import (
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"time"

_ "github.com/go-sql-driver/mysql"
"log"
)

var id int64 = time.Now().Unix() * 1000000

func generateUniqueID() int64 {
return atomic.AddInt64(&id, 1)
}

func main() {
dsn := "root:root@tcp(localhost:3306)/test?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer func() { _ = db.Close() }()

// 设置连接池参数
db.SetMaxOpenConns(100) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
reqStart := time.Now()
var err error
uni := generateUniqueID()

start := time.Now()
// Insert unique ID int0 the database
insertQuery := `INSERT INTO test_last_insert_id (uni) VALUES (?)`
result, err := db.Exec(insertQuery, uni)
fmt.Printf("insert since: %v uni:%d \n", time.Since(start), uni)
if err != nil {
log.Fatalf("Error inserting data: %v", err)
}

lastInsertID, err := result.LastInsertId()
if err != nil {
log.Fatalf("Error getting last insert ID: %v", err)
}

selectStart := time.Now()
// Verify the last insert ID
selectQuery := `SELECT id FROM test_last_insert_id WHERE uni = ?`
var id int64
err = db.QueryRow(selectQuery, uni).Scan(&id)
fmt.Printf("select since:%v uni:%d \n", time.Since(selectStart), uni)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}

if id != lastInsertID {
log.Fatalf("ID mismatch: %d != %d", id, lastInsertID)
}

fmt.Printf("success req since:%v uni:%d \n", time.Since(reqStart), uni)
})

_ = http.ListenAndServe(":8080", nil)

}

截取了后面的一部分输出,这不会是SQL库的问题吧,


success req since:352.310146ms uni:1729393975000652 
insert since: 163.316785ms uni:1729393975000688
insert since: 154.983173ms uni:1729393975000691
insert since: 158.094503ms uni:1729393975000689
insert since: 136.831695ms uni:1729393975000697
insert since: 141.857079ms uni:1729393975000696
insert since: 128.115216ms uni:1729393975000702
select since:412.94524ms uni:1729393975000634
success req since:431.383768ms uni:1729393975000634
select since:459.596445ms uni:1729393975000601
success req since:568.576336ms uni:1729393975000601
insert since: 134.39147ms uni:1729393975000700
select since:390.926517ms uni:1729393975000643
success req since:391.622183ms uni:1729393975000643
select since:366.098937ms uni:1729393975000648
success req since:373.490764ms uni:1729393975000648
insert since: 136.318919ms uni:1729393975000699
select since:420.626209ms uni:1729393975000640
success req since:425.243441ms uni:1729393975000640
insert since: 167.181068ms uni:1729393975000690
select since:272.22808ms uni:1729393975000671

单次请求的时候输出结果是符合预期的, 但是并发SQL时会出现执行慢的问题,这就很奇怪了


% curl localhost:8080
insert since: 1.559709ms uni:1729393975000703
select since:21.031284ms uni:1729393975000703
success req since:22.62274ms uni:1729393975000703

经群友提示还和唯一键的区分度有关,两边算法一致有点太难了,Go换了雪法ID之后就正常了。
因为之前 Go这边生成的uni值是递增的导致区分度很低,最终导致并发写入查询效率变低。


% ./wrk -c100 -t2 -d3s --latency  "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 44.51ms 24.87ms 187.91ms 77.98%
Req/Sec 1.17k 416.31 1.99k 66.67%
Latency Distribution
50% 37.46ms
75% 54.55ms
90% 80.44ms
99% 125.72ms
6960 requests in 3.01s, 509.77KB read
Requests/sec: 2316.02
Transfer/sec: 169.63KB

2024-10-23 更新


今天本来是想验证一下有关,并发插入自增有序的唯一键高延迟的问题,发现整个有问题的只有一行代码。
就是在查询时,类型转换的问题,插入和查询都转换之后,空表的情况下QPS 可以到4000多。即使在已有大数据量(几十万)的情况也有两千多的QPS。
现在又多了一个问题,为什么用雪花ID时不会有这样的问题。雪花ID也是int64类型的,这是为什么呢。


// 旧代码
err = db.QueryRow(selectQuery, uni).Scan(&id)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}


// 新代码 变化只有一个就是把uni 转成字符串之后就没有问题了
var realId int64
err = db.QueryRow(selectQuery, fmt.Sprintf("%d", uni)).Scan(&realId)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}

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

AI 治好了我的 CSS 框架恐惧症

web
00. 写在前面 大家好,我是大家的林语冰。 前端工程中,苦于“前 CSS3 时代”相对落后的原生语法,CSS 架构一直是前端架构师的痛点之一。 因此,我们一般会在项目里引入更先进的 CSS 框架或预处理器,比如国内比较火的 SCSS/LESS,或者海外人气爆...
继续阅读 »

00. 写在前面


大家好,我是大家的林语冰。


前端工程中,苦于“前 CSS3 时代”相对落后的原生语法,CSS 架构一直是前端架构师的痛点之一。


因此,我们一般会在项目里引入更先进的 CSS 框架或预处理器,比如国内比较火的 SCSS/LESS,或者海外人气爆棚的 Tailwind CSS/UnoCSS 等。


00-css.png


问题在于,当我们学习从原生 CSS 升级到 SCSS,或者老板要求从 SCSS 迁移到人气更高的 Tailwind 框架时,不同 CSS 框架的学习成本也不容小觑。


本质上而言,这些 CSS 框架提供的高级语法最终都会被转译为原生 CSS,而这种语法转换工作恰恰是 AI 编程助手的拿手好戏。


所以,本期我想分享如何利用 VSCode 和 MarsCode AI 插件,在原生 CSS 和不同 CSS 框架中无缝衔接,直接让 AI 解放我们的双手,不必再因为不同的 CSS 框架而头大。


01. 前期准备


本文的示例代码是用原生 CSS 实现一个仿真的 iPhone 手机,类似的产品模型网页预览效果在很多电商网站都比较常见,最终实现效果如下所示:


01-iphone.png


上述手机模型对应的原生的 HTML 结构和 CSS 代码如下:


(PS:此处代码仅供参考,大家可以用自己的样式代码进行后续测试,不需要关注这里的代码细节)


02-html.png


03-css.png


02. VSCode AI 插件


假设上述示例是项目遗留的旧代码,我们想要使用其他 CSS 框架重构为可维护的高级样式代码,就需要和 AI 助手联动,让 AI 帮我们写代码。


首先,我们需要可以使用手机号或邮箱注册一个账号,然后在 VSCode 里搜索和安装 MarsCode 扩展插件,登录后就可以在 VSCode 里直接使用 AI 编程助手。


另外,豆包 MarsCode 使用的是字节跳动的国产大模型,所以我们不需要考虑科学上网等复杂问题。


接着就可以让 AI 干活了,我们可以把原生 CSS 抽离到单独的样式文件中,然后让 AI 把它转译为 SCSS 版本,只需要通过聊天的方式命令 AI 执行任务即可,不需要我们手动敲一行代码。


04-ai.png


MarsCode 比较人性化的一点是,生成的代码可以直接一键保存到新文件中,然后我们可以测试生成的 SCSS 代码是否和原生版本等效,如果效果有偏差,可以尝试多生成几次。


我这里生成的 SCSS 代码也可以正常工作,因为样式逻辑并不复杂,但所有原生 CSS 都被重构为 SCSS 的嵌套语法。


毋庸置疑,在代码编译或重构方面,AI 可以明显提高我们的生产力,哪怕是复杂的样式代码也不例外。


03. 样式构建


目前前端工程中,大部分项目可能会依赖 Vite 工具链构建,因此我们也可以引入 Vite,再集成需要的 CSS 框架。


Vite 配置在官方文档有具体介绍,以 SCSS 为例,我们需要安装模块,然后更改配置文档。


05-vite.png


实际测试中,我偷懒不看文档,而是直接询问 AI 助手如何配置,MarsCode 虽然给出了答案,但是答案未必有效,可能出现配置失败,或者配置生效,但不是最佳配置的情况,我猜可能跟目前 MarsCode 的预训练模型的局限性有关。


这也说明和 AI 编程助手一起使用时,我们最好还是有对应 CSS 框架的知识储备,才能放心地偷懒,遇到 bug 也能了然于胸。


另外,在 CSS 框架选型方面,目前我更推荐 UnoCSS,因为它是一个同构引擎,这意味着,UnoCSS 默认兼容 Tailwind 同款语法,也能够支持类似 SCSS 的功能,更加通用。


在 AI 生成代码过程中,不同 CSS 框架语法本身不会给 AI 带来太大负担,我们同样只需要通过对话,就能生成对应框架的代码。


比如我让 MarsCode 生成的 UnoCSS/Tailwind 代码,也能一键实现相同的样式效果。


06-uno.png


高潮总结


CSS 框架或预处理器的本质是提供了某些比原生 CSS 高级的语法,方便我们在前端工程中实现可维护的样式架构,但它们最终还是要编译为原生 CSS。


一般而言,在不同的 CSS 框架中迁移,我们需要重新学习和手动重构,AI 编程助手可以辅助我们一键迁移。


在 VSCode 中,我们可以借助 MarsCode 插件,轻松地将原生 CSS 代码重构为不同 CSS 框架的代码,无需手动敲一行代码,这提高了我们的开发效率,但同时也要注意 AI 工具的局限性。


目前 AI 无法淘汰程序员,但 AI 会淘汰不懂 AI 的程序员。你可以注册和安装 VSCode 插件,在 VSCode 中提前尝试 AIGC 时代的低代码编程方式。


官方链接和二维码在这里分享给大家:http://www.marscode.cn/events/s/ik…


#豆包MarsCode 双节创意征文话题 #豆包 MarsCode 放码过来


poster.png


作者:前端俱乐部
来源:juejin.cn/post/7424016262094012443
收起阅读 »

纠结多年终于决定彻底放弃Tailwindcss

web
上图来源Fireship 团队代价=深入成本 Tailwindcss很容易火,因为从看到它的的第一眼就会觉得超级直观、超级简单、超级方便,所以无论是写文章 出视频 都很容易吸引一波流量。 但是真要用在企业级应用我们需要考虑全部的复杂性,你至少要吃透官方文档的...
继续阅读 »

image.png



上图来源Fireship



团队代价=深入成本


Tailwindcss很容易火,因为从看到它的的第一眼就会觉得超级直观、超级简单、超级方便,所以无论是写文章 出视频 都很容易吸引一波流量。


但是真要用在企业级应用我们需要考虑全部的复杂性,你至少要吃透官方文档的大部分内容,最好是写几个稍微复杂点的Demo,光是吃透文档就需要至少10小时以上的成本你才能彻底在企业级应用all in tailwind,如果你的团队有10名前端同学,你们将会付出100个小时的代价,这些代价不光是个人的,更是企业的损失,而花了这100小时掌握之后能够靠那一点点便捷提速弥补损失吗?不能。或许100小时早就用以前的方式写完了全部样式。团队还会扩大,新招进来的同学还得培训一下。


范式强偏好


Tailwindcss是非常opinionated强偏好的,他会鼓励一种他们特定的范式和规则,这种规则不是通用的,是tw创新的。那scss less是不是强偏好呢?不是,因为你还是以标准的css范式书写,只是scss less给你提供了额外的语法和工具方法 你的范式没有改变。


tw强偏好的范式包括不限于:



  • tailwindconfig 配置文件

  • 默认主题、工具类

  • 行内class书写规则

  • IDE插件


强偏好本身没有对错之分,通常我们使用UI组件库就是强偏好的,但是对于样式的书写,这种强偏好会缺少一定的规范一致性,说白了就是潜规则太多了。


强IDE插件依赖


没有IDE的插件提示,tw基本不可用,因为你可用的类名强依赖与上面说的范式中的tailwindconfig配置文件。


但是这好像也没什么问题,装个插件也很轻松,事情没这么简单,你失去了静态类型检查 和可测试性。假设你写了个错误的类名shadows,tailwind插件可不会给你报红,而传统的css样式文件是会报红的。


image.png


image.png


既然静态阶段无法发现这个错误,那编译时能不能发现呢?也不能,tw会将主题中未定义的类名 当成你自己的类名,所以对tw来说不是错误。


单元测试?很遗憾,也不行,这个范式最大的好处也是最大的缺点,样式全部在类目中,你不可能去equal所有的类名 这样就没有用tw的意义了。


所以tw最方便的地方,也是最容易出错且难以被发现的地方。


完全错误的主题范式


官方文档提供了Dark Mode暗色主题切换的方式,但是如果现在客户提个需求,需要增加4套颜色主题 和亮暗色无关 就是额外的主题,你会发现tw根本没有考虑到这点(或者说很难实现,网上几乎没有解决方案,我有但我不说😝(下面补充解释了


tw是通过类名中以dark:前缀开头来表示暗色下的样式,默认不加就是亮色, 所以你根本无法增加这两种主题以外的更多主题,你只能在亮色暗色这两之间切换,这就是tw官方强偏好导致的弊端。


我们假设,即使tw实现了可以增加主题前缀比如 onedark: monokai: ...,那么你需要在每一个元素类名上 书写所有这些前缀的样式


<div
className="
bg-blue-500 //亮色
dark:bg-blue-700 //暗色
onedark:bg-black-900 //onedark主题
monokai:bg-gold-600 //monokai主题
kanagawa:bg-green-200 //kanagawa主题
"

></div>

真的你会写疯掉,因为每增加一个主题,意味着你要在源码中所有元素身上加上新的主题和样式类名,想象一下如果有20个主题,你一个标签的类名可能就占了100行。


并且你无法动态增加主题,因为tw是编译时的,生产环境下,你无法实现让用户自己任意配置主题、持久、载入这样的功能。


总结


文章还会更新,想当什么补充什么。以上几个最大的痛点是导致我对这个库关注多年,尝试多次,却迟迟没有投入使用,最终决定放弃的原因。我相信肯定很多同学会有同感,也会有很多持反对意见,非常欢迎评论区讨论,如果真能解决这几个大痛点,我会毅然决然All in tw。




↓↓↓↓🆕 以下为更新内容 时间升序↓↓↓↓


2024-08-22 17:45:21


难以调试


image.png



实现复杂UI会让类名又臭又长,无法根据类名理解样式,影响对html结构的浏览


image.png


来对比看一下传统类名的可读性,这是MDN网站的类名,干净整洁,一眼就知道每一个标签块代表什么内容



类名即样式导致dev tool中无法通过style面板根据特定一类元素修改样式,因为你改的是工具类名 而不是一类元素的类名,例如修改.text-left {text-align: right} 会将所有元素的样式修改完全不符合预期


菜就多练?


好吧我猜到评论区会有此类不和谐的声音,怪我没有事先叠甲,但是文章的开始其实已经说的很清楚了,个人能力再强是没用的,开发从来不是一个人的事,在公司需要跟同事配合,在开源社区需要和世界各地的开源爱好者协作。


如果你是组长、技术经理、CTO甚至老板,你一定要站在团队的角度对新兴技术栈评估收益比,因为对于企业来说商业价值永远是第一位的,所以你不能只考虑自己的效率,还要考虑团队整体的效率和质量。


如果你是开源作者,你也要为贡献者的参与门槛考虑,如果你的技术栈不是主流 只是一小挫人会用 甚至难度极高,那么你很难收获世界各地的爱心,只能自己一个人默默发电。你甚至要考虑技术栈的可替换性,因为我们大部分的依赖库都是开源的,人家也是为爱发电,意味着人家很有可能哪天累了不再维护了,你要留足能够用其他库或框架平滑替换的可能,否则为了某个库的废弃你可能需要做大量的重构工作甚至Breaking Change破坏性升级,再甚至你也没办法坚持下去了,因为你花了大量的时间在填坑而不是专注于自己项目的开发。


复杂性守恒原理


泰斯勒定律 复杂性守恒,临界复杂性不能凭空消失 只能被转移。我经常用这个原理审视各种新兴技术栈的两面性,因为你们懂得-前端娱乐圈,经常会出现很多让你快乐的新东西,而往往容易忽视背后的代价。当我们收获巨大的简化后,一定要思考 曾经的复杂性被转移到哪里去了呢?如果你能搞清楚两面性,仔细评估后再做决定,会走的更顺。


就如上面所述,tw在简化的背后,牺牲了静态类型检查、单元测试、调试、运行时动态主题载入、文档强依赖、IDE插件强依赖、构建工具强依赖等等诸多缺点。


2024-08-23 17:53:51


关于tw错误主题范式的补充



掘友还是很多大佬的,评论区发表了很多关于解决主题受限于默认和dark这两种的局限,这里补充一下我自己的方式

tw配置文件 theme中不配置darkMode,将所有主题值绑定css变量如 colors: {primary: 'var(--primary)'},然后依然是动态控制css变量来切换主题。


坏处很多: classname不再允许使用dark变量;tw配置麻烦,变量套变量需要两边维护,css变量文件 和tw配置要保持同步;


我为什么前面不把解决办法说出来?因为不重要。


tw是范式强偏好的,首先理解什么是范式?我的理解是很有fan的方式就是范式,有fan的前提是流行、统一。


所以我们在使用强偏好的库时,一定要在范式之内,不要自己创新方式,否则你会脱离流行、统一、一致性,并且会因为库的迭代升级导致适配问题,并且会与其他人无法达成共识 你们之间的技术经验产生越来越大的偏差 为合作带来困扰。



如果我遵循tw范式不魔改,与别人协作只需要告诉别人“看tw文档就行了”;但如果我不遵循,魔改了主题范式,我需要格外提醒别人“注意! 我们的主题不是按tw用的 请不要写dark:xxx”


这还只是一例,如果项目中有10例你自己创新的范式别人就很难快速上手了



2024-08-24 00:36:52


与tw同样方便的CSS in JS用法


image.png


上图是react中使用styled-component(后面简称sc)结合style工具写的一段样式,它既能拥有sc组件即样式的好处 又能拥有类似vue中样式分离且scoped的方便。


还能倒过来,jsx在上面,style在下面


image.png


通用组件通常用sc组件即样式来定义


const Button = styled.button`...`

view组件(业务域)通常结合sc组件和style来灵活使用


...
return style`font-size: 12px;`(
<div>...</div>
)

我们还能将常用样式抽离出来,达到如同tw的方便程度


return style`
${[button.md, bg.200, text.color.primary]}
`
(<Button>...</Button>)

如果样式很长,你可以抽离,也可以直接折叠,完全不需要像tw那样还需要vscode插件


image.png


篇幅关系,只是简单介绍,后续可能单独出个文章细讲css in js


2024-08-27 12:52:07


@apply解决类名过多的问题?


评论区出现多个建议用@apply在css复用类名的样式来减少class中书写类名,因此觉得有必要单独拿出来讲一下给大家避坑。


结论是千万万万不要这么用!这就是文档没看完,上手就用犯的错误,10小时的学习成本你是逃不掉的,不然以后麻烦就会找上你。


在tw官方文档中明确强调不要以减少类名为目的使用@apply,鼓励你就把所有类名写在class中。


image.png



上图来源 tailwindcss.com/docs/reusin…



除此之外,@apply语法其实是一个被废弃的css标准语法,曾经在chromium内核中被短暂实现过,后来废弃掉了,废弃的原因也是因为会破坏常规书写类名+样式的范式,会导致用户无节制的重用类名样式,最终无法溯源,修改困难。


image.png


image.png


作者:于谦
来源:juejin.cn/post/7405449753741328393
收起阅读 »

第三届OpenHarmony技术大会|Watch生态分论坛成功举行

2024年10月12日第三届OpenHarmony技术大会在上海成功举办。开源四年以来,OpenAtom OpenHarmony(以下简称“OpenHarmony”)不仅在覆金融,电力,交通,教育,医疗,航天等众多国计民生行业,也在消费电子领域逐步确定影响力,...
继续阅读 »

2024年10月12日第三届OpenHarmony技术大会在上海成功举办。开源四年以来,OpenAtom OpenHarmony(以下简称“OpenHarmony”)不仅在覆金融,电力,交通,教育,医疗,航天等众多国计民生行业,也在消费电子领域逐步确定影响力,OpenHarmony项目群工作委员会主席龚体在大会上给大家分享了OpenHarmony在穿戴领域的突破和发展。

本次大会专门为穿戴产业安排了生态分论坛,论坛聚集了穿戴产业上下游伙伴,有芯片商,解决方案商,品牌商,应用厂商,设计厂家,Top独立开发者以及应用分发商,与会产业大佬们分享了基于OpenHarmony做穿戴产品的经验,并对当前穿戴行业遇到的问题展开了讨论,对穿戴产业未来的发展做了展望。 这不仅为OpenHarmony在穿戴领域的开源生态拓展提供了重要参考,也为行业合作与创新开辟了新的思路,必将推动智能穿戴设备的跨越式发展。

华为OpenHarmony使能部副部长李彦举主持了本次论坛。OpenHarmony社区执行总监陶铭;华为OpenHarmony使能专家黎亮齐;华为技术专家,OpenHarmony兼容性工作组成员纪永;华为穿戴软件架构师段谦;上海海思穿戴芯片产品薛总监;恒玄科技商务拓展副总裁高亢;深圳市领为创新科技有限公司CEO林义巡;深圳市魔样科技股份有限公司执行总裁黄立阳;深圳市岍丞技术有限公司总经理张昊;Gomore inc.执行长郭信甫;OpenHarmony项目群技术指导委员会委员、深圳鸿信智联数字科技有限公司CEO张兆生;上海喜马拉雅科技有限公司IOT事业部副总经理娄建林;TOP独立开发者李尚儒;深圳仪品信息技术有限公司设计总监董奎出席本场论坛并发表主题演讲。

(华为OpenHarmony使能部副部长李彦举发言)

OpenHarmony社区执行总监陶铭即以《OpenHarmony赋能穿戴行业,开启智能新篇章》为主题发表报告。演讲中陶铭对当前OpenHarmony助力穿戴生态伙伴实现技术领先,商业成功做出了肯定,对OpenHarmony在穿戴行业做出更大的成绩很期待,陶总监表示OpenHarmony社区将持续支持穿戴行业更大发展。

(OpenHarmony社区执行总监陶铭发言)

华为OpenHarmony的使能专家黎亮齐强调了智能穿戴设备统一架构的重要性,他指出统一架构对屏蔽不同平台和设备之间的内部差异,确保各类应用和服务的兼容具有非常重要意义。通过基于统一的架构方案,API标准化,互联标准化,统一工具,兼容性评测,以及统一的应用市场,可以有效解决当前穿戴北向生态的问题,从而提升行业效能,降低生态伙伴成本。 这一生态战略不仅能促进行业伙伴合作,而且能推动智能穿戴设备行业的良性发展。他表示:独行快,众行远,期待更多的生态伙伴一起把穿戴行业做的更大,事业做的更长久。

(华为OpenHarmony使能专家黎亮齐发言)

华为技术专家、OpenHarmony兼容性工作组成员纪永介绍了当前兼容性工作组的职责,对OpenHarmony兼容性评测进行了解读,他强调:兼容性测评服务是看护OpenHarmony生态的重要手段,针对穿戴设备品类,兼容性工作组专门制定一套行之有效的测评标准,并与OpenHarmony 5.0一起发布,发布后可以有效防止生态的分裂,并将助力穿戴行业更健康发展。

(华为技术专家、OpenHarmony兼容性工作组成员纪永发言)

华为穿戴软件架构师段谦围绕OpenHarmony助力华为穿戴产品软件平台构建,分享了华为基于OpenHarmony构建的支持穿戴多系列产品的软件平台和华为穿戴今年秋季发布的玄玑感知系统,段谦的介绍让与会者了解到OpenHarmony底座能力、生态和产品三者的相辅相成,在相互协同前进的过程中,可以不断给消费者提供满意的产品。 段谦表示华为穿戴将会和业界一起持续的支持和贡献OpenHarmony社区,支持穿戴产业的发展。

(华为穿戴软件架构师段谦)

面向穿戴迈入OpenHarmony时代,上海海思分享了基于OpenHarmony在智能穿戴产业的探索与创新,构建底层芯片底座。上海海思W610 是首家支持OpenHarmony智能穿戴解决方案,具有卓越处理能力、领先连接和定位能力、智能化语音图片交互等特性,助力产业上下游做出更有竞争力的产品,大会现场上海海思展台展示了伙伴(魔样,领为,北斗,宜准,酷泰丰,库觅等)的10+款商用产品,此外百度、喜马拉雅、腕上阅读等多家应用厂家也展示了他们在穿戴上的适配效果。未来上海海思也将全力的支持OpenHarmony 穿戴产业的发展,引领智能穿戴将会朝着更智能、更强连接、更大生态方向演进。

(上海海思穿戴芯片产品薛总监发言)

恒玄科技商务拓展副总裁高亢强调了发展穿戴生态的重要意义,并在本次论坛中分享了恒玄科技基于OpenHarmony在可穿戴方案应用探索。恒玄科技基于OpenHarmony的技术底座,进行了多方面的探索和推进工作,致力于提升产品的功能和用户体验。这些努力不仅推动了企业自身的创新,也为整个穿戴设备的生态发展注入了新活力,他说:芯片 + OpenHarmony + 行业伙伴一定会实现1+1+1>3的效果。

(恒玄科技商务拓展副总裁高亢发言)

深圳市领为创新科技有限公司CEO林义巡以《依托OpenHarmony生态打造智能穿戴科技第三极》为主题发表演讲。他分享了领为创新科技在智能手表上的多年耕耘历程,也对比剖析了海思芯片和OpenHarmony的优势:开放、包容、互通互联、潜力无限;领为科技为更是为大会带来了多款基于海思W610+OpenHarmony的商用穿戴展品,他对OpenHarmony助力穿戴第三级品牌崛起有很强的信心。

(深圳市领为创新科技有限公司CEO林义巡发言)

深圳市魔样科技股份有限公司执行总裁黄立阳介绍了其基于海思W610+OpenHarmony打造的穿戴解决方案,智能穿戴设备与OpenHarmony融合创新与开发心得展开分享。黄立阳表示有了OpenHarmony的加持,将有力的推动穿戴设备向更加智能化、专业化及多样化方向发展,帮助中小厂家拉平与大厂的技术代差。 “依托OpenHarmony平台优势,将推出更多定制化,专业化的解决方案引领行业的创新。”黄立阳说。

(深圳市魔样科技股份有限公司执行总裁黄立阳发言)

深圳市岍丞技术有限公司总经理张昊分享了岍丞技术这几年基于OpenHarmony为底座打造的AOS系统,这一系统实现了针对多种设备,包括耳机,手表,AR眼镜等众多带屏的显示方案。张昊表示岍丞将专注于以技术作为核心驱动力,做好A-IOT应用和服务。

(深圳市岍丞技术有限公司总经理张昊发言)

Gomore Inc. 执行长郭信甫从AI算法与IC搭配角度切入,分享了他对于人工智能算法与晶片发展如何赋能穿戴装置健康应用的看法。他强调,AI算法的演进离不开集成电路(IC)的发展,两者的紧密结合对推动穿戴设备健康应用发展至关重要。郭信甫呼吁行业关注这一重要结合,携手利用人工智能提升全球人口的健康水平。

(Gomore inc 执行长郭信甫发言)

OpenHarmony项目群技术指导委员会委员、深圳鸿信智联数字科技有限公司CEO张兆生以《轻应用市场助力OpenHarmony Watch生态》为主题发表演讲。他观察到穿戴设备的智能化发展,对应用的诉求越来越强烈。面对七国八制的现状,如何给穿戴设备提供统一的应用商店,实现行业效率更高,是当前面临的关键的问题。他表示鸿信智联将帮助穿戴行业做好应用分发商店,并呼吁行业伙伴们加入到穿戴应用生态,助力穿戴生态发展,实现穿戴行业的互利共赢。

(OpenHarmony项目群技术指导委员会委员、深圳鸿信智联数字科技有限公司CEO张兆生发言)

上海喜马拉雅科技有限公司IOT事业部副总经理娄建林分享了喜马拉雅在音频领域的多年耕耘成果,以及携手海思+OpenHarmony打造腕间声音标杆体验的实践和探索经历。 娄建林指出,与OpenHarmony的合作,不仅丰富了穿戴产品的功能,也推动了音频内容消费方式变革,同时也对未来一起合作做出“更有温度”的用户体验提出了展望。

(上海喜马拉雅科技有限公司IOT事业部副总经理娄建林发言)

吉林市独立开发者李尚儒的演讲围绕基于OpenHarmony穿戴打造腕上阅读器以实现极致的长文本阅读体验展开。李尚儒以独立开发者的角度分享了基于海思 + OpenHarmony穿戴设备上的应用开发体验,完善的开发工具,容易上手的开发语言,丰富的API文档都成为开发者的福音。他希望在手腕阅读器应用上用更细节、更精致、更人性化的交互设计,打破穿戴设备的屏幕局限,未来通过小小的表盘即可阅读长篇巨著,真正实现文字不限载体的灵活管理。

(吉林市九七软件部CTO李尚儒发言)

表盘作为智能手表生态中的重要组成部分,它不仅能够提升用户体验,丰富生态内容,还具有潜在的商业价值。深圳仪品信息技术有限公司设计总监董奎从设计角度出发,对表盘创意助力OpenHarmony穿戴生态进行了展望。董奎指出,在OpenHarmony Watch 生态系统的加持下,表盘不仅仅是美学的体现,更是功能与用户体验的完美融合,能为用户带来全新的智能、便捷、个性化体验,也为 OpenHarmony Watch 生态系统注入新的活力。

(深圳仪品信息技术有限公司设计总监董奎发言)

现场与会的行业大咖KUMI总裁郭锦炜表示:穿戴生态依托OpenHarmony强大的技术底座支撑、繁荣的生态发展以及不断扩展的行业应用,必将全面开花,穿戴领域必将迎来蓬勃繁荣。

经过一下午的热烈讨论,与会专家一致认为,当前,OpenHarmony是支撑手表产业标准化,规模化,智能化发展的更优选择,并同意加入OpenHarmony Watch SIG,实现手表生态的共建,共享,共赢。也非常期待明年的大会,为产业带来更新的技术,更多的产品,更具创新的体验。

收起阅读 »

前端大佬都在用的useFetcher究竟有多强?

web
useFetcher:让数据管理变得如此简单 大家好,今天我要和你们分享一个让我惊喜万分的小工具——useFetcher。说实话,第一次用它的时候,我感觉自己像是发现了新大陆!它彻底改变了我处理数据预加载和跨组件更新的方式。 alovajs简介 在介绍useF...
继续阅读 »

useFetcher:让数据管理变得如此简单


大家好,今天我要和你们分享一个让我惊喜万分的小工具——useFetcher。说实话,第一次用它的时候,我感觉自己像是发现了新大陆!它彻底改变了我处理数据预加载和跨组件更新的方式。


alovajs简介


在介绍useFetcher之前,我们先来聊聊alovajs。它是一个革命性的新一代请求工具,可以大大简化我们的API集成流程。 与react-query和swrjs等hooks库不同,alovajs提供了针对各种请求场景的完整解决方案。


alovajs的强大之处在于:



  • 它将API的集成从7个步骤降低为只需要1个步骤

  • 提供了15+个针对特定场景的"请求策略"

  • 不仅能在客户端使用,还提供了服务端的请求策略


如果你想深入了解alovajs,强烈推荐去官网 alova.js.org 看看。相信你会像我一样,被它的强大功能所吸引。


useFetcher的妙用


现在,让我们聚焦到今天的主角——useFetcher。这个小工具真的太棒了,它让我轻松实现了一些以前觉得很复杂的功能。


数据预加载


想象一下,你正在开发一个分页列表,希望在用户浏览当前页面时就预加载下一页的数据。useFetcher可以轻松实现这一点:


const { fetch } = useFetcher({ updateState: false });

const currentPage = ref(1);
const { data } = useWatcher(() => getTodoList(currentPage.value), [currentPage], {
immediate: true
}).onSuccess(() => {
fetch(getTodoList(currentPage.value + 1));
});

这段代码会在当前页加载成功后,自动预加载下一页的数据。是不是感觉很简单?我第一次实现这个功能时,都被自己的效率惊到了!


跨组件更新


另一个让我惊喜的功能是跨组件更新。假设你在一个组件中修改了todo数据,想要在另一个组件中更新列表。useFetcher配合method快照匹配器可以轻松实现:


const { fetch } = useFetcher();

const handleSubmit = () => {
// 提交数据...
const lastMethod = alovaInstance.snapshots.match({
name: 'todoList',
filter: (method, index, ary) => index === ary.length - 1
}, true);
if (lastMethod) {
await fetch(lastMethod);
}
};

这段代码会在提交数据后,自动找到最后一个名为'todoList'的method实例并重新获取数据,从而更新列表。这种优雅的数据管理方式,让我的代码结构变得更加清晰了。


总结


useFetcher真的改变了我对数据管理的看法。它不仅可以帮我们实现数据预加载,还能轻松处理跨组件更新的问题。使用它,我们可以写出更加高效、更加优雅的代码。


你们平时是怎么处理这些数据管理的问题的呢?有没有遇到过什么困难?我很好奇大家的经验和想法,欢迎在评论区分享。如果这篇文章对你有帮助,别忘了点个赞哦!让我们一起探讨,一起进步!


作者:坡道口
来源:juejin.cn/post/7425225508613816320
收起阅读 »

解析vue中nextTick

web
在 Vue.js 中,nextTick 是一个非常重要的函数,它用于延迟回调函数的执行,直到下次 DOM 更新循环之后执行。这是因为 Vue 的数据绑定系统需要确保在数据变化后 DOM 被更新之前不执行某些依赖于 DOM 状态的操作。 实现原理 Vue 的 n...
继续阅读 »

在 Vue.js 中,nextTick 是一个非常重要的函数,它用于延迟回调函数的执行,直到下次 DOM 更新循环之后执行。这是因为 Vue 的数据绑定系统需要确保在数据变化后 DOM 被更新之前不执行某些依赖于 DOM 状态的操作。


实现原理


Vue 的 nextTick 主要是为了处理异步更新队列的问题。当数据发生变化时,并不是立刻更新 DOM,而是将这个更新放入一个队列中,等到当前执行栈清空时(也就是当前同步任务执行完毕),Vue 会进行一次更新,将所有需要更新的内容一次性渲染到 DOM 中,这样做是为了减少 DOM 操作次数,提高性能。


在 Vue 2 中的实现


在 Vue 2 中,nextTick 的实现在 src/core/util/next-tick.js 文件里。它利用了 setTimeout, setImmediate(如果支持的话),以及 MessageChannel 这几种不同的异步方法来实现。如果浏览器环境支持 Promise,那么它也会使用 Promise.then 来实现异步操作。


Javascript
深色版本
function nextTick (cb, ctx) {
var _resolve
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})

if (!pending) {
pending = true
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 使用 Promise 的话,创建一个 resolve 函数,并立即调用它。
var resolve = Promise.resolve
// 使用 then 回调来执行
resolve.call(Promise, function () { _resolve = $internal; })
} else {
// 如果没有 Promise 支持,就退而求其次使用 setImmediate 或 setTimeout
setTimeout(_flushCallbacks, 0)
}
}
}

这里的关键在于 Promise.resolve() 后面跟着的 .then() 会放到 JavaScript 引擎的微任务队列中,等待当前执行栈清空后执行。这样可以确保 nextTick 的回调是在所有同步任务完成后被执行。


在 Vue 3 中的实现


Vue 3 中使用了更现代的方法来实现 nextTick,主要是基于 queueMicrotask API。这个 API 可以让回调函数在当前任务完成后的下一个微任务时机执行。如果没有 queueMicrotask 支持,则退回到 Promise.then() 方法。


Javascript
深色版本
export function nextTick(cb?: Function, delay = 0): PromiseLike<any> | void {
if (cb) {
// 如果提供了回调函数,则注册回调
registerFlushSyncCallback(() => {
queueMicrotask(() => {
// 使用 queueMicrotask 来确保回调是在微任务时机执行
// 这样可以保证在任何其他异步任务之前执行
callWithAsyncErrorHandling(cb, activeInstance, SchedulerGuards.NEXT_TICK_HANDLER);
});
});
} else {
// 如果没有提供回调,则返回一个 promise
return new Promise((res) => {
queueMicrotask(res);
});
}
}

总结来说,nextTick 的主要作用是确保某些操作在 DOM 更新之后进行,从而避免了由于数据变化但 DOM 尚未更新导致的问题。通过使用异步机制(如微任务或宏任务),nextTick 能够确保在适当的时机执行回调。


作者:侯亮平
来源:juejin.cn/post/7426206782022074431
收起阅读 »

关于WebView的秒开实战

web
通过这篇文章你可以了解到: 性能优化的思维方式 WebView进阶知识 写在前面 最近组里做新的Web容器的,一次承载多个H5页面,以实现左右切换,默认展示主会场页,并要达到提升打开率的目标。要达到这个目标,那势必要从加载优化入手,缩短页面的打开时间。优化...
继续阅读 »

通过这篇文章你可以了解到



  • 性能优化的思维方式

  • WebView进阶知识


写在前面


最近组里做新的Web容器的,一次承载多个H5页面,以实现左右切换,默认展示主会场页,并要达到提升打开率的目标。要达到这个目标,那势必要从加载优化入手,缩短页面的打开时间。优化的点包括但不限于,Activity初始化、ViewPager和Fragment的初始化、WebView的初始化等等。


上一片文章给大家分享了我在ViewPager上面做的优化,本篇文章再接着给大家分享下WebView秒开的尝试。


优化效果



我们以提升打开率为目标,口径是资源位点击WebView的onPageFinished



使用新的容器之后,打开率提升了大约10%-20%65%—>85%),在低端机上的提升较为明显。为了让各位同学更加直观的感受到优化后的效果,这里用两张图简化的流程图来表示:


ago.png


以上是我们的容器简略的加载过程需经过6个步骤,加载时长从Activity的onCreate开始计算到WebView的onPageFinished大约需要3000ms(低端机)。很显然,在如今这个快节奏的社会,用户是不会等待这么长时间的。为此我们对它进行了一场手术,把它整成了下边的样子:


after.png


???what's the ****?玩俄罗斯方块吗?


同学憋急,你现在只需要关注的是:它由6个冗长的步骤,变成了两个步骤(Na组件放在了WebView初始化完成后加载),大大缩减了我们首页的加载时间。关于你的疑问,我会在下边的章节解释。


过程分析



上一节我们讲到,一次完整的打开过程需要经过6个步骤,经过了我们大刀阔斧的改造后,只需要两个步骤。这节接着给大家剖析我们这么做的底层逻辑。



Native优化


资源预加载


WebView组件加载过程有三处网络耗时分别是主文档HTML的加载、JS/CSS的加载和内容数据的加载,串行的流程是效率及其低下的。那么我们是不是改成并行的?当然不能!



  • 主文档HTML其实就是一个H5的框架,一个页面内所有的资源都是先通过主文档来触发加载,在主文档被加载之前我们是不能知道有哪些JS和CSS文件的。

  • 内容数据(包括图片)是由我们的业务方决定的,涵盖了各个营销场景,不像新闻浏览类的页面有固定的排版样式。由于页面不统一,单独对它进行下载再注入的改造成本有点大。(后续的离线化方案可实现


基于上述两点,可取的做法是:先把主文档数据预取后缓存,待WebView loadUrl之后,通过WebViewClient的监听去拦截主文档的请求


@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
for (RequestInterceptor interceptor : Interceptors) {
//拦截请求,去缓存中取出主文档数据
WebResourceResponse response = interceptor.intercept(view, request);
if (response != null) {
return response;
}
}
return super.shouldInterceptRequest(view, request);
}

预加载时机


预加载放在点击资源位(资源位在首页区域)之前是最理想的,这也是我们的初步设想。但是涉及到首页模块的改造,需要对应组件方的配合支持,会导致开发周期的延长。所以我们决定在第一版以HOOK Instumentation的方式,在点击资源位之后,Activity的onCreate之前去开启子线程对主文档进行预加载


wait4.png


同时跟首页组件方协调方案:在首页的T2阶段(不影响其它优先级更高的任务)对资源位进行预加载,点击后如果首页预加载成功则直接打开Activity,否则继续Instrumentation加载逻辑。


JS/CSS预加载


主文档加载完成了之后,可以对缓存的数据进行识别查找到需要加载的JS/CSS文件,紧接着开始进行JS/CSS的预加载。


下面时查找JS文件的伪代码:


private static final String JS_PATTERN = "<script\\s+[^>]*?src=[\"']([^\"']+)[\"'][^>]*>(?:<\\/script>)?";

/**
*@param htmlData 将主文档的二进制文件转换成的String类型
*@param JSPattern 用于从主文档内匹配JS文件的正则表达式
*/

private void recognitionJS(String htmlData, String JSPattern) {
try {
Pattern scriptPattern = Pattern.compile(JSPattern, Pattern.CASE_INSENSITIVE);
Matcher scriptMatcher = scriptPattern.matcher(htmlData);
while (scriptMatcher.find()) {
String link = scriptMatcher.group(1) + "";
if (TextUtils.isEmpty(link)) {
continue;
}

mResSet.add(link);
}
} catch (Exception e) {
}
}

这样一来我们的流程在第二版就变成了:


wait6.png


到这里我们在数据请求这一块所做的优化就结束了,那么我们的矛头接下来该指向哪里?


WebView预热


首次创建耗时较长


我从埋点的数据中发现,容器冷启打开的时间比热启要长的多,从Activity onCreate到WebView loadUrl之前的耗时比起热启大约慢了200多ms。这个过程中初始化的组件除了WebView还有有ViewPager和Fragment,通过再次细分阶段的埋点统计耗时发现,启动方式对这两者的初始化时间影响不大,WebView初始化时间自然就成了我们攻克的对象


我们找来了其它几个机型重复上述的步骤,高端机上表现并不明显,但也存在差异(大约80ms)。进一步确定了是WebView自身的原因,可以得出结论:WebView第一次初始化的时间会比后续创建的时间长,具体差异取决于机型性能


WebView Pool


利用前面得出的结论,可以在App启动时开始WebView的第一次初始化再销毁,以减少后续使用过程的创建时间。但还是避免不了往后创建带来的时间开销,这个时候池化技术就呼之欲出了。


我们可以将创建好的WebView放入容器中,可以一个也可以多个,取决于业务。由于创建WebView需要和Context绑定,而预创建WebView是无法提前获知所需要挂载的Activity的,为此我们找到了MutableContextWrappe。引用官方对它的介绍:



Special version of ContextWrapper that allows the base context to be modified after it is initially set. Change the base context for this ContextWrapper. All calls will then be delegated to the base context. Unlike ContextWrapper, the base context can be changed even after one is already set.



翻译成人话:它允许在运行时动态地更改 Context。这在某些特定场景下非常有用,例如,当您需要在不同的 Context 之间灵活切换或修改 Context 时。真是完美的解决了我们预创建绑定Context的问题!


//预创建WebView,存入缓存池
MutableContextWrapper contextWrapper = new MutableContextWrapper(getAppContext());
mWebViewPool.push(new WebView(contextWrapper));

//取出WebView,替换我们所需要的Context
WebView webView = mWebViewPool.pop();
MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
contextWrapper.setBaseContext(activityContext);

看到这里,如果你是不是以为WebView的池化就这样结束了?


那是不可能滴


那是不可能滴


那是不可能滴


子进程承接


众所周知,一个亿级DAU的商业化App是非常庞杂的。在App启动时,有许多的任务需要初始化,势必会带来很大的性能开销。如果在这个阶段进行WebView的创建和池化的操作。前者可能会引出ANR,后者则是会面临内存溢出的风险。一波刚平,一波又起!


怎么办?再开个线程?WebView不能在子线程初始化,即使可以也解决不了内存开销的问题。PASS!


线程不行,进程呢?Bingo!


我们可以在App启动时开启一个子进程,在子进程进行WebView的初始化和池化的任务。系统会为子进程重新开辟内存空间,同时在子进程创建WebView也不会阻塞主进程的主线程,顺带也可以提高我们主进程的稳定性,可谓是一举多得。整个加载流程也就变成了三个大步骤。


wait67.png


组件懒加载


上一篇文章里有讲到我们容器的页面结构,没看过的请点击这里。在开始WebView加载之前会经过ViewPager和Fragment的初始化,经过线下实验统计,省去这两玩意儿大约可以提升67%(口径:Activity onCreate到WebView loadUrl,也就是说ViewPager和Fragment占这个过程的67%)。这不,优化点又来了。


打开容器的第一阶段,只需加载一个页面。因此我们可以将WebView直接放在Activity上显示,无需ViewPager和Fragment的介入,等到首页加载完成后再初始化这两组件,并开始缓存其它页面。


到这里我们的加载流程就变成了开头的样子了:


wait68.png


不要抬杠:你开头画的也不是这个样子的啊?


咱这不是为了更方便的理解,所以在开头小小的抽象了一下吗。手动狗头


其它优化


剩下还有一些前端的通用优化方式、网络通用优化方式在网上有同学总结的很清楚,在这里我就不一一列举。感兴趣的可以跳转对应文章进行查阅


今日头条品质优化 - 图文详情页秒开实践


作者:图灵1024
来源:juejin.cn/post/7364283070869028899
收起阅读 »

如何开发一个chrome 扩展

web
前言 最近开发一个涉及到很多颜色转换的工作,每次都搜索打开一个新页面在线转换,十分麻烦,于是想着开发一个颜色转换的浏览器插件,每次点击即可使用。 查看Chrome插件开发的文档developer.chrome.com/docs/extens… ,从头开始开发一...
继续阅读 »

前言


最近开发一个涉及到很多颜色转换的工作,每次都搜索打开一个新页面在线转换,十分麻烦,于是想着开发一个颜色转换的浏览器插件,每次点击即可使用。


查看Chrome插件开发的文档developer.chrome.com/docs/extens… ,从头开始开发一个插件还是比较麻烦且原始的。搜索网上资料,发现了2个工具



最后选择了WXT,因为它用起来更方便,且支持多浏览器。


WXT是什么


WXT号称下一代浏览器扩展开发框架,免费、开源、易用且支持多种浏览器。


这段文字是关于WXT框架的介绍,它是一个用于构建浏览器扩展的开源框架。下面是对文中提到的几个关键点的解释:



  • WXT有自己一套约定的框架,为开发者提供了一套标准化的做法,有助于保持项目的一致性,使得新手能够更容易地理解和接手项目。

  • 基于项目文件结构自动生成manifest。manifest是浏览器扩展的配置文件,定义了扩展的名称、版本、权限等信息。WXT框架能够根据项目结构自动创建这个文件,简化开发过程。

  • 单文件配置Entrypoint,比如背景脚本或内容脚本,这样可以更直观地管理和维护代码。

  • WXT提供了开箱即用的TypeScript支持,并且改进了浏览器API的类型定义。TypeScript是一种强类型语言,它在JavaScript的基础上增加了类型系统,有助于在开发过程中捕捉到潜在的错误。

  • 输出文件的路径最小化,这意味着WXT在构建扩展时会优化文件路径,减少runtime的path长度,可以提高扩展的加载速度和性能。


WXT 安装&开发


我们直接脚手架开一个项目


pnpm dlx wxt@latest init wxt-demo
cd wxt-demo
pnpm install

套用官网的一个图


不过我选的react,生成的工作目录文件如下


image.png


调试运行pnpm dev,WXT直接开了一个无头浏览器,可以实时看到效果


image.png


颜色转换开发


因为我的需求比较简单,实现各种颜色的转换,页面UI就直接使用antd,样式直接Inline。代码如下:


    <>
<header style={pageLayoutStyle}>
<p style={{ fontSize: '1.5rem', textAlign: 'center', fontWeight: 'medium' }}>Color Converter</p>
<p>This tool helps you convert colors between different color formats.</p>
</header>

<main style={pageLayoutStyle}>
<div>
<p style={{ fontSize: '1.2rem', textAlign: 'left' }}>Enter a color:</p>
</div>
<Input
suffix={
<ColorPicker
defaultValue={defaultColor}
value={hex === '' ? defaultColor : hex}
styles={{ popupOverlayInner: { position: 'absolute', left: '50%', transform: 'translate(-100%, -50%)' } }}
onChangeComplete={(color) => {
const str = (color.toRgbString())
}} />
}
placeholder={defaultColor}
autoFocus={true}
onChange={(e) => {
const str = (e.target.value)
}} />

<div>
<p style={{ fontSize: '1.2rem', textAlign: 'left' }}>Results</p>
</div>
{contextHolder}
<Input addonBefore="RGB" value={rgb} suffix={<CopyOutlined onClick={() => { copyToClipboard(rgb) }} />} readOnly={true} defaultValue="" />
<Input addonBefore="HEX" value={hex} suffix={<CopyOutlined onClick={() => { copyToClipboard(hex) }} />} readOnly={true} defaultValue="" style={{ marginTop: '8px' }} />
<Input addonBefore="HSL" value={hsl} suffix={<CopyOutlined onClick={() => { copyToClipboard(hsl) }} />} readOnly={true} defaultValue="" style={{ marginTop: '8px' }} />
<Input addonBefore="HSV" value={hsv} suffix={<CopyOutlined onClick={() => { copyToClipboard(hsv) }} />} readOnly={true} defaultValue="" style={{ marginTop: '8px' }} />
<Input addonBefore="CMYK" value={cmyk} suffix={<CopyOutlined onClick={() => { copyToClipboard(cmyk) }} />} readOnly={true} defaultValue="" style={{ marginTop: '8px' }} />
</main>
</>

新建color.ts,定义几个变量名称和方法名称,一路按tab,AI自动补全了代码。代码太长就不贴出来了,主要是颜色的正则匹配和转换。同上面UI绑定后,最终实现效果如下:


image.png


在调试firefox时,遇到一个小坑:content.ts中需要至少有一个匹配matches,否则会直接退出提示插件invalid。


发布


WXT发布也比较简单,直接运行 pnpm zip就会构建chrome的扩展压缩包,发布firefox只需要pnpm zip:firefox。在ouput目录下就会生成对应产物。


不过记得在打包前修改wxt.config.ts,添加名称、版本、描述等。如:


export default defineConfig({
modules: ['@wxt-dev/module-react'],
manifest: {
version: '1.0.0',
name: 'color-converter',
description: 'A color converter tool',
}
});

最后完整代码见github: github.com/xckevin/col…


现在插件也已经上架了市场,欢迎下载:



作者:大贝壳kevinliu
来源:juejin.cn/post/7425803259443019815
收起阅读 »

从2s优化到0.1s,我用了这5步

前言 分类树查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。 但就是这样一个简单的分类树查询功能,我们却优化了5次。 到底是怎么回事呢? 苏三的免费刷题网站:http://www.susan.net.cn 里面:面试八股文、BAT面试...
继续阅读 »

前言


分类树查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。


图片但就是这样一个简单的分类树查询功能,我们却优化了5次。


到底是怎么回事呢?



苏三的免费刷题网站:http://www.susan.net.cn 里面:面试八股文、BAT面试真题、工作内推、工作经验分享、技术专栏等等什么都有,欢迎收藏和转发。



背景


我们的网站使用了SpringBoot推荐的模板引擎:Thymeleaf,进行动态渲染。


它是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发。


它提供了一个用于整合SpringMVC的可选模块,在应用开发中,我们可以使用Thymeleaf来完全代替JSP或其他模板引擎,如Velocity\FreeMarker等。


前端开发写好Thymeleaf的模板文件,调用后端接口获取数据,进行动态绑定,就能把想要的内容展示给用户。


由于当时这个是从0-1的新项目,为了开快速开发功能,我们第一版接口,直接从数据库中查询分类数据,组装成分类树,然后返回给前端。


通过这种方式,简化了数据流程,快速把整个页面功能调通了。


第1次优化


我们将该接口部署到dev环境,刚开始没啥问题。


随着开发人员添加的分类越来越多,很快就暴露出性能瓶颈。


我们不得不做优化了。


我们第一个想到的是:加Redis缓存


流程图如下:图片于是暂时这样优化了一下:



  1. 用户访问接口获取分类树时,先从Redis中查询数据。

  2. 如果Redis中有数据,则直接数据。

  3. 如果Redis中没有数据,则再从数据库中查询数据,拼接成分类树返回。

  4. 将从数据库中查到的分类树的数据,保存到Redis中,设置过期时间5分钟。

  5. 将分类树返回给用户。


我们在Redis中定义一个了key,value是一个分类树的json格式转换成了字符串,使用简单的key/value形式保存数据。


经过这样优化之后,dev环境的联调和自测顺利完成了。


第2次优化


我们将这个功能部署到st环境了。


刚开始测试同学没有发现什么问题,但随着后面不断地深入测试,隔一段时间就出现一次首页访问很慢的情况。


于是,我们马上进行了第2次优化。


我们决定使用Job定期异步更新分类树到Redis中,在系统上线之前,会先生成一份数据。


当然为了保险起见,防止Redis在哪条突然挂了,之前分类树同步写入Redis的逻辑还是保留。


于是,流程图改成了这样:图片增加了一个job每隔5分钟执行一次,从数据库中查询分类数据,封装成分类树,更新到Redis缓存中。


其他的流程保持不变。


此外,Redis的过期时间之前设置的5分钟,现在要改成永久。


通过这次优化之后,st环境就没有再出现过分类树查询的性能问题了。


第3次优化


测试了一段时间之后,整个网站的功能快要上线了。


为了保险起见,我们需要对网站首页做一次压力测试。


果然测出问题了,网站首页最大的qps是100多,最后发现是每次都从Redis获取分类树导致的网站首页的性能瓶颈。


我们需要做第3次优化。


该怎么优化呢?


答:加内存缓存。


如果加了内存缓存,就需要考虑数据一致性问题。


内存缓存是保存在服务器节点上的,不同的服务器节点更新的频率可能有点差异,这样可能会导致数据的不一致性。


但分类本身是更新频率比较低的数据,对于用户来说不太敏感,即使在短时间内,用户看到的分类树有些差异,也不会对用户造成太大的影响。


因此,分类树这种业务场景,是可以使用内存缓存的。


于是,我们使用了Spring推荐的caffine作为内存缓存。


改造后的流程图如下:图片



  1. 用户访问接口时改成先从本地缓存分类数查询数据。

  2. 如果本地缓存有,则直接返回。

  3. 如果本地缓存没有,则从Redis中查询数据。

  4. 如果Redis中有数据,则将数据更新到本地缓存中,然后返回数据。

  5. 如果Redis中也没有数据(说明Redis挂了),则从数据库中查询数据,更新到Redis中(万一Redis恢复了呢),然后更新到本地缓存中,返回返回数据。



需要注意的是,需要改本地缓存设置一个过期时间,这里设置的5分钟,不然的话,没办法获取新的数据。



这样优化之后,再次做网站首页的压力测试,qps提升到了500多,满足上线要求。


最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。


你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。


添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。


第4次优化


之后,这个功能顺利上线了。


使用了很长一段时间没有出现问题。


两年后的某一天,有用户反馈说,网站首页有点慢。


我们排查了一下原因发现,分类树的数据太多了,一次性返回了上万个分类。


原来在系统上线的这两年多的时间内,运营同学在系统后台增加了很多分类。


我们需要做第4次优化。


这时要如何优化呢?


限制分类树的数量?


答:也不太现实,目前这个业务场景就是有这么多分类,不能让用户选择不到他想要的分类吧?


这时我们想到最快的办法是开启nginxGZip功能。


让数据在传输之前,先压缩一下,然后进行传输,在用户浏览器中,自动解压,将真实的分类树数据展示给用户。


之前调用接口返回的分类树有1MB的大小,优化之后,接口返回的分类树的大小是100Kb,一下子缩小了10倍。


这样简单的优化之后,性能提升了一些。


第5次优化


经过上面优化之后,用户很长一段时间都没有反馈性能问题。


但有一天公司同事在排查Redis中大key的时候,揪出了分类树。之前的分类树使用key/value的结构保存数据的。


我们不得不做第5次优化。


为了优化在Redis中存储数据的大小,我们首先需要对数据进行瘦身。


只保存需要用到的字段。


例如:


@AllArgsConstructor
@Data
public class Category {

    private Long id;
    private String name;
    private Long parentId;
    private Date inDate;
    private Long inUserId;
    private String inUserName;
    private List children;
}

像这个分类对象中inDate、inUserId和inUserName字段是可以不用保存的。


修改自动名称。


例如:


@AllArgsConstructor
@Data
public class Category {
    /**
     * 分类编号
     */

    @JsonProperty("i")
    private Long id;

    /**
     * 分类层级
     */

    @JsonProperty("l")
    private Integer level;

    /**
     * 分类名称
     */

    @JsonProperty("n")
    private String name;

    /**
     * 父分类编号
     */

    @JsonProperty("p")
    private Long parentId;

    /**
     * 子分类列表
     */

    @JsonProperty("c")
    private List children;
}

由于在一万多条数据中,每条数据的字段名称是固定的,他们的重复率太高了。


由此,可以在json序列化时,改成一个简短的名称,以便于返回更少的数据大小。


这还不够,需要对存储的数据做压缩。


之前在Redis中保存的key/value,其中的value是json格式的字符串。


其实RedisTemplate支持,value保存byte数组


先将json字符串数据用GZip工具类压缩成byte数组,然后保存到Redis中。


再获取数据时,将byte数组转换成json字符串,然后再转换成分类树。


这样优化之后,保存到Redis中的分类树的数据大小,一下子减少了10倍,Redis的大key问题被解决了。


性能优化问题,无论在面试,还是工作中,都会经常遇到。



作者:苏三说技术
来源:juejin.cn/post/7425382886297600050
收起阅读 »

课表拖拽(一)拖拽实现

web
最近接到一个任务,要求实现一个课表拖拽的功能,支持快速修改个人日程时间。项目采用taro框架。 基于性能的抉择 container采用grid布局,7colum+12row,共84个单元格 拖拽的方式有两种 盒子跟随手指,并实时显示松手后落入的位置,松手时...
继续阅读 »

最近接到一个任务,要求实现一个课表拖拽的功能,支持快速修改个人日程时间。项目采用taro框架。


751CC6CFE376180E3B70EA0372593F85.jpg


基于性能的抉择


container采用grid布局,7colum+12row,共84个单元格


拖拽的方式有两种



  • 盒子跟随手指,并实时显示松手后落入的位置,松手时寻找一个离手指最近的单元格放入

  • 盒子实时在格子之内,根据手指位置实时计算填入的格子,将盒子放入


哪一种性能更高??????


显然第一种方案在第二种方案上多了盒子实时跟随手指这个额外操作,性能不占优势。


catch-move避免滑动穿透


因为课表支持左右滑动查看自己每一周的课程安排,采用了一个Swiper包裹在container之外,在滑动时会带动Swiper的滑动,那该怎么办?????????


不妨请教学长,经过学长的指导,告诉了我一个api


屏幕截图 2024-10-15 105302.png


不得不感慨阅读官方文档的重要性(老实了,以后必须多看官方文档)


如何根据手指的位置,计算所在单元格


const unitwith = 350 / 7;
const unitheight = 600 / 12;

先得到了每一个单元格的宽高


然后通过滑动的事件对象可以获取当前的(x,y),那么动态设置grid样式就可以实现


  const getGridPositionByXY = (xp, yp) => {
return `gridColumn:${Math.floor(xp / unitwith)} ;gridRow:${Math.floor(
yp / unitheight
)}
/${Math.floor(yp / unitheight) + 2}`
;
};

handleTouchMove函数的实现


我们需要两个响应式的变量x,y,通过在handleTouchMove函数中修改x,y来带动style的修改


 const [x, setX] = useState(350 / 7);
const [y, setY] = useState(600 / 12);

const handleTouchMove =(e) => {
setX(e.changedTouches[0].clientX + 50);
setY(e.changedTouches[0].clientY + 50);
};

性能优化(节流)


节流:在一定时间内,无论函数被触发多少次,函数只会在固定的时间间隔内执行一次


为防止handleTouchMove的触发频率太高,我们采用节流函数来让它在固定时间内只执行一次


function Throttle(fn, delay) {
let timer = null;
return function () {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
}

const handleTouchMove = Throttle((e) => {
setX(e.changedTouches[0].clientX + 50);
setY(e.changedTouches[0].clientY + 50);
}, 10);

提升交互性


我们可以让用户长按激活,随后才能滑动,并且在激活的时候触发震动


直接贴完整代码在这里


import { View, Swiper, SwiperItem } from "@tarojs/components";
import { useState, useRef } from "react";
import Taro, { useLoad } from "@tarojs/taro";
import "./index.css";

function Throttle(fn, delay) {
let timer = null;
return function () {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
export default function Index() {
const [isLongPress, setIsLongPress] = useState(false);
useLoad(() => {
console.log("Page loaded.");
});
const timer = useRef(null);

const [x, setX] = useState(350 / 7);
const [y, setY] = useState(600 / 12);
// const [StartPosition, setStartPosition] = useState({ x: 0, y: 0 });

const unitwith = 350 / 7;
const unitheight = 600 / 12;
const getGridPositionByXY = (xp, yp) => {
return `gridColumn:${Math.floor(xp / unitwith)} ;gridRow:${Math.floor(
yp / unitheight
)}
/${Math.floor(yp / unitheight) + 2}`
;
};

const handleTouchMove = Throttle((e) => {
if (!isLongPress) return;
setX(e.changedTouches[0].clientX + 50);
setY(e.changedTouches[0].clientY + 50);
}, 10);

return (
<View className='index'>
<Swiper circular style={{ width: "100vw", height: "100vh" }}>
<SwiperItem>
<view className='container'>
<view
style={getGridPositionByXY(x, y)}
className={`items-1 ${isLongPress ? "pressActive" : ""}`}
catch-move
onTouchStart={() =>
{
timer.current = setTimeout(() => {
setIsLongPress(true);
Taro.vibrateShort();
// console.log("长按");
}, 1000);
}}
onTouchMove={handleTouchMove}
onTouchEnd={() => {
clearTimeout(timer.current);
setIsLongPress(false);
}}
></view>
<view className="items-2">2</view>
</view>
</SwiperItem>
<SwiperItem>
<view className="container">
<view className="items-1">no</view>
</view>
</SwiperItem>
<SwiperItem>
<view className="container">
<view className="items-1" catch-move></view>
</view>
</SwiperItem>
<SwiperItem>
<view className="container">
<view className="items-1" catch-move></view>
</view>
</SwiperItem>
<SwiperItem>
<view className="container">
<view className="items-1" catch-move></view>
</view>
</SwiperItem>
</Swiper>
</View>

);
}

//index.css
.container {
width: 700px;
height: 1200px;
background-color: #ccc;
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(12, 1fr);
}
.griditems-1 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: cadetblue;
}
.griditems-2 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: aquamarine;
}
.griditems-3 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: burlywood;
}
.griditems-4 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: darkcyan;
}
.griditems-5 {
border: 1px solid #fff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background-color: darkgoldenrod;
}

.items-1 {
grid-column: 1; /* 从第1列开始,到第2列结束 */
grid-row: 1 / 4;
border-radius: 10px;
border: 1px solid #fff;
background-color: burlywood;
}
.items-2 {
grid-column: 3;
grid-row: 1 / 4;
border-radius: 10px;
border: 1px solid #fff;
background-color: burlywood;
}
.pressActive {
border-radius: 10px;
border: 1px solid #fff;
background-color: #fff;
opacity: 0.5;
}

下一期将介绍如何控制方块不重合,以及在展开后方块的处理和对多方块的情况怎么单独管理每一个方块的情况


作者:破晓19
来源:juejin.cn/post/7425562027412815882
收起阅读 »

车机系统与Android的关系

前言:搞懂 Android 系统和汽车到底有什么关系。 一、基本概念 1、Android Auto 1)是什么 Android Atuo 是一个 Android 端的 app,专门为驾驶环境设计的; 运行环境:需要在 Android 5.0 或者更高版本的...
继续阅读 »

前言:搞懂 Android 系统和汽车到底有什么关系。



一、基本概念


1、Android Auto


1)是什么



  • Android Atuo 是一个 Android 端的 app,专门为驾驶环境设计的;

  • 运行环境:需要在 Android 5.0 或者更高版本的系统,并且需要 Google 地图和 Google Play 音乐应用;


2)功能



  • Android Atuo 可以用来将 Android 设备上的部分功能映射到汽车屏幕上;

  • 满足了很多人在开车时会使用手机的需求;


2、Google Assistant



  • Google 将 GoofleAssistant 集成到 AndroidAuto 中;

  • 交互方式有键盘、触摸、语音等;

  • 对于汽车来说,语音无疑是比触摸更好的交互方式;

  • 在驾驶环境中,语音交换存在的优势

    • 用户不改变自身的物理姿势,这种交互方式不影响驾驶员对驾驶的操作;

    • 有需要多次触摸的交互时,可能只需要一条语音就可以完成;

    • 语音交互不存在入口的层次嵌套,数据更加扁平;

    • 优秀的语音系统可以利用对话的上下文完成任务,避免用户重复输入;




3、Android Automotive


1、Android Auto 和 Android Automotive 的区别



  • Android Auto 是以手机为中心的

    • 好处:数据和应用始终是一致的,不存在需要数据同步的问题,手机上装的软件和已有数据,接到汽车上就直接有了;

    • 坏处:每次都需要拿出手机,汽车只是作为手机的一个外设;这种模式不便于对于汽车本身的控制和相关数据的获取;



  • Android Automotive

    • 如果将系统直接内置于汽车中,会大大提升用户体验;

    • Android Automotive 就是面向这个方向进行设计的;

    • 一旦将系统内置于汽车,可以完成的功能就会大大增加;例如,直接在中控触摸屏上调整座椅和空调;同时,系统也能获取更多关于汽车的信息,例如:油耗水平、刹车使用等;




加两张中控和仪表的图片


4、App


1)App 的开发



  • Android Auto 目前仅支持两类第三方应用

    • 音频应用:允许用户浏览和播放汽车中的音乐和语音内容;

    • 消息应用:通过 text-to-speech 朗读消息并通过语音输入回复消息;




2)App 的设计



  • Google 专门为 Android Auto 上的 UI 设计做了一个指导网站:Auto UI guidelines;

  • 基本指导原则(车机交互系统的借鉴)

    • Android Auto 上的互动步调必须由驾驶员控制;

    • 汽车界面上的触摸目标必须足够大,以便可以轻松地浏览和点击;

    • 适当的私彩对比可以帮助驾驶员快速解读信息并做出决定;

    • 应用必须支持夜间模式,因为过高的强度可能会干扰注意力;

    • Roboto 字体在整个系统中用于保持一致性并帮助提高可读性;

    • 通过触摸来进行分页应用用来作为滑动翻页的补充;

    • 有节制地使用动画来描述两个状态间的变化;






二、源码和架构


1、Android Automative的整体架构




  • Android Automative 的源码包含在 AOSP 中;

  • Android Automative 是在原先 Android的 系统架构上增加了一些与车相关的(图中虚线框中绿色背景的)模块;

    • Car App:包括 OEM 和第三方开发的 App;

      • OEM:就是汽车厂商利用自身掌握的核心技术负责设计和开发新产品,而具体的生产制造任务则通过合同订购的方式委托给同类产品的其他厂家进行,最终产品会贴上汽车厂商自己的品牌商标。这种生产方式被称为定牌生产合作,俗称“贴牌”。承接这种加工任务的制造商就被称为OEM厂商,其生产的产品就是OEM产品;



    • Car API:提供给汽车 App 特有的接口;

    • Car Service:系统中与车相关的服务;

    • Vehicle Network Service:汽车的网络服务;

    • Vehicle HAL:汽车的硬件抽象层描述;




1)Car App



  • /car_product/build/car.mk 这个文件中列出了汽车系统中专有的模块;

  • 列表中,首字母大写的模块基本上都是汽车系统中专有的 App;

  • App的源码都位于 /platform/packages/services/Car/ 目录下


    # Automotive specific packages
    PRODUCT_PACKAGES += \
    vehicle_monitor_service \
    CarService \
    CarTrustAgentService \
    CarDialerApp \
    CarRadioApp \
    OverviewApp \
    CarLensPickerApp \
    LocalMediaPlayer \
    CarMediaApp \
    CarMessengerApp \
    CarHvacApp \
    CarMapsPlaceholder \
    CarLatinIME \
    CarUsbHandler \
    android.car \
    libvehiclemonitor-native \



2)Car API



  • 开发汽车专有的App自然需要专有的API;

  • 这些API对于其他平台(例如手机和平板)通常是没有意义的;

  • 所以这些API没有包含在Android Framework SDK中;

  • 下图列出了所有的 Car API;




  • android.car:包含了与车相关的基本API。例如:车辆后视镜,门,座位,窗口等。

    • cabin:座舱相关API。

    • hvac:通风空调相关API。(hvac是Heating, ventilation and air conditioning的缩写)

    • property:属性相关API。

    • radio:收音机相关API。

    • pm:应用包相关API。

    • render:渲染相关API。

    • menu:车辆应用菜单相关API。

    • annotation:包含了两个注解。

    • app

    • cluster:仪表盘相关API。

    • content

    • diagnostic:包含与汽车诊断相关的API。

    • hardware:车辆硬件相关API。

    • input:输入相关API。

    • media:多媒体相关API。

    • navigation:导航相关API。

    • settings:设置相关API。

    • vms:汽车监测相关API。




3)Car Service



  • Car Service并非一个服务,而是一系列的服务。这些服务都在ICarImpl.java构造函数中列了出来;


public ICarImpl(Context serviceContext, IVehicle vehicle, SystemInterface systemInterface,
CanBusErrorNotifier errorNotifier)
{
mContext = serviceContext;
mHal = new VehicleHal(vehicle);
mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
mCarPowerManagementService = new CarPowerManagementService(
mHal.getPowerHal(), systemInterface);
mCarSensorService = new CarSensorService(serviceContext, mHal.getSensorHal());
mCarPackageManagerService = new CarPackageManagerService(serviceContext, mCarSensorService,
mSystemActivityMonitoringService);
mCarInputService = new CarInputService(serviceContext, mHal.getInputHal());
mCarProjectionService = new CarProjectionService(serviceContext, mCarInputService);
mGarageModeService = new GarageModeService(mContext, mCarPowerManagementService);
mCarInfoService = new CarInfoService(serviceContext, mHal.getInfoHal());
mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
mCarAudioService = new CarAudioService(serviceContext, mHal.getAudioHal(),
mCarInputService, errorNotifier);
mCarCabinService = new CarCabinService(serviceContext, mHal.getCabinHal());
mCarHvacService = new CarHvacService(serviceContext, mHal.getHvacHal());
mCarRadioService = new CarRadioService(serviceContext, mHal.getRadioHal());
mCarNightService = new CarNightService(serviceContext, mCarSensorService);
mInstrumentClusterService = new InstrumentClusterService(serviceContext,
mAppFocusService, mCarInputService);
mSystemStateControllerService = new SystemStateControllerService(serviceContext,
mCarPowerManagementService, mCarAudioService, this);
mCarVendorExtensionService = new CarVendorExtensionService(serviceContext,
mHal.getVendorExtensionHal());
mPerUserCarServiceHelper = new PerUserCarServiceHelper(serviceContext);
mCarBluetoothService = new CarBluetoothService(serviceContext, mCarCabinService,
mCarSensorService, mPerUserCarServiceHelper);
if (FeatureConfiguration.ENABLE_VEHICLE_MAP_SERVICE) {
mVmsSubscriberService = new VmsSubscriberService(serviceContext, mHal.getVmsHal());
mVmsPublisherService = new VmsPublisherService(serviceContext, mHal.getVmsHal());
}
mCarDiagnosticService = new CarDiagnosticService(serviceContext, mHal.getDiagnosticHal());

4)Car Tool


a、VMS



  • VMS全称是Vehicle Monitor Service。正如其名称所示,这个服务用来监测其他进程;

  • 在运行时,这个服务是一个独立的进程,在init.car.rc中有关于它的配置


service vms /system/bin/vehicle_monitor_service
class core
user root
group root
critical

on boot
start vms


  • 这是一个Binder服务,并提供了C++和Java的Binder接口用来供其他模块使用;


作者:一个写代码的修车工
来源:juejin.cn/post/7356981730765291558
收起阅读 »

离了大谱,和HR互怼后被开了!

世界之大无奇不有,不靠谱的人见多了,但是不靠谱的公司还是第一次见。 今天故事的主角是某上市公司,其号称为中国电声行业的龙头企业,名字这里就不说了。 故事发生的背景是某大学生秋招找工作,投递和面试的是这家公司的嵌入式开发,但最终却被分到了 IT 部门(猜测应该是...
继续阅读 »

世界之大无奇不有,不靠谱的人见多了,但是不靠谱的公司还是第一次见。


今天故事的主角是某上市公司,其号称为中国电声行业的龙头企业,名字这里就不说了。


故事发生的背景是某大学生秋招找工作,投递和面试的是这家公司的嵌入式开发,但最终却被分到了 IT 部门(猜测应该是 IT 支持岗)。


于是这位哥们儿不服,就找到了这家公司的 HR,但却没想到被 HR 怒怼,并被质问“你配做嵌入式开发吗?”、“我可没有你这么闲”等极具人身攻击的词汇,以下是聊天截图:



当然,故事的结局也大快人心,这哥们儿把他和 HR 的天截图反馈给了官方,于是不出意外,这位 HR 很就被光速开除了(可以看出满满的求生欲),如下图所示:




人在做天在看,不是不报时候未到。打工人何必为难打工人呢?这下好了,小伙子的事情解决了,HR 可倒好,还得重新找工作。



但博主在评论区看到这家公司随意更改应聘者的岗位也是常规操作了,有个哥们儿说他投入的是算法但被直接干到行政去了,离了大谱:



合着找工作这件事,也能像报考大学的志愿一样,上不了某个专业,还可以滑到另一个专业?这也是让我开眼了,大家怎么看?欢迎评论区讨论留言。



本文已收录到我的面试小站 http://www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。



作者:Java中文社群
来源:juejin.cn/post/7426315840222593062
收起阅读 »

第三届OpenHarmony技术大会应用生态实践分论坛成功举办

开放、兼容、安全是OpenAtom OpenHarmony(以下简称“OpenHarmony”)生态蓬勃发展的重要要素。随着用户需求的多元化,对应用生态的需求迫切增加。2024年10月12日,第三届OpenHarmony技术大会的应用生态实践分论坛在上海世博中...
继续阅读 »

开放、兼容、安全是OpenAtom OpenHarmony(以下简称“OpenHarmony”)生态蓬勃发展的重要要素。随着用户需求的多元化,对应用生态的需求迫切增加。2024年10月12日,第三届OpenHarmony技术大会的应用生态实践分论坛在上海世博中心举行,来自不同领域的技术专家聚焦于通过应用开发适配的实战经验,剖析OpenHarmony系统特性,为应用的开发和迁移提供经验和参考。与会嘉宾的分享内容涵盖了OpenHarmony应用开发的多个方面。话题覆盖了从不同应用的OpenHarmony开发经验,到高校应用开发实践案例再到跨平台框架的实践经验和项目优化经验等多个关键主题。通过典型的实战经验分享,分析系统的能力和特性,帮助开发者了解应用开发的最新技术和实践,共同推进应用生态的技术共建、生态共享。

OpenHarmony社区应用工作组组长(代)闫诗文、社区代码共建组组长林志南等专业人士担任出品人,OpenHarmony社区应用工作组运营王霞为分论坛主持人。演讲嘉宾包括:华为三方库和跨平台系统架构师潘锦玲、企查查科技股份有限公司技术专家汤嘉琪、武汉初心科技有限公司(石墨文档)技术总监饶欣、北京风行在线技术有限公司高级技术专家韩超、中国科学院软件研究所高级工程师郑森文、江苏润开鸿数字科技有限公司技术专家徐建国、上海交通大学副教授吴明瑜、华为 ArkUI - X 跨平台系统架构师刘龙、深圳开鸿数字产业发展有限公司 OS 框架开发工程师宫跃纪、华为开源技术专家王晔晖、华为 OpenHarmony 跨平台框架专家高迪。

作为开场嘉宾,华为的三方库和跨平台系统架构师潘锦玲详细介绍了OpenHarmony开源三方库和跨平台框架的技术分类、技术地图以及OpenHarmony化进程等内容,带领与会者对应用三方库和跨平台框架进行了全面了解。

(华为三方库和跨平台系统架构师潘锦玲)

企查查科技股份有限公司的技术专家汤嘉琪在分论坛上分享了“企查查HarmonyOS Next应用适配实战”。在适配过程中,企查查面对了一系列技术挑战。汤嘉琪深入剖析在适配中遇到的技术难题及应对方案,分享了企查查在HarmonyOS Next适配过程中的最新进展与架构设计,讨论实际适配经验以及新系统带来的业务生态创新体验。企查查通过与华为的紧密合作,成功成为垂直领域首家通过KCP7准出标准的企业,并成功在HarmonyOS Next上完成商用级还原。

(企查查科技股份有限公司的技术专家汤嘉琪)

武汉初心科技有限公司(石墨文档)的技术总监饶欣,在分论坛上分享了“石墨文档基于 OpenHarmony 端云一体实践”。饶欣介绍了石墨文档如何利用OpenHarmony操作系统的能力及特性,设计并实现了端云一体化的Office办公软件。他详细阐述了端云一体化的通讯机制、安全机制,以及零注入无感云端站点客户端集成方案,展示了石墨文档在提升OpenHarmony应用开发效率方面的创新实践。

(武汉初心科技有限公司(石墨文档)技术总监饶欣)

大屏端视频用户对于直观流畅操作体验有强烈的需求。北京风行在线技术有限公司的高级技术专家韩超分享了“橙瓣-风行大屏视频产品在OpenHarmony中的开发实践”,讲述了风行如何依托自身在长视频领域的资源积累,率先完成大屏端视频类应用的OpenHarmony化。韩超详细介绍了在具体实践中,利用OpenHarmony的技术特性实现高效开发,以及基于ArkTS Gird布局特性,快速实现瀑布流各类基础行列模版中的应用,这些实践显著节省了开发时间和运营成本。通过ArkUI的动态化加载能力,为用户带来流畅的大屏操作体验。

(北京风行在线技术有限公司的高级技术专家韩超)

“OpenHarmony作为智能终端操作系统,面向RISC-V的应用生态支持以及多端应用生态环境将会是万物智联蓬勃发展的土壤,这需要南北向的开发者一起倾力打造。”中国科学院软件研究所的OpenHarmony项目群工作委员会委员、高级工程师郑森文探讨了“面向RISC-V的OpenHarmony多端应用生态及挑战”。郑森文指出,OpenHarmony对RISC-V架构的应用生态支持至关重要,分享了将ARM平台开发的OpenHarmony应用迁移到RISC-V架构上的策略。同时,也展示了中国科学院软件研究所在PC端形态上的应用需求研究和取得的成果。

(中国科学院软件研究所的OpenHarmony项目群工作委员会委员、高级工程师郑森文)

江苏润开鸿数字科技有限公司的技术专家徐建国,在分论坛上介绍了“思否社区应用开发实践”。徐建国分享了思否社区作为开发者交流平台,在OpenHarmony系统上的应用开发和适配经验。徐建国详细阐述了在构建高性能的MarkDown插件、封装第三方极验包、构建高性能UI、状态管理等方面的实践,以及这些实践如何助力同类社区完成OpenHarmony迁移。徐建国的分享为开发者提供了实用的技术参考和经验分享。

(江苏润开鸿数字科技有限公司的技术专家徐建国)

上海交通大学副教授吴明瑜,在分论坛上分享了“基于OpenHarmony的智能制造软件栈构建实践与探索”。吴明瑜阐述了OpenHarmony在智能制造领域的潜力,展示了其团队如何在OpenHarmony上构建软件化控制系统,实现控制逻辑的弹性部署和更新。他详细介绍了如何通过与云侧能力的整合,实现对制造现场的远程监控,以及这些研究成果如何在真实产线中部署,为智能制造系统提供经验和参考。

(上海交通大学副教授吴明瑜)

华为ArkUI-X跨平台架构系统师刘龙则介绍了“OpenHarmony跨平台框架ArkUI-X实践及思考”。 刘龙讲述了ArkUI-X跨平台框架的工作原理,并分享了跨平台应用开发的前沿佳实践,强调码多平台开发降本增效。

(华为ArkUI-X跨平台架构系统师刘龙)

深圳开鸿数字产业发展有限公司OS框架开发工程师宫跃纪的演讲主题为“OpenHarmony应用性能分析优化项目实践”。 宫跃纪分享了如何从系统层性能调度优化到应用层UI渲染、模块动态加载等优化方案,并结合实际案例分享实践经验。

(深圳开鸿数字产业发展有限公司OS框架开发工程师宫跃纪)

OpenHarmony北向生态面临适配大量安全、先进的第三方开源软件挑战。华为开源技术专家王晔晖认为,这需要构建可持续的管理机制与能力,构筑高价值第三方开源软件供应链。王晔晖介绍了“应用三方库和跨平台框架治理平台”,详细阐述了如何通过构建OpenHarmony应用三方库与跨平台框架(TPC)发行版及治理平台,形成TPC选型、孵化、发行版维护工程落地,通过持续呈现与跟踪、度量TPC平台上的多维度信息,能给持续提升开发者体验和生态繁荣。

(华为开源技术专家王晔晖)

本次分论坛的最后,华为OpenHarmony跨平台框架专家高迪分享了关于Flutter Impeller-Vulkan OpenHarmony化方案和性能优化实践的内容。高迪认为Impeller渲染引擎是Flutter保持优势竞争力的核心组件,高迪深入解析了将Impeller渲染引擎应用到OpenHarmony系统中所面临的跨平台兼容性和性能挑战,并分享了相应的优化方案和实践经验。

(华为OpenHarmony跨平台框架专家高迪)

通过本次分论坛的内容分享,专家们对OpenHarmony应用生态的发展做了总结和展望,大家一致认为OpenHarmony的应用生态在技术共建、生态共享方面将迎来更大的发展机遇,OpenHarmony已经在多个领域展现出强大的潜力,开源社区和企业的紧密合作是OpenHarmony生态建设的关键,这种跨界合作不仅增强了OpenHarmony的应用能力,实现了应用生态的蓬勃发展,而且为整个产业链创造了更多的创新机会。

收起阅读 »

第三届OpenHarmony技术大会通信互联分论坛圆满举行

长期以来,通信互联行业面临着包括设备兼容性差、缺乏统一的通信标准、数据安全和隐私保护问题、设备管理和维护成,高等挑战。随着物联网设备数量的增加,如何实现高效、稳定的设备间通信,成为行业研究重要方向。OpenAtom OpenHarmony(以下简称“OpenH...
继续阅读 »

长期以来,通信互联行业面临着包括设备兼容性差、缺乏统一的通信标准、数据安全和隐私保护问题、设备管理和维护成,高等挑战。随着物联网设备数量的增加,如何实现高效、稳定的设备间通信,成为行业研究重要方向。OpenAtom OpenHarmony(以下简称“OpenHarmony”)作为开源的操作系统,带来了一种全新的技术架构和解决方案,其统一互联的技术底座为设备间的无缝协作提供了可能。

2024年10月12日,第三届OpenHarmony技术大会通信互联分论坛在上海世博中心举行。该分论坛主要面向OpenHarmony设备开发者、解决方案合作伙伴、设备集成商以及高校和科研机构。分论坛活动从下午2点持续到5点,与会嘉宾就OpenHarmony的通信互联技术进行了全面的分享与讨论。聚焦于探讨社区伙伴如何基于OpenHarmony统一互联构建创新性的行业实践,主题内容涵盖了从基础协议到实际应用的多个层面,旨在推动技术的进一步发展和行业的应用实践。

分论坛由OpenHarmony通信互联TSG主任、华为OpenHarmony网络协议首席架构师李杰和OpenHarmony统一互联PMC(筹)负责人吕鑫担任出品人,并在分论坛进行内容分享。

OpenHarmony项目群工作委员会执行主席、华为终端BG软件部副总裁柳晓见,西安交通大学OpenHarmony技术俱乐部主任、副教授李昊,上海海思解决方案首席架构师姚亚群,中国移动杭州研究院家庭IoT产品部副总经理施超,中国科学院软件研究所高级工程师陈美汝,鸿湖万联(江苏)科技发展有限公司高级架构师韩琰,深圳开鸿数字产业发展有限公司高级工程师张芳舵,江苏润开鸿数字科技有限公司研发总监张勇赛,湖南开鸿智谷数字产业发展有限公司研发中心产品研发部部长蔡志刚,深圳华龙讯达信息技术股份有限公司交互技术研究所所长张喜权,华为分享技术专家王春风等13位嘉宾出席活动并发表演讲。

OpenHarmony项目群工作委员会执行主席、华为终端BG软件部副总裁柳晓见带来以《OpenHarmony统一互联打造万物互联的智能世界》为主题的开篇致辞,柳晓见强调了OpenHarmony在构建智能世界中的关键作用,以及通过统一互联技术实现设备间的无缝协作的重要意义。柳晓见指出,OpenHarmony正加速成为千行百业的数字底座,技术架构领先,打造先进智能终端操作系统体验,基于OpenHarmony的商用发行版和商用设备覆盖千行百业,已成为智能终端操作系统根社区,OpenHarmony设备统一互联标准为统一生态助力,面向未来,OpenHarmony将赋能更多终端形态,基于OpenHarmony统一互联打造智能世界。

(OpenHarmony项目群工作委员会执行主席、华为终端BG软件部副总裁柳晓见发言)

要构建一个万物互联的智能世界,就要降低不同设备间的通信协议,降低信息交互的门槛。华为2012实验室专家、OpenHarmony通信互联TSG主任和华为OpenHarmony网络协议首席架构师李杰,详细介绍了“OpenHarmony下一代通信互联技术演进以及通途极简协议构建策略”。李杰探讨了通途极简协议如何为OpenHarmony通信互联技术底座提供支持,以及如何面对无线高抖动、终端平台能力差异和功耗约束等挑战以及无限通信标准向更高带宽、极致低时延、高可靠等方面的演进趋势,基于OpenHarmony统一互联社区开源部分协议和竞争力,赋能社区伙伴更高效的实现通信互联。

(华为2012实验室专家、OpenHarmony通信互联TSG主任和

华为OpenHarmony网络协议首席架构师李杰发言)

在OpenHarmony 5.0版本之前,即使通过XTS认证,但跨厂商OH设备之间不互通,HOS设备和OH设备系统不互通,这成为有共同连接诉求的伙伴之间的阻碍。在此背景下,OpenHarmony统一互联PMC应运而生。OpenHarmony统一互联PMC(筹)负责人吕鑫介绍了OpenHarmony统一互联PMC建立初衷、共建进展并阐释了建设目标。从启动孵化共建项目,聚焦OH和HOS之间,以及OH不同设备厂家之间设备的互联互通,包括富对瘦设备控制、富对富投屏、富对富文件互传这3大类应用场景,到继续丰富拓展到业务接续、分布式摄像头等场景,后续逐步扩展到跟三方OS之间的互联互通,OpenHarmony统一互联PMC致力于打造一个坚实的互联互通互操作的数字底座,通过共建共享共成长的方式,夯实联接能力,丰富应用场景,实现设备间的万物互联。

(OpenHarmony统一互联PMC(筹)负责人吕鑫发言)

西安交通大学OpenHarmony技术俱乐部的主任、副教授李昊介绍了“面向多模态场景的可编程网络协议栈技术”。李昊对比了创新可编程协议栈技术相比传统网络协议栈在多样化传输协议和动态协议层结构方面的优势,探讨了全可编程的网络体系结构在满足千行百业对网络灵活性和性能需求方面的重要价值,并阐述了可编程协议栈技术通过高级抽象大幅简化开发流程、提升系统性能,及通过编译器的中间表示层进行优化,在保持高性能的同时,实现灵活扩展,在多变网络环境中具有广泛适用性的突出特点。

(西安交通大学OpenHarmony技术俱乐部的主任、副教授李昊发言)

上海海思分享了基于OpenHarmony统一互联标准构筑智能终端芯片方案底座。上海海思面向“消费电子、智慧家庭、汽车电子”三大场景,打造5+2智能终端解决方案,覆盖音视频和联接等领域。5+2解决方案携手OpenHarmony,从底层软硬芯一体的深度优化,并基于系统软总线实现跨终端场景的媒体处理、感知能力共享,带来星闪指向遥控、穿戴手表信息流转、分布式内容共享等更便捷的跨设备互联互通的应用场景,并在未来提供更丰富的OpenHarmony生态场景解决方案。

(上海海思解决方案首席架构师发言)

智能家居已成为近年来家装领域的热门话题,一个能够洞悉居住者需求并主动提供服务的家,标志着我们向更智能化的生活方式迈进。中国移动杭州研究院家庭IoT产品部副总经理施超分享了中国移动基于OpenHarmony统一互联打造高品质全屋智能解决方案的创新案例。施超指出,在家庭IoT领域,操作系统的碎片化和互联互通的难题日益凸显。中国移动基于OpenHarmony构建的“移鸿”操作系统,旨在覆盖家庭网络设备、感知设备、娱乐设备、中控设备和算力设备等全量终端,实现家庭算力、存储、网络资源的共享和统一调度。

(中国移动杭州研究院家庭IoT产品部副总经理施超发言)

中国科学院软件研究所高级工程师陈美汝,探讨了“面向OpenHarmony PC形态的统一互联挑战”。陈美汝表示,“在OpenHarmony全场景时代下,统一互联为PC设备带来了新的可能性,同时也面临着硬件异构性和软件生态局限等挑战”。她分享了中国科学院软件研究所如何面向OpenHarmony PC形态打造智慧互联与智能协同生态,为用户提供高效任务处理与无缝操作智能化体验,服务PC形态下的远程协同、办公教育等场景。

(中国科学院软件研究所高级工程师陈美汝发言)

商用液晶显示行业过去一直存在配网过程繁琐、连接标准不统一的问题。鸿湖万联(江苏)科技发展有限公司高级架构师韩琰,介绍了“OpenHarmony统一互联赋能商显行业跨端融合”。韩琰阐述了SwanLinkOS 5商业发行版如何集成统一互联技术底座,实现不同形态设备的连接组网、设备控制、文件互传,赋能商显行业跨端融合,提升用户体验,打通不同设备之间的信息鸿沟,从而实现不同设备之间的高效互联互通。

(鸿湖万联(江苏)科技发展有限公司高级架构师韩琰)

深圳开鸿数字产业发展有限公司的高级工程师张芳舵,分享了“OpenHarmony星闪统一互联赋能燃气行业智慧化控制新篇章”。张芳舵介绍了星闪技术发展历程和性能特点,以及该公司基于OpenHarmony打造的统一物联智能软件商业发行版KaihongOS。据悉,深开鸿面向千行百业提供全量系统化部署“KaihongOS+星闪”技术服务,KaihongOS旨在让设备互联更安全、更智能。

(深圳开鸿数字产业发展有限公司的高级工程师张芳舵发言)

江苏润开鸿数字科技有限公司研发总监张勇赛,介绍了“OpenHarmony统一互联构建能源行业数字底座”。能源行业设备类型多样、厂商众多、通信协议复杂,导致设备互联困难、维护成本高。张勇赛分享了HiHopeOS如何在OpenHarmony统一互联技术基础上,构建能源行业设备互联的整体解决方案,通过增加多协议转换、拖拉组网、统一平台等系统能力,有效解决行业痛点。

(江苏润开鸿数字科技有限公司研发总监张勇赛发言)

湖南开鸿智谷数字产业发展有限公司研发中心产品研发部部长蔡志刚向现场观众进行“分布式相机OpenHarmony统一互联实践”的分享。蔡志刚围绕智慧城市、智慧家庭、智慧园区等商业化场景,基于上海海思不同芯片不同设备,展示了如何通过OpenHarmony的核心软件能力实现互联互通,为未来OpenHarmony生态商业化拓展提供了丰富的案例。

(湖南开鸿智谷数字产业发展有限公司研发中心产品研发部部长蔡志刚发言)

深圳华龙讯达信息技术股份有限公司交互技术研究所所长张喜权,介绍了“轻工行业OpenHarmony统一互联创新应用实践”。张喜权阐述了基于Openharmony工业操作系统和华龙讯达自主可控的工业自动化平台,如何在制造业的智能化应用场景中进行研究和实践。他提到,这一数字工厂解决方案以Openharmony操作系统为核心,整合了先进的智能制造技术,为制造企业提供全面的数字化转型支持。

(深圳华龙讯达信息技术股份有限公司交互技术研究所所长张喜权发言)

作为最后一位登台嘉宾,华为的分享技术专家王春风带来了“OpenHarmony统一互联2.0文件互传规范技术分享”。王春风讨论了OpenHarmony在设备互联互通方面面临的挑战,包括生态大屏、广告牌等与HarmonyOS设备间的文件互传需求。OpenHarmony统一互联文件规范,以及如何基于社区开源部分协议和竞争力,赋能社区伙伴更高效地实现统一互联。

(华为的分享技术专家王春风发言)

第三届OpenHarmony技术大会的通信互联分论坛汇聚了行业专家、学者以及技术从业者,共同探讨了OpenHarmony在通信互联领域的最新进展和未来趋势。论坛中,嘉宾们深度分享了从基础的网络协议到实际的行业应用,再到操作系统的创新实践,内容涵盖了智能终端、家庭IoT、燃气行业、能源行业以及轻工行业的数字化转型。这些分享不仅展示了OpenHarmony技术在多个领域的广泛应用,也反映了其在推动行业创新中的重要作用。

通过深入的讨论和技术展示,分论坛明确了OpenHarmony统一互联技术在解决设备互联互通问题上的巨大潜力。随着技术的不断成熟和应用场景的不断拓展,OpenHarmony已经成为连接不同设备和行业的重要桥梁。随着更多行业伙伴的加入和技术创新的持续推进,OpenHarmony将有望进一步提升其在全场景智能互联中的影响力,为用户带来更加丰富和便捷的智能体验。

分论坛最后,OpenHarmony项目群工作委员会执行主席、华为终端BG软件部副总裁柳晓见为OpenHarmony统一互联非凡伙伴进行了授牌,致谢他们在OpenHarmony统一互联共建中的突出贡献。此次授牌的非凡伙伴包括:深圳开鸿数字产业发展有限公司,鸿湖万联(江苏)科技发展有限公司,湖南开鸿智谷数字产业发展有限公司,鼎桥通信技术有限公司,山东亚华电子股份有限公司,广东九联开鸿科技发展有限公司,深圳鸿信智联数字科技有限公司。

此外,柳晓见还为OpenHarmony统一互联先锋专家授牌,以表达对专家们在OpenHarmony统一互联共建中敢于担责,表现突出,起到先锋模范作用的致谢,此处被授予OpenHarmony统一互联先锋专家包括:刘永保、张国荣、沈春萍、韩琰、陈施、王从鼎、江哲凯、王东东、徐昌、侯乐武、白政锋、喻绍强、李贵、胡孝东、庞敏、王纪睿、王小松、刁月磊、刘浩、张坤、李昊、高伟、张振。(排名不分先后)

收起阅读 »

你是否遇到过断网检测的需求?

web
你也碰到断网检测的需求啦? 一般的断网检测需求,大部分都是在用户网络状态不好或者网络掉线时,我们给用户一个提示或者引导界面,防止用户不知道自己卡了在那里一直等待。 方案1,轮询请求 直接说最有效的方案,我们通过轮训请求来检测网络是否可用,比如加载图片或者访问接...
继续阅读 »

你也碰到断网检测的需求啦?


一般的断网检测需求,大部分都是在用户网络状态不好或者网络掉线时,我们给用户一个提示或者引导界面,防止用户不知道自己卡了在那里一直等待。


方案1,轮询请求


直接说最有效的方案,我们通过轮训请求来检测网络是否可用,比如加载图片或者访问接口等...

下面我以加载图片为例,搞个小demo:



  1. 首先尽可能的找一个小的图片,不要让图片的请求堵塞我们的其他功能使用。
    推荐一个图片在线的压缩的网站: https://www.yalijuda.com/ ,然后把图片上到内部的服务器上


    image.png

  2. 既然搞了,就搞个通用的,使用tsup我们搞个npm包,然后想一想我们要搞的功能,先把入口函数的出入参类型定了。我们既然要做轮训请求图片,我们首先需要一个图片地址,然后请求后的回调事件,甚至可能需要一些控制参数,那我们的入口代码也就有了。


    const request = (imgUrl) => {
    // do something
    };

    type CheckNetworkOptionsType = {
    interval: number; // 循环时间 单位ms
    };

    type CheckNetworkType = (
    imgUrl: string, // 图片url
    callback: (isOnline: boolean) => void, // 回调,返回网络是否在线
    options?: CheckNetworkOptionsType, // 配置项
    ) =>
    void;

    const checkNetwork: CheckNetworkType = (imgUrl, callback, options) => {
    const { interval = 30_000 } = options || {};
    const timer = setInterval(() => {
    request(imgUrl)
    }, interval);
    return timer
    };

    export default checkNetwork


  3. 接下来我们要考虑一下如何进行请求,我们需要一个创一个promiseimg标签resove 出去 onloadonerror 对应的在线和离线状态。


    const request = (imgUrl) => {
    return new Promise((resolve) => {
    let imgRef = null;
    let isRespond = false;
    imgRef = document.createElement('img');
    imgRef.onerror = () => {
    isRespond = true;
    resolve(false);
    return '';
    };
    imgRef.onload = () => {
    isRespond = true;
    resolve(true);
    return '';
    };
    imgRef.src = `${imgUrl}?time=${new Date().toLocaleString()}`;

    });
    };

    type CheckNetworkOptionsType = {
    interval: number; // 循环时间 单位ms
    };

    type CheckNetworkType = (
    imgUrl: string, // 图片url
    callback: (isOnline: boolean) => void, // 回调,返回网络是否在线
    options?: CheckNetworkOptionsType, // 配置项
    ) =>
    void;

    const checkNetwork: CheckNetworkType = (imgUrl, callback, options) => {
    const { interval = 30_000 } = options || {};
    const timer = setInterval(async () => {
    const status = (await request(imgUrl)) as boolean;
    callback(status);
    }, interval);
    return timer
    };

    export default checkNetwork


  4. 这样基本的功能似乎就差不多了,但是感觉好像少点什么?比如服务器就是返回图片资源慢?那我们是不是可以加个超时时间?又或者是不是可以可以让用户手动取消循环?


    const request = (imgUrl) => {
    return new Promise((resolve) => {
    let imgRef = null;
    let isRespond = false;
    imgRef = document.createElement('img');
    imgRef.onerror = () => {
    isRespond = true;
    resolve(false);
    return '';
    };
    imgRef.onload = () => {
    isRespond = true;
    resolve(true);
    return '';
    };
    // 加个参数,防止浏览器缓存
    imgRef.src = `${imgUrl}?time=${new Date().toLocaleString()}`;

    });
    };

    type CheckNetworkOptionsType = {
    interval: number; // 循环时间 单位ms
    };

    type CheckNetworkType = (
    imgUrl: string, // 图片url
    callback: (isOnline: boolean) => void, // 回调,返回网络是否在线
    options?: CheckNetworkOptionsType, // 配置项
    ) =>
    void;

    const checkNetwork: CheckNetworkType = (imgUrl, callback, options) => {
    const { interval = 30_000 } = options || {};
    const timer = setInterval(async () => {
    const status = (await request(imgUrl)) as boolean;
    callback(status);
    }, interval);
    return timer
    };

    export default checkNetwork


  5. 完整的代码就完整了,具体用的时间还是建议大家关键模块来进行断网检测。不要一个后台配置表单都弄检测,这种完全可以在提交表单的时候接口响应进行处理,断网检测一般都是用在需要实时监控之类的。不多说了,我们来体验下:


image.png


image.png
6. 没问题,一切ok,发个包,就叫network-watcher吧,欢迎大家star!

github.com/waltiu/netw…


方案2,直接调api


首先说下这种方案不推荐,其一浏览器兼容性有问题,其二只能检测到是否网络有连接,但是不能检测是否可用,这种实用性真的很差。

浏览器也提供了navigator.onLinenavigator.connection 可以直接查询网络状态,我们可以监听网络状态的变化。


image.png


    window.addEventListener('online',function () {
alert("正常上网");
})
window.addEventListener('offline',function () {
alert('无网络');
})

这种实用性真的很差,用户网络连接但是没有网或者网很慢,实际上都会影响用户体验!!


用户的体验永远是NO.1


作者:Waltiu
来源:juejin.cn/post/7299671709476700212
收起阅读 »

MapStruct这么用,同事也开始模仿

前言 hi,大家好,我是大鱼七成饱。 前几天同事review我的代码,发现mapstruct有这么多好用的技巧,遇到POJO转换的问题经常过来沟通。考虑到不可能每次都一对一,所以我来梳理五个场景,谁在过来问,直接甩出总结。 环境准备 由于日常使用都是spri...
继续阅读 »

前言


hi,大家好,我是大鱼七成饱。


前几天同事review我的代码,发现mapstruct有这么多好用的技巧,遇到POJO转换的问题经常过来沟通。考虑到不可能每次都一对一,所以我来梳理五个场景,谁在过来问,直接甩出总结。


1641341087201917.jpeg


环境准备


由于日常使用都是spring,所以后面的示例都是在springboot框架中运行的。关键pom依赖如下:


<properties>
<java.version>1.8</java.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.30</org.projectlombok.version>
</properties>
<dependencies>

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

场景一:常量转换


这是最简单的一个场景,比如需要设置字符串、整形和长整型的常量,有的又需要日期,或者新建类型。下面举个例子,演示如何转换


//实体类
@Data
public class Source {
private String stringProp;
private Long longProp;
}
@Data
public class Target {
private String stringProperty;
private long longProperty;
private String stringConstant;
private Integer integerConstant;
private Long longWrapperConstant;
private Date dateConstant;
}


  • 设置字符串常量

  • 设置long常量

  • 设置java内置类型默认值,比如date


那么mapper这么设置就可以


@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface SourceTargetMapper {

@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1l")
@Mapping(target = "stringConstant", constant = "Constant Value")
@Mapping(target = "integerConstant", constant = "14")
@Mapping(target = "longWrapperConstant", constant = "3001L")
@Mapping(target = "dateConstant", dateFormat = "yyyy-MM-dd", constant = "2023-09-")
Target sourceToTarget(Source s);
}

解释下,constant用来设置常量值,source的值如果没有设置,则会使用defaultValue的值,日期可以按dateFormat解析。


Talk is cheap, show me the code !废话不多说,自动生成的转换类如下:


@Component
public class SourceTargetMapperImpl implements SourceTargetMapper {
public SourceTargetMapperImpl() {
}

public Target sourceToTarget(Source s) {
if (s == null) {
return null;
} else {
Target target = new Target();
if (s.getStringProp() != null) {
target.setStringProperty(s.getStringProp());
} else {
target.setStringProperty("undefined");
}

if (s.getLongProp() != null) {
target.setLongProperty(s.getLongProp());
} else {
target.setLongProperty(-1L);
}

target.setStringConstant("Constant Value");
target.setIntegerConstant(14);
target.setLongWrapperConstant(3001L);

try {
target.setDateConstant((new SimpleDateFormat("dd-MM-yyyy")).parse("09-01-2014"));
return target;
} catch (ParseException var4) {
throw new RuntimeException(var4);
}
}
}
}

是不是一目了然


image-20231105105234857.png


场景二:转换中调用表达式


比如id不存在使用UUID生成一个,或者使用已有参数新建一个对象作为属性。当然可以用after mapping,qualifiedByName等实现,感觉还是不够优雅,这里介绍个雅的(代码少点的)。


实体类如下:


@Data
public class CustomerDto {
public Long id;
public String customerName;

private String format;
private Date time;
}
@Data
public class Customer {
private String id;
private String name;
private TimeAndFormat timeAndFormat;
}
@Data
public class TimeAndFormat {
private Date time;
private String format;

public TimeAndFormat(Date time, String format) {
this.time = time;
this.format = format;
}
}

Dto转customer,加创建TimeAndFormat作为属性,mapper实现如下:


@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, imports = UUID.class)
public interface CustomerMapper {

@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")

@Mapping(target = "id", source = "id", defaultExpression = "java( UUID.randomUUID().toString() )")
Customer toCustomer(CustomerDto s);

}

解释下,id为空则走默认的defaultExpression,通过imports引入,java括起来调用。新建对象直接new TimeAndFormat。有的小伙伴喜欢用qualifiedByName自定义方法,可以对比下,哪个合适用哪个,都能调用转换方法。


生成代码如下:


@Component
public class CustomerMapperImpl implements CustomerMapper {
public CustomerMapperImpl() {
}

public Customer toCustomer(CustomerDto s) {
if (s == null) {
return null;
} else {
Customer customer = new Customer();
if (s.getId() != null) {
customer.setId(String.valueOf(s.getId()));
} else {
customer.setId(UUID.randomUUID().toString());
}

customer.setTimeAndFormat(new TimeAndFormat(s.getTime(), s.getFormat()));
return customer;
}
}
}

场景三:类共用属性,如何复用


比如下面的Bike和车辆类,都有id和creationDate属性,我又不想重复写mapper属性注解


public class Bike {
/**
* 唯一id
*/

private String id;

private Date creationDate;

/**
* 品牌
*/

private String brandName;
}

public class Car {
/**
* 唯一id
*/

private String id;

private Date creationDate;
/**
* 车牌号
*/

private String chepaihao;
}

解决起来很简单,写个共用的注解,使用的时候引入就可以,示例如下:


//通用注解
@Retention(RetentionPolicy.CLASS)
//自动生成当前日期
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
//忽略id
@Mapping(target = "id", ignore = true)
public @interface ToEntity { }

//使用
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface TransportationMapper {
@ToEntity
@Mapping( target = "brandName", source = "brand")
Bike map(BikeDto source);

@ToEntity
@Mapping( target = "chepaihao", source = "plateNo")
Car map(CarDto source);
}

这里Retention修饰ToEntity注解,表示ToEntity注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期,辅助生成mapper实现类。上面定义了creationDate和id的转换规则,新建日期,忽略id。


生成的mapper实现类如下:


@Component
public class TransportationMapperImpl implements TransportationMapper {
public TransportationMapperImpl() {
}

public Bike map(BikeDto source) {
if (source == null) {
return null;
} else {
Bike bike = new Bike();
bike.setBrandName(source.getBrand());
bike.setCreationDate(new Date());
return bike;
}
}

public Car map(CarDto source) {
if (source == null) {
return null;
} else {
Car car = new Car();
car.setChepaihao(source.getPlateNo());
car.setCreationDate(new Date());
return car;
}
}
}

坚持一下,还剩俩场景,剩下的俩更有意思


image-20231105111309795.png


场景四:lombok和mapstruct冲突了


啥冲突?用了builder注解后,mapstuct转换不出来了。哎,这个问题困扰了我那同事两天时间。


解决方案如下:


 <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>

加上lombok-mapstruct-binding就可以了,看下生成的效果:


@Builder
@Data
public class Person {
private String name;
}
@Data
public class PersonDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface PersonMapper {

Person map(PersonDto dto);
}
@Component
public class PersonMapperImpl implements PersonMapper {
public PersonMapperImpl() {
}

public Person map(PersonDto dto) {
if (dto == null) {
return null;
} else {
Person.PersonBuilder person = Person.builder();
person.name(dto.getName());
return person.build();
}
}
}

从上面可以看到,mapstruct匹配到了lombok的builder方法。


场景五:说个难点的,转换的时候,如何注入springBean


image-20231105112031297.png
有时候转换方法比不是静态的,他可能依赖spring bean,这个如何导入?


这个使用需要使用抽象方法了,上代码:


@Component
public class SimpleService {
public String formatName(String name) {
return "您的名字是:" + name;
}
}
@Data
public class Student {
private String name;
}
@Data
public class StudentDto {
private String name;
}
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class StudentMapper {

@Autowired
protected SimpleService simpleService;

@Mapping(target = "name", expression = "java(simpleService.formatName(source.getName()))")
public abstract StudentDto map(StudentDto source);
}

接口是不支持注入的,但是抽象类可以,所以采用抽象类解决,后面expression直接用皆可以了,生成mapperimpl如下:


@Component
public class StudentMapperImpl extends StudentMapper {
public StudentMapperImpl() {
}

public StudentDto map(StudentDto source) {
if (source == null) {
return null;
} else {
StudentDto studentDto = new StudentDto();
studentDto.setName(this.simpleService.formatName(source.getName()));
return studentDto;
}
}
}

思考


以上场景肯定还有其他解决方案,遵循合适的原则就可以。驾驭不了的代码,可能带来更多问题,先简单实现,后续在迭代优化可能适合更多的业务场景。


本文示例代码放在了github,需要的朋友请关注公众号大鱼七成饱,回复关键词MapStruct使用即可获得。


image-20231105112515470.png


作者:大鱼七成饱
来源:juejin.cn/post/7297222349731627046
收起阅读 »

安卓开发转做鸿蒙后-开篇

一、为什么转做鸿蒙 本人从事安卓开发已近十年,大部分时间还是在不停的需求迭代,或者一遍遍优化各种轮子,自己的职业生涯已经进入了瓶颈期,同时现有工作也很难让自己产生成就感。正好年初有机会转入鸿蒙开发团队,虽然清楚肯定少不了加班,最终也不一定会有预期中的产出,还是...
继续阅读 »

一、为什么转做鸿蒙


本人从事安卓开发已近十年,大部分时间还是在不停的需求迭代,或者一遍遍优化各种轮子,自己的职业生涯已经进入了瓶颈期,同时现有工作也很难让自己产生成就感。正好年初有机会转入鸿蒙开发团队,虽然清楚肯定少不了加班,最终也不一定会有预期中的产出,还是希望自己能有一些新东西的刺激和积累。


二、App鸿蒙化的回顾


本人所在公司差不多算是中厂,C端App日活大概有个几百万,各部门团队大概有30人+,历时半年多的时间,差不多完成了全部功能70%左右。前期主要是个人自学及各种培训、前期调研、App基础库的排期、业务排期、开发上架等几个环节。


1、基础库



  • 网络库

  • 图片库

  • 埋点库

  • 路由库

  • 公共组件

  • 崩溃监控

  • 打包构建


2、业务排期



  • 业务拆分优先级

  • 分期迭代开发测试


三、跟安卓相比的差异性


1、ArkUI和Android布局



  • Android控件习惯于宽高自适应,ArkUI中部分子组件会超过容器组件区域,所以部分组件需要控制宽度

  • Android是命令式UI比较简单直接,ArkUI是声明式,需要重点关注状态管理的合理使用

  • Android列表重复相对简单,ArkUI中List懒加载和组件复用使用比较繁琐

  • Android基于Java可以通过继承抽取一些公共能力,ArkUI组件无法进行继承


2、鸿蒙开发便捷的一面


1、问题的反馈和响应比较及时,华为技术支持比较到位。


2、应用市场对性能要求和各类适配要求比较高,倒逼开发提高自己的开发能力。


3、跟安卓比提供了各种相对完善的组件,避免了开发者需要进行各种封装



  • 路由库

  • 网络库

  • 图片库

  • 扫码

  • 人脸识别

  • picker

  • 统一拖拽

  • 预加载服务

  • 应用接续

  • 智能填充

  • 意图框架

  • AI语音识别


3、鸿蒙开发不便的一面



  • ArkTS文档不够完善,没有从0到1的完整学习流程

  • ArkUI部分组件使用繁琐

  • DevEco-Studio的稳定性需要提升

  • 组件渲染性能需要提升,


四、跨平台方案



  • RN

  • Flutter

  • ArkUI-X


ArkUI-X作为鸿蒙主推的跨平台框架,主要问题是生态的建立和稳定性。所以还是要基于公司基建的完善程度和技术生态进行选择。同时由于鸿蒙的加入,适配3个OS系统的成本提高,公司为降本提效会加快跨平台技术的接入和推进。后续还是需要熟悉跨平台开发的技术。


五、知识体系(待完善)


1、ArkTS应用


1、应用程序包结构(hap、har、hsp)


2、整体架构


3、开发模型


2、ArkTs


1、基本语法


2、方舟字节码


3、容器类库


4、并发


3、ArkUI


1、基本语法


2、声明式UI描述


3、自定义组件


4、装饰器


5、状态管理


6、渲染控制


4、Stage模型


1、应用配置文件


2、应用组件


3、后台任务


4、进程模块


5、线程模型


5、性能优化


1、冷启动


2、响应时延


3、完成时延


4、滑动帧率


5、包大小


作者:村口老王
来源:juejin.cn/post/7409877909999026217
收起阅读 »

拼多多冷启真的秒开

背景 最近在使用拼多多购物,除了价格比较香之外,每次冷启打开的体验非常好,作为一个Android开发不免好奇 简单分析记录一下 冷启数据 体验好,让我想到了郭德纲的那句话"全靠同行的衬托",那找几个同行过来对比下,这里使用淘宝、京东、闲鱼,从点击开始图标开始录...
继续阅读 »



背景


最近在使用拼多多购物,除了价格比较香之外,每次冷启打开的体验非常好,作为一个Android开发不免好奇 简单分析记录一下


冷启数据


体验好,让我想到了郭德纲的那句话"全靠同行的衬托",那找几个同行过来对比下,这里使用淘宝、京东、闲鱼,从点击开始图标开始录个屏直接数秒,



测试手机是 华为 Mate 60



我这个样本比较少,机器性能也比较好,仅仅是个人对比,不代表大众使用的真实情况


可以粗略的看几个常见app的冷启对比下,这里录屏使用的剪映来分析
帧率为 每秒30帧,后面会涉及一些时间换算
image.png


拼多多


无广告冷启动 从点击图标到到首页完整展示 大概花了 29帧,
1000ms * 29/30 约为 0.96s
,太惊人了,基本冷启秒开



拼多多可能真的没有开屏广告,我印象中没有见过拼多多的开屏广告



image.pngimage.png


淘宝


无广告冷启东 从点击图标到到首页完整展示
image.png
image.png
大概花了 1s+21帧,
1000ms+ 21/30*1000ms = 1.7s
还可以



淘宝可能没有开屏广告,或者非常克制,我刷了十几次都没有见到开屏广告



京东


无广告冷启京东
从点击图标到到首页完整展示
image.png
image.png


大概花了 1s+28帧,
1000ms + 28/30*1000ms 约为 1.93s
也是不错的



不过京东的开屏有开屏广告,但是做了用户频控,刷了几次就没了,这里仅对比无广告冷启开屏



闲鱼


毕竟是国内最大的二手平台(虽然现在小商家也特别多),而且是flutter深度使用者,看看它的表现
image.png
image.png
大概花了** 2s+10帧**
2000ms+ 10/30*1000ms 约为 2.3s


image.png
从上面数据来看,怪不得 我使用拼多多之后,打开app 确实比较舒服,因为我就是奔着买东西去的,越快到购物页面越舒服的。或许这就是极致的用户体验吧


首屏细节


拼多多的首页数据咋这么快就准备好了,网络耗时应该也有呢,应该是它提前准备好了数据
image.png
我们来实操验证下



  • 切后台的截图


我们记住 手枪、去虾线、行李箱、停电 这几个卡片
image.png


冷启打开之后首先展示的是 还是切后台之前的数据
image.png
紧接着网络数据到了做了一次屏幕刷新
image.png


到这里大概就明白了,冷启使用上次feeds流的数据,先让用户看到数据,然后等新数据请求到之后再刷新页面就好


为了严谨点,把缓存数据清除的话,那么肯定首次冷启白屏,ok最后再验证一下
image.png


此时冷启白茫茫的一片,看来拼多多的策略还是让用户尽快进应用优先,或者这里并没有刻意设计🤔,都是先进首页有缓存就使用 没有的话就等网络数据,毕竟这种情况也只是新用户或者缓存数据过期才会这样
image.png


因此这里我可以得出把这种缓存优先的技术方案也可以学习学习,看看我们自己的app是不是可以复用一下,绩效这不就来了吗🤔
首页 = 数据 + UI
数据是使用缓存,UI也能吧一些UI组件提前预加载,不过这里也无法判断 是否预加载了首页UI🤔


开屏无广告


我目前在字节就是搞广告的,所以对广告稍微敏感些,开屏广告是一个很棒收入来源,特别是合约广告这种,之前应用冷启时间长,有时候其实是故意抽出一些时间来等待冷启的开屏广告,
但是我试了很多次,确实没看过拼多多的开屏广告,不过从这个结果来看 肯定是 经过严密的ab实验,不过拼多多在开屏广告上确实比较克制,



关于现在互联网的计算广告业务还是蛮有意思的比如 广告类型有 开屏、原生、激励、插屏、横幅,sdk类型有单个adn或者聚合广告sdk,有时间再单独分享几篇。



image.png


冷启优化一些常见手段


冷启动往往是大型应用的必争之地



  1. 实打实的提升用户体验

  2. 可能会带来一些GMV的转化


拼多多技术是应该是有些东西的,但是非常低调,属于人狠话不多那种,也没找到他们的方案。这里结合自身经验聊聊这块,主要是以下4个阶段结合技术手段做优化
image.png


Application attachBaseContext


这个阶段由于 Applicaiton Context 赋值等问题,一般不会有太多的业务代码,可能的耗时会在低版本机器4.x机器比较多,首次由于MultiDex.install耗时



dex 的指令格式设计并不完善,单个 dex 文件中引用的 Java 方法总数不能超过 65536 个,在方法数超过 65536 的情况下,将拆分成多个 dex。一般情况下 Dalvik 虚拟机只能执行经过优化后的 odex 文件,在 4.x 设备上为了提升应用安装速度,其在安装阶段仅会对应用的首个 dex 进行优化。对于非首个 dex 其会在首次运行调用 MultiDex.install 时进行优化,而这个优化是非常耗时的,这就造成了 4.x 设备上首次启动慢的问题。



可以使用一些开源方案,比如 github.com/bytedance/B…
不过 这里优化难度比较大,roi的话 看看app低版本的机型占比再做决定


ContentProvider


这里要注意检查 ContentProvider,特别是一些sdk在 AndroidManifest 里面注册了自己的 xxSDkProvider,然后在 xxSDkProvider 的 onCreate 方面里面进行初始化,确实调用者不需要自己初始化了,可却增加了启动耗时,
我们可以打开 Apk,看一下最终merge的 AndroidManiest 里面有多少 provider,看一下是否有这样的骚操作,往往这里容易忽视,这种情况可以使用谷歌App Startup来收敛ContentProvider


Application 优化



  1. 精简Application 中的启动任务

  2. 基于进程进行任务排布,比如常见的push进程、webview进程


西瓜视频 在冷启优化就将 push、小程序、sandboxed这几个进程做了优化拿到一些不错的收益mp.weixin.qq.com/s/v23jEhF9k…



搞进程难度大风险高




  1. 启动链路任务编排


这里需要先梳理启动链路,做成1任务编排,



  1. 比如之前串2.2行的,搞成并行初始化

  2. 核心任务做有向无环图(DGA)编排,非核心的延迟初始化


idlehandler是个好东西。
image.png
image.png



关于初始化DGA框架有不少框架,谷歌官方也有个 App Startup,感兴趣可以研究下



首页优化


首页是用户感知到的第一个页面,也是冷启优化的关键,前面也提过 首页 = 数据 + UI



  1. 数据 可以使用缓存

  2. UI的话 通常是xml解析优化,或者预加载


在性能较差的手机上,xml inflate 的时间可能在 200 到 500 毫秒之间。自定义控件和较深的 UI 层级会加重这个解析耗时。
一些框架比如x2c,或者AsyncLayoutInflater 可以帮助我们在UI这里做做文章



  1. 插件化


把非核心模块做成插件,使用时候下载使用,一劳永逸,不过插件化也有各种弊端


后台任务优化


主线程相关耗时的优化,事实上除了主线程直接的耗时,后台任务的耗时也是会影响到我们的启动速度的,因为它们会抢占我们前台任务的 cpu、io 等资源,导致前台任务的执行时间变长,因此我们在优化前台耗时的同时也需要优化我们的后台任务



  1. 减少后台线程不必要的任务的执行,特别是一些重 CPU、IO 的任务;

  2. 对启动阶段线程数进行收敛,防止过多的并发任务抢占主线程资源,同时也可以避免频繁的线程间调度降低并发效率

  3. GC 抑制


触发 GC 后可能会抢占我们的 cpu 资源甚至导致我们的线程被挂起,如果启动过程中存在大量的 GC,那么我们的启动速度将会受到比较大的影响。通过hook手段在启动阶段去抑制部分类型的 GC,以达到减少 GC 的目的。这个就比较高端了,也是只在一些大厂文章里面见过。


OK 本期就到这里了


作者:程序员龙湫
来源:juejin.cn/post/7331607384932876326
收起阅读 »

请不要自己写,Spring Boot非常实用的内置功能

在 Spring Boot 框架中,内置了许多实用的功能,这些功能可以帮助开发者高效地开发和维护应用程序。 松哥来和大家列举几个。 一 请求数据记录 Spring Boot提供了一个内置的日志记录解决方案,通过 AbstractRequestLoggingFi...
继续阅读 »

在 Spring Boot 框架中,内置了许多实用的功能,这些功能可以帮助开发者高效地开发和维护应用程序。


松哥来和大家列举几个。


一 请求数据记录


Spring Boot提供了一个内置的日志记录解决方案,通过 AbstractRequestLoggingFilter 可以记录请求的详细信息。


AbstractRequestLoggingFilter 有两个不同的实现类,我们常用的是 CommonsRequestLoggingFilter



通过 CommonsRequestLoggingFilter 开发者可以自定义记录请求的参数、请求体、请求头和客户端信息。


启用方式很简单,加个配置就行了:


@Configuration
public class RequestLoggingConfig {
@Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setIncludeHeaders(true);
filter.setIncludeClientInfo(true);
filter.setAfterMessagePrefix("REQUEST ");
return filter;
}
}

接下来需要配置日志级别为 DEBUG,就可以详细记录请求信息:


logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG


二 请求/响应包装器


2.1 什么是请求和响应包装器


在 Spring Boot 中,请求和响应包装器是用于增强原生 HttpServletRequestHttpServletResponse 对象的功能。这些包装器允许开发者在请求处理过程中拦截和修改请求和响应数据,从而实现一些特定的功能,如请求内容的缓存、修改、日志记录,以及响应内容的修改和增强。


请求包装器



  • ContentCachingRequestWrapper:这是 Spring 提供的一个请求包装器,用于缓存请求的输入流。它允许多次读取请求体,这在需要多次处理请求数据(如日志记录和业务处理)时非常有用。


响应包装器



  • ContentCachingResponseWrapper:这是 Spring 提供的一个响应包装器,用于缓存响应的输出流。它允许开发者在响应提交给客户端之前修改响应体,这在需要对响应内容进行后处理(如添加额外的头部信息、修改响应体)时非常有用。


2.2 使用场景



  1. 请求日志记录:在处理请求之前和之后记录请求的详细信息,包括请求头、请求参数和请求体。

  2. 修改请求数据:在请求到达控制器之前修改请求数据,例如添加或修改请求头。

  3. 响应内容修改:在响应发送给客户端之前修改响应内容,例如添加或修改响应头,或者对响应体进行签名。

  4. 性能测试:通过缓存请求和响应数据,可以进行性能测试,而不影响实际的网络 I/O 操作。


2.3 具体用法


请求包装器的使用

import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class RequestWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
// 可以在这里处理请求数据
byte[] body = requestWrapper.getContentAsByteArray();
// 处理body,例如记录日志
//。。。
filterChain.doFilter(requestWrapper, response);
}
}

响应包装器的使用

import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class ResponseWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, responseWrapper);

// 可以在这里处理响应数据
byte[] body = responseWrapper.getContentAsByteArray();
// 处理body,例如添加签名
responseWrapper.setHeader("X-Signature", "some-signature");

// 必须调用此方法以将响应数据发送到客户端
responseWrapper.copyBodyToResponse();
}
}

在上面的案例中,OncePerRequestFilter 确保过滤器在一次请求的生命周期中只被调用一次,这对于处理请求和响应数据尤为重要,因为它避免了在请求转发或包含时重复处理数据。


通过使用请求和响应包装器,开发者可以在不改变原有业务逻辑的情况下,灵活地添加或修改请求和响应的处理逻辑。


三 单次过滤器


3.1 OncePerRequestFilter


OncePerRequestFilter 是 Spring 框架提供的一个过滤器基类,它继承自 Filter 接口。这个过滤器具有以下特点:



  1. 单次执行OncePerRequestFilter 确保在一次请求的生命周期内,无论请求如何转发(forwarding)或包含(including),过滤器逻辑只执行一次。这对于避免重复处理请求或响应非常有用。

  2. 内置支持:它内置了对请求和响应包装器的支持,使得开发者可以方便地对请求和响应进行包装和处理。

  3. 简化代码:通过继承 OncePerRequestFilter,开发者可以减少重复代码,因为过滤器的执行逻辑已经由基类管理。

  4. 易于扩展:开发者可以通过重写 doFilterInternal 方法来实现自己的过滤逻辑,而不需要关心过滤器的注册和执行次数。


3.2 OncePerRequestFilter 使用场景



  1. 请求日志记录:在请求处理之前和之后记录请求的详细信息,如请求头、请求参数和请求体,而不希望在请求转发时重复记录。

  2. 请求数据修改:在请求到达控制器之前,对请求数据进行预处理或修改,例如添加或修改请求头,而不希望这些修改在请求转发时被重复应用。

  3. 响应数据修改:在响应发送给客户端之前,对响应数据进行后处理或修改,例如添加或修改响应头,而不希望这些修改在请求包含时被重复应用。

  4. 安全控制:实现安全控制逻辑,如身份验证、授权检查等,确保这些逻辑在一次请求的生命周期内只执行一次。

  5. 请求和响应的包装:使用 ContentCachingRequestWrapperContentCachingResponseWrapper 等包装器来缓存请求和响应数据,以便在请求处理过程中多次读取或修改数据。

  6. 性能监控:在请求处理前后进行性能监控,如记录处理时间,而不希望这些监控逻辑在请求转发时被重复执行。

  7. 异常处理:在请求处理过程中捕获和处理异常,确保异常处理逻辑只执行一次,即使请求被转发到其他处理器。


通过使用 OncePerRequestFilter,开发者可以确保过滤器逻辑在一次请求的生命周期内只执行一次,从而避免重复处理和潜在的性能问题。这使得 OncePerRequestFilter 成为处理复杂请求和响应逻辑时的一个非常有用的工具。


OncePerRequestFilter 的具体用法松哥就不举例了,第二小节已经介绍过了。


四 AOP 三件套


在 Spring 框架中,AOP(面向切面编程)是一个强大的功能,它允许开发者在不修改源代码的情况下,对程序的特定部分进行横向切入。AopContextAopUtilsReflectionUtils 是 Spring AOP 中提供的几个实用类。


我们一起来看下。


4.1 AopContext


AopContext 是 Spring 框架中的一个类,它提供了对当前 AOP 代理对象的访问,以及对目标对象的引用。


AopContext 主要用于获取当前代理对象的相关信息,以及在 AOP 代理中进行一些特定的操作。


常见方法有两个:



  • getTargetObject(): 获取当前代理的目标对象。

  • currentProxy(): 获取当前的代理对象。


其中第二个方法,在防止同一个类中注解失效的时候,可以通过该方法获取当前类的代理对象。


举个栗子:


public void noTransactionTask(String keyword){    // 注意这里 调用了代理类的方法
((YourClass) AopContext.currentProxy()).transactionTask(keyword);
}

@Transactional
void transactionTask(String keyword) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) { //logger
//error tracking
}
System.out.println(keyword);
}

同一个类中两个方法,noTransactionTask 方法调用 transactionTask 方法,为了使事务注解不失效,就可以使用 AopContext.currentProxy() 去获取当前代理对象。


4.2 AopUtils


AopUtils 提供了一些静态方法来处理与 AOP 相关的操作,如获取代理对象、获取目标对象、判断代理类型等。


常见方法有三个:



  • getTargetObject(): 从代理对象中获取目标对象。

  • isJdkDynamicProxy(Object obj): 判断是否是 JDK 动态代理。

  • isCglibProxy(Object obj): 判断是否是 CGLIB 代理。


举个栗子:


import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;

public class AopUtilsExample {
public static void main(String[] args) {
MyService myService = ...
// 假设 myService 已经被代理
if (AopUtils.isCglibProxy(myService)) {
System.out.println("这是一个 CGLIB 代理对象");
}
}
}

4.3 ReflectionUtils


ReflectionUtils 提供了一系列反射操作的便捷方法,如设置字段值、获取字段值、调用方法等。这些方法封装了 Java 反射 API 的复杂性,使得反射操作更加简单和安全。


常见方法:



  • makeAccessible(Field field): 使私有字段可访问。

  • getField(Field field, Object target): 获取对象的字段值。

  • invokeMethod(Method method, Object target, Object... args): 调用对象的方法。


举个栗子:


import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.Map;

public class ReflectionUtilsExample {
public static void main(String[] args) throws Exception {
ExampleBean bean = new ExampleBean();
bean.setMapAttribute(new HashMap<>());

Field field = ReflectionUtils.findField(ExampleBean.class, "mapAttribute");
ReflectionUtils.makeAccessible(field);

Object value = ReflectionUtils.getField(field, bean);
System.out.println(value);
}

static class ExampleBean {
private Map<String, String> mapAttribute;

public void setMapAttribute(Map<String, String> mapAttribute) {
this.mapAttribute = mapAttribute;
}
}
}

还有哪些实用内置类呢?欢迎小伙伴们留言~


作者:江南一点雨
来源:juejin.cn/post/7417630844100231206
收起阅读 »

VirtualList虚拟列表

web
首先感谢 Vue3 封装不定高虚拟列表 hooks,复用性更好!这篇文章提供的一些思路,在此基础作者进一步对相关代码进行了一些性能上的优化(解决了通过鼠标操作滚动条时的卡顿)。因为项目没有用到ts,就先去掉了。 hooks import { ref, onM...
继续阅读 »

首先感谢
Vue3 封装不定高虚拟列表 hooks,复用性更好!这篇文章提供的一些思路,在此基础作者进一步对相关代码进行了一些性能上的优化(解决了通过鼠标操作滚动条时的卡顿)。因为项目没有用到ts,就先去掉了。



hooks


import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";

export default function useVirtualList(config) {
// 获取元素
let actualHeightContainerEl = null,
translateContainerEl = null,
scrollContainerEl = null;
// 数据源,便于后续直接访问
let dataSource = [];

onMounted(() => {
actualHeightContainerEl = document.querySelector(
config.actualHeightContainer
);
scrollContainerEl = document.querySelector(config.scrollContainer);
translateContainerEl = document.querySelector(config.translateContainer);
});

// 数据源发生变动
watch(
() => config.data.value,
(newValue) => {
// 更新数据源
dataSource = newValue;
// 计算需要渲染的数据
updateRenderData();
}
);

/*



更新相关逻辑



*/

// 更新实际高度
let flag = false;
const updateActualHeight = (oldValue, value) => {
let actualHeight = 0;
if (flag) {
// 修复偏差
actualHeight =
actualHeightContainerEl.offsetHeight -
(oldValue || config.itemHeight) +
value;
} else {
// 首次渲染
flag = true;
for (let i = 0; i < dataSource.length; i++) {
actualHeight += getItemHeightFromCache(i);
}
}
actualHeightContainerEl.style.height = `${actualHeight}px`;
};

// 缓存已渲染元素的高度
const RenderedItemsCache = {};
const RenderedItemsCacheProxy = new Proxy(RenderedItemsCache, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 更新实际高度
updateActualHeight(oldValue, value);
return result;
},
});

// 更新已渲染列表项的缓存高度
const updateRenderedItemCache = (index) => {
// 当所有元素的实际高度更新完毕,就不需要重新计算高度
const shouldUpdate =
Reflect.ownKeys(RenderedItemsCacheProxy).length < dataSource.length;
if (!shouldUpdate) return;

nextTick(() => {
// 获取所有列表项元素(size条数)
const Items = Array.from(document.querySelectorAll(config.itemContainer));
// 进行缓存(通过下标作为key)
for (let i = 0; i < Items.length; i++) {
const el = Reflect.get(Items, i);
const itemIndex = index + i;
if (!Reflect.get(RenderedItemsCacheProxy, itemIndex)) {
Reflect.set(RenderedItemsCacheProxy, itemIndex, el.offsetHeight);
}
}
});
};

// 获取缓存高度,无缓存,取配置项的 itemHeight
const getItemHeightFromCache = (index) => {
const val = Reflect.get(RenderedItemsCacheProxy, index);
return val === void 0 ? config.itemHeight : val;
};

// 实际渲染的数据
const actualRenderData = ref([]);

// 更新实际渲染数据
const updateRenderData = (scrollTop = 0) => {
let startIndex = 0;
let offsetHeight = 0;

for (let i = 0; i < dataSource.length; i++) {
offsetHeight += getItemHeightFromCache(i);

// 第几个以上进行隐藏
if (offsetHeight >= scrollTop - (config.offset || 0)) {
startIndex = i;
break;
}
}
// 计算得出的渲染数据
actualRenderData.value = dataSource
.slice(startIndex, startIndex + config.size)
.map((data, idx) => {
return {
key: startIndex + idx + 1, // 为了在vue的for循环中绑定唯一key值
data,
};
});

// 缓存最新的列表项高度
updateRenderedItemCache(startIndex);

updateOffset(offsetHeight - getItemHeightFromCache(startIndex));
};

// 更新偏移值
const updateOffset = (offset) => {
translateContainerEl.style.transform = `translateY(${offset}px)`;
};

/*



注册事件、销毁事件



*/

// 滚动事件
const handleScroll = (e) =>
// 渲染正确的数据
updateRenderData(e.target.scrollTop);

// 注册滚动事件
onMounted(() => {
scrollContainerEl?.addEventListener("scroll", handleScroll);
});

// 移除滚动事件
onBeforeUnmount(() => {
scrollContainerEl?.removeEventListener("scroll", handleScroll);
});

return { actualRenderData };
}

vue


<script setup>
import { ref } from "vue";
import useVirtualList from "../utils/useVirtualList.js"; // 上面封装的hooks文件
import list from "../json/index.js"; // 造的数据模拟
const tableData = ref([]);
// 模拟异步请求
setTimeout(() => {
tableData.value = list;
}, 0);
const { actualRenderData } = useVirtualList({
data: tableData, // 列表项数据
scrollContainer: ".scroll-container", // 滚动容器
actualHeightContainer: ".actual-height-container", // 渲染实际高度的容器
translateContainer: ".translate-container", // 需要偏移的目标元素,
itemContainer: ".item", // 列表项
itemHeight: 400, // 列表项的大致高度
size: 10, // 单次渲染数量
offset: 200, // 偏移量
});
</script>

<template>
<div>
<h2>virtualList 不固定高度虚拟列表</h2>
<ul class="scroll-container">
<div class="actual-height-container">
<div class="translate-container">
<li
v-for="item in actualRenderData"
:key="item.key"
class="item"
:class="[{ 'is-odd': item.key % 2 }]"
>
<div class="item-title">第{{ item.key }}条:</div>
<div>{{ item.data }}</div>
</li>
</div>
</div>
</ul>
</div>
</template>

<style scoped>
* {
list-style: none;
padding: 0;
margin: 0;
}
.scroll-container {
border: 1px solid #000;
width: 1000px;
height: 200px;
overflow: auto;
}
.item {
border: 1px solid #ccc;
padding: 20px;
display: flex;
flex-wrap: wrap;
word-break: break-all;
}
.item.is-odd {
background-color: rgba(0, 0, 0, 0.1);
}
</style>


作者:wzyoung
来源:juejin.cn/post/7425598941859102730
收起阅读 »

谈谈我做 Electron 应用的这一两年

大家好,我是徐徐。今天和大家谈谈我做 Electron 桌面端应用的这一两年,把一些经验和想法分享给大家。 前言 入职现在这家公司三年了,刚进公司的时候是 21 年年初,那时候会做一些稍微复杂的后台管理系统以及一些简单的 C 端 SDK。准备开始做 Elect...
继续阅读 »

大家好,我是徐徐。今天和大家谈谈我做 Electron 桌面端应用的这一两年,把一些经验和想法分享给大家。


前言


入职现在这家公司三年了,刚进公司的时候是 21 年年初,那时候会做一些稍微复杂的后台管理系统以及一些简单的 C 端 SDK。准备开始做 Electron 项目是因为我所在的是安全部门,急需一款桌面管控软件来管理(监控)员工的电脑安全以及入网准入,可以理解为一款零信任的桌面软件。其实之前公司也有一款安全管控的软件,但是Windows 和 Mac是分端构建的,而且维护成本极高,Windows 是使用的 C#, Mac 是用的 Objective-C,开发和发版效率低下,最后在研发老大的同意下,我和另外一个同事开始研究如何用 Electron 这个框架来做一款桌面端软件。


我们发起这个项目大概是在 21 年年底,Windows 版本上线是在上海疫情封城期间,2022年4月份的时候,疫情结束后由于事业部业务方向的调整,又被抽调到了另外一个组去做一个 C 端的创业项目,后面项目结束了,又回来做 Electron 相关的工作直到现在,之所以是一两年,其实就是这个时间线。


对桌面端开发的一些看法


如果你是前端的话,多一门桌面端开发的技能也不是坏事,相当是你的一个亮点,进可攻,退可守。因为桌面端开发到后期的架构可以非常的复杂,不亚于服务端(chromium 就是一个例子),当然也取决于你所应对的场景的挑战,如果所做的产品跟普通前端无异,那也不能说是一个亮点,但是如果你的工作已经触及到一些操作系统的底层,那肯定是一个亮点。


当然也有人说,做桌面端可能就路越走越窄了,但是我想说的是深度和广度其实也可以理解为一个维度,对于技术人来说,知道得越多就行,因为到后期你要成为某个方面的专家,就是可能会非常深入某一块,换一种思路其实也是叫知道得越多。所以,我觉得前端能有做桌面端的机会也是非常好的,即拓展了自己的技能,还能深入底层,因为现阶段由于业务方向的需要,我已经开始看 chromium 源码了,前端的老祖宗。当然,以上这些只代表自己的观点,大家自行斟酌。


谈谈 Electron


其实刚刚工作前两年我就知道这个框架了,当时也做过小 demo,而且还在当时的团队里面分享过这个技术,但是当时对这门技术的认知是非常浅薄的,就知道前端可以做桌面端了,好厉害,大概就停留在这个层面。后面在真正需要用到这门技术去做一个企业级的桌面应用的时候才去真正调研了一下这个框架,然后才发现它真的非常强大,强大到几乎可以实现你桌面端的任何需求。网上关于 Electron 与其他框架的对比实在是太多了,Google 或者 Baidu 都能找到你想要的答案,好与不好完全看自己的业务场景以及自己所在团队的情况。


谈谈自己的感受,什么情况下可以用这门框架



  • 追求效率,节省人力财力

  • 团队前端居多

  • UI交互多


什么情况下不适合这门框架呢?



  • 包体积限制

  • 性能消耗较高的应用

  • 多窗口应用


我们当时的情况就是要追求效率,双端齐头并进,所以最后经过综合对比,选择了 Electron。毕竟 Vscode 就是用它做的,给了我们十足的信心和勇气,一点都不虚。


一图抵千字,我拿出这张图你自己就有所判断了,还是那句话,仁者见仁,智者见智,完全看自己情况。


image.png
图片来源:http://www.electronjs.org/apps


技术整体架构


这里我画了一张我所从事 Electron 产品的整体技术架构图。

整个项目基于 Vite 开发构建的,基础设施就是常见的安全策略,然后加上一些本地存储方案,外加一个外部插件,这个插件是用 Tauri 做的 Webview,至于为什么要做这个插件我会在后面的段落说明。应用层面的框架主要是分三个大块,下面主要是为了构建一些基础底座,然后将架构进行分层设计,添加一些原生扩展,上面就是基础的应用管理和 GUI 相关的东西,有了这个整体的框架在后面实现一些业务场景的时候就会变得易如反掌(夸张了一点,因为有的技术细节真的很磨人😐)。


当然这里只是一个整体的架构图,其实还有很多技术细节的流程图以及业务场景图并没有在这里体现出来,不过我也会挑选一些方案在后面的篇幅里面做出相应的讲解。


挑战和方案


桌面端开发会遇到一些挑战,这些挑战大部分来源于特殊的业务场景,框架只能解决一些比较常见的应用场景,当然不仅仅是桌面端,其实移动端或者是 Web 端我相信大家都会遇到或多或少的挑战,我这里遇到的一些挑战和响应的方案不一定适合你,只是做单纯的记录分享,如果有帮助到你,我很开心。下面我挑选软件升级更新,任务队列设计,性能检测优化以及一些特殊的需求这几个方面来聊聊相应的挑战和方案。


软件升级更新


桌面端的软件更新升级是桌面端开发中非常重要的一环,一个好的商业产品必须有稳定好用的解决方案。桌面端的升级跟 C 端 App 的升级其实也是差不多的思路,虽然我所做的产品是公司内部人使用,但是用户也是你面向公司所有用户的,所以跟 C 端产品的解决思路其实是无异的。


升级更新主要是需要做到定向灰度。这个功能是非常重要的,应该大部分的应用都有定向灰度的功能,所以我们为了让软件能够平滑升级,第一步就是实现定向灰度,达到效果可回收,性能可监控,错误可告警的目的。定向更新的功能实现了之后,后面有再多的功能需要实现都有基础保障了,下面是更新相关的能力图。



整个更新模块的设计是分为两大块,一块是后台管理系统,一块是客户端。后台管理系统主要是维护相应的策略配置,比如哪些设备需要定向更新,哪些需要自动更新,不需要更新的白名单以及更新后是需要提醒用户相应的更新功能还是就静默更新。客户端主要就是拉取相应的策略,然后执行相应的更新动作。


由于我们的软件是比较特殊的一个产品,他是需要长期保活的,Mac 端上了文件锁是无法删除的。所以我们在执行更新的时候和常规的软件更新是不一样的,软件的更新下载是利用了 electron-update 相应的钩子,但是安装的时候并没有使用相应的钩子函数,而是自己研究了 electron 的更新源码后做了自己的更新脚本。 因为electron 的更新它自己也会注册一个保活的更新任务的服务,但是这个和我们的文件锁和保活是冲突的,所以是需要禁用掉它的保活服务,完成自己的更新。


整体来说,这一块是花了很多时间去研究的,windows 还好,没有破坏其整个生命周期,傻瓜式的配置一下electron-update 相关的函数钩子就可以了。Mac 的更新花了很多时间,因为破坏了文件的生命周期,再加上保活任务,所以会对 electron-update 的更新钩子进行毁灭性的破坏,最后也只能研究其源码然后自己去实现特殊场景下的更新了。


任务队列设计


任务模块的实现在我们这个软件里面也是非常重要的一环,因为客户端会跑非常多的定时任务。刚开始研发这个产品的时候其实还好,定时任务屈指可数,但是随着长时间的迭代,端上要执行的任务越来越多,每个任务的触发时间,触发条件都不一样,以及还要考虑任务的并发情况和对性能的影响,所以在中后期我们对整个任务模块都做了相应的改造。


下面是整个任务模块的核心能力图。


业界也有一些任务相关的开源工具包,比如 node-schedule、node-cron、cron,这些都是很优秀的库,但是我在使用过程中发现他们好像不具备并发限制的场景,比如有很多任务我们在开始设置的时候都会有个时间间隔,这些任务的时间间隔都是可以在后台随意配置的,如果端上不做并发限制会导致一个问题,就是用户某一瞬间会觉得电脑非常卡。


比如你有 4 个 10 分钟间隔的任务 和 2 个五分钟间隔的任务,那么到某一个时间段,他最大并发可能就是 6,如果刚好这 6 个任务都是非常耗费 CPU 的任务,那他们一起执行的时候就会导致整个终端CPU 飙升,导致用户感觉卡顿,这样就会收到相应的 Diss。


安全类的软件产品其实有的时候不需要太过醒目,后台默默运行就行,所以我们的宗旨就是稳定运行,不超载。为此我们就自己实现了相应的任务队列模式,然后去控制任务并发。其实底层逻辑也不难,就是一个 setInterval 的函数,然后不断的创建销毁,读取队列的函数,执行相应的函数。


性能优化


Electron相关的性能优化其实网上也有非常多的文章,我这里说说我的实践和感受。


首先,性能优化你需要优化什么?这个就是你的出发点了,我们要解决一个问题,首先得知道问题的现状,如果你都不知道现在的性能是什么样子,如何去优化呢?所以发现问题是性能优化的最重要的一步。


这里就推荐两个工具,一个是chrome dev-tool,一个是electron 的 inspector,第一个可以观测渲染进程相关的性能情况,第二个可以观测主进程相关的性能情况。


具体可参考以下网址:



有了工具之后我们就需要用工具去分析一些数据和问题,这里面最重要的就是内存相关的分析,你通过内存相关的分析可以看到 CPU 占用高的动作,以及提前检测出内存泄漏的风险。只要把这两个关键的东西抓住了,应用的稳定性就可以得到保障了,我的经验就是每次发布之前都会跑一遍内存快照,内存没有异常才进行发布动作,内存泄漏是最后的底线。


我说说我大概的操作步骤。



  • 通过Performance确认大体的溢出位置

  • 使用Memory进行细粒度的问题分析

  • 根据heap snapshot,判断内存溢出的代码位置

  • 调试相应的代码块

  • 循环往复上面的步骤


上面的步骤在主进程和渲染进程都适用,每一步实际操作在这里就不详细展开了,主要是提供一个思路和方法,因为 dev-tool 的面板东西非常多,扩展开来都可以当一个专题了。


然后我再说说桌面端什么地方可能会内存泄漏或者溢出,下面这些都是我血和泪的教训。



  • 创建的子进程没有及时销毁


如果子进程在完成任务后未被正确终止,这些进程会继续运行并占用系统资源,导致内存泄漏和资源浪费。


假设你的 Electron 应用启动了一个子进程来执行某些计算任务,但在计算完成后未调用
childProcess.kill() 或者未确保子进程已正常退出,那么这些子进程会一直存在,占用系统内存。


const { spawn } = require('child_process');
const child = spawn('someCommand');

child.on('exit', () => {
console.log('Child process exited');
});

// 未正确终止子进程可能导致内存泄漏


  • HTTP 请求时间过长没有正确处理


长时间未响应的 HTTP 请求如果没有设定超时机制,会使得这些请求占用内存资源,导致内存泄漏。


在使用 fetchaxios 进行 HTTP 请求时,如果服务器长时间不响应且没有设置超时处理,内存会被这些未完成的请求占用。


const fetch = require('node-fetch');

fetch('https://example.com/long-request')
.then(response => response.json())
.catch(error => console.error('Error:', error));

// 应该设置请求超时
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 5000); // 5秒超时

fetch('https://example.com/long-request', { signal: controller.signal })
.then(response => response.json())
.catch(error => console.error('Error:', error));


  • 事件处理器没有移除


未正确移除不再需要的事件处理器会导致内存一直被占用,因为这些处理器仍然存在并监听事件。


在添加事件监听器后,未在适当时机移除它们会导致内存泄漏。


const handleEvent = () => {
console.log('Event triggered');
};

window.addEventListener('resize', handleEvent);

// 在不再需要时移除事件监听器
window.removeEventListener('resize', handleEvent);


  • 定时任务未被正确销毁


未在适当时候清除不再需要的定时任务(如 setInterval)会导致内存持续占用。


使用 setInterval 创建的定时任务,如果未在不需要时清除,会导致内存泄漏。


const intervalId = setInterval(() => {
console.log('Interval task running');
}, 1000);

// 在适当时机清除定时任务
clearInterval(intervalId);


  • JavaScript 对象未正确释放


长时间保留不再使用的 JavaScript 对象会导致内存占用无法释放,特别是当这些对象被全局变量或闭包引用时。


创建了大量对象但未在适当时机将它们置为 null 或解除引用。


let bigArray = new Array(1000000).fill('data');

// 当不再需要时,应释放内存
bigArray = null;


  • 窗口实例未被正确销毁


未关闭或销毁不再使用的窗口实例会继续占用内存资源,即使用户已经关闭了窗口界面。


创建了一个新的 BrowserWindow 实例,但在窗口关闭后未销毁它。


const { BrowserWindow } = require('electron');
let win = new BrowserWindow({ width: 800, height: 600 });

win.on('closed', () => {
win = null;
});

// 应确保在窗口关闭时正确释放资源


  • 大文件或大数据量的处理


处理大文件或大量数据时,如果没有进行内存优化和分批处理,会导致内存溢出和性能问题。


在读取一个大文件时,未采用流式处理,而是一次性加载整个文件到内存中。


const fs = require('fs');

// 不推荐的方式:一次性读取大文件
fs.readFile('largeFile.txt', (err, data) => {
if (err) throw err;
console.log(data);
});

// 推荐的方式:流式读取大文件
const readStream = fs.createReadStream('largeFile.txt');
readStream.on('data', (chunk) => {
console.log(chunk);
});

一些特殊的需求


做这个产品也遇到一些特殊的需求,有的需求还挺磨人的,这里也和大家分享一下。



  • 保活和文件锁


作为一个前端,桌面端的保活和文件锁这种需求基本是之前不可能接触到的,为了做这个需求也去了解了一下业界的实现,其实实现都还好,主要是它会带来一些问题,诸如打包构建需要自定义前置脚本和后置脚本,root 用户环境下 mac 端无法输入中文,上面提到的用 Tauri 构建一个 webview 组件就是为了解决 root 用户无法输入中文的场景。



  • 静默安装应用


这个需求也是很绝的一个需求。我想如果是做常规的前端开发,估计一般都不会遇到这种需求,你需要从头到尾实现一个下载器,一个软件安装器,而且还要双端适配,不仅如此,还需要实现 exe、zip、dmg、pkg 等各种软件格式的安装,里面包含重试机制,断点下载,队列下载等各种技术细节。当时接到这个需求头也特别大,不过技术方案做出来后感觉也还好,再复杂的需求只要能理清思路,其实都可以慢慢解决。



  • VPN 和 访问记录监控


这种需求对一个前端来说更是无从入手,但是好在之前有老版本的 VPN 做参考,就是根据相应的代码翻译一遍也能实现,大部分可以用命令行解决。至于访问记录监控这个玩意咋说呢,客户端做其实也挺费神的,如果不借助第三方的开源框架,自己是非常难实现的,所以这种就是需要疯狂的翻国外的网站,就GitHub,Stackoverflow啰,总有一款适合你,这里就不具体说明了。



  • 进程禁用


违规进程禁用其实在安全软件的应用场景是非常常见的,它需要实时性,而且对性能要求很高,一个是不能影响用户正常使用,还要精准杀掉后台配置的违规进程,这个地方其实也是做了很多版优化,但是最后的感觉还是觉得任务队列有性能瓶颈,无法达到要求,现阶段我们也在想用另外的方式去改造,要么就是上全局钩子,要么就是直接把相应的进程文件上锁或者改文件权限。


上面所提到的需求只是一小部例子,还好很多奇奇怪怪的需求没有举例,这些奇怪的需求就像小怪物,不断挑战我的边界,让我也了解和学习和很多奇奇怪怪的知识,有的时候我就会发出这样的感叹:我去,还能这样?


结语


洋洋洒洒,不知不觉已经写了 5000 字了,其实做 Electron 桌面端应用的这一两年自我感觉还是成长了不少,不管是技术方面还是产品设计方面,自己的能力都有所提升。但是同样会遇到瓶颈,就是一个东西一直做一直做,到后面创新会比较难,取得的成就也会慢慢变少。


另外就是安全类的桌面端产品在整个软件开发的里其实是非常冷门的一个领域,他有他的独特性,也有相应的价值,他需要默默的运行,稳定的运行,出问题可以监控到,该提醒的时候提醒用户。你说他低调吧,有时候也挺高调的,真的不好定论,你说没影响力吧,有的时候没他还真不行。让用户不反感这种软件,拥抱这种软件其实挺难的。从一个前端开发的视角来看,桌面端的体验的确很重要,不管是流畅度还是美观度,都不能太差,这也是我们现阶段追求的一个点,就是不断提升用户体验。


路漫漫其修远兮,吾将上下而求索。前端开发这条路的确很长,如果你想朝某个方面深度发展,你会发现边界是非常难触达的,当然也看所处的环境和对应的机遇,就从技术来说的话,前端的天花板也可以很高,不管是桌面端,服务端,移动端,Web端,每个方向前端的天花板都非常难触摸到。


最后,祝大家在自己的领域越来越深,早日触摸到天花板。


原文链接


mp.weixin.qq.com/s/SzN8wvqxj…


作者:前端徐徐
来源:juejin.cn/post/7399100662610395147
收起阅读 »

为什么程序员的社会地位不高?

互联网时代,程序员承担着数字世界构建和技术发展的大任,如此重要,为什么存在感不高,社会地位不高呢? 知乎上针对这个问题也有过讨论,分享给大家。 http://www.zhihu.com/question/58… 回答1 什么是社会地位? 社会地位可以简化成...
继续阅读 »

互联网时代,程序员承担着数字世界构建和技术发展的大任,如此重要,为什么存在感不高,社会地位不高呢?


知乎上针对这个问题也有过讨论,分享给大家。



http://www.zhihu.com/question/58…



回答1


什么是社会地位?

社会地位可以简化成,一个人可支配社会资源的数量,例如:


医生 医疗资源


教师 教育资源

...


而程序员可支配的社会资源只有他自己。从这一点上说,程序员和工人没有本质上的区别。

时代的红利成就了这个职业,抛弃它的时候,一样不会留情。


回答2


程序员作为一种社会职业,既没有政府职能部门的公权力,又没有有钱人的一掷千金,挣得也都是辛苦钱,何来社会地位高不高一说,无非就是资本的韭菜罢了。


回答3


这个问题我曾经思考过很久。按知乎的习惯,先问是不是,再问为什么。


首先说“是不是”。

按大家的直觉也好,或者现有的各个社区讨论来看,程序员的社会地位肯定不是高的。


最多有人说程序员的社会地位和其他职业一样高,但没见过谁说程序员的社会地位能高过GWY,医生,老师的。这么说来,“是不是”这个问题已经基本没有大的争议——在公众认知内,程序员的社会地位的确不高。


再来就是“为什么”。


这个为什么是我想了很久了,如果单独拿程序员和某个职业/行业比较,可以有很多个维度的对比,但如果想把大部分的职业/行业进行对比,需要找一个更有共性的比较方式,或者说是能归纳出比较重要的影响因素。对此,我归纳出来的最主要因素是“自由裁量权”。


这里的“自由裁量权”,又分为两个维度:


第一个是权力本身影响后果的大小,比如影响10块钱和影响10亿元的大小肯定不一样;


第二个是权力影响的范围,比如影响一个区和影响全国肯定不一样。


这里举电视剧《人民的名义》里面的人物来说明这一点。


第一个剧中是京州市副市长兼光明区区委书记丁义珍。丁义珍是“负责土地划批,矿产资源整合,还有老城改造”,这里无论是土地划批给某开发商,或者矿产资源交给哪个煤老板开挖,对于这些开发商和煤老板,都一笔稳赚不赔的买卖。而剧中的丁义珍在具体能把这块地或者这片矿批给谁上面,有很大的自由裁量权,也就是说,他能在规则范围之内,把地给批了。于是各个房地产开发商老板,煤矿老板都要找丁义珍去批地批矿,自然丁义珍社会地位就高了。


第二个是京州市城市银行副行长欧阳菁。作为银行副行长,很多带款她拥有最终决定权。是放贷或者不放贷,放贷放给哪个企业,她拥有决定权力,甚至还能影响汉东农信社的决定。比如在蔡成功申请六千万的带款的事情上,欧阳菁一直阻挠,甚至打电话让汉东农信社不给蔡成功带款。为什么以前能贷给蔡成功,而这次不行了呢,那是因为之前每次过桥贷蔡成功都给欧阳菁50万好处费,而这次没有。


从以上两个例子可以出,无论是在ZF,还是银行这种企业里,当官至一定地位时,就拥有了影响社会面的一定量的自由裁量权。无论是丁义珍还是欧阳菁,他们的自由裁量权总体上还是在规则之内运行的,没有明显超出规则之外。要不是赵德汉被查,丁义珍还没那么快会被抓以至于后面要逃亡国外。而欧阳菁如果不是因为侯亮平下来查山水集团等案子,也不会露出马脚。


在最开始说了,自由裁量权除了影响的后果大小,还有涉及面的大小,比如丁义珍和欧阳菁的影响力,主要还是在京州市之内,出了京州市,尤其是出了汉东省,他们也影响不到啥。而剧中的第一个出场的贪官赵德汉,就有影响全国资源项目的审核权,这就是影响面的区别了。所以才有那个全国各地都有人找赵德汉,在他办公室门口排队的事情了。


通过《人民的名义》这三个例子,自由裁量权的影响力和影响面应该都有一定的了解了。


那么我们回过头来看现实中的程序员,这个职业带来的对于社会影响的自由裁量权,可以看出是非常小的,影响面也非常不适合操作。


首先,程序员可以决定程序的技术架构和代码,但很难影响其功能。真正决定功能是怎么样的,是产品经理(网站、APP类)、策划(游戏类)、甲方(to B和to G类),程序员本身几乎没有话语权,即没有自由裁量权,更多地是执行权。即使程序员做到了manager,或者技术VP,甚至CTO,对于这些功能特性的影响都是有限的。


比如说你是某游戏的技术leader,过年了你侄子在玩这款游戏,他希望你帮他的角色属性全部乘以10,你也是做不到的,甚至在内部评审阶段都被砍了。从影响面的角度来看,如果程序员是做某个APP的,他没法影响同公司另一款APP怎么做,更别说影响别的公司的APP怎么做。用通俗点的话来说,无论是社会上的陌生人,还是亲戚朋友,希望找程序员去做一些其职业内能自由裁量的内容从而获益,是很难的。这也就是程序员社会不高的主要原因。


同理,按照这个框架,我们能分析其他的一些职业的社会地位,同时也能看到一些职业除了稳定之外,还有自由裁量权这一微妙的东西,让不少人甘愿去追逐。


全文完


或许,这些讨论,并不能改变现实。


我觉得我们要思考的是:


社会地位的标准到底是什么?


技术人如今的社会地位,合理不合理?


技术人做什么,能够改变自己的社会地位?


作者:程序员凌览
来源:juejin.cn/post/7425807410764546098
收起阅读 »

聊聊try...catch 与 then...catch

web
处理错误的两种方式:try...catch 与 then、catch 在前端编程中,错误和异常处理是保证代码健壮性和用户体验的重要环节。JavaScript 提供了多种方式来处理错误,其中最常见的两种是 try...catch 和 Promise 的 then...
继续阅读 »

处理错误的两种方式:try...catch 与 thencatch


在前端编程中,错误和异常处理是保证代码健壮性和用户体验的重要环节。JavaScript 提供了多种方式来处理错误,其中最常见的两种是 try...catch 和 Promise 的 thencatch,但是什么时候该用try...catch,什么时候该用thencatch呢,下面将详细探讨这两种机制的区别及其适用场景。


1学习.png


一、try...catch

try...catch 是一种用于捕获和处理同步代码中异常的机制。其基本结构如下:


try {
// 可能会抛出异常的代码
} catch (error) {
// 处理异常
}

使用场景



  • 主要用于同步代码,尤其是在需要处理可能抛出的异常时。

  • 适用于函数调用、操作对象、数组等传统代码中。


示例


function divide(a, b) {
try {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
} catch (error) {
console.error(error.message);
}
}

divide(4, 0); // 输出: Cannot divide by zero

在这个例子中,如果 b 为零,则会抛出一个错误,并被 catch 块捕获。


二、then 和 catch

在处理异步操作时,使用 Promise 的 thencatch 方法是更加常见的做法。其结构如下:


someAsyncFunction()
.then(result => {
// 处理成功的结果
})
.catch(error => {
// 处理错误
});

使用场景



  • 主要用于处理异步操作,例如网络请求、文件读取等。

  • 可以串联多个 Promise 操作,清晰地处理成功和错误。


示例


function fetchData() {
return new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const success = Math.random() > 0.5; // 随机决定成功或失败
if (success) {
resolve("Data fetched successfully");
} else {
reject("Failed to fetch data");
}
}, 1000);
});
}

fetchData()
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error);
});

在这个示例中,fetchData 函数模拟了一个异步操作,通过 Promise 来处理结果和错误。


三、async/await 与 try...catch

为了使异步代码更具可读性,JavaScript 引入了 async/await 语法。结合 try...catch,可以让异步错误处理更加简洁:


async function fetchDataWithAwait() {
try {
const result = await fetchData();
console.log(result);
} catch (error) {
console.error(error);
}
}

fetchDataWithAwait();

总结



  • try...catch:适合于同步代码,能够捕获代码块中抛出的异常。

  • then 和 catch:用于处理 Promise 的结果和错误,适合异步操作。

  • async/await 结合 try...catch:提供了清晰的异步错误处理方式,增强了代码的可读性。


在实际开发中,选择哪种方式取决于代码的性质(同步或异步)以及个人或团队的编码风格。


往期推荐


前端如何实现图片伪防盗链,保护页面图片🔥


适当使用$forceUpdate()🔥


怎么进行跨组件通信,教你如何使用provide 和 inject🔥


使用reactive导致数据失去响应式?🔥


1再见.png


作者:不爱说话郭德纲
来源:juejin.cn/post/7418133347543121939
收起阅读 »

用零宽字符来隐藏代码

web
什么是零宽度字符 一种不可打印的Unicode字符,在浏览器等环境不可见,但是真是存在,获取字符串长度时也会占位置,表示某一种控制功能的字符。 常见的零宽字符有: 空格符:格式为U+null00B,用于较长字符的换行分隔; 非断空格符:格式为U+FEFF,用于...
继续阅读 »

什么是零宽度字符


一种不可打印的Unicode字符,在浏览器等环境不可见,但是真是存在,获取字符串长度时也会占位置,表示某一种控制功能的字符。


常见的零宽字符有:


空格符:格式为U+null00B,用于较长字符的换行分隔;
非断空格符:格式为U+FEFF,用于阻止特定位置的换行分隔;
连字符:格式为U+null00D,用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果;
断字符:格式为U+200C,用于阿拉伯文、德文、印度语系等文字中,阻止会发生连字的字符间的连字效果;
左至右符:格式为U+200E,用于在混合文字方向的多种语言文本中,规定排版文字书写方向为左至右;
右至左符:格式为U+200F : 用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左;


使用零宽字符给信息加密


(function(window) {
var rep = { // 替换用的数据,使用了4个零宽字符代理二进制
'00': '\u200b',
'0null': '\u200c',
'null0': '\u200d',
'nullnull': '\uFEFF'
};

function hide(str) {
str = str.replace(/[^\x00-\xff]/g, function(a) { // 转码 Latin-null 编码以外的字符。
return escape(a).replace('%', '\\');
});

str = str.replace(/[\s\S]/g, function(a) { // 处理二进制数据并且进行数据替换
a = a.charCodeAt().toString(2);
a = a.length < 8 ? Array(9 - a.length).join('0') + a : a;
return a.replace(/../g, function(a) {
return rep[a];
});
});
return str;
}

var tpl = '("@code".replace(/.{4}/g,function(a){var rep={"\u200b":"00","\u200c":"0null","\u200d":"null0","\uFEFF":"nullnull"};return String.fromCharCode(parseInt(a.replace(/./g, function(a) {return rep[a]}),2))}))';

window.hider = function(code, type) {
var str = hide(code); // 生成零宽字符串

str = tpl.replace('@code', str); // 生成模版
if (type === 'eval') {
str = 'eval' + str;
} else {
str = 'Function' + str + '()';
}

return str;
}
})(window);

var code = hider('测试一下');
console.log(code);

直接复制到项目中可以使用,我们现在来试试


var code = hider('测试一下');
console.log(code);

结果如下:


image.png


实际用法


image.png


功能用途


这个技术可以应用到很多领域,非常具有实用性。


比如:代码加密、数据加密、文字隐藏、内容保密、隐形水印,等等。


原理介绍


实现字符串隐形,技术原理是“零宽字符”。


在编程实现隐形字符功能时,先将字符串转为二进制,再将二进制中的1转换为\u200b;0转换为\u200c;空格转换为\u200d,最后使用\ufeff 零宽度非断空格符作分隔符。这几种unicode字符都是不可见的,因此最终转化完成并组合后,就会形成一个全不可见的“隐形”字符串。


功能源码


function text_2_binary(text){
return text.split('').map(function(char){ return char.charCodeAt(0).toString(2)}).join(' ');
}
function binary_2_hidden_text(binary){
return binary.split('').map(function (binary_num){
var num = parseInt(binary_num, 10);
if (num === 1) {
return '\u200b';
} else if(num===0) {
return '\u200c';
}
return '\u200d';
}).join('\ufeff')
}
var text = "jshaman是专业且强大的JS代码混淆加密工具";
var binary_text = text_2_binary(text);
var hidden_text = binary_2_hidden_text(binary_text);
console.log("原始字符串:",text);
console.log("二进制:",binary_text);
console.log("隐藏字符:",hidden_text,"隐藏字符长度:",hidden_text.length);

隐型还原


接下来介绍“隐形”后的内容如何还原。


在了解上文内容之后,知道了字符隐形的原理,再结合源代码可知:还原隐形内容,即进行逆操作:将隐形的unicode编码转化成二进制,再将二进制转成原本字符。


直接给出源码:


function hidden_text_2_binary(string){
return string.split('\ufeff').map(function(char){
if (char === '\u200b') {
return '1';
} else if(char === '\u200c') {
return '0';
}
return ' ';
}).join('')
}
function binary_2_Text(binaryStr){
var text = ""
binaryStr.split(' ').map(function(num){
text += String.fromCharCode(parseInt(num, 2));
}).join('');
return text.toString();
}
console.log("隐形字符转二进制:",hidden_text_2_binary(hidden_text));
console.log("二进制转原始字符:",binary_2_Text(hidden_text_2_binary(hidden_text)));

运行效果


image.png


如果在代码中直接提供“隐形”字符内容,比如ajax通信时,将“隐形”字符由后端传给前端,并用以上解密方法还原,那么这种方式传递的内容会是非常隐秘的。


但还是存在一个安全问题:他人查看JS源码,能看到解密函数,这可能引起加密方法泄露、被人推导出加密、解密方法。


前端的js想做到纯粹的加密目前是不可能的,因为 JavaScript 是一种在客户端执行的脚本语言,其代码需要在浏览器或其他 JavaScript 运行环境中解释和执行,由于需要将 JavaScript 代码发送到客户端,并且在客户端环境中执行,所以无法完全避免代码的逆向工程和破解。


作者:浮游本尊
来源:juejin.cn/post/7356208563101220915
收起阅读 »

前端如何生成临时链接?

web
前言 前端基于文件上传需要有生成临时可访问链接的能力,我们可以通过URL.createObjectURL和FileReader.readAsDataURAPI来实现。 URL.createObjectURL() URL.createObjectURL() 静态...
继续阅读 »



前言


前端基于文件上传需要有生成临时可访问链接的能力,我们可以通过URL.createObjectURLFileReader.readAsDataURAPI来实现。


URL.createObjectURL()


URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。


1. 语法


let objectURL = URL.createObjectURL(object);

2. 参数


用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。


3. 返回值


一个DOMString包含了一个对象URL,该URL可用于指定源 object的内容。


4. 示例


"file" id="file">

document.querySelector('#file').onchange = function (e) {
console.log(e.target.files[0])
console.log(URL.createObjectURL(e.target.files[0]))
}

0f40e1fff9674142889f8bacc6d455b9.png


将上方console控制台打印的blob文件资源地址粘贴到浏览器中


blob:http://localhost:8080/1ece2bb1-b426-4261-89e8-c3bec43a4020

5cc4d088c5c941b7950f6f930cb9a1bc.png


URL.revokeObjectURL()


在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。


浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。


1. 语法


window.URL.revokeObjectURL(objectURL);

2. 参数 objectURL


一个 DOMString,表示通过调用 URL.createObjectURL() 方法返回的 URL 对象。


3. 返回值


undefined


4. 示例


"file" id="file">
<img id="img1" style="width: 200px;height: auto" />
<img id="img2" style="width: 200px;height: auto" />

document.querySelector('#file').onchange = function (e) {
const file = e.target.files[0]

const URL1 = URL.createObjectURL(file)
console.log(URL1)
document.querySelector('#img1').src = URL1
URL.revokeObjectURL(URL1)

const URL2 = URL.createObjectURL(file)
console.log(URL2)
document.querySelector('#img2').src = URL2
}

ecba01284f034c42a2bf4200054b0e9f.png


与FileReader.readAsDataURL(file)区别


1. 主要区别



  • 通过FileReader.readAsDataURL(file)可以获取一段data:base64的字符串

  • 通过URL.createObjectURL(blob)可以获取当前文件的一个内存URL


2. 执行时机



  • createObjectURL是同步执行(立即的)

  • FileReader.readAsDataURL是异步执行(过一段时间)


3. 内存使用



  • createObjectURL返回一段带hashurl,并且一直存储在内存中,直到document触发了unload事件(例如:document close)或者执行revokeObjectURL来释放。

  • FileReader.readAsDataURL则返回包含很多字符的base64,并会比blob url消耗更多内存,但是在不用的时候会自动从内存中清除(通过垃圾回收机制)


4. 优劣对比



  • 使用createObjectURL可以节省性能并更快速,只不过需要在不使用的情况下手动释放内存

  • 如果不在意设备性能问题,并想获取图片的base64,则推荐使用FileReader.readAsDataURL




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

小程序海报绘制方案(原生,Uniapp,Taro)

web
背景 小程序海报绘制方案有很多,但是大多数都是基于canvas的,而且都是自己封装的,不够通用,不够灵活,不够简单,不够好用。 本方使用一个开源的小程序海报绘制,非常灵活,扩展性非常好,仅布局就能得到一张海报。 准备工作 安装依赖,也可以把源码下载到本地,...
继续阅读 »

背景



  1. 小程序海报绘制方案有很多,但是大多数都是基于canvas的,而且都是自己封装的,不够通用,不够灵活,不够简单,不够好用。

  2. 本方使用一个开源的小程序海报绘制,非常灵活,扩展性非常好,仅布局就能得到一张海报。


准备工作


安装依赖,也可以把源码下载到本地,源码地址


npm install wxml2canvas

布局


无论哪种方案,布局都是一致的,需要注意一些暂未支持的属性:



  1. 变形:transform,但是节点元素使能读取此属性,但是库不支持,所以不要使用

  2. 圆角,border-radius,同上,不要使用,圆形图片有特定的属性去实现,除此之外无法实现其他类型的圆角


布局示例:


注意,除了uniapp,原生和Taro要使用原生组件的方式绘制canvas,因为Taro不支持data-xx的属性绑定方式,这一点很糟糕


<!-- 外层wrap用于fixed定位,使得整个布局离屏,离屏canvas暂未支持 -->
<view class='wrap'>
<!-- canvas id,一会 new 的时候需要 -->
<canvas canvas-id="poster-canvas"></canvas>
<view class="container">
<view data-type="text" data-text="测试文字绘制" class='text'>测试文字绘制</view>
<image data-type="image" data-src="https://img.yzcdn.cn/vant/cat.jpeg" class='image'></image>
<image data-type="radius-image" data-src="https://img.yzcdn.cn/vant/cat.jpeg" class='radius-image'></image>
</view>
</view>

原生小程序


import Wxml2Canvas from 'wxml2canvas'

Component({
methods: {
paint() {
wx.showLoading({ title: '生成海报' });
// 创建绘制实例
const drawInstance = new Wxml2canvas({
// 组件的this指向,组件内使用必传
obj: this,
// 画布宽高
width: 275,
height: 441,
// canvas-id
element: 'poster-canvas',
// 画布背景色
background: '#f0f0f0',
// 成功回调
finish: (url) => {
console.log('生成的海报url,开发者工具点击可预览', url);
wx.hideLoading();
},
// 失败回调
error: (err) => {
console.error(err);
wx.hideLoading();
},
});
// 节点数据
const data = {
list: [
{
// 此方式固定 wxml
type: 'wxml',
class: '.text', // draw_canvas指定待绘制的元素
limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算
}
{
// 此方式固定 wxml
type: 'wxml',
class: '.image', // draw_canvas指定待绘制的元素
limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算
}
{
// 此方式固定 wxml
type: 'wxml',
class: '.radius-image', // draw_canvas指定待绘制的元素
limit: '.container', // 限定绘制元素的范围,取指定元素与它的相对位置计算
}
]
}
// 调用绘制方法
drawInstance.draw(data);
}
}
})


Uniapp


uniapp 主要讲Vue3的版本,因为涉及 this,需要获取 this 以及时机


import {  getCurrentInstance} from 'vue';
// 调用时机 setup内,不能在其他时机
// @see https://github.com/dcloudio/uni-app/issues/3174
const instance = getCurrentInstance();


function paint() {
uni.showLoading({ title: '生成海报' });
const drawInstance = new Wxml2Canvas({
width: 290, // 宽, 以iphone6为基准,传具体数值,其他机型自动适配
height: 430, // 高
element: 'poster-canvas', // canvas-id
background: '#f0f0f0',
obj: instance,
finish(url: string) {
console.log('生成的海报url,开发者工具点击可预览', url);
uni.hideLoading();
},
error(err: Error) {
console.error(err);
uni.hideLoading();
},
});
// 节点数据
const data = {
list: [
{
// 此方式固定 wxml
type: 'wxml',
class: '.text', // draw_canvas指定待绘制的元素
}
{
// 此方式固定 wxml
type: 'wxml',
class: '.image', // draw_canvas指定待绘制的元素
}
{
// 此方式固定 wxml
type: 'wxml',
class: '.radius-image', // draw_canvas指定待绘制的元素
}
]
}
// 调用绘制方法
drawInstance.draw(data);
}


Taro


Taro 比较特殊,框架层面的设计缺陷导致了 Taro 组件增加的 data-xx 属性在编译的时候是会清除的,因此Taro要使用这个库要用原生小程序的方式编写组件。


代码和原生的一样,只是要用原生的方式编写组件,然后在 Taro 中使用。
参考原生的代码,原生小程序js参考这


假设原生组件名为 draw-poster,那么首先需要再Taro的页面中引入这个组件,然后在页面中使用这个组件,然后在组件中使用这个库。


export default {
navigationBarTitleText: '',
usingComponents: {
'draw-poster': '../../components/draw-poster/index',
},
};

  const draw = useCallback(() => {
const { page } = Taro.getCurrentInstance();
// 拿到目标组件实例调用里面的方法
const instance = page!.selectComponent('#draw_poster');
// 调用原生组件绘制方法
instance.paint();
}, []);
return <draw-poster id="draw_poster"/>

总结


对比原生的canvas绘制方案,布局的方式获取节点的方式都是一样的,只是绘制的时候不一样,原生的是直接绘制到canvas上,而这个库是先把布局转换成canvas,然后再绘制到canvas上,所以这个库的性能会比原生的差一些,但是这个库的优势在于布局的方式,不需要自己去计算位置,只需要布局,然后调用绘制方法就可以了,非常方便,而且扩展性非常好,可以自己扩展一些布局方式,比如说flex布局,grid布局等等,这些都是可以的,只需要在布局的时候把布局转换成canvas的布局就可以了,这个库的布局方式是参考的微信小程序的布局方式,所以布局的时候可以参考微信小程序的布局方式,这样就可以很方便的布局了。


作者:码上秃
来源:juejin.cn/post/7300460850010521654
收起阅读 »

还在用轮询、websocket查询大屏数据?sse用起来

web
常见的大屏数据请求方式 1、http请求轮询:使用定时器每隔多少时间去请求一次数据。优点:简单,传参方便。缺点:数据更新不实时,浪费服务器资源(一直请求,但是数据并不更新) 2、websocket:使用websocket实现和服务器长连接,服务器向客户端推送大...
继续阅读 »

常见的大屏数据请求方式


1、http请求轮询:使用定时器每隔多少时间去请求一次数据。优点:简单,传参方便。缺点:数据更新不实时,浪费服务器资源(一直请求,但是数据并不更新)

2、websocket:使用websocket实现和服务器长连接,服务器向客户端推送大屏数据。优点:长连接,客户端不用主动去请求数据,节约服务器资源(不会一直去请求数据,也不会一直去查数据库),数据更新及时,浏览器兼容较好(web、h5、小程序一般都支持)。缺点:有点大材小用,一般大屏数据只需要查询数据不需要向服务端发送消息,还要处理心跳、重连等问题。

image.png


3、sse:基于http协议,将一次性返回数据包改为流式返回数据。优点:sse使用http协议,兼容较好、sse轻量,使用简单、sse默认支持断线重连、支持自定义响应事件。缺点:浏览器原生的EventSource不支持设置请求头,需要使用第三方包去实现(event-source-polyfill)、需要后端设置接口的响应头Content-Type: text/event-stream

image.png


sse和websocket的区别



  1. websocket支持双向通信,服务端和客户端可以相互通信。sse只支持服务端向客户端发送数据。

  2. websocket是一种新的协议。sse则是基于http协议的。

  3. sse默认支持断线重连机制。websocket需要自己实现断线重连。

  4. websocket整体较重,较为复杂。sse较轻,简单易用。


Websocket和SSE分别适用于什么业务场景?


根据sse的特点(轻量、简单、单向通信)更适用于大屏的数据查询,业务应用上查询全局的一些数据,比如消息通知未读消息等。


根据websocket的特点(双向通信)更适用于聊天功能的开发


前端代码实现


sse的前端的代码非常简单


 const initSse = () => {
const source = new EventSource(`/api/wisdom/terminal/stats/change/notify/test`);

// 这里的stats_change要和后端返回的数据结构里的event要一致
source.addEventListener('stats_change', function (event: any) {
const types = JSON.parse(event.data).types;
});
// 如果event返回的是message 数据监听也可以这样监听
// source.onmessage =function (event) {
// var data = event.data;
// };

// 下面这两个监听也可以写成addEventListener的形式
source.onopen = function () {
console.log('SSE 连接已打开');
};

// 处理连接错误
source.onerror = function (error: any) {
console.error('SSE 连接错误:', error);
};
setSseSource(source);
};

// 关闭连接
sseSource.close();

这种原生的sse连接是不能设置请求头的,但是在业务上接口肯定是要鉴权需要传递token的,那么怎么办呢? 我们可以使用event-source-polyfill这个库


 const source = new EventSourcePolyfill(`/api/wisdom/terminal/stats/change/notify/${companyId}`, {
headers: {
Authorization: sessionStorage.get(StorageKey.TOKEN) || storage.get(StorageKey.TOKEN),
COMPANYID: storage.get(StorageKey.COMPANYID),
COMPANYTYPE: 1,
CT: 13
}
});

//其它的事件监听和原生的是一样

后端代码实现


后端最关键的是设置将响应头的Content-Type设置为text/event-streamCache-Control设置为no-cacheConnection设置为keep-alive。每次发消息需要在消息体结尾用"/n/n"进行分割,一个消息体有多个字段每个字段的结尾用"/n"分割。


var http = require("http");

http.createServer(function (req, res) {
var fileName = "." + req.url;

if (fileName === "./stream") {
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");

interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);

req.connection.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(8844, "127.0.0.1");

其它开发中遇到的问题


我在开发调试中用的是umi,期间遇到个问题就是sse连接上了但是在控制台一直没有返回消息,后端那边又是正常发出了的,灵异的是在后端把服务干掉的一瞬间可以看到控制台一下接到好多消息。我便怀疑是umi的代理有问题,然后我就去翻umi的文档,看到了下面的东西:


image.png


一顿操作之后正常


image.png


作者:Mozambique_Here
来源:juejin.cn/post/7424908830902042658
收起阅读 »

你的团队是“活”的吗?

最近有同学离职,让我突然思考一个话题。 之前在腾讯,内部转岗叫做活水,是希望通过内部转岗,盘活团队。让团队保持一定的人员流动性,让个人与团队双向奔赴,满足各自的需要。因此,我们都希望,团队是活水,而不是一潭死水。 为什么团队要保持一定的人员流动性呢? “优”...
继续阅读 »

最近有同学离职,让我突然思考一个话题。


之前在腾讯,内部转岗叫做活水,是希望通过内部转岗,盘活团队。让团队保持一定的人员流动性,让个人与团队双向奔赴,满足各自的需要。因此,我们都希望,团队是活水,而不是一潭死水


为什么团队要保持一定的人员流动性呢?



  • “优”胜“劣”汰。这里不是指恶意竞争和卷。而是通过一定的人员流动性,有进有出,从而找到更加适合团队的人。找到跟团队价值观一致的,志同道合的成员。而跟团队匹配度不是很高的人,可以去寻找更加适合自己的团队和岗位,这对于双方都是有好处的。

  • 激活团队。当一个团队保持稳定太久,就会有点思想固化,甚至落后了。这时候,需要通过一些新鲜血液,带来不同的思想和经验,来激活团队,这就像鲶鱼一样。


那想要形成一个“活”的团队,需要什么条件呢?



  • 薪资待遇要好。首先是基本福利待遇要高于业界平均水平。其次,绩效激励是有想象空间的。如果没有这个条件,那人员流动肯定是入不敷出的,优秀的人都被挖跑了。

  • 团队专业。团队在业界有一定的影响力,在某一方面的专业技术和产出保持业界领先。这个条件隐含了一个信息,就是团队所在业务是有挑战的,因为技术产出一般都是依赖于业务的,没有业务实践和验证,是做不出优秀的技术产出的。因此,待遇好、有技术成长、有职业发展空间,这三者是留住人才的主要手段。

  • 梯队完整。在有了前面 2 个条件之后,就有了吸引人才的核心资源了。那接下来就需要有一个完整的梯队。因为资源是有限的,团队资源只能分配到有限人手里,根据最经典的 361,待遇和职业发展空间最多只能覆盖 3 成,技术成长再多覆盖 3 成人已经不错了。那剩下的 4 成人怎么办?所以,团队需要有一些相对稳定的人,他们能完成安排的事情,不出错,也不需要他们卷起来。


这是我当前的想法,我想我还需要更多的经验和讨论的。


那我目前的团队是“活”的吗?答案是否定的。


首先,过去一年,公司的招聘被锁了,内部转岗也基本转不动。薪资待遇就更不用说了。整个环境到处都充斥着“躺”的氛围。


其次,团队专业度一般,在金融业务,前端的发挥空间极其有限。我也只能尽自己所能,帮大家寻求一些技术成长的空间,但还是很有限。


最后,梯队还没有完整,还在建设中,不过也是步履维艰。因为前两个条件限制,别说吸引优秀人才了,能不能保住都是个问题。


最近公司开始放开招聘了,但还不是大面积的,不过还是有希望可以给有人员流失的团队补充 hc 的。但比较难受的是,这个 hc 不是过我的手的,哈哈,又有种听天由命的感觉。


这就是我最近的一个随想,那么,你的团队是“活”的吗?


----------------【END】----------------



【往期文章】


《程序员职场工具库》必须及格的职场工具 —— PPT 系列1


《程序员职场工具库》高效工作的神器 —— checklist


2023 年上半年最值得看的一篇文章



欢迎关注公众号【潜龙在渊灬】(点此扫码关注),收获程序员职场相关经验、提升工作效率和职场效能、结交更多人脉。


作者:潜龙在渊灬
来源:juejin.cn/post/7298347391164383259
收起阅读 »

工作两年,本地git分支达到了惊人的361个,该怎么快速清理呢?

说在前面 不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。...
继续阅读 »

说在前面



不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。而我平时本地分支都不怎么清理,这就导致了我这两年来本地分支的数量达到了惊人的361个,所以便开始写了这个可以批量删除分支的命令行工具。



1697987090644.jpg


功能设计


我们希望可以通过命令行命令的方式来进行交互,快速获取本地分支列表及各分支的最后提交时间和合并状态,在控制台选择我们想要删除的分支。


功能实现


1、命令行交互获取相关参数


这里我们使用@jyeontu/j-inquirer模块来完成命令行交互功能,@jyeontu/j-inquirer模块除了支持inquirer模块的所有交互类型,还扩展了文件选择器文件夹选择器多级选择器交互类型,具体介绍可以查看文档:http://www.npmjs.com/package/@jy…


(1)获取操作分支类型


我们的分支分为本地分支和远程分支,这里我们可以选择我们需要操作的分支类型,选择列表为:"本地分支"、"远程分支"、"本地+远程"


(2)获取远程仓库名(remote)


我们可以输入自己git的远程仓库名,默认为origin


(3)获取生产分支名


我们需要判断各分支是否已经合并到生产分支,所以需要输入自己项目的生产分支名,默认为develop


相关代码


const branchListOptions = [
{
type: "list",
message: "请选择要操作的分支来源:",
name: "branchType",
choices: ["本地分支", "远程分支", "本地+远程"],
},
{
type: "input",
message: "请输入远程仓库名(默认为origin):",
name: "gitRemote",
default: "origin",
},
{
type: "input",
message: "请输入生产分支名(默认为develop):",
name: "devBranch",
default: "develop",
},
];
const res = await doInquirer(branchListOptions);

image.png


2、命令行输出进度条


在分支过多的时候,获取分支信息的时间也会较长,所以我们需要在控制台中打印相关进度,避免用户以为控制台卡死了,如下图:


image.png


image.png


3、git操作


(1)获取git本地分支列表


想要获取当前仓库的所有的本地分支,我们可以使用git branch命令来获取:


function getLocalBranchList() {
const command = "git branch";
const currentBranch = getCurrentBranch();
let branch = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branch = branch
.split("、")
.filter(
(item) => item !== "" && !item.includes("->") && item !== currentBranch
);
return branch;
}

(2)获取远程仓库分支列表


想要获取当前仓库的所有的远程分支,我们可以使用git ls-remote --heads origin命令来获取,git ls-remote --heads origin 命令将显示远程仓库 origin 中所有分支的引用信息。其中,每一行显示一个引用,包括提交哈希值和引用的全名(格式为 refs/heads/)。


示例输出可能如下所示:


Copy Code
refs/heads/master
refs/heads/develop
refs/heads/feature/xyz

其中, 是每个分支最新提交的哈希值。


function getRemoteList(gitRemote) {
const command = `git ls-remote --heads ${gitRemote}`;
let branchList = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branchList = branchList
.split("、")
.filter((item) => item.includes("refs/heads/"))
.map((branch) => {
return gitRemote + "/" + branch.split("refs/heads/")[1];
});
return branchList;
}

(3)获取各分支详细信息


我们想要在每个分支后面显示该分支最后提交时间和是否已经合并到生产分支,这两个信息可以作为我们判断该分支是否要删除的一个参考。



  • 获取分支最后提交时间
    git show -s --format=%ci 命令用于查看 指定 分支最新提交的提交时间。其中,--format=%ci 用于指定输出格式为提交时间。


在 Git 中,git show 命令用于显示某次提交的详细信息,包括作者、提交时间、修改内容等。通过使用 -s 参数,我们只显示提交摘要信息,而不显示修改内容。


git show -s --format=%ci develop 命令将显示 develop 分支最新提交的提交时间。输出格式为 ISO 8601 标准的时间戳,例如 2023-10-22 16:41:47 +0800


function getBranchLastCommitTime(branchName) {
try {
const command = `git show -s --format=%ci ${branchName}`;
const result = child_process.execSync(command).toString();
const date = result.split(" ");
return date[0] + " " + date[1];
} catch (err) {
return "未获取到时间";
}
}


  • 判断分支是否合并到生产分支
    git branch --contains 命令用于查找包含指定分支()的所有分支。


在 Git 中,git branch 命令用于管理分支。通过使用 --contains 参数,我们可以查找包含指定提交或分支的所有分支。


git branch --contains 命令将列出包含 的所有分支。输出结果将显示每个分支的名称以及指定分支是否为当前分支。


示例输出可能如下所示:


Copy Code
develop
* feature/xyz
bugfix/123

其中,* 标记表示当前所在的分支,我们只需要判断输出的分支中是否存在生产分支即可:


function isMergedCheck(branch) {
try {
const command = `git branch --contains ${branch}`;
const result = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
const mergedList = result.split("、");
return mergedList.includes(gitInfoObj.devBranch)
? `已合并到${gitInfoObj.devBranch}`
: "";
} catch (err) {
return "未获取到合并状态";
}
}

(4)删除选中分支


选完分支后我们就该来删除分支了,删除分支的命令大家应该就比较熟悉了吧



  • git branch -D


git branch -D 命令用于强制删除指定的分支()。该命令会删除本地仓库中的指定分支,无法恢复已删除的分支。



  • git push :


git push : 命令用于删除远程仓库中的指定分支()。这个命令通过推送一个空分支到远程仓库的 分支来实现删除操作。


async function doDeleteBranch(branchList) {
const deleteBranchList = await getDeleteBranch(branchList);
if (!deleteBranchList) return;
console.log("正在删除分支");
progressBar.run(0);
deleteBranchList.forEach((branch, index) => {
let command = `git branch -D ${branch}`;
if (branch.includes("/")) {
const tmp = branch.split("/");
command = `git push ${tmp[0]} :${tmp[1]}`;
}
child_process.execSync(command);
progressBar.run(Math.floor(((index + 1) / deleteBranchList.length) * 100));
});
console.log("");
console.log("已删除分支:" + deleteBranchList);
}

image.png


1697995985140.png


1697996057503.jpg


可以看到我们的分支瞬间就清爽了很多。


使用


该工具已经发布到 npm 上,可以直接通过命令npm i -g jyeontu进行安装,安装完后在控制台中输入jyeontu git即可进行操作。


源码


该工具的源码也已经开源,有兴趣的同学可以到Gitee上查看:Gitee地址


作者:JYeontu
来源:juejin.cn/post/7292635075304964123
收起阅读 »

程序员攻占小猿口算,炸哭小学生!

小学生万万没想到,做个加减乘除的口算练习题,都能被大学生、博士生、甚至是程序员大佬们暴打! 最近这款拥有 PK 功能的《小猿口算》App 火了,谁能想到,本来一个很简单的小学生答题 PK,竟然演变为了第四次忍界大战! 刚开始还是小学生友好 PK,后面突然涌...
继续阅读 »

小学生万万没想到,做个加减乘除的口算练习题,都能被大学生、博士生、甚至是程序员大佬们暴打!



最近这款拥有 PK 功能的《小猿口算》App 火了,谁能想到,本来一个很简单的小学生答题 PK,竟然演变为了第四次忍界大战!


登上应用商店榜一


刚开始还是小学生友好 PK,后面突然涌入一波大学生来踢馆,被网友称为 “大学生炸鱼”;随着战况愈演愈烈,硕士生和博士生也加入了战场,直接把小学生学习软件玩成了电子竞技游戏,谁说大一就不是一年级了?这很符合当代大学生的精神状态。


小猿口算排行榜(博一也是一年级)


然而,突然一股神秘力量出现,是程序员带着科技加入战场! 自动答题一秒一道 ,让小学生彻底放弃,家长们也无可奈何,只能在 APP 下控诉严查外挂。



此时很多人还没有意识到,小学生口算 PK,已经演变为各大高校和程序员之间的算法学术交流竞赛!



各路大神连夜改进算法,排行榜上的数据也是越发离谱,甚至卷到了 0.1 秒一道题!



算法的演示效果,可以看我发的 B 站视频。


接口也是口,算法也是算,这话没毛病。


这时,官方不得不出手来保护小学生了,战况演变为官方和广大程序员的博弈。短短几天,GitHub 上开源的口算脚本就有好几页,程序员大神们还找到了多种秒速答题的方案。



官方刚搞了加密,程序员网友马上就成功解密,以至于 网传 官方不得不高价招募反爬算法工程师,我建议直接把这些开源大佬招进去算了。



实现方法


事情经过就是这样,我相信朋友们也很好奇秒答题目背后的实现原理吧,这里我以 GitHub 排名最高的几个脚本项目为例,分享 4 种实现方法。当然,为了给小学生更好的学习体验,这里我就不演示具体的操作方法了,反正很快也会被官方打压下去。


方法 1、OCR 识别 + 模拟操作


首先使用模拟器在电脑上运行 App,运用 Python 读取界面上特定位置的题目,然后运用 OCR 识别技术将题目图片识别为文本并输入给算法程序来答题,最后利用 Python 的 pyautogui 库来模拟人工点击和输入答案。


这种方法比较好理解,应用范围也最广,但缺点是识别效果有限,如果题目复杂一些,准确度就不好保证了。


详见开源仓库:github.com/ChaosJulien…



方法 2、抓包获取题目和答案


通过 Python 脚本抓取 App 的网络请求包,从中获取题目和答案,然后通过 ADB(Android Debug Bridge)模拟滑动操作来自动填写答案。然而,随着官方升级接口并加密数据,这种方法已经失效。


详见开源仓库:github.com/cr4n5/XiaoY…



方法 3、抓包 + 修改答案


这个方法非常暴力!首先通过抓包工具拦截口算 App 获取题目数据和答案的网络请求,然后修改请求体中的答案全部为 “1”,这样就可以通过 ADB 模拟操作,每次都输入 1 就能快速完成答题。 根据测试可以达到接近 0 秒的答题时间!


但是这个方法只对练习场有效,估计是练习场的答题逻辑比较简单,且没有像 PK 场那样的复杂校验。


详见开源仓库:github.com/cr4n5/XiaoY…



方法 4、修改 PK 场的 JavaScript 文件


这种方法就更暴力了!在 PK 场模式下,修改 App 内部的 JavaScript 文件来更改答题逻辑。通过分析 JavaScript 响应中的 isRight 函数,找到用于判定答案正确与否的逻辑,然后将其替换为 true,强制所有答案都判定为正确,然后疯狂点点点就行了。


详见开源仓库:github.com/cr4n5/XiaoY…



能这么做是因为 App 在开发时采用了混合 App 架构,一些功能是使用 WebView 来加载网页内容的。而且由于 PK 场答题逻辑是在前端进行验证,而非所有请求都发送到服务器进行校验,才能通过直接修改前端 JS 文件绕过题目验证。


官方反制


官方为了保护小学生学习的体验,也是煞费苦心。


首先加强了用户身份验证和管理,防止大学生炸鱼小学生;并且为了照顾大学生朋友,还开了个 “巅峰对决” 模式,让俺们也可以同实力竞技 PK。



我建议再增加一个程序员模式,也给爱玩算法的程序员一个竞技机会。


其实从技术的角度,要打击上述的答题脚本,并不难。比如检测 App 运行环境,发现是模拟器就限制答题;通过改变题目的显示方式来对抗 OCR 识别;通过随机展示部分 UI, 让脚本无法轻易通过硬编码的坐标点击正确的答案;还可以通过分析用户的答题速度和操作模式来识别脚本,比如答题速度快于 0.1 秒的用户,显然已经超越了人类的极限。


0.0 秒的这位朋友,是不是有点过分(强大)了?



但最关键的一点是,目前 App 的判题逻辑是在前端负责处理的,意味着题目答案的验证可以在本地进行,而不必与服务器通信,这就给了攻击者修改前端文件的机会。虽然官方通过接口加密和行为分析等手段加强了防御,但治标不治本,还是将判题逻辑转移到服务端,会更可靠。


当然,业务流程改起来哪有那么快呢?




不过现在的局面也不错,大学生朋友快乐了,程序员玩爽了,口算 App 流量赢麻了,可谓是皆大欢喜!


等等,好像有哪里不对。。。别再欺负我们的小学生啦!





作者:程序员鱼皮
来源:juejin.cn/post/7425121392738140214
收起阅读 »

第二届OpenHarmony竞赛训练营颁奖 ——创新驱动,培育未来科技人才

在科技日新月异的时代背景下,OpenAtom OpenHarmony(以下简称“OpenHarmony”)竞赛训练营2024年再度扬帆起航,为高校学子们提供了一个展现创新才能、深入探索前沿技术的广阔舞台。在10月12日以“技术引领筑生态,万物智联创未来”为主题...
继续阅读 »

在科技日新月异的时代背景下,OpenAtom OpenHarmony(以下简称“OpenHarmony”)竞赛训练营2024年再度扬帆起航,为高校学子们提供了一个展现创新才能、深入探索前沿技术的广阔舞台。在10月12日以“技术引领筑生态,万物智联创未来”为主题的第三届OpenHarmony技术大会上,OpenHarmony项目群工作委员会(PMC)和OpenHarmony项目群技术指导委员会(TSC)的专家出席仪式并为10个获奖团队颁奖。

本届 OpenHarmony竞赛训练营聚焦产业需求和AI应用,希望开发者为行业注入新鲜活力,并且结合AI时代的大背景,探索“OpenHarmony+AI”应用先进能力。这不仅为学生们提供了更广阔的创新空间,也为OpenHarmony生态系统的拓展和完善注入了新的动力。竞赛训练营自启动以来,便吸引了众多高校的热切关注与积极参与。来自全国各大重点院校的学生们踊跃报名,涵盖了计算机科学、电子工程、通信技术等多个专业领域。今年来自84个赛队共220+学生报名参与,10月11日经过一天激烈的决赛角逐共有10个赛队脱颖而出。最终,上海交通大学的鸿智宇飞队获得OpenHarmony竞赛训练营特别创新奖;南开大学的拿奖公布蟹黄堡秘方、上海交通大学的古神yyds队、武汉大学的Lumos Solem、华中科技大学的阁下又该如何应队、武汉大学的从容应对、金陵科技学院的你也是个rapper等赛队获得OpenHarmony竞赛训练营三等奖;华中科技大学的加冰陈化、上海交通大学的鸿智宇飞队获得OpenHarmony竞赛训练营二等奖;北京邮电大学的LIVIN赛队获得OpenHarmony竞赛训练营一等奖,该赛队的指导老师熊永平老师荣获OpenHarmony竞赛训练营最佳指导老师奖。

OpenHarmony项目群技术指导委员会(TSC)委员张荣超为特别创新奖团队颁奖

OpenHarmony项目群技术指导委员会(TSC)委员贾宁

OpenHarmony项目群工作委员会(PMC)执行总监陶铭为三等奖团队颁奖

OpenHarmony项目群技术指导委员会(TSC)委员臧斌宇,

OpenHarmony项目群工作委员会(PMC)执行主席柳晓见为二等奖团队获得者颁奖

OpenHarmony项目群工作委员会(PMC)主席、华为终端BG软件部总裁龚体,OpenHarmony项目群技术指导委员会(TSC)主席、华为Fellow、华为基础软件首席科学家陈海波为一等奖获奖团队和最佳指导老师颁奖。

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。OpenHarmony竞赛训练营始终致力于引导高校学生将理论知识与实际应用相结合,推动OpenHarmony产学研用的深度融合。通过精心设计的赛题,以OpenHarmony为核心技术底座,让学生们在解决实际问题的过程中,不断提升自己的技术水平和创新能力。

训练营延续使用实战竞赛+赋能培训的模式,邀请了OpenHarmony行业专家、TSC领域专家、AI领域专家和高校老师,为参赛者提供技术指导、培训和作品评审。OpenHarmony鼓励学生积极参与 OpenHarmony开源社区和相关技术交流平台,充分利用社区丰富的资源,包括文档、代码、开发工具等。同时,配备了专业的社区助手,随时为学生们解答技术难题,促进知识共享和技术交流。与此同时,来自OpenHarmony项目群技术指导委员会、生态、内核、编译优化、视窗等领域,以及各大高校的专家和学者凭借丰富的经验和专业知识,为学生们提供了宝贵的建议和指导,帮助学生们不断优化作品,提高竞争力。

赋能培训环节依然是训练营的重要组成部分。今年的赋能培训内容更加丰富多样,包括前沿技术讲座、赛题深度解读、成功案例分享、开发实战演练、作品要求与评分规则详解等。通过这些培训,学生们不仅能够深入了解OpenHarmony技术体系,还能掌握最新的开发方法和技巧,为作品的创作打下坚实的基础。

为了激励学生们的创新热情,本次训练营设置了丰厚的奖项与奖金。一等奖50000元、二等奖20000元、三等奖10000元,潜力无限奖5000元以及最佳指导老师奖3000元、特别创新奖10000元。这些奖项由行业领军人物、知名专家学者和企业代表颁发,充分体现了对学生们创新成果的高度认可和鼓励。

OpenHarmony 竞赛训练营已经成为培养创新人才、推动OpenHarmony生态发展的重要平台。展望未来,主办方表示将继续加大对训练营的投入和支持,不断丰富赛题内容,拓展合作领域,提高训练营的影响力和吸引力。同时,也希望更多的高校师生能够参与到 OpenHarmony 的开发和应用中来,共同为我国信息技术产业的发展贡献力量。相信在各方的共同努力下,OpenHarmony竞赛训练营将不断创造新的辉煌,为培养未来科技人才、推动科技创新发挥更大的作用。

收起阅读 »

融合大模型技术,激发开发新动力,IDE分论坛成功举办

在当今的数字化浪潮中,软件开发是企业和组织技术架构的核心部分。2024年10月12日,第三届OpenHarmony技术大会的IDE分论坛在上海世博中心举行。论坛聚焦于探讨如何利用IDE工具技术提升OpenAtom OpenHarmony(以下简称“OpenHa...
继续阅读 »

在当今的数字化浪潮中,软件开发是企业和组织技术架构的核心部分。2024年10月12日,第三届OpenHarmony技术大会的IDE分论坛在上海世博中心举行。论坛聚焦于探讨如何利用IDE工具技术提升OpenAtom OpenHarmony(以下简称“OpenHarmony”)应用的开发效率和软件质量,旨在构建一个开放且前瞻性的以IDE为核心的软件开发工具交流平台。

在本次分论坛中,与会嘉宾深入探讨了应用开发技术与工具的工程化解决方案,以及大模型技术与软件开发工具的深度融合,以全面提升OpenHarmony应用开发的效率和质量。通过分享OpenHarmony应用的优秀开发实践和学术前沿的软件开发工具探索,分论坛旨在帮助开发者在OpenHarmony生态中找到更高质量的IDE开发工具方案。

该分论坛由华为软件IDE实验室主任蒋奕和复旦大学计算机学院副院长彭鑫担任本论坛出品人,并由蒋奕主持。在活动中,华为软件IDE实验室主任蒋奕、华为 DevEco Studio 高级技术专家陈晓闯、北京趣拿软件科技有限公司移动端开发总监邹德文、飞书OpenHarmony架构师夏恩龙、中国工商银行软件开发中心研究员赵海强、深圳开鸿数字产业发展有限公司开源社区开发部开发工程师胡瑞涛、百度在线网络技术(北京)有限公司智能研发团队高级经理彭云鹏、北京航天航空大学教授石琳、DeepWisdom创始人兼CEO吴承霖、复旦大学计算机学院副院长彭鑫等嘉宾,分别就各自专业领域的最新进展和实践进行了深入的分享和讨论。

华为软件IDE实验室主任蒋奕发表了主题为“智慧化IDE助力OpenHarmony应用开发探索与实践”的演讲。蒋奕介绍了智慧化IDE在工程级代码生成技术上的突破,这些技术不仅提升了开发效率,还降低了开发门槛,加速了应用的OpenHarmony化进程。华为软件IDE实验室在AI加持的工程级代码生成技术、少语料代码生成技术方面进行了探索,赋能了OpenHarmony UI代码生成、元服务卡片生成及仓颉代码生成开发工具集,致力于打造极简开发体验。

(华为软件IDE实验室主任蒋奕发言)

华为技术专家陈晓闯先生对OpenHarmony应用开发工具DevEco Studio进行了深入介绍。陈晓闯强调了DevEco开发套件的核心特性,包括高效编码、调试、快速构建应用程序等,可以帮助开发者简化开发流程,提升开发效率。陈晓闯展示了DevEco Studio许多功能,包括高效编码、调试、快速构建应用程序、性能调优、代码静态检测等能力,以及如何帮助开发者专注于业务逻辑的实现,从而提高代码编写的效率和应用的整体体验。

(华为技术专家陈晓闯发言)

每次旅行不仅是目的地的探索,也可以是科技体验的旅程。北京趣拿软件科技有限公司移动端开发总监邹德文分享了“去哪儿OpenHarmony跨端技术落地实践”。邹德文讲述了去哪儿网在OpenHarmony平台上采用React Native、Flutter等跨端技术栈,实现了应用的高效跨平台运行能力。邹德文还提到了AI工具在生成目标平台代码方面的应用,这大幅提高了开发效率和应用稳定性,这种跨端技术栈在OpenHarmony化过程中发挥了重要作用。

(北京趣拿软件科技有限公司移动端开发总监邹德文发言)

飞书OpenHarmony架构师夏恩龙分享了“飞书的OpenHarmony化之旅”。夏恩龙详细介绍了飞书企业级应用在OpenHarmony上的适配与升级过程,展示了如何通过一次开发实现多端部署,为用户提供全新的办公体验。夏恩龙强调了飞书与OpenHarmony的合作,不仅提升了办公效率,还引领了智慧协同的新潮流。

(飞书OpenHarmony架构师夏恩龙发言)

中国工商银行软件开发中心互联网金融研究团队的研究员赵海强,在分论坛上介绍了“中国工商银行移动端用户体验提升支撑工具实践”。赵海强探讨了在竞争激烈的APP市场中,如何通过加强底层基础支撑和构建辅助工具,实现APP研发全生命周期体验质量控制。赵海强分享了工商银行在UI一致性、性能、体验、用户友好提示、业务流程交互等方面的研发工具,这些工具在需求、设计、开发、测试各阶段帮助及时发现潜在问题,从而提升工商银行移动端应用的用户体验。

(中国工商银行软件开发中心互联网金融研究团队的研究员赵海强发言)

深圳开鸿数字产业发展有限公司的开源社区开发部开发工程师胡瑞涛,分享了“开发者必备的应用开发工具”。胡瑞涛介绍了全栈开发工具链如何为OpenHarmony生态提供技术支持,强调了这些工具在提升开发效率和生态创新能力方面的重要性。胡瑞涛指出,这些工具不仅简化了开发流程,还推动了新硬件和服务模式的发展,为开发者提供了高效、便捷的开发环境,加速了OpenHarmony在各领域的应用和普及。

(深圳开鸿数字产业发展有限公司的开源社区开发部开发工程师胡瑞涛发言)

在人工智能时代,软件研发范式正在经历的变革。百度在线网络技术(北京)有限公司智能研发团队高级经理彭云鹏,带来了“人工智能原生软件研发新范式”的主题分享。彭云鹏阐述了如何利用AI工具提升研发效率。彭云鹏提到,百度在这一领域的探索和实践,包括代码生成工具Comate的应用,已经实现了全公司35%的新增代码由AI生成,这一比例还在持续增长。

(百度在线网络技术(北京)有限公司智能研发团队高级经理彭云鹏发言)

连续参加两年IDE分论坛的北京航天航空大学教授石琳,分享了“基于智能IDE的开发者个性化数据理解”的主题。石琳探讨了IDE作为开发者编程的主要场所,其中蕴含的丰富个性化数据对于提升大模型的理解能力、实现复杂软件自动化的重要性。石琳提出,通过深入挖掘和理解开发者的编程偏好和项目环境信息,可以助力大模型更好地理解开发者的意图,从而在人机协同的范式中实现从简单代码生成到复杂软件自动化的突破。

(北京航天航空大学教授石琳发言)

DeepWisdom创始人兼CEO吴承霖在分论坛上介绍了“MetaGPT: Coding Through Chat With Agents”。吴承霖展示了如何通过自然语言编程简化开发过程,使编程变得像聊天一样简单。吴承霖提出的MetaGPT框架通过多智能体协同工作,利用自然语言编程重塑了传统IDE模式,显著提升了开发效率。吴承霖还探讨了MetaGPT在代码转译方面的应用,尤其是其对OpenHarmony生态系统创新的推动作用,旨在优化开发流程和增强团队协作。

(DeepWisdom创始人兼CEO吴承霖发言)

复旦大学计算机学院副院长、教授彭鑫,分享了“基于大模型的人机协作生成式应用开发”的主题。彭鑫探讨了大模型技术如何触发软件智能化开发的质变,提出了从软件开发自身规律出发,探索人机协作的智能化开发模式的必要性。彭鑫强调了将演进式设计、特定领域语言(DSL)以及有效的代码审视与反馈与大模型的代码生成能力相结合,形成更高层次上的智能化开发能力的重要性。

(复旦大学计算机学院副院长、教授彭鑫发言)

第三届OpenHarmony技术大会的IDE分论坛的圆满落幕,为开发者社群搭建了一个宝贵的交流舞台。与会者深入探讨了IDE在OpenHarmony应用开发中的关键作用。论坛集中讨论了如何利用IDE提高开发效率、软件质量和用户体验。嘉宾们分享了他们在工程化解决方案、大模型技术与软件开发工具融合方面的见解和经验。此次分论坛的讨论不仅为开发者提供了宝贵的实践指导,还激励了更多开发者以更迅速、更深入的方式投身于OpenHarmony生态,携手促进其蓬勃发展。

收起阅读 »

第三届OpenHarmony技术大会星光璀璨,致谢社区贡献者

10月12日,在上海举办的第三届OpenHarmony技术大会上,32家高校OpenHarmony技术俱乐部璀璨亮相,30家高校OpenHarmony开发者协会盛大启幕。还分别致谢了年度星光TSG(技术专家组)、TSG星光贡献者和星光OpenHarmony技术...
继续阅读 »

10月12日,在上海举办的第三届OpenHarmony技术大会上,32家高校OpenHarmony技术俱乐部璀璨亮相,30家高校OpenHarmony开发者协会盛大启幕。还分别致谢了年度星光TSG(技术专家组)、TSG星光贡献者和星光OpenHarmony技术俱乐部、星光导师、星光贡献者、星光活动等OpenHarmony社区贡献者,大会同步举行了授牌仪式。

作为智能终端领域发展速度最快的开源操作系统,OpenAtom OpenHarmony(以下简称“OpenHarmony”)的进步与发展离不开产业界、学术界及生态伙伴们的协力共建。OpenHarmony项目群技术指导委员会(TSC)积极促进OpenHarmony产学研用深度协同,联合国内外产业界和学术界的操作系统领域高端技术专家,在课题牵引、开源项目孵化、创新人才培养、技术赋能等方面均作出了重要贡献。截至目前,已成立10个技术专家组,汇集了数百名顶尖专家深耕技术。他们识别OpenHarmony的关键技术方向、孵化技术项目、发布技术标准,是OpenHarmony关键技术和OpenHarmony社区发展的重要推动者。同时,OpenHarmony TSC还联合知名高校成立了32个OpenHarmony技术俱乐部及30个OpenHarmony开发者协会,构筑OpenHarmony技术创新与人才高地。OpenHarmony技术俱乐部与OpenHarmony开发者协会在承接技术课题、孵化创新项目、发表研究论文、培养开源人才、拓展技术生态等方面均取得了不俗的成果,为牵引高校师生参与OpenHarmony开源社区共建共享提供了良好助力。

为致谢取得丰硕成果的TSG团队、OpenHarmony技术俱乐部团队及个人,本次大会特别举办了星光团队和星光个人授牌仪式。

共授牌4个星光TSG,分别是安全及机密计算TSG、跨平台应用开发框架TSG、编程语言TSG、通信互联TSG。

授牌10位TSG星光贡献者,分别是编程语言TSG王学智、跨平台应用开发框架TSG晏国淇、安全及机密计算TSG王季、Web3标准TSG Wenjing Chu、机器人TSG巴延兴、IDE TSG刘芳、并发与协同TSG Diogo Behrens、应用开发工程技术TSG程帅、智能数据管理TSG李永坤、通信互联TSG李锋。

授牌5个“技术突破”星光OpenHarmony技术俱乐部,分别是来自上海交通大学、北京航空航天大学、北京理工大学、兰州大学、华中科技大学的OpenHarmony技术俱乐部。

授牌5个“菁英教育”星光OpenHarmony技术俱乐部,分别是来自大连理工大学、北京邮电大学、电子科技大学、西安电子科技大学、湖南大学的OpenHarmony技术俱乐部。

授牌11个“活力引领”星光OpenHarmony技术俱乐部,分别是来自中山大学、东南大学、西安交通大学、华南理工大学、武汉大学、南开大学、南昌大学、重庆大学、复旦大学、浙江大学、厦门大学的OpenHarmony技术俱乐部。

授牌5位星光导师,分别是上海交通大学OpenHarmony技术俱乐部夏虞斌、北京邮电大学OpenHarmony技术俱乐部邹仕洪、北京航空航天大学OpenHarmony技术俱乐部黎立、电子科技大学OpenHarmony技术俱乐部唐佐林、兰州大学OpenHarmony技术俱乐部周庆国。

授牌5位OpenHarmony技术俱乐部星光贡献者,分别是东南大学OpenHarmony技术俱乐部李光伟、北京航空航天大学OpenHarmony技术俱乐部陈岱杭、兰州大学OpenHarmony技术俱乐部王天一、华中科技大学OpenHarmony技术俱乐部刘浩毅、湖南大学OpenHarmony技术俱乐部银天杨。

授牌3项OpenHarmony技术俱乐部星光活动,分别是西安电子科技大学OpenHarmony技术俱乐部出品的“红色筑梦·智汇未来”基础软件开源生态研讨会暨OpenHarmony城市技术论坛延安站活动、上海交通大学OpenHarmony技术俱乐部出品的ASPLOS 2024-OpenHarmony国际学术教程会、厦门大学OpenHarmony技术俱乐部出品的海峡开源人才培养研讨会暨厦门大学OpenHarmony技术俱乐部成立仪式。

收起阅读 »

​OpenHarmony统一互联PMC启动孵化

在2024年10月12日于上海举办的第三届OpenHarmony技术大会上,OpenHarmony统一互联PMC(项目群项目管理委员会)正式启动孵化。OpenHarmony项目群工作委员会主席、华为终端BG软件部总裁龚体,OpenHarmony项目群技术指导委...
继续阅读 »

在2024年10月12日于上海举办的第三届OpenHarmony技术大会上,OpenHarmony统一互联PMC(项目群项目管理委员会)正式启动孵化。

OpenHarmony项目群工作委员会主席、华为终端BG软件部总裁龚体,OpenHarmony项目群技术指导委员会主席、华为基础软件首席科学家陈海波,以及海思技术有限公司、深圳开鸿数字产业发展有限公司、湖南开鸿智谷数字产业发展有限公司、鸿湖万联(江苏)科技发展有限公司、广东九联开鸿科技发展有限公司、鼎桥通信技术有限公司、山东亚华电子股份有限公司、华为终端有限公司、中国科学院软件研究所、福建汇思博数字科技有限公司、诚迈科技(南京)股份有限公司、福建升腾资讯有限公司、江苏润开鸿数字科技有限公司、深圳市证开鸿科技有限公司、北京奥思维科技有限公司、芯海科技(深圳)股份有限公司、北京中科鸿略科技有限公司、深圳华龙讯达信息技术股份有限公司、贯蒙(深圳)科技有限公司、天翼物联科技有限公司、北京航天万源科技有限公司、深圳鸿信智联数字科技有限公司、深圳鸿元智通科技有限公司、公安部第一研究所、鸿蒙生态服务公司等25家初创成员代表出席启动仪式。

OpenHarmony统一互联PMC 致力于解决OpenAtom OpenHarmony(以下简称“OpenHarmony”)设备跨操作系统、跨厂家之间的互联互通互操作问题,聚焦HarmonyOS之间、不同OpenHarmony厂商之间,以及与三方OS之间的设备连接,从建底座、定标准、搭平台三个维度构筑统一互联的技术底座。OpenHarmony统一互联PMC孵化范围包括OneConnect应用和组件、OneConnect云侧配套,包括OpenHarmony通用互联应用、OpenHarmony图库分享组件、OpenHarmony文件管理器分享组件、OpenHarmony投屏分享组件、OpenHarmony设备侧业务控制联动组件、统一互联物模型服务器、统一互联认证服务器等。

据了解,OpenHarmony统一互联PMC主要通过共建项目的方式运作,其中共建项目1.0分为3个子项目,分别包括富对瘦设备控制、富对富投屏、富对富文件互传,3个子项目均在交付中;共建项目2.0分为4个子项目,分别包括富对瘦设备控制2.0、富对富投屏2.0、富对富文件互传2.0、分布式摄像头,现已完成场景确定及相关需求分析。OpenHarmony统一互联PMC生态伙伴由最初的华为与7家生态伙伴扩增到25家。

在启动仪式上,还发布了OpenHarmony统一互联系列标准2.0。该系列标准作为统一互联PMC所孵化的设备互联、数据互通、业务互操作相关解决方案的技术沉淀,为教育、金融、交通、政务、医疗等行业形成统一互联互通提供了基础标准参考。

本次发布的标准共计六篇,不仅包含了富对瘦设备之间设备控制、设备联动场景,还涵盖了富对富设备之间的投屏、文件分享等常用业务。可以预见,OpenHarmony统一互联技术标准的发展,将助力打造真正的OpenHarmony物联网生态,实现设备之间的无缝连接,提供更流畅、更安全的用户体验。

收起阅读 »

第三届OpenHarmony技术大会应用开发工程技术分论坛成功举行

OpenAtom OpenHarmony(以下简称OpenHarmony)生态的繁荣,需要构建服务于千行万业的应用生态,提供高效的应用开发工程技术和完备的软件工程能力成为推动OpenHarmony应用生态高效、低成本可持续发展的关键因素。2024年10月12日...
继续阅读 »

OpenAtom OpenHarmony(以下简称OpenHarmony)生态的繁荣,需要构建服务于千行万业的应用生态,提供高效的应用开发工程技术和完备的软件工程能力成为推动OpenHarmony应用生态高效、低成本可持续发展的关键因素。2024年10月12日下午第三届OpenHarmony技术大会应用开发工程技术分论坛在上海成功举行。该分论坛围绕前沿的应用开发技术与移动软件工程能力,在人机物融合的智能系统及应用新形态、应用业务逻辑分析和安全检测技术、开发者自动化测试、Qt/Flutter框架新技术、大型应用构建和持续集成能力等议题展开深入探讨与经验分享。

OpenHarmony应用开发工程技术TSG主任任晗;北京航空航天大学教授、博士生导师史晓华作为应用开发工程技术分论坛出品人出席本次活动。复旦大学计算机科学技术学院副院长、教授彭鑫;中国科学院计算技术研究所研究员李炼;华东师范大学教授苏亭;复旦大学青年副研究员张晓寒;Qt资深方案工程师雒少华;华为高级技术专家邵甜鸽;华为技术专家武超;深圳开鸿数字产业发展有限公司架构设计工程师丁力出席本论坛并发表演讲。OpenHarmony应用开发工程技术TSG主任任晗主持了整场会议。

(OpenHarmony应用开发工程技术TSG主任任晗主持会议)

复旦大学计算机科学技术学院副院长、教授彭鑫以《软件定义的人机物融合智能化系统及应用》为主题发表演讲。他对软件定义的人机物融合智能系统的相关思想、发展现状和底层逻辑进行了阐释和分析,并介绍了团队在云边融合的运行支撑系统、人机物资源的软件定义方法及应用构造方法等方面进行的一些初步探索。据介绍,软件定义的人机物融合智能化系统将云计算和云原生的思想拓展到智慧园区、智慧大楼、智能家居、智能网联汽车等社会物理空间,以软件定义的方式实现人机物资源的编程抽象和平台化管理,支持系统的快速迭代和持续演化,同时支持基于低代码开发及自然语言编程的人机物融合应用构造,从而可以更好地实现以用户为中心的人机物融合应用执行与服务提供。OpenHarmony面向万物互联的智能化应用场景为开发者提供了一次开发多端部署、 分布式软总线、分布式数据服务、应用自由流转的平台能力,可以为建立面向人机物融合用应用的“开发运维一体化”目标提供支撑。

(复旦大学计算机科学技术学院副院长、教授彭鑫发言)

中国科学院计算技术研究所研究员李炼聚焦高层语义的自适应分析方法与工具展开分享。应用层的大部分安全性问题以及性能和功能问题都需要深入理解高层的应用逻辑语义。但这些高层语义和应用具体实现密切相关,往往无法进行通用的定义。那么如何通过自动或半自动的方法推断高层应用语义,以及严格表述这些语义信息?如何实现高效且易于扩展的高层语义分析工具?针对上述问题,李炼提出可以通过声明式方法定义高层语义,并扩展现有工具以自动检测自定义语义,从而兼顾可扩展性、效率和精度展开讨论。他指出,通过自动或半自动高层语义推断以及自适应分析方法与工具,可以解决灵活多变的应用层逻辑问题。

(中国科学院计算技术研究所研究员李炼发言)

华东师范大学教授苏亭分享了面向OpenHarmony应用的开发者自动化测试技术新范式。苏亭指出,保障OpenHarmony应用稳定和正确运行是OpenHarmony生态发展的重要目标。然而,与其他现有移动平台应用(如安卓、iOS等)相比,OpenHarmony应用在编程语言、开发特性、架构设计等方面有着显著的不同,这为设计和构建OpenHarmony应用自动化测试技术带来了挑战。鉴于此,苏亭教授介绍了其所带领的研究小组在OpenHarmony应用自动化测试方面的探索和工程化实践,并介绍了基于代码功能地图的OpenHarmony应用增强遍历测试技术和基于性质的OpenHarmony应用异常测试技术。

(华东师范大学教授苏亭发言)

“安全不是选项,而是必需”,复旦大学青年副研究员张晓寒在《移动应用业务安全研究与生态治理》的演讲中强调。本次论坛他带来了在移动应用业务安全方面开展的相关研究与实践成果,并与与会者共同探讨了基于移动应用逆向、程序分析、深度学习与大模型等技术形成的一套应用业务安全分析思路和方法。同时,张晓寒重点分享了团队在移动应用认证安全、端侧风控、应用行为理解、敏感行为感知等方面进行的学术探索,汇报了在漏洞挖掘与治理、应用生态治理等方面进行的尝试和实践。他的相关研究曾获华为优秀技术成果奖、CNVD最具价值漏洞等荣誉。

(复旦大学青年副研究员张晓寒发言)

Qt资深方案工程师雒少华在本次论坛中以《Qt携手OpenHarmony:共创软件新生态的适配之旅》为主题,深入剖析Qt框架如何高效适配OpenHarmony操作系统,展现其在软件生态构建中的关键角色;探讨Qt跨平台技术的独特优势,在OpenHarmony环境下的应用创新,以及如何促进开发者快速迁移,加速软件生态的繁荣。雒少华展望道:“在OpenHarmony的沃土上,Qt绽放新生,共绘软件生态的宏伟蓝图。”

(Qt资深方案工程师雒少华发言)

Flutter作为今年来流行的跨平台开发框架,在全球范围内获得了广泛的应用和认可。OpenHarmony系统如果能成功融入 Flutter 生态系统,将会对OpenHarmony生态产生深远影响。会有什么影响呢?华为技术专家邵甜鸽对此给予了解答。邵甜鸽认为:Flutter 的跨平台能力可以极大减少伙伴的开发和维护成本,且可以使应用快速迁移到OpenHarmony平台,迅速丰富OpenHarmony应用生态。Flutter的自渲染引擎可以有效保证在不同平台上的一致性用户体验,通过优化 Flutter 在OpenHarmony系统上的性能,进一步实现极致流畅的用户体验。Flutter 的广泛使用和社区支持吸引了更多的开发者加入OpenHarmony生态,其丰富的共享资源和插件可以提高开发效率,帮助OpenHarmony快速建立起应用生态,提升竞争力。

(华为高级技术专家邵甜鸽发言)

华为技术专家武超在本次演讲中分享了OpenHarmony大型工程的依赖管理与多产物构建的经验。为与会者介绍OpenHarmony系统依赖管理的几种最常见模式和相应的技术,讲解构建系统的几个核心概念和顶层的领域模型,并分享了OpenHarmony系统上的多产品、多环境、多设备的多目标构建工程能力。

(华为技术专家武超发言)

会议最后,深圳开鸿数字产业发展有限公司架构设计工程师丁力以《OpenHarmony应用开发持续集成工程能力构建》为主题做了报告分享。他指出,持续集成构建、gerrit管控代码、代码门禁集成增量编译、静态检查、单元测试等多种管控措施,是全力构筑好版本质量管控的首道防线。丁力分别从持续集成工具链的整体架构和流程架构两个方面,介绍了深开鸿软件工程团队在此方面的实践探索,并着重分享了OpenHarmony应用开发从编译构建、代码检查、到功能测试的持续集成能力关键技术。

(深圳开鸿数字产业发展有限公司架构设计工程师丁力发言)

应用开发工程技术分论坛通过实际案例和技术分享,旨在帮助开发者在OpenHarmony生态中找到最优的工程方案。OpenHarmony项目技术指导委员会应用开发工程技术TSG致力于构建一个开放且前瞻性的应用工程技术交流平台,为开发者提供从工程指导到模板应用的全方位支持,推动高质量OpenHarmony应用的开发与生态建设。通过共同探索和实践,打造一个高效、安全、高质量的OpenHarmony应用开发平台。

收起阅读 »

啊?两个vite项目怎么共用一个端口号啊

web
问题: 最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后: ...
继续阅读 »

问题:


最近在业务开发中遇到一个问题,问题是这样的,当前有一个主项目和一个子项目,主项目通过微前端wujie来嵌套这个子项目,其中呢为了方便项目之间进行通信,所以规定该子项目的端口号必须为5173,否则通信失败,但是这时候发现一个问题,当我启动了该子项目后:


image.png


该项目的端口号为5173,但是此时我再次通过vite的官方搭建一个react+ts+vite项目:npm create vite@latest react_demos -- --template react-ts,之后通过npm run dev启动项目,发现端口号并没有更新:


image.png


这是什么原因呢?


寻因:


查阅官方文档,我发现:


image.png


那么我主动在vite.config.ts中添加这个配置:


image.png


正常来说,会出现这个报错:


image.png


但是此时结果依然为:


image.png


我百思不得不得其解,于是再次查阅官方文档:


image.png
我寻思这也与文档描述不一致啊,于是我再次尝试,思考是不是vite版本号的问题,两个项目的版本号分别为:


image.png


image.png


我决定创建一个4版本的项目npm create vite@^4.1.4 react_demos3 -- --template react-ts


image.png


结果发现,还是有这个问题,跟版本号没有关系,于是我又耐心继续看官方文档,看到了这个配置:


image.png
我抱着试试的态度,在其中一个vite项目中添加这个配置:


image.png


发现,果然是这个配置的锅,当其中一个项目host配置为0.0.0.0时,vite不会自动尝试更新端口号


难道vite的端口监测机制与host也有关?


结果:


不甘心的我再次进行尝试,将两个项目的host都设置成:


image.png


image.png


vite会自动尝试更新端口号


原来如此,vite的端口号检测机制在对比端口号之前,会先对比host,由于我的微前端项目中设置了host,而新建的项目中没有设置host,新建的项目host默认值为localhost对比不成功,vite不会自动尝试下一个可用端口,而是共用一个端口


总结:


在遇到问题时,要多多去猜,去想各种可能,并且最重要的是去尝试各种可能,还要加上积极去翻阅官方文档,问题一定会得到解决的;哪怕不能解决,那也会在尝试中,学到很多东西


作者:进阶的鱼
来源:juejin.cn/post/7319699173740363802
收起阅读 »

还搞不明白浏览器缓存?

web
一:前言 浏览器缓存与浏览器储存是不一样的,友友们不要混淆,关于浏览器储存,具体可以看这篇文章 : 一篇打通浏览器储存 这里大概介绍一下: cookieslocalStoragesessionStorageIndexedDB服务端设置一直存在页面关闭就消失一...
继续阅读 »

一:前言


浏览器缓存浏览器储存是不一样的,友友们不要混淆,关于浏览器储存,具体可以看这篇文章 : 一篇打通浏览器储存


这里大概介绍一下:


cookieslocalStoragesessionStorageIndexedDB
服务端设置一直存在页面关闭就消失一直存在
4K5M5M无限大
自动携带在http请求头中不参与后端不参与后端不参与后端
默认不允许跨域,但可以设置可跨域可跨域可跨域

二:强缓存


强缓存是指浏览器在请求资源时,如果本地有符合条件的缓存,那么浏览器会直接使用缓存而不会向服务器发送新的请求。这可以通过设置 Cache-ControlExpires 响应头来实现。


2.1:Cache-Control 头详解


Cache-Control 是一个非常强大的HTTP头部字段,它包含多个指令,用以控制缓存的行为:



  • max-age:指定从响应生成时间开始,该资源可被缓存的最大时间(秒数)。

  • s-maxage:类似于 max-age,但仅对共享缓存(如代理服务器)有效。

  • public:表明响应可以被任何缓存存储,即使响应通常是私有的。

  • private:表明响应只能被单个用户缓存,不能被共享缓存存储。

  • no-cache:强制缓存在使用前必须先验证资源是否仍然新鲜。

  • no-store:禁止缓存该响应,每次请求都必须获取最新数据。

  • must-revalidate:一旦资源过期,必须重新验证其有效性。


例如,通过设置 Cache-Control: max-age=86400,可以告诉浏览器这个资源可以在本地缓存24小时。在这段时间内,如果再次访问相同URL,浏览器将直接使用缓存中的副本,而不与服务器通信。


2.2:Expires 头


Expires 是一个较旧的头部字段,用于设定资源过期的具体日期和时间。尽管现在推荐使用 Cache-Control,但在某些情况下,Expires 仍然是有效的。Expires 的值是一个绝对的时间点,而不是相对时间。例如:


Expires: Wed, 09 Oct 2024 18:29:00 GMT

2.3:浏览器默认行为


当用户通过地址栏直接请求资源时,浏览器通常会自动添加 Cache-Control: no-cache 到请求头中。这意味着即使资源已经存在于缓存中,浏览器也会尝试重新验证资源新鲜度,以确保用户看到的是最新的内容。


三:协商缓存


协商缓存发生在资源的缓存条目已过期设置了 no-cache 指令的情况下。这时,浏览器会向服务器发送请求,并携带上次请求时收到的一些信息,以便服务器决定是否返回完整响应或只是确认没有更新。


3.1:Last-Modified/If-Modified-Since


后端服务器可以为每个资源设置 Last-Modified 头部,表示资源最后修改的时间。当下一次请求同一资源时,浏览器会在请求头中加入 If-Modified-Since 字段,其值为上次接收到的 Last-Modified 值。服务器检查这个时间戳,如果资源自那以后没有改变,则返回304 Not Modified状态码,指示浏览器使用缓存中的版本。


3.2:ETag/If--Match


ETag 提供了一种更精确的方法来检测资源是否发生变化。它是基于文件内容计算出的一个唯一标识符。当客户端请求资源时,服务器会在响应头中提供一个 ETag 值。下次请求时,浏览器会发送 If--Match 头部,包含之前接收到的 ETag。如果资源未改变,服务器同样返回304状态码;如果有变化,则返回完整的资源及新的 ETag 值。


3.3:比较 Last-Modified 和 ETag


虽然 Last-Modified 简单易用,但它基于时间戳,可能会受到时钟同步问题的影响。相比之下,ETag 更加准确,因为它依赖于资源的实际内容。然而,ETag 计算可能需要更多的服务器处理能力。


四:缓存选择


合理的缓存策略能够显著提升网站性能和用户体验。例如,静态资源(如图片、CSS、JavaScript文件)适合设置较长的缓存时间,而动态内容则需谨慎对待,避免缓存不适当的信息。



  • 使用工具如 Chrome DevTools 来分析页面加载时间和缓存效果。

  • 对不同类型的资源设置合适的 Cache-Control 参数。

  • 注意安全性和隐私保护,确保敏感数据不会被错误地缓存。


五:使用示例



  1. 引入必要的模块:导入 http, path, fsmime 模块。

  2. 创建HTTP服务器:使用 http.createServer 创建一个HTTP服务器。

  3. 处理请求

    • 根据请求的URL生成文件路径。

    • 检查文件是否存在。

    • 如果是目录,指向该目录下的 index.html 文件。



  4. 处理协商缓存

    • 获取请求头中的 If-Modified-Since 字段。

    • 比较 If-Modified-Since 与文件的最后修改时间。



  5. 读取文件并发送响应

    • 读取文件内容。

    • 设置响应头(包括 Content-Type, Cache-Control, Last-Modified, ETag)。

    • 发送响应体。



  6. 启动服务器:监听3000端口并启动服务器。


server.js:


const http = require('http'); // 引入HTTP模块
const path = require('path'); // 引入路径处理模块
const fs = require('fs'); // 引入文件系统模块
const mime = require('mime'); // 引入MIME类型模块

// 创建一个HTTP服务器
const server = http.createServer((req, res) => {
// console.log(req.url); // /index.html // /assets/image/logo.png

// 根据请求的URL生成文件路径
let filePath = path.resolve(__dirname, path.join('www', req.url));

// 检查文件或目录是否存在
if (fs.existsSync(filePath)) {
const stats = fs.statSync(filePath); // 获取该路径对应的资源状态信息
// console.log(stats);

const isDir = stats.isDirectory(); // 判断是否是文件夹
const { ext } = path.parse(filePath); // 获取文件扩展名
if (isDir) {
// 如果是目录,则指向该目录下的 index.html 文件
filePath = path.join(filePath, 'index.html');
}

// +++++ 获取前端请求头中的if-modified-since
const timeStamp = req.headers['if-modified-since']; // 获取请求头中的 If-Modified-Since 字段
let status = 200; // 默认响应状态码为200
if (timeStamp && Number(timeStamp) === stats.mtimeMs) { // 如果 If-Modified-Since 存在且与文件最后修改时间相同
status = 304; // 设置响应状态码为304,表示资源未变更
}

// 如果不是目录且文件存在
if (!isDir && fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath); // 读取文件内容
res.writeHead(status, {
'Content-type': mime.getType(ext), // 设置 Content-Type 头
'cache-control': 'max-age=86400', // 设置缓存控制为一天
// 'last-modified': stats.mtimeMs, // 资源最新修改时间(可选)
// 'etag': '由文件内容生成的hash' // 文件指纹(可选)
});
res.end(content); // 发送文件内容作为响应体
}
}

});

// 启动服务器,监听3000端口
server.listen(3000, () => {
console.log('listening on port 3000');
});r.listen(3000, () => {
console.log('listening on port 3000');
});

index.html:


<body>
<h1>midsummer</h1>
<img src="assets/image/1.png" alt="">
</body>


项目结构如下图,友友们自行准备一张图片,将项目npm init -y 初始化为后端项目,之后下载mime@3包,在终端输入npx nodemon server.js运行起来,在浏览器中查看http://localhost:3000/index.html ,观察效果。在检查中的网络里看缓存效果,同时友友们可以更改图片或者缓存方式,体验下不同的浏览器缓存方式


image.png


作者:midsummer18
来源:juejin.cn/post/7423298788873142326
收起阅读 »

告别axios,这个库让你爱上前端分页!

web
嗨,我们又见面了! 今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了! 那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢...
继续阅读 »

嗨,我们又见面了!


今天咱们聊聊前端分页加载那些事儿。你有没有遇到过这样的烦恼:在做分页的时候,要手动维护各种状态,比如页码、每页显示数量、总数据量等等,还要处理各种边界情况,哎呀妈呀,真是太麻烦了!


那么,有没有什么好办法能让我们从这些繁琐的工作中解脱出来呢?这时候,alovajs就派上用场了!


alovajs:轻量级请求策略库


alovajs是一个轻量级的请求策略库,它可以帮助我们轻松处理分页请求。它支持开发者使用声明式实现各种复杂的请求,比如请求共享、分页请求、表单提交、断点续传等等。使用alovajs,我们可以用很少的代码就实现高效、流畅的请求功能。比如,在Vue中,你可以这样使用alovajs进行分页请求:


const alovaInstance = createAlova({
// VueHook用于创建ref状态,包括请求状态loading、响应数据data、请求错误对象error等
statesHook: VueHook,
requestAdapter: GlobalFetch(),
responded: response => response.json()
});

const { loading, data, error } = useRequest(
alovaInstance.Get('https://api.alovajs.org/profile', {
params: {
id: 1
}
})
);

看到了吗?只需要几行代码,alovajs就帮我们处理了分页请求的各种细节,我们再也不用手动维护那些繁琐的状态了!


对比axios,alovajs的优势


和axios相比,alovajs有哪些优势呢?首先,alovajs与React、Vue等现代前端框架深度融合,可以自动管理请求相关数据,大大提高了开发效率。其次,alovajs在性能方面做了很多优化,比如默认开启了内存缓存和请求共享,这些都能显著提高请求性能,提升用户体验的同时还能降低服务端的压力。最后,alovajs的体积更小,压缩后只有4kb+,相比之下,axios则有11+kb。


总之,如果你想在分页加载方面做得更轻松、更高效,alovajs绝对值得一试!


作者:古韵
来源:juejin.cn/post/7331924057925533746
收起阅读 »

如何用AI两小时上线自己的小程序

ChatGPT这个轰动全球的产品自问世以来,已经过了将近2年的时间,各行各业的精英们如火如荼的将AI能力应用到自己生产的产品中来。 为分担人类的部分工作,AI还具有非常大的想象空间,例如对于一个程序员来说,使用AI生成快速生成自己的小程序,相信在AI能力与开发...
继续阅读 »

ChatGPT这个轰动全球的产品自问世以来,已经过了将近2年的时间,各行各业的精英们如火如荼的将AI能力应用到自己生产的产品中来。


为分担人类的部分工作,AI还具有非常大的想象空间,例如对于一个程序员来说,使用AI生成快速生成自己的小程序,相信在AI能力与开发工具融合从可用性到易用性普及以后,会变成一个“习以为常”的操作。


App or 小程序?


在APP开发与小程序开发技术路径之间,本人选择了轻应用的技术开发路线,主要是相信“效率为王”,高产才能给自己赚取更高的收益。


好了,选定方向以后,接下来就是技能的学习和深入。AI的效率之高和学习成本之低,在技能深耕让我想到了是否能借助AI做更多的尝试,比如零基础开发一个页面,甚至一个小程序?


说干就干,开始着手进行准备工作:开发什么应用好呢?要不就一个简单的电商小程序吧。


一、准备工作


最开始的开始,我们先要找一个开发工具,既能帮助我们可视化的开发小程序的,又有可以接收prompt的AI能力。找度娘搜索了下,发现一款产品:FinClip的开发者工具(FinClip IDE)。


二、生成小程序


首先,随意输入一句话的提示词:


「创建一个product页面,每个product项有名称描述和单价」,看看能得出怎样的结果。



结果还是比较让人意外的,只是简单的prompt,就能得到下图的页面布局和结构,看来FinClip这个产品设计者也是很用心的,非常懂开发者的“痛”。



正所谓一个好的电影,70%都要靠导演和编导的构思,一个好的应用程序也不例外,如果要利用好AI能力,就需要有更详细的prompt规划,例如一些结构(如下),大家感兴趣的可以多尝试下:



  • 内容(什么类型的小程序):XXXXXXX

  • 布局(小程序的主要页面都有什么,按钮、图片之类的):XXXXXX

  • 交互(页面上用户的使用操作):XXXXXXX


如果prompt出来的效果并不能一次性的调整到位,FinClip的这个开发者工具还能局部修改页面代码,加上小程序页面的实时预览功能,就能够让一个开发小白尽可能的在成本输出之前进行多次调整,不得不说还是非常方便的。



其他有趣的功能,就是对于一个小程序开发小白来说,很有可能就连小程序开发语法和技术都不熟练,如何能够基于产品已有的开发文档,更便捷的进行知识提取,FinClip也通过一个AI agent连通了自有的小程序开发的知识连起来,让使用的开发者能够更好的对开发知识进行检索。



三、小结


从idea到上线,只花了2个小时,整个流程中,除了手动调整样式的数值,没有写一行代码,全部由AI能力,结合prompt帮助我完成。


这只是一次很浅层的探索案例,对我个人来说只是在小程序技能深入学习前的一个小实践,很有可能,对于熟练的前端开发来说可能就是一个小时工作量,但在这里分享的目的,是为了分享下所谓的拥抱新技术所带来的好处,与此同时,也是给大家带来一点小焦虑,正所谓“不进则退”,很多经验可能自己埋头积累并不能获得质的飞跃,最终可能自己是个"井底之蛙",花大力气却换来了小惊喜,还不如拥抱变化,使用新技术快速提升自己的工作技能。


共勉。


作者:Speedoooo
来源:juejin.cn/post/7423279449915293707
收起阅读 »