首页 文章 精选 留言 我的

精选列表

搜索[玩转EdgeOne],共1751篇文章
优秀的个人博客,低调大师

玩转 ByteBuffer

为什么要讲 Buffer 首先为什么一个小小的 Buffer 我们需要单独拎出来聊?或者说,Buffer 具体是在哪些地方被用到的呢? 例如,我们从磁盘上读取一个文件,并不是直接就从磁盘加载到内存中,而是首先会将磁盘中的数据复制到内核缓冲区中,然后再将数据从内核缓冲区复制到用户缓冲区内,在图里看起来就是这样: 再比如,我们往磁盘上写文件,也不是直接将数据写到磁盘。而是将数据从用户缓冲区写到内核缓冲区,由操作系统择机将其刷入磁盘,图跟上面这个差不多,就不画了,自行理解。 再再比如,服务器接受客户端发过来的数据时,也不是直接到用户态的 Buffer 中。而是会先从网卡到内核态的 Buffer 中,再从内核态的 Buffer 中复制到用户态的 Buffer 中。 那为什么要这么麻烦呢?复制来复制去的,首先我们用排除法排除这样做是为了好玩。 Buffer 存在的目的是为了减少与设备(例如磁盘)的交互频率,在之前的博客中也提到过「磁盘的读写是很昂贵的操作」。那昂贵在哪里呢?简单来说,和设备的交互(例如和磁盘的IO)会设计到操作系统的中断。中断需要保存之前的进程运行的上下文,中断结束之后又需要恢复这个上下文,并且还涉及到内核态和用户态的切换,总体上是个耗时的操作。 看到这里,不熟悉操作系统的话可能会有点疑惑。例如: 啥是用户态 啥是内核态 大家可以去看看我之前写的文章 《简单聊聊用户态和内核态的区别》 Buffer 的使用 我们通过 Java 中 NIO 包中实现的 Buffer 来给大家讲解,Buffer 总共有 7 种实现,就包含了 Java 中实现的所有数据类型。 本篇文章中,我们使用的是 ByteBuffer,其常用的方法都有: put get flip rewind mark reset clear 接下来我们就通过实际的例子来了解这些方法。 put put 就是往 ByteBuffer 里写入数据,其有有很多重载的实现: public ByteBuffer put(ByteBuffer src) {...} public ByteBuffer put(byte[] src, int offset, int length) {...} public final ByteBuffer put(byte[] src) {...} 我们可以直接传入 ByteBuffer 对象,也可以直接传入原生的 byte 数组,还可以指定写入的 offset 和长度等等。接下来看个具体的例子: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s','h'}); } 为了能让大家更直观的看出 ByteBuffer 内部的情况,我将它整理成了图的形式。当上面的代码运行完之后 buffer 的内部长这样: 当你尝试使用 System.out.println(buffer) 去打印变量 buffer 的时候,你会看到这样的结果: java.nio.HeapByteBuffer[pos=2 lim=16 cap=16] 图里、控制台里都有 position 和 limit 变量,capacity 大家能理解,就是我们创建这个 ByteBuffer 的制定的大小 16。 而至于另外两个变量,相信大家从图中也可以看出来,position 变量指向的是下一次要写入的下标,上面的代码我们只写入了 2 个字节,所以 position 指向的是 2,而这个 limit 就比较有意思了,这个在后面的使用中结合例子一起讲。 get get 是从 ByteBuffer 中获取数据。 public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s','h'}); System.out.println(buffer.get()); } 如果你运行完上面的代码你会发现,打印出来的结果是 0 ,并不是我们期望的 s 的 ASCII 码 115。 首先告诉大家结论,这是符合预期的,这个时候就不应该能获取到值。我们来看看 get 的源码: public byte get() { return hb[ix(nextGetIndex())]; } protected int ix(int i) { return i + offset; } final int nextGetIndex() { int p = position; if (p >= limit) throw new BufferUnderflowException(); // 这里 position 会往后移动一位 position = p + 1; return p; } 当前 position 是 2,而 limit 是 16,所以最终 nextGetIndex 计算出来的值就是变量 p 的值 2 ,再过一次 ix ,那就是 2 + 0 = 2,这里的 offset 的值默认为 0 。 所以简单来说,最终会取到下标为 2 的数据,也就是下图这样。 所以我们当然获取不到数据。但是这里需要关注的是,调用 get 方法虽然没有获取到任何数据,但是会使得 position 指针往后移动。换句话说,会占用一个位置。如果连续调用几次这种 get 之后,再调用 put 方法写入数据,就会造成有几个位置没有赋值。举个例子,假设我们运行以下代码: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s','h'}); buffer.get(); buffer.get(); buffer.get(); buffer.get(); buffer.put(new byte[]{'e'}); } 数据就会变成下图这样,position 会往后移动 那你可能会问,那我真的需要获取数据咋办?在这种情况下,可以像这样获取: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s'}); System.out.println(buffer.get(0)); // 115 } 传入我们想要获取的下标,就可以直接获取到,并且不会造成 position 的后移。 看到这那你更懵逼了,合着 get() 就没法用呗?还必须要给个 index。这就需要聊一下另一个方法 flip了。 flip 废话不多说,先看看例子: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s', 'h'}); // java.nio.HeapByteBuffer[pos=2 lim=16 cap=16] buffer.flip(); System.out.println(buffer); // java.nio.HeapByteBuffer[pos=0 lim=2 cap=16] } 有意思的事情发生了,调用了 flip 之后,position 从 2 变成了 0,limit 从 16 变成了 2。 这个单词是「翻动」的意思,我个人的理解是像翻东西一样把之前存的东西全部翻一遍 你会发现,position 变成了 0,而 limit 变成 2,这个范围刚好是有值的区间。 接下来就更有意思了: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s', 'h'}); buffer.flip(); System.out.println((char)buffer.get()); // s System.out.println((char)buffer.get()); // h } 调用了 flip 之后,之前没法用的 get() 居然能用了。结合 get 中给的源码不难分析出来,由于 position 变成了 0,最终计算出来的结果就是 0,同时使 position 向后移动一位。 终于到这了,你可以理解成 Buffer 有两种状态,分别是: 读模式 写模式 刚刚创建出来的 ByteBuffer 就处于一个写模式的状态,通过调用 flip 我们可以将 ByteBuffer 切换成读模式。但需要注意,这里讲的读、写模式只是一个逻辑上的概念。 举个例子,当调用 flip 切换到所谓的写模式之后,依然能够调用 put 方法向 ByteBuffer 中写入数据。 public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s', 'h'}); buffer.flip(); buffer.put(new byte[]{'e'}); } 这里的 put 操作依然能成功,但你会发现最后写入的 e 覆盖了之前的数据,现在 ByteBuffer 的值变成了 eh 而不是 sh 了。 所以你现在应该能够明白,读模式、写模式更多的含义应该是: 方便你读的模式 方便你写的模式 顺带一提,调用 flip 进入写读模式之后,后续如果调用 get() 导致 position 大于等于了 limit 的值,程序会抛出 BufferUnderflowException 异常。这点从之前 get 的源码也可以看出来。 rewind rewind 你也可以理解成是运行在读模式下的命令,给大家看个例子: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'s', 'h'}); buffer.flip(); System.out.println((char)buffer.get()); // s System.out.println((char)buffer.get()); // h // 从头开始读 buffer.rewind(); System.out.println((char)buffer.get()); // s System.out.println((char)buffer.get()); // h } 所谓的从头开始读就是把 position 给归位到下标为 0 的位置,其源码也很简单: public final Buffer rewind() { position = 0; mark = -1; return this; } 就是简单的把 position 赋值为 0,把 mark 赋值为 -1。那这个 mark 又是啥东西?这就是我们下一个要聊的方法。 mark & reset mark 用于标记当前 postion 的位置,而 reset 之所以要放到一起讲是因为 reset 是 reset 到 mark 的位置,直接看例子: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'a', 'b', 'c', 'd'}); // 切换到读模式 buffer.flip(); System.out.println((char) buffer.get()); // a System.out.println((char) buffer.get()); // b // 控记住当前的 position buffer.mark(); System.out.println((char) buffer.get()); // c System.out.println((char) buffer.get()); // d // 将 position reset 到 mark 的位置 buffer.reset(); System.out.println((char) buffer.get()); // c System.out.println((char) buffer.get()); // d } 可以看到的是 ,我们在 position 等于 2 的时候,调用了 mark 记住了 position 的位置。然后遍历完了所有的数据。然后调用 reset 使得 position 回到了 2 的位置,我们继续调用 get ,c d 就又可以被打印出来了。 clear clear 表面意思看起来是将 buffer 清空的意思,但其实不是,看这个: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'a', 'b', 'c', 'd'}); } put 完之后,buffer 的情况是这样的。 当我们调用完 clear 之后,buffer 就会变成这样。 所以,你可以理解为,调用 clear 之后只是切换到了写模式,因为这个时候往里面写数据,会覆盖之前写的数据,相当于起到了 clear 作用,再举个例子: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put(new byte[]{'a', 'b', 'c', 'd'}); buffer.clear(); buffer.put(new byte[]{'s','h'}); } 可以看到,运行完之后 buffer 的数据变成了 shcd,后写入的数据将之前的数据给覆盖掉了。 除了 clear 可以切换到写模式之外,还有另一个方法可以切换,这就是本篇要讲的最后一个方法 compact。 compact 先一句话给出 compact 的作用:将还没有读完的数据挪到 Buffer 的首部,并切换到写模式,代码如下: public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(16); buffer.put("abcd".getBytes(StandardCharsets.UTF_8)); // 切换到读模式 buffer.flip(); System.out.println((char) buffer.get()); // a // 将没读过的数据, 移到 buffer 的首部 buffer.compact(); // 此时 buffer 的数据就会变成 bcdd } 当运行完 flip 之后,buffer 的状态应该没什么问题了: 而 compact 之后发生了什么呢?简单来说就两件事: 将 position 移动至对应的位置 将没有读过的数据移动到 buffer 的首部 这个对应是啥呢?先给大家举例子;例如没有读的数据是 bcd,那么 position 就为 3;如果没有读的数据为 cd,position 就为 2。所以你发现了,position 的值为没有读过的数据的长度。 从 buffer 内部实现机制来看,凡是在 position - limit 这个区间内的,都算没有读过的数据 所以,当运行完 compact 之后,buffer 长这样: limit 为 16 是因为 compact 使 buffer 进入了所谓的写模式。 EOF 还有一些其他的方法就不在这里列举了,大家感兴趣可以自己去玩玩,都没什么理解上的难度了。之后可能会再专门写一写 Channel 和 Selector,毕竟 Java 的 nio 三剑客,感兴趣的可以关注一下。 欢迎微信搜索关注【SH的全栈笔记】,如果你觉得这篇文章对你有帮助,还麻烦点个赞,关个注,分个享,留个言。

优秀的个人博客,低调大师

玩转CocoaPods

作者:阿里-移动云-大前端 CocoaPods作为iOS的依赖管理工具,已然成为iOS开发的标准工具(官方给出的数据,超过42W个库和300W个App使用了CocoaPods)。本篇文章,非讲述CocoaPods的教学文章,而是围绕使用CocoaPods的两个主题:依赖管理和Pod库发布,讲述些易忽略、混淆的关键点和不为熟知的用法。 执行pod env,可查看本地环境: CocoaPods : 1.2.0 Ruby : ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15] RubyGems : 2.5.1 Host : Mac OS X 10.13.2 (17C88) Xcode : 9.2 (9C40b) Git : git ve

优秀的个人博客,低调大师

玩转 Databend UDF

引言 Databend 作为新一代云原生数据仓库,提供了六百多个内置函数,满足了大部分用户的需求。然而,随着业务的增长,需求也变的日新月异,内置的函数可能无法服务用户变化的需求。在这种场景下, Databend 提供了多种用户自定义函数(UDF)实现方式,满足不同场景下的数据处理需求。 本文将深入探讨三种 UDF 形态:Lambda UDF、UDF Script 和 External UDF Server,并通过具体的案例展示它们的实现方式,最后进行性能对比分析。 Lambda UDF:纯 SQL 定义的函数语法糖 Lambda UDF 是 Databend 中最简单的 UDF 形式,完全通过 SQL 语句定义和执行表达式,适合简单的数据转换和计算。 我们可以在 SQL 中定义一个闭包函数,然后进行调用。 Lambda UDF 示例: 🐳 root@default:) CREATE FUNCTION plus_3 AS (a,b,c) -&gt; a + b + c; 🐳 root@default:) select plus_3(1,2,3); ╭─────────────────╮ │ plus_3(1, 2, 3) │ │ UInt8 │ ├─────────────────┤ │ 6 │ ╰─────────────────╯ 🐳 root@default:) CREATE FUNCTION age AS (d) -&gt; date_diff(year, d, now()); 🐳 root@default:) select age('1992-01-01'::Date); ╭─────────────────────────╮ │ age('1992-01-01'::DATE) │ │ Int64 │ ├─────────────────────────┤ │ 33 │ ╰─────────────────────────╯ 它的特点: 纯 SQL 实现,无需外部语言支持 无法支持递归调用 执行性能受表达式定义影响 UDF Script:多语言扩展能力 Databend 引擎中支持内嵌的多语言执行器, 可以执行通过 Python、JavaScript 和 WASM 编写 UDF Script,适合复杂业务逻辑实现。 Python UDF Databend 使用了 pyo3 引擎来执行 Python script, 我们可以通过如下方式定义一个简单的 Python UDF。 CREATE FUNCTION fib_python ( Int32 ) RETURNS Int32 LANGUAGE python HANDLER = 'fib' AS $$ def fib(n): a, b = 0, 1 for _ in range(n): a, b = b, a + b return a $$; select plus_3(1,2,3); ╭─────────────────╮ │ plus_3(1, 2, 3) │ │ UInt8 │ ├─────────────────┤ │ 6 │ ╰─────────────────╯ Javascript UDF Databend 使用了 rquickjs 引擎来执行 Javascript, 我们可以通过如下方式定义一个简单的 Javascript UDF。 CREATE OR REPLACE FUNCTION fib_js ( Int32 ) RETURNS Int32 LANGUAGE JAVASCRIPT HANDLER = 'fib' AS $$ export function fib(n) { let a = 0, b = 1; for (let i = 0; i &lt; n; i++) { [a, b] = [b, a + b]; } return a } $$; 🐳 root@default:) select fib_js(10); ╭─────────────────╮ │ fib_js(10) │ │ Nullable(Int32) │ ├─────────────────┤ │ 55 │ ╰─────────────────╯ 特点: 支持完整语言特性,实现复杂逻辑 Python UDF 特别适合数据科学和AI集成 JavaScript UDF 适合轻量级数据处理,同时兼顾沙箱安全性 UDF script 属于 Databend 企业特性 WASM UDF 你可以使用 rust 代码来实现 UDF, 然后编译到 Wasm target 中,示例教程。 创建一个项目,Cargo.toml 包含 arrow-udf 依赖 [package] name = "arrow-udf-example" version = "0.1.0" [lib] crate-type = ["cdylib"] [dependencies] arrow-udf = "0.8" 使用 #[function] 宏来定义你的函数 use arrow_udf::function; #[function("fib(int) -&gt; int")] fn fib(n: i32) -&gt; i32 { let (mut a, mut b) = (0, 1); for _ in 0..n { let c = a + b; a = b; b = c; } a } 然后编译到 wasm32-wasip1 cargo build --release --target wasm32-wasip1 将生成的 result.wasm 文件传到 databend stage 中,并定义 wasm udf 函数 🐳 root@default:) create stage s_udf; 🐳 root@default:) put fs:///tmp/arrow_udf_example.wasm @s_udf/; ╭─────────────────────────────────────────────────╮ │ file │ status │ size │ │ String │ String │ UInt64 │ ├─────────────────────────────┼─────────┼─────────┤ │ /tmp/arrow_udf_example.wasm │ SUCCESS │ 1279392 │ ╰─────────────────────────────────────────────────╯ 🐳 root@default:) CREATE OR REPLACE FUNCTION fib_wasm (INT) RETURNS INT LANGUAGE wasm HANDLER = 'fib' AS $$@s_udf/arrow_udf_example.wasm$$; 🐳 root@default:) select fib_wasm(10::Int32); ╭─────────────────────╮ │ fib_wasm(10::Int32) │ │ Nullable(Int32) │ ├─────────────────────┤ │ 55 │ ╰─────────────────────╯ External UDF Server:灵活解耦的外部 UDF 服务 通过 Arrow Flight 协议与外部 UDF Server 通信,适合将已有的服务和 Databend 互联互通。 Example: 启动 UDF Server(Python 示例): from databend_udf import udf, UDFServer import logging logging.basicConfig(level=logging.INFO) @udf( input_types=["INT"], result_type="INT", skip_null=True, ) def fib(n: int) -&gt; int: a, b = 0, 1 for _ in range(n): a, b = b, a + b return a if __name__ == "__main__": udf_server = UDFServer( location="0.0.0.0:8815" ) udf_server.add_function(fib) udf_server.serve() 在 Databend 中注册外部函数: 🐳 root@default:) CREATE OR REPLACE FUNCTION fib_server (INT) RETURNS INT LANGUAGE python HANDLER = 'fib' ADDRESS = 'http://0.0.0.0:8815'; 调用示例: 🐳 root@default:) select fib_server(10); ╭─────────────────╮ │ fib_server(10) │ │ Nullable(Int32) │ ├─────────────────┤ │ 55 │ ╰─────────────────╯ 特点: 需要可靠的网络交互 支持灵活的参数配置:支持批量处理(默认 65536 行/批) 可横向扩展 UDF 服务节点来提高性能 适合与现有微服务架构集成, 与 Databend 服务解耦, 可以与任何支持 Arrow Flight 协议的语言交互 性能对比分析 在单机内存环境下(Databend v1.3.0,16GB RAM),计算 fib(x) 的性能对比: 性能测试环境: Intel(R) Core(TM) i9-12900KF 24C archlinux SQL: select fib((n % 10) ::Int32) from range(1, 1000000) t(n) ignore_result; UDF类型 平均每行数据执行耗时(us) 适用场景 Lambda UDF - 简单转换、快速原型 Python UDF 0.18 复杂逻辑、AI 集成 JavaScript UDF 2.68 轻量级数据处理 WASM UDF 0.11 高性能处理 External UDF 23.2 大规模数据处理 *注:External UDF 耗时包含网络通信,实际处理时间会更短。 性能优化建议: 简单逻辑优先使用 Lambda UDF 复杂计算考虑 Python/JavaScript UDF 高并发场景使用 External UDF 并部署多个服务节点 UDF 选型指南 根据您的业务需求选择合适的 UDF 类型: 对比维度 Lambda UDF UDF Script External UDF Server ​​开发效率​​ ⭐⭐⭐⭐ (纯SQL实现,无需编译) ⭐⭐⭐ (需编写脚本) ⭐ (需独立服务部署) ​​执行性能​​ ⭐⭐⭐⭐ (原生性能) ⭐⭐⭐ (Python/JS运行时开销, wasm性能最佳) ⭐⭐ (支持批量处理,动态扩容,网络开销) ​​复杂逻辑​​ ⭐ (仅限简单表达式) ⭐⭐⭐ (支持完整编程语言) ⭐⭐ (需服务化拆分逻辑) ​​系统集成​​ ⭐ (仅限数据库内部) ⭐⭐ (需适配语言运行时) ⭐⭐⭐⭐ (Arrow Flight协议集成) 实现​​ (n)->CASE WHEN n<=1 THEN n ELSE ... Python/JS实现完整算法逻辑 通过Arrow Flight RPC服务暴露计算接口 ​​适用场景​​ 快速原型/简单转换 AI集成/复杂数据处理 高并发/分布 结语:扩展您的数据能力 Databend 的多形态 UDF 支持为数据处理提供了极大的灵活性。无论您需要快速实现简单转换,还是集成复杂业务逻辑,或是构建分布式计算管道,都能找到合适的解决方案。 参考文档: 查看 UDF 开发文档:UDF指南 参与社区讨论:Github 关于 Databend Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式湖仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。 👨‍💻‍ Databend Cloud:databend.cn 📖 Databend 文档:docs.databend.cn 💻 Wechat:Databend ✨ GitHub:github.com/databendlab...

优秀的个人博客,低调大师

玩转 CSS 变量

如果当年的 CSS 预处理器变量对于初入前端的我来说是开启了新世界的大门,那么 CSS 变量对于我来说无疑就是晴天霹雳。其功能不但可以优雅的处理之前 js 不好处理或不适合的业务需求。更在创造力无穷的前端开发者手中大放异彩。 基础用法 在前端的领域中,标准的实现总是比社区的约定要慢的多,前端框架最喜欢的 $ 被 Sass 变量用掉了。而最常用的 @ 也被 Less 用掉了。官方为了让 CSS 变量也能够在 Sass 及 Less 中使用,无奈只能妥协的使用 --。 <style> /* 在 body 选择器中声明了两个变量 */ body { --primary-color: red; /* 变量名大小写敏感,--primary-color 和 --PRIMARY-COLOR 是两个不同变量 */ --PRIMARY-COLOR: initial; } /** 同一个 CSS 变量,可以在多个选择器内声明。优先级高的会替换优先级低的 */ main { --primary-color: blue; } /** 使用 CSS 变量 */ .text-primary { /* var() 函数用于读取变量。 */ color: var(--primary-color) } <style> <!-- 呈现红色字体,body 选择器的颜色 --> <div class="text-primary">red</div> <!-- 呈现蓝色字体,main 选择器定义的颜色 --> <main class="text-primary">blue</main> <!-- 呈现紫色字体,当前内联样式表的定义 --> <div style='--primary-color: purple" class="text-primary">purple</main> 这里我们可以看到针对同一个 CSS 变量,可以在多个选择器内声明。读取的时候,优先级最高的声明生效。这与 CSS 的"层叠"(cascade)规则是一致的。 由于这个原因,全局的变量通常放在根元素:root里面,确保任何选择器都可以读取它们。 :root { --primary-color: #06c; } 同时, CSS 变量提供了 JavaScript 与 CSS 通信的方法。就是利用 js 操作 css 变量。我们可以使用: <style> /* ...和上面 CSS 一致 */ </style> <!-- 呈现黄色字体 --> <div class="text-primary">red</div> <!-- 呈现蓝色字体,main 选择器定义的颜色 --> <main id='primary' class="text-primary">blue</main> <!-- 呈现紫色字体,当前内联样式表的定义 --> <div id="secondary" style='--primary-color: purple" class="text-primary">purple</main> <script> // 设置变量 document.body.style.setProperty('--primary-color', 'yellow'); // 设置变量,js DOM 元素 ID 就是全局变量,所以直接设置 main 即可 // 变为 红色 primary.style.setProperty('--primary-color', 'red'); // 变为 黄色,因为当前样式被移除了,使用 body 上面样式 secondary.style.removeProperty('--primary-color'); // 通过动态计算获取变量值 getComputedStyle(document.body).getPropertyValue('--primary-color') </script> 我们可以在业务项目中定义以及替换 CSS 变量,大家可以参考 mvp.css。该库大量使用了 CSS 变量并且让你去根据自己需求修改它。 :root { --border-radius: 5px; --box-shadow: 2px 2px 10px; --color: #118bee; --color-accent: #118bee0b; --color-bg: #fff; --color-bg-secondary: #e9e9e9; --color-secondary: #920de9; --color-secondary-accent: #920de90b; --color-shadow: #f4f4f4; --color-text: #000; --color-text-secondary: #999; --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; --hover-brightness: 1.2; --justify-important: center; --justify-normal: left; --line-height: 150%; --width-card: 285px; --width-card-medium: 460px; --width-card-wide: 800px; --width-content: 1080px; } 我们可以看到基于 CSS 变量,可以更友好的和设计师的设计意图结合在一起。也易于修改,在业务项目中合理使用无疑可以事半功倍。 实现默认配置 如果让我来思考,我肯定无法想象出结合 CSS 预处理器 + CSS 变量便可以实现组件样式的默认配置。这里我先介绍两个关于该功能的前置知识点: 事实上,CSS 变量的 var() 函数还可以使用第二个参数,表示变量的默认值。如果该变量此前没有定义或者是无效值,就会使用这个默认值。 /* 没有设置过 --primary-color,颜色默认使用 #7F583F */ color: var(--primary-color, #7F583F); 虽然目前 CSS 变量不是新的属性,但终究不是所有的浏览器都支持 CSS 变量的,这里我们还是要考虑一下优雅降级。 /* 对于不支持 CSS 变量的浏览器,可以采用下面的写法。*/ a { /* 颜色默认值 */ color: #7F583F; /* 不支持则不识别该条规则 */ color: var(--primary); } 结合 CSS 处理器 + CSS 变量便可以实现组件样式的默认配置。这里参考了有赞的 Vant Weapp 的做法。有赞代码 theme.less 如下所示: // 先导入所有 Less 变量 @import (reference) './var.less'; // 利用正则去替换 Less 变量 为 CSS 变量 .theme(@property, @imp) { @{property}: e(replace(@imp, '@([^() ]+)', '@{$1}', 'ig')); @{property}: e(replace(@imp, '@([^() ]+)', 'var(--$1, @{$1})', 'ig')); } 函数效果如下所示: @import '../common/style/theme.less'; .van-button { // ... 其他省略 .theme(height, '@button-default-height'); .theme(line-height, '@button-line-height'); .theme(font-size, '@button-default-font-size'); } // => less 编译之后生成 .van-button{ // ... 其他省略 height:44px; height:var(--button-default-height,44px); line-height:20px; line-height:var(--button-line-height,20px); font-size:16px; font-size:var(--button-default-font-size,16px); } 我们可以看到每调用一次 Less 函数将会被编译成两个属性。第一个属性的设定对于不支持 CSS 变量的设备可以直接使用,如果当前设备支持 CSS 变量,则会使用 CSS 变量,但是由于当前 CSS 变量未定义,就会使用变量的默认值。虽然 '@button-default-height 虽然也是一个变量,但是该变量仅仅只是 less 变量,最终生成的代码中并没有 --button-default-height 这样的变量。此时我们就可以在使用样式的位置或者 :root 中添加变量 --button-default-height。 这种方式更适合组件开发,因为该方案不声明任何 css 变量,只是预留的 css 变量名称和默认属性。这样的话,无论开发者的选择器优先度有多低,代码都可以很容易的覆盖默认属性。因为我们仅仅使用 css 的默认值。 大家可能有时候会想,这样的话,我们不是有更多的代码了吗?其实未必,事实上我们可以直接直接在页面内部定义变量样式。其他组件直接通过 style 去使用页面内的变量。当然了,事实上书写的代码多少,重点在于想要控制默认样式的粒度大小。粒度越小,则需要在各个组件内部书写的变量越多,粒度大,我们也就不必考虑太多。 Space Toggle 逻辑切换 CSS 没有逻辑切换似乎是一种共识,但是我们可以利用选框(单选与多选)和 CSS 变量来实现判断逻辑。我们先来看看如何使用 CSS 变量。 <style> .red-box { --toggler: ; --red-if-toggler: var(--toggler) red; background: var(--red-if-toggler, green); /* will be red! */ } .green-box { --toggler: initial; --red-if-toggler: var(--toggler) red; background: var(--red-if-toggler, green); /* will be green! */ } </style> <!-- 宽度高度为 100px 的 红色盒子 --> <div style="height: 100px; width: 100px" class="red-box" ></div> <!-- 宽度高度为 100px 的 绿色盒子 --> <div style="height: 100px; width: 100px" class="green-box" ></div> 这里因为一个变量 --toggler 使用空格 或者 initial 而产生了不同的结果,基于这样的结果我们不难想象我们可以触发变量的修改而产生不同的结果。 他不是一个 bug,也不是一个 hack。他的原理完完全全的在 CSS Custom Properties 规范 中。 This value serializes as the empty string, but actually writing an empty value into a custom property, like --foo: ;, is a valid (empty) value, not the guaranteed-invalid value. If, for whatever reason, one wants to manually reset a variable to the guaranteed-invalid value, using the keyword initial will do this. 解释如下,事实上 -foo: ; 这个变量并不是一个无效值,它是一个空值。initial 才是 CSS 变量的无效值。其实这也可以理解,css 没有所谓的空字符串,空白也不代表着无效,只能使用特定值来表示该变量无效。这个时候,我们再回头来看原来的 CSS 代码。 .red-box { /* 当前为空值 */ --toggler: ; /* 因为 var(--toggler) 得到了空,所以得到结果 为 --red-if-toggler: red */ --red-if-toggler: var(--toggler) red; /** 变量是 red, 不会使用 green */ background: var(--red-if-toggler, green); /* will be red! */ } .green-box { /** 当前为无效值 */ --toggler: initial; /** 仍旧无效数据,因为 var 只会在参数不是 initial 时候进行替换 */ --red-if-toggler: var(--toggler) red; /** 最终无效值没用,得到绿色 */ background: var(--red-if-toggler, green); /* will be green! */ /* 根据当前的功能,我们甚至可以做到 and 和 or 的逻辑 * --tog1 --tog2 --tog3 同时为 空值时是 红色 */ --red-if-togglersalltrue: var(--tog1) var(--tog2) var(--tog3) red; /* * --tog1 --tog2 --tog3 任意为 空值时是 红色 */ --red-if-anytogglertrue: var(--tog1, var(--tog2, var(--tog3))) red; } 新式媒体查询 当我们需要开发响应式网站的时候,我们必须要使用媒体查询 @media。先看一下用传统的方式编写这个基本的响应式 CSS: .breakpoints-demo > * { width: 100%; background: red; } @media (min-width: 37.5em) and (max-width: 56.249em) { .breakpoints-demo > * { width: 49%; } } @media (min-width: 56.25em) and (max-width: 74.99em) { .breakpoints-demo > * { width: 32%; } } @media (min-width: 56.25em) { .breakpoints-demo > * { background: green; } } @media (min-width: 75em) { .breakpoints-demo > * { width: 24%; } } 同样,我们可以利用 css 变量来优化代码结构,我们可以写出这样的代码: /** 移动优先的样式规则 */ .breakpoints-demo > * { /** 小于 37.5em, 宽度 100% */ --xs-width: var(--media-xs) 100%; /** 小于 56.249em, 宽度 49% */ --sm-width: var(--media-sm) 49%; --md-width: var(--media-md) 32%; --lg-width: var(--media-gte-lg) 24%; width: var(--xs-width, var(--sm-width, var(--md-width, var(--lg-width)))); --sm-and-down-bg: var(--media-lte-sm) red; --md-and-up-bg: var(--media-gte-md) green; background: var(--sm-and-down-bg, var(--md-and-up-bg)); } 可以看出,第二种 CSS 代码非常清晰,数据和逻辑保持在一个 CSS 规则中,而不是被 @media 切割到多个区块中。这样,不但更容易编写,也更加容易开发者读。详情可以参考 css-media-vars。该代码库仅仅只有 3kb 大小,但是却是把整个编写代码的风格修改的完全不同。原理如下所示: /** * css-media-vars * BSD 2-Clause License * Copyright (c) James0x57, PropJockey, 2020 */ html { --media-print: initial; --media-screen: initial; --media-speech: initial; --media-xs: initial; --media-sm: initial; --media-md: initial; --media-lg: initial; --media-xl: initial; /* ... */ --media-pointer-fine: initial; --media-pointer-none: initial; } /* 把当前变量变为空值 */ @media print { html { --media-print: ; } } @media screen { html { --media-screen: ; } } @media speech { html { --media-speech: ; } } /* 把当前变量变为空值 */ @media (max-width: 37.499em) { html { --media-xs: ; --media-lte-sm: ; --media-lte-md: ; --media-lte-lg: ; } } 其他 继 CSS 键盘记录器 暴露了 CSS 安全性问题之后,CSS 变量又一次让我看到了玩技术是怎么样的。CSS Space Toggle 技术不但可以应用于上面的功能,甚至还可以编写 UI 库 augmented-ui 以及 扫雷 游戏。这简直让我眼界大开。在我有限的开发生涯中,很难找到类似于 css 这种设计意图和使用方式差异如此之大的技术。 CSS 是很有趣的,而 CSS 的有趣之处就在于最终呈现出来的技能强弱与你自身的思维方式,创造力是密切相关的。上文只是介绍了 CSS 变量的一些玩法,也许有更多有意思的玩法,不过这就需要大家的创造力了。 参考资料 augmented-ui css-media-vars css-sweeper 鼓励一下 如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 博客地址

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册