SegmentFault 推荐的文章
2024-03-19T11:30:52+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
为什么现在连Date类都不建议使用了?
https://segmentfault.com/a/1190000044725611
2024-03-19T11:30:52+08:00
2024-03-19T11:30:52+08:00
sum墨
https://segmentfault.com/u/summo_java
0
<h2>一、有什么问题吗<code>java.util.Date</code>?</h2><p><code>java.util.Date</code>(<code>Date</code>从现在开始)是一个糟糕的类型,这解释了为什么它的大部分内容在 Java 1.1 中被弃用(但不幸的是仍在使用)。</p><p>设计缺陷包括:</p><ul><li>它的名称具有误导性:它并不代表一个日期,而是代表时间的一个瞬间。所以它应该被称为<code>Instant</code>——正如它的<code>java.time</code>等价物一样。</li><li>它是非最终的:这鼓励了对继承的不良使用,例如<code>java.sql.Date</code>(这<em>意味着</em>代表一个日期,并且由于具有相同的短名称而也令人困惑)</li><li>它是可变的:日期/时间类型是自然值,可以通过不可变类型有效地建模。可变的事实<code>Date</code>(例如通过<code>setTime</code>方法)意味着勤奋的开发人员最终会在各处创建防御性副本。</li><li>它在许多地方(包括)隐式使用系统本地时区,<code>toString()</code>这让许多开发人员感到困惑。有关此内容的更多信息,请参阅“什么是即时”部分</li><li>它的月份编号是从 0 开始的,是从 C 语言复制的。这导致了很多很多相差一的错误。</li><li>它的年份编号是基于 1900 年的,也是从 C 语言复制的。当然,当 Java 出现时,我们已经意识到这不利于可读性?</li><li>它的方法命名不明确:<code>getDate()</code>返回月份中的某一天,并<code>getDay()</code>返回星期几。给这些更具描述性的名字有多难?</li><li>对于是否支持闰秒含糊其辞:“秒由 0 到 61 之间的整数表示;值 60 和 61 仅在闰秒时出现,即使如此,也仅在实际正确跟踪闰秒的 Java 实现中出现。” 我强烈怀疑大多数开发人员(包括我自己)都做了很多假设,认为 for 的范围<code>getSeconds()</code>实际上在 0-59 范围内(含)。</li><li>它的宽容没有明显的理由:“在所有情况下,为这些目的而对方法给出的论据不必落在指定的范围内; 例如,日期可以指定为 1 月 32 日,并被解释为 2 月 1 日。” 多久有用一次?</li></ul><p><strong>关键原因如下:</strong><br><img src="/img/remote/1460000044725613" alt="" title=""></p><p>原文如下:<a href="https://link.segmentfault.com/?enc=aOyfUmM00c2ponpDs9c7cw%3D%3D.l6%2F1TRNFZvxgo5QWbuRDz03pvuu4psp6tJQnw7RPabALCaTDGyJf8zYImZU9NQ4cn3vAFe%2BaFqXIww5l43OXL%2Bvn1b0wmOTiZ4lFeQiAf3E%3D" rel="nofollow">为什么要避免使用Date类?</a></p><h2>二、为啥要改?</h2><p>我们要改的原因很简单,我们的代码缺陷扫描规则认为这是一个必须修改的缺陷,否则不给发布,不改不行,服了。</p><p><img src="/img/remote/1460000044725614" alt="" title=""></p><blockquote>解决思路:避免使用<code>java.util.Date</code>与<code>java.sql.Date</code>类和其提供的API,考虑使用<code>java.time.Instant</code>类或<code>java.time.LocalDateTime</code>类及其提供的API替代。</blockquote><h2>三、怎么改?</h2><p>只能说这种基础的类改起来牵一发动全身,需要从DO实体类看起,然后就是各种Converter,最后是DTO。由于我们还是微服务架构,业务服务依赖于基础服务的API,所以必须要一起改否则就会报错。这里就不细说修改流程了,主要说一下我们在改造的时候遇到的一些问题。</p><h3>1. 耐心比对数据库日期字段和DO的映射</h3><h4>(1)确定字段类型</h4><p>首先你需要确定数据对象中的 <code>Date</code> 字段代表的是日期、时间还是时间戳。</p><ul><li>如果字段代表日期和时间,则可能需要使用 <code>LocalDateTime</code>。</li><li>如果字段仅代表日期,则可能需要使用 <code>LocalDate</code>。</li><li>如果字段仅代表时间,则可能需要使用 <code>LocalTime</code>。</li><li>如果字段需要保存时间戳(带时区的),则可能需要使用 <code>Instant</code> 或 <code>ZonedDateTime</code>。</li></ul><h4>(2)更新数据对象类</h4><p>更新数据对象类中的字段,把 <code>Date</code> 类型改为适当的 <code>java.time</code> 类型。</p><h3>2. 将DateUtil中的方法改造</h3><h4>(1)替换原来的new Date()和Calendar.getInstance().getTime()</h4><p><strong>原来的方式:</strong></p><pre><code class="Java">Date nowDate = new Date();
Date nowCalendarDate = Calendar.getInstance().getTime();</code></pre><p><strong>使用 <code>java.time</code> 改造后:</strong></p><pre><code class="Java">// 使用Instant代表一个时间点,这与Date类似
Instant nowInstant = Instant.now();
// 如果需要用到具体的日期和时间(例如年、月、日、时、分、秒)
LocalDateTime nowLocalDateTime = LocalDateTime.now();
// 如果你需要和特定的时区交互,可以使用ZonedDateTime
ZonedDateTime nowZonedDateTime = ZonedDateTime.now();
// 如果你需要转换回java.util.Date,你可以这样做(假设你的代码其他部分还需要使用Date)
Date nowFromDateInstant = Date.from(nowInstant);
// 如果需要与java.sql.Timestamp交互
java.sql.Timestamp nowFromInstant = java.sql.Timestamp.from(nowInstant);</code></pre><p><strong>一些注意点:</strong></p><ol><li><code>Instant</code> 表示的是一个时间点,它是时区无关的,相当于旧的 <code>Date</code> 类。它通常用于表示时间戳。</li><li><code>LocalDateTime</code> 表示没有时区信息的日期和时间,它不能直接转换为时间戳,除非你将其与时区结合使用(例如通过 <code>ZonedDateTime</code>)。</li><li><code>ZonedDateTime</code> 包含时区信息的日期和时间,它更类似于 <code>Calendar</code>,因为 <code>Calendar</code> 也包含时区信息。</li><li><p>当你需要将 <code>java.time</code> 对象转换回 <code>java.util.Date</code> 对象时,可以使用 <code>Date.from(Instant)</code> 方法。这在你的代码需要与旧的API或库交互时非常有用。</p><h4>(2)一些基础的方法改造</h4><h5>a. dateFormat</h5><p><strong>原来的方式</strong></p><pre><code>public static String dateFormat(Date date, String dateFormat) {
SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);
return formatter.format(date);
}</code></pre><p><strong>使用<code>java.time</code>改造后</strong></p><pre><code class="java">public static String dateFormat(LocalDateTime date, String dateFormat) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
return date.format(formatter);
}</code></pre><h5>b. addSecond、addMinute、addHour、addDay、addMonth、addYear</h5><p><strong>原来的方式</strong></p><pre><code class="java">public static Date addSecond(Date date, int second) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(13, second);
return calendar.getTime();
}
public static Date addMinute(Date date, int minute) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(12, minute);
return calendar.getTime();
}
public static Date addHour(Date date, int hour) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(10, hour);
return calendar.getTime();
}
public static Date addDay(Date date, int day) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(5, day);
return calendar.getTime();
}
public static Date addMonth(Date date, int month) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(2, month);
return calendar.getTime();
}
public static Date addYear(Date date, int year) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(1, year);
return calendar.getTime();
}</code></pre><p><strong>使用<code>java.time</code>改造后</strong></p><pre><code class="java">public static LocalDateTime addSecond(LocalDateTime date, int second) {
return date.plusSeconds(second);
}
public static LocalDateTime addMinute(LocalDateTime date, int minute) {
return date.plusMinutes(minute);
}
public static LocalDateTime addHour(LocalDateTime date, int hour) {
return date.plusHours(hour);
}
public static LocalDateTime addDay(LocalDateTime date, int day) {
return date.plusDays(day);
}
public static LocalDateTime addMonth(LocalDateTime date, int month) {
return date.plusMonths(month);
}
public static LocalDateTime addYear(LocalDateTime date, int year) {
return date.plusYears(year);
}</code></pre><h5>c. dateToWeek</h5><p><strong>原来的方式</strong></p><pre><code class="java">public static final String[] WEEK_DAY_OF_CHINESE = new String[]{"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
public static String dateToWeek(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return WEEK_DAY_OF_CHINESE[cal.get(7) - 1];
}</code></pre><p><strong>使用<code>java.time</code>改造后</strong></p><pre><code class="java">public static final String[] WEEK_DAY_OF_CHINESE = new String[]{"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
public static String dateToWeek(LocalDate date) {
DayOfWeek dayOfWeek = date.getDayOfWeek();
return WEEK_DAY_OF_CHINESE[dayOfWeek.getValue() % 7];
}</code></pre><h5>d. getStartOfDay和getEndOfDay</h5><p><strong>原来的方式</strong></p><pre><code class="java">public static Date getStartTimeOfDay(Date date) {
if (date == null) {
return null;
} else {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);
return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
}
}
public static Date getEndTimeOfDay(Date date) {
if (date == null) {
return null;
} else {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX);
return Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant());
}
}</code></pre><p><strong>使用<code>java.time</code>改造后</strong></p><pre><code class="java">public static LocalDateTime getStartTimeOfDay(LocalDateTime date) {
if (date == null) {
return null;
} else {
// 获取一天的开始时间,即00:00
return date.toLocalDate().atStartOfDay();
}
}
public static LocalDateTime getEndTimeOfDay(LocalDateTime date) {
if (date == null) {
return null;
} else {
// 获取一天的结束时间,即23:59:59.999999999
return date.toLocalDate().atTime(LocalTime.MAX);
}
}</code></pre><h5>e. betweenStartAndEnd</h5><p><strong>原来的方式</strong></p><pre><code class="java">public static Boolean betweenStartAndEnd(Date nowTime, Date beginTime, Date endTime) {
Calendar date = Calendar.getInstance();
date.setTime(nowTime);
Calendar begin = Calendar.getInstance();
begin.setTime(beginTime);
Calendar end = Calendar.getInstance();
end.setTime(endTime);
return date.after(begin) && date.before(end);
}</code></pre><p><strong>使用<code>java.time</code>改造后</strong></p><pre><code class="Java">public static Boolean betweenStartAndEnd(Instant nowTime, Instant beginTime, Instant endTime) {
return nowTime.isAfter(beginTime) && nowTime.isBefore(endTime);
}</code></pre></li></ol><blockquote>我这里就只列了一些,如果有缺失的可以自己补充,不会写的话直接问问ChatGPT,它最会干这事了。最后把这些修改后的方法替换一下就行了。</blockquote><h2>四、小结一下</h2><p>这个改造难度不高,但是复杂度非常高,一个地方没改好,轻则接口报错,重则启动失败,非常耗费精力,真不想改。</p>
关于小程序如何做到强制更新
https://segmentfault.com/a/1190000044712850
2024-03-14T16:36:48+08:00
2024-03-14T16:36:48+08:00
南玖
https://segmentfault.com/u/fenanjiu
5
<h2>前言</h2><p>在小程序的日常迭代中,有一些场景我们可能需要在小程序发布后,用户能够马上感知并更新,比如上线新活动、修复高危漏洞等,如果用户因为各种原因未能及时更新小程序,这就可能导致一些功能无法正常使用或者存在安全隐患,因此,实现小程序的强制更新功能就显得尤为重要。本文将探讨小程序如何做到强制更新,以确保用户始终使用最新、最安全的小程序版本。</p><h2>小程序的运行机制</h2><p>在这之前,我们得先来了解一下小程序的生命周期,从启动到销毁它都是如何进行的</p><h3>生命周期</h3><p><img src="/img/remote/1460000044712852" alt="wx-1.png" title="wx-1.png"></p><h3>小程序的启动</h3><p>广义的小程序启动可以分为两种情况,一种是<strong>冷启动</strong>,一种是<strong>热启动</strong>。</p><p>从小程序生命周期的角度来看,我们一般讲的「<strong>启动</strong>」专指冷启动,热启动一般被称为后台切前台。</p><ul><li>冷启动:如果用户首次打开,或小程序销毁后被用户再次打开,此时小程序需要重新加载启动,即冷启动。</li><li>热启动:如果用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时小程序并未被销毁,只是从后台状态进入前台状态,这个过程就是热启动。</li></ul><h3>前台与后台</h3><ul><li>前台:小程序的「<strong>前台</strong>」状态一般指的是小程序处于打开状态,页面正在展示给用户</li><li>后台:当用户关闭小程序时小程序并没有真正被关闭,而是进入了「<strong>后台</strong>」状态</li></ul><p>切后台的方式包括但不限于以下几种:</p><ul><li>点击右上角胶囊按钮离开小程序</li><li>iOS 从屏幕左侧右滑离开小程序</li><li>安卓点击返回键离开小程序</li><li>小程序前台运行时直接把微信切后台(手势或 Home 键)</li><li>小程序前台运行时直接锁屏</li></ul><h3>挂起</h3><p>小程序进入「后台」状态一段时间后(目前是 5 秒),微信会停止小程序 JS 线程的执行,小程序进入「<strong>挂起</strong>」状态。此时小程序的内存状态会被保留,但开发者代码执行会停止,事件和接口回调会在小程序再次进入「前台」时触发。</p><p>当开发者使用了<strong>后台音乐播放</strong>、<strong>后台地理位置</strong>等能力时,小程序可以在 <strong>「后台」</strong> 持续运行,不会进入到 <strong>「挂起」</strong> 状态</p><h3>销毁</h3><p>当小程序进入后台或被挂起时,它并不会一直保留在后台,当满足以下两点时,小程序会被销毁</p><ul><li>当小程序进入后台并被「挂起」后,如果很长时间(目前是 30 分钟)都未再次进入前台,小程序会被销毁。</li><li>当小程序占用系统资源过高,可能会被系统销毁或被微信客户端主动回收。</li></ul><h2>小程序的更新机制</h2><p>小程序默认的更新机制可以分为两类:<strong>启动时同步更新</strong>、<strong>启动时异步更新</strong></p><h3>启动时同步更新</h3><p>在以下情况下,小程序启动时会同步更新代码包。同步更新会阻塞小程序的启动流程,影响小程序的启动耗时。</p><ul><li>定期检查发现版本更新,微信运行时定时检查下载更新,如果有更新,下次小程序启动时会同步进行更新,更新到最新版本后再打开小程序</li><li>用户长时间未使用小程序,会强制同步更新</li></ul><h3>启动时异步更新</h3><ul><li>即使启动前未发现更新,小程序每次冷启动时,都会异步检查是否有更新版本。如果发现有新版本,将会异步下载新版本的代码包。但当次启动仍会使用客户端本地的旧版本代码,即新版本的小程序需要等下一次冷启动才会使用</li></ul><h3>强制更新</h3><p>在启动时异步更新的情况下,如果开发者希望立刻进行版本更新,可以使用 <code>wx.getUpdateManager</code>API 进行处理,该API会返回一个<code>UpdateManager</code>实例</p><p><code>UpdateManager</code> 对象为小程序提供了四种关键的方法,用于管理和监控小程序的更新过程。</p><ul><li><code>UpdateManager.applyUpdate()</code>:在小程序新版本已经下载完成的情况下(即接收到 <code>onUpdateReady</code> 回调后),此方法用于强制小程序重启并启用新版本。</li><li><code>UpdateManager.onCheckForUpdate(function callback)</code>:此方法用于监听向微信后台发起的更新检查结果事件。微信小程序在冷启动时会自动进行更新检查,开发者无需主动触发。</li><li><code>UpdateManager.onUpdateReady(function callback)</code>:此方法用于监听小程序的新版本更新就绪事件。一旦新版本可用,客户端会自动触发下载过程(无需开发者额外操作),并在下载成功后调用此回调函数。</li><li><code>UpdateManager.onUpdateFailed(function callback)</code>:此方法用于监听小程序更新失败的事件。当小程序有新版本且客户端尝试自动下载更新时(同样无需开发者干预),如果因网络问题或其他原因导致下载失败,将会触发此回调函数。</li></ul><p>根据以上API,我们就能够在小程序法版后,通知用户进行强制更新</p><pre><code class="js">if (taro.canIUse('getUpdateManager')) {
const updateManager = taro.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
console.log('onCheckForUpdate====', res);
if (res.hasUpdate) {
// 小程序已更新
updateManager.onUpdateReady(function () {
taro.showModal({
title: '更新提示',
showCancel: false,
confirmText: '立即重启',
content: '新版本已经上线,是否重启小程序以应用新版本?',
success: function (res) {
if (res.confirm) {
// 调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
}
});
});
// 更新失败
updateManager.onUpdateFailed(function () {
taro.showModal({
title: '更新失败',
content: '新版本下载失败,请删除当前小程序后重新打开。',
});
});
}
});
} else {
// 版本过低
taro.showModal({
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。',
});
}</code></pre><p><img src="/img/remote/1460000044712853" alt="wx-2.gif" title="wx-2.gif"></p><p>因为小程序的开发版和体验版没有版本的概念,所以无法在开发版和体验版上测试更版本更新情况,但可以通过微信开发者工具 => 添加编译模式 => 编译设置 => 下次编译时模拟更新来进行调试</p><p><img src="/img/remote/1460000044712855" alt="wx-3.png" title="wx-3.png"></p>
尝试 Google Gemma 模型 MacOS 本地部署
https://segmentfault.com/a/1190000044710376
2024-03-13T20:08:58+08:00
2024-03-13T20:08:58+08:00
LinkinStar
https://segmentfault.com/u/linkinstar
0
<h2>前言</h2><p>最近 Google 发布了 <a href="https://link.segmentfault.com/?enc=3zNc5uo9FV1w5PC8MIpmiQ%3D%3D.meYfCgDiMqKoA4lDYl96A%2FxBHA5dsDw6633nzjhjDvmTGWaZapMJuJzCiHVIMjSr" rel="nofollow">Gemma</a>,是 Gemini 的低配版本,既然是 Google 出品那我一定要来吃螃蟹的。所以我本地部署了一个 7b 的版本来尝试使用一下看看效果。同时也来说明一些有关大模型本地部署使用的一些个人体会,比如,你可能会有以下问题:</p><ol><li>怎么本地部署使用?</li><li>我本地的电脑能不能跑?</li><li>本地跑的效果到底怎么样?</li></ol><p>首先,我想敲醒你沉睡的脑子。对于本地部署模型,你先要问清楚自己想要的是什么?也就是为什么需要本地部署,如果仅仅是想跑着玩,那没问题。<strong>如果只是平常使用,并且你已经能用 GPT 了,本地其实对于你来说毫无意义,因为你指望你的小电脑哪怕是大显卡能和别人成吨的 A100 相比吗?(夸张的修辞)</strong> 如果,醒了还是想玩,那么可以往下看了,最后我会总结本地去跑有哪些优势。</p><h2>如何部署</h2><p>这里我推荐两个:</p><ul><li><a href="https://link.segmentfault.com/?enc=pJ88uGiocO19%2BHNHwKDJrQ%3D%3D.0fpDNO8a46B5iDYzb7vSZfhYucmsRnrgievY3ay%2F6d8%3D" rel="nofollow">https://ollama.com/</a></li><li><a href="https://link.segmentfault.com/?enc=KREAx6h0IlIeCMb%2BxgjZDQ%3D%3D.w3R%2B9KIeAlbCUjgwDcm%2FRw%3D%3D" rel="nofollow">https://jan.ai/</a></li></ul><p>这二者基本都已经做到了开箱即用的地步了,其中我会更喜欢 ollama 一点。所以我就简单列一下它的步骤(其实官网已经描述的非常详细了,也很简单 <a href="https://link.segmentfault.com/?enc=19RmHcyuMOj76A2EMjl9mA%3D%3D.ihPBzxvrFzS4oACXC3%2B3BhWhTDfKynqzmv5XT%2FqidxwG4WDp5N0IND3i%2FJx8ml4U" rel="nofollow">https://github.com/ollama/ollama</a>)</p><ol><li>下载</li><li>运行 <code>ollama run gemma</code></li><li>使用</li></ol><p>对的,直接在命令行里面就能直接开始问了,并且也提供了 API 接口。如果你需要一个 UI 界面,我推荐使用 <a href="https://link.segmentfault.com/?enc=xwYGWZGCdC%2BdJq4LpuWy2Q%3D%3D.iXa4HJS0UXn813DlYQ4M%2BixIxWh%2BiQcbnvp0kA%2BcbEh5WiIrY8pBUtjPArn7wcfl" rel="nofollow">https://github.com/open-webui/open-webui</a> 也是一个命令就可以直接在本地运行</p><pre><code class="bash">docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main</code></pre><p><img src="/img/remote/1460000044710378" alt="demo" title="demo"></p><h2>能不能跑</h2><p>我本机是 MPB M1 16+256 的配置,一般回答结果反应在 20-30s 左右,根据具体问题的情况来看。我觉得,作为你个人使用,已经是够了。毕竟我也算是老设备了。</p><p>然后我给出我的跑的建议(个人总结,仅供参考,能不能跑起来还是实际说了算):</p><ul><li>8G 及以下的,并且用 CPU 的,只建议跑 2b 的版本,虽然官方认为 8G 跑 7b 为下限,但我还是不建议的。</li><li>16G 的,7b 够用,其他模型的 13b 也能跑但显然会慢一些,具体就看你 CPU 的能力了。</li><li>32G 建议跑 33b 的,当然也要当前模型有</li><li>64G 可以尝试跑 70b 的</li></ul><p>有大显存显卡的用户肯定会更吃的开一点,但我要说的重点其实是在后面</p><h2>效果如何</h2><p>能用!但又不完全能用。(感觉是不是像废话)听我慢慢道来。</p><h3>测试方面</h3><p>我不像很多 AI 模型的专业测试一样去测试各种疑难杂症,或者是测试各种幻觉问题或者是违法问题。我就是平常人最普通使用,哪里来那么多破事情呢?下面几个场景我相信能反应一些问题了</p><h4>翻译</h4><p>自从用了 AI 翻译之后我是再也不想用原来的普通翻译了,那种 one by one 中式翻译不可谓不难受。对于翻译任务来说,我觉得 Gemma 是可以帮助到你的,虽然依据可能有语法错误,但比一般的翻译好,它能理解一些语意意义的翻译。</p><h4>做题</h4><p>数学问题别想了,很容易翻车,其他场景问题还可以。所以如果用它来做题,我不建议。</p><p>比如,二进制转换,显然到 15 这里就不对了。</p><p><img src="/img/remote/1460000044710379" alt="数学" title="数学"></p><p>比如,求和,显然也不对。</p><p><img src="/img/remote/1460000044710380" alt="数学" title="数学"></p><h4>写代码</h4><p>我测试了 go 和 rust,go 不错,基本能实现思路,但也会有莫名奇妙不能被调用的方法,rust 可能不太行,也可能是我水平不够,至少编译总是有各种问题。</p><p><img src="/img/remote/1460000044710381" alt="写代码" title="写代码"></p><p><img src="/img/remote/1460000044710382" alt="写代码" title="写代码"></p><h4>总结</h4><p>可以的,我发现 AI 的总结能力还是比较强的,只要给出的内容不是特别零散的,它都能总结的不错,推荐的。</p><h4>写作</h4><p>别想了,这我原来也没有期望,当他开始胡说八道的时候我就知道不行了。</p><h4>重复劳动</h4><p>可以的,重复劳动有很多的,比如修改各种文章格式,空格、大小写等;再比如对齐;处理表格,简单计算求和。通常来说只要 prompt 写好,来回几次,基本就能得到想要的结果了。</p><h2>总结</h2><p>对于本地部署,我想你肯定是有这几方面的考虑:</p><ol><li>白嫖:不想花钱买 token,可以,一直白嫖一直爽</li><li>隐私:对于被推测的数据不能公开,这一点确实很重要,本地部署直接解决了很多内部数据使用的问题</li><li>服务:想要建立服务并调用接口以服务业务场景,说白就是拿来赚钱,但显然现在的反应速度不够,至少需要很多专业设备的支持。</li></ol><p>那么,我想告诉你的是,对于现阶段而言,基于我本地部署使用了一段时间之后,<strong>我会推荐给想要做本地总结和翻译的用户</strong>,这二者的使用上其实是让我满意的,也能达到我的基本需求。其他工作,我还是会直接尝试使用 GPT 来帮助我完成会更加靠谱。</p><h2>相关链接</h2><ul><li>Gemma 官方网站:<a href="https://link.segmentfault.com/?enc=RycXKV75eeGn73YwdI7Iww%3D%3D.I7TI0tK8l9i1naXU0ydot6PnFdG0UkEMXGrre3Cze2c%3D" rel="nofollow">https://ai.google.dev/gemma/</a></li><li>技术报告:<a href="https://link.segmentfault.com/?enc=mBRZ1vinMTHkV2UuHaS8oA%3D%3D.z0S0v9rHAjKlDGNLVbRXOLkzqAXiWazIk9ZYSdkg%2BfA%3D" rel="nofollow">https://goo.gle/GemmaReport</a></li><li>Kaggle地址:<a href="https://link.segmentfault.com/?enc=MRO8BfDtjONDczWiOoRNNg%3D%3D.9SKEG4ejOgJuAeklNxLJzf4XUcjUmPvuQeFqsuXot4otXUOmkTWxq0nKZqi4rYyXxF3ajpcSbHrlPSLMBkZRGQ%3D%3D" rel="nofollow">https://www.kaggle.com/models/google/gemma/code/</a></li><li>huggingface地址:<a href="https://link.segmentfault.com/?enc=IhpKX3Z3GKjflEPE7Y90fw%3D%3D.ATvIUKkfigzhzk1giyyDvpwKDFB9QSl1aTM%2B8n7vJabfypNPpm7%2BkSHCJm7XrmIYls%2BgJ1wvyi3h2LfRgpNBrA%3D%3D" rel="nofollow">https://huggingface.co/models?search=google/gemma</a></li></ul>
Golang 中 能否将 slice 作为 map 的 key?
https://segmentfault.com/a/1190000044706821
2024-03-12T23:03:57+08:00
2024-03-12T23:03:57+08:00
LinkinStar
https://segmentfault.com/u/linkinstar
0
<h2>前言</h2><p>最近好忙,也好久没水 Golang 的文章了,最近来水一些。说回今天的问题,这个问题非常简单,也能被快速验证。</p><blockquote>Golang 中 能否将 slice 作为 map 的 key?</blockquote><p>如果你现实中使用过,那么这个问题对于你来说其实意义不大,因为不行就是不行,可以就是可以。</p><p>如果你完全没这样使用过 map,那么这个问题对于你来说可能就有意义了。</p><h2>思路</h2><ol><li>首先这个问题的思路在于能否作为 key 的条件是什么?</li><li>key 在 map 中的作用是标记一个 kv,我们需要用 key 去查找对应的 value</li><li>那么我怎么知道,一个输入的 key 是否在这个 map 中呢?答案是比较</li><li>所以只要这个 key 能比较,说白了就是能使用 “==” 进行比较,大概率就没有问题</li></ol><p>所以其实,这个问题的本质是:“slice 能否进行比较?”</p><h2>答案</h2><p>答案显然是不能的,因为 slice 是不能使用 “==” 进行比较的,所以是不能做为 map 的 key 的。<br>而官方文档中也说明了 <a href="https://link.segmentfault.com/?enc=YVcFFge9BO6FB%2But0AQKzw%3D%3D.j8mxSZQ57BBVL%2FMSVjHAZxIc1zBZCEkJ6zkUXpBNjJ4%3D" rel="nofollow">https://go.dev/blog/maps</a></p><blockquote>As mentioned earlier, map keys may be of any type that is comparable. The language spec defines this precisely, but in short, comparable types are boolean, numeric, string, pointer, channel, and interface types, and structs or arrays that contain only those types. Notably absent from the list are slices, maps, and functions; these types cannot be compared using ==, and may not be used as map keys.</blockquote><p>所以如果真的需要以 slice 类似的数据来作为 key,你需要使用 array 而不是 slice,如下:</p><pre><code class="go">package main
import (
"fmt"
)
func main() {
var a, b [1]int
a = [1]int{1}
b = [1]int{2}
m := make(map[[1]int]bool)
m[a] = true
m[b] = true
for i := 0; i < 3; i++ {
fmt.Println(m[[1]int{i}])
}
}</code></pre><p>那么只要数组中的每个对应下标元素相等,则 key 相等</p>
原来浏览器插件有这么多风险?
https://segmentfault.com/a/1190000044703572
2024-03-12T09:51:14+08:00
2024-03-12T09:51:14+08:00
卡颂
https://segmentfault.com/u/kasong
2
<p>嫦美找到我时,整个人是崩溃的 —— “卡颂,我好像被监视了”。</p><p>傍晚的星巴克,她的影子被吊灯拉得很长,颤抖着如同她此刻的内心。</p><p>“怎么回事?”我尽量让声音听起来平静些。</p><p>“最近认识个男生,是我MBA同学,对我很热情,也很懂我”嫦美环顾四周,仿佛随时会有什么东西从夜色中跳出来。</p><p>“缘分啊,这不很好嘛?”我笑着说。</p><p>“不是那种心有灵犀的懂,是那种<strong>生活起居都被监视的懂</strong>”嫦美解释道。不待我回应,又补充道:“这次约你出来,也是想让你帮忙看看我电脑有没有被植入啥监听木马”。</p><p>说罢,从背包里取出<code>MacBook Air</code>递给我。</p><p>“Mac一般安全性都蛮高的,你最近没装啥来路不明的应用吧?”一边摆弄她的电脑,我一边问道。</p><p>“我也不会装应用,平时主要就上上网、刷刷剧”。</p><p>浏览完她的应用列表,我顺手打开了浏览器,又习惯性打开插件列表。</p><p>这时,一个浏览器插件吸引了我的注意:</p><p><img src="/img/remote/1460000044703574" alt="" title=""></p><p>“这是啥?”</p><p>“奥,我们MBA的网课需要在这个平台看。这个平台很严,看课不能快进,也不能切换到其他页面。这是那个男同学发我的,装了后就能突破这些限制,还挺方便”说罢,嫦美皱了皱眉“和这个插件不会有关系吧?”</p><p>“不好说,等我看看插件源码”。</p><p>事实证明,这个插件真的有问题......</p><blockquote>本文参考文章<a href="https://link.segmentfault.com/?enc=UXxc6OVh4OqeF5XNNGH82Q%3D%3D.pKHWiJafPtLmiJSmumX7eQrKsekXPuO1fJ7MwKoiks7qvKzDqeuk4GRupmmtTH%2BvaFk7RqdwSDfYd7xDC05XRg%3D%3D" rel="nofollow" title="Let's build a Chrome extension that steals everything">Let's build a Chrome extension that steals everything</a></blockquote><p><a href="https://link.segmentfault.com/?enc=ezoxs9Rsv4kfAjDCTC7Rww%3D%3D.jG4KeNJUkphfisAs1MIFcNIU%2Fm4Dz3NXLnCO3HGCxNv7Baz3hbQbga%2FDcMyan82kViK23dyx%2F3FF2jxtIo%2FApA%3D%3D" rel="nofollow">免费领取卡颂原创React教程(原价359)、加入人类高质量前端群</a></p><h2>浏览器插件能做什么?</h2><p>浏览器插件为我们上网提供了极大便利,比如:</p><ul><li><code>GPT</code>插件能帮我们一键总结网页内容</li><li>翻译插件能实时翻译网页内容</li><li>去广告插件能去掉网页牛皮癣,还我们清爽的页面</li></ul><p>实际上,浏览器插件除了能<strong>分析并修改原始页面</strong>外,还能:</p><ul><li>获取我们的实时位置</li><li>读取、修改我们复制粘贴的内容</li><li>读取<code>cookie</code>、浏览历史</li><li>屏幕截图</li><li>记录键盘输入</li><li>等等</li></ul><p>可以说,有心人只要利用得当,就能通过浏览器插件获得我们上网的所有足迹。</p><p>这时,有人会说:“插件能做这些没错,但必须申请必要的权限,我不给他权限不就行了?”</p><p>事实真的这么简单么?</p><h2>安全约束够么?</h2><p>《Building Browser Extensions》一书作者<strong>Matt Frisbie</strong>为了演示浏览器插件的潜在安全问题,构造了一个<strong>会申请全部49项权限</strong>的chrome浏览器插件<a href="https://link.segmentfault.com/?enc=0VHowywqixGe9ZfypGv9vQ%3D%3D.4MlWdrxv0mtgkyi2uGT4qHDdt1PkR%2B8pLoRpEJaM2d4ZAHnMdVqH4%2BdLf4p4Ozs%2F" rel="nofollow" title="spy-extension">spy-extension</a>。</p><p>当你在浏览器安装这个插件后,浏览器确实会提示你<strong>插件申请的权限</strong>:</p><p><img src="/img/remote/1460000044703575" alt="" title=""></p><p>不过,等等!明明申请了49项权限,这里为什么只显示5项?原来,窗口显示的内容行数有限,超出部分需要拖动滚动条才会显示。</p><p>可是,又有几个用户会发现<strong>在申请的5项权限下面,滚动条后面还隐藏了44项权限呢</strong>?</p><p>一旦有了权限,想做什么就取决于插件作者的想象力了。可以被用来做坏事的<code>WebExtensions API</code>非常多,比如:</p><h3>Service Worker</h3><p>后台运行的<code>Service Worker</code>可以监听发出的网络请求,并在请求发送到网络之前修改它们。</p><p>这意味着插件可以使用<code>Service Worker</code>发送数据到服务器,或者在用户浏览网页时拦截请求并发送额外的数据。</p><p>由于<code>Service Worker</code>运行于一个独立的后台进程中,所以打开调试工具的<code>Network</code>面板看不到插件发出的请求:</p><p><img src="/img/remote/1460000044703576" alt="" title=""></p><p>都有哪些有价值的数据可以收集呢?</p><h3>用户敏感数据</h3><p>最简单的,监听用户键盘输入:</p><pre><code class="js">[...document.querySelectorAll("input,textarea,[contenteditable]")].map((input) =>
input.addEventListener("input", _.debounce((e) => {
// 处理 用户输入
}, 1000))
);</code></pre><p>除此之外:</p><ul><li><code>chrome.cookies.getAll({})</code>会以数组的形式返回浏览器的所有<code>cookie</code></li><li><code>chrome.history.search({ text: "" })</code>会以数组形式返回用户的整个浏览历史记录</li><li><code>chrome.tabs.captureVisibleTab()</code>会静默将用户当前正在查看的页面截图,并以<code>data URL</code>的形式返回。</li><li><code>chrome.webRequest</code>可以让插件监控所有<code>Tab</code>的流量</li></ul><p>上述<code>API</code>结合<code>Service Worker</code>传输数据,用户在插件作者面前无异于裸奔。</p><h2>更高阶的玩法</h2><p>据嫦美表示 —— 她那个MBA同学好像知道她住哪儿,这是怎么做到的呢?很有可能是通过<strong>获取地理位置</strong>的插件功能。</p><p>一个网课插件获取地理位置,这不是太奇怪了么?可是嫦美一点都没发觉,这是怎么办到的?</p><p>如果插件脚本获取地理位置(通过<code>navigator.permissions.query({ name: "geolocation" })</code>),将询问用户授权。</p><p>但如果<strong>被注入脚本的网站</strong>已经获得用户的地理位置授权,插件不需要授权就能静默使用对应功能。</p><p>举个例子,如果百度地图向你请求<strong>获取地理位置</strong>的授权,这很合理,你也大概率会同意。</p><p>如果恶意插件可以向百度地图注入脚本,当你访问百度地图时,他就不用再获取授权就能访问你的地理位置。</p><h2>借尸还魂之法</h2><p>以上所说的所有功能都局限在 —— 插件向已有网站注入脚本。那插件是否能不被察觉的直接打开恶意网站呢?</p><p>答案是 —— 可以,我愿称其为<strong>借尸还魂</strong>之法。</p><p>很多朋友都会打开多个浏览器<code>Tab</code>,但常用的可能就是其中几个,剩下的<code>Tab</code>会闲置很长时间。</p><p>而这些<strong>闲置的Tab</strong>就是最好的下手目标。</p><p><img src="/img/remote/1460000044703577" alt="经常打开很多Tab" title="经常打开很多Tab"></p><p>首先,插件通过以下代码筛选出闲置的<code>Tab</code>:</p><pre><code class="js">const tabs = await chrome.tabs.query({
// 筛选用户当前没使用的Tab
active: false,
// 筛选用户没有pin的Tab,pin的Tab使用频率通常比较高
pinned: false,
// 不使用有音频的Tab
audible: false,
// 使用已经加载完毕的Tab
status: "complete",
});
// 筛选出闲置Tab
const [idleTab] = tabs.filter(/** ...省略其他筛选条件 **/)</code></pre><p>只要恶意网站的标题、图标(favicon)与闲置Tab一致,那么用恶意网站替换闲置Tab后,用户也不会有任何察觉。</p><p>举个例子,如果<code>闲置Tab</code>是<code>React官网</code>,那恶意网站只需要标题是<code>React</code>,图标是<code>React</code>,即使<code>闲置Tab</code>跳转到恶意网站,从<code>Tab</code>外观上也无法区分。</p><p><img src="/img/remote/1460000044703578" alt="" title=""></p><p>下面的代码构造了恶意网站的<code>url</code>,其中<strong>与闲置Tab一致的标题、图标</strong>保存在<code>url searchParams</code>中:</p><pre><code class="js">// 将标题、图标保存在searchParams中
const searchParams = new URLSearchParams({
returnUrl: idleTab.url,
faviconUrl: idleTab.favIconUrl || "",
title: idleTab.title || "",
});
const url = `${chrome.runtime.getURL(
"恶意网站.html"
)}?${searchParams.toString()}`;</code></pre><p>恶意网站在<code>url searchParams</code>中取出标题、图标数据,并替换:</p><pre><code class="js">// 修改标题
document.title = searchParams.get('title);
// 修改图标
document.querySelector(`link[rel="icon"]`)
.setAttribute("href", searchParams.get('faviconUrl'));</code></pre><p>最后,用恶意网站替换<code>闲置Tab</code>的网站:</p><pre><code class="js">await chrome.tabs.update(idleTab.id, {
url,
active: false,
});</code></pre><p>恶意网站只需要在<strong>做完坏事后</strong>或<strong>用户重新点击 闲置Tab 时</strong>跳回原来的网站即可。代码如下:</p><pre><code class="js">const searchParams = new URL(window.location.href).searchParams;
function useReturnUrl() {
// 跳回原来网站
window.location.href = searchParams.get('returnUrl');
}
if (document.visibilityState === "visible") {
useReturnUrl();
}
// 用户访问了闲置Tab
document.addEventListener("visibilitychange", () => useReturnUrl());
// ...开始做坏事
// 做完坏事,跳回原来网站
useReturnUrl();</code></pre><p>从用户的视角看,当他点击<code>闲置Tab</code>时,网站重新加载。对于一个闲置的<code>Tab</code>来说,重新访问时加载页面是再正常不过的逻辑。</p><p>只是用户不会知道,这并不是<strong>网站重新加载</strong>,而是<strong>退回到前一个网站</strong>。</p><h2>后记</h2><p>有人会说 —— 我只使用那些信得过的插件。</p><p>但今天信得过的插件,明天就一定信得过么?在暗网中,<strong>用户量大的免费浏览器插件</strong>能卖不错的价钱。</p><p>为什么会有人收购这类<strong>没有商业价值的免费插件</strong>呢?一种可能是 —— 收购后向代码中投毒,只要用户升级插件就会中招。</p><p>所以,好用的插件不一定没问题,今天没问题的插件明天也不一定没问题。</p><p>对于嫦美来说,技术上能做的只能是删除插件、清除缓存、清除<code>cookie</code>,退出所有的账号登录并修改密码。</p><p>但似乎更大的危险,来自现实世界......</p>
ReactNative:使用 react-native-mmkv 来提升应用性能
https://segmentfault.com/a/1190000044639518
2024-03-11T21:35:30+08:00
2024-03-11T21:35:30+08:00
王大冶
https://segmentfault.com/u/minnanitkong
0
<p><img width="730" height="487" src="/img/bVdbsNc" alt="image.png" title="image.png"></p><p>在使用React Native工作时,你很可能已经使用了 <code>AsyncStorage</code> 作为存储解决方案。例如,你可以使用 <code>AsyncStorage</code> 来存储键值对,比如你的应用程序的当前主题,甚至为了各种原因存储状态和令牌。</p><p>除了 <code>AsyncStorage</code> ,我们还可以使用一些第三方存储解决方案。在这篇文章中,我们将研究 <code>react-native-mmkv</code> 库,并探讨为什么你可能想要用它替代 <code>AsyncStorage</code> ,以及如何在我们的应用程序中使用它。</p><h2>为什么使用 react-native-mmkv ?</h2><p>由微信开发的 react-native-mmkv 库允许我们高效地从 MMKV 存储框架存储和读取键值对。它的名字是 React Native <code>memory-map key-value</code> 存储的简称。</p><p>类似于 AsyncStorage , <code>react-native-mmkv</code> 也具有跨平台兼容性,意味着它适用于 iOS 和 Android 平台。让我们来看看你可能会考虑使用 MMKV 而不是 <code>AsyncStorage</code> 的一些原因。</p><h2>加密</h2><p><code>AsyncStorage</code> 是一个未加密的存储系统。不建议使用像 <code>AsyncStorage</code> 这样的存储解决方案来存储密码、令牌和其他私人信息。</p><p><code>MMKV</code> 比 <code>AsyncStorage</code> 更安全,提供数据加密和其他更高级的安全特性。如果你想存储需要高级别安全保护的敏感数据,MMKV是更好的选择。</p><h2>完全同步存储</h2><p><code>AsyncStorage</code> 是一个异步存储系统,利用 <code>async/await</code> <code>与promises</code>。相比之下, <code>react-native-mmkv</code> 的所有调用都是完全同步的,因此可以不使用任何promises进行。</p><p>我们看一下下面的代码以便更好地理解:</p><pre><code>// AsyncStorage
// storing data
const storeUser = async (value) => {
try {
await AsynStorage.setItem("user", JSON.stringify(value));
} catch (error) {
console.log(error);
}
};
storeUser("Chimezie")
// getting data
const getUser = async () => {
try {
const userData = await AsynStorage.getItem("user")
const user = JSON.parse(userData)
} catch (error) {
console.log(error);
}
};
</code></pre><p>如果你看上面的代码,你可以看到我们在存储和检索存储数据时都使用了<code>async/await</code>。</p><p>使用 <code>react-native-mmkv</code> ,我们可以如下所示调用它,而不是发出Promise 请求:</p><pre><code>// react-native-mmkv
storage.set('username', 'Chimezie');
const username = storage.getString('username')</code></pre><p>在上面的 <code>react-native-mmkv</code> 示例中,我们正在将我们的用户名设置到我们的存储中。当我们想要检索数据时,我们使用 getString() 方法,因为我们获取的是字符串数据。<code>react-native-mmkv</code> 不像 <code>AsyncStorage</code> 那样返回一个 <code>promise</code>。</p><h2>序列化</h2><p>如果你观察我们上面的例子,你会注意到我们正在将我们想要存储在 <code>AsyncStorage</code> 中的用户的值转化为字符串。这是因为 <code>AsyncStorage</code> 处理的是字符串值,所以我们在保存之前必须将所有非字符串数据类型序列化。</p><pre><code>const storeUser = async () => {
try {
const userData = {
name: "Chimezie",
location: "Nigeria"
}
const serializedUser = JSON.stringify(userData)
await AsynStorage.setItem("user", serializedUser);
} catch (error) {
console.log(error);
}
};</code></pre><p>同样地,要使用 <code>AsyncStorage</code> 检索数据,我们必须将数据解析回原始数据类型——在这个例子中是一个对象:</p><pre><code>const getUser = async () => {
try {
const userData = await AsynStorage.getItem("user")
const userObject = JSON.parse(userData)
} catch (error) {
console.log(error);
}
};</code></pre><p>对于MMKV来说,并非如此。在这方面,MMKV更为高效,因为它支持不同的基本类型或数据类型,如布尔值,数字和字符串。简单来说,你不需要在存储之前手动序列化所有值。</p><pre><code>storage.set('username', 'Innocent') // string
storage.set('age', 25) // number
storage.set('is-mmkv-fast-asf', true) // boolean</code></pre><h2>更快的性能</h2><p>由于 <code>react-native-mmkv </code>不序列化和解析非字符串数据,所以它比 <code>AsyncStorage</code> 更快。下面的图片来自MMKV团队,展示了从不同存储解决方案中读取数据一千次所需的时间的基准测试结果。MMKV证明比其他所有方案都要快:</p><p><img width="730" height="475" src="/img/bVdbsQe" alt="image.png" title="image.png"></p><p>此外,由于 MMKV 是完全同步的,它消除了等待 <code>promise</code> 完成以获取数据的压力。这使得读写数据变得更加容易和快速——而且不需要处理任何 <code>promise</code> 或错误逻辑。</p><h2>react-native-mmkv 的限制</h2><p>尽管 <code>react-native-mmkv</code> 库有许多优点,但它也有一些限制。在这一部分,我们将讨论在使用MMKV时需要注意的一些事项。</p><h3>调试</h3><p><code>MMKV</code> 利用了JavaScript接口(JSI),它提供了同步的本地访问以提高效率和性能。然而,这对远程调试构成了挑战,因为像 Chrome DevTools 这样的工具使用的是传统的React Native桥接,而不是JSI桥接。</p><p>作为这个限制的一个解决方法,你可以<a href="https://link.segmentfault.com/?enc=lGvANDLDNMuY8a0nchdY%2Fw%3D%3D.m0LRLBpuHyYNK%2Bnav%2Fk4rPeHMrGA%2FjMa5zaXZGiC1bHeOe4gJFDlNFKUGPDQTnZ3EaQAZ8cVXLpkcpUfqSgU5Q%3D%3D" rel="nofollow">使用Flipper调试工具</a>。Flipper是为了在你的应用启用JSI或者你的应用使用像MMKV这样的JSI库时进行调试而设计的。或者,你也可以将你的错误记录到控制台以进行调试。</p><h3>内存大小</h3><p>MMKV库对于存储小量数据如用户偏好、主题状态或应用设置非常高效。没有特定的大小测量或限制,但推荐使用MMKV来存储小数据。</p><p>然而,由于它提供内存存储,存储大量数据将消耗内存并妨碍应用程序的性能。因此,不建议使用MMKV来存储大量数据。</p><h3>文档</h3><p>与 <code>AsyncStorage</code> 不同, <code>react-native-mmkv</code> 库的文档非常有限。唯一可用的文档是库的GitHub仓库中的 README.md 文件,它解释了如何使用这个库。</p><h2>使用 react-native-mmkv</h2><p>我们现在已经看到了一些你可能会考虑使用MMKV而不是 AsyncStorage 的原因,以及它的一些限制。在这个部分,我们将看看如何在我们的应用程序中使用 <code>react-native-mmkv</code> 包来存储和检索键值对数据。</p><p>MMKV 在 Expo 中不工作,因此你可以在一个裸 React Native 项目中使用它,或者通过预构建和弹出你的 Expo 应用来使用。要安装该包,请运行以下任一命令:</p><pre><code>// npm
npm install react-native-mmkv
//yarn
yarn add react-native-mmkv</code></pre><p>接下来,我们将创建一个实例,然后对其进行初始化。然后我们可以在应用程序的任何地方调用这个实例。</p><p>创建一个名为 <code>Storage.js </code>的文件,并复制下面的代码:</p><pre><code>// Storage.js
import { MMKV } from 'react-native-mmkv'
export const storage = new MMKV({
id: `user-storage`,
path: `${USER_DIRECTORY}/storage`,
encryptionKey: 'encryptionkey'
})</code></pre><p>在上述代码中,我们首先导入已安装的包。接下来,我们创建了一个名为 <code>storage</code> 的实例,它可以接受三个选项—— <code>id</code> , <code>path</code> 和 <code>encryptionKey</code> 。</p><p><code>id</code> 是一个用于区分 MMKV 实例的唯一标识符。你可以创建不同的实例来存储不同种类的数据。 <code>id</code> 有助于区分或分隔它们:</p><pre><code>const passwordStorage = new MMKV({
id: `password-storage`,
})
const themeStorage = new MMKV({
id: `theme-storage`,
})</code></pre><p><code>path</code> 是你的设备中MMKV存储数据的根文件。它也允许你自定义你喜欢的路径或目录。</p><p><code>encryptionKey</code> 是一个用于在存储前加密数据和在检索后解密数据的唯一密钥。</p><p>在我们创建了实例之后,我们可以在任何组件中导入该实例并使用它。</p><h2>存储数据</h2><p>正如我们之前看到的,MMKV支持不同的数据类型,这意味着我们可以存储不同的数据类型而无需序列化它们。</p><p>首先,让我们导入我们已经安装的包:</p><pre><code>//App.js
import { storage } from './Storage'</code></pre><p>接下来,我们将使用存储来储存我们的数据:</p><pre><code>storage.set('username', 'Innocent') // string
storage.set('age', 25) // number
storage.set('is-mmkv-fast-asf', true) // boolean</code></pre><p>就是这样!如你在上述代码注释中所见, <code>react-native-mmkv</code> 支持字符串、数字和布尔数据类型。然而,对于对象和数组,我们必须先序列化数据才能保存。</p><pre><code>// objects
const user = {
name: "Chimezie",
location: "Nigeria",
email: 'chimezieinnocent39@gmail.com',
}
storage.set("userDetails", JSON.stringify(user))
// arrays
const numberArray = [1, 2, 3, 4, 5];
const serializedArray = JSON.stringify(numberArray);
storage.set('numbers', serializedArray);</code></pre><h2>检索数据</h2><p>使用MMKV检索数据就像存储它一样简单直接。您使用 <code>getString()</code> 来获取字符串数据,使用 <code>getNumber()</code> 来获取数字数据,使用 <code>getBoolean()</code> 来获取布尔类型的数据:</p><pre><code>const username = storage.getString('username') // 'Innocent'
const age = storage.getNumber('age') // 25
const isMmkvFastAsf = storage.getBoolean('is-mmkv-fast-asf') // true</code></pre><p>对于对象和数组,我们将使用 <code>getString()</code> ,因为我们在保存之前对其进行了序列化——换句话说,将其存储为字符串数据。然后,我们将使用 <code>JSON.parse()</code> 来反序列化或转换回原始状态或数据类型。</p><pre><code>// objects
const serializedUser = storage.getString('userDetails');
const userObject = JSON.parse(serializedUser);
console.log(userObject);
/* output: const user = {
name: "Chimezie",
location: "Nigeria",
email: 'chimezieinnocent39@gmail.com',
} */
// arrays
const serializedArray = storage.getString('numbers');
const numberArray = JSON.parse(serializedArray);
console.log(numberArray); // Output: [1, 2, 3, 4, 5]</code></pre><p>此外,在你想查看存储中所有键的情况下,MMKV通过提供一个名为 <code>getAllKeys()</code> 的方法允许我们做到这一点。这个方法返回一个包含实例中存储的所有键的数组:</p><pre><code>// Set some key-value pairs
storage.set('name', 'Innocent');
storage.set('age', 25);
const allKeys = storage.getAllKeys();
console.log(allKeys); // Output: ['name', 'age']</code></pre><h2>删除数据</h2><p>使用MMKV删除数据类似于 <code>AsyncStorage</code> 。我们可以删除特定的键或我们实例中的所有键:</p><pre><code>// delete a key
storage.delete('username')
// delete all keys
storage.clearAll()
</code></pre><h2>加密数据</h2><p>数据加密是MMKV相较于 <code>AsyncStorage</code> 的另一个优势所在。MMKV提供了在存储数据前加密的选项,而 <code>AsyncStorage</code> 并未提供加密功能。</p><p>在加密任何数据之前,我们必须首先在我们的实例中提供一个加密密钥。MMKV使用这个密钥来加密和解密数据:</p><pre><code>// Storage.js
import { MMKV } from 'react-native-mmkv'
export const storage = new MMKV({
id: `user-storage`,
encryptionKey: 'EncrypTedKey123'
})</code></pre><p>然后,我们可以像这样加密我们想要的任何数据:</p><pre><code>// App.js
storage.set('userPassword', 'This is a secret user password');
// Retrieving data from the encrypted storage
const password = storage.getString('userPassword');
console.log(password); // Output: 'This is a secret user passwor</code></pre><h2>订阅更新</h2><p><code>react-native-mmkv</code> 允许我们订阅键值对数据的更新或更改。要订阅更新,我们可以使用 <code>addOnValueChangedListener()</code> 方法注册一个事件监听器。每当指定的键值对数据发生变化时,监听器就会收到通知。</p><p>我们看看我们如何能做到这一点:</p><pre><code>// App.tsx
import React, {useState, useEffect} from 'react';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import {
Text,
View,
Button,
StatusBar,
StyleSheet,
SafeAreaView,
} from 'react-native';
import {storage} from './Storage';
function App(): JSX.Element {
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
useEffect(() => {
const listener = storage.addOnValueChangedListener(changedKey => {
if (changedKey === 'isDarkMode') {
const newValue = storage.getBoolean(changedKey);
console.log('theme:', newValue);
}
});
return () => {
listener.remove();
};
}, []);
const toggleTheme = () => {
const newMode = !isDarkMode;
setIsDarkMode(newMode);
storage.set('isDarkMode', newMode);
};
return (
<SafeAreaView style={[backgroundStyle, styles.sectionContainer]}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<View>
<Text
style={[
styles.sectionTitle,
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{isDarkMode ? 'Dark Mode' : 'Light Theme'}
</Text>
<Text
style={[
styles.sectionDescription,
{
color: isDarkMode ? Colors.light : Colors.dark,
},
]}>
React Native MMKV Tutorial
</Text>
<Button
onPress={toggleTheme}
title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
sectionContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
textAlign: 'center',
},
sectionDescription: {
marginVertical: 8,
fontSize: 18,
fontWeight: '400',
textAlign: 'center',
},
highlight: {
fontWeight: '700',
},
});
export default App;
</code></pre><p>在上述代码中,我们使用 <code>addOnValueChangedListener()</code> 方法来注册一个回调函数。该函数监听MMKV存储中特定键的变化。</p><p>回调函数将 <code>changedKey</code> 作为参数,使用 <code>storage.getBoolean(changedKey)</code> 获取与该键关联的新值,并将其新值记录到控制台。</p><p>每当MMKV中的键值对被修改时,回调函数将会被调用并带有被改变的键的名称,允许你在应用程序中对变化做出反应。</p><p>最后,我们在 <code>useEffect</code> 返回函数中取消订阅监听器。这样做是为了避免或防止我们的组件卸载时出现内存泄漏。</p><h2>总结</h2><p>在这篇文章中,我们探讨了一些你可能会考虑使用MMKV而非 AsyncStorage 的原因,并考虑了它的一些限制。你可以在<a href="https://link.segmentfault.com/?enc=rzkmyIKOuamBO3sL5eiv1g%3D%3D.kBn5Q4EkzoBKQNZsAVqLJpr8t%2FjgIfGYELAKrFkSRWVbAcqE09kAG9cRNxn3L%2BYtBPIJiNmd26cI7U851WBt6A%3D%3D" rel="nofollow">这个GitLab</a>仓库中查看我们使用的代码示例。</p><p>确实,MMKV比 AsyncStorage 新,也没有像 AsyncStorage 那样广泛的社区。然而,MMKV提供了一个更快、更高效、更安全的存储系统。这个库有超过50个贡献者,并且维护得非常好。</p>
Go arena 民间库来了,可以手动管理内存!
https://segmentfault.com/a/1190000044704496
2024-03-12T12:39:46+08:00
2024-03-12T12:39:46+08:00
煎鱼
https://segmentfault.com/u/eddycjy
0
<p>大家好,我是煎鱼。</p><p>上年我们有讨论过关于 Go arena 手动管理内存的相关提案。一开始还高歌猛进,但没想到后面由于严重的 API 问题(想把 arena 应用到其他的标准库中,但会引入大问题):</p><p><img src="/img/remote/1460000044704498" alt="" title=""></p><p>Go 核心团队中途咕咕咕到现在,没有新的推动和突破性进展,实属尴尬。</p><p><img src="/img/remote/1460000044704499" alt="" title=""></p><p>最近有社区的大佬有了新的动作,来自 Grafana 的 @Miguel Ángel Ortuño 开源了一个新的第三方库 <a href="https://link.segmentfault.com/?enc=mPB2OBrTqDF4PkIZipvGvw%3D%3D.pdt1aApPsKHPoteY6Z9Zb2nPvlTKSJHUq4LHVi0HCOI%3D" rel="nofollow" title="ortuman/nuke">ortuman/nuke</a>,用于完成 arena 手动管理内存的诉求。</p><p>今天我们基于官方资料此进行使用分享和介绍,也好未雨绸缪一下。</p><h2>温习前置知识</h2><p>Arena 指的是一种从一个连续的内存区域分配一组内存对象的方式。当然了,它的重点是要手动管理内存,实现一些编程上的内存管理目标。</p><p>优点比一般的内存分配更有效率,也可以一次性释放。缺点上需要程序员在编程时手动管理,有可能会泄漏和错释放,增大了心智负担。</p><p>简单来讲就是,Arena 可以手动管理内存,可以做很多事,有利有弊。也 “容易” 崩。</p><h2>快速介绍</h2><h3>安装</h3><p>安装命令如下:</p><pre><code class="go">go get -u github.com/ortuman/nuke</code></pre><p>需要注意这个库要求 go >= 1.21.7,在实际下载前建议先进行升级。</p><h3>使用案例</h3><h4>常规使用</h4><p>基本使用该 arean 库的用法,代码如下:</p><pre><code class="go">import (
"github.com/ortuman/nuke"
)
type Foo struct{ A int }
func main() {
arena := nuke.NewMonotonicArena(256*1024, 80)
fooRef := nuke.New[Foo](arena "Foo")
fooSlice := nuke.MakeSlice[Foo](arena, 0, 10 "Foo")
for i := 0; i < 20; i++ {
fooSlice = nuke.SliceAppend(arena, fooSlice, Foo{A: i})
}
// 做一些煎鱼的业务逻辑...
arena.Reset(true)
...
}</code></pre><ul><li>初始化一个新的单一 arean 内存区域,缓冲区大小为 256KB,最大内存上限为 20MB。</li><li>声明和分配一个 Foo 类型的新对象和容量为 10 个元素的 Foo 切片。</li><li>业务逻辑完成后,重置所申请的 arean 内存区域(释放)。</li></ul><p>以上是最常用的方式,相当于在某一块代码片段中进行初始化和处理。</p><h4>基于 context 场景</h4><p>如果我们需要在 HTTP 请求这类整个生命周期中去使用。</p><p>可以借助 context,使用如下方式:</p><pre><code class="go">func httpHandler(w http.ResponseWriter, r *http.Request) {
arena := nuke.NewMonotonicArena(64*1024, 10)
defer arena.Reset(true)
ctx := nuke.InjectContextArena(r.Context(), arena)
processRequest(ctx)
// 给煎鱼静悄悄干点什么...
}
func processRequest(ctx context.Context) {
arena := nuke.ExtractContextArena(ctx)
// ...
}
func main() {
http.HandleFunc("/", httpHandler) fmt.Println("Server is listening on port 8080...")
http.ListenAndServe(":8080", nil)
}</code></pre><p>在请求端 http context 中注入 arena,再在实际处理的地方通过 context 获取 arena,以此达到穿越整体生命周期的方式。</p><h4>基于并发场景</h4><p>默认场景下,<code>nuke.NewMonotonicArena</code> 初始化出来的 arena,有一个隐性的坑,他不是并发安全的!</p><p>大胆猜测,这是基于性能的考虑,所以没有做到一起。毕竟锁会很吃资源。而在 Go 里要去做手动内存管理的场景,多少又对性能有一定的诉求。</p><p>在有并发诉求的场景下,可以使用 <code>NewConcurrentArena</code> 函数:</p><pre><code class="go">import (
"github.com/ortuman/nuke"
)
func main() {
arena := nuke.NewConcurrentArena(
nuke.NewMonotonicArena(256*1024, 20),
)
defer arena.Reset(true)
// 和煎鱼煎个鱼看看...
}</code></pre><p>除了换了个初始化方法,其他用法与常规用法差不多。</p><p>也看了下官方的 Benchmarks,确实是基于性能考虑的区分并发与非并发的业务场景。QPS 越大,性能差距越大:</p><pre><code>BenchmarkMonotonicArenaNewObject/100-8 124495 15469 ns/op 0 B/op 0 allocs/op
BenchmarkMonotonicArenaNewObject/1000-8 76744 19602 ns/op 0 B/op 0 allocs/op
BenchmarkMonotonicArenaNewObject/10000-8 24104 50845 ns/op 0 B/op 0 allocs/op
BenchmarkMonotonicArenaNewObject/100000-8 3282 366044 ns/op 0 B/op 0 allocs/op
BenchmarkConcurrentMonotonicArenaNewObject/100-8 90392 16679 ns/op 0 B/op 0 allocs/op
BenchmarkConcurrentMonotonicArenaNewObject/1000-8 43753 29823 ns/op 0 B/op 0 allocs/op
BenchmarkConcurrentMonotonicArenaNewObject/10000-8 8037 149923 ns/op 0 B/op 0 allocs/op
BenchmarkConcurrentMonotonicArenaNewObject/100000-8 879 1364377 ns/op</code></pre><h2>总结</h2><p>今天给大家分享了 Go 官方 arena 的最新进展和情况,主体上还是由于严重 API 原因(担忧像 context 一样造成传染性)没有突破性进展。虽然有人提出可以放到 unsafe 库中,也获得了许多人表情点赞。但仍然没能打动 Go 核心团队的同学。</p><p>基于此,我们介绍了民间大佬的 arena 开源库 ortuman/nuke。基本功能和使用都能够满足需求。后续有此类业务需求时,可以随时拿起来就用!</p><blockquote>文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 <strong>GitHub</strong> <a href="https://link.segmentfault.com/?enc=hDILaGQctJMKut1txHjEGQ%3D%3D.MKrpZPc5bWbZgY1xtibdNc5oOihTxVVhSvC9sy4wntY%3D" rel="nofollow">github.com/eddycjy/blog</a> 已收录,学习 Go 语言可以看 <a href="https://link.segmentfault.com/?enc=vv%2BxmuuF1Up7haMRVUW1Xg%3D%3D.mVgQazaviYb0%2BSIXH4l9mTqOKI1eqt9lRZbRj%2Fzz%2FFxP5T8WVFSwDg8l9Yuiy4hl" rel="nofollow">Go 学习地图和路线</a>,欢迎 Star 催更。</blockquote><h3>推荐阅读</h3><ul><li><a href="https://link.segmentfault.com/?enc=h%2F4BTazb0cFSKCUX%2BwpxaQ%3D%3D.R503LaVt8PtxS%2FP6o9bO%2B34WR0U%2BUxKzSUs8CySX2PFzp%2F6HqiKKgK7SLqhfgzEi1RO6cOcO0DbmNkRdToEU1g%3D%3D" rel="nofollow">Go1.22 新特性:增强 http.ServerMux 路由能力,将有更强的表现力!</a></li><li><a href="https://link.segmentfault.com/?enc=L9VoJq7sNva66b%2B3EP9K9Q%3D%3D.g7ByFDBDjnfvNGMb1potk8nQg4y7SeR1qP2UDuPsykMSFLcdpHtQxW8kIJQ9vvnYCVK1LoWPZiMWBX4AeAdU8g%3D%3D" rel="nofollow">Go1.22 新特性:for 循环不再共享循环变量,且支持整数范围</a></li><li><a href="https://link.segmentfault.com/?enc=Ww35ol2AUX%2FIA9u0SpgVPQ%3D%3D.HDuo1%2B4THekjVubFSFUppm67evuMmB1Ln9rRLbwa%2FVwvhdr7X%2BCjKDCrelVl6kEpmA1dY8LTYQQsL5Ig1WEA1Q%3D%3D" rel="nofollow">为什么 Go1.22 for 循环要支持整数范围?</a></li><li><a href="https://link.segmentfault.com/?enc=lZtAdjSUyULAXt8XLRfqeA%3D%3D.uJg0mPAFpIEfpcvSR%2FNqL30JLY%2BM2ZvzjZbO2ImNTG2mQNyTA15o36BeL21o0mbrZWqYUaJWor3UNFKGtBX9qg%3D%3D" rel="nofollow">Go1.22 新特性:Slices 变更 Concat、Delete、Insert 等函数,对开发挺有帮助!</a></li><li><a href="https://link.segmentfault.com/?enc=4hWnGjiZidjFlAawZHZrUQ%3D%3D.94lG8Q2XC26ozlXfIr0DXvP40%2FuprSErMvS2wHmboo8%2FSyg5u3D7S53oGUq4tYYDEMiVYYa5t4vPuxwbAT0bww%3D%3D" rel="nofollow">Go1.22 新特性:新的 math/rand/v2 库,更快更标准!</a></li></ul>
发布DDD脚手架到Maven仓库,IntelliJ IDEA 配置一下即可使用
https://segmentfault.com/a/1190000044703303
2024-03-12T08:09:24+08:00
2024-03-12T08:09:24+08:00
小傅哥
https://segmentfault.com/u/fuzhengwei
1
<p>作者:小傅哥 <br>博客:<a href="https://link.segmentfault.com/?enc=JKkiZbi%2BeVCUw80%2Fcn96SA%3D%3D.eEzvbchGFHZJcJ%2FMcyvrF3C0Kn7ZpGVTuo1O9MdJAhk%3D" rel="nofollow">https://bugstack.cn</a> <br>项目:<a href="https://link.segmentfault.com/?enc=PnNRmCGfWMe%2FQq%2BfYPI6Xw%3D%3D.oOjoQvlydcn9l3A6UAc2MPmO2GAL5D3hgP62gu2ywxA%3D" rel="nofollow">https://gaga.plus</a></p><blockquote>沉淀、分享、成长,让自己和他人都能有所收获!😄</blockquote><p>大家好,我是技术UP主,小傅哥。</p><p>这篇文章将帮助粉丝伙伴们更高效地利用小傅哥构建的<code>DDD(领域驱动设计)脚手架</code>,搭建工程项目,增强使用的便捷性。让👬🏻兄弟们直接在 IntelliJ IDEA 配置个在线的链接,就能直接用上这款脚手架!—— <strong>你就说猛不猛!🤨</strong></p><p> <img src="/img/remote/1460000044703305" alt="img" title="img"></p><p><strong>那小傅哥搞的这个在线版脚手架是怎么做到的呢?</strong> 🤔 想学吗,想学我教你呀?</p><p>在23年的时候小傅哥发布了 DDD 两款脚手架,一个轻量版 lite ,一个标准版 std。通过这两款脚手架,小伙伴们在学习小傅哥写的技术教程和实战项目的时候,可以把脚手架代码下载到本地,通过 <code>maven install</code> 在本地构建出脚手架再配置到上图中的地址后使用。</p><p>我的理想情况是大家都能很顺利的构建、配置,美滋滋的使用,但实际情况却是,编码的路上,错误是层出不穷!</p><p>所以,我动手了。我要为你们提供更Easy的方式!但也差点难住我。接下来我就给伙伴们分享下,这个东西是如何搞的。</p><blockquote>8个实战项目,项目:<a href="https://link.segmentfault.com/?enc=Sg%2F%2FuozPR%2FH7mv6FDwWS9g%3D%3D.I3YDvPYLZDi305UFez3scTms%2B6SEev4OXTvI49k5%2FKk%3D" rel="nofollow">https://gaga.plus</a></blockquote><h2>一、思考的开始 🤔</h2><p>我发布过自己的 IntelliJ IDEA vo2dto 到插件市场,也推送过 openai sdk、chatglm sdk、db-router 等组件到 Maven 中央仓库。</p><p>那我就心思了,这脚手架也是个 Jar 包,应该也能发布到 Maven 中央仓库呀。要不Maven Central 自己那个脚手架是怎么发上去的?在编程开发这个事上,我一直秉承着,只要我能看见的,就都应该能复刻出来。</p><p>但你知道这里有一点,发布到 Maven 仓库的是 Jar 包,那我配置脚手架的地址哪里来呢(地址里是脚手架的定义)?我应该也没办法把脚手架的定义推送到人家 maven.apache.org 下面去。毕竟那是人家的老巢。如果能推送,那我现在打开的 Maven Central 应该有一堆脚手架,而不只是 Maven Central 所发布自己的。</p><p>所以,我灵机一动😁,打开了 <a href="https://link.segmentfault.com/?enc=ukjK%2FiFHXEIRh9OJFoOJag%3D%3D.TBSBa%2B6mQfeZL%2B91EbZbKWnx8i7BIHqJ2Qq0oGAM9%2BzLt372Kw3eHzKnBzgcBt0p" rel="nofollow">https://repo.maven.apache.org/maven2</a></p><p> <img src="/img/remote/1460000044703306" alt="img" title="img"></p><p>这个 <code>archetype-catalog.xml</code> 就是在构建 Maven 项目的脚手架时所产生的脚手架定义文件,配置到 IntelliJ IDEA 中,才会展示出脚手架列表,并可以选择使用。</p><p>那我知道了,虽然我不能推送到 maven 的老巢里去。但我可以自己提供个地址呀,把 <code>archetype-catalog.xml</code> 推送进去。经过最开始的验证,确实可以,完全没问题!Easy!</p><p>但接下来的问题变得麻烦了,Jar 怎么推送到 Maven 仓库呢。<strong>傅哥,你不是推过吗,你咋不行了?</strong> 死鬼!tnnd,推送 Jar 到 Maven 仓库从24年2月改版了!!!全编程界的鸡鸭鹅狗🦆,都没有人发过教程!</p><p> <img src="/img/remote/1460000044703307" alt="img" title="img"></p><p>因为这个推送 Jar 在最开始的两天时间里,我一度怀疑是自己超限额了,不能创建了。也曾想过要不就只推送到 阿里云的私有仓库吧,大家配置个 阿里云 Maven 镜像地址,也能用。但这样的情况始终觉得不爽🙅🙅🏻,所以我走上了研究新版推送 Jar 的方式,直至最终成功啦💐!!!舒服!</p><blockquote>接下来我就给小伙伴们分享下,这东西是怎么推送上去的。</blockquote><h2>二、觉得我也行 🤨</h2><h3>1. 卡卡两脚</h3><p>做一个新的技术东西之前呢,先要检索下资料,看看有啥坑不。这个阶段也叫技术调研。卡卡一定能搜 <code>发布Jar到maven仓库</code>,全是以前的旧版本方案,没有一个能用的!</p><p> <img src="/img/remote/1460000044703308" alt="img" title="img"></p><p>还有一个我写的,卡卡上去给两脚!不行了,确实没啥关于新版上传 Jar 到 Maven 仓库的资料,自己去趟坑吧。</p><h3>2. 冒烟测试</h3><p>完成目标最快的方式是什么?兄弟们!当然是结果驱动,先干一脚,看它嚎不嚎。遇到问题再解决问题。所以我准备先无脑上传一波,看看都给我什么信息。</p><p> <img src="/img/remote/1460000044703309" alt="img" title="img"></p><ul><li>地址:<a href="https://link.segmentfault.com/?enc=%2FdmawVavJNNCc7qrZTP%2BYA%3D%3D.IsJRfoPA%2BmmvPo6fb1hOnYN3uINdJxIMRoQNG7%2FPqB54oh149C9Ac7esbrb1Cirq" rel="nofollow">https://central.sonatype.com/publishing</a></li><li>看着这个上传组件的小图,还挺简单的。不过在这里它是一句有用的话都不写。那我就写个名字和上传个Jar进去。<em>心里笑嘻嘻,难度,它会根据我的 Jar 自动分析 POM?</em></li></ul><p>但发现我想多了,第一次上传全是报错!</p><p> <img src="/img/remote/1460000044703310" alt="img" title="img"></p><ul><li>什么 pom 文件没传。所以我又机智的把 Jar 和 POM 打包了 zip 上传。</li><li>紧接着报错 <code>Invalid 'md5' checksum for file: scaffold-lite-1.0-sources.jar</code>,缺少 md5、sha1 验证。好在我以前发布过 maven 仓库,知道这些配置。</li><li>基本上知道要怎么传了,接下来,细看文档。遇到什么类错误,优先看什么内容。</li></ul><h3>3. 操作步骤</h3><p>在 <a href="https://link.segmentfault.com/?enc=DFrAk8nXpcmk%2FzTMKHThNw%3D%3D.MU1cWquKHqhypehMMgF%2FL225bedGSwZFLc7nmmlIvelDsZC0QRHUHFFv6mc7L%2F0A" rel="nofollow">https://central.sonatype.com/publishing</a> 首页有一个 Help 帮助文档,<a href="https://link.segmentfault.com/?enc=PLpjJBcZe56GfQpoGy9ssA%3D%3D.HkB2MdgdtdU9xcHeonum4mL9RrhiEjVjFsCjaWD0S2dy4knD6kib6ZRaBRjw%2FwU6HvecVujuHmNHTdD5%2FO9UOQ%3D%3D" rel="nofollow">https://central.sonatype.org/register/central-portal/#producers</a> 这里有非常详细的操作说明。接下来我讲一些核心的步骤,如果操作有失败,可以参考官网资料。</p><p> <img src="/img/remote/1460000044703311" alt="img" title="img"></p><p>开始前,登录注册 <a href="https://link.segmentfault.com/?enc=rCbix2orC087je3J3dq8AQ%3D%3D.Pq6036yvQ%2F0BloGCy9LmCL1CVMJRkq4vhkkaHDHlPNI%3D" rel="nofollow">https://central.sonatype.com</a> - 可以选择 github 登录。</p><h4>3.1 配置 NameSpace</h4><p>如果选择 github 登录,你会有一个默认配置的 NameSpace(io.github.fuzhengwei),这个东西的作用就是要和本地工程名 groupId 保持一致的。如工程是 cn.bugstack、plus.gaga、com.liergou,那么你在的 NameSpace 就需要配置一个这样的调过来的域名。</p><p> <img src="/img/remote/1460000044703312" alt="img" title="img"></p><p> <img src="/img/remote/1460000044703313" alt="img" title="img"></p><ul><li>如图配置完添加验证即可,最后验证成功就可以使用了。</li></ul><h4>3.2 上传要求</h4><p>文档:<a href="https://link.segmentfault.com/?enc=0W89kRIsSbNFEi5FHCZZnQ%3D%3D.naX3dV51IQP0O%2Fjsr%2F3HOmcuBchc%2F3VJkwCgDOqHY%2F5%2BA2uamuqx%2B%2BxD2m%2BC4ZFVlNgauHF40AU7l3UaHeraZw%3D%3D" rel="nofollow">https://central.sonatype.org/publish/publish-portal-upload/</a></p><p> <img src="/img/remote/1460000044703314" alt="img" title="img"></p><ul><li>如文档上传要求,你需要把jar、pom、doc、sources 全部打包到 zip 包,同时每个文件的 asc、md5、sha1 也需要打包进来。</li><li>这些文件也都是在旧版上传 maven 中央仓库的时候,所需提供的内容。</li></ul><h4>3.3 项目配置</h4><p><strong>源码</strong>:<a href="https://link.segmentfault.com/?enc=963mp02VC9fZS56skKd40A%3D%3D.ZICwje2DfAuLVTBz%2FYlSHEMwi4WRebKIKIyZtdNY0mZ4jtU5fCT8hx8uxv7phDlRTlq7FaA1xlnCmmxdHSrgmG0JcT9UzH6pjnEbsQitcqHTf9UVZzyar27DYBSQHjUUnrk3dnR4Dw9WwQmyecnToA%3D%3D" rel="nofollow">https://gitcode.net/KnowledgePlanet/road-map/xfg-frame-archet...</a></p><pre><code><?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.fuzhengwei</groupId>
<artifactId>ddd-scaffold-lite</artifactId>
<version>1.0</version>
<packaging>maven-archetype</packaging>
<name>ddd-scaffold-lite</name>
<properties>
<java.version>1.8</java.version>
<maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version>
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
<maven-gpg-plugin.version>1.6</maven-gpg-plugin.version>
<maven-checksum-plugin.version>1.10</maven-checksum-plugin.version>
</properties>
<build>
<extensions>
<extension>
<groupId>org.apache.maven.archetype</groupId>
<artifactId>archetype-packaging</artifactId>
<version>3.2.0</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>net.nicoulaj.maven.plugins</groupId>
<artifactId>checksum-maven-plugin</artifactId>
<version>${maven-checksum-plugin.version}</version>
<executions>
<execution>
<id>create-checksums</id>
<goals>
<goal>artifacts</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<encoding>UTF-8</encoding>
<aggregate>true</aggregate>
<charset>UTF-8</charset>
<docencoding>UTF-8</docencoding>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
<javadocExecutable>
/Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/bin/javadoc
</javadocExecutable>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>2.5.3</version>
<configuration>
<autoVersionSubmodules>true</autoVersionSubmodules>
<useReleaseProfile>false</useReleaseProfile>
<releaseProfiles>release</releaseProfiles>
<goals>deploy</goals>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.3.1</version> <!-- 使用最新版本 -->
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal> <!-- 绑定到 jar 目标 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<description>ddd scaffold lite by xiaofuge</description>
<url>https://spring.io/projects/spring-boot/xfg-frame-archetype</url>
<developers>
<developer>
<name>fuzhengwei</name>
<email>184172133@qq.com</email>
<organization>fuzhengwei</organization>
<organizationUrl>https://github.com/fuzhengwei</organizationUrl>
</developer>
</developers>
</project></code></pre><ul><li>注意 groupId、artifactId 名字,如果你有发布诉求,需要和你自己的一直。</li><li>maven-javadoc-plugin:生成 doc 文档。这里要注意,因为我们脚手架不是代码文件,没有doc的,所以要在工程中加一个任意类名文件。工程中小傅哥加了个 Api 类。</li><li>maven-source-plugin:生成 source 文件。</li><li>maven-gpg-plugin:是签名的加密文件,需要本地安装过 gpg 包。</li><li>checksum-maven-plugin:生成 md5、sha1 文件,但这里不会对 pom 生成此文件,还需要单独命令处理。</li></ul><h4>3.4 构建项目</h4><p><strong>第1次构建</strong></p><p> <img src="/img/remote/1460000044703315" alt="img" title="img"></p><p><strong>第2次构建</strong></p><p> <img src="/img/remote/1460000044703316" alt="img" title="img"></p><p><strong>执行脚本</strong></p><p> <img src="/img/remote/1460000044703317" alt="img" title="img"></p><h4>3.5 上传 archetype-catalog.xml</h4><p>把 archetype-catalog.xml 文件,上传到域名可访问云服务器的根目录中。</p><p> <img src="/img/remote/1460000044703318" alt="img" title="img"></p><h4>3.6 上传打包文件到 maven 仓库</h4><p> <img src="/img/remote/1460000044703319" alt="img" title="img"></p><ul><li>你需要按照你的工程结构也是 namespace 创建出文件夹结构,并把工程 target 打包的文件全部复制进来。</li><li>最后把 io 这个文件夹,打包一个 zip 包。就可以了。</li></ul><h4>3.7 上传 maven 仓库</h4><p> <img src="/img/remote/1460000044703320" alt="img" title="img"></p><h4>3.8 成功啦!💐</h4><p> <img src="/img/remote/1460000044703321" alt="img" title="img"></p><p>好啦,这就是整个脚手架的操作过程!现在你可以体验使用了。</p><hr><p> <img src="/img/remote/1460000044703322" alt="img" title="img"></p>
利用React Native JSI提升速度和性能
https://segmentfault.com/a/1190000044632895
2024-03-13T09:05:03+08:00
2024-03-13T09:05:03+08:00
王大冶
https://segmentfault.com/u/minnanitkong
1
<blockquote>首发于公众号 <strong>前端混合开发</strong>,欢迎关注。</blockquote><p>作为一个跨平台移动应用开发框架,React Native 需要与平台特定的编程语言(如 Android 的 Java 和 iOS 的 Objective-C)进行通信。这可以通过两种方式之一实现,这取决于你用于构建混合移动应用的架构。</p><p>在 React Native 的原始或传统架构中,这种通信过程是通过所谓的<strong>桥接</strong>来实现的。与此同时,较新的、更具实验性的架构使用 JavaScript 接口(JSI)来直接调用在 Java 或 Objective-C 中实现的方法。</p><p>让我们从高层次来看看每个选项是如何工作的,然后探索使用React Native JSI来提高我们应用的速度和性能。你可以在<a href="https://link.segmentfault.com/?enc=%2BWiHO47CA6rW6WQc3eFHDw%3D%3D.4rYxjtqqgTLA1U7%2FlJgvh%2B9JPsZG4ZHeEwv9RlToZXpgA57%2BEZPFqrZDXltAeqwr" rel="nofollow">这个GitHub仓库</a>中查看本文中使用的代码演示。</p><p>需要注意的是,这是一个相对高级的话题,因此需要一些 React Native 编程经验才能跟上。此外,至少对 Java 和 Objective-C 基础有一定的了解将帮助你从本教程中获得最大收益。</p><h2>React Native的原始架构是如何工作的?</h2><p>在 React Native 的传统架构中,一个应用被划分为三个不同的部分或线程:</p><ul><li>JavaScript线程:我们在这里编写JavaScript代码和业务逻辑,以及创建节点</li><li>阴影树:React Native 使用<a href="https://link.segmentfault.com/?enc=RTXrR0dfV60kVB3pFgkatQ%3D%3D.rgQIic9dTBZWxGLk9bPBgCNKVS6qdym3tzpp5PDuMG6fbKEnrDhZyjn1fjv2HYOH" rel="nofollow"> Yoga 框架</a>进行布局计算的地方。</li><li>平台 UI(Java 或 Objective-C):渲染这个计算出的布局。</li></ul><p>本质上,线程是进程中一系列可执行的指令。线程通常由操作系统的一个部分(称为调度器)处理,它包含有关何时以及如何执行线程的指令。</p><p>为了实现通信和协同工作,这三个线程依赖于一种被称为桥接的机制。这种操作模式始终是异步的,并确保使用 React Native 构建的应用总是使用平台特定视图而不是网页视图进行渲染。</p><p>由于这种架构,JavaScript 和平台 UI 线程不会直接通信。因此,原生方法不能直接在 JavaScript 线程中调用。</p><p>通常,为了开发一个能在 iOS 或 Android 上运行的应用,我们期望使用该平台的特定编程语言。这就是为什么新创建的 React Native 应用会有单独的 <code>ios</code> 和 <code>android</code> 文件夹,它们作为引导我们的应用在各自平台上运行的入口点。</p><p>因此,为了在每个平台上运行我们的 JavaScript代码,我们依赖于一个名为 JavaScriptCore 的框架。这意味着当我们启动一个React Native应用时,我们必须同时启动三个线程,同时允许桥接器处理通信。</p><p><img width="723" height="411" src="/img/bVdbrdi" alt="image.png" title="image.png"></p><h2>React Native 中的桥接是什么?</h2><p><strong>桥接</strong>,用 C++ 编写,是我们如何在 JavaScript 和平台 UI 线程之间发送编码消息的方式,这些消息格式化为 JSON 字符串:</p><ul><li>JavaScript 线程决定哪个节点在屏幕上渲染,并使用桥接以序列化的 JSON 格式传递消息。</li><li>在消息到达主线程之前,它需要到达阴影树线程,该线程计算屏幕上节点的位置。</li><li>然而,主线程是处理发生在 UI 上的动作的,比如在文本字段中输入或按下一个按钮。</li></ul><p>因此,每个线程都需要花费一些时间来解码 JSON 字符串。有趣的是,桥接还为 Java 或 Objective-C 提供了一个接口,用于从原生模块调度 JavaScript 执行。它异步完成所有这些操作。这些时间加起来可不少!</p><p>尽管如此,多年来桥接通常运行良好,为许多应用程序提供动力。然而,它也遇到了一些其他问题。例如:</p><ul><li>桥接与 React Native 的生命周期紧密相关,通常会在 React Native 初始化或关闭时一起初始化或关闭,这意味着启动时间更慢。</li><li>如果在用户与用户界面交互时,线程间的通信过程出现某种阻塞——例如,滚动浏览一长串数据时,他们可能会瞬间看到一个白色空白区域,从而导致用户体验不佳</li><li>应用中将要使用的每个模块都需要在启动时初始化,这可能导致启动时间变慢,以及系统资源使用增加、可扩展性问题等等。</li></ul><p>React Native团队在努力减少由桥接引起的性能瓶颈的过程中,引入了新的架构。让我们来探索如何做到这一点。</p><h2>React Native的新架构是如何工作的?</h2><p>React Native的新架构相比于经典架构,更加便于JavaScript和平台UI线程之间的直接通信。这意味着可以直接在JavaScript线程中调用原生模块。</p><p>新架构中的一些其他差异包括:</p><ul><li>能够与多个通用引擎(如Hermes或V8)一起工作,而不仅仅依赖于JavaScriptCore引擎</li><li>无需在JavaScript和平台UI线程之间序列化或反序列化消息。相反,它使用一种称为JavaScript接口或JSI的机制,我们将在下面详细讨论。</li><li>使用Fabric而不是Yoga来渲染UI组件</li><li>引入了 TurboModules,确保应用中使用的每个模块只在需要时加载,而不是在启动时加载。</li></ul><p>由于这些新实现,使用新架构的 React Native 应用将记录性能提升——包括更快的启动时间。</p><h2>什么是JavaScript接口(JSI)?</h2><p>JavaScript接口(JSI)修复了桥接的两个关键缺陷:</p><ul><li>允许我们直接在JavaScript中调用在Java或Objective-C中创建的原生方法</li><li>使我们能够与本地代码进行同步或异步通信,这提高了启动时间,并在像滚动长列表这样的场景中实现更快的响应</li></ul><p>除了这些巨大的优势之外,我们还可以使用 JSI 来利用设备的连接功能,例如蓝牙和地理定位,通过暴露我们可以直接用 JavaScript 调用的方法。</p><p>调用平台原生模块中的方法的能力并不是全新的——我们在网页开发中使用相同的模式。例如,在JavaScript中,我们可以像这样调用DOM方法:</p><pre><code>const paragraph = document.createElement('p')</code></pre><p>我们甚至可以在创建的DOM上调用方法。例如,这段代码在C++中调用了一个 setHeight 方法,它改变了我们创建的元素的高度:</p><pre><code>paragraph.setAttribute('height', 55)</code></pre><p>如我们所见,用C++编写的JSI为我们的应用程序带来了许多性能上的改进。接下来,让我们探索如何利用TurboModules和Codegen充分发挥其潜力。</p><h2>理解 Codegen 和 TurboModules</h2><p>TurboModules 是新的 React Native 架构中的一种特殊的原生模块。他们的一些优点包括:</p><ul><li>仅在需要时初始化模块,以实现更快的应用启动时间</li><li>使用JSI进行本地代码,这意味着平台UI和JavaScript线程之间的通信更加顺畅</li><li>在原生平台上提供强类型接口</li></ul><p>与此同时,<code>Codegen</code> 就像我们的 TurboModules 的静态类型检查器和生成器。本质上,当我们使用 TypeScript 或 Flow 定义我们的类型时,我们可以使用Codegen为 JSI 生成C++类型。Codegen 还为我们的模块生成更多的本地代码。</p><p>通常,使用 Codegen 和 TurboModules 使我们能够使用JSI构建可以与Java和 <code>Objective-C</code> 等平台特定代码进行通信的模块。这是享受JSI优势的推荐方式。</p><p><img width="723" height="426" src="/img/bVdbrdp" alt="image.png" title="image.png"></p><p>既然我们已经从高层次上介绍了这些信息,现在让我们将其付诸实践。在下一节中,我们将创建一个 TurboModule,它将允许我们在Java或 Objective-C 中访问方法。</p><h2>使用新架构启动一个 React Native 应用</h2><p>为了创建一个启用了新架构的新 React Native 应用,我们首先需要设置我们的文件夹。让我们开始创建一个文件夹——我们将命名为 <strong>JSISample</strong>——在这里我们将添加我们的 React Native 应用、设备名称模块和单位转换器模块。</p><p>对于下一步,我们可以按照实验性的React Native文档中的设置指南进行操作,或者简单地打开一个新的终端并运行以下命令:</p><pre><code>// Terminal
npx react-native@latest init Demo</code></pre><p>上述命令将创建一个新的React Native应用,其文件夹名称为 <code>Demo</code>。</p><p>一旦成功安装,我们就可以启用新的架构。要在Android上启用它,只需打开 <code>Demo/android/gradle.properties</code> 文件并设置 <code>newArchEnabled=true</code> 。要在iOS上启用它,打开终端到 <code>Demo/ios</code> ,然后运行此命令:</p><pre><code>bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install</code></pre><p>现在应该启用新的React Native架构。为了确认这一点,你可以通过运行 <code>npm start</code> 或 <code>yarn start</code> 来启动你的iOS或Android应用。在终端中,你应该会看到以下内容:</p><pre><code>LOG Running "Demo" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1}</code></pre><p>在我们的应用程序设置完成后,让我们继续创建两个 <code>TurboModules</code>:一个用于获取设备的名称,另一个用于单位转换。</p><h2>创建一个设备名称 TurboModule</h2><p>为了探索JSI的重要性,我们将创建一个全新的TurboModule,我们可以将其安装到启用了新架构的React Native应用程序中。</p><h2>设置文件夹结构</h2><p>在 <code>JSISample</code> 文件夹中,我们需要创建一个新的文件夹,前缀为 RTN ,例如 <code>RTNDeviceName</code> 。在这个文件夹内,我们将创建三个额外的文件夹: <code>ios</code> , <code>android</code> 和 <code>js</code> 。我们还将在文件夹旁边添加两个文件: <code>package.json</code> 和 <code>rtn-device-name.podspec</code> 。</p><p>目前,我们的文件夹结构应该是这样的:</p><pre><code>// Folder structure
RTNDeviceName
┣ android
┣ ios
┣ js
┣ package.json
┗ rtn-device-name.podspec</code></pre><h2>设置 package.json 文件</h2><p>作为一个React Native开发者,你肯定之前已经处理过 <code>package.json</code> 文件。在新的React Native架构的背景下,这个文件既管理我们模块的JavaScript代码,也与我们稍后设置的平台特定代码进行接口对接。</p><p>在 package.json 文件中,粘贴这段代码:</p><pre><code>// RTNDeviceName/package.json
{
"name": "rtn-device-name",
"version": "0.0.1",
"description": "Convert units",
"react-native": "js/index",
"source": "js/index",
"files": [
"js",
"android",
"ios",
"rtn-device-name.podspec",
"!android/build",
"!ios/build",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__"
],
"keywords": [
"react-native",
"ios",
"android"
],
"repository": "https://github.com/bonarhyme/rtn-device-name",
"author": "Onuorah Bonaventure Chukwudi (https://github.com/bonarhyme)",
"license": "MIT",
"bugs": {
"url": "https://github.com/bonarhyme/rtn-device-name/issues"
},
"homepage": "https://github.com/bonarhyme/rtn-device-name#readme",
"devDependencies": {},
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"codegenConfig": {
"name": "RTNDeviceNameSpec",
"type": "modules",
"jsSrcsDir": "js",
"android": {
"javaPackageName": "com.rtndevicename"
}
}
}</code></pre><p>我在上述文件中提供了我的详细信息。你的文件应该看起来稍有不同,因为你应该使用你自己的个人信息,仓库和模块的详细信息。</p><h2>设置 podspec 文件</h2><p>我们接下来要处理的文件是 <code>podspec</code> 文件,这是专门为我们的演示应用程序的iOS实现准备的。本质上, podspec 文件定义了我们正在设置的模块如何与iOS构建系统以及 CocoaPods(iOS应用程序的依赖管理器)进行交互。</p><p>你应该会看到很多与我们在上一节中设置的 <code>package.json</code> 文件相似之处,因为我们使用该文件中的值来填充这个文件中的许多字段。链接这两个文件确保了我们的 JavaScript 和原生 iOS 代码之间的一致性。</p><p><code>podspec</code> 文件的内容应该看起来类似于这样:</p><pre><code>// RTNDeviceName/rtn-device-name.podspec
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "rtn-device-name"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "11.0" }
s.author = package["author"]
s.source = { :git => package["repository"], :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
install_modules_dependencies(s)
end</code></pre><h2>为Codegen定义TypeScript接口</h2><p>接下来在我们的待办事项列表上是定义Codegen的TypeScript接口。在为这一步骤设置文件时,我们必须始终使用以下命名规则:</p><ul><li>以 Native 开始文件名</li><li>在 Native 后面跟上我们的模块名称,使用PascalCase命名法</li></ul><p>遵循这种命名规则对于使React Native JSI正确工作至关重要。在我们的情况下,我们将创建一个名为 <code>NativeDeviceName.ts</code> 的新文件,并在其中编写以下代码:</p><pre><code>// RTNDeviceName/js/NativeDeviceName.ts
import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
getDeviceName(): Promise<string>;
}
export default TurboModuleRegistry.get<Spec>('RTNDeviceName') as Spec | null;</code></pre><p>这个TypeScript文件包含了我们将在此模块中实现的方法的接口。我们首先导入必要的React Native依赖项。然后,我们定义了一个 Spec 接口,它扩展了一个 <code>TurboModule</code> 。</p><p>在我们的 <code>Spec</code> 接口中,我们定义了我们想要创建的方法——在这种情况下,是 <code>getDeviceName()</code> 。最后,我们从 <code>TurboModuleRegistry</code> 调用并导出模块,指定我们的 Spec 接口和我们的模块名称。</p><h2>生成原生iOS代码</h2><p>在这个步骤中,我们将使用Codegen生成用 Objective-C 编写的原生iOS代码。我们只需确保我们的终端在 <code>RTNDeviceName</code> 文件夹中打开,并粘贴以下代码:</p><pre><code>// Terminal
node Demo/node_modules/react-native/scripts/generate-codegen-artifacts.js \
--path Demo/ \
--outputPath RTNDeviceName/generated/</code></pre><p>这个命令在我们的 <code>RTNDeviceName</code> 模块文件夹内生成一个名为 <code>generated</code> 的文件夹。 <code>generated</code> 文件夹包含我们模块的原生iOS代码。文件夹结构应该看起来类似于这样:</p><pre><code>// RTNDeviceName/generated/
generated
┗ build
┃ ┗ generated
┃ ┃ ┗ ios
┃ ┃ ┃ ┣ FBReactNativeSpec
┃ ┃ ┃ ┃ ┣ FBReactNativeSpec-generated.mm
┃ ┃ ┃ ┃ ┗ FBReactNativeSpec.h
┃ ┃ ┃ ┣ RTNConverterSpec
┃ ┃ ┃ ┃ ┣ RTNConverterSpec-generated.mm
┃ ┃ ┃ ┃ ┗ RTNConverterSpec.h
┃ ┃ ┃ ┣ FBReactNativeSpecJSI-generated.cpp
┃ ┃ ┃ ┣ FBReactNativeSpecJSI.h
┃ ┃ ┃ ┣ RTNConverterSpecJSI-generated.cpp
┃ ┃ ┃ ┗ RTNConverterSpecJSI.h</code></pre><h2>实现iOS的模块方法</h2><p>在这个步骤中,我们需要用Objective-C编写一些iOS原生代码。首先,我们需要在 <code>RTNDeviceName/ios</code> 文件夹中创建两个文件: <code>RTNDeviceName.h</code> 和<code> RTNDeviceName.mm</code> 。</p><p>第一个文件是一个头文件,这种文件用于存储可以导入到Objective C文件中的函数。因此,我们需要将这段代码添加到其中:</p><pre><code>// RTNDevicename/ios/RTNDeviceName.h
#import <RTNDeviceNameSpec/RTNDeviceNameSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RTNDeviceName : NSObject <NativeDeviceNameSpec>
@end
NS_ASSUME_NONNULL_END</code></pre><p>第二个文件是一个实现文件,它包含我们模块的实际原生代码。添加以下代码:</p><pre><code>// RTNDevicename/ios/RTNDeviceName.mm
#import "RTNDeviceNameSpec.h"
#import "RTNDeviceName.h"
#import <UIKit/UIKit.h>
@implementation RTNDeviceName
RCT_EXPORT_MODULE()
- (void)getDeviceName:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSString *deviceName = [UIDevice currentDevice].name;
resolve(deviceName);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeDeviceNameSpecJSI>(params);
}
@end
</code></pre><p>在这个文件中,我们导入了必要的头文件,包括我们刚刚创建的 <code>RTNDeviceName.h</code> 和UIKit,我们将使用它来提取设备的名称。然后我们编写实际的Objective-C函数来提取并返回设备名称,然后在其下方编写必要的样板代码。</p><p>请注意,这段样板代码是由React Native团队创建的。它帮助我们实现JSI,因为编写纯JSI意味着编写纯粹的、未经掺杂的原生代码。</p><h2>生成安卓代码</h2><p>生成原生 Android 代码的第一步是在 <code>RTNDeviceName/android</code> 文件夹内创建一个 <code>build.gradle</code> 文件。然后,将以下代码添加到其中:</p><pre><code>// RTNDeviceName/android/build.gradle
buildscript {
ext.safeExtGet = {prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
repositories {
google()
gradlePluginPortal()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
}
}
apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
android {
compileSdkVersion safeExtGet('compileSdkVersion', 33)
namespace "com.rtndevicename"
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation 'com.facebook.react:react-native'
}</code></pre><p>接下来,我们将创建我们的 <code>ReactPackage</code> 类。在这个深层嵌套的文件夹中创建一个新的 <code>DeviceNamePackage.java</code> 文件。</p><pre><code>RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNamePackage.java</code></pre><p>在我们刚刚创建的 <code>DeviceNamePackage.java</code> 文件中,添加以下代码,它在Java中将相关类进行分组:</p><pre><code>// RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNamePackage.java
package com.rtndevicename;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
import java.util.Collections;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
public class DeviceNamePackage extends TurboReactPackage {
@Nullable
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(DeviceNameModule.NAME)) {
return new DeviceNameModule(reactContext);
} else {
return null;
}
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return () -> {
final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
moduleInfos.put(
DeviceNameModule.NAME,
new ReactModuleInfo(
DeviceNameModule.NAME,
DeviceNameModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
));
return moduleInfos;
};
}
}
</code></pre><p>我们还将创建一个名为 <code>DeviceNameModule.java</code> 的文件,该文件将包含我们在Android上的设备名称模块的实际实现。它使用 <code>getDeviceName</code> 方法获取设备名称。文件的其余部分包含了代码与JSI正确交互所需的样板代码。</p><p>这是该文件的完整代码:</p><pre><code>// RTNDeviceName/android/src/main/java/com/rtndevicename/DeviceNameModule.java
package com.rtndevicename;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;
import com.rtndevicename.NativeDeviceNameSpec;
import android.os.Build;
public class DeviceNameModule extends NativeDeviceNameSpec {
public static String NAME = "RTNDeviceName";
DeviceNameModule(ReactApplicationContext context) {
super(context);
}
@Override
@NonNull
public String getName() {
return NAME;
}
@Override
public void getDeviceName(Promise promise) {
promise.resolve(Build.MODEL);
}
}
</code></pre><p>遵循这些步骤后,我们可以通过打开终端并运行此命令来将我们的模块重新安装到我们的 Demo 应用中:</p><pre><code>cd Demo
yarn add ../RTNDeviceName</code></pre><p>此外,我们需要调用 Codegen 来为我们的 Demo 应用生成我们的 Android 代码,如下所示:</p><pre><code>cd android
./gradlew generateCodegenArtifactsFromSchema</code></pre><p>这完成了创建TurboModule的过程。我们的文件结构现在应该是这样的:</p><pre><code>// Turbo module supposed structure
RTNDeviceName
┣ android
┃ ┣ src
┃ ┃ ┗ main
┃ ┃ ┃ ┗ java
┃ ┃ ┃ ┃ ┗ com
┃ ┃ ┃ ┃ ┃ ┗ rtndevicename
┃ ┃ ┃ ┃ ┃ ┃ ┣ DeviceNameModule.java
┃ ┃ ┃ ┃ ┃ ┃ ┗ DeviceNamePackage.java
┃ ┗ build.gradle
┣ generated
┃ ┗ build
┃ ┃ ┗ generated
┃ ┃ ┃ ┗ ios
┃ ┃ ┃ ┃ ┣ FBReactNativeSpec
┃ ┃ ┃ ┃ ┃ ┣ FBReactNativeSpec-generated.mm
┃ ┃ ┃ ┃ ┃ ┗ FBReactNativeSpec.h
┃ ┃ ┃ ┃ ┣ RTNConverterSpec
┃ ┃ ┃ ┃ ┃ ┣ RTNConverterSpec-generated.mm
┃ ┃ ┃ ┃ ┃ ┗ RTNConverterSpec.h
┃ ┃ ┃ ┃ ┣ FBReactNativeSpecJSI-generated.cpp
┃ ┃ ┃ ┃ ┣ FBReactNativeSpecJSI.h
┃ ┃ ┃ ┃ ┣ RTNConverterSpecJSI-generated.cpp
┃ ┃ ┃ ┃ ┗ RTNConverterSpecJSI.h
┣ ios
┃ ┣ RTNDeviceName.h
┃ ┗ RTNDeviceName.mm
┣ js
┃ ┗ NativeDeviceName.ts
┣ package.json
┗ rtn-device-name.podspec</code></pre><p>然而,要使用我们刚刚创建的 <code>RTNDeviceName</code> 模块,我们必须设置我们的React Native代码。接下来让我们看看如何做。</p><h2>为我们的React Native代码创建 <code>DeviceName.tsx</code> 文件</h2><p>打开 <code>src</code> 文件夹并创建一个 <code>component</code> 文件夹。在 <code>component</code> 文件夹内,创建一个 <code>DeviceName.tsx</code> 文件并添加以下代码:</p><pre><code>// Demo/src/components/DeviceName.tsx
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import React, {useCallback, useState} from 'react';
import RTNDeviceName from 'rtn-device-name/js/NativeDeviceName';</code></pre><p>我们首先从React Native中导入了几个组件和 <code>StyleSheet</code> 模块。我们还导入了React库和两个Hooks。最后一个 import 语句是为了我们之前设置的 <code>RTNDeviceName</code> 模块,以便我们在应用中访问原生设备名称获取功能。</p><p>现在,让我们分解我们想要添加到文件中的其余代码。首先,我们定义一个 DeviceName 功能组件。在我们的组件内部,我们创建一个状态来保存我们最终获取的设备名称:</p><pre><code>export const DeviceName = () => {
const [deviceName, setDeviceName] = useState<string | undefined>('');
// next code below here
}</code></pre><p>接下来,我们添加一个异步回调。在其中,我们等待库的响应,并最终将其设置到状态中:</p><pre><code> const getDeviceName = useCallback(async () => {
const theDeviceName = await RTNDeviceName?.getDeviceName();
setDeviceName(theDeviceName);
}, []);
// Next piece of code below here</code></pre><p>在 <code>return</code> 声明中,我们使用了一个 <code>TouchableOpacity</code> 组件,该组件在按下时调用 <code>getDeviceName</code> 回调。我们还有一个 <code>Text</code> 组件来渲染 <code>deviceName</code> 状态:</p><pre><code> return (
<View style={styles.container}>
<TouchableOpacity onPress={getDeviceName} style={styles.button}>
<Text style={styles.buttonText}>Get Device Name</Text>
</TouchableOpacity>
<Text style={styles.deviceName}>{deviceName}</Text>
</View>
);</code></pre><p>在文件的末尾,我们使用之前导入的 StyleSheet 模块来定义我们组件的样式:</p><pre><code>const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 10,
justifyContent: 'center',
alignItems: 'center',
},
button: {
justifyContent: 'center',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 10,
backgroundColor: '#007bff',
},
buttonText: {
fontSize: 20,
color: 'white',
},
deviceName: {
fontSize: 20,
marginTop: 10,
},
});</code></pre><p>在此刻,我们可以调用我们的组件并在我们的 <code>src/App.tsx</code> 文件中像这样渲染它:</p><pre><code>// Demo/App.tsx
import React from 'react';
import {SafeAreaView, StatusBar, useColorScheme} from 'react-native';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import {DeviceName} from './components/DeviceName';
function App(): JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
flex: 1,
};
return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<DeviceName />
</SafeAreaView>
);
}
export default App;</code></pre><p>最后,我们可以使用 yarn start 或 npm start 运行我们的应用程序,并按照提示在iOS或Android上启动它。我们的应用程序应该看起来类似于下面的样子,具体取决于你在哪种设备上启动它:</p><p><img width="723" height="763" src="/img/bVdbrjZ" alt="image.png" title="image.png"></p><p>就是这样!我们已经成功创建了一个TurboModule,它允许我们在React Native应用中获取用户的设备名称。</p><h2>创建一个单位转换器TurboModule</h2><p>我们已经看到了如何直接从JavaScript应用程序访问设备的信息有多么简单。现在,我们将构建另一个模块,允许我们转换测量单位。我们将遵循类似的设置步骤,所以我们不会过多地详述代码。</p><h2>设置我们的文件夹和文件</h2><p>与之前一样,我们将首先创建一个新的TurboModule文件夹,与 Demo 和 <code>RTNDeviceName</code> 文件夹并列。这次,我们将这个文件夹命名为 <code>RTNConverter</code> 。</p><p>然后,创建 <code>js</code> 、 <code>ios</code> 和 <code>android</code> 文件夹,以及 <code>package.json</code> 和 <code>rtn-converter.podspec</code> 文件。我们的文件夹结构应该看起来类似于 <code>RTNDeviceName</code> 的初始文件夹结构。</p><p>接下来,像这样向 <code>package.json</code> 添加适当的代码:</p><pre><code>// RTNConverter/package.json
{
"name": "rtn-converter",
"version": "0.0.1",
"description": "Convert units",
"react-native": "js/index",
"source": "js/index",
"files": [
"js",
"android",
"ios",
"rtn-converter.podspec",
"!android/build",
"!ios/build",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__"
],
"keywords": [
"react-native",
"ios",
"android"
],
"repository": "https://github.com/bonarhyme/rtn-converter",
"author": "Onuorah Bonaventure Chukwudi (https://github.com/bonarhyme)",
"license": "MIT",
"bugs": {
"url": "https://github.com/bonarhyme/rtn-converter/issues"
},
"homepage": "https://github.com/bonarhyme/rtn-converter#readme",
"devDependencies": {},
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"codegenConfig": {
"name": "RTNConverterSpec",
"type": "modules",
"jsSrcsDir": "js",
"android": {
"javaPackageName": "com.rtnconverter"
}
}
}</code></pre><p>记得用你自己的详细信息填充字段。然后,添加 <code>podspec</code> 文件的内容:</p><pre><code>// RTNConverter/podspec
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "rtn-converter"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "11.0" }
s.author = package["author"]
s.source = { :git => package["repository"], :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
install_modules_dependencies(s)
end</code></pre><h2>定义我们的 TypeScript 接口和单位转换方法</h2><p>现在,我们将通过在 <code>RTNConverter/js</code> 文件夹中创建一个 <code>NativeConverter.ts</code> 文件来定义我们的单位转换器的TypeScript接口,并添加以下代码:</p><pre><code>// RTNConverter/js/NativeConverter.ts
import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
inchesToCentimeters(inches: number): Promise<number>;
centimetersToInches(centimeters: number): Promise<number>;
inchesToFeet(inches: number): Promise<number>;
feetToInches(feet: number): Promise<number>;
kilometersToMiles(kilometers: number): Promise<number>;
milesToKilometers(miles: number): Promise<number>;
feetToCentimeters(feet: number): Promise<number>;
centimetersToFeet(centimeters: number): Promise<number>;
yardsToMeters(yards: number): Promise<number>;
metersToYards(meters: number): Promise<number>;
milesToYards(miles: number): Promise<number>;
yardsToMiles(yards: number): Promise<number>;
feetToMeters(feet: number): Promise<number>;
metersToFeet(meters: number): Promise<number>;
}
export default TurboModuleRegistry.get<Spec>('RTNConverter') as Spec | null;</code></pre><p>如你所见,它包含了我们将要原生实现的方法。这与我们为 <code>RTNDeviceName</code> 模块所做的非常相似。然而,我们定义了几种方法来转换不同的测量单位,而不是 <code>getDeviceName()</code> 方法。</p><h2>生成平台特定代码</h2><p>我们将使用与之前类似的终端命令来生成iOS代码。打开你的终端,在我们的 <code>JSISample</code> 文件夹的根目录中运行命令,如下所示:</p><pre><code>node Demo/node_modules/react-native/scripts/generate-codegen-artifacts.js \
--path Demo/ \
--outputPath RTNConverter/generated/</code></pre><p>记住,这段代码使用Codegen在我们的 <code>RTNConverter</code> 文件夹内生成一个iOS构建。</p><p>接下来,我们将在 <code>RTNConverter/ios</code> 文件夹内为我们的单位转换模块创建头文件和实现文件—— <code>RTNConverter.h </code>和 <code>RTNConverter.mm</code> 。在头文件中,添加以下代码:</p><pre><code>// RTNConverter/ios/RTNConverter.h
#import <RTNConverterSpec/RTNConverterSpec.h>
NS_ASSUME_NONNULL_BEGIN
@interface RTNConverter : NSObject <NativeConverterSpec>
@end
NS_ASSUME_NONNULL_END</code></pre><p>然后,将实际的<code>Objective-C</code>代码添加到 <code>RTNConverter.mm</code> 文件中:</p><pre><code>// RTNConverter/ios/RTNConverter.mm
#import "RTNConverterSpec.h"
#import "RTNConverter.h"
@implementation RTNConverter
RCT_EXPORT_MODULE()
- (void)inchesToCentimeters:(double)inches resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:inches*2.54];
resolve(result);
}
- (void)centimetersToInches:(double)centimeters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:centimeters/2.54];
resolve(result);
}
- (void)inchesToFeet:(double)inches resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:inches/12];
resolve(result);
}
- (void)feetToInches:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:feet*12];
resolve(result);
}
- (void)kilometersToMiles:(double)kilometers resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:kilometers/1.609];
resolve(result);
}
- (void)milesToKilometers:(double)miles resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:miles*1.609];
resolve(result);
}
- (void)feetToCentimeters:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:feet*30.48];
resolve(result);
}
- (void)centimetersToFeet:(double)centimeters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:centimeters/30.48];
resolve(result);
}
- (void)yardsToMeters:(double)yards resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:yards/1.094];
resolve(result);
}
- (void)metersToYards:(double)meters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:meters*1.094];
resolve(result);
}
- (void)milesToYards:(double)miles resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:miles*1760];
resolve(result);
}
- (void)yardsToMiles:(double)yards resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:yards/1760];
resolve(result);
}
- (void)feetToMeters:(double)feet resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:feet/3.281];
resolve(result);
}
- (void)metersToFeet:(double)meters resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
NSNumber *result = [[NSNumber alloc] initWithDouble:meters*3.281];
resolve(result);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeConverterSpecJSI>(params);
}
@end</code></pre><p>我们的设备名称 TurboModule 的实现文件中包含了一个提取并返回设备名称的函数。这次,它包含了计算并返回转换后测量值的函数。</p><p>现在我们已经有了iOS的代码,是时候设置Android的代码了。与之前类似,我们将开始在 <code>RTNConverter/android/build.gradle</code> 中添加 <code>build.gradle</code> 文件。</p><pre><code>// RTNConverter/android/build.gradle
buildscript {
ext.safeExtGet = {prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
repositories {
google()
gradlePluginPortal()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
}
}
apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
android {
compileSdkVersion safeExtGet('compileSdkVersion', 33)
namespace "com.rtnconverter"
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation 'com.facebook.react:react-native'
}</code></pre><p>接下来,我们将在这个深层嵌套的文件夹中创建一个 <code>ConverterPackage.java</code> 文件,以添加 <code>ReactPackage</code> 类</p><pre><code>RTNConverter/android/src/main/java/com/rtnconverter</code></pre><p>将以下代码添加到此文件中:</p><pre><code>// RTNConverter/android/src/main/java/com/rtnconverter/ConverterPackage.java
package com.rtnconverter;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
import java.util.Collections;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
public class ConverterPackage extends TurboReactPackage {
@Nullable
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(ConverterModule.NAME)) {
return new ConverterModule(reactContext);
} else {
return null;
}
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return () -> {
final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
moduleInfos.put(
ConverterModule.NAME,
new ReactModuleInfo(
ConverterModule.NAME,
ConverterModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
));
return moduleInfos;
};
}
}</code></pre><p>上述代码在Java中将类进行了分组。在我们的情况下,它将 <code>RTNConverterModule.java</code> 文件中的类进行了分组。</p><p>接下来,我们将通过创建一个 <code>ConverterModule.java</code> 文件来为Android添加我们的转换器模块的实际实现,该文件将位于上述文件旁边。然后,将此代码添加到新创建的文件中:</p><pre><code>// RTNConverter/android/src/main/java/com/rtnconverter/ConverterPackage.java
package com.rtnconverter;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.Map;
import java.util.HashMap;
import com.rtnconverter.NativeConverterSpec;
public class ConverterModule extends NativeConverterSpec {
public static String NAME = "RTNConverter";
ConverterModule(ReactApplicationContext context) {
super(context);
}
@Override
@NonNull
public String getName() {
return NAME;
}
@Override
public void inchesToCentimeters(double inches, Promise promise) {
promise.resolve(inches * 2.54);
}
@Override
public void centimetersToInches(double centimeters, Promise promise) {
promise.resolve(centimeters / 2.54);
}
@Override
public void inchesToFeet(double inches, Promise promise) {
promise.resolve(inches / 12);
}
@Override
public void feetToInches(double feet, Promise promise) {
promise.resolve(feet * 12);
}
@Override
public void kilometersToMiles(double kilometers, Promise promise) {
promise.resolve(kilometers / 1.609);
}
@Override
public void milesToKilometers(double miles, Promise promise) {
promise.resolve(miles * 1.609);
}
@Override
public void feetToCentimeters(double feet, Promise promise) {
promise.resolve(feet * 30.48);
}
@Override
public void centimetersToFeet(double centimeters, Promise promise) {
promise.resolve(centimeters / 30.48);
}
@Override
public void yardsToMeters(double yards, Promise promise) {
promise.resolve(yards / 1.094);
}
@Override
public void metersToYards(double meters, Promise promise) {
promise.resolve(meters * 1.094);
}
@Override
public void milesToYards(double miles, Promise promise) {
promise.resolve(miles * 1760);
}
@Override
public void yardsToMiles(double yards, Promise promise) {
promise.resolve(yards / 1760);
}
@Override
public void feetToMeters(double feet, Promise promise) {
promise.resolve(feet / 3.281);
}
@Override
public void metersToFeet(double meters, Promise promise) {
promise.resolve(meters * 3.281);
}
}</code></pre><p>现在我们已经为我们的模块添加了必要的原生Android代码,接下来我们将像这样将它添加到我们的React Native应用中:</p><pre><code>// Terminal
cd Demo
yarn add ../RTNConverter</code></pre><h2>安装、故障排除和使用我们的模块</h2><p>接下来,我们将通过运行以下命令为Android安装我们的模块:</p><pre><code>// Terminal
cd android
./gradlew generateCodegenArtifactsFromSchema</code></pre><p>然后,我们将通过以下操作为iOS安装我们的模块:</p><pre><code>// Terminal
cd ios
RCT_NEW_ARCH_ENABLED=1 bundle exec pod install</code></pre><p>如果你遇到任何错误,你可以清理构建并重新安装我们库的模块,如下所示:</p><pre><code>cd ios
rm -rf build
bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install</code></pre><p>目前,我们的模块已经可以在我们的React Native项目中使用了。让我们打开<code> Demo/src/components </code>文件夹,创建一个 <code>UnitConverter.tsx </code>文件,并将这段代码添加进去:</p><pre><code>// Demo/src/components/UnitConverter.tsx
import {
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import React, {useCallback, useState} from 'react';
import RTNCalculator from 'rtn-converter/js/NativeConverter';
const unitCombinationsConvertions = [
'inchesToCentimeters',
'centimetersToInches',
'inchesToFeet',
'feetToInches',
'kilometersToMiles',
'milesToKilometers',
'feetToCentimeters',
'centimetersToFeet',
'yardsToMeters',
'metersToYards',
'milesToYards',
'yardsToMiles',
'feetToMeters',
'metersToFeet',
] as const;
type UnitCombinationsConvertionsType =
(typeof unitCombinationsConvertions)[number];
export const UnitConverter = () => {
const [value, setValue] = useState<string>('0');
const [result, setResult] = useState<number | undefined>();
const [unitCombination, setUnitCombination] = useState<
UnitCombinationsConvertionsType | undefined
>();
const calculate = useCallback(
async (combination: UnitCombinationsConvertionsType) => {
const convertedValue = await RTNCalculator?.\[combination\](Number(value));
setUnitCombination(combination);
setResult(convertedValue);
},
[value],
);
const camelCaseToWords = useCallback((word: string | undefined) => {
if (!word) {
return null;
}
const splitCamelCase = word.replace(/([A-Z])/g, ' $1');
return splitCamelCase.charAt(0).toUpperCase() + splitCamelCase.slice(1);
}, []);
return (
<View>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View style={styles.container}>
<Text style={styles.header}>JSI Unit Converter</Text>
<View style={styles.computationContainer}>
<View style={styles.calcContainer}>
<TextInput
value={value}
onChangeText={e => setValue(e)}
placeholder="Enter value"
style={styles.textInput}
inputMode="numeric"
/>
<Text style={styles.equalSign}>=</Text>
<Text style={styles.result}>{result}</Text>
</View>
<Text style={styles.unitCombination}>
{camelCaseToWords(unitCombination)}
</Text>
</View>
<View style={styles.combinationContainer}>
{unitCombinationsConvertions.map(combination => (
<TouchableOpacity
key={combination}
onPress={() => calculate(combination)}
style={styles.combinationButton}>
<Text style={styles.combinationButtonText}>
{camelCaseToWords(combination)}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 10,
},
header: {
fontSize: 24,
marginVertical: 20,
textAlign: 'center',
fontWeight: '700',
},
computationContainer: {
gap: 10,
width: '90%',
height: 100,
marginTop: 10,
},
calcContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
height: 50,
},
textInput: {
borderWidth: 1,
borderColor: 'gray',
width: '50%',
backgroundColor: 'lightgray',
fontSize: 20,
padding: 10,
},
equalSign: {
fontSize: 30,
},
result: {
width: '50%',
height: 50,
backgroundColor: 'gray',
fontSize: 20,
padding: 10,
color: 'white',
},
unitCombination: {
fontSize: 16,
},
combinationContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
justifyContent: 'center',
},
combinationButton: {
backgroundColor: 'gray',
width: '45%',
height: 30,
justifyContent: 'center',
paddingHorizontal: 5,
},
combinationButtonText: {
color: 'white',
},
});
</code></pre><p>然后,我们可以在 <code>App.tsx</code> 文件中像这样导入并使用我们的组件:</p><pre><code>// Demo/src/App.tsx
import React from 'react';
import {SafeAreaView, StatusBar, useColorScheme} from 'react-native';
import {Colors} from 'react-native/Libraries/NewAppScreen';
import {UnitConverter} from './components/UnitConverter';
// import {DeviceName} from './components/DeviceName';
function App(): JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
flex: 1,
};
return (
<SafeAreaView style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
{/* <DeviceName /> */}
<UnitConverter />
</SafeAreaView>
);
}
export default App;</code></pre><p>最终的应用应该看起来类似于下面的样子:</p><p><img width="723" height="763" src="/img/bVdbrll" alt="image.png" title="image.png"></p><h2>总结</h2><p>React Native JSI 仍然是一个实验性的功能。然而,从提升我们的 React Native 应用性能和开发体验的角度来看,它看起来非常有前途。它绝对值得一试!</p><p>在上述部分中,我们研究了经典的React Native架构中的桥接以及它与新架构的不同之处。我们还探讨了新架构以及它如何提升我们应用的速度和性能,涵盖了JSI,Fabric和TurboModules等概念。</p><p>为了更好地理解React Native JSI和新的架构,我们构建了一个模块,通过直接访问原生代码来检索和显示用户设备的名称。我们还构建了一个单位转换模块,允许我们调用我们在Java和Objective-C中定义的方法。</p>
Electron 获取不到设备 ID 了!
https://segmentfault.com/a/1190000044708184
2024-03-13T11:37:09+08:00
2024-03-13T11:37:09+08:00
杨成功
https://segmentfault.com/u/ruidoc
0
<p>大家好,我是杨成功。</p><p>在桌面应用开发中,常常需要获取设备唯一 ID 来表示当前客户端的唯一性。一般的设备 ID 需要满足两个条件:</p><ol><li>基于硬件和系统配置生成,确保设备的唯一性。</li><li>只要不重装系统,设备 ID 多次获取都是唯一的。</li></ol><p><code>node-machine-id</code> 是一个常用的 Node.js 模块,它能够在 Electron 中获取机器的唯一标识。</p><p>我们的产品就是使用该模块,用法也很简单:</p><pre><code class="js">import { machineIdSync } from 'node-machine-id';
let id = machineIdSync();</code></pre><p>但是昨天出现了问题,排查结果是多台设备获取的 ID 竟然是一样的,造成了一些设备的数据被篡改,我从 issues 中找到了一些端倪。</p><p><img src="/img/remote/1460000044708186" alt="2024-03-13-10-14-04.png" title="2024-03-13-10-14-04.png"></p><p>也就是在 <code>Window Ghost</code> 系统中会出现问题(啥是 Window Ghost ?)。</p><p>Window 中还经常遇到权限问题,而且这个 ID 总归不可控,所以还是用自定义的方式实现吧。</p><h2>自定义设置设备 ID</h2><p>自定义的设备 ID 首先需要唯一,其次在安装和卸载应用时设备 ID 不变。</p><p>满足这两个要求,最佳的方案就是将自己生成的设备 ID 存储在用户目录下。</p><p>假设当前用户叫张三,他的用户目录:</p><ul><li>Window:<code>C:\Users\张三\</code></li><li>MacOS:<code>/Users/张三/</code></li></ul><p>很多应用程序都把配置写到用户目录下,且该目录一般不会遇到权限问题。</p><p>(1)使用 <code>uuid</code> 生成设备 ID:</p><pre><code class="js">import { v4 as uuidv4 } from 'uuid';
const device_id = uuidv4();</code></pre><p>(2)在主进程中获取到用户目录,非常简单:</p><pre><code class="js">import { app } from 'electron';
const user_path = app.getPath('home'); // 自动获取 Win 或 Mac 的用户目录</code></pre><p>(3)在用户目录下创建 <code>.elappid</code> 文件,存放生成的设备 ID:</p><pre><code class="js">import { join } from 'node:path';
import fs from 'node:fs';
// 获取配置文件地址
let appid_path = join(user_path, '.elappid');
// 判断文件是否存在,不存在就先创建,并写入设备ID
if (!fs.existsSync(appid_path)) {
fs.writeFileSync(appid_path, device_id, 'utf8');
}</code></pre><p>(4)读取设备 ID,并发送给渲染进程:</p><pre><code class="js">let appid = fs.readFileSync(appid_path, 'utf8');
win.webContents.send('susr-config', { appid });</code></pre><p>写一个进程间交互的方法,就能拿到设备 ID 了。</p><h2>什么时候获取设备 ID</h2><p>正常情况下,我们希望用户打开应用的时候,主动获取设备 ID 并发给渲染进程。</p><p>然而经过测试,在创建浏览器窗口的同时立即获取设备 ID 并通知渲染进程,在正式环境中,渲染进程往往接受不到消息。</p><p>这是因为创建窗口时,页面还没有初始化完成,自然接收不到消息。</p><p>保险的方法就是在页面加载完成后再获取设备 ID,方法如下:</p><pre><code class="js">win = new BrowserWindow({...})
// 页面加载完成后触发:
win.webContents.on("did-finish-load", () => {
console.log('在这里获取设备ID吧')
})</code></pre><p>大功告成,你也试试吧!</p><blockquote>本文来自公众号 <a href="https://link.segmentfault.com/?enc=kc5s5ORoPyT69t%2BTfWDCrw%3D%3D.m49donXERcxYpNCMJCQPSXUcfPabgIAB8K5ccBLSNSziwjDg8ZDsdWf9fmnomq3S" rel="nofollow">程序员成功</a>,关注查看更多</blockquote>
3D模型+BI分析,打造全新的交互式3D可视化大屏开发方案
https://segmentfault.com/a/1190000044699641
2024-03-11T10:46:51+08:00
2024-03-11T10:46:51+08:00
葡萄城技术团队
https://segmentfault.com/u/grapecity
1
<p><strong>背景介绍</strong></p><p>在数字经济建设和数字化转型的浪潮中,数据可视化大屏已成为各行各业的必备工具。然而,传统的数据大屏往往以图表和指标为主,无法真实地反映复杂的物理世界和数据关系。为了解决这个问题,3D模型可视化和数字孪生技术应运而生,它们可以将真实世界的物理对象、过程或系统,以及它们之间的关系和相互作用,构建成虚拟的数字模型,并以立体、动态、交互的方式展示在数据大屏上,实现数据的可视化、可感知、可控制。</p><p><strong>应用场景</strong></p><p>3D模型可视化大屏和数字孪生相结合,可以为用户带来前所未有的视觉和交互体验。无论是工业制造、城市规划、交通运输、能源环保,还是教育医疗、文化旅游、军事安全等领域,都可以通过这种技术,实现数据的可视化、可感知、可控制,打造智慧数字应用的新场景。</p><p><strong>功能介绍</strong></p><ol><li>3D模型文件,通过3D Max、Blender等3D建模软件进行模型设计,然后导出为 glb 文件,该文件将用于3D场景搭建。</li><li>3D场景搭建,上传 glb 文件之后,便可以在3D场景设计器中建立模型与数据联系,比如:添加数据标签、条件格式化、自动轮播等应用效果。</li><li>3D可视化大屏设计,在嵌入式BI仪表板设计器中添加设计好的3D场景,将其作为3D可视化大屏的一部分,同时,3D场景也可以和仪表板中的图表、KPI、过滤器等组件实现联动分析。<br><img src="/img/remote/1460000044699644" alt="" title=""></li></ol><p><strong>环境准备</strong></p><p><a href="https://link.segmentfault.com/?enc=WpEFjO2l0Z1H8khQslNaUw%3D%3D.6MyVQkUpBWjCxhTY%2F4pMF%2F41rv%2BcE281ZRviGMsbyGd9Iuakkc%2B7De88wK4xhgb4JMNTTxOZQ0uNSinQvEs7yIiFT9uOHnpCyvLWgAIRgD3ATW6qKq4OMw6xjKRzYwXkD3fE47CkNDGw0hpPjW6LEw%3D%3D" rel="nofollow">嵌入式BI仪表板设计器</a></p><p><strong>使用步骤</strong></p><p><strong>1)准备 glb 模型文件</strong></p><p>根据业务需要,在3D建模软件中完成模型的设计,并导出为 glb 格式的模型文件。比如:小编这边设计了一个智慧车间的示例模型。<br><img src="/img/remote/1460000044699645" alt="" title=""></p><p><strong>2)创建3D场景</strong></p><p>(1)在嵌入式BI仪表板设计器中创建3D场景设计器<br><img src="/img/remote/1460000044699646" alt="" title=""></p><p>(2)在场景中添加模型</p><p>3D场景搭建第一步就是添加场景中所需的3D模型,这里包含了两种模型导入方式。</p><ol><li>将模型作为文件上传到,在【新建】菜单中可以直接上传30M以内的 glb 文件;</li><li>直接引用一个URL地址,大于30M的模型我们推荐先上传到CND中以提升模型打开速度。<br><img src="/img/remote/1460000044699647" alt="" title=""></li></ol><p>模型添加之后就可以看到对应的节点树,你可以在节点树上执行:选择、删除、隐藏等操作。<br><img src="/img/remote/1460000044699648" alt="" title=""></p><p>(3)给场景添加数据层</p><p>模型导入之后最关键的就是建立物体与数据之间的关系,这一步非常简单,我们只需要添加相应的数据图层,并在数据图层上绑定数据集/数据模型,这一步操作和基础图表的数据绑定方式一样。</p><p>数据图层是3D模型中展示数据的主要途径,我们在同一个3D场景中可以添加多个数据图层,以实现在同一个3D场景展示不同数据的需要,不如:你要在智慧车间中展示每台设备的生产数据,还要展示所有摄像头的数据,温度传感器的数据,这三种设备的数据可能来自不同的数据集,这时候你只需要添加三个数据图层并绑定不同的数据集就可以实现。<br><img src="/img/remote/1460000044699649" alt="" title=""></p><p>(4)数据层细节配置</p><p>每个数据图层都提供了丰富的配置属性,而且图层之间的配置相互独立。比如:我要给图层1添加条件格式化,可以从属性面板中找到条件格式化,并添加项目,如下图。条件格式化可以设定条件,以及对于数据标签、物体的展示样式。<br><img src="/img/remote/1460000044699650" alt="" title=""></p><p>设置好之后,可以点击顶部的【预览】按钮查看3D场景的运行效果,看看是否需要调整更多选项以提升效果。<br><img src="/img/remote/1460000044699651" alt="" title=""></p><p><strong>3)在仪表板中使用3D场景</strong></p><p>3D场景设计好之后,我们就可以新建仪表板来设计最终所需的3D可视化大屏了。首先,在完成仪表板的标题、图表等组件的设计工作,大致效果如下:<br><img src="/img/remote/1460000044699652" alt="" title=""></p><p>接下来就是我们的主角登场,从仪表板设计器左侧的工具箱中添加【3D场景】组件:<br><img src="/img/remote/1460000044699653" alt="" title=""></p><p>调整3D场景组件大小,铺满整个仪表板作为背景,那我们就完成了3D模型可视化大屏设计,感觉预览看看效果吧。<br><img src="/img/remote/1460000044699654" alt="" title=""></p><p><strong>总结</strong></p><p>以上就是实现一个3D模型的全过程。在没有任何编码的情况下,小编实现了一个外观炫酷、功能齐全、具备联动分析能力的3D可视化大屏。这个成果将数字化转型过程中的两大重要技术(BI分析+3D模型)巧妙地融合在一起。通过这些综合应用,能够以直观、生动的方式呈现数据,并实现深度交互和分析。无论是数据可视化还是数据分析,这个3D可视化大屏为我们提供了一个强大而灵活的工具,为数字化转型带来了巨大的推动力。</p><p><strong>扩展链接:</strong></p><p><a href="https://link.segmentfault.com/?enc=ZfeKEoKfoJymF806J79GZw%3D%3D.I21IEnW5QPpZXX4SiqM4J0mVuXmj58Q4UR4Mh7Wp%2BznuqVepYFgkavF5ee2DwIIGaAMxU%2F8cpjhV4vmkW1DCEImmVmH8AlandvaU5hwxt9U%3D" rel="nofollow">创意展示:打造数据大屏的炫酷天气预报插件</a></p><p><a href="https://link.segmentfault.com/?enc=WD61ld1iDvMz%2FVLGxWO8tg%3D%3D.2wFTc4dRStG7CYzN9btsjxnHAqVeR6s74CYTk7oq%2BvUGo4oMQjKsd8uDTvudhEYMOSjjCousLOkg0VdHmDt6Qug0PZTB%2B%2BvSbOkbYWo7KDs%3D" rel="nofollow"> 聊一聊数字孪生与3D可视化</a></p><p><a href="https://link.segmentfault.com/?enc=GFbdKU6lAohywy94s2AQVg%3D%3D.jlYggcEqrb7KZsaJyoTBYOPs%2BQg7yWfJwzjWYcSilo8T3P9gjZHSCyoA8yXEoVgFJr965knAUEj3spd%2F5r7EHA%3D%3D" rel="nofollow">探秘移动端BI:发展历程与应用前景解析</a></p>
JavaScript 最新动态:2024 年新功能
https://segmentfault.com/a/1190000044689219
2024-03-07T10:21:41+08:00
2024-03-07T10:21:41+08:00
南玖
https://segmentfault.com/u/fenanjiu
11
<h2>前言</h2><p>随着 Web 技术的日新月异,JavaScript 也在不断地吸收新的特性和技术,以满足日益复杂和多样化的开发需求。在 2024 年,JavaScript 迎来了一系列令人瞩目的新功能,这些功能不仅提升了开发者的效率,也极大地丰富了 Web 应用的表现力和交互性。</p><p>在接下来的内容中,我们将逐一介绍这些新功能,并探讨它们如何在实际开发中发挥作用,以及它们如何继续引领前端开发的未来。</p><h2>Object.groupBy</h2><blockquote>它是一个新的 JavaScript 方法,它可以根据提供的回调函数返回的字符串值对给定可迭代对象中的元素进行分组。返回的对象具有每个组的单独属性,其中包含组中的元素的数组。</blockquote><p>当我们想要根据数组中对象的一个或多个属性的名称对数组元素进行分类时,此方法非常有用。</p><h3>语法</h3><pre><code class="js">Object.groupBy(items, callbackFn)</code></pre><p><strong>参数</strong></p><ul><li><code>items</code>:一个将进行元素分组的可迭代对象</li><li><p><code>callbackFn</code>:对可迭代对象中的每个元素执行的函数。它应该返回一个值,可以被强制转换成属性键(字符串或 <a href="https://link.segmentfault.com/?enc=5Ez%2BTnzeULGQ2XairtTukw%3D%3D.SUgmh5tCdDryRQ%2BfEr71WHxx5NZrKcWc5bXlclzc3RKp2r6IFLSF1tzlYyDfJY4KvLVGgYxtZvw7ycpquT7OJ4Cif46Ux8mJePRwvrtm03%2FoaXl5kKbElQwl%2BTRw%2Bvxc" rel="nofollow">symbol</a>),用于指示当前元素所属的分组。该函数被调用时将传入以下参数:</p><ul><li>element:数组中当前正在处理的元素</li><li>index:正在处理的元素在数组中的索引</li></ul></li></ul><p><strong>返回值</strong></p><p>一个带有所有分组属性的 <a href="https://link.segmentfault.com/?enc=L6HL75IVkMLNSNFZ7q2epw%3D%3D.erJpCLVB1jzVG%2BB7M4ocQ4K%2Bzde5cHp2mgz8OIwidu7livls4amCXCfMEzpVvgjSx5rkGTKoXgZIrmcHQ3PSpDe25yu3%2F4sVEcNy5sJe%2BnG3q1wM%2BrStEFYNS%2BNzXlcoLAb8hm%2BRzRNfoMAr0mjLig%3D%3D" rel="nofollow"><code>null</code> 原型对象</a>,每个属性都分配了一个包含相关组元素的数组。</p><h3>对数组中的元素进行分组</h3><p>我们可能经常需要对数据库中的项目进行分组并通过 UI 将它们显示给用户。使用 <code>Object.groupBy()</code>就可以简化此类项目的分组。</p><p>比如有这样一堆数据:</p><pre><code class="js">const arr = [
{ product: "iPhone X", quantity: 25, color: "black" },
{ product: "Huawei mate50", quantity: 6, color: "white" },
{ product: "xiaomi 13", quantity: 0, color: "black" },
{ product: "iPhone 13", quantity: 10, color: "white" },
{ product: "Huawei P50", quantity: 5, color: "black" },
]</code></pre><p>然后我们希望将这些设备根据颜色进行分类</p><pre><code class="js">const newArr = Object.groupBy(arr, (item) => item.color)
console.log('【newArr】', newArr)</code></pre><p><img src="/img/remote/1460000044689221" alt="goupBy-1.png" title="goupBy-1.png"></p><p>上面的代码按产品的属性值<code>color</code>对产品进行分组,每次调用回调函数时,都会返回与每个对象的属性(“黑色”或“白色”)相对应的键。然后使用返回的键对数组的元素进行分组。</p><h3>有条件地对数组中的元素进行分组</h3><p>还是上面的数据,如果我们想要分成iphone和国产品牌两类,可以这么来实现:</p><pre><code class="js">const arr = [
{ product: "iPhone X", quantity: 25, color: "black" },
{ product: "Huawei mate50", quantity: 6, color: "white" },
{ product: "xiaomi 13", quantity: 0, color: "black" },
{ product: "iPhone 13", quantity: 10, color: "white" },
{ product: "Huawei P50", quantity: 5, color: "black" },
]
const list = Object.groupBy(arr, (item) => {
return item.product.includes('iPhone') ? 'iPhone' : '国产品牌'
})
console.log('【list】', list)</code></pre><p><img src="/img/remote/1460000044689222" alt="groupBy-2.png" title="groupBy-2.png"></p><h3>扩展</h3><p>注意: <code>Object.groupBy()</code>最初是作为典型的数组方法实现的。它最初的用途是这样的:</p><pre><code class="js">let myArray = [a, b, c]
myArray.groupBy(callbackFunction)</code></pre><p>然而,由于<a href="https://link.segmentfault.com/?enc=oYmSXH%2Bb9Dq8lcRLQTKdww%3D%3D.FpH6MOyD79%2FvQUslify4kqxfE0smil%2BHGOsZHXV5feUdzw5A%2FWJnMhBNOM0j3Nv1" rel="nofollow">ECMAScript</a>技术委员会在实现该方法 时遇到了<a href="https://link.segmentfault.com/?enc=HHG6tAqOCGuvJYr5dSjafQ%3D%3D.WeEiK0v1T9Ez2PSK%2F4BYgs3MrhMZcRZWkVgSUrYA7wq5QjwRW41%2FpXVerwALb3QlbYfImySjoROuPQB7sIAF0ljd4%2BS%2FRwSzMoqFHAJBqHg%3D" rel="nofollow">Web 兼容性问题</a>,因此他们决定将其实现为静态方法 ( )。</p><p><code>Object.groupBy()</code>只需两个参数即可简化数组中对象分组的过程:数组本身和回调函数。</p><p>在过去,您必须编写一个自定义函数来对数组元素进行分组或从外部库导入分组方法。</p><p><strong>可用性:</strong> <code>Object.groupBy()</code>现在所有主要浏览器平台都支持</p><h2>正则表达式v标志</h2><p>大家可能熟悉正则表达式 Unicode 标志 ( <code>u</code>),它允许启用对 Unicode 字符的支持。该<code>v</code>标志是<code>u</code>标志大部分功能的扩展。</p><p>它除了主要向后兼容该<code>u</code>标志之外,还引入了以下新功能:</p><h3>交集运算符</h3><p>交集运算符可以匹配两个字符集中必须存在的字符。其语法为<code>[operand-one&&operand-two]</code>,其中<code>&&</code>表示交集运算符, <code>operand-one</code>和<code>operand-two</code>表示各自的字符集。</p><pre><code class="js">const str = 'My name is nanjiu'
const strReg = /[[a-z]&&[^aeiou]]/gv
const strArr = str.match(strReg)
console.log('【strArr】', strArr)
// 【strArr】 ['y', 'n', 'm', 's', 'n', 'n', 'j']</code></pre><ul><li><code>[a-z]</code>上面的代码定义了一个匹配小写字母和非元音字符的交集的正则表达式<code>[^aeiuo]</code>。</li><li>运算<code>&&</code>符确保仅匹配两个集合共有的字符。</li><li>这些<code>gv</code>标志启用全局搜索(查找所有匹配项)和正则表达式 v 模式。</li></ul><h3>差异运算符</h3><p>差异运算符由两个连续的连字符 ( <code>--</code>) 表示,提供了一种在正则表达式中指定排除项的便捷方法。正则表达式引擎将忽略<code>--</code>后面的任何字符集</p><p>查找非 ASCII 表情符号字符:</p><pre><code class="js">let myEmojis = "😁,😍,😴,☉‿⊙,:O";
let myRegex = /[\p{Emoji}--\p{ASCII}]/gv;
console.log(myEmojis.match(myRegex));
// ["😁","😍","😴"]</code></pre><p><strong>可用性:</strong> 所有主要 JavaScript 环境都支持该<code>v</code>标志。</p><h2>Promise.withResolvers()</h2><p><code>Promise.withResolvers()</code>是一个静态方法,它返回一个包含三个属性的对象:</p><ul><li>promise:一个新的peomise对象</li><li>resolve:一个函数,用于解决该promise</li><li>reject:一个函数,用于拒绝该promise</li></ul><p>很多时候,我们希望能够在<code>promise</code>外部访问<code>resolve</code>和<code>reject</code>,在这之前我们都是通过以下形式解决的</p><pre><code class="js">let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});</code></pre><p>现在我们可以使用<code>Promise.withResolvers</code>来优雅的解决这个问题,并且<code>resolve</code>和<code>reject</code>函数现在与 Promise 本身处于同一作用域,而不是在执行器中被创建和一次性使用。这可能使得一些更高级的用例成为可能,例如在重复事件中重用它们,特别是在处理流和队列时。这通常也意味着相比在执行器内包装大量逻辑,嵌套会更少。</p><pre><code class="js">const getList = () => {
const { resolve, reject, promise } = Promise.withResolvers()
setTimeout(() => {
const list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
resolve(list)
}, 1000)
return promise
}
getList().then(res => {
console.log('【res】', res)
})
// 【res】 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]</code></pre><p><strong>可用性:</strong> 适用于所有主要浏览器。</p><p>注意:<code>Promise.withResolvers()</code>尚未包含在 Node.js 中。因此,提供的示例可能无法在 Node.js 中按预期运行</p><h2>四种新的非改变数组方法</h2><p>通过复制改变数组引入了四种新的非改变数组方法: <code>toReversed()</code>、<code>toSpliced()</code>、<code>toSorted()</code>和<code>with()</code></p><p>前三个在功能上等同于它们的相似方法: <code>reverse()</code>、<code>splice()</code>和<code>sort()</code>。</p><p><strong>它们与对应方法的功能相似,区别在于新增的三个方法不会改变原数组</strong></p><p><code>with()</code>是第四个新的数组方法。它允许我们替换数组中特定位置的元素,同样不会改变原数组</p><pre><code class="js">const groupList = [1, 2, 3, 4, 5, 6]
const newGroupList = groupList.with(2, 'nanjiu')
console.log('【newGroupList】', newGroupList)
console.log('【groupList】', groupList)</code></pre><p><img src="/img/remote/1460000044689223" alt="group-3.png" title="group-3.png"></p><p><strong>可用性:</strong> 适用于所有主要的 JavaScript 运行时和浏览器中。</p>
快速了解:user-valid和:user-invalid
https://segmentfault.com/a/1190000044682751
2024-03-05T12:45:34+08:00
2024-03-05T12:45:34+08:00
XboxYan
https://segmentfault.com/u/xboxyan
4
<p>最近,<code>Chrome 119</code> 终于正式对<code>:user-valid</code>和<code>:user-invalid</code>这两个验证伪类进行了支持</p><p><img src="/img/remote/1460000044682753" alt="image-20240302154633596" title="image-20240302154633596"></p><p>至此,现代浏览器总算是全面支持了。</p><p>看名称,似乎和<code>:valid</code>和<code>:invalid</code>有点相似,那么有什么区别呢?快速了解一下吧</p><h2>一、:valid 和 :invalid 的缺陷</h2><p>大家可能或多或少都用过或者见过这两个伪类,这里简单介绍一下</p><p>这两个都是做表单验证的,当表单输入合法或者非法的时候匹配。</p><p>比如这样一个输入框,设置了<code>required</code>属性,表示必填项</p><pre><code class="html"><form>
<label for="user">user *: </label>
<input id="user" name="user" required />
<span></span>
</form></code></pre><p>目前效果是这样的</p><p><img src="/img/remote/1460000044682754" alt="image-20240302155320463" title="image-20240302155320463"></p><p>下面我们通过<code>:invalid</code>伪类进行校验,在不合法的情况下边框变红,并给出提示</p><pre><code class="css">input:invalid {
border-color: red;
}
input:invalid + span::before {
content: "请输入";
color: red;
}</code></pre><p>当输入框没有内容时,就会出现错误提示</p><p><img src="/img/remote/1460000044682755" alt="image-20240302155644831" title="image-20240302155644831"></p><p>一旦输入内容了,提示就消失了</p><p><img src="/img/remote/1460000044682756" alt="image-20240302155726592" title="image-20240302155726592"></p><p>乍一看好像没什么毛病。</p><p>但是有个问题是,这个校验太及时了,用户还没做出任何交互,页面一进来就就出现错误提示了,你想想,用户一打开就看见大部分的输入框都是红色的,是不是不太友好?</p><p>为了解决这个问题,<code>:user-valid</code>和<code>:user-invalid</code>就出现了!</p><h2>二、更友好的校验方式</h2><p>相对于<code>:valid</code>和<code>:invalid</code>,新加了<code>user-</code>前缀,其实就是表示用户,也就是说,这个校验要等用户操作之后才会校验,避免满屏都是红色输入框的尴尬。</p><p>使用方式和规则和前面完全相同,我们把前面的<code>CSS</code>改一下</p><pre><code class="css">input:user-invalid {
border-color: red;
}
input:user-invalid + span::before {
content: "请输入";
color: red;
}</code></pre><p>下面是两种伪类的对比</p><p><img src="/img/remote/1460000044682757" alt="Kapture 2024-03-02 at 16.06.40" title="Kapture 2024-03-02 at 16.06.40"></p><p>可以看到,下面的输入框在初始情况下是不会校验的,只有用户手动输入后才会校验,这样体验就会好很多了~</p><p>值得注意的是,在用户操作之前(聚焦、输入等),即使是用脚本改变输入框内容,也是不会触发的</p><pre><code class="js">input.value = '1223';
input.value = ''; // 不会提示错误</code></pre><h2>三、简单总结一下</h2><p>这样一个小知识点,你学到了吗?再来简单回顾一下</p><ol><li><code>:valid</code>和<code>:invalid</code>可以用于表单验证</li><li>不过这种方式校验太及时了,用户还没做出任何交互,就出现一堆错误提示,体验不太友好</li><li><code>:user-valid</code>和<code>:user-invalid</code>只有在用户操作以后才会触发,提升了体验</li><li>在用户操作之前,用 <code>JS</code>改变表单内容是不会触发的</li></ol><p>当然这个特性目前三五年内都是无法使用的,不过没关系,我们也可以参考这样的交互方式,提升用户体验。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤</p>
JavaScript之structuredClone现代深拷贝
https://segmentfault.com/a/1190000044678700
2024-03-04T10:25:28+08:00
2024-03-04T10:25:28+08:00
南城FE
https://segmentfault.com/u/nanchengfe
6
<p>在JavaScript中,实现深拷贝的方式有很多种,每种方式都有其优点和缺点。今天介绍一种原生JavaScript提供的<code>structuredClone</code>实现深拷贝。</p><p>下面列举一些常见的方式,以及它们的代码示例和优缺点:</p><h3>1. 使用<code>JSON.parse(JSON.stringify(obj))</code></h3><p>代码示例:</p><pre><code class="javascript">function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}</code></pre><p>优点:简单易行,对于大多数对象类型有效。</p><p>缺点:不能复制原型链,对于包含循环引用的对象可能出现问题。比如以下代码:</p><pre><code class="js">const calendarEvent = {
date: new Date()
}
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))</code></pre><p>最终得到的date不是Data对象,而是字符串。</p><pre><code class="json">{
"date": "2024-03-02T03:43:35.890Z"
}</code></pre><p>这是因为<code>JSON.stringify</code>只能处理基本的对象、数组。任何其他类型都没有按预期处理。例如,日期转换为字符串。Set/Map只是转换为<code>{}</code>。</p><pre><code class="js">const kitchenSink = {
set: new Set([1, 3, 3]),
map: new Map([[1, 2]]),
regex: /foo/,
deep: { array: [ new File(someBlobData, 'file.txt') ] },
error: new Error('Hello!')
}
const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))</code></pre><p>最终得到如下数据:</p><pre><code class="json">{
"set": {},
"map": {},
"regex": {},
"deep": {
"array": [
{}
]
},
"error": {},
}</code></pre><h3><strong>2. 使用递归</strong></h3><p>代码示例:</p><pre><code class="javascript">function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
let clone = obj.constructor();
for (let attr in obj) {
if (obj.hasOwnProperty(attr)) {
clone[attr] = this.deepClone(obj[attr]);
}
}
return clone;
}</code></pre><p>优点:对于任何类型的对象都有效,包括循环引用。</p><p>缺点:对于大型对象可能会消耗大量内存,并可能导致堆栈溢出。</p><h3>3. 第三方库,如 lodash 的 <code>_.cloneDeep</code> 方法</h3><p>代码示例:</p><pre><code class="javascript">const _ = require('lodash');
function deepClone(obj) {
return _.cloneDeep(obj);
}</code></pre><p>优点:支持更多类型的对象和库,例如,支持 Proxy 对象。</p><p>缺点:会引入依赖导致项目体积增大。</p><p><img src="/img/remote/1460000044678702" alt="" title=""></p><p>因为这个函数会导致17.4kb的依赖引入,如果只是引入lodash会更高。</p><h3>4. 现代深拷贝structuredClone</h3><p>在现代浏览器中,可以使用 <code>structuredClone</code> 方法来实现深拷贝,它是一种更高效、更安全的深拷贝方式。</p><p>以下是一个示例代码,演示如何使用 <code>structuredClone</code> 进行深拷贝:</p><pre><code class="js">const kitchenSink = {
set: new Set([1, 3, 3]),
map: new Map([[1, 2]]),
regex: /foo/,
deep: { array: [ new File(someBlobData, 'file.txt') ] },
error: new Error('Hello!')
}
kitchenSink.circular = kitchenSink
const clonedSink = structuredClone(kitchenSink)</code></pre><p><code>structuredClone</code>可以做到:</p><ul><li>拷贝无限嵌套的对象和数组</li><li>拷贝循环引用</li><li>拷贝各种各样的JavaScript类型,如<code>Date</code>、<code>Set</code>、<code>Map</code>、<code>Error</code>、<code>RegExp</code>、<code>ArrayBuffer</code>、<code>Blob</code>、<code>File</code>、<code>ImageData</code>等</li></ul><p>哪些不能拷贝:</p><ul><li>函数</li><li>DOM节点</li><li>属性描述、<code>setter</code>和<code>getter</code></li><li>对象原型链</li></ul><p><strong>所支持的完整列表:</strong></p><p><code>Array</code>、<code>ArrayBuffer</code>、<code>Boolean</code>、<code>DataView</code>、<code>Date</code>、<code>Error</code>类型(下面具体列出的类型)、<code>Map</code>、<code>Object</code>,但仅限于普通对象、原始类型,除了<code>symbol</code>(又名<code>number</code>、<code>string</code>、<code>null</code>、<code>undefined</code>、<code>boolean</code>、<code>BigInt</code>)、<code>RegExp</code>、<code>Set</code>、<code>TypedArray</code></p><p><strong>Error类型:</strong></p><p><code>Error</code>, <code>EvalError</code>, <code>RangeError</code>, <code>ReferenceError</code> , <code>SyntaxError</code>, <code>TypeError</code>, <code>URIError</code></p><p><strong>Web/API类型:</strong></p><p><code>AudioData</code>, <code>Blob</code>, <code>CryptoKey</code>, <code>DOMException</code>, <code>DOMMatrix</code>, <code>DOMMatrixReadOnly</code>, <code>DOMPoint</code>, <code>DomQuad</code>, <code>DomRect</code>, <code>File</code>, <code>FileList</code>, <code>FileSystemDirectoryHandle</code>, <code>FileSystemFileHandle</code>, <code>FileSystemHandle</code>, <code>ImageBitmap</code>, <code>ImageData</code>, <code>RTCCertificate</code>, <code>VideoFrame</code></p><p>值得庆幸的是 <code>structuredClone</code> 在所有主流浏览器中都受支持,也支持Node.js和Deno。</p><p><img src="/img/remote/1460000044678703" alt="" title=""></p><h3>最后</h3><p>我们现在终于可以直接使用原生JavaScript中的<code>structuredClone</code>能力实现深度拷贝对象。每种方式都有其优缺点,具体使用方式取决于你的需求和目标对象的类型。</p><h3>参考</h3><ul><li>Deep Cloning Objects in JavaScript, the Modern Way(www.builder.io/blog/structured-clone)</li><li>mozilla structuredClone(developer.mozilla.org/zh-CN/docs/Web/API/structuredClone)</li></ul><hr><p><strong>看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~</strong></p><p>专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)</p>
60 万单项公益创新资助!开发者改变世界的机会来了
https://segmentfault.com/a/1190000044674387
2024-03-01T17:58:35+08:00
2024-03-01T17:58:35+08:00
ShirleyYD
https://segmentfault.com/u/shirleyyd
0
<p>你不一定真的给谁的 KFC V 过 50,但你或许为他们扫码参与过“捐一元”活动。</p><p><img src="/img/remote/1460000044674389" alt="图片" title="图片"></p><p>你或许还见过这样一张 404 寻人页面。</p><p><img src="/img/remote/1460000044674390" alt="图片" title="图片"></p><p>又或者你还参与过 2021 年的河南暴雨时一份“救命文档”的共建。</p><p><img src="/img/remote/1460000044674391" alt="图片" title="图片"></p><p>技术日新月异,公益的形式与内涵早已跳出了捐款箱和物资车。</p><p>当技术与公益相遇,技术就不再只是冰冷的代码,它们是神奇的咒语,是希望的微光,是通往更好世界的钥匙,而掌握着技术的开发者,拥有的是足以改变世界的力量。</p><p>在 SegmentFault 过往的 Hackathon 中,也诞生了包括服务于动物救助的项目,帮助听障人群沟通的项目以及助力乡村教育公平等方面的优秀公益项目。我们相信,科技会让世界更好,也应该让世界更好。</p><p>行动起来的还有许多头部科技公司。不久前腾讯 Light·技术公益创造营在云南丽江正式启动。这是腾讯举办 Light·技术公益创造营的第四年。过去三年,创造营吸引了超过 3600 支队伍、18000 名开发者参加,SegmentFault 思否和 Light 共同见证了诸如蒙新河狸保护、新生儿黄疸 AI 图像识别等优秀技术公益项目的诞生。</p><p>今年,创造营将围绕数字时代下的“儿童素养教育、社区公益、生态环保”三大重要议题展开,鼓励开发者利用技术力量共创社会公益解决方案。</p><h2>两大升级</h2><ul><li>服务覆盖面更广<br>本届创造营引入了全新的社区公益议题,将视线扩大到整个社会公民,鼓励开发者以社区为中心,从助老、爱幼、救灾、心理健康、社区服务等多个角度出发,设计提供技术解决方案。</li><li>技术工具箱更全<br> 本次,Light 加码开放腾讯云端领先技术能力,为公益开发者提供了七大数字化工具,包括AI、安全、音视频、智能客服、数智人、智能翻译和云开源等技术,进一步降低公益事业参与门槛,为技术公益注入丰沛动力。</li></ul><h2>三大领域</h2><p>本届创造营围绕“儿童素养教育、社区公益、生态环保”三大重要议题展开,无论是利用前沿技术助力生态保护,还是运用人工智能提升人类社区生活质量,亦或是通过创新应用推动儿童素养教育,你们的努力都将带来积极的影响。</p><p><img src="/img/remote/1460000044674392" alt="图片" title="图片"></p><h2>60 万单项激励</h2><p>这一次,你不仅可以展现个人的技术实力,团队的协作能力,还能获得丰厚的回报。最佳公益创新项目将获得最高 60 万的单项资助。此外,你还将有机会与各领域的优秀开发者深入交流,共同探索技术与公益结合的创新路径。</p><p>时间紧迫!现在就行动起来,让科技为更好!</p><p><img src="/img/remote/1460000044674393" alt="图片" title="图片"></p><p>了解更多活动详情,即刻与 AI 同行!</p><p><strong>活动官网:</strong><a href="https://link.segmentfault.com/?enc=8otbOjZ4Y8VGp8s3T%2FAsHg%3D%3D.0us1yHnUaCW8aVfa6eoiA%2FG8AelAn7T%2FBVNyFB72SDoVoKRN9NTcG9Is2ImETkB4" rel="nofollow">https://light.mofyi.tencent.com/</a><br><strong>报名通道:</strong><a href="https://link.segmentfault.com/?enc=LtBfVkDOxNasewyEP2vOBw%3D%3D.2Zbr3Bbc9LkkmPUN5I8%2BG2EcAXk1F65kgaC6F6s9av0%3D" rel="nofollow">https://jinshuju.net/f/G9cBSa</a></p><hr><p>点击<strong><a href="https://segmentfault.com/e/1160000044555549">阅读原文</a></strong>,马上报名!</p>
跳槽必看MySQL索引:B+树原理揭秘与索引优缺点分析
https://segmentfault.com/a/1190000044668913
2024-02-29T15:16:35+08:00
2024-02-29T15:16:35+08:00
王中阳Go
https://segmentfault.com/u/wangzhongyang_go
3
<blockquote>金三银四跳槽季,不知道你准备的怎么样了?</blockquote><p>前段时间我分享了两篇文章,粉丝股东们纷纷表示<strong>有用,有启发:</strong>,之前没看的话可以先看看:</p><p><a href="https://link.segmentfault.com/?enc=gwQRW2faBY3U4Sunty%2BNkA%3D%3D.tkpeYwlN0bKdPTH148mO0N%2FVbeABXO8A4XdB3JuCIANVCeCEo3gcQwwJZi61F2D6A5RzKtjUjBefZXOliA5qRFiyV8JyzKe3MSpsbWNdizGDafFIDnakrFxggUpELDvXyfhq8UsQUltjt3tr3S2x9zUc6dS3EHjsv1DuLlqtN%2FPf6uWprTPJbcMVb%2FuOwU2VeBM7zgSRHah8qbJP64%2BLUip3VKbU5Ya%2FJAjGgThPlzXQoMWsNQ3UvNZ1TpTucOPVs6EkKrQCIMSH6wDL0pTaJdUDHdRw6ak7yHedM4sl%2Fx8%3D" rel="nofollow">程序员金三银四跳槽指南:时间线&经典面试16问</a></p><p><a href="https://link.segmentfault.com/?enc=k17WkroRqI0UBTpKu%2FrOzg%3D%3D.rOa3%2FERBfe6PS0ShK%2B8FIxtGx0oeowPFnvJbJP3rJE1b6RJemdthgnpkyIsJudqsX0erVEvf9b3wO5CRX%2FCf31f7Dpj7ke9hg5P1YoQSUKX34m5i4NnXoeMh%2FqVk%2BdXOPZOqtNgqYN4xE%2Fjdkzot6UBKm1WAnsabgpJZIPHLk7gh08Kt1iwaas2aNirCT6IovqDGhy%2Fmgx%2FqIz0I57xopG5ay2G%2BqtKqoIXunm4v1JG890fpyev8%2Boob1W6NcqIs%2F2h%2B1nKRx6iINXAbGdN7zdPZnPjG5SRU3qkBpz3YvAM%3D" rel="nofollow">这才开工没几天就收到喜报了,简历改了是真有用!</a></p><p>今天再给大家分享一下数据库索引的详解文章,这基本是必考的知识点。</p><h2>一、索引介绍</h2><h3>1、索引定义</h3><p>索引是存储引擎中,<strong>用于快速找到记录的一种数据结构</strong>。索引能够帮助存储引擎快速获取数据,形象的说就是索引是数据的目录。</p><p>所谓的<strong>存储引擎</strong>,通俗的来说就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等 技术的实现方法。</p><p><code>MySQL</code>存储引擎有<code>MyISAM</code>、<code>InnoDB</code>、<code>Memory</code>,其中<code>InnoDB</code>是在<code>MySQL 5.5</code>之后成为默认的存 储引擎。</p><p>在实际场景中,索引对于良好的性能起到非常关键的作用。或许在数据量小且负载较低时,索引的不恰当使用可能对性能的影响可能不会太明显,但是当表中的数据量越来越大的时候,索引对性能的影响就愈发重要,不恰当的索引会让性能急剧的下降。</p><h3>2、索引的查找方式</h3><p>在<code>MySQL</code>的<code>InnoDB</code>存储引擎中</p><h4><strong>若没有索引的情况下进行数据查询</strong></h4><p><strong>a) 在一个数据页中查询</strong></p><p>当表中的记录比较少时,所有记录可以存放到一个数据页中。当查询记录时,根据搜索条件的不同查询分为两种情况:</p><ul><li>以<code>主键</code>为搜索条件:在一个数据页内的记录会根据主键值的大小从小到大的顺序组成一个单向链表。每个数据页都会为存储在它里面的记录生成一个页目录。通过主键查询某条记录可以<strong>在页目录中使用二分法快速定位到对应的槽</strong>,然后再遍历该槽对应分组的记录,即可快速找到指定的记录。</li><li>以<code>其他列</code>作为搜索条件:对于非主键列的查找,由于没有为非主键列建立对应的目录页,即<strong>未创建索引</strong>。无法用二分法快速定位相应的槽,只能从<code>Infimum</code>记录开始依次遍历单向链表中的每条记录,然后对比每条记录是否符合搜索条件,即全表扫描,因此效率非常低。</li></ul><p><strong>b) 在多个数据页中查询</strong></p><p>在很多情况下,表中存放的记录是非常多的,需要查询到的数据可能分布在多个数据页中,在多个页中查找记录可以分为两个步骤:</p><ul><li><strong>定位到记录所在的页</strong></li><li><strong>从所在的页内查找相应的记录</strong></li></ul><p>在没有索引的情况下,无论是根据主键列还是其他列的值查找,都不能快速定位到记录所在的页,因此只能从第一页沿着双向链表一直往下找,因而非常耗时。</p><h4><strong>若存在索引的情况下进行数据查询</strong></h4><p>在创建索引的情况下,每个数据页都会为存储在它里面的记录生成一个目录项,在通过索引查找某条记录时可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录,快速找到指定的记录,确定记录后,即可向下寻找当前记录对应的下一个页节点,直到寻找到存在目标记录的叶子结点。</p><h2>二、索引分类</h2><h3>1、按数据结构分类</h3><h4><strong>Hash索引</strong></h4><p>哈希表是一种以<code>键-值(key-value)</code>存储数据的结构,输入待查找的键,即<code>key</code>,就可以找到其对应的值,即<code>value</code>。</p><p>哈希的思路很简单,把值放在数组里,用一个哈希函数把<code>key</code>换算成一个确定的位置,然后把<code>value</code>放在数组的这个位置。</p><p>不可避免地,多个<code>key</code>值经过哈希函数的换算,会出现同一个值的情况,即哈希碰撞,处理这种情况的一种方法是,拉出一个链表。</p><p>但是,<strong>在哈希表中,数据的存储不是按顺序存放的</strong>,所以哈希索引做区间查询的速度是很慢的。</p><p>所以,哈希表这种结构<strong>适用于只有等值查询的场景</strong>,比如<code>Memcached</code>及其他一些<code>NoSQL</code>引擎。</p><h4><strong>有序数组</strong></h4><p>有序数组在<strong>等值查询和范围查询</strong>场景中的性能就都非常优秀。</p><p>在查找数据方面,有序数组可以<strong>通过二分查找的方式</strong>快速找到,时间复杂度是 <code>O(log(N))</code>。</p><p>同样,有序数组的索引结构<strong>支持范围查询</strong>,通过<strong>二分法</strong>找到需要查找的范围的首元素,然后向后遍历,直到找到第一个不满足条件的元素为止。</p><p>如果仅仅看查询效率,有序数组就是最好的数据结构了。但是,在需要更新数据的时候就麻烦了,往中间插入一个记录就必须得挪动后面所有的记录,成本太高。</p><p>所以,<strong>有序数组索引一般适用于静态存储引擎</strong>。</p><h4><strong>B+树(InnoDB索引结构)</strong></h4><p>在<code>MySQL 5.5</code>之后,<code>InnoDB</code>成为默认的<code>MySQL</code>存储引擎,<code>B+Tree</code>索引类型也是<code>MySQL</code>存储引擎采用最多的索引类型。</p><p>在<code>InnoDB</code>数据页中,各个数据页可以组成一个<strong>双向链表</strong>,而每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表。</p><p>在介绍<code>B+树</code>时,我们以主键索引为例,来看看<code>InnoDB</code>是如何构建主键索引的<code>B+树</code>。其他字段所建立的索引与主键索引相似,只是将主键字段替换成指定的索引字段来构建<code>B+树</code>。</p><h5>主键策略</h5><p>在创建表时,<code>InnoDB</code>存储引擎会根据不同的场景选择不同的列作为主键索引:</p><ul><li>如果有指定主键,默认会使用主键作为聚簇索引的索引键</li><li>如果没有指定主键,则选择第一个不包含<code>NULL</code>值的唯一列作为聚簇索引的索引键</li><li>在上面两个都没有的情况下,<code>InnoDB</code>将自动生成一个<code>隐式自增id</code>列作为聚簇索引的索引键</li></ul><p>除主键索引外,其它索引都属于<code>辅助索引(Secondary Index)</code>,也被称为<strong>二级索引</strong>或<strong>非聚簇索引</strong>。创建的主键索引和二级索引默认使用的是<code>B+Tree</code>索引。</p><h5>建立B+树索引的条件</h5><p><strong>条件一:下一个数据页中记录的主键值必须大于上一个数据页中记录的主键值</strong></p><p>我们知道,在<code>MySQL</code>中,新分配的数据页编号可能并不是连续的,即这些数据页在磁盘上并非紧挨着存储。需要通过维护上一下和下一页的编号,因此,<strong>在<code>InnoDB</code>中,每个数据页组成了一个双向链表</strong>来维护每个数据页之间的上下关系。</p><p><strong>为什么构建B+树需要满足条件一呢?</strong></p><p>原因在于为了<strong>提高范围查询的效率</strong>,<code>B+树</code>要求<strong>叶子节点中的数据记录按照主键值的顺序进行排列</strong>。</p><p>当进行范围查询时,如果叶子节点中的数据记录不按照主键值的顺序排列,就会增加查找的复杂度。如果下一个数据页中记录的主键值小于上一个数据页中记录的主键值,那么在进行范围查询时就需要在不同的叶子节点之间来回跳转,这样会增加IO操作次数和查询时间。</p><p>因此,为了保证范围查询的效率,B+树要求叶子节点中记录的主键值必须按照顺序排列,即下一个数据页中记录的主键值必须大于上一个数据页中记录的主键值。这样可以确保在进行范围查询时可以顺利地按照主键值的顺序进行遍历,提高查询效率,同样MySQL中的预加载页功能也可以减少IO操作次数。</p><p>在<code>InnoDB</code>中,在对页中的记录进行增删改操作时,必须通过一些记录移动的操作来始终保证:<strong>下一个数据页中用户的记录的主键值必须大于上一个页中用户记录的主键值</strong>,则这个过程也成为<strong>页分裂操作</strong>,即在一个数据页中插入记录,而该数据页在插入之前已经满了,则需要申请一个新的数据页,然后挪动部分数据过去。</p><p><strong>条件二:需要给所有的数据页建立一个目录页</strong></p><p>由于每个数据页的编号可能并不连续,因此需要为这些数据页建立一个目录。</p><p>比如当我们看一本书的时候,书的目录可以帮助我们快速定位到我们想看的内容,而目录标题对应的页号可以比作每个数据页的页号,通过书的目录我们可以快速定位到我们想看的内容,同样的道理,通过为数据页建立目录,在目录中存储数据页的编号,即可通过目录快速定位到相应的数据页。</p><p>目录页可以包括两个内容:</p><ul><li>数据页的记录中最小的主键值,用<code>key</code>表示</li><li>数据页页编号,用<code>page_no</code>表示</li></ul><p>为了方便说明,我们可以定义一个数据表:</p><pre><code>CREATE TABLE `index_demo` (
`a` int NOT NULL,
`b` int DEFAULT NULL,
`c` char(1) DEFAULT NULL,
PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=COMPACT;</code></pre><p>上述表定义中,使用了<code>COMPACT</code>行格式来存储数据,<code>COMPACT</code>行格式的简化存储如下:</p><p><img src="/img/remote/1460000044668915" alt="" title=""></p><p>我们假设每个数据页最多只能存放2条记录(实际一个数据页非常大,可以存放许多的记录)</p><p>当为数据页编制其对应的目录页时,如下图所示:</p><p><img src="/img/remote/1460000044668916" alt="" title=""></p><p>我们以<code>页10</code>为例,为其编制的目录项为目录项1,该目录项包含了该页的<code>页号10</code>以及该页的数据记录中<code>主键值最小的值1</code>。当我们把这些目录项在物理存储器中连续存储,就可以实现根据主键快速查找某条记录的功能了。举个例子,当我们想查询主键值为100的记录时,具体的查询过程如下:</p><ul><li>通过目录项,根据二分查找快速定位到主键值为<code>100</code>的记录在目录项3中(因为12 < 100 < 209),其对应的数据页页号为9</li><li>再根据上述聊到的在单个页中查询记录的方式,即可在页9中查询到主键值为<code>100</code>的记录。</li></ul><h5>B+树的结构</h5><p>上述我们聊到构建B+树的条件,其核心是<strong>通过使用二分法的方式快速定位到具体的目录项,再通过目录项快速定位到我们所需要的数据页</strong>。</p><p>我们在聊到给数据页记录建立其对应的目录项时,数据页内的每条记录都会有<code>record_type</code>字段,它的各个取值代表意思如下:</p><ul><li><code>0</code>:普通数据记录,即我们插入的数据记录</li><li><code>2</code>:<code>Infimum</code>记录,在B+树索引中,<code>Infimum</code>记录位于整个索引的最左边,用于表示没有更小值的情况。</li><li><code>3</code>:<code>Supermun</code>记录,在B+树索引中,<code>Supremum</code>记录位于整个索引的最右边,用于表示没有更大值的情况。</li></ul><p>你可能会好奇,取值有<code>0</code>、<code>2</code>、<code>3</code>,那取值<code>1</code>是什么意思呢?</p><p>我们注意到,在目录项中存储两个字段,分别是最小主键值以及对应的页号,事实上,<strong>存放目录项的页与实际存放数据的数据页并无区别</strong>,目录项中的两个字段(主键值以及页号)完全可以看作是存储的实际数据,因此,目录项中记录可以称作为目录项记录,即目录页,那InnoDB是如何区别实际数据记录与目录页记录呢,答案就是<code>record_type</code>字段的取值为<code>1</code>来表示目录页记录。</p><ul><li><code>0</code>:普通数据记录,即数据页记录</li><li><code>1</code>: 目录项记录,即目录页记录</li><li><code>2</code>:<code>Infimum</code>记录</li><li><code>3</code>:<code>Supermun</code>记录</li></ul><p><img src="/img/remote/1460000044668917" alt="" title=""></p><p>通过上图可以看到,我们使用页30来存放目录页记录,目录页记录与数据页记录的不同点在于:</p><ul><li>目录页记录的<code>record_type=1</code>,而数据页记录的<code>record_type=0</code></li><li>目录页记录只包含主键值(索引值)和页号,而数据页记录则可以由用户来自定义,可以包含很多列。</li></ul><p>除此之外,两者并无实际的区别,在目录页中,同样可以通过二分法来快速定位到目录页记录,再通过目录页记录向下查询到其对应的数据页。</p><p>当然,当目录页中的记录过多,一个目录页难以容纳时,我们可以再多分配一个存储目录项的页即可。当同层存在多个目录项时,我们则可以向上再分配更高一级的目录项,即多级目录。</p><p>我们可以观察到,这样多级的目录,不就像一颗树的数据结构了呢?其实,这就是<code>InnoDB</code>的<code>B+树</code>结构。</p><p><img src="/img/remote/1460000044668918" alt="" title=""></p><h5>B+树的特点</h5><p>无论是存放实际数据的数据页,还是存放目录项记录的目录页,都可以把它们放到<code>B+树</code>当中,这些页称为<code>B+树</code>的节点。</p><p>其中,存放我们插入的实际数据的记录存放在<code>B+树</code>的最底层节点,这些节点称为叶子节点。其余非叶子节点则用来存放目录项记录。其中<code>B+树</code>最上层的节点称为根节点。</p><p><code>B+树</code>的叶子节点之间是用「双向链表」进行连接,这样的好处是既能向右遍历,也能向左遍历。</p><h3>2、按物理存储分类</h3><h4><strong>聚簇索引(主键索引)</strong></h4><p>聚簇索引,又可以称为主键索引,在创建表时,InnoDB会默认为主键创建一棵B+树的主键索引。</p><p><strong>聚簇索引的特点</strong>在于:</p><ul><li>使用记录的主键值大小来对记录和页进行排序。</li><li>页(包括叶子节点和非叶子节点)内的记录按照主键值的大小进行排序组成单向链表。页内的记录会被划分为若干的组,每个组中主键值最大的记录在页内的偏移量会被当做该组的槽放在页目录中,以便后续可以通过二分法来查找定位到需要查找的记录。</li><li>存放实际数据的各个数据页(叶子节点)同样也根据主键大小排成双向链表。</li><li>存放目录数据的各个目录页(非叶子节点)可以分为B+树的多个层级,在同一层级的目录页页根据也中存储的主键值大小来排序成一个双向链表。</li><li>B+树的叶子节点存放的是完整的数据记录,即保存了该记录的所有列的值。</li></ul><p>包含以上两个特点的B+树称为聚簇索引。</p><h4><strong>非聚簇索引(二级索引)</strong></h4><p>不同于聚簇索引,<strong>非聚簇索引存储的并不是完整的数据,非聚簇索引的叶子节点存放的是指定的索引列+主键值</strong>。非聚簇索引的目录页存储的记录中不再是主键+页号的搭配,而是指定的索引列+页号。</p><p>以非主键列的大小为排序规则而建立的B+树需要执行回表操作才可以定位到完整的用户记录,这种B+树也称为二级索引或辅助索引。</p><p>同样非聚簇索引的B+树的叶子节点也会按照索引列排序并组成双向链表,同一层级的目录页也会根据索引列的大小来排序组成双向链表。</p><h5>回表</h5><p><strong>聚簇索引和非聚簇索引的查询有什么区别?</strong></p><p>当使用二级索引进行查询定位到符合条件的记录时,当索引中并未存储我们需要查询的字段时(非聚簇索引值的叶子节点只存储主键值以及指定索引值,可能存在需要查询的字段并未存储在索引B+树中),需要根据二级索引中存储的主键值,回表到主键索引查询到对应的字段才能够返回。</p><p>根据该记录中的主键信息回到聚簇索引中查找到完整的用户记录。通过携带主键信息到聚簇索引中重新定位完整的用户记录的过程也成为<strong>回表</strong>。</p><p>在查询时,<strong>回表是有代价的</strong>,我们知道,在使用二级索引进行<strong>范围查询</strong>的时候,二级索引对应的主键值的大小是毫无规律的,<strong>每读取一条二级索引记录,就需要根据该二级索引记录的主键值到聚簇索引中执行回表操作</strong>,如果对应的聚簇索引记录所在的页面不在内存中,就需要将该页面从磁盘加载到内存中由于要读取很多主键值并不连续的聚簇索引记录,而这些聚簇索引记录分布在不同的数据页中,这些数据页的页号也毫无规律,因此会造成大量的随机I/O。</p><p>需要执行回表操作的记录越多,使用二级索引进行查询的性能就越低,因此在某些查询场景下,MySQL优化器宁愿使用全表扫描也不使用二级索引,而<strong>选择全表扫描,还是二级索引+回表,这就是查询优化器的工作了</strong>。</p><p>查询优化器会事先针对表中的记录计算一些统计数据,再利用这些统计数据或者访问表中的少量记录来计算回表操作的记录数,如果需要执行回表操作的记录数越多,就越倾向于使用全表扫描,反之则倾向于使用二级索引+回表。</p><p>既然回表有一定的代价,那<strong>为什么还需要进行回表呢?</strong> 在创建索引时直接就完整的数据记录放入索引的叶子节点不就好了么?</p><p>如果完整的数据放入到每个索引建立的B+树的叶子节点中确实可以避免回表,但是取而代之的是需要更多的存储空间,太占地方,相当于每建立一棵B+树都需要将所有的完整数据复制一遍。</p><h3>3、按字段特性分类</h3><h4><strong>主键索引</strong></h4><p>主键索引,即聚簇索引,建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。</p><p>在创建表时,创建主键索引的方式如下:</p><pre><code>CREATE TABLE table_name (
...
PRIMARY KEY (index_column_1) USING BTREE
);</code></pre><h4><strong>唯一索引</strong></h4><p>唯一索引建立在<code>UNIQUE</code>字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。</p><p>在创建表时,创建唯一索引的方式如下:</p><pre><code>CREATE TABLE table_name (
....
UNIQUE KEY(index_column_1,index_column_2,...)
);</code></pre><p>建表后,如果要创建唯一索引,可以使用这面这条命令:</p><pre><code>CREATE UNIQUE INDEX index_name ON table_name(index_column_1,index_column_2,...);</code></pre><h4><strong>普通索引</strong></h4><p>普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为<code>UNIQUE</code>。</p><p>在创建表时,创建普通索引的方式如下:</p><pre><code>CREATE TABLE table_name (
....
INDEX(index_column_1,index_column_2,...)
);</code></pre><p>建表后,如果要创建普通索引,可以使用这面这条命令:</p><pre><code>CREATE INDEX index_name ON table_name(index_column_1,index_column_2,...);</code></pre><h4><strong>前缀索引</strong></h4><p><strong>前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引</strong>。前缀索引可以建立在字段类型为char、varchar、binary、varbinary的列上。</p><p><strong>使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率</strong>。</p><pre><code>CREATE TABLE table_name(
....
INDEX(column_name(length))
);</code></pre><p>建表后,如果要创建前缀索引,可以使用这面这条命令:</p><pre><code>CREATE INDEX index_name ON table_name(column_name(length));</code></pre><h3>4、按字段个数分类</h3><h4><strong>单列索引</strong></h4><p>当创建索引时,指定某一个字段作为索引列,建立在单列上的索引称为单列索引,比如主键索引。</p><h4><strong>联合索引</strong></h4><p>联合索引顾名思义,即指定多个字段列来作为索引列,同时以多个列的大小作为排序规则,即同时为多个列建立索引。</p><p>例如建立联合索引(a,b),则在创建的B+树中,记录的排序方式为:</p><ul><li>先将各个记录和页按照a列进行排序</li><li>在记录的a列相同的情况下,再采用b列进行排序</li></ul><p>在联合索引(a,b)的B+树中,存储的内容为:</p><ul><li>非叶子节点中,每条目录项记录都有a列、b列、页号3个部分组成。各条记录先按照a列的值进行排序,如果记录的a列相同,则按照b列的值进行排序</li><li>叶子节点中,数据页记录由a列、b列和主键列三部分组成;</li></ul><p><strong>使用联合索引时,存在最左匹配原则</strong>,即按照最左优先的方式进行索引的匹配。</p><p>举个例子:</p><p>当我们创建<code>联合索引(name, age)</code>时,如果你要查的是所有名字第一个字是<code>"张"</code>的人,你的SQL语句的条件是<code>where name like '张%'</code>。这时这条SQL能够用上这个联合索引。</p><p>从上述的例子可以看出,在对name字段进行单列条件查询时,同样能够使用上该联合索引,即<code>联合索引(name, age)</code>可以等效于<code>单列索引(name)</code>。</p><p>因此,在建立联合索引的时候,如何安排索引内的字段顺序呢?</p><p>评估标准是索引的复用能力。如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。例如上述的例子,通过<code>联合索引(name,age)</code>的顺序,可以让我们少维护一个<code>单列索引(name)</code>。当然<code>age</code>。</p><h2>三、索引的优缺点</h2><p>在了解了B+树的索引结构后,我们知道恰当的索引使用可以为我们带来极大的查询效率,提高性能。但是索引也并非没有缺点,了解好索引的优缺点,选择使用索引时,需要综合考虑索引的优点和缺点,才能让我们在实际使用索引的过程中得心应手。</p><h3>1、优点</h3><ul><li>提高数据检索的效率,降低数据库的I/O成本,将随机I/O转变为顺序I/O,这是创建索引最主要的原因;</li><li>通过创建唯一索引,可以保证数据库表中每一行数据的唯一性;</li><li>由于索引中记录的存储顺序,在使用分组和排序子句进行数据查询时,可以显著减少查询中分组和排序的时间,即帮助服务器避免排序和临时表,降低了CPU的消耗。</li><li>可以加速表和表之间的连接,即对于有依赖关系的子表和父表联合查询时,可以提高多表查询的速度。</li></ul><h3>2、缺点</h3><p>创建索引和维护索引要耗费时间 ,并且随着数据量的增加,所耗费的时间也会增加。</p><ul><li><strong>索引需要占磁盘空间</strong></li></ul><p>每个索引在创建后,需要占一定的物理空间存储在磁盘上,因为每建立一个索引,都要为它建立一棵B+树。每一棵B+树的每一个节点都是一个数据页。</p><p>一个数据页默认占用 16KB 的存储空间,而一棵很大的B+树由许多数据页组成,这将会占用很大的一片存储空间。</p><p>如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸。</p><ul><li><strong>虽然索引大大提高了查询速度,同时却会降低更新表的速度</strong></li></ul><p>当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,即都需要修改各个B+树索引,这样就降低了数据的维护速度。</p><p>增删改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行页面分裂、页面回收等操作,以维护节点和记录的排序。</p><ul><li><strong>影响优化器执行效率</strong></li></ul><p>在执行查询语句前,首先要生成一个执行计划。</p><p>一般情况下,一条查询语句在执行过程中最多使用一个二级索引,在生成执行计划时需要计算使用不同索引执行查询时所需的成本,最后选取成本最低的那个索引执行查询,如果此时建了太多的索引,可能会导致成本分析过程耗时太多,从而影响查询语句的执行性能。</p><h2>四、索引使用场景</h2><p>索引最大的好处是提高查询速度,但是索引使用不当则会出现上述描述的缺点所带来的影响。因此,索引不是万能钥匙,它也是根据场景来使用的。</p><p>那么在什么情况下,我们该使用索引呢?</p><h3>1、适用索引的场景</h3><ul><li><strong>字段需要唯一性限制</strong></li></ul><p>当业务中某个字段需要唯一性限制时,除了在业务逻辑层面校验,最后一道防线则是通过唯一索引来限制字段唯一。</p><ul><li>经常用于<code>WHERE</code>查询条件的字段</li></ul><p>对经常用于<code>WHERE</code>查询条件的字段建立索引,能够提高整个表的查询速度,尤其是在数据量大的情况下,创建索引就可以大幅提升数据查询的效率。</p><ul><li>经常用于<code>GROUP BY</code>和<code>ORDER BY</code>的字段</li></ul><p>对于经常用于<code>GROUP BY</code>和<code>ORDER BY</code>的字段,建立索引,利用索引其按索引字段值顺序存储数据的特性,减少排序与临时表带来的损耗。</p><ul><li>对<code>DISTINCT</code>字段创建索引</li></ul><p>有时候需要对某个字段进行去重,使用<code>DISTINCT</code>,那么对这个字段创建索引,也会提升查询效率。因为当去重字段创建了索引后是按照顺序递增的,所以在去重的时候会快很多。</p><ul><li>在多个字段都要创建索引的情况下,联合索引优于单值索引</li></ul><h3>2、不适用索引的场景</h3><ul><li><code>WHERE</code>条件、<code>GROUP BY</code>条件、<code>ORDER BY</code>条件中用不到的字段</li></ul><p>索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。</p><ul><li>表数据太少的时候,可以考虑不需要创建索引</li><li>经常更新的字段不用创建索引</li></ul><p>经常更新的字段不用创建索引,比如不要用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。</p><p>本文来自 <a href="https://link.segmentfault.com/?enc=%2FwfiUvkw3TmR%2B3TAqPwsnA%3D%3D.8adpgR0dFkRplF9DLu%2FEbeR9nMShPw5iDhmKJYVVvWnZBEzIBhSkWha95QWEvs%2Fov%2BpVyQHmFOASUJGlyC2R4RjoenAPaJi6J%2FO0CbCAcLY1jqW5wN4RSXJE6TbbE7%2FVXxPxuXx4Qe715SNop8CdeKyceqBarWAfWWLC2VwgDX%2FmIHgU8nhW0619jR8be9GXeEDBSdUUo1BHANrQADnB4jLwkcMGBXgVfiwa2vG%2BOzEOVsk5Z%2B0G%2F8NfqQBSlr5vqlNuvG%2Bpl2Nc1Q3YtTP7SmRc%2BkZTRtUwl1IrlJuOfis%3D" rel="nofollow">Go就业训练营</a> 小韬同学的投稿。</p><h2>又出成绩啦</h2><p><a href="https://link.segmentfault.com/?enc=Rv%2FBulmz10dT%2BzuCAj0%2FqQ%3D%3D.s4Zb1N5e9rIxXOUEidQlbZNIW4wDx4QIcWDHFo1P%2FPmyrp%2B7nIJPGdqkpDg2S%2BXcFKZwq8i%2BfCYrpsX4gCmg4g%3D%3D" rel="nofollow">我们又出成绩啦!大厂Offer集锦!遥遥领先!</a></p><p><a href="https://link.segmentfault.com/?enc=O9425zL1RI9vf%2Fs8tIf5Cg%3D%3D.qOQsGek3lSNek%2FfXl%2F73U9wtRVxknFATIwsOmxamkY%2B2J4oAxqn%2FwKtdP3JRm5138Jhv0hXjHPSFBHxsfeI4vA%3D%3D" rel="nofollow">这些朋友赢麻了!</a> </p><p><a href="https://link.segmentfault.com/?enc=hLYWgfWDS8qX2rLboJOdSQ%3D%3D.h0yUbO%2BQVRO6VM9J4jS6GtieJ7QKzvGM5laoABcVa82EF4p%2FKi%2FJzneX9iSBzQb5KgfvgtYIweJLGWjy34YV%2Bw%3D%3D" rel="nofollow">这是一个专注程序员升职加薪の知识星球</a></p><h2>答疑解惑</h2><p>需要「简历优化」、「就业辅导」、「职业规划」的朋友可以联系我。</p><p>加我微信:<strong>wangzhongyang1993</strong></p><p>我的文章均首发在同名公众号:<a href="https://link.segmentfault.com/?enc=ArERZL7To3epzvPLJll8qg%3D%3D.oKPc4TonqGc02N8RJsYsDsjoD614KugOKoEpJlLV3c5iELRiA%2FvTL71waRgDeWElrKPanKwjLcA7rHIQK81hbg%3D%3D" rel="nofollow">王中阳Go</a>,欢迎大家关注</p>
现代 CSS 解决方案:accent-color 强调色
https://segmentfault.com/a/1190000044660954
2024-02-27T10:25:36+08:00
2024-02-27T10:25:36+08:00
chokcoco
https://segmentfault.com/u/chokcoco
5
<p><code>accent-color</code> 是从 Chrome 93 开始被得到支持的一个不算太新属性。之前一直没有好好介绍一下这个属性。直到最近在给一些系统整体切换主题色的时候,更深入的了解了一下这个属性。</p><p>简单而言,CSS <code>accent-color</code> 支持使用几行简单的 CSS 为<strong>表单元素</strong>着色,是的,只需几行代码就可以将主题颜色应用到页面的表单输入。</p><p>表单元素一直被吐槽<a href="https://codepen.io/GeoffreyCrofte/pen/BiHzp">很难自定义</a>。而 <code>accent-color</code> 就是规范非常大的一个改变,我们开始能更多的自定义原生的表单的样式了!</p><h2>如何使用 <code>accent-color</code></h2><p>OK,我们一起来学习一下,我们应该如何使用 <code>accent-color</code>。</p><p>首先,我们来实现这么一个简单的表单界面:</p><pre><code class="HTML"><div class="wrapper">
<form action="">
<fieldset>
<legend>Accent-color Demo</legend>
<label>
Strawberries
<input type="checkbox" id="berries_1" value="strawberries">
</label>
<label>
Radio Buttons
<div>
<input type="radio" name="accented-demo" checked>
<input type="radio" name="accented-demo">
<input type="radio" name="accented-demo">
</div>
</label>
<label>
Range Slider
<input type="range">
</label>
<label>
Progress element
<progress max="100" value="50">50%</progress>
</label>
</fieldset>
</form>
</div></code></pre><p>只需要最简单的布局 CSS,与 <code>accent-color</code> 关系不大,我就不列出来了,这样,我们的 DEMO 大致如下:</p><p><img src="/img/remote/1460000044660956" alt="" title=""></p><p>可以看到,表单控件的主题颜色是<strong>蓝色</strong>,在之前,我们是没办法修改这个颜色的。</p><p>而现在,我们可以简单的借助 <code>accent-color</code>,修改表单的主题色:</p><pre><code class="CSS">:root {
accent-color: rgba(250, 15, 117);
}</code></pre><p>其中,<code>rgba(250, 15, 117)</code> 表示粉色,此时,整体的效果就变成了:</p><p><img src="/img/remote/1460000044660957" alt="" title=""></p><p>当然,这个 <code>accent-color</code> 也支持传入 CSS 变量,配合更多的其他颜色一起进行修改。</p><p>我们可以对上述的 DEMO 再简单改造:</p><pre><code class="CSS">:root {
--brand: rgba(250, 15, 117);
accent-color: var(--brand);
}
fieldset {
border: 1px solid var(--brand);
}
legend {
color: var(--brand);
}</code></pre><p>我们设置了一个 CSS 变量 <code>--brand: rgba(250, 15, 117)</code>,除了把这个颜色赋值给表单的 <code>accent-color</code>,还能赋值给其它更多元素。譬如这里,我们赋值给了 <code><fieldset></code> 的边框色和 <code><legend></code> 的文字颜色。</p><p>这样,当我们修改 CSS 变量值时,整个主题色会一起发生变化:</p><p><img src="/img/remote/1460000044660958" alt="" title=""></p><p>完整的 DEMO,你可以戳这:<a href="https://codepen.io/Chokcoco/pen/OJqGmBR">CodePen Demo -- Accent-color with custom property</a></p><p>通常而言,更多的时候,我们会将 <code>accent-color</code> 应用于:</p><ul><li><a href="https://link.segmentfault.com/?enc=mwwBpoi%2BV9tlze%2FcUNNk5Q%3D%3D.gBf5LdTYZK9GFhsj0U24TbSj6ewPzcbKP8pvTwVUvKd9JKriN8v2Bx1rg7sMzjHmZwCkOn1RrBlRBJqcQUvibA%3D%3D" rel="nofollow">checkbox</a></li><li><a href="https://link.segmentfault.com/?enc=%2Fxlt%2F3FTwLNbIJL8EO6qkA%3D%3D.a2R0SBknDGuK0MdzRUUXH0bWwzGQrzUvO9GKBti0MFziknz0jC%2B3qC2HKPkXaUdP3rk3ReH2kNQaXqKtV8diGw%3D%3D" rel="nofollow">radio</a></li><li><a href="https://link.segmentfault.com/?enc=3HnNrFgae4Wc93pNp0sTJQ%3D%3D.9Ew%2BCpzveD8y2f0T8DXTwAdRlhrlrRvQHp9hzssR4gk80D1HdLhnFW%2Fi58rdSZj2kzkfrZAB6RRLWP%2BukbyXJQ%3D%3D" rel="nofollow">range</a></li><li><a href="https://link.segmentfault.com/?enc=Ct76Al5UJU1hwKYd8dzC3g%3D%3D.bTiZkji2zwTw6jDiiVraK4kkv4UZ8Jj3Lp3Izlyph31qXUe6VHzGodTwpzsoahzZTG%2FC0jvCXz%2FtUV8GDBdLng%3D%3D" rel="nofollow">progress</a></li></ul><h2>与 <a href="https://link.segmentfault.com/?enc=q4AMUZTof%2BUCtl%2B3xhY3gw%3D%3D.032jjTr6YK36M0iq%2FariyguCTaCGYMYl%2FnrjE9s7ANeYGS8HYqydgVyU%2Fmw9gR3c" rel="nofollow">color-scheme</a> 配合使用,适配日间夜间模式</h2><p>还有一个容易忽略的细节点。<code>accent-color</code> 还支持和 <a href="https://link.segmentfault.com/?enc=P7cQ6Hwb7Ha%2FzHyehjGegg%3D%3D.UAHjBVvPZvqHvNi5FTVnrakzVf7ZIUFNDX126%2FCPFC406Ees3wMD%2FkGTP9Ua7tWx" rel="nofollow">color-scheme</a> 一起使用。</p><p>OK,什么是 color-scheme 呢?color-scheme 是 CSS 的一个属性,用于指定网页的颜色方案或主题。它定义了网页元素应该使用哪种颜色方案来呈现内容。</p><p>color-scheme 属性有以下几个可能的取值:</p><ul><li>auto:表示使用用户代理(浏览器)的默认颜色方案。这通常是浏览器自动根据操作系统或用户设置选择的方案。</li><li>light:表示使用浅色颜色方案。这通常包括浅色背景和深色文本。</li><li>dark:表示使用深色颜色方案。这通常包括深色背景和浅色文本。</li></ul><p>通过指定适当的 color-scheme 值,开发者可以为网页提供不同的颜色方案,以适应用户的偏好或操作系统的设置。这有助于提供更好的可访问性和用户体验。</p><p>譬如,我们可以将页面的 <code>color-schema</code> 设置为 <code>light dark</code>:</p><pre><code class="CSS">body {
color-scheme: light dark;
}</code></pre><p>上述代码表示页面将同时支持浅色和深色颜色方案。它告诉浏览器,网页希望适应用户代理(浏览器)的默认颜色方案,并同时支持浅色和深色模式。</p><p>当使用 <code>color-scheme: light dark</code> 时,浏览器会根据用户代理的默认颜色方案来选择适当的颜色方案。如果用户代理处于浅色模式,网页将使用浅色颜色方案来呈现内容;如果用户代理处于深色模式,网页将使用深色颜色方案来呈现内容。</p><p>此时,我们的代码可以这样改造:</p><pre><code class="CSS">:root {
--brand: rgba(250, 15, 117, 1);
accent-color: var(--brand);
}
@media (prefers-color-scheme: dark) {
:root {
--brand: rgba(3, 169, 244, 1);
}
body {
background: #000;
color: #fff;
}
}
body {
color-scheme: light dark;
}</code></pre><p>下面是我在手机上调整日间模式和夜间模式的效果图:</p><p><img src="/img/remote/1460000044660959" alt="" title=""></p><p>通过 <code>@media (prefers-color-scheme: dark) {}</code> 媒体查询,在黑夜模式下,展示不同的 <code>accent-color</code>。</p><blockquote>可能有人对 <code>@media (prefers-color-scheme: dark)</code> 还不太了解,可以看看我的这篇文章 -- <a href="https://link.segmentfault.com/?enc=VC%2FAfXs4U1NLyPXwvcD%2BBw%3D%3D.nFiyWBouLOO6e3DCyEWwTsUmTxOydqSdTvA7e6V3tmBNFB2mghhjaqkKhRNY2qIl" rel="nofollow">使用 CSS prefers-* 规范,提升网站的可访问性与健壮性</a></blockquote><p>完整的 DEMO,你可以戳这:<a href="https://codepen.io/Chokcoco/pen/OJqGmBR">CodePen Demo -- Accent-color with custom property</a></p><h2>最后</h2><p>怎样,学会了吗。并且,根据规范描述,后续 <code>accent-color</code> 将会应用于更多的元素。将未来的 CSS 中会逐渐变得更加重要。早点掌握不是坏事。</p><p>好了,本文到此结束,希望本文对你有所帮助 :)</p><p>想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- <strong>iCSS前端趣闻</strong> 😄</p><p>更多精彩 CSS 技术文章汇总在我的 <a href="https://link.segmentfault.com/?enc=vpeQgrEvEXoa3k%2FUytSlww%3D%3D.8FfSim8ShbUe8Pc3gRRSouzw48CAooOOd13SwE%2BixzIJXyVmslmhgCALSPwvN4q%2F" rel="nofollow">Github -- iCSS</a> ,持续更新,欢迎点个 star 订阅收藏。</p><p>如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。</p>
使用JavaScript手写一个简单的快捷键库
https://segmentfault.com/a/1190000044663739
2024-02-28T09:00:00+08:00
2024-02-28T09:00:00+08:00
玛尔斯通
https://segmentfault.com/u/zhouqichao
0
<h2>背景</h2><blockquote><p>前端开发中,有时项目会遇到一些快捷键需求,比如绑定快捷键,展示快捷键,编辑快捷键等需求,特别是工具类的项目。如果只是简单的绑定几个快捷键之类的需求,我们一般会通过监听键盘事件(如<code>keydown</code> 事件)来实现,如果是稍微复杂点的需求,我们一般都会通过引入第三方快捷键库来实现,比如常用的几个快捷键库<a href="https://link.segmentfault.com/?enc=tyQXi2CavmnGkmnzA%2FYJIw%3D%3D.lTSztARV8K5Uu7klXBEMxHFyX0q1SYolvueG4P%2FFpmNFm7e1fbVvrUWfM0voVv%2FO" rel="nofollow"><code>mousetrap</code></a>, <a href="https://link.segmentfault.com/?enc=Ia6ocdqquy2Tqi6ZWMuu5g%3D%3D.DXadDGUsU%2FXM23zunHHWlaqQ099WUZqMMUiFPAByyBijIBjuGqoH6BRR%2BZpPt51q" rel="nofollow"><code>hotkey-js</code></a>等。</p><p>接下来,我将会通过对快捷键库<code>mousetrap</code>第一次提交的源码进行简单分析,然后实现一个简单的快捷键库。</p></blockquote><h2>前置知识</h2><p>首先,我们需要了解一些快捷键相关的基础知识。比如,如何监听键盘事件?如何监听用户按下的按键?键盘上的按键有哪些?是如何分类的?只有知道这些,才能更好的理解<code>mousetrap</code>这种快捷键库实现的思路,才能更好地实现我们自己的快捷键库。</p><h3>如何监听键盘事件</h3><p>实现快捷键需要监听用户按下键盘按键的行为,那就需要使用到<a href="https://link.segmentfault.com/?enc=jAN6pH2t%2FWov4c8CNdVR4A%3D%3D.QtzK7DZfbKTazTllHSqDRm7aJEMkHys3RtKls%2Bt%2BNAPls%2BrsX5KezB90KBLoVcZL99%2BPLHAn%2BCiN3selpFlScA%3D%3D" rel="nofollow">键盘事件API</a>。</p><p>常用的键盘事件有<a href="https://link.segmentfault.com/?enc=knTBZ0qx9mxL9WLVZlCXJg%3D%3D.tgqv4A9NhsbTV%2FUqwMmH%2BiD7bxRS0np0vy1xwVHhoxKdHIuMge1voGmm0ze4X3WuDg3KVFS9Vgytfgzy9IniNU0bCA62wVYzbb%2Bekt%2BzvYY%3D" rel="nofollow"><code>keydown</code></a>, <a href="https://link.segmentfault.com/?enc=PXt9tfqb%2BHisIGk6itSt1Q%3D%3D.TGoqVel6c4%2FIBOHP5RPV8ghD7K%2Bji%2BwgvCww785ofgNYOnUmsYnyTTc646dYE8tpt%2FrUqvci0UAywgKjXSVIzRRaz44RiSJpSvygEPy2Et8%3D" rel="nofollow"><code>keyup</code></a>,<a href="https://link.segmentfault.com/?enc=AgzJAQuuKwjqekfxHsrQyg%3D%3D.wllcPmkRLwiF1%2F8JRhl92pA0da3GCtEqTnmkjDPusrhXpb1vKU1keRYbLIVAQiGdL%2BuglGXqsfjAuwRagoqLv9q27wXXXfOtmMsMmxU%2BEqI%3D" rel="nofollow"><code>keypress</code></a>事件。一般来说,我们会通过监听用户按下按键的行为,来判断是否要触发对应的快捷键行为。通常来说,在用户<strong>按下按键时</strong>,就会判断是否有匹配的绑定过的快捷键,即通过监听<strong><code>keydown</code></strong>事件来实现快捷键。</p><h3>如何监听键盘上按下的键</h3><p>我们可以通过键盘事件来监听用户按键行为。那如何知道用户具体按下了哪个/哪些按键呢?</p><p>比如,用户绑定的快捷键是<code>s</code>,那如何知道当前按下的按键是<code>s</code>?我们可以通过键盘事件对象<a href="https://link.segmentfault.com/?enc=LYEYnBVK4EwCZjczuFJDrA%3D%3D.clnvcNitpLhvwflV7uD%2BDUcuSKHAF9I16A%2FoPRYh%2BkgmfpATkWyTHhfM%2BHSOtY%2FjMX3o8eaem%2BZKfLWsxTKvzw%3D%3D" rel="nofollow">keyboardEvent</a>上的<a href="https://link.segmentfault.com/?enc=7nzscOUk1GiqTTY0txOxXQ%3D%3D.o5I03jtfaCDsSul9mHb2zZ6ELXiDEqtlY28SuSeHcWpr0kfU97%2BfB2xT%2FItglvaRL3zbUu6WZLiR3C8M%2FwelK9vS9jUtpBllG2EkJNlF%2BXo%3D" rel="nofollow"><code>code</code></a>, <a href="https://link.segmentfault.com/?enc=DNSx2y5JXuIit2leAUsnww%3D%3D.6MOFEj4q015VRKnal5sJ%2B5drPplw2gaxpVt4%2FZC6CG6MI6jeWwPeBYhuRH%2FMGBDxWuIh0e1ox1XAavbbAYbmbUB6GQSnkKDny63zneDIG5A%3D" rel="nofollow"><code>keyCode</code></a>, <a href="https://link.segmentfault.com/?enc=F3QWzv8NnW5EvqHav7%2BPMA%3D%3D.yIo4vII5nzjtfN2p9y%2BRU1icJW3H6SRe41Vn2YJ2rHSV64VT78yxCV3LrTHVFmTOsF4Hq%2BQ52%2BN17UdcaupPnaWgVeVMvwLgHD0M2HtIJKk%3D" rel="nofollow"><code>key</code></a>这些属性来判断用户当前按下的按键。</p><h3>键盘按键分类</h3><p>有些按键会影响其他按键按下后产生的字符。比如,用户同时按下了<code>shift</code>和<code>/</code>按键,此时产生的字符是<code>?</code>,然而实际上如果只按<code>shift</code>按键不会产生任何字符,只按<code>/</code>按键产生的字符本应该是<code>/</code>,最终产生的字符<code>?</code>就是因为同时按下了<code>shift</code>按键导致的。这里的<code>shift</code>按键就是影响其他按键按下后产生字符的按键,这种按键被称为<strong>修饰键</strong>。类似的修饰键还有<code>ctrl</code>, <code>alt</code>(<code>option</code>), <code>command</code>(<code>meta</code>)。</p><p>除了这几个修饰键以外,其他的按键称为<strong>非修饰键</strong>。</p><h3>快捷键分类</h3><p>常用的快捷键有单个键,键组合。有的还会用到键序列。</p><h4>单个键</h4><p>故名思义,单个键是只需要按下一个键就会触发的快捷键。比如常用的音视频切换播放/暂停快捷键<code>Space</code>,游戏中控制移动方向快捷键<code>w</code>,<code>a</code>,<code>s</code>,<code>d</code>等等。</p><h4>键组合</h4><p>键组合通常是一个或多个修饰键和一个非修饰键组合而成的快捷键。比如常用的复制粘贴快捷键<code>ctrl+c</code>,<code>ctrl+v</code>,保存文件快捷键<code>ctrl+s</code>,新建(浏览器或其他app)窗口快捷键<code>ctrl+shift+n</code>(<code>command+shift+n</code>)。</p><h4>键序列</h4><p>依次按下的按键称为键序列。比如键序列<code>h e l l o</code>,需要依次按下<code>h</code>,<code>e</code>,<code>l</code>,<code>l</code>,<code>o</code>按键才会触发。</p><h2>mousetrap源码分析</h2><p>以下将以<code>mousetrap</code>第一次提交的源码为基础进行简单分析,源码链接如下:<a href="https://link.segmentfault.com/?enc=YfHLMhdg6QRAZ%2Fn6%2Fb2m3g%3D%3D.F%2Bd2DWSISMZ3SXVZEUzKKJ09onulJDLx3WBQscB0tgM%3D" rel="nofollow">https://bit.ly/3TdcK8u</a></p><p>简单来说,代码只做了两件事,即<strong>绑定快捷键</strong>和<strong>监听键盘事件</strong>。</p><h3>代码设计和初始化</h3><p>首先,给<code>window</code>对象添加了一个全局属性<code>Mousetrap</code>,使用的是<a href="https://link.segmentfault.com/?enc=2MVAZ8iw9RnGKxmWh7khhQ%3D%3D.wjGCfIDk%2B7S0P0GL9CjpAa3B9hwIcCBnvZP7TSa7txvTYLVKXNxf8FLgHL8U8dUvZVksk5h6t%2FVDqvsxXv1FTQ%3D%3D" rel="nofollow">IIFE(立即执行函数表达式)</a>对代码进行封装。</p><p>该函数对外暴露了几个公共方法:</p><ul><li><code>bind(keys, callback, action)</code>: 绑定快捷键</li><li><code>trigger()</code>: 手动触发绑定的快捷键对应的回调函数。</li></ul><p>最后当<code>window</code>加载后立即执行<code>init()</code>函数,即执行初始化逻辑:添加键盘事件监听等。</p><pre><code class="js">// 以下为简化后的代码
window['Mousetrap'] = (function () {
return {
/**
* 绑定快捷键
* @param keys 快捷键,支持一次绑定多个快捷键。
* @param callback 快捷键触发后的回调函数
* @param action 行为
*/
bind: function (keys, callback, action) {
action = action || '';
_bindMultiple(keys.split(','), callback, action);
_direct_map[keys + ':' + action] = callback;
},
/**
* 手动触发快捷键对应的回调函数
* @param keys 绑定时的快捷键
* @param action 行为
*/
trigger: function (keys, action) {
_direct_map[keys + ':' + (action || '')]();
},
/**
* 给DOM对象添加事件,针对浏览器兼容性的写法
* @param object
* @param type
* @param callback
*/
addEvent: function (object, type, callback) {
_addEvent(object, type, callback);
},
init: function () {
_addEvent(document, 'keydown', _handleKeyDown);
_addEvent(document, 'keyup', _handleKeyUp);
_addEvent(window, 'focus', _resetModifiers);
},
};
})();
Mousetrap.addEvent(window, 'load', Mousetrap.init);</code></pre><h3>绑定快捷键</h3><p>一般来说,快捷键库都会提供一个绑定快捷键的函数,比如<code>bind(key, callback)</code>。在<code>mousetrap</code>中,我们可以通过调用<code>Mousetrap.bind()</code>函数来实现快捷键绑定。</p><p>我们可以结合调用时的写法对<code>Mousetrap.bind()</code>函数进行分析。比如,我们绑定了快捷键<code>ctrl+s</code>和<code>command+s</code>,如下:<code>Mousetrap.bind('ctrl+s, command+s', () => {console.log('保存成功')} )</code></p><h4>bind(keys, callback, action)</h4><p>由于<code>bind()</code>函数支持一次绑定多个快捷键(绑定时多个快捷键用逗号分隔),因此内部封装了<code>_bindMultiple()</code>函数用于处理一次绑定多个快捷键的用法。</p><pre><code class="js">window['Mousetrap'] = (function () {
return {
bind: function (keys, callback, action) {
action = action || '';
_bindMultiple(keys.split(','), callback, action);
_direct_map[keys + ':' + action] = callback;
},
};
})();</code></pre><h4>_bindMultiple(combinations, callback, action)</h4><p>该函数只是对绑定时传入的多个快捷键进行遍历,然后调用<code>_bindSingle()</code>函数依次绑定。</p><pre><code class="js">/**
* binds multiple combinations to the same callback
*/
function _bindMultiple(combinations, callback, action) {
for (var i = 0; i < combinations.length; ++i) {
_bindSingle(combinations[i], callback, action);
}
}</code></pre><h4>_bindSingle(combination, callback, action)</h4><p>该函数是实现绑定快捷键的核心代码。</p><p>主要分为以下几部分:</p><ol><li>将绑定的快捷键<code>combination</code>拆分为单个键数组,然后收集修饰键到修饰键数组<code>modifiers</code>中。</li><li>以<code>key</code>(<code>key code</code>)为属性名,将当前绑定的快捷键及其对应的回调函数等数据保存到回调函数集合<code>_callbacks</code>中。</li><li>如果之前有绑定过相同的快捷键,则调用<code>_getMatch()</code>函数移除之前绑定的快捷键。</li></ol><pre><code class="js">/**
* binds a single event
*/
function _bindSingle(combination, callback, action) {
var i,
key,
keys = combination.split('+'),
// 修饰键列表
modifiers = [];
// 收集修饰键到修饰键数组中
for (i = 0; i < keys.length; ++i) {
if (keys[i] in _MODIFIERS) {
modifiers.push(_MODIFIERS[keys[i]]);
}
// 获取当前按键(修饰键 || 特殊键 || 普通按键(a-z, 0-9))的 key code,注意这里charCodeAt()的用法
key = _MODIFIERS[keys[i]] || _MAP[keys[i]] || keys[i].toUpperCase().charCodeAt(0);
}
// 以 key code 为属性名,保存回调函数
if (!_callbacks[key]) {
_callbacks[key] = [];
}
// 如果之前有绑定过相同的快捷键,则移除之前绑定的快捷键
_getMatch(key, modifiers, action, true);
// 保存当前绑定的快捷键的回调函数/修饰键等数据到回调函数数组中
_callbacks[key].push({callback: callback, modifiers: modifiers, action: action});
}</code></pre><p>注意这里的<code>_callbacks</code>数据结构。假设绑定了以下快捷键:</p><pre><code class="js">Mousetrap.bind('s', e => {
console.log('sss')
})
Mousetrap.bind('ctrl+s', e => {
console.log('ctrl+s')
})</code></pre><p>则<code>_callbacks</code>值如下:</p><pre><code class="js">{
// key code 作为属性名,属性值为数组,用于保存当前绑定的修饰键和回调函数等数据
"83": [ // 83对应的是字符s的key code
{
modifiers: [],
callback: e => { console.log('sss') }
action: ""
},
{
modifiers: [17], // 17对应的是修饰键ctrl的key code
callback: e => { console.log('ctrl+s') }
action: ""
}
]
}</code></pre><h4>_getMatch(code, modifiers, action, remove)</h4><p>从快捷键回调函数集合<code>_callbacks</code>中获取/删除已经绑定的快捷键对应的回调函数<code>callback</code>。</p><pre><code class="js">function _getMatch(code, modifiers, action, remove) {
if (!_callbacks[code]) {
return;
}
var i,
callback;
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0; i < _callbacks[code].length; ++i) {
callback = _callbacks[code][i];
if (action == callback.action && _modifiersMatch(modifiers, callback.modifiers)) {
if (remove) {
_callbacks[code].splice(i, 1);
}
return callback;
}
}
}</code></pre><h3>监听键盘事件</h3><p>在初始化逻辑<code>init()</code>函数中给<code>document</code>对象注册了<code>keydown</code>事件监听。</p><p>⚠: <em>这里只分析<code>keydown</code>事件,<code>keyup</code>事件类似。</em></p><pre><code class="js">_addEvent(document, 'keydown', _handleKeyDown);</code></pre><h4>_handleKeyDown(e)</h4><p>首先,会调用<code>_stop(e)</code>函数判断是否需要停止执行后续操作。如果需要则直接return。</p><p>其次,根据键盘事件对象<code>event</code>获取当前按下的按键对应的<code>key code</code>,并收集当前按下的所有修饰键的<code>key code</code>到修饰键列表<code>_active_modifiers</code>中。</p><p>最后,调用<code>_fireCallback(code, modifers, action, e)</code>函数,获取当前匹配的快捷键对应的回调函数<code>callback</code>,并执行。</p><pre><code class="js">function _handleKeyDown(e) {
if (_stop(e)) {
return;
}
var code = _keyCodeFromEvent(e);
if (_MODS[code]) {
_active_modifiers.push(code);
}
return _fireCallback(code, _active_modifiers, '', e);
}</code></pre><h4>_stop(e)</h4><p>如果当前<code>keydown</code>事件触发时所在的目标元素是<code>input/select/textarea</code>元素,则停止处理<code>keydown</code>事件。</p><pre><code class="js">function _stop(e) {
var tag_name = (e.target || e.srcElement).tagName;
// stop for input, select, and textarea
return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA';
}</code></pre><h4>_keyCodeFromEvent(e)</h4><p>根据键盘事件对象<code>event</code>获取对应按键的<code>key code</code>。</p><p>注意,这里并没有直接使用<code>event.keyCode</code>。原因是有些按键在不同浏览器中的<code>event.keyCode</code>值不一致,需要进行特殊处理。</p><pre><code class="js">function _keyCodeFromEvent(e) {
var code = e.keyCode;
// right command on webkit, command on gecko
if (code == 93 || code == 224) {
code = 91;
}
return code;
}</code></pre><h4>_fireCallback(code, modifiers, action, e)</h4><p>获取当前匹配的快捷键对应的回调函数<code>callback</code>,并执行。</p><pre><code class="js">function _fireCallback(code, modifiers, action, e) {
var callback = _getMatch(code, modifiers, action);
if (callback) {
return callback.callback(e);
}
}</code></pre><h4>_getMatch(code, modifiers, action)</h4><p>获取当前匹配的快捷键对应的回调函数<code>callback</code>。</p><pre><code class="js">function _getMatch(code, modifiers, action, remove) {
if (!_callbacks[code]) {
return;
}
var i,
callback;
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0; i < _callbacks[code].length; ++i) {
callback = _callbacks[code][i];
if (action == callback.action && _modifiersMatch(modifiers, callback.modifiers)) {
if (remove) {
_callbacks[code].splice(i, 1);
}
return callback;
}
}
}</code></pre><h4>_modifiersMatch(modifiers1, modifiers2)</h4><p>判断两个修饰键数组中的元素是否完全一致。eg: <code>_modifiersMatch(['ctrl', 'shift'], ['shift', 'ctrl'])</code></p><pre><code class="js">function _modifiersMatch(group1, group2) {
return group1.sort().join(',') === group2.sort().join(',');
}</code></pre><h2>实现一个简单的快捷键库</h2><p>结合前置知识和对<code>mousetrap</code>的源码的分析,我们可以很容易实现一个简单的快捷键库。</p><h3>思路</h3><p>总体思路和<code>mousetrap</code>几乎完全一样,只做两件事。即1. 对外提供<code>bind()</code>函数用于绑定快捷键,2. 内部通过添加<code>keydown</code>事件,监听键盘输入,查找与对应快捷键匹配的回调函数<code>callback</code>并执行。</p><p>与<code>mousetrap</code>不同的是,这次将使用<code>event.key</code>属性来判断用户按下的具体按键,该属性也是规范/标准推荐使用的属性(<em>Authors SHOULD use the <code>key</code> attribute instead of the <code>charCode</code> and <code>keyCode</code> attributes.</em>)。</p><p>代码将使用ES6 class 语法,对外提供<code>bind()</code>函数用于绑定快捷键。</p><h3>功能</h3><p>支持绑定快捷键(单个键,键组合)。</p><h3>实现</h3><p>由于实现思路前文已经分析过,因此这里就不详细解释了,以下直接给出完整的源代码。</p><p>不过,代码有几点需要注意下:</p><ol><li><code>event.key</code>受<code>shift</code>按键影响。比如,绑定的快捷键是<code>shift+/</code>,实际上在<code>keydown</code>事件对象<code>event</code>中<code>event.key</code>的值是<code>?</code>,因此代码里维护了这种特殊字符的映射<code>_SHIFT_MAP</code>,用于判断用户是否按下了这类特殊字符。</li><li>有些特殊字符按键产生的字符(<code>event.key</code>)需要特殊处理,比如空格按键<code>Space</code>,按下后实际产生的字符(<code>event.key</code>)是<code>' '</code>,详情见代码中的<code>checkKeyMatch()</code>函数。</li></ol><pre><code class="js">/**
* this is a mapping of keys that converts characters generated by pressing shift key
* at the same time to characters produced when the shift key is not pressed
*
* @type {Object}
*/
var _SHIFT_MAP = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
$: '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
_: '-',
'+': '=',
':': ';',
'"': "'",
'<': ',',
'>': '.',
'?': '/',
'|': '\\',
};
/**
* get modifer key list by keyboard event
* @param {KeyboardEvent} event - keyboard event
* @returns {Array}
*/
const getModifierKeysByKeyboardEvent = (event) => {
const modifiers = [];
if (event.shiftKey) {
modifiers.push('shift');
}
if (event.altKey) {
modifiers.push('alt');
}
if (event.ctrlKey) {
modifiers.push('ctrl');
}
if (event.metaKey) {
modifiers.push('command');
}
return modifiers;
};
/**
* get non modifier key
* @param {string} shortcut
* @returns {string}
*/
function getNonModifierKeyByShortcut(shortcut) {
if (typeof shortcut !== 'string') return '';
if (!shortcut.trim()) return '';
const validModifierKeys = ['shift', 'ctrl', 'alt', 'command'];
return (
shortcut.split('+').filter((key) => !validModifierKeys.includes(key))[0] ||
''
);
}
/**
* check if two modifiers match
* @param {Array} modifers1
* @param {Array} modifers2
* @returns {boolean}
*/
function checkModifiersMatch(modifers1, modifers2) {
return modifers1.sort().join(',') === modifers2.sort().join(',');
}
/**
* check if key match
* @param {string} shortcutKey - shortcut key
* @param {string} eventKey - event.key
* @returns {boolean}
*/
function checkKeyMatch(shortcutKey, eventKey) {
if (shortcutKey === 'space') {
return eventKey === ' ';
}
return shortcutKey === (_SHIFT_MAP[eventKey] || eventKey);
}
/**
* shortcut binder class
*/
class ShortcutBinder {
constructor() {
/**
* shortcut list
*/
this.shortcuts = [];
this.init();
}
/**
* init, add keyboard event listener
*/
init() {
this._addKeydownEvent();
}
/**
* add keydown event
*/
_addKeydownEvent() {
document.addEventListener('keydown', (event) => {
const modifers = getModifierKeysByKeyboardEvent(event);
const matchedShortcut = this.shortcuts.find(
(shortcut) =>
checkKeyMatch(shortcut.key, event.key.toLowerCase()) &&
checkModifiersMatch(shortcut.modifiers, modifers)
);
if (matchedShortcut) {
matchedShortcut.callback(event);
}
});
}
/**
* bind shortcut & callback
* @param {string} shortcut
* @param {Function} callback
*/
bind(shortcut, callback) {
this._addShortcut(shortcut, callback);
}
/**
* add shortcut & callback to shortcut list
* @param {string} shortcut
* @param {Function} callback
*/
_addShortcut(shortcut, callback) {
this.shortcuts.push({
shortcut,
callback,
key: this._getKeyByShortcut(shortcut),
modifiers: this._getModifiersByShortcut(shortcut),
});
}
/**
* get key (character/name) by shortcut
* @param {string} shortcut
* @returns {string}
*/
_getKeyByShortcut(shortcut) {
const key = getNonModifierKeyByShortcut(shortcut);
return key.toLowerCase();
}
/**
* get modifier keys by shortcut
* @param {string} shortcut
* @returns {Array}
*/
_getModifiersByShortcut(shortcut) {
const keys = shortcut.split('+').map((key) => key.trim());
const VALID_MODIFIERS = ['shift', 'ctrl', 'alt', 'command'];
let modifiers = [];
keys.forEach((key) => {
if (VALID_MODIFIERS.includes(key)) {
modifiers.push(key);
}
});
return modifiers;
}
}</code></pre><h3>调用</h3><p>调用方法和<code>mousetrap</code>类似。以下仅列出部分测试代码,可以查看在线示例测试实际效果。</p><pre><code class="js">shortcutBinder.bind('ctrl+s', () => {
console.log('ctrl+s');
});
shortcutBinder.bind('ctrl+shift+s', () => {
console.log('ctrl+shift+s');
});
shortcutBinder.bind('space', (e) => {
e.preventDefault();
console.log('space');
});
shortcutBinder.bind('shift+5', (e) => {
e.preventDefault();
console.log('shift+5');
});
shortcutBinder.bind(`shift+\\`, (e) => {
e.preventDefault();
console.log('shift+\\');
});
shortcutBinder.bind(`f2`, (e) => {
e.preventDefault();
console.log('f2');
});</code></pre><h3>在线示例</h3><p><a href="https://codepen.io/Tom_chao/pen/jOJRoqr">CodePen: 手写一个简单的快捷键库</a></p><h3>TODO</h3><p>至此,我们已经实现了一个简单的快捷键库,可以满足常见的快捷键绑定相关的业务需求。当然,相对当前流行的几个快捷键库而言,我们实现的快捷键库比较简单,还有很多功能和细节有待实现和完善。以下列出待完成的几个事项,感兴趣的可以尝试实现下。</p><ul><li>支持设置键序列快捷键</li><li>支持设置快捷键作用域</li><li>支持解绑单个快捷键</li><li>支持重置所有绑定的快捷键</li><li>支持获取所有绑定的快捷键信息</li></ul><h2>总结</h2><p>通过学习<code>mousetrap</code>源码以及手写一个简单的快捷键库,我们可以学习到一些关于快捷键和键盘事件相关的知识。目的不是重复造轮子,而是通过日常业务需求,驱动我们去了解当前流行的常见快捷键库的实现思路,以便于我们更好地理解并实现相关业务需求。假如日后有展示、修改快捷键或者其他快捷键相关的需求,我们就可以做到胸有成竹,举一反三。</p>
使用原生 cookieStore 方法,让 Cookie 操作更简单
https://segmentfault.com/a/1190000044663121
2024-02-27T18:43:40+08:00
2024-02-27T18:43:40+08:00
南玖
https://segmentfault.com/u/fenanjiu
6
<h2>前言</h2><p>对于前端来讲,我们在操作<code>cookie</code>时往往都是基于<code>document.cookie</code>,但它有一个缺点就是操作复杂,它并没有像<code>localStorage</code>那样提供一些<code>get</code>或<code>set</code>等方法供我们使用。对与cookie的操作一切都是基于字符串来进行的。为了让<code>cookie</code>的操作更简便, Chrome87率先引入了<code>cookieStore</code>方法。</p><h2>document.cookie</h2><p><code>document.cookie</code>可以获取并设置当前文档关联的<code>cookie</code></p><h3>获取cookie</h3><pre><code class="js">const cookie = document.cookie</code></pre><p>在上面的代码中,<code>cookie</code> 被赋值为一个字符串,该字符串包含所有的 Cookie,每条 cookie 以"分号和空格 (; )"分隔 (即, <code>key=value</code> 键值对)。</p><p><img src="/img/remote/1460000044663123" alt="cookie-1.png" title="cookie-1.png"></p><p>但这拿到的是一整个字符串,如果你想获取cookie中的某一个字段,还需要自己处理</p><pre><code class="js">const converter = {
read: function (value) {
if (value[0] === '"') {
value = value.slice(1, -1);
}
return value.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent)
},
write: function (value) {
return encodeURIComponent(value).replace(
/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,
decodeURIComponent
)
}
}
function getCookie (key) {
const cookies = document.cookie ? document.cookie.split('; ') : [];
const jar = {};
for (let i = 0; i < cookies.length; i++) {
const parts = cookies[i].split('=');
const value = parts.slice(1).join('=');
try {
const foundKey = decodeURIComponent(parts[0]);
jar[foundKey] = converter.read(value, foundKey);
if (key === foundKey) {
break
}
} catch (e) {}
}
return key ? jar[key] : jar
}
console.log(getCookie('name')) // 前端南玖</code></pre><p>比如上面这段代码就是用来获取单个<code>cookie</code>值的</p><h3>设置cookie</h3><pre><code class="js">document.cookie = `name=前端南玖;`</code></pre><p>它的值是一个键值对形式的字符串。需要注意的是,<strong>用这个方法一次只能对一个 cookie 进行设置或更新</strong>。</p><p>比如:</p><pre><code class="js">document.cookie = `age=18; city=shanghai;`</code></pre><p>这样只有<code>age</code>能够设置成功</p><ul><li><p>以下可选的 cookie 属性值可以跟在键值对后,用来具体化对 cookie 的设定/更新,使用分号以作分隔:</p><ul><li><code>;path=path</code> (例如 '/', '/mydir') 如果没有定义,默认为当前文档位置的路径。</li><li><code>;domain=domain</code> (例如 'example.com', 'subdomain.example.com') 如果没有定义,默认为当前文档位置的路径的域名部分。与早期规范相反的是,在域名前面加 . 符将会被忽视,因为浏览器也许会拒绝设置这样的 cookie。如果指定了一个域,那么子域也包含在内。</li><li><code>;max-age=max-age-in-seconds</code> (例如一年为 60<em>60</em>24*365)</li><li><p>;expires=date-in-GMTString-format</p><p>如果没有定义,cookie 会在对话结束时过期</p><ul><li>这个值的格式参见<a href="https://link.segmentfault.com/?enc=0is51U157Ot0NkAaddUJYQ%3D%3D.BCgm8F0kK82I8awS4%2BUUdjDH6qMZ2ayyWGAn7gap%2BptLIg1yHnty5LbMt5rFFHqizXMCJR%2FkuuInftcEbJEd%2Baue%2F%2FB6rVJeMZxHoSRZh1EZGVrjKqsK%2FmDj678DsF4x1F30eRp4ln%2FJOxbrWVBd3g%3D%3D" rel="nofollow">Date.toUTCString() (en-US)</a></li></ul></li><li><code>;secure</code> (cookie 只通过 https 协议传输)</li></ul></li><li>cookie 的值字符串可以用<a href="https://link.segmentfault.com/?enc=zTARvklkac1958FH9L1I5w%3D%3D.1y%2FIASf1BoLiINoWD6KU3p32XbxGU6eaDNS9TLnFkRH1pmbXdsfpiycOFgJJOVr3%2BCkplp7UJOm7VLgDuts4V2gtgrJW74dQKj4E8hYY1jWWQEgmKFDkmj%2BcRHn9DEMOB3dQ88558cRL6eAE9NcNwg%3D%3D" rel="nofollow">encodeURIComponent() (en-US)</a>来保证它不包含任何逗号、分号或空格 (cookie 值中禁止使用这些值).</li></ul><pre><code class="js">function assign (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
target[key] = source[key];
}
}
return target
}
function setCookie (key, value, attributes) {
if (typeof document === 'undefined') {
return
}
attributes = assign({}, { path: '/' }, attributes);
if (typeof attributes.expires === 'number') {
attributes.expires = new Date(Date.now() + attributes.expires * 864e5);
}
if (attributes.expires) {
attributes.expires = attributes.expires.toUTCString();
}
key = encodeURIComponent(key)
.replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)
.replace(/[()]/g, escape);
var stringifiedAttributes = '';
for (var attributeName in attributes) {
if (!attributes[attributeName]) {
continue
}
stringifiedAttributes += '; ' + attributeName;
if (attributes[attributeName] === true) {
continue
}
stringifiedAttributes += '=' + attributes[attributeName].split(';')[0];
}
return (document.cookie =
key + '=' + converter.write(value, key) + stringifiedAttributes)
}
setCookie('course', 'fe', { expires: 365 })</code></pre><p>这里是<code>js-cookie</code>库对<code>setCookie</code>方法的封装</p><h3>删除cookie</h3><pre><code class="js">function removeCookie (key, attributes) {
setCookie(
key,
'',
assign({}, attributes, {
expires: -1
})
);
}
removeCookie('course')</code></pre><h2>新方法cookieStore</h2><p>以上就是通过<code>document.cookie</code>来操作<code>cookie</code>的方法,未封装方法之前操作起来都非常的不方便。现在我们再来了解一下新方法<code>cookieStore</code>,它是一个类似<code>localStorage</code>的全局对象。</p><p><img src="/img/remote/1460000044663124" alt="cookie-2.png" title="cookie-2.png"></p><p>它提供了一些方法可以让我们更加方便的操作<code>cookie</code></p><h3>获取单个cookie</h3><pre><code class="js">cookieStore.get(name)</code></pre><p>该方法可以获取对应<code>key</code>的单个cookie,并且以<code>promise</code>形式返回对应的值</p><pre><code class="js">async function getCookie (key) {
const name = await cookieStore.get(key)
console.log('【name】', name)
}
getCookie('name')</code></pre><p><img src="/img/remote/1460000044663125" alt="cookie-3.png" title="cookie-3.png"></p><p>当获取的<code>cookie</code>不存在时,则会返回<code>null</code></p><h3>获取所有cookie</h3><pre><code class="js">cookieStore.getAll()</code></pre><p>该方法可以获取所有匹配的<code>cookie</code>,并且以<code>promise</code>形式返回一个列表</p><pre><code class="js">async function getAllCookies () {
const cookies = await cookieStore.getAll()
console.log('【cookies】', cookies)
}
getAllCookies()</code></pre><p><img src="/img/remote/1460000044663126" alt="cookie-4.png" title="cookie-4.png"></p><p>当<code>cookie</code>不存在时,则会返回一个空数组</p><h3>设置cookie</h3><pre><code class="js">cookieStore.set()</code></pre><p>该方法可以设置cookie,并且会返回一个promise状态,表示是否设置成功</p><pre><code class="js">function setCookie (key, value) {
cookieStore.set(key, value).then(res => {
console.log('设置成功')
}).catch(err => {
console.log('设置失败')
})
}
setCookie('site', 'https://bettersong.github.io/nanjiu/')</code></pre><p>如果想要设置更多的属性,比如:过期时间、路径、域名等,可以传入一个对象</p><pre><code class="js">function setCookie (key, value) {
cookieStore.set({
name: key,
value: value,
path: '/',
expires: new Date(2024, 2, 1)
}).then(res => {
console.log('设置成功')
}).catch(err => {
console.log('设置失败')
})
}
setCookie('site', 'https://bettersong.github.io/nanjiu/')</code></pre><p><img src="/img/remote/1460000044663127" alt="cookie-5.png" title="cookie-5.png"></p><h3>删除cookie</h3><pre><code class="js">cookieStore.delete(name)</code></pre><p>该方法可以用来删除指定的cookie,同样会返回一个promise状态,来表示是否删除成功</p><pre><code class="js">function removeCookie (key) {
cookieStore.delete(key).then(res => {
console.log('删除成功')
}).catch(err => {
console.log('删除失败')
})
}
removeCookie('site')</code></pre><p><img src="/img/remote/1460000044663128" alt="cookie-6.png" title="cookie-6.png"></p><p><strong>需要注意的是:即使删除一个不存在的cookie也会返回删除成功状态</strong></p><h3>监听cookie</h3><pre><code class="js">cookieStore.addEventListener('change', (event) => {
console.log(event)
});</code></pre><p><img src="/img/remote/1460000044663129" alt="cookie-7.png" title="cookie-7.png"></p><p>可以通过<code>change</code>事件来监听cookie的变化,无论是通过<code>cookieStore</code>操作的,还是通过<code>document.cookie</code>来操作的都能够监听到。</p><p>该方法的返回值有两个字段比较重要,分别是:<code>change</code>盒<code>delete</code>,它们都是数组类型。用来存放改变和删除的cookie信息</p><h4>监听修改</h4><p>调用<code>set</code>方法时,会触发<code>change</code>事件,修改或设置的cookie会存放在<code>change</code>数组中</p><pre><code class="js">cookieStore.addEventListener('change', (event) => {
const type = event.changed.length ? 'change' : 'delete';
const data = (event.changed.length ? event.changed : event.deleted).map((item) => item.name);
console.log(`【${type}】, cookie:${JSON.stringify(data)}`);
});
function setCookie (key, value) {
cookieStore.set(key, value).then(res => {
console.log('设置成功')
}).catch(err => {
console.log('设置失败')
})
}
setCookie('site', 'https://bettersong.github.io/nanjiu/')</code></pre><p><img src="/img/remote/1460000044663130" alt="cookie-8.png" title="cookie-8.png"></p><p>⚠️需要注意的是:</p><ul><li>通过<code>document.cookie</code>设置或删除cookie时,都是会触发<code>change</code>事件,不会触发<code>delete</code>事件</li><li>即使两次设置cookie的<code>name</code>和<code>value</code>都相同,也会触发<code>change</code>事件</li></ul><h4>监听删除</h4><p>调用<code>delete</code>方法时,会触发<code>change</code>事件,删除的cookie会存放在<code>delete</code>数组中</p><pre><code class="js">cookieStore.addEventListener('change', (event) => {
const type = event.changed.length ? 'change' : 'delete';
const data = (event.changed.length ? event.changed : event.deleted).map((item) => item.name);
console.log(`【${type}】, cookie:${JSON.stringify(data)}`);
});
function removeCookie (key) {
cookieStore.delete(key).then(res => {
console.log('删除成功')
}).catch(err => {
console.log('删除失败')
})
}
removeCookie('site')</code></pre><p><img src="/img/remote/1460000044663132" alt="cokie-9.png" title="cokie-9.png"></p><p>⚠️需要注意的是:</p><ul><li>如果删除一个不存在的cookie,则不会触发<code>change</code>事件</li></ul><h3>兼容性</h3><p>在使用该方法时需要注意浏览器的兼容性</p><p><img src="/img/remote/1460000044663133" alt="cookie-10.png" title="cookie-10.png"></p><h2>总结</h2><p><code>cookieStore</code>提供的方法比起直接操作<code>document.cookie</code>要简便许多,不仅支持增删改查,还支持通过change事件来监听cookie的变化,但是在使用过程需要注意兼容性问题。</p>
CSS图像边框:Interop 2023的一个重点领域
https://segmentfault.com/a/1190000044660698
2024-02-27T09:41:07+08:00
2024-02-27T09:41:07+08:00
南城FE
https://segmentfault.com/u/nanchengfe
0
<blockquote>本文翻译自 <a href="https://link.segmentfault.com/?enc=N3F%2B3HLm0jqAPMrIpXdRAA%3D%3D.eb9FNWn66I0QpiIZQIBrcUnL%2FvHsRphJ%2B0y4x3eJGXP47w6vFFZEKPTRb68k2wGpVkTO8FLK8V5DDoXGG4NfUe1lJ3UBpKWi7zYsPcgMIxCzHmd6cwdvJygPLssibzpy" rel="nofollow">Border images in CSS: A key focus area for Interop 2023</a>,作者:Dipika Bhattacharya, 略有删改。</blockquote><p>Interop 2023是一项提高Web的互操作性为目标,以达到每种技术在各浏览器中完全相同的状态。(来源:Interop 2023)</p><p>在Interop 2023中,CSS图像边框被确定为一个关键的重点领域。这个特性允许您使用图像来设置元素边框的样式,浏览器已经支持这个特性很多年了。然而浏览器之间的行为差异导致Web开发人员不愿意完全使用此功能。随着Interop 2023中包含图像边框,重新承诺解决行为差异并鼓励广泛采用。这一举措强调了能够创建在不同浏览器之间保持一致的视觉吸引力的网页设计的重要性。</p><p>图像边框的每个方面都可以使用特定的<code>border-image</code>CSS属性进行控制,这些都在MDN的参考页面上进行了详细的解释。在这篇文章中,我们将提供与边框图像相关的所有属性的概述,并探索如何在边框中自定义图像。</p><h3>开始使用边框图像</h3><p>CSS中的边框图像允许您使用自定义图像作为网站上元素的边框,取代标准边框。这为您提供了一种独特而强大的方式来设计和添加您的个人创意风格到您的网站。例如,想象一下你正在经营一家在线花店。您可以使用花卉或自然图像作为网站上各种元素的边界,以增加整个网站的主题风格。</p><p>本文总结了使用图像作为元素边框的所有步骤。第一步是指定您喜欢的图像的来源。然后将图像切片以指定要在边框中使用的部分。接下来调整图像的宽度,这将控制图像在边框区域内的缩放方式。如果您希望图像延伸到元素的边界之外,则可以选择定义起点。最后决定图像如何在边框周围拟合或重复。这将定义图像是否重复、拉伸或调整以适应边界区域。通过遵循这些步骤,您可以有效地使用自定义图像作为网页设计中的元素边框。</p><p>我们将使用下面的自然主题图像(由pixabay提供)来演示如何将其用作边框图像。</p><p><img src="/img/remote/1460000044660700" alt="" title=""></p><p>作为参考,下面的框表示我们要添加边框图像的元素。厚的绿色边界区域是我们将用图像替换的区域。浅黄色背景色表示内容框和填充框。</p><p><img src="/img/remote/1460000044660701" alt="" title=""></p><h3>指定边框图像</h3><p>您可以使用<code>border-image-source</code>属性指定边框图像的源。与<code>background-image</code>类似,此属性接受图像文件的URL或渐变,并将其应用于框的边框。您可以使用各种图像格式,如PNG,JPG或SVG。</p><pre><code class="css">.box {
border: 30px solid transparent;
border-image-source: url("https://developer.mozilla.org/en-US/blog/border-images-interop-2023/nature.png");
}</code></pre><p>在这个阶段,您会注意到图像只出现在方框的四角。不是我们所期待的,这只是第一步,离定制边框中图像的外观还需要几个步骤。图像需要使用其他<code>border-image</code>属性值进行进一步处理,以呈现最终的边框外观,现在我们缺少有关如何在边框上切片或分布图像的说明。</p><p><img src="/img/remote/1460000044660702" alt="" title=""></p><p>在上面的代码中你会注意到<code>border-width</code>和<code>border-style</code>都是使用<code>border</code>快捷方式属性定义的。这是因为<code>border-image</code>属性只有在元素具有定义的边框时才可见。<code>border-width</code>属性设置边框图像的可用空间,<code>border-style</code>属性确保边框图像正确显示。如果没有<code>border-width</code>和<code>border-style</code>,则无论您设置的<code>border-image</code>属性如何,边框图像都不会显示。</p><h3>图像切片</h3><p>切片可以帮助我们定义图像的各个部分,这些部分将显示在元素边框的角落和侧面。这就像把蛋糕切成片,每一块都有它的位置。</p><p>您可以使用<code>border-image-slice</code>属性对图像进行切片。此属性使用四条假想线对图像进行切片,这四条假想线与相应的边缘相距指定的切片距离。四条切片线将图像有效地划分为九个区域:四个角、四个边缘和中间。这些线确定将用于边框的图像区域的大小。</p><p>例如,所有边的切片值为30,切片区域将如下图所示:</p><p><img src="/img/remote/1460000044660703" alt="" title=""></p><p>使用30的切片值上面显示的切片区域没有从图像中捕获足够的部分,至少不是我们真正想要在边界中显示的部分。大部分的叶子和花都被剪掉了。在下面的代码中,让我们使用一个更高的值,比如70,对图像进行切片,看看效果如何。</p><pre><code class="css">.box {
border: 30px solid transparent;
border-image-source: url("https://developer.mozilla.org/en-US/blog/border-images-interop-2023/nature.png");
border-image-slice: 70;
}</code></pre><p>这是看起来的效果要更好。正如你所看到的,这一步的结果将取决于你使用的图像。所以一定要探索不同的切片设置。</p><p><img src="/img/remote/1460000044660704" alt="" title=""></p><p>你可以使用<code>border-image-slice</code>值指定另一个选项。默认情况下,切片操作会丢弃图像的中间部分。如果你想保留它,你可以在值上加上<code>fill</code>,就像这样<code>border-image-slice: 70 fill</code>。这也将在中间区域的背景上绘制边框图像,此时的边框图像效果就像背景图一样了。</p><h3>调整宽度</h3><p>在下一步中我们调整图像边框的款度。这将确定边框图像在边界区域内的缩放方式。我们使用<code>border-image-width</code>属性设置边框图像的宽度。它定义图像边框从边界边缘向内的偏移。</p><p>你可以从下面的图片中看到设置宽度为10px的效果。在这个宽度上,边框中的图像变得非常拉伸。</p><p><img src="/img/remote/1460000044660705" alt="" title=""></p><p>让我们尝试将图像的宽度增加到30px。</p><pre><code class="css">.box {
border: 30px solid transparent;
border-image-source: url("https://developer.mozilla.org/en-US/blog/border-images-interop-2023/nature.png");
border-image-slice: 70;
border-image-width: 30px;
}</code></pre><p>在这个宽度下,边框图像看起来更好。</p><p><img src="/img/remote/1460000044660706" alt="" title=""></p><p>让我们在这里花点时间来确保我们理解了<code>border-image-width</code>和<code>border-width</code>属性的用途。虽然<code>border-image-width</code>属性定义了边框图像的宽度或厚度,但<code>border-width</code>属性定义了元素周围边框的宽度。所以<code>border-image-width</code>决定了如何在这个分配的边界区域内缩放图像边框。考虑以下场景:</p><ul><li>如果<code>border-image-width</code>大于<code>border-width</code>,则边框图像将超出填充,甚至可能超出内容框边缘。</li><li>如果<code>border-image-width</code>小于<code>border-width</code>,则图像可能无法填充整个边界区域。图像可能会出现扭曲或缩小,因为它被挤压到一个更小的空间。</li></ul><h3>将图像扩展到边界之外</h3><p>有时你可能希望边框图像延伸到元素的边框框之外,就像为设计添加更多深度一样。这就是<code>border-image-outset</code>属性发挥作用的地方。虽然指定属性的顺序并不严格,但<code>border-image-outset</code>通常在<code>border-image-width</code>属性之后指定。</p><p>下面是开始值10px(左侧)和20px(右侧)的并排比较。这些图像中的边框已高亮显示,以演示边框图像在边框区域外的扩展。</p><p><img src="/img/remote/1460000044660707" alt="" title=""></p><p>继续我们的示例,让我们使用值10px作为<code>border-image-outset</code>。</p><pre><code class="css">.box {
border-width: 30px;
border-style: solid;
border-image-source: url("https://developer.mozilla.org/en-US/blog/border-images-interop-2023/nature.png");
border-image-slice: 70;
border-image-width: 30px;
border-image-outset: 10px;
}</code></pre><p>基于上面的代码,边框图像将以30px的宽度显示,该宽度在元素的边框区域内,由<code>border-width</code>定义。<code>border-image-outset</code>值指定边框图像将在边框框外扩展10px。此扩展是对<code>border-image-width</code>指定的宽度的补充。边框及其起点所占的总宽度为40px(边框为30px,起点为额外的10px)。</p><h3>布局控制</h3><p>我们现在非常接近于在边界中获得图像的最终外观。由于我们使用的特定图像以及我们为切片和宽度指定的值,该元素的边框已经与最终所需的外观相似。但我们还可以为最终的布局调整。在这最后一步中,您将定义图像的切片部分如何围绕边框进行布局,也就是说它们是应该重复、拉伸还是调整以适合边框区域。<code>border-image-repeat</code>属性可以帮助您解决这个问题。这个属性通常在<code>border-image</code>属性中最后指定,因为它处理元素中边框图像的最终布局。</p><ul><li>若要使图像在边框周围重复,使其完全贴合而不剪裁,请使用值round。图像部分可能会被拉伸以获得适当的适合度。</li><li>要添加额外的空间而不是拉伸以获得适当的配合,请使用值space。</li><li>若要拉伸图像以填充边框区域而不重复图像,请使用值stretch。</li><li>值repeat将在整个边界上重复图像,如果它不完全适合边界区域,则可能会剪切它。</li></ul><p>您甚至可以通过为<code>border-image-repeat</code>指定两个值来为水平(顶部和底部)和垂直(左侧和右侧)侧定义不同的布局和缩放。作为一个练习,尝试探索对所有边使用相同值与对水平边和垂直边使用不同值之间的外观差异。</p><p>让我们在这里的例子中使用round来完美地适应图像的边界。</p><pre><code class="css">.box {
border: 30px solid transparent;
border-image-source: url("https://developer.mozilla.org/en-US/blog/border-images-interop-2023/nature.png");
border-image-slice: 70;
border-image-width: 30px;
border-image-outset: 10px;
border-image-repeat: round;
}</code></pre><p>当我们给元素设置了更高的高度,边框图像也不会被拉伸表现的很自然。</p><p><img src="/img/remote/1460000044660708" alt="" title=""></p><h3>使用border-image 简写属性</h3><p>我们可以使用简写<code>border-image</code>属性一次性设置所有这些属性。</p><p>下面的代码使用简写<code>border-image</code>属性来同时设置多个边框图像属性,包括<code>border-image-source、border-image-slice、border-image-width、border-image-outset和border-image-repeat</code>。任何未指定的值都将被设置为属性的初始值。</p><pre><code class="css">.box {
border: 30px solid transparent;
border-image: url("https://developer.mozilla.org/en-US/blog/border-images-interop-2023/nature.png")
70 / 30px / 10px round;
}</code></pre><h3>Interop 2023及以后</h3><p>作为Interop 2023对CSS边框图像的关注的一部分,正在做出重大努力来增强跨浏览器兼容性并标准化行为。浏览器开发人员承诺实现一致的边框图像行为,我们希望这篇文章能激发或更新您在未来Web项目中探索边框图像的兴趣。</p><hr><p><strong>看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~</strong></p><p>专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)</p>
团队协作如何确保项目Node版本的一致性?
https://segmentfault.com/a/1190000044655501
2024-02-24T21:15:44+08:00
2024-02-24T21:15:44+08:00
南玖
https://segmentfault.com/u/fenanjiu
12
<h2>前言</h2><p>想必大家在工作过程中都遇到过<code>node</code>版本带来的各种各样的问题,对于团队协作项目,你不能保证所有人的本地<code>node</code>版本都相同,所以在项目文档中往往会写上以下内容:</p><ul><li>为与线上环境一致,请保证以下版本</li><li>node:15.x.x</li><li>vue-cli:4.4.x</li></ul><p>但这样并不能完全避免问题,比如多个不同项目中使用的node版本也有所不同,在来回切换中就可能造成node版本混用,那么应该如何避免这个问题?</p><p><strong>如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,文章公众号首发,关注 <code>前端南玖</code> 第一时间获取最新文章~</strong></p><h2>package.json</h2><p>对于前端工程化项目,根目录下都会有一个<code>package.json</code>文件,在该文件中有一个属性<strong>engines</strong>,它表示声明node环境,如果不指定版本(或者指定<code>*</code>作为版本) ,那么任何版本的node都可以。</p><pre><code>"engines": {
"node": ">=15.0.0"
}</code></pre><p>比如这里指定<code>node</code>版本必须大于等于15。</p><p><strong>了解更多<code>package.json</code>内容,可以查看这篇文章:<a href="https://link.segmentfault.com/?enc=C09dsYfRsJN1KLqlyGwdUw%3D%3D.3kWj%2BhgauFRkGcR42HcyeLZgPmDwhL0JiesUJaBR76WjPYP0RuOnt6ByoUE1iSix" rel="nofollow">熟悉又陌生的package.json</a></strong></p><p>但对于 <code>npm</code> 来讲,但即使许多项目定义了最低 Node.js 版本,此配置也不会强制执行,也就是说它并不会阻止用户的安装操作。</p><h2>npm</h2><p>比如node版本限制了大于等于15,而我使用14.19.3的版本来安装依赖</p><p><img src="/img/remote/1460000044655503" alt="node-2.png" title="node-2.png"></p><p>你会发现这样还是能够正常安装,并没有按我们的期待阻止用户安装依赖。</p><h2>yarn</h2><p>同样的配置我们再来试试<code>yarn</code>的表现是怎样的?</p><p><img src="/img/remote/1460000044655504" alt="node-3.png" title="node-3.png"></p><p>可以看到同样的配置,yarn的表现是我们想要的效果。如果我们就是想用<code>npm</code>,能否到达同样的效果?</p><h2>.npmrc</h2><p>对于<code>npm</code>我们需要在项目根目录下新增<code>.npmrc</code>文件,并且显示启用严格的node引擎处理,如果一个项目包含一个<code>.npmrc</code>定义严格的引擎,那么执行<code>npm install</code>时如果 <code>Node</code> 版本不满足版本要求,安装依赖就会失败。</p><pre><code>// .npmrc
engine-strict=true</code></pre><p><img src="/img/remote/1460000044655505" alt="node-4.png" title="node-4.png"></p>
查漏补缺,盘点和toggle相关的几个API
https://segmentfault.com/a/1190000044654462
2024-02-26T10:00:00+08:00
2024-02-26T10:00:00+08:00
XboxYan
https://segmentfault.com/u/xboxyan
1
<blockquote>欢迎关注我的公众号:<strong>前端侦探</strong></blockquote><p><code>toggle</code>的意思很简单,表示“切换”,适用于两个状态之间的变化,不会出现第三者,就像这样</p><p><img src="/img/remote/1460000044654465" alt="CSS & JavaScript Toggle Button | Dark and Light Mode" title="CSS & JavaScript Toggle Button | Dark and Light Mode"></p><p><code>web</code> 中也有很多类似的<code>api</code>,一起看看有哪些吧</p><h2>一、toggle</h2><p>首先是最常用的<code>DOMTokenList.toggle</code>方法,这里的的<code>DOMTokenList</code>表示一组空格分隔的标记,最常见的就是<code>Element.classList</code>,比如</p><blockquote>除了<code>classList</code>还有<code>relList</code>,不过应该更少见了</blockquote><pre><code class="html"><div class="a b c"></div></code></pre><p>通过<code>el.classList</code>可以获取到 <code>class</code> 的详细信息</p><p><img src="/img/remote/1460000044654466" alt="image-20240220191322449" title="image-20240220191322449"></p><p>看着像一个数组一样,然后我们可以通过<code>toggle</code>方法去切换某个<code>class</code>,比如</p><pre><code class="js">el.classList.toggle('a'); // 移除 a
el.classList.toggle('a'); // 添加 a</code></pre><p>此时会动态去判断,如果存在就移除,如果不存在就添加,再也不需要去判断当前状态了</p><p><img src="/img/remote/1460000044654467" alt="image-20240220191632355" title="image-20240220191632355"></p><p>比如要切换页面主题,可以直接这样</p><pre><code class="js">// 深浅切换
btn.onclcik = () => {
document.body.classList.toggle('dark')
}
// 无需像这样
if (当前是深色) {
设置为浅色
} else {
设置为深色
}</code></pre><p>另外,<code>toggle</code>还支持第二个参数,表示强制,是一个布尔值,为 <code>true</code>表示添加,反之为移除,而<strong>不管当前是什么状态</strong></p><pre><code class="js">el.classList.toggle('a', force); </code></pre><p>比如</p><pre><code class="js">// 设置为浅色
btnLight.onclcik = () => {
document.body.classList.toggle('dark', false)
}
// 设置为深色
btnDark.onclcik = () => {
document.body.classList.toggle('dark', true)
}</code></pre><p>是不是非常方便呢?</p><h2>二、toggleAttribute</h2><p>还有一个和<code>toggle</code>比较类似的是<code>toggleAttribute</code>,顾名思义,这个是用来切换<strong>属性</strong>的,语法和前面一致</p><pre><code class="js">toggleAttribute(name)
toggleAttribute(name, force)</code></pre><p>这个使用场景更为广泛,例如控制一个输入框的禁用与开启</p><pre><code class="js">input.toggleAttribute('disabled')</code></pre><p>当然对于表单元素,还可以用<code>.</code>的方式直接设置</p><pre><code class="js">input.disabled = !input.disabled;</code></pre><p>但是,对于普通自定义属性,就不能用这种方式了,比如黑暗模式,用属性来控制</p><pre><code class="js">document.body.toggleAttribute('dark');</code></pre><p>第二个参数也是类似的</p><pre><code class="js">document.body.toggleAttribute('dark', ture); //添加dark属性
document.body.toggleAttribute('dark', false);//移除dark属性</code></pre><p>当然你还可以用更常规的方式</p><pre><code class="js">document.body.setAttribute('dark', ''); //添加dark属性
document.body.removeAttribute('dark');//移除dark属性</code></pre><p>个人觉得不如<code>toggleAttribute</code>优雅,你觉得呢?</p><h2>三、togglePopover</h2><p><code>togglePopover</code>是新出来的,是针对<code>popover</code>元素推出的打开与关闭的方法。</p><blockquote>关于<code>popover</code>,可以参考我之前写的这篇文章:<a href="https://segmentfault.com/a/1190000043835312">原生 popover 终于来了!</a></blockquote><p>语法略有差异,因为无需修改其他状态,所以只有一个可选参数</p><pre><code class="js">popoverEl.togglePopover(); //切换 popover
popoverEl.togglePopover(true); //打开 popover
popoverEl.togglePopover(false); //关闭 popover</code></pre><p>另外,带有的参数的情况下还有更直观的 <code>api</code>,推荐使用</p><pre><code class="js">// 打开
popoverEl.togglePopover(true)
// 等同于
popoverEl.showPopover()
// 关闭
popoverEl.togglePopover(false)
// 等同于
popoverEl.hidePopover()</code></pre><p>比较新,是跟着<code>popver</code>一起出现的,兼容性如下</p><p><img src="/img/remote/1460000044654468" alt="image-20240221201559148" title="image-20240221201559148"></p><h2>四、toggle event</h2><p>最后再来介绍一个<code>toggle</code>事件,表示监听切换事件。</p><p>这个也是跟随<code>poperver</code>推出的,可以通过<code>event</code>对象获取当前的新状态和旧状态,如下</p><pre><code class="js">popover.addEventListener("toggle", (event) => {
if (event.newState === "open") {
console.log("Popover has been shown");
} else {
console.log("Popover has been hidden");
}
});</code></pre><p>效果如下</p><p><img src="/img/remote/1460000044654469" alt="image-20240221202632884" title="image-20240221202632884"></p><p>有意思的是,这个事件同时也支持<code>details</code>元素</p><pre><code class="js">details.addEventListener("toggle", (event) => {
});</code></pre><p><img src="/img/remote/1460000044654470" alt="image-20240221202735710" title="image-20240221202735710"></p><h2>五、总结一下</h2><p>以上就是 web 中几个和<code>toggle</code>相关的<code>api</code>了,下面总结一下</p><ol><li><code>toggle</code>作用在<code>DOMTokenList</code>上,通常是<code>classList </code>, <code>classList.toggle</code>可以切换<code>class</code></li><li><code>toggle</code>还支持第二个参数,用于强制添加或者移出某个<code>class</code></li><li><code>toggleAttribute</code>可以控制属性的切换</li><li><code>togglePopver</code>是专门推出用于控制<code>popover</code>元素打开和关闭的方法</li><li><code>toggle event</code>可以监听<code>popover</code>元素和<code>details</code>元素的打开和关闭事件</li></ol><p>归类学习整理还是挺不错的。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤</p><blockquote>欢迎关注我的公众号:<strong>前端侦探</strong></blockquote>
读Paimon源码聊设计:引子
https://segmentfault.com/a/1190000044657594
2024-02-26T09:20:06+08:00
2024-02-26T09:20:06+08:00
泊浮目
https://segmentfault.com/u/camile
0
<table><thead><tr><th>版本</th><th>日期</th><th>备注</th></tr></thead><tbody><tr><td>1.0</td><td>2024.2.26</td><td>文章首发</td></tr></tbody></table><p>最近我司开始探索Paimon在部分场景下的使用,因此我这边需要做一些技术储备,慢慢关注起来Paimon的一些实现细节。</p><h2>简单介绍一下前辈Iceberg</h2><p>一般的数据湖都会设计成开放通用的,即不和特定的存储、计算引擎(比如Spark和Flink)绑定。所以数据湖的定位是在计算引擎之下,又在存储之上,将其称之为table format。需要强调的是数据湖一般是面向OLAP场景的,所以一般存储会选择分布式文件系统。现在OLAP底层存储支持对象存储基本算是快成业界共识了,这玩意儿挺划算的。</p><p>数据湖的前辈基本就是Hive了。当时大家用Hive碰到的问题就是Hive耦合HDFS很厉害,最主要体现在:</p><ol><li>Hive上的计算执行首先依赖于list操作。在对象存储上做list是个很慢的操作。</li><li>Hive的写数据依赖于rename。但同样这个操作在对象存储上做特别的慢。</li></ol><p>这两个问题直接导致无法降本。从这点上来说,Iceberg是自己维护了一套元数据,这块网上非常的全,就不再赘述了,google上搜<code>iceberg file layout</code>一大把。</p><p>Hive还有其他的问题,如:</p><ol><li>metastore的瓶颈问题。</li><li>没有ACID保证。</li><li>parition字段必须显示的在query里。</li><li>下推能力有限。Hive只能通过partition和bucket对需要扫描哪些文件进行过滤,无法更加细致。尽管parquet文件里保存了max和min值可以用于进一步的过滤,但没软用。</li></ol><p>Iceberg把这些都解了。基于快照实现事务、数据的更新,快照的数据也允许跳版本读取来做时间回溯。同时收集的统计信息也更加细粒度,不仅仅是文件parition级别的,还会记录文件级的内容(比如一个文件中的min、max值)和实现文件内容级的信息——一个文件中的min、max等等。</p><p>听起来一切都还很美好。唯一美中不足的就是Iceberg对于实时场景支持得不好:</p><ul><li>Flink写入Iceberg会引发小文件的问题。</li><li>Iceberg不支持CDC(OLAP支持CDC的确有点离谱,但是的确有需求呀)。</li><li>Iceberg主键表不支持部分字段更新。这在实时数仓的场景中有点离谱。</li></ul><h2>Paimon可以解决什么问题</h2><p>目前看来Paimon基于Iceberg的场景上,去支持流读流写(<em>这块后续会做源码分析</em>),甚至还支持了点查和预聚合。本质是分布式文件系统上套了一个LSM,这样数据都是有序写入——将多次写入优化成一次顺序写入,对存储系统上比较友好的。同时LSM可以作为一个简单的缓存,且有序写入为后面查询也可以减少代价,这都可以为查询减少代价。</p><p><img src="/img/remote/1460000044657596" alt="" title=""></p><p>从场景上来说它可以解决一些准实时业务的场景。因为基于对象存储来做底层存储,尤其还是列式存储。无论如何都不好做到实时场景:</p><ul><li>Paimon的CDC根据不同的模式,会有不同的新鲜度。发出完整CDC的模式要选择Lookup。一般是Checkpoint的间隔+10s多新鲜度,这是比较好的性能考量下。具体还要看数据量、分桶数、小文件数量的影响。</li><li>实时场景要求是毫秒级点查响应。Paimon支持的是秒级点查。</li></ul><blockquote>但现实中真正需要实时类场景的业务有多少呢?因为数据的新鲜度往往和业务决策周期有关系。这么来看,数据新鲜度的要求从高到低,对于业务场景的总数来说,一定是一个金字塔形状的。</blockquote><p>我们前面提到过数据湖一般不会和任何计算引擎绑定。因此业界还有一种玩法叫湖上建仓,计算能力用的是OLAP,数据来自数据湖。这样就很有想象力了,因此现在一些实时+离线的场景会用不同的存储引擎,那么数据就会拷贝好几份。如果数据都放在同一个数据引擎中,这样可以减少不少的存储成本。(<em>对于通用型设计</em> <em>这块后续会做源码分析</em>)</p><p>具体实现还是看对于性能的要求的:</p><ul><li>要求低就做一些简单的优化直接捞数据。</li><li>再高点就缓存到OLAP里。</li><li>再高点就不仅仅是缓存到OLAP里,还会做物化视图。</li></ul><h2>Trade Off</h2><h3>Serving、Trascantion、Analytics</h3><p>根据业界常识,我们会发现:</p><ul><li>面向在线应用,高并发、快速、简单,如:HBase、Redis</li><li>面向分析的,大规模数据扫描、过滤、汇总,如:Hive、Presto</li><li>面向事务、随机读写的,如:MySQL,PostgreSQL</li></ul><p>数据湖是典型的OLAP产物。但结合上文,Paimon具有一定的随机读写能力。</p><p><img src="/img/remote/1460000044657597" alt="" title=""></p><h3>Buffer、Mutable、Ordered<img src="/img/remote/1460000044657598" alt="" title=""></h3><p>存储结构有三个常见变量:是否使用缓冲、使用不可变的还是可变的文件,以及是否按顺序存储值(有序性)。</p><p>由于TiDB底层的RocksDB用了LSM。因此使用了缓冲、不可变性以及顺序性。</p><h3>RUM<img src="/img/remote/1460000044657599" alt="" title=""></h3><p>有一种流行的存储结构开销模型考虑了如下三个因素:读取(Read)、更新(Update)和内存(Memory)开销。它被称为RUM猜想。</p><p>RUM猜想指出,减少其中两项开销将不可避免地导致第三项开销的恶化,并且优化只能以牺牲三个参数中的一个为代价。我们可以根据这三个参数对不同的存储引擎进行比较,以了解它们针对哪些参数进行了优化,以及其中隐含着哪些可能的权衡。</p><p>一个理想的解决方案是拥有最小的读取开销,同时保持较低的内存与写入开销。但在现实中,这是无法实现的,因此我们需要进行取舍。</p><p>Paimon允许在配置中自由设置LSM的高度,以便获取读与写之前的权衡。</p><h2>内幕鸟瞰</h2><h2><img src="/img/remote/1460000044657600" alt="" title=""></h2><p>前面说到过,计算部分是依赖于计算引擎实现的,本身Paimon没有提供计算能力。存储则是基于部分是文件系统做了薄薄的一层LSM。</p><p>从文件布局来看,Partition相当于是一级索引,和Hive一样。Bucket为二级索引,每个Bucket下都会有Data file和Change log file。这意味着如果命中了Parition和Bucket条件,有一些额外的条件查询也不会太慢——一般都会收集文件级的统计信息,并对文件的Reader做一些过滤优化。</p><p>整体的布局还是和Iceberg挺像的,这边不再过多赘述。</p><h2>小结</h2><p>在这篇文章中我简单的介绍了一下Paimon要解决的问题,以及它的前辈Iceberg的强大与不足之处。</p><p>目前该项目还处于孵化中,后续我会一直关注其实现细节,敬请期待。</p>
深入剖析 Java 类属性与类方法的应用
https://segmentfault.com/a/1190000044646968
2024-02-21T21:11:15+08:00
2024-02-21T21:11:15+08:00
小万哥
https://segmentfault.com/u/caisekongbai
1
<h2>Java 类属性</h2><p>Java 类属性,也称为字段,是类中的变量。它们用于存储与类相关的数据。</p><p>创建类属性</p><p>在类定义中声明属性:</p><pre><code class="java">public class Main {
int x; // 属性
String name; // 属性
}</code></pre><p>访问类属性</p><p>使用点语法访问对象的属性:</p><pre><code class="java">Main myObj = new Main();
myObj.x = 5; // 设置属性值
System.out.println(myObj.x); // 获取属性值</code></pre><p>修改类属性</p><p>可以修改对象的属性值:</p><pre><code class="java">Main myObj = new Main();
myObj.x = 5;
myObj.x = 10; // 修改属性值
System.out.println(myObj.x); // 输出 10</code></pre><p>属性类型</p><p>属性可以是任何数据类型,包括:</p><ul><li>基本类型:int、double、boolean、char等</li><li>引用类型:String、Date、List等</li></ul><p>修饰符</p><p>可以使用修饰符来控制属性的访问权限:</p><ul><li>public:公开访问</li><li>private:私有访问</li><li>protected:受保护访问</li><li>default:默认访问</li></ul><p>示例</p><pre><code class="java">public class Main {
private int x; // 私有属性
public String name; // 公开属性
public void myMethod() {
// 可以访问私有属性
x = 10;
}
public static void main(String[] args) {
Main myObj = new Main();
// 可以访问公开属性
myObj.name = "John Doe";
// 无法访问私有属性
// myObj.x = 5; // 错误
}
}</code></pre><p>一些额外的说明:</p><ul><li>在 Java 中,类属性通常被定义为private,以便只能通过类的方法来访问它们。</li><li>为了方便起见,也可以将类属性定义为public,但这会使它们更容易被意外更改。</li><li>建议使用getter和setter方法来访问和修改类属性,以便更好地控制对属性的访问。</li></ul><p>示例:</p><pre><code class="java">public class Main {
private int x;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public static void main(String[] args) {
Main myObj = new Main();
myObj.setX(5);
System.out.println(myObj.getX()); // 输出 5
}
}</code></pre><p>在这个示例中,x 属性是私有的,但可以通过 getX() 和 setX() 方法来访问和修改。</p><p>还有一些其他与类属性相关的内容:</p><ul><li>静态属性:静态属性属于类本身,而不是类的实例。</li><li>常量属性:常量属性的值不能被修改。</li><li>枚举属性:枚举属性的值只能是预定义的一组值之一。</li></ul><h2>Java 类方法</h2><p>Java 类方法 是在类内声明的代码块,用于执行特定的操作。它们类似于函数,但与类本身相关联,而不是与类的实例相关联。</p><p>创建类方法</p><p>使用 <code>static</code> 关键字声明类方法:</p><pre><code class="java">public class Main {
static void myMethod() {
System.out.println("Hello World!");
}
}</code></pre><p>调用类方法</p><p>使用类名和方法名,后跟括号 () 调用类方法:</p><pre><code class="java">public class Main {
static void myMethod() {
System.out.println("Hello World!");
}
public static void main(String[] args) {
myMethod();
}
}</code></pre><p>示例:</p><pre><code class="java">public class Main {
static void myMethod(String name) {
System.out.println("Hello, " + name + "!");
}
public static void main(String[] args) {
myMethod("John Doe");
}
}</code></pre><p>输出:</p><pre><code class="java">Hello, John Doe!</code></pre><p>类方法与实例方法</p><ul><li>类方法属于类本身,而实例方法属于类的实例。</li><li>类方法可以直接通过类名调用,而实例方法需要通过类的实例调用。</li><li>类方法通常用于执行与类相关的通用操作,而实例方法通常用于操作类的实例。</li></ul><p>修饰符</p><p>可以使用修饰符来控制类方法的访问权限:</p><ul><li><code>public</code>:公开访问</li><li><code>private</code>:私有访问</li><li><code>protected</code>:受保护访问</li><li><code>default</code>:默认访问</li></ul><p>示例:</p><pre><code class="java">public class Main {
private static void myMethod() {
System.out.println("Hello World!");
}
public static void main(String[] args) {
// myMethod(); // 错误,无法访问私有方法
}
}</code></pre><p>一些额外的说明:</p><ul><li><p>类方法通常用于执行与类相关的通用操作,例如:</p><ul><li>创建新实例</li><li>验证输入</li><li>提供工具类方法</li></ul></li><li><p>实例方法通常用于操作类的实例,例如:</p><ul><li>获取或设置属性值</li><li>执行计算</li><li>改变对象的状态</li></ul></li><li>可以使用 <code>final</code> 关键字声明类方法,使其无法被重写。</li><li>可以使用 <code>abstract</code> 关键字声明抽象类方法,其定义必须由子类提供。</li></ul><h2>最后</h2><p>为了方便其他设备和平台的小伙伴观看往期文章:</p><p>微信公众号搜索:<code>Let us Coding</code>,关注后即可获取最新文章推送</p><p>看完如果觉得有帮助,欢迎 点赞、收藏、关注</p>
10个行锁、死锁案例⭐️24张加锁分析图🚀彻底搞懂Innodb行锁加锁规则!
https://segmentfault.com/a/1190000044643885
2024-02-21T08:23:31+08:00
2024-02-21T08:23:31+08:00
菜菜的后端私房菜
https://segmentfault.com/u/tcl_64f498a8d872b
8
<h2>10个行锁、死锁案例⭐️24张加锁分析图🚀彻底搞懂Innodb行锁加锁规则!</h2><p><a href="https://link.segmentfault.com/?enc=%2BbVCkrgrG0u7NSM26wfDZw%3D%3D.%2B4MIjlA%2FU5E8DRVklkadavYXeEyez8ProPkDA27fryibdhbn%2Fq15gD9gKANt%2BOJg" rel="nofollow">上篇文章</a> 我们描述原子性与隔离性的实现,其中描述读操作解决隔离性问题的方案时还遗留了一个问题:写操作是如何解决不同的隔离性问题?</p><p>本篇文章将会解决这个问题并描述MySQL中的锁、总结Innodb中行锁加锁规则、列举行锁、死锁案例分析等</p><p>再阅读本篇文章前,至少要理解查询使用索引的流程、mvcc等知识(不理解的同学可以根据专栏顺序进行阅读)</p><p><img src="/img/remote/1460000044643887" alt="image-20240219150520718" title="image-20240219150520718"></p><h3>MySQL锁的分类</h3><blockquote>从锁的作用域上划分:全局锁、表锁、页锁、行锁</blockquote><ol><li>全局锁:锁整个数据库实例,常用数据备份,禁止全局写,只允许读</li><li><p>表锁:锁表,对表进行加锁</p><ul><li>元数据锁:表结构修改时</li><li>表X锁:表独占锁</li><li>表S锁:表共享锁</li></ul></li><li>页锁:位于表锁与行锁中间作用域的锁</li><li>行锁(innodb特有):锁记录,其中包含独占锁(写锁,X锁)和共享锁(读锁,S锁)</li></ol><blockquote>从功能上划分:意向锁、插入意向锁、自增长锁、悲观锁、乐观锁...</blockquote><ol><li><p>意向锁:表锁,表示有意向往表中加锁,获取行锁前会加意向锁,当需要加表锁时,要通过意向锁判断表中是否有行锁</p><ul><li>独占意向锁 IX:意向往表中加X锁,兼容IX、IS,不兼容X、S(表级别)</li><li>共享意向锁 IS:意向往表中加S锁,兼容IX、IS、S,不兼容X(表级别)</li></ul></li><li>插入意向锁:隐式锁,意向往表中插入记录</li><li>自增长锁:隐式锁,在并发场景下不确定插入数量会使用自增长锁加锁生成自增值,如果确定则使用互斥锁(连续模式)</li><li>悲观锁:悲观锁可以用加行锁实现</li><li>乐观锁:乐观锁可以用版本号判断实现</li></ol><p>这些锁有些是MySQL提供的,有些是存储引擎提供的,比如Innodb支持的行锁粒度小,并发性能高,不易冲突</p><p>但在某些场景下行锁还是会发生冲突(阻塞),因此我们需要深入掌握行锁的加锁规则才能在遇到这种场景时分析出问题</p><h3>Innodb的行锁加锁规则</h3><p>前面说到行锁分为独占锁 X锁和共享锁 S锁,行锁除了会使用这种模型外,还会使用到一些其他的模型</p><h4>锁模型</h4><p><strong>record:记录锁,用于锁定某条记录</strong> 模型使用S/X (也就是独占锁、共享锁)</p><p><strong>gap:间隙锁,用于锁定某两条记录之间,禁止插入,用于防止幻读</strong> 模型使用GAP</p><p><strong>next key:临键锁,相当于记录锁 + 间隙锁</strong> 模型使用X/S,GAP</p><p>在lock mode中常用X或S代表独占/共享的record,GAP则不分X或S</p><p>如果是独占的临键锁则是X、GAP,共享的临键锁则是S、GAP</p><p>(这个lock mode后面案例时会用到,表示当前SQL被哪种锁模型阻塞)</p><h4>不同隔离级别下的加锁</h4><p><strong>加锁的目的就是为了能够满足事务隔离性从而达到数据的一致性</strong></p><p>在<strong>不同隔离级别、使用不同类型SQL(增删改查)、走不同索引(主键和非主键、唯一和非唯一)、查询条件(等值查询、范围查询)、MySQL版本</strong> 等诸多因素都会导致加锁的细节发生变化</p><p>因此只需要大致掌握加锁规则,并结合发生阻塞的记录排查出发生阻塞的问题,再进行优化、解决即可(本文基于5.7.X)</p><p><strong>在RU(Read Uncommitted)下,读不加锁,写使用X record,由于写使用X record 则不会产生脏写,会产生脏读、不可重复读、幻读,</strong></p><p><strong>在RC(Read Committed)下,读使用mvcc(每次读生成read view),写使用X record,不会产生脏写、脏读,但会有不可重复读和幻读</strong></p><p><strong>在RR(Repeatable Read)下,读使用mvcc(第一次读生成read view),写使用X next key,不会产生脏写、脏读、不可重复读和大部分幻读,极端场景的幻读会产生</strong></p><p><strong>在S(Serializable)下,自动提交情况下读使用mvcc,手动提交下读使用S next key,写使用X next key,不会产生脏写、脏读、不可重复读、幻读</strong></p><p>在常用的隔离级别RC、RR中,读都是使用mvcc机制(不加锁)来提高并发性能的</p><h4>锁定读的加锁</h4><p>在S串行化下,读会加S锁,那select如何加锁呢?</p><p>S锁:<code>select ... lock in share mode</code></p><p>X锁:<code>select ... for update</code></p><p>通常把加锁的select称为锁定读,而在普通的update和delete时,需要先进行读(找到记录)再操作,在这种情况下加锁规则也可以归为锁定读</p><p>update与delete是写操作,肯定是加X锁的</p><p>(以下锁定读和新增的加锁规则是总结,搭配案例查看,一开始看不懂不要紧~)</p><blockquote>锁定读加锁规则</blockquote><ol><li><p><strong>在RC及以下隔离级别,锁定读使用record锁;在RR及以上隔离级别,锁定读使用next key锁</strong> (间隙锁的范围是前开后闭,案例详细描述)</p><p>(具体S、X锁则看SQL,如果是 <code>select ... lock in share mode</code> 则是S锁,如果是 <code>select ... for update</code>、<code>update ...</code>、<code>delete ...</code> 则是X锁)</p></li><li><strong>等值查询:如果找不到记录,该查询条件所在区间加GAP锁;如果找到记录,唯一索引临键锁退化为记录锁,非唯一索引需要扫描到第一条不满足条件的记录,最后临键锁退化为间隙锁(不在最后一条不满足条件的记录上加记录锁)</strong></li><li><strong>范围查询:非唯一索引需要扫描到第一条不满足条件的记录</strong>(5.7中唯一索引也会扫描第一条不满足条件的记录8.0修复,后文描述)</li><li><p><strong>在查找的过程中,使用到什么索引就在那个索引上加锁,遍历到哪条记录就给哪条先加锁</strong></p><p>(查找时走二级索引,如果要回表查聚簇索引,则还会在聚簇索引上加锁)</p><p>(修改时如果二级索引上也存在要修改的值,则还要去二级索引中查找加锁并修改)</p></li><li><strong>在RC及以下隔离级别下,查找过程中如果记录不满足当前查询条件则会释放锁;在RR及以上无论是否满足查询条件,只要遍历过记录就会加锁,直到事务提交才释放</strong>(RR及以上获取锁的时间会更长)</li></ol><h4>新增的加锁</h4><p>前面说到update、delete这种先查再写的操作可以看成加X锁的锁定读,而select的锁定读分为S、X,还剩insert的规则没有说明</p><blockquote>新增加锁规则</blockquote><p>新增加锁规则分为三种情况:<strong>正常情况、遇到重复冲突的情况、外键情况</strong></p><p>新增时加的锁叫插入意向锁,它是隐式锁</p><p>当别的事务想要获取该记录的X/S锁时,查看该记录的事务id是不是活跃事务,如果活跃(事务未提交)则会帮新增记录的事务生成锁结构,此时插入意向锁变成显示锁(可以看成X锁)</p><p><strong>正常情况下加锁:</strong></p><ol><li><strong>一般情况下,插入使用隐式锁(插入意向锁),不生成锁结构</strong></li><li><strong>当插入意向锁(隐式锁)被其他事务生成锁结构时变为显示锁(X record)</strong></li></ol><p><strong>重复冲突加锁:</strong></p><ol><li><strong>当insert遇到重复主键冲突时,RC及以下加S record,RR及以上加S next key</strong></li><li><p><strong>当insert遇到重复唯一二级索引时,加S next key</strong></p><p><strong>如果使用 <code>ON DUPLICATE KEY update</code> 那么S锁会换成X锁</strong></p></li></ol><p>外键加锁:一般不做物理外键,略...</p><h3>行锁案例分析</h3><h4>搭建环境</h4><p>先建立一张测试表,其中id为主键,以s\_name建立索引</p><pre><code class="sql">CREATE TABLE `s` (
`id` int(11) NOT NULL,
`s_name` varchar(255) DEFAULT NULL,
`s_age` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name_idx` (`s_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;</code></pre><p>再插入一些记录</p><pre><code class="sql">INSERT INTO `s` (`id`, `s_name`, `s_age`) VALUES (1, 'juejin', '1');
INSERT INTO `s` (`id`, `s_name`, `s_age`) VALUES (10, 'nb', '10');
INSERT INTO `s` (`id`, `s_name`, `s_age`) VALUES (20, 'caicai菜菜', '20');
INSERT INTO `s` (`id`, `s_name`, `s_age`) VALUES (25, 'ai', '25');</code></pre><p>聚簇索引和s\_name索引的存储图像简化成如下:</p><p><img width="723" height="295" src="/img/bVdbt4r" alt="image-20240121131811808.png" title="image-20240121131811808.png"><br>前面说过GAP需要加在记录之间,如果是第一条记录或者最后一条记录要防止插入,该如何加GAP锁呢?</p><p>Infimum和Supremum的出现就能够解决这种问题,它们用于标识每页的最小值和最大值</p><p>注意:由于RC、RR是常用的隔离级别,案例也是使用这两种隔离级别进行说明</p><h4>分析方法</h4><p>可以通过系统库查看行锁阻塞的相关信息</p><p>5.7 阻塞的锁、事务等信息在information\_schema库中的innodb\_locks、innodb\_lock\_waits、innodb\_trx等表</p><p>8.0 的相关信息则是在performance\_schema库中</p><blockquote>lock记录信息简介</blockquote><p><img width="723" height="67" src="/img/bVdbt4s" alt="image-20240119113504300.png" title="image-20240119113504300.png"><br>lock\_id 锁id 由事务id、存储信息组成 会变动</p><p>lock\_trx\_id 事务ID 42388为先开启的事务 (事务ID全局自增)</p><p>lock\_mode 阻塞的锁为X锁,还有其他模式:S、X、IS、IX、GAP、AUTO\_INC等</p><p>lock\_type 锁类型为行锁record ,还有表锁:table</p><p>lock\_table 锁的表 ; lock\_index 锁的索引 (二级索引)</p><p>lock\_space 、page 、rec 锁的表空间id、页、堆号等存储信息</p><p>lock\_data 表示锁的数据,一般是行记录 'caicai菜菜',20 (s\_name,id)</p><blockquote>还可以通过联表查询获取行锁阻塞的信息</blockquote><pre><code class="sql">SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
rl.lock_mode waiting_lock_mode,
rl.lock_type waiting_lock_type,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query,
bl.lock_mode blocking_lock_mode,
bl.lock_type blocking_lock_type
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b
ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r
ON r.trx_id = w.requesting_trx_id
inner join information_schema.innodb_locks rl
on r.trx_id = rl.lock_trx_id
inner join information_schema.innodb_locks bl
on b.trx_id = bl.lock_trx_id;</code></pre><p>又或者通过 innodb的日志 (show engine innodb status)查看阻塞信息...</p><p>(后文分析再说)</p><h4>案例:RC、RR下的加锁</h4><table><thead><tr><th align="center"> </th><th align="center">T1</th><th align="center">T2</th></tr></thead><tbody><tr><td align="center">1</td><td align="center">begin;<br/>select * from s where id\>=10 and id<=20 for update;</td><td align="center"> </td></tr><tr><td align="center">2</td><td align="center"> </td><td align="center">insert into s values (12,'caicaiJava',12);<br/>(阻塞)</td></tr><tr><td align="center">3</td><td align="center">commit;</td><td align="center"> </td></tr></tbody></table><p>T1事务在10,20之间会加GAP锁,因此T2新增时会被阻塞</p><p><img width="723" height="76" src="/img/bVdbt4t" alt="image-20240121110912505.png" title="image-20240121110912505.png"><br>设置为RC后不再阻塞,因为RC下不加GAP锁不防止插入</p><pre><code class="sql">SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@tx_isolation;</code></pre><p>但如果是要获取记录锁则还是会被阻塞 (修改id为10的记录 <code>update s set s_name = '666' where id = 10</code>)</p><p><img width="723" height="66" src="/img/bVdbt4u" alt="image-20240121105954377.png" title="image-20240121105954377.png"><br>根据该案例可以说明规则一:<strong>RC及以下使用记录锁、RR及以上使用临键锁</strong></p><h4>案例:等值查询</h4><blockquote>等值查询:匹配不到满足条件的记录</blockquote><table><thead><tr><th> </th><th align="center">T1</th><th align="center">T2</th><th align="center">T3</th></tr></thead><tbody><tr><td>1</td><td align="center">begin;<br/>select * from s where id=15 for update;</td><td align="center"> </td><td align="center"> </td></tr><tr><td>2</td><td align="center"> </td><td align="center">insert into s values (11,'caicaiJava11',11);<br/>(阻塞)</td><td align="center"> </td></tr><tr><td>3</td><td align="center"> </td><td align="center"> </td><td align="center">insert into s values (19,'caicaiJava11',19);<br/>(阻塞)</td></tr><tr><td>4</td><td align="center">commit;</td><td align="center"> </td><td align="center"> </td></tr></tbody></table><p>通过阻塞记录可以看到T2,T3事务被主键索引上数据为20的临键锁(的GAP)阻塞</p><p><img width="723" height="177" src="/img/bVdbt4v" alt="image-20240121133606532.png" title="image-20240121133606532.png"><br><strong>等值查询如果匹配不到值会在该区间加GAP锁</strong></p><p>图中向下黑箭头为GAP锁</p><p><img width="723" height="214" src="/img/bVdbt4w" alt="image-20240121133306180.png" title="image-20240121133306180.png"><br>例如T1等值查询id=15,没有id=15的记录则会加锁在15这个区间加GAP锁</p><blockquote>等值查询:匹配到满足条件的记录</blockquote><table><thead><tr><th> </th><th align="center">T1</th><th align="center">T2</th><th align="center">T3</th></tr></thead><tbody><tr><td>1</td><td align="center">begin;<br/>select * from s where id=20 for update;</td><td align="center"> </td><td align="center"> </td></tr><tr><td>2</td><td align="center"> </td><td align="center">insert into s values (15,'菜菜的后端私房菜',15);<br/>(不阻塞)</td><td align="center"> </td></tr><tr><td>3</td><td align="center"> </td><td align="center"> </td><td align="center">update s set s\_name = '菜菜的后端私房菜' where id = 20;<br/>(阻塞)</td></tr><tr><td>4</td><td align="center">commit;</td><td align="center"> </td><td align="center"> </td></tr></tbody></table><p>因为唯一索引上相同的记录只有一条,当等值查询匹配时,临键锁会退化成记录锁,因此T2不被阻塞 T3被阻塞</p><p>图中为T3被数据为20上的X锁阻塞</p><p><img width="723" height="154" src="/img/bVdbt4x" alt="image-20240121134322398.png" title="image-20240121134322398.png"><br><strong>唯一索引等值查询间隙锁退化为记录锁</strong></p><p>(图中蓝色为记录锁)</p><p><img width="723" height="189" src="/img/bVdbt4y" alt="image-20240121134717251.png" title="image-20240121134717251.png"></p><blockquote>非唯一索引等值查询</blockquote><table><thead><tr><th> </th><th align="center">T1</th><th align="center">T2</th><th align="center">T3</th></tr></thead><tbody><tr><td>1</td><td align="center">begin;<br/>select s\_name,id from s where s\_name='caicai菜菜' for update;</td><td align="center"> </td><td align="center"> </td></tr><tr><td>2</td><td align="center"> </td><td align="center">insert into s values (15,'bilibili',15);<br/>(阻塞)</td><td align="center"> </td></tr><tr><td>3</td><td align="center"> </td><td align="center"> </td><td align="center">insert into s values (18,'da',18);<br/>(阻塞)</td></tr><tr><td>4</td><td align="center">commit;</td><td align="center"> </td><td align="center"> </td></tr></tbody></table><p>为了确保 <code>select s_name,id from s where s_name='caicai菜菜' for update</code> 使用s\_name索引,我将查询列换成s\_name上存在的列,避免回表(确保使用s\_name)</p><ol><li>先定位到 <code>s_name='caicai菜菜'</code> 的记录,加锁:(ai,caicai菜菜]</li><li>由于不确定满足 <code>s_name='caicai菜菜'</code> 的记录是否有重复,于是继续后查询,加锁:(caicai菜菜,juejin]</li><li>由于juejin不满足查询条件,于是退化为间隙锁,加锁:(caicai菜菜,juejin)</li></ol><p><img width="723" height="160" src="/img/bVdbt4z" alt="image-20240121140556218.png" title="image-20240121140556218.png"></p><p>最终加锁范围 = (ai,caicai菜菜] + (caicai菜菜,juejin) = (ai,juejin)</p><p>(注意:我这里的加锁范围是简化的,没有带上主键信息;完整信息如下图lock\_data中的juejin,1)</p><p>然后再来分析T2,T3的插入语句,首先它们需要在聚簇索引和name\_idx索引上新增数据,由于聚簇索引未加锁,因此不影响插入</p><p>但是name\_idx索引上存在锁,T2事务 bilibili 会插入到ai和caicai菜菜记录之间,T3事务会插入到caicai菜菜和juejin这两条记录间,因此被GAP锁阻塞</p><p>通过阻塞记录也可以看出T2,T3均被临键锁阻塞</p><p><img width="723" height="120" src="/img/bVdbt4A" alt="image-20240121141206330.png" title="image-20240121141206330.png"></p><p>至此等值查询的案例分析完毕,小结如下:</p><ul><li><strong>等值查询找不到记录:该区间加GAP锁</strong></li><li><p><strong>等值查询找到记录</strong></p><ol><li><strong>唯一索引:临键锁会退化为记录锁</strong></li><li><strong>非唯一索引:一直扫描到第一条不满足条件的记录并将临键锁退化为间隙锁</strong></li></ol></li></ul><h4>案例:范围查询</h4><p>在上面等值查询 + 非唯一索引的场景下,由于无法判断该值数量,因此会一直扫描,可以把这种场景理解成范围查询</p><table><thead><tr><th align="center"> </th><th align="center">T1</th><th align="center">T2</th></tr></thead><tbody><tr><td align="center">1</td><td align="center">begin;<br/>select * from s where id\>=10 and id<=20 for update;</td><td align="center"> </td></tr><tr><td align="center">2</td><td align="center"> </td><td align="center">insert into s values (21,'caicaiJava',21);<br/>(阻塞)</td></tr><tr><td align="center">3</td><td align="center">commit;</td><td align="center"> </td></tr></tbody></table><p>按照正常思路来说,我的查询条件在10-20,那么就不能往这个范围外再加锁了</p><p>但是新增该范围外的记录是会阻塞的(我明明查询条件在10\~20,结果超过20你也给我加锁是吧?)</p><p><img width="723" height="81" src="/img/bVdbt4B" alt="image-20240121112007123.png" title="image-20240121112007123.png"></p><p>我们来分析下T1加锁过程: <code>id>=10 and id<=20</code></p><ol><li>定位第一条记录(id=10),按道理加间隙锁(前开后闭)应该是(1,10],但是有等值查询的优化,间隙锁退化为记录锁,因此只对10加锁 [10]</li><li>继续向后范围扫描,定位到记录(id=20),加锁范围(10,20]</li><li>按照正常思路主键是唯一的,我已经找到一条20了,那我应该退出才对呀,但是它还是会继续扫描,直到第一条不满足查询条件的值(id=25)并将临键锁锁退化成间隙锁,也就是不在25加记录锁,因此加锁范围(20,25)</li></ol><p><img width="723" height="216" src="/img/bVdbt4C" alt="image-20240121131856921.png" title="image-20240121131856921.png"></p><p>最终加锁范围 [10] + (10,20] + (20,25) = [10,25),因此插入主键为21时会被阻塞</p><p>思考:按照正常的思路,当在非唯一索引上时,这么扫描没问题,因为不知道满足结果的20有多少条,只能往后扫描找到第一条不满足条件的记录;而在唯一索引上找到最后一个满足条件的记录20后,还继续往后加锁是不是有点奇怪呢?</p><p>我在8.0的版本中重现这个操作,插入id=21不再被阻塞,应该是在唯一索引上扫描到最终满足条件的记录(id=20)就结束,加锁范围如下图(在5.7中这应该算bug)</p><p><img width="723" height="217" src="/img/bVdbt4D" alt="image-20240121131946385.png" title="image-20240121131946385.png"><br><strong>范围查询时无论是否唯一索引都会扫描到第一条不满足条件的记录,然后临键锁退化为间隙锁</strong> (8.0修复唯一索引范围查询时的bug)</p><h4>案例:查找过程中怎么加锁</h4><table><thead><tr><th align="center"> </th><th align="center">T1</th><th align="center">T2</th></tr></thead><tbody><tr><td align="center">1</td><td align="center">begin;<br/>update s set s\_name = 'caicai菜菜' where id = 20;</td><td align="center"> </td></tr><tr><td align="center">2</td><td align="center"> </td><td align="center">select s\_name,id from s where s\_name like 'cai%' for update; <br/>(阻塞)</td></tr><tr><td align="center">3</td><td align="center">commit;</td><td align="center"> </td></tr></tbody></table><p>T1 事务在修改时先使用聚簇索引定位到id=20的记录,修改后通过主键id=20找到二级索引上的记录进行修改,因此聚簇索引、二级索引上都会获取锁</p><p><img width="723" height="365" src="/img/bVdbt4E" alt="image-20240121145339318.png" title="image-20240121145339318.png"></p><p>T2 事务锁定读二级索引时,由于查询条件满足二级索引的值,因此不需要回表,但由于T1事务锁住二级索引上的记录,因此发生阻塞</p><p><img width="723" height="67" src="/img/bVdbt4s" alt="image-20240119113504300.png" title="image-20240119113504300.png"></p><p>在该案例中说明:<strong>加锁时使用什么索引就要在那个索引上加锁,遍历到哪些记录就要在哪些记录上加锁</strong></p><p>delete:与主键相关的二级索引肯定也要删除,因此二级索引上对应主键值的记录也会被加锁</p><p>update:如果在二级索引上修改,那么一定回去聚簇索引上修改,因此聚簇索引也会被加锁;如果在聚簇索引上修改,二级索引可能会需要被加锁(如上案例,如果修改的是s\_age那么二级索引就不需要加锁)</p><p>select:使用什么索引就在什么索引上加锁,比如使用聚簇索引就要在聚簇索引上加锁,使用二级索引就在二级索引上加锁(如果要回表也要在聚簇索引上加锁)</p><h4>案例:RC、RR什么时候释放锁</h4><p>RC及以下,RR及以上在获取完锁后,释放锁的时机也不同</p><blockquote>RR下</blockquote><table><thead><tr><th> </th><th align="center">T1</th><th align="center">T2</th><th align="center">T3</th></tr></thead><tbody><tr><td>1</td><td align="center">begin;<br/>update s force index (name\_idx) set s\_age = 20 where s\_name \> 'c' and s\_age \> 18;</td><td align="center"> </td><td align="center"> </td></tr><tr><td>2</td><td align="center"> </td><td align="center">select * from s where id = 1 for update;<br/>(阻塞)</td><td align="center">insert into s values (33,'zz',33);<br/>(阻塞)</td></tr><tr><td>3</td><td align="center">commit;</td><td align="center"> </td><td align="center"> </td></tr></tbody></table><p>T3插入的记录满足 <code>s_name > 'c' and s_age > 18</code> 的记录被阻塞情有可原</p><p>那为啥T2 id=1不满足 <code>s_name > 'c' and s_age > 18</code> 也被阻塞了呢?</p><p>T1事务是一条修改语句,我使用force index 让它强制使用name\_idx索引,查询条件为 <code>s_name > 'c' and s_age > 18</code></p><p>由于name\_idx上不存在s\_age,需要判断s\_age就要去聚簇索引,因此聚簇索引上也会被加锁</p><p>T1在name\_idx上,根据查询条件s\_name > 'c'进行加锁</p><ol><li>定位第一条s\_name大于c的记录,加锁(ai,caicai菜菜]</li><li>根据主键值id=20去聚簇索引中找到该记录,加锁[20,20]</li><li>查看是否满足s\_age>18的条件,如果满足则进行修改(不满足不会释放锁)</li><li>继续循环,回到name\_idx上寻找下一条记录(直到不满足查询条件的记录或遍历完记录则退出)</li></ol><p>根据1-3的步骤,会在索引上这样加锁</p><p><img width="723" height="280" src="/img/bVdbt4F" alt="image-20240121154121934.png" title="image-20240121154121934.png"><br>最终加锁状态:(name\_id中的+∞则指的是supremum)</p><p><img width="723" height="367" src="/img/bVdbt4G" alt="image-20240121154321797.png" title="image-20240121154321797.png"></p><p>其中只有id=20的记录满足 <code>s_name > 'c' and s_age > 18</code>,即使这些记录不满足条件也不会释放锁</p><p>因此T2要获取聚簇索引id=1的记录时被阻塞,而T3则是被supremum阻塞</p><p><img width="723" height="92" src="/img/bVdbt4H" alt="image-20240121155044545.png" title="image-20240121155044545.png"></p><p><strong>在RR下使用的索引遍历到哪就把锁加到哪,即使不满足查询条件也不会释放锁,直到事务提交才释放</strong></p><blockquote>RC</blockquote><p>设置隔离级别</p><pre><code class="sql">SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@tx_isolation;</code></pre><table><thead><tr><th> </th><th align="center">T1</th><th align="center">T2</th><th align="center">T3</th></tr></thead><tbody><tr><td>1</td><td align="center">begin;<br/>update s force index (name\_idx) set s\_age = 20 where s\_name \> 'c' and s\_age \> 18;</td><td align="center"> </td><td align="center"> </td></tr><tr><td>2</td><td align="center"> </td><td align="center">select * from s where id = 1 for update;<br/>(不阻塞)</td><td align="center">select * from s where id = 20 for update;<br/>(阻塞)</td></tr><tr><td>3</td><td align="center">commit;</td><td align="center"> </td><td align="center"> </td></tr></tbody></table><p>遍历流程与RR情况相似,不同的是RC只加记录锁,并且不满足条件的记录会立即释放锁,因此T2不被阻塞,满足条件的T3被阻塞</p><p><img width="723" height="60" src="/img/bVdbt4I" alt="image-20240121160159473.png" title="image-20240121160159473.png"></p><p>加锁如下图</p><p><img width="723" height="342" src="/img/bVdbt4J" alt="image-20240121160353308.png" title="image-20240121160353308.png"></p><p><strong>遍历到哪条记录就先加锁,但是RC对于不满足查询条件的记录会释放锁</strong></p><h3>死锁案例分析</h3><p>死锁案例分析的是insert加的锁,配合上面新增加锁规则查看</p><h4>案例:新增死锁</h4><p>先将name\_idx改为唯一索引</p><table><thead><tr><th align="center"> </th><th align="center">T1</th><th align="center">T2</th></tr></thead><tbody><tr><td align="center">1</td><td align="center">begin;<br/>insert into s values (5,'bilibili',5);</td><td align="center"> </td></tr><tr><td align="center">2</td><td align="center"> </td><td align="center">insert into s values (7,'bilibili',7); <br/>(阻塞)</td></tr><tr><td align="center">3</td><td align="center">insert into s values (6,'balibali',6);</td><td align="center"> </td></tr><tr><td align="center">4</td><td align="center"> </td><td align="center">死锁 回滚</td></tr></tbody></table><p>T1插入bilibili,T2也插入bilibili,按照道理应该报错唯一键重复呀,T2怎么阻塞了呢?</p><p>T1后续再插入balibali竟然发生死锁了!啥情况呀?同学们可以先根据前面说到的insert加锁规则,大胆猜测喔\~</p><blockquote>查看最近的死锁日志</blockquote><p>需要注意的是innodb lock表中锁相关信息记录只有正在发生时才存在,像这种发生死锁,回滚事务后是看不到的,因此我们来看看死锁日志</p><p>show engine innodb status 查看innodb状态,其中有一段最近检测到的死锁 latest detected deadlock</p><p>红色框表示事务和持有/等待的锁</p><p>绿色框表示锁的信息(都是同一把X锁)</p><p><img width="723" height="483" src="/img/bVdbt4K" alt="image-20240122173149508.png" title="image-20240122173149508.png"></p><p>如果日志还是看不太懂的话,来看看下面这段分析吧(主要说name\_idx索引上的流程哈)</p><p>1、T1插入bilibili(隐式锁)</p><p>2、T2插入bilibili发生冲突,T2帮T1生成锁结构(隐式锁转化为显示锁,T1获得X record),T2要加S临键锁,先获取GAP锁(成功),再获取S锁(被T1的X record阻塞)</p><p>T1事务id为42861,T2事务id为42867:根据锁信息可以看到T2想加的S锁被T1的X锁阻塞</p><p><img width="723" height="65" src="/img/bVdbt4L" alt="image-20240122170704586.png" title="image-20240122170704586.png"></p><p>3、T1插入balibali,插入意向锁被T2的GAP锁阻塞(死锁成环:T1等待T2的GAP,T2等待T1的X)</p><p>图中蓝色与T1有关,黑色与T2有关</p><p>T1:持有[bilibili,bilibili] X锁,要插入balabala被T2的间隙锁(ai,bilibili)阻塞</p><p>T2:持有间隙锁(ai,bilibili),要插入[bilibili,bilibili]S锁被T1的[bilibili,bilibili]X锁阻塞</p><p><img width="723" height="250" src="/img/bVdbt4M" alt="image-20240122215439636.png" title="image-20240122215439636.png"></p><p>那么如何解决死锁呢?</p><p>先来看看死锁产生的四个条件:<strong>互斥、占有资源不放、占有资源继续申请资源、等待资源成环</strong></p><p>MySQL通过回滚事务的方式解决死锁,也就是解决占有资源不放</p><p>但MySQL死锁检测是非常耗费CPU的,为了避免死锁检测,我们应该在业务层面防止死锁产生</p><p>首先互斥、占有资源不放两个条件是无法破坏的,因为加锁由MySQL来实现</p><p>而破坏占有资源继续申请资源的代价可能会很大,比如:让业务层加锁处理</p><p>性价比最高的应该是破坏等待资源成环,<strong>当发生死锁时,通过分析日志、加锁规则,调整业务代码获取资源的顺序避免发生死锁</strong></p><h4>案例:相同的新增发生死锁</h4><table><thead><tr><th> </th><th align="center">T1</th><th align="center">T2</th><th align="center">T3</th></tr></thead><tbody><tr><td>1</td><td align="center">insert into s values (15,'bili',15);</td><td align="center"> </td><td align="center"> </td></tr><tr><td>2</td><td align="center"> </td><td align="center">insert into s values (15,'bili',15);<br/>(阻塞)</td><td align="center">insert into s values (15,'bili',15);<br/>(阻塞)</td></tr><tr><td>3</td><td align="center">rollback;</td><td align="center"> </td><td align="center"> </td></tr><tr><td>4</td><td align="center"> </td><td align="center"> </td><td align="center">死锁</td></tr></tbody></table><p>T1、T2、T3新增相同的记录</p><p>T1新增后,T2、T3 会帮T1生成锁结构X锁从而被阻塞</p><p>当T1回滚时,T2,T3竟然发生死锁?</p><blockquote>分析流程</blockquote><ol><li>T1 插入 加隐式锁</li><li>T2 插入相同唯一记录,帮T1生成X锁,自己获取S next key,先获取gap(成功),再获取S record(此时被T1的X锁阻塞);T3 与 T2 相似,获取到gap 再获取S record 时被T1的X阻塞</li><li>T1 回滚,T2、T3获取S record成功,此时它们都还要获取X record(插入意向锁转化为显示锁X)导致死锁成环(T2要加X锁被T3的GAP阻塞,T3要加X锁被T2的GAP阻塞)</li></ol><p>图中T2、T3都对bili加S next key锁(橙色记录和前面的黑色间隙),当它们都想加插入意向X锁(蓝色记录),同时也被各自的GAP锁阻塞</p><p><img width="723" height="239" src="/img/bVdbt4N" alt="image-20240122223007061.png" title="image-20240122223007061.png"></p><blockquote>查看死锁日志</blockquote><p><img width="723" height="537" src="/img/bVdbt4O" alt="image-20240122221910988.png" title="image-20240122221910988.png"></p><h3>总结</h3><p>本篇文章通过大量案例、图例分析不同情况下的行锁加锁规则</p><p>update、delete 先查再改,可以看成锁定读,insert则是有单独一套加锁规则</p><blockquote>锁定读加锁规则</blockquote><p><strong>在RC及以下隔离级别,锁定读使用record锁;在RR及以上隔离级别,锁定读使用next key锁</strong></p><p><strong>等值查询:如果找不到记录,该查询条件所在区间加GAP锁;如果找到记录,唯一索引临键锁退化为记录锁,非唯一索引需要扫描到第一条不满足条件的记录,最后临键锁退化为间隙锁(不在最后一条不满足条件的记录上加记录锁)</strong></p><p><strong>范围查询:非唯一索引需要扫描到第一条不满足条件的记录</strong>(5.7中唯一索引也会扫描第一条不满足条件的记录8.0修复,后文描述)</p><p><strong>在查找的过程中,使用到什么索引就在那个索引上加锁,遍历到哪条记录就给哪条先加锁</strong></p><p><strong>在RC及以下隔离级别下,查找过程中如果记录不满足当前查询条件则会释放锁;在RR及以上无论是否满足查询条件,只要遍历过记录就会加锁,直到事务提交才释放</strong></p><blockquote>insert加锁规则</blockquote><p><strong>正常情况下加锁:</strong></p><ol><li><strong>一般情况下,插入使用隐式锁(插入意向锁),不生成锁结构</strong></li><li><strong>当插入意向锁(隐式锁)被其他事务生成锁结构时变为显示锁(X record)</strong></li></ol><p><strong>重复冲突加锁:</strong></p><ol><li><strong>当insert遇到重复主键冲突时,RC及以下加S record,RR及以上加S next key</strong></li><li><strong>当insert遇到重复唯一二级索引时,加S next key</strong></li></ol><p><strong>如果使用<code>ON DUPLICATE KEY update</code>那么S锁会换成X锁</strong></p><p>外键加锁:一般不做物理外键,略...</p><h3>最后(不要白嫖,一键三连求求拉\~)</h3><p>本篇文章被收入专栏 <a href="https://link.segmentfault.com/?enc=6RDas0m%2B%2BB6rY3gRKtgkRg%3D%3D.my7Xwfze12BEVo%2FeMRgEsiPZ01O92OHDqom0SGTquX58RNeWoMIQsfPKEspQ2Otq" rel="nofollow" title="https://juejin.cn/column/7158476727939760135">MySQL进阶之路</a>,感兴趣的同学可以持续关注喔</p><p>本篇文章笔记以及案例被收入 <a href="https://link.segmentfault.com/?enc=%2FGn5Blk9I%2FRxyZXp%2FE9FoA%3D%3D.dSM9Jb5IP58QXc%2BYOMrnr%2BmR4n96Vyek8uZiPkJD3dq4VgEqq0gnwbCXEL0F8Mv7OI4KIDnYlOZhC1gXohs13cz5g9M5Qd0TVGYBKZu7%2B%2FkbDd3Ggd6UAdzeyIZT26Am" rel="nofollow" title="https://link.juejin.cn?target=https%3A%2F%2Fgitee.com%2Ftcl192243051%2FStudyJava">gitee-StudyJava</a>、 <a href="https://link.segmentfault.com/?enc=UPRb36FNJfEVVJ5Izt5PCQ%3D%3D.2SH%2FqI5oqbhv2DdfnceqU2OMLOqPGBM%2B6e6%2BzKjAwJeKITV1y756tqkcdnR2Xr0CH5ttbipEfgpDW1EBurHJlZspeThZmz8YqaNAgIlIuPo%3D" rel="nofollow" title="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FTc-liang%2FStudyJava">github-StudyJava</a> 感兴趣的同学可以stat下持续关注喔\~</p><p>有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下\~</p><p>关注菜菜,分享更多干货,公众号:菜菜的后端私房菜</p><blockquote>本文由博客一文多发平台 <a href="https://link.segmentfault.com/?enc=YG60BqSBcSxRfZzoZQ%2FjkQ%3D%3D.pUVeUVBdlJw8%2FpxhJkGn%2F9Vkgpk3%2BUrsyJmkzWytRuRqoaIUCj5y7RF%2BYcbRhSC5" rel="nofollow">OpenWrite</a> 发布!</blockquote>
通俗易懂剖析Go Channel:理解并发通信的核心机制
https://segmentfault.com/a/1190000044644615
2024-02-21T11:03:59+08:00
2024-02-21T11:03:59+08:00
王中阳Go
https://segmentfault.com/u/wangzhongyang_go
3
<p>本文来自 <a href="https://link.segmentfault.com/?enc=XoEiCR5F8DQXqm03%2BxhB%2Bg%3D%3D.xOoDEl91ivOHRUTEyN2NIuuf1mh%2FyyUxi1Q1iX01dMY0RvuamNcxSeAWbw0uUTGf1eP12FOLzvEncCjWvQ3xoRdeFNYEAqCanTfWlqn9wxk1kPvdx7nPeomsN%2FUKGnIf4Q2VH2nogUKjvAwA%2FPXCgz6TbToS37Zij8CexW3LQ0kiAktGQd8JNUNLnb7SprfOi%2FT286LwRD72oemTwwegVmnCr4nsDR8OSYG4ZAGUKcitBDZxbN8ApvfWD2EJIpt9NQANkFPCNMZeozBXNHey%2BEYKCOOtFnIAzmXWeW7ILXE%3D" rel="nofollow">Go就业训练营</a> 小韬同学的投稿。</p><p><strong>也强烈安利大家多写博客,不仅能倒逼自己学习总结,也能作为简历的加分项,提高求职面试的竞争力。</strong></p><p>你想想看:面试官看到你简历中的博客主页有几十篇文章,几千粉丝是什么感觉。<strong>要比你空洞洞的写一句“热爱技术”强太多啦!</strong></p><h2>正文</h2><p>我们在学习与使用Go语言的过程中,对<code>channel</code>并不陌生,<code>channel</code>是Go语言与众不同的特性之一,也是非常重要的一环,深入理解<code>Channel</code>,相信能够在使用的时候更加的得心应手。</p><h3>一、Channel基本用法</h3><h4>1、channel类别</h4><p><code>channel</code>在类型上,可以分为两种: <br>+ <strong>双向channel</strong>:既能接收又能发送的<code>channel</code> <br>+ <strong>单向channel</strong>:只能发送或只能接收的<code>channel</code>,即单向<code>channel</code>可以为分为: <br> + <code>只写channel</code> <br> + <code>只读channel</code> </p><p>声明并初始化如下如下:</p><pre><code class="go">func main() {
// 声明并初始化
var ch chan string = make(chan string) // 双向channel
var readCh <-chan string = make(<-chan string) // 只读channel
var writeCh chan<- string = make(chan<- string) // 只写channel
} </code></pre><p>上述定义中,<code><-</code>表示单向的<code>channel</code>。如果箭头指向<code>chan</code>,就表示只写<code>channel</code>,可以往<code>chan</code>里边写入数据;如果箭头远离<code>chan</code>,则表示为只读<code>channel</code>,可以从<code>chan</code>读数据。 </p><p>在定义channel时,可以定义任意类型的channel,因此也同样可以定义chan类型的channel。例如:</p><pre><code class="go">a := make(chan<- chan int) // 定义类型为 chan int 的写channel
b := make(chan<- <-chan int) // 定义类型为 <-chan int 的写channel
c := make(<-chan <-chan int) // 定义类型为 <-chan int 的读channel
d := make(chan (<-chan int)) // 定义类型为 (<-chan int) 的读channel </code></pre><p>当<code>channel</code>未初始化时,其<strong>零值为<code>nil</code></strong>。<strong>nil 是 chan 的零值,是一种特殊的 chan,对值是 nil 的 chan 的发送接收调用者总是会阻塞。</strong></p><pre><code class="go">func main() {
var ch chan string
fmt.Println(ch) // <nil>
} </code></pre><p>通过<code>make</code>我们可以初始化一个channel,并且可以设置其容量的大小,如下初始化了一个类型为<code>string</code>,其容量大小为<code>512</code>的<code>channel</code>:</p><pre><code class="go">var ch chan string = make(chan string, 512) </code></pre><p>当初始化定义了<code>channel</code>的容量,则这样的<code>channel</code>叫做<code>buffered chan</code>,即<strong>有缓冲<code>channel</code></strong>。如果没有设置容量,<code>channel</code>的容量为0,这样的<code>channel</code>叫做<code>unbuffered chan</code>,即<strong>无缓冲<code>channel</code></strong>。 </p><p>有缓冲<code>channel</code>中,如果<code>channel</code>中还有数据,则从这个<code>channel</code>接收数据时不会被阻塞。如果<code>channel</code>的容量还未满,那么向这个<code>channel</code>发送数据也不会被阻塞,反之则会被阻塞。 </p><p>无缓冲<code>channel</code>则只有当读写操作都准备好后,才不会阻塞,这也是<code>unbuffered chan</code>在使用过程中非常需要注意的一点,否则可能会出现常见的bug。 </p><p><strong>channel的常见操作:</strong> </p><p>1. 发送数据 </p><p>往channel发送一个数据使用<code>ch <-</code></p><pre><code class="go">func main() {
var ch chan int = make(chan int, 512)
ch <- 2000
} </code></pre><p>上述的<code>ch</code>可以是<code>chan int</code>类型,也可以是单向<code>chan <-int</code>。 </p><p>2. 接收数据 </p><p>从channel接收一条数据可以使用<code><-ch</code></p><pre><code class="go">func main() {
var ch chan int = make(chan int, 512)
ch <- 2000 // 发送数据
data := <-ch // 接收数据
fmt.Println(data) // 2000
} </code></pre><p>ch 类型是 <code>chan T</code>,也可以是单向<code><-chan T</code> </p><p>在接收数据时,可以返回两个返回值。<strong>第一个返回值返回<code>channel</code>中的元素,第二个返回值为<code>bool</code>类型</strong>,表示是否成功地从<code>channel</code>中读取到一个值。 </p><p>如果第二个参数是<code>false</code>,则<strong>表示<code>channel</code>已经被<code>close</code>而且<code>channel</code>中没有缓存的数据</strong>,这个时候第一个值返回的是零值。</p><pre><code class="go">func main() {
var ch chan int = make(chan int, 512)
ch <- 2000 // 发送数据
data1, ok1 := <-ch // 接收数据
fmt.Printf("data1 = %d, ok1 = %t\n", data1, ok1) // data1 = 2000, ok1 = true
close(ch) // 关闭channel
data2, ok2 := <-ch // 接收数据
fmt.Printf("data2 = %d, ok2 = %t", data2, ok2) // data2 = 0, ok2 = false
} </code></pre><p>所以,如果从<code>channel</code>读取到一个零值,可能是发送操作真正发送的零值,也可能是<code>closed</code>关闭<code>channel</code>并且<code>channel</code>没有缓存元素产生的零值,这是需要注意判别的一个点。 </p><p>3. 其他操作 </p><p>Go内建的函数<code>close</code>、<code>cap</code>、<code>len</code>都可以对<code>chan</code>类型进行操作。 <br>+ <code>close</code>:关闭channel。 <br>+ <code>cap</code>:返回channel的容量。 <br>+ <code>len</code>:返回channel缓存中还未被取走的元素数量。</p><pre><code class="go">func main() {
var ch chan int = make(chan int, 512)
ch <- 100
ch <- 200
fmt.Println("ch len:", len(ch)) // ch len: 2
fmt.Println("ch cap:", cap(ch)) // ch cap: 512
} </code></pre><p><strong>发送操作</strong>与<strong>接收操作</strong>可以作为<code>select</code>语句中的<code>case clause</code>,例如:</p><pre><code class="go">func main() {
var ch = make(chan int, 512)
for i := 0; i < 10; i++ {
select {
case ch <- i:
case v := <-ch:
fmt.Println(v)
}
}
} </code></pre><p><code>for-range</code>语句同样可以在<code>chan</code>中使用,例如:</p><pre><code class="go">func main() {
var ch = make(chan int, 512)
ch <- 100
ch <- 200
ch <- 300
for v := range ch {
fmt.Println(v)
}
}
// 执行结果
100
200
300 </code></pre><h4>2、select介绍</h4><p>在Go语言中,<code>select</code>语句用于监控一组<code>case</code>语句,根据特定的条件执行相对应的<code>case</code>语句或<code>default</code>语句,与<code>switch</code>类似,但不同之处在于<code>select</code>语句中所有<code>case</code>中的表达式都必须是<code>channel</code>的发送或接收操作。<code>select</code>使用示例代码如下:</p><pre><code class="go">select {
case <-ch1:
fmt.Println("ch1")
case ch2 <- 1:
fmt.Println("ch2")
} </code></pre><p>上述代码中,<code>select</code>关键字让当前<code>goroutine</code>同时等待<code>ch1</code> 的可读和<code>ch2</code>的可写,在满足任意一个<code>case</code>分支之前,<code>select</code> 会一直阻塞下去,直到其中的一个 <code>channel</code> 转为就绪状态时执行对应<code>case</code>分支的代码。如果多个<code>channel</code>同时就绪的话则随机选择一个<code>case</code>执行。 </p><p>当使用空<code>select</code>时,空的 <code>select</code> 语句会直接阻塞当前的<code>goroutine</code>,使得该<code>goroutine</code>进入无法被唤醒的永久休眠状态。空<code>select</code>,即<code>select</code>内不包含任何<code>case</code>。</p><pre><code class="go1">select{
} </code></pre><p>另外当<code>select</code>语句内只有一个<code>case</code>分支时,如果该<code>case</code>分支不满足,那么当前<code>select</code>就变成了一个阻塞的<code>channel</code>读/写操作。</p><pre><code class="go">select {
case <-ch1:
fmt.Println("ch1")
} </code></pre><p>上述<code>select</code>中,当<code>ch1</code>可读时,会执行打印操作,反之则阻塞当前<code>goroutine</code>。 </p><p>当<code>select</code>语句内包含<code>default</code>分支时,如果<code>select</code>内的所有<code>case</code>都不满足,则会执行<code>default</code>分支的逻辑,用于当其他<code>case</code>都不满足时执行一些默认操作。</p><pre><code class="go">select {
case <-ch1:
fmt.Println("ch1")
case ch2 <- 1:
fmt.Println("ch2")
default:
fmt.Println("default")
} </code></pre><p>上述代码中,当<code>ch1</code>可读或<code>ch2</code>可写时,会执行相应的打印操作,否则就执行<code>default</code>语句中的代码,相当于一个非阻塞的<code>channel</code>读取操作。 </p><p><code>select</code>的使用可以总结为: </p><p>+ <code>select</code>不存在任何的<code>case</code>且没有<code>default</code>分支:永久阻塞当前 goroutine; <br>+ <code>select</code>只存在一个<code>case</code>且没有<code>default</code>分支:阻塞的发送/接收; <br>+ <code>select</code>存在多个<code>case</code>:随机选择一个满足条件的<code>case</code>执行; <br>+ <code>select</code>存在<code>default</code>,其他<code>case</code>都不满足时:执行<code>default</code>语句中的代码;</p><h3>二、Channel实现原理</h3><p>从代码的角度剖析<code>channel</code>的实现,能够让我们更好的去使用<code>channel</code>。 </p><p>我们可以从<code>chan</code>类型的数据结构、初始化以及三个操作发送、接收和关闭这几个方面来了解<code>channel</code>。</p><h4>1、chan数据结构</h4><p>chan类型的数据结构定义位于<a href="https://link.segmentfault.com/?enc=BNr%2FkZqgGkwpHb6DjTZ2lg%3D%3D.eMqoO2mmQsUqhwNOshrStztKFLLuP9KcxGWLH9dzW31H3Hjuhb7p1yZYVr%2BCqot6mgtWe0zLf4dbiLZAWt6O%2FMDcgVlcN5ckkSRzgrJJMqk%3D" rel="nofollow">runtime.hchan</a>,其结构体定义如下:</p><pre><code class="go">type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
} </code></pre><p>解释一下上述各个字段的意义: </p><p>+ <code>qcount</code>:表示<code>chan</code>中已经接收到的数据且还未被取走的元素个数。内建函数<code>len</code>可以返回这个字段的值。 <br>+ <code>datasiz</code>:循环队列的大小。<code>chan</code>在实现上使用一个循环队列来存放元素的个数,循环队列适用于生产者-消费者的场景。 <br>+ <code>buf</code>:存放元素的循环队列<code>buffer</code>,<code>buf</code> 字段是一个指向队列缓冲区的指针,即指向一个<code>dataqsiz</code>元素的数组。<code>buf</code> 字段是使用 <code>unsafe.Pointer</code> 类型来表示队列缓冲区的起始地址。<code>unsafe.Pointer</code>是一种特殊的指针类型,它可以用于指向任何类型的数据。由于队列缓冲区的类型是动态分配的,所以不能直接使用某个具体类型的指针来表示。 <br>+ <code>elemtype</code>、<code>elemsize</code>:<code>elemtype</code>表示chan中元素的数据类型,<code>elemsize</code>表示其大小。当chan定义后,它的元素类型是固定的,即普通类型或者指针类型,因此元素大小也是固定的。 <br>+ <code>sendx</code>:处理发送数据操作的指针在buf队列中的位置。当channel接收到了新的数据时,该指针就会加上<code>elemsize</code>,移动到下一个位置。<code>buf</code> 的总大小是<code>elemsize</code>的整数倍且<code>buf</code>是一个循环列表。 <br>+ <code>recvx</code>:处理接收数据操作的指针在<code>buf</code>队列中的位置。当从buf中取出数据,此指针会移动到下一个位置。 <br>+ <code>recvq</code>:当接收操作发现<code>channel</code>中没有数据可读时,会被则色,此时会被加入到<code>recvq</code>队列中。 <br>+ <code>sendq</code>:当发送操作发现<code>buf</code>队列已满时,会被进行阻塞,此时会被加入到<code>sendq</code>队列中。 </p><p><p align=center><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ddc1b911e2ac40b9ad73bff9647e4dc0~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=395&h=524&s=37043&e=png&b=fbf7f6" alt="image.png" /></p></p><h4>2、chan初始化</h4><p><code>channel</code>在进行初始化时,Go编译器会根据是否传入容量的大小,来选择调用<code>makechan64</code>,还是<code>makechan</code>。<code>makechan64</code>在实现上底层还是调用<code>makechan</code>来进行初始化,<code>makechan64</code>只是对<code>size</code>做了检查。 </p><p><code>makechan</code>函数根据<code>chan</code>的容量的大小和元素的类型不同,初始化不同的存储空间。省略一些检查代码,<code>makechan</code>函数的主要逻辑如下:</p><pre><code class="go">func makechan(t *chantype, size int) *hchan {
elem := t.elem
...
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
...
var c *hchan
switch {
case mem == 0:
// 队列或元素大小为零,不必创建buf
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// 元素不包含指针,分配一块连续的内存给hchan数据结构和buf
// hchan数据结构后面紧接着就是buf,在一次调用中分配hchan和buf
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 元素包含指针,单独分配buf
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// 记录元素大小、类型、容量
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
...
return c
} </code></pre><h4>3、send发送操作</h4><p>Go在编译发送数据给<code>channel</code>时,会把发送操作<code>send</code>转换成<code>chansend1</code>函数,而<code>chansend1</code>函数会调用<code>chansend</code>函数。</p><pre><code class="go">func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
} </code></pre><p>我们可以来分段分析<code>chansend</code>函数的实现逻辑。 </p><p><strong>第一部分:</strong> </p><p>主要是对<code>chan</code>进行判断,判断<code>chan</code>是否为<code>nil</code>,若为<code>nil</code>,则判断是否需要将当前<code>goroutine</code>进行阻塞,阻塞通过<code>gopark</code>来对调用者<code>goroutine park</code>(阻塞休眠)。</p><pre><code class="go">func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 第一部分
if c == nil { // 判断chan是否为nil
if !block { // 判断是否需要阻塞当前goroutine
return false
}
// 调用这goroutine park,进行阻塞休眠
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
...
} </code></pre><p><strong>第二部分</strong> </p><p>第二部分的逻辑判断是当你往一个容量已满的<code>chan</code>实例发送数据,且不想当前调用的<code>goroutine</code>被阻塞时(<code>chan</code>未被关闭),那么处理的逻辑是直接返回。</p><pre><code class="go">func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 第二部分
if !block && c.closed == 0 && full(c) {
return false
}
...
} </code></pre><p><strong>第三部分</strong> </p><p>第三部分的逻辑判断是首先进行互斥锁加锁,然后判断当前<code>chan</code>是否关闭,如果<code>chan</code>已经被<code>close</code>了,则释放互斥锁并<code>panic</code>,即对已关闭的<code>chan</code>发送数据会<code>panic</code>。</p><pre><code class="go">func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 第三部分
lock(&c.lock) // 开始加锁
if c.closed != 0 { // 判断channel是否关闭
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
...
} </code></pre><p><strong>第四部分</strong> </p><p>第四部分的逻辑主要是判断接收队列中是否有正在等待的接收方<code>receiver</code>。如果存在正在等待的<code>receiver</code>(说明此时<code>buf</code>中没有缓存的数据),则将他从接收队列中弹出,直接将需要发送到<code>channel</code>的数据交给这个<code>receiver</code>,而无需放入到<code>buf</code>中,让发送操作速度更快一些。</p><pre><code class="go">func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 第四部分
if sg := c.recvq.dequeue(); sg != nil {
// 找到了一个正在等待的接收者。我们传递我们想要发送的值
// 直接传递给receiver接收者,绕过channel buf缓存区(如果receiver有的话)
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
...
} </code></pre><p><strong>第五部分</strong> </p><p>当等待队列中并没有正在等待的<code>receiver</code>,则说明当前<code>buf</code>还没有满,此时将发送的数据放入到<code>buf</code>中。</p><pre><code class="go">func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 第五部分
if c.qcount < c.dataqsiz { // 判断buf是否满了
// channel buf还有可用的空间. 将发送数据入buf循环队列.
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
...
} </code></pre><p><strong>第六部分</strong> </p><p>当逻辑走到第六部分,说明正在处理<code>buf</code>已满的情况。如果<code>buf</code>已满,则发送操作的<code>goroutine</code>就会加入到发送者的等待队列,直到被唤醒。当<code>goroutine</code>被唤醒时,数据或者被取走了,或者<code>chan</code>已经被关闭了。</p><pre><code class="go">func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 第六部分
// chansend1函数调用不会进入if块里,因为chansend1的block=true
if !block {
unlock(&c.lock)
return false
}
...
c.sendq.enqueue(mysg) // 加入发送队列
...
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2) // 阻塞
...
} </code></pre><h4>4、recv接收操作</h4><p>从<code>channel</code>中接收数据时,Go会将代码转换成<code>chanrecv1</code>函数。如果需要返回两个返回值,则会转换成<code>chanrecv2</code>,<code>chanrecv1</code>函数和<code>chanrecv2</code>都会调用<code>chanrecv</code>函数。<code>chanrecv1</code>和<code>chanrecv2</code>传入的 <code>block</code>参数的值是<code>true</code>,两种调用都是阻塞方式,因此在分析<code>chanrecv</code>函数的实现时,可以不考虑 <code>block=false</code>的情况。</p><pre><code class="go">// 从已编译代码中进入 <-c 的入口点
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
} </code></pre><p>同样,省略一些检查类的代码,我们也可以分段分析<code>chanrecv</code>函数的逻辑。 </p><p><strong>第一部分</strong> </p><p>第一部分主要判断当前进行接收操作的<code>chan</code>实例是否为<code>nil</code>,若为<code>nil</code>,则从<code>nil chan</code>中接收数据的调用这<code>goroutine</code>会被阻塞。</p><pre><code class="go">func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 第一部分
if c == nil { // 判断chan是否为nil
if !block { // 是否阻塞,默认为block=true
return
}
// 进行阻塞
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
...
} </code></pre><p><strong>第二部分</strong> <br>这一部分只要是考虑<code>block=false</code>且<code>c</code>为空的情况,<code>block=false</code>的情况我们可以不做考虑。</p><pre><code class="go">func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 检查未获得锁的失败非阻塞操作。
if !block && empty(c) {
...
}
...
} </code></pre><p><strong>第三部分</strong> </p><p>第三部分的逻辑为判断当前<code>chan</code>是否被关闭,若当前<code>chan</code>已经被<code>close</code>了,并且缓存队列中没有缓冲的元素时,返回<code>true</code>、<code>false</code>。</p><pre><code class="go">func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
lock(&c.lock) // 加锁,返回时释放锁
// 第三部分
if c.closed != 0 { // 当chan已被关闭时
if c.qcount == 0 { // 且 buf区 没有缓存的数据了
...
unlock(&c.lock) // 解锁
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
}
...
} </code></pre><p><strong>第四部分</strong> </p><p>第四部分是处理通道未关闭且<code>buf</code>缓存队列已满的情况。只有当缓存队列已满时,才能够从发送等待队列获取到<code>sender</code>。若当前的<code>chan</code>为<code>unbuffer</code>的<code>chan</code>,即<code>无缓冲区channel</code>时,则直接将<code>sender</code>的发送数据传递给<code>receiver</code>。否则就从缓存队列的头部读取一个元素值,并将获取的<code>sender</code>携带的值加入到<code>buf</code>循环队列的尾部。</p><pre><code class="go">func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
if c.closed != 0 { // 当chan已被关闭时
} else { // 第四部分,通道未关闭
// 如果sendq队列中有等待发送的sender
if sg := c.sendq.dequeue(); sg != nil {
// 存在正在等待的sender,如果缓存区的容量为0则直接将发送方的值传递给接收方
// 反之,则从缓存队列的头部获取数据,并将获取的sender的发送值加入到缓存队列尾部
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
}
...
} </code></pre><p><strong>第五部分</strong> </p><p>第五部分的主要逻辑是处理发送队列中没有等待的<code>sender</code>且<code>buf</code>中有缓存的数据。该段逻辑与外出的互斥锁共用一把锁,因此不存在并发问题。当<code>buf</code>缓存区有缓存元素时,则取出该元素传递给<code>receiver</code>,同时移动接收指针。</p><pre><code class="go">func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
// 第五部分
if c.qcount > 0 { // 发送队列中没有等待的sender,且buf中有缓存数据
// 直接从缓存队列中获取数据
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++ // 移动接收指针
if c.recvx == c.dataqsiz { // 指针若已到末尾则进行重置(循环队列)
c.recvx = 0
}
c.qcount-- // 获取数据后,buf缓存区元素个数减一
unlock(&c.lock) // 解锁
return true, true
}
if !block { // block=true
unlock(&c.lock)
return false, false
}
...
} </code></pre><p><strong>第六部分</strong> </p><p>第六部分的逻辑主要是处理<code>buf</code>缓存区中没有缓存数据的情况。当<code>buf</code>缓存区没有缓存数据时,那么当前的<code>receiver</code>就会被阻塞,直到它从<code>sender</code>中接收了数据,或者是<code>chan</code>被<code>close</code>,才会返回。</p><pre><code class="go">func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
c.recvq.enqueue(mysg) // 将当前接收操作入接收队列
...
// 进行阻塞,等待唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
...
} </code></pre><h4>5、close关闭</h4><p><code>close</code>函数主要用于<code>channel</code>的关闭,Go编译器会替换成<code>closechan</code>函数的调用。省略一些检查下的代码后,<code>closechan</code>函数的主要逻辑如下: <br>+ 如果当前<code>chan</code>为<code>nil</code>,则直接<code>panic</code> <br>+ 如果当前<code>chan</code>已关闭,再次<code>close</code>则直接<code>panic</code> <br>+ 如果<code>chan</code>不为<code>nil</code>,<code>chan</code>也没有<code>closed</code>,就把等待队列中的<code> sender(writer)</code>和 <code>receiver(reader)</code>从队列中全部移除并唤醒。</p><pre><code class="go">func closechan(c *hchan) {
if c == nil { // 若当前chan未nil,则直接panic
panic(plainError("close of nil channel"))
}
lock(&c.lock) // 加锁
if c.closed != 0 { // 若当前chan已经关闭,则直接panic
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
...
c.closed = 1 // 设置当前channel的状态为已关闭
var glist gList
// 释放接收队列中所有的reader
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// 释放发送队列中所有的writer (它们会panic)
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
} </code></pre><h3>三、总结</h3><p>通过学习<code>channel</code>的基本使用,了解其操作背后的实现原理,可以帮助我们更好的使用<code>channel</code>,避免一些操作不当而导致的<code>panic</code>或者说是<code>bug</code>,让我们在使用<code>channel</code>时能够更加的得心应手。 </p><p><code>channel</code>的值和状态有多种情况,而不同的操作<code>(send、recv、close)</code>又可能得到不同的结果,这是使用 <code>channel</code> 类型时需要经常注意的点,我们可以将不同<code>channel</code>值下的不同操作进行一个总结,<strong>特别注意操作<code>channel</code>时会产生<code>panic</code>的情况,已经可能会导致线程阻塞的情况</strong>,都是有可能导致死锁与<code>goroutine</code>泄漏的罪魁祸首。 </p><p>| channel执行操作\channel状态 | channel为nil | channel buf为空 | channel buf已满 | channel buf未满且不为空 | channel已关闭 | <br>| --------------------------- | ------------ | ------------------------------ | ----------------------------- | ----------------------------- | ------------------- | <br>| <code>receive</code>接收操作 | 阻塞 | 阻塞 | 读取数据 | 读取数据 | 返回buf中缓存的数据 | <br>| <code>send</code>发送操作 | 阻塞 | 写入数据 | 阻塞 | 写入数据 | <strong>panic</strong> | <br>| <code>close</code>关闭 | <strong>panic</strong> | 关闭channel,buf中没有缓存数据 | 关闭channel,保留已缓存的数据 | 关闭channel,保留已缓存的数据 | <strong>panic</strong></p><h3>又出成绩啦</h3><p><a href="https://link.segmentfault.com/?enc=1oFkZ7vJP%2BmrWBBLqQuC7w%3D%3D.YF5DFoj8BqCiCugA7ak5g6gqZypA1M120f%2BS9DsUULtUXmT3Gibg7ODrQiio%2B1zPzTUjlr%2Boxb3lY7v3CLrv3A%3D%3D" rel="nofollow">我们又出成绩啦!大厂Offer集锦!遥遥领先!</a></p><p><a href="https://link.segmentfault.com/?enc=EqPciIAonBqjSIAI%2F2rtFQ%3D%3D.tooOrnxIu3wADouTZmNDKjFNK%2B%2B9v7i98wmpeNVkyOHPWb%2BWE11utQI6q2I8KX1yH11Lr1aRD18Q%2BeREVGMQyQ%3D%3D" rel="nofollow">这些朋友赢麻了!</a> </p><p><a href="https://link.segmentfault.com/?enc=YvJ3DnKb9fnCzKEqEeFmAw%3D%3D.ZUZbokZT0gNSGQBMAeKwDUi70WUzfh1KudgogbiGD9pZjffyQ95MMNJ1WbXYGuR8OW22IjuUrWu3PHknJ6aMqQ%3D%3D" rel="nofollow">这是一个专注程序员升职加薪の知识星球</a></p><h3>答疑解惑</h3><p>需要「简历优化」、「就业辅导」、「职业规划」的朋友可以联系我。</p><p>加我微信:<strong>wangzhongyang1993</strong></p><p>关注我的同名公众号:<a href="https://link.segmentfault.com/?enc=yl3oIQEImBSL1WIawieEJA%3D%3D.D9vMIctXyfXpLruk5V9AiZg1U6ya7q6mSz7DfpRr9Ng4Hd8OhCWwK0cl%2Fzg%2FeadHAfVEVX24KJQxJn6rvttOMQ%3D%3D" rel="nofollow">王中阳Go</a></p>
TypeScript中,interface和type使用上有什么区别?
https://segmentfault.com/a/1190000044651935
2024-02-23T13:11:07+08:00
2024-02-23T13:11:07+08:00
方始终
https://segmentfault.com/u/fangsz
2
<p>TypeScript 是 JavaScript 的一个超集,通过为 JavaScript 提供类型系统和其他语言特性来增强 JavaScript 的功能(配合VSCode,代码提示真的丝滑)。TypeScript 可以在编译时进行类型检查,从而提供更好的代码可读性和可维护性,并且可以在开发过程中减少错误和调试时间),减少了很多低级语法错误,这类问题有时候排查好久才会发现,查到的时候往往会忍不住骂娘。</p><p>我使用TypeScript的时长差不多两年半了,在使用的初期,我便注意到在 TypeScript 中,interface 和 type 都可以用来定义对象的类型。</p><pre><code>// 使用 interface 定义对象类型
interface User {
name: string;
age: number;
}
// 使用 type 定义对象类型
type User = {
name: string;
age: number;
}</code></pre><p>上面定义的两个User对象类型,效果是等效的,在声明对象类型,函数的参数和返回类型等最常用的场景中使用起来效果没有任何差别。</p><p>但是二者的使用方式其实还是有挺多区别,花几分钟了解之后,还是可以在小白面前装一下的。</p><p>哈哈,玩笑玩笑,最终还是为了更好地使用工具。</p><p>总结起来包括了6点,TypeScript的官方文档中也有部分描述,点击末尾原文链接可以跳转官方文档(这里提一句,TS的官方文档写得真好,强烈建议读英文原版文档,一开始可能有点慢,但是很快就适应过来了)。</p><h2>1. interface 可以被类实现和扩展,而 type 不行</h2><p>下面的例子中,用interface声明了Animal,用type声明了Animal2,当我试图实现(implements)Animal2的时候,就报错了。</p><pre><code>
interface Animal {
name: string;
eat(): void;
}
type Animal2 {
name: string;
eat(): void;
}
class Cat implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
}
// 错啦,type定义的对象类型不能被实现
class Dog implements Animal2 {
name: string;
constructor(name: string) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
}</code></pre><p>interface就是用来实现的,就像信任就是用来辜负的一样。</p><h2>2. 同名interface 可以被合并,而 type 不行。</h2><p>在同一作用域内定义了两个相同名称的 interface,TypeScript 会将它们合并为一个。但是如果定义了两个相同名称的 type,则会产生命名冲突错误。</p><pre><code>
interface A {
name: string;
}
interface A {
age: number;
}
// A 接口被合并为 { name: string; age: number; }
const a: A = {
name: 'Jack',
age: 20
}
// Error: Duplicate identifier 'B'.
type B = {
name: string;
}
type B = {
age: number;
}</code></pre><h2>3. type可以用于声明组合类型和交叉类型,interface则不行</h2><p>下面这个case就是用type声明了组合类型和交叉类型。</p><pre><code>interface InterfaceA {
key1: string;
}
interface InterfaceB {
key2: number;
}
type UnionType = InterfaceA | InterfaceB;
type IntersectionType = InterfaceA & InterfaceB;
const obj1: UnionType = { key1: 'hello' }; // 符合 InterfaceA
const obj2: UnionType = { key2: 42 }; // 符合 InterfaceB
const obj: IntersectionType = { key1: 'hello', key2: 42 }; // 同时符合 InterfaceA 和 InterfaceB</code></pre><h2>4. type声明的对象类型可以拿来组合成新的对象类型,interface不行</h2><pre><code>type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
const bear = getBear();
bear.name;
bear.honey;</code></pre><h2>5. 在定义对象类型时,interface 和 type 的语法略有不同。interface 使用花括号 {},而 type 使用等号 =。</h2><p>这点有点凑数,但是确实是新手日常写代码经常混淆的点</p><pre><code>// interface 使用花括号 {} 定义对象类型
interface User {
name: string;
age: number;
}
// type 使用等号 = 定义对象类型
type User = {
name: string;
age: number;
}</code></pre><h2>6. type可以给基本类型起别名,interface不行</h2><pre><code>
type StringTypeHAHAHHAHA = string;
// interface做不到</code></pre><p>——分割线————————————————</p><p>列举出来一看,差别还是挺多的,但是这些点其实没有必要去记忆,因为TypeScript的工具链相当完善,不合适的用法编辑器都会有清晰的提示。</p><p>在实际应用中,除了功能性的刚性约束,更重要的是用类型去表达一个准确的语义,事实上许多复杂类型需要组合使用type和interface才能实现。</p><p>本文权当做了一个趣味探索,有其他关于TypeScript好玩的点,欢迎留言交流。</p>
JavaScript 的新数组分组方法
https://segmentfault.com/a/1190000044647088
2024-02-21T22:03:30+08:00
2024-02-21T22:03:30+08:00
chuck
https://segmentfault.com/u/chuckqu
4
<p>对数组中的项目进行分组,你可能已经做过很多次了。每次都会手动编写一个分组函数,或者使用 <code>lodash</code> 的 <code>groupBy</code> 函数。</p><p>好消息是,JavaScript 现在有了分组方法,所以你再也不必这样做了。<code>Object.groupBy</code> 和 <code>Map.groupBy</code> 这两个新方法将使分组变得更简单,并节省我们的时间或依赖性。</p><h2>以前的做法</h2><p>假设你有一个代表人的对象数组,你想按年龄对它们进行分组。你可以这样使用 <code>forEach</code> 循环:</p><pre><code class="jsx">const people = [
{ name: "Alice", age: 28 },
{ name: "Bob", age: 30 },
{ name: "Eve", age: 28 },
];
const peopleByAge = {};
people.forEach((person) => {
const age = person.age;
if (!peopleByAge[age]) {
peopleByAge[age] = [];
}
peopleByAge[age].push(person);
});
console.log(peopleByAge);
/*
{
"28": [{"name":"Alice","age":28}, {"name":"Eve","age":28}],
"30": [{"name":"Bob","age":30}]
}
*/</code></pre><p>或者可以像这样来使用<code>reduce</code>:</p><pre><code class="jsx">const peopleByAge = people.reduce((acc, person) => {
const age = person.age;
if (!acc[age]) {
acc[age] = [];
}
acc[age].push(person);
return acc;
}, {});</code></pre><p>无论哪种方法,代码都略显笨拙。你总是要检查对象是否存在分组键,如果不存在,就用一个空数组来创建它。然后再将项目推入数组。</p><h2>使用Object.groupBy</h2><p>有了新的 <code>Object.groupBy</code> 方法,你就可以像这样得出结果:</p><pre><code class="jsx">const peopleByAge = Object.groupBy(people, (person) => person.age);</code></pre><p>简单多了!不过也有一些需要注意的地方。</p><p><code>Object.groupBy</code> 返回一个空原型对象。这意味着该对象不继承 <code>Object.prototype</code> 的任何属性。这很好,因为这意味着你不会意外覆盖 <code>Object.prototype</code> 上的任何属性,但这也意味着该对象没有你可能期望的任何方法,如 <code>hasOwnProperty</code> 或 <code>toString</code>。</p><pre><code>const peopleByAge = Object.groupBy(people, (person) => person.age);
console.log(peopleByAge.hasOwnProperty("28"));
// TypeError: peopleByAge.hasOwnProperty is not a function</code></pre><p>传递给 <code>Object.groupBy</code> 的回调函数应返回字符串或<code>Symbol</code>。如果返回其他内容,则将强制转为字符串。</p><p>在我们的示例中,我们一直以数字形式返回<code>age</code>,但在结果中却被强制转为字符串。尽管如此,你仍然可以使用数字访问属性,因为使用方括号符号也会将参数强制为字符串。</p><pre><code class="jsx">console.log(peopleByAge[28]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]
console.log(peopleByAge["28"]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]</code></pre><h2>使用Map.groupBy</h2><p>除了返回 <code>Map</code> 之外,<code>Map.groupBy</code> 的功能与 <code>Object.groupBy</code> 几乎相同。这意味着你可以使用所有常用的 <code>Map</code> 函数。这也意味着你可以从回调函数返回任何类型的值。</p><pre><code class="jsx">const ceo = { name: "Jamie", age: 40, reportsTo: null };
const manager = { name: "Alice", age: 28, reportsTo: ceo };
const people = [
ceo,
manager,
{ name: "Bob", age: 30, reportsTo: manager },
{ name: "Eve", age: 28, reportsTo: ceo },
];
const peopleByManager = Map.groupBy(people, (person) => person.reportsTo);</code></pre><p>在本例中,我们是按照向谁汇报工作来对人员进行分组的。请注意,要从该 Map 中按对象检索项目,对象必须具有相同的引用。</p><pre><code class="jsx">peopleByManager.get(ceo);
// => [{ name: "Alice", age: 28, reportsTo: ceo }, { name: "Eve", age: 28, reportsTo: ceo }]
peopleByManager.get({ name: "Jamie", age: 40, reportsTo: null });
// => undefined</code></pre><p>在上面的示例中,第二行使用了一个看起来像 <code>ceo</code> 对象的对象,但它并不是同一个对象,因此它不会从 <code>Map</code> 中返回任何内容。要想成功地从 <code>Map</code> 中获取项目,请确保你保留了要用作键的对象的引用。</p><h2>何时可用</h2><p>这两个 <code>groupBy</code> 方法是 TC39 提议的一部分,目前处于第三阶段。这意味着它很有可能成为一项标准,因此也出现了一些实施方案。</p><p>Chrome 浏览器 117 版本刚刚推出了对这两种方法的支持,而 Firefox 浏览器 119 版本也发布了对这两种方法的支持。Safari 以不同的名称实现了这些方法,我相信他们很快就会更新。既然 Chrome 浏览器中出现了这些方法,就意味着它们已在 V8 中实现,因此下次 V8 更新时,Node 中也会出现这些方法。</p><h2>为什么使用静态方法</h2><p>你可能会问,为什么要以 <code>Object.groupBy</code> 而不是 <code>Array.prototype.groupBy</code> 的形式来实现呢?根据该提案,有一个库曾经用一个不兼容的 <code>groupBy</code> 方法对 <code>Array.prototype</code> 进行了猴子补丁。在考虑新的应用程序接口时,向后兼容性非常重要。几年前,在尝试实现 <code>Array.prototype.flatten</code> 时,这一点在一次被称为 <a href="https://link.segmentfault.com/?enc=s%2Bs4GFBdBSz6gLgxzJulbQ%3D%3D.jmp%2B12vOl25TM4LWhnDNIPQte7mk9fz%2B8Ume2adCrjP4btkQ9m5cVel0lI2yyta8" rel="nofollow">SmooshGate</a> 的事件中得到了强调。</p><p>幸运的是,使用静态方法似乎更有利于未来的可扩展性。当 Record 和 Tuples 提议实现时,我们可以添加一个 <code>Record.groupB</code>y 方法,用于将数组分组为不可变的记录。</p><h2>总结</h2><p>将项目分组显然是我们开发人员的一项重要工作。目前,每周从 npm 下载 <code>lodash.groupBy</code> 的次数在 150 万到 200 万之间。很高兴看到 JavaScript 填补了这些空白,让我们的工作变得更加轻松。</p><p>现在,下载 Chrome 117 并亲自尝试这些新方法吧。</p>
如何在React Native中添加自定义字体
https://segmentfault.com/a/1190000044635082
2024-02-22T09:02:20+08:00
2024-02-22T09:02:20+08:00
王大冶
https://segmentfault.com/u/minnanitkong
1
<p><img width="730" height="487" src="/img/bVdbrLy" alt="image.png" title="image.png"></p><p>字体是优秀用户体验的基石。使用定制字体可以为你的应用程序提供独特的身份,帮助你的项目在竞争激烈的市场中脱颖而出。</p><p>在这篇指南中,我们将探索使用 Google Fonts 在 React Native 应用中添加自定义字体的方法。要跟上进度,你应该熟悉 React Native 或 Expo SDK 的基础知识,包括 JSX、组件(类和函数式)和样式。</p><h2>向 React Native CLI 项目添加自定义字体</h2><p>对于我们的项目,我们将研究如何通过构建使用Google字体的基础应用程序,将自定义字体添加到React Native CLI项目中。Google字体是一个免费的开源字体库,可在设计网页和移动应用程序时使用。</p><p>要启动React Native CLI项目,请在终端中运行以下命令:</p><pre><code>npx react-native@latest init CustomFontCLI</code></pre><p>CustomFontCLI 是我们的项目文件夹的名称。一旦项目成功安装,你将会看到下面的图片:</p><p><img width="573" height="259" src="/img/bVdbrLE" alt="image.png" title="image.png"></p><p>在你喜欢的IDE中打开你的项目以开始。在这个教程中,我们将使用VS Code。</p><p>一旦项目已经启动,我们将继续获取我们想要使用的字体。我们将讨论如何导入它们并在我们的项目中使用它们。</p><h2>下载并将Google字体集成到我们的项目中</h2><p>在这个项目中,我们将使用两种字体:<strong>QuickSand</strong> 和 <strong>Raleway</strong>,演示自定义字体的集成,你可以在Google字体上找到它们。</p><p>在 Google Fonts 中找到你想要的字体,选择你想要的样式(例如,Light 300, Regular 400 等),并使用“下载全部”按钮下载整个字体文件夹:</p><p><img width="723" height="563" src="/img/bVdbrLH" alt="image.png" title="image.png"></p><p>该文件夹将以ZIP文件的形式下载,其中包含一个字体文件夹。在该文件夹内,有一个静态文件夹,所有的TTF文件都在其中。复制并保留这些TTF文件。</p><p>在下一部分,我们将会讲解如何将这些字体的TTF文件集成到我们的React Native CLI项目中。</p><h2>将Google字体集成到项目中</h2><p>在你的项目根目录中创建一个名为 <code>assets</code> 的文件夹,并在其中创建一个名为 <code>fonts</code> 的子文件夹。然后,将你之前从静态文件夹中复制的所有TTF文件粘贴到你的项目的 <code>fonts</code> 文件夹中:</p><p><img width="199" height="557" src="/img/bVdbrMy" alt="image.png" title="image.png"></p><p>接下来,在根目录中创建一个 <code>react-native.config.js</code> 文件,并将下面的代码粘贴到其中:</p><h2>将字体链接到要在项目文件中使用</h2><p>我们已经成功地将字体文件集成到我们的项目中。现在我们需要链接它们,这样我们就能在项目内的任何文件中使用它们。要做到这一点,运行以下命令:</p><pre><code>npx react-native-asset</code></pre><p>一旦资源成功链接,你应该会在终端看到以下消息:</p><p><img width="723" height="54" src="/img/bVdbrMC" alt="image.png" title="image.png"></p><p>然后,在你的 <code>App.js</code> 文件中,粘贴以下代码:</p><pre><code>import {StyleSheet, Text, View} from 'react-native';
import React from 'react';
const App = () => {
return (
<View style={styles.container}>
<Text style={styles.quicksandRegular}>
This text uses a quick sand font
</Text>
<Text style={styles.quicksandLight}>
This text uses a quick sand light font
</Text>
<Text style={styles.ralewayThin}>
This text uses a thin italic raleway font
</Text>
<Text style={styles.ralewayItalic}>
This text uses a thin italic raleway font
</Text>
</View>
);
};
export default App;
const styles = StyleSheet.create({
container: {
backgroundColor: 'lavender',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
quicksandLight: {
fontFamily: 'Quicksand-Light',
fontSize: 20,
},
quicksandRegular: {
fontFamily: 'Quicksand-Regular',
fontSize: 20,
},
ralewayItalic: {
fontFamily: 'Raleway-Italic',
fontSize: 20,
},
ralewayThin: {
fontFamily: 'Raleway-ThinItalic',
fontSize: 20,
},
});</code></pre><p>这是一个基本的 <code>App.js</code> 文件,其中四个文本被不同的 <code>Raleway</code> 和 <code>Quicksand</code> 字体样式所样式化。本质上,我们正在渲染 JSX 与四个文本以显示在屏幕上,并使用 React Native 的 StyleSheet API 为每个 <code>Text</code> 组件附加不同的 <code>fontFamily</code> 样式。</p><p>让我们看看输出:</p><p><img width="276" height="576" src="/img/bVdbrMF" alt="image.png" title="image.png"></p><h2>在Expo中使用自定义字体的React Native</h2><p>在这一部分,我们将学习如何在Expo中使用自定义字体。Expo 支持两种字体格式,<code>OTF</code> 和 <code>TTF</code>,这两种格式在 iOS、Android 和 Web上都能稳定运行。如果你的字体是其他格式,你将需要进行高级配置。</p><p>首先,通过运行此命令创建一个新的Expo项目:</p><pre><code>npx create-expo-app my-app</code></pre><p>一旦项目成功安装,通过运行 <code>npm run start</code> 启动开发服务器,并选择iOS 或 Android 选项来打开你的项目。</p><p>当你的模拟器完成项目加载后,你应该会看到这个:</p><p><img width="286" height="631" src="/img/bVdbrMG" alt="image.png" title="image.png"></p><h2>使用Google字体</h2><p>因为我们将 <code>Raleway</code> 和 <code>Quicksand</code> 字体添加为我们的自定义字体,我们将安装这两个包:</p><pre><code>@expo-google-fonts/raleway
@expo-google-fonts/quicksand</code></pre><p>如果你有其他想要使用的Google字体,你可以在这里查看Expo支持的可用字体。</p><h2>在Expo项目中集成自定义的Google字体</h2><p>在你的 <code>App.js</code> 文件中,粘贴以下代码块:</p><pre><code>import { Raleway_200ExtraLight } from "@expo-google-fonts/raleway";
import { Quicksand_300Light } from "@expo-google-fonts/quicksand";
import { useFonts } from "expo-font";
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
export default function App() {
const [fontsLoaded] = useFonts({
Raleway_200ExtraLight,
Quicksand_300Light,
});
if (!fontsLoaded) {
return <Text>Loading...</Text>;
}
return (
<View style={styles.container}>
<Text>This text has default style</Text>
<Text style={styles.raleway}>This text uses Raleway Font</Text>
<Text style={styles.quicksand}>This text uses QuickSand Font</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
raleway: {
fontSize: 20,
fontFamily: "Raleway_200ExtraLight",
},
quicksand: {
fontSize: 20,
fontFamily: "Quicksand_300Light",
},
});
</code></pre><p>在这里,我们从各自的包中导入了 <code>Raleway_200ExtraLight</code> 和 <code>Quicksand_300Light</code> 。 <code>useFonts</code> 钩子用于异步加载这些自定义字体。 <code>useFonts</code> 钩子的结果是一个布尔值数组,我们使用 const <code>[fontsLoaded]</code> 语法进行解构,以访问它返回的布尔值。</p><p>如果字体成功加载,结果将是 <code>[true, null]</code> ,这意味着 fontsLoaded 是真的。如果不成功,它将返回 <code>[false, null]</code> 。如果字体还未加载,我们将返回一个 <code>Loading</code> 文本。</p><p>如果传递给 <code>useFont</code> 钩子的字体(如上面的代码块所示)已经加载,那么就渲染应用程序,我们指定的字体应该会被使用。</p><p>在我们的模拟器中看看这是什么样子:</p><p><img width="295" height="647" src="/img/bVdbrMN" alt="image.png" title="image.png"></p><h2>使用自定义字体</h2><p>假设你正在构建一个个人的 React Native 项目,并且你得到了一些自定义字体,这些字体并不在 Expo 支持的 Google 字体库中。</p><p>首先,你需要下载 font 文件到你的项目中,并安装 <code>expo-font</code> 包。对于这个教程,我从 FontSquirrel 下载了 Source Code Pro 作为我的自定义字体。</p><p>创建一个名为 <code>assets</code> 的文件夹,并在其中创建一个 fonts 文件夹,就像你使用React Native CLI所做的那样。然后,从 <code>fonts</code> 文件夹获取并复制字体文件到你的机器和你的项目中,如下所示:</p><p><img width="197" height="161" src="/img/bVdbrMP" alt="image.png" title="image.png"></p><p>在你的 <code>App.js</code> 文件中,粘贴以下代码:</p><pre><code>import { useFonts } from "expo-font";
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
export default function App() {
const [fontsLoaded] = useFonts({
"SourceCodePro-ExtraLight": require("./assets/fonts/SourceCodePro-ExtraLight.otf"),
"SourceCodePro-LightIt": require("./assets/fonts/SourceCodePro-LightIt.otf"),
});
if (!fontsLoaded) {
return <Text>Loading...</Text>;
}
return (
<View style={styles.container}>
<Text style={styles.default}>This text has default style</Text>
<Text style={styles.sourcepro}>This text uses Source Pro Font</Text>
<Text style={styles.sourceprolight}>This text uses Source Pro Font</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
default: {
fontSize: 20,
},
sourcepro: {
fontSize: 20,
fontFamily: "SourceCodePro-ExtraLight",
},
sourceprolight: {
fontSize: 20,
fontFamily: "SourceCodePro-LightIt",
},
});</code></pre><p>就像集成 Google 字体一样, <code>useFonts</code> 钩子用于从 <code>font</code> 文件夹加载字体文件,根据字体是否成功加载,返回 <code>true</code> 或 <code>false</code> 的值。</p><p>如果 <code>fontsLoaded</code> 不为真,即 <code>useFonts</code> 钩子中指定的字体没有成功加载,我们将返回一个<code> Loading…</code> 文本。否则,我们渲染应用组件并使用已加载的自定义字体。</p><p><img width="295" height="653" src="/img/bVdbrMU" alt="image.png" title="image.png"></p><p>如上述模拟器输出所示,第一段具有 <code>default</code> 样式的文本使用默认的 <code>fontFamily</code> 样式,而接下来的两段文本分别使用了 <code>SourceCodePro-ExtraLight</code> 和 <code>SourceCodePro-Light</code> 字体家族来设置文本样式。</p><h2>在React Native中使用自定义字体时常见的陷阱</h2><p>在React Native中使用自定义字体时,你可能会遇到一些缺点:</p><ul><li><strong>字体族名称不匹配</strong>:如前文所述,确保字体族名称一致性至关重要。例如,如果你将一个字体作为 <code>SourceCodePro-ExtraLight.otf</code> 导入,但随后以不同的路径或文件名加载到应用程序中,例如 <code>/assets/fonts/SourceCodePro-ExtraLight.ttf</code>,这将导致应用程序抛出错误,因为存在 <code>fontFamily</code> 名称不匹配的情况。</li><li>使用不受支持的字体格式:在使用自定义字体时,验证你正在使用的系统(iOS,Android 或网页)是否支持你正在使用的字体格式(例如,<code>.ttf</code>,<code>.otf</code>)非常重要。如果不支持,可能会在开发过程中出现意外错误。</li><li>性能影响:在React Native应用程序中添加自定义字体时,请注意它们的文件大小(以kb/mb为单位)。大型字体文件可能会显著增加应用程序的加载时间,特别是在加载自定义字体时。</li></ul><h2>总结</h2><p>如本文所探讨的,将自定义字体集成到React Native应用程序中不仅仅是技术上的提升,更是一种改善用户体验的策略性方法。无论是使用Expo还是React Native CLI,这个过程都将大大提升你的应用的美观度和可用性。</p>
Go deadcode:查找没意义的死代码,对于维护项目挺有用!
https://segmentfault.com/a/1190000044648547
2024-02-22T12:31:21+08:00
2024-02-22T12:31:21+08:00
煎鱼
https://segmentfault.com/u/eddycjy
1
<p>大家好,我是煎鱼。</p><p>还记得我前两年在深圳参加了个技术大会,其中一个议题是携程的一个大佬分享他在日常工作中,发现一大堆过时的无意义代码和逻辑,导致大家工作较为繁琐且较为辛苦的情况。</p><p>携程应该是 Java 应用为主,他基于 Java 各种研究,通过 JVM 内参数结合各种手段找到了无意义的死代码,并通过灰度机制等完成了逐步上线和替换。</p><p>最近 Go 官方也终于有了类似的工具,今天分享给大家,可以持续关注!</p><h2>用 deadcode 检测代码</h2><p>普遍来讲,作为 Go 项目源代码一部分,但在任何执行过程中都无法触及的函数被称为 "死代码",它们会拖累代码库的维护工作。</p><p>也会造成程序员在阅读代码时的认知负担,看了半天发现这代码根本没用。或是莫名其妙就被引入模块依赖里里。尴尬得很。</p><p>现在我们可以用 deadcode 来识别他。安装方式如下:</p><pre><code class="shell">$ go install golang.org/x/tools/cmd/deadcode@latest
$ deadcode -help
The deadcode command reports unreachable functions in Go programs.
Usage: deadcode [flags] package...</code></pre><p>以下是一个简单 Demo:</p><pre><code class="go">func main() {
var g Greeter
g = Helloer{}
g.Greet()
}
type Greeter interface{ Greet() }
type Helloer struct{}
type Goodbyer struct{}
var _ Greeter = Helloer{}
var _ Greeter = Goodbyer{}
func (Helloer) Greet() { hello() }
func (Goodbyer) Greet() { goodbye() }
func hello() { fmt.Println("你好,煎鱼!") }
func goodbye() { fmt.Println("再见,煎鱼!") }</code></pre><p>运行结果:</p><pre><code>$ go run main.go
你好,煎鱼!</code></pre><p>咋一眼一看,可能没法知道是哪块代码没用到。还需要多看两眼。</p><p>这时候我们只需要借助 deadcode 工具去扫描,一下子就能得到结果了。</p><p>执行如下命令并查看输出结果:</p><pre><code class="go">$ deadcode .
main.go:20:17: unreachable func: Goodbyer.Greet
main.go:23:6: unreachable func: goodbye</code></pre><p>检测结果告诉我们 goodbye 函数和 Goodbyer.Greet 方法都无法访问。也就是这个代码本身的存在是没有运行意义的。</p><p>如果你希望清除这些骚扰代码,就可以依据这个结果去做删除代码了。</p><p>同时也可以借助命令的子选项 <code>-whylive</code>,让检测工具给我们解释为什么 <code>greet.hello</code> 函数是有效的。</p><p>如下解释结果:</p><pre><code class="go">$ deadcode -whylive=example.com/greet.hello .
example.com/greet.main
dynamic@L0008 --> example.com/greet.Helloer.Greet
static@L0019 --> example.com/greet.hello
example.com/greet.main
...</code></pre><p>该命令会把 main 开始到函数调用的过程打印出来,作为一种解释。证明这个函数确实是存在使用的。</p><h2>注意点和发现机制</h2><p>需要留意的是:deadcode 工具,必须要包含 main 函数。言外之意就是其检测链路是从 main 函数开始的。</p><p>否则会产生如下的报错信息:</p><pre><code class="shell">$ deadcode .
deadcode: no main packages</code></pre><p>deadcode 工具本身会加载、解析和类型检查指定的包,然后将其转换为类似编译器的中间表示形式。</p><p>然后会使用 Rapid Type Analysis(RTA)的算法来建立可达函数集,最初只包括每个主要包的入口点:main 函数和包初始化函数(分配全局变量并调用名为 init 的函数)。</p><p>RTA 分析每个可达函数的语句体以收集三种类型的信息:直接调用的函数集合、通过接口方法进行的动态调用集合以及转换为接口的类型集合。</p><p>因此他必须依赖 main 函数作为主入口来做调用链路分析。当然了,这也是相对正常的。总得有个 “客户端” 来做开始逻辑。</p><p>我们可以定期在 Go 项目上运行 deadcode 命令(特别是在重构工作之后),以帮助识别程序中不再需要的部分。</p><p>但需要留意的是,deadcode 工具还是在发展阶段。复杂场景下,可能无法保证 100% 的准确率,我们最好还是要自己做一遍 double check 和灰度上线。</p><h2>总结</h2><p>今天基于官方的《Finding unreachable functions with deadcode》给大家分享了 deadcode 工具的使用和机制。</p><p>整体上来讲,还是非常乐见这个工具的诞生和发展的。历史项目维护旧了,很多地方删删改改,堆积久了后,确实会给大家开发造成不少的认知负担和维护成本。</p><blockquote>文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 <strong>GitHub</strong> <a href="https://link.segmentfault.com/?enc=mUprS80iXgtuYlUirtHtyQ%3D%3D.lvqQkAz6msq3f9zj9Ge13TwMZhKn2HRc54lMptFMGiA%3D" rel="nofollow">github.com/eddycjy/blog</a> 已收录,学习 Go 语言可以看 <a href="https://link.segmentfault.com/?enc=0dj%2FPdSiEU8jfer1oYRc4w%3D%3D.TfEeglUrd0jxS38TR939IuU00UrL8Yhfycue4dApP5jpAV21XjUGns3SSnaw%2Bhjo" rel="nofollow">Go 学习地图和路线</a>,欢迎 Star 催更。</blockquote><h4>推荐阅读</h4><ul><li><a href="https://link.segmentfault.com/?enc=OVJQdkl7pkQ0wl8Ite7kFw%3D%3D.End%2FfPnzV0E8Lcq4NVgRy6ZIEdfg0NJhpXKnj%2F6TecBtC9MBO27whi7EpIr6FG5d%2BNvejUoAcZy5YXZ6Z6Hiqw%3D%3D" rel="nofollow">Go1.22 新特性:for 循环不再共享循环变量,且支持整数范围</a></li><li><a href="https://link.segmentfault.com/?enc=DhUCAKFCFRZ4pgDeRSolcA%3D%3D.%2BJDTyrI3XjU%2FrHFK2Z1QHiCnlJ28d8a%2BwExugraQ4fB0m%2B0SQnT4%2FpsIiV%2FuQGl74OdU0TXLFuEZE0PFyAdDMw%3D%3D" rel="nofollow">Go1.22 新特性:Slices 变更 Concat、Delete、Insert 等函数,对开发挺有帮助!</a></li><li><a href="https://link.segmentfault.com/?enc=zvzpkqPOEwI%2B6XyRH3tdlg%3D%3D.AzFVZdTa1zea%2Fn2A1zwTCVjwCCPY%2BqcCnS6lJBO8Ri59RxDVa6ZcCu3o6Uk%2FAkt49NMYxTNGOB%2BE5R%2BnpttEpA%3D%3D" rel="nofollow">Go1.22 新特性:新的 math/rand/v2 库,更快更标准!</a></li></ul>
深入浅出JVM(六)之前端编译过程与语法糖原理
https://segmentfault.com/a/1190000044651952
2024-02-23T13:14:26+08:00
2024-02-23T13:14:26+08:00
菜菜的后端私房菜
https://segmentfault.com/u/tcl_64f498a8d872b
1
<p>本篇文章将围绕Java中的编译器,深入浅出的解析前端编译的流程、泛型、条件编译、增强for循环、可变长参数、lambda表达式等语法糖原理</p><h2>编译器与执行引擎</h2><h3>编译器</h3><p>Java中的编译器不止一种,Java编译器可以分为:<strong>前端编译器、即时编译器和提前编译器</strong></p><p>最为常见的就是<strong>前端编译器javac,它能够将Java源代码编译为字节码文件,它能够优化程序员使用起来很方便的语法糖</strong></p><p><strong>即时编译器是在运行时,将热点代码直接编译为本地机器码,而不需要解释执行,提升性能</strong></p><p><strong>提前编译器将程序提前编译成本地二进制代码</strong></p><h3>前端编译过程</h3><ul><li>准备阶段: 初始化插入式注解处理器</li><li><p>处理阶段</p><ul><li><p>解析与填充符号表</p><ol><li><p><strong>词法分析: 将Java源代码的字符流转变为token(标记)流</strong></p><ul><li>字符: 程序编写的最小单位</li><li>标记(token) : 编译的最小单位</li><li>比如 关键字 static 是一个标记 / 6个字符</li></ul></li><li><strong>语法分析: 将token流构造成抽象语法树</strong></li><li><p><strong>填充符号表: 产生符号信息和符号地址</strong></p><ul><li>符号表是一组符号信息和符号地址构成的数据结构</li><li>比如: 目标代码生成阶段,对符号名分配地址时,要查看符号表上该符号名对应的符号地址</li></ul></li></ol></li><li><p>插入式注解处理器的注解处理</p><ol start="4"><li><p><strong>注解处理器处理特殊注解: 在编译器允许注解处理器对源代码中特殊注解作处理,可以读写抽象语法树中任意元素,如果发生了写操作,就要重新解析填充符号表</strong></p><ul><li>比如: Lombok通过特殊注解,生成get/set/构造器等方法</li></ul></li></ol></li><li><p>语义分析与字节码生成</p><ol start="5"><li><p><strong>标注检查: 对语义静态信息的检查以及常量折叠优化</strong></p><pre><code class="java"> int i = 1;
char c1 = 'a';
int i2 = 1 + 2;//编译成 int i2 = 3 常量折叠优化
char c2 = i + c1; //编译错误 标注检查 检查语法静态信息 </code></pre><p><img width="723" height="99" src="/img/bVdbwaM" alt="image-20210524202623150.png" title="image-20210524202623150.png"></p></li><li><p><strong>数据及控制流分析: 对程序运行时动态检查</strong></p><ul><li>比如方法中流程控制产生的各条路是否有合适的返回值</li></ul></li><li><strong>解语法糖: 将(方便程序员使用的简洁代码)语法糖转换为原始结构</strong></li><li><strong>字节码生成: 生成<code><init>,<clinit></code>方法,并根据上述信息生成字节码文件</strong></li></ol></li></ul></li></ul><blockquote>前端编译流程图</blockquote><p><img width="723" height="253" src="/img/bVdbwaN" alt="image-20210524205803664.png" title="image-20210524205803664.png"></p><blockquote>源码分析</blockquote><p><img width="723" height="211" src="/img/bVdbwaO" alt="image-20210524222754508.png" title="image-20210524222754508.png"><br>代码位置在JavaCompiler的compile方法中</p><p><img width="723" height="364" src="/img/bVdbwaP" alt="image-20210524221445424.png" title="image-20210524221445424.png"></p><h3>Java中的语法糖</h3><h4>泛型</h4><p><strong>将操作的数据类型指定为方法签名中一种特殊参数,作用在方法、类、接口上时称为泛型方法、泛型类、泛型接口</strong></p><p>Java中的泛型是<strong>类型擦除式泛型</strong>,泛型只在源代码中存在,<strong>在编译期擦除泛型,并在相应的地方加上强制转换代码</strong></p><blockquote>与具现化式泛型(不会擦除,运行时也存在泛型)对比</blockquote><ul><li><p>优点: 只需要改动编译器,Java虚拟机和字节码指令不需要改变</p><ul><li>因为泛型是JDK5加入的,为了满足对以前版本代码的兼容采用类型擦除式泛型</li></ul></li><li><p>缺点: 性能较低,使用没那么方便</p><ul><li>为提供基本类型的泛型,只能自动拆装箱,在相应的地方还会加速强制转换代码,所以性能较低</li><li><p>运行期间无法获取到泛型类型信息</p><ul><li><p>比如书写泛型的List转数组类型时,需要在方法的参数中指定泛型类型</p><pre><code class="java"> public static <T> T[] listToArray(List<T> list,Class<T> componentType){
T[] instance = (T[]) Array.newInstance(componentType, list.size());
return instance;
}</code></pre></li></ul></li></ul></li></ul><h4>增强for循环与可变长参数</h4><p><img width="723" height="339" src="/img/bVdbwaQ" alt="image-20210524213429033.png" title="image-20210524213429033.png"></p><p>增强for循环 -> 迭代器</p><p>可变长参数 -> 数组装载参数</p><p>泛型擦除后会在某些位置插入强制转换代码</p><h4>自动拆装箱</h4><blockquote>自动装箱、拆箱的错误用法</blockquote><pre><code class="java"> Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
//true
System.out.println(c == d);//范围小,在缓冲池中
//false
System.out.println(e == f);//范围大,不在缓冲池中,比较地址因此为false
//true
System.out.println(c == (a + b));
//true
System.out.println(c.equals(a + b));
//false
System.out.println(g == (b + a));
//true
System.out.println(g.equals(a + b));</code></pre><ul><li><p>注意:</p><ol><li>包装类重写的equals方法中不会自动转换类型<br><img width="723" height="107" src="/img/bVdbwaR" alt="image-20210524213853321.png" title="image-20210524213853321.png"></li><li>包装类的 == 就是去比较引用地址,不会自动拆箱</li></ol></li></ul><h4>条件编译</h4><p>布尔类型 + if语句 : <strong>根据布尔值类型的真假,编译器会把分支中不成立的代码块消除(解语法糖)</strong></p><p><img width="723" height="304" src="/img/bVdbwaS" alt="image-20210524214427206.png" title="image-20210524214427206.png"></p><h4>Lambda原理</h4><blockquote>编写函数式接口</blockquote><pre><code class="java"> @FunctionalInterface
interface LambdaTest {
void lambda();
}</code></pre><blockquote>编写测试类</blockquote><pre><code class="java"> public class Lambda {
private int i = 10;
public static void main(String[] args) {
test(() -> System.out.println("匿名内部类实现函数式接口"));
}
public static void test(LambdaTest lambdaTest) {
lambdaTest.lambda();
}
}</code></pre><blockquote>使用插件查看字节码文件</blockquote><p><img width="723" height="288" src="/img/bVdbwaT" alt="image-20210524230643123.png" title="image-20210524230643123.png"><br>生成了一个私有静态的方法,这个方法中很明显就是lambda中的代码</p><p><strong>在使用lambda表达式的类中隐式生成一个静态私有的方法,这个方法代码块就是lambda表达式中写的代码</strong></p><p><img width="723" height="261" src="/img/bVdbwaU" alt="image-20210524232010510.png" title="image-20210524232010510.png"><br>执行class文件时带上参数<code>java -Djdk.internal.lambda.dumpProxyClasses 包名.类名</code>即可显示出这个匿名内部类</p><p><img width="711" height="342" src="/img/bVdbwaV" alt="image-20210527083659256.png" title="image-20210527083659256.png"></p><p><strong>使用<code>invokedynamic</code>生成了一个实现函数式接口的匿名内部类对象,在重写函数式接口的方法实现中调用使用lambda表达式类中隐式生成的静态私有方法</strong></p><h3>总结</h3><p>本篇文章以Java中编译器的分类为开篇,深入浅出的解析前端编译的流程,Java中泛型、增强for循环、可变长参数、自动拆装箱、条件编译以及Lambda等语法糖的原理</p><p><strong>前端编译先将字符流转换为token流,再将token流转换为抽象语法树,填充符号表的符号信息、符号地址,然后注解处理器处理特殊注解(比如Lombok生成get、set方法),对语法树发生写改动则要重新解析、填充符号,接着检查语义静态信息以及常量折叠,对运行时程序进行动态检查,再解语法糖,生成init实例方法、clinit静态方法,最后生成字节码文件</strong></p><p><strong>Java中为了兼容之前的版本使用类型擦除式的泛型,在编译期间擦除泛型并在相应位置加上强制转换,想为基本类型使用泛型只能搭配自动拆装箱一起使用,性能有损耗且在运行时无法获取泛型类型</strong></p><p><strong>增加for循环则是使用迭代器实现,并在适当位置插入强制转换;可变长参数则是创建数组进行装载参数</strong></p><p><strong>自动拆装箱提供基本类型与包装类的转换,但包装类尽量不使用==,这是去比较引用地址,同类型比较使用equals</strong></p><p><strong>条件编译会在if-else语句中根据布尔类型将不成立的分支代码块消除</strong></p><p><strong>lambda原理则是通过<code>invokeDynamic</code>指令动态生成实现函数式接口的匿名对象,匿名对象重写函数时接口方法中调用使用lambda表达式类中隐式生成的静态私有的方法(该方法就是lambda表达式中的代码内容)</strong></p><h3>最后(不要白嫖,一键三连求求拉\~)</h3><p>本篇文章笔记以及案例被收入 <a href="https://link.segmentfault.com/?enc=EFT8yb%2FIjpSG31Ysp1G4qQ%3D%3D.%2FyVfJUomNH1QgQXPLkGMqmhauHGfIJ8gWRmSD83ugqUC2Mef60Xb2g5gbjbdsrwVkBBHDGNUJGI7KXwgD4ukKma%2FtXKuVPI0%2BD6iu1GywVE%2FwGhbZsNhb6Ii0XtV99Bv" rel="nofollow" title="https://link.juejin.cn?target=https%3A%2F%2Fgitee.com%2Ftcl192243051%2FStudyJava">gitee-StudyJava</a>、 <a href="https://link.segmentfault.com/?enc=U%2FAdJgTQHZDThSd2qNSFhg%3D%3D.naMWu4oogsDE8D7%2Fd3WLz6PP4aw9GrpONAqqdKZIis39lTOEbj7hRuIdBFFuqZSKTP4evRmdP2%2Fj7KVO0Fck7avwVxOXiJGTB%2Fo427D%2FN%2Bg%3D" rel="nofollow" title="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FTc-liang%2FStudyJava">github-StudyJava</a> 感兴趣的同学可以stat下持续关注喔\~</p><p>有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下\~</p><p>关注菜菜,分享更多干货,公众号:菜菜的后端私房菜</p><blockquote>本文由博客一文多发平台 <a href="https://link.segmentfault.com/?enc=U3JO8kTU%2Fimmz3uBakz86Q%3D%3D.7UAcoFAWnM3MOmIM7q3c2SC9gHyGS368%2Bv84ebvc2hBFTr%2FAzaIkc8Ea7pnE0jUd" rel="nofollow">OpenWrite</a> 发布!</blockquote>
在 React Native 中原生实现动态导入
https://segmentfault.com/a/1190000044635194
2024-02-20T09:14:24+08:00
2024-02-20T09:14:24+08:00
王大冶
https://segmentfault.com/u/minnanitkong
2
<p><img width="730" height="487" src="/img/bVdbrM6" alt="image.png" title="image.png"></p><p>在React Native社区中,原生动态导入一直是期待已久的功能。在React Native 0.72 版本发布之前,只能通过第三方库和其他变通方法实现动态导入,例如使用 <code>React.lazy()</code> 和 <code>Suspense</code> 函数。现在,动态导入已经成为React Native框架的原生部分。</p><p>在这篇文章中,我们将比较静态和动态导入,学习如何原生地处理动态导入,以及有效实施的最佳实践。</p><h2>静态导入 vs. 动态导入</h2><p>在深入研究实现细节之前,理解什么是动态导入以及它们与静态导入有何不同是至关重要的,静态导入是在JavaScript中包含模块的更常见方式。</p><p>静态导入是你在文件顶部使用 <code>import</code> 或 <code>require</code> 语法声明的导入。这是因为在应用程序启动时,它们可能需要在你的整个应用程序中可用。</p><p>这是一个例子:</p><pre><code>import React from 'react';
import {View, Text} from 'react-native';
const MyComponent = require('./MyComponent');</code></pre><p>静态导入是同步的,意味着它们会阻塞主线程,直到模块完全加载。这种行为可能导致应用程序启动时间变慢,特别是在较大的应用程序中。然而,当一个库或模块在代码库的多个时间或多个地方需要时,静态导入就会显得非常有用。</p><p>相比之下,动态导入赋予开发者在需要时即时导入模块的能力,引领了一个异步范式。这意味着代码是按需加载的。</p><p>总的来说,静态导入和动态导入的主要区别在于,静态导入在编译时解析,而动态导入在运行时解析。</p><p>在 React Native v0.72 版本之前,动态导入并不是开箱即用的支持,因为它们与 Metro 打包器不兼容,Metro 打包器负责在 React Native 应用程序中打包 JavaScript 代码和资产。</p><p>Metro 打包器不允许任何运行时更改,并通过移除未使用的模块并用静态引用替换它们来优化包大小。这意味着 React Native 开发者必须依赖第三方库或自定义解决方案来在他们的应用中实现动态导入。我们将在本文后面探讨这些。</p><h2>如何在React Native中原生实现动态导入</h2><p>要在 React Native中 使用原生动态导入,你需要安装0.72或更高版本的React Native。你可以通过在终端运行 <code>npx react-native --version</code> 来检查你的React Native版本。你还需要在你的项目中配置0.66或更高版本的Metro打包器。</p><p>React Native 中使用原生动态导入有两种方式:使用 <code>import()</code> 语法或使用 <code>require.context()</code> 方法。</p><h2>使用 import() 语法</h2><p>根据Metro Bundler官方文档:</p><blockquote><code>import()</code> 调用在开箱即用的情况下得到支持。在React Native中,使用 <code>import()</code> 会自动分割你的应用程序代码,使其在开发过程中加载速度更快,而不影响发布构建。</blockquote><p><code>import()</code> 语法与静态 <code>import</code> 关键字相似,但你可以在代码的任何地方使用它,只要你处理好 promise 的解决和拒绝。</p><p>例如,假设你有一个名为 <code>SomeComponent</code> 的组件,你希望根据某些条件动态加载它。你可以像这样使用 <code>import()</code> 语法:</p><pre><code>const loadSomeComponent = async () => {
try {
const SomeComponent = await import('./SomeComponent');
// Do something with SomeComponent
} catch (error) {
// Handle error
}
};
// Use SomeComponent conditionally
if (someCondition) {
loadSomeComponent();
}</code></pre><p>注意:你需要在 <code>async</code> 函数内使用 <code>await</code> 关键字来等待promise 的解决。或者,你可以使用 <code>.then()</code> 和 <code>.catch()</code> 方法来处理 promise 的解决和拒绝。</p><h2>使用 require.context() 方法</h2><p><code>require.context()</code> 方法现在是 Metro 打包器的一个支持特性,允许你为动态导入创建一个上下文。这个特性是由 Evan Bacon 添加到Metro库中的。</p><p><code>context</code> 是一个包含与给定模式匹配的一组模块或组件信息的对象。你可以使用 <code>require.context()</code> 方法来创建这样的上下文:</p><pre><code>// Create a context for all components in the ./components folder
const context = require.context('./components', true);</code></pre><p><code>require.context()</code> 方法的第一个参数是你想要查找模块或组件的基础目录。第二个参数是一个布尔值,表示你是否想要包含子目录。</p><p>有了 <code>require.context</code> ,你现在可以根据变量或正则表达式进行导入。</p><p>这是一个示例,展示了如何使用 <code>require.context</code> 从文件夹中导入所有图片并将它们显示在列表中:</p><pre><code>// App.js
import React from 'react';
import {FlatList, Image, StyleSheet} from 'react-native';
// Import all the images from the assets/images folder
const images = require.context('./assets/images', true, /\.png$/);
// Create an array of image sources
const imageSources = images.keys().map((key) => images(key));
const App = () => {
// Render each image in a flat list
return (
<FlatList
data={imageSources}
keyExtractor={(item) => item}
renderItem={({item}) => <Image style={styles.image} source={item} />}
/>
);
};
const styles = StyleSheet.create({
image: {
width: 100,
height: 100,
margin: 10,
},
});
export default App;</code></pre><p>React Native v0.72引入了通过 <code>require.contex</code>t 方法支持动态导入,这与webpack提供的方式类似。</p><p>但是 <code>require.context</code> 一直以来都被Expo路由器在后台使用,以根据文件目录结构和你拥有的文件自动创建路由。它使用一个带有正则表达式的 <code>require.context</code> 调用,所有的路由都可以在运行时被确定。</p><p>例如,如果你有一个名为 <code>app/home.tsx</code> 的文件,它将变成一条路径为 /home 的路由。如果你有一个名为 app/profile/settings.tsx 的文件,它将变成一条路径为 /profile/settings 的路由。</p><p>例如,如果你有一个名为 <code>app/home.tsx</code> 的文件,它将成为一个路径为 <code>/home</code> 的路由。如果你有一个名为 <code>app/profile/settings.tsx</code> 的文件,它将成为一个路径为 <code>/profile/settings</code> 的路由。</p><p>因此,你无需手动定义或导入你的路由——Expo Router会为你完成!</p><h2>实现动态导入的第三方解决方案</h2><h4>使用 React.lazy() 和 Suspense</h4><p><code>React.lazy()</code> 和 <code>Suspense</code> 是React的特性,允许你懒加载组件,也就是说,只有当它们被渲染时才会加载。你可以使用 <code>React.lazy()</code> 函数来创建一个包装动态导入的组件,你可以使用 Suspense 来显示一个备用组件,而动态导入正在加载。</p><p>这是一个例子:</p><pre><code>import React, { lazy, Suspense } from "react";
import { Text, View } from "react-native";
import { styles } from "./styles";
const DynamicComponent = lazy(() => import("./DynamicComponent"));
function App() {
return (
<View style={styles.container}>
<Suspense fallback={() => <Text>Loading ....</Text>}>
<DynamicComponent />
</Suspense>
</View>
);
}
export default App;</code></pre><p>在你的React Native应用程序中,使用 <code>React.lazy()</code> 和 <code>Suspense</code> 是实现动态导入的好方法。然而,需要注意的是 <code>React.lazy()</code> 是专门为 React 组件的代码分割设计的。如果你需要动态导入非组件的 JavaScript 模块,你可能需要考虑其他方法。</p><h4>可加载组件</h4><p><a href="https://link.segmentfault.com/?enc=sssTW48QERkN2ODNlTVoXA%3D%3D.kURxkln55QcnlmUE34QYwaqDxQzty7heuoEmz%2FNAnQV9IdQRAu5wikdl5jepFrnk" rel="nofollow">Loadable Components</a>是一种将你的React Native代码分割成可以按需加载的小块的方法。在React Native中,你可以使用<code>react-loadable</code>库来动态加载和渲染组件。</p><pre><code>import Loadable from 'react-loadable';
// Define a loading component while the target component is being loaded
const LoadingComponent = () => <ActivityIndicator size="large" color="#0000ff" />;
// Create a dynamic loader for the target component
const DynamicComponent = Loadable({
loader: () => import('./YourComponent'), // Specify the target component path
loading: LoadingComponent, // Use the loading component while loading
});
// Use the dynamic component in your application
function App() {
return (
<View>
<DynamicComponent />
</View>
);
}</code></pre><p>在这段代码中:</p><ul><li>从 <code>react-loadable</code> 库中导入 <code>Loadable</code> 函数</li><li>定义一个加载组件(例如,一个 ActivityIndicator ),在目标组件加载时将会显示。</li><li>使用 Loadable 函数创建一个动态组件。为 loader 属性提供一个导入目标组件的函数(将 <code>'./YourComponent'</code> 替换为组件的实际路径),并指定 <code>loading</code> 属性以在加载过程中显示加载组件。</li><li>最后,在你的应用的用户界面中使用 <code>DynamicComponent</code> 。它将动态加载目标组件,并在准备就绪后显示它,同时显示加载组件。</li></ul><p>这个库最初是为React网页应用设计的,所以它可能并不总是在React Native中运行得很好。</p><h2>React Native中动态导入的好处</h2><p>动态导入为开发者提供了几个优势:</p><ul><li><strong>更快的启动时间</strong>:通过只按需加载所需的代码,动态导入可以显著减少你的应用启动所需的时间。这对于提供流畅的用户体验至关重要,尤其是在设备或网络较慢的情况下。</li><li><strong>提高代码可维护性</strong>:动态导入可以通过让你将不常用的组件或库分离到单独的模块中,更有效地组织你的代码库。这可以提高代码的可维护性,使得在你的应用的特定部分工作变得更容易。</li><li>渐进式加载:动态导入支持渐进式加载。你可以优先加载关键组件,而不是强迫用户等待整个应用程序的加载,同时在后台加载次要功能。这确保了用户的初始体验无缝,同时你的应用程序的不太重要的部分在后台加载,保持用户的参与度。</li><li>优化的包:动态导入允许你通过将它们分割成更小、更易管理的块来优化你的JavaScript包。这可以导致包大小的减小,从而减少应用程序的内存占用并加速加载过程。</li></ul><h2>使用动态导入的最佳实践</h2><ul><li>谨慎使用动态导入:动态导入并非能解决你所有性能和用户体验问题的灵丹妙药。它们带来了一些权衡,如增加的复杂性,潜在的错误,以及对网络连接的依赖。因此,你应该只在必要时使用它们,而不是过度使用它们。</li><li>使用加载指示器和占位符:加载指示器可以向用户显示应用正在动态加载一些模块以及需要多长时间。占位符可以向用户展示当模块加载完成后应用会是什么样子,并防止布局变动或空白空间。你可以使用像 <code>ActivityIndicator</code> 或 Skeleton 这样的React Native内置组件,或者像 <code>react-native-loading-spinner-overlay </code>或 <code>react-native-skeleton-placeholder</code> 这样的第三方库来实现这个目的。</li><li>使用错误边界和回退:在使用动态导入时,你应该使用错误边界和回退来处理错误和失败。错误边界是可以捕获并处理其子组件中的错误的组件。回退是在原始组件无法加载或渲染时可以渲染的组件。你可以使用像React中的 <code>ErrorBoundary</code> 这样的内置组件,或者像 <code>react-error-boundary</code> 或 <code>react-native-error-boundary</code> 这样的第三方库来实现这个目的。</li></ul><h2>总结</h2><p>在这篇文章中,我们学习了如何在React Native中使用原生动态导入。有了动态导入这个强大的工具,你可以使你的React Native应用更高效、响应更快、用户体验更友好。谨慎使用动态导入并遵循最佳实践以确保无缝的用户体验是至关重要的。</p>
关于 React19,你需要了解的前因后果
https://segmentfault.com/a/1190000044640660
2024-02-20T08:17:59+08:00
2024-02-20T08:17:59+08:00
卡颂
https://segmentfault.com/u/kasong
3
<p>大家好,我卡颂。</p><p><code>React</code>当前的稳定版本是18.2,发布时间是22年6月,在此之后就没有新的稳定版本发布。</p><p><img src="/img/remote/1460000044640662" alt="" title=""></p><p>直到今年2月15日,<a href="https://link.segmentfault.com/?enc=83sxbE2lbwZW5VTCm%2BhFLg%3D%3D.J4Rua%2BPZtBVMHMLXSkZ3%2FlnHy2yBm822HRfkz6Hjxriu2nbSVFZwA1IiFRCN3VyfNfcnkIdAyn9zpITt80BvNAmibld13jaRJywkhCEsoFbiD2%2FzU9H8NnszFehwCbSJ" rel="nofollow" title="官方博客">官方博客</a>才透露下一个稳定版本的计划。没错,他就是<code>React19</code>。</p><p><img src="/img/remote/1460000044640663" alt="" title=""></p><p>为什么时隔1年多才公布下个稳定版本的计划?</p><p>为什么下个版本直接跳到了19?</p><p>18我都还没升呢,19就来了,是不是要学很多东西?</p><p>这篇文章会为你详细解答这些疑问。</p><p><a href="https://link.segmentfault.com/?enc=rKUBVMNb6p2bNf9Ww%2BA3gg%3D%3D.OxkzvKIpeNfuuX8IGnGT3mXoQCvioB6bFsYKZnIxTbFe8KDw4AHL5cnXMX3D%2FyGy1Y4u%2FWUfbGVFmELBuE8eVg%3D%3D" rel="nofollow">免费领取卡颂原创React教程(原价359)、加入人类高质量前端群</a></p><h2>从React16聊起</h2><p>近年来<code>React</code>最为人津津乐道的版本应该是<code>16.8</code>,这个版本引入了<code>Hooks</code>,为<code>React</code>(乃至整个前端框架领域)注入了新的活力。</p><p>再之后的<code>v17</code>没有新特性引入。既然没有新特性引入,为什么要发布一个大版本(从16到17)呢?</p><p>这是因为从<strong>同步更新</strong>升级到<strong>并发更新</strong>的<code>React</code>,中间存在<code>breaking change</code>。</p><p>这么大体量的框架,在升级时需要保证过程尽可能平顺。这除了是一种专业、负责的体现,更重要的,版本割裂会造成大量用户损失(参考当年<code>ng1</code>升级到<code>anuglar2</code>时)。</p><p>当升级到18后,<code>React团队</code>发现 —— 真正升级到18,并大量使用并发特性(比如<code>useTransition</code>)的开发者并不多。</p><p>更常见的场景是 —— 知名开源库集成并发特性,开发者再直接用这些库。</p><p>所以,<code>React团队</code>转变策略,将迭代重心从<strong>赋能开发者</strong>转变为<strong>赋能开源库</strong>。那么,什么样的库受众最多呢?显然是框架。</p><p>所以,<code>React</code>的重心逐渐变为 —— 赋能上层框架,开发者通过使用上层框架间接使用<code>React</code>。</p><p>为什么我说<code>React团队</code>转变了策略,而不是<code>React团队</code>一开始的计划就是<strong>赋能上层框架</strong>呢?</p><p>如果一开始的计划就是<strong>赋能上层框架</strong>,<code>React团队</code>就不会花大量精力在<strong>版本的渐进升级上</strong> —— 反正开发者最终使用的会是上层框架(而不是<code>React</code>),版本割裂上层框架会解决,根本不需要引导开发者升级<code>React</code>。</p><h2>策略改变造成的影响</h2><p>策略转变造成的影响是深远且广泛的,这也是为什么18.2后一年多都没有新的稳定版本出现。</p><p>最基本的影响是 —— 特性的迭代流程变了。</p><p><code>React</code>诞生的初衷是为了解决<code>Meta</code>内部复杂的前端应用,所以<code>React</code>之前的特性迭代流程是:</p><ol><li>新特性开发完成</li><li>新特性在<code>React</code>内部产品试用,并最终达到稳定状态</li><li>开源供广大开发者使用</li></ol><p>但随着策略转变为<strong>赋能上层框架</strong>,势必需要与主流上层框架团队(主要是<code>Next.js</code>)密切合作。</p><p>如果按照原来的迭代流程,上层框架团队属于<code>Meta</code>之外的第三方团队,只能等新特性开源后才能使用,这种合作模式显然太低效了。</p><p>于是,<code>React</code>团队提出了一个新的特性发布渠道 —— <code>canary</code>,即:新特性开发完成后,可以先打一个<code>canary版本的React</code>供外部试用,等特性稳定后再考虑将其加入稳定版本中。</p><p>可能有些存在于<code>canary</code>中的特性永远不会出现在稳定版本的<code>React</code>中,但不妨碍一些开源库锁死<code>canary</code>版本的<code>React</code>,进而使用这些特性。</p><p>那么,为什么时隔1年多才公布下个稳定版本的计划?主要有4个原因。</p><h2>原因1:新特性主要服务于Next,没必要出现在稳定版本中</h2><p>策略改变除了影响<strong>特性的迭代流程</strong>,还让<code>React团队成员</code>陷入一个两难的境地 —— 我该优先服务上层框架还是<code>Meta</code>?</p><p>我们可以发现,在之前的迭代流程中,一切都围绕<code>Meta</code>自身需求展开。<code>React团队成员</code>作为<code>Meta</code>员工,这个迭代流程再自然不过。</p><p>但是,新的迭代流程需要密切与<code>Next</code>团队合作,那么问题来了 —— 作为<code>Meta</code>员工,新特性应该优先考虑<code>Next</code>的需求还是<code>Meta</code>的需求?</p><p>为了完成<strong>赋能上层框架</strong>的任务,显然应该更多考虑<code>Next</code>的需求。我们能看到一些<code>React团队成员</code>最终跳槽到<code>Vercel</code>,进入<code>Next</code>团队。</p><p>所以,在此期间产出的特性(比如<code>server action</code>、<code>useFormStatus</code>、<code>useFormState</code>)更多是服务于<code>Next</code>,而不是<code>React</code>。</p><p>如果基于这些特性发布新的稳定版本,那不用<code>Next</code>的开发者用不到这些特性,用<code>Next</code>的开发者依赖的是<code>canary React</code>,所以此时升级稳定版本是没意义的。</p><h2>原因2:新特性必须满足各种场景,交付难度大</h2><p><code>Next</code>是<code>web框架</code>,围绕他创造的新<code>React</code>特性只用考虑<code>web</code>这一场景。</p><p>但<code>React</code>自身的定位是<code>宿主环境无关的UI库</code>,还有大量开发者在<code>非web</code>的环境使用<code>React</code>(比如<code>React Native</code>),所以这些特性要出现在稳定版本的<code>React</code>中,必须保证他能适配所有环境。</p><p>举个例子,<code>Server Actions</code>这一特性,用于<strong>简化客户端与服务器数据发送的流程</strong>,当前主要应用于<code>Next</code>的<code>App Router</code>模式中。</p><p>比如下面代码中的<code>MyForm</code>组件,当表单提交后,<code>serverAction</code>函数的逻辑会在服务端执行,这样就能方便的进行<code>IO</code>操作(比如操作数据库):</p><pre><code class="js">// 服务端代码
async function serverAction(event) {
'use server'
// 在这里处理服务端逻辑,比如数据库操作读写等
}
function MyForm() {
return (
<form action={serverAction}>
<input name="query" />
<button type="submit">Search</button>
</form>
);
}</code></pre><p><code>App Router</code>的场景主要是<code>RSC</code>(React Server Component),除了<code>RSC</code>外,<code>SSR</code>场景下是不是也有表单?不使用服务端相关功能,单纯使用<code>React</code>进行客户端渲染,是不是也有表单的场景?</p><p>所以,<code>Server Actions</code>特性后来改名为<code>Actions</code>,因为不止<code>Server</code>场景,其他场景也要支持<code>Actions</code>。</p><p>比如下面代码中,在客户端渲染的场景使用<code>Actions</code>特性:</p><pre><code class="js">// 前端代码
const search = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const query = formData.get('query');
// 使用 fetch 或其他方式发送数据
const response = await fetch('/search', /*省略*/);
// ...处理响应
};
function MyForm() {
return (
<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>
);
}</code></pre><p>你以为这就完了?还早。<code>form</code>组件支持<code>Actions</code>,那开发者自定义的组件能不能支持<code>Actions</code>这种<strong>前、后端交互模式</strong>?</p><p>比如下面的<code>Calendar</code>组件,之前通过<code>onSelect</code>事件响应交互:</p><pre><code class="html"><Calendar onSelect={eventHandler}></code></pre><p>以后能不能用<code>Actions</code>的模式响应交互:</p><pre><code class="html"><Calendar selectAction={action}></code></pre><p>如何将平平无奇的交互变成<code>Actions交互</code>呢?<code>React团队</code>给出的答案是 —— 用<code>useTransition</code>包裹。所以,这后面又涉及到<code>useTransition</code>功能的修改。</p><p><code>Actions</code>只是一个例子,可以发现,虽然新特性是以<code>web</code>为始,但为了出现在稳定版本中,需要以<strong>覆盖全场景</strong>为终,自然提高了交付难度。</p><h3>原因3:老特性需要兼容的场景越来越多,工作量很大</h3><p>新特性越来越多,老特性为了兼容这些新特性也必须作出修改,这需要大量的时间开发、测试。</p><p>举两个例子,<code>Suspense</code>在v16.6就引入了,它<strong>允许组件“等待”某些内容变得可用,并在此期间显示一个加载指示器(或其他后备内容)</strong>。</p><p><code>Suspense</code>最初只支持懒加载组件(<code>React.lazy</code>)这一场景。随着<code>React</code>新特性不断涌现,<code>Suspense</code>又相继兼容了如下场景:</p><ul><li><code>Actions</code>提交后的等待场景</li><li>并发更新的等待场景</li><li><code>Selective Hydration</code>的加载场景</li><li><code>RSC</code>流式传输的等待场景</li><li>任何<code>data fetching</code>场景</li></ul><p>为了兼容这些场景,<code>Suspense</code>的代码量已经非常恐怖了,但开发者对此是无感知的。</p><p>再举个和<code>Suspense</code>、<code>useEffect</code>这两个特性相关的例子。</p><p><code>Suspense</code>为什么能在<strong>中间状态</strong>与<strong>完成状态</strong>之间切换?是因为在<code>Suspense</code>的源码中,他的内部存在一个<code>Offscreen</code>组件,用于完成两颗子<code>Fiber树</code>的切换。</p><p><code>React团队</code>希望将<code>Offscreen</code>组件抽离成一个单独的新特性(新名字叫<code>Activity</code>组件),起到类似<code>Vue</code>中<code>Keep-Alive</code>组件的作用。</p><p><code>Activity</code>组件既然能让组件显/隐,那势必会影响组件的<code>useEffect</code>的触发时机。毕竟,如果一个组件隐藏了,但他的<code>useEffect create</code>函数触发了,会是一件很奇怪的事情。</p><p>所以,为了落地<code>Activity</code>组件,<code>useEffect</code>的触发逻辑又会变得更复杂。</p><h3>原因4:特性必须形成体系才能交付</h3><p>虽然这一年<code>React团队</code>开发了很多特性,但很多特性无法单独交付,必须构成一个体系后再统一交付。</p><p>比如刚才提到的<code>useFormStatus</code>、<code>useFormState</code>是服务于<code>Actions</code>特性的,<code>Actions</code>又是由<code>Server Actions</code>演化而来的,<code>Server Actions</code>又是<code>RSC</code>(React服务端组件)体系下的特性。</p><p>单独将<code>useFormStatus</code>发布在稳定版本中是没意义的,他属于<code>RSC</code>体系下的一环。</p><p>所以,即使新特性已经准备就绪,他所在的体系还没准备好的话,那体系下的所有特性都不能在稳定版本中交付。</p><h2>总结</h2><p>为什么时隔1年多才公布下个稳定版本的计划?主要是4个原因:</p><ol><li>新特性主要服务于Next,没必要出现在稳定版本中</li><li>新特性必须满足各种场景,交付难度大</li><li>老特性需要兼容的场景越来越多,工作量很大</li><li>特性必须形成体系才能交付</li></ol><p>那为什么下个稳定版本不是v18.x而是v19呢?这是因为部分新特性(主要是<code>Asset Loading</code>、<code>Document Metadata</code>这两类特性)对于一些应用会产生<code>breaking change</code>,所以需要发一个大版本。</p><p>从上述4个原因中的第四点可以知道,既然有v19的消息,势必是因为<strong>已经有成体系的新特性可以交付</strong>,那是不是意味着要学很多东西呢?</p><p>这一点倒不用担心,如果你不用<code>Next</code>,那你大概率不会接触到<code>RSC</code>,既然不会接触<code>RSC</code>,那么<code>RSC</code>体系下的新特性你都不会用到。</p><p>v19对你最大的影响可能就是新特性对老API的影响了,比如:</p><ul><li><code>useContext</code>变为<code>use(promise)</code></li><li><code>Activity</code>组件使<code>useEffect</code>的触发时机更复杂(应该不会在v19的第一个版本中)</li></ul><p>这些的学习成本都不大。</p><p>关于v19的进一步消息,会在今年5月15~16的<a href="https://link.segmentfault.com/?enc=%2FKrTd5K8BM%2BJzYq9DR9Dqg%3D%3D.Q%2F6tRbfG5qVUirBeRakJnE7sgLO2bkh2QlUiWWT7ujY%3D" rel="nofollow" title="React Conf">React Conf</a>公布。</p>
聊聊Git subModule(子模块)
https://segmentfault.com/a/1190000044639287
2024-02-19T15:36:27+08:00
2024-02-19T15:36:27+08:00
归思君
https://segmentfault.com/u/guisijun
2
<p>比如在公司不同开发团队中,有一个基础共享库,同时被多个项目调用。若要保证基础共享库的动态更新,那么就需要把共享库独立为一个代码库,但是分别把共享库中的文件拷贝到各自的项目中会造成冗余,而且再次更新共享库就会在不同项目下更新,会比较麻烦。</p><p>利用子模块可以作为解决该类问题的一种方案,子模块允许将一个Git仓库作为另一个Git仓库的子目录,并且可以独立地管理这个子仓库的版本,而且还能保持提交的独立性。</p><p>下面就通过实操来具体谈谈 Git submodule</p><h2>一、什么是Git Submodule</h2><p><code>Git Submodule</code> 是Git版本控制系统中的一种机制,用于在一个Git仓库中包含另一个Git仓库。它允许将一个Git仓库作为另一个Git仓库的子目录,并且可以独立地管理这个子仓库的版本,同时还保持提交的独立。<br><img src="/img/remote/1460000044639290" alt="image.png" title="image.png"><br>Submodule的作用在于,它允许你在一个项目中使用其他项目的特定版本,而无需将整个子项目的代码复制到主项目中。这对于依赖管理和代码复用非常有用。</p><h2>二、使用 Git Submodule</h2><h3>1 创建和初始化仓库</h3><ul><li>初始化主库和子模块库</li></ul><p>我采用的是 Gitee 来进行本次实操,首先在 Gitee 中创建一个主库 <code>GitSubmoduleDemo</code>,两个子模块库 <code>ModuleA</code>和 <code>ModuleB</code>:<br><img src="/img/remote/1460000044639292" alt="image.png" title="image.png"><br>先将主库 clone 到本地:</p><pre><code class="shell">$ git clone https://gitee.com/haohuin/git-submodule-demo.git</code></pre><ul><li>在主库中,使用 <code>git submodule add</code> 命令添加 <code>Submodule</code>子模块。</li></ul><p>其命令为:<code>git submodule add <remote repo url> <local repo url></code></p><ul><li>remote repo url 指远程仓库的 url</li><li>local repo url 指本地仓库的路径 url</li></ul><pre><code class="shell">//在主库中添加ModuleA
$ git submodule add ../module-a.git ModuleA
//在主库中添加ModuleB
$ git submodule add ../module-b.git ModuleB</code></pre><p>查看当前主库的状态:</p><pre><code class="shell">$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: .gitmodules
new file: ModuleA
new file: ModuleB</code></pre><p>发现有新的文件在主库中,分别是 <code>.gitmodules</code>文件,<code>ModuleA</code>和 <code>ModuleB</code>目录</p><h4>1.1 <code>.gitmodules</code>文件</h4><p>首先看一下 <code>.gitmodules</code>配置文件,它保存了远程仓库 url 与本地仓库 url 路径的映射:</p><pre><code class="shell">[submodule "ModuleA"]
path = ModuleA
url = ../module-a.git
[submodule "ModuleB"]
path = ModuleB
url = ../module-b.git</code></pre><p>每条记录对应一个子模块信息,path 和 url 对应远程仓库 url 和本地仓库路径</p><h4>1.2 主库中的子模块</h4><p>接着提交当前主库中出现的文件和目录:</p><pre><code class="shell">//暂存
$ git add .
//提交
$ git commit -m "ModuleA and ModuleB submodule commit"
[master 2dcfefd] ModuleA and ModuleB submodule commit
3 files changed, 8 insertions(+)
create mode 100644 .gitmodules
create mode 160000 ModuleA
create mode 160000 ModuleB</code></pre><p>发现<code>ModuleA</code>和 <code>ModuleB</code>目录的模式编码为 <code>160000</code>。<br>在之前讲解 <code>index</code>文件提到过,模式 <code>160000</code>是引用其他 Git 仓库的特殊文件类型,它允许将一个 Git 仓库作为另一个仓库的子目录进行管理。具体可以看这篇文章<a href="https://segmentfault.com/a/1190000044573067">Git暂存区机制详解</a><br>所以此时主库中只是存储了子模块的引用,也就是一次的提交记录。而不是其他的文件或者目录。最后将主库中的修改进行推送:</p><pre><code class="shell">$ git push origin master</code></pre><p><img src="/img/remote/1460000044639293" alt="image.png" title="image.png"><br>发现在主库中<code>ModuleA</code>和 <code>ModuleB</code>确实是以引用的形式存储的。</p><h3>2 更新和同步子模块</h3><p>那么如何将子模块的修改更新到主仓库中呢?这又分两种情况:</p><ul><li>在子模块所在的仓库中修改代码后提交,然后在主仓库中拉取更新</li><li>在主仓库中修改子模块代码后提交远程仓库,然后在子模块仓库中拉取更新</li></ul><h4>2.1 主仓库更新子模块仓库中的修改</h4><p>首先在子模块仓库 ModuleA 中新增一个文件,并提交到 ModuleA 的远程仓库中:</p><pre><code class="shell">$ echo "updateModuleA-1">updateModuleA-1.txt
$ git add .
$ git commit -m "update ModuleA"
$ git push</code></pre><p><img src="/img/remote/1460000044639294" alt="image.png" title="image.png"><br>此时子模块仓库中的 commit ID 值为 <code>6026b48</code>,再来看一下主仓库中的子模块的 Commit ID 值<br><img src="/img/remote/1460000044639295" alt="image.png" title="image.png"><br>发现此时主仓库的 Commit ID 值为 <code>3474554</code>,和子模块仓库中的 ID 值并不同步,所以需要在主仓库的子模块代码中同步子模块远程仓库的更新:</p><pre><code class="shell">$ cd ModuleA
$ git checkout master
$ git pull origin master</code></pre><p>然后在主仓库根目录中提交子模块仓库推送和更新:</p><pre><code class="shell">$ cd ..
$ git add .
$ git commit -m "update ModuleA"
$ git push</code></pre><p>此时发现远程主仓库已经同步子模块的更新内容,而且 Commit ID 值也同步成<code>6026b48</code> ,和子模块仓库中最新的 Commit ID 保持一致:</p><p><img src="/img/remote/1460000044639296" alt="image.png" title="image.png"></p><h4>2.2 子模块仓库更新主仓库中子模块代码的修改提交</h4><p>首先在主仓库的子模块目录下修改代码:</p><pre><code class="shell">//在主仓库中进入子模块ModuleA目录
$ cd ModuleA
//修改文件和提交和普通Git仓库提交流程一致:
//1.创建文件并提交
$ echo "ModuleA">updateModuleA.txt
$ git add .
$ git commit -m "update ModuleA"
//2.推送到远程ModuleA仓库
$ git push</code></pre><p>发现远程子模块仓库也同步完成:</p><p><img src="/img/remote/1460000044639297" alt="image.png" title="image.png"></p><p>接着需要将主仓库中的子模块 ModuleA 的变化同步到远程主仓库中具体的操作和上一节主仓库更新子模块仓库中的修改类似,用 add 和 commit 方式提交主仓库的更新,这个时候就完成了子模块和主仓库代码一致。</p><pre><code class="shell">$ cd ..
$ git add .
$ git commit -m "update ModuleA"
$ git push</code></pre><p>在实际的 IDE 环境中,只需要在子模块目录下进行代码修改:</p><p><img src="/img/remote/1460000044639298" alt="image.png" title="image.png"><br>然后在提交时先更新子模块 ModuleB 的远程仓库:<br><img src="/img/remote/1460000044639299" alt="image.png" title="image.png"><br>然后再同步远程主仓库中的 ModuleB 目录变化,这一步需要用命令行,直接在主目录提交没法更新远程主目录,但是本地主仓库的 ModuleB 目录确实发生了变化:</p><pre><code class="shell">$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ModuleB (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
</code></pre><p>需要命令行切换到主仓库目录执行 <code>git add </code>后再用 IDE 集成的 Git 提交操作:</p><pre><code class="shell">$ git add .</code></pre><p>再点击提交发现 IDE 中出现了 ModuleB 的更新变化: <br><img src="/img/remote/1460000044639300" alt="image.png" title="image.png"><br>接着推送到远程主仓库即可完成主仓库和子模块仓库的更新</p><h2>三、总结</h2><p>本文通过实践介绍子模块:</p><ul><li>包括子模块的初始化,如何使用 git submodule add 命令在主仓库添加子模块</li><li>如何在子模块和主仓库之间的更新和同步</li></ul><p>子模块主要有以下应用场景</p><ul><li>不同项目间需要共享同一个公共代码,如基础类库或工具包;</li><li>较大的项目需要拆分成多个子项目进行开发,通过子模块控制依赖关系;</li><li>第三方开源库或组件的本地开发与项目进行绑定;</li><li>需要隔离成组和组件版本的同时利用依赖关系,如微服务架构下的服务与组件;</li><li>子项目有较强的独立性同时也存在一定的耦合关系,通过子模块进行管理。</li></ul><p>同时从上面的时间可以看到,子模块和主仓库之间的同步比较复杂,维护成本高,子模块与主项目需要进行紧密的协调管理,否则很容易导致不同步的情况出现。在使用中要结合实际情况下操作,简单的项目就不要用子模块,采用分支管理可能更合适。</p><h3>参考资料</h3><p><a href="https://link.segmentfault.com/?enc=hSPGmZBYttqUt84DOng4uA%3D%3D.WFOHprGSPwvH%2FeMuicWVUeoAoCc0vZ7fj4ej5lbkRQVRDFfrYDju3H1HbzSK6XCofGu39Iookf8qXv9S13yDiw%3D%3D" rel="nofollow">公共模块管理之 Git Submodule 使用总结</a><br><a href="https://link.segmentfault.com/?enc=zccEmmvh7iynqJAF50qe4g%3D%3D.El3FjeADa5Uau7QpfteDdAo1HiM3tFWd829ynGUBqzXTsXq7SeEHyaF4cAwVGcQKWHZh2x1YCJf%2FXras%2FUhTqc4nef%2Bz113BvIm8kxfjuEpeYvRHa4WQeFS1E0CXItcX" rel="nofollow">Git - 子模块</a><br>《Git 权威指南 蒋鑫著》</p>
Spring Boot整合Postgres实现轻量级全文搜索
https://segmentfault.com/a/1190000044639185
2024-02-19T15:30:15+08:00
2024-02-19T15:30:15+08:00
程序猿DD
https://segmentfault.com/u/coderdd
0
<p>有这样一个带有搜索功能的用户界面需求:</p><p><img width="457" height="624" src="/img/bVdbsQZ" alt="" title=""></p><p>搜索流程如下所示:</p><p><img width="720" height="279" src="/img/bVdbsQ0" alt="" title=""></p><p>这个需求涉及两个实体:</p><ul><li>“评分(Rating)、用户名(Username)”数据与<code>User</code>实体相关</li><li>“创建日期(create date)、观看次数(number of views)、标题(title)、正文(body)”与<code>Story</code>实体相关</li></ul><p>需要支持的功能对<code>User</code>实体中的评分(Rating)的频繁修改以及下列搜索功能:</p><ul><li>按User评分进行范围搜索</li><li>按Story创建日期进行范围搜索</li><li>按Story浏览量进行范围搜索</li><li>按Story标题进行全文搜索</li><li>按Story正文进行全文搜索</li></ul><h2>Postgres中创建表结构和索引</h2><p>创建<code>users</code>表和<code>stories</code>表以及对应搜索需求相关的索引,包括:</p><ul><li>使用 btree 索引来支持按User评分搜索</li><li>使用 btree 索引来支持按Story创建日期、查看次数的搜索</li><li>使用 gin 索引来支持全文搜索内容(同时创建全文搜索列<code>fulltext</code>,类型使用<code>tsvector</code>以支持全文搜索)</li></ul><p>具体创建脚本如下:</p><pre><code class="sql">--Create Users table
CREATE TABLE IF NOT EXISTS users
(
id bigserial NOT NULL,
name character varying(100) NOT NULL,
rating integer,
PRIMARY KEY (id)
)
;
CREATE INDEX usr_rating_idx
ON users USING btree
(rating ASC NULLS LAST)
TABLESPACE pg_default
;
--Create Stories table
CREATE TABLE IF NOT EXISTS stories
(
id bigserial NOT NULL,
create_date timestamp without time zone NOT NULL,
num_views bigint NOT NULL,
title text NOT NULL,
body text NOT NULL,
fulltext tsvector,
user_id bigint,
PRIMARY KEY (id),
CONSTRAINT user_id_fk FOREIGN KEY (user_id)
REFERENCES users (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID
)
;
CREATE INDEX str_bt_idx
ON stories USING btree
(create_date ASC NULLS LAST,
num_views ASC NULLS LAST, user_id ASC NULLS LAST)
;
CREATE INDEX fulltext_search_idx
ON stories USING gin
(fulltext)
;</code></pre><h2>创建Spring Boot应用</h2><ol><li>项目依赖关系(这里使用Gradle构建):</li></ol><pre><code class="json">plugins {
id 'java'
id 'org.springframework.boot' version '3.1.3'
id 'io.spring.dependency-management' version '1.1.3'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}</code></pre><ol start="2"><li><code>application.yaml</code>中配置数据库连接信息</li></ol><pre><code class="yaml">spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: postgres</code></pre><ol start="3"><li>数据模型</li></ol><p>定义需要用到的各种数据模型:</p><pre><code class="java">public record Period(String fieldName, LocalDateTime min, LocalDateTime max) {
}
public record Range(String fieldName, long min, long max) {
}
public record Search(List<Period> periods, List<Range> ranges, String fullText, long offset, long limit) {
}
public record UserStory(Long id, LocalDateTime createDate, Long numberOfViews,
String title, String body, Long userRating, String userName, Long userId) {
}</code></pre><p>这里使用<a href="https://link.segmentfault.com/?enc=jkn29nmt0qlhVJlQ%2F%2B1d9w%3D%3D.5VKKEJMW7G%2Bil0w7RWLdbFl8UhcjUc70WE8t8QZGqoc%2BohpZDXJvPRtcU%2FSFsGev0t3g5sEa0ximDbP2cnsHQpMf8VnSX8zdWMMra8u9rBI%3D" rel="nofollow">Java 16推出的新特性record</a>实现,所以代码非常简洁。如果您还不了解的话,可以前往<a href="https://link.segmentfault.com/?enc=HPiD%2B4f2JORMT5liNw5PIw%3D%3D.MxHmP%2BgdMRZeM9hT86R5Y8o5arq5Kvbbq7cGETE%2F4IfIc1x5vmU4Fe9bbk4kZB9IIEi%2FEGrVK50JRRN14Acc%2BRrlh9MmD5%2Fz%2BgnJWiQB5%2Bs%3D" rel="nofollow">程序猿DD的Java新特性专栏</a>补全一下知识点。</p><ol start="4"><li>数据访问(Repository)</li></ol><pre><code class="java">@Repository
public class UserStoryRepository {
private final JdbcTemplate jdbcTemplate;
@Autowired
public UserStoryRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<UserStory> findByFilters(Search search) {
return jdbcTemplate.query(
"""
SELECT s.id id, create_date, num_views,
title, body, user_id, name user_name,
rating user_rating
FROM stories s INNER JOIN users u
ON s.user_id = u.id
WHERE true
""" + buildDynamicFiltersText(search)
+ " order by create_date desc offset ? limit ?",
(rs, rowNum) -> new UserStory(
rs.getLong("id"),
rs.getTimestamp("create_date").toLocalDateTime(),
rs.getLong("num_views"),
rs.getString("title"),
rs.getString("body"),
rs.getLong("user_rating"),
rs.getString("user_name"),
rs.getLong("user_id")
),
buildDynamicFilters(search)
);
}
public void save(UserStory userStory) {
var keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection
.prepareStatement(
"""
INSERT INTO stories (create_date, num_views, title, body, user_id)
VALUES (?, ?, ?, ?, ?)
""",
Statement.RETURN_GENERATED_KEYS
);
ps.setTimestamp(1, Timestamp.valueOf(userStory.createDate()));
ps.setLong(2, userStory.numberOfViews());
ps.setString(3, userStory.title());
ps.setString(4, userStory.body());
ps.setLong(5, userStory.userId());
return ps;
}, keyHolder);
var generatedId = (Long) keyHolder.getKeys().get("id");
if (generatedId != null) {
updateFullTextField(generatedId);
}
}
private void updateFullTextField(Long generatedId) {
jdbcTemplate.update(
"""
UPDATE stories SET fulltext = to_tsvector(title || ' ' || body)
where id = ?
""",
generatedId
);
}
private Object[] buildDynamicFilters(Search search) {
var filtersStream = search.ranges().stream()
.flatMap(
range -> Stream.of((Object) range.min(), range.max())
);
var periodsStream = search.periods().stream()
.flatMap(
range -> Stream.of((Object) Timestamp.valueOf(range.min()), Timestamp.valueOf(range.max()))
);
filtersStream = Stream.concat(filtersStream, periodsStream);
if (!search.fullText().isBlank()) {
filtersStream = Stream.concat(filtersStream, Stream.of(search.fullText()));
}
filtersStream = Stream.concat(filtersStream, Stream.of(search.offset(), search.limit()));
return filtersStream.toArray();
}
private String buildDynamicFiltersText(Search search) {
var rangesFilterString =
Stream.concat(
search.ranges()
.stream()
.map(
range -> String.format(" and %s between ? and ? ", range.fieldName())
),
search.periods()
.stream()
.map(
range -> String.format(" and %s between ? and ? ", range.fieldName())
)
)
.collect(Collectors.joining(" "));
return rangesFilterString + buildFulltextFilterText(search.fullText());
}
private String buildFulltextFilterText(String fullText) {
return fullText.isBlank() ? "" : " and fulltext @@ plainto_tsquery(?) ";
}
}</code></pre><ol start="5"><li>Controller实现</li></ol><pre><code class="java">@RestController
@RequestMapping("/user-stories")
public class UserStoryController {
private final UserStoryRepository userStoryRepository;
@Autowired
public UserStoryController(UserStoryRepository userStoryRepository) {
this.userStoryRepository = userStoryRepository;
}
@PostMapping
public void save(@RequestBody UserStory userStory) {
userStoryRepository.save(userStory);
}
@PostMapping("/search")
public List<UserStory> search(@RequestBody Search search) {
return userStoryRepository.findByFilters(search);
}
}</code></pre><h2>小结</h2><p>本文介绍了如何在Spring Boot中结合Postgres数据库实现全文搜索的功能,该方法比起使用Elasticsearch更为轻量级,非常适合一些小项目场景使用。希望本文内容对您有所帮助。如果您学习过程中如遇困难?可以加入我们超高质量的<a href="https://link.segmentfault.com/?enc=p7g4%2Bffen6p1vpvommJIvQ%3D%3D.OcSpA2NBqpiq6k9atfgQD6p%2B3PKumXYKXw%2FBxXADYNtEescHbC863M4NOr5lMSj%2F" rel="nofollow">Spring技术交流群</a>,参与交流与讨论,更好的学习与进步!更多<a href="https://link.segmentfault.com/?enc=VgPDzFG8nNnSTKXSEV7YgQ%3D%3D.7VXpaGXlH7%2BWb%2FNpq1kHCBUFNELgrkWZmcPGD%2BygZ5lqHyTN0RD59DK%2Bu9WZuxOf" rel="nofollow">Spring Boot教程可以点击直达!</a>,欢迎收藏与转发支持!</p><h2>参考资料</h2><ul><li><a href="https://link.segmentfault.com/?enc=iwH%2FZkzTMsYzeUqF4%2BGH9g%3D%3D.rHwwq54JP3ytC86cd%2Bf224SJBYpjpB%2FB2qVBk3doOJWBb%2BTILwmZb6KkixPgiTGpT2mq%2BfWDH42r8ok24ftnGK5iz4lN07azclAG6VB%2FXKi4PPtUWUk5bqRldQXlc8Ki" rel="nofollow">Postgres full-text search Spring boot integration</a></li><li><a href="https://link.segmentfault.com/?enc=NjMZQiYTMpQckJKPy7oXAQ%3D%3D.fkDoNoyAYgojk1Hk3eV7%2BCgnEYY9YsnUHRb3KPTRPOO0lTq3ip0JF0%2BDg5d%2FJpNlGNcsvzCLT2V0lnHhfm%2BeBTWFCxoqoxeCSdULy6PgijE%3D" rel="nofollow">Java 16新特性:record</a></li></ul><blockquote>欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源</blockquote>
GIT好习惯助你成为更出色的开发者
https://segmentfault.com/a/1190000044637962
2024-02-19T09:33:33+08:00
2024-02-19T09:33:33+08:00
南城FE
https://segmentfault.com/u/nanchengfe
4
<blockquote>本文翻译自 <a href="https://link.segmentfault.com/?enc=PpKDyITmnOwqYP%2Fhc5ASFQ%3D%3D.NGNNX24%2FV2RDOvtIqCJQwCC%2B1ibGu6SKZxwWDdJkdC0cwuhkVEDVQDbxBunV%2BWZnMltD3uoIDGA0SLmEDYwap%2FSDEYgep5Wv6m7%2BzKQrLmKrxD0HgWucQvpDf7cHsfVg" rel="nofollow">Be a better developer with these Git good practices</a>,作者:Anthony Vinicius, 略有删改。</blockquote><p>如果你是一名开发人员,你可能每天都在使用Git版本控制系统。无论是在团队中还是单独工作,使用此工具对于程序的开发过程都很重要。但在实际工作中却经常遇到提交不明确的消息,没有传达有用的信息,以及滥用分支等问题。了解如何正确使用Git并遵循良好的实践对于那些想要在工作中脱颖而出的人来说至关重要。</p><h3>Git分支的基本约定</h3><p>当我们使用代码版本控制时,我们应该遵循的主要良好实践之一是为分支、提交、拉取请求等使用清晰和描述性的名称。除了提高生产力之外,记录项目的开发过程,简化了团队沟通协作。通过遵循这些做法,你很快就会看到好处。</p><p>在此基础上,开源社区创建了一个分支命名约定,您可以在项目中遵循该约定。以下项目的使用是可选的,但它们可以帮助提高您的开发效率。</p><p><strong>1.小写</strong>:不要在分支名称中使用大写字母,坚持使用小写字母;</p><p><strong>2.连字符分隔</strong>:如果您的分支名称由多个单词组成,请用连字符分隔。避免PascalCase、camelCase或snake_case;</p><p><strong>3. (a-z,0-9)</strong>:在分支名称中仅使用字母数字字符和连字符。避免任何非字母数字字符;</p><p><strong>4.请不要使用连续的连字符(--)</strong>。这种做法可能令人困惑。如果您有分支类型(如功能、错误修复、热修复等),使用斜杠(/)代替;</p><p><strong>5.避免以连字符结束分支名称</strong>。这是没有意义的,因为连字符分隔单词,没有单词在最后分开;</p><p><strong>6.是最重要的</strong>:使用描述性的、简洁的、清晰的名称来解释在分支上做了什么;</p><h4>❎ 错误的分支名称</h4><ul><li><code>fixSidebar</code></li><li><code>feature-new-sidebar-</code></li><li><code>FeatureNewSidebar</code></li><li><code>feat_add_sidebar</code></li></ul><h4>✅ 好的分支名称</h4><ul><li><code>feature/new-sidebar</code></li><li><code>add-new-sidebar</code></li><li><code>hotfix/interval-query-param-on-get-historical-data</code></li></ul><h3>分支名称约定前缀</h3><p>有时候分支的目的并不明确。它可能是一个新功能,一个bug修复,文档更新,或其他任何东西。为了解决这个问题,通常的做法是在分支名称上使用前缀来快速解释分支的用途。</p><ul><li><strong>feature</strong>: 将要开发的新功能。例如,feature/add-filters;</li><li><strong>release</strong>:用于准备新版本。前缀release/通常用于在合并来自分支master的新更新以创建发布之前执行诸如最后一次修订之类的任务。例如,release/v3.3.1-beta;</li><li><strong>bugfix</strong>:你正在解决代码中的bug,并且通常与问题相关。例如,bugfix/sign-in-flow;</li><li><strong>hotfix</strong>:类似于bugfix,但它与修复生产环境中存在的关键错误有关。例如,hotfix/cors-error;</li><li><strong>docs</strong>:写一些文档。例如,docs/quick-start;</li></ul><p>如果您在工作流程中使用任务管理,如Jira、Trello、ClickUp或任何可以创建开发任务的类似工具,则每个任务都有一个关联的编号。所以,一般都可以用这些编号作为分支名称的前缀。举例来说:</p><ul><li>feature/T-531-add-sidebar</li><li>docs/T-789-update-readme</li><li>hotfix/T-142-security-path</li></ul><h3>提交信息</h3><p>提交消息在开发过程中非常重要。创造一个好的历史将帮助你很多次在你的开发过程中。像分支一样,提交也有社区创建的约定,你可以在下面了解到:</p><ul><li>提交消息有三个重要部分:主题、描述和页脚。提交的主题是必需的,它定义了提交的目的。描述(body)用于为提交的目的提供额外的上下文和解释。最后还有页脚,通常用于元数据,如分配提交。虽然同时使用描述和页脚被认为是一种良好的做法,但这不是必需的。</li><li>在主题行中使用祈使语气。举例:</li></ul><blockquote><p>Add README.md ✅;</p><p>Added README.md ❌;</p><p>Adding README.md ❌;</p></blockquote><ul><li>将主题行的第一个字母大写。举例:</li></ul><blockquote><p>Add user authentication ✅;</p><p>add user authentication ❌;</p></blockquote><ul><li>不要以句号结束主题行。举例:</li></ul><blockquote><p>Update unit tests ✅;</p><p>Update unit tests. ❌;</p></blockquote><ul><li>将主题长度限制为50个字符,要简明扼要;</li><li>正文以72个字符为单位换行,并将主题与空白行隔开;</li><li>如果你的提交正文有多个段落,那么用空行来分隔它们;</li><li>如有必要,可使用要点而非段落;</li></ul><h3>Conventional Commit's</h3><blockquote>“Conventional Commits规范是一个基于提交消息的轻量级约定。它提供了一组简单的规则来创建提交历史。“</blockquote><h4>结构</h4><pre><code class="shell"><type>[optional scope]: <description>
[optional body]
[optional footer(s)]</code></pre><h4>提交类型</h4><p>提交类型提供了一个清晰的上下文,关于在这个提交中所做的事情。下面你可以看到提交类型的列表以及何时使用它们:</p><ul><li>feat:引入新功能;</li><li>fix:代码错误的纠正;</li><li>refactor:用于代码更改重构,保留其整体功能;</li><li>chore:不影响生产代码的更新,包括工具、配置或库调整;</li><li>docs:对文档文件的添加或修改;</li><li>perf:代码更改增强性能;</li><li>style:与代码表示相关的调整,如格式和空白;</li><li>test:纳入或纠正试验相关代码;</li><li>build:影响构建系统或外部依赖项的修改;</li><li>ci:更改CI配置文件和脚本;</li><li>env:描述对配置项进程中的配置文件的调整或添加,例如容器配置参数。</li></ul><h4>范围</h4><p>作用域是一种可以添加在提交类型之后的结构,以提供额外的上下文信息:</p><ul><li><code>fix(ui): resolve issue with button alignment</code></li><li><code>feat(auth): implement user authentication</code></li></ul><h4>内容</h4><p>提交消息的主体提供了有关提交所引入的更改的详细说明。它通常添加在主题行之后的空白行之后。</p><p>示例:</p><pre><code>Add new functionality to handle user authentication.
This commit introduces a new module to manage user authentication. It includes
functions for user login, registration, and password recovery.</code></pre><h4>Footer</h4><p>提交消息的页脚用于提供与提交相关的附加信息。这可以包括详细信息,例如谁审查或批准了更改。</p><p>示例:</p><pre><code>Signed-off-by: John <john.doe@example.com>
Reviewed-by: Anthony <anthony@example.com></code></pre><h4>Breaking Change</h4><p>确认提交包含可能导致兼容性问题或需要修改相关代码的重大更改。您可以在页脚中添加<code>BREAKING CHANGE</code>或在类型/范围后包含<code>!</code>。</p><h3>使用常规提交的提交示例</h3><pre><code>chore: add commitlint and husky
chore(eslint): enforce the use of double quotes in JSX
refactor: type refactoring
feat: add axios and data handling
feat(page/home): create next routing
chore!: drop support for Node 18</code></pre><p>带主题、正文和页脚:</p><pre><code>feat: add function to convert colors in hexadecimal to rgba
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s.
Reviewed-by: 2
Refs: #345</code></pre><h4>最后</h4><p>文本分享了日常开发中的git相关操作,涉及分支创建以及代码提交格式规范和示例,希望对你有帮助。</p><hr><p><strong>看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~</strong></p><p>专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)</p>
【HTML】情人节给npy一颗炫酷的💗
https://segmentfault.com/a/1190000044632834
2024-02-14T20:20:48+08:00
2024-02-14T20:20:48+08:00
鑫宝Code
https://segmentfault.com/u/_628f7ec3df420
0
<h2>闲谈</h2><p>兄弟们,这不情人节快要到了,我该送女朋友什么🎁呢?哦,对了,差点忘了,我好像没有女朋友。<br><img src="/img/remote/1460000044632837" alt="图片" title="图片"></p><p><img src="/img/remote/1460000044632838" alt="图片" title="图片"><br>不过这不影响我们要过这个节日,我们可以学习技术。举个简单的🌰: 比如说,今天我们学习了如何画一颗炫酷的💗,以后找到了女朋友忘准备礼物了,是不是可以用这个救救场,🐶。</p><h2>开干</h2><p>首先,我们需要画一个💗的形状出来,例如下面这样<br><img src="/img/remote/1460000044632839" alt="图片" title="图片"><br>这个简单,我们通过豆包搜一波公式即可。公式如下:<br><img src="/img/remote/1460000044632840" alt="图片" title="图片"></p><p><img src="/img/remote/1460000044632841" alt="图片" title="图片"><br>思路:利用上面的公式,我们只需要根据许许多多的t去求得x,y的坐标,然后将这些点画出来即可。使用<code>Canvas</code>时用到的一些函数解释,这里<code>moveTo</code>和<code>lineTo</code>还是有点上头的:// 获取到一个绘图环境对象,这个对象提供了丰富的API来执行各种图形绘制和图像处理操作</p><pre><code class="js">ctx = canvas.getContext('2d');
/**
该方法用于在当前路径上从当前点画一条直线到指定的 (x, y) 坐标。
当调用 lineTo 后,路径会自动延伸到新指定的点,并且如果之前已经调用了 beginPath() 或 moveTo(),则这条线段会连接到前一个点。
要看到实际的线条显示在画布上,需要调用 stroke() 方法。
*/
ctx.lineTo(x, y);
/**
此方法用于移动当前路径的起始点到指定的 (x, y) 坐标位置,但不会画出任何可见的线条。
它主要用于开始一个新的子路径或者在现有路径之间创建空隙。当你想要从一个地方不连续地移动到另一个地方绘制时,就需要使用 moveTo。
*/
ctx.moveTo(x, y);</code></pre><blockquote>友情提示:上面的函数是个倒的爱心,所以Y轴要取负数。</blockquote><pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LoveCanvas</title>
<style>
body {
background: black;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const themeColor = "#d63e83";
// 爱心线的实体
let loveLine = null;
// 保存爱心方程的坐标
let XYPoint = [];
// 线条宽度,可自定义修改
const lineWidth = 5;
/**得到爱心方程的坐标 **/
function getXYPoint() {
const pointArr = [];
const enlargeFactor = 20;
for (let t = 0; t < 2 * Math.PI; t += 0.01) {
const x = 16 * Math.pow(Math.sin(t), 3) * enlargeFactor;
const y =
-(
13 * Math.cos(t) -
5 * Math.cos(2 * t) -
2 * Math.cos(3 * t) -
Math.cos(4 * t)
) * enlargeFactor;
// 将爱心的坐标进行居中
pointArr.push({ x: canvas.width / 2 + x, y: canvas.height / 2 + y });
}
return pointArr;
}
class LoveLine {
constructor(pointXY) {
this.pointXY = pointXY;
}
draw() {
for (let point of this.pointXY) {
ctx.lineTo(point.x, point.y);
ctx.moveTo(point.x, point.y);
}
ctx.strokeStyle = themeColor;
ctx.lineWidth = lineWidth;
ctx.stroke();
ctx.fill();
}
}
function initLoveLine() {
XYPoint = getXYPoint();
loveLine = new LoveLine(XYPoint);
loveLine.draw();
}
function init() {
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width;
canvas.height = height;
initLoveLine();
}
// 如果需要保持在窗口大小变化时也实时更新canvas尺寸
window.onresize = init;
init();
</script>
</html></code></pre><h2>粒子特效</h2><p>这么快就做好了,是不是显得不是很够诚意?<br><img src="/img/remote/1460000044632842" alt="图片" title="图片"></p><p><img src="/img/remote/1460000044632843" alt="图片" title="图片"><br>我们可以加入一波粒子特效,这里我采用的方案是基于之前的<code>Canvas</code>+<code>requestAnimationFrame</code>来做。<br>效果如下:<br><img src="/img/remote/1460000044632844" alt="图片" title="图片"><br>首先什么是<code>requestAnimationFrame</code>呢?<br>参见MDN</p><blockquote>你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行也就是我们可以使用这个函数达到每10ms刷新一次界面达到动态的效果。</blockquote><p>首先我们定义一个<code>粒子</code>类</p><pre><code class="js">// 粒子点的类
class Dot {
constructor(x, y, initX, initY) {
// 原始点的坐标,用来圈定范围
this.initX = initX;
this.initY = initY;
this.x = x;
this.y = y;
this.r = 1;
// 粒子移动的速度,也就是下一帧,粒子在哪里出现
this.speedX = Math.random() * 2 - 1;
this.speedY = Math.random() * 2 - 1;
// 这个粒子最远能跑多远
this.maxLimit = 15;
}
// 绘制每一个粒子的方法
draw() {
ctx.beginPath();
ctx.fillStyle = themeColor;
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
move() {
if (Math.abs(this.x - this.initX) >= this.maxLimit)
this.speedX = -this.speedX;
if (Math.abs(this.x - this.y) >= this.maxLimit)
this.speedY = -this.speedY;
this.x += this.speedX;
this.y += this.speedY;
this.draw();
}
}</code></pre><p>我们在定义两个使用到粒子函数的方法.</p><ol><li><code>initDots</code>函数,该函数主要是将粒子点初始化,并且画出来。</li></ol><pre><code class="js">function initDots(x, y) {
XYPoint = getXYPoint();
dots = [];
for (let point of XYPoint) {
for (let i = 0; i < SINGLE_DOT_NUM; i++) {
const border = Math.random() * 5;
const dot = new Dot(
border + point.x,
border + point.y,
point.x,
point.y
);
dot.draw();
dots.push(dot);
}
}
}</code></pre><ol start="2"><li><code>moveDots</code>函数,顾名思义,也就是移动粒子点</li></ol><pre><code class="js">function moveDots() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
loveLine.draw();
for (const dot of dots) {
dot.move();
}
animationFrame = window.requestAnimationFrame(moveDots);
}</code></pre><p>完整代码如下:</p><pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LoveCanvas</title>
<style>
body {
background: black;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const themeColor = "#d63e83";
// 爱心线的实体
let loveLine = null;
// 保存爱心方程的坐标
let XYPoint = [];
// 线条宽度,可自定义修改
const lineWidth = 5;
// 每个原来的点对应的粒子数目
const SINGLE_DOT_NUM = 15;
// 粒子点的集合
let dots = [];
let animationFrame = null;
/**得到爱心方程的坐标 **/
function getXYPoint() {
const pointArr = [];
const enlargeFactor = 20;
for (let t = 0; t < 2 * Math.PI; t += 0.01) {
const x = 16 * Math.pow(Math.sin(t), 3) * enlargeFactor;
const y =
-(
13 * Math.cos(t) -
5 * Math.cos(2 * t) -
2 * Math.cos(3 * t) -
Math.cos(4 * t)
) * enlargeFactor;
// 将爱心的坐标进行居中
pointArr.push({ x: canvas.width / 2 + x, y: canvas.height / 2 + y });
}
return pointArr;
}
class LoveLine {
constructor(pointXY) {
this.pointXY = pointXY;
}
draw() {
for (let point of this.pointXY) {
ctx.lineTo(point.x, point.y);
ctx.moveTo(point.x, point.y);
}
ctx.strokeStyle = themeColor;
ctx.lineWidth = lineWidth;
ctx.stroke();
ctx.fill();
}
}
function initLoveLine() {
XYPoint = getXYPoint();
loveLine = new LoveLine(XYPoint);
loveLine.draw();
}
// 粒子点的类
class Dot {
constructor(x, y, initX, initY) {
this.initX = initX;
this.initY = initY;
this.x = x;
this.y = y;
this.r = 1;
this.speedX = Math.random() * 2 - 1;
this.speedY = Math.random() * 2 - 1;
this.maxLimit = 15;
}
// 绘制每一个粒子的方法
draw() {
ctx.beginPath();
ctx.fillStyle = themeColor;
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
move() {
if (Math.abs(this.x - this.initX) >= this.maxLimit)
this.speedX = -this.speedX;
if (Math.abs(this.x - this.y) >= this.maxLimit)
this.speedY = -this.speedY;
this.x += this.speedX;
this.y += this.speedY;
this.draw();
}
}
function initLoveLine() {
XYPoint = getXYPoint();
loveLine = new LoveLine(XYPoint);
loveLine.draw();
}
function initDots(x, y) {
XYPoint = getXYPoint();
dots = [];
for (let point of XYPoint) {
for (let i = 0; i < SINGLE_DOT_NUM; i++) {
const border = Math.random() * 5;
const dot = new Dot(
border + point.x,
border + point.y,
point.x,
point.y
);
dot.draw();
dots.push(dot);
}
}
}
function moveDots() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
loveLine.draw();
for (const dot of dots) {
dot.move();
}
animationFrame = window.requestAnimationFrame(moveDots);
}
function init() {
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width;
canvas.height = height;
if (animationFrame) {
window.cancelAnimationFrame(animationFrame);
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
initLoveLine();
initDots();
moveDots();
}
// 如果需要保持在窗口大小变化时也实时更新canvas尺寸
window.onresize = init;
init();
</script>
</html></code></pre><blockquote>注:大家如果觉得中间那条线不好看,可以去掉initLoveLine()即可。</blockquote><h2>最后</h2><p>祝今天有情人终成眷属,无情人早日找到心仪的另一半,哈哈</p>
“AI”送祥瑞:“千帆杯”第一期10万大奖揭晓!
https://segmentfault.com/a/1190000044633962
2024-02-16T14:04:00+08:00
2024-02-16T14:04:00+08:00
SegmentFault思否
https://segmentfault.com/u/segmentfault
1
<p>距离千帆杯 AI 原生应用开发挑战赛第一期赛题“游乐场排队规划助手”的发布,已经过去了整整 14 天。在为期一周的报名挑战时间内,<strong>我们汇总收到了近 300+ 的作品提交</strong>,在专业的评审和仔细考量下,决胜出了第一期赛题 TOP10 选手。</p><p>百度智能云千帆杯·AI 原生应用开发挑战赛,旨在激发广大开发者的创意潜能,推动 AI 原生应用在中国市场的蓬勃发展。大赛以“创意无限·生成未来”为主题,紧密围绕当前 AI 技术的前沿动态和应用趋势,借助百度智能云千帆 AppBuilder 和 ModelBuilder 两大智能开发助手,鼓励参赛者打造出更多具有创新性、实用性和社会价值的 AI 原生应用。</p><h2>“千帆杯” 迎来第一期「最强挑战者」</h2><p>第一期赛题聚焦春节假期游乐园排队效率问题,鼓励开发者利用 AI 能力施展“时间魔法”,打造一款具有实用性的“游乐场排队规划助手”,帮助游客更好地了解乐园的排队情况,设计个性化的游玩路线,在有限的时间内获得最“High”的体验,同时为管理者提供优化运营策略的决策支持。</p><p><strong>第一期最强挑战者杨之正,带来作品“香港迪士尼游乐场路线规划助手”。</strong>基于 AppBuilder 平台提供的强大开发套件和资源环境,使用平台预置的 Agent 框架,以零代码的方式创建 Agent 智能体,自动化调用各种工具打造游乐场排队规划助手 AI 原生应用。游客只需要输入时间预算和游玩喜好,Agent 智能体就能生成并执行 Python 代码,求解优化问题,智能规划出游玩项目路线。</p><p>百度智能云千帆 AppBuilder,基于大模型构建并开放了 Agent 智能体框架,实现自动任务规划及代码生成与执行能力,使得原本只能通过专业算法编程才能解决的问题,能够通过自然语言指令配置和对话交互完成,极大降低了AI应用开发门槛,创造了无限的想象空间。</p><p>目前,百度智能云千帆 AppBuilder 作为国内唯一全面开放的具备代码规划与执行能力的平台,将大模型应用开发所需的框架和组件都做成了可扩展和可拼接的形式,<strong>每位开发者都可以利用 AppBuilder 来基于自然语言构建自己的“程序员”,快速打造属于自己的 AI 原生应用,探索 AI 大模型的无限可能。</strong></p><p><img src="/img/remote/1460000044633966" alt="图片" title="图片"></p><blockquote>龙年彩头到<br>新春佳节笑<br>千帆十强耀<br>技术风采妙</blockquote><p>(本诗由AI生成)</p><p><strong>恭喜第一期 TOP 10 选手</strong><br><strong>精彩的挑战持续进行</strong><br><strong>「贺岁灵感」的奇思妙想仍在发散</strong><br><strong>第二期十万大奖</strong><br><strong>等你来战</strong></p><hr><p>点击文末 <strong><a href="https://link.segmentfault.com/?enc=bFVOBV%2BTXDthzSFee0VVrA%3D%3D.WkAuCjgWUZNKcylaLAlzuVm4LBJhIL7rlpvAnLZEF%2BvqvNuwJ6eH5SdJpV4UfQmm" rel="nofollow">阅读原文</a></strong> 可了解第二期详细赛题说明哦~</p><h3>相关推荐</h3><p><a href="https://segmentfault.com/a/1190000044627183">千帆杯第二期赛题发布!你的贺岁灵感会是下一个流量密码吗</a></p><p><a href="https://segmentfault.com/a/1190000044609186">千帆杯首期赛题公布!一起探索 AI 原生应用的时间魔法</a></p>
千帆杯第二期赛题发布!你的贺岁灵感会是下一个流量密码吗
https://segmentfault.com/a/1190000044627183
2024-02-08T13:14:44+08:00
2024-02-08T13:14:44+08:00
SegmentFault思否
https://segmentfault.com/u/segmentfault
1
<p>春节将至,年味渐浓。譬如 AI 生成红包封面,AI 生成春联,AI 生成拜年视频等等一些应景的生成式 AI 新玩法正在被开发者们解锁。</p><p><img src="/img/remote/1460000044627185" alt="图片" title="图片"></p><h2>让“二大爷”更像你的“二大爷”</h2><p>不久前,一款名为《决战·拜年之巅》的大模型对话游戏在各大群聊中被热烈讨论。在这款游戏中,玩家需向七大姑、八大姨、二大爷和大姑妈等 AI 亲戚拜年,并应对亲戚们对结婚、工作等话题的讨论。亲戚们的话题围绕工作、结婚生子、家庭等颇具现实压力的内容展开,每轮亲戚对话上限为 8 次,每次对话都将造成亲戚情绪变化,只有顺利地与每位家人交流结束,才能通关游戏。</p><p>与过去的游戏交互不同,利用生成式 AI,玩家无需点击具体的选项,而是能够在对话框中输入自己的回答,自由地表达自己的想法。有人用它模拟拜年场景调整与亲戚的沟通方式,也有人用它释放现实中无法宣之于口的焦虑。</p><p>事实上,这并不是第一款爆火的基于大模型的对话游戏。在此之前,由姚班天才范浩强开发的《完蛋!我被大模型包围了》,以及来自独立开发者王登科的《哄哄模拟器》都备受追捧。充满沉浸感和代入感的大模型对话游戏中或许藏着流量密码,但更重要的共识是,让模型学习到海量文本中蕴含的潜在知识,将会进一步提升各个 NLP 任务效果。简单来说,就是让“二大爷”更像你的“二大爷”。</p><p>现在,开发者们已经可以在千帆 ModelBuilder 上很经济、很简单地调用百度自研的 ERNIE-Speed ,训练出具有更强的文本理解、内容创作、对话问答等能力的专用模型。</p><h2>解锁你的贺岁灵感</h2><p>做一个应用,找到合适的模型至关重要。千帆杯· AI原生应用开发挑战赛第二期赛题将围绕“贺岁灵感模型”展开,鼓励开发者使用 ERNIE-Speed 打磨一个专属春节文案的精调模型,要求对模型精调使其保持原有能力的同时,具备准确理解并执行文案创作中创作长度相关指令的能力。</p><p>在本期,开发者可充分利用百度智能云千帆 Modelbuilder 中提供的一整套包括数据管理、模型精调、模型评估与优化、推理服务部署等全链路的模型开发工具链,结合相关数据,基于 ENRIE-Speed 调优生成符合赛题主题要求且效果优秀的模型。此外,官方还将提供“春节文案”基础数据集,同时也支持用户做延伸和扩充。</p><p><img src="/img/remote/1460000044627186" alt="图片" title="图片"></p><p>目前,百度智能云千帆 ModelBuilder 已形成了一整套完整的模型开发工具链,累计精调了 10000 个模型,为大模型能力重构之下的 AI 原生应用的涌现奠定了坚实的基础。</p><p>依托千帆 ModelBuilder ,本期开发挑战赛将继续贯彻百度“不仅专注于前瞻技术探索,更致力通过技术应用解决实际问题”的理念,携手更多开发者奔赴生成未来。</p>
聊聊Git 合并和变基
https://segmentfault.com/a/1190000044595999
2024-01-28T17:49:08+08:00
2024-01-28T17:49:08+08:00
归思君
https://segmentfault.com/u/guisijun
4
<h2>一、 Git Merge 合并策略</h2><h3>1.1 Fast-Forward Merge(快进式合并)</h3><pre><code class="shell">//在分支1下操作,会将分支1合并到分支2中
git merge <分支2></code></pre><p>最简单的合并算法,它是在一条不分叉的两个分支之间进行合并。快进式合并是默认的合并行为,并没有产生新的commit。如下图所示,虚线部分是合并前,在经过如下命令后:</p><pre><code class="shell">//当前在Main分支下操作
git merge Dev </code></pre><p><img src="/img/remote/1460000044596001" alt="image.png" title="image.png"><br>Git 将 Main 和 HEAD 指针移动到 Dev 所在的提交对象上,此时合并完成,不涉及到内容的变更和比较,所以这种合并方式效率很高。从上面的图可以发现,这种合并策略要求合并的两个分支上的提交,必须是一条链上的前后关系。<br>可以通过<code>git merge --no-off</code>参数来进行关闭快进式合并,关闭后会强制使用<code>Three Way Merge</code>(三路合并),下面来具体讲讲三路合并</p><h3>1.2 Three Way Merge 三路合并</h3><p>当两个分支的提交对象不在一条提交链上时,Git 会默认执行三路合并的方式进行合并。<br>首先 Git 会通过算法寻找两个分支的最近公共祖先节点,再将找到的公共祖先节点作为base节点,并使用三路合并的策略来进行合并,也就是我们常见的合并方式(三路指的是两个需要合并的分支的提交对象(比如下图中的提交对象 1,2),最近共同祖先(提交对象 0)三个对象):<br><img src="/img/remote/1460000044596002" alt="image.png" title="image.png"><br>可以通过传递不同参数来使用不同的合并策略:<br><code>git merge [ -s <strategy>] [-X <strategy-option>] <commit>...</code></p><ul><li>参数 <code>-s <strategy></code>用于设定合并策略</li><li><p>参数 <code>-X <strategy-option></code>用于为所选的合并策略提供附加的参数</p><h4>1.3.2 <code>Resolve</code>策略</h4><p>Resolve 策略是默认的三路合并策略,既可以使用 <code>git merge <分支></code>又可以使用 <code>git merge -s resolve <分支></code>来执行合并,该合并策略只能用于合并两个分支,也就是当前分支和另外的一个分支,使用三路合并策略。这种合并策略被认为是最安全、最快的合并分支策略。<br>比如在 Main 分支下执行以下命令:</p><pre><code class="shell">//当前在Main分支下操作
git merge Dev</code></pre><p><img src="/img/remote/1460000044596003" alt="image.png" title="image.png"><br>Git 会创建一个新的提交对象(如上图中的提交对象 3),然后该提交对象将合并提交对象 1 和提交对象 2 的内容,形成提交对象 3。但是一旦遇到两个分支对象有多个共同祖先时,此时 Resolve 策略就无法实现合并了,这个时候就需要用 <code>Recursive</code>策略。</p><h4>1.3.1 Recursive 策略</h4><p>如下图所示,在对两个分支上的提交A和B进行合并时,我们发现了它们有两个共同祖先,分别是:<code>ancestor0</code> 和 <code>ancestor1</code>。这就是 <code>Criss-Cross</code> 现象:<br><img src="/img/remote/1460000044596005" alt="image.png" title="image.png"><br>我们在此处复现一下这个现象:</p><pre><code class="shell">//1.初始化一个recursiveMerge仓库
$ git init recursiveMerge
//2.创建一个master-commit1.txt文件并提交
$ git commit -m "master-commit1"
[master (root-commit) 85cba5f] master-commit1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 master-commit1.txt
//3.新建一个分支dev,并切换到dev。在dev分支创建一个dev-commit1.txt文件并提交
$ git checkout -b dev
Switched to a new branch 'dev'
$ git commit -m "dev-commit1"
[dev 9763dcc] dev-commit1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev-commit1.txt
//4.切换到master分支,创建一个master-commit2.txt文件并提交
$ git checkout master
Switched to branch 'master'
$ git commit -m "master-commit2"
[master 5c23645] master-commit2
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 master-commit2.txt
//5.新建一个分支feature, 切换到feature,合并dev和feature
$ git merge dev
hint: Waiting for your editor to close the file... unix2dos: converting file D:/recursiveMerge/.git/MERGE_MSG to DOS format...
libpng warning: iCCP: known incorrect sRGB profile
libpng warning: iCCP: known incorrect sRGB profile
dos2unix: converting file D:/recursiveMerge/.git/MERGE_MSG to Unix format...
Merge made by the 'ort' strategy.
dev-commit1.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev-commit1.txt
//6.切换到master,合并dev和master
$ git merge dev
hint: Waiting for your editor to close the file... unix2dos: converting file D:/recursiveMerge/.git/MERGE_MSG to DOS format...
libpng warning: iCCP: known incorrect sRGB profile
libpng warning: iCCP: known incorrect sRGB profile
dos2unix: converting file D:/recursiveMerge/.git/MERGE_MSG to Unix format...
Merge made by the 'ort' strategy.
dev-commit1.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev-commit1.txt
</code></pre><p>我们在 Git GUI 中查看此时的分支图像:<br><img src="/img/remote/1460000044596006" alt="image.png" title="image.png"><br>那么Recursive 策略对于这种情况是怎么做的呢?<br>如果Git在寻找共同祖先时,在参与合并的两个分支上找到了不只一个满足条件的共同祖先,它会先对共同祖先进行合并,建立临时快照。然后,把临时产生的“虚拟祖先”作为合并依据,再对分支进行合并。对于上图而言,会将<code>ancestor 0</code> 和 <code>ancestor 1</code> 先合并成一个虚拟祖先 <code>ancestor 2</code>,最后再将它与A和B提交一起进行三路合并:<br><img src="/img/remote/1460000044596007" alt="image.png" title="image.png"><br>在处理合并时,有可能会出现不同分支在相同文件上的冲突,因此可以使用下列选项来在发生冲突时起作用,常见的有:</p><h5><code>Ours</code>选项</h5><p>在遇到冲突时,选择当前分支版本,而忽略其他分支版本。如果他人的改动和本地改动不冲突,会将他人的改动合并进来:</p><pre><code class="shell"># 在冲突合并中,选择当前分支内容,自动丢弃其他分支内容
git merge -s recursive -Xours</code></pre><h5><code>Theirs</code>选项</h5><p>和 <code>Ours</code>选项相反,遇到冲突时,选择他人的版本,丢弃当前分支版本</p><pre><code class="shell"># 选择其他分支内容,自动丢弃当前分支内容
git merge -s recursive -Xtheis</code></pre><p>此外还有 <code>subtree[=<path>]</code>,<code>renormalize</code>,<code>no-renormalize</code>等等选项,具体可以看官方文档<a href="https://link.segmentfault.com/?enc=5FomBksHXOVTuwTb9tP4bg%3D%3D.3%2FOfmgZvVE0ICrUUvx2Z6pTSUOjj%2FSBPFKcH9RSzDVtpVpy4aeQaQO8PNTYxT47i" rel="nofollow">Git - merge-strategies Documentation</a></p><h3>1.3 <code>Octopus Merge</code> 多路合并</h3><pre><code class="shell">git merge -s octopus <分支1> <分支2> ... <分支N></code></pre><p>此合并策略可以合并两个以上的分支,但是拒绝执行需要手动解决的复杂合并,它的主要用途是将多个分支合并到一起,该策略是对三个及三个以上的分支合并时的默认合并策略。<br><img src="/img/remote/1460000044596008" alt="" title=""><br>来做一个实验,此时我的 Git 仓库中有三个分支 feature1,feature2,feature3,执行 octopus 合并:</p><pre><code class="shell">$ git merge -s octopus feature1 feature2 feature3
Trying simple merge with feature1
Trying simple merge with feature2
Trying simple merge with feature3
Merge made by the 'octopus' strategy.
feature1-1.txt | 0
feature3-2.txt | 0
four.txt | 0
second.txt | 0
third-2.txt | 0
third.txt | 0
6 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 feature1-1.txt
create mode 100644 feature3-2.txt
create mode 100644 four.txt
create mode 100644 second.txt
create mode 100644 third-2.txt
create mode 100644 third.txt</code></pre><p><img src="/img/remote/1460000044596009" alt="image.png" title="image.png"></p><h4>1.3.3 <code>subtree</code>策略</h4><p>这是一个经过调整的 <code>recursive</code>策略,当合并树 A 和树 B 时,如果树 B 和树 A 的一个子树相同,B 树首先进行调整以匹配 A 的树结构,以免两棵树在同一级别进行合并。同时也针对两棵树的共同祖先调整。</p><pre><code class="shell">git merge -s subtree <树A> <树B></code></pre><p>该部分会单独再出一篇,来姐姐子树合并和子模块合并</p><h4>1.3.4 <code>ours</code> 策略</h4><p>这种策略可以合并任意数量的分支,但是合并的结果总是使用当前分支的内容,丢弃其他分支的内容。<br>这种模式和上面的 <code>recursive</code>策略中的 <code>ours</code>选项不同,<code>ours</code> 选项是在某个分支下,忽略其他人的版本,<code>ours</code>策略是忽略其他的分支。</p><h2>二、 Git Rebase 变基</h2><blockquote><code>rebase</code>是指在另一个基端之上重新应用提交。很多时候我们在两个分支上用<code>rebase</code><strong>。但是</strong><code>**rebase**</code><strong>命令的作用对象不仅仅限于分支。任何的提交引用,都可以被视作有效的</strong><code>**rebase**</code><strong>基底对象。包括一个提交ID、分支名称、标签名称或者</strong><code>**HEAD~1**</code><strong>这样的相对引用。</strong></blockquote></li></ul><h3>2.1 标准模式</h3><p><code>git rebase</code>默认是标准模式,一般用于多个分支之间:在一个分支中集成另外分支的最新修改。</p><pre><code class="shell">#当前在Feature分支下操作,会得到如下的变基状态
git checkout Feature
git rebase Main</code></pre><p><img src="/img/remote/1460000044596010" alt="image.png" title="image.png"></p><pre><code class="shell">#当前在branch1分支下操作,会得到如下的变基状态
git checkout Branch1
#将Main分支中没有Branch1分支的提交,拷贝到Branch2分支中
git rebase Main --onto Branch2</code></pre><p><img src="/img/remote/1460000044596011" alt="image.png" title="image.png"><br>当执行<code>git rebase</code>命令后,整个变基过程都会自动进行,如果在该命令后加上<code>-i</code>参数后,就会进入到交互模式:</p><h3>2.2 交互模式</h3><p><code>git rebase -i</code>或者<code>git rebase -interactive</code>开始后执行一个交互式rebase 会话。一般用于当前分支的历史提交进行编辑操作。</p><pre><code class="shell"># 针对该commit 后的提交进行交互式操作
git rebase -i [commit ID]</code></pre><p>运行命令后,会跳出交互界面,比如在git bash中输入如下命令:<code>git rebase -i ecd259f</code> <br>页面会出现下面的界面:<br><img src="/img/remote/1460000044596012" alt="image.png" title="image.png"><br>上图中的两条记录是提交列表,指的是 ID 为<code>ecd259f...</code>的 commit 之前的提交。我们可以对这两个提交进行相关的操作,在上面的页面中也提供了几个命令:</p><ul><li><code>pick</code>:保留该commit,和drop相对,也就是什么也不做(在IDEA中不选择,直接rebase,也会得到如下结果)</li><li><code>reword</code>:修改该提交的commit message,如果该commit之后也有提交,那么之后的提交信息也会被改变</li><li><code>edit</code>: 选择一个提交并修改该提交的内容</li><li><code>squash</code>:合并该提交与上一个提交,也可以合并多个,两两合并,间隔合并</li><li><code>fixup</code>:与squash类似,但是会抛弃当前的commit message</li><li><code>exec</code>: 执行shell命令,运行一条自定义命令</li></ul><p>可以在提交列表前面进行参数修改,比如将 <code>pick</code>改成 <code>squash</code>就是将两个提交合并到 ID 为 <code>ecd259f...</code>的提交对象中</p><pre><code class="shell">squash a5eb754 first commit --init
squash 2df01ad ignoreList</code></pre><h2>三、总结</h2><h3>Git merge 和 Git rebase 的区别与联系</h3><p>在日常对冲突的处理中,很明显的区别在于 rebase 的处理方式能让提交链更加清晰,而使用 merge 方式会显得提交链复杂交错。<br>下面我们具体来看看两者的区别与联系</p><h4>区别</h4><ol><li><strong>合并方式</strong>:<code>git merge</code> 创建一个新的合并提交,将两个分支的更改合并到一起,而 <code>git rebase</code> 将一系列提交从一个分支转移到另一个分支,并重新组织这些提交:</li><li><strong>提交历史</strong>:<code>git merge</code> 保留每个分支的独立提交历史,形成一个合并的提交历史树,而git rebase修改提交历史的结构,使提交历史更线性:</li></ol><p><img src="/img/remote/1460000044596013" alt="image.png" title="image.png"></p><ol start="3"><li><strong>冲突处理</strong>:在合并过程中,如果存在冲突,<code>git merge</code> 和 <code>git rebase</code> 都需要手动解决冲突。但是,解决冲突的方式略有不同。<code>git merge</code> 在合并提交中保留冲突的解决方案,而 <code>git rebase</code> 在每个冲突点停下来,必须在解决冲突后才能继续进行rebase操作。</li><li><p><strong>提交ID</strong>:由于 <code>git rebase</code> 重新应用提交,所以重新组织后的提交具有新的提交ID,而 <code>git merge</code> 创建的合并提交保留了原始提交的ID。比如上图中,<code>git rebase</code> 命令执行后,对应的 master 分支的三个 commit 对象的提交 ID 都要被修改。</p><h4>联系</h4></li><li><strong>合并操作</strong>:无论是 <code>git merge</code> 还是 <code>git rebase</code>,它们都是用于将一个分支的更改合并到另一个分支上。两者都是在对分支进行合并,是来解决冲突的。</li><li><strong>版本同步</strong>:无论是 <code>git merge</code> 还是 <code>git rebase</code>,它们都可以用于将分支与上游分支同步,以确保分支包含上游的最新更改。</li><li><p><strong>解决冲突</strong>:无论是 <code>git merge</code> 还是 <code>git rebase</code>,在合并过程中都可能存在冲突,需要手动解决冲突。</p><h3>参考资料</h3><p><a href="https://link.segmentfault.com/?enc=V32i%2B%2Bwu%2BBsNJ5W1FaCttw%3D%3D.hkA%2BHElATAPPV3C%2Fd3pHdWtPqo26DJeo5Htlem4vUdNVuQuXsRVRihRZ0JsjPsAXfIGtPUVxwRh%2BDLWSGi6Nkg%3D%3D" rel="nofollow">https://www.geeksforgeeks.org/merge-strategies-in-git/</a><br><a href="https://link.segmentfault.com/?enc=bcTePwpSCVvek48sAZXhIg%3D%3D.7Fe4QQvAkukqJQHCPYBnUU5kdI2pv30z8C0ROU%2BMirrfurMq4cCO4DkoFB08whtEeIjNxloaKyTzF7hRkDWXJg%3D%3D" rel="nofollow">https://morningspace.github.io/tech/git-merge-stories-2/</a><br><a href="https://link.segmentfault.com/?enc=HRu24WYSt6myfJRs2flEkA%3D%3D.%2BmuPR17k7KD%2FDcULTrda1S3%2BRWsGz2WxljR2Bfr1rfq5P6lVK97XiiH4%2FymeKNdF" rel="nofollow">https://git-scm.com/docs/merge-strategies/</a></p></li></ol>
深度学习-最简代码实现目标检测模型
https://segmentfault.com/a/1190000044614610
2024-02-03T00:19:31+08:00
2024-02-03T00:19:31+08:00
汤青松
https://segmentfault.com/u/songboy
0
<h2>一、项目介绍</h2><blockquote><p>在深度学习领域中,目标检测一直是一个备受关注的研究方向。为了更深入地理解深度学习目标检测的原理和实现,我写了一个简单的单目标检测项目。在这个项目中,<br>我用最简单的方式实现了数据迭代器、网络模型、预测脚本和训练模型脚本,以及一些辅助脚本,通过这个过程提高对目标检测的认识和实践能力。</p><p>项目地址:<a href="https://link.segmentfault.com/?enc=G4Chr3BhQwJfXnpVapPFfw%3D%3D.TvdFsZZjcF2r3AkvYBr6f5XO1hugwAjwPZRbR4OT41OlcbVcbPAziSQ4ad7ZsArQ" rel="nofollow">https://github.com/78778443/QingNet</a></p></blockquote><h3>1.1 项目概要</h3><p>要实现目标检测系统,离不开数据加载器,网络模型,训练脚本,预测脚本这四大项;</p><ol><li>数据加载器的作用是将数据集加载出来,并将数据集的标注数据给格式化,便于后续训练;</li><li>网络模型的主要作用是提取网络特征,比如你给一张图,他把图里面的特征信息提取并返回给你;</li><li>训练脚本的主要作用是桥接数据集和网络模型,通常是给模型一个图片,模型返回特征结果后,对结果进行偏差(损失)计算;</li><li>预测脚本的主要作用是训练好一个模型(权重)后,将模型(权重)文件用于实际生产;</li></ol><h3>1.2 项目结构</h3><p>这个项目的结构相对简单,主要涉及以下几个文件:</p><ul><li>data.py: 数据迭代器,负责加载和处理训练和测试数据。</li><li>net.py: 网络模型的定义,包括卷积层、激活函数以及输出标签、位置、排序和置信度的信息。</li><li>train.py: 训练模型的脚本,包括数据加载、模型训练、损失函数计算、优化器更新等过程。</li><li>predict.py: 预测脚本,用训练好的模型进行单张图像的预测。</li><li>tools.py: 辅助脚本,用于可视化预测结果。</li></ul><h3>1.3 项目运行</h3><p>在运行项目时,只需执行<code>python train.py</code>命令即可。<br>如果缺少相关依赖包,可以通过使用pip进行安装。</p><pre><code class="bash">python train.py
train_loss 0===>> 0.8435055017471313
train_loss 10===>> 0.8142958283424377
train_loss 40===>> 0.8188565373420715
test_loss 0===>> 0.8148629665374756
test_loss 10===>> 0.8028237223625183
sort_acc 14==>> tensor(0.0397)
train_loss 0===>> 0.8068220615386963</code></pre><h2>二、数据集处理</h2><p>在做这个项目之前,要准备一批数据集,我将数据集文件放在data文件夹下,文件名里面包含图片序号,是否有目标,目标的四个坐标点,并用逗号隔开</p><pre><code class="zsh">(base) ➜ tree data
data
├── test
├── 1.0.0.0.0.0.0.jpg
├── 1.1.163.54.290.181.6.jpg
├── ....过长省略......
└── train
├── 1000.0.0.0.0.0.0.jpg
├── 1000.1.64.90.229.255.8.jpg
├── ....过长省略......</code></pre><h3>2.1 数据加载</h3><p>在项目里我写了一个自定义的<code>QingDataset</code>类来加载和处理训练和测试数据。首先,在初始化方法中,我遍历了指定目录下的所有文件名,并将它们拼接到数据集列表中:</p><pre><code class="python">def __init__(self, root):
self.dataset = []
for filename in os.listdir(root):
self.dataset.append(os.path.join(root, filename))</code></pre><p>这样,<code>self.dataset</code>中存储了所有图像文件的路径。</p><h3>2.2 数据处理</h3><p>在<code>__getitem__</code>方法中,我通过读取图像数据,对其进行归一化处理,并转换为PyTorch张量:</p><pre><code class="python">def __getitem__(self, index):
img_path = self.dataset[index]
img = cv2.imread(img_path)
img = img / 255
img = torch.tensor(img).permute(2, 0, 1)
data_list = img_path.split('.')
label = int(data_list[1])
position = [int(i) / 300 for i in data_list[2:6]]
sort = int(data_list[6]) - 1
return np.float32(img), np.float32(label), np.float32(position), sort, img_path</code></pre><p>这里我将图像进行了归一化处理,并从文件名中提取了标签、位置和排序信息。最后,返回了处理后的图像数据以及相应的标签、位置、排序和图像路径。</p><h2>三、神经网络模型</h2><blockquote>网络模型这里的<code>nn.Conv2d(3, 16, 3)</code>,<code>ReLU</code>,<code>MaxPool2d</code>里面的参数是我随意填写的,读者不用纠结参数的含义。</blockquote><h3>3.1 模型结构</h3><p>在<code>net.py</code>中,我定义了神经网络模型<code>QingNet</code>。该模型的结构采用了<code>Sequential</code>容器,通过堆叠卷积层、激活函数以及池化层来提取图像特征:</p><pre><code class="python">self.layers = nn.Sequential(
nn.Conv2d(3, 16, 3), nn.ReLU(), nn.MaxPool2d(3),
nn.Conv2d(16, 22, 3), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(22, 32, 5), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(32, 64, 5), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(64, 82, 3), nn.ReLU(),
nn.Conv2d(82, 128, 3), nn.ReLU(),
nn.Conv2d(128, 25, 3), nn.ReLU()
)</code></pre><p>这些层的输出形成了模型的最后特征图。</p><h3>3.2 输出信息</h3><p>模型的最后几个层分别输出了标签、位置、排序和置信度的信息:</p><pre><code class="python">self.label_layer = nn.Sequential(nn.Conv2d(25, 1, 3), nn.ReLU())
self.position_layers = nn.Sequential(nn.Conv2d(25, 4, 3), nn.ReLU())
self.sort_layers = nn.Sequential(nn.Conv2d(25, 20, 3), nn.ReLU())
self.confidence_layer = nn.Sequential(nn.Conv2d(25, 1, 3), nn.Sigmoid())</code></pre><p>这些输出对应了单目标检测任务中所需的各个要素。</p><h2>四、训练过程</h2><blockquote>训练的过程其实就是将数据集丢给网络模型,网络模型会返回目标的位置信息,我会那这个结果与数据集的正确结果进行损失计算,并告诉网络模型损失值。</blockquote><p>随着不断训练网络模型,网络模型会越来越靠近真实值,每训练一轮我都会把权重文件保存到磁盘中,这样电脑即使重启还可以接着上次的成果接着训练。</p><h3>4.1 损失计算和反向传播</h3><p>在<code>train.py</code>中,我对每个训练批次进行了循环迭代。对于每个批次,我计算了标签、位置和排序的损失,然后按照一定的权重组合得到了最终的训练损失:</p><pre><code class="python">label_loss = self.label_loss(out_label, label)
position_loss = self.position_loss(out_position, position)
sort_loss = self.sort_loss(out_sort, sort)
train_loss = 0.2 * label_loss + position_loss * 0.6 + 0.2 * sort_loss</code></pre><p>这里,我采用了<code>BCEWithLogitsLoss</code>、<code>MSELoss</code>和<code>CrossEntropyLoss</code>作为标签、位置和排序的损失函数。</p><h3>4.2 模型保存</h3><p>在每一轮训练结束后,我保存了模型的权重,方便后续的预测和部署:</p><pre><code class="python">torch.save(self.net.state_dict(), f'param/{date_time}-{epoch}.pt')</code></pre><p>这样,我们就可以在需要时加载训练好的模型进行预测。</p><h2>五、预测和可视化</h2><p>当我训练的效果达到满意后,我就可以把训练好的权重文件用于实际生产中了。</p><h3>5.1 模型加载和预测</h3><p>在<code>predict.py</code>中,我首先加载了训练好的模型权重,并将模型设置为评估模式:</p><pre><code class="python">predictor = Predictor('param/' + max(os.listdir('param/')))
predictor.net.eval()</code></pre><p>然后,通过<code>predict</code>方法对单张图像进行预测,获取标签、位置、排序和置信度的输出。</p><h3>5.2 可视化工具</h3><p>最后,通过<code>tools.py</code>中的<code>view_image</code>方法,我将原始图像与模型预测的标签、位置、排序进行可视化:</p><pre><code class="python">tools.view_image(img_path, label, position, sort, out_label, out_position, out_sort)</code></pre><p>这一步骤有助于直观地了解模型对于输入图像的处理效果,为进一步调优提供了参考。</p><h2>六、关于我</h2><p>作者:汤青松</p><p>微信:songboy8888</p><p>日期: 2024-02-02</p>
2023年总结,我被裁员了,最终去了外包
https://segmentfault.com/a/1190000044614872
2024-02-03T10:20:29+08:00
2024-02-03T10:20:29+08:00
xiangzhihong
https://segmentfault.com/u/xiangzhihong
9
<p>本文参与了<a href="https://segmentfault.com/a/1190000044433522">SegmentFault 思否 2023 年度有奖征文</a>活动,欢迎正在阅读的你也加入。</p><h2>2023年,酸甜苦辣咸</h2><p>有人说,2023年是“混乱且令人疲惫、像个高压锅”的一年。时代的颠簸、世界的震荡、行业的起伏,让身处其中的每个人都感受到了巨变。宏观层面,2023年全球经济减速、终端消费疲软,半导体行业的下行周期一次次击碎了人们对市场空间的想象;在微观层面,个人、企业与行业的命运相互交织,每一个人都在时代的焦虑漩涡中感到不安。</p><p>尽管2023年充满了各种困难,但是机遇也不少。比如,ChatGPT点亮人工智能,在阴影中升起一盏希望的灯塔。如果要我说,2023年这一年一定是“酸甜苦辣咸”,既有年初的意气风发,也有年底的迷茫与焦虑。</p><p>2023年,对于全球科技业来说是煎熬的一年。面对宏观经济的快速变化,各家大厂纷纷实行“断舍离”,裁员风暴席卷下,所有科技从业者都深切体会到“刺骨寒意”,互联网从业者当然也不例外。在互联网下行的背景下,我被裁员了。<br><img width="612" height="343" src="/img/bVdbmwE" alt="image.png" title="image.png"><br>对,你没有看错,我被裁员了。这是我工作10余年来,遇到的第二次裁员(第一次大概是2018年公司发展不好,业务部门整体裁员)。对于这次裁员,其实我没有太多的意外,因为在这之前,已经有好几个月没有实际的产出,虽然我是Leader,但是在大背景之下也免不了被淘汰的命运。</p><p>虽然,我被裁员了,可能会带有一丝的悲伤和无赖,但是在我看来离开未必不是一种解脱,长期在一个压抑的团队工作,可能对你的身心也是一种折磨。并且,2023年未必只有裁员,还有很多的美好,比如,年前加薪了,新书出版了,副业收入...。</p><p>由于年底工作机会太少,在gap了1个月之后,我选择降薪去了一个车联网公司,干的是项目外包,虽然工资有所降低,干的事情也有所变化。但是面对2023年大环境,我也只能面对现实。或许此刻的想法是,先就业再谋求出路:有可能会继续去一线漂,也有可能还是留在老家。至于后面怎么打算,还得跟家里具体商量后再做决定。</p><p><img width="360" height="250" src="/img/bVdbmwF" alt="image.png" title="image.png"></p><p>总的来说,2023年没有太多的惊喜,也没有太多的失落。因为工作10多年来,经历过太多的事情,所以一般的事情也不会太影响到我的生活和情绪。正如那句话说的“人生无常,大肠包小肠”,且平淡的看之。</p><h2>职业生涯回顾</h2><p>接下来,聊一聊我的职业生涯吧。我说2012年毕业的,2012年到2021年都在上海,属于典型的沪漂一族。在上海的职业生涯,我呆过四家大公司,小公司也有几个,不过都是那种干1,2个月跑路的,总的来说吧,在上海的职业生涯都还是比较顺利的。 </p><h3>2012.7 ~ 2013.4 青春年少</h3><p>我大学是在西安读的,由于西安的大学太多,让我们那个本就不出色的二本学院显得那么的弱小。记得还没毕业的时候,我去找实习单位,看到人事的桌子上都是什么电子科大、西北大学的简历,刚开始我还有点心虚,想想在没有任何学历优势的情况下如何与他们竞争。</p><p>是的,在那个学生年代,大多数的学校教会不了你什么,并且有些学校的教学方式跟高中毫无二致,因此,学校的好坏就成为公司选人的重要标准。在找了很久之后,结果去了一个外包公司,美其名曰【实习】,其实不过是自己拿钱培训而已。</p><p>不过,也应该庆幸,正是这次培训,才让我真正走上了软件开发的道路(因为学校学的真的不能让你胜任工作)。于是,在那段为期5-6个月的【实习】中,我学会了基本的Java开发,学会了使用struts、hibernate、Spring等框架技术,学会了oracle数据库开发。此时,前端还是基本的Jquery、Html等开发方式,还没有现在流行的React、Vue前端框架,那时候的软件系统体验上都很差。</p><p>不过,相比于系统软件开发,此时的移动应用开发如一颗升起的新星。我记得最清楚的是,那时候的iOS开发同学,只要会基本的界面开发,都能上万。我心动了,于是我【叛变】了,转到了移动班学习起了Android开发。相比于系统开发,移动开发的天花板比较低,并且那种可见即所得的开发方式,也让我找到了久违的成就感。</p><p>于是,在学习了一个多月之后,我出师了。由于西安本地就业环境差,为了保就业率,于是培训班的同学都跟着去了北京、上海、深圳这些一线城市,而我选择了上海。<br><img width="293" height="230" src="/img/bVdbmwG" alt="image.png" title="image.png"></p><p>作为中国最大的城市,也是中国GDP最高的城市,上海的繁华让之前从未到过上海的我感到兴奋,也第一次有了自己的小目标。可以说,那时候的上海“遍地黄金”,大家都在为自己的理想和目标奋斗着。</p><p><img width="612" height="343" src="/img/bVdbmwI" alt="image.png" title="image.png"><br>不过,刚到上海的几个月,我的工作并不是很顺利。由于当时的互联网行业还处于爆发的前夜,所以很多的互联网公司都还处于Web开发的时代,加上上海人才众多,如果没有任何的工作经验,想要得到公司的青睐,是非常考验你的实力的。</p><p>所以,在找了大半个月之后,我放弃了软件开发的工作,去了一个咨询工作干起了IT。虽然那段时间看起来过的很无聊,但是周围的同事都很友好,并且我学到了一些IT维护方面的技巧,比如组建交换器,各种剪辑,以及其他的Office办公技巧。并且,我的自由时间比较多,我可以学习一些自己感兴趣的东西。</p><p>有时候,人生就是需要一个运气和机遇。在过完年后,我觉得我已经准备得差不多了,开始频繁的出去面试了,加上那时候移动互联网兴起,很多公司对程序员的要求也降低了,于是在那年年底,我入职了一家OA公司,不过由于年底考核,自己实力并不是很好,所以我被淘汰了。不过,这次淘汰我却看的很平淡,因为在面试两周之后,我又拿到Offer,也是我后面将要讲到的某个旅游公司。</p><h3>2013.4 ~ 2021.5 辛苦搬砖</h3><p>2013年或许是我的幸运之年,由于移动互联网的发展,此时的Android、iOS开发可谓风光无限。我很明白我很菜,可以即时这样,我也拿到了好几个Offer,在这几个Offer当中,我最青睐的就是xx旅游公司。作为国内头部的OTA公司,我在这里学到了很多Android开发和前端开发的知识,也学到了很多职场规则。应该说,在这两年不到的时间中,我过得还是比较快乐的。<br><img width="470" height="274" src="/img/bVdbmwL" alt="image.png" title="image.png"><br>从xx旅游公司出来之后,我先后在电商公司和地产公司都待过,不过,让我成长比较快的还是xx地产公司。此时的房地产可以说是如日中天,每天都能接触到很多的新东西,不过我们干的项目却是电商项目,准确的说是社区电商。或许是走的太早,这个项目干了差不多两年就因为亏损太多,老板不愿意再投资了,后面只能放弃。此时, 我面临两个选择,一是换到其他的项目组,一是拿赔偿离职,我选择了后者。</p><p>从XX公司出来后,我先后去了某保险公司和某二次元公司,在某二次元公司干的其实还是比较愉快的,因为周围都是些年轻的小伙伴,干的事情也没有太多的挑战,并且在空闲之余,还可以干一些自己喜欢的事情,比如参加各种活动,分享自己的一些故事。</p><h3>2021.4 ~ 至今 衣锦还乡</h3><p>2021年,在很多事情的促使下,我决定离开上海。促使我回家的原因有很多,最重要的一点是感情方面的挫折以及上海的留不下。曾经,我也和很多人一样去相亲,可是到后来都没有成功,一个最重要的原因就是没有在上海买房。<br><img width="421" height="316" src="/img/bVdbmwO" alt="image.png" title="image.png"></p><p>现在看来,我回来的时候运气还是比较好的,薪资虽然有降低,但是也还能接受。在回来之后,我有更多机会陪伴家人,也遇到了陪自己走下半生的【小伙伴】,当然心态上也更加放松,不再那么追求工作的成就和薪资的高低,当然也就少了职场的尔虞我诈。<br><img width="261" height="348" src="/img/bVdbmwP" alt="image.png" title="image.png"></p><p>一句话,回家之后,很多事情都看开了。享受生活,活得快乐成为了我的人生目标。</p><h2>个人的一些成绩</h2><p>时光荏苒,岁月如梭,转眼间就已经毕业了10余年,回首自己工作的这些年,虽没有所谓的大成就,但也是取得了一些成绩的,主要的成绩当然是技术上的成长。这些年我先后从事过移动开发、前端开发和车机开发的工作,可以说工作经验和履历也是相当的丰富。个人喜欢分享,在各大技术平台也是收获了一批粉丝,并且先后出版了6本不同系列的书籍,如《React Native移动开发实战》1,2,3版、《Kotlin入门与实战》、《Weex跨平台开发与实战》、《Flutter跨平台开发入门与实战》1,2版和《Android应用开发详解》。<br><img width="612" height="122" src="/img/bVdbmwQ" alt="image.png" title="image.png"></p><p>以下是我的博客的连接:</p><p>CSDN:<a href="https://link.segmentfault.com/?enc=uWR7yQh8X13ALiwL%2FTAeww%3D%3D.Tg%2F8DIaTTGLUJLD5UzhZ7Vs27%2FnH4s7X0R6gHsxZe60Q6ftwmqGRMQ8B5zJbdC5uKDWTecPcHPaU5m865VCGhQ%3D%3D" rel="nofollow">https://blog.csdn.net/xiangzhihong8?spm=1000.2115.3001.5343</a></p><p>掘金:<a href="https://link.segmentfault.com/?enc=UF%2Flj%2FdBYWQyRkRpaDmCXA%3D%3D.0Bp0iufeEzj6OKvekIJLn1dG2hOxI5efkmSH479x6EzRkPA4DOPgdPaAhD5BRPAU" rel="nofollow">https://juejin.cn/user/3562073407103511</a></p><p>思否:<a href="https://segmentfault.com/u/xiangzhihong/articles">https://segmentfault.com/u/xiangzhihong/articles</a> </p><p>转眼间,2023 年已经过去了二十来天天,我内心也是咋纠结了好久才最终写下了这些文字。由于时间关系,我对我个人的经历不没有写的很详细,如果想要了解我的故事,也可以后台私信。</p><p>2024 年,于我来说是不平凡的一年,除了是我的本命年之外,我也即将站上所谓职业的终点(ps:35岁)。</p><p>35岁,意味着互联网已经给你关上了一扇门,同时还意味着很多工作不在适合你。面对35岁,面对未来的人生,我也开始感到迷茫甚至是不知所措。然而,35岁也可能是我人生另外的一个起点,虽然有些快节奏的工作不再适合我,但是我可以去一些传统的行业,正所谓“人生处处都是机遇,只不过要看你能不能把握和利用住这些机遇”罢了。</p><p>夜已深,就写到这吧,2024龙年大家一起加电⚡️(ps,我的故事还有很多,欢迎后台私信交流)。</p>
Beta攻略首发|HarmonyOS NEXT 1000问:开发者必看"清单"就在这里!
https://segmentfault.com/a/1190000044613384
2024-02-02T16:42:27+08:00
2024-02-02T16:42:27+08:00
HarmonyOS助手
https://segmentfault.com/u/anjingdezhuantou_u8rq7
5
<p>随着HarmonyOS NEXT开启开发者预览版Beta招募,开发者可以体验到全面升级的 OS开放新能力、鸿蒙特征新场景、开发工具等。这是一项需要广大开发者一起参与的伟大事业,华为期待携手开发者一路同行,共赴鸿蒙生态的星辰大海。</p><h2>如何借助HarmonyOS NEXT打造更具竞争力应用</h2><p>HarmonyOS技术专家历时数月,整理涵盖了ArkUI、Ability、ArkTS、ArkWeb、ArkData等80+kit的内容,共计1000+ HarmonyOS开发中的常见问题,旨在通过"HarmonyOS NEXT 1000问"让开发者更全面地了解HarmonyOS NEXT开发环境,快速且高效地借助HarmonyOS NEXT打造更具竞争力的应用。</p><p><img width="723" height="1246" src="/img/bVdbl7K" alt="" title=""></p><p>HarmonyOS NEXT开发者预览版不仅是一次体验,更是一场HarmonyOS的发现之旅,邀请你一起探索全场景下的崭新世界,成为第一批HarmonyOS NEXT尝鲜选手!</p><h2>HarmonyOS NEXT 1000问</h2><blockquote>下方问题均可在 <a href="https://segmentfault.com/site/harmonyos">HarmonyOS 开发者专区</a> 内搜索呈现,更多技术内容持续更新中,敬请关注~</blockquote><h3>一、一键Get TOP高频开发FAQ</h3><h4>1)Ability</h4><p><a href="https://segmentfault.com/q/1010000044561573">Q1:FA和Stage模型中,应用是否可以创建并指定UIAbility运行在哪个进程</a></p><p><a href="https://segmentfault.com/q/1010000044568263">Q2:如何获取设备横竖屏的状态变化通知</a></p><p><a href="https://segmentfault.com/q/1010000044570679">Q3:如何跳转至设置-权限管理页-指定应用</a></p><p><a href="https://segmentfault.com/q/1010000044570667">Q4:如何通过路由跳转到一个只有页面没有UIAbility的模块</a></p><p>Q5:应用的进程启用过程是怎样的</p><p><a href="https://segmentfault.com/q/1010000044570733">Q6:如何在手机桌面创建指向应用某个页面的快捷方式</a></p><p><a href="https://segmentfault.com/q/1010000044570859">Q7:如何实现设备内跨应用的UIAbility跳转</a></p><p><a href="https://segmentfault.com/q/1010000044571110">Q8:应用免安装的限制、字段解释以及如何自测</a></p><p><a href="https://segmentfault.com/q/1010000044571306">Q9:从包管理的角度,保证代码安全的措施有哪些</a></p><p><a href="https://segmentfault.com/q/1010000044562277">Q10:HSP/HAR包中如何引用外部编译的so库文件</a></p><h4>2)ArkData</h4><p><a href="https://segmentfault.com/q/1010000044571620">Q11:如何实现应用数据持久化存储</a></p><p>Q12:多个相同BundleName的hap包,使用preference数据如何共享</p><p><a href="https://segmentfault.com/q/1010000044571635">Q13:关于数据库存储的位置, 以及存储的区别</a></p><p><a href="https://segmentfault.com/q/1010000044571640">Q14:卡片开发中如何实现数据持久化</a></p><h4>3)ArkTS</h4><p>Q15:将rawfile中json格式的字符串转换成对应的object对象后,调用实例方法后程序崩溃</p><p><a href="https://segmentfault.com/q/1010000044562331">Q16:有哪些创建线程的方式</a></p><p><a href="https://segmentfault.com/q/1010000044562349">Q17:import依赖树较大如何优化</a></p><p><a href="https://segmentfault.com/q/1010000044562409">Q18:如何使用ohpm引入三四方库</a></p><p><a href="https://segmentfault.com/q/1010000044562427">Q19:如何打开键鼠穿越功能开关</a></p><h4>4)ArkUI</h4><p><a href="https://segmentfault.com/q/1010000044602402">Q20:如何实现页面加载前从接口获取数据</a></p><p><a href="https://segmentfault.com/q/1010000044562545">Q21:创建的单例换了页面后不生效问题</a></p><p><a href="https://segmentfault.com/q/1010000044571684">Q22:如何获取组件的宽高</a></p><p><a href="https://segmentfault.com/q/1010000044571877">Q23:如何去除自定义弹窗的白色背景</a></p><p><a href="https://segmentfault.com/q/1010000044571961">Q24:TextInput在聚焦时如何使光标回到起点</a></p><p><a href="https://segmentfault.com/q/1010000044562602">Q25:TextInput如何限制输入字符为某些字符</a></p><p><a href="https://segmentfault.com/q/1010000044576965">Q26:UI布局默认是多少vp为基准,以达到不同机器自适应</a></p><p><a href="https://segmentfault.com/q/1010000044577125">Q27:XComponent 怎么设置成透明</a></p><p><a href="https://segmentfault.com/q/1010000044577413">Q28:控制中心的下拉背景实时模糊是如何实现的</a></p><p><a href="https://segmentfault.com/q/1010000044577679">Q29:Image组件如何读入沙箱内的图片</a></p><p><a href="https://segmentfault.com/q/1010000044577723">Q30:ArkTS获取组件位置和大小的接口</a></p><p><a href="https://segmentfault.com/q/1010000044577795">Q31:使用router或Navigator实现页面跳转时,如何关闭页面间转场动效</a></p><p><a href="https://segmentfault.com/q/1010000044578766">Q32:触摸事件的TouchEvent调用stopPropagation时无法阻止事件分发</a></p><p><a href="https://segmentfault.com/q/1010000044578825">Q33:如何保持屏幕常亮</a></p><p><a href="https://segmentfault.com/q/1010000044578844">Q34:如何获取窗口的宽度</a></p><h4>5)ArkWeb</h4><p>Q35:H5页面如何与ArkTS交互</p><p>Q36:<a href="https://segmentfault.com/q/1010000044578877">为什么Web组件的onKeyEvent键盘事件不生效</a></p><p>Q37:<a href="https://segmentfault.com/q/1010000044578945">如何自定义拼接设置UserAgent参数</a></p><p>Q38:<a href="https://segmentfault.com/q/1010000044578951">Web组件中如何通过手势滑动返回上一个Web页面</a></p><h4>6)Core File</h4><p>Q39:<a href="https://segmentfault.com/q/1010000044580692">如何使用Zip模块解压项目目录rawfile中的文件至应用的沙箱目录中</a></p><p>Q40:<a href="https://segmentfault.com/q/1010000044580707">如何解决文件的中文乱码问题</a></p><p>Q41:<a href="https://segmentfault.com/q/1010000044580803">如何修改沙箱路径下json文件的指定内容</a></p><p>Q42:<a href="https://segmentfault.com/q/1010000044581479">沙箱路径的说明,以及如何获取沙箱路径</a></p><p>Q43:<a href="https://segmentfault.com/q/1010000044581505">如何将像素点保存到图片文件</a></p><h4>7)Data Loss Prevention</h4><p>Q44:<a href="https://segmentfault.com/q/1010000044581574">应用申请LOCATION位置信息权限为什么没有弹窗</a></p><p>Q45:<a href="https://segmentfault.com/q/1010000044581593">向用户申请授予权限但被用户拒绝后,如何处理才能避免应用二次进入时崩溃</a></p><p>Q46:<a href="https://segmentfault.com/q/1010000044581608">module.json5配置文件中extensionAbilities和requestPermissions的权限声明有何区别</a></p><p>Q47:<a href="https://segmentfault.com/q/1010000044581620">是否支持动态授权</a></p><h4>8)Form</h4><p>Q48:<a href="https://segmentfault.com/q/1010000044562582">如何设置卡片背景为透明</a></p><p>Q49:<a href="https://segmentfault.com/q/1010000044581835">Stage模型下如何开发一个服务卡片</a></p><p>Q50:<a href="https://segmentfault.com/q/1010000044581867">元服务与服务卡片的区别</a></p><h4>9)Lancet</h4><p>Q51:<a href="https://segmentfault.com/q/1010000044561783">hilog日志如何落盘存储</a></p><p>Q52:<a href="https://segmentfault.com/q/1010000044561789">hilog日志如何设置为只打印当前应用的日志</a></p><p>Q53:<a href="https://segmentfault.com/q/1010000044581925">应用打印日志是使用hilog还是console,hilog接口参数domain的设置范围是什么</a></p><p>Q54:<a href="https://segmentfault.com/q/1010000044561805">hilog格式化日志使用%d或者%s打印时,为何显示private</a></p><p>Q55:<a href="https://segmentfault.com/q/1010000044561828">如何使用HDC工具向只读路径(如system/lib64)中传输文件</a></p><p>Q56:<a href="https://segmentfault.com/q/1010000044582579">如何实现埋点采集数据</a></p><p>Q57:<a href="https://segmentfault.com/q/1010000044582596">如何查询应用堆内存的已分配内存大小和堆内存的空闲内存大小</a></p><p>Q58:<a href="https://segmentfault.com/q/1010000044582609">当应用发生故障时,如何获取系统日志</a></p><p>Q59:<a href="https://segmentfault.com/q/1010000044561810">如何解决hilog.debug日志无法打印</a></p><h4>10)Localization</h4><p>Q60:<a href="https://segmentfault.com/q/1010000044582646">怎么读取rawfile里的文件</a></p><p>Q61:<a href="https://segmentfault.com/q/1010000044582667">如何读取rawfile中的xml文件并转化为String类型</a></p><p>Q62:<a href="https://segmentfault.com/q/1010000044582678">如何通过接口获取resource目录的路径</a></p><p>Q63:<a href="https://segmentfault.com/q/1010000044582709">如何将app.media.app_icon,转换为PixelMap</a></p><p>Q64:<a href="https://segmentfault.com/q/1010000044582701">数字支持货币分隔符显示吗</a></p><p>Q65:<a href="https://segmentfault.com/q/1010000044582692">Resource类型如何转为String</a></p><h3>二、关于Beta 招募,不得不知的小tips</h3><h5>Q1: 如何报名参与HarmonyOS NEXT开发者预览版Beta招募?</h5><p>A:本次HarmonyOS NEXT开发者预览版Beta招募参与步骤如下:</p><p>1、注册与实名认证</p><p>本次招募活动仅面向开发者开放,开发者可进入活动页面进行注册,然后进行实名认证。如果您已经完成注册和实名认证,可以直接进入下一步。</p><p>2、应知测试通过</p><p>在报名前,需要先回答"应知测试"中的问题,确保充分了解本次开发者预览版升级带来的影响,再进行活动报名。</p><p>3、活动报名</p><p>您需完成"应知测试"并填写HarmonyOS NEXT开发者预览版Beta招募活动的申请信息后,方可获得本次招募活动的报名资格并进入审核阶段。审核结果将以华为开发者联盟官方邮件(或短信)进行通知,请您耐心等待。</p><p>4、获得在线升级及受控资源</p><p>审核通过后,您将获得HarmonyOS NEXT开发者预览版推送及对应的开发者套件受控资源查看权限(定向推送)。</p><h5>Q2:哪些机型设备可以参加本次HarmonyOS NEXT开发者预览版Beta招募?</h5><p>A:当前支持HUAWEI Mate 60、HUAWEI Mate 60 Pro以及HUAWEI Mate X5参与升级NEXT版本,具体型号请见"机型及基线版本清单";不同产品的版本规划有所不同,其他机型升级规划请您关注后续官方公告。</p><p>版本清单链接:</p><p><a href="https://link.segmentfault.com/?enc=Qo0qX7PNmGkJw%2FnUQW4T%2Fg%3D%3D.LhFM6P9oxGEx%2Ffj1vGnSA5dAu%2Fn%2BWVNNioQFpx8XVecGmJR6yntrnT656GM6IyEjeWwXy5z3jxv5mVufut5IUDyXb7H41g93HBz3WLjYgJY%2BpuKOYrNavao4ohD6%2BfCeBTM%2BY3ZH8kHgxVadjn7miw%3D%3D" rel="nofollow">https://developer.huawei.com/consumer/cn/forum/topic/02021403...</a></p><h5>Q3:如何验证我手机运行的是HarmonyOS NEXT开发者预览版本?</h5><p>A:请在设备上按照以下方式进行验证:进入设置 > 关于手机,HarmonyOS系统版本号中体现"HarmonyOS NEXT Developer Preview"字样。</p><h5>Q4:如何在华为设备中查看报名HarmonyOS NEXT开发者预览版Beta招募需反馈的设备基本信息?</h5><p>A:华为手机基本信息查看方式如下(以HUAWEI Mate X5为例):</p><p>设备型号:设置>关于手机>型号代码中查询,示例:ALT-AL10。</p><p>设备系统版本:设置>关于手机>(点击)HarmonyOS版本中查询,示例:4.0.0.152(SP2C00E150R6P16)。 </p><p>SN:16位字母+数字组合。如有拨号界面,可进入设备拨号界面:输入"*#06#"查询,长按复制SN填写;如无拨号界面,可进入:设置>关于手机 >序列号 查看,序列号即为SN号,需手动输入填写。请务必填写准确的SN码,填写错误会导致审核不通过。</p><h5>Q5:升级HarmonyOS NEXT开发者预览版本前,对手机有什么要求,有哪些注意事项?</h5><p>A:下载更新及解压系统包,需要占用一定的存储空间,为了保证您能正常升级新版本,请提前预留8G以上的内部存储空间,若内存不足将无法升级。</p><blockquote>请注意:本次升级为开发者预览版尝鲜升级,主要供开发者进行应用调测使用,除部分系统应用外,其他所有应用将被清除。因此在进行HarmonyOS NEXT开发者预览版升级前,请务必通过PC备份、云端备份等功能做好手机数据备份。</blockquote>
Nodejs - 9步开启JWT身份验证
https://segmentfault.com/a/1190000044611180
2024-02-02T09:46:08+08:00
2024-02-02T09:46:08+08:00
南城FE
https://segmentfault.com/u/nanchengfe
1
<blockquote>本文翻译自 <a href="https://link.segmentfault.com/?enc=e0uB%2FR5UccfsapIdFKKgzg%3D%3D.6Nzg57v3KmwJDyurOdM70mxUJaTgWH4d2IdN6HRhD3Uu%2Fufhv%2BP8MPIl35uUBdtJ" rel="nofollow">9 Steps for JWT Authentication in Node.js Application</a>,作者:Shefali, 略有删改。</blockquote><p>身份验证是Web开发的重要组成部分。JSON Web令牌(JWT)由于其简单性,安全性和可扩展性,已成为在Web应用程序中实现身份验证的流行方法。在这篇文章中,我将指导你在Node.js应用程序中使用MongoDB进行数据存储来实现JWT身份验证。 </p><p>在开始之前,我假设你已经安装了Node.js、MongoDB和VS Code,并且你知道如何创建MongoDB数据库和基本的RESTful API。</p><h3>什么是JWT认证?</h3><p>JWT身份验证依赖于JSON Web令牌来确认Web应用中用户的身份。JSON Web令牌是使用密钥对进行数字签名的编码JSON对象。</p><p>简而言之,JWT身份验证就像为网站提供一个密码。一旦你登录成功,你就得到了这个密码。</p><p><code>JSON Web Token</code>由三部分组成,由点(.)分隔:</p><ul><li>Header</li><li>Payload</li><li>Signature</li></ul><p>以下是JWT的基本结构:</p><pre><code class="sh">xxxx.yyyy.zzzz</code></pre><ul><li>Header:这部分包含有关令牌的信息,如其类型和如何保护。</li><li>Payload:这部分包含关于用户的声明,如用户名或角色。</li><li>Signature:确保令牌的完整性,并验证它没有被更改,这可以确保代码安全,不会被篡改。</li></ul><p>当你登录成功时,你会得到这个代码。每次你想访问某个数据时,你都要携带这个代码来证明是你。系统会检查代码是否有效,然后让你获取数据!</p><p>接下来让我们看看在node.js项目中进行JWT身份验证的步骤。</p><h3>步骤1:新建项目</h3><p>首先为您的项目创建一个新目录,并使用以下命令进入到该目录。</p><pre><code class="sh">mkdir nodejs-jwt-auth
cd nodejs-jwt-auth</code></pre><p>通过在终端中运行以下命令初始化项目(确保您位于新创建的项目文件夹中)。</p><pre><code class="sh">npm init -y</code></pre><p>接下来通过以下命令安装必要的依赖项:</p><pre><code class="sh">npm install express mongoose jsonwebtoken dotenv</code></pre><p>上面的命令将安装:</p><ul><li>express: 用于构建Web服务器。</li><li>mongoose:MongoDB的数据库。</li><li>jsonwebtoken:用于生成和验证JSON Web令牌(JWT)以进行身份验证。</li><li>dotenv:用于从.env文件加载环境变量。</li></ul><p>现在您的<code>package.json</code>文件应该看起来像这样:</p><p><img src="/img/remote/1460000044611182" alt="" title=""></p><h3>步骤2:连接MongoDB数据库</h3><p>要连接MongoDB数据库,请查看以下链接中的具体操作流程。</p><p><code>https://shefali.dev/restful-api/#Step_4_Creating_a_MongoDB_Database</code></p><h3>步骤3:创建 .env 文件</h3><p>为了 MongoDB 连接地址的安全,让我们在根目录下创建一个名为 <code>.env</code> 的新文件。</p><p>将以下代码添加到<code>.env</code>文件中。</p><pre><code class="sh">MONGODB_URL=<Your MongoDB Connection String>
SECRET_KEY="your_secret_key_here"</code></pre><p>将<code><Your MongoDB Connection String></code>替换为您从MongoDB Atlas获得的连接字符串(在步骤2中),并将<code>your_secret_key_here</code>替换为您想要的密钥字符串。现在你的<code>.env</code>文件应该是这样的。</p><pre><code>MONGODB_URL='mongodb+srv://shefali:********@cluster0.sscvg.mongodb.net/nodejs-jwt-auth'
SECRET_KEY="ThisIsMySecretKey"</code></pre><p>在<code>MONGODB_URL</code>最后我们加入<code>node.js-jwt-auth</code>,这是我们的数据库名称。</p><h3>步骤4:Express</h3><p>在根目录下创建一个名为<code>index.js</code>的文件,并将以下代码添加到该文件中。</p><pre><code class="js">const express = require("express");
const mongoose = require("mongoose");
require("dotenv").config(); //for using variables from .env file.
const app = express();
const port = 3000;
//middleware provided by Express to parse incoming JSON requests.
app.use(express.json());
mongoose.connect(process.env.MONGODB_URL).then(() => {
console.log("MongoDB is connected!");
});
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});</code></pre><p>现在我们可以通过以下命令运行服务器。</p><pre><code class="sh">node index.js</code></pre><p>输出应如下图所示。</p><p><img src="/img/remote/1460000044611183" alt="" title=""></p><p>通过使用命令<code>node index.js</code>,您必须在每次更改文件时重新启动服务器。为了避免这种情况,您可以使用以下命令安装<code>nodemon</code>。</p><pre><code class="sh">npm install -g nodemon</code></pre><p>现在使用下面的命令运行服务器,它会在每次更改文件时自动重新启动服务器。</p><pre><code class="sh">nodemon index.js</code></pre><h3>步骤5:创建用户数据库模型</h3><p>在根目录下创建一个名为<code>models</code>的新目录,并在其中创建一个名为<code>User.js</code>的新文件。</p><p><img src="/img/remote/1460000044611184" alt="" title=""></p><p>现在让我们为我们的项目创建一个简单的模型,将以下代码添加到<code>User.js</code>文件中。</p><pre><code class="js">const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
});
module.exports = mongoose.model("User", userSchema);</code></pre><h3>步骤6:实现身份验证路由</h3><p>在根目录中,创建一个名为<code>routes</code>的新目录,并在其中创建一个名为<code>auth.js</code>的文件。</p><p><img src="/img/remote/1460000044611185" alt="" title=""></p><p>然后将以下代码添加到该文件中:</p><pre><code class="js">const express = require("express");
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const router = express.Router();
// Signup route
router.post("/signup", async (req, res) => {
try {
const { username, password } = req.body;
const user = new User({ username, password });
await user.save();
res.status(201).json({ message: "New user registered successfully" });
} catch (error) {
res.status(500).json({ message: "Internal server error" });
}
});
// Login route
router.post("/login", async (req, res) => {
const { username, password } = req.body;
try {
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ message: "Invalid username or password" });
}
if (user.password !== password) {
return res.status(401).json({ message: 'Invalid username or password' });
}
// Generate JWT token
const token = jwt.sign(
{ id: user._id, username: user.username },
process.env.SECRET_KEY
);
res.json({ token });
} catch (error) {
res.status(500).json({ message: "Internal server error" });
}
});
module.exports = router;</code></pre><h4>分解上面的代码:</h4><p><strong>导入依赖:</strong></p><pre><code class="js">const express = require("express");
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const router = express.Router();</code></pre><p>在这里,我们导入以下依赖项:</p><ul><li>express: 用于构建Web服务器。</li><li>jsonwebtoken:用于生成和验证JSON Web令牌(JWT)以进行身份验证。</li><li>User:从第5步中创建的User模块导入的模型。</li><li>router:<code>Express</code>中的<code>Router()</code>函数用于单独定义路由,然后将其合并到主应用程序中。</li></ul><p><strong>注册路由:</strong></p><pre><code class="js">// Signup route
router.post("/signup", async (req, res) => {
try {
const { username, password } = req.body;
const user = new User({ username, password });
await user.save();
res.status(201).json({ message: "New user registered successfully" });
} catch (error) {
res.status(500).json({ message: "Internal server error" });
}
});</code></pre><ul><li>此路由监听对<code>/signup</code>的POST请求。</li><li>当接收到请求时,它从请求体中提取<code>username</code>和<code>password</code>。</li><li>然后使用提供的用户名和密码创建<code>User</code>模型的一个新实例。</li><li>调用<code>save()</code>方法将新用户保存到数据库。</li><li>如果用户成功保存,它会返回一个状态码201和一个JSON消息,表示“新用户注册成功”。</li><li>如果在此过程中发生错误,它会捕获错误并以状态代码500和错误消息“内部服务器错误”进行响应。</li></ul><p><strong>登录路由:</strong></p><pre><code class="js">// Login route
router.post("/login", async (req, res) => {
const { username, password } = req.body;
try {
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ message: "Invalid username or password" });
}
if (user.password !== password) {
return res.status(401).json({ message: 'Invalid username or password' });
}
// Generate JWT token
const token = jwt.sign(
{ id: user._id, username: user.username },
process.env.SECRET_KEY
);
res.json({ token });
} catch (error) {
res.status(500).json({ message: "Internal server error" });
}
});</code></pre><ul><li>此路由监听对<code>/login</code>的<code>POST</code>请求。</li><li>当接收到请求时,它从请求体中提取<code>username</code>和<code>password</code>。</li><li>然后在数据库中使用提供的<code>username</code>搜索用户。</li><li>如果没有找到用户,它会返回一个状态码401(未经授权)和一个JSON消息,指示用户名或密码无效。</li><li>如果找到用户,它会检查提供的<code>password</code>是否与数据库中存储的密码匹配。</li><li>如果密码不匹配,它会返回一个状态码401(未经授权)和一个JSON消息,指示用户名或密码无效。</li><li>如果密码匹配,它将使用<code>jwt.sign()</code>生成一个JWT。</li><li>生成的令牌然后作为JSON响应发送。</li><li>如果在此过程中出现错误,它会捕获错误并以状态代码500和错误消息“内部服务器错误”进行响应。</li></ul><p>最后路由被导出以在<code>index.js</code>文件中使用。</p><pre><code class="js">module.exports = router;</code></pre><h3>步骤7:使用中间件保护路由</h3><p>在根目录中,创建一个名为<code>middleware.js</code>的新文件,并将以下代码添加到该文件中。</p><pre><code class="js">const jwt = require("jsonwebtoken");
function verifyJWT(req, res, next) {
const token = req.headers["authorization"];
if (!token) {
return res.status(401).json({ message: "Access denied" });
}
jwt.verify(token, process.env.SECRET_KEY, (err, data) => {
if (err) {
return res.status(401).json({ message: "Failed to authenticate token" });
}
req.user = data;
next();
});
}
module.exports = verifyJWT;</code></pre><p>此代码是一个中间件函数,用于在应用程序中验证JSON Web令牌(JWT)。</p><p><strong>分解上面的代码:</strong></p><ul><li>在第一行中,我们导入<code>jsonwebtoken</code>库。</li><li>然后定义<code>verifyJWT</code>中间件函数,它有三个参数:<code>req</code>(请求对象)、<code>res</code>(响应对象)和<code>next</code>(下一个中间件函数)。</li><li>在中间件函数内部,它首先从请求头中提取token令牌。</li><li>如果请求头中没有令牌,它将返回401(未经授权)状态沿着JSON响应,指示“拒绝访问”。</li><li>如果存在令牌,它会尝试使用<code>jwt.verify()</code>进行验证。如果验证失败,它会捕获错误并返回一个401状态,其中包含一个JSON响应,指示“Failed to authenticate token”。</li><li>如果令牌被成功验证,它将解码的令牌数据附加到<code>req.user</code>对象。</li><li>最后导出<code>verifyJWT</code>函数,以便它可以用作应用程序其他部分的中间件。</li></ul><h3>第8步:验证JWT</h3><p>现在要验证JWT,请修改<code>index.js</code>,如下所示:</p><pre><code class="js">const express = require('express');
const authRouter = require('./routes/auth');
const mongoose = require("mongoose");
const verifyJWT = require("./middleware")
require("dotenv").config(); //for using variables from .env file.
const app = express();
const PORT = 3000;
mongoose.connect(process.env.MONGODB_URL).then(() => {
console.log("MongoDB is connected!");
});
app.use(express.json());
//Authentication route
app.use('/auth', authRouter);
//decodeDetails Route
app.get('/decodeDetails', verifyJWT, (req, res) => {
const { username } = req.user;
res.json({ username });
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});</code></pre><p>在上面的代码中,<code>/auth</code>路由是<code>authRouter</code>处理,其中包含的终端用户认证,例如登录和注册。</p><pre><code class="js">app.get('/decodeDetails', verifyJWT, (req, res) => {
const { username } = req.user;
res.json({ username });
});</code></pre><ul><li>当向<code>/decodeDetails</code>发出请求时,<code>verifyJWT</code>中间件验证附加到请求的<code>JWT</code>令牌。</li><li>如果令牌有效,则中间件从<code>req.user</code>中存储的解码令牌数据中提取<code>username</code>。</li><li>最后路由处理程序发送一个<code>JSON</code>响应,其中包含从令牌中提取的<code>username</code>。</li></ul><h3>步骤9:测试API</h3><h4>注册</h4><p>向<code>http://localhost:3000/auth/signup</code>发送一个POST请求,其中包含<code>Headers Content-Type : application/json</code>和以下<code>JSON</code>主体:</p><pre><code class="json">{
"username": "shefali",
"password": "12345678"
}</code></pre><p><img src="/img/remote/1460000044611186" alt="" title=""></p><p>在响应中,您将看到消息“新用户注册成功”。</p><h4>登录</h4><p>向<code>http://localhost:3000/auth/login</code>发送一个POST请求,其中包含<code>Header Content-Type : application/json</code>和<code>JSON</code>主体以及用户名和密码,这是您在注册路由中创建的。</p><pre><code class="json">{
"username": "shefali",
"password": "12345678"
}</code></pre><p><img src="/img/remote/1460000044611187" alt="" title=""></p><p>在响应中,您将收到一个令牌。记下这个令牌,因为在测试<code>decodeDetails</code>路由时需要它。</p><h4>decodeDetails</h4><p>向<code>http://localhost:3000/decodeDetails</code>发送一个GET请求,并带有一个带有令牌值的<code>Authorization</code>头(您在测试登录路由时得到了它)。</p><p><img src="/img/remote/1460000044611188" alt="" title=""></p><p>在响应中,您将获得用户名。恭喜你!🎉</p><p>您已经在Node.js应用程序中成功实现了<code>JWT</code>身份验证。这种方法提供了一种安全有效的方式来验证Web应用程序中的用户。</p><hr><p><strong>看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~</strong></p><p>专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)</p>
Spring Boot3,启动时间缩短 10 倍!
https://segmentfault.com/a/1190000044607165
2024-01-31T22:18:29+08:00
2024-01-31T22:18:29+08:00
江南一点雨
https://segmentfault.com/u/lenve
0
<p>前面松哥写了一篇文章和大家聊了 Spring6 中引入的新玩意 AOT(见<a href="https://link.segmentfault.com/?enc=63jPPixdujW8nJ8Q%2FgJDRw%3D%3D.Dv3bXiQJtnfg%2FQGh%2F2DouLlgNMkQFZCwciSwEqpmC2vgwOR%2Bui4LWZKxqg5UWb220SmEylBYosTugx5edU04%2BA%3D%3D" rel="nofollow">Spring Boot3 新玩法,AOT 优化!</a>)。</p><p>文章发出来之后,有小伙伴问松哥有没有做性能比较,老实说,这个给落下了,所以今天再来一篇文章,和小伙伴们梳理比较小当我们利用 Native Image 的时候,Spring Boot 启动性能从参数上来说,到底提升了多少。</p><blockquote>先告诉大家结论:启动速度提升 10 倍以上。</blockquote><h2>1. Native Image</h2><h3>1.1 GraalVM</h3><p>不知道小伙伴们有没有注意到,现在当我们新建一个 Spring Boot 工程的时候,再添加依赖的时候有一个 <code>GraalVM Native Support</code>,这个就是指提供了 GraalVM 的支持。</p><p><img src="/img/remote/1460000044607167" alt="" title=""></p><p>那么什么是 GraalVM 呢?</p><p>GraalVM 是一种高性能的通用虚拟机,它为 Java 应用提供 AOT 编译和二进制打包能力,基于 GraalVM 打出的二进制包可以实现快速启动、具有超高性能、无需预热时间、同时需要非常少的资源消耗,所以你把 GraalVM 当作 JVM 来用,是没有问题的。</p><p>在运行上,GraalVM 同时支持 JIT 和 AOT 两种模式:</p><ul><li>JIT 是即时编译(Just-In-Time Compilation)的缩写。它是一种在程序运行时将代码动态编译成机器码的技术。与传统的静态编译(Ahead-of-Time Compilation)不同,静态编译是在程序执行之前将代码编译成机器码,而 JIT 编译器在程序运行时根据需要将代码片段编译成机器码,然后再运行。所以 JIT 的启动会比较慢,因为编译需要占用运行时资源。我们平时使用 Oracle 提供的 Hotspot JVM 就属于这种。</li><li>AOT 是预先编译(Ahead-of-Time Compilation)的缩写。它是一种在程序执行之前将代码静态编译成机器码的技术。与即时编译(JIT)不同,即时编译是在程序运行时动态地将代码编译成机器码。AOT 编译器在程序构建或安装阶段将代码转换为机器码,然后在运行时直接执行机器码,而无需再进行编译过程。这种静态编译的方式可以提高程序的启动速度和执行效率,但也会增加构建和安装的时间和复杂性。AOT 编译器通常用于静态语言的编译过程,如 C、C++ 等。</li></ul><p>如果我们在 Java 应用程序中使用了 AOT 技术,那么我们的 Java 项目就会被直接编译为机器码可以脱离 JVM 运行,运行效率也会得到很大的提升。</p><p>那么什么又是 Native Image 呢?</p><h3>1.2 Native Image</h3><p>Native Image 则是 GraalVM 提供的一个非常具有特色的打包技术,这种打包方式可以将应用程序打包为一个可脱离 JVM 在本地操作系统上独立运行的二进制包,这样就省去了 JVM 加载和字节码运行期预热的时间,提升了程序的运行效率。</p><p>Native Image 具备以下特点:</p><ul><li>即时启动:由于不需要 JVM 启动和类加载过程,Native Image 可以实现快速启动和即时执行。</li><li>减少内存占用:编译成本地代码后,应用程序通常会有更低的运行时内存占用,因为它们不需要 JVM 的额外内存开销。</li><li>静态分析:在构建 Native Image 时,GraalVM 使用静态分析来确定应用程序的哪些部分是必需的,并且只包含这些部分,这有助于减小最终可执行文件的大小。</li><li>即时性能:虽然 JVM 可以通过JIT(Just-In-Time)编译在运行时优化代码,但 Native Image 提供了即时的、预先优化的性能,这对于需要快速响应的应用程序特别有用。</li><li>跨平台兼容性:Native Image 可以为不同的操作系统构建特定的可执行文件,包括 Linux、macOS 和 Windows,即在 Mac 和 Linux 上自动生成系统可以执行的二进制文件,在 Windows 上则自动生成 exe 文件。</li><li>安全性:由于 Native Image 不依赖于 JVM,因此减少了 JVM 可能存在的安全漏洞的攻击面。</li><li>与 C 语言互操作:Native Image 可以与本地 C 语言库更容易地集成,因为它们都是在同一环境中运行的本地代码。</li></ul><p>根据前面的介绍大家也能看到,GraalVM 所做的事情就是在程序运行之前,该编译的就编译好,这样当程序跑起来的时候,运行效率就会高,而这一切,就是利用 AOT 来实现的。</p><p>但是!对于一些涉及到动态访问的东西,GraalVM 似乎就有点力不从心了,原因很简单,GraalVM 在编译构建期间,会以 main 函数为入口,对我们的代码进行静态分析,静态分析的时候,一些无法触达的代码会被移除,而一些动态调用行为,例如反射、动态代理、动态属性、序列化、类延迟加载等,这些都需要程序真正跑起来才知道结果,这些就无法在编译构建期间被识别出来。</p><p>而反射、动态代理、序列化等恰恰是我们 Java 日常开发中最最重要的东西,不可能我们为了 Native Image 舍弃这些东西!因此,从 Spring6(Spring Boot3)开始支持 AOT Processing!AOT Processing 用来完成自动化的 Metadata 采集,这个采集主要就是解决<strong>反射、动态代理、动态属性、条件注解动态计算</strong>等问题,在编译构建期间自动采集相关的元数据信息并生成配置文件,然后将 Metadata 提供给 AOT 编译器使用。</p><p>道理搞明白之后,接下来通过一个案例来感受下 Native Image 的威力吧!</p><h2>2. 准备工作</h2><p>首先需要我们安装 GraalVM。</p><p>GraalVM 下载地址:</p><ul><li><a href="https://link.segmentfault.com/?enc=Hf2VI4EcpNJ6IQD514bGXA%3D%3D.vRjNcA850LpDQt3l27SsyIB%2B3S8MEj3dgmcw9wced73nDruvA%2FHrjvI76LzwdPxy" rel="nofollow">https://www.graalvm.org/downloads/</a></li></ul><p>下载下来之后就是一个压缩文件,解压,然后配置一下环境变量就可以了,这个默认大家都会,我就不多说了。</p><p>GraalVM 配置好之后,还需要安装 Native Image 工具,命令如下:</p><pre><code>gu install native-image</code></pre><p>装好之后,可以通过如下命令检查安装结果:</p><p>另一方面,Native Image 在进行打包的时候,会用到一些 C/C++ 相关的工具,所以还需要在电脑上安装 Visual Studio 2022,这个我们安装社区版就行了(<a href="https://link.segmentfault.com/?enc=s27boWzwML%2BrO%2FnJX4WkSQ%3D%3D.%2BZJAxgQSKJ0YCqr7Ia6UZU2w9gwEN3FsBx0jKBDKgp45ZuRujuWcY5SZqYyaQoayAr2%2F2vxq573sqgPkufpJPg%3D%3D" rel="nofollow">https://visualstudio.microsoft.com/zh-hans/downloads/</a>):</p><p><img src="/img/remote/1460000044607168" alt="" title=""></p><p>下载后双击安装就行了,安装的时候选择 C++ 桌面应用开发。</p><p><img src="/img/remote/1460000044607169" alt="" title=""></p><p>如此之后,准备工作就算完成了。</p><h2>3. 实践</h2><p>接下来我们创建一个 Spring Boot 工程,并且引入如下两个依赖:</p><p><img src="/img/remote/1460000044607167" alt="" title=""></p><p>然后我们开发一个接口:</p><pre><code class="java">@RestController
public class HelloController {
@Autowired
HelloService helloService;
@GetMapping("/hello")
public String hello() {
return helloService.sayHello();
}
}
@Service
public class HelloService {
public String sayHello() {
return "hello aot";
}
}</code></pre><p>这是一个很简单的接口,接下来我们分别打包成传统的 jar 和 Native Image。</p><p>传统 jar 包就不用我多说了,大家执行 mvn package 即可:</p><pre><code>mvn package</code></pre><p>打包完成之后,我们看下耗时时间:</p><p><img src="/img/remote/1460000044607170" alt="" title=""></p><p>耗时不算很久,差不多 3.7s 左右,算是比较快了,最终打成的 jar 包大小是 18.9MB。</p><p>再来看打成原生包,执行如下命令:</p><pre><code>mvn clean native:compile -Pnative</code></pre><p>这个打包时间就比较久了,需要耐心等待一会:</p><p><img src="/img/remote/1460000044607171" alt="" title=""></p><p>可以看到,总共耗时 4 分 54 秒。</p><p>Native Image 打包的时候,如果我们是在 Windows 上,会自动打包成 exe 文件,如果是 Mac/Linux,则生成对应系统的可执行文件。</p><p><img src="/img/remote/1460000044607172" alt="" title=""></p><p>这里生成的 aot_demo.exe 文件大小是 82MB。</p><p>两种不同的打包方式,所耗费的时间完全不在一个量级。</p><p>再来看启动时间。</p><p>先看 jar 包启动时间:</p><p><img src="/img/remote/1460000044607173" alt="" title=""></p><p>耗时约 1.326s。</p><p>再来看 exe 文件的启动时间:</p><p><img src="/img/remote/1460000044607174" alt="" title=""></p><p>好家伙,只有 0.079s。</p><p>1.326/0.079=16.78</p><p>启动效率提升了 16.78 倍!</p><p>我画个表格对比一下这两种打包方式:</p><table><thead><tr><th align="left"> </th><th align="left">jar</th><th align="left">Native Image</th></tr></thead><tbody><tr><td align="left">包大小</td><td align="left">18.9MB</td><td align="left">82MB</td></tr><tr><td align="left">编译时间</td><td align="left">3.7s</td><td align="left">4分54s</td></tr><tr><td align="left">启动时间</td><td align="left">1.326s</td><td align="left">0.079s</td></tr></tbody></table><p>从这张表格中我们可以看到,Native Image 在打包的时候比较费时间,但是一旦打包成功,项目运行效率是非常高的。Native Image 很好的解决了 Java 冷启动耗时长、Java 应用需要预热等问题。</p><p>最后大家可以自行查看打包成 Native Image 时候的编译结果,如下图:</p><p><img src="/img/remote/1460000044607175" alt="" title=""><br><img src="/img/remote/1460000044607176" alt="" title=""><br><img src="/img/remote/1460000044607177" alt="" title=""></p><p>看过松哥之前将的 <a href="https://link.segmentfault.com/?enc=B5jLCqVRmwejbzdNwv1KIQ%3D%3D.Bo6tfCd32d3ENczPnHSxz9j6TAFVeAoOjBMt%2FPs4qyextbx7kkKiT1rLL2dwFS%2BSej9pc6qJ%2BKcbkz8kYakotg%3D%3D" rel="nofollow">Spring 源码分析</a>的小伙伴,这块的代码应该都很好明白,这就是直接把 BeanDefinition 给解析出来了,不仅注册了当前 Bean,也把当前 Bean 所需要的依赖给注入了,将来 Spring 执行的时候就不用再去解析 BeanDefinition 了。</p><p>同时我们可以看到在 META-INF 中生成了 reflect、resource 等配置文件。这些是我们添加的 native-maven-plugin 插件所分析出来的反射以及资源等信息,也是 Spring AOT Processing 这个环节处理的结果。</p>
揭开空白网页背景色的神秘面纱
https://segmentfault.com/a/1190000044600438
2024-01-30T10:15:43+08:00
2024-01-30T10:15:43+08:00
南玖
https://segmentfault.com/u/fenanjiu
7
<h2>前言</h2><p>一个看似简单实则有坑的问题:空白网页的背景色是什么?</p><p>大家是不是都会认为是白色,但事实并非如此,有时候我们眼睛看到的也不一定是真的🧐</p><h2>页面根元素背景色</h2><p>比如下面这段代码:</p><pre><code class="html"><!-- ... -->
<style>
body {
background-color: skyblue;
}
</style>
<body>
前端南玖
</body></code></pre><p>这样我们能够看到整个页面都变成蓝色</p><p><img src="/img/remote/1460000044600440" alt="bg-1.png" title="bg-1.png"></p><p>看到这里可能有人会觉得是<code>body</code>填充了整个视图,但稍微有经验的同学知道,<code>body</code>的高度在没定义时应该是里面的内容撑起的</p><p>不信我们可以为<code>body</code>加上边框再来观察:</p><pre><code class="css">body {
background-color: skyblue;
border: 1px dashed black;
}</code></pre><p><img src="/img/remote/1460000044600441" alt="bg-2.png" title="bg-2.png"></p><p>那么问题又来了:<strong>既然<code>body</code>的高度只有内容区域那部分,那为什么整个页面的背景色都变成了蓝色?</strong></p><p>我们可以在<code>w3c</code>规则中找到<code>Backgrounds of Special Elements</code>这一节,可以看到这些内容:</p><blockquote><ol><li>画布是呈现文档的无限平面。</li><li>根元素的背景成为画布背景,其背景绘制区域扩展到覆盖整个画布。</li></ol></blockquote><p>看到这两句话是不是就能够理解为什么<code>body</code>的高度只有内容区域那部分,而整个页面的背景色都变成蓝色了。这是因为根元素的背景色绘制再了整个画布上</p><p>那这跟空白网页的背景是什么颜色也没关系呀?</p><p>别急,在<code>w3c</code>规则中还有这样一句话:</p><blockquote>根元素不会再次绘制这个背景,也就是说,根元素的背景色是透明的。</blockquote><p>因为对于浏览器来说把根元素背景与画布背景绘制成同一个颜色是没有意义的!</p><p>影响画布的根元素除了<code>body</code>,还有<code>html</code></p><p>比如我们再加上这段代码</p><pre><code class="css">html {
background-color: red;
border: 3px dashed seagreen;
}</code></pre><p>我们为<code>html</code>加上了背景色及边框,大家可以思考下此时的页面会怎样渲染呢?</p><p><img src="/img/remote/1460000044600442" alt="bg-3.png" title="bg-3.png"></p><p>这里我们可以看到html的背景色取代了body的背景色成为了画布的背景色,而html本身高度也是内容撑起的高度。</p><p>这样就能够证明空白网页的根元素背景色是透明的,而不是我们认为的白色</p><h2>画布背景色</h2><p>既然空白网页根元素的背景色是透明的,那我们看到的白色会不会是画布的颜色呢?</p><p>这里我们可以使用CSS中的<code>mix-blend-mode</code>混合颜色来验证:</p><pre><code class="html"><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.name {
color: green;
mix-blend-mode: difference
}
</style>
</head>
<body>
<div class="name">前端南玖</div>
</body>
</html></code></pre><p>这里如果画布的背景色是白色的话,那此时的文字颜色应该会变成粉色 <strong>between(绿色 + 白色)= 粉色</strong></p><p><img src="/img/remote/1460000044600443" alt="bg-4.png" title="bg-4.png"></p><p>但事实上文字颜色还是绿色,我们再来给<code>body</code>添加一个白色的背景</p><pre><code class="css">body {
background-color: white;
border: 1px dashed black;
}
.name {
color: green;
mix-blend-mode: difference
}</code></pre><p><img src="/img/remote/1460000044600444" alt="bg-5.png" title="bg-5.png"></p><p>此时可以看到文字颜色变成了粉色。</p><p>所以这里可以证明空白网页的画布背景色也是透明的。</p><h2>浏览器底色</h2><p>上面两个例子我们分别证明了空白网页的根元素背景色以及画布的背景色都是透明的。那么我们看到的“白色”到底是哪里来的?</p><p>在<code>w3c</code>中还有这样一句话:</p><blockquote>如果画布背景不是不透明的,则其下方的画布表面会显示出来。画布表面的纹理取决于 UA(但通常是不透明的白色)。</blockquote><p>所以,我们看到的所谓白色其实是浏览器的底色。</p>
千帆杯首期赛题公布!一起探索 AI 原生应用的时间魔法
https://segmentfault.com/a/1190000044609186
2024-02-01T15:06:08+08:00
2024-02-01T15:06:08+08:00
SegmentFault思否
https://segmentfault.com/u/segmentfault
0
<p>不知从什么时候起,各大主题乐园不再只是为孩子打造的童话世界,也俨然成为了成年人的理想国。无数的成年人换上霍格沃茨制服,挑选自己的魔杖,逃离“麻瓜世界”,前往环球影城;许多成年的迪士尼粉丝也蜂拥至上海迪士尼乐园的全球首个“疯狂动物城主题园区”亦或是香港迪士尼乐园的全球首个“冰雪奇缘主题园区”,不为遛娃,只为自己快乐。</p><p>迪士尼在公告中表示,每年乐园都会迎来大概 1 亿名客人,但那些对迪士尼有很高忠诚度,却尚未到访过乐园的人约有 7 亿左右。巨大的人流也意味着排队将为主题乐园的游客体验和运营效率带来新一轮挑战。</p><h2>主题乐园的效率设计</h2><p>事实上,作为最具人气的主题乐园,迪士尼有着自己的独到的效率设计。美国《大众科学》曾指出了游客在迪士尼乐园里奇怪的排队现象:即使游客已经被提前告知玩这个项目可能要排上 1 个小时的队伍,但一些热门的项目前依然有很长的队伍。</p><p>为了让大家心甘情愿排队,迪士尼会提供排队时长提示牌,帮助游客做好预期管理;还会在惊险刺激的项目下设置巨大的电子屏直播正在游玩的游客画面,提升期待值;迪士尼一些热门项目的等候区还会设计一些精彩的故事和精美的绘画,让游客从排队的状态中脱离出来,减缓排队焦虑。</p><p>明显,迪士尼乐园在解决游客排队问题时并没有着重于减少游客的排队时间,而是通过一些设计让大多数游客失去了排队的时间概念,即使围绕排队本身所提供的辅助工具也仅仅能告诉你园区地图上某个项目的预估排队时间。</p><p>但这样远远不够。</p><h2>来自未来的“时间魔法”</h2><p>随着大模型技术的飞速发展,把复杂功能交给 AI,让用户更加专注于创作和创意,AI原生应用正在为产业效率带来新的可能。</p><p>为此,百度智能云以“创意无限·生成未来”为主题,发起了千帆杯· AI原生应用开发挑战赛。第一期赛题将聚焦春节假期出游排队效率问题,鼓励开发者利用 AI 原生能力施展“时间魔法”,打造一款具有实用性的“游乐场排队规划助手”,帮助游客更好地了解乐园的排队情况,设计个性化的游玩路线,同时为管理者提供优化运营策略的决策支持。</p><p>本期挑战中,官方将为开发者们提供环球影城、上海迪士尼、香港迪士尼、广州长隆 4 个热门游乐场地图,地图中标注了各项目的排队和游玩时间,以及不同体验纬度的推荐指数。</p><p>同时,百度智能云千帆 AppBuilder 将会成为开发者们此次重构应用的“智能助手”,为开发者提供专业、便捷的 AI开发套件和资源环境,帮助开发者充分利用大模型的能力,攻克场景化题目,打造全新的 AI 原生应用。</p><p>值得一提的是,作为目前国内唯一全面开放的具备代码规划与执行能力的平台,AppBuilder 将框架和组件都做成了可扩展和可拼接的形式,以期给予 AI 应用开发者更多的选择和自由度。</p><p>开发者可任选一个游乐场,基于 Appbuilder 打造“排队规划助手”,游客仅需要输入总共可游玩的时间和期待获得的体验维度,即可获得最优的排队方案,畅玩“快乐老家”。</p><h2>赛题说明</h2><h3>1.赛题背景</h3><p>随着春节假期来临,环球影城等热门主题游乐场成为大人孩子的经典过节选项,但巨大的人流也意味着排队成为游客最大的痛点,游乐场项目多、如何在有限的游玩时间内收获最大的快乐,是本期赛题要解决的问题</p><h3>2.赛题内容:</h3><p>官方提供「环球影城」、「上海迪士尼」、「香港迪士尼」、「广州长隆」四个热门游乐场地图(地图信息见下图list),地图中标注各项目的排队+游玩时间、视觉体验指数、刺激指数,每个项目最多玩一次,不考虑项目之间的交通时间</p><h3>3.参赛任务:</h3><p>从以上 4 个游乐场中任选一个,通过 Appbuilder 的 Agent 框架搭建应用,在应用名称中明确具体的游乐场,如「环球影城排队规划助手」<br>你将获得官方提供的若干 Query 以及参考答案,你的应用需要实现最大化每个 Query 的回答准确率<br>本次赛题必须使用百度智能云千帆 AppBuilder 完成开发<br>不允许使用暴力穷举和直接通过代码计算的方式实现</p><h3>4.比赛时间:</h3><p>2024 年 2 月 1 日— 2024 年 2 月 7 日 24:00:00</p><h3>5.作品提交:</h3><ul><li>AppBuilder 应用命名:自拟,建议包含【游乐场规划助手】</li><li><p>在本贴下方盖楼评论,提交作品信息。必须按照以下信息格式提交,不要增加无关信息;</p><ul><li>信息格式要求:游乐场名字_应用ID</li><li>游乐场名字:你赛题选择的游乐场</li><li>应用ID 查找路径:我的应用—应用ID —复制粘贴<img width="723" height="188" src="/img/bVdbk1i" alt="image.png" title="image.png"></li><li>盖楼评论示例:<img width="723" height="165" src="/img/bVdbk1k" alt="image.png" title="image.png"></li></ul></li><li>若多次提交,以最后一次盖楼评论提交的时间和应用信息为准 ,提交后不能再更新APP的任何配置(官方系统会自动校验,若提交后更新配置,则该作品失效)</li></ul><h3>6.作品评分:</h3><p>通过官方测试集对最终提交的作品进行自动评测,准确率最高者获胜,相同准确率下提交时间最早者获胜;获奖结果将在赛题截止后 7 个自然日内在百度智能云千帆社区和社群公布</p><h3>7.赛前准备:</h3><ul><li>注册百度智能云账号,并完成实名认证。</li><li>选择赛题并在赛题表中填写账户 id:<a href="https://link.segmentfault.com/?enc=98rhDaBOaE9i1hvW9DInBw%3D%3D.YSvvySVmAgEnSlWlpMMISGhSNhrzK%2ByxnOvSJ%2Bouu0LZLOV%2FdnXnFuDk1RmmEYk2ZT1wTut2Cn23JpSKRAY8YQ%3D%3D" rel="nofollow">https://cloud.baidu.com/survey/appbuilders1-1.html</a> (请务必填写表单信息并提供准备的账户 id,我们会将代金券发至您提供的账户 id)</li><li>以上请参考操作指南:<a href="https://link.segmentfault.com/?enc=nbrKp6fgwH7AlvfH688heg%3D%3D.XacsVOXmEyB4m4W4mV1YyTxn2YzF5zs%2BbmL3GYhqwT4iXTwaLZcsLSoNZlo9GBXE" rel="nofollow">https://cloud.baidu.com/qianfandev/topic/268464</a></li><li>千帆代金券使用+付费模型配置,操作指南请参考:<a href="https://link.segmentfault.com/?enc=%2Fsell55ML6HCNFv9TedQSQ%3D%3D.WsLYC7SR8GDLutrCdnyyyyQaEZdXibN8w8xdroL1%2F97J1HdrrXyIB7RT4%2FpbNY%2FB%2F7SJz0qw63woCA658v8SI25iHeV1q0ExsnJmF63hOegpIXuYpK9f6Lex6z8XVFTq" rel="nofollow">百度智能云千帆杯AI原生应用挑战赛代金券+付费模型配置操作指南</a></li><li>Appbuilder 使用教程: <a href="https://link.segmentfault.com/?enc=ALn1OtXXNFKJdPeVy%2B4qzw%3D%3D.zfZAZpiGVipZjMuogw9J3wmuWL2VkGhcy%2FWtuc8THns%3D" rel="nofollow">https://dwz.cn/H1g7LKFW</a></li></ul><h2>具体题目:</h2><h3>游乐场地图:</h3><h4>环球影城</h4><p><img width="723" height="438" src="/img/bVdbk1v" alt="image.png" title="image.png"></p><p><strong>调试Query:</strong></p><ul><li><p>游玩5个小时,玩哪些项目的组合刺激指数最大?</p><ul><li>300分钟之内,刺激指数总和最大为50,组合为神偷奶爸小黄人闹翻天,萌转过山车,哈利波特禁忌之旅,鹰马飞行,霸天虎过山车,大黄蜂回旋机</li></ul></li><li><p>只有120分钟的时间,怎么玩视觉体验最大?</p><ul><li>120分钟之内,视觉指数最大为20,组合为神偷奶爸小黄人闹翻天,变形金刚:火种源争夺战</li></ul></li><li><p>我现在只有4小时20分钟的时间,请问玩哪些项目最刺激?</p><ul><li>260分钟之内,刺激指数总和最大为47,组合为哈利波特禁忌之旅,鹰马飞行,侏罗纪世界大冒险,霸天虎过山车,大黄蜂回旋机</li></ul></li></ul><h3>香港迪士尼</h3><p><img src="/img/remote/1460000044609188" alt="图片" title="图片"></p><p><strong>调试Query:</strong></p><ul><li><p>游玩5个小时,玩哪些项目的组合刺激指数最大?</p><ul><li>300分钟之内,刺激指数总和最大为49,组合为星战极速穿梭,钢铁奇侠飞行之旅,蚁侠与黄蜂女,魔雪奇幻之旅,冲天遥控车,迷离大宅</li></ul></li><li><p>只有120分钟的时间,怎么玩视觉体验最大?</p><ul><li>120分钟之内,视觉指数最大为20,组合为星战极速穿梭,冲天遥控车</li></ul></li><li><p>我现在只有4小时20分钟的时间,请问玩哪些项目最刺激?</p><ul><li>260分钟之内,刺激指数总和最大为42,组合为星战极速穿梭,钢铁奇侠飞行之旅,蚁侠与黄蜂女,魔雪奇幻之旅,冲天遥控车</li></ul></li></ul><h4>长隆欢乐世界</h4><p><img src="/img/remote/1460000044609189" alt="图片" title="图片"></p><p><strong>调试Query:</strong></p><ul><li><p>游玩5个小时,玩哪些项目的组合刺激指数最大?</p><ul><li>300分钟之内,刺激指数总和最大为100,组合为飓风飞椅,碰碰车,龙卷风暴,十环过山车,星际决战,飞马家庭过山车,梦幻旋马,急流勇进,自由落体,超级大摆锤,欢乐摩天轮,欢乐跳跳,垂直过山车</li></ul></li><li><p>只有120分钟的时间,怎么玩视觉体验最大?</p><ul><li>120分钟之内,视觉指数最大为63,组合为飓风飞椅,龙卷风暴,星际决战,飞马家庭过山车,梦幻旋马,急流勇进,自由落体,欢乐摩天轮,欢乐跳跳</li></ul></li><li><p>我现在只有4小时20分钟的时间,请问玩哪些项目最刺激?</p><ul><li>260分钟之内,刺激指数总和最大为100,组合为飓风飞椅,碰碰车,龙卷风暴,十环过山车,星际决战,飞马家庭过山车,梦幻旋马,急流勇进,自由落体,超级大摆锤,欢乐摩天轮,欢乐跳跳,垂直过山车</li></ul></li></ul><h4>上海迪士尼</h4><p><img src="/img/remote/1460000044609190" alt="图片" title="图片"></p><p><strong>调试Query:</strong></p><ul><li><p>游玩5个小时,玩哪些项目的组合刺激指数最大?</p><ul><li>300分钟之内,刺激指数总和最大为36,组合为小熊维尼历险记,抱抱龙冲天赛车,创极速光轮-雪佛兰呈献,小飞象,喷气背包飞行器</li></ul></li><li><p>只有120分钟的时间,怎么玩视觉体验最大?</p><ul><li>120分钟之内,视觉指数最大为17,组合为小熊维尼历险记,加勒比海盗-沉落宝藏之战</li></ul></li><li><p>我现在只有4小时20分钟的时间,请问玩哪些项目最刺激?</p><ul><li>260分钟之内,刺激指数总和最大为32,组合为抱抱龙冲天赛车,创极速光轮-雪佛兰呈献,小飞象,喷气背包飞行器</li></ul></li></ul><h2>获奖权益</h2><p>每期最强挑战者将获得:</p><ul><li>100,000 元人民币现金大奖</li><li>百度智能云千帆代金券/免费资源:7 天有效期,即每期赛题周期内有效</li><li>官方技术指导:技术大牛手把手教学,助力快速部署实战</li><li>高度宣传曝光:多渠道流量曝光和推广,提升品牌及认知度</li><li>品牌扶持:获得技术导师、商业投资机构对接等支持</li><li>职业直通车:获奖者可加入高阶开发者交流群,与业界知名企业和机构取得联系</li><li>行业峰会VIP:获奖者将有机会亮相百度 2024 年度重磅大会,展示作品/核心技术能力</li></ul><p>百度智能云想做的远不只是帮助成年人逃离“麻瓜世界”。在 AI 原生应用能力的支持下,游乐场排队规划助手的开发既是对技术的一次挑战,也是对服务理念的一次升级。一方面它能够为游客提供更加便捷、个性的游玩体验,让游客更加专注享受主题乐园的乐趣;另一方面它使得游乐场管理者能够实现更加智能的资源调度和排队管理,提升运营效率,推动相关文旅产业生态圈建设。更进一步而言,我们坚信 AI 原生应用将重塑主题公园生态,积极影响区域经济乃至城市经济的发展。</p><p>我们期待在本次看到更多优秀的开发者加入,打造出优秀的 AI 原生应用,为我们的生活带来更多便利和乐趣。</p><h3>【报名地址】</h3><p><a href="https://link.segmentfault.com/?enc=pTQZ0DzqnZKfwbd7KU%2BjAg%3D%3D.Z%2FwwD9%2FdBepvtrYSoOLUNX1BTro5R1qFqHK1InIF8qceasGrfncyRBQJ5LGtkUZu" rel="nofollow">https://cloud.baidu.com/survey/qianfanbei.html</a></p><h3>【大赛主页】</h3><p><a href="https://link.segmentfault.com/?enc=r0nzvS%2B509NxC%2FWGHuTxHg%3D%3D.9viE%2B8ZIPHxbD2LJgPtmm8hfkH%2FqyXulQC%2BYh8dQ1ObOBfFNXnLeUaX2WwA%2BnV7TyrFkjjoHI%2FDJorEvxHh9GPdVQaJpuUjb1Agharir9zo%3D" rel="nofollow">https://cloud.baidu.com/qianfandev/aimatch?track=b90cf9a62d6c...</a></p><p>请修改群昵称为:技术领域/行业场景 + 姓名</p><p>有任何疑问,可在群内咨询交流。群内请勿发布和讨论与本次大赛无关的内容,感谢大家的支持和配合。预祝大家荣获大奖!</p><p><img width="723" height="5643" src="/img/bVdbld0" alt="" title=""></p>
CSS 和 SVG 实现彩色图片阴影
https://segmentfault.com/a/1190000044600888
2024-01-30T11:30:09+08:00
2024-01-30T11:30:09+08:00
XboxYan
https://segmentfault.com/u/xboxyan
5
<p>在平时开发中,有时候会碰到这样的彩色阴影,效果如下</p><p><img src="/img/remote/1460000044600890" alt="image-20240127150305827" title="image-20240127150305827"></p><p>是不是非常有质感?下面分布介绍 <code>CSS</code> 和 <code>SVG</code> 两种实现方式,一起看看吧</p><h2>一、实现原理</h2><p>从设计上看,其实原理很简单,一张原图和一张模糊的图,叠加在一起就行了,示意如下</p><p><img src="/img/remote/1460000044600891" alt="image-20240127151048448" title="image-20240127151048448"></p><p>那么具体如何实现呢?接着往下看</p><h2>二、CSS 滤镜</h2><p>首先,单纯的 <code>CSS</code>并不能直接做出这种效果,毕竟无法生成一份相同的图片,因此,我们需要手动创建一个相同的图层。</p><p>假设<code>HTML</code>如下</p><pre><code class="html"><div class="wrap">
<img class="cover" src="https://bookcover.yuewen.com/qdbimg/349573/1036370336/180.webp">
</div></code></pre><p>为了节省 <code>dom</code>,我们可以通过伪元素的方式来生成这个图片,关键代码如下</p><pre><code class="css">.wrap{
position: relative;
/**/
}
.wrap::before{
content:'';
position: absolute;
background: url("https://bookcover.yuewen.com/qdbimg/349573/1036370336/180.webp");
transform: translate(10px,10px);
}</code></pre><p>稍微给点偏移,这样得到了两层图片</p><p><img src="/img/remote/1460000044600892" alt="image-20240127152242945" title="image-20240127152242945"></p><p>然后给这个伪元素设置模糊滤镜就行了</p><pre><code class="css">.wrap::before{
/**/
filter: blur(12px);
}</code></pre><p>这样就实现了文章开头效果</p><p><img src="/img/remote/1460000044600893" alt="image-20240127152434408" title="image-20240127152434408"></p><p>是不是很简单呢?</p><p>不过实际中可以采用 <code>CSS</code>的方式,将需要重复的图片抽离出来</p><pre><code class="html"><div class="wrap" style="--bg: url('https://bookcover.yuewen.com/qdbimg/349573/1036370336/180.webp')">
<img class="cover" src="https://bookcover.yuewen.com/qdbimg/349573/1036370336/180.webp">
</div></code></pre><p>然后<code>CSS</code>就可以保持统一了</p><pre><code class="css">.wrap::before{
/**/
background: var(--bg);
}</code></pre><h2>三、SVG滤镜</h2><p>有没有发现 上面这种方式需要手动去创建一个一模一样的图层有些多余呢?</p><p>确实是这样,<code>CSS</code> 目前还无法直接复制一个图层</p><blockquote>Firefox 中有个<code>element()</code>方法可以根据<code>dom</code>生成一份完全相同的图层,但是仅仅 Firefox 支持:<a href="https://link.segmentfault.com/?enc=8AA2LfLSgIuMWysSPLcj3Q%3D%3D.d7IWO2UyDY4we1WuV9EW0Cy1UEF5YMopQUbNsDx%2FeWVgZlGD7syAd9T9F4o5Pch8UhUtICjXJ9dX600c5aYsKQ%3D%3D" rel="nofollow">https://developer.mozilla.org/en-US/docs/Web/CSS/element</a></blockquote><p>那么,还有其他方式吗?</p><p>当然也是有了,那就是 <code>SVG</code>滤镜!</p><p>和前面的思路其实是一致的,先模糊图层,然后偏移一下,用<code>SVG</code>实现就是</p><pre><code class="html"><svg width="0" height="0">
<filter id="natural-shadow-filter">
<feGaussianBlur stdDeviation="12" />
<feOffset dx="10" dy="10" />
<feMerge>
<feMergeNode />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</svg></code></pre><p>似乎有些看不懂?没关系,我们一步步分析。</p><p>首先<code>filter</code>就滤镜的意思,表示整个就是定义了一个滤镜,后面可以给 <code>CSS</code> 直接使用。</p><p>接着,<code>feGaussianBlur</code>就是高斯模糊,<code>stdDeviation</code>表示模糊的范围。</p><p>然后,<code>feOffset</code>表示偏移,<code>dx</code>和<code>dy</code>分别是水平和垂直方向的位移。</p><p>最后是一个<code>feMerge</code>标签,这个表示合并,也就是将多个滤镜组合起来,里面的<code>feMergeNode</code>表示每一步滤镜的结果。这里有两个<code>feMergeNode</code>,第一个就是前面滤镜的最终结果,也就是<strong>模糊</strong>+<strong>偏移</strong>后的效果,第二个<code>feMergeNode</code>有一个<code>in</code>参数,表示输入,这里设置的是<code>SourceGraphic</code>,表示原始图像,也就是处理之前的原图。这里的叠加顺序是后来居上,也就是原图放在模糊图之上。</p><p>示意效果如下</p><p><img src="/img/remote/1460000044600894" alt="image-20240127155041021" title="image-20240127155041021"></p><p>最后,我们在<code>CSS</code>中直接通过 <code>id</code> 引入的方式实用这个滤镜就行了</p><pre><code class="css">.wrap{
filter: url("#natural-shadow-filter");
}</code></pre><p>效果如下,和 <code>CSS</code>基本一致</p><p><img src="/img/remote/1460000044600893" alt="image-20240127152434408" title="image-20240127152434408"></p><p>我们还可以多试几种其他图片,下面是 <code>CSS</code> 和 <code>SVG</code> 两种实现的效果对比</p><p><img src="/img/remote/1460000044600895" alt="image-20240127155714338" title="image-20240127155714338"></p><p>你可以查看以下链接</p><ul><li><a href="https://codepen.io/xboxyan/pen/yLwpmpX" title="CSS & SVG color shadow (codepen.io">CSS & SVG color shadow(codepen.io)</a>')</li></ul><h2>四、总结一下</h2><p>以上就是本文的全部内容了,主要介绍了 <code>CSS</code> 和 <code>SVG</code> 两种不同的实现方式,下面总结一下</p><ol><li>彩色阴影其实原理很简单,一张原图和一张模糊的图,叠加在一起就行了</li><li><code>CSS</code>无法直接创建一个完全相同的图层,需要手动去创建</li><li>手动去创建一个一模一样的图层有些多余,而<code>SVG</code>可以自动生成多份</li><li><code>SVG</code>可以将多个效果通过<code>feMerge</code>进行叠加,顺序是后来居上,<code>SourceGraphic</code>表示原始图像</li><li><code>CSS</code>可以通过<code>url(#id)</code>的方式引入<code>SVG</code>滤镜</li></ol><p>当然,<code>SVG</code>的潜力远不仅如此,在图像处理方面,<code>SVG</code>有着无可比拟的优势,CSS 滤镜可以称之为“残血版”滤镜,很多效果还是需要<code>SVG</code>出马,以后还会介绍更多实用场景。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤</p>
思否 HarmonyOS 技术问答马拉松来啦,快来领取大礼包🔥
https://segmentfault.com/a/1190000044600728
2024-01-30T11:02:26+08:00
2024-01-30T11:02:26+08:00
SegmentFault思否
https://segmentfault.com/u/segmentfault
13
<p>随着 <code>HarmonyOS</code> 的快速发展,越来越多的开发者对其展现出浓厚兴趣。为了进一步推动技术的普及与交流,思否小姐姐将在 <a href="https://segmentfault.com/site/harmonyos">HarmonyOS 开发者专区</a>展开一场技术问答马拉松活动,准备了超多的礼物,邀请所有小伙伴参加。</p><p>让我们凝聚社区的集体智慧,共同探索 <code>HarmonyOS</code> 的无限可能吧🚀</p><h2>📅活动时间</h2><p>2024 年 1 月 30 日 - 2024 年 3 月 20 日</p><h2>📖关卡说明</h2><table><thead><tr><th><strong>关卡</strong></th><th><strong>详情</strong></th></tr></thead><tbody><tr><td>Level 1:易错求解</td><td>活动期间,发布至少 <strong>30</strong> 个日常开发、学习中遇到的易错问题,请勿提问社区已存在问题</td></tr><tr><td>Level 2:答疑解惑</td><td>活动期间,完成至少 <strong>30</strong> 个符合要求的回答</td></tr><tr><td>Level 3:限时百题斩</td><td>活动期间,连续 <strong>10</strong> 天,每天提问<strong>+</strong>回答共 <strong>10</strong> 个</td></tr><tr><td>Level 4:(题)海王(者)</td><td>活动期间,完成 Level1+Level2+Level3,并在活动截止前发布问答(问题+回答)数量排名前三名</td></tr></tbody></table><p>自问自答不计入回答数量哦~</p><h2>🏆获奖规则</h2><ul><li><strong>完成 Lv.1 - Lv.3 中,任意一项挑战</strong><br>HarmonyOS 专属电子徽章一枚「HarmonyOS 先行者」</li><li><strong>完成 Lv.1 - Lv.3 中,任意二项挑战,最先完成的前 20 名</strong><br>华为定制 T 恤* HarmonyOS 专属电子徽章一枚「HarmonyOS 先行者」</li><li><strong>完成Lv.1 - Lv.3 全部挑战,最先完成的前 10 名</strong><br>华为手环 8 NFC版<br>华为定制 T 恤<br>HarmonyOS 专属电子徽章一枚「HarmonyOS 先行者」</li><li><strong>完成 Lv.4 关卡挑战的 3 名小伙伴 可获得华为开发者大礼包</strong><br>HarmonyOS 专属电子徽章一枚「HarmonyOS 先行者」<br>华为定制 T 恤<br>华为手环 8 NFC版<br>华为 FreeBuds 4E 蓝牙耳机</li></ul><p><strong>如出现数量相同的情况,则按照(发布时间 - 最先完成)计算排名。</strong></p><h2>🎁惊喜彩蛋</h2><p>奖品 up !我们将从积极参与活动的小伙伴中,随机选择 5 - 10 名,赠送惊喜随机盲盒礼物一份😄</p><h2>✏️活动规则</h2><ol><li>填写报名表参与活动:<a href="https://link.segmentfault.com/?enc=Cr4KM3PDTxDHTZ8xSG8q4w%3D%3D.iyhogF7BdrjbcHhocnJlSnntKhWk%2BJzv9e8JDcLVfvw%3D" rel="nofollow">https://jinshuju.net/f/VG7oVZ</a>(一定要报名喔❗️)</li><li>请前往 <a href="https://segmentfault.com/site/harmonyos">HarmonyOS 开发者专区</a>参与问答活动,提出问题或答疑解惑</li><li>问题 需要添加 #<a href="https://segmentfault.com/t/harmonyos">harmonyos</a> 或 +# <a href="https://segmentfault.com/t/harmonyos-next">harmonyos-next</a> 标签</li><li>问题 和 回答 都需要在文末添加小尾巴</li></ol><p>小尾巴如下:
本文参与了<a href="https://segmentfault.com/a/1190000044600728">思否 HarmonyOS 技术问答马拉松</a>,欢迎正在阅读的你也加入。
<br>使用 Markdown 编辑器 的小伙伴们可以直接复制以下内容到文末:</p><blockquote>本文参与了<a href="https://segmentfault.com/a/1190000044600728">思否 HarmonyOS 技术问答马拉松</a>,欢迎正在阅读的你也加入。</blockquote><h2>❗️问答要求</h2><ol><li>问答符合思否社区的内容标准和规范</li><li>标题简单明了,高概括问题正文内容</li><li>问题正文需使用相应模版撰写,排版得当,必要的代码缩进,文本代码分离排版</li><li>问题需同开发直接相关,且属于非讨论型问题,讨论型问题举例:vue 好还是 react 好</li><li>问题不可重复:如社区已有该问题,不可重复提问</li><li>精确添加同问题直接相关的技术领域标签</li><li>回答切勿发布答非所问,或无意义(包括但不限于“赞、顶、同问”)内容</li></ol><p><strong>适合作为回答的—我们建议以下的回答:</strong><br>√ 经过验证的有效解决办法<br>√ 自己的经验指引,对解决问题有帮助<br>√ 遵循 Markdown 语法排版,代码语义正确</p><p><strong>不该作为回答的—最好不要像这样回答:</strong><br>× 询问内容细节或回复楼层<br>× 与题目无关的内容<br>× “赞”“顶”“同问”“看手册”“解决了没”等毫无意义的内容</p><p>更多提问技巧可以查看 <a href="https://segmentfault.com/n/1330000013514712">关于【提问】和【写作】你需要知道的一些点</a>。</p><h2>❤️温馨 Tips</h2><p>完全没有接触过 HarmonyOS 也可以参赛!小姐姐为大家整理了各种学习资料,快来一边学习新技能,一边完赛赢大奖吧😄</p><h3>📺视频课程</h3><p><a href="https://link.segmentfault.com/?enc=2UWQimkFW6ff%2F5XWHFNMaA%3D%3D.JfThOdv2IY33I9l2oJZoGtyq3beGYGSc7eRHgIs1XKDaHpTKRwbKAm3eVfm6yfGvsot7retE8Oq4E1i5%2Bnka2FNDJd1jtvoh%2FfQlVpJOsmg%3D" rel="nofollow">HarmonyOS 应用开发系列课</a><br><a href="https://link.segmentfault.com/?enc=dZ2LTLnLVw3yFOmJDxMiSA%3D%3D.e1iiJXepqtQbi0sBwkXbSpUkE87Mgnurr%2FUCMUWEORNZpVZvWTQC7rDYP9hPUDFl0Y3F8rQfPXBiynRef5G26AAtgzBh9tKuX7ZEUIOo2GU%3D" rel="nofollow">HarmonyOS第一课</a><br><a href="https://link.segmentfault.com/?enc=B6hWTr6jcwcjj9vLIOd6RQ%3D%3D.WCcxHCSgZzBZlxsEG6v1lJq%2BuiWWy7YykRGBVN31tfbNL%2Fst4Hp2Jy2WH9LAFjoWz%2Bby8Bv04lkgaNF5bCOB5OSWIKDLvlRoXARYbGxYx8o%3D" rel="nofollow">HarmonyOS应用开发从入门到实践</a><br><a href="https://link.segmentfault.com/?enc=rYiffrZ13zvmSiekcPB3dQ%3D%3D.d2osj4K4NZkZPHWnXcI6m43CNO6817nV9HqKps%2BhK1mCvCg1pUaTxKM7gH1o8lIhxjhH6WF3GOLgsxyGqe8I5kR0CEPCJq2b9SnAKBOHoes%3D" rel="nofollow">边学边练:Codelabs HarmonyOS专区</a></p><p>还有超多免费教程详见:<a href="https://link.segmentfault.com/?enc=d4MXk4nYTVYuLBDVzoHDZw%3D%3D.4ivgHpIzFu5ctC426Uz85Kh41FqnZ1ZgNgB1VtX4ipD5ZDHoQrutsBJQNRaKbnpEe%2FZ1UZgfehZVK3Z6NdSBfE0kDhkvD6K%2BaQTyerdJ5Rq6T84JHWrOlq45sZ3zNp9jg1S3G%2B0Zj7W7K7nn6G1HhQ%3D%3D" rel="nofollow">HarmonyOS 开发者学堂 </a></p><h3>📖开发文档</h3><p><a href="https://link.segmentfault.com/?enc=yc6%2Fj9tnT%2FUsMHcDhjTchg%3D%3D.8YEldnzSMUkSIbLuylW2yoQb1YE6pq0cwKV4Xn5iyByRZZgP%2FRO62L0CEsEqJgNxT5e0OJvZ%2BSgcdUDxdZ3Fn2UXu5zrVsd78o9a01KSwmVE7buB7ya1Vne6RQYu9Jp7ZMpLbssVssAWgXBjU6zwvQ%3D%3D" rel="nofollow">HarmonyOS 应用开发指南</a><br><a href="https://link.segmentfault.com/?enc=GfgdtTBmmpdGYwSwJlDyrQ%3D%3D.AGjHPG5ukTYZiQGBoNsxOqDcCSOgcle1MSnGFqXVyJUyx5Qa2jrFJmYNIBS3A51ggFERpxCjXHME5IMuaqSNbEcNuo7XW15AOQHA%2FjQ8zlKn9zUKt1yIO4Q%2Bf42%2BTVKK" rel="nofollow">HarmonyOS 应用开发 API 参考</a><br><a href="https://link.segmentfault.com/?enc=aP89WGcdmkoXIKGjAGDyJw%3D%3D.8jhNDjRMV6fN0ChGgi6%2BzycLHzQRLVd7ZuT0FP5LLlgycp94pjb4HR049hLrWprKWLYR7%2BwiEJCYbUKgym%2FJLnolVM0ks3tF9KGZvzUPGkZBzKU%2FK5ijEwhrV31VYg8sk3ir6o1mg38NmLfAvvQUf63pXYvRRsZCZgvuz1aF%2BapSBQ0gcV3HJczP05altuyETuF%2FPW%2B0X14xR8AH9Hb4Htywxk423mxvJsgRt5sFv6NdEqo%2FC2LWLhaY40Q9K%2BEL" rel="nofollow">HarmonyOS 设备开发指南</a><br><a href="https://link.segmentfault.com/?enc=%2BzP7Ofkim1wI7i2IWl0PRg%3D%3D.MYY19Y5PXYztO4MbZn4wYmXI%2BNO6a8ZkNG8DtBLfT8he43V9C8R9yAWT9E6dmpU49iVEZodg%2FCoHPFb4fCBR%2FL5AZVUlyM7sbgpdGpqooxELWtooGdmapqnGZzQ4kbekRnnHKIdrmY6Fgi8jJYKglESN%2Bi%2BSPxt6h1T%2B3nElEJAVn%2FJxTbfQro7%2Fju%2BT70VKPqaqUlL3nbCtl7t7Ub5P29HLYVrWAIa4cxD4AJOWO8Y%3D" rel="nofollow">HarmonyOS 设备开发 API 参考</a></p><p>更多 HarmonyOS 相关文档见:<a href="https://link.segmentfault.com/?enc=zjDH%2BT7Xmqmz5Nf9TqVLZg%3D%3D.Vwwvi1tcOl4MuddIutYSTel3j%2B5HtxeQee4pw8S%2BOFItnt%2FqyZGt73Ezxy107dFZ" rel="nofollow">文档中心</a></p><hr><p>如有问题可以在评论区留言或添加小姐姐微信~</p><p><img width="672" height="209" src="/img/bVdbilK" alt="image.png" title="image.png"></p><blockquote>最终解释权归 SegmentFault 思否所有</blockquote>
18个JavaScript技巧:编写简洁高效的代码
https://segmentfault.com/a/1190000044600198
2024-01-30T09:27:41+08:00
2024-01-30T09:27:41+08:00
南城FE
https://segmentfault.com/u/nanchengfe
7
<blockquote>本文翻译自 <a href="https://link.segmentfault.com/?enc=Ayy0aUiAzWwhZFjqvqHXtQ%3D%3D.HpV5kzRxme1vo%2FdJbMTkzlhhJAsa2rb6IyLgN5iToW%2FjlYY5Vo6sOnnt70T6XIXk" rel="nofollow">18 JavaScript Tips : You Should Know for Clean and Efficient Code</a>,作者:Shefali, 略有删改。</blockquote><p><img src="/img/remote/1460000044600200" alt="" title=""></p><p>在这篇文章中,我将分享18个JavaScript技巧,以及一些你应该知道的示例代码,以编写简洁高效的代码。</p><p>让我们开始吧!🚀</p><h3>箭头函数</h3><p>可以使用箭头函数来简化函数声明。</p><pre><code class="js">function add(a, b) {
return a + b;
}
// Arrow function
const add = (a, b) => a + b;</code></pre><h3>Array.from()</h3><p><code>Array.from()</code>方法可用于将任何可迭代对象转换为数组。</p><pre><code class="js">const str = "Hello!";
const arr = Array.from(str);
console.log(arr); //Output: ['H', 'e', 'l', 'l', 'o', '!']</code></pre><h3>使用console.table显示数据</h3><p>如果您希望在控制台中组织数据或以表格格式显示数据,则可以使用<code>console.table()</code>。</p><pre><code>const person = {
name: 'John',
age: 25,
profession: 'Programmer'
}
console.table(person);</code></pre><p>输出效果:</p><p><img src="/img/remote/1460000044600201" alt="" title=""></p><h3>使用const和let</h3><p>对于不会被重新分配的变量使用const</p><pre><code class="js">const PI = 3.14;
let timer = 0;</code></pre><h3>使用解构提取对象属性</h3><p>通过使用解构从对象中提取属性,可以增强代码的可读性。</p><pre><code class="js">const person = {
name: 'John',
age: 25,
profession: 'Programmer'
}
//Instead of this 👇
console.log(person.name);
console.log(person.age);
//Use this👇
const {name, age} = person;
console.log(name);
console.log(age);</code></pre><h3>使用逻辑OR运算符设置默认值</h3><p>使用<code>||</code>操作符轻松设置默认值。</p><pre><code class="js">function greet(name) {
name = name || 'Person';
console.log(`Hello, ${name}!`);
}
greet(); //Output: Hello, Person!
greet("John"); //Output: Hello, John!</code></pre><h3>清空数组</h3><p>你可以使用length属性轻松清空数组。</p><pre><code class="js">let numbers = [1, 2, 3, 4];
numbers.length = 0;
console.log(numbers); //Output: []</code></pre><h3>JSON.parse()</h3><p>使用<code>JSON.parse()</code>将JSON字符串转换为JavaScript对象,这确保了无缝的数据操作。</p><pre><code class="js">const jsonStr = '{"name": "John", "age": 25}';
const person = JSON.parse(jsonStr);
console.log(person);
//Output: {name: 'John', age: 25}</code></pre><h3>Map()函数</h3><p>使用<code>map()</code>函数转换新数组中的元素,而不修改原始数组。</p><pre><code class="js">const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(numbers); //Output: [1, 2, 3, 4]
console.log(doubled); //Output: [2, 4, 6, 8]</code></pre><h3>Object.seal()</h3><p>您可以使用<code>Object.seal()</code>方法来防止在对象中添加或删除属性。</p><pre><code class="js">const person = {
name: 'John',
age: 25
};
Object.seal(person);
person.profession = "Programmer";
console.log(person); //Output: {name: 'John', age: 25}</code></pre><h3>Object.freeze()</h3><p>您可以使用<code>Object.freeze()</code>方法来阻止对对象的任何更改,包括添加,修改或删除属性。</p><pre><code class="js">const person = {
name: 'John',
age: 25
};
Object.freeze(person);
person.name = "Mark";
console.log(person); //Output: {name: 'John', age: 25}</code></pre><h3>删除数组重复项</h3><p>您可以使用<code>Set</code>从数组中删除重复的元素。</p><pre><code class="js">const arrWithDuplicates = [1, 12, 2, 13, 4, 4, 13];
const arrWithoutDuplicates = [...new Set(arrWithDuplicates)];
console.log(arrWithoutDuplicates);
//Output: [1, 12, 2, 13, 4]</code></pre><h3>使用解构交换值</h3><p>你可以使用解构轻松地交换两个变量。</p><pre><code class="js">let x = 7, y = 13;
[x, y] = [y, x];
console.log(x); //13</code></pre><h3>扩展运算符</h3><p>您可以使用扩展运算符有效地复制或合并数组。</p><pre><code class="js">const arr1 = [1, 2, 3];
const arr2 = [9, 8, 7];
const arr3 = [...arr2];
const mergedArr = [...arr1, ...arr2];
console.log(arr3); //[9, 8, 7]
console.log(mergedArr); //[1, 2, 3, 9, 8, 7]</code></pre><h3>模板字符串</h3><p>利用模板文字进行字符串插值并增强代码可读性。</p><pre><code class="js">const name = 'John';
const message = `Hello, ${name}!`;</code></pre><h3>三元运算符</h3><p>可以用三元运算符简化条件语句。</p><pre><code class="js">const age = 20;
//Instead of this👇
if(age>=18){
console.log("You can drive");
}else{
console.log("You cannot drive");
}
//Use this👇
age >= 18 ? console.log("You can drive") : console.log("You cannot drive");</code></pre><h3>使用===代替==</h3><p>通过使用严格相等(===)而不是<code>==</code>来防止类型强制转换问题。</p><pre><code class="js">const num1 = 5;
const num2 = '5';
//Instead of using ==
if (num1 == num2) {
console.log('True');
} else {
console.log('False');
}
//Use ===
if (num1 === num2) {
console.log('True');
} else {
console.log('False');
}</code></pre><h3>使用语义化变量和函数名称</h3><p>为变量和函数使用有意义的描述性名称,以增强代码的可读性和可维护性。</p><pre><code class="js">// Don't declare variable like this
const a = 18;
// use descriptive names
const numberOfTips = 18;</code></pre><p>今天的内容就到这里,希望对你有帮助。</p><hr><p><strong>看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~</strong></p><p>专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)</p>