注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

弃用qiankun!看古茗中后台架构如何破局

引言 我们团队之前发布过一篇文章,介绍了古茗前端到底在做什么,当然这只包含了我们团队内做的一部分事情。以端侧来划分,主要包括:中后台 web 端、H5 端、4 端小程序、Electron PC 端、Android/Flutter 客户端,我们需要为这些端侧方向...
继续阅读 »

引言


我们团队之前发布过一篇文章,介绍了古茗前端到底在做什么,当然这只包含了我们团队内做的一部分事情。以端侧来划分,主要包括:中后台 web 端、H5 端、4 端小程序、Electron PC 端、Android/Flutter 客户端,我们需要为这些端侧方向做一些通用能力的解决方案,同时也需要深入每个端侧细分领域做它们特有的技术沉淀。本文主要介绍古茗在中后台技术方向的一些思考和技术沉淀。


业务现状


古茗目前有大量的中后台业务诉求,包括:权限系统、会员系统、商品中心、拓展系统、营运系统、财务系统、门店系统、供应链系统等多个子系统,服务于内部产品、技术、运营,及外部加盟商等用户群体,这些子系统分别由不同业务线的同学负责开发和维护,而这些子系统还没有系统性的技术沉淀。随着业务体量和业务复杂度的不断增大,在“降本增效”的大背景下,如何保证业务快速且高质量交付是我们面临的技术挑战。


技术演进




如上述技术演进路线图所示,我们的中后台技术架构大致经历了 4 个阶段:当业务起步时,我们很自然的使用了单应用开发模式;当业务发展到一定的体量,多人协同开发变得困难时,我们拆分了多个子系统维护,并使用了 systemjs 来加载子系统资源(微前端模式的雏形);当遇到有三方库的多版本隔离诉求时,我们又引入了 qiankun 微前端框架来隔离多个子系统,保证各子系统间互不影响;那是什么原因让我们放弃 qiankun,转向完全自研的一套中后台解决方案?


弃用 qiankun?


其实准确来说,qiankun 并不能算是一个完整的中后台解决方案,而是一个微前端框架,它在整个技术体系里面只充当子应用管理的角色。我们在技术演进过程中使用了 qiankun 尝试解决子应用的隔离问题,但同时也带来了一些新的问题:某些场景跳转路由后视图无反应;某些三方库集成进来后导致奇怪的 bug...。同时,还存在一些问题无法使用 qiankun 解决,如:子应用入口配置、样式隔离、运维部署、路由冲突、规范混乱、要求资源必须跨域等。


探索方向


我们重新思考了古茗中后台技术探索方向是什么,也就是中后台技术架构到底要解决什么问题,并确定了当下的 2 大方向:研发效率用户体验。由此我们推导出了中后台技术探索要做的第一件事情是“统一”,这也为我们整个架构的基础设计确立了方向。




架构设计


我们的整体架构是围绕着“统一”和“规范” 2 大原则来设计,目标是提升整个团队的研发效能。我们认为好的架构应该是边界清晰的,而不是一味的往上面堆功能,所以我们思考更多的是,如果没有这个功能,能否把这件事情做好。


取个“好”名字


我老板曾说过一个好的(技术)产品要做的第一件事就是取个“好”名字。我们给这套中后台架构方案取名叫「Mars」,将相关的 NPM 包、组件库、SDK、甚至部署路径都使用 mars 关键字来命名,将这个产品名字深入人心,形成团队内共同的产品认知。这样的好处是可以加强团队对这套技术方案的认同感,以及减少沟通负担,大家一提到 mars,就都知道在说哪一件事。


框架设计




正如上述大图所示,我们是基于微前端的思路做的应用框架设计,所以与市面上大多数的微前端框架的设计思路、分层结构大同小异。这里还是稍微介绍一下整个流程:当用户访问网站时,首先经过网关层,请求到基座应用的资源并渲染基础布局和菜单,当监听路由变化时,加载并渲染路由关联的子应用及页面。


但是,市面上大部分的微前端框架往往需要在基座应用上配置子应用的 nameentryavtiveRule 等信息,因为框架需要根据这些信息来决定什么时机去加载哪一个子应用,以及如何加载子应用的资源。这就意味着每新增一个子应用,都需要去基座维护这份配置。更要命的是,不同环境的 entry 可能还要根据不同的环境做区别判断。如果遇到本地开发时需要跳转至其他子应用的场景,那也是非常不好的开发体验。所以,这是我们万万不能接受的。


针对这一痛点,我们想到了 2 种解决思路:

  1. 把基座应用的配置放到云端,用 node 作为中间层来维护各子应用的信息,每次新增应用、发布资源后同步更新,问题就是这套方案实现成本比较高,且新增子应用还是需要维护子应用的信息,只是转移到在云端维护了。
  2. 使用约定式路由及部署路径,当我们识别到一个约定的路由路径时,可以反推它的应用 ID 及部署资源路径,完全 0 配置。很明显,我们选择了这种方案。

约定式路由及部署路径


路由约定


我们制定了如下的标准 Mars 路由规范

  /mars/appId/path/some?name=ferret
\_/ \_/ \_____/ \_______/
| | | |
标识 appId path query


  1. 路由必须以 /mars 开头(为了兼容历史路由包袱)

  2. 其后就是 appId ,这是子应用的唯一标识

  3. 最后的 pathquery 部分就是业务自身的路由和参数


部署路径约定


我们制定了如下的标准 Mars 子应用部署路径规范

  https://cdn.example.com/mars/[appId]/[env]/manifest.json
\__________________/ \_/ \___/ \_/ \________/
| | | | |
cdn 域名 标识 appId 环境 入口资源清单

从上述部署路径规范可以看出,整个路径就 appIdenv 2 个变量是不确定的,而 env 可以在发布时确定,因此可由 appId 推导出完整的部署路径。而根据路由约定,我们可以很容易的从路由中解析出 appId,由此就可以拿到完整的 manifest.json 部署路径 ,并以此获取到整个子应用的入口资源信息。


编译应用


虽然制定了上述 2 大规范,但是如何保障规范落地,防止规范腐化也是非常重要的一个问题。我们是通过编译手段来强制约束执行的(毕竟“人”往往是靠不住的😄)。


依赖工程化体系



提示:Kone 是古茗内部前端工程化的工具产品。



首先,子应用需要配置一个工程配置文件,并注册 @guming/kone-plugin-mars 插件来完成子应用的本地开发、构建、发布等工程化相关的任务。其中:配置项 appId 就代表约定路由中的 appId 和 部署路径中的 appId,也是子应用的唯一标识。


工程配置文件:kone.config.json

{
"plugins": ["@guming/kone-plugin-mars"],
"mars": {
"appId": "demo"
}
}

编译流程


然后,子应用通过静态化配置式(json 配置)注册路由,由编译器去解析配置文件,注册路由,以及生成子应用 mountunmount 生命周期方法。这样实现有以下 3 个好处:

  • 完整的路由 path 由编译生成,可以非常好的保障约定式路由落地
  • 生命周期方法由编译生成,减少项目中的模板代码,同样可以约束子应用的渲染和卸载按照预定的方式执行
  • 可以约束不规范的路由 path 定义,例如我们会禁用掉 :param 形式的动态路由

应用配置文件:src/app.json

{
"routes": [
{
"path": "/some/list",
"component": "./pages/list",
"description": "列表页"
},
{
"path": "/some/detail",
"component": "./pages/detail",
"description": "详情页"
}
]
}


上述示例最终会生成路由:/mars/demo/some/list/mars/demo/some/detail



webpack-loader 实现


解析 src/app.json 需要通过一个自定义的 webpack-loader 来实现,部分示例代码如下:

import path from 'path';
import qs from 'qs';

export default function marsAppLoader(source) {
const { appId } = qs.parse(this.resourceQuery.slice(1));
let config;
try {
config = JSON.parse(source);
} catch (err) {
this.emitError(err);
return;
}

const { routes = [] } = config;

const routePathSet = new Set();
const routeRuntimes = [];
const basename = `/mars/${appId}`;

for (let i = 0; i < routes.length; i++) {
const item = routes[i];
if (routePathSet.has(item.path.toLowerCase())) {
this.emitError(new Error(`重复定义的路由 path: ${item.path}`));
return;
}

routeRuntimes.push(
`routes[${i}] = { ` +
`path: ${JSON.stringify(basename + item.path)}, ` +
`component: _default(require(${JSON.stringify(item.component)})) ` +
`}`
);
routePathSet.add(item.path.toLowerCase());
}

return `
const React = require('react');
const ReactDOM = require('react-dom');

// 从 mars sdk 中引入 runtime 代码
const { __internals__ } = require('@guming/mars');
const { defineApp, _default } = __internals__;

const routes = new Array(${routeRuntimes.length});
${routeRuntimes.join('\n')}

// define mars app: ${appId}
defineApp({
appId: '${appId}',
routes,
});

`.trim();
}

src/app.json 作为编译入口并经过此 webpack-loader 编译之后,将自动编译关联的路由组件,创建子应用路由渲染模板,注册生命周期方法等,并最终输出 manifest.json 文件作为子应用的入口(类似 index.html),根据入口文件的内容就可以去加载入口的 js、css 资源并触发 mount 生命周期方法执行渲染逻辑。生成的 manifest.json 内容格式如下:

{
"js": [
"https://cdn.example.com/mars/demo/prod/app.a0dd6a27.js"
],
"css": [
"https://cdn.example.com/mars/demo/prod/app.230ff1ef.css"
]
}

聊聊沙箱隔离


一个好的沙箱隔离方案往往是市面上微前端框架最大的卖点,我们团队内也曾引入 qiankun 来解决子应用间隔离的痛点问题。而我想让大家回归到自己团队和业务里思考一下:“我们团队需要隔离?不做隔离有什么问题”。而我们团队给出的答案是:不隔离 JS,要隔离 CSS,理由如下:

  1. 不隔离 JS 可能会有什么问题:window 全局变量污染?能污染到哪儿去,最多也就内存泄露,对于现代 B 端应用来说,个别内容泄露几乎可以忽略不计;三方库不能混用版本?如文章开头所提及的,我们要做的第一件事就是统一,其中就包括统一常用三方库版本,在统一的前提下这种问题也就不存在了。当然也有例外情况,比如高德地图 sdk 在不同子系统需要隔离(使用了不同的 key),针对这种问题我们的策略就是专项解决;当然,最后的理由是一套非常好的 JS 隔离方案实现成本太高了,需要考虑太多的问题和场景,这些问题让我们意识到隔离 JS 带来的实际价值可能不太高。
  2. 由于 CSS 的作用域是全局的,所以非常容易造成子应用间的样式污染,其次,CSS 隔离是容易实现的,我们本身就基于编译做了很多约束的事情,同样也可以用于 CSS 隔离方案中。实现方案也非常简单,就是通过实现一个 postcss 插件,将子应用中引入的所有 css 样式都加上特有的作用域前缀,例如:
.red {
color: red;
}

将会编译成:

.mars__demo .red {
color: red;
}

当然,某些场景可能就是需要全局样式,如 antd 弹层内容默认就会在子应用内容区外,造成隔离后的样式失效。针对这种场景,我们的解法是用隔离白名单机制,使用也非常简单,在最前面加上 :global 选择器,编译就会直接跳过,示例:

:global {
.some-modal-cls {
font-size: 14px;
}
}

将会编译成:

.some-modal-cls {
font-size: 14px;
}

除此之外,在子应用卸载的时候,还会禁用掉子应用的 CSS 样式,这是如何做到的?首先,当加载资源的时候,会找到该资源的 CSSStyleSheet 对象:

const link = document.createElement('link');
link.setAttribute('href', this.url);
link.setAttribute('rel', 'stylesheet');
link.addEventListener('load', () => {
// 找到当前资源对应的 CSSStyleSheet 对象
const styleSheets = document.styleSheets;
for (let i = styleSheets.length - 1; i >= 0; i--) {
const sheet = styleSheets[i];
if (sheet.ownerNode === this.node) {
this.sheet = sheet;
break;
}
}
});

当卸载资源的时候,将该资源关联的 CSSStyleSheet 对象的 disabled 属性设置为 true 即可禁用样式:

if (this.sheet) {
this.sheet.disabled = true;
}

框架 SDK 设计




框架 SDK 按照使用场景可以归为 3 类,分别是:子应用、基座应用、编译器。同样的遵循我们的一贯原则,如果一个 API 可以满足诉求,就不会提供 2 个 API,尽可能保证团队内的代码风格都是统一的。例如:路由跳转 SDK 只提供唯一 API,并通过编译手段禁用掉其他路由跳转方式(如引入 react-router-dom 的 API 会报错):

import { mars } from '@guming/mars';

// 跳转路由:/mars/demo/some/detail?a=123
mars.navigate('/mars/demo/some/detail', {
params: { a: '123' }
});

// 获取路由参数
const { pathname, params } = mars.getLocation();
// pathname: /mars/demo/some/detail
// params: { a: '123' }

当然,我们也会根据实际情况提供一些便利的 API,例如:跳转路由要写完整的 /mars/[appId] 路由前缀太繁琐,所以我们提供了一个语法糖来减少样板代码,在路由最前面使用 : 来代替 /mars/[appId] 前缀(仅在当前子应用内跳转有效):

import { mars } from '@guming/mars';

// 跳转路由:/mars/demo/some/detail?a=123
mars.navigate(':/some/detail', {
params: { a: '123' }
});

另外,值得一提的是在基座应用上使用的一个 API bootstrap(),得益于这个 API 的设计,我们可以快速创建多个基座应用(不同域名),还能使用这个 API 在本地开发的时候启动一个基座来模拟本地开发环境,提升开发体验。


本地开发体验


开发模拟器


为更好的支持本地开发环境,我们提供了一套本地开发模拟器,在子应用启动本地服务的时候,会自动启动一个模拟的基座应用,拥有和真实基座应用几乎一样的布局,运行环境,集成的登录逻辑等。除此之外,开发模拟器还提供了辅助开发的「debug 小组件」,比如通过 debug 工具可以动态修改本地开发代理规则,保存之后立即生效。




IDE 支持


为了提升开发体验,我们分别开发了 Webstorm 插件 和 VSCode 插件服务于 Mars 应用,目前支持路由组件配置的路径补全、点击跳转、配置校验等功能。此外,我们会为配置文件提供 json schema,配置后将会获得 IDE 的自动补全能力和配置校验能力。




历史项目迁移


技术架构演进对于业务项目来说最大的问题在于,如何完成历史项目向新架构的迁移改造?我们团队投入 2 个人花了 2 个月时间将 12 个历史项目全部迁移至最新的架构上,这里分享一些我们在迁移过程中的经验。


定目标


首先要确定历史项目迁移这件事情一定是要做的,这是毋庸置疑的,而且要越快越好。所以我们要做的第一件事就是制定迁移计划,最后确定投入 2 个人力,大约花 2 个月时间将历史项目全部迁移至新的架构。第二件事情就是确定改造的范围(确定边界,不做非目标范围内的改造),对于我们的业务现状来说,主要包括:

  • 统一 reactreact-dom 版本为 17.0.2
  • 统一 antd 版本为 4.24.8
  • 统一路由
  • 统一接入 request 请求库
  • 统一接入工程化体系
  • 统一环境变量

梳理 SOP


因为迁移的流程不算简单,迁移要做的事情还挺多的,所以接下来要做的一件事就是梳理迁移流程 SOP,SOP 文档要细化到每种可能的场景,以及遇到问题对应的解法,让后续项目的迁移可以傻瓜式的按照标准流程去操作即可。我们的做法是,先以一个项目作为试点,一边迁移一边梳理 SOP,如果在迁移其他项目中发现有遗漏的场景,再持续补充这份 SOP 文档。


例如:之前项目中使用了 dva 框架,但是它的 routermodel 是耦合的,这样就无法使用我们制定的统一路由方案,对此我们的解法是,通过 hack dva 的源代码,将 model 前置注入到应用中,完成与路由的解耦。


上线方案


由于业务迭代频繁,所以我们代码改造持续的时间不能太长,否则要多次合并代码冲突,而我们的经验就是,项目改造从拉分支到发布上线,要在 1 周内完成。当然,整个上线过程还遇到许多需要解决的问题,比如在测试参与较少的情况下如何保障代码质量,包括:业务回归的策略,回滚策略,信息同步等等。


总结


之前看到 Umi 4 设计思路文字稿 里面有句话我觉得特别有道理:“社区要开放,团队要约束”,我们团队也在努力践行“团队约束”这一原则,因为它为团队带来的收益是非常高的。


没有最完美的方案,只有最适合自己的方案,以上这套架构方案只是基于当下古茗前端团队现状做的选择后的结果,可能并不适合每个团队,希望本文的这些思考和技术沉淀能对您有所帮助和启发。


最后


关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~


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

往往排查很久的问题,最后发现都非常简单。。。

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。 大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommi...
继续阅读 »

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。


大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommitException,并且集中在灰度机器上。

E20:21:59.770 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR [Consumer clientId=xx-xx.4-0, groupId=xx-xx-consumer_[gray]] Offset commit with offsets {xx-xx-xx-callback-1=OffsetAndMetadata{offset=181894918, leaderEpoch=4, metadata=''}, xx-xx-xx-callback-0=OffsetAndMetadata{offset=181909228, leaderEpoch=5, metadata=''}} failed org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: Failed to send request after 30000 ms.


排查


检查了这个 Topic 的流量流入、流出情况,发现并不是很高,至少和 QA 环境的压测流量对比,连零头都没有达到。


但是从发生异常的这个 Topic 的历史流量来看的话,发生问题的那几个时间点的流量又确实比平时高出了很多。



同时我们检查 Broker 集群的负载情况,发现那几个时间点的 CPU 负载也比平时也高出很多(也只是比平时高,整体并不算高)。



对Broker集群的日志排查,也没发现什么特殊的地方。


然后我们对这个应用在QA上进行了模拟,尝试复现,遗憾的是,尽管我们在QA上把生产流量放大到很多倍并尝试了多次,问题还是没能出现。


此时,我们把问题归于当时的网络环境,这个结论在当时其实是站不住脚的,如果那个时刻网络环境发生了抖动的话,其它应用为什么没有这类异常?


可能其它的服务实例网络情况是好的,只是发生问题的这个灰实例网络发生了问题。


那问题又来了,为什么这个实例的其它 Topic 没有报出异常,偏偏问题只出现在这个 Topic 呢?。。。。。。。。。


至此,陷入了僵局,无从下手的感觉。


从这个客户端的开发、测试到压测,如果有 bug 的话,不可能躲过前面那么多环节,偏偏爆发在了生产环境。


没办法了,我们再次进行了一次灰度发布,如果过了一夜没有事情发生,我们就把问题划分到环境问题,如果再次出现问题的话,那就只能把问题划分到我们实现的 Kafka 客户端的问题了。


果不其然,发布后的第二天凌晨1点多,又出现了大量的 RetriableCommitFailedException,只是这次换了个 Topic,并且异常的原因又多出了其它Caused by 。

org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.DisconnectException
...
...
E16:23:31.640 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR 
...
...
org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: The request timed out.

分析


这次出现的异常与之前异常的不同之处在于:

  1. 1. Topic 变了
  2. 2. 异常Cause变了

而与之前异常又有相同之处:

  1. 1. 只发生在灰度消费者组
  2. 2. 都是RetriableCommitFailedException

RetriableCommitFailedException 意思很明确了,可以重试提交的异常,网上搜了一圈后仅发现StackOverFlow上有一问题描述和我们的现象相似度很高,遗憾的是没人回复这个问题:StackOverFlow。


我们看下 RetriableCommitFailedException 这个异常和产生这个异常的调用层级关系。



除了产生异常的具体 Cause 不同,剩下的都是让我们再 retry,You should retry Commiting the lastest consumed offsets。



从调用层级上来看,我们可以得到几个关键的信息,commit 、 async。


再结合异常发生的实例,我们可以得到有用关键信息: 灰度、commit 、async。


在灰度消息的实现上,我们确实存在着管理位移和手动提交的实现。



看代码的第 62 行,如果当前批次消息经过 filter 的过滤后一条消息都不符合当前实例消费,那么我们就把当前批次进行手动异步提交位移。结合我们在生产的实际情况,在灰度实例上我们确实会把所有的消息都过滤掉,并异步提交位移。


为什么我们封装的客户端提交就会报大量的报错,而使用 spring-kafka 的没有呢?


我们看下Spring对提交位移这块的核心实现逻辑。



可以同步,也可以异步提交,具体那种提交方式就要看 this.containerProperties.isSyncCommits() 这个属性的配置了,然而我们一般也不会去配置这个东西,大部分都是在使用默认配置。



人家默认使用的是同步提交方式,而我们使用的是异步方式。


同步提交和异步提交有什么区别么?


先看下同步提交的实现:



只要遇到了不是不可恢复的异常外,在 timer 参数过期时间范围内重试到成功(这个方法的描述感觉不是很严谨的样子)。



我们在看下异步提交方式的核心实现:



我们不要被第 645 行的 RequestFuture future = sendOffsetCommitRequest(offsets) 所迷惑,它其实并不是发送位移提交的请求,它内部只是把当前请求包装好,放到 private final UnsentRequests unsent = new UnsentRequests(); 这个属性中,同时唤醒真正的发送线程来发送的。



这里不是重点,重点是如果我们的异步提交发生了异常,它只是简单的使用 RetriableCommitFailedException 给我们包装了一层。


重试呢?为什么异步发送产生了可重试异常它不给我们自动重试?


如果我们对多个异步提交进行重试的话,很大可能会导致位移覆盖,从而引发重复消费的问题。


正好,我们遇到的所有异常都是 RetriableCommitException 类型的,也就是说,我们把灰度位移提交的方式修改成同步可重试的提交方式,就可以解决我们遇到的问题了。


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

苹果的产品经理设计的App Clip是有意为之,还是必然趋势,详解 App Clip技术之谜

iOS
苹果在 WWDC2020 上发布了 App Clip,有媒体叫做“苹果小程序”。虽然 Clip 在产品理念上和小程序有相似之处,但是在技术实现层面却是截然不同的东西。本文会针对 Clip 的技术层面做全面的介绍。 实现方式:native 代码、native 框...
继续阅读 »

苹果在 WWDC2020 上发布了 App Clip,有媒体叫做“苹果小程序”。虽然 Clip 在产品理念上和小程序有相似之处,但是在技术实现层面却是截然不同的东西。本文会针对 Clip 的技术层面做全面的介绍。


实现方式:native 代码、native 框架、native app 一样的分发


在实现上,Clip 和原生的 app 使用一样的方式。在 UI 框架上同时支持 UIKit 和 SwiftUI,有些开发者认为只能使用 SwiftUI 开发,这点是错误的。Clip 的定位和 watch app、app extension 类似,和 app 在同一个 project 里,是一个单独的 target。只是 Clip 并没有自己的专属 framework(其实有一个,但是主要包含的是一些特色 api),使用的框架和 app 一致,可以认为是一个精简版的原生 App。




Clip 不能单独发布,必须关联一个 app。因此发布的流程和 app 和一样的,在 apple connect 上创建一个版本,和 app 一起提交审核。和 app 在技术上的最大区别只是大小限制在 10MB 以内,因为 Clip 的基础就是希望用户可以最迅速的被用户使用,如果体积大了就失去了产品的根本。


产品定位:用完即走




苹果对 Clip 的使用场景非常明确:在一个特定的情境里,用户可以快速的使用 app 的核心服务。是小程序内味了!


坦率的说,很难说 Clip 的理念是苹果原创的,在产品的定位上和微信小程序如出一辙。尤其是微信小程序在国内已经完全普及了,微信小程序初始发布的时候也被苹果加了多条限制。其中一条就是小程序不能有虚拟商品支付功能。现在回头看苹果自己的 Clip 可以完美支持 apple pay,很难说苹果没有私心。


触手可及




Clip 使用一段 URL 标识自己,格式遵从 universal link。因为苹果对 Clip 的使用场景非常明确,所以在 Clip 的调起方式做了严格限制。Clip 的调用只能是用户主动要发起才能访问,所以不存在用户在某个 app 里不小心点了一个按钮,就跳转下载了 Clip。


Clip 的发起入口有以下几种:

  • NFC
  • 二维码
  • Safari 中关联了 Clip 的网页
  • 苹果消息应用
  • Siri 附近建议和苹果地图

NFC 和二维码的入口很容易理解,必须用户主动拿出手机靠近 NFC、打开相机扫描。苹果专属的 Clip 码生成工具在年底才会开放。




Safari 中发起和之前的 universal link 类似,在网站配置了关联的 Clip 信息后,会有一个 banner 提示打开应用。




因为 Clip 提交 app store 审核的信息里也会配置好相关的 url,因此如果在 message 里发了 clip 的链接,操作系统也会在应用里生成一个 Clip 的卡片,用户如果需要可以主动点击。




Siri 附近建议和苹果地图(在 connect 中可以配置 clip 的地理位置)。场景和前面的二维码类似,如果我在地图上看到一个商家,商家有提供服务的 Clip,我可以在地图或者 Siri 建议里直接打开 Clip。




再次总结一下 Clip 的入口限制:只能是用户主动发起才能访问。虽然 Clip 的入口是一段 universal link,在代码里的处理方式也和 universal link 一致,但是为了 Clip 不被滥用,Clip 的调起只能是操作系统调起。App 没有能力主动调起一个 Clip 程序。


无需安装、卸载


因为 Clip 的大小被限制在了 10MB 以下,在当下的网络状态下,可以实现快速的打开。为了给用户使用非常轻松的感觉,在 UI 上不会体现“安装”这样的字眼,而是直接“打开”。预期的场景下用户打开 Clip 和打开一个网页类似。因此在用户的视角里就不存在软件的安装、卸载。


Clip 的生命周期由操作系统全权接管。如果 Clip 用户一段时间后没有使用,操作系统就会自动清除掉 Clip,Clip 里存储的数据也会被一并清除。因此虽然 Clip 提供了存储的能力,但是程序不应该依赖存储的数据,只能把存储当做 cache 来使用,操作系统可能自动清除缓存的数据。


横向比较:PWA、Instant Apps、小程序


Instant Apps


18 年正式发布的 Android Instant apps 和 Clip 在技术上是最接近的。Instant apps 中文被翻成“免安装应用”,在体验上也是希望用户能够最低成本的使用上 app,让用户感受不到安装这个步骤。Instant apps 也可以通过 url 标识(deep link),如果在 chrome 里搜索到应用的网站,chrome 如果识别到域名下有关联应用,可以直接“打开”。消息中的链接也可以被识别。只是 Instant apps 发布的早,国外用户也没有使用二维码的习惯,所以入口上不支持二维码、NFC。


两者的根本区别还是在定位上,Instant apps 提出的场景是提供一个 app 的试用版。因此场景是你已经到了 app 的下载页面,这个时候如果一个 app 几百兆你可能就放弃下载了,但是有一个极简的试用版,就会提高你使用 app 的可能。这个场景在游戏 app 里尤其明显,一方面高质量的游戏 app 体积比较大。另一方面,如果是一个付费下载的应用,如果有一个免费的试用版,也可以增加用户的下载可能。在苹果生态里很多应用会提供一个受限的免费 lite 版本也是一样的需求。


但是 Instant apps 在国内没有产生任何影响。因为政策的原因,Google Play 不支持在国内市场使用。国内的安卓应用市场也是鱼龙混杂,对于 Instant apps 也估计也没有统一支持。另外国内的安卓生态也和欧美地区区别比较大,早期安卓市场上收费的应用很少,对于用户而言需要试用免费 app 的场景很少。另外大厂也可能会推出专门的急速版应用,安装后利用动态化技术下发代码,应用体积也可以控制在 10 MB 以内。


Clip 则是非常明确的面向线下提供服务的场景,在应用能力上可以接入 sign in with apple,apple pay。这样一个全新的用户,可以很快速的使用线下服务并且进行注册、支付。用户体验会好的多。安卓因为国内生态的原因,各个安卓厂商没有统一的新用户可以快速注册的接口,也没有统一的支付接口,很难提供相匹敌的体验。如果开发者针对各个厂商单独开发,那成本上就不是“小程序”了。


Progressive Web App(PWA)




Progressive Web App 是基于 web 的技术。在移动互联网兴起之后,大家的流量都转移到了移动设备上。然而在移动上的 web 体验并不好。于是 W3C 和谷歌就基于浏览器的能力,制定了一套协议,让 web app 可以拥有更多的 native 能力。


PWA 不是特指某一项技术,而是应用了多项技术的 Web App。其核心技术包括 App Manifest、Service Worker、Web Push。


PWA 相当于把小程序里的代码直接下载到了本地,有了独立的 app 入口。运行的时候基于浏览器的能力。但是对于用户感受和原生 app 一样。


我个人对 PWA 技术很有好感,它的初衷有着初代互联网般的美好。希望底层有一套协议后,用户体验还是没有边界的互联网。然而时代已经变了。PWA 在中国基本上是凉了。


PWA 从出生就带了硬伤,虽然谷歌希望有一套 web 标准可以运行在移动设备上,但是对于苹果的商业策略而言,这并不重要。因此 PWA 的一个协议,从制定出来,再到移动设备(iOS)上支持这个特性,几年就过去了。而且对于移动用户而言,可以拥有一个美好的 web app 并不是他们的痛点。


总结起来 PWA 看着美好,但似乎更多是对于 web 开发者心中的美好愿景。在落实中遇到了很多现实的问题,技术支持的不好,开发者就更没有动力在这个技术上做软件生态了。


微信小程序


前面提过在产品理念上小程序和 Clip 很相似,甚至说不定 Clip 是受了小程序的启发。在市场上,小程序是 Clip 的真正对手。


小程序基于微信的 app,Clip 基于操作系统,因此在能力上 Clip 有优势。小程序的入口需要先打开微信,而 Clip 可以通过 NFC 靠近直接激活应用。对于开发者而言,Clip 可以直接获得很多原生的能力(比如 push),如果用户喜欢可以关联下载自己的原生应用。在小程序中,微信出于商业原因开发者不能直接跳转到自有 app,小程序的能力也依赖于微信提供的接口。


对于从 Clip 关联主 app 苹果还挺重视的,提供了几个入口展示关联 app。


首先在 clip 的展示页就会显示:




每次使用 Clip 时也会有一个短暂的浮层展示:




开发者也可以自己通过 SKOverlay 来展示:




不过如果开发者没有自己的独立 app,那么也就只能选择小程序了。小程序发展到现在场景也比最早提出的线下服务更加多了,反而类似 Instant apps,更像一个轻量级的 app。


考虑到国内很多小程序的厂商都没有自己的独立 app,因此 clip 对于这部分群体也并没有什么吸引力。不过对于线下服务类,尤其有支付场景的,Clip 在用户体验上会比小程序好一些。


总结,Clip 的业务场景和小程序有一小部分是重叠的,小程序覆盖的场景还是更多一些。两者在大部分时候并不是互斥式的竞争关系,即便在一些场景下 Clip 有技术优势,商家也不会放弃小程序,因为还有安卓用户嘛。还是看商家在某些场景里,是否愿意为用户多提供一种更好的交互方式。


对比原生 app 的技术限制


虽然 Clip 可以直接使用 iOS framework,但是因为 Clip 的使用场景是新用户的初次、简短、当下(in-the-moment experience)的使用,相比原生 app 苹果还是进行了一些限制。


App 不能访问用户的隐私信息:

  • 运动和健身数据
  • Apple Music 和多媒体文件
  • 通讯录、信息、照片、文件等数据

不过为了能够提供给用户更加轻便的体验,通过专门为 Clip 设计了免申请的通知、定位权限。不过也有限制:免申请的通知只在 8 个小时内有效。位置只能获取一次。如果 app 需要重度使用这两类权限就还是和原来一样,可以弹窗申请。


某些高级应用能力也会受限,需要在完整的应用中才能使用:

  • 不能请求追踪授权
  • 不能进行后台请求任务
  • 没在激活状态蓝牙连接会断开

总的而言虽然有一些限制,但是这些限制的出发点是希望开发者关注 Clip 的正确使用场景。对于 Clip 所提倡的使用场景里,苹果提供的能力是完全够用的。


一些技术细节


可以建立一个共享 targets 的 Asset catalog 来共用图片资源。




在 Clip 中申请的授权,在下载完整应用后会被同步到应用中。


通过 App Group Container 来共享 clip 和 app 的数据。




image


Clip 的 url 可以配置参数:




在 App Store connect 中还可以针对指定的参数配置不一样的标题和图片。比如一家连锁咖啡店,可能不同的店你希望弹出的标题图片是不一样的,可以进行单独的配置。




总结


苹果给定义的 Clip 的关键词是:lightweight、native、fast、focused、in-the-moment experience。


Clip 在特定的线下场景里有着相当好的用户体验。对于已经拥有独立 app 的公司来说,开发一个 clip 应用的成本并不高。我个人还是期待这样一个好的技术可以被更多开发者接纳,可以提供给用户更好的体验。对于小程序,clip 的场景窄的多,两者并不是直接竞争关系。我更愿意看做是特定场景下,对于小程序原生能力不足的一种补充。


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

仿微信列表左滑删除、置顶。。

仿微信消息列表 前言 最近自己在利用空闲时间开发一个APP,目的是为了巩固所学的知识并扩展新知,加强对代码的理解扩展能力。消息模块是参照微信做的,一开始并没有准备做滑动删除的功能,觉得删除嘛,后面加个长按的监听不就行了,但是对于有些强迫症的我来说,是不大满意这...
继续阅读 »

仿微信消息列表


前言


最近自己在利用空闲时间开发一个APP,目的是为了巩固所学的知识并扩展新知,加强对代码的理解扩展能力。消息模块是参照微信做的,一开始并没有准备做滑动删除的功能,觉得删除嘛,后面加个长按的监听不就行了,但是对于有些强迫症的我来说,是不大满意这种解决方法的,但由于我对自定义view的了解还是比较少,而且之前也没有做过,所以就作罢。上周看了任玉刚老师的《Android开发艺术探索》中的View事件体系章节,提起了兴趣,就想着试一试吧,反正弄不成功也没关系。最后弄成了,但还是有些小瑕疵(在6、问题中),希望大佬能够指教一二。话不多说,放上一张动图演示下:


messlist.gif


1、典型的事件类型


在附上源码之前,想先向大家介绍下事件类型,在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:



  • ACTION_DOWN ---- 手指刚接触屏幕

  • ACTION_MOVE ---- 手指在屏幕上移动

  • ACTION_UP ---- 手指刚离开屏幕


正常情况下、一次手指触摸屏幕的行为会触发一系列点击事件:



  • 点击屏幕后松开,事件序列为DOWN -> UP

  • 点击屏幕滑动后松开,事件序列为DOWN -> MOVE -> ... -> MOVE -> UP


2、Scroller


Scroller - 弹性滑动对象,用于实现View的弹性滑动。
当使用View的scrollTo/scrollBy方法来实现滑动时,其过程是在瞬间完成的,这个过程没有过渡效果,用户体验感较差,这个时候就可以使用Scroller来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一定时间间隔内完成的。


3、View的滑动


Android手机由于屏幕较小,为了给用户呈现更多的内容,就需要使用滑动来显示和隐藏一些内容,不管滑动效果多么绚丽,它们都是由不同的滑动外加特效实现的。View的滑动可以通过三种方式实现:



  • scrollTo/scrollBy:操作简单,适合对View内容的滑动。

  • 修改布局参数:操作稍微复杂,适合有交互的View。

  • 动画:操作简单,适合没有交互的View和实现复杂的动画效果。


3.1、scrollTo/scrollBy


为了实现View的滑动,View提供了专门的方法来实现这一功能,也就是scrollTo/scrollBy。是基于所传参数的绝对滑动。


3.2、修改布局参数


即改变LayoutParams,比如想把一个布局向右平移100px,只需要将该布局LayoutParams中的marginLeft参数值增加100px即可。或者在该布局左边放入一个默认宽度为0px的空View,当需要向右平移时,重新设置空View的宽度就OK了。


3.3、动画


动画和Scroller一样具有过渡效果,View动画是对View的影像做操作,并不能真正改变View的位置,单击新位置无法触发onClick事件,在这篇文章中并没有使用到,所以不再赘叙了。


4、布局文件


<?xml version="1.0" encoding="utf-8"?>
### <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:widget="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<com.example.myapplication.view.ScrollerLinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<RelativeLayout
android:id="@+id/friend_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="10dp">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/friend_icon"
android:layout_width="45dp"
android:layout_height="45dp"
android:src="@mipmap/touxiang"
app:riv_corner_radius="5dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginLeft="12dp"
android:gravity="center_vertical"
android:orientation="vertical">

<TextView
android:id="@+id/friend_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="@color/black"
android:textSize="15dp"
tools:text="好友名" />

<TextView
android:id="@+id/friend_last_mess"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:layout_marginEnd="18dp"
android:singleLine="true"
android:textColor="@color/color_dbdbdb"
android:textSize="12dp"
tools:text="最后一条信息内容" />
</LinearLayout>

</LinearLayout>

<TextView
android:id="@+id/last_mess_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginTop="5dp"
android:singleLine="true"
android:textColor="@color/color_dbdbdb"
android:textSize="11dp"
tools:text="时间" />
</RelativeLayout>

<LinearLayout
android:layout_width="240dp"
android:layout_height="match_parent"
android:orientation="horizontal">

<Button
android:id="@+id/unread_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:background="@color/color_theme"
android:gravity="center"
android:text="标为未读"
android:textColor="@color/color_FFFFFF" />

<Button
android:id="@+id/top_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:background="@color/color_orange"
android:gravity="center"
android:text="置顶"
android:textColor="@color/color_FFFFFF" />

<Button
android:id="@+id/delete_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:background="@color/color_red"
android:gravity="center"
android:text="删除"
android:textColor="@color/color_FFFFFF" />
</LinearLayout>

</com.example.myapplication.view.ScrollerLinearLayout>

<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_alignParentBottom="true"
android:layout_marginLeft="60dp"
android:layout_marginRight="3dp"
android:background="@color/color_e7e7e7" />

</LinearLayout>

ScrollerLinearLayout布局最多包含两个子布局(默认是这样,后面可能还会修改成自定义),一个是展示在用户面前充满屏幕宽度的布局,一个是待展开的布局,在该xml布局中,ScrollerLinearLayout布局包含了一个RelativeLayout和一个LinearLayoutLinearLayout中包含了三个按钮,分别是删除、置顶、标为未读。


5、自定义View-ScrollerLinearLayout


/**
* @Copyright : China Telecom Quantum Technology Co.,Ltd
* @ProjectName : My Application
* @Package : com.example.myapplication.view
* @ClassName : ScrollerLinearLayout
* @Description : 文件描述
* @Author : yulu
* @CreateDate : 2023/8/17 17:05
* @UpdateUser : yulu
* @UpdateDate : 2023/8/17 17:05
* @UpdateRemark : 更新说明
*/

class ScrollerLinearLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) :
LinearLayout(context, attrs, defStyleAttr) {

private val mScroller = Scroller(context) // 用于实现View的弹性滑动
private val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var mVelocityTracker: VelocityTracker? = null // 速度追踪
private var intercept = false // 拦截状态 初始值为不拦截
private var lastX: Float = 0f
private var lastY: Float = 0f // 用来记录手指按下的初始坐标
var expandWidth = 720 // View待展开的布局宽度 需要手动设置 3*dp
private var expandState = false // View的展开状态
private val displayWidth =
context.applicationContext.resources.displayMetrics.widthPixels // 屏幕宽度
private var state = true


override fun onTouchEvent(event: MotionEvent): Boolean {
Log.e(TAG, "onTouchEvent $event")
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (!expandState) {
state = false
}
}
else -> {
state = true
}
}
return state
}


override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.e(TAG, "onInterceptTouchEvent Result : ${onInterceptTouchEvent(ev)}")
Log.e(TAG, "dispatchTouchEvent : $ev")
mVelocityTracker = VelocityTracker.obtain()
mVelocityTracker!!.addMovement(ev)
return super.dispatchTouchEvent(ev)
}

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
Log.e(TAG, "onInterceptTouchEvent $ev")
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
lastX = ev.rawX
lastY = ev.rawY
// 处于展开状态且点击的位置不在扩展布局中 拦截点击事件
intercept = expandState && ev.x < (displayWidth - expandWidth)
}
MotionEvent.ACTION_MOVE -> {
// 当滑动的距离超过10 拦截点击事件
intercept = lastX - ev.x > 10
moveWithFinger(ev)
}
MotionEvent.ACTION_UP -> {
// 判断滑动距离是否超过布局的1/2
chargeToRightPlace(ev)
intercept = false
}
MotionEvent.ACTION_CANCEL -> {
chargeToRightPlace(ev)
intercept = false
}
else -> intercept = false
}
return intercept
}

/**
* 将布局修正到正确的位置
*/

private fun chargeToRightPlace(ev: MotionEvent) {
val eventX = ev.x - lastX

Log.e(TAG, "该事件滑动的水平距离 $eventX")
if (eventX < -(expandWidth / 4)) {
smoothScrollTo(expandWidth, 0)
expandState = true
invalidate()
} else {
expandState = false
smoothScrollTo(0, 0)
invalidate()
}

// 回收内存
mVelocityTracker?.apply {
clear()
recycle()
}
//清除状态
lastX = 0f
invalidate()
}

/**
* 跟随手指移动
*/

private fun moveWithFinger(event: MotionEvent) {
//获得手指在水平方向上的坐标变化
// 需要滑动的像素
val mX = lastX - event.x
if (mX > 0 && mX < expandWidth) {
scrollTo(mX.toInt(), 0)
}
// 获取当前水平方向的滑动速度
mVelocityTracker!!.computeCurrentVelocity(500)
val xVelocity = mVelocityTracker!!.xVelocity.toInt()
invalidate()

}

/**
* 缓慢滚动到指定位置
*/

private fun smoothScrollTo(destX: Int, destY: Int) {
val delta = destX - scrollX
// 在多少ms内滑向destX
mScroller.startScroll(scrollX, 0, delta, 0, 600)
invalidate()
translationY = 0f
}

// 流畅地滑动
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.currX, mScroller.currY);
postInvalidate()
}
}

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
expandWidth = childViewWidth()
invalidate()
super.onLayout(changed, l, t, r, b)
}

/**
* 最多只允许有两个子布局
*/

private fun childViewWidth(): Int {
Log.e(TAG, "childCount ${this.childCount}")
return if (this.childCount > 1) {
val expandChild = this.getChildAt(1) as LinearLayout
if (expandChild.measuredWidth != 0){
expandWidth = expandChild.measuredWidth
}
Log.e(TAG, "expandWidth $expandWidth")
expandWidth
} else
0
}

companion object {
const val TAG = "ScrollerLinearLayout_YOLO"
}
}

思路比较简单,就是在ACTION_DOWN时记录初始的横坐标,在ACTION_MOVE中判断是否需要拦截该事件,
当滑动的距离超过10,拦截该点击事件,防止不必要的点击。并且View跟随手指移动。在ACTION_UPACTION_CANCEL中将布局修正到正确的位置,主要是根据滑动的距离来判断是否要展开并记录展开的状态。在ACTION_DOWN中判断是否处于展开状态,如果在展开状态且点击的位置不在扩展布局中,拦截点击事件,防止不必要的点击。


6、问题


自定义布局中的expandWidth参数在childViewWidth()方法和onLayout()方法中都赋值了一次,在onLayout()方法中查看日志expandWidth是有值的,可是在moveWithFinger()方法中打日志查看得到的expandWidth参数值仍然是0,导致无法正常滑动。去到其他的页面再返回到消息界面就可以正常滑动了,再次查看日志参数也有值了,这个问题不知道如何解决,所以需要手动设置expandWidth的值。


7、小结


初步的和自定义View认识了,小试牛刀,自己还是很满意这个学习成果的。希望在接下来的学习中不要因为没有接触过而放弃学习,勇于迈出第一步。文章若出现错误,欢迎各位批评指正,写文不易,转载请注明出处谢谢。


作者:遨游在代码海洋的鱼
来源:juejin.cn/post/7269590511095054395
收起阅读 »

刚咬了一口馒头,服务器突然炸了!

首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。 其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket...
继续阅读 »

首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。
其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket管理器。


看着群里一群“小可爱”疯狂乱叫,我被吵的头都炸了,赶紧尝试定位问题。



  1. 查看是否存在Jenkins发版 -> 无

  2. 查看最新提交记录 -> 最后一次提交是下午,到晚上这段时间系统一直是稳定的

  3. 查看服务器资源,htop后,发现三台相关的云服务器资源都出现闲置状态

  4. 查看PolarDB后,既MySQL,连接池正常、吞吐量和锁正常

  5. 查看Redis,资源正常,无异常key

  6. 查看前端控制台,出现一些报错,但是这些报错经常会变化


  7. 查看前端测试环境、后端测试环境,程序全部正常

  8. 重启前端服务、后端服务、NGINX服务,好像没用,过了5分钟后,咦,好像可以访问了


就在我们组里的“小可爱”通知系统恢复正常后,20分钟不到,再一次处于无法打开的状态,沃焯!!!
完蛋了,找不出问题在哪里了,实在想不通问题究竟出在哪里。


我不服啊,我不理解啊!


咦,Nginx?对呀,我瞅瞅访问日志,于是我浏览了一下access.log,看起来貌似没什么问题,不过存在大量不同浏览器的访问记录,刷的非常快。


再瞅瞅error.log,好像哪里不太对


2023/04/20 23:15:35 [alert] 3348512#3348512: 768 worker_connections are not enough
2023/04/20 23:33:33 [alert] 3349854#3349854: *3492 open socket #735 left in connection 1013

这是什么?貌似是连接池问题,赶紧打开nginx.conf看一下配置


events {
worker_connections 666;
# multi_accept on;
}

???


运维小可爱,你特么在跟我开玩笑?虽然我们这个系统是给B端用的,还是我们自己组的不到100人,但是这连接池给的也太少了吧!


另外,前端为什么会开到 1000 个 WS 连接呢?后端为什么没有释放掉FD呢?


询问后,才知道,前端的Socket管理器,会在连接超时或者其它异常情况下,重新开启一个WS连接。


后端的心跳配置给了300秒


Constant::OPTION_OPEN_WEBSOCKET_PROTOCOL => true, // websocket 协议
Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => 150,
Constant::OPTION_HEARTBEAT_IDLE_TIME => 300,

此时修改nginx.conf的配置,直接拉满!!!


worker_connections 655350;

重启Nginx,哇绰,好像可以访问了,但是每当解决一个问题,总会产生新的问题。


此时error.log中出现了新的报错:


2023/04/20 23:23:41 [crit] 3349767#3349767: accept4() failed (24: Too many open files)

这种就不怕了,貌似和Linux的文件句柄限制有关系,印象中是1024个。
至少有方向去排查,不是吗?而且这已经算是常规问题了,我只需小小的百度一下,哼哼~


拉满拉满!!


worker_rlimit_nofile 65535;

此时再次重启Nginx服务,系统恢复稳定,查看当前连接数:


netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# 打印结果
TIME_WAIT 1175

FIN_WAIT1 52

SYN_RECV 1

FIN_WAIT2 9

ESTABLISHED 2033

经过多次查看,发现TIME_WAITESTABLISHED都在不断减少,最后完全降下来。


本次问题排查结束,问题得到了解决,但是关于socket的连接池管理器,仍然需要优化,及时释放socket无用的连接。


作者:兰陵笑笑生666
来源:juejin.cn/post/7224314619865923621
收起阅读 »

树形列表翻页,后端: 搞不了搞不了~~

web
背景 记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用...
继续阅读 »

背景


记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用户说一进到这个报告页面就卡住不动了,上去一查发现不得了,都是铁杆用户,每年作品都几百个,导致几年下来,这个报告返回了几千个作品,包含上千的图片。


问题分析


上千的图片,肯定会卡,首先想到的是做图片懒加载。这个很简单,使用一个vue的全局指令就可以了。但是上线发现,没啥用,dom节点多的时候,懒加载也卡。


然后就问服务端能不能支持分页,服务端说数据太散,连表太多,树形结构很难做分页。光查询出来就已经很费劲了。


没办法于是想了一下如何前端来处理掉。


思路




  1. 由于是app中的嵌入页面,首先考虑通过滚动进行分页加载。




  2. 一次性拿了全部的数据,肯定不能直接全部渲染,我们可以只渲染一部分,比如第一个节点,或者前几个节点。




  3. 随着滚动一个节点一个节点或者一批一批的渲染到dom中。




实现


本文仅展示一种基于vue的实现


1. 容器

设计一个可以进行滚动翻页的容器 然后绑定滚动方法OnPageScrolling



<style lang="less" scoped>

.study-backup {

overflow-x: hidden;

overflow-y: auto;

-webkit-overflow-scrolling: touch;

width: 100%;

height: 100%;

position: relative;

min-height: 100vh;

background: #f5f8fb;

box-sizing: border-box;

}

</style>

<template>

<section class="report" @scroll="OnPageScrolling($event)">

</section>

</template>



2.初始化数据

这里定义一下树形列表的数据结构,实现初始化渲染,可以渲染一个树节点,或者一个树节点的部分子节点



GetTreeData() {

treeapi

.GetTreeData({ ... })

.then((result) => {

// 处理结果

const data = Handle(result)

// 这里备份一份数据 不参与展示

this.backTreeList = data.map((item) => {

return {

id: item.id,

children: item.children

}

})

// 这里可以初始化为第一个树节点

const nextTree = this.backTreeList[0]

const nextTansformTree = nextTree.children.splice(0)

this.treeList = [{

id: nextTree.id,

children: nextTansformTree

}]

// 这里可以初始化为第一树节点 但是只渲染第一个子节点

const nextTree = this.backTreeList[0]

const nextTansformTree = nextTree.children.splice(0, 1)

this.treeList = [{

id: nextTree.id,

children: nextTansformTree

}]

})

},


3.滚动加载

这里通过不断的把 backTreeList 的子节点转存入 treeList来实现分页加载。



OnPageScrolling(event) {

const container = event.target

const scrollTop = container.scrollTop

const scrollHeight = container.scrollHeight

const clientHeight = container.clientHeight

// console.log(scrollTop, clientHeight, scrollHeight)

// 判断是否接近底部

if (scrollTop + clientHeight >= scrollHeight - 10) {

// 执行滚动到底部的操作

const currentReport = this.backTreeList[this.treeList.length - 1]

// 检测匹配的当前树节点 treeList的长度作为游标定位

if (currentReport) {

// 判断当前节点的子节点是否还存在 如果存在则转移到渲染树中

if (currentReport.children.length > 0) {

const transformMonth = currentReport.children.splice(0, 1)

this.treeList[this.treeList.length - 1].children.push(

transformMonth[0]

)

// 如果不存在 则寻找下一树节点进行复制 同时复制下一节点的第一个子节点 当然如果寻找不到下一树节点则终止翻页

} else if (this.treeList.length < this.backTreeList.length) {

const nextTree = this.backTreeList[this.treeList.length]

const nextTansformTree = nextTree.children.splice(0, 1)

this.treeList.push({

id: nextTree.id,

children: nextTansformTree

})

}

}

}

}


4. 逻辑细节

从上面代码可以看到,翻页的操作是树copy的操作,将备份树的子节点转移到渲染树中




  1. copy备份树的第一个节点到渲染树,同时将备份树的第一个节点的子节点的第一个节点转移到渲染树的第一个节点的子节点中




  2. 所谓转移操作,就是数组splice操作,从一颗树中删除,然后把删除的内容插入到另一颗树中




  3. 由于渲染树是从长度1开始的,所以我们可以根据渲染树的长度作为游标和备份树进行匹配,设渲染树的长度为当前游标




  4. 根据当前游标查询备份树,如果备份树的当前游标节点的子节点不为空,则进行转移




  5. 如果备份树的当前游标节点的子节点为空,则查找备份树的当前游标节点的下一节点,设为下一树节点




  6. 如果找到了备份树的当前游标节点的下一节点,扩展渲染树,将下一树节点复制到渲染树,同时将下一树节点的子节点的第一节点复制到渲染树




  7. 循环4-6,将备份树完全转移到渲染树,完成所有翻页




扩展思路


这个方法可以进行封装,将每次复制的节点数目和每次复制的子节点数目作为传参,一次可复制多个节点,这里就不做展开


作者:CodePlayer
来源:juejin.cn/post/7270503053358612520
收起阅读 »

创建一个可以循环滚动的文本,可能没这么简单。

web
如何创建一个可以向左循环滚动的文本? 创建如上图效果的滚动文本,你能想到几种方式? -------- 暂停阅读,不如你自己先试一下 -------- 方式一: 根据页面宽度,生成多个元素。每个元素通过js控制,每帧向左偏移一点像素。 如果偏移的元素不可见后...
继续阅读 »

如何创建一个可以向左循环滚动的文本?


loop.gif


创建如上图效果的滚动文本,你能想到几种方式?


-------- 暂停阅读,不如你自己先试一下 --------


方式一:


根据页面宽度,生成多个元素。每个元素通过js控制,每帧向左偏移一点像素。

如果偏移的元素不可见后,将此元素再移动到屏幕最右边。


此方式容易理解,实现起来也不困难,但是有性能上的风险,因为每一帧都在修改元素的位置。


方式二:


根据页面宽度,生成多个元素。每个元素通过js控制,通过setInterval每一秒向左偏移一些像素。

然后结合css的transition: all 1s linear;使得偏移更加顺滑。

如果偏移的元素不可见后,将此元素再移动到屏幕最右边。


使用此方法可以避免高频率计算元素位置,但是此方式控制起来更复杂,主要是因为,将元素移动到最右边的时候,也会触发transition ,需要额外逻辑控制在元素移到最右边的时候不触发transition

并且在实际开发中发现。当窗口不可见时候动画实际会暂停,还需要控制当窗口隐藏时候,暂停setInterval


方式三:


换一种思路。按顺序排列元素,多个子元素首位相接。将每个子元素通过animation: xxx 10s linear infinite;

从左到右移动。在一定范围内移动子元素,通过视觉错觉,像是整个大元素(盒子)都在移动。

此方式简单,并且无需JS,性能较好。


下面是完整代码(可以控制浏览器宽度,查看不同尺寸屏幕的效果)


<!doctype html>  
<html>
<head>
<title>LOOP</title>
<style>
@keyframes loop {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}

.box {
white-space: nowrap;
}

.scrollContent {
width: 600px;
display: inline-block;
text-align: center;
animation: loop 3s linear infinite;
}
</style>
</head>
<body>
<div class="box">
<div class="scrollContent">
这是一段可以滚动的文本
</div>
<div class="scrollContent">
这是一段可以滚动的文本
</div>
<div class="scrollContent">
这是一段可以滚动的文本
</div>
<div class="scrollContent">
这是一段可以滚动的文本
</div>
<div class="scrollContent">
这是一段可以滚动的文本
</div>
<div class="scrollContent">
这是一段可以滚动的文本
</div>
</div>
</body>

</html>


方式四:


方式三会创建多份一样的文本内容,你可能会说,屏幕上同时出现这么多文本元素,当然要创建这么多一样的内容。

其实还有一种性能更佳的方式:text-shadow: 600px 0 currentColor,通过此方式创建多份文本副本,达到类似效果。

此方法性能最佳。但是对非文本无能为力。


<!doctype html>  
<html>
<head>
<title>LOOP</title>
<style>
@keyframes loop {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}

.box {
white-space: nowrap;
}

.scrollContent {
color: rebeccapurple;
width: 600px;
display: inline-block;
text-align: center;
animation: loop 3s linear infinite;
text-shadow: 600px 0 currentColor, 1200px 0 currentColor, 1800px 0 currentColor, 2400px 0 currentColor;
}
</style>
</head>
<body>
<div class="box">
<div class="scrollContent">
这是一段可以滚动的文本
</div>
</div>
</body>

</html>

总结


方式1:应该是最直接想到的方式。但是出于对性能的担忧。

方式2:由于方式1性能优化得到,但是方式2过于复杂。
方式3: 看上去非常易于实现,实际很难想到。
方式4:如果对text-shadow和css颜色掌握不熟,根本难以实现。


希望对你有所启发


作者:wuwei123
来源:juejin.cn/post/7273026570930257932
收起阅读 »

笨功夫是普通人最后的依靠

今天早上看到一篇文章《笨功夫是普通人最后的依靠》,有感而发,文中说的内容都是自己现在的一些想法, 本想在下面评论一下,但是好像要说的太多了,评论写不下,也就有了本文。 背单词是学英语的 “笨功夫” 故事还得从差不多十几年前的初中说起,在我上小学的时候,所在的小...
继续阅读 »

今天早上看到一篇文章《笨功夫是普通人最后的依靠》,有感而发,文中说的内容都是自己现在的一些想法,
本想在下面评论一下,但是好像要说的太多了,评论写不下,也就有了本文。


背单词是学英语的 “笨功夫”


故事还得从差不多十几年前的初中说起,在我上小学的时候,所在的小学是还没有教英语的,所以我的英语是从初中开始学的。
还好初中上英语课的时候,老师依然是从 26 个英语字母教起的,只是会有种错觉,那些小学学过英语的同学好像不用怎么学就能在英语上获得好成绩。
但这种想法不太客观,因为在有这种想法的时候,潜意识中将他们小学几年的积累忽略了,所以多少会有点沮丧。


九边的文章中,提到了学习英语得先背单词,背单词就是学习英语中的 “笨功夫”。对于这点深有体会,虽然我的英语是初中开始学的,
而且我对语法可以说知之甚少,但是我在背单词上可是花了不少时间的,所以英语成绩也不至于太难看。
后来上了大学,也能凭借初高中的英语单词的积累,考过了四级。
然后做为一名程序员,日常开发的时候需要看到英文文档也可以比较流畅,当然肯定不如看中文顺畅。
说这些,并不是觉得这是什么光荣的事,只是想表达比较认可背单词是学习英语的 “笨功夫” 这一观点。


笨功夫之外,方法也重要


几年前看极客时间的《数据结构与算法之美》这门课的时候,提到了一点:



数据结构和算法就是一个非常难啃的硬骨头,可以说是计算机学科中最难学的学科之一了。
我当时学习也费了老大的劲,能做到讲给你听,我靠的也是十年如一的积累和坚持。如果没有基础、或者基础不好,你怎能期望看 2 个小时就能完全掌握呢?



九边的文章中也提到了自己的经历,想学写代码,然后一推人推荐学习《算法导论》。
对于这个,我个人也是深受其害,十几年前我一直徘徊在玩游戏和学习之间,常常觉得自己的时间不应该全部还在玩游戏上,怎么也得学习一下。
然后我就会去学习,我怎么学习呢?也是跟九边一样,找那些前辈们推荐的经典教材,比如算法、操作系统、编译原理、计算机网络相关的经典书籍,


依然记得我在高三的时候就买来了一本《编译原理》,也就是那本 “龙书”(因为封面是一条龙)。
但是,这本编译原理就算让现在的我去看估计也很难看懂,所以在学习方面,个人其实走了很多弯路,跌了不少跟头。
因为学习的方法不对,这种学习自然是持续不下去的,在这种学习过程中,需要耗费大量的心力,然后自我怀疑,最后放弃。


对于这一点,九边的文章中也提到了背后的根本原因:



  • 选错教材了,你拿得太难了,不适合你;

  • 投入时间不足。


这两点都是我当时存在的问题,一上来就选择了最难的教材来看,没有考虑到自身实力能不能看得懂。
当然,选择难的教材也不是不行,可能得投入足够的时间去把教材看懂,前提是,有途径可以解决学习过程中的遇到的问题,
比如遇到问题可以问问前辈,又或者像现在的 GPT,如果想借助百度这种东西可能大概率要失望。
跳过少数问题可能不影响学习的效果,但是如果绝大部分问题都没有能找到答案的方法,那换一本简单点的教材先学学基础是个更好的方法。


当然,说这些并不是为了鼓励大家去学习数据结构算法,只是想说,在我们学习遇到困难的时候,可能得考虑一下是不是上面两个原因导致的。


大脑对熟悉的东西更感兴趣


这句话已经不记得是从哪里看到的了,但是觉得颇有道理。
有时候,我们对某件事情不感兴趣是因为不擅长,而不是因为真的不擅长。
在进入一个相对新的领域的时候,我们会接触到很多新的名词、术语,会觉得特别费劲。
这个时候我们可能就会选择放弃了,当然,我们也可以选择坚持下去,
这些年我学到的一个非常重要的学习方法就是,对于新接触的东西,去多看、多想、多实践几遍,当然,这里说的是学习编程领域的东西。
很多东西,在我们这样几轮学习下来,好像其实也没有太难的东西,当然也有可能我学习的东西本身没有很难的东西。
但不得不承认,就算是那些你觉得不难的东西,很多人也潜意识中会觉得自己学不会,
但实际上只是他们投入的时间还不够多。


在开始的时候,我也会觉得枯燥无味,但是在熟悉多几遍之后,发现好像还挺有意思,尤其是使用新学到的东西解决了自己实际遇到的一些问题之后。
学习的过程大多如此吧,说到这,想起了几天前看到的《如何取得杰出成就》中提到的一点:



有一些工作,我们可能必须在自己讨厌的事情上努力工作数年,才能接近喜欢的部分,但这不是杰出成就产生的方式,
杰出的成就是通过持续关注自己真正感兴趣的事情来实现的 —— 当我们停下来盘点时,会惊讶于自己已经走了多远。



这篇文章是《黑客与画家》作者博客《How to Do Great Work》的翻译版,有兴趣可以看看,感觉还不赖。
在实际工作中,我们遇到的很多问题其实并不需要坚持数年才能解决,又不是研究什么前沿领域的东西,
但是对于一些难题需要花几天或者一两个星期去解决这种可能性还是很大的。
在这个过程我们会对问题域中的东西越来越熟悉,直到最后它们不再是我们的障碍。


问题是可以分解的


搜狐 CEO 张朝阳这几年在 B 站上更新了很多物理的教程,当然我是全都看不懂,只是想起他说过的一段话:



很多东西的话,就是你看起来很复杂,是因为你不熟悉,其实这个知识,
天下的这个知识和所有的东西,其实都是不难的,你把所有的再复杂的东西,把它分解成每一步的话,
他的基本的这个思维过程的,跟你早上吃什么饭,怎么做饭,怎么打车怎么点东西,都是一样的思维过程。
很多东西你理解不了,不是因为你笨或者是你不够聪明,而是因为你,你自己认为你理解不了是吧,
很多可能因为过去的经历啊,就是在课堂上这个回答不了问题啊,一些挫败的经历,考试的失败导致,
你就有一种恐惧,是一种恐惧和你的认为理解不了导致你理解不了。



虽然道理是这么个道理,但是不代表物理都没学过的人能看得懂他讲的物理课,
因为问题虽然可以分解,但是一个需要有深厚基础的问题恐怕你连分解都不知道怎么分解,更不要提解决。
就好像上文提到的《算法导论》这本书,里面有大量的数学推导,很多人看不懂是因为,
从作者的角度来说,他已经把问题分解为了若干个小问题,在他看来,看这本书的读者应该是已经掌握了他分解之后的问题的相关知识。
从推荐看这本书的人来看,他推荐的对象应该也掌握了书中那些分解之后的知识。
但是实际是,可能也有很多人没有考虑到自身实力,然后就去啃这些大部头,自然而然地看不懂。


很多时候我们遇到的问题都能找到恰当的分解方法,尤其是编程领域,要不然我们不大可能会碰到那个问题。
在摸爬滚打多年之后,我们会发现,很多那些入行时觉得困难的点最后都不是问题了,
这是因为,常见的问题我们基本都解决过一遍了,以致于我们再遇到同样的问题之后,就能马上想到应该怎么去做,就已经在心中有一二三四几个步骤了。
举一个例子,在学习做 web 应用的时候,其实很多东西都不懂,但是现在已经很清楚一个 web 应用大概应该是长什么样子的了:



  • 从浏览器发起的请求到达 web 应用之后,我们需要确定具体执行什么逻辑,因此需要有 “路由” 来将请求拍发给一个具体的方法,也就是某个 Controller 里面的一个方法。

  • 在请求的处理逻辑里面,我们可能需要去查询数据库,所有常用的 web 框架都提供了关于数据库查询的一些抽象,直接调用封装的那些方法即可。

  • 在返回的时候,我们要返回给客户端的实质上是纯文本的东西(HTTP 协议),但是 HTTP 相关的功能往往由 HTTP 服务器来处理的,比如 nginx

  • nginx 处理 HTTP 相关的东西,比如反向代理的 upstream 返回的数据长度有多长,需要算出来,将这个长度写入到 HTTP 头中,这样浏览器收到 HTTP 报文的时候才能准确地解析出一个 HTTP 报文包


弄清楚这些问题之后,不管换哪一种语言,我们都可以拿来实现一个 web 应用,无非就是解析 HTTP 报文,在代码里面做一些业务逻辑处理,然后返回一串 HTTP 报文。
而这里提到的,其实就是针对 web 应用开发中的几个大问题的分解,这些问题对于写了几年 web 开发的人来说其实都不是问题了。


再举一个例子,对于程序员来说,我们往往需要持续地学习,当我们去学习一些新的编程语言的时候,我们可以去思考一下:对于编程语言来说,它可以分解为哪些问题?
个人感觉,这个问题其实挺有价值。要回答这个问题,我们可以回到没有今天这些高级编程语言的时候,那些计算机领域的先驱们是怎么让计算机工作起来的。
我们会发现,其实一开始他们是用的 0 和 1 来去写指令的,后面进化到汇编语言,毕竟一堆 0 和 1 谁记得住?
有了汇编,去做一些操作就简单多了,比如做加法,用一个 ADD 指令就可以了。
但是有了汇编之后,还有一个问题是,不管是从开发、维护上来说,都需要对 CPU 有非常清楚的了解,比如需要了解 CPU 有哪些寄存器之类的知识,
也就是说,使用汇编依然需要了解机器本身的很多运作机制,这无疑是一个比较高的门槛。
再后来到 C 语言的出现,我们就不需要了解 CPU 是怎么工作也可以写出能解决问题的代码了。
但是 C 语言依然有一个毕竟严重的问题,那就是需要开发者手动去申请内存,使用之后再释放内存,如果程序员忘记释放,那么就会造成内存的泄露。
所以更高级的一些语言就有了 GC,也就是说,由语言底层的运行时去帮程序员回收那些已经不再使用的对象。


扯得有点远了,回到问题本身,对于编程语言来说,它可以分解为哪些问题?
这个问题其实非常简单,只要我们随便找一门编程语言的教程来看它们的目录就会知道,一门编程语言本身包含了:



  • 一些基础语法:如代码组织结构是怎样的。Java 是通过一个个的类来组织的,Go 是通过结构体来建立抽象然后通过函数来进行组织的。

  • 对于面向对象的语言来说:不同的编程语言会有不同的类的编写方式。

  • 基本的变量定义是如何定义的

  • 关键字有哪些,比如非常常见的 classpublicdef 之类的

  • 如何实现循环结构

  • 如何实现条件判断

  • 如何在方法中返回值。有些语言需要使用 return,也有些语言比较省事,方法内的最后一行会被当做返回值,比如 ruby

  • 一些常用的数据结构是如何封装的。比如数组、map

  • 标准库。比如如何执行一个系统命令这种功能。

  • 其他...


这个清单不太完整,但是也足够了,因为编程语言的相似性,我们在熟悉了某一门编程语言之后,往往也会比较容易学会另一门编程语言。
但是这也并不代表,我们可以使用这门新的编程语言去解决一些实际的问题,除非,在此之前,我们已经使用了其他编程语言解决过相同的问题了。
比如,我们从 PHP 转换到 Go,我们在 PHP 中已经解决过很多数据库查询的问题了,切换到 Go 中,对于数据库查询的这一问题,我们可以作如下分解:



  • 找到 Go 中查询数据库相关的库

  • 调用库函数来建立到数据库的连接

  • 调用库函数来发送一个 SQL 查询语句到数据库服务器,然后等待数据库服务器返回查询结果

  • 取得查询结果,做其他处理


清楚了我们需要解决的问题之后,其实我们真正要解决的重要问题是如何组织我们的代码,从而使得我们针对这个问题的解决方案更好维护、能更好地使用。
所以现在在学习的时候,更喜欢从实际的问题出发(毕竟计算机领域其实是更偏向于实践)。
然后根据自己拆分后的问题去找解决方案,事实证明,这样效率更高。
如果我们从技术本身出发,我们可能无法知悉这个技术为什么是今天这个样子的,在这种学习方式之下,
我们新学习的东西无法跟我们脑子里原有的东西建立起连接,最终只会很快就忘记。
但是如果从我们熟悉的问题出发,去寻找一种新的解决方案的时候,其实新的知识跟自己的经验是可以很好的联系起来的,这样我们通过一个问题就能联系到不同的解决方案。



真的扯远了,说回正题。说这么多其实就是想说,碰到难题的时候我们也不能盲目地花笨功夫,
遇到难题的时候,我们也许可以考虑一下,这个问题可以如何分解,然后如何解决分解之后的问题。
如果分解后的问题是我们可以解决的,那我们的 “笨功夫” 那就是使用对了。



学习是为了找到学习方法


再说一个关于 “笨功夫” 的个人经历,还是初中的时候,在初中的时候花了很多时间在学习上,但是学习效果并不是非常明显,
多年以后,才明白,自己当初的那种学习其实是 “死学”,也就是不讲究方法的学习,免不了学了就忘。
初中的时候一个物理老师跟我们说他学生时代,有一天在思考问题很久之后突然 “开窍” 了,
以前没懂,现在知道了他说的 “开窍” 大概是找到了关于学习的套路。
可惜的是,我在读书的那十几年里,并没有经历过这样的 “开窍”,所以成绩一直平平无奇。


直到自己工作以后,因为自己从小到大是那种不太擅长交流的人,所以工作前几年遇到问题的时候也基本不会去请教别人,
那怎么办呢?那就自己想办法去解决各种技术问题呗,然后几年下来,好像自己的学习能力有所提升了,明显的表现是,学习新东西的时候会学习得更快了。
后面才懂,越来保持学习其实不只是为了学到各种解决问题的方法,实际上有很多东西都是学了之后用不上的,更重要的是在这个过程中学会如何学习。
关于这一点,陈皓有过一个经典的陈述。


学习不仅仅是为了找到答案,更是为了找到方法 - 陈皓


你有没有发现,在知识的领域也有阶层之分,那些长期在底层知识阶层的人,需要等着高层的人来喂养,
他们长期陷于各种谣言和不准确的信息环境中,于是就导致错误或幼稚的认知,
并习惯于那些不费劲儿的轻度学习方式,从而一点点地丧失了深度学习的独立思考能力,从而再也没有能力打破知识阶层的限制,被困在认知底层翻不了身。


可见深度学习十分重要,但应该怎样进行深度学习呢?下面有三个步骤:



  1. 知识采集。 信息源是非常重要的,获取信息源头、破解表面信息的内在本质、多方数据印证,是这个步骤的关键。

  2. 知识缝合。 所谓缝合就是把信息组织起来,成为结构体的知识。这里,连接记忆,逻辑推理,知识梳理是很重要的三部分。

  3. 技能转换。 通过举一反三、实践和练习,以及传授教导,把知识转化成自己的技能。这种技能可以让你进入更高的阶层。


这就好像,你要去登一座山,一种方法是通过别人修好的路爬上去,一种是通过自己的技能找到路(或是自己修一条路)爬上去。
也就是说,需要有路才爬得上山的人,和没有路能造路的人相比,后者的能力就会比前者大得多得多。
所以,学习是为了找到通往答案的路径和方法,是为了拥有无师自通的能力。


把时间当作朋友



这个标题来源于李笑来的《把时间当作朋友》这本书,书买了我还没看,但是看过他在得到的课程上这一话题的相关文章。



今天这个社会变得越来越浮躁,我们难免会受到影响,经常会想着今天做一件事,明天就能看到成果。
但实际上,在竞争激烈的今天,聪明人很多,又聪明又努力的也有很多,我们能做的只是接受这个事实,
然后持续在自己所在的领域花多一点 “笨功夫”,把时间当作朋友,就算最终我们没有实现最初的目标,
但是回头再看的时候,会发现原来自己已经走得很远了。


最后,用吴军《格局》中的一句话来结束本文:



事实上,功夫没下够,用什么方法都是在浪费时间。



作者:eleven26
来源:juejin.cn/post/7273435446574891062
收起阅读 »

位运算,能不能一次记住!

web
写给新手朋友的位运算,你是不是也是总是不记得,记了又忘记,记住了又不知道怎么运用到编程中?那记一次理论+题目的方式理解记忆,搞清楚它吧! 我们接触最多的是十进制数,加减乘除这种四则远算其实就是二进制数据中的一种运算法则,当谈到位运算,它不是用来操作十进制数的,...
继续阅读 »

写给新手朋友的位运算,你是不是也是总是不记得,记了又忘记,记住了又不知道怎么运用到编程中?那记一次理论+题目的方式理解记忆,搞清楚它吧!


我们接触最多的是十进制数,加减乘除这种四则远算其实就是二进制数据中的一种运算法则,当谈到位运算,它不是用来操作十进制数的,我们实际上是在操作二进制数的不同位。位运算在前端开发中可能不常用,但了解它们对你理解计算机底层运作和一些特定情况下的优化是有帮助的。


接下来我们从几种常见的位运算开始,以及它们的使用场景,好好理解一番。


1. 二进制转换


既然是写给新手朋友也能看得明白的,那就顺带提一下二进制数吧(熟悉二进制的可以跳过这段)



当计算机处理数据时,它实际上是在执行一系列的二进制操作,因为计算机内部使用的是电子开关,这些开关只能表示两个状态:开(表示1)和关(表示0)。因此,计算机中的所有数据最终都被转换为二进制表示。


二进制(binary)是一种使用两个不同符号(通常是 0 和 1)来表示数字、字符、图像等信息的数字系统。这种二元系统是现代计算机科学的基础。





  • 十进制到二进制的转换:




将十进制数转换为二进制数的过程涉及到不断地除以2,然后记录余数。最后,将这些余数按相反的顺序排列,就得到了对应的二进制数。


例如,将十进制数 13 转换为二进制数:



  1. 13 除以 2 得商 6,余数 1

  2. 6 除以 2 得商 3,余数 0

  3. 3 除以 2 得商 1,余数 1

  4. 1 除以 2 得商 0,余数 1


将这些余数按相反的顺序排列,得到二进制数 1101。


或者你也可以这么想


  1. (1 || 0) * 2^n + (1 || 0) * 2^(n-1) + ... + (1 || 0) * 2^0 = 13

  2. 只需要满足以上公式,加出来你想要的值

  3. 2 的 4次方大于13,2的3次方小于13,那么就从2的3次方开始依次递减到0次方

  4. 1 * 2^3 + 1 * 2^2 + 1 * 2^1 + 1 * 2^0 显然 8 + 4 + 2 + 1 = 15已经超出了13,所以你得在这个式子中减少2

  5. 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 取该等式中的1,0;所以 13 的二进制是 1101


以上两种方式都能得出一个数的二进制,看你喜欢




  • 二进制到十进制的转换:




将二进制数转换为十进制数的过程涉及到将每个位上的数字与2的幂相乘,然后将这些结果相加。


例如,将二进制数 1101 转换为十进制数:



  1. 第0位(最右边)上的数字是 1,表示 2^0 = 1

  2. 第1位上的数字是 0,表示 2^1 = 0

  3. 第2位上的数字是 1,表示 2^2 = 4

  4. 第3位上的数字是 1,表示 2^3 = 8


将这些结果相加:1 + 0 + 4 + 8 = 13,得到十进制数 13。


在编程中,通常会使用不同的函数或方法来实现十进制到二进制以及二进制到十进制的转换,这些转换可以帮助我们在计算机中处理和表示不同的数据。


2. 按位与(&)


按位与运算会将两个数字的二进制表示的每一位进行 操作,如果两个相应位都是 1,则结果为 1,否则为 0。


使用场景: 常用于权限控制和掩码操作。


image.png


一道题让你更好的理解它的用法


题目:判断一个整数是否是2的幂次方。


问题描述:给定一个整数 n,判断它是否是2的幂次方,即是否满足 n = 2^k,其中 k 是非负整数。


使用位运算中的按位与操作可以很巧妙地解决这个问题。


思路:如果一个数 n 是2的幂次方,那么它的二进制表示一定只有一位是1,其他位都是0(例如:8的二进制是 1000)。而 n - 1 的二进制表示则是除了最高位的1之外,其他位都是1(例如:7的二进制是 0111)。如果我们对 nn - 1 进行按位与操作,结果应该是0。


那我们可以这么写:


image.png


在这个示例中,我们巧妙的使用了 (n & (n - 1)) 来检查是否满足条件,如果结果为0,说明 n 是2的幂次方。


希望这个示例能够帮助你更好地理解按位与运算的应用方式!


2. 按位或(|)


按位或运算会将两个数字的二进制表示的每一位进行或操作,如果两个相应位至少有一个是 1,则结果为 1,否则为 0。


使用场景: 常用于设置选项和权限。


image.png


一道题让你更好的理解它的用法


题目:如何将一个整数的特定位设置为1,而不影响其余位。


问题描述:给定一个整数 num,以及一个表示要设置为1的位的位置 bitPosition(从右向左,最低位的位置为0),编写一个函数将 num 的第 bitPosition 位设置为1。


我们可以使用按位或运算来实现这个效果


image.png


在这个示例中,我们首先创建了一个掩码 mask(这里用到了另一个位运算,左移,下面会讲到),它只有第 bitPosition 位是1,其他位都是0。然后,我们使用按位或运算 num | masknum 的第 bitPosition 位设置为1,得到了结果。


这个问题演示了如何使用按位或运算来修改一个整数的特定位,而不影响其他位。希望这个示例能帮助你更好地理解按位或运算的应用方式!


3. 按位异或(^)


按位异或运算会将两个数字的二进制表示的每一位进行异或操作,如果两个相应位不相同则结果为 1,相同则为 0。


使用场景: 常用于数据加密和校验。


image.png


一道题让你更好的理解它的用法


题目:如何交换两个整数的值,而不使用额外的变量


问题描述:给定两个整数 ab,编写一个函数来交换它们的值,而不使用额外的变量。


我们可以使用按位异或运算来实现这个效果:


image.png


上述代码中,我们首先将 a 更新为 a ^ b,这使得 a 包含了 ab 的异或值。然后,我们使用同样的方法将 b 更新为 a 的原始值,最后,我们再次使用异或运算将 a 更新为 b 的原始值,完成了交换操作。



此处应该沉思,思考清楚这个问题:(a ^ b) ^ b 得到的是 a 的原始值



不使用额外的变量来做两个变量值的交换,这还是个面试题哦!


4. 按位非(~)


按位非运算会将一个数字的二进制表示的每一位取反,即 0 变成 1,1 变成 0。它将操作数转化为 32 位的有符号整型。


image.png


一道题让你更好的理解它的用法


题目:反转二进制数的位,然后返回其对应的十进制数


问题描述:给定一个二进制字符串,编写一个函数来反转该字符串的位,并返回其对应的十进制数。


image.png


这里你可能会有疑问,为什么13的二进制取反会的到-14,这里就不得不介绍一下 补码 的概念了


5. 补码小插曲


假设我们要求 -6 的二进制,那就相当于是求 -6 的补码


因为负数的二进制表示通常使用二进制补码来表示。要计算-6的二进制补码表示,可以按照以下步骤操作:



  1. 首先,找到6的二进制表示。6的二进制表示是 00000110

  2. 然后,对6的二进制表示进行按位取反操作,即将0变成1,将1变成0。这将得到 11111001

  3. 最后,将取反后的结果加1。11111001 + 1 = 11111010


所以,-6的二进制补码表示是 11111010。在补码中,最高位表示符号位,0表示正数,1表示负数,其余位表示数值的绝对值。因此,11111010 表示的是-6。


注意:

-6的二进制补码表示的位数不一定是8位。位数取决于数据类型和计算机系统的规定。在许多计算机系统中,整数的表示采用固定的位数,通常是32位或64位,但也可以是其他位数,例如16位。


在常见的32位表示中,-6的二进制补码表示可能是 11111111111111111111111111111010。这是32位二进制,其中最高位是符号位(1表示负数),其余31位表示数值的绝对值。


在64位表示中,-6的二进制补码表示可能是 1111111111111111111111111111111111111111111111111111111111110。这是64位二进制,同样,最高位是符号位,其余63位表示数值的绝对值。


因此,-6的二进制补码表示的位数取决于计算机系统和数据类型的规定。不同的系统和数据类型可能采用不同的位数。


6. 左移(<<)和右移(>>)


左移运算将一个数字的二进制表示向左移动指定的位数,右移运算将二进制表示向右移动指定的位数。


image.png



注意:因为我们的计算可以是32位或者是64位的,所以理论上 5 的二进制应该是 00... 00000101, 整体长度为32或者64。 左移我们只是把有效值 101 向左拖动,右边补0,右移左边补 0, 但是要保证整体32或64位长度不能变,所以,右移会砍掉超出去的值



一道题让你更好的理解它的用法


题目: 如何实现整数的乘法和除法,使用左移和右移操作来提高效率。


问题描述:编写一个函数,实现整数的乘法和除法运算,但是只能使用左移和右移操作,不能使用乘法运算符 * 和除法运算符 /


这也是一道面试题,实现起来很简单


image.png



想清楚,一个数的二进制,每次左移一位的结果会怎么样?


比如 6 的二进制是 00000110, 左移一次后变成 00001100,


也就是说 从 2^2 + 2^1 变成了 2^3+ 2^2 。 4 + 2 变成了 8 + 4。


所以每左移一位,都相当于是原数值本身放大了一倍



这样你是否更清楚了用左移来实现乘法的效果了呢?


最后


以上列举的是常见的位运算方法,还有一些不常见的,比如:



  1. 位清零(Bit Clearing):将特定位设置为0,通常使用按位与运算和适当的掩码来实现。

  2. 位设置(Bit Setting):将特定位设置为1,通常使用按位或运算和适当的掩码来实现。

  3. 位翻转(Bit Flipping):将特定位取反,通常使用按位异或运算和适当的掩码来实现。

  4. 检查特定位:通过使用按位与运算和适当的掩码来检查特定位是否为1或0。

  5. 位计数:计算一个整数二进制表示中1的个数,这通常使用一种称为Brian Kernighan算法的技巧来实现。

  6. 位交换:交换两个整数的特定位,通常使用按位异或运算来实现。


等等...有兴趣的可以自行摸索了


作者:一个大蜗牛
来源:juejin.cn/post/7274188187675902004
收起阅读 »

浅谈多人游戏原理和简单实现

一、我的游戏史 我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿...
继续阅读 »

在这里插入图片描述


一、我的游戏史


我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿是我挨得最狠的,以至于现在回想起来,屁股还条件反射的隐隐作痛。


后来我骗我妈说我要学习英语、练习打字以后成为祖国的栋梁之才!让她给我买台小霸王学习机(游戏机),在我一哭二闹三上吊胡搅蛮缠的攻势下,我妈妥协了。就此我接触到了FC游戏。现在还能记得我和朋友玩激龟快打,满屋子的小朋友在看的场景。经常有家长在我家门口喊他家小孩吃饭。那时候我们县城里面有商店卖游戏卡,小卡一张5块钱,一张传奇卡25-40块不等(所谓传奇卡就是角色扮演,带有存档的游戏),每天放学都要去商店去看看,有没有新的游戏卡,买不起,就看下封面过过瘾。我记得我省吃俭用一个多月,买了两张卡:《哪吒传奇》和《重装机兵》那是真的上瘾,没日没夜的玩。


再然后我接触到了手机游戏,记得那时候有个软件叫做冒泡游戏(我心目的中的Stream),里面好多游戏,太吸引我了。一个游戏一般都是几百KB,最大也就是几MB,不过那时候流量很贵,1块钱1MB,并且!一个月只有30Mb。我姑父是收手机的,我在他那里搞到了一部半智能手机,牌子我现在还记得:诺基亚N70,那时候我打开游戏就会显示一杯冒着热气的咖啡,我很喜欢这个图标,因为看见它意味着我的游戏快加载完成了,没想到,十几年后我们会再次相遇,哈哈哈哈。我当时玩了一款网游叫做:《幻想三国》,第一回接触网游简直惊呆了,里面好多人都是其他玩的家,这太有趣了。并且我能在我的手机上看到其他玩家,能够看到他们的行为动作,这太神奇了!!!我也一直思考这到底是怎么实现的!


最后是电脑游戏,单机:《侠盗飞车》、《植物大战僵尸》、《虐杀原型》;网游:《DNF》、《CF》、《LOL》、《梦幻西游》我都玩过。


不过那个疑问一直没有解决,也一值留在我心中 —— 在网络游戏中,是如何实时更新其他玩家行为的呢?


二、解惑


在我进入大学后,我选择了软件开发专业,真巧!再次遇到了那个冒着热气的咖啡图标,这时我才知道它叫做——Java。我很认真的去学,希望有一天能够做一款游戏!


参加工作后,我并没有如愿以偿,我成为了一名Java开发程序员,但是我在日常的开发的都是web应用,接触到大多是HTTP请求,它是种请求-响应协议模式。这个问题也还是想不明白,难道每当其他玩家做一个动作都需要发送一次HTTP请求?然后响应给其他玩家。这样未免效率也太低了吧,如果一个服务器中有几千几万人,那么服务器得承受多大压力呀!一定不是这样的!!!


直到我遇到了Websocket,这是一种长连接,而HTTP是一种短连接,顿时这个问题我就想明白了。在此二者的区别我就不过多赘述了。详细请看我的另一篇文章


知道了这个知识后,我终于能够大致明白了网络游戏的基本原理。原来网络游戏是由客户端服务器端组成的,客户端就是我们下载到电脑或者手机上的应用,而服务器端就是把其他玩家连接起来的中转站,还有一点需要说明的是,网络游戏是分房间的,这个房间就相当于一台服务器。首先,在玩家登陆客户端并选择房间建立长连接后,A玩家做出移动的动作,随即会把这个动作指令上传给服务器,然后服务器再将指令广播到房间中的其他玩家的客户端来操作A的角色,这样就可以实现实时更新其他玩家行为。


在这里插入图片描述


三、简单实现


客户端服务端在处理指令时,方法必须是配套的。比如说,有新的玩家连接到服务器,那么服务器就应当向其它客户端广播创建一个新角色的指令,客户端在接收到该指令后,执行客户端创建角色的方法。
为了方便演示,这里需要定义两个HTML来表示两个不同的客户端不同的玩家,这两套客户端代码除了玩家的信息不一样,其它完全一致!!!


3.1 客户端实现步骤


我在这里客户端使用HTML+JQ实现


客户端——1代码:


(1)创建画布


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Game</title>
<style>
canvas {
border: 1px solid black;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<canvas id="gameCanvas" width="800" height="800"></canvas>
</body>
</html>

(2)设置1s60帧更新页面


const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function gameLoop() {
clearCanvas();
players.forEach(player => {
player.draw();
});
}
setInterval(gameLoop, 1000 / 60);
//清除画布方法
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}

(3)连接游戏服务器并处理指令


这里使用websocket链接游戏服务器


 //连接服务器
const websocket = new WebSocket("ws://192.168.31.136:7070/websocket?userId=" + userId + "&userName=" + userName);
//向服务器发送消息
function sendMessage(userId,keyCode){
const messageData = {
playerId: userId,
keyCode: keyCode
};
websocket.send(JSON.stringify(messageData));
}
//接收服务器消息,并根据不同的指令,做出不同的动作
websocket.onmessage = event => {
const data = JSON.parse(event.data);
// 处理服务器发送过来的消息
console.log('Received message:', data);
//创建游戏对象
if(data.type == 1){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
console.log("玩家id:"+playerOfIds);
createPlayer(data.players[i].playerId,data.players[i].pointX, data.players[i].pointY, data.players[i].color);
}
}
//销毁游戏对象
if(data.type == 2){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
destroyPlayer(data.players[i].playerId)
}
}
//移动游戏对象
if(data.type == 3){
console.log("移动;玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
players.filter(player => player.id === data.players[i].playerId)[0].move(data.players[i].keyCode)
}
}
};

(4)创建玩家对象


//存放游戏对象
let players = [];
//playerId在此写死,正常情况下应该是用户登录获取的
const userId = "1"; // 用户的 id
const userName = "逆风笑"; // 用户的名称
//玩家对象
class Player {
constructor(id,x, y, color) {
this.id = id;
this.x = x;
this.y = y;
this.size = 30;
this.color = color;
}
//绘制游戏角色方法
draw() {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.size, this.size);
}
//游戏角色移动方法
move(keyCode) {
switch (keyCode) {
case 37: // Left
this.x = Math.max(0, this.x - 10);
break;
case 38: // Up
this.y = Math.max(0, this.y - 10);
break;
case 39: // Right
this.x = Math.min(canvas.width - this.size, this.x + 10);
break;
case 40: // Down
this.y = Math.min(canvas.height - this.size, this.y + 10);
break;
}
this.draw();
}
}

(5)客户端创建角色方法


//创建游戏对象方法
function createPlayer(id,x, y, color) {
const player = new Player(id,x, y, color);
players.push(player);
playerOfIds.push(id);
return player;
}

(6)客户端销毁角色方法


在玩家推出客户端后,其它玩家的客户端应当销毁对应的角色。


//角色销毁
function destroyPlayer(playId){
players = players.filter(player => player.id !== playId);
}

客户端——2代码:


客户端2的代码只有玩家信息不一致:


  const userId = "2"; // 用户的 id
const userName = "逆风哭"; // 用户的名称

3.2 服务器端


服务器端使用Java+websocket来实现!


(1)引入依赖:


 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>

(2)创建服务器


@Component
@ServerEndpoint("/websocket")
@Slf4j
public class Server {
/**
* 服务器玩家池
* 解释:这里使用 ConcurrentHashMap为了保证线程安全,不会出现同一个玩家存在多条记录问题
* 使用 static fina修饰 是为了保证 playerPool 全局唯一
*/

private static final ConcurrentHashMap<String, Server> playerPool = new ConcurrentHashMap<>();
/**
* 存储玩家信息
*/

private static final ConcurrentHashMap<String, Player> playerInfo = new ConcurrentHashMap<>();
/**
* 已经被创建了的玩家id
*/

private static ConcurrentHashMap<String, Server> createdPlayer = new ConcurrentHashMap<>();

private Session session;

private Player player;

/**
* 连接成功后调用的方法
*/

@OnOpen
public void webSocketOpen(Session session) throws IOException {
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
String userId = requestParameterMap.get("userId").get(0);
String userName = requestParameterMap.get("userName").get(0);
this.session = session;
if (!playerPool.containsKey(userId)) {
int locationX = getLocation(151);
int locationY = getLocation(151);
String color = PlayerColorEnum.getValueByCode(getLocation(1) + 1);
Player newPlayer = new Player(userId, userName, locationX, locationY,color,null);
playerPool.put(userId, this);
this.player = newPlayer;
//存放玩家信息
playerInfo.put(userId,newPlayer);
}
log.info("玩家:{}|{}连接了服务器", userId, userName);
// 创建游戏对象
this.createPlayer(userId);
}

/**
* 接收到消息调用的方法
*/

@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
log.info("用户:{},消息{}:",this.player.getPlayerId(),message);
PlayerDTO playerDTO = new PlayerDTO();
Player player = JSONObject.parseObject(message, Player.class);
List<Player> players = new ArrayList<>();
players.add(player);
playerDTO.setPlayers(players);
playerDTO.setType(OperationType.MOVE_OBJECT.getCode());
String returnMessage = JSONObject.toJSONString(playerDTO);
//广播所有玩家
for (String key : playerPool.keySet()) {
synchronized (session){
String playerId = playerPool.get(key).player.getPlayerId();
if(!playerId.equals(this.player.getPlayerId())){
playerPool.get(key).session.getBasicRemote().sendText(returnMessage);
}
}
}
}

/**
* 关闭连接调用方法
*/

@OnClose
public void onClose() throws IOException {
String playerId = this.player.getPlayerId();
log.info("玩家{}退出!", playerId);
Player playerBaseInfo = playerInfo.get(playerId);
//移除玩家
for (String key : playerPool.keySet()) {
playerPool.remove(playerId);
playerInfo.remove(playerId);
createdPlayer.remove(playerId);
}
//通知客户端销毁对象
destroyPlayer(playerBaseInfo);
}

/**
* 出现错误时调用的方法
*/

@OnError
public void onError(Throwable error) {
log.info("服务器错误,玩家id:{},原因:{}",this.player.getPlayerId(),error.getMessage());
}
/**
* 获取随即位置
* @param seed
* @return
*/

private int getLocation(Integer seed){
Random random = new Random();
return random.nextInt(seed);
}
}

websocket配置:


@Configuration
public class ServerConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}


(3)创建玩家对象


玩家对象:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
/**
* 玩家id
*/

private String playerId;
/**
* 玩家名称
*/

private String playerName;
/**
* 玩家生成的x坐标
*/

private Integer pointX;
/**
* 玩家生成的y坐标
*/

private Integer pointY;
/**
* 玩家生成颜色
*/

private String color;
/**
* 玩家动作指令
*/

private Integer keyCode;
}

创建玩家对象返回给客户端DTO:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerDTO {
private Integer type;
private List<Player> players;
}

玩家移动指令返回给客户端DTO:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerMoveDTO {
private Integer type;
private List<Player> players;
}


(4)动作指令


public enum OperationType {
CREATE_OBJECT(1,"创建游戏对象"),
DESTROY_OBJECT(2,"销毁游戏对象"),
MOVE_OBJECT(3,"移动游戏对象"),
;
private Integer code;
private String value;

OperationType(Integer code, String value) {
this.code = code;
this.value = value;
}

public Integer getCode() {
return code;
}

public String getValue() {
return value;
}
}

(5)创建对象方法


  /**
* 创建对象方法
* @param playerId
* @throws IOException
*/

private void createPlayer(String playerId) throws IOException {
if (!createdPlayer.containsKey(playerId)) {
List<Player> players = new ArrayList<>();
for (String key : playerInfo.keySet()) {
Player playerBaseInfo = playerInfo.get(key);
players.add(playerBaseInfo);
}
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.CREATE_OBJECT.getCode());
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
// 存放
createdPlayer.put(playerId, this);
}
}

(6)销毁对象方法


   /**
* 销毁对象方法
* @param playerBaseInfo
* @throws IOException
*/

private void destroyPlayer(Player playerBaseInfo) throws IOException {
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.DESTROY_OBJECT.getCode());
List<Player> players = new ArrayList<>();
players.add(playerBaseInfo);
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
}

四、演示


4.1 客户端1登陆服务器


在这里插入图片描述


4.2 客户端2登陆服务器


在这里插入图片描述


4.3 客户端2移动


在这里插入图片描述


4.4 客户端1移动


在这里插入图片描述


4.5 客户端1退出


在这里插入图片描述
完结撒花


完整代码传送门


五、总结


以上就是我对网络游戏如何实现玩家实时同步的理解与实现,我实现后心里也释然了,哈哈哈,真的好有趣!!!
我希望大家也是,不要失去好奇心,遇到自己感兴趣的事情,一定要多思考呀~


后来随着我经验的不断积累,我又去了解了一下Java作为游戏服务器的相关内容,发现Netty更适合做这个并且更容易入门,比如《我的世界》一些现有的服务器就是使用Netty实现的。有空也实现下,玩玩~


作者:是江迪呀
来源:juejin.cn/post/7273429629398581282
收起阅读 »

别让时代的悲哀,成为你的悲哀

全球经济下行,各大公司裁员,我们身处其中,又该如何自洽?本文分享我的一些观点,希望也能给你带来一些新的思考。 前言 最近这段时间,可谓一直都很不太平。 一开始有人说“前端已死”的时候,我身处其中,冷暖自知。 这是我今年找工作,在Boss直聘上花 68 元巨...
继续阅读 »

全球经济下行,各大公司裁员,我们身处其中,又该如何自洽?本文分享我的一些观点,希望也能给你带来一些新的思考。


前言


最近这段时间,可谓一直都很不太平。


一开始有人说“前端已死”的时候,我身处其中,冷暖自知。




这是我今年找工作,在Boss直聘上花 68 元巨款开的会员,主要功能就是每天有 5 次机会告诉你在某岗位的竞争力,纯纯花钱买焦虑。上面两张图只是普通小公司的一个前端岗位,竟有上千人竞争。



后来也有人说“前端死不了”,我没有发表过什么看法,因为我觉得不值得讨论。



前端发展迅速,同时也充满困境,只有站在一线的开发,才能明白前端本就半死不活。像前两年还有《现代 Web 开发困局》这样的文章在分析前端困局、讨论如何解放生产力,而现在这种声音大部分人可能并不关心。



再到现今一众技术公号不是《前端岗位又爆了》就是《前端这波起飞》的,我也只是微微一笑,软广吹牛从不打草稿。


这篇文章不聊技术,不蹭热点,只是单纯地从一个普通互联网从业者的角度,讲点近段时间以来的一些思考。


危机


历史的车轮滚滚而来,它与每个人息息相关。


随着口罩病三年的折磨、俄乌冲突爆发、漂亮国霸权制裁,国家整体经济呈现明显下滑,我们能切身的感觉到,大环境确实变差了许多。


然而放眼全球,大部分国家也都在经历严重的经济衰退,有的甚至已经破产或者走在了破产的路上。我们现在所处的是一个什么样的阶段呢?中高端产业永远在努力突破欧美的封锁,而低端制造业还要面对印度和东南亚等国家的竞争,作为世界经济的重要一环,中国不可能不受影响。


从元宇宙区块链,再到如今火热的 AI 人工智能,我们太想要新技术的突破了,然而这并不是简单的事情,大部分普通人能做的,其实就是等待和做好随时迎接新的改变的准备,要么就只能贩卖焦虑了。



有时危机的发生并不一定是要伤害你,也可能是让你从迷局中醒过来,或者把你以前故意忽略、拖延、认识不到位的问题集中爆发出来,逼你去解决问题。



努力


不知从什么时候开始,我们总是崇尚努力奋斗,然后理所当然地认为努力就是一切成功的根源,如果没能成功,就是你还不够努力。


找不到理想的工作,便认为是自己还不够努力,是简历写得不好,是面试题背的还不够多......试想要是市场的岗位供远大于求,谁还卡学历,谁还谈资历呢,HR们不跪着求你来面试吗?面试题咱也先别做了罢,进来干活再说。


可事实是,大部分公司都在降本增效,同时还存在着许多比你更聪明优秀、天赋异禀的人,关键是他们还都比你更努力。


所以在我看来,努力更多是为了拥有选择的权利,除此之外并不代表什么。
如果你觉得光靠努力就可以无所不能,那何尝不是一种傲慢。




出自动漫 ——《强风吹拂》。



既然很多事就算努力了也不一定有回报,那么是不是就干脆摆烂了,不努力了呢?


悲观者往往正确,很多人想摆烂的根本在于,这个世界上有太多东西是不确定的,这无可厚非,但有时过于悲观,往往就容易迷失自己。


著名软件 Homebrew 的作者 Max Howell 去谷歌面试的时候,因写不出反转二叉树被拒,留下了“虽然谷歌公司 90% 的工程师都在用你写的软件,但抱歉我们不能聘用你”的这段传说。


著名开源框架 Vue 的作者尤雨溪在直播中聊到,自己曾在某次面试时被问如何实现JS原型链的问题,结果他当时完全回答不上来。


那些真正成功的人,一定不用非要在某件事情上证明自己。


边界


最近我在思考一种处世的能力,我把它叫做“边界力”。简单来说,就是遇到难以克服的障碍,就承认自己做不到。


这听起来似乎很消极,但只有学会建立、掌控自己边界的人,才能够明确自己的责任与长处,从而找到更好的做事方向和解决问题方法,少走弯路。


人们或多或少都会有一些自恋的,而且很多时候自己还浑然不知。


比方说,同在一个写作训练营里,大部分人可以1-2天写出一篇稿子,但有一个人做不到,他就会想为什么别人可以,我不行?当他下意识地责怪自己达不到平均水平时,背后其实就是一种自恋。因为他默认别人能做到的,自己一定也能做到。可是,有人能保证自己的任何一项技能,都在集体的平均水平以上吗?


自恋感会让我们下意识地认为,面前这个事可以做到。它会误导我们,让我们不断把注意力集中在“为什么我就是做不到”上面,然后一遍又一遍碰壁,而不是去想“这太难了,也许我该换个办法”。


不过在承认自己做不到之前,要确认这件事是否真的超出了我们的能力范围,如何确认呢?我觉得有两种方法:



  1. 结果反馈


统计学上有个概念,叫大数定律,历史上有不少数学家做过抛硬币实验,很简单:不停抛一枚硬币,记录出现正面和反面的结果,最后随着抛的次数越多,结果就越明朗:一定是有一半的概率是正面,一半的概率是反面。


看似包含不确定性的事情,往往也有着某种统计的确定性。也就是说,偶然之中有必然


在承认做不到之前,要先问下自己是不是尝试的样本还不够大。当你让想做的事情出现次数足够多时,你一定会知道它到底能不能成。



  1. 压力反馈


看看你做一件事会不会导致极大的不适,比如开始失眠,身体出现莫名的疼痛,或是习惯性地拖延,又或是变得过度敏感、负能量爆棚。如果这些情况同时出现,那么你就得考虑承认自己确实做不到了。


能把时间都花在对的事情上,你就已经是一个很厉害的人了。


焦虑


在愈发“内卷”的社会形态下,焦虑几乎是所有人无法逃避的负面情绪,直到我看到一位博主季白羽说的这样一段话:



焦虑的人都有一个共同特征,那就是没有尊重世界的客观规律。


比如说:没有持续天天锻炼,却期待拥有健康体魄;没有好好经营关系,却期待别人都喜欢自己;没有大量刻意练习,却期待写出好文章。



我们总是容易把一切怪罪给外部因素,而忽略了焦虑的核心——还是对自我的认识不够清晰。


如果你期待有影响力,就要做好在一个领域长期积累的准备;如果你期待赚到钱,就要经常去做与赚钱相关的事。但你不能什么都不做,就期待能拥有一切。


提升“边界力”,想办法搞清楚什么是确定的,什么是不确定的,然后不断去重复那些确定的事,我想焦虑就会自动远离我们。


心态


当我翻开《腾讯传》一书的时候,歪歪斜斜每页都写着"中国互联网进化论"几个字,可我却从字缝里只读出了"幸运"。


在我看来,腾讯的崛起是十分幸运的,早年的腾讯给别人做过软件外包,无数次想要卖掉公司但卖不出去,拉投资时连创始人马化腾都说不清未来的方向,可谓前途一片渺茫,很难想象它会成长到如今的体量。


其实马化腾对互联网并不感兴趣,天文学才是他从小的志向所在。中学时为了能看见哈雷彗星,求着父母买台专业级望远镜,彼时的马化腾做梦都想成为天文学家,善良的父母最终答应了,那是他父亲四个月的工资。后来他谈及自己的爱好时说:



看着星空,会觉得自己很渺小,可能我们在宇宙中从来就是一个偶然。所以,无论什么事情,仔细想一想,都没有什么大不了的。



虽然马化腾后来也没真的成为天文学家,但这份爱好给了他独特的思考,始终帮助他在遇到挫折时稳定心态,想得更开。如果当时他一直为公司焦虑,也许就等不到后来属于腾讯的曙光了。


为什么运气也是实力的一部分,因为在黎明到来前,你必须有足够强大的心态面对黑暗的桎梏,才有机会配得上后来的幸运。


所以无论你当下正在经历多么煎熬痛苦的时刻,都请记得:



现在的怕和愁,只不过是能力小和经历少;十年后,所有的这些事,都只是下酒菜。



有生存就会有危机,有危机才会有机会。


然而危机并不可怕,可怕的是我们没有预料它的到来


没有预料危机的到来也不是那么可怕,可怕的是我们将危机想的太大,吓坏了自己,提前放弃了生存的机会。


天道


我们常说尽人事,听天命。罗翔老师说过一段话令我印象深刻:



如果你相信天道酬勤的话,很容易导致人走向骄傲,或者走向虚无。因为当你成功的时候,你会觉得是靠你努力拼搏得来的,你配拥有这一切,所以你就会瞧不起那些失败的人。而当你努力了最后却依然一事无成,又会开始抱怨天道不公。



由于个体太过于渺小,人生中大部分的事情其实都是你决定不了的,与其对抗,甚至会催生出人性潜藏的弱点。所以罗翔老师提出一种悖论式的命定论:即我们可以凡事尽力而为,同时也要学会接受命运的一切安排


换句话说就是:“但行好事,莫问前程”。


可能很多人会觉得“好事”是指“对他人或自己有好处的事”,而我则偏向于解读为“爱好之事”,这样反过来讲就是说:不要因为担心前程就放弃了热爱的事物。


全球经济下行已然是大势所趋,但别让时代的悲哀,成为你的悲哀!愿你我都有重新开始的勇气,也有一往无前的劲头,在有限的时间里,去将自己想做的事一件件地完成,因为我们只有先做到尽人事,才能更从容地听天命。


作者:茶无味的一天
来源:juejin.cn/post/7273516671574556687
收起阅读 »

工作 6 年,我不想再「键政」了

今天,刷推时看到一张图,感觉和我工作几年来的心路历程很像,特此分享下。 第一个人脚下空无一物,眼中均是美好。 第二个人读了一些书,看到美好背后的黑暗,开始陷入迷茫。 第三个人学识渊博,了解运行规律,明白世界不是非黑即白,故此看到曙光。 而我呢,目前可能还处在...
继续阅读 »

今天,刷推时看到一张图,感觉和我工作几年来的心路历程很像,特此分享下。



第一个人脚下空无一物,眼中均是美好。


第二个人读了一些书,看到美好背后的黑暗,开始陷入迷茫。


第三个人学识渊博,了解运行规律,明白世界不是非黑即白,故此看到曙光。


而我呢,目前可能还处在第二阶段,但也清楚应该继续向前,走向第三阶段。


第一阶段



无知小粉红心态



读书期间,小镇出身的我,比较追求应试教育和实用主义,所思所学全为了考高分、学技术,除此之外的素质教育全然不顾。


同样是去图书馆,我看的是「精通 Java」,而舍友看的是「毛选」、「中国近代史」这类的书籍。在那时,我是不屑一顾的,认为这就是 「浪费时间」,看这些又不能当饭吃。


毕业后,舍友进了体制内,而我去了一家小厂当码农。小厂也挺好,朝九晚六,不追求结婚买房,过得很快乐。


然而,我还是没有继续读书,技术之外脑袋空空,只会被动的接收主流媒体提供的资讯,从不思考内在逻辑。


有一次,社保税改(2018年)要求公司按员工真实收入去上报缴纳基数,也就是说社保缴纳金额变多、到手工资变少。看到群里都在吐槽,而那时的我却在群里发表了「高见」:



社保不也是自己的钱么,提高缴纳基数更赚么?gj 这是为我们个人谋福利!



结果招来一顿全嘲,说我「啥也不懂」。后面又工作了一段时间,我才彻底明白了他们的槽点。


第二阶段



生活压力,终使自己变成自己最讨厌的人



早期很喜欢逛知乎,也关注了一些前端大佬,希望学点技术。


但从某段时间开始(大概2020 左右),发现这些人很喜欢「键政」,大谈国事。


大多都是负面情绪,当时作为「小粉红」的我难以接受,于是拉黑了好几个人。


随着年龄上去,迫使自己需要关注技术之外的内容:房产、婚姻、生育、教育、理财、交际,往大点说,是政治、历史、和经济。


粗浅了解之后,我开始悲观:

  • 刑不上大夫
  • 十年寒窗凭什么拼得过人家三代人的努力
  • 历史就是圈,教员想改变的事情是无法改变的
  • zg人的劣根性
  • tz内的劣根性

于是,我也开始键政,变成了那个曾经最讨厌的人。


第三阶段



探索底层逻辑



工作压力加上生活压力,使我一度抑郁,甚至产生过极端想法。


好在,我有一个好伴侣,是她陪我度过了那段痛苦的岁月,鼓励我多看书、多思考。


现在,我也分享下我的一些想法,虽然还未正式踏入第三阶段,但也大概摆脱了第二阶段的影响。

  1. 接纳自己的平凡
  2. 最重要的能力,是获得能力的能力
  3. 遵从历史规律,做务实求进的人
  4. 思考底层逻辑,所有方法论都可以通过底层逻辑(相同之处)+ 环境变量 (不同之处) 来解释
  5. 提升思维认知,多学习技术之外的内容

最后


以上便是我工作六年的心路历程,从开始的无知,再到键政,最后开始寻求转变。


本文纯碎碎念,欢迎各位客官吐槽~


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

如何告诉后端出身的领导:这个前端需求很难实现

本文源于一条评论。 有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。” 这位朋友让我写一写,那我就写一写。 反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评...
继续阅读 »

本文源于一条评论。




有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。


这位朋友让我写一写,那我就写一写。


反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评论回复吧。愿意看扯淡的同学可以留一下。


现象分析


首先,前半句让我很有同感。因为,我就是前端出身,并且在研发管理岗上又待过。这个身份,让我既了解前端的工作流程,又经常和后端平级一起交流经验。这有点卧底的意思。


有一次,我们几个朋友聚餐闲聊。他们也都是各个公司的研发负责人。大家就各自吐槽自己的项目。


有一个人就说:“我们公司,有一个干前端的带项目了,让他搞得一团糟”


另一个人听到后,就叹了一口气:“唉!这不是外行领导内行吗?!


我当时听了感觉有点别扭。我扫视了一圈儿,好像就我一个前端。因此,我也点了点头(默念:我是卧底)。


是啊,一般的项目、研发管理岗,多是后端开发。因为后端是业务的设计者,他掌握着基本的数据结构以及业务流转。而前端在他们看来,就是调调API,然后把结果展示到页面上。


另外,一般项目遇到大事小情,也都是找后端处理。比如手动修改数据、批量生成数据等等。从这里可以看出,项目的“根基”在后端手里。


互联网编程界流传着一条鄙视链。它是这样说的,搞汇编的鄙视写C语言的,写C的鄙视做Java的,写Java的鄙视敲Python的,搞Python的鄙视写js的,搞js的鄙视作图的。然后,作图的设计师周末带着妹子看电影,前面的那些大哥继续加班写代码。


当然,我们提倡一个友好的环境,不提倡三六九等。这里只是调侃一下,采用夸张的手法来阐述一种现象。


这里所谓的“鄙视”,其本质是源于谁更接近原理。


比如写汇编的人,他认为自己的代码能直接调用“CPU”、“内存”,可以直接操纵硬件。有些前端会认为美工设计的东西,得依靠自己来实现,并且他们不懂技术就乱设计,还有脸要求100%还原。


所以啊,有些只做过后端的人,可能会认为前端开发没啥东西。


好了,上面是现象分析。关于谁更有优越感,这不能细究啊,因为这是没有结论的。如果搞一个辩论赛,就比方说”Python一句话,汇编写三年“之类论据,各有千秋。各种语言既然能出现,必定有它的妙用。


我就是胡扯啊。不管您是哪个工种,请不要对号入座。如果您觉得被冒犯了,可以评论“啥都不懂”,然后离开。千万不要砸自己的电脑。


下面再聊聊第二部分,面对这种情况,有什么好的应对措施?


应对方法


我感觉,后端出身的负责人,就算是出于人际关系,也是会尊重前端开发的


“小张啊,对于前端我不是很懂。所以请帮我评估下前端的工期。最好列得细致一些,这样有利于我了解人员的工作量。弄完了,发给我,我们再讨论一下!”


一般都是这么做。


这种情况,我们就老老实实给他上报就行了,顶多加上10%~30%处理未知风险的时间。


但是,他如果这样说:“什么?这个功能你要做5天?给你1天都多!


这时,他是你的领导,对你又有考核,你怎么办?


你心里一酸:“我离职吧!小爷我受不了这委屈!”


这……当然也可以。


如果你有更好的去处,可以这样。就算是没有这回事,有好去处也赶紧去。


但是,如果这个公司整体还行呢?只是这个直接领导有问题。那么你可以考虑,这个领导是流水的,你俩处不了多长时间。哪个公司每年不搞个组织架构调整啊?你找个看上的领导,吃一顿饭,求个投靠呗。


或者,这个领导只是因为不懂才这样。谁又能样样精通呢?给他说明白,他可能就想通了。人是需要说服的。


如果你奔着和平友好的心态去,那么可以试试以下几点:


第一,列出复杂原因


既然你认为难以实现,那肯定是难以实现。具体哪里不好实现,你得说说


记得刚工作时,我去找后端PK,我问他:“你这个接口怎么就难改了?你不改这个字段,我们得多调好几个接口,而且还对应不起来!”


后端回复我:“首先,ES……;其次,mango……;最后,redis……”


我就像是被反复地往水缸中按,听得”呜噜呜噜“的,一片茫然。


虽然,我当时不懂后端,但我觉得他从气势上,就显得有道理


到后来,还是同样的场景。我变成了项目经理,有一个年轻的后端也是这么回复我的。


我说:“首先,你不用……;其次,数据就没在……;最后,只需要操作……”。他听完,挠了挠头说,好像确实可以这么做。


所以,你要列出难以实现的地方。比如,没有成熟的UI组件,需要自己重新写一个。又或者,某种特效,业内都没有,很难做到。再或者,某个接口定得太复杂,循环调组数据,会产生什么样的问题。


如果他说“我看到某某软件就是这样”。


你可以说,自己只是一个初级前端,完成那些,得好多高级前端一起研究才行。


如果你懂后端,可以做一下类比,比如哪些功能等同于多库多表查询、多线程并发问题等等,这可以辅助他理解。不过,如果能到这一步,他的位子好像得换你来坐。


第二,给出替代方案


这个方案,适用于”我虽然做不了,但我能解决你的问题“。


就比如那个经典的打洞问题。需求是用电钻在墙上钻一个孔,我搞不定电钻,但是我用锤子和钉子,一样能搞出一个孔来。


如果,你遇到难以实现的需求。比如,让你画一个很有特色的扇形玫瑰图。你觉得不好实现,不用去抱怨UI,这只会激化问题。咱可以给他提供几个业界成熟的组件。然后,告诉他,哪里能改,都能改什么(颜色、间距、字体……)。


我们有些年轻的程序员很直,遇到不顺心的事情就怼。像我们大龄程序员就不这样。因为有房贷,上有老下有小。年龄大的程序员就想,到底是哪里不合适,我得怎样通过我的经验来促成这件事——这并不光彩,年轻人就得有年轻人的样子。


第二招是给出替代方案。那样难以实现,你看这样行不行


第三,车轮战,搞铺垫


你可能遇到了一个硬茬。他就是不变通,他不想听,也不想懂,坚持“怎么实现我不管,明天之前要做完”。


那他可能有自己的压力,比如老板就是这么要求他的。他转手就来要求你。


你俩的前提可能是不一样。记住我一句话,没有共同前提,是不能做对比的。你是员工,他是领导,他不能要求你和他有一样的压力,这就像你不能要求,你和他有一样的待遇。多数PUA,就是拿不同的前提,去要求同样的结果。


那你就得开始为以后扯皮找铺垫了。


如果你们组有多个前端,可以发动大家去进谏。


”张工啊,这个需求,确实不简单,不光是小刘,我看了,也觉得不好实现“


你一个人说了他不信,人多了可能就信了。


如果还是不信。那没关系,已经将风险提前抛出了


“这个需求,确实不好实现。非要这么实现,可能有风险。工期、测试、上线后的稳定性、用户体验等,都可能会出现一些问题”


你要表现出为了项目担忧,而不是不想做的样子。如果,以后真发生了问题,你可以拿出“之前早就说过,多次反馈,无人理睬”这类的说辞。


”你居然不想着如何解决问题,反倒先想如何逃避责任?!“


因此说,这是下下策。不建议程序员玩带有心机的东西。


以上都是浅层次的解读。因为具体还需要结合每个公司,每个领导,每种局面。


总之,想要解决问题,就得想办法


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

Kotlin和Swift的前世一定是兄弟

iOS
Swift介绍 Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。&nbs...
继续阅读 »

Swift介绍


Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。 


 playground这个名字起的好,翻译成中文就是操场,玩的地方,也就是说,你可以尽情的测试你的Swift代码。


声明变量和常量


Kotlin的写法:

var a: Int = 10
val b: Float = 20f

Swift的写法:

var a: Int = 10
let b: Float = 20.0

你会发现它俩声明变量的方式一模一样,而常量也只是关键字不一样,数据类型我们暂不考虑。


导包


Kotlin的写法:

import android.app.Activity

Swift的写法:

import SwiftUI

这里kotlin和swift的方式一模一样。


整形


Kotlin的写法:

val a: Byte = -10
val b: Short = 20
val c: Int = -30
val d: Long = 40

Swift的写法:

let a: Int8 = -10
let b: Int16 = 20
let c: Int32 = -30
let d: Int = -30
let e: Int64 = 40
let f: UInt8 = 10
let g: UInt16 = 20
let h: UInt32 = 30
let i: UInt = 30
let j: UInt64 = 40

Kotlin没有无符号整型,Swift中Int32等价于Int,UInt32等价于UInt。无符号类型代表是正数,所以没有符号。


基本运算符


Kotlin的写法:

val a: Int = 10
val b: Float = 20f
val c = a + b

Swift的写法:

let a: Int = 10
let b: Float = 20
let c = Float(a) + b

Swift中没有隐式转换,Float类型不用写f。这里Kotlin没那么严格。


逻辑分支


Kotlin的写法:

val a = 65
if (a > 60) {
}

val b = 1
when (b) {
1 -> print("b等于1")
2 -> print("b等于2")
else -> print("默认值")
}

Swift的写法:

let a = 65
if a > 60 {
}

let b = 1
switch b {
case 1:
print("b等于1")
case 2:
print("b等于2")
default:
print("默认值")
}

Swift可以省略if的括号,Kotlin不可以。switch的写法倒是有点像Java了。


循环语句


Kotlin的写法:

for (i in 0..9) {
}

Swift的写法:

for var i in 0...9 {
}
// 或
for var i in 0..<10 {
}

Kotlin还是不能省略括号。


字符串


Kotlin的写法:

val lang = "Kotlin"
val str = "Hello $lang"

Swift的写法:

let lang = "Swift"
let str = "Hello \(lang)"

字符串的声明方式一模一样,拼接方式略有不同。


数组


Kotlin的写法:

val arr = arrayOf("Hello", "JYM")
val arr2 = emptyArray<String>()
val arr3: Array<String>

Swift的写法:

let arr = ["Hello", "JYM"]
let arr2 = [String]()
let arr3: [String]

数组的写法稍微有点不同。


Map和Dictionary


Kotlin的写法:

val map = hashMapOf<String, Any>()
map["name"] = "张三"
map["age"] = 100

Swift的写法:

let dict: Dictionary<String, Any> = ["name": "张三", "age": 100]

Swift的字典声明时必须初始化。Map和Dictionary的本质都是哈希。


函数


Kotlin的写法:

fun print(param: String) : Unit {
}

Swift的写法:

func print(param: String) -> Void {
}

func print(param: String) -> () {
}

除了关键字和返回值分隔符不一样,其他几乎一模一样。


高阶函数和闭包


Kotlin的写法:

fun showDialog(build: BaseDialog.() -> Unit) {
}

Swift的写法:

func showDialog(build: (dialog: BaseDialog) -> ()) {
}

Kotlin的高阶函数和Swift的闭包是类似的概念,用于函数的参数也是一个函数的情况。


创建对象


Kotlin的写法:

val btn = Button(context)

Swift的写法:

let btn = UIButton()

这里kotlin和swift的方式一模一样。


类继承


Kotlin的写法:

class MainPresenter : BasePresenter {
}

Swift的写法:

class ViewController : UIViewController {
}

这里kotlin和swift的方式一模一样。


Swift有而Kotlin没有的语法


guard...else的语法,通常用于登录校验,条件不满足,就执行else的语句,条件满足,才执行guard外面的语句。

guard 条件表达式 else {
}

另外还有一个重要的语法就是元组。元祖在Kotlin中没有,但是在一些其他编程语言中是有的,比如Lua、Solidity。元组主要用于函数的返回值,可以返回一个元组合,这样就相当于函数可以返回多个返回值了。
Swift的元组:

let group = ("哆啦", 18, "全宇宙最强吹牛首席前台")

Lua的多返回值:

function group() return "a","b" end

Solidity的元组:

contract MyContract {
mapping(uint => string) public students;

function MyContract(){
students[0] = "默认姓名";
students[1] = "默认年龄";
students[2] = "默认介绍";
}

function printInfo() constant returns(string,uint,string){
return("哆啦", 18, "全宇宙最强吹牛首席前台");
}
}

总结


编程语言很多地方都是相通的,学会了面向对象编程,你学习其他编程语言就会非常容易。学习一门其他编程语言的语法是很快的,但是要熟练掌握,还需要对该编程语言的API有大量的实践。还是那句话,编程语言只是工具,你的编程思维的高度才是决定你水平的重要指标。所以我给新入行互联网的同学的建议是,你可以先学习面向对象的编程思想,不用局限于一门语言,可以多了解下其他的编程语言,选择你更喜欢的方向。选择好后,再深耕一门技术。每个人的道不一样,可能你就更适合某一个方向。


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

一文看懂互联网大裁员底层逻辑

继谷歌、微软之后,Zoom、eBay、波音、戴尔加入最新一波“裁员潮”中。 2 月 7 日,美国在线会议平台 Zoom 宣布将裁减 1300 名员工,成为最新一家进行裁员的公司,大约 15% 的员工受到影响。 同日,总部位于亚特兰大的网络安全公司 Secure...
继续阅读 »

继谷歌、微软之后,Zoom、eBay、波音、戴尔加入最新一波“裁员潮”中。


2 月 7 日,美国在线会议平台 Zoom 宣布将裁减 1300 名员工,成为最新一家进行裁员的公司,大约 15% 的员工受到影响。


同日,总部位于亚特兰大的网络安全公司 Secureworks 在一份提交给美国证券交易委员会( SEC )的文件中宣布,将裁员 9%,因为该公司希望在“一些世界经济体处于不确定时期”减少开支。据数据供应商 PitchBook估计,该公司近 2500 名员工中约有 225 人将在此轮裁员中受到影响。


此外,电商公司eBay于2月7日在一份SEC文件中宣布,计划裁员500人,约占其员工总数的4%。据悉,受影响的员工将在未来24小时内得到通知。


2月6日,飞机制造商波音公司证实,今年计划在财务和人力资源部门裁减约 2000 个职位,不过该公司表示,将增加 1 万名员工,“重点是工程和制造”。


个人电脑制造商戴尔的母公司,总部位于美国德克萨斯州的戴尔科技,2 月 6 日在一份监管文件中表示,公司计划裁减约 5% 的员工。戴尔有大约 13.3 万名员工,在这个水平上,约 6650 名员工将在新一轮裁员中受到影响。


除了 Zoom、eBay、波音、戴尔 等公司,它们的科技同行早已经采取了同样的行动。


从去年 11 月开始,许多硅谷公司员工就开始增加了关注邮箱的频率,害怕着某封解除自己公司内网访问权限的邮件来临,在仅仅在 2022 年 11 月,裁员数字就达到了近 6 万人,而如果从 2022 开始计算,各家已经陆续裁员了近十五万人。



但本次裁员并非是因为营收的直接下降:事实上,硅谷各家 2022 年营收虽然略有下跌,但总体上仍然保持了平稳,甚至部分业务略有上涨,看起来并没有到「危急存亡之秋」,需要动刀进行大规模裁员,才能在寒冬中存活的程度。


相较于备受瞩目的裁员,一组来自美国政府的就业数据就显得比较有意思了。据美国劳工统计局 2 月 3 日公布的数据,美国失业率 1 月份降至 3.4%,为 1969 年 5 月以来最低。



美国 1 月份非农就业人数新增 51.7 万人,几乎是经济学家预期的三倍,即使最近主要在科技行业裁员,但建筑、酒店和医疗保健等行业带来了就业增长。


一方面是某些企业的大规模裁员,仅 1 月份就影响超过 10 万人;而另一方面,政府报告显示就业市场强劲。这样来看,美国的就业市场情况似乎有些矛盾。


2022 年 12 月初,多名B站员工在社交媒体上表示,B站开始了新一轮裁员,B端、漫画、直播、主站、Goods等部门均有涉及,整体裁员比例在30%左右。12月19日,小米大规模裁员的消息又有曝出,裁员涉及手机部、互联网部、中国部等多部门,个别部门裁员比例高达75%。12月20日,知乎又传裁员10%。


似乎全球的科技公司都在裁员,而我们想要讨论裁员的问题,肯定绕不开两个大方向:经济下行和人员问题。


下行环境


联合国一月发布的《2023年世界经济形势与展望》中指出,2022 年,一系列相互影响的严重冲击,包括新冠疫情、乌克兰战争及其引发的粮食和能源危机、通胀飙升、债务收紧以及气候紧急状况等,导致世界经济遭受重创。美国、欧盟等发达经济体增长势头明显减弱,全球其他经济体由此受到多重不利影响。与新冠疫情相关的反复封锁以及房地产市场的长期压力,延缓了中国的经济复苏进程。


在此背景下,2023 年世界经济增速预计将从 2022 年估计的 3.0% 下降至 1.9%。2024 年,由于部分不利因素将开始减弱,预计全球经济增速将适度回升至 2.7%。不过,这在很大程度上将取决于货币持续紧缩的速度和顺序、乌克兰战争的进程和后果以及供应链进一步中断的可能性。


在通货膨胀高企、激进的货币紧缩政策以及不确定性加剧的背景下,当前全球经济低迷,导致全球经济从新冠疫情的危机中复苏的步伐减缓,对部分发达国家和发展中国家均构成威胁,使其 2023 年可能面临衰退的前景。


2022 年,美国、欧盟等发达经济体增长势头明显减弱。报告预计,2023 年美国和欧盟的经济增速分别为 0.4% 和 0.2%,日本为 1.5%,英国和俄罗斯的经济则将分别出现 0.8% 和 2.9% 的负增长。


与此同时,全球金融状况趋紧,加之美元走强,加剧了发展中国家的财政和债务脆弱性。自 2021 年末以来,为抑制通胀压力、避免经济衰退,全球超过85%的央行纷纷收紧货币政策并上调利率。


报告指出,2022 年,全球通胀率约为 9%,创数十年来的新高。2023 年,全球通胀率预计将有所缓解,但仍将维持在 6.5% 的高水平。


据美国商务部经济分析局(BEA)统计,第二、三季度,美国私人投资分别衰退 14.1% 和 8.5%。加息不仅对美国企业活动产生抑制作用,而且成为美国经济复苏的最主要阻力。尤其是,非住宅类建筑物固定投资已连续六个季度衰退。预计 2023 年美国联邦基金利率将攀升至 4.6%,远远超过 2.5% 的中性利率水平,经济衰退风险陡增,驱动对利率敏感的金融、房地产和科技等行业采取裁员等必要紧缩措施。


发展上限


美国企业的业务增长和经营利润出现问题。据美国多家媒体报道,第三季度,谷歌利润率急剧下滑,Meta 等社交媒体的广告收入迅速降温,微软等其他科技企业业务增长也大幅放缓。自7月以来,美国服务业PMI已连续5个月陷入收缩区间,制造业 PMI 也于 11 月进入收缩区间。在美国经济前景和行业增长空间出现问题的背景下,部分行业采取裁员、紧缩开支等“准备过冬”计划也就在意料之中了。


2022年,在市值方面,作为中概股的代表阿里、腾讯、快手等很多企业的市值都跌了 50%,甚至70%、80%。在收入方面,BAT 已经停止增长几个季度了,阿里和腾讯为代表的企业已经开始负增长。在经济下行的背景下,向内开刀、降本增效成为企业生存的必然之举。除了裁员,收缩员工福利、业务调整,也是企业降本增效的举动之一。


如果说 2021 年的裁员,很多是由于业务受到冲击,比如字节跳动的教育业务,以及滴滴、美团等公司的社区团购项目。但到了 2022 年,更多企业裁员的背后是降本增效、去肥增肌。


全球宏观经济表现不佳,由产业资本泡沫引发的危机感传导到科技企业的经营层,科技企业不得不面对现实。科技行业处在重要的结构转型期。iPhone 的横空出世开创了一个移动互联网的新时代,而当下的科技巨头也都是移动互联网的大赢家。但十多年过去了,随着智能手机全球高普及率的完成,移动互联网的时代红利逐渐消失,也再没有划时代的创新和新的热点。


这两年整个移动互联网时代的赢家都在焦急地寻找新的创新增长点。比如谷歌和 Meta 多年来一直尝试投资新业务,如谷歌云、Web3.0等,但实际收入仍然依赖于广告业务,未能找到真正的新增长点。这使得其中一些公司容易受到持有突破性技术的初创公司影响。


科技企业倾力“烧钱”打造新赛道,但研发投入和预期产出始终不成正比,不得不进行战略性裁员。


我们这里以这两年爆火的元宇宙举例:


各大券商亦争相拥抱元宇宙,不仅元宇宙研究团队在迅速组建,元宇宙首席分析师也纷纷诞生。 2021 年下半年,短短半年内便有数百份关于元宇宙的专题研报披露。


可以说,在扎克伯格和Meta的带领下,全世界的大厂小厂都在跟着往元宇宙砸钱。


根据麦肯锡的计算,自2021年以来,全世界已经向虚拟世界投资了令人瞠目结舌的数字——1770亿美元。


但即使作为元宇宙领军的 Meta 现实实验室(Reality Labs)2022 年三季度收入 2.85 亿美元,运营亏损 36.7 亿美元,今年以来已累计亏损 94 亿美元,去年亏损超过 100亿 美元。显然,Meta 的元宇宙战略还未成为 Meta的机遇和新增长点。


虽然各 KOL 高举“元宇宙是未来”的大旗,依旧无法改写“元宇宙未至”的局面。刨除亟待解决的关键性技术问题,如何兼顾技术、成本与可行性,实现身临其境的体验,更是为之尚远。元宇宙还在遥远的未来。


早在 2021 年12 月底,人民日报等官方媒体曾多次下场,呼吁理性看待“元宇宙”。中央纪委网站发布的《元宇宙如何改写人类社会生活》提及“元宇宙”中可能会涉及资本操纵、舆论吹捧、经济风险等多项风险。就连春晚的小品中,“元宇宙”也成为“瞎忽悠”的代名词。


2022 年 2月18日,中国银保监会发布了《关于防范以“元宇宙”名义进行非法集资的风险提示》,并指出了四种常见的犯罪手法,包括编造虚假元宇宙投资项目、打着元宇宙区块链游戏旗号诈骗、恶意炒作元宇宙房地产圈钱、变相从事元宇宙虚拟币非法谋利。


2022 年 2月7日,英国《金融时报》报道称,随着《网络安全法案》逐步落实,元宇宙将会受到严格的英国监管,部分公司可能面临数十亿英镑的潜在罚款。


2022 年 2月6日,据今日俄罗斯电视台(RT)报道,俄罗斯监管机构正在研究对虚拟现实技术实施新限制的可能性,他们担心应用该技术可能会协助非法活动。


各个国家的法律监管的到来,使得元宇宙的泡沫迅速炸裂。无数的元宇宙公司迅速破产,例如白鹭科技从 H5 游戏引擎转型到元宇宙在泡沫破裂的情况下个人举债 4000 万,公司破产清算。


本质上来说如今互联网行业已经到了一个明显的发展瓶颈,大家吃的都是移动网络和智能手机的普及带来的红利。在新的设备和交互方式诞生前,大家都没了新故事可讲,过去的圈地跑马模式在这样的大环境下行不通了。


法律监管


过去十年时间,互联网世界的马太效应越来越明显。一方面,几大巨头们在各自领域打造了占据了主导份额的互联网平台,不断推出包罗万象的全生态产品与服务,牢牢吸引着绝大多数用户与数据。他们的财务业绩与股价市值急剧增长,苹果、谷歌、亚马逊的市值先后突破万亿甚至是两万亿美元。


而另一方面,诸多规模较小的互联网公司却面临着双重竞争劣势。他们不仅财力与体量都无法与网络巨头抗衡,还要在巨头们打造的平台上,按照巨头制定偏向自己的游戏规则,与包括巨头产品在内的诸多对手激烈竞争用户。


2020 年 10 月,在长达 16 个月的调查之后,美国众议院司法委员会发布了一份长达 449 页的科技反垄断调查报告,直指谷歌、苹果、Facebook、亚马逊四大科技巨头滥用市场支配地位、打压竞争者、阻碍创新,并损害消费者利益。


2020 年 10 月 20 日,美国司法部连同美国 11 个州的检察长向 Google 发起反垄断诉讼,指控其在搜索和搜索广告市场通过反竞争和排他性行为来非法维持垄断地位。


2021 年明尼苏达州民主党参议员艾米·克洛布查尔(Amy Klobuchar)和爱荷华州共和党参议员查克·格拉斯利(Chuck Grassley)共同提出的《美国创新与选择在线法案》和 《开放应用市场法案》旨在打击谷歌母公司 Alphabet、亚马逊、Facebook 母公司 Meta 和苹果公司等科技巨头的一些垄断行为,这将是互联网向公众开放近30年来的首次重要法案。


《美国创新与选择在线法案》的内容包括禁止占主导地位的平台滥用把关权,给予营产品服务特权,使竞争对手处于不利地位;禁止施行对小企业和消费者不利,有碍于竞争的行为,例如要求企业购买平台的商品或服务以获得在平台上的优先位置、滥用数据进行竞争、以及操纵搜索结果偏向自身等。


不公平地限制大平台内其他商业用户的产品、服务或业务与涵盖平台经营者自己经营的产品、服务或业务相竞争能力,从而严重损害涵盖平台中的竞争。


除了出于大平台安全或功能的需要,严重限制或阻碍平台用户卸载预装的软件应用程序,将大平台用户使用大平台经营者提供的产品或服务设置为默认或引导为默认设置。


《开放应用市场法案》针对“守门人”执行,预计将会在应用商店、定向广告、互联操作性,以及垄断并购等方面,对相应企业做出一系列规范要求。此外欧盟方面还曾透露,如“守门人”企业不遵守上述规则,将按其上一财政年度的全球总营业额对其处以“不低于 4%、但不超过20%”的罚款。法案允许应用程序侧载(在应用商店之外下载应用程序),旨在打破应用商店对应用程序的垄断能力,将对苹果、谷歌的应用商店商业模式产生重要影响。


大型科技公司们史无前例搁置竞争,并且很有默契地联合起来。他们和他们的贸易团体在两年内耗费大约 1 亿美元进行游说,超过了制药和国防等高支出行业。他们向政界人士捐赠了 500 多万美元,科技游说人士向负责捍卫民主党多数席位的政治行动委员会(PAC)捐赠了 100 多万美元。他们还向不需要披露资金来源的黑钱组织、非营利组织和行业协会投入了数百万美元。几位国会助手表示,他们收到的有关这些法案的宣传比他们多年来处理的任何其他法案都要多。


这两项法案已通过国会相关委员会的审查,依然在等待众议院和参议院的表决。而美国即将开始中期选举。Deese 称,共和党已经明确表示,如果共和党重新控制国会两院,他们将不会支持这些法案。但如果民主党当选的话,科技巨头们估计不好过了。


很遗憾的是,2023年,新一届美国国会开幕后,众议院议长的选举经多轮投票仍然“难产”,导致新一届国会众议院无法履职。开年的这一乱象凸显美国政治制度失灵与破产,警示美国党争极化的趋势恐正愈演愈烈;


欧盟也多次盯上四大公司,仅谷歌一家,欧盟近三年来对其开出的反垄断处罚的金额已累计超过 90 亿美元。


而中国的举措也不小。


2020 年年初,实施了近 12 年的《反垄断法》(2008 年 8 月 1 日生效)首次进入“大修”——国家市场监督管理总局在其官网公布了《反垄断法修订草案(公开征求意见稿)》(以下简称“征求意见稿”)。


《法制日报》报道指出,征求意见稿中共有 8 章 64 条,较现行法要多出 7 条。可见,这次修法,已与另立新法有同等规模。


值得注意的是,征求意见稿还首次将互联网业态纳入其中,新增互联网领域的反垄断条款,针对性地列明相关标准和适用规程。


以市场支配地位认定为例,征求意见稿根据互联网企业的特点,新增了包括网络效应、规模经济、锁定效应、掌握和处理相关数据的能力等因素。


11 月 10 日,赶在双 11 前一天,国家市场监管管理总局再次出手,发布了《关于平台经济领域的反垄断指南(征求意见稿)》(以下简称《指南》)公开征求意见的公告。


《指南》不仅对“互联网平台”做了进一步界定,还结合个案更为具体详尽地对垄断协议,滥用市场支配地位行为,经营者集中,滥用行政权力排除、限制竞争四个方面作出分析和规定。


国家在平台经济领域、反垄断领域的法律规范,在《反垄断指南》出台以后,已经有了相当程度的完善。后续随着《反垄断法》修正案的通过,二者结合基本构建了我国反垄断领域的法律框架。


随着《反垄断法》的完善,在互联网领域的处罚案例逐渐浮出水面,针对阿里巴巴、美团等互联网公司都开出了大额罚单。


2021年我国在网络安全方面也加速发展。2021年6月10日颁布《中华人民共和国数据安全法》,2021年8月20日颁布《中华人民共和国个人信息保护法》。有关部门相继出台了《网络安全审查办法》《常见类型移动互联网应用程序必要个人信息范围规定》《数据出境安全评估办法(征求意见稿)》等部门规章和政策性文件。


可以预见的是,未来监管部门的监管措施更能兼顾互联网行业发展特征和社会整体福利,监管部门会不断完善规章、政策文件和标准文件,提供给企业明确和细化的指引。同时,相关部门的监管反应速度会越来越及时,监管层面对违法查处的力度也会越来越严。


人口红利


我们依然处在人口规模巨大的惯性中,人口规模巨大意味着潜在市场规模巨大,伴随经济持续发展、收入水平提高、消费能力强劲,由此带来的超大市场规模不可估量。而现在人口红利没了。


中国国家统计局 1 月 17 日公布,2022年末全国人口(包括 31 个省、自治区、直辖市和现役军人的人口,不包括居住在 31 个省、自治区、直辖市的港澳台居民和外籍人员) 141175 万人,比上年末减少 85 万人。这是近61年来中国首次人口负增长。人口负增长的早期阶段是一种温和的人口减少,所以依然会沿袭人口规模巨大的惯性;但在人口负增长的远期阶段,如果生育率仍未有所回升的话,就有可能导致一种直线性的减少。


目前所有行业都不得不面临从人口红利转向素质红利的转变。


人员过剩


微软在过去两年员工数新增 6 万,Google 则是新增了 7 万,Meta 则是直接从疫情之前的 4 万翻倍至 2022 年的 8.7 万。而依赖物流服务的亚马逊则最为激进,两年时间全球全职员工数增长了令人咂舌的 8.1 万,全职员工数近乎翻倍。



高盛的经济学家在一份报告中指出“那些正在裁员的科技公司有一些共同点,希望重新平衡业务的结构性转变,并为更好的利润开路。我们发现,许多最近宣布大规模裁员的公司都有三个共同特征。首先,许多都是在科技领域。其次,许多公司在疫情期间大肆招聘。第三,它们的股价出现了更大幅度的下跌,从峰值平均下跌了 43%。”


平均而言,那些进行裁员的公司在疫情期间的员工数量增长了 41%,此举往往是因为他们过度推断了与疫情相关的趋势,比如商品需求或在线时间的增长。


行裁员的公司并不能代表更广泛的情况,最近许多裁员公告并不一定意味着需求状况会减弱。与此一致的是,高盛预计更具代表性的实时估计的裁员率最近虽有所增加,但仅恢复到疫情前的水平,以历史标准衡量,裁员率水平较低。


结论


全球经济下行是大势,层层增加的法律监管是推动,没有人口红利和新玩法股价要大跌。


全球通胀激增,激进的货币紧缩政策以及不确定性加剧、俄乌战争等影响,全球经济低迷。新冠疫情带来的影响难以快速恢复。而中国还得面临人口红利消失、房地产饮鸩止渴的深远影响。而法律的层层监管和反垄断的推进在逐步打压科技巨头的已有市场,没有新技术的突破和新玩法让科技巨头们也没了新增和突破的空间。对于未来的经济发展的错误预估和疫情特殊时期的大量增长让科技巨头们大肆招聘,这些都成为了股价下跌和缩减利润的元凶。目前的大裁员可以算是一种虚假繁荣的泡沫爆裂后的回调,虽然不知道这个回调什么时候结束,但是随着人工智能的出圈和将来新技术的突破,也许整个行业可以浴火重生。


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

慎重选择~~第四家公司刚刚开我,加入重新找工作大队!!!

前景需知 这家公司是我的第四家公司,合同三年,6个月试用期,(当时入职时,谈过说可以提前转正,但是后续当作没这件事),然后7月25日,下午5点半,下班时候告诉我被开了。当天是我手上的一个新项目刚好完结,测试刚过,bug修复完毕,老板让人事通知我,被开了,说是没...
继续阅读 »

前景需知


这家公司是我的第四家公司,合同三年,6个月试用期,(当时入职时,谈过说可以提前转正,但是后续当作没这件事),然后7月25日,下午5点半,下班时候告诉我被开了。当天是我手上的一个新项目刚好完结,测试刚过,bug修复完毕,老板让人事通知我,被开了,说是没有新的项目了。当时我算了算应该是还有几天就转正了。


在职期间


总共是在职6个月差几天转正,期间一直是大小周,说是双休,加班没有任何补偿,然后9点到5.30.(从来没有5点半下班过,最早就是6点半吧,5点半下班会打电话给你,问你为啥下班那么早).然后在这家公司这么久,手上是写了3个新项目,翻新2个老项目,还有维护的。期间没有任何违纪行为,这肯定是一定的,不然也不会等到还有几天才把我开了。在职期间做的事,跟产品沟通为什么不能这么写,用户怎么交互比较合理,不必太过于麻烦,给后端沟通为什么要这个数据,为什么要这样,还要跟老板说 进度怎么样的,预计时间。因为没有测试,是所有员工用了以后提一个bug单,到我这里来,然后我统一看这是谁的问题,然后我去沟通,加上公司内部人员测试,很多东西产品出成那样,觉得不合理,也要给我,我去跟产品沟通,真是沟通成本大的要死,期间有一个要对接别人的app里的积分系统,对公到我们的积分体系里,还要我去对接,这不能找后端嘛?产品又甩给我了,最后又要我去跟第三方沟通,再给自己的后端沟通,成本是真的高啊,我真是有时候头大。听着有点小抱怨,但是吧,其实后面了还好,确实能让你学到很多东西,因为你很清楚这个项目的走向,以及问题,基本上所有东西有点围绕着前端做的感觉,反正每天都是被问,问到最后,无论是谁张嘴我都知道是什么个情况。反正学着接受就好了。


为什么会来到这家公司??


这家公司是我去年面过的一家公司,当时入职他们公司一天我就走了,为什么会走,就是因为代码累积,页面过于卡顿,前端没有任何标注,而且入职第一天,老板就要求改他们的东西,然后第二天就没去了,为什么今年去了,是因为去年这个老板也联系了我几次,说我可以去他们公司试试看,然后过年的前两天还在跟我说,我说那就去试试看看,然后年后那个老板也催着我入职,当时也不是没得选,朋友公司招人内推,他面我,说让我去。我当时主要是跟这个老板说好了,答应了,于是就回绝了我的朋友(真后悔啊,那是真后悔,真不如去朋友哪里了,现在还被开了,卸磨杀驴,我真气)。


在公司半年,我具体做了哪些东西


上面说做了3个新项目,翻新两个新项目。三个新项目是一个是可视化大屏项目,这个项目用的是(vue3加echarts,适配是用v-scae-screen这个组件做的,当然这时候就有人会问,你用这个组件 那其他屏幕的除了你用的这个分辨率,其他比例不对的分辨率,也会有问题,当然这个问题我也遇到了,但是也试了其他的几种方案,但是或多或少都有问题,所以我就选择了这个比较直接.原理## transform.scale(),更详细的可以看看这个组件。)还有一个是小程序的老师端批改作业,并给予点评。(uni-app加uview写的,这个直接上图片,有难点)



 第三个项目也是uni-app写的,就是刚刚写完这个项目我被开了,真是太离谱了。也是一个小程序(uni-app加uview,然后益智类的,可以直接搜索头脑王者这个小程序,基本上是功能还原。不贴我的项目图了,好像我走的第二天就在审核了,主要是websocket长连接写的,因为是对战类,所以长连接时时保持通讯,也是有难点的,因为长连接要多页面保持又要实时获取信息,可以想一下怎么做)。 翻新的项目就不谈了,算是整个翻新,翻新是最累的,因为有的能用有的不能用,该封装封装,数据该处理处理,哦,中间遇到一个有趣的问题,就是el-tabs这个缓存机制,不知道为啥,v-if也不行.


目前的看法


7月25下午被开当天其实我很痛苦,当时人事说话也很过分,让我自己签申请离职说,这样的话赔偿你 0.5,如果不行,你可以去仲裁我们,然后如果我去仲裁,那么离职单,离职证明,赔偿,工资都没有,就拖着你,甚至老板恶言相向的告诉人事说,怎么可以在他的工作简历上留下这个不好的痕迹,影响他以后的工作。其实我听到这些话的时候我除了恶心,我什么话都说不出来,面对这个种情况,我咨询了,12333他们说,让我照常上班,他把你提出打开的软件,你就手动拍摄视频,然后自己打开,直至出示他把你辞退的证明,或者待够15天。我把这个事情实施以后,并且告知公司,仍然不给我出示离职证明,出了一张,辞退通知书,这个通知书我直接上图片,首先这个假,是个病假,是因为后端对我进行了侮辱,然后导致我气的头疼,然后我去请假,是给领导直接请的,她允许以后,我才中午下班是,离开的公司。 


为什么会给后端吵架,因为后端不处理逻辑,还要怪我什么都不给他说,什么都不给讲,这是我最气的点,我每次都要给他讲,为什么需要这个数据,为什么你要这么给我,需要什么,我每次都在他没写之前就进行沟通。他最后怪我没讲,并且侮辱我。有的人这时候会说,你为什么不他给你什么就要什么呢?然后自己处理逻辑。降低了耦合性,再往后说 你自己可以写一个node.js啊 为什么不呢?这些都挺对的,但是吧,你不能每次都这么处理问题吧。一个选择题,他应该给你abcd,结果给你1234,然后他要abcd,你说这个转换你做不做?你好说歹说他给你改了,然后一道题4个选项 我回答完以后,他给你答案你自己判断对错,这个逻辑前端写吗,当然也可以,如果他给你的答案是 1呢 1就是a,这时候你又该如何是好?可能你觉得我不信后端会这个对你,一定是你的问题,哈哈 上图片



 

 是的没有错,我来教着写,这个时候大家可以喷我了,可以说,你怎么交后端写,你算什么东西,兄弟们,兄弟们,都是我的问题,实在是没办法了,写出了这样得东西 这个东西还能精简,这是只是我为了实现而写得逻辑。




反正一吐为快,目前是没找工作,下周找找看吧,缓解一下。


当下迷茫得点


希望大家给点建议,就是说因为没有遇到一个好的产品导致我现在想去做产品,我直接现在转产品工资会有一个大跳水,会少很多,但是我也愿意接受,可能是赌气吧,就真的想去做这个,让开发没那么难以沟通。也在想是不是继续前端,保持现状,但是就是想去转产品了,我现在24岁,前端3年多,我应该还有试错得机会,我真的不想在碰见这种情况了,真的好累,加上只是前端,人微言轻,只有出现问题,提出来的东西,才能被采纳,真的好难。所以我是有意愿转转看的,不知道各位怎么看?能评价就评价下,需要我爆雷得,我私信,他们目前好像又在招前端了,怕大家踩雷,在上海。


给大家得建议


就是入职前,还是要好好调查,然后不要只听片面之言,然后就是现状不好的,也不要气馁,就加油好吧,我都没气馁,顶住压力啊,还是希望大家吃好喝好玩好,生活美满。


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

iOS 开发中如何禁用第三方输入法

iOS
iOS 目前已允许使用第三方输入法,但在实际开发中,无论是出于安全的考虑,还是对某个输入控件限制输入法,都有禁用第三方输入法的需求。基于此,对禁用第三方输入法的方式做一个总结。 1. 全局禁用 Objective-C 语言版本:- (BOOL)applicat...
继续阅读 »

iOS 目前已允许使用第三方输入法,但在实际开发中,无论是出于安全的考虑,还是对某个输入控件限制输入法,都有禁用第三方输入法的需求。基于此,对禁用第三方输入法的方式做一个总结。


1. 全局禁用


Objective-C 语言版本:

- (BOOL)application:(UIApplication *)application
shouldAllowExtensionPointIdentifier:(UIApplicationExtensionPointIdentifier)extensionPointIdentifier
{
// 禁用三方输入法
// UIApplicationKeyboardExtensionPointIdentifier 等价于 @"com.apple.keyboard-service"
if ([extensionPointIdentifier isEqualToString:UIApplicationKeyboardExtensionPointIdentifier]) {
return NO;
}
return YES;
}

Swift 语言版本:

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 禁用三方输入法
if extensionPointIdentifier == .keyboard {
return false
}
return true
}

2. 针对某个视图禁用

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 遍历当前根控制器的所有子控制器,找到需要的子控制器
for vc in self.window?.rootViewController?.childViewControllers ?? []
      where vc.isKind(of: BaseNavigationController.self)
{
// 如果首页禁止使用第三方输入法
for vc1 in vc.childViewControllers where vc1.isKind(of: HomeViewController.self) {
      return false
    }
  }
return true
}

3. 针对某个 inputView 禁用


3.1 自定义键盘


如果需求只是针对数字的输入,优先使用自定义键盘,将 inputView 绑定自定义键盘,不会出现第三方输入法。


3.2 遍历视图内控件,找到需要设置的 inputView,专门设置

func application(
_ application: UIApplication,
shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier
) -> Bool {
// 遍历当前根控制器的所有子控制器,找到需要的子控制器
for vc in self.window?.rootViewController?.childViewControllers ?? []
      where vc.isKind(of: BaseNavigationController.self)
{
// 如果想要禁用的 inputView 在首页上
for vc1 in vc.childViewControllers where vc1.isKind(of: HomeViewController.self) {
// 如果 inputView.tag == 6 的 inputView 禁止使用第三方输入法
      for view in vc1.view.subviews where view.tag == 6 {
      return false
      }
    }
  }
return true
}

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

简历中不写年龄、毕业院校、预期薪资会怎样?

无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。 之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推...
继续阅读 »

无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。


之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推导,那么这些事实和观点是不足信的,需要慎重对待。


视频的内容是这样的:“不管你有多自信,简历中的个人信息都不要这样写。1、写了期望薪资,错!2、写了户籍地址,错!3、写了学历文凭,错!4、写了离职原因,错!5、写了生日年龄,错!6、写了自我评价,错!


正确写法,只需要写姓名和手机号、邮箱及求职意向即可,简历个人信息模块的作用是让HR顺利联系到你,所有任何其他内容都不要写在这里……”


针对这条视频的内容,有两个不同的表现:第一就是分享和点赞数量还可以,都破千了;第二就是评论区很多HR和求职着提出了反对意见。


第一类反对意见是:无论求职者或HR都认为这样的简历是不合格的,如果不提供这些信息,根本没有预约面试的机会,甚至国内的招聘平台的简历模板都无法通过。第二类,反对者认为,如果不写这些信息,特别是预期薪资,会导致浪费双方的时间。


针对上述质疑,作者的回复是:”看了大家的评论,我真的震惊,大家对简历的误解是如此至深……“


仔细看完视频和评论,在视频的博主和评论者之间产生了一个信息差。博主说的”个人信息“不要写,给人了极大的误导。是个人信息栏不要写,还是完全不写呢?看评论,大多数人都理解成了完全不写。博主没有说清楚是不写,还是写在别处,这肯定是作者的锅。


本人也筛选过近千份简历,下面分享一下对这则视频中提到的内容的看法:


第一,户籍、离职原因可以不写


视频中提到的第2项和第4项的确可以不写。


户籍这一项,大多数情况下是可以不写的,只用写求职城市即可,方便筛选和推送。比如,你想求职北京或上海的工作,这个是必须有的,而你的户籍一般工作没有强制要求。但也有例外,比如财务、出纳或其他特殊岗位,出于某些原因,某些公司会要求是本地的。写不写影响没那么大。


离职原因的确如他所说的,不建议写,是整个简历中都不建议写。这个问到了再说,或者填写登记表时都会提到,很重要,要心中有准备,但没必要提前体现。


第二,期望薪资最好写上


关于期望薪资这个有两种观点,有的说可以不写,有的说最好写上。其实都有道理,但就像评论中所说:如果不写,可能面试之后,薪资相差太多,导致浪费了双方的时间。


其实,如果可以,尽量将期望薪资写上,不仅节省时间,这里还稍微有一个心理锚定效应,可以把薪资写成范围,而范围的下限是你预期的理想工资。就像讨价还价时先要一个高价,在简历中进行这么一个薪资的锚定,有助于提高最终的薪资水平。


第三,学历文凭一定要写


简历中一定要写学历文凭,如果没有,基本上是会默认为没有学历文凭的,是不会拿到面试邀约的。仔细想了一下,那则视频的像传达的意思可能是不要将学历文凭写作个人信息栏,而是单独写在教育经历栏中。但视频中没有明说,会产生极大的误导。


即便是个人信息栏,如果你的学历非常漂亮,也一定要写到个人信息栏里面,最有价值,最吸引眼球的信息,一定要提前展现。而不是放在简历的最后。


第四,年龄要写


视频中提到了年龄,这个是招聘衡量面试的重要指标,能写尽量写上。筛选简历中有一项非常重要,就是年龄、工作经历和职位是否匹配。在供大于求的市场中,如果不写年龄,为了规避风险,用人方会直接放弃掉。


前两个月在面试中,也有遇到因为年龄在30+,而在简历中不写年龄的。作为面试官,感觉是非常不好的,即便不写,在面试中也需要问,最终也需要衡量年龄与能力是否匹配的问题。


很多情况下,不写年龄,要么认为简历是不合格的,拿不到面试机会,要么拿到了面试机会,但最终只是浪费了双方的时间。


第五,自我评价


这一项与文凭一样,作者可能传达的意思是不要写在个人信息栏中,但很容易让人误解为不要写。


这块真的需要看情况,如果你的自我评价非常好,那一定要提前曝光,展现。


比如我的自我评价中会写到”全网博客访问量过千万,CSDN排名前100,出版过《xxx》《xxx》书籍……“。而这些信息一定要提前让筛选简历的人感知到,而不是写在简历的最后。


当然,如果没有特别的自我评价,只是吃苦耐劳、抗压、积极自主学习等也有一定的积极作用,此时可以考虑放在简历的后面板块中,而不是放在个人信息板块中。这些主观的信息,更多是一个自我声明和积极心态的表现。


最后的小结


经过上面的分析,你会看到,并不是所有的结论都有统一的标准的。甚至这篇文章的建议也只是一种经验的总结,一个看问题的视角而已,并不能涵盖和适用所有的场景。而像原始视频中那样,没有分析,没有推导,没有数据支撑,没有对照,只有干巴巴的结论,外加的煽动情绪的配音,就更需要慎重对待了。


在写这篇文章的过程中,自己也在想一件事:任何一个结论,都需要在特定场景下才能生效,即便是牛顿的力学定律也是如此,这才是科学和理性的思维方式。如果没有特定场景,很多结论往往是不成立的,甚至是有害的。


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

OC项目用Swift开发方便吗?

iOS
前言 公司有个项目一直是用 OC 进行开发,现在想改成 Swift 来开发。那先说一下为什么有这样的想法,我们都知道 Swift 代码更简单,易维护,安全而且快,网络上也是很多描述,那我们主要的是担心一旦变成混编工程,会不会出现很多问题,还有如何解决这些问题。...
继续阅读 »

前言


公司有个项目一直是用 OC 进行开发,现在想改成 Swift 来开发。那先说一下为什么有这样的想法,我们都知道 Swift 代码更简单,易维护,安全而且快,网络上也是很多描述,那我们主要的是担心一旦变成混编工程,会不会出现很多问题,还有如何解决这些问题。性能问题方面Swift 和 OC 共用一套运行时环境,而且支持 Swift 桥接 到 OC上,所以呢,问题不大。如果有不同的想法,也欢迎留意指教。


桥接文件


我们只要在 OC 项目中,创建一个 swift 文件,系统就会弹出桥接文件,我们点击 "Create Bridging Header"即可。




OC 工程接入 Swift


OC 类 引用 Swift 类


如上面我们创建了一个 swift 文件,里面写一些方法提供给 OC 使用。

@objcMembers class SwiftText: NSObject {

func sayhello() -> String{

return "hello world"

}
}

class SwiftText2: NSObject {

@objc func sayhello() ->String{

returnOCAPI.sayOC()

}
}

这里我们有关键字2个,1个是@objcMembers,表示所有方法属性都可以提供给 OC 使用。另外一个是@objc,表示修饰的方法属性才可以提供给OC使用。


那我们 OC 类怎么用这个 swift 文件呢。
先在我们该类添加头文件

#import "项目Target-Swift.h"

然后我们点进去看下。




可以看到我们写的 swift 文件类,方法,属性,都被转化为 OC 了,有了这个我们直接使用即可。


OC类 使用 swift Pod库


说实话,这种用的比较少,但有时候我们真的觉得 swift Pod库 会更好用,那我们怎么去处理呢?


首先我们要搞懂一点,有些是支持使用的,如PromiseKit,有些是不支持使用的如Kingfisher


先说第一种支持使用的,我们直接导入#import <PromiseKit/PromiseKit.h>即可。


那要是第二种的话,我们还有一种办法,就是先用 swift 写一个该库管理类,然后里面引用我们该库的内容,我们通过 @objc 来提供给我们 OC 使用。


Swift类 引用 OC 类


如果我们编写的 Swift 类,想要用到 我们 OC 的方法,那我们如何处理呢?


我们直接在桥接文件"Target-Bridging-Header.h"里面,直接导入头文件#import "XXX.h"即可使用。


Swift类 使用 OC pod库


其实这个更简单,和 Swift 工程引入 OC pod库一样,在该类里面导入头文件即可。

import MJRefresh

遇到问题


问题1:引入swift pod库 问题


如果我们 OC 项目 是没有 使用use_frameworks!。那我们导入swift Pod库 就会报错。


那我们就在工程配置里面 Build Settings里面,搜索 Defines Module, 更改为 YES 即可。




问题2:OC 类继承问题


OC的类是不能继承至Swift的类,但Swift 类是可以继承 OC类的,其实方式也是"Target-Bridging-Header.h"导入头文件即可。


问题3:宏定义问题


我们自己重新一份
原来的是

#define kScreenWidth        [UIScreen mainScreen].bounds.size.width                      
#define kScreenHeight [UIScreen mainScreen].bounds.size.height

现在的是

let kScreenWidth = UIScreen.main.bounds.width
let kScreenHeight = UIScreen.main.bounds.height

有一些,我们可以定义问方法来替代宏。


问题4:OC经常调用swift库导入问题


我们知道xxx-Swift.h都是包含所有swift 提供给 OC 使用的类,所以我们可以把xxx-Swift.h放到 pch 文件里面,就可以在任意一个 OC 工程文件直接调用 swift 类。


OC 在线转为 swift


提供一个链接,可以支持 OC 转为 swift。
在线链接


最后


经过上面的总结,OC 项目 使用 swift 开发 的确是问题不大,使用过程中可能也会遇到编译问题,找不到文件问题,只要细心排查,也是很容易解决,那等后续项目用上正轨,还会把遇到的坑填补上来,如有不足,欢迎指点。


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

鸿蒙终于不套壳了?纯血 HarmonyOS NEXT 即将到来

对于移动开发者来说,特别是 Android 开发而言,鸿蒙是不是套壳 Android 一直是一个「热门」话题,类似的问题一直是知乎的「热点流量」之一,特别是每次鸿蒙发布新版本之后,都会有「套娃式」的问题出现。 例如最近 HDC 刚发布了鸿蒙 4.0 ,但是问题...
继续阅读 »

对于移动开发者来说,特别是 Android 开发而言,鸿蒙是不是套壳 Android 一直是一个「热门」话题,类似的问题一直是知乎的「热点流量」之一,特别是每次鸿蒙发布新版本之后,都会有「套娃式」的问题出现。


例如最近 HDC 刚发布了鸿蒙 4.0 ,但是问题已经提到了 6.0 ,不过也算是误打误撞,在 4.0 发布之后,华为宣布了 HarmonyOS NEXT 版本



HarmonyOS NEXT 在 2023 年 8 月 6 日开始面向合作企业开发者开放,2024 年第一季度面向所有开发者开放,也就是明年开始,更新后的鸿蒙,会使用全自研内核,去掉了传统的 AOSP 代码,仅支持鸿蒙内核和鸿蒙系统的应用,减少了 40% 的冗余代码,使系统的流畅度、能效、纯净安全特性大为提升




也就是说,你的 Android APK 已经不能在 HarmonyOS NEXT 上运行,因为系统已经不存在 AOSP 代码,甚至没有 JVM 。



虽然我们一直在吐槽鸿蒙套壳,但是这波华为终于是打算「釜底抽薪」,靠着 AOSP 「养住」开发者生态之后,这次终于开始「杀鸡取卵」。



这里不得不提到「纯血」这个词,虽然华为在此之前的宣传口径一直是纯国产自研,但是看来华为自身还是清楚里面的「血统不纯」,而这次决定「大换血」,“减少了 40% 的冗余代码” 的说法,就很有意思。




但是其实对于开发者来说,特别是移动端开发者来说,其实这是好事,我不从商业角度考虑,仅仅是从开发者生态考虑,因为移动端现在已经好久没有新活跃了,HarmonyOS NEXT 的全新适配工作应当大部分会落在 Android 开发上,或者说是否会新增全新的 HarmonyOS 开发岗位?



主要是转化的门槛不高,不过第一批吃螃蟹的,网上的资料肯定会相不足。




在全新的开发框架下, HarmonyOS NEXT 会采用全新自研的 ArkTS 和 ArkUI ,从目前看来,也就是你可能再也不能使用 Java 开发鸿蒙应用了,并且 ArkTS 是直接采用 AOT 编译优化,所以渲染模式可能会更接近 Flutter 和 Compose 的结构情况。




事实上从目前的文档和开发体验上看,控件结构和开发模式十分贴近 Flutter 和 Compose ,这对于相关领域的开发者来说可以说是能力加强,所以目前对于 HarmonyOS NEXT 来说,未来的生态适配难度会进一步降低。





即有适配负担,又有工作机遇,新技术领域代表存在新的红利,至少华为走在了 App 端「原生纯响应式开发」的前沿。



目前,华为已经从设计资源,编程语言,编译器到开发工具、调测工具实现全面升级,HarmonyOS SDK 升级至 API 10 端云一体,可以一次性集成。


另外一点是关于 ArkUI 的跨平台,这一点类似于苹果生态的一次开发多端部署,采用自研的 「方舟图形渲染」, HarmonyOS 也实现了类似手机,平板和电脑的统一「跨平台」效果。






目前猜测还是会机遇 Skia 底层支持。



最后就是大家关心的 HarmonyOS NEXT 会不会和 WPhone 一样遭遇滑铁卢,目前看来华为之前的技术积累和开发者关系运营的还不错:



根据 HDC 最新数据,鸿蒙生态的设备数量目前已超过 7 亿,已有 220 万 HarmonyOS 开发者投入到鸿蒙世界的开发中,API 日调用 590 亿次,软硬件产品超过 350 款。




华为鸿蒙 SDK 这些年确实沉淀了一部分开发者,虽然实际多少不清楚,但是这让鸿蒙 Next 不是从 0 开始,另外目前也有部分企业开始主动适配鸿蒙,并且华为提出了全新的鸿飞计划,在 3 年时间里投入 100 亿元资金支持鸿蒙生态建设



所以短期可能会有阵痛,但是 HarmonyOS NEXT 的基础其实挺好,不管是类似 Flutter/ Compose 的开发方式,还是原本已经存在的开发者基础,更有相关的政策扶持,很难看出鸿蒙会在明年遭遇滑铁卢的情况。



其实到这里我有个疑问,那就是 HarmonyOS NEXT 的生态会不会支持侧载,这决定了 HarmonyOS NEXT 之后的生态发展路线。



如果必须上架商店才能分发,这又是另外一个故事了。



最后就是现阶段的框架,例如 React Native 和 Flutter 能不能跑?官方目前已经有相关适配支持,目前消息上看:



  • RN 相关适配已经完成 60%

  • 游戏相关如 Unity 引擎,如前面提到过的新闻,其实游戏适配是最容易的

  • 最后 Flutter ,目前看来 Flutter For HarmonyOS 应该需要有好心社区进行适配




让我们最后一起期待纯血的鸿蒙可以走多

作者:恋猫de小郭
来源:juejin.cn/post/7264237761158643773
远。


收起阅读 »

最强实习生!什么?答辩刚结束,领导就通知她转正成功了?

文章目录 写在前面 灵魂三问 第一问,你了解转正流程吗? 第二问,实习期间的我为团队做了什么? 第三问,基础知识还记得吗? 一个日常实习阶段小tip 写在最后 FAQ时间 写在前面 熟知我的人应该都知道我是实习转正上岸字节的。 那是一个平平淡淡的下...
继续阅读 »

文章目录



写在前面


熟知我的人应该都知道我是实习转正上岸字节的。


那是一个平平淡淡的下午,leader突然神神秘秘凑到我身边:“一恩,快秋招了。我给你预留了一个HC,快准备准备转正答辩吧。”

于是乎,伴随着leader自以为充满关怀的安排下,我开始轰轰烈烈筹备起自己的转正大业。


和很多小伙伴一样,我刚刚准备转正时非常茫然无措。因为转正并没有明确的大纲,且不同业务、不同部门考核的形式都是不确定的,在网上搜索经验资料也少得可怜。


在这里插入图片描述


别急,转正的内容和形式虽然具有不确定性,但其固有流程又决定了他存在着一定的“潜规则”。下面一恩姐姐就带你发出灵魂三问,深度剖析转正那些不得不说的套路。


灵魂三问


第一问,你了解转正流程吗?


转正流程对于各个公司大同小异。


以字节为例,需要当年毕业的同学,在出勤满40个工作日(技术和测试序列)且经过部门Leader和HR同意后,即有资格发起转正流程。此时HR会根据评估人和候选人的时间,约一个时间组织进行转正答辩。这个短短1个小时的转正答辩,决定了你的去留。

在这里插入图片描述


转正答辩上,一般包含你的HR,部门领导和跨部门的领导。除了跨部门的领导外,其他人都是你在实习过程中可能一起干过饭喝过酒,讨论过诗和远方的伙伴。只要在实习过程中没有发生过什么反目成仇的惨剧,他们都是偏向你的,甚至私下有过“兄弟情义”,“生死之交”还会去引导你去把控答辩的节奏。


比如我就听过自己的同事说过,当时他的导师还在答辩时争着抢着帮他解答领导的问题……

在这里插入图片描述


所以你所需要的做的基本只有一件事:


就是保证转正答辩的过程是顺利的。


整个答辩过程基本分为三块,其中属于你的有效时间仅有两块。第一块为个人展示,你需要以PPT的形式去描述一下实习期间工作,这一块大约有40min;第二块为问答环节,评估人会去根据你的工作与业务询问一些项目及基础知识,这一块大约有20min;第三块为审判环节,评估人会根据转正答辩过程中对你的了解决定你最终的去留。


因此,只有利用好有效的两块时间,才能Hold住整个答辩过程,让评估人被你的魅力闪瞎双眼!


第二问,实习期间的我为团队做了什么?


日常有随时记录工作进度的好习惯,因此我非常迅速地将自己实习阶段的工作按照优先级总结总结写了一下答辩PPT。导师在看了我的草稿后,一个劲儿吐槽:“比起你在这里学的东西,老板们更关心的是你给团队和业务带来的产出,你跟别人做这件事的区别在哪里?你在团队的定位是什么?拿出让他们去选择你的理由吧!


这与我一开始想的完全不一样。我本以为答辩就是汇报自己学了什么,做了什么。但其实不是,公司看中的是你的个人想法和价值实现,以及你身上是否有可输出的内容。


你的一言一行都要表达出:你是完全能胜任这个职位的。


想明白这点,我重新组织了自己的PPT和答辩的内容:


首先,我用了一页画了一个时间轴,分别用关键词总结每一part工作主要内容,核心,和工作亮点项目。这一部分重在简洁清晰。目的是让评审人清晰的了解我的工作内容重点和核心。

在这里插入图片描述


接下来,我选择2~3个核心项目详细地介绍工作内容并量化自己的产出。如果大家不清楚如何介绍的话,可以参考金字塔原理中 先总后分的表达方式——先给你的听众一个核心结论,在后面逐层展开。



比如我去介绍自己做多人视频通话这个需求时,首先需求的背景是需要支持多个人一起视频通话,我的主要工作是技术方案的设计与开发,具体工作是通过获取多路视频流,并将视频流分给对应的成员,因此我需要去维护所有成员的视图窗口以及流的稳定性与正确性。为了实现这个功能,我去了解了视频流编码,推拉流的逻辑,并且与多媒体业务同学进行了沟通,保证整体形成一条稳定的通路。

在这里插入图片描述


(截图取自我的PPT答辩文档,针对强化通话感知的需求,我列出了需求的目标,以及技术方案,并采用流程图方便说明,以及最后写上了需求的收益)



第三部分我会去对自己的价值角色进行提炼,即向评估人去证明自己的独特价值以及在团队中的定位。如果你不知道如何去证明,那就将这个问题回答好:凭什么别人要选择你而不选择别人?


最后一部分可以向评估人讲述一下自己的期望和未来的规划,我当时是舒情并茂地表达了自己对团队的热爱和对前景的向往,并表达了自己对未来的无限期盼。说的导师当场差点“热泪盈眶”。


以及提供给大家一个小妙招,作为一名研发,如果拥有产品思维,无疑是非常加分的。因此大家可以对自己所在的业务从产品本身进行思考,比如能做些什么才能让产品吸引更多用户,以及在产品上有什么意见和规划。


在这里插入图片描述


第三问,基础知识还记得吗?


在40分钟ShowTime之后,剩余20分钟评估人可能会针对你的某个具体项目询问一些实现上的细节,也有可能会询问一些技术方案设计上的问题。因此需要保证你所介绍的每一个项目都是你切身参与且明确其中实现的技术方案与细节,而且你应该提前去准备一些代码或技术上可扩展或优化的思考,来体现出你对项目的一种全局的视角。


同时评估人也会针对你目前所处团队的业务特性去询问一些基础问题,这一点和面试比较像,虽然难度比较于面试会简单很多。但也需要去多少刻意准备一些基础知识。比如我做视频通话业务,当时评估人就问我,你觉得通话传输的音视频流信息是通过udp还是tcp传输的,以及他们的区别。


这些问题是不是对于现在的你实在太简单了?


一个日常实习阶段小tip


不清楚大家在日常工作的过程中有没有对自己工作进行总结的习惯。如果没有,请从现在开始,立!刻!记!录!


“记录”这个行为听起来难度很高,其实真正实施起来你会发现它就像一种“陪伴”,非常潜移默化地融入你的生活中。


我会在日常工作过程中我会将自己的每一份思考和产出都落地文档并定时整理与复盘,每周五下班前会抽出15分钟将本周的工作以及下周需要做的事情整理成一个TODO列表。且会以月为纬度进行一次工作量和心态的反思,并与导师进行一次整体沟通,这种定期的总结和复盘能够让我永远对自己保持清醒。


当我整理自己实习工作时,这些文字更是我的宝藏,我能很清楚地看到自己日积月累的自我升级,并非常轻松地以时间线的角度看出自己各个阶段的产出。


写在最后


希望大家在实习期间一直保持一个谦卑学习的态度,正式阶段繁重的工作压力会让你没有过多心思去进行一些软硬实力的提高。


因此实习是一个非常好的机会去适应、去成长,一定要耐心地倾听、观察,向身边优秀的同事学习。


相信在以后的工作中,你一定也能如鱼得水,熠熠生辉。

在这里插入图片描述




FAQ时间


Q1:工作上犯了个常识性错误,感觉转正无望,该不该及时止损?

首先,要明白,作为实习生,犯错是一件正常的事。错误才能让你意识到不足,才能成长。转正评估的不是你的过去,而是你的价值和你可以塑造的可能性。如果你能对自己过去的工作上的错误进行复盘与总结,并且能够对未来进行合理的规划。相信你也能给出一份完美的转正答卷。


Q2:秋招无望走实习转正是否可行?

这个选择是完全没有问题的。实习不仅能够提高转正的几率,也是给你一定机会提前感受一下社会环境,在体验过真实互联网工作环境后,有些人会明白自己是否合适,才会有更精确的职业规划。


新增一个小栏目,收集着目前为止小伙伴们私信一恩的一些关于实习转正问题的答复。如果大家还有其他问题欢迎继续

作者:李一恩
来源:juejin.cn/post/7257434794900832312
在评论区回复,一恩会一一回答的~

收起阅读 »

向前兼容与向后兼容

2012年3月发布了Go 1.0,随着 Go 第一个版本发布的还有一份兼容性说明文档。该文档说明,Go 的未来版本会确保向后兼容性,不会破坏现有程序。 即用10年前Go 1.0写的代码,用10年后的Go 1.18版本,依然可以正常运行。即较高版本的程序能正常...
继续阅读 »

2012年3月发布了Go 1.0,随着 Go 第一个版本发布的还有一份兼容性说明文档。该文档说明,Go 的未来版本会确保向后兼容性,不会破坏现有程序。


即用10年前Go 1.0写的代码,用10年后的Go 1.18版本,依然可以正常运行。即较高版本的程序能正常处理较低版本程序的数据(代码)


反之则不然,如之前遇到过的这个问题[1]:在Mac上用Go 1.16可正常编译&运行的代码,在cvm服务器上Go 1.11版本,则编译不通过;


再如部署Spring Boot项目[2]时遇到的,在Mac上用Java 17开发并打的jar包,在cvm服务器上,用Java 8运行会报错




一般会认为向前兼容是向之前的版本兼容,这理解其实是错误的。


注意要把「前」「后」分别理解成「前进」和「后退」,不可以理解成「从前」和「以后」


线上项目开发中,向后(后退)兼容非常重要; 向后兼容就是新版本的Go/Java,可以保证之前用老版本写的程序依然可以正常使用




前 forward 未来拓展。


后 backward 兼容以前。







  • 向前兼容(Forward Compatibility):指老版本的软/硬件可以使用或运行新版本的软/硬件产生的数据。“Forward”一词在这里有“未来”的意思,其实翻译成“向未来”更明确一些,汉语中“向前”是指“从前”还是“之后”是有歧义的。是旧版本对新版本的兼容 (即向前 到底是以前还是前面?实际是前面





  • 向上兼容(Upward Compatibility):与向前兼容相同。









  • 向后兼容(Backward Compatibility):指新的版本的软/硬件可以使用或运行老版本的软/硬件产生的数据。是新版本对旧版本的兼容





  • 向下兼容(Downward Compatibility):与向后兼容相同。











软件的「向前兼容」和「向后兼容」如何区分?[3]


参考资料


[1]

这个问题: https://dashen.tech/2021/05/30/gvm-%E7%81%B5%E6%B4%BB%E7%9A%84Go%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7/#%E7%BC%98%E8%B5%B7

[2]

部署Spring Boot项目: https://dashen.tech/2022/02/01/%E9%83%A8%E7%BD%B2Spring-Boot%E9%A1%B9%E7%9B%AE/

[3]

软件的「向前兼容」和「向后兼容」如何区分?: https://www.zhihu.com/question/47239021



作者:fliter
来源:mdnice.com/writing/b8eb5fdae77f42e897ba69898a58e0d8
收起阅读 »

对负载均衡的全面理解

title: 对负载均衡的全面理解 date: 2021-07-10 21:41:24 tags: TCP/IP 对负载均衡服务(LBS)大名入行不多久就一直听闻,后来的工作中,也了解到 软件负载均衡器,如被合入Linux内核的章文嵩的LVS,还有...
继续阅读 »


title: 对负载均衡的全面理解 date: 2021-07-10 21:41:24 tags: TCP/IP





负载均衡服务(LBS)大名入行不多久就一直听闻,后来的工作中,也了解到 软件负载均衡器,如被合入Linux内核的章文嵩的LVS,还有以应用程序形式出现的HAProxy、KeepAlived,以及更熟悉的Nginx 等


也知道价格高昂的硬件负载均衡器如F5,A10 (甚至搬运过报废的F5)



















但长期以来,也有一些疑惑不解,比如





  • 常说的四层负载均衡是不是就是在传输层实现负载均衡?





  • 四层负载均衡中常听到的三角传输模式IP隧道模式NAT模式,有何区别?哪个性能最好?





  • 四层负载均衡性能好,那为何还有如nginx这样名气更大的七层负载均衡的出现?(Nginx也可以用来做四层代理)





  • 负载均衡与反向代理有何异同?





  • 转发和代理有何本质不同?




这是几年前记的笔记,显然存有谬误。





计算机网络中常见缩略词翻译及简明释要




通读 凤凰架构--负载均衡一章,可知





  • 四层负载均衡 主要工作在第二层和第三层,即 数据链路层和网络层 (通过改写 MAC 地址IP 地址 实现转发)​​​





  • “三角传输模式”(Direct Server Return,DSR),是作用于 数据链路层负载均衡,也称“单臂模式”(Single Legged Mode)或者“直接路由”(Direct Routing)。 通过修改请求数据帧中的 MAC 目标地址,让用户原本是发送给负载均衡器的请求的数据帧,被二层交换机根据新的 MAC 目标地址转发到服务器集群中对应的服务器(“真实服务器”)的网卡上。 效率高性能好,但有些场景不能满足










  • 网络层负载均衡:IP隧道模式,NAT模式


IP隧道模式:





NAT模式:







在流量压力比较大的时候,NAT 模式的负载均衡会带来较大的性能损失,比起直接路由和 IP 隧道模式,甚至会出现数量级上的下降






  • 四层负载均衡进行转发,只有一条TCP通道; 七层负载均衡只能进行代理,需要有两条TCP通道








  • 七层负载均衡器就属于反向代理中的一种;





  • 如果只论网络性能,七层均衡器肯定是无论如何比不过四层均衡器的;但其工作在应用层,可以感知应用层通信的具体内容,往往能够做出更明智的决策,玩出更多的花样来。









负载均衡的两大职责是“选择谁来处理用户请求”和“将用户请求转发过去”。上面讲的都是怎样将用户请求转发过去


至于选择哪台应用服务器来处理用户请求(翻牌子),则有很多算法,如下图就是F5的一些选择算法












B站:一次性讲清楚四层负载均衡中的NAT模式和IP隧道模式


Shadowsocks源码解读——什么是代理?什么是隧道?


NAT模式、路由模式、桥接模式的区别


VLAN是二层技术还是三层技术?


四层负载均衡详解


作者:fliter
来源:mdnice.com/writing/c5b54a9bdd78478a87c6d39e38572358
收起阅读 »

Kotlin注解探秘:让代码更清晰

快速上手 @Target(   AnnotationTarget.CLASS,   AnnotationTarget.FUNCTION,   AnnotationTar...
继续阅读 »

快速上手


@Target(  
 AnnotationTarget.CLASS,  
 AnnotationTarget.FUNCTION,  
 AnnotationTarget.VALUE_PARAMETER,  
 AnnotationTarget.EXPRESSION,  
 AnnotationTarget.CONSTRUCTOR  
)

@Retention(AnnotationRetention.SOURCE)
@Repeatable
@MustBeDocumented
annotation class MyAnnotation

@MyAnnotation @MyAnnotaion class Test @MyAnnotation constructor(val name: String) {
    @MyAnnotation fun test(@MyAnnotation num: Int)Int = (@MyAnnotation 1)
}

注解的声明


注解使用关键字annotation来声明,比如快速上手中的例子,使用annotation class MyAnnotation就声明了一个注解,我们可以按照定义的规则将其放在其他元素身上


元注解


下面的注解了解过Java的肯定不陌生,元注解就是可以放在注解上面的注解





  • @Target: 用来指定注解可以应用到哪些元素上,有以下可选项



    • CLASS: 可以应用于类、接口、枚举类



    • ANNOTATION_CLASS: 可以应用于注解



    • TYPE_PARAMETER



    • PROPERTY



    • FIELD



    • LOCAL_VARIABLE



    • VALUE_PARAMETER: 可以应用于字面值



    • CONSTRUCTOR: 可以应用于构造函数



    • FUNCTION: 可以应用于函数



    • PROPERTY_GETTER



    • PROPERTY_SETTER



    • TYPE



    • EXPRESSION: 可以应用于表达式



    • FILE



    • TYPEALIAS





  • @Retention: 用来指定注解的生命周期



    • SOURCE: 仅保存在源代码中



    • BINARY: 保存在字节码文件中,但是运行是无法获取



    • RUNTIME: 保存在字节码文件中,运行时可以获取





  • @Repeatable: 允许此注解可以在单个元素上多次使用


拿上方的代码来简单介绍几个元注解


@Target


可以看一下@Target的源码


 * This meta-annotation indicates the kinds of code elements which are possible targets of an annotation.
 *
 * If the target meta-annotation is not present on an annotation declaration, the annotation is applicable to the following elements:
 * [CLASS], [PROPERTY], [FIELD], [LOCAL_VARIABLE], [VALUE_PARAMETER], [CONSTRUCTOR], [FUNCTION], [PROPERTY_GETTER], [PROPERTY_SETTER].
 *
 * @property allowedTargets list of allowed annotation targets
 */
@Target(AnnotationTarget.ANNOTATION_CLASS)
@MustBeDocumented
public annotation class Target(vararg val allowedTargets: AnnotationTarget)

在源码中可以看到,Target注解中要传入的参数为allowedTargets,使用了vararh关键字,可传入多个参数,参数的类型为AnnotationTarget,它是一个枚举类,再进入AnnotationTarget的源码就可以看到它有上方元注解中列出的那些。


在快速上手的示例中我们的@Target中传入了Class FUNCTION VALUE_PARAMETER EXPRESSION CONSTRCTOR,表示此注解可以放在类、接口、枚举、函数、字面值、表达式和构造函数上


@Retention


此注解就是指定它什么时候失效 默认是RUNTIME, 快速上手中是用的SOURCE,表示它仅存在于源码中,在编译成字节码后将会消失,如果指定了BINARY,则可以存在于字节码文件中,但是运行时无法获取,反射无法获取


注解的属性


注解可在主构造参数内传值


annotation class MyAnnotation2(val effect: String)

class Test2 {
    @MyAnnotation2("Test")
    fun test() {
        println("Run test")
    }
}

比如上面的例子,可以在主构造函数内传入一个参数,参数支持的类型有以下几种





  • Kotlin中的八种“基本数据类型”(Byte, Short, Int, Long, Float, Double, Boolean, Char)



  • String类型



  • 引用类型(Class)



  • 枚举类型



  • 注解类型



  • 以上类型的数组类型 需要注意的是,官网中特别说明参数不可以传入可空类型,比如"String?",因为JVM不支持null存储在注解的属性中


注解的作用


如果是熟悉Java的开发者对注解的作用肯定是非常熟悉。 注解可以提供给编译器、运行时环境、其他代码库以及框架提供很多可用信息。 可用作标记,可供第三方技术库、框架识别信息,比如大家熟悉的SpringBoot,很多事情就是通过注解和反射来实现 可用来提供更多的上下文信息,比如方法的类型参数、返回值类型、错误处理


后面可结合反射来深入理解Kotlin在开发中的用途


作者:AB-style
来源:mdnice.com/writing/5b8eb45e3b1e4b23a57926bd58b7f540
收起阅读 »

京东一面:post为什么会发送两次请求?🤪🤪🤪

web
在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
继续阅读 »

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


那么接下来这篇文章我们就一点一点开始引出这个问题。


同源策略


在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



  • 跨站脚本攻击(XSS)

  • SQL 注入攻击

  • OS 命令注入攻击

  • HTTP 首部注入攻击

  • 跨站点请求伪造(CSRF)

  • 等等......


如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


这就引出了最基础、最核心的安全策略:同源策略。


什么是同源策略


同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



  • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

  • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

  • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


URL结果原因
http://store.company.com:80/dir/page.html同源只有路径不同
http://store.company.com:80/dir/inner/another.html同源只有路径不同
https://store.company.com:80/secure.html不同源协议不同,HTTP 和 HTTPS
http://store.company.com:81/dir/etc.html不同源端口不同
http://news.company.com:80/dir/other.html不同源主机不同

同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



  • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

  • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

  • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


CORS


对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


简单请求


不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



  1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

  2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

  3. 请求中没有使用 ReadableStream 对象。

  4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

  5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


预检请求


非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


例如我们在掘金上删除一条沸点:


20230822094049


它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

  • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

  • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


20230822122441


上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


附带身份凭证的请求与通配符


在响应附带身份凭证的请求时:



  • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

  • 对于附带身份凭证的请求(通常是 Cookie),


这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


参考文章



总结


预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


整个完整的请求流程有如下图所示:


20230822122544


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果

作者:Moment
来源:juejin.cn/post/7269952188927017015
你也喜欢,欢迎 star 🥰🥰🥰

收起阅读 »

网易云音乐 Tango 低代码引擎正式开源!

web
📝 Tango 简介 Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供...
继续阅读 »

📝 Tango 简介


Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。


Tango 低代码引擎开发效果


如上图所示,Tango 低代码引擎支持可视化视图与源码双向同步,双向互转,为开发者提供 LowCode+ ProCode 无缝衔接的开发体验。


✨ 核心特性



  • 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等

  • 基于源码 AST 驱动,无私有 DSL 和协议

  • 提供实时出码能力,支持源码进,源码出

  • 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件

  • 使用 TypeScript 开发,提供完整的类型定义文件


🏗️ 基于源码的低代码搭建方案


Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的 基于 Schema 驱动的低代码方案 相比,不受私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。



📄 源码进,源码出


由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。


code in, code out


🏆 产品优势


与基于私有 Schema 的低代码搭建方案相比,Tango 低代码引擎具有如下优势:


对比项基于 Schema 的低代码搭建方案Tango(基于源码 AST 转换)
适用场景面向特定的垂直搭建场景,例如表单,营销页面等🔥 面面向以源码为中心的应用搭建场景
语言能力依赖私有协议扩展,不灵活,且难以与编程语言能力对齐🔥 直接基于 JavaScript 语言,可以使用所有的语言特性,不存在扩展性问题
开发能力LowCode🔥 LowCode + ProCode
源码导出以 Schema 为中心,单向出码,不可逆🔥 以源码为中心,双向转码
自定义依赖需要根据私有协议扩展封装,定制成本高🔥 原有组件可以无缝低成本接入
集成研发设施定制成本高,需要额外定制🔥 低成本接入,可以直接复用原有的部署发布能力

📐 技术架构


Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。此外,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。


具体的技术架构如下图所示:


low-code engine


⏰ 开源里程碑


Tango 低代码引擎是网易云音乐内部低代码平台的核心构件,开源涉及到大量的核心逻辑解藕的工作,这将给我们正常的工作带来大量的额外工作,因此我们计划分阶段推进 Tango 低代码引擎的开源事项。



  1. 今天我们正式发布 Tango 低代码引擎的第一个社区版本,该版本将会包括 Tango 低代码引擎的核心代码库,TangoBoot 应用框架,以及基于 antd v4 适配的低代码组件库。

  2. 我们计划在今年的 9 月 30 日 发布低代码引擎的 1.0 Beta 版本,该版本将会对核心的实现面向社区场景重构,移除掉我们在云音乐内部的一些兼容代码,并将核心的实现进行重构和优化。

  3. 我们计划在今年的 10 月 30 日 发布低代码引擎的 1.0 RC 版本,该版本将会保证核心 API 基本稳定,不再发生 BREAKING CHANGE,同时我们将会提供完善翔实的开发指南、部署文档、和演示应用。

  4. 正式版本我们将在 2023 年 Q4 结束前 发布,届时我们会进一步完善我们的开源社区运营机制。


milestones


🤝 社区建设


我们的开源工作正在积极推进中,可以通过如下的信息了解到我们的最新进展:



欢迎大家加入到我们的社区中来,一起参与到 Tango 低代码引擎的开源建设中来。有任何问题都可以通过 Github Issues 反馈给我们,我们会及时跟进处理。


💗 致谢


感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。


感谢 CodeSandbox 提供的 Sandpack 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。

作者:网易云音乐技术团队
来源:juejin.cn/post/7273051203562749971

收起阅读 »

北京前端五年经验问些什么?

这一天,我瘫坐在办公室的椅子上,回想这五年的一事无成,钱也没赚到,技术也没学到,最近投了简历去面试,我一定要把握住,这是我此生仅有的机会了。 穿好格子衫,带上假发,出发了。 路上的植发广告格外亮眼,玩了会儿手机终于到了。 某大型互联网公司,跟前台说了一下是面试...
继续阅读 »

这一天,我瘫坐在办公室的椅子上,回想这五年的一事无成,钱也没赚到,技术也没学到,最近投了简历去面试,我一定要把握住,这是我此生仅有的机会了。


穿好格子衫,带上假发,出发了。


路上的植发广告格外亮眼,玩了会儿手机终于到了。


某大型互联网公司,跟前台说了一下是面试的,然后让我填个表,填完去一个小屋子等着。


过了一会儿还是没人,前台小姐姐给我带了一杯水,说下一个就是我。


等了半小时终于来人了,微胖的一个中年男人,进来打了一个招呼,示意我坐下吧。


面试环节,请先自我介绍一下吧,他拿着简历看,我就说了一下我的情况,男,25,张满月,热爱编程,平时会写一些技术博客,文章,录制成视频等,(表示热爱学习),介绍了一下技术栈,Vue Nodejs python C++


然后介绍了一下项目,省略...


面试官问:为啥要离职?


我:(理由现编)家里庄稼要开始收割了 开玩笑 我就说了薪资问题。


然后就是问问题环节


问了一些计算机的基础知识,CPU运行原理,冯诺依曼体系结构,图形绘制原理,等


网络的一些东西 OSI七层参考模型,TCP/IP四层事实模型,双绞线,无线电波,光纤,路由器,交换机等。


还有一些协议 TCP/IP 协议簇里面的基本都问。 很考验基础知识


问的最多的就是TCP 三次握手 以及四次挥手 syn包 seq序列号 Ack确认号,滑动窗口思想等


http1.1 http2 http3 多路复用,保活,队头阻塞,二进制分帧层,头部压缩等。


操作系统知识


进程,线程,内存管理,汇编和机器语言的区别什么的,还有windows和Linux的常用命令。


web服务器 nginx 四层负载和七层SLB负载 这个就太简单了 stream upstream


nginx插件编写问了lua语言


nginx 反向代理 proxy_pass 线程什么,也都是一些基础问题


考察了很多基础知识 这些应该大家都会


然后换人了... 后面来了一个看着年轻点的,跟我说刚才那个人是运维主管。。。我也是服了我就说怎么不问前端的东西呢???


这位面试官是前端负责CICD,自动化流程负责人。


问了一些基础问题


Vue3的一些特性,和一些ts的简单的东西装饰器什么的,都比较简单,问了mvvm,和mvc,IOC控制反转和DI 依赖注入,这玩意就太熟了,巴拉巴拉说了一堆,问了一些前端的工具Babel,PostCss,webpack,vite,esbuild,rollup什么的。很杂,


然后问了一些js的问题,经典event loop... , es6, 也都是一些常问的。


他看我简历写着 webGL 问了一些相关的问题:openGL修饰符,类型,顶点着色器,片源着色器等,还问了光学的知识 冯氏光照模型 慢反射光,镜面高光等。都是简单的基础知识 大家应该也会


接着了问了一些CICD的知识 我就知道逃不过,问了一些docker,github Actions Jenkins 等一些问题,这些我也不是完全精通,只能说回答的一般😂。


接着就让我等通知,


后面HR发消息让我周六去复试。 周六牛的


周六的时候还是熟悉的广告,熟悉的地铁,这次是一个后端大哥面的,一进来就问,你们之前的网站吞吐率是多少,。。。这 我哪清楚,瞎说了一个,然后问有没有做过getway,我说有用Nest写的,任何人要先过网关层,然后才到业务层。


然后问负载怎么做的,堡垒机怎么部署的,有多少台机器,怎么部署的?


我们一共有XX台机器,使用动态扩容技术,(大概就是比如有10台机器,用的人多了CPU利用率过高,超过90%,就会进行动态扩容,自动增加机器11台,自动进行Nest服务部署,自动配置负载均衡,如果CPU下来了,就会动态缩容,删除代码,去掉负载,关闭机器),使用pm2 部署的,pm2自带了集群部署。


然后问mysql 基本的语法 索引 mysql事务的四大特性,等。。。


网络编程nodejs net模块socket套接字,如何跟python通讯,gRPC协议,以及如何编写addon,Npai用C++编写的使用node-gyp编译。


问了wasm,c++如何编译wasm等。


其他的不记得

作者:小满zs
来源:juejin.cn/post/7273309090657747000
了。。。 后面就没信了。。。。。

收起阅读 »

iOS 使用 CoreNFC 读取第三代社保卡信息

iOS
NFC 是 Near Field Communication 的缩写,即近场通信,是一种用于短距离无线设备与其他设备共享数据或触发这些设备上的操作的技术。它使用射频场构建,允许没有任何电源的设备存储小块数据,同时还允许其他供电设备读取该数据。 iOS 和 w...
继续阅读 »

NFC 是 Near Field Communication 的缩写,即近场通信,是一种用于短距离无线设备与其他设备共享数据或触发这些设备上的操作的技术。它使用射频场构建,允许没有任何电源的设备存储小块数据,同时还允许其他供电设备读取该数据。



iOS 和 watchOS 设备内置 NFC 硬件已经很多年了。在现实生活中,Apple Pay 就是使用这项技术与商店的支付终端进行交互。然而直到 iOS 11 开发者才能够使用 NFC 硬件。后来 Apple 在 iOS 13 系统中提升了 CoreNFC 的功能,开发者可以借助这项新技术,对 iOS 设备进行编程,使其以新的方式与周围的互联世界进行交互。


说明:本文提供的代码示例所用的开发环境为 Xcode14 + Swift 5.7 + iOS 13。需要登录已付费的开发者账号才能开启 NFC Capability。


工程配置


设置 Capability


在项目导航器中选中项目,转到 Signing & Capabilities 标签页并选择 +Capability,在弹出的列表中选择 Near Field Communication Tag Reading。这会自动生成 entitlements 文件中的必要配置信息,同时为您的应用程序激活 NFC 功能。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
</array>
</dict>

设置 Info.plist


添加 NFC 相关的隐私设置,向 Info.plist 文件中添加 Privacy - NFC Scan Usage Description 隐私设置项。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFCReaderUsageDescription</key>
<string>应用需要您的同意,才能访问 NFC 进行社保卡信息的读写。</string>
</dict>

添加 AID 相关的设置项,向 Info.plist 文件中添加 ISO7816 application identifiers for NFC Tag Reader Session 配置项。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>A000000632010105</string>
</array>
</dict>


说明:第三代社保卡使用统一的交通联合卡电子钱包规范,A000000632010105 为交通联合卡 AID 标识。参考网址:wiki.nfc.im/books



导入 CryptoSwift 第三方库


在项目导航器中选中项目,右键菜单选择 Add Packages...,在搜索框中输入 github.com/krzyzanowsk… 并点击 Add Package 按钮完成导入。





说明:CryptoSwift 提供了相关的十六进制字符串与 UInt8 相互转换的方法。



代码编程


扩展 NFCISO7816Tag


由于 Apple 是从 iOS 14 系统开始提供了 sendCommand API 的异步调用形式,为兼容 iOS 13 系统,并更好的使用 Swift 提供的 async/await 语法,现对其 NFCISO7816Tag 进行方法扩展。

import CoreNFC
import CryptoSwift

@available(iOS 13.0, *)
extension NFCISO7816Tag {

  @discardableResult
  func sendCommand(_ command: String) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
      // 通过 CryptoSwift 库提供的 API,将十六进制表示命令字符串转换成字节
      let apdu = NFCISO7816APDU(data: Data(hex: command))!
      // 将同步调用形式转换成异步调用形式
      sendCommand(apdu: apdu) { responseData, _, _, error in
        if let error {
          continuation.resume(throwing: error)
        } else {
          continuation.resume(returning: responseData)
        }
      }
    }
  }
}

封装 NFCTagReaderSession

import CoreNFC

@available(iOS 13.0, *)
class NFCISO7816TagSession: NSObject, NFCTagReaderSessionDelegate {

  private var session: NFCTagReaderSession? = nil
  private var sessionContinuation: CheckedContinuation<NFCISO7816Tag, Error>? = nil

  func begin() async throws -> NFCISO7816Tag {
// 实例化用于检测 NFCISO7816Tag 的会话
    session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)
    session?.alertMessage = "请将社保卡靠近手机背面上方的 NFC 感应区域"
    session?.begin()
    return try await withCheckedThrowingContinuation { continuation in
      self.sessionContinuation = continuation
    }
  }

  func invalidate(with message: String) {
// 关闭读取会话,以防止重用
    session?.alertMessage = message
    session?.invalidate()
  }

  // MARK: - NFCTagReaderSessionDelegate

  func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {}

  func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
// 检测到 NFCISO7816Tag
    if let tag = tags.first, case .iso7816(let iso7816Tag) = tag {
      session.alertMessage = "正在读取信息,请勿移动社保卡"
// 连接到 NFCISO7816Tag 并将同步调用形式转换成异步调用形式
      session.connect(to: tag) { error in
        if let error {
          self.sessionContinuation?.resume(throwing: error)
        } else {
          self.sessionContinuation?.resume(returning: iso7816Tag)
        }
      }
    }
  }

  func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
// 读取过程中发生错误
    self.session = nil
    sessionContinuation?.resume(throwing: error)
  }
}

编写 UI 界面


使用 SwiftUI 编写如下代码所示的页面,包含一个显示卡号的标签和一个读取按钮。

import SwiftUI

struct ContentView: View {
  @State private var cardNo = ""

  var body: some View {
    VStack(alignment: .leading) {
      Text("卡号:\(cardNo)")
        .font(.system(size: 17))
      Button(action: read) {
        Text("读取")
          .padding()
          .frame(maxWidth: .infinity)
          .foregroundColor(.white)
          .background(.blue)
          .cornerRadius(8)
      }
      Spacer()
    }
    .padding()
  }
}

实现读取逻辑

import SwiftUI
import CryptoSwift

struct ContentView: View {
// var body: some View {...}

private func read() {
    Task {
      let session = NFCISO7816TagSession()
      do {
// 检测 NFCISO7816Tag
        let tag = try await session.begin()
// 发送命令 00B0950A12 并截取前 10 个字节转换为 20 位卡号
        let cardNo = try await tag.sendCommand("00B0950A12")[0..<10].toHexString()
        self.cardNo = cardNo
// 关闭读取会话
        session.invalidate(with: "读取成功")
      } catch {
        print(error)
      }
    }
  }
}


说明:APDU 是卡与读卡器之间传送的信息单元,具体指令描述请参考 wiki.nfc.im/books



运行过程截图




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

Apipost: 开发者们的瑞士军刀

在当今的数字化时代,数据流通是推动社会进步的关键因素之一。其中,API(应用编程接口)已经成为跨平台数据交互的标准。然而,API开发和管理并非易事,Apipost一体化研发协作赋能平台,支持从API设计到API调试再到API测试覆盖整个API生命周期的API管...
继续阅读 »

在当今的数字化时代,数据流通是推动社会进步的关键因素之一。其中,API(应用编程接口)已经成为跨平台数据交互的标准。然而,API开发和管理并非易事,Apipost一体化研发协作赋能平台,支持从API设计到API调试再到API测试覆盖整个API生命周期的API管理平台,一起来看看Apipost有什么不同吧。


一、Apipost是什么?


Apipost是一个专为开发者设计的API管理工具,提供了全面的API文档生成、调试、测试和分享功能。它的目标是帮助开发者简化API开发流程,提高工作效率。


二、如何使用Apipost?


安装:


进入官网下载安装或直接使用web端



使用:


可以从其他平台如postman导入脚本文件,或创建接口。



接口调试:


输入接口URL后点击发送即可模拟接口请求,上方为请求区,下方为响应区



生成接口文档:


点击分享文档即可生成标准的接口文档,可以将链接分享给需要查看接口的其他同事



一键压测


接口调试完成后可以在一键压测页面进行并发测试,看看接口在高并发情况下的运行情况



总结


Apipost作为一款专为开发者设计的API管理工具,凭借其强大的功能和易用性,已经在开发者社区中积累了良好的口碑。通过使用Apipost,开发者可以节省大量时间,专注于创新和打造卓越的产品。如果你正在寻找一款强大且易用的API管理工具,那么Apipost无疑是一个值得考虑的选择。


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

前端比localStorage存储还大的本地存储方案

产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。 方案选择既然要存储的数量大,得排除cookielocalStorage,虽然比cookie多,但是同样有上限(5M)左右,备选websql 使用简单,存储量大,兼容性差,备选index...
继续阅读 »

产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。


方案选择

  • 既然要存储的数量大,得排除cookie
  • localStorage,虽然比cookie多,但是同样有上限(5M)左右,备选
  • websql 使用简单,存储量大,兼容性差,备选
  • indexDB api多且繁琐,存储量大、高版本浏览器兼容性较好,备选

既然罗列了一些选择,都没有十全十美的,那么有没有一种能够集合这多种方式的插件呢?渐进增强 or 优雅降级 的存在
冲着这个想法,就去github和谷歌找了一下,还真的有这么一个插件。


那就是 localforage


localforage


localForage 是一个 JavaScript 库,只需要通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。 




关于兼容性


localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。下面是 indexDB、web sql、localStorage 的一个浏览器支持情况,可以发现,兼容性方面loaclForage基本上满足99%需求


使用


解决了兼容性和存储量的点,我们就来看看localforage的基础用法


安装

# 通过 npm 安装:
npm install localforage
// 直接引用
<script src="localforage.js"></script>
<script>console.log('localforage is: ', localforage);</script>

获取存储


getItem(key, successCallback)


从仓库中获取 key 对应的值并将结果提供给回调函数。如果 key 不存在,getItem() 将返回 null。

localforage.getItem('somekey').then(function(value) {
// 当离线仓库中的值被载入时,此处代码运行
console.log(value);
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});

// 回调版本:
localforage.getItem('somekey', function(err, value) {
// 当离线仓库中的值被载入时,此处代码运行
console.log(value);
});

设置存储


setItem(key, value, successCallback)


将数据保存到离线仓库。你可以存储如下类型的 JavaScript 对象:

  • Array
  • ArrayBuffer
  • Blob
  • Float32Array
  • Float64Array
  • Int8Array
  • Int16Array
  • Int32Array
  • Number
  • Object
  • Uint8Array
  • Uint8ClampedArray
  • Uint16Array
  • Uint32Array
  • String
localforage
.setItem("somekey", "some value")
.then(function (value) {
// 当值被存储后,可执行其他操作
console.log(value);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});

// 不同于 localStorage,你可以存储非字符串类型
localforage
.setItem("my array", [1, 2, "three"])
.then(function (value) {
// 如下输出 `1`
console.log(value[0]);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});

// 你甚至可以存储 AJAX 响应返回的二进制数据
req = new XMLHttpRequest();
req.open("GET", "/photo.jpg", true);
req.responseType = "arraybuffer";

req.addEventListener("readystatechange", function () {
if (req.readyState === 4) {
// readyState 完成
localforage
.setItem("photo", req.response)
.then(function (image) {
// 如下为一个合法的 <img> 标签的 blob URI
var blob = new Blob([image]);
var imageURI = window.URL.createObjectURL(blob);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});
}
});

删除存储


removeItem(key, successCallback)


从离线仓库中删除 key 对应的值。

localforage.removeItem('somekey').then(function() {
// 当值被移除后,此处代码运行
console.log('Key is cleared!');
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});

清空存储


clear(successCallback)


从数据库中删除所有的 key,重置数据库。


localforage.clear() 将会删除离线仓库中的所有值。谨慎使用此方法。

localforage.clear().then(function() {
// 当数据库被全部删除后,此处代码运行
console.log('Database is now empty.');
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});

localforage是否万事大吉?


用上了localforage一开始我也以为可以完全满足万恶的产品了,然而。。。翻车了.。


内存不足的前提下,localforage继续缓存会怎么样?


在这种状态下,尝试使用localforage,不出意外,抛错了 QuotaExceededError 的 DOMErro


解决
存储数据的时候加上存储的时间戳和模块标识,加时间戳一起存储

setItem({
value: '1',
label: 'a',
module: 'a',
timestamp: '11111111111'
})

  • 如果是遇到存储使用报错的情况,try/catch捕获之后,通过判断报错提示,去执行相应的操作,遇到内存不足的情况,则根据时间戳和模块标识清理一部分旧数据(内存不足的情况还是比较少的)
  • 在用户手机上产生脏数据的情况,想要清理的这种情况的 处理方式是:
  • 让后端在用户信息接口里面加上缓存有效期时间戳,当该时间戳存在,则前端会进行一次对本地存储扫描
  • 在有效期时间戳之前的数据,结合模块标识,进行清理,清理完毕后调用后端接口上报清理日志
  • 模块标识的意义是清理数据的时候,可以按照模块去清理(选填)

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

数字签名为什么可以防篡改

iOS
数字签名是什么 数字签名是一种数字技术,用于验证和保护数据的完整性。 数字签名是通过一些加密算法将消息或文件与公钥(如果是非对称加密就有公钥不然就不用)绑定在一起,并生成唯一的签名。 数字签名的工作原理 数字签名的核心在于加密算法。最常用的是非对称加密算法,它...
继续阅读 »

数字签名是什么


数字签名是一种数字技术,用于验证和保护数据的完整性


数字签名是通过一些加密算法将消息或文件与公钥(如果是非对称加密就有公钥不然就不用)绑定在一起,并生成唯一的签名。


数字签名的工作原理


数字签名的核心在于加密算法。最常用的是非对称加密算法,它将原文通过特定HASH函数得到的摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文提炼出一个摘要信息,与解密得到的摘要进行对比。


数字签名也可以使用哈希函数对文件或消息的散列值进行加密,确保消息不会被篡改。(也有人认为摘要算法不能逆向也就是解密所以不是加密算法,在此不做讨论)


数字签名可以与数字证书结合使用,以证明密钥的归属和真实性,从而保护数字签名过程不被破坏。


数字签名的应用


JWT


JWT通常由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature),以点号分隔。第一部分是头部,第二部分是载荷,第三部分是签名。以下是一个包含了用户ID、用户名和时间戳的JWT实例,格式为 Header.Payload.Signature

// 为方便展示,在'.'处作了换行处理,可以更好地看清楚结构
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

HeaderPayload都是经过 Base64URL 编码的,所以每个人都能通过解码得到原来的信息,固不应该在里面存一些敏感信息。



Signature就是我们要讨论的数字签名了!Signature 部分是对前两部分的签名,防止数据篡改。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)


HMAC算法 是一种基于密钥的报文完整性的验证方法。HMAC算法利用哈希运算,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。其安全性是建立在Hash加密算法基础上的。


由于Signature是根据HeaderPayload以及服务器的secret来生成的,由于secret只有服务器知道,所以只要HeaderPayloadSignature其中一个被篡改了,那么后续验证的时候就不能通过。同时只有知道secret才能产生与HeaderPayload配对的Signature,所以也能确认该 Token 是否是该服务器所颁发的。


验证过程是用服务器的密钥通过同样的算法计算出一个新的 Signature 然后和旧的 Signature 进行比较,只要被篡改那么 Signature就会跟着改变,所以通过比较, Signature 一样的话则证明没有被篡改,否则则认为被篡改了。


CA 证书


其实就是这个 CA 证书的数字签名为什么可以防篡改,困扰了我好久,所以才去稍微深入了解了一下然后写下了这篇博客。就是为了这点醋,我才包的这顿饺子~


之前学习 https 的时候,看了各大论坛的帖子发现有挺多帖子对于 CA 证书是怎么做放篡改的讲的不太对或者讲的不太清晰,所以这个问题困扰了我挺久。以下是随便找的一些帖子(对事不对人):


例1:



例2:



例3:



(例1和例2是随便在掘金上面搜到的相关文章,例3是newbing的回答)


先回顾一下数字证书验证的大概过程:


CA 签发证书的过程:

  • 首先 CA 会把持有者的公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值;
  • 然后 CA 会使用自己的私钥将该 Hash 值加密,生成 Certificate Signature,也就是 CA 对证书做了签名;
  • 最后将 Certificate Signature 添加在文件证书上,形成数字证书;

客户端校验服务端的数字证书的过程:

  • 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;
  • 浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;
  • 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

核心问题在于验证服务器发来的数字证书的数字签名时所用到的公钥是哪里来的。


假设有那么一个场景:


客户端A 和 服务器A 的通信过程中,私钥是Secret_RSA_A,公钥是Secret_PUB_A。服务器A 将自己的证书CA发给客户端A的过程中被 中间人B 给截获了,中间人B 用自己的公钥Secret_PUB_B 替换了 服务器A 发给 客户端A 的CA证书的公钥Secret_PUB_A,并且用和公钥Secret_PUB_B 配对的私钥Secret_RSA_B 对替换公钥后的CA证书的公钥、用途、颁发者、有效时间等信息生成的新HASH 进行加密,生成新的 Certificate Signature 并把原本证书上的 Certificate Signature 替换掉。但客户端A 对这并不知情。然后在后续客户端对该 CA证书验证的过程中,如果使用的是证书上的公钥,那么计算出来的 H1 和 H2 就会一样,也就是认为证书是可信的。(实际上加密使用的是CA私钥而不是服务器私钥所以中间人伪造不了一对新的公私钥,但是如果使用服务器发送过来的公钥去验证的话那么就有可能被伪造)


所以更加安全的做法应该是不使用传过来的证书上面的公钥(证书上的公钥是服务器持有者的公钥而不是CA公钥),而是使用预置在操作系统里面的公钥,因为证书加密是用CA私钥加密的而不是用服务器持有者的私钥进行加密的,传服务器持有者的公钥过来是为了和客户端协商然后生成后续对称加密通信需要用到的秘钥。这也是我之前看到的一些文章没有提到的(如上面的图1/2/3所示,没有针对原作者的意思),容易让人困惑。服务器发送过来的证书中的公钥是服务器的公钥而不是可以解密数字签名的公钥(数字签名的公钥也就是和CA证书配对的公钥)。 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用操作系统内置的 CA 的公钥解密 Certificate Signature 内容。这行验证过程中存在一个证书信任链的问题。客户端收到服务器发送过来的CA证书后,浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发,如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密。


综上,数字签名只能验证数据的完整性(JWT 只有服务端可以验证他的身份,因为它有解密需要的密钥,而客户端是验证不了的),而验证身份需要的是数字证书。


最后


以上是本人在学习数字签名原理的过程中的一些感悟,由于个人的局限性,所以可能存在纰漏的情况,欢迎大家批评指正。


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

AI不会干掉程序员,反而会为程序员带来更多工作机会

AI不会干掉程序员,反而会为程序员带来更多工作机会 从chatGPT出来那一刻起,我就一直在思考这个问题:我们程序员是不是马上要被AI干掉了?搞得自己非常焦虑,各种自我怀疑,对除AI外的任何技术都失去了兴趣。网上也有各种铺天盖地的观点,大多数的title都是以...
继续阅读 »

AI不会干掉程序员,反而会为程序员带来更多工作机会


从chatGPT出来那一刻起,我就一直在思考这个问题:我们程序员是不是马上要被AI干掉了?搞得自己非常焦虑,各种自我怀疑,对除AI外的任何技术都失去了兴趣。网上也有各种铺天盖地的观点,大多数的title都是以"AI即将干掉程序员"开头,贩卖焦虑。
在今年这种大环境本身就非常不好的时候,AI的出现仿佛是雪上加霜,让我们对未来完全失去信心,感觉未来一片灰暗。甚至很多人开始研究起了自己失业后可以干哪些行业,如外卖员、网约车等。


对于我们是否会马上被AI干掉这个问题,通过这几个月不断的思考、学习、推理,总算有了一些自己的答案,在此做一个分享与总结。


AI出现后软件行业会变成怎样?


去除杂念,以史为鉴,尊重逻辑。


会出现各种各样基于AI的新型应用


这里的新型应用主要是在没有AI的加持下无法实现的应用。如AI医生、AI老师等。


当你身体出现异常的时候,很多人的第一反应就是先百度一下,然后吓出一身冷汗,然后医院检查,最后虚惊一场。


这里最大的问题就是我们通过搜索引擎很难精确地描述出病情,当我们去门诊诊断的时候,医生会问我们多轮问题,才能给出初步诊断。多轮问题是目前的搜索引擎无法做到的,但它却正好是ChatGPT的强项。


如果有AI加持,再加上有效的训练数据,AI医生就可以通过和你的多轮对话,给出更加准确的初步诊断。


当AI医生诞生后,你小孩发烧的时候你就可以以非常小的成本得到非常准确的处理方案,如是否需要物理降温、是否需要立即吃药、如果吃药应该吃什么药什么量等。而在这之前,你需要到网上搜索各种资料,不仅效率低下,还有可能得到一些质量很低的搜索结果,甚至错误的结果。因为想在网上搜索出一篇权威的文章是非常困难的。


另外,AI教师也是非常有用的应用。在AI老师出现以后,你的小孩可能就不再需要你的辅导就能完成各种题目的解答,且AI老师可以给出更好的解题思路,更准确的答案。AI教师除了可以教授K12,还能教授各个垂直领域的知识,这是一个非常巨大的市场,未来一定会出现各种各样的AI教师平台。


现有应用会逐步AI化


看文档真的很累,但是如果有一个掌握文档所有细节的人坐在你旁边为你服务,你还会那么累吗?这就是未来的AI文档。你只需要提出问题,AI就可以教你如何使用软件或者api而不需要一字一句的去研究文档。


除了文档,政府的各种政策、法律条文、保险条款等,未来都将AI化,我们每个人都可以拥有自己专有的“人工客服”,以前需要看几个小时文件才能得出的答案,现在可能一分钟就能得到答案了,而且更加准确!


未来的政府、保险公司等同类型主体间的信息化较量,很可能就是AI能力的较量。


软件行业的AI化,对程序员来说意味着什么?


政府的政策发布系统要升级、保险公司的客服系统要升级,AI医生、AI老师需要开发,这个工作量最终是由谁来完成?程序员。


很多人可能会说,既然AI会写代码了,那么为什么不是AI直接对现有系统进行升级或者自我孵化新应用?


AI完全替代程序员?这个事情真的那么简单吗?


仅从技术的角度来看,当前的AI虽然可以写一些小模块,但是要完成一个复杂系统的架构、研发、部署,AI当前还有些力不从心。现在AI可以完成一些点上的东西,但是我们的大型应用是需要把这些点组合起来,点与点之间还有很深的业务关联的。


虽然AI迭代速度很快,但是不要忘了即使是AI,要学习新的知识也需要人类去训练,而这个训练的成本不仅仅是人力成本,还有时间成本。想要AI完全达到人类程序员的理解能力和开发能力,可能不是一两年就能够实现的事情。


退一万步,即便是AI具备了编写复杂程序的能力,那谁来监督它,测试它?自动驾驶技术推出了这么多年,为什么没有很快替代人类司机?就是因为验证可靠性是一个非常复杂的过程,路遥才能知马力。信任关系不是一天两天的相处就能建立起来的。特别是这种涉及公司信息安全、软件可靠性的信任关系。


因此,正是由于将来会出现各种新型AI应用,以及现有应用需要AI化,才诞生了大量的工作量。这是一个行业基础设施的大升级,基建永远是最容易诞生工作机会的,程序员不仅不会失业,还会在这一波大基建建设中得到新一轮的工作机会


对于AI加持后的程序员工作畅享


‘基于型’程序员


我们现在开发软件,大多数情况都逃不过‘基于’二字。基于vue、基于react、基于flutter等等。


为什么会去‘基于’,就是因为这些框架或者库,能够提高我们的工作效率,减轻我们的心智负担,让开发复杂的应用变得简单。


我们基于的内容就是基础设施,而AI就是一种天然的基础设施。


未来一定会有大型的、成熟的AI平台和工具供我们‘基于’。当我们要开发一个AI应用,我们不需要自己去训练我们的基础模型,而是基于一个成熟的AI模型进行微调或者二次训练,就可以得到我们定制的AI模型,从而实现我们应用所需要的功能。


比如我们需要对公司开发的平台软件的文档进行AI化,我们不需要自己去训练一个AI机器人,而是基于现有大语言模型平台,新建一个AI实例,然后把我们的文档内容喂给这个实例,它就能变成一个我们定制的AI客服。我们还可以为这个实例设置各种参数,如定制它的聊天风格是严肃的还是活泼的、如定制它对于不相关的问题拒绝回答等等,当然这些参数都是AI平台提供的功能。


虽然AI可以帮我们编写如何调用平台api的代码,但是如何把这些代码集成到我们现有的软件中,还需要人类程序员的帮忙。


‘效率型’程序员


当有了AI大模型的加持,我们在写程序的过程中,可以让AI帮我们把一些点上的东西迅速完成掉,从而提高我们的编码效率。


如我们现在是一个vue新手,我们想实现一个列表的渲染。在从前我们可能需要去查阅vue官方文档,但是未来我们也许只需要问一问vue官方提供的机器人,它便能直接给出代码和解释。以前半个小时才能完成的工作量,现在10分钟便能完成。


而今日,已经有copilot,codeium这种ai编程工具,直接集成在编辑器中猜测我们的意图,很多情况下我们只需要按一按tab即可,甚至只需要在注释中描写下我们要实现的功能,它就能直接给出最终的代码。但是不要忘记,我们还是要写提示、写注释AI才能工作的,它只是让我们写得更快,即使没有AI,我们一样能完成最终的工能。


‘靠谱型’程序员


当我们一口气写完一个复杂的组件后,我们需要进行手工测试才能验证它的可靠性。而很多bug是在自测的时候很难发现的。有了AI加持后,我们写完一段代码就可以让它帮我们看看有没有什么明显的bug,然后迅速进行修复。把我们的代码喂给AI过一遍,可以让我们的代码更加靠谱,心里也更加有底气。这就相当于两个人在结对编程,而且和你结对的这个人水平还很高,很少出错,不会骂你是lowB,这样的编程体验,是不是真的好了很多?


一些建议


活到老,学到老。chatGPT和GEN应用的诞生让AI突然在今年爆发。我们能做的就是拥抱变化,积极去学习新式的编程方式,去学习使用AI带来的新工具、新平台。任何事情都有两面性,挑战和机遇永远都是并存。我们要做的不是自怨自艾,而是积极面对未来,未来不仅仅是AI,还会有各种各样不确定的事情等待着我们。正是这些不断出现的新东西让我们的人生更加丰富多彩,身在这个时代,真的很累,也真的很酷。


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

哇咔咔,体验了一把抖音的ChatGPT

说实在的,看到这里,让我忽然想吃过年时候蒸的豆包,不知道字节起这样一个名字有什么用意 最近一直在分享各种AIGC类的东西,感兴趣的可以看下主页历史干货。 你好,我是豆包 礼貌性的回复一句,你好,我是1点东西,我要开始使用你了。豆包是谁呢,可能有些朋友还不知道 ...
继续阅读 »


说实在的,看到这里,让我忽然想吃过年时候蒸的豆包,不知道字节起这样一个名字有什么用意


最近一直在分享各种AIGC类的东西,感兴趣的可以看下主页历史干货。


你好,我是豆包


礼貌性的回复一句,你好,我是1点东西,我要开始使用你了。豆包是谁呢,可能有些朋友还不知道


据悉,“豆包”的前身正是字节内部代号为“Grace”的AI项目。目前在AI浪潮下已经形成独立的AIGC产品供用户使用




刚进来可以看到经典的左右格局,左侧依然是历史问题记录区域,和其他国产GPT产品一样,有一些聚焦的功能模块。


不同的是,显的更加简洁大气,使用柔和不僵硬。毋庸置疑抖音的模型是基于字节产品多年数据沉淀最终服务于子节用户以及更好的发展。



可以看到回答问题响应很快,问题回答干脆不拖泥带水,同样在问题的最下方有点赞、复制、重新生成等功能。需要注意的是最下面有一个搜索功能,点击会跳转到今日头条进行搜索。


体验能力


左侧有英语小助手,先来看下英文能力怎么样




这能力杠杠的,学英语再也不是难事。接下来看全能写作助手体验。






接着问,测试上下文能力。




总体上还算总结的不错,我们问下小日本核废水排海事件




很明显,并不支持联网。




而且没有文生图功能




看下编程能力




编程能力也毫不逊色,最后可以问下GPT3.5都回答错误的问题。看看国产大模型咋样。




OK,今天的一个小分享暂时先到这里。上面的抖音的的申请体验链接:http://www.doubao.com/chat


最近涉猎于AIGC,总结了一些AI资料(实时更新),无套路分享给大家


1点东西AI资料地址



标签:

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

放弃使用Merge,开心拥抱Rebase!

iOS
1. 引言 大家好,我是比特桃。Git 作为现在最流行的版本管理工具,想必大家在开发过程中都会使用。由于 Git 中很多操作默认是采用 Merge 进行的,并且相对也不容易出错,所以很多人都会使用 Merge 来进行合并代码。但Rebase 作为 Git 中主...
继续阅读 »

1. 引言


大家好,我是比特桃。Git 作为现在最流行的版本管理工具,想必大家在开发过程中都会使用。由于 Git 中很多操作默认是采用 Merge 进行的,并且相对也不容易出错,所以很多人都会使用 Merge 来进行合并代码。但Rebase 作为 Git 中主要命令之一,我们还是有必要了解一下,在适合的场景中进行使用。


2. Rebase 的作用


Rebase 中文翻译过来:变基,我觉得这个翻译挺生硬的,导致很多人没有彻底理解变基的含义。我个人把 Rebase 意为 认爸爸,比如可以 Rebase 到马爸爸分支上,成为他的合理继承人。


上图为一次 Rebase 的情况,可以看到最终效果仿佛 Feature 分支没有存在过,新提交的 Commit 像真的在主分支上提交一样。而如果我们用 Merge 就会产生一个合并节点:


可能只看到一次合并所产生的 Commit 节点并没有什么,但实际项目中大概率会变成这样:


简直是乱的一批,仿佛看到了多年前其他人写的一堆代码,啥啥啥,这都是啥!反过来看看采用 Rebase 开发的真实项目,没有对比就没有伤害:


这也是为什么尤雨溪也比较推荐使用 rebase的原因:


3. Rebase 怎么用


其实很多人不用 Rebase ,一方面是不了解实际项目协同中怎么用。另一方面是用了,但问题很多,所以就误认为不好用从而再也不用。这里分享一下,我最近在做项目时所采用 Rebase 方面的协同流程(为了好说明,适当的进行了简化):


3.1 Checkout


首先,我们想从 master 分支上开发新的功能或者修复 bug ,需要 checkout出来一个分支。比如在

A节点中 checkout dev 分支,为了让场景更复杂,在 git checkout dev 分支后。master 上继续有人提交了B、C,形成如下Git 结构:


这里强调一下,很多人用 Rebase 出问题,都是出在了想要 Rebase 的分支是公共分支。其实这里的 dev 应该是只有自己用的分支才合适,回想一下,Git 本身就是分布式版本管理。其实不用远程仓库也是可以非常好的进行版本控制的,我们要将本地分支和远程分支的概念区分的开一些,这俩没有直接联系。所以你本机随便做个 NB 分支一样可以的,Rebase后没人知道你自己起了个什么鬼名字。


3.2 远程管理


如果自己的dev分支并不一定在一台电脑上开展,为了可以自己在多个电脑上开发,我们可以关联了一个自己的远程仓库。这一步是可选的。


3.3 开始变基


现在我们在 dev 上开发了D、E,然后dev rebase master,形成了A、B、C、D、E:


这里虽然看似已经一条直线了,但实际 只有 dev 知道自己的爸爸成为了 master,但 master 并没有认这个儿子。所以我们还需要:master merge dev,这样就在master上形成了一条完美的直线:


最后,再 git push origin master 到远程分支,完成本次开发。


3.4 善后


Rebase 后 dev 由于变基了,相当于已经认贼作父了,现在还想再认回来?休想!所以只能强制解决,在非保护分支中强制push到自己的远程仓库:git push --force origin dev,最后再将dev变基到自己的远程分支:git rebase origin dev,方便自己远程仓库的维护。至此,完成了一次rebase形式的开发,并且可以继续进行下次开发。


4. Rebase 的优缺点


先说说优点:

  • 保持提交历史的线性:使用 merge 合并分支时,会创建一个新的合并提交,从而在提交历史中形成一条新的分支。而使用 rebase,可以将提交记录直接添加到目标分支的末尾,从而保持提交历史的线性。
  • 减少不必要的合并提交:使用 merge 合并分支时,会创建一个新的合并提交,它可能会包含很多无意义的合并信息。而使用 rebase,可以将提交记录逐个添加到目标分支的末尾,避免了创建不必要的合并提交。
  • 更好的代码审查和追溯:使用 rebase,可以让提交历史更加直观和易于理解,从而更容易进行代码审查和问题追溯。
  • 避免冲突的产生:在合并分支时,可能会因为两个分支之间存在冲突而导致合并失败。而使用 rebase,可以在变基之前先解决这些冲突,从而避免了合并时出现的冲突。

总之,虽然 rebase 不是适用于所有情况的万能解决方案,但在大多数情况下,使用 rebase 能够帮助我们创建更加干净和直观的提交历史,提高团队的协作效率。


说了这么说好像都在说 Rebase 的优点,那 Rebase就没有缺点嘛?当然不是,要不然大家早就都从 Merge 转 Rebase了。Rebase 的缺点:

  • 解决冲突繁琐,rebase冲突 是按每个commit来对比的,merge冲突 是按最终结果来对比的,如果用rebase最好是经常去合并一下代码,不然长周期的开发如果就在最后rebase真的是解冲突解到人傻掉。
  • 没有合并记录,Merge 有记录,出了问题好解决。
  • 操作步骤相对繁琐。

5. 结语


协同开发最核心的问题其实就是合并,如何合理的合并,优雅的合并,是每个团队需要考虑的问题。Merge 和 Rebase 作为 Git 中主要的命令,其实各有各的优点,两个一起用也是很常见的。根据自身团队及项目情况,选择合适的方式才是最好的。最后,祝大家合并代码一切顺利~


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

iOS横滑组件实现

iOS
这是我早先实现的一个自定义横滑组件,本文回顾一下当时实现过程遇到的问题和细节,最后有源码地址 文中所有图片托管在Github上 所谓横滑组件其实就如图所示的效果: 列一下UI上的要求:每次滑动一页,有pageEnable的效果每次显示在屏幕中的item...
继续阅读 »

这是我早先实现的一个自定义横滑组件,本文回顾一下当时实现过程遇到的问题和细节,最后有源码地址




文中所有图片托管在Github上



所谓横滑组件其实就如图所示的效果:



列一下UI上的要求:

  • 每次滑动一页,有pageEnable的效果
  • 每次显示在屏幕中的item其实是三个而不是一个
  • 每个item的间距、视图与屏幕边缘的边距严格按照UI上样子

UICollectionView+pageEnable


使用UICollectionView并开启pageEnable是最容易想到的方案,我们来试一下能否满足需要


关键的几个参数如下所示

container.width = 375
collectionView.isPagingEnable = true
collectionView.width = 375
leftPadding = rightPadding = 16
cell.width = container.width - leftPadding - rightPadding
collectionView.contentInset = UIEdgeInset(0,16,0,0)

效果如下所示:



显然,没有达到预期:

  • 问题1,每次滑动停止后,cell的位置不对
    • 通过打印contentOffset得知,UIScrollView开启pagingEnable后的自动翻页,每次修改contentOffset的值等于UIScrollView.width
    • 而且我们无法自定义每次翻页移动的距离
  • 问题2,由于设置了collectionView.contentInset.left,所以第一cell可以移动到屏幕最左边而不能自动还原到初始位置

不甘心,继续调整


我画了一张图来表示要实现的效果:



  • 根据上图的效果,我们希望的效果是每次移动cell时移动的距离(两条红竖线之间的距离)是一个cell的宽度+cell之间的距离--cell.width+interval
  • 既然pageEnable特性每次移动的距离一定是scrollView.width,所以我们可以让scrollView.width = cell.width+interval
  • 这或许能解决上面显示异常问题

我们更新一下配置参数,如下:

leftPadding = rightPadding = 16
container.width = 375
collectionView.isPagingEnable = true
cell.width = container.width - leftPadding - rightPadding
interval = 8
collectionView.width = cell.width + interval
collectionView.contentInset = UIEdgeInset(0,0,0,interval) // 这一句可能会引起你的困惑,但经过测试必须设置成这样,否则效果有问题,本文不做详细解释,跟scrollView自身对于contentSize和contentOffset的调整有关

来看一下效果:



哇,好像不错!但还是有问题:

  • 我们希望同时显示三个cell,但该效果却只能显示1个cell
  • 这是因为collectionView的宽度刚好能显示下一个cell和一个interval,没有更多空间来显示其他cell了

这就很尴尬了,为了利用pageEnable的特性,我们不得不修改collectionView的宽度小一些,但这却导致无法足够的cell个数


所以,结论是:❌


UICollectionView + UIScrollView


在调研其他技术方案时,受一Paging a overflowing collection view启发,可以使用一个UICollectionView和一个UIScrollView一同实现类似效果


核心思想如下:

  • 单独用一个UIScrollView,利用pageEnable特性来实现符合要求的横滑、拖拽翻页效果
  • 单独用一个UICollectionView来利用它的cell显示、复用机制
  • UIScrollView是不显示的,只用它的拖拽手势能力。当拖拽UIScrollView时,将contentOffset的移动应用到UICollectionView中

具体实现过程中有些细节需要注意,比如:

  1. collectionView的contentInset需要设置
  2. 将scrollView的移动应用到collectionView中时如何计算准确
  3. 需要关闭collectionView的panGesture

再放一下效果



结论是:✅


源码地址:SlideView.swift


优缺点


优点很明显:

  • 既复用了UIScrollView的pageEnable手势和动画效果,也复用了UICollectionView的cell复用机制
  • 由于复用了UICollectionView,所以相比通过UIScrollView自定义实现,在一些用户交互体验上可能更好,比如在快速横滑时,自定义的实现可能就没办法快速的准备好每一个cell并无缝从上一页切换过来,可能会有点卡顿
  • 所有实现细节都是通过系统官方的public API,不存在任何trick行为,稳定性好

缺点:


在用户体验上没发现缺点。只是在封装为独立组件时需要注意更多细节,比如:

  • 该组件将CollectionView封装了起来,所以必须给外部使用者暴露dataSource和delegate等必要的回调和数据源方法

使用UIScrollView完全自定义实现


我还看过另一种方案:

  • 自己创建cell视图,添加到UIScrollView上
  • 完全由自己来控制cell的复用和显示逻辑
  • 滑动手势和效果方面,利用UIScrollViewDelegate方法来控制抬起手指后移动到到下一个或上一个cell的效果(该效果我曾经也实现过,可以参考设计与Swipe-Delete不冲突的UIPageViewController

这个思路看上去应该是可行的,我也看过类似的源码实现,是Github上的一个代码


但该源码的显示逻辑写的不好:

  • 每次切换cell时,会同时通过delegate要求更新所有的cell数据(显示在屏幕中的cell和在缓存池中未用到的cell)

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

iOS17兼容问题,[NSURL URLWithString:]逻辑BUG,影响WKWebView

iOS
[NSURL URLWithString:urlString]默认实现逻辑变动 [NSURL URLWithString:urlString]以前的逻辑是urlString有中文字符就返回nil,现在是默认对非法字符(包含中文)进行%转义。 URLWithSt...
继续阅读 »

[NSURL URLWithString:urlString]默认实现逻辑变动


[NSURL URLWithString:urlString]以前的逻辑是urlString有中文字符就返回nil,现在是默认对非法字符(包含中文)进行%转义。


URLWithString:方法并没有给出说明,但是iOS17新增了URLWithString:encodingInvalidCharacters:方法,具体可以参照此方法。

/// Initializes and returns a newly created `NSURL` with a URL string and the option to add (or skip) IDNA- and percent-encoding of invalid characters.
/// If `encodingInvalidCharacters` is false, and the URL string is invalid according to RFC 3986, `nil` is returned.
/// If `encodingInvalidCharacters` is true, `NSURL` will try to encode the string to create a valid URL.
/// If the URL string is still invalid after encoding, `nil` is returned.
///
/// - Parameter URLString: The URL string.
/// - Parameter encodingInvalidCharacters: True if `NSURL` should try to encode an invalid URL string, false otherwise.
/// - Returns: An `NSURL` instance for a valid URL, or `nil` if the URL is invalid.
+ (nullable instancetype)URLWithString:(NSString *)URLString encodingInvalidCharacters:(BOOL)encodingInvalidCharacters API_AVAILABLE(macos(14.0), ios(17.0), watchos(10.0), tvos(17.0));

附带的BUG


这一个改动本来没有什么大问题,但问题是有BUG。


如果urlString中没有中文,那urlString里原有的%字符不会转义。

(lldb) po [NSURL URLWithString:@"http://a.com?redirectUri=http%3A%2F%2Fb.com"]
http://a.com?redirectUri=http%3A%2F%2Fb.com

如果urlString中有中文字符,那么中文字符和%字符都会被转义,最终会影响运行效果。


(我就是因为这个BUG,从而导致原本能正常进行302重定向的页面无法重定向。)

(lldb) po [NSURL URLWithString:@"http://a.com?title=标题&redirectUri=http%3A%2F%2Fb.com"]
http://a.com?title=%E6%A0%87%E9%A2%98&redirectUri=http%253A%252F%252Fb.com

修改方案


对原方法进行替换,保证[NSURL URLWithString:urlString]在iOS17系统上的运行逻辑和iOS17以下系统保持一致。这样对于现有代码逻辑的影响最小。

#import "NSURL+iOS17.h"

@implementation NSURL (iOS17)

+(void)load {
[self sv_swizzleClassMethod:@selector(URLWithString:) withClassMethod:@selector(wt_URLWithString:) error:NULL];
}

+ (instancetype)wt_URLWithString:(NSString *)URLString {
if (@available(iOS 17.0, *)) {
return [self URLWithString:URLString encodingInvalidCharacters:NO];
} else {
return [self wt_URLWithString:URLString];
}
}

@end

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

原生应用要亡了!

iOS
跨平台混合应用(及替代方案)取代了性能优先的原生应用 纯粹的原生应用通常是一种依赖于平台的GUI程序, 它使用特定操作系统的本地开发语言和GUI框架. 例如, Gedit 是一个原生应用, 因为它使用 C 和 GTK 作为实现依赖. Notepad++ 是一...
继续阅读 »

跨平台混合应用(及替代方案)取代了性能优先的原生应用




纯粹的原生应用通常是一种依赖于平台的GUI程序, 它使用特定操作系统的本地开发语言和GUI框架. 例如, Gedit 是一个原生应用, 因为它使用 C 和 GTK 作为实现依赖. Notepad++ 是一个原生应用, 因为它使用 C/C++ 和 Win32 GUI API. 这些原生应用还保留了操作系统特有的UI/UX原则和本地功能. 因此, 电脑用户可以轻松上手并与其他内置原生应用一起使用这些应用. 这些传统的原生应用即使在低端硬件上也能流畅运行, 因为它们没有使用中间消息传递模块或嵌入式渲染/代码执行引擎--它们只是触发内置SDK功能的二进制文件. 原生桌面应用和移动应用开发的情况都是一样的.


混合应用开发运动结束了原生应用开发的黄金时代, 但却创造了一种新的方式, 可以在创纪录的时间内构建类似原生的跨平台应用. 此外, 混合应用的性能问题导致了另一种使用自定义渲染表面和代码执行环境的类原生应用的发展.


让我们来谈谈传统原生应用开发的弊端.


Why Native Apps Are the Best 为什么原生应用是最好的


每个操作系统通常都预装了通用的GUI软件程序. 例如, Ubuntu提供了原生终端, 文本编辑器, Settings应用, 文件管理器等. 这些内置应用无疑遵循了相同的UI/UX原则, 而且由于出色的软件设计和原生SDK的使用, 占用的磁盘空间, 内存和CPU处理能力更低. 第三方原生应用的工作原理也与内置操作系统应用相同. 它们不会过度使用系统资源, 而是根据为用户提供的功能公平地使用计算能力.


从所有面向用户的角度来看, 原生应用都非常出色. 它们绝不会拖慢低端电脑的运行速度. 此外, 它们也不会敦促用户改变操作系统特有的UI/UX做法. 看看Remmina RDP(原生GUI程序)与Ubuntu内置终端的对比:



 Remmina和Ubuntu上的终端


每个移动操作系统都提供了原生SDK, 用于开发特定平台的应用捆绑包. 例如, 您可以使用Android SDK构建高性能, 轻量级和用户友好的移动应用. 看看著名的VLC媒体播放器的Android版本是如何通过XML布局实现"关于"视图的:



 VLC Android项目实现了原生应用视图.


混合应用: 类似本地的Web应用


即使原生应用为用户提供了最好的GUI程序, 为什么现代开发人员还是开始开发混合应用呢? 从应用用户的角度来看, 原生应用是非常好的, 但它们却给应用开发人员带来了一个关键问题. 尽管一些操作系统提供了与POSIX标准类似的底层应用接口, 但大多数内置的应用开发SDK都提供了不同编程语言的不同应用接口. 因此, 应用开发人员不得不为一个软件产品维护多个与平台相关的代码库. 这种情况增加了跨平台原生应用的开发难度, 因为一个新功能需要多个特定平台的实现.


混合应用开发通过提供统一的SDK和语言来为多个平台开发应用, 从而解决了这一问题. 开发人员开始使用Electron, NW.js, Apache Cordova和类似Ionic的框架, 利用Web技术构建跨平台应用. 这些框架在Web浏览器组件内呈现基于HTML的类原生应用GUI, 并通过本地-JavaScript接口和桥接器调用基于JavaScript封装的特定平台本地API. 看看Skype如何在Ubuntu上用HTML呈现类似本地的屏幕:



 Skype的首选项窗口.


桌面应用配有Web浏览器和Node.js运行模块. 移动应用则使用现有的特定平台浏览器视图(即Android Webview).


混合应用解决方案解决了开发人员的问题, 却给用户带来了新的麻烦. 由于基于Web的解析和渲染, 混合应用的运行速度比原生应用慢数百倍. 一个简单的跨平台计算器应用可能会占用数百兆字节的存储空间. 运行多个跨平台应用窗口就像运行多个重型Web浏览器. 不幸的是, 大多数用户甚至感觉不到这些问题, 因为他们使用的是功能强大的现代硬件组件.


混合替代方案的兴起


一些开发人员仍然非常关注应用的性能--他们需要应用在低端机器上也能使用. 因此, 他们开始开发更接近原生应用的跨平台应用, 而不使用Web视图驱动方法. 开发人员开始使用Flutter和类似React Native的框架. 与基于网页视图的方法相比, 这些框架为跨平台应用开发提供了更好的解决方案, 但它们无法像真正的原生应用那样进行开发.


Flutter没有使用原生的, 特定平台的UI/UX原则. React Native在每个应用中嵌入了JavaScript引擎, 性能不如原生应用. 与基于网页视图的方法相比, 这些混合替代方案无疑提供了更好的跨平台开发解决方案, 但在应用大小和性能方面仍无法与真正的原生应用相媲美.


你可以从以下报道中了解Flutter如何与混合应用开发(Electron)竞争:


拜拜Electron, 你好Flutter


混合(和替代方案)赢得了软件市场!


每个商业实体都试图通过开发网站和Web应用进入互联网. 与独立的应用相比, 计算机用户更愿意使用在线服务. 因此, Web浏览器开始改进, 增加了各种以开发者为中心的功能, 如新的Web API, 可访问性支持, 离线支持等. 对开发人员友好的JavaScript鼓励每个开发人员在任何情况下都使用它.


借助混合应用开发技术, 开发人员可以在最短时间内将现有的Web应用转化为桌面应用(如WhatsApp, Slack 等). 他们将React, Vue和Svelte应用与本地窗口框架封装在一起, 创建了功能齐全的跨平台桌面应用. 这种方法节省了数千开发人员的工时和开发成本. 因此, Electron成为了现代桌面应用的开发解决方案. 然后, 一个只需几兆内存和存储空间的代码编辑器程序就变成了现在这样:



 Visual Studio Code占用约600M内存.


一般用户不会注意到这一点, 因为每个人都至少使用8或16GB内存. 此外, 他们的存储设备也不会让他们感受到 500M字节代码编辑器的沉重(TauriNeutralinojs解决了应用大小的问题, 但它们仍在制作混合应用).


同样, 如果应用变得缓慢, 典型的移动用户往往会将责任归咎于设备. 现代用户经常升级设备, 以解决应用开发人员造成的性能问题. 因此, 在当今的软件开发行业, 混合应用开发比本地应用开发更受欢迎. 此外, 混合替代方案(如 Flutter, React Native等)也变得更加流行.


总结一下


混合应用开发框架和其他替代框架为构建跨平台应用提供了一个高效, 开发人员优先的环境. 但是, 从用户的角度来看, 这些开发方法会产生一些隐藏的性能和可用性问题. 现代强大的硬件组件处理能力可以掩盖这些开发方法中的技术问题. 此外, 与依赖平台的原生应用开发相比, 这些方法提供了更富有成效, 开发人员优先的开发环境. 编程新手开始学习桌面应用的Electron开发, 移动应用的Flutter开发和React Native开发, 就像他们跳过C作为他们的第一门编程语言一样.


因此, 原生应用的黄金时代走到了尽头. 幸运的是, 程序员仍在维护旧的原生应用代码库. 操作系统永远不会将其预先包含的应用迁移到混合应用中. 与此同时, 一些开发人员使用类似SDL的跨平台, 高性能原生绘图库构建轻量级跨平台应用. 尽管现代混合应用开发和替代方法已成为软件行业的默认方式, 但我们仍可以保留现有的纯原生定位.


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

如何消除异步的传染性

web
本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。 前言 各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想...
继续阅读 »

本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。



前言


各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想着来分享一下!好了,接下来笔者会从两个方面来说这个知识点,一方面是概念,另一方面就是如何消除。


什么是 异步传染性


笔者通过一个例子来介绍异步传染性的概念。


CleanShot 2023-08-30 at <a href=10.10.55@2x.png" loading="lazy" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/448eaad1c5f34f319bc3361fcad882ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1234&h=528&s=154964&e=png&b=282a35"/>


上图中由于m2中的fetch是异步的,导致了使用m2m1变成了async functionmain 又使用了m1,从而main也变成了async function。类似这种现象就叫做异步的传染性。(可能你会觉得,为什么main不直接调m2,我们此处是为了理解这个概念,不要钻牛角尖😁)


m2就好像病毒🦠,m1明知道到m2有毒,还要来挨着,结果就被传染了,main也是一样。


那什么是消除传染性呢?就是希望不要 async/await,让mian、m1变成纯函数调用。也就是mian、m1不依赖fetch的状态。期望像下面这样调用:
CleanShot 2023-08-30 at <a href=10.52.24@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba99c7db117e46319d778002889c51ee~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=924&h=498&s=64284&e=png&b=282a35"/>



纯函数:



  1. 输入决定输出: 纯函数的输出完全由输入决定,即相同的输入始终产生相同的输出。这意味着函数不依赖于外部状态,也不会对外部状态进行修改。

  2. 没有副作用: 纯函数没有副作用,即在函数的执行过程中不会对除函数作用域外的其他部分产生影响。它不会修改全局变量、改变输入参数或进行文件IO等操作。




纯函数在函数式编程中具有重要作用,因为它们易于理解、测试和维护。由于不依赖于外部状态,纯函数可以很好地并行执行,也有助于避免常见的错误,例如竞态条件和不确定性行为。



接下来咱们就分析一下要如何实现消除。


如何消除


当我们把async/await去掉之后,就变成了同步调用,那么m2返回的肯定是pending状态的promisemain得到的也是,肯定达不到我们想要的效果。


那我们能不能等promise变成fulfilled/rejected状态再接着执行main


可以,第一次调用main,我们直接throw,第一次调用就会终止,然后等promise变成fulfilled/rejected状态,我们将返回结果或错误信息缓存一下,再调用一次main,再次调用时存在缓存,直接返回缓存即可,此时也就变成了同步。流程图如下:


CleanShot 2023-08-30 at <a href=11.30.26@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc591e6d9bf24b4eb0d3e78fecf5dcc1~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1590&h=1048&s=165391&e=png&b=fdfdfd"/>


具体实现如下:
CleanShot 2023-08-30 at <a href=11.34.06@2x.png" loading="lazy" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9b4193e4c6c4a35850c487f1ad0bcbc~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1494&h=1336&s=353606&e=png&b=282a35"/>


效果如下:
CleanShot 2023-08-30 at 11.44.35.gif


到此本次分享的内容就完了,感谢阅读!


总结


本文通过简单的例子,描述了什么是异步的传染性,以及如何利用缓存throw重写fetch实现了消除异步的传染性


如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^<

作者:Lvzl
来源:juejin.cn/post/7272751454497996815
/code>。

收起阅读 »

给你介绍一个工具,帮你找到未来的努力方向

前言 很多人都会认为,想要找到自己的人生目标是非常难的,主要有 2 个原因: 现在定的目标不一定是正确的,可能过几年之后就变了,何必浪费时间呢 不知道怎么找到自己的人生目标 你是不是也是这么认为的呢? 以前的我也是这么觉得的,所以从来没有探究过:我的人生目...
继续阅读 »

前言


很多人都会认为,想要找到自己的人生目标是非常难的,主要有 2 个原因:



  • 现在定的目标不一定是正确的,可能过几年之后就变了,何必浪费时间呢

  • 不知道怎么找到自己的人生目标


你是不是也是这么认为的呢?


以前的我也是这么觉得的,所以从来没有探究过:我的人生目标是什么?


现在的我却不这么想。探索自己的人生目标,是一件非常有意义,并应该定期去做的事情。因为:



  • 一个明确的、有意义的目标,是产生内驱力的关键要素之一,这个在我的《驱动力》读后感文章中有介绍;

  • 有了人生目标之后,我们对自己的成长就会有相应的规划,带着目的去成长是最快的。即使过了几年,随着人生阅历的增长,我们的人生目标发生了变化也没关系。因为在这个过程中,我们的成长是显著的。而能力的可迁移性也可以帮助自己更容易达成新的人生目标。

  • 探索人生目标的过程,就是一次重新思考自己人生和未来的过程。即使确定不了最终的人生目标,但你可能会发现自己的一些问题和薄弱点,找到未来努力的方向。


如果想通这点,阻碍你的第 1 个困难点已经解决了。接下来我介绍一个工具,帮助你解决第 2 个困难点。


插图1.png


认识 ikigai 人生四叶草


这个工具叫 ikigai 人生四叶草模型,也称为 ikigai 幸福公式。它是日本人追求幸福和满足感的一个重要概念。


ikigai 是由两个词汇组合而成: "iki"意为生命、存在,"gai"意为价值、意义。ikigai 可以被理解为生活的意义存在的价值


让我们先来看下 ikigai 人生四叶草的全貌:


插图2.jpeg


ikigai 强调了四个关键要素的平衡与融合,它们是:



  • 你热爱的事:指的是你对什么感兴趣、乐于投入并带来快乐的事物或活动。

  • 你擅长的事:指的是你具备的技能、知识和专长,能够在某个领域或事业中有所贡献。

  • 世界需要的:指的是你认为社会所需要的、能够为社会做出积极贡献的事物。

  • 你可以得到报酬的事:指的是你能够获得物质或非物质回报的事物,能够为你提供经济支持和满足。


图中每个大圆圈代表 1 个关键要素。再看两个圆圈相交的部分:



  • 激情:如果你在做自己热爱并很擅长的事情,那肯定很有干劲。

  • 使命:如果你非常热爱这个事业,并且认为这项事业是对世界有贡献的,那会充满使命感。

  • 职责:社会(公司)需要你做事,并且会给你报酬,那就是打工仔的职责了。

  • 专业:如果你非常擅长做某件事情,并且别人愿意付钱请你做事,那说明你是专业的。


再看有 3 个圆圈相交的部分,它代表了缺少了其中一个关键要素,那是不美满的:



  • 如果缺少报酬,那么你的经济是不富裕的,虽然你会觉得自己的工作快乐并充实。

  • 如果做的是自己不擅长的事情,有时候就会觉得很不确定,不知道能不能成功。

  • 如果缺少热爱,你就会感到空虚,心里空荡荡的。

  • 如果你认为自己的工作非常琐碎,就会觉得自己是不被重用的。


4 个圆圈重叠的部分就是 ikigai,它代表了 4 个关键要素的融合,也就是我们的人生目标了。


ikigai 人生四叶草的用法


寻找自己的人生目标


ikigai 人生四叶草可以帮助你寻找人生目标。为了让你更好地应用这个工具,我创建了 ikigai 人生四叶草画布工具,关注我的公众号,并回复【ikigai】即可获取这个画布工具。


插图3.png


可以按照以下步骤来使用这个工具:



  • 按照顺序,单独思考 4 个大圈的事项,尽量罗列多一点。

  • 然后按照顺序,思考 2 个大圈相交的事项。

  • 然后找到 ikigai。

  • 最后得出自我总结。


这里最关键的就是 4 个大圈的事项,你需要注意几点:




  • 喜爱的事可以简单分类为消费型和生产型,比如读书是消费型,写作是生产型。




  • 擅长的事除了当前的职业技能之外,还有一些能力上的。主要是思考自己的优势项,如果你不能很好地评估自己优势项的话,可以在网上找些在线测评,或者找周围不同角色的人给你评价。




  • 其实世界需要很多事情,我们不可能把所有事情都列出来。所以,在思考世界需要你做的事情时,可以从以下角度思考:



    • 当前你的身份带来的责任,比如父亲。

    • 当前你的工作。

    • 跟你热爱的事项相关联的,世界可能需要你做的事。

    • 跟你擅长的事项相关联的,世界可能需要你做的事。




  • 别人会付钱的事就是最后的过滤项,主要从世界需要你做的事项中去筛选出别人会付钱的事。




最后的【自我总结】就是在探索完成之后,梳理自己的感受总结以及未来努力的方向。


好了,画布的使用方法介绍完了,这个就是探索自己的人生目标的方法。接下来我介绍一个虚构的例子。


插图4.png


你可能一眼就看出来了,这是一个前端开发技术宅的探索结果。很多人一开始填这个画布的时候也会是这样子,非常简单,事项很少,因为真的不知道自己热爱的事情和擅长的事情。


没关系,当我们把当前的状况填好的时候,可能已经有一些启发了。回到这个例子,可以参考【自我总结】部分:



  • 从图中可以看出,小 A 在前端开发事项已经 3 缺 1 了,如果把最后的热爱补上的话,那不就是完整的 ikigai 了吗?所以,小 A 很有必要思考一下:我喜欢编程吗?我是不是有必要在前端开发的其他领域探索一下自己的兴趣?比如大数据可视化、h5 游戏、虚拟化?

  • 如果真的不喜欢编程,那也不能强求,那就需要重新寻找自己热爱的事项了,因为热爱是人生幸福的最核心前提。可以看到,小 A 现在罗列的都是消费型热爱事项,这些事项是不会同时满足世界需要+别人付钱的,所以小 A 需要思考:我还有其他的生产型的喜爱吗?


制定成长规划


小 A 静下心来思考和感受,发现自己很喜欢尝试新的游戏和新的玩法,平常也很喜欢跟朋友分享好玩的游戏,并且把一些很有意思的游戏心得和游戏经历分享给朋友,大家听了小 A 的分享,也在游戏中获得了快乐,小 A 自己也感到很快乐。


于是,小 A 在【我爱做的事】里面又添加了“分享游戏、分享游戏心得”,并开始思考,世界上有很多很有意思的游戏,但是大部分人都不了解它们,那多可惜呀,如果自己可以帮助其他人找到适合自己的那款游戏,并从中获得快乐,那该有多好呀。


根据这个思路,小 A 就又完成了新一轮的人生目标的探索过程,结果如下:


插图5.png


通过思考和探索,小 A 挖掘到了自己内心隐藏的热爱事项,还思考了如何把这个热爱转变成更有意义的事业。现在,小 A 有了另外一个 ikigai 了,那就是游戏推广运营,但是,小 A 还缺少关键技能呀,想要做成这个事业的话,小 A 需要学习什么知识?培养什么能力呢?


在有了明确的事业目标之后,小 A 就可以按照这个目标来规划自己未来的成长方向了。在了解了行业知识之后,就可以给自己制定未来 1 年的成长计划。还是那句话,带着目的去成长才是最快的!


插图6.png


评估自己的工作


除了寻找自己的人生目标之外,ikigai 人生四叶草模型还有另外一种用法,就是用来评估我们的工作,帮助我们做决策


评估版的 ikigai 人生四叶草画布我也为你准备好了,非常简单,可以看看:


插图7.png


根据 ikigai 幸福公式的定义,我们可以从 4 个维度来评估自己的职业与自己的人生意义的关联程度。因此,你可以给自己想要做的事业从 4 个维度进行打分,1 ~ 10 分,按照自己内心的统一标准来进行打分即可。


【最终得分】一列是 4 个维度得分的加总平均分数。


【加权得分】一列是给某些维度加了权重系数之后的平均分数,这个权重系数可以根据自己的偏好来决定。如果你觉得,对于现在的自己来说,热爱非常重要,那可以给热爱维度加一个非常高的权重。如果你没什么想法的话,我推荐画布工具的默认权重,【热爱】维度给权重 3,【世界需要】维度给权重 2。因为我认为,对于自己的事业,热爱是最重要的。提供价值排在第 2,也非常重要。而技能可以成长、只要有价值,别人就愿意付钱,因此【擅长】和【付钱】就不加权重了。


好了,先来看看小 A 给自己的“前端开发”职业做的评估吧:


插图8.png


经过评估得出分数值后,你能够得到什么信息?


什么信息都得不到,因为没有对比。通常,我们可以做以下 2 种对比:



  • 横向对比:跟其他职业对比,一般是在我们要做转行决策时使用。

  • 纵向对比:跟上一次评估对比,我们可以定期,比如每半年,至少每年,给自己当前的职业做下评估,然后再跟上一次的评估进行对比,这样我们可以通过一些变化项得出我们最近的收获,以及总结出接下来的努力方向。


好了,小 A 又给自己新的热爱事业做了一次评估:


插图9.png


你可以关注一下【最终得分】和【加权得分】的差异点,应该可以理解为什么需要添加适当的权重了。


可以看到,小 A 对游戏推广运营职业的评估中,【我擅长】、【需要我】、【支付给我】相对前端开发职业来说都要低,但这只是暂时的,随着相关技能的学习以及相关行业知识的熟悉,这 3 项都有非常大的成长空间。但相对的,对于前端开发职业来说,薄弱的一项【我喜爱】,就非常难以提升了。


好了,小 A 通过人生意义的探寻以及对于适合自己事业的评估,找到了自己未来的努力方向,接下来,就可以全力以赴地、坚定地往前走了!


小结


今天,我给你介绍了一个工具,叫 ikigai 人生四叶草模型,这个工具可以帮助你:



  • 探寻自己的人生目标

  • 挖掘自己潜在的事业

  • 重新审视自己的内心,找到未来努力的方向

  • 帮助自己做职业决策


可以把 ikigai 人生四叶草模型浓缩成一句话:


ikigai = 热爱 * 擅长 * 价值 * 回报


你可以记住这个本质公式,后续碰到一些相关决策时可以使用这个公式来进行快速地评估。


除此之外,我也提供了一套完整的 ikigai 人生四叶草画布工具,具体用法我已经通过案例详细介绍了,如果你可以通过它定期审视自己的内心,一定会有所收获。


插图10.png



【讨论问题】


如果你认可人生目标的意义和作用,欢迎分享一下你在探索自己人生目标过程中的经验哈。


欢迎在评论区分享你的想法,一起讨论。




作者:潜龙在渊灬
来源:juejin.cn/post/7268260762402340883

收起阅读 »

基于 Axios 封装一个完美的双 token 无感刷新

web
用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。 标识登录状态的方案有两种: session 和 jwt。 session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出...
继续阅读 »

用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。


标识登录状态的方案有两种: session 和 jwt。


session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出 cookie 里 id 对应的 session 对象,就可以拿到用户信息。



jwt 不在服务端存储,会直接把用户信息放到 token 里返回,每次请求带上这个 token,服务端就能从中取出用户信息。



这个 token 一般是放在一个叫 authorization 的 header 里。


这两种方案一个服务端存储,通过 cookie 携带标识,一个在客户端存储,通过 header 携带标识。


session 的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有。



jwt 的方案天然支持分布式,因为信息保存在 token 里,只要从中取出来就行。



所以 jwt 的方案用的还是很多的。


服务端把用户信息放入 token 里,设置一个过期时间,客户端请求的时候通过 authorization 的 header 携带 token,服务端验证通过,就可以从中取到用户信息。


但是这样有个问题:


token 是有过期时间的,比如 3 天,那过期后再访问就需要重新登录了。


这样体验并不好。


想想你在用某个 app 的时候,用着用着突然跳到登录页了,告诉你需要重新登录了。


是不是体验很差?


所以要加上续签机制,也就是延长 token 过期时间。


主流的方案是通过双 token,一个 access_token、一个 refresh_token。


登录成功之后,返回这两个 token:



访问接口时带上 access_token 访问:



当 access_token 过期时,通过 refresh_token 来刷新,拿到新的 access_token 和 refresh_token



这里的 access_token 就是我们之前的 token。


为什么多了个 refresh_token 就能简化呢?


因为如果你重新登录,是不是需要再填一遍用户名密码?而有了 refresh_token 之后,只要带上这个 token 就能标识用户,不需要传用户名密码就能拿到新 token。


而 access_token 一般过期时间设置的比较短,比如 30 分钟,refresh_token 设置的过期时间比较长,比如 7 天。


这样,只要你 7 天内访问一次,就能刷新 token,再续 7 天,一直不需要登录。


但如果你超过 7 天没访问,那 refresh_token 也过期了,就需要重新登录了。


想想你常用的 APP,是不是没再重新登录过?


而不常用的 APP,再次打开是不是就又要重新登录了?


这种一般都是双 token 做的。


知道了什么是双 token,以及它解决的问题,我们来实现一下。


新建个 nest 项目:


 npx nest new token-test


进入项目,把它跑起来:


npm run start:dev

访问 http://localhost:3000 可以看到 hello world,代表服务跑成功了:



在 AppController 添加一个 login 的 post 接口:



@Post('login')
login(@Body() userDto: UserDto) {
console.log(userDto);
return 'success';
}

这里通过 @Body 取出请求体的内容,设置到 dto 中。


dto 是 data transfer object,数据传输对象,用来保存参数的。


我们创建 src/user.dto.ts


export class UserDto {
username: string;
password: string;
}

在 postman 里访问下这个接口:



返回了 success,服务端也打印了收到的参数:



然后我们实现下登录逻辑:



这里我们就不连接数据库了,就是内置几个用户,匹配下信息。


const users = [
{ username: 'guang', password: '111111', email: 'xxx@xxx.com'},
{ username: 'dong', password: '222222', email: 'yyy@yyy.com'},
]

@Post('login')
login(@Body() userDto: UserDto) {
const user = users.find(item => item.username === userDto.username);

if(!user) {
throw new BadRequestException('用户不存在');
}

if(user.password !== userDto.password) {
throw new BadRequestException("密码错误");
}

return {
userInfo: {
username: user.username,
email: user.email
},
accessToken: 'xxx',
refreshToken: 'yyy'
};
}

如果没找到,就返回用户不存在。


找到了但是密码不对,就返回密码错误。


否则返回用户信息和 token。


测试下:


当 username 不存在时:



当 password 不对时:



登录成功时:



然后我们引入 jwt 模块来生成 token:


npm install @nestjs/jwt

在 AppModule 里注册下这个模块:



JwtModule.register({
secret: 'guang'
})

然后在 AppController 里就可以注入 JwtService 来用了:



@Inject(JwtService)
private jwtService: JwtService

这个是 nest 的依赖注入功能。


然后用这个 jwtService 生成 access_token 和 refresh_token:



const accessToken = this.jwtService.sign({
username: user.username,
email: user.email
}, {
expiresIn: '0.5h'
});

const refreshToken = this.jwtService.sign({
username: user.username
}, {
expiresIn: '7d'
})

access_token 过期时间半小时,refresh_token 过期时间 7 天。


测试下:



登录之后,访问别的接口只要带上这个 access_token 就好了。


前面讲过,jwt 是通过 authorization 的 header 携带 token,格式是 Bearer xxxx


也就是这样:



我们再定义个需要登录访问的接口:


@Get('aaa')
aaa(@Req() req: Request) {
const authorization = req.headers['authorization'];

if(!authorization) {
throw new UnauthorizedException('用户未登录');
}
try{
const token = authorization.split(' ')[1];
const data = this.jwtService.verify(token);

console.log(data);
} catch(e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}

接口里取出 authorization 的 header,如果没有,说明没登录。


然后从中取出 token,用 jwtService.verify 校验下。


如果校验失败,返回 token 失效的错误,否则打印其中的信息。


试一下:


带上 token 访问这个接口:



服务端打印了 token 中的信息,这就是我们登录时放到里面的:



试一下错误的 token:



然后我们实现刷新 token 的接口:


@Get('refresh')
refresh(@Query('token') token: string) {
try{
const data = this.jwtService.verify(token);

const user = users.find(item => item.username === data.username);

const accessToken = this.jwtService.sign({
username: user.username,
email: user.email
}, {
expiresIn: '0.5h'
});

const refreshToken = this.jwtService.sign({
username: user.username
}, {
expiresIn: '7d'
})

return {
accessToken,
refreshToken
};

} catch(e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}

定义了个 get 接口,参数是 refresh_token。


从 token 中取出 username,然后查询对应的 user 信息,再重新生成双 token 返回。


测试下:


登录之后拿到 refreshToken:



然后带上这个 token 访问刷新接口:



返回了新的 token,这种方式也叫做无感刷新。


那在前端项目里怎么用呢?


我们新建个 react 项目试试:


npx create-react-app --template=typescript token-test-frontend


把它跑起来:


npm run start


因为 3000 端口被占用了,这里跑在了 3001 端口。



成功跑起来了。


我们改下 App.tsx


import { useCallback, useState } from "react";

interface User {
username: string;
email?: string;
}

function App() {
const [user, setUser] = useState<User>();

const login = useCallback(() => {
setUser({username: 'guang', email: 'xx@xx.com'});
}, []);

return (
<div className="App">
{
user?.username
? `当前登录用户: ${ user?.username }`
: <button onClick={login}>登录button>

}
div>
);
}

export default App;

如果已经登录,就显示用户信息,否则显示登录按钮。


点击登录按钮,会设置用户信息。


这里的 login 方法因为作为参数了,所以用 useCallback 包裹下,避免不必要的渲染。



然后我们在 login 方法里访问登录接口。


首先要在 nest 服务里开启跨域支持:



在 main.ts 里调用 enbalbeCors 开启跨域。


然后在前端代码里访问下这个接口:


先安装 axios


npm install --save axios

然后创建个 interface.ts 来管理所有接口:


import axios from "axios";

const axiosInstance = axios.create({
baseURL: 'http://localhost:3000/',
timeout: 3000
});

export async function userLogin(username: string, password: string) {
return await axiosInstance.post('/login', {
username,
password
});
}

async function refreshToken() {

}
async function aaa() {

}

在 App 组件里调用下:


const login = useCallback(async () => {
const res = await userLogin('guang', '111111');

console.log(res.data);
}, []);

接口调用成功了,我们拿到了 userInfo、access_token、refresh_token



然后我们把 token 存到 localStorage 里,因为后面还要用。


const login = useCallback(async () => {
const res = await userLogin('guang', '111111');

const { userInfo, accessToken, refreshToken } = res.data;

setUser(userInfo);

localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
}, []);


在 interface.ts 里添加 aaa 接口:


export async function aaa() {
return await axiosInstance.get('/aaa');
}

组件里访问下:



const xxx = useCallback(async () => {
const res = await aaa();

console.log(res);
}, []);


点击 aaa 按钮,报错了,因为接口返回了 401。


因为访问接口时没带上 token,我们可以在 interceptor 里做这个。


interceptor 是 axios 提供的机制,可以在请求前、响应后加上一些通用处理逻辑:



添加 token 的逻辑就很适合放在 interceptor 里:



axiosInstance.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem('access_token');

if(accessToken) {
config.headers.authorization = 'Bearer ' + accessToken;
}
return config;
})

现在再点击 aaa 按钮,接口就正常响应了:



因为 axios 的拦截器里给它带上了 token:



那当 token 失效的时候,刷新 token 的逻辑在哪里做呢?


很明显,也可以放在 interceptor 里。


比如我们改下 localStorage 里的 access_token,手动让它失效。



这时候再点击 aaa 按钮,提示的就是 token 失效的错误了:



我们在 interceptor 里判断下,如果失效了就刷新 token:


axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
let { data, config } = error.response;

if (data.statusCode === 401 && !config.url.includes('/refresh')) {

const res = await refreshToken();

if(res.status === 200) {
return axiosInstance(config);
} else {
alert(data || '登录过期,请重新登录');
}
} else {
return error.response;
}
}
)

async function refreshToken() {
const res = await axiosInstance.get('/refresh', {
params: {
token: localStorage.getItem('refresh_token')
}
});
localStorage.setItem('access_token', res.data.accessToken);
localStorage.setItem('refresh_token', res.data.refreshToken);
return res;
}

响应的 interceptor 有两个参数,当返回 200 时,走第一个处理函数,直接返回 response。


当返回的不是 200 时,走第二个处理函数 ,判断下如果返回的是 401,就调用刷新 token 的接口。


这里还要排除下 /refresh 接口,也就是刷新失败不继续刷新。


刷新 token 成功,就重发之前的请求,否则,提示重新登录。


其他错误直接返回。


刷新 token 的接口里,我们拿到新的 access_token 和 refresh_token 后,更新本地的 token。


测试下:


我手动改了 access_token 让它失效后,点击 aaa 按钮,发现发了三个请求:



第一次访问 aaa 接口返回 401,自动调了 refresh 接口来刷新,之后又重新访问了 aaa 接口。


这样,基于 axios interceptor 的无感刷新 token 就完成了。


但现在还不完美,比如点击按钮的时候,我同时调用了 3 次 aaa 接口:



这时候三个接口用的 token 都失效了,会刷新几次呢?



是 3 次。


多刷新几次也没啥,不影响功能。


但做的再完美一点可以处理下:



加一个 refreshing 的标记,如果在刷新,那就返回一个 promise,并且把它的 resolve 方法还有 config 加到队列里。


当 refresh 成功之后,重新发送队列中的请求,并且把结果通过 resolve 返回。


interface PendingTask {
config: AxiosRequestConfig
resolve: Function
}
let refreshing = false;
const queue: PendingTask[] = [];

axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
let { data, config } = error.response;

if(refreshing) {
return new Promise((resolve) => {
queue.push({
config,
resolve
});
});
}

if (data.statusCode === 401 && !config.url.includes('/refresh')) {
refreshing = true;

const res = await refreshToken();

refreshing = false;

if(res.status === 200) {

queue.forEach(({config, resolve}) => {
resolve(axiosInstance(config))
})

return axiosInstance(config);
} else {
alert(data || '登录过期,请重新登录');
}
} else {
return error.response;
}
}
)

axiosInstance.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem('access_token');

if(accessToken) {
config.headers.authorization = 'Bearer ' + accessToken;
}
return config;
})

测试下:



现在就是并发请求只 refresh 一次了。


这样,我们就基于 axios 的 interceptor 实现了完美的双 token 无感刷新机制。


总结


登录状态的标识有 session 和 jwt 两种方案。


session 是通过 cookie 携带 sid,关联服务端的 session,用户信息保存在服务端。


jwt 是 token 保存用户信息,在 authorization 的 header 里通过 Bearer xxx 的方式携带,用户信息保存在客户端。


jwt 的方式因为天然支持分布式,用的比较多。


但是只有一个 token 会有过期后需要重新登录的问题,为了更好的体验,一般都是通过双 token 来做无感刷新。


也就是通过 access_token 标识用户身份,过期时通过 refresh_token 刷新,拿到新 token。


我们通过 nest 实现了这种双 token 机制,在 postman 里测试了一下。


在 react 项目里访问这些接口,也需要双 token 机制。我们通过 axios 的 interceptor 对它做了封装。


axios.request.interceptor 里,读取 localStorage 里的 access_token 放到 header 里。


axios.response.interceptor 里,判断返回的如果是 401 就调用刷新接口刷新 token,之后重发请求。


我们还支持了并发请求时,如果 token 过期,会把请求放到队列里,只刷新一次,刷新完批量重发请求。


这样,就是一个基于 Axios 的完美的双 token 无感刷新了。

作者:zxg_神说要有光
来源:juejin.cn/post/7271139265442021391

收起阅读 »

为啥count(*)会这么慢?

背景 本没想着写这篇文章的,因为我觉得这个东西大多数有经验的开发遇到过,肯定也了解过相关的原因,但最近我看到有几个关注的技术公众号在推送相关的文章。实在令我吃惊! 先上公众号文章的结论: count(*) :它会获取所有行的数据,不做任何处理,行数加1。 c...
继续阅读 »

背景


本没想着写这篇文章的,因为我觉得这个东西大多数有经验的开发遇到过,肯定也了解过相关的原因,但最近我看到有几个关注的技术公众号在推送相关的文章。实在令我吃惊!


先上公众号文章的结论:



  • count(*) :它会获取所有行的数据,不做任何处理,行数加1。

  • count(1):它会获取所有行的数据,每行固定值1,也是行数加1。

  • count(id):id代表主键,它需要从所有行的数据中解析出id字段,其中id肯定都不为NULL,行数加1。

  • count(普通索引列):它需要从所有行的数据中解析出普通索引列,然后判断是否为NULL,如果不是NULL,则行数+1。

  • count(未加索引列):它会全表扫描获取所有数据,解析中未加索引列,然后判断是否为NULL,如果不是NULL,则行数+1。


结论:count(*) ≈ count(1) > count(id) > count(普通索引列) > count(未加索引列)


我也不想卖关子了,以上结论纯属放屁。根本就是个人yy出来的东西,甚至不愿意去验证一下,哪怕看一眼执行计划,也得不出这么离谱的结论。


我不敢相信这是一篇被多个技术公众号转载的文章!


以下所有的内容均是基于,mysql 5.7 + InnoDB引擎, 进行的分析。


拓展:


MyISAM 如果没有查询条件,只是简单的统计表中数据总数,将会返回的超快,因为service层中获取到表信息中的总行数是准确的,而InnoDB只是一个估值。


实例


废话不多说,先看一个例子。


以下是一张表数据量有100w,表中字段相对较短,整体数据量不算大。


CREATE TABLE `hospital_statistics_data` (
`pk_id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`id` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '外键',
`hospital_code` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '医院编码',
`biz_type` tinyint NOT NULL COMMENT '1服务流程 2管理效果',
`item_code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核项目编码',
`item_name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核项目名称',
`item_value` varchar(36) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核结果',
`is_deleted` tinyint DEFAULT NULL COMMENT '是否删除 0否 1是',
`gmt_created` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT 'gmt_modified',
`gmt_deleted` datetime(3) DEFAULT '9999-12-31 23:59:59.000' COMMENT '删除时间',
PRIMARY KEY (`pk_id`)
) DEFAULT CHARSET=utf8mb4 COMMENT='医院统计数据';

此表初始状态只有一个聚簇索引


以下分不同索引情况,看一下COUNT(*)的执行计划。


1)在只有一个聚簇索引的情况下看一下执行计划。


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



关于执行计划的各个参数的含义,不在本文的讨论范围内,可自行了解。


这里只关注以下几个属性。



  1. type: 这里显示index,说明使用了索引。

  2. key:PRIMARY使用了主键索引。

  3. key_len: 索引长度8字节。


这里有很关键的一点:count(*)也会走索引,在当前情况下使用了聚簇索引。


好,再往下看。


2)存在一个非聚簇索引(二级索引)


给表添加一个hospital_code索引。


alter table hospital_statistics_data add index idx_hospital_code(hospital_code)

此时表中存在2个索引,主键 hospital_code


同样的,再执行一下:


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



同样的,看一下 type、key和key_len三个字段。


是不是觉得有点“神奇”。


为何索引变成刚添加的idx_hospital_code了。


先别急着想结论,再看下面一种情况。


3)存在两个非聚簇索引(二级索引)


在上面的基础上,再添加一个二级索引。


alter table hospital_statistics_data add index idx_biz_type(biz_type)

此时表中存在3个索引,主键 、hospital_code 和 biz_type。


同样的,执行一下:


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



是不是更困惑了,索引又..又又...变了.


变成新添加的idx_biz_type。


先不说为何会产生以上的变化,继续往下分析。


在以上3个索引的基础上,分别看一下,count(1)count(id)count(index)count(无索引)


这4种情况,与count(*)的执行计划有何区别。



  1. count(1)




  1. count(id)
    对于样例表来说是,主键是pk_id


image.png



  1. count(index)


这里选取biz_type索引字段。




  1. count(无索引)



小结:




  1. count(index) 会使用当前index指定的索引。




  2. count(无索引) 是全表扫描,未走索引。




  3. count(1) , count(*), count(id) 一样都会选择idx_biz_type索引




看到这,你还觉得那些千篇一律的公众号文章的结论正确吗?


必要知识点




  1. mysql 分为service层引擎层




  2. 所有的sql在执行前会经过service层的优化,优化分为很多类型,简单的来说可分为成本规则




  3. 执行计划所反映的是service层经过sql优化后,可能的执行过程。并非绝对(免得有些人说我只看执行计划过于片面)。绝大多数情况执行计划是可信的




  4. 索引类型分为聚簇索引非聚簇索引(二级索引)。其中数据都是挂在聚簇索引上的,非聚簇索引上只是记录的主键id。




  5. 抛开数据内存,只谈数据量,都是扯淡。什么500w就是极限,什么2个表以上的join都需要优化了,什么is null不会走索引等,纯纯的放屁。




  6. 相信一点,编写mysql代码的人比,看此文章的大部分人都要优秀。他们会尽可能在执行前,对我这样菜逼写的乱七八糟的sql进行优化。




原因分析


其实原因非常非常简单,上面也说了,service层会基于成本进行优化


并且,正常情况下,非聚簇索引所占有的内存要远远小于聚簇索引。所以问题来了,如果你是mysql的开发人员,你在执行count(*)查询的时候会使用那个索引?


我相信正常人都会使用非聚簇索引


那如果存在2个甚至多个非聚簇索引又该如何选择呢?


那肯定选择最短的,占用内存最小的一个呀,在回头看看上面的实例,还迷惑吗。


同样都是非聚簇索引。idx_hospital_codelen146字节;而idx_biz_typelen只有1。那还要选吗?


那为何count(*)走了索引,却还是很慢呢?


这里要明确一点,索引只是提升效率的一种方式,但不能完全的解决效率问题。count(*)有一个明显的缺陷,就是它要计算总数,那就意味着要遍历所有符合条件的数据,相当于一个计数器,在数据量足够大的情况下,即使使用非聚簇索引也无法优化太多。


官方文档:



InnoDBhandlesSELECT COUNT(*)andSELECT COUNT(1)operations in the same way. There is no performance difference.



简单的来说就是,InnoDB下 count(*) 等价于 count(1)


既然会自动走索引,那么上面那个所谓的速度排序还觉得对吗? count(*)的性能跟数据量有很大的关系,此外最好有一个字段长度较短的二级索引。


拓展:


另外,多说一下,关于网上说的那些索引失效的情况,大多都是片面的,我这里只说一点。量变才能引起质变,索引的失效取决于你圈定数据的范围,若你圈定的数据量占整体数据量的比例过高,则会放弃使用索引,反之则会优先使用索引。但是此规则并不是完美的,有时候可能与你预期的不同,也可以通过一些技巧强制使用索引,但这种方式少用。


举个栗子:


通过上面这个表hospital_statistics_data,我进行了如下查询:


select * from hospital_statistics_data where hospital_code is not null;

此时这个sql会使用到hospital_code的索引吗?


这里也不卖关子了,若hospital_code只有很少一部分数据是null值,那么将不会走索引,反之则走索引。


原因就2个字:回表


好比去买砂糖橘,如果你只买几斤,那么你随便挑筐里面好的就行。但是如果你要买一筐,我相信老板不会让你在里面一个个挑,而是一次给你一整筐,当然大家都不傻,都知道筐里里面肯定有那么几个坏果子。但是这样效率最高,而且对老板来说损失更小。


执行过程


摘抄自《从根上理解mysql》。我强烈推荐没有系统学过mysql的,看看这本书。


1.首先在server层维护一个count变量


2.server层向InnoDB引擎要第一条记录


3.InnoDB找到第一条二级索引记录,并返回给server层(注意:由于此时只是统计记录数量,所以并不需要回表)


4.由于COUNT函数的参数是*,MySQL会将*当作常数0处理。由于0并不是NULL,server层给count变量加1。


5.server层向InnoDB要下一条记录。


6.InnoDB通过二级索引记录的next_record属性找到下一条二级索引记录,并返回给server层。


7.server层继续给count变量加1。


8.重复上述过程,直到InnoDB向server层返回没记录可查的消息。


9.server层将最终的count变量的值发送到客户端。


总结


写完后还是心中挺郁闷的,现在能从公众号获取到的好文章越来越少了,现在已经是知识付费的时代了。


挺怀念刚工作的时候,那时候每天上午都花点时间看看公众号文章,现在全都是广告。哎!


不过也正常,谁也不能一直为爱发电。


学习还是建议多看看书籍,一般能成书的都不会太差。现在晚上能搜到的都

作者:微笑兔
来源:juejin.cn/post/7182491131651817531
是千篇一律的文章,对错不知。网上

收起阅读 »

如何治愈拖延症

如何治愈拖延症 背景 最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。 ...
继续阅读 »

如何治愈拖延症


背景


最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。


我的抖音打卡


就拿我昨天晚上来说吧,吃完饭已经是8点了,这个点没啥问题。和家里通了半小时的电话之后,发现手机没电了,于是又在充电。等到九点的时候,电池的电量还在30%左右,我知道我的手机电池不大行,不足以支撑一个小时,于是就放弃了😅。


但是当早上我坐在电脑前的时候,发现昨天的好多事情都没有完成,今天的事情又得往后推了。越堆积越是多,都喘不过气来了🤥。



哈哈🤭🤭,也不好意思让大家看到下周的推文内容啦,算是提前剧透了😎



我的todo list


我就不断的在思考,为什么我的执行力不行了。我觉得我的代言词就是:一个有思想有行动力的程序员。现在看来,我是一个懒惰、带有严重的拖延症的程序员了。不行,这个问题得治,不然我会更加的焦虑,堆积更多的任务导致更低的效率。


分析


结合这个低效率的周末,我反思了我为什么效率这么低。


🕢推迟开始


我发现我总喜欢做todo list,但是很少去看,也很少去核对一下我当前的进度。总觉得一天的时间很长,我可以先去做别的事情,比如碎片化的短视频、吃吃吃、发呆。于是一件件的本在计划中的事情被不断的推迟了。


⏲时间管理困难


从我8:00起来到晚上的凌晨入睡,减去我个人清洁、做饭、午睡,我剩下的时间大约是10个小时。但是,我一对比下来,我的时间利用率仅仅是40%,相当于我只有4个小时是在满满当当的学习的。我之前的ipad在的时候,我会用潮汐这个软件把我的时间分割成一个小时一个小时的。现在没了,我发现我的时间规划真的出了大问题。


🤖自我控制力下降


我觉得最近一年的时间,我真的太放松自我了。我的技术成长、学习上长进也是微乎其微。我总结下来就是因为我的自控力太差了,或者说没有承受着外界的干扰。因为一个短视频就可以刷上一个小时的短视频,因为一个好物就会不断的逛购物软件......碎片化的时间消耗,最终导致了效率低下。


解决方案


针对以上我总结的问题,我决定对症下药。


🧾明确的计划


我觉得我明确的计划真的很必要。就像我公众号shigen里面给自己定的一个目标一样:



2023年的8月开始,我先给自己定一个小目标:公众号文章不停更



《终于,我官宣了》文章


“不停更”的意思是我每天都要更新文章。我的推文里还带了“新闻早知道”栏目,我哪天没更新或者说更新晚了,我就觉得目标没有实现了,新闻也没什么意义了。我觉得日常的计划和这个目标的设定和实现有着相似的地方,我要把我的计划和目标更明确一点。🤔🤔比方说我今天要干嘛,我完成了怎么样了。


优先级


事情分清楚轻重缓急,我记得我在实习的时候,就有一次因为项目要上线和我一点不大紧要的事情次序搞混了,导致晚上加班上线。现在的我也是,很多重要的事情也是放到了最后做甚至只延期了。所以,我的行动之前,得先做最要紧的事情。但是也会混杂一些个人的情绪在里边,比方说明明一件事情很重要,但是自己就是不想做或者说觉得事情很简单,我先做最有意思的事情。很多时候都是这样的,兴趣和意义占据了主导因素,优先级反而不是那么重要了。


抗拒干扰


手机就在我的边上,这很难不因为一个消息或者一个发愣就去拿起手机,一旦拿起来就放不下了。所以,我觉得最好就是把它放在我的抽屉里,然后眼不见就不去想它了。


奖励惩罚机制


最后,我觉得奖罚分明也挺重要的。在这里,我也想起了我在一线的时候,我周末总会有一天去我住的地方隔壁去逛超市,每次的消费金额大约在100-150左右。但是我出去的前提是我的学习目标完成了或者代码写完了。我现在却相反,目标缺少了一个验收和奖惩的过程。我觉得和我更喜欢宅有一点关系了,所以,我也得奖励我自己一下:目标完成了可以去逛超市消费🛒,也可以去骑行🚲;但是没完成,健腹轮😭😭安排上!


好了,以上就是我对于最近的拖延症的分析和解决方式的思考了。也欢迎伙伴们在评论区交流一下自己对于拖延症的看法。


shigen一起

作者:shigen01
来源:juejin.cn/post/7272690326401728547
,每天不一样!

收起阅读 »

谈谈这十年的代码生涯👨‍💻

博客终于完成了,借着这次机会,我想好好回顾总结一下这十年的代码生涯。有人曾说过如果一个人专注做一件事做十年,那么他会成为这个行业的大师。先别在意其出处与真假,遗憾的是这十年我并没成为专家或大师,甚至,于这个行业而言我才算刚刚入门。 三句话总结这十年 空白:不...
继续阅读 »

博客终于完成了,借着这次机会,我想好好回顾总结一下这十年的代码生涯。有人曾说过如果一个人专注做一件事做十年,那么他会成为这个行业的大师。先别在意其出处与真假,遗憾的是这十年我并没成为专家或大师,甚至,于这个行业而言我才算刚刚入门。


三句话总结这十年



空白:不知道想要什么、想干什么、喜欢什么



image.png



活着的意义便是寻找活着的意义:挣扎、困顿、精神内耗、努力寻找出口



image.png



在我离开之前想要多认识这个世界一些:算是找到自己的信仰、人生的方向,不以物喜、不以己悲也不再随波逐流、随风飘扬



image.png


我想大多数人都会跟我一样经历这几个阶段,对此我想分享一些自己的经验看法:



  • 正在经历第一个阶段的小伙伴们不要焦虑,认识自己本就是人的一生中最复杂的事情,也不可能一天两天一年两年就能完成,因此再急、再焦虑都只是徒增烦恼而已,倒不如先做好眼前的事,而后慢慢的一步一步的认真的寻找和认识自己。

  • 正在经历第二个阶段的小伙伴们不要担心、恐惧,其实我比较赞同余华老师的观点,有时候精神内耗其实是一个好事情,因为你不安于现状,不满足于此,你在寻找出口也在认识你自己。这个过程毫无疑问是极度痛苦的,但你要相信不认识黑暗是无法知晓光明的,没有经历困顿挣扎也无法看清前方的路。

  • 正在经历第三个阶段的小伙伴们,首先恭喜你们找到了能为之奋斗终生的事业!但切勿忘了这个世界上唯一不变的就是变化,我们仍要面临许多挑战、选择与诱惑,坚持还是另寻他路?这是个问题!


由于从小就深受电影影响,看着那些"黑客"只需要动动键盘敲下几行神秘的英文就可以破解万事万物,当时觉得他们简直无所不能,他们的技能简直比魔法师还炫酷,由此编程的种子就种在了我的心里。


在读高一时机缘巧合下我参加了学校的技能大赛,便满心憧憬的开始了编程的修炼之路。当时主要还是用C#和winform框架写windows软件,我仍记得我写下的第一个软件——“识别十进制的位数”,我觉得计算机真是世界上最奇妙的物品了!会编程真的是太酷了!


image.png


第一个软件:"识别十进制的位数"


至此便一发不可收拾,也写很多很多有趣的玩意,界面也逐渐美观起来。





一个计数器





仿QQ界面





1024游戏


误入硬件


原本我是打算在大学读计算专业的,可是命运总是喜欢跟我们开玩笑,最后阴差阳错读了电子专业。不过还好,电子专业也没有跳脱编程的范畴,只不过面对的对象不一样了而已。不过由于自己一开始对软件开发的依赖导致我整个大学生涯对电路设计有着天生的反感,这也导致了我目前也没玩明白电路😅。不过也正因我的软件开发基础致使我许多编程课程都学得非常轻松😁


记得第一次接触硬件编程是STM8,当时作业是利用仿真软件基于STM8写个计数器,由于之前有软件的编程基础,在了解硬件一些逻辑后很快就上手了,于是乎很快我的第一个硬件作品也诞生了——"基于STM8的计数器"





基于STM8的计数器


在完成第一个硬件作品后,简直感觉成就感拉满!当时我觉得做硬件比做软件还要有趣,因为硬件是实实在在看得见摸得着的,当它在跟着你预期的逻辑一步一步动起来的时候,荷尔蒙会飙升直击你的大脑。


有了第一次做硬件的快感,自己便上瘾了起来,开始参加各式各样的比赛。记得第一个参加的是校内的硬件比赛,当时做的是一个无线充电循迹小车,这对于当时的我而言真的是个超级工程,整个项目有着6个传感器,2个电机,若干个led灯需要控制,这对新手而言并不友好😭,不过好在熬了几个通宵还是把它完成了!





无线充电循迹小车


之后各种作业、比赛自己又陆陆续续做了许许多多的作品





RGB蓝牙灯





局域网点歌器





一个超酷的软件


除此之外,我自学了大概半年的深度学习,不过就学了有监督部分,自己做了一些非常有趣的东西!





手写字体识别





银行卡号识别


大家可以从我的作品中看到我是一个彻头彻尾的垃圾佬,由于经济窘迫,所有作品都是泡沫板、雪糕棍、热胶等等拼接而成(我要是有钱绝对搞一个3D打印机😶‍🌫️)。不过有三件作品可以说是我这个垃圾佬的得意之作,它是真的有用真的帅呀!


第一件:恒温箱


这是我的一个课程作业,当时老师要求围绕PID算法做一个作品。考虑到倒立摆已经被玩烂,因此思来想去做个冰箱吧,主要是正好寝室缺个冰箱,除了缺个冰箱以外,还缺个加热箱(冬天室友带饭会冷的)。好的,那就做个恒温箱吧!





紧锣密鼓调试中....


熬了几周终于弄好了,它是真的帅呀!不但可以实现最低-10°C的制冷还可以实现最高60°C的保温,重点是它不是PWA的粗稳,而是PID的精稳呀!!!!





成品


第二件:基于STM32的游戏机


这件作品也是我的一个课程作业,当时一直都想给自己的作品上摇杆(总觉得摇杆真的很帅),但一直没有机会,正好这次可以用一用。之所以觉得这件作品很酷,一是它很简约(东西不多)一块芯片、一个喇叭、一个三极管、二个摇杆,二是它是唯一件自己从腐蚀板子到完成全由自己动手的一件作品(以前板子都是在嘉立创画好,最后直接就收到成品板子了),因此觉得它很格外的珍贵!


image.png


image.png


第三件:消防喷水枪


这件作品就是比赛作品了,完完全全由垃圾拼接的,哈哈哈哈哈!可以看到转向用的是回收的摄像头拆下来的外壳,底座是月饼盒子,管子是割的亚克力水管,哈哈哈哈,整个作品最贵的就是红外温度传感器了,当时买着是几十块还是百来块?不记得了,总之精度很差很差,导致远距离的火苗识别不到,气死了🤯!当时也有考虑摄像头方案,太贵了买不起呀😭!!!


image.png


微信图片_20230823150823.png


入坑Web


在大三的时候,为了凑学分,我选了一门学分很高的课——《网页设计与实现》,当时的我又如何能想到这会是我以后为生的技术呢!那时候虽然每天都在使用网页,但对于网页的实现是一窍不通,也更看不懂网页中那些恼人的代码,虽然看起来跟我在WPF中使用过XML非常相似,但由于没有深究,则以为它们只是长得像而已,除此之外并没有什么瓜葛。


在此我先要感谢一下这门课的老师,他实在是教会了我们太多东西,这门课也是我整个大学生涯中收获最大最多的一门课(没有之一),相较于他而言,其他课程老师则显得格外不称职。


记得当时我们还是使用的Dreamware写代码,当时第一个网页写的是table布局显示LPL排名(游戏中二少年,哈哈哈),那时候还不懂css并且也不知道什么HTML标签,就学了个< table>





第一个网页


学了CSS之后开始花哨起来,右边的奖杯是旋转的,整个底图是个视频,当时效果真的贼震撼贼帅(依旧是中二的LOL)





学了CSS之后的网页


在掌握了JS之后,突然就感觉打开了任督二脉,最终做了一个网页音乐播放器,背景地图也是视频,并且会跟着歌曲变化,效果究极炸裂!





学了JS之后的网页


大四创业


大三下之后我们就没什么课了,然后在导师的帮助下去了他朋友的公司实习。当时的工作内容主要是后端,这段时间自己也学了非常非常多的东西,主要就是一些后端的框架/中间件之类的,包括SpringMCVC、Springboot、kafak、redis等等,当时自己也乐在其中(主要是有钱还能学东西)。我以为我会慢慢度过实习期,然后大四找工作成为一名后端工程师,但是命运总是喜欢跟我们开玩笑的。突然有一天我的一个学长跟我说需要一套针对他们公司的管理系统,就这样我约上了三个小伙伴走向了创业的路。


完成第一版


由于团队的小伙伴没人愿意写前端(当时普遍对前端有偏见,觉得写页面没有什么技术含量)于是不得已我便承担了前端开发的工作,由于这次合作是我主导的,因此我也承担了需求分析、数据库/UI设计等工作,其余的小伙伴分别负责后端/小程序/IOS开发。


大约在进行了2周的需求分析后,我们开始了开发工作,但很快我们遇到了第一个问题——“我们做的并不是他们想要的”,并且他们常常天马行空,一天一个想法,这导致我们不得不停下开发工作思考下一步如何做。最终在查阅资料和讨论后,我们决定先仔细了解分析需求!通过深入了解他们公司员工的工作,了解整个公司的业务流程,从而知晓他们的痛点,并且同时构造出整个公司的业务流程图以及每员工/用户在其中的位置,也就是用例图。


最终我们花了大概一个多月的时间完成了用例图,然后花了大概三个多月的时间,按照他们的需求编写出了第一套管理系统。该系统包含一些常见的公司事务,例如:请假、打卡、薪资计算也有针对他们公司本身业务的工单系统等。





管理系统V1.0


发现问题


但很快问题便来了,由于公司架构/人员/流程/功能字段时常变更,但我们又是将流程/人员在代码中写死的,这导致他们每发生一次变更我们就需要改一次代码,这让我们非常头疼,于是我们寻求解决方案。


我们通过查找资料、看书、看社区很快找到了解决方案——"sass"。自20世纪90年代以来,以互联网为核心的现代信息技术在世界范围内迅猛发展,基于互联网为载体的信息化软件服务的在线租用模式SaaS(Software as a Service软件即服务)日渐成熟,为中小企业开展信息化建设提供了更合理更高效的发展平台。当然对于软件开发商而言从卖“代码”转变到卖“服务”的难度是可想而知的,他们需要对业务进行高度的抽象,从中找出它们共性与差别,以此用同一套代码来满足不同企业的输入、处理、输出数据三个环节。





程序模型


因此当前SaaS类系统的抽象也是从这三个环节入手的,数据输入部分为动态表单引擎、数据处理部分为流程引擎与计算引擎、数据输出部分为报表引擎。表单引擎作用是由用户拖动一些组件构成所需收集的信息,有点类似目前许多的在线问卷调查网站;流程引擎决定了用户填写数据的处理对象和流程走向;计算引擎计算和处理填写的数据;报表引擎则是自定义展示数据内容。





SaaS软件主要组成部分


如今我依旧认为“sass”是当下中小型企业降本增效最好的且最优的途径,sass的发展远不为此。找到解决方案后我们深知这次改动将是巨大的,并且这个项目也会变得非常困难的,因此我们决定把一切推倒重来并把之前的遇到的一系列问题全部解决。


推到重来


我们决定使用码云管理代码,各个端的代码分别存储,且加上一个文档仓库。以解决我们消息闭塞无法总览、难以合并、查阅代码以及分配、总览任务问题。


image.png


仓库架构


文档仓库中存放需求分析文档、用例图、架构图、数据库设计、接口设计等内容,方便大家查看修改


image.png


文档仓库存放内容


我们再次对需求进行了更加仔细的分析,结合以此进一步完善和改进了系统用例图。


image.png


系统用例图


由于之前开发中经常遇见忘记数据库依赖关系,无法总览全局的问题,因此我们决定根据用例图先画出数据库设计图,设计图让我们能够更加直观的看到各个模块的依赖关系,并且每次修改我们只需要共同基于设计图改动即可,后续可根据设计图生成表结构。


image.png


数据库设计


根据用例图与数据库设计,我们设计出了系统整体架构


image.png


系统架构


在之前的开发过程中,由于我们没有接口文档,这导致会有许多耗费时间且多余的沟通步骤,并且一些code码、状态消息等内容并未得到统一,因此我们也规定了相应接口文档的格式、参数等。


image.png


接口文档


我们开始规定、分配任务,并将其统一放入码云管理,规定其任务周期以及里程碑,对项目整体时间进行监管把控。


image.png


任务规划、分配、监管


完成第二版


在完成了这一系列的前期工作后,我们便开始着手开发了,大约用了半年多的时间,我们成功的完成了大部分的内容。正如我们起初设想的那样,整套系统分为三个端:管理端(web)、员工端(App)、用户端(小程序)。


image.png


系统总览


管理端主要设置整套系统的应用/查看应用提交的数据内容,应用分为固有应用与自建应用。固有应用指的是无法使用动态表单生成的应用,需要直接通过编写代码;自建应用则指的是用户可以自行通过动态表单/流程引擎/视图引擎创建的应用。


image.png


管理端设置页面


自建应用通过表单引擎拖拽组件生成业务需要填写的信息、通过流程引擎决定该业务的流程,并规定该业务的权限,面向的使用对象等,视图引擎则决定了该表单需要在首页中展示/统计的数据。创建应用完成后,该功能则会同步出现在有权限的用户的小程序中以供填写数据申请业务。


image.png


创建自建应用


image.png


设置应用表单


image.png


设置应用的流程





用户端显示应用





用户端提交申请


image.png


根据设置的流程流向相应人员处理业务


image.png


总览该应用数据


为了满足自建应用无法完成的需求,我们也写了许许多多的固有应用,包括了员工打卡、仓库管理等等,但遗憾的是我并没有记录下相关的内容图片。此外我们准备着手重构动态表单部分代码,然后将其开源!


image.png


准备开源的动态表单


在我们完成这版系统后,恰逢毕业论文选题,于是乎我选择了自主命题,并打算以这段工作来完成自己的毕业论文!


image.png


毕业论文


发布


在我们完成第二版没多久,我们便开始对外开始宣传这套系统,为此我们做了许许多多的工作,由于资金非常紧张,我们负责了许多设计工作,包含设计了一些海报、易拉宝、宣传册等等。


image.png


自己设计的相关海报





自己设计的宣传册


临近毕业,我们宣告失败


在临近毕业前几天,很遗憾最终我们还是失败了,失败的原因有很多,但直接原因是我们遇到了无法解决的技术难题。该系统一开始其实就是一个问卷调查系统然后加入一个流程引擎,这造成了一个问题,动态表单与动态表单之前无法产生数据关联,导致形成了数据孤岛。为此我们在动态表单中加入数据关联组件,也解决了一对一、一对多的关联,但多对多一直无法解决,大概挣扎了两个多月,但还是没有寻得解决方案。


虽说直接原因是技术难题,但我知道对于一个团队而言是永远不会被技术难倒的。其实主要原因还是我作为团队负责人,在团队遇到挫折与困难时没有积极调和团队氛围,不但没有积极鼓舞团队成员,反而还因为困难整天闷闷不乐,导致团队氛围跌至谷底,最终解散。其次我并没意识到软件是迭代出来的,并不是一面世就是完美,但我每次遇到问题时都急于解决不去划分轻重缓急,常常推到重来,这虽然使得软件更加完美,但也使得软件开发周期不断延期,也不断重复多项工作,打击成员的积极性。


当然团队成员也或多或少有问题,但归根到底还是我的问题,我并没有挑选更为合适的人选加入团队,这导致耽误了团队,也耽误了他们,我直到现在仍觉得愧对于他们!


虽然说项目最后失败了,但我并不后悔,直到如今我也常常怀恋我们把酒论码、午后敲码的日子,收获很多也很快乐!


image.png


image.png


image.png


回首十年


记得临近毕业时我的导师曾对我说“你太浮躁了,希望你离开学校之后好好改一改”。起初我也认为自己浮躁,常常东搞一下,西搞一下,并且急于求成,但如今我却有了不同的看法,是浮躁但不完全浮躁,我觉得我本质上是在寻找。由于刚进入大学,各种技术眼花缭乱,而且我并不知道对于这个专业而言,我应该打好什么样的基础,这些基础对应这哪些方向,以及最重要的,我应该如何才能学好这个专业。虽然我整个大学都非常努力,但遗憾的是直到我大学毕业都没有搞清楚这些基本的问题,一直在技术表面跳来跳去。由于缺乏清晰的认识以及能告知你这些的人,因此我唯有的办法则是不断地试,我只有不断的去尝试才能知道上述问题的答案,我也只有不断的尝试才能知道接下来要走的路。



最近又读了一遍《月亮与六便士》,产生了很多新的感悟,我想作为这篇文章的结尾再好不过了。



我们就如同被船掀起的浪花,被前浪牵着走,被后浪推着走。大多数浪花都会随波逐流,有一些浪花在这个过程中趁着风势逐渐变大变高,惹得其他浪花羡慕追随,给浪营造了强大的假象。也有极少数浪花有了此生必要到达的目的,开始挣脱前浪的牵引,摆脱后浪的束缚,这也打得前后浪措手不及,乱了阵脚,伤痛欲绝。


此时的我们想必早已争相着各抒己见,吵得不可开交。更有甚者大肆宣扬诸如“满地的六便士他却看到了月亮”等等片面观点想要其定为“真理”,殊不知我的叔叔亨利早已说过“魔鬼总是随心所欲地引用经文。他记得从前一个先令就能买到十三只上等的牡蛎。”


最后提一句,大学教育改革并不应该一味给大学生增压,因为我认为其本质并不是大学生懒散不愿意学,而是即使是努力学了也不会有太多收获。

作者:汪啊汪QAQ
来源:juejin.cn/post/7270464435297501196

收起阅读 »

求求别再叫我切图仔了,我是前端开发!

web
☀️ 前言 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。 群友2: 没有耕坏的田,只有累死...
继续阅读 »

☀️ 前言





  • 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。



    • 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。

    • 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。

    • 群友2: 没有耕坏的田,只有累死的牛啊,老哥!🐮。

    • 群友3: 用CodeFun啊,分分钟解决你这种外包需求。

    • 群友2: 对喔!可以试一下CodeFun,省下来的时间开黑去。




  • 在我印象中智能生成页面代码的工具一般都不这么智能,我抱着怀疑的心态去调研了一下CodeFun看看是不是群友们说的这么神奇,试用了过后发现确实挺强大的,所以这次借此机会分享给大家。




🤔 什么是 CodeFun



  • 大部分公司中我们前端现在的开发工作流大概是下面这几步。

    • 一般会有UI先根据产品提供的原型图产出设计稿。

    • 前端根据设计稿上的标注(大小,边距等)进行编写代码来开发。

    • 开发完后需要给UI走查来确认是不是他/她想要的效果。

    • 如果发现有问题之后又继续重复上面的工作->修改样式->走查。






  • 我们做前端的都知道,重复的东西都可以封装成组件来复用,而上面这种重复的劳作是我们最不想去做的。

  • 但是因为设计图的精细可能有时候会有1px的差异就会让产品UI打回重新编写代码的情况,久而久之就严重影响了开发效率。

  • 我时常会有这么一种疑惑,明明设计稿上都有样式了,为什么还要我重新手写一遍呢?那么有没有一种可能我们可以直接通过设计稿就自动生成代码呢?

  • 有的!通过我的调研过后发现,发现确实CodeFun在同类产品中更好的解决了我遇到的问题。




  • CodeFun是一款 UI 设计稿智能生成源代码的工具,可以将 SketchPhotoshopFigma 的设计稿智能转换为前端源代码。

  • 8 小时工作量,10 分钟完成是它的slogan,它可以精准还原设计稿,不再需要反复 UI 走查,我觉得在使用CodeFun后可以极大地程度减少工作流的复杂度,让我们的工作流变成以下这样:

    • UI设计稿产出。

    • CodeFun产出代码,前端开发略微修改交付。





🖥 CodeFun 如何使用



  • 接下来我就演示一下如何快速的根据设计稿来产出前端代码,首先我们拿到一个设计稿,这里我就在网上搜了一套Figma的设计稿来演示。

  • 我们在Figma中安装了一个CodeFun的插件,选择对应CodeFun的项目后点击上传可以看到很轻松的就传到我们的CodeFun项目中,当然除了FigamaCodeFun还支持Sketch,PSD,即时设计等设计稿。

  • 我们随便进入一个页面,引入眼帘的是中间设计稿,而在左侧的列表相当于这个页面的节点,而我们点击一下右上角的生成代码可以看到它通过自己的算法很智能的生成了代码。

  • 我上面选择生成的是React的代码,当然啦,他还有很多种选择微信小程序Vueuni-app等等等等,简直就是多端项目的福音!不止是框架,连Css预处理器都可以选择适合自己的。

  • 将生成的代码复制到编辑器中运行,可以看到对于简单的页面完全不用动脑子,直接就渲染出来我们想要的效果了,如果是很复杂的页面进行一些微调即可,是不是很方便嘿嘿。

  • CodeFun不管是根据你选择的模块进行生成代码还是整页生成代码用户进行复制使用之外,它还提供了代码包下载功能,在下载界面可以选择不同页面,不同框架,不同Css预处理器,不同像素单位

  • 如果是React相关甚至还会帮你把脚手架搭建好,直接下载安装依赖使用即可,有点牛呀。



🔥 CodeFun 好在哪



  • 笔者在这之前觉得想象中的AI生成前端代码的功能一直都挺简陋,用起来不会到达我的预期,到底能不能解决我的痛点,其实我是有以下固有思想的:

    • 生成代码就是很简单的帮你把HtmlCss写完嘛但是我们不同框架又不能生成。

    • 生成代码的变量名肯定不好看。

    • 生成的代码肯定固定了宽高,不同的手机端看的效果会差很多。

    • 平时习惯了v-for,wx:for,map遍历列表,这种生成代码肯定全部给你平铺出来吧。



  • 但是当我使用过CodeFun之后发现确实他可以解决我们很多的重复编写前端页面代码的场景,而且也打消了我对这类AI生成前端页面代码功能的一些固有思想,就如它的slogan所说:8 小时工作量,10 分钟完成


多平台、多框架支持



  • 支持 Vue 等主流 Web 开发框架代码输出。

  • 支持微信小程序代码输出,当你选择小程序代码输出时,像素单位会新增一个rpx的选项供大家选择。

  • 使用最简单的复制代码功能,我们可以快速的将我们想要的样式复制到我们的项目中进行使用 。

  • 笔者在使用的过程中一直很好奇下载代码的功能,如果我选择了React难不成还会给我自动生成脚手架?结果一试,还真给我生成了脚手架,只需要安装依赖即可,可以说是很贴心了~。



循环列表自动输出



  • 我们平时在写一个列表组件的时候都喜欢使用v-for,wx:for,map等遍历输出列表,而CodeFun也做到了这种代码的生成。

  • CodeFun在导入设计稿的时候会自动识别哪些是list组件,当然你也可以手动标记组件为List

  • 然后再开启“将 List 标签输出为循环列表”选项即可自动根据当前选择的框架生成对应的循环遍历语法,确实是很智能了~



批量数据绑定




  • 在我们平时Coding的过程中都不会把数据写死,而是用变量来代替进行动态渲染,而CodeFun支持批量数据绑定功能,我们可以把任何在页面中看到的元素进行数据绑定和命名修改




  • 就拿上面的循环列表举例吧,在我们一开始识别的Html中,遍历循环了一个typeCards数组,每一个都展示对应的信息,我们可以看到这里一开始是写死的,而我们平时写的时候会将它用变量替代。




  • 我们只需要点击右上角的数据绑定进行可视化修改即可,我们可以看到它的全部写法都改成了变量动态渲染,这就很符合我们平时编码的套路了。





一键预览功能



  • 有很多同学反馈在之前做小程序的情况下需要将代码编写完整并跑起来的情况下,使用微信的预览功能才可以看到效果,会比较繁琐

  • CodeFun支持直接预览,当我们导入设计稿后,选择右上角的预览功能可以直接生成小程序二维码扫码即可进行预览,好赞!。



更加舒适的“生成代码”



  • CodeFun生成的代码中是会让人看起来比较舒适的。

    • 变量名可读性会比较强。

    • 布局一般不会固定死宽高,而是使用padding等属性来自适应屏幕百分比

    • 自动处理设计稿中的无用图层、不可见元素、错误的编组乃至不合理的文字排列。

    • 全智能切图,自动分离背景图层、图标元素。




✍🏻 一些思考与建议



  • 前端开发不仅仅是一个切图的工具人,如果你一直局限于视图的表现的时候,你的前端水平也就是curd工程师的水平了,我们前端更多的要深入一些性能优化前端插件封装等等有意思的事情🙋🏻。

  • 总之如果你想你的前端水平要更加精进的情况下,可以减少一些在页面上的投入时间,现在的工具越来越成熟,而这些切图工作完全可以交给现有的工具去帮助你完成

  • 在使用体验上来说,CodeFun确实可以解决大部分切图功能,减少大家进行切图的工作时间,大家可以去试一下~但是肯定会有一些小细节不符合自己的想法,表示理解吧,毕竟AI智能生成代码能做成CodeFun这种水平已经很厉害了👍🏻。

  • 在使用建议上来说,我建议大家可以把CodeFun当成一个助手,而不要完全依赖,过度依赖,去找到更合适自己使用CodeFun的使用方法可以大量减少开发时间从而去做👉🏻更有意义的事情。

  • 很多人会很排斥,觉得没自己写的好,但是时代已经变啦~我还是那句话,所有东西都是一个辅助,一个工具,它提供了这些优质的功能而使用的好不好是看使用者本身的,欢迎大家去使用一下CodeFun~支持国产!!




  • 记住我们是前端开发,不是切图仔!做前端,不搬砖!



作者:快跑啊小卢_
来源:juejin.cn/post/7145977342861508638

收起阅读 »

为了弄清楚几个现象,重新学习了 flex

web
flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点: 相对于常规布局(float, position),它具备更高的灵活性; 相对于 grid 布局,它具有更强的兼容性; 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈) ...
继续阅读 »

flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点:



  • 相对于常规布局(float, position),它具备更高的灵活性;

  • 相对于 grid 布局,它具有更强的兼容性;

  • 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈)


但是在开发使用 flex 布局的过程中,也会遇到一些自己难以解释的现象;通俗表述:为什么会是这样效果,跟自己想象的不一样啊?


那么针对自己提出的为什么,自己有去研究过?为什么是这样的效果?如何解决呢?


自己也存在同样的问题。所以最近有时间,重新学习了一遍 flex,发现自己对 flex 的某些属性了解少之又少,也就导致针对一些现象确实说不清楚。


下面我就针对自己遇到的几种疑惑现象进行学习,来对 flex 部分属性深入理解。



每天多问几个为什么,总有想象不到的意外收获 ---我的学习座右铭



回顾 flex 模型


flex 的基本知识和基本熟悉就不介绍了,只需要简单的回顾一下 flex 模型。


在使用 flex 布局的时候,脑海中就要呈现出清晰的 flex 模型,利于正确的使用 flex 属性,进行开发。


flex1.png

理解如下的几个概念:



  • 主轴(main axis)

  • 交叉轴(cross axis)

  • flex 容器(flex container)

  • flex 项(flex item)



main size 也可以简单理解下,后面内容 flex-basis 会涉及到。



还顺便理解一下 flex-item 的基本特点



  1. flex item 的布局将由 flex container 属性的设置来进行控制的。

  2. flex item 不在严格区分块级元素和行内级元素。

  3. flex item 默认情况下是包裹内容的,但是可以设置的高度和宽度。


现象一:flex-wrap 换行引起的间距


关键代码:


<!-- css -->
<style>
 .father {
   width: 400px;
   height: 400px;
   background-color: #ddd;
   display: flex;
   flex-wrap: wrap;
}
 .son {
   width: 120px;
   height: 120px;
}
</style>

<!-- html -->
<body>
 <div class="father">
   <div class="son" style="background-color: aqua">1</div>
   <div class="son" style="background-color: blueviolet">2</div>
   <div class="son" style="background-color: burlywood">3</div>
   <div class="son" style="background-color: chartreuse">4</div>
 </div>
</body>

具体现象:


flex2.png

疑惑:为什么使用 flex-wrap 换行后,不是依次排列,而是排列之间存在间距?



一般来说,父元素的高度不会固定的,而是由内容撑开的。但是我们也不能排除父元素的高度固定这种情况。



排查问题:针对多行,并且在交叉轴上,不能想到是 align-content 属性的影响。但是又由于代码中根本都没有设置该属性,那么问题肯定出现在该属性的默认值身上。


那么通过 MDN 查询:


flex3.png

align-content 的默认值为 normal,其解释是按照默认位置填充。这里默认位置填充到底代表什么呢,MDN 上没有明确说明。


但是在 MDN 上查看 align-items 时,却发现了有用的信息(align-items 是单行,align-content 是多行),normal 在不同布局中有不同的表现形式。


flex4.png

可以发现,针对弹性盒子,normal 与 stretch 的表现形式一样。


自己又去测试 align-content,果然发现 normal 和 stretch 的表现形式一样。那么看看 stretch 属性的解释:


flex6.png

那么只需简单的需改,去掉 height 属性,那么 height 属性默认值就为 auto。


<!-- css -->
<style>
 .son {
   width: 120px;
   /* 注释掉 height */
   /* height: 120px */
}
</style>

看效果:


flex5.png

可以发现,子元素被拉伸了,这是子元素在默认情况下应该占据的空间大小。



这里就需要理解 flex item 的特点之一:flex item 的布局将由 flex container 属性的设置来进行控制的



那么当子元素设置高度时,是子元素自己把自己的高度限制了,但是并没有改变 flex container 对 flex item 布局占据的空间大小,所以就会多出一点空间,也就是所谓的间距。


所以针对上面这个案例,换行存在间隔的现象也就理解了,因为第四个元素本身就排布在弹性盒子的正确位置,只是我们把子元素高度固定了,造成的现象是有存在间隔。



可以想一下,如果子元素的高度加起来大于父元素的高度,又是什么效果呢?可以自己尝试一下,看自己能够解释不?



现象二:flex item 拉伸?压缩?


在使用 flex 时,最常见的现象是这样的:


flex7.png

当子元素为 3 个时,不会被拉伸,为什么呢?


当子元素为 6 个事,会被压缩,又是为什么呢?


其实上面这两个疑问❓,只需了解两个属性:flex-growflex-shrink。因为这两个属性不常用,所以容易忽略,从而不去了解,那么就会造成疑惑。


flex-grow 属性指定了 flex 元素的拉伸规则。flex 元素当存在剩余空间时,根据 flex-grow 的系数去分配剩余空间。 flex-grow 的默认值为 0,元素不拉伸


flex-shrink 属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值。flex-shrink 的默认值为 1,元素压缩



该两个属性都是针对 主轴方向的剩余空间



所以



  • 当子元素数量较少时,存在剩余空间,但是又由于 flex-grow 的值为 0,所以子元素宽度不会进行拉伸。

  • 当子元素数量较多时,空间不足,但是又由于 flex-shrink 的值为 1,那么子元素就会根据相应的计算,来进行压缩。


特殊场景: 当空间不足时,子元素一定会压缩?试试单词很长(字符串很长)的时候呢?


flex8.png

现象三:文本溢出,flex-basis?width?


在布局中,如果指定了宽度,当内容很长的时候,就会换行。但是会存在一种特殊情况,就是如果一个单词很长为内容时,则不会进行换行;跟汉字是一样的道理,不可能从把汉字分成两半。


那么在 flex 布局中,会存在两种情况:


flex9.png

可以发现:



  • 设置了固定的 width 属性,字符串超出宽度之后,就会截取。

  • 而设置了固定的 flex-basis 属性,字符串超出宽度之后,会自动扩充宽度。


其实在这里可能有人会有疑惑:为什么把 width 和 flex-basis 进行对比?或者说 flex-basis 这个属性到底是干什么?



其实我也是刚刚才熟悉到这个属性,哈哈哈,不知道吧!!!



因为 flex-basis 是使用在 flex item 上,而 flex-basis(主轴上的基础尺寸)属性在大多数情况下跟 width 属性是等价的,都是设置 flex-item 的宽度。


上面的案例就是属于特殊情况,针对单词超长不换行时,flex-basis 就会表现出不一样的形式,自动扩充宽度


简单学习一下 flex-basis 的基本语法吧。


flex-basis 属性值:



  • auto: 默认值,参照自身 width 或者 height 属性。

  • content: 自动尺寸,根据内容撑开。

  • <'width'>: 指定宽度。


当一个属性同时设置 flex-basis(属性值不为 auto) 和 width 时,flex-basis 具有更高的优先级


现象四:flex 平分


当相对父容器里面的子元素进行平分时,我们会毫不犹豫的写出:


.father {
 width: 400px;
 height: 400px;
 background-color: #ddd;
 display: flex;
}
.son {
 flex: 1; /* 平分 */
 height: 90px;
}

flex10.png

那么我们是否会想过为什么会平分空间? 其中 flex:1 起了什么作用?


我们也许都知道 flex 属性是一个简写,是 flex-growflex-shrinkflex-basis 的简写。所以,flex 的属性值应该是三个组合值。


但是呢,flex 又类似于 font 属性一样,是一个很多属性的简写,其中一些属性值是可以不用写的,采用其默认值。


所以 flex 的属性值就会分析三种情况:一个值,两个值,三个值。


MDN 对其做了总结:


flex11.png

看图,规则挺多的,如果要死记的话,还是挺麻烦的。


针对上面的规则,其实只需要理解 flex 的语法形式,还是能够完全掌握(有公式,谁想背呢)。


flex = none | auto | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]  

希望你能看懂这个语法,很多 api 都有类似的组合。



  • | 表示要么是 none, 要么是 auto, 要么是后面这一坨,三选一。

  • || 逻辑或

  • ? 可选


理解上面这种语法之后,总结起来就是如下:


一个值



  1. none(0 0 auto)auto(1 1 auto) 是需要单独记一下的,这个无法避免。

  2. 无单位,就是 flex-grow,因为存在单位,就是 flex-grow 属性值规定为一个 number 类型的

  3. 有单位,就是 flex-basis,因为类似 width 属性是需要单位的,不然没有效果。


两个值



  1. 无单位,就是 flex-grow 和 flow-shrink,理由如上

  2. 其中一个有单位,就是 flex-grow 和 flex-basis,因为 flex-shrink 是可选的(这种情况是没有任何实际意义的,flex-basis设置了根本无效)。


三个值


三个值不用多说,一一对应。


理解了上面的语法形式,再来看 flex: 1 的含义就轻而易举了。一个值,没有单位,就是 flex-grow,剩余空间平均分配


现象五:多行,两边对齐布局


无论是 app 开发,还是网页开发,遇到最多的场景就是这样的:


flex12.png

两边对齐,一行元素之间的间距相同;如果一行显示不下,就换行依次对齐排布。


那么不难想到的就是 flex 布局,会写下如此代码:


.father {
 display: flex;
 justify-content: space-between;
 flex-wrap: wrap;
}
.son {
 width: 90px;
 height: 90px;
}

那么你就会遇到如下情况:


flex13.png

其中的第二、三种情况布局是不可以接受,数据数量不齐的问题。但是数据是动态的,所以不能避免出现类似情况。


你们遇到过这种类似的布局吗?会存在这种情况吗?是怎么解决的呢?


第一种解决方案:硬算


不使用 flex 的 justify-content 属性,直接算出元素的 margin 间隔。


.father {
 width: 400px;
 background-color: #ddd;
 display: flex;
 flex-wrap: wrap;
}
.son {
 margin-right: calc(40px / 3); /* 40px 为 父元素的宽度 - 子元素的宽度总和,   然后平分剩余空间*/
 width: 90px;
 height: 90px;
 background-color: #5adacd;
 margin-bottom: 10px;
}
/* 针对一行最后一个,清空边距 */
.son:nth-child(4n) {
 margin-right: 0;
}

缺点:只要其中的一个宽度发生变化,又要重新计算。


第二种解决方案:添加空节点


为什么要添加空节点呢?因为在 flex 布局中,没有严格的区分块级元素和行内元素。那么就可以使用空节点,来占据空间,引导正确的布局。


<style>
 .father {
   width: 400px;
   background-color: #ddd;
   display: flex;
   justify-content: space-between;
   flex-wrap: wrap;
}
 .son {
   width: 90px;
   height: 90px;
   background-color: #5adacd;
   margin-bottom: 10px;
}
 
 /* height 设置为 0 */
 .father span {
   width: 90px; /*空节点也是 flex-item, width 是必须一致的,只是设置高度为0,不占据空间*/
}
</style>
</head>
<body>
 <div></div>
 <div class="father">
   <div class="son">1</div>
   <div class="son">2</div>
   <div class="son">3</div>
   <div class="son">4</div>
   <div class="son">5</div>
   <div class="son">6</div>
   <div class="son">7</div>
   <!-- 添加空节点,个数为 n-2 -->
   <i></i>
   <i></i>
 </div>
</body>

这样也能解决上面的问题。


添加空节点的个数:n(一行的个数) - 2(行头和行尾,就是类似第一种情况和第四种情况本身就是正常的,就不需要空间点占据)


缺点:添加了dom节点


上面两种方案都解决问题,但是都有着各自的缺点,具体采用哪种方式,就看自己的选择了。


那么你们还有其他的解决方案吗?


总结


其实本篇所解释的现象是自己对 flex 知识掌握不牢而造成的,从而记录此篇,提升熟悉度。也希望能够帮助对这些现象有困惑的码友。


作者:copyer_xyf
来源:juejin.cn/post/7273025171111444540
>如果存在错误解释,评论区留言。

收起阅读 »

人情世故职场社会生存实战篇(五)

人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二)人情人情世故职场社会生存实战篇(三)人情人情世故职场社会生存实战篇(四) 41、问:带我的大哥让我办事我的效率都是最有效的,但为什么大哥就是不提我?反而还得罪了很多人。 答:孙悟空刚出社...
继续阅读 »

人情人情世故职场社会生存实战篇(一)
人情人情世故职场社会生存实战篇(二)
人情人情世故职场社会生存实战篇(三)
人情人情世故职场社会生存实战篇(四)



41、问:带我的大哥让我办事我的效率都是最有效的,但为什么大哥就是不提我?反而还得罪了很多人。


答:孙悟空刚出社会时,帮阿唐办事儿,遇到问题都是一马当先斩妖除魔,得罪了不少人,还差点把工作丢了,后来猴子学聪明了,遇到问题就请示观音,如来,搞关系,交了很多朋友,最终修成正果。


42、问:我跟一个挺有实力的前辈合作,我整天累的跟啥一-样,他天天就见几个人,大头还让他挣了,凭啥啊?


答:挣钱的永远是前辈,因为前辈负责关系,拿80%的利润,小弟负责干活,得20%的利润,前辈有关系人脉,而手里有关系人脉的人寥寥无几,会干活的小弟比比皆是。干活的不挣钱,挣钱的不干活,世界皆如此。


43、问:我昨天出了一个大单,提了2万多,我开心的跟同事分享,结果今天另外一个大单被同事撬走了,为什么?


答:你要学会闷声发大财。高调的人总有一天会毁了自己,沉住气,千万不要炫耀你的财富与成就,看似有人吹捧,其实他们的心里充满了嫉妒和怨气,真正智慧的人早就把自己调成了静音模式。


44、问:我们团队的老大,是特别有野心的人,但是手段不干净,什么数据造假 抄袭这些全都做过。他的能力是很强 ,但人品也实在令我反感。敢问师傅:如果要选一个大佬跟着,人品和德行需要纳为考虑吗?


答:你先爬上去,随后再试试看,干干净净做事能不能生存下去。有的领域可以,有的领域就不行, 所以你的标准答案应该是,保护好自己,随后竭尽全力的学习他的思路和手段,随后在自己运用的过程中,去筛选和判断。


45、问:如果对方硬是不收礼物的话,那是不是就不送了,还是找个节日或者由头继续送?


答:之前村里的会计来找我,我给了他两盒竹叶青,他说什么也不要。我直接扔他车里了,他说:你啊,太实在了。拒绝我东西的人,几乎没发生过。


有的人,连一瓶水都送不出去,8个领导在那儿 你买一瓶水,8个领导都说不喝了 谢谢你了。你还觉得你牛逼,哎,无语了。你要是买一打提溜过去,都喝了 没一个拒绝的。有这个觉悟的很少很少,有这觉悟,在底层锻炼三年就上去了。而更多的人,都是在基层锻炼到老……


46、问:我是单位某个部门的负责人,上面的意思是在本部门门评两个优秀员工,我在发愁把名额给哪个员工,得罪人的事不想干啊,您有什么好主意嘛?


答:开会就行了,让你的心腹帮你做引子,让所有的人发言,把大家的意见综合起来,什么样的人该上,让他们讲,你不要讲。开会一次不行就开两次,开着开着,答案就讨论出来了,然后你最后拍板就行了。大家都心服口服。


47、问:朋友兄弟把我介绍他单位上班,我是发了工资请他们吃饭还是?


答:现在就请,等个锤子啊。你提前请了,有福利提前给你。你发了工资再请,他们就不帮你了。因为这事在帮你,就显得他们势利眼了。人活着都是要尊严的。他们当下帮你,当下就有回报率,帮你就会上瘾的。你是喜欢工资日结还是月结呀。


48、问:我在单位被投诉,虽然自己一再辩解是有人故意带节奏的,是有人利用其他人的弱势群体身份打小报告,但是领导就是认定我是被人投诉的,现在停了我原来的职务,我以前没给领导送过礼,现在去送不知道还有没有用。


答:去送就是了,投诉你的这个人,他百分百送礼了。同样你一直给领导送礼,谁投诉你了他会帮你压下去。人啊 总归是人。一句话你没有给领导送礼。你送礼了屁事儿没有。人人都是势利小人,人人都贪得无厌。这句话永远都对。


49、问:我在官场和商海周旋几年了,截止到现在唯一还没搞定的难题,就是一个领导权利有的,每次我请吃饭他也来,送礼物也收。就是帮忙的时候总是帮我的对手多一些。在物资采购这块我的对手胜我一筹。我该用什么办法来对付他。


答:哎,你二啊,肯定是你的竞争对手给的多啊,给他的返点多。你请我吃饭,你买单了。我看到小A请你吃饭,我帮小A买单了。你很生气 我说马勒戈壁,你生气个锤子啊。小A已经帮我买过1000次单了。啥都是等价交换。


50、问:你一直说:求人办事送礼要循序渐进,什么才是循序渐进,您能指点一下吗,谢谢了。


答:打个比方,我第一阶段,请他喝可乐。我说想请一天假。他说小事,准。第二个阶段,请他吃饭喝酒,我想要全勤奖。他说:没问题。第三阶段,送华子,我想找个闲差。他说,明天去仓库。第四个阶段,我送五粮液,什么要求都没提。他说:仓库要不你来管理吧,也挺闲的。第五个阶段,我送金子,也没提要求。他说:明天晚上有个局带你去,抓住机会能不能升上去看你自己……



作者:公z号_纵横潜规则
来源:juejin.cn/post/7269787962342490175

收起阅读 »

Git 合并冲突不知道选哪个?试试开启 diff3 吧

iOS
导读:Git 早在 2008 年就提供 diff3,用于冲突展示时额外提供该区域的原始内容(两个分支公共祖先节点在此区域的内容),帮助更好的合并冲突。在 2022 年 Q1 发布的 Git 2.35 ,提供了一个新的选项 zdiff3,进一步优化了diff3 ...
继续阅读 »

导读:Git 早在 2008 年就提供 diff3,用于冲突展示时额外提供该区域的原始内容(两个分支公共祖先节点在此区域的内容),帮助更好的合并冲突。在 2022 年 Q1 发布的 Git 2.35 ,提供了一个新的选项 zdiff3,进一步优化了diff3 的展现。



Git 合并冲突,常见的展示形式分为 Current Change (ours, 当前分支的变更)和 Incoming Change (theirs, 目标分支的变更),两者针对的是同一区域的变化。



观察上面这个冲突示例,我们并不清楚两个分支各自都发生了什么变化,有两种可能:

  1. 两个分支同时增加了一行代码 "pkg": xxx
  2. 原先的提交记录里就有 "pkg": xxx ,只是两个分支同时修改了版本号

实际上这个例子,是第二种情况,两个分支都对 pkg 的版本做了改变。




这样的场景还有很多,如果不知道上下文,在解决冲突的时候容易束手束脚。


现在,我们可以使用 git 提供的 diff3 选项来调整合并冲突的展示效果



红框区域(|||||||=======)表示的就是改动前的上下文,确切的说, 当前分支 目标合并分支 的最近公共祖先节点在该区域的内容。


如何开启


冲突展示有两个选项 diff3merge(默认选项),可以通过以下方法进行配置



在 v2.35 新增了 zdiff3 选项,下文会提到

  • 对单个文件开启
git checkout --conflict=diff3 <文件名>
# 示例
git checkout --conflict=diff3 package.json
# 使用默认配置
git checkout --conflict=merge package.json
  • 项目配置
git config merge.conflictstyle diff3
# 删除配置
git config --unset merge.conflictstyle
# 使用默认配置
git config merge.conflictstyle merge
  • 全局配置
git config --global merge.conflictstyle diff3
# 删除配置
git config --global --unset merge.conflictstyle

示例展示


在同一位置添加代码行

<<<<<<< HEAD
import 'some_pkg';
||||||| merged common ancestor
=======
c
>>>>>>> merged-branch

如上示例,合并的公共祖先节点在该位置是空白,每个分支都在相同的位置添加代码行。


我们通常希望保留两者,并按照最有意义的顺序排序,也可能选择只保留其中一个。以下是一个冲突修复后的示例:

import 'some_pkg';
import 'some_pkg';

一方修改一方删除

<<<<<<< HEAD
||||||| merged common ancestor
console.log('调试信息')
=======
console.log('调试信息2')
>>>>>>> merged-branch

如上示例,一方把调试信息删除,而另一方修改了调试信息内容。对于这个示例,我们通常是选择删除而不保留修改。


为什么不是默认选项


经常需要知道祖先节点的内容来确保正确的合并,而 diff3 解决了这个痛点。同时,diff3 没有任何弊端(除了冲突区域行数变多🌝),没有理由不启用它。


那为什么 Git 不将 diff3 作为默认的合并冲突展示选项呢?


stackoverflow 上有人回答了这个问题,大概意思是说可能和 Unix diff 有关,早前默认的 Unix diff 不支持展示 3-way diff (待考证)。


之后的新版本也不方便调整默认值,否则会对用户造成困扰 — “合并冲突区域怎么多了一块内容?”。


zdiff3 (zealous diff3)


2022 年 Q1 ,Git 发布 v2.35,其中有个变化是冲突展示新增了 zdiff3 的配置选项。


zdiff3 基于 diff3 ,并对冲突块两侧的公共代码行做了压缩。


举个例子:




使用默认配置,合并冲突展示如下:

1
2
3
4
A
<<<<<<< ours
B
C
D
=======
X
C
Z
>>>>>>> theirs
E
7
8
9

使用 diff3 后,合并冲突展示如下:

1
2
3
4
<<<<<<
A
B
C
D
E
||||||
5
6
======
A
X
C
Z
E
>>>>>>
7
8
9

通过观察可以发现,冲突区域两侧有公共的代码行 A、E 。而这些代码行在默认配置下会被提取到外部。


而用了 zdiff3 之后,A、E 两行又将移到冲突之外。

1
2
3
4
A
<<<<<<
B
C
D
||||||
5
6
======
X
C
Z
>>>>>>
E
7
8
9

一句话总结 zdiff3 的优化:即展示公共祖先节点内容,又能够充分压缩冲突的公共部分。


最后


解决 Git 合并冲突是一个难题,diff3 并不是一个“银弹”,它只能帮助提供更多的信息,减少决策成本。


推荐读者尝试下 zdiff3 ,至少使用 diff3 ,并将其作为默认配置。


最后,如果看完本文有收获,欢迎一键三连(点赞、收藏、分享)🍻 ~


拓展阅读


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

第一份只干了五天的前端工作

可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。我当时问了五险一金说是按照7000多交,结果公积金按最低2千多我去的第三天,后端管理A,负责给我分配任...
继续阅读 »

可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。

  • 当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。
  • 我当时问了五险一金说是按照7000多交,结果公积金按最低2千多
  • 我去的第三天,后端管理A,负责给我分配任务的,他早上和我讲,类似初创公司领导不愿意看到早下班,晚上也会找我过一下进度啥的,要定制一个学习计划,自己找时间去熟悉代码...

面试过程倒还顺利,前端A问了些技术,比较偏实际应用的,对我github上做的东西也感兴趣。前端管理B问的有些偏人事的,然后问我下午有没有其它安排,没有的话不走流程叫领导A面一下。领导A就直接说了平薪,试用期3个月80%能不能接受,当时那种场景我就顺势而为说可以,我之前的工作都是少一千。


我往回走的时候hr给我打电话,前面的没接到,我打过去得知是想问我在哪,想问一下总裁有没有时间,不然就要到周六。


等到周六面完,下周一打电话说通过,让我周三去报到,我说太快要下周一。然后这家公司每个月月末的周六是组织学习培训的,我也参加了。


第一天搭建环境,账号权限这些,电脑连不上WiFi,我问前台有没有网线,她说没有我也没去下面找,又换了一台,接口就不同了,又弄双屏的转接线。我找她一次,她都是从前台去存放物品的地方,发现不太对再找她已经回前台了,前后不到一分钟的时间吧。确实我对这个也不太熟,有些不好意思麻烦她,最后少一个转接器我就自己买了,否则屏幕显示不清晰,顺便买了两个增高架。


前端管理B给我说了几个项目,我看了会代码,管理B找我谈了一会。


九点上班六点下班,上班第一天我看快到六点半了就准备走,这时候管理B拉着我去展厅看了下公司的产品。


第二天后端管理A请假了,前端管理B给我说了会代码,后面主要负责的项目,跑了下本地联调。


基本上一个菜单一个项目,每个项目用iframe嵌套,然后有一些组件库这些,代码之间组件嵌套的比较深,多以component。数据走的是配置,流向很乱。接口的传递和返回都很庞大,有些还是json字符串,20-60多个kb,看结构话要单独复制出来。一些项目调试没有sourcemap,给我的感觉就是把简单的事做复杂了。


第三天后端管理A给我安排了一些事情,就是口头说了一下,意思这种改动不需要ui、不需要产品自己就可以定,基本上就是我做完让他看一下,他觉得不好在改。这里面就有一个问题,就是到底改哪里没有全部列出来,我对项目又不熟,一般都有一个上下文的概念,可对不上的时候,才发现是另一个地方,他又是没有规划的那种。


往往走配置基本会出现一种情况,就是一些东西需要单独处理,或者配置选项越加越多,或者当初实现的时候偷懒就给写死了,东改一下西改一下,而且这种封装太笨重了,不好优化,只能说熟悉了更快些,但是维护成本始终会处在一个固定的量级,而且随着功能迭代,补丁会越来越多。


下午的时候管理B把我叫过去,让我协助后台A排查两个问题,说别的环境没有只有这个环境有,据说问题已经存在很久了,我找到问题A的代码所在地、以及问题的原因就花了很长时间,单从前端看,是因为一个代码报错导致的。因为这些东西要按照业务流程来,我不知道什么是对的,只能关注接口的返回然后找对应的前端组件渲染逻辑,对比差异反馈,本质上就是反推接口返回有哪些不对,找到问题已经晚上10点了,两个问题都是后台在处理用户操作后,前后id不一样导致拿到的数据不对所致。


这种问题让一个刚入职的人排查显然浪费时间,中间链路太多了,我问了下后端A他之前都是和谁对接,得知前面的人离职了,我就想着这种情况是最难受的,总不可能我一上来就能接手他的工作,巧妇难为无米之炊,哪有那么丝滑的过度,总要有个渐进性的过程吧。


每天要建tapd,再把tapd的内容复制出来写成日报,然后也要写周报,还要在领导有时间的时候找他汇报进度,或者等他来找你。


第四天,后端管理A给我说了下今天的任务,有一些历史遗留问题,我处理的还是很快的,直到前端写完对接口的时候,他只是钉钉发了一些字段给我,发现还有另一个项目要改,找到代码熟悉,对好逻辑写好前端代码,我又在本地连了下测试环境,跑了下流程,接口报错了,我看六点半了就走了。


第四天上午还过了个需求,虽然我也听不太懂,但是管理B直接说这个事情15个自然日还是工作日搞完。


后端管理A评价我的日报,意思任务完不成要及时上报,晚上要和他汇报进度,我想着我都不知道一天到底有多少任务,也不知道完成任务花费多长时间,更不知道啥样算完成,我咋完成?


第五天,前端管理B找我聊了一会,说是来了一周,我就把我的感触说了,他问我打算怎么处理,我就说这种强度我就不干了,感觉不值,他说他来处理。


我对比了下我能够得到的和我将要面对的,平薪80%,三个月,我觉得有些不值得,待遇还不如我两年前,也没到山穷水尽的地步,受这罪干嘛?以前入职也不是没有压力大的时候,但待遇有所增长,看了代码啥的我也觉得对我是一种历练,即便不说我也会主动学习,因为我知道当我很熟悉的时候后面效率更高,算提早付出了,还是在时间不那么紧凑的时候,但这家公司给我一种压榨的感觉。


后面我把项目分支发给后端管理A,部署发版耽搁了一会,后面是找的前端管理B解决的,我后面了解到走的是自己的搭建的运维系统,两个项目有不同的分支名,我把自己的分支手动合并,再找后端管理A就好了。


接下来就是他发现一些问题让我改,持续到下午五点半左右,我再次提交代码时,发现gtilab账号已经被注销了,他让我把代码改动发给前端管理B,这个时候我的电脑已经重置,被前台收走了。还是有些遗憾,再次改动时发现轻车熟路了许多,前面还是花了不少精力的。


公司提供了午休床,我贴了个标签,前端管理B贴心的给我个东西增加区分性,但我用过一次后没再找见,离职也是要交给前台的,找的时候还在想不会所有放午休床的地方都要找一遍吧,还好发现了破碎的标签,应该被别人用的时候弄碎了。


其实周五不聊的话我可能想着再适应下,也没想到当天就能走完离职,清理tapd的时候发现有五十多个bug挂在我这,看到一个六月份的。


幸亏我带了包过去,不然东西都不好拿,看着8月份4天32小时...。


可能是太久没工作了吧,我便抱着试一试的态度,坦白的讲我也想过边干边找,入职的这几天有了新的方向,我github上写的工具依旧发挥稳定,替我节省了很多时间。


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

iOS小技能:去掉/新增导航栏黑边(iOS13适配)

iOS
引言 背景: 去掉导航栏下边的黑边在iOS15失效 原因:必须使用iOS13之后的APIUINavigationBarAppearance设置才能生效UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) NS_SW...
继续阅读 »

引言


背景: 去掉导航栏下边的黑边在iOS15失效
原因:必须使用iOS13之后的APIUINavigationBarAppearance设置才能生效

UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) NS_SWIFT_UI_ACTOR
@interface UINavigationBarAppearance : UIBarAppearance

I 导航栏的黑边设置


1.1 去掉导航栏下边的黑边(iOS15适配)



iOS15之前: [self.navigationBar setShadowImage:[[UIImage alloc] init]];

        [vc.navigationController.navigationBar setBackgroundImage:[ImageTools createImageWithColor: [UIColor whiteColor]] forBarMetrics:UIBarMetricsDefault];


iOS15之后


if(@available(iOS 13.0, *)) {
UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];

//去掉透明后导航栏下边的黑边
appearance.shadowImage =[[UIImage alloc] init];

appearance.shadowColor= UIColor.clearColor;



navigationBar.standardAppearance = appearance;

navigationBar.scrollEdgeAppearance = appearance;

}

1.2 设置导航栏下边的黑边(iOS13适配)




// 设置导航栏下边的黑边
+ (void)setupnavigationBar:(UIViewController*)vc{



if (@available(iOS 13.0, *)) {

UINavigationBar *navigationBar = vc.navigationController.navigationBar;

UINavigationBarAppearance *appearance =navigationBar.standardAppearance;


appearance.shadowImage =[UIImage createImageWithColor:k_tableView_Line];

appearance.shadowColor=k_tableView_Line;


navigationBar.standardAppearance = appearance;
navigationBar.scrollEdgeAppearance = appearance;

} else {
// Fallback on earlier versions

UINavigationBar *navigationBar = vc.navigationController.navigationBar;
[navigationBar setBackgroundImage:[[UIImage alloc] init] forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; //此处使底部线条颜色为红色
// [navigationBar setShadowImage:[UIImage createImageWithColor:[UIColor redColor]]];

[navigationBar setShadowImage:[UIImage createImageWithColor:k_tableView_Line]];

}



}




II 去掉TabBar的顶部黑线


  • setupshadowColor


- (void)setupshadowColor{

UIView * tmpView = self;
tmpView.layer.shadowColor = [UIColor blackColor].CGColor;//设置阴影的颜色
tmpView.layer.shadowOpacity = 0.08;//设置阴影的透明度
tmpView.layer.shadowOffset = CGSizeMake(kAdjustRatio(0), kAdjustRatio(-5));//设置阴影的偏移量,阴影的大小,x往右和y往下是正
tmpView.layer.shadowRadius = kAdjustRatio(5);//设置阴影的圆角,//阴影的扩散范围,相当于blur radius,也是shadow的渐变距离,从外围开始,往里渐变shadowRadius距离


//去掉TabBar的顶部黑线
[self setBackgroundImage:[UIImage createImageWithColor:[UIColor clearColor]]];
[self setShadowImage:[UIImage createImageWithColor:[UIColor clearColor]]];

}


see also


iOS小技能:自定义导航栏,设置全局导航条外观。(iOS15适配)
blog.csdn.net/z929118967/…


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

苹果回应 iPhone14 电池老化快:属于正常现象,iPhone 15 系列顶配机型有望首次搭载潜望式镜头

iOS
国内要闻 曝小米自研系统为全端系统 日前,有数码博主爆料,小米自研操作系统属于全端自研系统,兼容AOSP(Android 开放源代码项目)。如此看来,小米自研操作系统还可能有车机、平板、手表等终端系统,而且小米走的是华为鸿蒙操作系统的路子,前期先兼容安卓更为稳...
继续阅读 »



国内要闻


曝小米自研系统为全端系统

日前,有数码博主爆料,小米自研操作系统属于全端自研系统,兼容AOSP(Android 开放源代码项目)。如此看来,小米自研操作系统还可能有车机、平板、手表等终端系统,而且小米走的是华为鸿蒙操作系统的路子,前期先兼容安卓更为稳妥,保住既有的用户量。(手机中国)


华为辟谣网传3.2万名科学家正式移籍:造谣者毫无根据、无中生有

近日,网络上多家平台发布了针对华为公司的系列言论,经证实,该系列言论均为谣言,华为表示,造谣者毫无根据、无中生有。此外,华为呼吁各位网友“勿信勿传,果断举报”。(第一财经)


清华大学联合字节跳动,开源听觉大语言模型 SALMONN

清华大学联合字节火山语音团队提出了一种全新的「听觉」大语言模型——SALMONN (Speech Audio Language Music Open Neural Network)。相较于仅仅支持语音输入或非语音音频输入的其他大模型,SALMONN 对语音、音频事件、音乐等各类音频输入都具有感知和理解能力,相当于给大语言模型「加了个耳朵」,从而涌现出多语言和跨模态推理等高级能力。


钉钉公布 AI 版本商业定价:调用一次大模型不到 5 分钱

8 月 22 日,钉钉召开 2023 生态大会,据总裁叶军介绍:截至今年 3 月末,钉钉软件付费企业数达 10 万家,其中小微企业占 58%,中型企业占 30%,大型企业占 12%;钉钉付费 DAU 超过 2300 万。这也是钉钉首次公布其商业化核心进展。此外,钉钉还公布了大模型落地应用场景的商业化方案:在钉钉专业版年费 9800 元基础上,增加 10000 元即可获得 20 万次大模型调用额度;在专属钉钉年费基础上,增加 20000 元即可获得 45 万次大模型调用额度,约等于平均每次调用只需 0.44~5 分钱。(IT 之家)


吉利回应与百度合作造车:开发一直为吉利主导,百度提供技术支持

不久前,吉利与百度合作造车计划突遭生变,“集度”变身“极越”。近日,在吉利汽车半年业绩发布会上,吉利控股集团CEO李东辉回应腾讯新闻《远光灯》,极越定位为吉利控股旗下高端智能汽车机器人品牌。在极越的开发过程中,一直都是吉利控股来主导的,百度提供了大数据、无人驾驶等领域的技术支持。


非法注册 300 万个微信号!央视曝光特大黑灰产系列案

8月7日、8日、9日中午,中央电视台《今日说法》栏目以《揭秘“黑灰产”》为题,分上、中、下三集对山东淄博周村公安分局破获的特大黑灰产案件侦破过程进行专题报道。犯罪分子批量注册并贩卖微信号,形成产业链。这些微信号多用于电信诈骗等违法犯罪活动。该犯罪团伙共非法注册微信号 300 余万个,非法获利达 1000 余万元。警方通过追查嫌疑人注册微信的手机号码来源,打掉一个号商团伙,揪出二十余名省级运营商“内鬼”。他们利用手中的权力,牟取巨额私利,为犯罪团伙提供非法注册的手机号码。(今日说法)


国际要闻


苹果回应 iPhone14 电池老化快:属于正常现象

苹果公司的 iPhone 14 系列手机上市不到一年,就出现了电池健康度下降过快的问题。一些用户反映,他们的手机电池在使用几个月后,就损耗了 10% 以上的容量。苹果公司表示,这种情况属于正常现象,只有当电池容量低于 80% 时,才能在保修期内享受免费更换服务。


据了解,如果使用非正品电池或者其他 iPhone 14 手机上拆下来的电池进行更换,那么手机将无法识别新电池,并且会禁用电池健康度功能,这意味着用户无法查看电池的剩余容量和性能状况。(IT之家)


Meta 推出 AI 模型 SeamlessM4T,可翻译和转录近百种语言

Meta 近日发布了人工智能模型 SeamlessM4T,可以翻译和转录近 100 种语言的文本和语音。SeamlessM4T 支持对近百种语言进行语音以及文本识别,同时支持近 100 种输入语言和 36 种输出语言的语音到语音翻译。Meta 表示,将会以研究许可证的形式公开发布 SeamlessM4T,以便研究人员和开发人员在此基础上开展工作。Meta 还将发布 SeamlessAlign 的元数据,这是迄今为止最大的开放式多模态翻译数据集,共挖掘了 27 万小时的语音和文本对齐。(品玩)


iPhone 15 系列顶配机型有望首次搭载潜望式镜头

来自摩根士丹利的一份分析师报告指出,iPhone 15 Pro Max(或改名为 iPhone 15 Ultra)将获得苹果有史以来第一款潜望式镜头,其变焦能力从前代的 3 倍将提升到 5-6 倍,表现令人相当期待。这份报告还提到,由于全新传感器的加入,使 iPhone 15 系列顶配机型的备货能力受到影响,或许会在 iPhone 15 系列开售后 3-4 周时间才会陆续发货。毫无疑问,这将会是 iPhone 15 系列中最值得关注的一款机型。(雷科技)


微软宣布将把动视暴雪云游戏权益出售给育碧,以安抚英国监管机构

8 月 22 日消息,据外媒报道,当地时间周一,微软宣布将把动视暴雪的云游戏权益出售给育碧,以重组其拟议的动视暴雪收购交易。报道称,微软此举旨在安抚英国监管机构英国竞争和市场管理局(CMA),因为该机构担心这笔交易会扼杀快速增长的云游戏市场的竞争。当地时间周一,微软与育碧签署了一项为期 15 年的协议,将《使命召唤》等动视暴雪的云游戏相关权益授权给育碧,这是微软为其收购动视暴雪获得反垄断批准的最新举措。(TechWeb)


Meta 推出拥有 12 种复杂技能机器人,上得厅堂下得厨房

耗时 2 年,Meta 联手卡耐基梅隆大学推出通用机器人智能体——RoboAgent,可以通过图像或者语言指令,来指挥机器人完成任务。它拥有 12 种不同的复杂技能,泡茶、烘焙不在话下,未来还能泛化 100 多种未知任务。(网易科技)


IBM 推企业级 AI 平台!剑指企业级 AI 应用三大挑战

日前,IBM 面向中国区正式推出企业级 AI 平台 watsonx,包含企业级 AI 与数据平台 watsonx.ai、湖仓一体的数据存储方案 watsonx.data 以及 AI 治理工具包 watsonx.governance。


程序员专区


微软 Excel 宣布集成 Python

微软已经将 Python 原生集成到 Excel 公测版中,首先向 Microsoft 365 Insiders 推出,从而使用户能够借助 Python 库、数据可视化和分析的能力更好地使用 Excel。目前该功能只能在桌面版 Excel 中使用,但微软表示 Python 计算也可以在微软云中运行。


Google 更新 Android 运行时应用提速最高三成

Android 运行时 (Android Runtime 或 ART)的最新更新将帮助应用在部分设备上的启动时间缩短最多 30%。ART 是 Android 操作系统的引擎,提供了所有 Android 应用和绝大多数服务所依赖的运行时和核心 API。改进 ART 将能让所有开发者受益,让应用执行更快,字节码编译更高效。Google 表示它正致力于让 ART 模块化独立于操作系统更新。ART 的可独立更新将能让用户更快获得性能优化和安全更新,让开发者更快获得 OpenJDK 改进和编译器优化。它的测试显示,ART 13 的运行时和编译器优化在部分设备上实现了最高 30% 的应用启动改进。


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