注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

了不起的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 来说,用户不仅仅是被动的实体,而是安全生态系统中的主动参与者。用户是企业安全中的重要角色。通过培养用户主动报告潜在威胁的文化,不仅强化了安全防御,而且增强了用户的安全意识,从而减轻了管理负担。

如上图是“仿冒发信人,仿冒系统通知”的钓鱼漏判响应处理案例的流程。这个流程中,积极的用户参与、即时的邮件威胁响应以及管理员和邮件厂商的紧密合作,得以确保邮件系统的安全性和邮件威胁管理效率。

收起阅读 »

SQL中的DDL(数据定义)语言:掌握数据定义语言的关键技巧!

DDL(Data Definition Language),是用于描述数据库中要存储的现实世界实体的语言。前面我们介绍了数据库及SQL语言的相关概念和基础知识,本篇文章我们来重点讲述DDL(数据定义语言的语法格式)的相关内容以及DDL的常用语句。一、DDL介绍...
继续阅读 »

DDL(Data Definition Language),是用于描述数据库中要存储的现实世界实体的语言。

前面我们介绍了数据库及SQL语言的相关概念和基础知识,本篇文章我们来重点讲述DDL(数据定义语言的语法格式)的相关内容以及DDL的常用语句。

一、DDL介绍

这里我们先回顾一下前面讲过的SQL语言的概念:SQL(Structured Query Language),即结构化查询语言,是在关系型数据库(诸如Mysql、SQL Server、Oracle等)里进行相关操作的标准化语言,可以根据sql的作用分为以下几种类型:

下面再来看DDL语言是什么:

DDL,全称为Data Definition Language,即数据定义语言。它是SQL语言的重要组成部分,主要用于定义和管理数据库的结构。

二、DDL语言能做什么?

通过DDL,我们可以创建、修改和删除数据库、表、视图等对象。

创建数据库: 使用CREATE DATABASE语句,我们可以创建一个新的数据库。

删除数据库: 使用DROP DATABASE语句,我们可以删除一个已经存在的数据库。

创建表: 使用CREATE TABLE语句,我们可以在数据库中创建新的表。

** 删除表:**使用DROP TABLE语句,我们可以删除一个已经存在的表。

修改表结构: 使用ALTER TABLE语句,我们可以修改已经存在的表的结构,如添加、删除或修改字段等。

三、什么是数据库对象

数据库对象是数据库的组成部分,常见的有以下几种:

1、表(Table )

数据库中的表与我们日常生活中使用的表格类似,它也是由行(Row) 和列(Column)组成的。

Description

列由同类的信息组成,每列又称为一个字段,每列的标题称为字段名。行包括了若干列信息项。一行数据称为一个或一条记录,它表达有一定意义的信息组合。一个数据库表由一条或多条记录组成,没有记录的表称为空表。每个表中通常都有一个主关键字,用于唯一确定一条记录。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

2、索引(Index)

索引是根据指定的数据库表列建立起来的顺序。它提供了快速访问数据的途径,并且可监督表的数据,使其索引所指向的列中的数据不重复。

Description

3、视图(View)

视图看上去同表似乎一模一样,具有一组命名的字段和数据项,但它其实是一个虚拟的表,在数据库中并不实际存。视图是由查询数据库表产生的,它限制了用户能看到和修改的数据。

Description

4、图表(Diagram)

图表其实就是数据库表之间的关系示意图。利用它可以编辑表与表之间的关系。

Description

5、缺省值(Default)

缺省值是当在表中创建列或插入数据时,对没有指定其具体值的列或列数据项赋予事先设定好的值。

Description

6、规则(Rule)

规则是对数据库表中数据信息的限制,它限定的是表的列。

7、触发器(Trigger)

触发器是一个用户定义的SQL事务命令的集合。当对一个表进行插入、更改、删除时,这组命令就会自动执行。

Description

8、存储过程(Stored Procedure)

存储过程是为完成特定的功能而汇集在一起的一组SQL 程序语句,经编译后存储在数据库中的SQL程序。

Description

9、用户(User)

所谓用户就是有权限访问数据库的人。

四、DDL常用语句

4.1 数据库相关

1)查看所有数据库

格式:show databases;

2)创建数据库

格式:create database 数据库名 charset=utf8;

举例:

#创建一个名为test的数据库
#create database 库名;
create database test;
#创建一个名为test的数据库并指定字符集和编码格式
create database test default charset utf8 collate utf8_general_ci;

3)查看数据库信息

格式:show create database 库名;

**4)删除数据库 **

格式:drop database 数据库名;

举例:

#删除test数据库
drop database test;

5)使用数据库

执行表相关和数据库相关的SQL语句之前必须先使用了某个数据库

格式:use 数据库名;

举例:

use test;

4.2 表相关

1)创建表

格式:create table 表名(字段1名 类型,字段2名 类型,…)

举例:

create table person(name varchar(50),age int);
create table person(name varchar(50),age int);
create table stydent(name varchar(50),chinese int ,math int, english int)charset=utf8;
创建一个员工表emp 保存名字,工资和工作
create table emp(name varchar(50),salary int,job varchar(20));

2)查询所有表

格式:show tables;

3)查询表信息

格式:show create table 表名;

举例:

show create table emp;

4)查询表字段

格式:desc 表名; (description)

5)修改表名

格式:rename table 原名 to 新名;

举例:

rename table stydent to stu;

6)删除表

格式:drop table 表名;

4.3 alter表操作相关

1)添加表字段

格式(最后面添加):alter table 表名 add 字段名 类型;

格式(最前面添加):alter table 表名 add 字段名 类型 first;

在xxx字段后面添加:alter table 表名 add 字段名 类型 after 字段名;

举例:

alter table emp add gender gender varchar(5);
alter table emp add id int first;
alter table emp add dept varchar(20) after name;

2)删除表字段

格式:alter table 表名 drop 字段名;

举例:

alter table emp drop dept;

3)修改表字段

格式:alter table 表名 change 原名 新名 新类型;

举例:

alter table emp change job dept varchar(10);

4)修改列属性

格式:alter table 表名 modify 列名 新列属性

举例(只有MySQL是这样写的):

alter table student modify age int;

关于DDL常用语句就讲这么多了,尽管现在有许多图形化工具可以替代传统的SQL语句进行操作,同时在Java等语言中也可以使用数据库,但对于SQL各类语句的了解仍然非常重要。

收起阅读 »

人民网《外企谈信心》| Denodo:加强数据管理技术合作 护航数字经济高质量发展

本文摘自:人民网责编:王震、吕骞近日,Denodo技术公司首席执行官兼创始人AngelViña做客人民网时表示,中国拥有巨大的经济体量,吸引了众多大型全球公司来华设置分支机构,且中国企业怀有“从中国走向全球”的愿望,种种因素使得开辟中国市场成为以全球化为使命的...
继续阅读 »

本文摘自:人民网

责编:王震、吕骞

近日,Denodo技术公司首席执行官兼创始人AngelViña做客人民网时表示,中国拥有巨大的经济体量,吸引了众多大型全球公司来华设置分支机构,且中国企业怀有“从中国走向全球”的愿望,种种因素使得开辟中国市场成为以全球化为使命的Denodo公司的“必然的决策”。

微信图片_20231103092343.jpg

本期嘉宾:Denodo 全球 CEO 兼创始人Angel Viña


【采访摘要】

主持人:作为Denodo公司创始人您如何看待中国市场?Denodo选择来中国发展的缘由又有哪些?

Angel Viña:进入中国是一个必然的决策:

1. 因为我们现在有超过150家全球客户在中国有分支机构,Denodo管理数据的方式可以确保他们更好地运营全球业务。

2. 中国是全球第二大经济体,有很多公司在“从中国走向全球”,这是我们重点合作发展的一类客户。

3. Denodo是一个国际化的组织,中国是我们全球化布局的重要区域,是公司发展战略的重要一步。


主持人:近年中国出台了一系列政策,保障外资企业在华发展。您认为这一系列政策的出台将对外资企业在华发展起到什么作用?

Angel Viña:中国市场有很多积极的变化,包括新的外资政策以及创造稳定、互信的营商环境等。政府牵头进行咨询调研,公开征询外资企业的反馈,我们愿积极参与。

在一系列新法规中,跨境数据管理占到了相当的篇幅,数据交换、数据跨境、数据安全等。全球公司,无论是在华运营的跨国公司,还是出海拓展全球市场的中国公司,都有信息整合的需求。需要完整的数据来支持更明智的决策并实现公司更高效的运营,从而提供更好的客户体验。这些法规规定了数据跨境传输的方式,而Denodo的逻辑数据管理技术在这一场景中可以起到重要作用,确保数据使用的安全合规。

此外,我们会与中国本土丰富的人才网络对接,尤其是与中国的顶级研究中心合作。利用本地优势资源来支持我们的全球拓展,才能更好地服务国际公司和中国出海公司。

中国在不断加强知识产权保护,为像Denodo这样的知识密集型外资公司带来保障。同时我们希望与政府加强信任与合作,因为政府是Denodo在全球各地区的重要使用者,而信任需要依托于规范和监管,我们会积极拥护当地政策。


主持人:如今很多技术,如大数据、云计算、人工智能、区块链等正在加速创新,日益融入经济社会发展全过程。在您看来,数据管理在数字经济发展中发挥着怎样的作用?外国企业参与中国数字经济发展可以享受哪些红利?

Angel Viña:业务高层如今十分注重数据的价值,数据资产化的重要性已得到普遍的认知。用好数据才能真正地做好业务运营,政府部门才能更好地提供社会服务。任何交易、决策、工业系统或制造系统的自动化流程都离不开数据。数据技术是当今首席执行官最注重的技术,做好数据管理是第一要务。

数字经济需要构建在一个强大而统一的数据基础之上。Denodo就是用来帮助组织将多个数据资产统一为一个业务目标,创建统一的数据视图,以便业务用户能够使用数据。

各种新兴技术也都基于数据,如区块链主要用于交易的可追溯性,而不同的数据层都存储在不同的节点中,基于数据才能真正构建个性化的业务应用来支持不同行业和领域的应用场景。对于AI,数据是机器学习流程的核心,生成式AI也是如此,用AI算法构建的任何一个应用都需要一个连接数据资产的桥梁。对于制造业自动化,从机器人到各种先进技术,具备数据管理能力才能创建一个敏捷灵活的数据基础,才能实现所有现代化技术。


主持人:您认为中国企业在数字化转型中面临哪些困难?Denodo如何充分发挥自身优势助力中国企业数字化转型发展?

Angel Viña:第一点,数据的使用率。尽管数据成熟度会有或多或少的差异,但是全球的数据挑战都大同小异。拥有数据资产不是最重要的,重要的是理解数据可以如何使用。因为数据激增的现象非常普遍,数据量在未来两到三年内会以两位数的增速继续增长,甚至达到十几倍以上的增长。其次是数据碎片化的问题,数据普遍分散在许多不同的数据存储库或数据孤岛中。最后就是数据的使用,据统计,全球采集到的数据只有不到4%被使用。挖掘更多数据集的价值,才能获得更多更好的信息来支撑决策。我们也可称之为“数据民主化”,但最终目的是为了让数据更容易被用户访问和使用。

业务部门能够更容易地获得所需的数据,这就是Denodo的独特技术可以帮助中国公司的事情——显著提升数据的使用率。但在Denodo的功能之上,我认为需要来自政府或公司高层管理者对数据文化的贯彻才能真正提升数据的使用,更快地获取客户,更好地改善公民生活,改善公司的客户体验,提高运营效率和员工生产力。这些都需要对数据交易的可见性,在数据生态系统中实现数据使用。

第二点,安全和合规。在增加数据用量的同时,还需要对使用过程加以控制。我们用逻辑或虚拟化的方式来管理数据,就是为了在底层的物理数据存储和运算系统之上创建一个支持层,用来执行管控,确保数据安全,执行数据治理策略来真正地帮助企业把数据交付给正确的用户,或授权正确的用户来使用数据。控制数据使用的过程非常重要,可以确保数据交易的公开可见。一个组织每年可能有上百万或者上亿的数据交易,很多数据查询都可以实现秒级的执行,因此确保数据以安全、合法的方式被正确地使用至关重要。

第三点,对于在华运营的外国公司以及出海的中国企业如何才能解决跨境数据管理和数据分享的问题。各个国家对于数据隐私、数据主权、数据资产管控等都有强有力的立法约束,数据安全与国家安全密不可分。而我们提供的是一种真正能实现数据可操作性,同时又确保其符合跨境、跨国数据法规的方式,对全球公司来说意义重大。他们需要整合数据,做好跨全球数据的安全使用,才能更好地管理如全球人力资源部等关键职能部门的信息交换,或者更好地服务全球客户,Denodo在这方面有丰富的经验。


主持人:在您看来,数字化如何赋能中国企业更好地走出去,与世界共享合作与发展机遇?

Angel Viña:数字化是必经之路,没有数字化转型就没有未来。领导者与落后者的区别就在于数字化交付水平,也可称之为数据成熟度、数字化或者自动化。中国企业对数字化高度重视,非常清楚要想“走出去”并在全球获得竞争力,数据成熟度是一个关键指标。在这一过程中Denodo可以贡献我们的专长,这是我们多年来为世界最顶级最复杂的公司服务而积累的实践经验。20多年来始终帮助顶尖的国际公司建立数据成熟度和数据基础,让他们以更有竞争力的方式来运营和拓展现代业务。

图片5.png

嘉宾简介

Angel Viña是 Denodo Technologies 的创始人兼首席执行官,同时也是公司董事会主席。他负责监督 Denodo 整体企业战略、业务运营和全球扩张。在他的领导下,Denodo 已发展为全球数据管理的领导者,有 800 名员工和 1000 位客户,在 30 个国家/地区设有 25 个办事处。Angel 在开始 IT 职业生涯前已积淀深厚的学术基础,他于 1987 年获得马德里技术大学电信工程博士学位,随后在美国加利福尼亚大学洛杉矶分校和斯坦福大学担任博士后研究访问学者,并在多所大学担任教授。


关于Denodo

Denodo是数据管理领域的领导者。屡获殊荣的Denodo平台是领先的数据集成、管理和交付平台,使用逻辑方法实现自助式商业智能、数据科学、混合/多云数据集成和企业数据服务。Denodo的客户覆盖30多个行业的大型企业和中型市场公司。这些客户实现了超过400%的投资回报率和数百万美元的收益,在不到半年的时间内就获得回报。更多信息,请访问 denodo.com.cn。

收起阅读 »

Denodo全球CEO兼创始人Angel Viña访华 共襄中国经济数字化转型新机遇

近日,全球领先的企业数据管理服务公司Denodo全球CEO兼创始人Angel Viña叶苏斯先生携总部及亚太区高管团队访华。在为期一周的访问中,Angel Viña叶苏斯先生一行与政府机构及战略合作伙伴共商未来数据管理的新高度和新起点,共同探索Denodo助力...
继续阅读 »

近日,全球领先的企业数据管理服务公司Denodo全球CEO兼创始人Angel Viña叶苏斯先生携总部及亚太区高管团队访华。在为期一周的访问中,Angel Viña叶苏斯先生一行与政府机构及战略合作伙伴共商未来数据管理的新高度和新起点,共同探索Denodo助力中国经济数字化转型的新机遇。

图片1.png


据悉,来自西班牙的Denodo平台是领先的数据集成、管理和交付平台,使用逻辑方法实现自助式商业智能、数据科学、混合/多云数据集成和企业数据服务。目前,Denodo的客户覆盖30多个行业的大型企业和中型市场公司,拥有800多名员工和1000多家客户,在30个国家/地区设有25个办事处。


密集访问,共话在华发展机遇

北京市副市长靳伟先生、商务部投资促进局局长刘殿勋先生、中国国际商会秘书长孙晓先生、中国发展研究基金会副秘书长俞建拖先生等政府机构代表会见了Angel Viña团队,就数字科技外资企业在华发展及促进产业地方合作等议题进行了深入交流。商务部投资促进局刘殿勋局长表示,投资促进局将发挥专业机构的作用更大力度吸引和利用外资。中国数字科技领域市场发展前景广阔,欢迎Denodo把握中国市场机遇,深化在华投资合作,加强创新和绿色发展,为推动中外企业合作共赢做出贡献。

图片2.png


对此,Angel Viña先生表达了Denodo在华投资与发展的决心。他表示,尽管经过不平凡的三年,中国经济依旧展现出强大的韧性。政府在出台吸引外商投资相关政策和切实优化外商在华运营环境方面不懈努力,蓬勃发展的数字经济和各组织对数据的重视程度,让他对中国巨大的市场潜力充满信心。Denodo会持续加码在中国的投资发展和技术能力储备,为在华运营的跨国公司和出海拓展的中国企业提供安全、合规、高效的跨境数据管理平台。

Angel Viña先生一行还拜访了西班牙驻华大使馆,与大使拉斐尔·德兹卡拉尔·德马扎雷多(Rafael Dezcallar de Mazarredo)先生和商务参赞阿方索·诺列加·戈麦斯(Alfonso Noriega Gomez)先生进行会谈。


合作研讨,共探数据管理未来

访华期间,Angel Viña先生一行还与包括国家区块链技术创新中心、中国信息通信研究院、清华大学等专业学术和科研机构等在内的科研院校、机构积极接洽,探索数据管理技术发展,以及讨论该领域人才引入对接的可能性。

在清华大学和中国信息通信研究院等举办的一系列“数据编织(Data Fabric)”专家研讨会上Angel Viña先生作为特别嘉宾发表精彩演讲,并吸引众人的关注。他感叹于中国庞大的优秀人才储备,希望与各学术科研机构加深合作,为数据管理领域的技术标准制定和行业化数据解决方案贡献Denodo的专业知识和洞察。

图片3.png


在访问中国最大的IT服务提供商神州数码集团时,Angel Viña先生与神州数码集团总裁王冰峰先生及其核心管理团队进行了深入交流。对神州数码集团 “数云融合”的最新战略深表赞赏,希望通过双方密切的本地协作,为各行业大型企业和政府机构提供数字化转型的最佳产品和服务。

图片4.png


“此次访华所见证的技术创新水平给我留下了深刻的印象。中国是一个经济大国,也是数据大国、创新大国。”Angel Viña先生总结此次访华收获时如是说。在接受人民网专访时他还提出,Denodo进入中国是一个必选项,而不是可选项。Denodo是一家全球性公司,但在各个区域也要成为一家本地化公司,才能真正满足不同市场独特的需求,期待Denodo独特的逻辑数据管理能力能够为中国的数字化经济发展增添新色!

图片5.png

(图:来源人民网专访)

收起阅读 »

符号绑定的另一种打开方式

懒加载和非懒加载iOS对于引用的外部符号,分为Lazy Symbol和Non-Lazy Symbol,分别存储在__DATA,__got节和__DATA,__la_symbol_ptr节。Non-Lazy Symbol符号在dyld加载模块的时候,就会将真实的...
继续阅读 »

懒加载和非懒加载

iOS对于引用的外部符号,分为Lazy SymbolNon-Lazy Symbol,分别存储在__DATA,__got节和__DATA,__la_symbol_ptr节。

Non-Lazy Symbol符号在dyld加载模块的时候,就会将真实的函数地址写入到对应的地址中,实现绑定。而Non-Lazy Symbol则会在第一次调用该函数的时候,为其动态寻找真实函数地址并进行绑定。

facebook基于符号绑定机制,写出了hook神器fishhook,通过查找符号指针并替换,从而达到hook效果!!!

然而,基于模块检测反hook,却甚是烦人。你可能会有反反hook来应付,但是它也有可能会有反反反hook来对付你~~~

那么,怎么才能终结这场hook与反hook的心理战呢?

Mach-O View 分析动态符号绑定过程

简单分析一下Lazy Symbol的绑定过程:



这里以NSLog为例:

可以看出,符号NSLog所指向的地址为:0x0000000100006474。

转化为文件偏移为:0x0000000100006474 - 0x100008078 + 32888 = 0x6474;

到文件偏移为0x6474的位置查看:

这是一段可执行代码,地址0x6474处的意思是:读取0x647c位置处的四个字节的数据(0x1d),保存到w16寄存器。然后无条件跳转到0x645c(这里的地址,全部都是指文件偏移)。


这段代码,光这么看其实看不出什么,但是如果去调试的话,就会发现,这段代码实际上是在调用dyld_stub_binder为懒加载符号绑定真实地址。而刚刚在0x6474处的代码获取到的四字节的数据,实际上是符号绑定信息的偏移:


0xc428+0x1d = 0xc445

也就是说,动态绑定NSLog所需要的数据,就存储在0xc445处。

那么,理论上来说,如果我们尝试着修改这里的数据,是不是就会改变符号的查找的过程呢?

实践

想的再多,都不如动手操作!!!

新建一个工程,书写如下代码(main.m):

__attribute__((constructor)) static void entry(int argc,char *argv[],char **apple,char **executablepath,struct mach_header_64 **mh_ptr){

if (!strncmp(argv[0], "aaa", 3)) {
printf("the same!!");
}
}

并按照如上方式,查找到函数strncmp的Lazy Binding Info,做如下修改:


修改后:


编写动态库并注入到可执行文件:


__attribute__((visibility("default"))) int strncmq(const char *__s1, const char *__s2, size_t __n);

int strncmq(const char *__s1, const char *__s2, size_t __n){
      printf("hook:%s\nhook:%s",__s1,__s2);
       return strncmp(__s1, __s2, __n);
}

重签名运行!!


发现已经替换成功了!!!

但是,用ida或者hopper分析一下二进制文件,会发现调用的还是原来的strncmp符号:


说明如果进行模块检测的话,还是可以检测出来的~因为虽然符号查找替换了,但是实际上"外套"还是strncmp。所以,继续把外套也修改了!!!

修改这两个处:


修改后:


总结

对于动态绑定的外部引用符号,能动手脚的地方确实很多!!!

收起阅读 »

iOS应用砸壳

应用商店下载的app,都是进过加密过的,用hopper或者ida完全分析不了。那是不是就没办法了呢? 其实不然,解铃还须系铃人,要想得到解密后的文件,还是要依靠苹果爸爸啊!!! 首先,我们知道,加密后的应用,如果不解密的话,苹果自己都不知道怎么去解析可执行文件...
继续阅读 »

应用商店下载的app,都是进过加密过的,用hopper或者ida完全分析不了。那是不是就没办法了呢?
其实不然,解铃还须系铃人,要想得到解密后的文件,还是要依靠苹果爸爸啊!!!
首先,我们知道,加密后的应用,如果不解密的话,苹果自己都不知道怎么去解析可执行文件,所以当设备运行应用时,加载进内存中的数据,肯定是经过解密后的数据,因此咱们只需把内存里的对应的解密部分dump下来即可。
这里介绍一个砸壳工具:dumpdecrypted
该工具的原理就是在程序运行之后注入一个动态库,然在内存中dump下解密之后的部分。
原理和实现不赘述,想进一步了解,可以查看源码。
这里简单讲一下如何用该工具进行解密app:
1、运行源码,生成动态链接库。
解压下载下来的dumpdecrypted-master.zip到当前目录;打开终端,cd到该目录。


在该目录下直接运行make:


如果运行不成功,报错找不到文件。运行xcode-select --print-path ,查看目录是否指向/Applications/Xcode.app/Contents/Developer。如果不是,则用xcode-select -s /Applications/Xcode.app/Contents/Developer修改一下。再执行make。
运行成功,会在当前目录生成dumpdecrypted.dylib动态库。
2、注入动态库
ssh登录进手机,用ps命令查看目标app所在的目录。


记住这个目录。待会儿有用。
再用cycript获取目标app的沙盒目录。


获取到目标app的沙盒目录之后,退出cycriptcontrol+D),将第一步得到的dumpdecrypted.dylib,导入到沙盒目录(导入方法有很多,scp、PP助手、ifiles等)。
cd到沙盒目录,运行DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Containers/Bundle/Application/DDFA8DC8-F19A-4A6F-932B-22E8170BB22D/HDXinHuaDict.app/HDXinHuaDict:


看到一系列的+就说明运行成功,在沙盒目录下就会有被解密的二进制文件。



收起阅读 »

🔥🔥🔥15万奖池,环信 IM+AI编程挑战赛开启报名!

大赛背景即时通讯已成为现代生活中不可或缺的一部分,近年来人工智能的迅猛发展也为即时通讯带来了前所未有的智能化体验。高度拟人的陪伴型机器人,全能实用的服务型机器人,高效智能的对话机器人,AI 与即时通讯的结合创新,为用户提供了众多智慧化的沟通方式。借助 AI 模...
继续阅读 »


大赛背景

即时通讯已成为现代生活中不可或缺的一部分,近年来人工智能的迅猛发展也为即时通讯带来了前所未有的智能化体验。高度拟人的陪伴型机器人,全能实用的服务型机器人,高效智能的对话机器人,AI 与即时通讯的结合创新,为用户提供了众多智慧化的沟通方式。借助 AI 模型的强大能力,即时通讯正迎来全新的场景和体验。

本次「未来已来·创新无限 - 环信 IM+AI编程挑战赛」由环信举办,旨在鼓励开发者、编程爱好者和创新者们发挥创意,融合即时通讯与人工智能技术,重新定义人们的沟通方式,创造出更智能、更便捷、更有趣的沟通体验。无论您是热衷于编程技术,还是对即时通讯与人工智能融合充满好奇,本次编程挑战赛都将是您展示才华和创新的绝佳机会。

本次大赛已正式开启报名!让我们一起开创未来,用编程点亮「 IM+AI 」的交融之美!

赛程安排


赛题介绍

参赛作品须基于环信即时通讯IM(SDK或Demo),自由调用业内领先的AI大模型能力,实现具有应用场景和创新功能的AI+IM的应用。鼓励大家在本次挑战赛中发挥奇思妙想的创意, 展现花样丰富、智能化的即时通讯体验及应用场景。

以下场景及功能描述供参考:

社交场景 | AI聊天机器人、AI游戏陪玩、虚拟人物、

  • AI聊天机器人:闲聊、恋爱、角色扮演、心理咨询、情感陪伴等。
  • AI游戏陪玩: 根据玩家游戏习惯、兴趣爱好等信息,自动生成陪玩建议和游戏策略,使玩家在游戏中获得更加个性化的体验。

电商场景| 智能客服、精准营销、AI模特换装、虚拟主播

  • 智能客服:有效引导用户购买、使用产品、智能回复、处理订单和退货需求等。
  • 精准推荐:根据用户喜好,提供个性化商品描述、推荐语。
  • AI模特换装、虚拟主播

医疗场景| 智慧导诊、智能在线问诊、诊室听译机器人

  • 智慧导诊:安排预约、患者回访、发通知、收集信息,帮助患者找最近的就诊医院等。
  • 智能在线问诊:AI医生线上辅助接诊,辅助医师诊断和开方。
  • 诊室听译机器人:将医患沟通实时录音,结构化处理病情数据后自动生成病历。

教育场景| AI助教、智能教学、个性化教育

  • AI助教:智能化的处理学生反馈、教师评估、行政协助等基础功能。
  • 智能教学:智能解答和辅导学生问题,帮助学生更好理解和掌握知识。
  • 个性化教育:根据学生学习情况和反馈,自动分析和评估学生的学习成绩,能力,兴趣,提供针对性的学习计划,教学建议和改进措施。

金融保险| AI虚拟代理或顾问

  • AI虚拟代理或顾问:自动处理提交索赔,提供状态更新,执行其他基本任务等。

软件/工具 |会议助手AI洞察、群未读信息摘要、群分享链接摘要

  • 会议助手AI洞察:实时分析在线会议内容,生成会议关键点及摘要,会议结束后智能生成会议结论及待办。
  • 未读信息摘要: 群内大量未读信息,智能生成摘要,方便快速获取信息。
  • 分享链接摘要: 群内分享的海量信息智能生成摘要。

奖项设置


参赛流程

1 、点击 报名链接 完成报名
2 、扫码添加“环信冬冬”为好友,回复【队伍名+报名时填写的姓名】进群

3 、作品开发 开发应用,编写规范文档、帮助文档(包含应用介绍、应用截图、代码模块说明等);

4、 于12月10日前,根据作品提交流程,上传代码及相关文档,完成作品提交。

5、 等待评审结果

作品提交

  1. Fork 本次创新赛官方作品提交仓库至你的个人GitHub 仓库

  2. Clone 你的 GitHub 仓库代码「https://github.com/你的GitHub名/Easemob2023-Innovation-Challenge」

  3. 在本地的「Innovation- Challenge」文件夹下新创建个人项目文件夹,命名格式为“队伍名-作品名“,将参赛作品的相关文件与代码放置在该文件夹内,勿要“感染”其他文件或者文件夹

  4. 最后通过 Pull Request 将作品内容推送至官方仓库

  5. Review 通过,合入本次比赛的主分支。

参赛规则

  • 参赛者可以以个人或团队形式参赛,团队参赛请指定一名联系人

  • 竞赛所使用的编程语言和开发平台不限,参赛者可根据自身喜好和技术背景选择适合的开发平台。

  • 参赛作品不限平台,不限领域。移动端、Web端、PC端、小程序等均可。

  • 比赛期间可随时报名和提交作品,所有参赛作品必须为原创,不得抄袭他人的代码或设计。

  • 请注意作品截止提交时间,将作品内容在规定时间内推送至官方仓库,否则将不参与作品评选。

  • 参赛作品须保证原创性,不违反相关法律法规,不存在任何法律或合规风险,作品中使用的素材(包括但不限于开源代码、图片、视频等)不存在版权问题。

评选规则


活动支持

收起阅读 »

【Java集合】单列集合Collection常用方法详解,不容错过!

嗨~ 今天的你过得还好吗?路途漫漫终有一归幸与不幸都有尽头🌞在上篇文章中,我们简单介绍了下Java 集合家族中的成员,那么本篇文章,我们就来看看 Java在单列集合中,为我们提供的一些方法,以及单列集合的常用遍历玩法,一起来进入学...
继续阅读 »


嗨~ 今天的你过得还好吗?

路途漫漫终有一归

幸与不幸都有尽头

🌞

在上篇文章中,我们简单介绍了下Java 集合家族中的成员,那么本篇文章,我们就来看看 Java在单列集合中,为我们提供的一些方法,以及单列集合的常用遍历玩法,一起来进入学习吧。

在Java基础中我们也学过,在类实现接口后,该类就会将接口中的抽象方法继承过来,此时该类需要重写该抽象方法,完成具体的逻辑。


Collection 常用功能

Collection 是所有单列集合的父接口,因此在Collection 中定义了单列集合(List 和 Set)通用的一些方法,这些方法可用于操作所有的单列集合。

20190428183815681.png

1.1 方法如下:

image.png

打开api文档,我们可以看到Collection 在 java.util 下,我们通过练习来演示下这些方法的使用:


640 (28).gif


1.2方法演示

public class Demo1Collection {    

       public static void main(String[] args) {

           //创建集合对象

           //使用多态的形式 定义
           Collection<String> person = new ArrayList<>();
           //输出不是一个对象地址,所以说重写了toString 方法

           System.out.println(person);

   

   //        boolean add(Object o) 向集合中添加一个元素
   //        返回值是一个boolean值,一般可以不用接收
           person.add("科比");
           person.add("姚明");
           person.add("库里");

           person.add("麦迪");

           //添加完我们在输出一下这个集合

           System.out.println(person);


   //        boolean remove(Object o) 删除该集合中指定的元素

   //        返回 集合中存在元素,删除元素,返回true;集合中不存在,删除返回false

           boolean res1 = person.remove("科比");

           boolean res2 = person.remove("奥尼尔");

           System.out.println("res1=" +res1);

           System.out.println("res2=" +res2);

   //        boolean isEmpty() 判断该集合是否为空

           boolean empty = person.isEmpty();
           System.out.println("empty=" + empty);    
   //        boolean contains(Object o) 判断该集合中是否包含某个元素
           boolean contains = person.contains("麦迪");

           System.out.println("contains=" + contains);

   //        int size() 获取该集合元素个数

           int size = person.size();

           System.out.println("size = " + size);

   

   //        public Object[] toArray() 把集合总的元素,存储到数组中

           Object[] personArray = person.toArray();

           for (int i = 0; i < personArray.length; i++) {

               System.out.println("数组--" + personArray[i]);
           }    
   //        void clear() 删除该集合中的所有元素,但是集合还存在
           person.clear();
           System.out.println(person);    
   
           //通过多态的方式,如果我们把arrayList 换成HashSet,发现也能使用,这就是我们实现接口的好处
       }
   }

注意:有关Collection中的方法不止上面这些,其他方法可以自行查看API学习。


查询集合中的元素-Iterator 迭代器

2.1 Iterator 接口

在程序开发中,经常需要遍历集合中的所有元素,就是要看看里面所有的元素,那我们怎么办呢? 

20201216223250385.png

针对这种需求,JDK 专门提供了一个接口: java.util.Iterator。该接口也是 Java集合中的一员,但它与 Collection、Map 接口有所不同,Collection 接口 与 Map 接口 主要用于存储元素,而 Iterator 主要用于迭代访问(即 遍历) Collection中的元素,因为Iterator 对象也被称为迭代器。 


下面介绍一下迭代器的概念:

迭代即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种方式专业术语称为迭代。


通过文档,我们可以看到 Iterator 是一个接口,我们无法直接使用,而需要使用Iterator接口的实现类对象,通过Collection接口中的 Iterator()方法,就可以返回迭代器的实现类对象:

public Iterator iterator():  获取结合对应的迭代器,用来遍历集合中的元素。

通过API文档,我们可以看到 Collection 中 Itrerator 接口的常用方法如下:

  • public E next()  返回迭代的下一个元素

  • public boolean hasNext() 如果仍有元素可以迭代,则返回true

image.png

接下来我们通过案例学习,如何使用Iterator 迭代集合中的元素:

 /**
    *  Iterator 迭代器使用
    */

   public class Demo1Iterator {    
       public static void main(String[] args) {            /**
            * 使用步骤:
            * 1. 使用集合中的方法 iterator() 获取迭代器的实现类对象,使用Iterator接口接收 (使用接口接收返回值,这就是我们说的多态)
            * 2. 使用Iterator接口中的方法 hashNext() 判断有没有下一个元素
            * 3. 使用Iterator接口中的方法 next() 取出集合中的下一个元素
            *
            */

           Collection<String> ball = new ArrayList<>();
           ball.add("篮球");
           ball.add("足球");
           ball.add("排球");
           ball.add("乒乓球");    
           //我们来获取一个迭代器,多态
           Iterator<String> iterator = ball.iterator();    
           //判断
           boolean b = iterator.hasNext();
           System.out.println("是否有元素--" + b);            //取出
           String next = iterator.next();
           System.out.println("元素--" + next);    
           //判断
           boolean b1 = iterator.hasNext();
           System.out.println("是否有元素--" + b1);            //取出
           String next1 = iterator.next();
           System.out.println("元素--" + next1);    
           //判断
           boolean b2 = iterator.hasNext();
           System.out.println("是否有元素--" + b2);            //取出
           String next2 = iterator.next();
           System.out.println("元素--" + next2);    
           //判断
           boolean b3 = iterator.hasNext();
           System.out.println("是否有元素--" + b3);            //取出
           String next3 = iterator.next();
           System.out.println("元素--" + next3);    
           //判断
           boolean b4 = iterator.hasNext();
           System.out.println("是否有元素--" + b4);            //取出
   //        String next4 = iterator.next();
   //        System.out.println("元素--" + next4);
           //如果没有元素,在取的话,会报一个NoSuchElementException 的错误
   
   
           /**
            *
            * 代码优化 上面这些步骤是一个重复的过程,我们可以使用循环来优化,那我们选择哪种来呢
            * 我们说 知道元素个数,使用for
            * 不知道元素个数,使用while
            *
            * 那当前我们迭代器的个数,我们不知道,所以使用while循环,而我们的hasNext 就可以作为
            * while的条件来判断
            *
            */

           while (iterator.hasNext()) {
               String ballResult = iterator.next();
               System.out.println("--优化--" + ballResult);
           }
   
       }
   }

分析: 

在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生java.util.NoSuchElementException 没有集合元素的错误。


640 (28).gif


2.2 迭代器的实现原理

我们在之前的案例中已经完成了Iterator 遍历集合的整个过程。当遍历集合时,首先通过调用集合的iterator() 方法获得迭代器对象,然后使用hasNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已经到达集合末尾,停止遍历元素。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Itearator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了让初学者能更好的理解迭代器的工作原理,接下来通过一个图例来演示 Iterator 对象迭代元素的过程:

Description

在获取迭代器的实现类对象是,会把索引指向集合的-1位置,也就是在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素。

  • 当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,

  • 当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,

  • 以此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。


查询集合中的元素-增强for

3.1  概念

增强for循环(也称为 for each 循环)是JDK5以后出来的一个高级for循环,专门用来遍历数组和集合的。 通过api文档,Collection 继承了一个Iterable 接口 ,而实现这个接口允许对象成为 “foreach” 语句目标,也就是所有的单列集合都可以使用增强for。 

它的内部原理其实是个Iterator 迭代器,只是用for循环的方式来简化迭代器的写法,所以在遍历的过程中,不能对集合中的元素进行增删改查操作。

格式:

for (元素类型 元素名 : 集合名或数组) {
       访问元素
   }

它用于遍历 Collection 和数组。通常只进行遍历元素,不在遍历的过程中对集合元素进行增删操作。


640 (28).gif


3.2 练习1:遍历数组

public class Demo2Foreach {    
       public static void main(String[] args) {    
           int[] array = {1,2,3,4,5};    
           for (int i : array) {
               System.out.println("--数组元素--" + i);    
               if (i == 2) {
                   i = 19;
               }
           }    
           //在增强for中修改 元素,并不能赋值
           System.out.println(Arrays.toString(array));
       }
   }

640 (28).gif


3.3 练习2:遍历集合

public class Demo3Foreach {    
       public static void main(String[] args) {
           Collection<String> ball = new ArrayList<>();
           ball.add("篮球");
           ball.add("足球");
           ball.add("排球");    
           for (String s : ball) {
               System.out.println("---" + s);
           }    
           //相对于Iterator遍历方式,增强for 简化了很多,所以优先使用该方式。
       }
   }

新for 循环必须有被遍历的目标。目标只能是Collection 或者是数组。仅仅作为遍历操作出现。


总结

本篇中主要介绍了单列集合接口Collection为我们提供的常用接口,也通过代码的方式带大家体会了一下。在后面的内容中为大家介绍了如何把单列集合中的内容查看出来(遍历),通过讲解一些底层的原理,让大家感受了一下迭代器的使用。


当然集合的遍历不仅仅限于这两种方式,例如java8为我们提供的流式遍历集合,希望大家下去后自己也能搜搜相关的遍历方式,尝试使用一下,ok,本文就到这里了。



收起阅读 »

作为前端,这几个关于console的小知识点,你知道吗

web
在我们实际开发中呢,经常会遇到把一个变量打印到控制台,看一下它的结果的情况 就比如下面这个形式的对象: const obj = { "err_no": 0, "err_msg": "success", "data": { "user_ba...
继续阅读 »

在我们实际开发中呢,经常会遇到把一个变量打印到控制台,看一下它的结果的情况



就比如下面这个形式的对象:


const obj = {
"err_no": 0,
"err_msg": "success",
"data": {
"user_basic": {
"university": {},
"major": {}
},
"user_counter": {},
"user_growth_info": {}
}
}

我们一般会使用 console.log() 看一下它的值: console.log(obj)


image.png


我们点击这个按钮可以一层层的展开这个对象:


image.png


除了 console.log() 外,根据实际情况我们还可以使用下面几种。


console.dir


我们还可以使用 console.dir()。在使用它输出 JS 数据类型数据的时候它和使用 console.log() 的效果差不多:


image.png


我们展开这个对象,可以查看我们想看的数据:


image.png


当我们想打印出个某个 DOM 对象时就不一样了,使用 console.log() 输出的是这个 DOM 元素:


image.png


使用 console.dir() 输出的是这个 DOM 对象:


image.png


JSON.stringify()


我们还可以使用 console.log() 配合 JSON.stringify()


console.log(JSON.stringify(obj, null, 4))

运行效果如下:


image.png


可以看到,这里以字符串的形式将这个对象输出在了控制台。


console.table


我们还可以使用 console.table(),它会以一种表格的形式来输出结果:


image.png


可以看到,这样看着还是很整齐的。


如果我们要打印的是一个数组的话,使用 console.table() 输出数据,看起来会更方便一些:


const arr = ['a', 'b', 'c']
console.table(arr)

image.png


还有,输出多个数据的使用使用 console.table() 也有利于查看数据,如:


const a = 'a', b = 'b', c = 'c'
console.table({a, b, c})

效果如下:


image.png


consle.time 和 console.timeEnd


还有,在我们开发的过程中,有时候需要去看一段代码执行到底消耗了多少时间,我们可以使用 console.time()consle.timeEnd() 包裹想要测试运行时间的代码,比如下面这段代码:


function test() {
for (let i = 0; i < 10000; i++) { }
}

console.time()
test()
console.timeEnd()

运行代码,可以看到控制台输出了这段代码在本机大概的一个运行时间:


image.png



作者:程序员黑豆
来源:juejin.cn/post/7292969465298567187
收起阅读 »

只有一年前端经验,简历到底该怎么写?

Hello,大家好,我是雷布斯。 但不少同学还是会问“我是工作了 x 年的前端,该怎么改呢?”,“我觉得我的项目就是没有亮点,该怎么挖掘呢?”之类的问题。 那我们今天继续用一个真实的例子,给大家分享该用什么样的思路去优化自己的简历。 提前说明,参与简历修改分享...
继续阅读 »

Hello,大家好,我是雷布斯。


但不少同学还是会问“我是工作了 x 年的前端,该怎么改呢?”,“我觉得我的项目就是没有亮点,该怎么挖掘呢?”之类的问题。


那我们今天继续用一个真实的例子,给大家分享该用什么样的思路去优化自己的简历。


提前说明,参与简历修改分享的张同学,于今年 4 月参加了我们的面试全流程辅导,修改意见由我们团队的 uncle13 老师提供,关键信息已打码,并且已经获得该同学的授权来进行分享。


下面将按照简历中的模块顺序逐一进行分析。


个人简介 & 教育经历



这位同学可能是使用的简历模板,这两部分的内容都写的很简洁。


那么问题来了:



  • 简历右上角的二维码,扫出来是某简历网站,而且要求登录后才能查看电子简历,操作这么繁琐,放到纸质版的简历中是有什么目的呢?

  • 部分内容不标注清楚,面试官无法快速了解你,比如南京是现居地还是籍贯?zxxxxx.top是个人博客还是项目网站?


所以这部分可以优化下:



  • 去掉二维码

  • 把关键信息标注清楚。比如个人博客:https://xxxx.top

  • 教育经历如果没有太大的竞争力,可以考虑一块放到个人信息中,样式上会好看,改成:陕西理工大学 | 本科 | 网络工程


专业技能



简历中的专业技能是展示自己适合特定职位的能力和经验的重要方式。


但这位同学写的中规中矩,没有什么特色。


一般来说,面试官会从这个模块,了解到面试者的擅长的技术栈,熟练使用的工具等等。


编写清晰、明确的专业技能部分,有助于大家更好地展示自己的专业知识和经验。
但大家还是需要主要,对于自己写的所有关键字,都需要好好准备,面试官可以从任何一个感兴趣的方向进行提问。


针对这位同学的呢绒,可以这样进行优化:



  1. 熟练掌握HTML+CSS。熟练掌握弹性布局、网格布局以及响应式布局,能够快速实现Web端和移动端页面搭建。

  2. 熟练掌握 JavaScript,能脱离框架进行原生开发,熟悉 TypeScript

  3. 熟练使用 React 框架以及相关生态技术,能够独立完成项目搭建和项目部署,熟悉相关框架原理。

  4. 了解 vue 框架技术,有相关实践经验。

  5. 熟悉浏览器原理以及计算机网络相关技术,在性能优化方面有实践经验。

  6. 了解 Node.js 和常用模块,在此基础上能够使用 ExpressWeb 框架进行简单的服务器应用程序开发。

  7. 了解微信小程序原生开发,能够使用小程序原生开发进行开发。

  8. 对前端工程模块化有一定理解,熟悉 webpackvite 等打包工具及其日常开发配置,可以从 0 到 1 独立搭建项目,并优化构建流程。


大家发现没有,同样是描写熟悉的技术,但经过优化后,立马就变得高大上了。


工作经历



接着来看“工作经历”的模块。


其实比较推荐的介绍方式是,写清楚自己在公司的岗位、工作时间、个人的职责,同时也推荐加上公司的业务简介,不用太详细,1 - 2 句话简单介绍下就行。


在公司的个人职责就不要写的太简单了,你完全可以在这个模块,向面试官展示你的职业发展路径,工作经验和成果等等信息。


还有就是工作经历尽量用倒序的方式来组织,把最近的一份工作写在最上面。


还是以张同学的工作经历为例,我们可以这样修改:



  • 南京xx有限公司(前端开发工程师)20xx年xx月 - 至今


工作内容:



  1. 主要负责xx系统、xx系统等项目的业务开发工作,使用Vue+ElementUI技术栈。

  2. 通过使用虚拟列表等技术优化大列表性能,加快页面加载速度,提高用户体验。

  3. 参与各类项目,包括SPA应用、PC端和移动端的开发。

  4. 深入了解并熟练应用Webpack、ES6、Vue生命周期和组件通信等前端技术,持续提升自身技能水平。

  5. 在团队中积极沟通和合作,负责项目的需求分析、设计和开发,并对项目进展情况进行有效的跟踪和反馈。



  • 西安xxxx有限公司(实习前端开发)20xx年xx月 - 20xx年xx月


工作内容:



  1. 负责xx平台部分功能模块的前端开发工作。

  2. 和后端工程师配合完成接口联调,确保数据交互正常。

  3. 在实习期间积累了从需求分析到代码实现的全流程开发经验。


项目经历


“项目经历”和“工作经历”都是简历的重点。


之前分享的文章也介绍了很多挖掘项目亮点的方法,我们今天也不再过多介绍了。


现在直接来看张同学的例子。


项目一:生鲜到家



首先还是项目顺序的问题,最近的项目写在前面。


写的很简洁,一看就像是个人练手项目。但商城项目真的烂大街了,而且个人职责也完全体现不出这个项目的难度和亮点。


这样的项目,面试官真的是一点问的兴趣都没有。


不过这位同学附上了项目的访问地址,做了私有化的部署,能挽回一些印象分。


我们可以这样调整:



  • 项目名称:生鲜到家


项目简介:生鲜到家是一个在线购物商城,实现了商铺选品、购物计费、购物车、购物下单等功能。


项目地址xxxx.top/


技术栈:Vue3,Vuex,Vue-router,Axios,Mock.js


主要职责



  1. 负责整个项目前端生产环境的整体搭建(仓库创建、项目搭建、工程发布)以及项目管理。

  2. 采用 vue 技术生态开发了核心业务模块(购物车、商品选择等),并针对业务做了一些公共组件的抽离:通信、错误处理、图片优化以及其他业务组件的二次封装。

  3. 使用 Mock.js 进行数据模拟,并封装 Axios 发送请求,减少对后端接口的依赖。成功实现了前后端分离开发并独立部署上线。

  4. 使用 Vue3 组合式 API 进行项目逻辑的编写,提高了代码复用性和可维护性。同时,在模块化开发过程中,成功将代码行数降低了 20% 以上。

  5. 利用 Vue-router 路由钩子函数实现系统角色权限控制,并且统一了前端路由的交互模式。

  6. 结合业务以及 UI 规范封装了 css 原子化组件,组件开发效率提升了 30%

  7. 开发过程学了XX、XX、XX、并沉淀了相关文档 ,在社区与大家分享。


大家要注意,职责中的量化数据,可以让自己的工作更具体、更形象,也更有说服力。


项目二:xx旅行



其实看这个项目的介绍,也像是个人做的 demo。我们还是基于这个项目,再来介绍一种项目介绍方式。



  • xx旅行


项目简介:xx旅行是一款基于微信云开发模式的城市旅游小程序,包括景点浏览、预定、支付、评价等功能,并且配有一个后台管理系统。


主要职责



  1. 使用微信小程序云开发工具,从0到1完成了小程序的开发,实现了用户注册、登录、浏览、预定、支付、评价等核心功能。

  2. 利用Vue框架搭建前端页面,并使用 Koa2 搭建后台管理系统,实现了对景点信息、订单信息、评价信息等的管理和统计。

  3. 对小程序的性能进行了优化,提高了小程序界面响应速度和加载速度,缩短了用户等待时间。

  4. 借助微信云函数,实现了客户端和服务端之间的交互,提高了小程序的稳定性和可靠性。

  5. 配置小程序的各种环境变量,例如AppID、密钥等,成功将小程序发布上线。


项目成果



  1. 完成的小程序共计有 15个页面,为 xx 位用户提供了景点介绍服务。

  2. 对小程序进行的性能优化,平均响应时间由原来的 2s 降低至 500ms,提高了小程序流畅度和稳定性。

  3. 在后台的开发中,广泛应用 Vue 框架和 ElementUI 组件库,提高了代码的可读性和可维护性。


可以看到从项目简介、主要职责和项目成果三个方面对项目进行介绍,结构清晰,内容也很丰富。


张同学还有几个项目,就不再这儿一一列举了。


最后


相信看到了这儿的同学,都有不少收获,修改简历的技能又有所增长,仿佛知道了怎么去做简历的优化。


如果有的同学还要抱怨,“道理我都懂,但我怎么还是改不好自己的简历?”,那么推荐这样的同学报名我们的辅导服务,大厂导师一对一辅导,绝对物超所值。


作者:前端时代周刊
来源:juejin.cn/post/7254081954517073980
收起阅读 »

你知道 XHR 和 Fetch 的区别吗?

web
现如今,网站开发普遍采用前后端分离的模式,数据交互成为了不可或缺的关键环节。在这个过程中,XHR 和 Fetch API 是两种最常见的方法,用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的...
继续阅读 »

现如今,网站开发普遍采用前后端分离的模式,数据交互成为了不可或缺的关键环节。在这个过程中,XHRFetch API 是两种最常见的方法,用于从 Web 服务器获取数据。XHR 是一种传统的数据请求方式,而 Fetch API 则代表了现代 Web 开发的新兴标准。接下来,我们将一同深入学习它们的使用方法和适用场景。


XMLHttpRequest


XMLHttpRequest,通常简称为 XHR。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。XMLHttpRequest 在 AJAX 编程中(比如 jquery)被大量使用。



AJAX :异步 JavaScript 和 XML。许多人容易把它和 jq 的 ajax 混淆。它是一个技术统称,本身不是一种技术。



特点



  1. 异步请求:XHR 允许进行异步请求,它可以在后台执行,而不会阻止页面的其他操作。

  2. 支持跨域请求:通过服务器端设置允许跨域请求,从不同域的服务器获取数据。

  3. 事件驱动:提供了 onloadonerroronprogress 等一系列事件来监听请求的状态变化。

  4. 灵活性:提供了对请求头、响应头以及请求方法的完全控制,使其非常灵活。


工作原理


XHR 的工作原理主要为:



  1. 创建 XHR 对象实例:通过new XMLHttpRequest()创建一个 XHR 对象。

  2. 配置请求:使用open()方法设置请求方法(GET、POST 等)、URL,以及是否要异步执行请求。

  3. 设置回调函数:设置事件处理程序来处理请求完成、成功、失败等不同的状态。

  4. 发起请求:使用send()方法发送请求。

  5. 处理响应:在事件处理程序中处理响应数据,通常使用responseTextresponseXML来访问响应内容。


// 创建一个新的XHR对象
const xhr = new XMLHttpRequest();

// 配置请求
xhr.open("GET", "https://api.baidu.com/test", true);

// 设置响应处理函数
xhr.onload = function() {
if (xhr.status === 200) {
// 请求成功
const responseData = xhr.responseText;
console.log("成功获取数据:", responseData);
} else {
// 请求失败
console.error("请求失败,状态码:" + xhr.status);
}
};

// 发起请求
xhr.send();

XHR 的响应处理通常在onreadystatechange事件处理程序中完成。在上面的例子中,我们等待 XHR 对象的状态变为 4(表示请求完成)并且 HTTP 状态码为 200(表示成功响应)时,解析响应数据。


Fetch API


Fetch 是一种现代的数据网络请求 API,它旨在解决 XHR 的一些问题,提供了更强大、更灵活的方式来处理 HTTP 请求。可以理解为 XMLHttpRequest 的升级版。


特点



  1. Promise 风格:Fetch API 使用 Promise 对象来处理异步请求,使代码更具可读性和可维护性。

  2. 更简单的语法:相较于 XHR,Fetch API 的语法更加简单明了,通常只需要几行代码来完成请求。

  3. 默认不接受跨域请求:为了安全性,Fetch API 默认不接受跨域请求,但可以通过 CORS(跨域资源共享)来进行配置。

  4. 更现代的架构:Fetch API 是建立在 PromiseStream 之上的,支持更灵活的数据处理和流式传输。


工作原理


Fetch 的工作原理主要为:



  1. 使用fetch()函数创建请求:传入要请求的 URL,以及可选的配置参数,例如请求方法、请求头等。

  2. 处理响应:fetch()返回一个 Promise,您可以使用.then()链式调用来处理响应数据,例如使用.json()方法解析 JSON 数据或.text()方法获取文本数据。

  3. 错误处理:您可以使用.catch()方法来捕获任何请求或响应的错误。

  4. 使用async/await:如果需要,您还可以使用async/await来更清晰地处理异步操作。


Fetch API 的特性和简单的语法使它在许多前端项目中成为首选工具。然而,它也有一些限制,例如不支持同步请求,因此需要谨慎使用。


fetch("https://api.baidu.com/test")
.then(response => {
if (!response.ok) {
throw new Error("请求失败,状态码:" + response.status);
}
return response.json();
})
.then(data => {
// 请求成功,处理响应数据
console.log("成功获取数据:", data);
})
.catch(error => {
// 请求失败,处理错误
console.error(error);
});

XHR 和 Fetch 的对比


XHR 和 Fetch 都用于进行 HTTP 请求,但它们之间存在一些关键区别:



  • 语法: Fetch 使用 Promise,更直观和易于理解。

  • 跨域请求: Fetch 在跨域请求方面更灵活,支持 CORS。

  • 流式传输: Fetch 支持可读流,适用于大文件下载。

  • 维护性: Fetch 更容易维护和扩展。


常用库和插件


基于 XHR 封装的库



  • jquery:一个 JavaScript 库,提供了用于处理 DOM 操作、事件处理和 XHR 请求的便捷方法。

  • axios:一个流行的 HTTP 请求库,基于 XHR 开发,支持浏览器和 Node.js。


基于 fetch 封装的库



  • redaxios:它具有与 axios 类似的 API,但更轻量级且适用于现代 Web 开发。

  • umi-request:由 Umi 框架维护的网络请求库,提供了强大的拦截器、中间件和数据转换功能。


总结


XMLHttpRequest (XHR) 和 Fetch API 都是前端开发中用于进行数据请求的有力工具。XHR 在传统项目中仍然有用,而 Fetch API 则在现代 Web 开发中越来越流行。具体选择哪个工具取决于项目的需求和开发团队的偏好,希望本文对你有帮助!


作者:王绝境
来源:juejin.cn/post/7295551704816189467
收起阅读 »

以订单退款流程为例,聊聊如何优化策略模式

如果有人问你什么是策略模式?你可以尝试这样回答 策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。 在策略模式中,通常有一个抽象的策略接口,定义了一系列可以...
继续阅读 »

如果有人问你什么是策略模式?你可以尝试这样回答



策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。


在策略模式中,通常有一个抽象的策略接口,定义了一系列可以被不同策略实现类所实现的方法。在使用时,可以根据需要选择合适的具体策略类,并通过接口进行调用。



虽然解释了策略模式,但是面试官可能会认为你在吊书袋,完全没有自己的理解。我因为被领导卷到了,所以对策略模式有一些其他的理解,接下来我从业务中台实际遇到的问题出发,谈谈怎么被领导卷到,谈谈我对策略模式的优化和理解。


在业务中台,可以根据具体情况选择合适的策略类来执行相应的业务逻辑,从而满足不同业务方的要求。


由于各个业务方的业务逻辑存在差异性和相似性,编写中台代码时需要考虑这些差异性,并预留扩展点以供不同业务方根据自身需求提供策略类。在这种情况下,可以应用策略模式。


image.png


当业务方有特殊的业务逻辑时,只需要添加新的策略实现类即可,不需要修改现有的代码,方便了后续的维护和扩展。


然而在我们在应用策略模式时遇到了几个问题


遇到的实际问题


由于业务中台逻辑非常复杂,每个业务线的业务场景都很难保证完全一样,在代码实践中我们的系统出现了非常庞大复杂的策略类,将大量业务逻辑整合到同一个策略中,这导致系统能力复用非常困难。


举个例子,在退款校验逻辑中 业务场景 A,(售卖红包,类似于美团饿了么会员)。 需要判断如下校验逻辑,如果命中,则不允许退款!




  1. 订单是否在生效期,过期不允许退




  2. 订单是否在已使用,已使用不可退




  3. 订单如果是某类特殊商品订单,则不可退




  4. 如果超过当天最大退款次数,则不可退。




于是我们在编写代码时,就新定义 ConcreteStrategyA ,将以上 4 个业务逻辑放到策略类中。


过一段时间,又出现了业务场景 B(售卖红包,类似于美团饿了么会员)。它的校验逻辑和A 大同小异




  1. 订单是否在生效期,过期不允许退




  2. 订单售卖的红包完全使用不可退,部分使用可以退。




  3. 如果超过当天最大退款次数,则不可退。




业务场景B 相比A 而言,少校验了 “特殊商品订单不可退”的逻辑,同时增加了部分使用可以退的逻辑。


如何设计代码实现两种业务场景的校验逻辑呢? 这是非常具体的问题,如果是你如何实现呢?


完全独立的策略类


我们在最开始写代码时,分别独立实现了 ConcreteStrategyA、ConcreteStrategyB。两者的校验逻辑各自独立,没有代码复用。


此时的系统类图如下


image.png


这种实现方式是大量的代码拷贝,例如退款次数限制、生效期校验等代码都大量重复拷贝了。


后来我们发现了这个问题,将相关校验方法抽到父类中。


继承共同的父类


为了更好的复用校验策略,我们将校验生效期的方法、校验退款次数的方法抽取到共同的父级策略类。由具体的子策略类继承BaseStrategy 父策略类,在校验逻辑中,使用父级的校验方法。


如下图的类图所示
image.png


在相当长的一段时间里,我们认为这已经是最优的实现方式了。


但是被领导卷到了!


image.png


被卷到了


“新增业务场景时,为什么校验逻辑都是复用的原有能力,还需要新增扩展类,还需要开发代码呢?” 领导这样问道。


我尝试回答领导的问题:“开发这段代码,并不算太难,只需要增加一个扩展类就可以了!”


“你数数现在有多少个扩展类了?” 领导似乎有些生气,


我一看扩展类的数量,被吓到了,已经有了15个扩展类。这一刻真的非常尴尬,平时领导从不亲自看代码。估计是突然心血来潮。从表情来看,他好像很生气,估计是代码没看懂!


“我看到这部分代码时,想要查看具体的策略类,但Idea直接刷出了15个策略类……为什么会有这么多的扩展类呢?” 领导进一步补充道。


场面僵住了,我也没什么好办法……。当然领导向来的传统是:只提问题,不给解决办法! 我只能自己想解决办法!如何解决策略类过多、扩展类膨胀的问题呢?


image.png


策略类过多怎么办!


当业务场景非常多,业务逻辑非常复杂时,确实会出现非常多的策略类。但领导的问题是,现有业务场景有很多相似性,某些新增业务场景和原有业务场景类似,校验逻辑也是类似的,为什么还需要新增扩展类呢?


经过深思熟虑,让我发现问题所在!


策略类粒度太粗,导致系统复用难


目前我们的系统设计是每个业务场景都有单独的策略,这是大多数人认可的做法。不同的业务场景需要不同的策略。


然而,仔细思考一下我们会发现,不同业务场景的退款校验逻辑实际上是由一系列细分校验逻辑组合而成的,退款校验逻辑在不同的业务场景中是可以被重复使用的。


为什么不能将这些细分退款校验逻辑抽象为策略呢?例如,将过期不允许退款、已生效不可退款、超过最大退款次数不允许退款等校验逻辑抽象成为校验策略类。


各个业务场景组合多个校验策略,这样新增业务场景时,只需要组合多个校验策略即可。


如何组合校验策略


首先需要抽象校验策略接口: VerifyStrategy


classDiagram
class VerifyStrategy{
+void verify(VerifyContext context);
}

然后定义 VerifyScene 类


classDiagram
class VerifyScene{
+ Biz biz;
+ List<VerifyStrategy> strategies;
}

如何把对应具体策略类配置到VerifyScene中呢?本着 ”能不开发代码,就不要开发代码,优先使用配置!“的原则,我们选择使用Spring XML 文件配置。(在业务中台优先使用配置而非硬编码,否则这个问题不好回答。“业务方和中台都需要开发,为啥走你中台?”)


使用Spring XML 组合校验策略


在Spring XML文件中,可以声明VerifyScene类的Bean,初始化属性时,可以引用相关的校验策略。


 <bean name="Biz_A_Strategy" p:biz="A" class="com.XX.VerifyScene">
<property name="strategies">
<list>
<ref bean="checkPeriodVerifyStrategy"/> <!--校验是否未过期-->
<ref bean="checkUsageInfoVerifyStrategy"/> <!--校验使用情况-->
<ref bean="checkRefundTimeVerifyStrategy"/><!--校验退款次数-->
</list>
</property>
</bean>

当需要新增业务场景时,首先需要评估现有的校验策略是否满足需求,不满足则新增策略。最终在XML文档中增加 VerifyScene 校验场景,引用相关的策略类。


这样新增业务场景时,只要校验逻辑是复用的,就无需新增扩展类,也无需开发代码,只需要在XML中配置策略组合即可。


在XML文档中可以添加注释,说明当前业务场景每一个校验单元的业务逻辑。在某种程度上,这个XML文档就是所有业务的退款校验的业务文档。甚至无需再写文档说明每个业务场景的退款策略如何如何~


和领导汇报以后,领导很是满意。对业务方开始宣称,我们的中台系统支持零开发,配置化接入退款能力。


结束了吗?没有 ,我们后来想到更加优雅的方式。


使用Spring Configuration 和 Lamada


Spring 提供了@Bean注解注入Bean,我们可以使用Spring @Bean方式声明校验策略类


@Bean
public VerifyStrategy checkPeriodVerifyStrategy(){
return (context)->{
//校验生效期
};
}

通过以上方式,可以把checkPeriodVerifyStrategy 校验策略注入到Spring中,spring beanName就是方法名checkPeriodVerifyStrategy。


在Spring XML中可以使用 <ref bean="checkPeriodVerifyStrategy"/> 引用这个bean。


并且当点击XML中beanName时,可以直接跳转到 被@Bean修饰的checkPeriodVerifyStrategy方法。这样在梳理校验流程时,可以很方便地查看代码


点击这个BeanName,会跳转到对应的方法。(付费版Idea支持,社区版 Idea 不支持这个特性)
image.png


总结


总结几个问题




  1. 策略模式目的是:根据不同的业务场景选择不同的策略来执行相应的逻辑




  2. 策略模式一定要进行细化,通过组合多个细分策略模式为一个更大的策略,避免使用继承方案。




  3. 使用Spring XML 组合多个策略模式,可以避免开发。减少新增策略类




  4. 使用Spring Configuration @Bean 将策略类注入Spring 更加优雅




作者:他是程序员
来源:juejin.cn/post/7295010992122101801
收起阅读 »

从码农到工匠-怎么写好一个方法

感谢你阅读本文 今天我们来分享一下怎么怎么写好一个方法,我记得大四的时候彪哥推荐我去读《代码精进之路,从码农到工匠》这本书,那时候由于项目经验不多,所以对于书中很多东西没有感同身受,所以不以为然。 后面做的项目越来越多,从痛苦中不断思考,也看了一些优秀开源框架...
继续阅读 »

感谢你阅读本文


今天我们来分享一下怎么怎么写好一个方法,我记得大四的时候彪哥推荐我去读《代码精进之路,从码农到工匠》这本书,那时候由于项目经验不多,所以对于书中很多东西没有感同身受,所以不以为然。


后面做的项目越来越多,从痛苦中不断思考,也看了一些优秀开源框架的实现,逐渐对于开始理解书中的思想。


下文只列举了书中的的“方法封装”,“参数”和“方法短小”这三个,其他的没有列举,因为我觉得其实只要这三点我们执行到位,写出来的代码虽然谈不上优秀,但是维护性和可读性一定会大大提高。


封装的艺术


我们在学习面向对象编程时就知道面向对象的几大特征是抽象,继承,封装,多态,所以学会合理的封装是一件很重要的事情,这也是我们每个程序员应该有的意识。


判断封装


在方法中,总是难免会有很多判断,判断越多,程序就会变得越复杂,但是又不能不判断,我们可以使用设计模式来改善判断很多的方法,使用设计模式来优化判断,比如策略模式,但是其实它并没有消除判断,换句话说,判断是消除不了,我们能做的只能是让判断的代码可读,下面我们来看一段代码。


如下是一个判断,对文件小于1G,类型为txt,文件名中包含user_click_data或者super_user_click_data的文件可以进行处理


对于这个判断,虽然条件并不复杂,但是在代码中就显得“不清洁”,特别是随着系统的不断迭代,加入的人员不断增多,那么判断将会越来越多,如果开发人员加上清晰的注释,那么就比较容易读懂,但是并不是谁都有这样的习惯。


之前我遇到的代码中,有些一个方法中四五十个判断,而且判断还特别复杂,各种&&,||等一大堆,读起来十分费劲,对于这样的代码,遇到真的会让人抓狂。


如何解决呢?


封装判断就是一个好的方案,上面的判断代码我们就可以进行封装,如下。



上面我判断我们将其抽出来作为一个单独的方法,在加上可理解的方法名,那么别人在读你代码的时候就很容易读懂,也保证了代码的清洁,当然,方法命名是一件很难的事情,不过如果命名得不够准确,我们可以加上清晰的注释来解决。


虽然上面这段代码看似没啥改变,但是随着系统的不断复杂,就能体现出它的作用了,我经常看到别人在主流程的代码中加入很多复杂的判断,在我刚开始写代码的时候,也做过这种事,很多时候如果注释不清,命名不规范,上下文不清晰,那么读起来就很痛苦,而且整个方法看起来让人特别难受。


所以,一个看似很小的改动,却能在时间的作用下显得如此重要,保持这样一个习惯,不仅是对自己负责,也是对他人负责。


参数问题


参数问题也是我们应该去考虑的问题,当在一个方法中,如果参数特别多是很不友好的,我之前碰到过一个方法有十五六个参数,首先这么多个参数,在传递的时候,如果使用对IDEA使用比较熟悉,那么可以使用快捷键查看下一个参数是什么类型,如果使用不熟悉,那么就需要点进方法中去看,这是一个体力活。


加上参数越多,维护成本就越高,比如我需要加一个参数,但是这个参数并不是必须的,有些方法用到,有些不用,但是由于使用的是同一个方法,所以调用这个方法的地方都需要加上这个参数,调用的地方越多,成本就越大,如果是一个RPC接口,修改的成本更大。


复杂的参数封装成类。


对于复杂的参数,推荐封装成类然后进行传递,在添加或者减少字段的时候是在类中进行,没有对形参进行增加或者减少,对调用此方法的地方基本没任何影响,我们只需要在需要添加或者减少这个参数的地方进行操作,不需要的地方就保持不动。


方法尽量保持短小


方法短小更容易让人阅读和维护,当我去阅读别人的代码的时候,有些类有七八千行代码,一个方法有五六百行代码的时候,其实内心是奔溃的!


当然,在我刚开始写代码的时候,我也不懂,所以一个方法从来不去优化,也是一个方法一口气干到底,我记得当时给老师做一个项目,一个类我在Controller层直接写了几百行,Service层都不用,还有写Vue,也不会去使用组件和封装函数,所以一个Vue文件几千行,当需要修改的时候,十分麻烦,找一个方法和变量也特别费力。


记得上一个星期,我和前端同事联调,我看他去找一个方法找了足足三四分钟,因为项目十分庞大,加上设计有很大的缺陷,我看他们一个VUE文件可达到八九千行,所以才出现这个问题。


其实无论对于前端还是后端,Java还是golang等等,代码的短小都比较容易去阅读和维护,如果发现方法越写越大,那我们就要考虑去提取,去拆分了,《代码精进之路》作者推荐Java一个方法不要超过20行,不过也不是绝对的。


对于像Spring这种特别复杂的框架,因为不断升级和迭代,所以有些方法也变得特别长,代码也变得可读性不那个高,但是也不影响它是Java生态中使用最广泛,最强大的框架。


只不过对于我们普通人来说,因为水平肯定没有Spring团队的强,所以保证方法的可读性是十分重要的。


今天的分享就到这里,感谢你的观看,我们下期见。


参考:《代码精进之路,从码农到工匠》weread.qq.com/web/reader/…?


作者:刘牌
来源:juejin.cn/post/7295237661617995828
收起阅读 »

开发一个简单的管理系统,前端选择 Vue 还是 React?

web
在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。 我们在低代码开发领域探索了多年...
继续阅读 »

在前端开发的世界中,React和Vue都是非常流行的JavaScript库,它们都提供了许多有用的功能来帮助开发者构建高质量的用户界面。然而,在我个人的开发经验中,相比于React,我更喜欢使用Vue。接下来讲讲我的实践经验。



我们在低代码开发领域探索了多年,从2014 开始研发低代码前端渲染,到 2018 年开始研发后端低代码数据模型,发布了JNPF快速开发平台。


JNPF是一个Vue2/Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件,无需编写代码即可完成业务需求。


前端采用的是Vue、Element-UI…;后端采用Java(.net)、Springboot…;使用门槛低,支持分布式、k8s集群部署,适用于开发复杂的业务管理系统(ERP、MES等);采用可视化组件模式可以有效地扩展不同的业务功能,并方便实现各种业务需求,且不会导致系统臃肿,若想使用某个组件,按需引入即可,反之亦然。



低代码平台的前端框架采用Vue的优势有哪些?




  •  Vue是组件化开发,减少代码的书写,使代码易于理解。




  •  最突出的优势在于可以对数据进行双向绑定。




  •  相比较传统的用超链接进行页面的切换与跳转,Vue使用的是路由,不用刷新页面。




  •  Vue是单页应用,加载时不用获取所有的数据和dom,提高加载速度,优化了用户体验。




  •  Vue的第三方组件库丰富,低代码平台能够获得更多的支持和资源。




JNPF-Web-Vue3 的技术栈介绍


JNPF 快速开发平台的 Vue3.0 版本是基于 Vue3.x、Vue-router4.x、Vite4.x、Ant-Design-Vue3.x、TypeScript、Pinia、Less 的后台解决方案,采用 Pnpm 包管理工具,旨在为中大型项目做开发,提供开箱即用的解决方案。前端同时适配Vue2/Vue3技术栈。


以下对各项技术做简单的拓展介绍:


(1)Vue3.x

Vue3.x 作为一款领先的 JavaScript 框架,通过响应式数据绑定和组件化架构实现高效的应用开发。相较于 Vue2.x,在大规模应用场景下,Vue3.x 的渲染速度提升了近 3 倍,初始化速度提升了 10 倍以上,这不仅为我们提供了更出色的用户体验,也为企业应用的开发和维护提供了极大的便利。


此外,它所支持Composition API 可以更加灵活地实现代码复用和组件化,让我们的代码更加可读、可维护。总而言之,Vue3 在许多方面都进行了改进,包括更好的性能、更少的代码大小和更好的开发体验。


(2)Vue-router4.x

Vue-router4.x 作为 Vue.js 框架中的路由管理器,具备出色的性能和扩展性,为开发者提供了一种高效而灵活的前端路由解决方案。Vue Router 主要用于构建单页应用程序,允许创建可导航的Web 应用,使您可以轻松地构建复杂的前端应用。


(3)Vite4.x

一个基于 ES Module 的 Web 应用构建工具。作为一种全新的开发模式,Vite 相对于Webpack 更加出色,内置了许多优化手段,包括 HMR、代码分割、CSS 提取、缓存策略等,从而在保证开发速度的前提下,为应用程序的加载速度和性能提供了极致的保障。此外,它还支持快速的冷启动、模块化的打包方式以及自动化的多页面构建等特性,极大的提升了前端开发效率。


(4)Ant-Design-Vue3.x

一款基于 Vue3.x 的企业级 UI 组件库,旨在帮助开发者快速搭建出高质量、美观且易用的界面。不同于其他类似的组件库,Ant-Design-Vue3.x 更注重用户体验和可定制性,提供了一整套视觉、交互和动画设计解决方案,结合灵活的样式配置,可以满足大部分项目的UI 需求,帮助开发者事半功倍。


(5)TypeScript

TypeScript 作为一种静态类型的 JavaScript 超集,不仅完美兼容 JavaScript,还提供了强大的静态类型约束和面向对象编程特性,极大地提升了代码的可读性和重用性。TypeScript拥有强大的类型系统,可以帮助开发者在代码编写阶段发现潜在的错误,减少未知错误发生概率,并提供更好的代码补全和类型检查。这一特性让团队协作更加高效,同时也降低了维护代码的成本。


(6)Pinia

Pinia 是 Vue3.x 的状态管理库,基于 Vue3.x 的 Composition API 特性,为开发者提供了清晰、直观、可扩展和强类型化的状态管理方案,可以更好地管理应用数据和状态。无论是在小型项目还是庞大的企业级应用中,我们都可以依靠这个强大的状态管理库来迅速构建出高质量的应用。


(7)Less

一种 CSS 预处理器,能够以更便捷、灵活的方式书写和管理样式表。通过 Less,开发者可以使用变量、嵌套规则、混合、运算、函数等高级功能,使得样式表的编写更加简单、易于维护。使用 Less 不仅可以提高 CSS 开发效率,还可以生成更快、更小的 CSS 文件,从而减少网站加载时间,提升网站性能。


(8)Pnpm

Pnpm 作为一种快速、稳定、安全的包管理工具,它能够帮助我们管理 JavaScript 包的依赖关系,通过采用更为精简的数据存储结构,极大地减少冗余数据的存储,从而有效地节省磁盘空间。


其他亮点


作为一款基于SpringBoot+Vue3的全栈开发平台,满足微服务、前后端分离架构,基于可视化流程建模、表单建模、报表建模工具,快速构建业务应用,平台即可本地化部署,也支持K8S部署。


引擎式软件快速开发模式,除了上述功能,还配置了图表引擎、接口引擎、门户引擎、组织用户引擎等可视化功能引擎,基本实现页面UI的可视化搭建。内置有百种功能控件及使用模板,使得在拖拉拽的简单操作下,也能大限度满足用户个性化需求。


如果你是一名开发者,可以试试我们研发的JNPF开发平台。基于低代码充分利用传统开发模式下积累的经验,高效开发。


最后,给予一点建议


关于Vue,简单易上手,官方的文档很清晰,易于使用,同时它拥有更好的新能且占据的空间相比其他框架更少,同时vue的学习曲线是很平滑的,所以这是我为什么推荐优先学习vue的原因,对于新手来说易上手,快速帮助新手熟悉一些中小型的项目,但是对于大型的项目,这就要说到Vue响应机制上的问题了,大型项目的state(状态)是特别多的,这时watcher也会很多,进而导致卡顿。


对于React,主要是适应大型项目,由于React灵活的结构和可扩展性,相比Vue对于大型项目的适配性更高,此外其跨浏览器兼容、模块化、单项数据流等都是其优点,但是与Vue相反的就是它的学习曲线是陡峭的,由于复杂的设置过程,属性,功能和结构,它需要深入的知识来构建应用程序,这对于新手来说是不太适合作为一个入门级别的框架。


作者:冲浪中台
来源:juejin.cn/post/7295565904405790761
收起阅读 »

用1100天做一款通用的管理后台框架

web
前言 去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。 很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大...
继续阅读 »

前言


去年年底,我写了一篇《如何做好一款管理后台框架》的文章,这是我对开发 Fantastic-admin 这款基于 Vue 的中后台管理系统框架两年多时间的一个思考与总结。


很意外这么一篇标题平平无奇的文章能收获 30k 的浏览以及 600 多个收藏,似乎大家对这种非干货的文章也挺感兴趣。于是在这个三年的时间点上(没错,也就是1100天),我打算继续出来和大家唠唠,这一年我又做了些什么事,或者说,如何把一款好的后台框架变得通用?


题外话:如果你对我以前的文章感兴趣,可以点我头像进入主页查看;如果你期待我以后的文章,也可以点个关注。


痛点


因为 Fantastic-admin 是基于 Element Plus 这款 UI 组件库进行开发的,于是今年我陆陆续续被问到一些问题:



  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?

  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?

  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



类似的问题一多,我也在思考一个问题:我的这款框架是不是被 Element Plus 绑架了?如果开发者在做技术选型的时候,因为 UI 组件库不符合预期,而将我的框架筛掉,这是我不希望看到的结果。


基于这个潜在隐患,我开始计划对框架进行转型。


方案


方案一


既然开发者对 UI 组件库有各自的偏好,我又想拉拢这部分开发者,那是不是多出几套不同 UI 组件库版本的就可以了呢?没错,这是我最开始冒出来的念头。


我参考了一些同类产品的做法,尽管它们把不同 UI 组件库版本做得很像,但在使用体验过程中,还是会带来操作上的割裂感。并且因为无法抹平不同 UI 组件库在 API 上的差异,导致在框架功能上,不同版本之间也会有一些差异。



你可以分别对比左右或者上下两张图,包括左侧导航栏的样式、导航收起/展开按钮的位置、右侧项目配置中提供的功能等,都能明显发现它们的差异。


虽然这可能不是什么大问题,但我认为视觉风格上的统一是能帮助产品提高识别度的。就比如上面 4 款基于不同 UI 组件库开发的后台框架,虽然它们属于同一个产品,但如果我不告诉你,你未必能通过图片确定它们师出同门。


其次就是后台框架提供的功能不统一,这里面有一定的原因是因为 UI 组件库导致的。试想一个场景,如果你要从 Element Plus 版本的后台,迁移到 Ant Design Vue 版本的后台,框架的配置文件是否能原封不动的复制过去?如果导航(路由)数据是后端返回的,数据结构能否保持完全一致,后端无需做任何修改?因为不同 UI 组件库对菜单组件的使用方式是完全不同的,比如 Element Plus 是需要手动拼装的,而 Naive UI 则是数据驱动的,只需要传入一个树形结构的数据给组件即可。如果数据结构无法保证一致,就会增加迁移和学习的成本。


最后就是我的一点私心,因为多一个 UI 组件库的版本,势必会占据我更多的业余时间,如果同时维护 4、5 个版本,那我大概下班后的所有时间都要投入到其中,并且如果未来又有新的 UI 组件库成为流行,那就又多一个版本的维护,这并不是一个可持续发展的方案。


方案二


既然上一个方案不符合我的期望,于是我开始思考,框架本身能不能不依赖这些 UI 组件库?如果框架本身不依赖于三方的 UI 组件库,那开发者不就可以根据需要自行引入想要的组件库了么。



就如上图,主/次导航和顶栏是属于框架的部分,而这部分其实并没有用到太多 UI 组件库提供的组件,以 Element Plus 举例,我统计了一下目前 Fantastic-admin 用到的组件:



  • Menu 菜单(主/次导航)

  • Breadcrumb 面包屑(顶栏)

  • Popover 气泡卡片(顶栏右侧的工具栏)

  • Dropdown 下拉菜单(顶栏右侧的工具栏)

  • Drawer 抽屉(应用配置)

    • Message 消息提示

    • Button 按钮

    • Input 输入框

    • Radio 单选框

    • Select 选择器

    • Switch 开关

    • …(等等表单类组件)




可以看到,虽然抽屉组件里用了很多表单类的组件,但这部分组件都是在应用配置里使用的,而应用配置这个模块,主要是方便在线测试框架提供的各种功能,在实际业务开发中,是完全不需要这个模块的。



所以初步算下来,后台框架真正依赖于 Element Plus 实现的组件就只有 4 个:



  • Menu 菜单

  • Breadcrumb 面包屑

  • Popover 气泡卡片

  • Dropdown 下拉菜单


那我为什么不找一些独立的第三方插件替代呢?是的,这是我第二个方案,就是找一些独立的插件替换 UI 组件库中的组件。但问题也立马迎面而来,就是偌大一个 Github ,居然找不到符合我需求和审美的插件。


比如菜单插件,我希望它和 Element Plus 里的菜单组件在功能上没有太大差异,支持水平/垂直模式、支持折叠收起、支持设置默认激活菜单、支持默认展开等。


比如面包屑插件,或许是因为这个插件功能太简单,并且大部分 UI 组件库都有提供,在 Github 能搜到独立的面包屑插件很少,搜到的也基本上是 N 年前的上传的,既没有人维护,风格样式也很丑。


这个方案似乎也行不通……吗?


方案三


虽然方案二在实施的第一步就扑街了,但有一点思路还是正确的,就是让框架本身不依赖于三方 UI 组件库。既然网上搜不到合适的插件,那我为什么不自己写一个呢。


比如面包屑,这是一个很简单的功能,任何前端初学者应该都可以写一个面包屑组件。


而气泡卡片和下拉菜单我没有计划自己写,因为找到了一个还不错的插件 Floating Vue,它由 Vue 团队核心人员开发并维护,并且最重要的是它支持自定义样式,意味着我可以将它魔改成想要的样子,尽可能和我的框架在视觉风格上保持统一。


最后一个比较难啃的骨头就是菜单,因为找不到合适的替代品,自己写的话又比较有挑战,虽然我有一点实现思路,但不多。当然最终还是决定自己写一个,因为觉得三方 UI 组件库这么多,实在写不出来我就去读他们源码,总不能每一个源码我都读不懂吧。


这 4 个组件的替换方案确定后,剩下就是抽屉组件和它里面的一些表单组件了,这些要怎么解决呢?这会我想到了 Headless UI ,它是完全无样式的 UI 组件库,通过与 Tailwind CSS / UnoCSS 集成使用,可以快速构建出属于自己风格的组件。


但是 Headless UI 提供的组件非常有限,并不能覆盖我需要的表单组件。不过它的设计给了我启发。表单组件我并不需要非常复杂的功能,原生的表单控件其实就能满足我的使用需求,只是原生的样式比较丑,和我想要的风格不统一,那我只需要给他们定制一套统一的风格就可以了,也就写一套原子化的 CSS 样式。


于是,方案敲定,开始实操。


实操


我决定从易到难开始处理,因为这样在初期能快速看到进度推进,也避免一上来就被一个菜单功能卡住好几天,甚至十几天都没有进展,打击到自己的信心。


1. 面包屑


和预期一样,并没有什么难度,很轻松就实现了。只不过目前还是保持和 Element Plus 一样的使用方式,就是需要手动拼装,后期计划改成数据驱动的使用方式。



2. 气泡卡片 & 下拉菜单


这部分参考了 nuxt/devtoolsFloating Vue 的自定义样式,以及 nuxt/ui 中下拉菜单的样式风格,最终形成了我自己满意的风格



3. 抽屉


使用了 Headless UI 中的 Dialog 组件,因为它和抽屉组件有相同的交互方式,它们都是在遮罩层上展示内容,只不过 Dialog 更多时候是居中展示,而抽屉则是在左右两侧展示。


其次在使用过程中,发现 Headless UI 中的 Transition 组件是一个惊喜。虽然 Vue 本身就有提供 <transition> 组件用于处理过渡动画,但有一个场景会比较难处理,官方的描述是:



This technique is needed when you want to coordinate different animations for different child elements – for example, fading in a Dialog's backdrop, while at the same time sliding in the contents of the Dialog from one side of the screen.
当您要为不同的子元素协调不同的动画时,就需要使用这种技术,例如,在淡入对话框背景的同时,从屏幕的一侧滑入对话框的内容。



这说的不就是抽屉组件么?于是按照官方的示例,修改了整体风格,最终效果也就出来了。



4. 表单组件


之前的计划是修改原生表单控件的样式,但在开发过程中发现会有一定的局限性。比如 <select> 无法控制弹出选项框的样式,我的解决办法就是用 Floating Vue 封装模拟一个 select 组件。


同时也在开发过程中发现了一些被遗漏组件,于是边做边补,最终大概做了 10 多个组件。虽然看着不少,它们都秉持着最小可用的状态。什么意思呢?就是我不会给它们设计太多的 API ,因为它们的定位和三方 UI 组件库不同,它们只要满足框架本身使用即可,用不到的 API 不会进行开发。并且使用上也不会有太大负担,如果不是对框架进行二次开发,开发者是可以完全不用关注这部分组件。



5. 菜单


菜单组件确实是个难啃的骨头,我差不多用了 3 周的晚上时间去开发。


第一周,按照自己的思路徒手撸,做到一半卡壳,做不下去了;


第二周,开始看 Element Plus 、Naive UI 、Ant Design Vue 里菜单的源码;



Ant Design Vue 的没看懂,放弃;


Naive UI 的看到一半发现核心实现被作者封装到 treemate 这个独立包中了,虽然这个包是开源的,目的也是针对树形结构的一体化解决方案。但我粗略看了一遍文档,感觉有点大材小用,因为它有很多 API 我是用不到的,而我对菜单组件又有一些自己的想法,不确定是否它这个包能否满足我的需求,放弃;


最后选择看 Element Plus 的,通过在本地一点点打印数据,大概理解了实现思路,但组件递归调用,父子组件通过 provide / inject 传递数据和函数的方式,数据状态的变动也是一层层向上级组件通知,直到通知到顶层组件,在我看来有点不太优雅,如果数据能统一在顶层组件里操作就好了。其次我的计划是写一个数据驱动的菜单组件,而不是像 Element Plus 需要手动拼装的,所以虽然我大致看懂了 Element Plus 菜单组件是怎么实现的,但在我自己实现的时候,还是有很大的不同,能参考的代码并不多。


这部分的开发总结,我可能会在以后单独写一篇文章详细说说,因为这部分也是整个方案中唯一的难点。



第三周,因为实现思路大致有了,所以开发上就没有太多的卡壳,最终结果也还不错,基本达到了我的需求。


同时因为组件完全可控,顺带解决了之前使用 Element Plus 菜单组件上无法解决的 bug ,比如当菜单收起时,弹出的悬浮菜单如果数量过多,超出屏幕高度,超出的部分就无法查看了,就像这样:



但是现在则会有滚动条,使用体验上更舒服。



验证


至此,我的后台框架已经摆脱对 Element Plus 的依赖,接下来就需要验证一下是否可以方便的替换成其他 UI 组件库。


我分别用 Ant Design Vue 、Arco Design Vue 、Naive UI 、TDesign 这四款热度比较高的组件库进行了验证:















Ant Design Vue Arco Design Vue Naive UI TDesign

结果还是很满意的,都能够顺利替换,并且替换过程并没有花费很多时间,一个小时内就可以替换成功。



由于登录页这个特殊的存在,替换组件库后是需要对其用到的 Element Plus 组件进行手动修改的,这部分会比较花时间,因为会涉及到表单验证之类的东西,不同组件库的写法差异还是比较大的。



详细的替换步骤可以在 Fantastic-admin 官方文档里找到。


回顾


让我们重新看下一开始的痛点是否都解决了么:




  • 以后会有 Ant Design Vue 版本么?会有 Naive UI 版本么?会有 …… 版本么?



    虽然不会有,但可以自己动手,根据教程将默认的 Element Plus 替换成你想要的 UI 组件库就可以了





  • 我们公司/团队有一套内部的 UI 组件库,可以在 Fantastic-admin 里使用么?会和 Element Plus 有冲突么?



    不会有冲突,现在可以彻底移除 Element Plus ,安装并使用自己的 UI 组件库





  • 我们有一些老项目希望迁移到 Fantastic-admin 上来,但 UI 组件库用的不是 Element Plus ,有什么办法么?



    可以用 Fantastic-admin 源码先进行 UI 组件库的替换,之后再将老项目的业务代码逐部迁移





除了解决这些痛点,甚至还有新收获:




  • 帮助公司/企业打造视觉风格统一的产品,提高产品辨识度



    大公司可能有不止一个项目团队,不同项目团队的技术偏好可能无法完全统一,导致开发的后台长得也千变万化。但即使在这种情况下,使用 Fantastic-admin 依旧可以保持整体视觉风格上的统一。





  • 近乎于 0 的上手成本



    因为后台框架始终都只有一套,开发者不会因为切换 UI 组件库后,要重新了解后台框架的使用





  • 维护成本更低,产品生命周期更长



    这一点是对我自己说的,不管未来会出现多少个新的 UI 组件库,我都不需要去新增一个版本进行单独维护;或者 Element Plus 如果有一天停止维护了,我的产品也不会因此进入了死亡倒计时





总结


文章写到这里,差不多就结束了,虽然阅读一遍可能只花了不到10分钟,但为了做成这件事,我大概从今年 6 月份就开始构思了,也是花了蛮多的精力,所以很感谢你的耐心。


当一款产品做到第 4 个年头,周围大部分同类产品都进入到半停更的状态,这一年里我经常思考如何延长产品的生命周期,如何让更多人来使用,而这篇文章就是对我自己今年的一个总结,也是一份答卷,希望大家能喜欢。


另外,Fantastic-admin V4.0 已经正式发布,感兴趣的朋友可以来看看,或许你的下一个项目,就可以用上了。


作者:Hooray
来源:juejin.cn/post/7295624857432850468
收起阅读 »

终结屏幕适配这个话题

物理像素、逻辑像素、百分比适配 日常开发中,接触到最多的屏幕相关的单位,分别是物理像素(px),逻辑像素(dp, point)。 那物理像素和逻辑像素的区别是? 这里以一张 3x4px 的图片举例。假设该图片放置在 5x6px 的设屏幕中。如下图所示。 此时...
继续阅读 »

物理像素、逻辑像素、百分比适配


日常开发中,接触到最多的屏幕相关的单位,分别是物理像素(px)逻辑像素(dp, point)


那物理像素和逻辑像素的区别是?


这里以一张 3x4px 的图片举例。假设该图片放置在 5x6px 的设屏幕中。如下图所示。



此时想象这个图片放置在 `10*12px` 的屏幕中会是怎样呢。对比如下,会发现该图片放置在分辨率更高的屏幕中会变得非常狭小。

image.png
继续我们的例子,如果该屏幕想要保证图片能跟前面的低分辨率的设备显示效果一致的话,则图片的宽高应增加1倍的大小。即设备需要2倍的像素比例dpr(device percent ratio)。这样图片3*4逻辑像素的尺寸的图片在高分辨率设备中可以映射成6*8物理像素,而在低分辨率的设备(像素比例时1的设备),则3*4逻辑像素的图片映射为3*4物理像素的图片。


这是逻辑像素的大致机制。逻辑像素会根据目标设备的分辨率和尺寸计算出设备的缩放比例。逻辑像素出现是为了让不同分辨率的设备中显示相同的内容能取得大致相同的效果,当然逻辑像素并不是这样简单的百分比换算。


在Android中这个逻辑像素是dp,而ios中则是pt。在android中dp的换算公式中具体换算公式想了解的可以点击下面链接了解。
betterprogramming.pub/cracking-an…


在Android开发中将不同分辨率设备的中的物理像素比率进行如下分类。所以假设设备是230dpi的话也以hdpi1.5倍进行换算。所以这跟百分比的换算是不太一样的。以“微信”应用举例。


底部的Tab(微信、通讯录、发现,我),假设设计图中屏幕的宽度是375dp,根据tab均分,单个tab为93.75。你如果通过水平布局指定宽度为93.75逻辑像素的话则会发现出来的效果在某些手机上并不是均分的。


如下图类似微信界面运行在Iphone 14 pro。此时应该用百分比进行适配,即在不同的分辨率中基于设计图的尺寸进行等比例换算。如:设计图的分辨率为375*812,而显示设备的分辨率为1080*1920,则设计图上1像素相当于目标设备1 ✖️ “显示设备基于设计图的比例(1080/375=2.88)”像素,即 1✖️2.88=2.88像素。这就是百分比适配。对比下图可以发现逻辑像素适配的“我”是偏左的。


image.png


image.png


百分比适配是一种根据设计图的尺寸和设备的分辨率,以百分比的方式进行换算和适配的方法。通过计算设计图上的像素与目标设备分辨率的比例,可以得到百分比像素的值,从而实现在不同分辨率的设备上保持一致的布局和显示效果。


但是百分比并不是万能的。如下图逻辑像素适配和百分比像素适配的对比。在列表中,百分比布局则会出现一个问题。你会发现在大尺寸高分辨率的设备中,列表中的每一项都特别大。则如果用逻辑像素(dp、pt)则是这样。使用逻辑像素能充分发挥大屏的优势,屏幕越大显示的内容更多。


image.png


什么时候应该用逻辑像素,百分比像素。


具体什么时候应该用逻辑像素和百分比像素适配,取决于设计图UI。根据不同设计意图决定何种方案。大部分情况下使用逻辑像素不会出现什么问题,列表item必定使用逻辑像素。但是什么时候应该用百分比像素呢?


举个例子:



ps: 例子中我会以百分比像素表示将设计图像素根据不同分辨率设备等比例换算的像素。即1百分比像素= 1✖️ [(设计图分辨率)/ (目标设备分辨率)]。



下面是一个“购买成功”的UI图。中间有个票根信息。票根信息有个票根背景图片。


标注图中的屏幕分辨率为 393*852


image.png


这里票根信息UI应该用逻辑像素还是百分比像素适配呢?


通过标注图能明显看出票根信息在宽度上固定需要占用一定比例。所以这里宽度应该为 353百分比像素 。为了宽高比例正确,故高度也应为 346百分比像素 。注意这里高度的 346百分比像素 也应该是基于屏幕宽度 393 的百分比像素。即 目标设备屏幕宽度 * 346 / 393


因为整个票根的宽高都为百分比适配,则里面子部件的摆放、间距都应按照百分比的方式进行适配。不然则会出现子部件没法像标注图那样正确对齐的情况。


总结


物理像素(px)是屏幕上的实际物理点,表示屏幕上显示内容的最小单位。逻辑像素(dp、pt)是开发中使用的抽象单位,与物理像素的关系由设备的像素密度决定。


逻辑像素是开发中使用的抽象单位,它们与物理像素之间有一个映射关系。在不同的设备上,逻辑像素的布局和大小是相对统一的。使用逻辑像素可以让开发者在不同分辨率的设备上保持一致的布局和显示效果。


百分比适配是一种根据设计图的尺寸和设备的分辨率,以百分比的方式进行换算和适配的方法。通过计算设计图上的像素与目标设备分辨率的比例,可以得到百分比像素的值,从而实现在不同分辨率的设备上保持一致的布局和显示效果。


一般情况下,使用逻辑像素可以保持在不同设备上显示内容的一致性和最佳效果,特别是在涉及列表和大屏幕显示的情况下,需要根据设计图,决定使用何种方案。可以通过先分析使用逻辑像素思考是否合理,再考虑百分比适配的情况。在一些特定的设计需求下,如背景图片的铺满屏幕、比例布局等,可以考虑使用百分比适配来实现更精确的布局和显示效果。


作者:淹没
来源:juejin.cn/post/7294853623849812002
收起阅读 »

如何用Compose TV写电视桌面

写在前面 Compose TV 最近出来已经有一段时间,对电视开发支持的非常好,比如标题,横向/纵向列表,焦点等. 下图为最终效果成品。 Demo源码地址 整体UI框架搭建 标题(TabRow) + NatHost(内容切换) + 内容(TvLazyColu...
继续阅读 »

写在前面


Compose TV 最近出来已经有一段时间,对电视开发支持的非常好,比如标题,横向/纵向列表,焦点等.


下图为最终效果成品。



Demo源码地址


整体UI框架搭建


标题(TabRow) + NatHost(内容切换) + 内容(TvLazyColumn)



标题-TabRow



val tabs = listof("我的", "影视", "应用")

TabRow(
selectedTabIndex = selectedTabIndex,
indicator = { tabPositions, isActivated ->
// 移动的白色色块
TopBarMoveIndicator(...
}
) {
tabs.forEachIndexed { index, title ->
Tab(
// colors设置了 默认,上焦,选中的颜色
colors = TabDefaults.pillIndicatorTabColors(
contentColor = Color.White,
focusedContentColor = Color.Black,
selectedContentColor = Color.White,
)
...
) {
Text(...)
}
}
}

移动的白色色块,这里只是我写的Demo,都是可以自定义的.


fun TopBarMoveIndicator(
currentTabPosition: DpRect,
isFocused: Boolean
)
{
val width by animateDpAsState(targetValue = currentTabPosition.width, label = "width")
val height = if (isFocused) currentTabPosition.height else 2.dp
val leftOffset by animateDpAsState(targetValue = currentTabPosition.left, label = "leftOffset")
// 有焦点的时候,是矩形,无焦点的时候,是下划线.
val moveShape = if (isFocused) ShapeDefaults.ExtraLarge else ShapeDefaults.ExtraSmall

Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset(leftOffset, currentTabPosition.top)
.width(width)
.height(height)
.background(color = Color.White, shape = moveShape)
.zIndex(-1f)
)
}

NatHost(内容切换) + 内容(TvLazyColumn)


内容切换


NatHost 功能类似 ViewPager,对 "我的","影视","应用" 几个 页面内容进行切换.


NavHost(
...
builder = {
composable(...) { // 我的
// 我的野蛮
}
composable(...) {// 影视
// 影视页面
}
composable(...) { // 应用
// 应用页面
}
}
)

内容布局


TvLazyColumn 与 LazyColumn 功能是差不多的,纵向布局,就不过多赘述,具体看谷歌的开发文档,网上相关视频教程 或 看DEMO源码.


TvLazyColumn(
...
) {
item {
ImmersiveList(...) // 沉浸式列表
}
item {
TvLazyRow(...) // 热门推荐
}
item {
TvLazyRow(...)
}
item {
TvLazyRow(...) // 豆瓣高分
TvLazyRow(...)
}
item {
TvLazyRow(...) // 预热抢先看
}
... ...
}

TvLazyColumn的相关参数,记住这个参数 pivotOffsets,它是设置滚动的位置的,比如设置滚动一直在中间位置.


fun TvLazyColumn(
modifier: Modifier = Modifier,
state: TvLazyListState = rememberTvLazyListState()
,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
userScrollEnabled: Boolean = true,
pivotOffsets: PivotOffsets = PivotOffsets(),
content: TvLazyListScope.() -> Unit
)

TvLazyRow + Item


TvLazyColumn 每行又包含了 TvLazyRow 横向布局 (如果是固定的几个,可以用 Row。


自定义的布局可以用 Surface 包含的,几个关键属性, Scale(放大),Border(边框),Glow(阴影)。



TvLazyRow(...) {
items(...) { ...
Surface(
onClick = {//点击事件}
scale = ClickableSurfaceDefaults.scale(...),
border = ClickableSurfaceDefaults.border(...),
glow = ClickableSurfaceDefaults.glow(...)
) {
// 你自定义的卡片内容,比如 图片(AsyncImage) + 文本(Text)
}
}
}

我Demo里面用的是 谷歌提供的一个包含 图片+文本的控件 StandardCardLayout


ImmersiveList 沉浸式列表

有点类似 爱奇艺,腾讯,哔哩哔哩等电视应用这种列表.


ImmersiveList(
modifier = Modifier.onGloballyPositioned { currentYCoord = it.positionInWindow().y },
background = {
// 背景图片内容
}
) {
// 布局内容
// 大标题 + 详情
// TvLazyRow
}

TV其它控件推荐


Carousel 轮播界面



TvLazyVerticalGrid/TvLazyHorizontalGrid


ModalNavigationDrawer抽屉式导航栏


ListItem


分辨率适配


TV开发涉及分辨率适配问题,Compose 也能很简单的处理此问题,无论你在1920x1080,还是1280x720等分辨率下,无缝切换,毫无压力.


val displayMetrics = LocalContext.current.resources.displayMetrics
val fontScale = LocalDensity.current.fontScale
val density = displayMetrics.density
val widthPixels = displayMetrics.widthPixels
val widthDp = widthPixels / density
val display = "density: $density\nwidthPixels: $widthPixels\nwidthDp: $widthDp"
KLog.d("display:$display")
CompositionLocalProvider(
LocalDensity provides Density(
density = widthPixels / 1920f,
fontScale = fontScale
)
) {
// 我们写的Compose主界面布局
}

参考资料


What's new with TV and intro to Compose


Android TV 上使用 Jetpack Compose


Compose TV官方设计文档


JetStreaamCompose TV demo


Compose TV demo


写在后面


近几年Android推出了很多东西,我的心尖尖是 MVI,flow(完爆Rxjava),Compose>>>


TV开发的发展,一开始是 RecycleView,要去解决焦点,优化等问题,后来是Leanback,到现在的Compose TV(开发速度提升了很多很多).


我也真的很喜欢Compose的写法,简单明了,强烈推荐Compose TV开发电视,我相信谷歌,能将Compose性能优化的越来越好.


最后一篇TV开发的文章了,以后搞车载相关去了.


作者:冰雪情缘long
来源:juejin.cn/post/7294907512444010559
收起阅读 »

Android使用Hilt依赖注入,让人看不懂你代码

前言 之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt...
继续阅读 »

前言


之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt不了解或是想了解得更多,那么接下来的内容将助力你玩转Hilt。


通过本篇文章,你将了解到:




  1. 什么是依赖注入?

  2. Hilt 的引入与基本使用

  3. Hilt 的进阶使用

  4. Hilt 原理简单分析

  5. Android到底该不该使用DI框架?



1. 什么是依赖注入?


什么是依赖?


以手机为例,要组装一台手机,我们需要哪些部件呢?

从宏观上分类:软件+硬件。

由此我们可以说:手机依赖了软件和硬件。

而反映到代码的世界:


class FishPhone(){
val software = Software()
val hardware = Hardware()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}
//软件
class Software() {
fun handle(){}
}
//硬件
class Hardware() {
fun handle(){}
}

FishPhone 依赖了两个对象:分别是Software和Hardware。

Software和Hardware是FishPhone的依赖(项)。


什么是注入?


上面的Demo,FishPhone内部自主构造了依赖项的实例,考虑到依赖的变化挺大的,每次依赖项的改变都要改动到FishPhone,容易出错,也不是那么灵活,因此考虑从外部将依赖传进来,这种方式称之为:依赖注入(Dependency Injection 简称DI)

有几种方式:




  1. 构造函数传入

  2. SetXX函数传入

  3. 从其它对象间接获取



构造函数依赖注入:


class FishPhone(val software: Software, val hardware: Hardware){
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone的功能比较纯粹就是打电话功能,而依赖项都是外部传入提升了灵活性。


为什么需要依赖注入框架?


手机制造出来后交给客户使用。


class Customer() {
fun usePhone() {
val software = Software()
val hardware = Hardware()
FishPhone(software, hardware).call()
}
}

用户想使用手机打电话,还得自己创建软件和硬件,这个手机还能卖出去吗?

而不想创建软件和硬件那得让FishPhone自己负责去创建,那不是又回到上面的场景了吗?


你可能会说:FishPhone内部就依赖了两个对象而已,自己负责创建又怎么了?


解耦


再看看如下Demo:


interface ISoftware {
fun handle()
}

//硬件
interface IHardware {
fun handle()
}

//软件
class SoftwareImpl() : ISoftware {
override fun handle() {}
}

//硬件
class HardwareImpl : IHardware {
override fun handle() {}
}

class FishPhone() {
val software: ISoftware = SoftwareImpl()
val hardware: IHardware = HardwareImpl()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone 只关注软件和硬件的接口,至于具体怎么实现它不关心,这就达到了解耦的目的。
既然要解耦,那么SoftwareImpl()、HardwareImpl()就不能出现在FishPhone里。

应该改为如下形式:


class FishPhone(val software: ISoftware, val hardware: IHardware) {
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

消除模板代码


即使我们不考虑解耦,假若HardwareImpl里又依赖了cpu、gpu、disk等模块:


//硬件
class HardwareImpl : IHardware {
val cpu = CPU(Regisgter(), Cal(), Bus())
val gpu = GPU(Image(), Video())
val disk = Disk(Block(), Flash())
//...其它模块
override fun handle() {}
}

现在仅仅只是三个模块,若是依赖更多的模块或者模块的本身也需要依赖其它子模块,比如CPU需要依赖寄存器、运算单元等等,那么我们就需要写更多的模板代码,要是我们只需要声明一下想要使用的对象而不用管它的创建就好了。


class HardwareImpl(val cpu: CPU, val gpu: GPU, val disk: Disk) : IHardware {
override fun handle() {}
}

可以看出,下面的代码比上面的简洁多了。




  1. 从解耦和消除模板代码的角度看,我们迫切需要一个能够自动创建依赖对象并且将依赖注入到目标代码的框架,这就是依赖注入框架

  2. 依赖注入框架能够管理依赖对象的创建,依赖对象的注入,依赖对象的生命周期

  3. 使用者仅仅只需要表明自己需要什么类型的对象,剩下的无需关心,都由框架自动完成



先想想若是我们想要实现这样的框架需要怎么做呢?

相信很多小伙伴最朴素的想法就是:使用工厂模式,你传参告诉我想要什么对象我给你构造出来。

这个想法是半自动注入,因为我们还要调用工厂方法去获取,而全自动的注入通常来说是使用注解标注实现的。


2. Hilt 的引入与基本使用


Hilt的引入


从Dagger到Dagger2再到Hilt(Android专用),配置越来越简单也比较容易上手。

前面说了依赖注入框架的必要性,我们就想迫不及待的上手,但难度可想而知,还好大神们早就造好了轮子。

以AGP 7.0 以上为例,来看看Hilt框架是如何引入的。


一:project级别的build.gradle 引入如下代码:


plugins {
//指定插件地址和版本
id 'com.google.dagger.hilt.android' version '2.48.1' apply false
}

二:module级别的build.gradle引入如下代码:


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//使用插件
id 'com.google.dagger.hilt.android'
//kapt生成代码
id 'kotlin-kapt'
}
//引入库
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'

实时更新最新版本以及AGP7.0以下的引用请参考:Hilt最新版本配置


Hilt的简单使用


前置步骤整好了接下来看看如何使用。


一:表明该App可以使用Hilt来进行依赖注入,添加如下代码:


@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}

@HiltAndroidApp 添加到App的入口,即表示依赖注入的环境已经搭建好。


二:注入一个对象到MyApp里:

有个类定义如下:


class Software {
val name = "fish"
}

我们不想显示的构造它,想借助Hilt注入它,那得先告诉Hilt这个类你帮我注入一下,改为如下代码:


class Software @Inject constructor() {
val name = "fish"
}

在构造函数前添加了@Inject注解,表示该类可以被注入。

而在MyApp里使用Software对象:


@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: Software

override fun onCreate() {
super.onCreate()
println("inject result:${software.name}")
}
}

对引用的对象使用@Inject注解,表示期望Hilt帮我将这个对象new出来。

最后查看打印输出正确,说明Software对象被创建了。


这是最简单的Hilt应用,可以看出:




  1. 我们并没有显式地创建Software对象,而Hilt在适当的时候就帮我们创建好了

  2. @HiltAndroidApp 只用于修饰Application



如何注入接口?


一:错误示范
上面提到过,使用DI的好处之一就是解耦,而我们上面注入的是类,现在我们将Software抽象为接口,很容易就会想到如下写法:


interface ISoftware {
fun printName()
}

class SoftwareImpl @Inject constructor(): ISoftware{
override fun printName() {
println("name is fish")
}
}

@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: ISoftware

override fun onCreate() {
super.onCreate()
println("inject result:${software.printName()}")
}
}

不幸的是上述代码编译失败,Hilt提示说不能对接口使用注解,因为我们并没有告诉Hilt是谁实现了ISoftware,而接口本身不能直接实例化,因此我们需要为它指定具体的实现类。


二:正确示范

再定义一个类如下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftware(impl: SoftwareImpl):ISoftware
}



  1. @Module 表示该类是一个Hilt的Module,固定写法

  2. @InstallIn 表示模块在哪个组件生命周期内生效,SingletonComponent::class指的是全局

  3. 一个抽象类,类名随意

  4. 抽象方法,方法名随意,返回值是需要被注入的对象类型(接口),而参数是该接口的实现类,使用@Binds注解标记,



如此一来我们就告诉了Hilt,SoftwareImpl是ISoftware的实现类,于是Hilt注入ISoftware对象的时候就知道使用SoftwareImpl进行实例化。
其它不变运行一下:
image.png


可以看出,实际注入的是SoftwareImpl。



@Binds 适用在我们能够修改类的构造函数的场景



如何注入第三方类


上面的SoftwareImpl是我们可以修改的,因为使用了@Inject修饰其构造函数,所以可以在其它地方注入它。

在一些时候我们不想使用@Inject修饰或者说这个类我们不能修改,那该如何注入它们呢?


一:定义Provides模块


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():Hardware {
return Hardware()
}
}



  1. @Module和@InstallIn 注解是必须的

  2. 定义object类

  3. 定义函数,方法名随意,返回类型为我们需要注入的类型

  4. 函数体里通过构造或是其它方式创建具体实例

  5. 使用@Provides注解函数



二:依赖使用

而Hardware定义如下:


class Hardware {
fun printName() {
println("I'm fish")
}
}

在MyApp里引用Hardware:

在这里插入图片描述


虽然Hardware构造函数没有使用@Inject注解,但是我们依然能够使用依赖注入。


当然我们也可以注入接口:


interface IHardware {
fun printName()
}

class HardwareImpl : IHardware {
override fun printName() {
println("name is fish")
}
}

想要注入IHardware接口,需要定义provides模块:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}


@Provides适用于无法修改类的构造函数的场景,多用于注入第三方的对象



3. Hilt 的进阶使用


限定符


上述 ISoftware的实现类只有一个,假设现在有两个实现类呢?

比如说这些软件可以是美国提供,也可以是中国提供的,依据上面的经验我们很容易写出如下代码:


class SoftwareChina @Inject constructor() : ISoftware {
override fun printName() {
println("from china")
}
}

class SoftwareUS @Inject constructor() : ISoftware {
override fun printName() {
println("from US")
}
}

@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

//依赖注入:
@Inject
lateinit var software: ISoftware

兴高采烈的进行编译,然而却报错:
image.png


也就是说Hilt想要注入ISoftware,但不知道选择哪个实现类,SoftwareChina还是SoftwareUS?没人告诉它,所以它迷茫了,索性都绑定了。


这个时候我们需要借助注解:@Qualifier 限定符注解来对实现类进行限制。

改造一下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
@China
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
@US
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class US

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class China

定义新的注解类,使用@Qualifier修饰。

而后在Module里,分别使用注解类修饰返回的函数,如bindSoftwareCh函数指定返回SoftwareChina来实现ISoftware接口。


最后在引用依赖注入的地方分别使用@China @US修饰。


    @Inject
@US
lateinit var software1: ISoftware

@Inject
@China
lateinit var software2: ISoftware

此时,虽然software1、software2都是ISoftware类型,但是由于我们指定了限定符@US、@China,因此最后真正的实现类分别是SoftwareChina、SoftwareUS。



@Qualifier 主要用在接口有多个实现类(抽象类有多个子类)的注入场景



预定义限定符


上面提及的限定符我们还可以扩展其使用方式。

你可能发现了,上述提及的可注入的类构造函数都是无参的,很多时候我们的构造函数是需要有参数的,比如:


class Software @Inject constructor(val context: Context) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
//注入
@Inject
lateinit var software: Software

这个时候编译会报错:

image.png
意思是Software依赖的Context没有进行注入,因此我们需要给它注入一个Context。


由上面的分析可知,Context类不是我们可以修改的,只能通过@Provides方式提供其注入实例,并且Context有很多子类,我们需要使用@Qualifier指定具体实现类,因此很容易我们就想到如下对策。

先定义Module:


@Module
@InstallIn(SingletonComponent::class)
object MyContextModule {
@Provides
@GlobalContext
fun provideContext(): Context? {
return MyApp.myapp
}
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GlobalContext

再注入Context:


class Software @Inject constructor(@GlobalContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

可以看出,借助@Provides和@Qualifier,可以实现全局的Context。

当然了,实际上我们无需如此麻烦,因为这部分工作Hilt已经预先帮我们弄了。

与我们提供的限定符注解GlobalContext类似,Hilt预先提供了:


@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ApplicationContext {}

因此我们只需要在需要的地方引用它即可:


class Software @Inject constructor(@ApplicationContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

如此一来我们无需重新定义Module。




  1. 除了提供Application级别的上下文:@ApplicationContext,Hilt还提供了Activity级别的上下文:@ActivityContext,因为是Hilt内置的限定符,因此称为预定义限定符。

  2. 如果想自己提供限定符,可以参照GlobalContext的做法。



组件作用域和生命周期


Hilt支持的注入点(类)


以上的demo都是在MyApp里进行依赖,MyApp里使用了注解:@HiltAndroidApp 修饰,表示当前App支持Hilt依赖,Application就是它支持的一个注入点,现在想要在Activity里使用Hilt呢?


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {

除了Application和Activity,Hilt内置支持的注入点如下:
image.png


除了Application和ViewModel,其它注入点都是通过使用@AndroidEntryPoint修饰。



注入点其实就是依赖注入开始的点,比如Activity里需要注入A依赖,A里又需要注入B依赖,B里又需要注入C依赖,从Activity开始我们就能构建所有的依赖



Hilt组件的生命周期


什么是组件?在Dagger时代我们需要自己写组件,而在Hilt里组件都是自动生成的,无需我们干预。
依赖注入的本质实际上就是在某个地方悄咪咪地创建对象,这个地方的就是组件,Hilt专为Android打造,因此势必适配了Android的特性,比如生命周期这个Android里的重中之重。

因此Hilt的组件有两个主要功能:




  1. 创建、注入依赖的对象

  2. 管理对象的生命周期



Hilt组件如下:
image.png


可以看出,这些组件的创建和销毁深度绑定了Android常见的生命周期。

你可能会说:上面貌似没用到组件相关的东西,看了这么久也没看懂啊。

继续看个例子:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@InstallIn(SingletonComponent::class) 表示把模块安装到SingletonComponent组件里, SingletonComponent组件顾名思义是全局的,对应的是Application级别。因此安装的这个模块可在整个App里使用。


问题来了:SingletonComponent是不是表示@Provides修饰的函数返回的实例是同一个?

答案是否定的。


这就涉及到组件的作用域。


组件的作用域


想要上一小结的代码提供全局唯一实例,则可用组件作用域注解修饰函数:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@Singleton
fun provideHardware():IHardware {
return HardwareImpl()
}
}

当我们在任何地方注入IHardware时,获取到的都是同一个实例。

除了@Singleton表示组件的作用域,还有其它对应组件的作用域:

image.png


简单解释作用域:

@Singleton 被它修饰的构造函数或是函数,返回的始终是同一个实例

@ActivityRetainedScoped 被它修饰的构造函数或是函数,在Activity的重建前后返回同一实例

@ActivityScoped 被它修饰的构造函数或是函数,在同一个Activity对象里,返回的都是同一实例

@ViewModelScoped 被它修饰的构造函数或是函数,与ViewModel规则一致




  1. Hilt默认不绑定任何作用域,由此带来的结果是每一次注入都是全新的对象

  2. 组件的作用域要么不指定,要指定那必须和组件的生命周期一致



以下几种写法都不符合第二种限制:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityComponent::class)
object HardwareModule {
@Provides
@Singleton//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityRetainedComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

除了修饰Module,作用域还可以用于修饰构造函数:


@ActivityScoped
class Hardware @Inject constructor(){
fun printName() {
println("I'm fish")
}
}

@ActivityScoped表示不管注入几个Hardware,在同一个Activity里注入的实例都是一致的。


构造函数里无法注入的字段


一个类的构造函数如果被@Inject注入,那么构造函数的其它参数都需要支持注入。


class Hardware @Inject constructor(val context: Context) {
fun printName() {
println("I'm fish")
}
}

以上代码是无法编译通过的,因为Context不支持注入,而通过上面的分析可知,我们可以使用限定符:


class Hardware @Inject constructor(@ApplicationContext val context: Context) {
fun printName() {
println("I'm fish")
}
}

这就可以成功注入了。


再看看此种场景:


class Hardware @Inject constructor(
@ApplicationContext val context: Context,
val version: String,
) {
fun printName() {
println("I'm fish")
}
}

很显然String不支持注入,当然我们可以向@ApplicationContext 一样也给String提供一个@Provides和@Qualifier注解,但可想而知很麻烦,关键是String是动态变化的,我们确实需要Hardware构造的时候传入合适的String。


由此引入新的写法:辅助注入


class Hardware @AssistedInject constructor(
@ApplicationContext val context: Context,
@Assisted
val version: String,
) {

//辅助工厂类
@AssistedFactory
interface Factory{
//不支持注入的参数都可以放这,返回值为待注入的类型
fun create(version: String):Hardware
}

fun printName() {
println("I'm fish")
}
}

在引用注入的地方不能直接使用Hardware,而是需要通过辅助工厂进行创建:


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
@Inject
lateinit var hardwareFactory : Hardware.Factory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)

val hardware = hardwareFactory.create("3.3.2")
println("${hardware.printName()}")
}
}

如此一来,通过辅助注入,我们还是可以使用Hilt,值得一提的是辅助注入不是Hilt独有,而是从Dagger继承来的功能。


自定义注入点


Hilt仅仅内置了常用的注入点:Application、Activity、Fragment、ViewModel等。

思考一种场景:小明同学写的模块都是需要注入:


class Hardware @Inject constructor(
val gpu: GPU,
val cpu: CPU,
) {
fun printName() {
println("I'm fish")
}
}

class GPU @Inject constructor(val videoStorage: VideoStorage){}

//显存
class VideoStorage @Inject constructor() {}

class CPU @Inject constructor(val register: Register) {}

//寄存器
class Register @Inject() constructor() {}

此时小刚需要引用Hardware,他有两种选择:




  1. 使用注入方式很容易就引用了Hardware,可惜的是他没有注入点,仅仅只是工具类。

  2. 不选注入方式,则需要构造Hardware实例,而Hardware依赖GPU和CPU,它们又分别依赖VideoStorage和Register,想要成功构造Hardware实例需要将其它的依赖实例都手动构造出来,可想而知很麻烦。



这个时候适合小刚的方案是:



自定义注入点



方案实施步骤:

一:定义入口点


@InstallIn(SingletonComponent::class)
interface HardwarePoint {
//该注入点负责返回Hardware实例
fun getHardware(): Hardware
}

二:通过入口点获取实例


class XiaoGangPhone {
fun getHardware(context: Context):Hardware {
val entryPoint = EntryPointAccessors.fromApplication(context, HardwarePoint::class.java)
return entryPoint.getHardware()
}
}

三:使用Hardware


        val hardware = XiaoGangPhone().getHardware(this)
println("${hardware.printName()}")

注入object类


定义了object类,但在注入的时候也需要,可以做如下处理:


object MySystem {
fun getSelf():MySystem {
return this
}
fun printName() {
println("I'm fish")
}
}

@Module
@InstallIn(SingletonComponent::class)
object MiddleModule {
@Provides
@Singleton
fun provideSystem():MySystem {
return MySystem.getSelf()
}
}
//使用注入
class Middleware @Inject constructor(
val mySystem:MySystem
) {
}

4. Hilt 原理简单分析


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {}

Hilt通过apt在编译时期生成代码:


public abstract class Hilt_SecondActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {

private boolean injected = false;

Hilt_SecondActivity() {
super();
//初始化注入监听
_initHiltInternal();
}

Hilt_SecondActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}

private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
//真正注入
inject();
}
});
}

protected void inject() {
if (!injected) {
injected = true;
//通过manager获取组件,再通过组件注入
((SecondActivity_GeneratedInjector) this.generatedComponent()).injectSecondActivity(UnsafeCasts.<SecondActivity>unsafeCast(this));
}
}
}

在编译期,SecondActivity的父类由AppCompatActivity变为Hilt_SecondActivity,因此当SecondActivity构造时就会调用父类的构造器监听create()的回调,回调调用时进行注入。



由此可见,Activity.onCreate()执行后,Hilt依赖注入的字段才会有值



真正注入的过程涉及到不少的类,都是自动生成的类,有兴趣可以对着源码查找流程,此处就不展开说了。


5. Android到底该不该使用DI框架?


有人说DI比较复杂,还不如我直接构造呢?

又有人说那是你项目不复杂,用不到,在后端流行的Spring全家桶,依赖注入大行其道,Android复杂的项目也需要DI来解耦。


从个人的实践经验看,Android MVVM/MVI 模式还是比较适合引入Hilt的。
image.png


摘抄官网的:现代Android 应用架构

通常来说我们这么设计UI层到数据层的架构:


class MyViewModel @Inject constructor(
val repository: LoginRepository
) :ViewModel() {}

class LoginRepository @Inject constructor(
val rds : RemoteDataSource,
val lds : LocalDataSource
) {}

//远程来源
class RemoteDataSource @Inject constructor(
val myRetrofit: MyRetrofit
) {}

class MyRetrofit @Inject constructor(
) {}

//本地来源
class LocalDataSource @Inject constructor(
val myDataStore: MyDataStore
) {}

class MyDataStore @Inject constructor() {}

可以看出,层次比较深,使用了Hilt简洁了许多。


本文基于 Hilt 2.48.1

参考文档:

dagger.dev/hilt/gradle…

developer.android.com/topic/archi…

repo.maven.apache.org/maven2/com/…


作者:小鱼人爱编程
来源:juejin.cn/post/7294965012749320218
收起阅读 »

记一次使用babel做代码转换的经历

web
前言 前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。 这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访...
继续阅读 »

前言


前不久刚刚将公司项目中的静态图片资源放到阿里云oss服务器上,同时删除了项目中的图片资源,成功为项目瘦身。


这不,今天就来了一个私有化部署的需求,需要将现有的项目单独部署到客户那边的服务器上,而且客户还只使用内网,这也就导致使用阿里云访问的图片资源全部访问不通,还得拿到本地来。


得,谁让咱们天生就是找事的好手呢,那整吧。


方案对比


既然来活了,那咱们首先得先确定下这个事怎么做?有以下几个方案:


方案一: 发挥中华民族的优良传统,勤劳,即手动将全部的静态资源引用处替换为本地引用,想想手就疼


方案二: 将偷懒运用到极致,将静态资源全部放到public/assets目录下(Vite项目中public目录下的文件会被直接复制到打包目录下),同时修改资源引用的统一前缀为 /assets,即可引用到该静态资源。目测几分钟就能完成


方案三: 写个脚本,自动完成 1 操作,瞬间手就不疼了,但是脑壳开始疼了


对比下这三个方案的优缺点,选出最优解



方案一


优点:简单


缺点:手疼且低效


方案二


优点:省时、省力


缺点:需要考虑打包后的引用路径,同时因为文件都是直接复制到包中的,并没有经过hash处理,浏览器会缓存该文件,后续如果文件修改,不能第一时间反应再客户端。


方案三


优点:高效、一劳永逸、文件会经过Vite处理,生成带有hash值的新文件,没有缓存问题


缺点:这个脚本有点难写,涉及代码转换和项目文件扫描等知识,脑壳疼



最终,本着一劳永逸的想法,我选择了方案三。


过程


整体思路:



  1. 将全部静态资源引用汇总到统一文件中,方便管理及代码分析

  2. 使用代码转换工具将上面的文件内容转换为使用 import 导入的方式


静态资源汇总


所有的静态资源引用散布在项目的各个文件中,这不利于代码分析,也不利于代码转化,所以,第一步就是将散布在项目各个文件中的静态资源引用汇总到一个文件中,方便管理、代码分析、代码转化。


这一步是纯体力活,一次劳动,收益无穷。


最终静态资源汇总文件应该是这样的:


import { ASSETS_PREFIX } from './constants';

const contactUs = `${ASSETS_PREFIX}/login/contact_us.png`;
const userAvatar = `${ASSETS_PREFIX}/login/default_avatar.png`;
const loginBg = `${ASSETS_PREFIX}/login/login_bg.jpg`;

export {
contactUs,
userAvatar,
loginBg,
}


  1. 一个静态资源对应一个变量,一个变量对应一个静态资源路径

  2. 静态资源路径必须使用模版字符串统一前缀,便于后续做替换

  3. 统一导出


代码转换


静态资源全部会送完毕后,接下来就是做代码分析及转换。


我们的目标其实就是将上面的代码转换到下面这种:


import contactUs from '@/assets/login/contact_us.png';
import userAvatar from '@/assets/login/default_avatar.png';
import loginBg from '@/assets/login/login_bg.jpg'

export {
contactUs,
userAvatar,
loginBg,
}

既然涉及代码转换,很自然的就能想到使用babel做转换。


先来简单说下babel做代码转换的过程:



  1. 使用 @babel/parser 将代码解析为抽象语法树(AST: 表示当前代码结构的js对象)

  2. 找到标识为 const 的变量,拿出该变量,并将其后对应的变量内容拿出来,将模版字符串中的变量替换为@/assets,得到新静态资源本地路径(@/assets/login/contact_us.png)

  3. 组合 import 的 AST 对象,并使用该对象替换原来的 const 相关的AST

  4. 使用 @babel/generator 将新的AST转换为代码输出到对应文件中


代码如下:


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import fs from 'fs';

// 静态资源汇总文件
let imgInfoFilePath = 'src/scripts/assets.ts';
// 要替换为的静态资源路径前缀
let replaceToCode = '@/assets';

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历const声明节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

node.declarations.forEach(decl => {
// 存储变量名
const localName = decl.id.name;
// 组装import路径
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;
// 组装import结构
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
const result = generate.default(ast, {}, code);
// 备份原文件
fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);
// 代码输出
fs.writeFileSync(imgInfoFilePath, result.code);
} catch (error: any) {
logError(error);
}
}

这样,代码就转换完成了。


这样转换完后,ts文件中相关的静态资源引用就替换完成了,但是css文件中的静态资源引用还没有被转换。


因为css文件中的静态资源路径都是完整路径,不存在其中掺杂变量的情况,所以我们只需要找到所有的css文件,并将其中的路径前缀统一替换为@/assets 即可。


import { globSync } from 'glob';
import fs from 'fs';

let replaceStr = 'https://xxxxx.xxxx.xxxxx';
let replaceToCode = '@/assets';

function replaceHttpsCode() {
try {
// 扫描文件
const files = globSync('./src/**/*.{scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 写入文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}


  1. 使用 glob 扫描当前目录下的scss、css文件。

  2. 读取文件内容,并使用replace方法替换掉静态资源路径

  3. 写入文件,完成转换


至此,代码全部转换完成。


封装成工具包


因为多个项目都会涉及静态资源转换的问题,所以我将此脚本封装为npm包,并提炼了 transform build 命令,只需执行该命令,即可完成资源转换,以下是源码分享:


cli.ts


import { Command } from 'commander';
import { version } from '../package.json';
import buildAction from './transform';
const program = new Command();

program
.command('build')
.description('transform assets and code')
.option(
'--replaceStr <path>',
'[string] 需要全局替换的字符串,默认值: https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage',
)
.option('--imgInfoFilePath <path>', '[string] 统一的静态资源文件路径 默认值: src/scripts/assets.ts')
.option('--replaceToCode <path>', '[string] 替换为的代码 默认值: @/assets')
.option('--assetsDir <path>', '[string] 静态资源文件目录 默认值: src/assets')
.action(options => {
buildAction(options);
});

program.version(version);

program.parse();

transfrom.ts


import { parse } from '@babel/parser';
import generate from '@babel/generator';
import chalk from 'chalk';
import { globSync } from 'glob';
import fs from 'fs';

interface Options {
replaceStr?: string;
imgInfoFilePath?: string;
replaceToCode?: string;
assetsDir?: string;
}

let replaceStr = 'https://zkly-fe-resource.oss-cn-beijing.aliyuncs.com/safeis-web-manage';
let imgInfoFilePath = 'src/scripts/assets.ts';
let replaceToCode = '@/assets';
let assetsDir = './src/assets';

function checkAssetsDir() {
logInfo('检查 src/assets 目录是否存在');

if (!fs.existsSync(assetsDir)) {
logError('assets 目录不存在,请先联系相关人员下载对应项目的静态资源文件,并放置在 src/assets 目录下');
} else {
logSuccess('assets 目录存在');
}
}

function babelTransformCode() {
logInfo(`开始转换 ${imgInfoFilePath} 文件`);
try {
const code = fs.readFileSync(imgInfoFilePath, 'utf-8');

// 解析AST
const ast = parse(code, { sourceType: 'module', plugins: ['typescript'] });

// 遍历VariableDeclarator节点
ast.program.body.forEach(node => {
if (node.type === 'VariableDeclaration') {
// 构建导入声明
const importDecl = {
type: 'ImportDeclaration',
specifiers: [],
source: {
type: 'StringLiteral',
},
};

// @ts-ignore
node.declarations.forEach(decl => {
// @ts-ignore
const localName = decl.id.name;

// @ts-ignore
const filePath = `${replaceToCode}${decl?.init?.quasis?.[1]?.value?.raw}`;

// @ts-ignore
logInfo(`替换 ${replaceStr}${decl?.init?.quasis?.[1]?.value?.raw}${filePath}`);

// 构建导入规范
// @ts-ignore
importDecl.specifiers.push({
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: localName,
},
});

// 修改初始化为相对路径
// @ts-ignore
importDecl.source.value = filePath;
});

// 用importDecl替换原变量声明节点
Object.assign(node, importDecl);
}
});

// 最终代码
// @ts-ignore
const result = generate.default(ast, {}, code);

logInfo(`备份 ${imgInfoFilePath} 文件为 ${imgInfoFilePath}.bak`);

fs.renameSync(imgInfoFilePath, `${imgInfoFilePath}.bak`);

fs.writeFileSync(imgInfoFilePath, result.code);

logSuccess(`转换 ${imgInfoFilePath} 成功`);
} catch (error: any) {
logError(error);
}
}

function replaceHttpsCode() {
logInfo('开始转换 其余文件中引用https导入的静态资源');

try {
// 扫描文件
const files = globSync('./src/**/*.{vue,js,ts,scss,css}', { ignore: 'node_modules/**' });

files.forEach((file: string) => {
// 读取文件内容
let content = fs.readFileSync(file, 'utf8');

if (content.includes(replaceStr)) {
logInfo(`替换 ${file} 中的 ${replaceStr}${replaceToCode}`);
}

// 替换匹配到的字符串
content = content.replace(replaceStr, replaceToCode);

// 保存文件
fs.writeFileSync(file, content);
});

logSuccess('转换完成');
} catch (error: any) {
logError(error);
}
}

function logInfo(info: string) {
console.log(chalk.gray(`[INFO] - 🆕 ${info}`));
}

function logSuccess(info: string) {
console.log(chalk.green(`[SUCCESS] - ✅ ${info}`));
}

function logError(info: string) {
console.log(chalk.red(`[ERROR] - ❌ ${info}`));
}

export default function main(options: Options) {
replaceStr = options.replaceStr || replaceStr;
imgInfoFilePath = options.imgInfoFilePath || imgInfoFilePath;
replaceToCode = options.replaceToCode || replaceToCode;
assetsDir = options.assetsDir || assetsDir;

checkAssetsDir();
babelTransformCode();
replaceHttpsCode();
}

作者:程序员小杨v1
来源:juejin.cn/post/7295276751595798580
收起阅读 »

Flutter开发者,需要会原生吗?-- Android 篇

前言:随着Flutter在国内移动应用的成熟度,大部分企业都开始认可Flutter的可持续发展,逐步引入Flutter技术栈。 由此关于开发人员的技能储备问题,会产生一定的疑问。今天笔者将从我们在OS中应用Flutter的各种玩法,聊聊老生常谈的话题:Flut...
继续阅读 »

前言:随着Flutter在国内移动应用的成熟度,大部分企业都开始认可Flutter的可持续发展,逐步引入Flutter技术栈。

由此关于开发人员的技能储备问题,会产生一定的疑问。今天笔者将从我们在OS中应用Flutter的各种玩法,聊聊老生常谈的话题:Flutter开发者到底需不需要懂原生平台?



缘起


《Flutter开发者需要掌握原生Android吗?》

这个话题跟Flutter与RN对比Flutter会不会凉同属一类,都是前两年社群最喜欢争论的话题。激烈的讨论无非是观望者太多,加之Flutter不成熟,在使用过程中会遇到不少坑。


直到今年3.7.0、3.10.0相继发布,框架改进和社区的丰富,让更多人选择拥抱Flutter,关于此类型的话题才开始沉寂下来。很多招聘网站也直接出现了Flutter开发这个岗位,而且技能也不要求原生,甚至加分项前端的技能。似乎Flutter开发者在开发过程中很少用到原生的技能,然而事实绝非如此。


我专攻Flutter有3年了,期间Android、iOS、Windows应用做过不少,Web、Linux也都略有研究;这次我将直接从Android平台出发,用切身经历来论述下:Flutter开发者,真的需要懂Android。


Flutter只是个UI框架


打开一个Flutter的项目,我们可以看到整个应用其实是基于一个Activity运行的,属于单页应用。


package com.wxq.test

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}

Activity继承自FlutterActivity,FlutterActivityonCreate内会创建FlutterActivityAndFragmentDelegate


// io/flutter/embedding/android/FlutterActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
switchLaunchThemeForNormalTheme();

super.onCreate(savedInstanceState);
// 创建代理,ActivityAndFragment都支持哦
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this); // 这个方法创建引擎,并且将context吸附上去
delegate.onRestoreInstanceState(savedInstanceState);

lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);

configureWindowForTransparency();

// 设置Activity的View,createFlutterView内部也是调用代理的方法
setContentView(createFlutterView());
configureStatusBarForFullscreenFlutterExperience();
}

这个代理将会通过engineGr0up管理FlutterEngine,通过onAttach创建FlutterEngine,并且运行createAndRunEngine方法


// io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
void onAttach(@NonNull Context context) {
ensureAlive();

if (flutterEngine == null) {
setupFlutterEngine();
}

if (host.shouldAttachEngineToActivity()) {

Log.v(TAG, "Attaching FlutterEngine to the Activity that owns this delegate.");
flutterEngine.getActivityControlSurface().attachToActivity(this, host.getLifecycle());
}
platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);

host.configureFlutterEngine(flutterEngine);
isAttached = true;
}

@VisibleForTesting
/* package */ void setupFlutterEngine() {
Log.v(TAG, "Setting up FlutterEngine.");

// 省略处理引擎缓存的代码
String cachedEngineGr0upId = host.getCachedEngineGr0upId();
if (cachedEngineGr0upId != null) {
FlutterEngineGr0up flutterEngineGr0up =
FlutterEngineGr0upCache.getInstance().get(cachedEngineGr0upId);
if (flutterEngineGr0up == null) {
throw new IllegalStateException(
"The requested cached FlutterEngineGr0up did not exist in the FlutterEngineGr0upCache: '"
+ cachedEngineGr0upId
+ "'");
}

// *** 重点 ***
flutterEngine =
flutterEngineGr0up.createAndRunEngine(
addEntrypointOptions(new FlutterEngineGr0up.Options(host.getContext())));
isFlutterEngineFromHost = false;
return;
}

// Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our
// FlutterView.
Log.v(
TAG,
"No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
+ " this FlutterFragment.");

FlutterEngineGr0up group =
engineGr0up == null
? new FlutterEngineGr0up(host.getContext(), host.getFlutterShellArgs().toArray())
: engineGr0up;
flutterEngine =
group.createAndRunEngine(
addEntrypointOptions(
new FlutterEngineGr0up.Options(host.getContext())
.setAutomaticallyRegisterPlugins(false)
.setWaitForRestorationData(host.shouldRestoreAndSaveState())));
isFlutterEngineFromHost = false;
}

再调用onCreateView创建SurfaceView或者外接纹理TextureView,这个View就是Flutter的赖以绘制的画布。


// io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java
@NonNull
View onCreateView(
LayoutInflater inflater,
@Nullable ViewGr0up container,
@Nullable Bundle savedInstanceState,
int flutterViewId,
boolean shouldDelayFirstAndroidViewDraw)
{
Log.v(TAG, "Creating FlutterView.");
ensureAlive();

if (host.getRenderMode() == RenderMode.surface) {
FlutterSurfaceView flutterSurfaceView =
new FlutterSurfaceView(
host.getContext(), host.getTransparencyMode() == TransparencyMode.transparent);

// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterSurfaceViewCreated(flutterSurfaceView);

// Create the FlutterView that owns the FlutterSurfaceView.
flutterView = new FlutterView(host.getContext(), flutterSurfaceView);
} else {
FlutterTextureView flutterTextureView = new FlutterTextureView(host.getContext());

flutterTextureView.setOpaque(host.getTransparencyMode() == TransparencyMode.opaque);

// Allow our host to customize FlutterSurfaceView, if desired.
host.onFlutterTextureViewCreated(flutterTextureView);

// Create the FlutterView that owns the FlutterTextureView.
flutterView = new FlutterView(host.getContext(), flutterTextureView);
}

flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener);
// 忽略一些代码...
return flutterView;
}

由此可见,Flutter的引擎实际上是运行在Android提供的View上,这个View必然是设置在Android的组件上,可以是Activity、Framgent,也可以是WindowManager。

这就给我们带来了很大的可塑性,只要你能掌握这套原理,混合开发就随便玩了。


Android,是必须的能力


通过对Flutter运行机制的剖析,我们很明确它就是个单纯的UI框架,惊艳的跨端UI都离不开Android的能力,这也说明Flutter开发者不需要会原生注定走不远

下面几个例子,也可以充分论证这个观点。


一、Flutter插件从哪里来


上面讲述到的原理,Flutter项目脚手架已经帮我们做好,但这只是UI绘制层面的;实际上很多Flutter应用,业务能力都是由Pub.dev提供的,随着社区框架的增多,开发者大多时候是感知不到需要Android能力的。

然而业务的发展是迅速的,我们开始需要很多pub社区并不支持的能力,比如:getMetaDatagetMacAddressreboot/shutdownsendBroadcast等,这些能力都需要我们使用Android知识,以编写插件的形式,提供给Flutter调用。

Flutter Plugin在Dart层和Android层都实现了MethodChannel对象,同一个Engine下,只要传入一致的channelId字符串,就能建立双向的通道互相传输基本类型数据。


class FlutterNativeAbilityPlugin : FlutterPlugin, MethodCallHandler {
private var applicationContext: Context? = null

private lateinit var channel: MethodChannel

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
applicationContext = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_native_ability")
channel.setMethodCallHandler(this)
}

class MethodChannelFlutterNativeAbility extends FlutterNativeAbilityPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('flutter_native_ability');
}

发送端通过invokeMethod调用对应的methodName,传入arguments;接收端通过实现onMethodCall方法,接收发送端的invokeMethod操作,执行需要的操作后,通过Result对象返回结果。


@override
Future<String> getMacAddress() async {
final res = await methodChannel.invokeMethod<String>('getMacAddress');
return res ?? '';
}

@override
Future<void> reboot() async {
await methodChannel.invokeMethod<String>('reboot');
}

"getMacAddress" -> {
Log.i(TAG, "onMethodCall: getMacAddress")
val macAddress = CommonUtils().getDeviceMac(applicationContext)
result.success(macAddress)
}
"reboot" -> {
Log.i(TAG, "onMethodCall: reboot")
beginToReboot(applicationContext)
result.success(null)
}

ps:invokeMethod和onMethodCall双端都能实现,都能作为发送端和接收端。


二、Flutter依赖于Android机制,得以“横行霸道”


目前我们将Flutter应用于OS的开发,这需要我们不单是从某个独立应用去思考。很多应用、服务都需要从整个系统业务去设计,在以下这些需求中,我们深切感受到:Flutter跟Android配合后,能发挥更大的业务价值。



  • Android服务运行dart代码,广播接收器与Flutter通信


我们很多服务需要开机自启,这必须遵循Android的机制。通常做法是:接收开机广播,在广播接收器中启动Service,然后再去运行DartEngie,执行跨平台的代码;


class MyTestService : Service() {

private lateinit var engineGr0up: FlutterEngineGr0up

override fun onCreate() {
super.onCreate()
startForeground()

engineGr0up = FlutterEngineGr0up(this)
// initService是Flutter层的方法入口点
val dartEntrypoint = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"initService"
)
val flutterEngine = engineGr0up.createAndRunEngine(this, dartEntrypoint)
// Flutter调用Native方法的 MethodChannel 也初始化一下,调用安装接口需要
FlutterToNativeChannel(flutterEngine, this)
}
}

同时各应用之间需要通信,这时我们也会通过Broadcat广播机制,在Android的广播接收器中,通过MechodChannel发送给Flutter端。


总而言之,我们必须 遵循系统的组件规则,基于Flutter提供的通信方式,将Android的消息、事件等发回给Flutter, 带来的跨端效益是实实在在的!



  • 悬浮窗需求


悬浮窗口在视频/直播场景下用的最多,当你的应用需要开启悬浮窗的时候,Flutter将完全无法支持这个需求。

实际上我们只需要在Android中创建一个WindowManager,基于EngineGround创建一个DartEngine;然后创建flutterView,把DartEngine吸附到flutterView上,最后把flutterView Add to WindowManager即可。


private lateinit var flutterView: FlutterView
private var windowManager = context.getSystemService(Service.WINDOW_SERVICE) as WindowManager
private val inflater =
context.getSystemService(Service.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private val metrics = DisplayMetrics()

@SuppressLint("InflateParams")
private var rootView = inflater.inflate(R.layout.floating, null, false) as ViewGr0up

windowManager.defaultDisplay.getMetrics(metrics)
layoutParams.gravity = Gravity.START or Gravity.TOP

windowManager.addView(rootView, layoutParams)

flutterView = FlutterView(inflater.context, FlutterSurfaceView(inflater.context, true))
flutterView.attachToFlutterEngine(engine)

engine.lifecycleChannel.appIsResumed()

rootView.findViewById<FrameLayout>(R.id.floating_window)
.addView(
flutterView,
ViewGr0up.LayoutParams(
ViewGr0up.LayoutParams.MATCH_PARENT,
ViewGr0up.LayoutParams.MATCH_PARENT
)
)
windowManager.updateViewLayout(rootView, layoutParams)


  • 不再局限单页应用


最近我们在升级应用中,遇到一个比较尴尬的需求:在原有OTA功能下,新增一个U盘插入本地升级的功能,希望升级能力和UI都能复用,且互不影响各自流程。


如果是Android项目很简单,把升级的能力抽象,通过多个Activity管理自己的业务流程,互不干扰。但是Flutter项目属于单页应用,不可能同时展示两个路由页面各自处理,所以也必须 走Android的机制,让Flutter应用同时运行多个Activity。


我们在Android端监听了U盘的插入事件,在需要本地升级的时候直接弹出Activity。Activity是继承FlutterActivity的,通过<metadata>标签指定方法入口点。与MainActivity运行main区分开,然后通过重写getDartEntrypointArgs方法,把必要的参数传给Flutter入口函数,从而独立运行本地升级的业务,而且UI和能力都能复用。


class LocalUpgradeActivity : FlutterActivity() {
}

<activity
android:name=".LocalUpgradeActivity"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustResize">

<meta-data
android:name="io.flutter.Entrypoint"
android:value="runLocalUpgradeApp" />
<!-- 这里指定Dart层的入口点-->
</activity>

override fun getDartEntrypointArgs(): MutableList<String?> {
val filePath: String? = intent?.getStringExtra("filePath")
val tag: String? = intent?.getStringExtra("tag")
return mutableListOf(filePath, tag)
}

至此,我们的Flutter应用不再是单页应用,而且所有逻辑和UI都将在Flutter层实现!


总结


我们遵循Android平台的机制,把逻辑和UI都尽可能的交给Flutter层,让其在跨平台上发挥更大的可能性,在落地过程确实切身体会到Android的知识是何等的重要!

当然我们的应用场景可能相对复杂,一般应用也许不会有这么多的应用组合;但无论Flutter如何完善,社区更加壮大,它都离不开底层平台的支持。

作为Flutter开发者,有精力的情况下,一定要多学各个平台的框架和能力,让Flutter、更让自己走的更远!


作者:Karl_wei
来源:juejin.cn/post/7295571705689423907
收起阅读 »

正式变实习?掌趣科技这波操作太恶心

掌趣科技作为国内老牌游戏上市公司,按道理应该是不错的,然而拿到了他家的 Offer 之后,却感觉套路满满,非常失望!不知道,大家是否有类似的经历? 1.Offer 展示 2.面试题真题 问项目: 讲讲你最好的一个项目怎么实现的 ? 问八股: JVM 的执行...
继续阅读 »

掌趣科技作为国内老牌游戏上市公司,按道理应该是不错的,然而拿到了他家的 Offer 之后,却感觉套路满满,非常失望!不知道,大家是否有类似的经历?


1.Offer 展示



2.面试题真题


问项目:


讲讲你最好的一个项目怎么实现的 ?


问八股:



  1. JVM 的执行流程?

  2. JVM 是怎么找到一个类的?

  3. JVM 的内存布局?

  4. Java 虚拟机是怎么进行垃圾回收的?

  5. Java 有可能产生内存泄漏吗,举例? 内存泄露会导致什么问题?

  6. GC 的过程, 内存是怎么分配的, 是一片一片的使用呢, 还是一大块的使用? 还是想 C/C++ 那样零散的内存呢?怎么解决内存碎片问题?

  7. Java 中你都用过什么容器?HashMap 和 HashTable 的区别?ConcurrentHashMap 怎么保证线程安全问题的?

  8. 你用过 Java 的多线程锁吗?ReentrantLock 和 Synchronized 的区别?你了解什么是悲观锁?什么是乐观锁吗?

  9. Java 是怎么实现多态的? Java 中能多继承吗? Java 中的注解怎么实现的?

  10. Java 虚拟机是怎么找到一个方法,一个类,怎么根据注解找到对应的方法

  11. Java 中反射的作用,反射怎么实现的?

  12. 反射可以实现注解吗?

  13. 你了解自动拆箱装箱吗? 拆箱装箱怎么实现的?

  14. 项目中有碰见过死锁的问题吗? 什么是死锁? 只锁一个对象会产生死锁吗?

  15. Netty 用过吗?了解过吗?

  16. MySQL 有几种索引? 按字段特性分,按物理存储分呢? 索引底层有几种实现呢? MySQL 的有哪几种存储引擎? InnoDB 和 MyISAM 有什么区别?有什么优缺点?

  17. 做一个查询操作的时候,使用哪种索引?

  18. 什么叫回表查询? 如果没有创建主键,怎么进行回表查询?

  19. 什么叫索引覆盖?

  20. 一个表是索引越多越好,还是越少越好?

  21. 你了解什么叫表锁?什么叫行锁吗?什么情况下使用行锁?什么情况下使用表锁?

  22. 如果一个表的写操作比较多,是加行锁比较好,还是加表锁比较好?

  23. 你能说一下事务的隔离级别吗?

  24. Redis 数据库怎么是实现持久化的 ?Redis 里边都有哪些数据类型? ZSet 怎么用?

  25. 什么是缓存雪崩、缓存击穿、缓存穿透,分别怎么解决?

  26. 做过 Java 的网络编程吗?给我说一下 Socket 套接字的创建流程?

  27. 讲一讲三次握手,四次挥手的流程?TCP 和 UDP 的区别?

  28. 使用 UDP 来传输数据,怎么来保证他的可靠性,就像你刚刚说的后发先至问题?



PS:面试问的还挺细的,但因为做足了准备,所以这些面试题基本都拿下了。


以上问题来自学员的整理,在此感谢。



3.套路来了


拿到 Offer 本来是件开心的事,然而当聊完入职事项之后,整个人就不好了,来看看具体的经过吧:


问题1:正式工作变实习?


秋招明明投递的是正式工作,然而拿到 Offer 之后,HR 却必须先让去实习 5 个月。


投递详情如下:



明确是秋招正式岗位,而非实习。


正常的逻辑应该是拿到 Offer 之后,等明年毕业之后先去公司报道,只有 3 个月试用期,试用期没问题就转为正式员工了。



问题2:实习薪资


实习也是按照实习的工资,而非正式工资的 80% 发放的。


问题3:可能不通过&不谈正式薪资


HR 说明年四月实习期过了之后,再谈正式薪资,言外之意,如果实习期没过,那就不要你了,也就不用谈薪资了。那么请问,投递正式岗位又有什么意义呢?


问题4:实习时间超长


通常实习时间也就是 2-3 个月,而掌趣要求 11 月中旬去实习,至少实习到明年 4 月份,这个实习的时间未免要太长了。


小结


作为国内老牌上市公司,竟然以找正式工作的幌子把入选人悄悄转为实习生,这件事对有经验的人来说,一眼就知道怎么回事,然而对于涉世未深,刚步入社会的年轻人来说,却是满满的套路,还未感受生活的美好,就经历了人心的险恶。


人在做天在看,希望某些公司不要有这样的骚操作,招实习就是招实习,招正式员工就是招正式员工,不要混为一谈,更不要暗箱操作,更不要欺负那些涉世未深的年轻人。


作者:Java中文社群
来源:juejin.cn/post/7292946512585162806
收起阅读 »

登录页面一些有趣的css效果

web
前言 今天无意看到一个登录页,input框focus时placeholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及...
继续阅读 »

前言


今天无意看到一个登录页,inputfocusplaceholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码


919c40a2a264f683ab5e74e8a649ac5.png


title 的动画实现


首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke, 逐步点亮只需要使用filter即可


text-stroke


text-stroke属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke属性通常与-webkit-text-stroke前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持


text-stroke属性有两个主要值:



  1. 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。

  2. 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。


filter


filter是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。


filter属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:




  1. 模糊(blur) : 通过blur函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。


    .blurred-image {
    filter: blur(5px);
    }



  2. 对比度(contrast) : 通过contrast函数可以调整对比度。值为百分比,1表示原始对比度。


    .high-contrast-text {
    filter: contrast(150%);
    }



  3. 饱和度(saturate) : 通过saturate函数可以调整饱和度。值为百分比,1表示原始饱和度。


    .desaturated-image {
    filter: saturate(50%);
    }



  4. 反色(invert) : 通过invert函数可以实现反色效果。值为百分比,1表示完全反色。


    .inverted-text {
    filter: invert(100%);
    }



  5. 灰度(grayscale) : 通过grayscale函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。


    .gray-text {
    filter: grayscale(70%);
    }



  6. 透明度(opacity) : 通过opacity函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。


    .semi-transparent-box {
    filter: opacity(0.7);
    }



  7. 阴影(drop-shadow) :用于在图像、文本或其他元素周围添加阴影效果。这个属性在 CSS3 中引入,通常用于创建阴影效果,使元素看起来浮在页面上或增加深度感


    drop-shadow(<offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>?)

    各个值的含义如下:



    • <offset-x>: 阴影在 X 轴上的偏移距离。

    • <offset-y>: 阴影在 Y 轴上的偏移距离。

    • <blur-radius> (可选): 阴影的模糊半径。默认值为 0。

    • <spread-radius> (可选): 阴影的扩散半径。默认值为 0。

    • <color> (可选): 阴影的颜色。默认值为当前文本颜色。




filter属性的支持程度因浏览器而异,因此在使用时应谨慎考虑浏览器兼容性。


实现移入标题点亮的效果


想实现移入标题点亮的效果我们首先需要两个通过定位重叠的span元素,一个做镂空用于展示,另一个作为
hover时覆盖掉镂空元素,并通过filter: drop-shadow实现光影效果,需要注意的是这里需要使用inline元素实现效果。


title-animation.gif


input 的动画实现


input的效果比较简单,只需要在focusspan(placeholder)上移变成span(label)同时给inputborder-bottom做一个底色的延伸,效果确定了接着就看看实现思路。


input placeholder 作为 label


使用div作为容器包裹inputspanspan首先绝对定位到框内,伪装为placeholder, 当input状态为focus提高spantop值,即可伪装成label, 这里有两个问题是:




  1. 当用户输入了值的时候,span并不需要恢复为之前的top, 这里我们使用css或者js 去判断都可以, js就是拿到输入框的值,这里不多做赘述,css 有个比较巧妙的做法, 给input required属性值设置为required, 这样可以使用css:valid伪类去判断input是否有值。




  2. 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用pointer-events: none; 来解决。pointer-events 是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。


    pointer-events 具有以下几个可能的值:



    1. auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。

    2. none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。

    3. visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。

    4. visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    5. visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。

    6. painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。

    7. fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    8. stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。




pointer-events 属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。


input border bottom 延伸展开效果


效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span作为底部的边, 初始不可见, focus时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform变形,首先使用transform: scaleX(0);达到不可见的效果, 然后设置变形原点为中间transform-origin: center;,这样效果就可以实现了


input 的动画实现效果


input-animation.gif


按钮的动画实现


关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 background-image(radial-gradient)


background-image(radial-gradient)


background-image 属性用于设置元素的背景图像,而 radial-gradient 是一种 CSS 渐变类型,可用于创建径向渐变背景。这种径向渐变背景通常以一个中心点为基础,然后颜色渐变向外扩展,形成一种放射状的效果。


radial-gradient 的语法如下:


background-image: radial-gradient([shape] [size] at [position], color-stop1, color-stop2, ...);


  • [shape]: 可选,指定渐变的形状。常用的值包括 "ellipse"(椭圆)和 "circle"(圆形)。

  • [size]: 可选,指定渐变的大小。可以是长度值或百分比值。

  • at [position]: 可选,指定渐变的中心点位置。

  • color-stopX: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。


按钮移入动画效果实现


btn-animation.gif


结尾


css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。


作者:刘圣凯
来源:juejin.cn/post/7294908459002331171
收起阅读 »