美团多场景建模的探索与实践
本文介绍了美团到家/站外投放团队在多场景建模技术方向上的探索与实践。基于外部投放的业务背景,本文提出了一种自适应的场景知识迁移和场景聚合技术,解决了在投放中面临外部海量流量带来的场景数量丰富、场景间差异大的问题,取得了明显的效果提升。希望能给大家带来一些启发或帮助。
1 引言
美团到家Demand-Side Platform(下文简称DSP)平台,主要负责在美团外部媒体上进行商品或者物料的推荐和投放,并不断优化转化效果。随着业务的不断发展与扩大,DSP对接的外部渠道越来越丰富、展示形式越来越多样,物料展示场景的差异性愈发明显(如开屏、插屏、信息流、弹窗等)。
例如,用户在午餐时间更容易点击【某推荐渠道下】【某App】【开屏展示位】的快餐类商家的物料而不是【信息流展示位】的啤酒烧烤类商家物料。场景间差异的背后本质上是用户意图和需求的差异,因此模型需要对越来越多的场景进行定制化建设,以适配不同场景下用户的个性化需求。
业界经典的Mixture-of-Experts架构(MoE,如MMoE、PLE、STAR[1]等)能一定程度上适配不同场景下用户的个性化需求。这种架构将多个Experts的输出结果通过一个门控网络进行权重分配和组合,以得到最终的预测结果。早期,我们基于MoE架构提出了使用物料推荐渠道进行场景划分的多场景建模方案。然而,随着业务的不断壮大,场景间的差异越来越大、场景数量也越来越丰富,这版模型难以适应业务发展,不能很好地解决DSP背景下存在的以下两个问题:
- 负迁移现象:以推荐渠道为例,由于不同推荐渠道的流量在用户分布、行为习惯、物料展示形式等方面存在差异,其曝光数、点击率也不在同一个数量级(如下图1所示,不同渠道间点击率相差十分显著),数据呈现典型的“长尾”现象。如果使用推荐渠道进行多场景建模的依据,一方面模型会更倾向于学习到头部渠道的信息,对于尾部渠道会存在学习不充分的问题,另一方面尾部渠道的数据也会给头部渠道的学习带来“噪声”,导致出现负迁移。
- 数据稀疏难以收敛:DSP会在外部不同媒体上进行物料展示,而用户在访问外部媒体时,其所处的时空背景、上下文信息、不同App以及物料展示位等信息共同构成了当前的场景,这样的场景在十万的量级,每个场景的数据又十分稀疏,导致模型难以在每个场景上得到充分的训练。
在面对此类建模任务时,业界现有的方法是在不同场景间进行知识迁移。例如,SAML[2]模型采用辅助网络来学习场景的共享知识并迁移至各场景的独有网络;ADIN[3]和SASS[4]模型使用门控单元以一种细粒度的方式来选择和融合全局信息到单场景信息中。然而,在DSP背景中复杂多变的流量背景下,场景差异性导致了场景数量的急剧增长,现有方法无法在巨量稀疏场景下有效。
因此,在本文中我们提出了DSP背景下的自适应场景建模方案(AdaScene, Adaptive Scenario Model),同时从知识迁移和场景聚合两个角度进行建模。AdaScene通过控制知识迁移的程度来最大化不同场景共性信息的利用,并使用稀疏专家聚合的方式利用门控网络自动选择专家组成场景表征,缓解了负迁移现象;同时,我们利用损失函数梯度指导场景聚合,将巨大的推荐场景空间约束到有限范围内,缓解了数据稀疏问题,并实现了自适应场景建模方案。
2 自适应场景建模
在本节开始前,我们先介绍多场景模型的建模方式。多场景模型采用输入层 Embedding + 混合专家(Mixture-of-Experts, MoE)的建模范式,其中输入信息包括了用户侧、商家侧以及场景上下文特征。多场景模型的损失由各场景的损失聚合而成,其损失函数形式如下:
其中,为场景数量,为各场景的损失权重值。
我们提出的AdaScene自适应场景模型主要包含以下2个部分:场景知识迁移(Knowledge Transfer)模块以及场景聚合(Scene Aggregation)模块,其模型结构如下图2所示。场景知识迁移模块自适应地控制不同场景间的知识共享程度,并通过稀疏专家网络自动选择 K 个专家构成自适应场景表征。场景聚合模块通过离线预先自动化衡量所有场景间损失函数梯度的相似度,继而通过最大化场景相似度来指导场景的聚合。
该模型结构的整体损失函数如以下公式所示:
其中,为每个场景组的损失函数所对应的系数,为第个场景组下的的场景数量,为某种场景组的划分方式。
下面,我们分别介绍自适应场景知识迁移和场景聚合的建模方案。
2.1 自适应场景知识迁移
在多场景建模中,场景定义方式决定了场景专家的学习样本,很大程度上影响着模型对场景的拟合能力,但无论采用哪种场景定义方式,不同场景间用户分布都存在重叠,用户行为模式也会有相似性。
为提升不同场景间共性的捕捉能力,我们从场景特征和场景专家两个维度探索场景知识迁移的方法,在以物料推荐渠道×App×展示形态作为多场景建模Base模型的基础上,构建了如下图3所示的自适应场景知识迁移模型(Adaptive Knowledge Transfer Network, AKTN)。该模型建立了场景共享参数与私有参数的知识迁移桥梁,能够自适应地控制知识迁移的程度、缓解负迁移现象。
- 场景特征适配:通过Squeeze-and-Excitation Network[5]构建场景适应层(Scene Adaption Layer),其结构可表示为,其中表示全连接层,为激活函数。由于不同场景对原始特征的关注程度存在较大差异,该层能够根据不同场景的信息生成原始特征的权重,并利用这些权重对输入特征进行相应的变换,实现场景特定的个性化输入表征,提高模型的场景信息捕捉能力。
- 场景知识迁移:使用GRU门控单元构建场景知识迁移层(Scene Transfer Layer)。GRU门控单元通过场景上下文信息对来自全局场景专家和当前场景专家的信息流动进行控制,筛选出符合当前场景的有用信息;并且,该结构能以层级方式进行堆叠,不断对场景输出进行修正。
场景特征适配在输入层根据场景信息对不同特征进行权重适配,筛选出当前场景下模型最关注的特征;场景知识迁移在隐层专家网络中进行知识迁移,控制共享专家中共性信息向场景独有信息的流动,使得场景共性信息得以传递。
这两种知识迁移方式互为补充、相辅相成,共同提升多场景模型的预估能力。我们对比了不同模块的实验效果,具体结果如下表1所示。可以看出,引入场景知识迁移和特征权重优化在头部、尾部渠道都能带来一定提升,其中尾部小流量场景上(见下表1子场景2、3)有更为明显的提升,可见场景知识迁移缓解了场景之间的负迁移现象。
相关研究和实践表明[6][7][8],稀疏专家网络对于提高计算效率和增强模型效果非常有用。因此,我们在AKTN模型的基础上,在专家层进一步优化多场景模型。具体的,我们将场景知识迁移层替换为自动化稀疏专家选择方法,通过门控网络从大规模专家中选取与当前场景最相关的个构成自适应场景表征,其选择过程如下图4所示:
在实践中,我们通过使用可微门控网络对专家进行有效组合,以避免不相关任务之间的负迁移现象。同时大规模专家网络的引入扩大了多场景模型的选择空间,更好地支持了门控网络的选择。考虑到多场景下的海量流量和复杂场景特征,在业界调研的基础上对稀疏专家门控网络进行了探索。
具体而言,我们对以下稀疏门控方法进行了实践:
- 方法一:通过散度衡量子场景与各专家之间的相似度,以此选择与当前场景最匹配的个专家。在实现方式上,使用场景*专家的二维矩阵计算相似性,并通过散度选择出最适合的个专家。
- 方法二:每个子场景配备一个专家选择门控网络,个场景则有个门控网络。对于每个场景的门控网络,配备个单专家选择器[9],每个单专家选择器负责从个专家中选择一个作为当前场景的专家(为Experts个数)。在实践中,为提高训练效率,我们对单专家选择器中权重较小的值进行截断,保证每个单专家选择器仅选择一个专家。
在离线实验中,我们以物料推荐渠道 * 展示形态作为场景定义,对上述稀疏门控方法进行了尝试,离线效果如下表2所示:
可以看出,基于软共享机制的专家聚合方法能够更好地通过所激活的相同专家网络对各场景之间的知识进行共享。相较于常见的以截断方式为主的门控网络,使用二进制编码的方式使得其在不损失其他专家网络信息的同时,能够更好地收敛到目标专家数量,同时其可微性使得其在以梯度为基础的优化算法中训练更加稳定。
同时,为了验证稀疏门控网络能否有效区分不同场景并捕捉到场景间差异性,我们使用=16个专家中选择=7个的例子,对验证集中不同场景下各专家的利用率、选择专家的平均权重进行了可视化分析(如图5-图7所示),实验结果表明该方法能够有效地选择出不同的专家对场景进行表达。
例如,图6中KP_1更多地选择第5个专家,而KP_2更倾向于选择第15个专家。并且,不同场景对各专家的使用率以及选择专家的平均权重也有着明显的差异性,表明该方法能够捕捉到细分场景下流量的差异性并进行差异化的表达。
实验证明,在通过大规模专家网络对每个场景进行建模的同时,基于软共享机制的专家聚合方法能够更好地通过所激活的相同专家网络对各场景之间的知识进行共享。 同时,为了进一步探索Experts个数对模型性能的影响,我们在方法二的基础上通过调整专家个数和topK比例设计了多组对比实验,实验结果如下表3所示:
从实验数据可以看出,大规模的Experts结构会带来正向的离线收益;并且随着选取专家个数比例的增加(表3横轴),模型整体的表现效果也有上升的趋势。
2.2 自适应场景聚合
理想情况下,一条请求(流量)可以看作一个独立的场景。但如引言所述,随着DSP业务持续发展,不同的物料展示渠道、形式、位置等持续增加,每个场景的数据十分稀疏,我们无法对每个细分场景进行有效训练。因此,我们需要对各个推荐场景进行聚类、合并。我们使用场景聚合的方法对此问题进行求解,通过衡量所有场景间的相似度,并最大化该相似度来指导场景的聚合,解决了数据稀疏导致难以收敛的问题。具体的,我们将该问题表示为:
其中表示某种分组方式,为场景在分组内与其他场景的总体相似度。在将个场景聚合成个场景组的过程中,我们需要找到使得场景间整体相似度最大的分组方式。
因此,我们在2.1节场景知识迁移模型的基础上,增加了场景聚合部分,提出了基于Two-Stage策略进行训练的场景聚合模型:
- Stage 1:基于相似度衡量方法对各场景的相似度进行归纳,并以最大化分组场景的相似度为目标找到各场景的最优聚合方式(如Scene1与Scene 4可聚合为场景组合Scene Gr0up SGA);
- Stage 2:基于Stage 1得到的场景聚合方式,以交叉熵损失为目标函数最小化各场景下的交叉熵损失。
其中,Stage 2与2.1节中所述一致,本节主要针对Stage 1进行阐述。我们认为,一个有效的场景聚合方法应该能自适应地应对流量变化的趋势,能够发现场景之间的内在联系并依据当前流量特点自动适配聚合方法。我们首先想到的是从规则出发,将人工先验知识作为场景聚合的依据,按照推荐渠道、展示形式以及两者叉乘的方式进行了相应迭代。然而这类场景聚合方式需要可靠的人工经验来支撑,且在应对海量流量时不能迅速捕捉到其中的变化。
因此,我们对场景之间关系的建模方法进行了相关的探索。首先,我们通过离线训练时场景之间的表征迁移和组合训练来评估场景之间的影响,但这种方式存在组合空间巨大、训练耗时较长的问题,效率较低。
在多任务的相关研究中[10][11][12][13],使用梯度信息对任务之间的关系进行建模是一种有效的方法。类似的在多场景模型中,能够根据各场景损失函数的梯度信息对场景间的相似度进行建模,因此我们采用多专家网络并基于梯度信息自动化地对场景之间的相似度进行求解,模型示意如下图8所示:
基于上述思路,我们对场景之间的关系建模方法进行了以下尝试:
1. Gradient Regulation
基于梯度信息能够对场景信息进行潜在表示这一认知,我们在损失函数中加入各场景损失函数关于专家层梯度距离的正则项,整体的损失函数如下所示,该正则项的系数表示场景之间的相似度,为常见的评估梯度之间距离的方法,比如,距离。
2. Lookahead Strategy
3. Meta Weights
Lookahead Strategy该方法对场景间的关系进行了显式建模,但是这种根据损失函数的变化计算场景相关系数的策略存在着训练不稳定、波动较大的现象,无法像Gradient Regulation这一方法对场景相似度进行求解。
因此,我们引入了场景间的相关性系数矩阵(meta weights),结合前两种方法对该问题进行如下建模,通过场景的数据对其与其他场景的相关性系数进行更新,同时基于该参数对全局的参数模型进行优化。针对这种典型的两层优化问题,我们基于MAML[14]方法进行求解,并将meta weights作为场景间的相似度。
我们以推荐渠道和展示形式(是否开屏)的多场景模型作为Base,对上述3种方法做了探索。为了提高训练效率,我们在设计 Stage 1 模型时做了以下优化:
我们对每个方法的GAUC进行了比较,实验效果如下表4所示。相较于人工规则,基于梯度的场景聚合方法都能带来效果的明显提升,表明损失函数梯度能在一定程度上表示场景之间的相似性,并指导多场景进行聚合。
为了更全面的展现场景聚合对于模型预估效果的影响,我们选取Meta Weights进行分组数量的调优实验,具体的实验结果如下表5所示。可以发现:随着分组数的增大,GAUC提升也越大,此时各场景间的负迁移效应减弱;但分组超过一定数量时,场景间总体的相似度减小,GAUC呈下降趋势。
此外,我们对Meta Weigts方法中部分场景间的关系进行了可视化分析,分析结果如下图9所示。以场景作为坐标轴,图中的每个方格表示各场景间的相似度,颜色的深浅表示渠道间的相似程度大小。
从图中可以发现,以渠道和展示形式为粒度的细分场景下,该方法能够学习到不同场景间的相关性,例如A渠道下的信息流(s16)与其他场景的相关性较低,会将其作为独立的场景进行预估,而B渠道下的开屏展示(s9)与C渠道开屏展示(s8)相关性较高,会将其聚合为一个场景进行预估,同时该相似度矩阵不是对称的,这也说明各场景间相互的影响存在着差异。
3 总结与展望
通过多场景学习的探索和实践,我们深入挖掘了推荐模型在不同场景下的建模能力,并分别从场景知识迁移、场景聚合方向进行了尝试和优化,这些尝试提供了更好的理解和解释推荐模型对不同类型流量和场景的应对能力。然而,这只是多场景学习研究的开始,后续我们会探索并迭代以下方向:
- 更好的场景划分方式:当前多场景的划分主要还是依据渠道(渠道*展示形态)作为流量的划分方式,未来会在媒体、展示位、媒体*时间等维度上进行更详细地探索;
- 端到端的流量聚合方式:在进行流量聚合时,使用了Two-Stage的策略进行聚合。然而,这种方式不能充分地利用流量数据中相关的信息。因此,需要探索端到端的流量场景聚合方案将更直接和有效地提高推荐模型的能力。
结合多场景学习,在未来的研究中将不断探索新的方法和技术,以提高推荐模型对不同场景和流量类型的建模能力,创造更好的用户体验以及商业价值。
4 作者简介
王驰、森杰、树立、文帅、尹华、肖雄等,均来自美团到家事业群/到家研发平台。
5 参考文献
- [1] STAR:Sheng, Xiang-Rong, et al. "One model to serve all: Star topology adaptive recommender for multi-domain ctr prediction." Proceedings of the 30th ACM International Conference on Information & Knowledge Management. 2021.
- [2] SAML:Chen, Yuting, et al. "Scenario-aware and Mutual-based approach for Multi-scenario Recommendation in E-Commerce." 2020 International Conference on Data Mining Workshops (ICDMW). IEEE, 2020.
- [3] ADIN:Jiang, Yuchen, et al. "Adaptive Domain Interest Network for Multi-domain Recommendation." Proceedings of the 31st ACM International Conference on Information & Knowledge Management. 2022.
- [4]SASS:Zhang, Yuanliang, et al. "Scenario-Adaptive and Self-Supervised Model for Multi-Scenario Personalized Recommendation." Proceedings of the 31st ACM International Conference on Information & Knowledge Management. 2022.
- [5] Squeeze-and-Excitation:Hu, Jie, Li Shen, and Gang Sun. "Squeeze-and-excitation networks." Proceedings of the IEEE conference on computer vision and pattern recognition. 2018.
- [6] 美团外卖推荐情境化智能流量分发的实践与探索
- [7] PaLM:ai.googleblog.com/2022/04/pat…
- [8] GLaM:proceedings.mlr.press/v162/du22c.…
- [9] 单专家选择器:arxiv.org/abs/2106.03…
- [10] HOA:proceedings.mlr.press/v119/standl…
- [11] Gradient Affinity:proceedings.neurips.cc/paper/2021/…
- [12] SRDML:dl.acm.org/doi/abs/10.…
- [13] Auto-Lambda:arxiv.org/abs/2202.03…
- [14] MAML:arxiv.org/abs/1703.03…
来源:juejin.cn/post/7278597227785551883
终于找到一个比较好用的前端国际化方案了
在开发Vue/React应用,一直对现有的多语言方案不是很满足,现在终于出了一个比较满意好用的了。
本节以标准的Nodejs
应用程序为例,简要介绍VoerkaI18n
国际化框架的基本使用。
vue
或react
应用的使用流程也基本相同,可以参考Vue集成和React集成。
myapp
|--package.json
|--index.js
在本项目的所有支持的源码文件中均可以使用t
函数对要翻译的文本进行包装,简单而粗暴。
// index.js
console.log(t("中华人民共和国万岁"))
console.log(t("中华人民共和国成立于{}",1949))
t
翻译函数是从myapp/languages/index.js
文件导出的翻译函数,但是现在myapp/languages
还不存在,后续会使用工具自动生成。voerkai18n
后续会使用正则表达式对提取要翻译的文本。
第一步:安装命令行工具
安装@voerkai18n/cli
到全局。
> npm install -g @voerkai18n/cli
> yarn global add @voerkai18n/cli
> pnpm add -g @voerkai18/cli
第二步:初始化工程
在工程目录中运行voerkai18n init
命令进行初始化。
> voerkai18n init
上述命令会在当前工程目录下创建languages/settings.json
文件。如果您的源代码在src
子文件夹中,则会创建在src/languages/settings.json
settings.json
内容如下:
{
"languages": [
{
"name": "zh",
"title": "zh"
},
{
"name": "en",
"title": "en"
}
],
"defaultLanguage": "zh",
"activeLanguage": "zh",
"namespaces": {}
}
上述命令代表了:
- 本项目拟支持
中文
和英文
两种语言。 - 默认语言是
中文
(即在源代码中直接使用中文) - 激活语言是
中文
(代表当前生效的语言)
注意:
- 可以修改该文件来配置支持的语言、默认语言、激活语言等。可支持的语言可参阅语言代码列表。
voerkai18n init
是可选的,voerkai18n extract
也可以实现相同的功能。- 一般情况下,您可以手工修改
settings.json
,如定义名称空间。 voerkai18n init
仅仅是创建languages
文件,并且生成settings.json
,因此您也可以自己手工创建。- 针对
js/typescript
或react/vue
等不同的应用,voerkai18n init
可以通过不同的参数来配置生成ts
文件或js
文件。 - 更多的
voerkai18n init
命令的使用请查阅这里
第三步:标识翻译内容
接下来在源码文件中,将所有需要翻译的内容使用t
翻译函数进行包装,例如下:
import { t } from "./languages"
// 不含插值变量
t("中华人民共和国")
// 位置插值变量
t("中华人民共和国{}","万岁")
t("中华人民共和国成立于{}年,首都{}",1949,"北京")
t
翻译函数只是一个普通函数,您需要为之提供执行环境,关于t
翻译函数的更多用法见这里
第四步:提取文本
接下来我们使用voerkai18n extract
命令来自动扫描工程源码文件中的需要的翻译的文本信息。 voerkai18n extract
命令会使用正则表达式来提取t("提取文本")
包装的文本。
myapp>voerkai18n extract
执行voerkai18n extract
命令后,就会在myapp/languages
通过生成translates/default.json
、settings.json
等相关文件。
- translates/default.json : 该文件就是从当前工程扫描提取出来的需要进行翻译的文本信息。所有需要翻译的文本内容均会收集到该文件中。
- settings.json: 语言环境的基本配置信息,包含支持的语言、默认语言、激活语言等信息。
最后文件结构如下:
myapp
|-- languages
|-- settings.json // 语言配置文件
|-- translates // 此文件夹是所有需要翻译的内容
|-- default.json // 默认名称空间内容
|-- package.json
|-- index.js
如果略过第一步中的voerkai18n init
,也可以使用以下命令来为创建和更新settings.json
myapp>voerkai18n extract -D -lngs zh en de jp -d zh -a zh
以上命令代表:
- 扫描当前文件夹下所有源码文件,默认是
js
、jsx
、html
、vue
文件类型。 - 支持
zh
、en
、de
、jp
四种语言 - 默认语言是中文。(指在源码文件中我们直接使用中文即可)
- 激活语言是中文(即默认切换到中文)
-D
代表显示扫描调试信息,可以显示从哪些文件提供哪些文本
第五步:人工翻译
接下来就可以分别对language/translates
文件夹下的所有JSON
文件进行翻译了。每个JSON
文件大概如下:
{
"中华人民共和国万岁":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"] // 记录了该信息是从哪几个文件中提取的
},
"中华人民共和国成立于{}":{
"en":"<在此编写对应的英文翻译内容>",
"de":"<在此编写对应的德文翻译内容>",
"jp":"<在此编写对应的日文翻译内容>",
"$files":["index.js"]
}
}
我们只需要修改该文件翻译对应的语言即可。
重点:如果翻译期间对源文件进行了修改,则只需要重新执行一下voerkai18n extract
命令,该命令会进行以下操作:
- 如果文本内容在源代码中已经删除了,则会自动从翻译清单中删除。
- 如果文本内容在源代码中已修改了,则会视为新增加的内容。
- 如果文本内容已经翻译了一部份了,则会保留已翻译的内容。
总之,反复执行voerkai18n extract
命令是安全的,不会导致进行了一半的翻译内容丢失,可以放心执行。
第六步:自动翻译
voerkai18n
支持通过voerkai18n translate
命令来实现调用在线翻译服务进行自动翻译。
>voerkai18n translate --appkey <在百度翻译上申请的密钥> --appid <在百度翻译上申请的appid>
在项目文件夹下执行上面的语句,将会自动调用百度的在线翻译API
进行翻译,以现在的翻译水平而言,您只需要进行少量的微调即可。关于voerkai18n translate
命令的使用请查阅后续介绍。
第七步:编译语言包
当我们完成myapp/languages/translates
下的所有JSON语言文件
的翻译后(如果配置了名称空间后,每一个名称空间会对应生成一个文件,详见后续名称空间
介绍),接下来需要对翻译后的文件进行编译。
myapp> voerkai18n compile
compile
命令根据myapp/languages/translates/*.json
和myapp/languages/settings.json
文件编译生成以下文件:
|-- languages
|-- settings.json // 语言配置文件
|-- idMap.js // 文本信息id映射表
|-- index.js // 包含该应用作用域下的翻译函数等
|-- storage.js
|-- zh.js // 语言包
|-- en.js
|-- jp.js
|-- de.js
|-- formatters // 自定义扩展格式化器
|-- zh.js
|-- en.js
|-- jp.js
|-- de.js
|-- translates // 此文件夹包含了所有需要翻译的内容
|-- default.json
|-- package.json
|-- index.js
第八步:导入翻译函数
第一步中我们在源文件中直接使用了t
翻译函数包装要翻译的文本信息,该t
翻译函数就是在编译环节自动生成并声明在myapp/languages/index.js
中的。
import { t } from "./languages"
因此,我们需要在需要进行翻译时导入该函数即可。
但是如果源码文件很多,重次重复导入t
函数也是比较麻烦的,所以我们也提供了一个babel/vite
等插件来自动导入t
函数,可以根据使用场景进行选择。
第九步:切换语言
当需要切换语言时,可以通过调用change
方法来切换语言。
import { i18nScope } from "./languages"
// 切换到英文
await i18nScope.change("en")
// 或者VoerkaI18n是一个全局单例,可以直接访问
await VoerkaI18n.change("en")
i18nScope.change
与VoerkaI18n.change
两者是等价的。
一般可能也需要在语言切换后进行界面更新渲染,可以订阅事件来响应语言切换。
import { i18nScope } from "./languages"
// 切换到英文
i18nScope.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})
//
VoerkaI18n.on("change",(newLanguage)=>{
// 在此重新渲染界面
...
})
@voerkai18n/vue和@voerkai18n/react提供了相对应的插件和库来简化重新界面更新渲染。
第十步:语言包补丁
一般情况下,多语言的工程化过程就结束了,voerkai18n
在多语言实践考虑得更加人性化。有没有经常发现这样的情况,当项目上线后,才发现:
- 翻译有误
- 客户对某些用语有个人喜好,要求你更改。
- 临时要增加支持一种语言
一般碰到这种情况,只好重新打包构建工程,重新发布,整个过程繁琐而麻烦。 现在voerkai18n
针对此问题提供了完美的解决方案,可以通过服务器来为应用打语言包补丁
和动态增加语言
支持,而不需要重新打包应用和修改应用。
方法如下:
- 注册一个默认的语言包加载器函数,用来从服务器加载语言包文件。
import { i18nScope } from "./languages"
i18nScope.registerDefaultLoader(async (language,scope)=>{
return await (await fetch(`/languages/${scope.id}/${language}.json`)).json()
})
- 将语言包补丁文件保存在Web服务器上指定的位置
/languages/<应用名称>/<语言名称>.json
即可。 - 当应用启动后会自动从服务器上加载语言补丁包合并,从而实现动为语言包打补丁的功能。
- 利用该特性也可以实现动态增加临时支持一种语言的功能
来源:juejin.cn/post/7275944565885485116
前段时间面试了一些人,有这些槽点跟大家说说
大家好,我是拭心。
前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。
简历书写和自我介绍
- 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备
- 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了
- 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求
- 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色
- 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多
- 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!
- 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)
- 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点
- 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说
- 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到
- 实在不知道怎么介绍,翻上去看第 4 点和第 5 点
- 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略
- 你可以这样审视自己的简历和自我介绍:
a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点
b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考
c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点
面试问题
- 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质
- 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任
- 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质
项目经历
项目经历就是我们过往做过的项目。
项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。
有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?
大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。
在项目经历上,面试者常见的问题有这些:
- 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)
- 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)
- 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)
出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。
在看面试者的项目经历时,面试官主要关注这三点:
1. 之前做的项目有没有难度
2. 项目经验和当前岗位需要的是否匹配
3. 经过这些项目,这个人的能力有哪些成长
因此,我们在日常工作和准备面试时,可以这样做:
- 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处
- 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强
- 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点
- 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长
- 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案
- 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分
- 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验
技能知识点
技能知识点就是我们掌握的编程语言、技术框架和工具。
相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。
在技能知识点方面,面试者常见的问题有这些:
- 不胜任岗位:基础不扎实,不熟悉常用库的原理
- 技术不对口:没有岗位需要的领域技术
- 技术过剩:能力远远超出岗位要求
第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。
第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。
第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。
在我面试的人里,通过面试的都有这些特点:
- 技术扎实:不仅仅基础好,还有深度
- 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事
有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?
软素质
这里的「软素质」指面试时考察的、技术以外的点。
程序员的日常工作里,除了写代码还需要做这些事:
- 理解业务的重点和不同需求的核心点,和其他同事协作完成
- 从技术角度,对需求提出自己的思考和建议,反馈给其他人
- 负责某个具体的业务/方向,成为这个方面所有问题的处理者
因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:
- 理解能力和沟通表达能力
- 业务能力
- 稳定性
第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。
第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。
业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。
遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。
第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。
针对以上这三点,我们可以这样做:
- 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张
- 回答问题时有逻辑条理,可以采用类似总分总的策略
- 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法
- 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标
总结
好了,这就是我前段时间面试的感悟和吐槽。
总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。
来源:juejin.cn/post/7261604248319918136
使用Tauri快速搭建桌面项目
什么是 Tauri
Tauri 是一个跨平台的 GUI
框架,与 Electron
的理念相似。Tauri 的前端部分同样基于 Web 技术,但它的后端则采用了 Rust
语言。Tauri 可以创建体积更小、运行更快且更加安全的跨平台桌面应用。
与 Electron
不同,Tauri 并没有内置 Chromium
,因此打包后的应用体积要比 Electron
小很多,启动速度更快,内存和 CPU 占用率也更低。
然而,由于没有内置 Chromium
,Tauri 使用系统原生的 WebView 来渲染网页,这可能导致不同系统之间的页面表现存在差异。同时,Tauri 的后端需要使用 Rust
进行开发,这对前端开发人员来说可能会有一定的上手成本。
好在 Tauri 已经为我们封装了大部分 API,即使不懂 Rust
,也可以开发出一款简单的应用。
预先准备
我们以 macOS 为例:
1. 首先安装 Xcode 命令行工具
在终端中执行以下命令:
xcode-select --install
如果已经安装过 Xcode 命令行工具,则可以直接进行下一步。
2. 安装 Rust
在 macOS 上安装 Rust,请打开终端并输入以下命令:
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
安装成功后,终端将显示以下内容:
Rust is installed now. Great!
请确保重新启动终端以使更改生效。
快速开始
创建项目
Tauri 官方提供了多种项目模板,可以快速搭建项目:
# pnpm
pnpm create tauri-app
# npm
npm create tauri-app
# yarn
yarn create tauri-app
按照提示选择自己喜欢的模板。
这里我们选择 react
开发前端页面。
一路回车后,打开项目文件夹。执行安装依赖命令:pnpm i
。依赖安装完成后,执行 pnpm tauri dev
命令启动项目。这时便会启动一个应用,如下图所示:
开发
Tauri 的开发非常容易上手,我们先来看下项目文件结构:
是不是和 Vite 的目录结构一样?
没错,这就是一个常规的 Vite 目录结构,唯一的区别是增加了一个 src-tauri
文件夹,这里面是 Rust
部分的代码,也就是后端代码。
打包
首先,我们需要修改默认的包标识符,位置在 src-tauri > tauri.conf.json > tauri > bundle > identifier
。
这里我们随便填写一个标识符 com.example.app
,保存,然后执行命令:pnpm tauri build
就可以正常打包了。
tauri.conf.json
文件是我们的应用配置文件,包含了应用的基本信息。
打包完成后,就可以在 tauri-app/src-tauri/target/release/bundle
目录下找到我们的应用。
现在我们只构建了 macOS 下的应用。
打开之后就可以看到我们的应用了。
参考文档
来源:juejin.cn/post/7388842078798823433
DDD项目落地之充血模型实践 | 京东云技术团队
背景:
充血模型是DDD分层架构中实体设计的一种方案,可以使关注点聚焦于业务实现,可有效提升开发效率、提升可维护性;
1、DDD项目落地整体调用关系
调用关系图中的Entity为实体,从进入领域服务(Domin)时开始使用,直到最后返回。
2、实体设计
充血模型是实体设计的一种方法,简单来说,就是一种带有具体行为方法和聚合关联关系的特殊实体;
关于实体设计,需要明白的关键词为:领域服务->聚合->聚合根->实体->贫血模型->充血模型
聚合与聚合根:
聚合是一种关联关系,而聚合根就是这个关系成立的基础,没有聚合根,这个聚合关系就无法成立;
举个例子,存在3个实体:用户、用户组、用户组关联关系,这3个实体形成的关联关系就是聚合,而用户实体就是这个聚合中的聚合根;
实体:
定义在领域层,是领域层的重要元素,从领域划分到工程实践落地,都应该围绕实体进行,DDD中的实体和数据库表不只是1对1关系,可能是1对多或者仅为内存中的对象;
贫血模型:
实体不带有任何行为方法,也不带有聚合关联关系,作用基本相当于值对象(ValueObject),仅作为值传递的对象,和传统三层项目架构中的实体具有相同作用,不建议使用。补充说明:一般我们使用的DTO就可以被当做是值对象
充血模型:
实体中带有具有行为方法和聚合关联关系,行为方法是说create、save、delete等封装了一类可以指代行为的方法,比如在用户实体对象中具有用户组实体的引用,这样当我们需要操作用户组时,只通过用户实体进行操作就可以。
工程实践中,建议采用充血模型,好处是隐藏胶水代码,提升代码可读性,使关注点聚焦于业务实现。
充血模型在实践中的问题:
行为代码量过多,导致实体内部臃肿膨胀,难以阅读,难以维护,对于这种问题,我们需要根据实体行为的代码量多少来采取不同的解决方案。
解决方案:
场景1:行为不会导致实体臃肿的情况下,在实体中完成行为定义
public CooperateServicePackageConfig save() {
// 直接调用基础设施层进行保存
cooperateServicePackageConfigRepository.save(this);
return this;
}
场景2:行为导致实体臃肿的情况下,采用外部定义行为的方式,核心思想是借助其他类实现行为代码定义,将臃肿代码外移,保留干净的实体行为:
1)创建工具类,将某个实体中的行为定义其中,实体负责调用该工具类
public CooperateServicePackageConfig save() {
// 将处理过程放在工具类中
ServicePackageSaveUtils.save(this);
return this;
}
2)创建新实体,将该实体的使用场景明确至某个细分行为,比如一个聚合根(ExampleEntity)的保存可能涉及到5个实体的保存,那么我们定义一个ExampleSaveEntity实体,专门用来处理该聚合下的保存行为
实践经验:
1、关于spring bean注入:充血模型在实体中使用静态注入方法实现。例:
private LabelInfoRepository labelInfoRepository = ApplicationContextUtils.getBean(LabelInfoRepository.class);
2、充血模型的实体序列化,排除非必要属性,在一些redis对象缓存时可能会用到。例:
// 使用注解排除序列化属性
@Getter(AccessLevel.NONE)
private LabelInfoRepository labelInfoRepository = ApplicationContextUtils.getBean(LabelInfoRepository.class);
// 使用注解排除序列化属性
@JSONField(serialize = false)
private ServicePackageConfig servicePackageConfig;
// 使用注解排除序列化 get 方法
@Transient
@JSONField(serialize = false)
public static CooperateServicePackageRepositoryQuery getAllCodeQuery(Long contractId) {
CooperateServicePackageRepositoryQuery repositoryQuery = new CooperateServicePackageRepositoryQuery();
repositoryQuery.setContractIds(com.google.common.collect.Lists.newArrayList(contractId));
repositoryQuery.setCode(RightsPlatformConstants.CODE_ALL);
return repositoryQuery;
}
3、利用Set方法建立聚合绑定关系。例:
public void setServiceSkuInfos(List<ServiceSkuInfo> serviceSkuInfos) {
if (CollectionUtils.isEmpty(serviceSkuInfos))
{
return;
}
this.serviceSkuInfos = serviceSkuInfos;
List<String> allSkuNoSet = serviceSkuInfos
.stream()
.map(one -> one.getSkuNo())
.collect(Collectors.toList());
String skuJoinStr = Joiner.on(GlobalConstant.SPLIT_CHAR).join(allSkuNoSet);
this.setSkuNoSet(skuJoinStr);}
作者:京东健康 张君毅
来源:京东云开发者社区
来源:juejin.cn/post/7264235181778190373
学会Grid之后,我觉得再也没有我搞不定的布局了
说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局
、双飞翼布局
等非常耳熟的名词;
为了实现这些布局我们有很多种实现方案,例如:table布局
、float布局
、定位布局
等,当然现在比较流行的肯定是flex布局
;
flex布局
属于弹性布局,所谓弹性也可以理解为响应式布局
,而同为响应式布局的还有Grid布局
;
Grid布局
是一种二维布局,可以理解为flex布局
的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;
本篇不会过多介绍
grid
的基础内容,更多的是一些布局的实现方案和一些小技巧;
常见布局
所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局
、双飞翼布局
这种名词我个人觉得不用太过于去在意;
因为这类布局最后的解释都会变成几行几列
,内容在哪一行哪一列,而这些就非常直观的对标了grid
的特性;
接下来我们来一起看看一些非常常见的布局,并且用grid
来实现;
1. 顶部 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr;
height: 100vh;
}
.header {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.header,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
</body>
</html>
2. 顶部 + 内容 + 底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr 60px;
height: 100vh;
}
.header {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.footer {
background-color: #039BE5;
}
.header,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>
这里示例和上面的示例唯一的区别就是多了一个
footer
,但是我们可以看到代码并没有多少变化,这就是grid
的强大之处;
可以看
码上掘金
的效果,这里的内容区域是单独滚动的,从而实现了header
和footer
固定,内容区域滚动的效果;
实现这个效果也非常简单,只需要在
content
上加上overflow: auto
即可;
3. 左侧 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.left {
background-color: #039BE5;
}
.content {
background-color: #4FC3F7;
}
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>
这个示例效果其实和第一个是类似的,只不过是把
grid-template-rows
换成了grid-template-columns
,这里就不提供码上掘金
的示例了;
4. 顶部 + 左侧 + 内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-rows: 60px 1fr;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.header {
grid-column: 1 / 3;
background-color: #039BE5;
}
.left {
background-color: #4FC3F7;
}
.content {
background-color: #99CCFF;
}
.header,
.left,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
</body>
</html>
这个示例不同点在于
header
占据了两列,这里我们可以使用grid-column
来实现,grid-column
的值是start / end
,例如:1 / 3
表示从第一列到第三列;
如果确定这一列是占满整行的,那么我们可以使用
1 / -1
来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧
的布局,那么header
就不需要修改了;
5. 顶部 + 左侧 + 内容 + 底部
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header header"
"left content"
"left footer";
grid-template-rows: 60px 1fr 60px;
grid-template-columns: 240px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.left {
grid-area: left;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.footer {
grid-area: footer;
background-color: #6699CC;
}
.header,
.left,
.content,
.footer {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="left">Left</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</body>
</html>
这个示例的小技巧是使用了
grid-template-areas
,使用这个属性可以让我们通过代码来直观的看到布局的样式;
这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:
"header header"
表示第一行的两列都是header
,这里的header
是我们自己定义的,可以是任意值;
定义好了之后就可以在对应的元素上使用
grid-area
来指定对应的区域,这里的值就是我们在grid-template-areas
中定义的值;
在
码上掘金
中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto
即可;
响应式布局
响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;
这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;
1. 基础布局实现
移动端布局
以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是
header
、navigation
、content
;
注:这里不是要
100%
还原掘金的页面,只是为了演示grid
布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header"
"navigation"
"content";
grid-template-rows: 60px 48px 1fr;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
</body>
</html>
iPad布局
这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下
grid-template-rows
和grid-template-columns
的值即可;
由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的
css
代码,只保留需要修改的代码;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.right {
display: none;
background-color: #6699CC;
}
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header"
"navigation navigation"
"content right";
grid-template-columns: 1fr 260px;
}
.right {
grid-area: right;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
PC端布局
和上面处理方式相同,由于
Navigation
移动到了左侧,所以还要额外的修改一下grid-template-areas
的值;
这里就可以体现
grid
的强大之处了,我们可以简单的修改grid-template-areas
就可以实现一个完全不同的布局,而且代码量非常少;
为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用
.
来实现,这里的.
表示一个空白区域;
由于内容的宽度基本上是固定的,所以留白区域简单的使用
1fr
进行占位即可,这样就可以平均的分配剩余的空间;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 1fr;
}
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
完善一些细节
最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用
column-gap
和一个空的区域进行占位来实现的;
这里的
column-gap
表示列与列之间的间距,值可以是px
、em
、rem
等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr
;
空区域进行占位留间距其实我并不推荐,这里只是演示
grid
布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin
来实现;
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
}
body {
display: grid;
grid-template-areas:
"header header header"
"navigation navigation navigation"
". . ."
". content .";
grid-template-columns: 1fr minmax(0, 720px) 1fr;
grid-template-rows: 60px 48px 10px 1fr;
column-gap: 10px;
height: 100vh;
}
.header {
grid-area: header;
background-color: #039BE5;
}
.navigation {
grid-area: navigation;
background-color: #4FC3F7;
}
.content {
grid-area: content;
background-color: #99CCFF;
}
.right {
display: none;
background-color: #6699CC;
}
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header header header"
"navigation navigation navigation navigation"
". . . ."
". content right .";
grid-template-columns: 1fr minmax(0, 720px) 260px 1fr;
}
.right {
grid-area: right;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
@media (min-width: 1220px) {
body {
grid-template-areas:
"header header header header header"
". . . . ."
". navigation content right .";
grid-template-columns: 1fr 180px minmax(0, 720px) 260px 1fr;
grid-template-rows: 60px 10px 1fr;
}
}
.header,
.navigation,
.content {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
</style>
</head>
<body>
<div class="header">Header</div>
<div class="navigation">Navigation</div>
<div class="content">Content</div>
<div class="right">Right</div>
</body>
</html>
简单复刻版
以
码上掘金
上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;
异型布局
异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid
是如何实现的;
1. 照片墙
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html, body {
margin: 0;
padding: 0;
background: #f2f3f5;
overflow: auto;
}
body {
display: grid;
grid-template-columns: repeat(12, 100px);
grid-auto-rows: 100px;
place-content: center;
gap: 6px;
height: 100vh;
}
.photo-item {
width: 200px;
height: 200px;
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}
</style>
</head>
<body>
</body>
<script>
function randomColor() {
return '#' + Math.random().toString(16).substr(-6);
}
let row = 1;
let col = 1;
for (let i = 0; i < 28; i++) {
const div = document.createElement('div');
div.className = 'photo-item';
div.style.backgroundColor = randomColor();
div.style.gridRow = `${row} / ${row + 2}`;
div.style.gridColumn = `${col} / ${col + 2}`;
document.body.appendChild(div);
col += 2;
if (col > 11) {
row += 1;
col = row % 2 === 0 ? 2 : 1;
}
}
</script>
</html>
这是一个非常简单的照片墙效果,如果不使用
grid
的话,我们大概率是会使用定位去实现这个效果,但是换成grid
的话就非常简单了;
而且代码量是非常少的,这里就不提供
码上掘金
的 demo 了,感兴趣的同学可以将代码复制下来自行查看效果;
2. 漫画效果
在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用
grid
的话就非常简单了;
可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用
z-index
来实现,这里的z-index
值越大,元素就越靠前;
而且气泡文字效果也是通过
grid
来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;
3. 画报效果
在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;
在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用
grid
的话就会简单很多;
我这里将页面划分为
12 * 12
区域的网格,然后依次对不同的元素进行单独排列和样式的设置;
流式布局
流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;
但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;
通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))
这种;
直接看效果:
这里有两个关键字,一个是
auto-fit
,还有一个是auto-fill
,在行为上它们是相同的,不同的是它们在网格创建的不同,
就像上面图中看到的一样,使用
auto-fit
会将空的网格进行折叠,可以看到他们的结束colum
的数字都是6
;
像我们上面的实例中不会出现这个问题,因为我们使用了响应式单位
fr
,只有使用固定单位才会出现这个现象;
感兴趣的同学可以将
minmax(200px, 1fr)
换成200px
尝试;
对比 Flex 布局
在我上面介绍了这么多的布局场景和案例,其实可以很明显的发现一件事,那就是我使用grid
进行的布局基本上都是大框架;
当然上面也有一些布局使用flex
也是可以实现的,但是我们再换个思路,除了flex
可以做到上面的一些布局,float
布局、table
布局、定位布局其实也都能实现;
不同的是float
布局、table
布局、定位布局基本上都是一些hack
的方案,就拿table
布局来说,table
本身就是一个html
标签,作用就是用来绘制表格,被拿来当做布局的一种方案也是迫不得已;
而web布局
发展到现在的我们有了正儿八经可以布局的方案flex
,为什么又要出一个grid
呢?
grid
的出现绝对不是用来替代flex
的,在我上面的实现的一些布局案例中,也可以看到我还会使用flex
;
我个人理解的是使用grid
进行主体的大框架的搭建,flex
作为一些小组件的布局控制,两者搭配使用;
flex
能实现一些grid
不好实现的布局,同样grid
也可以实现flex
实现困难的布局;
本身它们的定位就不痛,flex
作为一维布局的首选,grid
定位就是比flex
高一个维度,它的定位是二维布局,所以他们之间没有必要进行对比,合理使用就好;
总结
上面介绍的这么多基于grid
布局实现的布局方案,足以看出grid
布局的强大;
grid
布局的体系非常庞大,本文只是梳理出一些常见的布局场景,通过grid
布局去实现这些布局,来体会grid
带来的便利;
可能需要完全理解我上面的全部示例需要对grid
有一定的了解才可以,但是都看到这里了,不妨去深挖一下;
grid
布局作为一项强大的布局技术,有望在未来继续发展,除了我上面说到的布局,grid
还有很多小技巧来实现非常多的布局场景;
碍于我的见识和文笔的限制,我这次介绍grid
肯定是有很多不足的,但是还是希望这篇文章能为你对于布局相关能有新的认识;
来源:juejin.cn/post/7310423470546354239
域名还能绑定动态IP?真是又涨见识了,再也不用购买固定IP了!赶快收藏
大家好,我是冰河~~
一般家庭网络的公网IP都是不固定的,而我又想通过域名来访问自己服务器上的应用,也就是说:需要通过将域名绑定到动态IP上来实现这个需求。于是乎,我开始探索实现的技术方案。
通过在网上查阅一系列的资料后,发现阿里云可以做到实现动态域名解析DDNS。于是乎,一顿操作下来,我实现了域名绑定动态IP。这里,我们以Python为例实现。
小伙伴们注意啦:Java版源码已提交到:github.com/binghe001/m…
好了,说干就干,我们开始吧,走起~~
阿里云DDNS前置条件
- 域名是在阿里云购买的
- 地址必须是公网地址,不然加了解析也没有用
通过阿里云提供的SDK,然后自己编写程序新增或者修改域名的解析,达到动态解析域名的目的;主要应用于pppoe拨号的环境,比如家里设置了服务器,但是外网地址经常变化的场景;再比如公司的pppoe网关,需要建立vpn的场景。
安装阿里云SDK
需要安装两个SDK库,一个是阿里云核心SDK库,一个是阿里云域名SDK库;
阿里云核心SDK库
pip install aliyun-python-sdk-core
阿里云域名SDK库
pip install aliyun-python-sdk-domain
阿里云DNSSDK库
pip install aliyun-python-sdk-alidns
设计思路
- 获取阿里云的accessKeyId和accessSecret
- 获取外网ip
- 判断外网ip是否与之前一致
- 外网ip不一致时,新增或者更新域名解析记录
实现方案
这里,我直接给出完整的Python代码,小伙伴们自行替换AccessKey和AccessSecret。
#!/usr/bin/env python
#coding=utf-8
# 加载核心SDK
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.acs_exception.exceptions import ClientException
from aliyunsdkcore.acs_exception.exceptions import ServerException
# 加载获取 、 新增、 更新、 删除接口
from aliyunsdkalidns.request.v20150109 import DescribeSubDomainRecordsRequest, AddDomainRecordRequest, UpdateDomainRecordRequest, DeleteDomainRecordRequest
# 加载内置模块
import json,urllib
# AccessKey 和 Secret 建议使用 RAM 子账户的 KEY 和 SECRET 增加安全性
ID = 'xxxxxxx'
SECRET = 'xxxxxx'
# 地区节点 可选地区取决于你的阿里云帐号等级,普通用户只有四个,分别是杭州、上海、深圳、河北,具体参考官网API
regionId = 'cn-hangzhou'
# 配置认证信息
client = AcsClient(ID, SECRET, regionId)
# 设置主域名
DomainName = 'binghe.com'
# 子域名列表 列表参数可根据实际需求增加或减少值
SubDomainList = ['a', 'b', 'c']
# 获取外网IP 三个地址返回的ip地址格式各不相同,3322 的是最纯净的格式, 备选1为 json格式 备选2 为curl方式获取 两个备选地址都需要对获取值作进一步处理才能使用
def getIp():
# 备选地址:1, http://pv.sohu.com/cityjson?ie=utf-8 2,curl -L tool.lu/ip
with urllib.request.urlopen('http://www.3322.org/dyndns/getip') as response:
html = response.read()
ip = str(html, encoding='utf-8').replace("\n", "")
return ip
# 查询记录
def getDomainInfo(SubDomain):
request = DescribeSubDomainRecordsRequest.DescribeSubDomainRecordsRequest()
request.set_accept_format('json')
# 设置要查询的记录类型为 A记录 官网支持A / CNAME / MX / AAAA / TXT / NS / SRV / CAA / URL隐性(显性)转发 如果有需要可将该值配置为参数传入
request.set_Type("A")
# 指定查记的域名 格式为 'test.binghe.com'
request.set_SubDomain(SubDomain)
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
# 将获取到的记录转换成json对象并返回
return json.loads(response)
# 新增记录 (默认都设置为A记录,通过配置set_Type可设置为其他记录)
def addDomainRecord(client,value,rr,domainname):
request = AddDomainRecordRequest.AddDomainRecordRequest()
request.set_accept_format('json')
# request.set_Priority('1') # MX 记录时的必选参数
request.set_TTL('600') # 可选值的范围取决于你的阿里云账户等级,免费版为 600 - 86400 单位为秒
request.set_Value(value) # 新增的 ip 地址
request.set_Type('A') # 记录类型
request.set_RR(rr) # 子域名名称
request.set_DomainName(domainname) #主域名
# 获取记录信息,返回信息中包含 TotalCount 字段,表示获取到的记录条数 0 表示没有记录, 其他数字为多少表示有多少条相同记录,正常有记录的值应该为1,如果值大于1则应该检查是不是重复添加了相同的记录
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
relsult = json.loads(response)
return relsult
# 更新记录
def updateDomainRecord(client,value,rr,record_id):
request = UpdateDomainRecordRequest.UpdateDomainRecordRequest()
request.set_accept_format('json')
# request.set_Priority('1')
request.set_TTL('600')
request.set_Value(value) # 新的ip地址
request.set_Type('A')
request.set_RR(rr)
request.set_RecordId(record_id) # 更新记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
response = client.do_action_with_exception(request)
response = str(response, encoding='utf-8')
return response
# 删除记录
def delDomainRecord(client,subdomain):
info = getDomainInfo(subdomain)
if info['TotalCount'] == 0:
print('没有相关的记录信息,删除失败!')
elif info["TotalCount"] == 1:
print('准备删除记录')
request = DeleteDomainRecordRequest.DeleteDomainRecordRequest()
request.set_accept_format('json')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
request.set_RecordId(record_id) # 删除记录需要指定 record_id ,该字段为记录的唯一标识,可以在获取方法的返回信息中得到该字段的值
result = client.do_action_with_exception(request)
print('删除成功,返回信息:')
print(result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("存在多个相同子域名解析记录值,请核查后再操作!")
# 有记录则更新,没有记录则新增
def setDomainRecord(client,value,rr,domainname):
info = getDomainInfo(rr + '.' + domainname)
if info['TotalCount'] == 0:
print('准备添加新记录')
add_result = addDomainRecord(client,value,rr,domainname)
print(add_result)
elif info["TotalCount"] == 1:
print('准备更新已有记录')
record_id = info["DomainRecords"]["Record"][0]["RecordId"]
cur_ip = getIp()
old_ip = info["DomainRecords"]["Record"][0]["Value"]
if cur_ip == old_ip:
print ("新ip与原ip相同,不更新!")
else:
update_result = updateDomainRecord(client,value,rr,record_id)
print('更新成功,返回信息:')
print(update_result)
else:
# 正常不应该有多条相同的记录,如果存在这种情况,应该手动去网站检查核实是否有操作失误
print("存在多个相同子域名解析记录值,请核查删除后再操作!")
IP = getIp()
# 循环子域名列表进行批量操作
for x in SubDomainList:
setDomainRecord(client,IP,x,DomainName)
# 删除记录测试
# delDomainRecord(client,'b.jsoner.com')
# 新增或更新记录测试
# setDomainRecord(client,'192.168.3.222','a',DomainName)
# 获取记录测试
# print (getDomainInfo(DomainName, 'y'))
# 批量获取记录测试
# for x in SubDomainList:
# print (getDomainInfo(DomainName, x))
# 获取外网ip地址测试
# print ('(' + getIp() + ')')
Python脚本的功能如下:
- 获取外网ip地址。
- 获取域名解析记录。
- 新增域名解析记录。
- 更新域名解析记录。
- 删除域名解析记录 (并不建议将该功能添加在实际脚本中)。
- 批量操作,如果记录不存在则添加记录,存在则更新记录。
另外,有几点需要特别说明:
- 建议不要将删除记录添加进实际使用的脚本当中。
- 相同记录是同一个子域名的多条记录,比如 test.binghe.com。
- 脚本并没有验证记录类型,所以同一子域名下的不同类型的记录也会认为是相同记录,比如:有两条记录分别是 test.binghe.com 的 A 记录 和 test.binghe.com 的 AAAA 记录,会被认为是两条相同的 test.binghe.com 记录.如果需要判定为不同的记录,小伙伴们可以根据上述Python脚本自行实现。
- 可以通过判断获取记录返回的 record_id 来实现精确匹配记录。
最后,可以将以上脚本保存为文件之后,通过定时任务,来实现定期自动更新ip地址。
来源:juejin.cn/post/7385106262009004095
我为什么选择Next.js+Supabase做全栈开发
作为一名前端工程师,选择合适的技术栈对项目的成功至关重要,我最近一个星期尝试了下这两个技术栈的组合,大概在一个星期就写了一个小 SAAS,总共 10 多个页面。在本文中,我将分享为什么我选择Next.js 14和Supabase作为全栈开发的首选组合,并通过最新的代码示例和比较数据,直观地展示这个选择带来的诸多优势。
Next.js 14: 现代React应用的革新框架
默认服务器组件的优势
Next.js 14默认使用服务器组件,这对于提升性能和开发体验至关重要。
例如,一个简单的服务器组件如下:
// app/page.js
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Home() {
const data = await getData()
return <div>Welcome to {data.name}div>
}
在这个例子中,Home
组件是一个异步的服务器组件,它可以直接进行数据获取,而无需使用useEffect或getServerSideProps。
App Router: 更强大的路由系统
Next.js 14采用了新的App Router,提供了更灵活和直观的路由方式:
app/
page.js // 对应路由 /
about/
page.js // 对应路由 /about
posts/
[id]/
page.js // 对应路由 /posts/1, /posts/2, 等
Server Actions: 无需API路由的表单处理
Next.js 14引入了Server Actions,允许我们直接在服务器上处理表单提交,无需单独的API路由:
// app/form.js
export default function Form() {
async function handleSubmit(formData) {
'use server'
// 在服务器上处理表单数据
const name = formData.get('name')
// ...处理逻辑
}
return (
<form action={handleSubmit}>
<input type="text" name="name" />
<button type="submit">Submitbutton>
form>
)
}
这个能力好用到哭,不用再写API路由了,直接在页面上处理表单提交。代码简单了不止一点点。
Supabase: 开源Firebase替代品的崛起
数据库即服务的便利性
Supabase提供了PostgreSQL数据库即服务,使用起来非常简单:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
// 插入数据
const { data, error } = await supabase
.from('users')
.insert({ name: 'John', email: 'john@example.com' })
实时功能的强大支持
Supabase的实时订阅功能让实现实时更新变得轻而易举:
import { useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
function RealtimeData() {
useEffect(() => {
const channel = supabase
.channel('*')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'users' }, payload => {
console.log('New user:', payload.new)
})
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [])
return <div>Listening for new users...div>
}
身份认证和授权的简化
Supabase内置的身份认证系统大大简化了用户管理:
const { data, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
})
Next.js 14 + Supabase: 完美的全栈开发组合
开发效率的显著提升
结合Next.js 14和Supabase,我们可以快速构建全功能的Web应用。以下是一个简单的例子,展示了如何在Next.js 14的服务器组件中使用Supabase:
// app/posts/page.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('YOUR_SUPABASE_URL', 'YOUR_SUPABASE_KEY')
export default async function Posts() {
const { data: posts } = await supabase.from('posts').select('*')
return (
<div>
{posts.map(post => (
<div key={post.id}>{post.title}div>
))}
div>
)
}
这个例子展示了Next.js 14服务器组件如何与Supabase无缝集成,直接在服务器端获取数据并渲染。
与其他技术栈的对比
为了更直观地展示Next.js 14+Supabase的优势,我们来看一个更新后的比较表格:
特性 | Next.js 14+Supabase | MERN Stack | Firebase | Django |
---|---|---|---|---|
默认服务器组件 | ✅ | ❌ | ❌ | N/A |
App Router | ✅ | ❌ | ❌ | ❌ |
Server Actions | ✅ | ❌ | ❌ | ✅ |
实时数据库 | ✅ | 需配置 | ✅ | 需配置 |
SQL支持 | ✅ (PostgreSQL) | ❌ (默认NoSQL) | ❌ (NoSQL) | ✅ |
身份认证 | ✅ | 需配置 | ✅ | ✅ |
学习曲线 | 中 | 中 | 低 | 高 |
全栈JavaScript | ✅ | ✅ | ✅ | ❌ |
开源 | ✅ | ✅ | ❌ | ✅ |
选型优势的直观感受
- 开发速度:使用Next.js 14+Supabase,你可以在几小时内搭建起一个包含用户认证、数据库操作和实时更新的全栈应用。
- 代码量减少:得益于Next.js 14的服务器组件和Supabase的简洁API,代码量可以减少40%-60%。
- 性能提升:通过Next.js 14的默认服务器组件和自动代码分割,页面加载速度可以提升40%-70%。
- 学习成本:虽然新概念(如服务器组件)需要一定学习时间,但整体学习曲线比传统全栈开发更平缓,2-3周即可上手。
- 维护简化:单一语言(TypeScript)贯穿全栈,加上Next.js的文件约定和Supabase的声明式API,大大减少了维护的复杂度。
- 可扩展性:Supabase基于PostgreSQL,为未来的扩展提供了更多可能性,而Next.js的渐进式框架特性也允许逐步采用高级功能。
一些想法
Next.js 14和Supabase是现代全栈开发的最佳选择,它们的结合提供了前所未有的开发体验和性能优势。如果你正在寻找一个全栈开发的新方向,不妨试试Next.js 14和Supabase,相信你会爱上这个组合。而且 supabase 学了也很划算,即便你想做 react native,Flutter,他都可以作为你坚实的后端。
来源:juejin.cn/post/7389925676520226825
大厂都在”偷偷“用语义化标签,你却还在div?
引言
在我们日常浏览网页的时候,通常会看到各种各样的内容,如文字、图片、视频等。这些内容背后都有一个共同的语言,那就是HTML(超文本标记语言)。HTML是构建网页的基础,它就像建筑物的框架,决定了网页的基本结构和布局。
然而,仅仅有结构是不够的。如果网页只是简单地用一些基础标签堆砌而成,那么浏览器、搜索引擎甚至我们自己在后期维护时,都会感到非常吃力。
这时候,HTML的语义化标签就显得尤为重要。语义化标签不仅能使网页结构更加清晰,还能帮助搜索引擎更好地理解和索引网页内容。
什么是HTML语义化标签?
HTML语义化标签,就是那些带有特定含义的标签,它们告诉浏览器和搜索引擎,每一部分内容是什么。这就好比是给每个内容部分都贴上了一个清晰的标签,让所有人都能明白这个部分是用来做什么的。
举个例子,假设你在看一本书,书的封面、目录、章节标题等都是明确标示出来的,这样你就能快速找到自己想看的部分。同样,HTML语义化标签也是为了让网页的内容更加明晰易懂。比如:
<header>
标签用来定义网页的头部内容,通常包含导航栏、Logo等信息;<nav>
标签专门用于定义导航链接,这样搜索引擎就能更好地理解网站的结构;<article>
标签用于定义独立的内容,比如一篇新闻文章或者博客帖子。- ……
通过使用这些语义化标签,不仅提高了网页的可读性和维护性,还能帮助搜索引擎更准确地抓取和排名内容,从而提升网站的SEO效果。
为什么使用语义化标签?
使用HTML语义化标签有很多好处,它们不仅能让代码更清晰,还能带来实际的效果和便利。
- 提高网页的可读性和结构化
- 语义化标签让HTML代码更加直观,其他开发者在阅读和维护代码时,可以快速理解每个部分的作用。这有助于团队合作和项目的长期维护。
- 有助于搜索引擎优化(SEO)
- 搜索引擎通过爬虫程序抓取网页内容,并根据网页的结构和内容进行索引。使用语义化标签可以帮助搜索引擎更好地理解网页的层次和重点内容,从而提升网站在搜索结果中的排名。
- 无障碍支持
- 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用
<nav>
标签可以让屏幕阅读器快速跳转到导航部分。 - 还记得浏览器内置的“沉浸阅读器”吗?它们也大多基于语义化标签提供服务。例如掘金的文章都是用
<article>
标签包裹的,所以你可以在掘金文章页面启用沉浸阅读器,而且精准的获取了文章的主体内容。
- 语义化标签对辅助技术(如屏幕阅读器)非常重要,它们可以帮助视障用户更好地理解网页内容。例如,使用
另外不得不说,目前苹果对语义化标签的使用是最炉火纯青的。怪不得都说苹果优雅,现在算是在前端上见识到了这个细节怪……
其实还有很多大厂都在使用,但都是偷偷地使用。它们没有全局使用语义化标签,而是在特定的关键位置使用语义化标签来 “谄媚” 一下搜索引擎或浏览器提供的无障碍功能。
所以我相信很多人还是非常支持div一把梭的,只要老板不限制,想怎么做就怎么做。不过如果你也能学习大厂,在漫天div下加一点语义化标签的小巧思,骗过搜索引擎和浏览器,这不是很香吗?
所以,本文着重介绍那些搜索引擎和浏览器有特别支持的语义化标签,搞定他们就搞定了一大半!
常用的语义化标签
搜索引擎钟爱的语义化标签
搜索引擎(如Google、Bing等)特别关注某些HTML语义化标签,因为这些标签能够帮助它们更好地理解网页的结构和内容,从而改进搜索结果的质量。
以下是一些被搜索引擎特别关注的语义化标签:
<header>
- 搜索引擎会识别
<header>
标签中的内容,通常包括页面的标题、导航链接等,有助于理解网页的整体结构和主要部分。
- 搜索引擎会识别
<nav>
<nav>
标签标示出导航链接区域,帮助搜索引擎理解网站的链接结构和页面之间的关系,有助于内部链接的优化。
<article>
<article>
标签表示独立的内容块,如新闻文章、博客帖子等。搜索引擎会特别关注这些标签,认为其包含主要的内容。
<footer>
<footer>
标签包含页脚内容,通常包括版权信息、联系信息等,搜索引擎会利用这些信息来补充网页的相关性数据。
<main>
<main>
标签标示出页面的主要内容区域,帮助搜索引擎更快地定位和抓取主要内容,而忽略导航栏、页脚等次要部分。
浏览器的无障碍功能
现代浏览器具备许多无障碍功能(accessibility features),这些功能可以帮助有特殊需求的用户更好地浏览网页。
以下是一些关键的无障碍功能:
- 屏幕阅读器支持
- 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,
<nav>
标签可以让用户快速跳转到导航部分,而<article>
标签则可以帮助用户找到主要的文章内容。
- 屏幕阅读器是一种软件工具,可以将网页内容转换为语音或盲文,帮助视障用户浏览网页。语义化标签可以极大地提升屏幕阅读器的效率和准确性。例如,
- 键盘导航
- 无障碍浏览器允许用户通过键盘进行导航,语义化标签如
<header>
、<nav>
、<main>
、<footer>
等,可以帮助键盘用户快速跳转到页面的不同部分,提高浏览效率。
<header>
<h1>网站标题</h1>
</header>
<nav>
<!-- 导航内容 -->
</nav>
<main>
<h2>主要内容标题</h2>
<p>这是主要内容区域。</p>
</main>
<footer>
<p>版权所有 © 2024 公司名称</p>
</footer>
- 无障碍浏览器允许用户通过键盘进行导航,语义化标签如
- 高对比度模式
- 一些浏览器提供高对比度模式,帮助视觉有障碍的用户更容易阅读内容。使用正确的语义化标签和良好的结构,可以确保在高对比度模式下内容的可读性和可访问性。
<section>
<h2>章节标题</h2>
<article>
<h3>文章标题</h3>
<p>文章内容...</p>
</article>
<aside>
<h3>附加内容</h3>
<p>例如广告或链接...</p>
</aside>
</section>
- ARIA(可访问性富互联网应用)标签
- 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,
aria-label
、aria-labelledby
等属性可以为非文本元素提供文本描述,帮助辅助技术更好地解释内容。
<button aria-label="关闭">X</button>
<div role="dialog" aria-labelledby="dialogTitle" aria-describedby="dialogDescription">
<h2 id="dialogTitle">对话框标题</h2>
<p id="dialogDescription">对话框内容描述。</p>
</div>
- 虽然ARIA标签不是HTML语义化标签的一部分,但它们可以补充HTML标签,提供更多的无障碍信息。例如,
语义化标签的实际应用
为了更好地理解语义化标签的使用方法,让我们通过一个具体的案例来展示它们的实际应用。
假设我们要创建一个简单的博客页面,包含标题、导航栏、文章内容、侧边栏和页脚。下面是一个示例代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>我的博客</title>
<style>
body { font-family: Arial, sans-serif; }
header, nav, article, aside, footer { margin: 20px; padding: 10px; border: 1px solid #ccc; }
nav ul { list-style-type: none; padding: 0; }
nav ul li { display: inline; margin-right: 10px; }
aside { float: right; width: 30%; }
article { float: left; width: 65%; }
</style>
</head>
<body>
<header>
<h1>我的博客</h1>
<nav>
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#about">关于我</a></li>
<li><a href="#contact">联系我</a></li>
</ul>
</nav>
</header>
<section>
<article>
<h2>文章标题</h2>
<p>这里是文章的正文内容。</p>
</article>
<aside>
<h2>侧边栏</h2>
<p>这里是一些附加内容,比如广告或链接。</p>
</aside>
</section>
<footer>
<p>版权所有 Dikkoo; 2024 我的博客</p>
</footer>
</body>
</html>
回顾一下
在这个案例中,我们使用了多个语义化标签来组织页面内容:
<header>
包含网站的标题和导航栏。<nav>
用于定义导航链接区域。<section>
用于分隔主要内容区域,包含文章和侧边栏。<article>
定义了独立的文章内容。<aside>
包含附加内容,如侧边栏。<footer>
包含页面的底部信息。
怎样合理运用语义化标签?
为了充分发挥HTML语义化标签的优势,以下是一些最佳实践建议:
- 规划页面结构,提前设计
- 在编写HTML之前,先绘制页面的结构图,明确各部分的功能和内容。根据设计选择合适的语义化标签,这样可以避免在编写过程中频繁修改结构。
- 保持代码简洁
- 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签和使用多余的标签。使用语义化标签替代大量的
<div>
和<span>
,使代码更加简洁和易读。
- 语义化标签旨在使代码更清晰,因此应尽量避免过度嵌套标签和使用多余的标签。使用语义化标签替代大量的
- 合理嵌套标签
- 语义化标签应按照其语义进行嵌套。例如,将
<nav>
放在<header>
内,表示导航是头部的一部分;将<section>
和<article>
合理地嵌套在一起,表示内容的层次结构。
- 语义化标签应按照其语义进行嵌套。例如,将
- 遵循HTML规范
- 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将
<header>
标签用在每个段落中,而应仅用于页面或章节的头部。
- 确保使用语义化标签时符合HTML规范,不要滥用标签。例如,不要将
来源:juejin.cn/post/7388056946121113637
虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑
今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。
2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然我的炒股成绩不是最出色的,但也超过了很多人。因为大多数股民都是亏损的,能够在股市长期盈利的人真的是凤毛麟角。
股市中普遍流传的七亏二平一赚的说法并不只是传闻,事实上,现实中的比例更加残酷,能够长期赚钱的人可能连10%都达不到。
接下来,我想谈谈我的炒股经历和心路历程,与大家分享一下我的内心体验,为那些有意向或正在炒股的朋友提供一些参考。希望劝退大家,能救一个是一个!
本文倒叙描述,先聊聊最后的疯狂和偏执!
不甘失败,疯狂上杠杆
股市有上涨就有下跌,在我卖出以后,股市继续疯涨了很多。当时长春高新,我是四百一股买入,六百一股就卖出了,只赚了2万。可是在我卖出去的两个月以后,它最高涨到了一千。相当于我本可以赚六万,结果赚了两万就跑了。
我简直想把大腿拍烂了,这严重的影响了我的认知。我开始坚信,这只股票和公司就是好的,非常牛,是我始乱终弃,我不应该早早抛弃人家。 除了悔恨,我还在期盼它下跌,好让我再次抄底,重新买入,让我有重新上车的机会!
终于这只股票后来跌了10%,我觉得跌的差不多了,于是我开始抄底买入!抄底买入的价格在900一股(复权前)。
没想到,这次抄底是我噩梦的开始。我想抄他的底,他想抄我的家!
这张图,完美的诠释了我的抄底过程。地板底下还有底,深不见底,一直到我不再敢抄底为止。一直抄到,我天天睡不着觉!
当时我九百多一股开始抄底买入,在此之前我都是100股,后来我开始投入更多的资金在这只股票上。当时的我 定下了规矩,鸡蛋不能放在一个篮子里;不能重仓一只股票,要分散投资;这些道理我都明白,但是真到了节骨眼上,我不想输,我想一把赢回来,我要抄底,摊平我的成本。
正所谓:高位加仓,一把亏光。之前我赚的两万块钱,早就因为高位加仓,亏回去了。可是我不甘心输,我想赢回来。当时意识不到也不愿意承认:这就是赌徒心理。
后来这只股票,从1000,跌倒了600,回调了40%。而我已经被深深的套牢。当时我盈利时,只买了1股。等我被套牢时,持有了9股。 按照1000一股,就是九十万。按照600一股,就是54万。
我刚毕业,哪来的那么多钱!
我的钱,早就在800一股的时候,我就全投进去了,我认为800已经算是底了吧,没想到股价很快就击穿了800。
于是我开始跟好朋友借钱。一共借了10万,商量好借一年,还他利息。后来这10万块钱,也禁不住抄底,很快手里没钱了,股价还在暴跌。我已经忘记当时亏多少钱了,我当时已经不敢看账户了,也不敢细算亏了多少钱!
于是,我又开始从支付宝和招商银行借贷,借钱的利率是相当高的,年利息在6%以上。当时一共借了30万。但是股价还不见底,我开始焦虑的睡不着觉。
不光不见底,还在一直跌,我记得当时有一天,在跌了很多以后,股价跌停 -10%。当时的我已经全部资金都投进去了,一天亏了5万,我的小心脏真的要受不了了。跌的我要吐血! 同事说,那天看见我的脸色很差,握着鼠标手还在发抖!
跌成这样,我没有勇气打开账户…… 我不知道什么时候是个头,除了恐惧只有恐惧,每天活在恐惧之中。
我盘算了一下,当时最低点的我,亏了得有二十多万。从盈利六万,一下子到亏二十多万。只需要一个多月的时间。
我哪里经历过这些,投资以来,我都是顺风顺水的,基本没有亏过钱,从来都是挣钱,怎么会成这个样子。
当时的我,没空反思,我只希望,我要赚回来!我一定会赚回来,当时能借的支付宝和招行都已经借到最大额度了…… 我也没有什么办法了,只能躺平。
所以股价最低点的时候,基本都没有钱加仓。
侥幸反弹,但不忍心止盈
股价跌了四个月,这是我人生极其灰暗的四个月。后来因为种种原因,股价涨回来了,当时被传闻的事情不攻自破,公司用实际的业绩证明了自己。
股价开始慢慢回暖,后来开始凶猛的反弹,当时的��一直认为:股价暴跌时我吃的所有苦,所有委屈,我都要股市给我补回来!
后来这段时间,股价最高又回到了1000元一股(复权前)。最高点,我赚了二十多万,但是我不忍心止盈卖出。
我觉得还会继续涨,我还在畅想:公司达到,万亿市值。
我觉得自己当时真的 失了智了。
结婚买房,卖在最高点
这段时间,不光股市顺丰顺水,感情上也比较顺利,有了女朋友,现在是老婆了。从那时起,我开始反思自己的行为,我开始意识到,自己彻彻底底是一个赌徒。
因为已经回本了,也赚了一点钱,我开始不断的纠结要不要卖出,不再炒股了。
后来因为两件事,第一件是我姐姐因为家里要做小买卖,向我借钱。 当时的我,很纠结,我的钱都在股市里啊,借她钱就得卖股票啊,我有点心疼。奈何是亲姐,就借了。
后来我盘算着,不对劲。我还有贷款没还呢,一共三十万。我寻思,我从银行借钱收6%的利息,我借给别人钱,我一分利息收不到。 我TM 妥妥的冤大头啊。
不行,我要把贷款全部还上,我Tm亏大了,于是我逐渐卖股票。一卖出便不可收拾。
我开始担心,万一股价再跌回去,怎么办啊。我和女朋友结婚时,还要买房,到时候需要一大笔钱,万一要是被套住了,可怎么办啊!
在这这样的焦虑之下,我把股票全部都卖光了!
冥冥之中,自有天意。等我卖出之后的第二周,长春高新开启了下一轮暴跌,而这一轮暴跌之后,直至今日,再也没有翻身的机会。从股价1000元一股,直至今天 300元一股(复权前是300,当前是150元)。暴跌程度大达 75%以上!
全是侥幸
我觉得我是幸运的,如果我迟了那么一步!假如反应迟一周,我觉得就万劫不复。因为再次开启暴跌后,我又会开始赌徒心理。
我会想,我要把失去的,重新赢回来!我不能现在卖,我要赢回来。再加上之前抄底成功一次,我更加深信不疑!
于是我可能会从1000元,一路抄底到300元。如果真会如此,我只能倾家荡产!
不是每个人都有我这么幸运,在最高点,跑了出去。 雪球上之前有一个非常活泼的用户, 寒月霖枫,就是因为投资长春高新,从盈利150万,到亏光100万本金,还倒欠银行!
然而这一切,他的家人完全不知道,他又该如何面对家人,如何面对未来的人生。他想自杀,想过很多方式了结。感兴趣的朋友可以去 雪球搜搜这个 用户,寒月霖枫。
我觉得 他就是世界上 另一个自己。我和他完全类似的经历,除了我比他幸运一点。我因为结婚买房和被借钱,及时逃顶成功,否则我和他一样,一定会输得倾家荡产!
我觉得,自己就是一个赌狗!
然而,在成为赌狗之前,我是非常认真谨慎对待投资理财的!
极其谨慎的理财开局
一开始,我从微信理财通了解到基金,当时2019年,我刚毕业两年,手里有几万块钱,一直存在活期账户里。其中一个周末,我花时间研究了一下理财通,发现有一些债券基金非常不错。于是分几批买了几个债券基金,当时的我对于理财既谨慎又盲目。
谨慎的一面是:我只敢买债券基金,就是年利息在 5%上下的。像股票基金这种我是不敢买的。
盲目的一面是:我不知道债券基金也是风险很大的,一味的找利息最多的债券基金。
后来的我好像魔怔了,知道了理财这件事,隔三差五就看看收益,找找有没有利息更高的债券基金。直到有一天,我发现了一个指数基金,收益非常稳定。
是美股的指数基金,于是我买了1万块钱,庆幸的是,这只指数基金,三个月就赚了八百多,当时的我很高兴。那一刻,我第一次体会到:不劳而获真的让人非常快乐!
如饥似渴的学习投资技巧
经过一段时间的理财,我对于理财越来越熟悉。
胆子也越来越大,美股的指数基金赚了一点钱,我害怕亏回去,就立即卖了。卖了以后就一直在找其他指数基金,这时候我也在看国内 A股的指数基金,甚至行业主题的基金。
尝到了投资的甜头以后,我开始花更多的时间用来 找基。我开始从方方面面评估一只基金。
有一段时间,我特别自豪,我在一个周末,通过 天天基金网,找到了一个基金,这只基金和社保投资基金的持仓 吻合度非常高。当时的我思想非常朴素, 社保基金可是国家队,国家管理的基金一定非常强,非常专业,眼光自然差不了。这只基金和国家队吻合度如此高,自然也差不了。
于是和朋友们,推荐了这只基金。我们都买了这只基金,而后的一个月,这只基金涨势非常喜人,赚了很多钱,朋友们在群里也都感谢我,说我很厉害,投资眼光真高!
那一刻,我飘飘然……
我开始投入更多的时间用来理财。下班后,用来学习的时间也不学习了,开始慢慢的过度到学习投资理财。我开始不停地 找基。当时研究非常深入,我会把这只基金过往的持仓记录,包括公司都研究到。花费的时间也很多。
我也开始看各种财经分析师对于股市的分析,他们会分析大盘何时突破三千点,什么时候股市情绪会高昂起来,什么行业主题会热门,什么时候该卖出跑路了。
总之,投资理财,可以学习的东西多种多样!似乎比编程有趣多了。
换句话说:我上头了
非常荒谬的炒股开局
当时我还是非常谨慎地,一直在投资基金,包括 比较火爆的 中欧医疗创新C 基金,我当时也买了。当时葛兰的名气还很响亮呢。后来股市下行,医疗股票都在暴跌,葛兰的基金 就不行了,有句话调侃:家里有钱用不完,中欧医疗找葛兰。腰缠万贯没人分,易方达那有张坤。
由此可见,股市里难有常胜将军!
当时的我,进入股市,非常荒谬。有一天,前同事偷偷告诉我,他知道用友的内幕,让我下午开盘赶紧买,我忙追问,什么内幕,他说利润得翻五倍。 我寻思一下,看了一眼用友股票还在低位趴着,心动了。于是我中午就忙不迭的线上开户,然后下午急匆匆的买了 用友。 事后证明,利润不光没有翻五倍,还下降了。当然在这之前,我早就跑了,没赚着钱,也没咋亏钱。
当时的我,深信不疑这个假的小道消息,恨不得立即买上很多股票。害怕来不及上车……
自从开了户,便一发不可收拾,此时差2个月,快到2019年底!席卷全世界的病毒即将来袭
这段时间,股市涨势非常好,半导体基金涨得非常凶猛! 我因为初次进入股市,没有历史包袱,哪个股票是热点,我追哪个,胆子非常大。而且股市行情非常好,我更加相信,自己的炒股实力不凡!
换句话说:越来越上头,胆子越来越大。 学习编程,学个屁啊,炒股能赚钱,还编个屁程序。
刚入股市,就赶上牛市,顺风顺水
2019年底到2020年上半年,A股有几年不遇的大牛市,尤其是半导体、白酒、医疗行业行情非常火爆。我因为初入股市,没有历史包袱,没有锚点。当前哪个行业火爆,我就买那个,没事就跑 雪球 刷股票论坛的时间,比上班的时间还要长。
上班摸鱼和炒股 是家常便饭。工作上虽然不算心不在焉,但是漫不经心!
在这之前,我投入的金额不多。最多时候,也就投入了10万块钱。当时基金收益达到了三万块。我开始飘飘然。
开始炒股,也尝到了甜头,一开始,我把基金里的钱,逐渐的转移到股市里。当时的我给自己定纪律。七成资金投在基金里,三成资金投在股市里。做风险平衡,不能完全投入到风险高的股市里。
我自认为,我能禁得住 炒股这个毒品。
但是逐渐的,股票的收益越来越高,这个比例很快就倒转过来,我开始把更多资金投在股市中,其中有一只股票,我非常喜欢。这只股票后来成为了很多人的噩梦,成为很多股民 人生毁灭的导火索!
长春高新 股票代码:000661。我在这只股票上赚的很多,后来我觉得股市涨了那么多,该跌了吧,于是我就全部卖出,清仓止盈。 当时的我利润有六万,我觉得非常多了,我非常高兴。
其中 长春高新 一只股票的利润在 两万多元。当时这是我最喜欢的一只股票。我做梦也想不到,后来这只股票差点让我倾家荡产……
当时每天最开心的事情就是,打开基金和证券App,查看每天的收益。有的时候一天能赚 两千多,比工资还要高。群里也非常热闹,每个人都非常兴奋,热烈的讨论哪个股票涨得好。商业互吹成风……
换句话说:岂止是炒股上头,我已经中毒了!
之后就发生了,上文说的一切,我在抄底的过程中,越套越牢……
总结
以上都是我的个人真实经历。 我没有谈 A 股是否值得投资,也不评论当前的股市行情。我只是想分享自己的个人炒股经历。
炒股就是赌博
我想告诉大家,无论你在股市赚了多少钱,迟早都会还回去,越炒股越上头,赚的越多越上头。
赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。即使你没有遇到长春高新,也会有其他暴跌的股票等着你!
什么🐶皮的价值投资! 谈价值投资,撒泡尿照照自己,你一个散户,你配吗?
漫漫人生路,总会错几步。股市里错几步,就会让你万劫不复!
”把钱还我,我不玩了“
”我只要把钱赢回来,我就不玩了“
这都是常见的赌徒心理,奉劝看到此文的 程序员朋友,千万不要炒股和买基金。
尤其是喜欢打牌、打德州扑克,喜欢买彩-票的 赌性很强的朋友,一定要远离炒股,远离投资!
能救一个是一个!
来源:juejin.cn/post/7303348013934034983
扒一扒uniapp是如何做ios app应用安装的
为何要扒
因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。
开干
官方模板
先打开uniapp云打包一下项目看看
复制地址到移动端浏览器打开看看
这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。
开扒
F12打开choromdevtools,ctrl+s保存网页html。
保存成功,接下来看看html代码(样式代码删除了)
<!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
</head>
<body>
<br><br>
<center>
<a class="button" href="itms-services://?action=download-manifest&url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
</center>
<br><br>
<center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>
解析
从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")
先看看itms-services是什么意思,下面是代码开发助手给的解释
大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。
什么又是plist呢,这里再请我们的代码开发助手解释一下
对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。
打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。
访问后会出现
别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求
直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>
直接抓重点,这里存你存放ipa包的地址
这里改你应用的昵称
这里改图标
因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。
为我所用
分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:
将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:
可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至此,本次扒拉过程结束,需求落幕!
来源:juejin.cn/post/7270799565963149324
听一听比尔盖茨老人家怎么看待 AI 革命
最近发现比尔盖茨还在写文章,确实了不起,68 岁的老人家还在坚持输出,除了写文章,比尔盖茨还致力于教育、医疗和卫生等慈善工作,奋斗在一线,看来美国人也延迟退休啊 😅
原文《AI 将彻底改变计算机的使用方式》 大约一万多字,从宏观的角度讲述了 AI 的形态,并且提及了 AI 将影响的四个行业,最后再讲到目前面临的技术问题和非技术问题,整体文章深入浅出,非常值得一读。
本文做了一些删减,并尝试用自己的话去解读,更加符合中文读者的语义,其中的一些观点仅供参考,大家自由评判之,毕竟比尔先生还预测过计算机只需要 768kb 内存足以,而现在 8 个 G 都不够下饭的。
未来是智能体
首先比尔先生回忆了和保罗·艾伦一起创立微软公司的感觉,然后语重心长讲到,虽然经过了几十年的更迭,但是计算机还是比较蠢笨的,你要完成一个任务,得先选择某一个 app,比如用微软的 Word,去画一个商业的草图,但是这个 app 不能帮你发送邮件、分享自拍、分析数据、计划一个聚会,或者买电影票,要完成上述这些事情,要么找一个亲密的朋友,要么有一个个人助理。
胆大的预测,在下一个五年,AI 将彻底重构,你不需要再用为了处理不同任务,而去使用不同的 app。你可以用任何语言,直接告诉你的设备,你想要做什么。AI 借由丰富的理解能力,为你做个性化的服务和响应。在不远的将来,每个人都能够拥有远超当前科技的个性化的人工智能助手。
这种软件形态,比尔先生思考了 30 多年,但是最近两年来才成为现实,这种形态叫做“智能体”(Agent),能够用自然语言回复,并且基于用户的背景知识去完成不同的任务。
智能体不只是改变每个人和计算机的交互,他们同样颠覆了整个软件工业,这是从命令行输入、图形化交互以来,计算机交互最大的变革。
这里就可以总结了,比尔先生认同「All in One」的观点,一个智能体,处理你的一切事务,并且用发展的眼光看待,在之后技术继续蓬勃发展,智能体将登基,成为新的软件形态之主。
区分机器人和智能体
现在很多公司,做出来的产品根本算不上智能体,只能叫做机器人(Bot),机器人内嵌在某一个 app,借由 AI 的能力处理一些特定的任务,比如文本润色、扩写等,他们不会记住你用过多少次,也不会记住你的喜好,只能冰冷冷的机器人。
智能体不一样,更聪明,更主动,在你询问它们意见之前,就能够给到你合适的建议。它们能够跨 app 处理任务,记录你的行为,识别你的动机和意图,随着时间的推移,它们会慢慢变得更好,更加准确的给你提供信息和建议。
举个例子 🌰,你想要来一场特种兵旅行,机器人只能根据你的预算,给你定位酒店。而智能体,了解你近年来的旅行资料,能够推断出你是想要找一个距离景点近一点的还是远一点的,来为你推荐合适的酒店,还能根据你的兴趣和倾向,为你规划行程、预定餐厅。
AI 智能体,还有最让人激动的能力,那就把现在一些昂贵的服务价格给打下来,在这四个领域中,医疗健康、教育、生产力以及娱乐购物,智能体将大展拳脚。
医疗健康
目前,AI 的作用局限于处理一些非医疗任务,比如,就诊时候录音,然后生成报告给医生检查回顾。接下来会真正的转变,智能体能够帮助病人去做一个基础的伤病分类,获取关于处理健康问题的建议,决定是否需要去做进一步治疗。这些智能体还能够帮助医疗人员做决定,使之更加的高效。
现在已经有类似的 app 了,比如 Glass Health,能够分析病人摘要、提供建议诊断给医生做参考。
有了这些帮助病人和医疗人员的智能体,才是真正利于处在贫困的国家地区,那些贫瘠地方的老百姓们,甚至从来都没见过医生。比尔先生大义 🫡
这类「临床医学智能体」普及的速度可能会比较慢,毕竟这事关生死。人们需要看到这些健康智能体真正起到作用的证据,才能够接受它们。虽然健康智能体可能不完美,会犯错,但是,人类也同样不完美,会犯错。
教育
十多年来,比尔先生一直对借助软件帮助学生学习、让老师工作更轻松这样的事情很上心,软件不会替代教师工作,它只能对他们的工作进行补充,比如对学生做到个性化教育,从批改作业的压力中解放,还有其他种种。
现在已经有初步的进展,那就是一些基于文本的教育智能体,他们能够解释二元方程、提供练习数学题等等,但这还仅仅只是初步能力,接下来智能体还会解锁更多的能力。
确实,AI 学习辅导这个需求确实不错,之前热搜有这么一个家长给孩子辅导的问题
通过 AI 可以给到很好的学习启发:
生产力
生产力这方面已经卷得飞起了,微软以及为 Word、Outlook 等其他 app 集成了 Copilot(副驾驶),谷歌也在做类似的事情,也把 Bard 集合在自家的生产力 app 中。这些 Copilot 可以做很多事情,比如把文本转换为 PPT,回答表格问题,总结电子邮件内容等等。
当然,智能体能做到还有更多,如果你有一个商业想法,智能体会做一份商业计划书,然后基于此创建一份展示汇报,还能根据你的内容插入生成合适的图片。
娱乐购物
好吧,AI 可能帮你挑选电视频道、推荐电影、书、电视剧等等。比如,最近比尔盖茨投资了一家创业公司 Pix,用问答的方式推荐电影。虾皮也有一个基于 AI 的「DJ」,它能够根据你的喜好播放歌曲,并且还能和你交流,甚至会喊你的名字。
对科技行业的冲击波
总而言之,智能体最终能够帮助到我们生活的方方面,这对整个软件行业和社会的影响将会是深远的。
在计算领域,我们常谈论的「平台」,比如安卓、iOS 还有 Windows,是目前 app 和服务赖以存在的基础,而智能体将会成为下一个平台。
创建一个 app 或者服务,你不需要知道如何如变成或者图形设计,你只需要告诉你的智能体你想要做什么,它就能够编码、设计界面、创建 logo,然后发布到 app 到在线商店上,OpenAI 的 GPTs 能让我们一窥未来,GPTs 可以让非开发人员创建并分享自己的的智能体。
推荐下 starflow.tech,可以直接体验 GPTs
没有哪一家公司可以垄断智能体生意,因为未来会有多款不同 AI 引擎可供使用。现在,智能体只能依赖于其他软件,比如 Word 和 Excel,但是最终,他们将会独立运行。现在他们可能是免费的,但以后,你会为这些聪明高效的智能体付费,那么商业逻辑将改变,公司不再需要迎合广告公司而恶心用户,而是真正地为用户量身打造智能体。
在这些聪明但又复杂的智能体落地成为现实之前,还有大量的技术问题需要解决。
技术挑战
至今还没有人搞清楚智能体的底层存储结构是怎么样的,要创建一个个性化的智能体,我们需要一种新型数据库,它能把记录你的兴趣和关系的微妙之处,在保障隐私的情况下还能够快速查询信息。目前向量数据库是一种,或许之后还会有其他更好的呢。
另一个开放的问题就是一个用户大概需要和多少智能体打交道呢?你的个性化智能体会被分为医疗智能体和数学教师智能体吗?如果是的话,你是希望这些智能体彼此能够协作,还是在各自领域保持独立?
智能体的形态会是怎么样的呢,是手机、眼睛、项链、徽章,甚至是全息投影?这里比尔先生推测,现阶段最适合的是耳机,它能够听取你的声音,然后通过耳机回复你,其他的好处是,它还能调节音量、屏蔽周围噪音。
这里面还有其他种种技术挑战存在
1️⃣ 智能体之间互相交流的标准协议?
2️⃣ 智能体的价格要怎么打到每个人都能够用得起?
3️⃣ 用户少量提示词和智能体的准确回复之间如何取得平衡?
4️⃣ 如何减少幻觉,特别是在医疗这种特别重要的场景下?
5️⃣ 如何确保智能体不会伤害 or 歧视人类?
6️⃣ 如何确保智能体不会越权进行犯罪?
在不远的将来,智能体会迫使人类去思考,我们这么做是为了什么?想象一下,一个足够优秀的智能体存在,我们基本不需要工作了,那么每个人还需要接受高水平的教育吗?在未来可能是这样的,人们怎么消磨他们的时间?在所有答案都是已知的情况下,每个人还想要去上学吗?每个人都有大量的空闲时间,你还能有一个安全和繁荣的社会吗?
不过到这个时间点还很早,但至少目前,智能体正在走来,在接下来的几年,他们将彻底改变我们的生活。
来源:juejin.cn/post/7312736427326504996
怎样实现每次页面打开时都清除本页缓存?
"```markdown
每次页面加载时清除本页缓存可以通过多种方式实现,具体方法取决于需要的粒度和数据类型。以下是一些常见的技术:
使用meta标签(HTML):
<meta http-equiv=\"cache-control\" content=\"no-cache, no-store, must-revalidate\">
<meta http-equiv=\"pragma\" content=\"no-cache\">
<meta http-equiv=\"expires\" content=\"0\">
使用JavaScript:
// 清除整个页面缓存
window.location.reload(true);
// 清除特定资源的缓存
const url = 'https://example.com/style.css';
fetch(url, {
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
}).then(response => {
// 处理响应
});
// 清除localStorage
localStorage.clear();
// 清除sessionStorage
sessionStorage.clear();
使用HTTP头信息(服务端设置):
// Express.js 示例
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
使用框架或库功能:
例如,React中可以通过key属性强制重新渲染组件来清除缓存:
function App() {
const [key, setKey] = useState(0);
const resetPage = () => {
setKey(prevKey => prevKey + 1);
};
return (
<div key={key}>
{/* 页面内容 */}
<button onClick={resetPage}>重置页面</button>
</div>
);
}
清除浏览器缓存:
用户可以手动清除浏览器缓存来达到相同的效果。这通常通过浏览器设置或开发者工具的Network面板来实现。
综上所述,实现每次页面加载时清除本页缓存可以根据具体情况选择合适的方法。无论是通过HTML标签、JavaScript代码、服务器端设置还是框架功能,都可以有效地控制和管理页面的缓存行为,确保用户获得最新和最准确的内容。
来源:juejin.cn/post/7389643363160965130
接口不能对外暴露怎么办?
在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。
面对这样的情况,我们该如何实现呢?
1. 内外网接口微服务隔离
将对外暴露的接口和对内暴露的接口分别放到两个微服务上,一个服务里所有的接口均对外暴露,另一个服务的接口只能内网服务间调用。
该方案需要额外编写一个只对内部暴露接口的微服务,将所有只能对内暴露的业务接口聚合到这个微服务里,通过这个聚合的微服务,分别去各个业务侧获取资源。
该方案,新增一个微服务做请求转发,增加了系统的复杂性,增大了调用耗时以及后期的维护成本。
2. 网关 + redis 实现白名单机制
在 redis 里维护一套接口白名单列表,外部请求到达网关时,从 redis 获取接口白名单,在白名单内的接口放行,反之拒绝掉。
该方案的好处是,对业务代码零侵入,只需要维护好白名单列表即可;
不足之处在于,白名单的维护是一个持续性投入的工作,在很多公司,业务开发无法直接触及到 redis,只能提工单申请,增加了开发成本;
另外,每次请求进来,都需要判断白名单,增加了系统响应耗时,考虑到正常情况下外部进来的请求大部分都是在白名单内的,只有极少数恶意请求才会被白名单机制所拦截,所以该方案的性价比很低。
3. 方案三 网关 + AOP
相比于方案二对接口进行白名单判断而言,方案三是对请求来源进行判断,并将该判断下沉到业务侧。避免了网关侧的逻辑判断,从而提升系统响应速度。
我们知道,外部进来的请求一定会经过网关再被分发到具体的业务侧,内部服务间的调用是不用走外部网关的(走 k8s 的 service)。
根据这个特点,我们可以对所有经过网关的请求的header里添加一个字段,业务侧接口收到请求后,判断header里是否有该字段,如果有,则说明该请求来自外部,没有,则属于内部服务的调用,再根据该接口是否属于内部接口来决定是否放行该请求。
该方案将内外网访问权限的处理分布到各个业务侧进行,消除了由网关来处理的系统性瓶颈;
同时,开发者可以在业务侧直接确定接口的内外网访问权限,提升开发效率的同时,增加了代码的可读性。
当然该方案会对业务代码有一定的侵入性,不过可以通过注解的形式,最大限度的降低这种侵入性。
具体实操
下面就方案三,进行具体的代码演示。
首先在网关侧,需要对进来的请求header添加外网标识符: from=public
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono < Void > filter ( ServerWebExchange exchange, GatewayFilterChain chain ) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate().header('id', '').header('from', 'public').build())
.build()
);
}
@Override
public int getOrder () {
return 0;
}
}
接着,编写内外网访问权限判断的AOP和注解
@Aspect
@Component
@Slf4j
public class OnlyIntranetAccessAspect {
@Pointcut ( '@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnClass () {}
@Pointcut ( '@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)' )
public void onlyIntranetAccessOnMethed () {
}
@Before ( value = 'onlyIntranetAccessOnMethed() || onlyIntranetAccessOnClass()' )
public void before () {
HttpServletRequest hsr = (( ServletRequestAttributes ) RequestContextHolder.getRequestAttributes()) .getRequest ();
String from = hsr.getHeader ( 'from' );
if ( !StringUtils.isEmpty( from ) && 'public'.equals ( from )) {
log.error ( 'This api is only allowed invoked by intranet source' );
throw new MMException ( ReturnEnum.C_NETWORK_INTERNET_ACCESS_NOT_ALLOWED_ERROR);
}
}
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OnlyIntranetAccess {
}
最后,在只能内网访问的接口上加上@OnlyIntranetAccess注解即可
@GetMapping ( '/role/add' )
@OnlyIntranetAccess
public String onlyIntranetAccess() {
return '该接口只允许内部服务调用';
}
4. 网关路径匹配
在DailyMart项目中我采用的是第四种:即在网关中进行路径匹配。
该方案中我们将内网访问的接口全部以前缀/pv开头,然后在网关过滤器中根据路径找到具体校验器,如果是/pv访问的路径则直接提示禁止外部访问。
使用网关路径匹配方案不仅可以应对内网接口的问题,还可以扩展到其他校验机制上。
譬如,有的接口需要通过access_token进行校验,有的接口需要校验api_key 和 api_secret,为了应对这种不同的校验场景,只需要再实现一个校验类即可,由不同的子类实现不同的校验逻辑,扩展非常方便。
来源:juejin.cn/post/7389092138900717579
Spring Boot集成pf4j实现插件开发功能
1.什么是pf4j?
一个插件框架,用于实现插件的动态加载,支持的插件格式(zip、jar)。
核心组件
- **Plugin:**是所有插件类型的基类。每个插件都被加载到一个单独的类加载器中以避免冲突。
- **PluginManager:**用于插件管理的所有方面(加载、启动、停止)。您可以使用内置实现作为JarPluginManager, ZipPluginManager, DefaultPluginManager(它是一个JarPluginManager+ ZipPluginManager),或者您可以从AbstractPluginManager(仅实现工厂方法)开始实现自定义插件管理器。
- **PluginLoader:**加载插件所需的所有信息(类)。
- **ExtensionPoint:**是应用程序中可以调用自定义代码的点。这是一个java接口标记。任何 java 接口或抽象类都可以标记为扩展点(实现ExtensionPoint接口)。
- **Extension:**是扩展点的实现。它是一个类上的 Java 注释
场景
有一个spring-boot
实现的web应用,在某一个业务功能上提供扩展点,用户可以基于SDK实现功能扩展,要求可以管理插件,并且能够在业务功能扩展点处动态加载功能。
2.代码工程
实验目的
实现插件动态加载,调用 卸载
Demo整体架构
- pf4j-api:定义可扩展接口。
- pf4j-plugins-01:插件项目,可以包含多个插件,需要实现 plugin-api 中定义的接口。所有的插件jar包,放到统一的文件夹中,方便管理,后续只需要加载文件目录路径即可启动插件。
- pf4j-app:主程序,需要依赖 pf4j-api ,加载并执行 pf4j-plugins-01 。
pf4j-api
导入依赖
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.0.1</version>
</dependency>
自定义扩展接口,集成 ExtensionPoint ,标记为扩展点
package com.et.pf4j;
import org.pf4j.ExtensionPoint;
public interface Greeting extends ExtensionPoint {
String getGreeting();
}
打包给其他项目引用
pf4j-plugins-01
如果你想要能够控制插件的生命周期,你可以自定义类集成 plugin 重新里面的方法
/*
* Copyright (C) 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pf4j.demo.welcome;
import com.et.pf4j.Greeting;
import org.apache.commons.lang.StringUtils;
import org.pf4j.Extension;
import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;
/**
* @author Decebal Suiu
*/
public class WelcomePlugin extends Plugin {
public WelcomePlugin(PluginWrapper wrapper) {
super(wrapper);
}
@Override
public void start() {
System.out.println("WelcomePlugin.start()");
// for testing the development mode
if (RuntimeMode.DEVELOPMENT.equals(wrapper.getRuntimeMode())) {
System.out.println(StringUtils.upperCase("WelcomePlugin"));
}
}
@Override
public void stop() {
System.out.println("WelcomePlugin.stop()");
}
@Extension
public static class WelcomeGreeting implements Greeting {
@Override
public String getGreeting() {
return "Welcome ,my name is pf4j-plugin-01";
}
}
}
打成jar或者zip包,方便主程序加载
pf4j-app
加载插件包
package com.et.pf4j;
import org.pf4j.JarPluginManager;
import org.pf4j.PluginManager;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.nio.file.Paths;
import java.util.List;
@SpringBootApplication
public class DemoApplication {
/* public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}*/
public static void main(String[] args) {
// create the plugin manager
PluginManager pluginManager = new JarPluginManager(); // or "new ZipPluginManager() / new DefaultPluginManager()"
// start and load all plugins of application
//pluginManager.loadPlugins();
pluginManager.loadPlugin(Paths.get("D:\\IdeaProjects\\ETFramework\\pf4j\\pf4j-plugin-01\\target\\pf4j-plugin-01-1.0-SNAPSHOT.jar"));
pluginManager.startPlugins();
/*
// retrieves manually the extensions for the Greeting.class extension point
List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
System.out.println("greetings.size() = " + greetings.size());
*/
// retrieve all extensions for "Greeting" extension point
List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
for (Greeting greeting : greetings) {
System.out.println(">>> " + greeting.getGreeting());
}
// stop and unload all plugins
pluginManager.stopPlugins();
//pluginManager.unloadPlugins();
}
}
3.测试
运行DemoApplication.java 里面的mian函数,可以看到插件加载,调用以及卸载情况
4.引用
来源:juejin.cn/post/7389912762045251584
看清裁员,你就没什么好焦虑的了
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
前一阵子,组里面悄悄地走了两个同事,大家对此讳莫如深,原因自然不言而喻,被裁员了。
虽然裁员这件事情,在各种媒体上已经见怪不怪。但是近几年,即使是在北京,所在的公司确实还没遇到过这个情况。
本以为裁员这件事只存在于快节奏的大城市,但真正发生发生在二线城市的时候,我还是觉着心里蛮不舒服的。
因为裁员这个事情,对于个体来说,影响的确不小。
经济影响,裁员对个人的第一个影响就是收入中断。别看程序员收入比其他行业可能高一些,但是收入断层的压力依然很大。如果有房贷、车贷,更是雪上加霜。
职业发展,失去当前职位对程序员的职业发展产生不小影响,长期失业会导致技术技能的滞后,特别是在快速发展的科技行业,技术更新换代迅速。最重要的是,即使是短期的空白期,也可能使程序员在寻找新工作时,简历上的gap year也可能被HR视为一个警示信号。
心理压力,被裁员或许会经历自我怀疑、焦虑和抑郁等情绪,尤其是如果他们认为裁员是因为自己技能不足或表现不佳。这种心理压力可能会进一步影响他们在新工作中的表现和求职过程中的自信心。
所以看到身边的同事走了,我心里认为这真的是一件很糟糕的事情,从个人角度看,我认为裁员是不应该发生的。
但事实上愿望与真相的确不符合,我们常常拒绝接受真相。因为接受真相意味着改变,而改变可能会带来痛苦。
但是认清客观规律,才能认清自己,接受自己,不要固守你对事物"应该"是什么样的看法 这将使你无法了解真实的情况。
裁员存在很久了
太阳底下无新鲜事,裁员潮这件事情,早就不是第一次了。客观来讲,裁员这件事是的确是符合客观规律的,我们先回顾前面几次大的裁员潮吧,挑三个来讲。
90年代国有企业改革
1987年确定了改革开放,改革开放初期,逐步推行市场化改革,打破计划经济体制。
许多国有企业效率低下,国有企业迎来改革,负债累累,生产过剩。为了提高经济效率,政府推行国有企业改革,裁减冗员、减少亏损企业数量。
叠加政策推动,1997年党的十五大提出“抓大放小”政策,即保留和发展大型国有企业,改制或关闭小型亏损企业。
2008年全球金融危机
2008年爆发的全球金融危机导致外需急剧下降,许多出口导向型企业受到重创,全球经济衰退。
企业利润下降,为了控制成本,许多企业不得不裁员。
危机暴露了中国经济过度依赖外需和低附加值产业的问题,促使政府推动经济结构调整。
2019年互联网行业的大量裁员潮
从2018年下半年开始,不少互联网公司的裁员就已经开始,百度、阿里、腾讯、京东等大企业更是成为人们关注的焦点。
那是像摩拜、滴滴、美团等互联网行业,都是靠大量烧钱进行维持的,由于长期烧钱,投资者热情散去,各路资本也变的谨慎了。
是的,其实互联网裁员潮2019年就已经逐步开始了,互联网行业经过10多年的高速发展,慢慢走向了平稳期,互联网红利期已经过去,资本寒冬到来。
裁员背后的规律
企业战略与内部因素
成本控制与财务压力
企业在面临财务压力时,裁员通常是首选的成本控制手段之一。
通过减少员工数量,企业可以直接降低工资支出和相关福利成本。尤其是在利润率下降或财务报表不佳时,裁员成为企业迅速改善财务状况的一种方式。
比如最近理想汽车大裁员,背后就是因为纯电市场推广不顺,销售目标不及预期。
战略转型与业务重组
企业在战略转型或业务重组过程中,往往会调整其人力资源配置。例如,当企业从传统业务转向新兴技术领域时,原有的一些岗位可能不再需要,从而导致裁员。又或者,企业合并或收购也会带来很多人员问题。
比如4月底,马斯克裁撤了整个超充团队,与企业的战略是相关的,欧美充电桩市场竞争很激烈,国内的超充体系技术革新也很快,特斯拉超充的竞争力明显减弱,其实主要就是盈利问题吧。
效率提升与自动化
随着技术的进步,企业会不断寻求通过自动化和技术创新来提升效率。自动化工具和人工智能的应用可以显著减少对人力的依赖,从而导致部分职位的消失。尤其是在重复性高、技术含量低的岗位上,自动化的影响尤为明显。
这个例子就太多了,不一一列举了。
市场环境与外部因素
经济周期与市场波动
宏观经济的周期性波动对企业的经营状况有着深远影响。在经济衰退或市场需求下降时,企业往往会通过裁员来应对收入减少和利润率下滑的挑战。反之,在经济繁荣期,企业则可能增加招聘以满足业务扩张的需求。
最简单的例子,在互联网高速发展期,1年p6、3年p7根本不是梦,后端只要有过简单的开发经验,找工作根本不是问题。
目前中国经济增速放缓、消费升级趋势减弱、人口红利消失等因素的影响,中国互联网市场的需求增长趋于饱和或下降。再叠加就业市场饱和,企业对学历、经验提出了更多的要求。
行业竞争与市场压力
激烈的市场竞争也会促使企业通过裁员来保持竞争力。
当企业面临市场份额的争夺和利润率的压力时,降低运营成本成为必然选择。通过优化人力资源配置,企业可以在价格战或市场扩张中保持灵活性和优势。
目前用户规模停滞,智能手机普及率饱和,互联网用户规模增长趋于停滞,由增量市场变为存量市场,互联网获客成本越来越高。
政策变动与法规影响
政府政策和法规的变化也可能引发企业的裁员行为。例如,税收政策的调整、劳动法规的变化或贸易政策的波动,都会影响企业的运营成本和市场策略。在这种情况下,企业可能通过裁员来适应新的政策环境和经营压力。
就像目前互联网行业规范和监管愈加严格,更加注重合规,因此互联网行业也会收到影响。
技术发展与行业趋势
技术创新与产业升级
技术创新是推动产业升级的重要动力,也直接影响着就业结构的变化。新技术的应用往往带来生产方式的变革,导致传统岗位的减少和新兴岗位的增加。企业在追求技术领先的过程中,不可避免地会进行人力资源的调整。
其实,每一轮的市场繁荣,都是由技术创新带来的,例如3G时代叠加iPhone这样的智能手机,才打开了移动互联网快速发展的时期,才有了阿里 All in 无线的战略。
而现在正在快速发展的AI大模型,就是新一轮技术创新的开始,AI的可能性太大了。
行业趋势
数字化转型和远程办公的普及也在改变企业的用工模式。数字化工具的应用使得部分岗位可以通过外包或灵活用工的方式来完成,从而减少了全职员工的需求。远程办公的兴起则使企业有更多的选择来优化人力资源配置。
怎么做
通过之前的裁员历史,还有裁员背后的一些深层次原因,你应该能对裁员这件事背后的规律,有了一定的了解。
我们每个人都是时代中微不足道的一粒沙子,那么从个人角度,我们应该如何应对呢?
调整心态
年年涨薪,按时晋升机会等等这些互联网黄金时期的待遇,在当前可能不再是标配了,想要做到这一点,你需要在职场中付出比别人多几倍的努力,大家或许应该深有体会。
那么跳槽也不一定有涨薪,甚至工作时间长的人、base高的人,跳槽可能还会面临降薪。
所以一定要调整预期,如果岗位未来还有不错的发展空间,降薪也值得去。
还有就是要做好选择,不能既要(收入、光环),还要(轻松、自由)。
稳住基本盘
技术&业务能力
其实有挺多朋友加我,也不乏一些工作只有一两年的朋友,他们会感到焦虑,比如工作不好找、大厂不好进,是不是需要更换赛道。但是针对于工作年限不久的朋友,我始终给出的建议就是要先深耕技术,做好当前工作,这是作为一个技术人最基本的要求,也是立身之本。
无论市场环境如何,只要自己的技术或者业务能力在线,能够解决工作中遇到的问题,能够产生价值,那么你一定就会持续有一定竞争力。
身体
身体才是奋斗的本钱,长期加班、久坐,大家可能都会有或多或少的一些小毛病。
之所以把身体也列出来,是因为当下我真的感觉很容易精力不足,在持续大量学习的时候,很容易感到疲倦,下班之后就会什么也不想干,窝在沙发一晚上。
在你想做一件事情的时候,良好的身体状况才能保证你能够全身心的投入进去。
财富积累
良好的财富积累,既能提供经济缓冲,也可以增加自己的心里安全感。
如果想做职业转换时,有足够的储蓄也可以让你有更多的时间和资源去学习一些新技能。
提前探索
开头既然说了,国企也曾有过裁员潮,那么在互联网行业,我们大概率也很难干到退休了。
所以你需要提前探索自己可以长期耕耘的方向,并且对自己有一个清晰的自我认知。
比如经常问自己几个问题:
- 你觉得自己的内驱力是什么?喜欢做什么事情?或者自己希望自己的人生是一个怎样的状态。
- 你擅长什么技能?或者说有什么资源?比如技术、运营、销售、沟通等
- 你喜欢做的事情和擅长的技能资源等,比如健身、拍视频、写作
说在最后
好了,文章到这里就要结束了。
所以你看,由于企业战略、市场环境、技术趋势的影响,都有可能成为裁员的原因。所以我们不要认为裁员是自身能力不足,甚至影响自己的情绪。
面对裁员与互联网里不断宣扬的“35岁危机”,与其担忧焦虑,不如先调整心态,稳住自己当下的基本盘。
最重要的是提前探索,找到自己喜欢的事情,去探索自己的能力边界。
来源:juejin.cn/post/7378321582399914003
使用 uni-app 开发 APP 并上架 IOS 全过程
教你用 uni-app 开发 APP 上架 IOS 和 Android
介绍
本文记录了我使用uni-app开发构建并发布跨平台移动应用的全过程,旨在帮助新手开发者掌握如何使用uni-app进行APP开发并最终成功上架。通过详细讲解从注册开发者账号、项目创建、打包发布到应用商店配置的每一步骤,希望我的经验分享能为您提供实用的指导和帮助,让您在开发之旅中少走弯路,顺利实现自己的应用开发目标。
环境配置
IOS 环境配置
注册开发者账号
如果没有开发者账号需要注册苹果开发者账号,并且加入 “iOS Developer Program”,如果是公司项目那么可以将个人账号邀请到公司的项目中。
获取开发证书和配置文件
登录Apple Developer找到创建证书入口
申请证书的流程可以参考Dcloud官方的教程,申请ios证书教程
开发证书和发布证书都申请好应该是这个样子
创建App ID
创建一个App ID。App ID是iOS应用的唯一标识符,稍后你会在uni-app项目的配置文件中使用它。
配置测试机
第一步打开开发者后台点击Devices
第二步填写UDID
第三步重新生成开发证书并且勾选新增的测试机,建议一次性将所有需要测试的手机加入将来就不用一遍遍重复生成证书了
Android 环境配置
生成证书
Android平台签名证书(.keystore)生成指南: ask.dcloud.net.cn/article/357…
uni-app 项目构建配置
基础配置
版本号versionCode 前八位代表年月日,后两位代表打包次数
APP 图标设置
APP启动界面配置
App模块配置
注意这个页面用到什么就配置什么不然会影响APP审核
App隐私弹框配置
注意根据工业和信息化部关于开展APP侵害用户权益专项整治要求应用启动运行时需弹出隐私政策协议,说明应用采集用户数据,这里将详细介绍如何配置弹出“隐私协议和政策”提示框
详细内容可参考Uni官方文档
注意!androidPrivacy.json不要添加注释,会影响隐私政策提示框的显示!!!
在app启动界面配置勾选后会在项目中自动添加androidPrivacy.json文件,可以双击打开自定义配置以下内容:
{
"version" : "1",
"prompt" : "template",
"title" : "服务协议和隐私政策",
"message" : " 请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/> 你可阅读<a href="https://xxx.xxx.com/userPolicy.html">《服务协议》</a>和<a href="https://xxxx.xxxx.com/privacyPolicy.html">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept" : "同意并接受",
"buttonRefuse" : "暂不同意",
"hrefLoader" : "system|default",
"backToExit" : "false",
"second" : {
"title" : "确认提示",
"message" : " 进入应用前,你需先同意<a href="https://xxx.xxxx.com/userPolicy.html">《服务协议》</a>和<a href="https://xxx.xxxx.com/userPolicy.html">《隐私政策》</a>,否则将退出应用。",
"buttonAccept" : "同意并继续",
"buttonRefuse" : "退出应用"
},
"disagreeMode" : {
"loadNativePlugins" : false,
"showAlways" : false
},
"styles" : {
"backgroundColor" : "#fff",
"borderRadius" : "5px",
"title" : {
"color" : "#fff"
},
"buttonAccept" : {
"color" : "#22B07D"
},
"buttonRefuse" : {
"color" : "#22B07D"
},
"buttonVisitor" : {
"color" : "#22B07D"
}
}
}
我的隐私协议页面是通过vite打包生成的多入口页面进行访问,因为只能填一个地址所以直接使用生产环境的例如:xxx.xxxx.com/userPolicy.…
构建打包
使用HBuilderX进行云打包
IOS打包
构建测试包
第一步 点击发行->原生app云打包
第二步配置打包变量
运行测试包
打开HbuildX->点击运行->运行到IOS App基座
选择设备->使用自定义基座运行
构建生产包
和构建测试包基本差不多,需要变更的就是ios证书的profile文件和密钥证书
构建成功后的包在dist目录下release文件夹中
上传生产包
上传IOS安装包的方式有很多我们选择通过transporter软件上传,下载transporter并上传安装包
确认无误后点击交付,点击交付后刷新后台,一般是5分钟左右就可以出现新的包了。
App store connect 配置
上传截屏
只要传6.5和5.5两种尺寸的就可,注意打包的时候千万不能勾选支持ipad选项,不然这里就会要求上传ipad截屏
填写app信息
配置发布方式
自动发布会在审核完成后直接发布,建议选手动发布
配置销售范围
配置隐私政策
配置完之后IOS就可以提交审核了,不管审核成功还是失败Apple都会发一封邮件通知你审核结果
安卓打包
构建测试包
构建的包在dist/debug目录下
运行测试包
如果需要运行的话,点击运行 -> 运行到Android App底座
构建生产包
构建后的包在dist目录下release文件夹中
构建好安卓包之后就可以在国内的各大手机厂商的应用商店上架了,由于安卓市场平台五花八门就不给大家一一列举了。
参考链接:
结语
本文介绍了使用uni-app开发并发布跨平台移动应用的完整流程,包括注册开发者账号、项目创建、打包发布以及应用商店配置,帮助开发者高效地将应用上架到iOS和Android平台。感谢您的阅读,希望本文能对您有所帮助。
来源:juejin.cn/post/7379958888909029395
我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱
我真的不想再用mybatis和其衍生框架了选择自研亦是一种解脱
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
GITEE地址 gitee.com/xuejm/easy-…
为什么要用orm
众所邹知orm的出现让本来以sql实现的复杂繁琐功能大大简化,对于大部分程序员而言一个框架的出现是为了生产力的提升.。dbc定义了交互数据库的规范,任何数据库的操作都是只需要满足jdbc规范即可,而orm就是为了将jdbc的操作进行简化。我个人“有幸”体验过.net和java的两个大orm,只能说差距很大,当然语言上的一些特性也让java在实现orm上有着比较慢的进度,譬如泛型的出现,lambda的出现。
一个好的orm我觉得需要满足以下几点
- 强类型,如果不支持强类型那么和手写sql没有区别
- 能实现80%的纯手写sql的功能,好的orm需要覆盖业务常用功能
- 支持泛型,“如果一个orm连泛型都不支持那么就没有必要存在”这是一句现实但是又很残酷的结论,但是泛型会大大的减少开发人员的编写错误率
- 不应该依赖过多的组件,当然这并不是orm特有的,任何一个库其实依赖越少越不易出bug
其实说了这么多总结一下就是一个好的orm应该有ide的提示外加泛型约束帮助开发可以非常顺滑的把代码写下去,并且错误部分可以完全的在编译期间提现出来,运行时错误应该尽可能少的去避免。
为什么放弃mybatis
首先如果你用过其他语言的orm那么再用java的mybatis就像你用惯了java的stream然后去自行处理数据过滤,就像你习惯了kotlin的语法再回到java语法,很难受。这种难受不是自动挡到手动挡的差距,而且自动挡到手推车的差距。
xml
配置sql也不知道是哪个“小天才”想出来的,先不说写代码的时候java代码和xml代码跳来跳去,而且xml下>
,<
必须要配合CDATA
不然xml解析就失败,别说转义,我写那玩意在加转义你确定让我后续看得眼睛不要累死吗?美名其曰xml和代码分离方便维护,但是你再怎么方便修改了代码一样需要重启,并且因为代码写在xml里面导致动态条件得能力相对很弱。并且我也不知道mybatis为什么天生不支持分页,需要分页插件来支持,难道一个3202年的orm了还需要这样吗,很难搞懂mybatis的作者难道不写crud代码的吗?有些时候简洁并不是偷懒的原因,当然也有可能是架构的问题导致的。
逻辑删除的功能我觉得稍微正常一点的企业一定都会有这个功能,但是因为使用了myabtis,因为手写sql,所以常常会忘记往sql中添加逻辑删除字段,从而导致一些奇奇怪怪的bug需要排查,因为这些都是编译器无法体现的错误,因为他是字符串,因为mybatis把这个问题的原因指向了用户,这一点他很聪明,这个是用户的错误而不是框架的,但是框架要做的就是尽可能的将一些重复工作进行封装隐藏起来自动完成。
可能又会有一些用户会说所见即所得这样我才能知道他怎么执行了,但是现在哪个orm没有sql打印功能,哪个orm框架执行的sql和打印的sql是不一样的,不是所见即所得。总体而言我觉得mybatis
充其量算是sqltemlate,比sqlhelper好的地方就是他是参数化防止sql注入。当然最主要的呀一点事难道java程序员不需要修改表,不需要动表结构,不需要后期维护的吗还是说java程序员写一个项目就换一个地方跳槽,还是说java程序员每个方法都有单元测试。我在转java后理解了一点,原来这就是你们经常说的java加班严重,用这种框架加班不严重就有鬼了。
为什么放弃mybatis衍生框架
有幸在201几年再网上看到了mybatis-plus
框架,这块框架一出现就吸引了我,因为他在处理sql的方式上和.net的orm很相似,起码都是强类型,起码不需要java文件和xml文件跳来跳去,平常50%的代码也是可以通过框架的lambda表达式来实现,我个人比较排斥他的字符串模式的querywrapper
,因为一门强类型语言缺少了强类型提示,在编写代码的时候会非常的奇怪。包括后期的重构,当然如果你的代码后续不需要你维护那么我觉得你用哪种方式都是ok的反正是一次性的,能出来结果就好了。
继续说mybatis-plus
,因为工作的需要再2020年左右针对内部框架进行改造,并且让mybatis-plus支持强类型gr0up by,sum,min,max,any等api。
这个时候其实大部分情况下已经可以应对了,就这样用了1年左右这个框架,包括后续的update的increment
,decrement
update table set column=column-1 where id=xxx and column>1
全部使用lambda强类型语法,可以应对多数情况,但是针对join始终没有一个很好地方法。直到我遇到了mpj
也就是mybatis-plus-join
,但是这个框架也有问题,就是这个逻辑删除在join的子表上不生效,需要手动处理,如果生效那么在where上面,不知道现在怎么样了,当时我也是自行实现了让其出现在join的on后面,但是因为实现是需要实现某个接口的,所以并没有pr代码.
首先定义一个接口
public interface ISoftDelete {
Boolean getDeleted();
}
//其中join mapper是我自己的实现,主要还是`WrapperFunction`的那段定义
@Override
public Scf4jBaseJoinLinq<T1,TR> on(WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> onFunction) {
WrapperFunction<MPJAbstractLambdaWrapper<T1, ?>> join= on->{
MPJAbstractLambdaWrapper<T1, ?> apply = onFunction.apply(on);
if(ISoftDelete.class.isAssignableFrom(joinClass)){
SFunction deleted = LambdaHelper.getFunctionField(joinClass, "deleted", Boolean.class);
apply.eq(deleted,false);
}
return apply;
};
joinMapper.setJoinOnFunction(query->{
query.innerJoin(joinClass,join);
});
return joinMapper;
}
虽然实现了join
但是还是有很多问题出现和bug。
- 比如不支持vo对象的返回,只能返回数据库对象自定义返回列,不然就是查询所有列
- 再比如如果你希望你的对象update的时候填充null到数据库,那么只能在entity字段上添加,这样就导致这个字段要么全部生效要么全部不生效.
- 批量插入不支持默认居然是foreach一个一个加,当然这也没关系,但是你真的想实现批处理需要自己编写很复杂的代码并且需要支持全字段。而不是null列不填充
MetaObjectHandler
,支持entity
的insert
和update
但是不支持lambdaUpdateWrapper
,有时候当前更新人和更新时间都是需要的,你也可以说数据库可以设置最后更新时间,但是最后修改人呢?- 非常复杂的动态表名,拜托大哥我只是想改一下表名,目前的解决方案就是try-finally每次用完都需要清理一下当前线程,因为tomcat会复用线程,通过threadlocal来实现,话说pagehelper应该也是这种方式实现的吧
当然其他还有很多问题导致最终我没办法忍受,选择了自研框架,当然我的框架自研是参考了一部分的freesql和sqlsuagr的api,并且还有java的beetsql的实现和部分方法。毕竟站在巨人的肩膀上才能看的更远,不要问我为什么不参考mybatis的,我觉得mybatis已经把简单问题复杂化了,如果需要看懂他的代码是一件很得不偿失的事情,最终我发现我的选择是正确的,我通过参考beetsql
的源码很快的清楚了java这边应该需要做的事情,为我编写后续框架节约了太多时间,这边也给beetsql
打个广告https://gitee.com/xiandafu/beetlsql
自研orm有哪些特点
easy-query
一款无任何依赖的java全新高性能orm支持 单表 多表 子查询 逻辑删除 多租户 差异更新 联级一对一 一对多 多对一 多对多 分库分表(支持跨表查询分页等) 动态表名 数据库列高效加解密支持like crud拦截器 原子更新 vo对象直接返回
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
GITEE地址 gitee.com/xuejm/easy-…
- 强类型,可以帮助团队在构建和查询数据的时候拥有id提示,并且易于后期维护。
- 泛型可以控制我们编写代码时候的一些低级错误,比如我只查询一张表,但是where语句里面可以使用不存在上下文的表作为条件,进一步限制和加强表达式
- easy-query提供了三种模式分别是lambda,property,apt proxy其中lambda表达式方便重构维护,property只是性能最好,apt proxy方便维护,但是重构需要一起重构apt文件
单表查询
//根据条件查询表中的第一条记录
List<Topic> topics = easyQuery
.queryable(Topic.class)
.limit(1)
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LIMIT 1
<== Total: 1
//根据条件查询id为3的集合
List<Topic> topics = easyQuery
.queryable(Topic.class)
.where(o->o.eq(Topic::getId,"3").eq(Topic::geName,"4")
.toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t WHERE t.`id` = ? AND t.`name` = ?
==> Parameters: 3(String),4(String)
<== Total: 1
多表
Topic topic = easyQuery
.queryable(Topic.class)
//join 后面是双参数委托,参数顺序表示join表顺序,可以通过then函数切换
.leftJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where(o -> o.eq(Topic::getId, "3"))
.firstOrNull();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM t_topic t LEFT JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t.`id` = ? LIMIT 1
==> Parameters: 3(String)
<== Total: 1
List<BlogEntity> blogEntities = easyQuery
.queryable(Topic.class)
//join 后面是双参数委托,参数顺序表示join表顺序,可以通过then函数切换
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
//join查询select必须要带对应的返回结果,可以是自定义dto也可以是实体对象,如果不带对象则返回t表主表数据
.select(BlogEntity.class, (t, t1) -> t1.columnAll())
.toList();
==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic t INNER JOIN t_blog t1 ON t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: 3(String)
<== Total: 1
子查询
```java
//SELECT * FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<BlogEntity> subQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "1"));
List<Topic> x = easyQuery
.queryable(Topic.class).where(o -> o.exists(subQueryable.where(q -> q.eq(o, BlogEntity::getId, Topic::getId)))).toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE EXISTS (SELECT 1 FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ? AND t1.`id` = t.`id`)
==> Parameters: false(Boolean),1(String)
<== Time Elapsed: 3(ms)
<== Total: 1
//SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?
Queryable<String> idQueryable = easyQuery.queryable(BlogEntity.class)
.where(o -> o.eq(BlogEntity::getId, "123"))
.select(String.class, o -> o.column(BlogEntity::getId));//如果子查询in string那么就需要select string,如果integer那么select要integer 两边需要一致
List<Topic> list = easyQuery
.queryable(Topic.class).where(o -> o.in(Topic::getId, idQueryable)).toList();
==> Preparing: SELECT t.`id`,t.`stars`,t.`title`,t.`create_time` FROM `t_topic` t WHERE t.`id` IN (SELECT t1.`id` FROM `t_blog` t1 WHERE t1.`deleted` = ? AND t1.`id` = ?)
==> Parameters: false(Boolean),123(String)
<== Time Elapsed: 2(ms)
<== Total: 0
自定义逻辑删除
//@Component //如果是spring
public class MyLogicDelStrategy extends AbstractLogicDeleteStrategy {
/**
* 允许datetime类型的属性
*/
private final Set<Class<?>> allowTypes=new HashSet<>(Arrays.asList(LocalDateTime.class));
@Override
protected SQLExpression1<WherePredicate<Object>> getPredicateFilterExpression(LogicDeleteBuilder builder,String propertyName) {
return o->o.isNull(propertyName);
}
@Override
protected SQLExpression1<ColumnSetter<Object>> getDeletedSQLExpression(LogicDeleteBuilder builder, String propertyName) {
// LocalDateTime now = LocalDateTime.now();
// return o->o.set(propertyName,now);
//上面的是错误用法,将now值获取后那么这个now就是个固定值而不是动态值
return o->o.set(propertyName,LocalDateTime.now())
.set("deletedUser",CurrentUserHelper.getUserId());
}
@Override
public String getStrategy() {
return "MyLogicDelStrategy";
}
@Override
public Set<Class<?>> allowedPropertyTypes() {
return allowTypes;
}
}
//为了测试防止数据被删掉,这边采用不存在的id
logicDelTopic.setId("11xx");
//测试当前人员
CurrentUserHelper.setUserId("easy-query");
long l = easyQuery.deletable(logicDelTopic).executeRows();
==> Preparing: UPDATE t_logic_del_topic_custom SET `deleted_at` = ?,`deleted_user` = ? WHERE `deleted_at` IS NULL AND `id` = ?
==> Parameters: 2023-04-01T23:15:13.944(LocalDateTime),easy-query(String),11xx(String)
<== Total: 0
差异更新
- 要注意是否开启了追踪
spring-boot
下用@EasyQueryTrack
注解即可开启
- 是否将当前对象添加到了追踪上下文 查询添加
asTracking
或者 手动将查询出来的对象进行easyQuery.addTracking(Object entity)
TrackManager trackManager = easyQuery.getRuntimeContext().getTrackManager();
try{
trackManager.begin();
Topic topic = easyQuery.queryable(Topic.class)
.where(o -> o.eq(Topic::getId, "7")).asTracking().firstNotNull("未找到对应的数据");
String newTitle = "test123" + new Random().nextInt(100);
topic.setTitle(newTitle);
long l = easyQuery.updatable(topic).executeRows();
}finally {
trackManager.release();
}
==> Preparing: UPDATE t_topic SET `title` = ? WHERE `id` = ?
==> Parameters: test1239(String),7(String)
<== Total: 1
关联查询
一对一
学生和学生地址
//数据库对像查询
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.toList();
//vo自定义列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolStudentAddress).asTracking().disableLogicDelete())
.select(SchoolStudentVO.class,o->o.columnAll()
.columnInclude(SchoolStudent::getSchoolStudentAddress,SchoolStudentVO::getSchoolStudentAddress))
.toList();
多对一
学生和班级
//数据库对像查询
List<SchoolStudent> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.toList();
//自定义列
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass,s->s.column(SchoolClassVO::getId))
)
.toList();
//vo自定义列映射返回
List<SchoolStudentVO> list1 = easyQuery.queryable(SchoolStudent.class)
.include(o -> o.one(SchoolStudent::getSchoolClass))
.select(SchoolStudentVO.class,o->o
.columnAll()
.columnInclude(SchoolStudent::getSchoolClass,SchoolStudentVO::getSchoolClass)
)
.toList();
一对多
班级和学生
//数据库对像查询
List<SchoolClass> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.toList();
//vo自定义列映射返回
List<SchoolClassVO> list1 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolStudents))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolStudents,SchoolClassVO::getSchoolStudents))
.toList();
多对多
班级和老师
List<SchoolClass> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers,1))
.toList();
List<SchoolClassVO> list2 = easyQuery.queryable(SchoolClass.class)
.include(o -> o.many(SchoolClass::getSchoolTeachers))
.select(SchoolClassVO.class,o->o.columnAll()
.columnIncludeMany(SchoolClass::getSchoolTeachers,SchoolClassVO::getSchoolTeachers))
.toList();
动态报名
List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a -> "aa_bb_cc")
.where(o -> o.eq(BlogEntity::getId, "123")).toList();
==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0
List<BlogEntity> blogEntities = easyQuery.queryable(BlogEntity.class)
.asTable(a->{
if("t_blog".equals(a)){
return "aa_bb_cc1";
}
return "xxx";
})
.where(o -> o.eq(BlogEntity::getId, "123")).toList();
==> Preparing: SELECT t.`id`,t.`create_time`,t.`update_time`,t.`create_by`,t.`update_by`,t.`deleted`,t.`title`,t.`content`,t.`url`,t.`star`,t.`publish_time`,t.`score`,t.`status`,t.`order`,t.`is_top`,t.`top` FROM aa_bb_cc1 t WHERE t.`deleted` = ? AND t.`id` = ?
==> Parameters: false(Boolean),123(String)
<== Total: 0
List<BlogEntity> x_t_blog = easyQuery
.queryable(Topic.class)
.asTable(o -> "t_topic_123")
.innerJoin(BlogEntity.class, (t, t1) -> t.eq(t1, Topic::getId, BlogEntity::getId))
.asTable("x_t_blog")
.where((t, t1) -> t1.isNotNull(BlogEntity::getTitle).then(t).eq(Topic::getId, "3"))
.select(BlogEntity.class, (t, t1) -> t1.columnAll()).toList();
==> Preparing: SELECT t1.`id`,t1.`create_time`,t1.`update_time`,t1.`create_by`,t1.`update_by`,t1.`deleted`,t1.`title`,t1.`content`,t1.`url`,t1.`star`,t1.`publish_time`,t1.`score`,t1.`status`,t1.`order`,t1.`is_top`,t1.`top` FROM t_topic_123 t INNER JOIN x_t_blog t1 ON t1.`deleted` = ? AND t.`id` = t1.`id` WHERE t1.`title` IS NOT NULL AND t.`id` = ?
==> Parameters: false(Boolean),3(String)
<== Total: 0
最后
感谢各位看到最后,希望以后我的开源框架可以帮助到您,如果您觉得有用可以点点star,这将对我是极大的鼓励
更多文档信息可以参考git地址或者文档
文档地址 xuejm.gitee.io/easy-query-…
GITHUB地址 github.com/xuejmnet/ea…
来源:juejin.cn/post/7259926933008908325
压缩炸弹,Java怎么防止
一、什么是压缩炸弹,会有什么危害
1.1 什么是压缩炸弹
压缩炸弹(ZIP)
:一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。
以下是安全测试几种经典的压缩炸弹
graph LR
A(安全测试的经典压缩炸弹)
B(zip文件42KB)
C(zip文件10MB)
D(zip文件46MB)
E(解压后5.5G)
F(解压后281TB)
G(解压后4.5PB)
A ---> B --解压--> E
A ---> C --解压--> F
A ---> D --解压--> G
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。
压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。
压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。
1.2 压缩炸弹会有什么危害
graph LR
A(压缩炸弹的危害)
B(资源耗尽)
C(磁盘空间耗尽)
D(系统崩溃)
E(拒绝服务攻击)
F(数据丢失)
A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
压缩炸弹可能对计算机系统造成以下具体的破坏:
资源耗尽
:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。磁盘空间耗尽
:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。系统崩溃
:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。拒绝服务攻击
:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。数据丢失
:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。
重要提示
:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。
二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹
2.1 个人有没有方法可以检测压缩炸弹?
有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:
graph LR
A(个人检测压缩炸弹)
B(安全软件和防病毒工具)
C(文件大小限制)
D(文件类型过滤)
A ---> B --> E(推荐)
A ---> C --> F(太大的放个心眼)
A ---> D --> G(注意不认识的文件类型)
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
安全软件和防病毒工具(推荐)
:使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。文件大小限制
:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。文件类型过滤
:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。
2.2 Java怎么防止压缩炸弹
在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:
graph LR
A(Java防止压缩炸弹)
B(解压缩算法的限制)
C(设置解压缩操作的资源限制)
D(使用安全的解压缩库)
E(文件类型验证和过滤)
F(异步解压缩操作)
G(安全策略和权限控制)
A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
解压缩算法的限制
:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。设置解压缩操作的资源限制
:使用Java的java.util.zip
或java.util.jar
等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。使用安全的解压缩库
:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。文件类型验证和过滤
:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。异步解压缩操作
:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。安全策略和权限控制
:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。
2.2.1 使用解压算法的限制来实现防止压缩炸弹
在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制
来实现防止压缩炸弹。
先来看看我们实现的思路
:
graph TD
A(开始) --> B[创建 ZipFile 对象]
B --> C[打开要解压缩的 ZIP 文件]
C --> D[初始化 zipFileSize 变量为 0]
D --> E{是否有更多的条目}
E -- 是 --> F[获取 ZIP 文件的下一个条目]
F --> G[获取当前条目的未压缩大小]
G --> H[将解压大小累加到 zipFileSize 变量]
H --> I{zipFileSize 是否超过指定的大小}
I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
J --> K[抛出 IllegalArgumentException 异常]
K --> L(结束)
I -- 否 --> M(保存解压文件) --> E
E -- 否 --> L
style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
实现流程说明如下:
- 首先,通过给定的
file
参数创建一个ZipFile
对象,用于打开要解压缩的 ZIP 文件。 zipFileSize
变量用于计算解压缩后的文件总大小。- 使用
zipFile.entries()
方法获取 ZIP 文件中的所有条目,并通过while
循环逐个处理每个条目。 - 对于每个条目,使用
entry.getSize()
获取条目的未压缩大小,并将其累加到zipFileSize
变量中。 - 如果
zipFileSize
超过了给定的size
参数,说明解压后的文件大小超过了限制,此时会调用deleteDir()
方法删除已解压的文件夹,并抛出IllegalArgumentException
异常,以防止压缩炸弹攻击。 - 创建一个
File
对象unzipped
,表示解压后的文件或目录在输出文件夹中的路径。 - 如果当前条目是一个目录,且
unzipped
不存在,则创建该目录。 - 如果当前条目不是一个目录,确保
unzipped
的父文件夹存在。 - 创建一个
FileOutputStream
对象fos
,用于将解压后的数据写入到unzipped
文件中。 - 通过
zipFile.getInputStream(entry)
获取当前条目的输入流。 - 创建一个缓冲区
buffer
,并使用循环从输入流中读取数据,并将其写入到fos
中,直到读取完整个条目的数据。 - 最后,在
finally
块中关闭fos
和zipFile
对象,确保资源的释放。
实现代码工具类
:
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* 文件炸弹工具类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/
public class FileBombUtil {
/**
* 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
*/
public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;
/**
* 文件超限提示
*/
public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";
/**
* 解压文件(带限制解压文件大小策略)
*
* @param file 压缩文件
* @param outputfolder 解压后的文件目录
* @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
* @throws Exception IllegalArgumentException 超限抛出的异常
* 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
* 要考虑后面的逻辑,比如告警
*/
public static void unzip(File file, File outputfolder, Long size) throws Exception {
ZipFile zipFile = new ZipFile(file);
FileOutputStream fos = null;
try {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L;
ZipEntry entry;
while (zipEntries.hasMoreElements()) {
// 获取 ZIP 文件的下一个条目
entry = zipEntries.nextElement();
// 将解缩大小累加到 zipFileSize 变量
zipFileSize += entry.getSize();
// 判断解压文件累计大小是否超过指定的大小
if (zipFileSize > size) {
deleteDir(outputfolder);
throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
}
File unzipped = new File(outputfolder, entry.getName());
if (entry.isDirectory() && !unzipped.exists()) {
unzipped.mkdirs();
continue;
} else if (!unzipped.getParentFile().exists()) {
unzipped.getParentFile().mkdirs();
}
fos = new FileOutputStream(unzipped);
InputStream in = zipFile.getInputStream(entry);
byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, count);
}
}
} finally {
if (null != fos) {
fos.close();
}
if (null != zipFile) {
zipFile.close();
}
}
}
/**
* 递归删除目录文件
*
* @param dir 目录
*/
private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
//递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}
}
测试类
:
import java.io.File;
/**
* 文件炸弹测试类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/
public class Test {
public static void main(String[] args) {
File bomb = new File("D:\temp\3\zbsm.zip");
File tempFile = new File("D:\temp\3\4");
try {
FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
} catch (IllegalArgumentException e) {
if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
FileBombUtil.deleteDir(tempFile);
System.out.println("原始文件太大");
} else {
System.out.println("错误的压缩文件格式");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、总结
文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。
文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。
总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。
在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管
。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:
- 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。
- 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。
- 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。
来源:juejin.cn/post/7289667869557178404
百亿补贴为什么用 H5?H5 未来会如何发展?
23 年 11 月末,拼多多市值超过了阿里。我想写一篇文章《百亿补贴为什么用 H5》,没有动笔;24 年新年,我想写一篇《新的一年,H5 会如何发展》,也没有动笔。
眼看着灵感就要烂在手里,我决定把两篇文章合为一篇,与大家分享。当然,这些分析预测只是个人观点,如果你有不同的意见,欢迎在评论区讨论交流。
拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。
百亿补贴为什么用 H5
我们先看两张图,在 Android 手机开发者模式下,开启显示布局边界,可以看到「百亿补贴」是一个完整大框,说明「百亿补贴」在 App 内是 H5;拷贝分享链接,在浏览器打开,可以看到资源中有 react 名字的 js 文件,说明「百亿补贴」技术栈大概率是 React。
不只是拼多多,我特地确认了,京东、淘宝的的「百亿补贴」技术栈也是 H5。
那么,为什么电商巨头会在「百亿补贴」这种重要活动上选择 H5 呢?用 H5 有什么好处呢?
H5 技术已经成熟
第一个原因,也是最基础的原因,就是 H5 技术已经成熟,能够完整地实现功能。具体来说:
浏览器兼容性不断提高
自 2008 年 HTML5 草案发布以来,截止 2024 年,HTML5 已有 16 年历史。16 年间,主流浏览器对 HTML5、CSS3 和 JavaScript 的标准语法兼容性一直持续改进,22 年微软更是亲手盖上了 IE 棺材板。虽然 Safari(iOS 浏览器)的兼容性仍然备受诟病,但总体来说兼容成本已经变得可以接受。
主流框架已经成熟
前端最主流的两大框架 Vue 和 React 已经成熟。它们的成熟体现在多个方面:
- 从时间的角度看,截止 2024 年,React 已经发布了 11 年,而 Vue 已经发布了 10 年。经过多年的发展,前端开发者已经非常熟悉 React 和 Vue,能熟练地应用它们进行开发。
- 从语法的角度看,自 React16.8 发布 Hooks,以及 Vue3 发布 Composition API 以来,两大框架语法基本稳定,不再有大的变化。前端开发者可以更加专注于业务逻辑,无需过多担心框架语法的变动。
- 从未来发展方向看,React 目前致力于推广 React Server Component 1;Vue 则在尝试着无 VDom 的 Vapor 方向,并计划利用 Rust 重写 Vite 2。这表明旧领域不再有大的颠覆,两大框架已经正寻求新的发展领域。
混合开发已经成熟
混合开发是指将原生开发(Android 和 iOS)和 Web 开发结合起来的一种技术。简而言之,它将 H5 嵌入到移动应用内部运行。近些年,业界对混合开发的优势和缺陷已经有清晰的认识,并针对其缺陷进行了相应的优化。具体来说:
- 混合开发的优势包括开发速度快、一套代码适配 Android 和 iOS,以及实现代码的热更新。这意味着程序员能更快地编写跨平台应用,及时更新应用、修复缺陷;
- 混合开发的缺陷则是性能较差、加载受限于网络。针对这个缺陷,各大 App、以及云服务商如阿里云 3 和腾讯云 4 都推出了自己的离线包方案。离线包方案可以将网页的静态资源(如 HTML、CSS、JS、图片等)缓存到本地,用户访问 H5 页面时,可以直接读取本地的离线资源,从而提升页面的加载速度。可以说,接入离线包后,H5 不再有致命缺陷。
前端基建工具已经成熟
近些年来,业界最火的技术话题之一,就是用 Rust 替代前端基建,包括:用 Rust 替代 Webpack 的 Rspack;用 Rust 替代 Babel 的 SWC;用 Rust 替代 Eslint 的 OxcLint 等等。
前端开发者对基建工具抱怨,已经从「这工具能不能用」,转变为「这工具好不好用」。这种「甜蜜的烦恼」,只有基建工具成熟后才会出现。
综上所述,浏览器的兼容性提升、主流框架的成熟、混合开发的发展和前端基建工具的完善,使 H5 完全有能力承载「百亿补贴」业务。
H5 开发成本低
前文我们已经了解到,成熟的技术让 H5 可以实现「百亿补贴」的功能。现在我们介绍另一个原因——H5 开发成本低。
「百亿补贴」需要多个 H5
「百亿补贴」的方式,是一个常住的 H5,搭配上多个流动的 H5。(「常住」和「流动」是我借鉴「常住人口」和「流动人口」造的词)
- 常住 H5 链接保持不变。站外投放的链接基本都是常住 H5 的,站内首页入口链接也是常住 H5 的,这样方便用户二次访问。
- 流动 H5 链接位于常住 H5 的不同位置,比如头图、侧边栏等。时间不同、用户不同、算法不同,流动 H5 的链接都会不同,流动 H5 可以区分用户,方便分发流量。
具体来看,拼多多至少有三个流量的分发点,第一个是可点击的头图,第二个是列表上方的活动模块,第三个是右侧浮动的侧边栏,三者可以投放不同的链接。最近就分别投放 3.8 女神节链接、新人链接和品牌链接:
「百亿补贴」需要及时更新
不难想到,每到一个节日、每换一个品牌,「百亿补贴」就需要更新一次。
有时还需要为一些品牌定制化 H5 代码。如果使用其他技术栈,排期跟进通常会比较困难,但是使用 H5 就能够快速迭代并上线。
H5 投放成本低
我们已经「百亿补贴」使用 H5 技术栈的两个原因,现在来看第三个原因——H5 适合投放。
拼多多的崛起过程中,投放到其他 App 的链接功不可没。早期它通过微信等社交平台「砍一刀」的模式,低成本地吸引了大量用户。如今,它通过投放「百亿补贴」策略留住用户。
H5 的独特之处,在于它能够灵活地在多个平台上进行投放,其他技术栈很难有这样的灵活性。即使是今天,抖音、Bilibili 和小红书等其他 App 中,「百亿补贴」的 H5 链接也随处可见。
拼多多更是将 H5 这种灵活性发挥到极致,只要你有「百亿补贴」的链接,你甚至可以在微信、飞书、支付宝等地方直接查看「百亿补贴」 H5 页面。
综上所述,能开发、能快速开发、且开发完成后能大量投放,是「百亿补贴」青睐 H5 的原因。
H5 未来会如何发展
了解「百亿补贴」选择 H5 的原因后,我们来看看电商巨头对 H5 未来发展的影响。我认为有三个影响:
H5 数量膨胀,定制化要求苛刻
C 端用户黏性相对较低,换一个 App 的成本微不足道。近年 C 端市场增长缓慢,企业重点从获取更多的新客变成留住更多的老客,很难容忍用户丢失。因此其他企业投放活动 H5 时,企业必须也投放活动 H5,电商活动 H5 就变得越来越多。
这个膨胀的趋势不仅仅存在于互联网巨头的 App 中,中小型应用也不例外,甚至像 12306、中国移动、招商银行这种工具性极强的应用也无法幸免。
随着市场的竞争加剧,定制化要求也变得越来越苛刻,目的是让消费者区分各种活动。用互联网黑话来说,就是「建立用户心智」。在可预见的未来,尽管电商活动 H5 结构基本相同,但是它们的外观将变得千差万别、极具个性。
SSR 比例增加,CSR 占据主流
在各家 H5 数量膨胀、竞争激烈的情况下,一定会有企业为提升 H5 的秒开率接入 SSR,因此 SSR 的比例会增加。
但我认为 CSR 依然会是主流,主要是因为两个原因:
- SSR 需要额外的服务器费用,包括服务器的维护、扩容等。这对于中小型公司来说是一个负担。
- SSR 对程序员技术水平要求比 CSR 更高。SSR 需要程序员考虑更多的问题,例如内存泄露。使用 CSR 在用户设备上发生内存泄露,影响有限;但是如果在服务器上发生内存泄露,则是会占用公司的服务器内存,增加额外的成本和风险。
因此,收益丰厚、技术雄厚的公司才愿意使用 SSR。
Monorepo 比例会上升,类 Shadcn UI 组件库也许会兴起
如前所述,H5 的数量膨胀,代码复用就会被着重关注。我猜测更多企业会选择 Monorepo 管理方式。所谓 Monorepo,简单来说,就是将原本应该放到多个仓库的代码放入一个仓库,让它们共享相同的版本控制。这样可以降低代码复用成本。
定制化要求苛刻,我猜测社区中类似 Shadcn UI 的 H5 组件库或许会兴起。现有的 H5 组件库样式太单一,即使是 Shadcn UI,也很难满足国内 H5 的定制化需求。然而,Shadcn UI 的基本思路——「把源码下载到项目」,是解决定制化组件难复用的问题的好思路。因此,我认为类似 Shadcn 的 H5 组件库可能会逐渐兴起。
总结
本文介绍了我认为「百亿补贴」会选用 H5 的三大原因:
- H5 技术已经成熟
- H5 开发成本低
- H5 投放成本低
以及电商巨头对 H5 产生的三个影响:
- 数量膨胀,定制化要求苛刻
- SSR 比例增加,CSR 占据主流
- Monorepo 比例增加,类 Shadcn UI 组件库也许会兴起
总而言之,H5 开发会越来越专业,对程序员要求会越来越高。至于这种情况是好是坏,仁者见仁智者见智,欢迎大家在评论区沟通交流。
拳打 H5,脚踢小程序。我是「小霖家的混江龙」,关注我,带你了解更多实用的 H5、小程序武学。
Footnotes
来源:juejin.cn/post/7344325496983732250
一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO
在现代软件架构中,不同类型的类扮演着不同的角色,共同构成了一个清晰、模块化和可维护的系统。以下是对实体类(Entity)、数据传输对象(DTO)、领域对象(Domain Object)、持久化对象(Persistent Object)、业务对象(Business Object)、应用对象(Application Object)、数据访问对象(Data Access Object, DAO)、服务层(Service Layer)和控制器层(Controller Layer)的总体介绍:
不同领域作用
POJO (Plain Old Java Object)
- 定义:POJO 是一个简单的Java对象,它不继承任何特定的类或实现任何特定的接口,除了可能实现
java.io.Serializable
接口。它通常用于表示数据,不包含业务逻辑。 - 案例的体现:
UserEntity
类可以看作是一个POJO,因为它主要包含数据字段和标准的构造函数、getter和setter方法。UserDTO
类也是一个POJO,它用于传输数据,不包含业务逻辑。
VO (Value Object)
- 定义:VO 是一个代表某个具体值或概念的对象,通常用于表示传输数据的结构,不涉及复杂的业务逻辑。
VO(View Object)的特点:
- 展示逻辑:VO通常包含用于展示的逻辑,例如格式化的日期或货币值。
- 用户界面相关:VO设计时会考虑用户界面的需求,可能包含特定于视图的属性。
- 可读性:VO可能包含额外的描述性信息,以提高用户界面的可读性。
实体类(Entity)
- 作用:代表数据库中的一个表,是数据模型的实现,通常与数据库表直接映射。
- 使用场景:当需要将应用程序的数据持久化到数据库时使用。
数据传输对象(DTO)
- 作用:用于在应用程序的不同层之间传输数据,特别是当需要将数据从服务层传输到表示层或客户端时。
- 使用场景:进行数据传输,尤其是在远程调用或不同服务间的数据交换时。
领域对象(Domain Object)
- 作用:代表业务领域的一个实体或概念,通常包含业务逻辑和业务状态。
- 使用场景:在业务逻辑层处理复杂的业务规则时使用。
持久化对象(Persistent Object)
- 作用:与数据存储直接交互的对象,通常包含数据访问逻辑。
- 使用场景:执行数据库操作,如CRUD(创建、读取、更新、删除)操作。
业务对象(Business Object)
- 作用:封装业务逻辑和业务数据,通常与领域对象交互。
- 使用场景:在业务逻辑层实现业务需求时使用。
应用对象(Application Object)
- 作用:封装应用程序的运行时配置和状态,通常不直接与业务逻辑相关。
- 使用场景:在应用程序启动或运行时配置时使用。
数据访问对象(Data Access Object, DAO)
- 作用:提供数据访问的抽象接口,定义了与数据存储交互的方法。
- 使用场景:需要进行数据持久化操作时,作为数据访问层的一部分。
服务层(Service Layer)
- 作用:包含业务逻辑和业务规则,协调应用程序中的不同组件。
- 使用场景:处理业务逻辑,执行业务用例。
控制器层(Controller Layer)
- 作用:处理用户的输入,调用服务层的方法,并返回响应结果。
- 使用场景:处理HTTP请求和响应,作为Web应用程序的前端和后端之间的中介。
案例介绍
- 用户注册:
- DTO:用户注册信息的传输。
- Entity:用户信息在数据库中的存储形式。
- Service Layer:验证用户信息、加密密码等业务逻辑。
- 商品展示:
- Entity:数据库中的商品信息。
- DTO:商品信息的传输对象,可能包含图片URL等不需要存储在数据库的字段。
- Service Layer:获取商品列表、筛选和排序商品等。
- 订单处理:
- Domain Object:订单的业务领域模型,包含订单状态等。
- Business Object:订单处理的业务逻辑。
- DAO:订单数据的持久化操作。
- 配置加载:
- Application Object:应用程序的配置信息,如数据库连接字符串。
- API响应:
- Controller Layer:处理API请求,调用服务层,返回DTO作为响应。
案例代码
视图对象(VO)
一个订单系统,我们需要在用户界面展示订单详情:
// OrderDTO - 数据传输对象
public class OrderDTO {
private Long id;
private String customerName;
private BigDecimal totalAmount;
// Constructors, getters and setters
}
// OrderVO - 视图对象
public class OrderVO {
private Long id;
private String customerFullName; // 格式化后的顾客姓名
private String formattedTotal; // 格式化后的总金额,如"$1,234.56"
private String orderDate; // 格式化后的订单日期
// Constructors, getters and setters
public OrderVO(OrderDTO dto) {
this.id = dto.getId();
this.customerFullName = formatName(dto.getCustomerName());
this.formattedTotal = formatCurrency(dto.getTotalAmount());
this.orderDate = formatDateTime(dto.getOrderDate());
}
private String formatName(String name) {
// 实现姓名格式化逻辑
return name;
}
private String formatCurrency(BigDecimal amount) {
// 实现货币格式化逻辑
return "$" + amount.toString();
}
private String formatDateTime(Date date) {
// 实现日期时间格式化逻辑
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}
}
实体类(Entity)
package com.example.model;
import javax.persistence.*;
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
// Constructors, getters and setters
public UserEntity() {}
public UserEntity(String username, String password) {
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
数据传输对象(DTO)
package com.example.dto;
public class UserDTO {
private Long id;
private String username;
// Constructors, getters and setters
public UserDTO() {}
public UserDTO(Long id, String username) {
this.id = id;
this.username = username;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
领域对象(Domain Object)
package com.example.domain;
public class UserDomain {
private Long id;
private String username;
// Business logic methods
public UserDomain() {}
public UserDomain(Long id, String username) {
this.id = id;
this.username = username;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
// Additional domain-specific methods
}
领域对象通常包含业务领域内的概念和逻辑。在订单系统中,这可能包括订单状态、订单项、总价等。
package com.example.domain;
import java.util.List;
public class OrderDomain {
private String orderId;
private List items; // 订单项列表
private double totalAmount;
private OrderStatus status; // 订单状态
// Constructors, getters and setters
public OrderDomain(String orderId, List items) {
this.orderId = orderId;
this.items = items;
this.totalAmount = calculateTotalAmount();
this.status = OrderStatus.PENDING; // 默认状态为待处理
}
private double calculateTotalAmount() {
double total = 0;
for (OrderItemDomain item : items) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
// 业务逻辑方法,例如更新订单状态
public void processPayment() {
// 处理支付逻辑
if (/* 支付成功条件 */) {
this.status = OrderStatus.PAYMENT_COMPLETED;
}
}
// 更多业务逻辑方法...
}
持久化对象(Persistent Object)
package com.example.model;
public class UserPersistent extends UserEntity {
// Methods to interact with persistence layer, extending UserEntity
}
业务对象(Business Object)
package com.example.service;
public class UserBO {
private UserDomain userDomain;
public UserBO(UserDomain userDomain) {
this.userDomain = userDomain;
}
// Business logic methods
public void performBusinessLogic() {
// Implement business logic
}
}
OrderBO
业务对象通常封装业务逻辑,可能包含领域对象,并提供业务操作的方法。
package com.example.service;
import com.example.domain.OrderDomain;
public class OrderBO {
private OrderDomain orderDomain;
public OrderBO(OrderDomain orderDomain) {
this.orderDomain = orderDomain;
}
// 执行订单处理的业务逻辑
public void performOrderProcessing() {
// 例如,处理订单支付
orderDomain.processPayment();
// 其他业务逻辑...
}
// 更多业务逻辑方法...
}
应用对象(Application Object)
package com.example.config;
public class AppConfig {
private String environment;
private String configFilePath;
public AppConfig() {
// Initialize with default values or environment-specific settings
}
// Methods to handle application configuration
public void loadConfiguration() {
// Load configuration from files, environment variables, etc.
}
// Getters and setters
}
数据访问对象(Data Access Object)
package com.example.dao;
import com.example.model.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDAO extends JpaRepository {
// Custom data access methods if needed
UserEntity findByUsername(String username);
}
- OrderDAO
DAO 提供数据访问的抽象接口,定义了与数据存储交互的方法。在Spring Data JPA中,可以继承JpaRepository
并添加自定义的数据访问方法。
package com.example.dao;
import com.example.domain.OrderDomain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderDAO extends JpaRepository { // 主键类型为String
// 自定义数据访问方法,例如根据订单状态查询订单
List findByStatus(OrderStatus status);
}
服务层(Service Layer)
package com.example.service;
import com.example.dao.UserDAO;
import com.example.dto.UserDTO;
import com.example.model.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserDAO userDAO;
@Autowired
public UserService(UserDAO userDAO) {
this.userDAO = userDAO;
}
public UserDTO getUserById(Long id) {
UserEntity userEntity = userDAO.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
return convertToDTO(userEntity);
}
private UserDTO convertToDTO(UserEntity entity) {
UserDTO dto = new UserDTO();
dto.setId(entity.getId());
dto.setUsername(entity.getUsername());
return dto;
}
// Additional service methods
}
OrderService
服务层协调用户输入、业务逻辑和数据访问。它使用DAO进行数据操作,并可能使用业务对象来执行业务逻辑。
package com.example.service;
import com.example.dao.OrderDAO;
import com.example.domain.OrderDomain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OrderService {
private final OrderDAO orderDAO;
@Autowired
public OrderService(OrderDAO orderDAO) {
this.orderDAO = orderDAO;
}
public List findAllOrders() {
return orderDAO.findAll();
}
public OrderDomain getOrderById(String orderId) {
return orderDAO.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found"));
}
public void processOrderPayment(String orderId) {
OrderDomain order = getOrderById(orderId);
OrderBO orderBO = new OrderBO(order);
orderBO.performOrderProcessing();
// 更新订单状态等逻辑...
}
// 更多服务层方法...
}
控制器层(Controller Layer)
package com.example.controller;
import com.example.dto.UserDTO;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
// Additional controller endpoints
}
总结
这些不同类型的类和层共同构成了一个分层的软件架构,每一层都有其特定的职责和功能。这种分层方法有助于降低系统的复杂性,提高代码的可维护性和可扩展性。通过将业务逻辑、数据访问和用户界面分离,开发人员可以独立地更新和测试每个部分,从而提高开发效率和应用程序的稳定性。
历史热点文章
- 命令模式(Command Pattern):网络爬虫任务队列实战案例分析
- 迭代器模式(Iterator Pattern):电商平台商品分类浏览实战案例分析
- 中介者模式(Mediator Pattern):即时通讯软件实战案例分析
- 备忘录模式(Memento Pattern):游戏存档系统实战案例分析
- 状态模式(State Pattern):电商平台订单状态管理实战案例分析
- 责任链模式(Chain of Responsibility Pattern):电商平台的订单审批流程实战案例分析
- 访问者模式(Visitor Pattern):电商平台商品访问统计实战案例分析
- 工厂方法模式(Factory Method Pattern): 电商多种支付实战案例分析
- 抽象工厂模式(Abstract Factory Pattern):多风格桌面应用实战案例分析
- 建造者模式(Builder Pattern): 在线订单系统实战案例分析
- 原型模式(Prototype Pattern): 云服务环境配置实战案例分析
- 适配器模式(Adapter Pattern):第三方支付集成实战案例分析
- 装饰器模式(Decorator Pattern):电商平台商品价格策略实战案例分析
- 单例模式(Singleton Pattern):购物车实战案例分析
来源:juejin.cn/post/7389212915302105098
七年前的一个思维,彻底改变了我的程序员职场轨迹
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
最近学习了12个生财思维,受益匪浅,但是纸上得来终觉浅,绝知此事要躬行,没有亲身实践,怎么能更好的理解呢?
单纯的学习,尤其是思维工具类的学习,只看但不实践,是不会有太好的效果的。
课程中的案例虽然真实,但是每个人的眼界、能力不同,所以案例对自己只能开开眼,但自己对于思维模式的理解却不会有太多的帮助。
为了更好的理解每一个生财思维,我决定根据每一个生财思维去复盘过去十年间遇到的机遇,看看自己错过了什么,有抓住了什么,然后把学习过程中的思考重新整理出来。
希望对你有所帮助。
对标思维
什么是对标思维?
对标思维,对,对比。标,标杆。
什么是标杆,领域里面做的好的,拿到成绩的,就是标杆。
如何真正的运用好对标思维?
分成三步:
- 第一步,找到对标,通过搜索、付费都可以找到领域内出色的人。
- 第二步,研究他的成功路径,比如他做了哪些事情,学习了哪些知识,遇到了什么障碍, 又是如何解决的,或者关注他有什么好的特质,值得你去学习。
- 第三步,复盘自己的做事路径和方法,对比标杆的方法、思维、路径,针对性的改进自己。
对标思维,每一个人都在下意识的应用到。
比如:
- 技术方案设计,我们对标大厂去设计我们的方案,为什么大厂要这样设计
- 身边的同事去了大厂,我们对标他,去看看他作对了什么,学习了什么知识
- 接手新的工作,先去看看别人是怎么做的
对标并不是对比,对比着重于把两个或多个事物放在一起,比较它们之间的相同点和不同点。
而对标更强调以优秀对象为目标进行追赶和超越
对标思维的应用
大学毕业后,我选择留在了老家,我的室友毕业去了北京。他去做了互联网,而我只能选择传统信息企业。
北京的生活节奏与二线城市很不一样,上学时我们一起开黑打游戏,毕业后,只剩下了留在济南的几个人,还能一起开黑娱乐,室友因为工作原因,工作日下班几乎无法和我们打游戏。
后来无意间得知,他的薪资比我高出一倍之多,我感到很震惊。(此时我无意间找到了对标)
同样的大学,同样的专业,相差不大的成绩,在我知道后,我不禁陷入思考,为什么收入会有着如此大的差距?
上面提到,对标并不是对比,如果仅仅从薪资角度来对比,那我们之间的差距无疑也太大了。
运用对标思维,我发现他做的对的事情,是去了北京。17年的互联网行业依然如日中天,大城市的机会、薪资,都是二线城市无法比较的,彼时只要有一些热门技术的经验,哪怕是培训机构出来的,都很容易找到工作。
我不禁反思,为什么当时的我毕业时,为什么没有作出这个选择?
大学同学,大概都来源于五湖四海,而我的室友的家在东北,他对我们所在大学的城市,一定是不会有太多的感情的,所以对他而言,毕业后,自然是像北京这样的大城市,有着更大的吸引力。
而彼时的我,还处于一个舒适区里。我从上学开始就没离开过济南,外面的城市对我而言是陌生的,而在家不用租房,上下班有车开不需要坐地铁,周末有朋友一起吃饭喝酒,上班时间因为城市不大所以也不远,因此缺少了闯荡的勇气。
当我认识到这一点后,便暗暗下决心要出去闯闯看看,随后没过多久,我就在没有任何准备的情况下,裸辞去了北京。
当然因为缺少准备,我吃了不少苦头,不过这个都是后话了,有兴趣的也可以看看我之前的文章。
这件事已经过去了7年之久,我依然记忆犹新,从对标、思考、选择的路径,可以很顺畅的把这件事情完整的回忆出来,因为我在不经意间用了对标思维,做出了很大的改变,也让我认识了更多的朋友,去了更大规模的公司。
看到这里,我猜你可能心里会想,你这个思考路径,并不一定有太大的参考价值呀。你的对标不用你寻找,就在你身边,并且薪资对比如此明显,你自然很容易就会去思考如何去改变。
但是我想说,想主动应用对标思维,没有这么简单。
故事继续。
找到目标
从小带着我玩到大的哥哥,也是程序员,我选择程序员行业,也是受到了我哥哥的影响。
在那个我还没有进入社会,沉浸在大学的美好时光中的时候,哥哥就已经从腾讯跳槽到阿里,并且在工作的城市买房、定居了。
其实我哥哥也不只一次的和我讲过,提前准备好面试内容,参加校招,可能会比毕业后再进入大厂的难度要小上很多。
但我始终没有听进去,在学习了一些内容之后,便放弃了。
理论上,我在上学的时候,就已经有了这么好的一个榜样,我如果早早去对标哥哥的工作路径,了解大厂的面试标准,毕业就选择进入大厂,我几乎可以少走4年的弯路,但我还是在毕业后,选择留在了济南。
瑞·达利欧在《原则》书中提到,
个人进化过程(即我在上一条描述的循环)通过5个不同的步骤发生。如果你能把那5件事都做好,你几乎肯定可以成功。
这五步大概是:
1.有明确的目标。
2.找到阻碍你实现这些目标的问题,并且不容忍问题。
3.准确诊断问题,找到问题的根源。
4.规划可以解决问题的方案。
5.做一切必要的事来践行这些方案,实现成果。
我们就聊第一步,有明确的目标。
大四那年我在干什么?尝试着考了研,尝试着去了一家公司实习,但大部分时间还是在打游戏。错过了秋招春招,最后毕业才拿到一家offer,只能选择入职。
大学期间,我很明显是没有目标的。比如,毕业后想去什么样的公司,想拥有多少收入,去哪个城市发展。
因此即使有再好的榜样在我身边,我也无动于衷。
但当我工作之后,发现了技术上的差距,发现了工资上的差距,发现了工作环境上的差距,自己不想再浑浑噩噩下去,因此才有了改变的动力。
因此让心里埋下一个想法的种子,找到目标,才是运用思维工具的第一步。
具体如何找到目标,进行拆分,可以看看我之前的这篇文章。
说在最后
好了,文章到这里就要结束了,总结一下。
对标思维从概念上看其实不难,只需要三步即可找到对标,研究路径,复盘改进。
但是并非有了这个思维就可以立即应用,还是要有明确的目标,知道自己想要什么,才能更好的利用对标思维。
欢迎你在评论区和我分享,也希望你点赞、评论、收藏,让我知道对你有所收获,这对我来说很重要。也欢迎你加我的wx:Ldhrlhy10,一起交流~
本篇文章是第42篇原创文章,2024目标进度42/100,欢迎有趣的你,关注我。
来源:juejin.cn/post/7388488504055955492
我去,怎么http全变https了
项目场景:
在公司做的一个某地可视化项目。
部署采用的是前后端分离部署,图片等静态资源请求一台minio服务器。
项目平台用的是http
图片资源的服务器用的是https
问题描述
在以https请求图片资源时,图片请求成功报200。
【现象1】: 继图片后续的请求,后续此域名和子域名下的的url均由http变为https
【现象2】: 界面阻塞报错,无法交互
原因分析:
经过现象查阅,发现出现该现象与浏览器的HSTS有关。
什么是HSTS ?
HTTP的Strict-Transport-Security
(HSTS)请求头是一种网络安全机制,用于告诉浏览器仅通过HTTPS与服务器通信,而不是HTTP。它的作用主要有以下几点:
- 防止协议降级攻击:当浏览器接收到HSTS响应头后,它会将该网站添加到HSTS列表中,并在后续的访问中强制使用HTTPS,即使用户或攻击者尝试通过HTTP访问该网站,浏览器也会自动将其重定向到HTTPS。
- 减少中间人攻击的风险:通过确保所有通信都通过加密的HTTPS进行,可以降低中间人攻击(MITM)的风险,因为攻击者无法轻易地截获或篡改传输的数据。
- 提高网站的安全性:HSTS可以作为网站安全策略的一部分,帮助保护用户的敏感信息,如登录凭据、支付信息等。
- 简化安全配置:对于网站管理员来说,HSTS可以减少需要维护的安全配置,因为浏览器会自动处理HTTPS的重定向。
- 提高用户体验:由于浏览器会自动处理重定向,用户不需要担心访问的是HTTP还是HTTPS版本,可以更顺畅地浏览网站。
HSTS的配置可以通过max-age
指令来设置浏览器应该记住这个策略的时间长度,还可以使用includeSubDomains
指令来指示所有子域名也应该遵循这个策略。此外,还有一个preload
选项,允许网站所有者将他们的网站添加到浏览器的预加载HSTS列表中,这样用户在第一次访问时就可以立即应用HSTS策略。
于是在我发现该相关的响应头确有此物
解决方案:
那就取决于服务器是在哪里设置的该请求头。可能是在Nginx
,Lighttpd
,PHP
等等,将该响应头配置去除
来源:juejin.cn/post/7382386471272448035
再有人问你WebSocket为什么牛逼,就把这篇文章发给他!
点赞再看,Java进阶一大半
2008年6月诞生了一个影响计算机世界的通信协议,原先需要二十台计算机资源才能支撑的业务场景,现在只需要一台,这得帮"抠门"老板们省下多少钱,它就是大名鼎鼎的WebSocket协议。很快在下一年也就是2009年的12月,Google浏览器就宣布成为第一个支持WebSocket标准的浏览器。
WebSocket的推动者和设计者就是下面的Michael Carter,他设计的WebSocket协议技术现在每天在全地球有超过20亿的设备在使用。
逮嘎猴,我是南哥。
一个Java进阶的领路人,今天指南的是WebSocket,跟着南哥我们一起Java进阶。
本文收录在我开源的《Java进阶指南》中,一份帮助小伙伴们进阶Java、通关面试的Java学习面试指南,相信能帮助到你在Java进阶路上不迷茫。南哥希望收到大家的 ⭐ Star ⭐支持,这是我创作的最大动力。GitHub地址:github.com/hdgaadd/Jav…。
1. WebSocket概念
1.1 为什么会出现WebSocket
面试官:有了解过WebSocket吗?
一般的Http请求我们只有主动去请求接口,才能获取到服务器的数据。例如前后端分离的开发场景,自嘲为切图仔实际扮猪吃老虎的前端大佬找你要一个配置信息
的接口,我们后端开发三下两下开发出一个RESTful
架构风格的API接口,只有当前端主动请求,后端接口才会响应。
但上文这种基于HTTP的请求-响应模式并不能满足实时数据通信的场景,例如游戏、聊天室等实时业务场景。现在救世主来了,WebSocket作为一款主动推送技术,可以实现服务端主动推送数据给客户端。大家有没听说过全双工、半双工的概念。
全双工通信允许数据同时双向流动,而半双工通信则是数据交替在两个方向上传输,但在任一时刻只能一个方向上有数据流动
HTTP通信协议就是半双工,而数据实时传输需要的是全双工通信机制,WebSocket采用的便是全双工通信。举个微信聊天的例子,企业微信炸锅了,有成百条消息轰炸你手机,要实现这个场景,大家要怎么设计?用iframe、Ajax异步交互技术配合以客户端长轮询不断请求服务器数据也可以实现,但造成的问题是服务器资源的无端消耗,运维大佬直接找到你工位来。显然服务端主动推送数据的WebSocket技术更适合聊天业务场景。
1.2 WebSocket优点
面试官:为什么WebSocket可以减少资源消耗?
大家先看看传统的Ajax长轮询和WebSocket性能上掰手腕谁厉害。在websocket.org网站提供的Use Case C
的测试里,客户端轮询频率为10w/s,使用Poling长轮询每秒需要消耗高达665Mbps,而我们的新宠儿WebSocet仅仅只需要花费1.526Mbps,435倍的差距!!
为什么差距会这么大?南哥告诉你,WebSocket技术设计的目的就是要取代轮询技术和Comet技术。Http消息十分冗长和繁琐,一个Http消息就要包含了起始行、消息头、消息体、空行、换行符,其中请求头Header非常冗长,在大量Http请求的场景会占用过多的带宽和服务器资源。
大家看下百度翻译接口的Http请求,拷贝成curl命令是非常冗长的,可用的消息肉眼看过去没多少。
curl ^"https://fanyi.baidu.com/mtpe-individual/multimodal?query=^%^E6^%^B5^%^8B^%^E8^%^AF^%^95&lang=zh2en^" ^
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" ^
-H "Accept-Language: zh-CN,zh;q=0.9" ^
-H "Cache-Control: max-age=0" ^
-H "Connection: keep-alive" ^
-H ^"Cookie: BAIDUID=C8FA8569F446CB3F684CCD2C2B32721E:FG=1; BAIDUID_BFESS=C8FA8569F446CB3F684CCD2C2B32721E:FG=1; ab_sr=1.0.1_NDhjYWQyZmRjOWIwYjI3NTNjMGFiODExZWFiMWU4NTY4MjA2Y2UzNGQwZjJjZjI1OTdlY2JmOThlNzk1ZDAxMDljMTA2NTMxYmNlM1OTQ1MTE0ZTI3Y2M0NTIzMzdkMmU2MGMzMjc1OTRiM2EwNTJQ==; RT=^\^"z=1&dm=baidu.com&si=b9941642-0feb-4402-ac2b-a913a3eef1&ss=ly866fx&sl=4&tt=38d&bcn=https^%^3A^%^2F^%^2Ffclog.baidu.com^%^2Flog^%^2Fweirwood^%^3Ftype^%^3Dp&ld=ccy&ul=jes^\^"^" ^
-H "Sec-Fetch-Dest: document" ^
-H "Sec-Fetch-Mode: navigate" ^
-H "Sec-Fetch-Site: same-origin" ^
-H "Sec-Fetch-User: ?1" ^
-H "Upgrade-Insecure-Requests: 1" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" ^
-H ^"sec-ch-ua: ^\^"Not/A)Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"126^\^", ^\^"Google Chrome^\^";v=^\^"126^\^"^" ^
-H "sec-ch-ua-mobile: ?0" ^
-H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" &
而WebSocket是基于帧传输的,只需要做一次握手动作就可以让客户端和服务端形成一条通信通道,这仅仅只需要2个字节。我搭建了一个SpringBoot集成的WebSocket项目,浏览器拷贝WebSocket的Curl命令十分简洁明了,大家对比下。
curl "ws://localhost:8080/channel/echo" ^
-H "Pragma: no-cache" ^
-H "Origin: http://localhost:8080" ^
-H "Accept-Language: zh-CN,zh;q=0.9" ^
-H "Sec-WebSocket-Key: VoUk/1sA1lGGgMElV/5RPQ==" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" ^
-H "Upgrade: websocket" ^
-H "Cache-Control: no-cache" ^
-H "Connection: Upgrade" ^
-H "Sec-WebSocket-Version: 13" ^
-H "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits"
如果你要区分Http请求或是WebSocket请求很简单,WebSocket请求的请求行前缀都是固定是ws://
。
2. WebSocket实践
2.1 集成WebSocket服务器
面试官:有没动手实践过WebSocket?
大家要在SpringBoot使用WebSocket的话,可以集成spring-boot-starter-websocket
,引入南哥下面给的pom依赖。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
dependencies>
感兴趣点开spring-boot-starter-websocket
依赖的话,你会发现依赖所引用包名为package jakarta.websocket
。这代表SpringBoot其实是集成了Java EE开源的websocket项目。这里有个小故事,Oracle当年决定将Java EE移交给Eclipse基金会后,Java EE就进行了改名,现在Java EE更名为Jakarta EE。Jakarta是雅加达的意思,有谁知道有什么寓意吗,评论区告诉我下?
我们的程序导入websocket依赖后,应用程序就可以看成是一台小型的WebSocket服务器。我们通过@ServerEndpoint可以定义WebSocket服务器对客户端暴露的接口。
@ServerEndpoint(value = "/channel/echo")
而WebSocket服务器要推送消息给到客户端,则使用package jakarta.websocket
下的Session对象,调用sendText
发送服务端消息。
private Session session;
@OnMessage
public void onMessage(String message) throws IOException{
LOGGER.info("[websocket] 服务端收到客户端{}消息:message={}", this.session.getId(), message);
this.session.getAsyncRemote().sendText("halo, 客户端" + this.session.getId());
}
看下getAsyncRemote
方法返回的对象,里面是一个远程端点实例。
RemoteEndpoint.Async getAsyncRemote();
2.2 客户端发送消息
面试官:那客户端怎么发送消息给服务器?
客户端发送消息要怎么操作?这点还和Http请求很不一样。后端开发出接口后,我们在Swagger填充参数,点击Try it out
,Http请求就发过去了。
但WebSocket需要我们在浏览器的控制台上操作,例如现在南哥要给我们的WebSocket服务器发送Halo,JavaGetOffer
,可以在浏览器的控制台手动执行以下命令。
websocket.send("Halo,JavaGetOffer");
实践的操作界面如下。
来源:juejin.cn/post/7388025457821810698
前端开发中过度封装的现象与思考
前言
作为公司内的一名高级前端码喽,大大小小也封装过了不少组件和功能,我逐渐意识到封装并非全是优点,也会存在一些不可忽视的潜在劣势。
在项目中,我们急切地对各种功能和 UI 进行封装,却在不经意间忽略了封装可能带来的额外成本与潜在问题。比如,在之前的一个项目中,为了实现一个看似简单的列表展示功能,我将数据获取、渲染逻辑以及交互处理都塞进了一个繁杂的组件中。后续当需要对列表的某一特定功能进行细微调整时,由于封装的过度复杂,修改工作变得极为棘手,耗费了大量时间去梳理内部的逻辑关系。
还有一次我在对一个表单验证功能的封装时,为追求过高的通用性,添加了过多的配置选项和繁杂的验证规则。这不但增加了代码量,还使得新加入团队的成员在使用时感到困惑,理解和运用这个封装的成本大幅提高。如果让我在写标准代码和学习过度封装的组件之间做选择,我绝对毫不犹豫的选择写标准代码。
一、前端功能封装的优势
- 可以提高代码复用性
在众多项目中,常碰到类似的数据请求、表单验证等功能需求。将这些功能封装成独立的函数或模块,能极大提升代码的复用程度。例如,我们成功封装了一个通用的数据获取函数,在不同页面中仅需传入各异的参数,就能顺利获取所需数据,无需反复编写请求逻辑。 - 有效增强代码的可维护性
封装后的功能代码相对独立,当需要对功能进行修改或优化时,只需在封装的模块内操作,不会对其他使用该功能的部分产生任何影响。如此一来,代码的维护工作变得更加清晰、易于掌控。 - 大幅提升代码的可读性
通过为封装的功能赋予清晰、有意义的函数名和详尽的参数说明,其他开发者能够迅速理解其功能和使用方式,这样也极大提高团队协作的效率。写到这里我突然想起曾经在一个屎山项目中看到过的aaaa、Areyouok、jiashizheng等变量和函数名,我花了好久的时间才把它们修改正常...
二、前端功能封装的劣势
- 事极必反
有时为追求极致的封装效果,可能会对一些简单且复用频率不高的功能进行封装,这反倒会增加代码的复杂程度和理解成本。例如,一个仅仅用于计算两个数之和的简单功能,若过度封装,可能会令后续的开发者感到迷茫。
代码示例:
function add(a, b) {
return a + b;
}
// 过度封装
function complexAdd(a, b) {
if (typeof a!== 'number' || typeof b!== 'number') {
throw new Error('输入必须为数字');
}
const result = a + b;
// 一些额外的复杂逻辑
return result;
}
在一个小型项目中,仅仅为了计算两个数字的和,使用了复杂的封装函数
complexAdd
,导致新同事在理解和使用时花费了过多时间,而原本简单的add
函数就能满足需求。 - 可能隐藏底层实现细节
过度封装或许会让使用功能的开发者对其内部实现一无所知。当问题出现时,可能需要耗费更多时间去理解封装内部的逻辑,进而影响问题的排查和解决效率。
三、UI 二次封装的优势
- 成功统一风格和交互
在大型项目中,保障 UI 的一致性至关重要。通过对基础 UI 组件进行二次封装,能够明确统一的样式、交互行为和响应式规则。例如,对按钮组件进行二次封装,设定不同状态下的颜色、尺寸和点击效果。
- 显著提高开发效率
开发人员能够直接运用封装好的 UI 组件,迅速搭建页面,无需在样式和交互的调整上耗费大量时间。
- 方便后期维护和更新
当需要对 UI 进行整体风格的调整或优化时,只需修改封装的组件,所有使用该组件的页面都会自动更新,大幅减少了维护的工作量。
四、UI 二次封装的劣势
- 过度封装的危害
- 增加不必要的代码量和复杂度,致使应用的加载性能降低。例如,一个简单的输入框组件,如果过度封装了很多复杂的逻辑和样式,可能会使代码体积过大。
- 可能引入过多的抽象层次,让代码变得难以理解和调试。复杂的封装结构可能让开发者在排查 UI 问题时感到无从下手。
- 过度复杂的封装在频繁的渲染和更新操作中,可能会导致性能瓶颈,影响用户体验。
代码示例:
// 过度封装的输入框组件
class OverlyComplexInput extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
handleChange = (e) => {
// 复杂的处理逻辑
this.setState({ value: e.target.value });
// 更多的额外操作
}
render() {
return (
<input
value={this.state.value}
onChange={this.handleChange}
// 过多的样式和属性设置
/>
);
}
}
在一个性能要求较高的页面中,使用了过度封装的输入框组件,导致页面加载缓慢,用户输入时出现明显的卡顿。
- 灵活性受限
过于严格的封装可能限制了开发者在特定场景下对 UI 进行个性化定制的能力。有时候,某些页面可能需要独特的样式或交互效果,而过度封装的组件无法满足这些特殊需求。 - 版本兼容性问题
当对封装的 UI 组件进行更新时,可能会与之前使用该组件的页面产生兼容性问题。新的版本可能改变了组件的行为、样式或接口,导致使用旧版本组件的页面出现显示异常或功能失效。
所以在实际的开发过程中,我们需要权衡封装带来的好处和潜在的问题。封装应该是有针对性的,基于实际的复用需求和项目的规模。同时,要保持封装的适度性,避免过度封装带来的负面影响。只有这样,才能真正提高前端开发的效率和质量。
来源:juejin.cn/post/7387731346733121551
MySQL 9.0 创新版发布,大失所望。。
大家好,我是程序员鱼皮。2024 年 7 月 1 日,MySQL 发布了 9.0 创新版本。区别于我们大多数开发者常用的 LTS(Long-Term Support)长期支持版本,创新版本的发布会更频繁、会更快地推出新的特性和变更,可以理解为 “尝鲜版”,适合追求前沿技术的同学体验。
我通过阅读官方文档,完整了解了本次发布的新特性,结果怎么说呢,唉,接着往下看吧。。。
下面鱼皮带大家 “尝尝鲜”,来看看 MySQL 9.0 创新版本有哪些主要的变化。
新特性
1、Event 相关 SQL 语句可以被 Prepared
在 MySQL 中,事件(Events)是一种可以在预定时间执行的调度任务,比如定期清理数据之类的,就可以使用事件。
MySQL 9.0 对事件 SQL 提供了 Prepared 支持,包括:
- CREATE EVENT
- ALTER EVENT
- DROP EVENT
prepared 准备语句是一种预编译的 SQL 语句模板,可以在执行时动态地传入参数,从而提高查询的性能和安全性。
比如下面就是一个准备语句,插入的数据可以动态传入:
PREPARE stmt_insert_employee FROM 'INSERT INTO employees (name, salary) VALUES (?, ?)';
2、Performance Schema 新增 2 张表
MySQL 的 Performance Schema 是一个用于监视 MySQL 服务器性能的工具。它提供了一组动态视图和表,记录了 MySQL 服务器内部的活动和资源使用情况,帮助开发者进行性能分析、调优和故障排除。
本次新增的表:
- variables_metadata 表:提供关于系统变量的一般信息。包括 MySQL 服务器识别的每个系统变量的名称、作用域、类型、范围和描述。此表的 MIN_VALUE 和 MAX_VALUE 列旨在取代已弃用的 variables_info 表的 MIN_VALUE 和 MAX_VALUE 列。
- global_variable_attributes 表:提供有关服务器分配给全局系统变量的属性-值对的信息。
3、SQL 语句优化
现在可以使用以下语法将 EXPLAIN ANALYZE(分析查询执行计划和性能的工具)的 JSON 输出保存到用户变量中:
EXPLAIN ANALYZE FORMAT=JSON INTO @variable select_stmt
随后,可以将这个变量作为 MySQL 的任何 JSON 函数的 JSON 参数使用。
4、向量存储
AI 的发展带火了向量数据库,我们可以利用向量数据库存储喂给 AI 的知识库和文档。
虽然 MySQL 官方更新日志中并没有提到对于向量数据存储的支持,但是网上有博主在 MySQL 9.0 社区版中进行了测试,发现其实已经支持了向量存储,如图:
在此之前,MySQL 推出过一个专门用于分析处理和高性能查询的数据库变体 HeatWave,本来以为只会在 HeatWave 中支持向量存储,没想到社区版也能使用。如果是真的,那可太好了。
5、其他
此外,还优化了 Windows 系统上 MySQL 的安装和使用体验。
废弃和移除
1)在 MySQL 8.0 中,已移除了在 MySQL 8.0 中已废弃的 mysql_native_password 认证插件,并且服务器现在拒绝来自没有 CLIENT_PLUGIN_AUTH 能力的旧客户端程序的 mysql_native 认证请求。为了向后兼容性,mysql_native_password 仍然在客户端上可用;客户端内置的认证插件已转换为动态加载插件。
这些更改还涉及移除以下服务器选项和变量:
- --mysql-native-password 服务器选项
- --mysql-native-password-proxy-users 服务器选项
- default_authentication_plugin 服务器系统变量
2)Performance Schema 中 variables_info 表的 MIN_VALUE 和 MAX_VALUE 列现在已废弃,并可能在将来的 MySQL 版本中移除。开发者应该改为使用 variables_metadata 表的 MIN_VALUE 和 MAX_VALUE 列。
3)ER_SUBQUERY_NO_1_ROW 已从忽略包含 IGNORE 关键字的语句的错误列表中移除。这样做的原因如下:
- 忽略这类错误有时会导致将 NULL 插入非空列(对于未转换的子查询),或者根本不插入任何行(使用 subquery_to_derived 的子查询)。
- 当子查询转换为与派生表联接时,行为与未转换查询不同。
升级到 9.0 后,如果包含 SELECT 语句的 UPDATE、DELETE 或 INSERT 语句使用了包含多行结果的标量子查询,带有 IGNORE 关键字的语句可能会引发错误。
总结
看了本次 MySQL 9.0 创新版的更新,说实话,大失所望。在这之前,网上有很多关于 MySQL 9.0 版本新特性的猜测,结果基本上都没有出现。毕竟距离 MySQL 上一次发布的大版本 8.0 已经时隔 6 年,本来以为这次 MySQL 会有一些王炸的新特性,结果呢,本次除了修复了 100 多个 Bug 之外,几乎没啥对开发者有帮助的点。别说没帮助了,我估计很多同学在看这篇文章前都没接触过这些有变更的特性。
我们最关注的,无非就是使用难度、成本和性能提升对吧,最好是什么代码都不用改,直接升级个数据库的版本,性能提升个几倍,还能跟老板吹一波牛皮。
你看看隔壁的 PostgreSQL,这几年,都已经从 11 更新到 17 版本了,AI 时代人家也早就能通过插件支持存储向量数据了。MySQL 你这真的是创新么?
最后,MySQL 9.0 创新版本的下载地址我就不放了,咱还是老老实实用 5.7 和 8.0 版本,MySQL 的新版本,还有很长一条路要走呀!
来源:juejin.cn/post/7387999151411920931
初中都没念完的我,是怎么从IT这行坚持下去的...
大家好,我是一名二线(伪三线,毕竟连续两年二线城市了)的程序员。
现阶段状态在职,28岁,工作了10年左右,码农从事了5年左右,现薪资9k左右。如文章标题所说,初二辍学,第一学历中专,自己报的成人大专。
在掘金也看了不少经历性质的文章,大多都是很多大牛的文章,在大城市的焦虑,在大厂的烦恼,所以今天换换口味,看一看我这个没有学历的二线的程序员的经历。
1.辍学
我是在初二的时候辍学不上的,原因很简单,太二笔了。
现在想来当时的我非常的der,刚从村里的小学出来上中学之后(我还是年级第7名进中学,殊不知这就是我这辈子最好的成绩了),认为别人欺负我我就一定要还回来,完全不知道那是别人的地盘,嚣张的一批,不出意外就被锤了,但是当时个人武力还是很充沛的,按着一个往地上锤,1V7的战绩也算可以了。自此之后,我就开始走上了不良的道路,抽烟喝酒打架,直到中专毕业那天。
我清楚的记得我推着电车望着天,心里只想着一个问题,我毕业了,要工作了,我除了打游戏还会什么呢,我要拿什么生存呢...
这是当时我心里真实的想法,我好像就在这一刻、这一瞬间长大了。
2.深圳之旅
因为我特别喜欢玩游戏,而且家里电脑总是出问题,所以我就来到了我们这当地的一个电脑城打工,打了半年工左右想学习一下真正的维修技术,也就是芯片级维修,毅然决然踏上了深圳的路。
在深圳有一家机构叫做迅维的机构,还算是在业内比较出名的这么一个机构,学习主板显卡的维修,学习电路知识,学习手机维修的技术。现在的我想想当时也不太明白我怎么敢自己一个人就往深圳冲,家里人怎么拦着我都没用,当时我就好像着了魔一样必须要去...
不过在深圳的生活真的很不错,那一年的时光仍旧是我现在非常怀念的,早晨有便宜好吃的肠粉、米粉、甜包,中午有猪脚饭、汤饭、叉烧饭,晚上偶尔还会吃一顿火锅,来自五湖四海的朋友也是非常的友好,教会了我很多东西,生活非常的不错。
3.回家开店
为什么说我工作了10年左右呢,因为我清楚记得我18岁那年在本地开了一个小店,一个电脑手机维修的小店。现在想想我当时也是非常的二笔,以下列举几个事件:
- 修了一个显示器因为没接地线烧了,还跟人家顾客吵了一架。
- 修苹果手机翘芯片主板线都翘出来了,赔了一块。
- 自己说过要给人家上门保修,也忘了,人家一打电话还怼了一顿。
- 因为打游戏不接活儿。
以上这几种情况比比皆是,哪怕我当时这么二笔也是赚了一些钱,还是可以维持的,唯一让我毅然决然转行的就是店被偷了,大概损失了顾客机器、我的机器、图纸、二手电脑等一系列的商品,共计7万元左右,至今仍没找回!
4.迷茫
接下来这三年就是迷茫的几年了,第一件事就是报成人大专,主要从事的行业就杂乱无章了,跟我爸跑过车,当过网吧网管,超市里的理货员,但是这些都不是很满意,也是从这时候开始接触了C和C++开始正式踏入自学编程的路,直到有一次在招聘信息里看到java,于是在b站开始自学java,当时学的时候jdk还是1.6,学习资料也比较古老,但是好歹是入了门了。
5.入职
在入门以后自我感觉非常良好,去应聘了一个外包公司,当时那个经理就问了我一句话,会SSM吗,我说会,于是我就这么入职了,现在想想还是非常幸运的。
当时的我连SSM都用不明白,就懂一些java基础,会一些线程知识,前端更是一窍不通,在外包公司这两年也是感谢前辈带我做一些项目,当时自己也是非常争气,不懂就学,回去百度、b站、csdn各种网站开始学习,前端学习了H5、JS、CSS还有一个经典前端框架,贤心的Layui。
干的这两年我除了学习态度非常认真,工作还是非常不在意,工作两年从来没有任何一个月满勤过,拖延症严重,出现问题从来就是逃避问题,职场的知识是一点也不懂,当时的领导也很包容我,老板都主持了我的婚礼哈哈哈。但是后来我也为我的嚣张买了单,怀着侥幸心理喝了酒开车,这一次事情真真正正的打醒了我,我以后不能这样了...
6.第二家公司
在第二家公司我的态度就变了很多很多 当时已经25岁了,开始真真正正是一个大人了,遵纪守法,为了父母和家人考虑,生活方面也慢慢的好了起来(在刚结婚两年和老婆经常吵架,从这时候开始到现在没有吵过任何架了就),生活非常和睦。工作方面也是从来不迟到早退,听领导的安排,认真工作,认真学习,认识了很多同行,也得到了一些人的认可,从那开始才开始学习springboot、mq、redis、ES一些中间件,学习了很多知识,线程知识、堆栈、微服务等一系列的知识,也算是能独当一面了。但好景不长,当时我的薪资已经到13K左右了,也是因为我们部门的薪资成本、服务器成本太大,入不敷出,公司决定代理大厂的产品而不是自研了,所以当时一个部门就这么毕业了...
7.现阶段公司
再一次找工作就希望去一些自研的大公司去做事情了,但是也是碍于学历,一直没有合适的,可以说是人厌狗嫌,好的公司看不上我,小公司我又不想去,直到在面试现在公司的时候聊得非常的好,也是给我个机会,说走个特批,让我降薪入职,算上年终奖每个月到手大概10k(构成:9k月薪,扣除五险一金到手7.5k,年终奖27k,仨月全薪,所以每个月到手10k),我也是本着这个公司非常的大、非常的稳定、制度非常健全、工作也不是很忙也就来了,工作至今。
总结
- 任何时候想改变都不晚,改变不了别人改变自己。
- 面对问题绝对不能逃避,逃避没有任何用,只有面对才能更好的继续下去。
- 不要忘了自己为什么踏入这行,因为我想做游戏。
- 解决问题不要为了解决而解决,一定要从头学到尾,要不然以后出现并发问题无从下手。
- 任何事情都要合规合法。
- 工作了不要脱产做任何事情,我是因为家里非常支持,我妈至今都难以相信我能走到今天(我认为我大部分是运气好,加上赶上互联网浪潮的尾巴)。
- 最重要的,任何事情都没有家人重要,想回家就回家吧,挣钱多少放一边,IT行业找个副业还是非常简单的,多陪陪他们!
来源:juejin.cn/post/7309645869644480522
部署完了,样式不生效差点让我这个前端仔背锅
大家好,作为今年刚毕业的大聪明(小🐂🐎),知道今年不好就业,费心费力的面试终于进了个小公司,然后我的奇妙之旅开始了。
叮!闹钟响了!牙都不刷了,直接起床出门去上班,身为🐂🐎就要有早起吃草干活的觉悟,所以我是个很合格的食草动物。只不过,下面的信息,让我一天当动物的好心情都没了。
部署?因为该公司人数比较少,也没有自动化部署操作,只能让前端build一下dist包然后丢给后端,让后端部署,然后该项目也比较急,我刚来,也不好意思@后端,恰巧跟我对接的后端也是刚来不熟悉,所以我就跟负责人继续扯皮拖延时间了。(真是不好意思,假的)↓↓↓
终于经过不断努力(后端),部署好了,但是,我的样式乱了,为什么呢?看了一下网络和源代码,样式文件都请求到了,问了后端,一致说是我前端的问题,我懵了,然后快马加鞭查找问题
解决
ok,那我先看打包的dist文件有样式有问题吗,使用npm安装个 http-server 运行打包后的index.html,运行打开运行后的地址,欧克样式没乱,文件引用也没问题,那到底是什么鬼影响的呢?
在我百思不得其解的时候,我打开了神器--csdn,搜索打包dist部署后样式文件不生效,就遇到了这个神文t.csdnimg.cn/WbQyq
欧克,按照博主说的Nginx没有配置这两个东西而导致的,我有点不确定是不是,有点犹犹豫豫的找到后端说帮我在Nginx配置文件的http加下这两个配置,然后样式就好了,完美!后端由于疏忽,没加上就把问题推给我,结果被我扳回一城。
include mime.types;
default_type application/octet-stream;
- include mime.types;
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
mime.types
文件,Nginx可以识别不同类型的文件并正确地处理它们。 - 示例:假设
mime.types
文件中定义了.html
文件为text/html
类型,Nginx在处理请求时会根据这个定义设置正确的HTTP响应头。
- 这个指令告诉Nginx去包含一个文件,这个文件通常包含了定义了MIME类型(Multipurpose Internet Mail Extensions)的配置。MIME类型指定了文件的内容类型,例如文本文件、图像、视频等。通过包含
- default_type application/octet-stream;
- 这个指令设置了默认的MIME类型。如果Nginx无法根据文件扩展名或其他方式确定响应的MIME类型时,就会使用这个默认类型。
application/octet-stream
是一个通用的MIME类型,表示未知的二进制数据。当服务器无法识别文件类型时,会默认使用这个类型。例如,如果请求的文件没有合适的MIME类型或没有被mime.types
文件中列出,Nginx就会返回application/octet-stream
类型。- 这种设置对于确保未知文件类型的安全传输很有用,因为浏览器通常会下载这些文件而不是尝试在浏览器中打开它们。
总之,添加 include mime.types;
和 default_type application/octet-stream;
配置后,Nginx能够正确地识别和处理CSS文件的MIME类型,从而确保浏览器能够正确加载和应用CSS样式。
所以,前端仔不能只当前端仔,还是要好好学点服务端的知识,不然锅给你,你还认为这是你该背的锅。
以上是开玩笑的描述,只是为了吸引增加阅读量
来源:juejin.cn/post/7388696625689051170
一份离职感想
一、和解
人的一生就是不断地与世界和解,以及与自己和解。
今天提离职了,相比于四年前的离职,更加的从容和淡定。所谓,心无外物,云淡风轻。
图:云卷云舒,心随境转
二、当下
今天晚上和一个刚来三周的应届生跑步,聊着聊着就说到了要是回到十八岁就好了的话题。
当然我也谈了我的一些想法, 如果是以前的我也会像你这样,要是回到以前就好了。 但是,现在的我更加从容、更加淡定、更加坚定,以及对事情的理解、看法、态度是那个时候没有的,同时懂得了筛选,也懂得舍弃,对社会的理解,对世界的理解也是那个时候没有的,甚至对自己的看法也更加立体,对自己也更宽容,现在反而会更加欣赏自己。
人生不必时时往过去看,"既往不恋,当下不杂,未来不迎"
图:操场跑步,有时候也会坐在最高的台阶上,享受这自然的风。
三、接纳
我们还谈到了失恋的话题,我说很多人是没有办法放下的,也没办法抹去,不管是事还是人都一样。
人的一生就像是石块不断垒起来的玛尼堆,有一些人会在特定时间段参与进来,帮你加一块,可能它的颜色、大小、形状、好看程度都不一样,但你不得不承认,那个时间点,那个高度就只有那么一块,无可替代。
与其不见,不如接纳!
图:某次户外徒步
四、选择
大学时的第一部课外读物 ,路遥的《人生》,开篇来自柳青的一段话,至今影响着我。
刚好昨天生日,有感而发,也时刻警醒着自己。
图:朋友圈,31岁的记录
五、感受
人这一生就是不断地给自己的感受积累素材
不知什么时候,我开始静下来,用心去感受这个世界。
在吃午餐的时候,变得专心,放下了手机,端起了饭碗,开始用舌头去感受食物原本的味道,就像《小森林》中的女主一样。静下来,都是收获。
图:午餐,三菜一汤
用心去感受,用心去体会,从容和淡然。享受生活,感恩生活。
你所看到的,就是你内心的一种投射,你看到了美好,那么你的内心就是美好的。你看到了恶,那么你的内心就是恶。 内心这块三分宝地,就不要装上一些乱七八糟的东西了。
六、经历
生命的厚度在于去经历一些不一样的事情。
人需要不断去经历一些不一样的事情,而不是将365天过成1天。
“人不是活一辈子,不是活几年几月几天,而是活那么几个瞬间。”——鲍利斯·帕斯捷尔纳克。
希望多一些这样怦然心动、与众不同的时刻, 看到黄山的日出、云海,觉得这个世界真的很酷,继续热爱。
图:黄山风景图
不要等到八十岁了才去做二十岁想做的事情。
七、自己
青年人要有青年人的样子,朝气、热血。
如罗曼罗兰说过:
“世界上只有一种真正的英雄主义,那就是在认清生活的真相后依然热爱生活”。
人不要给自己设限,勇敢地做自己,不要被这个世界迷失了自我。
在这个满是焦虑、满是浮躁的社会,更要做到正心正念。
就像我在 500px 网站上说过的一句话:“这个世界没有标准答案,活出自己,不受羁绊”。
来源:juejin.cn/post/7386497602193981481
丈母娘,你这是来真的啊?
今年春节在家里呆的时间比往年都要久一些,就算离家不远也会这样感觉,随着年龄的增加,越发思念故土以及父母。
以往回家都比较晚,待着待着就想着回工作的城市。因为回家总会伴随着和二老的争论,而且我那时也很戾气,合不到一块就气,或者进房间睡觉。
要说啥事能值得争论,我的确忘了,大概是思想观念以及家里生意的问题产生的。以至于身处异乡也要离开家里不愿意喋喋不休。
随着时间流逝,心态已经发生变化。我内心深处逐渐向着故土,和家里二老的关系也潜移默化,彼此心照不宣的拉近了很多。
二老也不过多问责一些,也不再以长者的心态和我对质。
事实也觉得普通家的孩子成年后就该放权干他自己的事,父母少些干预,有些做法和观念已经帮不上忙。父母与孩子之间往往都会产生很大分歧,尤其是思想观念的矛盾。
每一代都有可能推翻上一代的思想意识。父母与子女和谐,一般父母的思想观念足够前卫能够传承给子女,或者子女的思想观念没有很大差异,认可父母的思想体系。
我们会一起坐下来交谈家里的事情,想好对策处理外事,规划老家祠堂以及屋子的安排;参与布置家里,代替老爹出席各项习俗礼节活动。
从读大学开始到现在就没再给它们制造任何的麻烦和搞砸事情。只见他坐在店里抽着烟,沉默着望向我,迟迟不再发话,只在最后说了一句婚姻的问题。
这个话题别说他了,老家众多的亲戚都会抛出这个尖锐的问题,我说我回家只想休息,逢年过节和老朋友叙旧,去亲戚家串串门,一起乐呵乐呵聊聊家常,聊聊八卦。
95‘ 后教师彭老师
小学同学彭老师,现在是一名县城重点中学的高中老师。她是一名 90 后老师,我在她身上看到了很多老师的缩影。她毕业后第一份工作就是任高一班主任兼任课老师。
▲图/ 学生:???
彭老师很是焦虑很迷茫,害怕自己不能胜任;害怕自己教不好;害怕自己耽误学生的前途。
每一个担心都很沉重,责任肩负在身上,没日没夜的备课,改作业,演练怎么让学生听懂。
▲图/ 在最无能为力的学科遇到了最不想辜负的老师
小城市义务教育向来重视成绩不太注重心理建设,导致问题学生很多。彭老师经常 solo 学生和家长,试图说服他们把重心放在当前阶段要做的事。
尽管生病,喉咙发炎也坚持做着这些事情。
▲图/ 除夕夜,彭老师回学校,顺道提零食带红包给她
▲图/ 她赠了一罐饮料,握着只觉比当时天气还冷
▲图/ 彭老师收到开工红包请吃饭,还给了红包,呜呜呜感动
有时煎熬的对着同事和朋友说,看着自己的希望学生开始堕落真的很难受,说也说不通;面对自己的学生读着读着因家里或者各种的问题而辍学时更无语。
▲图/ 00 后学生在 95 后老师教案涂鸦
忍不住会直接联系家长 solo,开导家长有时候如同开山凿石一样,只有坚持不懈才使得那位学生继续读下去,尽管读大专,也改变了不少走向。
有一天她苦笑的跟我们说,她自己都没怎么谈过恋爱却还要开导失恋的学生,甚至被失恋学生反问:“老师,你谈过恋爱吗?”
???
彭老师所带的班级在 23 年第一批结业。上本率超额完成上面给的任务,这一路上面临学生堕落、早恋、辍学、逃课、网瘾...
彭老师有时候很想放弃摆烂,像那些老道的老师一样,风清云淡,看开一点,上课就上课,喝茶就喝茶,晒太阳就晒太阳。
后来她骑着小电瓶,向我重申了她那坚定的信念,绝不可能摆烂,我要对我的学生负责,要为他们的前途着想,就这么决定了。
我看着她感觉头上出现了一顶为人师表的光环。
实话说,老师的行为无论是怎样的,都会被学生刻在记忆深处,尽管有时不会联系,也会在某一刻回想起,念其良莠。
▲图/ 念大学的学生探望彭老师
她性格一直都很逗比很乐观,走路也喜欢蹦蹦跳跳,还被她的老师和学生嘲笑她走路蹦蹦跳跳,和她玩王者鲁班的走姿如出一辙。
在她学生毕业之后,彭老师回归万年鲁班,但技术依然停留在四五年前大学生时期,经常遭截杀一路“啊”,开疾跑徒走回水晶,也经常被我们和她的学生给护着。
现在见她时还是很逗比,嘴硬心软。脸上刻印出一副班主任的形象,坚定严肃而又亲和。
她说她带完一届之后不当班主任了,太辛苦太累了。后面她又被安排带复读班班主任。
阿姨,你来真的啊?
老家的天气很不错,逢年过节我们经常互串亲戚的门,晒晒太阳,欺负欺负小朋友,欺负过头了说送给他们一份《三年高考五年模拟卷》礼物,他们哭的更厉害。
▲图/帮人带孩子真好玩
春节假期充电的时间正在倒计时,最后赶着串堂弟的门,也就是大叔家,那时他们家里很忙,家族里的会做饭打点的都来帮忙了。
原来是堂弟未来丈母娘查家环节,家里上上下下忙活。
我等闲杂人在巷子里晒着太阳,准备迎接他们。堂弟我和同岁,月份比我小,在温州工作,这几年他老爹操够了心,费了不少钱。龙年又长了一岁,他父母更是着急。
▲图/ 回家路途中天色很震撼,大家都不想说话
闲聊之际,只见一列车齐刷刷地开到门口,不知道的还以为是迎亲来了。
二叔见状连忙赶上去打招呼并指挥停车泊位。只见那丈母娘下车后整理着装,望着周边的房屋装饰,一脸严肃对着二叔说位置有些偏远,绕来绕去的,二叔连忙解释可能走的道不一样,走国道会很顺。
亲友团齐刷刷招呼张罗着进去喝茶,握手,递烟,倒茶。摆了三桌才能坐下,我们自己人在旁边站着观望或帮忙。两方互相寒暄之后,不到十分钟对方便开始切入主题,商量儿女婚姻问题。
丈母娘吐露堂弟家位置偏僻,路道不好走,绕了很多弯才抵达,彩礼需要增加 3w 到 5w,作为她女儿的嫁衣钱。
阿姨,你这是来真的啊?
大叔一边忙着圆场有不同的道路可以走,镇与镇之间来往有很多路可走也方便,一边递烟倒茶使其思量再三。
丈母娘依然坚定不移,重申了一遍,对方亲戚应声附和。
大叔脸色像是喝酒上了头一样,随后陷入沉思,见其态度坚硬,且事已至此,作出退步可以增加 1w - 2w,给到对方女儿身上。
前些天,大叔带着堂弟相亲到女方家,据说对方开价 35.8w 彩礼,回礼是购买房之后支援 10w+,不知其是否商量后的价格。
只见那天大叔来我家喝茶水时带着儿子和未来儿媳去了县城买了“五金”, 4w 左右。
见此事既成,并不买二叔的帐,坚持需要增加 3w,并声称给女儿做嫁衣。
大叔又陷入了思考,心里计算着账面,上个月女儿刚嫁出去,彩礼还没捂热,就要付之东流,是为不甘且又无奈。
场面陷入了安静,对方只管握着茶杯吃着果子等待结果,势必做好了撤离的打算。
姑舅们遂即递烟倒茶聊家常。
堂弟陪同坐着喝茶望着对象低下了头,儿媳妇安静得陪在身边,挽着堂弟的手。
掂量之后大叔同意了对方的要求,双方态度方能缓和很多,继续喝茶,商量事宜,聊家常,聊孩子幸福。
饭后,互相道了别,每人随了红包礼。只觉得对方结婚习俗没有讲明白有点遗憾,但愿不会阻碍他两组建一个幸福的小家庭。
我和表弟坐在沙滩上,对着河扔石子打水漂,谁都不想再提,心里比谁都清楚。却和群里的伙伴嘲笑着自己家的那位是否也要几十个 w。
来源:juejin.cn/post/7336822951273824282
吾辈楷模!国人开源的Redis客户端被Redis官方收购了!
不久前开源圈子里的一则消息在网上引起了一阵关注和讨论。
一个由国人开发者所打造的开源项目被 Redis 公司官方给收购了,作者自己也发了动态,表示感谢项目9年以来的陪伴,同时也希望她未来一切都好。
这个开源项目的名字叫做:ioredis,相信不少小伙伴也用过。
目前在GitHub上我们可以看到,ioredis项目的开源地址已经被迁移至 Redis 官方旗下了。
iosredis是国人开发者所打造的一个Redis客户端,基于TypeScript所编写,以健壮性、高性能以及功能强大为特色,并且被很多大公司所使用。
截止到目前,该项目在GitHub上已累计获得超过 13000 个 Star标星和 1000+ Fork。
作者自己曾表示,自己创建这个开源项目的初衷也很简单,那就是当年在这方面并没有找到一个令自己满意的开源库,于是决定自己动手来打造一个,于是就利用闲暇时间,自己从零开发并开源了 ioredis 。
直到2022 年 8 月 30 日,历时整整7年,ioredis 成为了 Node.js 最流行的 Redis 客户端。
而直到如今,这个项目从个人的 side project 到被开源公司官方收购,作者9 年的坚持属实令人佩服,吾辈楷模啊!
而拜访了这位开发者的GitHub后我们会发现,作者非常热衷于创造工具,除了刚被收购的名作ioredis之外,主页还有非常多的开源项目,并且关注量都不低。
而且从作者发的一些动态来看,这也是一个热爱生活的有趣灵魂。
有一说一,个人开源作者真的挺不容易的,像上面这样的个人开源项目被官方收购的毕竟是个例,其实好多个人的开源项目到后期由于各种主客观原因,渐渐都停止更新和维护了。
大家都知道,伴随着这两年互联网行业的寒意,软件产业里的不少环节也受到了波动。行业不景气,连开源项目的主动维护也变得越来越少了。
毕竟连企业也要降本增效,而开源往往并不能带来快速直接的实际效益。付出了如果没有回报,便会很难坚持下去。
而对于一名学习者而言,参与开源项目的意义是不言而喻的,之前咱们这里也曾多次提及。
参与开源项目除了可以提升自身技术能力,收获项目开发经验之外,还可以让自己保持与开源社区其他优秀开发者之间的联系与沟通,并建立自己的技术影响力,另外参与优秀开源项目的经历也会成为自己求职简历上的一大亮点。
所以如果精力允许,利用业余时间来参与或维护一些开源项目,这对技术开发者来说,也是一段难得的经历!
来源:juejin.cn/post/7345746216150876198
我在国企当合同工的那段日子
@author: 郭瑞峰
@createTime: 2024/06/03
@updateTime: 2024/06/20
心血来潮
25号考完了,非常不理想,果然700页的东西不是一个月能搞完的。不对,我今儿写日志是为了纪念一下我的第一家公司,咋扯到别的了......言归正传,我在第一家公司待了仨年,可能是年纪到了(26岁咋还不退休啊),也可能是留了点感情在,离开前有些百感交集,思来想去还是写一个懒人日志吧,纪念一下我打工的三年光阴吧。
(:з」∠)
初说公司
先说一下俺的第一家公司,咱从学校出来就来这儿报道了,公司是国企控股,领导层全是国企员工,其他进公司的员工就是合同工,或者说是国企合同工,能吃公司东西,不是人力外包。
(:з」∠)
成都这边的开发都是围绕着云服务的,包括云操作系统、云桌面系统、云运维系统以及多云系统(我个人喜欢把他称为多个云集成系统),当然全是定制化项目。对,忘说了,公司主要业务是轨道交通行业,做云相关的产品是将轨道行业的运维放在云上面,算是相应国家的两化融合(信息化和工业化)。
对了,得说一下公司待遇,公司给的工资都在平均水平以下,尤其是对应届生而言,社保基数是工资八折(试用期)交的,公积金是12%,没有餐补但自带食堂以及饭卡补助,有些节假日有礼品,至少基础福利还好。
项目与业务
我所在的项目组就是多云系统,也算是我认为公司能拿得出手的项目。虽然是集成项目,但它只能集成。好像说了跟没说一样,那说具体点吧,比如说业主那边需要云,但怕私有云厂商垄断坐地起价,所以说一般配额划分为“7/3”、“4/3/3”、“6/4”,这样就有两套云系统,为了用起来顺心就需要一个集成系统,所以说我这个项目组的业务来源就是这样,至于你说的我们集成系统会不会垄断坐地起价,拜托,我们系统只会集成,没有底层设备控制权,坐地起价就直接禁用就行了,就不用这个系统呗,反正资源在另外的云操作系统中。
好了,话题回来,说说项目组开发相关的吧,项目开发受阻有三:与三方厂商沟通、项目代码老旧、随时随地变更的需求。
先说第一点吧,集成系统最大的麻烦就是跟三方厂商沟通,当然测试环境、测试数据获取这类的细节也算三方厂商沟通。因为地铁行业算是智能中国建设的一部分,所以说不光是我,连三方厂商的软件都必须是定制的。开发时候就要等着厂商环境稳定了,有数据了再联调,联调有bug了,再走一轮上面的流程,极大地增加了沟通成本以及开发成本。
在沟通,再沟通
其二就是和很多工业软件公司一样,软件项目时间跨度很大,里面东西不知道转手了好多次,缝缝补补式的开发,开发要考虑很多兼容性问题以及自己想办法写补丁。比如说node@6.x.x
不支持Object.entries
,你就要手动在webpack.base.conf.js
写的兼容,问我为啥不配置babel
呢,上次改babel
配置都是2016年的事儿了。代码要写兼容,久而久之就会忘记什么事封装、抽象,全部遗失在兼容的漩涡中。
我就改了一点点怎么崩了
其三就是随时随地变更的需求,这里我叠个甲,这个我不是甩锅给产品,虽然是产品改的需求,但产品不是想改就改,一定是业主/客户/上级/领导指示要改的。有需求变动谁都不会安逸,谁都烦,但请把炮火对准,不要误伤友军。频繁调整的需求会不断地消磨激情和热情,模糊项目方向,当然还有临时变卦导致的加班。
一直在变的需求
心态变化
三年工作时间虽然很短,但足以改变心态。原来有些迷茫到彻底迷茫;原来想要搞出一番事业到慢慢得过且过;原来想努力改变世界走到只想躺平加速世界毁灭。
公司的缝缝补补,工作的缝缝补补,项目的缝缝补补,这样的缝缝补补渐渐地缝补在人身伤,人心里。原来就算只有940的显卡也要努力熬夜玩游戏,现在用上3060ti后却只想打开直播看看,就只看看,重新上手玩太耗精力了。至于脱单嘛,自己都这么累了,为啥带着另一个一起累呢?
尾声
本来6月3号说写完的,忙着离职交接以及新公司入职,再加上拖延症又犯了,所以说一直到20号才写完,不过至少咱写完了,能发。
这篇算是自己里程记录,同时也是发牢骚,大家就当笑话看看吧。
来源:juejin.cn/post/7382121357608321059
.zip 结尾的域名很危险吗?有多危险?
Google 于 2023 年 5 月 10 日全面开放了以 .zip
结尾的域名,这一举动引起了安全研究人员和社区的警惕,他们担心该通用顶级域名(gTLD,Generic top-level domains)会被用于创建足以迷惑计算机高手的恶意 URL。
2023 年 5 月 3 日,Google 宣布了包括 .zip
和 .mov
在内的 8 个全新的通用顶级域名:
- .dad
- .phd
- .prof
- .esq
- .foo
- .zip
- .mov
- .nexus
并于 5 月 10 日通过 Google Domains 向公众开放注册。
Google Domains 是 Google 提供的一项域名注册和管理服务,支持用户搜索和注册域名
小心!含有 .zip 的恶意 URL
安全研究员 Bobby Rauch 指出(The Dangers of Google’s .zip TLD),要警惕含有 .zip
、Unicode 字符(特别是 U+2044、U+2215 等)以及 @
符号的恶意 URL。这类恶意 URL 迷惑性极强,甚至能欺骗十分有经验的用户。
若点击 https://google.com@bing.com
这个 URL,实际访问的是 https://bing.com
。这是因为根据 RFC 3986 Uniform Resource Identifier (URI): Generic Syntax 的规定,@
符号之前的 google.com
应识别为用户信息,其后的 bing.com
才是主机名(域名)。我们可以借助常用的编程语言来确认这一点,如利用 PHP 的 parse_url()
函数:
<?php
var_dump(parse_url("https://google.com@bing.com"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(8) "bing.com"
["user"]=>
string(10) "google.com"
}
然而,若 @
之前有正斜杠 /
,如 https://google.com/search@bing.com
,则浏览器会将 /search@bing.com
部分识别为路径,最终访问的是 https://google.com/
下的文件 search@bing.com
。由于没有这个文件,结果自然是 404。
<?php
var_dump(parse_url("https://google.com/search@bing.com"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(10) "google.com"
["path"]=>
string(16) "/search@bing.com"
}
Bobby Rauch 就是利用了上述规则,创建了一个恶意 URL,
https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@v1271.zip
乍看之下,这个 URL 似乎是用于从 GitHub 上下载 v1271
这个特定版本的 Kubernetes 的链接。但实际上 parse_url()
函数的解析结果显示,真正要访问的域名却是 v1271.zip
而不是 github.com
:
<?php
var_dump(parse_url("https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕@v1271.zip"));
array(3) {
["scheme"]=>
string(5) "https"
["host"]=>
string(9) "v1271.zip"
["user"]=>
string(63) "github.com?__kubernetes?__kubernetes?__archive?__refs?__tags?__"
}
若你不小心点击了这类域名,那么恭喜你,很可能喜提一个 evil.exe
(请注意动画演示中的左下角)。
仅凭肉眼可能难以分辨以下两个 URL 的区别吧:
https://github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕
https://github.com/kubernetes/kubernetes/archive/refs/tags/
但若调整一下字体,则可以发现端倪,
恶意 URL 中的正斜杠 /
根本不是真正的 /
(U+002F),
而是下面这个看起来很像 /
的 Unicode 字符:
由于恶意 URL 中并没有使用真正的 /
,因此根据 RFC 3986 的规定,@
之前的部分github.com∕kubernetes∕kubernetes∕archive∕refs∕tags∕
尽管看似域名与路径,但实际上却是用户信息(真够长的)。
在刚刚的动画演示中,Bobby Rauch 其实还使用了另一个迷惑人的小伎俩——在电子邮件客户端上,将 @
的字号大小更改为 1,让这个特殊字符几乎看不到,从而更隐秘地伪装了恶意 URL。
对于由以 .zip
结尾的域名带来的安全隐患,Bobby Rauch 给出的建议是,在单击 URL 之前,先将鼠标悬停在该 URL 上并检查浏览器底部显示真正要访问的 URL。
来源:juejin.cn/post/7384244866875146290
希望你多出去看看,别活在短视频和文字里!
感谢你阅读本文!
这段时间在校友群里看到一些“混得比较好的”同学发言,类似于“5w的月薪很高吗?”,“我身边年薪六七十w的人不少”之类的话,加上偶尔看到一些“年薪百万很简单”的标题党文章或者视频,其实对于我来说,我根本懒得去考证这些是真是假!
但是我觉得有必要去聊一聊!
一、知识的贫乏
首先在说这个问题之前,我想引用罗翔老师的一句话。
一个知识越贫乏的人,就越有莫名的优越感!
一年多以前,我回老家,和以前的高中女同学出来聊天,彼此聊了聊自己现在的工作,然后他问我,“你现在一个月能赚三四万吧!”,我当时惊呆了,我回她:“瞧你说的,捡黄树叶也要赶上秋天呢”,我反问她你现在多少呢,她说两千八,我继续问,“你觉得工资多少才算高?”,她说最起码5万以上吧!我苦笑答:“我的妈呀,怎么都这么厉害呀!”。
事实是怎样的呢?
我们先不把事情说得太远,“脉脉上人均年薪百万”,“抖音上人手一台劳斯莱斯”这些不在叙述范围内,感情咱也不会那么不要脸去吹!
二、大众才是真相
像我们这种普通二本学校的学生现状应该最能接近真相了,往上不谈双一流,往下不谈专科,据我所知,我校2021毕业的学生,如果继续做软件工程的话,现在一个月能拿两万以上的人没几个,还得是一线城市,在一线城市的大多都是一万多,所以一万多就是一个中位数。
不过要注意,软件工程专业毕业后从事本专业的人是很少的,就拿我们班来说,班上50人,但是从事软件的不超过20个,20个还是比较理想的。
那么就有一部分从事其它职业,一部分待业,一部分考公考编。
软件行业在整个市场来说工资高一点,就业相对来说简单一点,虽然近几年来行情越来越差,但是相比于其他行业来说,还是稍微好一点!
从事其他行业的人来说,如果家里有点关系的人,条件好一点的人,可能去到一个单位里面暂时上班,条件不好的,那就出来随便找一个班上,对于销售型的,在广州深圳,大多都是六七千,小城市的话,五千基本上已经很高了。对于待业的,那基本上没收入,考公考编的一般都回到了小县城,随便找个单位临时上班,一个月也就两千来块!
我们就不去分析双一流,专科,中职这些了,所以整体算下来,我们现在的年轻人的收入是很低的。
三、时代特征
努力在这个社会貌似已经不是一个正能量的词了,仿佛已经是一个调侃的词了,就像现在大多女孩子,他现在不会选择一个很努力的男孩子作为伴侣,因为努力后得到回报是一个概率事件,大多会选择有“存货”的人!当然,并不是人人都这样!
社会的发展就是这样,就像森林里面的狼越来越多,那么捕获到猎物的概率就越来越小,这和努力没多大的关系,这是时代特征!
八九十年代别说考上大学,考上中专谋个职位都不难,而现在一砖头下去都能打中几个研究生已经不是什么稀奇事了。
还有现在的经济形势如此严峻,企业和单位的寒冬一直在降临,无数的人蜂拥而至,导致形势更加紧张,本来在夹缝中已经难以生存了,现在变成了针眼!
所以前段时间网红带货主播李佳琦在网上说:“找找自己的原因,工资涨了没涨,有没有认真工作”,是因为的认知出现了谬误,所以才说出了这种言论,而他的成功完全靠努力吗?你怎么看!
四、这和你有鸡毛关系!
浮躁来自于你的认知水平,在这个信息爆炸的时代,如果不能分辨真假是非,那么就很容易陷入浮躁的状态!
网络上和现实中总是充斥着一股“赚钱很容易”的妖风,他们去编造一些故事,制造一些假象来迷惑人的双眼,如果你的甄别能力不够,那么你就会觉得为啥别人那么厉害,自己为啥混成这样,从而陷入浮躁和迷茫之中,当你进入这个状态后,等待你的要么是镰刀,要么是内耗!
做人过程中的一大蠢事就是自己啥也不是的时候,总是去炫耀自己拥有的那些八竿子打不着的人脉和资源,被那些不知真假的事物去影响,去自我否定,当一个人不能独立去思考问题,不站在现实角度去看待问题的时候,那么是永远不可能获得成长的。
五、最后
现实中,很多人都是很窘迫的,赚到钱的人永远在少数,这是时代特征和个人运气所决定的,努力只占了很小一部分,所以别被互联网上的一些妖风所影响!
这个时代我们虽然能决定的东西很少,事物都充满不确定性,但是依然要如罗曼罗兰说的那样“世界上只有一种英雄主义,看清生活的真相依然热爱生活”,正因为充满不确定性,所以才有“赌”的意义!
来源:juejin.cn/post/7289692200161329210
axios中的那些天才代码!看完我实力大涨!
axios的两种调用方式
经常调接口的同学一定非常熟悉aixos下面的两种使用方式:
- axios(config)
// 配置式请求
axios({
method: 'post',
url: '/user/12345',
});
- axios.post(url, config)
// 简洁的写法
axios.post('/user/12345')
不知道各位大佬有没有思考过这样的问题:
axios到底是个什么东西?我们为什么可以使用这两种方式请求接口呢?axios是怎么设计的?
axios原理简析
为了搞明白上面的问题,我们先按照传统思路仿照axios源码实现一个简单的axios。
手写一个简单的axios
创建一个构造函数
function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}
上面的代码中,我们实现了一个基本的Axios类,但它还不具备任何功能。我们现在给它添加功能。
原型上添加方法
Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}
上面的代码中,我们在request属性上创建了一个通用的接口请求方法,get和post实际都调用了request,但内部传递了不同的参数,这和axios(config)、axios.post()有异曲同工之妙。
参考aixos的用法, 现在,我们需要创建实例对象
let aixos = new Axios(config)
创建后的axios包含defaults
和interceptors
属性,其对象原型__proto__
上(指向Axios的prototype)包含request、get及post方法,因此,我们现在可以使用aixos.post()
的方法模拟调用接口了。
但注意,此时aixos只是一个实例对象,不是一个函数!我们似乎也没办法做到改造代码使用aixos(config)
的形式调用接口!
aixos是如何实现的呢?
aixos中的天才想法
为了即能使用axios(config)又能使用axios.get(),axios的核心伪逻辑如下
function Axios(config){
this.defaults = config; // 配置对象
this.interceptors = { // 拦截器对象
request:{},
response:{}
}
}
Axios.prototype.request = function(config){
console.log('发送Ajax 请求 type:' +config.method)
}
Axios.prototype.get = function(){
return this.request({method:'GET'})
}
Axios.prototype.post = function(){
return this.request({method: 'POST'})
}
function createInstance(config) {
//注意instance是函数
const instance = Axios.prototype.request;
instance.get = Axios.prototype.get
instance.post = Axios.prototype.post
return instance;
}
let axios = createInstance();
通过上述的伪代码,我们可以知道axios是createInstance()函数的返回值instance。
- instance 是一个函数,因此,axios也是一个函数,可以使用axios(config);
- instance也是一个对象(js万物皆对象),其原型上有get方法和post方法,因此,我们可以使用axios.post()。
我们看看aixos的源码
aixos的源码实现
function createInstance(config) {
//实例化一个对象
var context = new Axios(config); //但是不能直接当函数使用
var instance = Axios.prototype.request.bind(context);
//instance 是一个函数,并且可以 instance({}),
//将Axios.prototype 对象中的方法添加到instance函数中,让instance拥有get、post、request等方法属性
Object.keys(Axios.prototype).forEach(key => {
// console.log(key); //修改this指向context
instance[key] = Axios.prototype[key].bind(context);
})
//总结一下,到此instance自身即相当于Axios原型的request方法,
//然后又给instance的属性添加了上了Axios原型的request、get、post方法属性
//然后调用instance自身或instance的方法属性时,修改了this指向context这个Axios实例对象
//为instance函数对象添加属性 default 与 intercetors
Object.keys(context).forEach(key => {
instance[key] = context[key];
})
return instance;
}
可以说,上面的代码真的写的精妙绝伦啊!
注意这里,为什么要修改this的指向
var instance = Axios.prototype.request.bind(context);
首先,requset 是Axios原型对象上的方法,其方法内部的this指向的是其实例化对象context!
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
因此,如果我们直接使用Axios.prototype.request()
就会出现问题,因为这事reques方法内部的this会指向错误,导致函数不能运行,因此,我们必须将this重新指向其实例化对象。
来源:juejin.cn/post/7387029190620184611
后端同事下班早,前端排序我来搞
写掘金博客有一小段时间了,我发现了一个秘密。文章阅读量小了吧,很心烦,阅读量大了吧,更心烦。很多特别懂特别会的大哥们就会来“指点”我。感谢这些大哥们的“指点”,使我进步。后来我和群里的小伙伴们讨论了一下,为什么掘金文章会有那么多大哥们来“开心指点”呢?大概是这么几种情况:
1 他们爱学习,爱分享,但是即将要被裁员了,所以心情不是太好,怎么办呢?去掘金上指点他们去,让他们知道自己很会很懂;
2 他们在团队中就是翘楚,指点完了团队的人,然后不放心,怕世界不足够完美,反正自己也要被裁员了,有时间,然后补偿没拿够,心情不太好,去给他们指点指点去;
3 他们一直觉得自己不太懂,但是看见文章又想指点指点,所以呢,在家找了3个月工作这段时间,一边学习,然后看大家的文章,学习过程中呢,看哪里觉得不太合适,指点指点,希望趁着阅读量高的文章,好希望有人能发现他们,直接邀约他们入职;
这种人呢,我们总结了一下,他们可以叫“黑哥会”,意思就是黑哥们,比较会,啥都会那种,嗯,希望黑哥会的成员们早日找到心仪的工作,在家闲着不好的。
好啦,在文章正式开始之前呢,告诉大家个好消息,本文点赞,友善评论,友善建议的大哥大姐们,2024年的后半年,一定能够心想事成,工作顺利,家庭和睦,一直到永久。
321... 文本正式开始。
1 未排序的数据
今天早上来了公司,我赶紧喊老张,问:新来的前端妹子这么快就被你搞定啦?听说昨晚你俩10点一起出的公司?是不是,快说。 老张,猛地抬头,问:你咋知道的?我保密工作做这么好。 我说:门口的李大爷说的。你快说说什么情况啊。
老张说:别瞎说,昨天后端下班早,把接口就给妹子了,妹子本来以为调一调接口,传几个参数完事,结果发现后端给的数据没有排序,但看了产品文档,发现,又要根据学生姓名按字母排序,又要根据分数排序,又要根据年龄排序,又要根据日期排序,直接把妹子气的快哭了,所以我就帮他弄了弄。然后就弄到10点了呗,一起出的公司而已,别瞎想。
但是妹子为了感谢我,告诉了我一个好消息,过会儿和你说。我说:你快点说。老张说:你先听我把功能说完,我再告诉你。
你看,后端就一个接口,给的数据大概是这样子:
const users = [
{"name": "小张伟", "age": 19, "score": 55, "dateTime": '2021-03-03 15:33:10'},
{"name": "张三", "age": 22, "score": 65, "dateTime": '2023-03-03 10:10:10'},
{"name": "李四", "age": 30, "score": 87, "dateTime": '2024-04-03 10:10:10'},
{"name": "阿斌", "age": 50, "score": 90, "dateTime": '2021-03-03 10:10:10'},
{"name": "曹小操", "age": 1300, "score": 23, "dateTime": '1021-05-08 10:10:10'},
{"name": "小张灰", "age": 31, "score": 15, "dateTime": '1994-03-04 08:33:10'},
];
2 根据属性排序
这是一个杂乱的json型数组,但是要根据属性进行排序。我们目前做了3种类型的实现
2.1 引入工具库
这里说一个高效便捷功能丰富的前端JS库,首先引入js-tool-big-box工具库。
执行安装命令:
npm install js-tool-big-box
引入dataBox对象,排序的这些公共方法被放到了这个对象下面:
import { dataBox } from 'js-tool-big-box';
2.2 数值型排序
数值型排序呢,就是,你看,age 和 score 都是数值型的,我们把这些归结为一类进行排序。
2.2.1 根据age从小到大的排序
代码如下:
const ageResult1 = dataBox.sortByNumber(users, 'age');
console.log('age是数值型,从小到大,排序后的值为:', ageResult1);
结果如下:
2.2.2 根据age从大到小的排序
代码如下:
const ageResult2 = dataBox.sortByNumber(users, 'age', 1);
console.log('age是数值型,从大到小,排序后的值为:', ageResult2);
结果如下:
2.2.3 根据score从低到高的排序
代码如下:
const ageResult3 = dataBox.sortByNumber(users, 'score');
console.log('score是数值型,从低到高,排序后的值为:', ageResult3);
结果如下:
2.2.4 根据score从高到低的排序
代码如下:
const ageResult4 = dataBox.sortByNumber(users, 'score', 1);
console.log('score是数值型,从大到小,排序后的值为:', ageResult4);
结果如下:
2.3 中文按字母排序
比如我们的姓名,很多时候需要按字母从A到Z来展示,这个时候就可以用下面这个方法来快速实现:
2.3.1 按字母从A到Z排序
代码如下:
const nameResult1 = dataBox.sortByletter(users, 'name');
console.log('比如name,我们按照字母顺序排序后为:', nameResult1);
结果如下:
2.3.2 按字母从Z到A排序
代码如下:
const nameResult2 = dataBox.sortByletter(users, 'name', 1);
console.log('比如name,我们按照字母顺序倒序排序后为:', nameResult2);
结果如下:
2.3.3 注意
需要注意的是,我们这里只是传入了name的属性,如果这个json中有其他中文属性,也是可以使用这个方法进行按字母排序的,很灵活。
2.4 按日期时间排序
比如我们例子中的时间,按时间排序也是非常实用且常见的需求,
2.4.1 按时间从早到晚排序
代码如下:
const timeResult1 = dataBox.sortByTime(users, 'dateTime');
console.log('以时间从早到晚排序后的值为:', timeResult1);
结果如下:
2.4.2 按时间从晚到早排序
代码如下:
const timeResult2 = dataBox.sortByTime(users, 'dateTime', 1);
console.log('以时间从晚到早排序后的值为:', timeResult2);
结果如下:
2.4.3 注意
需要注意的是,我们例子中只是传入了dateTime属性,如果json对象中有其他的是时间格式的属性值,也可以把属性传入,就可以进行字段的属性排序啦,很便捷。
3 最后
把效果展示完了,我赶紧催促老张说:你刚才跟我说的好消息呢?老张悄声说:妹子和门口老大爷,还有咱们公司老板都姓李,你品去吧。妹子跟我说了,她跟她爸爸说:这个季度的优秀就是我。我一听也跟着高兴起来,希望看到这篇文章的大哥大姐们,也都能像老张一样,升职加薪,变得越来越优秀。
最后告诉你个消息:
js-tool-big-box的npm地址是(js-tool-big-box 的npm地址)
js-tool-big-box的git仓库地址(js-tool-big-box的代码仓库地址)
来源:juejin.cn/post/7384419675073789991
因为打包太慢,我没吃上午饭
事情的起因是这样的:
鄙人呢,在公司负责一个小小的后台管理系统。
这天中午临近饭点的时候,测试小哥坐我工位旁边现场监督我改一个Bug。
Bug本身它倒是不复杂,甚至时隔几天之后我已不记清具体内容是什么。当时只见我是三下C两下V、提交、合并测试分支、登录Jekins点击deploy,一顿操作如行云流水一般丝滑。
说时迟,那时快。公司给 极品廉价劳动力们 我们安排的午饭送到了,一人一份。但你也知道,这两年经济下行,无人幸免;有的人食不果腹,有的人衣不蔽体, 保不齐谁饿的紧,拿了两份饭,那可就意味着有一个人要饿肚子,我可万万不希望那个倒霉蛋是我。
看着Jekins的deploy进度条,我对测试小哥说:
“你先回去,等会发完了你再看一下,应该没有问题,我先去干饭了”
说罢,我转头看向测试小哥:他面无表情盯着屏幕,似乎是默许了我的提议。于是我便准备起身——
只见他头也不回一手把我按住,缓缓吐出四个字:
“看完再吃”
...
...
...
大约半个小时后,KFC。
我:“我都告诉你了,不会有问题,先干饭,你非不听”
测试小哥:“......”
我:“这下好了吧,上个月的工资还没发,现在又来付费上班”
测试小哥:“我就问你,星期四的这个辣翅,它香不香”
我:“香”
罪魁祸首
所以,项目到底deploy了多久?
Jekins的记录中可以看到,编译打包环节耗时基本在五六分钟,最近几次成功构建的整体耗时平均在5分50秒。
这个项目本身呢,说大不大,说小也不算小。是个普通CRUD
页面居多的管理后台,没有太多其他乱七八糟的东西。如果以页面、组件数量的维度来看:
使用资源管理器在项目的/src
目录下通配*.vue
可以看到有561个文件
说实话,这样的体量打包5-6分钟,属实有点过分。
我又找来公司的一个巨石应用来对比:由于巨石应用历史比较悠久,横跨了多个技术栈(HandleBars模板引擎、使用jQuery的原生HTML、Vue2),不能只看SFC
的数量,所以这次就来比较一下,用来存放页面文件的文件夹的体积。
我的项目
巨石应用
先不算其他的资源,单就页面文件体积已经接近5倍,如果其他东西都算上,打包时间就算没有5倍,一两倍总是要有的吧?
结果呢,时间甚至更短
好好好,有活干了。为了避免下次面试官问我对webpack做过什么优化时复述那些网上千篇一律的答案,现在就来实际操作一下。
本文真实记录了一次对项目构建耗时、产物体积优化的过程。没有对知识点系统的梳理,主要突出的是思路:面对问题时的解决思路。
日志分析
曾经有位技术能力超强的架构师说过:遇到问题不要慌,先看日志。
既然这么慢的构建过程是发生在Jekins上,那就先来捋一捋Jekins的log,有没有什么值得注意的地方。
这个项目的构建脚本中,抛开(Jekins)工具的准备、从git上拉取代码以及最后的部署这些动作,只看跟前端的打包有关的部分,命令很简单,只有四行:
rm -rf node_modules
rm package-lock.json
npm i
npm run build
在日志中体现如下图:
开局就是一记暴击!
11:22:08 + npm i
11:25:59 + added 1863 packages from 1199 contributors balabala...
...
...
11:25:59 + npm run build
...
...
合着这五六分钟的打包时间,安装依赖就占了一大半,阿西巴!
在继续往下进行之前,请允许我先介绍些项目的其他背景:
deploy脚本拉取代码这一步简单来说就是:cd进项目目录 -> git pull -> 切换至要构建的分支
项目的开发人员较少,算我在内三个人
项目的依赖变动频率十分低,以月或数月为单位
背景铺垫完了,开始研究npm i
为什么这么耗时,相关的命令有三句:
rm -rf node_modules
rm package-lock.json
npm i
其中npm i
这句是必须的,没什么好说的;rm -rf node_modules
和rm package-lock.json
这两句是变量,挨个做耗时的对比测试。
首先,我在本地使用跟Jekins上相同的node版本(14.16.1),使用相同的npm源(官方源),新建一个目录clone项目代码:
- 完整执行三行命令,耗时与Jekins上相差无几
- (此时已经有了
package-lock.json
文件)执行rm -rf node_modules
+npm i
,耗时极短 - (此时已经有了
node_modules
目录)执行rm package-lock.json
+npm i
,耗时也极短
第三步其实没什么意义,npm文档中有提到,这种情况就相当于梳理了node_modules的结构并生成了package-lock.json
,并没有安装任何东西。
而步骤一和步骤二之间为何耗时相差比较大,可以参考这篇我觉得npm install流程写的比较好的文章:简单来说就是花费了大量时间去远端获取包信息。
结合项目背景一,我们的package-lock.json
会提交到仓库,每次肯定都是最新的,所以Jekins deploy脚本中rm package-lock.json
这一步属实是没有必要,本地计算完了到Jekins上又来一遍,纯纯的浪费时间。
联系运维哥把测试环境的deploy脚本修改一下,去掉了rm package-lock.json
这一句,测试下耗时:
如图中下边两次构建,【编译打包】环节时间都去到了2分30秒左右,直接缩短了一半多。部署成功后,在测试环境的页面咔咔一顿点,似乎也没有什么依赖包引起的报错。
效果是不是很显著,你以为这就完了?不不不,图中那条49秒的构建我可不是大意截进去的,伪装成不经意的失误,就是为了丝滑的承上启下
既然这个项目的开发人员很少,而且依赖的添加/更新频率又极低,也就意味着每次npm i
所安装的东西,基本都是一样的,既然都一样,为什么我还要rm -rf node_modules
再安装?
想象一下你日常本地开发时,如果某次需求要用到一个新的npm包,你一定是先npm install xxx
,除非碰到了依赖冲突,否则不会清除node_moduels
重新安装。
明明npm
提供了梳理依赖树、只做局部更新的逻辑,我们却偏偏每次清除node_modules
再install
,这种行为吧,我感觉就像明明是个Vue
项目,却在里边到处使用Document API
。
哎嘿,我就不用你的响应式,就是玩~
冒着被打的风险又私聊了运维哥,把rm -rf node_modules
去掉,再发布了一次看看效果
优秀!打包时间从5分多直接干到了50秒,优化率80%+!
本文结束!
在正式结束前,觉得还是有必要补充两点
- 各位读者在做打包优化时,部署脚本是否清除
package-lock.json
和node_modules
还是主要取决于项目实际情况和团队协作模式,不能因为一味的追求构建速度而导致频繁的构建失败/安装依赖失败[滑稽]
- 如果您经过深思熟虑后觉得还是有必要清除
package-lock.json
和node_modules
,山人还有一计可供大王优化构建速度:打包时离不开babel,但babel又是个老大难,好在它能缓存转换结果。一般情况下缓存会放在项目目录下的/node_modules/.cache/
,那我们把删除node_modules
的命令稍微改那么亿点:
find node_modules/ -mindepth 1 -maxdepth 1 ! -name '.cache' -exec rm -rf {} +
删除
node_modules
里面除了/.cache
目录以外的其他内容,这样在构建过程中babel还是能使用到之前的缓存。那速度,体验过的都说好!(看babel-loader的缓存文件有多大)
全面升级
如果是本着以后不影响吃午饭的目的,那现在确实可以结束了。但我自幼便深受中国四大名句之一来都来了的兄弟句式——干都干了的文化熏陶:既然已经开始了,那就干脆给项目做个全套大保健!
不过此时我和在座的各位都一样,对打包优化这块着实没什么经验,可以说是毫无头绪。
浅浅百度了下webpack打包优化,有两个工具基本每篇文章都有提到:打包耗时分析speed-measure-webpack-plugin
、打包体积分析webpack-bundle-analyzer
(vue-cli内置)
目前的痛点是慢,那就先来个耗时分析试试水。
使用方法还是老样子,自己去查,别人都写的我就不再重复写了。
效果如图:
此时因为babel
和eslint
还没有缓存,耗时多是意料之中的;其他的loader或多或少的三两组合,展示了一个module计数和耗时小计,我从中并没有办法获得什么有用的信息;并且多次构建对比发现loader组合的规律和耗时的排名也无迹可寻。在我的认知里:所有被命中的文件会按照loader
配置的顺序依次处理,所以面对这样的结果我实在是无计可施。(有会看的朋友可以补充一下)
翻看speed-measure-webpack-plugin
的文档,发现有可以打印耗时top N文件的配置项,但开启后再次构建得到的这些文件,同样令我摸不着头脑:一个寥寥数十行的SFC
小组件,css-loader
耗时竟然能用四到五秒!要知道里边只有一条scoped
的样式规则。
无奈只好放弃,看了下项目用的是vue-cli@4.x
创建的,对应的webpack@4.x
,那就去webpack
的文档里逛逛碰碰运气吧!
可惜,福无双至祸不单行。文档里翻了半天,耳熟能详、配置简单的路子,例如babel-loader、eslint-loader的编译缓存
、多线程打包
、chunk分割
、代码混淆压缩
、tree shaking
这些,要么是之前已经被配置过了,要么是webpack
内置了。而复杂、高级一些的优化方式,我的项目又用不到...
直到我看到了这里:
升级webpack
简单(呸),npm upgrade webpack
嘛,先来搞这个~~
回到项目的package.json
里,咦,好奇怪,没有webpack
,也没有vue-cli
。
vue-cli
是装在全局的,而webpack
是作为依赖的依赖安装的,没有体现在package.json
中,所以直接npm upgrade webpack
应该是不行的。vue-cli
的文档提供了一个升级的命令:vue upgrade
既然要升级,干脆全上新的!Node
也给他干到20!(我也不知道我当时为什么要这么做,但这为后来的事情埋下了伏笔。。。)
vue-cli
升级完,扫了一遍webpack升级指南,发现我项目里的配置文件也没什么好改的,Nice!
本地浅浅的run了一下server
、run了一下build
,发现也都OK!那就提交上去在Jekins上试试Node V14
o不ok
emmmm...
报错倒是没报错,只是...
本地build
的时候没注意,Jekins上跑才发现,怎么慢了这么多!说好的更新到最新版本均有助于提高性能呢?
再看看这构建物的体积
Hà的我赶紧又本地build
了一次,还真让我发现了些东西:似乎build
了两次
按理来说应该只有下边这个print,那上边的legacy bundle
又是什么东西?百度上随便那么一搜,应该是不少人都被这么坑过,很容易搜到:这是一种兼容性的构建产物,主要是为了兼容一些很古老版本的浏览器/客户端。想控制也很容易,改package.json
里的browserslist
字段即可。
这就好办了,这项目是我们公司的内部项目,考虑兼容性?不存在的。
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
配置了之后又试了下,基本恢复到了升级webpack
之前的水平,但还是慢一点点...
构建速度的优化这块,实在是没头绪了,明明升级了webpack
版本,构建速度却变慢了。
不过刚才提到的两个工具,构建耗时分析的用过了,还有个vue-cli
内置的构建体积分析工具没体验;如果需要打包的东西变少了,那构建速度应该也能快一点吧!(吧?)
塑形瘦身
在正式瘦身前,有一个小插曲:
不知道在座的各位,项目里有没有这样的东西
console.log(123123)
// or
console.log('asdfasdf')
我是一个崇尚极简的人,我能接受的底线也就是
console.log('list data: ', data);
仅此而已
你要打印接口返回数据,Network
里能看
你要打印函数中某个变量的值,可以打断点
我实在是想不出什么必须console.log
的场景
如果你说为了方便线上调试
我能接受的最多也就是按规范打印有意义的log
更别提项目首屏就要翻好几页的无意义log,要知道,大量的console.log
也是会影响首屏加载性能的
在之前,我通过husky + lint-staged
进行过限制,但还是有人以我这个有用、这之前不是我写的等等诸多借口绕过了eslint
检测,提交了无意义的log。所以这次我最终还是决定,你不仁就休怪我不义,TerserPlugin
drop_console
走起,本地开发你随便log,只要发到线上我就删掉。
{
plugins: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ["console.log", "console.info"],
},
},
})
]
}
毕竟删掉几句console.log
,也算瘦身
接着就webpack-bundle-analyzer
走起,vue-cli
内置的使用方式是
vue-cli-service build --report
打包后会在你输出的目录里边生成一个report.html
,当时的截图找不到了,用语言描述一下就是:从node_moduels
里打进去的依赖包,面积直接占了整个屏幕的大概三分之一。那个图网上很容易找到,内容就是打包产物按照体积和来源绘制成一个个的矩形在页面里。
这其实是好事,打进去的依赖包多,我们的可操作空间就大,先拿Vue
开个刀。
// vue.config.js
module.exports = {
// ...
configureWebpack: {
externals: {
vue: "Vue",
},
},
// ...
}
也不要忘了把package.json
里的vue
依赖删除掉、在/public
的模版HTML
中,通过<script>
引入CDN文件。
再打个包看看效果:
可以看到vue
确实咩有了
但在调试的过程中,发现第三方CDN不稳定,时而获取超时
为了保险起见,只得把CDN文件copy到本地/public
里来(我司没有自己的CDN或者依赖私仓,正在筹备中)
暂时没什么问题了,下一个就是我们的UI组件库ant-design-vue@1.7.8
,按照相同的方式配置一下,不过这次运行后有报错了:
可以看到报错是和moment
有关系,在antd的文档也找到了原因:如果使用已经构建好的文件,要自行引入moment
为什么antdv不做按需引入?原因有二:
- 项目的入口
main.js
中全量导入了antdv
进行注册,页面中直接使用。如果要改成按需引入,要么每个页面里新增按需引入的语句,要么统计使用了哪些组件在main.js
里改为按需引入(似乎有plugin解决这个问题,记不太清了)
- 按需的这个需,基本等于全量了。。。粗略的扫了一下文档,除了像
Comment
和Mentions
这种带有互动性质的组件,其他的基本都用上了,所以改按需好像意义也不大
改moment
的时候国际化有一个小问题,CDN网站一般会提供以下几种文件:
- 无国际化的
moment
主体文件 - 带全部语言包的
moment
主体文件 - 单个语言包文件(无功能)
如果没有国际化的需求,那是万万没有必要引入全部语言包的moment
。但moment
默认是英语,至少需要引入一个中文语言包。碰巧antdv
也需要做国际化处理,是相同的问题。
moment
和antdv
的国际化方式很相似:
<a-config-provider :locale="antdLocale" />
moment.locale(momentLocale);
data() {
return {
antdLocale,
momentLocale
}
}
我们只需要知道这个locale
运行时的值,把它提取出来就行了。打印后发现其实就是个很简单的key-value
对象(不是JSON
),在node_modules
中的源码里找到 它们复制出来在/public
下新建zhCN
文件:
window.momentLocale = xxx /* 复制出来的对象 */
window.antdLocale = xxx /* 复制出来的对象 */
使用时:
<a-config-provider :locale="antdLocale" />
moment.locale(momentLocale);
data() {
return {
antdLocale: window.antdLocale,
momentLocale: window.momentLocale
}
}
以后如果有别的依赖也有类似的国际化需求,继续向zhCN.js
里添加就行。只需要新增一个http
请求,就解决了所有依赖的国际化问题。
剔除了antdv
和moment
之后的report.html
:
惊喜的发现,antdv
的icons
也被一起干掉了。
少了这么几个大家伙,此时必须要Jekins上build一波看看效果!
还记得之前把Node
给升到20了吗
于是就...报错了...Node
版本太低...
本地切回NodeV14
,发现连server
也起不来了。。
摸黑前行
预警:这将是一段枯燥且艰难的黑暗时光
搞过的都知道,处理Node
版本兼容问题时,如果是需要升级还好;如果是要降级,Node
内置的各种包会出现稀奇古怪的报错,而且这些报错还难以trace
...
由于这趴的问题实在过于稀奇,甚至在google上都搜不到有用的信息,所以基本都没有截图,但我会尽可能的描述出我对问题的看法。看文字也许你觉得云淡风轻解决起来很轻松,但实际上花费了我接近一整天的时间以及一撮撮掉落的头发...
1. npm run server
出现大量的.vue
单文件报错
具体的报错信息记不太清,但报错顺序与路由表注册的顺序相符(和动态路由懒加载是两码事,路由懒加载是在运行时访问到页面才会加载对应的chunk
,但编译打包时,只要是代码中webpack
能trace
到的文件,都会被处理)。目测是所有的.vue
都有报错,那问题就应该不是出在代码上,而是整体配置上。
翻看vue-loader
文档时看到了这个
升级vue-cli
时确实也升级了vue-loader
,按照指引配置了下,resolve
2. jsx语法报错
这个问题就有点奇怪了,在升级前,是没有给webpack
做过什么支持jsx
语法的配置的。升级后,却都报错了。
翻阅了一些资料和支持jsx
的解决方案,大部分都是说把SFC
的<script>
加上lang="jsx"
,里边的内容全部当作jsx
解析。这种方式对eslint
和babel
的配置改动比较大,曾数次尝试无法成功,最后都把所有配置还原重新开始。
后来灵光一闪,不如直接用刚更新的vue-cli
创建一个新项目,看是否支持,如果可行的话,直接把各个配置文件照搬即可。
结果还真可以。babel.config.js
、vue.config.js
以及package.json
里eslintConfig
字段,先全部按照官方脚手架的配置改掉,成功启动之后再挨个把我们自定义的配置添加回去。这过程当中没有出现什么问题,且按下不表,resolve
3. 启动之后,页面白屏报错:(0 , vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent) is not a function
其中resolveComponent
也有可能是其他一些Vue3
暴露出来的Api,通过打断点观察,推测是Vue
内部在初始化的时候出了问题。
不确定是哪里出了问题,但在把之前删除的Vue
依赖安装回来(只是开发环境会用到,打包不会打进去)以及把添加的VueLoaderPlugin
去掉以后,resolve
迎接黎明
以上这些问题解决以后,已经可以正常启动、打包项目了。但刚才的bundle analyzer
进行到一半还没结束,图中的第三方依赖库应该还有一些可以剔除掉的,比如隐藏在一个业务代码chunk
里的echarts
检索了代码后,发现有按需引入的:
import {xxx, xxx} from "echarts"
也有全量引入的:
import * as echarts from 'echarts';
在分析代码后整理了所使用到的echarts
Api和组件,把全量引入改为按需引入,重新打包后发现包体积没有变。我好奇难道echarts
只要有一个地方使用了按需引入,其他地方也能自动分析把全量引入改为按需引入?遂把按需引入的也反向改为全量引入重新打包,结果:
第二次的改动体积变化了,那就只能说明....
问了写那段代码的同事,果然,在需求迭代的过程当中技术方案变更了,所以那个文件废弃掉没有用了,改了个寂寞...
此时还剩下jquery
和lodash
计划剔除掉,其他的依赖包有一部分已经是比较规范的按需引入,剔除掉改为cdn引入带来的收益不大,当然主要的原因还是因为懒
jquery
:这个npm包有点意思
打进来的是非压缩版本,因为package.json
中设置的main
就确实是这个,但dist
包中明明提供了压缩后的版本。两个版本的体积差距在三倍多,不知包作者的意图是什么
但最后还是把jquery
这个依赖彻底放弃了:整个项目中只有一个远古时期添加的图片预览组件依赖了它,而我们现在开发了样式、功能更为强大的新组件,所以把所有使用到这个组件的地方都改为使用新组件,然后顺带把jquery uninstall
了。
lodash
:官网本身提供了可按需引入的版本lodash-es
,但项目中太多地方都是全量引入的方式在使用
import * as _ from "lodash"
暂且先改成CDN的方式全量引入
至此,bundle analyzer
的分析图变成了这样:
三方依赖的chunk
已经比包含了echarts
的那个业务代码chunk
体积还要小。瘦身瘦到这里感觉差不多了,那些更小的依赖包本身体积不大,换成一个http
请求也未必是一件划算的事。
然后就还是回到webpack
的配置上来,前边一直在琢磨怎么添加配置去做优化,但vue-cli
本身已经封装了一套久经考验的配置,不如从这个配置着手,看能否针对我们项目的实际情况做一些修改
获取配置命令(融合了自定义的配置)
vue inspect --mode=production > file-name.js
mode
不传的话默认是development
。下载下来打开,1400多行猛的一看似乎有点唬人,但实际上有1000行左右都是对样式文件的loader
配置。
粗略的看下vue-cli@5.0.8
中有哪些值得注意的配置
- 解析文件的优先级
// 导入模块时如果不提供文件后缀,同名文件 后缀名的优先级
extensions: [".mjs", ".js", ".jsx", ".vue", ".json", ".wasm"]
- Hash
optimization: {
realContentHash: false, // 使用非严格的hash计算,减少耗时
}
- 代码压缩:
css
使用的是CssMinimizerPlugin
,js
使用的是TerserPlugin
minimizer: [
// 已经内置了js压缩工具terser
new TerserPlugin({
terserOptions: {
compress: {
arrows: false,
collapse_vars: false,
comparisons: false,
computed_props: false,
hoist_funs: false,
hoist_props: false,
hoist_vars: false,
inline: false,
loops: false,
negate_iife: false,
properties: false,
reduce_funcs: false,
reduce_vars: false,
switches: false,
toplevel: false,
typeofs: false,
booleans: true,
if_return: true,
sequences: true,
unused: true,
conditionals: true,
dead_code: true,
evaluate: true,
},
mangle: {
safari10: true, // 代码混淆时兼容使用`let`关键字声明的循环迭代器变量可能会出现无法重复声明let变量的错误。
},
},
parallel: true, // 多进程打包
extractComments: false, // 不将注释单独提取到一个文件中
}),
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: {
preset: [
"default",
{
mergeLonghand: false,
cssDeclarationSorter: false,
},
],
},
}),
]
- Loader
- 大量的篇幅编排不同样式文件相关的
Loader
,分别有css
、postcss
、scss
、sass
、less
、stylus
,按照css moduels in SFC
->SFC style
->normal css modules
->normal css
的顺序依次处理。 - 对于脚本文件,已经开启了多线程转译以及babel缓存功能
- 大量的篇幅编排不同样式文件相关的
{
test: /\.m?jsx?$/,
exclude: [
function () {
/* omitted long function */
},
],
use: [
{
loader:
"path-to-your-project/node_modules/thread-loader/dist/cjs.js",
},
{
loader:
"path-to-your-project/node_modules/babel-loader/lib/index.js",
options: {
cacheCompression: false,
cacheDirectory:
"path-to-your-project/node_modules/.cache/babel-loader",
cacheIdentifier: "1d489a9c",
},
},
],
}
- Plugin
VueLoaderPlugin
:已经内置了DefinePlugin
:注入编译时的全局配置CaseSensitivePathsPlugin
:路径的大小写严格匹配FriendlyErrorsWebpackPlugin
:优化报错信息MiniCssExtractPlugin
HtmlWebpackPlugin
CopyPlugin
:配置了info.minimized = true
,copy的同时也会压缩ESLintWebpackPlugin
:同样开启了缓存
得,不仅没找到有啥可优化的地方,甚至还污染了人自带的配置:
已经内置了TerserPlugin
,前边为了打包时去除console
在plugin
里边又配置了一次,通过speed-measure-webpack-plugin
分析时发现似乎是走了两遍TerserPlugin
。
只好通过webpack-chain
去注入一下,顺便把项目中其他修改webpack
配置的地方也改为注入的形式。(使用ConfigureWebpack
去改,无法改到已有的TerserPlugin
配置):
chainWebpack: (config) => {
config.when(process.env.NODE_ENV === "production", (config) => {
config.devtool(false);
config.optimization.minimizer("terser").tap((args) => {
const compress = args[0].terserOptions.compress;
args[0].terserOptions.compress = {
...compress,
drop_console: true,
pure_funcs: ["console.log", "console.info"],
};
return args;
});
});
config
.externals({
vue: "Vue",
moment: "moment",
"moment/locale/zh-cn": "moment.local",
"ant-design-vue": "antd",
lodash: "_",
})
.resolve.alias
.set("@", path.join(__dirname, "src"))
.set("@worker", path.resolve(__dirname, "public/worker.js"))
.end();
config.plugin("speed-measure").use(SpeedMeasurePlugin);
}
如果使用ConfigureWebpack
:
configureWebpack: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
pure_funcs: ["console.log", "console.info"],
},
},
}),
],
},
集成的配置最下方会出现一个新的minimizer
数组,不是我们想要的效果
截止到目前,构建速度变成这样(果然还是没有变更快)
从项目剔出去的第三方依赖,体积是这么多
不过虽然没打进chunk里去,但还是作为静态依赖在构建产物中,下一步就是搭一个公司内部的简易缓存服务器(有关缓存的内容可以看我上一篇文章)。届时这部分体积才算真正从项目里移除了,不过此时我们还是可以把它视作优化的成果,由于没有对别的类型的资源做什么优化处理,也压根就没什么别的资源,所以只看打包后的js体积的话:
优化前
优化后
数据也基本对的上,所以综合来看:
- 平均构建速度(average full time):从5分50秒减少到54秒,优化率84.48%(
1 - 54秒 / 3分50秒
) - 脚本构建体积(script size):从5.5M减少到3.6M,优化率35.55%(
1 - 3.6M / 5.5M
)
先这样吧,至少下次被问到webpack
,多少有点自己的东西能讲,近期可能也不会再更新文章了,原因嘛,你们懂的
欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~
来源:juejin.cn/post/7389044903940603945
2024 年中总结:务虚年代,逆风飞翔
大家好,我是双越老师,也是 wangEditor 作者
这是一篇散文,没啥结构性,比较随性,想到啥就写点啥吧。
不觉 2024 年过了 6 个月了,你有没有总结一下自己这半年做了啥?除了上班下班,有没有攒下一些钱?有没有收获一些美好的经历?自己的专业技能有没有得到提升?
如果你日常有积累、有主动去争取什么东西,哪怕每天一点点,积累半年就是一个大成就。如果你什么都没有,或很少,那时间也不会可怜你。
回顾我自己的工作日志,我这半年积累的还不错,比去年要好很多。是的,自由职业这么多年,我一直有记录工作日志的习惯,保持自律。
首先,我觉得我做的最好一件事儿,不是工作中的,是戒烟。我从 2023.8 开始戒烟,一直到现在 9 个月的时间了。戒烟也是我 2023 年做的最成功的一件事儿,2024 年继续。
戒烟是一个持久的事情,有些人戒 3 年还会复吸,我能继续保持到现在也是非常不容易。未来继续保持,这比工作成绩都重要。
戒烟,第一是保持身体的健康状态,例如现在嗓子清爽,跑步时呼吸通畅;第二是要让自己摆脱对某个东西的强依赖,而恢复自由的状态。因为烟瘾本质上就是尼古丁的戒断反应、就是生理du瘾,戒不掉就永远是它的奴隶。
今年春天开始,我也几乎不喝酒了。这半年聚餐吃饭无数次,但几乎没有喝过酒(偶尔一点点啤酒),我朋友戏称:你这戒烟戒酒,再往后就得戒色了...
我不懂葡萄酒,但那种纯麦啤酒,喝起来很香甜的那种,我还是比较喜欢的,当然得少喝。但日常聚餐的这些工业啤酒、白酒就算了,尤其白酒,本身就是一种反人类的饮料。爱喝白酒的人,都是为了快速获得酒精的刺激,跟吸烟是一个道理,所谓好酒就是既能让你快速获得酒精刺激、又不那么辣嗓子、第二天又不是很难受 —— 第一个目的最重要。
所以,我觉得和烟拜拜了,酒也该再见了,都是一类东西,都是瘾品(虽然我没酒瘾)。古人说的“酒色财气”都归属于“酒”类,因为古代没有烟。
同时,我一直坚持着每日跑步和 15 分钟的力量训练,规律生活早睡早起,所以整体感觉还不错。颈椎病依然还有(只要得了,就无法完全恢复)但不难受,也没啥影响。
再聊聊工作吧。
1月的时候,我实在是不知道干啥了。PS. 其实一整个 2023 年都不知道干啥,太闲了,所以戒烟找点成就感,没想到成功了。
当时的任务就是把我的两门 ke 程更新一下,一门是面试课(这个每年都更新一次),一门是《React仿问卷星 低代码》升级了服务端,使用 Nest.js 写的。
同时也有很多人找我 1v1 面试咨询,我记得春节假期还没结束,就开始预约了。集中忙了大概一个月,每天都和 2-3 个人聊,一边分析简历,一边绞尽脑汁的思考,一边写总结,一边聊,非常累。
但是后来到了 3、4 月发现,今年的行情真的太差了,之前的“金三银四”今年是一点都没体现出来,没体现出任何热度,倒是裁员不断。这和刚刚过去的 6.18 很像,也没体现出任何热度,就和 5 月一样。
大家就像一头一头的死猪,拿开水刺激已经不管用了,无论是公司还是个人。
在这种行情下,我能干啥呢?很多人说自己被裁员,找不到工作,其实我也有相似之处,你们就业环境不好,我也找不到突破口。怎么办呢?
要不我也像其他“讲师”(网红)一样,去深入搞一搞面试服务?开个训练营,多收一些钱,宣传一顿:保证就业率、涨薪多少……
其实能实打实的为学生服务的讲师也挺好的,为你备课,为你准备面试题,为你准备项目,价格公道,这已经很好了。还有一些机构跟骗子也差不多。
有专门的销售催着你,学费 1w 起步,承诺内推、承诺涨薪 xx —— 理性思考,当前这环境,哪儿这好事。
很惭愧,我虽然也是个自由职业,但对圈里的这些事儿,我还真不清楚。咨询了一个朋友才知道具体的事情。
后来我跟那位同学说:这样,你先让她给你安排内推机会,只要有了面试邀请,你就报名 —— 你看她还理不理你。
我当时想了一个月,我该干嘛呢,最终决定还是踏踏实实的做个项目吧,慢慢开发慢慢积累,做出一个扎实的产品才是长久之道。
而且也是一个真正的壁垒,自己长久积累做出来的项目,别人不可能一下子就做出来。但像面试服务这种东西,想搭建起来是很容易的,没啥壁垒,面试题、考点就那么多,找个技术讲师就齐了。最近有很多前端 A 哥、B 哥、C 哥的,都是这种机制,自己负责发广告招人,然后雇讲师上课。
但做项目做个啥呢?选题和方向也是需要思考的,主动思考是最难的事情。
第一,不能过于简单,技术和业务都得有一定复杂度,而且要是全栈的,前后端都有。也不能过于复杂,一个人搞个淘宝微信也搞不了。
第二,要是真实上线的项目,因为课程项目一直被吐槽为 demo ,所以这次我要做一个不一样的,这样才有差异化。
第三,还要带有一定的话题性和热度,和当前技术发展趋势要吻合。例如今年低代码不再热门了,就不要再用低代码这个话题了。所以我选择了 AI 方向。
第四,要考虑自己擅长的方向,发挥自己擅长的技术点。我擅长富文本编辑器领域,这个要充分利用起来。
于是这个项目就是,使用 Node 全栈 Next.js 开发 AIGC 知识库项目 划水AI ,参考 Notion AI ,基于 GPT 大模型,开发 AI 写作、AI 处理文本,这是当前的热门话题。
现在一期功能(文档管理、富文本编辑器、AI 写作)已经上线试用,二期(团队、分享、多人协同编辑)正在开发中,想加入学习的同学可以找我。
这个项目的研发过程,被我记录成了详细的 wiki 还有代码提交记录,我每做一步就记录一步,遇到一个 bug 我就记录一个 bug —— 这都是宝贵的项目经验。
现在是一个务虚的时代,务实的人太少了。务虚低成本高收益。务实,成本高,而且你还得能做出来呀!君子只动口,傻子才动手呢。 中国人自古以来就是:圣人坐而论道、舌战群儒,没有去动手做实验的。
我已 36 岁,还能有这种创造力和执行力其实挺不容易的。
30 岁之前大家都精力满满,对未来充满好奇和希望,对技术充满动力和热情,浑身都充满了创造力。但是慢慢熬到 35+ 就容易如老牛一般(王小波说的“被锤的公牛”)被工作生活磨的角都没了。
我最早开始写博客是 2014 年,最早开发 wangEditor 是 2015 年,至今已快 10 年了。即便强如周杰伦,他的创作黄金期也不到 10 年(随便举个例子,不是拿自己和周杰伦比,我也是周杰伦铁粉~)
总结一下。
半年,从一开始的迷茫、没有方向,到后来思考、分析、确定项目,再到后来开发、测试、一期上线。时间一晃而过,但回头一看确实积累了很多东西。
下半年继续,把二期、三期搞定并上线,用一年时间做出一款优秀的产品,教你亮瞎面试官的眼睛!
与君共勉~
来源:juejin.cn/post/7388891131037614089
听说你会架构设计?来,弄一个打车系统
目录
- 引言
- 网约车系统
- 需求设计
- 概要设计
- 详细设计
- 体验优化
- 小结
1.引言
1.1 台风来袭
深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。
对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日已经停课、晚上 7 点后停运。
由于下午 4 点停市,于是大部分公司都早早下班。其中有赶点下班的,像这样:
有提前下班的,像这样:
还有像我们这样要居家远程办公的:
1.2 崩溃打车
下午 4 点左右,公交和地铁都人满为患。
于是快下班(居家办公)的时候就想着打个车回家,然而打开滴滴之后:
排队人数 142 位,这个排队人数和时长,让我的心一下就拔凉拔凉的。
根据历史经验,在雨天打上车的应答时间得往后推半个小时左右。更何况,这还是台风天气!
滴滴啊滴滴,你就不能提前准备一下嘛,这个等待时长,会让你损失很多订单分成的。
但反过来想,这种紧急预警,也不能完全怪打车平台,毕竟,车辆调度也是需要一定时间的。在这种大家争相逃命(bushi 的时候,周围的车辆估计也不太够用。
卷起来
等着也是等着,于是就回到公司继续看技术文章。这时我突然想到,经过这次车辆紧急调度之后,如果我是滴滴的开发工程师,需要怎么处理这种情况呢?
如果滴滴的面试官在我眼前,他又会怎么考量候选人的技术深度和产品思维呢?
2. 设计一个“网约车系统”
面试官:“滴滴打车用过是吧!看你简历里写道会架构设计是吧,如果让你设计一个网约车系统,你会从哪些方面考虑呢?”
2.1 需求分析
网约车系统(比如滴滴)的核心功能是把乘客的打车订单发送给附件的网约车司机,司机接单后,到上车点接送乘客,乘客下车后完成订单。
其中,司机通过平台约定的比例抽取分成(70%-80%不等),乘客可以根据第三方平台的信用值(比如支付宝)来开通免密支付,在下车后自动支付订单。用例图如下:
乘客和司机都有注册登录功能,分属于乘客用户模块和司机用户模块。网约车系统的另外核心功能是乘客打车,订单分配,以及司机送单。
2.2 概要设计
网约车系统是互联网+共享资源的一种模式,目的是要把车辆和乘客结合起来,节约已有资源的一种方式,通常是一辆网约车对多个用户。
所以对于乘客和司机来说,他们和系统的交互关系是不同的。比如一个人一天可能只打一次车,而一个司机一天得拉好几趟活。
故我们需要开发两个 APP 应用,分别给乘客和司机打车和接单,架构图如下:
1)乘客视角
如上所示:乘客在手机 App 注册成为用户后,可以选择出发地和目的地,进行打车。
打车请求通过负载均衡服务器,经过请求转发等一系列筛选,然后到达 HTTP 网关集群,再由网关集群进行业务校验,调用相应的微服务。
例如,乘客在手机上获取个人用户信息,收藏的地址信息等,可以将请求转发到用户系统。需要叫车时,将出发地、目的地、个人位置等信息发送至打车系统。
2)司机视角
如上图所示:司机在手机 App 注册成为用户并开始接单后,打开手机的位置信息,通过 TCP 长连接定时将自己的位置信息发送给平台,同时也接收平台发布的订单消息。
司机 App 采用 TCP 长连接是因为要定时发送和接收系统消息,若采用 HTTP 推送:
一方面对实时性有影响,另一方面每次通信都得重新建立一次连接会有失体面(耗费资源)。
司机 App:每 3~5 秒向平台发送一次当前的位置信息,包括车辆经纬度,车头朝向等。TCP 服务器集群相当于网关,只是以 TCP 长连接的方式向 App 提供接入服务,地理位置服务负责管理司机的位置信息。
3)订单接收
网关集群充当业务系统的注册中心,负责安全过滤,业务限流,请求转发等工作。
业务由一个个独立部署的网关服务器组成,当请求过多时,可以通过负载均衡服务器将流量压力分散到不同的网关服务器上。
当用户打车时,通过负载均衡服务器将请求打到某一个网关服务器上,网关首先会调用订单系统,为用户创建一个打车订单(订单状态为 “已创建”),并存库。
然后网关服务器调用打车系统,打车系统将用户信息、用户位置、出发地、目的地等数据封装到一个消息包中,发送到消息队列(比如 RabbitMQ),等待系统为用户订单分配司机。
4)订单分配
订单分配系统作为消息队列的消费者,会实时监听队列中的订单。当获取到新的订单消息时,订单分配系统会将订单状态修改为 “订单分配中”,并存库。
然后,订单分配系统将用户信息、用户位置、出发地、目的地等信息发送给订单推送 SDK。
接着,订单推送 SDK 调用地理位置系统,获取司机的实时位置,再结合用户的上车点,选择最合适的司机进行派单,然后把订单消息发送到消息告警系统。这时,订单分配系统将订单状态修改为 “司机已接单” 状态。
订单消息通过专门的消息告警系统进行推送,通过 TCP 长连接将订单推送到匹配上的司机手机 App。
5)拒单和抢单
订单推送 SDK 在分配司机时,会考虑司机当前的订单是否完成。当分配到最合适的司机时,司机也可以根据自身情况选择 “拒单”,但是平台会记录下来评估司机的接单效率。
打车平台里,司机如果拒单太多,就可能在后续的一段时间里将分配订单的权重分数降低,影响自身的业绩。
订单分派逻辑也可以修改为允许附加的司机抢单,具体实现为:
当订单创建后,由订单推送 SDK 将订单消息推送到一定的地理位置范围内的司机 App,在范围内的司机接收到订单消息后可以抢单,抢单完成后,订单状态变为“已派单”。
2.3 详细设计
打车平台的详细设计,我们会关注网约车系统的一些核心功能,如:长连接管理、地址算法、体验优化等。
1)长连接的优势
除了网页上常用的 HTTP 短连接请求,比如:百度搜索一下,输入关键词就发起一个 HTTP 请求,这就是最常用的短连接。
但是大型 APP,尤其是涉及到消息推送的应用(如 QQ、微信、美团等应用),几乎都会搭建一套完整的 TCP 长连接通道。
一张图看懂长连接的优势:
图片来源:《美团点评移动网络优化实践》
通过上图,我们得出结论。相比短连接,长连接优势有三:
- 连接成功率高
- 网络延时低
- 收发消息稳定,不易丢失
2)长连接管理
前面说到了长连接的优势是实时性高,收发消息稳定,而打车系统里司机需要定期发送自身的位置信息,并实时接收订单数据,所以司机 App 采用 TCP 长连接的方式来接入系统。
和 HTTP 无状态连接不同的是,TCP 长连接是有状态的连接。所谓无状态,是指每次用户请求可以随意发送到某一台服务器上,且每台服务器的返回相同,用户不关心是哪台服务器处理的请求。
当然,现在 HTTP2.0 也可以是有状态的长连接,我们此处默认是 HTTP1.x 的情况。
而 TCP 长连接为了保证传输效率和实时性,服务器和用户的手机 App 需要保持长连接的状态,即有状态的连接。
所以司机 App 每次信息上报或消息推送时,都会通过一个特定的连接通道,司机 App 接收消息和发送消息的连接通道是固定不变的。
因此,司机端的 TCP 长连接需要进行专门管理,处理司机 App 和服务器的连接信息,架构图如下:
为了保证每次消息的接收和推送都能找到对应通道,我们需要维护一个司机 App 到 TCP 服务器的映射关系,可以用 Redis 进行保存。
当司机 App 第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,司机 App 会通过用户长连接管理系统重新申请一个服务器连接(可用地址存储在 Zookeeper 中),TCP 连接服务器后再刷新 Redis 的缓存。
3)地址算法
当乘客打车后,订单推送 SDK 会结合司机所在地理位置,结合一个地址算法,计算出最适合的司机进行派单。
目前,手机收集地理位置一般是收集经纬度信息。经度范围是东经 180 到西经 180,纬度范围是南纬 90 到北纬 90。
我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。
根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识司机和乘客的位置信息。再通过 Redis 的 GeoHash 算法,来获取乘客附加的所有司机信息。
GeoHash 算法的原理是将乘客的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有司机。
它的实现用到了跳表数据结构,具体实现为:
将某个市区的一块范围作为 GeoHash 的 key,这个市区范围内所有的司机存储到一个跳表中,当乘客的地理位置出现在这个市区范围时,获取该范围内所有的司机信息。然后进一步筛选出最近的司机信息,进行派单。
4)体验优化
1. 距离算法
作为线上派单,通过距离运算来分配订单效果一定会比较差,因为 Redis 计算的是两点之间的空间距离,但司机必须沿道路行驶过来,在复杂的城市路况下,也许几十米的空间距离行驶十几分钟也未可知。
所以,后续需综合行驶距离(而非空间距离)、司机车头朝向以及上车点进行路径规划,来计算区域内每个司机到达乘客的距离和时间。
更进一步,如果区域内有多个乘客和司机,就要考虑所有人的等待时间,以此来优化用户体验,节省派单时间,提升盈利额。
2. 订单优先级
如果打车订单频繁取消,可根据司机或乘客行为进行判责。判责后给乘客和司机计算信誉分,并告知用户信誉分会影响乘客和司机的使用体验,且关联到派单的优先级。
司机接单优先级
综合考虑司机的信誉分,投诉次数,司机的接单数等等,来给不同信誉分的司机分配不同的订单优先级。
乘客派单优先级
根据乘客的打车时间段,打车距离,上车点等信息,做成用户画像,以合理安排司机,或者适当杀熟(bushi。
PS:目前有些不良打车平台就是这么做的 🐶 甚至之前爆出某打车平台,会根据不同的手机系统,进行差异收费。
4. 小结
4.1 网约车平台发展
目前,全球网约车市场已经达到了数千亿美元的规模,主要竞争者包括滴滴、Uber、Grab 等公司。在中国,滴滴作为最大的网约车平台已经占据了绝大部分市场份额。
网约车的核心商业逻辑比较简单,利益关联方主要为平台、司机、车辆、消费者。
平台分别对接司机、车辆【非必选项,有很多司机是带车上岗】和乘客,通过有效供需匹配赚取整个共享经济链省下的钱。
具体表现为:乘客和司机分别通过网约平台打车和接单,平台提供技术支持。乘客为打车服务付费,平台从交易金额中抽成(10%-30%不等)。
据全国网约车监管信息交互平台统计,截至 2023 年 2 月底,全国共有 303 家网约车平台公司取得网约车平台经营许可。
这些平台一部分是依靠高德打车、百度地图、美团打车为代表的网约车聚合平台;另一部分则是以滴滴出行、花小猪、T3 为代表的出行平台。
4.2 网约车平台现状
随着出行的解封,网约车平台重现生机。
但由于部分网约车聚合平台的准入门槛太低,所以在过去一段时间里暴露出愈来愈多的问题。如车辆、司机合规率低,遇到安全事故,产生责任纠纷,乘客维权困难等等。
由于其特殊的模式,导致其与网约车运营商存在责任边界问题,一直游离在法律边缘。
但随着网约车聚合平台的监管不断落地,全国各地都出行了一定的监管条例。
比如某打车平台要求车辆将司机和乘客的沟通记录留档,除了司机与乘客的在线沟通记录必须保存以外,还需要一个语音电话或车载录音转换,留存一段时间备查。
有了这些人性化的监管条例和技术的不断创新,网约车平台或许会在未来的一段时间内,继续蓬勃发展。
后话
面试官:嗯,又专又红,全面发展!这小伙子不错,关注了~
来源:juejin.cn/post/7275211391102746684
何时使用Elasticsearch而不是MySql
MySQL 和 Elasticsearch 是两种不同的数据管理系统,它们各有优劣,适用于不同的场景。本文将从以下几个方面对它们进行比较和分析:
- 数据模型
- 查询语言
- 索引和搜索
- 分布式和高可用
- 性能和扩展性
- 使用场景
数据模型
MySQL 是一个关系型数据库管理系统(RDBMS),它使用表(table)来存储结构化的数据,每个表由多个行(row)和列(column)组成,每个列有一个预定义的数据类型,例如整数、字符串、日期等。MySQL 支持主键、外键、约束、触发器等关系型数据库的特性,以保证数据的完整性和一致性。
Elasticsearch 是一个基于 Lucene 的搜索引擎,它使用文档(document)来存储半结构化或非结构化的数据,每个文档由多个字段(field)组成,每个字段可以有不同的数据类型,例如文本、数字、布尔、数组等。Elasticsearch 支持动态映射(dynamic mapping),可以根据数据自动推断字段的类型和索引方式。
MySQL 和 Elasticsearch 的数据模型有以下几点区别:
- MySQL 的数据模型是严格的,需要事先定义好表的结构和约束,而 Elasticsearch 的数据模型是灵活的,可以随时添加或修改字段。
- MySQL 的数据模型是二维的,每个表只有行和列两个维度,而 Elasticsearch 的数据模型是多维的,每个文档可以有嵌套的对象或数组。
- MySQL 的数据模型是关系型的,可以通过连接(join)多个表来查询相关的数据,而 Elasticsearch 的数据模型是非关系型的,不支持连接操作,需要通过嵌套文档或父子文档来实现关联查询。
推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。
github 地址:github.com/wayn111/way…
查询语言
MySQL 使用标准的 SQL 语言来查询和操作数据,SQL 语言是一种声明式的语言,可以通过简洁的语法来表达复杂的逻辑。SQL 语言支持多种查询类型,例如选择(select)、插入(insert)、更新(update)、删除(delete)、聚合(aggregate)、排序(order by)、分组(gr0up by)、过滤(where)、连接(join)等。
Elasticsearch 使用 JSON 格式的查询 DSL(Domain Specific Language)来查询和操作数据,查询 DSL 是一种基于 Lucene 查询语法的语言,可以通过嵌套的 JSON 对象来构建复杂的查询。查询 DSL 支持多种查询类型,例如全文检索(full-text search)、结构化检索(structured search)、地理位置检索(geo search)、度量检索(metric search)等。
MySQL 和 Elasticsearch 的查询语言有以下几点区别:
- MySQL 的查询语言是通用的,可以用于任何关系型数据库系统,而 Elasticsearch 的查询语言是专用的,只能用于 Elasticsearch 系统。
- MySQL 的查询语言是字符串形式的,需要拼接或转义特殊字符,而 Elasticsearch 的查询语言是 JSON 形式的,可以直接使用对象或数组表示。
- MySQL 的查询语言是基于集合论和代数运算的,可以进行集合操作和数学运算,而 Elasticsearch 的查询语言是基于倒排索引和相关度评分的,可以进行全文匹配和相似度计算。
索引和搜索
MySQL 使用 B+树作为主要的索引结构,B+树是一种平衡多路搜索树,它可以有效地存储和检索有序的数据。MySQL 支持主键索引、唯一索引、普通索引、全文索引等多种索引类型,以加速不同类型的查询。MySQL 也支持外部存储引擎,例如 InnoDB、MyISAM、Memory 等,不同的存储引擎有不同的索引和锁机制。
Elasticsearch 使用倒排索引作为主要的索引结构,倒排索引是一种将文档中的词和文档的映射关系存储的数据结构,它可以有效地支持全文检索。Elasticsearch 支持多种分词器(analyzer)和分词过滤器(token filter),以对不同语言和场景的文本进行分词和处理。Elasticsearch 也支持多种搜索类型,例如布尔搜索(boolean search)、短语搜索(phrase search)、模糊搜索(fuzzy search)、通配符搜索(wildcard search)等,以实现不同精度和召回率的检索。
MySQL 和 Elasticsearch 的索引和搜索有以下几点区别:
- MySQL 的索引是基于数据的值的,可以精确地定位数据的位置,而 Elasticsearch 的索引是基于数据的内容的,可以近似地匹配数据的含义。
- MySQL 的索引是辅助的,需要手动创建和维护,而 Elasticsearch 的索引是主要的,自动创建和更新。
- MySQL 的索引是局部的,只针对单个表或列,而 Elasticsearch 的索引是全局的,涵盖所有文档和字段。
分布式和高可用
MySQL 是一个单机数据库系统,它只能运行在一台服务器上,如果服务器出现故障或负载过高,就会影响数据库的可用性和性能。为了解决这个问题,MySQL 提供了多种复制(replication)和集群(cluster)方案,例如主从复制(master-slave replication)、双主复制(master-master replication)、MySQL Cluster、MySQL Fabric 等,以实现数据的冗余和负载均衡。
Elasticsearch 是一个分布式数据库系统,它可以运行在多台服务器上,形成一个集群(cluster)。每个集群由多个节点(node)组成,每个节点可以承担不同的角色,例如主节点(master node)、数据节点(data node)、协调节点(coordinating node)等。每个节点可以存储多个索引(index),每个索引可以划分为多个分片(shard),每个分片可以有多个副本(replica)。Elasticsearch 通过一致性哈希算法(consistent hashing algorithm)来分配分片到不同的节点上,并通过心跳检测(heartbeat check)来监控节点的状态。如果某个节点出现故障或加入集群,Elasticsearch 会自动进行分片的重新分配和平衡。
MySQL 和 Elasticsearch 的分布式和高可用有以下几点区别:
- MySQL 的分布式和高可用是可选的,需要额外配置和管理,而 Elasticsearch 的分布式和高可用是内置的,无需额外操作。
- MySQL 的分布式和高可用是基于复制或共享存储的,需要保证数据一致性或可用性之间的权衡,而 Elasticsearch 的分布式和高可用是基于分片和副本的,可以根据需求调整数据冗余度或容错能力。
- MySQL 的分布式和高可用是静态的,需要手动扩展或缩容集群规模,而 Elasticsearch 的分布式和高可用是动态的,可以自动适应集群变化。
性能和扩展性
MySQL 是一个面向事务(transaction)的数据库系统,它支持 ACID 特性(原子性、一致性、隔离性、持久性),以保证数据操作的正确性和完整性。MySQL 使用锁机制来实现事务隔离级别(isolation level),不同的隔离级别有不同的并发性能和一致性保证。MySQL 也使用缓冲池(buffer pool)来缓存数据和索引,以提高查询效率。MySQL 的性能主要取决于硬件资源、存储引擎、索引设计、查询优化等因素。
Elasticsearch 是一个面向搜索(search)的数据库系统,它支持近实时(near real-time)的索引和查询,以保证数据操作的及时性和灵活性。Elasticsearch 使用分片和副本来实现数据的分布式存储和并行处理,不同的分片数和副本数有不同的写入吞吐量和读取延迟。Elasticsearch 也使用缓存(cache)和内存映射文件(memory-mapped file)来加速数据和索引的访问,以提高搜索效率。Elasticsearch 的性能主要取决于集群规模、分片策略、文档结构、查询复杂度等因素。
MySQL 和 Elasticsearch 的性能和扩展性有以下几点区别:
- MySQL 的性能和扩展性是有限的,它受到单机资源、锁竞争、复制延迟等因素的限制,而 Elasticsearch 的性能和扩展性是无限的,它可以通过增加节点、分片、副本等方式来水平扩展集群。
- MySQL 的性能和扩展性是以牺牲搜索能力为代价的,它不能支持复杂的全文检索和相关度评分,而 Elasticsearch 的性能和扩展性是以牺牲事务能力为代价的,它不能保证数据操作的原子性和一致性。
- MySQL 的性能和扩展性是以提高写入速度为目标的,它优化了数据插入和更新的效率,而 Elasticsearch 的性能和扩展性是以提高读取速度为目标的,它优化了数据检索和分析的效率。
使用场景
MySQL 和 Elasticsearch 适用于不同的使用场景,根据不同的业务需求,可以选择合适的数据库系统或组合使用两者。以下是一些常见的使用场景:
- 如果需要存储结构化或半结构化的数据,并且需要保证数据操作的正确性和完整性,可以选择 MySQL 作为主要数据库系统。例如,电商网站、社交网络、博客平台等。
- 如果需要存储非结构化或多样化的数据,并且需要支持复杂的全文检索和相关度评分,可以选择 Elasticsearch 作为主要数据库系统。例如搜索引擎、日志分析、推荐系统等。
- 如果需要存储和分析大量的时序数据,并且需要支持实时的聚合和可视化,可以选择Elasticsearch作为主要数据库系统。例如,物联网、监控系统、金融市场等。
- 如果需要同时满足上述两种需求,并且可以容忍一定程度的数据不一致或延迟,可以将 MySQL 作为主数据库系统,并将部分数据同步到 Elasticsearch 作为辅助数据库系统。例如新闻网站、电影网站、招聘网站等。
自此本文讲解内容到此结束,感谢您的阅读,希望本文对您有所帮助。
来源:juejin.cn/post/7264528507932327948
无插件实现一个好看的甘特图
效果
预览地址 code.juejin.cn/pen/7272286…
前言
刚好看到这么一个东西,甘特图,然后又发现好像echarts 里面没有这个图形渲染,也去找了一下插件,都不符合我的要求,然后自己就想着看能不能实现一个,然后说干就干,过程还挺复杂的,不过一想清楚了,也就还行。
逻辑
刚开始看着这个图,想到肯定是用表格来实现的,后面开始渲染的时候,发现是我想简单了,这表格是渲染不出来的,也或者是我技术还没够,反正我找了很多相关的插件,都不是用表格去实现的,后面就改变了一些思路,用div去渲染,然后去定位,这么一想,发现事情就简单了许多。
为什么不用表格实现
每点击一个视图切换,表头就需要重新渲染,表头有索引,名称,负责人,如果是表格实现的话,就是如下代码,一看就知道了id=“tableYear”的不好渲染,因为上面有可能是年,也有可能是月,所以肯定是有循环的,但如果一循环,它们没有共同的父容器,怎么渲染呢,也想过表格里面套表格,但那样,样式又实现不了我要的效果。
<table>
<thead>
<tr>
<th rowspan="2">id</th>
<th rowspan="2">任务名称</th>
<th rowspan="2">负责人</th>
<th colspan="4" id="tableYear">2023-8</th>
</tr>
<tr id="tableDay">
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tr>
</thead>
</table>
第一个难点
日期渲染,因为我这表头是动态的,所以要复杂一些,有天数,月数,季度和年度显示的
点击日视图,就是按天去显示任务,月视图就是按月去显示任务,季视图和年视图同理,
比如说日视图,
先获取当前日期,年月日,然后是想渲染前后几年的
var currentDate=new Date;//当前日期
var currentYear = currentDate.getFullYear();//当前年份
var yearRange = 1; // 前后1年
var startDate = currentYear - yearRange;//前1年
var endDate = currentYear + yearRange;//后1年
var today = currentDate.getDate(); // 获取今天是几号
var currentYear = currentDate.getFullYear();//年
var currentMonth = currentDate.getMonth();//月
var displayedYears = {}; // 用于记录已显示的年份
开始渲染第一排是年月,一年的月份是固定的,所以都是可以写死的,这里有要注意一下的是,就是年月的宽度,因为要根据当月有多少天去计算宽度,所以我要知道,这一年这一月是多少天然后乘以40 下面是相关代码
for (var year = startDate; year <= endDate; year++) {
for (var month = 0; month < 12; month++) {
var lastDay = new Date(year, month + 1, 0).getDate();
var monthElement = $("<p>" + (month + 1) + "月: </p>"); // 创建表示月份的 <p> 元素
for (var day = 1; day <= lastDay; day++) {
dateRange.push(new Date(year, month, day));
}
// 在 .tableYear 中添加年份和月份信息
var yearMonthStr = year + "-" + (month + 1 < 10 ? "0" : "") + (month + 1);
var width = (lastDay * 40)-1 + "px"; // 计算宽度
$(".tableYear").append($("<p class='Gantt-table_header' style='width: " + width + "'>" + yearMonthStr + "</p>"));
}
}
渲染完成,年月以后就是日,我这里也做了一些小的显示,比如说周末深颜色表示,今天也深颜色表示,并且视图要显示在当前,而不是在2022-1-1号这里。
天数渲染
for (var i = 0; i < dateRange.length; i++) {
var currentDate = dateRange[i];
var dayNumber = currentDate.getDay(); // 获取星期几 (0 = 星期日, 1 = 星期一, ...)
var isWeekend = dayNumber === 0 || dayNumber === 6;
var dayText = currentDate.getDate();//获取日
var dayYear = currentDate.getFullYear(); // 获取年份
var dayMonth = currentDate.getMonth(); // 获取月份(注意:月份是从 0 到 11,0 表示一月,11 表示十二月)
var tableCell = $("<p>" + dayText + "</p>");
if (isWeekend) {
tableCell = $("<p class='Gantt-table_weekend'>" + dayText + "</p>");
}
//获取当前时间的年月日,与循环出来的年月日进行循环匹配,
if (dayText === today && dayYear === currentYear && dayMonth === currentMonth ) {
tableCell.addClass("today");
}
$(".tableDay").append(tableCell);
}
视口显示代码
// 将视口滚动到今天所在的位置
var todayElement = $(".today");
if (todayElement.length > 0) {
var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
var elementHeight = todayElement[0].clientHeight;
var offset = (viewportHeight - elementHeight) / 2;
//平滑滚动参数 smooth auto不滚动
todayElement[0].scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
}
第二个难点
如果不用表格,怎么去实现这个网格呢,刚开始是想着去渲染,表格对应有多少天,就渲染有多少个p标签,但一想这怎么行,如果任务很多,一条任务就要渲染上千个p标签,太浪费资源了,任务一多,那不直接挂壁。
后面在css里面找到了解决办法
background: repeating-linear-gradient(to right, rgb(221, 221, 221), rgb(221, 221, 221)
1px, transparent 1px, transparent 40px) 0% 0% / 40px 100%;
ChatGPT是这样解释的
然后完美解决问题,不用渲染这么多,一个任务一个div就可以了。
第三个难点
甘特图的核心,那个柱状图的东西。
柱状图渲染,比如说我提了一个任务,是2023年8月20号开始的,然后到2023年8月25号要完成,那这样就只有五天时间,那渲染肯定是从2023年8月20号开始的,然后到8月25号结束。
我的思路是这样的,显示的宽度是五天,然后一天的宽度是40px,那么这个任务的总宽度就是200px,然后定位,获取到这个任务开始的时候,我这里显示是前后一年的也就是2022-1-1号开始的,然后相减,知道中间相差了多少天,然后再乘以40,得到left的距离,就实现了这个效果。后面的描述也是类似的效果,这样甘特图差不多就完成了。下面是日期相减代码
function getOffsetDays(startDate, endDate){
var startDateArr = startDate.split("-");
var checkStartDate = new Date();
checkStartDate.setFullYear(startDateArr[0], startDateArr[1], startDateArr[2]);
var endDateArr = endDate.split("-");
var checkEndDate = new Date();
checkEndDate.setFullYear(endDateArr[0], endDateArr[1], endDateArr[2]);
var days = (checkEndDate.getTime() - checkStartDate.getTime())/ 3600000 / 24;
if(startDateArr[0]!=endDateArr[0]){
flag = true;
}
return days;
}
结语
虽然三言两语就讲解完了,但其中还是有很多逻辑的问题,我只是讲解了一下我的一个实现的思路,居体代码请看这。
以上就是本篇文章的全部内容,希望对大家的学习有所帮助,以上就是关于无插件实现一个好看的甘特图的详细介绍,如果有什么问题,也希望各位大佬们能提出宝贵的意见。当然也有很多需要优化的地方,我这只是给了一个思路,你们可以去实现很多的功能。
来源:juejin.cn/post/7272174836336132132
2024年,为啥我不建议应届生再去互联网?
最近快过年了,和还留在成都的一些研究生同学吃了顿饭,其中博士姐姐因为今年刚刚毕业,所以在饭局里面还跟我们谈了一下今年的师弟师妹们的去向。
她说今年虽然就业挺难的,但是师弟师妹们的工作还都挺好的,有去成飞的,有去选调的还有去了一些大国企研究所的。然后我就问今年没有去互联网的吗?她说有哇,有学弟去了美团,钱还开得挺多的,有学弟去了个独角兽做算法,但是也就这两个人去了互联网相关的了。
其实听到这个我还是蛮感慨的,在我毕业的时候互联网还是如日中天,大多数计算机毕业的孩子首选的就是去互联网狠狠的大赚一笔。短短3年间,去互联网的应届生就屈指可数了,一方面是这两年互联网大厂缩招严重,进互联网没有我们当年那么容易。另一方面是,在大环境不容乐观的今天,以及互联网增长见顶的背景下,互联网的工作其实已经不是应届生的首选工作了。
实际上,即使在今年你能过千军万马杀出重围拿到互联网的offer,作为一个过来人我也不是很建议你再去趟互联网这趟浑水。因为,作为一个新人在一个注定下行的行业当中,你可能搭上的不是通往财富自由的快车道,很快你需要考虑的可能就是你还能不能保住你手头的这份工作的问题。
说一个老生常谈的事情,互联网的增长确实见底了,阿里、腾讯、网易的股票最近狂跌,阿里都跌回2014年了,只有抖音还依然坚挺一些但是依然看不到未来成长的空间。从2014年到2024年,正好十年的时间,互联网员工们加班加点996,熬夜爆肝的奋斗,最终的结果尽然是回到了原点。
其实,这个事情也并不奇怪,这些互联网大厂只是坐在电梯里面的人,他们都觉得自己能够取得成功是因为自己在电梯里面做俯卧撑。实际上,跟你在电梯上做啥没有关系,你之所以能够成功只是因为你恰好赶上了这班电梯而已,跟你在里面睡觉还是瞎折腾关系都不大。如今风停了,电梯开始往下走了,作为个体你非要去搭上这个末班车并且期待在踩在早就已经上电梯的这群人的头上的话,那么我只能跟你说,祝你好运了。
其实,作为一名应届生的时候我对职场也没有清醒的认识,以为职场上的同事和学校的同学一样大家和和气气不争不抢的。但是,正是抱着这样的心态我入职了互联网之后的短短一年时间内,才深刻感受到了社会的毒打和职场真实的样貌。所以,我不知道在学校的应届生们有没有做好准备在互联网面对全方位的竞争,这种竞争不仅仅是技术,不仅仅是加班,更是向上管理和领导处理好关系。和国企、外企、体制内不一样,互联网的大多数公司是有强制末尾淘汰的,有些公司甚至连新人保护期都没有,那么你觉得你作为一个活蹦乱跳的应届生,这个名额是老油条扛呢还是你呢?
另外,以前的人扎堆朝互联网冲是因为真的有财富自由的机会的,那时候啥app都没有,张小龙找几个应届生关小黑屋都能写出未来的国民级app微信。16年的字节也还是个小公司,那时候往互联网里面冲的话搞不好真的可以一年能够赚到别人一辈子赚不到的钱,所以去互联网真是一点儿问题都没有。你那时候不去互联网我都会拿着鞭子抽你,劝你上进一点儿!但是都2024年了,市场永远比个人知道一个方向的未来,还是那句话你想创业互联网都拉不到风投的年代,你还能奢望能够实现财富自由吗?
The End
其实作为一名程序员还是挺享受写有趣代码的过程的,也希望做一点儿东西能够被大家认可,所以我劝退互联网但是并不是劝退计算机。
即使是Chatgpt大行其道的今天,我也不认为未来某一天机器能够真正意义上取代程序员,要取代也是从另外一个维度上取代,比如说根据需求直接生成机器码而不是生成代码的这种形式。虽然互联网是一片红海,但是像新的技术VR、物联网、工业软件、芯片和智能机器人等行业,在我们国家还是蕴含着无限机会的。但是,我并不认为去到我上面所说的这些行业工资收入上能够超过现在的互联网大厂给出的工资,我的意思真的有想法的人可以尝试在这些领域去找到自己的一席之地,尤其是在校学生。
你去卷一个注定下山的行业无论它钱给多少都是毫无意义的,因为入职就可能就是你职业生涯的巅峰。相比起来,我觉得华子未来比这些靠着广告赚钱的公司都更有前景,因为是真的有一些核心技术在的。
所以,选择一个还没有走过巅峰的行业,提前布局才是更有未来的职业选择。
来源:juejin.cn/post/7327447632111419443
每天都很煎熬,领导派的活太难,真的想跑路了
人在江湖身不由己,无论是领导的亲信还是团队的边缘,都可能遇到这种情况———不得不干一件特别难以推进的事情,茫然无措,不知如何推进。每天陷入焦虑和自我怀疑中……
这种事情一般有一些共同特点。
- 结果和目标极其模糊。
- 需要协调其他团队干活但是对方很不配合。
- 领导也不知道怎么干
领导往往是拍脑袋提想法,他们也不知道具体如何执行。反过来说,如果领导明确知道怎么做,能亲自指导技术方案、亲自解决关键问题,那问题就好办了,只要跟着领导冲锋陷阵就好了,就不存在烦恼了。
遇到这种棘手的事情,如果自己被夹在中间,真的非常难受啊!
今天重点聊聊领导拍脑袋、心血来潮想做的那些大事 如果让你摊上了,你该怎么做!
1、提高警惕!逆风局翻盘难!
互联网行业目前处于稳定发展期,很少会出现突然迅猛增长的业务,也很少有公司能够迅速崛起。这是整个行业的大背景。因此,我们应该对任何不确定或模糊的目标表示怀疑,因为它们更有可能成为我们的绊脚石,而不是机遇。即使在王者荣耀这样的游戏里,要逆风翻盘也很困难,更何况在工作中呢。
当领导提出一个棘手的问题时,我们应立刻警惕,这可能不是一个好的机会,而是一个陷阱。我们不应该被领导画的饼所迷惑,而是要冷静客观地思考。哪些目标和结果是难以达到的,这些目标和结果就是领导给我们画的大饼!
领导给出任务后,我们就要努力完成。通常情况下,他们会给我们一大堆任务,需要我们确认各种事情。简而言之,他们只是有个想法,而调研报告和具体实施方案就需要我们去做。
如果领导是一位优秀而谦虚的人,通常在我们完成调研后,会根据调研结果来判断这个想法是否可行。如果不可行,他们会立即放弃,而我们也不会有什么损失。
但是,一旦领导有了一个想法,肯定是希望我们来完成的,即便我们在调研后认为不可行,大多数情况下,他们也不会接受我们的结论!因此,我们的调研工作必须极度认真,如果我们认为不可行,就要清楚地阐述不可行的理由,要非常充分。
这是我们第一次逃离的机会,我们必须重视这次机会,并抓住机会。
2、积极想办法退出
对于这种模糊不靠谱的事情,能避开就避开,不要犹豫。因为这种事情往往占用大量时间,但很难取得显著的成果。对于这种时间周期长、收益低、风险高的事情,最好保持距离。
你还需要忍受巨大的机会成本
在你长期投入这种事情的过程中,如果团队接到更好的项目和需求,那肯定不会考虑你。你只能羡慕别人的机会。
因此,如果可以撤退的话,最好离开这种费力不讨好的活远远的!
子曰:吾日三省吾身,这事能不能不干,这事能不能晚点干,这事能不能推给别人干。
如何摆脱这件事呢?
2.1 借助更高优事情插入,及时抽身
例如,突然出现了一件更为紧急的事情,这就是脱身的机会。与此同时,我们也可以为领导保留一些颜面,因为随着工作的进展,领导也会意识到这件事情的意义不大,很难取得实质成果。但是,如果我们一开始就表示不再继续做这件事,那么领导可能会觉得自己的判断出了问题,失去面子。所以,我们可以寻找一个时机,给领导下台阶。
或者,突然出现了一个需求,与我们目前的重构方案存在冲突。这是一个很好的借口。重构方案和未来产品规划产生了冲突,我们应优先满足产品规划和需求。重构方案需要后续再次评估,以找到更好的解决方案。
2.2 自己规划更重要的事情,并说服领导
当你对系统优化没有想法时,不要怪领导给你找事干。
如果领导有一个系统重构的计划和目标需要你执行,但是你不想干,或者你认为这件事不靠谱。那么你可以思考一个更可行、更有效、更能带来收益的重构方案,并与领导进行汇报。如果领导认为你的计划更加重要且更具可行性,那他可能会放弃自己的想法。
这就是主动转被动的策略。这时你的技术能力将接受考验,你能提出一个更优秀的系统重构方向吗?你能提出一个更佳的系统建设方向吗?
2.3 选择更好的时机做这件事
如果领导让你去做技术重构,而这件事的优先级不如产品需求高,上下游团队也不愿意配合你,而且领导给你的人力和时间资源也不够充裕,你应该怎么办呢?可以考虑与产品需求一起进行技术重构。也就是说,边开发需求,边进行技术重构。这样做有以下好处:可以借助于产品的力量,很自然地协调上下游团队与你一同进行重构。同时也能推动测试同事进行更全面的测试。在资源上遇到的问题,也可以让产品帮助解决。
所以,技术重构最好和产品需求结合起来进行。如果技术重构规模庞大,记得一定要分阶段进行,避免因技术重构导致产品需求延期哦。
2.4 坦诚自己能力不足,暂时无法完成这件事,以后再干行不行
可以考虑向领导坦然承认自己的能力还不足以立即执行这项任务,因此提出先缓一缓,先熟悉一下这个系统的建议。我可以多做一些需求,以此来熟悉系统,然后再进行重构。
我曾经接手一个系统,领导分配给我一个非常复杂的技术重构任务。当时我并没有足够聪明,没有拒绝,而是勉强去做,结果非常不理想,还导致了线上P0级别的事故发生!
新领导告诉我,"先想清楚如何实施,再去行动。盲目地勉强上阵只会带来糟糕的结果。当你对一个系统不熟悉的时候,绝对不能尝试对其进行重构。"
先熟悉系统至少三个月到半年。再谈重构系统!
2.5 拖字诀,拖到领导不想干这件事!
拖到领导不想干的时候,就万事大吉了。
注意这是最消极的策略,运气好,拖着拖着就不用干了。但如果运气不佳,拖延只会让任务在时间上更加紧迫,而且还会招致领导的不满。
使用拖延策略很可能得罪领导,给他们留下不良的印象。
因此,在使用此策略时应谨慎行事!
2.6 退出时毫不犹豫,不要惋惜沉默成本
如果有撤退的机会,一定不要犹豫,不要为自己付出的投入感到遗憾,不要勉强继续前进,也不必试图得到明确的结果。错误的决策只会带来错误的结果。一定要及时止损。
因为我曾经犯过类似的错误,本来有机会撤退,但是考虑到已经付出了很多,想要坚持下去。幸好有一位同事更加冷静,及时制止了我。事后我反思,庆幸及时撤退,否则后果真的不敢想象啊。
3、适当焦虑
每个人都喜欢做确定性的事情,面对不确定的事情每个人都会感到焦虑。为此可能你每天都很焦虑,甚至开始对工作和与领导见面感到厌恶。之所以这个事情让你感到不适,是因为它要求你跳出舒适区。
但是,请记住,适度的焦虑是正常的。告诉自己,这并没有什么大不了的。即使做得不好,顶多被领导责备一下而已。不值得让生活充满焦虑,最重要的是保持身心健康和快乐。
当你沉浸在焦虑中时,可能会对工作和领导感到厌烦。这样一来,你可能会对和领导沟通感到反感。这种情况是可怕的,因为你需要不断和领导沟通才能了解他真正的意图。如果失去了沟通,这个事情肯定不会有好的结果。
因此,一定要保持适度的焦虑。
3.1 沟通放在第一位
面对模糊的目标和结果,你需要反复和领导沟通,逐步确认他的意图。或者在沟通中,让领导他自己逐渐确定的自己的意图。在这方面有几个技巧~
3.2 直接去工位找他
如果在线上沟通,领导回复可能慢,可能沟通不通畅。单独约会议沟通,往往领导比较忙,没空参加。所以有问题可以直接去工位找他,随时找他沟通问题。提高效率
3.3 没听懂的话让领导说清楚
平常时候领导没说清楚,无所谓,影响不大。例如普通的产品需求,领导说的不清楚没关系,找产品问清楚就行。
面对目标不明确的项目,领导的意图就十分重要。因为你除了问领导,问其他人没用。领导就是需求的提出方,你不问领导你问谁。 在这种情况下,没听懂的事情必须要多问一嘴。把领导模糊的话问清楚。
不要怕啰嗦,也不要自己瞎揣摩领导的意图。每个人的想法都不同,瞎猜没用。
3.4 放低姿态
如果领导和你说这件事不用干了,你肯定拍手叫好。很多烦恼,领导一句话,就能帮你摆平!
放低姿态就是沟通时候,该叫苦叫苦,该求助就求助,别把自己当成超人,领导提啥要求都不打折扣的行为完全没必要。可以和领导叫叫苦,可以活跃气氛,让领导多给自己点资源,包括人和时间。
说白了,就是和 领导 “撒娇”。这方面女生比较有优势,男生可能拉不下脸。之前的公司,我真见识过,事情太多,干不完,希望领导给加人,但被领导拒绝。 然后她就哭了,最后还真管用!是个女同事。
男孩子想想其他办法撒娇吧。评论区留下你们的办法!
3.5 维护几个和领导的日常话题
平常如果有机会和领导闲聊天,一定不要社交恐惧啊! 闲聊天很能提升双方的信任关系,可以多想想几个话题。例如车、孩子、周末干啥、去哪旅游了等等。
提升了信任关系,容易在工作中和领导更加融洽。说白了就是等你需要帮忙的时候,领导会多卖你人情!
4 积极想替代方案————当领导提的想法不合理时
积极寻求替代方案,不要被领导的思路局限!引导众人朝着正确的方向前进!
不同领导的水平和对技术问题的认知不尽相同,他们注重整体大局,而员工更注重细节。这种差异导致了宏观和微观层面之间存在信息不对称,再加上个人经验、路径依赖导致的个人偏见,使得领导的想法不一定正确,也不一定能够顺利实施。
就我个人的经历来说,领导要求我进行一次技术重构。由于我对这个项目还不够熟悉,所以我完全按照领导的方案去操作,没有怀疑过。事后回顾,发现这个方案过于繁重,其实只需要调整前端接口就能解决问题,但最终我们却对底层数据库存储、业务代码和接口交互方式进行了全面改变。
最终收益并不高,反而导致了一个严重的故障。既没有获得功劳,也没有得到应有的认可。
事后反思,我意识到我不应该盲目按照领导的方案去执行,而是应该怀着质疑和批判的态度去思考他的方案。多寻求几个备选方案,进行横向比较,找到成本最低、实施最简单的方案。
4.1 汇报材料高大上,实现方案短平快
私底下,可以对老板坦诚这件事,就是没什么搞头。但是对外文章要写得高大上!
技术方案要高大上,实现方案要短平快。
面对不确定的目标、面对不好完成的任务,要适当吹牛逼和画饼。汇报文档可以和实现方案有出入。
模糊的目标,往往难以执行和完成,技术方案越复杂,越容易出问题。本来就没什么收益,还引出一堆线上问题,只能当项目失败的背锅侠,得不偿失。
一定要想办法,把实现方案做的简单。这样有3个好处;
- 降低实现难度,减少上线风险。
- 缩短开发周期,尽快摆脱这个项目。
- 把更多的时间放在汇报材料上。代码没人看!!!
程序员一般情况下习惯于实话实说,如果说假话,一定是被人逼得。
不会写文档?# 写文档不用发愁,1000个互联网常用词汇送给你
不会写技术方案?# 不会画图? 17 张图教你写好技术方案!
5、申请专门的团队攻克难关!
例如重构系统涉及到上下游系统,一个人搞不定的!要向领导寻求帮助,让上下游同事一起干这件事。
让熟悉系统的人跟自己一起做,拉更多的人入伙!多个人一起承担重任! 这种组织上的安排,只能由领导出面解决。
假如别的同事经常打扰你,总让你确认这件事,确认那件事,总让你帮忙梳理文档,你愿意配合吗? 每个人都很忙,没人愿意长期给你干活。
让领导帮忙成立重构小组!然后你可以给每个人都分派任务,比自己独自硬扛,成功概率大很多。
虽然重构的目标不明确,但你可以尝试明确每个人的责任,设置短期的里程碑。例如前三天梳理整理资料,每天开早会, push大家干活。(这样很招人恨!没办法,领导卷的)
5.1 寻求合作的最大公约数
重大项目往往需要多个团队同时配合,即便你申请了专门的小组跟进这件事,但是别人可能出工不出力!
他们不配合的原因在于:不光没有收益,付出还很多。成本和收益不对等,人家不愿意很正常。保持平常心!不要带着脾气看待这件事!
略微想一下就明白,既然你觉得这件事风险高、收益低,难道其他人看不出来吗?
作为项目的负责人推动事情更加困难。当别人不配合时,除了把矛盾上升到上层领导外,还有哪些更好的办法呢?
- 平时多和相关同学打好关系。平时奶茶咖啡多送点,吃别人嘴短,到时候求人时候很管事的。
- 调动对方的积极性!例如重构系统需要人家配合,但是这件事对他们又没有收益。可以和他们一起头脑风暴,想一下对方系统可以做哪些重构。当双方一拍即合,各取所需时,才能合作融洽。双赢的合作,才能顺利。
- 多作妥协。上下游系统的交互边界很难划分,如果交互存在争议,可以适当让步,换取对方的积极合作。完成胜于完美!
总之,涉及多个团队合作时,除了依靠上层领导的强硬干预之外,还要想一些合作共赢的方案!
6、争取更多的资源支持
没有完不成的事情,只要资源充裕,任何事情都是有希望的。当你面临棘手的问题时,除了打起12分的精气神,还要多想想和领导申请资源啊!
最重要的包括人力资源、时间资源。如果空口白牙就要人,可能比较困难。
这需要你在调研阶段深入思考,预想到系统的挑战点,把任务细分,越细越好,然后拿着排期表找领导,要人、要时间。
如果人和时间都不给!可以多试几次,软磨硬泡也是好办法!
此外还有别的办法,例如 ”偷工减料"。你可以和领导沟通,方案中哪些内容不重要,是否可以砍掉。”既然你不给人,砍掉不重要的部分,减少工作量,总可以吧"
除此之外,还可以考虑分期做。信用卡可以分期付款,技术重构当然也可以分期优化!
7、能分期就分期
对于技术重构类工作,一定要想办法分期重构,不要一次性只求大而全!
- 越复杂的技术方案越容易出问题!
- 越长的开发周期越容易出问题!
- 越想一次性完成,越容易忙中出错!
分期的好处自不必说,在设计方案时一定要想如何分期完成。
如果对一个系统不熟悉,建议分期方案 先易后难!先做简单的,逐渐地你对系统会有更深入的理解!
如果对一个系统很熟悉,可以考虑先难后易。先把最困难的完成!后面会轻松很多!
但是我还是建议庞大的重构工作,先易后难!先做简单的,拖着拖着,也许就不需要重构了呢!
8、即便没有功劳但是要收获苦劳
当一件事干成很难的时候,要想办法把损失降到最低。一定要想着先保护自己!别逞能!
工作几年的朋友应该知道,不是所有的项目都能成功!甚至大部分项目在商业上是失败的!做不成一件事很正常!
如果一件事很难办成,功劳就不要想了。但是可以赚一份苦劳。
这要求你能把自己的困难说给领导,例如其他团队不配合!你可以一直和领导反馈,并寻求领导的帮助。
日常工作的内容也要有文档留存。工作以周报形式单独和领导汇报!要让领导知道你每周的进展,向领导传递一个事实:“每一周你都努力地在做事,并且也都及时汇报了,日后干不成,可别只怪我一人啊!”
接到一个烫手山芋,处理起来很难~ 斗智斗勇,所以能躲开还是躲开啊!
9、转变观念:放弃责任心,领导关注的内容重点完成
出于责任心的角度,我们可能认为领导提出的方案并不正确,甚至认为领导给自己派的工作完全没有意义。
你可能认为领导的Idea 不切合实际!
出于责任心,你有你的想法,你有你的原则!你认为系统这样重构更适合!但那又怎样,除非你有足够的理由说服领导,否则改变不了什么。
站在更高的位置能看的更远,一般领导都会争取团队利益最大化。虽然看起来不切实际,但是努力拼一拼,也许能给团队带来更大的利益。这可能是领导的想法!说白了,就是领导想让团队多去冲锋陷阵,多把一些不可能变成可能!
和领导保持节奏,领导更关注哪件事,就尽力把这件事做好! 放弃自己所谓的“责任心”。
10、挑战、机遇、风险并存。
在互联网稳定期,各行各业都在内卷,公司内部更是在内卷!
在没有巨大增量的团队和公司里,靠内卷出成绩是很困难的事情。有时候真的很绝望,每一分钟都想躺平 。
像这种目标不明确、执行方案不明确、结果不明确、需要协调其他团队干活的难事越来越多!风险高、低收益的事情谁都不想干!
但是一旦能做成,对于个人也是极大地锻炼。所以大家不要一味地悲观,遇到这种棘手的事情,多和领导沟通,多想想更优的解决方案。也许能走出一条捷径,取得极大的成果~
来源:juejin.cn/post/7290469741867565092
Easy-Es:像mybatis-plus一样,轻松操作ES
0. 引言
es的java客户端不太友好的语法一直饱受诟病,书写一个查询语句可能需要书写一大串的代码,如果能像mybatis--plus一样,支持比较灵活方便的语句生成器那就好了。
于是为elasticsearch而生的ORM框架Easy-Es诞生了,使用及其方便快捷,今天我们就一起来学习easy-es,对比看看原生java-client方便之处在哪儿。
1. Easy-Es简介
Easy-Es是以elasticsearch官方提供的RestHighLevelClient
为基础,而开发的一款针对es的ORM框架,类似于es版的mybatis-plus,可以让开发者无需掌握es复杂的DSL语句,只要会mysql语法即可使用es,快速实现es客户端语法
2. Easy-Es使用
1、引入依赖
<!-- 引入easy-es最新版本的依赖-->
<dependency>
<groupId>org.dromara.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>2.0.0-beta3</version>
</dependency>
<!-- 排除springboot中内置的es依赖,以防和easy-es中的依赖冲突-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.14.0</version>
</dependency>
2、添加配置项,这里只配置了几个基本的配置项,更多配置可参考官网文档:easy-es 配置介绍
easy-es:
# es地址、账号密码
address: 192.168.244.11:9200
username: elastic
password: elastic
3、在启动类中添加es mapper文件的扫描路径
@EsMapperScan("com.example.easyesdemo.mapper")
4、创建实体类,通过@IndexName
注解申明索引名称及分片数, @IndexField
注解申明字段名、数据类型、分词器等,更多介绍参考官方文档:essy-es 注解介绍
@IndexName(value = "user_easy_es")
@Data
public class UserEasyEs {
@IndexId(type = IdType.CUSTOMIZE)
private Long id;
private String name;
private Integer age;
private Integer sex;
@IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_SMART, searchAnalyzer = Analyzer.IK_SMART)
private String address;
@IndexField(fieldType = FieldType.DATE, dateFormat = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis")
private Date createTime;
private String createUser;
}
5、创建mapper类,继承BaseEsMapper
类,注意这里的mapper一定要创建到第3步中设置的mapper扫描路径下com.example.easyesdemo.mapper
public interface UserEsMapper extends BaseEsMapper<UserEasyEs> {
}
6、创建controller,书写创建索引、新增、修改、查询的接口
@RestController
@RequestMapping("es")
@AllArgsConstructor
public class UserEsController {
private final UserEsMapper userEsMapper;
/**
* 创建索引
* @return
*/
@GetMapping("create")
public Boolean createIndex(){
return userEsMapper.createIndex();
}
@GetMapping("save")
public Integer save(Long id){
UserEasyEs user = new UserEasyEs();
user.setId(id);
user.setName("用户"+id);
user.setAddress("江苏省无锡市滨湖区");
user.setAge(30);
user.setSex(1);
user.setCreateUser("admin");
user.setCreateTime(new Date());
Long count = userEsMapper.selectCount(EsWrappers.lambdaQuery(UserEasyEs.class).eq(UserEasyEs::getId, id));
if(count > 0){
return userEsMapper.updateById(user);
}else{
return userEsMapper.insert(user);
}
}
@GetMapping("search")
public List<UserEasyEs> search(String name, String address){
List<UserEasyEs> userEasyEs = userEsMapper.selectList(
EsWrappers.lambdaQuery(UserEasyEs.class)
.eq(UserEasyEs::getName, name)
.match(UserEasyEs::getAddress, address)
);
return userEasyEs;
}
}
7、分别调用几个接口
- 创建索引
kibana中查询索引,发现创建成功
- 新增接口
这里新增了4笔
数据新增成功
- 数据查询
如上便是针对easy-es的简单使用,这里的用法都与mp类似,上手相当简单,不用再写那些复杂的DSL语句了
3. 拓展介绍
- 条件构造器
上述演示,我们构造查询条件时,使用了
EsWrappers
来构造条件,用法与mp及其类型,大家根据提示就可以推导出方法如何书写,更详细的使用说明可以查看官方文档:easy-es 条件构造器介绍
- 索引托管
如果想要自动根据创建的es实体类来创建对应的索引,那么只需要调整索引的托管模式为非手动模式即可,因为这里我不需要自动同步数据,所以选择
非平滑模式
easy-es:
global-config:
process_index_mode: not_smoothly
其中三种模式的区别为:
平滑模式:smoothly,索引的创建、数据更新迁移等都由easy-es自动完成
非平滑模式:not_smoothly,索引自动创建,但不会自动迁移数据
手动模式:manual,全部操作由用户手动完成,默认模式
- 数据同步
如果数据源是来自mysql, 那么建议使用canal来进行同步,canal的使用可在我主页搜索。
其次还有DataX, Logstash等同步工具,当然你也可以使用easy-es提供的CRUD接口,来手动同步数据
- 日志打印
通过开启日志,可以在控制台打印执行的DSL语句,更加方便我们在开发阶段进行问题排查
logging:
level:
tracer: trace # 开启trace级别日志,在开发时可以开启此配置,则控制台可以打印es全部请求信息及DSL语句,为了避免重复,开启此项配置后,可以将EE的print-dsl设置为false.
- 聚合查询
easy-es实现的聚合查询,只要是针对gr0up by这类聚合,也就是es中的Terms aggregation
,以及最大值、最小值、平均值、求和,而对于其他类型的聚合,还在不断更新中,但这里大家也需要了解,es的聚合和mysql的聚合完全是不一样的维度和复杂度,es支持非常多的聚合查询,所以其他类型的实现还需要借助RestHighLevelClient
来实现
我们利用easy-es来实现下之前书写的聚合案例
@RestController
@AllArgsConstructor
@RequestMapping("order")
public class OrderEsController {
private final OrderTestEsMapper orderEsMapper;
@GetMapping("search")
public String search(){
SearchResponse search = orderEsMapper.search(EsWrappers.lambdaQuery(OrderTest.class).groupBy(OrderTest::getStatus));
// 包装查询结果
Aggregations aggregations = search.getAggregations();
Terms terms = (Terms)aggregations.asList().get(0);
List<? extends Terms.Bucket> buckets = terms.getBuckets();
HashMap<String,Long> statusRes = new HashMap<>();
buckets.forEach(bucket -> {
statusRes.put(bucket.getKeyAsString(),bucket.getDocCount());
});
System.out.println("---聚合结果---");
System.out.println(statusRes);
return statusRes.toString();
}
}
可以看到实际上的查询语句就一行,而其他的都是对返回结果的封装,因为es本身返回的数据是封装到嵌套的对象中的,所以我们需要对其进行包装
对比原始的查询语句,其易用性上的提升还是很明显的
4. 总结
至此对easy-es的介绍就结束了,可以看到如果是针对es实现CRUD上,easy-es表现出非常好的便捷性,而在复杂的聚合查询中,仍然还有进步空间,目前还需要借助RestHighLevelClient
,但easy-es的出现,为未来提供更好用的ES ORM框架,提供了希望和方向
文中演示代码见:gitee.com/wuhanxue/wu…
来源:juejin.cn/post/7271896547594682428
一次低端机 WebView 白屏的兼容之路
问题
项目:Vite4 + Vue3,APP WebView 项目
页面在 OPPO A5 手机上打不开,页面空白。
最开始是客户端在看,然后发现一个警告,大概也因为没看出什么问题,给到 Web 前端。
相关背景
为了方便描述过程的行为,先做一些相关背景的介绍。知道这些背景才能更好的了解问题的复杂。这些在解决问题的过程中始终是干扰因素,在反复调试试错的过程中才梳理总结出来,这里把它们列出来。
使用测试 App,其中有两个入口,一个是本地调试,这个地址是写在 App 里的,也就是要修改这个地址需要客户端重新出包;一个是项目的测试地址,这个地址测试可以进行配置。
修改客户端,重新出包,是很麻烦的,所以尽量避免。
项目配置了 HTTPS 支持,所以开发地址是 https 开头。但是也能启动 http 的地址。
关于项目支持 HTTPS,可以参考之前写的这篇:juejin.cn/post/732783…。
之所以要支持 HTTPS 是因为 iOS WebView 只支持 HTTPS 的地址。
而安卓 App WebView 却需要 HTTP 打开,原因是安卓 WebView 反馈不支持本地地址 HTTPS 的方式。但是在本机上用 MuMu 模拟器打开 App,是能打开本地 HTTPS 地址的,之前也尝试过给安卓手机安装根证书,但是还是不行,得到以上反馈。
所以我本地开发安卓在电脑 MuMu 上调试,iOS 可以用手机调试,如果要安卓真机本地调试,需要去掉本地 HTTPS 的支持,使用 HTTP 的地址(自然,iOS 本地调试就不能同时进行了)
快速尝试
拿到问题之后,快速进行问题验证,在 OPPO A5 上,进入 APP 中,打开本地调试,用 HTTP 的方式。发现确实白屏,查看了客户端相关的日志,发现一个警告:
[INFO:CONSOLE(9)] "The key "viewport-fit" is not recognized and ignored.", source: xxx
于是修改 viewport-fit,发现并没有区别,这只是一个提示,应该没有影响。
于是采用最简代码法,排除法,用最简单的页面进行测试,看是否能正常打开,确定 WebView 没有问题。直到确定 script type="module"
引入的 main.ts
的代码没有起作用。
于是,基本上确定 Vite 的开发模式在 OPPO A5 WebView 中有问题。那不支持 ESM,就是兼容性问题?
快速查看解决方案,引入官方插件:@vitejs/plugin-legacy。但是怎么测试呢?要验证兼容性是否生效,只能验证打包构建后的代码,而不是通过本地调试进行测试。那只能发布到测试了,但这样岂不是要改一点就要发布一次,这是没办法进行的。但是第一次,还是发布一下看有没有生效。
不出意料,没那么容易解决!测试地址依然白屏。
如何调试
确定如何方便的调试是解决问题的必要条件。
几天后又开始看这个问题。
浏览器是否能打开页面?
首先在 App 中进行调试是比较麻烦的,需求启动 App,那么能否在浏览器中进行测试呢?很遗憾,期间用手机系统浏览器打开测试地址是正常的,后面打开本地地址也是正常的。所以浏览器和 App WebView 是有区别的。
启动本地服务查看构建后的页面
兼容插件只是解决打包后的构建产物,想要看打包后的效果,于是我想到将打包后的文件起一个 Web 服务,这样就可以打开打包后的页面 index.html,而且手机访问同一网络,扫码就可以打开这个页面。
找了 Chrome 插件 Web Server for Chrome,发现已经不能用了- 找了 VS code 插件 Live Server,服务启了,但是有个报错。
- 换用 http-server,启动服务 xxx:8080。正常打开页面,手机也能访问。
那么考虑我们的实际问题,如何在手机调试呢?将本地调试改成本地起的服务 xxx:8080,看 WebView 能否打开,这样每次修改、打包,生成新的打包后文件,刷新 WebView 就可以了。
但是前面说了,找 APP 出包很麻烦,改一个地址要出个包,费时。还有其他的办法吗?如果把本地启的服务端口改成 5173 不就不用改 App 了吗,可以直接用本地调试来进行测试,突然又想到本地调试的地址是 HTTPS,可是启动的本地服务好像没法改成 HTTPS。
通过测试地址增加本地调试入口
又想到 App 中的本地调试入口本应该做成一个公共页面,里面放上很多可能的入口。这样只需要 APP 改一次,之后想要什么入口,可以自己添加。改这个调试入口还是需要 App 改动,还是麻烦。我可以在项目中增加一个页面 debug.html(因为项目是多页面应用),这样我增加页面\增加调试入口,发布一下测试就生效了,在测试入口就能看到,这样更快。于是做了一个公共页面。
Vite preview
而且突然想到根本不需要自己起一个服务,Vite 项目,Vite preview 就是把打包后的页面启动服务。地址是:xxx:4173。
修改测试地址为本地预览
然而,OPPO A5 WebView 本来就是打不开我们的系统,那么 WebView 打不开测试地址自然也就没法打开我的本地预览了。但是,测试地址是可以配置的,所以为了快速调试,让测试配置了我的本地预览地址 xxx:4173。
这样,终于在 APP WebView 中打开了我本地预览的页面。
如何查看 App WebView 的日志
手机连接电脑,adb 日志:
看起来这几个报错是正常的,报错信息也说了:
vite: loading legacy chunks, syntax error above and the same error below should be ignored
但是页面没有加载,不知道 WebView 打开页面和页面加载之间发生了什么。
Vite 兼容插件的原理
这期间,反复详细理解原理,是否是插件的使用不对。
用一句话说就是,Vite 兼容插件让构建打包的产物多了传统版本的 chunk 和对应 ES 语言特性的 polyfill,来支持传统浏览器的运行。兼容版的 chunk 只会在传统浏览器中运行。它是如何做到的呢?
- 通过 script type="module" 中的代码,判断当前浏览器是否是现代浏览器。如果是,设置全局变量:window.__vite_is_modern_browser = true。判断的依据是:
- import.meta.url;
- import("_").catch(() => 1);
- async function* g() { }
- 通过 script type="module",如果是现代浏览器,直接退出;如果不是,加载兼容文件:
- 通过 script type="nomodule",加载兼容 polyfill 文件;
- 通过 script type="nomodule",加载兼容入口文件;
传统浏览器不执行 type="module" 的代码,执行 type="nomodule" 的代码。
现代浏览器执行 type="module" 的代码,不执行 type="nomodule" 的代码。
为什么需要 type="module" 的代码?这里是针对浏览器支持 ESM 却不支持以上 3 个语法的情况,仍然使用兼容模式。
详细可以看参考文章,以及查看打包构建产物。
除了知乎那篇文章,我几乎翻遍了搜到的 vite 兼容 空白 白屏 相关的文章,参考相关的配置。这个插件就是很常规的使用,几乎没有看到有任何特殊的配置或处理。就是生成了兼容的代码,低版本浏览器就能使用而已,似乎没人碰到过我的问题。
尝试解决
前面说了,用手机系统浏览器打开页面,竟然正常。怀疑是不是 WebView 的问题。
WebView 的内核版本
借了几个低端机型,几个安卓 5.x 6.x 的系统,结果手机浏览器都能正常打开。
打印 console.log(navigator.appVersion)
,WebView 中:
5.0 (Linux; Android 8.1.0; PBAT00 Build/OPM1.171019.026; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36 uniweb/ma75 uniweb-apk-version/0.6.0 uniweb-script-version/0.6.0 uniweb-channel/netease Unisdk/2.1 NetType/wifi os/android27 ngwebview/4.1 package_name/com.netease.sky udid/944046b939d510b1 webview_orbit/1.2(1)
而手机浏览器版本为 Chrome 90,其他手机有 Chrome70。总之,OPPO A5 WebView 的内核 Chrome 版本较低。
Vite 文档对于构建生产版本浏览器兼容性的介绍:
用于生产环境的构建包会假设目标浏览器支持现代 JavaScript 语法。默认情况下,Vite 的目标是能够 支持原生 ESM script 标签、支持原生 ESM 动态导入 和 import.meta 的浏览器
原生 ESM script 标签的支持:
原生 ESM 动态导入的支持:
import.meta 的支持:
所以,原来 Chrome 62 支持 ESM,但是不支持其他 2 个。通过日志,也可以知道不支持兼容插件尝试的 3 个语法,因为打印了那句警告,来自 module 的代码位置,window.__vite_is_modern_browser 不为 true。
从 Android 4.4 开始,系统 WebView 使用 Chrome 内核。
手机系统浏览器内核和系统 WebView 不一样,手机系统的 WebView 也可能不是安卓默认的。
兼容生效了吗?
但还是不知道低版本的浏览器兼容性是否生效,当前我们只能确定 Chrome 62 的 WebView 中兼容有问题,那是否在浏览器中就正常呢?或者更低的版本兼容是否生效?(毕竟不支持 ESM 的浏览器版本的代码执行又不一样)
target 配置不对?
target 配置的是目标浏览器,针对这些浏览器生成对应的兼容代码,期间我一直调整 target 的配置,使用 .broswerlistrc 文件配置,target 直接配置,参考不同的配置方案,确保包含了 Chrome 62。
又是如何调试?
想要下载安卓 Chrome 62 进行测试,但是搜了一圈也没找到。
后来想到不一定要手机浏览器进行测试,Chrome 也行。这里面有点思维上的转换,之前我测试只能通过 WebView 进行调试,因为浏览器上没有问题。现在确定了是浏览器 Chrome 版本的问题,那么我们还是可以通过 PC 浏览器进行测试。
于是范围更大一些,找到了 Chrome 的所有历史版本,不得不说,Chrome 提供的下载真是太有用了,对于测试兼容性非常有帮助! 而且我所担心的覆盖现有浏览器版本的问题完全不存在,下载之后直接运行。
安装 Chrome 62,打开页面,果然空白。终于在浏览器复现,确定就是兼容的问题,而不是 WebView 的问题。安装安卓版本的 Chrome 62,也同样复现。
下载 win 的 Chrome 62,虽然在 refs 里找到同版本的记录:xxx。但是没找到同版本的下载,不过也都是 62,应该没问题。下打开页面,打开控制台,和在 adb 中看到的报错一样,只是这里是红色的:
安装更低版本的 Chrome,同样复现,说明不支持 ESM 的兼容也出现问题。同时可以看到那句提示没有了:
过程当然也没那么顺利,下载 Chrome 的过程中,Chrome 62 直接可以运行,下载 Chrome 59 却没法打开。于是又下载了 Chrome 55,mini exe 文件,可以直接打开。
报错到底要不要处理?
通过 adb日志可以看到报错,也可以从打包后的代码看到:对于语法报错是可以忽略的,因为那是预期中的行为。可是之后的代码为什么没执行了呢?
回到现在的问题,这个报错不是不需要处理吗?但是加载了兼容的 js,页面却没有渲染元素。此时隐隐觉得报错可能还是要处理,至少可能最后一个报错有点问题?
但是这个报错实在难以查看,之前我把它当作和前两个报错一样的来源。现在只剩这个报错了,问题是这是打包压缩后的代码,完全不知道真正的问题是什么。
通过请教网友,做了一些尝试:
通过对插件配置:renderModernChunks: false,只生成兼容代码,依然报错。
通过修改 Vite 配置:build.minify: false 不压缩代码,尝试查看报错位置。新的报错:
升级 Vite。新的报错:
所以每次的报错都不一样,越来越奇怪。不过看起来似乎是同一个原因导致的。
在构建源码中调试
通过在构建后的源码中打印,其中 excute 函数中,有两个参数 exports module,但是在其中使用 module.meta 报错,说明其他文件在使用这个方法是并没有传参。看起来像是模块规范的问题(commonJS 和 ES Module)。
ChatGPT
在这期间,也在 ChatGPT 搜素方法:
就尝试了一下 format: 'es',顺便看到有个配置 compact: true
,好像也是压缩,就顺手改成 false,这样全部不要压缩,方便看报错。
结果竟然 OK 了,页面打开,没有报错!
是这个配置生效的吗?通过排除,发现竟然是 compact 的原因。这个配置不是 Vite 本身的,是 Vite 使用的 rollup 的配置:
果然是插件冲突的结果。
再搜素 execute
,已经没有带参数了:
再次感叹 Webpack 配置工程师
build.sourcemap
后来想到开启 sourcemap 来定位报错的原始文件位置,未开启:
开启 sourcemap:
如果在打包过程中对代码进行了混淆或压缩,可能会导致 Source Map 无法准确映射到原始代码位置。
这就完了?
中午去吃个饭,下午回来,本以为打包发布验证一下就完了,结果测试地址能打开页面了,却和我在电脑浏览器上看到的一样,没有正常加载页面。晴天霹雳!
说明一下:我们这个项目是 App Hybrid 应用,Web 前端和服务端的通信通过客户端中转,所以在非客户端环境是拿不到服务端的数据的,联调测试都是在 App 中进行的。
但为什么前面一直用浏览器测试呢?虽然数据获取不到,但是如果页面加载出来了,说明打包代码是没问题,所以在前面页面显示了背景图等元素,id="app"
中有了内容以后,就是兼容版本的 js 正常执行了。但是再说一次,真实的环境是 WebView,最终的结果还是要看 WebView。
目前在 App 中还是没有显示完整的网页加载过程,又陷入了困境。页面已经没有了报错(除了 Vite 那个可以被忽略的),难道生成的兼容 js 有问题? 如果是具体的兼容代码有问题,那整个项目的代码又如何排查呢?
虽然没有头绪,但是隐隐觉得应该再坚持一下。继续分析,如果已经显示了页面,兼容的 js 已经执行了,那么数据接口请求了吗?
于是联系服务端,查看服务器日志,确定页面加载没有请求接口,说明请求接口的代码没执行。我用高版本 Chrome 查看 Preview(之前没做这一步),果然除了看到页面元素,还进行了请求尝试。于是又运用排除法,一步步打印首页加载的过程。
在这之前,又是如何调试的问题,我们把测试入口改为我本地的 preview 地址。
加入打印日志,本地 build,Preview,打印了日志信息,果然接口请求的部分未执行。排查发现是之前调起 WebView 导航的代码出问题,可能是 jsBridge 还未加载。
但是为什么其他设备没有出现这个问题呢,可能是设备性能较差。于是把相关代码放到 jsBridge 加载完成后执行,修复了这样一个隐藏的 bug。
但是为什么不报错呢??这真的是很不好的体验,之前 Vue router 不报错的问题也类似。
总结
同样,我们再回头看那个最初的报错:
vite: loading legacy chunks, syntax error above and the same error below should be ignored
上面的报错、项目相同的报错可以被忽略。很容易让人忽略了报错,明确提到了下面的报错,但是 Vite 打包中下面没有出现同样的报错了(我目前的打包和其他文章中提到的不一样,比如知乎文章提到的代码);而且相同的报错到底是什么相同,同样是语法报错而已啊。
这句提示值得商榷。
function __vite_legacy_guard() {
import.meta.url;
import("_").catch(() => 1);
async function* g() {};
};
主要是因为出问题的恰恰就是中间的版本,Chrome 62,Vite 让它报错又能正常运行。遇到这种情况少之又少,从权衡上来说,好像也没有问题
本质上,只是在解决一个打包后的文件报错的问题,问题是一开始并没有定位到这个问题,其次是打包后的报错仍然难以定位具体错误位置。然后这其中还涉及到项目自身环境的各种干扰。
几点感悟:
- 坚持不懈,这是解决问题的唯一原因。
- 总结熟练调试很重要,要快速找到方便调试的方法。
- 没有报错是开发的一大痛点。
- 针对当前的问题更深入的分析原因,更广泛的尝试。
- 多用 ChatGPT,ChatGPT 的强大在于它没有弱点,没有缺项。
说明
通过这个案例,希望能给大家一点解决问题的启发。是遇到类似的问题时:
- 了解相关的问题
- 熟悉相关的概念
- 学习解决问题的方法
- 学习调试的方法
- 坚持的重要性
参考
【原理揭秘】Vite 是怎么兼容老旧浏览器的?你以为仅仅依靠 Babel?
来源:juejin.cn/post/7386493910820667418
研发都认为DBA很Low?我反手一个嘴巴子
前言
我用十多年的DBA经验告诉你,如果你作为研发觉得DBA很Low,你是会吃苦头的
“你以为DBA就是安装一下数据库,管理一下数据库?你丢个SQL给DBA优化下?你的日志爆满了DBA给你清理一下?DBA帮你安装下中间件?你以为的DBA只是做这些事?”
秉持着和平交流的学习态度,我这里精选了几位高赞粉丝的精彩回答
1.救火能力
1.1 调优
IT界并没有一个通行的 ”拳头“ 来判断谁low,谁更low。有时候,研发写的程序,新功能发布后,就出现磁盘IO出现瓶颈了、或者CPU飙高到100%了,但是这个时候,只是表象,只知道Linux机器的资源耗尽了,DBA得先找到资源消耗在哪了,才能进一步分析原因,用数据说话是应用的问题,才能责令程序员整改。
SQL调优是一个复杂的过程,涉及多个方面,包括但不限于SQL语句的编写、索引的使用、表的连接策略、数据库的统计信息、系统资源的利用等。调优的难度取决于多个因素,包括查询的复杂性、数据量、硬件资源、数据库的工作负载和现有的优化策略。
在这里给大家分享一个执行计划变,1个SQL把系统干崩的情景,由于业务用户检索数据范围过大,导致执行计划谓词越界,通过矫正执行计划及开启操作系统大页,服务器DB一直存在的CPU高负载从75%降低到25%!
生产问题,瞬息万变,DBA要同时熟悉业务,并对硬件、网络要精通,要在这样的复杂情况下作出正确的决策,这一点我想难度不小吧。
1.2 高可用
数据库高可用是指DB集群中任何一个节点的故障都不会影响用户的使用,连接到故障节点的用户会被自动转移到健康节点,从用户感受而言, 是感觉不到这种切换。
那么DBA在高可用的配置方面,下面就是某制造业大厂,应用层的链接方式
--jdbc应用端的连接
jdbc:oracle:thin:@(DESCRIPTION =
(ADDRESS_LIST =(ADDRESS = (PROTOCOL = TCP)(HOST = rac1-vip)
(PORT = 1521))(ADDRESS = (PROTOCOL = TCP)(HOST = rac2-vip)
(PORT = 1521))(LOAD_BALANCE = no)(FAILOVER = yes))
(CONNECT_DATA =(SERVER = DEDICATED)(SERVICE_NAME = dbserver)))
那么这种配置FAILOVER = yes,Net会从多个地址中按顺序选择一个地址进行连接,直到连接成功为止,那么就会保证数据库单节点故障,自动的切换,高可用是故障发生的第一个救命的稻草,系统上线前一定要测试好,才能确保数据库的高可用,这期间DBA功不可没!
还有客户要求选择的一套国产数据库支持核心业务,那么作为DBA在选型及业务适配上就发挥作用了,跟研发确认发现应用是兼容PG的,而且客户要求要同时兼容OLAT和OLTP业务,看下以下这套openGauss国产数据库的高可用架构。
1.openGauss高可用:CM
通过配置VIP故障转移,OLTP连接VIP,进行事物交易
同时支持动态配置CM集群故障切换策略和数据库集群脑裂故障恢复策略,
从而能够尽可能确保集群数据的完整性和一致性。
2.写重定向,报表分析业务连接,支持读写分离
主备节点开启控制参数 enable_remote_execute=on之后
通过备库发起的写操作,会重定向到主库执行
2.监控能力
这方面我是最有发言权了,SA一直是我的本职工作,从机房硬件部署、弱电以及数据库的安装实施,很多东西需要依赖于DBA来做,全力保障应用的稳定性,而且监控到的指标随时可以推送到邮件以及微信。这期间我也发现了很多天窗,原来还可以这么干?
2.1 服务器监控
首先监控Linux服务嘛,那肯定是要全方位系统的监控,网络、磁盘、CPU、内存等等,这才叫监控,那么其实给大家推荐一款免费的监控工作
Prometheus提供了从指标暴露,到指标抓取、存储和可视化,以及最后的监控告警等组件。
数据库监控
Zabbix聚焦于帮助用户通过性能优化和功能升级来快速响应业务需求,从而满足客户的高期望值,并提升IT运维人员的生产力。在可扩展性与性能、稳定性与高可用、可观测性几个领域获得持续提升。监控做不好,救火救到老!拿下Zabbix,现在!立刻!马上!!
1.监控Oracle
博客地址:
https://jeames.blog.csdn.net/article/details/126825934
2.监控PostgreSQL
博客地址:
https://jeames.blog.csdn.net/article/details/120300581
3.监控MySQL
博客地址:
https://jeames.blog.csdn.net/article/details/126825934
3 数据源赋能者
从AI、智能化到云迁移和安全性,业务和技术趋势不断重塑DBA在组织中的角色.DBA 群体站在时代的岔路口,国产数据库太多了应该怎么选?DBA 会被云上数据库抛弃吗?应该如何应对新时代挑战?职业终点在哪里?
1.云数据库解决方案
DBA要善于利用云原生保障数据安全和优化成本
2.数据安全与合规
随着数据保护法律的出台、日益严峻的网络攻击,
DBA必须掌握加密、访问控制和审计等技能
3.灾难恢复和业务连续性
随着企业愈加依赖数据的连续性,
快速恢复丢失数据并最大限度地减少停机时间至关重要
4.自动化和脚本编写
自动化和脚本编写对于DBA管理重复性任务和提高效率尤为关键
5.有效的沟通和协作
有效的沟通和协作仍然是DBA的重要技能。
能够向同事清楚地传达技术信息、与跨职能团队合作,
打破IT部门和业务部门之间的信息差,确保数据库的策略与组织目标保持一致。
4.总结
在一个公司写了屎山代码的研发,可以拍拍屁股走人,然后继续去下一个企业再写个屎山。反正不会追着代码跨省找你。而一个搞崩了系统的DBA,这个闯祸经历将成为他的黑历史,并影响到他未来的就业.因为需要专业DBA的好企业,基本都是几百台服务器起步的大项目,难免不会查背景,这就导致DBA如果想干得好,圈子会越来越小,请记住是干得好,不是混得好,混是会出事的。
好了,以上就是我对DBA的理解了,有不足之处还望指正。
来源:juejin.cn/post/7386505099848646710
谈谈前端如何防止数据泄漏
最近突然发现了一个好玩的事情,部分网站进去的时候几乎都是死的,那种死是区别于我们常见的网站的死:
- 不能选中文字
- 不能复制粘贴文字
- 不能鼠标右键显示选项
- 不能打开控制台
- ……
各种奇葩的操作应接不暇,像极了我最初接触的某库。shigen
的好奇心直接拉满,好家伙,这是咋做的呀。一顿操作之后,发现这种是为了防止网站的数据泄露(高大上)。在我看来,不是为了装X就是为了割韭菜。
咱废话也不多说,就手动来一个,部分代码参考文章:如何防止网站信息泄露(复制/水印/控制台)。
那shigen
实现的效果是这样的:
用魔法生成了一个页面,展示的是李白的《将进酒》。我需要的功能有尽可能的全面,禁止复制、选择、调试……
找了很多的方式,最后能自豪的展示出来的功能有:
- 禁止选择
- 禁止鼠标右键
- 禁止复制粘贴
- 禁止调试资源(刷新页面的方式)
- 常见的页面水印
那其实也没有特别的技术含量,我就在这里展示了,希望能作为工具类供大家使用。
页面部分
html5+css,没啥好讲的。
html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
line-height: 1.6;
padding: 20px;
text-align: center;
background-color: #f8f8f8;
}
.poem-container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 1.5em;
margin-bottom: 20px;
}
p {
text-indent: 2em;
font-size: 1.2em;
}
style>
<title>李白《将进酒》title>
head>
<body>
<div class="poem-container">
<h1>将进酒h1>
<p>君不见,黄河之水天上来,奔流到海不复回。p>
<p>君不见,高堂明镜悲白发,朝如青丝暮成雪。p>
<p>人生得意须尽欢,莫使金樽空对月。p>
<p>天生我材必有用,千金散尽还复来。p>
<p>烹羊宰牛且为乐,会须一饮三百杯。p>
<p>岑夫子,丹丘生,将进酒,杯莫停。p>
<p>与君歌一曲,请君为我倾耳听。p>
<p>钟鼓馔玉不足贵,但愿长醉不复醒。p>
<p>古来圣贤皆寂寞,惟有饮者留其名。p>
<p>陈王昔时宴平乐,斗酒十千恣欢谑。p>
<p>主人何为言少钱,径须沽取对君酌。p>
<p>五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。p>
div>
body>
js部分
禁止选中
// 防止用户选中
function disableSelect() {
// 方式:给body设置样式
document.body.style.userSelect = 'none';
// 禁用input的ctrl + a
document.keyDown = function(event) {
const { ctrlKey, metaKey, keyCode } = event;
if ((ctrlKey || metaKey) && keyCode === 65) {
return false;
}
}
};
禁止复制、粘贴、剪切
document.addEventListener('copy', function(e) {
e.preventDefault();
});
document.addEventListener('cut', function(e) {
e.preventDefault();
});
document.addEventListener('paste', function(e) {
e.preventDefault();
});
禁止鼠标右键
// 防止右键
window.oncontextmenu = function() {
event.preventDefault()
return false
}
禁止调试资源
这个我会重点分析。
let threshold = 160 // 打开控制台的宽或高阈值
window.setInterval(function() {
if (window.outerWidth - window.innerWidth > threshold ||
window.outerHeight - window.innerHeight > threshold) {
// 如果打开控制台,则刷新页面
window.location.reload()
}
}, 1000)
这个代码的意思很好理解,当我们F12的时候,页面的宽度肯定会变小的,我们这个时候和屏幕的宽度比较,大于我们设置的阈值,我们就算用户在调试页面了。这也是我目前找到的比较好的方式了。但是,但是,认真思考一下以下问题需要你考虑吗?
- 页面频繁加载,流量的损失大吗
- 页面刷新,后端接口频繁调用,接口压力、接口幂等性
所以,我觉得这种方式不优雅,极度的不优雅,但是有没有别的好的解决办法。
加水印
// 生成水印
function generateWatermark(keyword = 'shigen-demo') {
// 创建Canvas元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 设置Canvas尺寸和字体样式
canvas.width = 100;
canvas.height = 100;
context.font = '10px Arial';
context.fillStyle = 'rgba(0,0,0,0.1)';
// 绘制文字到Canvas上
context.fillText(keyword, 10, 50);
// 生成水印图像的URL
const watermarkUrl = canvas.toDataURL();
// 在页面上显示水印图像(或进行其他操作)
const divDom = document.createElement('div');
divDom.style.cssText = `
position: fixed;
z-index: 99999;
top: -10000px;
bottom: -10000px;
left: -10000px;
right: -10000px;
transform: rotate(-45deg);
pointer-events: none;
background-image: url(${watermarkUrl});
`;
document.body.appendChild(divDom);
}
代码不需要理解,部分的参数去调整一下,就可以拿来就用了。
我一想,我最初接触到这种页面水印的时候,是在很老的OA办公系统,到后来用到了某书,它的app页面充满了水印,包括浏览器端的页面。
所以,我也实现了这个。but,but,有一种技术叫做OCR,大白话讲就是文字识别。我把图片截个图,让某信、某书识别以下,速度和效果那叫一个nice,当然也可能把水印也识别出来了。聪敏的开发者会把水印的颜色和文字的颜色设置成一种,这个时候需要准确的文字那可得下一番功夫了。换句话说,不是定制化的OCR,准确的识别出信息,真的够呛。
还有的很多页面实现了js的数据加密、接口数据加密。但是道高一尺,魔高一丈,各种都是在一种相互进步的。就看实际的业务场景和系统的设计了。
来源:juejin.cn/post/7300102080903675915
从劝退 flutter_screenutil 聊到不同尺寸 UI 适配的最佳实践
先说优点
💡 先说优点叠个甲,毕竟库本身没有太大问题,往往都是使用的人有问题。
由于是基于设计稿进行屏幕适配的框架,在处理不同尺寸的屏幕时,都可以使用相同的 尺寸数值+单位
,实现对设计稿等比例的适配,同时保真程度一般很高。
在有设计稿的情况下,只使用 Container + GestureDetector 都可以做到快速的开发,可谓是十分的无脑梭哈。
在:只考虑移动端、可以接受使用大屏幕手机看小屏幕 ui、不考虑大字体的模式、被强烈要求还原设计稿、急着开发。的情况下,还是挺好用的。
为什么劝退?
来到我劝退师最喜欢的一个问题,为什么劝退。如果做得不好,瞎搞乱搞,那就是我劝退的对象。
在亲身使用了两个项目并结合群里的各种疑惑,我遇到常见的有如下问题:
如何实现对平板甚至是桌面设备的适配?
由于基于设计稿尺寸,平板、桌面等设备的适配基本上是没法做的,要做也是费力不讨好的事。
千万不要想着说,我通过屏幕宽度断点来使用不同的设计稿,当用户拉动边框来修改页面的宽度时,体验感是很崩溃的。而且三套设计稿要写三遍不同的代码,就更不提了。(这里说三遍代码的原因是,计算 .w
.h
的布局,数据会跟随设计稿变化)
如何适配大字体无障碍?
因为大字体缩放在满屏的 .w
.h
下,也就是写死了尺寸的情况下,字体由于随系统字体放大,布局是绝对会溢出的。很多项目开发到最后上线才意识到自己有大字体无障碍的用户,甚至某些博客上,使用了一句:
MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
来处理掉自己的用户,强制所有屏幕字体不可缩放。一时的勉强敷衍过去,最后只能等项目慢慢腐烂。
为什么在 1.w 的情况下会很糊?同样是 16.sp 为什么肉眼可见的不一样大?
库的原理很简单,提供了一堆的 api 相对于设计图的宽高去做等比例计算,所以必然存在一个问题,计算结果是浮点数。可是?浮点数有什么问题吗?
梳理一下原理:已知屏幕设计图宽度 sdw
、组件设计图宽度 dw
,根据屏幕实际宽度 sw
,去计算得出组件实际宽度 w
。
w = sw / sdw * dw
可是设计图的屏幕宽度 sdw
作为分母时,并不能保证总是可以被表示为有限小数。举个例子:库的文档中给的示例是 const Size(360, 690),
的尺寸,如果我需要一个 100.w
会得到多少?在屏幕宽度为 420 的情况下,得到组件宽度应该为 116.6666... 的无限小数。
这会导致最终在栅格化时会面临消除小数点像素的锯齿问题。一旦有像素点的偏差,就会导致边缘模糊。
字体对尺寸大小更为敏感,一些非矢量的字体甚至只有几个档位的大小,当使用 14.5、15、15.5 的字体大小时,可能会得到一样的视觉大小,再加上 .sp 去计算一道,误差更是放大。
具体是否会发生在栅格化阶段,哪怕文章有误也无所谓,小数点像素在物理意义上就是不存在的,总是会面临锯齿平滑的处理,导致无法像素级还原 UI。
为什么部分屏幕下会溢出?
我们知道了有小数点问题,那么不得不说起计算机编程常见的一个不等式:
0.1 + 0.2 != 0.3
由于底层表示浮点数本身就有的精度问题,现在让 Flutter 去做这个加法,一样会溢出。考虑以下代码:
Row(
children: [
SizedBox(width: 60.w),
SizedBox(width: 100.w),
SizedBox(width: 200.w),
],
);
在一个总共宽度 360.w 的设计图上,可能出现了溢出,如果不去使用多个屏幕来调试,根本不会觉得异常,毕竟设计图是这样做的,我也是这样写的,怎么可能有错呢?
然而恰恰是库本身的小数问题,加上编程届常见的底层浮点数精度问题,导致边缘溢出一点点像素。
我使用了 screenutil 为什么和真实的单位 1px 1rem 1dp 的大小不同呢?
哪怕是 .sp
都是基于设计图等比例缩放的,使用 screenutil 就从来不存在真实大小,计算的结果都是基于设计稿的相对大小。就连 .w
和 .h
都没法保证比例相同,导致所有布局优先使用 .w
来编写代码的库,还想保证和真实尺寸相等?
为什么需要响应式 UI?
说个题外话:在面试淘菜菜的时候真的会有点崩不住,他们问如何做好不同屏幕的适配,我说首先这是 UI 出图的问题,如果 UI 出的图是响应式的,那没问题,照着写,闭着眼都能适配。
但是如果设计图不是响应式的,使用 flutter_screenutil 可以做到和设计图高保真等比还原,但是如果做多平台就需要 UI 根据屏幕断点出不同平台的设计图。
面试官立即就打断我说他们的 UI 只会出一份图。我当场就沉默了,然后呢?也不说话了?是因为只有移动端用户,或者说贵公司 UI 太菜了,还是说都太菜了。菜就给我往下学 ⏬
首先 UI 的响应式设计是 UI 的责任
抛开国情不谈,因为国内的 UI 能做到设计的同时,UI 还是响应式的,这样的 UI 设计师很少很少,他们能把主题规范好,约定好,已经是不得了的了。
但即使如此,响应式 UI 设计也还是应该归于 UI 设计中,在设计图中去根据不同的尺寸,拖动验证不同的布局效果是很容易的。在不同的尺寸下,应该怎么调整元素个数,应该如何去布局元素,只有 UI 使用响应式的写法去实现了,UI 和开发之间的无效交流才会减少。
响应式的 UI 可以避免精度问题
早在 19 年我就有幸翻阅了一本 iOS 的 UI 设计规范,当时有个特别的点特别印象深刻:尺寸大小应该为 2 的整数次幂,或者 4 的倍数。因为这样做,在显示和计算上会较为友好。
💡 这其实是有点历史原因的,之前的 UI 在栅格化上做得并不是很好,锯齿化严重也是常态,所以使用可以被 2 整除的尺寸,一方面使用起来只有几个档位,方便调整;另一方面这样的尺寸可以在像素的栅格化上把小数除尽。
举个例子,在屏幕中间显示一个 300 宽度的卡片,和边距 16 的卡片,哪一个更响应式,无疑是后者,前者由于需要计算 300 相对与设计稿屏幕的宽度,后者只需要准确的执行 16 的边距就好,中间的卡片宽度随屏幕的宽度自动变化。
同样的例子,带有 Expanded 布局的 Row 组件,相比直接给定每个子组件尺寸导致精度问题的布局,更能适配不同的屏幕。因为 Row 会先放置固定大小的组件,剩余空间由 Expanded 去计算好传给子组件,原理和 Web 开发中的 flex 布局一样。
响应式布局是通用的规范
如果有 Web 开发经验的,应该会知道 Web 的屏幕是最多变的,但是设计起来也可以很规范,常见的 bootstrap 框架就提到了断点这个观点,指出了当我们去做 UI 适配的时候,需要根据不同的屏幕大小去做适配。同时 flex 布局也是 Web 布局中常用的响应式布局手段。
在设计工具中,响应式 UI 也没有那么遥远,去下载一份 Material Design 的 demo,对里面的组件自由的拉伸缩放,再对比一下自己通过输入尺寸大小拼凑在一起的 UI,找找参数里面哪里有差异。
怎么做响应式 UI
这里直接放一个谷歌大会的演讲,我相信下面的总结其实都可以不用看了,毕竟本实验室没有什么可补充的,但是我们还是通过从外到内、从整体到局部的顺序来梳理一下如何去做一个响应式的 UI,从而彻底告别使用 flutter_screenutil。
http://www.youtube.com/watch?v=LeK…
SafeArea
一个简单的组件,可以确保内部的 UI 不会因为愚蠢的设备圆角、前置挖孔摄像头、折叠屏链接脚、全面屏边框等原因而被意外的裁剪,将重要的内容,显示在“安全区”中。
屏幕断点
让 UI 根据不同的尺寸的窗口变化而变化,首先就要使用 MediaQuery.sizeOf(context);
和 LayoutBuilder()
来实现对窗口的宽度的获取,然后通过不同的屏幕断点,去构建不同情况下的 UI。
其中 LayoutBuilder
还能获取当前约束下的宽度,以实现页面中子区域的布局,比如 Drawer
的宽度,对话框的宽度,导航的宽度。
这里举了个例子,使用媒体查询获得窗口宽度之后,展示不同的 Dialog
:
写出如此优雅的断点代码只需要三步:
- 抽象:找到全屏对话框和普通对话框中共同的属性,并将功能页面提取出来。
- 测量:思考应该使用窗口级别的宽度(MediaQuery),还是某个约束下的宽度(LayoutBuilder)。
- 分支:编写如上图所示的带有断点逻辑的代码。
GridView
熟悉了移动端的 ListView 布局之后,切换到 GridView 布局并适配到平板、桌面端,是一件十分自然的事,只需要根据情况使用不同的 gridDelegate
属性来设置布局方式,就能简单的适配。
这里一般使用 SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: )
方法来适配,传入一个期望的最大宽度,使其在任何屏幕上看到的子组件都自然清晰,GridView 会根据宽度计算出合适的一行里合适的列数。
Flex 布局,但是 Flutter 版
前面说过了尽量不要去写固定尺寸的几个元素加起来等于屏幕宽度,没有那么巧合的事情。在 Row/Column 中,善用 Expanded 去展开子组件占用剩余空间,善用 Flexible 去缩紧子组件,最后善用 Spacer 去占用空白,结合 MainAxisAlignment 的属性,你会发现布局是那样的自然。
只有部分组件是固定尺寸的
例如 Icon 一般默认 24,AppBar 和 BottomNavigationBar 高度为 56,这些是写在 MD 设计中的固定尺寸,但是一般不去修改。图片或许是固定尺寸的,但是一般也使用 AspectRatio 来固定宽高比。
我曾经也说过一个普遍的公理,因为有太多初学者容易因为这个问题而出错了。
当你去动态计算宽高的时候,可能是布局思路有问题了。
在大多数情况下,你的布局都不应该计算宽高,交给响应式布局,让组件通过自己的能力去得出自己的位置、约束、尺寸。
举一个遇到过的群友问题,他使用了 stack 布局包裹了应用栏和一个滚动布局,由于SliverAppBar 拉伸后的高度会变化,他想去动态的计算下方的滚动布局的组件起始位置。这个问题就连描述出来都是不可思议的,然后他问我,我应该如何去获取这个 AppBar 的高度,因为我想计算下方组件的高度。(原问题记不清了,但是这样的需求是不成立的)
最后,多看文档
最后补上关于 MD3 设计中,关于布局的文档,仔细学习:
最后的最后,响应式布局其实是一个很宽的话题,这里没法三言两语说完,只能先暂时在某些领域劝退使用这个库。任何觉得可能布局困难的需求,都可以发到评论区讨论,下一篇文章我们将根据几个案例来谈谈具体的实践。
来源:juejin.cn/post/7386947074640298038
没用的东西,你连个内存泄漏都排查不出来!!
背景 (书接上回)
- ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。
- 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。
- 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存泄漏呀。
- 我:我不会。你自己不会上吗?你tm天天端个茶,抽个烟,翘个二郎腿,色眯眯的看着ui妹妹。
- 领导:污蔑,你纯粹就是污蔑。我tm现在就可以让你滚蛋,你信吗?
- 我:我怕你个鸟哦,我还不知道你啥水平,你tm能写出来个防抖节流,我就给你磕头。
- 领导:hi~ui妹妹,今天过的好吗,来,哥哥这里有茶喝。(此时ui妹妹路过)。你赶快给我干活,以后ui妹妹留给你。
- 艹!你早这么说不就好了。
开始学习
Chrome devTools查看内存情况
- 打开
Chrome
的无痕模式,这样做的目的是为了屏蔽掉Chrome
插件对我们之后测试内存占用情况的影响
- 打开开发者工具,找到
Performance
这一栏,可以看到其内部带着一些功能按钮,例如:开始录制按钮;刷新页面按钮;清空记录按钮;记录并可视化js内存、节点、事件监听器按钮;触发垃圾回收机制按钮等
简单录制一下百度页面,看看我们能获得什么,如下动图所示:
从上图中我们可以看到,在页面从零到加载完成这个过程中
JS Heap
(js堆内存)、documents
(文档)、Nodes
(DOM节点)、Listeners
(监听器)、GPU memory
(GPU
内存)的最低值、最高值以及随时间的走势曲线,这也是我们主要关注的点
看看开发者工具中的Memory
一栏,其主要是用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况
堆快照就像照相机一样,能记录你当前页面的堆内存情况,每快照一次就会产生一条快照记录
如上图所示,刚开始执行了一次快照,记录了当时堆内存空间占用为
33.7MB
,然后我们点击了页面中某些按钮,又执行一次快照,记录了当时堆内存空间占用为32.5MB
。并且点击对应的快照记录,能看到当时所有内存中的变量情况(结构、占总占用内存的百分比...)
在开始记录后,我们可以看到图中右上角有起伏的蓝色与灰色的柱形图,其中
蓝色
表示当前时间线下占用着的内存;灰色
表示之前占用的内存空间已被清除释放
在得知有内存泄漏的情况存在时,我们可以改用Memory
来更明确得确认问题和定位问题
首先可以用Allocation instrumentation on timeline
来确认问题,如下图所示:
内存泄漏的场景
- 闭包使用不当引起内存泄漏
- 全局变量
- 分离的
DOM
节点 - 控制台的打印
- 遗忘的定时器
1. 闭包使用不当引起内存泄漏
使用Performance
和Memory
来查看一下闭包导致的内存泄漏问题
<button onclick="myClick()">执行fn1函数button>
<script>
function fn1 () {
let a = new Array(10000) // 这里设置了一个很大的数组对象
let b = 3
function fn2() {
let c = [1, 2, 3]
}
fn2()
return a
}
let res = []
function myClick() {
res.push(fn1())
}
script>
在退出
fn1
函数执行上下文后,该上下文中的变量a
本应被当作垃圾数据给回收掉,但因fn1
函数最终将变量a
返回并赋值给全局变量res
,其产生了对变量a
的引用,所以变量a
被标记为活动变量并一直占用着相应的内存,假设变量res
后续用不到,这就算是一种闭包使用不当的例子
设置了一个按钮,每次执行就会将fn1
函数的返回值添加到全局数组变量res
中,是为了能在performacne
的曲线图中看出效果,如图所示:
- 在每次录制开始时手动触发一次垃圾回收机制,这是为了确认一个初始的堆内存基准线,便于后面的对比,然后我们点击了几次按钮,即往全局数组变量
res
中添加了几个比较大的数组对象,最后再触发一次垃圾回收,发现录制结果的JS Heap曲线刚开始成阶梯式上升的,最后的曲线的高度比基准线要高,说明可能是存在内存泄漏的问题 - 在得知有内存泄漏的情况存在时,我们可以改用
Memory
来更明确得确认问题和定位问题 - 首先可以用
Allocation instrumentation on timeline
来确认问题,如下图所示:
- 在我们每次点击按钮后,动态内存分配情况图上都会出现一个
蓝色的柱形
,并且在我们触发垃圾回收后,蓝色柱形
都没变成灰色柱形,即之前分配的内存并未被清除 - 所以此时我们就可以更明确得确认内存泄漏的问题是存在的了,接下来就精准定位问题,可以利用
Heap snapshot
来定位问题,如图所示:
- 第一次先点击快照记录初始的内存情况,然后我们多次点击按钮后再次点击快照,记录此时的内存情况,发现从原来的
1.1M
内存空间变成了1.4M
内存空间,然后我们选中第二条快照记录,可以看到右上角有个All objects
的字段,其表示展示的是当前选中的快照记录所有对象的分配情况,而我们想要知道的是第二条快照与第一条快照的区别在哪,所以选择Object allocated between Snapshot1 and Snapshot2
即展示第一条快照和第二条快照存在差异的内存对象分配情况,此时可以看到Array的百分比很高,初步可以判断是该变量存在问题,点击查看详情后就能查看到该变量对应的具体数据了
以上就是一个判断闭包带来内存泄漏问题并简单定位的方法了
2. 全局变量
全局的变量一般是不会被垃圾回收掉的当然这并不是说变量都不能存在全局,只是有时候会因为疏忽而导致某些变量流失到全局,例如未声明变量,却直接对某变量进行赋值,就会导致该变量在全局创建,如下所示:
function fn1() {
// 此处变量name未被声明
name = new Array(99999999)
}
fn1()
- 此时这种情况就会在全局自动创建一个变量
name
,并将一个很大的数组赋值给name
,又因为是全局变量,所以该内存空间就一直不会被释放 - 解决办法的话,自己平时要多加注意,不要在变量未声明前赋值,或者也可以
开启严格模式
,这样就会在不知情犯错时,收到报错警告,例如
function fn1() {
'use strict';
name = new Array(99999999)
}
fn1()
3. 分离的DOM
节点
假设你手动移除了某个dom
节点,本应释放该dom节点所占用的内存,但却因为疏忽导致某处代码仍对该被移除节点有引用,最终导致该节点所占内存无法被释放,例如这种情况
<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')
let child = document.querySelector('.child')
let root = document.querySelector('#root')
btn.addEventListener('click', function() {
root.removeChild(child)
})
script>
该代码所做的操作就是点击按钮后移除
.child
的节点,虽然点击后,该节点确实从dom
被移除了,但全局变量child
仍对该节点有引用,所以导致该节点的内存一直无法被释放,可以尝试用Memory
的快照功能来检测一下,如图所示
同样的先记录一下初始状态的快照,然后点击移除按钮后,再点击一次快照,此时内存大小我们看不出什么变化,因为移除的节点占用的内存实在太小了可以忽略不计,但我们可以点击第二条快照记录,在筛选框里输入
detached
,于是就会展示所有脱离了却又未被清除的节点对象
解决办法如下图所示:
<div id="root">
<div class="child">我是子元素div>
<button>移除button>
div>
<script>
let btn = document.querySelector('button')
btn.addEventListener('click', function() {
let child = document.querySelector('.child')
let root = document.querySelector('#root')
root.removeChild(child)
})
script>
改动很简单,就是将对
.child
节点的引用移动到了click
事件的回调函数中,那么当移除节点并退出回调函数的执行上文后就会自动清除对该节点的引用,那么自然就不会存在内存泄漏的情况了,我们来验证一下,如下图所示:
结果很明显,这样处理过后就不存在内存泄漏的情况了
4. 控制台的打印
<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)
console.log(obj);
})
script>
我们在按钮的点击回调事件中创建了一个很大的数组对象并打印,用performance
来验证一下
开始录制,先触发一次垃圾回收清除初始的内存,然后点击三次按钮,即执行了三次点击事件,最后再触发一次垃圾回收。查看录制结果发现
JS Heap
曲线成阶梯上升,并且最终保持的高度比初始基准线高很多,这说明每次执行点击事件创建的很大的数组对象obj
都因为console.log
被浏览器保存了下来并且无法被回收
接下来注释掉console.log
,再来看一下结果:
<button>按钮button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)
// console.log(obj);
})
script>
可以看到没有打印以后,每次创建的obj
都立马被销毁了,并且最终触发垃圾回收机制后跟初始的基准线同样高,说明已经不存在内存泄漏的现象了
其实同理 console.log
也可以用Memory
来进一步验证
未注释 console.log
注释掉了console.log
最后简单总结一下:在开发环境下,可以使用控制台打印便于调试,但是在生产环境下,尽可能得不要在控制台打印数据。所以我们经常会在代码中看到类似如下的操作:
// 如果在开发环境下,打印变量obj
if(isDev) {
console.log(obj)
}
这样就避免了生产环境下无用的变量打印占用一定的内存空间,同样的除了
console.log
之外,console.error
、console.info
、console.dir
等等都不要在生产环境下使用
5. 遗忘的定时器
定时器也是平时很多人会忽略的一个问题,比如定义了定时器后就再也不去考虑清除定时器了,这样其实也会造成一定的内存泄漏。来看一个代码示例:
<button>开启定时器button>
<script>
function fn1() {
let largeObj = new Array(100000)
setInterval(() => {
let myObj = largeObj
}, 1000)
}
document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>
这段代码是在点击按钮后执行fn1
函数,fn1
函数内创建了一个很大的数组对象largeObj
,同时创建了一个setInterval
定时器,定时器的回调函数只是简单的引用了一下变量largeObj
,我们来看看其整体的内存分配情况吧:
按道理来说点击按钮执行fn1
函数后会退出该函数的执行上下文,紧跟着函数体内的局部变量应该被清除,但图中performance
的录制结果显示似乎是存在内存泄漏问题的,即最终曲线高度比基准线高度要高,那么再用Memory
来确认一次:
- 在我们点击按钮后,从动态内存分配的图上看到出现一个蓝色柱形,说明浏览器为变量
largeObj
分配了一段内存,但是之后这段内存并没有被释放掉,说明的确存在内存泄漏的问题,原因其实就是因为setInterval
的回调函数内对变量largeObj
有一个引用关系,而定时器一直未被清除,所以变量largeObj
的内存也自然不会被释放 - 那么我们如何来解决这个问题呢,假设我们只需要让定时器执行三次就可以了,那么我们可以改动一下代码:
<button>开启定时器button>
<script>
function fn1() {
let largeObj = new Array(100000)
let index = 0
let timer = setInterval(() => {
if(index === 3) clearInterval(timer);
let myObj = largeObj
index ++
}, 1000)
}
document.querySelector('button').addEventListener('click', function() {
fn1()
})
script>
现在我们再通过performance
和memory
来看看还不会存在内存泄漏的问题
performance
这次的录制结果就能看出,最后的曲线高度和初始基准线的高度一样,说明并没有内存泄漏的情况
memory
这里做一个解释,图中刚开始出现的蓝色柱形是因为我在录制后刷新了页面,可以忽略;然后我们点击了按钮,看到又出现了一个蓝色柱形,此时就是为fn1
函数中的变量largeObj
分配了内存,3s
后该内存又被释放了,即变成了灰色柱形。所以我们可以得出结论,这段代码不存在内存泄漏的问题
简单总结一下: 大家在平时用到了定时器,如果在用不到定时器后一定要清除掉,否则就会出现本例中的情况。除了
setTimeout
和setInterval
,其实浏览器还提供了一个API
也可能就存在这样的问题,那就是requestAnimationFrame
- 好了好了,学完了,ui妹妹我来了
- ui妹妹:去你m的,滚远点
好了兄弟们,内存泄漏学会了吗?
来源:juejin.cn/post/7309040097936474175
面包会有的,玫瑰也会有的
前言
在杭州持续半个多月的阴雨中,迎来了“大火收汁”的7月🥵。转眼间这个2024年也过去了一半,我也从大学毕业做了2年的“来杭州讨饭的🐕”了。本来最近是有点忙的,7月底要疗休养,但是突然来了不少活,不过现在要等接口开发好,所以还是来做一下年中暨成为社畜两周年总结了。
减肥(膝盖要紧)
减不动了🤣,之前走路走太多了,把自己走出了滑膜炎,现在多走几千步膝盖就会疼(想想自己养成走路习惯还是因为买 huawei 手环下载的 APP 上面的成就奖牌,当时为了拿成就一天两万多步)。之前减肥是真的快啊,三四个月从 83kg 减到 68kg ,最近 4 个多月没怎么运动了,也就保持在 71kg 左右。家里人也不让我减了,说再减就难看了。
补牙(双连:一战成名)
去年体检被发现有了一颗蛀牙,今年才有时间去补,第一次体验补牙还挺新奇的(不过很快就不新奇了)。好在只蛀到一点点神经,上了点药阻断一下就补上了。原本以为这种事短时间内不会再体验了,结果 2 个月前在我品尝我的梅干菜肉饼时,我左上的一颗槽牙被神奇地磕掉了一小块😅......好嘛,再次喜提牙科医生的修补打磨。
工作学习(平平淡淡)
我这个小小前端每天也就是砌个div了,这半年没整太多新东西,就是基于公司的已有平台,加加feature、修修bug,把官网PC端和移动端换了一遍样式,现在官网到处是UI加的毛玻璃特效,在我这个 8G 内存小 thinkbook 机子的浏览器上肉眼可见的卡顿🫠(可能因为我的核显不行吧,我加了搜到的translateZ(0)
也没体会到加速),没办法可能用户电脑都很强能带的动吧。
不过说了好几个月的基于 umi + qiankun 的新平台在下半年终于是要交付了,又重新过了一遍代码和开发流程。有一说一,真的很麻烦,整个平台根本就不大,完全想不通为什么要上微前端,我们目前开发团队算上外包同学也就3个前端了,而且后续可能就我一个人负责这个平台的前端开发和维护😣。
工作之外重温了一下之前了解过的 SolidJS、Svelte 和 Tauri,写了几个小 Demo
练练手:
3月份打算今年11月考软考高项的,但是刚看了一个月的书,就通知只有上半年考了🫤。我寻思报名费这么贵,两三个月准备时间岂不是做慈善?!那就明年5月份再考吧,下半年该把书啊什么的再拿出来看了。
吃喝玩乐(还得是家乡的味道)
5月份,带女朋友回了老家徐州玩。感觉物价比起过去涨了好多,尤其是节假日的酒店(不敢想要是高铁也在节假日涨价,我过年还回不回得起家),苏宁广场的绿茶餐厅比杭州in77的还贵。宝莲寺在最近徐州旅游火起来之前我都没听说过😹。羊肉串、菜煎饼、米线、蒸菜、冷面味道还是好吃的👍,不过在家待的几天都是出去下馆子,没怎么吃到家里人做的菜,饭馆基本都是除了几道特色菜好吃,其他都一般般。
还得是过年时,家里做的好吃🫡。
再秀两张在宝莲寺的情侣照😎
展望
女朋友也毕业了,后面就是两个人在杭州打拼了。现在的工作其实挺好的,但是没有机会爬上去,爬不上去就没法在这个房价、物价如此离谱的城市留下。爸妈总是跟我说他们当初在一起的时候什么也没有,后面还是一起打拼出了这个家,面包总会有的。不过我想社会发展到现在,想要的也不一样了,面包要有,玫瑰也要有的吧。
hh,再写就要丧起来了。说不准哪天就中彩-票了,什么面包、玫瑰都不是事😇,全都做成鲜花饼😡,硌不坏牙的那种。
来源:juejin.cn/post/7386848746913366056