注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

技术总监写的十个方法,让我精通了lambda表达式

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。 我自己照着写了一遍,改了名字,分享给大家。 一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如 将 C...
继续阅读 »

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。


我自己照着写了一遍,改了名字,分享给大家。


一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


Collection 集合类型到 Map类型的转化。


Collection 转化为 Map


由于 List 和 Set 是 Collection 类型的子类,所以只需要实现Collection 类型转化为 Map 类型即可。
Collection转化为 Map 共分两个方法



  1. Collection<OrderItem> Map<Key, OrderItem>,提取 Key, Map 的 Value 就是类型 OrderItem

  2. Collection<OrderItem>Map<Key,Value> ,提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。


使用样例


代码示例中把Set<OrderItem> 转化为 Map<Long, OrderItem>Map<Long ,Double>


@Test
public void testToMap() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);
}

@Test
public void testToMapV2() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, Double> map = toMap(set, OrderItem::getOrderId, OrderItem::getActPrice);
}

代码展示


public static <T, K> Map<K, T> toMap(Collection<T> collection, Function<? super T, ? extends K> keyMapper) {
return toMap(collection, keyMapper, Function.identity());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction)
{
return toMap(collection, keyFunction, valueFunction, pickSecond());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction,
BinaryOperator<V> mergeFunction)
{
if (CollectionUtils.isEmpty(collection)) {
return new HashMap<>(0);
}

return collection.stream().collect(Collectors.toMap(keyFunction, valueFunction, mergeFunction));
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

Map格式转换


转换 Map 的 Value



  1. 将 Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  2. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }。


测试样例


@Test
public void testConvertValue() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);

Map<Long, Double> orderId2Price = convertMapValue(map, item -> item.getActPrice());
Map<Long, String> orderId2Token = convertMapValue(map, (id, item) -> id + item.getName());

}

代码展示


public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> map, 
BiFunction<K, V, C> valueFunction,
BinaryOperator<C> mergeFunction)
{
if (isEmpty(map)) {
return new HashMap<>();
}
return map.entrySet().stream().collect(Collectors.toMap(
e -> e.getKey(),
e -> valueFunction.apply(e.getKey(), e.getValue()),
mergeFunction
));
}

public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> originMap, BiFunction<K, V, C> valueConverter) {
return convertMapValue(originMap, valueConverter, Lambdas.pickSecond());
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

集合类型转化


Collection 和 List、Set 的转化



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>


public static <T> List<T> toList(Collection<T> collection) {
if (collection == null) {
return new ArrayList<>();
}
if (collection instanceof List) {
return (List<T>) collection;
}
return collection.stream().collect(Collectors.toList());
}

public static <T> Set<T> toSet(Collection<T> collection) {
if (collection == null) {
return new HashSet<>();
}
if (collection instanceof Set) {
return (Set<T>) collection;
}
return collection.stream().collect(Collectors.toSet());
}

测试样例


@Test//将集合 Collection 转化为 List
public void testToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);
}

@Test//将集合 Collection 转化为 Set
public void testToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);
}

List和 Set 是 Collection 集合类型的子类,所以无需再转化。


List、Set 类型之间的转换


业务中有时候需要将 List<A> 转化为 List<B>。如何实现工具类呢?


public static <T, R> List<R> map(List<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> map(Set<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

public static <T, R> List<R> mapToList(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> mapToSet(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

测试样例



  1. List<OrderItem> 转化为 List<Long>

  2. Set<OrderItem> 转化为 Set<Long>

  3. Collection<OrderItem> 转化为 List<Long>

  4. Collection<OrderItem> 转化为 Set<Long>


@Test
public void testMapToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);

List<Long> orderIdList = map(list, (item) -> item.getOrderId());
}

@Test
public void testMapToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(coll);

Set<Long> orderIdSet = map(set, (item) -> item.getOrderId());
}

@Test
public void testMapToList2() {
Collection<OrderItem> collection = coll;

List<Long> orderIdList = mapToList(collection, (item) -> item.getOrderId());
}

@Test
public void testMapToSetV2() {
Collection<OrderItem> collection = coll;

Set<Long> orderIdSet = mapToSet(collection, (item) -> item.getOrderId());

}

总结一下 以上样例包含了如下的映射场景



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


作者:五阳神功
来源:juejin.cn/post/7305572311812587531
收起阅读 »

看不了电视直播了?那就自己做一个(一)

web
事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。 开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。 电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。 虽...
继续阅读 »

事情的起因是这样的,前两天打开电视看直播,突然变成了下面这个画面。


j29zRYWy.jpeg


开始以为是太久没更新了,想着重新安装一下就好了。结果上网一查才知道,电视直播软件都下架了。


电视直播只能安装有线,或者通过央视频手机收看了。有线暂时安装不了,用手机看又总是感觉很别扭。


a71ea8d3fd1f4134205611b2199935ccd0c85e21.png


虽然也可以通过投屏的方式用电视播放,但切换频道时还得使用手机操作,非常的麻烦。


广电的这波操作出发点可能是好的,后续应该会提供其他收看方式,但是目前这个真空期着实有点尴尬。思来想去,干脆自己动手做一个吧


就是干.jpg


经过一个周末的折腾,过程虽然有一点点曲折,但总算是完成了第一个电视版本,感觉和以前相比,清晰度还不错,切换也更流畅。


111.gif


再来看一下卫视。


也是OK的


22 (1).gif


电视端有了,又突然想着把它放到手机上,虽然手机上可以直接使用央视频播放,但还是有点繁琐,于是又稍微做了一些调整,推出了一个手机端的版本,切换还是相当的丝滑。


手机.gif


最后我其实还改了一版在电脑上使用的,但是这个除了摸鱼好像也没别的用处,所以对于我来说意义不大。


实现篇


接下来说一下具体的实现,会涉及到一些编程相关的内容,如果不感兴趣可以直接跳到结尾。


客户端应用开发


这一篇先介绍客户端的应用的开发,主要就是安卓应用的开发。虽然以前没有这方面经验,但是想法有了,剩下的交给chatGpt就好了。


1. 播放器


首先是播放器的选择,一开始我采用了原生MediaPlayer,主要是考虑到跟各版本安卓系统的兼容性会好一点,而且它使用起来非常的简单,十几行代码就搞定了。


public class PlayActivity extends Activity {
ChannelService channelService;
VideoView videoView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
channelService = new HttpChannelService();
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
channelService.loadChannels((success, message) -> runOnUiThread(() -> {
Channel channel = channelService.getDefaultChannel();
videoView.setVideoURI(Uri.parse(channel.getSource()));
videoView.start();
}));
}
}

后来替换成了谷歌开源的ExoPlayer,因为只是简单使用,所以代码基本上也没什么差别。


public class PlayActivity extends Activity {
ChannelService channelService;
private StyledPlayerView videoView;
private ExoPlayer player;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
initChannels();
}

private void initView() {
setContentView(R.layout.activity_play);
videoView = findViewById(R.id.video_view);
player = new ExoPlayer.Builder(this).build();
videoView.setPlayer(player);
}

private void initChannels() {
channelService = new HttpChannelService();
channelService.loadChannels((boolean success, String message) -> runOnUiThread(() -> {
ChannelService.Channel channel = channelService.getDefaultChannel();
play(channel);
}));
}

private void play(Channel channel){
player.setMediaItem(MediaItem.fromUri(channel.getSource()));
player.prepare();
player.setPlayWhenReady(true);
}
}

2. 监听器


接下来就是考虑对遥控器按键的监听处理,对于这个应用而言,只需要监控方向键以及退出键就好了,当然也可以根据需要对菜单键或者确定键进行响应。


videoView.setOnKeyListener((view, keyCode, event) -> {
switch (keyCode) {
// 向下操作处理 切换下一个频道
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.getAction() == KeyEvent.ACTION_DOWN) {
Channel channel = channelService.getNextChannel();
play(channel);
return true;
}
break;
}
return false;
});

3. 视频源管理


把ChannelService留到最后讲,是因为它的作用通过下面的接口定义就一目了然了。


这里之所以要定义成接口,比如常见的通过m3u获取,是因为考虑到视频源可能有不同实现,关于实现部分会在下一篇详细讲解。


public interface ChannelService {

/**
* 加载频道
*/

void loadChannels(LoadCallBack callBack);

/**
* 获取默认频道
*/

Channel getDefaultChannel();

/**
* 获取下一个频道
*/

Channel getNextChannel();

/**
* 获取前一个频道
*/

Channel getPrevChannel();

}

4. 手机版


手机版因为没有了遥控器,所以需要对触屏动作进行监听来对视频进行操控,主要就是左右滑动的切换,以及上下滑动的音量调节等。


结语


因为是即兴的创作,也没有打算能长久使用,所以很多细节我并没有考虑,比如内容的缓存,节目回看,网络监控等等这些。但是这几天使用下来体验还是挺不错的。后续我可以把整个源码开放出来,大家有兴趣可以自行去补充。


到这里客户端的实现就讲完了,下一篇再讲一下其他部分的实现。


作者:双子小匠
来源:juejin.cn/post/7311961893610995748
收起阅读 »

带圆角的虚线边框?CSS 不在话下

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样: 这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码: div { border-radius: 25px; border: 2px dashed...
继续阅读 »

今天,我们来看这么一个非常常见的切图场景,我们需要一个带圆角的虚线边框,像是这样:



这个我们使用 CSS 还是可以轻松解决的,代码也很简单,核心代码:


div {
border-radius: 25px;
border: 2px dashed #aaa;
}

但是,原生的 dashed 有一个问题,就是我们无法控制虚线的单段长度与间隙


假设,我们要这么一个效果呢虚线效果呢:



此时,由于无法控制 border: 2px dashed #aaa 产生的虚线的单段长度与线段之间的间隙,border 方案就不再适用了。


那么,在 CSS 中,我们还有其它方式能够实现带圆角,且虚线的单段长度与线段之间间隙可控的方式吗?


本文,我们就一起探讨探讨。


实现不带圆角的虚线效果


上面的场景,使用 CSS 实现起来比较麻烦的地方在于,图形有一个 border-radius


如果不带圆角,我们可以使用渐变,很容易的模拟虚线效果。


我们可以使用线性渐变,轻松的模拟虚线的效果:


div {
width: 150px;
height: 100px;
background: linear-gradient(90deg, #333 50%, transparent 0) repeat-x;
background-size: 4px 1px;
background-position: 0 0;
}

看看,使用渐变模拟的虚线如下:



解释一下上面的代码:



  1. linear-gradient(90deg, #333 50%, transparent 0),实现一段渐变内容,100% - 50% 的内容是 #333 颜色,剩下的一半 50% - 0 的颜色是透明色 transprent

  2. repeat-x 表示只在 x 方向重复

  3. background-size: 4px 1px 表示上述渐变内容的长宽分别是 4px\ 1px,这样配合 repeat-x就能实现只有 X 方向的重复

  4. 最后的 background-position: 0 0 控制渐变的定位


因此,我们只需要修改 background 的参数,就可以得到各种不一样的虚线效果:



完整的代码,你可以戳这里:CodePen Demo -- Linear-gradient Dashed Effect


并且,渐变是支持多重渐变的,因此,我们把容器的 4 个边都用渐变表示即可:


div {
background:
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(90deg, #333 50%, transparent 0) repeat-x,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y,
linear-gradient(0deg, #333 50%, transparent 0) repeat-y;
background-size: 4px 1px, 4px 1px, 1px 4px, 1px 4px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}

效果如下:



但是,如果要求的元素带 border-radius 圆角,这个方法就不好使了,整个效果就会穿帮。


因此,在有圆角的情况下,我们就需要另辟蹊径。


利用渐变实现带圆角的虚线效果


当然,本质上我们还是需要借助渐变效果,只是,我们需要转换一下思路。


譬如,我们可以使用角向渐变。


假设,我们有这么一个带圆角的元素:


<div>div>

div {
width: 300px;
height: 200px;
background: #eee;
border-radius: 20px;
}

效果如下:



如果我们修改内部的 background: #eee,把它替换成重复角向渐变的这么一个图形:


div {
//...
- background: #eee;
+ background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);
}

解释一下,这段代码创建了一个重复的角向渐变背景,从黑色(#000)开始,每 3deg 变为透明,然后再从透明到黑色,以此循环重复。


此时,这样的背景效果可用于创建一种渐变黑色到透明的重复纹理效果:



在这个基础上,我们只需要给这个图形上层,再利用伪元素,叠加一层颜色,就得到了我们想要的边框效果,并且,边框间隙和大小可以简单调整。


完整的代码:


div {
position: relative;
width: 300px;
height: 200px;
border-radius: 20px;
background: repeating-conic-gradient(#000, #000 3deg, transparent 3deg, transparent 6deg);

&::before {
content: "";
position: absolute;
inset: 1px;
background: #eee;
border-radius: 20px;
}
}

效果如下:



乍一看,效果还不错。但是如果仔细观察,会发现有一个致命问题:虚线线段的每一截长度不一致


只有当图形的高宽一致时,线段长度才会一致。高宽比越远离 1,差异则越大:



完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


那有没有办法让虚线长度能够保持一样呢?


可以!我们再换一种渐变,我们改造一下底下的角向渐变,重新利用重复线性渐变:


div {
border-radius: 20px;
background:
repeating-linear-gradient(
-45deg,
#000 0,
#000 7px,
transparent 7px,
transparent 10px
);
}

此时,我们能得到这样一个斜 45° 的重复线性渐变图形:



与上面方法一类似,再通过在这个图形的基础上,在元素中心,叠加多一层纯色遮罩图形,只漏出最外围一圈的图形,带圆角的虚线边框就实现了:



此方法比上面第一种渐变方法更好之处在于,虚线每一条线段的长度是固定的!是不是非常的巧妙?


完整的代码,你可以戳这里:CodePen Demo -- BorderRadius Dashed Border


最佳解决方案:SVG


当然,上面使用 CSS 实现带圆角的虚线边框,还是需要一定的 CSS 功底。


并且,不管是哪个方法,都存在一定的瑕疵。譬如如果希望边框中间不是背景色,而是镂空的,上述两种 CSS 方式都将不再使用。


因此,对于带圆角的虚线边框场景,最佳方式一定是 SVG。(切图也算是吧,但是灵活度太低)


只是很多人看到 SVG 会天然的感到抗拒,或者认为 SVG 不太好掌握。


所以,本文再介绍一个非常有用的开源工具 -- Customize your CSS Border



通过这个开源工具,我们可以快速生成我们想要的虚线边框效果,并且一键复制可以嵌入到 CSS background 中的 SVG 代码图片格式。


图形的大小、边框的粗细、虚线的线宽与间距,圆角大小统统是可以可视化调整的。


通过一个动图,简单感受一下:



总结一下


本文介绍了 2 种在 CSS 中,不借助切图和 SVG 实现带圆角的虚线边框的方式:



  1. 重复角向渐变叠加遮罩层

  2. 重复线性渐变叠加遮罩层


当然,两种 CSS 方式都存在一定瑕疵,但是对于一些简单场景是能够 Cover 住的。


最后,介绍了借助 SVG 工具 Customize your CSS Border 快速生成带圆角的虚线边框的方式。将 SVG 生成的矢量图像数据直接嵌入到 background URL 中,能够应付几乎所有场景,相对而言是更好的选择。


最后


好了,本文到此结束,希望本文对你有所帮助 :)


作者:Chokcoco
来源:juejin.cn/post/7311681326712487999
收起阅读 »

前端打包后,静态文件的名字为什么是一串Hash值?

web
引言 前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。 静...
继续阅读 »

引言


前段时间公司需要招聘几个初级前端,面试过程中,问了这么一个问题:“项目打包后的dist文件夹中,比如js、css这些文件的名称为什么是hash值的,就是一串无规则的字符串”。基本上都不知道静态文件为什么需要这种无规则的hash值,今天就稍微说一下哈。


静态文件何时被加载


拿常用的单页面应用举例,当我们访问一个网站的时候,最终会指向 index.html 这个文件,也就是打包后的 dist 文件夹中的 index.html


比如说 https://some-domain.com/home, 并点击回车键,我们的服务器中实际上没有这个路由,但我们不是返回 404,而是返回我们的 index.html。为什么地址中我们没有输入index.html这个路径,但还是指向到 index.html文件并加载它?因为现在大多数都用nginx去部署,一般在url中输入地址的时候末尾都会加个 “/” ,nginx中已经把“/”重定向到 index.html文件了


image.png


此时按下回车,这个 index.html 文件就被加载获取到了,然后开始自上而下的去加载里面的引用和代码,比如在html中引入的css、js、图片文件。


浏览器默认缓存


当用户按下回车键就向目标服务器去请求index.html文件,加载解析index.html文件的同时就会连带着加载里面的js、css文件。有没有想过,用户第一次已经从服务器请求下载静态文件到客户端了,第二次去浏览该网站该不会还让我去向服务器请求吧,不会吧不会吧,如果每次都请求下载,那用户的体验多不好,每次请求都需要时间,不说服务器的压力会增加,最重要的是用户的体验感,展现到用户眼前的时间会增加!


所以说浏览器已经想到这个了,当请求静态资源的时候,就会默认缓存请求过来的静态文件,这种浏览器自带的默认缓存就叫做 启发式缓存 。 除非手动设置不需要缓存no-store,此时请求静态文件的时候文件才不会缓存到本地!


浏览器默认缓存详情可见 MDN 启发式缓存。不管什么缓存都有缓存的时效性吧,如果想知道 启发式缓存 到底把静态文件缓存了多久,可以阅读笔者的这篇文章 浏览器的启发式缓存到底缓存了多久?


vue-cli里的默认配置,css和js的名字都加了哈希值,所以新版本css、js和就旧版本的名字是不同的,不会有缓存问题。


Hash值的作用


那既然知道了浏览器会有默认的缓存,当加载静态资源的时候会命中启发式缓存并缓存到本地。那如果我们重新部署前端包的时候,如何去请求新的静态资源呢,而不是缓存的静态资源?这时候就得用到hash值了


下面模拟了掘金网站的静态资源获取,当请求静态资源的时候,实际访问的是服务器中静态资源存放的位置
image.png


返回即是当前请求静态资源的具体内容
image.png


第一次进来的时候会去请求服务器的资源缓存到本地,比如 0dbcf63.js 这个文件就被缓存到本地了,后面再正常刷新就直接获取的是缓存中的资源了(disk cache 内存缓存)。


如果前端包重新部署后,试想一下如果 0dbcf63.js这个js文件不叫这个无规则的名字,而是固定的名字,那浏览器怎么知道去请求缓存中的还是服务器中的,所以浏览器的机制就是请求缓存中的,除非缓存中的过期了,才会去请求服务器中的静态资源。如果没有请求到最新的资源,页面也不会更新到最新的内容,影响用户的体验。


那浏览器这个机制是怎么判断的,那就是根据资源的名称,该资源的资源名称如果没变 并且 没有设置不缓存 并且 资源没过期,那就会请求缓存中的资源,否则就会请求服务器中的资源


image.png



当静态资源的名称发生变化的时候,请求路径就会发生变化,所以就会重新命中服务器新部署的静态资源!这就是为什么需要hash值的原因,为了区分之前部署的文件和这次部署文件的区别,好让浏览器去重新获取最新的资源。



第三方库


由于像 lodash 或 react 这样的第三方库很少像本地源代码一样频繁修改,因此通常推荐将第三方库提取到单独的 chunk-vendor 中


 const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
title: 'Caching',
}),
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGr0ups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};

这样依赖的静态文件就会打包到chunk-vendor中,并且多次打包不会改变文件的hash值,以上是webpack原生的配置,如果使用的vue脚手架,那么脚手架已经都配置好了。


image.png


作者:Lakeiedward
来源:juejin.cn/post/7311219067199881254
收起阅读 »

三行代码实现完美瀑布流

web
需求 最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。 难点 如果绝对定位,如何定位每个卡片的位置。 因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,...
继续阅读 »

需求


最近准备做一个瀑布流的需求,这里每个卡片的高度是不固定的,有点类似下图这样的。



难点



  1. 如果绝对定位,如何定位每个卡片的位置。
    因为可以发现需求里边的每个卡片的高度都是不固定的,所以如果想使用绝对定位的话,需要实时动态的计算每个卡片的left、top,这里会涉及大量的计算。


因为每个卡片的高度是不固定,所以如果想要计算left、top,必须首先获取卡片的高度,但是卡片里边不仅包含图片,还有文字,这个时候计算高度是比较困难的。




  1. 如何结合虚拟列表实现瀑布流


这个时候必须要根据scrollTop的位置,判断什么时候需要加载哪些数据,判断可视区域里边数据的起始索引以及结束索引,这里同样会涉及大量的计算,同时还因为每个卡片的高度不固定,甚至只有图片和文字加载到浏览器以后,才能得到真实的高度,这样会更困难。


解决方案


解决方案1



  1. 如果绝对定位,如何定位每个卡片的位置。


1.1 后端计算
后端可以先把每个图片的高度和宽度提前计算好,直接返回给前端进行处理,然后前端根据后端返回的图片高度和宽度,然后再动态的计算出每个卡片的高度(文字部分也可以固定高度,使用省略号实现)。


1.2 前端计算


前端计算还是比较麻烦的,需要先等卡片组件加载完成,才能得到宽度和高度,而且因为数据量比较大,每个卡片计算出来以后,还需要去根据计算出来的结果去更新left、top,会非常麻烦。
这里可以采用node作为中间层进行计算,还是使用类似后端计算的思路。


还有一种方法是使用observe api 动态观察每个卡片,当观察到卡片加载完成后,再动态根据卡片的宽度和高度计算,不过这样同样很麻烦。



  1. 如何结合虚拟列表实现瀑布流


这里因为卡片的高度是不固定的,同时也是瀑布流,所以不能使用react-window 来解决,不过可以使用react-window的类似思路,自己封装一个npm 包,根据scroll事件判断需要加载那些数据。


解决方案2


使用css3的columns来实现,该技术解决方案不需要计算高度,也不需要去定位,但是columns这个属性会把卡片高度给切开
如下图:



不过可以使用下面代码来解决,


js
复制代码

.test {
// color: red;
// height: 2000px;
background-color: red;
gap: 1rem;
columns: 5;
.no-break {
break-inside: avoid;
}
}

效果如下:



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

推荐一个小而全的第三方登录开源组件

大家好,我是 Java陈序员。 我们在企业开发中,常常需要实现登录功能,而有时候为了方便,就需要集成第三方平台的授权登录。如常见的微信登录、微博登录等,免去了用户注册步骤,提高了用户体验。 为了业务考虑,我们有时候集成的不仅仅是一两个第三方平台,甚至更多。这就...
继续阅读 »

大家好,我是 Java陈序员


我们在企业开发中,常常需要实现登录功能,而有时候为了方便,就需要集成第三方平台的授权登录。如常见的微信登录、微博登录等,免去了用户注册步骤,提高了用户体验。


为了业务考虑,我们有时候集成的不仅仅是一两个第三方平台,甚至更多。这就会大大的提高了工作量,那么有没有开源框架来统一来集成这些第三方授权登录呢?


答案是有的,今天给大家介绍的项目提供了一个第三方授权登录的工具类库


项目介绍


JustAuth —— 一个第三方授权登录的工具类库,可以让你脱离繁琐的第三方登录 SDK,让登录变得So easy!


JustAuth


JustAuth 集成了诸如:Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么、推特、飞书、京东、阿里云、喜马拉雅、Amazon、Slack和 Line 等第三方平台的授权登录。


功能特色:



  • 丰富的 OAuth 平台:支持国内外数十家知名的第三方平台的 OAuth 登录。

  • 自定义 state:支持自定义 State 和缓存方式,开发者可根据实际情况选择任意缓存插件。

  • 自定义 OAuth:提供统一接口,支持接入任意 OAuth 网站,快速实现 OAuth 登录功能。

  • 自定义 Http:接口 HTTP 工具,开发者可以根据自己项目的实际情况选择相对应的HTTP工具。

  • 自定义 Scope:支持自定义 scope,以适配更多的业务场景,而不仅仅是为了登录。

  • 代码规范·简单:JustAuth 代码严格遵守阿里巴巴编码规约,结构清晰、逻辑简单。


安装使用


回顾 OAuth 授权流程


参与的角色



  • Resource Owner 资源所有者,即代表授权客户端访问本身资源信息的用户(User),也就是应用场景中的“开发者A”

  • Resource Server 资源服务器,托管受保护的用户账号信息,比如 Github
    Authorization Server 授权服务器,验证用户身份然后为客户端派发资源访问令牌,比如 Github

  • Resource ServerAuthorization Server 可以是同一台服务器,也可以是不同的服务器,视具体的授权平台而有所差异

  • Client 客户端,即代表意图访问受限资源的第三方应用


授权流程


OAuth 授权流程


使用步骤


1、申请注册第三方平台的开发者账号


2、创建第三方平台的应用,获取配置信息(accessKey, secretKey, redirectUri)


3、使用 JustAuth 实现授权登陆


引入依赖


<dependency>
<groupId>me.zhyd.oauthgroupId>
<artifactId>JustAuthartifactId>
<version>{latest-version}version>
dependency>

调用 API


// 创建授权request
AuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder()
.clientId("clientId")
.clientSecret("clientSecret")
.redirectUri("redirectUri")
.build());
// 生成授权页面
authRequest.authorize("state");
// 授权登录后会返回code(auth_code(仅限支付宝))、state,1.8.0版本后,可以用AuthCallback类作为回调接口的参数
// 注:JustAuth默认保存state的时效为3分钟,3分钟内未使用则会自动清除过期的state
authRequest.login(callback);


说明:
JustAuth 的核心就是一个个的 request,每个平台都对应一个具体的 request 类。
所以在使用之前,需要就具体的授权平台创建响应的 request.如示例代码中对应的是 Gitee 平台。



集成国外平台



国外平台需要额外配置 httpConfig



AuthRequest authRequest = new AuthGoogleRequest(AuthConfig.builder()
.clientId("Client ID")
.clientSecret("Client Secret")
.redirectUri("应用回调地址")
// 针对国外平台配置代理
.httpConfig(HttpConfig.builder()
// Http 请求超时时间
.timeout(15000)
// host 和 port 请修改为开发环境的参数
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 10080)))
.build())
.build());

SpringBoot 集成


引入依赖


<dependency>
<groupId>com.xkcoding.justauthgroupId>
<artifactId>justauth-spring-boot-starterartifactId>
<version>1.4.0version>
dependency>

配置文件


justauth:
enabled: true
type:
QQ:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/qq/callback
union-id: false
WEIBO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/weibo/callback
GITEE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/gitee/callback
DINGTALK:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/dingtalk/callback
BAIDU:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/baidu/callback
CSDN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/csdn/callback
CODING:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/coding/callback
coding-group-name: xx
OSCHINA:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/oschina/callback
ALIPAY:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/alipay/callback
alipay-public-key: MIIB**************DAQAB
WECHAT_OPEN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_open/callback
WECHAT_MP:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_mp/callback
WECHAT_ENTERPRISE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_enterprise/callback
agent-id: 1000002
TAOBAO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/taobao/callback
GOOGLE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/google/callback
FACEBOOK:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/facebook/callback
DOUYIN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/douyin/callback
LINKEDIN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/linkedin/callback
MICROSOFT:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/microsoft/callback
MI:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/mi/callback
TOUTIAO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/toutiao/callback
TEAMBITION:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/teambition/callback
RENREN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/renren/callback
PINTEREST:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/pinterest/callback
STACK_OVERFLOW:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/stack_overflow/callback
stack-overflow-key: asd*********asd
HUAWEI:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/huawei/callback
KUJIALE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/kujiale/callback
GITLAB:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/gitlab/callback
MEITUAN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/meituan/callback
ELEME:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/eleme/callback
TWITTER:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/twitter/callback
XMLY:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/xmly/callback
# 设备唯一标识ID
device-id: xxxxxxxxxxxxxx
# 客户端操作系统类型,1-iOS系统,2-Android系统,3-Web
client-os-type: 3
# 客户端包名,如果 clientOsType 为12时必填。对Android客户端是包名,对IOS客户端是Bundle ID
#pack-id: xxxx
FEISHU:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/feishu/callback
JD:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/jd/callback
cache:
type: default

代码使用


@Slf4j
@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class TestController {
private final AuthRequestFactory factory;

@GetMapping
public List list() {
return factory.oauthList();
}

@GetMapping("/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}

@RequestMapping("/{type}/callback")
public AuthResponse login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse response = authRequest.login(callback);
log.info("【response】= {}", JSONUtil.toJsonStr(response));
return response;
}

}

总结


JustAuth 集成的第三方授权登录平台,可以说是囊括了业界中大部分主流的应用系统。如国内的微信、微博、Gitee 等,还有国外的 Github、Google 等。可以满足我们日常的开发需求,开箱即用,可快速集成!


最后,贴上项目地址:


https://github.com/justauth/JustAuth

在线文档:


https://www.justauth.cn/

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/

作者:Java陈序员
来源:juejin.cn/post/7312060958175559743
收起阅读 »

token过期了怎么办?

token过期了怎么办?一般做法是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。那应该怎么办呢?其实这是一个老生常谈的问题,但是最近发现很多人并不清楚,所以今天就一次讲清这...
继续阅读 »

token过期了怎么办?一般做法是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。那应该怎么办呢?其实这是一个老生常谈的问题,但是最近发现很多人并不清楚,所以今天就一次讲清这个问题!

token 过期处理

没有绝对的安全, 所谓的安全处理, 就是提高攻击者攻击的难度, 对他造成了一定的麻烦, 我们这个网站就是安全的! 网站安全性就是高的! 所以: token 必须要有过期时间!

token 过期问题

目标: 了解token过期问题的存在, 学习token过期的解决思路

现象:

你登陆成功之后,接口会返回一个token值,这个值在后续请求时带上(就像是开门钥匙)。

但是,这个值一般会有有效期(具体是多长,是由后端决定),在我们的项目中,这个有效期是2小时。

如果,上午8点登陆成功,到了10:01分,则token就会失效,再去发请求时,就会报401错误。

思考:

  1. token需要过期时间吗 ?

    token即是获取受保护资源的凭证,当然必须有过期时间。否则一次登录便可永久使用,认证功能就失去了其意义。非但必须有个过期时间,而且过期时间还不能太长,

    参考各个主流网站的token过期时间,一般1小时左右

    token一旦过期, 一定要处理, 不处理, 用户没法进行一些需要授权页面的使用了

  2. token过期该怎么办?

    token过期,就要重新获取。

    那么重新获取有两种方式,一是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。

    那么还剩第二种方法,那就是主动去刷新token. 主动刷新token的凭证是refresh token,也是加密字符串,并且和token是相关联的。相比可以获取各种资源的token,refresh token的作用仅仅是获取新的token,因此其作用和安全性要求都大为降低,所以其过期时间也可以设置得长一些。

目标效果 - 保证每一小时, 都是一个不同的token

第一次请求 9:00 用的是 token1  第二次请求 12:00 用的是 token2

当用户登陆成功之后,返回的token中有两个值,说明如下:

image.png

  • token:

    • 作用:在访问一些接口时,需要传入token,就是它。
    • 有效期:2小时。
  • refresh_token

    • 作用: 当token的有效期过了之后,可以使用它去请求一个特殊接口(这个接口也是后端指定的,明确需要传入refresh_token),并返回一个新的token回来(有效期还是2小时),以替换过期的那个token。
    • 有效期:14天。(最理想的情况下,一次登陆可以持续14天。)

image.png

对于 某次请求A 的响应,如果是401错误

  • 有refresh_token,用refresh_token去请求回新的token

    • 新token请求成功

      • 更新本地token
      • 再发一次请求A
    • 新token请求失败

      • 清空vuex中的token
      • 携带请求地址,跳转到登陆页
  • 没有refresh_token

    • 清空vuex中的token
    • 携带请求地址,跳转到登陆页

对于一个请求的响应 401, 要这么处理, 对于十个请求的响应 401, 也要这么处理,

我们可以统一将这个token过期处理放在响应拦截器中

请求拦截器: 所有的请求, 在真正被发送出去之前, 都会先经过请求拦截器 (可以携带token)

响应拦截器: 所有的响应, 在真正被(.then.catch await)处理之前, 都会先经过响应拦截器, 可以在这个响应拦截器中统一对响应做判断

响应拦截器处理token

目标: 通过 axios 响应拦截器来处理 token 过期的问题

响应拦截器: http://www.kancloud.cn/yunye/axios…

  1. 没有 refresh_token 拦截到登录页, 清除无效的token

测试: {"token":"123.123.123"}

// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 对响应数据做点什么 (成功响应) response 就是成功的响应 res
return response
}, function (error) {
// 对响应错误做点什么 (失败响应) 处理401错误
// console.dir(error)
if (error.response.status === 401) {
console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
// 获取 refresh_token, 判断是否存在, 存在就去刷新token
const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
console.log('存在refreshToken, 需要进行刷新token操作')
} else {
// 没有refreshToken, 直接去登录, 将来还能跳回来
// router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
// 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
store.commit('removeToken')
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
}
return Promise.reject(error)
})

提供清除token的mutation

// 移出tokenInfo的信息, 恢复成空对象
removeToken (state) {
state.tokenInfo = {}
// 更新到本地, 本地可以清掉token信息
removeToken()
},
  1. 有 refresh_token 发送请求, 刷新token

测试操作: 将 token 修改成 xyz, 模拟 token 过期, 而有 refresh_token 发现401, 会自动帮你刷新token

{"refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDYzNTcyODcsInVzZXJfaWQiOjExMDI0OTA1MjI4Mjk3MTc1MDQsInJlZnJlc2giOnRydWV9.2A81gpjxP_wWOjclv0fzSh1wzNm6lNy0iXM5G5l7TQ4","token":"xyz"}

const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
console.log('存在refreshToken, 需要进行刷新token操作')
// (1) 发送请求, 进行刷新token操作, 获取新的token
// 注意: 这边发请求, 不用http实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
// 这边, 直接用 axios 发送请求
const res = await axios({
method: 'put',
url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
// 请求头中携带refresh_token信息
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
const newToken = res.data.data.token
// (2) 将新token更新到vuex中
store.commit('setTokenInfo', {
refresh_token: refreshToken,
token: newToken
})
}
  1. 刷新token后, 应该重新发送刚才的请求 (让用户刷新token无感知)
return http(error.config)
  1. 那万一 refresh_token 也过期了, 是真正的用户登录过期了 (一定要让用户重新登录的)

测试: {"refresh_token":"123.123","token":"123.123.123"} 修改后, 修改的是本地, 记得刷新一下

从哪拦走的, 就回到哪去

// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 对响应数据做点什么 (成功响应) response 就是成功的响应 res
return response
}, async function (error) {
// 对响应错误做点什么 (失败响应) 处理401错误
// console.dir(error)
if (error.response.status === 401) {
console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
// 获取 refresh_token, 判断是否存在, 存在就去刷新token
const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
try {
console.log('存在refreshToken, 需要进行刷新token操作')
// (1) 发送请求, 进行刷新token操作, 获取新的token
// 注意: 这边发请求, 不用http实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
// 这边, 直接用 axios 发送请求
const res = await axios({
method: 'put',
url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
// 请求头中携带refresh_token信息
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
const newToken = res.data.data.token
// (2) 将新token更新到vuex中
store.commit('setTokenInfo', {
refresh_token: refreshToken,
token: newToken
})
// (3) 重新发送刚才的请求, http, 自动携带token (携带的是新token)
// error.config就是之前用于请求的配置对象, 可以直接给http使用
return http(error.config)
} catch {
// refresh_token 过期了, 跳转到登录页
// 清除过期的token对象
store.commit('removeToken')
// 跳转到登录页, 跳转完, 将来跳回来
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
} else {
// 没有refreshToken, 直接去登录, 将来还能跳回来
// router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
// 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
store.commit('removeToken')
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
}
return Promise.reject(error)
})

注意点:

  1. 响应拦截器要加在axios实例 http 上。
  2. 用refresh_token请求新token时,要用axios,不要用实例 http (需要: 手动用 refresh_token 请求)
  3. 得到新token之后,再发请求时,要用 http 实例 (用token请求)
  4. 过期的 token 可以用 refresh_token 再次更新获取新token, 但是过期的 refresh_token 就应该从清除了


作者:JoyZ
来源:juejin.cn/post/7308992811449172005
收起阅读 »

被中文输入法坑死了

web
PM:在PC端做一个@功能吧,就是那种...。 我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。 那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声) 坑1:KeyB...
继续阅读 »

PM:在PC端做一个@功能吧,就是那种...。



我:你不用解释🤔我知道那个功能,监听keydown事件,然后e.keycode === 50,那可太简单了。



那可太简单了,可太简单了,太简单了,简单了,单了,了......(掉进坑里的回声)


坑1:KeyBoardEvent.keycode



废弃的属性你就坚持用吧,一用一个不吱声。以后线上跑得好好的代码突然报错了,你都不知道bug在哪儿。


现在的web标准里,要确定一个键盘事件是靠e.keye.codecode代表触发事件的物理按键,比如2的位置code='Digit2'key返回用户按下的物理按键的值,它还与 shiftKey 等调节性按键的状态、键盘的区域和布局有关。


所以对于@来说,直接判断e.key === "@"来做后续的操作就行了。


addEventListner('keydown', (e) => {
if (e.key === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符的,可监听输入的元素
// 唤起小窗....
}
});


仔细看上面的这几行代码和注释,要开始考(坑)了。


坑2:输入法的坑


起因


在我美滋滋地以为避过了坑1就没事了的时候,一个夜晚我的测试同学告诉我,在测试环境突然就体验不到这个功能了,无论输入多少个@都不行,白天还好好的🤯。


好一个「白天还好好的」。


我自己测试的时候又百分百能体验到🤔,所以最开始我还在怀疑他没有配上测试系统......


于是,让测试同学的windows电脑连到我的开发环境debug一看:


好家伙,真是好家伙😅他的电脑的e.key === "Process"????!!!


什么意思呢,就是正常我们理想中的@字符产生是shift+2按键的组合,监听keydown之后我们会按顺序收到两个回调:



  1. e.key === "Shift"e.code === "ShiftLeft"或者shiftRight

  2. e.key === "@"e.code === "Digit2"


但是实际在测试同学的电脑里,1是一样的,但是2变了,2变成了e.key === "Process"


虽然键盘事件有变化,但是在前端页面上的@字符是没有任何变化的。难怪他说他会突然失效了。我问他做了什么怎么会突然变了,他想了想说晚上从系统输入法换成了微信输入法.....


上网检索(chatGPT)了一番,明白了一个新的知识点:


输入法的全称叫Input Method Editor输入法编辑器(IME)。本质上它也是个编辑器。为了能输入各类字符(比如汉字和阿拉伯字等),IME会先处理用户的英文字母输入然后通过系统的底层调用传递给浏览器,浏览器再显示到用户的界面。这里的Process很大概率就是当时输入法给出的某个值表示那个时刻它还在处理中。


解决办法


既然KeyBoardEvent靠不住,那我们换一种监听方式。


我找到了一个非常适用于输入法的监听事件叫做CompositionEvent,它表示用户间接输入文本(如使用输入法)时发生的事件。此接口的常用事件有compositionstart, compositionupdatecompositionend。它们三个事件分别对应的动作,通俗一点说就是你用输入法开始打字、正在打字和结束打字。


于是乎,我监听compositionend不就行了!在输入法end的时候我再去看你end的字符是不是@不就行了!


// addEventListner('keydown', (e) => {
addEventListner('compositionend', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
e.preventDefalut(); // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于输入法来说,按键的up和down的key值就算不尽人意也没什么损失,毕竟用户毫无感知。但是,compositionend永远是不会错的,如果compositionende.data都不是@字符了,那么在用户的编辑器界面上的显示肯定也会跟着出错。


所以监听这个肯定就是万无一失的方法了,哈哈哈我真是个“天才”(蠢材)。
修改之后让测试同学尝试之后果然就可以了。


坑3:输入法继续坑


起因


时间过去了没一会,本天才就收到了另一个测试同学反馈的问题说为什么输入了一个@字符之后,会出现两个@在界面上?


我第一反应就是难道没有执行到e.preventDefalut()?既然后续功能能正常使用,没执行到也不应该啊🤔。然后在我电脑一通尝试,发现safari浏览器在输入法为中文的情况下也会触发这个问题。


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开二度):


执行到了,也没有报错什么的,但是@字符并没有被prevent掉🤯。


再加上我自己传入的@,所以界面上就出现了两个@字符。啊这这这,这很难评......


ceeb653ely8gzozgvjq1cg20j20hhgvb.gif


我是左思右想,百思不得其解,于是只能:



stack overflow上也有这个问题


上面大概的意思就是compositionend事件里使用 e.preventDefault() 在技术上可行的,但它可能不会产生你期望的效果。可能是因为 compositionend 事件标志着一次输入构成(composition)会话的结束,而在这个点上阻止默认行为可能没有意义,因为输入的流程已经完成了。


更推荐用keydown,compositionstartinput来处理这种情况。


keydown是不可能keydown了,已经被坑了。compositionstart也不行,因为刚开始输入那会才按下了shift键,@字符还没出来呢。那就只能input了。


解决办法


最开始我没有选择input就是因为它不能使用e.preventDefault()。我必须要对输入的字符串进行单独处理,去掉@,当时觉得很麻烦就没有选择这个方法。


额....好好好,行行行,现在还是必须得处理一下了。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
?怎么去处理字符呢 // 这里去掉@字符是为了后续插入和监听方便处理
// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

对于这个处理字符的方法,也是一个新知识点了。起初我还想的是去处理编辑器里的content,然后再给它插入回去,这样子复杂度很高并且出错的概率极大。


这里的解决办法主要是使用CharacterData接口。CharacterData 接口是操作那些包含字符数据的节点的核心,特别是在需要动态地更改文本内容时。


例如,在一个文本节点上使用 deleteData() 方法可以从文本中移除一部分内容,而不必完全替换或重写整个节点


const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
}

写完这个之后,我用自己的safari浏览器测试发现果然没有问题了。


哈哈哈我真是个“天才”(蠢材)。


坑4:输入法深坑🕳️


我自信满满地让测试同学再重试一下😎,然后测试同学说:和之前一样啊,还是有两个@字符。


我:啊?啊啊??啊啊啊???


IMG_6547.jpg


于是,让测试同学的windows电脑连到我的开发环境debug一看(梅开三度):


发现测试同学电脑上的anchorOffset和正常的情况下是不一样的,会小一位,所以导致anchorOffset - 1拿到的前一个字符并不等于@,所以后续也没有把它处理掉🤯。


我是左思右想,百思不得其解,stack overflow上也没有相关的问题。不过,结合IME的概念,肯定还是输入法的问题。


结合之前keydown的e.key==="Processing",可能在input触发时输入法的编辑器其实还是没有完成工作(composition),导致在那个时候SelectionanchorOffset不一致。其实浏览器的Selection肯定不会错,那anchorOffset看起来像是错了,我觉得应该是输入法在转换的过程对我们的前端页面做了一些用户看不到的东西,而anchorOffset把它显化出来罢了。


解决办法


于是乎,我尝试性的对处理字符串的那串代码进行延时,目的是为了等待输入法彻底工作完毕。


// addEventListner('keydown', (e) => {
// addEventListner('compositionend', (e) => {
addEventListner('input', (e) => {
// if (e.key === "@") {
if (e.data === "@") {
setTimeout(() => {
const selection = window.getSelection();
const { anchorNode, anchorOffset } = selection;
if (anchorNode.substringData(anchorOffset - 1, 1) === '@') {
selection.anchorNode.deleteData(anchorOffset - 1, 1);
} // 这里去掉@字符是为了后续插入和监听方便处理
});

// 插入有@字符并且可监听输入的元素
// 唤起小窗....
}
});

然后,问题真的就彻底解决了。


这个功能做起来可太简单了......😅


作者:Liqiuyue
来源:juejin.cn/post/7307041255740981286
收起阅读 »

使用flex实现瀑布流

web
什么是瀑布流 瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。 特点: 固定宽度,高度不一 参差不齐的布局 使用flex实现瀑布流 实现的效果是分成两...
继续阅读 »

什么是瀑布流


瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。


特点:



  • 固定宽度,高度不一

  • 参差不齐的布局


使用flex实现瀑布流


实现的效果是分成两列的瀑布流,往下滑动会加载下一页的数据,并渲染到页面中!


微信图片_20230731231617.jpg


样式的实现


<view class="blessing-con">
<view class='blessing-con-half'>
<view id="leftHalf">
<view class="blessing-con-half-item" :class="bgColor[index % 3]"
v-for="(item, index) in newBlessingWordsList1" :key="index">
<view class="item-con">
</view>
</view>
</view>
</view>
<view class='blessing-con-half'>
<view id="rightHalf">
<view class="blessing-con-half-item" :class="bgColor[(index + 1) % 3]"
v-for="(item, index) in newBlessingWordsList2" :key="index">
<view class="item-con"></view>
</view>
</view>
</view>
</view>
<view class="blessing-more" @click="handlerMore">
<image v-if="hasWallNext" class="more-icon"
src="xx/blessingGame/arr-down.png">
</image>
<view class="blessing-more-text">{{ blessingStatus }}</view>
</view>

.blessing-con 定义外层容器为flex布局,同时设置主轴对齐方式为space-between


.blessing-con-half定义左右两侧的容器的样式


.blessing-con-half-item定义每一个小盒子的样式


.blessing-con {
padding: 32rpx 20rpx;
display: flex;
justify-content: space-between;
height: 1100rpx;
overflow-y: auto;
.blessing-con-half {
width: 320rpx;
height: 100%;
box-sizing: border-box;
.blessing-con-half-item {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin: 0 0 24rpx;
position: relative;
}
}
}

这里每个小盒子的背景色按蓝-黄-红的顺序,同时通过伪类给盒子顶部添加锯齿图片,实现锯齿效果


bgColor: ['blueCol', 'yellowCol', 'pinkCol'], //祝福墙背景

// 不同颜色
.blessing-con-half-item {
&.pinkCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/pink-bg.png');
}
.item-con {
background: #FFE7DF;
}
}

&.yellowCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/orange-bg.png');
}
.item-con {
background: #fff0e0;
}
}

&.blueCol {
&::before {
.setbg(320rpx, 16rpx, 'blessingGame/blue-bg.png');
}
.item-con {
background: #e0f7ff;
}
}
}
}

功能实现


在data中定义两个数组存储左右列表的数据


data(){
return{
blessingWordsList: [],// 祝福墙数据
newBlessingWordsList: [],//已添加的数据
newBlessingWordsList1: [],//左列表
newBlessingWordsList2: [],//右列表
isloading:false,//是否正在加载
hasWallNext:false,//是否有下一页
leftHeight: 0,//左高度
rightHeight: 0,//右高度
blessingWordsCount: 0,//计数器
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}

调取接口请求列表数据



  • 第一次请求数据需要初始化列表数据和计数器

  • 每一次请求完都需要开启定时器


// 获取祝福墙列表(type=1则请求下一页)
async getBlessingWall(type = 0) {
try {
let res = await api.blessingWall({
activityId: this.activityId,
pageNum: this.pageWallNum,
pageSize: this.pageWallSize
})
this.isloading = false
if (res.code == 1 && res.rows) {
let list = res.rows
this.blessingWordsList = (type==0 ? list : [...this.blessingWordsList, ...list])
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
// 处理请求下一页的情况
if (type == 1) {
this.start()
}
this.hasWallNext = res.hasNext
if (!this.hasWallNext) {
this.blessingStatus = "没有更多了哟"
} else {
this.blessingStatus = "点击加载更多"
}
}
} catch (error) {
console.log(error)
}
},
// 加载更多
async handlerMore() {
if (this.hasWallNext && !this.isloading) {
this.isloading = true
this.pageWallNum++
await this.getBlessingWall(1)
}
},

开启一个定时器,用于动态添加左右列表的数据


start() {
// 清除定时器
clearInterval(this.timer)
this.timer = null;

this.timer = setInterval(() => {
let len = this.blessingWordsList.length
if (this.blessingWordsCount < len) {
let isHave = false
// 在列表中获取一个元素
let item =this.blessingWordsList[this.blessingWordsCount]
// 判断新列表中是否已经存在相同元素,防止重复添加
this.newBlessingWordsList.forEach((tmp)=>{
if(tmp.id == item.id){
isHave = true
}
})
// 如果不存在
if (!isHave) {
this.newBlessingWordsList.push(item)//添加该元素
this.$nextTick(() => {
this.getHei(item)//添加元素到左右列表
})
}
} else {
// 遍历完列表中的数据,则清除定时器
clearInterval(this.timer)
this.timer = null;
}
}, 10)
}

计算当前左右容器的高度,判断数据要添加到哪一边



  • 使用uni-app的方法获取左右容器的dom对象,再获取他们当前的高度

  • 比较左右高度,向两个数组动态插入数据

  • 每插入一条数据,计数器+1


getHei(item) {
const query = uni.createSelectorQuery().in(this)
// 左边
query.select('#leftHalf').boundingClientRect(res => {
if (res) {
this.leftHeight = res.height
}
// 右边
const query1 = uni.createSelectorQuery().in(this)
query1.select('#rightHalf').boundingClientRect(dataRight => {
if (dataRight) {
this.rightHeight = dataRight.height != 0 ? dataRight.height : 0
if (this.leftHeight == this.rightHeight || this.leftHeight < this.rightHeight) {
// 相等 || 左边小
this.newBlessingWordsList1.push(item)
} else {
// 右边小
this.newBlessingWordsList2.push(item)
}
}
this.blessingWordsCount++
}).exec()
}).exec()
},

这里有一个注意点,调用start方法的时候,必须确保页面渲染了左右容器的元素,否则会拿不到容器的高度


比如我这个项目是有tab切换的!


微信图片_20230731231616.jpg
进入页面的时候会请求一次数据,这时候因为tab初始状态在0,所以并不会调用start方法,要到切换tab到1时,才会调用start方法开始计算高度。


data(){
return{
isActive: 0, //tab初始化索引
timer:null,//定时器
}
}
async onLoad(options) {
this.getBlessingWall()
}
// tab选项卡切换
tabClick(index) {
this.isActive = index
this.isLoaded = false;
if (this.blessingWordsList.length > 0 && !this.timer && this.isActive == 1) {
if(this.pageWallNum == 1){
this.newBlessingWordsList = []
this.newBlessingWordsList1 = []
this.newBlessingWordsList2 = []
this.blessingWordsCount = 0
}
this.start()
}
},

最后


这次选用了flex实现瀑布流,实现瀑布流的方式还有其他几种方法,后续有机会的话,我会补充其他几种方式,如果感兴趣的话,可以点点关注哦!


作者:藤原豆腐店
来源:juejin.cn/post/7260713996165021754
收起阅读 »

前端同事最讨厌的后端行为,看看你中了没有

web
前端同事最讨厌的后端行为,看看你中了没有 听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。 前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己...
继续阅读 »

前端同事最讨厌的后端行为,看看你中了没有



听说这是前端程序员最讨厌的后端行为,不知道你有没有碰到过,或者你的前端同事虽然没跟你说过,但是你已经被偷偷吐槽了。




前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。



听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,直接编译过了,就发到测试环境了。前端同时联调的时候一调接口,异常了。


好在后来改了,毕竟让人发现自己接口写的有问题,也是一件丢脸的事儿。


但是我还真见过后端的同学,写完接口一个都不测,直接发测试环境的。


我就碰到过厉害的,编译都不过,就直接提代码。以前,有个新来的同事,分了任务就默默的干着,啥也不问,然后他做的功能测试就各种发现问题。说过之后,就改一下,但是基本上还是不测试,本想再给他机会的,所以后来他每次提代码,我都review一下。直到有一天,我发现忍不了了,他把一段全局配置给注释了,然后把代码提了,我过去问他是不是本地调试,忘了取消注释了。他的回答直接让我震惊了,他说:不是的,是因为不注释那段代码,我本地跑步起来,所以肯定是那段代码有问题,所以就注释了。


然后,当晚,他就离职了。


解决方式


对于这种大表单类似的问题,应该怎么处理呢?


好像没有别的方法,只能克服自己的懒惰,为自己写的代码负责。就想着,万一接口有问题,别人可能会怀疑你水平不行,你水平不行,就是你不行啊,程序员怎么能不行呢。


你可以找那么在线 Java BeanJSON的功能,直接帮你生成请求参数,或者现在更可以借助 ChatGPT ,帮你生成请求参数,而且生成的参数可能比你自己瞎填的看上去更合理。


或者,如果是小团队,不拘一格的话,可以让前端的同事把代码提了,你本地跑着自测一下,让前端同事先做别的功能,穿插进行也可以。



前端吐槽:后端修改了字段或返回结构不通知前端



这个就有点不讲武德了。


正常情况下,返回结构和字段都是事先约定好的,一般都是先写接口,做一些 Mock 数据,然后再实现真实的逻辑。


除了约定好返回字段和结构外,还包括接口地址、请求方法、头信息等等,而且一个项目都会有项目接口规范,同一类接口的返回字段可能有很多相同的部分。


后端如果改接口,必须要及时通知前端,这其实应该是正常的开发流程。后端改了接口,不告诉前端,到时候测试出问题了,一般都会先找前端,这不相当于让前端背锅了吗,确实不地道啊。


后端的同学们,谨记啊。



前端吐槽:为了获取一个信息,要先调用好几个接口,可能参数还是相同的



假设在一个详情页面,以前端的角度就是,我获取详情信息,就调用详情接口好了,为什么调用详情接口之前,要调用3、4个其他的接口,你详情里需要啥参数,我直接给你传过去不就好了吗。


在后端看来可能是这样的,我这几个接口之前就写好了,前端拿过去就能用,只不过就是多调几次罢了,没什么大不了的吧。


有些时候,可能确实是必须这么做的,比如页面内容太多,有的部分查询逻辑复杂,比较耗时,这时候需要异步加载,这样搞确实比较好。


但是更多时候其实就是后端犯懒了,不想再写个新接口。除了涉及到性能的问题,大多数逻辑都应该在后端处理,能用一个接口处理完,就不应该让前端多调用第二个接口。


有前端的朋友曾经问过我,他说,他们现在做的系统中有些接口是根据用户身份来展示数据的,但是前端调用登录接口登录系统后,在调用其他接口的时候,除了在 Header 中加入 token 外,还有传很多关于用户信息的很多参数,这样做是不是不合理的。


这肯定不合理,token 本来就是根据用户身份产生的,后端拿到 token 就能获取用户信息,这是常识问题,让前端在接口中再传一遍,既不合理也不安全。


类似的问题还有,比如后端接口返回一堆数据,然后有的部分有用、有的部分没有,有的部分还涉及到逻辑,不借助文档根本就看不明白怎么用,这其实并不合理。


接口应该尽量只包含有用的部分,并且尽可能结构清晰,配合简单的字段说明就能让人明白是怎么回事,是最好的效果。


如果前后端都感觉形势不对了,后端一个接口处理性能跟不上了,前端处理又太麻烦了。这时候就要向上看了,产品设计上可能需要改一改了。


后端的同学可以学一点前端,前端的同学也可以学一点后端,当你都懂一些的时候,就能两方面考虑了,这样做出来的东西可能会更好用一点。总之,前后端相互理解,毕竟都是为了生活嘛。


作者:古时的风筝
来源:juejin.cn/post/7254927062425829413
收起阅读 »

面试官:你能说说常见的前端加密方法吗?

web
前言 本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。 一、哈希函数 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Dige...
继续阅读 »

前言


本篇文章略微介绍一下前端中常见的加密算法。前端中常见的加密算法主要形式包括——哈希函数,对称加密和非对称加密算法。


一、哈希函数


image.png



  • 定义:哈希也叫散列,是指将任意长度的消息映射为固定长度的输出的算法,该输出一般叫做散列值或者哈希值,也叫做摘要(Digest)。简单来说,这种映射就是一种数据压缩,而且散列是不可逆的,也就是无法通过输出还原输入。

  • 特点:不可逆性(单向性)、抗碰撞性(消息不同其散列值也不同)、长度固定

  • 常见应用场景:由于不可逆性,常用于密码存储、数字签名、电子邮件验证、验证下载等方面,更多的是用用在验证数据的完整性方面。



    • 密码存储:明文保存密码是危险的。通常我们把密码哈希加密之后保存,这样即使泄漏了密码,因为是散列后的值,也没有办法推导出密码明文(字典攻击难以破解)。验证的时候,只需要对密码(明文)做同样的散列,对比散列后的输出和保存的密码散列值,就可以验证同一性。

    • 可用于验证下载文件的完整性以及防篡改:比如网站提供安装包的时候,通常也同时提供md5值,这样用户下载之后,可以重算安装包的md5值,如果一致,则证明下载到本地的安装包跟网站提供的安装包是一致的,网络传输过程中没有出错。



  • 优势:不可逆,速度快、存储体积小,可以帮助保护数据的完整性和减轻篡改风险。

  • 缺点:安全性不高、容易受到暴力破解


image.png


常见类型:SHA-512、SHA-256、MD5(MD5生成的散列码是128位)等。



  • MD5(Message Digest Algorithm 5) :是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm) :可以对任意长度的数据运算生成一个固定位数的数值。

  • SHA/MD5对比:SHA在安全性方面优于MD5,并且可以选择多种不同的密钥长度。 但是,由于内存需求更高,运行速度可能会更慢。 不过,MD5因其速度而得到广泛使用,但是由于存在碰撞攻击风险,因此不再推荐使用。


二、对称加密



  • 定义:指加密和解密使用同一种密钥的算法。


image.png



  • 特点:优点是速度快,通信效率高;缺点是安全性相对较低。信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。

  • 优势:效率高,算法简单,系统开销小,速度快,适合大数量级的加解密,安全性中等

  • 缺点:秘钥管理比较难,密钥存在泄漏风险。

  • 常见应用场景:适用于需要高速加密/解密的场景,例如 HTTP 传输的 SSL/TLS 部分,适用于加密大量数据,如文件加密、网络通信加密、数据加密、电子邮件、Web 聊天等。



    • 文件加密:将文件用相同的密钥加密后传输或存储,只有拥有密钥的用户才能解密文件。

    • 数据库加密:对数据库中的敏感信息进行加密保护,防止未经授权的人员访问。

    • 通信加密:将网络数据通过对称加密算法进行加密,确保数据传输的机密性,比较适合大量短消息的加密和解密。

    • 个人硬盘加密:对称加密可以为硬盘加密提供较好的安全性和高处理速度,这对个人电脑而言可能是一个不错的选择。



  • 常见类型DES,3DES,AES 等:



    • DES(Data Encryption Standard):分组式加密算法,以64位为分组对数据加密,加解密使用同一个算法,速度较快,适用于加密大量数据的场合。

    • 3DES(Triple DES):三重数据加密算法,是基于DES,对每个数据块应用三次DES加密算法,强度更高。

    • AES(Advanced Encryption Standard):高级加密标准算法,速度快,安全级别高,目前已被广泛应用,适用于加密大量数据,如文件加密、网络通信加密等。




AES与DES区别

AES与DES之间的主要区别在于加密过程。在DES中,将明文分为两半,然后再进行进一步处理;而在AES中,整个块不进行除法,整个块一起处理以生成密文。相对而言,AES比DES快得多,与DES相比,AES能够在几秒钟内加密大型文件。



  • DES



    • 优点:DES算法具有极高安全性,到目前为止,除了用穷举搜索法对DES算法进行攻击外,还没有发现更有效的办法。

    • 缺点:分组比较短、密钥太短、密码生命周期短、运算速度较慢。



  • AES



    • 优点:运算速度快,对内存的需求非常低,适合于受限环境。分组长度和密钥长度设计灵活, AES标准支持可变分组长度;具有很好的抵抗差分密码分析及线性密码分析的能力。

    • 缺点:目前尚未存在对AES 算法完整版的成功攻击,但已经提出对其简化算法的攻击。




三、非对称加密


-定义:指加密和解密使用不同密钥的算法,通常情况下使用公共密钥进行加密,而私有密钥用于解密数据。公钥和私钥是成对存在,公钥是从私钥中提取产生公开给所有人的,如果使用公钥对数据进行加密,那么只有对应的私钥(不能公开)才能解密,反之亦然。


image.png



  • 特点:缺点是加密解密速度较慢,通信效率较低,优点是安全性高,需要两个不同密钥,信息一对多。因为它使用的是不同的密钥,所以需要耗费更多的计算资源。服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。

  • 优势:秘钥容易管理,不存在密钥的交换问题,安全性好,主要用在数字签名,更适用于区块链技术的点对点之间交易的安全性与可信性。

  • 缺点:加解密的计算量大,比对称加密算法计算复杂,性能消耗高,速度慢,适合小数据量或数据签名

  • 常见应用场景:在实际应用中,非对称加密通常用于需要确保数据完整性和安全性的场合,例如数字证书的颁发、SSL/TLS 协议的加密、数字签名、加密小文件、密钥交换、实现安全的远程通信等。



    • 数字签名:数字签名是为了保证数据的真实性和完整性,通常使用非对称加密实现。发送方使用自己的私钥对数据进行签名,接收方使用发送方的公钥对签名进行验证,如果验证通过,则可以确认数据的来源和完整性。常见的数字签名算法都基于非对称加密,如RSA、DSA等。

    • ** 身份认证**:Web浏览器和服务器使用SSL/TLS技术来进行安全通信,其中就使用了非对称加密技术。Web浏览器在与服务器建立连接时,会对服务器进行身份验证并请求其证书。服务器将其证书发送给浏览器,证书包含服务器的公钥。浏览器使用该公钥来加密随机生成的“对话密钥”,然后将其发送回服务器。服务器使用自己的私钥解密此“对话密钥”,以确保双方之间的会话是安全的。

    • 安全电子邮件:非对称加密可用于电子邮件中,确保邮件内容只能由预期的收件人看到。发件人使用收件人的公钥对邮件进行加密,收件人使用自己的私钥对其进行解密。这确保了只有目标收件人才能读取邮件。



  • 常见类型RSA,DSA,DSS,ECC 等



    • RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的。RSA 是一种非对称加密算法,即加密和解密使用一对不同的密钥,分别称为公钥和私钥。公钥用于加密数据,私钥用于解密数据。RSA 算法的安全性基于大数分解问题,密钥长度通常选择 1024 位、2048 位或更长。RSA 算法用于保护数据的机密性、确保数据的完整性和实现数字签名等功能。

    • DSA(Digital Signature Algorithm) :数字签名算法,仅能用于签名,不能用于加解密。

    • ECC(Elliptic Curves Cryptography) :椭圆曲线密码编码学。

    • DSS:数字签名标准,可用于签名,也可以用于加解密。




总结


前端使用非对称加密原理很简单,平时用的比较多的也是非对称加密,前后端共用一套加密解密算法,前端使用公钥对数据加密,后端使用私钥将数据解密为明文。中间攻击人拿到密文,如果没有私钥的话是没办法破解的。


欢迎大佬继续评论区补充


作者:优秀稳妥的Zn
来源:juejin.cn/post/7280057907055919144
收起阅读 »

你的网站如何接入QQ,微信登录

web
主要实现步骤 对接第三方平台,获取第三方平台的用户信息。 利用该用户信息,完成本应用的注册。 qq登录接入 接入前的配置 qq互联 登录后,点击头像,进行开发者信息填写,等待审核。 邮箱验证后,等待审核。 审核通过后,然后就可以创建应用了。 然后填写...
继续阅读 »

主要实现步骤



  • 对接第三方平台,获取第三方平台的用户信息。

  • 利用该用户信息,完成本应用的注册。


qq登录接入


接入前的配置


qq互联


登录后,点击头像,进行开发者信息填写,等待审核。


image.png


邮箱验证后,等待审核。


image.png


审核通过后,然后就可以创建应用了。


image.png


然后填写一些网站信息,等待审核。审核通过后,即可使用。


开始接入



  1. 导入qq登录的sdk



<script type="text/javascript" charset="utf-8" src="https://connect.qq.com/qc_jssdk.js" data-appid="您应用的appid"
data-redirecturi="qq扫码后的回调地址(上面配置中可以查到)">
script>


  1. 点击qq登录,弹出扫码窗口。


// QQ 登录的 URL
const QQ_LOGIN_URL =
'https://graph.qq.com/oauth2.0/authorize?client_id=您的appid&response_type=token&scope=all&redirect_uri=您的扫码后的回调地址'
window.open(
QQ_LOGIN_URL,
'oauth2Login_10609',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)


  1. 挂起qq登录。需要注意的是,扫码登录成功后,调试代码需要在线上环境。


"qqLoginBtn" v-show="false">

// QQ 登录挂起
onMounted(() => {
QC.Login(
{
btnId: 'qqLoginBtn' //插入按钮的节点id
},
// 登录成功之后的回调,但是需要注意,这个回调只会在《登录回调页面中被执行》
// 登录存在缓存,登录成功一次之后,下次进入会自动重新登录(即:触发该方法,所以我们应该在离开登录页面时,注销登录)
// data就是当前qq的详细信息
(data, opts) => {
console.log('QQ登录成功')
// 1. 注销登录,否则在后续登录中会直接触发该回调
QC.Login.signOut()
// 2. 获取当前用户唯一标识,作为判断用户是否已注册的依据。(来决定是否跳转到注册页面)
const accessToken = /access_token=((.*))&expires_in/.exec(
window.location.hash
)[1]
// 3. 拼接请求对象
const oauthObj = {
nickname: data.nickname,
figureurl_qq_2: data.figureurl_qq_2,
accessToken
}
// 4. 完成跨页面传输 (需要将数据传递到项目页面,而非qq登录弹框页面中进行操作)
brodacast.send(oauthObj)

// 针对于 移动端而言:通过移动端触发 QQ 登录会展示三个页面,原页面、QQ 吊起页面、回调页面。并且移动端一个页面展示整屏内容,且无法直接通过 window.close() 关闭,所以在移动端中,我们需要在当前页面继续进行后续操作。
oauthLogin(LOGIN_TYPE_QQ, oauthObj)
// 5. 在 PC 端下,关闭第三方窗口
window.close()
}
)
})


  1. 跨页面窗口通信


想要实现跨页面信息传输,通常有两种方式:



  • BroadcastChannel:允许 同源 的不同浏览器窗口,Tab页,frame或者 iframe 下的不同文档之间相互通信。但是会存在兼容性问题。

  • localStorage + window.onstorage:通过localStorage 进行 同源 的数据传输。用来处理 BroadcastChannel 不兼容的浏览器。以前写过一篇文章


// brodacast.js
// 频道名
const LOGIN_SUCCESS_CHANNEL = 'LOGIN_SUCCESS_CHANNEL'

// safari@15.3 不支持 BroadcastChannel,所以我们需要对其进行判定使用,在不支持 BroadcastChannel 的浏览器中,使用 localstorage
let broadcastChannel = null
if (window.BroadcastChannel) {
broadcastChannel = new BroadcastChannel(LOGIN_SUCCESS_CHANNEL)
}

/**
* 等待 QQ 登录成功
* 因为 QQ 登录会在一个新的窗口中进行,用户扫码登录成功之后会回调《新窗口的 QC.Login 第二参数 cb》,而不会回调到原页面。
* 所以我们需要在《新窗口中通知到原页面》,所以就需要涉及到 JS 的跨页面通讯,而跨页面通讯指的主要就是《同源页面的通讯》
* 同源页面的通讯方式有很多,我们这里主要介绍:
* 1. BroadcastChannel ->
https://developer.mozilla.org/zh-CN/docs/Web/API/BroadcastChannel

* 2. window.onstorage:注意:该事件不在导致数据变化的当前页面触发
*/

/**
* 等待回调,它将返回一个 promise,并携带对应的数据
*/

const wait = () => {
return new Promise((resolve, reject) => {
if (broadcastChannel) {
// 触发 message 事件时的回调函数
broadcastChannel.onmessage = async (event) => {
// 改变 promise 状态
resolve(event.data)
}
} else {
// 触发 localStorage 的 setItem 事件时回调函数
window.onstorage = (e) => {
// 判断当前的事件名
if (e.key === LOGIN_SUCCESS_CHANNEL) {
// 改变 promise 状态
resolve(JSON.parse(e.newValue))
}
}
}
})
}

/**
* 发送消息。
* broadcastChannel:触发 message
* localStorage:触发 setItem
*/

const send = (data) => {
if (broadcastChannel) {
broadcastChannel.postMessage(data)
} else {
localStorage.setItem(LOGIN_SUCCESS_CHANNEL, JSON.stringify(data))
}
}

/**
* 清除
*/

const clear = () => {
if (broadcastChannel) {
broadcastChannel.close()
broadcastChannel = null
}
localStorage.removeItem(LOGIN_SUCCESS_CHANNEL)
}

export default {
wait,
send,
clear
}


  1. 拿到数据后,进行登录(自己服务器登录接口)操作。



  • 传入对应参数(loginType, accessToken)等参数进行用户注册判断。

  • 通过accessToken判断用户已经注册,那么我们就直接在后台查出用户名和密码直接登录了。

  • 通过accessToken判断用户未注册,那么我们将跳转到注册页面,让其注册。


 // 打开视窗之后开始等待
brodacast.wait().then(async (oauthObj) => {
// 登录成功,关闭通知
brodacast.clear()
// TODO: 执行登录操作
oauthLogin("QQ", oauthObj)
})

// oauthLogin.js
import store from '@/store'
import router from '@/router'
import { message } from '@/libs'
import { LOGIN_TYPE_OAUTH_NO_REGISTER_CODE } from '@/constants'

/**
* 第三方登录统一处理方法
*
@param {*} oauthType 登录方式
*
@param {*} oauthData 第三方数据
*/

export const oauthLogin = async (oauthType, oauthData) => {
const code = await store.dispatch('user/login', {
loginType: oauthType,
...oauthData
})
// 返回 204 表示当前用户未注册,此时给用户一个提示,走注册页面
if (code === LOGIN_TYPE_OAUTH_NO_REGISTER_CODE) {
message('success', `欢迎您 ${oauthData.nickname},请创建您的账号`, 6000)
// 进入注册页面,同时携带当前的第三方数据和注册标记
router.push({
path: '/register',
query: {
reqType: oauthType,
...oauthData
}
})
return
}

// 否则表示用户已注册,直接进入首页
router.push('/')
}

微信扫码登录接入


微信开放平台


登录后,进行对应的应用注册,填写一大堆详细信息,然后进行交钱,就可以使用微信登录了。


image.png


开始接入


整个微信登录流程与QQ登录流程略有不同,分为以下几步:


1.通过 微信登录前置数据获取 接口,获取登录数据(比如 APP ID)。就是后台将一些敏感数据通过接口返回。


2.根据获取到的数据,拼接得到 open url 地址。打开该地址,展示微信登录二维码。移动端微信扫码确定登录。


// 2. 根据获取到的数据,拼接得到 `open url` 地址
window.open(
`https://open.weixin.qq.com/connect/qrconnect?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`,
'',
'height=525,width=585, toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes'
)

3.等待用户扫码后,从当前窗口中解析 window.location.search 得到用户的 code数据。 微信扫码后,会重定向到登录页面。


/**
* 微信登录成功之后的窗口数据解析
*/

if (window.location.search) {
const code = /code=((.*))&state/.exec(window.location.search)[1]
if (code) {
brodacast.send({
code
})
// 关闭回调网页
window.close()
}
}

4.根据 appId、appSecret、code 通过接口获取用户的 access_token


5.根据 access_token 获取用户信息


6.通过用户信息触发 oauthLogin 方法。


调用的接口,都是后端通过微信提供的api来获取到对应的数据,然后再通过接口返回给开发者。  以前也写过微信登录文章


// 等待扫码登录成功通知
brodacast.wait().then(async ({ code }) => {
console.log('微信扫码登录成功')
console.log(code)
// 微信登录成功,关闭通知
brodacast.clear()
// 获取 AccessToken 和 openid
const { access_token, openid } = await getWXLoginToken(
appId,
appSecret,
code
)
// 获取登录用户信息
const { nickname, headimgurl } = await getWXLoginUserInfo(
access_token,
openid
)
console.log(nickname, headimgurl)
// 执行登录操作
oauthLogin(LOGIN_TYPE_WX, {
openid,
nickname,
headimgurl
})
})

需要注意的是,在手机端,普通h5页面是不能使用微信扫码登录的。


总结


相同点



  • 接入前需要配置一些内容信息。

  • 都需要在线上环境进行调试。

  • 都是扫码后在三方窗口中获取对应的信息,发送到当前项目页面进行请求,判断用户是否已经注册,还是未注册。已经注册时,调用login接口时,password直接传递空字符串即可,后端可以通过唯一标识,获取到对应的用户名和密码,直接返回token进行登录。未注册,就跳转到注册页面,让其注册。


不同点



  • qq接入需要导入qc_sdk。

  • qq直接扫码后即可获取到用户信息,就可以直接调用login接口进行判断用户是否注册了。

  • 微信扫码后,获取code来换取access_token, openid,然后再通过access_token, openid来换取用户信息。然后再调用login接口进行判断用户是否注册了。


作者:Spirited_Away
来源:juejin.cn/post/7311343161363234866
收起阅读 »

工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
继续阅读 »

前言



哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



正文



不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




这种写法埋了一个不大不小的雷。




用一段测试代码就可以展示出来问题



1.jpg



打印结果如下:



2.jpg



很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




比如我如果换成2023-12-30又不会有问题了



3.jpg



另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



4.jpg



避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



总结




  1. 日期时间格式统一使用yyyy小写;

  2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人了。



作者:程序员济癫
来源:juejin.cn/post/7269013062677823528
收起阅读 »

HTML问题:如何实现分享URL预览?

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约2100+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 感谢关注微信公众号...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约2100+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


感谢关注微信公众号:“程序员大澈”,然后加入问答群,从此让解决问题的你不再孤单!


1. 需求分析


为了提高用户对页面链接分享的体验,需要对分享链接做一些处理。


以 Telegram(国外某一通讯软件) 为例,当在 Telegram 上分享已做过处理的链接时,它会自动尝试获取链接的预览信息,包括标题、描述和图片。


如此当接收者看到时,可以立即获取到分享链接的一些重要信息。这有助于接收者更好地了解链接的内容,决定是否点击查看详细内容。


图片


2. 实现步骤


2.1 实现前的说明


对于URL分享预览这个功能问题,在项目中挺常用的,只不过今天我们是以一些框架分享API的底层原理角度来讲的。


实现这种功能的关键,是在分享的链接中嵌入适当的元数据信息,应用软件会自动解析,请求分享链接的预览信息,并根据返回的元数据生成预览卡片。


对于国内的应用软件,目前我试过抖音,它可以实现分享和复制粘贴都自动解析,而微信、QQ等只能实现分享的自动解析。


对于国外的应用软件,我只实验过Telegram,它可以实现分享和复制粘贴都自动解析,但我想FacebookTwitterInstagram这些应用应该也都是可以的。


2.2 实现代码


实现URL链接的分享预览,你可以使用 Open Graph协议或 Twitter Cards,然后在 HTML 的 标签中,添加以下 meta 标签来定义链接预览的信息。


使用时,将所有meta全部复制过去,然后根据需求进行自定义即可。


还要注意两点,确保你页面的服务器正确配置了 SSL 证书,以及确保链接的URL有效(即:服务器没有做白名单限制)。


<head>
  
  <meta property="og:title" content="预览标题">
  <meta property="og:description" content="预览描述">
  <meta property="og:image:width" content="图片宽度">
  <meta property="og:image:height" content="图片高度">
  <meta property="og:image" content="预览图片的URL">
  <meta property="og:url" content="链接的URL">
  
  
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="预览标题">
  <meta name="twitter:description" content="预览描述">
  <meta property="twitter:image:width" content="图片宽度">
  <meta property="twitter:image:height" content="图片高度">
  <meta name="twitter:image" content="预览图片的URL">
  <meta name="twitter:url" content="链接的URL">
head>

下面我们做一些概念的整理、总结和学习。


3. 问题详解


3.1 什么是Open Graph协议?


Open Graph协议是一种用于在社交媒体平台上定义和传递网页元数据的协议。它由 Facebook 提出,并得到了其他社交媒体平台的支持和采纳。Open Graph 协议旨在标准化网页上的元数据,使网页在社交媒体上的分享和预览更加一致和可控。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Open Graph 协议可以定义和传递与网页相关的元数据信息,如标题、描述、图片等。这些元数据信息可以被社交媒体平台解析和使用,用于生成链接预览、分享内容和提供更丰富的社交图谱。


使用 Open Graph 协议,网页的所有者可以控制链接在社交媒体上的预览内容,确保链接在分享时显示的标题、描述和图片等信息准确、有吸引力,并能够准确传达链接的主题和内容。这有助于提高链接的点击率、转化率和用户体验。


Open Graph 协议定义了一组标准的 meta 标签属性,如 og:titleog:descriptionog:image 等,用于提供链接预览所需的元数据信息。通过在网页中添加这些 meta 标签并设置相应的属性值,可以实现链接预览在社交媒体平台上的一致展示。


需要注意的是,Open Graph 协议是一种开放的标准,并不限于 Facebook 平台。其他社交媒体平台,如 Twitter、LinkedIn 等,也支持使用 Open Graph 协议定义和传递网页元数据,以实现链接预览的一致性。


图片


3.2 什么是Twitter Cards?


Twitter Cards 是一种由 Twitter 推出的功能,它允许网站所有者在他们的网页上定义和传递特定的元数据,以便在 Twitter 上分享链接时生成更丰富和吸引人的预览卡片。通过使用 Twitter Cards,网页链接在 Twitter 上的分享可以展示标题、描述、图片、链接和其他相关信息,以提供更具吸引力和信息丰富的链接预览。


Twitter Cards 提供了多种类型的卡片,以适应不同类型的内容和需求。以下是 Twitter Cards 的一些常见类型:



  • Summary CardSummary Card 类型的卡片包含一个标题、描述和可选的图片。它适用于分享文章、博客帖子等内容。

  • Summary Card with Large ImageSummary Card with Large Image 类型的卡片与 Summary Card 类型类似,但图片尺寸更大,更突出地展示在卡片上。

  • App CardApp Card 类型的卡片用于分享移动应用程序的信息。它包含应用的名称、图标、描述和下载按钮,以便用户可以直接从预览卡片中下载应用。

  • Player CardPlayer Card 类型的卡片用于分享包含媒体播放器的内容,如音频文件、视频等。它允许在预览卡片上直接播放媒体内容。


通过在网页的 HTML  标签中添加特定的 meta 标签,使用 Twitter Cards 可以定义和传递与链接预览相关的元数据信息,如标题、描述、图片、链接等。这些元数据信息将被 Twitter 解析和使用,用于生成链接预览卡片。


使用 Twitter Cards 可以使链接在 Twitter 上的分享更加吸引人和信息丰富,提高链接的点击率和用户参与度。它为网站所有者提供了更多控制链接在 Twitter 上展示的能力,并提供了一种更好的方式来呈现他们的内容。


图片


图片


作者:程序员大澈
来源:juejin.cn/post/7310112330663231515
收起阅读 »

拼多多算法题,是清华考研真题!

写在前面 在 LeetCode 上有一道"备受争议"的题目。 该题长期作为 拼多多题库中的打榜题 : 据同学们反映,该题还是 清华大学 和 南京大学 考研专业课中的算法题。 其中南京大学的出题人,还真贴心地针对不同解法,划分不同分值: 细翻评论区。 不...
继续阅读 »

写在前面


在 LeetCode 上有一道"备受争议"的题目。


该题长期作为 拼多多题库中的打榜题


出现频率拉满


据同学们反映,该题还是 清华大学南京大学 考研专业课中的算法题。



其中南京大学的出题人,还真贴心地针对不同解法,划分不同分值:




细翻评论区。


不仅是拼多多,该题还在诸如 神州信息滴滴出行 这样的互联网大厂笔试中出现过:





但,这都不是这道题"备受争议"的原因。


这道题最魔幻的地方是:常见解法可做到 O(n)O(n) 时间,O(1)O(1) 空间,而进阶做法最快也只能做到 O(n)O(n) 时间,O(logn)O(\log{n}) 空间


称作"反向进阶"也不为过。


接下来,我将从常规解法的两种理解入手,逐步进阶到考研/笔面中分值更高的进阶做法,帮助大家在这题上做到尽善尽美。


毕竟在一道算法题上做到极致,比背一段大家都会"八股文",在笔面中更显价值。


题目描述


平台:LeetCode


题号:LCR 161 或 53


给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。


子数组是数组中的一个连续部分。


示例 1:


输入:nums = [-2,1,-3,4,-1,2,1,-5,4]

输出:6

解释:连续子数组 [4,-1,2,1] 的和最大,为 6

示例 2:


输入:nums = [1]

输出:1

示例 3:


输入:nums = [5,4,-1,7,8]

输出:23

提示:



  • 1<=nums.length<=1051 <= nums.length <= 10^5

  • 104<=nums[i]<=104-10^4 <= nums[i] <= 10^4


进阶:如果你已经实现复杂度为 O(n)O(n) 的解法,尝试使用更为精妙的分治法求解。


前缀和 or 线性 DP


当要我们求「连续段」区域和的时候,要很自然的想到「前缀和」。


所谓前缀和,是指对原数组“累计和”的描述,通常是指一个与原数组等长的数组。


设前缀和数组为 sumsum 的每一位记录的是从「起始位置」到「当前位置」的元素和。


例如 sum[x]sum[x] 是指原数组中“起始位置”到“位置 x”这一连续段的元素和。


有了前缀和数组 sum,当我们求连续段 [i,j][i, j] 的区域和时,利用「容斥原理」,便可进行快速求解。


通用公式:ans = sum[j] - sum[i - 1]



由于涉及 -1 操作,为减少边界处理,我们可让前缀和数组下标从 11 开始。在进行快速求和时,再根据原数组下标是否从 11 开始,决定是否进行相应的下标偏移。


学习完一维前缀和后,回到本题。


先用 nums 预处理出前缀和数组 sum,然后在遍历子数组右端点 j 的过程中,通过变量 m 动态记录已访问的左端点 i 的前缀和最小值。最终,在所有 sum[j] - m 的取值中选取最大值作为答案。


代码实现上,我们无需明确计算前缀和数组 sum,而是使用变量 s 表示当前累计的前缀和(充当右端点),并利用变量 m 记录已访问的前缀和的最小值(充当左端点)即可。


本题除了将其看作为「前缀和裸题用有限变量进行空间优化」以外,还能以「线性 DP」角度进行理解。


定义 f[i]f[i] 为考虑前 ii 个元素,且第 nums[i]nums[i] 必选的情况下,形成子数组的最大和。


不难发现,仅考虑前 ii 个元素,且 nums[i]nums[i] 必然参与的子数组中。要么是 nums[i]nums[i] 自己一个成为子数组,要么与前面的元素共同组成子数组。


因此,状态转移方程:


f[i]=max(f[i1]+nums[i],nums[i])f[i] = \max(f[i - 1] + nums[i], nums[i])

由于 f[i]f[i] 仅依赖于 f[i1]f[i - 1] 进行转移,可使用有限变量进行优化,因此写出来的代码也是和上述前缀和角度分析的类似。


Java 代码:


class Solution {
public int maxSubArray(int[] nums) {
int s = 0, m = 0, ans = -10010;
for (int x : nums) {
s += x;
ans = Math.max(ans, s - m);
m = Math.min(m, s);
}
return ans;
}
}

C++ 代码:


class Solution {
public:
int maxSubArray(vector<int>& nums) {
int s = 0, m = 0, ans = -10010;
for (int x : nums) {
s += x;
ans = max(ans, s - m);
m = min(m, s);
}
return ans;
}
};

Python 代码:


class Solution:
def maxSubArray(self, nums: List[int]) -> int:
s, m, ans = 0, 0, -10010
for x in nums:
s += x
ans = max(ans, s - m)
m = min(m, s)
return ans

TypeScript 代码:


function maxSubArray(nums: number[]): number {
let s = 0, m = 0, ans = -10010;
for (let x of nums) {
s += x;
ans = Math.max(ans, s - m);
m = Math.min(m, s);
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(1)O(1)


分治


“分治法”的核心思路是将大问题拆分成更小且相似的子问题,通过递归解决这些子问题,最终合并子问题的解来得到原问题的解。


实现分治,关键在于对“递归函数”的设计(入参 & 返回值)。


在涉及数组的分治题中,左右下标 lr 必然会作为函数入参,因为它能用于表示当前所处理的区间,即小问题的范围。


对于本题,仅将最大子数组和(答案)作为返回值并不足够,因为单纯从小区间的解无法直接推导出大区间的解,我们需要一些额外信息来辅助求解。


具体的,我们可以将返回值设计成四元组,分别代表 区间和前缀最大值后缀最大值最大子数组和,用 [sum, lm, rm, max] 表示。


有了完整的函数签名 int[] dfs(int[] nums, int l, int r),考虑如何实现分治:



  1. 根据当前区间 [l,r][l, r] 的长度进行分情况讨论:

    1. l=rl = r,只有一个元素,区间和为 nums[l]nums[l],而 最大子数组和、前缀最大值 和 后缀最大值 由于允许“空数组”,因此均为 max(nums[l],0)\max(nums[l], 0)

    2. 否则,将当前问题划分为两个子问题,通常会划分为两个相同大小的子问题,划分为 [l,mid][l, mid][mid+1,r][mid + 1, r] 两份,递归求解,其中 mid=l+r2mid = \left \lfloor \frac{l + r}2{} \right \rfloor




随后考虑如何用“子问题”的解合并成“原问题”的解:



  1. 合并区间和 (sum): 当前问题的区间和等于左右两个子问题的区间和之和,即 sum = left[0] + right[0]

  2. 合并前缀最大值 (lm): 当前问题的前缀最大值可以是左子问题的前缀最大值,或者左子问题的区间和加上右子问题的前缀最大值。即 lm = max(left[1], left[0] + right[1])

  3. 合并后缀最大值 (rm): 当前问题的后缀最大值可以是右子问题的后缀最大值,或者右子问题的区间和加上左子问题的后缀最大值。即 rm = max(right[2], right[0] + left[2])

  4. 合并最大子数组和 (max): 当前问题的最大子数组和可能出现在左子问题、右子问题,或者跨越左右两个子问题的边界。因此,max 可以通过 max(left[3], right[3], left[2] + right[1]) 来得到。


一些细节:由于我们在计算 lmrmmax 的时候允许数组为空,而答案对子数组的要求是至少包含一个元素。因此对于 nums 全为负数的情况,我们会错误得出最大子数组和为 0 的答案。针对该情况,需特殊处理,遍历一遍 nums,若最大值为负数,直接返回最大值。


Java 代码:


class Solution {
// 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
int[] dfs(int[] nums, int l, int r) {
if (l == r) {
int t = Math.max(nums[l], 0);
return new int[]{nums[l], t, t, t};
}
// 划分成两个子区间,分别求解
int mid = l + r >> 1;
int[] left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
int[] ans = new int[4];
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = Math.max(Math.max(left[3], right[3]), left[2] + right[1]); // 最大子数组和
return ans;
}
public int maxSubArray(int[] nums) {
int m = nums[0];
for (int x : nums) m = Math.max(m, x);
if (m <= 0) return m;
return dfs(nums, 0, nums.length - 1)[3];
}
}

C++ 代码:


class Solution {
public:
// 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
vector<int> dfs(vector<int>& nums, int l, int r) {
if (l == r) {
int t = max(nums[l], 0);
return {nums[l], t, t, t};
}
// 划分成两个子区间,分别求解
int mid = l + r >> 1;
auto left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
vector<int> ans(4);
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = max({left[3], right[3], left[2] + right[1]}); // 最大子数组和
return ans;
}
int maxSubArray(vector<int>& nums) {
int m = nums[0];
for (int x : nums) m = max(m, x);
if (m <= 0) return m;
return dfs(nums, 0, nums.size() - 1)[3];
}
};

Python 代码:


class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def dfs(l, r):
if l == r:
t = max(nums[l], 0)
return [nums[l], t, t, t]
# 划分成两个子区间,分别求解
mid = (l + r) // 2
left, right = dfs(l, mid), dfs(mid + 1, r)
# 组合左右子区间的信息,得到当前区间的信息
ans = [0] * 4
ans[0] = left[0] + right[0] # 当前区间和
ans[1] = max(left[1], left[0] + right[1]) # 当前区间前缀最大值
ans[2] = max(right[2], right[0] + left[2]) # 当前区间后缀最大值
ans[3] = max(left[3], right[3], left[2] + right[1]) # 最大子数组和
return ans

m = max(nums)
if m <= 0:
return m
return dfs(0, len(nums) - 1)[3]

TypeScript 代码:


function maxSubArray(nums: number[]): number {
const dfs = function (l: number, r: number): number[] {
if (l == r) {
const t = Math.max(nums[l], 0);
return [nums[l], t, t, t];
}
// 划分成两个子区间,分别求解
const mid = (l + r) >> 1;
const left = dfs(l, mid), right = dfs(mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
const ans = Array(4).fill(0);
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = Math.max(left[3], right[3], left[2] + right[1]); // 最大子数组和
return ans;
}

const m = Math.max(...nums);
if (m <= 0) return m;
return dfs(0, nums.length - 1)[3];
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:递归需要函数栈空间,算法每次将当前数组一分为二,进行递归处理,递归层数为 logn\log{n},即函数栈最多有 logn\log{n} 个函数栈帧,复杂度为 O(logn)O(\log{n})


总结


虽然,这道题的进阶做法相比常规做法,在时空复杂度上没有优势。


但进阶做法的分治法更具有 进一步拓展 的价值,容易展开为支持「区间修改,区间查询」的高级数据结构 - 线段树。


实际上,上述的进阶「分治法」就是线段树的"建树"过程。


这也是为什么「分治法」在名校考研课中分值更大,在大厂笔面中属于必选解法的原因,希望大家重点掌握。


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7310104657211293723
收起阅读 »

大厂是怎么封装api层的?ts,axios 基于网易公开课

web
先看一下使用方法 先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。 挺香的。 上核心代码 代码一:utils/request/getrequest.ts import axios, { type Axi...
继续阅读 »

先看一下使用方法
请求封装2.png


先声明,这套写法是我在一次网易公开课学来的,说是大厂内部是怎么封装请求的,本人只是给它改成ts版本。
挺香的。


上核心代码


WX20231124-143933@2x.png


代码一:utils/request/getrequest.ts



import axios, { type AxiosRequestConfig, type CancelTokenSource } from "axios";
import { manualStopProgram } from '@/utils/index';

import server from "./server";
import type { RequestConfig, ApiRouter, ServerRes } from './server.types'

class Requestextends keyof ApiRouter = keyof ApiRouter> {
requestRouter: ApiRouter = {} as ApiRouter
requestTimes = 0;
requestMap: Record<string, CancelTokenSource> = {};
toLogined = false

/**
*
@feat <注册请求路由>
*/

parseRouter(routerName: T, defaultAxiosConfigMap: Record) {
const apiModule = this.requestRouter[routerName] = {} as ApiRouter[T]

Object.entries(defaultAxiosConfigMap).forEach((item) => {
type ApiName = keyof ApiRouter[T]

const [apiName, defaultRequestConfig] = item as [ApiName, RequestConfig];

const request = this.sendMes.bind(
this,
routerName,
apiName,
defaultRequestConfig,
);

apiModule[apiName] = request as ApiRouter[T][ApiName]
apiModule[apiName].state = "ready";
});
}
async sendMes<ApiName extends keyof ApiRouter[T] = keyof ApiRouter[T]>(
routerName: T,
apiName: ApiName,
defaultRequestConfig: RequestConfig,
requestParams: Record<string, any>,
otherAxiosConfig?: RequestConfig
): Promise<ServerRes> {
this.requestTimes += 1;

return new Promise(async (resolve, reject) => {
try {
const selfMe = this.requestRouter[routerName][apiName];
const requstConfig: RequestConfig = {
...defaultRequestConfig,
...otherAxiosConfig,
data: requestParams,
};

/**
*
@feat <取消上一个同url请求>
*
@remarks [注:
* 个别页面需要同个api地址,多次请求,请传uniKey 作为区分,
* 不然有概率出现上一个请求被干掉
* ]
*/

if (selfMe.state === 'pending') this.cancelLastSameUrlRequest(requstConfig);

// 保险方案,传了 uniKey 才取消请求
// if (selfMe.state === 'pending' && requstConfig.uniKey) this.cancelLastSameUrlRequest(requstConfig);

const successCb = (res: ServerRes) => {
const ret = this.responseHandle(res, requstConfig)
resolve(ret);
};
const failCb = (error: unknown) => {
console.error("接口报错: " + requstConfig.url, error);
// 处理错误逻辑
throw error;
};
const complete = () => {
selfMe.state = "ready";
this.requestTimes -= 1;

if (this.requestTimes === 0) {
this.toLogined = false;
}
};

selfMe.state = "pending";
requstConfig.cancelToken = this.axiosSourceHandle(requstConfig).token;

await server(requstConfig).then(successCb).catch(failCb).finally(complete);

} catch (error) {
reject(error);
}
})
}

responseHandle(res: ServerRes, config: RequestConfig) {
const { code } = res;
console.warn(`请求返回: ${config.url}`, res);

if (code === 405) throw String("405 检查请求方式");
if (code === 401) this.toLogin();
if (code !== 200) throw String(res.message);

return res;
}

toLogin() {
if (this.toLogined) return;
throw String("请先登录");
}

generateReqKey(requestConfig: RequestConfig) {
return `${requestConfig.url}__${requestConfig.uniKey || ''}`
}
axiosSourceHandle(requestConfig: RequestConfig) {
const cancelToken = axios.CancelToken;
const source = cancelToken.source();

const reqKey = this.generateReqKey(requestConfig);
this.requestMap[reqKey] = source;

return source;
}
// 处理取消上一个请求
cancelLastSameUrlRequest(requestConfig: RequestConfig) {
const reqKey = this.generateReqKey(requestConfig);
const currentReqKey = this.requestMap[reqKey];

currentReqKey.cancel(`${manualStopProgram} reqKey: ${reqKey}`); // manualStopProgram 是一个标识,让外面的提示框忽略报错
}
}


export default new Request();



代码二:utils/request/server.ts


import axios from "axios";
import { UserInfo } from "@/utils/index";
import type { RequestConfig, ServerRes } from "./server.types";

export default async function server(
axiosRequestConfig: RequestConfig
): Promise<ServerRes> {
const token = UserInfo.getToken() || "";
const reqData = (() => {
const data = axiosRequestConfig.data;
const isFormData = data instanceof FormData;

if (isFormData) {
data.
append("token", token);
return data;
}
return {
...data,
token
};
}
)();

const { data: resBody, status } = await axios({
...axiosRequestConfig,
withCredentials: true,
data: reqData
}
).
catch((err) => {
const errMsg = err && typeof err === "object" && err !== null && "message" in err
if (errMsg) throw err.message;
throw err;
}
);

return resBody;
}

export {
server
}

import type { AxiosRequestConfig } from "axios";
import type { Api } from "@/apis/index";

export type RequestConfigany> = AxiosRequestConfig & {
uniKey?: string | number;
};

export type ApiConfig = {
params: T;
return: K;
};

export type List_pagiantion = {
page: number;
page_size: number;
};

// 这里有点绕,把各个api的参数和返回值 合成一个个特定的函数
export type ApiRouter = {
[K in keyof Api]: {
[T in keyof Api[K]]: Api[K][T] extends ApiConfig<any, any>
? {
(params: Api[K][T]["params"], otherRequestConfig?: RequestConfig): Promise<{
message: string;
code: number;
data: Api[K][T]["return"];
}>;
state?: 'pending' | 'ready'
}
: never;
};
}

export type ApiRouter__requestConfig = {
[K in keyof Api]: {
[T in keyof Api[K]]: RequestConfig;
};
}
export type ServerRes = {
code: number,
message: string,
data: any
};

接下来是apis文件夹(即开头的那个图片),在这里配置接口信息,日常业务代码在这里写


接口.png


代码一 写api配置 :src/apis/modules/admin-admin/index.ts


import type { ApiRouter__requestConfig } from "@/utils/modules/request/server.types.d";

const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {
getList: {
method: "post",
url: "/admin/admin/getList"
},
};

export default indexAdmin;

代码二 写接口类型声明 :src/apis/modules/admin-admin/index.types.d.ts


import type { ApiConfig } from "@/utils/modules/request/server.types.d";

export type AdminAdmin = {
getList: ApiConfig<
{
page: number,
page_size: number,
phone?: string,
status?: ManagerStatus,
groups_id?: number
},
{
count: number,
list: {
id: number,
phone: string,
groups_id: number,
create_at: string,
status: number,
status_txt: string,
groups_txt: string
}[]
}
>,
};


代码三 注册路由 :src/apis/index.ts


import { request } from "@/utils/index";
import indexAdmin from "./modules/index-admin/index";

request.parseRouter("indexAdmin", indexAdmin);

export type Api = {
indexAdmin: IndexAdmin;
}

// 这个是另一个作用,到处配置项,配合接口做权限控制,下面在说
export function getApiConfigMap() {
return {
indexAdmin,
};
}

下面说说这个封装方式好在哪里:


1. 解决的痛点:


以往我们看到最常见的封装方式,就这种

export function Api1() {
return axios.get('xx')
}

export function Api2() {
return axios.get('xx')
}

export function Api3() {
return axios.get('xx')
}

export function Api4() {
return axios.get('xx')
}

这种就非常麻木,一直写函数,每一个都要写配置项,没有数据结构结构=(无法复用)。
如果换成上面的 const indexAdmin: ApiRouter__requestConfig["adminAdmin"] = {},这种写法,就有数据结构了,有了结构之后就可以进行组合复用
比如上面提到的 getApiConfigMap 可以把数据结构直接导出,配合接口做按钮级权限控制,
接口会返回一份配置项{authen1: '/admin/admin/getList'}。
我们二者一比对,就能判断出是否有权限了。


比如看下面代码
PermissionWrapper 是一个权限容器组件 hasPermission=true就显示按钮
store.state.myPermission?.enterpriseAlarm?.edit 是用 getApiConfigMap 和结构权限表配合生成的


        <PermissionWrapper
hasPermission={store.
state.myPermission?.enterpriseAlarm?.edit}
>

<el-button type="primary" size="small" text onClick={openEditDialog}>
编辑
el-button>

PermissionWrapper>

ts直接提示,写起来很舒服,快准狠
ts提示.png


2. 请求函数封闭又开放


经过上面的 parseRouter 注册路由之后,sendMes 生成了N个请求函数,独一无二的函数,里面的fail success 可以做的事情很多,不如限制登录,取消上一个请求等等。大家有啥想法欢迎评论区写出来,我们一起优化它。


sendMes 最后一个参数,保持了开放性,在调用的时候我们传入uniKey就可以取消上一个请求了,还有一些特殊的参数,随便造。


3. 方便提取Api类型的参数和返回值类型(这是我额外拓展的)


我们经常会需要把参数和返回值的类型拿出来到页面上使用,这时候,就可以通过下面这个XX全局声明拿到。


declare namespace XX {
export type PromiseParams = T extends Promise ? T : never;

/**
*
@feat < 提取 api 参数 >
*
@describe <
type GetListParams = XX.ExtractApiParams

* >
*/

export type ExtractApiParams<Function_type> = Function_type extends (e: infer U) => any
? Excludeundefined>
: never;

/**
*
@feat < 提取 api 返回值 >
*
@describe <
type GetListReturn = XX.ExtractApiReturnType

* >
*/

export type ExtractApiReturnType<Function_type> = PromiseParams<
ReturnType<Function_type>
>["data"];

/**
*
@feat < 提取 api 为分页的 list item>
*
@describe <
type TableRow = XX.ExtractPromiseListItem

* >
*/

export type ExtractPromiseListItem<Function_type> =
ExtractApiReturnType<Function_type>["list"][number];
}

下面是一些使用方法举例
image.png


image.png


image.png


image.png


用上面的写法很方便就能在页面中把具体的类型拿出来。做到一次类型声明,到处使用。


api封装是一个长期的话题,axios很好用,但其实它就是一个请求方法而已。相信大家也见过很多乱七八糟的写法。特别是一些老项目,想新增api都不知道放在哪个文件夹。


很幸运无意中看到网易公开课的老师们讲解,那时候他们写的是js版本,看到这种由配置对象直接生成api函数的做法瞬间眼前一亮,这不就是我一直在找的封装方式,满足了我所有的想象。感谢感谢


后来我花了点时间,让它变成ts版本,还封装XX这个全局声明,让它彻底好用起来。希望这个封装能让大家受益。


细心的读者可能会发现上面的代码,一直抛错误,但是却没有拦截提示。 这是笔者推崇的报错终止程序,而不是用return的方式。(js终止程序,我常用throw 替代 return


如果您有什么好的建议或想法,欢迎评论区留言。有用请点点赞,还有更多经验总结在路上。
嘴下留情,骂我倒无所谓,重要的是别把评论区搞得乌烟瘴气


原课程链接:js es5版本,有兴趣的可以看看。但我觉得它那个取消请求,得再升级一下不然万一同个页面调用同个接口2次,就会取消第一个请求了。所以我加装了uniKey作为标识。
live.study.163.com/live/index.…


作者:闸蟹
来源:juejin.cn/post/7304594468157849640
收起阅读 »

前端学一点Docker,不信你学不会

虽然前端很少跟docker打交道,但随着工作流程的自动化现代化,docker正变得越来越重要。无论你是希望扩展技能到全栈领域,还是想要炫技,掌握Docker基本知识都是前端小伙伴重要的一步。 什么是Docker Docker 是一个开源的应用容器引擎,可以让...
继续阅读 »

虽然前端很少跟docker打交道,但随着工作流程的自动化现代化,docker正变得越来越重要。无论你是希望扩展技能到全栈领域,还是想要炫技,掌握Docker基本知识都是前端小伙伴重要的一步。


什么是Docker



Docker 是一个开源的应用容器引擎,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。



我们知道,软件的安装是要区分系统环境的;即使是运行环境相同,依赖的版本一旦有所出入,也容易出现“我这里明明好使,你那里为啥不行“的问题。容器技术解决了环境一致性依赖管理的问题。


因此,容器是我们的项目的运行环境,而docker是容器的运行环境与管理平台。


关键词


镜像 (Image)


镜像是构建容器的模板,可以简单理解为类似Js中的class类或构造函数。


镜像中详细记录了应用所需的运行环境、应用代码、如何操作系统文件等等信息;


容器 (Container)


容器是镜像的运行实例。可以简单理解为”new 镜像()“的实例,通过docker命令可以任意创建容器。


当前操作系统(宿主机)与容器之间的关系,可以参照浏览器页面与iframe之间的关系。容器可以拥有独立的IP地址,网络,文件系统以及指令等;容器与容器、容器与”宿主机“之间以隔离的方式运行,每个容器中通常运行着一个(或多个)应用。


仓库 (Registry)


仓库是集中管理镜像的地方。类似于npm平台与npm包之间的关系。


如果我们将搭建项目环境的详细过程以及具体的依赖记录进镜像中,每当需要部署新服务时,就可以很容易的通过镜像,创建出一个个完整的项目运行环境,完成部署。


示例——安装启动Mysql


1. 安装Docker


具体过程可参考菜鸟教程,下面以macOS系统作为例子进行演示。


启动docker客户端如下:



打开系统终端(下面是在vscode的终端中演示),输入命令:


docker -v

效果如下:



说明docker已经安装并启动。


2. 下载Mysql镜像


下载镜像有点类似于安装npm包:npm install <包名>,这里输入docker镜像的安装命令:docker pull mysql来下载安装mysql的镜像:



安装结束后,输入镜像列表的查看命令:docker images



当然,通过docker的客户端App也可以看到:



3. 创建mysql镜像的容器,启动Mysql


输入启动容器命令:


docker run -d -p 3308:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql

先来看下启动结果,下面的一堆数字是完整的容器id:



输入的这一串命令是什么意思?



  • docker run: 这是启动新容器的命令。

  • -d--detach 是使mysql服务在后台运行,而不是占用当前终端界面。

  • -p 3308:3306: 这是端口映射参数:

    • 创建容器会默认创建一个子网,与宿主机所处的网络互相隔离;mysql服务默认端口3306,如果要通过宿主机所在网络访问容器所处的子网络中的服务,就需要进行端口映射(不熟悉网络的可以看下《如何跟小白解释网络》)。

    • 宿主机的端口在前(左边),容器的端口在后(右边)。



  • -e MYSQL_ROOT_PASSWORD=123456: 设置环境变量 MYSQL_ROOT_PASSWORD=123456;也就是将mysql服务的root用户的密码为123456

  • mysql: 这是上面刚刚pull的镜像的名称。


通过上面的命令,我们启动了一个mysql镜像的容器,并将主机的3308端口映射到了容器所在子网中ip的3306端口,这样我们就可以通过访问主机的localhost:3308来访问容器中的mysql服务了。


4.访问Mysql服务


下面写一段nodeJs代码:


// mysql.js
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
port: '3308',
user: 'root',
password: '123456',
database: '',
});
connection.connect();
// 显示全部数据库
connection.query('show databases;', function (err, rows, fields) {
if (err) {
console.log('[SELECT ERROR] - ', err.message);
return;
}
console.log('--------------------------SELECT----------------------------');
console.log(rows);
console.log('------------------------------------------------------------');
});

这里调用了nodeJs的mysql包,访问localhost:3308,用户名为root,密码为123456,运行结果下:


$ node mysql.js;
[SELECT ERROR] - ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication
protocol requested by server; consider upgrading MySQL client

这里报错了,原因是mysql服务的认证协议与我们代码中的不同导致的。这里我们需要对mysql服务进行一些修改。


为此,我们需要进入mysql容器,对mysql进行一些直接的命令行操作。


5.进入容器


首先,我们需要知道容器的id,输入容器查看命令:docker ps,展示容器列表如下:



其中55cbcc600353就是我们需要的容器的短id,然后执行命令:docker exec -it 55cbcc600353 bash,以下是命令的解析:



  • docker exec:用于向运行中的容器发布命令。

  • -it:分配一个终端(伪终端),允许用shell命令进行交互。也就是将容器中的终端界面映射到宿主机终端界面下,从而对容器进行直接的命令行操作。

  • 55cbcc600353:容器ID或容器名称。

  • bash:这是要在容器内执行的命令。这里是启动了容器的Bash shell程序。


运行结果如下:



我们看到bash-4.4#  后闪烁的光标。这就是容器的bash shell命令提示符,这里输入的shell命令将会在容器环境中执行。


我们输入mysql登录命令,以root用户身份登录:mysql -uroot -p123456



成功登录mysql后,在mysql>命令提示符下输入:ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password By '123456';


这条命令用来修改’root’用户的认证方式为mysql_native_password ,将密码设置为123456,并允许来自任何主机(‘%’)的连接。


输入exit;命令退出mysql>命令提示符:



再按下:ctl+D退出容器终端,回到宿主机系统终端下。再次运行上面的js代码,效果如下:



这样我们就完成了本地mysql服务的部署。


结束


通过上面的简介以及安装部署mysql服务的例子,相信不了解docker的前端小伙伴已经有了一些概念;感兴趣的小伙伴可以继续深入,学习相关的知识。


作者:硬毛巾
来源:juejin.cn/post/7304538094782808105
收起阅读 »

社会现实告诉我,00后整顿职场就是个笑话

00后整顿职场,也算是我之前的关键词吧。 我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。 甚至还能在即将被开除的时候,反将一军把老板开除。 而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。 也...
继续阅读 »

00后整顿职场,也算是我之前的关键词吧。


我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。


甚至还能在即将被开除的时候,反将一军把老板开除。


而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。


也真正意义上让我感受到了,00后整顿职场,就是一个互联网笑话罢了。


1、职场宫斗,成功上位


我之前在苏州工作,堪称工作中的宫斗,并且在这场宫斗大戏中胜出,将原有的项目负责人开除,成功上位。


而这个项目存在的问题非常多,我就在六月被派遣去项目的总部合肥进行学习,等到打通项目的全部链路后,再回到苏州。


届时我将以这个项目的负责人,重新搭建团队,开展这个项目。所以我在合肥那边,以员工的身份深入各个工作组进行学习。


在市场部,运营部的办公大厅工作过,也在各部门的独立办公室工作过。


我感觉自己像个间谍,一边在以平级的打工人身份和我的同事们相处,一边又以苏州负责人的身份,参与那些领导才能参与的内部会议。


2、内心变化的开端


我在合肥总部工作中,接触了很多躺平摆烂的同事,但这个“躺平摆烂“要加上双引号。


他们是00后,90后,甚至有85后。如果放在三个月前,我可以不假思索地说,他们全都是我最讨厌的人。他们如同牛羊一般任人宰割,上级让加班,他们就加班,有时候加班甚至超过四五个小时也没有怨言。


我甚至从来没听他们感慨过为什么没有加班费。亲眼看着他们被自己的上级用一些与工作无关的鸡毛蒜皮之事骂得狗血淋头,但他们也只会在被骂完之后,背地里吐槽那个领导估计是在家被老婆骂了,才来拿他们泄愤。


我打听了他们的工资,只能说中规中矩,起码不是能让人当牛做马的数字。偶尔我见到一两个有骨气的人,觉得拿这么点钱就应该干这么点事。干不爽就马上离职,但马上就会有下一个人替补他的位置,形成闭环。


我惊讶于怎么有人能惹到这个地步,但后来和他们日渐熟落,我们一起吃饭,一起打游戏,一起下班顺路回家,还参加了他们的生日聚会。我发现他们活得其实真的很洒脱。一切都是随遇而安,下班时间一到,他们就真的可以无忧无虑。


因为他们有一份工资还行的工作,养活自己。他们没有啃老,也没有用卑鄙的手段,去抢想要努力的人应该分到的蛋糕,也压根不去想要赚很多钱,因为没有什么需要太高消费的需求。


加上现在的环境,找到一份可观收入的工作确实很难。所以公司偶尔的加班,领导偶尔的泄愤,这些毕竟还是少数时候的偶尔,也都没有超过他们的心理承受阈值,那也就得过且过了。


所以我们其实都一样,只是个普通人罢了。而像我们这样的普通人,取之不尽,用之不竭。这到底是好事还是坏事呢?


3、复杂的职场生态环境


建立在这个基础上,视觉转换到高层领导们这里。他们当着我的面说,这样的人就是个底层打工仔,缺人就招,加班照旧,心情不好还要扣他们的全勤绩效。


压根就不怕这些底层打工仔闹事,纵使有一两个所谓的决心者辞职,也能在很快时间找到下一位。


两者形成互补,共同铸就了这样恶劣的职场生态环境。但我说职场无法改变,远不止这么一点原因。


在这个项目中,我说好听一些只能算是项目负责人,在此之上还有着项目股东,这还要细分成大股东和小股东。而我所在的项目属于互联网赛道,也就是说需要一些新鲜事物的眼光和思维来对待。


但这些股东们经常提出一些奇怪的意见,就如同用微商时代的卖货思维,来指点直播带货,并且他们是出钱的股东,他们提出的战略方针不容我驳回,因为在他们的光辉历史中,有大量的成功案例,来佐证他们的思路是对的。


我刚开始觉得也有道理。他们能有钱投资,肯定是有什么过人的本领能让他们赚到钱,但是随着相处下来,我发现不过是他们本身家里条件就优越,在九几年就能拿出一百万给他们创业。


他们把这一百万分散到二十个领域,每个投资五万总能撞上那么一两个风口,让他们实现钱生钱。


九几年的五万也算是一笔不少的投资。他们这样的发财经历,让我很难不产生质疑,这不是给我我也行吗?


毕竟他们如果真的有什么过人的本领和远见,也不至于在每次内部开会之前,都要组织喊这样的口号:“好,很好,非常好,越来越好“


甚至试图把这样的口号,带到每一次迎接客户的项目介绍会上。我以自曝式要挟制止他们这个行为,我说如果你们这么干,那我当天就辞职,内部都是自己人,我可以陪你们这样弄,但如果对外这么搞,被录下来说我们是传销,我都不知道怎么辩解。


4、职场中的背锅人


他们就是这样坚信着自己能成功,是因为自己有过人的才华。所以自我洗脑着自己提出的方向没有错。如果出错了,亏损了,那一定是负责人的问题。


但好巧不巧,我就是那个负责人。我已经无数次告诉他们,我们这个项目压根就不需要穿黑丝短裙跳舞的小姐姐。


我甚至写了一篇报告给他们,分析我们的项目为什么不能用擦边这种手段引流。但他们执意要,说这样来流量快,我都有点分不清到底是他们自己想看,还是深信这样做确实是可行。


但如果最后这样还是没成功,导致项目亏损,大概率还是在我身上找原因吧。


面对他们这样的大佬,我心里很清楚,这已经远远不是宫斗了,这也绝对不是靠几个心计,或者有实力撑腰就能取胜上位了。这场权力的游戏,不是我等草民玩得起的。


5、换个思路,创造属于自己的职场


一边是被提供资金,但是瞎指挥的股东们摧残,一边是在有限的预算下,我作为负责人,确实很难做到尊重打工人的内心挣扎,回到苏州我虽然能身居高位,但我终将成为我曾经最鄙视的人。


我不要当这个背锅侠,我也不想在这个环境中,去逐渐接受这样的价值观。


这样看来确实如此,00后整顿职场不过是一场互联网的狂欢罢了。


这个题材的故事,也永远只能发生在职场的最底层。由一群家境优越,体验生活的公子哥和我这种不知好歹的普通人共同出演。


大部分人只是在手机屏幕前把我们当个乐子,成了扣个666,然后一起吃胜利的果实。没成,那就确实是看了个乐子。


或许是因为他们心里也清楚,凭我们压根就做不到。


00后现在确实整顿不了职场,因为社会的资源和命脉还不掌握在00后手上。


但就止步于此了吗?我曾说过我想有一个自己的小工作室,遵守劳动法,双休,按时发工资,交纳五险一金。


是的,换个思路,也许00后不需要整顿职场,而是直接创造属于自己的职场,那么接下来我就要向着这个目标去努力了,毕竟二十年后我也还是00后,不如到时候再来说00后整顿职场吧。


作者:程序员Winn
来源:juejin.cn/post/7311603432929984552
收起阅读 »

我在酷家乐这 4 年,项目成败与反思

引言 2023-12-06 是我在酷家乐的最后一天,想把我在酷家乐这 4 年主导落地的项目做个总结,聊聊每个项目的创立背景、结果成败,以及反思。为了防止业务敏感信息泄漏,文中不会涉及到任何业务情况,项目结果数据,项目截图等内容。 19 相遇 时间来到 19 年...
继续阅读 »

引言


2023-12-06 是我在酷家乐的最后一天,想把我在酷家乐这 4 年主导落地的项目做个总结,聊聊每个项目的创立背景、结果成败,以及反思。
为了防止业务敏感信息泄漏,文中不会涉及到任何业务情况,项目结果数据,项目截图等内容。


19 相遇


时间来到 19 年 8 月,那是我加入酷家乐的日子。作为 IC 投入到酷家乐用户增长团队,当时团队主要在做激励体系、积分抽奖、酷签到、勋章等 To C 促活业务。


新业务


19 年 10 月,用户平台线成立 “设计圈” 新项目,是个 To B 的 SaaS 业务,目的是打通企业内部设计孤岛,让企业内部设计师共享、共建、共成长。后被大家戏称 “小主站”,即主站的子功能 SaaS 化。
过程中我统一所有前台页面启动逻辑,增加启动的中间件机制,中间件机制也是首次被引入到页面启动流程中,对于多页、统一的场景至关重要;对于管理后台,引入当时比较前沿的 UForm(即现在的 Formily),并进行业务定制的封装,目的是简化表单、表格等场景的开发工作,而此动作也提效明显。在此感谢阿里对开源的贡献。
反思:



  1. 多页应用,需要有入口做全局逻辑的管控。而落地做法很多:html 入口/JS 统一使用固定的 boot 逻辑等等

  2. 垂直领域能做一定的技术轮子。例如:表单表格的管理后台场景,需要有垂直领域的组件来做提效,基础组件还不够

  3. 过程中担任 SM ,反推自己以全局视角考虑问题,并且关注团队成员的任务与过程


20 回归


20 年 2 月一半精力回归用户增长团队,直到 6 月份完全回归。


小程序平台


20 年 1 月,公司内部小程序业务增多,需要做一定的基础设施,以提升整体的开发效率。前端团队老大,推动成立“小程序平台”专项虚拟小组,由几个小程序的业务团队同学(设计圈也有小程序业务),以及基架组转岗过来的同学组成。
我主动负责其中的 CI/CD 部分,接入 Def(公司前端统一 CLI 工具),完成套件、构建、部署等功能。当时微信小程序还不支持 CLI 部署,只能借助 “微信小程序开发者” 工具,在 Windows/Mac 上使用,而公司已有工程化 Linux 相关的基建,完全用不了,故在 Windows 虚拟机上安装“微信小程序开发者”工具,并且本地启动 proxy server 与开发者工具互通,CLI 再调用 Windows 本地的 proxy server 完成互通。
反思:



  1. 微信开发者工具客户端等以 UI 的形式提供给使用方,对于小团队很友好,对于想集成到大团队自有工作流的系统中,很差(好在微信现在已提供 SDK 跨平台发布,以便于集成到现有系统中)

  2. 我只参与了“小程序平台”不到半年,随后平台越发庞大:包括微信公众号管理、用户管理,甚至域名管理、人员管理、运维中心、营销工具等微信本身已经提供的能力包装。投入了非常多的人力,但是我个人认为过于超前。主要原因是:

    1. 酷家乐本身各业务小程序并没有太多增长

    2. 边缘功能太多,绝大多数场景根本用不到。我理解仅需要这些核心能力:模板分发多商家小程序、CI/CD、组件库、脚手架。



  3. 基建不应太过超前,优先满足最核心、提效最高的能力。


TMS


对于 To C 的产品,用户增长当时主要靠运营驱动,借助一些营销获客、促留存的手段,而产品需求绝大部分来自于运营同学。而面向运营同学的工具,有 2 款:



  1. TMS:仿淘宝店铺装修的页面搭建平台,主要可以完成产品介绍页、营销页等功能的搭建;

  2. 云台:运营平台,面向 To C 用户的营销推送(短信、公众号、邮箱、站内信等场景)、广告位管理等核心应用场景。JS 全栈开发,包括对 MySQL、Redis 等持久层的直接调用。


对于 TMS 页面搭建平台,有个极大地痛点:所有的模块(前端开发的定制模块)很难开发与发布,所有模块都杂糅在一个 NPM 包内,所以开发一个模块的流程是这样:



  1. TMS 管理后台创建一个模块,就是元信息了拿到模块 ID

  2. Mod Package 里开发一个模块,包含展示场景和编辑场景的组件

  3. 将 NPM 包 Link 到 TMS 管理后台的仓库

  4. 启动仓库本地 debug mode (体验很差)



  5. 开发阶段结束,开始发布阶段




  6. 发布 NPM 包

  7. 分别安装到对外渲染、对内后台管理的 Repo 上

  8. 分别发布对外、对内系统


整体流程很长,导致业务开发同学更愿意 0-1 写一个静态页,而不是开发一个 TMS 模块,进而造成了业务模块并不是很多,生态不丰富。
我在开发 TMS 模块时也深感痛苦,故过程中对 “新开发一个模块” 的流程进行了改进。
整体原则就是将模块的安装、加载从主体中剥离,从 NPM 包转变为浏览器端运行时注入的模块。当时已经有了 SEED,它比较基础,且全局都有安装,它是一个运行时模块加载、管理器,可以简单类比 SeaJS。通过维护一个 Alias ,模块 key 与 JS/CSS CDN 列表的映射关系,来决定如何加载模块,而这个 Alias 本身也是通过一个大 JSON 进行保存。
那么解决思路很直观了,只需要保存一个 TMS 模块 Key 与模块打包后的 JS/CSS 产物,即可做到将模块的安装由 buildtime -> runtime,进而能做到模块的调试、打包、发布与 TMS 主系统完全隔离。
优化后的流程是这样的:



  1. TMS 管理后台创建一个模块,就是元信息了拿到模块 ID

  2. 各自业务仓库开发一个符合一定 Interface 的模块

  3. 业务仓库本地 debug (打开 TMS 测试环境,直接把模块请求代理到本地即可)



  4. 开发阶段结束,开始发布阶段




  5. 各业务仓库构建模块,产出 JS/CSS,并自动上传 CDN,修改 Alias JSON 完成发布


本地调试由于仅需要构建当前模块,所以开发体验很棒。


SEED 与微应用


但是 SEED 也有它自己的问题,Alias JSON 独立于现有其他发布平台维护,且无灰度、无回滚,是个很大的稳定性隐患。
当时公司基建(Pub)已初步成型,比较超前,核心是以最小可发布粒度的一站式解决方案,而首推的就是页面单元,能将传统的以 Repo 多页为发布单元,转为以独立页面为发布单元,且秒级发布、秒级回滚。前端微应用当时也初步成型,主要目的是拆分酷家乐工具的巨石应用,提升开发效能。
当时主站还在继续使用 SEED,前端微应用和 SEED 其实目标非常类似,核心都是独立开发、独立发布。这时产生了一个想法 “能否让前端微应用支持浏览器端运行时加载,以替代掉 SEED 模块管理部分的能力”,达到 “All in micro” 的效果。
此时,前端微应用的输出模式是 html 片段,此片段可以注入到 page 中,最终输出完整的 page html 给到浏览器,即拼接形式。页面与微应用可独立发布,在统一的 Node.js Page Render 层进行服务端拼接,以组装成一个可以由多团队共建的完整应用。
那么,做法很清晰了,需要将仅支持在服务端 html 拼接形式使用的微应用,扩展为支持浏览器端运行时动态获取微应用 html 片段,并注入到 DOM 中去,并解决 Script 等标签无法执行与如何同步有序执行的问题,这就诞生了“Pub 微应用加载器”。
此时已存在 Single SPA 或乾坤等库,独立发布的功能是大家共有的,沙箱&路由联动等特性是不需要的,所以也没有参考这些开源库实现。
此阶段之后就顺势推动 SEED 历史模块全量迁移 Pub 微应用,相对的好处是:



  1. 拥抱同样的基建(CI/CD),灰发&回滚等机制

  2. 无需页面预置环境

  3. 去中心化,微应用加载器分布式安装在各个微应用 or 页面 bundle 内,不到 8K (未压缩)


而 TMS 的新模块开发方式也由 SEED 模块过渡到使用 Pub 微应用模块。


公共包


基建相对比较成熟了,但是主站业务的公共包却一直比较混乱,质量也不高。“磨刀不误砍柴工”,工具库、业务组件库的重要性不言而喻,这半年也开启了公共库的创建和规范:



  1. types:以业务域划分,定义业务通用的类型单元,例如方案等

  2. utils:工具函数

  3. rc:业务特定的组件库

  4. etc...


这部分内容大多数公司做的事情类似,不细讲了。


反思



  1. HTML 是组成页面的基本单元,以它为切入点,相对以 JS Entry 能做更多事;

  2. 跨团队协作,独立发布,低耦合是效能王道

  3. 开源产品能解决部分通用问题,工作流的串联,整体架构还需独立设计

  4. 秒级发布&回滚,能解决绝大多数稳定性问题

  5. 发布卡点 or 审批对于新手是保护,对于老手是枷锁


H2 开始,也带来一些新的挑战:



  1. 如何快速搭建新站点

  2. 类似的区块如何复用,是否复制是个更好的选择?


21 创新


Star


基于 20 下半年业务上各种新站点搭建带来的效率以及质量的综合挑战,21 年初我在思考“是否要造一个全司共建共享的物料共享平台”,以打破团队间信息壁垒。
在此阶段我已经是敏捷组 TO,并且有一定的影响力,所以大家愿意跟着我的想法一起干,包括隔壁组同学。此时恰好 UED 团队同学有“设计物料共享”的想法,所以一拍即合,前端 5 人 + 设计 2 人,自建组成虚拟小组,利用业余时间创建:Star 物料平台
平台设想大而全:



  1. 开发物料:Web 端、VSCode 插件、物料开发 CLI;分为 2 大类:区块、页面模板

  2. 设计物料:Web 端、Sketch 插件


这里主要讨论下开发物料,区块和页面模板都是参考自“飞冰”的设计,利用“复制”的手段,达到复用的目的。好处就是可以任意修改,不会因为 Interface 不满足而无法使用或扩展,相似的视觉效果都能直接拿来用。
而 VSCode 和 Sketch 插件的代码分别 Fork 自开源项目 IceWork、Kitchen(好像是),进行自有系统以及物料库的集成。
整个系统全栈 TS 开发,包括 Sketch 插件,服务端采用 NestJS+MySQL+Serverless 完成。
反思:



  1. 现在看来,区块的复用方式不如组件的形式,而且也没有用起来

  2. 页面模板倒是用来做初始化页面 or 微应用的规范了,也是一种将各业务线规范落地的平台

  3. 设计物料和 Sketch 插件使用量可观,相对于原始 File 下载分发,借助 Sketch 插件自动享受最新的设计物料比较高效


所以就区块来说,Star 是失败的,所以后来又逐步优化,新增了微应用文档的接入,因为微应用的使用方必然是需要阅读文档的,Star 就是一个比较好的集成微应用使用文档的平台,直接关联微应用的唯一 Key。


登录注册


在这之前我也兼账号体系(UIC)的前端负责人。酷家乐的账号体系也许是互联网行业最复杂的系统之一,它的复杂性来源于:



  1. 面向多种产品:To C、To B

  2. 面向多种身份:设计师、业主、从业主,在这之下又有很多细分行业


登录注册链路也有一定的复杂性:



  1. 注册链路极长,三方绑定 -> 手机验证 -> 选择身份 -> 推荐设计师/业主 -> 发放奖励

  2. 登录的形式:三方、扫码,弹窗登录、登录页面

  3. 登录过程中的风控拦截,图片验证,

  4. 登录过程中的 C & B 多账号绑定

  5. etc.. 还有很多没有列出来的


面临的挑战:



  1. 整体偏过程式的写法:你可以想想一个回调函数内部写了非常长的逻辑,且牵一发动全身

  2. 数据流与执行流的混乱:Promise 可能存在一直 Pending 的状态,例如某个 callback(resolve) 一直不执行,流程中的数据传递混乱,没有一条主线

  3. 以上带来的结果就是,涉及登录注册的任务估时 x2

  4. 美间、模袋等业务的加入,需要打通账号体系,并且复用同一套登录注册能力(但是不接受走同一个页面完成 SSO,这决定了后续的架构模式)


基于此,对登录注册组件进行了彻底的重构:



  1. 更合理的分层:基础包(通用 UIC 逻辑)、核心能力(支持配置化的形式确定外部需要何种登录注册方式)、酷家乐业务场景下的微应用 以及 其他业务场景下的页面

  2. 插件化的架构模式:借助 Tapable 完成异步串行的执行场景,增加登录注册前后等超 10 个 Hook,为后续扩展奠定了基础,并解决执行流问题

  3. 全局 Store:解决数据流问题

  4. 将原有非核心链路的逻辑拆分出接近 10 个插件,完成业务逻辑


结果:



  1. 扩展性:最初设想就是未来至少 3 年不需要重构登录注册模块,目前我认为至少 5 年是没有问题的

  2. 研发效率的提升:统一群核之下的几乎所有业务线的登录注册;后续几年的实战中,对于登录注册业务上的各种大需求,都没有对核心部分造成影响,通过插件都能满足需求

  3. 整体的 ROI 还是很高的


反思:



  1. 对核心业务的架构优化是值得投入的

  2. 插件化不仅用于工程化领域,也可用于业务,需要一定复用性、扩展性的场景都可考虑

  3. 架构是为了不让复杂度指数级爆炸


开发效率与规范


21 年的以上 2 个偏全司基建或特定业务,开发效率与规范也在持续进行:



  1. 为了多仓库共享代码,造了 Rocket CLI,定位是基于 subtree 的业务线级别的代码共享

  2. 规范了业务域为单位的 Owner 机制,并且不分端(PC、H5、小程序)

  3. 规范了 lint/babel/postcss/ts config,并且基于 Rocket 可以做到及时的共享更新

  4. 规范了所有 page 的启动方式,也基于 Rocket 进行共享

  5. 规范了全局弹窗的管理器,支持优先级队列机制

  6. etc...


基于 Rocket 的基建能力,做了到所有业务仓库共享同一套 xxx config,共享同一套业务启动逻辑。
但是也带来了一些棘手的问题:



  1. Git subtree 的机制,会让 Repo 历史记录混乱,掺杂很多不相干的 commit

  2. 高版本 Git subtree 提交时,部分同学总是无法 push 上去

  3. 随着时间推移,2 年后的今天,commit 已达 2k 多条(中间应是某些同学误操作带上去的),导致后期又增加了 reset 的机制,并且把 shared Repo 给重置了,进而又导致 shared Repo 与业务 Repo history 对不上...


这些问题只能通过比较懂的同学人肉操作下,以达到可以正常 push pull。所以后期会弃用 Rocket,改回 Npm Package,但是增加一些功能让他能保持定期更新。
反思:



  1. Subtree 有其局限性,最好的协作模式我认为一个业务线采用单一的 monorepo,通过基建去直面单 Repo 的构建性能问题,部署效率问题;

  2. 对于业务线的开发团队,最优先的是制定规范、落地规范到代码里、及时更新规范,以达到开发者同一套开发思路,对于协同开发效率是极好的;

  3. 不要分端,其他端的开发成本相对团队多人的沟通成本低很多;


22 再创新


22 年底有写过《2022 年终总结》,所以这里尽量谈的更宽泛一些, 有一定的相似处。
职位的变化,21 年中开始担任 Action Mgr,22 年初转为正式 Mgr。也会有一些管理思考,但是本文不会涉及。


客户端打包平台


没错,又开始造平台了。背景是酷家乐的主要用户在 PC 端,且绝大多数都使用客户端(基于 Electron),而且其他业务线也会开发自己的客户端(例如美间)。
所以除了一些基础 Electron 扩展能力的复用之外,长远来看最好能有个工程化平台,集成端侧的构建、打包、发布、分发等一系列的能力。这就是“客户端打包平台” 也可以称之为“客户端 DevOps 平台”。
做了如下事情:



  1. 首先需要一个打包环境,不仅要打包 Windows/Mac 上的 Electron 应用,后续还支持了 Android App 的打包

  2. 其次需要一个打包管理后台,包括:应用管理、构建管理、版本管理、发布管理以及权限

  3. 最后定义一套接入规范,以 Node.js 脚本形式接入,脚本接收一些入参,根据参数构建、打包、签名产出最终的安装包(固定目录),平台进行上传并回调更新 CDN URL、版本等信息


整体逻辑并不复杂,说一些它和 Web 页面发布的区别:



  1. 存在版本,线上版本碎片

  2. 存在复杂的更新机制,也有灰发机制

  3. 存在不同渠道分发不同安装包,便于后续的安装来源统计

  4. 多种打包目标:Windows/Mac/Android,不同目标会有提供不同的打包环境


对于平台,还有很多事没做,例如:数据看板,版本分布等,但是对于近几年足够了。
有些同学可能会不理解,和 Gitlab CI 有啥区别?
借助 CI 仅能完成任务的触发,而任务是需要特定的运行环境的,除此之外:版本的管理、灰发、渠道分发都是平台特有的能力。
反思:



  1. 针对核心业务做基建更不易出错


SSR


过去几年,随着基建升级,老的 FreeMarker(JAVA) + JQuery ,慢慢转变为 Nunjucks(Node.js)+ JQuery,再转变为 Nunjucks(Node.js)+ React。而到 React 阶段,服务端直出页面关键 HTML(SSR)已不存在。产生的结果就是来自搜索引擎的流量逐渐下滑,而 SEO 对于酷家乐来说至关重要,是个非常重要的流量窗口。为了拯救 SEO,22 年上半年开始了一些 SSR 的尝试。
但是,要做 SSR ,会和业界常用方案有所不同:不会采用 Next.js 类似全栈框架,因为此类全栈框架带来的问题是每个业务都需要独立的 Node.js 服务,还需要持续的观测稳定性,出问题对于业务开发者来说是非常棘手的,对开发者的要求极高。
所以 SSR 服务需要做到的效果:



  1. 每个 SSR 页面都可以独立发布,即使他们在一个 Repo

  2. 创建 SSR 容器服务,由 SSR 服务的开发者管理服务的稳定性,业务开发者无需关心

  3. 所有 SSR 页面都运行在这个容器内

  4. 所有 SSR 页面需要有沙箱,运行上下文隔离

  5. 需要有降级到 CSR 的策略


除了 SSR 服务本身之外,也需要有其周边的工具链:



  • 针对每个 SSR 页面构建打包为独立的 Server Entry

  • Server Entry 需要符合约定的 Interface,输入请求上下文,输出渲染结果

  • TS type 包,便于接入


方案详情页是第一个接入的页面,上线前借助 Apache Benchmark 工具做了一定的压测,上线结果也是很好的,此阶段 22 年中上线。
到此阶段,还有一些工程化问题需要联合基架组一起解决,深入集成到 Pub 系统内:



  1. 本地开发:支持 SSR 和 CSR 的同步开发,以及规范的本地开发调用链

  2. 构建:自动识别哪些页面需要走 SSR ,完成 Server Entry 构建

  3. 发布:发布后,对于页面信息的变更,秒级同步到 PR 与 SSR,完成应用的自动更新

  4. 运行:集成请求链路 浏览器 -> PR -> SSR,自动降级能力;以及 Pub 上配置包、语言包等能力的打通


此阶段在 22 年下半年完成,完成后对于业务开发者来说,开发一个 SSR 页面和 CSR 一样简单,不仅是 SEO 的提升,对于首屏优化也有效。
过程中也遇到了各种问题:



  1. 一些二方包实现时没有考虑 Node 端运行场景,例如使用了很多 window/navigator 等浏览器端全局变量,使用 jsdom 注入到每个页面上下文里解决(但也带了问预料之外问题,见 3)

  2. OOM 问题:随着 SSR 流量增多,有一天触发了一端代码的临界点,即运行几天后内存溢出,服务被 K8s Pod 自动重启,反复;排查下来是一个非常基础的监控模块,在并发的 HTTP 链接达到一定数量后进入另一个分支,这个分支对缓存的清理有问题,导致持续增长的内存没有被回收

  3. GC 频繁导致 CPU 增高:根因是有个页面使用到了 react-helmet 库管理 document head 信息,helmet 又使用了 react-side-effect 来处理 props 变化引发的副作用,问题就出现在我们 Mock 了 window/document 等信息,让库误认为当前运行环境在浏览器端,进而将本应无副作用的 string 处理,变成了 DOM 处理,让 Node.js 内 new space 的空间增多,进而引发频繁 GC。


可以看到目前的 SSR 方案也并不是完美的,虽然做了沙箱,但是本质他们还是运行在同一个线程之内的,共享同一个 CPU 资源和内存资源。当出现 GC 引发的 CPU 问题,或 OOM 引发的问题,就会导致整体的不可用。
所以解决这些问题的方案就只能做多进程,一个页面的 SSR 就启动一个独立进程,由主进程管控数据分发、应用更新,这样能充分利用多核 CPU,不至于一个页面 Bug 引发整体的雪崩。
反思:



  1. 之前做的平台更偏研发效率,SSR 能解决一定的业务问题

  2. 不一定一开始就要最完美的方案,保留一定扩展性,能解决当下问题就是最好的


其他


22 年也有一些效果不错的优化:



  • 帮助中心的核心 API 性能提升 70%,帮助中心 JS 全站开发,主要优化的是对 MySQL 调用相关的业务层的逻辑优化。同时也解决其稳定性问题,之前总会因为流量的翻倍导致服务短时间不可用;

  • Star 部分也在持续优化:新增了一些页面模板,微应用的文档是在这一年做的

  • 客户端的可观测行:本地日志优化,以及用户的一键上报

  • 重写富文本编辑器,并应用在多条业务线

  • etc


23 优化


23 年主要是对现有系统的优化,年中也由主站转岗到了国际站。


SSR & Star & 客户端



  • SSR 一些接入文档,最佳实践之类的文档编写

  • Star 权限管理;支持培训物料类目

  • 客户端监控体系的建设,接入现有监控平台

  • 客户端打包平台的持续优化等


国际站


国际站的技术沉淀基本等价于 3 年前的主站,所以还有很多问题需要解决,以及一些提效工具都没用上,感觉和大多数业务线有些脱节。
国际站有很大的特点:多语言、多货币、多集群,依赖的很多三方也不同,例如登录、支付场景。以上都和前端息息相关,其中和开发方式密切度非常高的,就是多语言。
而多语言,目前公司已有基建也比较完善:语言包 CDN 化 + 配置后台 + 项目粒度管理 + VSCode 插件提效。但是也由很多问题:治理困难,例如如何清理一些无用词条;验证困难,例如如何验证其他语种的有效性等。我目前还没有想到比较好的解决手段。


Magi 配置平台


除此之外,页面配置化的能力,对于运营可快速尝试各种增长手段也至关重要。目前运营会采用上文提到的 TMS 来搭建一些营销页、产品介绍页。但也有一些是不满足需求的:SEO、性能、多语言等。
除了 SEO 之外,另外 2 条通过优化 TMS 都还能解决。因为 TMS 的整体架构决定了,想要能支持 SSR 很难,更不必说内置的或二方的组件了。除了页面编排需求之外,还有这些诉求:



  1. 开发者编写的页面也需要有配置化的能力,而针对特定功能开发特定的后台,成本极高

  2. 页面配置化需要能根据国家、人群维度进行不同的展示

  3. 分站点,例如不同国家不同站点


为了满足以上需求,计划造一个低代码配置平台,以及低代码引擎。目前还处于非常早期的阶段,仅完成整体的架构设计和部分 Core & Editor 逻辑的编写。


总结


至此,酷家乐的旅程告一段落。
这段旅程里,做了很多针对研发效率、质量方面的工作,也为其他岗位角色(UED、运营、市场)带来了人效的提升。我相信每一份努力和效率的提升,都会让酷家乐进步一点点,让我们在这个竞争激烈的市场上赢得胜利的机会多一点点。在这里收获满满,未来祝愿群核科技越来越好!
再额外聊一下关于离职,我的看法。我们常看到某些同学因为个别同事的离职,而内心动摇,也决定离职,我曾经也这样。但是在加入酷家乐前,就告诉自己,直面自己内心,不要在乎他人的去留,只要能确定自己能有成长、有收获、与自己规划相符就足够了,共勉。



2023-12-08
于 良渚


作者:洋葱x
来源:juejin.cn/post/7310028335480619027
收起阅读 »

喊话各大流行UI库,你们的Select组件到底行不行啊?

web
各种 UI 库的 Select,你们能不能人性化一点! 最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果... 大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到...
继续阅读 »

各种 UI 库的 Select,你们能不能人性化一点!


最近在云效上合并代码,本想着懒的目的输入了非连续的关键字搜索分支,结果...


1.gif


大概逻辑就是,搜索时必须输入连续的字母,比如,要找 “master-alpha”分支,非要输入 master-al 才能搜到,像图中输入 “masal” 就完全搜索不到。这导致了很多场景下使用起来很不方便,例如我们只记得几个非连续的关键字,或者懒得打那么多连续的关键字来搜索,用户体验较差。


然后我又看了几个流行组件库的 Select。


Element-ui


2.gif


Antd


3.gif


Naive-ui


4.gif


全军覆没!


那我们来自己实现一个吧!先来两个实战图。


不带高亮的非连续搜索


6.gif


带高亮的非连续搜索


5.gif


实现不带高亮的非连续搜索


以vue3+ElementUI为例,在这里将会用到一个小小的js库叫sdm2来实现非连续的字符串匹配。


视图部分


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

el-option>
el-select>

没有什么特别的,就是加了个filterMethod函数将关键词赋值给query状态,然后optionsComputedquery值根据关键词进行筛选


import { match } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() => options.filter(({ label }) =>
// 使用sdm2的match函数筛选
match(label, query.value, {
// 忽略大小写匹配
ignoreCase: true,
}
)));

就这么简单完成了。


实现带高亮的非连续搜索


视图部分


高亮部分使用v-html动态解析html字符串。


<el-select
v-model="value"
size="large"
placeholder="Filter options"
filterable
:filter-method="q => (query = q)">

<el-option
v-for="item in optionsComputed"
:key="item.value"
:value="item.value"
:label="item.label">

<div v-html="item.highlight">div>
el-option>
el-select>

为了让匹配到的关键字高亮,我们需要将匹配到的关键字转换为html字符串,并将高亮部分用包裹,最后用v-html动态解析。


import { filterMap } from 'sdm2';

const options = [/* 选项列表 */];
const query = ref('');
const optionsComputed = computed(() =>
// 为了过滤出选项,并将到的转换为html字符串,此时我们要用sdm2的filterMap函数
filterMap(options, query.value, {
ignoreCase: true,

// 把matchStr返回的字符串作为被匹配项
matchStr: ({ label }) => label,

// 匹配到后转换为html高亮字符串
onMatched: (matchedStr) => `${matchedStr}`,

// 将匹配到的项转换转换为需要的格式,str为onMatched转换后的字符串,origin为数组的每项原始值
onMap: ({ str, origin }) => {
return {
highlight: str,
...origin
}
}
})
);

然后一个带高亮的非连续搜索就完成了。


总结


这样你的搜索库就又更智能点了吧,然后各位 UI 库作者,你们也可以考虑考虑这个方案,或者有哪位朋友愿意的话也可以去为他们提一个issue或PR。

作者:古韵
来源:juejin.cn/post/7310104657212178459
收起阅读 »

从一线城市回老家后的2023“躺平”生活

归家 22年的十月份,在上海工作了三年多的我回到了老家。 前端,20年二本毕业的,当时在上海看老家的招聘信息,感觉很棒,很心动。又因为公司在大裁员,刚刚好在最后一轮裁员的时候,被裁了,拿了赔偿金,因为房租没有到期,又最后玩了玩,回了老家。 现实的落差感    ...
继续阅读 »

归家


22年的十月份,在上海工作了三年多的我回到了老家。


前端,20年二本毕业的,当时在上海看老家的招聘信息,感觉很棒,很心动。又因为公司在大裁员,刚刚好在最后一轮裁员的时候,被裁了,拿了赔偿金,因为房租没有到期,又最后玩了玩,回了老家。


现实的落差感


    回到老家后,又休息了十几天吧,就开始看招聘的信息,之前在上海看着很心动的岗位,简历投了又投,要么回复你,岗位已招满,要么压根不理你(后来我才知道,是学历的问题,老家这边的国企,最低学历就是研究生了,压根不看你工作履历)。剩余那些搭理你的公司,大都是小公司,可能只有十个人左右,而且大多都是单休或者大小周,有些甚至五险一金都没有,工资也低的可怜,是之前的三分之一差不多。


    心里难免有很强烈的落差感,但是由于我们(们:我老公,那个时候我们还是情侣,从大学开始的,在一起快7年了)家都是在这边的,两边父母都在这,觉得我们之后就是要在这里发展的,我俩硬着头皮,每天划拉着招聘信息,投着简历,适当地去面试。


    中间有一家公司,我感觉还可以,然后想着去试试,干了两天半。


    刚去的第一天,技术团队是:一个后端和一个外包的后端,以及一个跟我一样刚入职的前端,一共就我们四个人,然后就是老板,只有我是女生。除此之外,还有保洁阿姨(中午会做饭,公司中午管饭)、人事小姐姐等一些非开发人员。下午开会,老板居然直接在会议室抽起了烟(熏得我不要不要的)!然后项目用的是ruoyi的架子,里面有些代码是那俩后端暂时写的,看起来有些乱。就这样干了两天,那俩后端,很爱抽烟,再加上老板也带头会议室开会还抽烟,整天感觉身边烟熏火燎的。


    到第三天的时候,中午开了个会,意思是,之前我们开发的好像需求都不行,并且又提了一堆新需求,还告诉我们说只有两三天的时间搞完。我就意识到不对劲,是逼着人加班,没死没活的干的那种。然后再加上被熏了三天,于是开完会,我就赶紧收拾着我的东西,跑路了,干不了,根本干不了。既不合理,而且办公环境很糟糕(每天烟熏火燎),还没有社保,据说年后才缴纳。下午他们打来电话,问我咋回事,还要给我加薪让我再回去,但我已决心不去那干了。后来的我一点都不后悔这样做,甚至觉得很明智。


    就这样继续在招聘软件上看着,有新岗位咯,就投,就面试。


     突然有一天的周日,我接到了一个电话,说我可以来上班,他们缴纳五险,是双休,还有餐补,并且薪资也比之前面试的也差不多(之前还有个公司给的薪资和他一样,但是他是大小周,我不想去),这种待遇的公司,对于目前的我来说,已经很可以了,然后我就同意了,并且两天后去入职,这家公司就是我现在的公司。相比之前那家“烟熏火燎”的公司,这家就正规了许多,可能因为总部在深圳吧。


我们订婚啦


既然工作稳定了,那就开始丰富生活。2023年02月05日,我们举办了订婚宴~




工作


    这边的前端工作不太是普通的传统前端,而是electron打包出来是个exe啊,或者是针对模型3d渲染引擎啊,依托于基于threeJs二次开发出来的一些第三方,之类的,总之跟之前做的不一样,之前的我做的都是h5、微信小程序、或者接入一些公众号之类的。所以与其说是在工作,不如说是一直在学习吧。公司也知道我不太会,于是乎就给我很长时间先学,先熟悉,然后再去一点点开发。并且我几乎没加过班。


我们结婚啦


后面一切按照计划进行,拍摄婚纱照、男方那边在忙着新房装修,我们这边在置办嫁妆、买车车等。


在2023年10月10日,我们举行了典礼。



安稳且平淡


现在的我们每天安安稳稳,我想着适当提升下自己的学历(因为我们这边的好单位,现在好像都要研究生毕业了),在看着咱们计算机考研408的一些科目(双11心血来潮,一下子买了六七百块的书,不看总觉得买书钱白瞎了TAT),但是每天下班回家,还是忍不住看一些电视剧啥的,佛系考研,阿弥陀佛,哈哈哈哈


我们从上海一直养到现在的猫猫~



这就是我跟大家分享的我的这2023年的一年的经历。说实话,回老家的确比在一线城市更真实,因为身边有父母,有家人,每个周末都可充实。一线城市是素质高、节奏快,人的整个思想境界感觉都跟老家这边的人不一样。但兜兜转转,回老家似乎也并不是“躺平”,有落差感,因为接触过好的了。反正无论怎样,感觉简单、安稳、快乐的过好每一天就挺好。我们一起加油吧~


作者:wenLi
来源:juejin.cn/post/7311206584205869096
收起阅读 »

年底事故频发,做前端会不会出大型事故?

炽天使-S-蛇女-甜甜果实 前言 一些乐子,一些思考,不喜勿喷,欢迎交流 最近崩的有点多,来看看都有哪些 语雀崩了... 阿里云崩了... 滴滴崩了... 腾讯视频崩了... ...... 刚看完《三体》三部曲,最后一部《三体3:死神永生》里面,宇宙因为...
继续阅读 »

炽天使-S-蛇女-甜甜果实



前言



一些乐子,一些思考,不喜勿喷,欢迎交流



最近崩的有点多,来看看都有哪些


语雀崩了...


阿里云崩了...


滴滴崩了...


腾讯视频崩了...


......


刚看完《三体》三部曲,最后一部《三体3:死神永生》里面,宇宙因为质量流失过大,也快崩溃死亡了,也不知道会不会出个后续,归零者把宇宙的宏观维度重新回到十维的情况...


......


似乎什么都在崩,哪哪都在崩,好在掘金没有崩,不然掘友都看不到文章了,给掘金点赞一波,服务稳住老狗



继续回到主题上,做个前端开发工程师会不会导致项目出现大型事故?


前端都包含的职责


关于前端这个岗位负责的部分,分三个大类来分析讨论



  1. 只负责开发页面的前端

  2. 负责页面开发,也负责基础架子搭建和组件开发的前端

  3. 负责开发、基础建设、服务部署等等相关工作的前端


1. 只负责开发页面的前端


项目架子是别人搭好的,大部分核心或共同组件是别人写好的,或者负责维护的


日常工作具体内容包括但不限于,使用 vue, react 等框架开发业务,实现各种与后端的数据交互,展示效果,以及用一些库或者手写一些组件库之类的实现某些特定的业务效果或场景


如果项目经过的必要的功能测试,并运行稳定的项目,如果出现突发大型事故,百分之九十九点九的问题,都是运维或后端服务方面的,需要背锅侠大概率也轮不到前端


咱该吃吃该喝喝,遇事别往心里搁



2. 负责页面开发,也负责基础架子搭建和组件开发的前端


在稍有规模的团队中,负责前端工程搭建,一些通用组件封装,以及打包相关的配置,包括一些基础的性能优化等工作的一般是前端老鸟,或者项目中的资深技术选手做的,这种角色一般是比较熟悉业务了或者是团队中的主力选手了


如果项目中遇到突发大型事故,一般是冲在一线,虽然问题百分之九十的可能不是前端范畴,剩下的那百分之十还是很有可能的,例如那一年的开源项目 Ant Design 圣诞节彩蛋事件,在某些公司直接彩蛋变炸弹💣


由于官方没有通知,也排查不出问题,谁知道这玩意儿竟然是个官方出的圣诞节彩蛋,如果需要背锅侠,很可能直接就是前端开发相关负责人背锅了



3. 负责开发、基础建设、服务部署等等相关工作的前端


这种前端一般多少涉及点全栈了,相关服务的操作权限可能也有。一般微小型公司这种情况比较常见,一人多职,稍微人多点的中大型公司服务器运维相关岗位,这种一般都有专门的部门或单独岗位的人负责统一管理,这种角色可能是一线负责人或者技术团队负责人等等


在中大型公司,这种什么都参与,也有相关权限的,可能资深大拿,或者技术部负责人之类的,如果是负责人一般也很少直接参与业务开发了


如果项目遇到突发大型事故,一线负责人一般有事儿都是第一时间知道,然后第一时间协调资源或者参与问题解决,小公司中可能老板都会直接参与协调解决,这种核心选手在小公司中一般不会受到什么处罚,如果是大公司中需要有人承担责任的,一般也可以甩锅出去,找个具体干活的开发背锅



分享身边发生过的一件事


小创业公司,一天去潜在客户那边演示项目,由于做的是 toB 的项目,目前还是自用阶段,没对外开放,日常都在公司是内网开发测试使用,然后老板有天安排去xx公司演示,然后团队中俩人就去了,俩人提前一个小时多到的,在他们连好投屏显示器后准备先点点看看,结果发现项目不能登录😅😅😅


赶紧排查原因,原来后端接口服务域名地址没有对外开放,外网不能访问


当时那俩人拿的是公司的mac演示本,没有装远程工具之类的,负责运维的那个人不在公司,在公司的只了解一点点运维,也就会点 Linux 了解 Nginx 那种,操作方面和专业运维差远了,运维电话联系指挥操作弄了约半小时也没好,后来放弃了现场演示了,改成只讲一下 PPT 了,最后不知道和那边是怎么交代的,反正后来没和那边的公司升级成商业合作伙伴


这时候背不背锅可能意义不大了,小创业公司如果是重要的演示直接搞砸了,公司的业务可能直接就没有了,公司活着都是个问题,员工工资能不能正常发都得看工资的家底支不支持了


怎么做能大概率不背锅


明确职责,积极沟通解决问题,多产出等等这些因素是一方面,人是社会动物,有人的地方就有江湖,有江湖的地方也可能需要下面的因素



  • 在团队中保持较强的竞争力,让领导觉的你性价比高

  • 大量参与核心业务开发,让领导觉的替换了你以后其他人上手代价高

  • 成为组织或团队的核心圈人员,和领导混熟了,凡事好商量(重要!!!)


写在最后


一般来说是做的越多,责任越大;认真对待工作,出问题也不用怕,兵来将挡,水来土掩...


这年头各路公司都在 “开猿劫留,减猿增笑”,日常中的我们也需要进行一些准备,以应对突如其来的事故或者变故,以不变应万变...


每个人的经历,认知都是不一样的,同样的人不同角度下的世界也是不一样的,有不同意见是非常正常的,欢迎探讨交流不一样的心得,互相学习,共同进步



如果喜欢本文章或感觉文章有用,用你那发财的小手点赞、收藏、关注再走呗 ^_^ 


微信公众号:草帽Lufei




作者:草帽lufei
来源:juejin.cn/post/7311602153783705627
收起阅读 »

Ant Design Mini 支持微信小程序啦!

web
Ant Design Mini 经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微...
继续阅读 »

Ant Design Mini


经过一段时间的开发与不断的努力,我们兴奋地宣布,Ant Desigin Mini 组件库中已有 16 个核心组件完成了微信小程序的适配工作!现在你不仅可以在支付宝小程序中使用 Ant Desigin Mini 组件库,也可以在微信小程序中使用了!


目前这项适配正处于 Beta 阶段,我们诚挚地邀请大家前来体验。首批适配的组件包括:ButtonSliderContainerIconLoadingSwitchTagInputCalendarListResultPopoverMaskStepperPopupCheckbox


先来看看 Demo 效果:
image.png


我们的官网文档、Demo 都已同步更新双端切换能力:
2023-11-28 14.44.51.gif


你可以参考以下文档进行接入:



双平台无法对齐的特性


受制于各平台框架设计,以下这些特性存在差异:



  • 两个平台的事件 API 不同。支付宝小程序可以把实例上通过 props 传递给子组件,而微信需要在 data 里传递函数。视图层的写法也所有不同。

    • 下面是 Calendar 在两种平台的使用方式。

      • 微信小程序






Page({
data:{
handleFormat() {
}
}
})

<calendar onFormat="{{handleFormat}}" />

  - 支付宝小程序

Page({
handleFormat() {

}
})
<calendar onFormat="handleFormat" />


  • 微信小程序不支持在 slot 为空时显示默认值, 所以我们在微信平台取消了部分 slot,对应会损失一些定制能力。 比如说 Calendar 组件, 在支付宝可以通过 calendarTitle 这个 slot 定制标题,在微信端只能通过 css 来控制样式。


<View class="ant-calendar-title-container">
{/* #if ALIPAY */}
<Slot name="calendarTitle">
{/* #endif */}
<View class="ant-calendar-title">{currentMonth.title}</View>
{/* #if ALIPAY */}
</Slot>
{/* #endif */}
</View>


  • 微信小程序不支持循环渲染 slot , 所以部分组件无法迁移到微信, 比如说 IndexBar 组件, 使用了 Slot 递归渲染整个组件的内容。这种写法无法迁移到微信。


<view a:for="{{items}}">
<slot
value="{{item}}"
index="{{index}}"
name="labelPreview" />

</view>

双平台适配背后的工程技术


下面我们为大家介绍一下 Antd Mini 支持多平台背后的一些工程方案。


中立的模板语法: 使用 tsx 开发小程序


由于支付宝和微信的小程序的语法各有差异,为了解决让 Antd Mini 同时支持两个端,我们团队选择的 tsx 的一个子集作为小程序的模板语言。
使用 tsx 具有以下优势:



  • 可以直接使用 babel 解析代码,无需自己开发编译器。

  • 各个 IDE 原生支持 TSX 的类型推导与代码提示。

  • 视图层和逻辑层可以复用同一份 props 类型。

  • 可以直接通过 import 导入其他的小程序组件,使用 typescript 进行类型检查。

  • 视图层脚本也可以享受类型校验,无需依赖平台 IDE


由于小程序的视图语法比较受限,从 tsx 向跨平台视图语法转换是比较容易的。我们基于 babel 开发了一个简单的编译器,解析原先 tsx 的语法树以后,将 React 的语法平行转换为可读性比较强的小程序视图语法。
具体举例来看:



  • 条件判断 : 我们使用了 &&以及 ?: 三元表达式替代了之前的 :if 标签。

    • tsx: !!a && <Text>a</Text>

    • 微信小程序:<text wx:if="{{ !!a }}" />

    • 支付宝小程序:<text wx:if="{{ !!a }}" />



  • 循环: 我们使用了 map 代替之前的 :for 标签,从源码里自动分析出 :for-item :for-index :key 等标签。

    • tsx:




{todoList.map((task, taskIndex) => (
<Text
hidden={!mixin.value}
key={task.id}
data-item-id={taskIndex}
data-num={20}
>

{taskIndex} {task}
</Text>

))}


  • 微信小程序:


<block
wx:for="{{ todoList }}"
wx:for-index="taskIndex"
wx:for-item="task"
wx:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 支付宝小程序


  <block
a:for="{{ todoList }}"
a:for-index="taskIndex"
a:for-item="task"
a:key="{{ task.id }}">
<!-- display: inline -->
<text
hidden="{{ !mixin.value }}"
data-item-id="{{ taskIndex }}"
data-num="{{ 20 }}"
>
{{ taskIndex }}{{ task }}</text
>
</block>



  • 事件绑定: 我们会按照配置,自动将直接转换为两个平台的格式。

    • tsx:<Text onClick="handleClick" />

    • 微信小程序: <text bind:click="handleClick" />

    • 支付宝小程序: <text onClick="handleClick" />



  • 视图层脚本

    • 我们还规定了以 sjs.ts 作为视图层脚本的文件格式。 在编译时候转换为对应平台的文件格式。

      • tsx: import helper from './helper.sjs.ts'

      • 微信小程序: <wxs src="./helper.wxs" module="helper" />

      • 支付宝小程序: <import-sjs src="./helper.sjs" module="helper" />





  • 类型方案

    • 为了让逻辑层类型与视图层关联,我们设计了一些工具类型。 比如说下面使用的 TSXMLProps,将 IProps 的 onClick 转换成了字符串。




// Calendar.axml.tsx
import { TSXMLProps, View } from 'tsxml';

interface IProps {
className?: string;
style?: string;
onClick: (e) => void;
}

interface InternalData {
size: number;
}

export default (
{ className, style }: TSXMLProps<IProps>,
{ size }: InternalData
) => (
<View class={`ant-calendar ${className ? className : ''}`} style={style}>
{size}
</View>

);

// Page.axml.tsx

import Calendar from './Calendar.axml.tsx'

export default () => (<Calendar onClick="handleClick" />)

目前使用 tsx 的这套方案还存在一些限制:



  • 和小程序相同,一个文件内只能定义一个组件。

  • 如果使用自定义组件,需要配置组件事件在各个平台的写法。


老组件语法转换?用 AI 就行了


在决定使用 tsx 语法之后,我们还面临一个很棘手的工作量问题:如何把历史组件库 axml 代码全量转换为最新的 tsx 语法?
这时候就该 ChatGPT 出场了,我们请 AI 来帮助我们完成这个一次性转换工作。
为了让转换结果更靠谱,我们使用了一些技巧:



  • 使用了 tsx 编译器等测试用例作为 prompt ,让 AI 可以更好的了解 tsx 的写法。

  • 除了 tsx 文件以外,我们还将组件的 props.ts 与 config.json 加到了 propmt 里,可以帮助 AI 生成更好的 import 导入。


在这里,你可以看到这份转换的完整 prompt。


确保 AI 产出的正确性?再用我们的编译器转回来


为了确保 AI 产出的代码是正确的,我们使用编译器将 AI 编写的 tsx 重新编译回 axml ,再用 git diff 对原始代码做比对,由此即可核查 AI 转换的正确性。


当然,这两次转换的过程不会完全等价,比如转换 map 的过程中会出现一层嵌套的 <block/>。好在这样的差异不多,一般肉眼看一遍就能确认正确性了。


跨平台通用的组件逻辑:小程序函数式组件(functional-mini)


除了视图,我们还需要确保组件逻辑适配到双端。这里我们使用了小程序函数式组件( functional-mini )的形式来编写,functional-mini 的源码及文档放置均在 ant-design/functional-mini


使用了函数式组件后,Antd Mini 用上了计算属性、useEffect 等特性,也能通过 hooks 来替换原有的大量 mixin 实现,让代码的可维护性提升了一个台阶。


以典型的 Popover 组件为例,逻辑部分适配完成后,它的代码完全变成了 React 风格,数据变更流程一目了然:


const Popover = (props: IPopoverProps) => {
const [value] = useMergedState(props.defaultVisible, {
value: props.visible,
});
const [popoverStyle, setPopoverStyle] = useState({
popoverContentStyle: '',
adjustedPlacement: '',
});

useEffect(() => {
setPopoverStyle({
popoverContentStyle: '',
adjustedPlacement: '',
});
}, [value, props.autoAdjustOverflow, props.placement]);

return {
adjustedPlacement: popoverStyle.adjustedPlacement,
popoverContentStyle: popoverStyle.popoverContentStyle,
};
};

关于小程序函数式组件的原理、特性介绍,我们将在后续的分享中另行展开。


写在最后


欢迎大家一起来尝试 Ant Design Mini 的跨平台能力,你可以在 issue 区提出宝贵的建议与 Bug 反馈。


官网: mini.ant.design/


国内镜像:ant-design-mini.antgroup.com/


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

最全面包交友养网站甜蜜定制APP 学生老总竞争激烈

婚恋网站交朋友APP里白领学生礼模信息经常更新 ,为双方约星巴克咖啡厅见面谈了解清楚满意后开始相处包养的网站,女士大多上班或上学见面聊天相处可以看出来 都可长或短期相处私人助理 看缘分和新鲜感,微信993153133 为双方搭建一个相识的平台保护双方权利,张家...
继续阅读 »

婚恋网站交朋友APP里白领学生礼模信息经常更新 ,为双方约星巴克咖啡厅见面谈了解清楚满意后开始相处包养的网站,女士大多上班或上学见面聊天相处可以看出来 都可长或短期相处私人助理 看缘分和新鲜感,微信993153133 为双方搭建一个相识的平台保护双方权利,张家口北京每个城市都可提供双方认识的机会”

收起阅读 »

大厂前端开发规定,你也能写成诗一样的代码(保姆级教程)

web
BEM 使用起来很多人不晓得BEM是什么东西 我来解释给你们听  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Blo...
继续阅读 »

BEM 使用起来

很多人不晓得BEM是什么东西 我来解释给你们听

  BEM是一种前端开发的命名方法论,全称为Block-Element-Modifier,中文翻译为块、元素、修饰符。BEM的主要思想是将页面分解成多个可重用、独立的模块,每个模块由一个主块(Block)和零个或多个元素(Element)组成,可以使用修饰符(Modifier)来描述模块的不同状态和变化。

BEM的命名规则如下:

  • 块(Block):一个独立的、可重用的组件,通常由一个或多个元素组成,并且有一个命名空间作为标识符。通常以单个单词命名,使用连字符分割单词,例如:menu、button、header等。
  • 元素(Element):块的组成部分,不能独立存在,必须隶属于某个块。元素的命名使用两个连字符“__”与块名分隔,例如:menu__item、button__text、header__logo等。
  • 修饰符(Modifier):描述块或元素的某种状态或变化,以单个单词或多个单词组成,使用两个连字符“--”与块或元素名分隔。例如:menu--horizontal、button--disabled、header__logo--small等。

  通过使用BEM命名方法论,可以实现更好的代码复用性、可维护性和可扩展性。BEM的命名规则清晰明了,易于理解和使用,可以有效地提高团队开发效率和代码质量。

page+ hd/bd/ft 用起来

"page+ hd/bd/ft" 是一种简化的命名约定,常用于网页布局中。下面是对这些缩写的解释:

  • page:页面的整体容器,表示整个页面的最外层包裹元素。
  • hd:代表页头(header),用于放置页面的标题、导航栏等顶部内容。
  • bd:代表页体(body),用于放置页面的主要内容,如文章、图片、表格等。
  • ft:代表页脚(footer),用于放置页面的底部内容,如版权信息、联系方式等。

  这种命名约定的好处是简洁明了,可以快速理解页面的结构和布局。通过将页面划分为页头、页体和页脚,可以更好地组织和管理页面的各个部分,提高代码的可读性和可维护性。

更好的使用工具(stylus插件)

,Stylus 是一种 CSS 预处理器,它允许你使用更加简洁、优雅的语法编写 CSS。通过在命令行中运行 npm i -g stylus 命令,你可以在全局范围内安装 Stylus,并开始使用它来编写样式文件。 .styl 是 Stylus 文件的扩展名,你可以使用 Stylus 编写样式规则。然后,你可以将这些编写好的 Stylus 文件转换为普通的 CSS 文件,以便在网页中使用。

  具体地说,你可以创建一个名为 common.styl 的文件,并在其中编写 Stylus 样式规则。然后,通过运行 stylus -w common.styl -o common.css 命令,你可以让 Stylus 监听 common.styl 文件的变化,并自动将其编译为 common.css 文件。

  以下是一份示例代码来说明这个过程:

  1. 创建 common.styl 文件,并在其中编写样式规则:
// common.styl
$primary-color = #ff0000

body
font-family Arial, sans-serif
background-color $primary-color

h1
color white
  1. 打开终端,进入 common.styl 文件所在的目录,运行以下命令:
Copy Code
stylus -w common.styl -o common.css

  这将启动 Stylus 监听模式,并将 common.styl 文件编译为 common.css 文件。每当你在 common.styl 文件中进行更改时,Stylus 将自动重新编译 common.css 文件,以反映出最新的样式更改。 请注意,为了运行上述命令,你需要先在全局范围内安装 Stylus,可以使用 npm i -g stylus 命令进行安装。

stylus的优点

  Stylus 作为一种 CSS 预处理器,在实际开发中有以下几个优点:

  1. 更加简洁、优雅的语法:Stylus 的语法比原生 CSS 更加简洁,可以让我们更快地编写样式规则,同时保持代码的可读性和可维护性。
  2. 变量和函数支持:Stylus 支持变量和函数,可以提高样式表的重用性和可维护性。通过使用变量和函数,我们可以在整个样式表中轻松更改颜色、字体等属性,而无需手动修改每个样式规则。
  3. 混合(Mixins)支持:Stylus 的混合功能允许我们将一个样式规则集合包装在一个可重用的块中,并将其应用于多个元素。这可以大大简化样式表的编写,并减少重复代码。
  4. 自动前缀处理:Stylus 可以自动添加适当的浏览器前缀,以确保样式规则在不同的浏览器中得到正确的渲染。
  5. 非常灵活的配置:Stylus 提供了非常灵活的配置选项,可以根据项目的需要启用或禁用不同的功能,例如自动压缩、源映射等。

  总之,Stylus 通过提供更加简洁、灵活的语法和功能,可以使我们更加高效地编写 CSS 样式表,并提高代码的可重用性和可维护性。

最后一个大招阿里的适配神器 flexible.js

  flexible.js 是一款由阿里巴巴的前端团队开发的移动端适配解决方案。它通过对 Viewport 的缩放和 rem 单位的使用,实现了在不同设备上的自适应布局。

具体来说,flexible.js 主要包括以下几个步骤:

  1. 根据屏幕的宽度计算出一个缩放比例,并将该值设置到 Viewport 的 meta 标签中。
  2. 计算出 1rem 对应的像素值,并将其动态设置到 HTML 元素的 font-size 属性中。
  3. 在 CSS 中使用 rem 单位来定义样式规则。这些规则会自动根据 HTML 元素的 font-size 属性进行适配。

  通过这种方式,我们可以实现在不同设备上的自适应布局。具体来说,我们只需要在 CSS 中使用 rem 单位来定义样式规则,而不需要关注具体的像素值。当页面在不同设备上打开时,flexible.js 会自动根据屏幕宽度和像素密度等信息进行适配,从而保证页面的布局和样式在不同设备上都可以得到正确的显示。

  需要注意的是,flexible.js 并不能完全解决移动端适配的所有问题,还有一些特殊情况需要我们手动处理。例如,一些图片或者 Canvas 等元素可能需要根据不同设备的像素密度进行缩放,而这些操作需要我们手动实现。不过,flexible.js 可以帮助我们简化移动端适配的工作,提高开发效率。

  下期我来教大家手动写适配器 喜欢的来个关注 点赞 这个也是以后写文章的动力所在 谢谢大家能观看我的文章 咱下期在见 拜拜


作者:扯蛋438
来源:juejin.cn/post/7303126570323443722

收起阅读 »

JS问题:简单的console.log不要再用了!试试这个

web
前端功能问题系列文章,点击上方合集↑ 序言 大家好,我是大澈! 本文约1500+字,整篇阅读大约需要3分钟。 本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。 1. 需求分析 一...
继续阅读 »

前端功能问题系列文章,点击上方合集↑


序言


大家好,我是大澈!


本文约1500+字,整篇阅读大约需要3分钟。


本文主要内容分三部分,如果您只需要解决问题,请阅读第一、二部分即可。如果您有更多时间,进一步学习问题相关知识点,请阅读至第三部分。


1. 需求分析


一般情况下,我们在项目中进行代码调试时,往往只会在逻辑中使用console.log进行控制台打印调试。


这种方式虽然比较常规直接,但是如果打印数据多了,就会导致你的控制台消息变得异常混乱。


所以,我们有了更好的选择,那就是console对象提供的其它API,来让我们能够更清晰的区分打印信息。


图片


2. 实现步骤


2.1 console.warn


当我们需要区分一些比较重要的打印信息时,可以使用warn进行警告提示。


图片



2.2 console.error


当我们需要区分一些异常错误的打印信息时,可以使用error进行错误提示。


图片


2.3 console.time/timeEnd


想看看一段代码运行需要多长时间,可以使用time


这对于需要一些时间的CPU密集型应用程序非常有用,例如神经网络或 HTML Canvas读取。


下面执行这段代码:


console.time("Loop timer")
for(let i = 0; i < 10000; i++){
    // Some code here
}
console.timeEnd("Loop timer")


结果如下:图片


2.4 console.trace


想看看函数的调用顺序是怎样的吗?可以使用trace


下面执行这段代码:



  function trace(){
    console.trace()
  }
  function randomFunction(){
      trace();
  }
  randomFunction()


setup中,randomFunction 调用trace,然后又调用console.trace


因此,当您调用 randomFunction 时,您将得到类似的输出,结果如下:


图片


2.5 console.group/groupEnd


当我们需要将一类打印信息进行分组时,可以使用group


下面执行这段代码:


console.group("My message group");

console.log("Test2!");
console.log("Test2!");
console.log("Test2!");

console.groupEnd()

结果如下:


图片



2.6 console.table


在控制台中打印表格信息,可以使用table


对!你没听错,就是让我们以表格形式展示打印信息。


如果使用log打印:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.log(person1, person2);

结果如下:


这样做是不是让数据看起来很混乱。


图片


反之,如果我们使用table输出:


var person1 = {name: "Weirdo", age : "-23", hobby: "singing"}
var person2 = {name: "SomeName", age : "Infinity", hobby: "programming"}

console.table({person1, person2})

结果如下:


怎么样!从来不知道控制台可以看起来如此干净,对吧!


图片


2.7 console.clear


最后,使用clear把控制台清空吧!


图片


3. 问题详解


3.1 可以自定义log的样式吗?


答案当然是可以的,只需要借助%c这个占位符。


%c 是console的占位符,用于指定输出样式或应用 CSS 样式到特定的输出文本。


但请注意,%c 占位符只在部分浏览器中支持,如 Chrome、Firefox 等。


通过使用 %c 占位符,可以在 console.log 中为特定的文本应用自定义的 CSS 样式。这样可以改变输出文本的颜色、字体、背景等样式属性,以便在控制台中以不同的样式突出显示特定的信息。


以下是使用%c 占位符应用样式的示例:


console.log("%c Hello, World!", 
  "color: red; font-weight: bold;border1px solid red;");

结果如下:


图片


通过使用 %c 占位符和自定义的样式规则,可以在控制台输出中以不同的样式突出显示特定的文本,使得输出更加清晰和易于识别。


这在调试和日志记录过程中非常有用,特别是当需要突出显示特定类型的信息或错误时。


结语


建立这个平台的初衷:



  • 打造一个仅包含前端问题的问答平台,让大家高效搜索处理同样问题。

  • 通过不断积累问题,一起练习逻辑思维,并顺便学习相关的知识点。

  • 遇到难题,遇到有共鸣的问题,一起讨论,一起沉淀,一起成长。

作者:程序员大澈
来源:juejin.cn/post/7310102466570321958
收起阅读 »

2023 闲聊开猿节流 降本增笑

前言 2023年大环境的影响,互联网行业真是难啊,裁员风声四起,言论不一,说互联网增长不如预期,存量运营也不再需要多少人手。各大互联网公司,一直持续着开源节流,降本增效的策略,裁掉一批人,直接降低固定成本。这真是一剂猛药啊,效果也是杠杠的。最后变成了 开猿节...
继续阅读 »

前言


2023年大环境的影响,互联网行业真是难啊,裁员风声四起,言论不一,说互联网增长不如预期,存量运营也不再需要多少人手。各大互联网公司,一直持续着开源节流,降本增效的策略,裁掉一批人,直接降低固定成本。这真是一剂猛药啊,效果也是杠杠的。最后变成了



开猿节流 降本增笑



语雀宕机、阿里云几次宕机,滴滴宕机,最近腾讯也宕机,难道剩下的都是写ppt汇报的吗!

哎,当决策者不懂”岁月静好”是怎么得来的时候,就是”负重前行”的人越来越少的时候。

最后,雪崩的时候,没有一片雪花是无辜的。


经典的笑话



一群年薪百万的在加班讨论给年薪10w不到降本增效



image.png

说点经历


说说我以前的一家公司的降本增效案例


背景:好几年前了,那时候环境没这么差,但是公司的盈利点,增长不如预期,老板很焦虑,带着高管团队去了趟延安,主题:重走长征路,学习先辈创业的艰辛。回来之后,在公司发起,“赚一块钱活动”,就是开动你聪明的脑瓜子,出出金点子,能达到降本增效的目的,所有部门,所有员工都需要参与,那活动就轰轰烈烈的开始了,相对还好,没开猿节流,达到降本增效!


插曲


行政部门,怎么做的呢?为了达到开源节流的目的,他们也是花了心思的。


控制电灯,比如办公区10个电灯泡,他们拿掉5个灯泡,那电费不是可以少支出一半,他们还真是这样执行的,直接整个办公区的电灯泡,拿掉了一半。没过多少天,引起了整个公司对行政部门做法的不满,闹到老板那里去了,老板还是比较务实的,直接把行政的负责人臭骂一顿,让恢复原样。这是降本增效吗,这是牺牲员工的利益达到的,如果这样,还不如在家办公,是不是房租 水电费都省了呢。


技术贴近业务


我在的部门主要是负责供应链系统的开发,比如:订单履约系统、库存系统、商品系统......,做技术的,怎么才能做到降本增效,想想挺难的。我们搞开发的,不就通过代码实现产品,同时保障系统的稳定运行,就OK了。不管怎么样,做技术的,还是得从技术的角度想想,能不能完成公司给的任务?


优化系统


复盘部门负责的所有系统,找出系统的性能瓶颈点,通过技术手段、一定的策略进行优化,比如:以前需要三台机器才能支撑目前的流量,系统优化后,两台就行,同时系统比优化前前性能还好,支撑的流量更大。那减少了一台机器,变相的减少了硬件支出的固定成本。


贴近业务,提高人效


没事找业务的人聊聊天,喝喝咖啡,你会得到意想不到的收获。开发一般获取的需求如下:


image.png


产品对业务提出的需求,也不一定能准确描述,提供比较好的解决方案,需求经过产品理解然后再输出到开发,开发如果不深层次挖掘,只是按照产品的设计,进行开发,跟现实还是又一定的差距的。借用黄晓明的经典名言:



我不要你觉得,我要我觉得



在跟业务聊天中,谈到我们的订单履约系统



  1. 用户一个订单包含多个商品,商品不在同一个仓库,需要多仓发货;商品库存不足,有库存的先发。好多订单,都需要人工进行查看,进行手工拆单,系统自动化吗?减轻点我们的工作压力。有了,自动化拆单

  2. 业务还发现一个用户特点,同一个用户,在时间间隔不到30分钟,连续下两个或多个订单,用户、地址、收货人姓名、电话、信息都一样。有了,合单,节约物流成本


通过以上交谈,我得出了两个需求点:



  • 自菜单拆单 当收到订单信息,查看是否在同一个仓库,如果不在,自动拆分单个仓库进行发货。如果订单商品中,有库存不足的商品,拆分订单,有库存的先发货。注意:用户看到的还是一个订单,只是商品对应的发货单不一样而已

  • 订单自动化合并 根据用户的下单规律,我们在订单下发仓库进行发货的时候,我们先延迟半个小时,看看在这半小时,是否有用户,再次下单,并且满足(买家ID、收货人姓名、电话、地址信息都一样)的订单合并到一个发货单里发货,订单与发货单对应关系N:1


通过上面的策略,自动化拆单,提供了人效,订单自动化合并,降低了物流成本,真正达到了降本增效,不是降本增笑,得到了公司的一致好评,技术人不单单会写代码,也能搞产品


提高个人的技术能力


能力:技术+沟通


沟通能力强,才能准确把握需求


技术能力强,写出高质量的代码,提高系统性能、稳定性。


这个单纯的提高人效,不怎么好衡量,周期比较长



总结:上面几点,是我们部门,通过技术能力,赋能业务,达到降本增效的目的。



应对开源节流 降本增效


提高个人能力


技术人立命之本:技术,先精后广,比如:我是Java开发,那Java这门语言好好的研究,熟练掌握,源码读一读。各种框架的使用、原理,什么场景使用什么技术做为解决方案,起码你掌握了这些,面试能过吧。接下来,有时间、有精力学学其他语言,多门语言,多一种优势吧。再说了,现在貌似又回到了过去,全栈这次词,提的越来越多了。当年诺基亚很火的时候,一大堆搞塞班开发的,后面诺基亚哑火了,你如果还坚守塞班,不学学安卓、ios 是不是基本就GG了?


chatgpt,真心强大,完全可以替代初级工程师,你还有什么理由,不提高自己的技术力呢!


沟通


有的时候,沟通比技术更重要。有人的地方就有江湖,江湖是什么,江湖是人情世故,不是打打杀杀。如果说技术能力是智商,那语言艺术就是情商。会说话,对于程序员来说,真的是硬伤,大部分程序员的世界,都是机器的世界, 0 1 世界 除了 0 1 哪来的2啊?为什么说干技术的干不过写ppt的,因为人家把你的功劳抢了啊,会说话,会汇报啊,别看不起这些人,这也是一种软实力。


没事多去领导面前刷刷脸,混个脸熟,这个比起你做了多少个需求,重要很多。起码提起你的时候,领导知道这个人是谁。


不要认死理,程序员的世界 不应该只有 0 1,应该有更多可能性,2 3 4 5...都可以有。领导就算放个屁,你也要觉得是香的(有点跪舔的意思了,但事实就是这么残酷,虽然我也没做到)领导的面子一定要给,好处不知道有没有,起码没坏处,起码领导觉得你态度端正,执行力强。


跟你工作上接触的人员,多沟通,处理好关系。第一:从别人那里可能得到一些你不知道的有用信息,也有可能收获好基友吧 第二:让周边的人认可你,公司也发展壮大,你的部门大领导可能都没跟你沟通过,如果要了解你,你的信息来源可能是别人对你的评价,有好的有坏的。如果刚好有升职加薪的机会给到你,结果因为别人的几句话,你就被否决了,是不是很亏。所以搞好同事间的关系很重要。


贴近业务


技术都是为业务服务的,再牛逼的技术脱离了业务,只能等死。因为业务不盈利啊,持续亏损,你说老板还留着你过年吗?不要说,我们技术都是按要求按质量根据产品的需求,去做的,系统稳定,线上也没出现过问题,业务不行跟我们技术有什么关系。我在一家公司,业务不好,技术也得分担一部分责任,why?你们开发的东西,是不是没达到业务的目标,这是真实的存在的,产品想的不一定是业务想的,技术理解的也不一定是产品想的。


以前我也一直以为,只要技术好,在哪里不是干。其实真不是这样的,你再好的技术,如果没有一些场景的解决方案,真是纸上谈兵,理论跟落地,差距太大了。比如,阿里云经常提到的的是异地多活,还不几次宕机,造成的损失,真不是金钱能衡量的,理论说的头头是道,但落地的时候,难度远远超过我们的想象,所以要贴近业务,真正做到技术落地,服务好业务


好多大厂出来的人,在细分行业自己创业,其实就是在公司的时候,就很关注业务,技术赋能业务,业务反哺技术。
当你懂技术,懂业务,这样的人,能开源掉吗?


防御性编程


防御性编程,貌似今年技术人应对开猿节流提出的。说是代码不写注释、文档不写,代码能有多烂就多烂,最好写成屎山代码。离开你,换个人根本没法维护,要不重构,别无他法。还有就是不要尽力,能做到100分,我只做到60分就好,剩下的40分是你的保命符,留一点个人上升的空间。带新人,随便带带,教会徒弟饿死师傅不能全部教会他,不然你离走人也不远了。还有很多说法,我就不一一列举了


这种观点,我不支持也不反对,根据自身的实际情况来决定是否使用,过河拆桥的事情也不少。


发展副业


俗话说:猫有九条命,形容猫的生存能力很强,没那么容易死


发展副业,发展副业,发展副业


image.png

发展副业,真的很重要。不要一味只知道工作,拼死累活的给干。不要被公司轻松拿捏你,副业好处如下:



  1. 增加收入:通过副业,你可以获得额外的收入来源,增加财务稳定性,改善生活品质。

  2. 提升技能:副业需要学习和掌握新的技能,这对你的个人和职业发展都是有益的。你可以通过副业开拓新的领域,提高自己的专业能力。

  3. 备用职业选择:副业可以成为你的备用职业选择,当主业遇到困难或变故时,你有一个备选的收入来源和职业发展路径。

  4. 实现梦想和兴趣:副业可以让你追求自己的梦想和兴趣。你可以选择从事自己喜欢的工作,追求个人的创造力和热情。

  5. 社交机会和网络拓展:通过副业,你可以结识更多的人,与更多领域的专业人士交流和合作。这有助于扩大你的人脉和拓展人际关系。


以上观点中,我认为 备用职业选择,这个最重要,起码在公司在开展降本增效,开源节流的时候,你心不慌吧,没有这份工作,我也能活的好好的,起码能保证生活吧


怎么发展副业呢,可能有人看到这,会问,有什么副业途径呢?送外卖算副业吗?只要体力好,能干得了也算。开滴滴算吗?算啊。摆地摊算吗?算啊,除了主业,通过其他赚钱的途径,都是副业。


复盘自己,审视自己,找一个相对适合自己的。我个人也在找,也在尝试。


总结


公司发展到一定阶段,肯定会遇到瓶颈期,如果过不去,开源节流,降本增效,势在必行,公司也要活下去啊,如果公司不在了,全部一起手拉手走,还能怎么办?只是在执行的过程中,人为因素太大了,有能力的可能走了,写ppt、嘴活好的留下了,结果公司的线上服务宕机了,阿里、滴滴宕机事故损失的,比起裁员省的那点钱,简直没可比对性。



雪崩的时候,没有一片雪花是无辜的



作者:柯柏技术笔记
来源:juejin.cn/post/7310787455112495139
收起阅读 »

为啥IoT(物联网)选择了MQTT协议?

物联网设备要实现互相通信,须一套标准通信协议,MQTT(Message Queuing Telemetry Transport)专为物联网设备设计的一套标准消息队列通信协议。使用MQTT协议的IoT设备,可以连接到任何支持MQTT协议的消息队列上,进行通信。 ...
继续阅读 »

物联网设备要实现互相通信,须一套标准通信协议,MQTT(Message Queuing Telemetry Transport)专为物联网设备设计的一套标准消息队列通信协议。使用MQTT协议的IoT设备,可以连接到任何支持MQTT协议的消息队列上,进行通信。



  • 宏观,MQTT和其他MQ传输协议差不多。也是“发布-订阅”消息模型

  • 网络结构,也是C/S架构,IoT设备是客户端,Broker是服务端,客户端与Broker通信进行收发消息


但毕竟使用场景不同,所以,MQTT和普通MQ比,还有很多区别。


1 客户端都运行在IoT设备


1.1 IoT设备特点


① 便宜


最大特点,一个水杯才几十块钱,它上面智能模块成本十块钱最多,再贵就卖不出去。十块钱的智能设备内存都是按KB计算,可能都没有CPU,也不一定有os,整个设备就一个SoC(System on a Chip)。这样的设备就需要通信协议不能复杂,功能不能太多。


② 无线连接


IoT设备一般采用无线连接,很多设备经常移动,导致IoT设备网络连接不稳定,且是常态。


MQTT协议设计上充分考虑这些特点。协议的报文设计极简,惜字如金。协议功能也非常简单,基本就只有:



  • 发布订阅主题

  • 收发消息


这两个最核心功能。为应对网络连接不稳定问题,MQTT增加机制:



  • 心跳机制,可让客户端和服务端双方都能随时掌握当前连接状态,一旦发现连接中断,可尽快重连

  • 会话机制,在服务端来保存会话状态,客户端重连后就可恢复之前会话,继续收发消息。这样,把复杂度转移到服务端,客户端实现更简单


2 服务端高要求


MQTT面临的使用场景中,服务端需支撑海量IoT设备同时在线。


普通的消息队列集群,服务的客户端都运行在性能强大的服务器,所以客户端数量不会特别多。如京东的JMQ集群,日常在线客户端数量大概十万左右,就足够支撑全国人民在京东买买买。


而MQTT使用场景中,需支撑的客户端数量,远不止几万几十万。如北京交通委若要把全市车辆都接入进来,就是个几百万客户端的规模。路侧的摄像头,每家每户的电视、冰箱,每个人随身携带的各种穿戴设备,这些设备规模都是百万、千万级甚至上亿级。


3 不支持点对点通信


MQTT协议的设计目标是支持发布-订阅(Publish-Subscribe)模型,而不是点对点通信。


MQTT的主要特点之一是支持发布者(Publisher)将消息发布到一个主题(Topic),而订阅者(Subscriber)则可以通过订阅相关主题来接收这些消息。这种模型在大规模的分布式系统中具有很好的可扩展性和灵活性。因此,MQTT更适合用于多对多、多对一的通信场景,例如物联网(IoT)应用、消息中间件等。


虽然MQTT的设计目标不是点对点通信,但在实际使用中,你仍然可以通过一些设计来模拟点对点通信。例如,使用不同的主题来模拟点对点通信,或者在应用层进行一些额外的协议和逻辑以实现点对点通信的效果。


一般做法都是,每个客户端都创建一个以自己ID为名字的主题,然后客户端来订阅自己的专属主题,用于接收专门发给这个客户端的消息。即MQTT集群中,主题数量和客户端数量基本是同一量级。


4 MQTT产品选型


如何支持海量在线IoT设备和海量主题,是每个支持MQTT协议的MQ面临最大挑战。也是做MQTT服务端技术选型时,需重点考察技术点。


开源MQTT产品


有些是传统MQ,通过官方或非官方扩展,实现MQTT协议支持。也有一些专门的MQTT Server产品,这些MQTT Server在协议支持层面,大多没问题,性能和稳定性方面也都满足要求。但还没发现能很好支撑海量客户端和主题的开源产品。why?


传统MQ


虽可通过扩展来支持MQTT协议,但整体架构设计之初,并未考虑支撑海量客户端和主题。如RocketMQ元数据保存在NameServer的内存,Kafka是保存在zk,这些存储都不擅长保存大量数据,所以也支撑不了过多客户端和主题。


另外一些开源MQTT Server


很多就没集群功能或集群功能不完善。集群功能做的好的产品,大多都把集群功能放到企业版卖。


所以做MQTT Server技术选型,若你接入IoT设备数量在10w内,可选择开源产品,选型原则和选择普通消息队列一样,优先选择一个流行、熟悉的开源产品即可。


若客户端规模超过10w量级,需支撑这么大规模客户端数量,服务端只有单节点肯定不够,须用集群,并且这集群要支持水平扩容。这时就几乎没开源产品了,此时只能建议选择一些云平台厂商提供的MQTT云服务,价格相对较低,也可选择价格更高商业版MQTT Server。


另外一个选择就是,基于已有开源MQTT Server,通过一些集成和开发,自行构建MQTT集群。


5 构建一个支持海量客户端的MQTT集群


MQTT集群如何支持海量在线的IoT设备?
一般来说,一个MQTT集群它的架构应该是这样的:



从左向右看,首先接入的地址最好是一个域名,这样域名后面可配置多个IP地址做负载均衡,当然这域名不是必需。也可直接连负载均衡器。负载均衡可选F5这种专用的负载均衡硬件,也可Nginx这样软件,只要是四层或支持MQTT协议的七层负载均衡设备,都可。


负载均衡器后面要部署一个Proxy集群


Proxy集群作用



  • 承接海量IoT设备连接

  • 维护与客户端的会话

  • 作为代理,在客户端和Broker之间进行消息转发


在Proxy集群后是Broker集群,负责保存和收发消息。


有的MQTT Server集群架构:



架构中没Proxy。实际上,只是把Proxy和Broker功能集成到一个进程,这两种架构本质没有太大区别。可认为就是同一种架构来分析。


前置Proxy,易解决海量连接问题,由于Proxy可水平扩展,只要用足够多的Proxy节点,就可抗海量客户端同时连接。每个Proxy和每个Broker只用一个连接通信即可,这对每个Broker来说,其连接数量最多不会超过Proxy节点的数量。


Proxy对于会话的处理,可借鉴Tomcat处理会话的两种方式:



  • 将会话保存在Proxy本地,每个Proxy节点都只维护连接到自己的这些客户端的会话。但这要配合负载均衡来使用,负载均衡设备需支持sticky session,保证将相同会话的连接总是转发到同一Proxy节点

  • 将会话保存在一个外置存储集群,如Redis集群或MySQL集群。这样Proxy就可设计成完全无状态,对负载均衡设备也没特殊要求。但这要求外置存储集群具备存储千万级数据能力,同时具有很好性能


如何支持海量主题?


较可行的解决方案,在Proxy集群的后端,部署多组Broker小集群,如可以是多组Kafka小集群,每个小集群只负责存储一部分主题。这样对每个Broker小集群,主题数量就可控制在可接受范围内。由于消息是通过Proxy进行转发,可在Proxy中采用一些像一致性哈希等分片算法,根据主题名称找到对应Broker小集群。这就解决支持海量主题的问题。


UML


Proxy的UML图:


@startuml
package "MQTT Proxy Cluster" {
class MQTTProxy {
+handleIncomingMessage()
+handleOutgoingMessage()
+produceMessage()
+consumeMessage()
}

class Client {
+sendMessage()
+receiveMessage()
}

class Broker {
+publish()
+subscribe()
}

Client --> MQTTProxy
MQTTProxy --> Broker
}
@enduml

@startuml
actor Client
entity MQTTProxy
entity Broker

Client -> MQTTProxy : sendMessage()
activate MQTTProxy
MQTTProxy -> Broker : produceMessage()
deactivate MQTTProxy
@enduml

@startuml
entity MQTTProxy
entity Broker
actor Client

Broker -> MQTTProxy : publishMessage()
activate MQTTProxy
MQTTProxy -> Client : consumeMessage()
deactivate MQTTProxy
@enduml


Proxy收发消息的时序图:



Proxy生产消息流程的时序图:



Proxy消费消息流程的时序图:


image-20231208134111361

6 总结


MQTT是专门为物联网设备设计的一套标准的通信协议。这套协议在消息模型和功能上与普通的消息队列协议是差不多的,最大的区别在于应用场景不同。在物联网应用场景中,IoT设备性能差,网络连接不稳定。服务端面临的挑战主要是,需要支撑海量的客户端和主题。


已有的开源的MQTT产品,对于协议的支持都不错,在客户端数量小于十万级别的情况下,可以选择。对于海量客户端的场景,服务端必须使用集群来支撑,可以选择收费的云服务和企业版产品。也可以选择自行来构建MQTT集群。


自行构建集群,最关键技术点,就是通过前置Proxy集群解决海量连接、会话管理和海量主题:



  • 前置Proxy负责在Broker和客户端之间转发消息,通过这种方式,将海量客户端连接收敛为少量的Proxy与Broker之间的连接,解决了海量客户端连接数的问题

  • 维护会话的实现原理,和Tomcat维护HTTP会话一样

  • 海量主题,可在后端部署多组Broker小集群,每个小集群分担一部分主题这样的方式来解决


参考:



作者:JavaEdge在掘金
来源:juejin.cn/post/7310786611805929499
收起阅读 »

uniapp日常总结--uniapp页面传值

uniapp日常总结--uniapp页面传值在Uniapp中,不同页面之间传值可以通过以下几种方式实现:URL参数传递:可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.params或this.$route.query来获取传递的参数。 ...
继续阅读 »

uniapp日常总结--uniapp页面传值

在Uniapp中,不同页面之间传值可以通过以下几种方式实现:

  1. URL参数传递:

    可以通过在跳转链接中添加参数,然后在目标页面通过this.$route.paramsthis.$route.query来获取传递的参数。


    <uni-link :url="'/pages/targetPage/targetPage?param1=' + value1 + '¶m2=' + value2">跳转到目标页面uni-link>
    // 在目标页面获取参数
    export default {
    mounted() {
    const param1 = this.$route.params.param1;
    const param2 = this.$route.params.param2;
    console.log(param1, param2);
    }
    }
  2. 使用页面参数(Query):

    1. 在触发页面跳转的地方,例如在一个按钮的点击事件中:
    // 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage',
    // 传递的参数,可以是一个对象
    success(res) {
    console.log(res);
    },
    fail(err) {
    console.error(err);
    },
    // 参数传递方式,query 表示通过 URL 参数传递
    // params 表示通过 path 参数传递
    // 一般情况下使用 query 就可以了
    // 使用 params 时,目标页面的路径需要定义成带参数的形式
    // 如 '/pages/targetPage/targetPage/:param1/:param2'
    method: 'query',
    // 要传递的参数
    query: {
    key1: 'value1',
    key2: 'value2'
    }
    });



    //简写 在当前页面的某个事件触发时跳转到目标页面,并传递参数
    uni.navigateTo({
    url: '/pages/targetPage/targetPage?key1=value1&key2=value2',
    });
    1. 在目标页面中,可以通过this.$route.query来获取传递的参数:
export default {
onLoad(query) {
// 获取传递的参数
const key1 = this.$route.query.key1;
const key2 = this.$route.query.key2;

console.log(key1, key2);
},
// 其他页面生命周期或方法等
};

在目标页面的onLoad生命周期中,this.$route.query可以获取到传递的参数。key1key2就是在跳转时传递的参数。如果使用uni.switchTab方法进行页面跳转,是无法直接传递参数的。因为uni.switchTab用于跳转到 tabBar 页面,而 tabBar 页面是在底部显示的固定页面,不支持传递参数。如果需要在 tabBar 页面之间传递参数,可以考虑使用全局变量、本地存储等方式进行参数传递。

  • Vuex状态管理:

    使用Vuex进行全局状态管理,可以在一个页面中修改状态,而在另一个页面中获取最新的状态。

    适用于需要在多个页面之间共享数据的情况。

    如果你的应用使用了Vuex,可以在一个页面的computed属性或methods中触发commit,然后在另一个页面通过this.$store.state获取值。

    在第一个页面:

    // 在页面中触发commit
    this.$store.commit('setValue', value);

    在第二个页面:

    // 在另一个页面获取值
    const value = this.$store.state.value;
    console.log(value);
  • 使用本地存储(Storage):

    使用本地存储(localStorage或uni提供的存储API)将数据存储到本地,然后在另一个页面中读取。适用于需要持久保存数据的情况。如果数据不大,你也可以将数据存储在本地存储中,然后在目标页面读取。

    其中根据使用情景可以使用同步StorageSync或者异步Storage来实现。

    两者存在一定的区别,简单介绍可以查看下方链接:

    uniapp日常总结--setStorageSync和setStorage区别

    同步:使用uni.setStorageSyncuni.getStorageSync等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorageSync('key', value);
    // 在页面B中从本地存储中读取数据
    const value = uni.getStorageSync('key');
    console.log(value);

    异步:使用uni.setStorageuni.getStorage等方法,将数据存储在本地,然后在另一个页面读取。

    // 在页面A中保存数据到本地存储
    uni.setStorage({
    key: 'yourDataKey',
    data: yourData,
    });
    // 在页面B中从本地存储中读取数据
    uni.getStorage({
    key: 'yourDataKey',
    success: function (res) {
    const pageData = res.data;
    },
    });
  • 事件总线:

    使用uni提供的API进行页面传值,如uni.$emituni.$on

    通过事件触发和监听的方式在页面之间传递数据。

    使用Uniapp的事件总线来进行组件之间的通信。在发送组件中,使用uni.$emit触发一个自定义事件,并在接收组件中使用uni.$on监听这个事件。

    在发送组件:

    uni.$emit('customEvent', data);

    在接收组件:

    uni.$on('customEvent', (data) => {
    console.log(data);
    });
  • 应用全局对象:

    通过uni.$app访问应用全局对象,从而在不同页面之间共享数据。

    在发送页面:

    uni.$app.globalData.value = data;

    在接收页面:

    const value = uni.$app.globalData.value;
    console.log(value);
  • URL参数传递对于简单的场景比较方便。Vuex适用于较大的应用状态管理。本地存储适用于需要在页面刷新后仍然保持的简单数据。事件总线方法适用于简单的组件通信。页面参数相对常用于跳转。根据具体需求和应用场景,选择合适的方式进行数据传递。不同的场景可能需要不同的方法。


    作者:狐说狐有理
    来源:juejin.cn/post/7310786618390855717

    收起阅读 »

    TS中,到底用`type`还是`interface`呢?

    web
    结论 直接说结论,用type一把梭即可,除非你要发布到npm,接下来我一一解释 为什么定义对象都要使用type呢? 如图所示,我鼠标悬浮后,并不知道里面是什么东西 只能获取结果时调出代码提示,或者ctrl + 鼠标左键进入,查看类型定义 那么我用type呢? ...
    继续阅读 »

    结论


    直接说结论,用type一把梭即可,除非你要发布到npm,接下来我一一解释


    为什么定义对象都要使用type呢?


    如图所示,我鼠标悬浮后,并不知道里面是什么东西


    只能获取结果时调出代码提示,或者ctrl + 鼠标左键进入,查看类型定义


    那么我用type呢?


    image.png


    可以看到,现在鼠标悬浮能直接查看类型定义了


    这一点是让我最受不了的,所以直接选择type即可


    image.png


    区别


    1. 如何继承



    先看看interface,通过extends关键字



    image.png



    type,则通过交叉类型。不过我认为interface好看点



    image.png


    2. 其他特性



    interface重写时



    • 如果有不同的属性,则会添加;

    • 如果是相同的属性但是类型不同,则会报错;



    这点有好有坏,当你不小心名字重复了,那你就容易出问题


    但同时利于扩展,不过没有人会这么写吧?


    直接去原来的接口添加属性不行吗?


    唯一的场景,就是开发工具库后。别人使用你的工具时,可以为你扩展类型


    image.png


    3. type独有的优势


    除了上面的悬浮能查看具体类型外,type还提供了很多的关键字使用,这是interface不具备的


    比如in关键字,用来枚举类型


    这里我写个删除属性的泛型,和Omit一样的,但是interface不支持


    此外还有很多TS特有的关键字,都只能通过type使用,比如infer


    不过这也符合直觉,因为interface就是定义一个类型而已


    image.png


    经过以上探讨,可以得出一个结论


    平时开发可以都用type


    发布工具库给别人用时,用interface


    作者:寅时码
    来源:juejin.cn/post/7304867327752912906
    收起阅读 »

    个人代码优化技巧

    web
    背景 贴近目前公司的业务,做的增删改查比较多。基本上都是做一些表格的业务系统比较多,因此在写的过程中,都会遇到一些优化的细点,仅供参考,觉得好的可以采纳,不好的欢迎提出来。 一、import按需加载 很多小伙伴肯定不少看到过,性能优化路由要用import(...
    继续阅读 »

    背景



    贴近目前公司的业务,做的增删改查比较多。基本上都是做一些表格的业务系统比较多,因此在写的过程中,都会遇到一些优化的细点,仅供参考,觉得好的可以采纳,不好的欢迎提出来。



    一、import按需加载


    很多小伙伴肯定不少看到过,性能优化路由要用import('@/views/xxxx.vue')这样就可以按需加载了。
    本身的vue-cli自动创建出来的时候也会有这一条语句。除了给路由优化之外呢,还有别的场景优化空间呢?那肯定有的啦。那就是结合<component/>自带的组件去一起实现。


    场景呈现


    正常情况下,做一个业务模块,都会分为【基础表】、【业务表】,一般情况下,用户维护好了基础表信息了之后,剩下的就是信息交叉复用,有可能在某个业务页面,我需要点击某个按钮后根据某个值到某个基础表的页面进行搜索信息,并勾选行信息。


    <template>
    <div>
    <div class="count" @click="showDynamicComponent">按需加载页面</div>
    <Modal title="动态数据" :visible="visible" @ok="()=>dynamicComponent=null">
    <component :is="dynamicComponent" ref="dynamicRef"/>
    </Modal>
    </div>

    </template>

    <script>
    import { Modal } from 'ant-design-vue'
    export default {
    components: {
    Modal
    },
    data() {
    return {
    dynamicComponent: null,
    visible: false
    };
    },
    methods: {
    showDynamicComponent() {
    this.visible = true
    import('@/views/baseInfo/a.vue').then(res=>{
    this.dynamicComponent = res.module
    })
    },
    },
    };
    </script>


    最后通过this.$refs.dynamicRef这个方式来拿到组件的信息和方法。




    二、表格维护


    因为公司的做的系统报表比较多,这时候表头的数量和表单都是比较多的,恰好公司使用的UI框架是ant-design-vue,表头的数量达到40-50的时候,那么代码的占用函数就很大,而且在产品经常在开发阶段,定义的表头位置顺序变来变去,于是为了方便维护和开发,我封装成一个函数,我还没考虑过这个性能损耗问题,但是维护起来确实方便很多。


    业务场景


    举个例子,一个表头有用户姓名年龄,正常情况下,ant-design-vue表头是这么写的。


    const columns = [{
    dataIndex: 'username',
    title: '用户'
    }, {
    dataIndex: 'realname',
    title: '姓名'
    }, {
    dataIndex: 'age',
    title: '年龄'
    }]

    数据少的时候,维护没有什么问题,倒是表头数量很多的时候,可能40-50个,一百个?大概是这个数,看起来就很费劲。因为自己业务确实遇到过这个问题,维护起来要么单独创建一个文件大概一百多行一点点找,要么就放在业务代码里,但是无论如何阅读性都很差。所以我想了个办法,把它平铺变成数组形式。


    import { genTableColumnsUtil } from '@/utils/tableJs'
    const columns = genTableColumnsUtil([
    ['username', '用户'],
    ['realname', '姓名'],
    ['age', '年龄'],
    ])

    这时候是不是就好看多了?甚至这个可以做成二级表头,递归做嵌套。那额外的配置项拓展项怎么搞?


    const columns = genTableColumnsUtil([
    ['username', '用户'],
    ['realname', '姓名'],
    ['age', '年龄'],
    ],
    {username: { width: '20%' }})

    我的做法就是在函数里面在穿多一个对象,这样就可以填充上去了。毕竟大多数字段只是展示而已,没有做太多的单元格定制化,如果要定制化,搜索对应的dataIndex就好了。


    image.png


    image.png


    这时候调整顺序的时候,还有定制化的时候就阅读性就好很多。




    三、依赖包单独抽离


    性能优化不只是代码层面的优化,除了nginx配置http2,gzip...
    单独抽离chunk包也可以达到加快访问速度的目的。


    业务场景


    // 在vue.config.js加入这段代码
    module.exports = {
    configureWebpack: config => {
    // 分包,打包时将node_modules中的代码单独打包成一个chunk
    config.optimization.splitChunks = {
    maxInitialRequests: Infinity, // 一个入口最大的并行请求数,默认为3
    minSize: 0, // 一个入口最小的请求数,默认为0
    chunks: 'all', // async只针对异步chunk生效,all针对所有chunk生效,initial只针对初始chunk生效
    cacheGr0ups: { // 这里开始设置缓存的 chunks
    packVendor: { // key 为entry中定义的 入口名称
    test: /[\\/]node_modules[\\/]/, // 正则规则验证,如果符合就提取 chunk
    name(module) {
    const packageName = module.context.match(
    /[\\/]node_modules[\\/](.*?)([\\/]|$)/
    )[1]
    return `${packageName.replace('@', '')}`
    }
    }
    }
    }
    }
    }
    }

    最后在打包完了之后。可以查看一下。


    image.png




    四、thread-loader打包


    业务场景


    充分利用cpu核心数,进行快速打包、其实我也没感觉有多快。


     // 开启多线程打包
    config.module
    .rule('js')
    .test(/\.js$/)
    .use('thread-loader')
    .loader('thread-loader')
    .options({
    // worker使用cpu核心数减1
    workers: require('os').cpus().length - 1,
    // 设置cacheDirectory
    cacheDirectory: '.cache/thread-loader'
    })
    .end()



    五、ECharts按需使用


    业务场景


    数字化是趋势,图形可视化在所难免,但往往我们有时候没做那么复杂的图形,可能只用到了饼图和柱状图,或者别的,怎么样都用不完ECharts更多的图形,ECharts是大家常用的图形化之一,ECharts第一步教程都是告诉我们,在
    vue文件里


    import * as echarts from 'echarts'

    殊不知,我们用不到的图形都加载进来,打包的时候就可以看到,这玩意,3M多。
    所以,看情况来加载图形配置


    import * as echarts from 'echarts/core'

    import { BarChart, LineChart, PieChart } from 'echarts/charts'

    import {
    TitleComponent,
    TooltipComponent,
    GridComponent,
    LegendComponent,
    ToolboxComponent,
    } from 'echarts/components'

    import { CanvasRenderer } from 'echarts/renderers'

    echarts.use([
    TitleComponent,
    TooltipComponent,
    GridComponent,
    BarChart,
    LineChart,
    PieChart,
    CanvasRenderer,
    LegendComponent,
    ToolboxComponent
    ])

    export default echarts

    通过vscode的包插件,可以看到引入的模块大小


    image.png


    作者:hhope
    来源:juejin.cn/post/7309791510873784372
    收起阅读 »

    学会Grid之后,我觉得再也没有我搞不定的布局了

    web
    说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局、双飞翼布局等非常耳熟的名词; 为了实现这些布局我们有很多种实现方案,例如:table布局、float布局、定位布局等,当然现在比较流行的肯定是flex布局; flex布局属...
    继续阅读 »

    说到布局很多人的感觉应该都是恐惧,为此很多人都背过一些很经典的布局方案,例如:圣杯布局双飞翼布局等非常耳熟的名词;


    为了实现这些布局我们有很多种实现方案,例如:table布局float布局定位布局等,当然现在比较流行的肯定是flex布局


    flex布局属于弹性布局,所谓弹性也可以理解为响应式布局,而同为响应式布局的还有Grid布局


    Grid布局是一种二维布局,可以理解为flex布局的升级版,它的出现让我们在布局方面有了更多的选择,废话不多说,下面开始全程高能;



    本篇不会过多介绍grid的基础内容,更多的是一些布局的实现方案和一些小技巧;



    常见布局


    所谓的常见布局只是我们在日常开发中经常会遇到的布局,例如:圣杯布局双飞翼布局这种名词我个人觉得不用太过于去在意;


    因为这类布局最后的解释都会变成几行几列,内容在哪一行哪一列,而这些就非常直观的对标了grid的特性;


    接下来我们来一起看看一些非常常见的布局,并且用grid来实现;


    1. 顶部 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Headerdiv>
    <div class="content">Contentdiv>
    body>
    html>


    2. 顶部 + 内容 + 底部


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Headerdiv>
    <div class="content">Contentdiv>
    <div class="footer">Footerdiv>
    body>
    html>


    这里示例和上面的示例唯一的区别就是多了一个footer,但是我们可以看到代码并没有多少变化,这就是grid的强大之处;


    可以看码上掘金的效果,这里的内容区域是单独滚动的,从而实现了headerfooter固定,内容区域滚动的效果;


    实现这个效果也非常简单,只需要在content上加上overflow: auto即可;




    3. 左侧 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Leftdiv>
    <div class="content">Contentdiv>
    body>
    html>


    这个示例效果其实和第一个是类似的,只不过是把grid-template-rows换成了grid-template-columns,这里就不提供码上掘金的示例了;



    4. 顶部 + 左侧 + 内容


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Headerdiv>
    <div class="left">Leftdiv>
    <div class="content">Contentdiv>
    body>
    html>


    这个示例不同点在于header占据了两列,这里我们可以使用grid-column来实现,grid-column的值是start / end,例如:1 / 3表示从第一列到第三列;


    如果确定这一列是占满整行的,那么我们可以使用1 / -1来表示,这样如果后续变成顶部 + 左侧 + 内容 + 右侧的布局,那么header就不需要修改了;



    5. 顶部 + 左侧 + 内容 + 底部


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Headerdiv>
    <div class="left">Leftdiv>
    <div class="content">Contentdiv>
    <div class="footer">Footerdiv>
    body>
    html>


    这个示例的小技巧是使用了grid-template-areas,使用这个属性可以让我们通过代码来直观的看到布局的样式;


    这里的值是一个字符串,每一行代表一行,每个字符代表一列,例如:"header header"表示第一行的两列都是header,这里的header是我们自己定义的,可以是任意值;


    定义好了之后就可以在对应的元素上使用grid-area来指定对应的区域,这里的值就是我们在grid-template-areas中定义的值;





    码上掘金中的效果可以看到,左侧的菜单和内容区域都是单独滚动的,这里的实现方式和第二个示例是一样的,只需要需要滚动的元素上加上overflow: auto即可;



    响应式布局


    响应式布局指的是页面的布局会随着屏幕的大小而变化,这里的变化可以是内容区域大小可以自动调整,也可以是页面布局随着屏幕大小进行自动调整;


    这里我就用掘金的页面来举例,这里只提供一个思路,所以不会像上面那样提供那么多示例;


    1. 基础布局实现


    移动端布局


    image.png



    以移动端的效果开始,掘金的移动端的布局就是上面的效果,这里我简单的将页面分为了三个部分,分别是headernavigationcontent


    注:这里不是要100%还原掘金的页面,只是为了演示grid布局,具体页面结构和最后实现的效果会有非常大的差异,这里只会实现一些基础的布局;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    body>
    html>

    iPad布局


    image.png



    这里是需要借助媒体查询来实现的,在媒体查询中只需要调整一下grid-template-rowsgrid-template-columns的值即可;


    由于这里的效果是上面一个的延伸,为了阅读体验会移除上面相关的css代码,只保留需要修改的代码;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    PC端布局


    image.png



    和上面处理方式相同,由于Navigation移动到了左侧,所以还要额外的修改一下grid-template-areas的值;


    这里就可以体现grid的强大之处了,我们可以简单的修改grid-template-areas就可以实现一个完全不同的布局,而且代码量非常少;


    为了居中显示内容,我们需要在左右两侧加上一些空白区域,可以简单的使用.来实现,这里的.表示一个空白区域;


    由于内容的宽度基本上是固定的,所以留白区域简单的使用1fr进行占位即可,这样就可以平均的分配剩余的空间;



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    完善一些细节


    QQ录屏20231210000552.gif



    最终的布局大概就是上图这样,这里主要处理的各个版块的间距和响应式内容区域的大小,这里的处理方式主要是使用column-gap和一个空的区域进行占位来实现的;


    这里的column-gap表示列与列之间的间距,值可以是pxemrem等基本的长度属性值,也可以使用计算函数,但是不能使用弹性值fr


    空区域进行占位留间距其实我并不推荐,这里只是演示grid布局可以实现的一些功能,具体的实现方式还是要根据实际情况来定,这里我更推荐使用margin来实现;


    完整代码如下:



    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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">Headerdiv>
    <div class="navigation">Navigationdiv>
    <div class="content">Contentdiv>
    <div class="right">Rightdiv>
    body>
    html>

    简单复刻版




    码上掘金上的效果来说已经完成了大部分的布局和一些效果,目前来说就是还差一些交互,还有一些细节上的处理,感兴趣的同学可以自行完善;



    异型布局


    异性布局指的是页面中的元素不是按照常规的流式布局进行排版,又或者说不规则的布局,这里我简单的列出几个布局,来看看grid是如何实现的;


    1. 照片墙


    image.png


    html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Titletitle>
    <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. 漫画效果


    image.png




    在漫画中有很多类似这种不规则的漫画框,如果使用定位的话,那么代码量会非常大,而且还需要计算一些位置,使用grid的话就非常简单了;


    可以看到这里还有一个气泡文字显示的效果,按照页面书写顺序,气泡是不会显示的,这里我们可以使用z-index来实现,这里的z-index值越大,元素就越靠前;


    而且气泡文字效果也是通过grid来进行排版的,并没有使用其他的布局来实现,代码量也并不多,感兴趣的同学可以自行查看;



    3. 画报效果


    image.png




    在一个画报中,我们经常会看到文字和图片混合排版的效果,由于这里直接使用的是渐变的背景,而且我的文字都是随意进行排列的,没有什么规律,所以看起来会比较混乱;


    在画报效果中看似文字排版非常混乱不规则,但是实际上设计师在设计的时候也是会划分区域的,当然用定位也是没问题的,但是使用grid的话就会简单很多;


    我这里将页面划分为12 * 12区域的网格,然后依次对不同的元素进行单独排列和样式的设置;



    流式布局


    流式布局指的是页面的内容会随着屏幕的大小而变化,流式布局也可以理解为响应式布局;


    但是不同于响应式布局的是,流式布局的布局不会像响应式布局那样发生变化,只是内容会随着轴进行流动;


    通常这种指的是grid-template-columns: repeat(auto-fit, minmax(0, 1fr))这种;


    直接看效果:


    QQ录屏20231210222012.gif




    这里有两个关键字,一个是auto-fit,还有一个是auto-fill,在行为上它们是相同的,不同的是它们在网格创建的不同,



    image.png



    就像上面图中看到的一样,使用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
    收起阅读 »

    相比拼多多市值一路狂奔,阿里巴巴究竟输在哪里?

    相信最近在互联网界最热门的事情就是拼多多的市值超过了阿里。 这个事情为什么有这么大的轰动?想当年阿里可是电商的一个阿里巴巴在2017年的时候市值超过了亚马逊。成为了中国乃至世界互联网电商界不可小觑的力量。 然而短短在八年的时间里面万亿市值就被一个在2015年...
    继续阅读 »

    相信最近在互联网界最热门的事情就是拼多多的市值超过了阿里。



    这个事情为什么有这么大的轰动?想当年阿里可是电商的一个阿里巴巴在2017年的时候市值超过了亚马逊。成为了中国乃至世界互联网电商界不可小觑的力量。


    然而短短在八年的时间里面万亿市值就被一个在2015年9月份创建的拼多多给反超了,而且据公开数据,阿里现在员工有20万多同学,拼多多只有1万多。


    与此同时,阿里巴巴的国内外电商份额在急速的下降,而拼多多不仅在国内增速一骑绝尘,在全球范围已经开始输出拼多多的低价力量,据统计拼多多已经占据国内26%的市场份额,旗下的temu在欧美澳加如入无人之境,充分让这些外国人了解什么是“兄弟就砍我一刀”的消费降级的乐趣。


    与此同时,令人担忧的是,阿里巴巴目前没有任何能够快速绝地逢生的迹象。


    就如当年百度出了魏则西事件后,我们振聋发聩发馈的一问,谷歌退出中国后的百度到底怎么了?到底发生了什么,一个原来在中国互联网市值排名第一的公司,到底为什么在短短的几年间就到了道德沦丧不争气的地步?


    虽然百度和阿里面对问题性质截然不同,而今天我们相似的也可以问一句,为什么阿里巴巴到了目前这个境遇?


    网上已经有很多文章来谈论为什么阿里巴巴会被拼多多反超。理由有很多,比如说在战略上的决策失误,阿里坚持了新零售升级的消费主义,比如说拼多多非常聚焦收敛,而阿里巴巴投资收购了不少业务,业务分散,比如说拼多多比较低调,而阿里巴巴出了很多公关事件。比如说阿里巴巴的内部味道过重,而拼多多就是拿员工时间换钱不谈价值观。


    这些固然都是阿里巴巴为什么现在业绩下滑被拼多多反超的原因。但今天我想换一个思路来用拟人的方式,从情绪上所以说根本原因。


    我觉得根本原因就是阿里巴巴太过于傲慢。也就是傲慢这个本质上的原因,才导致了一系列战略决策的失误,用人的失误,情报的失误。


    为什么这么说?在原来一个庞然大物下居然还能存在一个拼多多能够釜底抽薪,难道阿里巴没有任何人能够觉察到拼多多从零开始的这种号召力和变革力吗?难道阿里巴巴没有牛逼的人物能够反制拼多多吗?难道阿里巴巴没有人才了吗?


    显然这些都是否定的,成立于1999年,历时已经24年,阿里巴巴能够从零做到全球目前的这个地步,意味着它就有一个强大的管理团队,强大的人才以及强大的组织力,那为什么依然没有阻止拼多多的起来呢?


    这个企业竞争形势变化在《创新者的窘境》里面说的非常的明确,这个就是所谓的小公司对大公司的颠覆式创新。也就是一个小公司,往往能够从新的维度,新的方向,形成快速的行动力,终究在不起眼的地方,再造一个大市场。往往小公司利用到了更新的一些理念和价值,使得小公司能够在短时间内在大公司的眼皮底下快速的形成大规模的创新力量,从小起步,犹如积蓄力量的蚂蚁,最终掀翻步履维艰的大象。


    而大公司往往在成功之后就会有自己的一个路径依赖。在路径依赖的情况下的话,就往往会主观上忽视掉最弱小的竞争对手,甚至完全不把竞争对手当回事儿。


    换一句话来说就是公司太大,大的极度傲慢,历史上已经有数见不鲜的例子,比如刚刚倒下的全球手机霸主诺基亚。


    在拼多多刚刚起来的时候,内部同学已经有很多人都感知到拼多多的竞争。但是阿里巴巴犹如一条非常大的一个航船,在让商家没有难做的生意愿景上,新零售升级,在双十一GMV增长方向上无法停止。于是慢慢导致高管乃至最底层的执行的人都有意无意忽略了拼多多的增长。


    我之前和一个天猫的研究生同学吐槽他的双十一优惠券计算复杂度之高。我这位同学骄傲的告诉我们,只是我不是天猫的目标客户,他说,其实你不知道有多少客户非常喜欢我们的搭楼游戏,喜欢我们的复杂的优惠计算,说完一脸傲娇。



    于是就在这种情况下,拼多多一路狂奔,简单粗暴的后续界面,简单粗暴的退款逻辑,一刀刀砍向了原来忠诚的淘宝用户。从开始抢走了淘宝的低端羊毛用户,到抢走了淘宝的中间用户,直到现在的强力补贴,连高端消费用户都抢走了。


    拼多多说秉承的客户第一理念,让所有消费者如沐春风,在被淘宝商家歧视的价值主张里,好像找到了另外一个发泄口。


    这个就是颠覆式创新的力量。也是无数大公司单纯的血的教训。只不过诸如淘宝这样的大公司还是依然没有躲过这样的故事。


    当然我依然相信阿里巴巴是一个有韧性,有希望的公司。毕竟阿里巴巴原来就从最艰难的路子里面杀出一条血路。此次确实是淘宝面对的最大危机,但我相信也是新希望的开始。


    正如微软CEO纳德拉所说,人们往往高估了短期的影响力而低估了长期的影响力。


    胜者坚持长期主义,鹿死谁手,犹未知之。


    作者:ali老蒋
    来源:juejin.cn/post/7308643782376570934
    收起阅读 »

    今天还要用 React 吗:利弊权衡

    web
    免责声明 本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 The Pros and Cons of Using React Today。 在过去的十年中,React 因辅助整个行业的开发者构建顶尖 UI 的超能力,为大家所熟知。...
    继续阅读 »




    免责声明


    本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 The Pros and Cons of Using React Today



    00-wall.jpg


    在过去的十年中,React 因辅助整个行业的开发者构建顶尖 UI 的超能力,为大家所熟知。


    本文在 2023 年底和 2024 年对 React 进行了深入而平衡的展望。我们将看看它值得称道的优势、明显的短板,以及对当今开发者的可靠性。


    让我们先从 React 与众不同的创新功能开始,然后再将注意力转向它给开发者带来的挑战。


    React JS 是什么鬼物?


    ReactJS 是一个组件筑基的 JS 库,最初由 Facebook 创建,并在十年前发布。该库简化了开发者创建交互式 UI,同时有效管理组件状态。它能够为复杂 App 编写多个组件,而不会丢失它们在浏览器的 DOM(文档对象模型)中的状态,这对一大坨开发者而言是一个明显的福利。


    虽然 React 主要是一个用于 Web App 的工具,但其多功能性通过 React Native 扩展到移动 App 开发。这个强大的开源库允许开发 Android、iOS 和 Windows App,展示了 React 跨平台开发的灵活性。


    React 生态系统


    React 最大的资源之一是其庞大的生态系统,其中充满了第三方库和工具,极大地扩展了其功能。这对于路线规划 App 等复杂项目尤其有利,这些项目通常依赖集成大量外部服务,比如地图 API 和路径算法。


    React 的灵活性和与各种第三方服务的兼容性简化了集成过程,允许开发者使用高级功能增强其 App,而不会产生过多的开销。


    其核心是基本的库和工具,比如 React Router,用于 SPA(单页应用程序)中的动态路由,确保无缝的 UX(用户体验)过渡。Redux 是一个关键的状态管理工具,它为状态创建了一个中心化 store,使不同的组件能够一致地访问和更新它,这在大型 App 中尤为重要。


    React.js:不仅仅是复杂性


    虽然 React 在 UI 创建方面表现出色,但在状态管理和 SEO 优化等领域存在不足。幸运的是,更广泛的 JS 生态系统提供了许多工具,这些工具好处多多,比如更简化的状态管理方案、通过 SSR(服务器端渲染)增强的 SEO 和数据库管理。让我们瞄一下 React 若干更突出的集成选项。


    对于那些寻求更简单替代方案的人而言,MobX 提供了一种直观的状态管理方案,并且样板更少。此外,Next.js 通过提供 SSR 和 SSG(静态站点生成)解决了客户端渲染 App 的当前 SEO 限制。在开发和测试方面,CRA(Create React App)简化了设置新前端构建管道的过程,使开发者能够立即开始运行,而不会受到配置的困扰。


    同时,Storybook 作为一个 UI 开发环境,开发者可以在其中独立可视化其 UI 组件的不同状态。Jest 在单元和快照测试中很受欢迎,它与 React 无缝集成。由 Airbnb 开发的 Enzyme 是一个测试工具,它简化了断言、操作和遍历 React 组件输出的过程。


    额外的库和工具进一步丰富了 React 生态系统;Material-UI 和 Ant Design 提供了全面的 UI 框架,可以满足美学和功能要求,而 Axios 则提供了一个 Promise 筑基的 HTTP 客户端来发送 HTTP 请求。React Query 简化了获取、缓存和更新异步数据的过程,React Helmet 有助于管理对文档头的更改,这对于 SPA 中的 SEO 至关重要。


    React 与其他技术的集成 —— 比如后端框架,包括 Node.js 和 Django;状态管理库,比如 Apollo for GraphQL,增强了其灵活性。如今,开发者甚至可以将 PDF 查看器嵌入到网站中,并大大优化 UX。


    然而,React 的不断发展要求开发者跟上最新的变化和进步,React 为试图制作高质量、可扩展和可维护的 Web App 的开发者提供的无数解决方案抵消了这一挑战。


    React 之利


    React 已经将自己确立为构建动态和响应式 Web App 的关键库,原因如下:


    组件筑基架构


    传统的 JS App 在扩展时经常会遇到状态管理问题。虽然但是,React 提供了复杂的、独立维护的可复用组件,允许开发者在不影响其他页面的情况下更新网页的局部 —— 确保松耦合和协作功能。


    当然,这个概念并不是 React 独有的;举个栗子,Angular 也使用组件作为基本构建块。尽管如此,React 庞大的社区、Meta 的支持和相对丝滑的学习曲线使其成为开发者的最爱。


    开发中的增强定制


    React 的多功能性在构建针对特定业务需求量身定制的 App 时大放异彩。尤其是其组件筑基架构允许在 App 中无缝组装复杂结构。


    举个栗子,在构建集成仪表板时,React 的生态系统有助于将各种模块(比如图表、小部件和实时数据源)集成到一个有凝聚力的 UI 中,使开发者能够打造不仅功能强大,而且直观且具有视觉吸引力的 UX。


    这种强大的适应性恰恰凸显了为什么 React 仍然是旨在创建多功能和健壮的 Web App 的开发者的首选。


    面向未来的开发者选项


    React 面向未来的特性是它为开发者提供的最引人注目的优势之一。React 灵活的架构迎合了当前的 Web 开发需求,同时也无缝地适应了将塑造行业近期的新兴技术。


    值得注意的是,机器学习正在向 Web 开发领域取得重大进展,2022 年全球 ML 市场价值已经达到 210 亿美元,这凸显了 React 面向未来的特性以及与此类进步相协调的能力的重要性。


    其中一个比较突出的例子是 TensorFlow.js,一个用于图像和模式识别的 ML 库。同样,React 允许集成 ML 驱动的聊天机器人甚至推荐功能。此外,WebAssembly 可以帮助允许用 Rust、Python 或 C++ 编码的 ML 应用程序存在于原生 App 中。


    用于状态管理的 Redux


    在 SPA 中,多个组件驻留在单个页面上,管理状态和组件间通信很快就会变得具有挑战性 —— 这正是 Redux for React 的亮点。


    作为 React 不可或缺的一部分,它充当“管理器”,确保组件之间的数据流一致且准确,集中状态管理并促进组件自治性,显着提高数据稳定性和 UX。


    React 之弊


    虽然 React 为不同技能水平的开发者提供了许多优势,但它并非没有各自的缺点,包括以下内容:



    • 复杂的概念和高级模式:React 引入了若干高级概念和模式,这些概念和模式一开始可能会让初学者不知所措。要了解 JSX、组件、props、状态管理、生命周期方法和钩子,需要扎实掌握 JS 基础知识。

    • 与其他技术的集成复杂性:React 经常与其他工具和技术结合使用——如 Redux、React Router 和各种中间件 —— 对于新手来说,了解如何将这些技术与 React 集成可能极具挑战。

    • 非 JS 开发者的障碍:React 对 JS 的严重依赖对于不精通 JS 的开发者而言可能是一个障碍。虽然 JS 是一种通用且广泛使用的语言,但来自不同编程背景的开发者可能会发现适应 JS 的范式和 React 的使用方式极具挑战。

    • 不是一个成熟的框架:React 主要处理 MVC 的“视图”部分,也称为模型视图控制器架构。对于“模型”和“控制器”,需要额外的库,与 Angular 等功能齐全的框架相比,这最终会导致结构化程度较低且可能更加混乱的代码。

    • 代码膨胀:React.js 的特点是其大量的库和依赖需求,因其臃肿的 App 而臭名昭著。这种膨胀通常表现为较长的加载时间,尤其是在复杂的项目中。该框架的结构严重依赖其虚拟 DOM,即使是次要功能也需要加载整个库,这大大增加了 App 的数字足迹并降低了其效率。

    • 在传统设备和弱网络上的性能下降:React.js App 的性能在较旧的硬件和互联网连接较差的地区往往会下降。这主要是由于框架的客户端渲染模型和密集的 JS 处理。这些因素可能会导致渲染交互式元素的延迟,这在计算能力有限的设备或带宽有限的环境中尤为明显,这会对 UX 产生不利影响。


    最终裁决


    随着 Web 开发领域的不断发展,React 的灵活性和强大的生态系统使其处于有利地位。它将继续使开发者能够将尖端功能无缝地整合到其 App 中。虽然但是,虽然 React 为开发者提供了很多好处,但它仍然有其缺点。


    React 的复杂性和对高级 JS 概念的依赖带来了曲折的学习曲线,尤其是对于新手或尚未精通 JS 的人。它还主要解决了 MVC 架构的“视图”方面,需要额外的工具来进行完整的 App 开发,这可能会导致更复杂和结构化更少的代码库。


    尽管存在这些挑战,但庞大而活跃的 React 社区在其持续发展中发挥着至关重要的作用。在可预见的未来,它将继续成为 Web 和移动 App 开发的关键库。


    作者:人猫神话
    来源:juejin.cn/post/7310033153905164303
    收起阅读 »

    一个30岁老前端的人生经历(学习+工作+婚姻+孩子),给迷茫的朋友一点激励。

    web
    前言 我93年的,还差几天就到30周岁生日。做前端开发大概也有6、7年,算是老前端了。 2023 写作 在掘金看文章已经很多年了,学到了很多东西。今年三月份在掘金写下了第一篇文章,开始的想法是把自己的开发笔记整理一下写成文章,来巩固一下自己的知识,没想到在写文...
    继续阅读 »

    前言


    我93年的,还差几天就到30周岁生日。做前端开发大概也有6、7年,算是老前端了。


    2023


    写作


    在掘金看文章已经很多年了,学到了很多东西。今年三月份在掘金写下了第一篇文章,开始的想法是把自己的开发笔记整理一下写成文章,来巩固一下自己的知识,没想到在写文章分享的过程中帮助了很多人,也得到了很多人认可,让我写文章分享的动力也越来越强,基本每周都会写一篇,闲在没事的时候基本都是在构思下一篇文章写什么。希望明年能写更多的文章,帮助更多的人。


    写作给我带来了什么



    1. 巩固知识。把自己学到的东西,分享出去,印象更深刻了。

    2. 更多的机会。写作过程中,有大厂私信我,给我一些面试机会,这个时候的面试机会还是很宝贵的,不过因为一些原因都拒绝了。

    3. 快乐。很多人私信我说很感谢我分享的东西,让他们学到了很多东西。看到这些感谢的话,自己得到认可,还是很开心的。

    4. 钱。写文章参与掘金的金石计划活动,陆陆续续差不多获得了接近1000的收入,每次拿到钱,带着老婆孩子去吃顿好的,还是不错的。


    降薪


    今年公司受大环境影响,裁了一部分人,留下来的人也都降薪了。开始有点接受不了,想跑,但是因为是在上海嘉定郊区,附近找不到好的工作,最近的也要1个小时的地铁,并且小孩刚上一年级,上海基本不可能跨区转校,所以打消了换工作的念头,只能在公司了干下去,相信公司会好起来的。


    家庭


    看了上面肯定有倔友怀疑,30岁小孩怎么能上一年级?不错我大四结的婚,还是奉子成婚,所以早早的有了孩子。


    因为自己小时候是留守儿童,不想让自己孩子过留守儿童的生活,所以孩子一直是和我们在一起,记得我刚出来工作的时候,一个月才3500,我老婆全职带孩子,这些薪资刚够花销,生活过的比较拮据,有时候还要靠我父母接济。


    现在收入稍微好了一些,但是我们还没有买房子,存款也不多,在别人看来压力可能会有点大,但是我心比较大,平时消费欲望也比较低,对钱不是那么渴望,一家人在一起也是开开心心的,不过还是想给老婆孩子一个自己的家,努力奋斗吧。


    孩子今年上一年级了,再上一年级后,作业明显变多了,每天都要写到很晚,看着孩子很累,也没办法,不写好作业,第二天老师就会在群里点名说。


    孩子比较调皮,经常在学校里和同学打架,最多的时候,一周被班主任叫了三次家长,有时候是他的错,有时候是别人的错。因为老婆全职在家带孩子,这些都是我老婆处理的,她最近有点焦虑,每天都担惊受怕的,害怕孩子在学校又闯祸,整的我也有点焦虑,工作状态有点差。


    关于孩子打架的事,我和老婆猜测可能是学习压力太大了,每天放学回来就开始写作业,一直写到睡觉,平时还有一些兴趣班要上,玩的时间太少了,积累了很多怨气没地方发泄,所以比较暴躁。现在每天放学后先让他玩半个小时再写作业,并且和他多次沟通,告诉他暴力解决问题是不对的,目前稍微好了一些。有这方面经验的兄弟,可以在评论区指点一下。


    健康


    今年五月份的时候,身体有点不舒服,平时熬夜比较多,人也比较胖,就想着去体检一下,体检结果肝功能有一项转氨酶比正常高三倍,然后到医院做了一次全身体检,抽了9管血,结果还好没啥大问题,可能是脂肪肝导致的转氨酶很高,医生建议要减肥。


    因为平时比较忙,没有时间健身,就搞了个自行车上下班骑,公司离家大概5公里左右,上下班每天骑10公里左右,从6月份买车到现在基本没断过,虽然体重没有降下来,但是精神状态和体力好了不少,以前稍微有点运动量,就气喘吁吁全身冒汗,现在好多了。


    希望倔友们多注意健康,少熬夜,身体才是最重要的。


    2024展望


    关于2024,立几个flag吧



    1. 最少分享40篇文章

    2. 完善fluxy-admin平台,把前后端低代码平台集成进来,做出一个企业级低代码平台开源出去。

    3. 看react源码,并做个专栏分享。每次面试,被面试官问react底层一些东西的时候,回答的都不是很好,就是因为没有彻底了解底层,所以回答的都很片面,明年一定要把react吃透。今年年初买了卡颂大佬的react设计原理书籍,现在在床头吃灰呢。

    4. 减肥


    个人真实经历分享


    给大家分享一下我的个人真实经历,与君共勉。


    我出生在一个很普通的农村家庭,有点小聪明,但是贪玩,高中三年基本都是看电子书度过,天天上课把手机放在书下面,装作看书,实际上都是在看小说,现在回想起来,想不通老师为啥重来没有发现过。


    开始决定好好学习是高三上学期,有次上课和同桌说话,被老师说你自己不学,不要影响别人学习,还说了一些很难听的话(当时我在班里大概倒数10几名的样子,同桌10几名左右。),虽然我贪玩,但是我自尊心比较强,我就不服气,然后上课开始好好听课,后面一次月考竟然考到了10几名,和同桌成绩差不多,然后就开始飘了,上课又开始看小说,下次月考又考的很差,然后难受,又开始好好听课,就这样成绩一会好一会坏,不过拿了几次进步奖,同学笑话我是不是为了拿进去奖,故意退步的。


    真正让我决定好好学习的是高三下学期开学的前一天晚上,我家庭条件不是特别好,而我当时因为中考考的很差,只能上一个学费比较贵的私立高中,高三下学期开学前一天晚上我爸还在为我筹学费(家里没有穷到付不起学费的地步,只是当前家里钱被其他地方占用了,拿不出来。),最终从亲戚那里借了点钱,然后我爸把钱交到我手里,让我明天交学费,看着我爸粗糙的手(我爸是干工地的),这一刻我决定好好学习,不然都对不起这学费。高三下学期上课就没看过手机了,由于底子太差,高考离二本线差了几分,最终上了个三本。


    高考结束,暑假期间迷上了英雄联盟。大学的时候,室友也玩,经常和室友一块包夜,第二天要么旷课在宿舍睡觉,要么在教室最后一排睡觉,导致第一学期就挂了三科,不过后面补考都过了。后面还是继续玩,大二下学期突然觉得不能这样浑浑噩噩了,还不如出去打工,给家里省点学费还能挣点钱(不知道当时为啥有这想法),然后就和父母说了一下,不上学了出去打工,当时是想退学的,还好我好朋友和我说先休学吧,以后后悔还有机会。


    在苏州找了一个工厂,干了一个星期干不下去了,身体上的劳累倒是其次,主要是看不到生活的希望,每天就像一个机器一样,后面就回去上学了,然后学习非常努力,后面还得了奖学金,毕业论文也被评上了优秀论文,也是优秀毕业生,但是毕业学校没有给学位证,只给了毕-业-证,因为挂科超过5门(补考过了也没用,只要挂科超过5门,就完了,我们那一届有不少没有学位证的。),这个政策最开始都不知道,没有学位证后问学校,学校才说的,也不能怨学校,算是自食其果吧。没有学位证对找工作还是有很大影响的,后面有几次面试通过大厂了,因为没有学位证而被拒。


    实习的时候,实习单位和学校是有合作的,学校知道我的事迹也知道我在实习单位表现的不错,所以就邀请我回去给学弟学妹们分享我的经历。当时分享完后,有几个学弟加我微信说,他们现在也是这个状态,我的经历让他们有了重新开始的信心。


    后面的工作之旅也是一路坎坷,不过最后的结果是好的,目前在公司里做前端负责人,收入还不错。工作之旅明年年终总结再和大家分享吧。


    和大家分享我的经历,就是想告诉大家永远不要放弃,只要坚持,就会有希望,同时也想告诉大家每个人都要为自己做过的事负责,因为贪玩我没考上好一点的大学,因为贪玩我没有学位证,但是我后面迷途知返,通过自己的努力,还是得到了一份不错的工作,一个美满的家庭。


    最后


    很喜欢deft在夺冠时说的一句话:我唯一会的仅剩英雄联盟,如果在这条路上我不能成功,那我的人生将没有任何意义。


    而我唯一会的就是写代码,我不一定能成功,但是我想努力做到更好。


    作者:前端小付
    来源:juejin.cn/post/7310549035965890614
    收起阅读 »

    惊!27岁程序媛的一年竟然干了这些事

    hello铁铁们,这是继年中总结之后的又一篇年底回顾。 首先,真的很高兴我的文章能被很多很多的小伙伴看到,每一条评论我都有认真看,有一部分评论也让我对未来产生了新的感悟,也从未想到,会有这么多朋友同我一样有着类似的困惑,也很开心在文章发布之后收获了志同道合的伙...
    继续阅读 »

    hello铁铁们,这是继年中总结之后的又一篇年底回顾。

    首先,真的很高兴我的文章能被很多很多的小伙伴看到,每一条评论我都有认真看,有一部分评论也让我对未来产生了新的感悟,也从未想到,会有这么多朋友同我一样有着类似的困惑,也很开心在文章发布之后收获了志同道合的伙伴。

    这篇文章,我将为我上一篇文章中还未产生结果的问题画上一个句号,并且浅浅思考一下我即将到来的28岁的人生。


    f0c09162ef99abf035453ada2e742d5e.jpeg

    我好像一只温水中的青蛙


    我依然在北京这座城市漂泊,我没有勇气或者说没有足够的能力与底气回到老家扎根,我依然过着普普通通的周中上班周末摆烂的人生,两点一线的在舒适圈中挣扎,不愿逃脱。

    再加上现在经济依然低迷,对于无房无车的普通的我来说,我依然不敢潇洒的离开这个岗位,经常和朋友说“等什么时候裁了我给我n+1”,但是如果那一天真的到来,我依然不知道,我的未来在哪

    (虽然但是,不思考人生时候的自己还是蛮开朗的)


    给2023画上句号


    我很喜欢27岁的我。


    因为这一年没什么坏消息,平淡的生活很快乐,偶尔周六和朋友聚会,周日在家休息,偶尔和朋友去临近的城市旅游,和闺蜜吐槽公司发生的种种奇葩事件,和同事的关系还算融洽,家人也依然健康。
    唯一的遗憾就是持续性的鼻炎让我查出了猫子过敏,不得不把我养了三年的猫子送回了老家,好在猫子回家之后心情还算不错,家人也很喜欢它。

    附上一张在东北看雪的傻猫


    微信图片_20231208154352.png

    然后我不甘寂寞又养了鼠子,哈哈


    微信图片_20231208160621.png

    学习方面


    1.软考通过!


    这是今年中旬定好了目标的任务,所以一直都有很认真的在复习,虽然今年笔试改成了机试,通过率降低了很多,并且很认真复习的内容好多都没有考到,但努力依然得到了好的结果,紧张的成绩查询之后带来了好的消息,真的很令人开心!
    我依然相信,努力就会有结果,哪怕最后可能不尽人意,但一定会有所收获。
    由于明年几个计划的优先级较高所以还没有考高级的打算,另外考过的小伙伴透露一下是不是真的很难啊(听说高级上了好几个level)


    QQ图片20231208155133.jpg

    2.开始参与开源项目


    无聊的生活每天枯燥的工作,通过上一篇文章有一个大哥联系了我,邀请我参与他的开源项目,因为他的项目体系已经很完整,所以我毫不犹豫的参与到其中去,接触到了一些新的思想,对于很久没有长进的我来说,做做新东西的免费劳动力也是很开心的事(虽然也没贡献几行代码哈哈)。


    3.接触web3、区块链


    开源的大哥还带我接触了web3,区块链等领域,也有机会一起合作区块链开发的兼职项目(虽然目前还是启动阶段没我啥事)。


    4.开始学习java


    程序员这个行业太卷了,与其不如别人卷我不如我先发制人,于是我决定努力向全栈发展,哪怕不能全栈也总归比只会前端要好,所以在软考结束之后,我就开始了java学习计划,目前还处于基础语法学习阶段,本来这个任务是放在明年开始的,因为有了一定的空闲时间所以比计划有所提前。


    5.依然坚持每周1~2道LeetCode


    leetcode依然在刷,但是已经没什么精力去突破困难的题了,而且由于长时间不动脑子,导致好多中等难度的题也不会做了,所以这方面还是要维持住,不然退步的实在太快。


    最后附上每天学习进度表,做计划真的很有用,不然我每天真的什么都不想做,每当计划完成打上√的时候还是会有成就感。


    微信图片_20231208160823.png
    微信图片_20231208160917.png
    微信图片_20231208161127.png

    好多人问我这个日历工具是什么,其实就是excel找个模板,然后自己写内容,不够智能但是这种自己逐渐调整出来的时常能带来更多思考。


    6.天气转凉,骑行搁置


    由于天冷,我的车锁都被冻得梆硬,虽然是东北人但是我怕冷,后来就没有再骑行,等春暖花开时我一定继续践行我的骑行计划!


    2024目标


    28岁大龄女中年坚持在程序员团队中浑水摸鱼,LeetCode继续刷,计划表继续做,坚持学习,励志坚决不做家庭主妇!


    学习java,向全栈发展


    坚持每天学习英语


    持续参与开源,接触兼职工作


    先附上12月定的计划表,如果有新的的临时计划依然会做补充。


    微信图片_20231208165407.png

    flag还是少立,最好一段时间认真做好一件事,有计划的践行目标,完成的概率会提升许多。


    真想永远做温水中的青蛙不被煮烂


    好像已经很努力了,但是依然很普通,逐渐饱和的市场让我这种螺丝钉即使做点什么努力也不会改变任何结果,我知道,这个世界上有很多很多更加优秀的人。

    我好像有一些居安思危的改变,但骨子里依然没变,我不知道当危险来临的那天我该怎么办,我预测不到未来,比如每次面试时面试官问我“未来的规划”时,我还是不知道如何回答他,就好像在我很小的时候立过的那些flag一个都没有实现,但是我现在依然不温不火的过着还算不错的人生,也许这就是普通人的一生

    确实,永远有人更好,当下便是最好。

    加油吧,为了梦想,曾经立过的flag,哪怕他们不会实现,也给日子一点奔头,一起努力吧,加油!


    2648ff5ff16d83fcde8e1f6117c4f472.jpeg
    作者:毛毛裤
    来源:juejin.cn/post/7310560146623021090
    收起阅读 »

    客户要一次加载上千张轮播图如何丝滑地实现?不用第三方(没找到),十来行核心代码实现

    web
    引言 最近再做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图 这不得卡死啊,现成且现代化好用的第三方库没找到 于是又到了我最爱的实现源码环节,核心代码十多行即可 底部有源码 思路 压缩图片 轮播只需要两张,来回交换,用点障眼法就是无缝了 批...
    继续阅读 »

    引言


    最近再做3D的大屏,然后客户发来1G的图片压缩包,让我全放上去当轮播图


    这不得卡死啊,现成且现代化好用的第三方库没找到


    于是又到了我最爱的实现源码环节,核心代码十多行即可


    底部有源码


    思路



    • 压缩图片

    • 轮播只需要两张,来回交换,用点障眼法就是无缝了


    批量压缩


    这个用canvas就能实现,于是我写了个HTML来批量压缩


    canvas转存图片时,使用jpeg | webp的格式即可压缩,MDN上有


    使用canvas.toBlob可以压缩更多空间,这个不是重点,提一嘴而已


    image.png


    虚拟无缝轮播实现


    直接一张动图,清晰明了的解决问题


    t.gif


    是不是看起来很简单,加载前两张图,当动画结束时,改变移动那张图的src,同时位移


    再加个overflow: hidden不就行了吗


    e.gif


    编码实现


    HTMLCSS我就不贴了,这个没什么难度,容器固定大小,子元素排列一行


    然后给包装的容器添加个transform即可


    下面是用vue3写的,定义了一个imgIndexArr数组装当前要显示的索引


    _data为计算属性,根据imgIndexArr自动变化,里面放的就是图片


    我们只需要修改imgIndexArr即可实现数据切换


    image.png


    我们需要在动画完成时改变,即添加ontransitionend事件


    当触发next方法,图片滚动停止后,就要执行onTransitionEnd


    定义俩变量,一个代表最左边的图,一个为右边的图


    这里根据变量,决定谁会更新src,并且改变left值实现位移,不好描述啊


    transform会一直向右位移,left值也是,所以他们会形成永动机


    image.png


    HTML里写上他们位移的样式即可自动更新


    image.png


    Bug


    至此,看着已完成,似乎没有任何问题


    但是你把页面隐藏了过后,过一会图片全都不见了,我们打开控制台看看为什么


    可以看到,left停止更新了,也就是说,onTransitionEnd没有执行


    image.png


    transitionend在你浏览器隐藏页面时,就会停止执行


    这时需要在页面隐藏时,停止执行,执行如下代码即可


    /** 离开浏览器时 不会执行`transitionend` 所以要停止 */
    function bindEvent() {
    window.addEventListener('visibilitychange', () => {
    document.hidden
    ? stop()
    : play()
    })
    }

    这时一定有人会说,你这不能往左啊,没有控制组件啊


    如果要往左的话,只需要把两张图轮流交换改成4张图即可


    具体逻辑都是差不多的



    源码: gitee.com/cjl2385/dig…



    作者:寅时码
    来源:juejin.cn/post/7310111620368597011
    收起阅读 »

    前段时间面试了一些人,有这些槽点跟大家说说

    大家好,我是拭心。 前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。 简历书写和自我介绍 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备 去年工作...
    继续阅读 »

    大家好,我是拭心。


    前段时间组里有岗位招人,花了些时间面试,趁着周末把过程中的感悟和槽点总结成文和大家讲讲。


    image.png


    简历书写和自我介绍



    1. 今年的竞争很激烈:找工作的人数量比去年多、平均质量比去年高。裸辞的慎重,要做好和好学校、有大厂经历人竞争的准备


    image.png



    1. 去年工作经历都是小公司的还有几个进了面试,今年基本没有,在 HR 第一关就被刷掉了

    2. 这种情况的,一定要走内推,让内推的人跟 HR 打个招呼:这人技术不错,让用人部门看看符不符合要求

    3. 用人部门筛简历也看学历经历,但更关注这几点:过去做了什么项目、项目经验和岗位对不对口、项目的复杂度怎么样、用到的技术栈如何、他在里面是什么角色

    4. 如果项目经历不太出彩,简历上可以补充些学习博客、GitHub,有这两点的简历我都会点开仔细查看,印象分会好很多

    5. 现在基本都视频面试,面试的时候一定要找个安静的环境、体态认真的回答。最好别用手机,否则会让人觉得不尊重!

    6. 我面过两个神人,一个在马路上边走边视频;另一个聊着聊着进了卫生间,坐在马桶上和我讲话(别问我怎么知道在卫生间的,他努力的声音太大了。。。)

    7. 自我介绍要自然一点,别像背课文一样好吗亲。面试官不是考你背诵,是想多了解你一点,就当普通聊天一样自然点

    8. 介绍的时候不要过于细节,讲重点、结果、数据,细节等问了再说

    9. 准备介绍语的时候问问自己,别人可以得到什么有用的信息、亮点能不能让对方快速 get 到

    10. 实在不知道怎么介绍,翻上去看第 4 点和第 5 点

    11. 出于各种原因,很多面试官在面试前没看过你的简历,在你做自我介绍时,他们也在一心二用 快速地浏览你的简历。所以你的自我介绍最好有吸引人的点,否则很容易被忽略

    12. 你可以这样审视自己的简历和自我介绍:


      a. 整体:是否能清晰的介绍你的学历、工作经历和技能擅长点


      b. 工作经历:是否有可以证明你有能力、有结果的案例,能否从中看出你的能力和思考


      c. 技能擅长点:是否有岗位需要的大部分技能,是否有匹配工作年限的复杂能力,是否有区别于其他人的突出点



    面试问题


    image.png



    1. 根据公司规模、岗位级别、面试轮数和面试官风格,面试的问题各有不同,我们可以把它们简单归类为:项目经历、技能知识点和软素质

    2. 一般公司至少有两轮技术面试 + HR 面试,第一轮面试官由比岗位略高一级的人担任,第二轮面试官由用人部门领导担任

    3. 不同轮数考察侧重点不同。第一轮面试主要确认简历真实性和基础技术能力,所以主要会围绕项目经历和技能知识点;第二轮面试则要确认这个人是否适合岗位、团队,所以更偏重过往经历和软素质


    项目经历


    项目经历就是我们过往做过的项目。


    项目经历是最能体现一个程序员能力的部分,因此面试里大部分时间都在聊这个。


    有朋友可能会说:胡说,为什么我的面试大部分时候都是八股文呢?


    大部分都是八股文有两种可能:要么是初级岗位、要么是你的经历没什么好问的。哦还有第三种可能,面试官不知道问什么,从网上搜的题。


    image.png


    在项目经历上,面试者常见的问题有这些:



    1. 不重要的经历占比过多(比如刚毕业的时候做的简单项目花了半页纸)

    2. 经历普通,没有什么亮点(比如都是不知名项目,项目周期短、复杂度低)

    3. 都是同质化的经历,看不出有成长和沉淀(比如都是 CRUD、if visible else gone)


    出现这种情况,是因为我们没有从面试官的角度思考,不知道面试的时候对方都关注什么。


    在看面试者的项目经历时,面试官主要关注这三点:


    1. 之前做的项目有没有难度


    2. 项目经验和当前岗位需要的是否匹配


    3. 经过这些项目,这个人的能力有哪些成长


    因此,我们在日常工作和准备面试时,可以这样做:



    1. 工作时有意识地选择更有复杂度的,虽然可能花的时间更多,但对自己的简历和以后发展都有好处

    2. 主动去解决项目里的问题,解决问题是能力提升的快车道,解决的问题越多、能力会越强

    3. 解决典型的问题后,及时思考问题的本质是什么、如何解决同一类问题、沉淀为文章、记录到简历,这些都是你的亮点

    4. 经常复盘,除了公司要求的复盘,更要做自己的复盘,复盘这段时间里有没有成长

    5. 简历上,要凸显自己在项目面试的挑战、解决的问题,写出自己如何解决的、用到什么技术方案

    6. 投简历时,根据对方业务类型和岗位要求,适当的调整项目经历里的重点,突出匹配的部分

    7. 面试时,要强调自己在项目里的取得的成果、在其中的角色、得到什么可复制的经验


    技能知识点


    技能知识点就是我们掌握的编程语言、技术框架和工具。


    相较于项目经历,技能知识点更关键,因为它决定了面试者是否能够胜任岗位。


    image.png


    在技能知识点方面,面试者常见的问题有这些:



    1. 不胜任岗位:基础不扎实,不熟悉常用库的原理

    2. 技术不对口:没有岗位需要的领域技术

    3. 技术过剩:能力远远超出岗位要求


    第一种情况就是我们常说的“技术不行”。很多人仅仅在工作里遇到不会的才学习,工作多年也没有自己的知识体系,在面试的时候很容易被基础知识点问倒,还给自己找理由说“我是高级开发还问这么细节的,面试官只会八股文”。框架也是浅尝辄止,会用就不再深入学了,这在面试的时候也很容易被问住。


    第二种情况,是岗位工作内容属于细分领域,但面试者不具备这方面的经验,比如音视频、跨端等。为了避免这种情况,我们需要打造自己的细分领域技能,最好有一个擅长的方向,越早越好。


    第三种情况简单的来说就是“太贵了”。有时候一些资深点的开发面试被挂掉,并不是因为你的能力有问题,而是因为岗位的预算有限。大部分业务需求都是增删改查和界面展示,并不需要多复杂的经验。这种情况下,要么再去看看更高级的岗位,要么降低预期。


    在我面试的人里,通过面试的都有这些特点:



    1. 技术扎实:不仅仅基础好,还有深度

    2. 解决过复杂的问题:项目经验里除了完成业务需求,也有做一些有挑战的事


    有些人的简历上只写项目经历不写技能知识点,对此我是反对的,这样做增加了面试官了解你的成本。问项目经历的目的还是想确认你有什么能力,为什么不直接明了的写清楚呢?


    软素质


    这里的「软素质」指面试时考察的、技术以外的点。


    程序员的日常工作里,除了写代码还需要做这些事:



    1. 理解业务的重点和不同需求的核心点,和其他同事协作完成

    2. 从技术角度,对需求提出自己的思考和建议,反馈给其他人

    3. 负责某个具体的业务/方向,成为这个方面所有问题的处理者


    image.png


    因此,面试官或者 HR 还会考察这些点,以确保面试者具备完成以上事情的能力:



    1. 理解能力和沟通表达能力

    2. 业务能力

    3. 稳定性


    第一点是指面试者理解问题和讲清楚答案的能力。遇到过一些面试者,面试的时候过于紧张,讲话都讲不清楚,这种就让人担心“会不会是个社恐”、“工作里该不会也这样说不清楚吧”;还有的人爱抢答,问题都没听明白就开始抢答,让人怀疑是不是性格太急躁太自大;还有的人过于能讲,但讲不到重点,东扯西扯,让人对他的经历和理解能力产生了怀疑。


    第二点是指在实现业务目标的过程中可以提供的能力。 业务发展是需要团队共同努力的,但有的人从来没这么想过,觉得自己上班的任务就是写代码,来什么活干什么活,和外包一样。


    业务发展中可能有各种问题。定方向的领导有时候会过于乐观、跨部门协作项目可能会迟迟推进不动、产品经理有时候也会脑子进水提无用需求、质量保障的测试同学可能会大意漏掉某个细节测试。这个时候,程序员是否能够主动站出来出把力,帮助事情向好的方向发展,就很重要了。


    遇到过一些面试者,在一家公司干了好几年,问起来业务发展情况语焉不详,让人感觉平时只知道写代码;还有的面试者,说起业务问题抱怨指责一大堆,“领导太傻逼”、“产品经理尽提蠢需求”,负能量满满😂。


    第三点是指面试者能不能在一家公司长久干下去。 对于级别越高的人,这点要求就越高,因为他的离开对业务的发展会有直接影响。即使级别不高,频繁换工作也会让人对你有担心:会不会抗压能力很差、会不会一不涨工资就要跑路。一般来说,五年三跳就算是临界线,比这个频繁就算是真的“跳的有点多”。


    针对以上这三点,我们可以这样做:



    1. 面试时调整心态,当作普通交流,就算不会也坦然说出,不必过于紧张

    2. 回答问题时有逻辑条理,可以采用类似总分总的策略

    3. 工作时多关注开发以外的事,多体验公司产品和竞品,在需求评审时不摸鱼、多听听为什么做、思考是否合理、提出自己的想法

    4. 定好自己的职业规划(三年小进步、五年大进步),在每次换工作时都认真问问自己:下一份工作能否帮助自己达到目标


    总结


    好了,这就是我前段时间面试的感悟和吐槽。


    总的来说,今年找工作的人不少,市面上的岗位没有往年那么多。如果你最近要换工作,最好做足准备。做好后面的规划再换、做好准备再投简历、经历整理清楚再面试。


    作者:张拭心
    来源:juejin.cn/post/7261604248319918136
    收起阅读 »

    吐槽大会,来瞧瞧资深老前端写的代码

    web
    忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年...
    继续阅读 »

    忍无可忍,不吐不快。



    本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


    知道了什么是烂代码,才能写出好代码。


    别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


    人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


    我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


    我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


    优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


    有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


    -------------更新------------


    集中回答一下评论区的问题:


    1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


    2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


    3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


    4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


    5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


    6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


    文件命名千奇百怪


    同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


    image.png


    组件职责不清


    还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


    image.png


    条件渲染逻辑置于底层


    这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


    image.png


    滥用、乱用 TS


    项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


    image.png


    留下大量无用注释代码和报错代码


    感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


    image.png


    image.png


    丑陋的、隐患的、无效的、混乱的 css


    丑陋的:没有空格,没有换行,没有缩进


    隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


    无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


    混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


    image.png


    一个文件 6 个槽点


    槽点1:代码的空行格式混乱,影响代码阅读


    槽点2:空函数,写了函数名不写函数体,但是还调用了!


    槽点3:函数参数过多不优化


    槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


    槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


    槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


    image.png


    变态的链式取值和赋值


    都懒得说了,各位观众自己看吧。


    image.png


    代码拆分不合理或者不拆分导致代码行数超标


    能写出这么多行数的代码的绝对是人才。


    尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


    image.png


    这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


    image.png


    杂七杂八的无用 js、md、txt 文件


    在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


    实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


    image.png


    less、scss 混用


    这是最奇葩的。


    image.png


    特殊变量重命名


    这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


    const G = window;
    const doc = G.document;

    混乱的 import


    规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


    image.png


    写在最后


    就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。


    要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


    我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


    写代码就像做人,现实总是千难万苦,各种妥协和无奈,但是这不意味着我们可以无底线的做事。给自己设个底线,不论做人还是做事。


    共勉。


    作者:北岛贰
    来源:juejin.cn/post/7265505732158472249
    收起阅读 »

    博客园又崩了,这个锅要不要阿里云背?

    昨天下午博客园又崩了,不过与其它大厂的崩溃不同,博客园出现崩溃的频率好像有点高。 这是怎么回事呢?和阿里云又有什么关系,这篇文章就带大家来一探究竟。 到底是谁的问题? 昨天下午(2023年12月8日)博客园官方发布了一个故障公告,官网截图如下: 博客园的故障...
    继续阅读 »

    昨天下午博客园又崩了,不过与其它大厂的崩溃不同,博客园出现崩溃的频率好像有点高。


    这是怎么回事呢?和阿里云又有什么关系,这篇文章就带大家来一探究竟。


    到底是谁的问题?


    昨天下午(2023年12月8日)博客园官方发布了一个故障公告,官网截图如下:



    博客园的故障是数据库CPU 100%,今年已经出现了7次,根据我这个不经常上博客园的人的观察,往年也有出现,好像频率没这么高。


    出现了7次都不能解决,这是个什么问题呢?


    根据我的技术经验,数据库CPU百分之百,一般是某些SQL写的质量不佳,在某些情况下可能出现了大数据量全表扫描的情况,迟迟不能执行完毕,长期霸占CPU资源导致的。


    按说这种问题只要定位到对应的SQL,改掉相关语句就可以了,但是就是这个问题把博客园难住了。


    参数嗅探问题?


    看看官方针对此次问题的说明:





    这里有两个重要的信息:博客园的数据库使用的是 SQL Server;博客园的主要查询使用的是存储过程。博客园是.NET技术体系的,使用SQL Server比较顺其自然;使用存储过程可以提高SQL执行的效率,博客园是08年创立的,这在十几年前也比较流行;看它使用的分页方法也是比较新的,这说明它也一直在优化。


    官方怀疑是参数嗅探问题造成 SQL Server 缓存了性能极差的执行计划,这句话中有两个名词:参数嗅探问题和执行计划,没接触过的同学可能会比较蒙,我先给大家普及一下。


    执行计划:每条SQL在数据库内部执行时都会有一个执行计划,主要就是先查询哪张表、表之间怎么关联、执行的时候使用哪些索引,等等。


    参数嗅探问题:存储过程在首次执行时会先进行编译,后续执行的时候都使用这个编译的结果,而不是每次都解释执行,因为编译相对比较耗时。编译时,数据库还会根据当前使用的存储过程参数确定一个最优的执行计划,并把这个执行计划也一并缓存起来,后续再执行的时候就会直接使用这个执行计划。


    问题主要就出现在这个缓存的执行计划,因为对于不同的参数来说,执行计划的效率可能差别很大,这主要是查询数据分布不均匀的问题造成的。


    我在公司的业务中也经常遇到这个问题,有的用户数据多,有的用户数据少,即使我们为用户Id字段设置了索引,数据库有时仍旧会认为不使用这个索引的效率更高,它会自己选择一个自认为更优的查询路径,比如全表扫描,实际执行时就出现了慢SQL的情况。


    到博客园这里,官方认为就是自己的某个存储过程因为参数嗅探问题导致某些慢SQL,慢SQL导致CPU使用过高,最后导致数据库崩溃。


    而官方一直没有定位到出现问题的SQL或者出现问题的存储过程,可能博客园的SQL太多了吧,出现问题的不止一个SQL。又或者是 SQL Server 的问题,或者阿里云的锅?


    SQL Server的问题?


    SQL Server 作为一款商业数据库,能活到现在,而且价格还不低,其产品能力是经过了残酷的市场考验的。虽然任何产品都不可避免的存在一些BUG,但是导致这种问题的BUG应该不会持续这么久。所以 SQL Server 本身的问题应该不大,或者说 SQL Server 的数据查询方式没有问题。


    还有很多同学提到 SQL Server 性能不行,单纯根据我的使用经验来说,类似的场景 SQL Server的查询性能往往比 MySQL 要好不少,其它很多用户也有类似的反馈:



    我也专门找了一些 SQL Server 和其它数据库的性能对比,截图如下:



    文章和数据来源:


    segmentfault.com/q/101000002…


    http://www.ijarcce.com/upload/2015…


    另外我们也可以从博客园分享的数据库的监控日志中略窥一二:



    从图上可以看出,出现问题的时间比较随机,也不是什么高峰期。博客园也提到过凌晨4-5点钟出现类似问题。看这个CPU使用率只有20%多一点,所以并非是遇到了性能瓶颈。



    阿里云的问题?


    阿里云为什么可能背锅?因为博客园部署在阿里云上,服务器和数据库都用的阿里云产品。


    记得之前出现这个问题时,博客园官方对阿里云颇多微词,后来双方可能进行了深入交流,博客园接受了参数嗅探问题,此后就一直在这块查找。


    那么阿里云能不能彻底撇清关系呢?


    正常情况下,阿里云上部署的 SQL Server 应该是从微软购买的,微软应该也要提供一些技术支持,包括安装和日常的运行维护支持。这个 SQL Server 可能和 Azure 上部署的有些差别,但微软也不会砸自己的招牌,数据库版本不应该有大问题。


    阿里云只是部署和运维 SQL Server,说白了阿里云只是搞了底层的存储、网络、操作系统等服务,上层的数据库应用完全是微软的,他插不上手,这种数据库程序的CPU百分百的故障很难和阿里云干的事挂上钩。


    再者阿里云自己也开发数据库,虽然 SQL Server 不开源,但是高手们对于一些底层的设计,或者可能存在问题的地方,应该也是门清的。阿里云上 SQL Server 服务使用者众多,如果很多企业都遇到这个问题,应该也早就爆出来并解决了。


    所以这个问题甩锅到阿里云身上的难度比较大。当然也没办法完全排除,毕竟总有些极端情况,阿里云最近也崩了很多次,会不会在某些方面有些幺蛾子?大家也不知道。


    怎么解决问题?


    换数据库?



    正如上文所说,问题出现在数据库自身上的可能性不大,而且换数据库要重写所有的SQL,还可能要修改表结构,这个工作量不是一星半点。


    如果真的是参数嗅探问题,换了数据库一样存在执行计划效率不一致的问题。


    换云?


    这基本是认为阿里云能力不行。


    如果真的怀疑是这方面的问题,倒是可以试试,不过不是直接迁移过去,而是把数据导出来一份,放到别的公有云上,或者本地部署一套SQL Server。


    然后采集SQL执行日志,在测试的数据库中进行重放执行,如果问题还会发生,那就不是云厂商的问题,如果跑了很久,问题都没有出现过,那才有根据说云服务的问题概率比较大一些。


    当然这个测试的成本比较高,也许可以通过精简样本或者提高SQL执行频率加速一下测试。


    作为技术人,甩锅时一定要有理有据。


    再或者就不讲理,博客园死磕阿里云,要么就是你的问题,要么就是你帮我找出问题来。有时候云厂商的技术团队也是可以上门或者以其他方式进行亲密沟通的。再不行花点钱找个高手呢?可能还是博客园太老实了?或者阿里云太傲慢了?又或者博客园太穷了?


    解决参数嗅探问题


    阿里云的问题只能是猜测,参数嗅探的问题确是能够实实在在抓住的,阿里云的数据库产品是提供了慢SQL日志查询的。


    只需要找出出现问题时的慢SQL,看博客园以往的故障公告也是曾经抓到过一些问题SQL的。


    但是问题为什么还会一直出现呢?


    有可能是问题SQL太多了。经过十几年的迭代,博客园的代码量可能十分庞大,再加上博客园这两年经营比较困难,没有人力和精力投入到这方面,只能问题出现了再去反查,然后改正。能活着就不错了,估计团队内部也没有技术牛人,精力都放到了活下来的事情上。


    具体为什么一直解决不了,咱们就说到这里。


    下面给大家聊聊怎么解决参数嗅探的问题,我想这个对于搞技术的同学来说才是最重要的.


    上面我们已经说过参数嗅探问题就是数据库使用了效率不高的执行计划,那么解决这个问题的核心思路就是让数据库不去使用这些低效计划。这里分享一些我了解的方法。


    暴力清理


    重启服务器、重启数据库,博客园采用的处理方法差不多都是这个。



    还有一个稍微优雅点的方案,清除所有的执行计划缓存:DBCC FREEPROCCACHE,不管这些执行计划是不是有问题。但是不确定这个指令能不能在阿里云的数据库服务上执行。


    这些都是强制重新创建执行计划的方法,坏处就是影响都比较大,很可能会影响用户使用服务,比较暴力。


    而且这些方法不能治本,只能短时间的缓解一下,说不定在某个时刻,执行计划又被重建了,或者SQL执行又超时了。


    优雅机制


    SQL Server本身也有一些优雅的方案来缓解这个问题。比如:



    • 不缓存执行计划,虽然缓存能带来一些效率上的提升,但相比参数嗅探问题带来的性能损失就是小巫见大巫了。可以在存储过程中使用WITH RECOMPILE,让查询每次都重新编译。

    • 强制使用某个查询计划,比如强制使用某个索引,这个索引对于所有的查询都不会太差;SQL Server中还可以强制使用某个条件的查询计划。不过找到这个索引或者条件的难度可能比较大,因为数据一直在变化,现在是好的并不代表一直好。

    • 只清除特定语句或存储过程的查询缓存,使用 DBCC FREEPROCCACHE(@plan_id) 指定执行计划,这样影响更小。

    • 另外表统计信息陈旧、索引碎片、缺少索引都可能导致参数嗅探问题,遇到问题时可以从这几个方面调查一下。


    详情可参考阿里的这篇文章: mysql.taobao.org/monthly/201…


    谨慎评估


    在我们设计表、编写SQL的时候,需要考虑数据会如何分布,查询有哪些条件,特别是数据可能分布不均匀的情况。


    比如有的用户的数据量可能是大部分用户的10倍、甚至百倍,排序的字段可能导致不使用包含条件字段的索引,查询可能在多个索引之间飘移。


    如果可能存在问题,就要考虑表如何设计、数据如何查询,普通关系数据库难以解决时,我们还可以考虑采用NoSQL、分布式数据库等方案,以稳定查询效率。




    以上就是本文的主要内容了,因本人才疏学浅,不免存在错漏,如有问题还望指正。


    关注微/信/公/众/号:萤火架构,技术提升不迷路。


    作者:萤火架构
    来源:juejin.cn/post/7310111620368826387
    收起阅读 »

    【Java集合】双列集合HashMap的概念、特点及使用

    HashMap是Java中的一个集合类,它实现了Map接口,提供了一种存储键值对的方式。你可以把它想象成一个没有固定大小和形状的储物柜,你可以随时往里面放东西,也可以随时取出东西。而且,这个储物柜还有一个神奇的功能,那就是无论你放进去的是什么,取出来的总是你放...
    继续阅读 »

    HashMap是Java中的一个集合类,它实现了Map接口,提供了一种存储键值对的方式。你可以把它想象成一个没有固定大小和形状的储物柜,你可以随时往里面放东西,也可以随时取出东西。而且,这个储物柜还有一个神奇的功能,那就是无论你放进去的是什么,取出来的总是你放进去的那个。

    上篇文章讲了Map接口的概念,以及Map接口中的常用方法和对Map集合的遍历,本篇文章我们将继续介绍另一个十分重要的双列集合—HashMap。


    HashMap 概念

    HashMap集合是Map接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空,但键不能重复,且集合中的元素是无序的。

    特点

    HashMap底层是由哈希表结构组成的,其实就是“数组+链表”的组合体,数组是HashMap的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap集合对于元素的增、删、改、查操作表现出的效率都比较高。

    结构

    在java1.8以后采用数组+链表+红黑树的形势来进行存储,通过散列映射来存储键值对,如下图:

    Description

    • 在初始化时将会给定默认容量为16

    • 对key的hashcode进行一个取模操作得到数组下标

    • 数组存储的是一个单链表

    • 数组下标相同将会被放在同一个链表中进行存储

    • 元素是无序排列的

    • 链表超过一定长度(TREEIFY_THRESHOLD=8)会转化为红黑树

    • 红黑树在满足一定条件会再次退回链表

    看到这个图,是不是挺熟悉!没错,这个就是我们在讲Set时,它的内存结构图,当时我们说 HashSet的底层就是 Map集合,只不过Set只使用了Map集合中的Key,没有使用Value而已。

    小练习

    在之前我们已经讲了不少Map的使用方法,本篇中就不做过多解释了,来上了个小练习,在体会下它的使用。

    每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。

    注意,学生姓名相同并且年龄相同视为同一名学生。

    编写学生类:

        public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
    this.name = name;
    this.age = age;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public int getAge() {
    return age;
    }

    public void setAge(int age) {
    this.age = age;
    }

    @Override
    public boolean equals(Object o) {
    if (this == o)
    return true;
    if (o == null || getClass() != o.getClass())
    return false;
    Student student = (Student) o;
    return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
    return Objects.hash(name, age);
    }
    }

    编写测试类:

        public class HashMapTest {
    public static void main(String[] args) {
    //1,创建Hashmap集合对象。
    Map<Student,String>map = new HashMap<Student,String>();
    //2,添加元素。
    map.put(newStudent("lisi",28), "上海");
    map.put(newStudent("wangwu",22), "北京");
    map.put(newStudent("zhaoliu",24), "成都");
    map.put(newStudent("zhouqi",25), "广州");
    map.put(newStudent("wangwu",22), "南京");

    //3,取出元素。键找值方式
    Set<Student>keySet = map.keySet();
    for(Student key: keySet){
    Stringvalue = map.get(key);
    System.out.println(key.toString()+"....."+value);
    }
    }
    }
    • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。

    • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

    你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

    LinkedHashMap

    我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

    在HashMap下面有一个子类LinkedHashMap,它继承自HashMap。特别的是,LinkedHashMap在HashMap的基础上维护了一个双向链表,可以按照插入顺序或者访问顺序来迭代元素。此外,LinkedHashMap结合了HashMap的数据操作和LinkedList的插入顺序维护的特性,因此也可以被看做是HashMap与LinkedList的结合。它是链表和哈希表组合的一个数据存储结构。把上个练习使用LinkedHashMap的使用一下

        publicclass LinkedHashMapDemo {
    publicstaticvoid main(String[] args) {

    //Map<String, String> map = new HashMap<String, String>();

    LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
    map.put("马云", "阿里巴巴");
    map.put("马化腾", "腾讯");
    map.put("李彦宏", "百度");
    Set<Entry<String, String>> entrySet = map.entrySet();
    for (Entry<String, String> entry : entrySet) {
    System.out.println(entry.getKey() + " " + entry.getValue());
    }
    }
    }

    总结

    总的来说,HashMap是Java中的一个强大工具,它可以帮助我们高效地处理大量的数据。但是,我们也需要注意,虽然HashMap的性能很高,但如果不正确地使用它,可能会导致内存泄漏或者数据丢失的问题。因此,我们需要正确地理解和使用HashMap,才能充分发挥它的强大功能。

    本系列文章写到这里,为大家介绍集合家族的知识,基本上就可以告一段落了。

    在这个系列文章中,我们讲述了单列和双列集合的家族体系以及简单的使用。集合中不少的实现类,我们并未讲述,大家下来可以通过java的API文档,去学习使用。还是那句话,熟能生巧!只看不练,假把式!

    本系列以上内容,都是在实际项目中,会经常碰到这些概念的使用,当然了,文中的内容可能也不是尽善尽美的,如有错误,可以私信,探讨!
    happy ending!

    收起阅读 »

    教你如何实现一个页面自动打字效果

    web
    前言: 最近在写一个类似于 windows 启动页的项目,不知道大家是否还记的 windows 很经典的小光标加载页面,我又稍微改造了一下效果如下: 一. 光标闪烁效果的实现 tips: 在这里我们使用了 UnoCSS,如果你不清楚 UnoCSS 的使用方...
    继续阅读 »

    前言: 最近在写一个类似于 windows 启动页的项目,不知道大家是否还记的 windows 很经典的小光标加载页面,我又稍微改造了一下效果如下:


    loading.gif




    一. 光标闪烁效果的实现


    tips: 在这里我们使用了 UnoCSS,如果你不清楚 UnoCSS 的使用方法,那你可以点击下面这篇文章。

    🫱 🎁手把手教你创建自己的代码仓库



    1. 首先准备一块黑色的背景。

      image.png

    2. 其实光标的样式非常非常简单,仅仅只需要你创建一个宽高合适的 div,然后创建一个底部的 border 效果即可。

      image.png
      下面应该是你目前的效果。

      image.png

    3. 现在需要清楚的知道,这个白块的展示其实就是我们控制展示这个 divborder 的显示还是隐藏。那么现在我们的思路就很清晰了,所以这里我们只需要写一个变量来动态的切换这个 border 值即可。

      image.png

    4. 现在你的页面效果应该是漆黑一片,那交给谁来动态的切换这个状态呢?这里其实很简单,当页面挂载的时候,我们只需要开启一个定时器来动态切换即可。

      image.png

      这时候我们其实就能看到一丢丢效果了:

      flash.gif


    二. 自动打字效果的实现



    1. 首先我们应该明确一个概念,我们目前要做的事很简单,只需要在百块 div 的前面插入文字其实就是在向后推白块

      image.png

      image.png

      所以白块的移动是我们无需关心的,我们仅仅只需要去处理如何插入字体的问题。

    2. 这里我们先准备一个常量来书写一段字符串文字,然后还需要给准备放文字的 div 打上 ref 为后面的工作做准备,之后我们需要用到它身上相关的属性。

      image.png

    3. 接下来我们要编写一个函数去处理这个问题,名字起的就随意点吧,就叫做 autoPrint

      image.png

    4. 这里我们仍需要开启一个循环定时器去控制,因为我们无法得知文字具体有多少,不考虑使用 setTimeout

      image.png

    5. 还需要准备两个变量,来存放接下来我们要处理的文字信息。

      image.png

    6. 下面代码的思路就比较简单了,其实就是调用了 substring 方法来一直切割获取下一个字符串的值。substring本身也是不改变原字符串的,所以我们只需要控制 index 就可以很轻松的获取到最后的值。

      image.png

      效果如下:

      3.gif

    7. 最后别忘了在合适的时机清除这个定时器。

      image.png


    三. 更优雅的实现小方块闪烁


    更新于 2023/02/22



    1. 在写上面的代码之前我没有考虑文字过长的问题,导致小光标不会换行的问题。

    2. 今天更新一下,修复这个 bug

      自动.gif

    3. 我们删除上面之前控制 border 的显示与否而展示的小光标样式。

      image.png

    4. 在放置文字的 div 添加一个伪元素来实现这个效果,更加简洁一点。

      image.png

    5. 并且使用动画来替换之前的 flicker
      image.png


    四. 源码



    <script>

    //tips: automatic printing welcome words.
    function autoPrintText(text: string) {
    let _str = ""
    let _index = 0
    const _timerID = window.setInterval(() => {
    if (!textAreas.value) return
    if (_index > text.length - 1) {
    clearInterval(_timerID)
    return
    }
    _str = _str + text.substring(_index, _index + 1)
    textAreas.value!.innerText = _str
    _index++
    }, printSpeed)
    }

    </script>

    <template>

    <div v-if="isFlicker" class="w-full h-full">
    <div class="text-box w-fit">
    <span ref="textAreas" class="text-1.8rem font-600"></span>
    </div>
    </template>

    <style scoped>
    .text-box::after {
    display: inline-block;
    content: "";
    width: 2rem;
    vertical-align: text-bottom;
    border-bottom: 3px solid white;
    margin-left: 8px;
    animation: flicker 0.5s linear infinite;
    }

    @keyframes flicker {
    from {
    opacity: 0;
    }
    to {
    opacity: 1;
    }
    }
    </style>

    预告


    最近在实现一个 window 的全套 UI ,PC 和移动端的效果是完全自适应的,两者有两套 UI

    4.gif

    我会在本周更新拖拽这个经典面试题的实现,仍会使用费曼学习法通俗易懂的讲解。如果你有兴趣,不妨保持关注。🎁


    作者:韩振方
    来源:juejin.cn/post/7200773486796914725
    收起阅读 »

    全网显示IP归属地,免费可用,快来看看

    前言 经常浏览小视频或各类帖子的朋友们可能已经注意到,目前许多网络平台都会显示作者和评论区用户的IP归属地。那么,这个功能是如何实现的呢? 某些收费平台的API 我们可以利用一些付费平台的API来实现这一功能,比如一些导航软件的开放平台API等。然而,这些服...
    继续阅读 »

    前言


    经常浏览小视频或各类帖子的朋友们可能已经注意到,目前许多网络平台都会显示作者和评论区用户的IP归属地。那么,这个功能是如何实现的呢?



    某些收费平台的API


    我们可以利用一些付费平台的API来实现这一功能,比如一些导航软件的开放平台API等。然而,这些服务通常是收费的,而且免费额度有限,适合测试使用,但如果要在生产环境中使用,很可能不够支撑需求。



    离线库推荐


    那么,有没有免费的离线API库呢?UP现在推荐一个强大的离线库给大家,一个准确率高达99.9%的离线IP地址定位库,查询速度仅需0.0x毫秒,而且数据库仅10兆字节大小。此库提供了Java、PHP、C、Python、Node.js、Golang、C#等多种查询绑定,同时支持Binary、B树和内存三种查询算法。



    这个库大家可以在GitHub上搜索:ip2region,即可找到该开源库。


    使用


    下面使用Java代码给大家演示下如何使用这个IP库,该库目前支持多重主流语言。


    1、引入依赖


    <dependency>
       <groupId>org.lionsoul</groupId>
       <artifactId>ip2region</artifactId>
       <version>2.7.0</version>
    </dependency>

    2、下载离线库文件 ip2region.xdb



    3、简单使用代码


    下面,我们通过Java代码,挑选某个国内的IP进行测试,看看会输出什么样的结果


    public class IpTest {

       public static void main(String[] args) throws Exception {
           // 1、创建 searcher 对象 (修改为离线库路径)
           String dbPath = "C:\Users\Administrator\Desktop\ip2region.xdb";
           Searcher searcher = null;
           try {
               searcher = Searcher.newWithFileOnly(dbPath);
          } catch (Exception e) {
               System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
               return;
          }

           // 2、查询
           String ip = "110.242.68.66";
           try {
               long sTime = System.nanoTime(); // Happyjava
               String region = searcher.search(ip);
               long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
               System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
          } catch (Exception e) {
               System.out.printf("failed to search(%s): %s\n", ip, e);
          }

           // 3、关闭资源
           searcher.close();

           // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
      }

    }


    输出结果为:


    {region: 中国|0|河北省|保定市|联通, ioCount: 3, took: 1192 μs}

    其中,region的格式为 国家|区域|省份|城市|ISP,缺省的地域信息默认是0。


    当然,这个库不只是支持国内的IP,也支持国外的IP。



    其他语言可以参考该开源库的说明文档。


    总结


    这是一个准确率非常高的离线库,如果项目里有IP定位需求的,可以试下该库。


    作者:happyjava
    来源:juejin.cn/post/7306334713992708122
    收起阅读 »

    前端如何使用websocket发送消息

    web
    1 基础介绍 1.1 什么是WebSocket WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议,它可以让客户端和服务器之间进行实时的双向通信。与传统的 HTTP 请求不同,WebSocket 使用了一个长连接,在客户端和服务器之间保...
    继续阅读 »

    1 基础介绍


    1.1 什么是WebSocket



    WebSocket 是一种在单个 TCP 连接上进行 全双工 通信的协议,它可以让客户端和服务器之间进行实时的双向通信。与传统的 HTTP 请求不同,WebSocket 使用了一个长连接,在客户端和服务器之间保持持久的连接,从而可以实时地发送和接收数据。




    在 WebSocket 中,客户端和服务器之间可以互相发送消息。
    客户端可以使用 JavaScript 中的 WebSocket API 发送消息到服务器,也可以接收服务器发送的消息。



    1.2 WebSocket与HTTP的区别



    WebSocket与HTTP的区别在于连接的性质和通信方式。WebSocket是一种双向通信的协议,通过一次握手即可建立持久性的连接,服务器和客户端可以随时发送和接收数据。而HTTP协议是一种请求-响应模式的协议,每次通信都需要发送一条请求并等待服务器的响应。WebSocket的实时性更好,延迟更低,并且在服务器和客户端之间提供双向的即时通信能力,适用于需要实时数据传输的场景。



    1.3 代码示例



    下面是一个使用 WebSocket API 发送消息的代码示例:



    var socket = new WebSocket("ws://example.com/socketserver");

    socket.onopen = function(event) {
    socket.send("Hello server!");
    };

    socket.onmessage = function(event) {
    console.log("Received message from server: " + event.data);
    };

    socket.onerror = function(event) {
    console.log("WebSocket error: " + event.error);
    };

    socket.onclose = function(event) {
    console.log("WebSocket connection closed with code " + event.code);
    };


    在上面的代码中,首先创建了一个 WebSocket 对象,指定了服务器的地址。然后在 onopen 回调函数中,发送了一个消息到服务器。当服务器发送消息到客户端时,onmessage 回调函数会被触发,从而可以处理服务器发送的消息。如果出现错误或者连接被关闭,onerror 和 onclose 回调函数会被触发,从而可以处理这些事件。


    需要注意的是,在使用 WebSocket 发送消息之前,必须先建立 WebSocket 连接。在上面的代码中,通过创建一个 WebSocket 对象来建立连接,然后在 onopen 回调函数中发送消息到服务器。如果在连接建立之前就尝试发送消息,那么这些消息将无法发送成功。



    2 前端使用WebSocket的流程


    2.1 创建WebSocket对象


    通过JavaScript中的new WebSocket(URL)方法创建WebSocket对象,其中URL是WebSocket服务器的地址。根据实际情况修改URL以与特定的WebSocket服务器进行连接。例如:


    const socket = new WebSocket('ws://localhost:8000');

    2.2 监听WebSocket事件


    WebSocket对象提供多种事件用于监听连接状态和接收消息,例如:open、message、close、error等。



    • open:当与服务器建立连接时触发。

    • message:当收到服务器发送的消息时触发。

    • close:当与服务器断开连接时触发。

    • error:当连接或通信过程中发生错误时触发。


    通过添加事件监听器,可以在相应事件发生时执行特定的逻辑。例如:


    socket.addEventListener('open', () => {
    console.log('WebSocket连接已建立');
    });

    socket.addEventListener('message', (event) => {
    const message = event.data;
    console.log('收到消息:', message);
    });

    socket.addEventListener('close', () => {
    console.log('WebSocket连接已断开');
    });

    socket.addEventListener('error', (error) => {
    console.error('发生错误:', error);
    });

    2.3 发送消息


    通过WebSocket对象的send(data)方法发送消息,其中data是要发送的数据,可以是字符串、JSON对象等。可以根据实际需求将数据格式化成特定的类型进行发送。例如:


    const message = 'Hello, server!';
    socket.send(message);

    2.4 关闭WebSocket连接


    当通信结束或不再需要与服务器通信时,需要关闭WebSocket连接以释放资源。通过调用WebSocket对象的close()方法可以主动关闭连接,也可以根据业务需求设置自动关闭连接的条件。例如:


    socket.close();

    3 前端发送消息的应用实例


    一个常见的前端发送消息的应用实例是在线聊天应用。在这种应用中,前端通过WebSocket与后端服务器建立连接,并实时发送和接收聊天消息。


    以下是一个简单的前端发送消息的示例代码:


    const socket = new WebSocket('ws://localhost:8000');

    // 连接建立事件
    socket.addEventListener('open', () => {
    console.log('WebSocket连接已建立');
    });

    // 消息接收事件
    socket.addEventListener('message', (event) => {
    const message = event.data;
    console.log('收到消息:', message);
    // 处理接收到的消息,将其显示在前端界面上
    });

    // 发送消息
    function sendMessage(message) {
    socket.send(message);
    }

    // 调用发送消息的函数,例如在点击按钮后发送消息
    const sendButton = document.getElementById('sendBtn');
    sendButton.addEventListener('click', () => {
    const messageInput = document.getElementById('messageInput');
    const message = messageInput.value;
    sendMessage(message);
    messageInput.value = ''; // 清空输入框
    });

    // 连接关闭事件
    socket.addEventListener('close', () => {
    console.log('WebSocket连接已断开');
    });

    // 连接错误事件
    socket.addEventListener('error', (error) => {
    console.error('发生错误:', error);
    });


    该示例中,通过创建WebSocket对象,监听连接建立事件、消息接收事件、连接关闭事件和错误事件,从而实现与服务器的实时通信。通过构建界面和处理消息的逻辑,可以实现实时聊天功能。


    这只是一个简单的示例,实际上,前端发送消息的应用可以更广泛,如实时数据更新、多人协作编辑、实时游戏等。具体的实现方式和功能根据实际需求而定,可以灵活调整和扩展。



    4 WebSocket的应用场景


    WebSocket的应用场景包括但不限于以下几个方面:



    1. 实时聊天应用:WebSocket能够提供双向、实时的通信机制,使得实时聊天应用能够快速、高效地发送和接收消息,实现即时通信。

    2. 实时协作应用:WebSocket可以用于实时协作工具,如协同编辑文档、白板绘画、团队任务管理等,团队成员可以实时地在同一页面上进行互动和实时更新。

    3. 实时数据推送:WebSocket可以用于实时数据推送场景,如股票行情、新闻快讯、实时天气信息等,服务器可以实时将数据推送给客户端,确保数据的及时性和准确性。

    4. 多人在线游戏:WebSocket提供了实时的双向通信机制,适用于多人在线游戏应用,使得游戏服务器能够实时地将游戏状态和玩家行为传输给客户端,实现游戏的实时互动。

    5. 在线客服和客户支持:WebSocket可以用于在线客服和客户支持系统,实现实时的客户沟通和问题解决,提供更好的用户体验,减少等待时间。



    WebSocket适用于需要实时双向通信的场景,在这些场景中,它能够提供更好的实时性、低延迟和高效性能,为Web应用程序带来更好的交互性和用户体验。



    作者:李泽南
    来源:juejin.cn/post/7277835425959886882
    收起阅读 »

    面试官:你知道websocket的心跳机制吗?

    web
    前言 哈喽,大家好,我是泽南Zn👨‍🎓。在之前的一篇文章写到, 前端如何使用websocket发送消息,websocket是怎么建立连接的呢?如果断开了会怎样?如何一直保持长连接呢?接下来,本篇文章将会带你了解--- WebSocket心跳机制 一、...
    继续阅读 »

    前言


    哈喽,大家好,我是泽南Zn👨‍🎓。在之前的一篇文章写到, 前端如何使用websocket发送消息,websocket是怎么建立连接的呢?如果断开了会怎样?如何一直保持长连接呢?接下来,本篇文章将会带你了解--- WebSocket心跳机制


    一、WebSocket心跳机制


    前端实现WebSocket心跳机制的方式主要有两种:




    1. 使用setInterval定时发送心跳包。

    2. 在前端监听到WebSocket的onclose()事件时,重新创建WebSocket连接。



    第一种方式会对服务器造成很大的压力,因为即使WebSocket连接正常,也要定时发送心跳包,从而消耗服务器资源。第二种方式虽然减轻了服务器的负担,但是在重连时可能会丢失一些数据。


    二、WebSocket心跳包机制


    WebSocket心跳包是WebSocket协议的保活机制,用于维持长连接。有效的心跳包可以防止长时间不通讯时,WebSocket自动断开连接。


    心跳包是指在一定时间间隔内,WebSocket发送的空数据包。常见的WebSocket心跳包机制如下:




    1. 客户端定时向服务器发送心跳数据包,以保持长连接。

    2. 服务器定时向客户端发送心跳数据包,以检测客户端连接是否正常。

    3. 双向发送心跳数据包。



    三、WebSocket心跳机制原理


    WebSocket心跳机制的原理是利用心跳包及时发送和接收数据,保证WebSocket长连接不被断开。WebSocket心跳机制的原理可以用下面的流程来说明:




    1. 客户端建立WebSocket连接。

    2. 客户端向服务器发送心跳数据包,服务器接收并返回一个表示接收到心跳数据包的响应。

    3. 当服务器没有及时接收到客户端发送的心跳数据包时,服务器会发送一个关闭连接的请求。

    4. 服务器定时向客户端发送心跳数据包,客户端接收并返回一个表示接收到心跳数据包的响应。

    5. 当客户端没有及时接收到服务器发送的心跳数据包时,客户端会重新连接WebSocket



    四、WebSocket心跳机制必要吗


    WebSocket心跳机制是必要的,它可以使 WebSocket 连接保持长连接,避免断开连接的情况发生。同时,心跳机制也可以检查WebSocket连接的状态,及时处理异常情况。


    五、WebSocket心跳机制作用


    WebSocket心跳机制的作用主要有以下几点:



    1. 保持WebSocket连接不被断开。

    2. 检测WebSocket连接状态,及时处理异常情况。

    3. 减少WebSocket连接及服务器资源的消耗。


    六、WebSocket重连机制


    WebSocket在发送和接收数据时,可能会因为网络原因、服务器宕机等因素而断开连接,此时需要使用WebSocket重连机制进行重新连接。


    WebSocket重连机制可以通过以下几种方式实现:




    1. 前端监听WebSocket的onclose()事件,重新创建WebSocket连接。

    2. 使用WebSocket插件或库,例如Sockjs、Stompjs等。

    3. 使用心跳机制检测WebSocket连接状态,自动重连。

    4. 使用断线重连插件或库,例如ReconnectingWebSocket等。



    七、WebSocket的缺点和不足


    WebSocket的缺点和不足主要有以下几点:




    1. WebSocket需要浏览器和服务器端都支持该协议。

    2. WebSocket会增加服务器的负担,不适合大规模连接的应用场景。



    八、关键代码


      // 开启心跳
    const start = () => {
    clearTimeout(timeoutObj);
    // serverTimeoutObj && clearTimeout(serverTimeoutObj);
    timeoutObj = setTimeout(function () {
    if (websocketRef.current?.readyState === 1) {
    //连接正常
    sendMessage('hello');
    }
    }, timeout);
    };
    const reset = () => {
    // 重置心跳 清除时间
    clearTimeout(timeoutObj);
    // 重启心跳
    start();
    };

    ws.onopen = (event) => {
    onOpenRef.current?.(event, ws);
    reconnectTimesRef.current = 0;
    start(); // 开启心跳
    setReadyState(ws.readyState || ReadyState.Open);
    };
    ws.onmessage = (message: WebSocketEventMap['message']) => {
    const { data } = message;

    if (data === '收到,hello') {
    reset();
    return;
    }
    if (JSON.parse(data).status === 408) {
    reconnect();
    return;
    }
    onMessageRef.current?.(message, ws);
    setLatestMessage(message);
    };
    const connect = () => {
    reconnectTimesRef.current = 0;
    connectWs();
    };

    主要思路:在建立长连接的时候开启心跳,通过和服务端发送信息,得到服务端给返回的信息,然后重置心跳,清楚时间,再重新开启心跳。如果网络断开的话,会执行方法,重新连接。


    作者:泽南Zn
    来源:juejin.cn/post/7290005438153867283
    收起阅读 »

    转转的Flutter实践之路

    前言 跨端技术一直是移动端开发领域的热门话题,Flutter 作为一种领先的移动跨端技术之一,凭借其快速的渲染引擎、丰富的UI组件库和强大的开发工具,成为了开发人员的首选之一。 从 Flutter 诞生之初,我们就一直关注着它的发展,Flutter 早期版本变...
    继续阅读 »

    前言


    跨端技术一直是移动端开发领域的热门话题,Flutter 作为一种领先的移动跨端技术之一,凭借其快速的渲染引擎、丰富的UI组件库和强大的开发工具,成为了开发人员的首选之一。


    从 Flutter 诞生之初,我们就一直关注着它的发展,Flutter 早期版本变更较为频繁,并且经常伴随着 Breaking Change,另外可用的三方插件较少且不稳定。直到2019年,Flutter 的热度暴涨,国内不少团队陆续把 Flutter 引入到了生产环境使用,社区也涌现出不少优秀的开源项目,我们也决定在这个时候做一些技术上的尝试。


    经过这几年在 Flutter 技术上的不断学习、探索和积累,Flutter 已经成为了客户端技术体系中的重要组成部分。


    回顾整个过程,我们大致经历了这么几个阶段:可行性验证、基建一期建设、小范围试验、基建二期建设、大范围推广、前端生态的探索,下文将分别对每个阶段展开进行介绍。


    可行性验证


    其实在这之前我们已经做过了一些调研,但许多结论都是来源于网上的一些文章或者其它团队的实践,这些结论是否靠谱是否真实还有待商榷,另外,网上的文章大都千篇一律,要么使劲吹捧,要么使劲贬低,要得出相对客观的结论还是得需要我们自己通过实践才能得出。


    目标


    我们确定了以下几个维度,用来评估 Flutter 是否值得我们进一步投入:



    • 开发效率

    • UI一致性

    • 性能体验

    • 学习成本

    • 发展趋势


    由于前期对 Flutter 的熟练度不高,基础设施也还没有搭建起来,所以在开发效率上,我们期望的 Flutter 的开发耗时能保持在原生开发耗时的 1.5 倍以内,不然虽然实现了跨端,但是需求的开发周期反而被拉长了,这样得不偿失。在UI一致性上,我们期望同一份代码在两端的表现要基本达到一致,不需要额外的适配成本。在性能方面,尽量保证崩溃、卡顿、内存、帧率这些指标在可控范围内。


    方案


    我们希望用较小的代价完成上述维度的评估,所以在试验期间的架构及基础设施方面我们做的比较简单。


    测试目标


    当时我们正在做一个叫切克的 App,用户量级比较小,工程架构也相对简单一些,正好可以用来做一些技术方面的探索和验证。


    我们选择的是切克的商品详情页,用 Flutter 技术实现了一个一模一样的商详,按1:1的流量分配给 Native 和 Flutter。


    项目架构


    由于我们的工程不是一个全新的项目,所以采用的是 Native 与 Flutter 混合开发的方式,Native 主工程只依赖 Flutter 产物即可,同时也尽量避免对原有工程的影响。


    关于混合页面栈的问题,我们没有额外处理,因为暂时只测试一个页面,不会涉及到多页面混合栈的问题,所以暂时先忽略。


    构建流程


    为了降低验证成本,我们没有对接现有的 Native 的持续集成流程,而是直接在本地构建 Flutter 产物,然后上传到远程仓库。


    结论


    经过一段时间的线上验证,我对 Flutter 技术基本有了一个比较全面的了解:


    在开发效率上由于基础库和基建的缺失,在处理 Flutter 业务跟 Native 业务的交互时需要更多的适配成本,包括像页面跳转、埋点上报、接口请求、图片加载等也需要额外的处理,但我们评估随着后续基建的不断完善,这部分的效率是可以逐步得到改善的;而在涉及UI开发方面,得益于热重载等技术,Flutter 的开发效率是要优于原生开发的。整体评估下来,在开发效率方面 Flutter 是符合我们的预期的。


    在UI一致性上,除了在状态栏控制和文本在某些情况下需要特殊适配下外,其它控件在两端的表现基本一致。


    在性能表现上,Flutter 会额外引入一些崩溃,内存占用也有所上涨,但还在可接受范围内。


    Flutter 的学习成本相对还是比较高,毕竟需要单独学习一门语言,另外 Flutter 的渲染原理也跟原生有很多差异,需要转变思维才能更快的适应,此外 Flutter 还提供了众多的 Widget 组件,也需要较长时间学习。


    在发展趋势上,Flutter 无疑是当时增长最快的跨端技术之一,社区的活跃程度以及官方的投入都非常高,国内不少团队也都在积极推进 Flutter 技术的发展,Flutter 正处在一个快速的上升期。


    整体来说,Flutter 是满足我们团队对跨平台技术的需求的,我们计划在接下来的一段时间投入更多资源,把 Flutter 的基础设施逐渐建立起来。


    基建一期建设


    基建一期内容主要包括以下几个方面:



    • 工程架构

    • 开发框架

    • 脚本工具

    • 自动化构建


    在基建一期完成后,我们的目标是要达到:



    • 基础能力足够支撑普通业务开发

    • 开发效率接近原生开发

    • 开发过程要基本顺畅


    工程架构


    工程架构指的是原生工程与 Flutter 工程之间的关系,以及 Flutter 工程与 Flutter 工程之间的关系。


    原生工程与Flutter工程的关系


    我们知道,使用 Flutter 开发通常有两种情况,一种是直接使用 Flutter 开发一个新的App,属于纯 Flutter 开发;一种是在已有的 Native 工程中引入,属于混合开发。我们当然属于后者。


    而混合开发又可分为两种:源码集成和产物集成。源码集成需要改变原工程的项目结构,并且需要 Flutter 开发环境才能编译,而产物集成则不需要改动原工程的项目结构,只需把 Flutter 的构建产物当作普通的依赖库引入即可,原有 Native 工程和 Flutter 工程从物理上完全独立。显而易见的我们选择产物集成的方式,引入 Flutter对于原工程以及非 Flutter 开发人员来说,基本上是毫无感知的。


    所以原生工程与 Flutter 工程之间的关系如下图所示:


    原生工程与Flutter工程之间的关系


    Flutter工程之间的关系


    根据已有的客户端基建的开发经验,我们将所有 Flutter 工程分为了四层:



    • 壳工程

    • 业务层

    • 公共层

    • 容器层


    容器层负责提供 Flutter 的基础运行环境,包括 Flutter 引擎管理、页面栈管理、网络框架、KV存储、数据库访问、埋点框架、Native 与 Flutter 通信通道和其它基础功能。


    公共层包含一些通用的开源库、自定义UI组件、部分通用业务等。


    业务层包含用户信息、商品、发布等业务组件。


    壳工程负责集成各业务组件,最终构建出产物集成到 Native 主工程。


    其中业务层、公共层、容器层都是由若干个独立的工程所组成,整体结构如下:


    Flutter分层架构


    开发框架


    开发框架是为了提高开发效率、规范代码结构、减少维护成本等考虑而设计的一套软件框架,包括:基础能力、状态管理、页面栈管理等。


    基础能力


    开发框架需要提供各种必要的能力,比如:页面跳转、埋点、网络请求、图片加载、数据存储等,为了最大化减少研发成本,我们在底层定义了一套通用的数据交互协议,直接复用了现有的 Native 的各项能力,也使得 Native 的各种状态与 Flutter 侧能够保持统一。


    状态管理


    相信了解 Flutter 的同学一定知道状态管理,这也是跟 Native 开发区别较大的地方。在开发较为复杂的页面时,状态维护是非常繁琐的,在不引入状态管理框架的情况下,开发效率会受很大影响,后期的维护成本以及业务交接都是很大的问题。


    另外,在开发框架设计之初,我们就期望从框架上能够在一定程度上限定代码结构、模块之间的交互方式、状态更新方式等,我们期望的是不同的人写出来的代码在逻辑、结构和风格上都能保持比较统一,即在提高开发效率的同时,也能保证项目后续的可维护性和扩展性,减少不同业务间的交接成本。


    基于上述这些需求,在我们对比了多个开源项目后,FishRedux 的整体使用感受正好符合我们的要求。


    如下图,两个页面的代码结构基本一致:


    收藏详情和个人主页


    页面栈管理


    在早期版本,Flutter 引擎的实例占用内存较高,为了减少内存消耗,大家普遍采用单实例的模式,而在 Native 和 Flutter 混合开发的场景下就会存在一个问题,就是 Native 有自己的页面栈,而 Flutter 也维护着一套自己的页面栈,如果 Native 页面与 Flutter 页面穿插着打开,在没有特殊处理的情况下,页面栈会发生错乱。在调研了业内的各种开源方案后,我们选择引入 FlutterBoost 用来管理页面混合栈。


    脚本工具


    为了方便开发同学搭建 Flutter 的开发环境,同时能够管理使用的 Flutter 版本,我们开发了 zflutter 命令行工具,包含以下主要功能:



    • Flutter开发环境安装

    • Flutter版本管理

    • 创建模版工程(主工程、组件工程)

    • 创建模版页面(常规页面、列表页、瀑布流页面)

    • 创建页面模块

    • 组件工程发布

    • 构建Flutter产物

    • 脚本自更新


    如图:


    zflutter


    自动化构建


    客户端使用的是自研的 Beetle 平台(集工程管理、分支管理、编译、发布于一体),短时间内要支持上 Flutter 不太现实,基于此,我们先临时自己搭台服务器,通过 gitlab 的 webhook 功能结合 zflutter 工具简单实现了一套自动化构建的服务,待 Beetle 支持 Flutter 组件化开发功能后,再将工作流切回到 Beetle 平台。


    小范围试验


    在完成基建一期的开发工作后,我们决定通过开发几个实际业务来试验目前的基础设施是否达到既定目标。


    我们以不影响主流程、能覆盖常见UI功能、并且能跟 Native 页面做AB测试(主要是方便在出问题时能够切换到 Native 版本)为条件挑选了个人资料页和留言列表页进行了 Flutter 化改造,如下图所示:


    个人资料页/留言列表页


    这两个页面涵盖了网络请求、图片加载、弹窗、列表、下拉刷新、上拉加载更多、左滑删除、埋点上报、页面跳转等常见功能,足以覆盖日常开发所需的基础能力。


    经过完整的开发流程以及一段时间的线上观察,我们得出如下结论:


    基础能力


    目前已具备的基础能力已经足够支撑普通业务开发(开发过程中补足了一些缺失的能力)。


    工作流


    整个开发过程在工程依赖管理和分支管理方面的支持还比较缺失,比较依赖人工处理。


    开发效率


    我们在开发前根据页面功能同时做了纯 Native 开发排期和 Flutter 开发排期,按单人日的成本来对比的话,Flutter 实际开发耗时跟 Native 排期耗时比为 1.25:2,Native 是按照 Android+iOS 两端各一人算的,也就是1.25人/日比2人/日,如果后续对 Flutter 技术熟悉度提升后相信效率还可以进一步提升。


    性能体验


    线上两个 Flutter 页面的体验效果跟 Native 对比基本感觉不到差别,但是首次进入 Flutter 页面时会有短暂的白屏等待时间,这个是由于 Flutter 环境初始化导致的延迟,后续可以想办法优化。


    包体积


    在引入 Flutter 之后,转转的安装包体积在两端都分别有所增加:



    • Android增加6.1M

    • iOS增加14M


    试验结果基本符合预期,包体积的增量也在我们的可接受范围内,接下来将进行基建二期的建设,补足目前缺失的能力。


    基建二期建设


    基建二期的内容主要包含以下工作:



    • 配合工程效率组完成 Beetle 对 Flutter 项目的支持

    • 组织客户端内部进行 Flutter 技术培训


    Beetle支持Flutter


    为了能让大家更清晰的了解 Beetle 的工程管理机制,这里先简单介绍下客户端的工程类型:



    • Native主工程(又分为 Android 和 iOS)

    • Native组件工程(又分为 Android 和 iOS)

    • Flutter主工程

    • Flutter组件工程(即 Flutter 插件工程)


    举个例子,当有一个新版本需要开发时,先从 Native 主工程创建一个版本同时创建一个 Release 分支,即版本分支,然后从版本分支根据具体需求创建对应 Native 组件的版本分支,Flutter 主工程此时可看作是一个 Native 组件,比如此时创建了一个 Flutter 主工程的版本分支后,可以进入 Flutter 主工程再根据需要创建对应的 Flutter 组件工程的版本分支。


    Beetle 目前已支持 Flutter 工程管理、分支管理、组件依赖管理以及组件的发布、Flutter 产物的构建等,Beetle 的作用贯穿从开发到上线的整个工作流。


    Flutter技术培训


    为了让大家更快的熟悉 Flutter 开发,我们在客户端内部组织了5次 Flutter 快速入门的系列分享:


    Flutter快速入门系列


    同时也逐步完善内部文档的建设,包括:FlutterSdk 源码维护策略、Flutter 入门指南、Flutter 混合开发方案、Flutter 与 Native 通信方案、Flutter 开发环境配置、Flutter 组件化工程结构、Flutter 开发与调试、Flutter 开发工作流、ZFlutter 工具使用介绍、Flutter 开发之 Beetle 使用指南等,涵盖了从环境搭建、开发调试到构建发布的整个过程。


    大范围推广


    在完成基建二期的建设后,整体基础设施已经能够支撑我们常见的业务,开发工作流也基本顺畅,于是我们开始了在内部大范围推广计划。


    我们先后改造和新开发了个人主页、我发布的页面、微商详、奇趣数码页等业务,基本涵盖了常见的各种类型的页面和功能,整体开发效率与原生单端开发效率持平,但是在特别复杂的页面的性能表现上,Flutter 的表现相对要差一些。


    部分页面如下图所示:


    个人主页


    微详情页/我发布的/奇趣数码


    探索前端生态


    在跨端技术领域我们知道 Web 技术是天然支持的,如果能把前端生态引入到 Flutter 中,那么对客户端来说,在业务的支持度上会更上一个台阶,Web 的体验得到提升的同时客户端也具备了动态化,基于此背景我们开始探索 Flutter 在 Web 上的可能性。


    技术调研


    当时可选的开源方案有:Kraken、MXFlutter、Flutter For Web。


    Kraken


    Kraken 是一款基于 W3C 标准的高性能渲染引擎。Kraken 底层基于 Flutter 进行渲染,通过其自绘渲染的特性,保证多端一致性。上层基于 W3C 标准实现,拥有非常庞大的前端开发者生态。


    Kraken 的最上层是一个基于 W3C 标准而构建的 DOM API,在下层是所依赖的 JS 引擎,通过 C++ 构建一个 Bridge 与 Dart 通信。然后这个 C++ Bridge 把 JS 所调用的一些信息,转发到 Dart 层。Dart 层通过接收这些信息,会去调用 Flutter 所提供的一些渲染能力来进行渲染。


    Kraken 是不依赖 Flutter Widget,而是依赖 Flutter Widget 的底层渲染数据结构 —— RenderObject。Kraken 实现了很多 CSS 相关的能力和一些自定义的 RenderObject,直接将生成的 RenderObject 挂载在 Flutter RenderView 上来进行渲染,通过这样的方式能够做到非常高效的渲染性能。


    MXFlutter


    MXFlutter 是一套使用 TypeScript/JavaScript 来开发 Flutter 应用的框架。


    MXFlutter 把 Flutter 的渲染逻辑中的三棵树(即:WidgetTree、Element、RenderObject )中的第一棵(即:WidgetTree),放到 JavaScript 中生成。用 JavaScript 完整实现了 Flutter 控件层封装,实现了轻量的响应式 UI 框架,支撑JS WidgetTree 的 build逻辑,build 过程生成的UI描述, 通过Flutter 层的 UI 引擎转换成真正的 Flutter 控件显示出来。


    Flutter For Web


    Flutter 在 Web 平台上以浏览器的标准 API 重新实现了引擎。目前有两种在 Web 上呈现内容的选项:HTML 和 WebGL。



    • 在 HTML 模式下,Flutter 使用 HTML、CSS、Canvas 和 SVG 进行渲染。

    • 在 WebGL 模式下,Flutter 使用了一个编译为 WebAssembly 的 Skia 版本,名为 CanvasKit。


    HTML 模式提供了最佳的代码大小,CanvasKit 则提供了浏览器图形堆栈渲染的最快途径,并为原生平台的内容提供了更高的图形保真度。


    结论


    我们对以上方案从接入成本、渲染性能、包体积、开发生态、学习成本等多维度进行了对比:



    • 接入成本:Kraken ≈ MXFlutter ≈ Flutter For Web

    • 渲染性能:Kraken > MXFlutter > Flutter For Web

    • 包体积增量:Flutter For Web < Kraken < MXFlutter

    • 开发生态:Kraken ≈ MXFlutter > Flutter For Web

    • 学习成本:Flutter For Web < Kraken ≈ MXFlutter


    最终选择了 Kraken 作为我们的首选方案。


    上线验证


    为了使 Kraken 顺利接入转转App,我们做了以下几个方面的工作:



    • 升级 FlutterSdk 到最新版,满足接入 Kraken 的基础条件

    • 统一客户端容器接口,使得 Kraken 容器能够完美继承 Web 容器的能力

    • 自己维护 Kraken 源码,及时修复官方来不及修复的问题,方便增加转转特有的扩展能力

    • 制定 Kraken 容器与 Web 容器的降级机制

    • 兼容 HTML 加载,保持跟 Web 容器一致的加载方式

    • 添加监控埋点,量化指标,指导后续优化方向

    • 选择一个简单 Web 页并协助前端同学适配


    上线后,我们对页面的各项指标进行了对比,使用 Kraken 容器加载比使用 WebView 加载,在首屏加载耗时的指标上平均增加了281毫秒,原因为:当前版本的 Kraken 容器不支持直接加载 HTML,且只能加载单个 JsBundle,导致加载效率比 WebView 差。


    通过跟前端同学沟通,从开发效率上来看,Kraken 工程的开发周期会比实现同样需求的普通 Web 工程增加1.5到2倍的时间,主要原因是受到 CSS 样式、Api 差异,无法使用现有UI组件,另外 Kraken 的调试工具目前还不够完善,使用浏览器调试后还须在客户端容器中调试,整体下来导致开发 Kraken 工程会比开发普通Web工程耗费更多时间。


    再次验证


    由于之前选择的 Web 页面太过简单,不具备代表性,所以我们重新选定了“附近的人”页面做为改造目标,再次验证 Kraken 在实际开发过程中的效率及性能体验。页面如图所示:


    附近的人


    最终因为部分问题得不到解决,并且整体性能较差,导致页面没能成功上线。


    存在的问题包括但不限于下面列举的一些:



    • 表现不一致问题

      1. CSS 定位、布局表现与浏览器表现不一致

      2. 部分 API 表现与浏览器不一致(getBoundingClientRect等)

      3. iOS,Android系统表现不一致



    • 重大 Bug

      1. 页面初始化渲染完成,动态修改元素样式,DOM不重新渲染

      2. 滑动监听计算导致 APP 崩溃



    • 调试成本高

      1. 不支持 vue-router,单项目单路由

      2. 不支持热更新,npm run build 预览

      3. 不支持 sourceMap,无法定位源代码

      4. 真机调试只支持 element 和 network;dom 和 element 无法互相选中;无法动态修改 dom 结构,无法直接修改样式.......

      5. 页面白屏,假死



    • 安全性问题

      1. 无浏览器中的“同源策略”限制



    • 兼容性

      1. npm 包不兼容等




    通过这一系列的探索和尝试,我们了解到了 Kraken 目前还存在许多不足,如果继续应用会带来高额的开发调试以及维护成本,所以暂时停止了在 Kraken 方向上的投入,但我们仍然在这个方向上保持着关注。


    结尾


    目前转转在Flutter方向上的实践和探索只是一个起点,我们意识到仍然有很多工作需要去做。我们坚信Flutter作为一项领先的跨端技术,将为转转业务的发展带来巨大的潜力和机会。我们将持续努力,加强技术建设,不断完善实践经验,推动Flutter在转转的应用和发展,为用户提供更好的产品和体验。


    作者:转转技术团队
    来源:juejin.cn/post/7304831120709697588
    收起阅读 »

    Android 绘制你最爱的马赛克

    前言 我们之前写过《Android 实现LED 展示效果》,在这篇文章中,我们使用了图像分块(或者是分片)的算法,这样做的目的是降低像素扫描的时间复杂度,并且也利于均色采样。其实图像分块是很常见的图像动效处理手段,一般主要用于场景变换,这项技术也和图像光栅化类...
    继续阅读 »

    前言


    我们之前写过《Android 实现LED 展示效果》,在这篇文章中,我们使用了图像分块(或者是分片)的算法,这样做的目的是降低像素扫描的时间复杂度,并且也利于均色采样。其实图像分块是很常见的图像动效处理手段,一般主要用于场景变换,这项技术也和图像光栅化类似。


    什么是光栅化


    光栅化渲染(Rasterized Rendering)直译过来是栅格化渲染。寻找图像中被几何图形占据的所有像素的过程称为栅格化,因此对象顺序渲染(Object-order rendering)也可以称为栅格化渲染。


    Qu-es-la-rasterizacin-y-cual-es-su-diferencia-con-el-Ray-Tracing.jpg


    我们今天的所要用到的技术也是栅格化和像素采样技术。


    LED原理简述


    马赛克是一种图像编辑技术,广泛应用于隐私保护和涂鸦渲染,很多手机系统自带了这种效果,那如何才能实现这种技术呢?


    了解过我之前的文章的知道,我们制作LED有几个特征



    • 每个LED单元要么亮要么不亮

    • 每个LED单元只有一种颜色

    • 每个LED单元和其他LED单元存在一定的间距

    • 所有LED的单元成网格排列

    • 每个LED单元大小一致


    以上相当于顶点坐标信息,我们拿到网格的位置,就能拿到LED整个区域的片段,知道这个区域的片段我们就可以修改其像素。


    着色采样:


    即便是每个矩形区域,也有很多像素点,如果每个矩形区域每个像素都要进行均色计算的话,那10x10的也要100此,因此为了更快的效率,需要对LED 范围内的像素点采样,求出颜色均值,均值色就是LED最终展示的颜色。


    避坑——修改像素


    上一篇我们知道,通过Bitmap.setPixel方法修改像素效率是极低的,我曾经写过一篇通过修改像素生成圆形图片的文章,在那篇文章里我们看到,像素本身也是有size的,导致最终的圆形图片存在大量锯齿,主要原因是通过这种方式没法做到双线性过滤(图片放大之后会对边缘优化),还有另一个问题,就是效率极差。
    总结一下修改像素的问题:



    • 无法抗锯齿

    • 效率低


    避坑——透明色


    像素中往往存在 color为0或者alpha通道为0的情况,甚至有的区域因为采样原因导致清晰度急剧下降,甚至出现了透明区域噪点,这些问题主要来自于alpha 通道引发的颜色稀释问题,因此在采样时一定要规避这两种情况,至于会不会失真?答案是如果采用alpha失真只会更严重。


    清晰度问题


    同样,清晰度也容易受到这olor为0或者alpha通道为0的情况情况干扰,除了这两种就是采样区域的大小了,理论上采样网格密度越密,清晰度越高,越接近原始图片,因此一定要权衡,太清晰不就很原图一样了么,还制作什么LED呢?


    马赛克原理


    实际上,马赛克原理和LED展示方式类似,为什么这么说呢?从特征来看,几乎一样,马赛克和LED效果只在两部分存在区别



    1. 马赛克网格之间不存在间距

    2. 马赛克采样次数比LED要少


    马赛克没有LED间距很好理解,至于次数少的好处第一肯定是效率高,其次是采样太多容易接近原色,而LED是要有一定程度接近原色。


    技术实现


    本篇我们邀请一位可爱的猫猫,老师们太耀眼的图片就算了,不利于大家阅读。


    ic_cat.png


    我们接下来的任务是把给猫脸打上马赛克,了解完这项技术实现后,其实你不仅可以给猫脸打马赛克,自行涂鸦,指哪儿打哪儿。


    基本信息


    private Bitmap mBitmap; //猫图
    private float blockWidth = 30; //30x30的像素快
    private RectF blockRect = new RectF(); //猫头区域
    private RectF gridRect = new RectF(); //网格区域

    Canvas 包裹Bitmap


    主要方便绘制和内存回收


    static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
    super(bitmap);
    this.bitmap = bitmap;
    }
    public Bitmap getBitmap() {
    return bitmap;
    }
    }


    定位猫头位置


    由于时间关系,我没有做TOUCH事件处理,就写了这个猫头区域


    / 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
    blockRect.set(bitmapCanvas.bitmap.getWidth() - 400, 0, bitmapCanvas.bitmap.getWidth() - 100, 300);

    网格分割


    //根据平分猫头矩形区域
    int col = (int) (blockRect.width() / blockWidth);
    int row = (int) (blockRect.height() / blockWidth);

    网格定位


    float startX = blockRect.left;
    float startY = blockRect.top;
    for (int i = 0; i < row * col; i++) {
    int x = i % col;
    int y = (i / col);
    gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x * blockWidth + blockWidth, startY + y * blockWidth + blockWidth);

    }

    采样和着色


    float startX = blockRect.left;
    float startY = blockRect.top;
    for (int i = 0; i < row * col; i++) {
    int x = i % col;
    int y = (i / col);
    gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x * blockWidth + blockWidth, startY + y * blockWidth + blockWidth);
    //采样
    int sampleColor = mBitmap.getPixel((int) gridRect.centerX(), (int) gridRect.centerY());
    mCommonPaint.setColor(sampleColor);
    //着色,我们这里不修改像素,而是drawRect,避免性能问题和锯齿问题
    bitmapCanvas.drawRect(gridRect, mCommonPaint);
    }

    渲染到View上


    canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);

    效果预览


    fire_56.gif


    避坑点


    网格区间不易过小,和LED一样,越小清晰度越高,就会失去了处理的意义。


    全部代码


    public class MosaicView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();

    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private Bitmap mBitmap; //猫图
    private float blockWidth = 30; //30x30的像素快
    private RectF blockRect = new RectF(); //猫头区域
    private RectF gridRect = new RectF(); //网格区域
    private boolean showMask = false;

    public MosaicView(Context context) {
    this(context, null);
    }
    public MosaicView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }
    public MosaicView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDM = getResources().getDisplayMetrics();
    initPaint();
    }

    private void initPaint() {
    //否则提供给外部纹理绘制
    mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mCommonPaint.setAntiAlias(true);
    mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
    mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }
    private Bitmap decodeBitmap(int resId) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inMutable = true;
    return BitmapFactory.decodeResource(getResources(), resId, options);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    if (widthMode != MeasureSpec.EXACTLY) {
    widthSize = mDM.widthPixels / 2;
    }
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (heightMode != MeasureSpec.EXACTLY) {
    heightSize = widthSize / 2;
    }
    setMeasuredDimension(widthSize, heightSize);
    mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
    bitmapCanvas.bitmap.recycle();
    }
    bitmapCanvas = null;

    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = getWidth();
    int height = getHeight();
    if (width < 1 || height < 1) {
    return;
    }
    if (bitmapCanvas == null || bitmapCanvas.bitmap == null) {
    bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888));
    } else {
    bitmapCanvas.bitmap.eraseColor(Color.TRANSPARENT);
    }
    float radius = Math.min(width / 2f, height / 2f);

    //关闭双线性过滤
    // int flags = mCommonPaint.getFlags();
    // mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
    // mCommonPaint.setFilterBitmap(false);

    int save = bitmapCanvas.save();
    bitmapCanvas.drawBitmap(mBitmap, 0, 0, mCommonPaint);


    // 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
    blockRect.set(bitmapCanvas.bitmap.getWidth() - 560, 10, bitmapCanvas.bitmap.getWidth() - 100, 410);

    if(showMask) {
    //根据平分猫头矩形区域
    int col = (int) (blockRect.width() / blockWidth);
    int row = (int) (blockRect.height() / blockWidth);

    float startX = blockRect.left;
    float startY = blockRect.top;

    for (int i = 0; i < row * col; i++) {
    int x = i % col;
    int y = (i / col);
    gridRect.set(startX + x * blockWidth, startY + y * blockWidth, startX + x * blockWidth + blockWidth, startY + y * blockWidth + blockWidth);
    //采样
    int sampleColor = mBitmap.getPixel((int) gridRect.centerX(), (int) gridRect.centerY());
    mCommonPaint.setColor(sampleColor);
    //着色,我们这里不修改像素,而是drawRect,避免性能问题和锯齿问题
    bitmapCanvas.drawRect(gridRect, mCommonPaint);
    }
    }else{
    Paint.Style style = mCommonPaint.getStyle();
    mCommonPaint.setStyle(Paint.Style.STROKE);
    mCommonPaint.setColor(Color.MAGENTA);
    mCommonPaint.setStrokeWidth(8);
    bitmapCanvas.drawRect(blockRect, mCommonPaint);
    mCommonPaint.setStyle(style);

    }

    bitmapCanvas.restoreToCount(save);
    int saveCount = canvas.save();
    canvas.translate(width / 2f, height / 2f);
    mainRect.set(-radius, -radius, radius, radius);
    canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);
    canvas.restoreToCount(saveCount);

    }

    public void openMask() {
    showMask = true;
    postInvalidate();
    }

    public void closeMask() {
    showMask = false;
    postInvalidate();

    }

    static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
    super(bitmap);
    //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
    this.bitmap = bitmap;
    }
    public Bitmap getBitmap() {
    return bitmap;
    }
    }
    }

    总结


    实际上还有另一种方法,我们绘制图片时关闭双线性过滤


    //关闭双线性过滤
    // int flags = mCommonPaint.getFlags();
    // mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
    // mCommonPaint.setFilterBitmap(false);

    然后将图片放到很大,这个时候你的图片就会产生一定的网格区域,截图然后进行一系列矩阵转换,最后把图贴到原处就出现了马赛克,但是这个有个问题,超高像素的图片得先缩小,然后再放大,显然处理步骤比较多。


    下图是先缩小20倍然后画到原来大小的效果

    企业微信20231205-221500@2x.png


    实现代码


    本来不打算放代码的,想想还是放上吧


    public class BitmapMosaicView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();
    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private BitmapCanvas srcThumbCanvas; //Canvas 封装的
    private Bitmap mBitmap; //猫图
    private RectF blockRect = new RectF(); //猫头区域

    public BitmapMosaicView(Context context) {
    this(context, null);
    }
    public BitmapMosaicView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }
    public BitmapMosaicView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDM = getResources().getDisplayMetrics();
    initPaint();
    }

    private void initPaint() {
    //否则提供给外部纹理绘制
    mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mCommonPaint.setAntiAlias(true);
    mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
    mBitmap = decodeBitmap(R.mipmap.ic_cat);

    }
    private Bitmap decodeBitmap(int resId) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inMutable = true;
    return BitmapFactory.decodeResource(getResources(), resId, options);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    if (widthMode != MeasureSpec.EXACTLY) {
    widthSize = mDM.widthPixels / 2;
    }
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (heightMode != MeasureSpec.EXACTLY) {
    heightSize = widthSize / 2;
    }
    setMeasuredDimension(widthSize, heightSize);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
    bitmapCanvas.bitmap.recycle();
    }
    if (srcThumbCanvas != null && srcThumbCanvas.bitmap != null && !srcThumbCanvas.bitmap.isRecycled()) {
    srcThumbCanvas.bitmap.recycle();
    }
    bitmapCanvas = null;

    }

    private Rect srcRectF = new Rect();
    private Rect dstRectF = new Rect();

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = getWidth();
    int height = getHeight();
    if (width < 1 || height < 1) {
    return;
    }
    if (bitmapCanvas == null || bitmapCanvas.bitmap == null) {
    bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888));
    } else {
    bitmapCanvas.bitmap.eraseColor(Color.TRANSPARENT);
    }
    if (srcThumbCanvas == null || srcThumbCanvas.bitmap == null) {
    srcThumbCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmap.getWidth()/35, mBitmap.getHeight()/35, Bitmap.Config.ARGB_8888));
    } else {
    srcThumbCanvas.bitmap.eraseColor(Color.TRANSPARENT);
    }
    float radius = Math.min(width / 2f, height / 2f);

    //关闭双线性过滤
    int flags = mCommonPaint.getFlags();
    mCommonPaint.setFlags(flags &~ Paint.FILTER_BITMAP_FLAG);
    mCommonPaint.setFilterBitmap(false);
    mCommonPaint.setDither(false);


    srcRectF.set(0,0,mBitmap.getWidth(),mBitmap.getHeight());
    dstRectF.set(0,0, srcThumbCanvas.bitmap.getWidth(), srcThumbCanvas.bitmap.getHeight());

    int save = bitmapCanvas.save();
    srcThumbCanvas.drawBitmap(mBitmap, srcRectF, dstRectF, mCommonPaint);

    srcRectF.set(dstRectF);
    dstRectF.set(0,0,bitmapCanvas.bitmap.getWidth(),bitmapCanvas.bitmap.getHeight());
    bitmapCanvas.drawBitmap(srcThumbCanvas.bitmap, srcRectF,dstRectF, mCommonPaint);
    // 这个区域到时候可以自己调制,想让什么部位有马赛克那就会有马赛克
    blockRect.set(bitmapCanvas.bitmap.getWidth() - 560, 10, bitmapCanvas.bitmap.getWidth() - 100, 410);
    bitmapCanvas.restoreToCount(save);
    int saveCount = canvas.save();
    canvas.translate(width / 2f, height / 2f);
    mainRect.set(-radius, -radius, radius, radius);
    canvas.drawBitmap(bitmapCanvas.bitmap, null, mainRect, mCommonPaint);
    canvas.restoreToCount(saveCount);

    }
    static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
    super(bitmap);
    //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
    this.bitmap = bitmap;
    }
    public Bitmap getBitmap() {
    return bitmap;
    }
    }
    }

    总结下本文分享技术特点:

    • 网格化
    • 采样
    • canvas着色,不要去修改像素


    作者:时光少年
    来源:juejin.cn/post/7308925069916225588
    收起阅读 »

    Android 实现LED 展示效果

    一、前言 LED以其卓越的亮度和醒目的文字和图案,已成为车水马龙的城市中充满烟火气息的象征,深层次的是您红灯的闪烁唤醒着人们的娱乐、怀旧、童年的记忆。当然对新时代来说这显然格格不入的,因此这种霓虹灯能存在多久显然还是个问题。 效果预览 二、实现原理 最初的设...
    继续阅读 »

    一、前言


    LED以其卓越的亮度和醒目的文字和图案,已成为车水马龙的城市中充满烟火气息的象征,深层次的是您红灯的闪烁唤醒着人们的娱乐、怀旧、童年的记忆。当然对新时代来说这显然格格不入的,因此这种霓虹灯能存在多久显然还是个问题。


    效果预览



    二、实现原理


    最初的设想是利用BitmapShader  + Shader 实现网格图片,但是最终是失败的,因此绘制出的网格不是纯色。


    为什么是需要网格纯色呢 ,主要原因是LED等作为单独的实体,单个LED智能发出一种光,电视也是一样的道理,微小的发光单元不可能同时发出多种光源,这也是LED显示屏的制作原理。至于我们的自定义View,本身是细腻的屏幕上发出的,如果一个LED发出多种光,就会显得很假。但事实上,在绘制View时一个区域可能会出现多种颜色,如何平衡这种颜色也是个问题,优化方式当然是增加采样点;但是采样点多了也会带来新的副作用,一是性能问题,而是过多的全透明和alpha为0的情况,因为这种情况会过度稀释真是的颜色,造成模糊不清的问题,其次和View本身的背景穿透,形成较大范围的噪点,所以绘制过程中一定要控制采样点的数量,其次对alpha为0或者过小的的情况剔除,当然不用担心失真,因为过度的透明人眼会认为是全透明,没有太多意义,我们来做个总结:



    • LED 单元智能发出一种光,因此不适合BitampShader做风格渲染

    • 颜色逼真程度和采样点有关,采样点越多越逼近真色

    • 清晰程度和LED单元大小相关,LED单元越小越清晰

    • 剔除alpha通道过小和颜色值为0的采样点颜色 


    三、核心逻辑


    生成刷子纹理


         if (brushBitmap == null) {
    brushBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    brushCanvas = new Canvas(brushBitmap);
    }

    for (int i = 0; i < drawers.size(); i++) {
    int saveCount = brushCanvas.save();
    drawers.get(i).draw(brushCanvas, width, height, mCommonPaint);
    brushCanvas.restoreToCount(saveCount);
    }

    生成网格数据


            float blockWidth = (squareWidth + padding);
    int w = width;
    int h = height;
    int columNum = (int) Math.ceil(w / blockWidth);
    int rowNum = (int) Math.ceil(h / blockWidth);

    if (gridRects.isEmpty() && squareWidth > 1f) {
    //通过rowNum * columNum方式降低时间复杂度
    for (int i = 0; i < rowNum * columNum; i++) {

    int col = i % columNum;
    int row = (i / columNum);

    Rect rect = new Rect();
    rect.left = (int) (col * blockWidth);
    rect.top = (int) (row * blockWidth);
    rect.right = (int) (col * blockWidth + squareWidth);
    rect.bottom = (int) (row * blockWidth + squareWidth);
    //记录网格点
    gridRects.add(rect);
    }

    }

    采样绘制


        //这里是重点 ,LED等可以看作一只灯泡,灯泡区域要么全亮,要么全不亮
    for (int i = 0; i < gridRects.size(); i++) {
    Rect rect = gridRects.get(i);

    if (brushBitmap.getWidth() <= rect.right) {
    continue;
    }
    if (brushBitmap.getHeight() <= rect.bottom) {
    continue;
    }

    if (sampleColors == null) {
    sampleColors = new int[9];
    }

    //取7个点采样,纯粹是为了性能考虑,如果想要更准确的颜色,可以多采样几个点

    sampleColors[0] = brushBitmap.getPixel(rect.left, rect.top); // left-top
    sampleColors[1] = brushBitmap.getPixel(rect.right, rect.top); // right-top
    sampleColors[2] = brushBitmap.getPixel(rect.right, rect.bottom); // right-bottom
    sampleColors[3] = brushBitmap.getPixel(rect.left, rect.bottom); // left-bottom
    sampleColors[4] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 2); //center

    sampleColors[5] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 4); //top line
    sampleColors[6] = brushBitmap.getPixel(rect.left + rect.width() * 3 / 4, rect.top + rect.height() / 2); //right line
    sampleColors[7] = brushBitmap.getPixel(rect.left + rect.width() / 4, rect.top + rect.height() / 2); // left line
    sampleColors[8] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() * 3 / 4); // bottom line

    int alpha = 0;
    int red = 0;
    int green = 0;
    int blue = 0;
    int num = 0;

    for (int c : sampleColors) {
    if (c == Color.TRANSPARENT) {
    //剔除全透明的颜色,必须剔除
    continue;
    }
    int alphaC = Color.alpha(c);
    if (alphaC <= 0) {
    //剔除alpha为0的颜色,当然可以改大一点,防止降低清晰度
    continue; }
    alpha += alphaC;
    red += Color.red(c);
    green += Color.green(c);
    blue += Color.blue(c);
    num++;
    }

    if (num < 1) {
    continue;
    }

    //求出平均值
    int rectColor = Color.argb(alpha / num, red / num, green / num, blue / num);
    if (rectColor != Color.TRANSPARENT) {
    mGridPaint.setColor(rectColor);
    // canvas.drawRect(rect, mGridPaint); //绘制矩形
    canvas.drawCircle(rect.centerX(), rect.centerY(), squareWidth / 2, mGridPaint); //绘制圆
    }
    }

    如果不剔除颜色,那么就会有噪点和清晰度问题



    全部代码


    public class LedDisplayView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mGridPaint;
    private TextPaint mCommonPaint;
    private List<IDrawer> drawers = new ArrayList<>();
    private Bitmap brushBitmap = null;
    private float padding = 2; //分界线大小
    private float squareWidth = 5; //网格大小
    private List<Rect> gridRects = new ArrayList<>();
    int[] sampleColors = null;
    private Canvas brushCanvas = null;

    public LedDisplayView(Context context) {
    this(context, null);
    }

    public LedDisplayView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public LedDisplayView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDM = getResources().getDisplayMetrics();
    initPaint();

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    if (widthMode != MeasureSpec.EXACTLY) {
    widthSize = mDM.widthPixels / 2;
    }

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (heightMode != MeasureSpec.EXACTLY) {
    heightSize = widthSize / 2;
    }
    setMeasuredDimension(widthSize, heightSize);
    }


    public float dp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (brushBitmap != null && !brushBitmap.isRecycled()) {
    brushBitmap.recycle();
    }
    brushBitmap = null;
    }


    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int width = getWidth();
    int height = getHeight();
    if (width <= padding || height <= padding) {
    return;
    }

    if (brushBitmap == null) {
    brushBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    brushCanvas = new Canvas(brushBitmap);
    }

    for (int i = 0; i < drawers.size(); i++) {
    int saveCount = brushCanvas.save();
    drawers.get(i).draw(brushCanvas, width, height, mCommonPaint);
    brushCanvas.restoreToCount(saveCount);
    }


    float blockWidth = (squareWidth + padding);
    int w = width;
    int h = height;
    int columNum = (int) Math.ceil(w / blockWidth);
    int rowNum = (int) Math.ceil(h / blockWidth);

    if (gridRects.isEmpty() && squareWidth > 1f) {
    //通过rowNum * columNum方式降低时间复杂度
    for (int i = 0; i < rowNum * columNum; i++) {

    int col = i % columNum;
    int row = (i / columNum);

    Rect rect = new Rect();
    rect.left = (int) (col * blockWidth);
    rect.top = (int) (row * blockWidth);
    rect.right = (int) (col * blockWidth + squareWidth);
    rect.bottom = (int) (row * blockWidth + squareWidth);
    //记录网格点
    gridRects.add(rect);
    }

    }
    int color = mGridPaint.getColor();

    //这里是重点 ,LED等可以看作一只灯泡,灯泡区域要么全亮,要们全不亮
    for (int i = 0; i < gridRects.size(); i++) {
    Rect rect = gridRects.get(i);

    if (brushBitmap.getWidth() <= rect.right) {
    continue;
    }
    if (brushBitmap.getHeight() <= rect.bottom) {
    continue;
    }

    if (sampleColors == null) {
    sampleColors = new int[9];
    }

    //取7个点采样,纯粹是为了性能考虑,如果想要更准确的颜色,可以多采样几个点

    sampleColors[0] = brushBitmap.getPixel(rect.left, rect.top); // left-top
    sampleColors[1] = brushBitmap.getPixel(rect.right, rect.top); // right-top
    sampleColors[2] = brushBitmap.getPixel(rect.right, rect.bottom); // right-bottom
    sampleColors[3] = brushBitmap.getPixel(rect.left, rect.bottom); // left-bottom
    sampleColors[4] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 2); //center

    sampleColors[5] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 4); //top line
    sampleColors[6] = brushBitmap.getPixel(rect.left + rect.width() * 3 / 4, rect.top + rect.height() / 2); //right line
    sampleColors[7] = brushBitmap.getPixel(rect.left + rect.width() / 4, rect.top + rect.height() / 2); // left line
    sampleColors[8] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() * 3 / 4); // bottom line

    int alpha = 0;
    int red = 0;
    int green = 0;
    int blue = 0;
    int num = 0;

    for (int c : sampleColors) {
    if (c == Color.TRANSPARENT) {
    //剔除全透明的颜色,必须剔除
    continue;
    }
    int alphaC = Color.alpha(c);
    if (alphaC <= 0) {
    //剔除alpha为0的颜色,当然可以改大一点,防止降低清晰度
    continue;
    }
    alpha += alphaC;
    red += Color.red(c);
    green += Color.green(c);
    blue += Color.blue(c);
    num++;
    }

    if (num < 1) {
    continue;
    }

    //求出平均值
    int rectColor = Color.argb(alpha / num, red / num, green / num, blue / num);
    if (rectColor != Color.TRANSPARENT) {
    mGridPaint.setColor(rectColor);
    // canvas.drawRect(rect, mGridPaint); //绘制矩形
    canvas.drawCircle(rect.centerX(), rect.centerY(), squareWidth / 2, mGridPaint); //绘制圆
    }
    }
    mGridPaint.setColor(color);

    }


    private void initPaint() {
    // 实例化画笔并打开抗锯齿
    mGridPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mGridPaint.setAntiAlias(true);
    mGridPaint.setColor(Color.LTGRAY);
    mGridPaint.setStyle(Paint.Style.FILL);
    mGridPaint.setStrokeCap(Paint.Cap.ROUND); //否则网格绘制

    //否则提供给外部纹理绘制
    mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mCommonPaint.setAntiAlias(true);
    mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mCommonPaint.setStrokeCap(Paint.Cap.ROUND);

    }

    public void addDrawer(IDrawer drawer) {
    if (drawer == null) return;
    this.drawers.add(drawer);
    gridRects.clear();
    postInvalidate();
    }

    public void removeDrawer(IDrawer drawer) {
    if (drawer == null) return;
    this.drawers.remove(drawer);
    gridRects.clear();
    postInvalidate();
    }

    public void clearDrawer() {
    this.drawers.clear();
    gridRects.clear();
    postInvalidate();
    }

    public List<IDrawer> getDrawers() {
    return new ArrayList<>(drawers);
    }

    public interface IDrawer {
    void draw(Canvas canvas, int width, int height, Paint paint);
    }

    }

    使用方式


           LedDisplayView displayView = findViewById(R.id.ledview);
    final BitmapDrawable bitmapDrawable1 = (BitmapDrawable)getResources().getDrawable(R.mipmap.mm_07);
    final BitmapDrawable bitmapDrawable2 = (BitmapDrawable)getResources().getDrawable(R.mipmap.mm_08);
    ledDisplayView.addDrawer(new LedDisplayView.IDrawer() {

    Matrix matrix = new Matrix();
    @Override
    public void draw(Canvas canvas, int width, int height, Paint paint) {
    canvas.translate(width/2,height/2);
    matrix.preTranslate(-width/2,-height/4);
    Bitmap bitmap1 = bitmapDrawable1.getBitmap();
    canvas.drawBitmap(bitmap1,matrix,paint);

    matrix.postTranslate(width/2,height/4);
    Bitmap bitmap2 = bitmapDrawable2.getBitmap();
    canvas.drawBitmap(bitmap2,matrix,paint);
    }
    });
    ledDisplayView.addDrawer(new LedDisplayView.IDrawer() {
    @Override
    public void draw(Canvas canvas, int width, int height, Paint paint) {
    paint.setColor(Color.CYAN);
    float textSize = paint.getTextSize();
    paint.setTextSize(sp2px(50));
    canvas.drawText("你好,L E D", 100, 200, paint);
    canvas.drawText("85%", 100, 350, paint);

    paint.setColor(Color.YELLOW);
    canvas.drawCircle(width*3 / 4, height / 4, 100, paint);

    paint.setTextSize(textSize);
    }
    });

    四、总结


    这个本质上的核心就是采样,通过采样我们最终实现了纹理贴图,这点类似open gl中的光栅化,将图形分割成小三角形一样,最后着色,理解本篇也能帮助大家理解open gl和led显示原理。


    作者:时光少年
    来源:juejin.cn/post/7304973928039153705
    收起阅读 »

    Android 图片分片过渡效果

    前言 在之前的文章中,通过LED效果、马赛克效果两篇文章,介绍了分片绘制的效果的方法和原理,通过这两篇文章,相信大家都已经熟悉了分片绘制的思路。其实分片绘制不仅仅能实现LED、马赛克等特殊效果,实际上类似百叶窗、图片对角线锯齿过渡等,很多PPT中存在的特效,基...
    继续阅读 »

    前言


    在之前的文章中,通过LED效果马赛克效果两篇文章,介绍了分片绘制的效果的方法和原理,通过这两篇文章,相信大家都已经熟悉了分片绘制的思路。其实分片绘制不仅仅能实现LED、马赛克等特殊效果,实际上类似百叶窗、图片对角线锯齿过渡等,很多PPT中存在的特效,基本上也是按照这种原理来实现的。


    分片可以有很多种意想不到的效,我们再来说一下分片特点:



    • [1] 按一定的距离、大小、角度对区域进行对一张图片或者区域裁剪或者提取区域图像

    • [2] 对提取出来的区域进行一系列变换,如百叶窗、微信摇一摇等

    • [3] 被裁剪的区域可以还原回去


    技术前景


    其实单纯的分片可以做一些瓦片效果,当然还可以做一些组合效果,下面是一个github开源项目(Camera2DApplication)利用Camera和图片分片实现的效果,这个过程中对一张图片进行分片绘制。


    fire_58.gif


    代码中的逻辑不是很复杂,本质上就是利用2张图片实现的,我们先来看下代码实现,作者的代码很认真,注释都写了,涉及postTranslate比较难懂的操作我也进行了微调。


    /**
    * 3d旋转效果
    *
    * @param canvas
    */

    private void drawModeNormal(Canvas canvas) {
    //VERTICAL时使用rotateY,HORIZONTAL时使用rotateX
    if (orientation == VERTICAL) {
    //如果是前进,则画当前图,后退则画上一张图,注释用的是前进情况
    matrix.reset();
    camera.save();
    //旋转角度 0 - -maxDegress
    camera.rotateX(-degress);
    camera.getMatrix(matrix);
    camera.restore();

    //绕着图片top旋转
    matrix.preTranslate(-viewWidth / 2f, 0);
    //旋转轴向下平移,则图片也向下平移
    matrix.postTranslate(viewWidth / 2f, rotatePivotY);
    //如果是前进,则画当前图,后退则画上一张图,因为后退时,这里画的是动画下方出来的图片,而下方的图片是前一张图
    canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? currentIndex : preIndex), viewWidth, viewHeight),
    matrix, mPaint);

    //在处理下一张图片
    matrix.reset();
    camera.save();
    //旋转角度 maxDegress - 0
    camera.rotateX(maxDegress - degress);
    camera.getMatrix(matrix);
    camera.restore();

    //绕着图片bottom旋转
    matrix.preTranslate(-viewWidth / 2f, -viewHeight);
    //旋转轴向下平移,则图片也向下平移
    matrix.postTranslate(viewWidth / 2f, rotatePivotY);
    //如果是前进,则画下一张图,后退则画当前图,后退时,这边代码画的是动画上方的图片,上方的图片是当前图片
    canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? nextIndex : currentIndex), viewWidth, viewHeight),
    matrix, mPaint);
    } else {
    //如果是前进,则画当前图,后退则画上一张图,注释用的是前进情况
    matrix.reset();
    camera.save();
    //旋转角度 0 - maxDegress
    camera.rotateY(degress);
    camera.getMatrix(matrix);
    camera.restore();

    //绕着图片left旋转
    matrix.preTranslate(0, -viewHeight / 2);
    //旋转轴向右平移,则图片也向右平移
    matrix.postTranslate(rotatePivotX, viewHeight / 2);
    //如果是前进,则画当前图,后退则画上一张图,因为后退时,这里画的是动画右方出来的图片,而右方的图片是前一张图
    canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? currentIndex : preIndex), viewWidth, viewHeight),
    matrix, mPaint);

    //在处理下一张图片
    matrix.reset();
    camera.save();
    //旋转角度 -maxDegress - 0
    camera.rotateY(-maxDegress + degress);
    camera.getMatrix(matrix);
    camera.restore();

    //绕着图片right旋转
    matrix.preTranslate(-viewWidth, -viewHeight / 2f);
    //旋转轴向右平移,则图片也向右平移
    matrix.postTranslate(rotatePivotX, viewHeight / 2f);
    //如果是前进,则画下一张图,后退则画当前图,后退时,这边代码画的是动画左方的图片,左方的图片是当前图片
    canvas.drawBitmap(getBitmapScale(bitmapResourceIds.get(isForward ? nextIndex : currentIndex), viewWidth, viewHeight),
    matrix, mPaint);
    }
    }

    分片操作


    下面是分片操作,这个地方其实可以不用创建Bitmap缓存,创建Path就行,绘制时对Path区域利用Shader贴图即可。


    private Bitmap getBitmapScale(int resId, float width, float height) {
    if (ImageCache.getInstance().getBitmapFromMemCache(String.valueOf(resId)) != null) {
    return ImageCache.getInstance().getBitmapFromMemCache(String.valueOf(resId));
    }
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
    //创建分片
    Bitmap bitmapDst = Bitmap.createScaledBitmap(bitmap, (int) width, (int) height, false);
    bitmap.recycle();

    ImageCache.getInstance().addBitmapToMemoryCache(String.valueOf(resId)
    , bitmapDst);
    return bitmapDst;
    }

    小试一下


    我们这里通过一个简单的Demo,实现一种特效,这次我们利用网格矩阵分片。说到矩阵,很多人面试的时候都会遇到一些算法题,比较幸运的人遇到的是矩阵旋转90度、逆时针打印矩阵、矩阵孤岛问题、从左上角开始进行矩阵元素搜索,运气稍差的会遇到由外到里顺时针打印矩阵和斜对角打印矩阵,后面两种看似简单的问题实际上做起来并不顺手,有点扯远了,我们来看看效果。


    fire_59.gif


    你没看错,这次遇到了算法问题,我这边用的空间换取时间的方法。


    图像分片


    将图片分片,计算出网格的列和行


    int col = (int) Math.ceil(mBitmaps[index].getWidth() / blockWidth);
    int row = (int) Math.ceil(mBitmaps[index].getHeight() / blockWidth);

    分片算法


    这个算法实际上是每次将列数 +1,然后按对角分割,把符合的区域添加到path中


    int x = xPosition;
    int y = 0;
    while (x >= 0 && y <= row) {
    if (x < col && y < row) {
    dstRect.set((int) (x * blockWidth), (int) (y * blockWidth), (int) (x * blockWidth + blockWidth), (int) (y * blockWidth + blockWidth));
    // bitmapCanvas.drawBitmap(mBitmaps[index], dstRect, dstRect, mCommonPaint);
    path.addRect(dstRect, Path.Direction.CCW); //加入网格分片
    }
    x--;
    y++;
    }

    Path 路径贴图



    • Path过程中我们添加的rect是闭合区域,是可以贴图的,当然,一般有三种方法:

    • Path的贴图一般使用 clipPath对图片裁剪然后贴图,当然还有将对应的图片区域绘制到View上

    • Path 是Rect,按照Rect将图片区域绘制到Rect区域

    • 使用BitmapShader一次性绘制


    实际上我们应该尽可能使用Bitmap,因为BitmapShader唯一是不存在锯齿性能比较好的绘制方法。


    int save = bitmapCanvas.save();
    mCommonPaint.setShader(new BitmapShader(mBitmaps[index], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    bitmapCanvas.drawPath(path,mCommonPaint);
    bitmapCanvas.restoreToCount(save);

    其实我们的核心代码到这里就结束了,我们可以看到,分片可以的意义很重要的,当然,借助其他工具也可以实现,不过代码实现的好处是可以编辑和交互,不是所有的动画都可以产生交互。


    到此,我们还可以对今天的demo添加一些想象



    • 从中间外扩效果

    • 奇偶行切换效果

    • 国际象棋黑白格子变换效果

    • ......


    总结


    这是我们的第三篇关于图片分片特效的博客,希望通过一些了的文章,熟悉一些技术,往往看似高大上的效果,其实就是通过普普通通的方法叠加在一起的,当然,让你的技术承载你的想象,才是最重要的。


    本篇demo全部代码


    实际上代码贴太多很可能没人看,但是依照惯例,我们给出完整代码。


    public class TilesView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private RectF mainRect = new RectF();
    private BitmapCanvas bitmapCanvas; //Canvas 封装的
    private Bitmap[] mBitmaps;
    private RectF dstRect = new RectF();
    Path path = new Path();
    private float blockWidth = 50f;
    private int xPosition = -2;
    private int index = 0;
    private boolean isTicking = false;
    public TilesView(Context context) {
    this(context, null);
    }
    public TilesView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public TilesView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDM = getResources().getDisplayMetrics();
    initPaint();
    }

    private void initPaint() {
    //否则提供给外部纹理绘制
    mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    mCommonPaint.setAntiAlias(true);
    mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
    mCommonPaint.setFilterBitmap(true);
    mCommonPaint.setDither(true);
    mBitmaps = new Bitmap[3];
    mBitmaps[0] = decodeBitmap(R.mipmap.mm_013);
    mBitmaps[1] = decodeBitmap(R.mipmap.mm_014);
    mBitmaps[2] = decodeBitmap(R.mipmap.mm_015);
    }

    private Bitmap decodeBitmap(int resId) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inMutable = true;
    return BitmapFactory.decodeResource(getResources(), resId, options);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    if (widthMode != MeasureSpec.EXACTLY) {
    widthSize = mDM.widthPixels / 2;
    }
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (heightMode != MeasureSpec.EXACTLY) {
    heightSize = widthSize / 2;
    }
    setMeasuredDimension(widthSize, heightSize);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapCanvas != null && bitmapCanvas.bitmap != null && !bitmapCanvas.bitmap.isRecycled()) {
    bitmapCanvas.bitmap.recycle();
    }
    bitmapCanvas = null;

    }


    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = getWidth();
    int height = getHeight();
    if (width < 1 || height < 1) {
    return;
    }
    if (bitmapCanvas == null || bitmapCanvas.bitmap == null || bitmapCanvas.bitmap.isRecycled()) {
    bitmapCanvas = new BitmapCanvas(Bitmap.createBitmap(mBitmaps[index].getWidth(), mBitmaps[index].getHeight(), Bitmap.Config.ARGB_8888));
    }
    int nextIndex = (index + 1) % mBitmaps.length;
    canvas.drawBitmap(mBitmaps[nextIndex],0,0,mCommonPaint);

    int col = (int) Math.ceil(mBitmaps[index].getWidth() / blockWidth);
    int row = (int) Math.ceil(mBitmaps[index].getHeight() / blockWidth);
    mCommonPaint.setStyle(Paint.Style.FILL);

    // path.reset();
    // for (int x = 0; x < row; x++) {
    // for (int y = 0; y < col; y++) {
    // gridRectF.set(x * blockWidth, y * blockWidth, x * blockWidth + blockWidth, y * blockWidth + blockWidth);
    // canvas.drawRect(gridRectF, mCommonPaint);
    // path.addRect(gridRectF, Path.Direction.CCW);
    // }
    // }

    diagonalEffect(col,row,xPosition,path);
    canvas.drawBitmap(bitmapCanvas.bitmap, 0, 0, mCommonPaint);

    if (isTicking && xPosition >= 0 && xPosition < col * 2) {
    clockTick();
    } else if(isTicking){
    xPosition = -1;
    index = nextIndex;
    isTicking = false;
    }
    }

    private void diagonalEffect(int col, int row, int xPosition,Path path) {
    int x = xPosition;
    int y = 0;
    while (x >= 0 && y <= row) {
    if (x < col && y < row) {
    dstRect.set((int) (x * blockWidth), (int) (y * blockWidth), (int) (x * blockWidth + blockWidth), (int) (y * blockWidth + blockWidth));
    // bitmapCanvas.drawBitmap(mBitmaps[index], dstRect, dstRect, mCommonPaint);
    path.addRect(dstRect, Path.Direction.CCW); //加入网格分片
    }
    x--;
    y++;
    }
    int save = bitmapCanvas.save();
    mCommonPaint.setShader(new BitmapShader(mBitmaps[index], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    bitmapCanvas.drawPath(path,mCommonPaint);
    bitmapCanvas.restoreToCount(save);

    }

    public void tick() {
    isTicking = true;
    xPosition = -1;
    path.reset();
    clockTick();
    }

    private void clockTick() {
    xPosition += 1;
    postInvalidateDelayed(16);
    }


    public float dp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }

    static class BitmapCanvas extends Canvas {
    Bitmap bitmap;
    public BitmapCanvas(Bitmap bitmap) {
    super(bitmap);
    //继承在Canvas的绘制是软绘制,因此理论上可以绘制出阴影
    this.bitmap = bitmap;
    }

    public Bitmap getBitmap() {
    return bitmap;
    }
    }
    }

    作者:时光少年
    来源:juejin.cn/post/7309329004497354804
    收起阅读 »

    qq农场私信我,您菜死了🥬

    web
    最近在写代码的时候发现自己总是有这样几种症状: 脸红心跳,像发烧一样😳; 口干舌燥、咳嗽不停😮‍💨; 脑袋放空,像刚通宵了一般👀; ...... 我逐渐怀疑,自己有没有可能是🐑了,甚至时不时就拿起体温计量一量,拿起自测试纸测一测,这样的情况一直没有得到好转...
    继续阅读 »

    最近在写代码的时候发现自己总是有这样几种症状



    1. 脸红心跳,像发烧一样😳;

    2. 口干舌燥、咳嗽不停😮‍💨;

    3. 脑袋放空,像刚通宵了一般👀;

    4. ......


    我逐渐怀疑,自己有没有可能是🐑了,甚至时不时就拿起体温计量一量,拿起自测试纸测一测,这样的情况一直没有得到好转,直到收到QQ农场给我发来这样的一条信息:


    尊敬的QQ农场主,您去年和今年菜死了!🥬🥬🥬


    🤔于是,我开始分析我症状根因是什么:



    1. 脸红心跳:是因为自己脑海中想象好的实现方案,但实际却写不出一行代码,或者各种Error,导致我心里落差很大,自我怀疑,或者是被人看穿菜的窘迫、害羞?

    2. 口干舌燥:是因为自己陷入了 写不出代码 => 憋着气接着写,不休息喝水 => 写不出代码 这样的闭环🐶里面;

    3. 脑袋放空:摆脱了内耗,很容易得出结论,就是看的技术不够多,写的代码不够多


    痛定思痛,决定在这里立下FLAG,要多看多实践,学习和思考好的代码写法,看得多,写得多。


    今天分享的主要是:用好发布订阅、偏函数的一对多 & 多对一关系工厂函数


    发布订阅 & 偏函数(一对多/多对一关系)


    是一种一对多的模式,或者说多对多的模式;一个事件对应多个处理函数,多个事件对应各自对应的处理函数



    那假如我们想实现一个多对一的关系呢?我们可以使用偏函数


    偏函数个人理解类似工厂函数,利用了闭包的特性


    // 偏函数
    function after(times, cb) {
    let count = 0;
    const result = {};
    return inner(key, value) => {
    result[key] = value;
    count++;
    if (count === times) {
    cb(result);
    }
    };
    }

    结合代码看此处相当于多个inner函数对应一个callback函数,由count来控制是否触发callback,这种模式常常用于异步编程,比如Promise.all



    综合一对多和多对一模式:


    // 偏函数
    function after(times, cb) {
    let count = 0;
    const result = {};
    return (key, value) => {
    result[key] = value;
    count++;
    if (count === times) {
    cb(result);
    }
    };
    }

    // 发布订阅
    const emitter = new (require("events").EventEmitter)();
    const done = after(3, render);

    emitter.on("done", done);
    emitter.on("done", other);

    fs.readFile(file, (err, template) => {
    emitter.emit("done", "template", template);
    });

    fs.readFile(file, (err, data) => {
    emitter.emit("done", "data", data);
    });

    fs.readFile(file, (err, str) => {
    emitter.emit("done", "str", str);
    });



    工厂函数


    类似现实工厂,在代码中用来生产特定结构函数/对象等的函数


    比如想实现一个生成校验函数的工厂函数:


    /**
    * config里可以包含一般的描述性属性,钩子函数等
    **/
    export function factory(config) {
    config.before = config.before || ((d) => d);
    // pre钩子
    handlersMap[config.type]?.pre(config);

    return function (data) {
    // before钩子函数
    data = config.before(data);
    return handlersMap[config.type].check(data);
    };
    }

    // 通过该方法注册不同的校验函数
    const handlersMap = {};
    factory.registerHandler = function (type, handler) {
    handlersMap[type] = handler;
    };

    在项目中的实现可如图:



    🌊总结:


    阅读好的代码,并学习一些好的写法,才是比较实际提高代码能力的方式,我也将💪持续阅读好的代码库,思考学习好的代码,把自己的成长分享出来。


    作者:Kuroo
    来源:juejin.cn/post/7182545613282623549
    收起阅读 »