注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

安卓知识点-应届生扫盲安卓WebView

作者 大家好,我叫Jack冯; 本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队; 目前主要负责海外游戏发行安卓相关开发。 背景 最近在接触活动相关需求,其中涉及到一个安卓的WebView; 刚毕业的我,对安卓知识积累比较少,所以在这里对...
继续阅读 »

作者


大家好,我叫Jack冯;


本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队;


目前主要负责海外游戏发行安卓相关开发。


背景


最近在接触活动相关需求,其中涉及到一个安卓的WebView;


刚毕业的我,对安卓知识积累比较少,所以在这里对Webview进行相关学习,希望自己可以在安卓方面逐步积累。


Webview介绍


1、关于MockView


( 1 ) 在targetSdkVersion 28/29的工程里面查看WebView继承关系


java.lang.Object
↳ android.view.View
↳ android.view.ViewGr0up
​ ↳ android.widget.FrameLayout
↳ android.layoutlib.bridge.MockView
↳ android.webkit.WebView

( 2 ) 使用26/27等低版本SDK,查看源码中的WebView 继承关系


java.lang.Object
↳ android.view.View
↳ android.view.ViewGr0up
↳ android.widget.AbsoluteLayout
↳ android.webkit.WebView

( 3 )对比


两种方式对比,AbsoluteLayout和FrameLayout都是重写ViewGr0up的方法,如与布局参数配置相关的 generateDefaultLayoutParams()、checkLayoutParams()等。两种方式明显不同的是多了一层MockView 。这里来看看MockView是什么:


public class MockView extends FrameLayout{
...
//创建方式
public MockView(Context context) {...}
public MockView(Context context,AttributeSet attrs) {...}
public MockView(Context context,AttributeSet attrs,int defStyleRes) {...}
//重写添加view方法
@Override
public void addView(View child){...}
@Override
public void addView(View child,int index){...}
@Override
public void addView(View child,int width,int height){...}
@Override
public void addView(View child,ViewGr0up.LayoutParams params){...}
@Override
public void addView(View child,int index,ViewGr0up.LayoutParams params){...}
public void setText(CharSequence text){...}
public void setGravity(int gravity){...}
}

MockView,译为"虚假的view"。


谷歌发布的Sdk其实只是为了提供App开发运行接口,实际运行时候替换为当前系统的Sdk。


具体说就是当谷歌在新的系统(Framework)版本上准备对WebView实现机制进行改动,同时又希望把新的sdk提前发出来,不影响用到WebView的App开发,于是谷歌提供给Android开发的sdk中让WebView继承自MockView,这个WebView只是暴露了接口,没有具体实现;这样当谷歌关于WebView新的实现做好,利用WebView,app也就做好了


2、基本使用


(1)创建


①一般方式:


WebView webView = findViewById(R.id.webview);

②建议方式:


LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);

好处:构建不用依赖本地xml文件,自定义页面参数;手动销毁避免内存泄露;


③更多方式 : 继承Webview和主要API等进行拓展


public class BaseWebView extends WebView {...}
public class BaseWebClient extends WebClient {...}
public class BaseWebChromeClient extends WebChromeClient {...}

(2)加载


① 加载某个网页


webView.loadUrl("http://www.google.com/");

②新建assets目录,将html文件放到目录下,通过路径加载本地页面


 webView.loadUrl("file:///android_asset/loadFailed.html");

③使用evaluateJavascript(String script, ValueCallback resultCallback)方法加载,(Android4.4+)


mWebView.evaluateJavascript("file:///android_asset/javascript.html",new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.e("测试", "onReceiveValue:"+value );
}
});

3、WebViewClient


当URL即将加载到当前窗口,如果没有提供WebViewClient,默认情况下WebView将使用Activity管理器为URL选择适当的处理器。


如果提供了WebViewClient,按自定义配置要求来继续加载URL。


(1)常用方法


//加载过程对url的处理(webview加载、系统浏览器加载、其他操作等)
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
}
//加载失败页面
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){
view.loadUrl("file:///android_asset/js_error.html");
}
//证书错误处理
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
}
//开始加载页面(可自定义页面加载计时等)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.e(TAG, "onPageStarted:" + url);
}
//结束加载页面
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.e(TAG, "onPageFinished: " + url);
}

(2)关于shouldOverrideUrlLoading


如果在点击链接加载过程需要更多的控制,就可以在WebViewClient()中重写shouldOverrideUrlLoading()方法。


涉及shouldOverrideUrlLoading()的情形,大概分为三种:


(1)没有设定setWebViewClient(),点击链接使用默认浏览器打开;


(2)设定setWebViewClient(new WebViewClient()),默认shouldOverrideUrlLoading()返回false,点击链接在Webview加载;


(3)设定、重写shouldOverrideUrlLoading()


返回true:可由应用代码处理该 url,WebView 中止处理(若重写方法没加上view.loadUrl(url),不加载);


返回false:由 WebView 处理加载该 url。(即使没加上view.loadUrl(url),也会在当前Webview加载)


【一般应用】


@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
if (url != null) {
if (!(url.startsWith("http") || url.startsWith("https"))) {
return true;
}
//重定向到别的页面
//view.loadUrl("file:///android_asset/javascript.html");
//区别不同链接加载
view.loadUrl(url);
}
return true;
}

(3)常见误区


【误区1】 : 需要重写 shouldOverrideUrlLoading 方法才能阻止浏览器打开页面。


解释:WebViewClient 源码中 shouldOverrideUrlLoading 方法已经返回 false,不设定setWebViewClient(),默认使用系统浏览器加载。如果重写该方法并返回true, 就可以实现在app页面中加载新链接而不去打开浏览器。


【误区2】 : 每一个url加载过程都会经过 shouldOverrideUrlLoading 方法。


Q1:加载一定会触发shouldOverrideUrlLoading?


Q2:触发时机一定在onPageStarted调用之前?


解释:关于shouldOverrideUrlLoading的触发


1)如果在点击页面链接时通过标签跳转,触发方法如下:


​ shouldOverrideUrlLoading() —> onPageStarted()—> onPageFinished()


2)如果使用loadUrl加载时,触发方法如下:


​ onPageStarted()—>onPageFinished()


3)如果使用loadUrl加载重定向地址时,触发方法如下:


​ shouldOverrideUrlLoadings—>onPageStarted —> onPageFinished


ps:多次重定向的过程,


onPage1Started


—>shouldOverrideUrlLoadings


—>onPage2Started —> xxx...


—> onPageNFinished


结论:shouldOverrideUrlLoading()方法不是每次加载都会调用,WebView的前进、后退等不会调用shouldOverrideUrlLoading方法;非loadUrl方式加载 或者 是重定向的,才会调用shouldOverrideUrlLoading方法。


【误区3 】: 重写 shouldOverrideUrlLoading 方法返回true比false的区别,多调用一次onPageStarted()和onPageFinished()。


解释:返回True:应用代码处理url;返回False,则由 WebView 处理加载 url。


ps:低版本系统(华为6.0),测试 False比True会多调用一次onPageStarted()和onPageFinished(),这点还在求证中。


4、WebChromeClient


对比WebviewClient , 添加了处理JavaScript对话框,图标,标题和进度等。


处理对象 : 影响浏览器的事件


(1)常用方法:


//alert弹出框
public boolean onJsAlert(WebView view, String url, String message,JsResult result){
return true;//true表示拦截
}

//confirm弹出框
public boolean onJsConfirm(WebView view, String url, String message,JsResult result){
return false;//false则允许弹出
}

public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result)

//打印 console 信息。return true只显示log,不显示js控制台的输入;false则都显示出来
public boolean onConsoleMessage(ConsoleMessage consoleMessage){
Log.e("测试", "consoleMessage:"+consoleMessage.message());
}

//通知程序当前页面加载进度,结合ProgressBar显示
public void onProgressChanged(WebView view, int newProgress){
if (newProgress < 100) {
String progress = newProgress + "%";
Log.e("测试", "加载进度:"+progress);
webProgress.setProgress(newProgress);
}
}

(2)拦截示例:


JsResult.comfirm() --> 确定按钮的调用方法


JsResult.cancle() --> 取消按钮


示例:拦截H5的弹框,并显示自定义弹框,点击按钮后重定向页面到别的url


@Override
public boolean onJsConfirm(final WebView view, String url, String message, final JsResult result) {
Log.e("测试", "onJsConfirm:"+url+",message:"+message+",jsResult:"+result.toString());
new AlertDialog.Builder(chromeContext)
.setTitle("拦截JsConfirm显示!")
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog,int which) {
//重定向页面
view.loadUrl("file:///android_asset/javascript.html");
result.confirm();
}
}).setCancelable(false).create().show();
return true;
}

5、WebSettings


用于页面状态设置\插件支持等配置.


(1)常用方法


WebSettings webSettings = webView.getSettings();
/**
* 设置缓存模式、支持Js调用、缩放按钮、访问文件等
*/

webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setJavaScriptEnabled(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(true);

//允许WebView使用File协议,访问本地私有目录的文件
webSettings.setAllowFileAccess(true);

//允许通过file url加载的JS页面读取本地文件
webSettings.setAllowFileAccessFromFileURLs(true);

//允许通过file url加载的JS页面可以访问其他来源内容,包括其他的文件和http,https等来源
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setLoadsImagesAutomatically(true);
webSettings.setDefaultTextEncodingName("utf-8")

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


企业微信截图_5d79a123-2e31-42cc-b03f-9312b8b99df3.png


作者:37手游移动客户端团队
来源:juejin.cn/post/7245084484756144186
收起阅读 »

马斯克接手Twitter一年后的成果-工作量化的重要性

马斯克接手Twitter的一年后,在10.27其官方团队发布了一条推文展示这一年的工程成果。 有点国内那味了,论工作量化的重要性。 这一年里,我们在工程技术上取得了许多出色的成就,除了大家在应用中看到的明显变化之外,在幕后我们还做了一系列重要的优化和改进。...
继续阅读 »

马斯克接手Twitter的一年后,在10.27其官方团队发布了一条推文展示这一年的工程成果。



有点国内那味了,论工作量化的重要性。



这一年里,我们在工程技术上取得了许多出色的成就,除了大家在应用中看到的明显变化之外,在幕后我们还做了一系列重要的优化和改进。




  • 将「为你推荐」、「关注」、「搜索」、「个人主页」、「列表」、「社区」和「探索」等功能的技术栈整合到了一个统一的产品框架中。




  • 彻底重建了「为你推荐」的服务和排名系统,代码行数从700K减少到70k,减少了90%;同时计算资源减少了50%,处理相同请求的能力提升了80%。




  • 统一了「为你推荐」和视频的个性化、排名模型,显著提高了视频推荐质量。




  • 重构了API中间层的架构,删除了超过10万行代码和数千个未使用的内部废弃接口,同时删除了一些没人用的客户端服务。




  • 将获取帖子元数据的时间减半,全局API超时错误减少90%。




  • 对外部机器人、爬虫的屏蔽,相比2022年,增长了37%。平均每天,阻止了超过100万次机器人注册,并将私信中的无用信息减少了95%。




  • 关闭了位于萨克拉门托的数据中心,重新调配了5200台机架和148000台服务器,每年为公司节约了超1亿美元,总的来说,节约了48兆瓦的电量,60000磅的网络机架。




  • 优化了对云服务厂商的使用,开始在本地进行更多的工作,这一转变使得每月的云服务成本降低60%,同时我们还将所有的媒体和大文件从云端迁出,减少了60%的云端存储空间,除此之外,还成功将云数据处理成本减少了75%。




  • 构建本地GPU超级计算集群,并设计、交付了43.2Tbps 的高性能网络架构。




  • 提升网络主干的容量和冗余性,每年节省1390万美元。




  • 开展了自动化峰值流量故障转移测试,以持续验证平台的可扩展性和可用性。




作者:云舒编程
来源:juejin.cn/post/7295397683397066762
收起阅读 »

近三个月的排错,原来的憧憬消失喽

web
作为应届生,带着无限憧憬来到这里,但是经过这三个月的经历,发现只有无限的消耗,并没有任何想要留下的感觉,每天携着自己早已不属于自己的脑袋来到早已不想来的地方... 动效逻辑实现 将元素布局设置好,调整元素的一个动态css属性让其置于可视视图以外,使用动效类库,...
继续阅读 »

作为应届生,带着无限憧憬来到这里,但是经过这三个月的经历,发现只有无限的消耗,并没有任何想要留下的感觉,每天携着自己早已不属于自己的脑袋来到早已不想来的地方...


动效逻辑实现


将元素布局设置好,调整元素的一个动态css属性让其置于可视视图以外,使用动效类库,去改变他的css属性让其还原回正确的位置。


动效类库




  • ScrollTrigger




  • ScrollMagic



    • ScrollMagic 是一个 JavaScript 库,用于在滚动事件上创建视差滚动和其他滚动效果。

    • ScrollMagic 允许您在页面滚动时触发动画,例如根据滚动位置触发动画、控制元素的可见性、触发事件等。

    • 您可以使用 ScrollMagic 来创建交互式滚动体验,例如滚动到特定部分时触发动画效果。

    • ScrollMagic 可以与 TweenMax 或其他动画库一起使用,以创建复杂的滚动动画。




  • TweenMax



    • TweenMax 是 GreenSock Animation Platform (GSAP) 库的一部分。GSAP 是一个用于创建高性能动画的 JavaScript 库,提供了丰富的动画功能。

    • TweenMax 是 GSAP 的核心库之一,它用于创建各种动画,包括基本的属性动画,缓动动画,时间轴动画等。

    • TweenMax 提供了灵活且易于使用的 API,允许您创建复杂的动画效果,如淡入淡出、缩放、旋转、移动等。

    • 您可以单独使用 TweenMax 来创建动画,或与其他库和插件一起使用,以实现更高级的效果。




  • animation.gsap.js



    • animation.gsap.js 是 ScrollMagic 的插件,它允许您在 ScrollMagic 中使用 GSAP(包括 TweenMax)来控制动画。

    • 此插件通过将 GSAP 和 ScrollMagic 集成,使您能够在滚动事件中触发和控制 TweenMax 动画。

    • 使用 animation.gsap.js,您可以创建更具交互性的滚动动画,将滚动事件与强大的 TweenMax 动画引擎结合使用,实现更丰富的效果。




综上所述,TweenMax 是 GSAP 库的一部分,用于创建各种动画。ScrollMagic 是一个独立的库,用于处理滚动事件和创建滚动效果。animation.gsap.js 是 ScrollMagic 的插件,它使 ScrollMagic 能够与 GSAP(包括 TweenMax)一起使用,以在滚动事件中创建动画效果。这些库和插件可以协同工作,以创建引人入胜的交互式网页效果。


ScrollMagic很久没有维护了。
image.png


浏览器跨页面通信


前几天有这样一个需求,当我们在当前页面点击编辑时,我们跳转到编辑页面,编辑完成后,我们需要刷新当前页面并关闭编辑页面。这就需要用到跨页面通信功能了。


image.png


image.png


下面总结一下前端中实现在一个页面上进行操作,然后刷新其他页面功能的实现方法:



前提条件是两个页面同源


在页面 A:


 // 判断是否是对比项目页面跳转过来的
if (route.query?.type === 'diff') {
localStorage.setItem('diffProjectChanged', 'true');
setTimeout(() => {
window.close();
}, 500);
} else {
router.back();
}

在页面 B:


// 进入页面将localStorage中的 diffProjectChanged 置为false
localStorage.setItem('diffProjectChanged', 'false');
// 监听编辑
onMounted(() => {
window.addEventListener('storage', (event) => {
if (event.key === 'diffProjectChanged' && event.newValue === 'true') {
location.reload();
}
});
});


在页面 A 中触发一个自定义事件,将相关数据传递给其他页面。


// 触发自定义事件
const event = new CustomEvent('dataChanged', { detail: { newData: 'someData' } });
window.dispatchEvent(event);

在页面 B 中监听该自定义事件,并在事件触发时执行刷新操作。


// 监听自定义事件
window.addEventListener('dataChanged', (event) => {
// 获取数据并执行刷新操作
const newData = event.detail.newData;
location.reload();
});


  • 使用 WebSocket


在页面 A 中通过 WebSocket 发送消息,通知其他页面。
在页面 B 中监听 WebSocket 消息,接收通知并执行刷新操作。


这种方法需要在服务器上设置 WebSocket 服务。


当前项目避免使用其他包管理工具


使用一些约束,让当前项目只能通过指定的包管理工具安装,防止项目配置乱七八糟的。


在当前根目录下


    // scripts/preinstall.js
if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.log('只能使用pnpm');
console.warn(
`\u001b[33mThis repository requires using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`,
);
process.exit(1);
}

在packages.json中scripts配置。


"preinstall": "node ./scripts/preinstall.js"

或者直接配置


"preinstall": "npx only-allow pnpm"

文本省略


这种方式需要设置具体宽高。如果是使用了 flex: 1 / 百分比数据 这种不会生效。动态的宽度是不会出现省略号的。


text-overflow: ellipsis;
width: 100%;
overflow: hidden;
white-space: nowrap;

所以我们可以使用多文本的方式代替。


word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

不同大小文字底部对齐方式


在CSS中,要让不同字体大小的内容底部对齐,你可以使用verti**cal-align属性。(设置在对齐元素上,只需要设置在一个元素上即可) 这属性用于控制内联元素(如文本)在其父元素内的垂直对齐方式。你可以将其设置为bottom来实现底部对齐。但需要注意的是,vertical-align属性通常用于内联元素,而不是块级元素。 如果是块级元素,我们可以使用flex布局的align-items: flex-end;来实现父容器内的文本底部对齐。要想实现底部对齐,父容器必须具有足够的高度来容纳最大的字体大小。


<style>
.container {
height: 100px;
border: 1px solid #ccc;
display: flex;
align-items: flex-end; /* 底部对齐的关键 */
}

.text {
font-size: 20px;
}

.small-text {
font-size: 14px;
}
</style>

<div class="container">
<p class="text">这是一些文本</p>
<p class="small-text">这是较小的文本</p>
</div>

了解的一些git操作


git提交错误分支,希望将中间的commit删除掉


先通过git rebase -i commitid切换到删除commit的前一个commitid。-i 表示要进行交互式 rebase,<commit-hash>^ 表示要删除指定提交及之后的提交。这将打开一个文本编辑器,列出了要进行 rebase 的提交。
image.png


image.png
这样他只是删除了本地的记录,但是并没有更新远程仓库。


image.png
所以我们需要强制当前记录提交


git push origin <branch> --force

image.png


如果直接执行git push他会告诉你需要拉取最新代码。如果执行了git pull前面做的工作就没用了。所以我们需要使用--force强制提交。


注意:最后git push一定要强制提交,不然按照他的提示拉取了远程代码,那么前面做的内容都没用了。


回退解决冲突之前的状态


git merge --about

查看当前分支基于那个分支创建的


git reflog show

修改分支名称


// 在当前分支
git branch -m feature/v1.4.1.0

// 不在当前分支
git branch -m old-branch new-branch

将已提交的记录提交到别的分支


ruanyifeng.com/blog/2020/0…


// 切换到需要提交的分支
// 找到需要提交的代码commit
git cherry-pick commitId

props的双向绑定便捷性


我们在使用表格编辑功能时,直接在dataSource中绑定props对应的值,当编辑单元格时,就直接更新props,很方便。由于一些其他的因素,这个模块并没有采用这个,导致以下bug出现。


这个问题是测试发现方改变一些字段时,字段为发送给后端,排查发现我在修改时,并没有通过emits将值更新到props中。所以造成bug。,导致最近需求一直变更代码bug很多,已经没有在去维护的力气了。😑
image.png


watch监听可能出现的问题


这个bug对于当时我写代码来说排查很困难。排查了很久,最后也是找我导师解决的,不得不说我导师排查bug的能力好nb。👍


最开始我是通过监听用户切换不同内容,监听diffProjectId,然后拿到formFirstValues和formSecondValues,去完成逻辑。这样看似没啥问题。但是完成逻辑的时候,拿到的formSecondValues总是上一次的值。这就很懵逼了。


出现这种情况的原因是我们监听的diffProjectId是同步的,而formFirstValues,formSecondValues这两个值是异步获取的。所以就会出现问题。最后通过下图方式实现功能。
image.png


删除后端不必要字段,造成的问题


由于后端定义的查询详情和请求传参字段不统一collectionPlanResp,collectionPlanReq,导致我回写数据不好处理,直接通过collectionPlanResp对象进行处理,在提交时在赋值。当时想着把多余字段删除collectionPlanResp,这样就会出现一个问题,我提交表单后,当后端服务抛出异常提示(比如字数限制),我们修改后,在当前页面再次提交,就会导致collectionPlanReq传递为空值。造成数据未传递给后端的bug。


分析了一下,字段的一些必填项长度限制,前端还是不要偷懒,做一下处理。


image.png


image.png


对于多字段UI处理


我们可以使用Collapse组件去处理,让UI看起来更简洁。


image.png


善于使用margin和定位来解决间距问题


调整间距时,如果margin不好调整,我们可以使用相对定位来配合调整。 这种方式是当时接了一个对比字段差异的需求,为了以后可以直接在当前对比页面编辑,所以采用了两个form去实现的。设置一个form的label,另一个不设置。这样就可以完美的在一行突出标题对比两个不同内容的字段了。为了做到响应式,就有了这样的做法。


image.png


这样看下来工作三个月基本都是在马代码,每天写不完的需求,发不完的版,上午写代码,下午改需求,真的挺无语的,下个月辞职回家种地喽。😅


往期文章



专栏文章



最近也在学习nestjs,有一起小伙伴的@我哦。


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

几条SQL把服务器干崩了

大家好,我是冰河~~ 今天跟大家分享一个发生在今天凌晨的真实案例,这篇文章也是我事后临时写出来的,处理事情的过程有点无语,又有点气愤! 事件背景 事情的背景是这样的:一个朋友今年年初新开了一家公司,自己是公司的老板,不懂啥技术,主要负责公司的战略规划和经营管理...
继续阅读 »

大家好,我是冰河~~


今天跟大家分享一个发生在今天凌晨的真实案例,这篇文章也是我事后临时写出来的,处理事情的过程有点无语,又有点气愤!


事件背景


事情的背景是这样的:一个朋友今年年初新开了一家公司,自己是公司的老板,不懂啥技术,主要负责公司的战略规划和经营管理,但是他们公司的很多事情他都会过问。手下员工30多人,涵盖技术、产品、运营和推广,从成立之初,一直在做一款社交类的APP。平时,我们一直保持联系,我有时也会帮他们公司处理下技术问题。


事件经过


今天凌晨,我被电话铃声吵醒了,一看是这个朋友打来的,说是他们公司数据库服务器CPU被打满了,并且一直持续这个状态,他说拉个群,把他们后端Java同事拉进来一起沟通下,让我帮忙看看是什么问题,尽快处理下。说话语气很急,听的出来事态很严重,因为目前正在加大力度推广,周末使用人数也比较多,出现这种问题着实让人着急。


后面我加了那个朋友拉的微信群,开始了解服务器出现问题的具体情况,下面就是一些处理的经过了。


注:聊天内容已经获得授权公布。


2023-10-29-006.png


他们后端Java把运维发的监控截图发出来了,咱继续跟他沟通。


2023-10-29-007.png


为啥我说CPU占用高呢?大家看下他们运维发的图就知道了。


2023-10-29-001.png


CPU已经飙升到了400%了,数据库服务器基本已经卡死。拿到他给我发的SQL后,我跟他们老板要了一份他们的数据库表结构,在我电脑上执行了下查询计划。


2023-10-29-008.png


这不看不知道,一看吓一跳,一个C端频繁访问的接口SQL性能极差,Using temporary、Using filesort、Using join buffer、Block Nested Loop全出来了。


我把这个图发出去了,也结合他们团队的实际情况,给出了优化的目标建议:SQL中不要出现Using filesort、Block Nested Loop,尽量不要出现Using join buffer和Using temporary,把Using where尽量优化到Using Index级别。


2023-10-29-009.png


说是尽量不要出现Using join buffer和Using temporary,把Using where尽量优化到Using Index级别,就是怕他们做不到这点,优先把Using filesort、Block Nested Loop优化掉。 但是这货后面说的话实属把我震惊到了。


2023-10-29-封面.png


我看完他的回复,直接有点无语:卧槽,不超过500万rows效率很高?你这SQL 500万数据效果很高?更让我无语的是这货说MySQL一般一亿以上数据量开始优化,这特么不是完全扯淡吗?他说这话时,我大概就知道这货的水平了。。。


后面我就问他说的这些数据的依据是哪里来的。


2023-10-29-011.png


这货说是什么大数据高并发MySQL数据库压测出来的,稍微有过压测经验的应该都知道,压测一个很重要的前提就是要明确压测的环境,最起码要明确压测环境服务器的CPU核数和内存,直接来句MySQL一亿数据是大数据高并发MySQL数据库压测出来的结果,这还是MySQL官方的数据。。。。


不知道是不是因为群里有他们老板的缘故,这货后面还在狡辩。


2023-10-29-012.png


沟通到这里,我特么有种想打人的冲动,生产环境所有业务快被数据库拖死了,数据库服务器CPU被干爆了,监控到慢SQL,并且查看这些慢SQL的执行计划,性能非常低下,SQL里不管是select部分还是where部分大量使用了MySQL自带的函数,这不慢才怪啊。看这货处理问题的态度,要是我下面的人,早就让他卷铺盖走人了。


处理结果


后续我跟他们老板要了一个代码只读权限的账号,将代码拉取下来后,好家伙,到处都是这种SQL查询,要是一两处还好,把SQL修改并优化下,关联的业务逻辑调整下,再把功能测试下,接口压测下,没啥问题就可以发版上线了。


但是,如果到处都是这种SQL的话,那优化SQL就要花费一定的时间了,甚至是新发布的重大功能逻辑都要重写。最终,我跟他们老板说的是回滚版本吧,最新的功能还是先下线,把新功能的SQL、缓存、业务逻辑、接口都优化好,压测没问题后再重新上线。


2023-10-29-013.png


事后总结


无论什么时候,生产环境一旦出现致命问题,第一时间要优先恢复生产环境正常访问,随后再认真排查、定位和解决问题,毕竟生产环境一旦出现问题,每一秒流失的都是真金白银。


搭建技术团队一定要找靠谱的人,最起码团队的核心人员要靠谱,像我朋友团队的这个技术,在他的认知里,MySQL执行计划出现Using temporary、Using filesort、Using join buffer、Block Nested Loop,500W rows效率都很高,殊不知他们生产环境实际主表数据才10几条,要是真达到500W量级就别查询了,数据库直接就趴下了。还有这个MySQL一般一亿以上开始优化,这个依据我也不知道这货是从哪里看到的,并且还说了大数据高并发MySQL数据库压测出来的,这不纯属扯淡吗?


更离谱的是我事后悄悄问了他们老板,他的工作年限是多久,据说工作10多年了,是位80后。


顿时让我想到了一句话:人的认知有几个层次:不知道自己不知道,知道自己不知道,知道自己知道,不知道自己知道。


好了,今天就到这儿吧,我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7298635800631459892
收起阅读 »

这款支持安全多人协作的在线终端,真的吓到我了❗️❗️

web
这款支持安全多人协作的在线终端 ☀️ 前言 事情是这样的: 周末一个同事的项目报错了,但是无法精准定位到问题😠。 他希望我帮忙看一下他的报错并协助解决,于是扔了一个链接给我🤔。 我心想你给我链接干啥,你倒是截图报错啊😡。 打开链接后我直呼 wassu...
继续阅读 »

这款支持安全多人协作的在线终端



☀️ 前言



  • 事情是这样的:

    • 周末一个同事的项目报错了,但是无法精准定位到问题😠。

    • 他希望我帮忙看一下他的报错并协助解决,于是扔了一个链接给我🤔。

    • 我心想你给我链接干啥,你倒是截图报错啊😡。



  • 打开链接后我直呼 wassup🔥,我居然可以在一个网页中操作他的终端,并且还是实时协同的!有鼠标动来动去那种!

  • 询问得知原来是用的 sshx ,那么我们本文就来了解一下这个神奇的产品。


🔥 sshx



  • sshx 这是一款基于网络的安全的团队协作终端,它允许您在多人无限画布上通过链接与任何人共享您的终端。

  • 只需要共享的人员执行一下“sshx”再将链接分享给你同事,则它能马上加入到你的终端进行操作。

  • 它具有实时协作远程光标聊天功能。它还具有快速且端到端加密的特点,并具有用 Rust 编写的轻量级服务器。

  • 实时协同代表着什么,这将使远程团队调试终端问题变得更加容易。


🤔 怎么使用


安装命令行界面



  • 通过在终端中运行此命令curl -sSf https://sshx.io/get | sh来获取 sshx CLI。它很小,只需几秒钟即可下载(3 MB)


分享您的终端



  • 直接在你需要分享的终端内执行 sshx,此时这个终端不要关闭,他会生成一个分享链接。




  • 将这个终端用浏览器打开即可,进入到这个网址,会让你输入一个名称方便团队协时展现光标的用户。





  • 在上方的操作栏新建一个虚拟终端即可操作真正的终端了。




  • 为了方便演示我这里打开两个浏览器来模拟别人协同操作我的终端,来我们跑个苹果来看看。




  • 实时对话也是很流畅。




  • 我们可以看到,在页面会出现另一个用户的移动光标,并且可以与他对话,他的延迟是非常低的,这真的可以帮助我们实时协作。




  • (协不协作我不知道,但是可以看到光标是真的帅啊!)




❓ 用来干啥



  • 那么这么一款产品,有的同学就会问了:他的作用是什么呢?看起来很鸡肋啊?

  • 有了这么一款产品,我们可以:

    • 在帮助客户部署相关公司产品的时候不需要远程操控别人的电脑,只需要客户安装这款 cli 并且联网,我们既可以远程帮忙操作。

    • 更好的公司运维,在同事操作的时候,可以随时介入进行操作。

    • 很多群友在前端群中问问题时习惯抛出一个截图,但是又没有说明白上下文,这时候就可以将你终端分享给大佬们定位问题。

    • (手摸手教女同学命令行操作🐶)



  • 那么肯定又会有同学问了:那我不是可以随便删除别人的文件?我直接rm -f * 敢问阁下如何应对?

  • 是的,看了下确实可以执行这些操作,所以还是尽量分享给你信得过的人,我觉得其实作者可以出一个只读模式only-read,这样你就可以让别人在你的终端上阅读和滚动,减少一些权限。


👋 写在最后



作者:快跑啊小卢_
来源:juejin.cn/post/7298642242117238834
收起阅读 »

Node.js如何处理多个请求?

web
前言 在计算机科学领域,关于并发和并行的概念经常被提及。然而,这两个术语常常被混为一谈,导致很多人对它们的理解存在着很多混淆。本文小编将通过对并发和并行的深入解析,帮助读者更好地理解它们之间的不同特点和应用场景。同时,文章还将介绍Node.js如何高效地处理多...
继续阅读 »

前言


在计算机科学领域,关于并发和并行的概念经常被提及。然而,这两个术语常常被混为一谈,导致很多人对它们的理解存在着很多混淆。本文小编将通过对并发和并行的深入解析,帮助读者更好地理解它们之间的不同特点和应用场景。同时,文章还将介绍Node.js如何高效地处理多个请求的技巧和方法。


什么是并发


并发是指两个或多个任务可以在重叠的时间段内开始、运行和完成。这并不一定意味着它们将同时运行,但它们可以交错执行,以便在任何给定的时间,总有一个任务在运行。


下面小编以一个简单的例子给读者详细的解释并发的特点:


假设在一个餐厅里面,有一个服务员从1号桌的客人那里接受了一份点单,然后这个服务员在厨房一直等待1号桌客人的饭做好,做好之后将饭端到1号桌。


这个服务员完成第一桌客人的点单后,再前往下一桌的2号客人处,接受订单,并前往厨房等待准备完成,等饭做好后再将点餐的餐点交给客人。


看到这里,各位读者可能会觉得这个服务员的做法一点都不高效,他完全可以在等第一单饭的时候去第二桌点单,按照这位服务员现在的做法,他在每一单的饭做好之前的这个时间段内什么事情都干不了,这样就浪费了大量的时间。


我们现在修改一下这位服务员的做法,修改后如下:


服务员将前往1号桌接受订单并将其交给厨房,然后返回2号桌接受订单并将其同样交给厨房。在这种情况下,服务员不会等待订单准备完成,而是会继续前往下一个桌子接受订单,直到食物准备好。当食物准备好后,服务员会为所有桌子上的客人上菜。像上述的这种情况,没有增加线程(服务员)的数量,但通过缩短空闲时间来加快处理过程。同时处理多个任务,这个就是并发。


例如:你正在做饭的同时,接到一通电话,于是你接听了电话,当听到炉子发出警报时,你回去关掉炉子,然后再继续接电话。


这个例子很好地展示了并发的概念。做饭的过程中,能够同时处理来自电话和炉子的不同事件。你在不中断一个任务的情况下,暂时切换到另一个任务,然后再回到原来的任务。这种并发的方式能够提高效率并更好地应对多个任务的情况。(同时做两件事,但是一次只做一件事)


什么是并行


并行是指两个或多个任务可以真正同时运行。为了实现这一点,这些任务必须能够在独立的CPU或核心上运行。同样的,小编依然以做饭的例子给大家解释一下什么是并行:


例如:你正在做饭的同时,接到一通电话,你的家人接听了电话,你继续做饭,你和你的家人谁也不会干扰谁,两个不同的事情发生在两个人身上,这个就是并行。


什么是单线程进程?


单线程进程是按照单一顺序执行编程指令的过程。话虽如此,如果一个应用程序具有以下一组指令:


指令A


指令B


指令C


如果这组指令在单线程进程中执行,执行过程将如下所示:


多线程进程是什么?


多线程进程是在多个序列中执行编程指令。因此,除非多个指令被分组在不同的序列中,否则指令不需要等待执行。


为什么Node.js是单线程的?


Node.js是一个单线程的平台。这意味着它一次只能处理一个请求。


例如:服务员从1号桌子上接订单并将其传给厨房,然后去2号桌子接订单。当从2号桌子接订单时,1号桌子的食物已经准备好了,但是服务员不能立即过去将食物送到1号桌子,服务员必须先完成1号桌子的订单,然后将其交给厨房,然后再将准备好的餐点送到1号桌子。


Node.js Web服务器维护一个有限的线程池,为客户端请求提供服务。多个客户端向Node.js服务器发出多个请求。Node.js接收这些请求并将它们放入事件队列中。Node.js服务器有一个内部组件,称为事件循环(Event Loop),它是一个无限循环,接收并处理请求。这个事件循环是单线程的,也就是说,事件循环是事件队列的监听器。


Node.js如何处理多个请求?


Node.js可以通过事件驱动模型轻松处理多个并发请求。


当客户端发送请求时,单个线程会将该请求发送给其他人。当前线程不会忙于处理该请求。服务器有工作人员为其工作。服务器将请求发送给工作人员,工作人员进一步将其发送给其他服务器并等待响应。同时,如果有另一个请求,线程将其发送给另一个工作人员,并等待来自另一个服务器的响应。


这样,单个线程将始终可用于接收客户端的请求。它不会阻塞请求。


Node.js实现多个请求的代码:


const http = require('http');

// 创建一个 HTTP 服务器对象
const server = http.createServer((req, res) => {
// 处理请求
if (req.url === '/') {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('Hello, World!');
} else if (req.url === '/about') {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('About Us');
} else {
// 设置响应头
res.writeHead(404, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('Page Not Found');
}
});

// 监听 3000 端口
server.listen(3000, () => {
console.log('Server listening on port 3000');
});

总结


总的来说,Node.js在处理多个请求方面具有优势。它利用事件驱动和非阻塞式I/O的特性,能够高效地处理并发请求,提供快速响应和良好的可扩展性。同时,通过采用适当的工具和技术,可以进一步优化性能,控制并发量,并提高系统的可靠性和稳定性。


扩展链接:


从表单驱动到模型驱动,解读低代码开发平台的发展趋势


低代码开发平台是什么?


基于分支的版本管理,帮助低代码从项目交付走向定制化产品开发


Redis从入门到实践


一节课带你搞懂数据库事务!


Chrome开发者工具使用教程


作者:葡萄城技术团队
来源:juejin.cn/post/7298646156437438464
收起阅读 »

Android 14 彻底终结大厂流氓应用?

hi 大家好,我是 DHL。大厂程序员,就职于美团、快手、小米。公众号:ByteCode,分享技术干货和编程知识点 在某些大厂内部通常都会有一个神秘的团队,他们的工作内容就是专门研究系统,而的事情就是如何让自家应用在后台存活的更久,达到永生的目的。 其中有个...
继续阅读 »

hi 大家好,我是 DHL。大厂程序员,就职于美团、快手、小米。公众号:ByteCode,分享技术干货和编程知识点



在某些大厂内部通常都会有一个神秘的团队,他们的工作内容就是专门研究系统,而的事情就是如何让自家应用在后台存活的更久,达到永生的目的。


其中有个别公司,甚者利用公开漏洞,达到远程操控用户手机的目的,做更多他们想做的事,可以随意获取用户的隐私,而且一旦安装,普通用户很难删除,之前写了一些揭露他们的文章,但是现在已经被全部删除了,就连评论区抨击他们的内容也全都被删除了。


而 Android 14 的出现,可以说是暂时性的彻底终结了这些流氓软件,想在后台通过保活的方式,让应用在后台达到永生的目的基本不可能了。


为什么这是暂时性的呢,因为没有完美的系统,新的系统虽然修复了公开漏洞,终结了现有的保活的方式,但是新系统可能存在新的漏洞,还是会给某些大厂可乘之机。


我们一起来看一下 Android 工程副总裁 Dave Burke 都介绍 Andorid 14 在性能、隐私、安全性方面做了那些改进,这篇文章是对之前的文章 适配 Android 14,你的应用受影响了吗Android 14 新增权限 的补充。



  • 冻结缓存应用,增强杀进程能力

  • 应用启动更快

  • 减少内存占用

  • 屏幕截图检查

  • 显示全屏系统通知

  • 精确闹钟权限

  • 提供了对照片和视频的部分访问权限

  • 最小 targetSdkVersion 限制

  • 返回手势


本文只会介绍我认为 Android 14 上最大的变化,关于 Android 14 的所有变更,可以前往查看变更
developer.android.com/about/versi…


冻结缓存应用,增强杀进程能力


在 Android 11 以上支持冻结已缓存的应用,当应用切换到后台并且没有其他活动时,系统会在一定时间内通过状态判断,是否冻结该应用,如果一个应用被冻结住了,将完全被 "暂停",不再消耗任何 CPU 资源,可以减少应用在后台消耗的 CPU 资源,从而达到节电的目的。


被冻结已缓存的应用并不会执行终止该应用,冻结的作用只是暂时挂起进程,消耗 CPU 的资源为 0,它有助于提高系统性能和稳定性,同时最大限度地节省设备的资源和电量的消耗,一旦应用再次切换到前台时,系统会将该应用的进程解冻,实现快速启动。


如果你的手机支持冻结已缓存的应用,在开发者选项里会显示 「暂停执行已缓存的应用」设置项。



冻结已缓存应用,在内核层面使用的是 cgroup v2 freezer,相对于使用信号 SIGSTOP 与 SIGCONT 实现的挂起与恢复,cgroup v2 freezer 无法被拦截,也就无法被开发者 Hook,从而彻底终结大厂想在这个基础上做一些事情。


当然 Google 也对 cgroup 进行了封装,提供了 Java API,在上层我们也可以调用对应的方法实现 CPU、内存资源的控制。


public static final native void setProcessFrozen(int pid, int uid, boolean frozen);
public static final native void enableFreezer(boolean enable);

经过测试 Android 14 相比于 Android 13,缓存进程的 CPU 使用量降低了高达 50%,因此,除了传统的 Android 应用生命周期 API,如前台服务、JobScheduler 或 WorkManager,后台工作将受到限制。


另外在 Android 14 上系统在杀进程之前,首先会将应用所有的进程进行 cgroup v2 freezer,被冻结的应用 cpu 资源占用为 0,然后在挨个杀掉进程,想通过进程互相拉取进程的方式,不断的想通过 fork 出子进程,达到应用永生的目的,在 Android 14 以上已经不可能了,这些黑科技都要告别历史的舞台了。


应用启动更快


在 Android 14 上对缓存应用进行优化,增加了缓存应用的最大数量的限制,从而减少了冷启动应用的次数。


而应用的最大缓存数量不是固定的,可以根据设备的内存容量进行调整,Android 测试团队在 8GB 设备上,发现冷启动应用的数量减少了 20%,而在 12GB 设备上减少了超过 30%,冷启动相对于热启动来说速度较慢,而且在电量消耗方面成本较高,这一工作有效地改善了电量使用和整体应用启动时间。


减少内存占用


代码大小是我们关注的关键指标之一,代码量越大虚拟内存占用越高,减少生成的代码大小,对内存(包括 RAM 和存储空间)的影响就越小。


在 Android 14 中,改进 Android 运行时(ART)对 Android 用户体验,ART 包含了优化措施,将代码大小平均减少了 9.3%,而不会影响性能。


屏幕截图检查


在 Android 14 中新增了一个特殊的 API,截取屏幕截图后会有个 ScreenCaptureCallback 的回调,当用户正在使用截取屏幕截图时,将会调用这些回调函数。


要使 API 正常工作,需要在 AndroidManifest 中添加 DETECT_SCREEN_CAPTURE 权限,然后在 onStart() 方法中注册回调,需要在 onStop() 中取消注册。


<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
</manifest>


class MainActivity : Activity() {

private val mainExecutor = MainEcxector()

private val screenshotCallback = ScreenCaptureCallback {
// A screenshot was taken
}

override fun onStart() {
super.onStart()
registerScreenCaptureCallback(mainExecutor, screenshotCallback)
}

override fun onStop() {
super.onStop()
unregisterScreenCaptureCallback(screenshotCallback)
}
}

显示全屏系统通知



Android 11 引入了全屏通知,当全屏应用程序运行时,这些通知将在锁屏屏幕上显示,任何应用都可以在手机处于锁定状态时使用 Notification. Builder. setFullScreenIntent 发送全屏 Intent,不过需要在 AndroidManifest 中声明 USE_FULL_SCREEN_INTENT 权限,在应用安装时自动授予此权限。


从 Android 14 开始,使用此权限的应用仅限于提供通话和闹钟的应用。对于不适合此情况的任何应用,Google Play 商店会撤消其默认的 USE_FULL_SCREEN_INTENT 权限。


在用户更新到 Android 14 之前,在手机上已经安装的应用仍拥有此权限,但是用户可以开启和关闭此权限,所以您可以使用新 API NotificationManager.canUseFullScreenIntent 检查应用是否具有该权限。


如果想在 Android 14 上使用这个权限,我们可以提示用户手动打开授权,通过 Intent(ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT) 来跳转到设置界面。


if(NotificationManager.canUseFullScreenIntent()){
startActivity(Intent(ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT))
}

精确闹钟权限


在 Andorid 12 之前我们可以直接调用 setAlarmClock()setExact()
setExactAndAllowWhileIdle() 等等方法设置精确闹钟时间,


但是在 Android 12 上 Google 引入了一个新的权限 SCHEDULE_EXACT_ALARM,如果想调用 setAlarmClock()setExact()
setExactAndAllowWhileIdle() 等等方法设置精确闹钟时间, 需要在 manifest 中申明 android.permission.SCHEDULE_EXACT_ALARM 权限。


所以运行在 Android 12 ~ Android 13 系统上,我们只需要声明一下权限就可以使用了,但是从 Android 14 开始 SCHEDULE_EXACT_ALARM 权限默认被禁止使用了。


如果你还想在 Andorid 14 以上使用精准闹钟的 API,我们可以提示用户手动打开授权,通过 Intent (ACTION_REQUEST_SCHEDULE_EXACT_ALARM) 来跳转到设置界面,代码如下。


val alarmManager: AlarmManager = context.getSystemService<AlarmManager>()!!
when {
// If permission is granted, proceed with scheduling exact alarms.
alarmManager.canScheduleExactAlarms() -> {
alarmManager.setExact(...)
}
else -> {
// Ask users to go to exact alarm page in system settings.
startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM))
}
}

提供了对照片和视频的部分访问权限


这个限制和 iOS 相似,Android 14 提供了对照片和视频的部分访问权限。当您访问媒体数据时,用户将看到一个对话框,提示用户授予对所有媒体的访问、或者授予单个照片/视频的访问权限,该新功能将适用于 Android 14 上所有应用程序,无论其 targetSdkVersion 是多少。



在 Android 13 上已经引入了单独的照片访问和视频访问权限,但是在 Android 14 上新增了新的权限 READ_MEDIA_VISUAL_USER_SELECTED


<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- To handle the reselection within the app on Android 14 -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

</manifest>

如果没有声明新的权限,当应用程序进入后台或用户终止应用程序时,单独的照片访问和视频访问权限将立即撤销,不会保存 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 权限的状态,每次都需要检查。


最小 targetSdkVersion 限制


Android 14 当中有一个比较大的变化就是,无法安装 targetSdk <= 23 的应用程序 (Android 6.0),不要将它与 minSdk 混淆。


在 Android 开发中有两个比较重要的版本号:



  • compileSdkVersion :用于编译当前项目的 Android 系统版本

  • targetSdkVersion :App 已经适配好的系统版本,系统会根据这个版本号,来决定是否可以使用新的特性


这个最小 targetSdkVersion 限制,主要是出于安全考虑,在 Android 6.0 中引入了运行时权限机制,App 想要使用一些敏感权限时,必须先弹窗询问用户,用户点击允许之后才能使用相应的权限。


但是一些 App 为了利用权限方便获取到用户的信息,通过不去升级 targetSdk 的版本号的方式,在安装过程中获得所有权限,以最低成本的方式,绕过运行时权限机制。


如果之前已经安装了的 App,就算升级到 Android 14 也会去保留,系统不能代表用户去删除某个应用,其实我在想,为什么不针对这些已经安装好的低版本的 App,Google 给出一些警告提示,让用户可以感知到呢


返回手势


在 Android 13 的时候,Google 已经预示我们在下一个版本中,返回手势将会有一些更新,并以预览屏幕的形式呈现动画,效果如下图所示。



我们来演示一下使用后退导航的动画。



在 Android 14 增加了在 App 中创建过渡动画的功能,比如在 OnBackPressedCallback 接口中添加了一个方法 handleonbackprogress() ,这个方法在返回手势执行过程中被调用,我们可以在这个方法中增加一些过渡动画。


OnBackPressedCallback 接口中还提供了两个方法 handleOnBackPressed()handleOnBackCancelled() 分别在动画完成和取消动画时调用,我们来看看在代码中如何使用。


class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val box = findViewById<View>(R.id.box)
val screenWidth =
Resources.getSystem().displayMetrics.widthPixels
val maxXShift = (screenWidth / 20)

val callback = object : OnBackPressedCallback(
enabled = true
) {

override fun handleOnBackProgressed(
backEvent: BackEvent
)
{
when (backEvent.swipeEdge) {
BackEvent.EDGE_LEFT ->
box.translationX = backEvent.progress *
maxXShift
BackEvent.EDGE_RIGHT ->
box.translationX = -(backEvent.progress *
maxXShift)
}
box.scaleX = 1F - (0.1F * backEvent.progress)
box.scaleY = 1F - (0.1F * backEvent.progress)
}

override fun handleOnBackPressed() {
// Back Gesture competed
}


override fun handleOnBackCancelled() {
// Back Gesture cancelled
// Reset animation objects to initial state
}
}
this.onBackPressedDispatcher.addCallback(callback)
}
}

API 被废弃


在 Android 中使用 overidePendingTransition () 方法实现进入和退出动画,但是在 Android 14 上提供了新的 overrideActivityTransition () 方法,而 overidePendingTransition () 方法已被标记为弃用。


// New API
overrideActivityTransition(
enterAnim = R.anim.open_trans,
exitAnim = R.anim.exit_trans,
backgroundColor = R.color.bgr_color
)

// deprecated
overridePendingTransition(R.anim.open_trans, R.anim.exit_trans)



全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!




我开了一个云同步编译工具(SyncKit),主要用于本地写代码,同步到远程设备,在远程设备上进行编译,最后将编译的结果同步到本地,代码已经上传到 Github,欢迎前往仓库 hi-dhl/SyncKit 查看。





Hi 大家好,我是 DHL,就职于美团、快手、小米。公众号:ByteCode ,分享有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经,真诚推荐你关注我。





最新文章



开源新项目




  • 云同步编译工具(SyncKit),本地写代码,远程编译,欢迎前去查看 SyncKit




  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit




  • 最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice




  • LeetCode / 剑指 offer,包含多种解题思路、时间复杂度、空间复杂度分析,在线阅读




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

Nginx与洗脚城技师那些有意思的小故事

FBI WARNING: 内容仅作为举例用途,本人不曾去过洗脚城,也不知道技师是什么 我们今天来讲讲Nginx的一些小常识吧。 什么是Nginx Nginx 是一款是由伟大且牛逼的俄罗斯的程序设计师 Igor Sysoev(伊戈尔 赛索耶夫) 在摸鱼玩耍时...
继续阅读 »

FBI WARNING:



内容仅作为举例用途,本人不曾去过洗脚城,也不知道技师是什么



我们今天来讲讲Nginx的一些小常识吧。


什么是Nginx


Nginx 是一款是由伟大且牛逼的俄罗斯的程序设计师 Igor Sysoev(伊戈尔 赛索耶夫) 在摸鱼玩耍时所开发高性能的 Web反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。


什么是反向代理


说到反向代理之前,我们来看看什么是 正向代理(跑腿代购)。


正向代理运行在客户端的出口,获取客户端发送的请求,用自己的身份发送到服务器并获取返回数据,最终返回给客户端。


常见的正向代理,如网络爬虫的IP代理、VPN、游戏加速器......


image.png


那么,什么是反向代理呢?


反向代理服务器运行在服务端的入口,根据客户端请求的部分参数进行请求的分发与调度等。(那不就是洗脚城的前台吗?)


常见的反向代理场景:



  • 负载均衡

  • 动静分离    

  • 业务分离

  • ......


image.png


Nginx 有哪些作用?


1. Nginx的虚拟站点服务


Nginx可基于 端口号、域名(IP) 实现多站点服务,已实现前端访问80或443端口,通过上述不同条件访问不同的后端服务



一个洗脚城前台,后面有不知道数量的技师。



server
{
listen 80;
server_name http://www.hamm.cn;
index index.html;
root /home/www/portal_site;
}
server
{
listen 80;
listen 443 ssl http2;
# 证书相关配置
server_name api.hamm.cn;
index index.html;
root /home/www/api_service;
}
server
{
listen 80;
server_name static.hamm.cn;
index index.html;
root /home/www/static_service;
autoindex on;
}

image.png


2. Nginx反向代理实现负载均衡


通过反向代理指定上游多个负载点,配置不同的负载优先级以及调度策略来实现负载均衡。



按业务能力和服务质量,洗脚城前台往往会针对不同的技师分配不同的任务。



upstream api_slb {
server 192.168.123.78:8100 weight=5 max_fails=3 fail_timeout=5;
server 192.168.123.78:8200 weight=3 max_fails=3 fail_timeout=5;
server 192.168.123.79:8100 weight=2 max_fails=3 fail_timeout=5;
# 优先使用局域网测试服务器的服务 按权重进行负载


# 如果测试服务器不可用,可通过下面两台备用服务跑

server 192.168.123.77:8080 weight=2 backup;
# 同事张三 代码一般是最新 优先使用
server 192.168.123.151:8080 weight=1 backup;
# 同事李四 工位最近 做备机 方便沟通
}
server {
listen 80;
server_name api.hamm.cn;
location / {
proxy_pass http://api_slb;
}
}

image.png


3. Nginx业务分离或动静分离


使用Nginx解决一些特定的需求



  • 相同域名,不同路径访问不同资源

  • 不同域名,解决访问跨域等问题
    .......



技师根据不同的客户需求,分配不同业务能力的技师。



upstream api_service {
server 192.168.123.78:8100;
}
upstream web_frontend {
server 192.168.123.66:8010;
}
server {
listen 80;

# 使用统一域名 http://hamm.cn访问所有资源

server_name hamm.cn;

# 匹配 http://hamm.cn/api/****
# 到系统API服务器上
location /api/ {
proxy_pass http://api_service;
}

# 如果资源在本机 可使用Nginx自带的静态资源服务
location /static {
index index.html;
alias /home/www/static
}

# 匹配其他请求到前端服务器上
location / {
proxy_pass http://web_frontend
}
}

image.png


4. Nginx完成其他场景下的小需求




  • 跨域问题




server {
# API服务
listen 80;
server_name api.hamm.cn;

# 设置允许跨域的域名 支持通配符 如* 代表所有
add_header Access-Control-Allow-Origin hamm.cn;

# 跨域时允许提交的header数据
add_header Access-Control-Allow-Headers 'authorization','content-type';

# 允许跨域的请求方法
add_header Access-Control-Allow-Methods 'option,get,put,post';

# 还有很多配置项 自己百度吧:)
}
server {
# 前端
listen 80;
server_name hamm.cn;
}




  • 代理过滤





使用 sub_filter 模块将代理中一些数据进行过滤或替换:)



server {
...

location /api {
...
sub_filter_types application/json;
sub_filter_once off;

sub_filter '搜藏成功' '收藏成功';
}
}


技师服务的时候有点不耐烦,大喊了一声 “傻X客户”,洗脚城前台为了洗脚城的形象,给替换成了 “好帅的客户”





  • 自定义错误页




server
{
listen 80;
server_name api.hamm.cn;
error_page 502 503 404 403 500 /error.json;
}


技师让客户不高兴的时候,洗脚城每次都出来给大家唱首歌缓和下气氛。





  • 流量控制 请求黑名单IP 防盗链 请求拦截





技师提前预判是不是意向客户,或者专门找事的客户,提前处理好这些事情,不让技师烦恼。



server
{
listen 80;
server_name static.hamm.cn;
root /home/www/static_files/images;
location ~ .*\.(jpg|gif|png|bmp)$ {
# 如果是百度爬虫 让它一边去
if ($http_user_agent ~ 'Baiduspider') {
# rewrite * "/getout.jpg";
return 403;
}
# 图片 允许直接访问 如有跨域 只允许指定的域名访问
valid_referers none *.hamm.cn;
if ($invalid_referer) {
# 其他的请求 通通甩掉
return 403;
}
}
location /admin/ {
# 如果是上传到admin目录下的文件
allow 123.45.67.89;
# 只允许指定的IP可以访问
deny all;
# 其他人通通甩掉
}
}

image.png


总结一下


我真的没去过洗脚城。


作者:Hamm
来源:juejin.cn/post/7295995236886396939
收起阅读 »

写了个APP「原色」—— 基于中国传统色

中国传统色 简介 这是一个工具类APP 颜色筛选以及关模糊查询 颜色详情信息查看以及复制 色卡分享 自定义主题色(长按色卡) 小组件支持 已上架应用宝/App Store,搜索原色即可找到 最初是做了个1.0版本(MVP),功能比较简单,后面感觉没什么可...
继续阅读 »

中国传统色



简介


这是一个工具类APP



  • 颜色筛选以及关模糊查询

  • 颜色详情信息查看以及复制

  • 色卡分享

  • 自定义主题色(长按色卡)

  • 小组件支持


已上架应用宝/App Store,搜索原色即可找到


最初是做了个1.0版本(MVP),功能比较简单,后面感觉没什么可加的就放置一边了


1.0版本.jpeg


最近比较空闲又拿起来,bug修一点加一点,界面改了又改哈哈哈,然后现在迭代到2.0版本(预览图为 iOS)


2.0版本.jpeg
除了界面大换新,也增加了一些功能,比如颜色搜索、筛选、小组件等。Android与iOS基本一致,除了搜索筛选界面不一样:


Android搜索筛选.jpg


下面介绍一下一些功能的实现以及碰到的问题


色卡与文字处理


在1.0版本对色卡的背景颜色和文字颜色关系处理比较粗暴简单,当系统出去浅色模式下。文字就在原来颜色的基础上降低亮度;在深色模式下文字就降低亮度,但是这种方式在部分过亮或者过暗背景上还是很难看清。

2.0版本对色卡和文字颜色都做了动态处理:

色卡:渐变处理,从上往下,比例为0——0.3——1.0。



在浅色模式下颜色为color(alpha=0.7)——color——color;


在深色模式下颜色为color(brightness + 0.2)——color——color



色卡文字:根据颜色是否为亮色进行处理,判断规则为:



颜色为亮色,则降低0.3亮度,否则 降低0.1亮度



在iOS上有用于修改view亮度的方法:brightness(Double),可惜安卓没有直接修改视图或者颜色亮度的方法,于是我就通过修改颜色 HSL来达到类似的效果。为了和ios的brightness 一致,changeBrightness的范围我设置为[-1F, 1F],但outHsl[2]的范围是[0F, 1F],所以计算做了一些调整:


// 修改颜色亮度
@ColorInt
fun @receiver:ColorInt Int.brightness(changeBrightness: Float): Int {
val outHsl = FloatArray(3)
ColorUtils.colorToHSL(this, outHsl)
if (changeBrightness <= 0) {
outHsl[2] = outHsl[2] * (1 + changeBrightness)
} else {
outHsl[2] = outHsl[2] + (1 - outHsl[2]) / 10 * changeBrightness * 10
}
return ColorUtils.HSLToColor(outHsl)
}

// 判断颜色为两色或者暗色
fun @receiver:ColorInt Int.isLight(): Boolean {
val red = Color.valueOf(this).red()
val green = Color.valueOf(this).green()
val blue = Color.valueOf(this).blue()
val brightness = (red * 299 + green * 587 + blue * 114) / 1000
return brightness > 0.5
}

颜色信息展示(BottomSheet)


设置BottomSheet默认完全展开,设置方法如下:


override fun onStart() {
super.onStart()
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}

至于圆角处理,只需要在主题文件里写好就行了:


 <!--Rounded Bottom Sheet-->
<style name="ThemeOverlay.App.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
<item name="bottomSheetStyle">@style/ModalBottomSheetDialog</item>
</style>

<style name="ModalBottomSheetDialog" parent="Widget.Material3.BottomSheet.Modal">
<item name="shapeAppearance">@style/ShapeAppearance.App.LargeComponent</item>
<item name="shouldRemoveExpandedCorners">false</item>
</style>

<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.Material3.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item>
</style>

如果不修改sheet背景色(默认为白色/黑色),只需要设置以上主题就可以了,但是如果修改了背景色,就需要在代码里对背景进行圆角处理,不能直接设置背景色,不然在圆角下面还会有颜色:


默认设置圆角背景.png


动态设置圆角背景.png


// 会存在背景色
// binding.bottomSheetLayout.setBackgroundColor(sheetBackground)

// 设置圆角背景
binding.bottomSheetLayout.setCornerBackground(24, 24, 0, 0, sheetBackground)

private fun View.setCornerBackground(leftRadius: Int, topRadius: Int, rightRadius: Int, bottomRadius: Int, @ColorInt color: Int) {
val shape = ShapeDrawable(RoundRectShape(
floatArrayOf(
leftRadius.dp(requireContext()).toFloat(),
leftRadius.dp(requireContext()).toFloat(),
topRadius.dp(requireContext()).toFloat(),
topRadius.dp(requireContext()).toFloat(),
rightRadius.dp(requireContext()).toFloat(),
rightRadius.dp(requireContext()).toFloat(),
bottomRadius.dp(requireContext()).toFloat(),
bottomRadius.dp(requireContext()).toFloat(),
), null, null)
)
shape.paint.color = color
this.background = shape
}

fun Int.dp(context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
context.resources.displayMetrics
).toInt()
}

小技巧(应该算啊吧):当我们有icon需要适配深色模式的时候,可以把android:tint的值设置为?android:attr/textColorPrimary ,就不用自己做额外处理了


<vector android:autoMirrored="true" android:height="24dp"
android:tint="?android:attr/textColorPrimary" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">

<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

SearchView背景色修改


可以看之前发的文章



MD3——SearchView自定义背景



效果参考上面的搜索筛选界面


滚动到指定位置(带偏移)


点击左上角的骰子图标,可以随机颜色(滚动到某一位置),通常我们使用recyclerView.scrollToPosition(int position)就可以实现。但是这个方法,会滚动到item的最边缘(红线位置),但是我希望他能够保留一定边距(绿色框框),看起来界面会和谐一点


image.png


解决办法如下:


private fun RecyclerView.scrollToPositionWithOffset(position: Int, offset: Int) {
(layoutManager as GridLayoutManager)
.scrollToPositionWithOffset(position, offset)
}

// 调用
binding.recyclerView.scrollToPositionWithOffset(
Random.nextInt(0, adapter.itemCount - 1),
16.dp(this)
)

用了kotlin扩展方法方便调用,这里的layoutManager根据实际情况来,我这里用列表到的是GridLayoutManager


小组件(App Widget)


提供了两种布局,小尺寸只显示颜色名称,大尺寸显示拼音和名称,效果如下:


Android小组件.jpg


iOS小组件.png


可能在部分系小尺寸统显示有问题,懒得搞了,这个组件大小搞的我脑壳疼,也没看到过什么好的解决方案,以下是我的配置:


// 31以下
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="57dp"
android:minHeight="51dp"
android:updatePeriodMillis="0"
android:previewImage="@drawable/appwidget_preview"
android:initialLayout="@layout/layout_wide_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">

</appwidget-provider>

// 31及以上
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:targetCellWidth="4"
android:targetCellHeight="2"
android:minResizeWidth="57dp"
android:minResizeHeight="51dp"
android:maxResizeWidth="530dp"
android:maxResizeHeight="450dp"
android:updatePeriodMillis="0"
android:previewImage="@drawable/appwidget_preview"
android:initialLayout="@layout/layout_wide_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable">

</appwidget-provider>

因为布局比较简单,所以尺寸兼容效果相对好一点


主动刷新小组件


当我们app没有运行的时候,添加小组件是没有数据的,当我们打开app的时候,通知小组件更新


// 刷新 Widget
sendBroadcast(Intent(this, ColorWidgetProvider::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(application)
.getAppWidgetIds(ComponentName(application, ColorWidgetProvider::class.java))
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
})

周期更新小组件


可通过配置updatePeriodMillis来设置时间,但是容易失效,所以使用WorkManager来通知更新,虽然WorkManager保证了周期执行,但如果app不在后台的话还是无法更新的,因为发送了广播app收不到,可能再加个服务就可以了,不加不加了


遗留的小问题


MIUI无法添加小组件


这段代码在MIUI上不生效,无法弹出添加小组件的弹窗


AppWidgetManager.getInstance(this).requestPinAppWidget(xxx)

如果添加该权限并授权,可以成功添加,但是无任何弹窗提示


<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />

当然最奇怪的还是我居然在MIUI的安卓小部件里找不到我自己的组件,我在原生都能看得到我的小组件的,也不知道是不是还需要配置什么,再一次头大


总结


这个app断断续续也写了好几个月,也也没啥功能还写了这么久。之前还看了下swiftUI,写了个iOS版本的,给我的感觉就是上手简单,写起来效率快多了,

其实这篇文章早就可以发了,就为了等app上架,可真煎熬。

个人开发者上架应用真的是难于上青天,对于安卓平台,国内一些主流应用市场(华米OV)都不对个人开发者开放了,要求低点的比如酷安、应用宝个人是可以上传的,但是需要软著,这又是一个头疼的事,申请基本一个月起步,除非花几百块找别人,三五天下证;

PS:现在App需要备案了,除非你不联网,应用宝就可以上架,酷安也要强制备案

ios也让我很难受,可能是我自己的问题,我注册流程走到付款了,当时想着先写完app再注册好了,就没付款,后来再去注册就提示账户存在问题,邮件联系后告诉我:



您的账号由于一个或多个原因,您无法完成 Apple Developer Program 的注册。



我想问清楚具体是什么原因,客服告知由系统判定,他们无法知道也无法干预,然后我寻思罢了,我再注册一个,还是失败,这次提示:



您使用另一 Apple ID 通过 Apple Developer App 验证了身份。要继续,请使用之前用于验证您身份的 Apple ID。



问号.jpeg


然后我又去把原来的账号注销掉,依旧无法注册成功...,最后无奈使用别人的信息注册了一个乛 з乛

所以,想注册苹果开发者的,注意最好是在同一个设备上一次性完成注册。


作者:FaceBlack
来源:juejin.cn/post/7294441582983626788
收起阅读 »

支持远程调试的 "vConsole"

web
背景 前阵子一直在做业务需求,是嵌在公司 APP 里的 H5。而且是跨地区协作,我在 A 城市,测试和产品都在 B 城市。 由于是 H5 项目,开发的时候一般都会实例化个 vConsole,方便查看项目的上下文信息。同时我想着当程序出现问题之后,测试小姐姐可以...
继续阅读 »

背景


前阵子一直在做业务需求,是嵌在公司 APP 里的 H5。而且是跨地区协作,我在 A 城市,测试和产品都在 B 城市。


由于是 H5 项目,开发的时候一般都会实例化个 vConsole,方便查看项目的上下文信息。同时我想着当程序出现问题之后,测试小姐姐可以直接截个 vConsole 的图给我,可以减少沟通时间。


痛点


后来发现一切都是想象之中,我们两个在沟通问题上依旧没少花时间!如果把程序出现的问题分级,那么会有:



  • 😄 简单问题:测试小姐姐 描述问题 发生的过程后,基本可以定位、解决;

  • 😅 中等问题:测试流程走不下去或者程序报错,这时候得查看调试信息去分析。此时需要测试小姐姐 截图 vConsole 上面显示的内容发我,但由于截的图并不一定是关键信息或者信息数据不够,导致这中间会产生几轮沟通;

  • 😥 复杂问题:遇到一些依赖外部信息或者奇奇怪怪的问题的时候,可能会 远程视频 操作测试机给我看,同时我会告诉她什么时候打开 vConsole 查看什么面板的信息。


可以看到只要问题牵扯到了项目的运行信息,前前后后就会导致很多沟通上的时间成本


不禁让人思考是什么原因导致的这个问题……


问题的本质


结合前面的描述我们得知,由于物理空间、跨地域的限制,程序的错误信息都是由测试人员转达给技术人员,不得不说这对测试人员有点勉为其难了,而另一方面造成问题的关键就在于此:技术人员无法和 Bug 直接来个正面交锋!


那么该如何解决这个「中间人」的问题呢?


这个问题的答案其实很简单,我们只要将浏览器的原生 API 进行一层包装将运行时调用的参数收集起来,然后再整一套类似控制台的 UI,最后整合成 SDK 处理参数 -> 中间层网络通信 -> UI 控制台展示的样子,开发同学直接和控制台上的 BUG 切磋,就能完美的解决这个问题!


虽然说起来简单,但是这一整套下来开发的工作量可不容小觑:



  • 包装原生 API 的 SDK

  • 负责通信的服务

  • 控制台 UI……


不用慌!开箱即用的 PageSpy 值得你拥有 😄!


PageSpy


Page Spy 是由货拉拉大前端开源的一款用于调试 H5 、或者远程 Web 项目的工具。基于对原生 API 的封装,它将调用原生方法时的参数进行过滤、转化,整理成格式规范的消息供调试端消费;调试端收到消息数据,提供类控制台可交互式的功能界面将数据呈现出来。


PageSpy是一个强大的开源前端远程调试平台,它可以显著提高我们在面对前端问题时的效率。以下是PageSpy的一些主要特点:



  • 一眼查看客户端信息 PageSpy 会对客户端的运行环境进行识别,其中系统识别支持 Mac / iOS / Window / Linux / Android,浏览器识别支持谷歌、火狐、Safari、Edge、微信、UC、百度、QQ;

  • 实时查看输出: PageSpy 可以实时捕获并显示程序输出,包括 Console、Network、Storage 和 Element。这使开发人员能够直观地了解页面的外观和行为,无需依赖用户的描述或截图。

  • 网络请求监控: PageSpy 还可以捕获和显示页面的网络请求,有助于开发人员更好的查看与后端的交互。

  • 远程控制台: PageSpy 支持远程调试JavaScript代码,允许开发人员执行 JavaScript 代码在用户的浏览器上运行。这对于排查特定问题或测试代码修复非常有帮助。

  • 跨浏览器兼容性: SDK 可以在各种主流浏览器中运行,确保你可以检查和调试不同浏览器上的问题。

  • 用户体验提升: 通过快速识别和解决前端问题,PageSpy可以显著提升用户体验,减少用户因前端问题而受到的不便。


使用 PageSpy 进行远程调试


使用PageSpy进行远程调试是相对简单的。以下是一些基本步骤:



  • 部署PageSpy: 首先,PageSpy 提供了 Docker、Node 和 Release 的一键部署方案,点击查看

  • 实例化 SDK: PageSpy 成功部署后,你可以在项目中引入对应的 SDK 文件并进行实例化,它提供了多场景类型的参数,以便于用户对它的行为进行定制。

  • 实时监控页面: 之后,你可以实时查看页面的各种数据,这有助于你直观地理解页面的问题。

  • 监控网络请求: 你还可以查看所有的网络请求,包括请求的URL、响应代码和响应时间。这可以帮助你识别与后端通信相关的问题。

  • 解决问题: 借助PageSpy提供的信息和工具,你可以更快速地定位和解决前端问题,从而提高用户体验。


相关截图


门户首页


image.png


待调试列表


image.png


调试界面


image.png


image.png


结语


前端远程调试对于快速而准确地解决前端问题至关重要。Page Spy 作为一个强大的开源工具,支持开箱即用,为开发人员提供了一个高效的方式来查看用户端的页面输出、网络请求和执行远程调试。它有助于加速问题的定位和解决,减少了对用户反馈和日志的依赖,提高了整体开发效率。除了解决跨地区协同的场景之外,还覆盖了本地开发时的调试 H5 的场景。


希望本文能够帮到大家对 PageSpy 有个初步的认识,感谢阅读。


作者:Blucas
来源:juejin.cn/post/7298161887882592310
收起阅读 »

把jsp重构成vue

web
记录一次重构的经历与感想!望自己将来开发之路越走越顺利。 话说,我在入职之前,公司一直使用的jsp技术,并结合jQuery来处理前端页面逻辑。 但在我入职之后不久,我们领导就要求把它重构成vue。 这时,我对jsp根本不熟,业务也没摸清楚。且在这个关键时刻,另...
继续阅读 »

记录一次重构的经历与感想!望自己将来开发之路越走越顺利。


话说,我在入职之前,公司一直使用的jsp技术,并结合jQuery来处理前端页面逻辑。


但在我入职之后不久,我们领导就要求把它重构成vue。


这时,我对jsp根本不熟,业务也没摸清楚。且在这个关键时刻,另一个前端因为离家较远直接离职了!


这个担子竟然压到我一个人身上,心里一万匹草泥马奔过。。。


但也没办法,只能赶鸭子上架,怀着极其忐忑的心情进入了开发阶段,当然也有点兴奋,于我而言这也是一个难得的实践机会!


我把这次重构的经历大致分成四个阶段:(1)摸清楚jsp项目的代码(2)用Vue CLI把项目工程搭建起来(3)整理业务逻辑(4)写vue代码


1.摸清楚jsp项目的代码


老项目中的jsp文件是长这样子:


jsp页面.png


JSP(JavaServer Pages)技术是一种基于Java的Web应用程序开发技术,它允许开发人员将Java代码嵌入到HTML中,以动态生成Web页面。


虽然这是一个比较古老的技术,我也是一脸懵圈,但秉承着前端框架无非是对html、css、js的结合的原则,我硬着头皮读代码。


经过一段时间的浸泡,并且在分析后,我发现,只需要稍微懂一点jsp技术,其实就完全可以读懂jsp了,jsp页面最大的特点就是可以通过java注入参数,除此,它和所有前端框架一样是由三部分组成:



  • html部分,就把jsp文件看成html文件,虽然里面注入了一些参数,但这些参数可以让后端通过接口返回,再拿去渲染就行了

  • css部分,老项目和vue基本可以共用

  • js部分,新老项目的功能是一样的,老项目中用jquery实现的,再拿vue去实现一遍即可


基于这些,此时我对把jsp重构成vue已经有了一些把握,总体原则大概是:对html、css、js这三部分,可以重用的部分就重用,不能重用的部分就重写。


2.用Vue CLI把项目工程搭建起来


使用脚手架,很快就搭建好了项目,都是傻瓜式的操作,这个没什么好说的,给大家看一下目录结构:


vue目录结构.png


3.整理业务逻辑


我后知后觉才发现,重构最难的不是编码,而是业务逻辑


我对业务逻辑的信息来源有两个:一是看老项目的源代码,二是问其他老员工


但是前者效率极低,后者又困难重重


为何这么说,参考以下两点:



  1. 程序员最痛苦的莫过于阅读别人写的代码

  2. 作为新人,得遵守一些职场潜规则


但又没办法,只得蛮力通关了,忍受着巨大的痛苦,一方面得加班阅读代码,一方面要虚心求教老员工(我司的环境大概是,对业务越熟的人,脾气就越大,问题问多了,他们会很不耐烦,对此,我做了很多心理建设)。


后来,我也是整理出了一份前端业务逻辑资料,然后被放到了公司公共文件夹里,被后来的员工永久查看学习🐶!


前端业务逻辑整理.png


4.写vue代码


最后阶段就是编码了,我把它分为前期和后期。


前期攻坚难点,重点关照那些难实现的功能,后期画页面,要保持效率,基本要能够一天画2个页面。


后来翻看了一下代码库的提交记录,从第一行代码的提交,到进入测试,历时3个月。


编码阶段是枯燥乏味的,前面靠"蛮劲"可以挺过去,但是现在每天得靠"有恒"二字给自己打气🐪!


然后,终于把项目重构完成了,我也长吁了一口气!


但是事情并没有我想象的那么简单,更恶心的事情来了,bug颇多!


短短几天,测试就提了几百个问题单!


问题单数.png


泪奔啊!蛮劲用完了,恒心也消磨的差不多了!但是问题还是不依不饶的出现。。。


可能我这人就属于那种打不死的小强,想着好不容易坚持到这一步,无论如何我都要拿下它!


于是又向bug们发起了"猛攻"!


又渡过了一段漫长且艰难的解bug时期。。。


终于把bug也解完了,我和测试都长吁一口气!


什么?代码要想顺利上线,还要处理CI?


最后,我精疲力竭的处理完了一千多个CI问题,也终于体会到了,有时候,不逼自己一把,你永远不知道自己可以做到什么程度!


CI数.png


至此,项目终于上线了!


这次重构经历,我思考了这么几个问题:




  1. 公司为什么要重构这个项目?


    答:这个项目本来用户量大,将来还有大量的新需求要接,但是技术架构上已经落后了,如果不重构,将来搞不定新需求,老代码也不好维护,毕竟新来的员工会jsp的没多少。




  2. 重构的重点在哪?


    答:在于业务。业务是大于技术的,特别是新员工,别急着钻研项目中用到了哪些技术,还是多花点时间了解业务吧!




  3. 重构的难点在哪?


    答:技术上的困难总有办法,但是沟通上的困难却似不可逾越的鸿沟,因为工作的日常除了编码,更多的是:和产品互怼、与测试撕逼、向领导交差,所以,程序员们,提升情商吧!




我觉得,最重要的,是进行心态建设,遇到难关不要怕,永远相信自己可以挺过去,毕竟知识是死的,人是活的,只要我们"有恒",就算再难的东西,用"蛮劲"去"猛攻",终将拿下!


作者:玄玄子
来源:juejin.cn/post/7298167437526269952
收起阅读 »

破涕为笑,一个node中间层bug我让同事的名字出现在全球用户的页面上

web
前言 近期遇到了一个线上故障,排查花了很多时间精力,在bug复现过程中,我还不小心让同事的名字出现在了全球用户的页面上,从“一把辛酸泪”到“破涕为笑”,感觉特别有意思。本文中的代码、描述都过滤了敏感信息,以简单demo的形式复现该故障。 如下图,不管用户搜索啥...
继续阅读 »

前言


近期遇到了一个线上故障,排查花了很多时间精力,在bug复现过程中,我还不小心让同事的名字出现在了全球用户的页面上,从“一把辛酸泪”到“破涕为笑”,感觉特别有意思。本文中的代码、描述都过滤了敏感信息,以简单demo的形式复现该故障。


如下图,不管用户搜索啥词,每个用户页面回显的都是sivan
image.png


业务功能描述


站点的搜索功能,搜索功能会根据业务场景继续细分为a搜索,b搜索...,每种搜索在node中间层走的可能是不同的链路。


如下图展示,a搜索回显了default


image.png


b搜索回显了sang和for


image.png


image.png


故障描述



  1. 偶现,但是触发频率很高

  2. 现象为在x国家站点上,不管用户搜索什么内容,页面回显的大概率是一串固定的字符串

  3. 只有在x国家站点会出现该故障,其他国家站点没有出现

  4. 测试环境无法复现,只有线上环境会出现该故障


image.png


image.png


排查


炒面代码分析


从线上的故障现象来看,像是搜索词被替换掉了,分析看客户端页面下发的参数是没有任何问题的,找搜索服务的后端协助,后端说他们接收到的搜索词就已经是有问题的搜索词了。


初步猜测是被类似xssFilter之类的转换函数替换掉了原来的搜索词,或者node中间层有某一条链路的代码把搜索词改掉了。于是把node中间层的搜索链路的相关代码都研究了好几遍,通过关键字搜项目全局,把每个可疑的地方都看了,感觉代码逻辑写的都没毛病(node中间层的代码链路写得跟炒面一样,看得头都大了)。


没办法,代码分析不出来问题所在,测试环境又无法复现,只能在代码分析的基础上,把每个有可能改到搜索词的可疑地方打上日志,在搜索链路的一些比较关键的执行地方也打上日志,重新发版,来辅助排查。


// 线上打日志的时候需要注意加条件限制,不然每个用户请求都打日志,一下子就打爆了
if (req.query.sdebug === 's') {
logger.warn({ /** data */ })
}

抓住日志这根救命稻草


之前也有猜测,可能是网关啊、waf啊把请求拦截下来更改了搜索词,所以我们在请求入口那里也打了日志。从日志上来看,从中间层入口进来时,此刻的搜索词还是正常的,说明不是网关、waf搞的鬼。第一次的日志帮助我们缩小了排查范围,但是还不能分析出来,还需要再补充一些日志,意味着还要再发版,没办法,就是这么麻烦。


考验你js能力的时候到了


日志只是一种辅助手段,帮你记录异常数据,缩小排查范围。是否能从一堆代码中找出那一行有问题的代码就要看你自己了,我把有问题的代码写成一个demo了,展示在下面,你能分析出来问题所在吗?


const express = require('express')
const app = express()

const aConfig = Object.freeze({
info: { word: 'default' },
getWord ({ word }) {
return word
},
})
const bConfig = Object.freeze({
info: {},
getWord ({ req, word }) {
// 日志记录到这里word是'sivan',正常word应该是undefined,取的是req.params[0]才对
return word || req.params[0]
},
})

const setRequestData = ({ info: { word }, getWord }) => {
return (req, res, next) => {
word = word || req.query.word
res.end(getWord({ req, word })) // 回显搜索词
}
}

const getHandler = (config) => {
return setRequestData(config)
}
const aSearch = getHandler(aConfig)
const bSearch = getHandler(bConfig)

app.get('/a-search', aSearch)
app.get(/^\/b-search\/([^\/]+)\/?$/, bSearch)

app.listen(2333, () => {
console.log('run')
})

开始揭开谜底


const setRequestData = ({ info: { word }, getWord }) => {
return (req, res, next) => {
word = word || req.query.word // 罪魁祸首
res.end(getWord({ req, word }))
}
}

在a搜索链路中,word是有值的,为'default'。
在b搜索链路中,word和req.query.word都应该是undefined,所以b搜索链路传给getWord的word应该是undefined才对。


观察setRequestData这个函数的实现,它对config解构出了word变量,然后返回了一个中间件函数,word变量的作用域是在setRequestData的函数作用域里的,setRequestData函数只会执行一次,而中间件函数在每一次请求中都会执行。


中间件函数使用了word变量,这就创建了一个闭包,闭包使得word变量可以长期存储和被访问。


复现步骤如下:



  1. 我们第一次输入http://localhost:2333/b-search/sang?word=sivan(拼接上?word=sivan),回显sivan

  2. 之后我们每次输入http://localhost:2333/b-search/xxx,xxx为任意字符串,都会回显sivan


setRequestData函数只会执行一次,中间件函数每一次请求都会执行,所以当我们第一次输入http://localhost:2333/b-search/sang?word=sivan时,word变量被赋值为req.query.word并因为闭包被存储起来,等下一次输入时,由于word = word || xx,会先取存储的word,这就导致了每一次输入都会回显sivan。


改动思路如下截图:虽然闭包还存在,但是这样修改就不会让闭包的变量值被意外篡改,导致意料之外的结果了。


image.png


归因


这个问题其实挺严重的,搜索功能直接没用,用户都搜不了内容了,打工人打工不容易,哭泣。也挺有意思的,我只要在链接后面拼接?word=sivan就可以让全球的用户看到同事的名字,扬名立万(不止万了,起码千万了),破涕为笑。


为什么是偶现的呢?因为是集群,有很多服务器节点,每一次请求都可能打到不同的节点上,你输入b-search/xx?word=sivan时,请求只会打到其中一个节点上,只会污染那一个节点上的那个长期存储的word变量。所以被污染的集群节点有问题,没被污染到的集群节点就没问题。


为什么只有x国家站点出现该故障?测试环境没出现过该故障?因为这个故障的触发条件比较苛刻,必须输入b-search/xx?word=sivan才会触发,而正常情况下b搜索链路是不会拼接word=sivan这个query参数的。猜测最开始之时,就是有人在x国家站点因为一些原因输入了http://localhost:2333/b-search/sang?word=sivan引发问题,其他国家站点和测试环境没有输入就没有问题。


触发条件这么苛刻,是谁触发的呢?



  • 有可能是用户,毕竟几千万用户在用搜索,什么情况在用户那都会发生

  • 有可能是测试人员,测试在线上环境偶然拼接了这个参数

  • 有可能是黑客或者友商(概率很低,因为只影响了部分站点)

  • 前端开发人员,实现了这么一段如此隐晦的bad代码,等哪一天加班太多,心里不爽了,回家敲几个字拼接url访问,网站功能立马下线。


删库跑路的梗大家都耳熟能详,我们前端不止是会在svg里面、console里面吐槽公司,我们还可以在node中间层里写bad bad的代码哦,而且还很难测出来,事后归因到前端身上。


作者:前端爆冲
来源:juejin.cn/post/7294852698460471308
收起阅读 »

记录一次接口加密的实现方案

web
隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。 背景介绍 由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日...
继续阅读 »

0002.jpg
隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。


背景介绍


由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日程,部门的大佬们也讨论了各种加密方式,考虑各种情况,最终敲定了方案。说到我们常用的数据加密方法,方式是各种各样的,根据我们实际的业务需求,我们可以选择其中的一种或者几种方式方法进行数据加密处理。




  • 加密方法:常用的AES,RSA,MD5,BASE64,SSL等等;

  • 加密方式:单向加密,对称加密,非对称加密,加密盐,数字签名等等;



首先我们来简单分析一下上面说到的这几种加密有什么区别吧:




  • AES加密:对称加密的方法,加解密使用相同的加密规则,密钥最小能够支持128,192,256位(一个字节8位,后面我使用的是16位字符);

  • RSA加密:非对称加密的方法,加解密使用一对公钥私钥进行匹配,客户端使用公钥加密,服务端使用私钥进行解密;

  • MD5加密:单向加密,加密后不可解密,只能通过相同的数据进行相同的加密再与库中数据进行对比;

  • BASE64:一种数据编码方式,伪加密,把数据转化为BASE64的编码形式,通过A-Z,a-z,0-9,+,/ 共64个字符对明文数据进行转化;

  • SSL加密:https协议使用的加密方式,使用多种加密方式进行加密(具体使用哪些,我也不了解,感兴趣的同学可以去搜一下告诉我哈);



想要详细了解各类加密方式方法的同学,可以自行百度一下哈,这里就不进行赘述了,之后就来详细讲一下本次使用的加密方式。本次为了更加全面加密,使用了AES,RSA,以及加密盐,时间戳,BASE64与BASE16转化等方式进行加密处理。


请求体AES加密


请求体使用AES的对称加密方式,每次接口请求会随机生成一个16位的秘钥,使用秘钥对数据进行加密处理,返回的数据也会使用此秘钥进行解密处理。


import CryptoJs from 'crypto-js'// AES加密库
import { weAtob } from './weapp-jwt' // atob方法

// 请求体加密方法
export const encryptBodyEvent = (data, aeskey, isEncryption) => {
// 请求体内容
const wirteData = {
data: data, // 接口数据
token: Taro.getStorageSync("authToken"), // token 校验
nonce: randomNumberEvent(32), // 32位随机数,接口唯一随机数,可查询服务日志
timestamp: new Date().getTime, // 时间戳,用于设置接口调用过期时间
}
const encryptBodyJson = CryptoJs.AES.encrypt(JSON.stringify(wirteData), CryptoJs.enc.Utf8.parse(aeskey), {
mode: CryptoJs.mode.ECB,
padding: CryptoJs.pad.Pkcs7
}).toString()
// 判断接口是否需要加密
// 服务接收BASE16数据,Base64toHex方法为BASE64转化为BASE16方法
return isEncryption ? Base64toHex(encryptBodyJson) : wirteData
}

// BASE64转化BASE16方法
function Base64toHex (base64) {
let raw = weAtob(base64)
let HEX = ""
for (let i=0; i < raw.length; i++) {
let _HEX = raw.charCodeAt(i).toString(16)
HEX = (_HEX.length == 2 ? _HEX : "0" + _HEX)
}
return HEX
}

// 生成n位随机数,默认生成16位
function randomNumberEvent (length = 16) {
let str = ""
let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
// 随机产生
for(let i=0; i < length; i++){
let pos = Math.round(Math.random() * (arr.length-1));
str += arr[pos];
}
return str
}


  • mode是分组加密模式:有五种模式(ECB、CBC、CFB、OFB、CTR),这里我们使用最简单的ECB模式,明文分组加密之后的结果将直接成为密文分组,对其他几种模式感兴趣的可以去搜索一下几种模式的区别;

  • padding是填充模式:正常的加密后的字节长度不可能刚刚好满足固定字节的对齐(块大小),所以需要进行一定的填充,常用的有三种模式(PKCS7、PKCS5、Zero,还有其他模式),这里我们使用的是PKCS7模式。假设数据长度需要填充n个字节才对齐,那么填充n个字节,每个字节都是n;假设数据本身就已经对齐了,则填充一个长度为块大小的数据,每个字节都是块大小;

  • weAtob即小程序使用的atob方法:atob是JS的一个全局函数,用于将BASE64编码转化为原始字符串,在正常的H5项目中atob可以直接使用,但是在小程序中此方法不可用,因此使用一个手动实现的方式(文件就不上传了,电脑是加密的,上传也是乱码,网上也是能找到类似的方法);

  • timestamp是用于防止过期调用:这里的时间是为了展示方便直接使用客户端时间,实际是会调用一个服务端的接口获取服务器时间进行时间校准,防止客户端手动修改时间,服务端设置过期时间,会根据传入的时间判断是否过期;


请求头RSA加密


看完上面的请求体加密,我们会想到一个问题,就是我们的aesKey是客户端随机生成的,但是服务端也需要这个aesKey进行数据的加解密,那么我们通过什么形式传给服务端呢?因此我们在请求头中设置一个secret-key字段,使用RSA中的公钥对aesKey进行加密,服务端使用对应私钥进行解密;


// import JSEncrypt from 'jsencrypt' // RSA加密库,小程序不支持
import WxmpRsa from 'wxmp-rsa' // RSA加密库,小程序支持

let public_key = 'xxxxxxxxxxxxxxxx' // 公钥
// 请求头加密方法
export const randomKeyEvent = (aesKey) => {
// JSEncrypt方法小程序不可用
// const RSAUtils = new JSEncrypt() // 新建JSEncrypt对象
// RSAUtils.setPublicKey(public_key) // 设置公钥
// return RSAUtils.encrypt(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')

const RSAUtils = new WxmpRsa() // 新建WxmpRsa对象
RSAUtils.setPublicKey(public_key) // 设置公钥
// 进行RSA加密后,生成字符串中的部分特殊字符在服务端会被自动转化为空格,导致解密失败,所以先进行转换处理
return RSAUtils.encryptLong(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
}


  • JSEncrypt在小程序不可用是由于库里面存在window对象以及navigator对象,但是小程序没有对应的方法,所以使用了一个优化后的wxmp-rsa库;

  • replaceAll处理字符是因为RSA加密后,生成字符串中的部分特殊字符传给服务端会被自动转化为空格,导致解密失败,所以需要进行转换处理,为了兼容低版本replaceAll方法不支持可使用replace加正则进行替换;


返回体AES解密


服务端返回的数据内容使用了相同的AES加密方法,因此也需要使用AES进行数据解密处理,并且返回的数据是BASE16,因此还需要进行一次编码转换处理;


// 返回体解密方法
export const decryptBodyEvent = (data, aeskey) => {
// HexToBase64为BASE16转化为BASE64方法
const responseData = CryptoJs.AES.decrypt(HexToBase64(data), CryptoJs.enc.Utf8.parse(aeskey), {
mode: CryptoJs.mode.ECB,
padding: CryptoJs.pad.Pkcs7
}).toString(CryptoJs.enc.Uth8)
return JSON.parse(responseData)
}

// base16转base64 网上找个一个方法,应该有其他简单的实现方式
function HexToBase64 (sha1) {
var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var base64_rep = ""
var ascv
var bit_arr = 0
var bit_num = 0

for (var n = 0; n < sha1.length; ++n) {
if (sha1[n] >= 'A' && sha1[n] <= 'Z') {
ascv = sha1.charCodeAt(n) - 55
} else if (sha1[n] >= 'a' && sha1[n] <= 'z') {
ascv = sha1.charCodeAt(n) - 87
} else {
ascv = sha1.charCodeAt(n) - 48
}

bit_arr = (bit_arr << 4) | ascv
bit_num += 4
if (bit_num >= 6) {
bit_num -= 6
base64_rep += digits[bit_arr >>> bit_num]
bit_arr &= ~ (-1 << bit_num)
}
}

if (bit_num > 0) {
bit_arr <<= 6 - bit_num
base64_rep += digits[bit_arr]
}
var padding = base64_rep.length % 4
if (padding > 0) {
for (var n = 0; n < 4 - padding; ++n) {
base64_rep += "="
}
}
return base64_rep
}

封装接口


因为是小程序项目,使用的是Taro框架进行封装的,Vue中使用axios封装其实也是类似的,还封装了一套ajax的方法,除了接口这里封装有区别,加密都是类似的。


const baseUrl = 'https://xxx.xxx.com' // 接口请求头

// Taro封装接口方法
export const requestEncHttp ({url, data, isEncryption = true}) => {
// 每次调用都会随机生成一个动态的aesKey,防止接口被复用
const aesKey = randomNumberEvent()
return new Promise((resolve, reject) => {
Taro.request({
method: "POST",
header: {
"content-type": "application/json",
"secret-key": isEncryption ? randomKeyEvent(aesKey): ''
},
dataType: 'text',
data: encryptBodyEvent(data, aesKey, isEncryption),
url: baseUrl + url,
success: (result) => {
if(result.status === 200) {
resolve(isEncryption ? decryptBodyEvent(result.data, aesKey) : JSON.parse(result.data))
} else {
reject(result)
}
}, fail: (err) => {
reject(err)
}
})
})
}


  • dataType使用text:ajax没有此问题,Taro框架会出现接口有返回数据,但是在success中接收不到数据,因为数据是BASE16形式,Taro封装的数据返回格式默认应该是JSON的,所以要单独设置一下。


总结


加密的方式有很多,这篇文章也只是浅尝即止,想更加详细了解的同学可以再搜一些大佬们的总结文章,我这里也只是结合业务做了一点总结,标注一下踩坑点;



  • 刚开始本来是准备一个月最少写一篇的,但是由于八月份刚换工作,不太有时间去写,所以也是一直拖着;而且是用公司电脑写的这篇文章,所以代码都没有直接粘贴过来,可能会存在疏漏,请多多包涵哈;


作者:追风筝的呆子
来源:juejin.cn/post/7298160530291490828
收起阅读 »

拖拽API的简单应用

web
我们在实际开发中经常能遇见拖拽的运用场景,比如说拖拽排序、拖拽删除等,本文将以实现一个简单的课程表来进行拖拽API的简单应用,帮助大家复习一下一些基础知识。 相关拖拽事件 实现一个元素拖拽,我们只需要在HTML标签设置draggable为true <...
继续阅读 »

我们在实际开发中经常能遇见拖拽的运用场景,比如说拖拽排序、拖拽删除等,本文将以实现一个简单的课程表来进行拖拽API的简单应用,帮助大家复习一下一些基础知识。


tutieshi_640x390_9s.gif


相关拖拽事件


实现一个元素拖拽,我们只需要在HTML标签设置draggabletrue


 <div class="left">
<div draggable="true" class="color1 item">语文</div>
<div draggable="true" class="color2 item">数学</div>
<div draggable="true" class="color3 item">英语</div>
<div draggable="true" class="color4 item">音乐</div>
<div draggable="true" class="color5 item">政治</div>
<div draggable="true" class="color6 item">历史</div>
<div draggable="true" class="color7 item">体育</div>
</div>

我们设置了拖拽属性,在拖动的过程中我们会触发很多事件


// 拖动开始
container.ondragstart = (e) => {
console.log('start', e.target)
}

// 拖动覆盖
container.ondragover = (e) => {
console.log('over', e.target)
}

// 拖动进入
container.ondragenter = (e) => {
console.log('enter', e.target)
}

// 拖动结束
container.ondrop = (e) => {
// 一般div、td是不允许有元素置于他们上面,在ondragover设置阻止冒泡
console.log('drop', e.target)
}

如上,我们在这个应用主要用到了这几个拖拽事件,其中要特别注意的是ondrop事件,因为很多的HTML标签是不允许有其他元素覆盖在他们上面的,我们在案例中最外层用了div标签,所以必须要设置阻止冒泡才能让该事件生效


设置拖拽鼠标样式


如效果图所演示,我们在新增课程的时候,鼠标呈现的是一个加号的状态,在移除时又是一个简单的鼠标样式。这里我们是通过datasetondragstart设置相关属性来进行动态实现的


    <div class="left">
<div data-effect="copy" draggable="true" class="color1 item">语文</div>
<div data-effect="copy" draggable="true" class="color2 item">数学</div>
<div data-effect="copy" draggable="true" class="color3 item">英语</div>
<div data-effect="copy" draggable="true" class="color4 item">音乐</div>
<div data-effect="copy" draggable="true" class="color5 item">政治</div>
<div data-effect="copy" draggable="true" class="color6 item">历史</div>
<div data-effect="copy" draggable="true" class="color7 item">体育</div>
</div>

container.ondragstart = (e) => {
// 设置拖拽鼠标样式 默认值为move
e.dataTransfer.effectAllowed = e.target.dataset.effect
}

设置拖拽背景色


依旧根据设置的datakey,并检索父级,通过ondragenter事件动态插入class,实现背景色的显示


  <div class="left" data-drop="move">
<div data-effect="copy" draggable="true" class="color1 item">语文</div>
<div data-effect="copy" draggable="true" class="color2 item">数学</div>
<div data-effect="copy" draggable="true" class="color3 item">英语</div>
<div data-effect="copy" draggable="true" class="color4 item">音乐</div>
<div data-effect="copy" draggable="true" class="color5 item">政治</div>
<div data-effect="copy" draggable="true" class="color6 item">历史</div>
<div data-effect="copy" draggable="true" class="color7 item">体育</div>
</div>


<tr>
<th rowspan="4" class="span">上午</th>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>


function getDropNode(node){
while(node){
if(node?.dataset?.drop){
return node
}
node = node.parentNode
}
}

function clearDropStyle(){
const dropNodes = document.querySelectorAll('.drop-over')
dropNodes.forEach((node) => {
node.classList.remove('drop-over')
})
}

container.ondragenter = (e) => {
clearDropStyle()
const dropNode = getDropNode(e.target)
if(!dropNode){
return
}
if( e.dataTransfer.effectAllowed === dropNode?.dataset?.drop){
dropNode.classList.add('drop-over')
}
}

实现新增删除


根据一开始的设想,我们是新增了dataset进行同类别的有效拖拽,依旧进行比较,根据情况新增、删除节点


let source;

container.ondragstart = (e) => {
// 设置拖拽鼠标样式
e.dataTransfer.effectAllowed = e.target.dataset.effect
source = e.target
}


container.ondrop = (e) => {
// 一般div、td是不允许有元素置于他们上面,在ondragover设置组织冒泡
console.log('drop', e.target)

clearDropStyle()
const dropNode = getDropNode(e.target)
if(!dropNode){
return
}
if(e.dataTransfer.effectAllowed !== dropNode.dataset.drop){
return
}
if(dropNode.dataset.drop === 'copy'){
dropNode.innerHTML = ''
const cloned = source.cloneNode(true)
cloned.dataset.effect = 'move'
dropNode.appendChild(cloned)
}else{
source.remove()
}
}

我们在ondrop是不能拿到拖拽的节点的,设置一个全局变量,在ondragstart中保存节点,同时在复制完节点后要将其dataset-effect改成move


作者:_初七
来源:juejin.cn/post/7297908859176681484
收起阅读 »

【复盘】2023年写过的bug

前言 在默默的算着,2023年300多天的日子已经悄然无息的从我手中流走,还没来得及细细的品味,2023年即将逝去。在最近工作中,遇到了两个线上比较棘手的bug,今天,就对自己2023年写过的bug来个复盘吧 2023年写过的bug 截至今日,以下数据是202...
继续阅读 »

前言


在默默的算着,2023年300多天的日子已经悄然无息的从我手中流走,还没来得及细细的品味,2023年即将逝去。在最近工作中,遇到了两个线上比较棘手的bug,今天,就对自己2023年写过的bug来个复盘吧


2023年写过的bug


截至今日,以下数据是2023年每月我解决操作过的bug数据分布:


image.png
以上大多数是测试阶段的bug,个别是线上的。作为一个码农,自认为在提测前对自己写过的功能保持着一个比较负责任的心态,尽量保持高质量的提测。但,没有谁能保证不写 bug,不出错的。我们要做的,是在bug出现以后,找到问题的根源,解决问题,避免同类问题再次发生。


解决一线上纰漏bug流程复盘


1、bug现场


前几天公司为迎接双十一,搞满减满折活动促销,对全场商品(除个别商品)仅仅开启每满200-70的满减满折活动,收到反馈,个别商品列表活动的标签显示错误,列表有500个左右商品,大概5个左右商品标签差个每字


image.png


2、代码逻辑


后台创建满减满折活动时,会将哪些商品能参加这个活动具体的标签写入缓存中,C端查询商品列表时,会去商品查询缓存,查看这个商品正在参加的进行中的活动的标签返回给前端展示。由于后台满减活动的配置逻辑是我做的,标签也是我写入缓存的,看到这个反馈,我心中第一反应,满减活动创建添加商品标签的缓存逻辑代码有bug?


3、排查思路



  • 测试环境创建一个满减满折活动,看是否个别商品满减标签显示错误,发现没有问题,复现失败

  • 让测试协助,是否测试环境能否复现此bug,测试也无法复现

  • 由于这块我没有特意代码补充日志,公司框架也没有全局去拦截请求的入参,靠日志排查之路走不通

  • 怀疑是否还开启了别的活动,商品是否还参与别的活动,冲突导致,查看数据库数据,发现目前仅仅就开启着这一个活动

  • 怀疑是否曾经开启过满200-70的活动,发现也没有开启过

  • 去看数据库500个左右的商品,5个标签错误的商品找出规律,没有发现

  • 活动创建,后编辑该了类型?发现活动创建成功后,也没有编辑过

  • 活动创建前端参数错误?回头着重去看我写的那块代码,再三斟酌,没有发现有问题,就算前端参数错误,为啥就那几个商品少了个每次,此路也不通

  • 想过并发,没有道理,因为创建活动就当时一个人操作

  • 想过很多人线上复现不了的bug最常见的解释,网络波动,我那也不应该,要么写入失败,不会写入成功,少一个字

  • 想过redis的缓存数据被人工手动改过?毕竟是线上的,不会随便改,也不应该,概率可以忽略

  • 既然C端的标签是从缓存拿去的,无奈我项目全局搜索所有用到设置商品缓存代码逻辑,一个个前后调用去看,最后给我看到了查询满减活动表,然后设置标签的代码,一步步定位最终最外层controller代码,才发现商品编辑那里有修改商品缓存的逻辑,只要涉及到商品编辑,那里会先删除这个商品的所有缓存,然后在添加缓存,由于那块组装商品标签逻辑和我创建活动添加标签逻辑不一致,少了一个类型判断导致


由于商品管理那块,我没有参与过需求的评审,开发、设计,所以从始至终排查问题都没有想到过那里会影响我做的满减满折活动的商品标签


4、问题解决


任何一个bug,能复现找到问题的根源就好解决,最棘手的时没有人能复现,又没有日志,完全去猜各种可能性


image.png


5、总结



  • 针对一些难以复现的bug或者遇到的技术问题 ,找到根本原因很重要

  • 要多了解业务,把各种变更造成的影响,要能提前预知到

  • 如果项目框架没有完善的全局日志记录,重要地方适当打印下日志


作者:千呼万唤始出来
来源:juejin.cn/post/7297917491795902476
收起阅读 »

面试官问我唯一ID如何生成

由于简历上,写了分库分表,还写了精通微服务和分布式系统,然后被面试官一顿蹂躏了这里面的技术细节;尤其给我印象最深的问题是,在分布式环境下,你们数据的唯一ID是如何去生成的? 当时条件反射,无脑的回答到,通过数据库自增方式生成的唯一ID;面试官听了后,叫我回家等...
继续阅读 »

由于简历上,写了分库分表,还写了精通微服务和分布式系统,然后被面试官一顿蹂躏了这里面的技术细节;尤其给我印象最深的问题是,在分布式环境下,你们数据的唯一ID是如何去生成的?


当时条件反射,无脑的回答到,通过数据库自增方式生成的唯一ID;面试官听了后,叫我回家等通知,然后就没有然后了。。。。。。。


目前业界有哪些唯一ID的生成方式了?


大概有5种。如下图


1698825367530.png


UUID模式


uuid全称是通用唯一识别码(Universally Unique Identifier)。底层是通过mac地址+时间+随机数进行生成的128位的二进制,转换为16进制字符串后,长度为32位。

uuid的生成非常简单, 如果用java代码生成,那只需要一行代码


public static void main(String[] args) {      
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println(uuid); }

优点: uuid生成的ID 能满足唯一性;并且不依赖任何中间件;生成速度超快。


但是uuid的缺点也很明显: 生成的ID太长了,字符长度有32位

在业务系统里,一般ID是要进行存储到库里的,并且很有可能会作为常用的查询条件,比如存储到mysql,比int32的4字节或者int64的8字节来讲,太占空间;


第二个缺点:就是生成的ID是无序的,不是递增的;这样在把ID做为索引字段,在底层插入索引时,还会引起索引节点的分裂,影响插入速度。 所以用UUID生成的ID,对数据库来说很不友好


那么UUID是不是就没有业务使用场景了?

并不是的,像很多大厂的日志链路追踪的ID,就是基于UUID进行生成的。


redis 自增模式


大家都知道 在redis里有一个INCR命令,能从0到Long.maxValue-1进行单一连续的自增,最大19位;9位数字就是亿基本的数了,还剩10位,假设每天1千W的订单数据,即1千W个ID,能支持25亿年;距离地球毁灭时间还差点


redis id的自增代码也很简单,不详述。


优点:

redis 自增生成的ID 能满足唯一性也能满足单调自增性。做底层数据存储或者索引的时候,对数据库也很友好。生成速度还飞快,因为都是在内存中生成。


缺点:

redis 自增ID的缺点也很明显可能会丢数据
我们知道redis是一个kv内存数据库,虽提供了异步复制数据+哨兵模式 来保证服务的高可用+数据高可靠;但redis不保证不丢数据,即使从库变为主库,但也不保证之前的从库数据是最新的,毕竟是异步同步数据。


建议中大厂,或者能稳定运维redis和具有高可用方案+失败重试方案的厂子使用;小厂慎用吧。


数据库自增模式


在早些年,确实看到过依靠数据库自增的方式,进行唯一ID的生成。此种方式主要是依赖数据库的自增来实现;想要一个唯一的ID,那么往带有自增主键的表里插入一条数据吧,这样sql返回的时候,就会把自增ID返回来了


具体sql


INSERT INTO your_table (column1, column2) VALUES ('value1', 'value2'); SELECT LAST_INSERT_ID();

优点:
数据库自增生成的ID 能满足唯一性也基本能满足自增性


缺点:

生成速度较慢

受限于数据库的实现方式,,ID生成速度相较于前两者至少慢一个数量级,毕竟在生成的时候数据库有磁盘IO操作;高并发下,不建议使用此种方式。而且每次要获得一个ID,还需要往数据库里先插入一条记录。本来高并发下,数据库操作就是个瓶颈了,用了此种方式还加剧了数据库的负担。


数据库号段模式


数据库号段模式,可以看做是数据库自增模式的升级版,在数据库自增模式上进行了性能优化,解决了ID生成慢的问题。
思路入下图:


1698825866585.png


数据库自增模式,每次获取一个ID,都需要操作一次库,太慢了。
号段模式每次操作数据库是申请一个号段的范围。
比如操作一次数据库,申请1000到2000是这个应用的业务申请的号段;然后只能这个应用实例的业务能用;应用申请了号段后放到内存中,每次业务需要的时候,从内存里累加1返回,在java里也有现成的工具,比如AtomicLong包装下(省事还能解决并发问题);如果发现快不够了,还能异步提前从数据库中申请,放入内存中,这样就避免了业务需要唯一ID的时候,在去数据库申请,加快业务获取ID的速度。


优点:

能满足唯一性,也能满足自增性,性能也不差,美团开源的leaf使用此种模式,速度能到 5W/秒。


缺点:

对数据库是强依赖;但我想大多数业务系统基本都依赖数据库吧


个人认为此方案最适合小厂;性能不差,而且也不需要依赖太多的中间件;而且大厂里也不缺乏使用此方案身影,比如滴滴开源的Tinyid也是用的此模式


雪花算法模式


雪花算法(Snowflake)出自大名鼎鼎的twitter公司之手;是的就是那个被硅谷钢铁侠 马斯克收购的公司。
该算法开源后,深受国内大厂好评。并且像百度这种大厂,基于雪花算法思想,开发了自己开源的
UidGenerator 分布式ID生成器。


1698825928764.png


Snowflake生成的是Long类型的ID,我们知道一个Long类型占8个字节空间,1个字节又占8比特,也就是说一个Long类型会用88 64个比特。


Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特。



  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。

  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年 (如果你建设的IT系统要使用100年,慎用此法)

  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。

  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID


优点:

* 雪花算法生成的ID,能保证唯一性;

* 随这时间齿轮的流转,也能满足自增性;有大厂的背书,生成速度也是超快;

* 对第三方中间件也是弱依赖


缺点:



  • 存在时钟回拨问题,所有机器时钟必须保存同步,否则会导致生成重复ID;但是也能解决

  • 生成的数字会随这时间的流逝增大,不会因为数据量的增大而增大,在某些业务场景不太适用

  • 需要维护机器ID的配置;或者依赖第三方中间件根据机器信息,生成机器ID


这么多的分布式唯一id生成方法,我该选择哪一种?


为了方便选择,列了个表。到底我的应用里,该选哪个方案了?还需结合自身业务,团队大小,技术实力,实现复杂度和维护难易度进行选择。


image.png


想偷懒,有现成的唯一ID生成工具吗?


有的,像比如美团开源的leaf 能支持数据库号段和雪花模式,传输门:github.com/Meituan-Dia…


百度的 UidGenerator 使用的是雪花模式,传输门:github.com/baidu/uid-g…


而滴滴的 Tinyid 使用的是号段模式,可以认为是基于美团leaf的开源版,传输门:github.com/didi/tinyid…


请注意:以上三个工具都是用java语言写的,用java语言的同学有福了,基本是开箱即用。


集成美团leaf 号段模式


1、从git上 github.com/Meituan-Dia… 下载 leaf源码


1698826132995.png


2、mvn clean install -DskipTests 编译打包工程


3、用工程里的script脚本目录下的sql 脚本,创建数据表


CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '',
`max_id` bigint(20) NOT NULL DEFAULT '1',
`step` int(11) NOT NULL,
`description` varchar(256) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

insert int0 leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')


4、在leaf-server工程里配置数据库连接信息


1698826171611.png


5、启动leaf-server工程,访问
浏览器访问 http://localhost:8080/api/segment/get/leaf-segment-test即可。


注意:这里的leaf-segment-test即时bizTag。如果想要换成其它业务标,往leaf_alloc插入一条数据即可。


不想用leaf的web模式;但又想用号段模式+本地api方式直接调用,是否可行?


可行的。只需要引入美团的leaf-core jar包即可。是的,这么好的工具,居然没有maven信息;不死心的我,去maven官方仓库搜了一把,还真发现了leaf-core的坐标。
如下:


<dependency>
<groupId>com.tencent.devops.leafgroupId>
<artifactId>leaf-coreartifactId>
<version>1.0.2-RELEASEversion>
dependency>

但是下载下来后,包名居然是腾讯的包名。反编译后,发现代码基本上没怎么变。如果想偷懒,可直接下载。


为了保险起间,我还是用了美团的leaf-core(只好做本地jar依赖)。
然后在代码里加入对ID生成器的初始化。代码如下:



@Service
public class LeafIdDBSegmentImpl implements InitializingBean {
IDGen idGen;
DruidDataSource dataSource;

@Override
public void afterPropertiesSet() throws Exception {
Properties properties = PropertyFactory.getProperties();

// Config dataSource
dataSource = new DruidDataSource();
dataSource.setUrl(properties.getProperty("leaf.jdbc.url"));
dataSource.setUsername(properties.getProperty("leaf.jdbc.username"));
dataSource.setPassword(properties.getProperty("leaf.jdbc.password"));
dataSource.init();

// Config Dao
IDAllocDao dao = new IDAllocDaoImpl(dataSource);

// Config ID Gen
idGen = new SegmentIDGenImpl();
((SegmentIDGenImpl) idGen).setDao(dao);
idGen.init();
}

public Long getId(String key){
Result result = idGen.get(key);
if (result.getStatus().equals(Status.EXCEPTION)) {
throw BizException.createBizException(BizErrorCode.LEAF_CREATE_ERROR_REQUIRED);
}
return result.getId();
}

}


该代码主要做了几件事:

1、在该单列类,实例化后,会进行 数据库源的初始化工作,并且自行实例化IDAllocDao;当然你可以把工程里已有的数据源赋给IDAllocDaoImpl

2、获取ID时,调用底层idGen.get获取

3、业务代码使用时 用 LeafIdDBSegmentImpl.get方法 传入数据库里事先配置好的bizTag即可。


作者:程序员猪佩琪
来源:juejin.cn/post/7296089060834312207
收起阅读 »

一文告诉你,如何实现 IP 属地功能

细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。 作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定...
继续阅读 »

细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。



作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定要来看一下这个功能是怎么实现的,今天这篇文章,就用几分钟的时间来讲述一下这个功能是怎么实现的。



获取用户 IP 地址


HttpServletRequest 获取 IP




首先我们来看一下,在 Java 中,是如何获取到 IP 属地的,主要有以下两步:



  1. 通过 HttpServletRequest 对象,获取用户的 「IP」 地址

  2. 通过 IP 地址,获取对应的省份、城市]


首先,我们这里写一个工具类用于获取 IP 地址,因为用户的每次 Request 请求都会携带请求的 IP 地址放到请求头中,所以我们可以通过截取请求中的 IP 来获取 IP 地址;


/**
* 网络工具类
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
public class NetUtils {

/**
* 获取客户端 IP 地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1")) {
// 根据网卡取本机配置的 IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
if (inet != null) {
ip = inet.getHostAddress();
}
}
}
// 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
// 本机访问
if ("localhost".equalsIgnoreCase(ip) || "127.0.0.1".equalsIgnoreCase(ip) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(ip)){
// 根据网卡取本机配置的IP
InetAddress inet;
try {
inet = InetAddress.getLocalHost();
ip = inet.getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
// 如果查找不到 IP,可以返回 127.0.0.1,可以做一定的处理,但是这里不考虑
// if (ip == null) {
// return "127.0.0.1";
// }
return ip;
}

/**
* 获取mac地址
*/
public static String getMacAddress() throws Exception {
// 取mac地址
byte[] macAddressBytes = NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress();
// 下面代码是把mac地址拼装成String
StringBuilder sb = new StringBuilder();
for (int i = 0; i < macAddressBytes.length; i++) {
if (i != 0) {
sb.append("-");
}
// mac[i] & 0xFF 是为了把byte转化为正整数
String s = Integer.toHexString(macAddressBytes[i] & 0xFF);
sb.append(s.length() == 1 ? 0 + s : s);
}
return sb.toString().trim().toUpperCase();
}
}

获取用户的 IP 地址属地


淘宝库获取用户 IP 地址属地




通过这个方法,就可以重请求头中获取到用户的 IP 地址了,然后接下来就是 IP 地址归属地省份、城市的获取了,这里可以用很多 IP 地址查询的库进行查询,这里用一个库来测试一下。


淘宝 IP 地址库:ip.taobao.com/



不过淘宝的 IP 地址查询库已经在 2022 年 3 月 31 日下线了,这里我们就不能使用它了,只能另辟蹊径了。



这里我们截取一段之前淘宝货期 IP 地址的源码,然后一起来看一下。



这里可以看到,在日志文件中,出现了大量的 the request over max qps for user 问题。


虽然这个方法已经寄了,但是我们求知的道路可以停吗?肯定不可以啊,这里我们就来整一个新的获取 IP 地址属地的方法,也就是我们今天文章的主角:Ip2region。


Ip2region 介绍




这个是在之前的一篇文章看到的,他是一个 Gthub 的开源项目,即 Ip2region 开源项目


地址如下:github.com/lionsoul201…


这个开源库目前已经更新到了 V2 的版本,现在的它是一个强大的离线IP地址定位库和IP定位数据管理框架,其达到了微秒级别的查询效率,还提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,可以说是非常得好用👍👍👍👍,今天这篇文章我们主要针对其 V2 版本进行讲解,如果想要查询 1.0 版本的内容的话,可以去 Github 上面进行查看。


Ip2region 详解


高达 99.9 % 的查询准确率




数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次),比例如下:





  1. 80%, 淘宝 IP 地址库, ip.taobao.com/%5C




  2. ≈10%, GeoIP, geoip.com/%5C

  3. ≈2%, 纯真 IP 库, http://www.cz88.net/%5C


Ip2region V2.0 特性




1、IP 数据管理框架


xdb 支持亿级别的 IP 数据段行数,默认的 region 信息都固定了格式:国家|区域|省份|城市|ISP,缺省的地域信息默认是0。 region 信息支持完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


2、数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


3、极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


多语言以及查询客户端的支持


已经客户端有:Java、C#、php、C、Python、Node.js、PHP 拓展(PHP 5 和 PHP 7)等,主要如下:


binding描述开发状态binary查询耗时b-tree查询耗时memory查询耗时
cANSC c binding已完成0.0x毫秒0.0x毫秒0.00x毫秒
c#c# binding已完成0.x毫秒0.x毫秒0.1x毫秒
Golanggolang binding已完成0.x毫秒0.x毫秒0.1x毫秒
Javajava binding已完成0.x毫秒0.x毫秒0.1x毫秒
Lualua实现 binding已完成0.x毫秒0.x毫秒0.x毫秒
Lua_clua的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
nginxnginx的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
nodejsnodejs已完成0.x毫秒0.x毫秒0.1x毫秒
phpphp实现 binding已完成0.x毫秒0.1x毫秒0.1x毫秒
php5_extphp5的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
php7_extphp7的c扩展已完成0.0毫秒0.0x毫秒0.00x毫秒
pythonpython bindng已完成0.x毫秒0.x毫秒0.x毫秒
rustrust binding已完成0.x毫秒0.x毫秒0.x毫秒

Ip2region xdb Java 查询客户端实现




这里简单展示一下 Java 的实现,这里使用开发中常用的 Maven 实现的方式:


1. 引入 Maven 仓库


由于项目使用Spring 的方式构建,这里可以选择使用引入 Spring 的 starter 的方式进行


<dependency>
<groupId>com.github.hiwepy</groupId>
<artifactId>ip2region-spring-boot-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>

<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.7.0</version>
</dependency>

在引入 Maven 依赖之后,我们这里引入几种实现的方式:


2. 实现方式 1:【基于文件查询】


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
// 1、创建 searcher 对象
String dbPath = "ip2region.xdb file path";
Searcher searcher = null;
try {
searcher = Searcher.newWithFileOnly(dbPath);
} catch (IOException e) {
System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
return;
}

// 2、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 3、备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
}
}

3. 实现方式 2:【缓存VectorIndex索引】


我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";

// 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
byte[] vIndex;
try {
vIndex = Searcher.loadVectorIndexFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
return;
}

// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
} catch (Exception e) {
System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
return;
}

// 3、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
}
}

4. 实现方式 3:「缓存整个 xdb 数据」


我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";

// 1、从 dbPath 加载整个 xdb 到内存。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
return;
}

// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
System.out.printf("failed to create content cached searcher: %s\n", e);
return;
}

// 3、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
}
}

5. 编译测试程序


通过 maven 来编译测试程序。


# cd 到 java binding 的根目录
cd binding/java/
mvn compile package

然后会在当前目录的 target 目录下得到一个 ip2region-{version}.jar 的打包文件。


6. 查询测试



  • 可以通过 java -jar ip2region-{version}.jar search 命令来测试查询:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search
java -jar ip2region-{version}.jar search [command options]
options:
--db string ip2region binary xdb file path
--cache-policy string cache policy: file/vectorIndex/content


  • 例如:使用默认的 data/ip2region.xdb 文件进行查询测试:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search --db=../../data/ip2region.xdb
ip2region xdb searcher test program, cachePolicy: vectorIndex
type 'quit' to exit
ip2region>> 1.2.3.4
{region: 美国|0|华盛顿|0|谷歌, ioCount: 7, took: 82 μs}
ip2region>>

输入 ip 即可进行查询测试,也可以分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。


bench 测试


可以通过 java -jar ip2region-{version}.jar bench 命令来进行 bench 测试,一方面确保 xdb 文件没有错误,一方面可以评估查询性能:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench
java -jar ip2region-{version}.jar bench [command options]
options:
--db string ip2region binary xdb file path
--src string source ip text file path
--cache-policy string cache policy: file/vectorIndex/content

例如:通过默认的 data/ip2region.xdb 和 data/ip.merge.txt 文件进行 bench 测试:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench --db=../../data/ip2region.xdb --src=../../data/ip.merge.txt
Bench finished, {cachePolicy: vectorIndex, total: 3417955, took: 8s, cost: 2 μs/op}

可以通过分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。 @Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。


作者:夭要7夜宵
来源:juejin.cn/post/7295576148364296231
收起阅读 »

为什么堂堂微信数据库表名、字段名起的如此随意?

1.微信数据库解密 微信数据库在在哪个文件夹 EnMicroMsg.db的父文件加密规则是md5("mm" + uin)这样就可以准确的获取到db文件的位置. uin的获取:/data/data/com.tencent.mm/shared_prefs/au...
继续阅读 »

1.微信数据库解密




  • 微信数据库在在哪个文件夹


    EnMicroMsg.db的父文件加密规则是md5("mm" + uin)这样就可以准确的获取到db文件的位置.


    uin的获取:/data/data/com.tencent.mm/shared_prefs/auth_info_key_prefs.xml`里面有个uinz字段,直接获取value值,示例如下图所示:


    image-20210526135110550




  • 解密微信数据库:目前只涉及两个数据库EnMicroMsg.db(微信数据涉数据库)和WxFileIndex.db(文件索引数据库)


    解密的密码:md5(手机imei+uin)的32位字符串取前7位,如果imei拿不到就用1234567890ABCDEF代替




2. 好友相关信息


微信的好友相关数据涉及三张表:rcontact,bizinfo,img_flag




  • rcontact表存放了该账号涉及的所有账号(微信账号,群账号)的基本信息(eg:微信昵称,备注,原微信号,改之后的微信号,全拼等等)。如下图所示:


    111




  • bizinfo表存放的是该账号的好友微信号,群账号,这里好友包括已经通过的和添加没通过的,如下所示:


    22222




  • img_flag表存放该账号所有涉及的微信(好友,同属一个群不是好友,添加的陌生人)的头像地址,数据如下图所示:


    3333


    总结:rcontact表是一张基础表,存放所有的账号基本信息,bizinfo存放是该账号的好友信息或者群组信息,img_flag存放了微信账号对应的头像信息,以下场景有:




    • 获取微信好友信息,查询sql如下:


      select r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin,r.encryptUserName, i.reserved2 from rcontact r INNER JOIN img_flag i  on r.username = i.username where r.type&2=2  and r.username not like '%@chatroom' and i.lastupdatetime > 0



    • 获取添加未通过的好友信息,此时有两种情况:1)添加同属一个群的好友。2)添加陌生人。比如说通过微信号,扫码什么。这两种情况在数据库的表现形式是不一样的,添加同属一个群的,在bizinfo表会插入一条username为添加好友的微信号记录,而如果是添加陌生人,则username是一个以@stranger结尾的key,对应的数据如下图所示:


      4444


      注意:这里如果通过微信号,扫码添加的陌生人,其username是一长串的以@stranger结尾的key,同 时pyInitial,qunPin两个字段存的并不是这个陌生人的微信号


      查询sql如下:


      SELECT r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin, r.encryptUserName, i.reserved2 FROM rcontact r INNER JOIN bizinfo b ON r.username = b.username INNER JOIN img_flag i ON r.username = i.username 
      WHERE r.type <> 33 and r.type & 2 <> 2 AND r.username <> '当前微信号' AND r.username NOT LIKE '%@chatroom' AND b.updateTime > 0



    • 获取同属一个群但不是好友的基本信息:


      查询sql如下:


      SELECT DISTINCT r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin, i.reserved2 FROM rcontact r  INNER JOIN img_flag i ON r.username = i.username 
      WHERE r.username not in(select username from bizinfo) and i.lastupdatetime >0

    3.微信群组


    微信群组信息表为chatroom,存放着一些基本信息,数据如下图所示:


    555


    注意:微信群组一开始建立显示群昵称是所有好友微信昵称加起来的一个字符串,即displayname字段,但是如果修改了群昵称之后,显示的是修改之后的,这时候需要根据根据群账号chatroomname去rcontact表做关联查询,根据rcontact表的username等于群账号查询出一条记录,此时这条记录的字段nickname即修改后的群昵称,查询sql如下:


    select c.chatroomname, c.memberlist, c.displayname, c.roomowner, c.selfDisplayName, r.nickname from chatroom c inner join rcontact r on r.username = c.chatroomname where c.modifytime > 0


    目前的微信群组的头像在img_flag表没有存储,暂时找不到资源所在


    4.微信聊天数据


    微信的聊天记录是保存在message表中的,数据示例如下图:


    666


    msgSvrId:唯一标示一条聊天记录的id,可以作为更新聊天记录数据的条件


    createTime:发送消息的时间


    talker:如果是群账号,说明这条消息是群会话消息,发送人在content内容前面(发送人微信号:发送内容);如果是好友微信号,说明这条消息是好友会话消息


    isSend:发送或者接收标志。0:接收 1:发送


    type:消息类型 1:文本内容(包括小表情) 3:图片 34:语音 43:视频 47:大表情 49:文件


    436207665:微信红包 419430449:微信转账


    图片,视频,语音, 文件 根据msgId去索引库WxFileIndex的表WxFileIndex2查询




    • 图片查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=3 and msgSubType20



    • 语音查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=34



    • 视频查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=43 and msgSubType=1



    • 文件查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=49 and msgSubType = 34



    • 大表情查询sql:根据groupId去找到对应的包名,md5即表情的文件名


      select e.md5, e.groupid, m.msgSvrId from emojiinfo e INNER JOIN message m on e.md5=m.imgpath where m.type=47



    5.总结


    以上分析师基于Android系统端的微信,且微信数据的撤销删除仍需要研究,待补充,未完待续.......




作者:shepherd111
来源:juejin.cn/post/7295160228879122458
收起阅读 »

适合小公司的自动化部署脚本

背景(偷懒) 在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力。 每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个2...
继续阅读 »

背景(偷懒)


在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力


每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个200多M的jar包;以龟速般的每秒几十KB网络,通过ftp上传到服务器;用烂熟透的jps命令查找到进程,kill后,重启服务。


是的,我想偷懒,想从已陷入到手工部署的沼泽地里走出来。如何救赎?


自我救赎之路


我的诉求很简单,想要一款“一键CI/CD的工具”,然后可以继续偷懒。为了省事,我做了以下工作


找了一款停止服务的脚本,并做了小小的优化


首推 陈皮大哥的停服脚本(我在里面加了个sleep 5);脚本见下文。只需要修改 APP_MAINCLASS的变量“XXX-1.0.0.jar”替换为自己jar的名字即可,其它不用动


该脚本主要是通过jps + jar的名字获得进程号,进行kill。( 脚本很简单,注释也很详细,就不展开了,感兴趣可以阅读下,不到5分钟,写过代码的你能看懂的)


把以下脚本保存为stop.sh


#!/bin/bash
# 主类
APP_MAINCLASS="XXX-1.0.0.jar"
# 进程ID
psid=0
# 记录尝试次数
num=0
# 获取进程ID,如果进程不存在则返回0,
# 当然你也可以在启动进程的时候将进程ID写到一个文件中,
# 然后使用的使用读取这个文件即可获取到进程ID
getpid() {
javaps=`jps -l | grep $APP_MAINCLASS`
if [ -n "$javaps" ]; then
psid=`echo $javaps | awk '{print $1}'`
else
psid=0
fi
}
stop() {
getpid
num=`expr $num + 1`
if [ $psid -ne 0 ]; then
# 重试次数小于3次则继续尝试停止服务
if [ "$num" -le 3 ];then
echo "attempt to kill... num:$num"
kill $psid
sleep 5
else
# 重试次数大于3次,则强制停止
echo "force kill..."
kill -9 $psid
fi
# 检查上述命令执行是否成功
if [ $? -eq 0 ]; then
echo "Shutdown success..."
else
echo "Shutdown failed..."
fi
# 重新获取进程ID,如果还存在则重试停止
getpid
if [ $psid -ne 0 ]; then
echo "getpid... num:$psid"
stop
fi
else
echo "App is not running"
fi
}
stop

编写2行的shell 启动脚本


修改脚本中的XXX-1.0.0.jar为你自己的jar名称即可。保存脚本内容为start.sh。jvm参数可自行修改


basepath=$(cd `dirname $0`; pwd)
nohup java -server -Xmx2g -Xms2g -Xmn1024m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -Xloggc:logs/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:HeapDumpPath=logs/dump.hprof -XX:ParallelGCThreads=4 -jar $basepath/XXX-1.0.0.jar &>nohup.log &

复用之前jenkins,自己写部署脚本


脚本一定要放到 Post Steps里


1689757456174.png


9行脚本,主要干了几件事:



  • 备份正在运行的jar包;(万一有啥情况,还可以快速回滚)

  • 把jenkins上打好的包,复制到目标服务上

  • 执行停服脚本

  • 执行启动服务脚本


脚本见下文:


ssh -Tq $IP << EOF 
source /etc/profile
#进入应用部署目录
cd /data/app/test
##备份时间戳
DATE=`date +%Y-%m-%d_%H-%M-%S`
#删除备份jar包
rm -rf /data/app/test/xxx-1.0.0.jar.bak*
#备份历史jar包
mv /data/app/test/xxx-1.0.0.jar /data/app/test/xxx-1.0.0.jar.bak$DATE
#从jenkins上拉取最新jar包
scp root@$jenkisIP:/data/jenkins/workspace/test/target/XXX-1.0.0.jar /data/app/test
# 执行停止应用脚本
sh /data/app/test/stop.sh
#执行重启脚本
sh /data/app/test/start.sh
exit
EOF

注:



  • $IP 是部署服务器ip,$jenkisIP 是jenkins所在的服务器ip。 在部署前请设置jenkins服务器和部署服务器之间ssh免密登录

  • /data/app/test 是部署jar包存放路径

  • stop.sh 是上文的停止脚本

  • start.sh 是上文的启动脚本


总结


如果不想把时间浪费在本地打包,忍受不了上传jar包的龟速网络,人肉停服和启动服务。请尝试下这款自动部署化脚本。小小的投入,带来大大的回报。


原创不易,请 点赞,留言,关注,转载 4暴击^^


参考资料:

xie.infoq.cn/article/52c…
Linux ----如何使用 kill 命令优雅停止 Java 服务

blog.csdn.net/m0_46897923… --ssh免密登录


作者:程序员猪佩琪
来源:juejin.cn/post/7257440759569055802
收起阅读 »

程序员必看的几大定律,你中招了吗?

一 晕轮效应 我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。 举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好! 当你看到一位发量稀疏的开发老哥,你通常会觉得这位...
继续阅读 »

一 晕轮效应



我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。



举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好!


当你看到一位发量稀疏的开发老哥,你通常会觉得这位老哥技术能力肯定非常强,做人肯定也很靠谱!



在晕轮效应影响下,一个人或事物的优点或缺点一旦变为光圈被扩大,其缺点或优点也就隐退到光圈的背后,被别人视而不见了。



对于程序员来说,有两点可以考虑:




  1. 打造自己的光晕:让自己成为专家或者像个专家,可以提升自己的话语权。


    比如在掘金写文章,当你拥有几千粉丝,几十万阅读量的时候,即使你什么都不说,别人看到这个账号都会觉得你是个厉害人物,与你交流的时候都会注意几分。




  2. 突破别人的光晕:在我使用npm上各种组件或者工具的时候,常常会感慨,好厉害!这么多star的项目,肯定没有Bug吧,如果有,那一定是我用的方式不对!打开源码看的时候更是惊呼,好厉害,完全看不懂,也不可能看的懂吧?


    但其实,褪去这些光环,你会发现,即使是成熟的项目,也会有bug,高深的代码,也都只是一个个基础的语句组合起来的,当你理解了作者的思想,也就能理解代码。




二 眼不见为净定律



看不见的,就是干净的。



看到这个定律,我的第一反应就是,屎山代码为什么会存在?


还不是因为管理人员看不到这坨屎山,他看到的是一个功能正常运行的系统,所以人家并不觉得这是屎山,而是美丽的风景线!


只有我们这些天天在这座屎山上添砖加瓦的程序员才能会感受到这种绝望!


所以面对屎山代码,不要抱怨,最好的方法就是找个机会把这座屎山丢给其他人,毕竟眼不见为净嘛!


当它不在你手上的时候,你会发现其实它也挺好的,毕竟眼不见为净嘛!


三 虚假同感偏差


你们是否会遇到这种情况:明明一件很重要的事情,催了某个人很久了,他却迟迟未做!


这里就涉及到虚假同感偏差,因为这件事对你来说很重要,所以通常会自我推断,觉得别人也会认为这件事情很重要,然而事实上,对你很重要的事,对他人来说可能回过头就给忘记了!


所以啊,要让别人重视一件你觉得很重要的事情,就是也让他感觉到重要,这样别人就不敢忘记了,比如可以补充一句:某某领导正在关注这件事,麻烦尽快,谢谢!


另外就是当我们非常确信自己观点或意见的时候,也很容易产生虚假同感偏差,这时候如果有人提出不同的观点,我们会下意识的反驳,并且觉得问题来自于他人。


比如我们自信满满地写完一段代码并且自测之后,提交给测试人员进行测试,当测试人员跟你反馈存在某BUG,我相信第一时间反应大多都是:我不信!!!


然后就有以下对话:



你:可能前端有缓存,你刷新一下再试试?


测试:行,我试一下。


过了十分钟......


测试:还是一样的问题啊,你看一下。


你:是不是测试数据有问题啊,我自己都测试过了,不应该有问题!


测试:行吧,我再看看。


过了十分钟......


测试:数据都排查过了,是正常的,你检查一下吧!


你:(还想再挣扎一下)你怎么操作的?


测试:就点击一下这个按钮,我还能玩出什么花吗?


排查了一会,哦~居然是空值的情况没有判断,我还能再白痴一点吗!


你:问题已经修复了,是某某复杂的场景我没考虑清楚,你再测试一下!



四 自我宽恕定律



人性有个根深蒂固的特点,就是容易发现别人的缺点和错误,却不容易看到自己的不足。


正所谓,见人之过易,见己之过难。





  • 当看到别人的代码存在一个空指针异常,心里想:这都能忘记判断,其他代码会不会也有问题!




  • 当发现自己的代码存在一个空指针异常,心里想:只是不小心忘记判断了嘛,加一下就好了!




  • 当接手别人项目的时候:卧槽,这代码写的啥啊,注释没几句,变量命名这么奇葩,逻辑这么混乱,我要咋接啊!!




  • 当项目给别人接手时候:我这代码虽然注释不多,但是很规范的,你看这变量命名不就能知道是什么含义了嘛,逻辑也非常顺,这个方法几百行你按顺序看下来就行了,我都给你写在一起,不需要跳来跳去地看,多方便!




五 补偿作用:



弱点也是一种力量源!



大家应该都听说过这个现象:瞎子的眼睛虽然看不见了,听力通常会变得非常灵敏!


这种生理上的现象吸引了很多有兴趣的心理学家,所谓补偿,就是发挥一个人的最大优势,激发其自信心,抵消其弱点。


看到补偿机制,我第一想到的就是在掘金看到的各种专科大佬。


虽然学历起点比其他人低一些,但有时候正是因为学历劣势,更加激发他们深耕技术的决心,反而达到其他更高学历人员都无法达到的高度。


这又让我想起了一句话:打不倒我的,会让我更强大!!


六 皮尔斯定理



意识到无知,才是知道的开始。



还有一句话,我觉得很适合接在这句话后面:知道的越多,才发现自己不懂的越多!


于是就形成了一个闭环: 意识到无知->开始知道->知道的越多->意识到无知


这句话我相信大部分人都听过很多遍了,不知道你们是从什么时候开始意识到自己的无知呢?


曾经,我还是小白的时候,在福州某公司上班,每天做的事情就是SpringBoot接口的开发,或者修改某些业务逻辑,我以为这差不多就是开发的全部了。


那时候对接的前端是使用Vue写的,我甚至不知道什么是Vue,只知道是某个挺流行的前端技术。


每次部署,我看前端文件里就只有一个index.html文件,我真的非常奇怪,为什么这么大的项目,只有一个html文件?


那时候我对前端认知还停留在html+js+css+jquery的时代,所以完全想不通。


本来还觉得自己前端也是有点基础的,直到接触Vue,我才惊呼,卧槽,前端怎么变成这样子了?什么nodejs,什么npm完全没听说过。


用过一段时间之后,我更是惊呼,卧槽,前端还能这样子?明明我HTML+CSS+JS只懂一点,都能做出这么好看的页面了。


有了各种开源前端组件,即使对原生HTML标签和CSS不太懂,也能算是个还不错的前端开发了。


还有这ES6语法用起来也太爽了吧,比JAVA可自由太多了。


所以很感慨,当我没进入前端圈子的时候,还以为自己懂一些,进入之后,才发现自己真的是一窍不通,啥都要学。


更感慨的是,当我第一次接触掘金,我惊呼,卧槽,这个社区分享的东西都好干啊,好多听都没听过的标题,原来我有这么多东西都不懂!原来前端是个这么卷的领域!


结语


感谢阅读,希望本篇文章对你有所帮助!


作者:林劭敏
来源:juejin.cn/post/7295623585363771443
收起阅读 »

如何优雅的判断一个对象是否为空?

我们在刚开始学习Java的时候,遇到过最多的异常肯定是臭名昭著的空指针异常(NullPointerException),可以说它陪伴了我们整个初学阶段。字符串、对象、集合等等一不留神就容易出现空指针异常! 那么如何优雅的判断一个对象是否为空并且减少空指针异常呢...
继续阅读 »

我们在刚开始学习Java的时候,遇到过最多的异常肯定是臭名昭著的空指针异常(NullPointerException),可以说它陪伴了我们整个初学阶段。字符串、对象、集合等等一不留神就容易出现空指针异常!


那么如何优雅的判断一个对象是否为空并且减少空指针异常呢?


今天来介绍一个容器类——Optional


Optional介绍


Optional是一个容器类,它只有两种情况:



  • 要么包含一个非空对象

  • 要么为空


它有助于编写更健壮的代码,以处理可能为空的值,而不必担心空指针异常!


Optional用法


Optional的创建


Optional有以下两种常见的创建方法:



  • Optional.of(T value):创建一个包含非空值的Optional,如果value为null,则抛出NullPointerException

  • Optional.ofNullable(T value):创建一个Optional,允许value为null


判断Optional容器中是否包含对象


isPresent(): 返回一个布尔值,如果Optional容器中包含一个非空对象则返回true,否则返回false


获取Optional容器的对象



  • get(): 如果Optional包含非空值,返回该值;否则抛出NoSuchElementException

  • orElse(T other): 如果Optional为空,返回指定的默认值other

  • orElseGet(Supplier<? extends T> other): 如果Optional为空,使用提供的Supplier函数生成默认值

  • orElseThrow(Supplier<? extends X> exceptionSupplier): 如果Optional为空,抛出由提供的Supplier函数生成的异常


过滤


filter(Predicate<? super T> predicate): 如果Optional包含非空值且满足predicate条件,则返回当前Optional,否则返回一个空Optional。


映射



  • map(Function<? super T, ? extends U> mapper): 如果Optional包含非空值,应用mapper函数并返回新的Optional。

  • flatMap(Function<? super T, Optional> mapper): 类似于map,但允许mapper函数返回Optional。


Optional示例


假如我们有一个User类,可以使用Optional来处理可能为空的User对象。User类结构如下:


public class User {
private String name;

public User(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

示例:创建Optional



Optional userOptional = Optional.ofNullable(new User("张三"));



示例:判断Optional是否包含对象


if (userOptional.isPresent()) {
System.out.println("用户存在:" + userOptional.get().getName());
} else {
System.out.println("用户不存在");
}

示例:获取Optional容器的对象


User user = userOptional.orElse(new User("李四"));
System.out.println("User: " + user.getName());

示例:过滤


Optional<User> filteredUserOptional = userOptional.filter(u -> u.getName().startsWith("张"));
if (filteredUserOptional.isPresent()) {
System.out.println("结果:" + filteredUserOptional.get().getName());
} else {
System.out.println("未找到对应用户");
}

示例:映射


Optional<String> userNameOptional = userOptional.map(User::getName);
userNameOptional.ifPresent(name -> System.out.println("用户名为: " + name));

使用场景总结



  • 当你从某个方法返回一个值,但该值可能为空,而调用者需要明确知道值是否存在。

  • 在处理方法参数时,你可以用Optional来表示某个参数可以为空,以提醒调用者可能会传入null。

  • 避免繁琐的null检查和条件语句,使代码更简洁和可读!



更多文章干货,推荐公众号【程序员老J】



作者:程序员老J
来源:juejin.cn/post/7298142364194979852
收起阅读 »

了不起的Base64

web
不要乱说话。话说出去之前我们还是话的主人,话说出去之后我们就成了话的奴隶。 大家好,我是柒八九。 前言 在我们项目开发中,Base64想必大家都不会很陌生,Base64是将二进制数据转换为文本的一种优雅方式,使存储和传输变得容易。但是,作为一个合格的程序员,...
继续阅读 »

不要乱说话。话说出去之前我们还是话的主人,话说出去之后我们就成了话的奴隶。



大家好,我是柒八九


前言


在我们项目开发中,Base64想必大家都不会很陌生,Base64是将二进制数据转换为文本的一种优雅方式,使存储和传输变得容易。但是,作为一个合格的程序员,我们应该有一种打破砂锅问到底的求助欲望。


所以,今天我们来讲讲在各种语言中出镜率都高的离谱的Base64算法。今天,我们就用我们在初高中语文老师教我们的描述一个事物的三大步骤:1. 是什么,2. 如何工作,3. 为什么它很重要。来讲讲Base64算法。


好了,天不早了,干点正事哇。



我们能所学到的知识点




  1. 前置知识点

  2. 为什么会出现 Base64 编码

  3. 什么是 Base64 编码?

  4. Base64 使用案例

  5. Base64 编码算法

  6. 如何进行 Base64 编码和解码





1. 前置知识点



前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用



RFC



RFC,全称为Request for Comments,是一种用于定义互联网标准和协议的文件系列。



RFC最早由互联网工程任务组(IETF)创建,用于记录和传播互联网协议、方法和最佳实践的提案、规范和讨论。


每个 RFC 都有一个唯一的编号,通常以RFC开头,后面跟着一个数字,例如RFC 791RFC 2616等。RFC文档通常包含了协议规范、技术说明、最佳实践、标准化提案等,以促进互联网技术的发展和互操作性。


我们可以在IETF-datatracker中输入指定的编号或者查找的关键字进行搜寻。



以下是一些常见的RFC文档,大家可以翻阅自己想了解的技术点:




  1. RFC 791 - Internet Protocol (IP): 定义了 IPv4,是互联网上最基本的协议之一。




  2. RFC 793 - Transmission Control Protocol (TCP): 定义了 TCP,一种重要的传输协议,用于可靠的数据传输。




  3. RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1: 定义了 HTTP 协议,用于在 Web 上传输超文本的基础通信协议。




  4. RFC 2326 - Real Time Streaming Protocol (RTSP): RTSP 用于流媒体传输,如音频和视频流的控制。




  5. RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2: 定义了 TLS 1.2,用于安全地传输数据,如 HTTPS 协议中使用的加密通信。




  6. RFC 4648 - 这是咱们今天的主角,Base64的相关内容









Latin-1 字符集


Latin-1,也称为ISO-8859-1,是一种由国际标准化组织(ISO)认可的8 位字符集,代表了西欧语言的字母表。正如其名称所示,它是ISO-8859的一个子集,该标准还包括用于写作系统如西里尔文、希伯来文和阿拉伯文的其他相关字符集。它被大多数Unix系统以及Windows系统使用。



Latin-1有时被不太准确地称为扩展 ASCII



这是因为其字符集的前 128 个字符与美国 ASCII 标准相同。其余字符集包含了带重音的字符和符号。


关于更详细的Latin-1的表格,可以参考Latin-1-table




btoa


btoaJavaScript 中的一个内置函数,用于将二进制数据(通常是 8 位字节)编码为 Base64 字符串。它的名称是 binary to ASCII 的缩写,用于将二进制数据转换为文本字符串,以便在文本协议中传输或存储。


用法:


btoa 函数接受一个字符串参数,该字符串包含二进制数据。它将该二进制数据转换为 Base64 编码的字符串。


const binaryData = "front789";
const base64String = btoa(binaryData);
console.log(base64String);

这段代码将 front789 这个字符串转换为 Base64 编码的字符串并将结果打印到控制台。


限制:


尽管 btoa 是一个有用的函数,但它有一些限制:




  1. 只能编码字符串: btoa 函数只接受字符串作为参数,而不接受其他类型的数据(如二进制数组)。如果需要编码二进制数据,需要先将其转换为字符串。




  2. 字符集限制: btoa 函数仅支持 Latin-1 字符集,这意味着它只能编码包含在 Latin-1 字符集内的字符。如果字符串包含超出 Latin-1 字符集的字符,那么会导致编码失败。




  3. 不适合加密:Base64 编码不是加密,它只是一种编码方式,不提供安全性。如果需要加密数据,应该使用专门的加密算法而不是仅仅进行 Base64 编码。




  4. 数据大小增加: Base64 编码会增加数据大小。通常情况下,Base64 编码后的数据会比原始二进制数据更大,这可能会对数据传输和存储造成额外开销。




Data URL


Data URL 是一种统一资源标识符(URI)方案,用于将数据嵌入到文档中,而不是从外部文件加载数据。Data URL 允许我们将数据(如文本、图像、音频等)直接包含在网页或文档中,而不需要额外的 HTTP 请求。这种方式对于小型资源或需要避免外部请求的情况非常有用。


Data URL 的基本结构如下:


data:[<mediatype>][;base64],<data>

其中:



  • <mediatype> 是可选的媒体类型(例如,text/plainimage/png),用于描述数据的类型。如果被省略,则默认值为 text/plain;charset=US-ASCII

  • ;base64 是可选的,表示数据以 Base64 编码方式包含。如果省略了 ;base64,则数据将以纯文本方式包含。

  • <data> 包含实际的数据,可以是文本或二进制数据。


以下是 Data URL 的一些常见用途和示例:




  1. 嵌入图像: Data URL 可用于将图像直接嵌入 HTMLCSS 中,而不需要外部图像文件。例如,将一张 PNG 图像嵌入 HTML 中:


    <img
    src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg=="
    alt="Embedded Image"
    />




  2. 内联 CSS: Data URL 可用于内联 CSS 样式表,以减少外部 CSS 文件的请求。例如,将 CSS 样式表嵌入 HTML 中:


    <style>
    body {
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg==);
    }
    </style>



  3. 嵌入字体: Data URL 可用于嵌入自定义字体,以确保字体在不同设备上显示一致。例如,嵌入一个字体文件:


    @font-face {
    font-family: "CustomFont";
    src: url(data:application/font-woff;base64,d09GRgABAAAA...) format("woff");
    }



  4. 内联脚本: Data URL 可用于内联小型 JavaScript 脚本,以减少外部脚本文件的请求。例如,内联一个简单的 JavaScript 函数:


    <script>
    let greeting = "前端柒八九";
    alert(greeting);
    </script>





2. 为什么会出现 Base64 编码


要理解为什么需要 Base64 编码,我们需要了解一些计算机历史。


计算机以二进制(01)进行通信,但人们通常希望使用更丰富的数据形式进行通信,如文本图像为了在计算机之间传输数据,首先必须将其编码为 0 和 1,然后再解码。以文本为例,有许多不同的编码方式。如果我们都能就一个单一的编码方式达成一致,那将会简单得多,但很遗憾,这并不是事实。针对这块的内容,可以参考了不起的 Unicode


最初创建了许多不同的编码方式(例如 Baudot 编码),每种方式使用不同数量的比特来表示一个字符,直到最终 ASCII 成为一个标准,每个字符使用 7 位。然而,大多数计算机将二进制数据存储为每个字节由 8 位组成的数据,因此 ASCII 不适合传输这种类型的数据。一些系统甚至会删除最高位。


为解决这些问题,引入了 Base64 编码。这允许我们将任意字节编码为已知不会损坏的字节(ASCII 字母数字字符和一些符号)。缺点是使用 Base64 对消息进行编码会增加其长度 - 每 3 个字节的数据编码为 4 个 ASCII 字符


要可靠地发送文本,我们可以首先使用自己选择的文本编码(例如 UTF-8)将其编码为字节,然后将结果的二进制数据使用 Base64 编码为可安全传输的 ASCII 文本字符串。接收者反转此过程以恢复原始消息。当然,这需要接收者知道使用了哪种编码,通常需要单独发送这些信息。


我们来看一个示例:


我希望发送一个带有两行的文本消息:


Hello
world!

如果我将其发送为 ASCII(或 UTF-8),它将如下所示:


72 101 108 108 111 10 119 111 114 108 100 33

某些系统会破坏字节 10,所以我们可以将这些字节作为 Base64 字符串进行 Base64 编码:


SGVsbG8Kd29ybGQh

这里的所有字节都是已知的安全字节,所以很少有机会使任何系统损坏此消息。我可以发送这个消息而不是我的原始消息,然后让接收者反转此过程以恢复原始消息。




2. 什么是 Base64 编码?


Base64编码将二进制数据转换为文本,具体来说是ASCII文本。生成的文本仅包含A-Za-z0-9以及符号+/这些字符。


而在之前我们在了不起的 Unicode中介绍过ASCII的。


由于字母表中有 26 个字母,我们有26 + 26 + 10 + 2(64)个字符。因此,这种编码被命名为Base64。这 64 个字符被认为是安全的,也就是说,与字符<>\n等不同,它们不会被旧计算机和程序误解


下面是经过 Base64 编码的文本front789的样子:ZnJvbnQ3ODk=


还有一点需要注意,如果在使用JS对某一个文本进行准换时,如果该文本包含非Latin1字符的字符串,会报错,所以我们需要对其进行准换处理。


// 原始文本字符串,包含非Latin1字符
const text = "前端柒八九";

// 创建一个 TextEncoder 对象,用于将文本编码为字节数组
const encoder = new TextEncoder();

// 使用 TextEncoder 对象将文本编码为字节数组
const data = encoder.encode(text);

// 使用 String.fromCharCode 和展开运算符 (...) 将字节数组转换为字符串
// 然后使用 btoa 函数将字符串转换为 Base64 编码
const base64 = btoa(String.fromCharCode(...data));

// 打印 Base64 编码后的结果
console.log(base64); //5YmN56uv5p+S5YWr5Lmd

我们在这里并没有加密文本。给定Base64编码的数据,非常容易将其转换回(解码)原始文本。我们只是改变了数据的表示,即编码



在本质上,Base64编码使用一组特定的、减少的字符来编码二进制数据,以防止数据损坏。



Base64字母表


由于只有64个字符可用于编码,我们可以仅使用6位来表示它们,因为2^6 = 64。每个Base64数字表示6位数据。一个字节中有8位,而 86最小公倍数24。因此,24 位,或 3 个字节,可以用四个 6 位的 Base64 数字表示


4. Base64 使用案例


我们可能在HTML文档中使用了<img src="789.jpeg">标签来包含图像。其实,我们可以直接将图像数据嵌入到 HTML 中,而不必使用外链!数据URL可以做到这一点,它们使用Base64编码的文本来内联嵌入文件。


<img src="data:image/gif;base64,xxxxbase64encodedtextxxxx" />

data:[<mime type
>
][;charset=<charset>][;base64],<encoded data></encoded></charset
></mime>

另一个常见的用例是当我们需要在网络上传输或存储一些二进制数据,而网络只能处理文本或ASCII数据时。这确保了数据在传输过程中保持不变。还有就是在 URL 中传递数据时,当数据包含不适合 URL 的字符时,此时Base64就有了用武之地。


Base编码还在许多应用程序中使用,因为它使得可以使用文本编辑器来操作对象。


我们还可以使用 Base64 编码将文件作为文本传输



  • 首先,获取文件的字节并将它们编码为 Base64

  • 然后传输 Base64 编码的字符串,然后在接收端解码为原始文件内容




5. Base64 编码算法


以下是将一些文本转换为 Base64 的简单算法。



  1. 将文本转换为其二进制表示

  2. 比特位分组为每组6位

  3. 将每个组转换为0到63的十进制数。它不能大于 64,因为每组只有 6 位。

    • 如果转换为十进制数的数字大于 64,我们可以将其取模64 例如:151 % 64 = 23



  4. 使用Base64字母表将此十进制数转换为等效的Base64字符


通过上述操作我们会得到一个Base64编码的字符串。如果最后一组中的比特位不足,可以使用===作为填充。


让我们以front7作为范例,来模拟上述操作。




  1. 通过首先将每个字符转换为其对应的 ASCII 数字,然后将该十进制数转换为二进制,(使用ASCII 转二进制工具)将文本front7转换为二进制:


    01100110 01110010 01101111 01101110 01110100 00110111

    f r o n t 7



  2. 将比特位分组为每组6位


    011001 100111 001001 101111 011011 100111 010000 110111



  3. 将每个组转换为 0 到 63 之间的十进制数:


    011001 100111 001001 101111 011011 100111 010000 110111

    25 23 9 47 27 23 16 27


    • 这步中如果数据超过 64,需要对其 64 取模




  4. 现在使用 Base64 字母表将每个十进制数转换为其 Base64 表示:


    25  23   9   47  27  23  16  27

    Z n J v b n Q 3



然后我们完成了。名字front7在 Base64 中表示为ZnJvbnQ3


乍一看,Base64 编码的好处并不是很明显。


想象一下,如果我们有一张图片或一个敏感文件(PDF、文本、视频等),而不是简单的字符串,我们想将它存储为文本。我们可以首先将其转换为二进制,然后进行 Base64 编码,以获得相应的 ASCII 文本。


现在我们可以将该文本发送或存储在任何地方,以任何我们喜欢的方式,而不必担心一些旧设备、协议或软件会错误解释原始二进制数据以损坏我们的文件。


6. 如何进行 Base64 编码和解码


所有编程语言都支持将数据编码为 Base64 格式以及从 Base64 格式解码数据。


JS 中处理


// 简单字符串
const text1 = "front789";
bota(text1); // ZnJvbnQ3ODk=

// 超出`Latin-1`字符的字符串
const text2 = "前端柒八九";
const encoder = new TextEncoder();
const data = encoder.encode(text);
const base64 = btoa(String.fromCharCode(...data));
console.log(base64); //5YmN56uv5p+S5YWr5Lmd

Rust 中处理


Rust的话,我们可以直接用 base64 crate。


Cargo.toml 文件中添加以下内容:


[dependencies]
base64 = "0.21.5"

use base64::{Engine as _, engine::general_purpose};

let orig = b"data";
let encoded: String = general_purpose::STANDARD_NO_PAD.encode(orig);
assert_eq!("ZGF0YQ", encoded);
assert_eq!(orig.as_slice(), &general_purpose::STANDARD_NO_PAD.decode(encoded).unwrap());

// or, URL-safe
let encoded_url = general_purpose::URL_SAFE_NO_PAD.encode(orig);

想了解更多关于Rust如何处理Base64,可以查看Rust base64


此外,终端也内置支持 Base64 编码。在终端中尝试以下命令:


echo "前端柒八九" | base64
5YmN56uv5p+S5YWr5LmdCg==

$ echo "5YmN56uv5p+S5YWr5LmdCg==" | base64 -d
前端柒八九



后记


分享是一种态度


全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。



作者:前端小魔女
来源:juejin.cn/post/7298190770401001512
收起阅读 »

某国外知名HR:参与开源贡献求职者的简历,直接扔掉

某国外知名HR:参与开源贡献求职者的简历,直接扔掉 一位自称曾在美国科技巨头(FAANG)担任招聘工作的“猎头”近日在推特上发表了一番引起争议的言论: 他声称在招聘时会直接将拥有开源贡献经历的开发者的简历扔掉。他的理由是,他怀疑那些有足够时间无私地为开源项目...
继续阅读 »

某国外知名HR:参与开源贡献求职者的简历,直接扔掉



  • 一位自称曾在美国科技巨头(FAANG)担任招聘工作的“猎头”近日在推特上发表了一番引起争议的言论:

  • 他声称在招聘时会直接将拥有开源贡献经历的开发者的简历扔掉。他的理由是,他怀疑那些有足够时间无私地为开源项目贡献代码的开发者,是否对自己的工作有足够的投入。

  • 进一步指出:“初级开发者参与开源项目可以理解,但高级开发者这么做可能意味着他们在职业成熟度上存在问题。”




  • 并附上了拒绝内推请求的截图:




  • 这一言论引发了广泛争议,因为他似乎忽略了许多开源贡献者是在下班时间和周末利用个人时间来参与开源项目,而这并不会对他们的工作产生负面影响。相反,这些贡献往往可以提升公司的技术声望和影响力。

  • 博主实在怀疑该 HR 的真实性,实际上源贡献不仅有助于个人技能提升,还可以为公司带来技术上的优势和创新。


个人简介


👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.


🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。


🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。


💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。


🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。


📖 保持关注我的博客,让我们共同追求技术卓越。


作者:Lorin洛林
来源:juejin.cn/post/7298179365040111642
收起阅读 »

众行远,携手打造全场景生态,OpenHarmony技术大会应用(北向)生态分论坛召开

全场景时代下,用户需求变得越来越多元化。软件服务又称北向应用,应用生态围绕着一个共同的技术平台,众多的参与者协同合作,最终形成大量的软件解决方案或服务。在2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会“应用(北向)生态分论坛”...
继续阅读 »

全场景时代下,用户需求变得越来越多元化。软件服务又称北向应用,应用生态围绕着一个共同的技术平台,众多的参与者协同合作,最终形成大量的软件解决方案或服务。

在2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会“应用(北向)生态分论坛”上,OpenAtom OpenHarmony(简称“OpenHarmony")项目群技术指导委员会主席、华为基础软件首席科学家陈海波先生发表了欢迎致辞。陈海波指出,对于一个新兴的操作系统来说,应用生态的蓬勃发展是影响其发展壮大的决定性因素。OpenHarmony以“生态统一,开放共赢”的理念,致力于搭建生态共赢的平台。OpenHarmony 4.0对应用开发的支持力度也更大,相对于前版本大幅度增加了约4000多个API的接口。

OpenHarmony项目群技术指导委员会主席、华为基础软件首席科学家陈海波致辞

OpenHarmony项目管理委员会PMC主席任格林在分论坛发言中表示,OpenHarmony的生态现在到了一个蓬勃汹涌的时候,伙伴“同行正当时,先行得先机,智联赢未来”,OpenHarmony现已可承载复杂性的开发,能力已具备,配套的工具链,相关的三方库也在不断完善。现在API发展到10,为什么没在API5、6的时候大规模推广,因为那时有很多东西要补充。此外,开发者论坛正在试用阶段,预计今年将正式发布,该论坛主要面向OpenHarmony的北向开发者。同时,他指出,OpenHarmony已经走过三年的历程,相关的设备已经达到了近亿台的量级,对应用生态的需求迫切。

OpenHarmony项目管理委员会PMC主席任革林发言

来自行业内的10位技术专家和应用开发者们共同分享了他们在OpenHarmony探索中的心路历程、最佳实践、示范场景以及对技术的新颖解读。出席论坛的演讲嘉宾包括:石墨文档(初心科技)副总裁齐惊,上海贝锐信息科技股份有限公司资深产品总监董恩双,腾讯科技(深圳)有限公司腾讯云通信产品专家林光柘,联通在线信息科技有限公司认证产品中心技术总监程福兴,论客科技(广州)有限公司产品副总裁郝家雨,北京信安世纪科技有限公司助理副总裁王洪骏,北京书生电子技术有限公司技术总监任春宁,武汉极意网络科技有限公司终端安全负责人许伟,深圳开鸿数字产业发展有限公司未来研究院应用生态首席架构师熊汉彪。

石墨文档(初心科技)副总裁齐惊在《石墨文档OpenHarmony技术探索之路》的主题演讲中介绍了石墨文档作为全新一代云Office办公软件,支持多人在线文档协同办公,实现了多终端、跨地域、随时随地在线办公。他强调了万物智联时代带来的交互方式、任务调度、数据安全等新场景,并阐述了石墨文档通过创新技术,使得AI在智能办公领域和基于OpenHarmony万物智联的基础框架上具备新的应用场景。

石墨文档(初心科技)副总裁齐惊主题分享

上海贝锐信息科技股份有限公司资深产品总监董恩双带来了《贝锐携手OpenHarmony跨平台协作》的主题分享。他指出,想要实现稳定、高效、安全、可靠的远程连接,需克服跨网络、跨平台兼容性等挑战,并介绍了向日葵、蒲公英等产品在跨平台连接方面兼容OpenHarmony、Windows、Linux等操作系统,满足了多系统、跨平台远程连接的需求。贝锐致力于打破边界连接人、设备、网络,让连接更美好。

上海贝锐信息科技股份有限公司资深产品总监 董恩双主题分享

腾讯云音视频通信产品专家林光柘指出,即时通信和实时音视频已成为互联网产品的基础技术组件,他探讨了如何在OpenHarmony系统中使用高质量的通信服务,以解决客户面临的挑战。腾讯云音视频通过IM&TRTC的实践应用,致力于提供最佳的服务体系,以解决这些痛点。

腾讯云音视频通信产品专家林光柘主题分享

联通在线信息科技有限公司联通认证产品中心技术总监程福兴带来了《免密登录更安全,OpenHarmony生态认证SDK最佳实践》的分享,他介绍联通认证免密登录SDK基于运营商网关认证技术,为4G/5G蜂窝网络数据流量用户提供用户授权的手机号码或号码核验服务,已被广泛集成于互联网应用APP,为移动应用提供了快捷的用户登录和号码认证风控保障。同时,他还探讨了OpenHarmony版的SDK将如何助力OpenHarmony生态实现与Android生态一致的用户便捷性,并提供更安全的免密登录解决方案。

联通在线信息科技有限公司联通认证产品中心技术总监程福兴主题分享

论客科技(广州)有限公司产品副总裁郝家雨在《Coremail OpenHamrony适配最佳实践》分享中提到,邮件系统及邮件客户端应用作为企业信息的门户及客户桌面高频应用,使用频繁且面临严峻的安全形势。打造功能强大、稳定可靠、开放灵活的邮件系统及邮件客户端应用需依赖架构先进的操作系统,如何借助OpenHarmony系统生态打造更安全、用户体验更好的邮件系统,邮件客户端产品应用,是未来重要的发展趋势。

论客科技(广州)有限公司产品副总裁郝家雨主题分享

北京信安世纪科技有限公司助理副总裁王洪骏的演讲主题是《基于OpenHarmony的零信任安全体系多维探索》。他从“零信任与OpenHarmony碰撞”、“信安世纪融合OpenHarmony推出零信任综合解决方案”、“AI+安全”、“信安世纪六大产品线”等四个方面展开介绍,强调了在网络环境中的基础性安全问题不容忽视的情况下,如何借助OpenHarmony和零信任安全体系来应对信息技术互联网化、移动化和云化等发展趋势的挑战。

北京信安世纪科技有限公司助理副总裁王洪骏主题分享

北京书生电子技术有限公司技术总监任春宁强调了经验的重要性并表示,书生阅读器具有安装便捷、界面友好、功能丰富等特点。此外,它还为第三方提供了丰富的二次开发接口。书生OFD的跨平台特性使其能够在不同的CPU和操作系统上流畅运行,应用适配OpenHarmony等多个操作系统。

北京书生电子技术有限公司技术总监任春宁主题分享

武汉极意网络科技有限公司终端安全负责人许伟阐述了公司的定位,他谈到极验是一家利用创新技术,帮助企业鉴别异常流量、提升用户流量价值的公司。极验行为验图形验证码在行业中占据 56.2%的市场份额,并在今年率先推出了适配基于 OpenHarmony 操作系统的解决方案。通过运用AIGC等前沿技术有效赋能企业,助力企业在业务场景中对抗黑灰产,辨别虚假流量。

武汉极意网络科技有限公司终端安全负责人许伟主题分享

深圳开鸿数字产业发展有限公司未来研究院应用生态首席架构师熊汉彪介绍了操作系统生态的建立离不开海量的北向应用支撑。他表示,应用生态直面终端用户和广大应用开发者,是整个操作系统生态的关键环节。同时,他还深度阐述了深开鸿如何面向行业,积极推进行业应用OpenHarmony化的进程,以推动整个生态系统的繁荣发展。

随后,论坛进入了圆桌讨论环节,该环节由华为终端云服务全球生态发展与销售部软件生态专家李华怡博士主持,以“众行远,携手打造全场景生态”为主题,展开讨论。参与嘉宾包括齐惊、董恩双、郝家雨、王洪骏和任春宁等业界专家。在圆桌讨论中,嘉宾们纷纷发表了对于OpenHarmony生态建设的看法和建议,共同探讨如何推动OpenHarmony在全场景应用中的发展。

深圳开鸿数字产业发展有限公司未来研究院应用生态首席架构师熊汉彪主题分享

OpenHarmony是由开放原子开源基金会孵化及运营的开源项目,目标是构建一个开放、全场景、分布式的操作系统。作为一款全新的开源操作系统,OpenHarmony已经在多个领域展现出强大的潜力。开源社区和企业的紧密合作是OpenHarmony生态建设的关键,在本次论坛中,多家企业表示将积极参与OpenHarmony的开发和推广,共同助力OpenHarmony生态建设。这种跨界合作不仅有助于提升OpenHarmony的应用能力,还将为全产业链带来更多的创新机遇。

圆桌讨论现场照片

收起阅读 »

从更新迭代中找寻发展OpenHarmony技术大会编程语言及开发框架分论坛召开

科技行业注重与时俱进,秉持创新不息的精神,编程语言和开发框架是影响应用体验和应用生态的关键要素,持续更新与发展至关重要。从编程语言Javascript/Typescript,Python,Java,Kotlin,Swift,Rust,ArkTS,再到开发框架W...
继续阅读 »

科技行业注重与时俱进,秉持创新不息的精神,编程语言和开发框架是影响应用体验和应用生态的关键要素,持续更新与发展至关重要。从编程语言Javascript/Typescript,Python,Java,Kotlin,Swift,Rust,ArkTS,再到开发框架Web,Flutter,SwiftUI,JetpackCompose,ArkUI等等的百花齐放,开发者面临着丰富的开发工具和语言的选择。为了更好地推动智能化发展,开发者必须关注万物互联到万物智联的产业趋势,积极拥抱变革,打造适应新场景和新应用模型的解决方案,为未来的智能化发展贡献力量。

2023年11月4日的第二届开放原子开源基金会OpenHarmony技术大会编程语言及开发框架分论坛上,一场关于万物智联时代应用软件开发技术方向与前景的探讨热烈展开。编程语言及开发框架分论坛邀请了OpenHarmony项目群技术指导委员会编程语言TSG负责人、华为编程语言首席专家冯新宇,OpenHarmony项目群技术指导委员会跨平台应用框架TSG负责人余枝强担任出品人。同时,来自华为、英特尔中国有限公司、支付宝(中国)网络技术有限公司、百度、字节跳动等科技企业的技术专家,与南京大学、北京航空航天大学等学术机构的代表共计9位嘉宾齐聚一堂,共同探讨了万物智联时代应用软件开发技术的方向与前景。

出品人:OpenHarmony项目群技术指导委员会编程语言TSG负责人、

华为编程语言首席专家冯新宇

出品人:OpenHarmony项目群技术指导委员会跨平台应用框架TSG负责人余枝强

英特尔中国有限公司WebAssemblyMicroRuntime开源项目技术委员会主席王鑫介绍WebAssembly是吸收了工业最佳实践与学术界前沿研究成果的一门程序语言,从C/C++最早作为支持WebAssembly的前端语言开始,至今有越来越多的前端编程语言如Rust,Go,Kotlin,AssemblyScript等已经支持将WebAssembly作为一个编译目标,促使一个基于WebAssembly的跨语言生态正在快速发展。会上也分享开源项目wasmnizer-ts在编译TypeScript到WebAssembly的技术实践。

英特尔中国有限公司WebAssemblyMicroRuntime开源项目技术委员会主席王鑫主题分享

支付宝终端基础平台技术部负责人黎三平带来《支付宝终端技术实践与思考》主题演讲,他指出在终端技术的迭代演进过程中具有一定的周期规律,从过去单领域工程化的阶段进入到跨领域整合的第二曲线阶段。那么,作为拥有数亿用户的生活服务平台,支付宝的终端技术是如何承载数字化的服务,特别是在面向多设备、多场景下的挑战是如何应对的?其实本质上就是通过编程语言和框架的升级迭代,来提供最佳的解决方案。

支付宝终端基础平台技术部负责人黎三平主题分享

随后,百度资深研发工程师、文心一言APP技术负责人樊中恺在介绍《AINative应用开发》时谈到AINative时代最具革命性的变化是:随版的产品功能迭代转化为随时的模型能力升级。而串联用户和产品的,则是对齐后的快捷交互和自然语言。

百度资深研发工程师、文心一言APP技术负责人樊中恺主题分享

华为2012实验室技术专家凌实认为面向下一代万物互联新生态,亟需突破传统架构,并且携手生态伙伴,方能共建全新的开放Web运行底座。他指出Web技术因其跨端能力和强动态性等特点,在应用开发中扮演着越来越重要的作用。面向应用的场景也对Web技术在性能、扩展性、用户体验等方面提出了相对传统网页场景不同的需求。因此,在OpenHarmony新生态下Web技术迎来了诸多的机会和挑战。

华为2012实验室技术专家凌实主题分享

随后,华为ArkUI技术专家兰守忍认为伴随着移动互联网的高速发展,跨平台框架经历了从Web技术、到原生渲染、到自渲染技术的发展历程。而ArkUI作为OpenHarmony原生应用开发框架,其具备简洁自然的声明式语法,并且已拓展到多个OS平台,基于一套主代码,可构建支持多平台的精美、高性能应用。此外,ArkUI开发框架开发效率上提升代码复用度,用户体验上提供UX一致性和极致的性能,业务上提供新特性快速更新的动态化能力,持续演进框架竞争力。

华为ArkUI技术专家兰守忍主题分享

南京大学计算机科学与技术系副教授,博导陈林则提出在软件生态系统中,混合语言编程会遇到诸多的问题与挑战。他表示混合语言编程是开放互联、协同演进的软件生态系统的自然选择。他主要从软件生态系统质量保障的视角,介绍了胶水代码自动生成和跨语言缺陷分析面临的问题和挑战。提出了一种基于参数化模板重写系统的胶水代码生成方法,可以高效地生成契合实际场景的JavaScript/C胶水代码,同时有效地阻止跨语言边界的软件缺陷。

南京大学计算机科学与技术系副教授、博士生导师陈林主题分享

北京航空航天大学教授、博士生导师史晓华谈到尽管JavaScript程序当前主要通过JS引擎执行,但随着WebAssembly成为越来越受欢迎的底层运行时系统,JWST给出了在WebAssembly运行时之上高效运行JavaScript的另一种选择。并指出JWST是一个将JavaScript程序静态编译为WebAssembly或者native代码的编译器,它采用QuickJS编译生成的字节码作为输入,编译优化后生成LLVMIR,最后通过LLVM后端编译器生成WebAssembly字节码或者nativecode。与此同时,JWST在JavaScript语言规范ECMATEST-262测试集上获得了不低于QuickJS的通过率。

北京航空航天大学教授、博士生导师史晓华主题分享

字节跳动SELab技术专家彭超带来了《移动应用GUI程序测试实践与挑战》分享,通过移动应用GUI程序测试挑战涉猎智能遍历、多机协同、基于函数变更的精准定向和拟人测试等能力,以及基于CV算法的图形异常断言能力带来强关联。

字节跳动SELab技术专家彭超主题分享

华为中央软件院编程语言实验室架构师徐潇在主题为《编程语言并发特性设计及面向移动应用开发的思考》演讲中指出,移动应用"天然异步”的特征决定了其开发语言需要具备对并发的支持,涉及语言、运行时、库及配套工具等。报告中介绍了华为编程语言实验室对上述特性的思考和相关语言特性的设计,以支持应用开发中的高并发编程。

华为中央软件院编程语言实验室架构师徐潇主题分享

通过各位嘉宾的精彩分享,不难看出,编程语言及应用框架是促使操作系统持续、高效发展的的重要组成部分,而应用生态的形成与建立才是最终的目标。OpenHarmony驱动了万物智联形态下应用软件开发的技术创新,从而更好地夯实了技术壁垒,加速了OpenHarmony应用生态的繁荣发展。期待更多合作伙伴加入到OpenHarmony应用生态中来,共同构建万物智联的美好世界。

收起阅读 »

【Java集合】数据结构与集合的神秘联系,一文读懂!

嗨~ 今天的你过得还好吗?变好的过程都不太舒服试试再努力点🌞- 2023.11.06 -上篇文章中我们对单列集合中常用的方法和遍历查询。通过本文章为我们解惑,好好的字符串用起来不就行了,为什么要用集合这些工具类?本篇文章将简要介绍数据结构,让读者了解...
继续阅读 »



嗨~ 今天的你过得还好吗?

变好的过程都不太舒服

试试再努力点

🌞

- 2023.11.06 -

上篇文章中我们对单列集合中常用的方法和遍历查询。通过本文章为我们解惑,好好的字符串用起来不就行了,为什么要用集合这些工具类?

本篇文章将简要介绍数据结构,让读者了解它们在计算机中以何种结构方式存在。那么,什么是数据结构呢?下面我们来详细解释。




数据结构

1.1 数据结构有什么用?

数据结构是计算机存储、组织数据的方式。 数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。 通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。

数据结构往往同高效的检索算法和索引技术有关。 这句话是啥意思呢? 我们举个简单的例子。就像金庸小说中所写的,武功招式就相当于我们的算法,而数据结构就是我们的内功心法;而武功的高低,不仅仅是武功招式,更重要的是 学会的内功心法。就比如张无忌在学会九阳神功之后,就可以大战六大门派。


而数据结构的学习,也会让我们事半功倍。凭借着“数据结构+算法=程序”这句话,Pascal之父获得了图灵奖。


总结来说:

  • 数据结构就是一种是将世界上各种数据转化为计算机可以存储和操作的形式,定义了逻辑结构如何在计算机上存储,以及相关的基本操作。

  • 算法是程序猿通过调用不同数据结构的基本操作,从而实现了数据的处理。

而这两点使我们作为程序开发人员的必备基本功,不是一朝一夕就能成为绝世高手的,我们需要一步步去不断的学习积累,积硅步以致千里。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看


1.2 常见的数据结构

在计算机学科中,数据结构是一门很重要的基础学科,知识点很多。在这里我们不讲那么多,只讲述我们集合中用到的几种数据结构,同学们可以下去自行学习更多的数据结构的知识。 常用结构三个:数组、链表、红黑树


我们分别来了解一下:

1)数组

数组的定义:

  • 数组是相同类型数据的有序集合;

  • 数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成;

  • 其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们。


存储思路:

所有数据存储在连续的空间中,数组中的每个元素都是一个具体的数据。




数组的特点:

  • 使用连续分配的内存空间;

  • 一次申请一大段连续的空间,需要事先声明最大可能要占的固定内存空间。

如下图:

 image

  • 通过索引,查询快

  • 当给数组插入新元素时,数组中的a2,a3,a4整体后移,代价高。

  • 如果插入元素时,数组长度,还要重新创建一个数组,然后循环赋值,代价高

优点:

设计简单,读取与修改表中的任意一个元素的时间都是固定的,速度快 。

缺点:

容易造成内存浪费;删除或者插入数据需要移动大量数据,速度慢。


2)链表

每个数据单独存在一小块内存中,这个单元叫做节点,每个节点知道下一个节点的地址,叫做单向链表。 每个节点既知道下一个节点地址,又知道上一个节点地址,叫做双向链表。 image 

链表的特点:

  • 使用不连续的内存空间;

  • 不需要提前声明好指定大小的内存空间,一次申请一小块,按需申请。

image

  • 查询元素,需要通过节点一次向后查找,直到查找到指定元素

  • 增删元素:只需修改连接节点的地址即可。

优点: 充分节省内存空间,数据插入和删除方便,不需要移动大量数据。

缺点: 查询数据必须按顺序找到该数据,操作麻烦。



3)红黑树

简单理解,就是一种类似于我们生活中树的结构,只不过每个节点最多只有两个叶子。计算机世界的树,刚好与我们现实中的树成镜像相反,树根在上,树枝在下。二叉树如下图: image

而我们要说的是二叉树的一种比较有意思的叫做红黑树,红黑树本身就是一颗二叉查找树。我们在这里只需要记住它的特点就可以非常方便的对树中的所有节点进行排序和检索。


小结

本文介绍了三种常用的数据结构:数组、链表和红黑树,以及这些数据结构在计算机中的重要意义。通过学习这些内容,我们可以逐步深入了解计算机世界。



我们下期再见!


收起阅读 »

工作不给转正又不辞退怎么办

工作篇 我在8月底结束了第一份工作后,在9月初迎来了我的二份工作,投简历投了大概一周约了12家面试,虽然那时候是疫情相对来说找工作还是比较容易点的,至少约的到面试,面了差不多6家,拿到了2家,当时情绪低落囊中羞涩想尽快入职工作选择第2家,后面的面试都推了,但是...
继续阅读 »

工作篇


我在8月底结束了第一份工作后,在9月初迎来了我的二份工作,投简历投了大概一周约了12家面试,虽然那时候是疫情相对来说找工作还是比较容易点的,至少约的到面试,面了差不多6家,拿到了2家,当时情绪低落囊中羞涩想尽快入职工作选择第2家,后面的面试都推了,但是问题来了,通勤距离过长,每天公交从首站坐20个站左右去上班,下班在科技园的东门而公司在北门,走大概10分钟,从家里去公交站走路20分钟,于是我就开启了长达单程快2小时的路程上班,房子还有两个月到期我决定到期再搬过来。


u=503144603,235984279&fm=253&fmt=auto&app=138&f=JPEG.webp


这份工作给的工资是6500,试用期80%,也就是拿到手5200,试用期不交社保[刚毕业不太懂社保这个],毕竟刚毕业,然后我就想努力的转正,这次我面试的职位的 .net开发工程师,离谱的事情我刚入职第一天让我接手一个五年经验大佬的项目[压缩成本也不是这样压缩的],那我只是一只小喽喽啊,害,我痛苦极了, 很显然很多东西我接不住


u=473232946,249253702&fm=175&s=D28469A4081186D45A1BA5950300008B&w=640&h=479&img.jpg


但是交接大佬人挺好的这里暂称他为大哥,大嫂还在公司,后面有问题我直接让大嫂帮我问大哥,就暂时还过的去,勉勉强强的干着但是我特别吃力,我的顶头管我的是一个50左右的经理,开发出生头发花白,在公司待了12年,公司是做支付平台的又特别的严格,我之前没有接触过这块,有什么问题我还会问我之前的师傅,就勉强干着,经理每天晚上带我加班,于是我每天早上6点20起床,晚上12点到家,我的长头发嫌洗头发麻烦头发也剪了,不出意外一个月多以后我生病了,大概就是发烧,睡眠不足,经期也有问题,大概休了三天假加周末后来就好了,我当时就想离职了,觉得自己顶不住真的顶不住,第一家公司6天70个小时,第二家6天达到快90个小时,我就差睡在公司了


9KNN0fLVFxnsMU9jUyI2xe0HAqRFN5T1.gif


那段时间我每天都会被骂,就在工位其他同事都听见,我真想找个地缝钻下去,在高强度的工作下,公司每天都在招人,明明已经不缺人,公司试用期三天没工资[如果换我现在肯定告他了],招人又开人,很离谱这样的公司现在还存在,后大概快两个月后我搬家离公司20分钟的车程附近我的日子稍微好过了一点,但是我却出了点问题,我每天焦虑的睡不觉,周末休息也不爱出门就在家里学习看代码,可能是怕被骂,我的精神状态一直不太好,睡觉喜欢抽,蹬腿特别严重时常蹬腿瞪醒,后来我就想着转正了好了,两个多月过去了快转正了,当时11月多了也想着快过年了,在这家公司我每个月还能存2000块钱,5200的工资,因为我几乎不出门就是吃饭睡觉,每天开销60左右,科技园里面吃饭挺贵的我算很省了,在这个公司除了老板跟经理,其他同事相处挺好的,我是里面最小的也挺照顾我的,他们都是25+,于是我就在这样过着,当时也没有意识去医院看看,就想着转正能加1300的工资,对于我来说是一笔不小的诱惑。。。。。


u=1160940878,4065028145&fm=253&fmt=auto&app=120&f=JPEG.webp


转眼三个月过去了公司没有给我转正,也没有开我就闭口不谈,我就急了在等我三个月后的10天左右,我就找人事询问,人事姐姐给我的意见是让我去问经理,经理说我被延迟转正了,说我表现不好,于是我又开始emo了,也不知都怎么办想离职又快过年了就这样混着,想着会找我谈,我还是太年轻了,直到12月20号左右,我找经理谈了大概意思就是要我转岗,说我基础太差了什么的,转做实施去客户现场调试工资4000,我拒绝了我知道这就是想赶我走,于是我离职了,离职后没有压力反而是一种解脱,我记得离职当天去看了场电影,那是我第一次在深圳看电影就在坂田,看完电影后回家哭了一场,然后就休息了两天总是失眠加蹬腿(我不知道大家是否也有过这样现象),医院给我建议是精神太紧张长期睡眠不足经期也不正常,让我好好休息,大概休息了几天月底我就和我同学去了张家界旅游去了,就这样我的第二份工作结束了。。。。。。。。。。。。。。。。


作者:很饿的秃头
来源:juejin.cn/post/7291496777688334336
收起阅读 »

Android 复杂UI界面分模块解耦的一次实践

一、复杂UI页面开发的问题 常见的比较复杂的UI界面,比如电商首页,我们看看某电商的首页部分UI: 上面是截取的首页部分,如果这个首页如果不分模块开发会遇到哪些问题? 开发任务不方便分割,一个人开发的话周期会很长 在XML文件中写死首页布局不够灵活 逻辑和...
继续阅读 »

一、复杂UI页面开发的问题


常见的比较复杂的UI界面,比如电商首页,我们看看某电商的首页部分UI:


Screenshot_2023-11-03-10-57-45-754_com.jingdong.app.mall.jpg


上面是截取的首页部分,如果这个首页如果不分模块开发会遇到哪些问题?



  • 开发任务不方便分割,一个人开发的话周期会很长

  • 在XML文件中写死首页布局不够灵活

  • 逻辑和UI塞在一起不方便维护

  • 首页不能动态化配置

  • UI和逻辑难以复用


那如何解决这个问题? 下面是基于基于BRVAH 3.0.11版本实现的复杂页面分模块的UI和逻辑的解耦。


二、解决思路


使用RecyclerView在BRVAH中利用不同的ViewType灵活的组装页面。但也面临一些问题,比如:



  • 如何实现模块间的通讯和互传数据?

  • 如何实现模块整理刷新和局部刷新?


下面都会给出答案。


三、具体实践


我们先看看模块拆分组装UI实现的效果:


Screen_Recording_20231103_124525_TestKotlin_V1.gif


模块二中有三个按钮,前面两个按钮可以启动和停止模块一中的计数,最后一个按钮获取模块一中的计数值。对应的就是模块间通讯和获取数据。


先看看模块一中的代码:


/**
* 模块一具有Activity生命周期感知能力
*/

class ModuleOneItemBinder(
private val lifecycleOwner: LifecycleOwner
) : QuickViewBindingItemBinder<ModuleOneData, LayoutModuleOneBinding>(),
LifecycleEventObserver, MultiItemEntity {

private var mTimer: Timer? = null
private var mIsStart: Boolean = true //是否开始计时
private var number: Int = 0
private lateinit var mViewBinding: LayoutModuleOneBinding

init {
lifecycleOwner.lifecycle.addObserver(this)
}

@SuppressLint("SetTextI18n")
override fun convert(
holder: BinderVBHolder<LayoutModuleOneBinding>,
data: ModuleOneData
)
{
//TODO 根据数据设置模块的UI
}

override fun onCreateViewBinding(
layoutInflater: LayoutInflater,
parent: ViewGr0up,
viewType: Int
)
: LayoutModuleOneBinding {
mViewBinding = LayoutModuleOneBinding.inflate(layoutInflater, parent, false)
return mViewBinding
}


/**
* 向外暴露调用方法
* 开始计时
*/

fun startTimer() {
if (mTimer != null) {
mIsStart = true
} else {
mTimer = fixedRateTimer(period = 1000L) {
if (mIsStart) {
number++
//修改Adapter中的值,其他模块可以通过Adapter取到这个值,也可以通过接口抛出去,这里是提供另一种思路。
(data[0] as ModuleOneData).text = number.toString()
mViewBinding.tv.text = "计时:$number"
}
}
}
}

/**
* 向外暴露调用方法
* 停止计时
*/

fun stopTimer() {
mTimer?.apply {
mIsStart = false
}
}

/**
* 生命周期部分的处理
*/

override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_DESTROY -> {
//页面销毁时计时器也取消和销毁
lifecycleOwner.lifecycle.removeObserver(this)
mTimer?.cancel()
mTimer = null
}

else -> {}
}
}

/**
* 设定itemType
*/

override val itemType: Int
get() = MODULE_ONE_ITEM_TYPE

}

模块一向外暴露了startTimer()stopTimer()二个方法,并且让模块一具备了Activity的生命周期感知能力,用于在页面销毁时取消和销毁计时。具备页面生命周期感知能力是模块很重要的特性。


再看看模块二中的代码:


class ModuleTwoItemBinder(private val moduleTwoItemBinderInterface: ModuleTwoItemBinderInterface) :
QuickViewBindingItemBinder<ModuleTwoData, LayoutModuleTwoBinding>(), MultiItemEntity {

@SuppressLint("SetTextI18n")
override fun convert(
holder: BinderVBHolder<LayoutModuleTwoBinding>,
data: ModuleTwoData
)
{

holder.viewBinding.btStartTimer.setOnClickListener { //接口实现
moduleTwoItemBinderInterface.onStartTimer()
}

holder.viewBinding.btStopTimer.setOnClickListener { //接口实现
moduleTwoItemBinderInterface.onStopTimer()
}

holder.viewBinding.btGetTimerNumber.setOnClickListener { //接口实现
holder.viewBinding.tv.text =
"获取到的模块一的计时数据:" + moduleTwoItemBinderInterface.onGetTimerNumber()
}

}

/**
* 可以做局部刷新
*/

override fun convert(
holder: BinderVBHolder<LayoutModuleTwoBinding>,
data: ModuleTwoData,
payloads: List<Any>
)
{
super.convert(holder, data, payloads)
if (payloads.isNullOrEmpty()) {
convert(holder, data)
} else {
//TODO 根据具体的payloads做局部刷新
}
}

override fun onCreateViewBinding(
layoutInflater: LayoutInflater,
parent: ViewGr0up,
viewType: Int
)
: LayoutModuleTwoBinding {
return LayoutModuleTwoBinding.inflate(layoutInflater, parent, false)
}

override val itemType: Int
get() = MODULE_TWO_ITEM_TYPE

}

模块二中有一个ModuleTwoItemBinderInterface接口对象,用于调用接口方法,具体接口实现在外部。convert有全量刷新和局部刷新的方法,对于刷新也比较友好。


接着看看是如何把不同的模块拼接起来的:


class MultipleModuleTestAdapter(
private val lifecycleOwner: LifecycleOwner,
data: MutableList<Any>? = null
) : BaseBinderAdapter(data) {

override fun getItemViewType(position: Int): Int {
return position + 1
}

/**
* 给类型一和类型二设置数据
*/

fun setData(response: String) {
val moduleOneData = ModuleOneData().apply { text = "模块一数据:$response" }
val moduleTwoData = ModuleTwoData().apply { text = "模块二数据:$response" }
//给Adapter设置数据
setList(arrayListOf(moduleOneData, moduleTwoData))
}

/**
* 添加ItemType类型一
*/

fun addItemOneBinder() {
addItemBinder(
ModuleOneData::class.java,
ModuleOneItemBinder(lifecycleOwner)
)
}

/**
* 添加ItemType类型二
*/

fun addItemTwoBinder(moduleTwoItemBinderInterface: ModuleTwoItemBinderInterface) {
addItemBinder(
ModuleTwoData::class.java,
ModuleTwoItemBinder(moduleTwoItemBinderInterface)
)
}

}

class MainModuleManager(
private val activity: MainActivity,
private val viewModel: MainViewModel,
private val viewBinding: ActivityMainBinding
) {

private var multipleModuleTestAdapter: MultipleModuleTestAdapter? = null

/**
* 监听请求数据的回调
*/

fun observeData() {
viewModel.requestDataLiveData.observe(activity) {
//接口请求到的数据
initAdapter(it)
}
}

private fun initAdapter(response: String) {
//创建Adapter
multipleModuleTestAdapter = MultipleModuleTestAdapter(activity)
//设置RecyclerView
viewBinding.rcy.apply {
layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = multipleModuleTestAdapter
}
//创建ModuleTwoItemBinder的接口实现类
val moduleTwoItemBinderImpl = ModuleTwoItemBinderImpl(multipleModuleTestAdapter)
//添加Item类型,组装UI,可以根据后台数据动态化
multipleModuleTestAdapter?.addItemOneBinder()
multipleModuleTestAdapter?.addItemTwoBinder(moduleTwoItemBinderImpl)
//给所有的Item添加数据
multipleModuleTestAdapter?.setData(response)
}


/**
* 刷新单个模块的数据,也可以刷新单个模块的某个部分,需要设置playload
*/

fun refreshModuleData(position: Int, newData: Any?) {
multipleModuleTestAdapter?.apply {
newData?.let {
data[position] = newData
notifyItemChanged(position)
}
}
}

}

MultipleModuleTestAdapter中定义了多种ViewType,通过MainModuleManager返回的数据,动态的组装添加ViewType


最后就是在MainActivity中调用MainModuleManager,代码如下:


class MainActivity : AppCompatActivity() {

private val mainViewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activityMainBinding: ActivityMainBinding =
ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)

//请求数据
mainViewModel.requestData()

//拆分RecyclerView的逻辑
val mainModuleManager = MainModuleManager(this, mainViewModel, activityMainBinding)
//回调数据到MainModuleManager中
mainModuleManager.observeData()

//TODO 如果有其他控件编写其他控件的逻辑

}

}

这样我们通过定义不同的ItemBinder实现了模块的划分,通过定义接口实现了模块间的通讯,通过后台返回数据动态的组装了页面。


其他代码一并写在末尾,方便阅读和理解:


image.png


ModuleConstant


object ModuleConstant {
//ItemType
const val MODULE_ONE_ITEM_TYPE = 0
const val MODULE_TWO_ITEM_TYPE = 1
}

ModuleOneDataModuleTwoData都是data类,内容完全一致,随便定义的:


data class ModuleOneData(
var text: String? = ""
)

ModuleTwoItemBinderImplModuleTwoItemBinderInterface的实现类,通过Adapter能轻松的获取到不同的ItemBinder,所以可以通过接口互相调用彼此的函数。


class ModuleTwoItemBinderImpl(private val multipleModuleTestAdapter: MultipleModuleTestAdapter?) :
ModuleTwoItemBinderInterface {

/**
* 外部实现里面的方法
*/

override fun onStartTimer() {
//通过`Adapter`能轻松的获取到不同的`ItemBinder`,所以可以通过接口互相调用彼此的函数
val moduleOneItemBinder =
multipleModuleTestAdapter?.getItemBinder(ModuleConstant.MODULE_ONE_ITEM_TYPE + 1) as ModuleOneItemBinder
moduleOneItemBinder.startTimer()
}

override fun onStopTimer() {
//通过`Adapter`能轻松的获取到不同的`ItemBinder`,所以可以通过接口互相调用彼此的函数
val moduleOneItemBinder =
multipleModuleTestAdapter?.getItemBinder(ModuleConstant.MODULE_ONE_ITEM_TYPE + 1) as ModuleOneItemBinder
moduleOneItemBinder.stopTimer()
}

override fun onGetTimerNumber(): String {
multipleModuleTestAdapter?.apply {
//通过Adapter可以轻松的拿到其他模块的数据
return (data[0] as ModuleOneData).text ?: "0"
}
return "0"
}

}

interface ModuleTwoItemBinderInterface {

//开始计时
fun onStartTimer()

//停止计时
fun onStopTimer()

//获取计时数据
fun onGetTimerNumber():String
}

四、总结


通过定义不同的ItemBinder将页面划分为不同模块,实现UI和交互解耦,单个ItemBinder也可以在其他页面进行复用。通过后台数据动态的添加ItemBinder页面组装更灵活。任务分拆,提高开发效率。


五、注意事项


1、不要把太复杂的UI交互放在单一模块,处理起来费劲。

2、如果二个模块中间需要大量的通讯,写太多接口也费劲,最好看能不能放一个模块。

3、数据最好请求好后再塞进去给各个ItemBinder用,方便统一处理UI。当然如果各个模块想自己处理UI,那各个模块也可以自己去请求接口。毕竟模块隔离,彼此也互不影响。

4、页面如果不是很复杂,不需要拆分成模块,不需要使用这种方式,直接一个XML搞定,清晰简单。


时间仓促,如有错误欢迎批评指正!!


作者:TimeFine
来源:juejin.cn/post/7296865632166477833
收起阅读 »

Android 签名、打包、上架

最近在做一些简单的Android需求开发,其他打包的过程碰到的一些问题做一个梳理。 【Android需要通过AS-> Open,打开工程,不然容易出问题】 1.签名 a.keystore.jks文件 接受的项目都是已经比较成熟的项目,在项目的目录下都有一...
继续阅读 »

最近在做一些简单的Android需求开发,其他打包的过程碰到的一些问题做一个梳理。
【Android需要通过AS-> Open,打开工程,不然容易出问题】


1.签名


a.keystore.jks文件

接受的项目都是已经比较成熟的项目,在项目的目录下都有一个.jks的文件,里面会包含一些秘钥信息
image.png
在工程中的Android目录下build.gradle(Module:xxxx.app)里面会有秘钥的详细image.png


b.开始签名

image.png
image.png



如果工程中已经有.jks文件,选择Choose existing...选项,选中Project目录中的.jks文件即可.



image.png
然后继续
image.png



至此,打包完成了,根目录下的app文件夹里面找到debugrelease里面就是刚刚打包成功的.apk文件。
如果需要创建新的秘钥



image.png



拓展:怎么生成.jks文件夹、怎么生成签名秘钥



2.生成.jks文件


a.创建并在Project工程目录下生成.jks文件,与app目录同级

image.png


选择Creat new进入创建界面



重要!!! 需要选择项目下的app目录下,然后修改Untitled名称改为keystore.jks,保存即可,保存之后会返回一下界面,填写相关信息即可成功创建相关秘钥,并保存在刚才创建的.jks文件中,保存即可。



image.png


b.配置打包Signing Configs

image.png
image.png
image.png
image.png
Pasted Graphic.png
image.png



按照图示的步骤来,即可完成配置。
然后在app 目录的build.gradle文件中可看到如下生成的代码配置。



image.png



注意:出现如下图示,不影响apk打包,但是有警告,相对路径去怎么解决这个问题,有知道的,可以告知一下。



Pasted Graphic 3.png


3.处理apk包名显示



正常情况下如果是内部软件,不需要加固,如果是外部软件加固一下【腾讯乐固】,对于生成的包名称可以配置显示【名称+版本+版本号+时间】,配置如下:截图框出的方法需要写在andriod方法里面



image.png


// 自定义打包名称
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "xxxAPK_${buildType.name}_v${versionName}_${generateTime()}.apk"
}
}

构建时间的方法需要在android方法外


//构建时间
def generateTime() {
return new Date().format("yyyyMMddHHmmss")
}

4.加固包重签名处理



AS打包生成的apk包是签名包,上传到 【腾讯乐固】加固后,这时候的加固包是不能直接安装或者上传应用市场,需要在签名一次才可以。以下就是加固包签名的命令行命令



 jarsigner -verbose -keystore xx[jsk文件绝对路径]xx.jks -signedjar xxx[加固前的apk包绝对路径]xxxAPK_release_v1.0.6_20231026092106.apk   xx[加固后的apk包绝对路径]xx.apk  xx[秘钥的名称keyAlias]xx

中间都是空格隔开就可以,主要理解是加固前和加固后的包的位置。然后秘钥keyAlias的名称需要app目录下的build.gradle文件里面找。



至此,可以上传重签名后的apk包到应用市场了 参考



5.相对路径


在Android工程配置中,可以使用相对路径来表达文件或目录的位置。相对路径是相对于当前文件或目录的路径,而不是完整的绝对路径。


以下是在Android工程配置中使用相对路径的一些示例:



  1. 在Gradle脚本中引用相对路径:


def relativePath = '../subdirectory/myfile.txt'


  1. 在AndroidManifest.xml文件中引用相对路径:


<meta-data
android:name="my_data"
android:value="../subdirectory/myfile.txt" />



  1. 在资源文件(如布局文件或字符串资源文件)中引用相对路径:


<ImageView
android:src="@drawable/../subdirectory/myimage.png" />


在上述示例中,相对路径使用../来表示从当前位置向上一级目录的相对路径。你可以根据实际情况调整相对路径的格式和层数。


使用相对路径的好处是,它提供了一种相对于当前位置的灵活方式来引用文件或目录。这样,当你的工程目录结构发生变化时,不需要修改绝对路径,只需调整相对路径即可。


请注意,相对路径的解析取决于当前位置,因此确保当前位置的准确性和相对路径的正确性。


总而言之,使用相对路径可以在Android工程配置中指定文件或目录的位置,使其更具可移植性和灵活性。根据你的具体需求,可以在相应的配置文件或资源中使用相对路径来引用文件或目录。


作者:AKA
来源:juejin.cn/post/7296011286093168659
收起阅读 »

强无敌!一个项目涵盖SpringBoot集成各种场景

大家好,我是 Java陈序员。我们都知道,作为 Java 后端开发肯定绕不开 Spring,而 SpringBoot 的横空出世更是帮助我们开发者可以快速迭代一个项目! SpringBoot 之所以强大,是因为支持自动化配置,可以快速装配组件,如持久化框架缓存...
继续阅读 »

大家好,我是 Java陈序员。我们都知道,作为 Java 后端开发肯定绕不开 Spring,而 SpringBoot 的横空出世更是帮助我们开发者可以快速迭代一个项目!


SpringBoot 之所以强大,是因为支持自动化配置,可以快速装配组件,如持久化框架缓存消息队列日志等等。


今天给大家介绍一个 SpringBoot 集成各种场景的项目,可以用来学习,也可以开箱即用无需重复造轮子


项目简介


spring boot demo 是一个用来深度学习并实战 spring boot 的项目,目前总共包含 66 个集成 demo,已经完成 55 个。


目前已经集成功能:



  • actuator:监控

  • admin:可视化监控

  • logback:日志

  • aopLog:通过 AOP 记录 Web 请求日志

  • 统一异常处理:json 级别和页面级别

  • freemarker:模板引擎

  • thymeleaf:模板引擎

  • Beetl:模板引擎

  • Enjoy:模板引擎

  • JdbcTemplate:通用 JDBC 操作数据库

  • JPA:强大的 ORM 框架

  • Mybatis:强大的 ORM 框架

  • 通用 Mapper:快速操作 Mybatis

  • PageHelper:通用的 Mybatis 分页插件

  • Mybatis-plus:快速操作 Mybatis

  • BeetlSQL:强大的 ORM 框架

  • upload:本地文件上传和七牛云文件上传

  • Redis:缓存

  • ehcache:缓存

  • email:发送各种类型邮件

  • task:基础定时任务

  • quartz:动态管理定时任务

  • xxl-job:分布式定时任务

  • swaggerAPI 接口管理测试

  • security:基于 RBAC` 的动态权限认证

  • SpringSessionSession 共享

  • Zookeeper:结合 AOP 实现分布式锁

  • RabbitMQ:消息队列

  • Kafka:消息队列

  • websocket:服务端推送监控服务器运行信息

  • socket.io:聊天室

  • ureport2:中国式报表

  • 打包成 War 文件

  • 集成 ElasticSearch:基本操作和高级查询

  • Async:异步任务

  • 集成Dubbo:采用官方的starter

  • MongoDB:文档数据库

  • neo4j:图数据库

  • Docker:容器化

  • JPA 多数据源

  • Mybatis 多数据源

  • 代码生成器

  • GrayLog:日志收集

  • JustAuth:第三方登录

  • LDAP:增删改查

  • 动态添加/切换数据源

  • 单机限流:AOP + Guava RateLimiter

  • 分布式限流:AOP + Redis + Lua

  • ElasticSearch 7.x:使用官方 Rest High Level Client

  • HTTPS

  • Flyway:数据库初始化

  • UReport2:中国式复杂报表


项目地址


https://github.com/xkcoding/spring-boot-demo

运行使用


开发环境



  • JDK 1.8 +

  • Maven 3.5 +

  • IntelliJ IDEA ULTIMATE 2018.2 + (注意:务必使用 IDEA 开发,同时保证安装 lombok 插件)

  • Mysql 5.7 + (尽量保证使用 5.7 版本以上,因为 5.7 版本加了一些新特性,同时不向下兼容。项目会尽量避免这种不兼容的地方,但还是建议尽量保证 5.7 版本以上)


代码导入



  1. 使用 git 克隆代码:


git clone https://github.com/xkcoding/spring-boot-demo.git

2. 使用 IDEA 打开 clone 下来的项目



  1. 找到各个 ModuleApplication 类就可以运行各个 demo



注意:



  1. 每个 demo 均有详细的 README,运行 demo 之前记得先看看

  2. 有些 demo 需要事先初始化数据库数据的



模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


最后


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


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

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


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


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



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

和斯坦福博士写代码的一个月

近一个月,闭关开发了一个面向海外的项目,合作的对象是斯坦福和麻省理工的博士,很荣幸能够和这些全球顶尖大学毕业的大牛合作,今天,趁着周末总结下自己的一些感受。 1. 英语是硬伤 因为项目是面向海外,整个合作全程是英语,这就一览无遗地暴露出自己的英文短板,特别是口...
继续阅读 »

近一个月,闭关开发了一个面向海外的项目,合作的对象是斯坦福和麻省理工的博士,很荣幸能够和这些全球顶尖大学毕业的大牛合作,今天,趁着周末总结下自己的一些感受。


1. 英语是硬伤


因为项目是面向海外,整个合作全程是英语,这就一览无遗地暴露出自己的英文短板,特别是口语,在技术评审过程和讨论中,自己捉襟见肘的英文,只能不断的通过技术流程图和文字来弥补口语表达的不足。


最开始合作时比较羞涩,毕竟知道自己的英语有几斤几两,后面慢慢的放开,把自己的英语老本和盘托出,不过这也坚定了自己加强英文的决心,毕竟做程序开发,英语功底直接决定你理解英文资料的速度和程度。


2. 编程语言要多样


我自己最常用的是 java,但这次项目开发的主要语言是python,另外还涉及到C/C++, java, nodejs。尽管python,C/C++都有所了解,但是,要像 Java一样轻松驾驭去开发这么庞大的工程还是有点吃力。而在合作的过程中,发现他们语言的全能性,python,Java,C++,前端语言,他们都可以平滑切换,绝对的全栈工程师。


这个或许跟每个国家IT环境不一样有很大的关系,但是,作为 Java程序员,我个人还是比较建议再掌握一门动态语言,首选是python,像目前比较主流的数据分析、机器学习、科学计算、自动化和游戏开发等,Python绝对功不可没。另外,通过两种语言的对比,你能更好的看清每种语言的优点和不足,在日常开发中,或许就能借鉴另外一种语言的优点。


3. CR的重要性


CR,就是我们常说的Code Review(代码审查)。在国内的公司工作了这么多年,会做 CR的公司很少,包括一线大厂,能严格执行 CR的更是微乎其微,很多人说是因为业务太多,没有时间做CR,但是,究其原因是国内没有 CR的基因,技术管理者不推动,底层开发人员借着业务繁忙的理由,所以 CR就形同虚设。


下面给了几张项目CR的截图,因为涉及到商业隐私,所以部分信息被打码了:


图一:对于错误的实现,CR 会指正和给出正解。


图片


图二:展示了 CR 甚至细化到了Doc文档级别。


图片


图三:展示了 CR 过程中的探讨,给出自己意见的同时也征询组员更多的idea


图片


CR看似很费时,反反复复的修改和讨论,但是,它和技术方案的讨论不一样,CR针对的是技术方案已经成型的代码实现,所以这个过程更能体现出不同技术人员对同一个技术方案的的思考和实现方式,对于整个项目的质量有大大的提高,另外对于开发者之间的磨合是一个很好的润滑剂。


4. 包容


越优秀的人越懂得包容。这句话放到这个项目上最合适,一个是对于我英语水平的包容。一个是对我 Python 掌握程度的包容。通过上面部分 CR截图也能体现出来。有了这些包容,让我可以更快,更有信心的融入到项目开发。


5. 较真


对于代码实现,技术方案的较真,通过上面的 CR截图也可以体现,他们对于代码的较真可以小到一个Doc文档和标点符号,以及代码的格式化,对于代码性能可以通过大量的技术测试和对比,甚至这块代码不是他们负责的,这个或许就是我们说的技术洁癖和极客精神。


依然记得几年前的一个技术峰会上,某大牛说:中国的码农千千万万,如何让你的代码有亮点,那就是不断的抠细节,大到技术架构,小到标点符号,然后不断的重构,重构。


总结

  1. 通过这个项目,让我有幸见识了从 Google,AWS出来的优秀程序员的编码习惯。
  2. 学英语绝对不是崇洋媚外,它在一定程度上绝对了程序员的技术高度和深度,可以观察你身边优秀的技术人员,英语是不是都不差。
  3. 一定要向优秀的人靠近,和他们一起写代码,勇敢的让他们帮你CR,在交流和讨论中获取更多的葵花宝典;
  4. 程序员是一个终身学习的职业,一定要持续学习;


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

你看现在的程序员,是怎么解bug的

大家好,我是伍六七。 干程序员的都会碰到各种各样的 bug,有的 bug 我们看一眼、或者多看几眼就能看出来问题。 但是,也有很多问题,我们看不出来,这个时候,我们不妨把我们的代码扔给 GPT,看看它能不能帮我们找到 bug,Fix the bug! 我们来看...
继续阅读 »

大家好,我是伍六七。


干程序员的都会碰到各种各样的 bug,有的 bug 我们看一眼、或者多看几眼就能看出来问题。


但是,也有很多问题,我们看不出来,这个时候,我们不妨把我们的代码扔给 GPT,看看它能不能帮我们找到 bug,Fix the bug!


我们来看个例子:


阿七来到这个公司之前,一直在传统 IT 公司工作,从来没用过什么分布式锁、多线程等高端技术。


这次,有一个业务场景,需要使用分布式锁解决问题。阿七按照自己的了解,写了一个分布式锁,结果死活有问题。


看了半天,头都挠凸了,就是不知道怎么回事,这个时候,他想到了大模型,他把问题扔给了 GPT。


prompt:



long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
redisLockService.lock(username + currentTimeMillis, sequenceId, 2, TimeUnit.SECONDS);
---
请你分析上面的Java代码,如果有bug,请指出来,并给出正确写法



大模型返回:


这段代码的目的是使用 Redis 锁来对某个资源进行加锁操作。下面是对代码的分析:




  1. 获取当前时间的毫秒数,存储在 currentTimeMillis 变量中。




  2. 生成一个随机的 UUID 字符串,存储在 sequenceId 变量中。




  3. 调用 redisLockService.lock 方法,传入参数 username + currentTimeMillissequenceId2TimeUnit.SECONDS,对资源进行加锁操作。




如果存在 bug,可能是以下几个方面:




  1. username 变量的值是否正确,确保它是唯一的且能够标识资源的唯一性。




  2. redisLockService.lock 方法的实现是否正确,确保它能够正确地加锁并设置过期时间。




  3. 锁的释放是否在适当的时机进行,以避免资源被长时间锁定。




正确的写法可能如下所示:


long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
String lockKey = username + currentTimeMillis;
boolean lockAcquired = redisLockService.lock(lockKey, sequenceId, 2, TimeUnit.SECONDS);
try {
if (lockAcquired) {
// 执行需要加锁的操作
} else {
// 锁未获取到,处理相应逻辑
}
} finally {
if (lockAcquired) {
redisLockService.unlock(lockKey, sequenceId);
}
}

在正确的写法中,我们首先获取锁,并在获取到锁的情况下执行需要加锁的操作。然后,在 finally 块中,我们释放锁,确保无论是否成功获取到锁,都能够正确释放资源。




一顿瞎扯,就是没找到最严重的那个 bug,我们继续问它。


prompt:



你没有找到这段代码的最严重的 bug,请你再深入分析分析,找到代码中的严重 bug



大模型返回:


非常抱歉,我之前的回答有误。这段代码存在一个严重的 bug,即在分布式环境下,由于锁的 key 值不唯一,可能会导致锁的误解锁或者锁的覆盖,从而导致并发问题。


正确的写法应该是使用 usernamesequenceId 作为锁的 key 值,以确保锁的唯一性。修改后的代码如下:


long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
String lockKey = username + "_" + sequenceId;
redisLockService.lock(lockKey, sequenceId, 2, TimeUnit.SECONDS);
try {
// 执行需要加锁的操作
} finally {
redisLockService.unlock(lockKey, sequenceId);
}

在正确的写法中,我们使用 usernamesequenceId 作为锁的 key 值,以确保锁的唯一性。同时,在 finally 块中,我们释放锁,确保无论是否成功获取到锁,都能够正确释放资源。


这次,我们发现,它找到了代码中最严重的 bug,但是它给出的代码依然没有解决这个 bug,我们继续问它。


prompt;



lockKey 还是没有保证唯一性,并不能解决分布式锁资源不唯一的问题。请你深入思考,再给出正确的设置 lockKey 的代码.



大模型返回:


非常抱歉之前的回答仍然有误。你是正确的,lockKey 的生成方式仍然存在问题,无法保证在分布式环境下的唯一性。


为了确保锁的唯一性,可以使用 Redis 的分布式锁实现方式,例如使用 Redisson 或者 RedLock 等库来实现分布式锁。这些库提供了可靠的分布式锁实现,可以确保在分布式环境下的唯一性和正确性。


下面是使用 Redisson 实现分布式锁的示例代码:


long currentTimeMillis = System.currentTimeMillis();
final String lockKey = "lock:" + username; // 使用固定的前缀加上 username 作为锁的唯一标识
final String sequenceId = UUID.randomUUID().toString();
RLock lock = redissonClient.getLock(lockKey);
boolean lockAcquired = false;
try {
lockAcquired = lock.tryLock(2, TimeUnit.SECONDS);
if (lockAcquired) {
// 执行需要加锁的操作
} else {
// 锁未获取到,处理相应逻辑
}
} finally {
if (lockAcquired) {
lock.unlock();
}
}

在上述代码中,我们使用 Redisson 提供的 RLock 接口来获取分布式锁。通过调用 tryLock 方法来尝试获取锁,设置超时时间为 2 秒。如果成功获取到锁,则执行需要加锁的操作;否则,处理相应的逻辑。在 finally 块中,我们根据是否成功获取到锁来决定是否释放锁。


请确保你已经正确地引入了 Redisson 依赖,并且配置了正确的 Redis 连接信息。


这次,它成功的理解了我们的意思,并且修改了代码,解决了分布式锁资源 key 不唯一的问题。




关注我,送你全套我整理的 Java 岗位面试资料。这是我自己之前整理的面试题,靠着这份面试题,我从 30 人的小公司,进了 2000 人+的央企子公司,之后又进了互联网大厂。


作者:程序员伍六七
来源:juejin.cn/post/7296111218720981027
收起阅读 »

后端除了增删改查还有什么?

大家好,我是伍六七。 刚工作 3、5 年的 Java 程序猿们,在日常工作中开始得心应手,基本上没有什么问题能难倒我们。 这个时候,我们很容易陷入迷茫当中,不知道怎么继续提升自己?怎么才能进阶资深、专家、总监。 知乎上更是有这么一个问题:后端除了增删改查还有什...
继续阅读 »

大家好,我是伍六七。


刚工作 3、5 年的 Java 程序猿们,在日常工作中开始得心应手,基本上没有什么问题能难倒我们。


这个时候,我们很容易陷入迷茫当中,不知道怎么继续提升自己?怎么才能进阶资深、专家、总监。


知乎上更是有这么一个问题:后端除了增删改查还有什么?


我们今天从面试八股文和项目问题,来看这个问题的答案。


面试八股文会问什么?




  • Java 虚拟机 JVM,Java 内存模型 JMM ,垃圾回收算法,垃圾回收器,CMS,G1,ZGC,JVM 调优




  • Java 基础:List,Map,并发包,锁




  • 类加载机制、动态代理、反射、设计模式




  • 多线程、高并发、分布式(分布式原理、分布式锁、分布式事务)




  • sql 优化、索引原理、事务操作




应用层:



  • Spring、MyBatis、Webflux、Redis、Elasticsearch、rpc、DDD 等框架应用以及原理,还有自己的理解。


上面这些东西你会了多少?是知其然还是知其所以然?


通用能力:



  • 操作系统、计算机网络、算法;八大设计原则你能融汇贯通嘛?


架构能力


技术架构、业务架构设计你会多少?能按需设计嘛?


系统的架构为什么要这么设计?解决了什么问题?有没有更好的设计?


学无止境!




或者,我们别那么自信,问一下 GPT :



我是一个Java新手,刚工作 3 年,我认为 Java 后端除了增删改查就没什么了。
请你为我解惑,给出 Java 后端从初级、中级、高级到专家的学习路线



GPT 也会告诉你答案:


Java 后端开发是一个广泛的领域,涉及到许多不同的技术和工具。以下是一个 Java 后端从初级到专家的学习路线,希望能够为您提供一些参考:


初级阶段


在初级阶段,您需要掌握 Java 编程语言的基础知识,包括语法、面向对象编程、异常处理、集合框架等。此外,您还需要了解基本的 Web 开发知识,包括 HTTP 协议、Servlet、JSP等。


以下是一些初级阶段需要学习的技术和工具:


- Java 编程语言


- 面向对象编程


- 异常处理


- 集合框架


- Servlet 和 JSP


- JDBC 和 SQL


- Tomcat 服务器


中级阶段


在中级阶段,您需要深入学习 Java 后端开发的各个方面,包括 Web 框架、ORM 框架、缓存、消息队列等。此外,您还需要了解一些常用的开发工具和技术,例如版本控制、构建工具、测试框架等。


以下是一些中级阶段需要学习的技术和工具:


- Spring 框架


- Hibernate ORM 框架


- MyBatis ORM 框架


- Redis 缓存


- RabbitMQ 消息队列


- Git 版本控制


- Maven 或 Gradle 构建工具


- JUnit 测试框架


高级阶段


在高级阶段,您需要深入了解 Java 后端开发的各个方面,并掌握一些高级的技术和工具,例如微服务、容器化、分布式系统等。此外,您还需要了解一些常用的设计模式和架构模式,例如 MVC、RESTful API等。


以下是一些高级阶段需要学习的技术和工具:


- Spring Boot 微服务框架


- Docker 容器化技术


- Kubernetes 容器编排系统


- Apache Kafka 消息队列


- OAuth2 认证和授权框架


- 设计模式和架构模式


- RESTful API 设计和实现


专家阶段


在专家阶段,您需要成为 Java 后端开发领域的专家,并深入了解一些前沿的技术和工具,例如人工智能、区块链等。此外,您还需要具备一定的架构设计和团队管理能力,能够带领团队完成复杂的项目。


以下是一些专家阶段需要学习的技术和工具:


- 人工智能和机器学习


- 区块链技术


- 大数据和数据分析


- 微服务架构设计和实现


- DevOps 和持续集成/持续交付


- 团队管理和领导力




关注我,送你全套我整理的 Java 岗位面试资料。这是我自己之前整理的面试题,靠着这份面试题,我从 30 人的小公司,进了 2000 人+的央企子公司,之后又进了互联网大厂。


一份让我进大厂&央企的面试题


作者:伍六七AI编程
来源:juejin.cn/post/7296830644113440803
收起阅读 »

揭秘小米手机被疯狂吐槽的存储扩容技术

前段时间,在小米14的发布会上,雷布斯公布了名为“Xiaomi Ultra Space存储扩容”的技术,号称可以在512G的手机中再搞出来16G,256G的手机中再搞出8G。对于普通用户来说,能多得一些存储空间,无异是个很好的福利,不过也有网友说这是以损害存储...
继续阅读 »

image.png
前段时间,在小米14的发布会上,雷布斯公布了名为“Xiaomi Ultra Space存储扩容”的技术,号称可以在512G的手机中再搞出来16G,256G的手机中再搞出8G。对于普通用户来说,能多得一些存储空间,无异是个很好的福利,不过也有网友说这是以损害存储使用寿命为代价的,那么真相到底如何呢?这篇文章我就从技术角度来给大家详细分析下。


认识闪存


首先让我们来了解一些手机存储的基本知识。


手机存储使用的是闪存技术,其本质和U盘、固态硬盘都是一样的。


在闪存中读写的基本单位是页(Page),比页更大的概念是块(Block),一个块会包含很多页。


虽然读写的基本单位都是页,但是写实际操作的很可能是块,这是为什么呢?


这要从删除谈起,在闪存中删除数据时不会立即删除页上的数据,而只是给页打上一个空闲的标签。这是因为谁也不知道这个页什么时候会再写入数据,这样处理起来比较简单快速。


再看写操作,如果写入分配的页是因为删除而空闲的,数据并不能立即写入,根据闪存的特性,此时需要先把页上之前存储的数据擦除,然后才能写入;但是闪存中擦除操作的基本单位是块,此时就需要先把整个块中的有效数据读出来,然后再擦除块,最后再向块中写入修改后的整块数据;这整个操作称为“读-改-写”。当然如果写入分配的页是空白的,并不需要先进行擦除,此时直接写入就可以了。


预留空间


小米这次抠出来的存储空间来源于一个称为“预留空间”的区域,它的英文全称是Over Provisio,简称 OP。


那么“预留空间”是什么呢?我将通过5个方面来介绍它的用途,让大家近距离认识下。


提高写入速度


在上面介绍闪存的基本知识时,我们谈到闪存的写操作存在一种“读-改-写”的情况,因为额外的读和擦除操作,这种方法的耗时相比单纯的写入会增加不少,闪存使用的时间越长,空白的空间越少,这种操作越容易出现,闪存的读写性能下降的越快。


为了提升写入的性能,我们可以先将新数据写入到预留空间,此时上层系统就可以认为已经写入完成,然后我们在后台将预留空间中的新数据和原数据块中需要保留的数据合并到一个新的数据块中,这样就避免了频繁的读-修改-写操作,从而可以大大提高写入速度。


垃圾回收和整理


在上面介绍闪存的基本知识时,我们还谈到删除数据并不是立即清除空间,而是给数据页打一个标签,这样做的效率比较高。这样做就像我们标记了垃圾,但是并没有把它们运走,时间久了,这些垃圾会占用很多的空间。这些垃圾空间就像一个个的小碎片,所以有时也把这个问题称为碎片化问题。


虽然我们可以通过“读-改-写”操作来重新利用这些碎片空间,包括通过异步的“读-改-写”操作来提升上层应用的写入效率,但无疑还是存在写入的难度,实际写入之前还是要先进行擦除。


为了解决上述问题,聪明的设计师们又想到了新办方法:让存储器在后台自动检测、自动整理存储中的数据碎片,而不是等到写入数据时再进行整理。


考虑到闪存的读擦写特性,当需要移除数据块中部分碎片或者将不同数据碎片合并时,就得把需要保留的数据先放到一个临时空间中,以免数据出现丢失,待存储中的数据块准备好之后再重新写入,预留空间就可以用作这个临时空间。


磨损均衡


闪存中每个块的写入次数都是有限制的,超过这个限制,块就可能会变得不可靠,不能再被使用。这就是我们通常所说的闪存的磨损。


为了尽可能延长闪存的使用寿命,我们需要尽量均匀地使用所有的闪存块,确保每个块的使用频率大致相同。这就是磨损均衡的主要目标。


假设我们发现块A的使用频率过高,我们需要将它的数据移动到没怎么用过的块B去,以达到磨损均衡的目的。首先,我们需要读取块A中的数据,然后将这些数据暂时存储到预留空间。然后,我们擦除块A,将它标记为空闲。最后,我们从预留空间中取出数据,写入到块B。实际上,磨损均衡的策略比这更复杂,不仅仅是看使用频率,还需要考虑其他因素,比如块的寿命,数据的重要性等。


可以看到,预留空间在这个过程中起到了临时存储数据的作用。


不过你可能会问,为什么不直接将块A的数据复制到块B,而需要一个临时空间?


这是因为在实际操作中直接复制块A的数据到块B会带来一些问题和限制。


假如直接进行这种数据复制,那么在数据从块A复制到块B的过程中,块A和块B中都会存在一份相同的数据,如果有其他进程在这个过程中访问了这份数据,可能会产生数据一致性的问题。此外,如果移动过程中发生意外中断,如电源故障,可能会导致数据在块B中只复制了一部分,而块A中的数据还未被擦除,这样就可能导致数据丢失或者数据不一致的问题。


而如果我们使用预留空间,也就是引入一个第三方,就可以缓解这些问题。我们先将数据从块A复制到预留空间,然后擦除块A,最后再将预留空间中的数据写入到块B。在这个过程中,我们可以借助预留空间来实现一些原子性的机制,来保证数据不会丢失和数据的一致性。


错误校正


预留空间还可以用来存储错误校正码(ECC)。如果在读取数据时发现有错误,可以用错误校正码来修复这些错误,提高数据的可靠性。


很多同学可能也不了解这个错误校正码的来龙去脉,这里多说几句。


我们知道计算机中的数据最终都是二进制的0和1,0和1使用硬件比较好表达,比如我们使用高电压表示1,低电压表示0。但是硬件有时候会出错,本来写进去的是1,读出来的却是0。为了解决这个问题,设计师们就搞出来个错误校正码,这个校正码是使用某些算法基于要存储的数据算出来的,存储数据的时候把它一起保存起来。读取数据的时候再使用相同的算法进行计算,如果两个校正码对不上,就说明存储的数据出现错误了。然后ECC算法可以通过计算知道是哪一位出现了错误,改正它就可以恢复正确的数据了。


注意ECC能够修正的二进制位数有限,因为可以修复的位数越多,额外需要的存储空间也越大,具体能修复几位要考虑出现坏块的概率以及数据的重要性。


坏块管理


当闪存单元变为坏块时,预留空间可以提供新的闪存单元来替代坏块,此时读取对应数据时不再访问坏块,而是通过映射表转到预留空间中读取,从而保证数据的存储和读取不受影响,提高了固态硬盘的可靠性和耐用性。


综上所述,预留空间在提升固态硬盘性能,延长其使用寿命,提高数据的可靠性等方面发挥着重要的作用。


小米的优化


根据公开资料,小米将预留空间的占比从6.9%压缩到了约3%。


那么小米是怎么做到的呢?以下是官方说法:


小米在主机端也基于文件管理深度介入了 UFS 的资源管理,通过软件实现“数据非必要不写入(UFS)”,通过软件 + 固件实现“写入数据非必要不迁移”,减少写入量的同时也实现了更好的 wear-leveling 和 WAF


还有一张图:



优化解读


这里用了一些术语,文字也比较抽象,我这里解读下:


UFS(Universal Flash Storage)即通用闪存存储,可以理解为就是手机中的存储模块。


“数据非必要不写入(UFS)”也就是先把数据写入到缓冲区,然后等收到足够的数据之后(比如1页),再写入闪存单元,这样就可以减少闪存单元的擦写次数,自然就能延长闪存单元的使用寿命,推迟坏块的产生。这个缓冲区类似于计算机的内存,如果突然掉电可能会丢失一部分数据,但是对于手机来说,突然掉电这个情况发生的几率极低,所以小米在这里多缓存点数据对数据丢失的影响很小,不过还是需要注意缓冲空间有限,这个值也不能太大,具体多少小米应该经过大量测试之后做了评估。


“写入数据非必要不迁移” 没有细说怎么做的,大概率说的是优化磨损均衡、垃圾回收和整理策略,没事别瞎整理,整理的时候尽量少擦写,目的还是延长闪存单元的使用寿命。


“增加坏块预留” 小米可以根据用户的使用情况调整坏块预留区的大小,比如用户是个重度手机使用狂,他用1年相当于别人用4年,小米系统就会增加坏块预留区,以应对擦写次数增加带来的坏块几率增加。注意这个调整是在云端实现的,如果手机不联网,这个功能还用不上。


wear-leveling:就是上面提到的磨损均衡,小米优化了均衡算法,减少擦写。


WAF:写放大,Write Amplification Factor,缩写WAF。写放大就是上面提到的“读-改-写”操作引起的,因为擦除必须擦掉整个块的数据,所以上层系统只需要写一个页的情况下,底层存储可能要重写一个块,从页到块放大了写操作的数据量。因为闪存的寿命取决于擦除次数,所以写放大会影响到闪存的使用寿命。


概括来说就是,小米从存储的预留空间中抠出来一部分作为用户存储,不过预留空间的减小,意味着坏块管理、错误纠正等可以使用的空间变小,这些空间变小会减少存储的使用寿命,所以小米又通过各种算法延缓了手机存储的磨损速度,如此则对大家的使用没有什么影响,而用户又能多得一些存储空间。


小米的测试结果


对于大家担心小米手机存储的寿命问题,小米手机系统软件部总监张国全表示:“按照目前重度用户的模型来评估,在每天写入40GB数据的条件下, 256GB的扩容芯片依然可以保证超过10年, 512GB可以超过20年,请大家放心。”


同时一般固态硬盘往往都拥有5年的质保,而很多消费者往往会5年之内更换手机。因此按着这个寿命数据来看,普通消费者并不用太担心“扩容芯片”的寿命问题。所以如果你的手机用不了10年,可以不用担心这个问题。


当然更多的测试细节,小米并没有透漏,比如读写文件的大小等。不过按照小米的说法,存储的供应商也做了测试,没有什么问题。这个暂时只能相信小米是个负责任的企业,做好了完备的测试。




最后小米搞了这个技术,申请了专利,但是又把标准和技术方案贡献给了UFS协会,同时还要求存储芯片厂商设置了半年的保护期,也就是说技术可以分享给大家,但是请大家体谅下原创的辛苦,所以半年后其它手机厂商才能用上。


大家猜一下半年后其它手机厂商会跟进吗?


关注微/信/公/众/号:萤火架构,提升技术不迷路。


作者:萤火架构
来源:juejin.cn/post/7297423930225639465
收起阅读 »

无悬浮窗权限实现全局Dialog

有些场景下需要显示一些提示弹窗,但把握不好弹出时机容易先弹出弹窗然后界面马上被杀掉进而看不到提示内容,例如强制下线:客户端退回登录界面并弹出提示弹窗。 如果是直接拿的栈顶activity去弹出,没有将弹窗逻辑写到具体activity中,或不好确定activty...
继续阅读 »

有些场景下需要显示一些提示弹窗,但把握不好弹出时机容易先弹出弹窗然后界面马上被杀掉进而看不到提示内容,例如强制下线:客户端退回登录界面并弹出提示弹窗。


如果是直接拿的栈顶activity去弹出,没有将弹窗逻辑写到具体activity中,或不好确定activty的变化就容易出现这种现象。


由于applicationContext没有AppWindowToken,所以dialog无法使用applicationContext创建,要么就使用windowManager配合WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY使用创建全局悬浮窗。但是这种做法需要申请权限。那么,在没有悬浮权限情况下如何做到让dialog不受栈顶activity变化的影响?


我的想法是通过application.registerActivityLifecycleCallbacks在activity变化时,关闭原来的弹窗,并重新创建一个一样的dialog并显示。


效果演示:


1. 栈顶界面被杀


界面退出

2. 有新界面弹出


界面退出

以下是代码实现:


/**
* @Description 无需悬浮权限的全局弹窗,栈顶activity变化后通过反射重建,所以子类构造方法需无参
*/

open class BaseAppDialog<T : ViewModel>() : Dialog(topActivity!!.get()!!), ViewModelStoreOwner {

companion object {
private val TAG = BaseAppDialog::class.java.simpleName
private var topActivity: WeakReference<Activity>? = null
private val staticRestoreList = linkedMapOf<Class<*>, Boolean>() //第二个参数:是否临时关闭
private val staticViewModelStore: ViewModelStore = ViewModelStore()

@JvmStatic
fun init(application: Application) {
application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
topActivity = WeakReference(activity)
}

override fun onActivityStarted(activity: Activity) {

}

override fun onActivityResumed(activity: Activity) {
topActivity = WeakReference(activity)
val tempList = arrayListOf<BaseAppDialog<*>>()
val iterator = staticRestoreList.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
val topName = (topActivity?.get() ?: "")::class.java.name
if (next.value == true) { //避免onCreate创建的弹窗重复弹出
val newInstance = Class.forName(next.key.name).getConstructor().newInstance() as BaseAppDialog<*>
tempList.add(newInstance)
Log.e(TAG, "重新创建${next.key.name},于$topName")
iterator.remove()
}

}

tempList.forEach {
it.show()
}

if (staticRestoreList.size == 0) {
staticViewModelStore.clear()
}
}

override fun onActivityPaused(activity: Activity) {
}

override fun onActivityStopped(activity: Activity) {

}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
}
})
}
}


var vm: T? = null

init {
val genericClass = getGenericClass()
if (vm == null) {
(genericClass as? Class<T>)?.let {
vm = ViewModelProvider(this)[it]
}
}

topActivity?.get()?.let {
(it as LifecycleOwner).lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
dismissSilent()
}
})
}
}


//用于栈顶变化时的关闭
private fun dismissSilent() {
super.dismiss()
staticRestoreList.replace(this::class.java, true)
}

override fun show() {
super.show()
staticRestoreList.put(this::class.java, false)
}

override fun dismiss() {
super.dismiss()
staticRestoreList.remove(this::class.java)
}


//获取泛型实际类型
private fun getGenericClass(): Class<*>? {
val superclass = javaClass.genericSuperclass
if (superclass is ParameterizedType) {
val actualTypeArguments: Array<Type>? = superclass.actualTypeArguments
if (!actualTypeArguments.isNullOrEmpty()) {
val type: Type = actualTypeArguments[0]
if (type is Class<*>) {
return type
}
}
}
return ViewModel::class.java
}


//自己管理viewModel以便恢复数据
override fun getViewModelStore(): ViewModelStore {
return staticViewModelStore
}
}

参数传递的话,直接通过修改dialog的viewmodel变量或调用其方法来实现。


class TipDialogVm : ViewModel() {
val content = MutableLiveData<String>("")
}


class TipDialog2 : BaseAppDialog<TipDialogVm>() {

var binding : DialogTip2Binding? = null

init {
binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.dialog_tip2, null, false)
binding?.lifecycleOwner = context as? LifecycleOwner
binding?.vm = vm
setContentView(binding!!.root)

}
}

弹出弹窗


TipDialog2().apply {
vm?.content?.value = "嗨嗨嗨"
}.show()

作者:Abin
来源:juejin.cn/post/7295576843653087266
收起阅读 »

游戏服务器搭建过程中Maven多模块编译遇到的一些问题

游戏服务器有好几个项目,所以涉及到多个模块,这个开发中也是经常遇到的,但是因为之前一直没怎么关注,所以在开发的过程中的一些细节问题还是不懂,这次记录下,也分享下同样的问题给没试过的同学 1、多模块的创建 使用idea进行模块的创建,主要分为以下几步 1.1 父...
继续阅读 »

游戏服务器有好几个项目,所以涉及到多个模块,这个开发中也是经常遇到的,但是因为之前一直没怎么关注,所以在开发的过程中的一些细节问题还是不懂,这次记录下,也分享下同样的问题给没试过的同学


1、多模块的创建


使用idea进行模块的创建,主要分为以下几步


1.1 父模块的创建


直接创建一个Maven项目,在这个过程中选择了Spring web 和Lombok,也可以选择其他的包,到时候省的手写


image.png


1.2 删除垃圾文件


删除所有的文件,只留下pom.xml就行,因为父模块只是做一个模块和依赖管理的作用,因此不需要代码。


image.png


1.3 修改pom.xml


修改这个父模块的pom.xml文件,首先把节点、节点和全部删除:然后修改版本号为自己定义的(方便后续子模块指定父模块)


    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>MultMoudle</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>MultMoudle</name>
<description>MultMoudle</description>
<packaging>pom</packaging>
</project>


1.4 创建子模块继承


继承父模块


image.png


可以看到父模块的pom中,已经有子模块的配置了。


2、子模块之间的互相引用


因为有一些接口文件需要在几个项目中共同使用,所以就要抽取相同的文件到common项目中,这个是非常合理的,在room和game中怎么引用呐,非常简单,和其他的jar包一样


    
<dependency>
<groupId>com.pdool</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

使用坐标和版本进行引用,刷新引用就可以在引用中看到了


image.png


3、多个模块间版本的管理


多个模块中使用的依赖很有可能会有重复,但是怎么管理各个版本呐?


假设上述module-one和module-two都需要依赖fastjson2,我们之前会在两个模块的pom.xml中加入依赖,但是这样重复的配置看起来似乎不太优雅,如果需要升级要改2个地方,这个时候parent模块的管理功能就可以发挥了


3.1 dependencis


image.png


在上图中,在dependencis中加入了两个常用的工具库,在子模块中即使不加入也可以使用这个库了!因为子模块除了可以使用自己的依赖之外,还会向上查找父模块的依赖,也就是说,父模块的依赖是向下继承的,因此对于所有模块都要使用的依赖,我们可以写在父模块中。


所以,两个模块都依赖于Spring Web话,也可以将两个模块的Spring Web依赖移至父模块。


所以说父模块和子模块中,依赖也有着继承的关系!父模块的properties也是向下继承的。


3.2 dependencyManagement


dependencyManagement用于管理依赖的版本,我们在父模块的pom.xml加入这个标签:


image.png


dependencyManagement的注意事项:


dependencyManagement仅用于管理版本,而不会为自己以及子模块导入依赖,因此在dependencyManagement中声明依赖后,对应的子模块仍然需要在dependencies中加入依赖
在pom.xml中dependencyManagement和dependencies同级,并且dependencyManagement中也需要有一个dependencies
dependencyManagement不仅可以管理子模块的依赖版本,也可以管理自身的依赖版本
若不想让某个子模块使用父模块dependencyManagement的版本,那就在这个子模块的dependencies中声明对应版本
4、依赖执行
因为在项目中使用了mybatis-plus ,在common模块中定义了一些mapper,在room和game中需要使用,在项目中增加了componentscan ,但是依然在运行的时候报错,提示找不到common中的类,在编辑器中并不报错,这很奇怪,需要在依赖项目中增加一个标签。


这是由于Spring Boot打包的模式问题,我们打开被依赖模块module-two的pom.xml文件找到最下面节点中,在spring-boot-maven-plugin插件部分中加入下面配置:



exec
最终common中pom的定义



    
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>execute</classifier>
</configuration>
</plugin>
</plugins>

</build>

5、在Spring Boot项目中加载依赖项目的组件有几种常用的方法


在Spring Boot项目中加载依赖项目的组件有几种常用的方法:


5.1. 使用@ComponentScan注解:


在主应用程序类上使用@ComponentScan注解,指定要扫描的包路径。这将使Spring Boot扫描并加载依赖项目中带有@Component、@Service、@Repository等注解的组件。例如:


    
@SpringBootApplication
@ComponentScan(basePackages = "com.example.dependencyproject")
public class MyApplication {
// ...
}

2. 使用@Import注解:


在主应用程序类上使用@Import注解,引入依赖项目中的配置类或组件类。这将使Spring Boot加载这些配置类并注册其中的组件。例如:


     @SpringBootApplication
@Import(com.example.dependencyproject.MyConfiguration.class)
public class MyApplication {
// ...
}

3. 使用@Configuration注解和@Bean方法:


如果依赖项目中有@Configuration注解的配置类,可以在主应用程序类中使用@Bean方法来加载其中的组件。例如:


    
@SpringBootApplication
public class MyApplication {
// ...

@Bean
public MyComponent myComponent() {
return new MyComponent();
}
}

这样,MyComponent将作为一个Bean被加载到Spring应用程序上下文中。


根据你的具体情况和依赖项目的结构,你可以选择适合的方法来加载依赖项目的组件。请注意,为了能够加载依赖项目的组件,确保依赖项目已被正确地添加为项目的依赖项,并且在构建和部署过程中能够被正确地引用和访问。


6、@ComponentScan 扫描依赖项的包


@ComponentScan 注解可以用于指定要扫描的包,它的作用不限于只扫描依赖项的包。@ComponentScan`可以扫描指定包及其子包下的组件,并将其加载到应用程序的上下文中。


当你在 Spring Boot 应用程序的主类上使用 @ComponentScan 注解时,它将扫描指定的包及其子包,并注册在这些包中使用了 @Component@Service@Repository@Controller 等注解的组件。


如果你指定的包路径包括了依赖项的包,那么它将扫描并加载依赖项中的组件。但是需要注意,@ComponentScan 不会限制只扫描依赖项的包,它将扫描所有指定的包路径下的组件。


举个例子,假设你的 Spring Boot 应用程序的主类上使用了以下的 @ComponentScan 注解:


    @SpringBootApplication
@ComponentScan(basePackages = {"com.example.myapp", "com.example.dependency"})
public class MyApplication {
// ...
}

在上述示例中,@ComponentScan 将扫描 com.example.myapp 包及其子包以及 com.example.dependency 包及其子包下的组件,并将它们加载到应用程序的上下文中。这样,你可以同时加载依赖项中的组件和应用程序本身的组件。


总之,@ComponentScan 注解不仅限于扫描依赖项的包,它可以扫描指定包及其子包下的所有组件,并将它们加载到应用程序的上下文中。


注:


如果你在 Spring Boot 应用程序的主类上使用了 @ComponentScan 注解,并指定了包路径,只有在指定的包路径下的本项目组件会被自动加载到应用程序的上下文中。


@ComponentScan 注解仅扫描指定的包及其子包下的组件,并将它们加载到应用程序的上下文中。如果本项目中的组件不在指定的包路径下,它们将不会被自动加载。


7、总结


不做不知道,手高眼低不行,必须得实践


作者:香菜菜
来源:juejin.cn/post/7297848688244441122
收起阅读 »

qiankun?这次我选了wujie!

web
写在最前: 本文面向对无界和微前端有一定了解的人群,不再对微前端的概念和无界的基础使用方式做解释说明 前言 掘金上搜wujie,那么大眼一看,好像全都是介绍的,并没有几个落地方案的分享。正好我上个月把部门内三个业务系统用wujie整合了一下,记录成文章和大...
继续阅读 »

写在最前:



本文面向对无界和微前端有一定了解的人群,不再对微前端的概念和无界的基础使用方式做解释说明



前言


掘金上搜wujie,那么大眼一看,好像全都是介绍的,并没有几个落地方案的分享。正好我上个月把部门内三个业务系统用wujie整合了一下,记录成文章和大家分享一下。(为什么不用qiankun?qiankun之前做了好多次了,这次想尝个鲜~)


背景说明


笔者部门内有三个管理系统,技术栈分别是:


A: Vue2 + Webpack4 + ant-design-vue@1.7.8:该项目是部门内“司龄”最长的,从部门成立之初起,所有的业务都堆在里边。


B: Vue3 + Webpack5 + ant-desgin-vue@3.2.20:由于业务目标不清晰以及前端开发各自为战,部分需求被拆出来了一个单独的项目进行开发,但实际上然并卵。


C: Vue3 + Vite2 + ant-design-vue@3.2.20:为了响应领导“统一前端UI规范”和“低代码降本增效”的号召,这个项目应运而生,使用JSON Scheme渲染列表页 + 手写Form表单的形式开发需求。


没错,就是3个纯业务向的管理系统。对接我们部门的大部分业务人员,日常都至少需要操作3个系统,甚至有些人还会用到别的部门的系统,甚至有的人习惯打开多个浏览器tab页来回切换对比同个页面的数据。。。poor guy。。。浏览器密密麻麻的全是tab页。。。


契机


某天,发生了如下对话:



  • 领导:业务部门老大说,系统间来回切换太麻烦了,有没有办法解决这个问题?

  • 我:有,微前端。

  • 领导:之前XXX不是用qiankun做过吗,问题很多,不了了之了。

  • 我:我看过他的代码,没有什么大问题,都是一些细节方面的小bug,而且还有别的微前端方案可以选择。

  • 领导:行,你安排一下,尽快上线

  • 我:好的。( 打工人被安排任务就是这么朴实,无华,且枯燥。。。)


为什么选择无界?


(此处省略万字长文对比分析qiankun、micro app、single-app...)


直接摆出站在个人角度以及团队技术、业务背景下选择无界的原因:



  1. 喜欢吃螃蟹:之前有过多次qiankun的落地经验,直接上qiankun,一点都不酷。(第一次了解到无界是22年的10月份左右,彼时的无界还在beta版,想尝尝鲜。况且就算使用无界出了岔子,也有信心能cover住)

  2. 子应用改造,侵入程度低:就像文档中宣传的那样,我用公司的项目跑demo,除去登录态的因素外,基本可以说是0改动接入,当时脑海中只有2个字----牛X!(当然,仅仅这样接入,离上生产的标准还相距甚远;而且最后我还是选择了类似qiankun根据宿主应用动态选择layout的布局方案,改造成本也可以说是不算低了,这个暂且按下不表)

  3. 方便独立开发、部署:与第2点相似但又不同:现有的项目有独立的域名、部署方案、且在生产环境已经稳定运行,在保留这些基础的前提下,无界的iframe方案算是最理想的出路(另外也有一点私心,如果生产环境的无界挂了,业务人员可以直接使用老的域名访问独立的子应用进行业务操作,毕竟出了生产事故是要通报批评的)


综上所述,确实没经过太多深思熟虑,想用就用,干就完了image.png


干货区


下面,就是在我接入文章开头提到的3个系统后,总结出来的大致接入步骤:



  1. 准备主应用,在接入第一个系统之前,不出意外的要先准备宿主应用。

  2. 子系统登录态管理

  3. 根据宿主环境,选择layout方案

  4. 安装wujieEventBus(基于无界去中心化的通信系统做的二次封装)

  5. 子应用afterMount生命周期

  6. 子系统网络请求管理

  7. UI组件定位修复

  8. 公共状态提升


1.准备主应用


一个比较常规、纯净的管理系统,没有过多的封装,因为宿主应用本身,也不需要什么内容。技术栈为Vue3 + Vite2 + ant-design-vue@3.2.20(没错,和系统C的技术栈一致,主打的就是一个偷懒),放张目录结构大家就明白了,没什么特殊的,有些细节后边会提到。


image.png

2.子系统登录态管理


简单来说,对于一个子应用,无论你是基于JWT还是Cookie的用户鉴权方案,在他单独运行时发生登陆态失效的情况,是要被redirect到自己的Login页面去;而当集成到了无界中运行的时候,登录态失效则应该被redirect到主应用的Login页面。


一般情况下,有两个地方需要做处理:



  1. http响应拦截,以axios为例:


if (response.status === 401) {
if (window.__POWERED_BY_WUJIE__) {
wujieEventBus.$emit("LOGIN_EXPIRED", APP_NAME_IN_WUJIE);
} else {
message.error("登录失效,请重新登录");
router.replace("/login");
}
}

window.__POWERED_BY_WUJIE__是无界注入到子应用window当中的一个全局变量。


wujieEventBus是我对无界自带的去中心化通信方式eventBus的封装,具体内容放在第四点展开讲,这里只需要知道,是通知主应用“我”登录失效了,并且附上“我”在主应用中的身份标识(对应组件方式使用无界的<WujieVue />所需的name属性)



  1. 路由守卫:可根据你的需要更改路由钩子,这里以beForeEach为例:


router.beforeEach((to, from, next) => {
if(validToken()) {
// some your logic ...
next();
}else {
wujieEventBus.$emit("LOGIN_EXPIRED", APP_NAME_IN_WUJIE);
}
}

当然,通过路由守卫拦截下登录态失效的情况可能很少很少,但操作和上面是一样的:通知主应用“我”登录失效了,并且附上“我”在主应用中的身份标识


3.根据宿主环境,子应用动态选择layout方案


如果你的主应用布局是打算这样:


8f1fb5771d3992651707926b38a8e5d.png


子应用甚至不用切换layout方案,在下方content区域中保留子应用所有的模块;上方的Menu区作为一个应用级的切换菜单。


但如果你的主应用是打算像这样常规布局:


b144f129bd3ba973060ee43d6273b1c.png
想实现应用级的切换,大体上有三种思路:



  1. 主应用不设任何layout模块:即Header、Menu、Content全都是子应用的模块。那么就需要所有子应用都是这种布局,且每个子应用的Menu菜单都必须是所有应用菜单的集合,当切换到非自身的路由时,与宿主通信进行应用切换。

  2. 与1相同,Header、Menu、Content全都是子应用的模块,但Menu仍是自己的菜单。你问我怎么切换应用?加个position: fixed的悬浮球呗(或类似的可折叠菜单)。


bb92fd682e09d744792a370c513d41c.png


通过hover悬浮球,展开/折叠菜单,点击进行应用切换。


说实话,这方案我自己都不相信有人会用。950fcc41577cd669da1b68e54714ad8.png



  1. 而第三个,也就是我选择的方案:主应用设有Header和Menu,剔除所有子应用的Header和Menu,只保留子应用的Content模块接入进来。熟悉吗?就是接qiankun那套。


大概长这样:


<template v-if="!isInWujieContainer">
<Menu />
<Layout>
<Header />
<Layout>
<keep-alive>
<router-view />
</keep-alive>
</Layout>
</Layout>
</template>
<template v-else>
<keep-alive>
<router-view />
</keep-alive>
</template>

// const isInWujieContainer = window.__POWERED_BY_WUJIE__

为什么选择方案3,在我看来:Menu维护在主应用中,相比于对每个子应用的Menu进行侵入式改造,开发成本和维护成本都更小。Header维护在主应用中,可以方便的管理路由栈(面包屑、tab页签,这里多提一下,我的子应用接入方式是保活+sync路由同步)


既然Menu维护在了主应用中,那么问题来了:点击了Menu中的某个菜单,怎么通知子应用跳转到对应的路由?


我们都知道,当无界开启了url sync同步的时候,主应用、子应用的url变化规则是:子应用url发生变化时,子应用的iframe会与主应用进行通信,主应用同步更新url;当页面刷新时,子应用iframe会从主应用的url中读取路由信息,保证子应用路由状态不丢失。但是并没有一种规则是主应用主动发起改变url、并且子应用能同步更新路由的方案。


我的做法其实也很简单,点击主应用Menu中的菜单时,通过wujieEventBus进行广播,对应的子应用收到消息时,切换路由:


// 主应用中点击Menu菜单
export const openChildRoute = (
_router: RouterObj,
app: AppCollection,
) => {
// 通知子应用路由已改变,registerMountedQueue可以理解为给子应用注册一个mounted后需要立即执行的事件,防止出现跳转到一个还未初始化的子应用时,$emit miss的问题。
EventBus.$registerMountedQueue(
app,
"CHANGE_ROUTE",
{ path: _router.path, app }
);

// 更新主应用自己的url和tab页签
router.push(fullPath);
store.commit("tabs/setList", {
fullPath,
name: _router?.name || "",
title: _router?.name,
});
setActiveKey(fullPath);
};

// 子应用收到消息
wujieEventBus.$on("CHANGE_ROUTE", function ({ path, query, app }) {
if (app !== APP_NAME_IN_WUJIE) return;
router.push({ path, query });
});

并且CHANGE_ROUTE这个事件可以是双向的:可以由主应用主动发起,通知子应用改变路由;也可以由子应用主动发起,通知主应用改变url和tab页签的显示状态。


企业微信截图_16991865379344.png


之所以这样设计,是因为我们的系统中存在一种特殊的路由页面,他不存在于Menu菜单中,是必须通过点击页面中的指定按钮才能进入。所以对于这类页面,必须是由子应用主动发起的。


4.安装wujieEventBus


无界提供了一套去中心化的通信方案,去中心化的优点显而易见:



  • 不关心发送方和接收方是谁,可以是不同应用之间通信,可以是一个应用内不同路由通信,可以是一个应用内不同组件通信

  • 可以很方便的一对多通信


但同时也有一个致命的缺点:通信成功的前提是建立在通信双方都online的情况下


假设这样一个场景:用户从站外的某个带参链接进入系统,参数的目的是告诉系统要重定向到指定子应用的指定路由,甚至具体要打开某个弹框。


bb72a5d9b7de765bdf88bd8d089d942.png


正常情况下,主应用判断url参数做跳转的逻辑不管放在哪里,都存在子应用未加载完成的可能性。


(如果你说每个子应用component的afterMount事件里都写一遍,fine,你赢了)


这个时候,只需要对无界的eventBus稍作改动,即可满足需求:


import WujieVue from "wujie-vue3";
import { AppCollection } from "@/constant";
import store from '@/store';
const { bus } = WujieVue;
type EventList = "LOGIN_EXPIRED" | "EVENT_NAME1" | "EVENT_NAME2"; // 一些事件类型涉及到公司业务,这里省去了

type EventBusInstance = {
$emit: (e: EventList, params: Record<string, any>) => void;
$on: (e: EventList, fn: (...args: any[]) => void) => void;
$registerMountedQueue: (
app: AppCollection,
e: EventList,
params: Record<string, any>
) =>
void; // 将事件注册到子应用mount成功的的事件队列中
$cleanMountedQueue: (app: AppCollection) => void; // 清空子应用mount事件队列
};

type Queue = {
[app in AppCollection]?: any[];
};

let instance: EventBusInstance | undefined = undefined;

export default () => {
const queue: Queue = {};
if (!instance) {
instance = {
$emit: (event, params) => bus.$emit(event, params),
$on: (event, fn) => bus.$on(event, fn),
$registerMountedQueue: (app, event, params) => {
const isMounted = store.state.globalState.appMounted[app]; // store中存储了子应用是否mount完成的状态
const fn = () => bus.$emit(event, params);

// 子应用已挂载完成可以直接通信
if (isMounted) return fn();

if (queue[app] && queue[app]!.length) {
queue[app]!.push(fn);
} else {
queue[app] = [fn];
}
},
$cleanMountedQueue: (app) => {
while (queue[app] && queue[app]!.length) {
const fn = queue[app]!.shift();
fn();
}
},
};
}

return instance;
};

为每个子应用都维护一个事件队列,主应用通过$registerMountedQueue注册事件时,若对应子应用已经mount完成,则直接emit进行通信;若子应用没有mount完成,则将注册的事件推入队列中。


子应用afterMount钩子中调用$cleanMountedQueue,清空属于自己的事件队列。


目前根据业务需要,只做了这一点封装,后续有可能会继续补充。


当然前边提到的这个场景,肯定还有许多不同的解决方案,根据自己的项目因地制宜才是最重要的。


5.子应用afterMount生命周期


上边第4点已经提到过,子应用afterMount钩子中要做两件事情:



  1. store中保存自己mount完成的状态。

  2. 调用$cleanMountedQueue清空自己的事件队列。


6.子系统网络请求管理


网络请求管理,主要解决的是跨域问题,分两种:




  • 调用后端服务跨域
    如果你的用户鉴权是基于cookie的,那最方便的就是使用无界推荐的方法:将主应用的fetch自定义改写后传给子应用。如果你的用户鉴权是基于JWT或者你使用了其他的http请求库,赶快买上两杯咖啡贿赂一下运维大佬,给子应用对应的服务配置下Response Header,支持主应用域名的跨域资源共享。但是要切记,生产环境不要使用Access-Control-Allow-Origin: *




  • 请求子应用静态资源跨域




刚才为啥要让买两杯咖啡,因为一杯是改后端服务支持跨域,还有一杯是改前端静态资源服务器(比如Nginx)支持跨域。48d109abb6ffcad175c35c4c8ecf90c.png


至此,你(wo)的无界微前端方案已经落地大半了,不出意外的话,除了个别地方的样式比较古怪,业务流程已经没啥大问题了,下面的工作就是各个页面点一点,修一修奇怪的样式问题。


7.UI组件定位修复


无界官方针对element-plus冒泡系列组件弹出位置不正确的解决方案是给子应用的body添加position: relative,但我这边使用ant-design-vue@1.7.8的项目并不是弹出位置不正确,而是弹出方向不对,只能暂时通过调整组件位置+修改placement的方式见一个改一个。48d109abb6ffcad175c35c4c8ecf90c.png


我这边还有一些使用左弹出的drawer组件也会有问题,起始位置并不是屏幕最左边,而是content区域的最左边。


企业微信截图_16991870384309.png


不知是否是无界的bug,drawer有个fixed定位的包裹容器,按理来说,创建这个包裹容器的时候会使用webcomponent代理的appendChild方法,可以突破iframe的区域限制,但通过审查元素发现,这个position: fixed; left: 0的元素,开始位置还是iframe的左侧。。。导致drawerposition: absolute的主体开始位置也只能是iframe的左侧。但又不是所有的左弹出drawer都有这个问题,很神奇。。。没办法,只好把这些有问题的暂且改为右弹出。。。有解决方案的朋友也可以交流一下。。。


8.公共状态提升


其实从这里开始,就属于优化的范畴了,目前只做了这一趴,后续有其他优化会持续补充。


做公共状态提升的原因,简单来讲就是:除了登录用户的信息以外,我们不同系统中也有着很多相同的枚举数据,这些数据本身也是从同样的接口中读的,存在vuex/pinia中。所以当一个系统独立运行时,他数据获取的逻辑不变;当作为子应用接入了微前端体系中时,只需要从主应用中等待数据同步,不需要自己再调接口去取。


// 主应用
export default () => {
const duties = [
// some http request callbacks
];
duties.forEach(async (d) => {
const { action, type, commition } = d;
const data = await action();
store.commit(commition, data);
bus.$registerMountedQueue(
'APP_NAME', // 业务系统name标识
"SYNC_STATE",
{
type,
data: toRaw(data),
}
);
});
};

// 子应用
const state = {
// a vuex state
}

const mutations = {
// a vuex mutation
}

const actions = {
// a vuex action
}

if(window.__POWERED_BY_WUJIE__){
wujieEventBus.$on("SYNC_STATE", ({ type, data }) => {
const [updateFn, stateKey, ...restPath] = type;
let config = state[stateKey];
if (restPath && restPath.length) {
set(config, restPath, data); // lodash set
} else {
config = data;
}
mutations[updateFn](state, config);
});
}else {
// old logic, init all states by actions
}

结语


这篇文章从开篇到写下结语,中间经历了一整个星期。后半部分整体写的比较仓促,可能有些地方和起笔之初的设想有所出入;并且许多的细节之处涉及到公司业务也没有做过多的说明。有不明白的地方、或者有想交流的同学也可以留言,我会尽可能的做答复。


另外做个说明,其实最开始的时候文章标题叫【无界(wujie-micro)微前端落地方案分享】,后来才改成现在这个名字,原因有二:



  • 这并不是一套完整的落地方案,只是我对我落地整个过程中,值得记录、分享的一些点的总结

  • 原先的名字有种让人一看就不想点进来的感觉


48d109abb6ffcad175c35c4c8ecf90c.png

行吧,第一版先到这里,欢迎真诚交流,但如果你来抬杠?阿,对对对~ 你说的都对~


作者:Elecat
来源:juejin.cn/post/7297592806569164810
收起阅读 »

热爱前端,也没能逃过七年之痒

web
大家好,我是杨成功。 从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。 以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工...
继续阅读 »

大家好,我是杨成功。


从参加工作到今年十月底,我做前端已经整整七年了。都说婚姻有七年之痒,我觉得工作也同样如此。所谓工作的“七年之痒”,即职业倦怠期。我觉得我倒没有倦怠,但感受不一样了。


以前我一直想前端可以干到退休,这是我的理想职业。现在我虽然还是一名前端工程师,但是工作内容已经离前端越来越远了。


以前我觉得做一个骨灰级程序员、掌握各种牛逼的技术是毕生目标;现在我会想人生精彩多样,多尝试一些不一样的事情不也同样有趣?


1-3 年:热爱、探索


我参加工作很早,二十出头。那时候啥也不懂,但是精力旺盛啥也想学,经常写代码到凌晨 2 点也不觉得累。有一部分人选择前端是因为简单,我就是纯粹的喜欢前端。


前端中有很多好玩的东西,比如各种动画、特效,我都非常感兴趣。在工作中常常因为研究出一种“高级”的写法、实现了某个“牛逼”的功能而沾沾自喜。虽然现在看起来很小儿科,但想起来真让人怀念。


我的第一份工作工资很低(<3k),应该比 95% 的前端都低。当时没有经验,心里想着只要能学到东西就成。在那家公司干了一年多,公司用到的技术基本都学了一遍,进步飞快。“又穷又爱”的状态估计以后再也不会有了。


3-5 年:积累、挑战


工作三年多的时候,我换了家公司,带一个前端小团队,每天都扎在项目里。以前总是追求新技术,怎么花哨怎么来。可负责项目后才发现,解决问题和快速产出才是第一位。


当时的前端非常火热,全社会都是跳槽的机会,跳槽等于涨薪。于是面试变得千奇百怪,大家在卷各种原理、源码、八股文,不管面不面试刷题成了必修课。很多开发者们非常讨厌这些东西,但是又不得不去做。


当然也有好处,就是各种新技术层出不穷。虽然很多都是轮子,但确实有不少突破性的技术,帮助传统前端接触到更广的技术面,能做更多的事情。


我没有花大量时间刷面试题,新技术倒是跟了不少,而且很多都用在了项目中。像 JS 原理题、算法题、某些框架的源码之类,我基本没怎么看过;但是像 Node.js、Android、Linux、跨端开发这些,我花了很多的时间研究,因为确实可以解决项目中的问题。


我一直认为我属于“外卷”类型的:Title 一直是前端,但从不认为自己只是一个前端。什么技术都想试试。所以后来我承担过很多攻坚的角色,像服务器、原生 App、音视频等。我发现能让我上头的可能并不是前端,而是搞定一个难题的快感。


得益于这种心态吧,五年内我积累了很多,但我认为收获最大的是习惯了面对挑战。


5-7 年:瓶颈、迷茫


工作五年以上,年龄直逼 30 岁,好像一瞬间就老了,可我总觉得自己还是个孩子。这个时候总会问自己:我的工作有什么意义?我要一直这样下去吗?我想要什么样的生活?


我是在第 6 年的时候感受到了瓶颈。技术方面一直在进步,但对项目的帮助越来越小———项目进入了稳定期。稳定期意味着没有了涨薪的机会,工作重点逐渐从“怎么实现”变成了“怎么汇报”。以前写日报是“汇总成果”,现在变成了“显得有事可做”。


可能任何一家产品成熟的公司都是这样吧,我不习惯,我还在适应阶段。


从今年开始,我最大的迷茫是工作与生活如何平衡。我在北京这几年,大部分精力都扑在了工作上,家人离的很远,每年见个一两次,也没把谈女朋友当回事。想和家人朋友在一块,可工作又不能放弃。成年人说自己不做选择全都要,而我好像只能二选一。


以前一门心思地想靠技术跳槽、进大厂,今年突然觉得没意思。看到很多人被裁员、加班、互卷,我突然想也许现在挺好的呢?双休不加班、领导也 Nice、没有绩效考核、办公室关系也简单。是不是以前自己太浮躁了,没有好好享受当下呢?


所以,要不要继续写代码?还是回老家做别的事?工作上要不要再卷一点?努力攒钱还是趁年轻消费?要不要参加相亲考虑结婚?一连串的问题汹涌而来。


有些问题能想明白,有些问题还是不明白,但更多的是想明白了也做不到。人的成长流失最快的是勇气,可能某天一件意料之外的事情,会让你一下子做出决定。


写了一本书


工作五年之后,我常常会思考一个问题:如果有一天不做程序员了,我还能干什么?


程序员大概都不喜欢社交吧,或者不擅长社交。我特别羡慕大圣老师,他可以把自己的知识通过视频很生动的表达出来。但我就不行,我好像对镜头恐惧,尝试过好多次全身的不自在。


录视频有难度,不过写文章还行。正好积累了很多知识经验,一边总结一边练笔,于是开始写掘金。后来又碰到个机会写书,我就觉得这个更好,可以把这么多年的经验总结浓缩到一本书里。或许可以帮助一些前端朋友快速进阶,或许还能赚点稿费。


这本书名叫 《前端开发实战派》,还在印刷中,估计两个月后就能成书了。


之后怎么走


七年之前觉得我会写代码到 70 岁,直到写不动了为止。七年之后,我最喜欢的工作依然是程序员,但我不再执着于能不能干到 35 岁了。世界还有很多不一样的精彩,我不能把自己困在程序里。


与那些大厂大佬们相比,我赚的不多,心气也不高。没有想过一定要留在大城市,也不觉得以后有了小孩,就一定要奔着“好的教育”和“名校”去卷,太累了。其实只要没有大城市和名校的执念,生活压力也不会那么大。


这样来看,如果有一天我被裁了,其实也没什么可担心的。选择一个离家近的地方,没有大都市的物欲和诱惑,过一些简单轻松的生活,或许并不糟糕。只是身在大城市,面对万千繁华仿佛难以自拔,但你心里好像知道这不是你追求的,却又停不下来。


我有一个预感,可能 30 岁后不再做程序员了,至少不会只埋头钻研技术。做前端这几年让我在各方面成长迅速,不过做久了也有弊端,比如表达能力、社交能力退化,不擅长处理人际关系,不直接接触商业,而这些往往是人生下半场,决定幸福和事业的关键。


但我依然喜欢技术。无论做什么,技术都会是我自己的优势。


我们大老板是技术出身,孩子都上小学了,还经常熬夜帮我们处理技术难题。有次聚会我问他,公司那么多事情要忙,怎么还有精力写代码呢?他说写代码就是我最放松的时候。我不由得一阵佩服,或许这就是技术人的魅力吧。


但在 30 岁之前,我会继续站在技术一线,做一个什么都搞的前端人。


作者:杨成功
来源:juejin.cn/post/7295551745580793919
收起阅读 »

微信内H5页面唤醒App

web
首先,简述一下这个需求的背景,产品希望能够让用户在微信内,打开一个h5页面,然后就能唤醒公司中维护的app,这个是为了能够更好的引流。 唤醒app的三种方案 IOS系统-Universal Link(通用链接) Universal Links可以通过配置指定域...
继续阅读 »

首先,简述一下这个需求的背景,产品希望能够让用户在微信内,打开一个h5页面,然后就能唤醒公司中维护的app,这个是为了能够更好的引流。


唤醒app的三种方案


IOS系统-Universal Link(通用链接)


Universal Links可以通过配置指定域名路径直接唤醒APP,一步到位


具体配置看这篇文章


juejin.cn/post/693761…


遇到的问题:


apple-app-site-association文件放在app域名(假设: my.app.com/)下


{
"applinks": {
"apps": [],
"details": [
{
"appID": "******",
"paths": [ "/abc/*" ]
},
]
}
}

使用Universal Link其实就是跳转到一个页面(中间页),地址:my.app.com/abc/index.h…


根据上面配置,这个地址是已经固定了的,这需要跟app域名保持一致,并且在paths配置里面的目录下,为了能够获取到apple-app-site-association文件


const universalLink = 'https://my.app.com/abc/index.html?redirectUrl=' + window.location.href
location.replace(universalLink);

如果未下载app,则会跳转失败,在中间页中处理,跳转失败后再返回到当前页面。


<script>
function getQueryStringArgs(url, opt) {
const { decode = true, multiple = false } = opt || {};
const args = {};
if (!(typeof url === 'string' && url.includes('?'))) return args;

const arr = url.split('?');
const qs = arr.length === 2 ? arr[1] : '';
if (!(typeof qs === 'string' && qs.length)) return args;

const items = qs.split('&');
for (let i = 0; i < items.length; i++) {
const meta = items[i];
if (!(typeof meta === 'string' && meta.includes('='))) continue;
const item = meta.split('=');
const key = decode ? decodeURIComponent(item[0]) : item[0];
const value = decode ? decodeURIComponent(item[1]) : item[1];
if (Object.prototype.hasOwnProperty.call(args, key) && multiple) {
const temp = args[key];
args[key] = Array.isArray(temp) ? [...temp, value] : [temp, value];
} else {
args[key] = value;
}
}
return args;
}
const { redirectUrl } = getQueryStringArgs(location.href)
if (typeof redirectUrl === 'string' && redirectUrl) {
location.replace(redirectUrl + '?callType=universalLink') // 处理唤醒app失败场景
}
</script>

上面这段逻辑如果直接放在html中,最好先手动转一下ES5语法,然后压缩一下,这样兼容性好,上面这样展示,是为了可读性好。


总结:


ios系统使用Universal Link在微信和浏览器内都能够正常的唤醒App,且兼容性比较好。但是需要注意中间页域名需要跟app域名保持一致;唤醒app的h5链接域名不能跟中间页域名一致。


直接扫二维码进入另一个页面,需要进行点击操作才能跳转,IOS不允许打开页面立刻就跳转。


URL-Schemes


URL scheme是App提供给外部的可以直接操作App的规则。



  • 比如微信提供了打开扫一扫的URL scheme。weixin://dl/scan

  • 比如支付宝提供了转账的URL scheme。alipayqr://platformapi/startapp?saId=20000116

  • 比如知乎提供了打开回答页面的URL scheme。zhihu://answers/{id}


如何找到某个app的URL Scheme呢?可以看下面这篇文章


zhuanlan.zhihu.com/p/53439246


安卓唤醒app呢,就是使用这种方式


比如:安卓开发提供的是


那跳转的链接是什么样的呢?


const schemeURL = 'myapp://www.myapp.apk'
window.href = schemeURL;

如何判断唤醒失败呢?


没有什么好办法来判断,后面只能触发了唤醒操作之后,监听页面几秒之后是否隐藏来判断,目前默认是2秒


export function getSupportedProperty() {
let hidden;
let visibilityChange;

if (typeof document.hidden !== 'undefined') {
// Opera 12.10 and Firefox 18 and later support
hidden = 'hidden';
visibilityChange = 'visibilitychange';
// @ts-ignore
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
// @ts-ignore
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}

return {
hidden,
visibilityChange,
};
}
/**
* 判断页面是否隐藏(进入后台)
*/

export function isPageHidden() {
const ob = getSupportedProperty();
const hidden = ob?.hidden;
if (typeof hidden === 'undefined') return false;
// @ts-ignore
return document[hidden];
}
/**
* 检测是否唤端成功
* 在唤起执行后,当前页面调用此方法根据页面隐藏变化检测是否唤醒成功
* @param {number} timeout 定时时间,默认2秒
* @return {Object} Promise对象
*/

export function checkOpen(timeout = 2000) {
return new Promise((resolve, reject) => {
const ob = getSupportedProperty();
const visibilityChange = ob?.visibilityChange;

const check = () => {
const pageHidden = isPageHidden();
if (pageHidden) {
resolve(); // 页面被隐藏,说明唤醒成功
} else {
reject(new Error('唤醒超时'));
}
};
const timer = setTimeout(() => {
check();
}, timeout);

const fn = () => {
if (typeof visibilityChange !== 'undefined') {
document.removeEventListener(visibilityChange, fn);
} else {
window.removeEventListener('pagehide', fn);
}
check(); // 唤醒执行后,立马触发页面隐藏变化,可检测是否唤醒成功
clearTimeout(timer); // 未到达指定时间,页面隐藏变化,清除定时器
};

if (typeof visibilityChange !== 'undefined') {
document.addEventListener(visibilityChange, fn);
} else {
window.addEventListener('pagehide', fn);
}
});
}

总结:


安卓使用URL Schemes在微信中是不能跳转的,在浏览器中是能够正常拉起。


微信开放标签


由于在微信环境内,所以可以使用微信提供的能力来唤醒app,微信内禁止使用URL Schemes唤醒app,其实就是微信的一种保护机制。


微信文档:


developers.weixin.qq.com/doc/oplatfo…



如上图,使用这个功能,有很多限制,而且需要配置,但是为了安卓用户成功引流,产品还是要求使用这个功能。


微信配置


1.关联App-微信开发平台


微信开发平台配置关联App,关联App需要appId,已经有App的域名


微信开发平台地址: open.weixin.qq.com/



2.H5页面域名配置-微信公众平台


JS安全域名需要配置当前h5页面的域名


微信公众号地址: mp.weixin.qq.com/



3.初始化微信SDK,需要获取签名


微信开发SDK文档


developers.weixin.qq.com/doc/offiacc…


这需要后端开发接口, 去获取签名



使用微信开放标签说明:


developers.weixin.qq.com/doc/offiacc…


async getWxSignatureData() {
const url = window.location.href.split('#')[0];
const res = await getJsapiSignParamers(url);
const { appId, signature, timestamp, nonceStr } = res.data;
wx.config({
debug: false,
appId: appId,
timestamp: timestamp,
nonceStr: nonceStr,
signature: signature,
jsApiList: ['showOptionMenu'], // 必填,故使用一个非实际使用的api用于填充
openTagList: ['wx-open-launch-app'], // 可选,需要使用的开放标签列表
});

wx.ready(() => {
console.info('wx sdk ready');
console.info('调用接口初始化wx sdk 成功');
this.initWxSDKStatus = 'success';
});

wx.error(res => {
console.error('调用接口初始化wx sdk 失败', res);
this.initWxSDKStatus = 'fail';
});
},

接口返回的就是这样的数据结构



只有这样才能正常初始化微信的SDK,只有正常初始化SDK才能够使用微信开放标签的能力。


然后后端开发的时候要注意:签名需要后端配置白名单ip,文档说明如下:


developers.weixin.qq.com/doc/offiacc…



安卓手机,如果出现唤醒app之后,打开了应用,但是并未成功唤起,那是因为Android应用有要求,需要安卓开发兼容一下就行了~



微信环境内场景


接下来就分析一下,在微信中有几种分享的场景:


1.微信好友之间链接分享



这种方式,使用微信标签是不能唤醒App的,除非是在关注公众号里面,这个公众号就是上面绑定了JS安全域名的公众号



这样点击这个链接就能正常用微信标签唤醒


2.微信好友之间卡片分享



这种点击打开是能够正常唤醒App的,而且不需要使用公众号,但是这种分享有限制,需要打开页面点击右上角分享给其他好友会带上卡片形式,如果在浏览器中就只是复制链接了,微信不会自动识别成卡片


而且这个分享其实就是微信的一个功能


developers.weixin.qq.com/minigame/de…


3.长按识别二维码识别H5链接



这种也能正常唤醒App,而且不需要关注公众号,也很方便,不需要将链接分享给其他人,只需要将唤醒App的链接做出二维码就行了。


全部流程图


无标题-2023-11-05-1641.png


作者:0522Skylar
来源:juejin.cn/post/7297526380333400083
收起阅读 »

偷偷给网站写了一个霓虹风格计数器

web
阅读原文,体验更佳 👉 http://www.xiaojun.im/posts/2023-… 有很长一段时间,我都想在博客中集成拟物化的访问计数器用于增加一些趣味性,可是我这网站一开始是纯静态的,没用到任何数据库,所以后边不了了之,但最近我在博客中赋予了一些...
继续阅读 »

2023-10-28-retro-hit-counter.webp


阅读原文,体验更佳 👉 http://www.xiaojun.im/posts/2023-…




有很长一段时间,我都想在博客中集成拟物化的访问计数器用于增加一些趣味性,可是我这网站一开始是纯静态的,没用到任何数据库,所以后边不了了之,但最近我在博客中赋予了一些动态能力,这个想法随之也就又浮现了出来。



这个创意最初来自大佬 Joshua Comeau 开源的 react-retro-hit-counter,但后续我产生了自己的一些想法。




本教程不会涉及任何关于数据库的东西,我假设你已经准备了一个数字,不关心你的数据来源,这里就以 1024 来做演示啦~



认识七段数码管


最初我只想实现一个类似计算器那种数字显示效果,它专业点叫做七段数码管(Seven-segment display),你可以在 wikipedia 上见到具体介绍,它一般长下边这种样子,地球人都见过:


image.png


这种形态还是比较好处理的,让我们先实现这个效果,最终要实现的霓虹灯效果也是以此为基础才行。



以下所有组件皆是用 tailwindcss + react 编写,为了教程简练省略了部分代码,具体请阅读源码



SevenSegmentDisplay 组件开发


开发之前让我们先分析该组件有哪些部分构成,它可以拆分为哪些子组件?



  • 入口组件,也就是父组件,我们将它命名为 SevenSegmentDisplay.jsx

  • 数字单元组件,我们将它命名为 Digit.jsx

  • 数字单元的片段,每个数字有 7 个片段,我们将它命名为 Segment.jsx


SevenSegmentDisplay


作为入口组件,它负责接收所有的 props 配置,并且将传入的 value 分解为单个数字后传给 Digit 组件。


import React, { useMemo } from 'react'
import Digit from './Digit'

const SevenSegmentDisplay = props => {
const {
value, // 要展示的数字
minLength = 4, // 最小长度,不足则前补 0
digitSize = 40, // 数字大小(高度)
digitSpacing = digitSize / 4, // 数字之间的间距
segmentThickness = digitSize / 8, // 片段厚度
segmentSpacing = segmentThickness / 4, // 片段之间的缝隙大小
segmentActiveColor = '#adb0b8', // 片段激活时候的颜色
segmentInactiveColor = '#eff1f5', // 片段未激活时候的颜色
backgroundColor = '#eff1f5', // 背景色
padding = digitSize / 4, // 整个组件的 padding
glow = false, // 微光效果,其实就是阴影效果
} = props

// 将传入的 number 类型数字转为 string 并且根据 minLength 传入的长度进行前补 0
const paddedValue = useMemo(() => value.toString().padStart(minLength, '0'), [value, minLength])
// 将补 0 后的数字转为单个字符
const individualDigits = useMemo(() => paddedValue.split(''), [paddedValue])

return (
<div
className="inline-flex items-center justify-between"
style={{ padding, backgroundColor, gap: digitSpacing }}
>

{individualDigits.map((digit, idx) => (
<Digit
key={idx}
value={Number(digit)}
digitSize={digitSize}
segmentThickness={segmentThickness}
segmentSpacing={segmentSpacing}
segmentActiveColor={segmentActiveColor}
segmentInactiveColor={segmentInactiveColor}
glow={glow}
/>

))}
</div>

)
}

export default SevenSegmentDisplay

Digit


一个 Digit 包含 7 个 Segment,通过控制不同 Segment 的点亮状态,便可以模拟数字显示。


import React from 'react'
import Segment from './Segment'

// Segment 排布规则
//
// A
// F B
// G
// E C
// D
//

const segmentsByValue = {
[0]: ['a', 'b', 'c', 'd', 'e', 'f'],
[1]: ['b', 'c'],
[2]: ['a', 'b', 'g', 'e', 'd'],
[3]: ['a', 'b', 'g', 'c', 'd'],
[4]: ['f', 'g', 'b', 'c'],
[5]: ['a', 'f', 'g', 'c', 'd'],
[6]: ['a', 'f', 'g', 'c', 'd', 'e'],
[7]: ['a', 'b', 'c'],
[8]: ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
[9]: ['a', 'b', 'c', 'd', 'f', 'g'],
}

const isSegmentActive = (segmentId, value) => segmentsByValue[value].includes(segmentId)

const segments = ['a', 'b', 'c', 'd', 'e', 'f', 'g']

const Digit = props => {
const { value, digitSize } = props

return (
<div className="relative w-6 h-8" style={{ width: digitSize * 0.5, height: digitSize }}>
{segments.map(segment => (
<Segment
key={segment}
segmentId={segment}
isActive={isSegmentActive(segment, value)}
segmentThickness={segmentThickness}
segmentSpacing={segmentSpacing}
segmentActiveColor={segmentActiveColor}
segmentInactiveColor={segmentInactiveColor}
glow={glow}
/>

))}
</div>

)
}

export default Digit

Segment


根据 segmentId 以及激活状态用 SVG 渲染出对应的 Segment,这是一个不复杂但是比较繁琐的工作 🤖。


import React, { useMemo } from 'react'
import color from 'color'

const Segment = props => {
const {
segmentId,
isActive,
digitSize,
segmentThickness,
segmentSpacing,
segmentActiveColor,
segmentInactiveColor,
glow,
} = props
const halfThickness = segmentThickness / 2
const width = digitSize * 0.5

const segments = {
a: {
top: 0,
left: 0,
},
b: {
top: 0,
left: width,
transform: 'rotate(90deg)',
transformOrigin: 'top left',
},
c: {
top: width * 2,
left: width,
transform: 'rotate(270deg) scaleY(-1)',
transformOrigin: 'top left',
},
d: {
top: width * 2,
left: width,
transform: 'rotate(180deg)',
transformOrigin: 'top left',
},
e: {
top: width * 2,
left: 0,
transform: 'rotate(270deg)',
transformOrigin: 'top left',
},
f: {
top: 0,
left: 0,
transform: 'rotate(90deg) scaleY(-1)',
transformOrigin: 'top left',
},
g: {
top: width - halfThickness,
left: 0,
},
}

// a, d
const path_ad = `
M ${segmentSpacing} ${0}
L ${width - segmentSpacing} 0
L ${width - segmentThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


// b, c, e, f
const path_bcef = `
M ${segmentSpacing} ${0}
L ${width - halfThickness - segmentSpacing} 0
L ${width - segmentSpacing} ${halfThickness}
L ${width - halfThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


// g
const path_g = `
M ${halfThickness + segmentSpacing} ${halfThickness}
L ${segmentThickness + segmentSpacing} 0
L ${width - segmentThickness - segmentSpacing} 0
L ${width - halfThickness - segmentSpacing} ${halfThickness}
L ${width - segmentThickness - segmentSpacing} ${segmentThickness}
L ${segmentThickness + segmentSpacing} ${segmentThickness} Z
`


const d = useMemo(
() =>
({
a: path_ad,
b: path_bcef,
c: path_bcef,
d: path_ad,
e: path_bcef,
f: path_bcef,
g: path_g,
}[segmentId]),
[path_ad, path_bcef, path_g, segmentId],
)

return (
<svg
className="absolute"
style={{
...segments[segmentId],
// 此处用到了 color 它可以很方便的对颜色进行调整
filter:
isActive && glow
? `
drop-shadow(0 0 ${segmentThickness * 1.5}px ${color(segmentActiveColor).fade(0.25).hexa()})
`
: 'none',
zIndex: isActive ? 1 : 0,
}}
width={width}
height={segmentThickness}
viewBox={`0 0 ${width} ${segmentThickness}`}
xmlns="http://www.w3.org/2000/svg"
>

<path fill={isActive ? segmentActiveColor : segmentInactiveColor} d={d} />
</svg>

)
}

export default Segment

基础效果展示


到此,基础的显示组件已经完成了,让我们测试一下显示效果:


www.xiaojun.im_posts_2023-10-28-retro-hit-counter.png


这是它的配置参数 👇


<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

粗略一看还不错,但这与霓虹效果还相差甚远,因为它看起来有些扁平,边缘过于“锐利”,不够真实,所以接下来的目标是要把它变得更真实拟物一些。



如果你不需要霓虹效果,其实到这一步就足够了 😣,在我的网站中浅色模式也是使用的扁平风格,只有在切换到深色模式才会显示为拟物风格,算是一个小小的彩蛋吧。



霓虹灯效果


先分析一下为什么上边的样式看上去不够真实?



  1. 也许是曝光问题?真实世界中发光物本身相对于它的边缘来说看上去会更亮、更白,并且会稍微模糊一些。

  2. 很多情况下发光源做不到均匀照射到所有地方,所以会产生一片区域亮一片区域稍暗的效果,如果你留意过,很多透字键盘背光灯就是这样。


基于以上两点,接下来就想办法用 CSS 将它模拟的更真实一些。


让我们在 SevenSegmentDisplay 组件的基础上再封装一个 NeonHitCounter 组件。


模拟曝光过度效果


我们可以使用 CSS 中的 backdrop-filter 属性模拟过曝效果。


const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"></div>
</div>

)
}

export default NeonHitCounter

在上边代码中我们新建了一个 div 盖在 SevenSegmentDisplay 上边并使用 badckdrop-filter 使组件变亮变模糊,看上去效果已经好了不少。


image.png


模拟亮度不均匀效果


让我们将组件中间部分变得更亮,用于模拟亮度不均匀的效果。我们可以用 radial-gradient 创建一个白色径向渐变盖在它上边,然后通过 mix-blend-mode 来控制混合模式,这里用 overlay 比较合适。



有关 mix-blend-mode 的更多详细介绍你可以参考这篇文章



const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div
className="absolute inset-0 z-10 mix-blend-overlay pointer-events-none"
style={{
// 通过 luminosity 获取颜色相对亮度如果一个颜色很亮我们则减少亮度增益
background: `radial-gradient(rgba(255, 255, 255, ${
1 - color('#ff5e00').luminosity()
}), transparent 50%)`,
}}
>
</div>
<div className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"></div>
</div>

)
}

export default NeonHitCounter

在上边代码中又创建了一层 div,它利用 radial-gradient + mix-blend-mode: overlay 实现局部颜色增亮,并且根据颜色相对亮度动态判断增益比例,看起来是不是更真实了 👇


image.png



了解相对亮度 👉 developer.mozilla.org/en-US/docs/…



模拟玻璃质感


为了模拟透明玻璃质感,我用 Figma 画了一个 SVG 背景(也可以用 CSS 实现,我偷懒了),另外又用 conic-gradient 实现了 4 颗螺丝效果。


<svg width="76" height="38" viewBox="0 0 76 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.68" clip-path="url(#clip0_467_36)">
<rect width="76" height="38" fill="url(#paint0_radial_467_36)"/>
<rect width="76" height="38" fill="white" fill-opacity="0.01"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-80.0879 0H191.953V272.041H-80.0879V0ZM54.9326 263.211C125.178 263.211 182.124 206.266 182.124 136.021C182.124 65.7744 125.178 8.8291 54.9326 8.8291C-15.3135 8.8291 -72.2588 65.7744 -72.2588 136.021C-72.2588 206.266 -15.3135 263.211 54.9326 263.211Z" fill="url(#paint1_linear_467_36)"/>
</g>
<defs>
<radialGradient id="paint0_radial_467_36" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(38 19) scale(38 19)">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="1" stop-color="white" stop-opacity="0.05"/>
</radialGradient>
<linearGradient id="paint1_linear_467_36" x1="-8.40528" y1="-21.8896" x2="68.8142" y2="-4.89117e-06" gradientUnits="userSpaceOnUse">
<stop offset="0.199944" stop-color="white" stop-opacity="0.26"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_467_36">
<rect width="76" height="38" fill="white"/>
</clipPath>
</defs>
</svg>

import React from 'react'
import SevenSegmentDisplay from '@/components/SevenSegmentDisplay'
import clsx from 'clsx'
import color from 'color'

const Screw = props => {
const { className } = props

return (
<div
className={clsx(className, 'w-[5px] h-[5px] rounded-full ring-1 ring-zinc-800')}
style={{ background: `conic-gradient(#333, #666, #333, #666, #333)` }}
>
</div>

)
}

const NeonHitCounter = () => {
return (
<div className="relative">
<SevenSegmentDisplay
value={1024}
minLength={6}
digitSize={18}
digitSpacing={4}
segmentThickness={2}
segmentSpacing={0.5}
segmentActiveColor="#ff5e00"
segmentInactiveColor="#161616"
backgroundColor="#0c0c0c"
padding="10px 14px"
glow
/>

<div
className="absolute inset-0 z-10 mix-blend-overlay pointer-events-none"
style={{
background: `radial-gradient(rgba(255, 255, 255, ${
1 - color('#ff5e00').luminosity()
}), transparent 50%)`,
}}
>
</div>
<div
className="absolute inset-0 z-10 backdrop-blur-[0.25px] backdrop-brightness-150 pointer-events-none"
style={{
backgroundImage: 'url(/hit-counter-glass-cover.svg)',
backgroundSize: 'cover',
backgroundPosition: 'center',
boxShadow: `
0 0 1px rgba(255, 255, 255, 0.1) inset,
0 1px 1px rgba(255, 255, 255, 0.1) inset
`,
}}
>

<Screw className="absolute left-1 top-1 -rotate-45" />
<Screw className="absolute left-1 bottom-1 rotate-45" />
<Screw className="absolute right-1 top-1 rotate-45" />
<Screw className="absolute right-1 bottom-1 -rotate-45" />
</div>
</div>

)
}

export default NeonHitCounter

大功告成 ✨


image.png


作者:xiaojundebug
来源:juejin.cn/post/7297487761615552564
收起阅读 »

为什么年轻人要珍惜机会窗口

今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。...
继续阅读 »


今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。


什么是好机会


就以职业的成长性来说,互联网整个行业的二十年蓬勃发展就是极好的一个机会,大概从20年起到如今这个时间段都有一个非常好的机会,那指的就是哪怕你的能力稍微弱一点,你都能够在这个机会里面找到自己的红利。比如我有很多稍微找我几届的同事或者主管,他们可能在学历或者能力方面都没有特别高,但是正因为赶上了红利,他们的晋升特别快,拿到了股票也特别多,我好几个同事基本上在上海或者杭州都有两三套房,并且还有大量的现金。甚至有一些大专的同事,都拿到大量的股票,接近财富自由。


所以这种机会窗口是整个行业变革,整个现代社会发展带来的,它打开了一扇可以改变命运的窗口。这种时间窗口相对来说会比较长,特别是相对一个人的职业三十年来说。而且这种行业的机会,可能就有持续五年或者十年这样的时间。而在这样的机会窗口内,你不管是哪个点入局都能吃到一定的发展红利。


比如我记得早个五六年,很多人在找工作的时候,往往会纠结于去百度还是腾讯或者是阿里,但实际上我们发现站在更高,更长远的角度来说,他们选择任何一个公司收获到的都非常的丰厚,相比现在的毕业生,哪怕是双985可能也是无法找到一份工作,想想那时候是不是很幸福?在这种大背景下,在机会窗口来临的时候,你选错了,选的不是很好,都没有关系,你都能够收获到足够的红利,最多就是你赚50万还是100万的区别,而时代没有的话,上限就是赚10万。


除了这个例子之外,还有一个红利机会点就是房地产。我知道在差不多2005年~2018年这个时间段里面,只要你买房基本上都是赚的,所以我很多同学往往都有一个非常巨大的认知论,就认为他买房赚钱是因为他牛逼,他地段选的好,户型选的好,他完全归因于他买的房价大涨是因为眼光好,怎么样怎么样才能赚到钱,而实际上这只是时代给他的红利而已,其实再往回倒个七八年你在哪里买房都是赚的。但实际上以我的经验来看,不管那个时候,哪怕你在小城市买一套房子,涨幅可能都是两三倍的。


所以当时的眼光和认知和选择能力确实会决定了你的资产增值多少,但是只要在那个红利周期内,你做的选择大概率都不会太差,这也是雷军所说,站在风口上的猪也可以飞起来,说的就是这个道理。



这就是整个时代给我们的窗口,这个窗口可能会给的特别大,而且很多时候在这个周期里面,你根本感觉不到这是时代给你的机会,你只是做你正常的操作,到了指定的时间去指定的公司,去选合适热门专业,去买认为合适的房子,你觉得很自然,但实际上从后面再看,你会发现你在十年前做的选择和十年后做的选择成本、难度以及你付出的代价完全不一样。同样是89平米的房子,放在2010年就是3000一平米,放在现在就是8万一平米。同样是去阿里巴巴,以前大专就行,现在本硕985都直接被Pass。


上面说的都是比较大的机会,那我再说一个相对来说比较小的窗口。这些非常大的机会窗口还是依赖于各种不同不一样的大背景,但是有很多机会并没有像这种时代给的机会一样,可以有长达五年,十年你可以认真去选,你可以去大胆的犯错和试错,选错了你重新再来一次就可以了,但是我们在实际工作里面,我们碰到的一些机会点,其实时间窗口非常的短。如果你稍微不慎,可能就错过了这个机会,而等待下一个机会就不知道猴年马月了,所以我们就要在这个地方要抓住那稍纵即逝的机会窗口。



我举一个例子,比如说这两年是低代码的元年,而这个时候如果你之前刚好一直在从事低代码或者低代码相关的工作,那么到了这两年,你的议价空间是非常大的,因为很多公司都在如火如荼的去做这块的业务,在短时间内是没有办法慢慢培养出或者招聘到这类专才,所以往往公司愿意溢价去花费大价钱去购买和招聘相关的同学,所以这个时候如果你抓住了机会,你可以得到一个很高的议价,比如说层级直接变高了一层或者你的总包直接变成了两倍,甚至非常有机会作为骨干负责人拉起一支团队,那么你进入管理岗位也就水到渠成了。


为什么机会有窗口


而这种机会窗口往往只有半年,一年或者最多两年,因为到了一两年之后,有很多的同学也感知到了这个先机,往往就会把自己的精力投到这一块来,那么意味着供需就发生了变化,供应方就会越来越多,那么就使得需求方有溢价的能力,这个时候到了两年之后可能就完全拉平了,这个低代码行业跟其他行业变得完全一样,甚至再往后人才堆积的更加的过分,你可能连这个机会都没有了,只剩下被选择的命运。历史历代,都演绎着完全相同的剧本。


到了直播行业也是一样,在直播刚刚兴起的时候,如果你恰巧做的是相关业务,这个时候你跳过去往往会能够涨薪特别高,工资的幅度也是特别高,所以在这个时候你有充分的议价权,但是窗口我们也知道往往只有几年,而且在互联网这么变化快的情况下的话,时间可能会进一步缩短,比如这两年已经到了直播的红海,基本上该用直播的用户已经到顶了,这个时候虽然还有大把的招聘,但需求实际上已经是强弩之末了。


随着人口红利到底的时候,我们所谓的互联网这些机会的窗口实际上已经是没了,变得普普通通的一份职业而已,而且这个时候入局往往有可能会遭受灭顶之灾,比如说最近就听说到整个直播行业要整顿,一旦业务发生了整顿,对人才的需求的调整就会变得非常的明显,往往再激烈一点可能就会快速裁员,不要说红利了,拿到的全部是负债。


再往小的一些说,可能针对每个人的职业窗口也是不一样的,比如说对于有些大企业,有一些管理的岗位,但往往是因为原管理的同学离职或者新增的岗位,这个时候会有短时间的招聘名额来等待这个位置,而一旦你错过了这个机会以后,这个位置没了以后,可能这个坑位就不需要人了。这个时候不是你能力好不好的问题,是有没有坑位的问题。


所以好机会往往只是一瞬间而已,很多同学担心稳定性,希望在一个地方一直苟着求稳定,这个其实跟体制内没有任何的区别。风险和收益从哲学层面上来说,都是相对的,或者说没有决定的风险,也没有决定的稳定,风险和稳定阶段性只能取其一,长期看稳定和风险是互相转化的。我经常听到有人说大厂稳定,但是实际上我们在分析背后的原因,大厂稳定本身就是个伪命题。又稳定,又高薪,又轻松,这是不可能的。所以我称之为「工作不可能的三角特点」。


但很多人说我能否要里面的两个因素,我要稳定要高薪但是我愿意加班吃苦。


对不起,这个其实也是不可能的。我们可以站在企业的角度来考虑一下,一旦我这个工作特别的高薪又稳定的情况下的话,那虽然你干的很苦,但我始终在人力成本特别充分的情况下的话,公司能找到更好的替代者来。同样的工作量,但是花更少的钱来解决,说白了大部分所谓的高薪岗位没有什么严格的技术壁垒。


所以我们说过的,站在更大的角度来说,互联网也是一个机会窗口,因为过了这个窗口之后,可能你想加班加点熬夜,你可能都拿不到这样的一个薪水和待遇。


如何抓住机会窗口


反而换一个角度来说,我们一定要抓住这样的机会窗口,这样的机会窗口可以给我们的发展带来一个质的变化,当然也有很多时候我们会做一些错误的选择,比如说我们找到了一个我们认为好的机会,但实际上这个机会是有问题的,比如说我去了某一个创业公司,原本以为会有巨大的发展,但是后面倒闭了。当然这个也是一种博弈,这里面非常考核一个同学的综合的认知能力、选择能力和纠错能力。不仅要判断能否找到合适的机会,还要在碰到了困难的时候能够去快速的去纠错。


从我的例子来看,如敢于去挑战这种新机会的同学,哪怕其中有一些不如意的变动,但是大概率他的结果大概率不会太差。比如我有个同学从集团跳槽到蚂蚁国际,呆了一年就觉得部门有问题,后面又去了字节头条,现在也非常稳定。还有一个同学出去创业,也不顺利,但是后面又折腾成了另外一个大型公司的高级主管。


反而是事事求稳,稳住某一个大厂,稳住某一个职位,稳住每一个薪水,到了最后往往收益会越来越小,直到最后完全被动。整体上来看,整个社会会把更多的报酬分向于这些敢于挑战,敢于冒险,敢于拼搏的人的,而不会把大量的资源分享到又稳定,又顽固,又不愿意改变的这群人,这是当前社会的游戏规则。这个在大数据上面完全是合理的,只不过落到每个人的头上的尺度和比例会有点不一样。


所以站在我现在的角度上来看,我觉得所有的想向上奋进的同学都应该主动抓住变革的机会。因为这个好机会可能对在你的人生来说,几十年可能就这么一两次,甚至有些都是完全为你量身定做的机会,如果你一旦错过了以后,可能你抓住下一个机会的成本和代价就变得会非常的大。



尤其是年轻人更应该去折腾,因为你的试错的成本会非常低,当你发现了你的错误决策以后,你能够快速的去更正,去变化,所以在年轻的时候往往就应该多折腾一点,善于去准备好去等待好的机会,如果机会来了,大胆的出击。


作者:ali老蒋
来源:juejin.cn/post/7296865632166805513
收起阅读 »

绝对超乎你想象,程序员一辈子到底能挣多少钱?

薪资追求 作为资深码农,对这个行业还是比较了解的。 大部分年轻程序员可能都只是想着如何提高技术,为拿到更好的Offer尔努力,很多码农到30岁后才突然发现,程序员高薪也就那么回事,自己好像也并没有存到什么钱,结婚生子,房贷,车贷,个人健康问题好像一样也没落下。...
继续阅读 »

薪资追求


作为资深码农,对这个行业还是比较了解的。


大部分年轻程序员可能都只是想着如何提高技术,为拿到更好的Offer尔努力,很多码农到30岁后才突然发现,程序员高薪也就那么回事,自己好像也并没有存到什么钱,结婚生子,房贷,车贷,个人健康问题好像一样也没落下。其实回头看,互联网高薪也就那么几年,大部分没赶上的跟其它专业的差距也没那么大,甚至35失业后可能还不如同时期传统行业的同学,毕竟码农离开了平台,个人能发挥的空间很有限。


image.png
薪水档位


避免盲目的埋头学技术,努力工作,我们可以提前算一下,大部分程序员一辈子到底能挣多少钱。从毕业22岁算起,不同路线的码农的职业生涯可能有所区别,我们以互联网聚集的一线城市为例来看下各种情况:


第一档:


毕业即一线大厂,年薪30w-50w;普遍的能干三年,薪资区间在:120w-150w;


普遍三年一跳或不跳,一般不跳30%涨幅,跳槽可能有50%涨幅,年薪40w-55w再干两年能拿:80w-110w;


一般五年会有一波比较大的涨幅,算50%,年薪加股票一般能翻一倍,年薪区间会在80w-120w;能到这个段  位都是相对稳定,一般股票需要四年拿完,就按四年算,能拿到:320w-480w;


这个阶段算下来,大部分已经组建家庭区域稳定,31岁左右也不大会频繁变动,职位基本在P7以上,个人利益与公司利益绑定,如果自身没有想法,不创业基本能保证年薪区间在100w-200w,以40岁为终点来算基本能再多拿:900w-1800w;


总体的薪水在:120w+80w+320w+900w=1420w 到 150w+110w+480w+1800w=2540w;


image.png


第二档:


毕业二线大厂,准大厂,年薪15w-25w;普遍干三年,薪资区间在:45w-75w;


普遍三年一跳或不跳,一般不跳30%涨幅,跳槽可能有50%涨幅,年薪20w-40w再干两年能拿:40w-80w;


一般五年会有一波比较大的涨幅,算50%,这个段位基本没有股票,很多公司会卡薪水上限,年薪区间会在30w-50w;社招对标P7基本会在6年经验以上,所以这里普遍能再干两年拿:60w-100w;


这个档位工作7到8年,是大厂比较喜欢的人群,大部分能到P7,加上股票年薪能到60w-120w,一般四年拿完,算下来能多拿:240w-480w;


整体算下来这个段位基本也到33岁左右,稳定来讲大部分人的职级稳定到P7,P8,考虑年龄因素薪水幅度不会太大,维持70w-120w,假设能稳定干到38岁,可以拿到:350w-600w;


总体的薪水在:45w+40w+60w+240w+350w约700w 到 75w+80w+100w+480w+600w=1300w;


第三档:


毕业优质中小公司,年薪10w-15w;普遍干三年,薪资区间在:24w-45w;


普遍三年,五年靠跳槽涨薪,三年涨30%,干两年能拿:25w-40w;


五年跳槽涨50%,年薪30w-45w,干三年多拿:90w-140w;


30岁左右,比较尴尬,能冲一下P7进去也是大头兵,薪资也不会高出很多,年薪基本40w-70w;假设能稳定干到35岁;可以拿到:200w-350w;


总体的薪水在:24w+25w+90w+200w约350w 到 45w+40w+140w+350w约580w;


第四档:


毕业一般公司,年薪6w-12w;普遍干三年,薪资区间在:18w-36w;


起薪低,一般三年能翻一倍,起薪高的30%-50%涨幅,年薪:12w-20w,干两年能拿:24w-40w;


五年跳槽涨50%,年薪30w-45w,干三年多拿:90w-140w;


同第三档差不多,30岁左右,比较尴尬,能冲一下P7进去也是大头兵,薪资也不会高出很多,年薪比第三档第一点基本30w-50w;假设能稳定干到35岁;可以拿到:150w-250w;


总体的薪水在:18w+24w+90w+150w约280w 到 36w+40w+140w+250w约480w;


薪水总结


按照二八法则来看,普通人对应第三档到第四档,向上突破空间有限,也就是说80%以上的程序员在一线城市工作到35岁最多也就能挣500w左右,所以要找到自己的定位,面对一线城市的房价理性做出选择,而且税后的薪水能存下来的最多也就250w左右,所以一线互联网码农最合适的安家还是二线城市。


image.png


作者:sharloon
来源:juejin.cn/post/7295594732948308006
收起阅读 »

开发者,独立吧!

很多技术人在做了几年一线开发后,都会面临职业生涯上的一个选择——是继续走技术路线?还是转管理路线? 事实上,这个「烦恼」有点肤浅,是把自己置于打工者地位而产生的;当因被裁或公司倒闭等而失业时,你是做技术的还是做管理的,又有何差别? 真正该去选择的是继续做打工者...
继续阅读 »

很多技术人在做了几年一线开发后,都会面临职业生涯上的一个选择——是继续走技术路线?还是转管理路线?


事实上,这个「烦恼」有点肤浅,是把自己置于打工者地位而产生的;当因被裁或公司倒闭等而失业时,你是做技术的还是做管理的,又有何差别?


真正该去选择的是继续做打工者还是「晋级」为创业者?若选择后者的话,对于热爱技术的技术人来说,做个独立开发者是最为适合的!


独立开发者


独立开发者也好,创业者也罢,无论现实层面的处境怎样,更重要的是精神层面的思维方式的转变——精神可以改变并扭曲现实。


就好像一个人要想独立,首先得精神独立,做到在精神、情感上不强烈地依赖他人,其次再谈经济独立——由内而外地去独立。


作为一个普通人,若说与原生家庭进行切割是人生中的第一次独立,那么不与某一家具体的公司绑定就是人生中的第二次独立。


成为独立开发者或其他什么类型的创业者,就是第二次独立的象征。


一般特征


从「独立开发者」这个称谓可以看出,这是一群以软件开发为主要技能,且具备变卖该技能及相关成果的能力的个人。


这就意味着,不能固步自封而要保持开放,不仅专业能力要达到甚至超越一般水准,还要学习并思考如何去做运营、市场营销和商务谈判等。


独立开发者有两样东西是可出售的:用于从事软件开发活动的劳动时间;自己研发的开发套件或其他数字产品。


作为独立开发者,除了用技术手段解决问题所带来的快感之外,也追求利润最大化;因而,单纯出售劳动时间只能是过渡,最终还是得以出售自研产品为重。


同一个独立开发者,在出售劳动时间时,看起来像常规的打工者一样;而当出售自研产品时,则会变得如同商人一般。


出售劳动时间做技术支持的话,雇佣形式可以是全职或兼职,工作方式可以为远程或驻场,较为灵活多变。


心态差异


常会听到有人说在工作中要了解业务,有创业者心态,懂得客户第一什么的——这听起来很「政治正确」,令人感到被 PUA 了。


如果是作为打工者,这话是自己公司或者网上哪个领导说的,那妥妥的是 PUA 无疑,就是一种压榨——既没业务发展决定权,又无公司分红,仅有微薄的工资而已。


但若是独立开发者,对那几个「要求」的理解就不一样了——


给别人打工是将自己的时间和服务作为商品进行出售,公司及其他员工都是自己的客户,无所谓同事、领导,也没有「阶级」之分,一视同仁,皆为合作关系。


自己的职责就是交付符合要求的代码及文档等,若去深入了解业务或公司及其他员工的客户,那也只是为了提高自己的服务质量,以便争取到更多更久的合作。


这才真的是「创业者心态」——所做的一切是为了自己的生意和事业而非他人的,通过成就他人的生意和事业而成就自己的。


基本能力


合格的专业能力自不用说,这是做独立开发者的基本中的基本,总不能用三脚猫的功夫和态度去坑别人吧?


发掘客户


能够有源源不断的合作机会是最重要的,否则独立开发者这条路将不可持续,发掘新的客户就是其中一个途径。


确立自己的个人品牌并借由自媒体塑造相应的形象是非常重要的手段,但不应为了流量去写没啥含金量的文章或拍垃圾视频,而要体现自己是善于独立思考的,以及专业性。


自媒体的主要作用是广撒网,培养「信徒」,再借助互联网与人际网络的力量,将自己的触角延伸到潜在客户那里去。


这是以坚实的专业能力为基础,需要长期用心投入经营的事情——



社区影响力决定的是你能把自己的正确想法放大多少倍出去,这种能力,能争取一定要争取,毛主席教导我们:宣传阵地你不占领,别人就要占领。但扩大社区影响力的同时,一定要牢记,它是个双刃剑,如果没有与之相称的实力,很可能伤到自己。


徐飞《程序员的自我营销》



然而,自媒体上的内容是较为零散琐碎的,常常需要多看些文章或视频才可能把这个人的大致轮廓给勾勒出来。


尤其是对潜在客户这目的性较强的群体而言,想要知道这个独立开发者是否满足合作要求,得花费更多的时间与精力去研究他。


若此时有个专门用于商务的「个人名片」,针对潜在客户提纲挈领地告知他们自己具有哪些优势、是个怎样的人、都提供什么服务等,将会提高合作概率——


「个人名片」网站首页


上图为我的「个人名片」(访问 yaol.in 查看),是最近为走独立开发者这条路而特意重新设计并改版的。


留住客户


从陌生人那里发掘新客户的成本较高,留住已经合作过的客户会好很多;当然,留住客户并不意味着要去无原则地跪舔,而是建立健康的长期合作关系。


在第一次选择合作时,客户是相信了独立开发者所塑造出的形象;这第一次的合作就是验金石,只有在双方都觉得合作愉快时才会有继续合作的机会。


要让对方感到愉快,除了专业能力被认可之外,还得令人觉得靠谱、讲诚信,想客户之所想,急客户之所急——客户第一。


这样一来,不仅多了「回头客」,他们还很有可能会自发地带来新客户,从而逐渐地形成口碑效应。


道理很简单,然而对很多人来说做起来并没那么容易。


总结


要想一直做个技术人,独立开发者是最适合的最终归宿,它更倾向于是一种思维方式,而非实际上是否在某个公司里全职上班。


独立开发者需要借助自媒体去营销自己,并用「个人名片」提高合作概率,以这两者为自己从陌生人中带来客户。


从陌生人中发掘新客户的成本较高,不如以自己过硬的专业能力、诚信的为人及优质的服务赢得「回头客」,没准儿他们还会带来新客户。


本文是以「独立开发者」为主题的文章系列的第一篇,主要从出售劳动时间的角度去阐述,日后会根据实践心得编写更多文章,欢迎关注~


作者:欧雷殿
来源:juejin.cn/post/7297152068782473243
收起阅读 »

程序员的副业发展

前言 之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快 因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么 希望能对你有些帮助~ 正文 学生单 学生单是我接过最多的,已经写...
继续阅读 »

前言


之前总有小伙伴问我,现在没有工作,或者想在空闲时间做一些程序员兼职,怎么做,做什么,能赚点外快


因为我之前发别的文章的时候有捎带着说过一嘴我做一些副业,这里就说一下我是怎么做的,都做了什么


希望能对你有些帮助~


正文


学生单


学生单是我接过最多的,已经写了100多份毕设,上百份大作业了,这里给大家介绍一下


python这种的数据处理的大作业也很多,但是我个人不太会,所以没结过,我只说我做过的


我大致做过几种单子,最多的是学生的单子,分为大作业单子毕设单子


大作业单一般指一个小作业,比如:



  • 几个web界面(大多是html、css、js)

  • 一个全栈的小demo,大多是jsp+SSM或者vue+springboot,之所以不算是毕设是因为,页面的数目不多,数据库表少,而且后端也很简单


我不知道掘金这里能不能说价格,以防万一我就不说大致价格了,大家想参考价格可以去tb或者咸鱼之类的打听就行


然后最多的就是毕设单子,一般就是一个全栈的项目



  • 最多的是vue+springboot的项目,需求量特别大,这里说一下,之前基本都是vue2的项目,现在很多学校要求vue3了,但是大部分商家vue3的模板很少,所以tb上接vue3的项目要么少,要么很贵,所以我觉得能接vue3和springboot项目的可以打一定的价格战,vue2的市面上价格差不多,模板差不多,不好竞争的

  • 少数vue+node的全栈项目,一般是express或者koa,价格和springboot差不多,但是需求量特别少

  • uni+vue+springboot的项目,其实和vue+springboot项目差不多,因为单纯的vue+springboot项目太多了,所以现在很多人要求做个uni手机端,需求量适中

  • .net项目,信管专业的学生用.net的很多,需求量不少,有会的可以考虑一下


这是我接过的比较多的项目,数据库我没有单说,基本上都是MySQL,然后会要求几张表,以及主从表有几对,这就看客户具体要求了



需要注意的点:大部分你得给客户配环境,跑程序,还是就是毕设一般是要求论文的,有论文的会比单纯程序赚的多,但是一定要注意对方是否要求查重,如果要求查重,一般是不建议接的,一般都是要求维普和知网查重,会要了你的老命。还有需要注意的是,学生单子一般是需要答辩的,你可以选择是否包答辩,当然可以调整价格,但是你一旦包答辩,你的微信在答辩期间就不会停了。你永远不知道他们会有怎样的问题



商业单


商业单有大有小,小的跟毕设差不多,大的需要签合同


我接的单子大致就一种,小程序+后台管理+后端,也就是一个大型的全栈项目,要比学生单复杂,而且你还要打包、部署、上线,售后,有一个周期性,时间也比较长


72761aa2847097aa719f2c9728dc560.jpg


image.png


ff5d9aaae6207ab8cbbe847c73cbd36.jpg


9e157d5ddab294d3214fa1d8ece07dc.jpg


为了防止大家不信,稍微放几个聊天记录,是这两个月来找的,也没有给自己打广告,大家都是开发者,开发个小程序有什么打广告,可吹的(真的是被杠怕了)


技术栈有两种情况:自己定客户定


UI也有两种情况:有设计图的无设计图的(也就是自己设计)


基本上也就是两种客户:懂技术的客户,不懂技术的客户


指定技术栈的我就不说了,对于不指定技术栈的我大致分为两种



  • 小程序端:uni/小程序原生、后台:vue、后端:云开发

  • 小程序端:uni/小程序原生、后台:vue、后端:springboot


这取决于预算,预算高的就用springboot、预算不高的就云开发一把嗦,需要说的是并不是说云开发差,其实现在云开发已经满足绝大部分的需求,很完善了,而springboot则是应用广泛,客户后期找别人接手更方便


对于没有UI设计图的,我会选择去各种设计网站去找一些灵感


当项目达到一定金额,会签署合同,预付定金,这是对双方的一种保障


其实在整个项目中比较费劲的是沟通,不是单独说与客户的沟通,更多的是三方沟通,作为上线的程序,需要一些资料手续,这样就需要三方沟通,同时还有一定的周期,可能会被催


讲解单


当然,有的时候人家是有程序的,可能是别人代写的,可能是从开源扒下来的,这个时候客户有程序,但是看不懂,他们可能需要答辩,所以会花钱找人给他们梳理一下,讲一讲, 这种情况比较简单,因为不需要你去写代码,但是需要你能看懂别人的代码


这种情况不在少数,尤其是在小红书这种单子特别多,来钱快,我一般是按照小时收费


cb519bce3fedc451116b659f6cb7388.jpg


e4531c4d8d6527208a03e1dcc6ede32.jpg


aef2baeabe8859caac59fd7ae0b456c.jpg


知识付费这东西很有意思,有时候你回答别人的一些问题,对方也会象征性地给你个几十的红包


接单渠道


我觉得相对于什么单,大家更在意的是怎么接单,很多人都接不到单,这才是最难受的


其实对此我个人并没有太好的建议的方法,我认为最重要的,还是你的交际能力,你在现实中不善于交际,网络上也不善于交际,那就很难了


因为我之前是在学校,在校期间干过一些兼职,所以认识的同学比较多,同时自身能力还可以,所以会有很多人来找,然后做完之后,熟人之间会慢慢介绍,人就越来越多,所以我不太担心能否接单这件事,反而是单太多,自己甚至成立一个小型工作室去接单


如果你是学生的话,一定要在学校积累客户,这样会越来越多,哪怕是现在我还看到学校的各种群天天有毕业很多年以及社会人士来打广告呢,你为什么就不可以呢


当然但是很多人现在已经不是学生了,也不知道怎么接触学生,那么我给大家推荐另外的道路



  • 闲鱼接单

  • 小红书接单


大部分学生找的写手都会比较贵,这种情况下,很多学生都会选择去上面的两个平台去货比三家,那么你的机会就来了


有人说不行啊,这种平台发接单帖子就被删了,那么你就想,为什么那么多人没被删,我也没被删,为什么你被删除了


其次是我最不推荐的一种接单方式:tb写手


为什么不推荐呢,其实就是tb去接单,然后会在tb写手群外包给写手,也就是tb在赚你的差价


这种感觉很难受,而且赚的不多,但是如果你找不到别的渠道,也可以尝试一下


最后


我只是分享一下自己接单的方式,但是说实在的,接一个毕设单或者是商业单其实挺累的,不是说技术层面的,更多的是心累,大家自行体会吧,而且现在商场内卷严重,甚至有人200、300就一个小程序。。。


所以大家要想,走什么渠道,拿什么竞争


另外,像什么猪八戒这种的外包项目的网站,我只是见过,但是没实际用过,接过,所以不好评价


希望大家赚钱顺利,私单是一种赚钱的方式,但是是不稳定的,一定还是要以自己本身的工作为主,自行判断~


作者:Shaka
来源:juejin.cn/post/7297124052174848036
收起阅读 »

听说前端出大事儿了

web
最近这两天,在前端圈最火的图片莫过于下面这张了。 这是一段 React 代码,就算你完全没用过 React 也没关系,一眼看过去就能看到其中最敏感的一句代码,就是那句 SQL 。 咱们把这端代码简化一下,大概就是下面这个样子。

最近这两天,在前端圈最火的图片莫过于下面这张了。



这是一段 React 代码,就算你完全没用过 React 也没关系,一眼看过去就能看到其中最敏感的一句代码,就是那句 SQL 。
咱们把这端代码简化一下,大概就是下面这个样子。




意思就是在页面上点击一个叫做「提交」的按钮,触发一个 formAction(提交表单)的动作。这有点看到了当年 JSP 和 PHP 的味道了。这还不是最神奇的,最厉害的是提交表单要执行的动作不是一个接口请求,而是直接执行一条 SQL 。使用 use server标签,标示这是一个服务端端执行的方法。



一时间竟分不出这到底是前端还是后端了。


这么发展下去,React 就是妥妥的全栈语言了。此时的 PHP 在旁边笑而不语,还说我不是世界上最好的语言,你们终究还是会活成我的样子。



自从前后端分离以来,前端框架可谓是百花齐放,一片繁荣。最早的是 Angular,然后就是 React 和 Vue,到现在基本都是 Vue 和 React 的天下了。


如果你用过原生的 JavaScript 或者 JQuery,那就能感受到 React 或者 Vue 的出现,完全改变了前端的开发方式。


React 目前的最新版本是 18,支持 ES(ECMAScript) 和TS(TypeScript),除了画界面和写CSS之外,完全可以把它当做一个面向对象的语言工具使用。


这次支持执行执行后端 SQL 的特性是 Next.js 开放的,Next.js 是 在React 框架上再次高度封装的一个框架。有点像 Spring Boot与 Spring 的关系,Spring 好比是 React,Spring Boot 就是 Next.js。


本来好好的前端,为什么要直接支持写 SQL 呢,这也并不是无迹可寻的。前两年,React 就推出了React Server Components 。大致的意思就是说这是一种服务器端组件,为了提高性能,由服务器直接渲染,渲染出来的结果通过元数据的形式发给前端 React,React 拿到元数据后与现有的 UI 树合并,最终由浏览器渲染。


React 官方是大力推荐 Next.js 的,有了官方推荐加上本身已经支持的服务器端组件,Next.js 不知道是出于什么样的目的,竟然直接支持执行服务端方法了。之前要通过 HTTP 请求,现在直接就跳过这一步了。


说实话,站在一个前端框架的视角上,加上我本身是一个后端开发,我是有一点看不懂这个操作了。服务端组件还能理解,毕竟开发效率和性能要兼顾,这无可厚非。


但是直接支持服务端执行,是技术的轮回(照着PHP的方向)还是技术的变革呢,此时的 Next.js 就像是一个站在十字路口的汽车,油都加满了,就看各位开发者驾驶员开着它往哪边走了。


反正依我看来,我是觉得前端框架越简单越好。原因很简单,搞这么复杂,我都快不会用了。



不光是我看不懂,毕竟咱是个后端外行,不是专业的。但是前端同学也是一片调侃,调侃的大致意思就是 React Next.js 啥都能干,既然连后端都能整了,那其他的也能全栈了。


比如有人调侃给 Next.js 赋能 AI,使用 use ai,直接 prompt 编程了。



还有赋能 k8s 的



以及赋能二进制编程的



最厉害的,还有赋能删库跑路的。



调侃归调侃,既然口子已经开了,就会有过来吃螃蟹的人,至于之后会变成什么样子,只能拭目以待了。


作者:古时的风筝
来源:juejin.cn/post/7296384298902929417

挖掘潜力 拥抱挑战 第二届OpenHarmony技术大会OS内核及视窗分论坛召开

2023年11月4日,第二届开放原子开源基金会OpenHarmony技术大会在北京成功举办,本届大会的主题为“技术筑生态,智联赢未来”,旨在通过技术手段,为全场景、全连接、全智能时代,基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的...
继续阅读 »

2023年11月4日,第二届开放原子开源基金会OpenHarmony技术大会在北京成功举办,本届大会的主题为“技术筑生态,智联赢未来”,旨在通过技术手段,为全场景、全连接、全智能时代,基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。

作为本届大会最为引人瞩目的分论坛之一,“OS内核及视窗分论坛”在10位顶尖的专家、教授、研究员的深入探讨中圆满落幕。论坛的核心议题围绕如何深度挖掘操作系统内核及视窗的潜能,以更好地助力客户和用户拥抱多元化的行业挑战。 为了实现这一目标,OpenHarmony始终积极寻求系统内核及视窗的优化和迭代,全面降低系统负载,高效构建系统壁垒,更需秉持开放的态度,广纳开发者创新思维,多方聆听领袖的意见,以促进多方合作和共同进步。

为此,OS内核及视窗分论坛特邀了OpenHarmony技术指导委员会委员、华为OS内核实验室主任贾宁和华为操作系统技术专家、菲尔兹实验室主任付明担任出品人,同时邀请深开鸿未来研究院基础架构首席研究员李宝,清华大学计算机系副教授陆游游,上海交通大学助理研究员杜东,华为OS内核技术专家魏勇军,华为终端图形图像资深技术专家黄然,湖南开鸿智谷数字产业发展有限公司副总裁李传钊,厦门大学信息学院教授陈中贵,华为OS视窗架构师杨程云,中国科学院软件研究所开源基础设施组负责人、高级工程师郑森文以及北京航空航天大学虚拟现实技术与系统国家重点实验室副教授高阳10位专家学者共同参与,带来了极具价值的技术内容分享。

出品人:OpenHarmony技术指导委员会委员、华为OS内核实验室主任贾宁

出品人:华为操作系统技术专家、菲尔兹实验室主任付明

深开鸿未来研究院基础架构首席研究员李宝指出,深开鸿通过混合部署技术为OpenHarmony引入实时特性,将OpenHarmony应用到更多关键行业,保障关键任务的实时运行。OpenHarmony 致力于成为万物互联时代的技术底座,深开鸿基于OpenHarmony结合虚拟化技术打造面向更多关键行业的操作系统KaihongOS,在为各行业提供分布式、AI、UI框架等特性的同时也提供了机器人、工业控制等场景需要的实时计算环境,并积极探索基于KaihongOS搭建便捷高效的混合部署统一编程基础设施,构建千行百业的统一数字底座。

深开鸿未来研究院基础架构首席研究员李宝主题分享

清华大学计算机系副教授陆游游对软硬件协同的闪存存储系统进行了深入的探讨。闪存为存储系统带来了颠覆性的革新,清华大学的研究团队在闪存新架构、并发一致性和近数据计算等方向都取得突破性的研究成果,包括软件存储系统架构的变革、I/O并行架构的创新和新型近数据计算I/O栈。这些研究成果为闪存存储系统带来了巨大的性能提升。

清华大学计算机系副教授陆游游主题分享

上海交通大学助理研究员杜东《低时延分布式共享设备研究探索》的报告提出了面向万物智联的应用场景和生态,突破异构分布式操作系统前沿挑战的解决方案。分布式设备共享与协同是万物智联场景操作系统下的重要能力,相比传统分布式共享内存(DSM)等经典技术,分布式设备面临的设备异构、操作系统的驱动框架耦合等挑战。为了应对这些挑战,杜老师提出了用户态框架层的分布式(远端)设备、内核态用户态混合分布式设备、基于新型硬件的设备池等架构方案,这些方案在易用性、兼容性和性能等多方面都取得了一定的突破。

上海交通大学助理研究员杜东主题分享

作为华为OS内核的技术专家,魏勇军带来了《极致性能打造持续流畅的基础内核底座》的主题分享,详细阐述了作为高性能底座的OpenHarmony内核是如何构建Qos智能感知调度、swap内存管理增强、F2FS以及分布式文件系统等核心特性,同时为关键业务提供资源的优先供给,助力OpenHarmony持续构筑流畅低功耗和极致性能的竞争力。

华为OS内核技术专家魏勇军主题分享

华为终端图形图像资深技术专家黄然先生认为,游戏生态是用户对操作系统性能衡量的最直接方式,对终端操作系统至关重要。他介绍了主流游戏的三大引擎Cocos、Unity、Unreal对于游戏而言的重要性,与大家同步了OpenHarmony与游戏引擎厂商合作在游戏垂类生态构筑情况和取得的一些阶段性成果。并号召社区力量更多参与共建游戏生态,共同打造OpenHarmony的游戏体验竞争力。

华为终端图形图像资深技术专家黄然主题分享

湖南开鸿智谷数字产业发展有限公司副总裁李传钊先生,通过结合OpenHarmony商业项目中的真实案例实践经验,分享了OpenHarmony在UI/UX层面创新性突破。他介绍了OpenHarmony在交通行业、教育行业应用中UI/UX的创新技术特点和使用成果,充分帮助开发者和技术同仁了解OpenHarmony商业落地时跨设备UI过程中的实际的问题,以及彻底解决定制化在OpenHarmony UI体验上的痛点的实践。

湖南开鸿智谷数字产业发展有限公司副总裁李传钊主题分享

厦门大学信息学院教授陈中贵老师认为,在OpenHarmony的世界里,视窗图形技术可以创造无限可能,为世界带来更多美好的变化。在OS视窗领域中,矢量图形技术非常重要,它可以利用基于数学方程表达的几何图元来描述图像,相比于位图它具有分辨率无关、文件小、易于编辑和打印质量高等优点,从而广泛应用于文字、图标和图形设计等。然而矢量图的绘制相对复杂,在显示前需要经过一个光栅化的过程,而在一般的视窗系统中通常包含大量的矢量图形,所以矢量图形的绘制效率对视窗效率影响非常大。高质量高效率的矢量图形绘制是OS视窗重点关键技术之一。

厦门大学信息学院教授陈中贵主题分享

除了对矢量图形绘制等视窗关键技术的探讨,华为OS视窗架构师杨程云博士还带来了《2D绘制引擎发展及演进趋势》的分享。在该分享中,我们看到支撑操作系统图形用户界面的基础底座的2D绘制引擎技术发演进轨迹,以及其性能及效率表现对GUI应用交互体验的巨大影响。近年来随着终端设备显示分辨率、屏幕刷新率的持续提升以及应用对UI界面高画质的不断追求,2D绘制引擎面临的性能挑战日益凸显。因此,通过构建下一代2D绘制引擎来解决当前挑战是迫在眉睫的工作。

华为OS视窗架构师杨程云主题分享

中国科学院软件研究所开源基础设施组负责人、高级工程师郑森文老师认为,视窗决定了各类应用程序显示的性能、质量以及图形的响应和反馈,是用户操作和交互中首先能直观感受到的重要环节。尤其是在当前这样一个交互技术极速发展、智联场景多样化、定制化的时代背景下,OpenHarmony视窗系统有机会通过开放式创新,进一步加强了用户的交互体验,成为新一代智联交互显示应用的抓手。

中国科学院软件研究所开源基础设施组负责人、高级工程师郑森文主题分享

最后,北京航空航天大学虚拟现实技术与系统国家重点实验室副教授高阳老师,在分享中谈到虚拟现实领域的OS视窗。高阳老师认为高可置信物理仿真与虚实人机交互是支撑实时场景建模与新型人机交互技术的算法核心,无论是增强现实还是元宇宙等应用,都需要实现高效动态场景模拟与智能人机交互。虚实融合物理仿真与人机交互将对相关物理模型、AI技术融合、数值算法优化关键技术等进行深入研究,为新型虚实融合空间操作系统的高效可视内容呈现方法和智能人机交互模式研究提供了新的思路。

北京航空航天大学虚拟现实技术与系统国家重点实验室副教授高阳主题分享

随着10位专家学者的精彩分享,第二届OpenHarmony技术大会“OS内核及视窗分论坛”完美收官,与此同时,我们也清晰的看到了操作系统内核与视窗的发展路径,深入洞察了未来的发展规划。未来,在多方的协同下,OpenHarmony将攻克技术难关,同时构建技术壁垒,助力分布式全场景协同的开源操作系统底座与生态系统蓬勃发展。

收起阅读 »

OpenHarmony技术俱乐部亮相第二届OpenHarmony技术大会, 揭榜课题科研共建OpenHarmony

11月4日,以“技术筑生态,智联赢未来”为主题的第二届开放原子开源基金会OpenHarmony技术大会在北京圆满召开,汇聚了技术领袖、权威专家与高校师生共同探讨操作系统未来技术发展方向。其中,17所OpenHarmony技术俱乐部高校成员作为未来技术生力军,在...
继续阅读 »

11月4日,以“技术筑生态,智联赢未来”为主题的第二届开放原子开源基金会OpenHarmony技术大会在北京圆满召开,汇聚了技术领袖、权威专家与高校师生共同探讨操作系统未来技术发展方向。其中,17所OpenHarmony技术俱乐部高校成员作为未来技术生力军,在大会主论坛共绘璀璨星图。本次大会还重磅发布了2023 OpenHarmony年度课题,并由OpenHarmony技术俱乐部专家学者共同揭晓。希望通过OpenHarmony技术俱乐部的不断建设和壮大,能够引领操作系统领域的产学研互动,并吸引更多的高校师生参与OpenHarmony的科研共建工作。

17所高校加入技术俱乐部,开源人才储备日益雄厚

OpenHarmony是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。自诞生之初,OpenHarmony就遵循着“共建、共治、共享”的原则,坚持“生态统一、开放共赢”的理念,积极投入OpenHarmony的技术生态与人才生态共建中。

在人才方面,OpenHarmony时刻关注人才和开发者培养,通过产教融合实践,为生态发展注入新动力。OpenHarmony不仅为院校提供全流程服务,支持产业人才培养闭环,助力院校专业建设,还正式发布开发者论坛及开发者成长计划,为开发者提供全方位的赋能支持和帮助。此外,早在2022年7月,OpenHarmony发起了“技术俱乐部计划”,进一步推动产学研合作,加速技术创新和应用。

OpenHarmony“技术俱乐部计划”本着“开展创新工作,成就一流人才”的宗旨,鼓励广大高校师生积极参与OpenHarmony技术研究,承接相关科研课题,展开创新探索;同时,OpenHarmony还通过举办直播课、城市技术论坛等系列线上线下活动,加强社区技术专家与高校师生的互动交流。此外,OpenHarmony技术俱乐部还会定期组织各种形式的技术竞赛、挑战赛,在全国范围内识别优秀创新项目与创新人才。通过这一计划,OpenHarmony邀请专家走进校园开展指导与交流,让师生走进OpenHarmony生态并获得成长,从而全面建设起OpenHarmony社区与高校的桥梁,繁荣OpenHarmony学术生态,打造开源生态与高校结合的标杆。

截至目前,已有上海交通大学、北京理工大学、大连理工大学、兰州大学、武汉大学、东南大学、复旦大学、北京邮电大学、重庆大学、西安交通大学、湖南大学、华中科技大学、浙江大学、西安电子科技大学、南开大学、北京航空航天大学、电子科技大学共17所高校加入OpenHarmony技术俱乐部,为OpenHarmony技术与生态发展储备了坚实的人才基础。

OpenHarmony技术俱乐部高校成员

高校技术俱乐部齐亮相,人才生态日益繁荣

11月4日,来自这17所高校的OpenHarmony技术俱乐部代表也亮相第二届OpenHarmony技术大会现场,并参与了OpenHarmony技术俱乐部发起的“璀璨星空”点亮仪式。现场,OpenHarmony技术俱乐部主任和代表们一起放置“星形灯“,霎时间一颗颗代表OpenHarmony技术俱乐部成员的“星星”接连亮起,汇聚成一片璀璨星空。这寓意着操作系统生态的未来主力军将如同“繁星点点”,通过对创新技术的不懈追求与钻研,以恰同学少年的昂扬姿态,推动下一代终端操作系统社区的发展与繁荣。

OpenHarmony技术俱乐部“星空璀璨”点亮仪式

随后,OpenHarmony项目群技术指导委员会主席陈海波和各高校院级领导,共同发布了OpenHarmony年度课题。随着现场环状灯带亮起并向中间汇聚,“科研共建OpenHarmony”美好寓意跃然眼前,年度课题也一并揭晓。随后,现场的高校教师代表则逐一揭榜各自的意向年度课题,他们将以这些课题为方向,在智能终端操作系统领域展开深入研究。

2023 OpenHarmony年度课题发布仪式

今年4月以来,OpenHarmony已陆续在上海、武汉、长沙、北京和张掖等城市举办了OpenHarmony城市技术论坛,与学术界和产业界技术领袖共同探讨OpenHarmony的未来技术趋势。在OpenHarmony技术俱乐部的点亮仪式环节结束后,大会主论坛还特别举办了OpenHarmony城市技术论坛致谢环节,向上海、武汉、长沙、北京、张掖五个城市技术论坛的出品人及嘉宾致以诚挚的谢意。

OpenHarmony向城市技术论坛出品人颁发致谢纪念

根深才能叶茂,众行方能致远。OpenHarmony在技术人才生态领域的共建举措,已吸引越来越多的高校师生加入到社区生态中。在OpenHarmony技术俱乐部之外,OpenHarmony也吸引了社区内大量技术专家与贡献者的加入。当前,OpenHarmony已有6200+共建者、51家共建单位,贡献代码行数超过1亿行。其中2023年新增代码超过900万行,10家单位志愿成为2023年度“百人代码贡献单位”,OpenHarmony社区已成为下一代智能终端操作系统根社区。也期待未来有越来越多的高等院校师生和技术专家加入OpenHarmony生态,为生态的构建、发展与繁荣注入新动力。

收起阅读 »

首届OpenHarmony竞赛训练营结营颁奖,75所高校学子助力建设开源生态

由OpenAtomOpenHarmony(以下简称“OpenHarmony”)项目群工作委员会和OpenHarmony项目群技术指导委员会主办的首届OpenHarmony竞赛训练营,历时2个月,吸引了来自上海交通大学、复旦大学、电子科技大学、湖南大学、北京理工...
继续阅读 »

由OpenAtomOpenHarmony(以下简称“OpenHarmony”)项目群工作委员会和OpenHarmony项目群技术指导委员会主办的首届OpenHarmony竞赛训练营,历时2个月,吸引了来自上海交通大学、复旦大学、电子科技大学、湖南大学、北京理工大学、四川大学、华中科技大学、中北大学等75个赛队共200+学生报名参与,其中重点本科学院覆盖85%,11月3日经过一天激烈的决赛角逐共有7个赛队脱颖而出。

在11月4日“技术筑生态,智联赢未来”第二届开放原子开源基金会OpenHarmony技术大会上,OpenHarmony项目群工作委员会和OpenHarmony项目群技术指导委员会的专家出席仪式并为12个获奖团队颁奖。

OpenHarmony竞赛训练营合影

构筑开源未来,培育学生英才

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。行业创新,人才先行。为培养更多应用型人才和产业需求有效链接,吸引更多的高校师生参与到OpenHarmony的开发和应用中,今年OpenHarmony项目群工作委员会和OpenHarmony项目群技术指导委员会联合创新推出首届OpenHarmony竞赛训练营。训练营以实战竞赛+赋能培训的模式,帮助高校师生充分掌握并利用OpenHarmony进而实现行业需求和三方库补齐,推动OpenHarmony生态系统的建设和发展,促进技术创新和知识共享,为行业发展注入新的活力和动力。

本届OpenHarmony竞赛训练营分为报名、作品开发、赋能培训、提交作品、作品公示、决赛等环节。其中,赋能培训环节包含技术赋能、线上直播培训、线下核心城市路演培训。主办方邀请了OpenHarmony行业使能专家、三方库领域专家、TSC专家和高校老师作为技术指导和评委,为参赛者提供专业的技术指导、培训、项目评审,以提高其作品的质量和竞争力。

此外,OpenHarmony竞赛训练营鼓励参赛者在作品开发过程中积极参与OpenHarmony开源社区和拉瓦尔社区,利用社区丰富的文档、代码、开发工具等资源优化作品。同时训练营配备了专门的社区小助手为参赛选手解决问题,以积极互动问答的方式和开源合作的精神促进OpenHarmony生态系统的健康发展。

动手实践成果初现,开源英才未来可期

以终为始,着眼实际。训练营旨在鼓励高校人才以OpenHarmony为技术底座去解决更加具体的问题,此次获奖的团队作品涌现出很多创新亮点,充分体现了开源生态蓬勃共建的良好势头和巨大潜力。

一等奖花落选择行业使能赛队的华中科技大学“名称暂定队”赛队,他们提交的病床巡检终端作品,能够更快速、方便地获取病床上病人的关键数据,从而提升巡检效率,帮助实现医疗行业需求,为OpenHarmony行业生态系统的建设和发展作出了突出贡献。

金陵科技学院“我是个rapper”赛队、上海交通大学“Almony”赛队荣获二等奖。作为本次竞赛的黑马,“我是个rapper”赛队提交的参赛作品是一款轻量的图表绘制组件库,提供了丰富的图表类型和灵活的定制选项,使用户能够轻松地创建出精美、直观的图表,可以增强OpenHarmony在数据可视化方面的能力。

在本次比赛中,荣获三等奖的队伍分别是:华中科技大学的“宵宫世界第一”赛队、“阁下又将如何应队”赛队以及“1024队”。而上海交通大学的“Almony”赛队则获得了创新奖。获得优秀奖的队伍分别是:西安交通大学的“西安交通大学2队”、武汉大学的“小乖乖”、大连理工大学的“6舍622队”以及“男2舍708”、兰州大学的“咕咕队伍”。

值得一提的是,本次OpenHarmony竞赛训练营三方库赛道的多个作品达到了合入OpenHarmony社区主干的要求,完成门禁审核后,将提供给所有开发者访问和使用。

OpenHarmony项目群技术指导委员会主席陈海波为获奖赛队颁发一等奖

OpenHarmony项目群技术指导委员会委员武延军

OpenHarmony项目管理委员会主席任革林颁发二等奖

OpenHarmony项目群技术指导委员会委员贾宁

OpenHarmony项目管理委员会委员李锋颁发三等奖

OpenHarmony项目群技术指导委员会委员臧斌宇为获奖赛队颁发创新奖

OpenHarmony项目群技术指导委员会委员张兆生

OpenHarmony知识体系组组长王治文颁发优秀奖

我们正迎来最好的万物智联时代,同时也伴随着诸多挑战。躬身入局、合力共建将是智赢未来的关键所在。未来OpenHarmony竞赛训练营将带来更多领域的命题和更强大多元的技术指导和支持,多方位为高校师生赋能,吸引更多OpenHarmony参与者、贡献者、共创者,构筑开源生态美好未来。

收起阅读 »

构建系统安全“堡垒” OpenHarmony技术大会OS安全分论坛意义深远

随时工业4.0时代的到来,数据已成为企业数字化、智能化发展的基石。然而,面对呈爆发式增长的数据量,如何确保系统安全、防止数据被窃取,已成为全球用户关注的焦点。在这样的背景下,2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会OS安...
继续阅读 »

随时工业4.0时代的到来,数据已成为企业数字化、智能化发展的基石。然而,面对呈爆发式增长的数据量,如何确保系统安全、防止数据被窃取,已成为全球用户关注的焦点。在这样的背景下,2023年11月4日举办的第二届开放原子开源基金会OpenHarmony技术大会OS安全分论坛上,众多专家学者就系统安全、数据安全、大模型安全以及软件安全分析等方向展开了热烈探讨,并且针对OS安全所关注的前沿技术和实践应用提出了建设性意见,一同为OpenHarmony提倡的万物智联筑起了坚实的安全保障。华为副首席科学家、终端BG/车BU首席安全架构师付天福担任论坛出品人。

论坛汇聚了中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华,上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌,华为系统安全实验室主任、系统安全首席专家谭寅,清华大学网络研究院副院长、副教授、博士生导师张超(临时时间冲突委托朱文宇博士),OpenHarmony安委会产业安全使能与标准化工作组副组长翟世俊,华为OS内核安全专家、形式化验证助理科学家李屹,北京中科微澜科技有限公司CEO杨牧天,北京邮电大学副教授、博士生导师梁洪亮,中国科学院软件研究所特别研究助理凌祥等十位技术学者,共同探讨了OpenHarmony安全的前沿技术和未来发展方向。

OS安全分论坛现场

首先,中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华在《数据要素流通的安全挑战与对策》分享中介绍了万物智慧互联是推动数据广泛传播的关键驱动力,促进了数据泛在共享,使得数字经济高度依赖于数据要素的流通。然而,随着数据从传统的共享模式演变为要素流通,现有的安全技术无法满足数据多轮交易的安全需求,因此急需构建一套全面的解决方案,以确保数据要素流通的安全性。李老师提出隐私计算整体的一套理论体系和技术方案,正确利用不同技术解决泛在共享环境中不同环节的数据安全与隐私保护问题。

付天福补充说明,OpenHarmony原生提供了全套数据生命周期的创建、存储、流通、使用和销毁的安全机制,为数据成为数字经济最重要的生产要素,提供了基础保障能力。

中国科学院信息工程研究所二级研究员、副总师、“百人计划学者”李凤华主题分享

随后,华为副首席科学家、终端BG/车BU首席安全架构师付天福谈到,安全是OpenHarmony内嵌在基因里的核心架构,为了应对大数据时代的安全挑战, OpenHarmony 提供了对数据整个生命周期的标记、加密、隔离、访问控制等全套机制,为数据在虚拟世界建立了一套完备的保障措施。他表示,在智能汽车时代,安全是最为核心的竞争力,安全是最大的豪华。智能辅助驾驶安全已经成为OpenHarmony赋能汽车行业的独特优势,而网络安全对功能安全的影响也日益增大,OpenHarmony基于分级安全架构,天生就适合智能汽车这样的分布式计算机体系,充分发挥了强安全部件的能力为整车提供保护,为智能汽车提供了网络安全上的领先解决方案。

华为副首席科学家、终端BG/车BU首席安全架构师付天福主题分享

上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌在报告中提出了“智能终端操作系统的个人数据处理与保护”的新思路。该方法利用人工智能和机密计算等新技术,实现对个人数据的存储、使用和保护。他指出,智能终端作为个人的数字世界主要入口,记录了大量的个人数据,合理利用这些数据将带来效率的极大提升。通过提供一个端云一体化的机密计算能力,既满足了AI时代对数据驱动模型下的智能化广泛应用,又保障了个人隐私不泄露企业数据不泄密,OpenHarmony有望成为大模型的入口,为个人数据的安全和隐私提供更强大的保障。

上海交通大学教授、OpenHarmony技术俱乐部主任夏虞斌主题分享

华为系统安全实验室主任、系统安全首席专家谭寅在报告中提出了面向OpenHarmony的软硬协同系统安全底座的构筑方法。当前操作系统安全技术已经取得了长足的进展,但随着软件功能复杂性和体量急剧提升,攻击面也持续扩大。即使采用了大量的系统安全防护方法,还是避免不了高危漏洞被利用并最终导致系统被攻破的情况。在此背景下,面向OpenHarmony体系化构筑安全底座,突破关键安全技术,从机制上打造OpenHarmony的安全架构就尤为重要。本报告抛砖引玉介绍当前在OpenHarmony安全防护的工作,以期促进系统安全领域学者与研究人员共同构筑OpenHarmony安全。

华为系统安全实验室主任、系统安全首席专家谭寅主题分享

清华大学网络研究院副院长、副教授、博士生导师张超老由于临时时间冲突,委托博士生朱文宇博士代为分享了题为《闭源软件安全智能化分析》的主题报告,深入探讨了在安全对抗场景下闭源软件分析的挑战,并指出了传统分析技术的局限性。他强调,提取和恢复闭源程序的结构和语义信息对分析其安全性具有重要意义,但由于编译过程中的信息损失,二进制程序分析相较于源码分析更具挑战性。令人欣喜的是,张超分享了团队近期的研究成果,展示了流行的大语言模型在二进制程序分析中的显著效果。这一创新方法为闭源软件安全智能化分析提供了新的思路和解决方案,为未来的软件安全研究开辟了广阔的前景。

清华大学网络研究院副院长、副教授、博士生导师张超委托朱文宇博士主题分享

OpenHarmony安委会产业安全使能与标准化工作组副组长翟世俊对OpenHarmony发行版证通电子的LightBeeOS获得CCRCEAL4+认证做了介绍,介绍了中国网络安全审查技术与认证中心(CCRC)评估保障级(EAL)认证情况,CCRCEAL4+认证对操作系统的安全提出了非常高的要求,需要对系统的安全架构进行全面严格的审核。首先对LightBeeOS的安全功能进行全面测评,并结合详细设计、源码审计、渗透测试、现场审核等对LightBeeOS安全进行全面评估,证明LightBeeOS以及OpenHarmony具有较高的安全水平,为万物互联期待提供了一套坚实的底座,生态厂商可基于OpenHarmony开发更多高安应用场景的发行版OS,聚焦业务场景,减少对安全的巨大投入。

OpenHarmony安委会产业安全使能与标准化工作组副组长 翟世俊 主题分享

本次论坛同时也邀请到了证通电子公司董事、安全支付事业部总经理、OpenHarmony研究院院长程胜春先生,程总表示:拿通用的整套操作系统,而且是国产自主的基于OpenHarmony的操作系统进行这么高级别的安全认证我们是第一次,这次LightBeeOS通过EAL4+认证,我们感到非常开心也深受鼓舞,非常感谢在整个认证过程当中,各位专家老师们给予证通电子的宝贵意见与帮助。证通电子LightBeeOS通过Eal4+认证,首先证明我们OpenHarmony数字底座本身的具有基础的安全能力;其次,融合证通在金融安全上的安全组件等安全技术优势,整体上让这样的一个发行版操作系统信息安全保障能力达到了较高水平。

证通电子董事、安全支付事业部总经理程胜春先生和付天福、翟世俊博士合影

在《操作系统视角看大模型安全挑战》的主题演讲中,华为OS内核安全专家、形式化验证助理科学家李屹指出,操作系统是软件栈的底座,操作系统的安全性在各种场景都具有举足轻重的意义。随着大模型等AI应用的广泛兴起,数据安全正在面临前所未有的挑战。所以,他认为行业有必要站在操作系统的视角,来观察我们将如何应对这些挑战。李博提出了以隔离、跟踪、协同一体化的思路,希望以操作系统为底座,以数据安全为第一原则,逐步构建可信的原生智能,为大模型时代构建坚实的数据和系统安全底座。

华为OS内核安全专家、形式化验证助理科学家李屹主题分享

北京中科微澜科技有限公司CEO杨牧天谈到了建设安全可靠的OpenHarmony生态软件仓库的重要性。他认为,尽管OpenHarmony生态的软件数量众多,但如何确保它们的安全性仍然是一个关键问题。因此,他介绍了OHPM在OpenHarmony生态中三方软件管理方面的完善安全运营机制,旨在预防和处理开源三方库常见的风险,保障OpenHarmony生态的安全可靠发展。

北京中科微澜科技有限公司CEO杨牧天主题分享

北京邮电大学副教授、博士生导师梁洪亮在《软件缺陷漏洞分析》的主题演讲中分享了他的深刻见解。他认为,软件缺陷或漏洞广泛存在,攻击者可以利用它们窃取信息或控制系统。因此,在部署之前,检测和分析软件中的缺陷或漏洞至关重要。鉴于传统测试方法效率的局限,梁老师提出了一种序列引导的多目标混合模糊测试的方案LeoFuzz,通过创新的技术手段,大大提升了软件系统的缺陷的发现效率和准确率。为开源社区软件安全分析提供了一种高效的解决思路,为安全的OpenHarmony生态安全助力。

北京邮电大学副教授、博士生导师梁洪亮主题分享

中国科学院软件研究所特别研究助理凌祥在主题演讲中强调了正确使用OpenHarmony等开源操作系统的API对系统稳定性和安全性的重要性。由于API更新频繁且使用复杂,开发者容易发生误用,导致系统安全漏洞。因此,他介绍了一种基于自动挖掘API路径模式的API误用缺陷检测方法APP-Miner,此方法在针对开源操作系统和大型软件系统的缺陷检测中发挥了显著效果,发现了隐藏很深的多个Linux内核API误用缺陷。希望类似的创新方案能为社区安全治理助力,共同推动开源软件社区安全发展。

中国科学院软件研究所特别研究助理凌祥主题分享

随着万物智联时代的到来,设备联网数量不断增加,技术发展带来的大模型的机遇,随之而来的安全和隐私问题也日益突出。大量的数据将被传输和存储,包括敏感数据如医疗记录、银行信息和公司机密。这些数据的泄露可能会导致巨大的经济损失和信任危机。因此,OpenHarmony将充分发挥其操作系统的安全优势,从数据安全、系统安全、AI安全、软件安全分析等各个维度,打造坚固的“堡垒”,构建分布式全场景协同的开源操作系统底座与生态系统,以保障万物智联产业的繁荣发展。

收起阅读 »

Coremail重磅发布2023年Q3企业邮箱安全性报告

10月25日,Coremail邮件安全联合北京中睿天下信息技术有限公司发布《2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护?一、国内垃圾邮件激增,环比增长31.63%根据Corem...
继续阅读 »

10月25日,Coremail邮件安全联合北京中睿天下信息技术有限公司发布2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护?

一、国内垃圾邮件激增,环比增长31.63%

根据Coremail邮件安全人工智能实验室(以下简称“AI实验室”)数据,2023 年Q3国内企业邮箱用户共收到近 亿封的垃圾邮件,环比增长 7.89%,同比去年同期增长 0.91%尤其是国内垃圾邮件激增,环比增长31.63%

经 AI 实验室分析,在 TOP100 接收列表中,教育领域收到的垃圾邮件高达 2.41 亿封,环比上涨13.8%,持续处于前列。

二、境内钓鱼邮件数量激增,首次超过境外

2023 年Q3,全国的企业邮箱用户共收到钓鱼邮件高达 8606.4 万封,同比激增 47.14%,环比也有 23.67%的上升。从总的钓鱼邮件数量来看,境内和境外的钓鱼邮件都呈现增长趋势。但在 2023 年第三季度,境内钓鱼邮件的数量显著增长,超过了境外钓鱼邮件的数量。

Coremail 邮件安全人工智能实验室发现黑产越来越多利用国内的云平台的监管漏洞发送钓鱼邮件,这对国内云服务提供商而言是巨大的挑战

三、Q3垃圾邮件呈现多元化趋势

2023 年 Q3 的垃圾邮件呈现出多元化的趋势,利用各种语言、主题和策略来达成发送垃圾邮件的目的,包括测试邮件、多语言内容、退税和通知等,数量巨大,层出不穷。

而钓鱼邮件常伪装为系统通知或补贴诈骗,这增加了账户被劫和数据泄露的风险。钓鱼邮件主题常利用紧迫性日常相关性模糊性专业性来吸引受害者,建议用户对此类钓鱼邮件保持高度警惕。

四、关键发现:基于邮件的高级威胁

1横向钓鱼攻击

横向钓鱼攻击直接利用了人们的信任关系,已经成为组织面临的重大威胁,而生成型 AI 为攻击者提供了更加强大的工具,使得这些攻击更加难以防范。

以下为 Coremail 在第三季度的横向钓鱼 (也称为内域钓鱼邮件)的检测和拦截数据分析解读:

① 嵌入式钓鱼 URL 的利用:高达 95%的横向钓鱼攻击使用嵌入钓鱼 URL 的邮件。

② 攻击频率:平均每月,约 25%的组织或企业会遭受一次横向钓鱼攻击。

③ 检测挑战:79%的横向钓鱼邮件需要动态分析嵌入的钓鱼URL,这增加了检测的复杂性和时间成本。

④ 更高的威胁等级:接收横向钓鱼邮件的人员的中招率上升了 200%。

2、商业电子邮件欺诈

商业电子邮件欺诈(BEC)涉及网络罪犯伪装成高管或受信任的供应商,以操纵员工转移资金或敏感信息。

针对商业电子邮件欺诈,以下为 Coremail 在第三季度的数据分析解读:

① 账号失陷与社交工程:高达 90%的 BEC 攻击与账户失陷同时发生,而 9%采用社交工程方法。

② 攻击方法:BEC 攻击主要侧重于直接诈骗钱财或信息。

③ 仿冒策略:85%的 BEC 攻击使用以下仿冒策略。

④ 邮件内容分析:70%的邮件为“银行信息变更请求”,15%为催促付款,12%为银行信息变更。

基于 AI 的新威胁

当然,BEC 攻击不仅仅是技术挑战,它更多的是一个人为问题。这类攻击强调了员工培训和安全意识的重要性,因为员工是这类攻击的第一道防线。同时,技术如双因素身份验证、邮件过滤防护和 AI 驱动的安全工具可以提供额外的防护。

五、新措施:监控,响应与安全意识

邮件作为企业沟通的主要方式,不幸地成为了许多网络威胁的首要入口。鉴于此,维护邮件安全不仅是技术问题,还涉及到组织的多个层面。以下分析了邮件安全厂商、邮箱管理员和用户在邮件安全中的作用以及他们分别在监控、响应和安全意识三个方面的关键角色。

1、组织安全的关键挑战

① 员工的安全意识

员工经常成为安全的最弱环节。安全意识方面的缺乏、不够严格的密码策略、轻率地点击可疑链接或不当地处理敏感信息,都可能导致严重的安全事件。

② 威胁响应流程

一个好的安全响应不仅要能有效地解决问题,还要迅速执行。然而,许多组织的反馈机制和响应矩阵的复杂性导致了繁琐的流程,最终导致效率低下和暴露更多风险。

2Coremail 针对性解决方案

① 利用 LLM 进行用户报告的预分类

为了应对迫在眉睫的网络威胁,Coremail 策略性地利用了大语言模型(LLM)即时预分类用户报告的电子邮件。通过 LLM 系统进行即时评估,安全团队可以迅速优先处理威胁,确保高风险邮件得到及时处理。这不仅极大地提高了威胁管理的效率,而且显著降低了由于延迟响应而可能出现的损害风险。

② 让用户成为安全架构的一部分

对于 Coremail 来说,用户不仅仅是被动的实体,而是安全生态系统中的主动参与者。用户是企业安全中的重要角色。通过培养用户主动报告潜在威胁的文化,不仅强化了安全防御,而且增强了用户的安全意识,从而减轻了管理负担。

如上图是“仿冒发信人,仿冒系统通知”的钓鱼漏判响应处理案例的流程。这个流程中,积极的用户参与、即时的邮件威胁响应以及管理员和邮件厂商的紧密合作,得以确保邮件系统的安全性和邮件威胁管理效率。

收起阅读 »